
简介这份资源是面向深度学习入门者与计算机视觉学习者的LeNet人民币二分类实战数据集聚焦于用经典卷积神经网络区分不同面额的人民币图像适合想通过完整项目理解CNN训练流程的读者。压缩包为zip格式整体约62.1MB文件总数与类型明细上游暂未提供从描述看应包含经过尺寸归一化、灰度化等预处理的人民币图像数据可直接用于模型输入。目前已有452人学习下载具备一定参考热度。借助该数据集读者可完整走通数据划分、卷积与池化特征提取、全连接分类、反向传播与梯度下降权重更新等环节并在验证阶段通过准确率、精确率、召回率和F1分数监控过拟合在测试阶段评估模型泛化能力从而掌握LeNet在真实二分类任务中的落地思路与调参排错方法。1. 从 RMB_train.zip 说起二分类任务里最容易被低估的“脏活”如果你手头正压着一个二分类项目数据清洗做完了特征也凑得七七八八却在“训练脚本怎么写才不翻车”这一步卡住那 RMB_train.zip 这个包值得你花十分钟拆开看看。它不是什么框架级的大工程而是一套围绕二分类任务组织起来的训练资源包里面通常包含数据读取、模型定义、训练循环、评估指标这几块最核心的骨架。很多人一上来就急着调参结果 loss 不降反升回头一看是标签编码错了、正负样本比例没处理、验证集切分时数据泄漏——这些坑恰恰是这类训练包想帮你提前趟平的。它适合两类人一是刚接触二分类、想找一个能跑通的最小闭环的新手二是做过几个项目、但每次都要重新搭训练脚手架、想找个稳定模板的老手。下面我就按“这东西怎么用、参数怎么设、哪里容易翻车”的顺序把它拆开讲透。2. 拆开 RMB_train.zip二分类训练骨架的四个核心模块2.1 数据读取与标签对齐别让第一行就埋雷二分类任务里数据读取看似简单实则最容易出玄学问题。常见做法是先把数据整理成两列一列是特征向量或文件路径一列是 0/1 标签。RMB_train.zip 里的读取逻辑一般会封装成一个 Dataset 类核心是保证__getitem__返回的样本和标签严格一一对应。我一般会先跑一遍全量扫描确认没有空文件、没有标签缺失、没有路径大小写不一致的问题。下面这段代码是典型的读取与校验逻辑你可以直接抄进自己的项目里改路径。import os import pandas as pd from PIL import Image from torch.utils.data import Dataset class BinaryDataset(Dataset): def __init__(self, csv_file, img_root, transformNone): # csv_file 至少包含两列filename, label self.df pd.read_csv(csv_file) self.img_root img_root self.transform transform # 强制校验标签只能是 0 或 1 assert set(self.df[label].unique()).issubset({0, 1}), 标签必须为 0/1 # 校验文件是否存在避免训练中途才报错 missing [f for f in self.df[filename] if not os.path.exists(os.path.join(img_root, f))] if missing: raise FileNotFoundError(f缺失 {len(missing)} 个文件示例{missing[:3]}) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_root, row[filename]) img Image.open(img_path).convert(RGB) label int(row[label]) if self.transform: img self.transform(img) return img, label这段代码的关键点有三个一是assert标签集合防止有人把 -1/1 或者 1/2 的标签直接塞进来二是提前检查文件存在性避免训练到一半才崩三是convert(RGB)强制三通道防止灰度图混进来导致维度不匹配。参数上csv_file的列名可以按你的实际数据改但标签列建议统一叫label后面训练脚本里就不用反复改字段名。如果你用的是表格型特征而不是图像把Image.open换成pd.read_csv或numpy.load即可校验逻辑不变。2.2 模型定义与损失函数二分类不是“加个 sigmoid”就完事二分类的模型结构通常不复杂但损失函数和输出层的搭配经常被搞混。RMB_train.zip 里一般会给出两种常见写法一种是输出单个 logit配合BCEWithLogitsLoss另一种是输出两个 logit配合CrossEntropyLoss。前者数值更稳定后者在多分类扩展时更顺手。我一般会优先用BCEWithLogitsLoss因为它把 sigmoid 和 BCE 合在一起避免了手动 sigmoid 后再取 log 导致的数值溢出。下面是一个最小模型定义和损失函数配置的示例。import torch import torch.nn as nn class BinaryClassifier(nn.Module): def __init__(self, input_dim512, hidden_dim256): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), # 防止小数据集过拟合 nn.Linear(hidden_dim, 1) # 输出单个 logit不加 sigmoid ) def forward(self, x): return self.net(x).squeeze(-1) # 形状从 [B,1] 变成 [B] # 损失函数BCEWithLogitsLoss 内部自带 sigmoid criterion nn.BCEWithLogitsLoss(pos_weighttorch.tensor([3.0]))这里pos_weight是正样本权重当正负样本比例失衡时比如 1:10把它设成负正样本比的倒数附近能明显缓解模型“全预测成负类”的问题。Dropout(0.3)是我在小数据集上的习惯值数据量上万后可以降到 0.1 或去掉。注意forward里的squeeze(-1)如果忘了这一步后面算 loss 时形状对不上报错信息往往指向 loss 函数容易误判成损失函数用错。如果你坚持用CrossEntropyLoss那就把最后一行改成nn.Linear(hidden_dim, 2)标签保持 0/1 不变但记得不要再加 softmax因为CrossEntropyLoss内部已经包含了。2.3 训练循环与验证策略早停和阈值搜索要一起做训练循环本身不难难的是什么时候停、用什么阈值判断正类。很多人训练完直接取 0.5 作为阈值结果在失衡数据上 F1 惨不忍睹。RMB_train.zip 里通常会带一个验证集评估函数我建议你在训练循环里同时记录验证集的 loss 和 AUC并保存 AUC 最高那一轮的权重。下面是一个带早停和阈值搜索的训练骨架。import numpy as np from sklearn.metrics import roc_auc_score, f1_score def evaluate(model, loader, criterion, device): model.eval() all_probs, all_labels [], [] total_loss 0.0 with torch.no_grad(): for x, y in loader: x, y x.to(device), y.float().to(device) logits model(x) loss criterion(logits, y) total_loss loss.item() probs torch.sigmoid(logits).cpu().numpy() all_probs.extend(probs) all_labels.extend(y.cpu().numpy()) auc roc_auc_score(all_labels, all_probs) # 在验证集上搜索最佳阈值步长 0.01 best_f1, best_thr 0, 0.5 for thr in np.arange(0.1, 0.9, 0.01): preds (np.array(all_probs) thr).astype(int) f1 f1_score(all_labels, preds) if f1 best_f1: best_f1, best_thr f1, thr return total_loss / len(loader), auc, best_f1, best_thr这段代码里roc_auc_score不依赖阈值适合用来判断模型排序能力f1_score依赖阈值所以我在验证集上从 0.1 到 0.9 扫一遍取 F1 最高的阈值。实际部署时这个阈值要固定下来不能每次预测都重新搜。早停策略我一般设 patience5即验证 AUC 连续 5 轮不提升就停同时保存最佳权重。注意y.float()这一步如果标签是 int64BCEWithLogitsLoss会报类型错误转成 float 是最省事的做法。2.4 类别失衡与评估指标准确率是二分类里最会骗人的数字二分类任务里准确率在失衡数据上几乎没有参考价值。比如负样本占 95%模型全预测成负类也能拿 95% 准确率但正类一个都没抓到。RMB_train.zip 里如果带了评估脚本通常会包含 precision、recall、F1 和 AUC 这几项。我一般会先看 recall再看 precision最后用 F1 做综合判断。处理失衡的常见做法有三种一是损失函数里设pos_weight二是采样时用WeightedRandomSampler给正样本更高权重三是数据增强时只对正样本做。下面是一个WeightedRandomSampler的配置示例。from torch.utils.data import WeightedRandomSampler # 假设 labels 是全部训练样本的标签列表 labels train_dataset.df[label].values class_counts np.bincount(labels) # [负样本数, 正样本数] class_weights 1.0 / class_counts # 越少样本权重越高 sample_weights class_weights[labels] # 每个样本对应权重 sampler WeightedRandomSampler( weightstorch.DoubleTensor(sample_weights), num_sampleslen(sample_weights), replacementTrue ) # DataLoader 里传入 sampler不要再设 shuffleTrue train_loader DataLoader(train_dataset, batch_size32, samplersampler)这里replacementTrue表示有放回采样正样本会被反复抽到从而达到平衡效果。注意用了sampler之后shuffle必须为 False否则 PyTorch 会报冲突错误。num_samples一般设成训练集总样本数保证每个 epoch 看到的样本量和原来一致。如果你同时用了pos_weight和WeightedRandomSampler相当于双重加权可能会导致正样本过拟合建议只选一种。我一般先试pos_weight效果不明显再上 sampler。3. 避坑与排查二分类训练里最常见的五条血泪经验3.1 现象loss 一直不降准确率卡在多数类比例原因标签编码错误比如把正类标成 2或者把标签读成了字符串0/1模型实际在学一个恒等映射。解决在 Dataset 里强制int(row[label])并加assert set(labels).issubset({0,1})。如果标签是字符串先做映射再进 Dataset。3.2 现象验证集 AUC 很高但上线后效果崩了原因训练集和验证集切分时没有按样本 ID 分组同一个实体的不同样本同时出现在两边造成数据泄漏。解决用GroupShuffleSplit按实体 ID 切分或者手动按时间切分。RMB_train.zip 里如果没带切分脚本自己补一个分组切分逻辑别直接用train_test_split。3.3 现象训练到一半报 CUDA out of memory原因batch_size设得太大或者验证时没有加torch.no_grad()导致计算图一直累积。解决先把batch_size减半确认验证函数里加了with torch.no_grad():。如果还不行检查是否有中间变量被意外保留在 GPU 上比如把 loss 累加到了列表里而不是.item()。3.4 现象F1 分数波动很大每次跑结果都不一样原因没有固定随机种子或者DataLoader的num_workers导致采样顺序不可复现。解决在训练脚本开头固定torch.manual_seed(42)、np.random.seed(42)、random.seed(42)并把num_workers设为 0 做一次对照。如果必须用多进程在worker_init_fn里设置种子。3.5 现象正样本 recall 极低但 precision 很高原因阈值设得过高或者pos_weight设得太小。解决先跑一遍验证集阈值搜索看最佳阈值是不是远低于 0.5如果是说明模型对正类的置信度整体偏低适当提高pos_weight或改用WeightedRandomSampler。同时检查正样本是否在数据增强时被过度变换导致特征失真。4. 进阶技巧用校准曲线和阈值固化把二分类模型真正落地训练完一个二分类模型拿到 AUC 0.9 并不代表可以直接上线。我见过太多项目在验证集上 F1 很好看一到线上就翻车核心原因有两个一是概率没校准模型输出的 0.8 并不代表 80% 的置信度二是阈值没有固化每次预测都动态调整导致结果不可复现。RMB_train.zip 里的训练骨架通常只到评估为止落地这一步需要你自己补上。我一般会做两件事先画校准曲线再用验证集确定一个固定阈值写进配置文件。校准曲线用sklearn.calibration.calibration_curve就能画核心是看模型输出的概率和实际正类比例是否接近。如果偏离很大可以用CalibratedClassifierCV做 Platt scaling 或 isotonic 校准。下面是一个简单的校准和阈值固化流程。from sklearn.calibration import calibration_curve from sklearn.isotonic import IsotonicRegression import json # 假设 val_probs 是验证集预测概率val_labels 是真实标签 prob_true, prob_pred calibration_curve(val_labels, val_probs, n_bins10) # 如果 prob_true 和 prob_pred 偏离明显用 isotonic 回归校准 iso_reg IsotonicRegression(out_of_boundsclip) iso_reg.fit(val_probs, val_labels) calibrated_probs iso_reg.predict(val_probs) # 在校准后的概率上重新搜索最佳阈值 best_f1, best_thr 0, 0.5 for thr in np.arange(0.1, 0.9, 0.01): preds (calibrated_probs thr).astype(int) f1 f1_score(val_labels, preds) if f1 best_f1: best_f1, best_thr f1, thr # 把阈值和校准器参数固化到 json推理时直接加载 config {threshold: float(best_thr), calibration: isotonic} with open(deploy_config.json, w) as f: json.dump(config, f) print(f固化阈值{best_thr:.2f}验证集 F1{best_f1:.4f})这段代码的关键在于校准后的概率更接近真实置信度阈值搜索才有意义。IsotonicRegression适合样本量较大的情况样本少时用methodsigmoid的 Platt scaling 更稳。固化到 JSON 后推理脚本里直接读threshold不要再手动改。我一般还会在推理脚本里加一行日志记录当前使用的阈值和校准方式方便回滚。还有一个容易被忽略的点二分类模型的输出概率在业务上往往需要分档处理比如低于 0.3 判负、0.3 到 0.7 转人工、高于 0.7 判正。这种场景下校准曲线比 F1 更重要因为分档边界直接依赖概率的准确性。我习惯在验证集上同时看校准曲线和 F1如果两者冲突优先保证校准质量再在分档边界上做微调。从那以后我每次训练完二分类模型都强制走一遍“校准曲线 阈值固化 配置文件落盘”的流程哪怕验证集指标已经很好看。这个习惯帮我省掉了至少三次上线后的紧急回滚。希望帮到你。本文还有配套的精品资源点击获取