【自学记录7】【Pytorch2.0深度学习从零开始学 王晓华】第七章 实战ResNet

7.1.4 ResNet网络的实现

遇到的问题:
1、nn.ReLU(inplace=True)
在PyTorch中,nn.ReLU(inplace=True)中的inplace=True参数表示该ReLU激活函数将直接修改输入张量,而不是创建一个新的输出张量。这意味着它会在原地(in-place)执行操作,不会占用额外的内存空间来存储输出。
具体地说,当你使用inplace=True时,输入张量x在经过ReLU激活函数后,其值会直接被ReLU的结果所替换。这样做可以节省内存,但需要注意,由于输入张量被修改了,因此在后续的计算中,如果你还需要原始张量的值,就可能会遇到问题。
2、ResNet的四个主要阶段
ResNet(残差神经网络)的四个主要阶段是指其网络结构中的四个部分,每个部分都包含多个残差块(Residual Block)。这些阶段的设计旨在逐步提取输入图像的特征,并随着网络的深入,逐渐加深和提升这些特征。以下是关于这四个阶段的详细解释:

第一阶段(Stage 1):

通常包含几个卷积层(例如3个)和池化层(例如2个)。
这一阶段的主要任务是对输入的图像进行初步的特征提取。卷积层通过卷积操作捕捉图像的局部特征,而池化层则用于减小特征图的空间尺寸,降低计算量,并增加特征的鲁棒性。
第二阶段(Stage 2):

包含更多的卷积层(例如4个)和池化层(例如2个)。
这一阶段进一步加深和提升了第一阶段提取的特征。通过增加卷积层的数量,网络能够学习到更复杂的特征表示。
第三阶段(Stage 3):

包含更多的卷积层(例如6个)和池化层(例如2个)。
在这一阶段,网络继续对特征进行加深和提升,进一步抽象和提取更高层次的特征。
第四阶段(Stage 4):
通常包含较少的卷积层(例如3个)和一个池化层。
这一阶段主要是对第三阶段提取的特征进行最终的加深和提升,为后续的分类或回归任务做准备。
除了这四个主要阶段外,ResNet还包含一个全连接层,用于将提取的特征映射到输出类别上。在每个阶段中,残差块的使用是关键。残差块通过引入恒等映射(identity mapping),使得网络在加深时能够更容易地优化,从而解决了深度神经网络训练中的梯度消失或爆炸问题。
总的来说,ResNet的四个主要阶段构成了一个深度卷积神经网络,通过逐步提取和加深特征,实现了对输入图像的高效处理。这种结构使得ResNet在图像识别、目标检测、人脸识别等领域取得了显著的成果。

源码\第七章\ resnet.py

import torch
import torch.nn as nn

#BasicBlock 是 ResNet 中使用的基本残差块类型,它是构成整个 ResNet 网络的基本组件。
#BasicBlock包含两部分:a、学习残差。b、捷径连接
class BasicBlock(nn.Module):

    expansion = 1
    #in_channels 和 out_channels 分别代表输入和输出通道数。stride 是卷积操作的步长。
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()

        #a、residual function 执行【卷积】操作以学习残差。
        self.residual_function = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),#批量正则化
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels * BasicBlock.expansion, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels * BasicBlock.expansion)
        )

        #b、shortcut:捷径连接,用于将输入直接连接到输出,以进行残差学习。
        self.shortcut = nn.Sequential()#里面啥也没有
       
        
        #判定输出的维度是否和输入相一致
        #shortcut 是一个序列。如果输入和输出的维度不匹配(比如由于步长不为1或通道数不同),则shortcut会包含一个1x1的卷积来确保维度匹配。
        #BasicBlock.expansion 是一个类属性,它表示基础块(BasicBlock)的输出通道数与输入通道数之间的比例
        if stride != 1 or in_channels != BasicBlock.expansion * out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels * BasicBlock.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * BasicBlock.expansion)
            )

    def forward(self, x):#将residual_function(x) 残差和self.shortcut(x) 捷径连接 两者相加,并通过ReLU激活函数得到输出。
        return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x))

class ResNet(nn.Module):
    #block 是用于构建网络的块类型(这里是BasicBlock)。
    #num_block 是一个列表,指定了每个阶段(conv2_x, conv3_x, conv4_x, conv5_x)中block的数量。
    #num_classes 是分类任务的类别数。
    def __init__(self, block, num_block, num_classes=100):
        super().__init__()
        self.in_channels = 64
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False),#卷积层,输入通道数为3(对应于RGB图像),输出通道数为64,卷积核大小为3x3,使用padding=1以保持空间维度不变,不使用偏置(bias=False)。
            nn.BatchNorm2d(64),#批量归一化层,用于加速训练并增加模型稳定性。
            nn.ReLU(inplace=True)#原地(in-place)直接修改输入张量,不会占用额外的内存空间来存储输出。
        )

        #3、定义ResNet的四个主要阶段:
        #self.conv2_x、self.conv3_x、self.conv4_x、self.conv5_x:每个阶段都是由一系列的block(这里指的是BasicBlock)组成。
        #每个阶段的空间维度(宽度和高度)可能会通过stride来减小。

        #block:要使用的残差块类型(例如 BasicBlock 或 Bottleneck)。
        #64:该层中每个残差块的输出通道数。
        #num_block[0]:该层中要创建的残差块的数量。
        #1:第一个残差块中卷积层的步长。
        self.conv2_x = self._make_layer(block, 64, num_block[0], 1)
        self.conv3_x = self._make_layer(block, 128, num_block[1], 2)
        self.conv4_x = self._make_layer(block, 256, num_block[2], 2)
        self.conv5_x = self._make_layer(block, 512, num_block[3], 2)
        
        #4、自适应平均池化层,用于将特征图转化为1*1的特征向量。
        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))

        #5、全连接层,用于将提取的特征映射到输出类别上
        #全连接层,输入特征数为512(最后一个阶段的输出通道数)乘以block.expansion(对于BasicBlock,这个值为1),输出特征数为分类任务的类别数num_classes。
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    #用于创建一系列block(这里是BasicBlock)。它接收输出通道数、block的数量以及stride作为参数。
    def _make_layer(self, block, out_channels, num_blocks, stride):

        strides = [stride] + [1] * (num_blocks - 1)
        layers = []
        for stride in strides:
            layers.append(block(self.in_channels, out_channels, stride))
            self.in_channels = out_channels * block.expansion

        return nn.Sequential(*layers)

    def forward(self, x):
        output = self.conv1(x)
        output = self.conv2_x(output)
        output = self.conv3_x(output)
        output = self.conv4_x(output)
        output = self.conv5_x(output)
        output = self.avg_pool(output)#x通过自适应平均池化层avg_pool,将空间维度减小到1x1。
        output = output.view(output.size(0), -1)#使用view方法将x展平为一维张量,以便它可以作为全连接层fc的输入。
        output = self.fc(output)#x通过全连接层fc,输出分类结果。

        return output

#18层的resnet
def resnet18():
    return ResNet(BasicBlock, [2, 2, 2, 2])

#34层的resnet
def resnet34():
    return ResNet(BasicBlock, [3, 4, 6, 3])

if __name__ == '__main__':
    #创建一个随机的图像张量 image,这表示有 5 张图像,每张图像有 3 个颜色通道(RGB),且每张图像的高度和宽度都是 32 像素。
    image = torch.randn(size=(5,3,32,32))
    #例化一个 18 层的 ResNet 模型 resnet(通过调用 ResNet(BasicBlock, [2, 2, 2, 2]))。
    resnet = ResNet(BasicBlock, [2, 2, 2, 2])

    img_out = resnet(image)
    print(img_out.shape)#torch.Size([5, 100])

7.2 实战ResNet

跟之前的训练代码一样,数据集下载地址:https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz

import torch
import resnet
import get_data
import numpy as np

train_dataset, label_dataset, test_dataset, test_label_dataset = get_data.get_CIFAR10_dataset(root="../dataset/cifar-10-batches-py/")

train_dataset = np.reshape(train_dataset,[len(train_dataset),3,32,32]).astype(np.float32)/255.
test_dataset = np.reshape(test_dataset,[len(test_dataset),3,32,32]).astype(np.float32)/255.
label_dataset = np.array(label_dataset)
test_label_dataset = np.array(test_label_dataset)

device = "cuda" if torch.cuda.is_available() else "cpu"
model = resnet.resnet18()                  #导入Unet模型
model = model.to(device)                #将计算模型传入GPU硬件等待计算
#model = torch.compile(model)            #Pytorch2.0的特性,加速计算速度
optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)   #设定优化函数
loss_fn = torch.nn.CrossEntropyLoss()

batch_size = 128
train_num = len(label_dataset)//batch_size
for epoch in range(63):

    train_loss = 0.
    for i in range(train_num):
        start = i * batch_size
        end = (i + 1) * batch_size

        x_batch = torch.from_numpy(train_dataset[start:end]).to(device)
        y_batch = torch.from_numpy(label_dataset[start:end]).to(device)

        pred = model(x_batch)
        loss = loss_fn(pred, y_batch.long())

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item()  # 记录每个批次的损失值

    # 计算并打印损失值
    train_loss /= train_num
    accuracy = (pred.argmax(1) == y_batch).type(torch.float32).sum().item() / batch_size

    test_num = 512 #2048
    x_test = torch.from_numpy(test_dataset[:test_num]).to(device)
    y_test = torch.from_numpy(test_label_dataset[:test_num]).to(device)
    pred = model(x_test)
    test_accuracy = (pred.argmax(1) == y_test).type(torch.float32).sum().item() / test_num
    print("epoch:",epoch,"train_loss:", round(train_loss,2),";accuracy:",round(accuracy,2),";test_accuracy:",round(test_accuracy,2))

结果:
请添加图片描述

上一篇:Linux命令学习—linux 的常用命令


下一篇:机器学习和深度学习--李宏毅 (笔记与个人理解)Day7