パーセプトロンの実装とアルゴリズムの説明

#はじめに
こんにちは。
私は最近MITがディープラーニング(以降 DL)の授業をYouTube上で公開しているのを知り、それを使って目下勉強中です。
下記リンクには授業内で使っているスライドと各授業のYouTube動画パスがあります。
[6S.191 Introduction to Deep Learning](http://introtodeeplearning.com/)
もちろん英語ですが、読める方はぜひ挑戦してみてください。結構面白いです！

#Lab1-1
ディープラーニングを実際に実装しているソースコードが公開されているので、それの解説をしていきます(要は備忘録です)
[ソースコード](https://github.com/aamini/introtodeeplearning/tree/master/lab1) ⇦ 今回はpart1です！
今回の内容はTensorFlowの基本的な機能の説明です
1.　tesnorflowの機能の説明
2.　パーセプトロンの実装
3.　勾配降下法の実装
以上のラインナップです。
ちなみにTensorFlowはなぜTensorFlowと呼ばれるかご存知でしょうか？
Tensor(N次元のベクトルや行列)のFlow(nodeや計算)をするのでTensorFlowと言う名前らしいです！明日から友達に「TesnsorFlowの名前の由来知ってる？」でマウントを取れますね！

#コードの解説

## TensorFlowで次元数の確認と特定の次元数を持つ変数を作成

```python:
# 0次元
sport = tf.constant('Tennis', tf.string)
number = tf.constant(1.41421356237, tf.float64)

print("`sport` is a {}-d Tensor".format(tf.rank(sport).numpy()))
print("`number` is a {}-d Tensor".format(tf.rank(number).numpy()))
```

```:output
`sport` is a 0-d Tensor
`number` is a 0-d Tensor
```
***0次元***のtensorを定義しました。
これはスカラーを表しています。スカラーはただの値なので次元を持っていません。
`tf.rank(変数).numpy()`メソッドを使うことによって、その変数の次元数を取得することができます。
numpy()で次元数のみの取得を実現しています。rank()だけだと不要な値まで返ってきます。
また、tf.constantでは公式ドキュメントより***「Creates a constant tensor from a tensor-like object」***とあるので、
tensor型の変数を作成しているというのがわかります。第二引数でどんな型のデータを持つかを決めます。

```python:
# 1次元　＝　ベクトル

sports = tf.constant(["Tennis", "Basketball"], tf.string)
numbers = tf.constant([3.141592, 1.414213, 2.71821], tf.float64)

print("`sports` is a {}-d Tensor with shape: {}".format(tf.rank(sports).numpy(), tf.shape(sports)))
print("`numbers` is a {}-d Tensor with shape: {}".format(tf.rank(numbers).numpy(), tf.shape(numbers)))
```

```:output
`sports` is a 1-d Tensor with shape: [2]
`numbers` is a 1-d Tensor with shape: [3]
```
***1次元のtensor***を定義しました。
これはベクトルの形です。ベクトルは1次元として扱われます。tf.shape(変数)で要素数を返します。

```python:
### Defining higher-order Tensors ###

'''TODO: Define a 2-d Tensor'''
matrix = tf.constant([['Baseball', 'Softball'], ['Soccer', 'Football'], ['Tennis', 'Badminton']], tf.string)

assert isinstance(matrix, tf.Tensor), "matrix must be a tf Tensor object"
assert tf.rank(matrix).numpy() == 2
# assertについて　https://codezine.jp/article/detail/12179

print("`matrix` is a {}-d Tensor with shape: {}".format(tf.rank(matrix).numpy(), tf.shape(matrix)))
```

```:output
`matrix` is a 2-d Tensor with shape: [3 2]
rank=次元数  shape=(行数、列数)を返す。
```
***2次元のtensor***を定義しました。
2次元のデータはマトリックス(行列)になります。形としてはリストの中にリストがある形となります。
```assert関数```を使うことで条件を満たしていない場合AssertionErrorを表示することができ何を満たせてないかがすぐわかるようになります。
```assert 条件```

例として上のコードを書き換えてみます。

```python:
### Defining higher-order Tensors ###

'''TODO: Define a 2-d Tensor'''
# matrix = tf.constant([['Baseball', 'Softball'], ['Soccer', 'Football'], ['Tennis', 'Badminton']], tf.string)
matrix = tf.constant([1, 2 ,3 ], tf.int64)

assert isinstance(matrix, tf.Tensor), "matrix must be a tf Tensor object"
assert tf.rank(matrix).numpy() == 2
# assertについて　https://codezine.jp/article/detail/12179

print("`matrix` is a {}-d Tensor with shape: {}".format(tf.rank(matrix).numpy(), tf.shape(matrix)))
```

```:output
AssertionError                            Traceback (most recent call last)
<ipython-input-42-2e33b2575781> in <module>()
      6 
      7 assert isinstance(matrix, tf.Tensor), "matrix must be a tf Tensor object"
----> 8 assert tf.rank(matrix).numpy() == 2
      9 # assertについて　https://codezine.jp/article/detail/12179
     10 

AssertionError: 
```
このように次元数がクリアできてない(2次元以上ではない)のでエラーが出ています。
これは自分の書いた計算式とかがあっているや欲しいoutputが手に入っているのかエラーで教えてくれるのですごく便利なものだと思います！

```python:
'''TODO: Define a 4-d Tensor.'''
# Use tf.zeros to initialize a 4-d Tensor of zeros with size 10 x 256 x 256 x 3. 
#   You can think of this as 10 images where each image is RGB 256 x 256.
# (画像の数、画像のheight, 画像のwidth, 画像のチャネル数)
images = tf.constant(tf.zeros([10, 256, 256, 3]))

assert isinstance(images, tf.Tensor), "matrix must be a tf Tensor object"
assert tf.rank(images).numpy() == 4, "matrix must be of rank 4"
assert tf.shape(images).numpy().tolist() == [10, 256, 256, 3], "matrix is incorrect shape"
```
***4次元のtensor***を定義しました。
コメントアウトでも書いてますが4次元のリストは画像データを表す際に使われます。上のコードでは10枚のRGB表記の写真(256x256)となります。
実際にどのような構造になっているかみてみます。

```python:
images
```

```:output
<tf.Tensor: shape=(10, 256, 256, 3), dtype=float32, numpy=
array([[[[0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.],
         ...,
         [0., 0., 0.],
         [0., 0., 0.],
         [0., 0., 0.]],...
```
ちゃんと4次元のリストになっていますね。3個の要素が入ったリストが並んでいると思いますが、これはRGB(Red, Green, Blue)の値になります。それが256x256あると言うことになります。
このように`tf.zeros()`を使うことで4次元のtensorを簡単に実現することができます。他にも`tf.ones`, `tf.fill`, `tf.eye`なども同じようなことができます。

またリストやarrayと同じようにスライスを用いることで指定の要素にアクセスできます。

```python:
# use slices to access substensors
row_vector = matrix[1]
column_vector = matrix[:,1]
scalar = matrix[0,1]

print("`row_vector`: {}".format(row_vector.numpy()))
print("`column_vector`: {}".format(column_vector.numpy()))
print("`scalar`: {}".format(scalar.numpy()))
```

```:output
`row_vector`: [b'Soccer' b'Football']
`column_vector`: [b'Softball' b'Football' b'Badminton']
`scalar`: b'Softball'
```

##Tesnor型同士の計算
Tensor型同士の簡単な計算方法をみていきます。

```python:
# Create the nodes in the graph, and initialize values
a = tf.constant(15)
b = tf.constant(61)

# Add them!
c1 = tf.add(a,b)
c2 = a + b # TensorFlow overrides the "+" operation so that it is able to act on Tensors
print(c1)
print(c2)
```

```:output
tf.Tensor(76, shape=(), dtype=int32)
tf.Tensor(76, shape=(), dtype=int32)
```
```python:
### Defining Tensor computations ###

# Construct a simple computation function
def func(a,b):
  '''TODO: Define the operation for c, d, e, f (use tf.add, tf.subtract, tf.multiply, tf.divide).'''
  c = tf.add(a,b)
  d = tf.subtract(b,1)
  e = tf.multiply(c,d)
  f = tf.divide(e, a)
  return f

# Consider example values for a,b
a, b = 1.5, 2.5
# Execute the computation
f_out = func(a,b)
print(f_out)
```

```:output
tf.Tensor(4.0, shape=(), dtype=float32)
```
このようにtensorflowのメソッドを使って計算を行うこともできますし、tesnorflowは`+`, `-`, `*`, `/`の計算オペレーションもカバーしているのでこれを使っても計算できます！

##パーセプトロンの実装
そして今からが面白いところですね！tensorflowライブラリを使ってディープニューラルネットワークの考えの基になっているパーセプトロンを実装してみましょう！
![パーセプトロン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1674311/f70690d6-a29d-6c75-6827-c18fe617fac8.png)
パーセプトロンはすごくシンプルなもので`y=ax+b`の式に非常に似ています。`ベクトルx`をインプットとして受け取り、各xの要素に`重みw1`をかけて(複数output時は行列)、全てを足して`バイアスw0`を足すことで`結果z`を出します。そして最後に`非線形関数(例:sigmoid)`を適用して、出力zを出します。非線形関数を使う理由としてはそのままでいくと直線の出力しか出せなくて、分類を行う時直線でしか分類が行えなくなるので使います！イメージはこんな感じですね！より細かく分類することを可能にしています！
![linearVSnonlinear.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1674311/abbdc8d8-3095-6be4-320d-754ea7b146ed.png)

パーセプトロンの式も書いておきます。

```math:
y=g(w_{0}+\sum_i^mx_{i}w_{i})
```
アウトプットが一つの単純パーセプトロンの式です。i=1からはじまります(書き方がわからなかったです)。
gは非線形関数を表しています。

今回は複数のアウトプットを求めますので下の式になります。
![multiPerceptron.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1674311/a6141558-142d-5443-a7fc-d3de50013077.png)


```math:
z_{i}=w_{0,i}+\sum_j^mx_{j}w_{j,i}
```
やることはほとんど変わりません。変わったところはoutputを複数出すために重みとバイアスが増えて行列になっているところですね。

それではpythonで実装していきましょう！
ここではクラス作成して使っています。tensorflowだけを使って実装することもできますが、クラス、関数を作ることで可読性や汎用性のあるコードにすることができますので、おすすめです！
では、コード全体をみましょう！

```python:
# Simple perceptron
### Defining a network Layer ###

# n_output_nodes: number of output nodes
# input_shape: shape of the input
# x: input to the layer

class OurDenseLayer(tf.keras.layers.Layer):
  def __init__(self, n_output_nodes):
    super(OurDenseLayer, self).__init__() # 親クラスのコンストラクタを呼び出し
    self.n_output_nodes = n_output_nodes

  def build(self, input_shape):
    d = int(input_shape[-1])
    # Define and initialize parameters: a weight matrix W and bias b
    # parameter is random at first
    self.W = self.add_weight('weight', shape=[d, self.n_output_nodes]) # note the dimensionality
    self.b = self.add_weight('bias', shape=[1, self.n_output_nodes]) # note the dimensionality

  def call(self, x):
    '''TODO: define the operation for z (hint: use tf.matmul)'''
    z = tf.matmul(x, self.W) + self.b # TODO

    '''TODO: define the operation for out (hint: use tf.sigmoid)'''
    y = tf.sigmoid(z) # TODO
    return y

# Since layer parameters are initialized randomly, we will set a random seed for reproducibility
tf.random.set_seed(1)
layer = OurDenseLayer(3) # 3 outputs
layer.build((1,2)) # 1行、2列 → inputが2つある
x_input = tf.constant([[1,2.]], shape=(1,2))
y = layer.call(x_input)

# test the output
print(y.numpy()) # should output 3 answers
mdl.lab1.test_custom_dense_layer_output(y)
```
```:output
[[0.2697859  0.45750412 0.66536945]]
[PASS] test_custom_dense_layer_output
True
```
となります。アウトプットを見てわかる通り今回は3つのアウトプットを出すようにしたので3つでてきてます。
では順番に解説していきます。クラスの解説は今回の目的とは外れるのでしません。

まずコンストラクタ`__init__`でアウトプットがいくつかを指定できるようにします。
`build()`メソッドでインプットの形を受け取ります。インプットはベクトルなので(1, n)となります。
それを使い必要な重みとバイアスを初期化しています。

最後に`call()`メソッドで計算を行なっています。`tf.matmul`でベクトルxと重みとバイアスの行列を計算しています。
そして最後にシグモイド関数(`tf.sigmoid`)を適用してアウトプットyを出しています。
ちなみにcallメソッドは`obj.call()`か`obj()`で呼び出すことができます。
パーセプトロンのアルゴリズムは非常にシンプルでわかりやすかったと思います！今はインプット層とアウトプット層しかありませんでしたが、
これをもっと多層にしていけば最終的にディープニューラルネットワーク(Fully Connected Neural Network)になります！

続いてSequential modelを使っても実装してみます。

```python:
# Sequential model using Model API
### Defining a model using subclassing ###

from tensorflow.keras import Model
from tensorflow.keras.layers import Dense

class SubclassModel(tf.keras.Model):
  # Define the model's layer
  def __init__(self, n_output_nodes):
    super(SubclassModel, self).__init__()
    '''TODO: Our model consists of a single Dense layer. Define this layer.''' 
    self.dense_layer = Dense(n_output_nodes, activation='sigmoid') # アウトプット層のobj作成

  # call function to define the Model's forward pass
  def call(self, inputs):
    return self.dense_layer(inputs).numpy()

n_output_nodes = 3
model = SubclassModel(n_output_nodes)

x_input = tf.constant([[1,2.]], shape=(1,2))

print(model.call(x_input))
```

```:output
[[0.5607363 0.6566898 0.1249697]]
```
Sequentialは次回以降も頻繁に使われますが、ネットワーク層を順番に定義していくことができ非常に読みやすく使いやすいです。
今回はアウトプット層のみですが、より多層にすることも可能です。

以上でパーセプトロンの実装例は終わりです！このアルゴリズムの理解は今後のニューラルネットワークの理解に非常に役に立ちますのでぜひ理解してください！

##勾配降下法の実装
勾配降下法ニューラルネットワークの学習において非常に重要になってくる考えです。学習フェーズでは`Loss関数(例：relu)`を使用して、`予測されyと正解yのloss`を出します。lossは小さければ誤差がない(正解に近い)のでそれを使って勾配降下法のアルゴリズムで重みを最適化していきます。要は重みに対するLossの微分をとって最適な重みを探そうと言うものです。
![gradient.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1674311/024ba313-b634-4ef5-8750-ee92e3bdeb34.png)

```math:
new W=W-n\frac{\partial L}{\partial W}
```
微分を求めることで傾きを知れるのでlossが小さくなる向きがわかります(つまりマイナス方向に移動したい)。nはlearning rateと呼ばれるもので実際にどのくらいの距離を移動するかを決定しています。そして移動した分をWから引いて新しいWを取得します。これを繰り返すことで図のようにlossが小さい方へと移動し精度の高い予測ができるようになります。簡単ですが以上が勾配降下法の説明です。これの派生で色々な手法や勾配降下方の問題点もありますので興味ある方は調べてみてください。

では、まずtensorflowを使って簡単な勾配(微分)を計算してみます。ここではわかりにくいですが***確率的勾配降下法(SGD)***が使われています。
GradientTapeを使ってしてみます。

```python:
# First, we will look at how we can compute gradients using GradientTape and access them for computation. 
### Gradient computation with GradientTape ###

# y = x^2
# Example: x = 3.0
x = tf.Variable(3.0)

# Initiate the gradient tape
with tf.GradientTape() as tape:
  # Define the function
  y = x * x
# Access the gradient -- derivative of y with respect to x
dy_dx = tape.gradient(y,x)

assert dy_dx.numpy() == 6.0
print(dy_dx)
```
```:output
tf.Tensor(6.0, shape=(), dtype=float32)
```

`with tf.GradientTape()`の中で関数を定義します。
`tape.gradient(y,x)`でxに対するyの勾配を求めます。
簡単に実装することができました！これは自分でも微分できるものなので正しく計算できているか、ぜひ確認してください。

次にlossが本当に小さくなっていくのかをみていきます。ここではxをランダムに決めて正解x=4に勾配降下法を使って近づけていきます。
パーセプトロンを使っての計算などはせずに単純に定義したxが正解xに近づけるかをみていきます。

```python:
# how actually we use gradient to minimize loss
### Function minimization with automatic differentiation and SGD ###

# initialize a random value for our initial x
x = tf.Variable([tf.random.normal([1])])
print('Initializing x={}'.format(x.numpy()))

learning_rate = 1e-2 # learning rate for SGD
history = []
# Define the target value
x_f = 4

# We will run SGD for a number of iterations. At each iteration, we compute the loss, 
#   compute the derivative of the loss with respect to x, and perform the SGD update.
for i in range(500):
  with tf.GradientTape() as tape:
    '''TODO: define the loss as described above'''
    # loss = tf.keras.metrics.mean_squared_error(x_f, x)
    loss = (x_f - x)**2

   # loss minimization using gradient tape
   grad = tape.gradient(loss, x) # compute the derivative of the loss with respect to x
   new_x = x - learning_rate*grad # sgd update
   x.assign(new_x) # update the  value of x
   history.append(x.numpy()[0])

  # plot the evolution of x as we optimize towards x_f
  plt.plot(history)
  plt.plot([0,500], [x_f,x_f])
  plt.legend(('Predicted', 'True'))
  plt.xlabel('Iteration')
  plt.ylabel('x value')
```

```:output
Initializing x=[[-1.1771784]]
```
![optimize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1674311/4fbe6664-4d11-a479-311f-8327f51b3bb9.png)

実際に正解に近づいてるのがわかりますね。やっていることは先程説明した通りで、勾配降下法で最適なxを見つけて、xをアップデートしてより正解xに近づくようにしこれを500回繰り返してます。グラフで見てわかるように正解にどんどん近づいていってます。
以上で勾配降下法の説明終わりです！

#まとめ
以上でtensorflowの説明を終わります。今回は学習や予測を通しての実装はしませんでしたが、今回の書いたコードをうまく組み合わせば、自分で予測を行うようプログラムできると思います。興味あればぜひやってみてください！
パーセプトロンの理解は非常に重要ですので押さえておいてください。また、勾配降下法は学習において最重要なので、理屈を知っておくことは非常に重要です。
では、次回は音楽自動生成をRNNでしていきます！

間違いや理解しにくいところがあればコメントしていただけると幸いです。