事前学習モデルのファインチューニング
田中VPoE:「BERT と GPT の概要を理解したところで、実際にファインチューニングする方法を学ぼう。NetShop のレビュー感情分析には日本語 BERT をファインチューニングするのが最も実践的だ。」
あなた:「Hugging Face の Transformers ライブラリを使えば、比較的簡単にできるんですよね。」
田中VPoE:「そうだ。ただし、トークナイザーの使い方やデータの前処理、学習率の設定など、押さえるべきポイントがある。一つずつ見ていこう。」
ファインチューニングの全体フロー
1. タスクの定義(感情分析:ポジティブ/ネガティブ)
2. データの準備(レビューテキスト + ラベル)
3. トークナイザーでテキストをトークン化
4. 事前学習済みモデルに分類ヘッドを追加
5. 学習ループの実装
6. 評価
データの前処理
トークナイズ
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('tohoku-nlp/bert-base-japanese-v3')
# 単一テキストのトークナイズ
text = "この商品はとても使いやすく、デザインも良いです"
encoded = tokenizer(
text,
max_length=128,
padding='max_length',
truncation=True,
return_tensors='pt'
)
print(f"input_ids: {encoded['input_ids'].shape}") # (1, 128)
print(f"attention_mask: {encoded['attention_mask'].shape}") # (1, 128)
print(f"トークン: {tokenizer.convert_ids_to_tokens(encoded['input_ids'][0][:10])}")
カスタム Dataset
import torch
from torch.utils.data import Dataset, DataLoader
class ReviewDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_length=128):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
encoded = self.tokenizer(
self.texts[idx],
max_length=self.max_length,
padding='max_length',
truncation=True,
return_tensors='pt'
)
return {
'input_ids': encoded['input_ids'].squeeze(0),
'attention_mask': encoded['attention_mask'].squeeze(0),
'label': torch.tensor(self.labels[idx], dtype=torch.long)
}
モデルの構築
分類ヘッド付きモデル
from transformers import AutoModelForSequenceClassification
# 感情分析用(2クラス分類)
model = AutoModelForSequenceClassification.from_pretrained(
'tohoku-nlp/bert-base-japanese-v3',
num_labels=2
)
# モデル構造の確認
print(model.classifier) # Linear(768, 2)
カスタム分類ヘッド
from transformers import AutoModel
import torch.nn as nn
class SentimentClassifier(nn.Module):
def __init__(self, model_name, num_classes=2, dropout=0.3):
super().__init__()
self.bert = AutoModel.from_pretrained(model_name)
self.dropout = nn.Dropout(dropout)
self.classifier = nn.Sequential(
nn.Linear(self.bert.config.hidden_size, 256),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(256, num_classes)
)
def forward(self, input_ids, attention_mask):
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
# [CLS] トークンの出力を使用
cls_output = outputs.last_hidden_state[:, 0, :]
cls_output = self.dropout(cls_output)
logits = self.classifier(cls_output)
return logits
model = SentimentClassifier('tohoku-nlp/bert-base-japanese-v3', num_classes=2)
学習の設定
学習率とスケジューラ
ファインチューニングでは、事前学習済みの知識を壊さないよう小さな学習率を使用します。
from torch.optim import AdamW
from transformers import get_linear_schedule_with_warmup
# AdamW optimizer(weight decay 付き Adam)
optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)
# 線形ウォームアップ付きスケジューラ
num_epochs = 3
num_training_steps = len(train_loader) * num_epochs
num_warmup_steps = int(0.1 * num_training_steps) # 最初の10%でウォームアップ
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=num_warmup_steps,
num_training_steps=num_training_steps
)
学習率のスケジュール:
0 ──── ウォームアップ ──── ピーク ──── 線形減衰 ──── 0
| 10% | 90% |
層ごとの学習率
# BERT の層ごとに学習率を変える(Discriminative Fine-tuning)
param_groups = [
{'params': model.bert.embeddings.parameters(), 'lr': 1e-6},
{'params': model.bert.encoder.layer[:6].parameters(), 'lr': 5e-6},
{'params': model.bert.encoder.layer[6:].parameters(), 'lr': 1e-5},
{'params': model.classifier.parameters(), 'lr': 2e-5},
]
optimizer = AdamW(param_groups, weight_decay=0.01)
学習ループ
import torch.nn.functional as F
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
# === 学習 ===
model.train()
total_loss, correct, total = 0, 0, 0
for batch in train_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['label'].to(device)
optimizer.zero_grad()
logits = model(input_ids, attention_mask)
loss = criterion(logits, labels)
loss.backward()
# 勾配クリッピング(勾配爆発の防止)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
total_loss += loss.item()
_, predicted = logits.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
train_acc = correct / total
avg_loss = total_loss / len(train_loader)
# === 検証 ===
model.eval()
val_correct, val_total = 0, 0
with torch.no_grad():
for batch in val_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['label'].to(device)
logits = model(input_ids, attention_mask)
_, predicted = logits.max(1)
val_correct += predicted.eq(labels).sum().item()
val_total += labels.size(0)
val_acc = val_correct / val_total
print(f"Epoch {epoch+1}/{num_epochs} - "
f"Loss: {avg_loss:.4f}, Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}")
ファインチューニングのベストプラクティス
| 項目 | 推奨値 | 理由 |
|---|---|---|
| 学習率 | 2e-5 〜 5e-5 | 大きすぎると事前学習の知識が壊れる |
| バッチサイズ | 16 〜 32 | GPU メモリと相談 |
| エポック数 | 2 〜 4 | 過学習しやすいので少なめ |
| ウォームアップ | 全体の10% | 学習初期の不安定さを軽減 |
| 勾配クリッピング | max_norm=1.0 | 勾配爆発を防止 |
| Weight Decay | 0.01 | 過学習の抑制 |
まとめ
- ファインチューニングは事前学習済みモデルを特定タスクに適応させる手法
- トークナイザーでテキストを数値に変換し、適切な前処理を行う
- 小さな学習率(2e-5程度)とウォームアップスケジューラが重要
- 勾配クリッピングで学習の安定化を図る
- エポック数は少なめ(2〜4)にして過学習を防ぐ
チェックリスト
- トークナイザーの使い方(max_length、padding、truncation)を理解した
- 分類ヘッド付きモデルの構築方法を理解した
- AdamW + ウォームアップスケジューラの設定ができる
- ファインチューニングの推奨ハイパーパラメータを把握した
推定読了時間: 30分