Hayami Kaito Web
~/tech

自己教師あり学習と CLIP のしくみ:ラベルなしで表し方を学ぶ

published

深層学習が強くなった理由の 1 つは、大量のデータです。しかし、画像 1 枚 1 枚に「これは犬」「これは猫」と正解のラベルを付けるには、人手と時間がかかります。一方、ラベルの付いていない画像や文章なら、インターネット上にいくらでもあります。

そこで、データそのものから問題を作り、正解のラベルなしで学習する方法が広く使われるようになりました。これを 自己教師あり学習と呼びます。LLM の学習 の「次のトークンを当てる」も、単語の埋め込み の word2vec の「周りの単語を当てる」も、自己教師あり学習です。この記事では、画像の分野で発達した 対照学習と、画像と文章を結び付けた CLIP のしくみを、小さな計算例で確かめます。

結論を先に書くと、次のとおりです。

  • 自己教師あり学習の目的は、正解を当てることそのものではなく、その過程で役に立つ表し方(埋め込み)を身に付けること
  • 対照学習は、「同じものから来た組」を、ほかの組の中から当てる分類の問題を解く。損失は InfoNCE と呼ばれ、温度で当て方の厳しさが変わる
  • CLIP は、画像とその説明文の組で対照学習をして、画像と文章を同じベクトルの空間に置く。すると、学習していない分類でも、ラベルを文章にするだけで分類できる(ゼロショット分類)
  • 近づけるだけで比べる相手(負例)がないと、すべてが同じベクトルに潰れてしまう(崩壊)。負例を使うか、別の工夫で防ぐ

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

自己教師あり学習:データから問題を作る

当てる問題の作り方

自己教師あり学習では、データの一部を隠したり変えたりして、それを当てる問題を作ります。

問題の作り方例
続きを当てる文章の次のトークンを当てる(GPT)
隠した部分を当てる文章の一部の単語を隠して当てる(BERT)。画像の 75% のます目を隠して元に戻す(MAE)
同じものから来た組を当てる同じ画像を別々に加工した 2 枚を、ほかの画像の中から見つける(SimCLR)。画像と、その説明文を組にする(CLIP)

どの問題も、正解はデータそのものから決まるので、人がラベルを付ける必要がありません。そして、問題をうまく解くためには、モデルはデータの意味をとらえた表し方を身に付ける必要があります。この表し方を、少しのラベル付きデータで仕上げたり(ファインチューニング)、そのまま検索や分類に使ったりします。

同じ画像の 2 つの見え方

2020 年に発表された SimCLR は、次のように学習します。

  1. 1 枚の画像から、切り抜き・色の変化・ぼかしなどの加工をランダムに 2 回して、2 枚の画像を作る
  2. 2 枚をそれぞれエンコーダーでベクトルにする
  3. 同じ画像から作った 2 枚のベクトルは近く、違う画像から作ったベクトルとは遠くなるように学習する

加工しても変わらないもの(写っている物の種類)は残し、加工で変わるもの(位置・色合い)は気にしない表し方が身に付きます。どんな加工をするかで、「何を同じとみなすか」をモデルに教えているわけです。

対照学習の損失:InfoNCE

正しい組を当てる分類

「近づける・遠ざける」を損失の形にしたのが InfoNCE です。バッチの中に N 組のデータがあるとき、1 つ目の側(たとえば画像)のそれぞれについて、もう一方の側(文章)の N 個の候補の中から、正しい相手を当てる N 択の分類問題とみなします。

  1. 2 つのベクトルの近さを、コサイン類似度で測る
  2. 近さを 温度 τ で割り、ソフトマックスで「それぞれの候補が正しい相手である確率」にする
  3. 正しい相手に付けた確率の −log を損失にする(分類の交差エントロピー と同じ)

4 組の画像と文章の近さの表で、温度を変えて計算してみます。表の対角線のます目が正しい組です。

import numpy as np

# 1. InfoNCE:正しい組を、ほかの組の中から当てる分類の損失
def softmax(Z):
    E = np.exp(Z - Z.max(axis=1, keepdims=True)); return E / E.sum(axis=1, keepdims=True)
sim = np.array([[0.9, 0.3, 0.2, 0.1],     # 行:画像、列:文章。対角が正しい組
                [0.4, 0.8, 0.5, 0.2],
                [0.1, 0.6, 0.7, 0.3],
                [0.2, 0.1, 0.3, 0.6]])
for tau in [1.0, 0.1, 0.01]:
    P = softmax(sim / tau)
    loss = -np.log(np.diag(P)).mean()
    print(tau, np.round(np.diag(P), 3), round(float(loss), 3))

実行結果:

1.0 [0.401 0.338 0.32  0.331] 1.061
0.1 [0.996 0.934 0.72  0.93 ] 0.118
0.01 [1. 1. 1. 1.] 0.0

左から、温度、それぞれの画像について正しい文章に付けた確率、損失です。

  • 温度 1 では、コサイン類似度の差(最大でも 1 程度)がそのまま確率の差になるので、正しい相手の確率は 0.3〜0.4 しかなく、損失は 1.061 です
  • 温度 0.1 では、近さの差が 10 倍に強調され、正しい相手の確率は 0.72〜0.996 になりました。3 行目の画像は、正しい文章との近さ 0.7 と、間違った文章との近さ 0.6 の差が小さいので、確率が 0.72 にとどまっています
  • 温度 0.01 では、少しでも近ければ確率がほぼ 1 になり、損失は 0 です

温度を小さくするほど、紛らわしい相手(近さの差が小さい負例)との区別を厳しく求めることになります。小さすぎると、わずかな差で損失がほぼ 0 になってしまい、学習が進みにくくなります。CLIP では、温度もパラメーターとして学習します。

CLIP:画像と文章を同じ空間に置く

画像と説明文の組で学習する

2021 年に OpenAI が発表した CLIP は、インターネットから集めた約 4 億組の画像と、その説明文(画像の周りに書かれた文章など)で、画像のエンコーダーと文章のエンコーダーを同時に学習しました。損失は InfoNCE で、画像から正しい説明文を当てる方向と、説明文から正しい画像を当てる方向の両方を使います。

小さな例で試します。「画像」は、形(丸・三角・四角)と色(赤・青・緑・黄色)の組み合わせから作った 64 次元のベクトルで、形と色の情報が混ざり合った、そのままでは読めない形にしています。「説明文」は、「赤い」「丸」のような単語と、「写真」「絵」「小さな」「大きな」のどれか 1 つの余計な単語からなる単語の袋(どの単語が入っているかのベクトル)です。12 通りの組み合わせのうち、「青い三角」と「黄色い四角」は学習に一度も出てこないようにします。

画像のエンコーダーも文章のエンコーダーも、16 次元のベクトルに変換する 1 枚の重みの表です。まずは学習前に、後で説明するゼロショット分類の正解率を測っておきます。

import numpy as np

def softmax(Z):
    E = np.exp(Z - Z.max(axis=1, keepdims=True)); return E / E.sum(axis=1, keepdims=True)

# 「画像」:形(3 種類)と色(4 種類)の組み合わせから作った 64 次元のベクトル。雑音つき
rng = np.random.default_rng(0)
shapes, colors = ["丸", "三角", "四角"], ["赤い", "青い", "緑の", "黄色い"]
shape_vec, color_vec = rng.standard_normal((3, 8)), rng.standard_normal((4, 8))
mix = rng.standard_normal((16, 64)) / 4
def image(s, c):
    z = np.r_[shape_vec[s], color_vec[c]] + 0.3 * rng.standard_normal(16)
    return np.tanh(z @ mix)                     # 形と色が混ざり合った、そのままでは読めない表し方
# 「文章」:単語の袋(どの単語が入っているか)。説明文にはときどき余計な単語も入る
vocab = shapes + colors + ["写真", "絵", "小さな", "大きな"]
def text(s, c):
    v = np.zeros(len(vocab))
    v[s] = v[3 + c] = 1
    v[7 + rng.integers(4)] = 1
    return v

# 学習に使う画像と説明文の組。「青い三角」と「黄色い四角」の組み合わせは学習に出てこない
held_out = {(1, 1), (2, 3)}
combos = [(s, c) for s in range(3) for c in range(4) if (s, c) not in held_out]

dim, tau, lr = 16, 0.1, 0.05
Wi = 0.1 * rng.standard_normal((64, dim))      # 画像のエンコーダー
Wt = 0.1 * rng.standard_normal((len(vocab), dim))   # 文章のエンコーダー

def encode(X, W):
    Z = X @ W
    n = np.linalg.norm(Z, axis=1, keepdims=True)
    return Z / n, Z, n

def back_normalize(g, U, n):
    # 長さ 1 にそろえる操作の勾配
    return (g - (g * U).sum(axis=1, keepdims=True) * U) / n

# ゼロショット分類:12 通りの「色 形」の文章をベクトルにし、画像にいちばん近い文章を答える
prompts = np.zeros((12, len(vocab)))
for s in range(3):
    for c in range(4):
        prompts[s * 4 + c, [s, 3 + c, 7]] = 1        # 「赤い 丸 の 写真」のような文章
Up, _, _ = encode(prompts, Wt)
def accuracy(pairs, n=200):
    hit = 0
    for _ in range(n):
        s, c = pairs[rng.integers(len(pairs))]
        u, _, _ = encode(image(s, c)[None], Wi)
        hit += (u @ Up.T).argmax() == s * 4 + c
    return round(hit / n, 3)
print("学習前", accuracy(combos))

32 組ずつのバッチで、InfoNCE の損失を両方向で計算し、3,000 回学習します。同じ組み合わせ(「赤い丸」の画像が 2 枚など)が同じバッチに入ることがあるので、その場合はどれも正解とみなします。上のコードの続きです。

for step in range(3000):
    batch = [combos[k] for k in rng.choice(len(combos), 32)]
    I = np.array([image(s, c) for s, c in batch]); T = np.array([text(s, c) for s, c in batch])
    Ui, _, ni = encode(I, Wi); Ut, _, nt = encode(T, Wt)
    logits = Ui @ Ut.T / tau
    # 同じ組み合わせが同じバッチに複数あるときは、それらすべてを正解とみなす
    same = np.array([[a == b for b in batch] for a in batch], float)
    target = same / same.sum(axis=1, keepdims=True)
    G = (softmax(logits) - target) + (softmax(logits.T) - target).T   # 画像→文章と文章→画像の両方向
    G /= len(batch) * tau
    gUi, gUt = G @ Ut, G.T @ Ui
    Wi -= lr * I.T @ back_normalize(gUi, Ui, ni)
    Wt -= lr * T.T @ back_normalize(gUt, Ut, nt)

ゼロショット分類

学習が終わったら、分類の問題を一度も解かせずに、画像を 12 通りの組み合わせに分類します。12 通りの「赤い 丸 の 写真」のような文章を文章のエンコーダーでベクトルにし、画像のベクトルにいちばん近い文章を答えにするのです。これを ゼロショット分類と呼びます。上のコードの続きです。

Up, _, _ = encode(prompts, Wt)                   # 学習したエンコーダーで文章をベクトルにし直す
print("学習した組み合わせ", accuracy(combos))
print("学習していない組み合わせ", accuracy(sorted(held_out)))
u, _, _ = encode(image(1, 1)[None], Wi)
top = np.argsort(-(u @ Up.T)[0])[:3]
print([colors[k % 4] + shapes[k // 4] for k in top])
Ui, _, _ = encode(np.array([image(s, c) for s, c in combos]), Wi)
print("対照学習", round(float((Ui @ Ui.T).mean()), 3), round(float((Up @ Up.T).mean()), 3))

実行結果(学習前の正解率も含めて、ここまでの出力):

学習前 0.03
学習した組み合わせ 1.0
学習していない組み合わせ 0.945
['青い三角', '赤い三角', '緑の三角']
対照学習 0.019 0.029

学習前の正解率は 3% で、12 択の当てずっぽう(約 8%)と変わりません。学習後は、学習に出てきた 10 通りの組み合わせの画像を 100% 正しく分類できました。

注目したいのは、学習に一度も出てこなかった「青い三角」と「黄色い四角」も 94.5% 正しく分類できたことです。4 行目は、「青い三角」の画像に近い文章の上位 3 つで、いちばん近いのは「青い三角」、次は同じ三角の「赤い三角」「緑の三角」でした。モデルは、「青い」と「三角」をそれぞれ別の単語の画像として学んでおり、文章のベクトルが単語の足し算でできているので、見たことのない組み合わせでも、画像の空間の正しい場所を指せたのです。

最後の行は、10 通りの組み合わせの画像どうし、12 通りの文章どうしのコサイン類似度の平均で、0.019 と 0.029 です。違うもののベクトルは、互いにほぼ直角の、ばらばらの向きに散らばっています。

実際の CLIP は、ImageNet の 1,000 種類の分類で、ImageNet の画像を 1 枚も学習に使わずに、ImageNet で学習した ResNet-50 と同じくらいの正解率を出しました。

文章の書き方で正解率が変わる

ゼロショット分類では、ラベルを どんな文章にするかが重要です。この例では、学習の説明文にはいつも「写真」などの余計な単語が 1 つ入っていたので、分類に使う文章にも「写真」を入れています。CLIP の論文でも、ラベルの単語だけ(「dog」)より、「A photo of a dog.」のような学習の説明文に近い文章にすると、正解率が上がることが報告されています。

画像と文章の空間をつなぐ

画像と文章が同じ空間に置かれると、いろいろなことができます。

  • 画像検索:「夕焼けの海と船」のような文章のベクトルに近い画像を、ベクトル検索 で探す
  • 画像生成の条件付け:画像生成 AI で、文章の内容に合う画像を作るための手がかりにする
  • マルチモーダル LLM:CLIP のような画像のエンコーダーが作ったベクトルを、LLM が読めるトークンのベクトルに変換して入力し、画像について質問に答えられるようにする(2023 年の LLaVA など)

崩壊:負例がないとすべてが同じになる

近づけるだけの学習

InfoNCE では、正しい組を近づけるだけでなく、ほかの組(負例)を遠ざけることも同時に求めています。負例を使わずに、正しい組のコサイン類似度を上げるだけにすると、どうなるでしょうか。上のコードの続きで、同じデータ・同じ回数で学習してみます。

# 正しい組を近づけるだけで、ほかの組と比べない(負例を使わない)と、どうなるか
Wi = 0.1 * rng.standard_normal((64, dim)); Wt = 0.1 * rng.standard_normal((len(vocab), dim))
for step in range(3000):
    batch = [combos[k] for k in rng.choice(len(combos), 32)]
    I = np.array([image(s, c) for s, c in batch]); T = np.array([text(s, c) for s, c in batch])
    Ui, _, ni = encode(I, Wi); Ut, _, nt = encode(T, Wt)
    # 損失 = −(正しい組のコサイン類似度の平均)
    Wi -= lr * I.T @ back_normalize(-Ut / len(batch), Ui, ni)
    Wt -= lr * T.T @ back_normalize(-Ui / len(batch), Ut, nt)
Up, _, _ = encode(prompts, Wt)
Ui, _, _ = encode(np.array([image(s, c) for s, c in combos]), Wi)
print("負例なし", accuracy(combos), round(float((Ui @ Ui.T).mean()), 3), round(float((Up @ Up.T).mean()), 3))

実行結果:

負例なし 0.94 0.966 0.964

左から、ゼロショット分類の正解率、画像どうしのコサイン類似度の平均、文章どうしのコサイン類似度の平均です。正解率は 94% と、まだ高く見えます。しかし、画像どうし・文章どうしのコサイン類似度の平均は 0.966 と 0.964 で、対照学習の 0.019・0.029 と比べると、どのベクトルもほぼ同じ向きを向いてしまっています。

「すべての画像とすべての文章を同じベクトルにする」と、正しい組のコサイン類似度は最大の 1 になり、損失は最小になります。負例がないと、データを区別しない、この意味のない答えに向かって学習が進んでしまうのです。これを 崩壊(collapse)と呼びます。この例では、学習の回数が限られていて、まだ完全には潰れていないので、わずかに残った違いで分類できていますが、違いはごくわずかで、雑音に弱い表し方です。

負例を使わずに崩壊を防ぐ

負例を使う対照学習は、たくさんの負例と比べるほど性能が上がるので、SimCLR では数千枚の大きなバッチが必要でした。そこで、負例を使わずに崩壊を防ぐ方法も研究されています。

  • MoCo(2019 年):過去のバッチのベクトルを列に貯めておき、大量の負例として使う
  • BYOL(2020 年)・SimSiam(2021 年):片方の枝にだけ小さな予測用のネットワークを付け、もう片方には勾配を流さない(stop-gradient)。この非対称さで、潰れた答えに向かいにくくなる
  • DINO(2021 年):生徒と教師の 2 つのネットワークで、教師の出力を中心化・鋭くする操作で潰れるのを防ぐ
  • MAE(2021 年):画像の大部分を隠して元に戻す問題なので、そもそも潰れた答えでは解けない

自己教師あり学習の手法の流れ

この記事で紹介した手法を年代順に並べます。

年手法・出来事ポイント
2013word2vec周りの単語を当てて、単語のベクトルを学ぶ
2018CPC(InfoNCE)・BERT・GPT正しい組を当てる損失。隠した単語や次の単語を当てる事前学習
2019MoCo過去のバッチのベクトルを負例として貯めておく
2020SimCLR・BYOL同じ画像の 2 つの加工を近づける。負例なしで崩壊を防ぐ
2021CLIP・ALIGN・SimSiam・DINO・MAE画像と説明文の組で学習し、ゼロショット分類。いろいろな崩壊の防ぎ方。隠したます目を戻す
2023SigLIP・DINOv2・LLaVAソフトマックスのかわりにシグモイドの損失。大規模な画像の自己教師あり学習。画像を読める LLM

まとめ

  • 自己教師あり学習は、データの一部を隠したり変えたりして作った問題を解き、その過程で役に立つ表し方を身に付ける
  • 対照学習は、同じものから来た組を、ほかの組の中から当てる分類として学習する。損失は InfoNCE
  • 温度を小さくするほど、紛らわしい負例との区別を厳しく求める。小さすぎると学習が進みにくい
  • CLIP は、画像と説明文の組で対照学習をし、画像と文章を同じ空間に置く。ラベルを文章にするだけで、学習していない分類(ゼロショット分類)ができ、見たことのない組み合わせにも答えられる
  • ゼロショット分類では、ラベルを学習の説明文に近い文章にすると正解率が上がる
  • 負例を使わずに近づけるだけだと、すべてが同じベクトルに潰れる(崩壊)。負例を貯める、stop-gradient、隠して戻す問題などで防ぐ

参考

  • T. Mikolov et al., "Distributed Representations of Words and Phrases and their Compositionality", NeurIPS 2013(arXiv:1310.4546)
  • A. van den Oord, Y. Li and O. Vinyals, "Representation Learning with Contrastive Predictive Coding", 2018(arXiv:1807.03748)
  • J. Devlin et al., "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding", NAACL 2019(arXiv:1810.04805)
  • K. He et al., "Momentum Contrast for Unsupervised Visual Representation Learning", CVPR 2020(arXiv:1911.05722)
  • T. Chen et al., "A Simple Framework for Contrastive Learning of Visual Representations", ICML 2020(arXiv:2002.05709)
  • J.-B. Grill et al., "Bootstrap Your Own Latent: A New Approach to Self-Supervised Learning", NeurIPS 2020(arXiv:2006.07733)
  • A. Radford et al., "Learning Transferable Visual Models From Natural Language Supervision", ICML 2021(arXiv:2103.00020)
  • C. Jia et al., "Scaling Up Visual and Vision-Language Representation Learning With Noisy Text Supervision", ICML 2021(arXiv:2102.05918)
  • X. Chen and K. He, "Exploring Simple Siamese Representation Learning", CVPR 2021(arXiv:2011.10566)
  • M. Caron et al., "Emerging Properties in Self-Supervised Vision Transformers", ICCV 2021(arXiv:2104.14294)
  • K. He et al., "Masked Autoencoders Are Scalable Vision Learners", CVPR 2022(arXiv:2111.06377)
  • X. Zhai et al., "Sigmoid Loss for Language Image Pre-Training", ICCV 2023(arXiv:2303.15343)
  • M. Oquab et al., "DINOv2: Learning Robust Visual Features without Supervision", 2023(arXiv:2304.07193)
  • H. Liu et al., "Visual Instruction Tuning", NeurIPS 2023(arXiv:2304.08485)