😳

PyTorchの気になるところ

2022/11/03に公開

はじめに

PyTorchを普段触っているのですが、細かいところをなんとなくで今まで過ごしてきたので、今回しっかりまとめていきたいと思います。自分の備忘録になっていますが、少しでも参考になればと思います。

(Qiitaで書いていたものを引っ越してきたものです)

この記事の対象者

  • .copy(), .detach(), .bachward()がよくわかっていない方
  • nn.Conv2dのweightやbiasの取得ってどうやんの?
  • model.eval()って結局何してる?って方
  • torch.no_grad(), torch.set_grad_enabled()の区別がわからない方
  • F.relu, nn.ReLUの違いなんやねんって方

コアなネタが多いですが、よかったら参考にしてみてください。

この記事の内容

1. Tensorの操作テクニック
2. Layerのweightやbiasの取得と初期化
3. model.eval()の振る舞いについて
4. torch.no_grad()とtorch.set_grad_enabled()の違い
5. nn.ReLUとnn.functional.reluの違い

1.Tensorの操作テクニック

Tensorから値の取り出し

item()を使う。かっこも忘れずにつけましょう。

>>> import torch
>>> x = torch.tensor([[1]])
>>> x
tensor([[1]])
>>> x.item()
1
>>> 

自動微分

tensorでrequires_grad = Trueにすると自動微分可能です。
.backwardで逆伝搬(勾配情報を計算)。.gradで勾配取得。

>>> x = torch.tensor([[1., -1.], [1., 1.]], requires_grad=True)
>>> out = x.pow(2).sum()
>>> out.backward()
>>> x.grad
tensor([[ 2., -2.],
        [ 2.,  2.]])
>>> 
>>> x = torch.tensor(2.5, requires_grad=True)
>>> y = 5*x + 2
>>> y.backward()
>>> y
tensor(14.5000, grad_fn=<AddBackward0>)
>>> x
tensor(2.5000, requires_grad=True)
>>> x.grad
tensor(5.)
>>> 

grad_fn=<AddBackward0>は、yの計算過程にrequires_grad=Trueの変数がありbackward()可能ですよってことを示しているようです。勾配情報は、.gradでアクセス。

requires_grad=Trueとした変数に対してはin-place operationができないらしい、、
とりあえず自動微分をしたいなら、Tensor型でかつrequires_grad=Trueにすれば良い。

Tensornumpyの変換

numpyのarrayをtensorに変換するには、from_numpy()を使います。
自動で型が決まることに注意してください。PyTorchで機械学習をやるときはdtype=torch.float32で扱うことが多いです。
castして置くことをオススメします。

>>> a = np.array([2.1,3.6,4.9])
>>> a
array([2.1, 3.6, 4.9])
>>> b = torch.from_numpy(a)
>>> b.dtype
torch.float64
>>> 

from_numpy()を使用していると、メモリが共有されているようなのでメモリを共有したくないときはcopy()を使うようにします。

>>> a
array([2.1, 3.6, 4.9])
>>> b
tensor([2.1000, 3.6000, 4.9000], dtype=torch.float64)
>>> b[0] = 1.1
>>> a
array([1.1, 3.6, 4.9])
>>> 

>>> a = np.array([2.1,3.6,4.9])
>>> b = torch.from_numpy(a.copy())
>>> a
array([2.1, 3.6, 4.9])
>>> b
tensor([2.1000, 3.6000, 4.9000], dtype=torch.float64)
>>> b[0] = 1.1
>>> a
array([2.1, 3.6, 4.9])
>>> 

tensorからnumpyへの変換
.numpy()を使用すれば良いのだが上記と同じようにメモリをシェアしてしまっているので、.clone()を使うようにしましょう。

>>> a = torch.tensor(np.array([1, 2, 3, 4]))
>>> b = a.clone().numpy()
>>> a
tensor([1, 2, 3, 4])
>>> b
array([1, 2, 3, 4])
>>> b[0] = -1
>>> a
tensor([1, 2, 3, 4])
>>> b
array([-1,  2,  3,  4])
>>> 

このときに注意が必要で、tensorはGPUで計算可能であったり、勾配情報を保持しているので直接numpyに代入できない時がある。解決策は、

  1. GPU->CPU への変換が必要 XXX.to('cpu')
  2. 勾配情報を無視する XXX.detach()
>>> x = torch.ones([2, 2], device=device, requires_grad=True)
>>> device = 'cpu'
>>> x = x.to(device)
>>> x2 = x.detach().clone().numpy()
>>> 

2. Layerのweightやbiasの取得と初期化

重みの取得は、XXX.weightでバイアスの取得はXXX.biasで行います。

>>> import torch.nn as nn
>>> dense = nn.Linear(3,2)
>>> dense
Linear(in_features=3, out_features=2, bias=True)
>>> dense.weight
Parameter containing:
tensor([[-0.4833,  0.4101, -0.2841],
        [-0.4558, -0.0621, -0.4264]], requires_grad=True)
>>> dense.bias
Parameter containing:
tensor([ 0.5758, -0.2485], requires_grad=True)
>>> 

weightに対しては、nn.initメソッドを使用することで重みの初期化ができます。
例として以下では、正規分布で初期化する例です。

>>> dense.weight
Parameter containing:
tensor([[-0.4833,  0.4101, -0.2841],
        [-0.4558, -0.0621, -0.4264]], requires_grad=True)
>>> nn.init.normal_(dense.weight, 0.0, 1.0)
Parameter containing:
tensor([[ 0.6169, -0.4530, -0.8232],
        [ 0.8218,  1.0096,  1.1140]], requires_grad=True)
>>> 

XXX.weight.dataで勾配情報を持っていないtensorを取得できます。

>>> dense
Linear(in_features=3, out_features=2, bias=True)
>>> dense.weight
Parameter containing:
tensor([[ 0.6169, -0.4530, -0.8232],
        [ 0.8218,  1.0096,  1.1140]], requires_grad=True)
>>> dense.weight.data
tensor([[ 0.6169, -0.4530, -0.8232],
        [ 0.8218,  1.0096,  1.1140]])
>>> 

正規分布 : nn.init.normal_(weight, mean, std)
一様分布 : nn.init.uniform_(weight,a,b)
定数 : nn.init.constant_(weight, c)
Xavier : nn.init.xavier_normal_(weight, gain=1)
He : nn.init.kamiming_normal_(weight)

いろんな初期化の方法があるので、タスク合わせて使用しましょう。
biasの初期化もweightの初期化と同じようにできます。ちなみにnn.Linearの重みの初期化はデフォルトでHeの初期化が使われています。

モデルのパラメーターについて

モデルのパラメータ確認方法。net.parameters()でモデルのパラメータを取得できるので中身がみたければ、for文でまわして見れば良いかと思います。

モデルは適当です。

 
import torch
import torch.nn as nn
import torch.optim as optim

class Net(nn.Module):
     def __init__(self):
         super(Net, self).__init__()
         self.conv1 = nn.Conv2d(3,5, kernel_size=2)
         self.bn = nn.BatchNorm2d(5)
         self.relu = nn.ReLU(inplace=True)
  
     def forward(self,x):
         x = self.conv1(x)
         x = self.bn(x)
         x = self.relu(x)
         return x
  
  
net = Net()
print(net)
for param in net.parameters():
     print(param)
  
print(net.state_dict())
print(net.state_dict().keys())

出力は以下になります。

Net(
  (conv1): Conv2d(3, 5, kernel_size=(2, 2), stride=(1, 1))
  (bn): BatchNorm2d(5, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
)
Parameter containing:
tensor([[[[ 0.2479, -0.0084],
          [ 0.0205,  0.1804]],

         [[ 0.0198,  0.1924],
          [ 0.1032, -0.1188]],

      .......


         [[-0.1267,  0.1418],
          [-0.2472,  0.2115]],

         [[ 0.0276,  0.2738],
          [ 0.2632,  0.0726]]]], requires_grad=True)
Parameter containing:
tensor([ 0.1161, -0.0659, -0.0313,  0.0420, -0.0755], requires_grad=True)
Parameter containing:
tensor([1., 1., 1., 1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0., 0., 0., 0.], requires_grad=True)

パラメータの名前を取得したいときは、net.state_dict().keys()を使います。

print(net.state_dict().keys())
# odict_keys(['conv1.weight', 'conv1.bias', 'bn.weight', 'bn.bias', 'bn.running_mean', 'bn.running_var', 'bn.num_batches_tracked'])

optimizerのパラメーターについて

せっかくなので、optimizerのパラメーターについても見ていきたいと思います。
まずは、optimizerにnet.parameters()を渡します。つまりoptimizerは、渡したパラメータを使って最適化していきます。
optimizer.param_groupsでパラメータの全部を確認できます。
あとは、欲しい情報にアクセスしてみてください。以下は例です。

optimizer = optim.SGD(net.parameters(),lr=1e-3)
print(optimizer.param_groups)
print(optimizer.param_groups[0]['lr'])
print(optimizer.param_groups[0]['params'][0])

3. model.eval()の振る舞いについて

機械学習でモデルを作成して、学習しているときにvalidationやtestの時model.eval()って書きますよね。結局あれって何しているのか気になっている方もいるかと思います。

結論から言いますと、
モデルの振る舞いが変わります。例をあげると、DropoutやBatch Normです。
これらのモジュールは、training/evaluation の二種類のモードを持っています。

##eval()

Sets the module in evaluation mode.
This has any effect only on certain modules. See documentations of particular modules for details of their behaviors in training/evaluation mode, if they are affected, e.g. Dropout, BatchNorm, etc.
This is equivalent with self.train(False).

Batch normは、学習の際はバッチ間の平均や分散を計算しています。
推論するときは、平均/分散の値が正規化のために使われます。
まとめると、eval()はdropoutやbatch normの on/offの切替です。

4. torch.no_grad()とtorch.set_grad_enabled()の違い

PyTorchをはじめたとき、いろんな方のコードをみていると**torch.no_grad()**って書いている人もいれば **torch.set_grad_enabled()**と書いている人もいて、え?ってなっていたことを今でも覚えています。

1. torch.no_grad

これがあるブロックは勾配の計算をしないようになっています。それによって、メモリ消費を減らすことができるみたいです。
以下は、公式のドキュメントです。

Context-manager that disabled gradient calculation.
Disabling gradient calculation is useful for inference, when you are sure that you will not call >Tensor.backward(). It will reduce memory consumption for computations that would otherwise have >requires_grad=True.
In this mode, the result of every computation will have requires_grad=False, even when the inputs have requires_grad=True.

2. torch.set_grad_enabled()

これは勾配の計算をするかしないかの on/off の切り替えができるとのこと。
つまり、機械学習のコードを書いていて、trainとvalをひとつにまとめて書きたい時などに使えます。コードの量を減らせると思います。

Context-manager that sets gradient calculation to on or off.
set_grad_enabled will enable or disable grads based on its argument mode. It can be used as a context-manager or as a function.
When using enable_grad context manager, set_grad_enabled(False) has no effect.
This context manager is thread local; it will not affect computation in other threads.

簡単な例です。

>>> is_train = False
>>> with torch.set_grad_enabled(is_train):
...   y = x * 2
>>> y.requires_grad
False
>>> torch.set_grad_enabled(True)
>>> y = x * 2
>>> y.requires_grad
True
>>> torch.set_grad_enabled(False)
>>> y = x * 2
>>> y.requires_grad
False

5. nn.ReLUとnn.functional.reluの違い

確かにこの2つは、なんであるのって思っていました。

ある方は、このように答えていました。

nn.ReLU() creates an nn.Module which you can add e.g. to an nn.Sequential model.
nn.functional.relu on the other side is just the functional API call to the relu function, so that you can add it e.g. in your forward method yourself.

nn.ReLU()は、nn.Moduleを作ります。つまり、nn.Sequential()に追加できます。反対にnn.functional.reluは、relu関数を呼ぶだけで、もし記述するならforward methodの方に書きます。好きな方を使えば良いみたいですが、個人的には、print(model)とモデルの構造を表示させたときにreluも表示させたいので、nn.ReLU()の方を使います。

終わりに

実際にコードを書いてたり、読んだりしていて気になっていたところをまとめてみました。文字に起こすことによって頭の中が整理され、今までより理解できました。困ったら公式のドキュメントをしっかり読むことの重要性を改めて感じました。

参考文献

Discussion