☰
基于LSTM的电池SOH估算:MATLAB实现与NASA数据集实战
2026/10/20 15:14:41 网站建设 项目流程

这两年新能源项目越来越多,电池管理系统(BMS)里最让人头疼的模块,除了SOC(荷电状态)估计,就是SOH(健康状态)估算。SOC还能靠安时积分、卡尔曼滤波凑合,SOH这东西却带着明显的“长期历史依赖”特征——当前能放出多少电,取决于过去几百个循环里你把它折腾成什么样。我一开始尝试过用多项式拟合容量衰减曲线,效果在那几颗电池上看起来还凑合,一旦换电池、换温度工况就立刻被打回原形。后来索性把目光转向了循环神经网络,用LSTM做时序建模,整体思路上完全不一样了。

这里我会把从NASA公开电池数据集的数据整理,到MATLAB里搭建LSTM网络的完整过程都过一遍,包括我自己的翻车记录、参数选择的考虑,以及单电池拟合和跨电池泛化的差距。这篇内容更适合手里已经有一定MATLAB和深度学习基础、但还没有系统跑过一个电池老化项目的朋友,当然,如果你刚接触电池数据也可以跟着走,我会把数据集的坑也一并说明。

1. SOH估算为什么要盯上LSTM这种时序网络

先别急着打开MATLAB敲代码。你要先想清楚一个问题:为什么传统方法搞不定SOH?

1.1 SOH到底是什么,为什么难算

SOH的定义在工程上并不完全统一。业内最常见的口径是当前满充状态下可放出的最大容量除以出厂标称容量,再乘以100%。比如一节18650电池标称容量1.8Ah,用了半年后测出来满充只能放出1.5Ah,那SOH就是83.3%。有些BMS还会用内阻增长来辅助判断,但容量维度始终是主线索。

难点在于:容量你不能直接测,只能靠“一次完整放电”去反推。而在实际用车或用电池的场景里,完整放电的机会非常少,往往是充一会停一会、脉冲放电、跳变负载交替着来。所以真实系统里SOH只能靠间接量——电压、电流、温度、循环次数——去推断。这个推断关系是非线性的、时变的,还会因为环境温度不同而发生偏移。

1.2 传统拟合方法的局限

最朴素的做法是把容量衰减看成循环次数的函数,直接上二次多项式或者指数衰减模型。比如有些论文喜欢用Q = a*exp(b*N) + c*exp(d*N)这种双指数形式。这种模型的好处是参数少、可解释强,但前提是电池一直处在同一个工况下循环老化。我试过把这种模型套到另一组不同放电深度的数据上,误差瞬间从2%涨到8%以上。

内阻法、电化学阻抗谱法又是另一路子。EIS可以通过频域分析把欧姆内阻、电荷转移内阻、扩散阻抗拆出来,但是离线测量容易,在线实现复杂,而且车规级BMS几乎没有空间给你装一台阻抗分析仪。

在这类问题里,最大的矛盾在于:衰减过程确实和长期历史有关,但常规回归模型没有“记忆”机制。你给它输入当前这一时刻的状态,它就只知道这一时刻的状态,之前几百个循环积累了哪些不可逆损伤,它一概不认。

1.3 LSTM凭什么能处理这种时间依赖

LSTM(长短期记忆网络)是RNN家族里最常见的一种结构。RNN的初衷就是让网络在计算当前输出时,把前序时间步的隐含状态也一起带进来。理论上RNN能记住很长时间的上下文,但实际上梯度在反向传播过程中会指数级衰减,学到长度超过10~20步的依赖就很难了。LSTM通过“输入门、遗忘门、输出门”这三组门控,再加上一条细胞状态(cell state)的直通通道,让信息可以跨越多步保持或者丢弃,从而缓解了长依赖问题。

放在电池SOH上,这个特性正好对口。电池的第300次循环表现,取决于第1次到第299次循环的累积损伤,这是一个明摆着的长程时间依赖问题。你不需要人为去构造“累计变量”,LSTM理论上能在训练过程中自己学习到类似“损伤积累”的隐藏特征。这也是我最终选择LSTM而不是普通全连接网络的原因。

2. NASA数据集的下载、字段解构与容量曲线提取

既然定下来用LSTM,下一步就是找数据。这里采用的公开数据集,是NASA某研究中心的电池老化数据集,在电池健康管理领域几乎算标准教材。

2.1 数据集结构和下载要点

该数据集可以在NASA对应的公开页面下载到,最常见的几个文件是B0005.mat、B0006.mat、B0007.mat、B0018.mat。这几颗电池都是18650磷酸铁锂或钴酸锂类型,在室温下按固定策略循环充放电:先恒流充电再恒压充电至4.2V,然后放电至截止电压,如此反复,直到容量衰减到寿命终结标准。

下载下来的是一个.mat文件,如果你用MATLAB直接双击加载,工作区里会出现一个结构体,比如B0005。这个结构体里核心字段是cycle,它是一个结构体数组,每个元素代表一次充放电循环。

2.2 一步步读懂cycle字段

我用load命令加载一颗电池后,通常会先看一下cycle数组的长度和字段:

load('B0005.mat'); disp(length(B0005.cycle)); disp(fieldnames(B0005.cycle(1)));

每个循环元素里都会包含这些字段:

字段含义常见取值
type这个循环属于哪类操作charge / discharge / impedance
temp环境温度(摄氏度)24左右
voltage电压采样向量(V)充电0~4.2,放电4.2~2.5
current电流采样向量(A)充电正,放电负
capacity该循环测得的容量(Ah)放电循环才有有效值
time采样时间向量相对时间

注意一个关键点:capacity字段并不在所有循环里都有意义。在type为charge和discharge的循环里,结构体里都能看到容量,但充电循环计算出来的容量往往偏大,因为它包含了充电过程中的副反应和库仑效率的影响。工程上更认可的是放电循环测得的容量,所以第一步就是只筛选出discharge类型的循环。

2.3 从多循环数据中提取SOH标签

实际提取时我会这样写:

cycleData = B0005.cycle; q = []; fori = 1:length(cycleData) if strcmp(cycleData(i).type, 'discharge') q(end+1, 1) = cycleData(i).capacity; % 这一循环放电容量 end end

正常情况下,第一周期的放电容量应该最接近出厂容量,比如1.8Ah或2.0Ah。因为所有循环都是同一块电池连续跑下来的,这个容量序列本质上就是你的SOH序列。SOH可以这样换算:

Q0 = q(1); soh = q / Q0 * 100;

有一个我在拿到数据时踩过的小坑:有些电池数据里的容量序列第一点并不是单调下降的,甚至第二点可能比第一点高一点。原因在于电池测试刚开始时电化学状态还没完全稳定,或者环境温度有轻微起伏。这是物理现象,不是数据错误,处理时不要大惊小怪,直接以第一个放电循环为基准即可。

2.4 特征选择:先别急着上万维特征

LSTM虽然能处理时序,但并不是输入维度越多越好。很多人一开始就想着把电压、电流、温度全塞进去,以为信息越全效果越好。实际上NASA这套数据里,电压、电流曲线的形状在大部分循环里高度一致,真正的信息增量有限。盲目加入高维特征,不仅让模型训练更慢,还容易把噪声一起学进去。

我的做法是分阶段尝试。第一阶段只用循环编号和容量历史,把SOH估算当成一个“从容量衰减历史预测未来容量”的时间序列预测问题。第二阶段再尝试加入温度特征,因为后期电池老化加剧时温度会有变化。这里先保证主干流程跑通,后面再谈特征扩展。

3. MATLAB里搭LSTM:从输入设计到训练配置

环境选用MATLAB而不是Python,其实是有实际考量的。深度学习工具箱在MATLAB R2019a之后的版本里已经非常成熟,trainNetwork加lstmLayer可以直接完成大部分工作,不需要自己写自动求导,也不需要配CUDA的坑。对很多以Simulink和BMS开发为主业、并不想专门维护一套Python环境的工程师来说,MATLAB是个顺手的工具。

3.1 网络需要什么样的数据输入输出

先想清楚输入输出关系。网络要完成的任务是:给你前面若干个循环的容量序列,预测当前时刻的SOH。

每个训练样本对应一个“滑窗”。比如设置滑窗长度lookback = 30,第31个循环的SOH就由第1到第30个循环的序列来预测;第32个循环由第2到第31个循环来预测。用MATLAB代码生成训练集:

lookback = 30; X = []; Y = []; for t = lookback+1 : length(soh) X{end+1, 1} = soh(t-lookback : t-1)'; % 过去30个SOH值,行向量 Y(end+1, 1) = soh(t); % 当前SOH值 end

这里有个容易混淆的细节:sequenceInputLayer默认要求输入数据的维度是[特征维度, 时间步长, 样本数],而用cell数组组织样本时,每个cell里的矩阵维度是[特征维度, 时间步长]。因为这里每个样本只有一个特征(SOH),所以是1行、30列。有些新手会把soh(t-lookback : t-1)写成列向量,结果输入维度变成30行1列,直接报维度不匹配。

3.2 网络结构设计:不宜过深

MATLAB的LSTM网络不需要很深的层数。电池容量衰减序列本身是一个平滑趋势序列,没有复杂的高频成分,一个LSTM层就足够捕获主要动态。我在多个项目里对比过,两层的效果比单层好不了太多,但训练时间几乎翻倍,还更容易过拟合。

一个我最终经常使用的结构:

layers = [ sequenceInputLayer(1) lstmLayer(32, 'OutputMode', 'last') fullyConnectedLayer(16) reluLayer fullyConnectedLayer(1) regressionLayer ];

这里选择'OutputMode', 'last'是因为我们只需要序列最终输出一个SOH预测值,不需要每个时间步都输出。如果设置成'sequence',输出就是一个向量序列,后面的全连接层会变得很难处理,通常只有在做序列到序列预测时才这样用。

LSTM隐藏单元数32在这个任务里已经够用。NASA数据集每颗电池一共只有一百多个充放电循环,训练样本数本身就不大,隐藏单元设成64甚至128反而会迅速记住训练集里的细微波动,换一颗电池就彻底失效。

3.3 训练选项的调法

训练选项里最关键的是学习率、批次大小和验证集策略。我常用:

options = trainingOptions('adam', ... 'MaxEpochs', 500, ... 'MiniBatchSize', 16, ... 'InitialLearnRate', 0.01, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 100, ... 'LearnRateDropFactor', 0.5, ... 'Shuffle', 'never', ... 'Verbose', 1);

用Shuffle, 'never'而不是默认的'once'或'every-epoch',是因为时间序列数据一旦打乱,模型会失去时间顺序的概念。你要预测的是未来的SOH,训练时却把未来的数据打乱塞进历史样本里,这等于作弊——虽然训练误差可能很好看,实际预测必然翻车。

3.4 数据归一化的正确做法

归一化在时间序列预测里是个暗坑。LSTM的激活函数对输入尺度敏感,数值太大会让训练不稳定,所以归一化确实要做。但是很多人直接对整个序列做min-max归一化,用到了全部数据的最大值和最小值。

仔细想一下:如果训练时你已经知道了整条序列的最大值,那相当于把未来信息泄漏给了模型。测试时你对测试段的归一化,应该只使用训练段统计出来的最小值、最大值。

具体做法是:先把数据按时间顺序切成训练段和测试段,再单独对训练段统计min和max,并把测试段用同样的参数归一化。写成代码就是这样:

trainLen = round(length(soh) * 0.8); sohTrain = soh(1:trainLen); sohTest = soh(trainLen+1:end); minVal = min(sohTrain); maxVal = max(sohTrain); sohTrainNorm = (sohTrain - minVal) / (maxVal - minVal); sohTestNorm = (sohTest - minVal) / (maxVal - minVal);

这一点看似基础,但在很多同学的作业里都会载跟头。模型的训练代码没问题,就是归一化写错了,最后指标虚高,一换数据就崩。

4. 实测中的翻车现场与调参经验

理论讲再多,不如亲手跑一轮训练。我实际跑B0005这颗电池时,前期试了好几个版本,各有各的翻车法,这里汇总整理一下。

4.1 翻车场景一:验证误差一直降不下去

第一个版本里,我设置了'ValidationData'用于验证,但验证集是在后面数据里随机选的。训练过程看起来loss一直在下降,验证误差却居高不下。

排查之后发现问题出在随机验证集上。电池衰减是非平稳的,前期的容量值普遍高,后期普遍低。随机抽出去一部分作为验证集,会让训练数据里缺少某个容量区间,模型很难学习到完整的衰减形态。后来改了策略,直接按时间顺序把后20%循环全部作为验证集,效果立刻好转。

这也提醒了一件事:针对退化数据的划分,尽量不要随机划分。时序数据必须按时间顺序切分,否则会严重低估预测难度。

4.2 翻车场景二:测试开头出现一个尖刺误差

第一版模型在B0005上前80%数据训练,后20%数据测试,测试误差还算平稳,但是测试阶段第一个点的预测值突然偏差很大,形成一个明显的尖刺。

原因是滑窗的“冷启动”问题。训练时每个滑窗的数据都是归一化后的,幅度范围基本一致。但测试段开头第一个样本的滑窗还包含大量训练段末尾的数据,模型在训练时没见过“预测输入里混着训练段和测试段”这种状态,导致第一个预测点跳动特别大。

缓解思路有两个:一个是在测试推理时,先让模型预测测试段第一个点,再把这个预测值当作已知历史,滚动去预测下一个点;另一个是给测试段前面叠加一段训练段的尾部数据作为缓冲。我用的是第二种,因为实现简单,效果也不错。

4.3 翻车场景三:训练loss下降但预测结果是一条近似直线

这个现象比较有迷惑性。loss明明在下降,训练集上的误差也很小,但是画出来测试集的预测曲线,简直是一条水平线,只会在末端稍微波动。

原因在于LSTM被训练成了“记住最后一个输入值”的无脑模型。因为SOH序列本身相邻值非常接近,前一个SOH本身就包含数量极大的信息量,模型发现直接输出上一个时间步的SOH就能让loss很小,于是就不再学习指数趋势或局部曲率变化。这叫“滞后预测”或“记忆退化滞后”。

解决办法有几个方向:

  • 在数据上做差分,让模型去预测SOH变化量而不是SOH绝对值,这样模型没法偷懒。
sohDiff = diff(soh);
  • 或者在loss设计上给变化量更大的权重。
  • 更实用的办法是,在训练阶段加入小噪声到输入序列,让模型不能单纯依赖最后一个值。

我实际采用的是差分法。先预测SOH变化量,再把变化量累加回历史值来得到SOH预测。这一步改动很小,但对模型行为影响很大,测试集曲线明显开始跟随真实衰减趋势。

4.4 参数选择参考表

训练多次之后,我把多次试出来的适宜参数整理成了一张表,这是针对单颗电池、序列长度百来次循环的情况:

参数推荐范围说明
lookback20~40太短学不到长期趋势,太长样本数不足
隐藏单元数16~32单颗电池数据量少,不宜过大
学习率0.001~0.01adam配合分段衰减,效果较好
MiniBatchSize8~16样本数只有几十个,批次太大会震荡
MaxEpochs300~600配合验证集早停更稳

这套参数不是唯一解,不同电池拆成不同训练集后都会有变化,但是方向可以复用。

5. 单电池拟合之外的泛化问题与后续玩法

很多文章做到这一步就停了:拿一颗电池训练,拿同一颗电池的后半段测试,RMSE很好,于是结论是LSTM有效。但这种验证方式离工程应用还有距离。

5.1 单电池测试结果长什么样

用B0005训练、B0006测试的一次典型结果,RMSE大约在2.5%~3%左右,MAE在2%左右,R²不低于0.93。具体数值会随随机种子和划分方式略有浮动,但整体上预测曲线能抓住衰减趋势的大体走向。

有趣的是B0006这种测试电池,如果只拿它自己的前半段做训练、后半段做测试,RMSE反而能降到1.5%以内。原因很简单:同一颗电池的衰减习惯是一致的,包括它自己的电解质消耗速率和极化特征,模型相当于在跟踪一条已经很平滑的曲线。

5.2 跨电池泛化的真实瓶颈

换一颗电池测试时,问题就暴露了。不同电池即使同型号、同批次,初始容量都会有差异,衰减速率也不完全一样。训练时模型把所有样本都归一化到同一个区间,测试时新电池的绝对容量水平出现在一个模型没见过的范围内,预测误差会明显上升。

我试过B0005和B0006数据合并训练、在B0018上测试,误差比单电池自训练大了不少。这说明模型的泛化能力并没有想象中那么强。

跨电池泛化的瓶颈主要在特征表示上。如果只使用归一化后的SOH历史序列,模型几乎不可能区分“这是一颗老化较快的电池”还是“这是一颗初始容量偏低的电池”。解决办法是在输入特征里加入电池自身的一些物理特征,比如初始容量、当前内阻变化趋势、平均温度等,这些特征才是不同电池之间差异的根本来源。

5.3 用容量增量曲线辅助的进阶思路

除了直接预测容量,还有一个工程上非常常见的辅助特征:容量增量(Incremental Capacity,IC)曲线。IC曲线是dq/dV,即单位电压变化对应的容量变化,它的峰值位置和高度与电池内部的相变、活性物质损失状态直接相关。

你可以把每个循环放电过程的dq/dV曲线提取出来,然后把峰值位置或峰面积作为一个循环的特征送入LSTM。这样做的好处是,它显式地把电化学状态信息注入给了模型。缺点也很明显:计算量大,且对数据采样频率有要求,NASA数据集的电压采样点有限,做出来的IC曲线噪声偏大,需要先平滑。如果你打算接着做工程落地,这一块值得深入研究。

5.4 部署和实际落地时还要考虑什么

LSTM在BMS上的落地,目前遇到的更多是算力问题。BMS主控芯片的算力通常很有限,一个完整的LSTM推理过程在嵌入式上可能要跑到几十毫秒甚至更久,而且针对SOH的监管频率一般不需要毫秒级,通常一天估算一次就足够。

更稳妥的工程做法是分成离线训练和在线推理两段。离线用MATLAB训练好模型,然后用深度学习工具箱的代码生成功能,把网络导出成C代码或CUDA代码,部署到目标芯片上。MATLAB的coder.ExternalDependency和相关导出函数在这一步能减少很多重复工作。

我自己的经验是:如果目标芯片连LSTM推理扛不住,就退而求其次,离线用LSTM生成一组训练数据,然后在嵌入式端用一个轻量级的查找表或多项式来逼近。虽然精度略低,但胜在稳定和没有环境依赖。

6. 再分享几点个人经验

聊几个不容易在论文里看到、但实际做项目一定会遇到的细节。

第一点是数据质量永远排在模型结构之前。我一开始拿到NASA数据集,没有先观察容量曲线形状就急着搭网络,结果训练到一半才发现B0018这颗电池早期的容量曲线有一段明显异常跳变。如果没有人眼看过数据、直接用这个序列训练,模型的测试指标会被污染得不轻。

第二点是离线训练时的随机种子问题。MATLAB里可以通过rng(1)设置随机种子。同样的数据、同样的参数,种子不同,最终测试指标会有波动。所以比较不同模型、不同特征组合时,必须固定种子再对比,不然你很难判断效果差异是来自模型改进还是随机波动。

第三点,滑窗长度不要拍脑袋定。我的习惯是先画出SOH随循环数变化的曲线,观察它有没有明显的“平缓段”和“快速衰减段”。如果平缓段持续了40个循环,那lookback至少要比这个尺度更长一点,否则模型看不到一个完整的衰减形态。NASA数据的老化周期不算太长,所以我最后一般落在30左右。如果换成长循环寿命的电池,可能需要50甚至100。

第四点,预测SOH曲线时,务必同时画出置信范围。LSTM本身输出的是一个确定性数值,工程上给BMS输出还需要一个误差界。你可以用多个不同种子训练的LSTM组一个轻量集成,把输出的标准差作为不确定性估计,这样系统在预测值之外还能得到一个可靠度指标,后续采取维修或均衡策略时更有依据。

最后再补充一点,如果只是做课程项目或毕业设计,单颗电池的自训练加后段测试已经足够展示LSTM效果,但建议在陈述时明确区分“拟合能力”和“泛化能力”。如果把跨电池泛化作为亮点,就需要在数据预处理、特征设计和评价指标上多下功夫,否则答辩时很容易被问住。能真正跑通跨电池泛化的方案,确实比自拟合有价值得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询