CIFAR-10、PyTorch 和 AlexNet

先在自己的电脑上走一遍,保证语法和逻辑不会错。再放到远程配置高的电脑上跑。

这些经典的神经网络结构,要熟悉到能白板编程的技能。

最近看到有关机器学习的一个推特,觉得很有启发。

What a lot of machine learning courses & books teach:

  • Load clean data
  • Train an MNIST-* classifier with 99% accuracy

What they should actually be teaching:

  • Process, clean, load your own data
  • Fail many experiments and research/find/understand ways to improve your ML model

很多机器学习课程与书籍所教授的内容:

  • 加载干净的数据
  • 训练一个MNIST-* 分类器,准确率达 99%。

他们其实应该教什么。

  • 处理、清理、加载自己的数据
  • 多次实验失败,研究/发现/了解改进你的 ML 模型的方法。

环境准备

第一步:启动虚拟环境

➜ source activate ldl-env

第二步:安装 PyTorch

(ldl-env) ➜  workspace conda install pytorch torchvision torchaudio -c pytorch

要多看 PyTorch 的官方文档。

数据集

torchvision.datasets

torchvision.datasets 中包含了较多的数据集

  • MNIST(0-9 手写数字)

  • COCO(用于图像标注和目标检测)

  • LSUN(包含数百万个场景和对象的彩色图像)

  • CIFAR-10

CIFAR-10 dataset (Canadian Institute For Advanced Research) :https://www.cs.toronto.edu/~kriz/cifar.html

CIFAR-10 和 CIFAR-100 是一个包含 8000 万张微小图像的数据集,由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 收集。

CIFAR-10 数据集包含 10 个类别,总共 60000 张 $$32\times32$$ 彩色图像。

每个类别 6000 张图像。

有 50000 张训练图像(train set)和 10000 张测试图像(test set)。

数据集分为 5 个训练 Batch 和 1 个测试 Batch。每个Train Batch 有 10000 张图像,Test Batch 正好包含 1000 张从每个类中随机选取的图像。 Train Batch 按随机顺序包含剩余的图像,但一些 Train Batch 可能包含来自一个类的图像比另一个类更多。在它们之间,Train batch 正好包含来自每个类的 5000 张图像。

  • Fashion-MNIST

GitHub: https://github.com/zalandoresearch/fashion-mnist

每个训练和测试样本都按照以下类别进行了标注:

标注编号 描述
0 T-shirt/top(T恤)
1 Trouser(裤子)
2 Pullover(套衫)
3 Dress(裙子)
4 Coat(外套)
5 Sandal(凉鞋)
6 Shirt(汗衫)
7 Sneaker(运动鞋)
8 Bag(包)
9 Ankle boot(踝靴)

找到了一个可供参考的小项目,数据集是 CIFAR-10。

AlexNet

GitHub:pytorch-cifar10

输入图片的大小为 $$32\times32\times3$$

class AlexNet(nn.Module):
    def __init__(self, num_classes=NUM_CLASSES):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(64, 192, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
        )
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 2 * 2, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), 256 * 2 * 2)
        x = self.classifier(x)
        return x

我按照吴恩达老师的视频教程,绘制了一个图,如下。

Papers with Code

这里的图片输入应该是 $$227\times227\times3$$,所以网络的内部 kernel_sizestride 会有不同。

class AlexNet(nn.Module):
    """
    Neural network model consisting of layers propsed by AlexNet paper.
    """
    def __init__(self, num_classes=1000):
        """
        Define and allocate layers for this neural net.
        Args:
            num_classes (int): number of classes to predict with this model
        """
        super().__init__()
        # input size should be : (b x 3 x 227 x 227)
        # The image in the original paper states that width and height are 224 pixels, but
        # the dimensions after first convolution layer do not lead to 55 x 55.
        self.net = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4),  # (b x 96 x 55 x 55)
            nn.ReLU(),
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),  # section 3.3
            nn.MaxPool2d(kernel_size=3, stride=2),  # (b x 96 x 27 x 27)
            nn.Conv2d(96, 256, 5, padding=2),  # (b x 256 x 27 x 27)
            nn.ReLU(),
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),
            nn.MaxPool2d(kernel_size=3, stride=2),  # (b x 256 x 13 x 13)
            nn.Conv2d(256, 384, 3, padding=1),  # (b x 384 x 13 x 13)
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, padding=1),  # (b x 384 x 13 x 13)
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, padding=1),  # (b x 256 x 13 x 13)
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),  # (b x 256 x 6 x 6)
        )
        # classifier is just a name for linear layers
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5, inplace=True),
            nn.Linear(in_features=(256 * 6 * 6), out_features=4096),
            nn.ReLU(),
            nn.Dropout(p=0.5, inplace=True),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(),
            nn.Linear(in_features=4096, out_features=num_classes),
        )
        self.init_bias()  # initialize bias

    def init_bias(self):
        for layer in self.net:
            if isinstance(layer, nn.Conv2d):
                nn.init.normal_(layer.weight, mean=0, std=0.01)
                nn.init.constant_(layer.bias, 0)
        # original paper = 1 for Conv2d layers 2nd, 4th, and 5th conv layers
        nn.init.constant_(self.net[4].bias, 1)
        nn.init.constant_(self.net[10].bias, 1)
        nn.init.constant_(self.net[12].bias, 1)

    def forward(self, x):
        """
        Pass the input through the net.
        Args:
            x (Tensor): input tensor
        Returns:
            output (Tensor): output tensor
        """
        x = self.net(x)
        x = x.view(-1, 256 * 6 * 6)  # reduce the dimensions for linear layer input
        return self.classifier(x)

Zhihu 知乎

https://zhuanlan.zhihu.com/p/29786939

给出了局部响应归一化的实现。

import torch.nn as nn
from torch.nn import functional as F
from torch.autograd import Variable

class LRN(nn.Module):
    def __init__(self, local_size=1, alpha=1.0, beta=0.75, ACROSS_CHANNELS=False):
        super(LRN, self).__init__()
        self.ACROSS_CHANNELS = ACROSS_CHANNELS
        if self.ACROSS_CHANNELS:
            self.average=nn.AvgPool3d(kernel_size=(local_size, 1, 1), #0.2.0_4会报错,需要在最新的分支上AvgPool3d才有padding参数
                    stride=1,
                    padding=(int((local_size-1.0)/2), 0, 0)) 
        else:
            self.average=nn.AvgPool2d(kernel_size=local_size,
                    stride=1,
                    padding=int((local_size-1.0)/2))
        self.alpha = alpha
        self.beta = beta
    
    
    def forward(self, x):
        if self.ACROSS_CHANNELS:
            div = x.pow(2).unsqueeze(1)
            div = self.average(div).squeeze(1)
            div = div.mul(self.alpha).add(1.0).pow(self.beta)#这里的1.0即为bias
        else:
            div = x.pow(2)
            div = self.average(div)
            div = div.mul(self.alpha).add(1.0).pow(self.beta)
        x = x.div(div)
        return x

这个专门分了 layer1layer2layer3 ,更加的好理解了。

from torch import nn
from torch.nn import functional as F
from torch.autograd import Variable

import torch
class AlexNet(nn.Module):
    def __init__(self, num_classes = 1000):#imagenet数量
        super().__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            LRN(local_size=5, alpha=1e-4, beta=0.75, ACROSS_CHANNELS=True)
        )
        
        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, groups=2, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            LRN(local_size=5, alpha=1e-4, beta=0.75, ACROSS_CHANNELS=True)
        )
        
        self.layer3 = nn.Sequential(
            nn.Conv2d(in_channels=256, out_channels=384, padding=1, kernel_size=3),
            nn.ReLU(inplace=True)
        )
        self.layer4 = nn.Sequential(
            nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True)
        )
        
        self.layer5 = nn.Sequential(
            nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)
        )
        
         #需要针对上一层改变view
        self.layer6 = nn.Sequential(
            nn.Linear(in_features=6*6*256, out_features=4096),
            nn.ReLU(inplace=True),
            nn.Dropout()
        )
        self.layer7 = nn.Sequential(
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(inplace=True),
            nn.Dropout()
        )
        
        self.layer8 = nn.Linear(in_features=4096, out_features=num_classes)
        
    def forward(self, x):
        x = self.layer5(self.layer4(self.layer3(self.layer2(self.layer1(x)))))
        x = x.view(-1, 6*6*256)
        x = self.layer8(self.layer7(self.layer6(x)))
        
        return x

思考

我有几个疑问?

  • AlexNet 论文里面的数据集使用的是 $$256\times256\times3$$,采用数据增强后真正输入到网络的图像 size 为 $$224\times224\times3$$,但是编程实现的时候,输入的大多是 $$227\times227\times3$$ 这个 227 是怎么算出来的?

    • 2021.02.05:224 应该是作者的一个失误,其实没关系的。将 224 代入网络,就会发现 224 存在的问题,227 是一个更好的取值。

  • 我感觉有人用 224,有人用 227,有什么区别吗?

    • 2021.02.05:用 227,不要用 224。代入网络推一遍就知道为什么了!

  • 上文我记录的第一个程序 pytorch-cifar10,每张图像的输入是 $$32\times32\times3$$。这种情况下一种处理方式是 resize 成 224;另一种是更改网络的 kernel_size、padding、stride,修改过之后的网络还能叫做是 AlexNet 吗?

    • 2021.02.05:我认为修改后的网络就不能叫做 AlexNet 了。

      AlexNet 中的卷积核大小、步长、填充都是作者为 ImageNet 数据集精心设计的。