概要
BERTを使うとき,2つの文を入力したいときありますよね.別々のデータではなく,文のペアを一つ入力するということです.
質問応答や含意関係認識(NLI)等のタスクでやると思います.
そんなときに,データの前処理がめんどくさかったりします.それをより簡単にやっちゃいましょう.
「そんなの知っとるわ」と思われる方法かもしれませんが,ご了承ください.
(BERTに関する簡単な解説は付け加えていますが,より詳しく知りたい方は自分で調べてください.)
BERTに文のペアを入力する
本題の前に少し.
BERTはその事前学習において Next Sentence Predictoin(NSP) というタスクを行います.
日本語でいうと次文予測です.文字通り文Aに続く文Bを予測するというものです.
その名残(?)なのかは分からないですけど,BertTokenizerで文をエンコードすると,input_idsなどの他にtoken_type_idsと呼ばれる配列が得られます.
これは,2つの文を判別するためにトークンごとに0と1で表したものです.
'token_type_ids': [0, 0, 0, 1, 1, 1]
この場合,トークンの3つ目までか前半,それ以降が後半の文みたいな感じです .
これを使えば,BERTに文のペアを入力させられるわけです.
条件
Tokenizer = BertJapaneseTokenizer
model = cl-tohoku/bert-base-japanese-whole-word-masking
よくやるやつ
上で書いた実際のコードがこんな感じ
tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
text = "私の名前は太郎です。 [SEP] 三時のおやつの時間です。"
inputs = tokenizer(text)
文の間にある[SEP]は,特殊トークンと呼ばれるものの一つで,文字通りSeparation 文の区切りを表すためのものです.
これにより,token_type_idsがうまく生成できるはずです.
そしてエンコードして出てくる結果がこれ
{
'input_ids': [2, 1325, 5, 1381, 9, 5250, 2992, 8, 3, 240, 72, 5, 73, 18874, 5, 640, 2992, 8, 3],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
あれ?
[SEP]を挿入したにも関わらず,token_type_idsがなぜか全部0です.
これではモデルが文のペアであることを認識できません.
どうやら自分で[SEP]トークンを自分で入れ込んだとしても,tokenizer側はそれを認識はするものの,token_type_idsに反映することはしてくれないそうです.
そんなわけで,こういうことをやるわけです
token_type_ids = [0 if i <= input_ids.index(3) else 1 for i in range(len(input_ids))]
#[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
これは何をしているかというと,[SEP]の場所より前を0,あとを1になるような長さがtoken数のリストを作っています.
要するに,token_type_idsを自作しています.
なんでこんな面倒なことをしなければならないのでしょう?
そもそもtoken_type_idsがちゃんとできるように[SEP]をわざわざ自分で挿入しているわけです.
なのに,encodeしても肝心のtoken_type_idsが全部0になるのはおかしいですよね.
これで万事解決
解決策は以下のとおりです
text1, text2 = "私の名前は太郎です", "三時のおやつの時間です。"
inputs = tokenizer(text1, text2)
実はBertTokenizerは入力文のペアを別々で与えられます.
[SEP]の挿入も必要ありません
そうすると,
{
'input_ids': [2, 1325, 5, 1381, 9, 5250, 2992, 3, 240, 72, 5, 73, 18874, 5, 640, 2992, 8, 3],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
今度はしっかりtoken_type_idsが出力されています.
中身を確認してみます
tokenizer.convert_ids_to_tokens(inputs['input_ids'])
['[CLS]',
'私',
'の',
'名前',
'は',
'太郎',
'です',
'[SEP]',
'三',
'時',
'の',
'お',
'やつ',
'の',
'時間',
'です',
'。',
'[SEP]']
ちゃんと[SEP]が文の間に入っていることも確認できます.
これのおかげで,わざわざ自分で[SEP]を間に入れたり,token_type_idsを自作する必要もなくなりました.
かなり楽になりましたね.
文のペアそれぞれを,別々の変数で与えているので間違いも防げそうです.
おわりに
そもそもこれが正しい方法なんでしょうね.
Huggingfaceにあるドキュメントを見てみると,
ちゃんと例として示されてました.
これはしっかり目を通してなかった私が悪いですね.(BertTokenizerの説明にもペアを与えられることを書いていてほしいものですが)
ちなみに,encode()やbatch_encode()では確認していません.
これが初投稿です.見にくい記事ですいません.
参考