fast.ai 深度学习笔记(三)(3)https://developer.aliyun.com/article/1482898
消融研究
这是一个尝试打开和关闭模型不同部分以查看哪些部分产生哪些影响的过程,原始批量归一化论文中没有进行任何有效的消融。因此,缺失的一点是刚刚提出的这个问题——批量归一化放在哪里。这个疏忽导致了很多问题,因为原始论文实际上没有将其放在最佳位置。自那时以来,其他人已经弄清楚了这一点,当 Jeremy 向人们展示代码时,实际上放在更好位置的人们会说他的批量归一化放错了位置。
- 尽量在每一层上都使用批量归一化。
- 不要停止对数据进行归一化,这样使用您的数据的人就会知道您是如何对数据进行归一化的。其他库可能无法正确处理预训练模型的批量归一化,因此当人们开始重新训练时可能会出现问题。
class ConvBnNet(nn.Module): def __init__(self, layers, c): super().__init__() self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2) self.layers = nn.ModuleList([ BnLayer(layers[i], layers[i + 1]) for i in range(len(layers) - 1) ]) self.out = nn.Linear(layers[-1], c) def forward(self, x): x = self.conv1(x) for l in self.layers: x = l(x) x = F.adaptive_max_pool2d(x, 1) x = x.view(x.size(0), -1) return F.log_softmax(self.out(x), dim=-1)
- 代码的其余部分类似——使用
BnLayer
而不是ConvLayer
- 在开始时添加了一个单个卷积层,试图接近现代方法。它具有更大的内核大小和步幅为 1。基本思想是我们希望第一层具有更丰富的输入。它使用 5x5 区域进行卷积,这使它可以尝试在该 5x5 区域中找到更有趣更丰富的特征,然后输出更大的输出(在这种情况下,是 10x5x5 个滤波器)。通常是 5x5 或 7x7,甚至是 11x11 卷积,输出相当多的滤波器(例如 32 个滤波器)。
- 由于
padding = kernel_size — 1 / 2
和stride=1
,输入大小与输出大小相同——只是有更多的滤波器。 - 这是尝试创建更丰富的起点的好方法。
深度批量归一化
让我们增加模型的深度。我们不能只添加更多的步幅为 2 的层,因为每次都会将图像的大小减半。相反,在每个步幅为 2 的层之后,我们插入一个步幅为 1 的层。
class ConvBnNet2(nn.Module): def __init__(self, layers, c): super().__init__() self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2) self.layers = nn.ModuleList([ BnLayer(layers[i], layers[i+1]) for i in range(len(layers) - 1) ]) self.layers2 = nn.ModuleList([ BnLayer(layers[i+1], layers[i + 1], 1) for i in range(len(layers) - 1) ]) self.out = nn.Linear(layers[-1], c) def forward(self, x): x = self.conv1(x) for l,l2 in zip(self.layers, self.layers2): x = l(x) x = l2(x) x = F.adaptive_max_pool2d(x, 1) x = x.view(x.size(0), -1) return F.log_softmax(self.out(x), dim=-1) learn = ConvLearner.from_model_data((ConvBnNet2([10, 20, 40, 80, 160], 10), data) %time learn.fit(1e-2, 2) ''' A Jupyter Widget [ 0\. 1.53499 1.43782 0.47588] [ 1\. 1.28867 1.22616 0.55537] CPU times: user 1min 22s, sys: 34.5 s, total: 1min 56s Wall time: 58.2 s ''' %time learn.fit(1e-2, 2, cycle_len=1) ''' A Jupyter Widget [ 0\. 1.10933 1.06439 0.61582] [ 1\. 1.04663 0.98608 0.64609] CPU times: user 1min 21s, sys: 32.9 s, total: 1min 54s Wall time: 57.6 s '''
准确率与之前相同。现在深度为 12 层,即使对于批量归一化来说也太深了。可以训练 12 层深的卷积网络,但开始变得困难。而且似乎并没有太多帮助。
ResNet
class ResnetLayer(BnLayer): def forward(self, x): return x + super().forward(x) class Resnet(nn.Module): def __init__(self, layers, c): super().__init__() self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2) self.layers = nn.ModuleList([ BnLayer(layers[i], layers[i+1]) for i in range(len(layers) - 1) ]) self.layers2 = nn.ModuleList([ ResnetLayer(layers[i+1], layers[i + 1], 1) for i in range(len(layers) - 1) ]) self.layers3 = nn.ModuleList([ ResnetLayer(layers[i+1], layers[i + 1], 1) for i in range(len(layers) - 1) ]) self.out = nn.Linear(layers[-1], c) def forward(self, x): x = self.conv1(x) for l,l2,l3 in zip(self.layers, self.layers2, self.layers3): x = l3(l2(l(x))) x = F.adaptive_max_pool2d(x, 1) x = x.view(x.size(0), -1) return F.log_softmax(self.out(x), dim=-1)
ResnetLayer
继承自BnLayer
并覆盖forward
。- 然后添加一堆层,使其深度增加 3 倍,仍然可以很好地训练,只是因为
x + super().forward(x)
。
learn = ConvLearner.from_model_data(Resnet([10, 20, 40, 80, 160], 10), data)wd=1e-5%time learn.fit(1e-2, 2, wds=wd) ''' A Jupyter Widget [ 0\. 1.58191 1.40258 0.49131] [ 1\. 1.33134 1.21739 0.55625] CPU times: user 1min 27s, sys: 34.3 s, total: 2min 1s Wall time: 1min 3s ''' %time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd) ''' A Jupyter Widget [ 0\. 1.11534 1.05117 0.62549] [ 1\. 1.06272 0.97874 0.65185] [ 2\. 0.92913 0.90472 0.68154] [ 3\. 0.97932 0.94404 0.67227] [ 4\. 0.88057 0.84372 0.70654] [ 5\. 0.77817 0.77815 0.73018] [ 6\. 0.73235 0.76302 0.73633] CPU times: user 5min 2s, sys: 1min 59s, total: 7min 1s Wall time: 3min 39s ''' %time learn.fit(1e-2, 8, cycle_len=4, wds=wd) ''' A Jupyter Widget [ 0\. 0.8307 0.83635 0.7126 ] [ 1\. 0.74295 0.73682 0.74189] [ 2\. 0.66492 0.69554 0.75996] [ 3\. 0.62392 0.67166 0.7625 ] [ 4\. 0.73479 0.80425 0.72861] [ 5\. 0.65423 0.68876 0.76318] [ 6\. 0.58608 0.64105 0.77783] [ 7\. 0.55738 0.62641 0.78721] [ 8\. 0.66163 0.74154 0.7501 ] [ 9\. 0.59444 0.64253 0.78106] [ 10\. 0.53 0.61772 0.79385] [ 11\. 0.49747 0.65968 0.77832] [ 12\. 0.59463 0.67915 0.77422] [ 13\. 0.55023 0.65815 0.78106] [ 14\. 0.48959 0.59035 0.80273] [ 15\. 0.4459 0.61823 0.79336] [ 16\. 0.55848 0.64115 0.78018] [ 17\. 0.50268 0.61795 0.79541] [ 18\. 0.45084 0.57577 0.80654] [ 19\. 0.40726 0.5708 0.80947] [ 20\. 0.51177 0.66771 0.78232] [ 21\. 0.46516 0.6116 0.79932] [ 22\. 0.40966 0.56865 0.81172] [ 23\. 0.3852 0.58161 0.80967] [ 24\. 0.48268 0.59944 0.79551] [ 25\. 0.43282 0.56429 0.81182] [ 26\. 0.37634 0.54724 0.81797] [ 27\. 0.34953 0.54169 0.82129] [ 28\. 0.46053 0.58128 0.80342] [ 29\. 0.4041 0.55185 0.82295] [ 30\. 0.3599 0.53953 0.82861] [ 31\. 0.32937 0.55605 0.82227] CPU times: user 22min 52s, sys: 8min 58s, total: 31min 51s Wall time: 16min 38s '''
ResNet 块
return x + super().forward(x)
y = x + f(x)
其中x是来自上一层的预测,y是来自当前层的预测。重新排列公式,我们得到:公式重新排列
f(x) = y − x
差异y − x是残差。残差是迄今为止我们计算的错误。这意味着尝试找到一组卷积权重,试图填补我们偏离的量。换句话说,我们有一个输入,我们有一个函数试图预测错误(即我们偏离的量)。然后我们将输入的错误预测量相加,然后再添加另一个错误预测量,然后重复这个过程,逐层放大到正确答案。这基于一种称为boosting的理论。
- 完整的 ResNet 在将其添加回原始输入之前进行了两次卷积(我们这里只做了一次)。
- 在每个块
x = l3(l2(l(x)))
中,其中一层不是ResnetLayer
而是一个带有stride=2
的标准卷积——这被称为“瓶颈层”。ResNet 不是卷积层,而是我们将在第 2 部分中介绍的不同形式的瓶颈块。
ResNet 2 [01:59:33]
在这里,我们增加了特征的大小并添加了 dropout。
class Resnet2(nn.Module): def __init__(self, layers, c, p=0.5): super().__init__() self.conv1 = BnLayer(3, 16, stride=1, kernel_size=7) self.layers = nn.ModuleList([ BnLayer(layers[i], layers[i+1]) for i in range(len(layers) - 1) ]) self.layers2 = nn.ModuleList([ ResnetLayer(layers[i+1], layers[i + 1], 1) for i in range(len(layers) - 1) ]) self.layers3 = nn.ModuleList([ ResnetLayer(layers[i+1], layers[i + 1], 1) for i in range(len(layers) - 1) ]) self.out = nn.Linear(layers[-1], c) self.drop = nn.Dropout(p) def forward(self, x): x = self.conv1(x) for l,l2,l3 in zip(self.layers, self.layers2, self.layers3): x = l3(l2(l(x))) x = F.adaptive_max_pool2d(x, 1) x = x.view(x.size(0), -1) x = self.drop(x) return F.log_softmax(self.out(x), dim=-1) learn = ConvLearner.from_model_data(Resnet2([**16, 32, 64, 128, 256**], 10, 0.2), data) wd=1e-6 %time learn.fit(1e-2, 2, wds=wd) %time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd) %time learn.fit(1e-2, 8, cycle_len=4, wds=wd) log_preds,y = learn.TTA() preds = np.mean(np.exp(log_preds),0) metrics.log_loss(y,preds), accuracy(preds,y) ''' (0.44507397166057938, 0.84909999999999997) '''
85%是 2012 年或 2013 年 CIFAR 10 的最新技术。如今,它已经达到了 97%,因此还有改进的空间,但所有都基于这些技术:
- 更好的数据增强方法
- 更好的正则化方法
- 对 ResNet 进行一些调整
问题[02:01:07]:我们可以将“训练残差”方法应用于非图像问题吗?是的!但是它已经被其他地方忽略了。在 NLP 中,“transformer 架构”最近出现,并被证明是翻译的最新技术,并且其中有一个简单的 ResNet 结构。这种一般方法称为“跳过连接”(即跳过一层的想法),在计算机视觉中经常出现,但似乎没有其他人多使用,尽管它与计算机视觉无关。好机会!
狗与猫 [02:02:03]
回到狗和猫。我们将创建 resnet34(如果您对尾随数字的含义感兴趣,请参阅这里——只是不同的参数)。
PATH = "data/dogscats/" sz = 224 arch = resnet34 # <-- Name of the function bs = 64 m = arch(pretrained=True) # Get a model w/ pre-trained weight loaded m ''' ResNet( (conv1): Conv2d (3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (maxpool): MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), dilation=(1, 1)) (**layer1**): Sequential( (0): BasicBlock( (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) ) (1): BasicBlock( (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) ) (2): BasicBlock( (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True) ) ) (layer2): Sequential( (0): BasicBlock( (conv1): Conv2d (64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) (downsample): Sequential( (0): Conv2d (64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) ) ) (1): BasicBlock( (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) ) (2): BasicBlock( (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) ) (3): BasicBlock( (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) (relu): ReLU(inplace) (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True) ) ) ... (avgpool): AvgPool2d(kernel_size=7, stride=7, padding=0, ceil_mode=False, count_include_pad=True) (fc): Linear(in_features=512, out_features=1000) ) '''
我们的 ResNet 模型具有 Relu → BatchNorm。TorchVision 使用 BatchNorm → Relu。有三个不同版本的 ResNet 在流传,最好的是 PreAct (arxiv.org/pdf/1603.05027.pdf
)。
- 目前,最后一层有数千个特征,因为 ImageNet 有 1000 个特征,所以我们需要摆脱它。
- 当您使用 fast.ai 的
ConvLearner
时,它会为您删除最后两层。fast.ai 用自适应平均池化和自适应最大池化替换AvgPool2d
,并将两者连接在一起。 - 对于这个练习,我们将做一个简单版本。
m = nn.Sequential(*children(m)[:-2], nn.Conv2d(512, 2, 3, padding=1), nn.AdaptiveAvgPool2d(1), Flatten(), nn.LogSoftmax() )
- 删除最后两层
- 添加一个只有 2 个输出的卷积。
- 进行平均池化然后进行 softmax
- 最后没有线性层。这是产生两个数字的不同方式——这使我们能够进行 CAM!
tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, max_zoom=1.1) data = ImageClassifierData.from_paths(PATH, tfms=tfms, bs=bs) learn = ConvLearner.from_model_data(m, data) learn.freeze_to(-4) learn.fit(0.01, 1) learn.fit(0.01, 1, cycle_len=1)
ConvLearner.from_model
是我们之前学到的——允许我们使用自定义模型创建 Learner 对象。- 然后冻结除了我们刚刚添加的层之外的所有层。
类激活图(CAM)[02:08:55]
我们选择一个特定的图像,并使用一种称为 CAM 的技术,询问模型哪些部分的图像被证明是重要的。
它是如何做到的?让我们逆向工作。它是通过生成这个矩阵来做到的:
大数字对应于猫。那么这个矩阵是什么?这个矩阵简单地等于特征矩阵feat
乘以py
向量的值:
f2=np.dot(np.rollaxis(feat,0,3), py) f2-=f2.min() f2/=f2.max() f2
py
向量是预测,表示“我对这是一只猫有 100%的信心”。feat
是最终卷积层(我们添加的Conv2d
层)输出的值(2×7×7)。如果我们将feat
乘以py
,我们会得到所有第一个通道的值,而第二个通道的值为零。因此,它将返回与猫对应的部分的最后一个卷积层的值。换句话说,如果我们将feat
乘以[0, 1]
,它将与狗对应。
sf = SaveFeatures(m[-4]) py = m(Variable(x.cuda())) sf.remove() py = np.exp(to_np(py)[0]); py ''' array([ 1., 0.], dtype=float32) ''' feat = np.maximum(0, sf.features[0]) feat.shape
换句话说,在模型中,卷积层之后唯一发生的事情是平均池化层。平均池化层将 7×7 的网格平均化,计算出每个部分有多少“像猫”。然后,我们将“猫样”矩阵调整大小为与原始猫图像相同的大小,并叠加在顶部,然后你就得到了热图。
您可以在家中使用这种技术的方法是:
- 当您有一幅大图像时,您可以在一个快速小的卷积网络上计算这个矩阵。
- 放大具有最高值的区域
- 仅在该部分重新运行
由于时间不够,我们很快跳过了这部分,但我们将在第 2 部分中学习更多关于这种方法的内容。
“Hook”是让我们要求模型返回矩阵的机制。register_forward_hook
要求 PyTorch 每次计算一个层时运行给定的函数 - 类似于每次计算一个层时发生的回调。在以下情况下,它保存了我们感兴趣的特定层的值:
class SaveFeatures(): features=None def __init__(self, m): self.hook = m.register_forward_hook(self.hook_fn) def hook_fn(self, module, input, output): self.features = to_np(output) def remove(self): self.hook.remove()
Jeremy 的问题[02:14:27]:“您对深度学习的探索”和“如何跟上从业者的重要研究”
“如果您打算参加第 2 部分,您应该掌握我们在第 1 部分学到的所有技术”。以下是您可以做的一些事情:
- 至少观看每个视频 3 次。
- 确保您可以重新创建笔记本而无需观看视频 - 可能使用不同的数据集来使其更有趣。
- 密切关注论坛上的最新论文和最新进展。
- 坚持不懈,继续努力!