LoginSignup
240
185

More than 3 years have passed since last update.

pyTorchのNetworkのパラメータの閲覧と書き換え

Last updated at Posted at 2019-09-29

2019/9/29 投稿
2019/11/8 やや見やすく編集(主観)
2020/2/1 SGDの解説Link追加
2020/4/22 パラメータを途中で書き換える方法を追加した

0. この記事の対象者

  • pythonを触ったことがあり,実行環境が整っている人
  • pyTorchをある程度触ったことがある人
  • pyTorchによる機械学習でNetworkのパラメータを閲覧,書き換えしたい人
  • pyTorchによる機械学習でNetworkのパラメータを途中で書き換えたい人

1. はじめに

昨今では機械学習に対してpython言語による研究が主である.なぜならpythonにはデータ分析や計算を高速で行うためのライブラリ(moduleと呼ばれる)がたくさん存在するからだ.
その中でも今回はpyTorchと呼ばれるmoduleを使用し,Networkからパラメータの操作周りのことを閲覧,最初の書き換え,途中の書き換えの3つについて説明する.

ただしこの記事は自身のメモのようなもので,あくまで参考程度にしてほしいということと,簡潔に言うために間違った表現や言い回しをしている場合があるかもしれないが,そこはどうかご了承していただきたい.

また,この記事ではNetworkを定義,そこから操作するだけであって,実際にNetworkを使って学習をしたりはしない.
そちらに興味がある場合は以下のLinkを参照してほしい.

pyTorchでCNNsを徹底解説

2. 事前知識

pythonには他言語同様,「」というものが定義した変数には割り当てられており,中でも「list型」,「tuple型」,「dictionary型」が今回の記事ではよく出てくるように思う.さらに,moduleとして「numpy」というものもあり,このnumpyが持つ特殊な型,「ndarray型」もよく出てくる.

ここではあえて説明はしないが,わからない人は是非検索をしてそれぞれをしっかり理解しておいてほしい.

3. pyTorchのインストール

pyTorchを初めて使用する場合,pythonにはpyTorchがまだインストールされていないためcmdでのインストールをしなければならない.
下記のLinkに飛び,ページの下の方にある「QUICK START LOCALLY」で自身の環境のものを選択し,現れたコマンドをcmd等で入力する(コマンドをコピペして実行で良いはず).

pytorch 公式サイト

4. pyTorchに用意されている特殊な型

numpyにはndarrayという型があるようにpyTorchには「Tensor型」という型が存在する.
ndarray型のように行列計算などができ,互いにかなり似ているのだが,Tensor型はGPUを使用できるという点で機械学習に優れている.
なぜなら機械学習はかなりの計算量が必要なため計算速度が早いGPUを使用するからだ.
Tensor型の操作や説明は下記Linkより参照していただきたい.

pyTorchのTensor型とは

ただし,機械学習においてグラフの出力や画像処理などでnumpyも重要な役割を持つ.そのためndarrayとTensorを交互に行き来できるようにしておくことがとても大切である.

5. pyTorchによるNetworkの作成

5-1. pyTorchのimport

ここからはcmd等ではなくpythonファイルに書き込んでいく.
下記のコードを書くことでmoduleの使用をする.

filename.rb
import torch
import torch.nn as nn

ここで「import torch.nn as nn」はNetwork内で使用する関数のためのものである.
torch.nn moduleはたくさんの関数を保持しているのだ.

5-2. Networkの作成

以下に簡単なConvolutional Neural Networks(CNNs)を作成したコードを示す.
ただしこのCNNsはMNISTというDatasetを使用することを想定して作ったため,他のDatasetを使用する場合は各パラメータが変わることがあるので注意する.

filename.rb
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(2, stride=2)

        self.conv1 = nn.Conv2d(1,16,3)
        self.conv2 = nn.Conv2d(16,32,3)

        self.fc1 = nn.Linear(32 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool(x)
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

以下にNetworkの説明をする.

5-2-1. classとしてNetworkを作成

Networkをclassとして作成する.
雛形で言えばこんな形になる.

filename.rb
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
           ......

    def forward(self, x):
           ......

以下各行の説明.

  • 1行目の 「Net」はただの名前だから好きなもので良い.
    その名前の後の「nn.Module」はこのclassがnn.Moduleというclassを継承していることを意味する.
    なぜ継承するかというとnn.ModuleがNetworkを操作する上でパラメータ操作などの重要な機能を持つためである.

  • 2行目の「def __init__(self)」は初期化関数の定義で,コンストラクタなどと呼ばれる.初めてclassを呼び出したときにこの中身のものが実行される.

  • 3行目の「super(Net, self).__init__()」は継承したnn.Moduleの初期化関数を起動している.superの引数の「Net」はもちろん自身が定義したclassの名前である.

  • 最後の「def forward(self, x)」には実際の処理を書いていく.

5-2-2. 初期化関数の定義

以下に初期化関数部分のみを再掲する.

filename.rb
    def __init__(self):
        super(Net, self).__init__()
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(2, stride=2)

        self.conv1 = nn.Conv2d(1,16,3)
        self.conv2 = nn.Conv2d(16,32,3)

        self.fc1 = nn.Linear(32 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 10)

この中で定義した「self.xxxxx」という変数はずっと保持される.
なので,convolutionやfully connectなどの学習に必要なパラメータを保持したいものなどをここに書いていく.
以下にコードの説明をする.

  • nn.ReLU()」は活性化関数というもので,各層の後に必ずと言っていいほど使用されている.種類はたくさんある.

  • nn.MaxPool2d(2, stride=2)」はpooling用でこのパラメータの場合データサイズが半分になるような処理をする(サイズの小数点以下は切り下げられる).

  • nn.Conv2d(1,16,3)」はconvolution(畳み込み)の定義で,第1引数はその入力のチャネル数,第2引数は畳み込み後のチャネル数,第3引数は畳み込みをするための正方形フィルタ(カーネルとも言う)の1辺のサイズである.
    入力dataの画像サイズH×Wと出力の画像サイズH×Wは通常異なるものとなり,Convolutionのカーネルサイズなどから計算できる.計算方法は以下Linkを見てほしい.
    nn.Conv2dの公式ページ

  • nn.Linear(32 * 5 * 5, 120)」はfully connectの定義で,第1引数は入力のサイズ(ただし入力は行列ではなくベクトルでなければならない),第2引数は出力後のベクトルサイズを示す.
    この場合の第1引数の意味は入力ベクトルがベクトルになる前のdataでは32Channel × 5Height× 5Width(5×5の画像が32枚)となっていたことがわかる.どうやってベクトルにしたかは5-2-3で説明する.

注意してほしいのは,この「nn.Linear(32 * 5 * 5, 120)」より32ChannelというのはConvolutionの最後の出力を見ればわかるが,5と5については入力画像のサイズからConvolutionとpoolingを経てどれだけサイズが小さくなるかを事前に計算して得なければならないのである.

これらの他にも多数の関数をpyTorchは用意しており,それらは以下のLinkから参照してほしい.

torch.nn 公式サイト

5-2-3. 処理内容の定義

以下に処理部分のみ再掲する.

filename.rb
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool(x)
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

def forward(self, x)」の引数xがこのネットワークに対しての入力dataである.もちろんこのxはdataloaderから取得したバッチサイズ数のdata群である.
このように入力xを__init__()で定義した各層に上から順に入力していく.
このxは「x = xxxx(x)」のように毎回更新されていることに注意して欲しい.

ここで「x.view(x.size()[0], -1)」はTensor型の形を変換するもので,引数を 縦×横 とみた行列に変換される(この-1の意味は片方のサイズになるようにもう片方を自動でサイズ調節するということである).
これが先程言った行列をベクトルの形に変換する操作である.

詳しく説明すると「x.size()」というのはxのサイズを返すもので(Batchsize,Channel,Height,Width)というtuple型が返ってくる.
その0番目をx.size()[0]で指定しているから,書き換えるとこの文は「x.view(Batchsize, -1)」ということになっている.
ということは縦がBatchsize(今の場合100個)になるように横を調節すると,100個の行ベクトルができたことと同じになっているのである(つまり100個の行列データを100個のベクトルデータにした).

5-3. Networkの使用

Networkの使用宣言を以下に示す.

filename.rb
net = Net()

これはNet() classのインスタンスとなるnetを生成している.
この瞬間に定義した初期化関数「__init__()」が実行している.
このnetがパラメータやNetworkの構造を保持していることとなるのだ.
netを出力してみるとその詳細が以下のように確認できる.

filename.rb
print(net)

------'''以下出力結果'''--------
Net(
  (relu): ReLU()
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
  (fc1): Linear(in_features=800, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=10, bias=True)
)

6. Networkのパラメータ閲覧

今,netがNetworkの情報を持っている.
Networkのパラメータの閲覧方法は複数あり,今回はその中でも以下の閲覧方法を説明する.

  • net.parameters()の使用
  • net.state_dict()の使用
  • net.xxxxx.weightの使用
  • optimizerの使用

6-1. net.parameters()の使用

単純にパラメータを閲覧したい場合は「net.parameters()」を使用し,以下のようにすればよい.

filename.rb
for param in net.parameters():
    print(param)

------'''以下出力結果'''--------
Parameter containing:
tensor([[パラメータ1],[パラメータ2],...,[パラメータ16]],requires_grad=True)
Parameter containing:
tensor([パラメータ1のバイアス,パラメータ2のバイアス,...,パラメータ16のバイアス], requires_grad=True)
Parameter containing:
tensor([[パラメータ1],[パラメータ2],...,[パラメータ32]],requires_grad=True)
Parameter containing:
tensor([パラメータ1のバイアス,パラメータ2のバイアス,...,パラメータ32のバイアス], requires_grad=True)
Parameter containing:
tesnor([[パラメータ1],[パラメータ2],...,[パラメータ120]], requires_grad=True)
Parameter containing:
tesnor([[パラメータ1のバイアス],[パラメータ2のバイアス],...,[パラメータ120のバイアス]],requires_grad=True)
Parameter containing:
tesnor([[パラメータ1],[パラメータ2],...,[パラメータ10]],device='cuda:0', requires_grad=True)
Parameter containing:
tesnor([[パラメータ1のバイアス],[パラメータ2のバイアス],...,[パラメータ10のバイアス]],requires_grad=True)

上から順に,この出力は以下のようになっている.

  1. conv1のパラメータ
  2. conv1のバイアス
  3. conv2のパラメータ
  4. conv2のバイアス
  5. fc1のパラメータ
  6. fc1のバイアス
  7. fc2のパラメータ
  8. fc2のバイアス

各パラメータの個数(パラメータ16とか書いてる)が異なっていてNetworkのそれぞれの層の定義に依存している.
この「net.parameters()」がパラメータの情報を引き出しているのだが,単純に出力せず,for文を使用していることに注意してほしい.
単純に出力してしまうと以下のようになってしまう.

filename.rb
print(net.parameters())

------'''以下出力結果'''--------
<generator object Module.parameters at 0x7fb663b45c00>

これは「net.parameters()」がnetのパラメータを参照しているわけではなく,netのパラメータを持ったiteratorを返しているからである.
つまり「in」というのを使用しないと値を取り出せないのである.
もちろん,これはただの出力のために値を取り出しているだけであるため,パラメータの書き換えなどはできない.

6-2. net.state_dict()の使用

以下に出力を示す.

filename.rb
print(net.state_dict()['conv1.weight'])

------'''以下出力結果'''--------
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
               ....
        [conv1のチャネル16のパラメータ]])

この「['conv1.weight']」のように閲覧したいパラメータを指定する.
つまり「['定義した名前.weight']」とすることでそのパラメータが閲覧できる.
このように配列参照のようにして,かつそれが数字ではなく文字列での参照であるから「net.state_dict()」は辞書型(collections.OrderedDict型)であることがわかり,もちろん辞書のインデックス名も以下のように確認できる.

filename.rb
print(net.state_dict().keys())

------'''以下出力結果'''--------
odict_keys(['conv1.weight', 'conv1.bias', 'conv2.weight', 'conv2.bias', 'fc1.weight', 'fc1.bias', 'fc2.weight', 'fc2.bias'])

6-3. net.xxxxx.weightの使用

以下に出力を示す.

filename.rb
print(net.conv1.weight)

------'''以下出力結果'''--------
Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
               ....
        [conv1のチャネル16のパラメータ]],requires_grad=True)

この「net.conv1.weight」のように閲覧したいパラメータを指定する.
前のパラメータ参照と違う点は出力に「requires_grad」があるかないかであるが,だからと言って前の参照法では勾配情報がないというわけではない.
要はどちらでもよい.

また,バイアス情報を見たいときは以下のようにする.

filename.rb
print(net.conv1.bias)

------'''以下出力結果'''--------
Parameter containing:
tensor([conv1のバイアス1,conv1のバイアス2,...,conv1のバイアス16],requires_grad=True)

6-4. optimizerの使用

optimizerとは最適化手法のことで,学習をする際に使用するものである.
以下にその使用方法を示す.

filename.rb
import torch.optim as optim

optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9, weight_decay=0.005)

この詳しい説明は気になる場合下記URLより参照してほしい.

pyTorchでCNNsを徹底解説
pyTorch optim SGD徹底解説

要はこの最適化手法の定義の際に「net.parameters()」を渡していることから,この変数optimizerはパラメータ情報を見ることができるのである.
以下に閲覧方法を示す.

filename.rb
print(optimizer.param_groups)

------'''以下出力結果'''--------
[{'params': [Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
             ...
        [conv1のチャネル16のパラメータ]],
        requires_grad=True), Parameter containing:
tensor([conv1のバイアス1,conv1のバイアス2,...,conv1のバイアス16],
        requires_grad=True), Parameter containing:
tensor([[conv2のチャネル1のパラメータ],
             ...
             ...
tensor([fc2のベクトル1のパラメータ],
       [fc2のベクトル2のパラメータ],
             ...
       [fc2のベクトル10のパラメータ],
        requires_grad=True), Parameter containing:
tensor([fc2のバイアス1,fc2のバイアス2,...,fc2のバイアス10],
        requires_grad=True)],
'lr': 0.0001,
'momentum': 0.9,
'dampening': 0,
'weight_decay': 0.005,
'nesterov': False}]

このように「optimizer.param_groups」とすることでoptimizerが持つ情報をほぼ全て確認できる.
余談だが,このoptimizer.param_groupsの形は一番外側はカッコ「[...]」で囲まれており,中の要素は「'xxx':要素」となっていることからlist型の中に辞書型が入っている形となっている.
このことから中へのアクセスは以下のようにすればよい.

filename.rb
print(optimizer.param_groups[0])

------'''以下出力結果'''--------
{'params': [Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
             ...
        [conv1のチャネル16のパラメータ]],
        requires_grad=True), Parameter containing:
tensor([conv1のバイアス1,conv1のバイアス2,...,conv1のバイアス16],
        requires_grad=True), Parameter containing:
tensor([[conv2のチャネル1のパラメータ],
             ...
             ...
tensor([fc2のベクトル1のパラメータ],
       [fc2のベクトル2のパラメータ],
             ...
       [fc2のベクトル10のパラメータ],
        requires_grad=True), Parameter containing:
tensor([fc2のバイアス1,fc2のバイアス2,...,fc2のバイアス10],
        requires_grad=True)],
'lr': 0.0001,
'momentum': 0.9,
'dampening': 0,
'weight_decay': 0.005,
'nesterov': False}

出力してみるとわかるようにほとんど前の出力と同じ結果になっている.
違いと言えば出力の先頭と最後にカッコ「[...]」があるかないかである.
実はoptimizer.param_groupsは辞書型の要素を1つしかもたないlist型だったわけだ.

さらに中へアクセスする.

filename.rb
print(optimizer.param_groups[0]['lr'])
print(optimizer.param_groups[0]['params'][0])

------'''以下出力結果'''--------
0.0001
Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
             ...
        [conv1のチャネル16のパラメータ]],
        grad_fn=<CopySlices>)

このように辞書型への普通の参照法で中を見ることができる.
ここで,conv1のパラメータ出力に「grad_fn=<CopySlices>」というのが初めて出てきていると思うが,これは勾配情報を計算するために必要なもので,あまり気にしなくてもよい.

6-5. おまけ,勾配情報の閲覧

各Networkの層であるconv1,conv1のバイアス,fc1,....等はパラメータを更新するために勾配情報を持つことができる.
今までの出力時に「requires_grad=True」とあるのがその証拠である.

勾配情報は以下のように閲覧できる.

filename.rb
print(net.state_dict()['conv1.weight'].grad)
print(net.conv1.weight.grad)
print(optimizer.param_groups[0]['params'][0].grad)

------'''以下出力結果'''--------
None
None
None

None」と出ているが,勾配情報を計算するためのbackward()をしていないとこれが出てくる.
つまり勾配情報がまだ計算されていないことを示しており,正しい出力結果なのである.

7. Networkのパラメータを最初に書き換え

Networkのパラメータを最初に書き換える方法はいくつかあり,以下にそれを示す.

  • 普通に代入をする
  • nn.Parameter()を使う
  • net.load_state_dict()を使う

以下にそれぞれの説明をする.

7-1. 普通に代入する

前に説明したパラメータの閲覧方法をそのまま使用し,そのまま代入する.
以下に例としてチャネルの書き換えを示す.

filename.rb
net.state_dict()['conv1.weight'][0] = torch.tensor([conv1の新しいパラメータ])
print(net.state_dict()['conv1.weight'])

------'''以下出力結果'''--------
tensor([conv1の新しいチャネル1のパラメータ],
       [conv1のチャネル2のパラメータ],
            ...
       [conv1のチャネル16のパラメータ])

もちろん以下も可能である.

filename.rb
net.conv1.weight[0] = torch.tensor([conv1の新しいチャネル1のパラメータ])
print(net.conv1.weight)

------'''以下出力結果'''--------
Parameter containing:
tensor([conv1の新しいチャネル1のパラメータ],
       [conv1のチャネル2のパラメータ],
            ...
       [conv1のチャネル16のパラメータ]
       grad_fn=<CopySlices>)

これはあくまでconv1のある1つのチャネル(conv1は16チャネルのパラメータを持ち,そのうちの1つという意味)の書き換えを行っている.
実はconv1などの層の全チャネル(16チャネル全て)を書き換える方法は少し特殊で,以下のようにやるとうまくできない.

filename.rb
net.state_dict()['conv1.weight'] = torch.ones(16,1,3,3)
print(net.state_dict()['conv1.weight'])

------'''以下出力結果'''--------
書き換わっていないパラメータが出力される

これは全てが1である同サイズのTensor型を代入している.
しかし,エラーは出ないもののパラメータは書き換わらない.
またもう一方の方ではエラーが出力される.

filename.rb
net.conv1.weight = torch.ones(16,1,3,3)
print(net.conv1.weight)

------'''以下出力結果'''--------
TypeError: cannot assign 'torch.FloatTensor' as parameter 'weight' (torch.nn.Parameter or None expected)

解決方法として,以下のようにすると書き換えることができる.

filename.rb
net.state_dict()['conv1.weight'][0:16] = torch.ones(16,1,3,3)
print(net.state_dict()['conv1.weight'])

------'''以下出力結果'''--------
tensor([同サイズで全て1の要素])

これは「[0:16]」とすることで無理やり全チャネルを参照しているのだ.
ただしもう一方の方ではこのやり方でもエラーが出ることに注意してほしい.

filename.rb
net.conv1.weight[0:16] = torch.zeros(16,1,3,3)
print(net.conv1.weight)

------'''以下出力結果'''--------
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.

とにかくこの方法ではデータの一括変換に難があるのだ.

7-2. nn.Parameter()を使う

これはパラメータの全チャネル一括変換する際に便利な方法である.
この「nn」というのは最初の方に説明した「torch.nn」というmoduleである.
実はこのnn.Parameter()は当たり前のように我々が使用しているnn.Linear()nn.Conv2d()の内部で使用されており,networkパラメータの生成の要である方法と言ってもよい.

以下にまず,使用方法を示す.

filename.rb
weight = nn.Parameter(Tensor型のdata)
print(weight)

------'''以下出力結果'''--------
Parameter containing:
tensor([data], requires_grad=True)

とにかく優れる点は自動で「requires_grad=True」となる点である.
また,この出力からわかる通り実はずっと疑問だった「Parameter containing:」という文の出力はnn.Parameter()で宣言されたdataを出力するときに出るものなのである.
このように,このdataは普通のTensor型とは異なり,以下のように別の型であることを理解してほしい.

filename.rb
print(type(weight))

------'''以下出力結果'''--------
<class 'torch.nn.parameter.Parameter'>

では,実際に全チャネルのパラメータを書き換える方法を以下に示す.

filename.rb
net.conv1.weight = nn.Parameter(torch.zeros(16,1,3,3))
print(net.conv1.weight)

------'''以下出力結果'''--------
Parameter containing:
tensor([同サイズで全て0の要素], requires_grad=True)

ただし以下ではエラーは出ないが書き換えはできない.

filename.rb
net.state_dict()['conv1.weight'] = nn.Parameter(torch.zeros(16,1,3,3))
print(net.state_dict()['conv1.weight'])

------'''以下出力結果'''--------
書き換わっていないパラメータが出力される

この理由はもちろんnet.conv1.weightとnet.state_dict()['conv1.weight']の型が以下のようにそれぞれ異なるからである.

filename.rb
print(type(net.conv1.weight))
print(type(net.state_dict()['conv1.weight']))

------'''以下出力結果'''--------
<class 'torch.nn.parameter.Parameter'>
<class 'torch.Tensor'>

7-3. net.load_state_dict()を使う

この方法はNetworkのパラメータを事前に外部に保存しておき,それをloadすることでパラメータを書き換えるという方法である.
利点は事前学習をしたパラメータを使いたい場合などである.

まず,以下にsaveの方法を示す.

7-3-1. torch.save()によるパラメータsave

以下にsave方法を示す.

filename.rb
torch.save(net.state_dict(), 'ファイル名.pth')

これにより,「net.state_dict()」によるnetのパラメータを「ファイル名.pth」というファイル名で保存する.
なお,この場合のnetは先ほどから使っているものとは別のもの(例えば事前に何か学習したパラメータとか)である.

7-3-2. net.load_state_dict()によるパラメータload

上で保存したファイルをまず以下のように参照してloadする.

filename.rb
param = torch.load('ファイルのpath')

これでparamという変数にloadしたパラメータを格納した.
このparamを以下のように使用する.

filename.rb
net.load_state_dict(param)

これでnetのパラメータを書き換える.
ちなみに部分的に書き換える場合は以下のようにすればよい.

filename.rb
param = torch.load('ファイルのpath')
param2 = net.state_dict()
param2['conv1.weight'] = param['conv1.weight']
net.load_state_dict(param2)

少々回りくどいが,事前に保存したパラメータをparamに取得し,さらに今のnetworkのパラメータをparam2に取り出す.
そのparam2のconv1だけをparamのものに書き換え,それを今のnetに適応する.
これで今のnetworkはconv1の16チャネルのみパラメータが書き換わったことになる.

8. Networkのパラメータを途中で書き換え

さらにここではNetworkのパラメータを途中で書き換える方法を説明する.
ここで紹介する方法は下記である

  • .dataを使って代入する
  • .dataと.add_(), .mul_()を使って演算する

まず,なぜ途中で書き換える操作が上のセクションで話した最初に書き換えるものと違うのかを説明する.

8-1. 途中で書き換える場合の注意点

networkは機械学習において通常パラメータを更新しながら学習していく.
従って最初の定義は以下のようにする.

filename.rb
net = Net()
optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9, weight_decay=0.005)

このように1行目でnetworkを定義し,2行目でnetworkのパラメータを渡して更新手法を定義している.
この瞬間に問題が発生するのだ.
セクション7の方法でnetworkのパラメータを書き換えた場合,optimizerに渡したパラメータは書き換わらないので学習はされなくなってしまうのだ.

以下にそれを見るプログラムを示す.

filename.rb
net = Net()
optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9, weight_decay=0.005)
print(net.conv1.weight)
print(optimizer.param_groups[0]['params'][0])

-----------'''以下出力結果'''-----------
Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
               ....
        [conv1のチャネル16のパラメータ]],requires_grad=True)
Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
             ...
        [conv1のチャネル16のパラメータ]],
        grad_fn=<CopySlices>)

このように同じ内容のものが出力される(こまかいところは違うがデータの要素は同じ).
従って,どちらのデータも同じメモリ空間に存在している.
それでは普通に代入をして書き換えてみる.

filename.rb
net.conv1.weight = nn.Parameter(torch.zeros(16,1,3,3))

print(net.conv1.weight)
print(optimizer.param_groups[0]['params'][0])

------'''以下出力結果'''--------
Parameter containing:
tensor([同サイズで全て0の要素], requires_grad=True)
Parameter containing:
tensor([全く変わっていないdata], grad_fn=<CopySlices>)

このようにoptimizerの方のdataは書き換わらないのだ.
このギャップがあるため,optimizerと併用して使用するときは別の手段で書き換えを置こうのだ.

8-2. .dataを使って代入する

やり方は非常にシンプルで,「.data」というものを使う.
これはnn.Parameter()で定義されたTensor型dataで使える機能で,nn.Parameter()内にあるtensor型データ要素のみを取得することができる.
実際に以下のように見ることができる.

filename.rb
print(net.conv1.weight)
print(net.conv1.weight.data)

-----------'''以下出力結果'''-----------
Parameter containing:
tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
               ....
        [conv1のチャネル16のパラメータ]],requires_grad=True)

tensor([[conv1のチャネル1のパラメータ],
        [conv1のチャネル2のパラメータ],
               ....
        [conv1のチャネル16のパラメータ]])

もちろん「optimizer.param_groups[0]['params'][0].data」としても同様の結果を得られる.
それでは実際に代入してみよう.

filename.rb
net.conv1.weight.data = torch.zeros(16,1,3,3)

print(net.conv1.weight)
print(optimizer.param_groups[0]['params'][0])

-----------'''以下出力結果'''-----------
Parameter containing:
tensor([同サイズで全て0の要素], requires_grad=True)
Parameter containing:
tensor([同サイズで全て0の要素], grad_fn=<CopySlices>)

これで両方書き換えることができた.
もちろん以下のように部分的に書き換えることもできる.

filename.rb
net.conv1.weight.data[0] = torch.ones(1,3,3)

print(net.conv1.weight)
print(optimizer.param_groups[0]['params'][0])

-----------'''以下出力結果'''-----------
Parameter containing:
tensor([同サイズで最初だけ10の要素], requires_grad=True)
Parameter containing:
tensor([同サイズで最初だけ10の要素], grad_fn=<CopySlices>)

もちろんこれらは「optimizer.param_groups[0]['params'][0].data」を使用しても同様の結果を得られる.

8-2. .dataと.add_(), .mul_()を使って演算する

今度は代入ではなく今あるデータに加減,乗除をすることを考える.
加減を「.add_()」,乗除を「.mul_()」で実現する.
これも難しいことはなく,以下のように書けばよい.

filename.rb
net.conv1.weight.data.add_(5.0)
net.conv1.weight.data.mul_(0.1)

print(net.conv1.weight)
print(optimizer.param_groups[0]['params'][0])

-----------'''以下出力結果'''-----------
Parameter containing:
tensor([5が足されて10で割られた要素], requires_grad=True)
Parameter containing:
tensor([5が足されて10で割られた要素], grad_fn=<CopySlices>)

これも代入の時と同様に部分的に定期王できるし「optimizer.param_groups[0]['params'][0].data」も使うことができる.

9. ひとこと

今回はnetworkのパラメータの閲覧と書き換えの方法を説明した.
さらに途中でパラメータを書き換える方法を追加した.
不透明な部分や回りくどい部分もあったが何かの参考程度になっていただけると幸いである.
読みづらい点も多かったと思うが読んでいただきありがとうございます.

240
185
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
240
185