PythonによるBERTトークン化器とTF2.0を用いたテキスト分類

Python for NLPの連載も今回で23回目となりました。

本連載の前回記事では、Pythonの深層学習用ライブラリKerasを用いて、seq2seqアーキテクチャによるニューラル機械翻訳を行う方法を説明しました。

今回は、BERT(Bidirectional Encoder Representations from Transformersの略)とそのテキスト分類への応用について勉強していきます。

BERTはWord Embeddingsのようなテキスト表現技法です。

もし、Word Embeddingsの仕組みが分からない場合は、Word Embeddingsに関する私の記事を見てください。

BERTもWord Embdingsと同様に、双方向エンコーダLSTMやTransformerなどの様々な最先端のディープラーニングアルゴリズムを融合したテキスト表現技術です。

BERTは2018年にGoogleの研究者によって開発され、テキスト分類、テキスト要約、テキスト生成など、さまざまな自然言語処理タスクで最先端であることが実証されています。

つい最近、GoogleはBERTを検索アルゴリズムの中核として使用し、クエリをより理解できるようにすると発表しました。

この記事では、BERTがどのように実装されているかという数学的な詳細には立ち入らない。

むしろ、BERT トークン化器を使用してテキスト分類を行う方法について見ていきます。

この記事では、テキスト分類モデルを作成するために BERT トークン化器をどのように使用できるかを説明します。

次回は、BERT トーケナイザーを BERT 埋め込み層とともに使用して、さらに効率的な NLP モデルを作成する方法を説明する予定です。

注:この記事のすべてのスクリプトは、PythonのランタイムをGPUに設定し、Google Colab環境を使用してテストされています。

データセット

この記事で使用したデータセットは、このKaggleのリンクからダウンロードできます。

データセットをダウンロードし、圧縮ファイルを解凍すると、CSVファイルが表示されます。

このファイルには、50,000件のレコードと、reviewとsentimentの2つのカラムが含まれています。

reviewカラムはレビューのテキストを含み、sentimentカラムはレビューのセンチメントを含みます。

sentiment列は2つの値、すなわち「ポジティブ」と「ネガティブ」を持つことができ、これは我々の問題をバイナリ分類問題にします。

我々は以前の記事でこのデータセットのセンチメント分析を行い、単語埋め込み技術と畳み込みニューラルネットワークにより、トレーニングセットで92%の最大精度を達成しました。

テストセットでは、単語埋め込みと128ノードのシングルLSTMを用いて、最大85.40%の精度を達成することができました。

BERT表現を用いてより良い精度を得ることができるか見てみましょう。

必要なライブラリのインストールとインポート

BERTのテキスト表現を使用する前に、BERT for TensorFlow 2.0をインストールする必要があります。

端末上で以下のpipコマンドを実行し、BERT for TensorFlow 2.0をインストールします。

!pip install bert-for-tf2
!pip install sentencepiece


次に、TensorFlow 2.0が動作していることを確認します。

Google Colabは、デフォルトではTensorFlow 2.0上でスクリプトを実行しません。

そのため、TensorFlow 2.0経由でスクリプトが実行されていることを確認するために、以下のスクリプトを実行します。

try:
    %tensorflow_version 2.x
except Exception:
    pass
import tensorflow as tf


import tensorflow_hub as hub


from tensorflow.keras import layers
import bert


上記のスクリプトでは、TensorFlow 2.0の他に、Tensorflowで開発されたプレビルトモデルやプレトレーニングモデルを集めたtensorflow_hubをインポートしています。

今回は、TFハブからビルトインされたBERTモデルをインポートして使用します。

最後に、出力で以下のように表示されればOKです。

TensorFlow 2.x selected.


データセットの取り込みと前処理

以下のスクリプトは、Pandasのdataframeの read_csv() メソッドを用いてデータセットをインポートしている。

また、データセットの形状を表示します。

movie_reviews = pd.read_csv("/content/drive/My Drive/Colab Datasets/IMDB Dataset.csv")


movie_reviews.isnull().values.any()


movie_reviews.shape


出力

(50000, 2)


出力結果から、今回のデータセットは50,000行、2列であることがわかります。

次に、データの前処理として、句読点や特殊文字を除去する。

これを行うために、生のテキストレビューを入力とし、それに対応するクリーンなテキストレビューを返す関数を定義します。

def preprocess_text(sen):
    # Removing html tags
    sentence = remove_tags(sen)


# Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sentence)


# Single character removal
    sentence = re.sub(r"s+[a-zA-Z]s+", ' ', sentence)


# Removing multiple spaces
    sentence = re.sub(r's+', ' ', sentence)


return sentence


TAG_RE = re.compile(r'<[^>]+>')


def remove_tags(text):
    return TAG_RE.sub('', text)


次のスクリプトは、すべてのテキスト・レビューをクリーニングします。

reviews = []
sentences = list(movie_reviews['review'])
for sen in sentences:
    reviews.append(preprocess_text(sen))


以下のスクリプトで確認できるように、我々のデータセットには2つのカラムが含まれています。

print(movie_reviews.columns.values)


出力

['review' 'sentiment']


reviewカラムにはテキストが、sentimentカラムにはセンチメントが格納される。

sentimentsカラムには、テキスト形式の値が格納されている。

次のスクリプトはsentiment` カラムにユニークな値を表示します。

movie_reviews.sentiment.unique()


出力されます。

array(['positive', 'negative'], dtype=object)


sentiment列には2つのユニークな値、すなわちpositivenegativeが含まれていることがわかります。

ディープラーニングのアルゴリズムは数値で動作する。

出力には2つのユニークな値しかないので、1と0に変換できます。

次のスクリプトは、positiveの感情を1に、negativeの感情を0に置き換えています。

y = movie_reviews['sentiment']


y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))


ここで、変数 reviews にはテキストのレビューが格納され、変数 y にはそれに対応するラベルが格納されます。

ランダムにレビューを表示してみましょう。

print(reviews[10])


出力してみましょう。

Phil the Alien is one of those quirky films where the humour is based around the oddness of everything rather than actual punchlines At first it was very odd and pretty funny but as the movie progressed didn find the jokes or oddness funny anymore Its low budget film thats never problem in itself there were some pretty interesting characters but eventually just lost interest imagine this film would appeal to stoner who is currently partaking For something similar but better try Brother from another planet


明らかにネガティブなレビューのように見えます。

対応するラベルの値を出力して確認しましょう。

print(y[10])


出力してみましょう。

0


出力0は、ネガティブレビューであることを確認します。

これで、データの前処理が完了し、テキストデータから BERT 表現を作成する準備が整いました。

BERTトークナイザーの作成

テキスト分類モデルを訓練するための入力として BERT テキスト埋め込みを使用するために、我々は、テキストレビューをトークン化する必要があります。

トークン化とは、文章を個々の単語に分割することです。

テキストをトークン化するために、BERTトークナイザーを使用する予定です。

次のスクリプトを見てください。

BertTokenizer = bert.bert_tokenization.FullTokenizer
bert_layer = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1",
                            trainable=False)
vocabulary_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
to_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
tokenizer = BertTokenizer(vocabulary_file, to_lower_case)


上記のスクリプトでは、まず bert.bert_tokenization モジュールから FullTokenizer クラスのオブジェクトを作成します。

次に、hub.KerasLayerからBERTモデルをインポートして、BERT埋め込み層を作成します。

trainableパラメータはFalseに設定し、BERT 埋め込みを学習しないことを意味します。

次の行では、BERT 語彙ファイルを numpy 配列の形式で作成します。

そして、テキストを小文字に設定し、最後にvocabulary_fileto_lower_case変数をBertTokenizer` オブジェクトに渡します。

この記事では、BERT Tokenizer のみを使用することに言及するのが適切でしょう。

次の記事では、トークナイザーとともに BERT Embeddings を使用する予定です。

それでは、BERT トークナイザーが実際に動作しているかどうかを見てみましょう。

そのために、以下のようなランダムな文章をトークン化します。

tokenizer.tokenize("don't be so judgmental")


出力。

['don', "'", 't', 'be', 'so', 'judgment', '##al']


テキストが正常にトークン化されたことがわかります。

トークンの ID は、トークン化オブジェクトの convert_tokens_to_ids() を使って取得することもできます。

次のスクリプトを見てください。

tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so judgmental"))


出力してください。

[2123, 2102, 2022, 2061, 8689, 2389]


次に、1つのテキストレビューを受け取り、レビューの中のトークン化された単語のidを返す関数を定義します。

次のスクリプトを実行する。

def tokenize_reviews(text_reviews):
    return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_reviews))


そして、入力データセットのすべてのレビューを実際にトークン化するために、以下のスクリプトを実行します。

tokenized_reviews = [tokenize_reviews(review) for review in reviews]


学習用データの準備

我々のデータセットに含まれるレビューの長さは様々です。

あるレビューはとても小さく、あるレビューはとても長い。

モデルを学習するために、入力センテンスは同じ長さであるべきです。

同じ長さの文章を作成するために、1つの方法は短い文章に0を埋め込むことです。

しかし、この方法では、多数の0を含む疎な行列になる可能性がある。

もう一つの方法は、各バッチ内の文を埋めることである。

モデルをバッチで学習させるので、最長文の長さに応じて、学習バッチ内の文を局所的に埋めることができます。

そのためには、まず各文章の長さを求める必要があります。

次のスクリプトは、トークン化されたレビュー、レビューのラベル、レビューの長さを含むリストのリストを作成します。

reviews_with_len = [[review, y[i], len(review)]
                 for i, review in enumerate(tokenized_reviews)]


我々のデータセットでは、レビューの前半は肯定的で、後半は否定的なレビューが含まれています。

したがって、トレーニングバッチにポジティブとネガティブの両方のレビューを持たせるために、レビューをシャッフルする必要があります。

以下のスクリプトはデータをランダムにシャッフルします。

random.shuffle(reviews_with_len)


データがシャッフルされたら、レビューの長さによってデータをソートします。

これを行うには、リストの sort() 関数を使用し、サブリストの3番目の項目、つまりレビューの長さに関してリストをソートするように指示します。

reviews_with_len.sort(key=lambda x: x[2])


レビューが長さでソートされたら、すべてのレビューから長さ属性を削除することができます。

次のスクリプトを実行してください。

sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in reviews_with_len]


レビューがソートされたら、TensorFlow 2.0モデルを学習するためにdデータセットを変換します。

以下のコードを実行して、ソートされたデータセットをTensorFlow 2.0に準拠した入力データセット形状に変換してください。

processed_dataset = tf.data.Dataset.from_generator(lambda: sorted_reviews_labels, output_types=(tf.int32, tf.int32))


最後に、各バッチのデータセットにパッドを入れることができるようになりました。

今回使用するバッチサイズは32で、これは32件のレビューを処理した後、ニューラルネットワークの重みが更新されることを意味します。

バッチに対してローカルにレビューを詰めるには、以下を実行します。

BATCH_SIZE = 32
batched_dataset = processed_dataset.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))


最初のバッチをプリントして、パディングがどのように適用されたかを見てみましょう。

next(iter(batched_dataset))


出力します。

(<tf.tensor: ......="" 0,="" 0],="" 10102,="" 1045,="" 10828,="" 10904,="" 10958,="" 11056,="" 11259,="" 11813,="" 12635,="" 12826,="" 14888,="" 15580,="" 16755,="" 1996,="" 1997,="" 1998,="" 1999,="" 2000,="" 2004,="" 2005,="" 2006,="" 2007,="" 2008,="" 2012,="" 2017,="" 2022,="" 2023,="" 2024,="" 2028,="" 2031,="" 2040,="" 2043,="" 2045,="" 2054,="" 2062,="" 2064,="" 2068,="" 2081,="" 2086,="" 2092,="" 2097,="" 21),="" 2102,="" 2111,="" 2130,="" 2145,="" 2146,="" 2169,="" 2172,="" 2179,="" 2189,="" 2191,="" 2197,="" 2204,="" 2293,="" 2296,="" 2305,="" 2307,="" 23191,="" 2338,="" 23873,="" 2402,="" 2466,="" 2467,="" 2472,="" 2498,="" 2508,="" 2552]],="" 2569,="" 2619],="" 2633,="" 2696,="" 2856,="" 2876,="" 2926,="" 3078,="" 3135,="" 3152,="" 3153,="" 3185,="" 3191,="" 3257,="" 3522,="" 3532,="" 3773,="" 3802,="" 3811,="" 3993,="" 4276,="" 4281,="" 4393,="" 4438,="" 4438],="" 4569,="" 5293,="" 5436,="" 5691,="" 5760,="" 5896,="" 5934,="" 6170,="" 6370,="" 6702,="" 6752,="" 7244,="" 7613,="" 7788,="" 7922,="" 8428,="" 8808,="" 8847,="" 9278,="" [="" dtype="int32)" numpy="array([[" shape="(32,",
 <tf.tensor: 0,="" 1,="" 1],="" dtype="int32)" numpy="array([0," shape="(32,),")


上の出力は、最初の5つと最後の5つのパディングされたレビューを示しています。

最後の5つのレビューから、最大の文の単語数の合計が21であることがわかります。

したがって、最初の5つのレビューでは、文の最後に0が追加され、それらの合計の長さも21となるようにしました。

次のバッチのパディングは、そのバッチの最大の文のサイズに応じて異なるものになります。

データセットにパディングを適用したら、次のステップはデータセットをテストセットとトレーニングセットに分けることである。

これは以下のコードで可能です。

TOTAL_BATCHES = math.ceil(len(sorted_reviews_labels) / BATCH_SIZE)
TEST_BATCHES = TOTAL_BATCHES // 10
batched_dataset.shuffle(TOTAL_BATCHES)
test_data = batched_dataset.take(TEST_BATCHES)
train_data = batched_dataset.skip(TEST_BATCHES)


上記のコードでは、まずレコードの総数を32で割ってバッチの総数を求めている。

次に、データの10%をテスト用に残しておく。

そのために、batched_dataset() オブジェクトの take() メソッドを用いて、データの10%を test_data 変数に格納する。

残りのデータは train_data オブジェクトに格納され、 skip() メソッドを用いて学習します。

データセットが準備できたので、テキスト分類モデルを作成する準備ができました。

モデルの作成

これでモデルを作成する準備が整った。

そのために、tf.keras.Modelクラスを継承したTEXT_MODELというクラスを作成します。

このクラスの中で、モデルのレイヤーを定義します。

モデルは3つの畳み込みニューラルネットワーク層で構成されます。

代わりにLSTM層を使うこともできますし、層の数を増やしたり減らしたりすることもできます。

私はSuperDataScienceのGoogle colab notebookからレイヤーの数と種類をコピーしましたが、このアーキテクチャはIMDB Movie reviewsデータセットでも同様にうまく機能するようです。

それでは、モデルクラスを作成しましょう。

class TEXT_MODEL(tf.keras.Model):

    def __init__(self,
                 vocabulary_size,
                 embedding_dimensions=128,
                 cnn_filters=50,
                 dnn_units=512,
                 model_output_classes=2,
                 dropout_rate=0.1,
                 training=False,
                 name="text_model"):
        super(TEXT_MODEL, self).__init__(name=name)

        self.embedding = layers.Embedding(vocabulary_size,
                                          embedding_dimensions)
        self.cnn_layer1 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=2,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer2 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=3,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer3 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=4,
                                        padding="valid",
                                        activation="relu")
        self.pool = layers.GlobalMaxPool1D()

        self.dense_1 = layers.Dense(units=dnn_units, activation="relu")
        self.dropout = layers.Dropout(rate=dropout_rate)
        if model_output_classes == 2:
            self.last_dense = layers.Dense(units=1,
                                           activation="sigmoid")
        else:
            self.last_dense = layers.Dense(units=model_output_classes,
                                           activation="softmax")

    def call(self, inputs, training):
        l = self.embedding(inputs)
        l_1 = self.cnn_layer1(l) 
        l_1 = self.pool(l_1) 
        l_2 = self.cnn_layer2(l) 
        l_2 = self.pool(l_2)
        l_3 = self.cnn_layer3(l)
        l_3 = self.pool(l_3) 

        concatenated = tf.concat([l_1, l_2, l_3], axis=-1) # (batch_size, 3 * cnn_filters)
        concatenated = self.dense_1(concatenated)
        concatenated = self.dropout(concatenated, training)
        model_output = self.last_dense(concatenated)

        return model_output


上のスクリプトはとても簡単です。

クラスのコンストラクタで、いくつかの属性をデフォルト値で初期化しています。

これらの値は、後で TEXT_MODEL クラスのオブジェクトを生成するときに渡される値で置き換えられる。

次に、3つの畳み込みニューラルネットワークの層が、それぞれ2、3、4のカーネルまたはフィルタの値で初期化されています。

ここでも、必要に応じてフィルタのサイズを変更することができます。

次に、call()関数内で、それぞれの畳み込みニューラルネットワーク層の出力にグローバルマックスプーリングを適用しています。

最後に、3つの畳み込みニューラルネットワーク層を連結し、その出力を最初の密結合ニューラルネットワークに供給する。

2番目の密結合ニューラルネットワークは、2つのクラスしか含まれていないため、出力感情を予測するために使用されます。

出力にもっと多くのクラスがある場合は、それに応じて output_classes 変数を更新することができます。

それでは、モデルのハイパーパラメータの値を定義しましょう。

VOCAB_LENGTH = len(tokenizer.vocab)
EMB_DIM = 200
CNN_FILTERS = 100
DNN_UNITS = 256
OUTPUT_CLASSES = 2


DROPOUT_RATE = 0.2


NB_EPOCHS = 5


次に、TEXT_MODEL クラスのオブジェクトを作成して、TEXT_MODEL クラスのコンストラクタに最後のステップで定義したハイパーパラメータを渡します。

text_model = TEXT_MODEL(vocabulary_size=VOCAB_LENGTH,
                        embedding_dimensions=EMB_DIM,
                        cnn_filters=CNN_FILTERS,
                        dnn_units=DNN_UNITS,
                        model_output_classes=OUTPUT_CLASSES,
                        dropout_rate=DROPOUT_RATE)


実際にモデルを学習させる前に、モデルをコンパイルする必要があります。

以下のスクリプトはモデルをコンパイルします。

if OUTPUT_CLASSES == 2:
    text_model.compile(loss="binary_crossentropy",
                       optimizer="adam",
                       metrics=["accuracy"])
else:
    text_model.compile(loss="sparse_categorical_crossentropy",
                       optimizer="adam",
                       metrics=["sparse_categorical_accuracy"])


最後に、モデルを学習するために、モデルクラスの fit メソッドを使用します。

text_model.fit(train_data, epochs=NB_EPOCHS)


5エポック後の結果は以下の通りです。

Epoch 1/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.3037 - accuracy: 0.8661
Epoch 2/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.1341 - accuracy: 0.9521
Epoch 3/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0732 - accuracy: 0.9742
Epoch 4/5
1407/1407 [==============================] - 381s 271ms/step - loss: 0.0376 - accuracy: 0.9865
Epoch 5/5
1407/1407 [==============================] - 383s 272ms/step - loss: 0.0193 - accuracy: 0.9931
<tensorflow.python.keras.callbacks.history 0x7f5f65690048="" at=""


トレーニングセットで99.31%の精度を得たことがわかります。

それでは、テストセットでモデルのパフォーマンスを評価してみましょう。

results = text_model.evaluate(test_dataset)
print(results)


出力

156/Unknown - 4s 28ms/step - loss: 0.4428 - accuracy: 0.8926[0.442786190037926, 0.8926282]


出力から、テストセットで89.26%の精度が得られたことがわかります。

結論

この記事では、BERT Tokenizer を使用して、テキスト分類を実行するために使用できる単語埋め込みを作成する方法について見ました。

我々は、IMDB 映画レビューの感傷的な分析を行い、テストセットで 89.26% の精度を達成しました。

この記事では、BERT 埋め込みを使用せず、BERT Tokenizer を使用して単語をトークン化しただけです。

次回の記事では、BERT Tokenizer を BERT Embeddings と共に使用してテキスト分類を実行する方法を紹介します。

となります。

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