基于Paddle手敲ResNet

基于Paddle手敲ResNet

放在开头的话

作为一名算法工程师,接触各类模型是必须的,但将模型从论文转换成代码的能力往往比较欠缺;

习惯了搬运开源代码,也被冠以了"炼丹师"的称号,但调参能力并不是算法工程师能力的唯一衡量指标,对模型结构的理解、通过结构图复现代码的能力往往能考验对于各类模型的理解度;

从工作中来看,对于不同的任务,套用相同模型的效果是不一样的,这也就说明哪怕是开源的达到了SOTA的模型,也不适合所有任务;当遇到新的任务时,我们需要思考如何魔改模型结构,在当前任务下达到更优的效果,这是作为算法工程师需要具备的能力;

参考资源

ResNet论文地址:https://arxiv.org/pdf/1512.03385.pdf

主要关注下面两张图:

1、模型结构图

在这里插入图片描述

2、ResNet最关键结构(跳连结构)

在这里插入图片描述

本次实现都是基于Paddle实现的,Paddle的环境对于新手来说十分友好,也提供了代码环境;

Paddle课程网址:https://aistudio.baidu.com/aistudio/course

个人观点来说,Paddle的课程做的都挺不错的,并且提供了配套的环境来实践,虽然框架是基于Paddle,但原理和Pytorch等框架是一致的;

本次参考的课程为实战进阶中的Vision Transformer,其中的第一章就讲解了ResNet的实现过程,在作业部分也有参考代码,大家可以尝试一下;

在这里插入图片描述

网络层实现

本次训练的数据集为cifar10数据集,所以需要修改头两层结构,具体结构图实现如下:

在这里插入图片描述

主要关注18层的模型结构,并且将头两层修改为3x3的卷积层;

1、ResNet18主结构:

import paddle				# 导入需要用到的Paddle的库 
import paddle.nn as nn

class ResNet18(nn.Layer):
	# num_classes可通过传入参数修改,表示类别
    def __init__(self, in_dim=64, num_classes=1000):
        super().__init__()
        self.in_dim = in_dim
        # 实现头两层结构,3x3卷积层接一个BatchNorm层
        self.conv1 = nn.Conv2D(in_channels=3,
                               out_channels=in_dim,
                               kernel_size=3,
                               stride=1,
                               padding=1,
                               bias_attr=False)
        self.bn1 = nn.BatchNorm2D(in_dim)
        self.relu = nn.ReLU()
        
        # blocks结构(将实现封装在_make_layer这个函数中)
        self.layer1 = self._make_layer(dim=64, n_block=2, stride=1)
        self.layer2 = self._make_layer(dim=128, n_block=2, stride=2)
        self.layer3 = self._make_layer(dim=256, n_block=2, stride=2)
        self.layer4 = self._make_layer(dim=512, n_block=2, stride=2)
        
        # head layer(进行分类的结构,用全连接输出对应类别的特征)
        self.avgpool = nn.AdaptiveAvgPool2D(1)
        self.classifier = nn.Linear(512, num_classes)
	
    """
    _make_layer主要是实现blocks结构,通过图可以看出blocks的结构只是传入的参数不同,后续更深层	ResNet的实现也是块的叠加而已;
    """
    def _make_layer(self, dim, n_block, stride):
        layer_list = []
        # Block结构另外实现
        layer_list.append(Block(self.in_dim, dim, stride))
        self.in_dim = dim
        for i in range(1, n_block):
            layer_list.append(Block(self.in_dim, dim, stride))
        # 注意Sequential这个接口对于多层来说很适用
        return nn.Sequential(*layer_list)
	
    # forward表示定义网络的顺序
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.avgpool(x)
        # 将第一个维度的矩阵推平,整体变为二维矩阵
        x = x.flatten(1)
        x = self.classifier(x)

        return x

2、Block结构的实现

# 返回自身的值
class Identity(nn.Layer):
    def __init_(self):
        super().__init__()

    def forward(self, x):
        return x

class Block(nn.Layer):
    def __init__(self, in_dim, out_dim, stride):
        super().__init__()
        self.conv1 = nn.Conv2D(in_dim, out_dim, 3, stride=stride, padding=1, bias_attr=False)
        self.bn1 = nn.BatchNorm2D(out_dim)
        self.conv2 = nn.Conv2D(out_dim, out_dim, 3, stride=1, padding=1, bias_attr=False)
        self.bn2 = nn.BatchNorm2D(out_dim)
        self.relu = nn.ReLU()
		
	    # 这里是关键结构,也就是上图中的跳连结构
        # 这里需要判断相加的值的维度是否相等,不等的话需要做同样的卷积操作
        if stride == 2 or in_dim != out_dim:
            self.downsample = nn.Sequential(*[
                nn.Conv2D(in_dim, out_dim, 1, stride=stride),
                nn.BatchNorm2D(out_dim)])
        else:
            # Identity表示返回自身的值,在这用单独一个类定义
            self.downsample = Identity()

    def forward(self, x):
        h = x
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.conv2(x)
        x = self.bn2(x)
        identity = self.downsample(h)
        # 跳连结构本质就是两个矩阵的值相加
        x = x + identity
        x = self.relu(x)
        return x

数据层实现

本次训练采用简单的cifar10数据集,并且Paddle提供的API可下载该数据集;

# 导入对应的库
from paddle.io import Dataset
from paddle.io import DataLoader
from paddle.vision import datasets
from paddle.vision import transforms

# 这里是对每张图像做一个前处理的操作
def get_transforms(mode='train'):
    if mode == 'train':
    	# 下面为一些前处理的操作
        data_transforms = transforms.Compose([
            transforms.RandomCrop(32, padding=4),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            # 标准化的参考值是由数据集提供的
            transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010])])
    else:
    	# 如果不是训练集,则不需要经过裁剪翻转等处理
        data_transforms = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010])])
    return data_transforms

# 加载数据,用内置的datasets提供有Cifar10数据集的下载和加载
def get_dataset(name='cifar10', mode='train'):
    if name == 'cifar10':
        dataset = datasets.Cifar10(mode=mode, transform=get_transforms(mode))
    return dataset

# 将数据集保存为DataLoader,可以理解成存在一个数组中,这个后续会做详细讲解
def get_dataloader(dataset, batch_size=128, mode='train'):
    dataloader = DataLoader(dataset, batch_size=batch_size, num_workers=2, shuffle=(mode == 'train'))
    return dataloader

主程序实现

首先定义训练一个epoch的函数:

"""
这里需要传入多个参数:
criterion: 损失函数
optimizer: 优化器
report_freq: 间隔多少个batch打印一次信息
"""
def train_one_epoch(model, dataloader, criterion, optimizer, epoch, total_epoch, report_freq=20):
    print(f'----- Training Epoch [{epoch}/{total_epoch}]:')
    # AverageMeter是定义的一个工具类,可以理解成存放信息的数据结构
    loss_meter = AverageMeter()
    acc_meter = AverageMeter()
    # 模型在训练模式,dropout层会执行
    model.train()
    # 取出dataloader中的数据
    for batch_idx, data in enumerate(dataloader):
        image = data[0]
        label = data[1]

        out = model(image)
        # 计算损失值
        loss = criterion(out, label)
	   # 反向传播
        loss.backward()
        optimizer.step()
        optimizer.clear_grad()
	   # 计算最终的结果 
        pred = nn.functional.softmax(out, axis=1)
        # 这里用paddle自带的函数计算准确率,就不用我们自己写函数了
        acc1 = paddle.metric.accuracy(pred, label.unsqueeze(-1))

        batch_size = image.shape[0]
        # 更新损失值和准确率的值
        loss_meter.update(loss.cpu().numpy()[0], batch_size)
        acc_meter.update(acc1.cpu().numpy()[0], batch_size)
	   # 间隔20个batch打印一次信息, f可以在其中传入变量
        if batch_idx > 0 and batch_idx % report_freq == 0:
            print(f'----- Batch[{batch_idx}/{len(dataloader)}], Loss: {loss_meter.avg:.5}, Acc@1: {acc_meter.avg:.4}')

    print(f'----- Epoch[{epoch}/{total_epoch}], Loss: {loss_meter.avg:.5}, Acc@1: {acc_meter.avg:.4}')

验证函数的实现:

当我们训练的同时,也需要同步对验证集做验证,便于在训练过程中发现问题;

# 验证集函数实现,
def validate(model, dataloader, criterion, report_freq=10):
    print('----- Validation')
    loss_meter = AverageMeter()
    acc_meter = AverageMeter()
    # 模型切换到推理模式
    model.eval()
    for batch_idx, data in enumerate(dataloader):
        image = data[0]
        label = data[1]

        out = model(image)
        loss = criterion(out, label)

        pred = paddle.nn.functional.softmax(out, axis=1)
        acc1 = paddle.metric.accuracy(pred, label.unsqueeze(-1))
        batch_size = image.shape[0]
        loss_meter.update(loss.cpu().numpy()[0], batch_size)
        acc_meter.update(acc1.cpu().numpy()[0], batch_size)
	   # 间隔10个batch打印一次loss信息 
        if batch_idx > 0 and batch_idx % report_freq == 0:
            print(f'----- Batch [{batch_idx}/{len(dataloader)}], Loss: {loss_meter.avg:.5}, Acc@1: {acc_meter.avg:.4}')

    print(f'----- Validation Loss: {loss_meter.avg:.5}, Acc@1: {acc_meter.avg:.4}')

最后就是主函数的实现:

# 主函数实现
def main():
    # 定义epoch和batchsize的值
    total_epoch = 200
    batch_size = 512
    #batch_size = 256
	
    # 引入模型
    model = ResNet18()
    # 导入训练集
    train_dataset = get_dataset(mode='train')
    train_dataloader = get_dataloader(train_dataset, batch_size, mode='train')
    # 导入验证集
    val_dataset = get_dataset(mode='test')
    val_dataloader = get_dataloader(val_dataset, batch_size, mode='test')
    # 损失函数定义
    criterion = nn.CrossEntropyLoss()
    # 梯度下降策略
    scheduler = paddle.optimizer.lr.CosineAnnealingDecay(0.02, total_epoch)
    #scheduler = paddle.optimizer.lr.CosineAnnealingDecay(0.01, total_epoch)
    # 优化器
    optimizer = paddle.optimizer.Momentum(learning_rate=scheduler,
                                          parameters=model.parameters(),
                                          momentum=0.9,
                                          weight_decay=5e-4)
	# 这里可以直接加载训练后的模型,将该标志位改为True即可
    eval_mode = False
    if eval_mode:
        state_dict = paddle.load('./resnet18_ep200.pdparams')
        model.set_state_dict(state_dict)
        validate(model, val_dataloader, criterion)
        return

    save_freq = 50		# 间隔多少个epoch保存模型
    test_freq = 10		# 间隔多少个epoch验证模型
    # 开始迭代训练和验证
    for epoch in range(1, total_epoch+1):
        train_one_epoch(model, train_dataloader, criterion, optimizer, epoch, total_epoch)
        scheduler.step()

        if epoch % test_freq == 0 or epoch == total_epoch:
            validate(model, val_dataloader, criterion)

        if epoch % save_freq == 0 or epoch == total_epoch:
        	# 保存模型的路径
            paddle.save(model.state_dict(), f'./resnet18_ep{epoch}.pdparams')
            paddle.save(optimizer.state_dict(), f'./resnet18_ep{epoch}.pdopts')

执行训练后打印信息如下图所示:

在这里插入图片描述

上图可以看出,随着训练的进行,损失值不断在下降,准确率不断上升,说明训练的方式是正确的;仅仅训练了3个epochs,准确率就到了55%,证明模型对该数据集的效果还是很明显的;

当训练结束后,执行验证结果如下图所示:

在这里插入图片描述

可以看出在验证集上也可以达到90%以上的准确率;

总结

ResNet算是结构比较简单的网络了,像现在最新的一些网络都会加入很多小trick,并且模型结构相对复杂一些;复现网络的能力还需要不断地提升,可以通过看网络的源码,多结合图片理解;实际上网络的很多结构是相通的,掌握了一个结构往往在别的模型中也会用到,就好比残差结构,基本在最新的模型中都会采用该技巧提升模型的深度;

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值