使用する環境
- TensorFlow 2.1.0
- Python 3.6.9 (tensorflow/tensorflow:2.2.0rc0-gpu-py3-jupyter の docker image を使用。)
- ライブドアニュース
コンテナの起動
docker pull tensorflow/tensorflow:2.2.0rc0-gpu-py3-jupyter
# --runtime は、最新版では、 --gpus みたいな書き方になっているかもしれません。
docker run --runtime nvidia -d -p 8889:8888 -v $PWD:/tf/notebook tensorflow/tensorflow:2.2.0rc0-gpu-py3-jupyter
以下で実際に書いたコードの重要なところだけ抜粋し、解説を書いていきます。 実際に書いたコードの全体は、gistにおいておきます。
前提
articles という DataFrameの articles['body'] に、記事の本文が入っています。 また、 articles['label'] には、 0 ~ 8 までの数字が入っており、それぞれの記事の分類を表しています。
tokenize
まず、articles['body']
をtokenize します。
tokenize
した結果は、半角スペースで区切って articles['model_input']
に代入します。
from janome.tokenizer import Tokenizer
from janome.tokenfilter import *
from janome.analyzer import Analyzer
from tqdm import tqdm
tqdm.pandas()
tokenizer = Tokenizer()
token_filters = [POSKeepFilter(['名詞', '動詞', '形容詞']), TokenCountFilter()]
a = Analyzer(token_filters=token_filters)
def tokenize(text):
tokenized = []
for t in a.analyze(text):
tokenized.append(t[0])
return ' ' .join(tokenized)
articles['model_input'] = articles.body.progress_apply(tokenize)
articles.model_input.head(1)
# => 美しき ケイト・ベッキンセール ヴァンパイア 処刑 人 演じる 人気 シリーズ アンダー ワ...
今回は、 '名詞', '動詞', '形容詞'
だけを抽出しました。
Encode
次に、文字を数字に置き換えます。
train と validation に分割
最初に、train データと validation データを分割します。
出てくる文字を調べる
train データに、どんな文字が出てくるかを調べます。
tokens = []
def to_ts(x, tokens):
tokens += x.split(' ')
return x
_ = train.model_input.apply(lambda x: to_ts(x, tokens))
tokens #=> ['美しき', 'ケイト・ベッキンセール', 'ヴァンパイア', '処刑', '人', '演じる', '人気', ... ]
その後、それぞれの文字が何回出てきたかを数えます。
import collections
counter = collections.Counter(tokens)
str_to_num という dict を準備し、 文字列 -> 数字 の辞書を作ります。 この際、すべてのtoken に対して数字を振るのではなく、特定の回数以上出てきた文字に対してのみ数字を割り振ります。 こうすることで、未知語を取り扱うことができるようになります。
また、 num_to_str という、数字を単語になおす辞書も一緒に作っておきます(今回は分類問題なので使用しません)。 num_to_str[0] は、'UNK' という文字列に対応させたいので、その補正を最後の行で行っています。(今回は使用しませんが。。) また、後述する入力の長さ補正のために、 'PAD' という特殊な文字列も準備しておきます。
MIN_COUNT = 5
str_to_num = {
'UNK': 0,
'PAD': 1,
}
num = 1
for key in counter:
# 出現回数が MIN_COUNT 未満のものは UNK にする。
if counter[key] < MIN_COUNT:
str_to_num[key] = 0
continue
num += 1
str_to_num[key] = num
num_to_str = dict((v,k) for k,v in str_to_num.items())
num_to_str[0] = 'UNK'
# 怪文書
str_to_num['怪文書'], num_to_str[str_to_num['怪文書']]
モデルへの入力を作成
モデルへの入力を作成します。 手順は、train.model_input を str_to_num の辞書を作成 -> 数字の配列に変換 という感じです。 この際、入力が同じ長さになる必要があるので、長さが足りない文章についてはpadding していきます。 padding した箇所には、 'PAD' の文字に対応する数字、 1を入れていきます。
import copy
def encode(vec):
max_length = max(lengths)
z = [1 for _ in range(max_length)]
ary = []
for text in tqdmn(vec):
tmp = copy.deepcopy(z)
for i, t in enumerate(text):
if i >= max_length:
break
tmp[i] = t
ary.append(tmp)
return np.array(ary)
データセットの作成
ここまでできたら、データセットを作成します。
import tensorflow as tf
train_label = tf.keras.utils.to_categorical(
train.label.values, num_classes=9, dtype='float32'
)
ds_train = tf.data.Dataset.from_tensor_slices((encode(train_input_vec), train_label))
token_keys = set(str_to_num.keys())
バリデーションデータに対しても、同じように処理を行います。
# validationデータのEncode
validation_input_vec = validation.model_input.apply(lambda x: [str_to_num[y] if y in token_keys else 0 for y in x.split(' ')])
validation_label = tf.keras.utils.to_categorical(
validation.label.values, num_classes=9, dtype='float32'
)
ds_valid = tf.data.Dataset.from_tensor_slices((encode(validation_input_vec), validation_label))
作成したデータセットに対して、バッチサイズとシャッフルの設定を行います。
BATCH_SIZE=64
train_batch = ds_train.batch(BATCH_SIZE, drop_remainder=True).shuffle(len(train))
valid_batch = ds_valid.batch(BATCH_SIZE, drop_remainder=True)
学習
その後、モデルを作成します。 注意点は、
- tf.keras.layers.Embeddingの第一引数には作成したvocab_sizeを使う
- 9クラス分類なので、最後の出力は9
- 9クラス分類なので、ロス関数は CategoricalCrossentropy ということです。
vocab_size = len(num_to_str)
embedding_dim = 256
rnn_units = 1024
model = tf.keras.Sequential([
# vocab_size: 作成した vocab_size を使う
tf.keras.layers.Embedding(vocab_size, 64),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
tf.keras.layers.Dense(64, activation='relu'),
# 9クラス分類
tf.keras.layers.Dense(9, activation='softmax')
])
model.summary()
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-4),
# 9クラス分類なので、CategoricalCrossentropy を loss 関数とする。
loss=tf.keras.losses.CategoricalCrossentropy(),
metrics=['accuracy']
)
以上で、モデルの準備が終了しました。 あとは、先程作成したデータセットを model.fit() に入れてあげれば学習が始まります。
model.fit(train_batch, epochs=20, validation_data=valid_batch)
このあとは、パラメータのチューニングやモデル構造の変更 (Dense に Dropout を追加するとか)を行って、精度を高めていくことになります。 このへんで、先日発表された Keras Tuner が使えたりするんだろうなと踏んでいます。面白そうなので、そちらも記事にしようと思います。
以上で、RNN を使ってライブドアニュースをテキスト分類する事ができました。
Yasuaki Hamano
I'm a software engineer in Fukuoka, Japan. Recently, I am developing machine learning models using TensorFlow, and also developing Web services by using PHP.