【No.L006】日本語ニュース分類モデルの構築 – Livedoor ニュースコーパスを用いたBERTファインチューニング

はじめに

本記事では、日本語ニュース分類モデルの構築方法について解説します。具体的には、Livedoor ニュースコーパスを用いて、事前学習済みBERTモデルをファインチューニングし、ニュース記事のカテゴリを分類するモデルを構築します。

本記事は、自然言語処理の基礎知識とPythonプログラミングの経験がある方を対象としています。特に、BERTを用いた自然言語処理タスクの経験がある方は、本記事の内容をより理解しやすいかと思います。

本記事では、以下の手順でモデルを構築していきます。(Google Colabをベースに解説。2024.6.26動作確認済)

  1. ニュースコーパスを展開: 学習用のデータとして、Livedoor ニュースコーパスをダウンロード
  2. サブディレクトリのリストを取得: カテゴリ別に分類されたサブディレクトリをリスト化
  3. データセットの作成: ニュース記事を、BERTの入力形式に合わせたデータセットに変換
  4. データローダの作成: 訓練、検証、テスト用にデータセットを分割しデータローダーを作成
  5. 分類タスク用のモデル作成: 事前学習済みBERTモデルに分類用の層を追加
  6. ファインチューニング: 作成したモデルを、訓練データを用いて分類性能を向上させる

本記事を通して、日本語ニュース分類モデルの構築に必要な手順を理解し、実際にコードを作成することができるようになります。また、BERTを用いたファインチューニングの基礎知識を習得し、他の自然言語処理タスクへの応用も可能になります。

★関連リンク
業務で活用できるAI技集のまとめはこちら

解説

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

まずは必要なライブラリをpipコマンドでインストールします。Google Colabには予め多数のライブラリが準備されていますが、日本語をトークンに変えるための形態素分析の工程で必要な以下2つを入れる必要があります。

  • fugashi: 形態素解析を行うライブラリです。日本語の文を単語に分割し、各単語の品詞や活用形などの情報を取得することができます。
  • ipadic: 形態素解析に使用する辞書データです。 fugashi は ipadic を用いて形態素解析を実行します。
!pip install fugashi ipadic

①ニュースコーパスを展開

このコードは、ldcc-20140209.tar.gz という圧縮ファイルからデータを解凍する処理を行います。ニュースコーパスのサイトからダウンロードし、コーディングしているファイル(.ipynb)と同じ階層に格納して実行して下さい。

import tarfile

tar = tarfile.open("./ldcc-20140209.tar.gz")
tar.extractall("./data/livedoor/")
tar.close()

②サブディレクトリのリストを取得

このコードは、./data/livedoor/text/ ディレクトリ内のサブディレクトリ名を取得して、cate_list に格納します。うまくいくとcate_listには、以下の9つの文字列が格納されます。

import os

cate_list = [name for name in os.listdir("./data/livedoor/text/") if os.path.isdir(os.path.join("./data/livedoor/text/", name))]
['dokujo-tsushin',
'it-life-hack',
'kaden-channel',
'livedoor-homme',
'movie-enter',
'peachy',
'smax',
'sports-watch',
'topic-news']

③データセットの作成

このコードは、日本語ニュース記事をBERTの入力形式に合わせたデータセットに変換します。まず、必要なライブラリをインポートし、n_label と max_length を定義します。その後、BertJapaneseTokenizer を初期化し、cate_list から2つのカテゴリを選択します。次に、ループ処理で各カテゴリのファイルを読み込み、テキストを抽出します。tokenizer を使用してテキストをトークン化し、max_length に合わせてパディングと切り捨てを行います。最後に、ラベルとトークン化されたデータを含む辞書をdataset に追加します。

from transformers import BertJapaneseTokenizer, BertModel
import fugashi
import ipadic
import glob
import torch

n_label = 2
max_length = 10
cate_list = cate_list[:n_label]
tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')

dataset = []
for label, cate in enumerate(cate_list):
    for f in glob.glob(f'./data/livedoor/text/{cate}/{cate}*'):
        lines = open(f, encoding="utf-8_sig").read().splitlines()
        text = '\n'.join(lines[2:])
        encoding = tokenizer(text, max_length=max_length, padding='max_length', truncation=True)
        encoding['label'] = label
        encoding = {k: torch.tensor(v) for k, v in encoding.items()}
        dataset.append(encoding)
[参考] コード解説
10行目: 事前学習済みのBertJapaneseTokenizerを初期化する。
13行目: カテゴリリストをループ処理し、ラベルとカテゴリ名のペアを生成する。
14行目: カテゴリ内のファイルをループ処理し、指定されたパターンに一致するファイル名のリストを取得する。
15行目: ファイルをUTF-8でエンコードして開き、行ごとに読み込む。
16行目: ファイルから本文部分を取得する。
17行目: テキストをトークン化し、指定された形式にエンコードする。
18行目: エンコーディングにラベルを追加する。
19行目: エンコーディング内の値をPyTorchテンソルに変換する。

④データローダの作成

このコードは、作成したデータセットを訓練、検証、テスト用に分割し、バッチ処理を行うデータローダーを作成します。まず、データセットをコピーして dataset_for_loader に格納し、乱数のシードを設定してシャッフルします。その後、データセットを6:2:2の割合で訓練、検証、テスト用に分割します。最後に、DataLoader を使用して、各データセットに対してバッチサイズ16でデータローダーを作成します。訓練用データローダーはシャッフルを有効化し、検証用とテスト用データローダーはシャッフルを無効化します。

import random
from torch.utils.data import DataLoader

dataset_for_loader = dataset.copy()
random.seed(0)
random.shuffle(dataset_for_loader)
n = len(dataset_for_loader)
n_train, n_valid = int(n*0.6), int(n*0.2)
dataset_train = dataset_for_loader[:n_train]
dataset_valid = dataset_for_loader[n_train:n_train+n_valid]
dataset_test = dataset_for_loader[n_train+n_valid:]

batch_size = 16
dataloader_train = DataLoader(dataset_train, batch_size = batch_size, shuffle = True)
dataloader_valid = DataLoader(dataset_valid, batch_size = batch_size, shuffle = False)
dataloader_test = DataLoader(dataset_test, batch_size = batch_size, shuffle = False)
[参考] コード解説
4行目: データセットをコピーして、ローダー用のデータセットを作成する。
8行目: 訓練用(60%)、検証用(20%)、テスト用(20%)のデータ数を定義する。
9行目: 訓練用のデータセットを作成する。
10行目: 検証用のデータセットを作成する。
11行目: テスト用のデータセットを作成する。

⑤分類タスク用のモデル作成

このコードは、事前学習済みBERTモデルに分類用の層を追加した、ニュース分類モデルBertClassifier を定義しています。__init__ メソッドでは、事前学習済みBERTモデル (model_bert) と分類層 (self.linear) を初期化します。分類層は、BERTモデルの出力サイズ (768) を入力とし、カテゴリ数 (n_label) を出力とする全結合層です。forward メソッドでは、入力テキスト (input_ids) をBERTモデルに入力し、その出力から分類層への入力となる特徴量を抽出します。その後、分類層で特徴量を処理し、各カテゴリの確率を出力します。

import torch.nn as nn

class BertClassifier(nn.Module):
    def __init__(self):
        super(BertClassifier, self).__init__()
        self.model_bert = model_bert
        self.linear = nn.Linear(in_features=768, out_features=n_label)
        nn.init.normal_(self.linear.weight, std=0.02)
        nn.init.normal_(self.linear.bias, 0)
        
    def forward(self, input_ids):
        output_bert = self.model_bert(input_ids)
        output_bert = output_bert[0]
        output_bert = output_bert[:, 0, :].view(-1, 768) # [batch_size, hidden_size]に変換
        output = self.linear(output_bert)
        return output
[参考] コード解説
7~9行目: 全結合層を定義。入力はCLSトークン[batch size, 768]、出力は[batch size, label数]。
13行目: モデル出力の最初の要素を抽出。output_bert[0]は最終層の出力[batch size, seq長, 次元数(768)]。output_bert[1]はAttentionの重み[batch size, head数, seq長, seq長]。
14行目: [:, 0, :]で最初のトークン([CLS]トークン)の特徴量を抽出。全結合層の入力用にサイズを調整。

⑥ファインチューニング

このコードは、モデルのファインチューニングのための準備を行います。まず、GPUが利用可能であればGPUを使用するようにデバイスを設定し、事前学習済みBERTモデルとBertClassifier を初期化してGPUに対応させます。次に、BERTモデルのパラメータのうち、最終エンコーダー層と分類層のパラメータのみを学習対象とするよう設定します。その他のBERTモデルのパラメータは凍結されます。最後に、最適化アルゴリズムとしてAdamを使用し、学習率をそれぞれ 5e-5 と 1e-4 に設定します。さらに、損失関数は交差エントロピー誤差 (nn.CrossEntropyLoss()) を使用します。

import torch.optim as optim

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model_bert = BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
model = BertClassifier()
model.to(device)

for param in model.parameters():
    param.requires_grad = False
for param in model.model_bert.encoder.layer[-1].parameters():
    param.requires_grad = True
for param in model.linear.parameters():
    param.requires_grad = True   


optimizer = optim.Adam([
    {'params': model.model_bert.encoder.layer[-1].parameters(), 'lr': 5e-5},
    {'params': model.linear.parameters(), 'lr': 1e-4}
])
criterion = nn.CrossEntropyLoss()
[参考] コード解説
3行目: GPUが使える環境の場合、deviceに'cuda'が格納。
5~7行目: modelの定義。model_bertに学習済みモデル(5行目)、modelに学習済みモデル+分類用レイヤを格納(6行目)して、GPU対応化(7行目)。(GPU対応化はmodelと入力Tensorにも必要なので後述で実施)
9~14行目: パラメータの設定。Fine TuningなのでBERTモデルの最終エンコーダーレイヤーと分類層のパラメータのみを学習対象としている。
17~20行目: パラメータの最適化アルゴリズムにAdamを採用。9~14行目に合わせたレイヤで設定。lr(学習率)はどれくらいパラメータを変動させるかを決める値。
21行目: 分類タスクなので、損失関数はクロスエントロピー損失を使用。

このコードは、モデルを5エポック(5回)訓練し、各エポックでの訓練データと検証データに対する損失と精度の結果を表示します。まず、各エポックの損失と精度を格納する変数を初期化します。その後、訓練モードと検証モードを切り替えながら、バッチ単位で処理を行い、損失を計算し、勾配を更新します。さらに、予測結果に基づいて精度を計算します。各エポックの最後に、訓練データと検証データに対する平均損失と精度を計算して出力します。このコードは、モデルの学習状況を監視し、過学習を防ぐために役立ちます。

n_epochs = 5
for epoch in range(n_epochs):
    loss_train = 0
    loss_valid = 0
    accuracy_train = 0
    accuracy_valid = 0
    
    model.train()
    for batch in dataloader_train:
        x = batch['input_ids'].to(device)
        t = batch['label'].to(device)
        batch_size_iter = len(x)
        model.zero_grad()
        optimizer.zero_grad()
        
        output = model(x)
        loss = criterion(output, t)
        loss.backward()
        optimizer.step()
        pred = output.argmax(dim=1)
        
        loss_train += loss.item() * batch_size_iter
        accuracy_train += torch.sum(pred==t.data)
    
    model.eval()
    for batch in dataloader_valid:
        x = batch['input_ids'].to(device)
        t = batch['label'].to(device)
        batch_size_iter = len(x)
        
        output = model(x)
        loss = criterion(output, t)
        pred = output.argmax(dim=1)
        
        loss_valid += loss.item() * batch_size_iter
        accuracy_valid += torch.sum(pred == t.data)
    
    average_loss_train = loss_train / len(dataset_train)
    average_loss_valid = loss_valid / len(dataset_valid)
    total_accuracy_train = accuracy_train / len(dataset_train)
    total_accuracy_valid = accuracy_valid / len(dataset_valid)
    print('| epoch %d | train loss %.2f, train accuracy %.2f | valid loss %.2f, valid accuracy %.2f'
          % (epoch+1, average_loss_train, total_accuracy_train, average_loss_valid, total_accuracy_valid))
[参考] コード解説
1行目: epoch数。何回訓練を繰り返すかを提示。
9~11行目: for文で訓練用のデータを繰り返し処理。ちなみにbatch['input_ids']は[batch size, max_length]、batch['label']は[batch size]となっている。to(device)でGPU対応。
12行目: batchのサイズを格納。後で出てくるロス計算の重みづけに使用。
13~14行目: パラメータの勾配を初期化(パラメータ自体の初期化ではない点に注意)。Pytorchは誤差逆伝播計算時に勾配を累積する仕様になっているため、明示的にzero_grad()を記載しないと正しい勾配の値を把握できない。
22行目: loss.item()には交差エントロピー誤差の値(スカラー)が格納されており、batchサイズを掛け算して重みづけ。
23行目: 予測値predと正解値t.dataが合致していれば+1される。predもt.dataも大きさはbatchサイズ。
25~36行目: 推論モード。基本的に訓練モードと同じ内容なので説明は割愛。推論モードはパラメータを変化させてはいけないため、loss.backward()は不要。

コメント