2020/1/27 投稿
2021/7/11 少しの修正と追加情報
0. この記事の対象者
- pythonを触ったことがあり,実行環境が整っている人
- pyTorchをある程度触ったことがある人
- pyTorchによる機械学習でbackwardによる自動微分について知りたい人
- pyTorchのbackwardができないことを知りたい人
1. はじめに
昨今では機械学習に対してpython言語による研究が主である.なぜならpythonにはデータ分析や計算を高速で行うためのライブラリ(moduleと呼ばれる)がたくさん存在するからだ.
その中でも今回はpyTorchと呼ばれるmoduleを使用し,どのように自動微分を行っているのか、またどんなことができてどんなことができないのかを説明する.
ただしこの記事は自身のメモのようなもので,あくまで参考程度にしてほしいということと,簡潔に言うために間違った表現や言い回しをしている場合があるかもしれないが,そこはどうかご了承していただきたい.
また,この記事では実際にNetworkを使って学習をしたりはしない.
そちらに興味がある場合は以下のLinkを参照してほしい.
2. pyTorchのインストール
pyTorchを初めて使用する場合,pythonにはpyTorchがまだインストールされていないためcmdでのインストールをしなければならない.
下記のLinkに飛び,ページの下の方にある「QUICK START LOCALLY」で自身の環境のものを選択し,現れたコマンドをcmd等で入力する(コマンドをコピペして実行で良いはず).
3. pyTorchに用意されている特殊な型
numpyにはndarrayという型があるようにpyTorchには「Tensor型」という型が存在する.
ndarray型のように行列計算などができ,互いにかなり似ているのだが,Tensor型はGPUを使用できるという点で機械学習に優れている.
なぜなら機械学習はかなりの計算量が必要なため計算速度が早いGPUを使用するからだ.
さらに,Tensor型は機械学習のパラメータ更新のための微分が非常に簡単にできる.
これがとても簡単にできることが今回の記事の鍵となるのだ.
Tensor型の操作や説明は下記Linkより参照していただきたい.
4. 自動微分 backward
4-1. pyTorchのimport
まずはpyTorchを使用できるようにimportをする.
ここからはcmd等ではなくpythonファイルに書き込んでいく.
下記のコードを書くことでmoduleの使用をする.
import torch
4-2. 自動微分の例
以下のような簡単な計算プログラムを示す.
x = torch.tensor(4.0, requires_grad = True)
c = torch.tensor(8.0)
b = 5.0
y = c*x + b
print(y)
------------以下出力---------------
tensor(37., grad_fn=<AddBackward0>)
これは式で言えば
y = 8x+5
の$x=4$となる時の計算であり,$y$は37と答えが出力されている.この出力の「grad_fn=<AddBackward0>」は$y$が足し算によって算出されたということを示しており,これを各変数が保持することで微分を可能としている.
この微分は以下のようにする.
y.backward()
これで$y$内の全変数の値を微分したことになる.
何も出力されないので確認をしてみると
print(x)
print(x.grad)
------------以下出力---------------
tensor(4., requires_grad=True)
tensor(8.)
このように$x$の出力では微分情報は出ないが,「x.grad」とすることでその変数名の微分値8.0を見ることができる.
ここで,先ほど全変数の値を微分したといったのだが,実際に他の変数の微分情報を見てみると
print(c.grad)
print(b.grad)
------------以下出力---------------
None
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-881d89d572bd> in <module>
1 print(c.grad)
----> 2 print(b.grad)
AttributeError: 'float' object has no attribute 'grad'
まず最初の出力として「None」というものが出ている.
実は最初の変数の用意時に変数cには「requires_grad = True」を付けていないのだ.
これにより変数cは微分をしようとするがただの定数として解釈される.
さらに二つ目の出力はエラー文が出ている.
これはpyTorchの特殊な型であるTensor型にしかできない微分計算をTensor型以外(この変数bはただのfloat型)に行おうとしたから出たエラーである.
こうみるとpyTorchのTensor型が非常に優秀なことがわかり,「requires_grad = True」としておけばその微分情報はたった一行ですべて計算されるのだ.
4-3. もう少し自動微分の例
さらに複雑な計算をする例を示す.
x = torch.ones(2,3, requires_grad = True)
c = torch.ones(2,3, requires_grad = True)
y = torch.exp(x)*(c*3) + torch.exp(x)
print(torch.exp(x))
print(c*3)
print(y)
------------以下出力---------------
tensor([[2.7183, 2.7183, 2.7183],
[2.7183, 2.7183, 2.7183]], grad_fn=<ExpBackward>)
tensor([[3., 3., 3.],
[3., 3., 3.]], grad_fn=<MulBackward0>)
tensor([[10.8731, 10.8731, 10.8731],
[10.8731, 10.8731, 10.8731]], grad_fn=<AddBackward0>)
まず,「torch.exp()」は引数のデータの全要素それぞれに$e^{要素}$となる計算を施す.
それぞれの出力は見ての通りで,今回は変数x,c両方に「requires_grad = True」を適用した.
さて,実際にbackwardすると以下のようになる.
y.backward()
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-11-ab75bb780f4c> in <module>
----> 1 y.backward()
......(省略)......
RuntimeError: grad can be implicitly created only for scalar outputs
エラーが出力されるのだ.
このエラーで書かれている通り,backwardは実はスカラー値(簡単に言えば行列やベクトルでないただ1つの値のみのデータ)に対してしかできないのである.
実際に解決策として以下のようにする.
s = torch.sum(y)
print(s)
------------以下出力---------------
tensor(65.2388, grad_fn=<SumBackward0>)
この「torch.sum()」は引数の各要素すべてを足した結果を返す.
これでスカラー値にすることができた.
実際にbackwardをすると
s.backward()
print(x.grad)
print(c.grad)
------------以下出力---------------
tensor([[10.8731, 10.8731, 10.8731],
[10.8731, 10.8731, 10.8731]])
tensor([[8.1548, 8.1548, 8.1548],
[8.1548, 8.1548, 8.1548]])
このように多変数でしかも行列に対しても微分はしっかり行われている.
5. backwardできない&nan,infが出る例
ここからは実際にbackwardされない例, nanやinfが出る例を書いていこうと思う.
ここから先はこれからそういった例を新たに見つけたり,報告を受けたりし次第追記していく.
5-1. 変数がTensor型でない例
上の自動微分の例5-2でも説明したが微分したい変数がTensor型,しかも「requires_grad = True」でない場合に起こる.
解決は簡単で,要件を満たすような型にすればよい.
5-2. 最終的な出力がスカラー値でない例
上の自動微分の例5-3でも説明したが微分したい変数がスカラー値でない場合に起こる.
解決は,どうにかスカラー値にするようにすればよい.
例えば上の例でやった要素の総和は行列の形を崩すことなく行えるだろう.
5-3. 微分したい変数を上書きする例
以下に例を示す.
x = torch.tensor(1.0, requires_grad = True)
x = torch.exp(x)
c = torch.tensor(1.0, requires_grad = True)
c = c*3
b = 5.0
y = c*x + b
print(y)
------------以下出力---------------
tensor(13.1548, grad_fn=<AddBackward0>)
すごく簡単な例で式で書けば
y = (c*3)*e^{x}+5
の$c=1$,$x=1$となる式であり,「requires_grad = True」により,cもxも互いに微分可能な状態にしている.
実際に微分値は以下のようになる.
y.backward()
print(x.grad)
print(c.grad)
------------以下出力---------------
None
None
なんと,xもcも微分値を持たないのである.
なぜなら微分可能な変数x,cを上書きしたことにより,微分計算用の変数情報が消滅してしまうからである(xやcの定義が「torch.exp()」や「*3」で上書きされてしまう).
ただしこれくらいの例ならtorchが用意するoptimizer (SGDなど)にいれようとするとエラーをはいてくれる.
実際の例を以下に示す.
import torch.optim as optim
op = optim.SGD([x,c], lr=1.0)
------------以下出力---------------
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-18-775027da6d38> in <module>
----> 1 op = optim.SGD([x,c], lr=1.0)
............(省略)............
ValueError: can't optimize a non-leaf Tensor
optimizerであるSGDを使用するとエラーが出力される.
このoptimizerの詳しい説明が気になる方は下記Linkより参照してほしい.
ここでは簡単に説明するが,このSGDクラスは引数のパラメータ「[x,c]」に関してその勾配情報を使ってそれぞれのパラメータの更新をする準備をしているわけだ.
この時点で,これらの変数の計算グラフが切れていることをエラーとして出してくれるのだ.
解決は上書きをせずに別の変数に代入するか,式を直接書き下せばよい.
別変数に代入することはわかるだろうから,式を直接書き下す例を以下に示す.
x = torch.tensor(1.0, requires_grad = True)
c = torch.tensor(1.0, requires_grad = True)
b = 5.0
y = c*3*torch.exp(x)
y = y + b
y.backward()
print(x.grad)
print(c.grad)
------------以下出力---------------
tensor(8.1548)
tensor(8.1548)
ここでわざと4行目と5行目にかけてyの演算を分けた.
実はこのyの上書きにはペナルティは存在しない.
何故ならyは微分したい変数ではないから,計算さえきちんと行われていればどうなっていても良いのだ.
5-4. ルート(平方根)を使う例
まず,以下のような計算をしようと考えてみる.
y = c\sqrt{x_1^2+x_2^2+x_3^2}
パッと見てわかるが,これはベクトル$[x_1, x_2, x_3]$のL2ノルム(または単に距離)をc倍した値である.
これをプログラムで以下に示す.
x = torch.tensor([2.0,5.0,3.0], requires_grad = True)
c = torch.tensor(2.0)
y = torch.sqrt(torch.sum(x**2))
y = y*c
y.backward()
print(x.grad)
------------以下出力---------------
tensor([0.6489, 1.6222, 0.9733])
きちんとベクトルxに関する各要素の微分値が計算できていることがわかる.
少しプログラムの説明をすると,3行目の「torch.sqrt(torch.sum(x**2))」は最初にxの各要素を2乗し,その各要素の総和,そしてそれをルートに入れているわけだ.
ではこの式で以下の例を考えてみる.
x = torch.tensor([0.0,0.0,0.0], requires_grad = True)
c = torch.tensor(2.0)
y = torch.sqrt(torch.sum(x**2))
y = y*c
y.backward()
print(x.grad)
------------以下出力---------------
tensor([nan, nan, nan])
今,変数xの各要素の値を全て0.0に書き換えた(ベクトルxの距離が0).
その結果微分値はすべてnanになった.
こうすると各要素の微分値は当然だが∞をとる.
なぜなら上の式の微分は
\frac{\partial y}{\partial x_1} = c\frac{x_1}{\sqrt{x_1^2+x_2^2+x_3^2}}
であり,$xの距離=0$であることから0除算を行ってしまうからだ.
実際に機械学習をする際ではパラメータは自動で更新されていくわけなのだが,その過程で一度でもパラメータの値が0となってしまえば,計算過程に$\sqrt{x}$があればこの現象が起こってしまうのだ.
これにより見えないところでlossが発散したりnanになってしまう.
解決として以下のようにすればよい.
x = torch.tensor([0.0,0.0,0.0], requires_grad = True)
c = torch.tensor(2.0)
y = torch.norm(x)
y = y*c
y.backward()
print(x.grad)
------------以下出力---------------
tensor([0., 0., 0.])
このように3行目の「torch.norm()」を使用すると微分値はnanにならず0をとるようになる.
このtorch.norm()は計算自体は全く同じことをしているが,内部で0除算が起こらないような仕組みが備わっているのだろう.
5-5. in-placeを使った例
pythonにはin-place operationというのが備わっており,以下のようなことができる.
i += 1
x *=3
これらは通常「i = i+1」,「x = x*3」と書くところを省略して記述している.
この記法の方が処理速度は速いらしいが,自動微分には適していない.
以下に例を示す.
x = torch.tensor(3.0, requires_grad = True)
c = torch.tensor(2.0)
c += 2.0
x += 2.0
y = x + c
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-94-beb1a427373d> in <module>
2 c = torch.tensor(2.0)
3 c += 2.0
----> 4 x += 2.0
5 y = x + c
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
このように「requires_grad = True」とした変数に対してはin-place operationができないのである(もちろん変数cは微分と関係ないからできる).
解決方法は簡単で,in-place operationを使わなければよい.
すなわち,通常の書き方をするだけでよいのだ.
5-6. cpuとgpuを同時に使用する例
以下に例を示す.
x = torch.tensor(3.0, requires_grad = True).cuda()
c = torch.tensor(2.0, requires_grad = True).cpu()
y = x*c
print(y)
------------以下出力---------------
tensor(6., device='cuda:0', grad_fn=<MulBackward0>)
ここで変数xは「.cuda()」によりgpuを使用し,変数cは「.cpu()」によりcpuを使用するように明示している.
また,両方の変数は微分可能な状態にしている.
答えの出力は「device='cuda:0'」とあるようにgpuを使用している.
では,backwardをしてみる
y.backward()
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-118-8117d53c0658> in <module>
----> 1 y.backward()
...........(省略).............
RuntimeError: Function MulBackward0 returned an invalid gradient at index 1 - expected type torch.FloatTensor but got torch.cuda.FloatTensor
このように微分したい対象の変数がそれぞれ別の資源を使っていることで,このエラーが起こる.
解決としては,互いに使う資源を合わせればよい.
もちろん微分に関係ない変数はそろえる必要はない.
5-7. torch.Floattensorでない例
pyTorchで用意されているTensor型の中にもさらにint型やfloat型, double型などがある.
この型の使い分けは以下のようにすれば良い.
a = torch.tensor(2)
b = torch.tensor(2.134)
c = torch.tensor(3.5)
c = c.type(torch.int32)
d = torch.tensor(3.1514, dtype = torch.float64)
print(a)
print(b)
print(c)
print(d)
------------以下出力---------------
tensor(2)
tensor(2.1340)
tensor(3, dtype=torch.int32)
tensor(3.1514, dtype=torch.float64)
このように宣言時に「**dtype = **」と付け加えるか,「xxxx.type(型の種類)」としてあげれば良い.
さらに各変数の型は以下のように見る.
print(a.dtype)
print(b.dtype)
print(c.dtype)
print(d.dtype)
------------以下出力---------------
torch.int64
torch.float32
torch.int32
torch.float64
これを見てわかるように,変数aのように宣言時に指定なく整数を渡せば自動でint64に,変数bのように実数を渡せば自動でfloat32になる
更に面白いのは,変数cに関してはint32にcastしたせいで小数部は消滅してしまうのだ.
さて,ここまでを踏まえて実際に計算過程を例で以下に示す.
x = torch.tensor(3.0, dtype = torch.int64, requires_grad = True)
c = torch.tensor(2.0, requires_grad = True)
y = x*c
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-22-7183168e453f> in <module>
----> 1 x = torch.tensor(3.0, dtype = torch.int64, requires_grad = True)
2 c = torch.tensor(2.0, requires_grad = True)
3 y = x*c
RuntimeError: Only Tensors of floating point dtype can require gradients
ここでは変数xを整数型にしようとした.
整数型だとそもそも「requires_grad = True」とできないようで,このエラーが現れる.
それではfloatに書き換えてみる.
x = torch.tensor(3.0, dtype = torch.float64, requires_grad = True)
c = torch.tensor(2.0, requires_grad = True)
y = x*c
print(y)
------------以下出力---------------
tensor(6., dtype=torch.float64, grad_fn=<MulBackward0>)
うまく動作した.
それでは自動微分を行う.
y.backward()
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-8-ab75bb780f4c> in <module>
----> 1 y.backward()
............(省略)............
RuntimeError: Function MulBackward0 returned an invalid gradient at index 1 - expected type torch.FloatTensor but got torch.DoubleTensor
エラーが出てしまう.
この理由は簡単で,実はbackward()はtorch.float32のtypeでしかできないからである.
今回用意していたtorch.float64は厳密にはDouble型として扱われるため,backward()できない.
解決としてはtorch.float64の代わりにtorch.float32を使うだけで良い.
5-8. tensor型配列(ベクトル,行列)を使った例
実際の機械学習ではパラメータとしてバクトルや行列を用意して使うのが普通である.
以下に例を示す.
x = torch.tensor([10.0,20.0,30.0], requires_grad = True)
c = torch.tensor([1.0,2.0,3.0], requires_grad = True)
x[0] = c[0]*x[0]
x[1] = c[1]*x[1]
x[2] = c[2]*x[2]
y = torch.sum(x)
print(y)
------------以下出力---------------
tensor(140., grad_fn=<SumBackward0>)
これはベクトルxとベクトルcの内積を計算しているプログラムだ.
では,backward()をしてみる.
y.backward()
------------以下出力---------------
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-11-ab75bb780f4c> in <module>
----> 1 y.backward()
.........(省略)..........
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.FloatTensor []], which is output 0 of SelectBackward, is at version 3; expected version 2 instead. Hint: enable anomaly detection to find the operation that failed to compute its gradient, with torch.autograd.set_detect_anomaly(True).
このようなエラーが出力された.
ここで肝なのはエラーの中に「** gradient computation has been modified by an inplace operation**」とあることで,in-placeに関しては先程も例を出したが今回のプログラムではどこにも見当たらないのだ.
実は「x[0] = c[0]*x[0]」という配列のこの計算はin-placeに相当しているのだ.
このように見れば上でも出した変数の上書きのようなエラーにも見えるが,エラーはin-placeが原因だと言っているので注意する.
解決としては以下の様なプログラムにすればよい.
x = torch.tensor([10.0,20.0,30.0], requires_grad = True)
c = torch.tensor([1.0,2.0,3.0], requires_grad = True)
w = torch.zeros(3)
w[0] = c[0]*x[0]
w[1] = c[1]*x[1]
w[2] = c[2]*x[2]
y = torch.sum(w)
print(y)
------------以下出力---------------
tensor(140., grad_fn=<SumBackward0>)
このように微分と関係のない変数を用意してあげればよいのだ.
実際にbackward()してみると
y.backward()
print(x.grad)
------------以下出力---------------
tensor([1., 2., 3.])
うまく動作している.
5-9. 変数の割り算が入っている例
以下のように変数で割り算をしているようなケースは0割を起こしてしまう可能性がある.
x = torch.tensor(0.0, requires_grad=True)
y = 1./x
print(y)
------------以下出力---------------
tensor(inf, grad_fn=<MulBackward0>)
0割をしてもエラーにはならずinfを出す.
これを微分計算してみると
y.backward()
print(x.grad)
------------以下出力---------------
tensor(-inf)
微分値を使ってネットワークの重みの更新なんかをしようとするとinfはあまり使い物にならない.
解決策として,単純にすごく小さい値を分母に足すようにしておけばよい.
eps = 1e-6
x = torch.tensor(0.0, requires_grad=True)
y = 1./(x+eps)
print(y)
------------以下出力---------------
tensor(1000000., grad_fn=<MulBackward0>)
ここで1e-6とは10の-6乗を意味する(0に近いすごく小さな値).
微分値は以下のようになる
y.backward()
print(x.grad)
------------以下出力---------------
tensor(-1.0000e+12)
5-10. logを使った例
以下のようにlogを使う場合もlogの中身が0より大きくないとだめなので注意する.
x = torch.tensor(0.0, requires_grad=True)
y = torch.log(x)
print(y)
------------以下出力---------------
tensor(-inf, grad_fn=<LogBackward>)
0をlogに入れてもエラーにはならず-infを出す.
これを微分計算してみると
y.backward()
print(x.grad)
------------以下出力---------------
tensor(inf)
解決策として,単純にすごく小さい値をlog内に足すようにしておけばよい.
eps = 1e-6
x = torch.tensor(0.0, requires_grad=True)
y = torch.log(x+eps)
print(y)
------------以下出力---------------
tensor(-13.8155, grad_fn=<LogBackward>))
0をlogに入れてもエラーにはならず-infを出す.
これを微分計算してみると
y.backward()
print(x.grad)
------------以下出力---------------
tensor(1000000.)
5-11. 自作softmax関数を使った例
自作softmax関数を定義の通りに実装すると予期しない値を得る可能性がある.
ここでsoftmax関数は以下の式で定義される.
\mathbf{x} = [x_1, x_2, ..., x_k, ..., x_K]\\
y_k = \frac{e^{x_k}}{\sum_{j=1}^{K}{e^{x_j}}}
K次元のベクトルに対して,あるk番目の要素のsoftmaxはk番目の要素のexponentialを全ての要素のexponentialの総和で割ったものである.
これを定義して実際に実装したのが以下である.
def softmax(x):
y = torch.exp(x)/torch.sum(torch.exp(x))
return y
x = torch.tensor([1.,2.,3.,4.,5.])
y = softmax(x)
print(y)
------------以下出力---------------
tensor([0.0117, 0.0317, 0.0861, 0.2341, 0.6364])
期待通りのものが出ている.
しかし,ここで以下のような例を考える.
def softmax(x):
y = torch.exp(x)/torch.sum(torch.exp(x))
return y
x = torch.tensor([1000.,2.,3.,4.,5.])
y = softmax(x)
print(y)
------------以下出力---------------
tensor([nan, 0., 0., 0., 0.])
このように,入力に大きな値が入るとsoftmaxの結果はnanになってしまう.
この原因は指数関数の正方向の増加が非常に急であるために$e^{1000}$という値は大きすぎて発散してしまうのである.
そこで,以下のような変数変換をする.
\begin{align}
\mathbf{x} &= [x_1, x_2, ..., x_k, ..., x_K]\\
y_k &= \frac{e^{x_k}}{\sum_{j=1}^{K}{e^{x_j}}}\\
&=\frac{e^{-max(\mathbf{x})}}{e^{-max(\mathbf{x})}}\frac{e^{x_k}}{\sum_{j=1}^{K}{e^{x_j}}}\\
&=\frac{e^{x_k-max(\mathbf{x})}}{\sum_{j=1}^{K}{e^{x_j-max(\mathbf{x})}}}
\end{align}
ここで$max(\mathbf{x})$はベクトルxの中で最も大きい値を意味する.
この変数変換は出力の結果は変わらないのに,exponentialの中の値が小さくなっている.
これの嬉しい点は指数関数の急な正方向ではなく緩やかな負方向で考えることで値が発散しないようになる.
実際に結果を見てみる.
def softmax(x):
y = torch.exp(x-torch.max(x))/torch.sum(torch.exp(x-torch.max(x)))
return y
x = torch.tensor([1000.,2.,3.,4.,5.])
y = softmax(x)
print(y)
------------以下出力---------------
tensor([1., 0., 0., 0., 0.])
5-12. Cross Entropy Loss 学習途中でnanが出る
画像分類をCNNなどで学習させているときに学習の途中でlossがnanになって学習しなくなることがある.
例えばCIFAR10というデータセットを使って画像分類をするような例を以下に示す.
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
PATH = "自分のデータセットがあるpath"
trainset = torchvision.datasets.CIFAR10(root = PATH, train = True, download = True, transform = transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = 100, shuffle = True)
testset = torchvision.datasets.CIFAR10(root = PATH, train = False, download = True, transform = transform)
testloader = torch.utils.data.DataLoader(testset, batch_size = 100, shuffle = False)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2, stride=2)
self.conv1 = nn.Conv2d(1,16,3)
self.conv2 = nn.Conv2d(16,32,3)
self.fc1 = nn.Linear(32 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
device = torch.device("cuda:0")
net = Net()
net = net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0005)
for epoch in range(100):
for (inputs, labels) in trainloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
print(loss.item())
loss.backward()
optimizer.step()
------------以下出力---------------
2.342613
2.051346
...
1.400551
nan
nan
...
上の例では最初にデータセットの定義をし,次にネットワークのクラス定義, 最後にネットワークの学習を行っている.
例の出力を見てみると確かに途中でnanが出始めていることがわかる.
この原因はネットワークの重みの更新値が突然大きくなりすぎて起こってしまう.
解決策は以下のようにいくつかある.
- learning rateを下げる : learning rateが大きすぎて学習のlossが発散することがあるため
- weight decayを上げる : 正則化項の係数の値を大きくすることでNetowrkの重みが大きくなるのを防ぐ
- BachNormを入れる : これにより出力が大きくなりすぎるのを防ぐ
- 入力をNormalizeする : 入力の範囲を0-1だったものを任意の平均と分散値でNormalizeする
今回は学習に使用する入力画像をNormalizeする例を示す.
書き換える部分と前後の部分を以下に示す.
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.5,), (0.5,))])
PATH = "自分のデータセットがあるpath"
trainset = torchvision.datasets.CIFAR10(root = PATH, train = True, download = True, transform = transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = 100, shuffle = True)
testset = torchvision.datasets.CIFAR10(root = PATH, train = False, download = True, transform = transform)
testloader = torch.utils.data.DataLoader(testset, batch_size = 100, shuffle = False)
これでnanが出なくなる.
6.ひとこと
今回はpyTorchのbackwardで見えない部分である自動部分についてとそのできない例についてまとめさせていただいた.
この記事はこれからもそういった例を見つけ次第随時更新していくつもりである.
読みづらい点も多かったと思うが読んでいただきありがとうございます.