经典网络模型ResNet(残差网络)


代码将在图像分类的数据集进行分析。

1 ResNet 图解分析(论文)

1.1 论文中的模型图、解释

1.1.1 残差结构块

————————图示
在这里插入图片描述
—————————解释

从上述图中可以看出:x为残差块的输入,然后复制成两部分,一部分输入到层(weight layer)之中,进行层间的运算(相当于将x输入到一个函数中做映射),结果为f(x);另一部分作为分支结构,输出还是原本的x,最后将分别两部分的输出进行叠加:f(x) + x,再通过激活函数。这便是整个残差块的基本结构。

1.1.2 残差结构模型——34层

————————图示
在这里插入图片描述
————————解释

对于批标准化(batch-normalization)以及激活函数(activation function)在这里先不做分析,在之后的代码中会有分析。

开头部分首先进行了一次卷积核为7×7,步长为2×2的卷积操作,然后进行了一次最大池化操作。

中间部分,resnet34分成了四块部分,每部分分别为3个残差块、4个残差块、6个残差块、3个残差块,逐一分析:
·
第一部分都是(卷积核大小3×3,卷积核个数64)卷积操作。
第二部分都是(卷积核大小3×3,卷积核个数128)卷积操作。
第三部分都是(卷积核大小3×3,卷积核个数256)卷积操作。
第四部分都是(卷积核大小3×3,卷积核个数512)卷积操作。
·
对于每一部分的第一个残差块的第一次卷积操作,它的步长为2,其余的都是1.
·
对于每一部分的第一个残差块的输入 (即每一条虚线部分) 来说,由于上一部分的通道数与本部分的通道数不一致,所以在其中隐含了利用1×1的卷积操作,形成downsample,增加通道数(详情看代码分析部分),使到本部分的输入的通道数与本部分通道数保持一致,这样才可以进行相同通道上的像素叠加。

最后部分,进行了平均池化,然后经过拉直层,最后进行一个全连接,输出分类概率。

1.1.3 残差结构模型——多种类型

————————图示
在这里插入图片描述

在18,34层的ResNet 网络中他们在每一大部分内部的通道数都不会发生变化,然后在50和101,152层中的通道数都会发生变化,在代码中的expansion这个参数设定为1还是4,就是因为在大部分中通道数是否发生变化。


2 ResNet-34 代码分析

2.1 模型代码分析

pytoch官方也给出了ResNet的代码,查询方法:

import torchvision.models.resnet
### 鼠标悬浮resnet字符上,然后按住ctrl + 鼠标左键 即可跳转至官方ResNet的代码

2.1.1 (BasicBlock)ResNet-34基本块

class BasicBlock(nn.Module):  ## ()内为继承nn的模型
    expansion = 1    ### 这个参数在resnet34层中并没有什么用处  这个参数是为了控制在一大部分中的通道数变化的

    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        """
        @param in_channel:  此块输入的通道数
        @param out_channel: 输出的通道数
        @param stride: 在第一个卷积层的步长
        @param downsample: 是否进行下采样
        @param kwargs: 其他参数(可变长参数)
        """
        ### 父类初始化
        super(BasicBlock, self).__init__()
        ### 自定义操作赋值给变量
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, kernel_size=3, stride=stride, padding=1, bias=False) #定义卷积层
        self.bn1 = nn.BatchNorm2d(out_channel) #定义归一化
        self.relu = nn.ReLU()# 定义激活函数
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=1, padding=1, bias=False) # 定义卷积层
        self.bn2 = nn.BatchNorm2d(out_channel)# 定义归一化

        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=1, padding=1, bias=False) # 定义卷积层
        self.bn3 = nn.BatchNorm2d(out_channel)# 定义归一化

        self.downsample = downsample   ##定义下采样部分

    def forward(self, x):
    	### 在这里就是构造残差块的基本结构
        identity = x ## 先将最开始的输入 进行赋值到identity  这一部分是为了进行恒等映射
        if self.downsample is not None:  
        # 如果downsample 不是空值(下采样)的话  
        #就在后方进行下采样层相应的操作,因为在上述分析模块部分已经说到,
        #在虚线部分,会因为通道数不一致,要进行下采样操作,使得通道数一致。
            identity = self.downsample(x)
        ### 两部分 卷积批标准化 卷积批标准化
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)  ## 过一层激活层
        
        out = self.conv2(out)
        out = self.bn2(out)

        out = out + identity   ## 将此基本结构块的输入与本结构块的最后一层的输出进行叠加,
        #形成最终的输出

        out = self.relu(out) # 过激活函数

        return out  ## 返回此结构块的输出 也就是下一个残差块的基本结构的输入了

2.2.2 (Bottleneck)ResNet-更多层基本块

如果只看resnet-34可以不看这部分。

class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    """

    expansion = 4  ## 这个就是通道数变化的系数 

    def __init__(self, in_channel, out_channel, stride=1, downsample=None, groups=1, width_per_group=64):
        super(Bottleneck, self).__init__()
		## resnext50_32x4d 和resnext101_32x8d 会使用
        width = int(out_channel * (width_per_group / 64.)) * groups
        
		### 定义三个卷积过程
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width, kernel_size=1, stride=1, bias=False)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups, kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion, kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)
        ##下采样
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)
            
		## 基本块为三次卷积层的过程 并非跟resnet34一致
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity
        out = self.relu(out)

        return out ## 返回输出

2.2.3 (ResNet)网络总模块

class ResNet(nn.Module):  ##继承自nn.Module函数

    def __init__(self, block, blocks_num, num_classes=1000, include_top=True, groups=1, width_per_group=64):
        """
        @param block:  传入实例化BasicBlock,就是上一部分代码的基本块
        @param blocks_num:  块的个数此为一个列表 列表长度为 resNet几大块  
        对应列表中的每一个数——> 即为每一部分的基本块的块数,
        例如resnet34 中的blocks_num = [3, 4, 6, 3]   可以看上述图中的resnet34的结构 本人画出的四大块
        
        @param num_classes: 几分类
        @param include_top: 判定条件是否采用适应性平均池化
        @param groups:
        @param width_per_group:
        """
        super(ResNet, self).__init__()	
        ## 进行赋值
        self.include_top = include_top
        self.in_channel = 64   ### 输入的通道数

        self.groups = groups
        self.width_per_group = width_per_group
        ### 最开始的一大层     先进行卷积核为7*7 卷积操作
        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2, padding=3, bias=False)
		 ## 然后进行归一化
        self.bn1 = nn.BatchNorm2d(self.in_channel)
         ## 然后过激活函数  增加非线性表达
        self.relu = nn.ReLU(inplace=True)
         ## 然后经过最大池化层
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        #### 进行ResNet的四大部分[3, 4, 6, 3]
        # 构造每一部分函数:_make_layer :为本类的成员函数  在下方
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
        
        ### 一个判定条件  为True则是会有自适应平均池化
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # output size = (1, 1)   ### 自适应平均池化  自适应去需要进行均值的数值

            self.fc = nn.Linear(512 * block.expansion, num_classes)   ## 全连接层

        ### 遍历每一个模块进行模块的权重的初始化
        for m in self.modules():## 遍历所有的层
            if isinstance(m, nn.Conv2d):   ## m 是否为卷积的实例化
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
				### 以何凯明大佬命名的初始化的函数

    ### 此函数为构造每一大部分的函数
    def _make_layer(self, block, channel, block_num, stride=1):
        """
        @param block: 基本块   resnet34层传入的是BasicBlock,更多层数的会传入瓶颈块 Bottleneck
        @param channel: 每一大部分的通道数
        @param block_num: 此部分的基本块的数量
        @param stride:  步长
        @return:
        """
        downsample = None
        # 步长不为1   因为需要downsample进行对于不同残差块之间的统一   上一个残差块的输出与本残差块的输出宽高保持一致
        ### 如果步长不为1 也就是每个大部分的第一个基本块的第一个卷积层,
        ### 或者 输入通道与通道不匹配,也就是resnet 更多层会出现 一大部分中通道数发生变化
        ### 会出现通道数不匹配 所以要进行下采样,统一通道数的
        if stride != 1 or self.in_channel != channel * block.expansion:
            ###block.expansion = 1
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),  ## 进行卷积保持宽高一致
                nn.BatchNorm2d(channel * block.expansion)  ## 批归一化
            )
        #定义一个层列表
        layers = []
        ## 添加基本块  先添加一个基本块  之后的用循环,因为第一个基本块,可能会出现下采样的情况
        layers.append(
            block(self.in_channel, channel, downsample=downsample, stride=stride, groups=self.groups, width_per_group=self.width_per_group)
        )
        # 注意注意    只有在不同大部分的情况下才进行此操作  因为上一大部分跟下一大部分的channel通道不一样
        self.in_channel = channel * block.expansion    ## 下一层的输入等于本层的输出
        ### 进行循环 通过小的基本块 构造一个大部分
        for _ in range(1, block_num):
            layers.append(
                block(self.in_channel, channel, groups=self.groups, width_per_group=self.width_per_group)
            )
		## 返回本大部分
        return nn.Sequential(*layers)

    def forward(self, x):
        #开始一部分
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        ## 中间四大部分
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        if self.include_top:## 判定
            x = self.avgpool(x)   ### 自适应平均池化
            x = torch.flatten(x, 1)  ## 拉直层
            x = self.fc(x) # 全连接层

        return x  ##返回输出

2.2.4 网络多种架构模块

## 对于不同层的resnet网络 ,所传入的参数设定
def resnet34(num_classes=1000, include_top=True):
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet50(num_classes=1000, include_top=True):
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)

def resnet101(num_classes=1000, include_top=True):
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)

def resnext50_32x4d(num_classes=1000, include_top=True):
    groups = 32
    width_per_group = 4
    return ResNet(Bottleneck, [3, 4, 6, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)

def resnext101_32x8d(num_classes=1000, include_top=True):
    groups = 32
    width_per_group = 8
    return ResNet(Bottleneck, [3, 4, 23, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)

2.2 训练代码分析

import os
import json
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets
from tqdm import tqdm
from model import resnet34
import random
import numpy as np
import pandas as pd

save_path = "./resnet34.pth"   ## 模型保存路径
epochs = 30    #训练轮数

### 本部分是为了进行 实验复现
seed = 3
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False


def main():

    ### 查看可用gpu 设备
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("using {} device.".format(device))

    # 定义数据转换格式   Image->tensor
    data_transform = {
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
        "val": transforms.Compose([transforms.Resize(256),
                                   transforms.CenterCrop(224),
                                   transforms.ToTensor(),
                                   transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
    }
    # 寻找数据集文件夹  将路径进行拼接
    data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
    image_path = os.path.join(data_root, "data_set", "flower_data")  # flower data set path
    assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
    ## 读取文件夹中的图片文件
    train_dataset = datasets.ImageFolder(
        root=os.path.join(image_path, "train"),
        transform=data_transform["train"]
    )

    train_num = len(train_dataset)    #3306 张图片

    # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
    flower_list = train_dataset.class_to_idx   ##定义列表 类与所对应的数字分类的字典
    # 将key value 进行调换顺序
    cla_dict = dict((val, key) for key, val in flower_list.items())

    # write dict into json file   将字典写入json文件
    json_str = json.dumps(cla_dict, indent=4)
    with open('class_indices.json', 'w') as json_file:
        json_file.write(json_str)

    batch_size = 128
    #### 确定数据加载器的进程个数
    nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
    print('Using {} dataloader workers every process'.format(nw))

    ## 训练的加载器
    train_loader = torch.utils.data.DataLoader(
       train_dataset,
       batch_size=batch_size,
       shuffle=True,
       num_workers=nw
    )
    ## 读取测试集文件夹
    validate_dataset = datasets.ImageFolder(
        root=os.path.join(image_path, "val"),
        transform=data_transform["val"]
    )

    val_num = len(validate_dataset)
    ## 测试集数据加载器
    validate_loader = torch.utils.data.DataLoader(
        validate_dataset,
        batch_size=batch_size, shuffle=False,
        num_workers=nw
    )
    ## 输出用于训练以及测试的数据集数据的个数
    print("using {} images for training, {} images for validation.".format(train_num, val_num))


    # 实例化网络模型
    net = resnet34()
    # load pretrain weights
    # change fc layer structure
    ##### 重写网络中的全连接层部分  将输出改写为5
    ## 其实在最开始num_classes 赋值为5就可以
    in_channel = net.fc.in_features
    net.fc = nn.Linear(in_channel, 5)
    ### 如果有模型的话就进行加载模型
    if os.path.exists(save_path):
        print("---Loading_Model---")
        net.load_state_dict(torch.load(save_path, map_location=device))
    net.to(device)

    # define loss function
    loss_function = nn.CrossEntropyLoss()

    # construct an optimizer
    params = [p for p in net.parameters() if p.requires_grad]
    optimizer = optim.Adam(params, lr=0.0001)

    best_acc = 0.0   ### 记录最好的准确度

    train_steps = len(train_loader)   ###  将数据分为多少批次    3306/batch_size
    print("train_steps", train_steps)
    rows = []
    ones = []
    for epoch in range(epochs):
        # train
        net.train()
        running_loss = 0.0   ## 初始化在一个epoch中的损失值
        train_bar = tqdm(train_loader)   ## 进度条库
        for step, data in enumerate(train_bar):   ## 用数据加载器读取数据
            images, labels = data   ## 特征目标值
            optimizer.zero_grad()   ## 梯度归零
            logits = net(images.to(device))    ##调用网络  将数据喂入到网络中
            loss = loss_function(logits, labels.to(device))    ## 求损失
            loss.backward()  ### 反向传播
            optimizer.step()  ## 损失优化
            # print statistics
            running_loss += loss.item()   ## 累加损失
            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1, epochs, loss)

        # validate
        net.eval()
        ### 测试部分
        acc = 0.0  # accumulate accurate number / epoch    ## 定义准确度 每一轮训练要归零
        with torch.no_grad():   ## 不产生梯度
            val_bar = tqdm(validate_loader)
            for val_data in val_bar:
                val_images, val_labels = val_data
                outputs = net(val_images.to(device))
                # loss = loss_function(outputs, test_labels)
                predict_y = torch.max(outputs, dim=1)[1]
                acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
                val_bar.desc = "valid epoch[{}/{}]".format(epoch + 1,epochs)
        val_accurate = acc / val_num

        ones.append(running_loss / train_steps)
        ones.append(val_accurate)
        rows.append(ones)
        ones=[]

        print('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %(epoch + 1, running_loss / train_steps, val_accurate))
        # 这一轮训练的比之前最好的准确度还高的话就保存
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)

    print('Finished Training')
    print(rows)

if __name__ == '__main__':
    main()


2.3 预测代码分析

## 预测代码类似训练代码 调用模型就可以了,然后传入自己的图片进行分类
import os
import json

import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
from model import resnet34
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'

def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    data_transform = transforms.Compose([
         transforms.Resize(256),
         transforms.CenterCrop(224),
         transforms.ToTensor(),
         transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    # load image
    img_path = "../tulip.jpg"
    assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
    img = Image.open(img_path)
    plt.imshow(img)
    # [N, C, H, W]
    img = data_transform(img)
    # expand batch dimension
    img = torch.unsqueeze(img, dim=0)

    # read class_indict
    json_path = './class_indices.json'
    assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)

    json_file = open(json_path, "r")
    class_indict = json.load(json_file)

    # create model
    model = resnet34(num_classes=5).to(device)

    # load model weights
    weights_path = "./resNet34.pth"
    assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
    model.load_state_dict(torch.load(weights_path, map_location=device))

    # prediction
    model.eval()
    with torch.no_grad():
        # predict class
        output = torch.squeeze(model(img.to(device))).cpu()
        predict = torch.softmax(output, dim=0)
        predict_cla = torch.argmax(predict).numpy()

    print_res = "class: {}   prob: {:.3}".format(class_indict[str(predict_cla)], predict[predict_cla].numpy())
    plt.title(print_res)
    print(print_res)
    plt.show()


if __name__ == '__main__':
    main()


  • 15
    点赞
  • 127
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ResNet(Residual Network)是由Microsoft Research团队提出的一种深度残差网络结构,通过引入残差连接(residual connection)来解决深度神经网络中梯度消失和梯度爆炸的问题,提高了网络的训练效率和性能。 基于ResNet残差神经网络模型训练的步骤如下: 1. 数据预处理:对训练数据进行数据增强处理,包括图像随机裁剪、水平/垂直翻转、随机旋转等,以增加数据的多样性和丰富性。 2. 构建ResNet模型:按照ResNet网络结构,搭建深度残差网络模型。可以使用现有的深度学习框架(如TensorFlow、PyTorch等)来实现模型构建。 3. 设置超参数:包括学习率、批量大小、训练轮数等。可以通过交叉验证等方法来调整超参数。 4. 训练模型:使用训练数据对模型进行训练。在训练过程中,可以使用优化算法(如SGD、Adam等)来更新模型权重,以最小化损失函数。 5. 模型评估:使用测试数据对训练好的模型进行评估,计算模型的准确率、精度、召回率等指标,以评估模型性能。 6. 模型优化:根据评估结果,对模型进行优化,可以调整网络结构、重新设置超参数等。 7. 模型应用:将训练好的模型应用到新的数据上进行预测,可以用于图像分类、目标检测、人脸识别等领域。 总的来说,基于ResNet残差神经网络模型的训练过程需要经过数据预处理、模型构建、超参数设置、模型训练、模型评估、模型优化和模型应用等步骤,需要结合实际问题进行具体设计和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值