RDNet实战:使用RDNet实现图像分类任务(一)

AI浩 2024-09-06 15:31:01 阅读 58

文章目录

摘要1. 强调并优化连接操作(Concatenation)2. 扩大中间通道维度3. 记忆高效的DenseNet设计

安装包安装timm

数据增强Cutout和MixupEMAEMA概述工作原理应用优势使用场景

项目结构计算mean和std均值(Mean)标准差(Standard Deviation, Std)归一化(Normalization)为什么需要归一化?如何计算和使用mean和std

生成数据集

摘要

论文提出的模型主要基于对传统DenseNet架构的改进和复兴,通过一系列创新设计,旨在提升模型性能并优化其计算效率,提出了RDNet模型。该模型的主要特点和改进点:

1. 强调并优化连接操作(Concatenation)

论文首先强调了DenseNet中连接操作(Concatenation)的重要性,并通过广泛的实验验证了连接操作在性能上能够超越传统的加法快捷连接(Additive Shortcut)。这一发现促使研究者们重新审视并优化DenseNet的连接机制。

2. 扩大中间通道维度

为了进一步提升模型性能,论文提出通过调整扩展比(Expansion Ratio, ER)来增大中间张量(Tensor)的尺寸,使其超过输入维度。传统方法中,ER主要用于调整输入和输出维度,但在这篇论文中,ER被重新设计为与输入维度成比例,即ER与增长率(Growth Rate, GR)解耦。这种设计使得在非线性处理之前能够更充分地丰富特征,同时为了管理由此产生的计算需求,将GR减半(例如从120减少到60),从而在不影响准确性的前提下控制计算量。

3. 记忆高效的DenseNet设计

为了优化DenseNet的架构设计,论文采用了更加内存高效的设计策略,通过丢弃无效组件并增强架构和块设计,同时保持DenseNet的核心连接机制不变。这种设计使得模型在保持高性能的同时,也减少了内存占用,提升了处理大规模数据集的能力。

在这里插入图片描述

本文使用RDNet模型实现图像分类任务,模型选择rdnet_tiny,在植物幼苗分类任务ACC达到了97%+。

在这里插入图片描述

在这里插入图片描述

通过深入阅读本文,您将能够掌握以下关键技能与知识:

数据增强的多种策略:包括利用PyTorch的<code>transforms库进行基本增强,以及进阶技巧如CutOut、MixUp、CutMix等,这些方法能显著提升模型泛化能力。

RDNet模型的训练实现:了解如何从头开始构建并训练RDNet(或其他深度学习模型),涵盖模型定义、数据加载、训练循环等关键环节。

混合精度训练:学习如何利用PyTorch自带的混合精度训练功能,加速训练过程同时减少内存消耗。

梯度裁剪技术:掌握梯度裁剪的应用,有效防止梯度爆炸问题,确保训练过程的稳定性。

分布式数据并行(DP)训练:了解如何在多GPU环境下使用PyTorch的分布式数据并行功能,加速大规模模型训练。

可视化训练过程:学习如何绘制训练过程中的loss和accuracy曲线,直观监控模型学习状况。

评估与生成报告:掌握在验证集上评估模型性能的方法,并生成详细的评估报告,包括ACC等指标。

测试脚本编写:学会编写测试脚本,对测试集进行预测,评估模型在实际应用中的表现。

学习率调整策略:理解并应用余弦退火策略动态调整学习率,优化训练效果。

自定义统计工具:使用AverageMeter类或其他工具统计和记录训练过程中的ACC、loss等关键指标,便于后续分析。

深入理解ACC1与ACC5:掌握图像分类任务中ACC1(Top-1准确率)和ACC5(Top-5准确率)的含义及其计算方法。

指数移动平均(EMA):学习如何在模型训练中应用EMA技术,进一步提升模型在测试集上的表现。

若您在以上任一领域基础尚浅,感到理解困难,推荐您参考我的专栏“经典主干网络精讲与实战”,该专栏从零开始,循序渐进地讲解上述所有知识点,助您轻松掌握深度学习中的这些核心技能。

安装包

安装timm

使用pip就行,命令:

pip install timm

mixup增强和EMA用到了timm

数据增强Cutout和Mixup

为了提高模型的泛化能力和性能,我在数据预处理阶段加入了Cutout和Mixup这两种数据增强技术。Cutout通过随机遮挡图像的一部分来强制模型学习更鲁棒的特征,而Mixup则通过混合两张图像及其标签来生成新的训练样本,从而增加数据的多样性。实现这两种增强需要安装torchtoolbox。安装命令:

pip install torchtoolbox

Cutout实现,在transforms中。

from torchtoolbox.transform import Cutout

# 数据预处理

transform = transforms.Compose([

transforms.Resize((224, 224)),

Cutout(),

transforms.ToTensor(),

transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])

])

需要导入包:from timm.data.mixup import Mixup,

定义Mixup,和SoftTargetCrossEntropy

mixup_fn = Mixup(

mixup_alpha=0.8, cutmix_alpha=1.0, cutmix_minmax=None,

prob=0.1, switch_prob=0.5, mode='batch',code>

label_smoothing=0.1, num_classes=12)

criterion_train = SoftTargetCrossEntropy()

Mixup 是一种在图像分类任务中常用的数据增强技术,它通过将两张图像以及其对应的标签进行线性组合来生成新的数据和标签。

参数详解:

mixup_alpha (float): mixup alpha 值,如果 > 0,则 mixup 处于活动状态。

cutmix_alpha (float):cutmix alpha 值,如果 > 0,cutmix 处于活动状态。

cutmix_minmax (List[float]):cutmix 最小/最大图像比率,cutmix 处于活动状态,如果不是 None,则使用这个 vs alpha。

如果设置了 cutmix_minmax 则cutmix_alpha 默认为1.0

prob (float): 每批次或元素应用 mixup 或 cutmix 的概率。

switch_prob (float): 当两者都处于活动状态时切换cutmix 和mixup 的概率 。

mode (str): 如何应用 mixup/cutmix 参数(每个’batch’,‘pair’(元素对),‘elem’(元素)。

correct_lam (bool): 当 cutmix bbox 被图像边框剪裁时应用。 lambda 校正

label_smoothing (float):将标签平滑应用于混合目标张量。

num_classes (int): 目标的类数。

EMA

EMA(Exponential Moving Average)在深度学习中是一种用于模型参数优化的技术,它通过计算参数的指数移动平均值来平滑模型的学习过程。这种方法有助于提高模型的稳定性和泛化能力,特别是在训练后期。以下是关于EMA的总结,表达进行了优化:

EMA概述

EMA是一种加权移动平均技术,其中每个新的平均值都是前一个平均值和当前值的加权和。在深度学习中,EMA被用于模型参数的更新,以减缓参数在训练过程中的快速波动,从而得到更加平滑和稳定的模型表现。

工作原理

在训练过程中,除了维护当前模型的参数外,还额外保存一份EMA参数。每个训练步骤或每隔一定步骤,根据当前模型参数和EMA参数,按照指数衰减的方式更新EMA参数。具体来说,EMA参数的更新公式通常如下:

EMA

new

=

decay

×

EMA

old

+

(

1

decay

)

×

model_parameters

\text{EMA}_{\text{new}} = \text{decay} \times \text{EMA}_{\text{old}} + (1 - \text{decay}) \times \text{model\_parameters}

EMAnew​=decay×EMAold​+(1−decay)×model_parameters

其中,decay是一个介于0和1之间的超参数,控制着旧EMA值和新模型参数值之间的权重分配。较大的decay值意味着EMA更新时更多地依赖于旧值,即平滑效果更强。

应用优势

稳定性:EMA通过平滑参数更新过程,减少了模型在训练过程中的波动,使得模型更加稳定。泛化能力:由于EMA参数是历史参数的平滑版本,它往往能捕捉到模型训练过程中的全局趋势,因此在测试或评估时,使用EMA参数往往能获得更好的泛化性能。快速收敛:虽然EMA本身不直接加速训练过程,但通过稳定模型参数,它可能间接地帮助模型更快地收敛到更优的解。

使用场景

EMA在深度学习中的使用场景广泛,特别是在需要高度稳定性和良好泛化能力的任务中,如图像分类、目标检测等。在训练大型模型时,EMA尤其有用,因为它可以帮助减少过拟合的风险,并提高模型在未见数据上的表现。

具体实现如下:

import logging

from collections import OrderedDict

from copy import deepcopy

import torch

import torch.nn as nn

_logger = logging.getLogger(__name__)

class ModelEma:

def __init__(self, model, decay=0.9999, device='', resume=''):code>

# make a copy of the model for accumulating moving average of weights

self.ema = deepcopy(model)

self.ema.eval()

self.decay = decay

self.device = device # perform ema on different device from model if set

if device:

self.ema.to(device=device)

self.ema_has_module = hasattr(self.ema, 'module')

if resume:

self._load_checkpoint(resume)

for p in self.ema.parameters():

p.requires_grad_(False)

def _load_checkpoint(self, checkpoint_path):

checkpoint = torch.load(checkpoint_path, map_location='cpu')code>

assert isinstance(checkpoint, dict)

if 'state_dict_ema' in checkpoint:

new_state_dict = OrderedDict()

for k, v in checkpoint['state_dict_ema'].items():

# ema model may have been wrapped by DataParallel, and need module prefix

if self.ema_has_module:

name = 'module.' + k if not k.startswith('module') else k

else:

name = k

new_state_dict[name] = v

self.ema.load_state_dict(new_state_dict)

_logger.info("Loaded state_dict_ema")

else:

_logger.warning("Failed to find state_dict_ema, starting from loaded model weights")

def update(self, model):

# correct a mismatch in state dict keys

needs_module = hasattr(model, 'module') and not self.ema_has_module

with torch.no_grad():

msd = model.state_dict()

for k, ema_v in self.ema.state_dict().items():

if needs_module:

k = 'module.' + k

model_v = msd[k].detach()

if self.device:

model_v = model_v.to(device=self.device)

ema_v.copy_(ema_v * self.decay + (1. - self.decay) * model_v)

加入到模型中。

#初始化

if use_ema:

model_ema = ModelEma(

model_ft,

decay=model_ema_decay,

device='cpu',code>

resume=resume)

# 训练过程中,更新完参数后,同步update shadow weights

def train():

optimizer.step()

if model_ema is not None:

model_ema.update(model)

# 将model_ema传入验证函数中

val(model_ema.ema, DEVICE, test_loader)

针对没有预训练的模型,容易出现EMA不上分的情况,这点大家要注意啊!

项目结构

RDNet_Demo

├─data1

│ ├─Black-grass

│ ├─Charlock

│ ├─Cleavers

│ ├─Common Chickweed

│ ├─Common wheat

│ ├─Fat Hen

│ ├─Loose Silky-bent

│ ├─Maize

│ ├─Scentless Mayweed

│ ├─Shepherds Purse

│ ├─Small-flowered Cranesbill

│ └─Sugar beet

├─models

│ └─rdnet.py

├─mean_std.py

├─makedata.py

├─train.py

└─test.py

mean_std.py:计算mean和std的值。

makedata.py:生成数据集。

train.py:训练RDNet模型

models:来源官方代码。

计算mean和std

在深度学习中,特别是在处理图像数据时,计算数据的均值(mean)和标准差(standard deviation, std)并进行归一化(Normalization)是加速模型收敛、提高模型性能的关键步骤之一。这里我将详细解释这两个概念,并讨论它们如何帮助模型学习。

均值(Mean)

均值是所有数值加和后除以数值的个数得到的平均值。在图像处理中,我们通常对每个颜色通道(如RGB图像的三个通道)分别计算均值。这意味着,如果我们的数据集包含多张图像,我们会计算所有图像在R通道上的像素值的均值,同样地,我们也会计算G通道和B通道的均值。

标准差(Standard Deviation, Std)

标准差是衡量数据分布离散程度的统计量。它反映了数据点与均值的偏离程度。在计算图像数据的标准差时,我们也是针对每个颜色通道分别进行的。标准差较大的颜色通道意味着该通道上的像素值变化较大,而标准差较小的通道则相对较为稳定。

归一化(Normalization)

归一化是将数据按比例缩放,使之落入一个小的特定区间,通常是[0, 1]或[-1, 1]。在图像处理中,我们通常会使用计算得到的均值和标准差来进行归一化,公式如下:

Normalized Value

=

Original Value

Mean

Std

\text{Normalized Value} = \frac{\text{Original Value} - \text{Mean}}{\text{Std}}

Normalized Value=StdOriginal Value−Mean​

注意,在某些情况下,为了简化计算并确保数据非负,我们可能会选择将数据缩放到[0, 1]区间,这时使用的是最大最小值归一化,而不是基于均值和标准差的归一化。但在这里,我们主要讨论基于均值和标准差的归一化,因为它能保留数据的分布特性。

为什么需要归一化?

加速收敛:归一化后的数据具有相似的尺度,这有助于梯度下降算法更快地找到最优解,因为不同特征的梯度更新将在同一数量级上,从而避免了某些特征因尺度过大或过小而导致的训练缓慢或梯度消失/爆炸问题。

提高精度:归一化可以改善模型的泛化能力,因为它使得模型更容易学习到特征之间的相对关系,而不是被特征的绝对大小所影响。

稳定性:归一化后的数据更加稳定,减少了训练过程中的波动,有助于模型更加稳定地收敛。

如何计算和使用mean和std

计算全局mean和std:在整个数据集上计算mean和std。这通常是在训练开始前进行的,并使用这些值来归一化训练集、验证集和测试集。

使用库函数:许多深度学习框架(如PyTorch、TensorFlow等)提供了计算mean和std的便捷函数,并可以直接用于数据集的归一化。

动态调整:在某些情况下,特别是当数据集非常大或持续更新时,可能需要动态地计算mean和std。这通常涉及到在训练过程中使用移动平均(如EMA)来更新这些统计量。

计算并使用数据的mean和std进行归一化是深度学习中的一项基本且重要的预处理步骤,它对于加速模型收敛、提高模型性能和稳定性具有重要意义。新建mean_std.py,插入代码:

from torchvision.datasets import ImageFolder

import torch

from torchvision import transforms

def get_mean_and_std(train_data):

train_loader = torch.utils.data.DataLoader(

train_data, batch_size=1, shuffle=False, num_workers=0,

pin_memory=True)

mean = torch.zeros(3)

std = torch.zeros(3)

for X, _ in train_loader:

for d in range(3):

mean[d] += X[:, d, :, :].mean()

std[d] += X[:, d, :, :].std()

mean.div_(len(train_data))

std.div_(len(train_data))

return list(mean.numpy()), list(std.numpy())

if __name__ == '__main__':

train_dataset = ImageFolder(root=r'data1', transform=transforms.ToTensor())

print(get_mean_and_std(train_dataset))

数据集结构:

image-20220221153058619

运行结果:

<code>([0.3281186, 0.28937867, 0.20702125], [0.09407319, 0.09732835, 0.106712654])

把这个结果记录下来,后面要用!

生成数据集

我们整理还的图像分类的数据集结构是这样的

data

├─Black-grass

├─Charlock

├─Cleavers

├─Common Chickweed

├─Common wheat

├─Fat Hen

├─Loose Silky-bent

├─Maize

├─Scentless Mayweed

├─Shepherds Purse

├─Small-flowered Cranesbill

└─Sugar beet

pytorch和keras默认加载方式是ImageNet数据集格式,格式是

├─data

│ ├─val

│ │ ├─Black-grass

│ │ ├─Charlock

│ │ ├─Cleavers

│ │ ├─Common Chickweed

│ │ ├─Common wheat

│ │ ├─Fat Hen

│ │ ├─Loose Silky-bent

│ │ ├─Maize

│ │ ├─Scentless Mayweed

│ │ ├─Shepherds Purse

│ │ ├─Small-flowered Cranesbill

│ │ └─Sugar beet

│ └─train

│ ├─Black-grass

│ ├─Charlock

│ ├─Cleavers

│ ├─Common Chickweed

│ ├─Common wheat

│ ├─Fat Hen

│ ├─Loose Silky-bent

│ ├─Maize

│ ├─Scentless Mayweed

│ ├─Shepherds Purse

│ ├─Small-flowered Cranesbill

│ └─Sugar beet

新增格式转化脚本makedata.py,插入代码:

import glob

import os

import shutil

image_list=glob.glob('data1/*/*.png')

print(image_list)

file_dir='data'code>

if os.path.exists(file_dir):

print('true')

#os.rmdir(file_dir)

shutil.rmtree(file_dir)#删除再建立

os.makedirs(file_dir)

else:

os.makedirs(file_dir)

from sklearn.model_selection import train_test_split

trainval_files, val_files = train_test_split(image_list, test_size=0.3, random_state=42)

train_dir='train'code>

val_dir='val'code>

train_root=os.path.join(file_dir,train_dir)

val_root=os.path.join(file_dir,val_dir)

for file in trainval_files:

file_class=file.replace("\\","/").split('/')[-2]

file_name=file.replace("\\","/").split('/')[-1]

file_class=os.path.join(train_root,file_class)

if not os.path.isdir(file_class):

os.makedirs(file_class)

shutil.copy(file, file_class + '/' + file_name)

for file in val_files:

file_class=file.replace("\\","/").split('/')[-2]

file_name=file.replace("\\","/").split('/')[-1]

file_class=os.path.join(val_root,file_class)

if not os.path.isdir(file_class):

os.makedirs(file_class)

shutil.copy(file, file_class + '/' + file_name)

完成上面的内容就可以开启训练和测试了。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。