言語処理100本ノック(2020)-71: 単層ニューラルネットワークによる予測(TensorFlow)

[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)の[「第8章: ニューラルネット」](https://nlp100.github.io/ja/ch08.html)の[71本目「単層ニューラルネットワークによる予測」](https://nlp100.github.io/ja/ch08.html#71-%E5%8D%98%E5%B1%A4%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%88%E6%B8%AC)記録です。前回、TFRecord形式で保存したファイルを読み込み、ニューラルネットワークに流し込みます。やはり**TFRecord形式ファイルの扱いに手間取り**ました。この時点では訓練なしで流し込むのですが、そんな処理は初めて書きました。本当はSoftmaxとか解説すべきなのでしょうが、ググればいい記事がすぐに見つかるので解説しません。
[記事「まとめ: 言語処理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/)についても更新します。

# 参考リンク

|リンク|備考|
|:--|:--|
|[71_単層ニューラルネットワークによる予測.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/08.%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88/71_%E5%8D%98%E5%B1%A4%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%88%E6%B8%AC.ipynb)|回答プログラムのGitHubリンク|
|[言語処理100本ノック 2020 第8章: ニューラルネット](https://qiita.com/nymwa/items/9c8484ff511123e03ba8)|(PyTorchだけど)解き方の参考|
|[【言語処理100本ノック 2020】第8章: ニューラルネット](https://qiita.com/yamaru/items/b54989d772c4323bde69)|(PyTorchだけど)解き方の参考|
|[まとめ: 言語処理100本ノックで学べることと成果](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)|言語処理100本ノックまとめ記事|
|[TFRecords と tf.Example の使用法](https://www.tensorflow.org/tutorials/load_data/tfrecord?hl=ja)|今回の課題メイン|

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

|種類|バージョン|内容|
|:--|:--|:--|
|Python|3.7.12|Google Colaboratoryのバージョン|
|google|2.0.3|Google Driveのマウントに使用|
|tensorflow|2.6.0|ディープラーニングの主要処理|

# [第8章: ニューラルネット](https://nlp100.github.io/ja/ch08.html)
## 学習内容
> 深層学習フレームワークの使い方を学び，ニューラルネットワークに基づくカテゴリ分類を実装します．

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

### [71. 単層ニューラルネットワークによる予測](https://nlp100.github.io/ja/ch08.html#71-%E5%8D%98%E5%B1%A4%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%88%E6%B8%AC)
> 問題70で保存した行列を読み込み，学習データについて以下の計算を実行せよ．

>```math
\hat{\boldsymbol y_1} = {\rm softmax}(\boldsymbol x_1 W), \\
\hat{Y} = {\rm softmax}(X_{[1:4]} W)
```

> ただし，${\rm softmax}$はソフトマックス関数，$X_{[1:4]} \in \mathbb{R}^{4 \times d}$は特徴ベクトル$\boldsymbol x_1, \boldsymbol x_2, \boldsymbol x_3, \boldsymbol x_4$を縦に並べた行列である．

```math
X_{[1:4]} = \begin{pmatrix} 
  \boldsymbol x_1 \\ 
  \boldsymbol x_2 \\ 
  \boldsymbol x_3 \\ 
  \boldsymbol x_4 \\ 
\end{pmatrix}
```

> 行列$W \in \mathbb{R}^{d \times L}$は単層ニューラルネットワークの重み行列で，ここではランダムな値で初期化すればよい（問題73以降で学習して求める）．なお，$\hat{\boldsymbol y_1} \in \mathbb{N}^L$は未学習の行列$W$で事例$x_1$を分類したときに，各カテゴリに属する確率を表すベクトルである．
同様に，$\hat{Y} \in \mathbb{N}^{n \times L}$は，学習データの事例$x_1, x_2, x_3, x_4$について，各カテゴリに属する確率を行列として表現している．


# 回答
## 回答結果

```:結果(事例1件)
[[0.23627585 0.25216544 0.25520897 0.25634965]]
```

```:結果(事例集合4件)
[[0.23627585 0.25216544 0.25520897 0.25634965]
 [0.24906628 0.2570258  0.2513765  0.24253134]
 [0.2553226  0.24705489 0.24583162 0.25179094]
 [0.24834852 0.25343078 0.24328355 0.2549371 ]]
```

## 回答プログラム [71_単層ニューラルネットワークによる予測.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/08.%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88/71_%E5%8D%98%E5%B1%A4%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%88%E6%B8%AC.ipynb)
GitHubには確認用コードも含めていますが、ここには必要なものだけ載せています。

```python
import tensorflow as tf
from google.colab import drive

drive.mount('/content/drive')

BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/08.NeuralNetworks/'
train_raw = tf.data.TFRecordDataset(BASE_PATH+'train.tfrecord')

def _parse_function(example_proto):
    # 特徴の記述
    feature_description = {
        'title': tf.io.FixedLenFeature([], tf.string),
        'category': tf.io.FixedLenFeature([], tf.string)}
  
  # 上記の記述を使って入力の tf.Example を処理
    features = tf.io.parse_single_example(example_proto, feature_description)
    X = tf.io.decode_raw(features['title'], tf.float32)
    y = tf.io.decode_raw(features['category'], tf.int32)
    return X, y

dense_layer = tf.keras.layers.Dense(4, activation='softmax', use_bias=False, input_dim=300, kernel_initializer='random_uniform')

def output_result(batch_size):
    for X, y in train_raw.map(_parse_function).batch(batch_size).take(1):

        # 入力データ
        print('---入力データ---')
        print(X.numpy().shape) 
        print(y.numpy())  # 使わないけど確認だけする

        print('---出力データ---')
        print(dense_layer(X).numpy())


output_result(1)
output_result(4)
```

## 回答解説
### TFRecord形式ファイル読込
TFRecord形式ファイルの読込は以下のコード。

```python:
BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/08.NeuralNetworks/'
train_raw = tf.data.TFRecordDataset(BASE_PATH+'train.tfrecord')
print(train_raw)
```

このままだと、データとして使えないので形式変更にする必要あり。

```:結果
<TFRecordDatasetV2 shapes: (), types: tf.string>
```

parseするための関数を作ります。正直、面倒なのでデータ量が機械学習のボトルネックにならないのであれば、pickleやnumpy形式でファイルI/Oすべきです。

```python:
def _parse_function(example_proto):
    # 特徴の記述
    feature_description = {
        'title': tf.io.FixedLenFeature([], tf.string),
        'category': tf.io.FixedLenFeature([], tf.string)}
  
  # 上記の記述を使って入力の tf.Example を処理
    features = tf.io.parse_single_example(example_proto, feature_description)
    X = tf.io.decode_raw(features['title'], tf.float32)
    y = tf.io.decode_raw(features['category'], tf.int32)
    return X, y
```

で、`map`関数を使って先程定義した関数`_parse_function`を呼んで、batchとtakeでデータ取得の調整をします。

```python:
def output_result(batch_size):
    for X, y in train_raw.map(_parse_function).batch(batch_size).take(1):

        # 入力データ
        print('---入力データ---')
        print(X.numpy().shape) 
        print(y.numpy())  # 使わないけど確認だけする

        print('---出力データ---')
        print(dense_layer(X).numpy())
```

あとは、バッチサイズを渡して先に定義した関数を実行。

```python:
output_result(1)
output_result(4)
```

訓練などをしていない状態なので、1/4に近い数値がSoftmax結果として返ってきます。

```:結果
---入力データ---
(1, 300)
[[1 0 0 0]]
---出力データ---
[[0.23627585 0.25216544 0.25520897 0.25634965]]
---入力データ---
(4, 300)
[[1 0 0 0]
 [0 1 0 0]
 [0 0 0 1]
 [0 0 1 0]]
---出力データ---
[[0.23627585 0.25216544 0.25520897 0.25634965]
 [0.24906628 0.2570258  0.2513765  0.24253134]
 [0.2553226  0.24705489 0.24583162 0.25179094]
 [0.24834852 0.25343078 0.24328355 0.2549371 ]]
```

### softmax計算
以下のノック内容部分です。活性化関数`activation`をsoftmaxに、入力`input_dim`を300次元にしています。
`kernel_initializer`で*ランダムな値で初期化*をして、`use_bias`を`False`にすることでバイアス項をなくしています。バイアス項をなくしている理由は、ノック79本目に「*バイアス項の導入や多層化など，ニューラルネットワークの形状を変更しながら，高性能なカテゴリ分類器を構築せよ*」と書かれていて、ここではまだ入れてはいけないことに気づきました。

> 行列$W \in \mathbb{R}^{d \times L}$は単層ニューラルネットワークの重み行列で，ここではランダムな値で初期化すればよい

```python:
dense_layer = tf.keras.layers.Dense(
    4, activation='softmax', 
    use_bias=False, 
    input_dim=300, 
    kernel_initializer='random_uniform')
```