Hugging FaceのBERTモデルでクラス分類(classification)を実践する
この記事では、HuggingFace TransformerのBERTモデルを使用して、日本語テキストのクラス分類を実践する方法を解説します。BERTは高精度なテキスト分類を実現する自然言語処理モデルであり、Hugging Faceのライブラリを使うことで、BERTを簡単に導入できます。具体的なデータセットを用いた手順やコード例を交えながら、BERTを用いたクラス分類のやり方を解説します。
コードは、githubに置きましたのでそちらも参考にしてください。
github: Colabで動くコード
BERTを使って回帰問題を解く場合は以下の記事を参考にしてください。
Hugging Faceについて
Hugging Faceは、transformersをベースにした機械学習モデルを公開するプラットフォームとして有名です。Hugging Faceを利用することで、自然言語処理に興味がある技術者が簡単に自然言語処理を試すことができるようになりました。
ということで、日本語のテキストを分類する問題を実際にHugging Faceを使ってやってみたいと思います。
BERTを利用する場合の流れについて
BERTを利用する場合は、再学習ではなくてファインチューニングがメインとなります。というのも、BERTをゼロから学習させるのは大変で、それなりのGPUなどのリソースが必要となるため、個人レベルでやる場合はどうしてもファインチューニングレベルまでとなってしまします。
ファインチューニングを行う場合、処理の流れは以下になります。
- データセットを準備
- モデルを選択(学習済みモデルを利用する)
- モデルに合わせたトークナイザーで、テキストをトークン化
- トークン化したデータセットで学習
- ファインチューニングしたモデルの保存、推論テストなど
画像のクラス分類などと大きく異なるのは、テキストをトークン化する部分でしょうか。自然言語処理をする場合は、ここが少し特殊になります。
クラス分類を実践(日本語テキスト)
今回も、Google Colabを利用して実装していますのでColabを利用して試すことができます(GPUを利用するようにしてください。利用しないと処理時間がとんでもなくかかります)
transformerのインストールなど
必要となるパッケージをインストールします。まずはHugging Face関連です。
!pip install transformers
!pip install datasets
!pip install evaluate
!pip install git+https://github.com/huggingface/accelerate
今回は、evaluateは使っていませんのでインストールしなくてもOKです。
注意点は、accelerateです。これを
!pip install accelerate
としてインストールするとColabではうまく動きません(ランタイムの再起動が必要になります)。普通に、ローカルの環境で動かす場合は、pip install accelerate
でOKです。
次に、日本語関連のパッケージをインストールします。日本語のBERTを利用する場合には、日本語の形態素解析などのために、以下のパッケージのインストールが必要となります。
!pip install fugashi
!pip install ipadic
データセットを準備
今回は、Hugging Faceで準備されているtyqiangz/multilingual-sentimentsというデータセットを利用します。
実は、amazon_reviews_multi
というデータセットを使って記事を書いていたのですがデータセットが公開停止になりましたので、別データセットで書き直しました(2023.10.15)。コードが動作しなかった方、すみませんでした。
これを利用するには、load_dataset
関数を利用します。また、日本語のものだけ使うので、日本語だけを取り出すようにします。
from datasets import load_dataset
dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese")
データセットは以下の構成になっています
DatasetDict({
train: Dataset({
features: ['text', 'source', 'label'],
num_rows: 10000
})
validation: Dataset({
features: ['text', 'source', 'label'],
num_rows: 1000
})
test: Dataset({
features: ['text', 'source', 'label'],
num_rows: 1000
})
})
データセットをPandasのデータフレームに変換する場合は以下のようにします。
dataset.set_format(type="pandas")
train_df = dataset["train"][:]
train_df.head(5)
Tokenizerの取得
データセットをトークン化するには、Tokenizerを利用します。まず、学習済みのトークナイザーを取得します。今回利用するモデルは、cl-tohoku/bert-base-japanese
という日本語向けのBERTのモデルです。
Tokenizerの動きに興味がある場合は以下の記事を参考にしてください
Hugging Faceでは、ここからモデルの検索ができるので使いたいモデルを探してください。cl-tohokuは、東北大学が作成したモデルになります。今回は、この中のベースモデルを利用しています。
from transformers import AutoTokenizer
model_ckpt = "cl-tohoku/bert-base-japanese"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
トークナイザーで変換するとテキストは以下のようになります。
元のテキスト: 「普段使いとバイクに乗るときのブーツ兼用として...」
トークン化したもの: 「2, 9406, 3276, 13, 10602, 7, 11838, 900, 5, ...」
トークンを文字に変換: 「'[CLS]', '普段', '使い', 'と', 'バイク', 'に', '乗る', ...」
トークン化するとCLSとSEPという特殊なトークンが入ります。CLSは先頭に入るものでclassification embeddingと呼ばれています。
BERTのモデルでは、文字列は数値化されたトークンとして入力するわけです。
ところで、日本語用のトークナイザーを使わないと、日本語の分割はうまくいきません(形態素解析が必要なため)。multilingualという多国語対応のBERTもありますが日本語だけを対象とするなら、日本語モデルを利用した方が良い結果が得られると思います。
データサイズを減らす
tyqiangz/multilingual-sentimentsは大きくて、学習に時間がかかります。今回は、動作実験ということでデータを削減したサブセットで処理します。学習用データを10,000、検証用とテスト用データはそれぞれ1,000としました。なお、並び順が気になるので一応シャッフルして抽出しています。
SEED = 42
TRAIN_SIZE = 10000
TEST_SIZE = 1000
dataset["train"] = dataset["train"].shuffle(seed=SEED).select(range(TRAIN_SIZE))
dataset["validation"] = dataset["validation"].shuffle(seed=SEED).select(range(TEST_SIZE))
dataset["test"] = dataset["test"].shuffle(seed=SEED).select(range(TEST_SIZE))
データセットの加工
データセットをトークン化します。まず、トークン化するための関数を定義します。
labelは0〜2の数値ですのでそのままターゲットとして利用します。なお、ポジティブなレビューが0で、ネガティブなレビューが2となっています。星で考えると星3が最大とした場合、0が★★★、1が★★、2が★となります。
tokenizerは、テキストを入力するとトークンを返します。引数のpaddingは、文字数が少ない場合にパディング処理を行うかどうかの指示、truncationは、最大長を超えた場合にカットするかどうかの指示です。
import torch
def tokenize(batch):
enc = tokenizer(batch["text"], padding=True, truncation=True)
enc.update({'label': batch['label']})
return enc
padding, truncation, max_lengthの仕様が少し複雑です。
padding = True
tokenizerはリストで複数テキストを入力できます。padding=Trueにすると、一番長い文字列のトークン数に合わせてパディングが行われますpadding = "max_length"
パディングはmax_lengthの設定値に従って最大文字数まで行われます。こちらが、padding=Trueとした場合の動作イメージに近いかと思います。truncation = True
トークン数がmax_lengthを超えた場合に、超えた部分がカットされます
Tokenizerの挙動については以下にまとめていますのでそちらも参考にしてください
トークン化のための関数を定義したので、次にデータをトークン化します。今回読み込んだデータセットは、以下のやり方でトークン化する関数を呼び出します
dataset_encoded = dataset.map(tokenize)
最後に、train, valid, testの3つのデータセットを変数に代入しておきます。
small_train_dataset = dataset_encoded['train']
small_valid_dataset = dataset_encoded['validation']
small_test_dataset = dataset_encoded['test']
学習
データの準備ができたので、次は学習です。
まず、モデルを取得します。基本、モデルはトークナイザーと同じものを利用します。
AutoModelForSequenceClassification
を使ってモデルを取得しています。Amazonの評価は5段階なので、クラス数(ラベル数)は、5としています。ここは、labelsのクラス数と合わせる必要があります。
import torch
from transformers import AutoModelForSequenceClassification
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
num_labels = 5
model = (AutoModelForSequenceClassification
.from_pretrained(model_ckpt, num_labels=num_labels)
.to(device))
次に、評価関数を定義します。今回は、他の方のコードを参考に、accuracy とf1スコアを評価関数としています。evaluate
を使っても良いですが、ここでは、使い慣れたsklearnを用いています。
from sklearn.metrics import accuracy_score, f1_score
def compute_metrics(pred):
preds, labels = pred
preds = preds.argmax(-1)
f1 = f1_score(labels, preds, average="weighted")
acc = accuracy_score(labels, preds)
return {"accuracy": acc, "f1": f1}
トレーニング用のパラメータを設定します。ファインチューニングなので、それほどepoch数を増やさなくて良いと思います(過学習を防止する意味もあります)。
from transformers import TrainingArguments
batch_size = 16
logging_steps = len(small_train_dataset) // batch_size
model_name = "multilingual-sentiments-classification-bert"
training_args = TrainingArguments(
output_dir=model_name,
num_train_epochs=2,
learning_rate=2e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
evaluation_strategy="epoch",
disable_tqdm=False,
logging_steps=logging_steps,
push_to_hub=False,
log_level="error"
)
次に、トレーニングです。ここも、関数が用意されているので簡単です。
たった、データ数も減らして2epochしか学習させていませんが13分ほどかかりました。フルのデータで処理する場合は、数時間を覚悟する必要があるかと思います。
from transformers import Trainer
trainer = Trainer(
model=model,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=small_train_dataset,
eval_dataset=small_valid_dataset,
tokenizer=tokenizer
)
trainer.train()
実行結果はこんな感じになると思います。
テストデータに対する結果を評価
学習結果を確認するために、混同行列を表示させてみます。
ここで利用するのはテストデータです。テストデータは、学習ループで使っていないデータなので、モデルとしては初めて入力されるデータとなります。
テストデータに対する推論の実行は以下になります。
preds_output = trainer.predict(small_test_dataset)
グラフ作成は以下になります。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
y_preds = np.argmax(preds_output.predictions, axis=1)
y_valid = np.array(small_test_dataset["label"])
labels = ["1star", "2star", "3star"]
#dataset_encoded["train"].features["label"].names
def plot_confusion_matrix(y_preds, y_true, labels):
cm = confusion_matrix(y_true, y_preds, normalize="true")
fig, ax = plt.subplots(figsize=(6, 6))
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels)
disp.plot(cmap="Blues", values_format=".2f", ax=ax, colorbar=False)
plt.title("Normalized confusion matrix")
plt.show()
plot_confusion_matrix(y_preds, y_valid, labels)
結果は以下の通りです。これを見ると、正解のスターに対しておおよそ評価±1の範囲で推論されているようです。文章だけから推論しているので、多少の揺れはあると思っていましたが、学習データを減らした割には精度が出ているのではないでしょうか。
モデルの保存、読み込み
最後にモデルの保存と読み込みです。保存は以下のようにします。
trainer.save_model(f"./{model_name}-test")
読み込みは、以下のようにトークナイザーとモデルを別々に行います。
tokenizer = AutoTokenizer\
.from_pretrained(f"./{model_name}-test")
model = (AutoModelForSequenceClassification
.from_pretrained(f"./{model_name}-test")
.to(device))
読み込みすれば、tokenizerとmodelを普通に使うことができます。
pipelineを使って学習したモデルで推論する
Hugging Faceで用意されているpiplineを使うと推論は簡単に記述できます。以下に例を挙げます。
読み込んでいるモデルは、学習したモデルです。pipelineを使うと学習したモデルの推論を簡単な記述で行うことができます。
from transformers import pipeline
pipe = pipeline("text-classification", f"./{model_name}-test")
pipe("同価格帯のガン型電ドラより力が入りにくいが、手回しより楽です。締めすぎないので良いです。")
出力の部分を自作して精度向上するテクニックについては以下の記事を参考にしてください(こちらの記事は少し上級者向けです)
終わりに
Hugging Faceの登場からだいぶ経ちますが、このサイトのおかけでtransformerがとても簡単に使えるようになりました。
今回は、いろいろなWeb記事を参考にしつつ、データセットを変更、モデルを変更してトライしました。色々変更しつつ自分で書いてみることで、細かな動きについても理解することができたと感じています。
ぜひ、モデルやデータセットを変えて実験してみてください。色々知見が広がるかと思います。