Fusic Tech Blog

Fusion of Society, IT and Culture

[TensorFlow]RNN を使ってライブドアニュースをテキスト分類する
2020/03/21

[TensorFlow]RNN を使ってライブドアニュースをテキスト分類する

こちらのチュートリアルを、日本語データを使ってやってみるという記事です。 https://www.tensorflow.org/tutorials/text/text_classification_rnn?hl=ja

チュートリアルでは、

tfds.load('imdb_reviews/subwords8k', with_info=True, as_supervised=True)

のように、予めTensorFlow が準備しているデータを使う形式になっており、 load メソッドを呼ぶと、データセットとEncoder が取得できるようになっています。

このチュートリアルでは、データセットの作成は本筋ではないので tfds.load()を使うと良いと思いますが、 いざ日本語のデータセットを入力してみたい と思ったときに困ったので、その解決策を書いていきます。

参考にしたのは、RNN によるテキスト生成 という別のチュートリアルです。

こちらのチュートリアルにある方法を日本語に読み替えてデータセットを作成し、 日本語のデータセットについてRNNでテキスト分類を行っていきます。

使用する環境

- 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 に対して数字を振るのではなく、特定の回数以上出てきた文字に対してのみ数字を割り振ります。 こうすることで、未知語を取り扱うことができるようになります。

str_to_numの説明

また、 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)

学習

その後、モデルを作成します。 注意点は、

  1. tf.keras.layers.Embeddingの第一引数には作成したvocab_sizeを使う
  2. 9クラス分類なので、最後の出力は9
  3. 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 を使ってライブドアニュースをテキスト分類する事ができました。

hamano

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.