ギークなエンジニアを目指す男

機械学習系の知識を蓄えようとするブログ

【言語処理100本ノック 2020】 8章をPythonで解いた(TensorFlowを使用)

f:id:taxa_program:20200502163654p:plain

こんにちは。takapy(@takapy0210)です。

本エントリは言語処理100本ノック2020の8章を解いてみたので、それの備忘です。
簡単な解説をつけながら紹介していきます。

ネット上に掲載されている解答例はPytorchによる解法が多かったので、TensorFlowを用いて解いてみました。

nlp100.github.io

コードはGithubに置いてあります。

github.com

第8章: ニューラルネット

第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ

70. 単語ベクトルの和による特徴量

SWEMを用いて単語の平均ベクトルを計算しています.
SWEMのコード部分にはこちらのGithubに掲載しています.

"""
70. 単語ベクトルの和による特徴量
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.
i番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものがxiである.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.
以下の行列・ベクトルを作成し,ファイルに保存せよ.
学習データの特徴量行列: Xtrain∈ℝNt×d
学習データのラベルベクトル: Ytrain∈ℕNt
検証データの特徴量行列: Xvalid∈ℝNv×d
検証データのラベルベクトル: Yvalid∈ℕNv
評価データの特徴量行列: Xtest∈ℝNe×d
評価データのラベルベクトル: Ytest∈ℕNe
"""

import pandas as pd
from gensim.models import KeyedVectors
import texthero as hero

from swem import SWEM


def load_data() -> dict:
    """データの読み込み"""
    # 読み込むファイルを定義
    inputs = {
        'train': '../chapter6/train.txt',
        'valid': '../chapter6/valid.txt',
        'test': '../chapter6/test.txt',
    }
    dfs = {}
    use_cols = ['title', 'category']
    for k, v in inputs.items():
        dfs[k] = pd.read_csv(v, sep='\t')
        dfs[k] = dfs[k][use_cols]

    return dfs


def preprocess(text) -> str:
    """前処理"""
    clean_text = hero.clean(text, pipeline=[
        hero.preprocessing.fillna,
        hero.preprocessing.lowercase,
        hero.preprocessing.remove_digits,
        hero.preprocessing.remove_punctuation,
        hero.preprocessing.remove_diacritics,
        hero.preprocessing.remove_stopwords
    ])

    return clean_text


if __name__ == "__main__":

    # chapter6で生成したデータを読み込む
    dfs = load_data()

    # 事前学習済みモデルのロード
    # ref. https://radimrehurek.com/gensim/models/word2vec.html#usage-examples
    model = KeyedVectors.load_word2vec_format('../chapter7/GoogleNews-vectors-negative300.bin.gz', binary=True)

    # 前処理
    dfs['train']['title'] = dfs['train'][['title']].apply(preprocess)
    dfs['valid']['title'] = dfs['valid'][['title']].apply(preprocess)
    dfs['test']['title'] = dfs['test'][['title']].apply(preprocess)

    # 説明変数の生成(SWEMの計算)
    swem = SWEM(model)
    X_train = swem.calculate_emb(df=dfs['train'], col='title', window=3, swem_type=1)
    X_valid = swem.calculate_emb(df=dfs['valid'], col='title', window=3, swem_type=1)
    X_test = swem.calculate_emb(df=dfs['test'], col='title', window=3, swem_type=1)

    # 目的変数の生成
    y_train = dfs['train']['category'].map({'b': 0, 'e': 1, 't': 2, 'm': 3})
    y_valid = dfs['valid']['category'].map({'b': 0, 'e': 1, 't': 2, 'm': 3})
    y_test = dfs['test']['category'].map({'b': 0, 'e': 1, 't': 2, 'm': 3})

    # 保存
    X_train.to_pickle('X_train.pkl')
    X_valid.to_pickle('X_valid.pkl')
    X_test.to_pickle('X_test.pkl')
    y_train.to_pickle('y_train.pkl')
    y_valid.to_pickle('y_valid.pkl')
    y_test.to_pickle('y_test.pkl')

71. 単層ニューラルネットワークによる予測

TensorFlowを用いて、単層ニューラルネットワークを構築し、指示された内容を計算しています.

"""
71. 単層ニューラルネットワークによる予測
問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.

ŷ 1=softmax(x1W),Ŷ =softmax(X[1:4]W)
ただし,softmaxはソフトマックス関数,X[1:4]∈ℝ4×dは特徴ベクトルx1,x2,x3,x4を縦に並べた行列である.

X[1:4]=⎛⎝⎜⎜⎜⎜x1x2x3x4⎞⎠⎟⎟⎟⎟
行列W∈ℝd×Lは単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).
なお,ŷ 1∈ℝLは未学習の行列Wで事例x1を分類したときに,各カテゴリに属する確率を表すベクトルである.
同様に,Ŷ ∈ℝn×Lは,学習データの事例x1,x2,x3,x4について,各カテゴリに属する確率を行列として表現している.
"""

import pandas as pd
import tensorflow as tf


class SimpleNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        output_layer = self.output(input_layer)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    X_train = pd.read_pickle('X_train.pkl')
    model = SimpleNet(X_train.shape[1], 4).build()

    print(model(X_train.values[:1]))
    print(model(X_train.values[:4]))

実行結果

tf.Tensor([[0.2661007  0.25712514 0.2329659  0.2438082 ]], shape=(1, 4), dtype=float32)
tf.Tensor(
[[0.2661007  0.25712514 0.23296592 0.2438082 ]
 [0.27437785 0.25097498 0.23388673 0.24076048]
 [0.27228996 0.25715688 0.234876   0.23567708]
 [0.27745858 0.25357178 0.22825074 0.24071899]], shape=(4, 4), dtype=float32)

72. 損失と勾配の計算

損失の計算にはtf.keras.losses.CategoricalCrossentropy()を使っています.

"""
72. 損失と勾配の計算
学習データの事例x1と事例集合x1,x2,x3,x4に対して,クロスエントロピー損失と,行列Wに対する勾配を計算せよ.なお,ある事例xiに対して損失は次式で計算される.

li=−log[事例xiがyiに分類される確率]
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.
"""

import pandas as pd
import tensorflow as tf
from tensorflow.keras.utils import to_categorical


class SimpleNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        output_layer = self.output(input_layer)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    # データのロード
    X_train = pd.read_pickle('X_train.pkl')
    y_train = pd.read_pickle('y_train.pkl')

    # モデル構築
    model = SimpleNet(X_train.shape[1], len(y_train.unique())).build()
    preds = model(X_train.values[:4])

    # 目的変数をone-hotに変換
    y_true = to_categorical(y_train)
    y_true = y_true[:4]

    # 計算
    cce = tf.keras.losses.CategoricalCrossentropy()
    print(cce(y_true, preds.numpy()).numpy())

実行結果

1.4818511

73. 確率的勾配降下法による学習

ラベルはone-hotに変換していないので、lossにはSparseCategoricalCrossentropy()を用いています.

"""
73. 確率的勾配降下法による学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列Wを学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など)
"""

import pandas as pd
import tensorflow as tf


class SimpleNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        output_layer = self.output(input_layer)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    # データのロード
    X_train = pd.read_pickle('X_train.pkl')
    y_train = pd.read_pickle('y_train.pkl')

    # モデル構築
    model = SimpleNet(X_train.shape[1], len(y_train.unique())).build()
    opt = tf.optimizers.SGD()
    model.compile(
        optimizer=opt,
        loss=tf.keras.losses.SparseCategoricalCrossentropy()
    )

    # 学習
    tf.keras.backend.clear_session()
    model.fit(
        X_train,
        y_train,
        epochs=50,
        batch_size=32,
        verbose=1
    )

    # モデルの保存
    model.save("tf_model.h5")

実行結果

Epoch 1/50
334/334 [==============================] - 0s 1ms/step - loss: 1.1319
Epoch 2/50
334/334 [==============================] - 0s 1ms/step - loss: 1.1315
...

Epoch 48/50
334/334 [==============================] - 0s 1ms/step - loss: 1.1124
Epoch 49/50
334/334 [==============================] - 1s 2ms/step - loss: 1.1121
Epoch 50/50
334/334 [==============================] - 0s 1ms/step - loss: 1.1118

74. 正解率の計測

推論結果に関しては、そのままだと各クラスの確率が返却されるので、np.argmaxで一番確率の高いクラスを取得して正解率を計算しています.

"""
74. 正解率の計測
問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.
"""

import pandas as pd
import numpy as np
import tensorflow as tf
from sklearn.metrics import accuracy_score


class SimpleNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        output_layer = self.output(input_layer)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    # データのロード
    X_train = pd.read_pickle('X_train.pkl')
    y_train = pd.read_pickle('y_train.pkl')
    X_valid = pd.read_pickle('X_valid.pkl')
    y_valid = pd.read_pickle('y_valid.pkl')

    # モデルのロード
    model = tf.keras.models.load_model("tf_model.h5")

    # 推論
    y_train_preds = model.predict(X_train, verbose=1)
    y_valid_preds = model.predict(X_valid, verbose=1)

    # 一番確率の高いクラスを取得
    y_train_preds = np.argmax(y_train_preds, 1)
    y_valid_preds = np.argmax(y_valid_preds, 1)

    # 正解率を出力
    print(f'Train Accuracy: {accuracy_score(y_train, y_train_preds)}')
    print(f'Valid Accuracy: {accuracy_score(y_valid, y_valid_preds)}')

実行結果

334/334 [==============================] - 0s 695us/step
42/42 [==============================] - 0s 680us/step
Train Accuracy: 0.5493815592203898
Valid Accuracy: 0.5374812593703149

75. 損失と正解率のプロット / 76. チェックポイント / 77. ミニバッチ化

3つ一気に実装しています.

"""
75. 損失と正解率のプロット
問題73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,検証データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.

76. チェックポイント
問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.

77. ミニバッチ化
問題76のコードを改変し,B事例ごとに損失・勾配を計算し,行列Wの値を更新せよ(ミニバッチ化).Bの値を1,2,4,8,…と変化させながら,1エポックの学習に要する時間を比較せよ.
"""

import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf


class SimpleNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        output_layer = self.output(input_layer)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    # データのロード
    X_train = pd.read_pickle('X_train.pkl')
    y_train = pd.read_pickle('y_train.pkl')

    # モデル構築
    model = SimpleNet(X_train.shape[1], len(y_train.unique())).build()
    opt = tf.optimizers.SGD()
    model.compile(
        optimizer=opt,
        loss=tf.keras.losses.SparseCategoricalCrossentropy(),
        metrics=['accuracy']
    )

    # チェックポイント
    checkpoint_path = 'ck_tf_model.h5'
    cb_checkpt = tf.keras.callbacks.ModelCheckpoint(
        checkpoint_path,
        monitor='loss',
        save_best_only=True,
        mode='min',
        verbose=1
    )
    # 学習
    tf.keras.backend.clear_session()
    history = model.fit(
        X_train,
        y_train,
        epochs=100,
        batch_size=32,
        callbacks=[cb_checkpt],
        verbose=1
    )

    # 学習曲線の保存
    pd.DataFrame(history.history).plot(figsize=(10, 6))
    plt.grid(True)
    plt.savefig("learning_curves.png")

実行結果

Epoch 1/100
334/334 [==============================] - 1s 918us/step - loss: 1.2599 - accuracy: 0.4254

Epoch 00001: loss improved from inf to 1.21570, saving model to ck_tf_model.h5
Epoch 2/100
334/334 [==============================] - 0s 968us/step - loss: 1.1667 - accuracy: 0.4256

Epoch 00002: loss improved from 1.21570 to 1.16557, saving model to ck_tf_model.h5
...

Epoch 00098: loss improved from 1.11229 to 1.11196, saving model to ck_tf_model.h5
Epoch 99/100
334/334 [==============================] - 0s 1ms/step - loss: 1.1190 - accuracy: 0.5538

Epoch 00099: loss improved from 1.11196 to 1.11144, saving model to ck_tf_model.h5
Epoch 100/100
334/334 [==============================] - 0s 1ms/step - loss: 1.1135 - accuracy: 0.5518

Epoch 00100: loss improved from 1.11144 to 1.11131, saving model to ck_tf_model.h5

f:id:taxa_program:20210703014541p:plain
学習曲線

79. 多層ニューラルネットワーク

単層のときより、若干スコアが改善しました.

"""
79. 多層ニューラルネットワーク

問題78のコードを改変し,バイアス項の導入や多層化など,ニューラルネットワークの形状を変更しながら,高性能なカテゴリ分類器を構築せよ.
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from sklearn.metrics import accuracy_score


class MLPNet:

    def __init__(self, feature_dim, target_dim):
        self.input = tf.keras.layers.Input(shape=(feature_dim), name='input')
        self.hidden1 = tf.keras.layers.Dense(128, activation='relu', name='hidden1')
        self.hidden2 = tf.keras.layers.Dense(32, activation='relu', name='hidden2')
        self.dropout = tf.keras.layers.Dropout(0.2, name='dropout')
        self.output = tf.keras.layers.Dense(target_dim, activation='softmax', name='output')

    def build(self):
        input_layer = self.input
        hidden1 = self.hidden1(input_layer)
        dropout1 = self.dropout(hidden1)
        hidden2 = self.hidden2(dropout1)
        dropout2 = self.dropout(hidden2)
        output_layer = self.output(dropout2)
        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
        return model


if __name__ == "__main__":

    # データのロード
    X_train = pd.read_pickle('X_train.pkl')
    y_train = pd.read_pickle('y_train.pkl')
    X_valid = pd.read_pickle('X_valid.pkl')
    y_valid = pd.read_pickle('y_valid.pkl')

    # モデル構築
    model = MLPNet(X_train.shape[1], len(y_train.unique())).build()
    opt = tf.optimizers.SGD()
    model.compile(
        optimizer=opt,
        loss=tf.keras.losses.SparseCategoricalCrossentropy(),
        metrics=['accuracy']
    )

    # チェックポイント
    checkpoint_path = 'ck_tf_model.h5'
    cb_checkpt = tf.keras.callbacks.ModelCheckpoint(
        checkpoint_path,
        monitor='loss',
        save_best_only=True,
        mode='min',
        verbose=1
    )
    # 学習
    tf.keras.backend.clear_session()
    history = model.fit(
        X_train,
        y_train,
        epochs=100,
        batch_size=32,
        callbacks=[cb_checkpt],
        verbose=1
    )

    # 推論
    y_train_preds = model.predict(X_train, verbose=1)
    y_valid_preds = model.predict(X_valid, verbose=1)

    # 一番確率の高いクラスを取得
    y_train_preds = np.argmax(y_train_preds, 1)
    y_valid_preds = np.argmax(y_valid_preds, 1)

    # 正解率を出力
    print(f'Train Accuracy: {accuracy_score(y_train, y_train_preds)}')
    print(f'Valid Accuracy: {accuracy_score(y_valid, y_valid_preds)}')

    # 学習曲線の保存
    pd.DataFrame(history.history).plot(figsize=(10, 6))
    plt.grid(True)
    plt.savefig("learning_curves.png")

実行結果

...

Train Accuracy: 0.5812406296851574
Valid Accuracy: 0.5704647676161919

f:id:taxa_program:20210703015418p:plain
学習曲線