先言

在图像分类领域,细粒度分类一直是一个具有挑战性的任务,特别是在宠物狗品种识别这样的场景中——不同品种的狗往往具有极其相似的视觉特征。传统的机器学习方法在这一任务上表现有限,而深度卷积神经网络特别是ResNet的出现,为解决这类细粒度分类问题提供了强有力的工具。

本文将带您完整实现一个基于ResNet的宠物狗细粒度分类项目:
✅ ResNet架构深度解析:残差连接的工作原理与优势
✅ 细粒度图像分类挑战:如何区分视觉相似的不同狗品种
✅ 迁移学习实战:使用预训练模型加速收敛提升性能
✅ 完整项目 pipeline:从数据预处理到模型部署的全流程

无论您是希望掌握ResNet实战应用,还是需要解决具体的细粒度分类问题,本文都将为您提供从理论到实践的完整指导!

一、项目简介

该项目旨在通过卷积神经网络(CNN)实现宠物狗的小颗粒度分类通过引入深度学习中的CNN模型,本项目将构建一个专门用于宠物狗细粒度分类的网络架构,充分利用卷积层的局部特征提取能力以及深层网络的高阶特征融合能力。项目将包括数据预处理、特征提取、模型训练与评估等步骤,使用犬类图像数据集进行模型训练,并通过精细的特征学习提升分类精度。此外,项目还将探索数据增强、迁移学习,继续训练等技术手段,以提高模型的泛化能力和鲁棒性,最终实现高精度的小颗粒度宠物犬分类。

关于项目的基本介绍。

  • 小颗粒度分类:宠物狗的十分类

    在这里插入图片描述 在这里插入图片描述
    小颗粒度分类之伯恩山犬 小颗粒度分类之柯基

二、数据获取以及处理

1.数据获取

通过kaggle官方网站宠物狗图像分类下载(先要访问这里需要用梯子,自己想办法弄咯)

2.数据处理

对于分类任务,我们需要的数据格式如下:

datasets/data/
├── category1/
│ ├── img1.jpg
│ └── img2.jpg
├── category2/
│ ├── img1.jpg
│ └── img2.jpg
└──
我们得到的数据是不符合规范的,所以要对数据进行初步的处理:

import os
import random
import shutil
from sklearn.model_selection import train_test_split

# 原始数据路径
data_dir = "./data"
train_dir = "./data/train"
val_dir = "./data/val"
# 创建目标文件夹
os.makedirs(train_dir, exist_ok=True)
os.makedirs(val_dir, exist_ok=True)
# 遍历每个类别
for class_name in os.listdir(data_dir):
    class_dir = os.path.join(data_dir, class_name)
    if not os.path.isdir(class_dir):
        continue
    # 获取当前类别的所有图像文件
    images = [f for f in os.listdir(class_dir) if f.endswith(('.jpg', '.png'))]
    # 划分训练集和验证集(80%训练,20%验证)
    train_imgs, val_imgs = train_test_split(
        images,
        test_size=0.2,
        random_state=42
    )
    # 创建类别子目录
    os.makedirs(os.path.join(train_dir, class_name), exist_ok=True)
    os.makedirs(os.path.join(val_dir, class_name), exist_ok=True)
    # 复制文件到目标目录
    for img in train_imgs:
        src = os.path.join(class_dir, img)
        dst = os.path.join(train_dir, class_name, img)
        shutil.copy(src, dst)
    for img in val_imgs:
        src = os.path.join(class_dir, img)
        dst = os.path.join(val_dir, class_name, img)
        shutil.copy(src, dst)

最终将数据处理为我们需要的数据存储格式效果:
在这里插入图片描述
在train和val目录下的子文件夹名称就是我们宠物狗的十个分类名

3.数据划分(train AND test)

在训练集的数据划分同时进行了数据增强,一般都要加而且不一定够,因为模型优化是少不了这一步的
代码(随便看看):

def getTrainDataset():
    transform = transforms.Compose([
        transforms.ToTensor(),
        #随机旋转
        transforms.RandomRotation(30),
        #模糊
        transforms.GaussianBlur(3, sigma=(0.1, 2.0)),
        #随机翻转
        transforms.RandomHorizontalFlip(),
        #缩放
        transforms.Resize((224, 224)),
        transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),  # 随机平移
        transforms.RandomGrayscale(p=0.1),  # 随机灰度化
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    train_data = datasets.ImageFolder(
        root = './data/train',
        transform=transform
    )
    train_loader = DataLoader(
        dataset=train_data,
        batch_size=32,
        shuffle=True
    )
    return train_loader
def getTestDataset():
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((224, 224)),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    test_data = datasets.ImageFolder(
        root='./data/val',
        transform=transform
    )
    class_names = test_data.classes
    test_loader = DataLoader(
        dataset=test_data,
        batch_size=16,
        shuffle=False
    )
    return test_loader,class_names
#test多返回了一个类别名称,只是为了获取一份类别名称,为后边的中英字典做映射(可以不用)

三、模型的选择和调整

1.残差网络模型ResNet(深度学习的深层奥义)

在这里插入图片描述
残差网络的最大优点:在训练的过程中残差作为捷径参数加入到梯度下降中,这使得网络在每一次的迭代加入一个梯度不为零的参数,很有效的解决了梯度在多次下降时出现的消失问题,也就是说,残差思想可以使模型不断进行学习,梯度就会不断的下降,没有止境。这才是深度学习的真正奥义,以至于在大模型阶段学习到的神经网络,依旧有残差的身影

2.获取ResNet网络并做一定的修改

def buildModel():
    #获取基本模型
    base_model = resnet18(pretrained=False)
    in_features = base_model.fc.in_features
    base_model.fc = nn.Sequential(
        nn.Linear(in_features,256),
        nn.BatchNorm1d(256),
        nn.ReLU(),
        nn.Linear(256,10),
    )

    opt = torch.optim.Adam(base_model.parameters(),lr=0.001)
    # 判断模型文件是否存在
    if os.path.exists('./model/resnet_model.pth'):
        base_model.load_state_dict(torch.load('./model/resnet_model.pth'))
    criterion = nn.CrossEntropyLoss(reduction='sum')
    return base_model,opt, criterion

上述代码中我们需要的分类数和ResNet的分类个数是不同的,所以再全连接层的最终输出我们做一些修改,为了提高泛化能力,顺便多加一层全连接结构(加强模型复杂度

3.给模型加一下注意力吧(总的会吧)

CBAM注意力,在深度学习基础,经典神经网络够用了
CBAM注意力机制:
在我们学习卷积神经网络的时候,我们在空间,深度,池化角度讲到过对于不同的卷积核对图像的特征提取是不一样的,所以CBAM利用了这一个特点,在空间和维度上分别对图像的特征进行提取,这是CBAM注意力机制的核心思想,现在我们用代码来实现一下:

class CBAM(nn.Module):
    def __init__(self, channels, reduction_ratio=16):
        super(CBAM, self).__init__()
        # 通道注意力
        self.channel_attention = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(channels, channels // reduction_ratio, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(channels // reduction_ratio, channels, kernel_size=1),
            nn.Sigmoid()
        )
        # 空间注意力
        self.spatial_attention = nn.Sequential(
            nn.Conv2d(2, 1, kernel_size=7, padding=3),
            nn.Sigmoid()
        )

    def forward(self, x):
        # 通道注意力
        channel_att = self.channel_attention(x)
        x_channel = x * channel_att
        # 空间注意力
        spatial_avg = torch.mean(x_channel, dim=1, keepdim=True)
        spatial_max, _ = torch.max(x_channel, dim=1, keepdim=True)
        spatial_concat = torch.cat([spatial_avg, spatial_max], dim=1)
        spatial_att = self.spatial_attention(spatial_concat)
        return x_channel * spatial_att

上述定义一个注意力机制的类,在模型添加注意力机制时,通过调用创建的实力对象可以多层添加注意力机制,用类来封装再调用是很方便的,在一般的项目开发,我们很多时候都会用到解耦合思想(高内聚,低耦合),当然这里作为教学也可以用类来封装模型,可以自己试一试。
那么我们模型函数就要做一些更改了

def buildModelWithCBAM():
    base_model = resnet18(pretrained=False)

    # 在ResNet的每个残差块后添加CBAM注意力
    def add_cbam_to_layer(layer, channels):
        return nn.Sequential(layer, CBAM(channels))

    # 为不同层添加注意力(示例:在layer1、layer2、layer3、layer4后添加)
    base_model.layer1 = add_cbam_to_layer(base_model.layer1, 64)
    base_model.layer2 = add_cbam_to_layer(base_model.layer2, 128)
    base_model.layer3 = add_cbam_to_layer(base_model.layer3, 256)
    base_model.layer4 = add_cbam_to_layer(base_model.layer4, 512)
    in_features = base_model.fc.in_features
    base_model.fc = nn.Sequential(
        nn.Linear(in_features,256),
        nn.BatchNorm1d(256),
        nn.ReLU(),
        nn.Linear(256,10),
    )
    opt = torch.optim.Adam(base_model.parameters(),lr=0.001)
    criterion = nn.CrossEntropyLoss(reduction='sum')
    if os.path.exists('./model/resnet_model_CBAM.pth'):
        base_model.load_state_dict(torch.load('./model/resnet_model_CBAM.pth'))
    return base_model,opt, criterion

我们在之前对ResNet网络进行过深度解读,我们知道ResNet有四层残差结构,那么就它了(残差层添加注意力机制)

四、模型的训练和验证

1.训练之前(可视化工具)

Tensorboard:TensorBoard是TensorFlow自带的一个强大的可视化工具,也是一个Web应用程序套件。TensorBoard目前支持7种可视化,Scalars,Images,Audio,Graphs,Distributions,Histograms和Embeddings。其中可视化的主要功能如下。
(1)Scalars:展示训练过程中的准确率、损失值、权重/偏置的变化情况。
(2)Images:展示训练过程中记录的图像。
(3)Audio:展示训练过程中记录的音频。
(4)Graphs:展示模型的数据流图,以及训练在各个设备上消耗的内存和时间。
(5)Distributions:展示训练过程中记录的数据的分部图。
(6)Histograms:展示训练过程中记录的数据的柱状图。
(7)Embeddings:展示词向量后的投影分部。
这里我们只用Images就行了

在使用之前,我们先pip一下(可以自己找一下镜像网上都是,加速下载)

pip install tensorboard

怎么用:tensorboard官方操作文档这是必须要经历的文档阅读时间。
这里我们只用一个例子:
add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None)[source]
Add many scalar data to summary.
在这里插入图片描述

from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter()
r = 5
for i in range(100):
    writer.add_scalars('run_14h', {'xsinx':i*np.sin(i/r),
                                    'xcosx':i*np.cos(i/r),
                                    'tanx': np.tan(i/r)}, i)
writer.close()
#This call adds three values to the same scalar plot with the tag
#run_14h' in TensorBoard's scalar section.

2.带可视化工具的模型训练

import torch
import torch.nn as nn
from model import buildModel
from data import getTestDataset,getTrainDataset
from torch.utils.tensorboard import SummaryWriter
import torchvision
import os
writer = SummaryWriter('runs')


model,opt,criterion = buildModel()
train_dataset = getTrainDataset()
# test_dataset = getTestDataset()
def train(model,train_dataset,opt,criterion,epochs):
    model.train()
    # acc = 0
    for epoch in range(epochs):
        correct = 0
        loss_total = 0
        loss_mean = 0
        # 学习率优化器
        # scheduler = torch.optim.lr_scheduler.StepLR(opt, step_size=5, gamma=0.1)
        for idx, (data,lable) in enumerate(train_dataset):
            y_pred= model(data)
            loss = criterion(y_pred,lable)
            loss_total += loss.item()
            opt.zero_grad()
            loss.backward()
            opt.step()
            # 更新学习率
            # scheduler.step()
            index = torch.argmax(y_pred,dim=1)
            correct+= (index.eq(lable)).sum().item()
            image_grid = torchvision.utils.make_grid(data)
            writer.add_image(f'r_m_{epoch+1}_{idx*100}',image_grid,epoch*len(train_dataset.dataset)+ idx)
        torch.save(model.state_dict(), './model/resnet_model.pth')

        loss_mean = round(loss_total/len(train_dataset.dataset),5)
        acc = round(correct/len(train_dataset.dataset)*100,2)

        writer.add_scalar('训练集准确率',acc,epoch+1)
        writer.add_scalar('训练集损失',loss_mean,epoch+1)

        print(f'第{epoch+1}次更新损失为:{loss_mean}, 准确率为:{acc}%')


print('训练开始..')
train(model,train_dataset,opt,criterion,20)

在这里插入图片描述

4.带可视化工具的模型验证并转为表哥数据(csv)

from model import buildModel
import torch.nn as nn
import torch
from data import getTestDataset
import numpy as np
import pandas as pd
def eval(model,test_dataset):
    model.eval()

    with torch.no_grad():
        correct_total = 0
        data_csv = np.empty((0,12))
        for idx,( data,target) in enumerate(test_dataset):
            output = model(data)
            inx = torch.argmax(output,dim=1)
            correct_total += sum(inx==target).item()
            predict=torch.argmax(output,dim=1).detach().numpy().reshape(-1,1)
            output1 = output.detach().numpy()
            label = target.numpy().reshape(-1,1)
            data_total = np.concatenate([output1,predict,label],axis=1)
            data_csv = np.concatenate([data_csv,data_total],axis=0)
        acc = correct_total/len(test_dataset.dataset)*100
        print(f'测试集预测结果准确率为:{acc}%')
    return data_csv

test_dataset,class_names = getTestDataset()
model,_,_ = buildModel()
data_csv=eval(model,test_dataset)
columns = [*class_names,'predict','label']
data = pd.DataFrame(data_csv,columns=columns)
data.to_csv('./excel/data.csv',index=False)

5.使用tensorBoard查看训练和验证的走势图和数据增强过程

在训练和验证的代码执行过后都对在根目录下出现一个文件名为runs,所有的数据都会在runs目录下我们需要TensorBoard的官方执行命令查看

tensorboard --logdir=runs

之后会出现一个地址在浏览器访问,就可以得到下面的数据过程图像和走势图

在这里插入图片描述 在这里插入图片描述
准确率走势图 损失率走势图
在这里插入图片描述 在这里插入图片描述
数据增强效果 数据增强效果

6.模型的应用

def pre(model,image_path):
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    img = cv.imread(image_path)
    img = np.transpose(img, (2, 0, 1))
    img = torch.tensor(img, dtype=torch.float32)
    img = img.unsqueeze(0)
    img = transform(img)
    # print(img.shape)

    model.eval()
    output = model(img)
    pred = output.argmax(dim=1, keepdim=True)
    _,classnames =getTestDataset()
    English_to_Chinese = {
        "bernese mountain_dog": "伯恩山犬",
        "border_collie": "边境牧羊犬",
        "chihuahua": "吉娃娃",
        "corgi": "柯基犬",
        "dachshund": "腊肠犬",
        "golden_retriever": "金毛寻回犬",
        "jack_russell": "杰克罗素梗",
        "labrador": "拉布拉多犬",
        "pug": "巴哥犬",
        "siberian_husky": "西伯利亚哈士奇"
    }
    # print(f'预测结果为:{pred.item()},该图中为:{English_to_Chinese[classnames[pred.item()]]}')
    return English_to_Chinese[classnames[pred.item()]]
model,_,_ = buildModel()
image_path = './image/2.jpg'
pre(model,image_path)
image_list = [os.path.join('./image',i)for i in os.listdir('./image')]
for index,image_path in enumerate(image_list):
    classname = pre(model,image_path)
    print(f'第{index+1}张图像的预测结果为:{classname}')

在模型定义的函数中我们使用的参数读取继续训练的代码思想,每次训练,每次使用的都是最后一次的模型训练参数,一般都是最准确的,训练的多和话
看到没,上边类别分类名就用上了(中英文映射字典)主要是为了看懂代码

五、完整代码

整体项目代码:

import torch
import torch.nn as nn
from torchvision import datasets,transforms
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torchvision
import numpy as np
import pandas as pd
import cv2 as cv
import os


def getTrainDataset():
    transform = transforms.Compose([
        transforms.ToTensor(),
        #随机旋转
        transforms.RandomRotation(30),
        #模糊
        transforms.GaussianBlur(3, sigma=(0.1, 2.0)),
        #随机翻转
        transforms.RandomHorizontalFlip(),
        #缩放
        transforms.Resize((224, 224)),
        transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),  # 随机平移
        transforms.RandomGrayscale(p=0.1),  # 随机灰度化
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    train_data = datasets.ImageFolder(
        root = './data/train',
        transform=transform
    )
    train_loader = DataLoader(
        dataset=train_data,
        batch_size=32,
        shuffle=True
    )
    return train_loader
def getTestDataset():
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((224, 224)),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    test_data = datasets.ImageFolder(
        root='./data/val',
        transform=transform
    )
    class_names = test_data.classes
    test_loader = DataLoader(
        dataset=test_data,
        batch_size=16,
        shuffle=False
    )
    return test_loader,class_names

class CBAM(nn.Module):
    def __init__(self, channels, reduction_ratio=16):
        super(CBAM, self).__init__()
        # 通道注意力
        self.channel_attention = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(channels, channels // reduction_ratio, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(channels // reduction_ratio, channels, kernel_size=1),
            nn.Sigmoid()
        )
        # 空间注意力
        self.spatial_attention = nn.Sequential(
            nn.Conv2d(2, 1, kernel_size=7, padding=3),
            nn.Sigmoid()
        )

    def forward(self, x):
        # 通道注意力
        channel_att = self.channel_attention(x)
        x_channel = x * channel_att

        # 空间注意力
        spatial_avg = torch.mean(x_channel, dim=1, keepdim=True)
        spatial_max, _ = torch.max(x_channel, dim=1, keepdim=True)
        spatial_concat = torch.cat([spatial_avg, spatial_max], dim=1)
        spatial_att = self.spatial_attention(spatial_concat)

        return x_channel * spatial_att
def buildModelWithCBAM():
    base_model = resnet18(pretrained=False)

    # 在ResNet的每个残差块后添加CBAM注意力
    def add_cbam_to_layer(layer, channels):
        return nn.Sequential(layer, CBAM(channels))

    # 为不同层添加注意力(示例:在layer1、layer2、layer3、layer4后添加)
    base_model.layer1 = add_cbam_to_layer(base_model.layer1, 64)
    base_model.layer2 = add_cbam_to_layer(base_model.layer2, 128)
    base_model.layer3 = add_cbam_to_layer(base_model.layer3, 256)
    base_model.layer4 = add_cbam_to_layer(base_model.layer4, 512)
    in_features = base_model.fc.in_features
    base_model.fc = nn.Sequential(
        nn.Linear(in_features,256),
        nn.BatchNorm1d(256),
        nn.ReLU(),
        nn.Linear(256,10),
    )
    opt = torch.optim.Adam(base_model.parameters(),lr=0.001)
    criterion = nn.CrossEntropyLoss(reduction='sum')
    if os.path.exists('./model/resnet_model_CBAM.pth'):
        base_model.load_state_dict(torch.load('./model/resnet_model_CBAM.pth'))
    return base_model,opt, criterion
writer = SummaryWriter('runs')


model,opt,criterion = buildModel()
train_dataset = getTrainDataset()
# test_dataset = getTestDataset()
def train(model,train_dataset,opt,criterion,epochs):
    model.train()
    # acc = 0
    for epoch in range(epochs):
        correct = 0
        loss_total = 0
        loss_mean = 0
        # 学习率优化器
        # scheduler = torch.optim.lr_scheduler.StepLR(opt, step_size=5, gamma=0.1)
        for idx, (data,lable) in enumerate(train_dataset):
            y_pred= model(data)
            loss = criterion(y_pred,lable)
            loss_total += loss.item()
            opt.zero_grad()
            loss.backward()
            opt.step()
            # 更新学习率
            # scheduler.step()
            index = torch.argmax(y_pred,dim=1)
            correct+= (index.eq(lable)).sum().item()
            image_grid = torchvision.utils.make_grid(data)
            writer.add_image(f'r_m_{epoch+1}_{idx*100}',image_grid,epoch*len(train_dataset.dataset)+ idx)
        torch.save(model.state_dict(), './model/resnet_model.pth')

        loss_mean = round(loss_total/len(train_dataset.dataset),5)
        acc = round(correct/len(train_dataset.dataset)*100,2)

        writer.add_scalar('训练集准确率',acc,epoch+1)
        writer.add_scalar('训练集损失',loss_mean,epoch+1)

        print(f'第{epoch+1}次更新损失为:{loss_mean}, 准确率为:{acc}%')


print('训练开始..')
train(model,train_dataset,opt,criterion,20)
    
def eval(model,test_dataset):
    model.eval()

    with torch.no_grad():
        correct_total = 0
        data_csv = np.empty((0,12))
        for idx,( data,target) in enumerate(test_dataset):
            output = model(data)
            inx = torch.argmax(output,dim=1)
            correct_total += sum(inx==target).item()
            predict=torch.argmax(output,dim=1).detach().numpy().reshape(-1,1)
            output1 = output.detach().numpy()
            label = target.numpy().reshape(-1,1)
            data_total = np.concatenate([output1,predict,label],axis=1)
            data_csv = np.concatenate([data_csv,data_total],axis=0)
        acc = correct_total/len(test_dataset.dataset)*100
        print(f'测试集预测结果准确率为:{acc}%')
    return data_csv

test_dataset,class_names = getTestDataset()
model,_,_ = buildModel()
data_csv=eval(model,test_dataset)
columns = [*class_names,'predict','label']
data = pd.DataFrame(data_csv,columns=columns)
data.to_csv('./excel/data.csv',index=False)

def pre(model,image_path):
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    img = cv.imread(image_path)
    img = np.transpose(img, (2, 0, 1))
    img = torch.tensor(img, dtype=torch.float32)
    img = img.unsqueeze(0)
    img = transform(img)
    # print(img.shape)

    model.eval()
    output = model(img)
    pred = output.argmax(dim=1, keepdim=True)
    _,classnames =getTestDataset()
    English_to_Chinese = {
        "bernese mountain_dog": "伯恩山犬",
        "border_collie": "边境牧羊犬",
        "chihuahua": "吉娃娃",
        "corgi": "柯基犬",
        "dachshund": "腊肠犬",
        "golden_retriever": "金毛寻回犬",
        "jack_russell": "杰克罗素梗",
        "labrador": "拉布拉多犬",
        "pug": "巴哥犬",
        "siberian_husky": "西伯利亚哈士奇"
    }
    # print(f'预测结果为:{pred.item()},该图中为:{English_to_Chinese[classnames[pred.item()]]}')
    return English_to_Chinese[classnames[pred.item()]]
model,_,_ = buildModel()
image_path = './image/2.jpg'
pre(model,image_path)
image_list = [os.path.join('./image',i)for i in os.listdir('./image')]
for index,image_path in enumerate(image_list):
    classname = pre(model,image_path)
    print(f'第{index+1}张图像的预测结果为:{classname}')

结语

通过本实战项目,您不仅能够掌握ResNet在实际项目中的应用,更能深入理解细粒度图像分类的核心技术和解决方案。残差网络的优势在这类困难分类任务中得到了充分体现,为解决现实世界的复杂视觉问题提供了有效工具。

🚀 动手实践:本文提供完整可运行的代码示例,您可以在Google Colab或本地环境中亲身体验ResNet的强大能力!您希望看到哪些具体的实现细节?欢迎在评论区留言讨论!

📌 下篇预告:下次再说还没想好(大模型吧)

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐