スマートフォンの加速度センサで「歩行」「階段昇降」「着席」などの行動を分類するモデルを開発したとします。しかし、被験者Aのスマートフォンで学習したモデルを、被験者Bのスマートフォンにそのまま適用すると、精度が大幅に低下することがあります。センサの設置位置、歩き方の癖、デバイスの違い — これらが「ドメインシフト」を引き起こし、ソースドメイン(被験者A)で学習した特徴がターゲットドメイン(被験者B)ではうまく機能しなくなるのです。
この問題に対処する手法が教師なしドメイン適応(Unsupervised Domain Adaptation, UDA)です。しかし、多変量時系列のUDAには特有の難しさがあります。既存手法の多くは時間方向の特徴整列にのみ注力し、チャネル間の周波数的な依存関係という重要な情報を見落としています。さらに、ソースとターゲットのデータだけを使い、外部の豊富な知識を活用していません。
ConFGD(Guo et al., AAAI 2025)は、この2つの盲点に正面から取り組んだフレームワークです。離散ウェーブレット変換(DWT)で時系列を周波数分解し、周波数チャネル間の相関をグラフとして発見・整列するFrequency Graph Discovery Module (FGD)、周波数文脈の対照学習を行うFCCL、そして大規模言語モデル(LLM)のテキスト埋め込みで分類特徴を誘導するLanguage-Guided Adversary Alignment (LAA)の3つを統合しています。
ConFGDの理解は、以下の応用領域で直接的に役立ちます。
- ウェアラブル行動認識: デバイスやユーザが変わっても高精度な行動分類を維持する必要がある、ヘルスケアやフィットネストラッキング
- 睡眠段階判定: 脳波(EEG)データの患者間ドメインシフトを克服し、汎用的な睡眠ステージング
- 産業IoT: 工場のセンサデータが設備の経年変化や環境変動でドメインシフトを起こす場合の予知保全
- 金融時系列: 市場レジームの変化に対してモデルの予測能力を維持する
本記事の内容
- ConFGDが解決する課題 — 時系列UDAにおける周波数情報とチャネル間依存性の活用
- 離散ウェーブレット変換(DWT)による周波数分解の仕組み
- Frequency Graph Discovery Module(FGD)の数理
- 周波数損失($L_{FF}$ + $L_{FC}$)の定式化
- MoCoスタイルの周波数対照学習(FCCL)
- GPT-3を用いた言語誘導ドメイン整列(LAA)
- PyTorchによる簡易版ConFGDの実装
- 実験結果の解説とアブレーション分析
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。


ConFGDの全体像 — 周波数グラフで「見えない構造」を捉える
ConFGDが取り組む課題を一言で述べると、多変量時系列のドメインシフトを、周波数ドメインのグラフ構造を通じて解消するということです。
従来の時系列UDAは、時間方向の特徴(波形のパターンやトレンド)を整列することに集中していました。しかし、これは「楽譜の音符の並び」だけを比較しているようなものです。音楽の本質を捉えるには、「どの音程がどの強さで鳴っているか」という周波数成分と、「メロディとベースラインがどう絡み合っているか」というチャネル間の関係性を見る必要があります。
ConFGDの設計思想は、3つの核心的な仮説に基づいています。
- 周波数の共有性仮説: ソースとターゲットが同じラベル空間を共有するなら、周波数チャネル間の相関構造も類似するはず
- 周波数文脈の識別性仮説: 周波数分解された表現で対照学習を行えば、ドメインに依存しない識別的特徴が得られるはず
- 言語知識の補完性仮説: LLMが持つラベルの意味的知識は、ソース・ターゲットのデータだけでは得られない「外部アンカー」として機能するはず
これらの仮説を実現するために、ConFGDは以下の3つのモジュールを統合しています。
- Frequency Graph Discovery Module (FGD): DWTで時系列を周波数分解し、チャネル間の相関をグラフとして構築・整列する
- Frequency-Contextual Contrastive Learning (FCCL): MoCoスタイルの対照学習で、周波数文脈の表現を洗練する
- Language-Guided Adversary Alignment (LAA): GPT-3のテキスト埋め込みをアンカーとして、分類特徴のドメイン整列を誘導する
まずは、既存手法の限界を具体的に見ていくことで、ConFGDの設計が必然的であることを理解しましょう。
既存手法の課題 — なぜ時間ドメインだけでは不十分なのか
多変量時系列のUDAは、コンピュータビジョンのUDA(DANN, Deep CORALなど)の成功を受けて発展してきました。しかし、時系列固有の課題がいくつかあります。
時間ドメインのみの特徴整列
VRADA(Purushotham et al., 2022)やCoDATS(Wilson et al., 2020)などの手法は、RNNやCNNで時間的な特徴を抽出し、敵対的学習でドメインを整列します。しかし、時系列のドメインシフトは時間方向だけでなく周波数方向にも現れます。例えば、あるユーザの「歩行」の加速度データは周波数2Hzが支配的かもしれませんが、別のユーザでは1.5Hzかもしれません。時間ドメインだけでは、このような周波数的なシフトを明示的に捉えることが困難です。
チャネル間依存性の無視
既存手法の多くは、各チャネル(センサ軸)を独立に、あるいは単純に連結して処理します。しかし、「x軸加速度が増加すると同時にy軸加速度が減少する」というチャネル間の相関パターンこそが、行動を特徴づける重要な手がかりです。この相関構造がドメイン間で共有されていれば、それを明示的に捉えて整列すべきです。
ソース・ターゲットデータのみの利用
CLUDAのような対照学習ベースの手法は、ソースとターゲットのデータから表現を学びますが、外部知識を活用していません。ラベルの意味(例えば「歩行」と「ジョギング」の意味的な近さ)は、大規模言語モデルが持つ豊富な知識で補完できるはずです。
ConFGDは、これら3つの課題すべてに対処するために設計されています。まずは最も基礎的なコンポーネントである、DWTによる周波数分解から見ていきましょう。
離散ウェーブレット変換(DWT)による周波数分解
なぜフーリエ変換ではなくウェーブレット変換か
時系列を周波数分解する手法として最も有名なのはフーリエ変換ですが、ConFGDはあえて離散ウェーブレット変換(DWT)を採用しています。その理由は、時系列データの特性にあります。
フーリエ変換は「全体の周波数構成」を捉えるのは得意ですが、「いつ周波数が変化したか」という時間局所的な情報を失います。一方、ウェーブレット変換はスケーリングウィンドウを使うため、低周波成分では粗い時間解像度で長期トレンドを、高周波成分では細かい時間解像度で急激な変化を同時に捉えられます。
行動認識の文脈で考えると、「歩行のゆっくりした体の揺れ」(低周波)と「着地時の衝撃」(高周波)は異なる時間スケールの情報であり、ウェーブレット変換はこの両方を自然に分離できます。
DWT分解の数式
入力の多変量時系列を $\bm{X} \in \mathbb{R}^{D \times T}$ とします($D$ はセンサチャネル数、$T$ は時間ステップ数)。DWTは各チャネルに対して特徴量方向(feature-wise)に適用されます。
$$ \tilde{\bm{X}}_{\text{coeff}} = DWT(\bm{X}) $$
$\tilde{\bm{X}}_{\text{coeff}}$ は $S$ 組のウェーブレット係数に分解されます。各レベル $s$ の係数に対して、低周波パラメータ $\lambda_s^{lf}$ と高周波パラメータ $\lambda_s^{hf}$ を掛けて分離します。
$$ \tilde{\bm{X}}_{\text{coeff}}^{lf}[s] = \lambda_s^{lf} \tilde{\bm{X}}_{\text{coeff}}[s], \quad \tilde{\bm{X}}_{\text{coeff}}^{hf}[s] = \lambda_s^{hf} \tilde{\bm{X}}_{\text{coeff}}[s] $$
$s$ が小さいとき $\lambda_s^{lf}$ は小さく $\lambda_s^{hf}$ は大きい(高周波成分が支配的)、$s$ が大きいとき逆になります。この設計により、各レベルが明確に低周波か高周波のどちらかに特化した表現を持ちます。
フィルタリングされた係数を逆DWT(IDWT)で時間ドメインに戻します。
$$ \overline{\bm{X}} = \text{Concat}(\{IDWT(\tilde{\bm{X}}_{\text{coeff}}^{hf}[s])\}_{s=1}^{S}), \quad \underline{\bm{X}} = \text{Concat}(\{IDWT(\tilde{\bm{X}}_{\text{coeff}}^{lf}[s])\}_{s=1}^{S}) $$
高周波と低周波の両方を連結して最終的な周波数分解表現を得ます。
$$ \tilde{\bm{X}} = \text{Concat}(\overline{\bm{X}}, \underline{\bm{X}}) \in \mathbb{R}^{D \times T \times 2S} $$
ConFGDでは $S = 5$ を採用しており、各チャネルの時系列が10個(高周波5 + 低周波5)の周波数レベル信号に分解されます。
Temporal Projection Layer
各周波数チャネルは独立な temporal projection layer $T[s]$ で処理されます。これはSASA(Cai et al., 2021)に触発された設計で、周波数レベルごとに異なる時間的パターンを独立に捉えるためのものです。
$$ \bm{H} = \text{Concat}(\{T[s](\tilde{\bm{X}}[s])\}_{s=1}^{2S}), \quad \bm{H} \in \mathbb{R}^{2S \times P} $$
ここで $P$ は temporal projection の隠れ表現の次元です。TCN(Temporal Convolutional Network)が各 $T[s]$ として使用され、カーネルサイズ3、拡張率2の3層64チャネル構成です。
周波数分解と temporal projection が完了したところで、次はこの表現から周波数チャネル間の関係性をグラフとして発見する FGD モジュールの核心部分に進みましょう。
Frequency Graph Discovery Module (FGD) — グラフで周波数関係を捉える
Graph Discovery の直感
教室に10人の生徒(= 10個の周波数チャネル)がいて、各自が異なる科目(= 時間的パターン)を勉強しているとしましょう。FGD が行うのは、「生徒Aと生徒Bは実は同じプロジェクトに取り組んでいる」「生徒Cと生徒Dの間には強い協力関係がある」といった隠れた関係性(グラフ構造)を発見することです。しかも、この関係性がソースドメイン(月曜クラス)とターゲットドメイン(火曜クラス)で共通しているかどうかを検証し、共通の構造を使ってドメイン間を橋渡しします。
Graph Neural Network (GNN) エンコーダ
FGDの中核は、Interaction Network(Li et al., 2020)に基づくGNNです。$2S$ 個の周波数チャネルをノードとする有向グラフ $\mathcal{G} = \{\mathcal{V}, \mathcal{E}\}$ を考えます。各ノード $f_i$ は周波数チャネル $i$ の埋め込み、各エッジ $(f_i, f_j, f_{ij})$ はチャネル $i$ から $j$ への相互作用を表します。自己ループも含みます。
GNNのエンコーダは、エッジとノードの埋め込みを以下のように計算します。
$$ \bm{h}_{ij} = \text{Edge}(f_i, f_j, f_{ij}), \quad (f_i, f_j, f_{ij}) \in \mathcal{E} $$
$$ \bm{h}_i = \text{Node}(f_i, \sum_{j \in S_i} \bm{h}_{ij}), \quad f_i \in \mathcal{V} $$
ここで $S_i$ はノード $i$ に向かうエッジを持つ全ノードの集合です。$\text{Edge}(\cdot)$ と $\text{Node}(\cdot)$ はそれぞれ単一の線形層で実装されています。
GNNエンコーダの出力として、ノード埋め込み $\bm{v}_{\text{node}} \in \mathbb{R}^{Q \times P}$ とエッジ埋め込み $\bm{v}_{\text{edge}} \in \mathbb{R}^{Q^2 \times P}$ を得ます($Q = 2S$)。
$$ \bm{v}_{\text{node}}, \bm{v}_{\text{edge}} = G^{\text{enc}}(\bm{H}) $$
ノード埋め込み $\bm{v}_{\text{node}}^S$ は分類にも使用されます。予測ヘッド $\text{Pred}(\cdot)$ を通じて分類を行い、予測損失を計算します。
$$ \hat{y}_{\text{src}} = \text{Pred}(\bm{v}_{\text{node}}^{S_i}), \quad L_{\text{pred}} = \frac{1}{N_s} \sum_{i=1}^{N_s} L_{\text{ce}}(\hat{y}_{\text{src}}^i, y_{\text{src}}^i) $$
Top-k エッジ選択
全てのエッジ情報を使うと計算コストが高く、無関係なエッジのノイズも入ります。そこで、各ノードについて重要度の高い上位 $k$ 個のエッジのみを保持し、残りをゼロにします。
$$ \bm{v}_{\text{edge}} = \text{Top\_k}(\bm{v}_{\text{edge}}) $$
実験では $\text{Top\_k} = 5, 7, 10$ で検証され、$k = 7$ が最もバランスの良い性能を示しています。これにより、計算効率を維持しながら精度を保てます。
周波数位置エンコーディング
Transformerと同様に、グラフ内のノードとエッジに位置情報を付与します。ノード埋め込み $\bm{v}_{\text{node}}$ には1次元の位置エンコーディングが加えられます。
$$ PE_{(q, 2p)} = \sin\left(\frac{q}{10000^{2p/P}}\right), \quad PE_{(q, 2p+1)} = \cos\left(\frac{q}{10000^{2p/P}}\right) $$
エッジ埋め込み $\bm{v}_{\text{edge}}$ には2次元の位置エンコーディングが加えられます。エッジは2つのノード $(q_1, q_2)$ の関係を表すため、2次元が自然です。
$$ 2DPE_{(q_1, q_2, 2p)} = \sin\left(\frac{q_1}{10000^{2p/P}} + \frac{q_2}{10000^{2p/P}}\right) $$
$$ 2DPE_{(q_1, q_2, 2p+1)} = \cos\left(\frac{q_1}{10000^{2p/P}} + \frac{q_2}{10000^{2p/P}}\right) $$
Aggregate Module
ノードとエッジの特徴を集約するために、max poolingとaverage poolingを組み合わせたモジュールが使われます。
$$ \bm{v}’ = \bm{v} \otimes \sigma(AP(\bm{v}) + MP(\bm{v})) $$
$$ \bm{w} = \bm{v}’^T \otimes \sigma(BN(Conv(AP(\bm{v}’^T) \oplus MP(\bm{v}’^T)))) $$
ここで $\otimes$ は要素ごとの積、$\oplus$ は連結、$\sigma$ はシグモイド関数、$AP$ と $MP$ はそれぞれ平均プーリングと最大プーリングです。この2段階の集約により、周波数方向と空間方向の両方の情報が統合され、残差接続のような効果も得られます。
集約後の特徴 $\bm{w}_{\text{node}} \in \mathbb{R}^{Q \times P}$ と $\bm{w}_{\text{edge}} \in \mathbb{R}^{Q^2 \times P}$ は、Graph Discovery Decoder $G^{\text{dec}}$ に渡されます。
Graph Discovery Decoder
デコーダは、ノードとエッジの集約特徴から周波数相関グラフを構築します。
$$ \mathcal{G} \sim \{\bm{g}_{\text{node}}, \bm{g}_{\text{edge}}\} = G^{\text{dec}}(\bm{w}_{\text{node}}, \bm{w}_{\text{edge}}) $$
ここで $\bm{g}_{\text{node}} \in \mathbb{R}^{Q \times P}$ はノード相関射影、$\bm{g}_{\text{edge}} \in \mathbb{R}^{Q^2 \times 1}$ はエッジ相関射影です。$\bm{g}_{\text{node}}$ は周波数特徴の損失 $L_{FF}$ の計算に使われ、$\bm{g}_{\text{edge}}$ は周波数対照損失 $L_{FC}$ の計算に使われます。
FGDの構造が見えたところで、次はこのグラフ表現を使ってドメイン間の整列を行う損失関数を見ていきましょう。
周波数損失 — $L_{FF}$ と $L_{FC}$
Frequency Feature-wise Loss ($L_{FF}$)
$L_{FF}$ は、ソースとターゲットの周波数グラフのノード特徴の差を直接最小化する損失です。同じラベル空間を共有するなら、周波数チャネルのノード表現もドメイン間で類似するはずだという仮説に基づいています。
$$ L_{FF} = \mathbb{E}(|\bm{g}_{\text{node}}^S – \bm{g}_{\text{node}}^T|) $$
これはシンプルなL1距離(平均絶対誤差)です。ソースの周波数ノード表現 $\bm{g}_{\text{node}}^S$ とターゲットの $\bm{g}_{\text{node}}^T$ が近づくように学習が進みます。
Frequency Contrastive Loss ($L_{FC}$)
$L_{FC}$ は、周波数グラフのエッジ相関に対するInfoNCEスタイルの対照損失です。基本的な考え方は、「ソースとターゲットで同じ周波数ペア(例: 高周波レベル3同士)のエッジ相関は類似するべきだが、異なる周波数ペア間のエッジ相関は異なるべき」というものです。
$$ L_{FC} = -\frac{1}{Q} \sum_{i=1}^{Q} \log \frac{\exp(\bm{g}_{\text{edge}}^{S_i} (\bm{g}_{\text{edge}}^{T_i})^T)}{\sum_{j=1, j \neq i}^{Q} \exp(\bm{g}_{\text{edge}}^{S_i} (\bm{g}_{\text{edge}}^{T_j})^T)} $$
ここで $i$ と $j$ はエッジ相関ベクトルの周波数レベルインデックスです。分子はソースとターゲットの同一周波数レベルのエッジ相関の類似度(正例)、分母はソースの周波数レベル $i$ と ターゲットの異なる周波数レベル $j$ のエッジ相関の類似度の和(負例)です。
$L_{FF}$ がノードレベルの整列を担い、$L_{FC}$ がエッジレベルの整列を担うことで、周波数グラフのノードと構造の両方がドメイン間で整列されます。
ドメイン判別損失 ($L_{\text{domain}}$)
FGDにはさらに、Gradient Reversal Layer(GRL)を用いた敵対的ドメイン判別も組み込まれています。
$$ L_{\text{domain}} = \frac{1}{N_s} \sum_{i=1}^{N_s} L_{\text{ce}}(D_{\text{disc}}(R(\bm{v}_{\text{node}}^{S_i})), d_{\text{src}}) + \frac{1}{N_t} \sum_{i=1}^{N_t} L_{\text{ce}}(D_{\text{disc}}(R(\bm{v}_{\text{node}}^{T_i})), d_{\text{trg}}) $$
$R(\cdot)$ はGRL(勾配反転層)で、フォワードパスでは恒等関数、バックワードパスで勾配の符号を反転させます($\frac{dF}{dx} = -\bm{I}$)。これにより、エンコーダ $G^{\text{enc}}$ はドメイン判別器を騙すような(ドメイン不変な)特徴を学習します。
周波数グラフの整列手法がわかったところで、次はこの周波数表現をさらに洗練する対照学習フレームワーク FCCL を見ていきましょう。
Frequency-Contextual Contrastive Learning (FCCL)
MoCoスタイルの対照学習
FCCLは、MoCo(Momentum Contrast)の設計思想を周波数文脈の対照学習に応用したフレームワークです。MoCoの鍵となるアイデアは、momentum-updated encoderを使って大きな負例キューを維持し、バッチサイズに制約されない豊富な負例を対照学習に利用することです。
ConFGDでは、各入力サンプルに対して2つのaugmented viewを生成します — query $\bm{X}^q$ と key $\bm{X}^k$ です。データ拡張には semantic-preserving augmentation 戦略(ガウスノイズ、history crop、history cutout、channel dropout)が使われます。
Momentum Update
key側のエンコーダは、query側のエンコーダのExponential Moving Average(EMA)として更新されます。
$$ \{\theta_{\tilde{T}}, \theta_{\tilde{G}^{\text{enc}}}\} \leftarrow \alpha \{\theta_{\tilde{T}}, \theta_{\tilde{G}^{\text{enc}}}\} + (1 – \alpha) \{\theta_T, \theta_{G^{\text{enc}}}\} $$
$\alpha \in [1, 0)$ はmomentum係数で、0.9〜0.99の範囲で設定されます。大きい $\alpha$ ほどkey側の更新が緩やかになり、負例の表現が急激に変化することを防ぎます。この安定性が、大きなキューを維持する上で重要です。
対照損失 $L_{CL}$
queryのノード埋め込みをデコーダに通して得た $\bm{g}_{\text{node}}^q$ と、keyのノード埋め込み $\bm{v}_{\text{node}}^k$ の間で対照学習を行います。
$$ L_{CL} = -\frac{1}{B} \sum_{i=1}^{B} \log \frac{\exp(\bm{g}_{\text{node}}^{q_i} (\bm{v}_{\text{node}}^{k_i})^T / \tau)}{\exp(\bm{g}_{\text{node}}^{q_i} (\bm{v}_{\text{node}}^{k_i})^T / \tau) + \sum_{j=1}^{J} \exp(\bm{g}_{\text{node}}^{q_i} (\bm{v}_{\text{node}}^{k_j})^T / \tau)} $$
$\tau > 0$ は温度パラメータ、$B$ はバッチサイズ、$J$ はキューサイズ($J \gg B$)です。キューには過去のバッチから集めた負例が格納されており、$J = 8192, 12288, 24576$ の範囲で設定されます。
重要な点は、この対照損失がソースドメインとターゲットドメインの両方で独立に計算されることです。
$$ L_{CL}^S: \text{ソースドメイン内の周波数対照学習} $$
$$ L_{CL}^T: \text{ターゲットドメイン内の周波数対照学習} $$
各ドメインで周波数文脈の識別的な表現を学習し、$L_{FF}$ と $L_{FC}$ がドメイン間の整列を担当するという役割分担です。
ここまでで、ConFGDの基本フレームワーク(FGD + FCCL)が完成しました。次は、外部知識を活用してさらに性能を向上させるLAAモジュールを見ていきましょう。
Language-Guided Adversary Alignment (LAA) — LLMの知識で整列を誘導する
なぜ LLM を使うのか
ConFGDの基本モデル(ConFGD)は、ソースとターゲットのデータだけを使ってドメイン整列を行います。しかし、ラベルの「意味」という観点では、データだけでは得られない情報があります。例えば、「歩行(walking)」と「ジョギング(jogging)」は動作として類似していますが、「歩行」と「着席(sitting)」は大きく異なります。この意味的な関係性は、大規模言語モデル(LLM)が膨大なテキストデータから学習した知識に豊富に含まれています。
ConFGD+は、この外部知識を「軽量に」活用するLAAモジュールを追加したバージョンです。
埋め込み辞書の事前生成
LAAの最大の工夫は、LLMを学習ループに組み込まない点です。LLMのパラメータ数が巨大なため、推論だけでも学習を遅くしてしまいます。そこで、学習開始前にラベルのテキスト埋め込みを一度だけ生成し、辞書として保存します。
$$ \text{Eb} = LLM(LP), \quad \text{Dict} \leftarrow \{Dataset_{\text{name}}, y_{\text{src}}^i, LP, \text{Eb}\} $$
ここで $LP$ はラベルプロンプト(ラベルのテキスト表現)です。各データセットのプロンプトは以下の通りです。
- HAR: {“biking”, “sitting”, “standing”, “walking”, “stair up”, “stair down”}
- WISDM: {“walking”, “jogging”, “stair up”, “stair down”, “sitting”, “standing”}
- HHAR: {“biking”, “sitting”, “standing”, “walking”, “walking upstairs”, “walking downstairs”}
GPT-3(text-embedding-3-small等)でこれらのプロンプトを埋め込み、辞書に格納します。学習時は辞書をルックアップするだけなので、計算コストはほぼゼロです。
Language-Guided Alignment Loss
分類特徴 $\bm{v}_{\text{node}}^{S,q}$ をフラットにして $\mathbb{R}^{1 \times QP}$ に変換し、テキスト埋め込み $\text{Eb}’$ も同じ次元に射影します。両者のベースが異なるため、学習可能な整列行列 $\bm{W}_l$(単位行列付近で初期化)を介してアライメントを行います。
$$ L_{\text{llm\_align}} = \frac{1}{N_s} \sum_{i=1}^{N_s} \left(1 – \frac{\bm{v}^{S_i} \cdot \bm{W}_l \text{Eb}’^i}{\max(\|\bm{v}^{S_i}\|_2 \cdot \|\bm{W}_l \text{Eb}’^i\|_2, \epsilon)}\right) $$
これはコサイン類似度を最大化する損失です。分類特徴 $\bm{v}^{S_i}$ がそのラベルに対応するLLM埋め込みに近づくように学習されます。$\epsilon = 10^{-8}$ はゼロ除算を防ぐ定数です。
LAAの利点は3つあります。
- ドメイン不変のアンカー: LLM埋め込みはソース・ターゲットどちらにも依存しないため、安定した整列の基準点となる
- 意味的な構造の注入: 「歩行」と「ジョギング」が近く、「着席」とは遠いという意味的関係が、分類特徴の空間にも反映される
- 計算効率: 辞書ルックアップのみで、学習時の追加コストは $\bm{W}_l$ のパラメータ更新のみ
実験では、GPT-3、BERT、LLaMA2の3つのLLMで検証され、GPT-3が若干の精度優位を示しましたが、いずれのLLMでも一貫した精度向上が確認されています。
全てのコンポーネントが揃ったところで、統合損失関数を見ていきましょう。
統合損失関数
ConFGDとConFGD+の最終的な損失関数は以下の通りです。
ConFGD(基本モデル):
$$ \min L_{\text{ConFGD}} = L_{\text{pred}} + \lambda_{\text{domain}} L_{\text{domain}} + \lambda_{\text{freq}}(L_{FF} + L_{FC}) + \lambda_{CL}(L_{CL}^S + L_{CL}^T) $$
ConFGD+(LAA付き):
$$ \min L_{\text{ConFGD+}} = L_{\text{ConFGD}} + \lambda_{\text{llm\_align}} L_{\text{llm\_align}} $$
各項の役割を整理します。
- $L_{\text{pred}}$: ソースドメインの分類損失(クロスエントロピー)— 正しい分類を学習
- $\lambda_{\text{domain}} L_{\text{domain}}$: 敵対的ドメイン判別損失 — ドメイン不変な特徴を学習
- $\lambda_{\text{freq}}(L_{FF} + L_{FC})$: 周波数グラフの整列損失 — 周波数構造のドメイン間整列
- $\lambda_{CL}(L_{CL}^S + L_{CL}^T)$: 周波数対照学習損失 — ドメイン内の識別的表現の獲得
- $\lambda_{\text{llm\_align}} L_{\text{llm\_align}}$: 言語誘導整列損失 — LLMの意味的知識による分類特徴の洗練
ハイパーパラメータのチューニング範囲は $\lambda_{\text{domain}} \in \{0.1, 0.5, 1\}$、$\lambda_{\text{freq}} \in \{0.5, 0.1, 0.2\}$、$\lambda_{CL} \in \{0.05, 0.1, 0.2\}$、$\lambda_{\text{llm\_align}} \in \{0.05, 0.1, 0.2\}$ です。
理論の全体像が把握できたので、次はPyTorchで核心部分を実装して理解を深めましょう。
Python実装: 簡易版ConFGD
ここでは、ConFGDの核心的な要素 — DWT分解、GNNエンコーダ、周波数損失、FCCL対照学習 — をPyTorchで実装します。
DWT分解モジュール
まず、離散ウェーブレット変換で時系列を周波数分解するモジュールを実装します。
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
class DWTDecomposition(nn.Module):
"""離散ウェーブレット変換による周波数分解(簡易版)"""
def __init__(self, num_levels=5):
super().__init__()
self.num_levels = num_levels
# 低周波・高周波パラメータ(学習可能)
self.lambda_lf = nn.Parameter(torch.linspace(0.2, 1.0, num_levels))
self.lambda_hf = nn.Parameter(torch.linspace(1.0, 0.2, num_levels))
def haar_dwt_1d(self, x):
"""Haarウェーブレットによる1レベルDWT"""
# x: (B, D, T)
T = x.shape[-1]
if T % 2 != 0:
x = x[..., :T - 1]
# 近似係数(低周波)と詳細係数(高周波)
approx = (x[..., 0::2] + x[..., 1::2]) / np.sqrt(2)
detail = (x[..., 0::2] - x[..., 1::2]) / np.sqrt(2)
return approx, detail
def forward(self, x):
"""
x: (B, D, T)
returns: (B, D, T, 2*num_levels) - 高周波+低周波の周波数分解
"""
B, D, T = x.shape
hf_signals = []
lf_signals = []
current = x
for s in range(self.num_levels):
approx, detail = self.haar_dwt_1d(current)
# 周波数パラメータでスケーリング
hf = detail * self.lambda_hf[s]
lf = approx * self.lambda_lf[s]
# 元の長さにアップサンプリング(簡易版)
hf_up = F.interpolate(hf, size=T, mode='linear', align_corners=False)
lf_up = F.interpolate(lf, size=T, mode='linear', align_corners=False)
hf_signals.append(hf_up)
lf_signals.append(lf_up)
current = approx
# (B, D, T, num_levels) for each
hf_stack = torch.stack(hf_signals, dim=-1)
lf_stack = torch.stack(lf_signals, dim=-1)
# 連結: (B, D, T, 2*num_levels)
freq_decomp = torch.cat([hf_stack, lf_stack], dim=-1)
return freq_decomp
このモジュールは、Haarウェーブレットを使った簡易版のDWT分解です。各レベルで近似係数(低周波)と詳細係数(高周波)を計算し、学習可能な $\lambda$ パラメータでスケーリングします。実際の論文ではPyWTライブラリが使われていますが、ここではアルゴリズムの理解を優先して手動実装しています。
Frequency Graph Discovery エンコーダ
周波数分解された表現から、チャネル間の関係をグラフとして発見するGNNを実装します。
class FrequencyGraphEncoder(nn.Module):
"""Interaction Network ベースの Graph Discovery Encoder"""
def __init__(self, num_freq_channels, hidden_dim):
super().__init__()
self.Q = num_freq_channels # 2S
self.hidden_dim = hidden_dim
# Temporal Projection (各周波数チャネル独立)
self.temporal_projs = nn.ModuleList([
nn.Sequential(
nn.Linear(128, hidden_dim), # T=128想定
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
)
for _ in range(num_freq_channels)
])
# Edge encoder: (f_i, f_j, f_ij) -> h_ij
self.edge_encoder = nn.Linear(hidden_dim * 3, hidden_dim)
# Node encoder: (f_i, sum_h_ij) -> h_i
self.node_encoder = nn.Linear(hidden_dim * 2, hidden_dim)
def forward(self, freq_decomp):
"""
freq_decomp: (B, D, T, 2S) - DWT分解された表現
returns: v_node (B, Q, P), v_edge (B, Q*Q, P)
"""
B, D, T, S2 = freq_decomp.shape
# 各周波数チャネルのtemporal projection
# (B, D, T, 2S) -> 各チャネルを独立に処理
channel_embs = []
for s in range(self.Q):
# (B, D, T) -> (B*D, T) -> projection -> (B, P)
ch_input = freq_decomp[:, :, :, s].reshape(B * D, T)
ch_emb = self.temporal_projs[s](ch_input)
ch_emb = ch_emb.reshape(B, D, -1).mean(dim=1) # Dチャネルを平均
channel_embs.append(ch_emb)
# H: (B, Q, P)
H = torch.stack(channel_embs, dim=1)
# GNN: エッジとノードの埋め込みを計算
# 全ペアのエッジを計算
v_edges = []
for i in range(self.Q):
for j in range(self.Q):
f_i = H[:, i, :] # (B, P)
f_j = H[:, j, :]
f_ij = f_i * f_j # 簡易的な相互作用特徴
edge_input = torch.cat([f_i, f_j, f_ij], dim=-1)
h_ij = self.edge_encoder(edge_input)
v_edges.append(h_ij)
v_edge = torch.stack(v_edges, dim=1) # (B, Q*Q, P)
# ノード埋め込み
v_nodes = []
for i in range(self.Q):
f_i = H[:, i, :]
# ノードiに向かう全エッジの和
edge_sum = torch.zeros_like(f_i)
for j in range(self.Q):
edge_sum = edge_sum + v_edge[:, j * self.Q + i, :]
node_input = torch.cat([f_i, edge_sum], dim=-1)
h_i = self.node_encoder(node_input)
v_nodes.append(h_i)
v_node = torch.stack(v_nodes, dim=1) # (B, Q, P)
return v_node, v_edge
GNNエンコーダは、各周波数チャネルをノードとし、全ペアのエッジ関係を計算します。実際の論文のInteraction Networkと同じ構造で、Edge関数とNode関数が単一線形層で実装されています。
周波数損失の実装
$L_{FF}$(ノード特徴整列)と $L_{FC}$(エッジ対照損失)を実装します。
class FrequencyLoss(nn.Module):
"""周波数損失: L_FF (feature-wise) + L_FC (contrastive)"""
def __init__(self, hidden_dim, num_freq_channels):
super().__init__()
self.Q = num_freq_channels
# Graph Discovery Decoder (簡易版)
self.node_decoder = nn.Linear(hidden_dim, hidden_dim)
self.edge_decoder = nn.Linear(hidden_dim, 1)
def forward(self, v_node_src, v_node_trg, v_edge_src, v_edge_trg):
"""
v_node_src/trg: (B, Q, P)
v_edge_src/trg: (B, Q*Q, P)
"""
# デコーダでグラフ相関射影を計算
g_node_src = self.node_decoder(v_node_src)
g_node_trg = self.node_decoder(v_node_trg)
g_edge_src = self.edge_decoder(v_edge_src).squeeze(-1) # (B, Q*Q)
g_edge_trg = self.edge_decoder(v_edge_trg).squeeze(-1)
# L_FF: ノード特徴の L1 距離
L_FF = torch.mean(torch.abs(g_node_src - g_node_trg))
# L_FC: エッジ相関の対照損失
# (B, Q*Q) -> (B, Q, Q) に reshape してチャネルペアごとに
g_e_src = g_edge_src.reshape(-1, self.Q, self.Q)
g_e_trg = g_edge_trg.reshape(-1, self.Q, self.Q)
# 各周波数レベルiに対するInfoNCE
L_FC = 0.0
B = g_e_src.shape[0]
for i in range(self.Q):
# 正例: ソースのi行目 × ターゲットのi行目
pos = torch.sum(g_e_src[:, i, :] * g_e_trg[:, i, :], dim=-1)
# 負例: ソースのi行目 × ターゲットのj行目 (j≠i)
neg_sum = torch.zeros(B, device=pos.device)
for j in range(self.Q):
if j != i:
neg = torch.sum(g_e_src[:, i, :] * g_e_trg[:, j, :], dim=-1)
neg_sum += torch.exp(neg)
L_FC += -torch.mean(torch.log(
torch.exp(pos) / (neg_sum + 1e-8)
))
L_FC /= self.Q
return L_FF, L_FC
$L_{FF}$ はノード表現のL1距離というシンプルな損失で、$L_{FC}$ はエッジ相関のInfoNCE損失です。同じ周波数レベル同士が正例、異なるレベルが負例として構成されています。
統合モデルと学習ループ
全コンポーネントを統合した簡易版ConFGDで学習を行います。
class SimpleConFGD(nn.Module):
"""簡易版ConFGDモデル"""
def __init__(self, input_channels, time_steps, num_classes,
num_levels=5, hidden_dim=64):
super().__init__()
self.num_freq = num_levels * 2
self.dwt = DWTDecomposition(num_levels)
self.graph_enc = FrequencyGraphEncoder(self.num_freq, hidden_dim)
self.freq_loss = FrequencyLoss(hidden_dim, self.num_freq)
self.classifier = nn.Linear(hidden_dim * self.num_freq, num_classes)
def encode(self, x):
freq_decomp = self.dwt(x)
v_node, v_edge = self.graph_enc(freq_decomp)
return v_node, v_edge
def predict(self, v_node):
B = v_node.shape[0]
flat = v_node.reshape(B, -1)
return self.classifier(flat)
def forward(self, x_src, x_trg, y_src):
v_node_s, v_edge_s = self.encode(x_src)
v_node_t, v_edge_t = self.encode(x_trg)
logits = self.predict(v_node_s)
L_pred = F.cross_entropy(logits, y_src)
L_FF, L_FC = self.freq_loss(
v_node_s, v_node_t, v_edge_s, v_edge_t
)
return L_pred, L_FF, L_FC, logits
# ハイパーパラメータ
input_channels = 9
time_steps = 128
num_classes = 6
hidden_dim = 32
num_levels = 3
batch_size = 16
num_epochs = 100
lr = 1e-3
lambda_freq = 0.1
model = SimpleConFGD(input_channels, time_steps, num_classes,
num_levels, hidden_dim)
optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
losses_total, losses_pred, losses_ff, losses_fc = [], [], [], []
for epoch in range(num_epochs):
model.train()
# 合成データ生成(ドメインシフトをシミュレート)
freqs_src = torch.rand(batch_size, input_channels, 1) * 3 + 1
freqs_trg = freqs_src + 0.5 * torch.randn_like(freqs_src) # シフト
t = torch.linspace(0, 2 * np.pi, time_steps).unsqueeze(0).unsqueeze(0)
x_src = torch.sin(freqs_src * t) + 0.1 * torch.randn(batch_size, input_channels, time_steps)
x_trg = torch.sin(freqs_trg * t) + 0.1 * torch.randn(batch_size, input_channels, time_steps)
y_src = (freqs_src[:, 0, 0] * 2).long() % num_classes
L_pred, L_FF, L_FC, logits = model(x_src, x_trg, y_src)
loss = L_pred + lambda_freq * (L_FF + L_FC)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses_total.append(loss.item())
losses_pred.append(L_pred.item())
losses_ff.append(L_FF.item())
losses_fc.append(L_FC.item())
if (epoch + 1) % 20 == 0:
acc = (logits.argmax(dim=1) == y_src).float().mean()
print(f"Epoch {epoch+1:3d} | Total: {loss.item():.4f} | "
f"Pred: {L_pred.item():.4f} | FF: {L_FF.item():.4f} | "
f"FC: {L_FC.item():.4f} | Acc: {acc.item():.2%}")
# 損失推移の可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(losses_total, label='Total Loss', linewidth=2)
axes[0].plot(losses_pred, label='Prediction Loss', linewidth=1.5, alpha=0.8)
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('ConFGD Training Loss')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(losses_ff, label='L_FF (Feature-wise)', linewidth=1.5)
axes[1].plot(losses_fc, label='L_FC (Contrastive)', linewidth=1.5)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].set_title('Frequency Losses')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
上のグラフから、以下の特徴が読み取れます。
- 予測損失が着実に減少していることから、周波数グラフベースの特徴がソースドメインの分類に有効であることがわかります
- $L_{FF}$(Feature-wise損失)が低下していることは、ソースとターゲットの周波数ノード表現が徐々に整列されていることを意味します
- $L_{FC}$(対照損失)の推移から、同一周波数レベルのエッジ相関がドメイン間で近づき、異なるレベル間のエッジ相関が分離されていく過程が確認できます
LAA(言語誘導整列)のデモ
LLM埋め込みを使った分類特徴の整列を簡易的に示します。
# LAA: 言語誘導整列のデモ
num_labels = 6
label_names = ["biking", "sitting", "standing", "walking", "stair_up", "stair_down"]
# 疑似LLM埋め込み(実際にはGPT-3で生成)
# 意味的に近いラベルは近い埋め込みになるよう設計
llm_dim = 64
torch.manual_seed(42)
llm_embeddings = torch.randn(num_labels, llm_dim)
# "walking"と"stair_up"/"stair_down"を近くに
llm_embeddings[3] = llm_embeddings[4] * 0.7 + 0.3 * torch.randn(llm_dim)
llm_embeddings[5] = llm_embeddings[4] * 0.6 + 0.4 * torch.randn(llm_dim)
# "biking"と"sitting"を遠くに
llm_embeddings[0] = -llm_embeddings[1] + 0.2 * torch.randn(llm_dim)
# 学習可能な整列行列
W_l = nn.Parameter(torch.eye(llm_dim) + 0.01 * torch.randn(llm_dim, llm_dim))
# コサイン類似度ベースのアライメント損失
def laa_loss(features, labels, W_l, llm_embs, eps=1e-8):
"""Language-Guided Alignment Loss"""
label_embs = llm_embs[labels] # (B, llm_dim)
aligned_embs = label_embs @ W_l # (B, llm_dim)
# コサイン類似度
cos_sim = F.cosine_similarity(features, aligned_embs, dim=-1)
loss = (1 - cos_sim).mean()
return loss
# テスト: ランダム特徴に対するLAA損失
test_features = torch.randn(8, llm_dim)
test_labels = torch.tensor([0, 1, 2, 3, 4, 5, 0, 1])
loss_val = laa_loss(test_features, test_labels, W_l, llm_embeddings)
print(f"LAA Loss (before training): {loss_val.item():.4f}")
# 意味的類似度の可視化
cos_matrix = F.cosine_similarity(
llm_embeddings.unsqueeze(0),
llm_embeddings.unsqueeze(1),
dim=-1
)
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(cos_matrix.detach().numpy(), cmap='RdBu_r', vmin=-1, vmax=1)
ax.set_xticks(range(num_labels))
ax.set_yticks(range(num_labels))
ax.set_xticklabels(label_names, rotation=45, ha='right')
ax.set_yticklabels(label_names)
ax.set_title('LLM Label Embedding Cosine Similarity')
plt.colorbar(im, ax=ax, shrink=0.8)
plt.tight_layout()
plt.show()
この類似度マトリクスから、LLM埋め込みの意味的構造が読み取れます。「walking」「stair_up」「stair_down」は運動系の行動として互いに類似度が高く、「sitting」はこれらとは異なるクラスタを形成しています。ConFGD+はこの意味的構造を分類特徴空間に注入することで、ドメインを跨いだより頑健な分類を実現しています。
実験結果の解説
Guo et al. (2025) は、3つのベンチマークデータセットで ConFGD と ConFGD+ を評価しています。
データセット
| データセット | ドメイン数 | チャネル数 | クラス数 | 系列長 | 訓練数 |
|---|---|---|---|---|---|
| HAR | 30 | 9 | 6 | 128 | 7,194 |
| WISDM | 30 | 3 | 6 | 128 | 3,870 |
| HHAR | 9 | 3 | 6 | 128 | 10,336 |
HAR(Human Activity Recognition)は9軸慣性センサ(加速度3軸 + ジャイロ3軸 + 体加速度3軸)、WISDMは3軸加速度のみ、HHARは異なるスマートフォン間のドメインシフトを対象としています。
性能比較
HARデータセットでは、ConFGDが最善のベースライン(CLUDA)を精度で+4.00%、Macro F1で+4.44%上回っています。さらにLAAを追加したConFGD+は全20ドメインペアの全てでベースラインを上回り、精度96.81%を達成しています。
WISDMは最も困難なデータセットで、チャネル数が3と少なく、正常/異常の区別が曖昧です。ここでもConFGDは精度+9.40%、F1+15.44%という大幅な改善を達成しました。周波数グラフによるチャネル間関係の活用が、チャネルの少ないデータセットでも有効であることを示しています。
HHARデータセットでも同様に、ConFGD+が精度84.11%、F1 83.94%で state-of-the-art を達成しています。
LLM比較(Table 7, WISDM)
LAA モジュールを GPT-3、BERT、LLaMA2 の3つのLLMで検証した結果、GPT-3が精度で若干優位(Avg. Acc: 83.46)、LLaMA2がF1で若干優位(Avg. F1: 77.64)を示しました。BERTは精度82.06と最も低いものの、依然としてLAAなしのConFGD(81.68)を上回っています。いずれのLLMでも一貫した精度向上が確認されており、LAA モジュールのLLM非依存な有効性が示されています。
アブレーション分析
ConFGDの各コンポーネントの貢献を検証するアブレーション実験の結果は以下の通りです(HARデータセット)。
$L_{FC}$ と $L_{FF}$ の除去: Avg. Acc が 94.86% → 91.57% に低下し、周波数グラフ整列の重要性が確認されました。
$L_{CL}$ の除去: Avg. Acc が 94.86% → 89.18% に低下し、FCCL対照学習が最も大きな貢献をしていることがわかります。
$L_{\text{domain}}$ の除去: Avg. Acc が 94.86% → 85.49% に大幅低下し、敵対的ドメイン判別が基盤的に重要であることが示されました。
PE(位置エンコーディング)の除去: Avg. Acc が 94.86% → 85.34% に低下し、周波数位置情報の重要性が確認されました。
全コンポーネントが独立に貢献しており、ConFGDの設計が各要素の相互補完的な作用を巧みに統合していることがわかります。
まとめ
本記事では、AAAI 2025で発表された ConFGD(Guo et al., 2025)について解説しました。ConFGDは、多変量時系列の教師なしドメイン適応を、周波数グラフの発見とLLMの言語知識で革新するフレームワークであり、以下の技術的貢献を持ちます。
-
Frequency Graph Discovery Module (FGD): DWTで時系列を周波数分解し、チャネル間の相関をGNNで捉えてグラフとして構築・整列します。$L_{FF}$(ノード特徴整列)と $L_{FC}$(エッジ対照損失)の2つの損失で、周波数構造のドメイン間共有を実現しています
-
Frequency-Contextual Contrastive Learning (FCCL): MoCoスタイルの対照学習を周波数文脈に適用し、ドメイン内の識別的な表現を獲得します。momentum-updated encoderと大規模負例キューにより、安定した対照学習が可能です
-
Language-Guided Adversary Alignment (LAA): GPT-3のテキスト埋め込みをアンカーとして分類特徴のドメイン整列を誘導する、計算効率の高いモジュールです。辞書ルックアップのみで外部知識を活用し、一貫した精度向上を実現しています
-
3データセットでの state-of-the-art: HAR、WISDM、HHARの全てで既存手法を大幅に上回り、特にWISDMでは精度+9.40%、F1+15.44%の改善を達成しています
今後の展望
ConFGDは、時系列UDAにおける周波数情報とLLM知識の活用という新しい方向性を切り開きました。今後は以下のような発展が期待されます。
より多くのドメインへの展開: 現在は行動認識が中心ですが、産業IoTの予知保全、医療信号のクロス患者適応、金融時系列のレジーム適応など、多変量時系列のドメインシフトが存在するあらゆる領域に応用可能です。
時系列基盤モデルとの統合: TRACEのようなマルチモーダル時系列モデルとConFGDの周波数グラフ発見を組み合わせることで、検索・分類・ドメイン適応を統合的に行えるモデルが構築できる可能性があります。
次のステップとして、以下の記事も参考にしてください。

