TensorFlowのクロスエントロピー関数の動作

# はじめに
TensorFlowでは多クラス分類をするためにクロスエントロピーを計算する関数が用意されていますが、内部でソフトマックス関数を実行しているため、動作がわかりにくくなっています。サンプルコードを見れば使い方はなんとなくわかりますが、自分の理解があっているのか確認したかったので、そのためのコードを書いてみました。
（この記事はTensorFlow ver 1.x向けに書かれた記事です。ver 2.xでは変更されている可能性があります。）

# ソフトマックス関数
ソフトマックス関数はニューラルネットワークが出力する値$x_i$を確率に変更する活性化関数の一種で次のように定義されます。ただし、分類するクラス数を$K$として、ニューラルネットワークは$x_1,x_2,...,x_K$の値を出力しているものとします。

```math
softmax(x_i) := \frac{\exp(x_i)}{\sum_{k=1}^K \exp(x_k)}
```

ソフトマックス関数の出力はすべて$0$から$1$の範囲に収まり、合計は$1$になります。

多クラス分類ではニューラルネットワークの出力をこの関数に入力して得られた結果を分類の推定確率とします。


# クロスエントロピー
クロスエントロピーは多クラス分類でよく使われる誤差関数の一つで、$d$番目のデータに対して、クラス$i$である推定確率$x_{d,i}$と真の確率$y_{d,i}$の誤差を計算します。
定義は次のようになります。

```math
crossentropy(\boldsymbol{x}, \boldsymbol{y}) := - \sum_{d=1}^D\sum_{i=1}^K y_{d,i} \log(x_{d,i})
```

ディープラーニングの多クラス分類では$x_{d,i}$にソフトマックス関数の出力を、$y_{d,i}$に教師データをそれぞれ入力します。

# TensorFlowでは
ソフトマックス関数は`tf.nn.softmax`という関数で計算できます。

クロスエントロピーについてはTensorFlowでは単体の関数は用意されていないようなので、ソフトマックス関数とクロスエントロピーをセットにした、`tf.nn.softmax_cross_entropy_with_logits_v2`や`tf.nn.sparse_softmax_cross_entropy_with_logits`を使います。"with_logits"というのは、この関数は内部でソフトマックスも計算するから、ニューラルネットワークの出力をそのまま入力してね、という意味です。
（古いバージョンでは`_v2`という名前ではなかったのですが、最新版では`_v2`のみ残っています。また、`sparse_`で始まる方は`_v2`はないようです。）

注意点として、TensorFlowの関数はクロスエントロピーの式の２つある$\sum$のクラスに対する和$\sum_{i=1}^K$しか計算しないので、バッチ内データの和$\sum_{d=1}^D$は自分で計算する必要があり、reduce_sumを使うか、tf.lossesに定義されている関数を使うことで計算できます。

`tf.nn.softmax_cross_entropy_with_logits_v2`や`tf.nn.sparse_softmax_cross_entropy_with_logits`は教師データのデータ形式で使い分けます。
前者の教師データにはone hot（一つだけ1で他は0の配列）や各クラスに属する確率の配列を使用します。
後者は教師データとしてクラスのインデックスを整数で指定します。

# 確認用コード
TensorFlowでクロスエントロピーを計算するコードを書いてみました。クロスエントロピーを手動で計算する方法(`y1`)と`tf.nn.softmax_cross_entropy_with_logits_v2`(`y2`)、`tf.nn.sparse_softmax_cross_entropy_with_logits`(`y3`)で結果を比較しています。

下のコードでは`x`が実際のニューラルネットワークの出力（ソフトマックス関数などの活性化関数を通す前）の値になります。
`label`が`softmax_cross_entropy_with_logits`用の教師データでクラス数と同じ数の要素を持つ配列になります。要素の値はそれぞれの確率になります。
`sparse_label`は`sparse_softmax_cross_entropy_with_logits`用の教師データで一番確率の高いクラスのインデックスを指定します。

`y1`と`y2`は同じ結果になります。（多少の誤差があるようですが。）
`y2`と`y3`については、`y2`用の教師データがone hotの時は同じ結果になり、確率分布で与えた場合は異なる結果になります。

TensorFlowの`softmax_cross_entropy_with_logits`のリファレンスにも、教師データが一つのクラスだけを指定する場合は`sparse_softmax_cross_entropy_with_logits`が使えるよ、みたいなことが書いてあります。

```py3
import tensorflow as tf

### グラフ定義
x = tf.placeholder(tf.float32, [None,3])
label = tf.placeholder(tf.float32, [None,3])
sparse_label = tf.placeholder(tf.int32, [None])

# 個別に計算
#   xをソフトマックスしてからlabelとのクロスエントロピーを計算
soft = tf.nn.softmax(x)                            # ソフトマックス: exp(x) / sum(exp(x))
y1 = - tf.reduce_sum(tf.log(soft) * label, axis=1) # クロスエントロピー（クラス方向の和のみ計算）

# softmax_cross_entropy_with_logits
#   ソフトマックスとクロスエントロピーを同時に実行してくれる
#   labelは確率分布（one hotでなくてもよい）
# y2 = tf.nn.softmax_cross_entropy_with_logits(labels=label, logits=x) # 古いバージョン
y2 = tf.nn.softmax_cross_entropy_with_logits_v2(labels=label, logits=x)

# sparse_softmax_cross_entropy_with_logits
#   ソフトマックスとクロスエントロピーを同時に実行してくれる
#   labelは分類先のインデックスを指定する
y3 = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=sparse_label, logits=x)

### 計算実行
sess = tf.Session()
x_ = [
    [6,5,4],
    [2,5,4],
    [3,1,6],
    [3,1,6]
]
l = [
    [1,0,0],
    [0,1,0],
    [0,0,1],
    [0.3,0.1,0.6]
]
sl = [
    0,
    1,
    2,
    2
]
y = sess.run([y1, y2, y3], feed_dict={x: x_, label: l, sparse_label: sl})

# y1とy2は同じ値になる
# y2とy3はlabelがone hotの時に同じ値になる
for i in range(len(y[0])):
    print('y1: %.8f, y2: %.8f, y3: %.8f' % (y[0][i], y[1][i], y[2][i]))
```

結果

```
y1: 0.40760601, y2: 0.40760595, y3: 0.40760595
y1: 0.34901217, y2: 0.34901217, y3: 0.34901217
y1: 0.05498519, y2: 0.05498521, y3: 0.05498521
y1: 1.45498538, y2: 1.45498538, y3: 0.05498521
```

# 平均化するための関数
`tf.nn.softmax_cross_entropy_with_logits`や`tf.nn.sparse_softmax_cross_entropy_with_logits`は各データのクロスエントロピーを出力するので、学習時の損失関数としてはバッチ内の各データのクロスエントロピーの平均を取る必要があります。
tf.**losses**にはそのための関数が用意されており、それぞれ
`tf.losses.softmax_cross_entropy`と`tf.losses.sparse_softmax_cross_entropy`が対応します。（with_logitsがなくなっていることに注意）