はじめに
本記事では、日本語ニュース分類モデルの構築方法について解説します。具体的には、Livedoor ニュースコーパスを用いて、事前学習済みBERTモデルをファインチューニングし、ニュース記事のカテゴリを分類するモデルを構築します。
本記事は、自然言語処理の基礎知識とPythonプログラミングの経験がある方を対象としています。特に、BERTを用いた自然言語処理タスクの経験がある方は、本記事の内容をより理解しやすいかと思います。
本記事では、以下の手順でモデルを構築していきます。(Google Colabをベースに解説。2024.6.26動作確認済)
- ニュースコーパスを展開: 学習用のデータとして、Livedoor ニュースコーパスをダウンロード
- サブディレクトリのリストを取得: カテゴリ別に分類されたサブディレクトリをリスト化
- データセットの作成: ニュース記事を、BERTの入力形式に合わせたデータセットに変換
- データローダの作成: 訓練、検証、テスト用にデータセットを分割しデータローダーを作成
- 分類タスク用のモデル作成: 事前学習済みBERTモデルに分類用の層を追加
- ファインチューニング: 作成したモデルを、訓練データを用いて分類性能を向上させる
本記事を通して、日本語ニュース分類モデルの構築に必要な手順を理解し、実際にコードを作成することができるようになります。また、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()は不要。
コメント