今回は、現在実務で超がつくほど注目され、応用されている自然言語モデルであるBERTを解説していきます。有名なので、名前だけは聞いたことあるという方もいるかと思いますが、そのBERTは何なのか、何がすごいのかといったところから、どのように使うのかまでAI初学者の方にも分かりやすく解説していきます。
Google Colabで動かせる実装コードも用意したので、ぜひTryしてみてください!より理解が深まり、実務で応用できるようになると思います。
BERTとは
Bidirectional Encoder Representations from Transformersの略です。
文章の意味をベクトルにしたり、単語の意味をベクトルにしたりできます。モデルは、Transformer のEncoderを用いています(図-1)。Google検索で用いられたり、法律関係の文章が得意なBERT(LEGAL BERT)が登場したり、ビジネスの現場でも用いられていたりと様々なところで利用されています。詳細を知りたい方は下記論文まで。
図-1 Transformerモデルの構造
論文
Devlin, Jacob, et al. “Bert: Pre-training of deep bidirectional transformers for language understanding.” arXiv preprint arXiv:1810.04805 (2018).
https://arxiv.org/pdf/1810.04805.pdf
BERTのすごさ
BERTのすごさは下記の2点です。
・高精度
・誰でも使える
高精度
一言で表すと、Transformerのポテンシャルを活かしている点にあります。Transformerは、Huggingface社が提供しているオープンソースのライブラリで、様々なニューラルネットワークを用いた言語モデルが実装されています。Transformerの特徴として、注目する情報を上手く選べることと、並列処理との相性が良いことを活かしている点が挙げられます。
誰でも使える
一言でいうと、Pre-Training & Fine-Tuningのおかげです。
従来のDeep Learning
一気に高精度モデルを作ろうとしたため、大量の教師データと大量の計算資源が必要でした。そのため、普通の人には厳しいことが課題として挙げられます。懐かしのたまごっちを例にいうと、たまごから大人(くちぱっちなど)を作るということです。
BERT
二つの段階に分けて高精度のモデルを作ります。
第一段階は、pre-training(事前学習)と呼ばれ、言語の基礎理解を目指して、大量のデータと大量の計算資源で学習します。ここでの特徴として、教師データが必要ではないということと、一度事前学習したモデルは使い回せるということです。
第二段階は、fine-tuningと呼ばれ、事前学習したモデルを個別のタスクに習熟させることを目指して、学習します。ここで、少ないデータと少ない計算資源でも高精度なモデルを作ることができることから、誰でも使えると言われるようになりました。
たまごっちを例にいうと、たまごから事前学習によって成人(くちたまっち)を作り、そこからくちぱっちやたらこっち、にょろっちなどの大人を作るということです。成人(くちたまっち)の作成には、大学の機関やでかい企業などにまかせてしまい、作りたい大人のごっちを自分で作るということです。
BERTの実装
習うより慣れろということで、実際にBERTを用いて穴埋め問題を解いていきます。本記事では、東北大学の研究チームによって作成された事前学習モデルを使用します。
前処理
ライブラリのインストールと読み込み
1 |
!pip install transformers==4.5.0 fugashi==1.1.0 ipadic==1.0.0 |
1 2 3 |
import torch from transformers import BertJapaneseTokenizer, BertModel import numpy as np |
文章をトークン化する
②WordPieceを用いて単語をトークンに分割する。
単語に分割する
1 2 |
model = 'cl-tohoku/bert-base-japanese-whole-word-masking' tokenizer = BertJapaneseTokenizer.from_pretrained(model) |
1 |
tokenizer.tokenize('サーフィンしよう!') |
[‘サーフィン’, ‘しよ’, ‘う’, ‘!’]
1 |
tokenizer.tokenize('オンショア強くなってきました。') |
単語をトークンに分割する
1 2 |
input = tokenizer.encode('サーフィンしよう!') print(input) |
[2, 27677, 2132, 205, 679, 3]
1 |
tokenizer.convert_ids_to_tokens(input) |
BERTモデルの実装 文章の穴埋め
1 2 3 4 5 6 7 |
from transformers.models.auto.modeling_auto import BertForMaskedLM model = 'cl-tohoku/bert-base-japanese-whole-word-masking' tokenizer = BertJapaneseTokenizer.from_pretrained(model) bert = BertForMaskedLM.from_pretrained(model) #BERTをGPUに乗せる bert = bert.cuda() |
1 2 3 |
text = '独学で[MASK]を目指す大学院生' tokens = tokenizer.tokenize(text) print(tokens) |
1 2 3 4 5 6 7 8 |
input = tokenizer.encode(text,return_tensors='pt') #データをGPUに載せる input = input.cuda() with torch.no_grad(): output = bert(input_ids=input) scores = output.logits |
①[MASK]のトークンがどこにあるのかを調べる。ここではjとする。→[MASK]のトークンIDが4なので、4のインデックスを調べる
②スコアが最も良いトークンのIDを取り出す。
③[MASK]を②で求めたトークンに変換する。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#① mask_position = input[0].tolist().index(4) #② k番目の要素の値scores[i,j,k]は、IDがkのトークンのスコアを表す。 id_best = scores[0,mask_position].argmax(-1).item() token_best = tokenizer.convert_ids_to_tokens(id_best) token_best = token_best.replace('##','') #③ text = text.replace('[MASK]',token_best) print(text) |
独学でプロを目指す大学院生
上位10件も気になるので出力してみます。ここでは関数化して試してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def predict_mask_topk(text, tokenizer, bert_mlm, num_topk): input_ids = tokenizer.encode(text, return_tensors='pt') input_ids = input_ids.cuda() with torch.no_grad(): output = bert_mlm(input_ids=input_ids) scores = output.logits mask_position = input_ids[0].tolist().index(4) topk = scores[0, mask_position].topk(num_topk) ids_topk = topk.indices tokens_topk = tokenizer.convert_ids_to_tokens(ids_topk) scores_topk = topk.values.cpu().numpy() text_topk = [] # 穴埋めされたテキストを格納 for token in tokens_topk: token = token.replace('##', '') text_topk.append(text.replace('[MASK]', token, 1)) return text_topk, scores_topk |
1 2 3 |
text = '独学で[MASK]を目指す大学院生' text_topk, _ = predict_mask_topk(text,tokenizer,bert,10) print(*text_topk,sep='\n') |
独学でプロを目指す大学院生
独学で作家を目指す大学院生
独学でアナウンサーを目指す大学院生
独学でジャーナリストを目指す大学院生
独学で文学を目指す大学院生
独学でトップを目指す大学院生
独学で声優を目指す大学院生
独学で芸術を目指す大学院生
独学で音楽を目指す大学院生
独学で学問を目指す大学院生
復数の[MASK]が存在しているケース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def beam_search(text,tokenizer,bert,num_topk): num_mask = text.count('[MASK]') text_topk = [text] scores_topk = np.array([0]) for _ in range(num_mask): #最初の[MASK]に対する穴埋め text_candidates = [] #穴埋めした結果を格納 score_candidates = [] #穴埋めに使用したトークンのスコアを格納 for text_mask, score in zip(text_topk,scores_topk): text_topk_inner, scores_topk_innner = predict_mask_topk(text_mask,tokenizer,bert,num_topk) text_candidates.extend(text_topk_inner) score_candidates.append(score+scores_topk_innner) #合計スコアの高いものを選ぶ score_candidates = np.hstack(score_candidates) idx_list = score_candidates.argsort()[::-1][:num_topk] text_topk = [text_candidates[idx] for idx in idx_list] scores_topk = score_candidates[idx_list] return text_topk |
1 2 3 |
text = '[MASK]で[MASK]を目指す大学院生' text_topk = beam_search(text,tokenizer,bert,10) print(*text_topk,sep='\n') |
進んで大学を目指す大学院生
自力で大学を目指す大学院生
進んでトップを目指す大学院生
現役でプロを目指す大学院生
大学で教員を目指す大学院生
自力でトップを目指す大学院生
日本で起業を目指す大学院生
自力で卒業を目指す大学院生
進んで進学を目指す大学院生
社会で自立を目指す大学院生
文章のほとんどが[MASK]のケース
1 2 3 |
text = '独学で[MASK]を[MASK][MASK]' text_topk = beam_search(text,tokenizer,bert,10) print(*text_topk,sep='\n') |
独学で武道を学ぶ。
独学で外交を学ぶ。
独学で武道を習得。
独学で本を読む。
独学で文化を学ぶ。
独学で社会を学ぶ。
独学で本を書く。
独学で社会を知る。
独学で武道を覚える。
独学で話を覚える。
まとめ・考察
BERTについて理論と実践に分けて見てきました。
理論についてたまごっちを例に解説したことと、BERTを試してみて感じたことについてまとめてみました。
今後は、BERTを用いて文単位のタスク(分類など)や単語単位のタスクに挑戦していきたいと思います。