图像分类项目复现:从GitHub训练到C++部署全链路实践
简介这是一套基于GitHub开源项目的深度学习图像分类复现工程面向具备一定Python基础、希望快速搭建图像分类任务的中级开发者。整体包含222个文件压缩包约106.48MB其中97个Python脚本与111个pyc编译文件构成核心训练与推理逻辑csv文件保存分类统计结果pth为预训练权重ipynb提供特征可视化演示另附cpp、图片及说明文档便于二次开发调试。使用时可从数据集导入、模型加载、训练到评估完整走通简单修改路径与超参数即可运行配套博客对关键环节做了详细展开。已有2839人学习下载适合拿来对照开源项目理解图像分类的完整流程快速验证模型效果并迁移到自己的数据集上。1. 从GitHub复现图像分类这个项目把训练、可视化和C部署串在了一条链路上很多复现项目把训练脚本跑通就停了真正要落地时却卡在部署侧。这个GitHub项目不一样文件里既有训练和评估用的Feature_Visualization.ipynb也有main.cpp这个C推理入口再加上method_1/2/3.csv三个结果文件正好覆盖“训练产出模型—验证效果—C加载模型跑图片”的完整闭环。项目本身代码量不大网络结构也比较常规适合想弄清楚一条图像分类链路、并且需要把模型搬到C服务里的人。你不需要有多深的C底子只要跟着下面步骤把文件关系理顺再替换成自己的数据集就能把流程完整复现出来。2. 文件与数据流先看懂main.cpp和method_CSV再动手跑2.1 文件清单与各自分工文件类型在链路中的作用main.cppC源文件加载TorchScript模型对单张图片做分类推理method_1.csv / method_2.csv / method_3.csvCSV不同实验配置下的预测概率用于离线对比Feature_Visualization.ipynbJupyter Notebook训练/评估和特征图可视化Multi-class ROC.jpg图片多分类ROC曲线评估结果f1_conv1.png图片第一个卷积层输出的特征图从文件命名看三个method_CSV不是随机结果它们大概率是三种预处理或者三种训练配置在同一验证集上的softmax输出。这种命名方式在GitHub复现项目里很常见目的是不用每次重跑训练只要读CSV就能对比不同方案的效果。main.cpp则是部署端它不关心你训练时用了多少trick它只需要一个导出的TorchScript模型和一份与训练时完全一致的前处理逻辑。所以复现的第一步不是直接编译main.cpp而是先确定你到底要跑哪条链路如果想看模型效果先打开CSV和Notebook如果想复现部署先看README里model.pt的导出方式。不要一上来就g main.cpp否则你大概率会卡在torch/torch.h找不到这类问题上。2.2 method_CSV的结构和读取方式以method_1.csv为例多分类softmax概率按列存放常见格式是这样img_name,true_id,p0,p1,p2,p3 img_001.jpg,0,0.01,0.88,0.05,0.06 img_002.jpg,1,0.73,0.20,0.04,0.03其中true_id是整数标签p0到p3是模型对四个类别的预测概率。复现时建议直接用pandas读进来先确认列名和排序是否与Notebook里一致import pandas as pd df pd.read_csv(method_1.csv) print(df.shape) print(df.head())逻辑说明pandas默认把第一行当作列名行数就是样本数后面画ROC和计算AUC直接使用p0~p3列即可。参数说明如果CSV是用分号分隔需要在pd.read_csv里传sep;如果第一列是id而不是文件名保留它但不作为特征列。还要检查true_id的取值个数是否等于类别数如果缺少某个类说明测试集不完整需要回到Notebook重新采样。2.3 main.cpp 在整个流程中的位置main.cpp 通常不承担训练任务它只是把TorchScript模型作为输入对单张图片做前向推理。容易混淆的是CSV里记录的是验证集的多分类概率而main.cpp里往往只有单张图片的输出两者评估方式不同。我拆项目时一般先读CSV判断哪个方法更稳再把对应的模型导出成TorchScript最后才编译main.cpp。为了保证部署效果C端的前处理必须和训练端测试时保持一致。常见的是这段OpenCVLibTorch预处理torch::Tensor preprocess(const cv::Mat img, const cv::Size size) { cv::Mat resized; cv::resize(img, resized, size, 0, 0, cv::INTER_LINEAR); cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB); cv::Mat float_img; resized.convertTo(float_img, CV_32FC3, 1.0 / 255.0); torch::Tensor tensor torch::from_blob( float_img.data, {1, size.height, size.width, 3}, torch::kFloat); tensor tensor.permute({0, 3, 1, 2}).contiguous(); return tensor; }逻辑说明OpenCV读进来的图像是BGR通道顺序和HWC排布这里先resize到模型输入尺寸再转换到RGB然后归一化到0到1。torch::from_blob不会复制数据所以必须使用contiguous()保证内存连续否则permute后取到的数据是乱序的。参数说明size应和训练时的resize尺寸一致比如ResNet常用的224×224INTER_LINEAR是双线性插值PyTorch里的Resize默认用的也是双线性改成这个可以减少部署和训练之间的精度差异。如果训练时做过减均值除方差这部分也要补上具体在第4章展开。2.4 为什么要有三个method_CSV三个CSV的存在说明作者做了对照实验。常见做法是method_1保持基础Resizemethod_2加入随机翻转和颜色抖动method_3做测试时增强。你没有必要把三个网络都重新训练可以直接读CSV对比平均概率熵、误分类分布选出最好的方案后再决定导出哪个模型。如果你只关心部署那就跳过method_2和method_3直接看与method_1对齐的模型文件。读CSV时如果发现某一类的概率始终很低优先怀疑类别索引错位而不是网络结构问题。3. 复现训练与评估从Notebook到多分类ROC和f1_conv1特征图3.1 用Notebook代替训练脚本的好处Feature_Visualization.ipynb把数据加载、训练和可视化放在同一个文件里最大的好处是每一步中间结果都能看到。对于复现项目来说这比纯训练脚本更容易发现数据加载错误。例如CSV里的true_id从0开始而图片文件夹编号从1开始如果不做减1操作训练loss会一直不降。用Notebook可以在训练前先打印一批锚点检查类别索引是否对齐。常见做法是在Notebook里先定义一组预处理方式不同method对应不同策略method_标记常见预处理差异使用场景method_1.csv直接Resize到224快速验证基线method_2.csvRandomCropFlip轻微数据增强method_3.csv多尺度TTA测试时增强打榜用如果你发现method_2的AUC比method_1高说明你的数据集有轻度过拟合空间如果反而低考虑增强强度过大。TTA虽然能提升指标但部署时会增加推理时间生产环境要慎用。3.2 生成Multi-class ROC.jpg 的代码多分类ROC的正确做法是one-vs-rest每个类别单独拉一条曲线。直接从method_1.csv里读概率和真实标签复用项目里的Multi-class ROC.jpg即可from sklearn.metrics import roc_curve, auc import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(method_1.csv) y_true df[true_id].values y_score df[[fp{i} for i in range(4)]].values plt.figure(figsize(8, 6)) for i in range(4): fpr, tpr, _ roc_curve((y_true i).astype(int), y_score[:, i]) roc_auc auc(fpr, tpr) plt.plot(fpr, tpr, labelfclass {i} AUC{roc_auc:.3f}) plt.plot([0, 1], [0, 1], k--) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend(loclower right) plt.savefig(Multi-class ROC.jpg, dpi150)逻辑说明roc_curve输入的正负标签必须是二分类所以用(y_true i)生成当前类的正样本掩码其余类统一当负样本。分母是当前类的样本数如果某个类样本太少曲线会显得很锯齿需要回到前面检查样本分布。参数说明y_score必须是非负概率值如果method_csv里保存的是未归一化得分要先做softmax再画图AUC对概率的绝对大小不敏感但logits和softmax产生的排序可能不同最终ROC会不一样。如果你看到某条ROC曲线明显低于对角线不要急着调模型先去验证集里找对应类别的图片是否标签错乱尤其注意类别编号是否从0开始。3.3 用hook导出f1_conv1.png第一个卷积层输出的特征图最能反映模型在学习什么。如果特征图基本是噪声通常是学习率太大如果特征图大部分响应集中在边缘轮廓说明模型当前在学形状纹理。复现用的f1_conv1.png可以用一个forward hook抓出来import torch from PIL import Image import torchvision.transforms as T conv1_outs [] def hook_fn(module, inputs, outputs): conv1_outs.append(outputs.detach()) model.conv1.register_forward_hook(hook_fn) transform T.Compose([T.Resize((224, 224)), T.ToTensor()]) img transform(Image.open(sample.jpg).convert(RGB)).unsqueeze(0) with torch.no_grad(): model(img) feats conv1_outs[0][0] # shape [C, H, W]逻辑说明hook不会改动前向结果只把第一层输出记录下来。conv1_outs[0]是batch维取第0张图后面把它切片成网格图就能拼出f1_conv1.png。参数说明model.conv1要和实际网络里的模块名一致如果你的网络写成了features[0]就要改成model.features[0].register_forward_hook。保存图片前要把每个通道单独归一化到0到255否则像素值可能都在0到1附近画出来全黑。3.4 从CSV反推训练配置没有作者原始配置时通过CSV的概率分布能反推。如果method_1.csv的概率绝大多数在0.99以上说明模型过拟合或者测试时没有加Dropout如果三个CSV之间的同一张图输出差异很大说明你的图片加载顺序和原项目不一致。最简单的办法是选一个明显有歧义的样图分别跑三个method的CSV观察概率列的变化。如果三个CSV里的true_id都不一致基本可以断定是数据shuffle顺序不同需要以Feature_Visualization.ipynb里的数据集类为准。4. C部署踩坑让main.cpp跑出和PyTorch一样的结果4.1 main.cpp 的完整骨架进入部署环节。main.cpp的职责很单一把图像从文件读到内存预处理走模型输出top1类别和概率。一个可用版本如下#include torch/script.h #include opencv2/opencv.hpp #include iostream torch::Tensor preprocess(const cv::Mat img, const cv::Size size) { cv::Mat resized; cv::resize(img, resized, size, 0, 0, cv::INTER_LINEAR); cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB); cv::Mat float_img; resized.convertTo(float_img, CV_32FC3, 1.0 / 255.0); torch::Tensor tensor torch::from_blob( float_img.data, {1, size.height, size.width, 3}, torch::kFloat); tensor tensor.permute({0, 3, 1, 2}).contiguous(); tensor (tensor - torch::tensor({0.485f, 0.456f, 0.406f}).view({3, 1, 1})) / torch::tensor({0.229f, 0.224f, 0.225f}).view({3, 1, 1}); return tensor; }逻辑说明这是标准的ImageNet归一化mean和std来自PyTorch预训练模型的标准配置。torch::tensor({0.485f, 0.456f, 0.406f})创建了一个3元素张量用view({3,1,1})变成可以按通道广播的shape。参数说明如果你的训练代码里没有减均值除方差这里要删掉否则相当于把输入又变了一次AUC会掉。resize插值方式建议和训练保持一致训练里如果用了双三次这边也要换成INTER_CUBIC。接着是main函数int main(int argc, char** argv) { if (argc 3) { std::cerr Usage: ./classifier model.pt image\n; return -1; } torch::jit::script::Module module; try { module torch::jit::load(argv[1]); } catch (const c10::Error e) { std::cerr failed to load model\n; return -1; } cv::Mat img cv::imread(argv[2]); if (img.empty()) { std::cerr failed to read image\n; return -1; } torch::NoGradGuard no_grad; torch::Tensor input preprocess(img, {224, 224}); torch::Tensor output module.forward({input}).toTensor(); torch::Tensor probs torch::softmax(output, 1); auto accessor probs.accessorfloat, 2(); int max_idx 0; for (int i 1; i probs.size(1); i) { if (accessor[0][i] accessor[0][max_idx]) max_idx i; } std::cout class max_idx prob accessor[0][max_idx] \n; return 0; }逻辑说明先加载模型读图然后NoGradGuard确保推理不保存梯度。module.forward({input})返回IValue需要toTensor()拿输出再softmax成概率。accessor是高性能原始指针访问但只适用于CPU张量如果你把模型放到了GPU在输出上要先调用.cpu()。参数说明probs.size(1)就是类别数如果类别数和你训练时不一致说明模型文件导错了。4.2 容易忽略的channel order和内存对齐很多复现项目里model.pt在Python端跑得很好但main.cpp一进来就全错原因是预处理阶段少转了通道。OpenCV默认读成BGR而PyTorch模型按RGB训练。如果漏掉cvtColor模型的top1准确率会显著下降尤其是对颜色敏感的数据集。另外torch::from_blob要求数据在内存里连续而OpenCV的Mat行之间有padding不过convertTo之后通常已经是连续内存为了保险最好调用float_img.isContinuous()判断。如果返回false用float_img.clone()复制一份再传。4.3 编译命令和CMake配置编译LibTorch程序时通常使用CMake最小配置如下cmake_minimum_required(VERSION 3.10) project(classifier) set(CMAKE_CXX_STANDARD 17) set(CMAKE_PREFIX_PATH /path/to/libtorch) find_package(Torch REQUIRED) find_package(OpenCV REQUIRED) add_executable(classifier main.cpp) target_link_libraries(classifier ${TORCH_LIBRARIES} ${OpenCV_LIBS})编译命令mkdir -p build cd build cmake .. make -j4 ./classifier ../model.pt ../test.jpg逻辑说明CMAKE_PREFIX_PATH要让CMake能找到LibTorch的cmake配置文件TORCH_LIBRARIES包含了libtorch和c10的所有依赖。参数说明如果你的libtorch是CPU版不需要额外链接CUDA如果训练时有GPU算子部署机只有CPU记得在Python导出模型时用map_locationcpu加载权重否则C端会出现operator not found。4.4 部署精度下降时的对照表现象可能原因处理方式每个类输出概率几乎相等输入归一化数据范围不对确认训练时是0~1还是用mean/std归一化top1老是在两类之间跳跃resize后没有做相同比例裁剪训练里用RandomCrop推理就要用CenterCrop和Python输出差3%以上BGR/RGB顺序不一致加cvtColor(COLOR_BGR2RGB)编译报错undefined symbolLibTorch版本与训练torch版本不符换成与训练时相同的release版CPU推理极慢模型里存在动态控制流用torch.jit.script重写网络或固定输入尺寸这里建议复现时在Python端先用TorchScript跑同一张图保存softmax输出然后在main.cpp里跑同样图比对数值。误差通常在1e-5以内超过这个量级就去查预处理。5. 进阶调试技巧用特征图差异定位前后处理不一致5.1 把f1_conv1变成部署端的测试指标如果你想确认main.cpp里的预处理和训练端完全一致一个直观的方法是比对第一个卷积层的特征图。Feature_Visualization.ipynb里已经生成了f1_conv1.png你可以在Python端把特征图原始值保存为二进制feats.cpu().numpy().tofile(conv1_feats.bin)然后在C部署端修改main.cpp用与训练端相同的输入图片拿到第一层输出再和bin文件比对。更简单的做法是对比最终输出概率。如果PyTorch端和C端输出几乎一样那么预处理基本没有偏差如果差很多我一般会在preprocess的返回值前后打印tensor的mean和sum判断归一化是否生效。逻辑说明特征图比对的本质是验证两个框架里同一张图片经过完全相同的卷积计算后中间结果是否一致。参数说明保存bin时一定要用feats.cpu().numpy().tofile否则GPU显存里的张量无法直接转numpyC端读取时用fread按float读取即可。有一次训练采用Resize(320)CenterCrop(224)部署时误写成直接Resize(224)最终softmax输出第一类概率比Python端高0.03AUC从0.97掉到0.95。所以别小看这一步。5.2 用method_CSV找错误标注method_1.csv包含每个样本的概率值和真实标签如果某个样本的真实标签概率一直很低而另一个相似类别的概率很高那大概率是原始标注错了。可以写一段极短的Python扫描df[confidence] df[[fp{i} for i in range(4)]].max(axis1) low_conf df[df[confidence] 0.6].sort_values(confidence)逻辑说明低置信度样本往往是错误标注或模糊样本人工检查这些样本比检查全部样本更快。参数说明0.6是经验阈值如果你的模型的AUC很高可以放宽到0.7如果AUC偏低建议降到0.4。把这些样本导出为另一个CSV再回到Feature_Visualization.ipynb里做可视化能更快找到数据集问题。这种方法不仅适用于这个项目的三个CSV任何带预测概率的输出文件都能这么用。如果验证集上的最优阈值不在0.5记得调整main.cpp里的取值范围用从Multi-class ROC.jpg上选出的点作为最终部署口径。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →