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

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

BERTを用いて含意関係認識をやってみる

f:id:nogawanogawa:20200808170852p:plain

この前は、BERTを使って文章の空欄を埋めるタスクをやってみました。 今回はBERTの勉強がてら含意関係認識(Recognizing Textual Entailment, RTE)というタスクをやってみたいと思います。

今回非常に参考にさせていただいたのはこちらの記事です。

hironsan.hatenablog.com

上の記事とほとんど同じことをやるわけですが、自分の勉強のためやっていきます。

含意関係認識 is なに?

まずは含意関係認識(Recognizing Textual Entailment, RTE)とはなんぞや?というところから見ていきます。

今2つの文が与えられたとします。 一方の文の内容が正しいと仮定したとき、他方の文の内容が正しいと言えるかどうかを判定するタスクが含意関係認識です。

このように含意関係認識では登場する単語だけではなく、文の意味まで適切に把握することが求められます。

やってみる

さて、この問題についてBERTを使って解いてみたいと思います。

BERT学習済みモデル

BERTを使うメリットの一つに学習済みモデルが多数公開されている点が挙げられます。 0から自分で学習するのではなく、学習済みモデルから転移学習して自分が使いたい用途に合わせてモデルを調整するといった使い方が可能になります。

こちらもこの前も紹介したように、日本語のBERT学習モデルは数多く公開されています。

www.nogawanogawa.com

データセット

今回も京都大学の黒橋・河原研究室で公開しているTextual Entailment評価データを使用します。

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

判定は、下記のように二値分類として使っていきたいと思います。

  • ◎ -> ○
  • ○ -> ○
  • △ -> ✗
  • ✗ -> ✗

実装

さて、実際に含意関係認識をやっていきたいと思います。 含意関係認識と言っても、含意・矛盾の二値分類です。 ということで、BERTを使用した文書ベクトルを二値分類していきます。

データの読み込み

まずはデータの読み込みです。

df = pd.read_csv("data/entail_evaluation_set.txt", sep=" ", index_col=0, header=None, names=["id", "cat", "label", "t1", "t2"])
mapping = {
        '×': '×',
        '△': '×',
        '○': '○',
        '◎': '○'
    }
df.label = df.label.map(mapping)

前処理

前処理と言っても、シンプルにBERTに突っ込むための形態素解析をするだけです。

まずは上で○✗に分けた正解ラベルを0と1にエンコーディングします。

# ラベルエンコーディング(LabelEncoder)
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
labels = le.fit_transform(labels)

文書分類だと単文をBERTに突っ込むわけですが、含意関係認識だと2文突っ込まないといけないので、[SEP][CLS]で文の間を区切っています。 これは、どうやらBERTの形態素解析する際にこれらの記号が挿入されているようで、これらを付け加えて2文まとめてBERTに突っ込んでいるだけです。

前もってBERTに突っ込む際に最大単語数を指定してあげるので、そのための単語のカウントをしてあげます。

# 最大単語数の確認
max_len = []

# 1文づつ処理
for sent1, sent2 in zip(t1, t2):
    token_words_1 = tokenizer.tokenize(sent1)
    token_words_2 = tokenizer.tokenize(sent2)
    token_words_1.extend(token_words_2)
    # 文章数を取得してリストへ格納
    max_len.append(len(token_words_1))
    
max_length = max(max_len) +4 # 最大単語数にSpecial token([CLS], [SEP])の+2をした値が最大単語数

# 最大の値を確認
print('最大単語数: ', max_length)

次に2文をセットにしてtorch.tensorに変換します。

input_ids = []
attention_masks = []

# 1文づつ処理
for x , y in zip(t1, t2):
    sent= x  + "[SEP]" + "[CLS]"+ y

    encoded_dict = tokenizer.encode_plus(
                        sent,                      
                        add_special_tokens = True, # Special Tokenの追加
                        max_length = max_length,           # 文章の長さを固定(Padding/Trancatinating)
                        pad_to_max_length = True,# PADDINGで埋める
                        return_attention_mask = True,   # Attention maksの作成
                        return_tensors = 'pt',     #  Pytorch tensorsで返す
                   )


    # 単語IDを取得     
    input_ids.append(encoded_dict['input_ids'])

    # Attention maskの取得
    attention_masks.append(encoded_dict['attention_mask'])

# リストに入ったtensorを縦方向(dim=0)へ結合
input_ids = torch.cat(input_ids, dim=0)
attention_masks = torch.cat(attention_masks, dim=0)

# tenosor型に変換
labels = torch.tensor(labels)

# 確認
print('Original: ', t1[0])
print('Token IDs:', input_ids[0]) 

あとはデータローダーに突っ込んで学習するだけです。 学習にはhuggingfaceから出されている日本語学習済みモデルを使用してその上から更にコーパスに対して学習するようにしています。

結果

一応、10%をテスト用に切り出して評価してみた結果、accuracyが70%という結果になりました。

              precision    recall  f1-score   support

           0     0.3780    0.5741    0.4559        54
           1     0.8614    0.7371    0.7944       194

    accuracy                         0.7016       248
   macro avg     0.6197    0.6556    0.6252       248
weighted avg     0.7562    0.7016    0.7207       248

今回のデータセットで含意関係認識をやる場合には、この辺がベースラインになるんでしょうね。

あとは、実行時間が手元のMacbookで5時間近くかかったので、CPUで実行する際にはご注意を。

使ったコード

今回使った残骸はこちら。

参考にさせていた記事

この記事を書くにあたり、下記の記事を非常に参考にさせていただきました。

hironsan.hatenablog.com

qiita.com

qiita.com

感想

最近はテキスト生成をやっていましたが、今回は逆にテキストの内容を認識するタスクをやってみました。 まだまだBERTの仕組みについては全然わかってないので、その辺りはいつかちゃんと落ち着いて論文読んで勉強したいものです。(やるとは言ってない)