深層学習で高い性能を得るには大量のデータが必要ですが、医療画像や衛星画像などの専門分野では、ラベル付きデータを大量に集めることが困難な場合がほとんどです。たとえば、皮膚がんの画像分類を行いたいとして、皮膚科医が数万枚の画像にラベルを付けるのは現実的ではありません。
しかし、ImageNetの120万枚で学習されたResNetは、「エッジ」「テクスチャ」「形状」といった汎用的な視覚特徴を既に学習しています。皮膚がんの画像にもエッジやテクスチャは存在するので、この「知識」を再利用できないでしょうか。
この「あるタスクで学習した知識を別のタスクに再利用する」というアイデアが転移学習(Transfer Learning)です。転移学習により、少量のデータでも高い性能を達成でき、学習時間も大幅に短縮されます。
転移学習を理解すると、以下のことが可能になります。
- 少量データでの高精度モデル構築: 数百枚の画像でも実用的な分類器を作れる
- 学習時間の短縮: ゼロからの学習に比べて桁違いに高速
- NLPでの応用: BERTやGPTの事前学習モデルを下流タスクに適用
- ドメイン適応: ソースドメインとターゲットドメインの違いに対処
本記事の内容
- 転移学習の理論的背景
- 特徴抽出器(Feature Extractor)としての利用
- ファインチューニング(Fine-tuning)の戦略
- ドメインシフトと転移可能性の分析
- Pythonによる実践的な実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- CNNの基礎 — 畳み込みネットワークの構造
- ResNetとスキップ接続 — 事前学習モデルの代表例
転移学習とは
基本的な考え方
転移学習の核心は、ソースタスク(source task)で学習した表現をターゲットタスク(target task)に再利用することです。
たとえるなら、ピアノを弾ける人がギターを始めるとき、音楽の基礎知識やリズム感は既に身についているため、ゼロからギターだけを練習する人より上達が早いのと同じです。ピアノで学んだ「音楽の知識」がギターに「転移」しているのです。
深層学習では、ImageNetなどの大規模データセットで事前学習されたモデルが「音楽の基礎知識」に相当します。浅い層は汎用的な低次特徴(エッジ、色、テクスチャ)を、深い層はタスク固有の高次特徴(犬の顔、車のタイヤ)を学習しています。
形式的な定義
ドメイン $\mathcal{D}$ は特徴空間 $\mathcal{X}$ と周辺確率分布 $P(\bm{X})$ の組 $\mathcal{D} = \{\mathcal{X}, P(\bm{X})\}$ です。
タスク $\mathcal{T}$ はラベル空間 $\mathcal{Y}$ と条件付き確率 $P(Y | \bm{X})$ の組 $\mathcal{T} = \{\mathcal{Y}, P(Y | \bm{X})\}$ です。
転移学習: ソースドメイン $\mathcal{D}_S$ とソースタスク $\mathcal{T}_S$ から得た知識を使って、ターゲットドメイン $\mathcal{D}_T$ でのターゲットタスク $\mathcal{T}_T$ の性能を改善すること。
ここで $\mathcal{D}_S \neq \mathcal{D}_T$ または $\mathcal{T}_S \neq \mathcal{T}_T$ です。
CNNが学習する特徴の階層性
Zeiler and Fergus (2013) は、CNNの各層が何を学習しているかを可視化しました。
| 層の深さ | 学習する特徴 | 転移可能性 |
|---|---|---|
| 浅い層(Conv1-2) | エッジ、色の勾配、テクスチャ | 非常に高い(ほぼ普遍的) |
| 中間層(Conv3-4) | テクスチャの組み合わせ、パターン | 高い |
| 深い層(Conv5+) | 物体の部分、意味的な構造 | タスク依存(転移しにくい) |
| 全結合層(FC) | タスク固有の分類 | 低い(再学習が必要) |
この階層性が転移学習を可能にしています。浅い層の特徴はドメインによらず普遍的なので、そのまま再利用できます。深い層は新しいタスクに合わせて調整(ファインチューニング)する必要があります。
この理論を踏まえて、転移学習の2つの主要な戦略を見ていきましょう。
戦略1: 特徴抽出器としての利用
方法
最もシンプルな転移学習の方法は、事前学習済みモデルを特徴抽出器(feature extractor)として使い、最終層だけを新しいタスク用に置き換えて学習する方法です。
- 事前学習済みモデル(例: ImageNetで学習したResNet-50)を読み込む
- 最後の全結合層を、新しいタスクのクラス数に合わせた層に置き換える
- 事前学習済みの重みは凍結(freeze)し、新しい層だけを学習する
$$ \hat{y} = \text{softmax}(\bm{W}_\text{new} \cdot \text{GAP}(\text{ResNet}_\text{frozen}(\bm{x})) + \bm{b}_\text{new}) $$
利点と適用場面
利点: – 学習パラメータが少ないため、少量のデータでも過学習しにくい – 計算が高速(特徴抽出は1回だけ計算してキャッシュできる) – 実装が簡単
適用場面: – ターゲットデータが少ない(数百〜数千枚) – ソースタスクとターゲットタスクが類似(例: ImageNet → 花の分類) – 計算リソースが限られている
ターゲットデータが増えるか、ソースとターゲットのドメインが大きく異なる場合は、より多くの層を再学習するファインチューニングが効果的です。
戦略2: ファインチューニング
方法
ファインチューニングでは、事前学習済みモデルの一部または全体のパラメータを、新しいタスクのデータで微調整します。
- 事前学習済みモデルを読み込み、最終層を置き換える
- 事前学習済みの重みを初期値として使い、小さい学習率で全体を再学習する
- 浅い層は学習率を小さく、深い層は学習率を大きくする(差分学習率)
段階的ファインチューニング
実践的には、以下の段階的なアプローチがよく使われます。
Phase 1: 最終層のみ学習(5〜10エポック) – 事前学習済みの重みを凍結 – 新しい最終層のみを学習 – 学習率: $10^{-3}$ 程度
Phase 2: 深い層を解凍(10〜20エポック) – 最後の数ブロックの重みを解凍 – 全体を小さい学習率で学習 – 学習率: $10^{-4}$ 程度
Phase 3: 全体を解凍(必要に応じて) – 全ての重みを解凍 – 非常に小さい学習率で学習 – 学習率: $10^{-5}$ 程度
差分学習率(Discriminative Learning Rates)
層ごとに異なる学習率を設定する手法です。浅い層の汎用的な特徴を壊さないように小さい学習率を、深い層のタスク固有の特徴には大きい学習率を使います。
$$ \eta_l = \eta_\text{base} \times \gamma^{L-l} $$
$l$ は層のインデックス、$L$ は総層数、$\gamma < 1$ は減衰率です。
ファインチューニングの注意点
学習率を小さくする: 事前学習済みの良い特徴を壊さないため。大きすぎる学習率は「壊滅的忘却」(catastrophic forgetting)を引き起こします。
データ拡張を活用する: 少量のデータでファインチューニングする場合、回転、反転、色変換などのデータ拡張が特に重要です。
早期停止を使う: 過学習を防ぐため、バリデーション損失を監視して適切なタイミングで学習を停止します。
特徴抽出器とファインチューニングの使い分けを理解したところで、ドメインの違いが転移可能性にどう影響するかを分析しましょう。
ドメインシフトと転移可能性
ドメインシフトとは
ソースドメインとターゲットドメインの分布が異なることをドメインシフト(domain shift)と呼びます。ドメインシフトが大きいほど、転移学習の効果は小さくなります。
ドメインシフトの例: – 共変量シフト: $P_S(\bm{X}) \neq P_T(\bm{X})$ だが $P_S(Y|\bm{X}) = P_T(Y|\bm{X})$(入力の分布が異なるが、ラベルの規則は同じ) – ラベルシフト: $P_S(Y) \neq P_T(Y)$ だが $P_S(\bm{X}|Y) = P_T(\bm{X}|Y)$(クラスの頻度が異なる) – 概念ドリフト: $P_S(Y|\bm{X}) \neq P_T(Y|\bm{X})$(同じ入力でもラベルの意味が異なる)
負の転移(Negative Transfer)
ソースタスクの知識がターゲットタスクの性能を悪化させる現象を負の転移と呼びます。これは、ソースとターゲットのドメインが大きく異なる場合に起こります。
たとえば、自然画像で学習したCNNを医療用X線画像に転移する場合、自然画像の色彩やテクスチャに最適化された特徴は、X線画像の微妙な濃淡の違いには適切でない可能性があります。
負の転移を避けるためには: – ソースとターゲットの類似度を事前に評価する – 転移する層の深さを調整する(浅い層のみ転移する) – ドメイン適応(Domain Adaptation)の手法を使う
データ量と戦略の対応関係
| ターゲットデータ量 | ドメインの類似度 | 推奨戦略 |
|---|---|---|
| 少量 | 高い | 特徴抽出器(最終層のみ学習) |
| 少量 | 低い | 特徴抽出器 + 浅い層も調整 |
| 中程度 | 高い | ファインチューニング(深い層から) |
| 中程度 | 低い | ファインチューニング(全体、小学習率) |
| 大量 | 任意 | ファインチューニング or スクラッチ学習 |
Pythonでの実装
転移学習のシミュレーション
事前学習の効果をシンプルなMLPでシミュレーションします。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_circles
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
class TransferMLP:
"""転移学習シミュレーション用MLP"""
def __init__(self, dims, lr=0.1):
self.Ws = []
self.bs = []
self.lr = lr
for i in range(len(dims) - 1):
self.Ws.append(np.random.randn(dims[i+1], dims[i])
* np.sqrt(2.0 / dims[i]))
self.bs.append(np.zeros((dims[i+1], 1)))
def copy_weights(self, other, n_layers):
"""他のモデルからn_layers層分の重みをコピー"""
for i in range(n_layers):
self.Ws[i] = other.Ws[i].copy()
self.bs[i] = other.bs[i].copy()
def forward(self, X, freeze_layers=0):
self.hs = [X]
self.zs = []
h = X
for i in range(len(self.Ws)):
z = self.Ws[i] @ h + self.bs[i]
self.zs.append(z)
if i < len(self.Ws) - 1:
h = np.maximum(0, z)
else:
h = sigmoid(z)
self.hs.append(h)
return h
def backward(self, t, freeze_layers=0):
m = t.shape[1]
delta = self.hs[-1] - t
for i in range(len(self.Ws) - 1, -1, -1):
dW = (1/m) * delta @ self.hs[i].T
db = (1/m) * np.sum(delta, axis=1, keepdims=True)
if i > 0:
delta = (self.Ws[i].T @ delta) * (self.zs[i-1] > 0).astype(float)
# 凍結層はスキップ
if i >= freeze_layers:
self.Ws[i] -= self.lr * dW
self.bs[i] -= self.lr * db
def compute_loss(self, y, t):
eps = 1e-8
return -np.mean(t * np.log(y + eps) + (1-t) * np.log(1-y + eps))
# --- ソースタスク: 多めのデータで学習 ---
X_source, y_source = make_moons(n_samples=2000, noise=0.15, random_state=42)
scaler_s = StandardScaler()
X_source = scaler_s.fit_transform(X_source)
X_s_tr, X_s_te, y_s_tr, y_s_te = train_test_split(
X_source, y_source, test_size=0.2, random_state=42)
dims = [2, 32, 32, 32, 1]
source_model = TransferMLP(dims, lr=0.5)
for epoch in range(2000):
y_pred = source_model.forward(X_s_tr.T)
source_model.backward(y_s_tr.reshape(1, -1))
# --- ターゲットタスク: 少量のデータ ---
X_target, y_target = make_circles(n_samples=200, noise=0.15,
factor=0.5, random_state=42)
scaler_t = StandardScaler()
X_target = scaler_t.fit_transform(X_target)
X_t_tr, X_t_te, y_t_tr, y_t_te = train_test_split(
X_target, y_target, test_size=0.3, random_state=42)
X_t_tr_T = X_t_tr.T
t_t_tr = y_t_tr.reshape(1, -1)
X_t_te_T = X_t_te.T
t_t_te = y_t_te.reshape(1, -1)
# --- 3つの戦略を比較 ---
strategies = {
"From Scratch": {"freeze": 0, "use_pretrained": False},
"Feature Extractor\n(freeze 2 layers)": {"freeze": 2, "use_pretrained": True},
"Fine-tuning\n(all layers, small lr)": {"freeze": 0, "use_pretrained": True},
}
epochs_target = 1000
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))
colors = ["tab:red", "tab:blue", "tab:green"]
ax_loss = axes[0]
results = {}
for (name, config), color in zip(strategies.items(), colors):
np.random.seed(42)
model = TransferMLP(dims, lr=0.3 if not config["use_pretrained"] else 0.1)
if config["use_pretrained"]:
model.copy_weights(source_model, len(dims) - 1)
losses = []
for epoch in range(epochs_target):
y_pred = model.forward(X_t_tr_T, freeze_layers=config["freeze"])
loss = model.compute_loss(y_pred, t_t_tr)
model.backward(t_t_tr, freeze_layers=config["freeze"])
losses.append(loss)
ax_loss.plot(losses, linewidth=2, color=color, label=name, alpha=0.8)
y_test_pred = (model.forward(X_t_te_T) > 0.5).astype(int)
acc = np.mean(y_test_pred == t_t_te)
results[name] = acc
ax_loss.set_xlabel("Epoch", fontsize=12)
ax_loss.set_ylabel("Loss", fontsize=12)
ax_loss.set_title("Training Loss on Target Task (200 samples)", fontsize=13)
ax_loss.set_yscale("log")
ax_loss.legend(fontsize=9)
ax_loss.grid(True, alpha=0.3)
# テスト精度
ax = axes[1]
bars = ax.bar(range(len(results)), results.values(), color=colors, alpha=0.8)
ax.set_xticks(range(len(results)))
ax.set_xticklabels(results.keys(), fontsize=9)
for bar, acc in zip(bars, results.values()):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f"{acc:.1%}", ha="center", fontsize=11, fontweight="bold")
ax.set_ylabel("Test Accuracy", fontsize=12)
ax.set_title("Test Accuracy on Target Task", fontsize=13)
ax.set_ylim(0, 1.1)
ax.grid(True, alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig("transfer_learning.png", dpi=150, bbox_inches="tight")
plt.show()
この実験結果から、転移学習の効果が明確に確認できます。
-
損失曲線(左図): ゼロからの学習(赤)は初期損失が高く、収束に時間がかかっています。事前学習済みモデルを使った特徴抽出器(青)とファインチューニング(緑)は、初期損失が低く、素早く収束しています。事前学習された重みが良い初期値として機能していることがわかります
-
テスト精度(右図): ターゲットデータが200枚と少ない状況では、転移学習を使った手法がゼロからの学習を上回っています。特にファインチューニング(全層を小さい学習率で調整)が最も高い精度を示す傾向にあります
NLPにおける転移学習
事前学習 + ファインチューニングのパラダイム
画像認識で確立された転移学習のパラダイムは、自然言語処理(NLP)でも大成功を収めています。
ELMo(2018): 双方向LSTMで文脈に依存した単語埋め込みを学習
BERT(2018): Transformerエンコーダをマスク言語モデルで事前学習し、各種タスクにファインチューニング
GPT(2018-): Transformerデコーダを次単語予測で事前学習。GPT-3以降はファインチューニング不要のin-context learningが可能
NLPでの転移学習の流れは以下の通りです。
- 事前学習(Pre-training): 大規模テキストコーパスで言語モデルを学習(自己教師あり学習)
- ファインチューニング(Fine-tuning): タスク固有のラベル付きデータで微調整
- (GPT-3以降)プロンプトエンジニアリング: モデルの重みを変更せず、入力プロンプトの工夫で下流タスクを解く
画像とNLPの転移学習の比較
| 画像(CNN) | NLP(Transformer) | |
|---|---|---|
| 事前学習データ | ImageNet(120万枚) | テキストコーパス(数十億〜数兆トークン) |
| 事前学習タスク | 画像分類(教師あり) | マスク言語モデル、次単語予測(自己教師あり) |
| 転移の仕組み | 低次 → 高次の視覚特徴の階層 | 文法 → 意味 → 推論の言語知識の階層 |
| ファインチューニング | 浅い層を凍結、深い層を調整 | 全体を小さい学習率で調整 |
まとめ
本記事では、転移学習の理論と実践的な活用戦略を解説しました。
- 転移学習はソースタスクで学習した知識をターゲットタスクに再利用する。CNNでは浅い層の汎用的特徴が転移の基盤となる
- 特徴抽出器戦略は事前学習済みモデルの重みを凍結し、最終層だけを学習する。データが少ないときに有効
- ファインチューニングは事前学習済みの重みを初期値として小さい学習率で再学習する。段階的解凍と差分学習率が実践的
- ドメインシフトが大きい場合は負の転移のリスクがある。浅い層のみの転移やドメイン適応を検討する
- NLPでも事前学習+ファインチューニングのパラダイムが確立し、BERTやGPTが代表的
次のステップとして、以下の記事も参考にしてください。
- 過学習の対策まとめ — 転移学習時の正則化
- BERTとGPTの違い — NLPでの転移学習アーキテクチャ
- PyTorch入門 — 事前学習モデルの活用方法