☰
基于Python和LSTM的锂离子电池SOH与RUL寿命预测实战
2026/10/1 16:45:09 网站建设 项目流程

简介:一套面向毕业设计场景的完整项目,提供基于Python和机器学习的锂离子电池寿命预测实现,适合需要完成相关课题或期末大作业的本专科生。压缩包内共2000个文件,体积65.89MB,涵盖7个py源码文件、1个ipynb分析文档、24个npy数据文件、15个pkl与5个pth模型相关文件,以及xlsx/xls数据表格、png可视化图和pdf说明文档等,目录结构清晰,兼顾训练、评估与结果展示。项目代码经过优化与测试,包含数据预处理、特征工程、模型训练和评估环节,可从原始电池循环数据出发完成寿命预测全流程。配合模型文件、数据处理notebook与说明文档,读者能快速复现结果,理解迁移学习或回归预测在电池健康管理中的应用;大量png图表也为撰写报告和答辩展示提供了直观素材。已有360人学习,对想系统完成毕业设计或积累机器学习项目经验的同学有较高参考价值。

1. 用 Python 从零搭一套锂离子电池寿命预测项目,值得动手吗

在毕设季里,“锂离子电池寿命预测”是出现频率很高的题目,原因很简单:公开数据容易找、算法方向明确,还带一点硬科技色彩。但你拿到的“项目源码+数据集+模型”这份压缩包,本质上不只是代码,而是一条完整链路:从电池循环充放电数据里提取容量衰减序列,用 Python 构建序列模型,训练出能预测健康状态和剩余寿命的模型,最后用指标证明它有效。适合两类人:一是要交付毕业设计、需要快速跑通并讲清楚原理的学生;二是刚转入电池算法方向、想拿公开数据练手的新工程师。我给你的建议是先把目标拆清楚:这个项目真正难的往往不是模型,而是数据划分方式——划分不对,再好的网络也会翻车。

2. 先解决预测什么:SOH、RUL 与公开数据的选型逻辑

2.1 先分清目标:SOH 和 RUL 到底在预测什么

电池寿命预测通常拆成两个指标:SOH 和 RUL。SOH 是 State of Health,健康状态;RUL 是 Remaining Useful Life,剩余寿命。SOH 的定义比较统一,当前最大可用容量除以额定容量,出厂时是 1.0,随着循环衰减。工程上一般认为 SOH 掉到 80% 以下时电池寿命终止,这个阈值来自实际应用中容量衰减到一定程度后内阻增大、功率特性变差的工程经验。

RUL 就是从现在到寿命终止还能跑的循环数,它和 SOH 本质上是同一个过程的两个视角:SOH 描述“现在衰到哪了”,RUL 描述“还能撑多久”。放在代码里看,假设当前在第 100 圈,真实容量下降到额定容量的 70%,那么寿命终点大约在第 150 圈,RUL 就是 50 圈。两者是单调映射关系,但建模难度差别很大。

用 Python 做这个项目时,我建议把 SOH 当主任务,RUL 当派生任务。原因很实际:RUL 只有到寿命终点时才能算精确标签,样本标签是一个严格递减的整数序列,信息量低;而 SOH 每个循环都有一个真值,是标准的回归问题,模型容易训练,答辩时也更好讲。实现时先让模型输出下一圈的 SOH,再人工判断它掉到 0.8 时对应第几圈,RUL 自然就出来了。这个角度看起来简单,但很多毕设源码会在这一步埋坑:直接拿 RUL 当回归目标,数据集又小,训练出来的模型几乎全在背数字,换一块电池就失灵。

2.2 公开数据集怎么选:从 NASA 到 CALCE 的经验

寿命预测能成为热门毕设题,有一个前提:公开数据集足够规范。最常用的是 NASA PCoE 随机电池数据集,就是常说的 B0005、B0006 那组文件;它的数据是 .mat 格式,里面按循环记录了电压、电流、温度、放电容量,压缩包里“数据集”目录对应的通常就是这类文件。日常做验证,我一般先选同一工况下至少四块电池:三块用来训练,一块留作测试,这是支撑“模型泛化性”结论的最低配置。

马里兰大学的 CALCE 数据也用得多,CSV 格式居多,字段更直观,适合不想和 MATLAB 格式打交道的同学。牛津大学的电池退化数据集适合做更精细的老化特征分析,但维度和数据量都比前两者杂,新手不建议第一步就碰。挑选数据集的判断标准有三条:一是同类电池数量不少于三块,否则训练和测试的分割没有说服力;二是要有明确的循环记录,能还原出每放一次电的容量,而不是只有聚合统计量;三是容量序列要能看到明显的下降趋势,至少掉到初始容量的 85% 左右,这样才有“预测寿命”的意义。

满足这三条,才值得写进论文的实验部分。相反,如果网上某个源码包缓存了一份来源不明的二手数据,我建议果断换掉,因为答辩老师一旦问“数据来源和标定方式”,回答不上来会很被动。数据这一块弄对了,后面所有代码才有根;数据源头选错,后面模型做得再漂亮,论文也站不住脚。还有一点容易被忽略:NASA 原始 .mat 文件里除了放电容量,还有电压、电流、温度的时间序列,但毕设阶段不要一上来就全塞进模型。先用容量单通道跑通链路,再逐步加入温度特征做对比实验,这个节奏比一开始就堆多变量要稳。

2.3 为什么毕设优先用数据驱动而不是等效电路模型

你可能在原理部分见过等效电路模型、电化学模型那套方法,它们适合描述机理,但在毕设场景下很难落地:需要辨识欧姆内阻、极化内阻等参数,电池老化以后还得重新辨识,参数之间互相耦合,新手调起来非常痛苦。而数据驱动的思路简单得多:把容量衰减序列当成一个时间序列,让模型自动学习“过去几十圈长了什么样、下一圈大概率变成什么样”,训练完直接预测。

模型选型也有讲究。RNN 系里的 LSTM 和 GRU 在这个场景下表现稳定,文档多、可引用案例多,调参路径成熟;TCN 训练更快,但论文里可引用的电池寿命案例比 LSTM 少,遇到问题排查资料也少;Transformer 虽然热门,但在几百条样本的小数据集上很容易过拟合,训练时还需要更多 epoch 才能看到收敛趋势,显存占用也不划算。所以“项目源码+模型”里的模型,常见做法就是两层 LSTM 加一层全连接,参数规模很小,CPU 也能训练。不必迷信复杂结构——先跑通数据闭环,再把结构替换成 GRU 或 TCN 做对比实验,这才是合理的推进顺序。数据量只有几百条时,模型结构的边际收益远小于数据处理方式的收益,这句话在寿命预测任务里尤其成立。

3. 用 Python 跑通寿命预测最小闭环:数据解析、滑窗构造与 LSTM 训练

3.1 从原始充放电记录到容量衰减曲线:数据解析与逻辑说明

拿到 .mat 文件以后的第一步,是把充放电循环记录解析成一条容量衰减曲线。NASA 数据的内部结构是按 cycle 组织数组,每个循环里有 type 字段区分 charge、discharge、impedance 三类记录。容量真值一般取放电容量,因为放电过程更容易精确计量,充电末端常受恒压阶段影响,电量的数值口径不稳定。

import numpy as np from scipy.io import loadmat # 加载 NASA 电池老化数据 mat = loadmat('B0005.mat') battery = mat['B0005'][0, 0] # 取出所有循环记录 cycles = battery['cycle'][0, 0] # 结构是 (1, N),N 为循环数 cap_curves = [] # 遍历每个循环,只取 discharge 记录中的容量字段 for idx in range(cycles.shape[1]): entry = cycles[0, idx] record_type = entry['type'][0] if record_type == 'discharge': capacity = float(entry['data'][0, 0]['Capacity'][0, 0]) cap_curves.append([idx, capacity]) cap_curves = np.array(cap_curves) # shape: (num_cycles, 2)

这段代码的核心逻辑是用 type 字段做过滤,再从 data 结构里取出 Capacity 标量。需要注意进入 charge 记录时还要区分恒流和恒压阶段,数据解析有一个经典的坑:不要把充电末期的容量当全部容量。NASA 原始数据里每循环的 discharge 记录通常只有一条,而 charge 可能有两到三条,如果解析逻辑不严格,画出来的容量曲线就是乱跳的。解析完先打印 shape 确认循环数在 100 到 200 的量级,再做下一步。

用 B0006、B0007、B0018 等文件重复同样的解析,然后把这些电池的容量曲线画在一张图上。这一步不是走过场,你至少要从图上确认三点:第一,曲线整体是下降的;第二,下降过程里有没有明显的容量再生台阶;第三,不同电池的初始容量是否一致。如果发现某块电池的初始容量比别的电池高出一大截,那可能是额定容量定义不一致,需要单独处理,而不是混在一起训练。

3.2 用滑窗把容量序列变成监督学习样本

模型要训练,得先把一维的容量序列变成“输入过去、预测未来”的监督样本。这一步叫滑动窗口构造,也叫滑窗法:设定一个窗口长度 seq_len,从第 0 圈开始,连续取 seq_len 个点作为输入,第 seq_len+1 个点作为标签,然后窗口整体往后挪一步。这样一条 168 圈的序列,能生成 168 减 seq_len 条样本。

def build_sliding_windows(capacity, seq_len=40, stride=1): """把容量序列切成 (X, y) 监督样本。""" samples = [] length = len(capacity) # 越靠后的窗口,越靠近当前预测起点 for start in range(0, length - seq_len, stride): end = start + seq_len X = capacity[start:end] # 过去 seq_len 圈的 SOH y = capacity[end] # 下一圈的 SOH samples.append((X, y)) return np.array([s[0] for s in samples]), np.array([s[1] for s in samples])

参数上,seq_len 默认 40,stride 默认 1。stride 越大样本越少、训练越快,但曲线信息利用不充分;我一般在数据量不足时保持 stride=1。这里有个容易踩坑的位置:shuffle 不能放在整条容量序列上做,而是要在生成窗口之后,对样本整体做 shuffle;更严格一点,还要保证同一块电池的样本不会被随机分配到训练集和测试集两边——这条后面避坑章会展开。

滑窗生成后,需要把 X 的形状从 (样本数, seq_len) 改成 (样本数, seq_len, 1),因为 LSTM 期望输入最后一个是特征维度。如果你后面要加温度特征,这里就变成 (样本数, seq_len, 2)。我一般在这个环节顺手把数据切成训练集和测试集,测试集直接选某一整块电池的全部窗口,而不是随机抽 20% 样本——随机抽样本在时序任务里几乎没有意义。

3.3 训练一个 LSTM 寿命预测模型:PyTorch 逐段解释

样本形状是 (样本数, seq_len, 1),正好喂给 LSTM。模型结构很简单:两层 LSTM 提取时序特征,取最后一个时间步的输出接一个全连接层,输出下一圈 SOH。下面的定义可以直接在 CPU 上跑通,不需要额外处理 CUDA。

import torch import torch.nn as nn class SOH_LSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() # 两层 LSTM,batch_first=True 让输入形状为 [B, seq_len, input_size] self.lstm = nn.LSTM(input_size, hidden_size, num_layers, dropout=dropout, batch_first=True) # 只取最后一个时间步,所以展开成一个标量 self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # [B, seq_len, hidden] last = out[:, -1, :] # [B, hidden] return self.fc(last) # [B, 1]

训练循环里我习惯把样本封装成 TensorDataset,用 DataLoader 分 batch 加载。通常 batch_size 取 32,Adam 优化器 lr 取 1e-3,MSE 损失,训练轮数 100 到 200,同时监控验证集 RMSE 做早停。

from torch.utils.data import TensorDataset, DataLoader # 假设 X_train: [N, seq_len, 1],y_train: [N, 1] dataset = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) loader = DataLoader(dataset, batch_size=32, shuffle=True) model = SOH_LSTM(input_size=1, hidden_size=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.MSELoss() for epoch in range(200): for xb, yb in loader: pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step()

训练完成后保存 state_dict 作为压缩包里的“模型”部分;做推理时只保留最后一层全连接输入所需的内存,所以显存占用非常低——哪怕只有 4GB 显存的笔记本,也能把 168 圈的 NASA 数据完整训练好几遍。如果你是在纯 CPU 环境,把 hidden_size 降到 32,训练时间仍然在一两分钟内能结束,属于低显存甚至无 GPU 也能复现的典型方案。推理时有个细节:预测下一圈时,要把预测值拼回输入序列末尾,再丢掉最开头一个值,形成长度为 seq_len 的新窗口,这样才能继续往下预测。这个递推过程单独写一个循环,不要依赖 DataLoader。

4. 决定预测精度的三个坑位:滑动窗口、归一化与训练策略

4.1 滑动窗口长度怎么定:40 圈的依据与边界

在电池寿命预测里,seq_len 是影响结果最直接的参数,它决定了模型能看到多长的“历史记忆”。容量序列有两个尺度:短尺度上,单次循环的测量噪声和微小波动;长尺度上,整体容量随循环下降的趋势。seq_len 太短,比如小于 10,模型只学到局部噪声,预测曲线会紧跟真实值来回抖动;太长,比如大于 80,样本数量骤减,训练不稳定。

我的经验值是 seq_len=40,理由有三:第一,NASA 电池全生命周期大约 120 到 168 圈,40 圈约占整个寿命的四分之一,足够容纳一次明显的容量再生波动;第二,生成的有效窗口数量在 80 到 120 之间,刚好维持 DataLoader 的稳定梯度;第三,论文里可引用的同参数工作很多,答辩容易站得住。

如果你把目标换成直接预测 RUL 而非 SOH,可以把窗口加大到 60,给模型更长的趋势上下文。窗口变大后样本数量下降,这时可以把 stride 从 1 改成 2 来补偿,虽然相邻窗口信息有重叠,但能保证训练样本量不过少。另外提醒一句:滑动窗口滤波模型是信号处理里做平滑去噪的东西,和这里构造训练样本的滑窗法是两回事,不要混进代码里。你可以在项目里同时保留两者:先对容量序列做滑动窗口滤波去掉高频噪声,再用滑窗法构造样本,顺序不能反,否则会再次引入信息泄露。

4.2 归一化方式决定了会不会“测试集泄题”

归一化是毕设翻车重灾区。很多人拿到容量序列,先按全序列最大值最小值做 min-max 归一化,再切训练集测试集。表面看指标极好,实际上测试集的最大值和最小值已经被模型“看见”了,这在形式上属于信息泄露。容量序列从 1.0 衰减到 0.7 左右,如果你用全局 min/max 归一化,等于提前告诉模型整条曲线要掉到哪,预测自然准。

正确做法有两种。第一种是只对训练集拟合 scaler 参数,再应用到验证集和测试集,scikit-learn 的 MinMaxScaler 就能做,代码上先 fit 训练数据再 transform 所有数据:

from sklearn.preprocessing import MinMaxScaler # 只从训练段拟合,再应用到全部数据 scaler = MinMaxScaler() train_scaled = scaler.fit_transform(X_train.reshape(-1, 1)) X_train = train_scaled.reshape(X_train.shape) X_test = scaler.transform(X_test.reshape(-1, 1)).reshape(X_test.shape)

第二种更推荐:容量本身是物理量,直接除以额定容量,比如新电池标称 2.0Ah,就得到 0 到 1 的相对值 SOH,不做任何数据形态的归一化变换。这条做法的好处是物理含义不变、可解释性强,而且不会引入任何来自“未来”的信息。因为模型输入只有 SOH 序列,量纲天然统一,LSTM 在这种小幅值输入下收敛也很快。我自己的项目里这两条路都走过,最终留的是物理归一化:模型训练少一个可调参数,答辩时还能把“防止信息泄露”作为一个设计亮点。

注意:不要把平滑滤波当成归一化的一部分来做,先隔离训练集和测试集,再做任何变换。凡是“用整条曲线算出来的统计量”出现在特征里,都要警惕。

4.3 损失函数、批大小与早停的参数基线

小样本时序回归任务里,参数不用追求奇技淫巧,稳定的组合就是好的组合。下面是我在 NASA 数据集上反复验证过的参数基线,供你直接抄作业:

参数常用值表现特征毕设建议
滑动窗口 seq_len40覆盖趋势,保留样本量20 到 60 之间做对比实验
批量大小 batch_size32梯度稳定,训练快速16 到 64,样本少时优先 16
优化器Adam收敛快保持默认 beta 参数
学习率1e-3收敛稳定不收敛时降到 1e-4
损失函数MSE对大偏差敏感可加 Huber 对离群点鲁棒
早停patience=30防止过拟合监控验证集 RMSE
最大轮数200足够收敛早停优先于调轮数

这里特别说下损失函数。MSE 在预测值离真实容量差 0.05 时会给出很大梯度,适合强调大误差场景;但容量再生造成的高波动会让 MSE 的梯度不稳定。我试过 Huber loss,delta 取 0.01,训练曲线明显更平滑。对毕设来说,MSE 最容易解释,答辩时不建议刻意绕开;如果实验里你发现预测曲线末端发散,就把 Huber 加进对比表里,这是很好的亮点。

早停的 patience 设 30 圈比较稳妥,太多会让模型在训练集上磨洋工,太少则容易在损失下降中途被掐断。监控指标用验证集 RMSE,而不是训练 loss,因为训练 loss 一直在降,参考价值低。如果四块电池分成三训练一测试,没有独立验证集,可以只训练 100 轮取最后 20 轮的平均预测作为输出,效果通常比挑最低训练 loss 的 checkpoint 稳定。环境方面,这套方案在 CPU 上跑完全没问题,批大小 16 加 hidden_size 32 的组合,单次训练不超过两分钟,不依赖大显存。

5. 毕设避坑指南:数据泄露、容量再生与评估口径

5.1 数据泄露:测试集指标好看,答辩一问就露馅

现象:训练完在测试集上 RMSE 只有 0.02,SOH 预测曲线和真实曲线几乎重合,但换一块电池或换一段工况就完全失效。

原因:归一化时用到了全序列的 min/max,测试集的未来信息在预处理阶段混进了特征;另一种常见变体是直接对整条容量序列做平滑滤波后再切训练测试,同样属于用未来平滑过去。

解决:训练集、验证集、测试集在进入任何变换之前就要严格隔离;归一化只允许从训练集统计参数;如果做平滑滤波,只能在训练集段内做,且测试段的滤波核不得看到测试段之外的样本。判断标准很简单:逐行检查预处理脚本,凡是“用全部数据算统计量”的那一行,都要停下来问自己——这段未来信息会不会通过特征进入模型。

5.2 按循环随机划分样本导致同电池前后样本串扰

现象:训练损失下降很快,测试损失也很低,但测试集是随机从所有循环里抽的,真实效果自我感觉良好。

原因:把同一块电池的 1 到 168 圈随机分成 train、val、test,相邻窗口序列有大量重叠。比如窗口 40 到 80 和 41 到 81 几乎同源,一个进了训练集,一个进了测试集,等于模型拿着近乎标准答案考试。

解决:样本划分必须按电池实体分,而不是按循环序号分。至少三块电池训练、一块电池独立测试。如果你只有四块电池,只能三训练一测试,不要再单独拆验证集,可以用早停轮数代替验证集监督。这条要求不仅在代码层面,在论文里也要写清楚,否则评审时会直接被质疑实验合理性。很多开源毕设源码的 README 里写着“随机划分数据集”,这句轻飘飘的说明就是最大雷点,照做的人答辩时几乎都会被问住。

5.3 容量再生现象让预测曲线局部回升

现象:真实容量序列在连续下降中出现阶段性的小幅回升,模型为了贴近训练集,把回升“学”进了预测,预测曲线跟着反弹,看起来像过拟合,但实际是数据本身的非线性。

原因:锂离子电池存在容量再生机制,特别是镍钴体系电池,在循环搁置后容量会季节性恢复。这种小幅度回升不是测量噪声,而是真实物理规律。

解决:分两步。第一,预处理阶段可以对容量序列做中值滤波或移动平均,窗口取 3 到 5,削弱小幅度再生;第二,如果不想引入额外滤波参数、担心难以解释,就把预测目标从“下一圈 SOH”改成“未来三圈的平均 SOH”,用平滑过的目标函数让模型专注于趋势。不要试图在 LSTM 里强行学高频再生成分,那不是几百个样本能表达的信息。你在论文里也可以把容量再生作为研究对象来分析,说明模型对它不敏感是合理取舍,而不是缺陷。

5.4 滑窗末端样本不足,预测覆盖不了整个寿命周期

现象:模型在中间段预测得很漂亮,但寿命后段曲线没跑完,预测提前终止,RUL 输出不了。

原因:滑窗要求完整的“输入+标签”二元组,序列最后 seq_len 圈根本没有对应的标签,所以预测天然截止在 len 减 seq_len 的位置。

解决:两种做法可自选。第一种是训练时把标签右移一位,预测时对最后一段序列不取标签,直接用最后 seq_len 个观测值预测下一个未知值,递推多步预测 RUL;第二种是只预测到已知序列的末尾,把“从当前到最后一段”做成存活曲线,再用阈值 0.8 求循环数。递推预测的代价是误差逐圈累积,超过 20 圈后累积误差明显,所以论文里要交代预测长度边界。如果毕设要求的是 RUL 超过 30 圈的预测,建议改成“位置预测加不确定性区间”,具体算法见下一章。预处理阶段把容量序列延拓几个点也可以,但延拓本身会引入假设,答辩时不好解释。

5.5 只报一个整体 RMSE,答辩追问时没有后手

现象:整体 RMSE 看起来很好,被追问“早期和末期分别差多少”时支支吾吾。

原因:容量衰减曲线早期平缓、中期稳定、末期加速,单一整体指标把三段误差平均化,掩盖了末期发散的问题;真实场景里末期估算恰恰最重要。

解决:把测试段按循环数分成三段,分别报 RMSE 和 MAPE,再加上 RUL 误差,比如预测寿命终点与真实终点相差多少圈。一个合理的呈现方式是表格,第一行整体 RMSE 是 0.02,第二行前三分之一段 0.015、中段 0.018、后段 0.034,最后加一行 RUL 绝对误差 8 圈。这组指标能立刻说明你在关注预测的什么阶段,也把答辩问题的走向引到你能解释的领域。如果后段误差确实大,主动讲清楚原因和可接受的工程边界,比藏着掖着强得多。

6. 让预测结果扛得住追问:分相建模、不确定性区间与基线对比

要把毕设从“跑通”做到“能答辩”,我会额外加三件小事。

第一,分相建模。SOH 在 0.9 以上时衰减接近线性,0.8 到 0.9 区间开始加速,很多项目用一个模型硬拟合全程,末期必然滞后。常见做法是在 SOH 等于 0.85 附近把序列切两段,分别训练两个 LSTM;第二个模型专门学加速段,测试时先判断当前 SOH 在哪一段,再选对应模型。这个方法只多写一层判断逻辑,收益却很直接。分段点不用卡得很精确,0.85 附近取整即可,答辩时你说这是根据曲线拐点位置确定的,站得住。

第二,输出不确定性区间。把损失函数从 MSE 换成分位数损失:

def quantile_loss(y_pred, y_true, q=0.1): error = y_true - y_pred return torch.mean(torch.maximum(q * error, (q - 1) * error))

训练三个模型分别预测 q 等于 0.1、0.5、0.9 的三条分位数曲线,0.1 到 0.9 之间的宽度就是置信区间。答辩时讲“SOH 预测值落在区间内的比例接近 80%”,比只报 RMSE 高一个层次。分位数模型可以在同一训练脚本里循环生成,不会增加太多代码量。

第三,跟基线比。我每次做完模型,都会先跑一个线性外推作为对照组:拿最近 40 圈的平均衰减速率,直接外推到 0.8。如果 LSTM 的预测误差不优于线性外推,问题几乎一定出在数据处理,而不是模型结构。这一条经验救过我很多次,也是我筛选毕设源码时最看重的一种保守做法。对比,也可以在论文里做成一张小表,第一列是线性外推的 RMSE,第二列是 LSTM 的 RMSE,不用多解释,数据自己会说话。

最后补一句:做完之后再复盘一遍数据划分和归一化两段代码,用“换一块电池是否还能预测”为标准自测。如果换电池就翻车,说明项目还停留在背数据阶段;如果测试电池来自不同工况还能给出合理趋势,那这个模型才算真正落地。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询