这段时间在学机器学习,花了两天时间利用paddle框架手撕了几大图像分类模型:

  • LeNet
  • AlexNet
  • VGG
  • GoogLeNet
  • ResNet-50

有一说一,还是有很大收获的(尽管中间在训练调参的时候踩坑了,这个放到文末讲)。

这里使用到了眼疾识别数据集iChallenge-PM,是百度大脑和中山大学中山眼科中心联合举办的iChallenge比赛中,提供的关于病理性近视(Pathologic Myopia,PM)的医疗类数据集,包含1200个受试者的眼底视网膜图片,训练、验证和测试数据集各400张。

数据集下载地址:https://aistudio.baidu.com/aistudio/datasetdetail/19065

数据集的图片

iChallenge-PM中既有病理性近视患者的眼底图片,也有非病理性近视患者的图片,命名规则如下:

  • 病理性近视(PM):文件名以P开头
  • 非病理性近视(non-PM):
    • 高度近视(high myopia):文件名以H开头
    • 正常眼睛(normal):文件名以N开头

我们将病理性患者的图片作为正样本,标签为1; 非病理性患者的图片作为负样本,标签为0

图像分类的流程

LeNet

LeNet的网络结构如下(输入的shape形状是手写数字识别的,本任务中已经改成了224*224*3)

然后,我尝试在LeNet上进行训练:

class LeNet(paddle.nn.Layer):
    def __init__(self, num_classes=1):
        super(LeNet, self).__init__()

        # 创建卷积和池化层块,每个卷积层使用sigmiod激活函数,后面跟一个2x2的池化
        self.conv1 = Conv2D(in_channels=3, out_channels=6, kernel_size=5)
        self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
        self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5)
        self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
        self.conv3 = Conv2D(in_channels=16, out_channels=120, kernel_size=4)
        self.fc1 = Linear(in_features=300000, out_features=64)
        # 第二个全连接层输出神经元个数为分类标签的类别数
        self.fc2 = Linear(in_features=64, out_features=num_classes)

    def forward(self, x, label=None):
        x = self.conv1(x)
        x = F.sigmoid(x)
        x = self.max_pool1(x)
        x = self.conv2(x)
        x = F.sigmoid(x)
        x = self.max_pool2(x)
        x = self.conv3(x)
        x = F.sigmoid(x)
        x = paddle.reshape(x, [x.shape[0], -1])
        x = self.fc1(x)
        x = F.sigmoid(x)
        x = self.fc2(x)
        x = F.softmax(x)

        if label is not None:
            acc = paddle.metric.accuracy(input=x, label=label)
            return x, acc
        else:
            return x

然而,得到的结果却不令人满意:

在训练集的准确率
loss
在测试集的准确率

从上面的图表可以看到,随着训练迭代次数的增加,模型在训练集和测试集的准确率并没有明显的上升,甚至由于过拟合导致了准确率的降低。损失函数的大小下降的幅度也很小。因此,LeNet在较大的图片上的表现并不好。

AlexNet

AlexNet是Alex Krizhevsky等人提出的,并且在2012年ImageNet比赛中,以很大优势获得了冠军。

AlexNet与LeNet相比,具有更深的网络结构,包含5层卷积和三层全连接,同时使用了下面的这三种方法改进模型的训练:

  • 数据增广:深度学习中常用的一种处理方式,通过对训练随机加一些变化,比如平移、缩放、裁剪、旋转、翻转或者增减亮度等,产生一系列跟原始图片相似但又不完全相同的样本,从而扩大训练数据集。通过这种方式,可以随机改变训练样本,避免模型过度依赖于某些属性,能从一定程度上抑制过拟合。
  • 使用Dropout抑制过拟合。
  • 使用ReLU激活函数减少梯度消失现象。

在眼疾筛查的问题上,对应的网络实现代码:

class AlexNet(paddle.nn.Layer):
    def __init__(self, num_classes=1):
        super(AlexNet, self).__init__()
        # AlexNet与LeNet不同的是激活函数换成了Relu
        self.conv1 = Conv2D(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=5)
        self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
        self.conv2 = Conv2D(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2)
        self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
        self.conv3 = Conv2D(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1)
        self.conv4 = Conv2D(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1)
        self.conv5 = Conv2D(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1)
        self.max_pool5 = MaxPool2D(kernel_size=2, stride=2)

        self.fc1 = Linear(in_features=12544, out_features=4096)
        self.drop_ratio1 = 0.5
        self.drop1 = Dropout(self.drop_ratio1)
        self.fc2 = Linear(in_features=4096, out_features=4096)
        self.drop_ratio2 = 0.5
        self.drop2 = Dropout(self.drop_ratio2)
        self.fc3 = Linear(in_features=4096, out_features=num_classes)

    def forward(self, x, label=None):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.max_pool1(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = self.max_pool2(x)
        x = self.conv3(x)
        x = F.relu(x)
        x = self.conv4(x)
        x = F.relu(x)
        x = self.conv5(x)
        x = F.relu(x)
        x = self.max_pool5(x)
        # print(x.shape)
        x = paddle.reshape(x, [x.shape[0], -1])
        # print(x.shape)
        x = self.fc1(x)
        x = F.relu(x)
        # 在全连接层后使用dropout抑制过拟合
        x = self.drop1(x)
        x = self.fc2(x)
        x = F.relu(x)
        x = self.drop2(x)
        x = self.fc3(x)

        x = F.softmax(x)

        if label is not None:
            acc = paddle.metric.accuracy(input=x, label=label)
            return x, acc
        else:
            return x

训练10个epoch后,AlexNet的准确率达到了92%。

在训练集的准确率
损失loss
在验证集的准确率

VGG

VGG模型真的是超级整洁的,满足了强迫症患者的需求!(严重怀疑研究者也有强迫症)

VGG网络的设计严格使用3×3的卷积层和池化层来提取特征,并在网络的最后面使用三层全连接层,将最后一层全连接层的输出作为分类的预测。 在VGG中每层卷积将使用ReLU作为激活函数,在全连接层之后添加dropout来抑制过拟合。使用小的卷积核能够有效地减少参数的个数,使得训练和测试变得更加有效。由于使用了较小的卷积核,因此可以堆叠更多的卷积层。

它的代码真的是很整洁,满足了强迫症患者的需要:

在眼疾识别数据集上训练10个epoch后,在验证集上的准确率达到了93%

损失loss
在训练集的准确率
在验证集的准确率

GoogLeNet

这个GoogLeNet一看就知道是谷歌开发的,而且还蹭了一下LeNet的名字哈哈哈。它是2014年ImageNet的冠军,它和前面这些网络相比,特点就是不仅有“深度”,还具有“宽度”。

由于图像信息的空间尺寸上存在巨大差异,不同尺寸的图像信息适合使用不同大小的卷积核来提取。单一大小的卷积核无法满足要求。这个模型的宽度体现在使用了一种叫做Inception的模块的解决方案。

上面左边的图片就是Inception模块的设计思想了,它使用不同大小的卷积核对图像进行处理,然后拼接成一个输出。这样子就达到了捕捉不同尺度的信息的效果。

Inception模块采用的是多通路的设计,每个支路使用不同大小的卷积核,最终输出的特征图就是每个支路输出通道数的总和。

这里存在一个问题,就是输出的参数量会很大,尤其是多个模块串联的时候。

因此Inception模块采用了上面右图的设计方式,在每个3*3和5*5的卷积层前面加上一个1*1的卷积层来减少输出通道的数量。

下面是GoogLeNet的结构

GoogLeNet在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3 ×3最大池化层来减小输出高宽。

  • 第一模块使用一个64通道的7 × 7卷积层。
  • 第二模块使用2个卷积层:首先是64通道的1 × 1卷积层,然后是将通道增大3倍的3 × 3卷积层。
  • 第三模块串联2个完整的Inception块。
  • 第四模块串联了5个Inception块。
  • 第五模块串联了2 个Inception块。
  • 第五模块的后面紧跟输出层,使用全局平均池化层来将每个通道的高和宽变成1,最后接上一个输出个数为标签类别数的全连接层。

PS:图片里面的softmax1和softmax2是辅助分类器,训练的时候将三个分类器的损失函数进行加权求和。这样子的目的是缓解梯度消失现象。

然后,又是训练环节了,经过10个epoch的训练,我们可以看到,在验证集中的准确率达到了96%以上。

损失loss
在训练集的准确率
在验证集的准确率

ResNet

ResNet“残差神经网络”,它的结构有点恐怖…层数非常的多。

我们知道,当网络层数增加后,训练误差往往不降反升,效果不如之前的。

ResNet的设计思想如下:

上左图表示,增加网络的时候,将x映射成y=F(x),这是传统的神经网络的模式。

上右图对其作了改进,变成了y = F(x)+x,这时,学习的就是y-x。

F(x)=y-x也叫做残差项,如果x➡y的映射接近恒等映射,上面右图通过学习残差项也比学习完整映射形式更加的容易。

上面右边的结构就是残差块,通过将输入x跨层连接,能更快的向前传播数据,或者向后传播梯度。这个过程和“传声筒”是类似的,由于有短路连接的x,每一层都有机会与最终的loss进行“直接对话”,可以更好的解决梯度弥散的问题。

残差块的具体设计就如下图所示了,这种结构是“两头大,中间小”的结构,因此也叫做瓶颈结构。

  • 1*1的卷积核是为了调整中间层的通道的数量,在进入3*3的卷积层之间减少通道数量,然后经过该卷积层后,再恢复通道数量。这样子就可以减少网络的参数量。
ResNet-50

上图是ResNet-50, 一共有49层卷积和一层全连接,因此被称为ResNet-50

然后,我就把ResNet-50用paddle框架手撕了一遍,在眼疾分类数据集上进行训练,最终得到的在验证集中的准确率大约是94%。

loss损失

可以发现,尽管层数增加了,但是ResNet的Loss下降的还是比其他神经网络都要快一些,这得益于其残差模块的设计,能使得数据传播的更快。

在训练集的准确率
在验证集的准确率

学习到的一些知识点

  • 二分类模型使用SGD可以快速收敛
  • 当loss不收敛或者验证集的准确率一直很低的时候,应当尝试更换优化器或者降低学习率。(我就是在这里折腾了很久,发现模型一直准确率很低,loss也不收敛。尝试了很久,也不知道怎么办,后来问了大佬才知道,换个优化器或者Adam降低学习率就能解决这个问题)
  • 交叉熵损失函数自带了一个softmax,有些情况下,如果模型的输出已经带上softmax,就有可能导致loss不收敛

转载请注明来源:https://www.longjin666.top/?p=1075

欢迎关注我的公众号“灯珑”,让我们一起了解更多的事物~

你也可能喜欢

发表评论