はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
3/9の週はもろもろ締切が重なりやむなく断念。
お気づきでしょうか、自然言語処理ネタで週1ブログを週末にリリースしていないことに。
— nikkie 技書博のPython argparse本 boothにて頒布中 (@ftnext) 2020年3月16日
某日本語レビューや諸々の締切が重なり、やむなく見送ったのです。
今週は書こう。2本かけたらリカバリだけど無理せずに https://t.co/rkCnYCIsYi
3/8の記事で「次回はいまのNLPへのキャッチアップに踏み出す」ことにしていたので、今回はその続きでBERTを触りました。
BERTでテキスト分類をするのが今回手を動かす中での目標でした。
目次
- はじめに
- 目次
- 動作環境
- データセットと前処理
- TensorFlowからBERTを使う 🤗
- ハマった:TFBertForSequenceClassificationに多クラス分類をやらせるには?
- 感想
- おまけ
動作環境
$ 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
データセットと前処理
これは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
ドキュメント):
str.lower()
で小文字に揃えるgsp.strip_tags
でHTMLタグを除くgsp.strip_punctuation
でpunctuations(句読点)を空白に変えて除くgsp.strip_multiple_whitespaces
で繰り返された空白を除き、空白文字(タブや改行)を空白に揃えるgsp.strip_numeric
で整数・数値を除くgsp.remove_stopwords
でストップワードを除く(例えば ‘and’, ‘to’, ‘the’ などが除かれる)gsp.strip_short
で指定した長さ(デフォルトでは3)より短い語を除く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クラス分類を試します。
すると、
動いたあああああ!!🤗
$ 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
こちらのPodcastたしかに聞きやすいですね。移動中聞いてみてます
— nikkie 技書博のPython argparse本 boothにて頒布中 (@ftnext) 2020年3月16日
・BERTは1つのモデルで複数のタスク(Q&Aなど)が解けるNLPのゲームチェンジャー
・事前学習は大量のテキストが必要なだけ(アノテーション不要!)→解きたい問題のテキストでファインチューニング
「人間じゃん」←それな!😆 https://t.co/xXzhKZ0hv4
-
この記事自体写経の題材でした。今回はトピックの違いから盛り込めなかったのですが、またの機会にアウトプット予定です↩
-
元の記事にならって8つの処理を順番に適用しましたが、2〜8は
preprocess_string
メソッドにまとまっているようです。gensim
、恐ろしい子!↩ -
導入にあたり、先日発売された『機械学習・深層学習による自然言語処理入門』の10章を参考にしました↩
-
transformers.tokenization_bert — transformers 2.5.0 documentation の
PRETRAINED_VOCAB_FILES_MAP
↩ -
TensorFlow Blogに記事がありました:Hugging Face: State-of-the-Art Natural Language Processing in ten lines of TensorFlow 2.0 — The TensorFlow Blog↩
-
2018年の言語モデル概要 - LINE ENGINEERING の一覧にも助けられました↩