【PyTorch】自由にネットワークを設計するよ

#自分の手で自由にパラメータを定義したい
PyTorchを使ったDeepLearningが流行ってるらしいですが、
Qiitaとかはてなブログで他人の記事を読んでると
```h = nn.Linear(N, M)```
みたいに、$\boldsymbol{h}=\boldsymbol{x}^T\boldsymbol{W} + \boldsymbol{b}$における$\boldsymbol{W}$と$\boldsymbol{b}$（ネットワークパラメータ）の定義を全て
PyTorch任せにしてるのが多いですよね。

まぁこれで所望のネットワークを構築できるなら全く問題ないですし、
むしろ恩恵も多いんだろうな〜とは思いますよ。

ただ、$\boldsymbol{W} = \boldsymbol{AH}$みたいに別々のパラメータを用いて表現したい場合は困っちゃいます。

だからこの記事では

```h = nn.Linear(N, M)
```
みたいな書き方じゃなくて、ちゃんと全てのパラメータを手動で定義する方法をメモとして残します。
#
#
# ######################################################################
#
#
#シンプルなDNN
ということでDNNを作りたいと思います。
問題設定はとりあえずどうでも良いので、、、
#
###まず最初
```python
import torch
import numpy as np
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
```
FはDeepLearningで使う便利な関数ライブラリです。活性化関数とかも入ってます。
optimは最適化手法のライブラリです。SGDとかAdamとか。

###パラメータ・最適化手法の定義

```python
class DNN(nn.Module):
  def __init__(self, I, J, K):
  super().__init__()
    self.I, self.J, self.K = I, J, K 

    self.W = [nn.Parameter(torch.tensor(np.random.randn(I,K))),\
              nn.Parameter(torch.tensor(np.random.randn(K,K))),\
              nn.Parameter(torch.tensor(np.random.randn(K,J)))]
    self.b = [nn.Parameter(torch.tensor(np.zeros(K))),\
              nn.Parameter(torch.tensor(np.zeros(K))),\
              nn.Parameter(torch.tensor(np.zeros(J)))]
  
    self.params = self.W + self.b
    self.optimizer = optim.Adam(self.params, lr=0.01)
```
I, J, Kはとりあえず入力層の次元、出力層の次元、中間層の次元とします。
面倒なのでとりあえず中間層の次元は同じとします。
（あくまで今回の記事は、自分でネットワークを定義するためのメモなので次元数はどうでもE）

```python
nn.Parameter(torch.tensor(x))
```
これはtorch.tensor()で変数xをPyTorchで扱う変数として定義して、
nn.Parameter()で勾配計算によって更新するパラメータですよっていう宣言というか
そういう設定にするための記述ですかね。
ちなみに今回は中間層は２層です。

```python
self.params = self.W + self.b
```
これは、ただネットワークのパラメータをリスト化してるだけです。
今回はパラメータをリスト化してるのでリスト同士を足してあげて
１つのリストにまとめた感じです。

self.optimizer（最適化手法）にAdamを指定しました。
別に何でも良いですよ。SGDでも。
第一引数が更新したいパラメータ（リスト）です。
そのためにさっきリストでまとめ上げました。

#
###ちなみに
最適化手法の定義時に、今回はパラメータをリスト化してself.paramsと自分で定義して渡しました。
ただ、```optim.Adam(self.parameters(), ~)```という記述もよく見ます。

実は、今回のようにパラメータをリストで梱包した場合、
self.parameters()には定義したパラメータが反映されません！
詳細は忘れましたが、nn.Parameterで宣言したパラメータをlistで梱包してしまったので、
このモデルでは「リスト」として認識されてます。
なのでわざわざself.paramsなるものを用意してoptimizerの引数に渡し直しました。

```self.W = nn.ParameterList([nn.Parameter(torch.tensor(~)), nn.Parameter(torch.tensor(~), ~])```
のように、「nn.ParameterrList」を使用してあげれば、
このモデルでもリスト内の各パラメータはちゃんと「パラメータ」として認識されます。
なので```optim.Adam(self.parameters(), ~)```でもちゃんと動きます。


###目的関数の定義

```python
  def compute_loss(self, y, x):
    loss = torch.sum(torch.square(y - x), axis=1)
    return torch.mean(loss)
```
目的関数です。ただの平均２乗誤差です。ここも別に何でも良いです。
ちなみに

```python
nn.MSELoss(y,x)
```
でも同じです。ただここも自分で自由に定義できるように！というコンセプトの元、
あえて自分で書いてます。


###FeedForwardについて
```python
  def feedforward(self, x):
    h = F.sigmoid(self.linear(x, self.W[0], self.b[0]))
    for l in range(1, len(self.W)):
      h = F.sigmoid(self.linear(h, self.W[l], self.b[l]))
    
    return h

  def linear(self, x, w, b):
    return torch.matmul(x, w) + b
```  
まぁここは特に説明する必要は無いでしょう。

ぼちぼち面倒臭くなってきました。。。

#
#######################################################
#
###学習について
```python
  def fit(self, X, Y, nbatch=1024, nepoch=100):
    N = X.shape[0] 
    
    for epoch in range(nepoch):
      perm = np.random.permutation(N)
      
      for i in range(0, N-nbatch, nbatch):
        x = X[perm[i:i + nbatch]]
        x_ = torch.tensor(x.astype(np.float32), requires_grad=False)

        y = Y[perm[i:i + nbatch]]
        y_ = torch.tensor(y.astype(np.float32), requires_grad=False)

        x2y = self.feedforward(x_)
        loss = self.compute_loss(y_, x2y)
        
        self.optimizer.zero_grad()                         
        loss.backward()
        self.optimizer.step()
```
まず、Xは学習データでN-by-Iのnumpy.arrayとします。
Nはデータ数（音声系のデータならフレーム数とか）、Iは各データの次元数です。
Yは教師データです。作りはXと基本同じですが、N-by-Jになります（Jは出力データの次元数）。

とりあえずミニバッチ法で学習するとして、
Xからミニバッチxを取り出し、
お馴染みtorch.tensorでPyTorchのTensorであるx_に変換し、
feedforwardネットで出力y2xを得ます。
>※お馴染みtorch.tensorで、データ型を指定したり、requires_grad=False（変数だから勾配計算要らないよ、という）を指定したりしてますがもしかしたら不要かもしれません。面倒臭いので確認してません。。。

教師データy_と出力データx2yのロスであるlossを計算します。

ほいで、

```python
self.optimizer.zero_grad()
```
はネットワークのパラメータ勾配を0にリセットするという意味です。
直前の勾配はとりあえず関係ないので0にしましょうってことですかね。

```python
loss.backward()
```
これはネットワークのパラメータの勾配を計算するための記述です。
lossには「入力x_からパラメータを用いてx2yを算出した」という計算履歴が残ってるので（知らんけど。まぁそうだろうという勝手な解釈です）、
それを辿って勾配を計算してるんですかね。

```python
self.optimizer.step()
```
lossを算出するために使ったパラメータのうち、optimizer定義時に「更新するよ！」と宣言したself.paramsに含まれるパラメータを、いまさっき求めた勾配を用いて更新します。

#
#########################################################################
#
#以上をまとめると

```python
import torch
import numpy as np
from torch import nn
import torch.nn.functional as F
import torch.optim as optim

class DNN(nn.Module):
  def __init__(self, I, J, K):
  super().__init__()
    self.I, self.J, self.K = I, J, K 

    self.W = [nn.Parameter(torch.tensor(np.random.randn(I,K))),\
              nn.Parameter(torch.tensor(np.random.randn(K,K))),\
              nn.Parameter(torch.tensor(np.random.randn(K,J)))]
    self.b = [nn.Parameter(torch.tensor(np.zeros(K))),\
              nn.Parameter(torch.tensor(np.zeros(K))),\
              nn.Parameter(torch.tensor(np.zeros(J)))]
  
    self.params = self.W + self.b
    self.optimizer = optim.Adam(self.params, lr=0.01)

  def compute_loss(self, y, x):
    loss = torch.sum(torch.square(y - x), axis=1)
    return torch.mean(loss)

  def feedforward(self, x):
    h = F.sigmoid(self.linear(x, self.W[0], self.b[0]))
    for l in range(1, len(self.W)):
      h = F.sigmoid(self.linear(h, self.W[l], self.b[l]))
    
    return h

  def linear(self, x, w, b):
    return torch.matmul(x, w) + b


  def fit(self, X, Y, nbatch=1024, nepoch=100):
    N = X.shape[0] 
    
    for epoch in range(nepoch):
      perm = np.random.permutation(N)
      
      for i in range(0, N-nbatch, nbatch):
        x = X[perm[i:i + nbatch]]
        x_ = torch.tensor(x.astype(np.float32), requires_grad=False)

        y = Y[perm[i:i + nbatch]]
        y_ = torch.tensor(y.astype(np.float32), requires_grad=False)

        x2y = self.feedforward(x_)
        loss = self.compute_loss(y_, x2y)
        
        self.optimizer.zero_grad()                         
        loss.backward()
        self.optimizer.step()
```

#
############################################################
#
#ということで
だいたいの理解は正しいのかなと思い、自分のメモも兼ねて書きましたが
間違ってたらお恥ずかしい限りです 笑

それと、うんコードを量産しちゃうタイプなので
もっと頭良い書き方があれば