Hayami Kaito Web
~/tech

ラベルが少ないときの機械学習:半教師あり学習・能動学習のしくみ

published

機械学習で手間がかかるのは、モデルの学習より 正解ラベルを付ける作業 であることがよくあります。画像の不良品を 1 枚ずつ見て判定する、問い合わせのメールを 1 通ずつ読んで分類する、医師が画像に病変の印を付ける、といった作業は、件数が増えるほど時間とお金がかかります。一方で、ラベルの付いていないデータ(撮っただけの画像、届いただけのメール)は、たくさん手に入ることが多いものです。

この記事では、ラベルの少なさを補う 2 つの方法を説明します。

  • 半教師あり学習:少しのラベル付きのデータと、たくさんのラベルなしのデータを一緒に使って学習する
  • 能動学習:どのデータにラベルを付けるかをモデルに選ばせて、少ないラベルで効率よく学習する

結論を先に書くと、次のようになります。

方法計算例での結果効く条件
自己訓練(疑似ラベル)ラベル 6 件で、正解率 0.806 → 0.922同じクラスのデータがまとまっていて、クラスの間に隙間がある
ラベル拡散ラベル 6 件で、正解率 0.806 → 0.935同上。データの形(つながり方)を使う
不確実性サンプリングラベル 40 件で、ランダムに選ぶより正解率 0.847 → 0.881モデルが「迷っている」場所が、本当の境界の近くにある

どちらの方法も、データの分布についての仮定 に頼っています。クラスどうしが大きく重なっていると、半教師あり学習の効き目はほとんどなくなりました。

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

環境

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

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

半教師あり学習:ラベルなしのデータの形を使う

ラベルなしのデータが役に立つ理由

ラベルなしのデータには答えが書いてありません。それでも役に立つのは、データがどこに集まり、どうつながっているか が分かるからです。半教師あり学習は、次のような仮定を置きます。

仮定内容
なめらかさの仮定近くにある点どうしは、同じクラスになりやすい
クラスターの仮定同じクラスの点はまとまりを作る。クラスの境界は、データの少ない隙間を通る
多様体の仮定高い次元のデータも、実際には低い次元の曲がった面の上に並んでいる

ラベル付きのデータが少ないと、境界をどこに引けばよいかが決められません。ラベルなしのデータで「隙間」が見えれば、境界はそこに引けばよいと分かります。

計算例のデータ:2 つの三日月

2 つの三日月がかみ合った形のデータを 600 点作り、そのうち 各クラス 3 点、合計 6 点だけ にラベルを付けます。残りの 594 点はラベルなしで、これらのラベルを当てる問題です。

import numpy as np

def two_moons(rng, n, noise=0.12):
    """2 つの三日月が組み合わさった形のデータ"""
    t = rng.random(n) * np.pi
    y = rng.integers(2, size=n)
    x = np.where(y == 0, np.cos(t), 1 - np.cos(t))
    z = np.where(y == 0, np.sin(t), 0.5 - np.sin(t))
    return np.column_stack([x, z]) + rng.normal(0, noise, (n, 2)), y

def knn_predict(Xl, yl, X, k=1):
    d = ((X[:, None] - Xl[None]) ** 2).sum(-1)
    nn = np.argsort(d, 1)[:, :k]
    return (yl[nn].mean(1) > 0.5).astype(int)

基準は、6 点のラベルだけを使い、それぞれの点にいちばん近いラベル付きの点の答えを返す 1-近傍法です(深層学習以前の機械学習 の k 近傍法)。三日月は曲がっているので、6 点からの距離だけで決めると、三日月の先端の部分を、もう一方のクラスと間違えやすくなります。

自己訓練:自信のある予測を仮のラベルにする

自己訓練(self-training)は、いちばん素朴な半教師あり学習です。

  1. ラベル付きのデータでモデルを作る
  2. ラベルなしのデータを予測し、自信のある予測 を仮のラベル(疑似ラベル)として、ラベル付きのデータに加える
  3. 1 に戻る

ここでは、ラベル付きの点にいちばん近いラベルなしの点を 50 点ずつ選んで、近いラベル付きの点と同じラベルを付ける、という操作を 10 回くり返します。ラベルが、データのつながりに沿って少しずつ広がっていきます。

def self_training(Xl, yl, Xu, rounds=10, per_round=50):
    """自信のある予測から順に、仮のラベルとして足していく"""
    Xl, yl, Xu = Xl.copy(), yl.copy(), Xu.copy()
    for _ in range(rounds):
        d = ((Xu[:, None] - Xl[None]) ** 2).sum(-1)
        nn = np.argsort(d, 1)[:, :1]
        conf = -d[np.arange(len(Xu)), nn[:, 0]]           # いちばん近いラベル付きの点に近いほど自信がある
        pick = np.argsort(-conf)[:per_round]
        Xl = np.vstack([Xl, Xu[pick]]); yl = np.concatenate([yl, yl[nn[pick, 0]]])
        Xu = np.delete(Xu, pick, 0)
    return Xl, yl

ラベル拡散:グラフの上でラベルをしみ出させる

ラベル拡散(label spreading, Zhou ら, 2004)は、すべての点を、近い 10 点とつないだ グラフ にして、ラベル付きの点から隣へ、隣からさらにその隣へと、ラベルをしみ出させる方法です。グラフニューラルネットワークのしくみ の「隣から情報を集める」計算を、学習なしでくり返すのに近い考え方です。

F ← α × S × F + (1 − α) × Y₀

F は各点の「クラスごとの点数」、Y₀ は最初のラベル(ラベル付きの点だけ 1)、S はつながりの強さを正規化した行列です。α は、隣から受け取る割合と、最初のラベルに引き戻す割合の釣り合いを決めます。くり返すと F は一定の値に落ち着くので、点数の大きいクラスを答えにします。

def label_spreading(X, labeled, yl, k=10, alpha=0.99, iters=200):
    """近い点どうしをつないだグラフの上で、ラベルをしみ出させる(Zhou ら, 2004)"""
    n = len(X)
    d = ((X[:, None] - X[None]) ** 2).sum(-1)
    W = np.zeros((n, n))
    nn = np.argsort(d, 1)[:, 1:k + 1]
    rows = np.repeat(np.arange(n), k)
    W[rows, nn.ravel()] = np.exp(-d[rows, nn.ravel()] / 0.05)
    W = np.maximum(W, W.T)                             # つながりを左右対称にする
    Dinv = 1 / np.sqrt(W.sum(1))
    S = Dinv[:, None] * W * Dinv[None]
    Y0 = np.zeros((n, 2)); Y0[labeled, yl] = 1
    F = Y0.copy()
    for _ in range(iters):
        F = alpha * S @ F + (1 - alpha) * Y0           # 隣から受け取りつつ、最初のラベルにも引き戻す
    return F.argmax(1)

3 つを比べる

ラベルを付ける 6 点の選び方を変えて 10 回試し、ラベルなしの 594 点での正解率の平均と最低を比べます。三日月の太さ(ノイズの大きさ)も 2 通り試します。

for noise in (0.12, 0.3):
  res = {"ラベル付きの 6 点だけ(1-NN)": [], "自己訓練": [], "ラベル拡散": []}
  for seed in range(10):
      rng = np.random.default_rng(seed)
      X, y = two_moons(rng, 600, noise)
      labeled = np.concatenate([rng.choice(np.where(y == c)[0], 3, replace=False) for c in (0, 1)])
      unl = np.setdiff1d(np.arange(len(y)), labeled)
      res["ラベル付きの 6 点だけ(1-NN)"].append(np.mean(knn_predict(X[labeled], y[labeled], X[unl]) == y[unl]))
      Xl, yl = self_training(X[labeled], y[labeled], X[unl])
      res["自己訓練"].append(np.mean(knn_predict(Xl, yl, X[unl]) == y[unl]))
      res["ラベル拡散"].append(np.mean(label_spreading(X, labeled, y[labeled])[unl] == y[unl]))
  for k, v in res.items():
      print(f"ノイズ {noise}: {k}: 平均 {np.mean(v):.3f}, 最低 {np.min(v):.3f}")

実行結果:

ノイズ 0.12: ラベル付きの 6 点だけ(1-NN): 平均 0.806, 最低 0.668
ノイズ 0.12: 自己訓練: 平均 0.922, 最低 0.722
ノイズ 0.12: ラベル拡散: 平均 0.935, 最低 0.818
ノイズ 0.3: ラベル付きの 6 点だけ(1-NN): 平均 0.726, 最低 0.498
ノイズ 0.3: 自己訓練: 平均 0.726, 最低 0.487
ノイズ 0.3: ラベル拡散: 平均 0.786, 最低 0.507

結果の読み方

  • 三日月がはっきり分かれているとき(ノイズ 0.12)は、ラベルなしのデータを使うと大きく良くなる。6 点だけの 0.806 に対して、自己訓練で 0.922、ラベル拡散で 0.935 になりました。ラベルが三日月に沿って広がり、先端の部分まで正しいラベルが届くからです
  • 最低の値に注意。自己訓練は、最低が 0.722 です。最初の 6 点の位置が悪いと、早い段階で間違った疑似ラベルを付け、その間違いがさらに広がっていきます(確証バイアス、確認の偏り)。ラベル拡散は、すべての点を同時に考えるので、この崩れ方が小さくなっています
  • 三日月が重なっているとき(ノイズ 0.3)は、効き目がほとんどなくなる。クラスの間に隙間がないので、ラベルが境界を越えてもう一方のクラスへしみ出してしまいます。自己訓練は 6 点だけのときと同じ 0.726 で、最低はむしろ下がりました。半教師あり学習は、仮定が成り立たないと役に立たず、悪くすることさえある 方法です

深層学習での半教師あり学習

画像や文章のニューラルネットワークでも、同じ考え方が使われています。

方法内容
疑似ラベル(Lee, 2013)自信のある予測を仮のラベルにして学習する。自己訓練をニューラルネットワークで行う
一貫性の正則化同じラベルなしの画像に、少し違う加工(反転・色の変化など)をしても、予測が変わらないように学習する
FixMatch(Sohn ら, 2020)弱い加工をした画像の自信のある予測を、強い加工をした画像の正解として学習する。疑似ラベルと一貫性の正則化の組み合わせ
Noisy Student(Xie ら, 2020)大きな「生徒」モデルを、加工とドロップアウトでノイズを入れながら、「先生」の疑似ラベルで学習させ、それを次の先生にする

さらに今では、大量のラベルなしのデータで先に 自己教師あり学習(自己教師あり学習と CLIP のしくみ)を行い、少ないラベルで 転移学習 するのが、ラベルの少なさに対するもっとも強力な方法の 1 つになっています。

能動学習:どれにラベルを付けるかを選ぶ

ランダムに選ぶのはもったいない

ラベル付けの予算が 40 件分しかないとき、ラベルなしのデータからランダムに 40 件を選ぶのが、いちばん良い選び方とは限りません。誰が見ても明らかに陽性・陰性のデータにラベルを付けても、モデルはあまり賢くなりません。モデルが迷っているデータ にラベルを付けてもらう方が、境界の位置をはっきりさせるのに役立ちます。

このように、学習しながら「次にどのデータのラベルを聞くか」をモデルが選ぶ方法を 能動学習(active learning)と呼びます。

手順内容
1少しのラベル付きのデータでモデルを作る(最初はランダムに選ぶ)
2ラベルなしのデータの中から、次にラベルを聞く点を選ぶ
3人がラベルを付ける
4モデルを学習し直して、2 に戻る

不確実性サンプリング

2 の選び方で、いちばんよく使われるのが 不確実性サンプリング です。2 値分類なら、予測確率が 0.5 にいちばん近い点、つまりモデルがいちばん迷っている点を選びます。

10 次元のデータのうち 2 次元だけが答えに効く問題で、2000 件のラベルなしのデータから、ロジスティック回帰のために 1 件ずつラベルを聞いていきます。最初の 4 件はランダムに選びます。ランダムに選ぶ場合と比べ、20 通りの乱数の種で、ラベル 10・20・40・80 件のときの評価用のデータでの正解率を平均します。

def fit_logreg(X, y, steps=30, l2=1.0):
    Xb = np.column_stack([np.ones(len(X)), X]); beta = np.zeros(Xb.shape[1])
    for _ in range(steps):
        p = 1 / (1 + np.exp(-Xb @ beta))
        hess = (Xb * (p * (1 - p))[:, None]).T @ Xb + l2 * np.eye(len(beta))
        beta -= np.linalg.solve(hess, Xb.T @ (p - y) + l2 * beta)
    return beta

def proba(beta, X):
    return 1 / (1 + np.exp(-(beta[0] + X @ beta[1:])))

def make_pool(rng, n):
    """10 次元のうち、答えに効くのは 2 次元だけ。境界の近くのデータは少ない"""
    X = rng.normal(size=(n, 10))
    y = (X[:, 0] + 0.5 * X[:, 1] + rng.normal(0, 0.3, n) > 0).astype(int)
    return X, y

def active_learning(rng, X, y, Xte, yte, budget, strategy, n_init=4):
    labeled = list(rng.choice(len(y), n_init, replace=False))   # 最初の数件はランダムに選ぶ
    accs = {}
    while len(labeled) <= budget:
        beta = fit_logreg(X[labeled], y[labeled])
        accs[len(labeled)] = np.mean((proba(beta, Xte) > 0.5) == yte)
        rest = np.setdiff1d(np.arange(len(y)), labeled)
        if strategy == "random":
            pick = rng.choice(rest)
        else:
            pick = rest[np.argmin(np.abs(proba(beta, X[rest]) - 0.5))]   # いちばん迷っている点
        labeled.append(pick)
    return accs

checkpoints = (10, 20, 40, 80)
for strategy, name in (("random", "ランダムに選ぶ"), ("uncertainty", "迷っている点を選ぶ")):
    table = {c: [] for c in checkpoints}
    for seed in range(20):
        rng = np.random.default_rng(seed)
        X, y = make_pool(rng, 2000); Xte, yte = make_pool(rng, 5000)
        accs = active_learning(rng, X, y, Xte, yte, max(checkpoints), strategy)
        for c in checkpoints:
            table[c].append(accs[c])
    print(name, {c: round(float(np.mean(v)), 3) for c, v in table.items()})

rng = np.random.default_rng(0)
X, y = make_pool(rng, 2000); Xte, yte = make_pool(rng, 5000)
print("2000 件すべてにラベルを付けたとき", round(float(np.mean((proba(fit_logreg(X, y), Xte) > 0.5) == yte)), 3))

実行結果:

ランダムに選ぶ {10: 0.734, 20: 0.798, 40: 0.847, 80: 0.888}
迷っている点を選ぶ {10: 0.731, 20: 0.811, 40: 0.881, 80: 0.901}
2000 件すべてにラベルを付けたとき 0.91
  • 10 件の時点では差がない。モデルがまだ不正確なので、「迷っている点」も的外れな場所になりがちです
  • 20 件を過ぎると差が開く。迷っている点を選ぶと 40 件で 0.881 になり、ランダムに選んだときの 80 件(0.888)に近い成績です。ラベル付けの手間を、およそ半分にできたことになります
  • 80 件で 0.901 と、2000 件すべてにラベルを付けたとき(0.910)にかなり近い。境界から遠いデータは、ラベルがなくても答えが明らかなので、それらにラベルを付ける手間を省けています

能動学習の注意点

注意内容
最初はランダムにモデルが不正確なうちは、迷っている点の選び方も当てにならない(コールドスタート)。最初の数十件はランダムに選ぶ
選んだデータは偏っている境界の近くのデータばかりになるので、選んだデータでモデルの成績を評価してはいけない。評価用のデータは別にランダムに取っておく
似た点ばかり選ぶ1 度に多くの点を選ぶと、同じあたりの似た点ばかりになる。迷っている点の中から、ばらけるように選ぶ(多様性を考える)工夫がある
ノイズを選びやすいラベルを付ける人でも判断できないような、本当にあいまいなデータばかりを選んでしまうことがある
モデルを変えると効かないこともロジスティック回帰で選んだデータが、勾配ブースティングにとっても良いデータとは限らない

迷い方を測るいろいろな方法

方法選ぶ点
最小確信度いちばん高いクラスの確率が、いちばん低い点
マージン1 位と 2 位のクラスの確率の差が、いちばん小さい点
エントロピークラスの確率の分布が、いちばん平らな点
委員会による質問(Seung ら, 1992)複数のモデルの予測が、いちばん割れている点
コアセット(Sener と Savarese, 2018)選んだ点の集まりが、データ全体をまんべんなく覆うような点

そのほかの方法:ラベル付けの手間を減らす

方法内容
弱教師あり学習「件名に『請求』があれば請求の問い合わせ」のような、不正確でも大量に適用できるルールでラベルを付け、ルールどうしの食い違いから正しさを推定する(Snorkel など)
LLM によるラベル付けLLM にラベルを付けさせ、人は確認と修正だけをする。基準を書いた指示文と、少しの例を渡す
ゼロショット・少数ショットCLIP や LLM のように、事前学習済みのモデルに、クラスの説明や数件の例だけで分類させる

ラベルが少ないときの手法の流れ

年手法・出来事ポイント
1965自己訓練(Scudder)自分の予測を使って学習をくり返す、半教師あり学習のはじまり
1992委員会による質問複数のモデルの意見が割れる点のラベルを聞く
1994不確実性サンプリング(Lewis と Gale)いちばん迷っている点のラベルを聞く
1998共訓練(Blum と Mitchell)2 種類の見方(特徴の組)でそれぞれモデルを作り、互いの自信のある予測を教え合う
2002・2004ラベル伝播・ラベル拡散データのグラフの上で、ラベルを広げる
2013疑似ラベルニューラルネットワークで、自信のある予測を仮のラベルにする
2018コアセットによる能動学習データ全体を覆うように選ぶ
2020FixMatch・Noisy Student一貫性の正則化と疑似ラベルの組み合わせで、ラベルの少ない画像分類の成績を大きく上げた

まとめ

  • 半教師あり学習は、ラベルなしのデータから「データのまとまりと隙間」を読み取り、少ないラベルを広げる。三日月のデータでは、ラベル 6 件で正解率が 0.806 から 0.93 前後に上がった
  • 自己訓練は、最初の間違いが広がる確証バイアスに弱い。グラフ全体で考えるラベル拡散の方が崩れにくかった
  • クラスどうしが重なって隙間がないと、半教師あり学習はほとんど効かず、悪くすることもある
  • 能動学習は、モデルが迷っている点のラベルを聞く。計算例では、ランダムに選ぶ場合の半分くらいのラベルで同じ成績になった
  • 能動学習で選んだデータは偏っているので、評価用のデータは別にランダムに取っておく

参考

  • H. J. Scudder, "Probability of Error of Some Adaptive Pattern-Recognition Machines", IEEE Transactions on Information Theory 1965
  • H. S. Seung, M. Opper and H. Sompolinsky, "Query by Committee", COLT 1992
  • D. D. Lewis and W. A. Gale, "A Sequential Algorithm for Training Text Classifiers", SIGIR 1994
  • A. Blum and T. Mitchell, "Combining Labeled and Unlabeled Data with Co-Training", COLT 1998
  • X. Zhu and Z. Ghahramani, "Learning from Labeled and Unlabeled Data with Label Propagation", Technical Report CMU-CALD-02-107, 2002
  • D. Zhou et al., "Learning with Local and Global Consistency", NIPS 2004
  • O. Chapelle, B. Schölkopf and A. Zien (eds.), "Semi-Supervised Learning", MIT Press 2006
  • B. Settles, "Active Learning Literature Survey", Computer Sciences Technical Report 1648, University of Wisconsin–Madison 2009
  • D.-H. Lee, "Pseudo-Label: The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks", ICML 2013 Workshop
  • O. Sener and S. Savarese, "Active Learning for Convolutional Neural Networks: A Core-Set Approach", ICLR 2018(arXiv:1708.00489)
  • A. Ratner et al., "Snorkel: Rapid Training Data Creation with Weak Supervision", VLDB 2018(arXiv:1711.10160)
  • K. Sohn et al., "FixMatch: Simplifying Semi-Supervised Learning with Consistency and Confidence", NeurIPS 2020(arXiv:2001.07685)
  • Q. Xie et al., "Self-training with Noisy Student improves ImageNet classification", CVPR 2020(arXiv:1911.04252)