本記事では、日本語 BERT モデルをファインチューニングして感情分析する方法を解説します。
BERT の詳細な解説は、この記事のスコープ外とします。
この記事は、part01 です。
part02 では、まとまったデータセットを使って実際に学習と評価を行っています。
Hugging Face Transformers とは
Hugging Face 社によって開発されている、言語モデルを容易に扱うための OSS ライブラリ です。
また同社によって、学習済みモデルやデータセットを公開するリポジトリが提供されています。
BERT
BERT とは、Bidirectional Encoder Representations from Transformersの略称で、Googleが開発した自然言語処理のモデルです。
ラベルのないテキストから文章の中の単語やフレーズの意味や関係性を事前学習し、出力層を1つ追加してファインチューニングを行うことで、幅広いタスクに対して性能の良いモデルを作成できます。
!pip install -q transformers fugashi[unidic-lite]
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline
from torch.optim import AdamW
最初に、huggingface transformers を使った日本語 BERT pre-trained model の使い方や fine tuning の方法を、見ていきます。
今回試す事前学習済みモデルとして、東北大学のグループによって公開されているものを利用します。
参考
BERT モデルは、mask された token ([MASK]
) を予測するように学習されています。
したがって、pre-trained model を使って、文章中の穴埋め (文章中の欠損箇所の予測) を 行えます。
以下の2種類のモデルを使って推論を試して、結果を比較してみましょう。
pipeline に fill-mask
タスクを指定することで、簡単に試すことができます。
model_name = "cl-tohoku/bert-large-japanese"
unmasker = pipeline('fill-mask', model=model_name)
unmasker("今日の昼食は[MASK]でした。")
Some weights of the model checkpoint at cl-tohoku/bert-large-japanese were not used when initializing BertForMaskedLM: ['cls.seq_relationship.bias', 'cls.seq_relationship.weight'] - This IS expected if you are initializing BertForMaskedLM from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model). - This IS NOT expected if you are initializing BertForMaskedLM from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
[{'score': 0.04221104457974434, 'token': 32474, 'token_str': 'サラダ', 'sequence': '今日 の 昼食 は サラダ でし た 。'}, {'score': 0.036806173622608185, 'token': 18526, 'token_str': 'カレー', 'sequence': '今日 の 昼食 は カレー でし た 。'}, {'score': 0.0313434936106205, 'token': 31893, 'token_str': 'ご飯', 'sequence': '今日 の 昼食 は ご飯 でし た 。'}, {'score': 0.021632177755236626, 'token': 17540, 'token_str': '元気', 'sequence': '今日 の 昼食 は 元気 でし た 。'}, {'score': 0.020115602761507034, 'token': 23869, 'token_str': 'うどん', 'sequence': '今日 の 昼食 は うどん でし た 。'}]
model_name = "bert-base-multilingual-uncased"
unmasker = pipeline('fill-mask', model=model_name)
unmasker("今日の昼食は[MASK]でした。")
Some weights of the model checkpoint at bert-base-multilingual-uncased were not used when initializing BertForMaskedLM: ['cls.seq_relationship.bias', 'cls.seq_relationship.weight'] - This IS expected if you are initializing BertForMaskedLM from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model). - This IS NOT expected if you are initializing BertForMaskedLM from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
[{'score': 0.17987696826457977, 'token': 7753, 'token_str': '見', 'sequence': '今 日 の 昼 食 は 見 てした 。'}, {'score': 0.06706605106592178, 'token': 4080, 'token_str': '捨', 'sequence': '今 日 の 昼 食 は 捨 てした 。'}, {'score': 0.06436670571565628, 'token': 2073, 'token_str': '全', 'sequence': '今 日 の 昼 食 は 全 てした 。'}, {'score': 0.060412339866161346, 'token': 5216, 'token_str': '満', 'sequence': '今 日 の 昼 食 は 満 てした 。'}, {'score': 0.02542056515812874, 'token': 4518, 'token_str': '果', 'sequence': '今 日 の 昼 食 は 果 てした 。'}]
課題
unmasker
に別の文章を渡して推論させてみましょう。
感情分析タスクは "sentiment-analysis" を pipeline
のタスクに指定することで行えます。
pipeline で利用可能なタスク一覧は、Hugging Face のドキュメント で確認できます。
ただし、モデルが指定されたタスクに対応している必要があります。
本来であれば、もっと大規模な学習データセットを用意するべきですが、ここでは説明の簡素化のために、単純なサンプルを手作業で作成して手順を確認します。
以下のように、3 種類のラベル (positive: 2, neutral: 1, negative: 0) のデータを用意します。
# 確認用のデータセット
df = pd.DataFrame(
[
{"text": "私はこの映画をみることができて、とても嬉しい。", "label": "POSITIVE"},
{"text": "今日の晩御飯は何だろう。", "label": "NEUTRAL"},
{"text": "猫に足を噛まれて痛い。", "label": "NEGATIVE"}
]
)
df
text | label | |
---|---|---|
0 | 私はこの映画をみることができて、とても嬉しい。 | POSITIVE |
1 | 今日の晩御飯は何だろう。 | NEUTRAL |
2 | 猫に足を噛まれて痛い。 | NEGATIVE |
train_docs = df["text"].tolist()
train_labels = df["label"].tolist()
model_name = "cl-tohoku/bert-large-japanese"
id2label = {0: "NEGATIVE", 1: "NEUTRAL", 2: "POSITIVE"}
label2id = {"NEGATIVE": 0, "NEUTRAL": 1, "POSITIVE": 2}
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=3, id2label=id2label, label2id=label2id)
tokenizer = AutoTokenizer.from_pretrained(model_name)
Some weights of the model checkpoint at cl-tohoku/bert-large-japanese were not used when initializing BertForSequenceClassification: ['cls.predictions.decoder.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.decoder.weight', 'cls.seq_relationship.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.bias', 'cls.predictions.transform.LayerNorm.bias', 'cls.seq_relationship.weight', 'cls.predictions.transform.dense.bias'] - This IS expected if you are initializing BertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model). - This IS NOT expected if you are initializing BertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model). Some weights of BertForSequenceClassification were not initialized from the model checkpoint at cl-tohoku/bert-large-japanese and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
encodings = tokenizer(train_docs, return_tensors='pt', padding=True, truncation=True, max_length=128)
input_ids = encodings['input_ids']
attention_mask = encodings['attention_mask']
# Fine-tuning in native PyTorch
# the AdamW() optimizer which implements gradient bias correction as well as weight decay.
optimizer = AdamW(model.parameters(), lr=1e-5)
labels = [label2id[label] for label in train_labels]
labels = torch.tensor(labels).unsqueeze(0)
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
loss.backward()
optimizer.step()
sentiment_analyzer = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
sentiment_analyzer("これは、テストのための文章です")
[{'label': 'POSITIVE', 'score': 0.41579246520996094}]
# 学習データに対する推論
_ = list(map(lambda x: print(f"{x}: {sentiment_analyzer(x)}"), train_docs))
私はこの映画をみることができて、とても嬉しい。: [{'label': 'POSITIVE', 'score': 0.5623015761375427}] 今日の晩御飯は何だろう。: [{'label': 'NEUTRAL', 'score': 0.4257284700870514}] 猫に足を噛まれて痛い。: [{'label': 'NEGATIVE', 'score': 0.586097776889801}]
課題
sentiment_analyzer
に任意の文章を渡して推論してみましょう。