日本語BERTモデルに、センター試験や文章生成をやらせてみる

#1.はじめに
　前回、BERTの事前学習済みモデルを[**ファインチューニングしてネガ・ポジ判定タスク**](https://qiita.com/jun40vn/items/b7f32621ec7399baa3f8)をやってみたわけですが、ファインチューニング無しでも当然ながら動きます。

　今回は、GoogleColabで**日本語BERT事前学習済みモデル**をそのまま試してみます。

#2.セットアップ
　セットアップ手順は、以下の様です。コードは、Google Colabで参照下さい（最後にリンクがあります）。

**1) モジュールのインストール**
　必要なモジュール( **pyknp, transformers** )をインストールします。

**2) 形態素分析ライブラリーのインストール**
　京都大学／黒橋・褚・村脇研究室が提供している[**「日本語形態素解析システムJUMMAN++」**](http://nlp.ist.i.kyoto-u.ac.jp/index.php?JUMAN++)を使用します。

**3) 日本語BERT事前学習済みモデルのダウンロード**
　京都大学／黒橋・褚・村脇研究室が提供している[**「BERT日本語Pretrainedモデル」**](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT日本語Pretrainedモデル)を使用します。

#3.BERTはセンター試験問題を解けるか？
　今回のモデルは**日本語Wikipedia**を使って事前学習しています。ということは**古今東西の様々な知識を穴埋め問題を解く形で勉強している**わけで、それなら**大学入試センター試験の穴埋め問題**くらい解けるかもしれないと無茶振りしてみることにしました。題材は[平成３０年度の世界史Bの問題９](https://www.dnc.ac.jp/daigakunyugakukibousyagakuryokuhyoka_test/pre-test_h30_1110.html)です。
![スクリーンショット 2020-08-06 12.55.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/01ed0054-ab49-1299-96c8-c8d5ab7dfac1.png)
　正解は①の「貴族」と「カエサル」なわけですが、さてBERTはどう解答するでしょうか。

```python
import torch
from transformers import BertTokenizer, BertForMaskedLM, BertConfig
import numpy as np
import textwrap
config = BertConfig.from_json_file('./bert/Japanese_L-12_H-768_A-12_E-30_BPE_transformers/config.json')
model = BertForMaskedLM.from_pretrained('./bert/Japanese_L-12_H-768_A-12_E-30_BPE_transformers/pytorch_model.bin', config=config)
bert_tokenizer = BertTokenizer('./bert/Japanese_L-12_H-768_A-12_E-30_BPE_transformers/vocab.txt',
 do_lower_case=False, do_basic_tokenize=False)
from pyknp import Juman
jumanpp = Juman()
```
　まず、必要なライブラリーをインポートし、BERTの設定をします。

　BERTへの入力は、単語リストの先頭に[CLS]を入れ、文の区切りに[SEP]を入れ、予測したい単語は[MASK]に置き換える仕様になっているので、それを行う関数を定義します。

```python
# 単語リストへ[CLS],[SEP],[MASK]を追加する関数
def preparation(tokenized_text):
    
    # [CLS],[SEP]の挿入
    tokenized_text.insert(0, '[CLS]')  # 単語リストの先頭に[CLS]を付ける
    tokenized_text.append('[SEP]')  # 単語リストの最後に[SEP]を付ける 
        
    maru = []
    for i, word in enumerate(tokenized_text):
        if word =='。' and i !=len(tokenized_text)-2:  # 「。」の位置検出
            maru.append(i)

    for i, loc in enumerate(maru):
        tokenized_text.insert(loc+1+i, '[SEP]')  # 単語リストの「。」の次に[SEP]を挿入する
        
    # 「□」を[MASK]に置き換え　
    mask_index = []
    for index, word in enumerate(tokenized_text):
        if word =='□':  # 「□」の位置検出
            tokenized_text[index] = '[MASK]'
            mask_index.append(index)
    
    return tokenized_text, mask_index  
```
　関数は、単語リストにの先頭に[CLS]を挿入し、最後に[SEP]を付加し、途中は「。」の後に[SEP]を挿入します。そうやって単語位置を決めてから、予測箇所「□」を[MASK]に置き換え、単語リストと[MASK]位置を返します。

　次に、テキストをIDテンソルに変換します。

```python
# テキストをIDテンソルに変換
text = "ギリシア人ポリュビオスは，著書『歴史』の中で，ローマ共和政の国制（政治体制）を優れたものと評価している。彼によれば，その国制には，コンスルという王制的要素，元老院という□制的要素，民衆という民主制的要素が存在しており，これら三者が互いに協調や牽制をしあって均衡しているというのである。ローマ人はこの政治体制を誇りとしており，それは，彼らが自らの国家を指して呼んだ「ローマの元老院と民衆」という名称からも読み取ることができる。共和政期末の内戦を勝ち抜いたかに見えた□でさえも，この体制を壊そうとしているという疑いをかけれ，暗殺されてしまった。"
result = jumanpp.analysis(text)  # 分かち書き
tokenized_text = [mrph.midasi for mrph in result.mrph_list()]  # 単語リストに変換
tokenized_text, mask_index = preparation(tokenized_text)  # [CLS],[SEP],[MASK]の追加
tokens = bert_tokenizer.convert_tokens_to_ids(tokenized_text)  # IDリストに変換
tokens_tensor = torch.tensor([tokens])  # IDテンソルに変換
```
　テキストを単語リストにし、先程の関数を使って[CLS],[SEP],[MASK]を追加したら、IDリストに変換し、Pytorchが読めるIDテンソルに変換します。

![スクリーンショット 2020-08-06 18.47.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/98c5537c-8940-dec2-df67-e4b9e00910ba.png)
　こんな感じに変換を行います。

　それでは、[MASK]箇所を推論(上位5つの候補)します。

```python
# [MASK]箇所を推論(TOP5)
model.eval() 
tokens_tensor = tokens_tensor.to('cuda')
model.to('cuda')
print(textwrap.fill(text, 45))
print()

with torch.no_grad():
  outputs = model(tokens_tensor)
  predictions = outputs[0]
  
  for i in range(len(mask_index)):
     _, predicted_indexes = torch.topk(predictions[0, mask_index[i]], k=5)
     predicted_tokens = bert_tokenizer.convert_ids_to_tokens(predicted_indexes.tolist())
     print(i, predicted_tokens)
```
![スクリーンショット 2020-08-06 15.35.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d342893e-604c-a80a-94fd-3a3f07883e65.png)

　無茶振りでしたが、１つ目の予測にはちゃんと正解の**「貴族」**が含まれています！ 残念ながら2つ目の「カエサル」は正解できませんでしたが、BERT思ったよりやりますね。

#4.BERTは文章生成ができるか？
　事前学習しかしていないBERTは**穴埋め問題**と**２つの文の繋がり**しか学習していないので、そのままでは文章生成には向かないです。しかし原理上やれないことはないです。

　あるテキストを用意して、先頭の単語に[MASK]を掛け予測をしたら、先頭の単語を予測結果に置き換え、次の単語に[MASK]を掛け予測する、ということを繰り返すとテキストに似た新たな文が生成ができるはずです。

　では、やってみましょう。題材は、[「ケネディ大統領がアポロ計画の支援を表明した演説(和訳)」](https://ja.wikipedia.org/wiki/アポロ計画)です。
![スクリーンショット 2020-08-06 13.28.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/b993260c-c87e-22d1-873e-812affc56f58.png)

```python
# 形態素解析
text = "我々が10年以内に月に行こうなどと決めたのは、それが容易だからではありません。むしろ困難だからです。この目標が、我々のもつ行動力や技術の最善といえるものを集結しそれがどれほどのものかを知るのに役立つこととなるからです。その挑戦こそ、我々が受けて立つことを望み、先延ばしすることを望まないものだからです。そして、これこそが、我々が勝ち取ろうと志すものであり、我々以外にとってもそうだからです。"
result = jumanpp.analysis(text)  # 分かち書き
tokenized_text = [mrph.midasi for mrph in result.mrph_list()]  # 単語リストに変換
tokenized_text, mask_index = preparation(tokenized_text)  # [CLS],[SEP]の追加
tokens = bert_tokenizer.convert_tokens_to_ids(tokenized_text)  # IDリストに変換
tokens_tensor = torch.tensor([tokens])  # IDテンソルに変換
```
　先程同様、テキストを単語リストにし、定義した関数を使って[CLS],[SEP]を追加したら、IDリストに変換し、Pytorchが読めるIDテンソルに変換します。

　何度も単語予測を行うので、単語を1つ予測する関数を定義します。　

```python
# 1単語予測関数
def predict_one(tokens_tensor, mask_index):

    model.eval()    
    tokens_tensor = tokens_tensor.to('cuda')
    model.to('cuda')
 
    with torch.no_grad():
      outputs = model(tokens_tensor)
      predictions = outputs[0]
 
      _, predicted_indexes = torch.topk(predictions[0, mask_index], k=5)
      predicted_tokens = bert_tokenizer.convert_ids_to_tokens(predicted_indexes.tolist())
    return predicted_tokens, predicted_indexes.tolist()
```
　[MASK]を掛けた単語を予測し、予測した単語とIDを返す関数です。

　そして、文生成をするコードを書きます。

```python
# 文生成
for i in range(1,len(tokens_tensor[0])):
    tmp = torch.tensor(tokens_tensor)  # tokens_tensorをtmpにコピー
    tmp[0, i]=4  # i番目を[mask]に書き換え
    predicted_tokens, predicted_indexes =predict_one(tmp, i)  # [mask]を予測
    if predicted_indexes !=1:  # 予測が[UNK]でなければ
      tokens_tensor[0, i] = predicted_indexes[0]  # 予測IDの[0]番目でtokens_tensorのi番目を上書きする

target_list = tokens_tensor.tolist()[0]  
predict_list = bert_tokenizer.convert_ids_to_tokens(target_list)  
predict_sentence = ''.join(predict_list[1:])

print('------ original_text -------')
print(textwrap.fill(text,45))
print('------ predict_text -------')
print(textwrap.fill(predict_sentence,45))  
```
　tokens_tensorを一端tmpにコピーして、tmpに順次[MASK]を掛け予測した結果で、tokens_tensorの該当箇所を上書きする、ということを繰り返します。さて、これを実行すると、

![スクリーンショット 2020-08-06 15.37.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/fec56b94-9874-e89a-6591-91b0dd3e7435.png)

　オリジナルが「10年以内に月に行こう」と言っているのに、文生成は「１年以内に海外に行くべきだ」と、やたらこじんまりしてしまいました（笑）。文の中身は、ちょっと意味不明な感じです。事前学習だけだと、文生成はあまり上手く行かないようです。

　コード全体は Google Colab で作成し Github に上げてありますので、自分でやってみたい方は、この [**「リンク」**](https://github.com/cedro3/BERT/blob/master/BERT_pretrained_model.ipynb) をクリックし表示されたシートの先頭にある**「Colab on Web」**ボタンをクリックすると動かせます。

（参考）
・[BERT日本語モデルを使って、クリスマスプレゼントに欲しいものを推測してみた](https://www.cresco.co.jp/blog/entry/11517/)
・[ColabでJUMAN++を使う](https://www.mojirca.com/2019/08/colab-jumanpp.html)