CORAL(相関整合)によるドメイン適応を理解して実装する

同じ機種のポンプが2台並んでいる工場を想像してください。1号機の振動データで学習した異常判定器を、そのまま2号機に持っていくと、なぜか誤検知が急に増えます。センサーの取り付け角がわずかに違う、基礎の剛性が違う、設置場所の環境温度が違う——それだけで特徴量の分布はずれます。分類のルール(「この振動パターンは異常」)自体は共通のはずなのに、入力の分布がずれただけでモデルは壊れてしまう。これがドメインシフトの怖さです。

この「分布がずれたぶんだけ、特徴量を座標変換して合わせ直そう」という素朴な発想を、二次統計量(共分散行列)だけで実行するのが CORAL(CORrelation ALignment、相関整合) です。学習も敵対的な最適化も要らず、行列の平方根を1回計算するだけで済むのに、驚くほど強いベースラインになります。

CORALが効く場面は工場だけではありません。人工衛星に載ったセンサーは打ち上げ後に感度が少しずつドリフトしますが、地上試験データで作った判別器を軌道上データに持ち込むときの「あわせこみ」に使えます。医用画像でもCT装置のメーカーが違えば画素値の分布が変わるので、装置間で特徴分布をそろえる前処理として使われます。音声認識でマイクや話者が変わったとき、物体認識で撮影条件(Amazonの商品写真 vs スマホ撮影)が変わったときも同じです。

ソースで学習した判定ルールがターゲットでは崩れ、点群の形をそろえると再び当たるようになることを示す3枚組の概念図

この記事でやろうとしていることを、先に絵で見ておきます。左は1号機(ソース)のデータで引いた判定の線で、正解率は96.6%あります。真ん中は同じ線を2号機(ターゲット)のデータに当てたもので、点群が別の場所に別の形で広がっているために正解率が51.9%——コインを投げるのとほぼ同じ水準——まで落ちています。右はターゲットの点群と「同じ形」になるようソースを座標変換してから引き直した線で、ターゲットの正解率が94.4%まで戻っています。ラベルを一切使わずにこの変換を作るのがCORALです。

本記事の内容

  • ドメインシフトが分類器を壊す仕組みと、「分布をそろえる」というアプローチの位置づけ
  • CORAL損失 $\ell_{\mathrm{CORAL}} = \frac{1}{4d^2}\|\bm{C}_S – \bm{C}_T\|_F^2$ の定義と、なぜ $4d^2$ で割るのか
  • 線形変換 $\bm{A}$ に対する最適解が $\bm{A}^\ast = \bm{C}_S^{-1/2}\bm{C}_T^{1/2}$(白色化してから再着色)と閉形式で求まることの導出
  • Deep CORAL:同じ損失を中間層に正則化項として足すだけ、という設計と勾配の導出
  • MMDとの比較——分布全体を見るMMDに対し、CORALは二次統計に絞る
  • Pythonでの実装(2次元トイデータの可視化/転移精度の比較/整合度の推移/CORALが効かない例)

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

ドメインシフトはなぜ分類器を壊すのか

まず問題を正確に切り出しましょう。手元には、ラベル付きのソースドメインのデータ

$$ \mathcal{D}_S = \{(\bm{x}_i^S, y_i^S)\}_{i=1}^{n_S}, \quad \bm{x}_i^S \in \mathbb{R}^d $$

と、ラベルがないターゲットドメインのデータ

$$ \mathcal{D}_T = \{\bm{x}_j^T\}_{j=1}^{n_T}, \quad \bm{x}_j^T \in \mathbb{R}^d $$

があります。ターゲットのラベルが一切ない設定を教師なしドメイン適応(unsupervised domain adaptation, UDA)と呼びます。目的は、ソースのラベルだけを使って、ターゲットで高い精度を出す分類器を作ることです。

ここで置く標準的な仮定が共変量シフトです。

$$ p_S(\bm{x}) \neq p_T(\bm{x}), \qquad p_S(y \mid \bm{x}) = p_T(y \mid \bm{x}) $$

「入力の分布は変わったが、入力が決まったときのラベルの決まり方は変わっていない」という仮定です。1号機と2号機のポンプなら、「この波形なら軸受け異常」という物理は共通で、変わったのはセンサーの向きやゲインだけ、という状況に対応します。

なぜこれで分類器が壊れるのか。学習した分類器は、ソース分布の上でだけ損失を小さくするように決定境界を引きます。ターゲット分布がその境界から遠く離れた場所に集まると、境界は「データが無かった場所」に引かれていたことになり、そこでの振る舞いは何も保証されません。線形分類器なら、特徴空間が全体的に回転・伸縮しただけでも、固定された超平面はまったく別の意味の切り方になってしまいます。

対処法は大きく3つに分かれます。

方針 考え方 代表手法
重要度重み付け 密度比 $p_T(\bm{x})/p_S(\bm{x})$ でソース標本に重みを付け直す KLIEP、uLSIF
統計量マッチング 特徴の統計量(平均・共分散・カーネル平均)をそろえる MMD、CORAL、Deep CORAL
敵対的学習 ドメイン判別器を騙す特徴を学ぶ DANN、ADDA

密度比推定は高次元で破綻しやすく、敵対的学習は学習が不安定になりがちです。その中間にある「統計量マッチング」は、何をそろえるかを人間が明示的に決めるぶん、安定していて解釈しやすいのが利点です。CORALはその中でも極端に割り切った手法——二次統計量(共分散)だけをそろえる——です。

では、「二次統計量だけ」で本当に足りるのでしょうか。次の節で、まず絵で直感をつかみましょう。

CORALの直感 — 点群の「形」をそろえる

2次元の散布図を思い浮かべてください。ソースのデータ点は原点付近に、右上がりの細長い楕円状に広がっているとします。ターゲットのデータ点は、そこから少し離れた場所に、別の向きに傾いた、もっと大きい楕円として広がっています。

このとき、平均をそろえるだけなら簡単です。ソースの点群を平行移動して、重心をターゲットの重心に重ねればよい。しかしそれだけでは、楕円の「傾き」と「太さ」が合っていないので、分布は依然として重なりません。細長い方向が90度ずれていれば、ソースで学んだ「この方向に進むとクラスが変わる」という知識はターゲットではまったく別方向を指してしまいます。

CORALのアイデアは、この楕円の形そのものをターゲットの楕円に一致させることです。楕円の形と向きを決めているのは共分散行列 $\bm{C}$ です($\bm{x}^\top \bm{C}^{-1}\bm{x} = 1$ が等確率楕円)。だから共分散行列を一致させれば、点群の「形」が一致します。

共分散行列の4つの成分を変えたときに点群の等確率楕円がどう変形するかを6パターン並べた図

共分散行列の成分と楕円の関係を、6通り並べて確かめておきます。上段のように対角成分だけを変えると楕円は座標軸に沿って伸び縮みするだけで、傾きはまったく変わりません。一方、下段のように非対角成分を入れると楕円が斜めに倒れ、符号を反転させると倒れる向きが逆になります。黒矢印(固有ベクトル)の向きが非対角成分によってのみ回転していることに注目してください。後で「特徴ごとの標準化では足りない」と述べるのは、標準化が上段の操作しかできないからです。

やり方は2段階のレシピになっています。

  1. 白色化(whitening):ソースの点群を $\bm{C}_S^{-1/2}$ で変換して、共分散を単位行列 $\bm{I}$ にする。楕円をいったん真円(等方的な雲)に戻す操作です。
  2. 再着色(re-coloring):そこに $\bm{C}_T^{1/2}$ を掛けて、ターゲットと同じ楕円に「着色し直す」。

料理でたとえるなら、味付けの済んだソースの素材をいったん洗って素の状態に戻し、ターゲットのレシピで味を付け直す、という手順です。全体を1つの行列にまとめると

$$ \bm{A}^\ast = \bm{C}_S^{-1/2}\bm{C}_T^{1/2} $$

となります。この行列を掛けるだけ——学習もイテレーションも要りません。しかも後で見るように、これは「共分散差のフロベニウスノルムを最小化せよ」という最適化問題の厳密な大域最適解になっています。原論文(Sun, Feng, Saenko, Return of Frustratingly Easy Domain Adaptation, AAAI 2016)のタイトルに “Frustratingly Easy”(あきれるほど簡単)とあるのは、この身も蓋もない簡単さのことです。

ソースの楕円を白色化で真円に戻し、ターゲットの共分散で再着色して形をそろえる3段階の図

この2段階を実際の点群で追ったのが上の図です。左(元のソース)は右上がりに細長く、ターゲットの2シグマ楕円(橙の破線)とは向きも大きさも合っておらず、$\|\bm{C}-\bm{C}_T\|_F$ は 6.993 あります。真ん中で $\bm{C}_S^{-1/2}$ を掛けると点群は灰色の単位円にぴたりと重なる等方的な雲になりますが、ターゲットとのずれはむしろ 7.719 に増えています。白色化それ自体はゴールではなく、共通の基準座標にいったん戻すための中継点にすぎません。右で $\bm{C}_T^{1/2}$ を掛けると距離は 0.000 まで落ち、緑の楕円が橙の破線に完全に一致します。

直感がつかめたところで、記号と定義をきちんと決めましょう。「共分散をそろえる」を数式にするには、まず標本共分散行列の書き方を固めておく必要があります。

記号の準備 — 標本共分散行列の行列表現

データを行方向に並べた行列を使います。ソースのデータ行列を

$$ \bm{D}_S \in \mathbb{R}^{n_S \times d} $$

とし、その第 $i$ 行が $i$ 番目のサンプル $(\bm{x}_i^S)^\top$ だとします。ターゲットも同様に $\bm{D}_T \in \mathbb{R}^{n_T \times d}$ とします。

標本平均ベクトルは、全要素1のベクトル $\bm{1} \in \mathbb{R}^{n_S}$ を使って

$$ \bm{\mu}_S^\top = \frac{1}{n_S}\bm{1}^\top \bm{D}_S $$

と書けます。中心化されたデータ行列は、中心化行列 $\bm{H}_S = \bm{I}_{n_S} – \frac{1}{n_S}\bm{1}\bm{1}^\top$ を左から掛けて

$$ \bar{\bm{D}}_S = \bm{H}_S \bm{D}_S = \bm{D}_S – \frac{1}{n_S}\bm{1}\bm{1}^\top \bm{D}_S $$

です。$\bm{H}_S$ は対称かつ冪等($\bm{H}_S^2 = \bm{H}_S$)な射影行列で、「平均成分を抜き取る」働きをします。これを使うと不偏標本共分散行列は

$$ \begin{equation} \bm{C}_S = \frac{1}{n_S-1}\bar{\bm{D}}_S^\top \bar{\bm{D}}_S = \frac{1}{n_S-1}\left(\bm{D}_S^\top \bm{D}_S – \frac{1}{n_S}(\bm{1}^\top \bm{D}_S)^\top(\bm{1}^\top \bm{D}_S)\right) \end{equation} $$

と書けます。2つ目の等号は、$\bm{H}_S^\top \bm{H}_S = \bm{H}_S$ を使って展開しただけです。実装ではこの右辺の形が便利で、$\bm{D}_S^\top \bm{D}_S$(グラム行列)と列和 $\bm{1}^\top\bm{D}_S$ を1回のパスで求めれば計算できます。Deep CORALの原論文もこの形で共分散を書いています。

$\bm{C}_S$ は $d \times d$ の対称半正定値行列です。$n_S > d$ かつデータが退化していなければ正定値になり、逆行列も平方根も定義できます。逆に $n_S \le d$ のときは必ずランク落ちするので、後で述べる正則化が必要になります。

これで「そろえたい対象」が数式で書けました。次は「そろっている度合い」を測るものさし——CORAL損失——を定義します。

CORAL損失の定義

2つの共分散行列がどれだけ違うかを測る、いちばん素朴なものさしは何でしょうか。行列を $d^2$ 個の数が並んだベクトルだと思って、成分ごとの差の二乗和を取る——つまりフロベニウスノルムです。CORALはこれをそのまま採用します。

$$ \begin{equation} \ell_{\mathrm{CORAL}} = \frac{1}{4d^2}\left\|\bm{C}_S – \bm{C}_T\right\|_F^2 = \frac{1}{4d^2}\sum_{p=1}^{d}\sum_{q=1}^{d}\left(\left[\bm{C}_S\right]_{pq} – \left[\bm{C}_T\right]_{pq}\right)^2 \end{equation} $$

対角成分の差は「各特徴の分散のずれ」、非対角成分の差は「特徴どうしの相関のずれ」を表します。CORAL(相関整合)という名前は後者に由来しますが、実際には分散も相関もまとめて合わせています。

前の係数 $\frac{1}{4d^2}$ には2つの役割があります。

  • $\frac{1}{4}$:二乗の微分で出てくる係数 2 と、後で見る $\partial\|\bm{X}\|_F^2/\partial \bm{X} = 2\bm{X}$ の 2 を合わせた 4 を打ち消して、勾配の式をきれいにするためのものです。損失の最小化問題としては定数倍なので本質ではありません。
  • $\frac{1}{d^2}$:フロベニウスノルムの二乗は $d^2$ 個の項の和なので、次元 $d$ が大きいほど自然に大きくなります。$d^2$ で割ると「成分あたりの平均二乗誤差」になり、層ごとに次元が違うネットワークでも、同じ $\lambda$ で重み付けできるようになります。Deep CORALで複数の層に損失を掛けたいときに効いてきます。

この損失の基本的な性質を押さえておきましょう。

  1. $\ell_{\mathrm{CORAL}} \ge 0$ であり、$\ell_{\mathrm{CORAL}} = 0 \iff \bm{C}_S = \bm{C}_T$。つまり「二次統計量が完全に一致する」ことと同値です。
  2. 平行移動不変:$\bm{x} \mapsto \bm{x} + \bm{b}$ としても共分散は変わらないので、損失は平均のずれをまったく見ません。平均をそろえるのは別処理(センタリング+ターゲット平均の足し戻し)の仕事です。
  3. 直交変換に対する共変性:両ドメインの特徴に同じ直交行列 $\bm{Q}$ を掛けると $\bm{C} \mapsto \bm{Q}^\top \bm{C}\bm{Q}$ となり、フロベニウスノルムは直交変換で不変なので損失は変わりません。座標系の取り方に依らない、という良い性質です。
  4. 二次までしか見ない:分布が多峰でも歪んでいても、共分散が同じなら損失はゼロです。これは軽さと引き換えの限界であり、後でMMDと比べるときの論点になります。

ここまでで「ずれの大きさ」を測る関数が手に入りました。あとはこれを小さくする変換を探すだけです。驚くべきことに、線形変換に限れば答えは紙と鉛筆で書き下せます。

線形CORALの閉形式解を導出する

問題設定

ソースのデータ行列 $\bm{D}_S$ に線形変換 $\bm{A} \in \mathbb{R}^{d\times d}$ を右から掛けて、新しいソース特徴 $\hat{\bm{D}}_S = \bm{D}_S\bm{A}$ を作ります。行ベクトル表記なので、各サンプルは $\hat{\bm{x}}^\top = \bm{x}^\top\bm{A}$、すなわち $\hat{\bm{x}} = \bm{A}^\top \bm{x}$ と変換されます。

変換後の共分散は、定義に代入するだけで

$$ \bm{C}_{\hat{S}} = \frac{1}{n_S-1}(\bar{\bm{D}}_S\bm{A})^\top(\bar{\bm{D}}_S\bm{A}) = \bm{A}^\top \left(\frac{1}{n_S-1}\bar{\bm{D}}_S^\top\bar{\bm{D}}_S\right)\bm{A} = \bm{A}^\top \bm{C}_S \bm{A} $$

と求まります。つまり共分散行列は合同変換を受けます。そこで解きたい最適化問題は

$$ \begin{equation} \bm{A}^\ast = \arg\min_{\bm{A}} \left\|\bm{A}^\top \bm{C}_S \bm{A} – \bm{C}_T\right\|_F^2 \end{equation} $$

です(定数 $\frac{1}{4d^2}$ は最小化点に影響しないので省いています)。一見すると $\bm{A}$ について4次の非凸問題で、勾配法を回さないと解けなさそうに見えます。ところが、対称行列の平方根を使うと一発で解けてしまいます。

対称平方根の定義

準備として、対称正定値行列の平方根を定義します。$\bm{C}_S$ は対称なので、固有値分解

$$ \bm{C}_S = \bm{U}_S \bm{\Sigma}_S \bm{U}_S^\top, \qquad \bm{U}_S^\top\bm{U}_S = \bm{I}, \quad \bm{\Sigma}_S = \mathrm{diag}(\sigma_1,\dots,\sigma_d),\ \sigma_k > 0 $$

ができます。ここで

$$ \bm{C}_S^{1/2} := \bm{U}_S \bm{\Sigma}_S^{1/2}\bm{U}_S^\top, \qquad \bm{C}_S^{-1/2} := \bm{U}_S \bm{\Sigma}_S^{-1/2}\bm{U}_S^\top $$

と定義します($\bm{\Sigma}_S^{1/2}$ は対角成分を $\sqrt{\sigma_k}$ に置き換えたもの)。$\bm{U}_S^\top\bm{U}_S = \bm{I}$ を使えば

$$ \bm{C}_S^{1/2}\bm{C}_S^{1/2} = \bm{U}_S\bm{\Sigma}_S^{1/2}\underbrace{\bm{U}_S^\top\bm{U}_S}_{=\bm{I}}\bm{\Sigma}_S^{1/2}\bm{U}_S^\top = \bm{U}_S\bm{\Sigma}_S\bm{U}_S^\top = \bm{C}_S $$

が確かめられます。同様に $\bm{C}_S^{-1/2}\bm{C}_S\bm{C}_S^{-1/2} = \bm{I}$ も成り立ちます。これが「白色化すると共分散が単位行列になる」ことの数式表現です。定義から $\bm{C}_S^{1/2}$ と $\bm{C}_S^{-1/2}$ はどちらも対称であることに注意してください(この対称性を後の検算で使います)。

解の提示と検算

主張はこうです。

$$ \begin{equation} \bm{A}^\ast = \bm{C}_S^{-1/2}\,\bm{C}_T^{1/2} \end{equation} $$

が問題 (3) の大域最適解であり、そのとき目的関数の値はちょうど 0 になります。

証明は代入するだけです。$\bm{A}^\ast$ を目的関数に入れると

$$ (\bm{A}^\ast)^\top \bm{C}_S \bm{A}^\ast = \left(\bm{C}_S^{-1/2}\bm{C}_T^{1/2}\right)^\top \bm{C}_S \left(\bm{C}_S^{-1/2}\bm{C}_T^{1/2}\right) $$

転置を分配します。$(\bm{X}\bm{Y})^\top = \bm{Y}^\top\bm{X}^\top$ と、$\bm{C}_T^{1/2}, \bm{C}_S^{-1/2}$ がともに対称であることを使うと

$$ = \bm{C}_T^{1/2}\,\bm{C}_S^{-1/2}\,\bm{C}_S\,\bm{C}_S^{-1/2}\,\bm{C}_T^{1/2} $$

中央の3つをまとめます。$\bm{C}_S^{-1/2}\bm{C}_S\bm{C}_S^{-1/2} = \bm{I}$ でしたから

$$ = \bm{C}_T^{1/2}\,\bm{I}\,\bm{C}_T^{1/2} = \bm{C}_T^{1/2}\bm{C}_T^{1/2} = \bm{C}_T $$

したがって

$$ \left\|(\bm{A}^\ast)^\top \bm{C}_S \bm{A}^\ast – \bm{C}_T\right\|_F^2 = \|\bm{0}\|_F^2 = 0 $$

目的関数はノルムの二乗なので常に 0 以上です。その下限 0 を達成する点を1つ見つけたので、$\bm{A}^\ast$ は大域最適解です。非凸問題なのに、局所解に悩む必要がまったくありません。

この式を右から読むと、まさに前節のレシピそのものです。データ $\bm{x}$ に対して $\hat{\bm{x}} = (\bm{A}^\ast)^\top \bm{x} = \bm{C}_T^{1/2}\bm{C}_S^{-1/2}\bm{x}$、つまり「まず $\bm{C}_S^{-1/2}$ で白色化し、次に $\bm{C}_T^{1/2}$ で再着色する」という2段構えになっています。

解は一意ではない — 直交行列ぶんの自由度

ここで大事な注意があります。最適解は $\bm{A}^\ast$ ただ1つではありません。任意の直交行列 $\bm{Q}$($\bm{Q}^\top\bm{Q}=\bm{I}$)に対して

$$ \bm{A}_{\bm{Q}} = \bm{C}_S^{-1/2}\,\bm{Q}\,\bm{C}_T^{1/2} $$

も同じく最適解です。実際、同じ計算を繰り返すと

$$ \bm{A}_{\bm{Q}}^\top \bm{C}_S \bm{A}_{\bm{Q}} = \bm{C}_T^{1/2}\bm{Q}^\top \left(\bm{C}_S^{-1/2}\bm{C}_S\bm{C}_S^{-1/2}\right)\bm{Q}\,\bm{C}_T^{1/2} = \bm{C}_T^{1/2}\underbrace{\bm{Q}^\top\bm{Q}}_{=\bm{I}}\bm{C}_T^{1/2} = \bm{C}_T $$

となり、やはり損失ゼロです。

この $\bm{Q}$ の自由度は、CORALの本質的な限界を表しています。共分散だけを見ていても、点群をどの向きに「回して」ターゲットに重ねるべきかは決まりません。 白色化した後の空間では分布が等方的(真円)なので、どう回しても見た目は同じだからです。CORALは「$\bm{Q} = \bm{I}$」という何の根拠もない選択をしているにすぎません。それでも実務でうまくいくことが多いのは、多くの現実的なドメインシフトが「回転よりも伸縮・相関の変化」に支配されているからです。逆に、回転が支配的なシフトではCORALは効きません。この点は後でPythonで実演します。

直交行列Qを0度・90度・180度と変えても損失はゼロのまま、ターゲット正解率だけが0.951から0.045まで激変することを示す3枚組の散布図

$\bm{Q}$ の自由度がどれだけ危険かを、同じデータで $\bm{Q}$ だけを変えて描いたのが上の図です。3枚ともCORAL損失は $10^{-31}$ 台、つまり数値誤差の範囲でゼロで、二次統計の整合という意味では完全に等価な解です。ところがターゲットでの正解率は、$\bm{Q}=\bm{I}$ で 0.951、90度回転で 0.512(偶然と同じ)、180度回転では 0.045——ラベルが総入れ替わりになって当てずっぽう以下——と激変します。損失関数はこの3つを区別する情報を持っていません。CORALが $\bm{Q}=\bm{I}$ を選ぶのは原理的な根拠ではなく、単なる既定値だということがはっきり見て取れます。

なお、この $\bm{Q}$ の自由度の中に「最適輸送の意味で最も動かさない解」も含まれています。ガウス分布どうしのモンジュ写像(2-Wasserstein最適輸送写像)は

$$ \bm{T} = \bm{C}_S^{-1/2}\left(\bm{C}_S^{1/2}\bm{C}_T\bm{C}_S^{1/2}\right)^{1/2}\bm{C}_S^{-1/2} $$

で与えられ、これは対称行列で $\bm{T}\bm{C}_S\bm{T} = \bm{C}_T$ を満たします。つまり最適輸送版のCORALは、上の族の中から「移動距離の総和が最小になる $\bm{Q}$」を選んだものだと解釈できます。CORALの素朴な選択がやや粗いことの裏返しでもあります。

ランク落ちへの対処と平均の扱い

実データでは $n_S \le d$(サンプル数より次元が多い)ことが珍しくなく、そのとき $\bm{C}_S$ は特異になって $\bm{C}_S^{-1/2}$ が計算できません。原論文はここにリッジ型の正則化を入れます。

$$ \bm{A}^\ast_\lambda = (\bm{C}_S + \lambda\bm{I})^{-1/2}(\bm{C}_T + \lambda\bm{I})^{1/2} $$

固有値の小さい方向(ノイズが支配的な方向)を $\lambda$ が持ち上げるので、白色化がその方向を過剰に増幅するのを防げます。$\lambda$ は $10^{-3}$ 程度の小さい値から試すのが定石です。原論文では特異値分解を使い、小さい特異値を切り捨ててから平方根を取る実装も示されています。

標本数15・次元20で共分散がランク落ちする様子と、リッジλに対する増幅率と整合残差のトレードオフを示す2枚組の図

左の図が問題の正体です。$n=2000$ なら固有値は最小でも $3\times10^{-3}$ 程度に留まりますが、$n=15 < d=20$ では15番目以降の固有値が $10^{-15}$ 以下——数値的にゼロ——に落ち込み、$\bm{C}_S^{-1/2}$ がその方向を $10^{7}$ 倍以上に増幅してしまいます。緑の線が示すとおり、$\lambda=10^{-3}$ を足すと固有値の下限が $10^{-3}$ に持ち上がり、増幅が抑えられます。右の図はそのトレードオフを実測したもので、$\lambda$ を $10^{-8}$ まで下げると整合残差はほぼ最小(3.13)になる代わりに変換の最大増幅率が $1.6\times10^4$ まで爆発し、逆に $\lambda=10$ では増幅率 1.3 と穏やかになる代わりに残差が 10.3 まで悪化します。「整合の良さ」と「変換の暴れなさ」は両立せず、$\lambda$ はその折り合いを決めるつまみなのです。

もう1つ、共分散は平行移動不変なので、CORALは平均のずれを直しません。実務では次の3ステップにするのが自然です。

  1. ソースを中心化:$\bar{\bm{D}}_S = \bm{D}_S – \bm{1}\bm{\mu}_S^\top$
  2. CORAL変換:$\hat{\bm{D}}_S = \bar{\bm{D}}_S\bm{A}^\ast_\lambda$
  3. ターゲット平均を足し戻す:$\hat{\bm{D}}_S \leftarrow \hat{\bm{D}}_S + \bm{1}\bm{\mu}_T^\top$

これで一次統計量(平均)と二次統計量(共分散)の両方がそろいます。原論文が「特徴を標準化してからCORALを掛ける」と書いているのも実質は同じ話です。

計算量も確認しておきましょう。共分散の計算が $O(n d^2)$、固有値分解が $O(d^3)$、変換の適用が $O(nd^2)$ です。$d$ が数百〜数千程度なら一瞬で終わります。学習ループが一切ないのは大きな利点です。

理論の骨格が見えたので、次は小さい数値例で手を動かして確認しましょう。

具体例 — 2次元で手計算する

$d=2$ とし、

$$ \bm{C}_S = \begin{pmatrix} 4 & 0 \\ 0 & 1\end{pmatrix}, \qquad \bm{C}_T = \begin{pmatrix} 2 & 1 \\ 1 & 2\end{pmatrix} $$

とします。ソースは「横に長く、相関ゼロ」、ターゲットは「正の相関を持つ、45度方向に伸びた」分布です。

まず変換前の損失を計算します。差は

$$ \bm{C}_S – \bm{C}_T = \begin{pmatrix} 2 & -1 \\ -1 & -1\end{pmatrix} $$

なので、フロベニウスノルムの二乗は $2^2 + (-1)^2 + (-1)^2 + (-1)^2 = 7$、$d=2$ なので

$$ \ell_{\mathrm{CORAL}} = \frac{7}{4\cdot 2^2} = \frac{7}{16} = 0.4375 $$

次に $\bm{C}_S^{-1/2}$ を求めます。$\bm{C}_S$ は対角なので固有値分解は自明で、固有値 $4, 1$ の平方根の逆数を取れば

$$ \bm{C}_S^{-1/2} = \begin{pmatrix} 1/2 & 0 \\ 0 & 1\end{pmatrix} $$

$\bm{C}_T$ の固有値分解は、固有ベクトルが $\frac{1}{\sqrt{2}}(1,1)^\top$(固有値 3)と $\frac{1}{\sqrt{2}}(1,-1)^\top$(固有値 1)です。平方根は固有値だけを $\sqrt{3}, 1$ に置き換えて組み直せばよいので、

$$ \bm{C}_T^{1/2} = \frac{\sqrt{3}}{2}\begin{pmatrix}1&1\\1&1\end{pmatrix}\cdot\frac{1}{1} + \frac{1}{2}\begin{pmatrix}1&-1\\-1&1\end{pmatrix} = \frac{1}{2}\begin{pmatrix}\sqrt{3}+1 & \sqrt{3}-1 \\ \sqrt{3}-1 & \sqrt{3}+1\end{pmatrix} $$

数値にすると $\begin{pmatrix}1.366 & 0.366\\ 0.366 & 1.366\end{pmatrix}$ です。念のため二乗すると $1.366^2 + 0.366^2 = 2.000$、$2\times1.366\times0.366 = 1.000$ となり、確かに $\bm{C}_T$ に戻ります。

したがって

$$ \bm{A}^\ast = \bm{C}_S^{-1/2}\bm{C}_T^{1/2} = \begin{pmatrix} 1/2 & 0 \\ 0 & 1\end{pmatrix}\begin{pmatrix}1.366 & 0.366\\ 0.366 & 1.366\end{pmatrix} = \begin{pmatrix}0.683 & 0.183\\ 0.366 & 1.366\end{pmatrix} $$

検算しましょう。$(\bm{A}^\ast)^\top\bm{C}_S\bm{A}^\ast$ の $(1,1)$ 成分は $4\times0.683^2 + 1\times0.366^2 = 1.866 + 0.134 = 2.000$、$(1,2)$ 成分は $4\times0.683\times0.183 + 1\times0.366\times1.366 = 0.500 + 0.500 = 1.000$ です。確かに $\bm{C}_T$ に一致し、損失は $0.4375 \to 0$ になりました。

Cs・Ct・その差・最適変換A・変換後の共分散・変換後の差を6枚のヒートマップで並べ、差がゼロになる様子を示した図

ここまでの手計算を行列のまま並べたのが上の図です。上段右の $\bm{C}_S-\bm{C}_T$ には、対角の $(1,1)$ 成分に $+2.000$(分散のずれ)と、非対角に $-1.000$(相関のずれ)がはっきり残っており、二乗和 7、損失 0.4375 の内訳が目で読めます。下段中央の $(\bm{A}^\ast)^\top\bm{C}_S\bm{A}^\ast$ は $\bm{C}_T$ と1つの成分も違わず、下段右の差は全成分が $10^{-15}$ 台(損失 $6.9\times10^{-32}$)——倍精度の丸め誤差だけです。導出した閉形式解が本当に厳密解であることが、数値の上でも確認できました。

$\bm{A}^\ast$ が対称でないことにも注目してください。「白色化」と「再着色」という非対称な2段構えの結果であり、これがさきほど述べた $\bm{Q}$ の自由度のうちの1つを選んだ形になっています。

手計算で仕組みが確認できました。ここまでは特徴量を固定して線形変換を探す話でしたが、深層学習では特徴量そのものを学習します。同じ損失をニューラルネットにどう組み込むかを見ていきましょう。

Deep CORAL — 損失を正則化項として足すだけ

なぜ線形版では足りないのか

線形CORALは、すでにある特徴量(例えば事前学習済みCNNの中間層出力)を後処理でそろえるだけです。しかし特徴抽出器自体を学習できるなら、「そもそも両ドメインで統計量が一致するような特徴」を学ばせたほうが強いはずです。しかも分類精度と同時に最適化すれば、「ドメインの違いは消えるが、クラスの違いは残る」特徴が得られます。

Deep CORAL(Sun & Saenko, ECCV 2016 Workshops)はこれを、拍子抜けするほど単純に実現します。分類損失にCORAL損失を足すだけです。

$$ \begin{equation} \mathcal{L} = \mathcal{L}_{\mathrm{CLS}} + \sum_{l \in \mathcal{L}_{\mathrm{align}}} \lambda_l \, \ell_{\mathrm{CORAL}}^{(l)} \end{equation} $$

ここで $\mathcal{L}_{\mathrm{CLS}}$ はソースのラベルに対する交差エントロピー、$\ell_{\mathrm{CORAL}}^{(l)}$ は第 $l$ 層の特徴に対するCORAL損失です。原論文ではAlexNetの最終全結合層(fc8)1か所に掛けています。DANNのように勾配反転層やドメイン判別器を追加する必要はなく、既存の学習ループに数行足すだけで動きます。

勾配の導出

逆伝播に必要な勾配を導きます。ミニバッチ内のソース特徴を $\bm{F}_S \in \mathbb{R}^{n_b \times d}$、ターゲット特徴を $\bm{F}_T \in \mathbb{R}^{n_b \times d}$ とし、$\bm{C}_S, \bm{C}_T$ をそれぞれから計算するとします。

まず損失を $\bm{C}_S$ で微分します。$\|\bm{X}\|_F^2 = \mathrm{tr}(\bm{X}^\top\bm{X})$ の微分公式 $\partial \|\bm{X}\|_F^2/\partial\bm{X} = 2\bm{X}$ を使うと

$$ \frac{\partial \ell_{\mathrm{CORAL}}}{\partial \bm{C}_S} = \frac{1}{4d^2}\cdot 2(\bm{C}_S – \bm{C}_T) = \frac{1}{2d^2}(\bm{C}_S – \bm{C}_T) $$

次に $\bm{C}_S$ を特徴 $\bm{F}_S$ で微分します。中心化した特徴を $\bar{\bm{F}}_S = \bm{H}\bm{F}_S$ と書くと $\bm{C}_S = \frac{1}{n_b-1}\bar{\bm{F}}_S^\top\bar{\bm{F}}_S$ です。ここで中心化行列 $\bm{H}$ が対称冪等であることを使うと、$\bar{\bm{F}}_S$ を通した連鎖律は $\bm{H}$ を1回掛けるだけで済み、

$$ \frac{\partial \ell_{\mathrm{CORAL}}}{\partial \bm{F}_S} = \frac{2}{n_b-1}\,\bar{\bm{F}}_S \cdot \frac{\partial \ell_{\mathrm{CORAL}}}{\partial \bm{C}_S} = \frac{1}{d^2(n_b-1)}\,\bar{\bm{F}}_S\left(\bm{C}_S – \bm{C}_T\right) $$

を得ます。ここで $\frac{1}{4}$ が効いて、係数が $\frac{1}{d^2(n_b-1)}$ という素直な形になりました。原論文に載っている勾配式(成分表記で $\bar{\bm{F}}_S^\top$ を使って書かれているもの)と同じです。ターゲット側 $\bm{F}_T$ についても符号を反転しただけの同型の式になります。

この勾配の意味を読むと面白いことがわかります。$\bar{\bm{F}}_S(\bm{C}_S – \bm{C}_T)$ は、「共分散のずれ $(\bm{C}_S-\bm{C}_T)$ が大きい方向に強く、各サンプルを押し戻す」力です。ある方向でソースの分散が過大なら、その方向の座標を縮める向きに特徴が動かされます。特徴を陽に変換するのではなく、エンコーダの重みを通じて間接的に楕円の形を合わせに行くわけです。

実装上は、現代の自動微分フレームワークならこの式を手で書く必要はありません。共分散の計算を微分可能な演算(行列積と平均)だけで書けば、あとは backward() が処理してくれます。

自明解の危険とλの選び方

ここで自然な疑問が湧きます。CORAL損失だけを最小化するなら、特徴を全部 0 に潰してしまえば $\bm{C}_S = \bm{C}_T = \bm{0}$ で損失ゼロになるのでは? そのとおりで、これは実際に起こりうる退化です。

これを防いでいるのが分類損失 $\mathcal{L}_{\mathrm{CLS}}$ です。特徴が潰れればクラスを判別できなくなるので分類損失が跳ね上がります。原論文も「CORAL損失だけだと特徴が退化し、分類損失だけだとドメイン差が残る。両者のバランスが本質」と明記しています。したがって $\lambda$ は、学習終了時に2つの損失が同程度のオーダーになるように選ぶのが実務的な指針です。$\lambda$ を大きくしすぎると分類が壊れ、小さすぎるとただの通常学習になります。

もう1点、ミニバッチで共分散を推定することの影響も重要です。$d$ 次元の共分散を $n_b$ サンプルから推定するので、$n_b < d$ だと推定した $\bm{C}$ は必ずランク落ちします。バッチサイズは最低でも特徴次元と同程度、できればそれ以上が望ましく、逆に特徴次元を絞る($d$ を小さくする)のも有効です。また、同じ分布から取った2つの標本でも有限標本ゆえに $\ell_{\mathrm{CORAL}} > 0$ になるので、損失には $O(d/n_b)$ 程度の下駄(バイアス)が乗ります。これは後でPythonで実測します。

CORALの中身が見えたところで、「では分布全体を合わせるMMDと何が違うのか」を整理しましょう。実はこの2つは、思っているより近い親戚です。

MMDとの比較 — 何を「そろえて」いるのか

MMD(Maximum Mean Discrepancy、最大平均食い違い) は、カーネル $k$ に対応する再生核ヒルベルト空間 $\mathcal{H}$ での平均埋め込みの距離として定義されます。

$$ \mathrm{MMD}^2(P,Q) = \left\|\mathbb{E}_{\bm{x}\sim P}[\phi(\bm{x})] – \mathbb{E}_{\bm{y}\sim Q}[\phi(\bm{y})]\right\|_{\mathcal{H}}^2 $$

経験推定量は、カーネルトリックで内積を計算して

$$ \widehat{\mathrm{MMD}}^2 = \frac{1}{n_S^2}\sum_{i,i’}k(\bm{x}_i,\bm{x}_{i’}) + \frac{1}{n_T^2}\sum_{j,j’}k(\bm{y}_j,\bm{y}_{j’}) – \frac{2}{n_Sn_T}\sum_{i,j}k(\bm{x}_i,\bm{y}_j) $$

となります。ガウスカーネルのような特性的カーネルを使えば、$\mathrm{MMD}=0 \iff P=Q$ が成り立ちます。つまり、平均だけでなく分布のあらゆるモーメントを一致させる強い規準です。

CORALは二次多項式カーネルMMDである

ここで、カーネルを2次の多項式カーネル $k(\bm{x},\bm{y}) = (\bm{x}^\top\bm{y})^2$ に取ってみましょう。この場合の特徴写像は何でしょうか。

$$ (\bm{x}^\top\bm{y})^2 = (\bm{x}^\top\bm{y})(\bm{y}^\top\bm{x}) = \mathrm{tr}\!\left(\bm{x}\bm{x}^\top\bm{y}\bm{y}^\top\right) = \left\langle \bm{x}\bm{x}^\top,\ \bm{y}\bm{y}^\top\right\rangle_F $$

1つ目の等号はスカラーの転置がそれ自身であること、2つ目はトレースの巡回性 $\mathrm{tr}(\bm{a}\bm{b}^\top) = \bm{b}^\top\bm{a}$ を逆向きに使ったものです。つまり特徴写像は $\phi(\bm{x}) = \bm{x}\bm{x}^\top$(を $d^2$ 次元ベクトルに並べたもの)で、内積はフロベニウス内積です。

これをMMDの定義に代入すると

$$ \mathrm{MMD}^2_{\mathrm{poly2}} = \left\|\mathbb{E}_S[\bm{x}\bm{x}^\top] – \mathbb{E}_T[\bm{y}\bm{y}^\top]\right\|_F^2 $$

となります。両ドメインを中心化しておけば $\mathbb{E}[\bm{x}\bm{x}^\top] = \bm{C}$ ですから、

$$ \mathrm{MMD}^2_{\mathrm{poly2}} = \|\bm{C}_S – \bm{C}_T\|_F^2 = 4d^2\,\ell_{\mathrm{CORAL}} $$

CORAL損失は、二次多項式カーネルを使ったMMDの定数倍そのものです。Sun & Saenkoもこの対応を指摘しています。つまり両者は「別系統の手法」ではなく、カーネルの選び方が違うだけの兄弟です。

使い分けの整理

観点 CORAL MMD(ガウスカーネル)
そろえる対象 二次統計量(共分散)のみ 分布全体(全モーメント)
損失ゼロの意味 $\bm{C}_S=\bm{C}_T$ $P_S = P_T$
計算量 $O(n d^2)$(+閉形式なら $O(d^3)$) $O(n^2 d)$(グラム行列)
ハイパーパラメータ $\lambda$(正則化 or 重み)のみ カーネル幅(バンド幅選択が難物)
閉形式解 あり(線形変換なら厳密解) なし(勾配法が必要)
高次元での安定性 共分散推定が要るので $n \gtrsim d$ 距離が集中しやすくバンド幅に敏感

MMDのほうが原理的には強力ですが、バンド幅(カーネル幅)の選択が性能を大きく左右するという現実的な弱点があります。複数バンド幅を混ぜるMK-MMDのような工夫が必要で、実装も $O(n^2)$ のグラム行列を扱うぶん重くなります。CORALは表現力を二次統計に絞る代わりに、ハイパーパラメータがほぼ無く、計算も軽く、閉形式解まで手に入る。「まず試すベースライン」として非常に優秀なのはこの割り切りのおかげです。

一方でCORALには明確な限界があります。

  • 三次以上の違いは見えない:分布が二峰性になっているようなシフトは、共分散が同じなら検出できません。
  • 条件付きシフトに無力:$p(y\mid\bm{x})$ が変わっている場合(ラベルの意味がずれた場合)は、そもそもの仮定が崩れます。
  • 回転の曖昧性:先に述べた $\bm{Q}$ の自由度。共分散をそろえても、クラスの対応が保たれる保証はありません。
  • クラス条件を無視:全サンプルをまとめた共分散を合わせるので、クラスごとの構造は考慮されません。クラス比がドメイン間で違うと、合わせた結果がかえって悪化することがあります(これを直すのがCORALのクラス条件付き拡張です)。

共分散が完全に同一なガウス・4つの塊・リング状の3分布を並べ、CORAL損失はどれもゼロだがMMDは区別できることを示す図

「二次までしか見ない」がどういうことかを、共分散をぴたりと一致させた3つの分布で可視化しました。ガウスの雲、4つの塊に分かれた分布、ドーナツ状のリング——見た目はまったく違うのに、2シグマ楕円は3枚とも同一で、基準ガウスとのCORAL損失はいずれも $10^{-31}$ 以下、つまり完全にゼロです。一方でガウスカーネルMMD$^2$ は、同分布どうしの 0.0013(有限標本の下駄)に対し、4つの塊で 0.0126、リングで 0.0246 と1桁大きい値を返して形の違いを検出します。CORAL損失がゼロでも分布が一致しているとは限らない——これが二次統計に絞ったことの代償です。

理論の整理が済みました。ここからは実際にコードを書いて、これらの主張——閉形式解で損失がゼロになること、転移精度が実際に上がること、そして効かない場合があること——を自分の目で確かめます。

Pythonでの実装

共通のユーティリティ

まず、対称平方根とCORAL損失を計算する関数を用意します。固有値分解を使うので、数値誤差で負になった固有値はクリップしておきます。

import numpy as np
from numpy.linalg import eigh

def sqrtm_sym(C, eps=1e-12):
    """対称半正定値行列の平方根 C^{1/2}"""
    w, V = eigh(C)                 # 対称行列専用の固有値分解
    w = np.clip(w, eps, None)      # 数値誤差で負になった固有値を除去
    return (V * np.sqrt(w)) @ V.T

def invsqrtm_sym(C, eps=1e-12):
    """対称正定値行列の逆平方根 C^{-1/2}"""
    w, V = eigh(C)
    w = np.clip(w, eps, None)
    return (V / np.sqrt(w)) @ V.T

def cov(X):
    """行方向にサンプルを並べたデータ行列の不偏標本共分散"""
    return np.cov(X, rowvar=False)

def coral_loss(Cs, Ct):
    """CORAL損失 ||Cs - Ct||_F^2 / (4 d^2)"""
    d = Cs.shape[0]
    return np.sum((Cs - Ct) ** 2) / (4 * d * d)

(V * np.sqrt(w)) @ V.T は $\bm{U}\bm{\Sigma}^{1/2}\bm{U}^\top$ を、対角行列を作らずにブロードキャストで計算するイディオムです。$d$ が大きいときのメモリと計算時間を節約できます。eigh は対称行列専用なので、一般の eig より速く、固有値が実数で返る保証があります。

次に、CORAL変換を作る関数です。平均のずれも一緒に直す3ステップ版にします。

def coral_fit(Xs, Xt, lam=1e-3):
    """ソースをターゲットの二次統計に合わせる線形変換 A と平均を返す"""
    d = Xs.shape[1]
    Cs, Ct = cov(Xs), cov(Xt)
    # ランク落ち対策のリッジ正則化を入れて白色化 → 再着色
    A = invsqrtm_sym(Cs + lam * np.eye(d)) @ sqrtm_sym(Ct + lam * np.eye(d))
    return A, Xs.mean(0), Xt.mean(0)

def coral_apply(X, A, mu_s, mu_t):
    """中心化 → 変換 → ターゲット平均を足し戻す"""
    return (X - mu_s) @ A + mu_t

coral_fit は学習ループを一切持たないことに注目してください。共分散を2回計算し、固有値分解を2回するだけで適応が完了します。これが「あきれるほど簡単」の中身です。

実験1: 2次元トイデータでの可視化

まず2次元で、CORAL変換が本当に点群を重ねるのかを目で確認します。ターゲットはソースを60度回転させて異方的に伸縮し、さらに平行移動したものにします。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

rng = np.random.default_rng(0)
n = 600
Cs2 = np.array([[1.0, 0.8], [0.8, 1.0]])           # ソースの共分散(右上がりの楕円)
Ls = np.linalg.cholesky(Cs2)
Xs2 = rng.standard_normal((n, 2)) @ Ls.T

th = np.deg2rad(60)                                 # ターゲットは 60 度回して伸縮+平行移動
R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
M2 = R @ np.diag([2.2, 0.5]) @ R.T
Xt2 = rng.standard_normal((n, 2)) @ Ls.T @ M2 + np.array([3.0, -1.5])

A2, mu_s, mu_t = coral_fit(Xs2, Xt2, lam=0.0)
Xs2_new = coral_apply(Xs2, A2, mu_s, mu_t)

print("変換前の CORAL 損失: %.4f" % coral_loss(cov(Xs2), cov(Xt2)))
print("変換後の CORAL 損失: %.3e" % coral_loss(cov(Xs2_new), cov(Xt2)))
print("変換行列 A =\n", A2)

実行すると、変換前の損失は 3.0285、変換後は $4.7\times10^{-31}$(実質的にゼロ、倍精度の丸め誤差の範囲)になります。$\|\bm{C}_S-\bm{C}_T\|_F$ でいえば 6.9611 から $2.7\times10^{-15}$ です。導出どおり、閉形式解が損失をぴたりとゼロにしていることが数値でも確認できました。

続いて散布図と等確率楕円を描いて、幾何的に何が起きたかを見ます。

def ellipse(C, mu, nsig=2.0, m=200):
    """共分散 C の nsig シグマ等確率楕円の座標を返す"""
    t = np.linspace(0, 2 * np.pi, m)
    circle = np.stack([np.cos(t), np.sin(t)])
    L = np.linalg.cholesky(C)
    return (mu[:, None] + nsig * L @ circle)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
for ax, (X, ttl) in zip(axes, [(Xs2, "変換前:ソース(青)とターゲット(橙)はずれている"),
                               (Xs2_new, "CORAL変換後:ソースがターゲットに重なる")]):
    ax.scatter(X[:, 0], X[:, 1], s=8, alpha=0.4, label="ソース", color="tab:blue")
    ax.scatter(Xt2[:, 0], Xt2[:, 1], s=8, alpha=0.4, label="ターゲット", color="tab:orange")
    e1 = ellipse(cov(X), X.mean(0)); e2 = ellipse(cov(Xt2), Xt2.mean(0))
    ax.plot(e1[0], e1[1], color="tab:blue", lw=2)
    ax.plot(e2[0], e2[1], color="tab:orange", lw=2, ls="--")
    ax.set_title(ttl); ax.set_xlabel("特徴1"); ax.set_ylabel("特徴2")
    ax.legend(); ax.grid(alpha=0.3); ax.set_aspect("equal")
plt.tight_layout(); plt.show()

左の図では、青い楕円(ソース)は右上がりに細長く、橙の破線(ターゲット)は別の方向に大きく傾いていて、位置も離れています。右の図では2つの楕円がぴったり重なります。平均と共分散という一次・二次の統計量だけを合わせれば、ガウス分布どうしなら分布そのものが一致する——CORALがガウス的なデータで強い理由がここに現れています。

実験2: 転移精度の比較

次に、本命の「分類器の転移精度が上がるか」を確かめます。20次元の合成データを作り、ターゲットは対称正定値行列 $\bm{M}$ による線形変換+平行移動で生成します。ラベルは両ドメインで共通の潜在変数から決まるので、共変量シフトの仮定を満たします。

import numpy as np
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(0)
d, ns, nt = 20, 2000, 2000
B = rng.standard_normal((d, d)) / np.sqrt(d) + np.eye(d) * 0.6   # 潜在→観測の混合行列
w = rng.standard_normal(d)                                        # 真の判別方向(潜在空間)
Q = np.linalg.qr(rng.standard_normal((d, d)))[0]
M = Q @ np.diag(np.exp(rng.uniform(-0.9, 0.9, d))) @ Q.T          # ターゲット側の歪み
mt = rng.standard_normal(d) * 1.5                                 # 平均シフト

def make(n, target):
    z = rng.standard_normal((n, d))                 # 潜在変数(両ドメイン共通の分布)
    y = (z @ w + 0.5 * rng.standard_normal(n) > 0).astype(int)
    x = z @ B
    if target:
        x = x @ M + mt                              # ターゲットだけ線形に歪ませる
    return x, y

Xs, ys = make(ns, False)
Xt, yt = make(nt, True)
Xs_te, ys_te = make(2000, False)
print("初期 CORAL 損失: %.4f  (||Cs-Ct||_F = %.3f)"
      % (coral_loss(cov(Xs), cov(Xt)), np.linalg.norm(cov(Xs) - cov(Xt))))

初期のCORAL損失は 0.0514、$\|\bm{C}_S-\bm{C}_T\|_F$ は 9.072 です。実験1の2次元トイ(3.0285)と比べると1桁小さく見えますが、$\frac{1}{d^2}$ で割っていることを思い出してください。成分あたりでならすとこの値でも十分に大きなシフトです。ここで4つの方法を比べます。

def acc(Xtr, ytr, Xte, yte):
    return LogisticRegression(max_iter=5000).fit(Xtr, ytr).score(Xte, yte)

# (a) 無適応:ソースでそのまま学習してターゲットに適用
clf = LogisticRegression(max_iter=5000).fit(Xs, ys)
print("ソース内テスト精度      : %.4f" % clf.score(Xs_te, ys_te))
print("(a) 無適応              : %.4f" % clf.score(Xt, yt))

# (b) 平均だけそろえる
Xs_mean = Xs - Xs.mean(0) + Xt.mean(0)
print("(b) 平均シフトのみ      : %.4f" % acc(Xs_mean, ys, Xt, yt))

# (c) 対角のみ(特徴ごとの標準化)=共分散の対角成分だけ合わせる
Xs_std = (Xs - Xs.mean(0)) / Xs.std(0) * Xt.std(0) + Xt.mean(0)
print("(c) 対角のみ(標準化)  : %.4f" % acc(Xs_std, ys, Xt, yt))

# (d) CORAL(非対角も含めて共分散全体をそろえる)
A, mu_s, mu_t = coral_fit(Xs, Xt, lam=1e-3)
Xs_coral = coral_apply(Xs, A, mu_s, mu_t)
print("(d) CORAL               : %.4f" % acc(Xs_coral, ys, Xt, yt))
print("    変換後 CORAL 損失   : %.3e" % coral_loss(cov(Xs_coral), cov(Xt)))

# (参考) ターゲットのラベルを使って学習した上限
print("(参考) オラクル         : %.4f" % acc(Xt, yt, Xt, yt))

結果は次のようになります。

手法 ターゲット精度
ソース内テスト(参考) 0.9205
(a) 無適応 0.4995
(b) 平均シフトのみ 0.6465
(c) 対角のみ(標準化) 0.6395
(d) CORAL 0.8705
(参考) オラクル(ターゲットのラベルを使用) 0.8930

無適応・平均のみ・対角のみ・CORAL・オラクルのターゲット正解率を並べた棒グラフ

棒グラフにすると差がはっきりします。無適応(赤)は点線で示した偶然の水準 0.5 にぴったり張り付いており、ソース内テストの 0.9205 との落差が「ドメインシフトで壊れる」という言葉の実体です。平均だけ(0.6465)と対角だけ(0.6395)はほぼ同じ高さで、どちらも半分程度しか回復していません。CORAL(緑、0.8705)だけがオラクル(灰、0.8930)のすぐ手前まで届いています。

読み取れることは3つあります。第一に、無適応では 0.4995——2クラス問題で偶然と同じ水準——まで落ちており、ソース内の 0.9205 から完全に崩壊しています。ドメインシフトは確かに分類器を壊します。第二に、平均をそろえるだけ(0.6465)や特徴ごとの標準化だけ(0.6395)では、半分ほどしか回復しません。標準化は共分散行列の対角成分だけを合わせる操作なので、ずれの本体が非対角成分、つまり特徴間の相関構造にあったことがわかります。第三に、共分散全体を合わせるCORALは 0.8705 まで回復し、ターゲットのラベルを使って学習したオラクル 0.8930 に肉薄します。ラベルを一切使わず、固有値分解を2回するだけでここまで戻るのは驚異的です。変換後のCORAL損失も $8.3\times10^{-6}$ とほぼゼロで、リッジ正則化 $\lambda=10^{-3}$ ぶんのわずかなずれだけが残っています。

適応前・対角だけ・CORAL後の共分散差行列を20×20のヒートマップで並べ、非対角成分がずれの本体であることを示す図

なぜ標準化では届かなかったのかを、共分散の差 $\bm{C}_S-\bm{C}_T$ を20×20のヒートマップにして確かめます。左(適応前)には対角にも非対角にも赤青のまだら模様が広がっています。真ん中(対角だけ合わせた後)は対角線が白く消えているのに、非対角の模様はそっくりそのまま残っており、損失も 0.0514 から 0.0387 にしか下がりません。右端の棒グラフが示すとおり、適応前の $\|\bm{C}_S-\bm{C}_T\|_F^2$ のうち 84% は非対角由来(対角 13 に対し非対角 69)で、標準化はその 84% にまったく手を付けられないのです。右(CORAL後)はほぼ真っ白で、両成分とも $10^{-2}$ 台まで落ちています。

実験3: CORALが効かない場合

CORALの限界も実演しておきましょう。理論の節で述べた「回転の曖昧性」が牙をむく例です。ターゲットの歪みを直交行列(回転)にします。

Qr = np.linalg.qr(rng.standard_normal((d, d)))[0]     # 直交行列=純粋な回転
z = rng.standard_normal((nt, d))
yt2 = (z @ w + 0.5 * rng.standard_normal(nt) > 0).astype(int)
Xt_rot = z @ B @ Qr

print("変換前 CORAL 損失: %.4f" % coral_loss(cov(Xs), cov(Xt_rot)))
A3, m1, m2 = coral_fit(Xs, Xt_rot, lam=1e-3)
Xs_rot = coral_apply(Xs, A3, m1, m2)
print("変換後 CORAL 損失: %.3e" % coral_loss(cov(Xs_rot), cov(Xt_rot)))
print("無適応の精度      : %.4f" % clf.score(Xt_rot, yt2))
print("CORAL 後の精度    : %.4f" % acc(Xs_rot, ys, Xt_rot, yt2))

出力は、変換前の損失 0.0376 → 変換後 $1.2\times10^{-4}$ と、統計量の整合は2桁ぶんきちんと達成されているのに、精度は無適応の 0.5280 から CORAL 後は 0.4430 へ悪化します(2クラスなのでどちらもほぼ偶然の水準、むしろ 0.443 は反転してしまった側です)。これがCORALの本質的な弱点です。共分散をそろえる変換は $\bm{A}_{\bm{Q}} = \bm{C}_S^{-1/2}\bm{Q}\bm{C}_T^{1/2}$ という直交行列ぶんの族をなしており、CORALはその中から $\bm{Q}=\bm{I}$ を根拠なく選びます。真のシフトが回転だった場合、正しい $\bm{Q}$ を引き当てる仕組みがないので、当たるも八卦です。「損失が下がった=適応がうまくいった」ではないことは、ドメイン適応全般で肝に銘じておくべき教訓です。

伸縮シフトと回転シフトについて、CORAL損失は両方下がるのに正解率は回転シフトでのみ悪化することを対比した2枚組の棒グラフ

実験2(伸縮・相関のシフト)と実験3(純粋な回転のシフト)を並べると、この教訓が一目でわかります。左の損失はどちらのシフトでも2〜4桁下がっており、0.0514→$8.3\times10^{-6}$、0.0376→$1.2\times10^{-4}$ と、指標の上ではどちらも「大成功」です。ところが右の正解率は正反対で、伸縮シフトでは 0.4995→0.8705 と大幅に改善するのに、回転シフトでは 0.5280→0.4430 と偶然の水準を割り込むところまで悪化します。ターゲットのラベルが無い状況では、損失の低下だけを見て適応の成否を判断することはできない——左右の図の食い違いが、その警告そのものです。

実験4: 標本数と整合度の下限

Deep CORALでミニバッチから共分散を推定するとき、どのくらいの下駄が乗るのかを測ります。同じ分布から取った2つの標本でCORAL損失を計算し、標本数を変えて平均します。

import numpy as np

rng = np.random.default_rng(1)
sizes = [50, 100, 200, 500, 1000, 5000]
means = []
for nn in sizes:
    vals = [coral_loss(cov(rng.standard_normal((nn, d)) @ B),
                       cov(rng.standard_normal((nn, d)) @ B)) for _ in range(20)]
    means.append(np.mean(vals))
    print("n = %5d :  平均 CORAL 損失 = %.5f" % (nn, means[-1]))

plt.figure(figsize=(8, 5))
plt.loglog(sizes, means, "o-", label="同一分布から取った2標本の CORAL 損失")
plt.loglog(sizes, means[0] * sizes[0] / np.array(sizes), "--", label="1/n の傾き(参考線)")
plt.xlabel("1ドメインあたりの標本数 n"); plt.ylabel("CORAL 損失")
plt.title("有限標本による CORAL 損失の下駄(同一分布でもゼロにはならない)")
plt.legend(); plt.grid(alpha=0.3, which="both"); plt.tight_layout(); plt.show()

同一分布から取った2標本のCORAL損失が標本数に対して1/nで減衰する両対数プロット

両対数グラフにすると、実測点(青)が $1/n$ の参考線(灰の破線)にぴたりと重なっているのがわかります。注目すべきは赤い一点鎖線との位置関係で、これは実験2の「本物のドメイン差」0.0514 です。$n=50$ の下駄 0.0192 はその 37% に達しており、この規模のバッチでは測っている損失の3分の1以上が推定ノイズだということになります。$n \ge 200$ まで来ると下駄は本物の差の1割以下に収まります。

$n=50$ で 0.01923、$n=100$ で 0.00997、$n=200$ で 0.00497、$n=1000$ で 0.00101、$n=5000$ で 0.00019。標本数を10倍にすると損失はほぼ10分の1になり、きれいに $O(1/n)$ で減衰しています(共分散推定の分散が $O(1/n)$ であることから予想される挙動です)。ここが重要な警告になります。実験2の初期損失は 0.0514 でしたが、$n=50$ の下駄はその4割近い 0.019 です。つまりバッチサイズ 50 程度でDeep CORALを回すと、測っている損失の相当部分は「本物のドメイン差ではなく共分散の推定ノイズ」だということになります。$n=200$ 以上なら下駄は 0.005 未満に収まり、シフトの信号(0.0514)を十分に上回ります。バッチサイズを特徴次元 $d$ より十分大きく取るべき理由が、この曲線に表れています。

実験5: Deep CORAL の学習曲線

最後に、ニューラルネットの中間層にCORAL損失を足すDeep CORALを実装します。共分散を微分可能な演算だけで書けば、勾配は自動微分に任せられます。

import numpy as np
import torch
import torch.nn as nn

def cov_t(X):
    """torch 版の不偏標本共分散(自動微分が通る書き方)"""
    n = X.shape[0]
    Xc = X - X.mean(0, keepdim=True)
    return Xc.T @ Xc / (n - 1)

def coral_torch(Fs, Ft):
    d_ = Fs.shape[1]
    return ((cov_t(Fs) - cov_t(Ft)) ** 2).sum() / (4 * d_ * d_)

def mmd_torch(Fs, Ft, mults=(0.25, 0.5, 1.0, 2.0, 4.0)):
    """複数バンド幅のガウスカーネルによる MMD^2(メディアンヒューリスティック)"""
    X = torch.cat([Fs, Ft], 0)
    D = torch.cdist(X, X) ** 2
    n = Fs.shape[0]
    med = D.detach().flatten().median().clamp(min=1e-6)
    K = sum(torch.exp(-D / (m * med)) for m in mults)
    return K[:n, :n].mean() + K[n:, n:].mean() - 2 * K[:n, n:].mean()

cov_t は平均を引いてグラム行列を作るだけなので、backward() がそのまま通ります。手で導いた勾配 $\frac{1}{d^2(n_b-1)}\bar{\bm{F}}_S(\bm{C}_S-\bm{C}_T)$ を実装する必要はありません。MMD側は比較用で、バンド幅を距離の中央値の定数倍で複数用意しています。

class Net(nn.Module):
    def __init__(self, d, h=32, k=16):
        super().__init__()
        self.enc = nn.Sequential(nn.Linear(d, h), nn.ReLU(), nn.Linear(h, k), nn.ReLU())
        self.clf = nn.Linear(k, 2)
    def forward(self, x):
        f = self.enc(x)          # ドメイン損失を掛ける中間層特徴(k 次元)
        return self.clf(f), f

def train(Xs, ys, Xt, yt, mode="none", lam=0.0, epochs=60, bs=256, seed=0, wd=1e-2):
    torch.manual_seed(seed)
    net = Net(Xs.shape[1])
    opt = torch.optim.Adam(net.parameters(), lr=1e-3, weight_decay=wd)
    ce = nn.CrossEntropyLoss(); hist = []
    ns_, nt_ = len(Xs), len(Xt)
    for ep in range(epochs):
        net.train()
        ps, pt = torch.randperm(ns_), torch.randperm(nt_)
        for i in range(0, ns_ - bs + 1, bs):
            ids, idt = ps[i:i+bs], pt[i:i+bs]
            opt.zero_grad()
            logit, fs = net(Xs[ids]); _, ft = net(Xt[idt])
            dom = coral_torch(fs, ft) if mode == "coral" else (
                  mmd_torch(fs, ft) if mode == "mmd" else torch.zeros(()))
            (ce(logit, ys[ids]) + lam * dom).backward(); opt.step()
        with torch.no_grad():                       # 1エポックごとに評価
            net.eval()
            ls, fs = net(Xs); lt, ft = net(Xt)
            hist.append(((ls.argmax(1) == ys).float().mean().item(),
                         (lt.argmax(1) == yt).float().mean().item(),
                         coral_torch(fs, ft).item()))
    return np.array(hist)

学習ループの中で通常の学習と違うのは、ターゲットのミニバッチも一緒に流して dom を足す2行だけです。DANNのように勾配反転層や判別器を用意する必要がないので、既存の学習スクリプトへの組み込みコストがほとんどありません。重み減衰 wd=1e-2 を入れているのは、ソースへの過学習を抑えるためです(過学習したネットは、ターゲットで測る前にソース特有の癖を覚えてしまいます)。

# 入力はソース統計で標準化(ターゲットのラベルは一切使わない)
rng = np.random.default_rng(0)
Xs_np, ys_np = make(ns, False); Xt_np, yt_np = make(nt, True)
mu, sd = Xs_np.mean(0), Xs_np.std(0)
to_t = lambda X: torch.tensor((X - mu) / sd, dtype=torch.float32)
Xs_t, Xt_t = to_t(Xs_np), to_t(Xt_np)
ys_t, yt_t = torch.tensor(ys_np), torch.tensor(yt_np)

results = {}
for name, mode, lam in [("正則化なし", "none", 0.0),
                        ("Deep CORAL (λ=1000)", "coral", 1000.0),
                        ("MMD 正則化 (λ=1)", "mmd", 1.0)]:
    h = train(Xs_t, ys_t, Xt_t, yt_t, mode=mode, lam=lam)
    results[name] = h
    print("%-20s ソース %.4f / ターゲット %.4f / CORAL損失 %.3e"
          % (name, h[-1, 0], h[-1, 1], h[-1, 2]))

出力は次のとおりです。

手法 ソース精度 ターゲット精度 最終CORAL損失
正則化なし 0.8910 0.5140 $4.4\times10^{-3}$
Deep CORAL ($\lambda=1000$) 0.8720 0.6310 $5.1\times10^{-7}$
MMD 正則化 ($\lambda=1$) 0.8370 0.7125 $9.2\times10^{-7}$
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
for name, h in results.items():
    axes[0].plot(h[:, 1], label=name)
    axes[1].semilogy(h[:, 2], label=name)
axes[0].set_xlabel("エポック"); axes[0].set_ylabel("ターゲット精度")
axes[0].set_title("ドメイン損失を足すとターゲット精度の劣化が緩やかになる")
axes[1].set_xlabel("エポック"); axes[1].set_ylabel("中間層の CORAL 損失(対数軸)")
axes[1].set_title("フロベニウスノルムで測った二次統計の整合度の推移")
for ax in axes:
    ax.legend(); ax.grid(alpha=0.3)
plt.tight_layout(); plt.show()

正則化なし・Deep CORAL・MMD正則化のソース精度、ターゲット精度、中間層CORAL損失の学習曲線を並べた3枚組の図

3つの曲線を並べると、Deep CORALが何を防いでいるのかがはっきりします。左のソース精度はどれも 0.84〜0.89 に収束し、ドメイン損失を足しても分類そのものが壊れているわけではないことがわかります。それでも中央のターゲット精度は3手法で運命が分かれ、右の中間層CORAL損失を見ると理由が読めます。正則化なし(赤)だけが右上がりに悪化し続けており、中央でターゲット精度が転落していく区間と、右で損失が増えていく区間がぴたりと重なっています。

右の図が、本記事のタイトルにある「相関整合」の推移そのものです。正則化なしのネットは、学習が進むほど中間層のCORAL損失が増えていきます——初期の $1.1\times10^{-5}$ から60エポック後には $4.4\times10^{-3}$ と、およそ390倍です。分類に都合のよい特徴を彫り込んでいくうちに、ソース側の特徴分布だけがどんどん尖っていき、ターゲットの特徴分布と形が食い違っていくのです。中央の図で正則化なしのターゲット精度が8エポック目の 0.6935 をピークに 0.5140 まで転落するのは、これと同じ現象を精度側から見たものです。CORAL項を足したネットは損失を $10^{-6}$ 前後に抑え込み続け、最終精度も 0.6310 と、劣化を大きく緩和します。「学習が進むほどドメイン差が広がる」という現象そのものを、フロベニウスノルムというたった1本の指標で可視化できている点に注目してください。ターゲットのラベルが無い実務でも、この曲線ならモニタリングできます。

一方でこの実験には、正直に受け止めるべき結果も出ています。同じデータに閉形式の線形CORALを掛けたときの 0.8705 が、Deep CORALの 0.6310 を大きく上回っているのです。理由ははっきりしていて、この合成データのシフトは入力空間で厳密に線形($\bm{x}\mapsto\bm{M}^\top\bm{x}+\bm{m}_T$)だからです。線形なシフトなら、閉形式解がその逆変換をほぼ言い当てます。対してDeep CORALは、16次元まで圧縮された中間層の共分散という「間接的で情報の少ない手がかり」だけを頼りに、エンコーダの重み経由でシフトを打ち消さなければなりません。Deep CORALの価値は、シフトが入力空間で線形に書けないとき(照明条件やレンダリング様式の違いなど)にこそ出るわけです。まず閉形式版を試し、それで足りないときに深層版へ進む、という順序が理にかなっています。

MMD正則化が 0.7125 とDeep CORALより良いのも、この設定では自然です。中間層にReLUが入ると特徴分布はガウス的でなくなり、二次統計だけでは捉えきれないずれが残ります。ガウスカーネルMMDはそこまで見にいくので有利です。ただし代償として、ソース精度が 0.8910 → 0.8370 と最も大きく下がっており、バンド幅の選び方次第で結果が動く点にも注意が要ります。「CORALが常に勝つ」でも「MMDが常に勝つ」でもなく、シフトの素性で決まる——このことを実験で確認できたのが、この節のいちばんの収穫です。

5つの実験を通して、CORALが効く条件と効かない条件、そして「損失だけを見ていると騙される」ことがはっきりしました。最後に、これらを実務のプロジェクトに落とし込むときの勘所を、チェックリストの形でまとめておきます。

実務で使うときのチェックリスト

最後に、CORALを実際のプロジェクトに入れるときに効いてくる勘所をまとめます。

どの特徴に掛けるか。生の入力ではなく、事前学習済みモデルの中間層特徴に掛けるのが基本です。原論文もAlexNetのfc7/fc8特徴を対象にしています。生画素にCORALを掛けても意味のある整合にはなりません。

次元と標本数のバランス。$d$ 次元の共分散を推定するには最低でも $n > d$ が必要です。特徴次元が数千あるのにターゲットのサンプルが数百しかないなら、PCAで次元を落としてからCORALを掛けるか、$\lambda$ を大きめに取ります。実験4で見たとおり、標本数が足りないと損失そのものが推定ノイズに埋もれます。

$\lambda$ の決め方。ターゲットのラベルが無い以上、$\lambda$ を検証精度でチューニングすることはできません(できるならそもそもラベルがある)。実務的には、①学習終了時に分類損失とCORAL損失が同オーダーになるよう合わせる、②$\lambda$ を 0 から徐々に上げるランプアップを使う、③ソースの検証精度が大きく落ちない範囲で最大の $\lambda$ を選ぶ、といった間接的な基準を使います。

バッチ統計の揺らぎ。ミニバッチごとに共分散を推定すると勾配が暴れます。移動平均で共分散を平滑化する、バッチサイズを大きくする、といった安定化が有効です。

対数版・log-Euclidean CORAL。共分散行列は対称正定値行列の多様体上の点なので、ユークリッド距離(フロベニウスノルム)で測るのは幾何学的には粗い近似です。$\log$ を取ってから差を測る log-Euclidean 版や、アフィン不変計量を使う版が提案されており、スケールの違いが大きいときに安定します。

まず無適応と比べる。ドメイン適応手法は「効いた気になる」ことが多い分野です。実験3で見たように、損失が下がっても精度が下がることは普通に起こります。必ず無適応ベースラインと、可能ならターゲットラベルを使ったオラクルの両方を並べて評価しましょう。

ここまでで、CORALを「理解して実装して、使いどころと落とし穴まで把握する」ところまで来ました。最後に全体を振り返ります。

まとめ

本記事では、CORAL(相関整合)によるドメイン適応を、定義から閉形式解の導出、Deep CORAL、MMDとの関係まで通しで解説しました。要点を整理します。

  • CORAL損失は $\ell_{\mathrm{CORAL}} = \frac{1}{4d^2}\|\bm{C}_S-\bm{C}_T\|_F^2$。共分散行列の差をフロベニウスノルムで測るだけの、極めて単純な規準です。$\frac{1}{4}$ は勾配式を整えるため、$\frac{1}{d^2}$ は層ごとの次元差を吸収するための係数です。
  • 線形CORALには閉形式の大域最適解がある。$\min_{\bm{A}}\|\bm{A}^\top\bm{C}_S\bm{A}-\bm{C}_T\|_F^2$ の解は $\bm{A}^\ast = \bm{C}_S^{-1/2}\bm{C}_T^{1/2}$ で、目的関数値はちょうど 0 になります。「$\bm{C}_S^{-1/2}$ で白色化し、$\bm{C}_T^{1/2}$ で再着色する」という2段のレシピそのものです。
  • 解は直交行列ぶん一意でない。$\bm{A}_{\bm{Q}} = \bm{C}_S^{-1/2}\bm{Q}\bm{C}_T^{1/2}$ も同じく最適で、CORALは根拠なく $\bm{Q}=\bm{I}$ を選んでいます。実験3で見たように、回転が支配的なシフトでは損失が2桁下がっても精度は 0.528 → 0.443 と悪化しました。損失が下がった=適応が成功した、ではありません。
  • 学習不要でも強い。20次元の合成データで、無適応 0.4995、平均そろえ 0.6465、標準化 0.6395 に対し、CORALは 0.8705。ターゲットラベルを使ったオラクル 0.8930 に迫ります。
  • Deep CORALは正則化項を足すだけ。$\mathcal{L} = \mathcal{L}_{\mathrm{CLS}} + \lambda\,\ell_{\mathrm{CORAL}}$ で、勾配は $\frac{1}{d^2(n_b-1)}\bar{\bm{F}}_S(\bm{C}_S-\bm{C}_T)$。自明解(特徴の潰れ)を防いでいるのは分類損失とのバランスです。正則化なしのネットは学習が進むほど中間層のCORAL損失が増え(390倍)、ターゲット精度がピークから転落しました。
  • シフトが線形なら閉形式版のほうが強い。実験5では閉形式CORAL 0.8705 に対しDeep CORAL 0.6310。深層版の出番は、入力空間で線形に書けないシフトのときです。
  • CORALは二次多項式カーネルMMD。$k(\bm{x},\bm{y})=(\bm{x}^\top\bm{y})^2$ に対するMMDは $\|\bm{C}_S-\bm{C}_T\|_F^2$ に一致します。ガウスカーネルMMDが全モーメントを合わせるのに対し、CORALは二次に絞ることで軽さとハイパーパラメータの少なさを得ています。

CORALは「まず試すべきベースライン」として今も現役です。閉形式版を前処理として当ててみて、それでも足りなければDeep CORAL、さらに敵対的手法や最適輸送ベースの手法へ進む、という順序が実務的です。

次のステップとして、以下の記事も参考にしてください。