LESSON

事前学習モデルのファインチューニング

田中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 〜 32GPU メモリと相談
エポック数2 〜 4過学習しやすいので少なめ
ウォームアップ全体の10%学習初期の不安定さを軽減
勾配クリッピングmax_norm=1.0勾配爆発を防止
Weight Decay0.01過学習の抑制

まとめ

  • ファインチューニングは事前学習済みモデルを特定タスクに適応させる手法
  • トークナイザーでテキストを数値に変換し、適切な前処理を行う
  • 小さな学習率(2e-5程度)とウォームアップスケジューラが重要
  • 勾配クリッピングで学習の安定化を図る
  • エポック数は少なめ(2〜4)にして過学習を防ぐ

チェックリスト

  • トークナイザーの使い方(max_length、padding、truncation)を理解した
  • 分類ヘッド付きモデルの構築方法を理解した
  • AdamW + ウォームアップスケジューラの設定ができる
  • ファインチューニングの推奨ハイパーパラメータを把握した

推定読了時間: 30分