前提
ここに質問の内容を詳しく書いてください。
ResNet50を用いて学習を行っています。データセットにはcifar10を用いています。
Resnet50のモデル部分のコードに関しては以下のサイトを参考にしました。
https://qiita.com/tchih11/items/377cbf9162e78a639958
発生している問題・エラーメッセージ
エラーは発生していないのですが、Resnet50にしては精度が上がらなくて困っています。調査したところ、cifar10を用いた場合、90%ほど精度がでているのですが70%ほどで止まってしまいます。
該当のソースコード
python
1import torch 2import torchvision 3import torch.nn as nn 4import torch.optim as optim 5import torch.nn.functional as F 6import torchvision.transforms as transforms 7import numpy as np 8from matplotlib import pyplot as plt 9 10 11train_dataset = torchvision.datasets.CIFAR10(root='./data/',train=True,transform=transforms.ToTensor(),download=True) 12test_dataset = torchvision.datasets.CIFAR10(root='./data/',train=False,transform=transforms.ToTensor(),download=True) 13train_loader = torch.utils.data.DataLoader(dataset=train_dataset,batch_size=256,shuffle=True,num_workers=2) 14test_loader = torch.utils.data.DataLoader(dataset=test_dataset,batch_size=256,shuffle=False,num_workers=2) 15 16 17class block(nn.Module): 18 def __init__(self, first_conv_in_channels, first_conv_out_channels, identity_conv=None, stride=1): 19 """ 20 残差ブロックを作成するクラス 21 Args: 22 first_conv_in_channels : 1番目のconv層(1x1)のinput channel数 23 first_conv_out_channels : 1番目のconv層(1x1)のoutput channel数 24 identity_conv : channel数調整用のconv層 25 stride : 3x3conv層におけるstide数。sizeを半分にしたいときは2に設定 26 """ 27 super(block, self).__init__() 28 29 # 1番目のconv層(1×1) 30 self.conv1 = nn.Conv2d( 31 first_conv_in_channels, first_conv_out_channels, kernel_size=1, stride=1, padding=0) 32 self.bn1 = nn.BatchNorm2d(first_conv_out_channels) 33 34 # 2番目のconv層(3×3) 35 # パターン3の時はsizeを変更できるようにstrideは可変 36 self.conv2 = nn.Conv2d( 37 first_conv_out_channels, first_conv_out_channels, kernel_size=3, stride=stride, padding=1) 38 self.bn2 = nn.BatchNorm2d(first_conv_out_channels) 39 40 # 3番目のconv層(1×1) 41 # output channelはinput channelの4倍になる 42 self.conv3 = nn.Conv2d( 43 first_conv_out_channels, first_conv_out_channels*4, kernel_size=1, stride=1, padding=0) 44 self.bn3 = nn.BatchNorm2d(first_conv_out_channels*4) 45 self.relu = nn.ReLU() 46 47 # identityのchannel数の調整が必要な場合はconv層(1×1)を用意、不要な場合はNone 48 self.identity_conv = identity_conv 49 50 def forward(self, x): 51 52 identity = x.clone() # 入力を保持する 53 54 x = self.conv1(x) # 1×1の畳み込み 55 x = self.bn1(x) 56 x = self.relu(x) 57 x = self.conv2(x) # 3×3の畳み込み(パターン3の時はstrideが2になるため、ここでsizeが半分になる) 58 x = self.bn2(x) 59 x = self.relu(x) 60 x = self.conv3(x) # 1×1の畳み込み 61 x = self.bn3(x) 62 63 # 必要な場合はconv層(1×1)を通してidentityのchannel数の調整してから足す 64 if self.identity_conv is not None: 65 identity = self.identity_conv(identity) 66 x += identity 67 68 x = self.relu(x) 69 70 return x 71 72class ResNet(nn.Module): 73 def __init__(self, block, num_classes=10): 74 super(ResNet, self).__init__() 75 76 # conv1はアーキテクチャ通りにベタ打ち 77 self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3) 78 self.bn1 = nn.BatchNorm2d(64) 79 self.relu = nn.ReLU() 80 self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) 81 82 # conv2_xはサイズの変更は不要のため、strideは1 83 self.conv2_x = self._make_layer(block, 3, res_block_in_channels=64, first_conv_out_channels=64, stride=1) 84 85 # conv3_x以降はサイズの変更をする必要があるため、strideは2 86 self.conv3_x = self._make_layer(block, 4, res_block_in_channels=256, first_conv_out_channels=128, stride=2) 87 self.conv4_x = self._make_layer(block, 6, res_block_in_channels=512, first_conv_out_channels=256, stride=2) 88 self.conv5_x = self._make_layer(block, 3, res_block_in_channels=1024, first_conv_out_channels=512, stride=2) 89 90 self.avgpool = nn.AdaptiveAvgPool2d((1,1)) 91 self.fc = nn.Linear(512*4, num_classes) 92 93 def forward(self,x): 94 95 x = self.conv1(x) # in:(3,224*224)、out:(64,112*112) 96 x = self.bn1(x) # in:(64,112*112)、out:(64,112*112) 97 x = self.relu(x) # in:(64,112*112)、out:(64,112*112) 98 x = self.maxpool(x) # in:(64,112*112)、out:(64,56*56) 99 100 x = self.conv2_x(x) # in:(64,56*56) 、out:(256,56*56) 101 x = self.conv3_x(x) # in:(256,56*56) 、out:(512,28*28) 102 x = self.conv4_x(x) # in:(512,28*28) 、out:(1024,14*14) 103 x = self.conv5_x(x) # in:(1024,14*14)、out:(2048,7*7) 104 x = self.avgpool(x) 105 x = x.reshape(x.shape[0], -1) 106 x = self.fc(x) 107 108 return x 109 110 def _make_layer(self, block, num_res_blocks, res_block_in_channels, first_conv_out_channels, stride): 111 layers = nn.ModuleList() 112 113 # 1つ目の残差ブロックではchannel調整、及びsize調整が発生する 114 # identifyを足す前に1×1のconv層を追加し、サイズ調整が必要な場合はstrideを2に設定 115 identity_conv = nn.Conv2d(res_block_in_channels, first_conv_out_channels*4, kernel_size=1,stride=stride) 116 layers.append(block(res_block_in_channels, first_conv_out_channels, identity_conv, stride)) 117 118 # 2つ目以降のinput_channel数は1つ目のoutput_channelの4倍 119 in_channels = first_conv_out_channels*4 120 121 # channel調整、size調整は発生しないため、identity_convはNone、strideは1 122 for i in range(num_res_blocks - 1): 123 layers.append(block(in_channels, first_conv_out_channels, identity_conv=None, stride=1)) 124 125 return nn.Sequential(*layers) 126 127device = 'cuda' if torch.cuda.is_available() else 'cpu' 128net = ResNet(block).to(device) 129 130 131#交差エントロピー誤差を使用 132criterion = nn.CrossEntropyLoss() 133#モーメンタム法を使用 134optimizer = optim.SGD(net.parameters(),lr=0.015,momentum=0.9,weight_decay=5e-4) 135 136 137 138# 繰り返し回数 139num_epochs = 50 140train_loss_list = [] 141train_acc_list = [] 142val_loss_list = [] 143val_acc_list = [] 144 145for epoch in range(num_epochs): 146 #値の初期化 147 train_loss = 0 148 train_acc = 0 149 val_loss = 0 150 val_acc = 0 151 # ここから訓練 152 net.train() 153 for i,(images,labels) in enumerate(train_loader): 154 images,labels = images.to(device),labels.to(device) 155 # 勾配を0にする 156 optimizer.zero_grad() 157 # 順伝播の計算 158 outputs = net(images) 159 # 誤差の計算 160 loss = criterion(outputs,labels) 161 # 訓練誤差 162 train_loss += loss.item() 163 # 予測結果と正解ラベルが同じ数(同じ場合に1となる) 164 train_acc += (outputs.max(1)[1]==labels).sum().item() 165 # 誤差の逆伝播 166 loss.backward() 167 # 重みの更新 168 optimizer.step() 169 # 訓練誤差の平均 170 avg_train_loss = train_loss / len(train_loader.dataset) 171 # 正解率(訓練)の平均 172 avg_train_acc = train_acc / len(train_loader.dataset) 173 174 # ここから評価(dropoutやバッチ正規化を定義した場合に必要) 175 net.eval() 176 # 学習モデルを固定 177 with torch.no_grad(): 178 for i,(images,labels) in enumerate(test_loader): 179 images,labels = images.to(device),labels.to(device) 180 # 順伝播の計算 181 outputs = net(images) 182 # 誤差の計算 183 loss = criterion(outputs,labels) 184 # 評価誤差 185 val_loss += loss.item() 186 # 正解率(評価) 187 val_acc += (outputs.max(1)[1]==labels).sum().item() 188 # 評価誤差の平均 189 avg_val_loss = val_loss / len(test_loader.dataset) 190 # 正解率(評価)の平均 191 avg_val_acc = val_acc / len(test_loader.dataset) 192 193 print(f'Epoch[{epoch+1}/{num_epochs}], loss: {avg_train_loss:5f}, acc: {avg_train_acc:.5f}, val_loss: {avg_val_loss:5f}, val_acc: {avg_val_acc:.5f}') 194 195 # プロット用リストへの格納 196 train_loss_list.append(avg_train_loss) 197 train_acc_list.append(avg_train_acc) 198 val_loss_list.append(avg_val_loss) 199 val_acc_list.append(avg_val_acc) 200 201plt.figure() 202plt.plot(range(num_epochs),train_loss_list,label='train_loss', color = "blue") 203plt.plot(range(num_epochs),val_loss_list,label='val_loss', color = "red") 204plt.legend() 205plt.xlabel('epoch') 206plt.ylabel('loss') 207plt.title('Training and Validation loss') 208plt.grid() 209plt.savefig( 'test_3.png' ) 210 211plt.figure() 212plt.plot(range(num_epochs),train_acc_list,label='train_acc', color = "blue") 213plt.plot(range(num_epochs),val_acc_list,label='val_acc', color = "red") 214plt.legend() 215plt.xlabel('epoch') 216plt.ylabel('acc') 217plt.title('Training and Validation accuracy') 218plt.grid() 219plt.savefig( 'test_4.png' ) 220
試したこと
構造に問題があるのかと思って元論文と比較してみたのですがモデルに問題はなかったです。
また、学習率を変えてみたりもしたのですが、あまり変わりませんでした。
補足情報(FW/ツールのバージョンなど)
ここにより詳細な情報を記載してください。
論文では「We start with a learning rate of 0.1, divide it by 10 at 32k and 48k iterations, and terminate training at 64k iterations」と言っていることから学習率0.1スタートの32k iterationで0.01,48k iterationで0.001にしてますが,そちらのコードではlr=0.015のままですね,最終的な学習率と比較して15倍もの大きさの学習率を利用してしまっているようです.
ネットワークの学習が停滞した場合に有効なのは学習率を低下させることですが,それを実現できていないようです.
また, data augmentationも論文ではやったと言っていますので,これも実現すると良いでしょう.
さらに,「adopt the weight initialization in [13]」とあることから,重み初期値もHeの初期値を利用したみたいなので,これも反映すると学習速度の上昇につながると思います.
lr=0.001にしてみました。data augmentationとはどのような処理でしょうか。
lr=0.001にしてみたところ精度も落ち、lossも増えてしまいました。
Data Augmentationに関して論文では
We follow the simple data augmentation in [24] for training: 4 pixels are padded on each side, and a 32×32 crop is randomly sampled from the padded image or its horizontal flip.
と言っているので,4pixelを上下左右paddingして32x32の領域をランダムに黒にしてしまったり,左右反転したりすることで,ネットワークに与えるデータを増やす操作をやっているのです.
https://pytorch.org/vision/stable/transforms.html
torchではPadとRandomCropとRandomHorizontalFlipが使えますね.
> lr=0.001にしてみたところ精度も落ち、lossも増えてしまいました。
論文通り,学習率を「徐々に」減少させましたか?
初っ端から0.001にすると学習速度が遅いのも相まって50epoch目の精度が低くなるのは当然のことと思います.
CIFAR10は50kの教師データと10kの検証データであるから,バッチサイズ256である現状から計算するに,32k iterationはおよそ32k / (50k / 256) = 16epochと同義ですね,同様に48k iterationが25 epoch目になるので,これを境界にして学習率を1/10に減少させるようにしてください.
論文のFigure 6を見て分かる通り,32k iterationで急激に学習が進むのがわかると思います.学習率0.1で学習が停滞していた状態で学習率を0.01に減少させることで,学習が進むようになったと捉えることができます.また48 iterationのときに学習率0.001を減少させても効果が少ないのがわかると思います.この学習率0.001を序盤からやっても全然学習進まないのは,まぁそれはそうとしか言えないです.
https://take-tech-engineer.com/pytorch-lr-scheduler/
Learning Rate Schedulerの利用を推奨します.
optimizer = torch.optim.SGD(net.parameters(),lr=0.1,momentum=0.9,weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[16,25], gamma=0.1)
のように変更しました。ですが最初のlrを0.1に設定すると、lossの値がでなくなり、val_accもずっと100%に
なってしまいます。
https://github.com/kuangliu/pytorch-cifar
の「main.py」の下記を変更して、google colabで実行してみました
net = SimpleDLA()
↓ 変更
net = ResNet50()
200エポック終わった時のテストデータでの精度は95%くらいでした
上記githubのコードを分析してみたら、いかがでしょうか?
今回はモデルを書くことを頑張ってみたので、事前学習済のモデルを使わないでやりたいです。
前処理などNormalizeの部分はこちらのコードを参考にさせていただきます。
ですがやはり学習率の部分が0.1だとおかしくなってしまいます。
Epoch[1/50], loss: nan, acc: 0.09996, val_loss: nan, val_acc: 0.10000
このようにlossがでなく精度もおかしくなってしまいます。
> 事前学習済のモデルを使わないでやりたい
が、私が紹介したgithubのコードのことでしたら、私が実行した時には事前学習の重みファイルをダウンロードしてるっぽい動作には気付きませんでしたので、事前学習無しで学習をスタートしたのだと思ってましたが、コードのどこかに重みを読み込んでるところがありました?
ちなみに、初回エポックのテストデータでの精度は20%くらいでした
わかりやすくepochで示しましたが,1 iterationは1回のoptimize.update()が相当しますので,そのまま32kと48kを指定したスケジューラにしてください.
最初のlrが0.1で不適であれば,0.05スタートか0.01とかでも良いと思います.
また,初期値はちゃんとHeの初期値を利用しているのでしょうか.
jbpb0さん。このgithubのコードにはconvの記述などがなかったので勝手に事前学習済のモデルかと勘違いしてました。
ps_aux_grepさん。
Heの初期位置とはなんでしょうか。
>わかりやすくepochで示しましたが,1 iterationは1回のoptimize.update()が相当しますので,そのまま32kと48kを指定したスケジューラにしてください.
というのは
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[32,48], gamma=0.1)
ということでしょうか。
> Heの初期値とは
一番最初にコメントで申し上げた論文中の「adopt the weight initialization in [13]」の文言のことです.論文中のReferencesの13番目にあるHe氏の論文を見るとわかるはずです.内容は,重み初期値の分散を,2割ることのユニット数にすると学習がうまくいき精度向上に繋がる.というものです.TorchではデフォルトでLeCunの初期値が使われているので一致しません.
> スケジューラに関して
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[32000,48000], gamma=0.1)
にしてください.kは1000倍を意味します.
> このgithubのコードにはconvの記述などがなかった
ネットワークの定義は
https://github.com/kuangliu/pytorch-cifar/blob/master/models/resnet.py
にあり、
https://github.com/kuangliu/pytorch-cifar/blob/master/main.py
の「from models import *」で読み込まれてます
複数のファイルに分かれてはいますが、基本的な構成はこの質問のコードと似てるように見えたので、差異がどこにあるのか分析したら、精度が上がらない原因が分かるかも、と思って紹介しました
ps_aux_grepさん
重み初期値の分散を,2割ることのユニット数にすると学習がうまくいき精度向上に繋がる.というものです.TorchではデフォルトでLeCunの初期値が使われているので一致しません.
ってどの部分でしょうか。
jbpb0さん。ありがとうございます。比較してみます。
初期値の設定は、私が紹介したコードでは、
https://github.com/kuangliu/pytorch-cifar/blob/master/utils.py
の29行目からの「def init_params(net):」でやってるようです
33行目の「init.kaiming_normal(...」は、
https://pystyle.info/pytorch-parameters-initialization/#outline__12
の「12. torch.nn.init.kaimingnormal – He の方法 (正規分布)」を見てください
ありがとうございます。
32k iterationはおよそ32k / (50k / 256) = 16epochと同義ですね,同様に48k iterationが25 epoch目
ここの計算が合わないのですが、詳しく教えていただきたいです。
> 32k iterationはおよそ32k / (50k / 256) = 16epochと同義ですね,
約160エポックだと思う
やはりそうですよね。ありがとうございます。
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[A,B], gamma=0.1)
AとBの部分にはepoch数を入れると思ってたのですが、違いますか?
> AとBの部分にはepoch数
https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.MultiStepLR.html
の「Example」のコメントにはエポックと書かれてます
https://github.com/kuangliu/pytorch-cifar/blob/master/utils.py
をインポートして初期値の処理をやってみたのですが、
45行目のところで
ValueError: not enough values to unpack (expected 2, got 0)
というエラーが出てしまいました。引数が足りないのでしょうが、どこの部分かわかりません。
https://github.com/kuangliu/pytorch-cifar/blob/master/utils.py
の45行目以降は初期値設定とは関係無いものなので、削除したらいいのではないですかね
https://qiita.com/siruku6/items/5435f4e52c9cfa6cdda4
も参考になると思います
>https://github.com/kuangliu/pytorch-cifar/blob/master/utils.py の45行目以降は初期値設定とは関係無いものなので、削除したらいいのではないですかね
utils.pyからはprogress_barをimportしているのにdef progress_barの部分を削除していいんですか?
私が上げたコードにどのように初期値設定を組み込めばいいのかわからないです、、、
utils.pyのdef_init_paramsの部分をclass ResNetの部分に入れてみました。とりあえず結果待ちます。
> utils.pyからはprogress_barをimportしている
質問者さんのコードに「progress_bar」が要るのですか?
https://github.com/kuangliu/pytorch-cifar/blob/master/utils.py
から、初期値設定の部分だけを質問者さんのコードに取り込もうとしてると思って、
> 45行目以降は初期値設定とは関係無いものなので、削除したらいい
と書きましたが、もしかして、
https://github.com/kuangliu/pytorch-cifar/blob/master/main.py
をそのまま動かそうとしたら、
> ValueError: not enough values to unpack (expected 2, got 0)
というエラーが出るのでしょうか?
> utils.pyのdef_init_paramsの部分をclass ResNetの部分に入れてみました。
torchvisionのコードも参考になると思います
https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py
の208行目から
いやmain.pyは使ってないです。自分がこの質問でのせたコードをちょこちょこ変えて使っています。
utils.pyのdef_init_paramsの部分をclass ResNetの部分に入れてみたら、精度が50%になってしまいました、、
> utils.pyのdef_init_paramsの部分をclass ResNetの部分に入れてみたら、精度が50%になってしまいました
https://github.com/kuangliu/pytorch-cifar
をざっと調べたのですが、「utils.py」の「def init_params(net):」は使われてないっぽいです
(見落としがあったらごめんなさい)
そこで、今回の質問の精度がイマイチ出ない件の主原因は初期値ではないかも、と思ってgoogle colabで確認してみました
まず、下記を実行
!git clone https://github.com/kuangliu/pytorch-cifar
%cd pytorch-cifar
次に、質問のコードで下記を変更してから実行
(最後の行はインデント有り)
net = ResNet(block).to(device)
↓ 変更
import torch.backends.cudnn as cudnn
from models import *
net = ResNet50().to(device)
if device == 'cuda':
cudnn.benchmark = True
「utils.py」はインポートしてないので、初期値はデフォルトだと思います
質問のコードからネットワークを変えただけですが、50エポックで「val_acc」は84%になり、
> 70%ほどで止まってしまいます。
より精度が上がりました
(1回しか実行してないので、たまたまかもしれないけど)
torchvisionのコード
https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py
では初期値設定をしてるので、上記で使ったネットワークに初期値設定を追加したら精度がさらに上がるかもしれませんが、試してません
ありがとうございます。
今回のこの質問の自分のコードを使って、90%ほど出すのはむずかしいでしょうか。
https://github.com/kuangliu/pytorch-cifar/blob/master/main.py
にさらに近づけるため、私の一つ前のコメントに書いた変更を行った状態から、さらに下記を変更して実行してみました
train_dataset = ...
test_dataset = ...
train_loader = ...
test_loader = ...
↓ 変更
https://github.com/kuangliu/pytorch-cifar/blob/master/main.py
の30〜50行目のコード (ただし、変数名は質問のコードに合わせる)
optimizer = optim.SGD(net.parameters(),lr=0.015,momentum=0.9,weight_decay=5e-4)
↓ 変更
optimizer = optim.SGD(net.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200)
num_epochs = 50
↓ 変更
num_epochs = 200
「for epoch in range(num_epochs):」のループの最後に、インデントを合わせて下記を追加
scheduler.step()
200エポックで「val_acc」は95%になり、
https://github.com/kuangliu/pytorch-cifar/blob/master/main.py
を「net = ResNet50()」で実行した時とほぼ同じ結果になりました
> 今回のこの質問の自分のコードを使って、90%ほど出すのはむずかしいでしょうか。
私の一つ前のコメントで実行した(200エポックで「val_acc」95%)コードから、下記のみ戻して(他は変更した状態のまま)実行してみました
from models import *
net = ResNet50().to(device)
↓ 変更を戻す
net = ResNet(block).to(device)
そうすると、エポックが進んでも「val_acc」はずっと10%(0.10000)のまま変わらず、学習ができませんでした
私の二つ前のコメントで実行した(200エポックで「val_acc」95%)コードから、下記を変更して実行してみました
from models import *
net = ResNet50().to(device)
↓ 変更
net = torchvision.models.resnet50(num_classes=10, weights=None).to(device)
200エポックで「val_acc」は90%になり、
from models import *
net = ResNet50().to(device)
の場合の95%よりもやや劣る結果になりました
> 今回のこの質問の自分のコードを使って、90%ほど出すのはむずかしいでしょうか。
ネットワークを変えただけで、他は同じで、結果は下記の通りでした
・質問のコード:学習できず
・torchvision.models.resnet50:200エポックで90%
・https://github.com/kuangliu/pytorch-cifar/blob/master/models/resnet.py :200エポックで95%
ネットワーク構造の差異を調べて、何の違いが学習や精度に効いてるのかを分析したらいかがでしょうか