如题,本人想在开源项目Hyperspectral-Classification Pytorch中加入自定义的网络。
然后产生了一系列的bug,差不多耗费了我一整天出头的时间。
这里把加入自定义网络的流程写出来。同时把调试bug的过程也记录下来。
这部分是在确定模型后,初始化模型相关的超参数,并将模型实例化(即由类创建对象)。
需要在get_model(name, **kwargs):
函数中加入下面的代码。
if name == 'nn':
……
……
elif name == 'mou':
……
# -------------------------自加网络----------------------------
elif name =='OwnNet':
patch_size = kwargs.setdefault('patch_size', 17)
center_pixel = True
model = OwnNet(n_bands, n_classes, n_planes=32, patch_size=patch_size)
lr = kwargs.setdefault('learning_rate', 0.01)
optimizer = optim.SGD(model.parameters(),
lr=lr, momentum=0.9, weight_decay=0.0005)
epoch = kwargs.setdefault('epoch', 200)
criterion = nn.CrossEntropyLoss(weight=kwargs['weights'])
# kwargs.setdefault('scheduler', optim.lr_scheduler.MultiStepLR(optimizer, milestones=[epoch // 2, (5 * epoch) // 6], gamma=0.1))
# -------------------------自加网络----------------------------
else:
raise KeyError("{} model is unknown.".format(name))
这里将自定义网络的名称定为OwnNet
。
需要注意,这些超参数和超参数的值都是以键值对的形式存储在dictionary kwargs
中。
patch_size = kwargs.setdefault('patch_size', 17)
将patch_size
的值定为17(当然也可以定为其他值),表示每次传入网络的高光谱数据立方的W×H
为17×17
。setdefault()
的作用就是在字典中加入键值对,如果已有则不变,没有在使用指定的默认值。
model = OwnNet(n_bands, n_classes, n_planes=32, patch_size=patch_size)
将class OwnNet
实例化,同时传入n_bands
、n_classes
、n_planes
、patch_size
这四个超参数。
lr = kwargs.setdefault('learning_rate', 0.01)
是设定学习率。
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=0.0005)
,设定优化函数为SGD。(并没有采用常用的Adam)
epoch = kwargs.setdefault('epoch', 200)
设定epoch
为200。
criterion = nn.CrossEntropyLoss(weight=kwargs['weights'])
,设定损失函数交叉熵CrossEntropyLoss
。
这个模块其实就是网络的class的定义。
# -------------------------自加网络---------------------------
class OwnNet(nn.Module):
@staticmethod
def weight_init(m):
if isinstance(m, nn.Linear) or isinstance(m, nn.Conv3d):
init.xavier_uniform_(m.weight.data)
init.constant_(m.bias.data, 0)
def __init__(self, input_channels, n_classes, n_planes=32, patch_size=17):
super(OwnNet, self).__init__()
self.input_channels = input_channels
self.n_planes = n_planes
self.patch_size = patch_size
self.conv1 = nn.Conv3d(1, 32, (32, 4, 4), padding=(1, 1, 1))
self.conv2 = nn.Conv3d(32, 2 * 32, (32, 5, 5), padding=(1, 1, 1))
self.conv3 = nn.Conv3d(2 * 32, 4 * 32, (4, 3, 3), padding=(1, 0, 0))
self.pool1 = nn.MaxPool3d((1, 2, 2), stride=(1, 2, 2))
self.pool2 = nn.MaxPool3d((1, 2, 2), stride=(1, 2, 2))
self.features_size = self._get_final_flattened_size()
self.fc = nn.Linear(self.features_size, n_classes)
self.apply(self.weight_init)
def _get_final_flattened_size(self):
with torch.no_grad():
x = torch.zeros((1, 1, 103,
self.patch_size, self.patch_size))
x = self.pool1(self.conv1(x))
x = self.pool2(self.conv2(x))
x = self.conv3(x)
_, t, c, w, h = x.size()
return t * c * w * h
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = F.relu(self.conv3(x))
print(x.size())
print(self.features_size)
x = x.view(-1, self.features_size)
x = self.fc(x)
return x
# -------------------------自加网络---------------------------
class的主要的框架参考Hyperspectral-Classification Pytorch中已有的网络类。
主要修改的如下:
def __init__(self, input_channels, n_classes, n_planes=32, patch_size=17):
主要就是卷积层和池化层的超参数,全连接层保持不变。
self.conv1 = nn.Conv3d(1, 32, (32, 4, 4), padding=(1, 1, 1))
self.conv2 = nn.Conv3d(32, 2 * 32, (32, 5, 5), padding=(1, 1, 1))
self.conv3 = nn.Conv3d(2 * 32, 4 * 32, (4, 3, 3), padding=(1, 0, 0))
self.pool1 = nn.MaxPool3d((1, 2, 2), stride=(1, 2, 2))
self.pool2 = nn.MaxPool3d((1, 2, 2), stride=(1, 2, 2))
def _get_final_flattened_size(self):
这一部分是计算全连接层的输入展成一维的长度。
整体的思路是在with torch.no_grad():
下,用与网络输入数据同维度的全零tensor,按顺序走一遍网络的卷积层和池化层(直到全连接层之前)。然后提取数据维度,将channels、depth、width、height相乘即为所求。
def _get_final_flattened_size(self):
with torch.no_grad():
x = torch.zeros((1, 1, 103,
self.patch_size, self.patch_size))
x = self.pool1(self.conv1(x))
x = self.pool2(self.conv2(x))
x = self.conv3(x)
_, t, c, w, h = x.size()
return t * c * w * h
def forward(self, x):
这部分是前向传播过程。
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = F.relu(self.conv3(x))
print(x.size())
print(self.features_size)
x = x.view(-1, self.features_size)
x = self.fc(x)
return x
这部分主要是设定合适的patch_size
,并根据论文提供的架构(论文题目见LiEtAi
的介绍)选择合适的卷积层和池化层参数。
[外链图片转存失败(img-pppAwpUK-1568986092193)(C:\Users\73416\AppData\Local\Temp\1568980471509.png)]
论文确实给出了网络的架构,但是有3个问题:
邻域neighborhood的选择:
对于 Pavia University 数据集,邻域neighborhood的选择为27×27。但是这么大的数据量在我的电脑中无法运行。
RuntimeError: CUDA out of memory. Tried to allocate 253.13 MiB (GPU 0; 2.00 GiB total capacity; 1.15 GiB already allocated; 191.68 MiB free; 1.10 MiB cached)
关于网络架构architecture的理解。
我之前对4×4×32×32
的理解是kernel_height × kernel_width × kernel_depth × kernel_number
,但是这样理解的话,到第三个卷积层,kernel_depth
为32,已经大于的数据的depth
。运行是会报错的。出于这个原因我不得不大幅度修改了第三卷积层的kernel_depth
。
后来我也可以理解为pooling为2×2的意思是,只在W,H两个维度上进行池化,而保持D维度不变。这样就得到了新的网络结构(上面代码中的网络结构)。
padding与否。
在每一个卷积层和池化层,论文并没有给出是否在D,H,W三个维度上是否使用padding。
代码替换的时候,最好以大块代码替换为好。
因为如果你以句为单位替换代码,可能会导致上下句之间的报错。在一个类中,以方法为单位替换代码,可能会导致不同方法之家你的报错。
比如我下面的第一个坑。
对于已经出现的bug,仔细理解报错信息(报错信息非常重要),并且推测报错的原因。
之后定为到报错的语句(值可以追溯到的最原始的语句),并且在合适的位置print相关数据的信息。
之后根据打印出的信息,确定可能出错的位置。
重复上述的操作,直到确定出错的代码。
RuntimeError: shape '[-1, 564480]' is invalid for input of size 768
这一部分报错的语句是x = x.view(-1, self.features_size)
,即将数据展平时发生错误。
究其原因应该是网络的输入数据维度不对,虽然全连接层之前的运算没有报错(got luck),但是到了将特征展平的时候发生了错误。
对应的解决方式就是强制设置超参数:n_planes=32, patch_size=17
至此,第一个报错解决。
RuntimeError: Calculated padded input size per channel: (5 x 18 x 18). Kernel size: (32 x 5 x 5). Kernel size can't be greater than actual input size at c:\a\w\1\s\tmp_conda_3.7_183942\conda\conda-bld\pytorch_1549564939537\work\aten\src\thnn\generic/VolumetricConvolutionMM.c:78
这一部分是在卷积层报错。报错语句是:self.features_size = self._get_final_flattened_size()
而且注意到(5 x 18 x 18)
,这里的Depth不应该为5,而应该比5大得多。
这种错误不会发生在卷积层和池化层运算中,只可能出现在输入数据维度的初始化上。
后面检查发现将波段数从103写成了32,导致了波段数异常,而报错。
至此第二个报错解决。
RuntimeError: shape '[-1, 564480]' is invalid for input of size 768
首先我将view的参数强制置为564480,想先看一下网络的信息。
尝试失败,报错信息和上面一样。
之后将view的强制置为768,想先看一下网络的信息。
尝试继续失败,但报错信息改变了:
RuntimeError: size mismatch, m1: [1 x 768], m2: [564480 x 10] at c:\a\w\1\s\tmp_conda_3.7_183942\conda\conda-bld\pytorch_1549564939537\work\aten\src\thc\generic/
THCTensorMathBlas.cu:266
这里看到依旧是维度的问题,报错语句是x = x.view(-1, self.features_size)
。
现在问题更明显了:_get_final_flattened_size()
函数计算出来的size
,和summary计算的size
不同,二者中至少有一个是错的。
现在将self.features_size恢复(不再用数字代替),并在报错的x = x.view(-1, self.features_size)前打印数据x的size
:
torch.Size([2, 128, 44, 1, 1])
而正常的维度应该是:
[-1, 128, 44, 1, 1]
可以看到batch
的值从100变到了2,这是不正常的。正常来说batch
的值不应该变化。
尝试去打印一下self.features_size的信息,看是不是128×44×1×1=5,632。单并不是,反而是564480这个数。
为什么?
回去看_get_final_flattened_size()
这个函数。
好家伙,忘了更新网络的结构,导致现在还是之前网络的结果(只有卷积层没有池化层),算出来的维度肯定不对。改过来。
然后成功!
Network :
torch.Size([100, 1, 103, 17, 17])
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv3d-1 [-1, 32, 74, 16, 16] 16,416
MaxPool3d-2 [-1, 32, 74, 8, 8] 0
Conv3d-3 [-1, 64, 45, 6, 6] 1,638,464
MaxPool3d-4 [-1, 64, 45, 3, 3] 0
Conv3d-5 [-1, 128, 44, 1, 1] 295,040
Linear-6 [-1, 10] 56,330
================================================================
Total params: 2,006,250
Trainable params: 2,006,250
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.11
Forward/backward pass size (MB): 6.81
Params size (MB): 7.65
Estimated Total Size (MB): 14.58
----------------------------------------------------------------
请按任意键继续. . .
当然这里有一个小疑问,这里的全连接层用不用再多加一层,现在是直接从5,632降到10。
现在由于patch_size
太大,导致在我的笔记本电脑上运行过于慢,导致现在网络的性能在暂时无法评判。