「Pytorch:ResNet」の版間の差分
(→19) |
タグ: 手動差し戻し |
||
| (同じ利用者による、間の4版が非表示) | |||
| 3行目: | 3行目: | ||
= 方針 = | = 方針 = | ||
いきなりResNetを作るのはややこしいので,まずはシンプルなCNNを作成し,それをResNetに拡張することにします. | |||
ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.) | ただし,論文で実装されているものとは細部が異なることに注意(演習をやりやすいように少しアレンジを加えた.) | ||
| 11行目: | 11行目: | ||
今回用いるのは以下の4つのみ | 今回用いるのは以下の4つのみ | ||
* torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias) | * torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias) | ||
* torch.nn.BatchNorm2d( | * torch.nn.BatchNorm2d(num_features) | ||
* torch.nn.Linear(in_features, out_features) | * torch.nn.Linear(in_features, out_features) | ||
* torch.nn.MaxPool2d(kernel_size, stride) | * torch.nn.MaxPool2d(kernel_size, stride) | ||
| 142行目: | 142行目: | ||
各畳み込み層の直後(かつReLUの手前)にバッチ正規化層を導入してみましょう.torch.nn.Batchnorm2d を使い,バッチ正規化層を導入してください. | 各畳み込み層の直後(かつReLUの手前)にバッチ正規化層を導入してみましょう.torch.nn.Batchnorm2d を使い,バッチ正規化層を導入してください. | ||
名前は,convX に対して bnX のようにつけましょう.(以下,convX 等の "X" は 1 および 2 で読み替えてください.) | 名前は,convX に対して bnX のようにつけましょう.(以下,convX 等の "X" は 1 および 2 で読み替えてください.) | ||
== 9 == | == 9 == | ||
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を無しにすると精度がどれだけ変わるか,など,自由に色々試してみてください.