基于MATLAB和LSTM的电池SOH预测:NASA数据集完整流程

基于MATLAB和LSTM的电池SOH预测:NASA数据集完整流程 简介MATLAB结合LSTM实现电池SOH预测的完整工程包面向从事电池健康管理、电动汽车或储能系统研究的工程师与科研人员。资源包含B0005、B0006两个真实电池老化数据集以及特征提取、充放电循环处理等多个MATLAB脚本可帮助读者系统掌握从数据预处理、LSTM网络搭建到模型评估与预测的完整流程。压缩包共5个文件以.m脚本和.mat数据文件为主包体约30.5MB体量精简但代码与数据结构清晰便于直接运行和二次开发。目前已有633人学习下载适用于希望快速复现基于深度学习的电池SOH预测方法、对比不同数据集效果并开展相关实验的入门及进阶用户。 上个月有个做电池管理系统的师弟跑来找我说他照着网上很多教程用NASA公开电池数据集跑LSTM预测电池SOH但验证集误差一直下不去训练曲线倒是很漂亮。我看了眼他的代码问题出在最开始的数据整理上——他把charge和discharge的容量混在一起当成了时间序列。这个错误太典型了。这篇内容我就用MATLAB把B0005和B0006这两组公开电池数据从读取、清洗到LSTM建模、评估的完整流程过一遍。B0005和B0006是锂离子电池老化实验里最常用的两组数据非常适合练手。文章会讲清楚数据文件的结构、SOH怎么定义、为什么SOH预测适合用LSTM而不是多项式拟合以及训练完之后单步迭代预测怎么实现。想用MATLAB做电池健康状态估计或者刚接触深度学习的工程师、研究生都可以对着这份流程跑通自己的第一版模型。1. 拆开NASA电池数据B0005与B0006的结构和协议1.1 充放电协议B0005和B0006的差别B0005和B0006来自NASA PCoE电池老化数据集是一组商用18650锂离子电池在不同实验条件下循环老化的记录。两者的充电流程基本一致先以1.5A恒流充电到4.2V再转为恒压充电直到电流降到20mA以下停止。区别主要在放电环节B0005以1A的电流放电B0006以2A的电流放电截止电压和静置时间也有对应设置。这个差别很重要直接导致了两组电池的容量衰减速度不同。B0005的放电电流小容量衰减相对平缓B0006放电电流大老化速度更快、容量曲线下降更陡。所以你把同一套模型从B0005搬到B0006时不能指望超参数完全一样后文我会专门说怎么迁移。两组电池的停止条件都是容量衰减到额定容量的70%左右。实验做了大概一百六十多次循环就终止了。对深度学习来说这个样本量非常小所以数据预处理做得对不对比模型选得多花哨更关键。1.2 从.mat文件结构里找到真正的容量序列NASA数据集提供的是.mat文件MATLAB可以直接加载。你需要先去官网下载B0005.mat和B0006.mat放到当前工作目录然后执行load(B0005.mat); whos你会看到一个名为B0005的结构体。它的核心是B0005.cycle这是一个结构体数组里面每一条记录代表一次测试动作不是严格的“一次循环”。所以必须用type字段区分for k 1:length(B0005.cycle) disp(B0005.cycle(k).type); end运行之后你会看到三类charge、discharge、impedance。我们要的容量数据只在discharge类型里字段路径是B0005.cycle(i).discharge.Capacity单位是Ah。discharge结构体下面还保留了整条放电过程的电压、电流、温度和时间序列比如Voltage_measured、Current_measured、Temperature_measured这些字段后面如果要加多维健康因子就从这里取。1.3 第一版模型效果差的根源数据序列错位我在很多人的代码里见到同一个错误直接用for循环遍历全部cycle把所有Capacity字段都取出来拼成一个序列。问题在于charge和impedance记录里没有Capacity字段取出来就是空值或直接报错一旦你用了某些“默认值”填充时间序列就被人为扭曲了。更隐蔽的做法是把每个cycle里的discharge.Capacity和charge数据混在一起画图看起来也能看出下降趋势但喂给LSTM时序列里相邻两个点可能一个是充电末期的电压测量值、一个是放电容量值模型学到的依赖关系完全是乱的。正确做法很简单先筛出type为discharge的记录再按顺序提取容量。我会在第三章给出完整代码。2. 为什么非用LSTM容量再生现象让传统曲线拟合失效2.1 容量序列里的“小回跳”和它的物理来源如果你把B0005的放电容量随循环数画出来会发现整体趋势是下降的但并不是一条平滑下降的曲线。每隔几轮循环容量会出现一次小幅回升然后再继续下降。这个现象叫容量再生capacity regeneration。物理原因也不复杂电池在静置或阻抗测试过程中内部电化学状态会发生一定程度的恢复锂离子重新分布活性物质的可及性短暂提升表现出来就是下一轮放电容量比上一轮高。这个现象在所有电池老化数据里都存在只是程度不同。传统方法处理这个问题很头疼。你用指数衰减函数去拟合整条容量曲线抓得住长期趋势但残差里全是再生的尖峰你用粒子滤波或卡尔曼滤波又必须对过程噪声和观测噪声做较强的假设如果模型跟实际再生周期对不上滤波结果会严重滞后。2.2 传统拟合、滤波和LSTM的本质差别多项式拟合、指数拟合本质上是在做“全局回归”用一条固定函数形式描述整个时间范围。问题是电池老化在不同阶段有不同的衰减速率初期快、中期平稳、末期加速一个全局函数很难同时拟合这些局部特征。卡尔曼滤波和粒子滤波属于状态估计方法需要你建立电池容量衰减的状态方程。学术界有不少工作这么做但工程上最大的问题是状态噪声到底怎么建模才能把“再生”这种突然变化也估计进去这个问题没有统一答案调起来很费时间。LSTM的思路不同。它不做全局函数假设而是从数据里学局部时间模式。每个时间步它接收当前输入和上一时刻的隐藏状态通过遗忘门、输入门、输出门来决定保留什么、更新什么、输出什么。经过足够多训练样本它能学会“整体往下走但局部可能回升”这类模式并将记忆在隐状态里传递下去。2.3 为什么选LSTM而不是RNN或GRU既然数据是时序的那常规RNN理论上也能处理但RNN在长序列上存在梯度消失问题一百多个循环的序列已经足以让早期信息传不到末端。GRU参数少、训练快在B0005这类小样本上其实也值得试但LSTM在电池SOH预测领域文献更丰富、公开案例更多遇到问题更容易找到参考所以作为入门首选更稳妥。这里多说一句数据集只有一百多条样本没必要一上来就堆Transformer或注意力机制。模型复杂度超过数据承载能力之后过拟合比欠拟合更难处理。先用简单的LSTM把流程跑通再逐步加复杂度这是最务实的路径。3. 训练前最关键的一步健康因子提取与滑动窗口构造3.1 提取SOH的MATLAB代码SOH最直接的定义是当前最大可用容量与额定容量的比值。NASA数据集没有直接给额定容量我们通常把第一次放电循环测得的容量当作基准load(B0005.mat); cap []; for k 1:length(B0005.cycle) if strcmp(B0005.cycle(k).type, discharge) cap(end1) B0005.cycle(k).discharge.Capacity; end end SOH cap / cap(1) * 100;这里解释一下为什么要转成百分比。B0005初始容量大约1.85AhB0006接近2.0Ah如果直接用Ah作为标签两组电池的数值范围不同模型跨电池迁移时没有可比性。转成SOH后起点都是100%衰减终点都在70%左右不同电池之间有了统一尺度。提取后建议画一下SOH曲线确认数据没问题plot(SOH, LineWidth, 1.5); xlabel(Cycle); ylabel(SOH (%)); grid on;如果画出来是一条干净、整体下降、带局部回升的曲线说明数据抽取正确。3.2 构造滑动窗口并划分数据集LSTM不是把整条序列一次性扔进去就完事。我们需要构造“窗口到下一步”的训练样本。假设窗口长度win20意思是用第1到第20个循环的SOH作为输入第21个循环的SOH作为标签然后窗口右移一步用第2到第21个循环预测第22个循环依此类推。win 20; X {}; Y []; for i 1:length(SOH) - win X{end1, 1} SOH(i:iwin-1); % 1 x win Y(end1, 1) SOH(iwin); % 第 iwin 个循环 end这里X是cell数组每个元素是一个1×win的行向量。正常来说sequenceInputLayer要求每个样本是numFeatures×numTimeSteps的形式我们的特征维度是1时间步是20所以用行向量即可。数据划分必须按时间顺序绝不能随机打乱。时间序列一旦shuffle验证集就会“偷看”训练集未来的信息模型性能评价就完全失真了。numTrain floor(length(X) * 0.7); numVal floor(length(X) * 0.15); XTrain X(1:numTrain); YTrain Y(1:numTrain); XVal X(numTrain1:numTrainnumVal); YVal Y(numTrain1:numTrainnumVal); XTest X(numTrainnumVal1:end); YTest Y(numTrainnumVal1:end);3.3 要不要归一化如果模型只输入SOH这一个特征SOH的数值范围在70到100之间量纲本身就很小不归一化完全可以直接训练我实测下来收敛情况很好。但如果想把放电时间、平均放电电压、平均温度等作为额外特征加入模型就必须做归一化。因为这些特征的数值尺度差异很大比如温度可能是20度左右电压是3到4伏放电时间可能是几百秒不归一化会让数值范围大的特征主导梯度更新模型训练会变得很不稳定。常见做法是zscore标准化SOH_std (SOH - mean(SOH)) / std(SOH);不过在单特征的入门示例里保持原始SOH数值就行预测结果反归一化时也少一道步骤。4. 网络搭建和训练选项小样本下如何避免过拟合4.1 网络结构不是越深越好这里给出一个在网络结构上相对克制但效果稳定的方案。对B0005这样只有一百多个样本的数据集深层LSTM反而容易过拟合。layers [ sequenceInputLayer(1) lstmLayer(50, OutputMode, last) fullyConnectedLayer(1) regressionLayer ];逐个解释sequenceInputLayer(1)输入特征维度是1即SOH。lstmLayer(50, OutputMode, last)50个隐藏单元last表示只输出最后一个时间步的隐藏状态。因为我们是用20步的窗口预测第21步这是一个sequence-to-one任务只需要最后一步的输出。fullyConnectedLayer(1)将隐藏状态映射成1个实数值也就是SOH预测值。regressionLayer回归任务对应的损失层。有人会给我看100层那种复杂结构但我要泼一盆冷水样本量就这么多隐藏单元太多网络会把训练样本里的噪声背下来而不是学到容量衰减的规律。50个隐藏单元是我在NASA几组电池数据上试出来比较稳的经验值如果你换了数据集可以从20到100之间做简单搜索。4.2 训练选项Adam、学习率衰减和验证集监控训练选项用trainingOptions配置options trainingOptions(adam, ... MaxEpochs, 300, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 100, ... LearnRateDropFactor, 0.5, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 20, ... Verbose, 1, ... Plots, training-progress);几个关键参数逻辑说一下MaxEpochs300数据量少单轮训练时间很短多迭代几轮才能收敛到稳定误差。InitialLearnRate0.005LSTM在小数据集上学习率不宜太大0.01以上很容易损失震荡。LearnRateSchedulepiecewise每100轮学习率乘0.5后期用小学习率做精细调整防止在最优解附近来回震荡。ValidationData{XVal, YVal}用验证集在每个ValidationFrequency周期计算一次损失。训练进度图里会同时画训练损失和验证损失这是判断过拟合最重要的工具。4.3 过拟合的早期信号是什么训练误差持续下降、验证误差在第几十轮开始掉头向上这就是过拟合的典型信号。一旦出现这种情况优先处理顺序是增加验证集监控频率、降低隐藏单元数、加入dropout层、增大训练数据量。数据量已经有限所以最先调的是网络复杂度而不是盲目加训练轮数。如果验证损失从一开始就不降先别怀疑模型回到第三章检查数据SOH序列是否连续窗口划分是否正常X是不是cell数组Y是不是列向量维度问题在MATLAB里的报错有时候不够直观这几个位置是高频出错点。5. 滚动迭代预测与误差评估模型性能到底怎么看5.1 为什么用单步滚动而不是直接多步输出训练时我们给的是一段窗口和紧挨着的下一时刻标签这叫teacher forcing。测试时真实未来值不可见所以必须让模型自回归拿测试集第一个窗口做种子预测下一步再把预测值拼到窗口尾部、丢掉最前面的值循环往复。这样做的原因是LSTM本身就是自回归结构训练时用的是历史信息预测时最合理的方式也是用预测值作为下一时刻的输入。需要特别说明的是迭代步数越长误差会累积所以评估时通常关注前20到40步的预测效果就已经能说明问题不要求从第1步滚到第168步还保持高精度。5.2 单步滚动预测的MATLAB实现seed XTest{1}; YPred zeros(length(XTest), 1); for i 1:length(XTest) yhat predict(net, {seed}); if iscell(yhat) yhat yhat{1}; end YPred(i) yhat(end); seed [seed(2:end), yhat(end)]; end这里加了一行iscell判断因为不同MATLAB版本对predict返回值的类型处理不完全一样加上这个判断能提高代码的健壮性。YPred就是模型对测试集各时间点的SOH预测值YTest是真实值。两者直接相减就能算误差指标。5.3 评估指标和参考经验值我习惯同时看RMSE、MAE和R²三个指标单独一个指标容易被极端值误导。RMSE sqrt(mean((YPred - YTest).^2)); MAE mean(abs(YPred - YTest)); R2 1 - sum((YTest - YPred).^2) / sum((YTest - mean(YTest)).^2); fprintf(RMSE %.4f, MAE %.4f, R2 %.4f\n, RMSE, MAE, R2);R²的定义是模型解释了多少方差最理想是1可以为负负值说明模型预测比直接用均值还差。根据我的实测经验在B0005这种单个电池测试集上的单步滚动预测RMSE在0.02到0.04之间是比较合理的范围R²一般能到0.9以上。如果RMSE超过0.05几乎可以肯定是数据处理环节出了问题先回第三章检查。把预测结果画在真实SOH曲线上是判断模型是否学到了衰减趋势最直观的方法testIdx (numTrain numVal 1) win : length(SOH); plot(testIdx, YTest, LineWidth, 1.5); hold on; plot(testIdx, YPred, --, LineWidth, 1.5); legend(真实SOH, 预测SOH); xlabel(Cycle); ylabel(SOH (%));6. 完整代码实测与高频踩坑排查6.1 可以直接跑通的B0005完整代码下面这段代码把从数据读取到评估绘图的完整流程放在一起在MATLAB R2021b及以后版本上可以直接运行。前提是你已经下载了B0005.mat并放到当前目录。% 1. 读取数据并提取SOH load(B0005.mat); cap []; for k 1:length(B0005.cycle) if strcmp(B0005.cycle(k).type, discharge) cap(end1) B0005.cycle(k).discharge.Capacity; end end SOH cap / cap(1) * 100; % 2. 构造滑动窗口样本 win 20; X {}; Y []; for i 1:length(SOH) - win X{end1, 1} SOH(i:iwin-1); Y(end1, 1) SOH(iwin); end % 3. 时间顺序划分 numTrain floor(length(X) * 0.7); numVal floor(length(X) * 0.15); XTrain X(1:numTrain); YTrain Y(1:numTrain); XVal X(numTrain1:numTrainnumVal); YVal Y(numTrain1:numTrainnumVal); XTest X(numTrainnumVal1:end); YTest Y(numTrainnumVal1:end); % 4. 定义网络 layers [ sequenceInputLayer(1) lstmLayer(50, OutputMode, last) fullyConnectedLayer(1) regressionLayer ]; % 5. 训练选项 options trainingOptions(adam, ... MaxEpochs, 300, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 100, ... LearnRateDropFactor, 0.5, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 20, ... Verbose, 0, ... Plots, training-progress); % 6. 训练 net trainNetwork(XTrain, YTrain, layers, options); % 7. 单步滚动预测 seed XTest{1}; YPred zeros(length(XTest), 1); for i 1:length(XTest) yhat predict(net, {seed}); if iscell(yhat) yhat yhat{1}; end YPred(i) yhat(end); seed [seed(2:end), yhat(end)]; end % 8. 评估 RMSE sqrt(mean((YPred - YTest).^2)); MAE mean(abs(YPred - YTest)); R2 1 - sum((YTest - YPred).^2) / sum((YTest - mean(YTest)).^2); fprintf(RMSE %.4f, MAE %.4f, R2 %.4f\n, RMSE, MAE, R2); % 9. 绘图 testIdx (numTrain numVal 1) win : length(SOH); plot(testIdx, YTest, LineWidth, 1.5); hold on; plot(testIdx, YPred, --, LineWidth, 1.5); legend(真实SOH, 预测SOH); xlabel(Cycle); ylabel(SOH (%)); grid on;6.2 B0006迁移和跨电池验证思路把B0005换成B0006代码里最容易的改动是把load(B0005.mat)换成load(B0006.mat)变量名从B0005改成B0006。但要注意B0006放电倍率更大容量下降更快序列的波动模式也和B0005不完全一样。我的建议是先把win从20降到15试试因为更短的窗口能更快适应更剧烈的变化如果验证误差不理想再对隐藏单元数量做小范围搜索。如果你想做更有说服力的实验可以用B0005训练、B0006测试或者在两组数据拼接后训练一个统一的模型。前者的评估结果更能说明模型的泛化能力后者则需要先解决两组电池SOH尺度统一的问题我在这篇文章开头已经把SOH转换成百分比了这为跨电池训练扫清了一个大障碍。6.3 高频踩坑排查表现象可能原因解决办法训练时报维度错误X不是cell数组或cell元素尺寸不一致用class(X)检查类型cellfun(size, X)检查尺寸验证损失不下降窗口过短、学习率过大、特征未归一化增大win到25或30学习率降到0.002训练损失降验证损失升网络过拟合减小hidden units加入dropout减少epoch预测曲线滞后真实曲线一拍窗口太短模型没学到趋势增大win观察预测曲线形态预测步数越远越发散自回归误差累积导致这属于正常现象只看前20到40步B0006效果明显差于B0005数据分布不同超参未调整减小win重新调训练参数6.4 可以继续深入的方向文章到这里用LSTM预测电池SOH的主流程已经完整跑通。接下来有几个很自然的扩展方向把放电时间、平均放电电压、放电平均温度作为额外特征一起送入LSTM输入维度从1变成多维用贝叶斯优化或Grid Search对win、hidden units、InitialLearnRate做自动搜索把B0005、B0006、B0007、B0018四个电池的数据合并训练然后逐个电池验证泛化能力。我的个人体会是这类电池健康预测任务80%的功夫都在数据整理和质量检查上模型选LSTM只是把已经整理好的时间规律学出来。很多人卡住不是因为LSTM难而是数据准备阶段埋了雷。把B0005跑通不难难的是你愿意把整条数据链路从头到尾检查一遍。按本文的流程走你已经避开了那些最常踩的坑。本文还有配套的精品资源点击获取