简介:这是一套基于Python的锂离子电池寿命预测完整项目,面向本科毕业设计、课程设计以及期末大作业等场景,重点覆盖数据清洗、特征构造、模型选择、剩余寿命回归预测和图表可视化等环节,适合具备一定编程基础的学生快速复现并做二次开发。项目已获导师指导并通过,整体代码完整、目录规整,下载后按照说明执行即可运行,无需额外修改。压缩包共含2000个文件,大小约64.68MB,其中1937张png图片用于展示预测结果与误差分析,24个npy和15个pkl保存了经过处理的数据集与训练模型,7个py是核心脚本,另有5个pth权重文件、xlsx与xls格式的原始数据表、ipynb交互式分析文档以及readme说明,方便按模块查阅。目前已有335人学习浏览,可直接用于完成毕业设计、课程报告或作为论文实验支撑,也可基于其完整流程进一步探索不同预测算法,节省从零搭建项目的时间。
1. 从一份能跑的毕业设计说起:锂电池寿命预测到底在预测什么
把这份 zip 解压后你会发现,真正让项目“能跑”的核心不是模型代码,而是三个 npy 文件:MIT.npy、HUST.npy、RWTH.npy。它们是三个公开电池数据集的特征化结果,分别来自 MIT 的早期预测实验、武汉理工的电池测试数据和德国亚琛工大(RWTH)的商用电池老化数据。锂离子电池寿命预测,业界一般不叫“预测电池还能用几天”,而是预测两个更工程化的量:SOH(健康状态,当前容量与额定容量的比值)和 RUL(剩余使用寿命,还能完成多少次完整充放电循环)。这个项目的完整链路是:解析 npy 数据矩阵 → 构造时序滑窗样本 → 训练循环神经网络 → 输出未来若干次循环的容量衰减曲线,从而外推出寿命终点。它适合两类人:一类是正在做电池管理、储能系统相关的工程师,想快速验证 LSTM 类模型在老化工况下的表现;另一类是本科毕业设计或课程设计需要一份“数据+代码+模型”闭环的完整参考实现。
2. 读懂 npy 数据集结构与防泄漏的特征工程
2.1 三个数据集的背景差异与格式解读
MIT、HUST、RWTH 这三个数据集在电池领域是高频出现的基准。MIT 数据集来自 Severson 等人 2019 年发表在 Nature Energy 上的早期预测工作,内容是 124 个商用 LFP/石墨电池在不同快充策略下的老化实验,每个电池循环到容量衰减到 80% 寿命终止。HUST 数据集是武汉理工团队公开的磷酸铁锂电池不同温度下的循环老化数据,温度范围一般在 30°C 到 50°C 之间。RWTH 数据集来自亚琛工大 ACC 项目,用的是 18650 商用电池,放电策略更贴近真实工况。
这份 zip 里没有原始 CSV 或 mat 文件,而是统一转成了 npy 二进制数组,说明作者已经完成了“原始数据 → 特征数组”的预处理。拿到手第一步不是写模型,而是先把数组的 shape 和 dtype 看清。
import numpy as np data = np.load('MIT.npy', allow_pickle=True) print(data.shape) # 观察维度 print(data.dtype) # 观察数值类型 print(data.item().keys()) # 如果是 dict 结构,查看键名allow_pickle=True是因为部分公开数据集转 npy 时会保留对象数组,内部可能存的是 dict,每个键对应一个电池编号,值是该电池的循环特征矩阵。你需要先确认是“单一大数组”还是“字典套数组”。如果是后者,常见做法是先看键的数量,再抽查某一个电池矩阵的形状,通常排列是[循环数, 特征维度],特征维度可能包含容量、电压、内阻、温度等维度。这一步虽然简单,但能避免后面把字典对象直接喂给张量时出现零维数组错误。
2.2 时序滑窗构造训练样本
寿命预测的输入输出设计直接决定模型能不能收敛。常见做法是:用前window_size次循环的容量序列(必要时拼接充电时间、温度等特征)预测未来pred_len次循环的容量,或者直接预测到寿命终止的剩余循环数。前者是序列到序列,后者是序列到标量。毕业设计场景下,我一般选“滑窗预测下一段容量序列”,因为评估时能画出预测退化曲线和真实曲线的叠加图,答辩时直观。
def create_sequences(data, window_size=50, pred_len=20): X, y = [], [] for i in range(len(data) - window_size - pred_len + 1): X.append(data[i:i + window_size]) y.append(data[i + window_size:i + window_size + pred_len]) return np.array(X), np.array(y) # 假设 cell_data 是单个电池的容量序列 [循环数] cap = cell_data[:, 0] # 取容量列 X, y = create_sequences(cap, window_size=50, pred_len=20) print(X.shape, y.shape) # (样本数, 50, 1) (样本数, 20)这里面有几个容易被忽略的点。window_size取 50 意味着用 50 次循环的历史推断未来 20 次循环,窗口越大包含的退化趋势越完整,但会吃掉早期样本。pred_len不是越大越好,预测距离越远,误差累积越快。另外,构建滑窗时不要把多个电池的数据混在一起滑,因为每个电池的初始容量有差异,混在一起等于给模型加了噪声,正确做法是每个电池单独调用一次create_sequences,最后再拼接。
2.3 归一化与数据泄漏的边界
容量序列需要归一化,但不能在全体数据上做MinMaxScaler.fit,否则验证集和测试集的信息在训练前就渗进了归一化参数,这叫归一化泄漏,会让测试指标虚高。正确做法是先切分后归一化,或者按电池编号切分。
from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split # 按电池编号切分,而不是按随机行切分 cell_ids = list(data.keys()) train_ids, test_ids = train_test_split(cell_ids, test_size=0.2, random_state=42) # 每个电池独立归一化,拟合时只用训练集的scaler scaler = MinMaxScaler() train_norm = [scaler.fit_transform(data[cid][:, 0].reshape(-1, 1)) for cid in train_ids] test_norm = [scaler.transform(data[cid][:, 0].reshape(-1, 1)) for cid in test_ids]按电池切分比按行随机切分更接近真实场景:实际部署时,你手里的模型要面对的是从未见过的新电池。如果按行随机切,同一个电池的前半段在训练集、后半段在测试集,等于模型“已经见过这个电池的前半生”,测试结果没有参考价值。这也是很多电池寿命预测论文被审稿人质疑的常见点。
3. 基于 LSTM 的剩余寿命预测模型构建与参数设定
3.1 为什么选 LSTM 而不选 TCN 或 Transformer
电池容量退化本质上是一个带噪声的长程相关时序过程,早期循环的微小差异会累积影响后期寿命。LSTM 通过输入门、遗忘门、输出门三个门控结构控制信息在时间步间的流动,能在数十步的窗口内保留早期退化信号。TCN 的膨胀卷积对长程依赖需要叠加更多层,Transformer 在序列长度只有 50 左右时优势不明显,而且小数据集上更容易过拟合。在这个项目的数据规模下(几百个电池、每个几十到上千次循环),LSTM 是收敛稳定性和实现成本之间最平衡的选择。
模型定义上,我习惯用双向 LSTM 拿上下文信息,再接一层 attention 对时间步加权,最后通过全连接层输出预测序列。双向并不增加多少计算量,但对容量曲线这种整体趋势性序列,能同时看到当前步前后的退化走向。
import torch import torch.nn as nn class BatteryLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=20, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=dropout ) self.attn = nn.MultiheadAttention( embed_dim=hidden_size * 2, num_heads=2, batch_first=True ) self.reg = nn.Sequential( nn.Linear(hidden_size * 2, hidden_size), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size, pred_len) ) def forward(self, x): # x: (batch, window_size, input_size) out, _ = self.lstm(x) # (batch, window_size, hidden*2) attn_out, _ = self.attn(out, out, out) last = attn_out[:, -1, :] # 取最后一个时间步 return self.reg(last)input_size=1表示每条序列是单变量容量,如果后续想把电压、温度、内阻一并作为输入,改成对应维度即可。hidden_size=64在这个数据量级下够用,过大容易过拟合,过小拟合不了退化趋势的非线性。num_layers=2是因为单层 LSTM 对容量曲线的长期单调下降趋势建模能力有限,两层能捕获更高层级的退化特征。batch_first=True对应输入维度(batch, seq_len, feature),这个设置和 PyTorch 默认的(seq_len, batch, feature)不同,很多人在这里踩维度错误的坑。
3.2 训练之前的数据封装与超参数基线
把上一章的滑窗样本封装成 PyTorch 的 Dataset,注意在__getitem__里做数据增强没有意义,这里要做的是把 numpy 数组转成float32张量,否则默认的float64会让训练速度慢一倍且显存翻倍。
from torch.utils.data import Dataset, DataLoader, TensorDataset class BatteryDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X, dtype=torch.float32) self.y = torch.tensor(y, dtype=torch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx]训练超参数我一般先跑一组基线:batch_size=32,lr=1e-3,epochs=50,optimizer=AdamW,loss=MSE。50 次循环的滑窗生成的数据量属于小样本范畴,batch 太大容易让梯度方向被少数异常电池主导。验证集每个 epoch 结束后计算一次 RMSE,保存验证指标最优的模型权重,而不是最后一个 epoch 的权重。
3.3 关键参数对训练结果的影响
| 参数 | 取值 | 影响方向 | 说明 |
|---|---|---|---|
window_size | 30~80 | 窗口越大,看到的退化历史越长,但样本数越少 | 数据充足时用 80,数据紧张用 30 |
pred_len | 10~30 | 预测越远误差越大 | 答辩演示建议 20,兼容可视化和误差 |
hidden_size | 32~128 | 过大过拟合,过小欠拟合 | 64 起步,看验证集曲线微调 |
num_layers | 1~3 | 层数越多非线性和表达力越强 | 2 层在多数电池数据上够用 |
dropout | 0.1~0.3 | 抑制过拟合 | dropout 在 LSTM 层间和全连接层都加 |
lr | 1e-4~1e-3 | 过大训练震荡,过小收敛慢 | 用 AdamW + 余弦退火更稳 |
4. 训练收敛策略与评估指标解读
4.1 早停与学习率调度
电池容量序列噪声不小,最后的容量衰减阶段波动尤其明显,模型容易在训练后期震荡。我推荐一套组合拳:ReduceLROnPlateau 配合 EarlyStopping。监测指标用验证集的 RMSE,连续 10 个 epoch 不下降就把学习率乘 0.5,连续 20 个 epoch 不下降就停止训练。这能避免为了“等它收敛”而盲目加大 epoch 数导致过拟合。
from torch.optim.lr_scheduler import ReduceLROnPlateau model = BatteryLSTM() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10) loss_fn = nn.MSELoss() best_rmse = float('inf') early_stop_patience, no_improve = 20, 0 for epoch in range(100): model.train() epoch_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = loss_fn(pred, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() epoch_loss += loss.item() val_rmse = evaluate(model, val_loader) scheduler.step(val_rmse) if val_rmse < best_rmse: best_rmse = val_rmse torch.save(model.state_dict(), 'best_model.pt') no_improve = 0 else: no_improve += 1 if no_improve >= early_stop_patience: breakclip_grad_norm_的参数max_norm=1.0是防止梯度爆炸的关键。LSTM 在长序列上很容易因为循环权重连乘出现梯度爆炸,裁剪后训练稳定很多。ReduceLROnPlateau的mode='min'表示监测指标越低越好,如果监测的是准确率则改为max。epoch_loss只是打印观察,真正的早停条件始终以验证集指标为准。
4.2 评估指标体系:RMSE、MAE、R²
电池寿命预测的评估不能只看一个指标,RMSE 对异常预测敏感,MAE 反映平均偏差,R² 衡量模型对容量衰减方差的解释程度。三个指标配合使用才能判断模型是“整体偏了一点”还是“个别电池预测崩了”。这里给出一个直接可用的评估函数。
import numpy as np from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate(model, loader): model.eval() y_trues, y_preds = [], [] with torch.no_grad(): for x_batch, y_batch in loader: pred = model(x_batch) y_trues.append(y_batch.numpy()) y_preds.append(pred.numpy()) y_true = np.concatenate(y_trues, axis=0) y_pred = np.concatenate(y_preds, axis=0) rmse = np.sqrt(mean_squared_error(y_true.flatten(), y_pred.flatten())) mae = mean_absolute_error(y_true.flatten(), y_pred.flatten()) r2 = r2_score(y_true.flatten(), y_pred.flatten()) # 按预测序列长度逐点分析,观察远期的误差膨胀 per_step_rmse = [ np.sqrt(mean_squared_error(y_true[:, i], y_pred[:, i])) for i in range(y_true.shape[1]) ] return {'rmse': rmse, 'mae': mae, 'r2': r2, 'per_step_rmse': per_step_rmse}per_step_rmse是容易被忽略但很有诊断价值的一个输出:它反映的是预测第 k 个未来时间点时的误差。如果前 5 步 RMSE 很小、第 15 步开始陡然上升,说明模型对短期退化学习得好,但对长期趋势的外推能力不足,这时候需要调整的不是网络结构,而是滑窗窗口长度或损失函数。
误差可视化上,我通常画两张图:一张是某个代表性电池的真实容量曲线与预测曲线的对比,另一张是全部测试电池的误差分布直方图。第一张图能看到“预测是否跟得上转折点”,第二张图能看到“是否有某个电池是离群点”。如果误差直方图呈双峰分布,说明测试集里可能混入了不同工况的电池,需要考虑按数据集单独评估。
4.3 常见训练陷阱与排查思路
容量序列末段波动大,你会发现 loss 下降到一定程度后不再下降,这是正常的,因为电池寿命终止前的容量骤降是强随机过程,任何模型都无法精确预测。此时应该检查的是:模型是否在验证集上出现了“预测曲线平滑、但没有抓住早期拐点”的情况。如果是,优先尝试把输入特征从纯容量扩展到容量+充电时间;如果原始 npy 里只有容量列,可以在特征工程阶段用差分法构造容量衰减速率特征,即np.diff(cap),把它拼接到滑窗样本里。
另一个高频坑是标准化时把测试集电池的容量序列整个 fit 了一遍,这在离线评估里会高估实际性能。正确的部署思维是:模型在训练阶段只见过训练集和验证集的电池,测试集的电池从头到尾都是“陌生人”。如果你发现测试集误差远大于验证集误差,先检查是不是归一化泄漏,再检查是不是随机种子导致的数据划分不均匀。
5. 跨数据集迁移的微调技巧与模型落盘
MIT、HUST、RWTH 三个数据集虽然都是锂电池老化数据,但电池化学体系、测试温度、充放电策略各不相同。直接用 MIT 训练的模型去预测 RWTH 的电池,RMSE 通常会明显变大。这里介绍一个实用技巧:域自适应微调。具体做法是拿预训练好的模型,冻结 LSTM 层参数,只用目标数据集的一小部分样本重新训练回归头;等回归头收敛后,再把 LSTM 层解冻,用很小的学习率全量微调。
# 冻结 LSTM 层,训练回归头 for name, param in model.named_parameters(): if 'lstm' in name: param.requires_grad = False optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3 ) # 训练 10 个 epoch 后,解冻 LSTM for name, param in model.named_parameters(): param.requires_grad = True optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)微调的核心参数是lr。冻结阶段用1e-3让回归头快速适应目标域的容量范围,解冻后降到1e-4,避免大步长把预训练学到的退化规律冲掉。迁移后对比两组数字:微调前的跨域 RMSE 和微调后的跨域 RMSE,通常会有 15% 到 40% 的下降。模型部署时用torch.jit.script把模型和预处理管线打包成一个文件,推理时不需要重新加载 torchvision 或 sklearn 的每一个组件。
scripted_model = torch.jit.script(model) scripted_model.save('battery_lstm_mit.pt')这块模型文件可以直接嵌入到 BMS 边缘设备或云端推理服务里。毕业设计答辩时,把跨数据集误差对比表一页展示,再配合三行脚本说明“从数据到模型到部署”的完整链路,这个项目的完成度和工程深度就都体现出来了。最后留一个小改进方向:把pred_len从固定值改成动态衰减——当模型预测输出容量低于 80% 额定容量时提前终止,这样输出的不再是一段固定序列,而是一个具体的寿命循环数。
本文还有配套的精品资源,点击获取