BERTでTOEICの問題を解いてみる②

こんにちわ
AIチームの戸田です

前回に引き続き、学習済みのBERTのモデルを使ってTOEICの問題を解いてみようと思います。
今回はPart6に挑戦しました。

TOEICのPart 6は長文穴埋め問題となっており、空白がある英語の長文に、文法・意味的に最も適した選択肢を選んで入れます。Part5のように一文だけを読んで答えられるものもありますが、前後の文脈から判断しなければならない問題も含まれており、Part5 より難易度が上がっています。また今回は選択肢が単語ではなくイディオムや文章だったりします。学習済みのBERTを使って解く際にこのあたりを工夫しなければいけませんでした。

今回も問題はIIBC公式のサンプル問題を使用させていただきます。

BERT学習済みモデルの読み込み

huggingfaceのtransformers を利用します。

import torch
from transformers import BertTokenizer, BertForPreTraining

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForPreTraining.from_pretrained('bert-base-uncased')

問題定義

textに問題の文章、candidate1~4に問題1~4の穴埋め候補をリストで定義します
問題文章の空欄部分は*とします。

text = """
To: Project Leads
From: James Pak
Subject: Training Courses

To all Pak Designs project leaders:
In the coming weeks, we will be organizing several training sessions for * employees.
At Pak Designs, we believe that with the proper help and support from our senior project leaders, less experienced staff can quickly * a deep understanding of the design process.
* , they can improve their ability to communicate effectively across divisions.
When employees at all experience levels interact, every employee's competency level rises and the business overall benefits.
For that reason, we are urging experienced project leaders to attend each one of the interactive seminars that will be held throughout the coming month.
* .

Thank you for your support.

James Pak
Pak Designs
"""

candidate1 = ["interest", "interests", "interested", "interesting"]
candidate2 = ["develop", "raise", "open", "complete"]
candidate3 = ["After all", "For", "Even so", "At the same time"]
candidate4 = ["Let me explain our plans for on-site staff training.",
              "We hope that you will strongly consider joining us.",
              "Today’s training session will be postponed until Monday.",
              "This is the first in a series of such lectures."] 

問題1,2はPart 5と同じ方法で解けそうですが、問題3,4は複数単語あるので何か別の方法を考える必要がありそうです。

回答方法

BERTは空白部分のtokenベクトルと候補の文章ベクトルをcos類似度で比較して、最も近いものを回答とすることにしました。

問題文章をTokenIDのリストに変換する

こちらはPart5の時と同様ですね。
ただし今回は空欄が4箇所あります。

tokens = tokenizer.tokenize(text)
tokens = ["[MASK]" if t == "*" else t for i, t in enumerate(tokens)]
tokens = ["[CLS]"] + tokens + ["[SEP]"]
ids = tokenizer.convert_tokens_to_ids(tokens)
ids = torch.tensor(ids).reshape(1,-1)
空欄部分のインデックスを取得

空欄部分のベクトルを取得するため、空欄tokenのインデックスを確保しておきます。

masked_indexs = [i for i, v in enumerate(ids[0]) if v == 103]  # [MASK]のIDは103
# -> [33, 60, 69, 131]
文章全体をベクトル化し、空欄部分のベクトルを取得

文章全体をベクトル化し、先程確保しておいたインデックスから空欄部分のベクトルを取得します。

with torch.no_grad():
    outputs, _ = model.bert(ids)
q1, q2, q3, q4 = outputs[0].numpy()[[i+1 for i in masked_indexs]]
空欄埋め候補の文章をベクトル化する

空欄埋め候補の4つの選択肢の文章をベクトル化します。
最終層の出力の平均をとっています。

# 文章ベクトルを取得する関数
def get_bert_vec(c):
    tokens = tokenizer.tokenize(c)
    ids = tokenizer.convert_tokens_to_ids(tokens)
    ids = torch.tensor(ids).reshape(1,-1)
    with torch.no_grad():
        outputs, _ = model.bert(ids)
    vec = outputs[0].numpy().mean(0)
    return vec
    
c_vecs = np.array([get_bert_vec(c) for c in candidate3])
各ベクトル同士のcos類似度を計算する

文章全体のベクトルと空欄埋め候補の各ベクトルのcos類似度を比較します。
最も近い候補を回答候補とします。

from sklearn.metrics.pairwise import cosine_similarity

candidate3[cosine_similarity([q3], c_vecs)[0].argsort()[-1]]
# ->'At the same time'

前後の文章は

  • 経験のないスタッフが早くデザイン手順について理解を深める事ができる
  • 彼らは効果的な部門をまたいだコミュニケーション能力を改善することができる

となっており、これらをつなぐのは「同時に」を意味する「At the same time」が適切で、見事正解です。

関数化

一連の処理を関数にまとめました
回答候補が単語のものはPart5の回答手法を利用します

def part6_slover(text, candidate, q):
    if max([len(tokenizer.tokenize(c)) for c in candidate]) == 1:
        return part5_slover(text, candidate)
    tokens = tokenizer.tokenize(text)
    tokens = ["[MASK]" if t == "*" else t for i, t in enumerate(tokens)]
    tokens = ["[CLS]"] + tokens + ["[SEP]"]
    
    ids = tokenizer.convert_tokens_to_ids(tokens)
    ids = torch.tensor(ids).reshape(1,-1)
    
    masked_indexs = [i for i, v in enumerate(ids[0]) if v == 103]
    
    with torch.no_grad():
        outputs, _ = model.bert(ids)
    mask_vecs = outputs[0].numpy()[[i+1 for i in masked_indexs]]
    
    c_vecs = np.array([get_bert_vec(c) for c in candidate])
    return candidate[cosine_similarity([mask_vecs[q-1]], c_vecs)[0].argsort()[-1]]

結果

図2.png (115.3 kB)

4問中3問正解でした
1問ミスですが、間違えた2問目はPart5のロジックを使った部分なので、長文中での空欄予測は1単語であってもまた異なった方法が必要なのかもしれません。

おわりに

本記事では事前学習済みのBERTのモデルを使って、TOEICのPart 6の問題を解いてみました。複数単語の問題は解けましたが、完全回答するにはやはり問題に特化したfine-tuningが必要そうです。

次回はいよいよ最難関と思われるPart 7です。
イメージとしてはSQuADに近いと思うので、SQuADの事前トレーニングモデルを使おうと思います。

最後まで読んでいただきありがとうございました

参考
https://www.iibc-global.org/toeic/test/lr/about/format/sample06.html
https://arxiv.org/abs/1810.04805
https://github.com/huggingface/transformers