DANN(敵対的ドメイン適応) — 勾配反転でドメイン不変な特徴を学ぶ

ドメイン汎化とLODOCVでは、「学習した環境と違う環境ではモデルが滑る」問題を見ました。そこではターゲット環境のデータが一切ない設定でしたが、現実にはターゲット環境のデータが(ラベルは無いものの)手に入ることがよくあります。たとえば「新しい工場のセンサーデータは大量に取れるが、正解ラベルを付ける人手がない」といった状況です。

このとき、ラベルのないターゲットデータを使ってモデルをその環境に寄せていくのが ドメイン適応(domain adaptation) です。そして、その最も有名で美しい手法が DANN(Domain-Adversarial Neural Network, 敵対的ドメイン適応) です。DANNのアイデアは一言で言えば——「ソース環境とターゲット環境を見分けられないような特徴を学べ」。両環境を区別できない特徴なら、それは環境によらない本質的な特徴(ドメイン不変表現)のはずで、ソースで学んだ分類器がターゲットでもそのまま通用する、という発想です。

この「見分けられないように学ぶ」を、敵対的学習勾配反転層(GRL)という巧妙な仕掛けで実現します。本記事では、DANNの構造・目的関数・理論的根拠を解説し、PyTorchで「ソースのみで学習した場合」と「DANN」のターゲット精度を実測して比べます。転移可能性と負の転移のドメイン距離の議論と地続きなので、あわせて読むと理解が深まります。

DANNの概念

図がDANNの狙いです。ラベルありのソースとラベル無しのターゲットから、両者に共通するドメイン不変な特徴表現を学び、ターゲットでも高精度に分類する——これをどう実現するかが本記事のテーマです。

DANNの構造

DANNのネットワークは3つの部品からなります。

DANNの構造

  • 特徴抽出器 $G_f$:入力 $x$ を特徴ベクトルに変換する。ここが「ドメイン不変な特徴」を生む主役。
  • ラベル予測器 $G_y$:特徴からクラスを分類する。ソースのラベルで学習する(通常の分類器)。
  • ドメイン判別器 $G_d$:特徴を見て「これはソースか、ターゲットか」を当てる。

ポイントは、特徴抽出器の出力が2つの頭(ラベル予測器とドメイン判別器)に分岐することです。ラベル予測器は「クラスをうまく当てたい」、ドメイン判別器は「ソースかターゲットかを当てたい」。そして特徴抽出器には、ラベル予測には協力しつつ、ドメイン判別器を妨害するという矛盾した役割が課されます。この「妨害」をどう実装するかが、DANNの核心である勾配反転層です。

勾配反転層(GRL)

特徴抽出器に「ドメイン判別器を負かす」よう学習させたい。普通に考えると、判別器の損失を増やす方向に特徴抽出器を更新すればよいのですが、それを通常の誤差逆伝播の中で自然に行う仕掛けが 勾配反転層(Gradient Reversal Layer, GRL) です。

勾配反転層

GRLは、特徴抽出器とドメイン判別器の間に挟む特殊な層で、次のように振る舞います。

  • 順伝播(forward):何もせず、入力をそのまま通す(恒等写像)。
  • 逆伝播(backward):流れてくる勾配に $-\lambda$ を掛けて符号を反転させる。

順伝播では素通りなので、ドメイン判別器は普通に「ソース/ターゲット」を学習します。ところが逆伝播では勾配が反転されて特徴抽出器に届くため、特徴抽出器は判別器の損失を増やす(=判別器を混乱させる)方向に更新されます。たった1つの層を挟むだけで、「判別器は見分けようと最適化し、特徴抽出器は見分けられなくしようと最適化する」という敵対関係が、ひとつの逆伝播で同時に実現するのです。実装上もforwardは恒等・backwardは符号反転を書くだけで済む、という簡潔さがDANNの魅力です。

敵対的ゲーム

この敵対的ゲームの均衡点では、ドメイン判別器がどう頑張ってもソースとターゲットを見分けられない状態——つまり特徴空間で両ドメインの分布が重なったドメイン不変な特徴——が得られます。GAN(敵対的生成ネットワーク)と同じ「2者の競争で望ましい状態に追い込む」発想を、ドメイン適応に持ち込んだものと理解できます。

目的関数

DANN全体の目的関数は、2つの損失の組み合わせです。

DANNの目的関数

$$ \begin{equation} E(\theta_f, \theta_y, \theta_d) = L_y(\theta_f, \theta_y) – \lambda\, L_d(\theta_f, \theta_d) \end{equation} $$

ここで $L_y$ はソースでのラベル分類損失、$L_d$ はドメイン判別損失です。最適化は次のように行います。

  • ラベル予測器 $\theta_y$ と特徴抽出器 $\theta_f$ は、$L_y$ を最小化(分類をうまく当てる)。
  • ドメイン判別器 $\theta_d$ は、$L_d$ を最小化(ドメインをうまく当てる)。
  • しかし特徴抽出器 $\theta_f$ は、$L_d$ を最大化(判別器を負かす)——ここがGRLによる符号反転で実現される部分です。

$\lambda$ は、ドメイン整合をどれだけ重視するかを決めるトレードオフ係数です。$\lambda=0$ なら単なるソース分類器(ドメイン適応なし)、$\lambda$ を大きくするほどドメイン整合の圧力が強まります。式の上では「$L_y$ を下げ、$\lambda L_d$ を引く」という1本の目的関数に、相反する3者の最適化がきれいに畳み込まれています。

なぜ効くのか — 理論的根拠

DANNが効く理由は、ドメイン適応の理論的なバウンドに裏打ちされています。

理論的根拠

ターゲットドメインでの誤差 $\varepsilon_T$ は、おおよそ次の形で上から抑えられることが知られています。

$$ \begin{equation} \varepsilon_T(h) \leq \varepsilon_S(h) + \frac{1}{2}d_{\mathcal{H}\triangle\mathcal{H}}(\mathcal{D}_S, \mathcal{D}_T) + \lambda^* \end{equation} $$

第1項はソースでの誤差、第2項は転移可能性の記事でも登場したドメイン間距離、第3項は両ドメインで同時に良い予測器が存在するか(理想誤差)です。ターゲット誤差を下げるには、第1項(ソース誤差)と第2項(ドメイン距離)の両方を小さくすればよい。

DANNはまさにこれを直接実装しています。ラベル予測器が第1項を、ドメイン判別器(の敗北)が第2項を最小化する。ドメイン判別器が両ドメインを見分けられないということは、特徴空間でのドメイン距離が小さいということに他なりません。理論が指し示す2つの量を、2つの頭で同時に下げにいく——これがDANNの設計思想です。

特徴空間の変化

学習の前後で特徴空間がどう変わるかが図です。学習前はソースとターゲットの特徴が分離していますが、DANN後は両ドメインの分布が重なります。この「重なり」こそがドメイン距離の縮小であり、ソース分類器がターゲットでも通用する理由です。

ただし、これを安定して達成するには学習の進め方に工夫が要ります。

λのスケジューリング

学習の最初から $\lambda$ を大きくすると、まだ特徴が育っていないうちにドメイン整合の圧力がかかり、学習が不安定になります。そこで実務では、図のように $\lambda$ を0から徐々に大きくするスケジューリングを使います。序盤はまず分類を学ばせ、特徴が育ってから徐々にドメイン整合を強める、という段取りです。

PyTorchで確かめる

ソースを通常の2つの三日月(make_moons)、ターゲットをそれを25度回転させたものとして、共変量シフトを作ります。ターゲットのラベルは学習に使いません(ドメイン適応の設定)。「ソースのみで学習した分類器」と「DANN」で、ターゲット精度を比べます。

まず、勾配反転層とネットワークを定義します。

import numpy as np, torch, torch.nn as nn
from sklearn.datasets import make_moons

class GRL(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, lam):
        ctx.lam = lam
        return x.view_as(x)                 # 順伝播:恒等
    @staticmethod
    def backward(ctx, g):
        return -ctx.lam * g, None           # 逆伝播:勾配を -λ 倍

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.feat = nn.Sequential(nn.Linear(2, 32), nn.ReLU(),
                                  nn.Linear(32, 32), nn.ReLU())   # 特徴抽出器 G_f
        self.cls = nn.Linear(32, 2)                                # ラベル予測器 G_y
        self.dom = nn.Sequential(nn.Linear(32, 32), nn.ReLU(),
                                 nn.Linear(32, 2))                 # ドメイン判別器 G_d
    def forward(self, x, lam=0.0):
        f = self.feat(x)
        return self.cls(f), self.dom(GRL.apply(f, lam))            # 分岐

次にデータを作り、学習ループを書きます。use_dann=True のときだけドメイン判別損失とGRLを有効にします。

def rot(X, deg):
    th = np.deg2rad(deg)
    R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
    return X @ R.T

Xs, ys = make_moons(600, noise=0.1, random_state=0)              # ソース
Xt, yt = make_moons(600, noise=0.1, random_state=1); Xt = rot(Xt, 25)  # ターゲット(25度回転)
mu, sd = Xs.mean(0), Xs.std(0)
Xs = (Xs - mu) / sd; Xt = (Xt - mu) / sd
Xs_t = torch.tensor(Xs, dtype=torch.float32); ys_t = torch.tensor(ys)
Xt_t = torch.tensor(Xt, dtype=torch.float32); yt_t = torch.tensor(yt)

def acc(m, X, y):
    m.eval()
    with torch.no_grad():
        return (m(X)[0].argmax(1) == y).float().mean().item()

def train(use_dann, epochs=400):
    torch.manual_seed(0)
    m = Net(); opt = torch.optim.Adam(m.parameters(), lr=1e-3); ce = nn.CrossEntropyLoss()
    for ep in range(epochs):
        p = ep / epochs
        lam = (2/(1+np.exp(-10*p)) - 1) if use_dann else 0.0      # λスケジューリング
        cy, _ = m(Xs_t); Ly = ce(cy, ys_t)                        # ソースのラベル損失
        if use_dann:
            _, ds = m(Xs_t, lam); _, dt = m(Xt_t, lam)
            dom_x = torch.cat([ds, dt])
            dom_y = torch.cat([torch.zeros(len(ds)), torch.ones(len(dt))]).long()
            loss = Ly + ce(dom_x, dom_y)                          # +ドメイン判別損失
        else:
            loss = Ly
        opt.zero_grad(); loss.backward(); opt.step()
    return m

m0 = train(False); m1 = train(True)
print(f"ソースのみ: target acc = {acc(m0, Xt_t, yt_t):.3f}")
print(f"DANN      : target acc = {acc(m1, Xt_t, yt_t):.3f}")

出力は次の通りです。

ソースのみ: target acc = 0.828
DANN      : target acc = 0.962

DANNの結果

結果は明快です。ソースだけで学習した分類器はターゲットで 0.828 にとどまりますが、ラベルを一切使わずターゲットの「ドメイン不変な特徴」を学んだDANNは 0.962 まで改善しました。ターゲットの正解ラベルを1つも使っていないにもかかわらず、未ラベルデータの分布情報だけで13ポイント以上の改善です。判別器を負かすことでソースとターゲットの特徴を重ね、ソース分類器をターゲットでも通用させる——DANNの狙いが数値で確認できました。

他の特徴整合手法との違い

「ドメイン不変な特徴を学ぶ」という目標は同じでも、ドメイン距離をどう測り・どう縮めるかには複数の流派があります。DANNを相対化するために、代表的な仲間を並べておきます。

  • DANN(敵対的整合):ドメイン判別器という「学習される距離尺度」を使い、判別器を負かすことで距離を縮める。距離の測り方そのものをデータから学べるのが強み。
  • CORAL(相関整合, CORrelation ALignment):ソースとターゲットの特徴の2次統計量(共分散行列)を一致させる。敵対的学習なしで、共分散の差のフロベニウスノルムを損失に加えるだけ。実装が単純で安定する反面、2次までの情報しか揃えられない。
  • MMD(最大平均食い違い, Maximum Mean Discrepancy):カーネルを使って、2つの分布の任意の次数のモーメントの差を測る。これを最小化して分布を揃える(Deep CORALやDANと呼ばれる手法群)。

ざっくり言えば、CORAL/MMDは「あらかじめ決めた数式で分布の距離を測って縮める」のに対し、DANNは「距離を測るネットワーク(判別器)ごと学習する」アプローチです。判別器が任意の複雑な違いを捉えうるぶんDANNは表現力が高い一方、敵対的学習ゆえの不安定さを抱えます。CORALは安定だが捉えられる違いが限定的——という対照的なトレードオフがあり、問題に応じて使い分けます。いずれも「特徴空間で分布を寄せる」というドメイン汎化で触れた不変表現の系譜に属します。

位置づけと注意点

DANNはドメイン適応の手法であり、ドメイン汎化とは前提が異なります。

DAとDGの位置づけ

  • DANN(ドメイン適応):ターゲットの未ラベルデータが使える前提。それを使って特徴を揃える。
  • ドメイン汎化:ターゲットのデータが一切無い前提。未知環境一般に備える。

実務での注意点もまとめます。

  • シフトが大きすぎると逆効果:ソースとターゲットの分布が大きく食い違う(ラベルの対応が崩れる)と、無理に特徴を揃えることでかえって分類が壊れます。本記事でも回転を大きくしすぎる(35度など)とDANNが悪化することを確認しています。「揃えられる程度のシフト」が前提です。
  • λの調整が肝心:強すぎるドメイン整合は分類性能を犠牲にします。スケジューリングで徐々に上げるのが定石。
  • ラベルシフトに弱い:ソースとターゲットでクラス比率が大きく違うと、特徴を揃える操作がクラス分布の不一致を悪化させることがあります。
  • 学習の不安定さ:敵対的学習ゆえにGANと同様、学習が振動・不安定になることがあります。

特徴を「環境で見分けられないように」鍛えることでドメインの壁を越える——DANNは、敵対的学習というアイデアがドメイン適応にもたらした転換点であり、その後の数多くの手法の出発点になりました。

まとめ

DANN(敵対的ドメイン適応)を、理論から実装まで解説しました。

  • DANNは、ソースとターゲットを見分けられないドメイン不変な特徴を学ぶことで、ラベル無しのターゲットへ分類器を適応させる手法。
  • 構造は特徴抽出器・ラベル予測器・ドメイン判別器の3部品。特徴抽出器は「分類に協力しつつ判別器を妨害する」。
  • 勾配反転層(GRL)は順伝播で恒等・逆伝播で勾配を $-\lambda$ 倍する層。これ1つで敵対的最適化を1本の逆伝播に畳み込む。
  • 目的関数 $E = L_y – \lambda L_d$ は、理論バウンドの「ソース誤差」と「ドメイン距離」を同時に下げる設計。
  • 実測では、ソースのみ target 0.828 に対し、DANNは 0.962(ターゲットのラベル不使用で13ポイント改善)。ただしシフトが大きすぎると逆効果になる点に注意。

次のステップとして、ドメイン距離を別の方法で測るCORAL(相関整合)や、スプリアス相関を排除するIRM(不変リスク最小化)との比較に進むと、分布シフト対策の全体像がさらに鮮明になります。