NLP を学ぶ - 6

はじめに

今回はBERTについて正しく理解したい。
BERT1はTransformerを用いたは汎用的な文章の表現モデルである。
Fine-tuningにより様々なタスクでSOTAを達成した。
BERTに関する解説記事はすでに多く存在するため、実際に自分で触れて
気になった部分を中心に理解したい。
検証にはtransformers2のコードを利用した。
検証環境はGoogle Colaboratory。

BERTの前処理

BERTは事前学習モデルであり、fine-tuning時は学習時と同じvocabularyを利用する必要があるため、vocabularyの作り方も汎用性をもたせている。
参考 : https://github.com/huggingface/transformers/blob/master/src/transformers/tokenization_bert.py

語彙数の確認
from transformers.tokenization_auto import AutoTokenizer
# 小さめのbertモデルで大文字小文字を区別しないものを利用
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
vocab = tokenizer.vocab
print("語彙数", len(vocab))

語彙数は30522だが実際には使われていないものもある。

tokenizeの実行例
tokenizer.tokenize("I feeel fine today")

出力
['i', 'fee', '##el', 'fine', 'today']

feeelは誤字だが、BERTのtokenizerは無視するのではなく、 "fee" + "##el"に分解している。
これはvocabularyにない単語を取り扱うための処理であり、これによって比較的少ない語彙数で
多くの入力に対応できる。

単語の分割アルゴリズム(WordpieceTokenizer)
  1. 単語全体がvocabularyに含まれるかチェックし含まれるならば分割しない
  2. 単語の後ろから1文字ずつ削除していきvocabularyに含まれるかチャックしていく
  3. 2でvocabularyに含まれる文字列が見つかった場合、 部分文字列を保存し元単語からその部分を削除する。
  4. 先頭部を削除した文字列に対して1~3を繰り返す。単語の最初の部分文字列1つ以外は先頭に##をつけることで先頭部分文字列と区別する。

コード- WordPeiceTokenizerテスト

import pandas as pd
examples = ["gogle", "amzon", "facebok", "appple", "imissyou", "zzzzz"]
rows = [(exa, tokenizer.wordpiece_tokenizer.tokenize(exa)) for exa in examples]
mis_df = pd.DataFrame(rows, columns=["元単語", "tokenizer結果"]).set_index("元単語")
print(mis_df.to_markdown())

元単語 tokenizer結果
gogle ['go', '##gle']
amzon ['am', '##zon']
facebok ['face', '##bo', '##k']
appple ['app', '##ple']
imissyou ['im', '##iss', '##you']
zzzzz ['z', '##zz', '##zz']

vocabularyには1文字の先頭文字(a, b, c, ...)や1文字の先頭以外の文字(##a, ##b, ...)が含まれているため任意の未知文字が表現可能。 (ただし1単語あたりの最大文字数は存在)。
これらの誤字が最終的に元単語に近い表現として学習されているかどうか検証する必要がある。

BERTのEmbedding

BERTはTransformerを利用したモデルであり、そのままでは文章内の単語の位置情報がない。
また、BERTは汎用的な事前学習モデルを目指して設計されており、1つのモデルで文章分類などの単文入力タスクと文脈つき質疑応答などの2文入力タスクの両方に対応できる必要がある。
そのためBERTのembeddingは以下の3つから構成される。

  1. Token Embedding
  2. Segment Embedding
  3. Positional Embedding

Token Embedding

基本的には前処理されたwordを単語単位でembeddingするが以下の処理を加える。

  1. 最初の文章の先頭には[CLS]トークンを付与
  2. 2文入力の場合は2つの文章の間に[SEP]トークンを入れて1つの文章として入力する。

Segment Embedding

Segment Embeddingは二文入力に対応するために各単語がどちらの文章に所属するかを示すために存在する。
0, 1の値のみ許容され、タスクによって設定される。

Positional Embedding

単語の位置を表すEmbedding。
BERTは最大で512(先頭、末尾のトークン含む)までの入力を許容しているため0~511。

前処理例

sentiment analysis
nlp = pipeline("sentiment-analysis", model="bert-base-uncased")
nlp._parse_and_tokenize(["I know you"])

{'attention_mask': tensor(1, 1, 1, 1, 1),
'input_ids': tensor( 101, 1045, 2113, 2017, 102),
'token_type_ids': tensor(0, 0, 0, 0, 0)}

input_ids が Token Embeddingに対する入力。
token_type_ids が Segment Embeddingへの入力に対応する。
Positional Embeddingは指定しない場合はモデル内で自動で割り振られるため
前処理に現れていない。

question answering
# 参考 https://huggingface.co/transformers/usage.html#extractive-question-answering
from transformers import pipeline
from transformers.data.processors.squad import squad_convert_examples_to_features

nlp = pipeline("question-answering", model="bert-base-uncased")
context = r"""
Extractive Question Answering is the task of extracting an answer from a text given a question. An example of a
question answering dataset is the SQuAD dataset, which is entirely based on that task. If you would like to fine-tune
a model on a SQuAD task, you may leverage the `run_squad.py`.
"""
question = "What is a good example of a question answering dataset?"

examples = nlp._args_parser(question=question, context=context)

max_seq_len = 384
doc_stride = 128
max_question_len = 64
features_list = [
            squad_convert_examples_to_features(
                [example],
                nlp.tokenizer,
                max_seq_len,
                doc_stride,
                max_question_len,
                False,
            )
            for example in examples
        ]
print(features_list[0][0].tokens)

['[CLS]', 'what', 'is', 'a', 'good', 'example', 'of', 'a', 'question', 'answering', 'data', '##set', '?', '[SEP]', 'extract', '##ive', 'question', 'answering', 'is', 'the', 'task', 'of', 'extract', '##ing', 'an', 'answer', 'from', 'a', 'text', 'given', 'a', 'question', '.', 'an', 'example', 'of', 'a', 'question', 'answering', 'data', '##set', 'is', 'the', 'squad', 'data', '##set', ',', 'which', 'is', 'entirely', 'based', 'on', 'that', 'task', '.', 'if', 'you', 'would', 'like', 'to', 'fine', '-', 'tune', 'a', 'model', 'on', 'a', 'squad', 'task', ',', 'you', 'may', 'leverage', 'the', '', 'run', '_', 'squad', '.', 'p', '##y', '', '.', '[SEP]']

文脈付きQ-Aの場合は質問文を一文目として文脈文を2文目としている。

print(features_list[0][0].token_type_ids)

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Segment Embddingについては第一文の部分は0, 第二文の部分は1、その後の部分は0となっている。

文脈付きQ-Aの場合はこの入力でモデルからcontext文章内で解答に対応する部分の両端を予測する。

BERTのTransformer

BERTの特徴量抽出部は以下の構成要素から成る。

  • Embedding層
    • word embeddings
    • position embeddings
    • token type_embeddings (segment embedding)
    • LayerNorm
    • dropout
  • Encoder層 (Transformer層 x 12)
    • Transformer層
      • BertAttention
        • BertSelfAttention
        • BertSelfOutput
      • BertIntermediate
      • BertOutput

Embedding層

Embedding層はBERTの最初の層であり、入力をEmbeddingする。
word embedding, position embedding, segment embeddingの出力を足しあげて
layer normalizationとdropout層に通す。
この層を通す間は単語あたりの特徴量数は変わらない("bert-base-uncased"の場合768次元)。

Encoder層

Encoder層はEmbeddingで整形した入力から12層のTransformerを通すことで特徴量を抽出する層である。 1つのTransformerはBertAttention, BertIntermediate, BertOutputの3パートに分かれている。

BertAttention

BertAttentionはBertSelfAttentionとBertSelfOutputからなる。 BertSelfAttentionはself attentionによる特徴量の抽出を行い、BertSelfOutputはBertSelfAttentionの出力結果と その全結合層とdropout層を通した出力を足してlayer normalization層に通している(residual connection)。

BertIntermediate

BertIntermediateは全結合層1層と活性化関数(gelu)のみからなる層でありBertAttentionの結果を入力として そのまま全結合層と活性化関数に通している。

BertOutput

BertOutputはBertAttentionとBertIntermediateそれぞれの出力を入力としている。
BertIntermediate側の出力をさらに全結合層に通してdropoutをかけた後に BertOutputの出力と足しあげてlayer normalizationに通している。 この出力とBertAttentionで求めたattention情報がTransformer部の出力となる。

BERTのテスト

transformer(pytorch-transformers)フレームワークを用いてBERTの特徴量解析を行ってみる。

以下の4つの文をBERTの学習済み特徴量抽出文に通して、単語単位で特徴量空間をPCAしてみる。

番号 文章
0 I love you
1 I hate you
2 I am you
3 I know you

コード - 特徴量空間解析

import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt


nlp = pipeline("feature-extraction", model="bert-base-uncased")
sentences = [
    "I love you",
    "I hate you", 
    "I am you",
    "I know you", 
]

vecs = nlp(sentences)
vec = np.array(vecs)[:, 1:-1, :]


pca = PCA(n_components=2)
results = pca.fit_transform(vec.reshape([-1, 768]))


strs = []
for s, sent in enumerate(sentences):
#  strs.extend(["sos"] + [st + "-{}".format(s) for st in sent.split()] + ["eos"])
  strs.extend([st + " - {}".format(s) for st in sent.split()])
plt.figure(figsize=(14, 10))
plt.plot(results[:, 0], results[:, 1], ".")
for s, (x, y) in zip(strs, results):
  plt.annotate(s, (x,y), fontsize=15)
plt.title("PCA of BERT word embedding", fontsize=20)

f:id:nakamrnk:20200407143056j:plain

各単語の横の数字は文章番号である。
これを見るとI, you, と各文の動詞(love, hate, am know)がそれぞれクラスタを形成しているように見える。
一方で全く同じ単語であるI, youも文章によって特徴量空間上での位置が少しずつずれており, BERTが文脈も含めて単語をembeddingしていることが分かる。
また、感情に関係する0, 1の文章を構成する単語がペアになって存在しているように見えるため学習した何らかの特徴を反映している可能性がある。

まとめと今後

今回はtransformersライブラリを用いて学習済みのBERTモデルに触れてみた。
BERTの前処理部分についてはこれまであまり知らなかったため実際の処理を確認できてよかった。
また、transformersライブラリは学習済みのモデルを利用する上では非常に使いやすかった。
今後はBERTの派生を調査するかBERTを何かしらの問題に適用できないかの検討を行いたい。

参考文献