#自分の手で自由にパラメータを定義したい
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}$みたいに別々のパラメータを用いて表現したい場合は困っちゃいます。
だからこの記事では
みたいな書き方じゃなくて、ちゃんと全てのパラメータを手動で定義する方法をメモとして残します。
#シンプルなDNN
ということでDNNを作りたいと思います。
問題設定はとりあえずどうでも良いので、、、
###まず最初
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とか。
###パラメータ・最適化手法の定義
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)
nn.Parameter(torch.tensor(x))
これはtorch.tensor()で変数xをPyTorchで扱う変数として定義して、
nn.Parameter()で勾配計算によって更新するパラメータですよっていう宣言というか
そういう設定にするための記述ですかね。
ちなみに今回は中間層は2層です。
self.params = self.W + self.b
これは、ただネットワークのパラメータをリスト化してるだけです。
今回はパラメータをリスト化してるのでリスト同士を足してあげて
1つのリストにまとめた感じです。
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(), ~)
でもちゃんと動きます。
###目的関数の定義
def compute_loss(self, y, x):
loss = torch.sum(torch.square(y - x), axis=1)
return torch.mean(loss)
目的関数です。ただの平均2乗誤差です。ここも別に何でも良いです。
ちなみに
nn.MSELoss(y,x)
でも同じです。ただここも自分で自由に定義できるように!というコンセプトの元、
あえて自分で書いてます。
###FeedForwardについて
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()
まず、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を計算します。
ほいで、
self.optimizer.zero_grad()
はネットワークのパラメータ勾配を0にリセットするという意味です。
直前の勾配はとりあえず関係ないので0にしましょうってことですかね。
loss.backward()
これはネットワークのパラメータの勾配を計算するための記述です。
lossには「入力x_からパラメータを用いてx2yを算出した」という計算履歴が残ってるので(知らんけど。まぁそうだろうという勝手な解釈です)、
それを辿って勾配を計算してるんですかね。
self.optimizer.step()
lossを算出するために使ったパラメータのうち、optimizer定義時に「更新するよ!」と宣言したself.paramsに含まれるパラメータを、いまさっき求めた勾配を用いて更新します。
#########################################################################
#以上をまとめると
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()
############################################################
#ということで
だいたいの理解は正しいのかなと思い、自分のメモも兼ねて書きましたが
間違ってたらお恥ずかしい限りです 笑
それと、うんコードを量産しちゃうタイプなので
もっと頭良い書き方があれば