【後編】PyTorchの自動微分を使って線形回帰をやってみた
IT技術
後編~PyTorchの自動微分を使って線形回帰に挑戦!~
「PyTorch」を使っていると、こんなことありませんか?
「model.zero_grad() って何やってるんだろう?」
「loss.backward() では、何が計算されているの?」
「Tensor の属性の requires_grad って何?」
そんな方のために、前回に引き続き、「PyTorch」の自動微分による線形回帰を、わかりやすく解説していきます!
前編をまだお読みでない方は、まずはこちらをお読みください。
勾配降下法(Gradient descent method)と「torch.no_grad()」について
まずは、損失カーブを見てみましょう。
1plt.plot(losses)
2plt.show()
良い形で、減少しているのがわかりますね!
これはグラディエントを使って、損失関数の値が下がるように、変数の値を何度もループの中で調節してます。
この手法は、「勾配降下法」と呼ばれます。
先ほど訓練をさせたコードだと、次の部分ですね。
1 # グラディエントを使って変数`w`と`b`の値を更新する。
2 with torch.no_grad():
3 w -= w.grad * lr
4 b -= b.grad * lr
5 w.grad.zero_()
6 b.grad.zero_()
このコードでは、「勾配降下法」で変数の w と b を更新するときに、 with torch.no_grad() を使っています。
「グラディエントの計算を継続しない」という意味です。
これをやらないと、PyTorch はグラフ上の変数を直接変更しているとみなし、エラーになります。
さらに変数 w と b の更新後は、 w.grad.zero_() と b.grad.zero_() で、グラディエントの値を「0」にします。
PyTorch が、グラディエントの値を自動で「0」にしないのは、グラディエントの値を継続して貯めていきたい場合もあるため。
つまり、ユーザーが決めたタイミングで、グラディエントの値を「0」に設定する必要があるわけです。
「with torch.no_grad()」を使わないと?
with torch.no_grad() を使わないと、一体どうなるか、実際にやってみましょう!
まず、変数 w2 と b2 を設定し、予測値 p2 を計算します。
損失値を出して、自動微分を実行していきましょう。
1# 変数を初期化します
2w2 = torch.tensor(1.0, requires_grad=True)
3b2 = torch.tensor(0.0, requires_grad=True)
4
5# 線形モデルによる値の予測
6p2 = w2 * x + b2
7
8# 損失値と自動微分
9loss = mse(p2, y)
10loss.backward()
11
12print('w2 = ', w2.grad)
13print('b2 = ', b2.grad)
14
15>>> w2 = tensor(-149.1921)
16>>> b2 = tensor(-0.5735)
ここまでは、問題ありませんね。
ではグラディエントを使って、変数 w と b の値を、with torch.no_grad() を使わずに更新してみます。
1# グラディエントを使って変数`w`と`b`の値を更新する。
2w2 += w2.grad * lr
3b2 += b2.grad * lr
4
5>>> RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
エラーが出ましたね…。
このエラーの意味は、「グラディエントを必要とする leaf Variable の値を、直接変更することはできない」です。
これを許してしまうと、PyTorch がせっかく自動で作り上げてくれたグラフが、無意味になってしまうのです。
線形回帰の結果
線形回帰の結果を描画して、実際に目で確認しましょう!
1def draw_linear_regression(x, y, p):
2 # PyTorchのTensorからNumpyに変換
3 x = x.numpy()
4 y = y.numpy()
5 p = p.detach().numpy()
6
7 plt.scatter(x, y, marker='.')
8 plt.scatter(x, p, marker='.')
9 plt.xlabel('x')
10 plt.ylabel('y')
11 plt.show()
12
13draw_linear_regression(x, y, p)
結果を描画
散乱しているデータに対して、直線の近似ができていますね!
detach()を呼ばないとエラーになる?
ここで、疑問が浮かんだ方もいるかもしれません。
予測値 p に対して、Numpy に変換するときに、p = p.detach().numpy() と detach() を呼んでいます。
実は、p.numpy() と、直接 Numpy へ変換しようとするとエラーになるのです。
1p.numpy()
2
3>>> RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
エラーのメッセージを読むと、 detach() を挟んでから numpy() を呼べば、解決できそうです。
なぜ、そんなことが必要なのでしょうか?
これは、x.numpy() や p.numpy() が呼ばれたときに、何が起きているかを考えると理解しやすいです。
numpy()を呼ぶときに何が起きているのか
PyTorch は、効率性を重視するので、不必要なデータのコピーなどは、なるべく行わないようになっています。
なので、numpy() が呼ばれたときに返されるのは、コピーされたデータではなく、元のデータを参照したものになっているわけです。
コードを見てみると?
具体的に、コードを見てみましょう。
1v1 = torch.tensor([1., 2.])
2v2 = v1.numpy()
3
4print('v1 = ', v1)
5print('v2 = ', v2)
6
7>>> v1 = tensor([1., 2.])
8>>> v2 = [1. 2.]
この v1 と v2 は、同じデータを参照しており、v1 値を変更すると v2 の値も変更されてしまいます。
両方とも同じデータを指しているので、「両方が変更される」というのは、正確には間違い…。
「変更されたデータを、両方が参照している」と言ったほうが正しいです。
実際に、 v2 の値を変更してみましょう。
1v2[0] = 100
2
3print('v1 = ', v1)
4print('v2 = ', v2)
5
6>>> v1 = tensor([100., 2.])
7>>> v2 = [100. 2.]
v1 と v2 の値が、両方とも同じ値を指しているのが分かりますね!
コピーして利用したい場合は?
もし、切り離したいのであれば、clone() を呼んでコピーする必要があります。
1v3 = v1.clone().numpy()
2v3[0] = 300
3
4print('v1 = ', v1)
5print('v2 = ', v2)
6print('v3 = ', v3)
7
8>>> v1 = tensor([100., 2.])
9>>> v2 = [100. 2.]
10>>> v3 = [300. 2.]
ちなみに、コピーである v3 の変更は、v1 と v2 には反映されません。
detach()は変数から定数を作る
さて、変数に numpy() を呼ぶと、エラーが起きる現象に戻ります。
1v4 = torch.tensor([1., 2.], requires_grad=True)
2v5 = v4.numpy()
3
4>>> RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
エラーは、
「変数(Tensor that requires grad)に、 numpy() を呼べません。代わりに、detach().numpy() を使ってください」
といっています。
噛み砕くと、「PyTorchのグラディエントを、Numpy に持っていくことはできません」ということです。
つまり、グラディエントが必要のない「定数の Tensor」にすればいいわけです。
それが、 detach() の役割ということですね!
イメージ的には「変数をグラフから離す(detach)」ですが、実際には元の変数に変更はないので、「変数から定数を作る」という解釈の方がより正しいです。
コピーがされないことに注意!
ここでも、データ自体はコピーされないので注意が必要です。
1v6 = v4.detach().numpy()
2v6[0] = 600
3
4print('v4 = ', v4)
5print('v6 = ', v6)
6
7>>> v4 = tensor([600., 2.], requires_grad=True)
8>>> v6 = [600. 2.]
v6 は、requires_grad がないので、変数ではないのがわかります。
ただし、v4 と v6 は同じデータを参照しているので、v6 に対する変更が v4 からも参照されているのです。
データ変更するなら、cloneかcopyを使う
もし、データを変更する必要があるならば、「clone」や「copy」を使って、データそのものを複製しましょう!
1v7 = torch.tensor([1., 2.], requires_grad=True)
2
3v8 = v7.detach().clone().numpy()
4v9 = v7.detach().numpy().copy()
5
6v8[0] = 800
7v9[0] = 900
8
9print('v7 = ', v7)
10print('v8 = ', v8)
11print('v9 = ', v9)
12
13>>> v7 = tensor([1., 2.], requires_grad=True)
14>>> v8 = [800. 2.]
15>>> v9 = [900. 2.]
v7 と v8 と v9 は、全て別のデータを参照しているのがわかります。
PyTorchの「nnパッケージ」で同じことをやってみる
比較として、PyTorchの「nn パッケージ」でも、線形回帰をやってみました。
早速、結果からみていきましょう!
「nn パッケージ」での線形回帰の結果
1import torch.nn as nn
2
3# 線形モデル
4model1 = nn.Linear(1, 1)
5
6# パラメータの初期化
7torch.nn.init.constant_(model1.weight, 1.0)
8torch.nn.init.constant_(model1.bias, 0.0)
9
10# 平均2条誤差
11criteria = nn.MSELoss()
12
13# 勾配降下法と学習率
14optimizer = torch.optim.SGD(model1.parameters(), lr=1.0e-4)
15
16losses = []
17for epoch in range(3000):
18 # 線形モデルによる値の予測
19 p = model1(x.reshape(-1, 1))
20
21 # グラディエントをゼロにリセット
22 model1.zero_grad()
23
24 # 損失値と自動微分
25 loss = criteria(p, y.reshape(-1, 1))
26 loss.backward()
27
28 # 勾配降下法でパラメータを更新
29 optimizer.step()
30
31 # グラフ描画用
32 losses.append(loss.item())
33
34print('loss = ', loss.item())
35print('weight = ', model1.weight.item())
36print('bias = ', model1.bias.item())
37
38>>> loss = 24.312515258789062
39>>> weight = 2.007673501968384
40>>> bias = 0.01103506051003933
weight が「近似直線の傾き」で、 bias が「切片の値」です。
この方法も、「weight は2に近く、bias が0に近い」ので、うまく近似できていますね!
損失カーブは?
損失関数カーブも良い感じです。
1plt.plot(losses)
2plt.show()
近似の直線は?
近似の直線も、うまくデータを説明できています。
1draw_linear_regression(x, y, p)
プログラムを追ってみよう!
あとは、コメントを見ながら、プログラムの流れを追ってみてください。
なぜ model.zero_grad() が呼ばれているのか、loss.backward() でどんなことが起きているのかも、想像できるはずです。
プログラムの全体的な流れとしては、「nn パッケージ」を使わない線形回帰のバージョンと、あまり違いがないですよね?
grad とか、 with torch.no_grad() などが出てこないぶん、簡単になっていますよ。
「nn パッケージ」の注意点
ただし、2つだけ注意点があります。
ひとつ目は、x のデータタイプが torch.float32 になるように、Numpyでは x を設定したときに np.float32 を指定していました。
これは「nn パッケージ」のモジュールが、Float 64型を受け付けないからです。
2つ目は、x.reshape(-1, 1) と y.reshape(-1, 1) を呼んでいること。
これは、nn.Linear では x の次元を、「(バッチサイズ、入力値の変数の数)」の形で期待しているからです。
y に関しても、同様に「(バッチサイズ、実測値の次元)」である必要があります。
さいごに
お疲れ様でした!
今回は、PyTorch の自動微分を使って、「線形回帰」を実装してみました。
nn.Linear を、「使わないバージョン」と「使うバージョン」を通して、自動微分についての理解が深まったと思います。
今回紹介したコードでは、入力値が1つの変数である「単回帰」を扱いましたが、ディープラーニングなどでは何百もの変数の値を、 nn.Linear に渡します。
「nn パッケージ」は、とてもありがたいものですね!
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit