Bootstrap

深度学习分类任务——Alexnet、VggNet、ResNet经典模型讲解等

(慢慢更新中...)

前言:SOTA模型

通俗来讲,就是当今现在最牛叉模型

SOTA(State of the Art)模型指的是在某一特定领域或任务中,当前表现最优、代表着该领域最高水平和最新技术发展状况的算法或模型。例如在图像识别领域中准确率最高的卷积神经网络,自然语言处理领域里理解能力最强的语言模型如 GPT 系列等。

SOTA 模型通常具备以下特点:

  • 高性能指标:在公认的测试集上能取得最高的跑分等性能表现,如在 ImageNet 图像分类任务中准确率突破较高水平。
  • 创新性:会提出新方法,如改进模型架构或训练技巧,像 Transformer 架构就颠覆了传统的 RNN 架构。
  • 可复现性:其他团队能够通过公开的代码和数据集复现其结果。
  • 实际应用价值:不仅在理论测试中跑分高,还能在实际应用场景中解决真实问题,比如自动驾驶模型在复杂路况中表现稳定。

SOTA 模型不是固定不变的,随着技术的不断发展和创新,新的模型会不断涌现并取代旧的 SOTA 模型。

模型发展及对比:如图, AlexNet(2012)、VGG(2014)、GoogleNet(2014)、Residual Net(2015)等经典模型,展示它们的错误率,其中 Residual Net 错误率为 3.57% ,体现模型的性能提升。


一、经典神经网络 AlexNet

AlexNet 在 ImageNet LSVRC-2012的比赛中, 取得了top-5错误率为15.3%的成绩。 第二名 是30多的错误率。 AlexNet有6亿个参数和650,000个神经元,包含5个卷积层,有些层后面跟了max-pooling层, 3个全连接层 自AlexNet,引起了深度学习的狂潮。

A.创新点: relu、 drop out、 池化、 归一化。

①relu

reLU(Rectified Linear Unit)即修正线性单元,是深度学习中常用的激活函数,以下是关于它的详细介绍:

定义与公式

  • ReLU 的数学表达式为:,也就是说,当输入x大于等于 0 时,输出就是x本身;当输入x小于 0 时,输出为 0。例如,对于输入-2,ReLU 函数的输出为 0;对于输入3,输出为 3。

特点:

  • 计算简单高效:只需要进行比较和取最大值操作,计算量小,大大提高了神经网络的训练和推理速度。在大规模的神经网络中,这种计算效率的提升尤为显著,能够减少训练时间和计算资源的消耗。
  • 缓解梯度消失问题:在传统的 Sigmoid 或 Tanh 激活函数中,当输入值的绝对值较大时,梯度会趋近于 0,导致梯度消失,使得模型难以训练。而 ReLU 函数在(x>0)时,梯度恒为 1,不会出现梯度消失问题,能让模型更容易收敛,尤其是在深层神经网络中,有助于信息的有效传递和模型的训练。
  • 稀疏性:ReLU 函数会使一部分神经元的输出为 0,这就使得神经网络具有了稀疏性。稀疏性可以减少模型的参数数量,降低模型的复杂度,起到一定的正则化作用,有助于防止过拟合,提高模型的泛化能力。比如在图像识别任务中,经过 ReLU 激活后,很多表示不重要特征的神经元会被抑制为 0,只保留对图像识别有重要作用的特征。

缺点:

  • 神经元死亡问题:当输入小于 0 时,ReLU 的输出恒为 0,梯度也为 0,这意味着在训练过程中,如果某个神经元的输入一直小于 0,那么这个神经元就会 “死亡”,即永远不会被激活,无法再对模型的训练产生贡献。例如,如果在训练初期,某个神经元的权重初始化不当,导致其输入始终为负,那么该神经元就会一直处于不激活状态。
  • 输出不是以 0 为中心:ReLU 的输出始终大于等于 0,这可能会导致神经网络的输出分布出现偏态,使得后续的训练不够稳定。在一些需要以 0 为中心的数据分布的算法中,如梯度下降算法,可能会影响算法的收敛速度和效果。

应用场景:

  • 图像识别:在卷积神经网络(CNN)中广泛应用,能够有效地提取图像的特征。例如在经典的 AlexNet、VGGNet 等图像识别模型中,ReLU 作为激活函数,帮助模型快速收敛,提高了图像识别的准确率。
  • 自然语言处理:在循环神经网络(RNN)、长短时记忆网络(LSTM)等用于自然语言处理的模型中,ReLU 也常被用作激活函数,用于处理文本数据中的非线性关系,例如在文本分类、机器翻译等任务中,帮助模型更好地理解和处理自然语言。

②池化:

​​​​​​深度学习——分类任务基础原理 及 卷积 基础原理-CSDN博客这里有详述

优点

  • 降低计算复杂度:减少了特征图的尺寸,从而降低了后续卷积层或全连接层的计算量,加快了模型的训练和推理速度。
  • 防止过拟合:减少了模型的参数数量,有助于防止模型过拟合,提高模型的泛化能力。
  • 增强特征的稳定性:对输入特征图的小变化具有一定的不变性,使模型更加稳定。

③drop out

  • 原理:在训练神经网络时,按照一定概率随机 “丢弃”(将权重设置为 0)网络中的部分神经元,让它们在本次前向传播和反向传播中不工作。这样可以减少神经元之间的复杂共适应关系,避免模型过度依赖某些局部特征,提升模型的泛化能力。比如在一个有 100 个神经元的隐藏层中,若 Dropout 概率设为 0.5,那么每次训练就随机让 50 个神经元不参与计算。
  • 优点:有效防止过拟合,减少模型的训练时间和计算量。
  • 缺点:测试阶段需要对输出结果进行调整,乘以 Dropout 概率的倒数,以保证测试时模型的输出期望与训练时一致。

④归一化

一句话:它可以让模型关注数据的分布,而不受数据量纲的影响。 归一化可以 保持学习有效性, 缓解梯度消失和梯度爆炸。

通俗化来说,归一化主要就是为了让不同的输入特征在数值大小上不会相互干扰,都被映射到一个特定范围 ,从而更便于讨论不同特征间的相对影响,是根据某一特征值的相对大小来讨论这个特征值的相关影响的

  • 原理:对数据进行某种变换,使其落入特定范围或满足特定分布。常见的归一化方法有数据标准化(如将数据转化为均值为0,方差为 1的分布)和最小 - 最大缩放(将数据缩放到 [0, 1] 或 [-1, 1] 等区间)。在神经网络中,还有批归一化(Batch Normalization,BN),它对每个批次的数据在网络的每一层进行归一化处理,使得输入数据分布稳定,加速模型收敛。
  • 优点:加快模型训练时的收敛速度,提高模型的稳定性和泛化能力还能缓解梯度消失和梯度爆炸问题。
  • 缺点:在处理动态数据或小批量数据时,归一化效果可能不理想。比如 BN 在 RNN 中应用时,由于序列长度不一,批次内数据差异大,会影响归一化效果。

分类

特征归一化

将每个特征值自己的数据弄成均值变为 0,方差变为 1

批量归一化(Batch Norm)



作用

批量归一化化和特征归一化的区别

通俗讲,批量归一化就是训练每个batch之前才对每个batch里的每个特征对应的数据进行归一化,特征归一化就是开始训练之前所有的特征对应的数据已经进行特征化了。一个是过程中再归一,一个是提前就归一好。

  • 处理范围
    • 特征归一化:通常是针对整个数据集的某个特征维度,利用整个数据集该维度的均值和方差来归一化,不依赖数据的批次划分 。比如在一个房价预测数据集中,对 “房屋面积” 这一特征,计算所有样本房屋面积的均值和方差来归一化该特征。
    • 批量归一化:是在训练过程中,针对每个 mini - batch 的数据,分别计算每个特征维度在这个 mini - batch 内的均值和方差进行归一化 。不同 mini - batch 计算出的均值和方差可能不同。
  • 应用位置
    • 特征归一化:一般在数据预处理阶段进行,在数据输入模型之前,对原始数据的各个特征维度做归一化处理 。
    • 批量归一化:应用于神经网络内部,通常在仿射变换(矩阵乘法加偏置)之后,激活函数之前,对中间层的输入进行归一化 。
  • 目的侧重点
    • 特征归一化:主要目的是消除不同特征之间的量纲差异,使模型更容易学习不同特征之间的关系,加快模型收敛速度
    • 批量归一化:除了加快收敛,还能减少内部协变量转移(Internal Covariate Shift,即训练过程中神经网络各层输入分布的变化),让各层的输入分布更稳定,同时允许使用更大的学习率,并且一定程度上起到正则化的作用,防止模型过拟合 。
  • 测试时处理方式
    • 特征归一化:测试时使用训练集计算出的均值和方差对测试数据进行归一化,计算方式固定 。
    • 批量归一化:测试时一般使用训练过程中记录的各 mini - batch 均值和方差的指数加权移动平均值来进行归一化 ,与训练时按当前 mini - batch 计算有所不同。

 B.模型

具体阐述括号里内容: 

 (输入特征图数量=上一层输出特征图数量=上一层卷积核的数量,

输出特征图数量=本层卷积核的数量,

卷积核大小,

步长,

padding)

在这张 AlexNet 相关图示中,输入特征图数量指的就是初始特征图的通道(channel)数 。比如图中最初输入为 “3*224*224” ,这里的 “3” 就代表输入特征图数量,即图像的 RGB 三通道 。 后续 Conv2d 括号内的第一个数字,也对应着上一层输出特征图数量(通道数),也就是上次层的卷积核数量。

池化:

图中展示了 AlexNet 网络中的两种池化操作:?

  • 普通池化(Pool):参数 (3, 2) 表示池化核大小为 3×3,步长为 2 。该操作会对输入特征图进行下采样,减少空间维度大小,同时保留主要特征信息,通常不改变特征图的通道数。
  • 自适应池化(adaPool):参数 (6) 表示将输出特征图的空间维度调整为 6×6 。无论输入特征图尺寸是多少,自适应池化会自动计算合适的池化方式,把特征图转换为指定的 6×6 尺寸,同样不改变通道数,图中最终输出为 256*6*6,256 为通道数 。

C.代码实现

import torchvision.models as models
import torch.nn as nn


alexnet = models.alexnet()
print(alexnet)
  1. 导入模块import torchvision.models as models:导入torchvision库中的models模块,该模块提供了许多预定义的经典神经网络模型,比如 AlexNet、VGG、ResNet 等。import torch.nn as nn导入 PyTorch 的神经网络模块nn,用于构建和定义神经网络的各种层和操作,如卷积层、全连接层、激活函数层等。
  2. 实例化模型alexnet = models.alexnet()通过调用models.alexnet()函数实例化了一个 AlexNet 模型对象,此时创建的是默认配置的 AlexNet 网络结构,该网络在 ImageNet 数据集上进行过预训练(如果本地没有预训练权重,会自动下载)。
  3. 打印模型print(alexnet)将实例化后的 AlexNet 模型结构打印输出。执行该语句后,会在控制台看到 AlexNet 网络详细的层次结构,包括每一层的名称、输入输出维度等信息。

这里就是导入模块,用一些pytorch里现成的alexnet模型,我们先康康长啥样,不想看的化也可以不写这两句

定义模型类,写一下alexnet

class MyAlexNet(nn.Module):
    def __init__(self):
        super(MyAlexNet, self).__init__()
        self.relu = nn.ReLU()
        self.drop = nn.Dropout(0.5)

        self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=11, stride=4,padding=2)
        self.pool1 = nn.MaxPool2d(3, stride=2)
        self.conv2 = nn.Conv2d(64,192,5,1,2)
        self.pool2 = nn.MaxPool2d(3, stride=2)

        self.conv3 = nn.Conv2d(192,384,3,1,1)

        self.conv4 = nn.Conv2d(384,256,3,1,1)

        self.conv5 = nn.Conv2d(256, 256, 3, 1, 1)

        self.pool3 = nn.MaxPool2d(3, stride=2)
        self.adapool = nn.AdaptiveAvgPool2d(output_size=6)

        self.fc1 = nn.Linear(9216,4096)

        self.fc2 = nn.Linear(4096,4096)

        self.fc3 = nn.Linear(4096,1000)

    def forward(self,x):

        x = self.conv1(x)
        x = self.relu(x)
        x = self.pool1(x)

        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool2(x)


        x = self.conv3(x)
        x = self.relu(x)
        print(x.size())
        x = self.conv4(x)
        x = self.relu(x)
        print(x.size())
        x = self.conv5(x)
        x = self.relu(x)
        x = self.pool3(x)
        print(x.size())
        x= self.adapool(x)
        x = x.view(x.size()[0], -1)

        x = self.fc1(x)
        x = self.relu(x)

        x = self.fc2(x)
        x = self.relu(x)

        x = self.fc3(x)
        x = self.relu(x)

        return x

 照着这个图啊,清晰明了啊只能说,后边来了几个全连接神经网络,懂得都懂啊,有丢丢不懂的看深度学习——分类任务基础原理 及 卷积 基础原理-CSDN博客和问deepseek,永远的神哈哈哈哈哈哈哈哈哈哈哈^_^

一些小操作

import torch

myalexnet = MyAlexNet()

def get_parameter_number(model):
    total_num = sum(p.numel() for p in model.parameters())
    trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
    return {'Total': total_num, 'Trainable': trainable_num}


print(get_parameter_number(myalexnet))

img = torch.zeros((4,3,224,224))

out = myalexnet(img)

print(out.size())
  1. 导入模块import torch导入 PyTorch 库,后续代码将基于该库进行神经网络相关操作。
  2. 实例化模型myalexnet = MyAlexNet()尝试实例化一个名为MyAlexNet的模型,但从现有代码看,并没有定义MyAlexNet类,这会导致运行时出现NameError错误。

定义获取参数数量的函数

  • get_parameter_number函数用于计算模型的总参数数量和可训练参数数量。                
  • total_num = sum(p.numel() for p in model.parameters())通过遍历模型的所有参数p,使用p.numel()获取每个参数的元素数量,然后求和得到总参数数量。                               
  • trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)类似地,不过只对需要计算梯度(即可训练)的参数进行求和,得到可训练参数数量。

打印参数数量print(get_parameter_number(myalexnet))调用上述函数打印模型的参数数量信息。

创建输入数据并运行模型img = torch.zeros((4, 3, 224, 224))创建一个形状为(4, 3, 224, 224)的张量,模拟输入图像数据,其中4是批量大小,3是通道数,224, 224是图像的高度和宽度。out = myalexnet(img)将输入数据传入模型,得到模型的输出。print(out.size())打印输出结果的形状。

二、经典神经模型 VggNet

2014年,VGG网络被提出,其在AlexNet的基础上, 运用了更小的卷积核,并且加深了网络, 达到了更好的效果 ,直到今天,在某些场合,我们仍在使用VGG

A.创新点:用小卷积核代替大的卷积核

知识补充:CNN感受野

什么是感受野:

在卷积神经网络中,感受野是指神经网络中某一层的输出特征图上的一个点所对应的输入图像上的区域大小。简单来说,就是输出特征图上的一个神经元能够 “看到” 的输入图像的范围。例如,在一个简单的卷积层中,一个 3×3 的卷积核在对输入图像进行卷积操作时,它所覆盖的输入图像区域就是该卷积核的感受野,即 3×3 的区域,输出特征图上的某一个神经元的感受野大小就是3*3

从顶层特征图神经元出发,逐步推导其对应下层输入图的区域范围,说明了顶层某一点的感受野是对应初始特征图的区域,

比如说,这个图片中,首先我们有一个卷积层conv1,用一个3*3的卷积核以步长为2去卷,得到了4*4的新的输出图,这个图片里都是channel=1哈,之后我们又经过了一个MaxPool池化层,得到了新的特征图为2*2的大小

现在我们看这个2*2的最新输出特征图,它的一个神经元(一个小格格)其实对应的是上一层的输入图(还未池化的中间那层的图)的2*2,这就是它的感受野,

再向下推,对应的中间那层的2*2,其实对应于最下方的初始特征图的5*5,那这2*2的感受野就是它下层输入图(还未经过卷积层1的图中最下层的图)的5*5

所以最顶上一层的某一个点的感受野就是对应初始特征图的5*5

可以再用公式计算一下,第三层的一个点的第三层感受野肯定是1*1啊 ,所以F(3) = 1

然后我们看第三层的一个点的第二层感受野呢,就是F(2) = (F(3) - 1)*2 + 2 = 2

然后我们看第三层的一个点的第一层感受野呢,就是F(1) = (F(2) - 1)*2 + 3 = 5

所以按公式也算出是5*5的感受野

懂了感受野是什么以及怎么计算,我们就可以接着继续vgg的神奇创新了

VGGNet 采用 3×3 的小卷积核,通过堆叠多个小卷积核来替代传统的大尺寸卷积核,如两个 3×3 卷积核的堆叠效果相当于一个 5×5 卷积核,三个 3×3 卷积核的堆叠相当于一个 7×7 卷积核

同理按照公式也一样,经三层 3*3的卷积核的卷积层,最后第四层的一个点对应的感受野是7*7,我们想一下直接拿一个7*7的卷积核去只卷一层的效果一样,第二层的一个点的感受野也是7*7

但是!!!

达到同样的感受野,这两种方式对应的参数却不同,

第一种是3个3*3,也就是共27个参数

第二种1个7*7,却是49个参数

so!!!很牛叉!!!

用小卷积核代替大的卷积核

  • 减少参数数量:相比于大卷积核,小卷积核在保持相同感受野的同时,参数更少,计算效率更高。例如,当输入通道数和输出通道数均为 c 时,3 层 conv3×3 卷积所需要的卷积层参数是 27c²,而一层 conv7×7 卷积所需要的卷积层参数是 49c²,conv7×7 的卷积核参数比 conv3×3 多了约 81%。
  • 扩大感受野:小卷积核通过堆叠可以有效地扩大网络的感受野,实现了与大卷积核相同的感受野效果,同时增加了网络的深度,有助于学习更复杂的特征
  • 增加非线性变换:多个小卷积核的堆叠可以引入更多的非线性变换。因为每一个小卷积核后都可以接 ReLU 激活函数,相比于单一的大卷积核,多个小卷积核提供了更多的非线性操作,增强了网络的非线性表达能力,使网络能够更好地拟合复杂的图像数据分布。
  • 保持图像细节:小卷积核的接受域较小,能够捕捉到更多的局部细节信息,有助于更好地保持图像的边缘、纹理等性质,从而提取到更精细的特征,提高图像分类等任务的准确性。

B.模型

具体阐述括号里内容: 

 (输入特征图数量=上一层输出特征图数量=上一层卷积核的数量,

输出特征图数量=本层卷积核的数量,

卷积核大小,

步长,

padding)和AlexNet同理哈

1. 特征图尺寸变化

  • 第一组卷积和池化
    • 第一个 Conv2d(3, 64, 3, 1, 1) :输入通道 3,输出通道 64,卷积核 3×3,步长 1,填充 1。根据公式\(W_{out} = \lfloor\frac{W_{in} - K + 2P}{S}\rfloor + 1\)(\(W_{in}\)为输入宽度 / 高度,K为卷积核大小,P为填充,S为步长),输出特征图尺寸为\(224×224\),通道数变为 64 ,即\(64×224×224\)。
    • 第二个 Conv2d(64, 64, 3, 1, 1) :输出特征图尺寸仍为\(224×224\),通道数保持 64 ,即\(64×224×224\)。
    • Pool(默认 2×2 最大池化,步长 2):输出特征图尺寸变为\(112×112\),通道数不变,即\(64×112×112\)。
  • 第二组卷积和池化
    • 第一个 Conv2d(64, 128, 3, 1, 1) :输出特征图尺寸为\(112×112\),通道数变为 128,即\(128×112×112\)。
    • 第二个 Conv2d(128, 128, 3, 1, 1) :输出特征图尺寸为\(112×112\),通道数保持 128,即\(128×112×112\)。
    • Pool :输出特征图尺寸变为\(56×56\),通道数不变,即\(128×56×56\)。
  • 第三组卷积和池化
    • 第一个 Conv2d(128, 256, 3, 1, 1) :输出特征图尺寸为\(56×56\),通道数变为 256,即\(256×56×56\)。
    • 第二个 Conv2d(256, 256, 3, 1, 1) :输出特征图尺寸为\(56×56\),通道数保持 256,即\(256×56×56\)。
    • Pool :输出特征图尺寸变为\(28×28\),通道数不变,即\(256×28×28\)。
  • 第四组卷积和池化
    • 第一个 Conv2d(256, 512, 3, 1, 1) :输出特征图尺寸为\(28×28\),通道数变为 512,即\(512×28×28\)。
    • 第二个 Conv2d(512, 512, 3, 1, 1) :输出特征图尺寸为\(28×28\),通道数保持 512,即\(512×28×28\)。
    • Pool :输出特征图尺寸变为\(14×14\),通道数不变,即\(512×14×14\)。
  • 自适应平均池化
    • adaPool(7) :将特征图尺寸调整为\(7×7\),通道数不变,即\(512×7×7\)。

2. 参数量计算

  • 卷积层参数量
    • 对于 Conv2d(3, 64, 3, 1, 1) :参数量为\((3×3×3 + 1)×64 = 1792\)。(64个卷积核,每个卷积核都是3*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(64, 64, 3, 1, 1) :参数量为\((3×3×64 + 1)×64 = 36928\)。(64个卷积核,每个卷积核都是64*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(64, 128, 3, 1, 1) :参数量为\((3×3×64 + 1)×128 = 73728\)。(128个卷积核,每个卷积核都是64*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(128, 128, 3, 1, 1) :参数量为\((3×3×128 + 1)×128 = 147584\)。(128个卷积核,每个卷积核都是128*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(128, 256, 3, 1, 1) :参数量为\((3×3×128 + 1)×256 = 295168\)。(256个卷积核,每个卷积核都是128*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(256, 256, 3, 1, 1) :参数量为\((3×3×256 + 1)×256 = 590080\)。(256个卷积核,每个卷积核都是256*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(256, 512, 3, 1, 1) :参数量为\((3×3×256 + 1)×512 = 1179648\)。(512个卷积核,每个卷积核都是256*3*3个参数,且每个卷积核还要带一个b)
    • 对于 Conv2d(512, 512, 3, 1, 1) :参数量为\((3×3×512 + 1)×512 = 2359808\)。(512个卷积核,每个卷积核都是512*3*3个参数,且每个卷积核还要带一个b)
    • 卷积层总参数量为\(1792 + 36928 + 73728 + 147584 + 295168 + 590080 + 1179648 + 2359808 = 4684744\) 。

这里面的每一个“+1”都是对应于——一个卷积核要有一个bias偏置项b

  • 全连接层参数量
    • 对于 Linear(25088, 4096) :参数量为\(25088×4096 + 4096 = 102760448\)。
    • 对于 Linear(4096, 4096) :参数量为\(4096×4096 + 4096 = 16781312\)。
    • 对于 Linear(4096, 1000) :参数量为\(4096×1000 + 1000 = 4097000\)。 全连接层总参数量为\(102760448 + 16781312 + 4097000 = 123638760\) 。
  • 模型总参数量:卷积层总参数量 + 全连接层总参数量,即\(4684744 + 123638760 = 128323504\) 。

C.代码实现

import torchvision.models as models
import torch.nn as nn


vgg = models.vgg13()
print(vgg)

class vggLayer(nn.Module):
    def __init__(self,in_cha, mid_cha, out_cha):
        super(vggLayer, self).__init__()
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(2)
        self.conv1 = nn.Conv2d(in_cha, mid_cha, 3, 1, 1)
        self.conv2 = nn.Conv2d(mid_cha, out_cha, 3, 1, 1)

    def forward(self,x):
        x = self.conv1(x)
        x= self.relu(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool(x)
        return x



class MyVgg(nn.Module):
    def __init__(self):
        super(MyVgg, self).__init__()

        self.layer1 = vggLayer(3, 64, 64)

        self.layer2 = vggLayer(64, 128, 128)

        self.layer3 = vggLayer(128, 256, 256)

        self.layer4 = vggLayer(256, 512, 512)

        self.layer5 = vggLayer(512, 512, 512)
        self.adapool = nn.AdaptiveAvgPool2d(7)
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(25088, 4096)
        self.fc2 = nn.Linear(4096, 4096)
        self.fc3 = nn.Linear(4096, 1000)

    def forward(self,x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)
        x = self.adapool(x)

        x= self.adapool(x)
        x = x.view(x.size()[0], -1)

        x = self.fc1(x)
        x = self.relu(x)

        x = self.fc2(x)
        x = self.relu(x)

        x = self.fc3(x)
        x = self.relu(x)

        return x


import torch

myVgg = MyVgg()
#
img = torch.zeros((1, 3, 224,224))

out = myVgg(img)

print(out.size())

def get_parameter_number(model):
    total_num = sum(p.numel() for p in model.parameters())
    trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
    return {'Total': total_num, 'Trainable': trainable_num}


print(get_parameter_number(myVgg))
print(get_parameter_number(myVgg.layer1))
print(get_parameter_number(myVgg.layer1.conv1))
#
# print(get_parameter_number(vgg))

和AlexNet一个道理哈,开头两句是用torch已经内置好的vgg我们来看看

后边是自己写的

对应前面劈里啪啦的一堆,无非就是引入了一个layer类把一些都整合到一起,这样写的好看且方便

很好理解的^_^

若不懂依旧是问deepseek豆包等等,永远的神,我们的好老师!!!

三、ResNet(更新中

A.创新点:1*1卷积 和 残差连接

①1*1卷积

简而言之,就是减少特征图的通道数,减少参数,减少计算量,更高效

在使用 1×1 卷积时,输出特征图的通道数和这一层使用的1×1 卷积核的数量是一样的。

  • 定义:1×1 卷积是卷积神经网络(CNN)中的一种卷积操作,卷积核的大小为 1×1。它在不改变特征图的空间尺寸的情况下,主要用于调整特征图的通道数。
  • 计算过程:对于输入特征图中的每个像素点,1×1 卷积核会与该像素点在所有通道上的值进行加权求和,得到输出特征图中对应位置的一个值。如果输入特征图的通道数为Cin,输出特征图的通道数为Cout,那么每个输出通道的每个像素值都是通过将输入特征图对应位置的像素在Cin个通道上的值与一个1×1×Cin的卷积核进行点积运算,再加上偏置项得到的。
  • 作用
    • 通道数调整:可以实现升维或降维操作。例如在 Inception 模块中,通过 1×1 卷积将高维特征图的通道数降低,减少后续卷积操作的计算量。
    • 跨通道信息整合:能够融合不同通道的信息,学习到通道之间的相关性,使网络可以根据不同通道的重要性进行特征组合。
    • 增加非线性:在卷积层之间插入 1×1 卷积层,可以引入额外的非线性变换,增强网络的表达能力,因为它可以在不改变空间结构的情况下,对通道维度进行非线性映射。

②残差连接

在先讲残差连接之前,讲一下梯度爆炸 和 梯度消失,帮助我们更好理解为什么要引入残连接

梯度爆炸 和 梯度消失

梯度爆炸和梯度消失是在深度学习训练过程中经常遇到的两个问题,

梯度爆炸:

就比方说,梯度回传(链式求导),如果前边的偏导都太大,乘很多次到后边的偏导值就会很大,甚至指数级爆炸,以至于大到无法更新参数了

  • 定义:在神经网络训练过程中,梯度值变得非常大,以指数级增长,导致网络的参数更新幅度极大,使得模型训练变得不稳定,甚至无法收敛,这种现象就被称为梯度爆炸。
  • 产生原因
    • 网络层数过深:随着网络层数的增加,梯度在反向传播过程中需要经过多个层的计算。如果每一层的权重矩阵的某些特征值大于 1,那么在多次矩阵乘法后,梯度就会以指数形式增长,从而导致梯度爆炸。
    • 激活函数选择不当:某些激活函数,如 Sigmoid 函数和 Tanh 函数,在输入值较大或较小时,其导数会趋近于 0 或一个较小的值。当网络的输出值处于这些区域时,会导致梯度在反向传播过程中逐渐变小。但如果网络参数初始化不当,使得激活函数的输入值远离其合适的范围,也可能导致梯度在正向传播时变得很大,进而在反向传播中引发梯度爆炸。
    • 学习率设置过大:学习率决定了参数更新的步长。如果学习率设置得过大,那么在每次参数更新时,可能会使参数更新的幅度超出了合适的范围,导致梯度急剧增大,引发梯度爆炸。
  • 影响
    • 模型无法收敛:由于梯度爆炸会导致参数更新过于剧烈,模型的权重会快速偏离最优值,使得模型无法收敛到一个稳定的状态,从而无法得到有效的训练结果。
    • 数值溢出:梯度值过大可能会导致数据类型溢出,使得计算结果变得不准确,甚至可能导致程序崩溃。

梯度消失

就比方说,梯度回传(链式求导),如果前边的偏导都太小,乘很多次到后边的偏导值就会很小,以至于小到无法更新参数了

  • 定义:与梯度爆炸相反,梯度消失是指在神经网络反向传播过程中,梯度值随着传播层数的增加而逐渐减小,最终趋近于 0,导致网络前面的层无法接收到足够的梯度信息来进行参数更新,使得这些层的权重几乎不发生变化,网络难以训练。
  • 产生原因
    • 链式求导法则:在反向传播中,梯度是通过链式求导法则从输出层向输入层传递的。每经过一层,梯度都要乘以该层的权重矩阵和激活函数的导数。如果这些值在每一层都小于 1,那么经过多层传递后,梯度就会以指数形式衰减,趋近于 0。
    • 激活函数的选择:如 Sigmoid 函数和 Tanh 函数,其导数的取值范围是有限的,在输入值较大或较小时,导数会趋近于 0。当网络的输出值处于这些区域时,会导致梯度在反向传播过程中逐渐变小,容易引发梯度消失问题。
    • 权重初始化不当:如果网络的权重初始化值过小,可能会导致在反向传播时,梯度在经过这些层时不断缩小,最终导致梯度消失。
  • 影响
    • 训练停滞:网络前面的层无法得到有效的梯度更新,使得这些层的权重几乎不发生变化,网络的训练就会停滞,无法继续优化模型。
    • 模型欠拟合:由于前面的层无法学习到有效的特征,整个模型的表达能力受限,无法很好地拟合训练数据,导致模型的准确率较低,泛化能力差。

解决方法

  • 梯度裁剪:在反向传播过程中,当梯度值超过一定阈值时,就对梯度进行裁剪,使其保持在一个合理的范围内,避免梯度爆炸。
  • 合适的初始化方法:采用合适的权重初始化方法,如 Xavier 初始化、Kaiming 初始化等,能够使权重在初始化时就处于一个合理的范围,减少梯度消失或爆炸的可能性。
  • 选择合适的激活函数:使用如 ReLU、LeakyReLU 等激活函数,它们在一定程度上可以避免梯度消失问题,因为这些函数在某些区域的导数是一个常数或大于 0 的值,不会导致梯度在反向传播中迅速衰减。
  • 使用残差连接:残差连接可以为梯度提供直接传播的路径,使得梯度在反向传播过程中能够更容易地传递到前面的层,减少梯度消失的影响。
  • 调整学习率:采用合适的学习率调度策略,如学习率衰减、自适应学习率算法等,根据训练的进展动态调整学习率,避免学习率过大或过小导致的问题。

所以我们要引入残差连接,避免梯度消失和梯度爆炸

如何实现的呢?

残差连接

B.模型

restnet有很多种类

C.代码实现

import torch
import torch.nn as nn
import torchvision.models as models


resNet = models.resnet18()

print(resNet)


class Residual_block(nn.Module):  #@save
    def __init__(self, input_channels, out_channels, down_sample=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, out_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(out_channels, out_channels,
                               kernel_size=3, padding=1, stride= 1)
        if input_channels != out_channels:
            self.conv3 = nn.Conv2d(input_channels, out_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()
    def forward(self, X):
        out = self.relu(self.bn1(self.conv1(X)))
        out= self.bn2(self.conv2(out))

        if self.conv3:
            X = self.conv3(X)
        out += X
        return self.relu(out)

class MyResNet18(nn.Module):
    def __init__(self):
        super(MyResNet18, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
        self.bn1 = nn.BatchNorm2d(64)
        self.pool1 = nn.MaxPool2d(3, stride=2, padding=1)
        self.relu = nn.ReLU()

        self.layer1 = nn.Sequential(
            Residual_block(64, 64),
            Residual_block(64, 64)
        )
        self.layer2 = nn.Sequential(
            Residual_block(64, 128, strides=2),
            Residual_block(128, 128)
        )
        self.layer3 = nn.Sequential(
            Residual_block(128, 256, strides=2),
            Residual_block(256, 256)
        )
        self.layer4 = nn.Sequential(
            Residual_block(256, 512, strides=2),
            Residual_block(512, 512)
        )
        self.flatten = nn.Flatten()
        self.adv_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(512, 1000)
    def forward(self, x):

        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.pool1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.adv_pool(x)
        x = self.flatten(x)
        x = self.fc(x)

        return x

myres = MyResNet18()

def get_parameter_number(model):
    total_num = sum(p.numel() for p in model.parameters())
    trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
    return {'Total': total_num, 'Trainable': trainable_num}


print(get_parameter_number(myres.layer1))
print(get_parameter_number(myres.layer1[0].conv1))
print(get_parameter_number(resNet.layer1[0].conv1))

x = torch.rand((1,3,224,224))
out = resNet(x)

out = myres(x)


 

;