(備忘録-python)自然言語処理超入門:(やっと)BERTの仕組みを学び・使う(英文)(準初心者向け)

自然言語処理・画像解析

この記事は、ちょっと前に話題になり、今でも最前線の自然言語処理タスクで用いられるBERTについて学びたいと思い、自分なりに解釈してまとめて記事です。

初めて聞いた人はざっくりと、気になってた人には何となく概要をつかんでいただけるような記事になっていると思います。

この記事は様々なネットの記事と、原論文

arxiv.org

を参考にしています。生暖かい目で見ていただけたら嬉しいです。

BERTを学ぶ

特徴や概要

BERT

=Bidirectional Encoder Representations from Transformers

=「Transformerによる双方向のエンコード表現」

  1. 名の通り、TransformerのEncoderを使ったモデル
  2. 様々な自然言語処理タスクをこなすことができる
    • 出力層を付け加えるだけで簡単にファインチューニングが可能。
    • 翻訳、文書分類、質問応答などが高精度で予測可能
  3. 事前学習として2つのタスクを学習することで精度が向上(MLM,NSP)
    • 文章を双方向(文頭と文末)から学習する、文章ごとの関係も学習することが可能
    • 「文脈を読むことが可能になった」ともよばれるくらい高性能

導入

自然言語処理タスクにおいて、精度向上には言語モデルによる事前学習が有効

事前学習には「教師なし単語表現の特徴量ベース」と「ファインチューニング」の2つの方法

その前に転移学習の話を少し…

転移学習

解きたいタスクとは別のタスクを事前に学習し、その結果として得られたパラメータを用いて、解きたかったタスクを解くこと

  • パラメータを学習の際に固定し、追加層を学習:「教師なし単語表現の特徴量ベース」
  • 全パラメータを微調整しつつ、追加層も学習:「ファインチューニング」
教師なし単語表現の特徴量ベース

どんなタスクにでも使える単語表現特徴量を獲得することを目的としているモデルで、タスクごとに構造は変える必要がある

  • 事前学習で得られた表現ベクトルを特徴量の1つとして用いるもの
  • タスクごとにアーキテクチャを定義

Ex)ELMo(2018)※(双方向だが一方向の結果を結合しただけであるため、浅い双方向らしいです…)

ファインチューニング
  • 事前学習によって得られたパラメータを重みの初期値として学習させるもの で、タスクごとでパラメータを変える必要があまりない
  • 最後に追加層を加えるだけでもよく、大きなモデル変更はなし

Ex)OpenAI GPT(2018)

ただし、いずれも…事前学習に用いる言語モデルの方向が1方向だけが多い。

文章の文頭から文末に向けてのみの方向にしか学習していない

前後の文脈が大事なもの:文章タスクやQ&Aには不向きかな

BERTでは…「ファインチューニングによる事前学習」に注力し、精度向上

  1. Masked Language Model(= MLM)
  2. Next Sentence Prediction(= NSP)

  1. MLM: 複数箇所が穴になっている文章のトークン(単語)予測→双方向から見れる(詳細は次節で)
  2. NSP: 2文が渡され、連続した文かどうか判定

この2つの事前学習を行うことで、単語に関して文章に関してのタスクに強いモデルが出来上がる。

  • 文章の関係性の学習と単語を見る際の双方向のモデルを作る.
  • ファインチューニングにも力を入れる

原理

自然言語処理は、単語を高次元のベクトルに置き換える分散表現という技術を用いて入力

※単語データの並びのことを「シーケンス」=文章

BERTは入力されたシーケンスから別のシーケンスを予測

BERTの学習には以下の2段階がある。

  1. 事前学習: ラベルなしデータを用いて、複数のタスクで事前学習を行う
  2. ファインチューニング: 事前学習の重みを初期値として、ラベルありデータでファインチューニングを行なう。

記号の意味

  • E[Embedding]:入力の埋め込み表現,
  • C[CLS]:トークンの隠れベクトル
  • Ti::文章のi番目のトークンの隠れベクトル
  • [CLS]はすべての入力文頭に追加される特別な記号
  • [SEP]は特別なセパレータトークン(例:質問と回答の区切り)、文の間にあるもの

論文の図を見てわかることだが

  1. 出力層を除いて、事前学習と微調整の両方で同じ構造を使用
  2. 異なるタスクにおいても構造が統一されている
  3. Fine-tuningの際には、すべてのパラメータが微調整される

入出力

タスクによりことなる:1つの文(Ex.分類系)、2つの文をつなげたもの(Ex.質疑応答)

文章の先頭に[CLS]トークン

※2文をくっつける時は、間に[SEP]トークンを入れ かつ それぞれに1文目か2文目かを表す埋め込み表現を加算

最終的に入力文は以下のようになる。

BERTの事前学習

文を両方向(「文頭から文末」および「文末から文頭」)で学ぶためにBERTでは2つの事前学習を行なう。それがMLMとNSPである。

TransformerがMLMとNSPを同時進行で行う

1つめのタスク:Masked Language Model(MLM)

  • 従来の手法に比べ精度が向上をMLMが実現を担っている
  • 複数箇所が穴になっている文章のトークン(単語)予測
  • 前と後ろの双方向を元に中央の単語を予測するタスク

従来の自然言語処理モデルでは、文章を単一方向からでしか処理できない

目的の単語の前の文章データ(単語群)予測していた

BERTは双方向のTransformerによって学習するため、対応可能

入力の15%のトークンを[Mask]トークンでマスクし、元のトークンを当てるタスク。
しかし、ファインチューニングでは出てこない[Mask]トークンを事前学習で使用しているため事前学習とファインチューニング間の差異が生じる

そのため、マスクするトークンを常に[Mask]トークンに置き換えるのではなく、マスクするトークンに対して
選択された15%のうち

  • 80%は[MASK]に置き換えるマスク変換
  • 10%をランダムな別の単語に変換
  • 10%(残り)はそのままの単語にしておく

このように置換された単語を周りの文脈から当てるタスクを解くことで、単語に対応する文脈情報を学習します。

2つ目のタスク:Next Sentence Prediction(NSP)

  • 2文が渡され、連続した文かどうか判定
  • NSPによって文章間の関係性をも考慮した、より広範的な自然言語処理モデルとして機能

MLMにおいて単語に関しての学習はできますが、文単位の学習はできていない
つまり、文章間の関係を考慮する自然言語処理タスクには対処できないモデルになってしまう…

2つの入力文に対して「その2文が隣り合っているか」を当てるよう学習

2つの文の関係性を学習するNSPを用いる

文の片方を50%の確率で他の文に置き換え、それらが隣り合っているかor否かを判別・学習する。

2文を[SEP]というトークンで分け、隣り合っているかor否かを分類するために[CLS]というトークンが用意

このように、BERTは単語だけでなく文全体の表現についても学習可能に

※上図のCを用いて予測を行なう。 (ここで、C=CLSはこの時点ではNSPに特化したものなので、文全体を表現したベクトルにはなっていないそう?。)

実際に用いる方法(ファインチューニング等で)

  • C=CLSは感情分析・テキスト分類等に用いられる
  • Tiはトークンレベルのタスク(Ex.Q&A) に使われる

他のファインチューニングの例は以下の図のようになる。

つまり、BERTモデルを既存のタスク処理モデルに接続し、転移学習・ファインチューニングするだけで使用可能である。

有名な

huggingface.co

というサイトを活用すると誰でもBERTを活用・試すことが可能である。

一応コード(自身のPCでは学習に時間がかかるため1部凍結)

データのロードと前処理まで

from bs4 import BeautifulSoup    # importする

from tensorflow.keras.datasets import imdb

# imdb = imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data()

#単語が整数にマッピングされた辞書を取得
word_index = imdb.get_word_index()

# 最初の要素を予約(単語を登録)
word_index = {k:(v+3) for k,v in word_index.items()} 
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # 不明な単語
word_index["<UNUSED>"] = 3

# 整数を単語にマッピングする辞書を作成
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])

def decode_review(text):
    return ' '.join([reverse_word_index.get(i, '?') for i in text])

import pandas as pd
train_df=pd.DataFrame(train_data)[0].map(decode_review).reset_index(drop=True)#データを軽くするため
test_df=pd.DataFrame(test_data)[0].map(decode_review).reset_index(drop=True)#データを軽くするため



#前処理
import re
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
#nltk.download('wordnet')
#nltk.download('omw-1.4')

from nltk.stem.porter import PorterStemmer 
stemmer = PorterStemmer()

import nltk
from nltk.corpus import stopwords
# nltk.download('stopwords')

def clean_text(x):
    #ノイズ除去
    soup = BeautifulSoup(x, 'html.parser')
    text= soup.get_text()
    
    #アルファベット以外をスペースに置き換え
    text_ = re.sub(r'[^a-zA-Z]', ' ', text)
    
    #単語長が短いものものは削除(中身による)+その後の処理のために分割
    text_ = [word for word in text_.split()]# if len(word) > 2]
    
    #形態素=>動詞
    text_ = [lemmatizer.lemmatize(word.lower(), pos="v") for word in text_]
    
    #ステミング
#     text_ = [stemmer.stem(word) for word in text_]
    
    #stopword除去
#     A = [word for word in text_ if word not in stopwords.words('english')]
    
    #単語同士をスペースでつなぎ, 文章に戻す
    #その後の処理で戻す必要ない場合はコメントアウト
#     clean_text = ' '.join(A)
    clean_text = ' '.join(text_)
    return clean_text



#データ数減らして処理を軽くしたい...
clean_text_df=train_df[::5].map(clean_text)
clean_text_df

clean_text_test_df=test_df[::10].map(clean_text)
clean_text_test_df


train_texts=clean_text_df.reset_index(drop=True)
train_label=train_labels[::5]

test_texts=clean_text_test_df.reset_index(drop=True)
test_label=test_labels[::10]

np.shape(train_texts),np.shape(train_label),np.shape(test_texts),np.shape(test_label)

tokenizer(BERT用)

BERTのモデルに入れられるようにデータを成形する。

import tqdm as notebook_tqdm

#from transformers import AutoTokenizer,glue_convert_examples_to_features,BertTokenizer,DistilBertTokenizer
#from tensorflow.keras.preprocessing.text import Tokenizer
#from tensorflow.keras.preprocessing.sequence import pad_sequences

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")


# テキストのリストをtransformers用の入力データに変換(BERTの、他のBERTシリーズでは使えない可能性あり)

def to_features(texts, max_length):
    shape = (len(texts), max_length)
    # input_idsやattention_mask, token_type_idsの説明はglossaryに記載(cf. https://huggingface.co/transformers/glossary.html)
    input_ids = np.zeros(shape, dtype="int32")
    attention_mask = np.zeros(shape, dtype="int32")
    token_type_ids = np.zeros(shape, dtype="int32")
    for i, text in enumerate(texts):
        encoded_dict = tokenizer.encode_plus(text, max_length=max_length, pad_to_max_length=True,truncation=True)
        input_ids[i] = encoded_dict["input_ids"]
        attention_mask[i] = encoded_dict["attention_mask"]
        token_type_ids[i] = encoded_dict["token_type_ids"]
    return [tf.cast(input_ids, tf.int32), tf.cast(attention_mask, tf.int32), tf.cast(token_type_ids, tf.int32)]


max_length=500

x_train = to_features(train_texts, max_length)
# y_train = tf.keras.utils.to_categorical(train_labels, num_classes=4)
y_train = tf.cast(train_label, tf.int32)

x_valid = to_features(test_texts, max_length)
# y_valid = tf.keras.utils.to_categorical(valid_labels, num_classes=4)
y_valid = tf.cast(test_label, tf.int32)

BERT+分類器の学習

from transformers import TFBertModel
bert = TFBertModel.from_pretrained('bert-base-uncased')

# 層をfreeze(学習させないように)する
# bert.trainable= not True

from tensorflow import keras
from tensorflow.keras import optimizers, losses, metrics
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

def make_model(bert, num_classes, max_length, bert_frozen=True):
    # bertモデルはリストになっているので、取り出す

    # 層をfreeze(学習させないように)する、消せばfine-tune
    #bert.layers[0].trainable = not bert_frozen

    # input
    input_ids = Input(shape=(max_length, ), dtype='int32', name='input_ids')
    attention_mask = Input(shape=(max_length, ), dtype='int32', name='attention_mask')
    token_type_ids = Input(shape=(max_length, ), dtype='int32', name='token_type_ids')
    inputs = [input_ids, attention_mask,token_type_ids]

    # bert
    x = bert.layers[0](inputs)
    # x: sequence_output, pooled_output
    # 2種類の出力がある。

    # TFBertForSequenceClassificationにならってpooled_outputのみ使用
    out = x[1]

    # fc layer(add layers for transfer learning)
    #out = Dropout(0.25)(out)
    out = Dense(128, activation='relu')(out)
    out = Dropout(0.4)(out)
    out = Dense(num_classes, activation='softmax')(out)
    return Model(inputs=inputs, outputs=out)



seed_everything(42)
adam=optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)

epochs = 10
max_length = 500
batch_size = 64
num_classes = 2

model = make_model(bert, num_classes, max_length)

model.compile(optimizer=adam,
              loss='sparse_categorical_crossentropy',
              metrics=['acc'])

# Train the model.
seed_everything(42)

callbacks = [
    EarlyStopping(patience=3,restore_best_weights=True)
]

result=model.fit(x=x_train,
              y=y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_valid, y_valid),#遅いから、いったん学習のみで
              callbacks=callbacks,
              use_multiprocessing=True,
              workers=-1,
              )

時間的に関係で転移学習(BERT層凍結)とエポック数をかなり少なめにやっただけでもでも約77%でした。

ここまでやっての感想

トピックモデル→ニューラルネットワーク系→Transformer→BERT→BERT派生…と無限に勉強することは連鎖すると思いました。

ChatGPTについて

大規模言語モデル等の台頭により、新しい技術を取り入れることも大事になっていると考えているため、最近学んだChatGPTでどういう文章を送ればよい答えが返ってくるかということをまとめてみました。

トピックモデル系

ニューラルネットワークモデル系

Transformer

古典的なところから順々に技術が進歩し今があると思うので、基本に立ち返るのも大事なのかなって思いました。精度に関しても古典的な方が高い場合もありますし、単純に知識として役に立つと思います。

使ってみるだけならBERTすらも個人的に学習できるような時代になったので、こんな今こそ知識を身に着けるとアドバンテージだけでなく、他の人よりうまく利用できるのではないでしょうか?(※個人の感想です)

是非、参考にしてみてください。

まとめ

実際に調べてまとめる過程で、基礎を理解しなくても、BERTを使用できる環境がもう作られているのだと感じた。だが、BERTの系譜をやる中ではこういった基礎を学ばなくてはいけないのであろうか…。

もっとBERTについて学んでいきたい…。

もし、この記事を読んで参考になった・他の記事も読んでみたいと思った方、以下のボタンを押していただけるとモチベーション向上につながるので、よろしくお願いいたします。

コメント

タイトルとURLをコピーしました