【GPUSOROBAN】はじめてのBert

Contents 

はじめに

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]:

idkeywordlocationtexttarget
01NaNNaNOur Deeds are the Reason of this #earthquake M…1
14NaNNaNForest fire near La Ronge Sask. Canada1
25NaNNaNAll residents asked to ‘shelter in place’ are …1
36NaNNaN13,000 people receive #wildfires evacuation or…1
47NaNNaNJust 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]:

idkeywordlocationtext
00NaNNaNJust happened a terrible car crash
12NaNNaNHeard about #earthquake is different cities, s…
23NaNNaNthere is a forest fire at spot pond, geese are…
39NaNNaNApocalypse lighting. #Spokane #wildfires
411NaNNaNTyphoon 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)
Copy

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 keraslayer[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]:

idtarget
000
120
230
390
4110

ここでは予め準備した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によるファインチューニング〜

関連記事

ページ上部へ戻る