BERTのファインチューニング入門 – 感情分析モデルを30分で作る

Generative AI
この記事は約33分で読めます。

こんにちは、JS2IIUです。
自然言語処理の分野において、BERTは革命的な存在として知られています。しかし、「BERTは難しそう」「膨大な計算資源が必要なのでは?」と感じている方も多いのではないでしょうか。

実は、Hugging Face Transformersライブラリを使えば、事前学習済みのBERTモデルを自分のタスクに合わせてファインチューニングすることが、驚くほど簡単にできます。本記事では、映画レビューの感情分析を例に、BERTのファインチューニングから推論パイプラインの構築までを、約30分で実装できる手順を詳しく解説します。

Pythonの基本的な知識と、PyTorchやTransformersライブラリの基礎があれば、誰でも実践できる内容になっています。それでは、一緒にBERTの世界に飛び込んでいきましょう。今回もよろしくお願いします。

BERTとファインチューニングの基礎知識

BERTとは何か

BERT(Bidirectional Encoder Representations from Transformers)は、Googleが開発した自然言語処理モデルです。従来のモデルと大きく異なる点は、文脈を「双方向」から理解できることです。

例えば、「かける」という単語を考えてみましょう。「電話をかける」と「橋をかける」では、同じ「かける」でも意味が全く異なります。BERTは前後の文脈を同時に見ることで、このような文脈依存の意味を正確に捉えられるのです。

ファインチューニングのメリット

BERTは膨大なテキストデータで事前学習されていますが、そのままでは特定のタスクには使えません。そこで「ファインチューニング」という手法を使います。

これは、料理に例えるとわかりやすいでしょう。BERTの事前学習は「基本的な調理技術を学ぶこと」、ファインチューニングは「その技術を使って特定の料理を作ること」に相当します。ゼロから料理技術を学ぶよりも、基礎ができている状態から特定の料理を学ぶ方が、はるかに効率的です。

ファインチューニングの主なメリットは以下の3点です。

  • 少量のデータで高精度なモデルが作れる
  • 学習時間が大幅に短縮できる
  • 計算資源が限られていても実装可能

環境構築とデータの準備

以下のサンプルはGoogle Colabで試すことをお勧めします。

必要なライブラリのインストール

まず、必要なライブラリをインストールしましょう。Hugging Face Transformersと、その依存ライブラリをインストールします。

Python
# 必要なライブラリのインストール
!pip install transformers datasets torch scikit-learn

次に、必要なモジュールをインポートします。

Python
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import (
    BertTokenizer,
    BertForSequenceClassification,
    get_linear_schedule_with_warmup # <- このスケジューラは通常残します
)
# from transformers.optimization import AdamW # <- この行は削除/コメントアウト
from torch.optim import AdamW # <- PyTorchの標準AdamWをインポート
     
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import numpy as np
from tqdm import tqdm

サンプルデータセットの作成

実際のプロジェクトでは独自のデータを使用しますが、ここでは学習用に簡単な映画レビューのサンプルデータを作成します。

Python
# サンプルデータの作成(実際のプロジェクトでは外部データを使用)
sample_data = [
    ("この映画は素晴らしい傑作だった。感動して涙が止まらなかった。", 1),
    ("つまらない映画だった。時間の無駄だと感じた。", 0),
    ("期待を上回る出来栄えで、最後まで楽しめた。", 1),
    ("退屈で眠くなってしまった。ストーリーに魅力がない。", 0),
    ("俳優の演技が素晴らしく、映像も美しかった。", 1),
    ("脚本が弱く、展開が予測できてしまった。", 0),
    ("心に残る名作。何度でも見たくなる作品だ。", 1),
    ("期待外れで残念な内容だった。", 0),
]

# テキストとラベルに分離
texts = [item[0] for item in sample_data]
labels = [item[1] for item in sample_data]

# 訓練データとテストデータに分割(実際はもっと大きなデータセットを使用)
train_texts, test_texts, train_labels, test_labels = train_test_split(
    texts, labels, test_size=0.25, random_state=42
)

print(f"訓練データ数: {len(train_texts)}")
print(f"テストデータ数: {len(test_texts)}")
Plaintext
訓練データ数: 6
テストデータ数: 2

このコードでは、ポジティブ(ラベル1)とネガティブ(ラベル0)の映画レビューを用意し、訓練用とテスト用に分割しています。実際のプロジェクトでは、数千から数万のデータを使用することが一般的です。

カスタムデータセットクラスの実装

PyTorchでBERTを使うには、データを適切な形式に変換する必要があります。そのために、カスタムDatasetクラスを作成します。

Python
class SentimentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_length=128):
        """
        Args:
            texts: レビューテキストのリスト
            labels: 感情ラベル(0: ネガティブ, 1: ポジティブ)のリスト
            tokenizer: BERTトークナイザー
            max_length: テキストの最大長(トークン数)
        """
        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):
        text = self.texts[idx]
        label = self.labels[idx]

        # テキストをトークン化し、BERTの入力形式に変換
        encoding = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,  # [CLS]と[SEP]トークンを追加
            max_length=self.max_length,
            padding='max_length',      # 最大長まで0でパディング
            truncation=True,           # 最大長を超える場合は切り詰め
            return_attention_mask=True, # アテンションマスクを返す
            return_tensors='pt'        # PyTorchテンソルとして返す
        )

        return {
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

このDatasetクラスは、テキストデータをBERTが理解できる形式に変換します。encode_plusメソッドは、以下の処理を自動的に行います。

  • テキストをトークン(単語の断片)に分割
  • 特殊トークン([CLS]と[SEP])を追加
  • 数値IDに変換
  • 長さを統一するためのパディング
  • どの部分が実際のテキストかを示すアテンションマスクの生成

BERTモデルの読み込みとファインチューニング

モデルとトークナイザーの準備

事前学習済みのBERTモデルをHugging Faceから読み込みます。日本語のテキストを扱う場合は、日本語BERTモデルを使用することが推奨されます。Hugging Faceのトークンを事前に取得してGoogle Colabに設定しておくと良いでしょう。

Python
# 日本語BERTモデルとトークナイザーの読み込み
model_name = 'cl-tohoku/bert-base-japanese-whole-word-masking'
tokenizer = BertTokenizer.from_pretrained(model_name)

# 2クラス分類用のBERTモデルを読み込み
model = BertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2  # ポジティブとネガティブの2クラス
)

# GPUが利用可能な場合はGPUを使用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
print(f"使用デバイス: {device}")

データローダーの作成

作成したDatasetクラスを使って、DataLoaderを構築します。

Python
# データセットの作成
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer)
test_dataset = SentimentDataset(test_texts, test_labels, tokenizer)

# DataLoaderの作成(バッチ処理のため)
train_loader = DataLoader(
    train_dataset,
    batch_size=8,      # 一度に処理するデータ数
    shuffle=True       # エポックごとにデータをシャッフル
)

test_loader = DataLoader(
    test_dataset,
    batch_size=8,
    shuffle=False
)

バッチサイズは、メモリの制約に応じて調整してください。GPUメモリが少ない場合は、4や2に減らすことも可能です。

オプティマイザーとスケジューラーの設定

BERTのファインチューニングには、AdamWオプティマイザーと学習率スケジューラーを組み合わせるのが一般的です。

Python
# オプティマイザーの設定
optimizer = AdamW(
    model.parameters(),
    lr=2e-5,          # BERTには小さな学習率が推奨される
    eps=1e-8          # 数値安定性のための小さな値
)

# 学習率スケジューラーの設定(徐々に学習率を下げる)
epochs = 3
total_steps = len(train_loader) * epochs

scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=0,           # ウォームアップステップ数
    num_training_steps=total_steps
)

学習率2e-5(0.00002)は、BERTのファインチューニングで広く使われている値です。これは、事前学習で獲得した知識を壊さないよう、慎重に調整するためです。

訓練ループの実装

いよいよモデルを訓練します。以下のコードは、標準的なPyTorchの訓練ループです。

Python
def train_epoch(model, data_loader, optimizer, scheduler, device):
    """1エポック分の訓練を実行"""
    model.train()  # 訓練モードに設定
    total_loss = 0

    for batch in tqdm(data_loader, desc="Training"):
        # バッチデータをデバイスに移動
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)

        # 勾配をゼロにリセット
        optimizer.zero_grad()

        # モデルの順伝播
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=labels
        )

        loss = outputs.loss
        total_loss += loss.item()

        # 逆伝播と勾配更新
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()

    return total_loss / len(data_loader)

# 訓練の実行
print("訓練を開始します...")
for epoch in range(epochs):
    print(f"\nエポック {epoch + 1}/{epochs}")
    avg_loss = train_epoch(model, train_loader, optimizer, scheduler, device)
    print(f"平均損失: {avg_loss:.4f}")
Plaintext
訓練を開始します...

エポック 1/3
Training: 100%|██████████| 1/1 [00:15<00:00, 15.25s/it]
平均損失: 0.7330

エポック 2/3
Training: 100%|██████████| 1/1 [00:09<00:00,  9.28s/it]
平均損失: 0.5888

エポック 3/3
Training: 100%|██████████| 1/1 [00:09<00:00,  9.24s/it]平均損失: 0.4805

このコードでは、各エポックでデータセット全体を学習し、損失(モデルの予測誤差)を最小化していきます。clip_grad_norm_は勾配爆発を防ぐための処理で、BERTのような大規模モデルでは重要です。

モデルの評価と推論パイプライン

テストデータでの評価

訓練が完了したら、テストデータでモデルの性能を評価します。

Python
def evaluate_model(model, data_loader, device):
    """モデルの評価を実行"""
    model.eval()  # 評価モードに設定
    predictions = []
    actual_labels = []

    with torch.no_grad():  # 勾配計算を無効化
        for batch in tqdm(data_loader, desc="Evaluating"):
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)

            # 予測を実行
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask
            )

            # 最も確率の高いクラスを選択
            _, preds = torch.max(outputs.logits, dim=1)

            predictions.extend(preds.cpu().tolist())
            actual_labels.extend(labels.cpu().tolist())

    # 精度とレポートを計算
    accuracy = accuracy_score(actual_labels, predictions)
    report = classification_report(
        actual_labels, 
        predictions,
        target_names=['ネガティブ', 'ポジティブ']
    )

    return accuracy, report

# 評価の実行
print("\nモデルの評価を実行します...")
accuracy, report = evaluate_model(model, test_loader, device)
print(f"\n精度: {accuracy:.4f}")
print("\n詳細レポート:")
print(report)
Plaintext
モデルの評価を実行します...
Evaluating: 100%|██████████| 1/1 [00:05<00:00,  5.12s/it]
精度: 0.0000

詳細レポート:
              precision    recall  f1-score   support

       ネガティブ       0.00      0.00      0.00       2.0
       ポジティブ       0.00      0.00      0.00       0.0

    accuracy                           0.00       2.0
   macro avg       0.00      0.00      0.00       2.0
weighted avg       0.00      0.00      0.00       2.0

この評価では、精度だけでなく、適合率、再現率、F1スコアなどの詳細な指標も確認できます。

推論パイプラインの構築

訓練済みモデルを使って、新しいテキストの感情を予測する関数を作成しましょう。

Python
def predict_sentiment(text, model, tokenizer, device, max_length=128):
    """
    テキストの感情を予測する関数

    Args:
        text: 予測したいテキスト
        model: 訓練済みモデル
        tokenizer: トークナイザー
        device: 使用デバイス
        max_length: 最大トークン長

    Returns:
        予測ラベル(0: ネガティブ, 1: ポジティブ)と確信度
    """
    model.eval()

    # テキストをトークン化
    encoding = tokenizer.encode_plus(
        text,
        add_special_tokens=True,
        max_length=max_length,
        padding='max_length',
        truncation=True,
        return_attention_mask=True,
        return_tensors='pt'
    )

    input_ids = encoding['input_ids'].to(device)
    attention_mask = encoding['attention_mask'].to(device)

    # 予測を実行
    with torch.no_grad():
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )

    # ソフトマックスで確率に変換
    probs = torch.nn.functional.softmax(outputs.logits, dim=1)
    confidence, predicted = torch.max(probs, dim=1)

    return predicted.item(), confidence.item()

# 推論のテスト
test_reviews = [
    "この映画は本当に最高だった。また見たい。",
    "退屈で二度と見たくない作品だった。",
]

print("\n新しいレビューの感情予測:")
for review in test_reviews:
    label, confidence = predict_sentiment(review, model, tokenizer, device)
    sentiment = "ポジティブ" if label == 1 else "ネガティブ"
    print(f"\nレビュー: {review}")
    print(f"予測: {sentiment} (確信度: {confidence:.4f})")
Plaintext
新しいレビューの感情予測:

レビュー: この映画は本当に最高だった。また見たい。
予測: ポジティブ (確信度: 0.5431)

レビュー: 退屈で二度と見たくない作品だった。
予測: ポジティブ (確信度: 0.5418)

この推論関数は、任意のテキストに対して感情を予測し、その確信度も返します。確信度が低い場合は、モデルが判断に迷っていることを示します。さすがにうまく予測できていないですね。

モデルの保存と読み込み

訓練したモデルは、後で再利用できるように保存しておきましょう。

Python
# モデルとトークナイザーの保存
output_dir = './sentiment_model'
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"モデルを {output_dir} に保存しました。")

# 保存したモデルの読み込み(別のスクリプトで使用する場合)
loaded_model = BertForSequenceClassification.from_pretrained(output_dir)
loaded_tokenizer = BertTokenizer.from_pretrained(output_dir)
loaded_model.to(device)
print("保存したモデルを読み込みました。")

実践的なヒントと応用例

ハイパーパラメータの調整

モデルの性能を向上させるには、以下のハイパーパラメータを調整することが有効です。

  • 学習率: 1e-5から5e-5の範囲で試す
  • バッチサイズ: メモリが許す限り大きくする(8, 16, 32など)
  • エポック数: 過学習に注意しながら3-5エポック程度
  • 最大トークン長: データの特性に応じて64-512の範囲で調整

データ拡張の活用

少量のデータしかない場合、以下のようなデータ拡張手法が有効です。

  • 同義語置換: 単語を類義語に置き換える
  • バックトランスレーション: 他言語に翻訳して再度日本語に戻す
  • ノイズ追加: ランダムに単語を削除・挿入する

他のタスクへの応用

今回の感情分析の手法は、以下のようなタスクにも応用できます。

  • テキスト分類: ニュース記事のカテゴリ分類
  • 意図理解: チャットボットのユーザー意図認識
  • スパム検出: メールやコメントのスパム判定
  • トピック抽出: 文書の主題分類

まとめ

本記事では、Hugging Face TransformersとPyTorchを使って、BERTモデルのファインチューニングから推論パイプラインの構築までを実装しました。

重要なポイントをおさらいしましょう。

  • BERTは事前学習済みモデルなので、少量のデータで高精度な分類器が作れる
  • Hugging Face Transformersを使えば、複雑な実装を意識せずにBERTを活用できる
  • カスタムDatasetクラスでデータを適切に前処理することが重要
  • 小さな学習率と適切なスケジューラーで、安定した訓練が可能
  • 訓練したモデルは簡単に保存・再利用できる

BERTのファインチューニングは、一見複雑に思えますが、Pythonの基礎知識があれば、今回紹介したコードをベースに様々なタスクに応用できます。まずは小さなデータセットで試してみて、徐々に規模を拡大していくことをお勧めします。

今回のコードは、実際のプロジェクトの出発点として活用できます。データの品質向上、ハイパーパラメータの最適化、アーキテクチャの改良など、さらなる改善の余地は多くあります。ぜひ、自分のデータで試してみてください。

最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました