【ディープラーニング入門（第3回）】ニューラルネットワークの出力層を理解して実装しよう

# 本連載記事の概要

本連載記事では、全5回でDeepLearning(深層学習)をゼロから実装し、学ぶことを目的としています。

「深層学習」はなんとなく知っているけど使えない方、Pythonで動かせるけど中身がブラックボックスになっていて理解できていない方が、実際の実装を通してプログラムがどういう仕組みで動いているか理解でき、使いこなせるようになることを目指します。

# 連載記事目次
第1回：[入力層・中間層](https://qiita.com/kwi0303/items/e494eeddac4998df2d03)
第2回：[活性化関数](https://qiita.com/kwi0303/items/4fcab8161657ee1550b1)
第3回：出力層（本記事）
第4回：[勾配降下法](https://qiita.com/kwi0303/items/7bfd7180f80a52296e64)
第5回：[誤差逆伝播法](https://qiita.com/kwi0303/items/e43efa6657ff4b6b59ad)

# 目次
[1. 出力層の概要](#1-出力層の概要)
[2. 出力層の活性化関数](#2-出力層の活性化関数)
[3. 誤差関数（損失関数）](#3-誤差関数（損失関数）)
[4. Pythonによる実装](#4-Pythonによる実装)
[5. おわりに](#5-おわりに)

#1. 出力層の概要

ニューラルネットワークは、分類問題と回帰問題に利用されます。分類問題とは、データがどのクラスに属するかを判定する問題で、数字の分類などに利用されます。一方回帰問題は、ある入力から出力の数値の予測を行う問題で、株価の予測などに利用されます。

[第1回](https://qiita.com/kwi0303/items/e494eeddac4998df2d03)、[第2回](https://qiita.com/kwi0303/items/4fcab8161657ee1550b1)にわたってニューラルネットワークを実装してきましたが、これまでは得られた予測結果が望ましい結果だったかどうかについては、議論されていません。そこで、予測結果を真の結果（教師データ）と比較して誤差を算出することができれば、予測モデルの妥当性を確認することができます。

そのためには、（1）予測結果と教師データを比較するために出力を教師データと比較できる形にする、（2）出力と教師データを比較して誤差を定量化する、この2つのステップが必要になってきます。本記事では、出力層から誤差関数を利用して誤差を求めるモデルを実装していきます。

#2. 出力層の活性化関数
冒頭で説明したようにニューラルネットワークでは分類問題と回帰問題の両方に用いることができ、それぞれに適した出力層の活性化関数を利用する必要があります。

##2.1. 回帰問題に利用される恒等関数
回帰問題とは、下記図のように入力に対する数値を予測する問題です。教師データは、入力に対しての実際の数値（実験データなど）にあたりますので、出力層の出力をそのまま比較して誤差を算出することができます。つまり、出力層からの入力信号をそのまま出力するだけなので、恒等関数が用いられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/7ee0c928-c120-da81-4d23-4ecc9255597d.png)

この出力と活性化関数の関係を図的に表すと以下の通りになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/986d520b-b54c-051d-c723-452914170711.png)

##2.2. 分類問題（多クラス分類）に利用されるソフトマックス関数
分類問題は、データをカテゴリに分類する問題です。下記にイメージ図を示しましたが、3クラスの動物（いぬ、ねこ、ねずみ）の分類問題で、教師データがOne-hot表現でいぬになっています（※One-hot表現；正解ラベルのみ1で、その他が0のベクトル）。ここで、出力層の値はこのOne-hot表現の教師データと比較され誤差を算出する必要があるので、値が0～1の範囲でその分類の確率、つまり総和が1という形で表すことができると、誤差を算出するのに都合がよくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/5c0dbf5e-65cc-e93f-eb08-038658528776.png)

そこで活躍するのがソフトマックス関数です。ソフトマックス関数は次の式で表されます。k番目の出力（yk）は、uの指数関数の総和を分母にとり、k番目のuの指数関数を分子にとっています。つまり、出力（y）は、全体が1のk番目のyになる割合（＝確率）を表しています。この出力と活性化関数の関係を図的に表すと以下の通りになります。出力の各ニューロンがすべてのuの信号から影響を受けています。

```math
y_k(u)=\frac{exp(u_k)}{\sum_{i=1}^{n}exp(u_i)} \
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/2b580a44-59e1-dbe0-fdd3-7c82c3f43994.png)

このソフトマックス関数をPythonで実装してみましょう。

```python
def softmax(x):
  x = x - np.max(x) # オーバーフロー対策
  return np.exp(x) / np.sum(np.exp(x))
```

ここで、オーバーフロー対策という処理を入れています。ソフトマックス関数は指数関数を扱うため、入力によっては値が大きくなってしまうことがあります。例えば、

```python
print(np.exp(10))
> 22026.465794806718

print(np.exp(100))
> 2.6881171418161356e+43
```

入力の桁が1桁変わるだけで、取り扱う数値がこれほど大きく変わってしまいます。そのため、あらかじめ入力の最大値で入力全体を処理しておけば、このような大きな値を使わずにコンピュータでの安定した処理ができます。

#3. 誤差関数（損失関数）

前章まで出力層の活性化関数を取り扱ってきました。次はこの出力と教師データを比較し、誤差を算出するステップになります。[第1回](https://qiita.com/kwi0303/items/e494eeddac4998df2d03)、[第2回](https://qiita.com/kwi0303/items/4fcab8161657ee1550b1)を通して取り扱ってきた3層のニューラルネットワークで誤差関数を確認してみましょう。新しく追加した部分を赤字で記入しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/d7c387ed-d639-b0ff-1ce5-c9bcdac0091f.png)

この誤差関数（E）には、二乗和誤差（mean square error）や交差エントロピー誤差（cross entropy loss）などが用いられます。利用の主な組み合わせとしては以下の通りで、回帰問題には二乗和誤差、分類問題には交差エントロピー誤差が用いられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/e4c3ae39-315c-5eed-4365-9d41b7a6f2d4.png)

本章では二乗和誤差と交差エントロピー誤差をPythonで実装し、下記の例で誤差の計算をしていきます。※例が分類問題ですが、説明の便宜上二乗和誤差の計算も取り扱っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/b47a9a4f-142e-ae15-5b82-d6c82d19e951.png)

##3.1. 二乗和誤差
二乗和誤差をPythonで実装してみましょう。

```python
# 二乗和誤差
def mean_squared_error(y, t):
    return np.sum((y - t)**2) / 2
```

試しに、先ほどの3クラス分類の例で計算させてみます。

```python
y = np.array([0.6, 0.3, 0.1]) # 出力
t = np.array([1, 0, 0]) # 3クラス分類の教師データ
mean_squared_error(y, t)
> 0.13
```

結果が得られました。二乗和誤差はデータ数が少ない場合計算も簡単なので、確認をしてみます。

```math
E=\frac{1}{2}\sum_{k}(y_k-t_k)^2\\
E=\frac{1}{2}((0.6-1)^2+(0.3-0)^2+(0.1-0)^2)=\frac{1}{2}(0.16+0.09+0.01)=0.13
```

結果が一致しました。今は予測結果が教師データのラベルと一致していますが、例えばyが間違っている場合の計算をさせてみます。

```python
y_incorrect = np.array([0.1, 0.6, 0.3]) # 出力 不正解の結果に変更
t = np.array([1, 0, 0]) # 3クラス分類の教師データ
mean_squared_error(y_incorrect, t)
> 0.63
```

誤差が大きくなりました。一つ目の結果のほうが誤差が小さいことが分かるので、この誤差を小さくするよう学習を進めれば言いわけですね。

##3.2. 交差エントロピー誤差

次に交差エントロピー誤差を実装していきます。

```python
# 交差エントロピー誤差
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y+delta))
```

交差エントロピー誤差はlogを取っているので、ｙの値が0の場合-∞になってしまいコンピュータでの計算がます。そのため、極微小な無視できる値（1e-7）を足しておくことでエラーを回避しています。

二乗和誤差と同様に3クラス分類での計算をしてみましょう。

```python
y = np.array([0.6, 0.3, 0.1])
t = np.array([1, 0, 0])
cross_entropy_error(y, t)
> 0.63

y_incorrect = np.array([0.1, 0.6, 0.3])
t = np.array([1, 0, 0])
cross_entropy_error(y_incorrect, t)
2.302584092994546
```

結果が得られました。傾向も二乗和誤差と一致しています。

#4. Pythonによる実装

それでは最後にニューラルネットワークに組み込んで、Pythonでの実装をしていきましょう。[第1回](https://qiita.com/kwi0303/items/e494eeddac4998df2d03)、[第2回](https://qiita.com/kwi0303/items/4fcab8161657ee1550b1)で、3層のニューラルネットワークの活性化関数前までを実装しました。今回は出力層の活性化関数と誤差関数を組み込みます。※出力が2ノードなので、ニューラルネットワークによる2クラス分類を想定していますが、今回は出力層の活性化関数としてソフトマックス関数（シグモイド関数でも可）、誤差関数として二乗和誤差を用いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1529393/71a8df66-58df-0e7e-ad27-17adb83dc1b7.png)

```python
import numpy as np

# シグモイド関数（ロジスティック関数）
def sigmoid(x):
    return 1/(1 + np.exp(-x))

# ソフトマックス関数
def softmax(x):
  x = x - np.max(x) # オーバーフロー対策
  return np.exp(x) / np.sum(np.exp(x))

# 二乗和誤差
def mean_squared_error(y, t):
    return np.sum((y - t)**2) / 2

# 行列、行列形状の出力用
def print_vec(text, vec):
    print("*** " + text + " ***")
    print(vec)
    print("shape: " + str(vec.shape))
    print("")

# ニューラルネットワークのサイズ設定 入力層:1層(2ノード), 中間層:2層(3ノード・2ノード), 出力層:1層(2ノード)
def init_network():
  # ネットワークの初期化
  network = {}

  input_layer_size = 2
  hidden_layer_size_1 = 3
  hidden_layer_size_2 = 2
  output_layer_size = 2

  # ネットワークの初期値設定
  network['W1'] = np.array([[0.1,0.2],[0.3,0.4],[0.5,0.6]])
  network['W2'] = np.array([[0.1, 0.2, 0.3],[0.4, 0.5, 0.6]])
  network['W3'] = np.array([[0.1,0.2],[0.3,0.4]])

  network['b1'] =  np.array([0.1,0.2,0.3])
  network['b2'] =  np.array([0.1,0.2])
  network['b3'] =  np.array([0.1,0.2])

  print_vec("重み1", network['W1'] )
  print_vec("重み2", network['W2'] )
  print_vec("重み3", network['W3'] )
  print_vec("バイアス1", network['b1'] )
  print_vec("バイアス2", network['b2'] )
  print_vec("バイアス3", network['b3'] )

  return network

def forward(network, x, t):
  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['b1'], network['b2'], network['b3']

  # 1層の総入力
  u1 = np.dot(W1,x) + b1
  # 1層の総出力
  z1 = sigmoid(u1)
  # 2層の総入力
  u2 = np.dot(W2,z1) + b2
  # 2層の総出力
  z2 = sigmoid(u2)
  # 出力層の総入力
  u3 = np.dot(W3,z2) + b3
  # 出力層の総出力
  y = softmax(u3)

  # 誤差関数による誤差の計算
  loss = mean_squared_error(y, t)

  print_vec("総入力1", u1)
  print_vec("中間層出力1", z1)
  print_vec("総入力2", u2)
  print_vec("中間層出力2", z2)
  print_vec("総入力3", u3)
  print_vec("出力", y)
  print_vec("誤差", loss)

  return y, loss

# 入力値
x = np.array([1.0,0.5])
#  教師データ
t = np.array([1,0])
# ネットワークのインスタンス化と順伝播の実行
network = init_network()
y = forward(network, x, t)
print(y)

> *** 重み1 ***
  [[0.1 0.2]
   [0.3 0.4]
   [0.5 0.6]]
  shape: (3, 2)

  *** 重み2 ***
  [[0.1 0.2 0.3]
   [0.4 0.5 0.6]]
  shape: (2, 3)

  *** 重み3 ***
  [[0.1 0.2]
   [0.3 0.4]]
  shape: (2, 2)

  *** バイアス1 ***
  [0.1 0.2 0.3]
  shape: (3,)

  *** バイアス2 ***
  [0.1 0.2]
  shape: (2,)

  *** バイアス3 ***
  [0.1 0.2]
  shape: (2,)

  *** 入力 ***
  [1.  0.5]
  shape: (2,)

  *** 総入力1 ***
  [0.3 0.7 1.1]
  shape: (3,)

  *** 中間層出力1 ***
  [0.57444252 0.66818777 0.75026011]
  shape: (3,)

  *** 総入力2 ***
  [0.51615984 1.21402696]
  shape: (2,)

  *** 中間層出力2 ***
  [0.62624937 0.7710107 ]
  shape: (2,)

  *** 総入力3 ***  ## 前回は出力層に活性化関数を利用していないので、前回の出力が総入力3(u3)にあたります
  [0.31682708 0.69627909]
  shape: (2,)

  *** 出力 ***  ## ソフトマックス関数を利用しているため、出力の総和が1になります。
  [0.40625907 0.59374093]
  shape: (2,)

  *** 誤差 ***
  0.35252829011170406
  shape: ()

  (array([0.40625907, 0.59374093]), 0.35252829011170406)
```

#5. おわりに
本記事では、ニューラルネットワークの出力層の活性化関数の具体例を実装しながら、ニューラルネットワークの順伝播Pythonでの実装を実施しました。
引き続き、連載に沿って深層学習を学んでいきます。

# 連載記事目次
第1回：[入力層・中間層](https://qiita.com/kwi0303/items/e494eeddac4998df2d03)
第2回：[活性化関数](https://qiita.com/kwi0303/items/4fcab8161657ee1550b1)
第3回：出力層（本記事）
第4回：[勾配降下法](https://qiita.com/kwi0303/items/7bfd7180f80a52296e64)
第5回：[誤差逆伝播法](https://qiita.com/kwi0303/items/e43efa6657ff4b6b59ad)

# 参考文献
この記事は以下の情報を参考にして執筆しました。
参考：[ゼロから作るDeepLearning](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8BDeep-Learning-%E2%80%95Python%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%AE%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%A3%85-%E6%96%8E%E8%97%A4-%E5%BA%B7%E6%AF%85/dp/4873117585/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B&qid=1622723947&sr=8-1), O'REILLY社