2022.05.31

はじめてのBert

はじめに

BERT(*1)はさまざまな自然言語処理タスクでSOTA(*2)を達成しているDeepLearningモデルです。この記事はKaggleで掲載されているLearning BERT for the first timeにならってBERTの使い方を示します。

*1 Bidirectional Encoder Representations for Transformersの略。 2018年10月にGoogleのJacob Devlinらの論文で発表された自然言語処理モデル。日本語では「Transformerによる双方向のエンコード表現」と訳されている。BERTではTransformerというアーキテクチャで文章を文頭・文末の双方向から学習することで、文脈を読めるようになった。

*2 State-of-the-Artの略称。ある特定の専門技術領域において現時点での最先端レベルの性能(=機械学習では正解率などのスコア/精度)を達成していることを表す。

 

BERTの仕組み

これまでの一般的な自然言語処理モデルは文章を単一方向からしか処理することができませんでした。しかし、BERTは双方向のエンコード表現ができます。BERTにはMasked Language ModelとNext Sentence Predictionという2つの手法を同時に進行していき、学習できる仕組みです。Masked Language Modelで文章の文頭及び文末の双方向から学習していくことができます。一方Next Sentence Predictionでは二つの入力された文に対し、その二つの文が隣り合っているのかを当てるように学習します。詳細については参考記事1~5をご参照ください。それでは早速BERTの関連のnotebookを実行してみましょう。このnotebookではBidirectional Encoder Representations from Transformers (BERT, bert_en_uncased_L-12_H-768_A-12)というAPIをどのようにアクセス(wrap)するかを説明します。

インスタンスへのログイン

早速インスタンスにログインをしましょう。ターミナルから以下のコマンドを入力して、アクセスサーバへ接続をしてください(*ここではインスタンスのIPアドレスは10.233.101.21とします)。下図のように表示されれば接続完了です。

ssh -L 20122:10.233.101.21:22 user@202.122.50.154 -p 30022 -i .\.ssh\ackey.txt

続いて新規にターミナルを立ち上げて、下記のコマンドを入力してください。下図のように表示されれば接続完了です。

ssh -L 8888:localhost:8888 user@localhost -p 20122 -i .ssh/mykey.txt

データセットとコードの準備

今回使用するデータセットおよびソースコードをgithubからダウンロードします。

gitがインストールされていない場合、下記コマンドにてインストールしてください。

sudo apt -y update
sudo apt -y install git  

続いて下記コマンドにて必要なデータ類をダウンロードします。                                                                               

git clone https://github.com/highreso/bert.git

Jupyterへのログイン

インスタンスに接続されたターミナルにて、下記のコマンドを実行してjupyter labを起動してください。

jupyter lab --ip=* --no-browser

続いてローカルPCにてブラウザを立ち上げて、"http://localhost:8888" にアクセスしてください。以降はjupyterへのログインが成功した前提で、各セルで実行するコードの解説をいたします。

ここからは先ほどgithubから取得した、bertフォルダ内のjp-learning-bert-for-the-first-time.ipynbの各セルについて、順に内容を確認しながらセルを実行していきましょう。

まずは今回はtensorflow環境を使用するため、Environmentを"conda_tensorflow24_py36"に設定します。

最初にGPUがtensorflowで使用可能か確認します。

In [1]:
## Check GPU recognized
from tensorflow.python.client import device_lib
device_lib.list_local_devices()
Out[1]:

[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {
}
incarnation: 1029349297547213705,
name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 39395347712
locality {
bus_id: 1
links {
}
}
incarnation: 11126425406798181006
physical_device_desc: "device: 0, name: A100-PCIE-40GB, pci bus id: 0000:c1:00.0, compute capability:
8.0"]

device_type: "GPU" が表示されていればGPUが認識されている状態です。

続いて、必要なライブラリをインストールします。

In [2]:
!pip install -U pandas==1.1.5
!pip install -U tensorflow_hub==0.12.0
!pip install -U bert-tensorflow==1.0.1

import sys
sys.path.append('/home/user/.local/lib/python3.6/site-packages')

import numpy as np
import pandas as pd
import re
import tensorflow as tf
from tensorflow.keras.layers import Dense, Input, Dropout
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.models import Model
from tensorflow.keras.callbacks import ModelCheckpoint
import tensorflow_hub as hub

from bert import tokenization

pd.read.scv("./input/train.csv") を使って、inputフォルダにある学習用データtrain.csvファイルを読み込みます。読み込んだデータの先頭5行分のデータをtrain.head()というコマンドで、リストにします。

In [3]:
train =pd.read_csv("./input/train.csv")
train.head()
Out[3]:
id keyword location text target
0 1 NaN NaN Our Deeds are the Reason of this #earthquake M... 1
1 4 NaN NaN Forest fire near La Ronge Sask. Canada 1
2 5 NaN NaN All residents asked to 'shelter in place' are ... 1
3 6 NaN NaN 13,000 people receive #wildfires evacuation or... 1
4 7 NaN NaN Just got sent this photo from Ruby #Alaska as ... 1

pd.read.scv("./input/test.csv") を使って、inputフォルダにあるテスト用データtest.csvファイルを読み込みます。読み込んだデータの先頭5行分のデータをtest.head()というコマンドで、リストにします。

In [4]:
test =pd.read_csv("./input/test.csv")
test.head()
Out[4]:
id keyword location text
0 0 NaN NaN Just happened a terrible car crash
1 2 NaN NaN Heard about #earthquake is different cities, s...
2 3 NaN NaN there is a forest fire at spot pond, geese are...
3 9 NaN NaN Apocalypse lighting. #Spokane #wildfires
4 11 NaN NaN Typhoon Soudelor kills 28 in China and Taiwan

BERT APIの利用方法

下記のやり方で事前にトレーニングされたBERTエンコーダーとプリプロセッサーを利用することができます。このトレーニングされたモデルを利用することで、簡単に単語の分類予測等を実施することができます。%%timeは実行時間を計測するためのコマンドになります。

In [5]:
%%time
module_url = "https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1"
bert_layer = hub.KerasLayer(module_url, trainable=True)

CPU times: user 5.24 s, sys: 735 ms, total: 5.98 s
Wall time: 5.97 s

その後、取得したBERTのレイヤーを下記のようにトークン化します。

In [6]:
tf.gfile = tf.io.gfile

vocab_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
do_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
tokenizer = tokenization.FullTokenizer(vocab_file, do_lower_case)

BERTが受け取れる形式にデータ整形

ここは前処理の基本的な考え方を説明します。

In [7]:
text = "This is a Goat, and I am riding a Boat...."
tokenize_ = tokenizer.tokenize(text)
print("Text after tokenization: ")
print(tokenize_)
max_len = 25

text = tokenize_[:max_len-2]
input_sequence = ["[CLS]"] + text + ["[SEP]"]
pad_len = max_len - len(input_sequence)

print("After adding [CLS] and [SEP]: ")
print(input_sequence)
tokens = tokenizer.convert_tokens_to_ids(input_sequence)
print("After converting Tokens to Id: ")
print(tokens)
tokens += [0] * pad_len
print("tokens: ")
print(tokens)
pad_masks = [1] * len(input_sequence) + [0] * pad_len
print("Pad Masking: ")
print(pad_masks)
segment_ids = [0] * max_len
print("Segment Ids: ")
print(segment_ids)

Text after tokenization:
['this', 'is', 'a', 'goat', ',', 'and', 'i','am', 'riding', 'a', 'boat', '.', '.', '.','.']
After adding [CLS] and [SEP]:
['[CLS]', 'this', 'is', 'a', 'goat', ',', 'and','i', 'am', 'riding', 'a', 'boat', '.', '.','.', '.', '[SEP]']
After converting Tokens to Id:
[101, 2023, 2003, 1037, 13555, 1010, 1998, 1045, 2572, 5559, 1037, 4049, 1012, 1012, 1012, 1012, 102]
tokens:
[101, 2023, 2003, 1037, 13555, 1010, 1998, 1045, 2572, 5559, 1037, 4049, 1012, 1012, 1012, 1012, 102, 0, 0, 0,0, 0, 0, 0, 0]
Pad Masking:
[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]
Segment Ids:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

上記のコードではデータの前処理(BERTが受け取れる形式にデータを整形)します。まずは下記のようにBERT Tokenizerを用いて文章をトークン化(単語分割)します。

['this', 'is', 'a', 'goat', ',', 'and', 'i','am', 'riding', 'a', 'boat', '.', '.', '.','.']

その後、文章の最初及び最後に、Special tokenの[CLS]と[SEP]を追加します。

['[CLS]', 'this', 'is', 'a', 'goat', ',', 'and','i', 'am', 'riding', 'a', 'boat', '.', '.','.', '.', '[SEP]']

その次はトークン化(単語分割)したトークンを下記のようにID化します。

[101, 2023, 2003, 1037, 13555, 1010, 1998, 1045, 2572, 5559, 1037, 4049, 1012, 1012, 1012, 1012, 102]

BERTが処理できるように、文章の長さが固定されているため、文章あたりの最大単語数に合わせて、ゼロで”文章”の長さを合わせています。合わせたIDは下記の通りになります。これはPaddingというプロセスといいます。指定した長さに満たない文章を[Pad]という意味を持たない単語の埋める処理を行うことです。今回は長すぎるプロセスがないですが、ある場合、[Truncating]で指定した長さを超える単語を切り捨てることです。

[101, 2023, 2003, 1037, 13555, 1010, 1998, 1045, 2572, 5559, 1037, 4049, 1012, 1012, 1012, 1012, 102, 0, 0, 0,0, 0, 0, 0, 0]

上述の考えに基づき、BERTが受け取れる形式にデータを整形する関数を下記に示します。

In [8]:
def pre_Process_data(documents, tokenizer, max_len=512):
    '''
    For preprocessing we have regularized, transformed each upper case into lower case, tokenized,
    Normalized and remove stopwords. For normalization, we have used PorterStemmer. Porter stemmer transforms 
    a sentence from this "love loving loved" to this "love love love"
    
    '''
    all_tokens = []
    all_masks = []
    all_segments = []
    print("Pre-Processing the Data.........\n")
    for data in documents:
        review = re.sub('[^a-zA-Z]', ' ', data)
        url = re.compile(r'https?://\S+|www\.\S+')
        review = url.sub(r'',review)
        html=re.compile(r'<.*?>')
        review = html.sub(r'',review)
        emoji_pattern = re.compile("["
                           u"\U0001F600-\U0001F64F"  # emoticons
                           u"\U0001F300-\U0001F5FF"  # symbols & pictographs
                           u"\U0001F680-\U0001F6FF"  # transport & map symbols
                           u"\U0001F1E0-\U0001F1FF"  # flags (iOS)
                           u"\U00002702-\U000027B0"
                           u"\U000024C2-\U0001F251"
                           "]+", flags=re.UNICODE)
        review = emoji_pattern.sub(r'',review)
        text = tokenizer.tokenize(review)
        text = text[:max_len-2]
        input_sequence = ["[CLS]"] + text + ["[SEP]"]
        pad_len = max_len - len(input_sequence)
        
        tokens = tokenizer.convert_tokens_to_ids(input_sequence)
        tokens += [0] * pad_len
        pad_masks = [1] * len(input_sequence) + [0] * pad_len
        segment_ids = [0] * max_len
        
        all_tokens.append(tokens)
        all_masks.append(pad_masks)
        all_segments.append(segment_ids)
    return np.array(all_tokens), np.array(all_masks), np.array(all_segments)

In [9]:
input_word_id = Input(shape=(max_len,),dtype=tf.int32, name="input_word_ids")
input_mask = Input(shape=(max_len,), dtype=tf.int32, name="input_mask")
segment_id = Input(shape=(max_len,), dtype=tf.int32, name = "segment_id")

_, sequence_output = bert_layer([input_word_id, input_mask, segment_id])
clf_output = sequence_output[:, 0, :]
model = Model(inputs=[input_word_id, input_mask, segment_id],outputs=clf_output)
model.compile(Adam(lr=2e-5), loss='binary_crossentropy', metrics=['accuracy'])
model.summary()
print("shape of _ layer of BERT: "+str(_.shape))
print("shape of last layer of BERT: "+str(sequence_output.shape))

Model: "model"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_word_ids (InputLayer) [(None, 25)] 0
__________________________________________________________________________________________________
input_mask (InputLayer) [(None, 25)] 0
__________________________________________________________________________________________________
segment_id (InputLayer) [(None, 25)] 0
__________________________________________________________________________________________________
keras_layer (KerasLayer) [(None, 768), (None, 109482241 input_word_ids[0][0]
input_mask[0][0]
segment_id[0][0]
__________________________________________________________________________________________________
tf.__operators__.getitem (Slici (None, 768) 0 keras_layer[0][1]
==================================================================================================
Total params: 109,482,241
Trainable params: 109,482,240
Non-trainable params: 1
__________________________________________________________________________________________________
shape of _ layer of BERT: (None, 768)
shape of last layer of BERT: (None, None, 768)

In [10]:
def build_model(bert_layer, max_len=512):
    input_word_id = Input(shape=(max_len,),dtype=tf.int32, name="input_word_ids")
    input_mask = Input(shape=(max_len,), dtype=tf.int32, name="input_mask")
    segment_id = Input(shape=(max_len,), dtype=tf.int32, name = "segment_id")
    
    _, sequence_output = bert_layer([input_word_id, input_mask, segment_id])
    clf_output = sequence_output[:, 0, :]
    dense_layer1 = Dense(units=256,activation='relu')(clf_output)
    dense_layer1 = Dropout(0.4)(dense_layer1)
    dense_layer2 = Dense(units=128, activation='relu')(dense_layer1)
    dense_layer2 = Dropout(0.4)(dense_layer2)
    out = Dense(1, activation='sigmoid')(dense_layer2)
    
    model = Model(inputs=[input_word_id, input_mask, segment_id],outputs=out)
    model.compile(Adam(lr=2e-5), loss='binary_crossentropy', metrics=['accuracy'])
    
    return model
In [11]:
train_input = pre_Process_data(train.text.values, tokenizer, max_len=260)
test_input = pre_Process_data(test.text.values, tokenizer, max_len=260)
train_labels = train.target.values

Pre-Processing the Data.........
Pre-Processing the Data.........

データセットを前処理した後で、下記のコードで、モデルにロードします。

In [12]:
model = build_model(bert_layer, max_len=260)
model.summary()

Model: "model_1"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_word_ids (InputLayer) [(None, 260)] 0
__________________________________________________________________________________________________
input_mask (InputLayer) [(None, 260)] 0
__________________________________________________________________________________________________
segment_id (InputLayer) [(None, 260)] 0
__________________________________________________________________________________________________
keras_layer (KerasLayer) [(None, 768), (None, 109482241 input_word_ids[0][0]
input_mask[0][0]
segment_id[0][0]
__________________________________________________________________________________________________
tf.__operators__.getitem_1 (Sli (None, 768) 0 keras_layer[1][1]
__________________________________________________________________________________________________
dense (Dense) (None, 256) 196864 tf.__operators__.getitem_1[0][0]
__________________________________________________________________________________________________
dropout (Dropout) (None, 256) 0 dense[0][0]
__________________________________________________________________________________________________
dense_1 (Dense) (None, 128) 32896 dropout[0][0]
__________________________________________________________________________________________________
dropout_1 (Dropout) (None, 128) 0 dense_1[0][0]
__________________________________________________________________________________________________
dense_2 (Dense) (None, 1) 129 dropout_1[0][0]
==================================================================================================
Total params: 109,712,130
Trainable params: 109,712,129
Non-trainable params: 1
__________________________________________________________________________________________________

トレーニング及び分類予測

ここではモデルのファインチューニングをするために、トレーニングを行います。train.csvファイルのデータは既にtrain_inputに入れてあるので、model.fitの関数(厳密にはfit関数)を利用してモデルのトレーニングを行います。

In [13]:
checkpoint = ModelCheckpoint('model.h5', monitor='val_loss', save_best_only=True)
train_history = model.fit(
    train_input, train_labels,
    validation_split=0.2,
    epochs=10,
    callbacks=[checkpoint],
#     batch_size=32
    batch_size=2
)

Epoch 1/10
3045/3045 [==============================] - 105s 30ms/step - loss: 0.5305 - accuracy: 0.7514 - val_loss:
0.4145 - val_accuracy: 0.8253
Epoch 2/10
3045/3045 [==============================] - 90s 30ms/step - loss: 0.3250 - accuracy: 0.8813 - val_loss: 0.4846
- val_accuracy: 0.8201
Epoch 3/10
3045/3045 [==============================] - 93s 30ms/step - loss: 0.1672 - accuracy: 0.9417 - val_loss: 0.6379
- val_accuracy: 0.7873
Epoch 4/10
3045/3045 [==============================] - 96s 31ms/step - loss: 0.0861 - accuracy: 0.9693 - val_loss: 0.7866
- val_accuracy: 0.7951
Epoch 5/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0876 - accuracy: 0.9682 - val_loss: 0.8580
- val_accuracy: 0.8201
Epoch 6/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0654 - accuracy: 0.9771 - val_loss: 0.8860
- val_accuracy: 0.7932
Epoch 7/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0764 - accuracy: 0.9759 - val_loss: 0.8610
- val_accuracy: 0.8070
Epoch 8/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0549 - accuracy: 0.9782 - val_loss: 0.7016
- val_accuracy: 0.8050
Epoch 9/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0523 - accuracy: 0.9783 - val_loss: 1.0104
- val_accuracy: 0.8024
Epoch 10/10
3045/3045 [==============================] - 96s 32ms/step - loss: 0.0465 - accuracy: 0.9778 - val_loss: 1.1859
- val_accuracy: 0.8162

In [14]:
submission = pd.read_csv("./input/sample_submission.csv")
submission.head()
Out[14]:
id target
0 0 0
1 2 0
2 3 0
3 9 0
4 11 0

ここでは予め準備したtest.csvのテストデータを用いて下記のコードでモデルの確度をテストすることができます。

test_pred = model.predict(test_input)

In [15]:
model.load_weights('model.h5')
test_pred = model.predict(test_input)

test_pred
Out[15]:

array([[0.95257264],
[0.9160163 ],
[0.9756269 ],
...,
[0.9794409 ],
[0.92635137],
[0.5679488 ]], dtype=float32)

まとめ

モデルの訓練が終われば、model.predict()という関数で、そのモデルを使って分類予測を実施することができます。arrayに入っている数字は「確信度」を表しています。上述のように、BERTの使い方を説明しました。

参考記事1:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

参考記事2:Paper : BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

参考記事3:Learning BERT for the first time

参考記事4:BERT for Humans: Tutorial+Baseline

参考記事5:Googleが誇る「BERT」とは?次世代の自然言語処理の特徴を解説

参考記事6:自然言語処理モデル(BERT)を利用した日本語の文章分類 〜GoogleColab & Pytorchによるファインチューニング〜


いかがでしたでしょうか。
弊社クラウドGPUサーバのご利用をご検討中の方はhttps://soroban.highreso.jp/から
お気軽にお申込みやお問い合わせください。

初回インスタンス作成から
3日間は無料利用可能!

新規申し込み