ラベルが少ないときの機械学習:半教師あり学習・能動学習のしくみ
機械学習で手間がかかるのは、モデルの学習より 正解ラベルを付ける作業 であることがよくあります。画像の不良品を 1 枚ずつ見て判定する、問い合わせのメールを 1 通ずつ読んで分類する、医師が画像に病変の印を付ける、といった作業は、件数が増えるほど時間とお金がかかります。一方で、ラベルの付いていないデータ(撮っただけの画像、届いただけのメール)は、たくさん手に入ることが多いものです。
この記事では、ラベルの少なさを補う 2 つの方法を説明します。
- 半教師あり学習:少しのラベル付きのデータと、たくさんのラベルなしのデータを一緒に使って学習する
- 能動学習:どのデータにラベルを付けるかをモデルに選ばせて、少ないラベルで効率よく学習する
結論を先に書くと、次のようになります。
| 方法 | 計算例での結果 | 効く条件 |
|---|---|---|
| 自己訓練(疑似ラベル) | ラベル 6 件で、正解率 0.806 → 0.922 | 同じクラスのデータがまとまっていて、クラスの間に隙間がある |
| ラベル拡散 | ラベル 6 件で、正解率 0.806 → 0.935 | 同上。データの形(つながり方)を使う |
| 不確実性サンプリング | ラベル 40 件で、ランダムに選ぶより正解率 0.847 → 0.881 | モデルが「迷っている」場所が、本当の境界の近くにある |
どちらの方法も、データの分布についての仮定 に頼っています。クラスどうしが大きく重なっていると、半教師あり学習の効き目はほとんどなくなりました。
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.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 に戻る
ここでは、ラベル付きの点にいちばん近いラベルなしの点を 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 | コアセットによる能動学習 | データ全体を覆うように選ぶ |
| 2020 | FixMatch・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)