機械学習
記事内に商品プロモーションを含む場合があります

Hugging Face Transformer(BERT)でクラス分類(classification)

tadanori

この記事では、Hugging FaceのBERTモデルを使用して、日本語テキストのクラス分類を実践する方法を解説します。BERTを使うと、高精度なテキスト分類が可能です。Hugging Faceのライブラリを利用すればBERTモデルの利用も簡単です。

コードは、githubに置きましたのでそちらも参考にしてください。

github: Colabで動くコード

BERTを使って回帰問題を解く場合は以下の記事を参考にしてください。

あわせて読みたい
Hugging Face Transformer(BERT)で回帰分析(Regression)
Hugging Face Transformer(BERT)で回帰分析(Regression)

Hugging Faceについて

Hugging Faceは、transformersをベースにした機械学習モデルを公開するプラットフォームとして有名です。Hugging Faceを利用することで、自然言語処理に興味がある技術者が簡単に自然言語処理を試すことができるようになりました。

ということで、日本語のテキストを分類する問題を実際にHugging Faceを使ってやってみたいと思います。

BERTを利用する場合の流れについて

BERTを利用する場合は、再学習ではなくてファインチューニングがメインとなります。というのも、BERTをゼロから学習させるのは大変で、それなりのGPUなどのリソースが必要となるため、個人レベルでやる場合はどうしてもファインチューニングレベルまでとなってしまします。

ファインチューニングを行う場合、処理の流れは以下になります。

  1. データセットを準備
  2. モデルを選択(学習済みモデルを利用する)
  3. モデルに合わせたトークナイザーで、テキストをトークン化
  4. トークン化したデータセットで学習
  5. ファインチューニングしたモデルの保存、推論テストなど

画像のクラス分類などと大きく異なるのは、テキストをトークン化する部分でしょうか。自然言語処理をする場合は、ここが少し特殊になります。

クラス分類を実践(日本語テキスト)

今回も、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に変換する場合

データセットを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のTokenizerの動作まとめ
Hugging Faceの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
tokenizerの仕様について

padding, truncation, max_lengthの仕様が少し複雑です。

  • padding = True
    tokenizerはリストで複数テキストを入力できます。padding=Trueにすると、一番長い文字列のトークン数に合わせてパディングが行われます
  • padding = "max_length"
    パディングはmax_lengthの設定値に従って最大文字数まで行われます。こちらが、padding=Trueとした場合の動作イメージに近いかと思います。
  • truncation = True
    トークン数がmax_lengthを超えた場合に、超えた部分がカットされます

Tokenizerの挙動については以下にまとめていますのでそちらも参考にしてください

あわせて読みたい
Hugging FaceのTokenizerの動作まとめ
Hugging Faceの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の範囲で推論されているようです。文章だけから推論しているので、多少の揺れはあると思っていましたが、学習データを減らした割には精度が出ているのではないでしょうか。

Confusion Matrix

モデルの保存、読み込み

最後にモデルの保存と読み込みです。保存は以下のようにします。

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("同価格帯のガン型電ドラより力が入りにくいが、手回しより楽です。締めすぎないので良いです。")
参考情報

出力の部分を自作して精度向上するテクニックについては以下の記事を参考にしてください(こちらの記事は少し上級者向けです)

あわせて読みたい
HuggingFace Transformerの精度向上テクニック | 出力のPooling手法
HuggingFace Transformerの精度向上テクニック | 出力のPooling手法

終わりに

Hugging Faceの登場からだいぶ経ちますが、このサイトのおかけでtransformerがとても簡単に使えるようになりました。

今回は、いろいろなWeb記事を参考にしつつ、データセットを変更、モデルを変更してトライしました。色々変更しつつ自分で書いてみることで、細かな動きについても理解することができたと感じています。

ぜひ、モデルやデータセットを変えて実験してみてください。色々知見が広がるかと思います。

おすすめ書籍

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました