言語処理100本ノック(2020)-87: 確率的勾配降下法によるCNNの学習(Keras)

[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)の[「第9章: RNN, CNN」](https://nlp100.github.io/ja/ch09.html)の[87本目「確率的勾配降下法によるCNNの学習」](https://nlp100.github.io/ja/ch09.html#87-%E7%A2%BA%E7%8E%87%E7%9A%84%E5%8B%BE%E9%85%8D%E9%99%8D%E4%B8%8B%E6%B3%95%E3%81%AB%E3%82%88%E3%82%8Bcnn%E3%81%AE%E5%AD%A6%E7%BF%92)記録です。RNNに比べると精度が良くなり、高速化しています。RNNの方がNLPに対しては直感的に理解しやすいですし、精度が良いだろうと勘違いしていました。CNNすごい。
[記事「まとめ: 言語処理100本ノックで学べることと成果」](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)に[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)についてはまとめていますが、追加で差分の[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)についても更新します。

# 参考リンク

|リンク|備考|
|:--|:--|
|[87_確率的勾配降下法によるCNNの学習.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/09.RNN%2CCNN/87_%E7%A2%BA%E7%8E%87%E7%9A%84%E5%8B%BE%E9%85%8D%E9%99%8D%E4%B8%8B%E6%B3%95%E3%81%AB%E3%82%88%E3%82%8BCNN%E3%81%AE%E5%AD%A6%E7%BF%92.ipynb)|回答プログラムのGitHubリンク|
|[言語処理100本ノック 2020 第9章: RNN, CNN](https://qiita.com/nymwa/items/4542b45837a10766890b)|(PyTorchだけど)解き方の参考|
|[【言語処理100本ノック 2020】第9章: RNN, CNN](https://qiita.com/yamaru/items/6dbd1a44f5910d8848f6)|(PyTorchだけど)解き方の参考|
|[まとめ: 言語処理100本ノックで学べることと成果](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)|言語処理100本ノックまとめ記事|

# 環境
後々GPUを使わないと厳しいので、Google Colaboratory使いました。Pythonやそのパッケージでより新しいバージョンありますが、新機能使っていないので、プリインストールされているものをそのまま使っています。

|種類|バージョン|内容|
|:--|:--|:--|
|Python|3.7.12|Google Colaboratoryのバージョン|
|google|2.0.3|Google Driveのマウントに使用|
|tensorflow|2.7.0|ディープラーニングの主要処理|
|nltk|3.2.5|Tokenの辞書作成に使用|
|pandas|1.1.5|行列に関する処理に使用|

# [第8章: ニューラルネット](https://nlp100.github.io/ja/ch08.html)
## 学習内容
> 深層学習フレームワークを用い，再帰型ニューラルネットワーク（RNN）や畳み込みニューラルネットワーク（CNN）を実装します．

### [87. 確率的勾配降下法によるCNNの学習](https://nlp100.github.io/ja/ch09.html#87-%E7%A2%BA%E7%8E%87%E7%9A%84%E5%8B%BE%E9%85%8D%E9%99%8D%E4%B8%8B%E6%B3%95%E3%81%AB%E3%82%88%E3%82%8Bcnn%E3%81%AE%E5%AD%A6%E7%BF%92)
> 確率的勾配降下法（SGD: Stochastic Gradient Descent）を用いて，問題86で構築したモデルを学習せよ．訓練データ上の損失と正解率，評価データ上の損失と正解率を表示しながらモデルを学習し，適当な基準（例えば10エポックなど）で終了させよ．

# 回答
## 回答結果
[85本目ノック(RNN)](https://qiita.com/FukuharaYohei/items/a814e582e7aee9a9a10e)との精度比較です。**全データセットに対して精度が多少改善しています**。

|データセット|Loss|正答率|
|:-:|:-:|:-:|
|訓練|0.374 :arrow_right: 0.332(-0.42)|86.8% :arrow_right: 88.7%(+0.9%)|
|検証|0.458 :arrow_right: 0.455(-0.03)|84.2% :arrow_right: 84.4%(+0.2%)|
|評価|0.423 :arrow_right: 0.420(-0.03)|85.3% :arrow_right: 86.1%(+0.8%)|

参考に評価データセットの結果です。

```:結果
> model.evaluate(X_test, y_test)
42/42 [==============================] - 0s 7ms/step - loss: 0.4202 - acc: 0.8608
[0.4201797544956207, 0.8607784509658813]
```

また、訓練時間が5分42秒から1分53秒へと短縮。やはりCNN速いですね。

## 回答プログラム [87_確率的勾配降下法によるCNNの学習.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/09.RNN%2CCNN/87_%E7%A2%BA%E7%8E%87%E7%9A%84%E5%8B%BE%E9%85%8D%E9%99%8D%E4%B8%8B%E6%B3%95%E3%81%AB%E3%82%88%E3%82%8BCNN%E3%81%AE%E5%AD%A6%E7%BF%92.ipynb)
GitHubには確認用コードも含めていますが、ここには必要なものだけ載せています。

```python
import numpy as np
import nltk
from gensim.models import KeyedVectors
import pandas as pd
import tensorflow as tf
from google.colab import drive

drive.mount('/content/drive')

BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/'
max_len = 0
vocabulary = []
w2v_model = KeyedVectors.load_word2vec_format(BASE_PATH+'07.WordVector/input/GoogleNews-vectors-negative300.bin.gz', binary=True)

def read_dataset(type_):
    global max_len
    global vocabulary
    df = pd.read_table(BASE_PATH+'06.MachineLearning/'+type_+'.feature.txt')
    df.info()
    sr_title = df['title'].str.split().explode()
    max_len_ = df['title'].map(lambda x: len(x.split())).max()
    if max_len < max_len_:
        max_len = max_len_
    if len(vocabulary) == 0:
        vocabulary = [k for k, v in nltk.FreqDist(sr_title).items() if v > 1]
    else:
        vocabulary.extend([k for k, v in nltk.FreqDist(sr_title).items() if v > 1])
    y = df['category'].replace({'b':0, 't':1, 'e':2, 'm':3})
    return df['title'], tf.keras.utils.to_categorical(y, dtype='int32')  # 4値分類なので訓練・検証・テスト共通でone-hot化


X_train, y_train = read_dataset('train')
X_valid, y_valid = read_dataset('valid')
X_test, y_test = read_dataset('test') # あまりこだわらずにテストデータセットも追加

# setで重複削除し、タプル形式に設定
tup_voc = tuple(set(vocabulary))

print(f'vocabulary size before removing duplicates: {len(vocabulary)}')
print(f'vocabulary size after removing duplicates: {len(tup_voc)}')
print(f'sample vocabulary: {tup_voc[:10]}')
print(f'max length is {max_len}')

vectorize_layer = tf.keras.layers.TextVectorization(
 output_mode='int',
 vocabulary=tup_voc,
 output_sequence_length=max_len)

print(f'vocabulary size is {vectorize_layer.vocabulary_size()}')

embedding_dim = 300
hits = 0
misses = 0

embedding_matrix = np.zeros((vectorize_layer.vocabulary_size(), embedding_dim))
for i, word in enumerate(vectorize_layer.get_vocabulary()):
    try:
        embedding_matrix[i] = w2v_model.get_vector(word)
        hits += 1
        # Words not found in embedding index will be all-zeros.
        # This includes the representation for "padding" and "OOV"
    except:
        misses += 1
        if misses < 7:  # Show 6 words as example
            print(word)

embedding_layer = tf.keras.layers.Embedding(
    vectorize_layer.vocabulary_size(),
    embedding_dim,
    embeddings_initializer=tf.keras.initializers.Constant(embedding_matrix),
    trainable=False
)


model = tf.keras.models.Sequential()
model.add(tf.keras.Input(shape=(1,), dtype=tf.string))
model.add(vectorize_layer)
model.add(embedding_layer)
model.add(tf.keras.layers.Reshape((max_len * embedding_dim, 1)))
model.add(tf.keras.layers.Conv1D(filters=8, 
                                 kernel_size=embedding_dim*3, 
                                 padding='same',
                                 strides=embedding_dim,
                                 activation='relu'))
model.add(tf.keras.layers.MaxPooling1D(pool_size=2))
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(4, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['acc'])

model.fit(X_train, y_train, epochs=100, validation_data=(X_valid, y_valid),
          callbacks=[tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=False)])

model.evaluate(X_test, y_test)
```

## 回答解説
前回ノックでCNN部分の解説したので、[記事「自然言語処理における畳み込みニューラルネットワークを理解する」](https://tkengo.github.io/blog/2016/03/11/understanding-convolutional-neural-networks-for-nlp/)を見て、気になった点のメモのみです。

### 文字単位での学習
Tokenではなく文字単位で学習するのも有効らしい。

> [14] では文字レベル (character-level) で埋め込み表現を学習し、それらを事前学習した単語埋め込み表現と結合し、品詞タグ付けをするために CNN を使っています。[15] と [16] では、事前学習された埋め込み表現を使わずに、文字から直接学習する CNN を説明しています。とりわけ著者は、全部で 9 個の層を持つ比較的深いネットワークを感情分析やテキスト分類に適用しています。大規模なデータセット (100万件程度のデータ) を使えば、文字レベルの入力を直接学習することで良い結果が得られることがわかっていますが、小規模なデータセット (数千件程度のデータ) しかない場合は単純なモデルにも負けてしまいます。