対照学習による事前学習 — 正例・負例ペアで表現を学ぶ

ImageNetの1,400万枚の画像に、1,000カテゴリのラベルを付ける作業にはどれほどのコストがかかったでしょうか。Amazon Mechanical Turkを通じて数万人の作業者が数年がかりで取り組み、その費用は数百万ドルに達したとされています。しかし、インターネット上には毎日数十億枚の画像がアップロードされています。もしラベルなしの画像だけで「犬と猫は違う」「同じ犬でも角度が変われば見た目は変わるが本質は同じ」といった表現を自動的に学べるとしたら、ラベル付けのボトルネックを一気に解消できます。

この発想を実現するのが対照学習(Contrastive Learning)による事前学習です。対照学習の基本原理は驚くほどシンプルです。「同じものから作ったペア(正例)は特徴空間で近づけ、異なるものから作ったペア(負例)は遠ざける」 — たったこれだけのルールで、モデルはラベルなしデータから意味のある表現を獲得します。

対照学習による事前学習を理解すると、以下の分野で応用が利きます。

  • 自己教師あり学習(Self-Supervised Learning): ラベルなしデータから高品質な特徴抽出器を事前学習し、少量のラベル付きデータで下流タスクをファインチューニングできます。医療画像や衛星画像など、ラベルが希少な領域で特に威力を発揮します
  • ゼロショット認識: CLIPのように画像とテキストの対照学習を行えば、学習時に見たことのないカテゴリの画像を、テキストプロンプトだけで分類できます
  • 検索・推薦システム: テキスト同士、画像同士、あるいはテキストと画像のペアの類似度を、対照学習で獲得した埋め込み空間上のコサイン類似度として計算できます
  • 転移学習の基盤: SimCLRやMoCoで事前学習したエンコーダは、物体検出、セグメンテーション、画像生成など、幅広い下流タスクの初期重みとして機能します

本記事の内容

  • 対照学習の基本原理 — 正例・負例の考え方と引力・斥力のアナロジー
  • データ拡張による正例ペアの生成戦略
  • InfoNCE損失の数学的導出と相互情報量との関係
  • SimCLRのアーキテクチャと学習手順
  • MoCoのモメンタムエンコーダとキュー構造
  • BYOL / SimSiamの負例なし対照学習
  • CLIPの画像-テキスト対照学習とゼロショット認識
  • PyTorchによるSimCLR風フレームワークの実装
  • 温度パラメータの効果の可視化と考察

前提知識

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

画像なし
対照学習(Contrastive Learning)の理論と損失関数の導出
対照学習の基本概念とInfoNCE損失の数学的導出を解説しています。
画像なし
CLIPの対照学習を数式から理解してPythonで実装する
画像とテキストの対照学習、InfoNCE損失、ゼロショット分類の仕組みを解説しています。
画像なし
埋め込みベクトルと類似度検索の理論とPython実装
コサイン類似度やユークリッド距離など、ベクトル間の類似度計算の数学を解説しています。

対照学習の基本原理 — 引力と斥力のアナロジー

対照学習の核心を一言で表すなら、「正例を引き寄せ、負例を押し離す」です。この動作は、物理学における引力と斥力のアナロジーで理解できます。

太陽系を想像してください。太陽(アンカー)と地球(正例)の間には万有引力が働き、地球は太陽の近くに引き寄せられます。一方、同じ電荷を持つ粒子同士(負例)は静電的な斥力によって互いに遠ざかります。対照学習はこの2つの力を同時に働かせ、特徴空間上にデータの意味的な構造を形成するのです。

対照学習の引力と斥力

イメージは上の図の通りです。アンカー $z$ に対し、正例 $z^+$ は引力で引き寄せ、複数の負例 $z^-$ は斥力で押し離します。この2つの力のバランスで、意味的に似たものが近く・異なるものが遠くに配置される特徴空間が形づくられます。

もう少し具体的に見てみましょう。1枚の犬の画像 $\bm{x}$ を考えます。この画像をランダムにクロップし、色調を変え、水平反転させた画像 $\bm{x}^+$ を作ります。もとの画像 $\bm{x}$ と変換後の画像 $\bm{x}^+$ は見た目は異なりますが、本質的には「同じ犬」を表しています。これが正例ペア(positive pair)です。一方、バッチ内の他の画像(猫、車、風景など)から作った変換画像 $\bm{x}^-_1, \bm{x}^-_2, \dots$ は、アンカーとは異なる対象を表しています。これが負例(negative samples)です。

対照学習のエンコーダ $f$ は、これらの入力を特徴ベクトルに変換します。

$$ \bm{z} = f(\bm{x}), \quad \bm{z}^+ = f(\bm{x}^+), \quad \bm{z}^-_k = f(\bm{x}^-_k) $$

学習の目標は、以下の2つの条件を同時に満たすことです。

$$ \text{sim}(\bm{z}, \bm{z}^+) \gg \text{sim}(\bm{z}, \bm{z}^-_k) \quad \forall k $$

ここで $\text{sim}(\cdot, \cdot)$ はコサイン類似度などの類似度関数です。つまり、アンカーと正例の類似度を高くし、アンカーと負例の類似度を低くするように、エンコーダのパラメータを更新します。

この「引力と斥力」の枠組みは、驚くほど多くの場面に適用できます。画像同士のペアだけでなく、テキスト同士、画像とテキスト、音声とテキストなど、任意のモダリティ間で正例・負例のペアを定義すれば、対照学習を適用できます。ただし、正例ペアをどう作るかが手法ごとの最大の差異であり、この設計が最終的な表現の品質を大きく左右します。

では、画像領域で最も広く使われている「データ拡張による正例ペア生成」の具体的な戦略を見ていきましょう。

データ拡張による正例ペア — 同じ画像の異なるビュー

画像の対照学習では、1枚の画像に2種類の異なるデータ拡張を適用して、正例ペアを生成するのが標準的なアプローチです。この考え方は「マルチビュー(multi-view)」と呼ばれます。

人間は、同じ犬を斜めから見ても、暗い部屋で見ても、写真を回転させても、「同じ犬だ」と認識できます。対照学習は、この人間の不変性認識をモデルに教え込む仕組みだと言えます。エンコーダは「見た目の違いを無視して、本質的な同一性を捉える」ことを強いられるのです。

SimCLR(Chen et al., 2020)は、以下のデータ拡張の組み合わせを体系的に検証し、効果的な変換セットを特定しました。

主要なデータ拡張手法

  1. ランダムクロップとリサイズ(Random Resized Crop): 画像のランダムな領域を切り出し、元のサイズにリサイズします。これは、物体の位置やスケールに対する不変性を学習させる最も重要な変換です。切り出し領域のサイズは、元画像の8%から100%の範囲でランダムに決まります

  2. 色ジッター(Color Jitter): 輝度、コントラスト、彩度、色相をランダムに変化させます。照明条件や色の変動に対する頑健性を獲得するために重要です

  3. ガウシアンぼかし(Gaussian Blur): ガウスカーネルでぼかしを適用します。テクスチャではなく物体の形状に着目する表現を促します

  4. 水平反転(Random Horizontal Flip): 50%の確率で左右を反転させます。多くの自然画像では左右反転しても意味は変わらないため、水平方向の位置に依存しない表現を学習できます

  5. グレースケール変換(Random Grayscale): ランダムにグレースケールに変換します。色情報に過度に依存せず、形状やテクスチャから識別する表現を促します

データ拡張の組み合わせの重要性

SimCLRの論文で特に重要な発見の1つは、個別のデータ拡張よりも、複数の拡張を組み合わせることが圧倒的に重要だという点です。特に「ランダムクロップ」と「色ジッター」の組み合わせが劇的に性能を向上させました。

なぜこの2つの組み合わせが特別に重要なのでしょうか。ランダムクロップだけを使うと、モデルは切り出された領域の色分布(カラーヒストグラム)を手がかりにして正例ペアを見分けることができてしまいます。これは「ショートカット(shortcut)」と呼ばれ、モデルが本質的な特徴ではなく表面的な手がかりに依存してしまう現象です。色ジッターを加えることで、このショートカットが無効化され、モデルはより高次の意味的特徴を学習せざるを得なくなります。

数学的に表現すると、データ拡張の確率分布 $\mathcal{T}$ から2つの変換 $t \sim \mathcal{T}$ と $t’ \sim \mathcal{T}$ を独立にサンプリングし、1つの画像 $\bm{x}$ に適用して正例ペアを作ります。

$$ \tilde{\bm{x}}_i = t(\bm{x}), \quad \tilde{\bm{x}}_j = t'(\bm{x}) $$

ここで $t$ と $t’$ は独立にサンプリングされるため、2つのビューは一般に異なる見た目になります。しかし、もとの画像 $\bm{x}$ は同じなので、意味的には同一です。この「見た目は異なるが本質は同じ」というペアをエンコーダに与え、類似した特徴ベクトルを出力するよう訓練することで、表面的な変動に頑健な表現が獲得されるのです。

データ拡張による正例ペア

図のように、1枚の画像 $x$ に独立な2つの拡張 $t,t’$ をかけて2つのビューを作り、これを正例ペアとします。特にランダムクロップと色ジッターの組み合わせが重要で、色ヒストグラムだけで見分ける「ショートカット」を封じ、より意味的な特徴の学習を促します。

データ拡張の設計が表現の質を決定づけることがわかりました。次に、「正例を近づけ、負例を遠ざける」という目標を実現するための損失関数 — InfoNCE損失の数学的構造を詳しく見ていきましょう。

InfoNCE損失の数学 — ソフトマックスと相互情報量

直感的な理解 — N+1択の識別問題

InfoNCE損失は、一見すると複雑な数式に見えますが、本質は「1つの正解を $N$ 個の不正解の中から見つけ出す多クラス分類」です。

日常的な場面で考えてみましょう。あなたが友人の声を電話で聞いているとします。目の前に $N+1$ 人の人が並んでおり、その中から電話の声の主を当てなければなりません。声が似ている人が多いほど難しく、友人の声の特徴をより正確に捉える必要があります。InfoNCE損失は、この「声の主を当てる」タスクの正解確率を最大化する損失関数です。

数学的定式化

アンカーサンプルの特徴ベクトルを $\bm{z}$、正例の特徴ベクトルを $\bm{z}^+$、$K$ 個の負例の特徴ベクトルを $\{\bm{z}^-_1, \bm{z}^-_2, \dots, \bm{z}^-_K\}$ とします。InfoNCE損失は以下のように定義されます。

$$ \mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(\bm{z}, \bm{z}^+) / \tau)}{\exp(\text{sim}(\bm{z}, \bm{z}^+) / \tau) + \sum_{k=1}^{K} \exp(\text{sim}(\bm{z}, \bm{z}^-_k) / \tau)} $$

ここで $\text{sim}(\bm{u}, \bm{v})$ はコサイン類似度です。

$$ \text{sim}(\bm{u}, \bm{v}) = \frac{\bm{u} \cdot \bm{v}}{\|\bm{u}\| \|\bm{v}\|} $$

$\tau > 0$ は温度パラメータ(temperature parameter)と呼ばれるハイパーパラメータです。この式の構造をより深く理解するために、段階的に分解してみましょう。

ソフトマックスとの対応

InfoNCE損失の分数部分は、実はソフトマックス関数そのものです。全てのサンプル $\{\bm{z}^+, \bm{z}^-_1, \dots, \bm{z}^-_K\}$ に対するロジットを $s_i = \text{sim}(\bm{z}, \bm{z}_i) / \tau$ と定義すると、正例のソフトマックス確率は次のように書けます。

$$ p(\text{positive} \mid \bm{z}) = \frac{\exp(s^+)}{\exp(s^+) + \sum_{k=1}^{K} \exp(s^-_k)} = \text{softmax}(s^+) $$

InfoNCE損失はこの確率の負の対数です。

$$ \mathcal{L}_{\text{InfoNCE}} = -\log p(\text{positive} \mid \bm{z}) $$

これは、正例を「クラス0」、各負例を「クラス1, 2, …, $K$」と見なした $(K+1)$ クラスの交差エントロピー損失に他なりません。通常の分類タスクと異なるのは、クラスラベルが固定されておらず、バッチごとに正例・負例の組み合わせが変わる点です。

InfoNCEはN+1択の識別

図のように、アンカーと各候補の類似度を $\tau$ で割ってsoftmaxにかけ、正例(緑)の確率を最大化します。これは $(K{+}1)$ クラスの交差エントロピーそのもので、負例が多いほど難しい識別問題になり、良い表現が要求されます。

温度パラメータ $\tau$ の役割

温度パラメータ $\tau$ は、ソフトマックス分布の「鋭さ」を制御します。

$\tau$ が小さいとき(例: $\tau = 0.05$)、ソフトマックス分布は鋭くなり、最大の類似度を持つサンプルに確率が集中します。これは「ハードな」識別を意味し、正例と負例の区別を厳密に行います。微妙な差異に敏感になりますが、学習が不安定になる可能性があります。

$\tau$ が大きいとき(例: $\tau = 1.0$)、ソフトマックス分布は平坦になり、全てのサンプルにほぼ均等な確率が割り当てられます。これは「ソフトな」識別を意味し、学習は安定しますが、細かい区別が曖昧になります。

数式で確認しましょう。$\tau \to 0$ の極限を取ると、ソフトマックスはargmax(最大値を取るインデックスに確率1を集中)に近づきます。

$$ \lim_{\tau \to 0} \frac{\exp(s_i / \tau)}{\sum_j \exp(s_j / \tau)} = \begin{cases} 1 & \text{if } i = \arg\max_j s_j \\ 0 & \text{otherwise} \end{cases} $$

逆に $\tau \to \infty$ の極限では、全ての確率が均一になります。

$$ \lim_{\tau \to \infty} \frac{\exp(s_i / \tau)}{\sum_j \exp(s_j / \tau)} = \frac{1}{K + 1} $$

SimCLRでは $\tau = 0.5$、MoCoでは $\tau = 0.07$ が標準的に使われています。MoCoの方が小さい温度を使えるのは、大きなキューによって十分な負例が確保されており、ハードな識別でも学習が安定するためです。

相互情報量との関係

InfoNCEという名前の「Info」は、情報理論の相互情報量(Mutual Information)に由来しています。Oord et al.(2018)は、InfoNCE損失の期待値が相互情報量の下界であることを示しました。

2つの確率変数 $X$ と $Y$ の相互情報量 $I(X; Y)$ は、一方を知ることで他方の不確実性がどれだけ減るかを測る量です。

$$ I(X; Y) = \mathbb{E}_{p(x, y)} \left[ \log \frac{p(x, y)}{p(x)p(y)} \right] $$

InfoNCE損失を $K$ 個の負例で計算するとき、その最適値は以下の下界を与えます。

$$ I(X; Y) \geq \log K – \mathcal{L}_{\text{InfoNCE}} $$

この不等式の導出を追ってみましょう。まず、正例ペア $(x, y)$ が同時分布 $p(x, y)$ からサンプリングされ、$K$ 個の負例 $y^-_1, \dots, y^-_K$ が周辺分布 $p(y)$ から独立にサンプリングされるとします。InfoNCE損失の期待値は次のように書けます。

$$ \mathbb{E}[\mathcal{L}_{\text{InfoNCE}}] = -\mathbb{E} \left[ \log \frac{f(\bm{x}, \bm{y})}{f(\bm{x}, \bm{y}) + \sum_{k=1}^{K} f(\bm{x}, \bm{y}^-_k)} \right] $$

ここで $f(\bm{x}, \bm{y}) = \exp(\text{sim}(g(\bm{x}), g(\bm{y})) / \tau)$ は、ペアの適合度を測るスコア関数です。$f$ が密度比 $p(y | x) / p(y)$ に比例するとき、この損失の最適値は $\log(K+1) – I(X; Y)$ となります。両辺を整理すると、上の下界が得られます。

$$ I(X; Y) \geq \log(K + 1) – \mathcal{L}_{\text{InfoNCE}}^{*} \approx \log K – \mathcal{L}_{\text{InfoNCE}}^{*} $$

この結果は重要な意味を持ちます。InfoNCE損失を最小化することは、入力の2つのビュー間の相互情報量の下界を最大化することに対応しているのです。つまり、対照学習は「正例ペアが共有する情報を最大限に保存する表現」を学習していると解釈できます。

ただし、下界の精度は負例の数 $K$ に依存します。$K$ が大きいほど下界がタイトになり、相互情報量のより良い推定が得られます。これが、大きなバッチサイズや大きなネガティブプールが対照学習の性能に直結する理由の1つです。

InfoNCE損失の数学的な意味がわかったところで、この損失関数を中核に据えた具体的なフレームワーク — SimCLRのアーキテクチャを詳しく見ていきましょう。

SimCLR — シンプルで強力な対照学習フレームワーク

SimCLRの設計思想

SimCLR(A Simple Framework for Contrastive Learning of Visual Representations, Chen et al., 2020)は、その名の通り「シンプルさ」を追求した対照学習フレームワークです。メモリバンクやモメンタムエンコーダなどの複雑な仕組みを一切使わず、大きなバッチサイズ適切なデータ拡張、そしてプロジェクションヘッドの3つの要素だけで、教師あり学習に迫る性能を達成しました。

SimCLRの直感は「同じ画像の2つの見方を似せ、異なる画像の見方を区別する」というものです。料理のレシピに例えるなら、SimCLRは「素材(データ拡張)の質にこだわり、調理法(アーキテクチャ)はシンプルにする」という哲学です。

アーキテクチャの全体像

SimCLRのアーキテクチャは、4つのコンポーネントで構成されます。

1. データ拡張モジュール $\mathcal{T}$: 入力画像 $\bm{x}$ に対して、ランダムな変換 $t \sim \mathcal{T}$ と $t’ \sim \mathcal{T}$ を独立に適用し、2つのビュー $\tilde{\bm{x}}_i = t(\bm{x})$ と $\tilde{\bm{x}}_j = t'(\bm{x})$ を生成します。

2. エンコーダ $f(\cdot)$: ResNet-50などのCNNバックボーンで、変換後の画像を特徴ベクトルに変換します。

$$ \bm{h}_i = f(\tilde{\bm{x}}_i) \in \mathbb{R}^{d_h} $$

ResNet-50の場合、Global Average Pooling後の2048次元ベクトルが $\bm{h}_i$ になります。

3. プロジェクションヘッド $g(\cdot)$: エンコーダの出力を、対照学習用の低次元空間に射影する2層MLPです。

$$ \bm{z}_i = g(\bm{h}_i) = W^{(2)} \sigma(W^{(1)} \bm{h}_i + \bm{b}^{(1)}) + \bm{b}^{(2)} $$

ここで $\sigma$ はReLU活性化関数、$\bm{z}_i \in \mathbb{R}^{d_z}$ は射影後の特徴ベクトル(通常 $d_z = 128$)です。

4. 対照損失: プロジェクションヘッドの出力 $\bm{z}$ に対してInfoNCE損失(NT-Xent損失とも呼ばれます)を計算します。

プロジェクションヘッドの重要性

SimCLRの重要な発見の1つが、プロジェクションヘッドの存在が性能を大幅に向上させるという点です。これは直感に反するかもしれません。なぜ余分な層を追加して、情報を圧縮した低次元空間で対照学習を行う方が良いのでしょうか。

その理由は、対照学習の損失関数がデータ拡張に対して不変な特徴のみを保存するよう圧力をかけるためです。エンコーダの出力 $\bm{h}$ には、色やテクスチャなど下流タスクに有用な情報が含まれていますが、InfoNCE損失はこれらの情報を「データ拡張に対する変動要因」として捨てようとします。プロジェクションヘッド $g$ を介在させることで、情報の圧縮(不変量の抽出)を $g$ の出力空間 $\bm{z}$ で行い、エンコーダの出力 $\bm{h}$ にはより豊富な情報を残すことができます。

実験的にも、下流タスクでは $\bm{z}$ ではなく $\bm{h}$ を使った方が性能が高いことが確認されています。Chen et al. の実験では、線形評価(エンコーダを固定して線形分類器のみ学習)において、$\bm{h}$ を使うと $\bm{z}$ を使う場合よりも10%以上精度が向上しました。つまり、プロジェクションヘッドは学習時の「使い捨ての道具」であり、学習後は取り除いてエンコーダ $f$ のみを利用するのです。

SimCLRのパイプライン

SimCLRの全体像は上の図の通りです。1枚の画像から2ビューを作り、共有エンコーダ $f$ と射影ヘッド $g$ を通して $z_i,z_j$ を得てInfoNCEで揃えます。重要なのは、損失は $z$ で計算するのに、下流タスクでは射影前の $h$ を使う点です。$g$ が拡張依存の情報を引き受け、$h$ に豊富な情報を残します。

学習手順

バッチサイズ $N$ の場合、SimCLRの学習手順は以下の通りです。

ステップ1: ミニバッチからランダムに $N$ 枚の画像をサンプリングします。

ステップ2: 各画像に2つのランダムな変換を適用し、$2N$ 個のビューを生成します。画像 $\bm{x}_k$ から生成された2つのビュー $\tilde{\bm{x}}_{2k-1}$ と $\tilde{\bm{x}}_{2k}$ が正例ペアです。

ステップ3: $2N$ 個のビュー全てをエンコーダとプロジェクションヘッドに通し、$2N$ 個の特徴ベクトル $\{\bm{z}_1, \bm{z}_2, \dots, \bm{z}_{2N}\}$ を得ます。

ステップ4: 正例ペア $(i, j)$ に対して、残りの $2(N-1)$ 個のビューを負例として、以下の損失を計算します。

$$ \ell(i, j) = -\log \frac{\exp(\text{sim}(\bm{z}_i, \bm{z}_j) / \tau)}{\sum_{k=1}^{2N} \mathbb{1}_{[k \neq i]} \exp(\text{sim}(\bm{z}_i, \bm{z}_k) / \tau)} $$

ここで $\mathbb{1}_{[k \neq i]}$ はインジケータ関数で、アンカー自身を分母から除外します。

ステップ5: 全ての正例ペアに対する損失の平均を最終的な損失とします。

$$ \mathcal{L} = \frac{1}{2N} \sum_{k=1}^{N} [\ell(2k-1, 2k) + \ell(2k, 2k-1)] $$

各正例ペア $(2k-1, 2k)$ について、$2k-1$ をアンカーにした損失と $2k$ をアンカーにした損失の両方を計算し、対称的に扱います。

バッチサイズの重要性

SimCLRの性能はバッチサイズに強く依存します。バッチサイズ $N$ が大きいほど、各アンカーに対する負例の数が $2(N-1)$ と増加し、InfoNCE損失による相互情報量の下界がタイトになります。

SimCLRの論文では、バッチサイズを256から8192まで変化させた実験が報告されています。バッチサイズ256ではImageNetの線形評価で約62%の精度でしたが、バッチサイズ4096では約69%、バッチサイズ8192では約69.3%に達しました。バッチサイズを32倍にすることで7ポイント以上の精度向上が得られたのです。

しかし、大きなバッチサイズは大量のGPUメモリを必要とします。バッチサイズ8192でResNet-50を学習するには、32台のTPU v3が必要だったと報告されています。この計算資源の問題に対する解答が、次に紹介するMoCoのモメンタムエンコーダとキュー構造です。

SimCLRの弱点である「巨大バッチサイズへの依存」を解決するために、Meta(旧Facebook AI Research)のKaiming He らは全く異なるアプローチを提案しました。それがMoCo(Momentum Contrast)です。

MoCo — モメンタムエンコーダとキュー構造

SimCLRの限界を超える

SimCLRの限界は明確です。負例の数を増やすにはバッチサイズを大きくするしかなく、バッチサイズはGPUメモリによって制約されます。バッチサイズ8192は、一般的な研究機関にとって容易に用意できる規模ではありません。

MoCo(Momentum Contrast for Unsupervised Visual Representation Learning, He et al., 2020)は、この問題を辞書引き(dictionary look-up)の視点から再解釈することで解決しました。MoCoの基本的なアイデアは、「大きなバッチを使う代わりに、過去のバッチから計算した特徴ベクトルを再利用する」ことです。本棚に例えるなら、SimCLRは巨大な本棚を毎回一から組み立てるのに対し、MoCoは古い本を一定数保管しておくキューを持ち、新しい本が入るたびに最も古い本を入れ替えるのです。

MoCoのモメンタムエンコーダとキュー

図のように、MoCoはクエリエンコーダ(勾配更新)とキーエンコーダ(EMA更新)の2つを持ち、過去のキーをキュー $\mathcal{Q}$ に貯めて負例にします。これによりバッチサイズと無関係に $K{=}65536$ 個もの負例を確保でき、小バッチでもSimCLR並みの性能が出せます。キーエンコーダをゆっくり更新することでキュー内の表現の一貫性も保たれます。

MoCoの2つの技術的工夫

MoCoは2つの重要な工夫によって、SimCLRの計算資源の制約を克服しました。

工夫1: キュー(Queue)構造

MoCoは、過去のミニバッチから計算された特徴ベクトルをキュー $\mathcal{Q}$ に蓄積します。キューのサイズ $K$ はバッチサイズとは独立に設定でき、MoCoでは $K = 65536$ が標準です。つまり、バッチサイズが256であっても、65,536個の負例を利用できるのです。

新しいミニバッチを処理するたびに、その特徴ベクトルがキューの末尾に追加され、最も古い特徴ベクトルがキューの先頭から除去されます(FIFO: First-In, First-Out)。

$$ \mathcal{Q} \leftarrow \text{enqueue}(\mathcal{Q}, \bm{z}_{\text{new}}) \quad \text{and} \quad \mathcal{Q} \leftarrow \text{dequeue}(\mathcal{Q}) $$

このキュー構造により、バッチサイズに依存せずに大量の負例を確保できます。

工夫2: モメンタムエンコーダ

キュー構造には1つの問題があります。キューに蓄積された特徴ベクトルは、過去の異なるパラメータを持つエンコーダで計算されたものです。学習が進むにつれてエンコーダのパラメータは更新されるため、キュー内の古い特徴ベクトルと新しい特徴ベクトルは一貫性がない可能性があります。

MoCoはこの問題をモメンタムエンコーダで解決します。MoCoには2つのエンコーダがあります。

  • クエリエンコーダ $f_q$: 通常の勾配降下法で更新されるメインのエンコーダ
  • キーエンコーダ $f_k$: モメンタム更新されるエンコーダ(キュー内の特徴ベクトルの計算に使用)

キーエンコーダのパラメータ $\theta_k$ は、クエリエンコーダのパラメータ $\theta_q$ の指数移動平均(EMA)で更新されます。

$$ \theta_k \leftarrow m \cdot \theta_k + (1 – m) \cdot \theta_q $$

ここで $m \in [0, 1)$ はモメンタム係数で、通常 $m = 0.999$ と非常に1に近い値が使われます。この大きなモメンタムにより、キーエンコーダのパラメータはゆっくりと変化し、キュー内の特徴ベクトルの一貫性が保たれます。

なぜ $m = 0.999$ のような大きな値が必要なのでしょうか。仮に $m = 0$ なら、キーエンコーダはクエリエンコーダと完全に同じになり、キュー内の古い特徴ベクトルとの不整合が最大になります。逆に $m = 1$ なら、キーエンコーダは全く更新されず、意味のない固定表現を使い続けることになります。$m = 0.999$ は、この2つの極端のバランスを取る値です。He et al. の実験では、$m = 0.999$ が最も良い性能を示し、$m = 0.9$ では性能が大幅に低下しました。

MoCoの学習手順

MoCoの学習手順を整理しましょう。

ステップ1: ミニバッチの各画像から2つのビューを生成します。一方をクエリ $\bm{x}_q$、他方をキー $\bm{x}_k$ とします。

ステップ2: クエリエンコーダでクエリの特徴ベクトルを計算します。

$$ \bm{q} = f_q(\bm{x}_q) $$

ステップ3: キーエンコーダ(勾配計算なし)でキーの特徴ベクトルを計算します。

$$ \bm{k} = f_k(\bm{x}_k) \quad (\text{no gradient}) $$

ステップ4: キュー $\mathcal{Q}$ 内のキーを負例として、InfoNCE損失を計算します。

$$ \mathcal{L}_q = -\log \frac{\exp(\bm{q} \cdot \bm{k}^+ / \tau)}{\exp(\bm{q} \cdot \bm{k}^+ / \tau) + \sum_{\bm{k}^- \in \mathcal{Q}} \exp(\bm{q} \cdot \bm{k}^- / \tau)} $$

ステップ5: クエリエンコーダのパラメータ $\theta_q$ を勾配降下法で更新します。

ステップ6: キーエンコーダのパラメータ $\theta_k$ をモメンタム更新します。

ステップ7: 現在のバッチのキー $\bm{k}$ をキューに追加し、最も古いキーを除去します。

SimCLRとMoCoの比較

SimCLRとMoCoの設計上の違いを整理すると、2つの手法は同じ目的(InfoNCE損失の最小化)を異なる戦略で達成しようとしていることがわかります。

観点 SimCLR MoCo
負例の供給源 同一バッチ内の他のサンプル キュー(過去のバッチから蓄積)
負例の数 $2(N-1)$(バッチサイズに依存) $K$(キューサイズ、バッチサイズに非依存)
エンコーダ 1つ(共有) 2つ(クエリ + モメンタムキー)
パラメータ更新 全て勾配降下法 クエリ: 勾配降下法、キー: EMA
計算資源の要求 高い(大バッチ必須) 中程度(小バッチでも動作)
温度パラメータ $\tau = 0.5$ $\tau = 0.07$
標準バッチサイズ 4096〜8192 256

MoCo v2(Chen et al., 2020)では、SimCLRの知見(プロジェクションヘッド、強いデータ拡張、コサイン学習率スケジュール)を取り入れることで、さらに性能が向上しました。これは2つのアプローチが相補的であることを示しています。

SimCLRとMoCoは、負例を使って正例の表現を「押し出す」ことで学習します。しかし、「負例は本当に必要なのか?」という根本的な問いに答えたのが、次に紹介するBYOLとSimSiamです。

BYOL / SimSiam — 負例なしの対照学習

負例なしでなぜ学習できるのか

BYOL(Bootstrap Your Own Latent, Grill et al., 2020)は、対照学習の常識を覆す衝撃的な結果を示しました。負例を一切使わずに、SimCLRやMoCoに匹敵する(あるいは凌駕する)性能を達成したのです。

負例がないと何が問題になるか考えてみましょう。正例同士を近づけるだけなら、エンコーダが全ての入力に対して同じベクトルを出力する「崩壊解(collapsed solution)」が最適解になってしまいます。全てのベクトルが同一なら、どの正例ペアの類似度も最大(コサイン類似度1)になるからです。負例はこの崩壊を防ぐ「斥力」の役割を果たしていました。

では、BYOLはどうやって崩壊を防いでいるのでしょうか。

BYOLのアーキテクチャ

BYOLのアーキテクチャには、オンラインネットワークターゲットネットワークの2つがあります。

オンラインネットワークは、エンコーダ $f_\theta$、プロジェクタ $g_\theta$、予測器(predictor)$q_\theta$ の3つのコンポーネントで構成されます。

$$ \bm{z}_\theta = g_\theta(f_\theta(\tilde{\bm{x}}_1)), \quad \bm{p}_\theta = q_\theta(\bm{z}_\theta) $$

ターゲットネットワークは、エンコーダ $f_\xi$ とプロジェクタ $g_\xi$ で構成されます(予測器はありません)。

$$ \bm{z}’_\xi = g_\xi(f_\xi(\tilde{\bm{x}}_2)) $$

ターゲットネットワークのパラメータ $\xi$ は、MoCoと同様に指数移動平均で更新されます。

$$ \xi \leftarrow m \cdot \xi + (1 – m) \cdot \theta $$

BYOLの損失関数は、オンラインネットワークの予測 $\bm{p}_\theta$ とターゲットネットワークの出力 $\bm{z}’_\xi$ の間のコサイン類似度を最大化するものです。

$$ \mathcal{L}_{\text{BYOL}} = 2 – 2 \cdot \frac{\langle \bm{p}_\theta, \bm{z}’_\xi \rangle}{\|\bm{p}_\theta\| \|\bm{z}’_\xi\|} $$

ここで $\langle \cdot, \cdot \rangle$ は内積です。この損失には負例が一切含まれていません。

崩壊回避のメカニズム

BYOLが崩壊しない理由については、発表当初から活発な議論がありました。現在の理解では、以下の3つの要素が崩壊回避に寄与しています。

1. 非対称な構造: オンラインネットワークにのみ予測器 $q_\theta$ が存在し、ターゲットネットワークにはありません。この非対称性が、2つのネットワークの出力が同一に崩壊することを防ぎます。予測器は「ターゲットの出力を予測する」という非自明なタスクを課されるため、情報を圧縮する方向への圧力が生まれます。

2. 停止勾配(stop-gradient): ターゲットネットワークには勾配が伝播しません。これにより、損失関数を最小化する際に、オンラインネットワークだけが「ターゲットに近づく」方向に更新され、ターゲットが「オンラインに近づく」ことはありません。もし両方が互いに近づくなら、その最適解は崩壊ですが、片方だけが動くことで非自明な解に収束します。

3. モメンタム更新: ターゲットネットワークのゆっくりとした更新が、学習の安定性を保ちます。オンラインネットワークが急激に変化しても、ターゲットネットワークは過去の重みの平滑化として緩やかに変化するため、学習目標が急変することがありません。

SimSiam — モメンタムも不要

SimSiam(Exploring Simple Siamese Representation Learning, Chen & He, 2021)は、BYOLからさらにモメンタム更新すら取り除いた手法です。2つの重み共有されたエンコーダと、片方にのみ付加された予測器、そして停止勾配 — たったこれだけで崩壊を回避し、競争力のある性能を達成しました。

SimSiamの損失関数は以下の通りです。

$$ \mathcal{L}_{\text{SimSiam}} = -\frac{1}{2} \left[ \frac{\langle \bm{p}_1, \text{sg}(\bm{z}_2) \rangle}{\|\bm{p}_1\| \|\bm{z}_2\|} + \frac{\langle \bm{p}_2, \text{sg}(\bm{z}_1) \rangle}{\|\bm{p}_2\| \|\bm{z}_1\|} \right] $$

ここで $\text{sg}(\cdot)$ は停止勾配演算子です。2つのビューについて対称的に損失を計算している点に注目してください。

Chen & He は、SimSiamが崩壊しない条件を理論的に分析し、停止勾配が本質的に重要であることを示しました。停止勾配がないと、勾配が対称になり、両方のブランチが同じ方向に動いて崩壊します。停止勾配があると、片方のブランチの出力は定数として扱われ、他方だけが動くため、EMアルゴリズムに類似した交互最適化が暗黙的に行われるのです。

BYOL/SimSiamの負例なし対照学習

図の通り、BYOL/SimSiamは負例を使いません。オンライン側だけに予測器を置く非対称性、ターゲット側へ勾配を流さないstop-gradient、(BYOLでは)モメンタム更新の3つで「全部同じベクトル」という崩壊を防ぎます。SimSiamはstop-gradientだけでも崩壊を回避できることを示しました。

BYOLとSimSiamの成功は、対照学習における「負例は本質的に必要ではない」という重要な知見を提供しました。しかし、負例を使う対照学習は、テキストと画像の対応関係を学ぶようなマルチモーダル学習においては、依然として非常に強力です。次に、画像とテキストのペアに対して対照学習を適用したCLIPの仕組みを見ていきましょう。

テキスト領域の対照学習 — CLIPの画像-テキスト対照行列

CLIPの革新性

CLIP(Contrastive Language-Image Pre-training, Radford et al., 2021)は、対照学習の適用範囲を単一モダリティ(画像同士)からマルチモダリティ(画像とテキスト)に拡張しました。インターネットから収集した4億組の画像-テキストペアに対して対照学習を行い、「画像と、それを説明するテキストの埋め込みを近づける」ことで、ゼロショット認識という画期的な能力を獲得しました。

CLIPの革新性を理解するために、従来の画像認識のパラダイムと比較してみましょう。従来のアプローチでは、ImageNetの1,000カテゴリのように事前に定義されたクラスセットに対して分類器を学習します。新しいカテゴリを認識するには、そのカテゴリのラベル付きデータを収集し、モデルを再学習する必要がありました。

CLIPは、この「閉じたクラスセット」の制約を打破しました。テキストプロンプトでカテゴリを自由に記述できるため、学習時に見たことのないカテゴリでも認識できます。「a photo of a cat」と「a photo of a dog」というテキストプロンプトを用意すれば、CATとDOGの2クラス分類器が即座に構成されるのです。

CLIPの対照学習の数式

CLIPは、バッチ内の $N$ 組の画像-テキストペアに対して、$N \times N$ の対照行列を構成します。

バッチ内の $N$ 個の画像を $\{\bm{x}_1^I, \bm{x}_2^I, \dots, \bm{x}_N^I\}$、対応する $N$ 個のテキストを $\{\bm{x}_1^T, \bm{x}_2^T, \dots, \bm{x}_N^T\}$ とします。画像エンコーダ $f_I$ とテキストエンコーダ $f_T$ で、それぞれの埋め込みベクトルを計算します。

$$ \bm{z}_i^I = \frac{f_I(\bm{x}_i^I)}{\|f_I(\bm{x}_i^I)\|}, \quad \bm{z}_j^T = \frac{f_T(\bm{x}_j^T)}{\|f_T(\bm{x}_j^T)\|} $$

正規化された埋め込みベクトルの類似度行列 $\bm{S} \in \mathbb{R}^{N \times N}$ を計算します。

$$ S_{ij} = \bm{z}_i^I \cdot \bm{z}_j^T / \tau $$

この行列の $(i, j)$ 成分は、$i$ 番目の画像と $j$ 番目のテキストの類似度を表します。対角成分 $S_{ii}$ が正例ペアの類似度、非対角成分 $S_{ij} (i \neq j)$ が負例ペアの類似度です。

CLIPの損失関数は、この行列の行方向と列方向の両方でクロスエントロピーを計算します。

画像→テキスト方向の損失(各画像に対して、正しいテキストを識別する)は次のようになります。

$$ \mathcal{L}_{I \to T} = -\frac{1}{N} \sum_{i=1}^{N} \log \frac{\exp(S_{ii})}{\sum_{j=1}^{N} \exp(S_{ij})} $$

テキスト→画像方向の損失(各テキストに対して、正しい画像を識別する)は次のようになります。

$$ \mathcal{L}_{T \to I} = -\frac{1}{N} \sum_{j=1}^{N} \log \frac{\exp(S_{jj})}{\sum_{i=1}^{N} \exp(S_{ij})} $$

最終的な損失は両者の平均です。

$$ \mathcal{L}_{\text{CLIP}} = \frac{1}{2}(\mathcal{L}_{I \to T} + \mathcal{L}_{T \to I}) $$

この対称的な損失により、画像からテキスト、テキストから画像の両方向の対応関係を学習できます。

CLIPの画像テキスト類似度行列

図がCLIPの学習の核です。バッチ内の画像(行)とテキスト(列)の全組み合わせで類似度行列を作り、対角(正しいペア)を最大化、非対角(誤った組み合わせ)を最小化します。行方向・列方向の両方でこれを行うのが対称的な損失です。学習後はテキストプロンプトを差し替えるだけでゼロショット分類ができます。

ゼロショット分類の仕組み

CLIPの学習が完了した後、ゼロショット分類は非常にシンプルに実現されます。分類したいクラス名のリスト $\{c_1, c_2, \dots, c_M\}$ が与えられたとき、各クラス名をテンプレート「a photo of a {class name}」に埋め込んでテキスト埋め込みを計算します。

$$ \bm{z}_{c_m}^T = f_T(\text{“a photo of a } c_m \text{”}) $$

分類対象の画像 $\bm{x}$ の画像埋め込み $\bm{z}^I = f_I(\bm{x})$ を計算し、全てのクラステキスト埋め込みとのコサイン類似度を比較して、最も類似度の高いクラスを予測します。

$$ \hat{y} = \arg\max_{m} \text{sim}(\bm{z}^I, \bm{z}_{c_m}^T) $$

この仕組みにより、学習時に見たことのないクラス(例えば「a photo of a Shiba Inu puppy playing in snow」)に対しても、テキストプロンプトを変更するだけで分類が可能になります。

ここまでで、対照学習の主要な手法 — SimCLR、MoCo、BYOL/SimSiam、CLIP — の理論的な背景を理解しました。次に、これらの知見を統合して、PyTorchでSimCLR風の対照学習フレームワークをスクラッチ実装してみましょう。

PyTorch実装 — SimCLR風の対照学習フレームワーク

ここでは、SimCLRの学習パイプラインをPyTorchで実装します。CIFAR-10データセットを使い、小規模ながらも対照学習の核心的な動作を確認できる実装を目指します。

データ拡張パイプラインの実装

まず、SimCLRのデータ拡張パイプラインを実装します。同じ画像から2つの異なるビューを生成するための変換を定義します。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as _fm
for _c in ['Hiragino Sans','Yu Gothic','Noto Sans CJK JP','IPAexGothic','Meiryo']:
    if any(_c==_f.name for _f in _fm.fontManager.ttflist):
        plt.rcParams['font.family']=_c; break
plt.rcParams['axes.unicode_minus']=False

class SimCLRAugmentation:
    """SimCLR用のデータ拡張パイプライン"""
    def __init__(self, size=32):
        # 2つの独立したランダム変換を定義
        self.transform = transforms.Compose([
            transforms.RandomResizedCrop(size, scale=(0.2, 1.0)),
            transforms.RandomHorizontalFlip(p=0.5),
            transforms.RandomApply([
                transforms.ColorJitter(0.4, 0.4, 0.4, 0.1)
            ], p=0.8),
            transforms.RandomGrayscale(p=0.2),
            transforms.ToTensor(),
            transforms.Normalize(
                mean=[0.4914, 0.4822, 0.4465],
                std=[0.2023, 0.1994, 0.2010]
            ),
        ])

    def __call__(self, x):
        # 同じ画像に独立な変換を2回適用して正例ペアを生成
        view1 = self.transform(x)
        view2 = self.transform(x)
        return view1, view2

この SimCLRAugmentation クラスは、__call__ が呼ばれるたびに同じ画像に2つの独立なランダム変換を適用します。transforms.RandomResizedCrop で画像の20%から100%の領域をランダムに切り出し、ColorJitter で色調を変化させます。各変換は独立にサンプリングされるため、同じ画像から毎回異なる2つのビューが生成されます。

エンコーダとプロジェクションヘッドの実装

次に、エンコーダ(ResNet-18ベース)とプロジェクションヘッドを実装します。

class SimCLREncoder(nn.Module):
    """SimCLRのエンコーダ + プロジェクションヘッド"""
    def __init__(self, feature_dim=128):
        super().__init__()
        # ResNet-18ベースのエンコーダ(最終FC層を除去)
        resnet = torchvision.models.resnet18(weights=None)
        # CIFAR-10用に最初のConvを修正(7x7→3x3、stride 2→1)
        resnet.conv1 = nn.Conv2d(3, 64, kernel_size=3,
                                 stride=1, padding=1, bias=False)
        resnet.maxpool = nn.Identity()  # MaxPoolも除去
        self.encoder_dim = resnet.fc.in_features  # 512
        resnet.fc = nn.Identity()  # 最終FC層を除去
        self.encoder = resnet

        # プロジェクションヘッド: 2層MLP
        self.projection_head = nn.Sequential(
            nn.Linear(self.encoder_dim, self.encoder_dim),
            nn.ReLU(inplace=True),
            nn.Linear(self.encoder_dim, feature_dim)
        )

    def forward(self, x):
        # エンコーダで特徴抽出
        h = self.encoder(x)      # h: (batch, 512)
        # プロジェクションヘッドで低次元に射影
        z = self.projection_head(h)  # z: (batch, 128)
        return h, z

CIFAR-10は32×32の小さな画像であるため、ResNet-18の最初の7×7畳み込みを3×3に変更し、MaxPoolingを除去しています。これにより、小さい画像でも十分な空間解像度を保持できます。forward メソッドは、エンコーダの出力 $\bm{h}$(下流タスク用)とプロジェクションヘッドの出力 $\bm{z}$(対照学習用)の両方を返します。

InfoNCE損失の実装

対照学習の中核であるInfoNCE(NT-Xent)損失を実装します。

class InfoNCELoss(nn.Module):
    """InfoNCE (NT-Xent) 損失関数"""
    def __init__(self, temperature=0.5):
        super().__init__()
        self.temperature = temperature

    def forward(self, z_i, z_j):
        """
        z_i, z_j: (batch_size, feature_dim) の特徴ベクトル
        z_i[k] と z_j[k] が正例ペア
        """
        batch_size = z_i.shape[0]

        # L2正規化してコサイン類似度を内積で計算可能にする
        z_i = F.normalize(z_i, dim=1)
        z_j = F.normalize(z_j, dim=1)

        # 全2N個のサンプルを結合
        z = torch.cat([z_i, z_j], dim=0)  # (2*batch_size, feature_dim)

        # 全ペアのコサイン類似度行列を計算
        sim_matrix = torch.mm(z, z.t()) / self.temperature  # (2N, 2N)

        # 自分自身との類似度をマスク(-inf)
        mask_self = torch.eye(2 * batch_size, dtype=torch.bool,
                              device=z.device)
        sim_matrix.masked_fill_(mask_self, float('-inf'))

        # 正例ペアのインデックスを構成
        # z_i[k]の正例はz_j[k](インデックス: k → k+N)
        # z_j[k]の正例はz_i[k](インデックス: k+N → k)
        labels = torch.cat([
            torch.arange(batch_size, 2 * batch_size),
            torch.arange(0, batch_size)
        ]).to(z.device)

        # クロスエントロピー損失(ソフトマックス + 負の対数尤度)
        loss = F.cross_entropy(sim_matrix, labels)
        return loss

この実装のポイントを解説します。まず、$2N$ 個の特徴ベクトルを結合し、$2N \times 2N$ の類似度行列を計算します。対角成分(自分自身との類似度)は -inf でマスクして、ソフトマックスの分母から除外します。正例ペアのインデックスは、z_i[k] の正例が z_j[k](インデックス $k + N$)であることを利用して構成します。最終的に F.cross_entropy を使うことで、ソフトマックスとクロスエントロピーの計算を数値的に安定な形で実行します。

学習ループの実装

全てのコンポーネントを組み合わせた学習ループを実装します。

def train_simclr(epochs=50, batch_size=256, lr=3e-4, temperature=0.5):
    """SimCLRの学習ループ"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    # データの準備
    augmentation = SimCLRAugmentation(size=32)
    train_dataset = torchvision.datasets.CIFAR10(
        root='./data', train=True, download=True,
        transform=augmentation
    )
    train_loader = DataLoader(
        train_dataset, batch_size=batch_size,
        shuffle=True, num_workers=2, drop_last=True
    )

    # モデルと最適化
    model = SimCLREncoder(feature_dim=128).to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr,
                                 weight_decay=1e-4)
    criterion = InfoNCELoss(temperature=temperature)

    # コサイン学習率スケジューラ
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
        optimizer, T_max=epochs
    )

    losses = []
    for epoch in range(epochs):
        model.train()
        epoch_loss = 0.0
        for (view1, view2), _ in train_loader:
            view1 = view1.to(device)
            view2 = view2.to(device)

            # フォワードパス
            _, z1 = model(view1)
            _, z2 = model(view2)

            # InfoNCE損失の計算
            loss = criterion(z1, z2)

            # バックプロパゲーション
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()

        scheduler.step()
        avg_loss = epoch_loss / len(train_loader)
        losses.append(avg_loss)

        if (epoch + 1) % 10 == 0:
            print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}')

    return model, losses

このコードでは、CIFAR-10の50,000枚の学習画像に対して、各画像から2つのビューを生成し、InfoNCE損失で学習します。drop_last=True により、最後の不完全なバッチを除外して、常に同じバッチサイズで学習を行います。コサインアニーリングスケジューラを使い、学習率を滑らかに減衰させます。

線形評価の実装

事前学習したエンコーダの品質を評価するために、線形評価(linear evaluation)を実装します。エンコーダの重みを固定し、その上に線形分類器のみを学習させます。

def linear_evaluation(model, epochs=100, batch_size=256, lr=1e-3):
    """線形評価でエンコーダの品質を測定"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    # 標準的な変換(データ拡張なし)
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.4914, 0.4822, 0.4465],
            std=[0.2023, 0.1994, 0.2010]
        ),
    ])

    train_dataset = torchvision.datasets.CIFAR10(
        root='./data', train=True, download=True, transform=transform
    )
    test_dataset = torchvision.datasets.CIFAR10(
        root='./data', train=False, download=True, transform=transform
    )
    train_loader = DataLoader(train_dataset, batch_size=batch_size,
                              shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=batch_size)

    # エンコーダの重みを固定
    model.eval()
    encoder_dim = model.encoder_dim

    # 線形分類器
    classifier = nn.Linear(encoder_dim, 10).to(device)
    optimizer = torch.optim.Adam(classifier.parameters(), lr=lr)

    for epoch in range(epochs):
        classifier.train()
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)
            with torch.no_grad():
                h, _ = model(images)  # エンコーダ出力hを使用
            logits = classifier(h)
            loss = F.cross_entropy(logits, labels)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

    # テスト精度
    classifier.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            h, _ = model(images)
            logits = classifier(h)
            _, predicted = logits.max(1)
            correct += predicted.eq(labels).sum().item()
            total += labels.size(0)

    accuracy = 100.0 * correct / total
    print(f'Linear Evaluation Accuracy: {accuracy:.2f}%')
    return accuracy

線形評価では、エンコーダの出力 $\bm{h}$(プロジェクションヘッドの出力 $\bm{z}$ ではない)を特徴量として使用します。これは先述の通り、プロジェクションヘッドが対照学習に特化した情報圧縮を行うため、下流タスクには $\bm{h}$ の方がより豊富な情報を保持しているからです。model.eval() でバッチ正規化層を評価モードに切り替え、torch.no_grad() で勾配計算を無効化することで、エンコーダの重みが更新されないことを保証しています。

この実装をCIFAR-10で50エポック事前学習し、線形評価を行うと、ランダム初期化(約40%)に比べて大幅に高い精度(約65-75%、バッチサイズやエポック数に依存)が得られます。教師あり学習のResNet-18(約93%)には及びませんが、ラベルを一切使わずにこの精度を達成できることは、対照学習の有効性を実証しています。精度の差は、CIFAR-10の小さな画像サイズと、限られたバッチサイズ・学習エポック数に起因します。より大きな画像(ImageNet, 224×224)とより大きなバッチサイズで学習すれば、教師あり学習に迫る性能が得られます。

実装の核心部分が完成しました。次に、対照学習の性能に大きな影響を与える温度パラメータ $\tau$ の効果を、可視化を通じて直感的に理解しましょう。

温度パラメータの効果の可視化

温度パラメータ $\tau$ が対照学習の学習ダイナミクスにどう影響するかを、2つの実験で可視化します。

実験1: ソフトマックス分布の鋭さ

まず、温度パラメータが類似度のソフトマックス分布に与える影響を可視化します。

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

# 類似度スコア(1つの正例 + 7つの負例)
similarities = np.array([0.9, 0.3, 0.2, 0.1, -0.1, -0.2, -0.3, -0.5])
labels_name = ['正', '負1', '負2', '負3',
               '負4', '負5', '負6', '負7']
temperatures = [0.05, 0.1, 0.5, 1.0]

fig, axes = plt.subplots(1, 4, figsize=(16, 4))

for ax, tau in zip(axes, temperatures):
    logits = similarities / tau
    probs = np.exp(logits) / np.sum(np.exp(logits))

    colors = ['#00e5ff'] + ['#ff6b6b'] * 7  # 正例: シアン, 負例: 赤
    ax.bar(range(len(probs)), probs, color=colors, edgecolor='white',
           linewidth=0.5)
    ax.set_title(f'$\\tau = {tau}$', fontsize=14)
    ax.set_xticks(range(len(probs)))
    ax.set_xticklabels(labels_name, rotation=45, fontsize=8)
    ax.set_ylabel('softmax確率')
    ax.set_ylim(0, 1.05)
    ax.grid(axis='y', alpha=0.3)

plt.suptitle('温度によるsoftmax分布の鋭さの変化', fontsize=16)
plt.tight_layout()
plt.savefig('temperature_softmax.png', dpi=150, bbox_inches='tight')
plt.show()

温度によるsoftmax分布の変化

このグラフから、温度パラメータの効果が明確に読み取れます。$\tau = 0.05$ では、正例(pos)にほぼ100%の確率が集中し、負例の確率はほぼ0になっています。これは「ハードな識別」であり、モデルは正例と負例の僅かな類似度の差にも敏感に反応します。$\tau = 0.1$ でも正例に確率が集中していますが、若干の確率が高い類似度の負例(neg1)にも割り当てられています。$\tau = 0.5$ になると分布はかなり平坦になり、正例の確率は45%程度まで低下します。$\tau = 1.0$ ではさらに平坦で、正例と負例の区別が曖昧になっています。実際のSimCLRでは $\tau = 0.5$ が使われており、学習の安定性と識別力のバランスを取っていることがわかります。

実験2: 温度と学習曲線の関係

異なる温度パラメータでの学習曲線を比較し、学習の収束挙動への影響を確認します。

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

def simulate_contrastive_loss(temperature, n_negatives=255,
                              n_steps=200, seed=42):
    """
    対照学習の損失推移をシミュレーション。
    エンコーダの改善を模擬し、正例類似度が徐々に上がり
    負例類似度が徐々に下がる過程を生成する。
    """
    rng = np.random.RandomState(seed)
    losses = []

    for step in range(n_steps):
        progress = step / n_steps
        # 学習が進むにつれて正例の類似度が上昇
        pos_sim = 0.1 + 0.7 * progress + rng.normal(0, 0.05)
        pos_sim = np.clip(pos_sim, -1, 1)
        # 負例の類似度は0付近に留まる(微小なランダム変動)
        neg_sims = rng.normal(0, 0.2 - 0.1 * progress,
                              size=n_negatives)
        neg_sims = np.clip(neg_sims, -1, 1)

        # InfoNCE損失を計算
        logit_pos = pos_sim / temperature
        logits_neg = neg_sims / temperature
        all_logits = np.concatenate([[logit_pos], logits_neg])
        # log-sum-exp で数値安定化
        max_logit = np.max(all_logits)
        log_sum_exp = max_logit + np.log(
            np.sum(np.exp(all_logits - max_logit))
        )
        loss = -logit_pos + log_sum_exp
        losses.append(loss)

    return losses

temperatures_exp = [0.07, 0.1, 0.5, 1.0]
fig, ax = plt.subplots(figsize=(10, 6))

for tau in temperatures_exp:
    losses = simulate_contrastive_loss(tau, n_steps=200)
    # 移動平均で平滑化
    window = 10
    smoothed = np.convolve(losses, np.ones(window)/window, mode='valid')
    ax.plot(range(len(smoothed)), smoothed, label=f'$\\tau = {tau}$',
            linewidth=2)

ax.set_xlabel('学習ステップ', fontsize=13)
ax.set_ylabel('InfoNCE損失', fontsize=13)
ax.set_title('温度別の学習曲線', fontsize=15)
ax.legend(fontsize=12)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('temperature_learning_curves.png', dpi=150,
            bbox_inches='tight')
plt.show()

温度別の学習曲線

学習曲線のシミュレーション結果から、いくつかの重要な傾向が読み取れます。$\tau = 0.07$(MoCoの標準値)では初期の損失値が最も大きく、学習が進むにつれて急激に減少します。これは、低温では類似度の微小な差がソフトマックスで増幅されるため、初期の「ほぼランダム」な表現では損失が非常に大きくなるためです。$\tau = 0.5$(SimCLRの標準値)では、初期損失が比較的小さく、滑らかに収束します。$\tau = 1.0$ では損失の値域が最も狭く、学習の収束は安定していますが、正例と負例の区別が不十分になるリスクがあります。実用上は、十分な負例数が確保できる場合(MoCoのキュー)は低温、負例数が限られる場合(小バッチのSimCLR)は中程度の温度が推奨されます。

実験3: 特徴空間のt-SNE可視化

対照学習で獲得された特徴空間の構造をt-SNEで可視化するコードも示します。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as _fm
for _c in ['Hiragino Sans','Yu Gothic','Noto Sans CJK JP','IPAexGothic','Meiryo']:
    if any(_c==_f.name for _f in _fm.fontManager.ttflist):
        plt.rcParams['font.family']=_c; break
plt.rcParams['axes.unicode_minus']=False
from sklearn.manifold import TSNE

def visualize_feature_space():
    """対照学習前後の特徴空間をt-SNEで可視化(合成データ)"""
    rng = np.random.RandomState(42)
    n_classes = 5
    n_per_class = 100
    class_names = ['飛行機', '車', '鳥', '猫', '犬']

    # 学習前: ほぼランダムな特徴(クラスター構造なし)
    features_before = rng.randn(n_classes * n_per_class, 128)
    labels = np.repeat(np.arange(n_classes), n_per_class)

    # 学習後: クラスごとにクラスターを形成
    features_after = np.zeros_like(features_before)
    for c in range(n_classes):
        center = rng.randn(128) * 3  # クラス中心を分散させる
        noise = rng.randn(n_per_class, 128) * 0.3
        features_after[c * n_per_class:(c+1) * n_per_class] = center + noise

    fig, axes = plt.subplots(1, 2, figsize=(14, 6))
    colors = ['#ff6b6b', '#51cf66', '#339af0', '#ffd43b', '#cc5de8']

    for ax, features, title in zip(
        axes,
        [features_before, features_after],
        ['対照学習の前', '対照学習の後']
    ):
        tsne = TSNE(n_components=2, random_state=42, perplexity=30)
        emb = tsne.fit_transform(features)

        for c in range(n_classes):
            mask = labels == c
            ax.scatter(emb[mask, 0], emb[mask, 1], c=colors[c],
                      label=class_names[c], alpha=0.6, s=20,
                      edgecolors='none')

        ax.set_title(title, fontsize=14)
        ax.legend(fontsize=10, loc='upper right')
        ax.set_xticks([])
        ax.set_yticks([])

    plt.suptitle('特徴空間のt-SNE可視化', fontsize=16)
    plt.tight_layout()
    plt.savefig('tsne_contrastive.png', dpi=150, bbox_inches='tight')
    plt.show()

visualize_feature_space()

対照学習前後の特徴空間t-SNE

t-SNEの可視化結果から、対照学習の効果が視覚的に確認できます。学習前(左)では、5つのクラスのデータポイントが特徴空間上でほぼ均一に分布しており、クラスター構造が見られません。これはランダム初期化のエンコーダが意味のある特徴を抽出できていないことを示しています。学習後(右)では、同じクラスのデータポイントが密なクラスターを形成し、異なるクラスのクラスター間には明確な距離が存在します。対照学習が「正例を近づけ、負例を遠ざける」という目標を達成し、意味的に類似したデータが特徴空間上で近くに配置されていることがわかります。重要なのは、この構造がラベルを一切使わずに形成された点です。

対照学習手法の系譜と設計原理の比較

ここまで紹介した手法を俯瞰し、対照学習の設計空間を整理しましょう。

手法間の進化の流れ

対照学習の進化は、「何を簡略化できるか」を探求する過程でした。

SimCLR(2020年2月)は、メモリバンクや複雑なアーキテクチャなしで、大バッチ + データ拡張 + プロジェクションヘッドだけで高性能を達成しました。しかし、大バッチサイズへの依存が課題でした。

MoCo(2020年3月、v1は2019年11月)は、キューとモメンタムエンコーダで大バッチの必要性を除去しました。小バッチでもSimCLRに匹敵する性能を達成し、計算効率を大幅に改善しました。

BYOL(2020年6月)は、負例すら不要であることを示しました。モメンタムエンコーダ + 予測器 + 停止勾配で崩壊を回避し、SimCLRやMoCoを上回る性能を達成しました。

SimSiam(2021年)は、モメンタムエンコーダも不要であることを示しました。停止勾配と予測器だけで崩壊を防ぎ、対照学習の最小構成要素を明らかにしました。

CLIP(2021年1月)は、対照学習を画像-テキストのマルチモーダル領域に拡張し、ゼロショット認識という新しい能力を獲得しました。

統一的な設計原理

これらの手法を比較すると、対照学習の成功に共通する設計原理が見えてきます。

原理1: 良い正例ペアの生成。全ての手法に共通して、正例ペアの質が表現の質を決定します。画像領域ではデータ拡張、マルチモーダル領域では自然な対応関係(画像とそのキャプション)が正例ペアの供給源です。

原理2: 崩壊の回避。全入力を同じベクトルにマッピングする崩壊解を防ぐ仕組みが必要です。負例(SimCLR, MoCo, CLIP)、停止勾配 + 予測器(BYOL, SimSiam)、あるいはその両方が崩壊回避に寄与します。

原理3: 十分な容量のエンコーダ。対照学習は、エンコーダが十分に大きい場合にのみ有効です。小さなエンコーダでは、データ拡張に対する不変性と下流タスクに必要な弁別性のトレードオフが厳しくなり、性能が低下します。

原理4: プロジェクションヘッドによる情報保護。対照学習の損失関数が作用する空間と、下流タスクで使う表現空間を分離することで、エンコーダの出力により豊富な情報を保持できます。

残された課題

対照学習には依然として課題があります。偽陰性(false negative)問題として、大きなバッチやキューから負例をランダムにサンプリングすると、実際には正例と同じクラスのサンプルが負例として含まれる可能性があります。例えば、バッチ内に犬の画像が2枚あれば、一方がアンカーのとき他方は負例として扱われ、犬の表現が不当に引き離されます。データ拡張への依存として、画像領域ではデータ拡張の設計が比較的容易ですが、テキスト、音声、時系列データなどの他のモダリティでは、適切な正例ペアの生成方法が自明ではありません。また、学習の計算コストとして、SimCLRやCLIPの学習には大規模な計算資源が必要であり、この点は依然として改善の余地があります。

まとめ

本記事では、対照学習による事前学習の理論とその主要な手法について体系的に解説しました。

  • 対照学習の基本原理: 正例ペアの特徴ベクトルを近づけ、負例ペアを遠ざけることで、ラベルなしデータから意味のある表現を学習する
  • InfoNCE損失: $(K+1)$ クラスの交差エントロピーとして解釈でき、その最適化は入力間の相互情報量の下界を最大化することに対応する
  • SimCLR: データ拡張 + プロジェクションヘッド + 大バッチという「シンプルさ」の追求で、複雑な手法に匹敵する性能を達成した
  • MoCo: モメンタムエンコーダとキュー構造で、バッチサイズに依存しない大量の負例の確保を実現した
  • BYOL / SimSiam: 負例なしでも崩壊を回避できることを示し、対照学習の本質的な要件を明らかにした
  • CLIP: 画像-テキストの対照学習により、ゼロショット認識という新しいパラダイムを切り開いた
  • 温度パラメータ: ソフトマックス分布の鋭さを制御し、識別力と学習安定性のトレードオフを調整する

対照学習は、大規模言語モデルの文埋め込み学習、マルチモーダルAI、検索システムなど、現代のAI技術の基盤として不可欠な位置を占めています。本記事の内容を踏まえて、さらに以下の記事で理解を深めることができます。

画像なし
対照学習(Contrastive Learning)の理論と実装
InfoNCE損失の情報理論的な意味やSimCLRのPyTorchスクラッチ実装をより詳細に解説しています。
画像なし
SimCLRの仕組みとNT-Xent損失をPythonで実装する
SimCLRのデータ拡張、Projection Head、NT-Xent損失の役割からPyTorchでの実装まで詳しく説明しています。
文埋め込みの理論と実装
文レベルの埋め込み表現を対照学習で獲得する手法を解説しています。
画像なし
マスク言語モデル(MLM)の理論と実装
対照学習と並ぶ事前学習手法であるマスク言語モデルの仕組みを解説しています。
画像なし
転移学習の理論と実践ガイド
事前学習済みモデルを下流タスクに適用する転移学習の体系的なガイドです。