言語処理100本ノック(2020)-72: 損失と勾配の計算(TensorFlow)

[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)の[「第8章: ニューラルネット」](https://nlp100.github.io/ja/ch08.html)の[72本目「損失と勾配の計算」](https://nlp100.github.io/ja/ch08.html#72-%E6%90%8D%E5%A4%B1%E3%81%A8%E5%8B%BE%E9%85%8D%E3%81%AE%E8%A8%88%E7%AE%97)記録です。勾配計算はいつも`keras`に任せていたので、勾配の取得などしようと思ったこともなく、調べるのに時間がかかりました。本当は**`keras`を使わずに、自分でコーディングすることこそが良い学習なのでしょうが、取り急ぎ`keras`を使い続けます**。
[記事「まとめ: 言語処理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/)についても更新します。

# 参考リンク

|リンク|備考|
|:--|:--|
|[72_損失と勾配の計算.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/72_%E6%90%8D%E5%A4%B1%E3%81%A8%E5%8B%BE%E9%85%8D%E3%81%AE%E8%A8%88%E7%AE%97.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本ノックまとめ記事|
|[公式ガイド: モデルに関する勾配](https://www.tensorflow.org/guide/autodiff#gradients_with_respect_to_a_model)|勾配取得の公式ガイド|

# 環境
後々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などの機械学習プラットフォームを活用せよ．

### [72. 損失と勾配の計算](https://nlp100.github.io/ja/ch08.html#72-%E6%90%8D%E5%A4%B1%E3%81%A8%E5%8B%BE%E9%85%8D%E3%81%AE%E8%A8%88%E7%AE%97)
> 学習データの事例$x_1$と事例集合$x_1$,$x_2$,$x_3$,$x_4$に対して，クロスエントロピー損失と，行列$W$に対する勾配を計算せよ．なお，ある事例$x_i$に対して損失は次式で計算される．
>
$$l_i=−log[事例x_iがy_iに分類される確率]$$

> ただし，事例集合に対するクロスエントロピー損失は，その集合に含まれる各事例の損失の平均とする．

# 回答
## 回答結果
これ見ても何も判断できないですが、一応回答結果です。1行目がLossで、行列部分が勾配。

```:結果(事例1件)
1.3719598
dense_1/kernel:0, shape: (300, 4)
[[-0.06244335  0.02067989  0.02176488  0.01999857]
 [-0.00420518  0.00139266  0.00146573  0.00134678]
 [-0.00508651  0.00168454  0.00177292  0.00162904]
 ...
 [-0.05607251  0.01857001  0.0195443   0.0179582 ]
 [-0.03232378  0.01070494  0.01126659  0.01035225]
 [ 0.06478423 -0.02145514 -0.02258081 -0.02074828]]
```

```:結果(事例集合4件)
1.3873249
dense_1/kernel:0, shape: (300, 4)
[[-0.05536687  0.00082223  0.08464365 -0.03009901]
 [ 0.01126796 -0.01080388 -0.03230093  0.03183684]
 [-0.03271427  0.06947812 -0.0883422   0.05157837]
 ...
 [-0.09661584  0.08457799 -0.00822031  0.02025817]
 [-0.00410734  0.08742411  0.00315564 -0.08647241]
 [ 0.07903252  0.00211453  0.01354344 -0.09469048]]
```

## 回答プログラム [72_損失と勾配の計算.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/72_%E6%90%8D%E5%A4%B1%E3%81%A8%E5%8B%BE%E9%85%8D%E3%81%AE%E8%A8%88%E7%AE%97.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', 
    input_dim=300, 
    use_bias=False, 
    kernel_initializer='random_uniform')

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

        print('---y_true---')
        print(y_true.numpy())

        with tf.GradientTape() as tape:
            y_pred = dense_layer(X)
            print('---y_pred---')
            print(y_pred.numpy())
            loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)
            print('---loss---')
            print(tf.math.reduce_sum((loss.numpy()/batch_size)).numpy())

        grad = tape.gradient(loss, dense_layer.trainable_variables)
        for var, g in zip(dense_layer.trainable_variables, grad):
            print(f'{var.name}, shape: {g.shape}')
            print(g.numpy())

print_loss_gradients(1)
print_loss_gradients(4)
```

## 回答解説
### 勾配の記録
[`GradientTape`](https://www.tensorflow.org/api_docs/python/tf/GradientTape)を使って勾配を記録します。`with`内で記録するための計算を書きます。

```python:
with tf.GradientTape() as tape:
    y_pred = dense_layer(X)
    print('---y_pred---')
    print(y_pred.numpy())
    loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)
    print('---loss---')
    print(tf.math.reduce_sum((loss.numpy()/batch_size)).numpy())
```

`gradient`関数で勾配を入出力を渡して取得します。

```python:
grad = tape.gradient(loss, dense_layer.trainable_variables)
```

今回はバイアス項がないので`for`でまわしても1件しか取得出来ませんが、バイアス項があると複数回ループして取得できます。

```python:
        for var, g in zip(dense_layer.trainable_variables, grad):
            print(f'{var.name}, shape: {g.shape}')
            print(g.numpy())
```

1件のみ取得するとこんな出力です。

```:結果(事例1件)
---y_true---
[[1 0 0 0]]
---y_pred---
[[0.25360948 0.24718848 0.2601575  0.23904456]]
---loss---
1.3719598
dense_1/kernel:0, shape: (300, 4)
[[-0.06244335  0.02067989  0.02176488  0.01999857]
 [-0.00420518  0.00139266  0.00146573  0.00134678]
 [-0.00508651  0.00168454  0.00177292  0.00162904]
 ...
 [-0.05607251  0.01857001  0.0195443   0.0179582 ]
 [-0.03232378  0.01070494  0.01126659  0.01035225]
 [ 0.06478423 -0.02145514 -0.02258081 -0.02074828]]
```