ホーム
記事一覧
プライバシーポリシー
免責事項
お問い合わせ

PyTorchの勾配更新方法の解説

2020-07-07

今回もPyTorchに関する記事です。

この記事では、requires_gradzero_gradなどについて説明します。

私自身も勉強中ということもあり間違い等あるかもしれません。その際はTwitterなどで教えてください。

requires_grad とは

【学び直し】Pytorchの基本とMLPでMNISTの分類・可視化の実装までで紹介したように、requires_gradは自動微分を行うためのフラグです。

単純にtensorを定義した場合はデフォルトでFalseになっています。

x = torch.ones([3, 32, 32])
x.requires_grad
# >>> False

一方で、ネットワークを定義した場合のパラメータはデフォルトでrequires_grad=Trueです。

意外とこれを知らずにわざわざ学習時にrequires_grad=Trueを設定していることがあります。

実際に定義したネットワークで確かめてみました。

# Networkを定義
class MLPNet (nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(1 * 28 * 28, 512)
        self.fc2 =nn.Linear(512, 512)
        self.fc3 = nn.Linear(512, 10)
        self.dropout1=nn.Dropout2d(0.2)
        self.dropout2=nn.Dropout2d(0.2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.dropout1(x)
        x = F.relu(self.fc2(x))
        x = self.dropout2(x)
        return F.relu(self.fc3(x))

net = MLPNet().to(device)
for name, p in net.named_parameters():
    print(name, p.requires_grad)

# >>> fc1.weight True
# >>> fc1.bias True
# >>> fc2.weight True
# >>> fc2.bias True
# >>> fc3.weight True
# >>> fc3.bias True

このようにrequires_grad=Trueになってます。 ちなみにnamed_parameters()とすることでレイヤーの名前とパラメータを参照できます。

ドキュメントによるとネットワークで各層を定義した際に用いられるnn.Prameterがデフォルトでrequires_grad=Trueなので上記の結果になります。

requires_grad=Trueが求められるのは、backwardで勾配を計算したいところです。

逆に、勾配の更新を行わないところは明示的にrequires_grad=Falseとする必要があります。

optimについて

optimはpytorchで学習を行う際に用いる最適化関数です。

今回も簡単な式で挙動を確認します。

import torch
import torch.optim as optim

x = torch.tensor(3.0, requires_grad=True)
w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
yy = torch.tensor(5.0, requires_grad=True)
y = w * x + b

opt = optim.SGD([w,b], lr=0.01, momentum=0.9)

optimの引数には学習したいパラメータと学習率などを渡します。

ここで、学習したいパラメータはiterationの形である必要があるためカッコでくくってます。

実際にoptimで最適化してみます。

criterion = nn.MSELoss()

for epoch in range(3):
    opt.zero_grad()
    y = w * x + b
    loss = criterion(y, yy)
    loss.backward()
    opt.step()

まず、opt.zero_grad()で勾配をゼロにリセットします。

これをしないと勾配が蓄積されたままになってしまうので、正しい方向に更新されなくなります。

数式でかくと、パラメータ$w$を更新する際は以下のようになります(SGDの場合)。

$$ w ← w - lr \frac{\partial L}{\partial w} $$

opt.zero_grad()を使わないと、偏微分部分が前の勾配との和になってしまうので、正しい方向に更新されません。

簡単に手計算で確かめられるので、一度計算してみると何が行われているかイメージできます。

パラメータの更新について

PyTorchを使い始めてまず混乱するのが、なぜoptimの操作でmodelの値が更新されるのかです。

結論からいうと、modelパラメータのアドレスをoptimの初期化のときに渡しているからです。

opt = optim.SGD([w,b], lr=0.01, momentum=0.9)

ここまでは大丈夫だと思います。

気をつけないといけないのが、学習の途中でパラメータをoptim以外の方法で更新する場合です。

例えば、GANで重みの移動平均を計算する場合など。

そういう時は.dataで値を更新します。

modelは先ほどのMLPだとして、まずは普通に値を更新します。

net = MLPNet().to(device)

# 最適化方法
optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)

net.fc1.weight = nn.Parameter(torch.ones([512, 784]))
print(net.fc1.weight)
print(optimizer.param_groups[0]['params'][0])

この時の出力は以下のようになります。

Parameter containing:
tensor([[1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        ...,
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.]], requires_grad=True)
Parameter containing:
tensor([[ 0.0323, -0.0328, -0.0338,  ..., -0.0061, -0.0316,  0.0097],
        [-0.0309, -0.0281,  0.0229,  ...,  0.0164, -0.0236,  0.0347],
        [-0.0184, -0.0026,  0.0323,  ..., -0.0353, -0.0310,  0.0208],
        ...,
        [ 0.0015,  0.0148,  0.0143,  ..., -0.0112, -0.0215,  0.0123],
        [ 0.0332, -0.0148,  0.0235,  ..., -0.0001, -0.0148, -0.0104],
        [ 0.0044, -0.0282, -0.0292,  ..., -0.0311, -0.0068,  0.0349]],
       device='cuda:0', requires_grad=True)

このようにoptimizerの方の値は変わっていないです。

なので、optimizer以外で重みを更新する場合は以下のようにします。

# .dataで更新する
net.fc1.weight.data = nn.Parameter(torch.ones([512, 784]))
print(net.fc1.weight)
print(optimizer.param_groups[0]['params'][0])

こうするとoptimizerの方の値も書き換わります。

Parameter containing:
tensor([[1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        ...,
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.]], requires_grad=True)
Parameter containing:
tensor([[1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        ...,
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.],
        [1., 1., 1.,  ..., 1., 1., 1.]], requires_grad=True)

まとめ

PyTorchは便利で、なんとなく書いてもできてしまいます。

しかし、ちょっと発展的な内容を実装しようと思うと、勾配がどのように更新されるかなどを知らないと実装できないです。

今回実際に、細かく処理の経過を出力したりすることでだいぶ理解が進みました。