
简介这是一份基于Visual Studio 2013开发的C#深度学习源码项目采用纯CPU运行方式演示深度学习的核心流程面向希望在Windows环境直接运行、不愿折腾Linux版第三方库配置的C#开发者。与网上常见的Linux移植源码不同本工程已将运行所需库文件全部打包只要装有VS2013即可打开编译并运行免去多天配置环境的困扰。压缩包共123个文件整体约6.11MB核心包括38个.cs源文件、26个dll运行库、7个XAML及其BAML界面资源另有一些配置文件、PDB调试符号和可执行程序层次清楚便于按源码、界面、依赖分类查看。目前已有1526人学习下载。通过运行示例并结合源码可以直观理解深度学习网络的层级组织、网络结构图绘制、节点状态展示、性能曲线监控等关键设计由于是纯CPU实现还能轻松单步调试适合把底层计算逻辑和界面交互对应起来学习也是一份轻量、完整且方便改动的C#与深度学习结合入门案例。 在C#生态里聊深度学习源码第一反应往往是“绕道走”。毕竟Python有PyTorch、TensorFlow社区资源几乎一边倒很多搞工控的老哥一听CNN、Transformer就默认那必须是Python的活儿。但实际项目里我见过太多做上位机、做厂内视觉检测、做设备自动化的团队软件底座是C#写的WinForms或WPF设备通讯走串口、网口、Modbus相机用海康或Basler的SDK。这时候如果识别模型要在本地跑最顺手的方案绝对不是另起一个Python服务而是在C#进程内直接集成推理引擎。这个标题看起来很大但放到实际工程语境里“C#深度学习源码”指的无非是两类东西一类是C#写的深度学习训练或推理框架源码另一类是在C#项目中集成深度学习模型推理的完整实现源码。前者目前在工业落地中相对少见后者才是真正的刚需。这篇文章我主要围绕后者展开把我踩过的坑、验证过能用能上产线的路子按从选型到落地的顺序完整梳理一遍。1. 先看清大局C#跑深度学习的几条技术路线1.1 不自己造轮子四种主流方案的横向对比在C#里做深度学习本质上是两条路要么用纯C#实现的框架要么绑定到已有的原生深度学习库。纯C#方案的典型代表是ML.NET它的设计哲学是“让.NET开发者用熟悉的方式做机器学习”支持分类、回归、聚类、推荐、目标检测等常见任务也支持导出ONNX模型。但ML.NET对深度神经网络的支持深度有限像自定义Transformer架构、复杂的注意力机制、细粒度的训练控制用起来都比较别扭。另一条路是绑定方案主流选项有三类TensorFlow.NET、TorchSharp、ONNX Runtime。TensorFlow.NET是TensorFlow的C#绑定继承了TF的完整能力但TF本身模型部署偏重环境依赖复杂工业场景里有点杀鸡用牛刀。TorchSharp是PyTorch的C#绑定能写训练代码也能加载PyTorch模型灵活性高但它的API风格始终带着“翻译味”上手成本不低。ONNX Runtime则是微软主推的跨平台推理引擎定位非常纯粹只做推理不负责训练支持从PyTorch、TensorFlow、PaddlePaddle等框架导出ONNX模型然后在C#里加载执行。我整理了这四个方案在工程维度的核心差异方便大家按场景选方案训练/推理模型来源部署体积上手难度典型场景ML.NET训练推理自带API或ONNX中等低结构化数据、简单视觉任务TensorFlow.NET训练推理TensorFlow大依赖TF runtime高需要完全复用TF生态TorchSharp训练推理PyTorch大需要libtorch较高需要C#侧微调模型ONNX Runtime仅推理任意导出ONNX小单DLL对应运行时低工业部署、边缘设备、产线视觉1.2 我为什么在工业场景里锁定ONNX Runtime我自己做过不少产线视觉项目最终的落地方案几乎都是ONNX Runtime。原因很直接第一模型训练在Python侧完成团队可以继续用PyTorch或YOLO生态不改变算法同事的工作习惯第二ONNX Runtime在Windows x64下的部署极度轻量NuGet包引入后直接调用原生库没有虚拟环境、没有解释器依赖第三推理性能经过微软持续优化CPU和GPU都支持单张工业相机图片的推理延迟可以压到几十毫秒甚至更低。还有一个容易被忽略但很关键的点C#项目在工控机上跑往往需要和相机SDK、运动控制卡、PLC通讯库、数据库等一大堆原生库共存。ONNX Runtime的依赖极其干净几乎不会和这些库打架。而TorchSharp或TensorFlow.NET对VC运行时的版本比较敏感有时候一个DLL版本冲突就够排查半天。另外ONNX Runtime对多模型并发、多线程推理的支持也很完善这一点在产线多工位场景中特别重要。2. 从零搭一个能跑的C#深度学习推理项目2.1 环境准备与NuGet包选择我用的开发环境是Visual Studio 2022 .NET 8目标平台是x64。这里有个细节ONNX Runtime的NuGet包有CPU版和GPU版CPU版包名是Microsoft.ML.OnnxRuntimeGPU版是Microsoft.ML.OnnxRuntime.Gpu。如果只是简单测试先装CPU版就够了等模型验证通过再切GPU版因为GPU版还额外依赖CUDA和cuDNN配置不当反而会拖垮启动时间。创建一个控制台或WinForms项目后在NuGet里搜索并安装Microsoft.ML.OnnxRuntime即可。另外如果需要对图像做预处理建议一并引入OpenCvSharp4和OpenCvSharp4.runtime.win用OpenCV做Resize、归一化、通道变换比手动用GDI处理图像高效得多。安装完成后最简单的验证代码是这样using Microsoft.ML.OnnxRuntime; var sessionOptions new SessionOptions(); // 开启内存优化适合长期驻留的工控程序 sessionOptions.EnableMemoryPattern true; sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; using var session new InferenceSession(yolov8n.onnx, sessionOptions); Console.WriteLine($输入节点: {string.Join(, , session.InputMetadata.Keys)}); Console.WriteLine($输出节点: {string.Join(, , session.OutputMetadata.Keys)});如果这段代码能正确打印模型的输入输出节点名说明环境已经通了一半。2.2 模型从PyTorch转换到ONNX的完整链路很多初学者卡在这一步训练好的PyTorch模型怎么变成ONNX这里以最常用的YOLOv8为例算法同事在Python侧只需要这样做import torch from ultralytics import YOLO model YOLO(yolov8n.pt) model.model.eval() # 构造一个假的输入张量batch_size1输入尺寸为640x640RGB三通道 dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model.model, dummy_input, yolov8n.onnx, opset_version17, input_names[images], output_names[output0], dynamic_axes{images: {0: batch}, output0: {0: batch}} )导出时务必确认三个参数opset_version不能太低否则有些算子不支持输入尺寸必须和预处理尺寸一致dynamic_axes可以根据业务需求决定是否把Batch维设为动态。产出ONNX文件后用onnxruntime在Python侧先做一个推理验证确认输出结果和PyTorch一致再交给C#侧集成。这一步前置验证特别重要能过滤掉大量模型转换引入的隐形问题。2.3 在C#里加载模型并完成第一次推理拿到ONNX文件后在C#里的推理流程分成三步读取图像并预处理、构造输入Tensor、执行推理并解析输出。下面以YOLOv8检测模型为例演示一个无封装的最小实现using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; // 1. 读取图像并预处理为640x640 RGB张量 using var image new Mat(test.jpg, ImreadModes.Color); var resized new Mat(); Cv2.Resize(image, resized, new Size(640, 640)); var inputData new float[1 * 3 * 640 * 640]; // 注意OpenCV默认是BGR顺序需要转换为RGB并做归一化 for (int y 0; y 640; y) { for (int x 0; x 640; x) { var pixel resized.AtVec3b(y, x); inputData[0 * 3 * 640 * 640 0 * 640 * 640 y * 640 x] pixel[2] / 255f; inputData[0 * 3 * 640 * 640 1 * 640 * 640 y * 640 x] pixel[1] / 255f; inputData[0 * 3 * 640 * 640 2 * 640 * 640 y * 640 x] pixel[0] / 255f; } } // 2. 构造输入Tensor var inputTensor new DenseTensorfloat(inputData, new[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; // 3. 执行推理 using var session new InferenceSession(yolov8n.onnx); using var results session.Run(inputs); var output results.First().AsTensorfloat(); // 输出形状是 [1, 84, 8400]解析方式这里不展开后文细说这段代码的执行时间在CPU上通常只有几十毫秒GPU上更快。如果你打印的是分类模型的输出直接找output里最大值对应的下标即可检测模型则要额外解析候选框。2.4 预处理必须严格对齐训练阶段预处理是整个C#集成里最容易踩坑的环节没有之一。训练YOLO时PyTorch侧的处理流程通常包含等比缩放填充letterbox、BGR转RGB、归一化到[0,1]。如果C#侧只做了普通Resize没有做letterbox那么同样一张图推理结果会出现大量漏检和误检。所谓letterbox缩放是保持图像宽高比不变的情况下将短边缩放到目标尺寸长边按比例缩放然后四周填充灰色通常是114、114、114。实现并不复杂但必须保证和训练配置一致。我见过不止一次算法同事换了个模型训练时新版用了RectangularTrueC#侧还按老版的方式预处理结果现场模型几乎全盲。所以拿到模型的第一件事是去问算法同事要一份完整的预处理参数表包括归一化均值、方差、输入尺寸、是否填充、填充颜色然后封装成C#方法和Python侧逐像素对比验证。3. 源码怎么读、怎么改几个关键模块的逐段拆解3.1 推理封装类的设计读C#深度学习项目的源码时抓大放小的顺序应该是先找模型加载逻辑再看输入输出Tensor构造最后看后处理解析。一个规范的项目会把这三件事拆到不同的类里。实践下来我比较推荐的封装方式如下public class YoloDetector : IDisposable { private readonly InferenceSession _session; private readonly int _inputSize; private readonly float[] _means; private readonly float[] _stds; public YoloDetector(string modelPath, int inputSize 640) { var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; _session new InferenceSession(modelPath, options); _inputSize inputSize; _means new float[] { 0f, 0f, 0f }; _stds new float[] { 1f / 255f, 1f / 255f, 1f / 255f }; } public ListDetectionResult Detect(Mat image) { // 预处理 推理 后处理 } public void Dispose() { _session?.Dispose(); } }设计时注意三点第一InferenceSession是线程安全的多线程场景下可以共享同一个实例不必每次推理都new一个第二Dispose方法必须实现句柄泄漏在工控长稳测试中是大忌第三不要在后处理函数里输出日志高频推理时日志IO会严重影响实时性。3.2 输入输出张量的绑定细节不少人在NamedOnnxValue.CreateFromTensor这里栽跟头。这里的输入名“images”必须严格对应ONNX模型的输入节点名大小写敏感不能凭感觉写。拿到模型后我习惯在启动时打印一次所有输入输出节点的名称和维度作为校验foreach (var input in _session.InputMetadata) { Console.WriteLine($Input: {input.Key}, dims: {string.Join(,, input.Value.Dimensions)}); } foreach (var output in _session.OutputMetadata) { Console.WriteLine($Output: {output.Key}, dims: {string.Join(,, output.Value.Dimensions)}); }输出张量的解析也各有门道。YOLOv8的输出形状是[1, 84, 8400]其中84表示4个框坐标80个类别概率8400表示不同尺度下的候选框总数。解析时要先做维度交换把形状变为[1, 8400, 84]再用置信度阈值过滤。有些模型还带NMS算子可以直接在推理结果里得到最终检测框没有的话就要自己写非极大值抑制。3.3 一个常见瓶颈单例Session还是每次都创建这个问题我在好几个项目里都被问过。结论很明确在工业场景中InferenceSession永远应该复用不应该每次推理都重新创建。我实测过创建一个YOLOv8的Session在CPU上可能要几百毫秒到1秒等于直接丢掉了实时性。正确做法是把Session当作一个长时间存活的单例在程序启动时初始化在程序退出时释放。但这里有个容易忽略的前提如果你在C#里同时跑多个模型或者需要动态切换模型比如生产品种变化时切换检测模型需要合理规划Session的生命周期。多模型场景下的稳妥做法是为每个模型维护一个ConcurrentDictionary以模型路径为Key值为对应的InferenceSession实例切换时按需获取避免反复创建销毁带来的GC压力。4. 工业部署中的三大坑4.1 相机采集和UI刷新卡顿处理检索热词里出现“c# 循环数据采集和ui刷新卡顿”这在带视觉检测的上位机里特别典型。很多刚接触上位机开发的程序员会把图像采集、推理、结果显示全部放在UI线程里执行结果就是界面一卡一卡甚至会报“UI线程无响应”。正确的思路是采集和推理全部放在后台线程UI线程只负责显示。用一个经典的System.Threading.Channels或者BlockingCollection做队列相机线程往队列里塞图像推理线程从队列取图执行模型推理完成后通过BeginInvoke或Dispatcher把结果回调到UI线程。瓶颈队列的长度要设置上限比如10帧满了就丢最旧的帧保证处理的是实时画面而不是积压的历史帧。4.2 扫码枪触发与推理业务串行还是并行热词里“c# 扫码枪触发事件”对应的场景是扫码枪扫到条码后触发相机拍照并进入检测流程。这里有个并发陷阱扫码枪事件触发频率可能远高于单张图片的推理耗时如果每次触发都同步执行拍照推理事件会积压现场表现为扫码没反应或漏检。我的做法是把扫码事件当成一个“拍照请求”入队相机采集线程轮询队列有请求则触发硬触发或软触发拍照然后进入检测流水线。另一个更快的方案是相机一直处于连续采集模式后台线程检测到扫码事件后从最近一帧图像里处理即可。两种方案各有优劣前者适合需要精确定位抓拍瞬间的场景后者适合生产线速度较快、不允许额外拍照延时的场景。4.3 调试模型输出完全不符怎么办这是排查过程最痛苦的一类问题C#代码跑起来了模型也加载了但输出结果就是和Python侧对不上。遇到这种情况我的排查顺序是固定的第一步确认输入张量是否一致。把C#预处理后的inputData保存成二进制文件同时在Python侧用同样的预处理把Tensor导出逐元素对比看差异出现在哪一环。图像预处理最常见的坑包括颜色通道顺序错误、归一化参数不一致、Resize方式不同OpenCV默认双线性插值和PyTorch默认插值可能不同、letterbox填充值不同。第二步确认模型是否在C#侧被错误地再次归一化。有些初学者拿到PyTorch模型Python侧训练时做过归一化C#侧又加了一次除以255导致输入变成原来的1/255推理结果自然面目全非。第三步确认输出解析逻辑是否正确。比如YOLOv8的输出是cx, cy, w, h形式的中心坐标解析时却按x1, y1, x2, y2去算得到的框位置必然不对。建议在C#后处理每个阶段都打印一条中间结果和Python侧逐行对照。这一步如果全部走通基本可以确定问题不在集成层。剩下的可能是模型导出时算子的兼容性问题但这类问题在ONNX Runtime和opset 17以上的组合下已经非常少见了。5. 源码工程里的高频细节补充5.1 自己动手编译ONNX Runtime的必要性如果只是做普通项目用NuGet官方包完全够用。但老实说官方包为了通用性做了一些取舍比如默认不启用OpenMP、可能使用MLAS默认调度策略等在特定CPU上性能未必最优。我遇到的少数需要自己编译的场景有两个一是目标平台是国产CPU比如ARM架构的边缘盒子官方包没有对应的预编译版本二是需要裁剪算子只保留模型用到的算子把DLL体积压到最小。自己编译ONNX Runtime并不是一个轻松活需要用CMake配置、拉取Python工具链、跑完整构建脚本整个过程非常耗时。建议先确认官方发布页面里是否有匹配目标平台的包有的情况下就不要自己折腾编译。5.2 用GPU加速前的硬件适配如果工控机有独立显卡比如NVIDIA的MX系列或RTX系列可以考虑使用Microsoft.ML.OnnxRuntime.Gpu提升推理速度。但这里有两个前提显卡驱动必须支持CUDA且需要额外安装对应版本的CUDA和cuDNN。GPU推理并非在所有场景都有优势小模型在CPU上本身就很快GPU反而会因为内存拷贝增加毫秒级延迟。所以我的经验是先把CPU推理跑通再根据实际帧率决定是否上GPU而不是一上来就铺开GPU环境徒增部署复杂度。5.3 日志与诊断信息的保留工控软件在产线现场跑日志是排查问题最重要的抓手。建议在推理线程里用Stopwatch记录预处理、推理、后处理三个环节各自的耗时写日志时带上时间戳、模型版本、图像尺寸、推理结果概要。这个习惯我坚持了很多年帮我在现场省了大量时间。模型迭代后如果出现性能下降先翻日志往往就能定位到是预处理参数变化还是模型本身变化导致的。6. 最后分享一点实际经验如果你现在正准备在C#里集成深度学习模型我的建议是先不要纠结框架选型直接走“PyTorch导出ONNX ONNX Runtime推理”这条路这是目前生态最成熟、文档最齐全、坑最少的方向。整个链路跑通后再回头根据自己的场景优化是换GPU、调线程模型、还是按需裁剪DLL。从源码阅读的角度把重心放在输入输出张量构造和后处理解析这两部分因为这部分高度依赖具体模型也是出错率最高的地方。模型加载本身反而没什么玄机。C#做深度学习这件事核心挑战从来不是语言能力而是整个工程链路的畅顺度。把Python训练侧和C#部署侧的约定对齐保证预处理、张量布局、后处理逻辑的每一步都能对上剩下的就都是工程优化问题。希望这篇文章能帮你少走一些弯路。本文还有配套的精品资源点击获取