首页 / 资讯中心 / 文章详情

Food-11食物图像分类冲榜实战:迁移学习、K折集成与伪标签

Food-11食物图像分类冲榜实战:迁移学习、K折集成与伪标签 ★ FEATURED ARTICLE
1. 先搞清楚这个 hw3 到底在考什么1.1 任务设定与数据全貌hw3 这个作业做的是 11 类食物图像分类。数据集叫 Food-11官方给的三份切分是带标签训练集 9866 张、带标签验证集 3430 张、无标签测试集 3347 张原图统一是 512×512 的彩色图。类别从面包、乳制品、甜点、蛋类、油炸食品、肉类、面食、米饭、海鲜、汤、蔬果覆盖面广但类间边界很模糊——这是这个任务最恶心的地方汤和面食、甜点和面包、油炸食品和肉类光看缩略图人眼都容易错。所以它不像 MNIST 那种“跑通就有分”的玩具任务Boss baseline 的核心难点全在细粒度区分 数据量偏小 类别不平衡这三个坑上。我先说说数据量的直观感受。9866 张摊到 11 个类平均每类不到 900 张实际分布还很不均匀最多的类和最少的类能差出两三倍。这意味着你要是从头训一个大模型基本就是过拟合教科书训练集准确率冲到 95% 以上验证集卡在 60% 出头晃荡。这也是为什么整个作业的梯度线是从“自己搭 CNN”到“用预训练骨干 数据增强 集成”这么一路走上来的。Boss baseline 不是靠某一个 trick 突然打上去的是一层层叠加出来的工程结果。前置知识上你需要会 PyTorch 的基本训练循环、会用 torchvision 的 transforms、知道什么是迁移学习、看得懂混淆矩阵。如果连optimizer.step()在干什么都含糊建议先回去把前面的作业补完否则你会在调参的时候完全失去方向感只能靠瞎试。提示下文的分数区间是基于社区里流传的历年经验值用来做方向判断不是官方标准。每年公告的分档线可能微调动笔前一定去翻当期作业说明别把别人的数字当圣旨。1.2 四个档位的分水岭到底在哪课程一般把 baseline 分成 Simple / Medium / Strong / Boss 四档。按大部分年份的公开讨论大致可以这样理解Simple 大概是“模型能收敛、测试准确率过半”Medium 是“会做基本的增强和正则”Strong 通常要摸到 0.80 上下而 Boss 一般要 0.87 以上才算稳。这个数字不是我拍脑袋说的是历年提交分布的经验中位数但请务必以你自己的榜单为准。关键问题在于从 Strong 到 Boss 这最后几个点靠调学习率是调不出来的。我见过太多人卡在 0.82 附近反复改 lr、改 batch、改 dropout改了两周原地踏步。真正的分水岭在于三件事——有没有用上预训练权重、有没有做交叉验证集成、有没有把无标签数据榨干。这三件事任意一件没做你都很难突破 Strong 的天花板。另外要提前说清楚一个纪律问题作业规则通常允许你在训练阶段以无标签形式使用测试集图片半监督但绝对禁止用它的标签、也禁止拿测试集反复调超参调到“过拟合测试集”。这条线一旦踩过去分数再高也是虚的而且对你自己的技术成长毫无帮助。我在正文里讲伪标签的时候会专门强调这个边界怎么把握。2. 从零到 Boss 的方案选型思路2.1 为什么不该自己搭 CNN 打天下先说结论除非作业明确禁止预训练模型否则不要在自定义 CNN 上浪费超过一天。原因不是自定义网络不行而是数据量不配。9866 张图要撑起一个几百万参数的卷积网络参数量和样本量的比例严重失衡。你可以用 ResNet18 从随机初始化开始训做足了增强也能到 0.70 左右但再往上就是硬墙——模型没见过的纹理模式太多泛化能力上不去。而 ImageNet 预训练的骨干网络等于免费借给你一千多万张图学到的底层特征边缘、纹理、局部形状、材质感。食物分类恰恰高度依赖纹理和材质米饭的颗粒感、汤的液面反光、油炸食品的酥脆质地这些特征在 ImageNet 上学得非常充分。这就是迁移学习在这个任务上收益巨大的根本原因——不是玄学是特征分布的重叠度高。从工程角度看用预训练还能大幅压缩训练成本。冻结骨干只训分类头几个 epoch 就能看到明显效果再解冻做全量微调二三十个 epoch 就收敛。整个过程在一张消费级显卡上完全跑得动不需要什么集群。这对个人做作业来说是性价比最高的一条路。2.2 骨干网络与预训练权重怎么挑骨干选型我一般按这个顺序试ResNet50 → EfficientNet-B0/B3 → ConvNeXt-Tiny → ViT-B/16。别一上来就上最大的先跑通一个稳的再横向对比。骨干参数量单 epoch 耗时相对我的实测表现适用场景ResNet1811M1.0x0.83 左右快速验证管线是否正确ResNet5025M2.1x0.87 上下主力模型稳、好调EfficientNet-B05M0.8x0.86 左右显存紧张时的首选ConvNeXt-Tiny28M2.4x0.88 上下冲榜提分ViT-B/1686M4.5x不稳定依赖强增强有经验再碰ResNet50 是我最推荐的起点理由很实在结构简单、社区资料多、BN 层对小 batch 相对宽容、预训练权重质量稳定。EfficientNet 系列参数效率高但它的复合缩放带来的深度可分离卷积在小分辨率下有时候反而吃亏而且对学习率更敏感。ViT 这类 Transformer 结构在数据量小于一万的时候非常吃增强策略你要是没做过 MixUp/CutMix 那套组合拳很容易训崩。权重的来源优先用 torchvision 官方那套weightsIMAGENET1K_V2这类新版权重比老的 V1 普遍高一个点以上。如果你在别的模型库比如 timm里找权重注意看清楚是 ImageNet-1k 还是 ImageNet-21k 预训练后者在小数据集微调时通常更强但学习率要给得更小。2.3 训练策略的取舍微调深度怎么定这里有个很多人忽略的决策点全量微调还是分层微调。我的做法是两阶段。第一阶段冻结骨干所有层只训最后的全连接头学习率给 1e-3跑 3 到 5 个 epoch。这一步的目的是让随机初始化的分类头先收敛到一个合理的区域避免一上来就用大梯度把预训练权重冲烂。你可以把这理解成“先让新来的实习生熟悉业务别急着让他改公司制度”。第二阶段解冻全部层用差异化的学习率微调骨干部分 1e-4 到 3e-4分类头 1e-3。这个差距不是随便定的是因为预训练权重已经在一个很好的局部最优附近大学习率会直接把它踢出去而分类头是新的需要更大的步子。实测下来两阶段比直接全量微调能稳定高出 1 到 2 个点而且训练曲线平滑得多。还有一个取舍是输入分辨率。原始图是 512但训练用 224 是主流选择。原因有三预训练权重的归一化统计和感受野是在 224 上形成的直接上 512 会有分布偏移224 的显存和耗时都友好224 已经足够区分食物类别了。如果你最后想再抠一点分可以在训练末期把分辨率提到 320 做几个 epoch 的微调这叫 progressive resizing能白捡零点几个点但要注意 BN 的统计量需要重新校准。3. 数据管线把 DataLoader 调顺是一切的前提3.1 划分策略与防泄漏官方给的验证集可以拿来监控训练但我强烈建议你另外从训练集里切一份 holdout或者干脆做 5 折交叉验证。原因很直接验证集是你反复看指标的地方看得多了你就在无意中对着它调参它对你的泛化能力估计会越来越乐观。等你把模型交上去测试集分数掉下来你都不知道问题出在哪。我的做法是把 9866 张训练图按类别分层切成 5 折每折训 4 份验 1 份。分层很重要因为类别不平衡随机切很可能某一折里某个类只剩十几张那一折的验证指标就完全不可信了。分层切分用 sklearn 的StratifiedKFold一行搞定。防泄漏这块要单独强调。有几种常见的隐形泄漏同一张图的不同增强版本同时出现在训练和验证里如果你先做增强再切分就会这样数据集的文件夹里混进了重复图片以及最隐蔽的一种——你用验证集的指标去挑选“最佳 epoch”然后把那个 epoch 的模型拿去预测这本身是合法的但如果你反复在验证集上试了几十个配置验证集就变成了第二个训练集。注意切分必须在增强之前完成。顺序永远是读原始图片列表 → 分层切分 → 各自套 transform。3.2 增强组合与参数选择的底层逻辑增强不是越多越好每一个增强都要能对应到“这个类别的真实变化”。食物图像的真实变化有哪些拍摄角度、光照色温、盘子摆放位置、食物在画面中的占比、轻微遮挡。我的训练增强组合是这样的from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.65, 1.0), ratio(0.8, 1.25)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.25, contrast0.25, saturation0.25, hue0.05), transforms.RandomRotation(degrees10), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), transforms.RandomErasing(p0.25, scale(0.02, 0.12)), ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逐条解释一下为什么这么设。RandomResizedCrop的 scale 下限设 0.65 而不是默认的 0.08是因为食物分类需要看到整体形态你要是裁得只剩一小块酥皮模型根本判断不了这是炸鸡还是面包。0.65 到 1.0 这个区间对应的是“食物在画面里占 65% 到 100%”的真实拍摄差异合理。ColorJitter的 hue 只给 0.05这是刻意的。食物的颜色是有判别力的强特征——红色的番茄、黄色的咖喱、绿色的蔬菜。色相偏移给大了模型学到的颜色先验就被破坏了反而掉分。亮度和对比度可以给大一些因为餐厅灯光差异确实大。RandomErasing放在 Normalize 之后这是 torchvision 的硬性要求它对张量操作。p0.25 是个温和值模拟的是盘子边缘遮挡或者筷子挡住一部分的情况。给到 0.5 就过头了小数据集容易被它拖慢收敛。3.3 Dataset 实现里那些容易翻车的细节自定义 Dataset 本身很简单但有几个细节处理不好会直接影响最终分数。import os from PIL import Image from torch.utils.data import Dataset class FoodDataset(Dataset): def __init__(self, root, class_to_idx, transformNone): self.items [] self.transform transform for cls_name, label in class_to_idx.items(): folder os.path.join(root, cls_name) if not os.path.isdir(folder): continue for fn in os.listdir(folder): if fn.lower().endswith((.jpg, .jpeg, .png)): self.items.append((os.path.join(folder, fn), label)) def __len__(self): return len(self.items) def __getitem__(self, idx): path, label self.items[idx] img Image.open(path).convert(RGB) if self.transform is not None: img self.transform(img) return img, label第一个坑是convert(RGB)。这个数据集里混着少量灰度图和带 alpha 通道的 PNG不转的话 collate 的时候直接报维度不匹配。这个错误很典型但报错信息指向的是 DataLoader 内部新手往往要排查半天。第二个坑是num_workers。设成 4 到 8 通常能显著提速但在某些环境下尤其是容器里设大了会报共享内存错误。遇到BusDevice或者shared memory相关的报错先把 num_workers 降到 0 验证是不是这个原因再往上加。另外记得配pin_memoryTrue和persistent_workersTrue前者加速 CPU 到 GPU 的拷贝后者避免每个 epoch 重建进程。第三个坑是类别映射的一致性。训练时用的class_to_idx必须原封不动地保存下来推理时按同一份映射解码。我见过有人训练时按文件夹字母序映射推理时又按sorted(os.listdir())重新生成一遍结果因为大小写或者隐藏文件导致顺序错位分数直接崩到 0.1还以为是模型坏了。4. 训练流程的完整实现4.1 超参数配置与优化器选型优化器我只用 AdamW不用 SGD。这不是信仰问题是效率问题。SGD 在预训练模型微调上确实可能收敛到略好的解但它对学习率调度和 warmup 的敏感度高得多需要你花大量时间调。AdamW 对超参宽容默认配置就能给出接近最优的结果对做作业的人来说时间成本低太多。核心配置长这样import torch from torch import nn, optim model torchvision.models.resnet50(weightsIMAGENET1K_V2) model.fc nn.Linear(model.fc.in_features, 11) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer optim.AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: [p for n, p in model.named_parameters() if not n.startswith(fc.)], lr: 2e-4}, ], weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-6)几个参数背后的账算一下。label_smoothing0.1是把 one-hot 标签软化成 [0.9, 0.011, 0.011, ...]作用是抑制模型对训练样本过度自信。食物分类里有些图本身就标注得模棱两可比如一个带面包的汤硬标签会强迫模型给出极端概率软化之后泛化更好。这个改动几乎零成本收益稳定在零点几到一个点。weight_decay1e-4对微调来说是个偏保守的值。如果你发现训练 loss 降得很快但验证 loss 抬头可以加到 5e-4 甚至 1e-3如果模型欠拟合两边 loss 都高就降到 1e-5。注意 AdamW 的 weight decay 是解耦的和 Adam 里的 L2 正则不是一回事数值上可以放心给。batch size 我给 32。这个值的考量是太小则 BN 统计量不稳对 ResNet 影响明显太大则在 9866 张样本下一个 epoch 只有 300 来个 step收敛慢且泛化略差。32 到 64 之间都能接受显存允许就往上走。4.2 训练循环里的三个提分技巧标准训练循环人人会写我想说的是三个真正影响最终分数的实现细节。混合精度训练。用torch.cuda.amp把前向和反向的半精度算起来显存占用能省 30% 到 40%速度提升 20% 以上。省下来的显存你可以拿去开更大的 batch 或者更大的分辨率。写法上注意用 GradScaler 包住 loss 的缩放不然小梯度会直接下溢成 0from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for imgs, labels in loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad(set_to_noneTrue) with autocast(): loss criterion(model(imgs), labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()EMA指数移动平均。这个技巧在作业场景下性价比极高但很多人不知道。做法是维护一份模型权重的滑动平均验证和推理时用这份平均权重。因为训练后期的权重在最优解附近震荡平均一下相当于免费做了个小集成。decay 取 0.999 是个不错的起点训练步数多的话可以调到 0.9995。实测能稳定加 0.3 到 0.8 个点代码量不到二十行。梯度裁剪。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)。微调阶段偶尔会因为某几个难样本产生异常大的梯度把已经学好的权重带偏。加上这一行训练曲线会平滑很多尤其是用 MixUp 这类混合增强的时候。4.3 验证、保存与早停的正确姿势验证部分最容易出错的是忘记切 eval 模式。model.eval()和torch.no_grad()两个都要加前者关掉 dropout 并让 BN 用滑动统计量后者关掉计算图省显存。只加其中一个都会导致验证指标偏低且波动大很多人以为是模型不行其实是评估代码写错了。保存策略上我保存两份一份是验证集准确率最高的best_acc.pth一份是最后一个 epoch 的last.pth。这两份后面都有用——best 用于单模型提交last 用于配合 EMA 或者做权重平均SWA。早停的 patience 我给 8 个 epoch也就是连续 8 轮验证准确率没有提升就停。cosine 调度下其实不太需要早停但如果你的数据切分比较小早停能防止后期过拟合。日志记录建议至少记四列train_loss、train_acc、val_loss、val_acc。很多人只看 val_acc结果模型明明已经开始过拟合val_loss 连续上升却还在训。我的经验是 val_loss 是最灵敏的信号它比准确率早两三个 epoch 就开始报警。5. 冲榜的关键增量K 折、TTA 与伪标签5.1 五折交叉验证与权重融合单模型到 0.87 之后再往上几乎全靠集成。最省事的集成方式是 K 折把训练集切 5 份训 5 个模型推理时把 5 个模型的 softmax 输出取平均。这个操作通常能加 1 到 1.5 个点而且实现成本极低——你只是把同一个脚本跑五遍改一下 fold 编号。为什么要平均概率而不是平均 logits因为概率经过了 softmax各模型在同一量纲上取算术平均相当于投票而 logits 的尺度各模型不同直接平均会被某个尺度大的模型主导。如果要做更精细的融合可以给每折按验证准确率加权但一般提升有限不值得折腾。融合前记得检查一件事五折的验证准确率方差有多大。如果某一折明显低比如其他四折都在 0.88就它 0.83大概率是那一折切分时某些类样本太少导致训练不充分。这种情况下可以考虑把折数降到 4或者对稀有类做过采样。5.2 TTA 的正确用法与常见误用TTA测试时增强是把一张测试图做多种变换各自预测后取平均。听起来简单但用错了会掉分。正确的做法是选标签保持的变换。水平翻转是最安全的因为食物类别和左右方向无关。多尺度裁剪比如中心裁 224、裁 256 再 resize 到 224也安全因为食物的类别不依赖于精确的画面范围。颜色抖动要谨慎尤其是色相——前面说过食物颜色是有判别力的特征你把色相抖一圈再平均等于把模型的颜色判断给模糊掉了。def predict_tta(model, img_tensor, n_crop5): model.eval() probs [] with torch.no_grad(): probs.append(torch.softmax(model(img_tensor), dim1)) probs.append(torch.softmax(model(torch.flip(img_tensor, dims[3])), dim1)) # 多尺度 for scale in (0.85, 0.95): h, w img_tensor.shape[-2:] nh, nw int(h * scale), int(w * scale) crop img_tensor[..., :nh, :nw] crop torch.nn.functional.interpolate( crop, size(h, w), modebilinear, align_cornersFalse) probs.append(torch.softmax(model(crop), dim1)) return torch.stack(probs).mean(0)TTA 的收益一般在 0.3 到 0.7 个点不算大但是白拿。缺点是推理时间成倍增长——5 个视角就是 5 倍耗时。作业场景下测试集只有 3000 多张时间完全不是问题放心用。误用最典型的是“把 TTA 当调参手段”看到 TTA 加了 0.5 点就不断加各种花式变换想再加 0.5。加到七八种变换之后你会发现分数反而降了因为弱相关的增强在平均时引入了噪声。我的经验是 TTA 变换控制在 4 到 6 个视角比较合适。5.3 伪标签半监督的边界与操作方法这是从 Strong 冲到 Boss 最有效的一招也是最需要守规矩的一招。核心思路很简单先用有标签数据训一个好模型用它给无标签的测试图打软标签然后把置信度高的那些样本连同伪标签一起加入训练集重新训一轮。流程上我分三步走。第一步用前面所有手段训出五折模型在测试集上推理得到概率分布。第二步设定一个置信度阈值我一般取 0.85 到 0.9 之间的最大类概率把超过阈值的样本挑出来软标签直接用模型输出的概率分布而不是硬标签的 argmax——软标签能保留类别间的相对关系训练更稳。第三步把伪标签样本按大概 1:1 的比例混进真实训练集重新做一轮五折训练此时学习率要调小比如原来的一半训练 epoch 也减半因为数据变多了但质量参差。有个细节值得说伪标签的样本建议用软标签 KL 散度或直接用交叉熵配软目标不要用 argmax 转 one-hot。因为模型对很多样本是有把握但不确定的比如 0.7 汤、0.3 面食硬转标签会把这种不确定性抹掉等于引入了错误的高置信监督。注意这里有个明确的红线。伪标签用的是模型对无标签图片的预测绝不能是任何形式的真实标签也不能通过任何渠道去获取测试集的标签信息。作业通常允许“以无监督方式使用测试图片”但用标签就是作弊。这条线我自己是死守的因为一旦越过你后面的所有实验结论都失去意义了你也永远不知道自己真实水平在哪。另外要提醒的是伪标签这招不是必然有效。如果你第一轮模型本身只有 0.80打出来的伪标签错误率很高混进去反而是自污染效果可能比不用还差。经验判断标准是单模型或集成在验证集上稳定超过 0.87 再考虑伪标签低于这个线就别碰。6. 踩坑实录与排查速查表6.1 那些让我熬到凌晨的典型故障训练 loss 不降卡在 2.4 附近。交叉熵在 11 类均匀预测时就是 ln(11)≈2.4。出现这个数说明模型完全没学到东西。排查顺序先看数据有没有正常读进来随便抽 9 张画出来看确认不是全黑或全噪点再看标签和图片对不对得上把 batch 里的 label 解码成类别名和图片比对最后看分类头的输出维度是不是真的 11。我遇到过一次是num_classes写成了 10损失一直在 2.3 左右晃找了两个小时。验证准确率比训练准确率高出一大截。这个现象看着像好事其实往往是坏事。最常见原因是验证时忘了model.eval()dropout 还在随机丢弃神经元相当于每次预测都是不同子模型的投票反而更稳而训练时因为 dropou 存在准确率被压低了。另一可能是训练时的增强太强把训练集难度人为拉高了。先检查 eval 模式再检查增强强度。验证指标剧烈震荡相邻 epoch 能差 10 个点。小数据集 BN 层的经典问题。如果 batch size 小于 16BN 的 batch 统计量噪声极大直接导致性能不稳。解法有三个加大 batch、换成 GroupNorm、或者冻住 BN 层只训其他部分。我一般先用第三个改一行代码的事。提交分数远低于本地验证。这个最伤人。原因通常是本地验证集的分布和测试集不一致或者你的验证集被你调参调过拟合了。还有个隐蔽原因提交时预处理写错了比如忘了 Normalize或者用了 val_tf 之外的裁剪方式。建议提交前跑一遍“用训练集图片做推理”看看准确率是不是接近 100%如果不是说明推理管线和训练管线不一致。显存溢出但 batch 明明很小。检查是不是在验证时没加torch.no_grad()计算图累积起来了。另一个常见原因是把整个测试集一次性推理改成 64 张一批就没问题了。现象最可能原因快速验证方法处理方式loss 卡在 2.4类别数配错 / 数据未读入打印一个 batch 的 shape 和标签修正 num_classes 或路径验证 acc 高于训练 acc忘了 model.eval()加打印确认 training 标志补上 eval 和 no_grad指标剧烈震荡batch 太小 BN 不稳看 batch size 是否小于 16冻结 BN 或加大 batch提交远低于本地推理管线不一致用训练图做推理应近 100%统一 transform 配置后期 val_loss 上升过拟合对比 train/val loss 曲线加正则、早停、强增强伪标签后变差初始模型不够强看第一轮验证准确率回到 0.87 以上再做6.2 我自己总结的避坑清单第一永远先跑通再调优。上来就开五折 伪标签 TTA结果某个环节有 bug你连是哪一环出问题都定位不了。正确的顺序是单折小样本跑通 → 单折全量跑到收敛 → 加增强和正则 → 五折集成 → TTA → 伪标签。每一步都确认有正向收益再往下走。第二把随机种子固定死。torch.manual_seed、np.random.seed、random.seed三个都要设还要设torch.backends.cudnn.deterministic True。不然后面做 A/B 对比时你以为的“这个改动加了 0.8 点”可能只是随机波动。我踩过这个坑一次调了两周最后发现改动效果全在噪声范围内。第三验证集指标要分类型看。只看总体准确率会掩盖问题。打印混淆矩阵看哪些类之间互相混淆。食物任务里最常见的是面食 vs 米饭、甜点 vs 面包、汤 vs 面食这几对。看清楚了才知道该往哪个方向加数据或者加针对性的增强。第四不要在网上抄别人的超参直接套。别人的 lr 是在他的数据切分、他的 batch size、他的骨干网络上调出来的直接套到你的配置上大概率不是最优。要理解参数之间的联动关系batch size 翻倍lr 通常也要相应上调用了混合精度梯度缩放会影响可用的 lr 上限。第五留够推理和提交的时间。五折 TTA 的推理时间可能是单模型的十几倍别等到截止前两小时才开始跑。我习惯在训练阶段就把推理脚本写好一边训一边用中间 checkpoint 测一遍流程确保提交格式、输出文件命名、类别顺序全都对。最后分享一个我自己摸索出来的小技巧如果时间紧把力气优先花在数据质量的排查上而不是模型结构。这个数据集里图片质量参差不齐我清掉过一批明显重复和标注错误的样本单这一项就带来了接近一个点的提升——比我把骨干从 ResNet50 换成 ConvNeXt 的收益还大。花两个小时写个脚本检查文件大小异常、宽高比异常、以及用感知哈希找重复图回报率远超同等时间的调参。
阅读完成 · 觉得有帮助?
咨询建站