今日在写程序时,遇到了一个蜜汁 bug
,加载别人训练好的 ResNet18
,识别精度很低,只有 16%,但理论上而言应该有 92%,我也好奇那 80% 的准确率去哪里了。而程序和数据本身又无错误,所以来探究一下这是为什么。
首先,网络结构和预训练的模型来自这里 1 ,这里声明一下,他提供的网络、参数都是没任何问题的,准确率低是我自己的原因。
错误程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import torchimport numpy as npfrom resnet18 import ResNet18from torch.utils.data import Dataset, DataLoaderfrom torch.autograd import Variablefrom torchvision import transformsclass testdataset (Dataset ): def __init__ (self, data_path, label_path ): self.x_data = np.load(data_path) self.x_data = self.x_data / 255 self.x_data = self.x_data[50000 :] self.y_data = np.load(label_path) self.y_data = self.y_data[50000 :] def __getitem__ (self, index ): x_ = self.x_data[index] x_ = x_.transpose(2 , 1 , 0 ) y_ = self.y_data[index] return torch.from_numpy(x_), torch.from_numpy(y_) def __len__ (self ): return len (self.x_data) def _error (model, X, y ): out = model(X) prediction = torch.argmax(out, 1 ) prediction = prediction.unsqueeze(1 ) correct = (prediction == y).sum ().float () return correct def _eval (model, device, test_loader ): model.eval () model.to(device) natural_err_total = 0 for data, target in test_loader: data, target = data.to(device, dtype=torch.float ), target.to(device, dtype=torch.float ) X, y = Variable(data, requires_grad=True ), Variable(target) err_natural = _error(model, X, y) natural_err_total += err_natural print ('acc: ' , natural_err_total / 10000 ) if __name__ == "__main__" : resnet = ResNet18() resnet_path = "resnet18_ckpt.pth" checkpoint = torch.load(resnet_path, map_location='cpu' ) print ('loaded model...' ) net_state = {} for key in checkpoint['net' ]: net_state[key[7 :]] = checkpoint['net' ][key] resnet.load_state_dict(net_state) print ('set model...' ) data_path = "cifar10_data.npy" label_path = "cifar10_label.npy" test_data = testdataset(data_path=data_path, label_path=label_path) test_loader = DataLoader(test_data, batch_size=128 ) print ('load data...' ) print ('natural' , end=', ' ) _eval (model=resnet, device='cpu' , test_loader=test_loader)
在这样操作下,准确率只有 16.89%
,我也很奇怪是哪里错了。
正确程序 从师兄那里找到了一份正确的程序,准确率是 92.84%
。后文会进行说明,这份代码只是结果对,但逻辑不对。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import torchfrom resnet18 import ResNet18from collections import OrderedDictfrom torch.utils.data import Datasetimport numpy as npclass TensorDataset (Dataset ): """ """ def __init__ (self, dataPath, labelPath ): x = np.load(dataPath) x = x[50000 :] / 255. x = x.astype("float32" ) data = x.transpose(0 , 3 , 1 , 2 ) label = np.load(labelPath)[50000 :] label = np.reshape(label, (data.shape[0 ], )) data, label = torch.from_numpy(data), torch.from_numpy(label) self.data_tensor = data self.target_tensor = label def __getitem__ (self, index ): return self.data_tensor[index], self.target_tensor[index] def __len__ (self ): return self.data_tensor.size(0 ) net = ResNet18() resnet_path = "resnet18_ckpt.pth" d = torch.load(resnet_path, map_location=torch.device('cpu' ))['net' ] d = OrderedDict([(k[7 :], v) for (k, v) in d.items()]) net.load_state_dict(d) dataPath = "cifar10_data.npy" labelPath = "cifar10_label.npy" dataset = TensorDataset(dataPath, labelPath) dataloader = torch.utils.data.DataLoader(dataset, batch_size=64 , shuffle=True , num_workers=0 ) total = 0 correct = 0 for batch_idx, (inputs, targets) in enumerate (dataloader): outputs = net(inputs) _, predicted = outputs.max (1 ) total += targets.size(0 ) correct += predicted.eq(targets).sum ().item() acc = 100. * correct / total print (acc)
问题分析 两份代码的数据加载、准确率计算都是正确的。经过二分法逐行注释,终于找到了问题所在,来依次分析一下。
第一个问题,因为网络的输入是 channel first
,而 numpy
的数据中 channel
位于最后面,所以需要对数据进行转置处理。但是这里有坑 。假设,图片之前的维度是 height, width, channel
,大小是 32 X 32 X 3
。
假设此时转置的方法是 np.transpose(2, 0, 1)
,得到矩阵的维度是 channel, height, width
,大小是 3 X 32 X 32
,这样输入网络是没有问题的。
假设此时转置的方法是 np.transpose(2, 1, 0)
,得到矩阵的维度是 channel, width, height
,大小是 3 X 32 X 32
,这样输入网络不会报错,但准确率会很低,大概只有 50%
,究其原因是数据增强所导致的。
第二个问题,是 model.eval()
导致的,如果开启这个,准确率就会很低;如果不开启,准确率又正常,所以来深究一下这是为哈。踩坑无数。首先来回顾下 pytorch
的训练过程:
1 2 3 4 5 6 7 8 9 10 11 for x, y in dataloader: y_pre = model(x) loss = criterion(y_pre, y) optimizer.zero_grad() loss.backward() optimizer.step()
而 model.eval()
会将 batchnormal
和 dropout
固定住,用训练好的值。否则,一旦测试时数据的 batchsize
变小,就会导致图像失真,模型准确率降低。这可能 是问题所在。且在使用 pytorch
训练模型时,一定要注意 train
和 eval
模式的切换。
沿着这个思路,先在一些网站先找到一些可能 的解决方案 2 4 。这里说可能是因为:任何问题都需要上下文才能知道准确的解决方案,即使描述的是同一个问题,也可能有多种解决方案。里面大概的解决方案如下:
batchsize
很小导致的,但我这里 batchsize
已经是 128 了,显然并不是这个原因
许多地方用同一个 bn
层,但我打开训练代码 3 ,里面 bn
层没有共用,所以也不是这个原因
在训练阶段,将模型设置为 model.train()
之前,一定要更新参数,包括 zero_grad
,但我看了代码,不是这个问题
bn
层的参数 track_running_stats
导致的。那我们来看看这个参数是啥意思,打开原文档 5 ,发现描述的佷晦涩,我没看懂。所以在网上找了找 6 ,意思是,如果这个参数为 True
,bn
就会追踪历史数据,以滑动加权平均的方式来更新 $\mu$ 和 $\sigma$。而 track_running_stats
取值为 False
,就不会追踪历史数据,只会根据当前的 batch
计算均值和方差。
可能由于数据比较不稳且 batchsize
很小影响了精度,但我打开程序,也不是这个原因。何况 model.eval()
模式下,track_running_stats
取值为 True
,使用训练好的模型参数,且 bn
的参数也不会更新。
多说一些,bn
的均值和方差是在 forward
方法中更新的 7 ,而不是在 optimizer.step
中更新。所以处于训练模式,bn
的参数不需要反向传播仍然能更新。这里需要注意。
以上,也就是打开搜索引擎,能看到了解决方案了。我屏蔽了某DN,某园等,以及另外的一些抄袭社区等等,他们的解决方案我看不到。捕获回过头来分析下我看到的解决方案,也能意识到是数据处理部分的问题了。反过来想,训练模式下精度很高,说明 bn
层在动,而 bn
影响的是数据。众所周知,颠覆结果的不是模型,是数据,所以猜测是数据的问题。
打开源程序,我们发现训练阶段对数据做了标准化处理 8 ;而我们处理测试数据时,只是单纯的除以 255,保证数据在 0 到 1 之间,但这是不对的 。那么也大概找到了解决方案,加载测试数据的时候进行标准化,此时模型的精度终于正确了!!!原因居然是:数据没有标准化处理。而 train
模式下精度很高,可能是因为通过 bn
层调整了数据分布。所以正确代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class testdataset (Dataset ): def __init__ (self, data_path, label_path ): self.x_data = np.load(data_path) self.x_data = self.x_data / 255 self.x_data = self.x_data[50000 :] self.y_data = np.load(label_path) self.y_data = self.y_data[50000 :] mean = np.array([0.4914 , 0.4822 , 0.4465 ]).reshape(1 , 3 ) var = np.array([0.2023 , 0.1994 , 0.2010 ]).reshape(1 , 3 ) self.x_data = (self.x_data - mean) / var def __getitem__ (self, index ): x_ = self.x_data[index] x_ = x_.transpose(2 , 0 , 1 ) y_ = self.y_data[index] return torch.from_numpy(x_), torch.from_numpy(y_) def __len__ (self ): return len (self.x_data) if __name__ == "__main__" : ... net.eval () ... predict(net, testloader) ...
标准化 既然标准化如此重要,就来回顾下标准化的用途。
第一步,数据归一化,也就是映射到 0 到 1 之间,这是为了防止梯度爆炸以及特征尺度的缩放。
因为误差反向传播求偏导的时候,会作用到原始数据 $x$,如果 $x$ 的取值是 255,梯度瞬间爆炸。
一个特征的变化范围可能是[1000,10000],另一个特征的变化范围可能是[−0.1,0.2],在进行距离有关的计算时,单位的不同会导致计算结果的不同,尺度大的特征会起决定性作用。所以要归一化,消除特征间单位和尺度差异的影响。刚才代码归一化的方式很简单,就是除以 255,当然还有很多其它的方式以及优缺点 9 10 。
在数据归一化后,可以标准化也可以不标准化,上述代码所用的 Z-Score 标准化的意思就是将数据映射到均值为 0,方差为 1 的分布空间中。
\begin{equation} x = \frac{x-\mu}{\sigma} \end{equation}
此类标准化是通过特征的平均值和标准差,将特征缩放成一个标准的正态分布,缩放后均值为0,方差为1。特别适用于数据的最大值和最小值未知,或存在孤立点。
标准化是为了方便数据的下一步处理,而进行的数据缩放等变换,不同于归一化,并不是为了方便与其他数据一同处理或比较。归一化是为了消除纲量压缩到 [0, 1]
区间;标准化只是调整特征整体的分布,也就是平移到原点附近。且,归一化与最大,最小值有关;标准化与均值,标准差有关。
估算均值与方差需要总体的平均值与方差,但是这一值在真实的分析与挖掘中很难得到,大多数情况下是用样本的均值与标准差替代,所以一般要求数据符合正态分布。
因此,也很容易定位到问题所在。训练的时候归一化处理的数据,而预测的时候没有,数据在两种不同的分布空间,所以导致预测精度降低。本文记录了全部 debug 的过程,学到了好多东西(误
reference