「Pytorch:ResNet」の版間の差分
(ページの作成:「= ResNetの解説 = https://arxiv.org/pdf/1512.03385 = 方針 = いきなりResNetを作るのはややこしいので,まずはシンプルCNNを作成し,それをResNetに拡張することにします. ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.) Pytorchのモデルを実装する際は,まず小さなモジュールを別のモジュー…」) |
タグ: 手動差し戻し |
||
| (同じ利用者による、間の13版が非表示) | |||
| 1行目: | 1行目: | ||
= ResNetの解説 = | = ResNetの解説 = | ||
https://arxiv.org/pdf/1512.03385 | https://arxiv.org/pdf/1512.03385 | ||
= 方針 = | = 方針 = | ||
いきなりResNetを作るのはややこしいので,まずはシンプルなCNNを作成し,それをResNetに拡張することにします. | |||
ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.) | ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.) | ||
| 11行目: | 10行目: | ||
最小単位のモジュールは,torch.nn に含まれるクラスを用いて作成する(自作も可能であるが,まあまあ大変.) | 最小単位のモジュールは,torch.nn に含まれるクラスを用いて作成する(自作も可能であるが,まあまあ大変.) | ||
今回用いるのは以下の4つのみ | 今回用いるのは以下の4つのみ | ||
* torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias) | |||
* torch.nn.BatchNorm2d(num_features) | |||
* torch.nn.Linear(in_features, out_features) | |||
* torch.nn.MaxPool2d(kernel_size, stride) | |||
次に,複数のモジュールを組み合わせる方法について.以下では,2つの畳み込み層を内包するモジュールのクラスを作成している. | 次に,複数のモジュールを組み合わせる方法について.以下では,2つの畳み込み層を内包するモジュールのクラスを作成している. | ||
class mymodule(torch.nn.Module): | class mymodule(torch.nn.Module): | ||
def __init__(self): | |||
super(mymodule, self).__init__() | |||
self.conv1 = torch.nn.Conv2d(in_channels=16, out_channels=8, kernel_size=3, stride=1, padding=1) | |||
self.conv2 = torch.nn.Conv2d(in_channels=8, out_channels=4, kernel_size=3, stride=1, padding=1) | |||
def forward(self, x): | |||
x = self.conv1(x) | |||
x = self.conv2(x) | |||
return x | |||
このmymoduleクラスを用いてMYMODULEというオブジェクトを生成し,適当な入力 x を処理させる方法は以下の通り. | このmymoduleクラスを用いてMYMODULEというオブジェクトを生成し,適当な入力 x を処理させる方法は以下の通り. | ||
MYMODULE = mymodule() | MYMODULE = mymodule() | ||
x = torch.randn(1,16,10,10) | x = torch.randn(1,16,10,10) | ||
y = MYMODULE(x) | y = MYMODULE(x) | ||
print(x.shape, y.shape) | print(x.shape, y.shape) | ||
これだと,mymodule に含まれるサブモジュール(conv1, conv2)の入力チャネル数等のパラメータが固定されてしまう. | これだと,mymodule に含まれるサブモジュール(conv1, conv2)の入力チャネル数等のパラメータが固定されてしまう. | ||
もう少し柔軟性を持たせるために,入力チャネル数を引数で与えるようにしたい場合は,以下のようにする. | もう少し柔軟性を持たせるために,入力チャネル数を引数で与えるようにしたい場合は,以下のようにする. | ||
class mymodule(torch.nn.Module): | class mymodule(torch.nn.Module): | ||
def __init__(self, c): | |||
super(mymodule, self).__init__() | |||
self.conv1 = torch.nn.Conv2d(in_channels=c, out_channels=c // 2, kernel_size=3, stride=1, padding=1) | |||
self.conv2 = torch.nn.Conv2d(in_channels=c // 2, out_channels=c // 4, kernel_size=3, stride=1, padding=1) | |||
def forward(self, x): | |||
x = self.conv1(x) | |||
x = self.conv2(x) | |||
return x | |||
MYMODULE = mymodule(16) | MYMODULE = mymodule(16) | ||
x = torch.randn(1,16,10,10) | x = torch.randn(1,16,10,10) | ||
y = MYMODULE(x) | y = MYMODULE(x) | ||
print(x.shape, y.shape) | print(x.shape, y.shape) | ||
さらに,mymodule を別のクラスである mymodule2 に組み込むには,以下のようにする. | さらに,mymodule を別のクラスである mymodule2 に組み込むには,以下のようにする. | ||
class mymodule2(torch.nn.Module): | class mymodule2(torch.nn.Module): | ||
def __init__(self): | |||
super(mymodule2, self).__init__() | |||
self.module1 = mymodule(16) | |||
self.module2 = torch.nn.Conv2d(in_channels=4, out_channels=5, kernel_size=3, stride=1, padding=1) | |||
def forward(self, x): | |||
x = self.module1(x) | |||
x = self.module2(x) | |||
return x | |||
MYMODULE2 = mymodule2() | MYMODULE2 = mymodule2() | ||
z = MYMODULE2(x) | z = MYMODULE2(x) | ||
print(z.shape) | print(z.shape) | ||
モジュールの情報(の一部)は print() で確認できる. | モジュールの情報(の一部)は print() で確認できる. | ||
print(MUMODULE2) | print(MUMODULE2) | ||
= 手順 = | = 手順 = | ||
プログラムの雛形とデータセットをダウンロードしてください. | プログラムの雛形とデータセットをダウンロードしてください. | ||
$ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/exersise_resnet.py | $ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/exersise_resnet.py | ||
$ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/stl10_binary.tar -P ./data | $ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/stl10_binary.tar -P ./data | ||
$ cd data | $ cd data | ||
$ tar -xvf stl10_binary.tar | $ tar -xvf stl10_binary.tar | ||
$ cd .. | $ cd .. | ||
このプログラムでは,STL-10というデータセットを用いてモデルを学習させます.プログラム中のモデルの部分をこれから完成させます. | このプログラムでは,STL-10というデータセットを用いてモデルを学習させます.プログラム中のモデルの部分をこれから完成させます. | ||
STL-10 のクラス数は 10 なので,最終層のニューロン数は10となります. | STL-10 のクラス数は 10 なので,最終層のニューロン数は10となります. | ||
1 | == 1 == | ||
まずは,全結合層を1つだけ持つ NN を作成します."ResNet"というクラスを作成し,全結合層"fc"を持つモデルを作成しましょう. | |||
ただし,全結合層である torch.nn.Linear が扱えるのは(バッチサイズxニューロン数)2次元のデータである一方,入力データ x は(バッチサイズxチャネル数x縦x横)の4次元でとなっています. | ただし,全結合層である torch.nn.Linear が扱えるのは(バッチサイズxニューロン数)2次元のデータである一方,入力データ x は(バッチサイズxチャネル数x縦x横)の4次元でとなっています. | ||
そこで, x の各チャネルについて,画素値の平均をとり,(バッチサイズxチャネル数)の大きさのテンソルにしてから,それを全結合層に渡す,という処理を行うモデルを実装してください. | そこで, x の各チャネルについて,画素値の平均をとり,(バッチサイズxチャネル数)の大きさのテンソルにしてから,それを全結合層に渡す,という処理を行うモデルを実装してください. | ||
| 97行目: | 95行目: | ||
以下,一つ進めるたびにプログラムを実行して,結果を確認するようにしてください. | 以下,一つ進めるたびにプログラムを実行して,結果を確認するようにしてください. | ||
2 | == 2 == | ||
畳み込み層を1層("conv1"という名前にする)追加してみましょう.畳み込み層は以下のように設計してください. | |||
in_channels = 3 | * in_channels = 3 | ||
out_channels = 16 | * out_channels = 16 | ||
kernel_size = 3 | * kernel_size = 3 | ||
stride = 1 | * stride = 1 | ||
padding = 1 | * padding = 1 | ||
bias = False | * bias = False | ||
畳み込み層の後は,先ほどと同じようにチャネルごとの画素値の平均を取り,全結合層に接続します. | 畳み込み層の後は,先ほどと同じようにチャネルごとの画素値の平均を取り,全結合層に接続します. | ||
3 | == 3 == | ||
これでは線形変換しかできないので,畳み込み層の直後にReLUを入れましょう.ReLUはモジュールも用意されていますが,torch.relu()という関数を用いる方が楽です. | |||
def forward(self, x): | def forward(self, x): | ||
x = torch.relu(self.conv1(x)) | |||
... | |||
これ以降,全ての畳み込み層の直後に,活性化関数 ReLU による処理を行うようにしてください. | これ以降,全ての畳み込み層の直後に,活性化関数 ReLU による処理を行うようにしてください. | ||
4 | == 4 == | ||
畳み込み層をさらに2層(conv2, conv3)追加してみましょう(計3層). | |||
チャネル数は,3 -(conv1)-> 16 -(conv2)-> 32 -(conv3)-> 64 のようにしてください.それ以外のパラメータは②と同じものを用いてください. | チャネル数は,3 -(conv1)-> 16 -(conv2)-> 32 -(conv3)-> 64 のようにしてください.それ以外のパラメータは②と同じものを用いてください. | ||
5 | == 5 == | ||
ダウンサンプリング(空間解像度を落とす処理)を行えるようにします.最大値プーリング層を2つ(conv1とconv2の間に一つ,conv2とconv3の間に一つ)導入します. | |||
これにより,小さな構造的差異に惑わされにくい,頑健なモデルを作成できます. | これにより,小さな構造的差異に惑わされにくい,頑健なモデルを作成できます. | ||
パラメータは以下のようにしてください. | パラメータは以下のようにしてください. | ||
kernel_size = 2 | * kernel_size = 2 | ||
stride = 2 | * stride = 2 | ||
6 | == 6 == | ||
畳み込み層をさらに2つ追加して,5層にすることを考えます.(プーリング層を追加する必要はありません.) | |||
現在,畳み込み層が既に3つ(conv1, conv2, conv3)あります.これらのうち,conv2 と conv3 をそれぞれ,2つの畳み込み層で置き換えます. | 現在,畳み込み層が既に3つ(conv1, conv2, conv3)あります.これらのうち,conv2 と conv3 をそれぞれ,2つの畳み込み層で置き換えます. | ||
* conv2 を消して, conv2_1 と conv2_2 で置き換えます.チャネル数は,16 -(conv2_1)-> 16 -(conv2_2)-> 32 となるようにしてください. | |||
* conv3 を消して, conv3_1 と conv3_2 で置き換えます.チャネル数は,32 -(conv3_1)-> 32 -(conv3_2)-> 64 となるようにしてください. | |||
7 | == 7 == | ||
上記と同じ要領で,畳み込み層をさらに4層追加しましょう.(conv2_3, conv2_4, conv3_3, conv3_4 を追加) | |||
チャネル数は,conv2_1 の入力から conv2_4 の出力に至る過程で,チャネル数が 16 -> 16 -> 16 -> 16 -> 32 のように,最後の畳み込み層のところでチャネル数が2倍になるようにしてください. | チャネル数は,conv2_1 の入力から conv2_4 の出力に至る過程で,チャネル数が 16 -> 16 -> 16 -> 16 -> 32 のように,最後の畳み込み層のところでチャネル数が2倍になるようにしてください. | ||
おそらく,これぐらいの深さになると,勾配消失により学習の進みが遅くなります.もっと深くすると,全く進まなくなるはずです. | おそらく,これぐらいの深さになると,勾配消失により学習の進みが遅くなります.もっと深くすると,全く進まなくなるはずです. | ||
8 | == 8 == | ||
各畳み込み層の直後(かつReLUの手前)にバッチ正規化層を導入してみましょう.torch.nn.Batchnorm2d を使い,バッチ正規化層を導入してください. | |||
名前は,convX に対して bnX のようにつけましょう.(以下,convX 等の "X" は 1 および 2 で読み替えてください.) | 名前は,convX に対して bnX のようにつけましょう.(以下,convX 等の "X" は 1 および 2 で読み替えてください.) | ||
9 | == 9 == | ||
ここで,shortcut を導入して,ResNet化します.まずは,convX_1 の手前から,convX_2 の直後に shortcut を入れます. | |||
forward() 関数を編集してください. | forward() 関数を編集してください. | ||
10 | == 10 == | ||
さらに,convX_3 の手前と convX_4 の直後を shortcut で接続します. | |||
ただし,ここではチャネル数が変化しているので,前項と同じようにはできません.ここでは,畳み込み層を使ってチャネル数を変更します.(この部分では,"特徴マップの差分を学習する"というResNetの原則が崩れています.) | ただし,ここではチャネル数が変化しているので,前項と同じようにはできません.ここでは,畳み込み層を使ってチャネル数を変更します.(この部分では,"特徴マップの差分を学習する"というResNetの原則が崩れています.) | ||
ここで作る畳み込み層は "shortcut_convX" という名前にしましょう.入出力チャネル数以外のパラメータは以下のようにしてください. | ここで作る畳み込み層は "shortcut_convX" という名前にしましょう.入出力チャネル数以外のパラメータは以下のようにしてください. | ||
kernel_size = 1 | * kernel_size = 1 | ||
stride = 1 | * stride = 1 | ||
padding = 0 | * padding = 0 | ||
11 | == 11 == | ||
ここで,(convX_1, bnX_1, convX_2, bnX_2) と (convX_3, bn_X_3, convX_4, bnX_4, shortcut_convX) のように,shortcut をちょうど一つ含むモジュールのブロックが作れそうですね. | |||
shortcut_convX を持つ方は後回しにして,まずは (convX_1, bn_X_1, convX_2, bnX_2) を持つ方を1つの "block" というクラスにまとめてみましょう. | shortcut_convX を持つ方は後回しにして,まずは (convX_1, bn_X_1, convX_2, bnX_2) を持つ方を1つの "block" というクラスにまとめてみましょう. | ||
このとき,__init__() に引数を設定して,入力チャネル数(in_channels)を指定できるようにしてください. | このとき,__init__() に引数を設定して,入力チャネル数(in_channels)を指定できるようにしてください. | ||
この block クラスを用いて生成した"blockX_1"というモジュールで,convX_1, bn_X_1, convX_2, bnX_2 を置き換えましょう. | この block クラスを用いて生成した"blockX_1"というモジュールで,convX_1, bn_X_1, convX_2, bnX_2 を置き換えましょう. | ||
12 | == 12 == | ||
さらに,block を改良して,(convX_3, bnX_3, convX_4, bnX_4, shortcut_convX) にも対応させましょう. | |||
ここでは,チャネル数が変化するので,block クラスの __init__() の引数で出力チャネル数(out_channels)も指定できるようにしましょう. | ここでは,チャネル数が変化するので,block クラスの __init__() の引数で出力チャネル数(out_channels)も指定できるようにしましょう. | ||
ただし,単純に以下のようにするわけにはいきません. | ただし,単純に以下のようにするわけにはいきません. | ||
def __init__(self, in_channels, out_channels): | def __init__(self, in_channels, out_channels): | ||
... | |||
self.shortcut_convX = torch.nn.Conv2d(... | |||
def forward(self, x): | def forward(self, x): | ||
... | |||
t = torch.relu(self.bnX_3(self.convX_4(x))) | |||
x = torch.relu(self.bnX_4(self.shortcut_convX(x) + self.convX_4(t))) | |||
return x | |||
上記のようにしてしまうと,shortcut_convX を使いたくない場面でも,勝手に shortcut_convX が適用されてしまいます. | 上記のようにしてしまうと,shortcut_convX を使いたくない場面でも,勝手に shortcut_convX が適用されてしまいます. | ||
この場合,if を用いて,in_channels と out_channels が等しいかどうかで,生成するモジュールを切り替えられるようにしましょう. | この場合,if を用いて,in_channels と out_channels が等しいかどうかで,生成するモジュールを切り替えられるようにしましょう. | ||
def __init__(self, in_channels, out_channels): | def __init__(self, in_channels, out_channels): | ||
... | |||
if in_channels == out_channels: | |||
self.shortcut = ... | |||
else: | |||
self.shortcut = ... | |||
ここで,self.shortcut = torch.nn.Sequential() を使うと”何もしない”モジュール(入力をそのまま出力するモジュール)を作成できます. | ここで,self.shortcut = torch.nn.Sequential() を使うと”何もしない”モジュール(入力をそのまま出力するモジュール)を作成できます. | ||
| 183行目: | 191行目: | ||
ソースコードがずいぶんスッキリしてましたね! | ソースコードがずいぶんスッキリしてましたね! | ||
13 | == 13 == | ||
現在のアーキテクチャは概ね以下のような感じになっていると思います. | |||
class ResNet(torch.nn.Module): | class ResNet(torch.nn.Module): | ||
def __init__(self): | |||
super(ResNet, self).__init__() | |||
self.conv1 = torch.nn.Conv2d(3, 16, 3, 1, 1, bias=False) | |||
self.bn1 = torch.nn.BatchNorm2d(16) | |||
self.pool1 = torch.nn.MaxPool2d(2,2) | |||
self.block2_1 = block(16, 16) | |||
self.block2_2 = block(16, 32) | |||
self.pool2 = torch.nn.MaxPool2d(2,2) | |||
self.block3_1 = block(32, 32) | |||
self.block3_2 = block(32, 64) | |||
self.fc = torch.nn.Linear(64, 10) | |||
ここで,一気に20層の畳み込み層を追加してみましょう.block 一つにつき,畳み込み層が 2 つなので,block を10個追加すればOKです. | ここで,一気に20層の畳み込み層を追加してみましょう.block 一つにつき,畳み込み層が 2 つなので,block を10個追加すればOKです. | ||
| 205行目: | 211行目: | ||
ただし,いちいち以下のように書くのは面倒です. | ただし,いちいち以下のように書くのは面倒です. | ||
def __init__(self): | |||
... | |||
self.block2_1 = ... | |||
self.block2_2 = ... | |||
self.block2_3 = ... | |||
self.block2_4 = ... | |||
... | |||
ここでは,torch.nn.Sequential() を用います.以下のようにすると,mod1, mod2, mod3 を順に適用するモジュールを作れます. | ここでは,torch.nn.Sequential() を用います.以下のようにすると,mod1, mod2, mod3 を順に適用するモジュールを作れます. | ||
mod1 = module() | |||
... | |||
seq = torch.nn.Sequential(mod1, mod2, mod3) | |||
しかし,同じようなモジュールを,1行に一つずつ書いて定義していくのは面倒です.そこで,for文を活用します. | しかし,同じようなモジュールを,1行に一つずつ書いて定義していくのは面倒です.そこで,for文を活用します. | ||
以下の例では,mod クラスのオブジェクトを5つ作って連結しています. | 以下の例では,mod クラスのオブジェクトを5つ作って連結しています. | ||
blocks = [] | |||
for i in range(5): | |||
blocks.append(mod()) | |||
self.bigblock = torch.nn.Sequential(*blocks) | |||
また,(*blocks) という処理は見慣れない方もいるかもしれませんが,これは list の中身を取り出して順番に並べる処理です.よって,上記は以下のコードと同じです. | また,(*blocks) という処理は見慣れない方もいるかもしれませんが,これは list の中身を取り出して順番に並べる処理です.よって,上記は以下のコードと同じです. | ||
torch.nn.Sequential(mod(), mod(), mod(), mod(), mod()) | |||
17 | == 17 == | ||
bigblock2 も bigblock3 も,作る処理は同じなので,冗長です,また,層の数を変えたい場合にも,ちょっと不便です. | |||
これを,関数にまとめてスッキリさせましょう.ResNet クラスに新たにメソッド make_bigblock() を追加します. | これを,関数にまとめてスッキリさせましょう.ResNet クラスに新たにメソッド make_bigblock() を追加します. | ||
make_bigblock には,入力チャネル数,出力チャネル数,ブロック数を引数として受け取り,畳み込み層を連結したものを返します. | make_bigblock には,入力チャネル数,出力チャネル数,ブロック数を引数として受け取り,畳み込み層を連結したものを返します. | ||
これを __init__() の中から呼び出すことにより,bigblock を作成しましょう. | これを __init__() の中から呼び出すことにより,bigblock を作成しましょう. | ||
18 | == 18 == | ||
最後に,ResNet クラスの__init__() に引数を設定し,bigblock2 と bigblock3 におけるブロック数を設定できるようにしましょう. | |||
19 | == 19 == | ||
完成です! | |||
あとは,ブロック数(num_blocks)やエポック数などのパイパーパラメータに対して精度がどのように変わるか,shortcutを無しにすると精度がどれだけ変わるか,など,自由に色々試してみてください. | あとは,ブロック数(num_blocks)やエポック数などのパイパーパラメータに対して精度がどのように変わるか,shortcutを無しにすると精度がどれだけ変わるか,など,自由に色々試してみてください. | ||
= 解答例 = | |||
[https://vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/exersise_resnet_answer.py 解答例] | |||
2025年8月6日 (水) 06:02時点における最新版
ResNetの解説
https://arxiv.org/pdf/1512.03385
方針
いきなりResNetを作るのはややこしいので,まずはシンプルなCNNを作成し,それをResNetに拡張することにします. ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.)
Pytorchのモデルを実装する際は,まず小さなモジュールを別のモジュールに組み込むことにより,徐々に大きなモデルを組み立てていく,というやり方が基本になります.
最小単位のモジュールは,torch.nn に含まれるクラスを用いて作成する(自作も可能であるが,まあまあ大変.) 今回用いるのは以下の4つのみ
- torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias)
- torch.nn.BatchNorm2d(num_features)
- torch.nn.Linear(in_features, out_features)
- torch.nn.MaxPool2d(kernel_size, stride)
次に,複数のモジュールを組み合わせる方法について.以下では,2つの畳み込み層を内包するモジュールのクラスを作成している.
class mymodule(torch.nn.Module):
def __init__(self):
super(mymodule, self).__init__()
self.conv1 = torch.nn.Conv2d(in_channels=16, out_channels=8, kernel_size=3, stride=1, padding=1)
self.conv2 = torch.nn.Conv2d(in_channels=8, out_channels=4, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
return x
このmymoduleクラスを用いてMYMODULEというオブジェクトを生成し,適当な入力 x を処理させる方法は以下の通り.
MYMODULE = mymodule() x = torch.randn(1,16,10,10) y = MYMODULE(x) print(x.shape, y.shape)
これだと,mymodule に含まれるサブモジュール(conv1, conv2)の入力チャネル数等のパラメータが固定されてしまう. もう少し柔軟性を持たせるために,入力チャネル数を引数で与えるようにしたい場合は,以下のようにする.
class mymodule(torch.nn.Module):
def __init__(self, c):
super(mymodule, self).__init__()
self.conv1 = torch.nn.Conv2d(in_channels=c, out_channels=c // 2, kernel_size=3, stride=1, padding=1)
self.conv2 = torch.nn.Conv2d(in_channels=c // 2, out_channels=c // 4, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
return x
MYMODULE = mymodule(16)
x = torch.randn(1,16,10,10)
y = MYMODULE(x)
print(x.shape, y.shape)
さらに,mymodule を別のクラスである mymodule2 に組み込むには,以下のようにする.
class mymodule2(torch.nn.Module):
def __init__(self):
super(mymodule2, self).__init__()
self.module1 = mymodule(16)
self.module2 = torch.nn.Conv2d(in_channels=4, out_channels=5, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.module1(x)
x = self.module2(x)
return x
MYMODULE2 = mymodule2()
z = MYMODULE2(x)
print(z.shape)
モジュールの情報(の一部)は print() で確認できる.
print(MUMODULE2)
手順
プログラムの雛形とデータセットをダウンロードしてください.
$ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/exersise_resnet.py $ wget vrl.sys.wakayama-u.ac.jp/class/pytorch_tutorial/exersise_resnet/stl10_binary.tar -P ./data $ cd data $ tar -xvf stl10_binary.tar $ cd ..
このプログラムでは,STL-10というデータセットを用いてモデルを学習させます.プログラム中のモデルの部分をこれから完成させます. STL-10 のクラス数は 10 なので,最終層のニューロン数は10となります.
1
まずは,全結合層を1つだけ持つ NN を作成します."ResNet"というクラスを作成し,全結合層"fc"を持つモデルを作成しましょう. ただし,全結合層である torch.nn.Linear が扱えるのは(バッチサイズxニューロン数)2次元のデータである一方,入力データ x は(バッチサイズxチャネル数x縦x横)の4次元でとなっています. そこで, x の各チャネルについて,画素値の平均をとり,(バッチサイズxチャネル数)の大きさのテンソルにしてから,それを全結合層に渡す,という処理を行うモデルを実装してください. (全結合層 fc は,入力サイズが3で,出力サイズが10,ということになります.)
できたら,プログラムを実行してみましょう.ここでは,プログラムのエラーが出ないことを確認することが目的です.こんな小さなNNではまともに学習ができませんので,精度は低くてもOKです. 以下,一つ進めるたびにプログラムを実行して,結果を確認するようにしてください.
2
畳み込み層を1層("conv1"という名前にする)追加してみましょう.畳み込み層は以下のように設計してください.
- in_channels = 3
- out_channels = 16
- kernel_size = 3
- stride = 1
- padding = 1
- bias = False
畳み込み層の後は,先ほどと同じようにチャネルごとの画素値の平均を取り,全結合層に接続します.
3
これでは線形変換しかできないので,畳み込み層の直後にReLUを入れましょう.ReLUはモジュールも用意されていますが,torch.relu()という関数を用いる方が楽です.
def forward(self, x):
x = torch.relu(self.conv1(x))
...
これ以降,全ての畳み込み層の直後に,活性化関数 ReLU による処理を行うようにしてください.
4
畳み込み層をさらに2層(conv2, conv3)追加してみましょう(計3層). チャネル数は,3 -(conv1)-> 16 -(conv2)-> 32 -(conv3)-> 64 のようにしてください.それ以外のパラメータは②と同じものを用いてください.
5
ダウンサンプリング(空間解像度を落とす処理)を行えるようにします.最大値プーリング層を2つ(conv1とconv2の間に一つ,conv2とconv3の間に一つ)導入します. これにより,小さな構造的差異に惑わされにくい,頑健なモデルを作成できます. パラメータは以下のようにしてください.
- kernel_size = 2
- stride = 2
6
畳み込み層をさらに2つ追加して,5層にすることを考えます.(プーリング層を追加する必要はありません.) 現在,畳み込み層が既に3つ(conv1, conv2, conv3)あります.これらのうち,conv2 と conv3 をそれぞれ,2つの畳み込み層で置き換えます.
- conv2 を消して, conv2_1 と conv2_2 で置き換えます.チャネル数は,16 -(conv2_1)-> 16 -(conv2_2)-> 32 となるようにしてください.
- conv3 を消して, conv3_1 と conv3_2 で置き換えます.チャネル数は,32 -(conv3_1)-> 32 -(conv3_2)-> 64 となるようにしてください.
7
上記と同じ要領で,畳み込み層をさらに4層追加しましょう.(conv2_3, conv2_4, conv3_3, conv3_4 を追加) チャネル数は,conv2_1 の入力から conv2_4 の出力に至る過程で,チャネル数が 16 -> 16 -> 16 -> 16 -> 32 のように,最後の畳み込み層のところでチャネル数が2倍になるようにしてください. おそらく,これぐらいの深さになると,勾配消失により学習の進みが遅くなります.もっと深くすると,全く進まなくなるはずです.
8
各畳み込み層の直後(かつReLUの手前)にバッチ正規化層を導入してみましょう.torch.nn.Batchnorm2d を使い,バッチ正規化層を導入してください. 名前は,convX に対して bnX のようにつけましょう.(以下,convX 等の "X" は 1 および 2 で読み替えてください.)
9
ここで,shortcut を導入して,ResNet化します.まずは,convX_1 の手前から,convX_2 の直後に shortcut を入れます. forward() 関数を編集してください.
10
さらに,convX_3 の手前と convX_4 の直後を shortcut で接続します. ただし,ここではチャネル数が変化しているので,前項と同じようにはできません.ここでは,畳み込み層を使ってチャネル数を変更します.(この部分では,"特徴マップの差分を学習する"というResNetの原則が崩れています.) ここで作る畳み込み層は "shortcut_convX" という名前にしましょう.入出力チャネル数以外のパラメータは以下のようにしてください.
- kernel_size = 1
- stride = 1
- padding = 0
11
ここで,(convX_1, bnX_1, convX_2, bnX_2) と (convX_3, bn_X_3, convX_4, bnX_4, shortcut_convX) のように,shortcut をちょうど一つ含むモジュールのブロックが作れそうですね. shortcut_convX を持つ方は後回しにして,まずは (convX_1, bn_X_1, convX_2, bnX_2) を持つ方を1つの "block" というクラスにまとめてみましょう. このとき,__init__() に引数を設定して,入力チャネル数(in_channels)を指定できるようにしてください. この block クラスを用いて生成した"blockX_1"というモジュールで,convX_1, bn_X_1, convX_2, bnX_2 を置き換えましょう.
12
さらに,block を改良して,(convX_3, bnX_3, convX_4, bnX_4, shortcut_convX) にも対応させましょう. ここでは,チャネル数が変化するので,block クラスの __init__() の引数で出力チャネル数(out_channels)も指定できるようにしましょう. ただし,単純に以下のようにするわけにはいきません.
def __init__(self, in_channels, out_channels):
...
self.shortcut_convX = torch.nn.Conv2d(...
def forward(self, x):
...
t = torch.relu(self.bnX_3(self.convX_4(x)))
x = torch.relu(self.bnX_4(self.shortcut_convX(x) + self.convX_4(t)))
return x
上記のようにしてしまうと,shortcut_convX を使いたくない場面でも,勝手に shortcut_convX が適用されてしまいます. この場合,if を用いて,in_channels と out_channels が等しいかどうかで,生成するモジュールを切り替えられるようにしましょう.
def __init__(self, in_channels, out_channels):
...
if in_channels == out_channels:
self.shortcut = ...
else:
self.shortcut = ...
ここで,self.shortcut = torch.nn.Sequential() を使うと”何もしない”モジュール(入力をそのまま出力するモジュール)を作成できます.
ソースコードがずいぶんスッキリしてましたね!
13
現在のアーキテクチャは概ね以下のような感じになっていると思います.
class ResNet(torch.nn.Module):
def __init__(self):
super(ResNet, self).__init__()
self.conv1 = torch.nn.Conv2d(3, 16, 3, 1, 1, bias=False)
self.bn1 = torch.nn.BatchNorm2d(16)
self.pool1 = torch.nn.MaxPool2d(2,2)
self.block2_1 = block(16, 16)
self.block2_2 = block(16, 32)
self.pool2 = torch.nn.MaxPool2d(2,2)
self.block3_1 = block(32, 32)
self.block3_2 = block(32, 64)
self.fc = torch.nn.Linear(64, 10)
ここで,一気に20層の畳み込み層を追加してみましょう.block 一つにつき,畳み込み層が 2 つなので,block を10個追加すればOKです. ここでは,block2_X と block3_X をそれぞれ5つずつ追加することにします. ただし,いちいち以下のように書くのは面倒です.
def __init__(self):
...
self.block2_1 = ...
self.block2_2 = ...
self.block2_3 = ...
self.block2_4 = ...
...
ここでは,torch.nn.Sequential() を用います.以下のようにすると,mod1, mod2, mod3 を順に適用するモジュールを作れます.
mod1 = module()
...
seq = torch.nn.Sequential(mod1, mod2, mod3)
しかし,同じようなモジュールを,1行に一つずつ書いて定義していくのは面倒です.そこで,for文を活用します. 以下の例では,mod クラスのオブジェクトを5つ作って連結しています.
blocks = []
for i in range(5):
blocks.append(mod())
self.bigblock = torch.nn.Sequential(*blocks)
また,(*blocks) という処理は見慣れない方もいるかもしれませんが,これは list の中身を取り出して順番に並べる処理です.よって,上記は以下のコードと同じです.
torch.nn.Sequential(mod(), mod(), mod(), mod(), mod())
17
bigblock2 も bigblock3 も,作る処理は同じなので,冗長です,また,層の数を変えたい場合にも,ちょっと不便です. これを,関数にまとめてスッキリさせましょう.ResNet クラスに新たにメソッド make_bigblock() を追加します. make_bigblock には,入力チャネル数,出力チャネル数,ブロック数を引数として受け取り,畳み込み層を連結したものを返します. これを __init__() の中から呼び出すことにより,bigblock を作成しましょう.
18
最後に,ResNet クラスの__init__() に引数を設定し,bigblock2 と bigblock3 におけるブロック数を設定できるようにしましょう.
19
完成です! あとは,ブロック数(num_blocks)やエポック数などのパイパーパラメータに対して精度がどのように変わるか,shortcutを無しにすると精度がどれだけ変わるか,など,自由に色々試してみてください.