転移学習とファインチューニングのしくみ:少ないデータで学習済みモデルを使い回す
画像検査や文書の分類を AI で作ろうとすると、まずぶつかるのが「正解ラベル付きのデータが少ない」という問題です。数百枚の画像から、ニューラルネットワークをゼロから学習させても、なかなか良い成績は出ません。
そこで使われるのが 転移学習 です。大量のデータがある別の問題(たとえば 1000 種類の物体の分類)で学習したモデルを持ってきて、その中身を新しい問題に使い回します。製造業の画像検査 AI の使い方と作り方 で出てきた「学習済みのモデルの特徴を使う」方法も、LLM を自分の用途に合わせる「ファインチューニング」も、転移学習の一種です。
この記事では、小さなニューラルネットワークで「元の問題」と「新しい問題」を作り、次のことを確かめます。
- 転移学習が データの少ないとき にどれだけ効くか
- 最後の層だけを学習する 方法と、全体を微調整する 方法をどう使い分けるか
- 微調整の 学習率 を大きくすると何が起きるか
結論を先に書くと、次のようになります。
| 状況 | 向いている方法 | 理由 |
|---|---|---|
| 新しい問題のデータが少なく、元の問題と近い | 最後の層だけを学習する | 学習済みの特徴がそのまま使える。調整する重みが少ないので過学習しにくい |
| 新しい問題が元の問題と遠い | 全体を小さな学習率で微調整する | 特徴そのものを新しい問題に合わせ直す必要がある |
| 新しい問題のデータが十分に多い | 全体の微調整、またはゼロからの学習 | 事前学習の効き目は、データが増えるほど小さくなる |
どの場合も、微調整の学習率は事前学習のときより小さくするのが基本です。大きくすると、学習済みの特徴が壊れ、元の問題もほとんど解けなくなります。
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.5 |
転移学習の考え方
ニューラルネットワークの前半は「特徴を作る部分」
画像分類 AI のしくみ で説明したように、画像を分類するニューラルネットワークの前の方の層は、縁・模様・部品のような 特徴 を作り、最後の層がその特徴から「どのクラスか」を決めます。前の方の層が作る特徴は、猫と犬を見分けるときにも、傷のある製品と良品を見分けるときにも役に立つ、汎用的なものです。
転移学習では、ネットワークを 2 つの部分に分けて考えます。
| 部分 | 役割 | 転移学習での扱い |
|---|---|---|
| 体(バックボーン) | 入力から特徴を作る | 元の問題で学習したものを持ってくる |
| 頭(ヘッド) | 特徴から答えを出す | 新しい問題に合わせて作り直す(クラスの数が違うため) |
3 つのやり方
| やり方 | 学習する重み | 呼び方 |
|---|---|---|
| ゼロから学習する | 全部(乱数から始める) | — |
| 体を固定して、頭だけを学習する | 頭だけ | 線形プローブ、特徴抽出 |
| 体も頭も学習する | 全部(体は学習済みの値から始める) | ファインチューニング(微調整) |
実験の準備
元の問題と新しい問題を作る
本物の画像を使うかわりに、「共通の特徴」から 2 つの問題を作ります。30 次元の入力から、2 段の非線形の変換で 8 個の「本当の特徴」を作り、元の問題(10 クラス)と新しい問題(2 クラス)の答えを、その特徴の別々の線形な組み合わせで決めます。2 つの問題は、答えの決め方は違いますが、役に立つ特徴が共通している という関係です。画像の場合の「物体の分類」と「製品の検査」の関係を、小さくまねたものです。
モデルは、64 個のニューロンの隠れ層が 2 つあるネットワークです。最適化には Adam を使います。
import numpy as np
D, K = 30, 8 # 入力の次元、本当の特徴の数
world = np.random.default_rng(42)
A1 = world.normal(0, 1, (D, 32)) / np.sqrt(D)
A2 = world.normal(0, 1, (32, K)) / np.sqrt(32)
W_src = world.normal(0, 1, (K, 10)) # 元の問題:10 クラス
w_tgt = world.normal(0, 1, (K, 2)) # 新しい問題:2 クラス
def true_features(X):
return np.tanh(np.tanh(X @ A1 * 2) @ A2 * 3)
def make(rng, n, W):
X = rng.normal(size=(n, D))
logits = true_features(X) @ W * 3
return X, logits.argmax(1)
def init(rng, n_out, H=64):
return {"W1": rng.normal(0, 1/np.sqrt(D), (D, H)), "b1": np.zeros(H),
"W2": rng.normal(0, 1/np.sqrt(H), (H, H)), "b2": np.zeros(H),
"W3": rng.normal(0, 1/np.sqrt(H), (H, n_out)), "b3": np.zeros(n_out)}
def forward(p, X):
h1 = np.maximum(0, X @ p["W1"] + p["b1"])
h2 = np.maximum(0, h1 @ p["W2"] + p["b2"])
return h1, h2, h2 @ p["W3"] + p["b3"]
def grads(p, X, y):
h1, h2, z = forward(p, X)
z = z - z.max(1, keepdims=True)
P = np.exp(z); P /= P.sum(1, keepdims=True)
dz = P; dz[np.arange(len(y)), y] -= 1; dz /= len(y)
g = {"W3": h2.T @ dz, "b3": dz.sum(0)}
dh2 = (dz @ p["W3"].T) * (h2 > 0)
g["W2"], g["b2"] = h1.T @ dh2, dh2.sum(0)
dh1 = (dh2 @ p["W2"].T) * (h1 > 0)
g["W1"], g["b1"] = X.T @ dh1, dh1.sum(0)
return g
train は、trainable に指定した重みだけを更新します。体を固定するときは、頭の重み(W3・b3)だけを渡します。
def train(p, X, y, steps, lr, trainable, rng, batch=64):
m = {k: np.zeros_like(v) for k, v in p.items()}; v = {k: np.zeros_like(v) for k, v in p.items()}
for t in range(1, steps + 1):
idx = rng.choice(len(y), min(batch, len(y)), replace=False)
g = grads(p, X[idx], y[idx])
for k in trainable:
m[k] = 0.9 * m[k] + 0.1 * g[k]; v[k] = 0.999 * v[k] + 0.001 * g[k]**2
p[k] -= lr * (m[k] / (1 - 0.9**t)) / (np.sqrt(v[k] / (1 - 0.999**t)) + 1e-8)
return p
def acc(p, X, y):
return np.mean(forward(p, X)[2].argmax(1) == y)
ALL = ["W1", "b1", "W2", "b2", "W3", "b3"]
HEAD = ["W3", "b3"]
元の問題で事前学習する
元の問題は、データが 20000 件あるとします。
rng = np.random.default_rng(0)
Xs, ys = make(rng, 20000, W_src)
Xs_test, ys_test = make(rng, 5000, W_src)
pre = train(init(rng, 10), Xs, ys, 6000, 1e-3, ALL, rng, batch=128)
print("事前学習:元の問題の正解率", round(acc(pre, Xs_test, ys_test), 3))
実行結果:
事前学習:元の問題の正解率 0.655
10 クラスで当て推量なら 0.1 なので、本当の特徴をある程度つかめています。この pre が「学習済みのモデル」です。
データの量で比べる
3 つのやり方を比べる
新しい問題のデータを 20・100・500・5000 件にして、3 つのやり方を比べます。転移学習のときは、頭を 2 クラス用に作り直します。全体の微調整の学習率は、事前学習(1e-3)の 10 分の 1 の 1e-4 にしています。乱数の種を変えて 5 回ずつ学習させ、5000 件の評価用のデータでの正解率を平均します。
import copy
def new_head(p, rng, n_out=2):
q = copy.deepcopy(p)
q["W3"] = rng.normal(0, 1/np.sqrt(64), (64, n_out)); q["b3"] = np.zeros(n_out)
return q
Xt_test, yt_test = make(np.random.default_rng(99), 5000, w_tgt)
print("新しい問題の多数派の割合", round(max(yt_test.mean(), 1 - yt_test.mean()), 3))
for n in (20, 100, 500, 5000):
res = {"ゼロから": [], "最後の層だけ": [], "全体を微調整": []}
for seed in range(5):
rng = np.random.default_rng(seed)
Xt, yt = make(rng, n, w_tgt)
res["ゼロから"].append(acc(train(init(rng, 2), Xt, yt, 1000, 1e-3, ALL, rng), Xt_test, yt_test))
res["最後の層だけ"].append(acc(train(new_head(pre, rng), Xt, yt, 1000, 1e-3, HEAD, rng), Xt_test, yt_test))
res["全体を微調整"].append(acc(train(new_head(pre, rng), Xt, yt, 1000, 1e-4, ALL, rng), Xt_test, yt_test))
print(n, {k: round(float(np.mean(v)), 3) for k, v in res.items()})
実行結果:
新しい問題の多数派の割合 0.504
20 {'ゼロから': 0.626, '最後の層だけ': 0.744, '全体を微調整': 0.683}
100 {'ゼロから': 0.729, '最後の層だけ': 0.789, '全体を微調整': 0.772}
500 {'ゼロから': 0.773, '最後の層だけ': 0.819, '全体を微調整': 0.811}
5000 {'ゼロから': 0.809, '最後の層だけ': 0.832, '全体を微調整': 0.823}
結果の読み方
- データが少ないほど、転移学習の効き目が大きい。20 件では、ゼロからの 0.626 に対して、最後の層だけの学習で 0.744 になりました。ゼロから学習したモデルは、500 件でようやく 0.773 で、転移学習なら 100 件で同じくらいの成績が出ています
- データが増えると差は縮む。5000 件では、ゼロからでも 0.809 まで上がり、差は 0.02 ほどです
- この問題では、最後の層だけの学習がいちばん良い。新しい問題は、元の問題と同じ特徴の別の組み合わせで答えが決まるので、学習済みの特徴をそのまま使うのがいちばん効率的です。全体を微調整すると、学習する重みが多いぶん、少ないデータでは学習用のデータに合わせすぎやすくなります(過学習と正則化のしくみ)
問題の近さで比べる
遠い問題を作る
次に、元の問題と 遠い 新しい問題を作ります。1 段目の変換は共通ですが、2 段目の変換を別のものに変えて、答えに関係する特徴そのものを変えます。画像でいえば、縁や模様のような低いレベルの特徴は共通でも、どの部品の組み合わせが大事かは違う、という状況です。
# 遠い問題:1 層目の特徴は共通だが、2 層目は元の問題と別のもの
A2_far = np.random.default_rng(7).normal(0, 1, (32, K)) / np.sqrt(32)
def make_far(rng, n):
X = rng.normal(size=(n, D))
z = np.tanh(np.tanh(X @ A1 * 2) @ A2_far * 3)
return X, (z @ w_tgt * 3).argmax(1)
Xf_test, yf_test = make_far(np.random.default_rng(99), 5000)
for n in (100, 500, 5000):
res = {"ゼロから": [], "最後の層だけ": [], "全体を微調整": []}
for seed in range(5):
rng = np.random.default_rng(seed)
Xt, yt = make_far(rng, n)
res["ゼロから"].append(acc(train(init(rng, 2), Xt, yt, 1000, 1e-3, ALL, rng), Xf_test, yf_test))
res["最後の層だけ"].append(acc(train(new_head(pre, rng), Xt, yt, 1000, 1e-3, HEAD, rng), Xf_test, yf_test))
res["全体を微調整"].append(acc(train(new_head(pre, rng), Xt, yt, 1000, 1e-4, ALL, rng), Xf_test, yf_test))
print(n, {k: round(float(np.mean(v)), 3) for k, v in res.items()})
実行結果:
100 {'ゼロから': 0.691, '最後の層だけ': 0.688, '全体を微調整': 0.702}
500 {'ゼロから': 0.738, '最後の層だけ': 0.733, '全体を微調整': 0.748}
5000 {'ゼロから': 0.771, '最後の層だけ': 0.752, '全体を微調整': 0.771}
今度は結果が変わりました。
- 最後の層だけの学習は、ゼロからの学習と同じくらいか、それより悪い。5000 件では 0.752 で、ゼロからの 0.771 に負けています。学習済みの特徴は元の問題のためのものなので、遠い問題の答えに必要な情報が十分に入っていません。体を固定すると、それ以上良くなれません
- 全体の微調整は、データが少ないときにいちばん良い。特徴を新しい問題に合わせて直しつつ、1 段目の共通の部分は学習済みの値から始められるからです
問題が近ければ体を固定し、遠ければ体も動かす、というのが使い分けの基本です。実際の画像のモデルでは、その中間として「前の方の層は固定し、後ろの方の層だけを微調整する」こともよく行います。前の方の層ほど汎用的な特徴を作っているためです(Yosinski ら, 2014)。
微調整の学習率と「元の問題を忘れる」現象
学習率を変えて比べる
近い方の新しい問題(100 件)で、全体の微調整の学習率を変えて比べます。あわせて、微調整したあとの体に、事前学習のときの頭を付け直して、元の問題がまだ解けるか も測ります。体の特徴がどれだけ変わったかの目安です。
「最後の層 → 全体(LP-FT)」は、まず頭だけを学習し、そのあと全体を小さな学習率で微調整する方法です(Kumar ら, 2022)。
Xt_test, yt_test = make(np.random.default_rng(99), 5000, w_tgt)
Xs_test, ys_test = make(np.random.default_rng(98), 5000, W_src)
def source_acc(q):
"""体の部分はそのまま、最後の層だけ事前学習のときのものに戻して、元の問題を解かせる"""
r = copy.deepcopy(q); r["W3"], r["b3"] = pre["W3"], pre["b3"]
return acc(r, Xs_test, ys_test)
print("事前学習のままの元の問題の正解率", round(acc(pre, Xs_test, ys_test), 3))
n = 100
methods = {
"最後の層だけ": lambda q, X, y, rng: train(q, X, y, 1000, 1e-3, HEAD, rng),
"全体を微調整(学習率 1e-4)": lambda q, X, y, rng: train(q, X, y, 1000, 1e-4, ALL, rng),
"全体を微調整(学習率 1e-2)": lambda q, X, y, rng: train(q, X, y, 1000, 1e-2, ALL, rng),
"最後の層 → 全体(LP-FT)": lambda q, X, y, rng: train(train(q, X, y, 1000, 1e-3, HEAD, rng), X, y, 1000, 1e-4, ALL, rng),
}
for name, f in methods.items():
tgt, src = [], []
for seed in range(5):
rng = np.random.default_rng(seed)
Xt, yt = make(rng, n, w_tgt)
q = f(new_head(pre, rng), Xt, yt, rng)
tgt.append(acc(q, Xt_test, yt_test)); src.append(source_acc(q))
print(f"{name}: 新しい問題 {np.mean(tgt):.3f}, 元の問題 {np.mean(src):.3f}")
実行結果:
事前学習のままの元の問題の正解率 0.655
最後の層だけ: 新しい問題 0.791, 元の問題 0.655
全体を微調整(学習率 1e-4): 新しい問題 0.772, 元の問題 0.603
全体を微調整(学習率 1e-2): 新しい問題 0.763, 元の問題 0.391
最後の層 → 全体(LP-FT): 新しい問題 0.786, 元の問題 0.632
1 つ前の実験の 100 件の結果(最後の層だけで 0.789)と少し違うのは、乱数の使われ方が違い、学習に使ったデータが別のものになっているためです。
結果の読み方
- 学習率を大きくすると、元の問題がほとんど解けなくなる。1e-2 では、元の問題の正解率が 0.655 から 0.391 まで下がりました。新しい問題の 100 件に合わせて、体の特徴が大きく書き換わったためです。これを 破滅的忘却(catastrophic forgetting)と呼びます。新しい問題の成績も、学習率 1e-4 のときより下がっています
- 最初に頭だけを学習しておくと、特徴が壊れにくい。頭を作り直した直後は、頭の重みが乱数なので、大きく間違った勾配が体にまで流れ込みます。LP-FT では、頭がある程度できてから体を動かすので、元の問題の正解率は 0.632 にとどまり、新しい問題の成績も全体の微調整より良くなりました
元の問題の成績を気にしない場合でも、特徴を壊さないことは、新しい問題の成績にとって大事です。学習済みの特徴は、新しい問題の少ないデータからは作れない情報を持っているからです。
実際に使うときのポイント
画像のモデル
| ポイント | 内容 |
|---|---|
| 学習済みのモデルを選ぶ | ImageNet などの大きなデータで学習した ResNet・Vision Transformer などを使う。自己教師あり学習のモデル(自己教師あり学習と CLIP のしくみ)は、さまざまな問題に転移しやすい |
| 入力をそろえる | 画像の大きさ・色の正規化(平均と標準偏差)を、事前学習のときと同じにする |
| 学習率 | 体は小さく、頭は大きくする(層ごとに学習率を変える)。全体で同じにするなら、事前学習より 1 桁ほど小さくする |
| データ拡張 | データが少ないときは、反転・切り抜き・色の変化などで水増しする |
| 正規化の層 | データが少ないときは、バッチ正規化の統計を固定することもある(正規化層のしくみ) |
言語モデル
LLM を自分の用途に合わせるときも、考え方は同じです。
| 方法 | 内容 |
|---|---|
| プロンプトだけで使う | 重みは変えず、指示と例を入力に入れる。学習が要らない |
| 全体の微調整 | すべての重みを更新する。大きなモデルではメモリーが足りなくなりやすい |
| LoRA などの省メモリーの微調整 | 元の重みは固定し、小さな追加の行列だけを学習する(LLM を小さく速く動かすしくみ)。元の重みが変わらないので、忘却も起きにくい |
| 指示チューニング・RLHF | 質問に答える形や好ましい答え方を学習する(LLM の学習のしくみ) |
転移学習の手法の流れ
| 年 | 手法・出来事 | ポイント |
|---|---|---|
| 1976 | 転移の研究の始まり(Bozinovski と Fulgosi) | 学習したことを別の問題に移す効果を、ニューラルネットワークで調べた |
| 1989 | 破滅的忘却の指摘(McCloskey と Cohen) | 新しい問題を学習すると、前の問題を急に忘れることを示した |
| 2009 | ImageNet | 大規模な画像のデータセット。事前学習の元として広く使われるようになる |
| 2014 | CNN の特徴の転移(Razavian ら、Yosinski ら) | ImageNet で学習した CNN の特徴が、さまざまな画像の問題に効くことを示した |
| 2017 | 弾性重み固定(EWC) | 元の問題に大事な重みを動かしにくくして、忘却を減らす |
| 2018 | ULMFiT・BERT | 言語モデルの事前学習と微調整の組み合わせが、自然言語処理の標準になる |
| 2019 | アダプター | 小さな追加の層だけを学習する、省メモリーの微調整 |
| 2021 | LoRA | 重みの変化を低ランクの行列で表して学習する |
| 2022 | LP-FT | 頭を先に学習してから全体を微調整し、特徴の崩れを減らす |
まとめ
- 転移学習は、元の問題で学習したモデルの体(特徴を作る部分)を、新しい問題に使い回す。データが少ないときほど効き目が大きい
- 新しい問題が元の問題に近ければ、体を固定して頭だけを学習するのが効率的。遠ければ、全体を小さな学習率で微調整する
- 微調整の学習率が大きいと、特徴が新しい少ないデータに合わせて書き換わり、元の問題を忘れる。新しい問題の成績も下がる
- 頭を先に学習してから全体を微調整すると、頭の乱数のせいで特徴が壊れるのを防げる
- LLM の微調整も同じ考え方で、LoRA のように元の重みを固定する方法がよく使われる
参考
- S. Bozinovski and A. Fulgosi, "The influence of pattern similarity and transfer learning upon training of a base perceptron B2", Proceedings of Symposium Informatica 1976
- M. McCloskey and N. J. Cohen, "Catastrophic Interference in Connectionist Networks: The Sequential Learning Problem", Psychology of Learning and Motivation 1989
- J. Deng et al., "ImageNet: A Large-Scale Hierarchical Image Database", CVPR 2009
- A. S. Razavian et al., "CNN Features off-the-shelf: an Astounding Baseline for Recognition", CVPR Workshops 2014(arXiv:1403.6382)
- J. Yosinski et al., "How transferable are features in deep neural networks?", NIPS 2014(arXiv:1411.1792)
- J. Kirkpatrick et al., "Overcoming catastrophic forgetting in neural networks", PNAS 2017(arXiv:1612.00796)
- J. Howard and S. Ruder, "Universal Language Model Fine-tuning for Text Classification", ACL 2018(arXiv:1801.06146)
- N. Houlsby et al., "Parameter-Efficient Transfer Learning for NLP", ICML 2019(arXiv:1902.00751)
- E. J. Hu et al., "LoRA: Low-Rank Adaptation of Large Language Models", ICLR 2022(arXiv:2106.09685)
- A. Kumar et al., "Fine-Tuning can Distort Pretrained Features and Underperform Out-of-Distribution", ICLR 2022(arXiv:2202.10054)