做工控上位机这几年不少客户都提过设备预警的需求能不能根据实时采集的温度、振动、电流数据提前预判电机会不会出故障别等停机了才抢修。最开始我也觉得做AI就得学Python、搭PyTorch一想到现场工控机都是Win7、Win10的精简系统装个Python环境都费劲还要解决依赖、版本兼容的问题就迟迟没动手。直到去年用ML.NET落地了一条输送线的电机故障预测项目全程纯C#开发从Modbus采集PLC数据、数据清洗、模型训练到上位机集成推理一行Python都不用写部署的时候就多拷一个模型文件现场不用装任何额外运行时。跑了快一年误报率控制在5%以内稳定性完全达标。很多.NET开发者对工业AI的认知还停留在“必须Python栈”其实对于结构化数据的故障预测、异常检测、参数优化这类需求ML.NET完全能打而且最大的优势就是和现有工控技术栈无缝衔接部署成本极低。这篇文章就从系统架构、PLC采集、数据处理、模型训练、上位机集成、现场避坑六个维度完整复盘整个项目的落地流程所有代码都是生产环境验证过的核心片段.NET工控开发者看完就能照着落地。一、系统整体架构整个系统采用分层设计完全基于.NET技术栈从底层设备到上层应用全程C#没有跨语言调用没有额外运行时依赖部署和维护都非常简单。各层职责清晰设备层PLC传感器负责现场物理量采集寄存器地址统一映射采集层独立后台服务稳定采集数据异常自动重连数据双写缓存与历史库数据处理层清洗脏数据、提取时域特征、构建标注数据集是模型效果的关键AI引擎层负责模型训练与实时推理封装成独立服务供上层调用应用层上位机监控界面展示设备健康状态触发预警与记录处理二、PLC数据采集模块稳定是第一位的工业AI的前提是数据采集不稳定后面全白搭。我们用标准Modbus TCP协议兼容绝大多数品牌PLC采集逻辑单独封装成模块和业务逻辑完全解耦。数据实体与地址映射首先定义统一的传感器数据结构对应PLC保持寄存器地址提前和电气工程师确认好地址、数据类型、缩放系数避免采集到的数据错位。/// summary /// 设备传感器原始数据 /// /summary public class DeviceSensorData { public DateTime Time { get; set; } public int DeviceId { get; set; } /// summary /// 电机温度 ℃寄存器地址0缩放系数0.1 /// /summary public float Temperature { get; set; } /// summary /// 电机振动 mm/s寄存器地址1缩放系数0.01 /// /summary public float Vibration { get; set; } /// summary /// 工作电流 A寄存器地址2缩放系数0.01 /// /summary public float Current { get; set; } /// summary /// 设备运行状态 0待机 1运行 2故障 /// /summary public int RunStatus { get; set; } }采集核心实现采集逻辑跑在独立后台线程绝对不能占用UI线程。核心要点是异常自动重连、采集失败不崩溃、数据平滑过渡。public class ModbusAcquisitionService { private readonly string _ip; private readonly int _port; private TcpClient _tcpClient; private ModbusIpMaster _master; private Thread _collectThread; private volatile bool _isRunning; // 采集频率1秒/次 private const int CollectIntervalMs 1000; public ModbusAcquisitionService(string ip, int port 502) { _ip ip; _port port; } public void Start() { _isRunning true; _collectThread new Thread(CollectLoop) { IsBackground true, Name Modbus采集线程 }; _collectThread.Start(); } private void CollectLoop() { while (_isRunning) { try { // 连接断开自动重连 if (_master null || !_tcpClient.Connected) { Connect(); } // 批量读取寄存器从地址0开始读10个 ushort[] regs _master.ReadHoldingRegisters(1, 0, 10); // 解析数据 var data new DeviceSensorData { Time DateTime.Now, DeviceId 1, Temperature regs[0] * 0.1f, Vibration regs[1] * 0.01f, Current regs[2] * 0.01f, RunStatus regs[3] }; // 写入缓存和历史库 DataCache.Instance.Update(data); HistoryDb.Instance.Insert(data); } catch (Exception ex) { // 异常只记录日志不中断线程 LogHelper.Error($采集异常: {ex.Message}); _master null; Thread.Sleep(3000); // 异常后延迟3秒重连 } finally { Thread.Sleep(CollectIntervalMs); } } } private void Connect() { _tcpClient new TcpClient(_ip, _port); _master ModbusIpMaster.CreateIp(_tcpClient); _master.ReadTimeout 2000; } public void Stop() { _isRunning false; _master?.Dispose(); _tcpClient?.Close(); } }采集模块的稳定性优先级最高哪怕偶尔丢几个点也不能让整个采集服务挂掉。工业现场网络波动、设备重启都是常事重连机制必须做扎实。三、工业数据预处理脏数据才是最大的坑很多人训练模型效果不好不是算法不行是数据没处理好。工业现场的数据脏得很传感器偶尔跳变、网络波动丢数、设备启停产生的极值直接拿去训练模型肯定不准。我们的处理流程分三步层层过滤最终得到可用的高质量数据集。第一步异常值过滤与缺失值填充首先根据物理边界过滤明显的错误数据比如电机温度不可能超过180℃振动不可能超过20mm/s超出范围的直接判定为异常值。// 物理范围校验 public bool IsValid(DeviceSensorData data) { if (data.Temperature -10 || data.Temperature 180) return false; if (data.Vibration 0 || data.Vibration 20) return false; if (data.Current 0 || data.Current 50) return false; return data.RunStatus is 0 or 1 or 2; }对于短时间的缺失数据小于3个采集点用线性插值填充长时间缺失直接丢弃该段数据不要硬补否则会引入噪声影响模型。第二步滑动窗口特征工程这是最关键的一步。原始的单点数据没有意义设备故障是一个渐变的过程单个时间点的数据看不出趋势。我们用滑动窗口提取时域特征把一段时间的数据转化成一组能反映设备状态的特征量。我们用的窗口大小是10秒步长1秒每个窗口提取6个核心特征温度均值、振动均值、电流均值振动峰值、振动标准差温度变化率/// summary /// 特征样本模型输入 /// /summary public class FeatureSample { public float TempAvg { get; set; } public float VibAvg { get; set; } public float CurrentAvg { get; set; } public float VibPeak { get; set; } public float VibStd { get; set; } public float TempChangeRate { get; set; } /// summary /// 标签1故障前兆0正常 /// /summary public bool IsFault { get; set; } }特征提取的核心逻辑public ListFeatureSample ExtractFeatures(ListDeviceSensorData rawData, int windowSize 10) { var samples new ListFeatureSample(); for (int i windowSize - 1; i rawData.Count; i) { var window rawData.Skip(i - windowSize 1).Take(windowSize).ToList(); // 窗口数据完整性校验缺失超过10%直接丢弃 if (window.Count(d d ! null) windowSize * 0.9) continue; var temps window.Select(d d.Temperature).ToArray(); var vibs window.Select(d d.Vibration).ToArray(); var currents window.Select(d d.Current).ToArray(); var sample new FeatureSample { TempAvg temps.Average(), VibAvg vibs.Average(), CurrentAvg currents.Average(), VibPeak vibs.Max(), VibStd CalculateStd(vibs), TempChangeRate (temps.Last() - temps.First()) / windowSize }; samples.Add(sample); } return samples; }第三步数据集标注标注是个苦力活但直接决定模型上限。正常数据就是设备稳定运行时采集的数据故障数据来自历史故障记录取故障发生前5分钟的窗口标注为“故障前兆”样本这样模型学到的是故障发生前的特征才能做到提前预警而不是故障发生了才识别。最终处理好的数据集存为CSV格式方便ML.NET加载训练。四、ML.NET故障预测模型训练工业场景选算法不要盲目追新上深度学习稳定、可解释、速度快比单纯的准确率更重要。算法选择我们最终选用SDCA逻辑回归二元分类算法原因非常明确对于结构化的时域特征准确率和浅层神经网络差别不大完全满足需求训练速度极快几万条样本几分钟就能训完参数调整迭代非常方便推理速度快单样本预测不到1毫秒完全满足实时性要求可解释性强可以输出每个特征的权重出了问题能排查原因不像深度学习是黑盒完整训练代码class Program { static void Main(string[] args) { // 1. 创建ML上下文固定种子保证结果可复现 var mlContext new MLContext(seed: 1234); // 2. 加载数据集 IDataView fullData mlContext.Data.LoadFromTextFileFeatureSample( path: fault_dataset.csv, separatorChar: ,, hasHeader: true); // 3. 按8:2拆分训练集和测试集 var split mlContext.Data.TrainTestSplit(fullData, testFraction: 0.2); // 4. 构建训练管道 var pipeline mlContext.Transforms // 合并所有特征列 .Concatenate(Features, nameof(FeatureSample.TempAvg), nameof(FeatureSample.VibAvg), nameof(FeatureSample.CurrentAvg), nameof(FeatureSample.VibPeak), nameof(FeatureSample.VibStd), nameof(FeatureSample.TempChangeRate)) // 二分类训练器SDCA逻辑回归 .Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression( labelColumnName: nameof(FeatureSample.IsFault), featureColumnName: Features)); // 5. 训练模型 Console.WriteLine(开始训练模型...); ITransformer model pipeline.Fit(split.TrainSet); // 6. 评估模型 IDataView predictions model.Transform(split.TestSet); var metrics mlContext.BinaryClassification.Evaluate( predictions, labelColumnName: nameof(FeatureSample.IsFault)); Console.WriteLine( 模型评估结果 ); Console.WriteLine($准确率: {metrics.Accuracy:F4}); Console.WriteLine($召回率: {metrics.Recall:F4}); // 工业场景优先看召回率 Console.WriteLine($精确率: {metrics.Precision:F4}); Console.WriteLine($AUC: {metrics.AreaUnderRocCurve:F4}); // 7. 保存模型 mlContext.Model.Save(model, split.TrainSet.Schema, motor_fault_model.zip); Console.WriteLine(模型已保存至 motor_fault_model.zip); } static float CalculateStd(float[] values) { float avg values.Average(); float sum values.Sum(v (v - avg) * (v - avg)); return (float)Math.Sqrt(sum / values.Length); } }指标解读工业故障预测场景召回率优先级远高于精确率。漏报一个故障可能导致设备停机损失几万误报一次只是工人去检查一下成本很低。我们最终的模型召回率做到了92%误报率控制在5%以内现场完全可以接受。如果误报率太高可以调整分类阈值把阈值设高一点牺牲少量召回率换更低的误报率根据现场实际需求平衡。五、上位机实时推理集成模型训练好只是第一步集成到现有上位机里稳定运行才是真正落地。线程安全的推理引擎这里有个巨坑必须提醒ML.NET的PredictionEngine不是线程安全的。很多人图方便做成单例上位机多线程环境下调用会偶发崩溃而且没有明显错误日志非常难排查。工业上位机基本都是多线程采集线程、UI线程、后台处理线程所以必须用PredictionEnginePool通过对象池管理实例天生线程安全性能也更好。/// summary /// 推理服务封装 /// /summary public class FaultPredictionService { private readonly MLContext _mlContext new MLContext(); private readonly PredictionEnginePoolFeatureSample, FaultPrediction _enginePool; public FaultPredictionService(string modelPath) { // 加载模型 ITransformer model _mlContext.Model.Load(modelPath, out _); // 创建对象池默认池大小10满足多线程调用 _enginePool _mlContext.Model.CreatePredictionEnginePoolFeatureSample, FaultPrediction(model); } /// summary /// 执行预测 /// /summary public FaultPrediction Predict(FeatureSample sample) { return _enginePool.Predict(sample); } } /// summary /// 预测结果 /// /summary public class FaultPrediction { [ColumnName(PredictedLabel)] public bool IsFault { get; set; } [ColumnName(Probability)] public float FaultProbability { get; set; } }实时推理流程采集线程拿到原始数据后写入滑动窗口缓冲区窗口数据满了就计算特征然后交给推理服务做预测故障概率超过阈值就触发预警。整个流程在后台线程执行绝对不阻塞UI// 滑动窗口缓冲区 private readonly QueueDeviceSensorData _windowBuffer new QueueDeviceSensorData(); private const int WindowSize 10; // 采集数据到达回调 private void OnDataReceived(DeviceSensorData data) { lock (_windowBuffer) { _windowBuffer.Enqueue(data); if (_windowBuffer.Count WindowSize) { _windowBuffer.Dequeue(); } if (_windowBuffer.Count WindowSize) { // 窗口满了计算特征 var sample ExtractFeatureFromBuffer(); // 异步预测不阻塞采集线程 Task.Run(() { var result _predictionService.Predict(sample); if (result.FaultProbability 0.85f) { // 触发预警 RaiseAlarm(data.DeviceId, result.FaultProbability); } }); } } }上位机界面展示界面上不用搞太复杂核心展示三个信息设备实时运行参数、当前健康故障概率、历史预警记录。预警的时候弹框声音提示操作人员确认后记录处理结果形成闭环。六、现场部署踩坑与优化实验室跑通不算现场稳定运行才是真的。说几个我踩过的实坑基本上第一次部署都会遇到。坑1模型文件路径错误问题开发时用相对路径没问题部署到工控机程序从快捷方式启动工作目录变了提示找不到模型文件。解决方案用程序基目录拼接绝对路径不要用相对路径。模型文件属性设为“较新则复制”确保输出到bin目录。要求高的可以把模型作为嵌入资源编译进exe彻底避免路径问题。string baseDir AppDomain.CurrentDomain.BaseDirectory; string modelPath Path.Combine(baseDir, Models, motor_fault_model.zip);坑2低功耗工控机推理慢问题现场用的是赛扬低功耗CPU一开始多设备同时预测CPU占用直接拉满采集都卡了。解决方案必装Microsoft.ML.CpuMath启用SIMD指令优化性能直接提升30%以上精简特征去掉相关性低的特征减少计算量降低预测频率不用每秒都预测5秒预测一次足够故障是慢过程坑3数据漂移导致准确率下降问题运行三四个月后误报率越来越高。因为设备磨损、环境温度变化数据分布和训练时不一样了模型就不准了。解决方案建立数据回流机制现场的误报和漏报数据都标注后收回来每季度用新数据重新训练一次模型更新到现场做模型热更新监听模型文件变化自动重新加载不用重启上位机坑4预警直接联动设备有安全风险问题一开始想做自动停机后来发现风险太大模型万一误判正常生产中途停机损失非常大。解决方案AI只做预警不直接控制设备。预警弹出后必须由操作人员人工确认再决定是否停机检修。工业场景安全永远是第一位的AI只能是辅助工具绝对不能替代人做关键操作。坑5采集中断导致特征异常问题网络波动断了几分钟恢复后滑动窗口里的数据不全计算出来的特征不对导致大量误报。解决方案特征计算前先做完整性校验窗口内有效数据少于90%就直接丢弃不做预测等数据连续完整了再继续。七、总结整个项目做下来最大的感受就是工业AI不用追求高大上的技术栈适合的才是最好的。ML.NET可能不是功能最强的AI框架但对于.NET工控开发者来说它是落地成本最低的方案。不用学新语言不用装额外环境就能把AI能力集成到现有系统里稳定性和部署性都非常适合工业现场。当然它也有边界复杂的图像检测、3D视觉、大模型应用这些需求还是得用Python生态的方案。但对于设备故障预测、工艺参数优化、数据异常检测这类结构化数据需求纯C#栈的ML.NET完全够用而且优势明显。做工控的朋友如果想给系统加AI能力又不想折腾Python环境真的可以试试ML.NET大概率能满足你的需求。
企业数字化 ERP 产品动态
相关推荐
VNWOA优化LSSVM参数:原理、实现与MATLAB实战指南 简介:资源围绕鲸鱼算法(WOA)优化最小二乘支持向量机(LSSVM)这一主题,面向从事智能优化、故障诊断与预测性维护的研究者和工程师。压缩包内含107个文件,以103个MATLAB脚本为主,辅以4个… · 2026/9/26 18:55:34
从0到1搭建AI Agent平台:架构设计与工程落地实践 我先说个真实的感受。早几年做 AI 应用,大家基本还是一个 Agent 一个 Agent 地手工“敲”出来的,从 Prompt 设计、工具接入到记忆管理,全都得自己从头搭,就像手工作坊里的老师傅,一个人既是设计师又是流水线工人。等到… · 2026/9/26 18:55:34
火车售票系统数据库课程设计实战指南 简介:本资源是一套完整的数据库课程设计项目——火车售票系统,面向计算机相关专业本科生及初学者,解决课程设计、毕业设计与项目实训中数据库建模、C#桌面应用开发与SQL Server集成实践等核心需求。压缩包共165个文件,含26个C#源码… · 2026/9/26 18:55:34
AIUEBridge 实战:用自研 UE 插件 + MCP 服务打通虚幻编辑器 AI 协同开发 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 19:37:33
MiniMax M2.1 首发评测:祖传屎山代码重构实战,这种爽感谁用谁懂 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 19:37:27
开启新纪元:让牛马(NB的AI工具)——Aipy帮你干活,TaoToken统一Key接入配置指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 19:37:27
LLM 工程实践:从 LLM 到 RAG、Agent、MCP 的一体化配置与验证 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 19:37:21
用Cursor / Trae AI 开发Go项目时,记得先做这些 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 19:37:21
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46