• トップ
  • ブログ一覧
  • 【後編】PyTorchの自動微分を使って線形回帰をやってみた
  • 【後編】PyTorchの自動微分を使って線形回帰をやってみた

    広告メディア事業部広告メディア事業部
    2020.09.25

    IT技術

    後編~PyTorchの自動微分を使って線形回帰に挑戦!~

    PyTorch」を使っていると、こんなことありませんか?

    model.zero_grad() って何やってるんだろう?」

    loss.backward() では、何が計算されているの?」

    「Tensor の属性の requires_grad って何?」

    そんな方のために、前回に引き続き、「PyTorch」の自動微分による線形回帰を、わかりやすく解説していきます!

    前編をまだお読みでない方は、まずはこちらをお読みください。

    featureImg2020.09.23【前編】PyTorchの自動微分を使って線形回帰をやってみた前編~PyTorchの自動微分を使って線形回帰に挑戦!~「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_()

    このコードでは、「勾配降下法」で変数の wb を更新するときに、 with torch.no_grad() を使っています。

    グラディエントの計算を継続しない」という意味です。

    これをやらないと、PyTorch はグラフ上の変数を直接変更しているとみなし、エラーになります。

    さらに変数 wb の更新後は、 w.grad.zero_()b.grad.zero_() で、グラディエントの値を「0」にします。

    PyTorch が、グラディエントの値を自動で「0」にしないのは、グラディエントの値を継続して貯めていきたい場合もあるため。

    つまり、ユーザーが決めたタイミングで、グラディエントの値を「0」に設定する必要があるわけです。

    「with torch.no_grad()」を使わないと?

    with torch.no_grad() を使わないと、一体どうなるか、実際にやってみましょう!

    まず、変数 w2b2 を設定し、予測値 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)

    ここまでは、問題ありませんね。

    ではグラディエントを使って、変数 wb の値を、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.]

    この v1v2 は、同じデータを参照しており、v1 値を変更すると v2 の値も変更されてしまいます。

    両方とも同じデータを指しているので、「両方が変更される」というのは、正確には間違い…。

    変更されたデータを、両方が参照している」と言ったほうが正しいです。

    実際に、 v2 の値を変更してみましょう。

    1v2[0] = 100
    2
    3print('v1 = ', v1)
    4print('v2 = ', v2)
    5
    6>>> v1 =  tensor([100.,   2.])
    7>>> v2 =  [100.   2.]

    v1v2 の値が、両方とも同じ値を指しているのが分かりますね!

    コピーして利用したい場合は?

    もし、切り離したいのであれば、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 の変更は、v1v2 には反映されません。

    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 がないので、変数ではないのがわかります。

    ただし、v4v6 は同じデータを参照しているので、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.]

    v7v8 と 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 パッケージ」は、とてもありがたいものですね!

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事