Re:ゼロから始めるML生活

どちらかといえばエミリア派です

BERTの学習済みモデルを使って穴埋め問題を解く

f:id:nogawanogawa:20200727195124p:plain

最近ではBERTやその派生が自然言語処理の多くのタスクでSOTAを更新していて、非常に話題になっています。 そんなBERTですが、日本語の学習済みモデルも非常に多く公開される様になっており、計算資源が乏しい私でも使用するだけなら不自由なく使えるようになっています。

ということで、今回はBERTの学習済みモデルを使用して、日本語文章の穴埋め問題を解いてみたいと思います。

日本語 pretrained model

BERTの日本語pretrained modelは現在では、公開されているものが多くあります。

nlp.ist.i.kyoto-u.ac.jp

www.nlp.ecei.tohoku.ac.jp

qiita.com

laboro.ai

alaginrc.nict.go.jp

yoheikikuta.github.io

などなど、いろいろございます。使用に際し、ライセンスに関してはそれぞれ個別にご確認いただきたく思います。

穴埋め問題

中学校・高校などで、文章の途中に空欄があって、空欄に入る文言を答えるという形式の問題が記憶にあるのではないでしょうか? 今回は、その穴埋め問題をBERTにやらせてみたいと思います。

準備

1.Huggingfaceのtransformersから使用できる学習済みモデルを使用

この際、必要なファイルを置きます。(下記のようにすること)

こちらを使用するにあたり、下記のようにtokenizerに関するエラーが出ましたので、その対応が必要になるかと思います。

Cannot Load bert-base-japanese tokenizer · Issue #4060 · huggingface/transformers · GitHub

─ models
   └- bert-base-japanese
      ├- vocab.txt
      ├- config.json
      └- pytorch_model.bin

2.Mecabをインストールする

適当にMecabもインストールします。

自分は下記のエラーが出たので、下のブログの記事の通りにやったらうまくいきました。

no such file or directory: /usr/local/etc/mecabrc

http://min117.hatenablog.com/entry/2020/07/11/145738

$ cp /etc/mecabrc /usr/local/etc/

空欄を作る

まずは、空欄のあるテキストを作っていきたいと思います。 こちらは簡単のため、GiNZAを使って固有表現をMASKしたいと思います。

例えば、量を表す表現については、QUANTITYとなっていますので、その部分をMASKしたいと思います。

最大全長は約13メートル、最大体重は約9トンと、現在まで報告されている獣脚類の中で史上最大級の体格を誇る種の一つに数えられており、古今東西を通じて最大最強と名高い肉食動物でもある。

こちらをGiNZAを使って検出するとこんな感じになりました。

import spacy
from spacy import displacy
nlp = spacy.load('ja_ginza')

def mask_quantity(s):
    doc = nlp(s) 
    
    result = []

    for sent in doc.sents:
        for token in sent:
            if token._.ne.endswith('QUANTITY'):
                result.append("[MASK]") 
            else:
                result.append(token.orth_) 

    text = ""
    for token in result:
        text = text + token

    return text

最大全長は[MASK][MASK][MASK]、最大体重は[MASK][MASK][MASK]と、現在まで報告されている獣脚類の中で史上最大級の体格を誇る種の[MASK][MASK]に数えられており、古今東西を通じて最大最強と名高い肉食動物でもある。

空欄を予想する

次に作成した空欄をBERTを使って予想してみます。

import torch
from transformers import BertJapaneseTokenizer, BertForMaskedLM

def word_predict(text):
    
    pretrained_model_name = "cl-tohoku/bert-base-japanese-whole-word-masking"

    tokenizer = BertJapaneseTokenizer.from_pretrained(pretrained_model_name)
    tokenized_text = tokenizer.tokenize(text)

    if "[MASK]" in tokenized_text:
        masked_indices = [i for i, x in enumerate(tokenized_text) if x == '[MASK]']

    indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)

    tokens_tensor = torch.tensor([indexed_tokens])

    model = BertForMaskedLM.from_pretrained(pretrained_model_name)
    model.eval()

    # Predict
    with torch.no_grad():
        outputs = model(tokens_tensor)
        for masked_index in masked_indices:
            predictions = outputs[0][0, masked_index].topk(1) # 予測結果を1件取得

            for i, index_t in enumerate(predictions.indices):
                index = index_t.item()
                token = tokenizer.convert_ids_to_tokens([index])[0]
                tokenized_text[masked_index] = token
    
    text = ""
    for token in tokenized_text:
        if token.startswith("##"):
            text = text + token.strip("#")
        else :
            text = text + token

    return(text)

こちらを実行するとこんな感じ。

最大全長は約mm、最大体重は約0kgと、現在まで報告されている獣脚類の中で史上最大級の体格を誇る種の1つに数えられており、古今東西を通じて最大最強と名高い肉食動物でもある。

なんとなく、頑張ってるのはわからなくないけど、あんまりうまくいってないですね。

空欄の回答を評価する

最後に、回答に対して評価をしてみます。 こちらには、この前使ったBERTScoreを使ってみたいと思います。

www.nogawanogawa.com

要するに、元になった文と、空欄を補完した文とを比較してどれだけ意味的な乖離があるかを評価してみたいと思います。

done in 0.20 seconds, 5.07 sentences/sec
P:0.976208, R:0.981136, F1:0.978666

この値が良いのかどうかについては、色々見方ができると思いますが、空欄の推論結果を踏まえて文の意味を評価できることはなんとなくわかりました。

書いた残骸

作った残骸はこちら。

github.com

参考にさせていただいた記事

下記の記事を非常に参考にさせていただきました。

www.cresco.co.jp

kento1109.hatenablog.com

感想

今回はBERTを使って文書の穴埋め問題を解いてみました。 正直BERTの仕組みとかは全然わかってませんが、そんなんでも使えるというのはすごいですね。

BERTを含め、近年のNLP周りの技術については少しずつ追いついていきたいので、そのうちちゃんと勉強したら別で記事を書きたいと思います。 ひとまず、今回は"何ができるのか、雰囲気をつかむ"というレベルまでやってみたって感じでした。