BERTを用いて文章の文法的な正しさをスコア化する

こんにちにゃんです。
水色桜（みずいろさくら）です。
本記事では、BERTを用いて文章の文法的な正しさをスコアリングする方法について書いていこうと思います。
本記事は

https://aclanthology.org/2020.acl-main.240/

を元として、書いています。（この論文は英語に関して実装していますが、本記事では日本語に関して実装していきます。）
（上記の論文に関して日本語で解説している記事↓）

https://zenn.dev/hellorusk/articles/f9e6c503dc54e2

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/b8ee0035-5201-ad98-75d7-37b1aeb5a091.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/b8716b4e-0c69-d60e-a6ec-6000d9b8ba3d.png)

BERTのようなMLMモデルでは従来のAutoregressive Modelsは適用することができません。論文で解説された擬似対数尤度スコアを用いて本記事では文法的な正しさをスコア化します。論文では、擬似対数尤度スコアによる MLM Scoring が、GPT-2 のような Autoregressive Models での言語モデルのスコアと同程度、あるいはそれよりも高い確率で、言語学的に正しい文章を判断できることを実験で示していました。

# 実装

では実際にこの論文のスキームに沿って実装していきます。実装したソースコードの全体は次の通りです。

```python:BERT_sentence_accuracy.py
from transformers import BertJapaneseTokenizer, BertForMaskedLM, pipeline, BertConfig
import torch
import tkinter as tk 
import re
import numpy as np

# tkinterを用いてGUIを作成
root = tk.Tk() # ウィンドウを作成
root.title(u'BERTによる文章の正しさ判定プログラム') # タイトルを定義
root.geometry('550x470') # ウィンドウサイズを定義
frame=tk.Frame(root,bg='old lace') # フレーム（テキストボックスなどを載せるためのもの）を定義
frame.pack() # フレームの設置
sc=tk.Scrollbar(frame) # スクロールバーの定義
sc.pack(side='right',fill='y') # スクロールバーの設置
msgs=tk.Listbox(frame,width=115,height=24,x=0,y=0,yscrollcommand=sc.set,bg='azure') # テキストボックスの定義
msgs.pack(side='left',fill='both',pady=20) # テキストボックスの設置

# 例文
input1='昨日をサッカーがした。'
input2='昨日はサッカーをした。'

pretrained = 'cl-tohoku/bert-base-japanese-whole-word-masking'#事前学習済みモデルの選択
tokenizer = BertJapaneseTokenizer.from_pretrained(pretrained) #分析器の事前学習モデルの選択
model = BertForMaskedLM.from_pretrained(pretrained) #事前学習モデルのロード
config=BertConfig.from_pretrained(pretrained) # 事前学習済みモデルの設定
MLM=pipeline('fill-mask',model=model,tokenizer=tokenizer,config=config) # pepeline関数でモデルを利用できるようにする

# ボタンが押されたらこの関数を実行
def bert():
    score1=0
    tokenized_text1=tokenizer.tokenize(input1) # 文章を形態素解析
    for i in range(len(tokenized_text1)):
        provisional=tokenized_text1[i] # マスクする部分を一時的に保持する変数
        tokenized_text1[i]='[MASK]' # 一単語をマスクする
        text=''.join(tokenized_text1) # リストになっているので、連結して一つの文にする
        score=re.findall(r'\d+\.\d+',str(MLM(text)[0])) # 解析結果の中から小数を抜き出す
        score1=+np.log(float(score[0])/len(tokenized_text1)) #生起確率を足し合わせる
        tokenized_text1[i]=provisional # マスクした単語を元に戻す
    
    # 文章と文法的な正しさの度合を表示
    msgs.insert('end',input1)
    msgs.insert('end',str(score1))

    score2=0
    tokenized_text2=tokenizer.tokenize(input2)
    for i in range(len(tokenized_text2)):
        provisional=tokenized_text2[i]
        tokenized_text2[i]='[MASK]'
        text=''.join(tokenized_text2)
        score=re.findall(r'\d+\.\d+',str(MLM(text)[0]))
        score2=+np.log(float(score[0])/len(tokenized_text2))
        tokenized_text2[i]=provisional

    msgs.insert('end',input2)
    msgs.insert('end',str(score2))

#ボタンの定義
btn=tk.Button(root,text='送信',font=('utf-8_sig',10),bg='cyan',command=bert)
btn.place(x=480,y=435)
# 画面の保持
root.mainloop()
```

まず、必要なモジュールをインポートします。

```python:import.py
from transformers import BertJapaneseTokenizer, BertForMaskedLM, pipeline, BertConfig
import torch
import tkinter as tk 
import re
import numpy as np
```

次にtkiterを用いて、GUIを作製していきます。

```python:GUI.py
# tkinterを用いてGUIを作成
root = tk.Tk() # ウィンドウを作成
root.title(u'BERTによる文章の正しさ判定プログラム') # タイトルを定義
root.geometry('550x470') # ウィンドウサイズを定義
frame=tk.Frame(root,bg='old lace') # フレーム（テキストボックスなどを載せるためのもの）を定義
frame.pack() # フレームの設置
sc=tk.Scrollbar(frame) # スクロールバーの定義
sc.pack(side='right',fill='y') # スクロールバーの設置
msgs=tk.Listbox(frame,width=115,height=24,x=0,y=0,yscrollcommand=sc.set,bg='azure') # テキストボックスの定義
msgs.pack(side='left',fill='both',pady=20) # テキストボックスの設置
```

各単語をMASKして、予測したときの条件付確率の対数尤度の和を求めていきます。

```python:calc.py
# 例文
input1='昨日をサッカーがした。'
input2='昨日はサッカーをした。'

pretrained = 'cl-tohoku/bert-base-japanese-whole-word-masking'#事前学習済みモデルの選択
tokenizer = BertJapaneseTokenizer.from_pretrained(pretrained) #分析器の事前学習モデルの選択
model = BertForMaskedLM.from_pretrained(pretrained) #事前学習モデルのロード
config=BertConfig.from_pretrained(pretrained) # 事前学習済みモデルの設定
MLM=pipeline('fill-mask',model=model,tokenizer=tokenizer,config=config) # pepeline関数でモデルを利用できるようにする

# ボタンが押されたらこの関数を実行
def bert():
    score1=0
    tokenized_text1=tokenizer.tokenize(input1) # 文章を形態素解析
    for i in range(len(tokenized_text1)):
        provisional=tokenized_text1[i] # マスクする部分を一時的に保持する変数
        tokenized_text1[i]='[MASK]' # 一単語をマスクする
        text=''.join(tokenized_text1) # リストになっているので、連結して一つの文にする
        score=re.findall(r'\d+\.\d+',str(MLM(text)[0])) # 解析結果の中から小数を抜き出す
        score1=+np.log(float(score[0])/len(tokenized_text1)) #生起確率を足し合わせる
        tokenized_text1[i]=provisional # マスクした単語を元に戻す
    
    # 文章と文法的な正しさの度合を表示
    msgs.insert('end',input1)
    msgs.insert('end',str(score1))

    score2=0
    tokenized_text2=tokenizer.tokenize(input2)
    for i in range(len(tokenized_text2)):
        provisional=tokenized_text2[i]
        tokenized_text2[i]='[MASK]'
        text=''.join(tokenized_text2)
        score=re.findall(r'\d+\.\d+',str(MLM(text)[0]))
        score2=+np.log(float(score[0])/len(tokenized_text2))
        tokenized_text2[i]=provisional

    msgs.insert('end',input2)
    msgs.insert('end',str(score2))
```

以上のコード実行すると、下記のような結果になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/c57ec633-8471-f6a8-261d-33b7c9c4d743.png)

正しい文法の時のほうが、高い値になっていることがわかります。このように文法的な正しさをスコア化することができます。ただし、全く文法構造および文章長のことなる文章同士を比べることはできないため（もう少し工夫すれば可能ですが、このままだとできません）、助詞助動詞の正しさを判別するために応用できます。
とくに、GPT-２で文章を自動生成した際に助詞助動詞が間違っていることが多々あるため、その場合に正しさを測定することで文章の校正を行うことができます。

皆さんもぜひ試してみてください。
では、ばいにゃん～。