多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于神经网络的电池健康度模型:从数据集到Python源代码实战

基于神经网络的电池健康度模型:从数据集到Python源代码实战 简介这份资源围绕锂离子电池健康度SOH估算展开利用神经网络与实测数据集训练电池老化模型面向计算机、人工智能、自动化、电子信息等专业的在校学生、教师及企业员工也适合作为毕设、课程设计或项目立项的参考案例。压缩包共41个文件约1.23MB包含12个Python源码文件、11张结果图、6个h5模型文件以及xml配置、joblib标准化器、xlsx电池数据、md说明文档等覆盖从数据处理、模型训练到验证的完整流程。资源以3号电池循环数据为样本涉及电压、电流、温度曲线与SOH公式推导并给出模型损失值变化和4-12电池验证结果便于理解容量衰减与内阻增加对SOH的影响。目前已有197人学习。代码均经测试运行成功作者为ldxxxxll下载后可按README说明使用适合在此基础上修改扩展实现其他功能。1. 电池健康度模型从一份数据集到能跑的 Python 源代码电动车跑了三年表显续航从 500 掉到 420车主心里没底维修厂也说不清电池到底还剩多少寿命。电池健康度SOH这件事靠电压表量一下、靠经验估一估误差能到 10% 以上换电池还是继续用的决策就悬在半空。基于神经网络、利用数据集训练出电池健康度模型再配一套能直接跑的 Python 源代码解决的正是这个「说不清」的问题。它适合三类人做 BMS 的嵌入式工程师想把算法从查表升级到模型推理做储能运维的工程师想给电池组做寿命预测以及手里有充放电数据、想用 Python 跑通第一个 SOH 模型的学生和转行者。下面按「数据怎么来、特征怎么提、模型怎么搭、坑在哪」一路讲透代码可以直接抄。2. 电池数据集怎么选NASA、CALCE 与自采数据的取舍2.1 三类公开数据集的字段差异与适用场景做电池健康度模型第一步不是写网络是找数据。公开数据集里最常被拿来练手的是 NASA Ames 的锂电池老化数据、马里兰大学 CALCE 的 CS2 系列以及牛津大学那批带温度控制的电池数据。它们都是 18650 或软包电芯在恒流恒压充放电循环下测出来的字段结构类似循环序号、电压、电流、温度、容量。差别在于采样密度和老化路径。NASA 数据集采样间隔偏粗每个循环只记录关键节点适合做「循环级」的 SOH 回归也就是一个循环出一个健康度标签。CALCE 的 CS2 系列采样更密充放电曲线完整适合做「片段级」特征提取比如从一段恒流充电曲线里截取等压升时间。牛津那批数据带多温度工况适合验证模型对温度漂移的鲁棒性。选数据集的核心判断标准只有一条你的目标场景是循环级预测还是实时片段预测。循环级预测用 NASA 就够实时片段预测必须用采样密的 CALCE 或自采数据。很多人一上来就抓 NASA训出来的模型在实车上跑不动就是因为采样粒度对不上。提示公开数据集里的容量字段单位不统一NASA 用 AhCALCE 有的文件用 mAh合并前先统一否则标签差三个数量级。2.2 自采数据的采集频率与标签构造如果手里有真实的充放电设备自采数据反而比公开数据集更靠谱因为工况和你的目标场景一致。采集时至少记录四路信号单体电压、充放电电流、温度、累计安时。采样频率建议 1 Hz 起步做片段级特征可以到 10 Hz再高对 SOH 预测没有明显增益反而让数据量翻倍。标签构造是自采数据最容易翻车的地方。SOH 的标准定义是当前最大可用容量除以额定容量但实际采集时你很难每个循环都做一次完整满充满放。常见做法是每隔 50 个循环做一次容量标定中间循环用安时积分估算容量再对齐到标定值做修正。这个修正步骤不做标签里会混入大量积分漂移模型学到的就是噪声。import pandas as pd import numpy as np # 读取一个循环的原始充放电记录 df pd.read_csv(battery_cycle_001.csv) # 字段time_s, voltage_v, current_a, temp_c # 安时积分估算本循环容量放电段电流为负 discharge df[df[current_a] 0].copy() discharge[dt] discharge[time_s].diff().fillna(0) discharge[ah] (discharge[current_a].abs() * discharge[dt] / 3600).cumsum() estimated_capacity discharge[ah].iloc[-1] # 用最近一次标定值修正 calibrated_capacity 2.8 # 上一次满放标定得到的 Ah soh estimated_capacity / calibrated_capacity print(f估算容量 {estimated_capacity:.3f} Ah, SOH {soh:.3f})这段代码做的是单循环容量估算。dt是相邻采样点的时间差单位秒除以 3600 换成小时再乘电流得到安时。estimated_capacity取放电段最后一行的累计值。calibrated_capacity是额定或标定容量实际项目里应该从标定记录表里读不要写死。跑完这一步每个循环得到一个 SOH 标签才能进入特征工程。3. 特征工程从充放电曲线里提取可训练的输入3.1 等压升时间与温度统计量的提取原始电压电流曲线不能直接喂给网络维度太高、信息冗余。工程上最有效的特征是「等压升时间」在恒流充电阶段电压从 3.8 V 升到 4.0 V 所用的时间。电池老化后内阻增大同样电流下电压爬升更快这个时间会缩短和 SOH 强相关。另一个是「等流降时间」放电时电压从 4.0 V 降到 3.6 V 的时间同样随老化缩短。温度特征不要只取平均温度要取充电段和放电段的最大温差、温度上升速率。老化电池内阻大大电流下温升更明显这个差异在平均温度里被抹掉了。我一般会提取六个统计量充电等压升时间、放电等流降时间、充电最大温升、放电最大温升、充电段温度标准差、放电段温度标准差。def extract_features(cycle_df): charge cycle_df[cycle_df[current_a] 0] discharge cycle_df[cycle_df[current_a] 0] # 等压升时间充电时电压从3.8到4.0的耗时 c_mask (charge[voltage_v] 3.8) (charge[voltage_v] 4.0) t_rise charge[c_mask][time_s].max() - charge[c_mask][time_s].min() # 等流降时间放电时电压从4.0到3.6的耗时 d_mask (discharge[voltage_v] 4.0) (discharge[voltage_v] 3.6) t_drop discharge[d_mask][time_s].max() - discharge[d_mask][time_s].min() feats { t_rise: t_rise, t_drop: t_drop, chg_temp_rise: charge[temp_c].max() - charge[temp_c].min(), dis_temp_rise: discharge[temp_c].max() - discharge[temp_c].min(), chg_temp_std: charge[temp_c].std(), dis_temp_std: discharge[temp_c].std(), } return featsextract_features接收一个循环的 DataFrame返回六个特征。t_rise和t_drop是时间差单位秒。温度统计量用极差和标准差避免平均温度掩盖局部热点。注意c_mask和d_mask可能为空实际代码里要加长度判断否则max()会抛异常。这个函数对每个循环跑一遍拼成特征矩阵就是网络的输入。3.2 特征归一化与训练集划分的边界六个特征的量纲差很大时间特征在几百到几千秒温度特征在几度到十几度。不归一化直接训网络会把大部分权重分给数值大的时间特征温度特征被淹没。标准做法是 z-score 归一化按训练集的均值和标准差算验证集和测试集用同一组参数不能各自归一化。训练集划分有个容易忽略的边界同一个电池的不同循环不能同时出现在训练集和测试集里。电池老化是连续过程相邻循环的特征高度相似随机划分会让测试集里混入和训练集几乎一样的样本准确率虚高。正确做法是按电池划分比如三块电池两块训练、一块测试。如果只有一块电池的数据就按循环序号切前 70% 训练、后 30% 测试但要在文章里说明这个局限。from sklearn.preprocessing import StandardScaler # X: (n_cycles, 6), y: (n_cycles,) scaler StandardScaler() X_train scaler.fit_transform(X[:int(0.7 * len(X))]) X_test scaler.transform(X[int(0.7 * len(X)):]) # 按电池划分的写法 train_mask df[battery_id].isin([B01, B02]) X_train scaler.fit_transform(X[train_mask]) X_test scaler.transform(X[~train_mask])fit_transform只在训练集上调用transform用在测试集这是铁律。按电池划分时battery_id列必须在特征提取阶段就保留下来不能只存特征矩阵。归一化参数要保存下来推理时用同一组均值和标准差否则线上和线下特征分布对不上模型输出会漂。4. 神经网络搭起来前馈网络与 LSTM 的选型对比4.1 前馈网络的层数、激活与 Dropout 配置特征只有六个样本量通常几百到几千前馈神经网络FNN是最稳的选择。层数不要深两层隐藏层足够每层 32 到 64 个神经元。再深就会过拟合因为特征维度低深层网络没有足够的信息去学。激活函数用 ReLU输出层用线性因为 SOH 是连续值回归。Dropout 放在每个隐藏层之后比率 0.2 到 0.3。样本量小于 500 时用 0.3大于 2000 时用 0.2。损失函数用 MSE优化器用 Adam学习率 1e-3 起步。训练轮数不要设死用早停验证集损失连续 20 轮不降就停。import torch import torch.nn as nn class SOHNet(nn.Module): def __init__(self, in_dim6): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.2), nn.Linear(32, 1), ) def forward(self, x): return self.net(x).squeeze(-1) model SOHNet() criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)SOHNet的输入维度是 6对应六个特征。两层隐藏层 64 和 32Dropout 0.2。squeeze(-1)把输出从(batch, 1)压成(batch,)和标签维度对齐。MSELoss对 SOH 回归是标准选择如果标签里有异常值可以换成 HuberLoss对离群点更稳。Adam 的默认 betas 一般不用改学习率是唯一需要调的。4.2 LSTM 处理序列输入的适用条件如果你做的是片段级预测输入是一段时序而不是六个统计量那就该用 LSTM 或一维卷积。LSTM 适合捕捉充放电曲线里的时序依赖比如电压曲线的形状变化。但 LSTM 对样本量要求高每个样本是一个序列序列长度 100 到 500 个点样本数至少上千条才训得动。实际项目里我一般先用 FNN 加手工特征跑一版基线看测试集 MAE 能不能到 1% 以内。如果能就不上 LSTM因为 LSTM 推理慢、部署复杂在嵌入式端跑不动。如果 FNN 的 MAE 卡在 2% 以上再考虑 LSTM并且要用滑动窗口做数据增强把一条完整曲线切成多个重叠片段扩充样本量。class SOHLSTM(nn.Module): def __init__(self, in_dim3, hidden48): super().__init__() self.lstm nn.LSTM(in_dim, hidden, batch_firstTrue) self.fc nn.Linear(hidden, 1) def forward(self, x): # x: (batch, seq_len, in_dim) out, _ self.lstm(x) return self.fc(out[:, -1, :]).squeeze(-1)SOHLSTM的输入是(batch, seq_len, 3)三个通道分别是电压、电流、温度。out[:, -1, :]取最后一个时间步的隐藏状态接一个线性层输出 SOH。hidden设 48 是折中再大推理耗时明显上升。序列长度建议 200滑动步长 50这样一条 1000 点的曲线能切出 16 个样本数据利用率高。5. 训练与验证损失曲线怎么看、指标怎么定5.1 训练循环与早停的实现训练循环本身不复杂关键是每个 epoch 后记录训练损失和验证损失画出来看两条曲线的走势。正常情况是两条都降验证损失略高于训练损失。如果训练损失降、验证损失升就是过拟合该加 Dropout 或减层数。如果两条都不降是欠拟合或学习率不对。早停的实现要保存验证损失最低那一轮的模型参数不是最后一轮。很多人直接拿最后一轮去测试结果比最优轮差一截。早停的耐心值设 20样本量小的时候设 30避免验证损失正常波动被误判为不降。best_val float(inf) patience, wait 20, 0 best_state None for epoch in range(300): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: val_loss criterion(model(xb), yb).item() val_loss / len(val_loader) if val_loss best_val: best_val val_loss best_state model.state_dict() wait 0 else: wait 1 if wait patience: break model.load_state_dict(best_state)best_state保存的是验证损失最低时的参数字典训练结束后加载回来。wait计数连续不降的轮数到patience就停。val_loss按 batch 平均不是求和否则验证集大小不同时没法比较。这段代码里train_loader和val_loader的 batch size 建议 32样本量小于 200 时用 16。5.2 MAE、RMSE 与 SOH 误差的实际含义回归任务看两个指标MAE 和 RMSE。MAE 是平均绝对误差直接对应「平均差几个百分点」。RMSE 对大误差更敏感如果 RMSE 远大于 MAE说明有个别样本预测偏得很厉害要去查那些样本的特征是不是异常。SOH 预测的工程可接受误差是多少做寿命预警MAE 在 1.5% 以内就够用因为电池衰减到 80% 以下才需要干预1.5% 的误差不会改变决策。做质保判定要求更严MAE 要压到 1% 以内。如果测试集 MAE 在 2% 以上先别调网络回去查特征提取和标签构造八成是标签里有积分漂移。from sklearn.metrics import mean_absolute_error, mean_squared_error model.eval() with torch.no_grad(): preds model(X_test_tensor).numpy() mae mean_absolute_error(y_test, preds) rmse mean_squared_error(y_test, preds, squaredFalse) print(fMAE {mae:.4f}, RMSE {rmse:.4f})preds是模型输出y_test是真实 SOH。MAE 和 RMSE 都按原始尺度算不要用归一化后的值否则数字没有物理意义。如果 MAE 是 0.015就是平均差 1.5 个百分点。RMSE 和 MAE 的比值超过 1.5就去查误差最大的那几个样本通常是某个循环的充放电数据缺失或温度传感器异常。6. 避坑与排查电池健康度模型训练中的五个血泪教训6.1 标签泄漏容量标定值混进了特征现象训练集 MAE 0.3%测试集 MAE 5%差距大得离谱。原因特征提取时把「当前循环的标定容量」当成了输入特征而标定容量本身就是 SOH 标签的分子等于把答案喂给了模型。解决检查特征列表任何和容量直接相关的字段都不能进输入。标定容量只用来算标签不能出现在特征矩阵里。6.2 温度特征被平均抹平现象模型在常温数据上表现好一到高温工况就偏 8% 以上。原因只用了平均温度高温下内阻变化导致的局部温升被平均掉了。解决改用最大温升和温度标准差并且把工况温度作为单独特征加进去。如果训练集里没有高温样本模型学不到温度补偿只能靠数据覆盖。6.3 按循环随机划分导致准确率虚高现象测试集 MAE 0.8%上线后实际误差 4%。原因随机划分让相邻循环同时进了训练集和测试集相邻循环特征几乎一样模型在测试集上等于在背答案。解决按电池划分或者按时间前 70% 训练、后 30% 测试。划分方式要在代码里写死不能每次跑随机种子。6.4 归一化参数没有保存现象线下测试正常部署到设备上输出全偏。原因推理时重新算了一遍均值和标准差和训练时的不一致。解决训练完把scaler.mean_和scaler.scale_存成文件推理时加载同一组参数。归一化参数是模型的一部分不能丢。6.5 早停保存了最后一轮而不是最优轮现象训练日志里验证损失最低是第 80 轮但测试用的是第 200 轮的模型效果差一截。原因训练循环结束后直接拿当前模型去测试没有加载最优参数。解决用best_state保存最优轮参数训练结束加载回来再测试。这个坑不报错但会让你的模型白训。7. 把模型压到嵌入式端ONNX 导出与推理耗时实测模型训完只是半成品真正落地要把它塞进 BMS 或边缘网关。Python 训练出来的 PyTorch 模型不能直接跑在嵌入式端标准路径是导出 ONNX再用 ONNX Runtime 或 TensorRT 推理。导出时注意输入维度要固定动态维度在嵌入式端支持不好。import torch.onnx dummy torch.randn(1, 6) torch.onnx.export( model, dummy, soh_model.onnx, input_names[features], output_names[soh], opset_version11, dynamic_axesNone, )dummy的 shape 是(1, 6)对应单样本推理。opset_version11兼容性最好嵌入式端的 ONNX Runtime 基本都支持。dynamic_axesNone表示输入维度固定batch 只能是 1这对 BMS 逐循环推理够用。导出后用onnxruntime跑一遍对比 PyTorch 和 ONNX 的输出差异差异应该在 1e-5 以内。推理耗时实测在树莓派 4B 上这个两层网络单次推理约 0.3 ms一秒钟能跑三千次对 BMS 的秒级循环完全够。如果换成 LSTM同样硬件上单次推理约 8 ms序列长度 200 时涨到 15 ms就有点吃紧了。这也是我前面说先用 FNN 跑基线的原因FNN 在嵌入式端的优势是压倒性的。最后说个习惯每次训完模型我都会把特征提取函数、归一化参数、模型权重、ONNX 文件打成一个包版本号写在文件名里。电池数据是持续采集的三个月后新数据进来要重新训没有这套版本管理你根本分不清线上跑的是哪一版。这个习惯帮我省了至少两次「模型效果突然变差但找不到原因」的排查时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表