nikkie-ftnextの日記

イベントレポートや読書メモを発信

transformersのBERTをTensorFlowからいじって多クラス分類しようとしたところ、ハマった末に😫、BERTは特徴量生成に使うのがよさそうと体験しました🤗

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

3/9の週はもろもろ締切が重なりやむなく断念。

3/8の記事で「次回はいまのNLPへのキャッチアップに踏み出す」ことにしていたので、今回はその続きでBERTを触りました。

BERTでテキスト分類をするのが今回手を動かす中での目標でした。

目次

動作環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V  # venvによる仮想環境を利用
Python 3.7.3
$ pip list  # 手動で入れたものを抜粋して記載
gensim                   3.8.1
ipython                  7.13.0
tensorflow               2.1.0
transformers             2.5.1

ファイル配置

.
├── bbc-text.csv  # 原データ
├── bert_classify.py
├── env  # 仮想環境
├── preprocess.py  # 前処理
└── preprocessed-bbc.csv

データセットと前処理

データセットはこちらの記事1英語テキストを使います:

これはBBCニュースのテキストで、スポーツ、ビジネスなどのカテゴリが付与されています。
カテゴリは全部で5つあり、その中の1つが付与されるため、問題設定は多クラス分類となります2

上記の記事にgensimを使った前処理があったので、それを使いました(NLTKでもできそうですよね。宿題です)

import csv

import gensim.parsing.preprocessing as gsp


filters = [
    gsp.strip_tags,
    gsp.strip_punctuation,
    gsp.strip_multiple_whitespaces,
    gsp.strip_numeric,
    gsp.remove_stopwords,
    gsp.strip_short,
    gsp.stem_text
]


def clean_text(text):
    text = text.lower()
    for f in filters:
        text = f(text)
    return text


if __name__ == '__main__':
    with open('bbc-text.csv') as fin:
        reader = csv.DictReader(fin)
        rows = list(reader)
    texts = [clean_text(row['text']) for row in rows]
    categories = [row['category'] for row in rows]

    with open('preprocessed-bbc.csv', 'w') as fout:
        writer = csv.writer(fout)
        contents = list(zip(texts, categories))
        writer.writerows(contents)

原データのテキストを前処理し、単語の原形を半角スペースでつないだ状態にします。
前処理に使っている処理は以下の通りです(gensim.parsing.preprocessingドキュメント):

  1. str.lower()で小文字に揃える
  2. gsp.strip_tagsでHTMLタグを除く
  3. gsp.strip_punctuationでpunctuations(句読点)を空白に変えて除く
  4. gsp.strip_multiple_whitespacesで繰り返された空白を除き、空白文字(タブや改行)を空白に揃える
  5. gsp.strip_numericで整数・数値を除く
  6. gsp.remove_stopwordsストップワードを除く(例えば ‘and’, ‘to’, ‘the’ などが除かれる)
  7. gsp.strip_shortで指定した長さ(デフォルトでは3)より短い語を除く
  8. gsp.stem_textでステミング3

ステミングは、単語から語幹を取り出すことです。
これは単語から活用を除くと考えられます。
gsp.stem_textで使っているPorterステマーの副作用か、語尾のeが消えがちでした。

TensorFlowからBERTを使う 🤗

huggingface/transformersを使いました4(もう定跡感がありますね)。

テキスト分類はTFBertForSequenceClassificationを使えばよさそうと分かり、まずはドキュメントのExampleを試します。

In [1]: import tensorflow as tf

In [2]: from transformers import BertTokenizer, TFBertForSequenceClassification

In [3]: tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

In [4]: model = TFBertForSequenceClassification.from_pretrained('bert-base-uncased')

In [5]: input_ids = tf.constant(tokenizer.encode("Hello, my dog is cute", add_special_tokens=True))[None, :]

In [7]: outputs = model(input_ids)

In [8]: outputs
Out[8]: (<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.27946013, 0.17132862]], dtype=float32)>,)

BERTで予測までできました!
'bert-base-uncased' という指定ですが、これはS3にアップロードされている単語のリスト5のどれを取得するかなどを指定しているようです。

ここから、上記のBBCのデータをBERTで分類しようとしたのですが、だいぶハマりました 😫

ハマった:TFBertForSequenceClassificationに多クラス分類をやらせるには?

Exampleの TensorFlow 2.0 Bert models on GLUE を元にBBCニュースの多クラス分類を試しました。

examples/run_tf_glue.py を参考にします6

たどり着いたコード

動作しますが、多クラス分類を断念しました。

import csv

import tensorflow as tf
from transformers import (
    BertTokenizer,
    glue_convert_examples_to_features,
    TFBertForSequenceClassification,
)


BUFFER_SIZE = 10000  # データは2000件程度(超えていればシャッフルには十分)
BATCH_SIZE = 32
EVAL_BATCH_SIZE = BATCH_SIZE * 2
EPOCHS = 1  # BERTを学習させるので激重

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = TFBertForSequenceClassification.from_pretrained('bert-base-uncased')

categories = ['tech', 'business']  # SST-2は二値分類タスクなのでカテゴリを絞る
# categories = ['tech', 'business', 'sport', 'entertainment', 'politics']
category_to_id = {
    category: index for index, category in enumerate(categories)
}
with open('preprocessed-bbc.csv') as fin:
    reader = csv.reader(fin)
    rows = [{
        'idx': index,
        'sentence': row[0],
        'label': category_to_id[row[1]],
    } for index, row in enumerate(reader) if row[1] in categories]

datasets = tf.data.Dataset.from_generator(
    lambda: rows,
    {'idx': tf.int64, 'sentence': tf.string, 'label': tf.int64}
)
datasets.shuffle(BUFFER_SIZE)

train_count = int(len(rows) * 0.8)
valid_count = len(rows) - train_count

train_dataset = datasets.skip(valid_count).take(train_count)
valid_dataset = datasets.take(valid_count)

train_dataset = glue_convert_examples_to_features(
    train_dataset, tokenizer, 128, 'sst-2')
valid_dataset = glue_convert_examples_to_features(
    valid_dataset, tokenizer, 128, 'sst-2')

train_dataset = train_dataset.batch(BATCH_SIZE)
valid_dataset = valid_dataset.batch(EVAL_BATCH_SIZE)

opt = tf.keras.optimizers.Adam(learning_rate=3e-5, epsilon=1e-08)
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')

model.compile(optimizer=opt, loss=loss, metrics=[metric])

train_steps = train_count // BATCH_SIZE  # 整数商を求める
valid_steps = valid_count // EVAL_BATCH_SIZE

history = model.fit(
    train_dataset,
    epochs=EPOCHS,
    steps_per_epoch=train_steps,
    validation_data=valid_dataset,
    validation_steps=valid_steps,
)

上記のコードに至る道

元のコードがtf.data.Datasetを使っているようだったので、1月に触ったときの記事を見返しながらもくもく。


まず詰まったのは、BertTokenizerでテキストを整数の並びにエンコードする処理をどこで適用するのかということ。
以下の記事(☆)を見つけ、glue_convert_examples_to_featuresというメソッドを使うことに気づきました。

ところがglue_convert_examples_to_featuresを使ってもエラー(TypeError: tuple indices must be integers or slices, not str)。
ソースを見たり、上記の記事を見返したりしたところ、原因は(1)指定するtaskの値と(2)tf.data.Datasetの形式と判明。

taskはサンプルコードの'mrpc'がテキスト分類ではなかったので、(☆)をもとに'sst-2'(テキストの2クラス分類)を設定7
その後、スタックトレースを元にコードを見たところ、tf.data.Datasetの形式が違う(辞書だ!)と気づきます。

class Sst2Processor(DataProcessor):
    """Processor for the SST-2 data set (GLUE version)."""

    def get_example_from_tensor_dict(self, tensor_dict):
        """See base class."""
        return InputExample(
            tensor_dict["idx"].numpy(),
            tensor_dict["sentence"].numpy().decode("utf-8"),
            None,
            str(tensor_dict["label"].numpy()),
        )

ref: transformers/glue.py at v2.5.1 · huggingface/transformers · GitHub

(☆)を見返して冒頭で辞書を作っていることを確認(読み飛ばしていました)、
from_generatorを使って辞書を元にtf.data.Datasetを作るコード(以下に引用)がドキュメントにあったと思い出し、それを適用。

# Each element is a dictionary mapping strings to `tf.Tensor` objects. 
elements =  ([{"a": 1, "b": "foo"}, 
              {"a": 2, "b": "bar"}, 
              {"a": 3, "b": "baz"}]) 
dataset = tf.data.Dataset.from_generator( 
    lambda: elements, {"a": tf.int32, "b": tf.string}) 

するとエラーがIndexError: list index out of rangeに変わります。
'sst-2'というタスク設定が2クラス分類だからではと思い至り、他クラス分類を断念
2つのカテゴリを選んで、2クラス分類を試します。
すると、

f:id:nikkie-ftnext:20200322213800p:plain

動いたあああああ!!🤗

$ python -i bert_classify.py
Train for 22 steps, validate for 2 steps
22/22 [==============================] - 302s 14s/step - loss: 0.3840 - accuracy: 0.8438 - val_loss: 0.1248 - val_accuracy: 0.9531

感想

ハマりながら粘ってようやく動かせた(学習させられた)BERTなのですが、学習がものすっっっっごく遅いです。😢
学習の進み具合を目にして、BERTで作った特徴量をもとにニューラルネットワークで学習するというアプローチがなぜ取られるのか悟りました。
BERTは優秀なモデルですが、よほどのマシンスペックがないと学習がボトルネックなのですね。
たしかにBERTを特徴量を作る役割で使ったほうがよさそうです(試行錯誤が繰り返せそうですね)。

TensorFlowからTFBertForSequenceClassificationを使って多クラス分類に取り組む事例を探しても全然見つけられなかったのですが、情報がないのは皆さんやらないからなんですね。
このアプローチは筋が悪かったです。

ただ、業務でやっていたと思うとゾッとする経験をしたということで、今週の取り組みに価値はあったと思います。
もし手元のマシンでBERTを学習させてみたいという方は、この記事のコードを手元で動かしてみてください 笑

週1ブログは3月末までとしていたので、来週でいったん最終回を迎えます。
来週のネタは迷っています。
NLTKで触りきれていないところもありますし、BERTで特徴量を作ってニューラルネットワークを学習する方法も知りたいですし。
「終わりよければ全てよし」となるよう、どちらかでまずアウトプットが来週の目標です。

おまけ

BERTのキャッチアップにはWantedlyさんのPodcastがよかったです。
BERTを中心に自然言語処理モデルについてバーっと解説してみた with agatan by Pod de Engineer • A podcast on Anchor


  1. この記事自体写経の題材でした。今回はトピックの違いから盛り込めなかったのですが、またの機会にアウトプット予定です

  2. ライブドアニュースデータセットのイギリス版といった趣ですね

  3. 元の記事にならって8つの処理を順番に適用しましたが、2〜8はpreprocess_stringメソッドにまとまっているようです。gensim恐ろしい子

  4. 導入にあたり、先日発売された『機械学習・深層学習による自然言語処理入門』の10章を参考にしました

  5. transformers.tokenization_bert — transformers 2.5.0 documentationPRETRAINED_VOCAB_FILES_MAP

  6. TensorFlow Blogに記事がありました:Hugging Face: State-of-the-Art Natural Language Processing in ten lines of TensorFlow 2.0 — The TensorFlow Blog

  7. 2018年の言語モデル概要 - LINE ENGINEERING の一覧にも助けられました