简介:一套基于RNN与LSTM的锂电池寿命预测Python项目源码,面向机器学习、数据挖掘以及电池管理相关方向的开发者与学生,可用于算法研究、课程设计或毕业设计。项目以CALCE数据集为对象,完成异常值处理、关键特征提取与归一化,分别构建RNN与LSTM模型捕捉电池充放电过程中的时间依赖关系,实现剩余寿命预测,并通过可视化直观呈现容量衰减趋势。压缩包共234个文件,其中212个xlsx为CALCE实验数据,8个ipynb为模型构建、训练与结果展示的交互式笔记本,3个py为数据加载与绘图等辅助脚本,3个md为项目说明,另有2个npy保存模型输出或中间结果,整体大小461.28MB。目前已有392人学习浏览,文件类型划分明确,便于按用途查找。下载后可获得完整源码、数据集、可视化图表及详细项目文档,既能快速上手锂电池寿命预测实践,也能系统掌握RNN/LSTM在时间序列预测中的建模方法。
1. 拿到“RNN/LSTM 锂电池寿命预测”压缩包,先确认 CALCE 数据里哪一列是“寿命”
压缩包里的东西一般不是“一个模型”,而是一整套链路:CALCE 数据文件、Python 源码、依赖说明、预测结果可视化的绘图脚本。真正决定预测效果的不是 LSTM 本身有多复杂,而是你从原始充放电数据里把“电池寿命”这个目标抽出来的方式——是预测剩余循环数,还是预测当前容量(SOH),还是从头做一个容量衰减曲线外推。我一般会先把数据读出来,确认字段,再决定用 RNN 还是 LSTM、用几层、窗口多大。适合谁看:手头有 CALCE 同类放电数据,想用 Python 做锂电池寿命预测,并希望输出一张能直接放进报告里的曲线图。
2. RNN 与 LSTM 建模原理:从 CALCE 数据到锂电池寿命预测的序列样本
2.1 普通 RNN 学不动电池容量衰减,LSTM 的门控结构解决什么
RNN 在每个时间步按 h_t = f(W·h_{t-1} + U·x_t) 更新隐藏状态,理论上能保留整段历史。但容量衰减是跨几十上百个循环的缓慢趋势,误差在长序列反向传播时连续乘以权重矩阵,梯度会指数级变小,也就是常说的梯度消失。结果就是第 5 个循环的输入对第 150 个循环的输出几乎没有影响,普通 RNN 只能记住近几轮变化,看起来像在“追”容量曲线,而不是在“预测”它。
LSTM 在 1997 年 Hochreiter 与 Schmidhuber 那篇原文里提出了门控记忆单元,核心改动是把隐藏状态拆成长期记忆 C_t 和短期输出 h_t:遗忘门决定上一时刻记忆保留多少,输入门决定当前时刻写入多少,输出门决定从记忆单元读出多少。由于 C_t 沿着时间步走的是线性更新路径,误差信号可以不经过非线性压缩直接回传,梯度消失被大幅缓解。实际复现时 PyTorch 里 RNN 换到 LSTM 只差一行代码,但同样训练轮数下,测试段预测曲线的形态往往差很多。
2.2 用 Python 读取 CALCE 数据集的三个字段:循环序号、容量、状态
拿到 CALCE 数据后不要急着建模,先用 pandas 打印列名和形状,确认数据是“每次循环一行”还是“每个时间点一行”。常见导出格式是一个电池一个 CSV 或 Excel 文件,里面包含多次充放电循环的逐点记录。最小读取步骤:
import pandas as pd # 按实际情况调整路径;一个电池一个 sheet/文件 df = pd.read_csv("CALCE/CS2_1.csv") # 若为Excel则用 pd.read_excel print(df.columns.tolist()) print(df.head()) print(df.shape)运行后你会看到类似下面的字段,不同批次的导出格式不完全一样,但核心信息是这几类:
| 常见字段 | 典型含义 | 在寿命预测里的用途 |
|---|---|---|
| Cycle_Index | 循环序号 | 时间轴,严格按它排序 |
| Discharge_Capacity | 放电容量 | 主要预测目标 |
| Charge_Capacity | 充电容量 | 辅助特征,可参与预测 |
| Temperature | 温度 | 辅助特征,需先做循环聚合 |
| Discharge_Voltage | 放电电压 | 可选特征,一般先降维再用 |
如果文件是逐时间点记录,需要先按 Cycle_Index 分组,把每个循环的放电容量最大值或末端值取出来,得到“循环序号 → 放电容量”的一维序列。寿命预测一般以放电容量为准,充电容量受充电策略影响,噪声相对大。放电容量才是电池健康状态(SOH)的直接反映。
2.3 构造 LSTM 输入需要滑动窗口,而不是整段曲线
LSTM 的输入形状是 (batch, seq_len, features),seq_len 是“看过去多少个循环来预测下一个循环”。这个滑窗思路和时间序列预测的标准做法一致,水文径流预测、股票序列也都是这么切。窗口每步向前移动一格,前 32 个点作为特征,下一个点作为标签:
import numpy as np def sliding_window(caps, seq_len=32): x, y = [], [] for i in range(len(caps) - seq_len): x.append(caps[i:i+seq_len]) y.append(caps[i+seq_len]) return np.array(x, dtype=np.float32), np.array(y, dtype=np.float32) # caps 为按循环排序的容量序列,shape (N,) X, y = sliding_window(caps, seq_len=32) print(X.shape, y.shape) # (N-seq_len, 32) (N-seq_len,)seq_len 的选择要看总循环数。CALCE 常见电池有几百个循环,取 16 到 64 之间比较合理;窗口太短学不到长期衰减趋势,太长会让样本数量快速缩水。这里预测目标是“下一个循环的容量值”,不是“还能用多少个循环”。剩余寿命通常是在得到整段预测容量后,再去和 80% SOH 阈值求交点,分两步做比让网络直接回归剩余循环数稳定得多。
3. LSTM 锂电池寿命预测模型实现:PyTorch 训练流程与超参数设置
3.1 归一化范围:只能用训练段做 fit,测试段不能参与
把容量直接丢进 LSTM 会出问题:不同电池额定容量不同,数值范围差异大,激活函数在大输入下容易饱和。常见做法是用 MinMaxScaler 缩放到 (0,1),但 fit 的范围是关键。如果对整段数据做 fit,相当于让模型在训练时就看到了测试段的容量上下界,测试曲线会异常好看,换一块新电池就原形毕露。
from sklearn.preprocessing import MinMaxScaler # 按循环顺序切分 80% / 20% split = int(len(caps) * 0.8) train_raw, test_raw = caps[:split], caps[split:] scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_raw.reshape(-1, 1)).ravel() test_scaled = scaler.transform(test_raw.reshape(-1, 1)).ravel()min 和 max 只由训练段决定,测试段只做 transform。预测完成后调用 scaler.inverse_transform 把结果还原成容量(Ah),否则后面 80% 阈值线对不上真实坐标。这个细节出错时,预测曲线形状看着对,但数值整体偏移。
3.2 基于 PyTorch 的单向 LSTM 模型类,以及和 RNN 的差异
寿命预测这个场景只能看过去,不能拿未来循环做输入,所以一般用单向 LSTM。双向 LSTM 在这里反而不合理,因为测试时未来的真实容量根本不存在。模型结构很直接:LSTM 处理序列,取最后一个时间步的隐藏状态,接一个全连接层输出容量值。开发环境用 VSCode 配好 Python 解释器就能跑,PyCharm 也可以,代码不依赖 IDE。
import torch import torch.nn as nn class BatteryLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=1, dropout=0.0): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden) return self.fc(out[:, -1, :]) # 取最后时间步的隐藏状态 # 对比用:把 nn.LSTM 换成 nn.RNN 就是普通 RNN 主干hidden_size=64 对几百个循环的小规模数据通常够用。num_layers=2 能拟合更复杂的容量曲线,但数据量小的时候很容易过拟合,测试段残差变大;如果训练损失不下降或者震荡,先降回单层。dropout 参数只在 num_layers 大于 1 时生效,单层 LSTM 设置 dropout 是无效的。全连接层输入取 out[:, -1, :],也就是最后一个时间步的隐藏状态,它聚合了整个窗口信息。
3.3 训练循环与 LSTM 与 PyTorch 的超参数对照表
训练部分用标准 PyTorch 流程:TensorDataset 包成 DataLoader,MSE 作为损失函数,Adam 优化器,加梯度裁剪。滑窗函数沿用 2.3 节的实现:
from torch.utils.data import TensorDataset, DataLoader X_train, y_train = sliding_window(train_scaled, seq_len=32) X_train = X_train.reshape(-1, 32, 1) # (样本数, seq_len, features) train_ds = TensorDataset( torch.from_numpy(X_train), torch.from_numpy(y_train), ) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) model = BatteryLSTM(input_size=1, hidden_size=64, num_layers=1) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.MSELoss() for epoch in range(200): model.train() epoch_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb).squeeze(-1) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() epoch_loss += loss.item() * len(xb) if (epoch + 1) % 20 == 0: print(f"epoch {epoch+1:03d}, loss {epoch_loss / len(X_train):.5f}")clip_grad_norm 设 1.0 是为了防止多步滚动预测时梯度累积造成爆炸。训练时 shuffle 没问题,但验证阶段必须按时间顺序推理。下面是我在这个任务上常用的超参数范围:
| 参数 | 建议范围 | 说明 |
|---|---|---|
| seq_len | 16~64 | 决定“看多少历史循环预测下一个” |
| hidden_size | 32~128 | 太小欠拟合,太大过拟合 |
| num_layers | 1~2 | CALCE 数据量级优先用 1 层 |
| dropout | 0~0.3 | 多层时才生效 |
| batch_size | 32~64 | 样本量不大,批次不用太大 |
| lr | 1e-4~1e-3 | 从 1e-3 起,震荡就降到 3e-4 |
| max_epochs | 100~300 | 配合早停使用 |
4. 预测结果可视化:用曲线和误差指标判断 LSTM 有没有学到衰减规律
4.1 滚动预测生成测试段容量曲线
模型训练完,不能把测试段整段直接丢进模型拿输出,那是训练模式下的“一步预测”。真实场景里测试段的真实容量是拿不到的,必须用滚动预测:先把测试段前 32 个真实容量点作为初始窗口,之后每步把模型预测值拼到窗口末尾,丢掉最旧一个点,再预测下一个。这样生成的整条曲线才是真正可用的外推结果。
model.eval() seq_len, feat = 32, 1 # 测试段开始的真实窗口 window = torch.from_numpy(test_scaled[:seq_len].reshape(seq_len, feat)) preds_scaled = [] with torch.no_grad(): n_steps = len(test_scaled) - seq_len for _ in range(n_steps): out = model(window.unsqueeze(0)) # (1, seq_len, 1) -> (1, 1) preds_scaled.append(out.item()) next_in = out.unsqueeze(0) # (1, 1, 1) window = torch.cat([window[1:], next_in], dim=0) pred_capacity = scaler.inverse_transform( np.array(preds_scaled, dtype=np.float32).reshape(-1, 1)).ravel() true_capacity = test_raw[seq_len:]这段代码有两个容易出错的地方。第一,window 的初始值必须是真实测试数据,不能用随机数或训练段末端;第二,window[1:] 丢掉最旧时间步,next_in 拼在末尾,顺序不能反,否则模型看到的窗口是时间逆序的。pred_capacity 与 true_capacity 长度相同,起点都对应测试段第 33 个循环。
4.2 可视化“真实 vs 预测”容量曲线与 80% 阈值线
画图是判断模型有没有真正学到衰减规律的快速方式。把训练段真实容量画成灰色,测试段真实容量画成蓝色,预测结果画成红色虚线,再加一条 80% SOH 阈值线。横坐标循环数一多刻度就会挤在一起,python 画图时用 MaxNLocator 限制刻度数量,或者把标签旋转 45 度:
import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator plt.figure(figsize=(10, 5)) plt.plot(range(split), train_raw, color="#999999", lw=1, label="Training Actual") plt.plot(range(split, split + len(test_raw)), test_raw, color="#1a73e8", lw=1.2, label="Test Actual") plt.plot(range(split + seq_len, split + seq_len + len(pred_capacity)), pred_capacity, color="#d93025", ls="--", lw=1.5, label="Predicted") rated = caps.max() plt.axhline(0.8 * rated, color="black", ls=":", lw=1, label="80% SOH") plt.gca().xaxis.set_major_locator(MaxNLocator(8)) plt.xlabel("Cycle Index") plt.ylabel("Capacity") plt.legend() plt.title("LSTM Capacity Prediction on CALCE") plt.tight_layout() plt.savefig("prediction_result.png", dpi=150)如果红色虚线在后段系统性偏离蓝色真实线,说明模型只学到了前段近似线性趋势,没有学会衰减拐点;如果训练段灰色线几乎贴住真实值、测试段发散,优先怀疑归一化泄漏或 seq_len 过短。注意测试段真实曲线从 split 开始画,预测曲线从 split+seq_len 开始画,因为前 32 个点是初始观测窗口。
4.3 计算“真实 vs 预测”的残差与三个量化指标
曲线之外,还要算残差序列,看误差是否随时间步增大。评估测试段用三个指标:RMSE 反映整体误差水平,MAE 反映平均偏差,最大误差反映最坏情况:
rmse = np.sqrt(np.mean((true_capacity - pred_capacity) ** 2)) mae = np.mean(np.abs(true_capacity - pred_capacity)) max_err = np.max(np.abs(true_capacity - pred_capacity)) residual = true_capacity - pred_capacity plt.figure(figsize=(10, 3)) plt.plot(residual, color="#1a73e8") plt.axhline(0, color="black", linestyle=":") plt.xlabel("Step in Test Window") plt.ylabel("Residual (Ah)") plt.title("Prediction Residual") plt.tight_layout() plt.savefig("residual.png", dpi=150) print(f"RMSE={rmse:.4f}, MAE={mae:.4f}, max_err={max_err:.4f}")残差围绕 0 波动但幅度大,说明衰减非线性没有学进去,优先提高 hidden_size 或加一层,而不是盲目堆 epoch;残差随时间递增说明模型外推能力弱,基本是拿训练段尾部的趋势硬推,这时候该考虑增大 seq_len 或改用曲线拟合加外推的两阶段方案。可视化不是给模型看,是给你自己做诊断用的。
5. 锂电池寿命预测实战中的三个坑,以及“留一块电池”的验证方法
5.1 三个典型坑:归一化泄漏、窗口跨电池、shuffle
第一个坑是拿全部数据做 MinMaxScaler 的 fit,测试段的容量上下界被模型偷看,测试误差虚低。第二个坑是把两块电池的容量序列首尾拼接再滑窗,窗口会跨过电池边界,制造出“前一块电池的尾部决定后一块电池头部”的假样本,跨电池验证时模型直接失效。第三个坑是验证阶段对测试样本做随机打散,时间序列必须保持原始循环顺序。如果你发现测试集 RMSE 明显小于训练集 RMSE,先怀疑泄漏,而不是模型变强。
5.2 跨电池验证:留一整块电池做测试
同电池随机切分测出来的误差只是上界表现,真正应用时面对的是一块没参与训练的新电池。正确做法是:从 CALCE 数据里留一块完整电池,完全不参与归一化、训练和超参数调整,等模型定稿后再跑一遍。多次随机选电池取平均 RMSE 和最大误差,这个数字才是可以写进报告的性能。如果跨电池误差比同电池误差大很多,说明模型记住了特定电池的衰减模式,而不是通用的衰减规律。
5.3 进阶方向:外推 SOH 曲线到 RUL 而不是直接回归循环数
预测结果可视化之后,通常把预测容量除以额定容量得到预测 SOH 曲线,再用 np.where 找 80% 阈值交点,算出剩余使用寿命。这个流程比“直接让网络输出剩余循环数”更稳定,因为后者要求网络做精确计数回归,对噪声更敏感。想再压缩误差,可以把 LSTM 和 GRU 做一次对比,很多小样本任务上 GRU 参数更少、收敛更快。多块电池验证时,预测 RUL 相比真实 RUL 的偏差,比单纯容量 RMSE 更有汇报价值。
本文还有配套的精品资源,点击获取