
简介这是一份基于卷积神经网络的鸟类识别实验报告面向机器学习、深度学习方向的科研人员、学生及技术人员帮助读者掌握细粒度图像分类模型的构建与调优方法。报告以AlexNet和ResNet-18为主干网络详细展示了将输出层修改为200类、加载ImageNet预训练权重并微调的关键步骤同时围绕CUB-200-2011数据集介绍了训练集/验证集/测试集划分策略。文中呈现了训练过程中的Loss和Accuracy变化曲线并对使用预训练权重与随机初始化、不同学习率及训练步数等情形进行了对比分析有助于理解过拟合、收敛速度与超参数选择之间的关系。压缩包内仅含1个docx文档大小1.36MB结构紧凑、阅读方便既可作为课堂教学的补充材料也能作为初学者动手实践的入门项目。目前已有180人学习适合需要从理论到实验全面了解CNN图像识别流程的读者。1. 鸟类识别实验从 CNN 选型到 CUB-200-2011 微调全流程做细粒度图像分类的人都知道鸟类识别是这类任务里最磨人的一个200 个类别很多鸟外形几乎一样区别只在翅膀上一撮羽毛的颜色或者嘴型的弧度。我拿到这份基于卷积神经网络的鸟类识别实验报告时最关心的是它怎么处理这种「类间差异极小」的问题。实验用了 AlexNet 和 ResNet-18 两种经典卷积神经网络结构在 CUB-200-2011 数据集上做预训练权重微调最终把 200 类鸟类识别做到了可用的精度。这份报告适合正在学深度学习、想找一个完整 CNN 图像分类练手项目的学生也适合要快速搭建细粒度分类基线、想参考别人参数设置的研究人员。下面按我自己的复现路径拆解网络怎么改、数据怎么喂、训练怎么调、坑在哪儿。2. 网络结构选型AlexNet 与 ResNet-18 的差异在哪里细粒度分类任务的网络选择不是随便挑一个 CNN 就能出效果的。实验里选 AlexNet 和 ResNet-18 这两个结构背后是有明确考量的一个代表经典全连接主导的设计一个代表残差结构主导的设计两者在参数量、感受野和信息传递方式上差别很大正好能用来对比迁移学习的效果基线。2.1 两种卷积神经网络结构图背后的设计逻辑AlexNet 的卷积神经网络结构图核心是 5 个卷积层加 3 个全连接层卷积层通道数从 96 一路扩到 384 再回到 256最后接 4096 维的全连接层整个网络参数量在 6000 万左右输入要求是 227×227 的 RGB 图像。这个结构放在 2012 年是碾压级的但放到细粒度任务上问题在于全连接层占掉了绝大多数参数而全连接层对「局部细微差异」的建模能力不如卷积层直接。ResNet-18 的卷积神经网络结构图就完全不同了它由 4 个 stage 的 BasicBlock 堆叠而成每个 stage 经历一次空间下采样通道数从 64 翻到 128、256、512参数量只有约 1100 万输入 224×224。残差连接让梯度可以跨层直接回传网络可以做得更深而不退化。在鸟类识别这种任务上深层的特征图能捕捉到翅膀纹理、羽毛边界这类高判别性信息这是 AlexNet 那种「卷积提特征 全连接硬分类」的设计做不到的。做图像分类选型还有一个基本认知要摆正一维卷积神经网络通常用来处理时序信号或者序列数据比如音频、传感器波形图像是二维数据天然应该用二维卷积。有些新手把一维卷积的代码硬套到图像上通道对不上、感受野也完全不是那回事属于白费力气。本实验用的两个结构都是标准二维卷积不存在这个问题。2.2 把输出层从 1000 改成 200模型改造脚本两个预训练模型原本都是在 ImageNet 上做 1000 类分类的输出层的维度是 1000。而 CUB-200-2011 数据集只有 200 个鸟类类别所以第一步就是替换分类头。常见做法是保留主干特征提取部分只把最后一层全连接换掉。在 PyTorch 里这个操作非常直接import torchvision.models as models import torch.nn as nn # 加载 ImageNet 预训练权重 alexnet models.alexnet(weightsmodels.AlexNet_Weights.IMAGENET1K_V1) # 替换最后一层原输出 1000 - 200 alexnet.classifier[6] nn.Linear(4096, 200) # 同样的方式处理 ResNet-18 resnet18 models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 替换全连接层 resnet18.fc nn.Linear(resnet18.fc.in_features, 200)这里有几个关键点要说明。classifier[6]是 AlexNet 的 Sequential 容器里第 7 个模块也就是最后的全连接层in_features自动读取前一层的 4096 维输出不用手写死。对 ResNet-18它的分类头叫fc原来是Linear(512, 1000)替换成Linear(512, 200)即可。替换完之后新的输出层参数是随机初始化的而主干网络的参数仍然是 ImageNet 上训练好的。实验报告里明确说了「对于新的输出层我们从头开始训练并使用较小的学习率对预训练的参数进行微调」这句话就是迁移学习的标准操作新层学得快主干学得慢。替换输出层之后建议先做一步验证打印model.fc.out_features确认是 200再跑一次 1×3×224×224 的假输入做 forward 测试防止维度不匹配的问题在训练中途才爆出来。我见过不少人在这一步翻车以为是代码问题查了半天发现是预训练权重版本和模型结构对不上。2.3 预训练权重初始化为什么不建议随机初始化细粒度数据集有个天然劣势数据量太小。CUB-200-2011 总共才 11788 张图片200 类平均每类不到 60 张。这点数据量让一个 6000 万参数的 AlexNet 从零开始学 200 类的细微差异结果必然是欠拟合或者过拟合二选一。而 ImageNet 预训练权重已经让网络学会了通用的视觉特征——边缘、纹理、形状、颜色分布这些底层特征在鸟类识别里同样有效。实验报告给出的对比结果也验证了这一点使用预训练权重进行微调的方法在性能和收敛速度方面都显著优于从头开始训练的方法。收敛速度的提升尤其明显因为初始化的特征已经不是随机噪声了新的输出层只需要在已有特征和 200 个类别之间建立映射关系训练初期 loss 就能快速下降。从头训练的话前几十个 epoch 网络都在摸索底层特征loss 曲线会拖得很长。手工初始化输出层时我一般会单独设置一个kaiming_normal初始化因为随机初始化的输出层在第一个 batch 的梯度可能会很大把主干网络刚微调好的权重冲乱。实验报告里没有细讲这一步但实际复现时值得加上def init_output_layer(model): for name, param in model.named_parameters(): if classifier.6 in name or fc in name: if param.dim() 2: nn.init.kaiming_normal_(param)这段逻辑是按参数名匹配输出层的权重用 Kaiming 初始化把新层的初始分布拉到一个合理的方差范围避免前向传播时信号过大或过小。常见做法是只对新层做初始化主干参数保持预训练状态不动。两个网络的选型对比可以整理成一张表方便后面调参时查阅网络参数量输入尺寸卷积结构全连接层细粒度任务表现AlexNet约 6000 万227×2275 层卷积3 层全连接基线水平泛化一般ResNet-18约 1100 万224×2244 stage 残差块1 层全连接精度更高收敛更快3. 数据集划分与 DataLoaderCUB-200-2011 的读入姿势网络结构定好了接下来最关键的是数据怎么进模型。CUB-200-2011 不是那种下下来就能直接喂给torchvision.datasets.ImageFolder的标准分类目录它带了一套自己的标注文件系统。如果直接按目录结构去读大概率会漏掉边界框和属性信息而这恰恰是细粒度识别最值钱的标注。3.1 CUB-200-2011 数据集的标注文件和目录结构这份数据集是加州理工学院 2010 年发布的224 MB 左右解压后主要包含images目录和四个关键文本文件。images目录下是 200 个子文件夹每个文件夹对应一种鸟文件夹名是类别 ID 加英文名。但四个文本文件才是真正的入口文件内容格式images.txt全部图片文件名图片ID 文件名image_class_labels.txt每张图片的类别标签图片ID 类别ID(1-200)bounding_boxes.txt每张图片的边界框图片ID x y 宽 高attributes.txt每张图片的属性标注图片ID 属性ID 是否具备train_test_split.txt官方划分的图片图片ID 是否为训练集attributes.txt 里有 312 个二值属性比如「翅膀主色是蓝色」「嘴型是尖锥形」这些是细粒度分类的额外监督信号。实验报告里提到「每张图片都经过了精细的标注包括鸟类的边界框和属性等」说的就是这套东西。有一点和实验报告描述不完全一致的地方需要注意报告写「每种鸟类大约有 60 张图片」这是平均值。实际上 CUB-200-2011 的类别分布并不完全均匀有的类有 70 多张有的类只有 40 多张。处理类别不平衡时这个差异会体现出来。读取这套标注文件的标准姿势是先把所有文本文件按图片 ID 关联起来合成一个元数据表。常见做法是写一个小工具函数import pandas as pd def load_cub_metadata(root_dir): # 读取图片列表与类别标签 images_df pd.read_csv(f{root_dir}/images.txt, sep , headerNone, names[img_id, filename]) labels_df pd.read_csv(f{root_dir}/image_class_labels.txt, sep , headerNone, names[img_id, label]) bbox_df pd.read_csv(f{root_dir}/bounding_boxes.txt, sep , headerNone, names[img_id, x, y, w, h]) split_df pd.read_csv(f{root_dir}/train_test_split.txt, sep , headerNone, names[img_id, is_train]) # 合并通过 img_id 关联 meta images_df.merge(labels_df, onimg_id) \ .merge(bbox_df, onimg_id) \ .merge(split_df, onimg_id) meta[label] meta[label] - 1 # 标签从 1 开始转为 0-199 return meta这段代码做的事很朴素四个文件都按图片 ID 对齐合成一张宽表。label减 1 是把类别 ID 从 1-based 转成 0-based这也是 PyTorch 的CrossEntropyLoss期望的标签格式。训练集和测试集的划分在train_test_split.txt里已经有了但如果你像我一样想自定义训练/验证集比例可以忽略这一列自己重新划分。3.2 按类别做分层划分而不是随机划分实验报告说「我们将数据集分为训练集、验证集和测试集」但没细说划分方法。网上很多复现直接random_split一把梭这在普通分类任务上问题不大但在细粒度任务上会埋一个坑随机划分可能让某个类别在验证集里一条数据都没有或者训练集里某个类只有三五张图。200 个类别每个类别样本量本来就少任何一类的缺失都会让验证指标失真。正确的做法是按类别做分层抽样保证每个类别在训练集、验证集、测试集中的比例一致。我用的是 scikit-learn 的StratifiedShuffleSplitimport numpy as np from sklearn.model_selection import StratifiedShuffleSplit meta load_cub_metadata(CUB_200_2011) labels meta[label].values # 第一次划分训练集 60%临时集 40% sss1 StratifiedShuffleSplit(n_splits1, test_size0.4, random_state42) train_idx, temp_idx next(sss1.split(np.zeros(len(labels)), labels)) # 第二次划分临时集拆成验证集和测试集各占 20% temp_labels labels[temp_idx] sss2 StratifiedShuffleSplit(n_splits1, test_size0.5, random_state42) val_idx, test_idx next(sss2.split(np.zeros(len(temp_labels)), temp_labels)) val_idx, test_idx temp_idx[val_idx], temp_idx[test_idx]这里test_size0.4的意思是临时集占总样本的 40%第二次划分时再把这 40% 对半拆成验证集和测试集最终比例是 6:2:2。random_state42固定随机种子保证每次复现划分结果一致。分层抽样确保 200 个类别在每个子集中的相对比例与全集一致这样训练时每个类都有足够样本验证时每个类都能被监控到。实际使用中如果你不想写这套划分逻辑也可以直接用官方train_test_split.txt里的划分然后把训练集再按 9:1 拆出验证集。但分层划分的逻辑适用于任何细粒度数据集值得写一遍。3.3 图像预处理先按 bbox 裁剪再增强CUB-200-2011 的原图大多是一整张带复杂背景的照片鸟可能只占画面中心的一小块区域。如果直接整图 resize 到 224×224背景像素会淹没鸟本身的特征模型很容易学到「背景是绿色就是这种鸟」这种错误关联。实验报告可视化部分展示的 bounding_boxes 预测结果说明边界框信息是实验明确使用的所以在预处理阶段就应该利用它。我通常会先按照 bounding box 把鸟的区域裁出来再 resize 到网络输入尺寸最后做数据增强from torchvision import transforms class BBoxCrop: def __init__(self, meta): self.bbox {row.img_id: (row.x, row.y, row.w, row.h) for row in meta.itertuples()} def __call__(self, img, img_id): x, y, w, h self.bbox[img_id] # 边界框越界时做截断防止裁剪区域超出图像范围 x, y max(0, int(x)), max(0, int(y)) return img.crop((x, y, x int(w), y int(h))) train_transform transforms.Compose([ BBoxCrop(meta), transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ BBoxCrop(meta), transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])BBoxCrop先按标注框裁出鸟的主体这一步对细粒度识别的影响极大。RandomResizedCrop的scale(0.8, 1.0)做了随机缩放裁剪相当于轻度模仿不同拍摄距离下的尺度变化。ColorJitter的三个分量都设为 0.2让颜色特征不至于被模型当成唯一判别依据。Normalize 的均值和方差用的是 ImageNet 的统计值因为主干网络是在 ImageNet 上预训练的输入分布对齐后特征提取才不会被扭曲。验证集就不用随机增强了只做 CenterCrop 取中心区域保证评估时结果可复现。4. 微调训练配置学习率、衰减策略与收敛判断网络结构改完了数据集也喂得进去了剩下的就是训练过程本身。这一章是最容易出「看起来在训练、实则没学到东西」的阶段。实验报告里有一段话值得反复读「过大的学习率可能导致模型在训练过程中发生震荡而过小的学习率则可能导致模型收敛速度过慢」——这句话是实验结果里最直接的经验总结。4.1 预训练微调 vs 从头训练的性能鸿沟实验报告对比了「仅使用 CUB-200-2011 数据集从随机初始化的网络参数开始训练的结果」和「使用预训练权重进行微调的结果」结论是预训练在性能和收敛速度两方面都显著占优。这个结论不意外但背后的原因值得说透。CUB-200-2011 只有 11788 张图随机初始化意味着网络连「边缘」和「纹理」这种最基础的特征都要从头学。一个 6000 万参数的 AlexNet 在这点数据上拟合 200 个细分类别几乎必然过拟合。预训练权重则把底层特征层变成了「现成的特征提取器」微调阶段只需要调整高层的语义信息把 ImageNet 的 1000 类抽象映射到 CUB 的 200 类具体鸟种。这也是为什么实验里预训练模型在训练初期 loss 下降速度明显更快——它起步的位置已经接近局部最优了。按我的经验预训练模型的收敛 epoch 数大约是随机初始化模型的四分之一到三分之一。也就是说如果随机初始化要跑 80 个 epoch 才能到 60% 精度预训练微调大概 20 个 epoch 就能到 75% 以上而且继续训练还能往上走。4.2 参数设置与训练循环实验报告没有给出具体的超参数数值只说「尝试了不同的训练步数、学习率以及学习率衰减策略」。我按细粒度分类的常见配置补一套能直接跑通的参数优化器用 SGD momentum学习率主干 1e-4、输出层 1e-3权重衰减 1e-4批次大小 32训练 60 个 epoch学习率在第 30 和第 50 个 epoch 各衰减一次到原来的 0.1。输出层的学习率比主干大 10 倍因为它是随机初始化的新层学得快主干已经有预训练基础学太猛会把原有特征破坏掉。训练循环的核心代码如下import torch import torch.optim as optim # 分组设置学习率输出层 1e-3其余层 1e-4 resnet18.fc nn.Linear(512, 200) optimizer optim.SGD([ {params: [p for n, p in resnet18.named_parameters() if fc not in n], lr: 1e-4}, {params: resnet18.fc.parameters(), lr: 1e-3} ], momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) criterion nn.CrossEntropyLoss() for epoch in range(60): resnet18.train() total_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs resnet18(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() # 每 5 个 epoch 在验证集上评估一次 if epoch % 5 0: resnet18.eval() correct 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.cuda(), labels.cuda() preds resnet18(imgs).argmax(dim1) correct (preds labels).sum().item() val_acc correct / len(val_loader.dataset) print(fEpoch {epoch}: loss{total_loss / len(train_loader):.4f}, val_acc{val_acc:.4f})PyTorch 的optimizer支持按参数组设置独立学习率named_parameters()按名称匹配fc来区分输出层和主干层这是微调任务的标准写法。StepLR(step_size30, gamma0.1)表示每 30 个 epoch 把学习率乘 0.1让训练后期用更小的步长做精细调优。CrossEntropyLoss内部已经包含了 softmax 操作所以模型输出的 logits 直接喂进去就行不要再手动加一次 softmax。训练过程中每 5 个 epoch 做一次验证目的是监控模型有没有过拟合。验证时一定记得切换成eval()模式把 dropout 和 batch normalization 的行为切换到推理状态否则验证集指标会偏高或偏低不稳定。4.3 loss 曲线和 accuracy 曲线怎么读实验报告用可视化方式展示了训练过程文字描述是「loss 曲线随着训练步数的增加而逐渐下降前期快、后期慢accuracy 逐渐提高并趋于稳定」。这两句话放在任何深度学习训练里都对但真正会读图的人看的不是整体趋势而是具体形态。loss 曲线前期快速下降说明预训练特征正在有效迁移后期缓慢下行是正常的因为网络在做精细调整。但如果 loss 曲线在第 10 个 epoch 之后出现「断崖式下降再回升」大概率是学习率太大导致参数越过最优点在振荡中回升。如果 loss 曲线基本水平验证集 accuracy 也没显著提升那就是学习率太小训练陷入停滞。验证集 accuracy 曲线后期趋平有两种可能一是模型确实收敛到了性能上限二是已经过拟合继续训练只会让训练集精度继续上升、验证集精度不再变化甚至下降。分辨两者的方法很简单——把训练集精度也算出来对比两条曲线。如果训练集精度接近 100% 而验证集停在 70%过拟合实锤如果两条曲线都停在相近位置那就是模型容量到顶了该换网络或者加数据增强。5. 避坑细粒度训练里最容易翻车的四个现场细粒度分类的坑和普通分类任务不太一样普通任务翻车大多是数据预处理的问题细粒度则更多出在「模型根本没在学目标特征」和「训练过程不稳定」这两类问题上。下面四个场景都是我复现时真实遇到过的按「现象 → 原因 → 解决」逐个说。5.1 数据侧的坑精度卡死不下降问题出在输入上第一个现象很典型训练了 20 个 epoch验证集 accuracy 一直卡在 50% 上下loss 也降不下去。一开始我以为是网络结构改错了后来发现原因很简单——我没有先用 bbox 裁剪直接把整张原图 resize 到 224×224 送进网络。CUB 的原始图片背景占比太高有些鸟在图里只占 30% 的像素背景的树干、草地、天空占了大部分模型学到的全是「有蓝天就是某种鸟」的捷径根本没在学鸟本身。解决方法是回到第 3.3 节的BBoxCrop先按标注框裁剪出鸟主体再送进增强管线。这一步改完之后同样参数下验证集精度立刻跳了 10 个点以上。第二个现象是某几个类别在验证集上 accuracy 恒为 0而且每次固定是那几个类。查数据后发现CUB-200-2011 虽然官方说每类约 60 张但实际个别类别只有 40 张出头训练集按 6:2:2 划分后只剩 25 张左右其中还有几张被随机裁剪增强后变得几乎不可辨认模型对这几个类的特征覆盖不足。解决方法是给 DataLoader 按类别权重做加权采样让样本少的类别在每个 epoch 里被采样更多次。具体做法是用WeightedRandomSampler权重设置为1 / 类别样本数这样样本量少的类每轮被抽到的概率更高。5.2 训练侧的坑loss 变 nan 和验证集不涨第三个现象是训练到中途 loss 突然变成nan精度直接清零。原因是新替换的输出层参数方差过大加上初始学习率较高前向传播经过最后一个全连接层时数值溢出。输出层Linear(512, 200)默认初始化是按均匀分布取的方差跟输入维度有关如果主干网络输出的特征值偏大几个 batch 之后梯度爆炸就发生了。解决方法是给输出层单独做 Kaiming 初始化或者把输出层的学习率单独调小到 5e-4。我习惯在替换完输出层之后先用一个小 batch 跑一次 forward 和 backward观察 loss 是否是有限值有异常趁早调别等训练到第 20 个 epoch 才爆。第四个现象是训练集 loss 持续下降但验证集 accuracy 纹丝不动。这种情况我先看训练集 accuracy如果训练集已经超过 95% 而验证集只有 60%就是过拟合。CUB 每类样本太少模型参数又多过拟合几乎是必然趋势。解决方式按优先级排先把weight_decay从 1e-4 提高到 5e-4再把RandomResizedCrop的scale下限从 0.8 降到 0.5增强尺度多样性最后加上Mixup增强alpha 取 0.2把不同类别的样本按比例混合让模型不能只记住训练样本的精确外观。如果这三步做完验证集还是不动那就不是过拟合问题而是模型容量不够应该换 ResNet-50 而不是继续调参。6. 进阶验证用 Grad-CAM 看模型到底在关注什么训练完模型、精度也达标了但「精度高」不等于「模型学会了正确答案」。细粒度分类有个经典风险模型可能靠背景线索把类别分对了而不是靠鸟本身的特征。要验证这一点最直接的方法是用 Grad-CAM 对验证集图片做热力图可视化看模型的注意力集中在哪里。Grad-CAM 的核心逻辑很简单取网络最后一个卷积层的输出特征图用类别得分对特征图通道做梯度加权得到一个空间热力图。热力图亮的位置就是模型做出该分类决策时重点关注的区域。对于 200 类鸟类识别如果热力图的高亮区域集中在鸟的头部、翅膀、腹部说明模型学到了生物特征如果高亮跑到背景的树枝或者地面上说明模型捡了便宜这模型换个背景就会失效。实现一个简化的 Grad-CAM 不需要复杂的封装PyTorch 的 hook 机制几十行就能搞定import torch import torch.nn.functional as F def grad_cam(model, img_tensor, label): model.eval() features {} gradient {} # 注册 hook捕获最后一个卷积层的输出和梯度 def forward_hook(module, input, output): features[value] output def backward_hook(module, grad_input, grad_output): gradient[value] grad_output[0] target_layer model.layer4[-1] # ResNet-18 最后一个 BasicBlock handler_f target_layer.register_forward_hook(forward_hook) handler_b target_layer.register_full_backward_hook(backward_hook) output model(img_tensor.unsqueeze(0)) score output[0, label] model.zero_grad() score.backward() weights gradient[value].mean(dim(2, 3), keepdimTrue) # 全局平均池化 cam (weights * features[value]).sum(dim1, keepdimTrue) cam F.relu(cam).squeeze().detach().cpu().numpy() # 归一化到 0-1 用于叠加显示 cam (cam - cam.min()) / (cam.max() - cam.min() 1e-6) handler_f.remove() handler_b.remove() return camregister_forward_hook拿最后卷积层的输出特征图register_full_backward_hook拿反向传播时的梯度。梯度的全局平均池化得到每个通道的权重乘上特征图再求和就得到类的空间响应。relu只保留正响应负响应对分类决策没有语义贡献。unsqueeze(0)把单张图片扩成 batch 维度前后向结束后记得移除 hook避免内存累积。做可视化时我一般会抽查每一个类别的 5 张验证集图片统一观察热力图分布。如果某个类别的高亮区域明显偏移到背景就把这类单独拉出来看原始图和 bbox 是否对齐必要时对这个类单独做更重的裁剪增强。从那以后我每次跑完细粒度分类实验都强制自己走一遍 Grad-CAM 可视化而不仅仅看 accuracy 数字。精度达标但注意力不集中在目标上这种模型上线等于裸奔——换了个环境就崩。养成这个习惯之后很多「看起来精度不错但泛化很差」的模型在我这里都会被拦下来重新调。希望帮到你。本文还有配套的精品资源点击获取