实验12 卷积神经网络(3)

目录

1.模型构建:

 1.1统计模型参数

1.2统计计算量

2.模型训练

3.Debug 

4.模型评价  

5.带残差连接ResNet

1.模型构建:

         残差块通过跳跃连接将输入直接添加到输出上,通过跳跃连接,确保梯度可以直接反向传播到浅层,缓解了梯度消失问题。通过学习残差 F(x)=H(x)−x,也能简化目标函数的优化。

       残差模块变相地增加了网络的层数,提高了反向传播时参数的更新效率。但是同时,也提高了网络的运算量。

       首先构建一个不带残差连接的ResNet来观察与带残差连接的网络相比,模型对数据的训练能力。

class ResBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, use_residual=True):
        """
        残差单元
        输入:
            - in_channels:输入通道数
            - out_channels:输出通道数
            - stride:残差单元的步长,通过调整残差单元中第一个卷积层的步长来控制
            - use_residual:用于控制是否使用残差连接
        """
        super(ResBlock, self).__init__()
        self.stride = stride
        self.use_residual = use_residual
        # 第一个卷积层,卷积核大小为3×3,可以设置不同输出通道数以及步长
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1, stride=self.stride, bias=False)
        # 第二个卷积层,卷积核大小为3×3,不改变输入特征图的形状,步长为1
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False)

        # 如果conv2的输出和此残差块的输入数据形状不一致,则use_1x1conv = True
        # 当use_1x1conv = True,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if in_channels != out_channels or stride != 1:
            self.use_1x1conv = True
        else:
            self.use_1x1conv = False
        # 当残差单元包裹的非线性层输入和输出通道数不一致时,需要用1×1卷积调整通道数后再进行相加运算
        if self.use_1x1conv:
            self.shortcut = nn.Conv2d(in_channels, out_channels, 1, stride=self.stride, bias=False)

        # 每个卷积层后会接一个批量规范化层,批量规范化的内容在7.5.1中会进行详细介绍
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        if self.use_1x1conv:
            self.bn3 = nn.BatchNorm2d(out_channels)

    def forward(self, inputs):
        test = inputs.detach().numpy()
        y = F.relu(self.bn1(self.conv1(inputs)))
        y = self.bn2(self.conv2(y))
        if self.use_residual:
            if self.use_1x1conv:  # 如果为真,对inputs进行1×1卷积,将形状调整成跟conv2的输出y一致
                shortcut = self.shortcut(inputs)
                shortcut = self.bn3(shortcut)
            else:  # 否则直接将inputs和conv2的输出y相加
                shortcut = inputs
            y = torch.add(shortcut, y)
        out = F.relu(y)
        return out

# 定义完整残差网络
def make_first_module(in_channels):
    # 模块一:7*7卷积、批量规范化、汇聚
    m1 = nn.Sequential(
        nn.Conv2d(in_channels, 64, 7, stride=2, padding=3),
        nn.BatchNorm2d(64), nn.ReLU(),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
    )
    return m1

def resnet_module(input_channels, out_channels, num_res_blocks, stride=1, use_residual=True):
    blk = []
    # 根据num_res_blocks,循环生成残差单元
    for i in range(num_res_blocks):
        if i == 0: # 创建模块中的第一个残差单元
            blk.append(ResBlock(input_channels, out_channels,
                                stride=stride, use_residual=use_residual))
        else:      # 创建模块中的其他残差单元
            blk.append(ResBlock(out_channels, out_channels, use_residual=use_residual))
    return blk


def make_modules(use_residual):
    # 模块二:包含两个残差单元,输入通道数为64,输出通道数为64,步长为1,特征图大小保持不变
    m2 = nn.Sequential(*resnet_module(64, 64, 2, stride=1, use_residual=use_residual))
    # 模块三:包含两个残差单元,输入通道数为64,输出通道数为128,步长为2,特征图大小缩小一半。
    m3 = nn.Sequential(*resnet_module(64, 128, 2, stride=2, use_residual=use_residual))
    # 模块四:包含两个残差单元,输入通道数为128,输出通道数为256,步长为2,特征图大小缩小一半。
    m4 = nn.Sequential(*resnet_module(128, 256, 2, stride=2, use_residual=use_residual))
    # 模块五:包含两个残差单元,输入通道数为256,输出通道数为512,步长为2,特征图大小缩小一半。
    m5 = nn.Sequential(*resnet_module(256, 512, 2, stride=2, use_residual=use_residual))
    return m2, m3, m4, m5

class Model_ResNet18(nn.Module):
    def __init__(self, in_channels=3, num_classes=10, use_residual=True):
        super(Model_ResNet18,self).__init__()
        m1 = make_first_module(in_channels)
        m2, m3, m4, m5 = make_modules(use_residual)
        # 封装模块一到模块6
        self.net = nn.Sequential(m1, m2, m3, m4, m5,
                        # 模块六:汇聚层、全连接层
                        nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(512, num_classes) )

    def forward(self, x):
        test = np.array(x)
        return self.net(x)
  • 第一模块:包含了一个步长为2,大小为7×7的卷积层,卷积层的输出通道数为64,卷积层的输出经过批量归一化、ReLU激活函数的处理后,接了一个步长为2的3×3的最大汇聚层;
  • 第二模块:包含了两个残差单元,经过运算后,输出通道数为64,特征图的尺寸保持不变;
  • 第三模块:包含了两个残差单元,经过运算后,输出通道数为128,特征图的尺寸缩小一半;
  • 第四模块:包含了两个残差单元,经过运算后,输出通道数为256,特征图的尺寸缩小一半;
  • 第五模块:包含了两个残差单元,经过运算后,输出通道数为512,特征图的尺寸缩小一半;
  • 第六模块:包含了一个全局平均汇聚层,将特征图变为1×1的大小,最终经过全连接层计算出最后的输出。

 1.1统计模型参数

model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=True)
params_info = summary(model, (1, 1, 32, 32))

1.2统计计算量

flops,param = get_model_complexity_info(model,(1,32,32),as_strings=True, print_per_layer_stat=True)

        部分统计量如下: 

2.模型训练

# 定义网络,不使用残差结构的深层网络
model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=False)
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(),lr=LR)
# 实例化RunnerV3
runner = RunnerV3(model, optimizer, loss_fn, metric)
# 启动训练
log_steps = 15
eval_steps = 15
runner.train(train_loader, dev_loader, num_epochs=5, log_steps=log_steps,
            eval_steps=eval_steps, save_path="best_model.pdparams")
# 可视化观察训练集与验证集的Loss变化情况
plot_training_loss_acc(runner, 'cnn-loss2.pdf')

3.Debug 

        这个结果显然很奇怪啊,因此这里测试了预测值的大小,发现预测值全为9,首先推测是数据集初始化的问题。

 第一次尝试  

        经过排查是因为数据集初始化的时候归一化和书上不一样。tranform的方法采用了to_tensor的方法,它会将图像从[0, 255]范围(uint8格式)缩放到[0, 1]范围(float32格式)。

        经过查询资料发现,如果图片本身是正确的,但数据数值非常小(例如,接近0),观察数据时可能会误认为全零。因此存在可能是图片归一化范围太小导致的问题。

 第二次尝试 

        在排除了这个错误并对数据重新进行[-1,1]的归一化之后,的数据任然得出了错误的结果。那么还有一个可能就是激活函数导致的错误。

        这里查询了在验证集上的logits的数值,也就是最后经过前向传播的输出值,发现他们在第一维上完全一样

        这里查看但是这里在train函数上计算loss的时候的激活是对的,那么问题应该出在测试集上。  ​​​​

         但是发现运行结果还是错的。

        这里提出了一个猜想:学习率过低。

第三次尝试: 

        这里打印了所有层的权重。

        于是我猜测可以是因为学习率太小了,导致批归一化层完全没有更新,也就是发生了梯度消失的现象。因此在测试集上得出的logits结果完全一样。

lr:0.01->0.05

        在增大学习率之后虽然前向传播后的权重发生了变化,但是预测结果仍然没有发生变化。

第四次尝试

        这里猜测是梯度更新的问题。以下是部分权重的更新现象。

        打印了反向传播后的权重之后,发现卷积核上出现了梯度消失的现象。

        一开始是怀疑relu函数的问题,但是更换激活函数也没有发生显著的变化。 

        于是倒查了输入量,采用了其他的代码后发现我的代码初始化有问题,在第一维上的数据全部相同。也就是说所有的通道上的卷积核一样。最后修改了代码,终于跑对了。

卷积后的数据 ​​​

输入数据 

         正确的结果如下:

         可以看出,不带残差连接的ResNet上,相较于LetNet而言,正确率并没有很大的提高,甚至不如LetNet的计算时间少。

        正确的不同通道上,不同卷积核的卷积表现:

4.模型评价  

5.带残差连接ResNet

        下面实验带残差连接的ResNet。 

model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=True)

        从结果可以看出,相较于无残差连接的ResNet来说,该网络模型显著提高了正确率。但是运行时长也相应有些增长。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值