• トップ
  • ブログ一覧
  • 【後編】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...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background