简介:本资源是一份面向计算机及相关专业本科生的深度学习实战项目包,专为课程设计与期末大作业打造,聚焦水果图像识别任务,提供从模型搭建(CNN基础网络与轻量级MobileNetV2)、训练调优到结果分析的完整闭环方案。压缩包共2000个文件,含1667张JPG/JPEG格式水果原始图像、63张PNG标注图、260个JPEG样本及少量XML标注文件,支撑数据预处理与模型验证;另有4个核心Python训练/推理脚本、1份Markdown文档说明及配套实验报告,结构清晰、注释详尽,便于理解模型差异与工程实现细节。资源包大小661.64MB,已获603人学习下载。作为作者大三学期高分通过(98分)的导师指导项目,内容涵盖数据集划分逻辑、准确率对比表格、混淆矩阵可视化代码及常见过拟合调试建议,可直接复用或二次开发,显著降低初学者在图像分类项目中的入门门槛与试错成本。
1. 项目概述与核心价值
最近在整理硬盘,翻出来几年前带学生做的一个深度学习大作业,主题是水果识别。这个项目当时的要求是不仅要跑通模型,还得从零开始搭建网络、处理数据、撰写完整的实验报告。我手头这份资料,包含了基于经典CNN和MobileNetV2的两种模型源码、详细的实验报告、项目文档说明,以及一个整理好的水果数据集。这不仅仅是一份作业,更像是一个完整的、可复现的计算机视觉入门项目模板。无论你是正在为课程大作业发愁的学生,还是想通过一个具体案例上手PyTorch或TensorFlow的开发者,这套材料都能给你提供一个清晰的路径。它解决的问题很直接:如何从一堆水果图片开始,一步步构建、训练、评估一个能实际工作的分类模型,并把整个过程严谨地记录下来。
2. 项目整体设计与思路拆解
2.1 为什么选择水果识别作为课题?
水果识别看似简单,实则是一个非常适合入门深度学习的“练手”项目。首先,它的目标明确——区分不同种类的水果,这属于经典的图像多分类问题。其次,数据相对容易获取和标注,网上有多个公开的水果数据集,自己用手机拍一些图片也能快速构建一个小型数据集。最重要的是,这个问题复杂度适中:类别数通常在10到30之间,既不会像MNIST手写数字那样过于简单而缺乏挑战,也不会像ImageNet那样庞大到需要昂贵的计算资源。通过这个项目,你可以完整地走一遍深度学习项目的标准流程:数据准备、模型选择与搭建、训练调优、评估测试以及结果分析。
2.2 模型选型背后的考量:CNN与MobileNetV2
在这个项目中,我们特意选择了两种具有代表性的模型架构进行对比实现,这背后有教学和实战两方面的考虑。
经典CNN(卷积神经网络):这是我们手动搭建的一个多层卷积网络。它的结构并不复杂,通常由4到6个卷积层、池化层和全连接层堆叠而成。选择手动搭建CNN的核心目的,是让学习者“知其然更知其所以然”。你需要亲手定义每一个卷积核的大小、步长和填充,设计池化方式,理解特征图尺寸的变化,以及最终如何将三维特征图“拍平”送入全连接层进行分类。这个过程能帮你牢固掌握卷积、池化、激活函数、批量归一化等基础概念,这是理解任何复杂模型的前提。手动实现的CNN参数量可控,在小数据集上训练速度快,非常适合验证想法和教学演示。
MobileNetV2:作为对比,我们引入了MobileNetV2这个现代的、高效的轻量级网络。它的核心是深度可分离卷积和倒残差结构。选择它有几个关键理由:第一,效率。MobileNetV2在保持较高精度的同时,大幅减少了参数和计算量。这意味着你可以在CPU或者算力有限的GPU上更快地进行训练和推理,这对于课程作业或资源有限的个人开发者非常友好。第二,学习迁移学习和预训练模型。我们通常会使用在ImageNet上预训练好的MobileNetV2权重进行初始化,然后针对我们的水果数据集进行微调。这让你能直观体会到迁移学习的威力——用大规模数据集上学习到的通用特征,快速适配到自己的小规模特定任务上,往往只需要很少的迭代就能获得很好的效果。第三,接触工业级实践。MobileNet系列模型被广泛部署在移动端和嵌入式设备上,了解其设计思想对你未来从事模型优化和部署工作很有帮助。
通过对比这两种模型,你不仅能学会如何“造轮子”(手动CNN),也能学会如何“用轮子”并理解其精妙之处(MobileNetV2),从而建立起从理论到实践的完整认知。
2.3 数据集构建与处理的核心思路
数据集是模型的“粮食”。我们提供的这个水果数据集,虽然可能来源于公开数据集如“Fruits-360”的某个子集或自行整理,但其构建和处理思路是通用的。
数据收集与标注:最理想的情况是每个水果类别(如苹果、香蕉、橙子)都有数百到数千张图片。图片应尽可能涵盖不同的品种、颜色、成熟度、摆放角度、光照条件和背景复杂度,这样才能让模型学到鲁棒的特征,而不是记住某些特定场景。标注工作就是为每张图片打上正确的类别标签,通常用一个文件夹对应一个类别是最简单的组织形式。
数据预处理流水线:这是训练前至关重要的一步,我们的代码中会包含一个标准化的预处理流程:
- 读取与解码:从磁盘加载图像文件,解码为RGB像素矩阵。
- 调整尺寸:将所有图像缩放到统一的尺寸(例如224x224),这是大多数卷积网络输入的要求。
- 数据增强:这是提升模型泛化能力、防止过拟合的关键手段。我们会在训练时实时进行增强,包括:
- 随机水平翻转:模拟水果左右摆放的不同。
- 随机旋转:小幅度的旋转(如±15度)。
- 颜色抖动:轻微调整亮度、对比度和饱和度,模拟光照变化。
- 标准化:将像素值从[0, 255]范围缩放到[0, 1]或[-1, 1],并减去均值、除以标准差,使数据分布更稳定,加速模型收敛。
- 数据集划分:按照一定比例(如7:2:1或8:1:1)将数据随机划分为训练集、验证集和测试集。训练集用于模型参数更新;验证集用于在训练过程中监控模型表现,调整超参数(如学习率);测试集则用于最终评估模型的泛化性能,在整个训练过程中模型绝对不能接触到测试集。
注意:数据增强仅应用于训练集。验证集和测试集只进行确定性的预处理(如缩放、标准化),不进行任何随机变换,以确保评估结果的一致性和可比性。
3. 核心模块解析与代码实现要点
3.1 手动搭建CNN网络:层与层的堆叠艺术
手动实现一个CNN,就像用乐高积木搭建一座城堡。下面我们拆解一个典型的五层CNN结构,并说明每一层的设计意图和代码实现要点。
网络结构示例:
- 输入层:
(batch_size, 3, 224, 224)# 3通道RGB,224x224分辨率 - 卷积层1:64个3x3卷积核,填充1,步长1 ->
(batch_size, 64, 224, 224) - 激活函数:ReLU
- 池化层1:2x2最大池化,步长2 ->
(batch_size, 64, 112, 112) - 卷积层2:128个3x3卷积核 ->
(batch_size, 128, 112, 112) - 激活函数:ReLU
- 池化层2:2x2最大池化 ->
(batch_size, 128, 56, 56) - 卷积层3:256个3x3卷积核 ->
(batch_size, 256, 56, 56) - 激活函数:ReLU
- 全局平均池化层 ->
(batch_size, 256, 1, 1)-> 展平为(batch_size, 256) - 全连接层1:256个神经元 -> 输出
(batch_size, 128) - Dropout层(防止过拟合)
- 全连接层2(输出层):128个神经元 -> 输出
(batch_size, num_classes)
代码实现关键点(以PyTorch为例):
import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() # 特征提取部分 self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(128, 256, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1)) # 分类部分 self.fc1 = nn.Linear(256, 128) self.dropout = nn.Dropout(0.5) # Dropout率设为0.5 self.fc2 = nn.Linear(128, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = F.relu(self.conv3(x)) x = self.global_avg_pool(x) x = x.view(x.size(0), -1) # 展平 x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x实操心得:
- 卷积核与通道数:通常遵循“通道数翻倍,空间尺寸减半”的原则。随着网络加深,特征图尺寸变小,但通道数(即特征深度)增加,以捕获更抽象、更复杂的特征。
- 激活函数:ReLU是目前最常用的,因为它能有效缓解梯度消失问题,计算简单。
- 池化层:最大池化比平均池化更常用,因为它能保留最显著的特征,并提供一定的平移不变性。
- 全局平均池化:在最后一个卷积层后使用全局平均池化,而不是直接展平再接全连接层,可以大幅减少参数数量,并有一定防止过拟合的作用。
- Dropout:在全连接层之前加入Dropout是防止模型在训练集上过拟合的经典技巧。0.5是一个常用的初始值。
3.2 使用与微调MobileNetV2:站在巨人的肩膀上
使用MobileNetV2,我们无需从零开始设计网络结构,而是利用PyTorch或TensorFlow等框架提供的预定义模型。
核心步骤:
- 加载预训练模型:从模型库中加载在ImageNet上预训练好的权重。ImageNet包含1000个类别,其学到的底层特征(如边缘、纹理、形状)对于识别水果是高度可迁移的。
- 修改分类头:预训练模型的最后一层全连接层是为1000类设计的。我们需要将其替换为适合我们水果类别数的新全连接层。
- 选择微调策略:
- 方案一(快速微调):冻结除最后一层外所有骨干网络的参数,只训练新替换的分类头。这种方式训练极快,适用于数据量很少或计算资源极其有限的情况。
- 方案二(全面微调):解冻所有层,用较小的学习率对整个网络进行训练。这种方式通常能获得最好的性能,因为模型可以根据我们的水果数据调整所有层的参数。
代码实现关键点(PyTorch):
import torchvision.models as models import torch.nn as nn # 加载预训练模型,并设置 pretrained=True model = models.mobilenet_v2(pretrained=True) # 冻结所有骨干网络参数(如果选择方案一) # for param in model.parameters(): # param.requires_grad = False # 修改分类器:MobileNetV2的最后一层叫 `classifier` num_ftrs = model.classifier[1].in_features # 获取原分类器输入特征数 model.classifier = nn.Sequential( nn.Dropout(0.2), # MobileNetV2原结构中的Dropout率 nn.Linear(num_ftrs, num_classes) # 替换为我们的类别数 ) # 如果选择方案二(全面微调),则不需要冻结参数,但优化器中的学习率需要分层设置。 # 通常为骨干网络设置较小的学习率(如1e-4),为新分类头设置较大的学习率(如1e-3)。注意事项:
- 使用预训练模型时,输入图像的预处理方式(均值和标准差)必须与模型训练时一致。对于TorchVision的模型,通常使用ImageNet的统计量:均值=[0.485, 0.456, 0.406],标准差=[0.229, 0.224, 0.225]。
- 微调时,学习率的设置至关重要。一个常见的策略是使用
Adam优化器,初始学习率设为3e-4,并配合学习率调度器(如ReduceLROnPlateau)在验证集loss停滞时自动降低学习率。
3.3 数据加载与增强的工程化实现
一个健壮的数据管道是训练稳定性的保障。我们使用torchvision.transforms和DataLoader来构建。
from torchvision import transforms, datasets from torch.utils.data import DataLoader # 定义训练和验证/测试的不同变换 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放 transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_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]) ]) # 加载数据集 train_dataset = datasets.ImageFolder(root='path/to/train', transform=train_transform) val_dataset = datasets.ImageFolder(root='path/to/val', transform=val_transform) # 创建数据加载器 train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)参数解析与避坑指南:
RandomResizedCropvsResize + CenterCrop:训练时用前者增加多样性;验证时用后者保证评估一致性。batch_size:根据你的GPU内存调整。常见的有32, 64, 128。太大会导致内存溢出,太小则梯度更新噪声大,训练不稳定。可以从32开始尝试。shuffle=True:仅在训练集上打乱数据,让每个epoch看到的数据顺序都不同,有助于模型学习。num_workers:用于数据加载的子进程数。设置为CPU核心数附近的值(如4或8)可以加速数据I/O,避免训练时GPU等待数据。在Windows系统下,有时设为0可避免多进程错误。pin_memory=True:当使用GPU时,将此参数设为True可以将数据直接加载到页锁定内存,加速从CPU到GPU的数据传输。
4. 模型训练、验证与评估全流程
4.1 训练循环的构建与核心组件
训练一个深度学习模型,本质上是不断重复“前向传播 -> 计算损失 -> 反向传播 -> 更新参数”的过程。下面是一个标准的训练循环框架。
import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() # 多分类任务使用交叉熵损失 optimizer = optim.Adam(model.parameters(), lr=3e-4) # 使用Adam优化器 scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5, verbose=True) # 当监控指标(验证损失)在5个epoch内不再下降时,学习率乘以0.1 num_epochs = 50 best_val_acc = 0.0 for epoch in range(num_epochs): # 训练阶段 model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() # 清零梯度,非常重要! outputs = model(images) # 前向传播 loss = criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 running_loss += loss.item() * images.size(0) epoch_loss = running_loss / len(train_loader.dataset) # 验证阶段 model.eval() val_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): # 关闭梯度计算,节省内存和计算 for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs.data, 1) # 获取预测类别 total += labels.size(0) correct += (predicted == labels).sum().item() val_epoch_loss = val_loss / len(val_loader.dataset) val_acc = 100 * correct / total # 学习率调度 scheduler.step(val_epoch_loss) # 打印日志并保存最佳模型 print(f'Epoch [{epoch+1}/{num_epochs}], Train Loss: {epoch_loss:.4f}, Val Loss: {val_epoch_loss:.4f}, Val Acc: {val_acc:.2f}%') if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f'==> Best model saved with Val Acc: {val_acc:.2f}%')核心组件详解:
- 损失函数(Criterion):
CrossEntropyLoss是分类任务的标准选择,它内部集成了Softmax和对数计算,直接接收模型的原始输出(logits)和真实标签即可。 - 优化器(Optimizer):
Adam是自适应学习率优化器,结合了动量和自适应梯度,在大多数情况下表现稳定,是默认的首选。SGD配合动量(如momentum=0.9)和学习率衰减,在精心调参后可能达到更好的最终精度,但需要更多调参经验。 - 学习率调度器(Scheduler):
ReduceLROnPlateau是一个实用的调度策略,它根据验证集损失是否下降来动态调整学习率。当模型性能陷入平台期时,降低学习率有助于其找到更优的局部最小值。
4.2 模型评估与性能指标分析
训练完成后,我们需要在独立的测试集上对保存的最佳模型进行最终评估,以衡量其真实的泛化能力。
# 加载最佳模型 model.load_state_dict(torch.load('best_model.pth')) model.eval() test_correct = 0 test_total = 0 all_labels = [] all_predictions = [] with torch.no_grad(): for images, labels in test_loader: # 假设有独立的test_loader images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs.data, 1) test_total += labels.size(0) test_correct += (predicted == labels).sum().item() all_labels.extend(labels.cpu().numpy()) all_predictions.extend(predicted.cpu().numpy()) test_accuracy = 100 * test_correct / test_total print(f'Final Test Accuracy: {test_accuracy:.2f}%')除了整体准确率,更细致的评估能帮助我们发现问题:
- 混淆矩阵:可视化模型在各个类别上的分类情况,能清晰看出哪些类别容易混淆(例如,将青苹果误判为梨)。
from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt cm = confusion_matrix(all_labels, all_predictions) plt.figure(figsize=(10,8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.show() - 分类报告:提供精确率、召回率、F1-score等指标,对于类别不平衡的数据集尤为重要。
print(classification_report(all_labels, all_predictions, target_names=class_names))
4.3 实验报告与文档撰写的要点
一份优秀的实验报告或项目文档,其价值不亚于代码本身。它体现了你的思考过程和工程素养。我们的项目资料中包含了完整的报告模板,这里提炼几个关键部分:
- 引言与问题定义:清晰说明项目目标、背景和意义。
- 相关工作:简要介绍CNN和MobileNet的基本原理,引用关键文献。
- 数据集描述:详细说明数据来源、类别数、每类样本量、数据集划分比例,并展示一些样本图片。
- 方法:这是核心。需要详细描述:
- 网络结构图(可以用代码生成或手动绘制)。
- 数据预处理和增强的具体参数。
- 训练的超参数设置(批量大小、学习率、优化器、迭代轮数等)及其选择理由。
- 实验环境(Python、PyTorch/TensorFlow版本、GPU型号)。
- 实验结果与分析:
- 用曲线图展示训练损失和验证损失、训练准确率和验证准确率随epoch的变化。这能直观反映模型是否过拟合或欠拟合。
- 展示最终的测试集准确率、混淆矩阵和分类报告。
- 对比分析手动CNN和MobileNetV2在准确率、参数量、训练时间上的差异,并分析原因。
- 结论与展望:总结项目成果,指出模型的局限性(如对某些特定背景或光照条件识别不佳),并提出可能的改进方向(如尝试其他网络结构、集成学习、使用更大的数据集等)。
5. 常见问题、调试技巧与项目扩展
5.1 训练过程中的典型问题与解决方案
在实际操作中,你几乎一定会遇到下面这些问题。这里是我的“踩坑”记录和解决方法。
问题1:损失(Loss)不下降,准确率(Accuracy)一直很低(例如,等于随机猜测的概率)。
- 检查数据:首先确认你的数据加载是否正确。打印一个批次的数据和标签,看看图片是否显示正常,标签是否对应正确。一个常见错误是数据预处理(特别是归一化)出错,导致输入数据范围异常。
- 检查学习率:学习率太大可能导致损失震荡甚至爆炸(NaN);学习率太小则导致下降极其缓慢。尝试使用一个经典的学习率(如Adam用3e-4,SGD用0.01)开始,或者使用学习率查找器(如PyTorch的
torch.optim.lr_scheduler中的LambdaLR配合一个范围测试)来寻找合适的学习率范围。 - 检查模型输出:在训练前,让模型对一个批次的数据进行前向传播,检查输出的形状和数值范围是否合理(例如,分类任务输出应为
[batch_size, num_classes])。 - 检查损失函数:确认损失函数的输入(模型输出和真实标签)格式是否正确。对于交叉熵损失,标签应该是
LongTensor类型的类别索引,而不是one-hot编码。 - 简化问题:用一个极小的数据集(比如每类5张图)和简单的模型(比如只有一两层的CNN)进行过拟合测试。如果模型能快速过拟合这个小数据集(训练准确率达到100%),说明你的训练流程基本正确,问题可能出在数据或模型复杂度上。如果不能,则说明训练代码有根本性错误。
问题2:模型在训练集上表现很好,但在验证集上表现很差(过拟合)。
- 增加数据增强:这是最有效的手段之一。可以增加更多样化的增强,如随机裁剪、颜色抖动、CutMix、MixUp等。
- 添加或加强正则化:
- 在模型中增加Dropout层。
- 为优化器添加权重衰减(L2正则化),例如
optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)。
- 降低模型复杂度:对于手动CNN,可以减少卷积层的通道数或层数。
- 早停:持续监控验证集损失,当其在连续多个epoch不再下降时,提前停止训练。
- 使用预训练模型并冻结部分层:如果使用MobileNetV2,可以尝试冻结大部分骨干网络,只微调最后几层。
问题3:训练速度非常慢。
- 使用GPU:确保你的代码在GPU上运行(
model.to(device),data.to(device))。 - 优化数据加载:设置
DataLoader的num_workers大于0,并启用pin_memory=True。 - 增大批量大小:在GPU内存允许的范围内,适当增大
batch_size可以提高GPU利用率,但可能会影响泛化性能,需要配合调整学习率(批量增大,学习率也可适当增大)。 - 使用混合精度训练:对于较新的GPU(如Volta架构及以后),可以使用
torch.cuda.amp进行自动混合精度训练,这能显著减少内存占用并加速计算。
5.2 项目扩展与进阶思路
完成基础的水果识别后,你可以尝试以下方向来深化你的理解和技能:
模型轻量化与部署:
- 尝试比MobileNetV2更轻量的模型,如
MobileNetV3、ShuffleNetV2或EfficientNet-Lite。 - 学习使用
ONNX将PyTorch模型导出,并尝试在OpenVINO、TensorRT或移动端框架(如TensorFlow Lite、Core ML)上进行推理加速。 - 搭建一个简单的Web应用(使用Flask或Gradio),让用户上传图片并实时看到识别结果。
- 尝试比MobileNetV2更轻量的模型,如
提升模型性能:
- 集成学习:训练多个不同的模型(如CNN, MobileNetV2, ResNet),然后对它们的预测结果进行投票或平均,通常能提升最终准确率。
- 注意力机制:在CNN中引入
SENet、CBAM等注意力模块,让模型学会“关注”图像中更重要的区域(比如水果本身,而不是背景)。 - 更先进的数据增强:尝试
AutoAugment、RandAugment等策略搜索适合你数据集的增强组合。
改变任务类型:
- 目标检测:如果数据集中每张图片包含多个水果,或者你想知道水果在图片中的位置,可以将任务升级为目标检测。使用
YOLOv8、Faster R-CNN等框架,标注水果的边界框,训练一个检测模型。 - 实例分割:更进一步,如果你想精确地分割出每个水果的像素轮廓,可以尝试
Mask R-CNN等实例分割模型。
- 目标检测:如果数据集中每张图片包含多个水果,或者你想知道水果在图片中的位置,可以将任务升级为目标检测。使用
处理实际挑战:
- 类别不平衡:如果某些水果的图片很少,可以尝试过采样(如复制少数类图片)、欠采样、或使用带权重的损失函数。
- 细粒度分类:区分不同品种的苹果(如红富士、嘎啦果),这是一个更具挑战性的细粒度图像分类任务,可能需要更专业的网络结构和训练技巧。
这个水果识别项目是一个绝佳的起点。它麻雀虽小,五脏俱全,覆盖了深度学习项目从数据到部署的完整生命周期。希望这份详细的拆解和源码资料,能帮你少走弯路,更快地享受到动手实现一个AI模型的乐趣和成就感。在实际操作中,最宝贵的经验往往来自于解决那些报错信息和调试模型性能的过程,多动手,多思考,你一定会收获满满。
本文还有配套的精品资源,点击获取