これは、なにをしたくて書いたもの?
Transformersかつ日本語で、なにか動かしてみたいなと思って題材を探していたのですが、MLM(Masked Language Modeling)が
ちょうど良さそうだったので試してみました。
MLM(Masked Language Modeling)
MLM(Masked Language Modeling)は、マスクされたテキストからオリジナルのテキストを予測するものです。
A pretraining task where the model sees a corrupted version of the texts, usually done by masking some tokens randomly, and has to predict the original text.
Glossary / masked language modeling (MLM)
TransformersにおけるMLMタスクの説明は、こちらにあります。
MLMをサポートしているモデルは以下になります。
You can finetune other architectures for masked language modeling following the same steps in this guide. Choose one of the following architectures:
ALBERT, BART, BERT, BigBird, CamemBERT, ConvBERT, Data2VecText, DeBERTa, DeBERTa-v2, DistilBERT, ELECTRA, ERNIE, ESM, FlauBERT, FNet, Funnel Transformer, I-BERT, LayoutLM, Longformer, LUKE, mBART, MEGA, Megatron-BERT, MobileBERT, MPNet, MRA, MVP, Nezha, Nyströmformer, Perceiver, QDQBert, Reformer, RemBERT, RoBERTa, RoBERTa-PreLayerNorm, RoCBert, RoFormer, SqueezeBERT, TAPAS, Wav2Vec2, XLM, XLM-RoBERTa, XLM-RoBERTa-XL, X-MOD, YOSO
今回はBERTを使ってみたいと思います。
BERT
Transformersにおける、BERTモデルの説明はこちら。
BERTは、トロント書籍コーパスとWikipediaで構成される大規模なコーパスの上に構築された、マスクされた言語モデル目標と次の文を
予測の組み合わせを使用した、事前学習済みの双方向Transformerです。
It’s a bidirectional transformer pretrained using a combination of masked language modeling objective and next sentence prediction on a large corpus comprising the Toronto Book Corpus and Wikipedia.
要するに、補完したい単語の前後のトークンを見て、その文脈から推論できるように学習したモデルのようです。
BERTのページを見ていると、MLM以外にも次の文の予測、分類、多肢選択、質疑応答などができそうです。
なお、BERT自体はGoogleが提案した言語モデルのようです。
[1810.04805] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
BERT Japanese
BERTの特徴はわかりましたが、これを日本語で使おうと思うと単語を認識する必要があるらしく、日本語向けのBERTがTransformersでは
提供されています。
トークン化にはMeCab(のラッパーであるfugashi)とWordPieceを使用していて、実体としては東北大学が作成したもののようです。
GitHub - cl-tohoku/bert-japanese: BERT models for Japanese text.
モデルはHugging Face Hubで公開されています。
これは、CC-100コーパスで学習した後にWikipediaコーパスで学習するという2段階の学習を行ったもののようです。
モデルには複数の種類があり、これらのモデルをベースにファインチューニングして評価したJGLUEベンチマークで結果が記載してあります。
Pretrained Japanese BERT models / Model Performances
今回は、cl-tohoku/bert-base-japanese-v3を使ってMLMを試してみたいと思います。
cl-tohoku/bert-base-japanese-v3 · Hugging Face
余談:JGLUE
JGLUEというのは「Japanese General Language Understanding Evaluation」の略で、日本語の一般的な
NLU(Natural Language Understanding)の能力を測定するためのベンチマークです。
GitHub - yahoojapan/JGLUE: JGLUE: Japanese General Language Understanding Evaluation
NLUは、テキストの内容を理解することに関するすべてのタスクです。
Glossary / Natural language understanding (NLU)
もともとGLUE(General Language Understanding Evaluation)というベンチマークがあり、日本語理解能力に関する版として
構築されているのがJGLUEのようです。Yahoo Japanと早稲田大学により構築されたのがJGLUEです。
日本語言語理解ベンチマークJGLUEの構築 〜 自然言語処理モデルの評価用データセットを公開しました - Yahoo! JAPAN Tech Blog
JGLUEは以下のタスクとデータセットで構成されています。
タスク | データセット | 説明 |
---|---|---|
テキスト分類(Text Classification) | MARC-ja | テキストを肯定的(positive)と否定的(negative)でラベル付けして判定する |
JCoLA | (未公開) | |
文ペアの分類(Sentence Pair Classification) | JSTS | 2つの文の類似度を0〜5(5が最も類似)で判定する |
JNLI | 含意(entailment)、矛盾 (contradiction)、中立(neutral)のいずれの推論関係かを推定する | |
質疑応答(QA) | JSQuAD | 抽出型質疑応答(extractive question answering)または機械読解(machine reading comprehension)と呼ばれ、質問文と答えを含む可能性のある文章を元に解答箇所を抽出する |
JCommonsenseQA | 常識推論能力を評価するための多肢選択式質疑応答(multiple choice question answering)のデータセット |
JGLUEはデータセットが提供されており、Hugging FaceのTransformersでファインチューニングする方法も書かれています。
https://github.com/yahoojapan/JGLUE/tree/v1.1.0/fine-tuning
つまり、こちらを使って既存のモデルをファインチューニングするとMARC-jaを使った肯定・否定の判定や、JSTSを使ったテキストの
類似度を判定するようなタスクが実行できるということになります。
先述の東北大学のBERTもそのようにファインチューニングした旨が記載されていましたし、JGLUEにもいくつかのモデルで実行した
結果が記載されています。
- Tohoku BERT base
- Tohoku BERT base (char)
- Tohoku BERT large
- NICT BERT base
- Waseda RoBERTa base
- Waseda RoBERTa large (s128)
- Waseda RoBERTa large (s512)
- XLM RoBERTa base
- XLM RoBERTa large
JGLUE: Japanese General Language Understanding Evaluation / Baseline Scores
BERT以外にもRoBERTa(BERTを改善したモデル)、XLM RoBERTa(XLM-R:RoBERTaの他言語版)などもあるのだなと思いつつも、
今回は学習まではさせないので東北大学のBERTをそのまま使います。
環境
今回の環境はこちら。
$ python3 --version Python 3.10.12 $ pip3 --version pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
CPU環境です。
Transformersのマスクされたトークンを予測するタスクを試す
では、Transformersを使ってMLMを試してみたいと思います。
仮想環境の設定。
$ python3 -m venv venv $ . venv/bin/activate
BertJapaneseを見ると、BERTの日本語モデルはtransformers[ja]
に含まれているようです。
Transformersを含め、以下のようにインストール。
$ pip3 install transformers[torch,ja]
インストールされたライブラリー。
$ pip3 list Package Version ------------------------ ---------- accelerate 0.25.0 certifi 2023.11.17 charset-normalizer 3.3.2 filelock 3.13.1 fsspec 2023.12.2 fugashi 1.3.0 huggingface-hub 0.20.1 idna 3.6 ipadic 1.0.0 Jinja2 3.1.2 MarkupSafe 2.1.3 mpmath 1.3.0 networkx 3.2.1 numpy 1.26.3 nvidia-cublas-cu12 12.1.3.1 nvidia-cuda-cupti-cu12 12.1.105 nvidia-cuda-nvrtc-cu12 12.1.105 nvidia-cuda-runtime-cu12 12.1.105 nvidia-cudnn-cu12 8.9.2.26 nvidia-cufft-cu12 11.0.2.54 nvidia-curand-cu12 10.3.2.106 nvidia-cusolver-cu12 11.4.5.107 nvidia-cusparse-cu12 12.1.0.106 nvidia-nccl-cu12 2.18.1 nvidia-nvjitlink-cu12 12.3.101 nvidia-nvtx-cu12 12.1.105 packaging 23.2 pip 22.0.2 plac 1.4.2 psutil 5.9.7 PyYAML 6.0.1 regex 2023.12.25 requests 2.31.0 rhoknp 1.3.0 safetensors 0.4.1 setuptools 59.6.0 SudachiDict-core 20230927 SudachiPy 0.6.8 sympy 1.12 tokenizers 0.15.0 torch 2.1.2 tqdm 4.66.1 transformers 4.36.2 triton 2.1.0 typing_extensions 4.9.0 unidic 1.1.0 unidic-lite 1.0.8 urllib3 2.1.0 wasabi 0.10.1
容量。
$ du -sh venv 5.2G venv
マスクする箇所は[MASK]
と表記すればよいようなので、以下のようにpipelineで作成。
fill_mask_pipeline.py
import time from transformers import pipeline start_time = time.perf_counter() mask_filler = pipeline("fill-mask", model="cl-tohoku/bert-base-japanese-v3") texts = [ "日本の首都は[MASK]である", "魚は[MASK]を泳ぐ", "吾輩は[MASK]である" ] for text in texts: outputs = mask_filler(text, top_k=3) print(f"input = {text}:") print("outputs:") for output in outputs: print(f" {output}") print() elapsed_time = time.perf_counter() - start_time print(f"elapsed time = {elapsed_time:.3f} sec")
パイプラインの使用例はこちらを参考に。
使用しているモデルはこちらですね。
cl-tohoku/bert-base-japanese-v3 · Hugging Face
3つの文で試してみます。
texts = [ "日本の首都は[MASK]である", "魚は[MASK]を泳ぐ", "吾輩は[MASK]である" ]
実行結果。
fill_mask_pipeline.py input = 日本の首都は[MASK]である: outputs: {'score': 0.8841696977615356, 'token': 12569, 'token_str': '東京', 'sequence': '日本 の 首都 は 東京 で ある'} {'score': 0.024819981306791306, 'token': 12759, 'token_str': '大阪', 'sequence': '日本 の 首都 は 大阪 で ある'} {'score': 0.020863836631178856, 'token': 13017, 'token_str': '京都', 'sequence': '日本 の 首都 は 京都 で ある'} input = 魚は[MASK]を泳ぐ: outputs: {'score': 0.4581770598888397, 'token': 18965, 'token_str': '水中', 'sequence': '魚 は 水中 を 泳ぐ'} {'score': 0.10925627499818802, 'token': 23309, 'token_str': '水面', 'sequence': '魚 は 水面 を 泳ぐ'} {'score': 0.0645589679479599, 'token': 27988, 'token_str': '海中', 'sequence': '魚 は 海中 を 泳ぐ'} input = 吾輩は[MASK]である: outputs: {'score': 0.4953293800354004, 'token': 3767, 'token_str': '猫', 'sequence': '吾輩 は 猫 で ある'} {'score': 0.04334617033600807, 'token': 20277, 'token_str': '勇者', 'sequence': '吾輩 は 勇者 で ある'} {'score': 0.037576641887426376, 'token': 3732, 'token_str': '犬', 'sequence': '吾輩 は 犬 で ある'} elapsed time = 11.969 sec
スコアの高いものを見ると、それっぽい文がsequence
として現れています。
いくつか試してみましたが、こういうキレイなハマり方をしない文章も出てきましたね。
続いて、Auto Classesで作成してみます。AutoTokenizer
とAutoModelForMaskedLM
を使えば良さそうです。
fill_mask_autoclasses.py
import time from transformers import AutoTokenizer, AutoModelForMaskedLM start_time = time.perf_counter() tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese-v3") model = AutoModelForMaskedLM.from_pretrained("cl-tohoku/bert-base-japanese-v3") texts = [ "日本の首都は[MASK]である", "魚は[MASK]を泳ぐ", "吾輩は[MASK]である" ] for text in texts: inputs = tokenizer(text, return_tensors='pt') outputs = model.generate(**inputs, max_new_tokens=15) generated_texts = tokenizer.batch_decode(outputs, skip_special_tokens=True) print(f"input = {text}:") print("outputs:") for generated_text in generated_texts: print(f" {generated_text}") print() elapsed_time = time.perf_counter() - start_time print(f"elapsed time = {elapsed_time:.3f} sec")
実行結果。
$ python3 fill_mask_autoclasses.py input = 日本の首都は[MASK]である: outputs: 日本 の 首都 は で ある 日本 国 の 首都 一覧 # 日本 の 首都 一覧 日本 の 歴史 # 歴史 input = 魚は[MASK]を泳ぐ: outputs: 魚 は を 泳ぐ 魚 一覧 深海 魚 一覧 深海 魚 一覧 # 深海 魚 # 魚類 深海 魚 input = 吾輩は[MASK]である: outputs: 吾輩 は で ある 」 ) 。 」 ( 「 吾輩 は 猫 」 「 そして 猫 」 elapsed time = 4.284 sec
ちょっと変?です。
そこで、デコードする際にskip_special_tokens=True
を指定しないようにしてみます。
#generated_texts = tokenizer.batch_decode(outputs, skip_special_tokens=True)
generated_texts = tokenizer.batch_decode(outputs)
結果。
$ python3 fill_mask_autoclasses.py input = 日本の首都は[MASK]である: outputs: [CLS] 日本 の 首都 は [MASK] で ある [SEP] 日本 国 の 首都 一覧 # 日本 の 首都 一覧 日本 の 歴史 # 歴史 input = 魚は[MASK]を泳ぐ: outputs: [CLS] 魚 は [MASK] を 泳ぐ [SEP] 魚 一覧 深海 魚 一覧 深海 魚 一覧 # 深海 魚 # 魚類 深海 魚 input = 吾輩は[MASK]である: outputs: [CLS] 吾輩 は [MASK] で ある [SEP] 」 ) 。 」 ( 「 吾輩 は 猫 」 「 そして 猫 」 elapsed time = 4.136 sec
どうやら、pipelineで実行した時には[MASK]
にトークンを入れ込むところなどの面倒をみていてくれたみたいですね…。
おわりに
TransformersでMLMを使って、マスクされたトークンを予測してみました。
MLMというよりはBERTやJGLUEなどの周辺知識の勉強になった気がしますが、これはこれで良いかなと思います。
Transformersの使い方として、Auto Classesを全然使いこなせていない感じがするので…ここはちょっとなんとかしたいですね…。