资讯动态

Transformer-LSTM混合模型:锂电池剩余寿命预测实战

发布时间:2026/9/18 7:20:40 来源:尧图企业网站定制
简介面向新能源与电池管理领域的数据科学从业者及研究生这份Python项目实例基于Transformer-LSTM混合模型实现锂电池剩余寿命RUL预测覆盖数据预处理、滑动窗口采样、特征工程、模型训练优化全流程并集成GUI交互界面支持数据加载、模型推理与预测结果动态展示。文档通过MSE、MAE、R²、RMSE、MAPE等指标及残差分析评估精度提供注意力权重与LSTM隐状态可视化增强模型可解释性。代码详解从Transformer自注意力机制与LSTM时序建模的融合入手兼顾工程化部署可直接复现或扩展至多模态输入与轻量化场景。资源为单份docx文档约72KB内容紧凑完整。目前已有262人学习适合具备PyTorch基础、希望深入时序预测与GUI落地的中高级读者。1. 从容量衰减到 Transformer-LSTMRUL 预测为什么需要混合模型锂电池剩余寿命RUL预测的难点不是拟合一条平均衰减曲线而是捕捉同型号电池在不同工况下的分散性。同样是 20% 容量衰减有的电池还能跑 300 次循环有的只剩不到 100 次。LSTM 擅长记住最近的退化趋势但面对长序列中的全局模式容易遗忘Transformer 的自注意力恰好擅长跨时间步的长程特征提取。把两者串成混合结构先由 Transformer 建模全局依赖再由 LSTM 提取局部时序特征最后融合回归是当前比较稳定的方案。这篇文章记录一个用 Python PyTorch 实现的 Transformer-LSTM 锂电池 RUL 预测项目覆盖数据构造、模型构建、训练评估与 GUI 热插拔推理可直接作为 BMS 电池健康管理或课程设计的复现底稿。2. 数据准备与特征工程滑动窗口、归一化与 DataLoaderRUL 预测本质是时序回归。输入是电池前若干次循环的观测序列输出是剩余可用循环数。如果拿整条退化曲线训练模型学到的只是“这条曲线对应某个寿命”换一条曲线就失效滑动窗口采样把长序列切成固定长度的子序列让模型在每个时间步都能看到最近 N 次循环的特征。这是项目能在小样本下保持泛化能力的第一步。2.1 生成合成电池退化数据真实电池数据集如 NASA 或 CALCE 公开数据量小、工况不完全一致做课程设计和算法验证时通常先用合成数据跑通流程。项目里生成 40 条电池容量衰减曲线每条曲线从 1.0 开始按指数 线性趋势下降叠加高斯噪声和随机波动模拟不同电池的初始差异。import numpy as np import pandas as pd def generate_battery_curves(n_batteries40, cycles600, noise_std0.006): data [] for i in range(n_batteries): # 不同电池的衰减速率在 0.00035~0.00055 之间浮动 rate np.random.uniform(0.00035, 0.00055) start_cap np.random.uniform(0.98, 1.02) # 初始容量轻微偏移 idx np.arange(cycles) # 指数线性组合模拟早期慢、后期快的退化过程 capacity start_cap - rate * idx - 0.08 * np.exp(idx / 300) capacity np.random.normal(0, noise_std, cycles) # 温度、电流、电压模拟统一放另一组特征列这里先只保留容量 df pd.DataFrame({ battery_id: i, cycle: idx, capacity: capacity }) data.append(df) return pd.concat(data, ignore_indexTrue) df generate_battery_curves() print(df.shape)说明n_batteries40控制样本数量cycles600是每条曲线的最大循环数noise_std是观测噪声。这里用“指数 线性”组合是为了模拟锂电池先缓慢衰减、后期加速老化的非线性趋势。实际使用公开数据集时把df替换成读取文件即可后续特征工程代码不用改动。输入特征工程中除了容量还常加入循环次数、恒流充电时间、温度变化率等。项目示例为了突出模型结构先用容量序列做单特征输入但代码里保留了扩展多特征的接口。最终每条样本是一个形状为(batch, window_size, n_features)的三维张量。2.2 滑动窗口采样与归一化滑动窗口有两个关键参数window_size和horizon。window_size是输入序列长度horizon是预测目标距离当前时刻的步数。常见做法是window_size30, horizon1即用最近 30 次循环预测下一次循环后的剩余寿命也可以把horizon设为 5输出未来第 5 次循环时对应的 RUL。窗口长度的选择要结合电池循环数据长度太小会丢失退化趋势太大会让 Transformer 注意力矩阵变稀疏、训练变慢。归一化使用 MinMaxScaler。注意必须只对特征列拟合cycle和battery_id不做归一化预测目标 RUL 单独定义不参与特征归一化。下面的代码先构造滑动窗口样本再切分训练集最后在训练集上拟合 scaler。def create_sequences(data, window_size30, horizon1): X, y [], [] values data[capacity].values for i in range(len(values) - window_size - horizon 1): # 输入连续 window_size 个容量观测值 X.append(values[i:iwindow_size]) # 目标当前窗口结束位置到寿命结束的剩余循环数 rul len(values) - (i window_size horizon - 1) y.append(rul) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) X, y create_sequences(df[df.battery_id 0], window_size30, horizon1) print(X.shape, y[:3])逻辑说明循环用i遍历起始位置X[i]取[i, iwindow_size)的容量值y[i]是窗口末端到曲线末尾的剩余循环次数。这里RUL定义为“还能循环多少次”和容量阈值的映射关系由外部设置。如果按 80% 容量作为报废线需要先截断到失效点再生成序列否则模型会学到“寿命固定在 600”的假规律。多电池组合时按battery_id分组后分别调用该函数再合并。归一化代码from sklearn.preprocessing import MinMaxScaler def normalize_features(X_train, X_val, X_test): # shape: (n_samples, window_size) - (n_samples, window_size, 1) scaler MinMaxScaler() n_samples_train, window X_train.shape X_train_2d X_train.reshape(-1, window) scaler.fit(X_train_2d) X_train_norm scaler.transform(X_train_2d).reshape(n_samples_train, window, 1) X_val_norm scaler.transform(X_val.reshape(-1, window)).reshape(-1, window, 1) X_test_norm scaler.transform(X_test.reshape(-1, window)).reshape(-1, window, 1) return X_train_norm, X_val_norm, X_test_norm, scaler参数说明reshape(-1, window)是把三维样本先压成二维再喂给 scaler目的是让每个时间步的容量分布统一映射到 0~1reshape(..., 1)是为了给 PyTorch 增加特征维度。验证集和测试集只用训练集拟合好的 scaler 变换不能在验证集上重新 fit否则会引入未来信息。参数建议值作用调参方向window_size30输入序列长度决定模型看多长的历史数据长度短时取 10~20数据密集取 30~50horizon1预测目标与当前时刻的步数差做长期预测时增大到 5~10但误差会明显上升训练/验证/测试比例60%/20%/20%按电池曲线划分不能按样本随机划分数据量大时可增加验证集比例noise_std0.006观测噪声幅度模拟传感器误差数据噪声大时可以到 0.01按电池曲线划分而非按样本划分是因为时序样本本身存在时间重叠。随机划分会让同一条曲线的相邻窗口同时出现在训练集和验证集评估出的指标会明显高于真实部署表现。2.3 划分训练集、验证集、测试集与 DataLoader时序数据的划分有一个最容易踩的坑直接train_test_split(X, y, random_state42)会随机打乱样本导致验证集里出现与训练集时间重叠的样本评估结果偏高。正确做法是把电池按 ID 分组先切分电池再在每组内生成滑动窗口样本。battery_id为 0-29 的进训练集30-35 进验证集36-39 进测试集。import torch from torch.utils.data import TensorDataset, DataLoader def build_dataloader(battery_ids, window_size30, horizon1, batch_size64, shuffleTrue): X_all, y_all [], [] for bid in battery_ids: sub df[df.battery_id bid] X, y create_sequences(sub, window_size, horizon) X_all.append(X) y_all.append(y) X_cat np.concatenate(X_all, axis0) y_cat np.concatenate(y_all, axis0) # 演示简化这里用 X_cat 统一归一化实际应先用训练集 fit 再 transform X_norm, _, _, _ normalize_features(X_cat, X_cat, X_cat) dataset TensorDataset(torch.tensor(X_norm), torch.tensor(y_cat).float()) return DataLoader(dataset, batch_sizebatch_size, shuffleshuffle) train_loader build_dataloader(range(0, 30)) val_loader build_dataloader(range(30, 36), shuffleFalse) test_loader build_dataloader(range(36, 40), shuffleFalse)这段演示代码为了简洁在build_dataloader里重新拟合了 scaler实际项目里应该把训练集 scaler 作为参数传入否则验证集和测试集的归一化基准不一致。shuffleFalse用于验证集和测试集保留时间顺序方便后面对应样本序号画误差曲线。到这里数据部分已经准备好了符合 PyTorch 输入规范的三维张量下一步开始构造 Transformer-LSTM 混合模型。3. Transformer-LSTM 混合模型构建自注意力、时序记忆与特征融合模型结构分三层先用 Transformer 编码器提取全局依赖再用 LSTM 对局部退化趋势做时序建模最后把两条路径的特征拼接并通过全连接层回归出 RUL。选择这种结构的原因是Transformer 的 self-attention 能直接看到窗口内任意两个时刻的关系比如早期一次容量骤降与后期加速衰减之间可能强相关LSTM 则保持了对连续波动的敏感性。混合结构会带来额外的参数量但在本项目的 600 循环样本量级下不会过拟合前提是做好 dropout 和早停。3.1 Transformer 编码器模块PyTorch 自带nn.TransformerEncoderLayer不需要从零实现多头注意力。这里用d_model64nhead4dim_feedforward128。batch_firstTrue让输入形状直接是(batch, seq_len, features)避免频繁 permute。import torch.nn as nn class TransformerEncoderBlock(nn.Module): def __init__(self, d_model64, nhead4, dim_feedforward128, dropout0.1): super().__init__() self.encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwarddim_feedforward, dropoutdropout, batch_firstTrue, activationrelu ) self.norm nn.LayerNorm(d_model) def forward(self, x): # x: (batch, seq_len, d_model) x self.encoder_layer(x) return self.norm(x)参数说明d_model64表示每个时间步被映射成 64 维向量太小会丢失特征表达太大在电池数据这种小样本场景容易过拟合nhead4表示多头注意力切成 4 个子空间注意力输出维度仍然是 64。这里在 Transformer 输出后又接了一个LayerNorm因为编码器内部可能已经有 norm但多一次 norm 能稳定后期与 LSTM 特征拼接后的梯度。3.2 LSTM 时序编码模块LSTM 部分只取最后一个时间步的隐状态还是取所有时间步的平均池化常见做法是双向 LSTM 取最后时刻的隐状态拼接但项目里更推荐取最后一个前向隐状态和最后一个反向隐状态拼接因为电池退化方向性明确反向信息对“当前状态距离寿命终点还有多远”有直接提示。下面代码用nn.LSTM的batch_firstTrue输入是 Transformer 输出的同维度特征。class LSTMTemporalEncoder(nn.Module): def __init__(self, input_size64, hidden_size128, num_layers1, dropout0.2): super().__init__() # 双向 LSTM输出维度为 hidden_size * 2 self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0.0 ) def forward(self, x): out, (h_n, _) self.lstm(x) # h_n shape: (num_layers * num_directions, batch, hidden_size) last_forward h_n[-2] # 前向最后层 last_backward h_n[-1] # 反向最后层 combined torch.cat([last_forward, last_backward], dim-1) return combinedh_n[-2]和h_n[-1]的取法在双向 LSTM 里固定最后一层前向隐状态在前反向在后。hidden_size128定义单方向维度双向拼接后变成 256 维。这个维度会直接决定后面的全连接层输入大小。这里num_layers1避免多层 LSTM 在短序列上出现梯度退化。3.3 特征融合与输出层把 Transformer 的输出序列再做一次全局平均池化和 LSTM 得到的 256 维向量拼接形成 64256320 维特征。平均池化的动机是自注意力已经编码了位置关系取平均可以保留全局模式而 LSTM 部分提供方向性状态。融合层采用三层全连接320 - 128 - 32 - 1激活函数用 ReLU最后一层不接激活函数。class TransformerLSTM(nn.Module): def __init__(self, n_features1, d_model64, nhead4, hidden_size128, dropout0.2): super().__init__() self.input_proj nn.Linear(n_features, d_model) self.transformer TransformerEncoderBlock(d_modeld_model, nheadnhead) self.lstm LSTMTemporalEncoder(input_sized_model, hidden_sizehidden_size, dropoutdropout) fusion_dim d_model hidden_size * 2 self.fc nn.Sequential( nn.Linear(fusion_dim, 128), nn.ReLU(), nn.Dropout(dropout), nn.Linear(128, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x: (batch, seq_len, n_features) x self.input_proj(x) # 映射到 d_model 维 x self.transformer(x) # 全局特征 avg_pool x.mean(dim1) # (batch, d_model) lstm_feat self.lstm(x) # (batch, hidden_size*2) feat torch.cat([avg_pool, lstm_feat], dim-1) out self.fc(feat) return out.squeeze(-1)逻辑说明input_proj把原始单特征从 1 维扩展到 64 维这样 Transformer 的 positional encoding 才有足够的维数做位置区分。x.mean(dim1)对序列长度维求平均与 LSTM 输出拼接。在真实项目中如果同时输入电压、电流、温度等多个特征只需修改n_features和input_proj的输入维数后续结构不需要改动。模块输入维度输出维度作用input_proj(batch, seq_len, 1)(batch, seq_len, 64)升维并构造特征表示TransformerEncoderBlock(batch, seq_len, 64)(batch, seq_len, 64)提取全局长程依赖LSTM Temporal Encoder(batch, seq_len, 64)(batch, 256)提取局部退化模式全局平均池化(batch, seq_len, 64)(batch, 64)压缩时序信息全连接回归层(batch, 320)(batch, 1)输出 RUL 预测值3.4 位置编码与前向形状验证Transformer 本身不像 LSTM 那样天然带顺序信息必须给每个时间步添加位置编码。常见做法是使用可学习的位置嵌入维度与d_model一致。下面定义PositionalEncoding模块并在模型前向中叠加。class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len200): super().__init__() self.pos_embedding nn.Parameter(torch.randn(1, max_len, d_model) * 0.02) def forward(self, x): return x self.pos_embedding[:, :x.size(1), :]完整使用时在TransformerLSTM.forward里把x self.transformer(x)之前插入一行x self.pos_enc(x)即可。位置编码初始化为随机值训练后会学到窗口内不同时序位置的相对关系。前向验证代码model TransformerLSTM(n_features1) x torch.randn(8, 30, 1) y model(x) print(y.shape) # (8,)输出(8,)表示 8 个样本各自获得一个标量 RUL 预测值。如果输出维度是(8, 1)说明squeeze(-1)没有被正确调用不影响评估逻辑但后续写 GUI 时需要自行处理维度。4. 训练与评估损失函数、早停、多指标对比与可视化Transformer-LSTM 混合模型的训练策略和纯 LSTM 有两点不同一是学习率要更低通常在 1e-4 以下因为 Transformer 对学习率敏感二是 Batch 内样本的 RUL 分布差异很大训练前期丢给模型一批早期电池样本和一批接近报废的样本会让 loss 震荡。项目里采用 AdamW 余弦退火学习率并在验证集 loss 连续 20 个 epoch 不下降时提前停止。4.1 损失函数与优化器选择回归任务首选 MSE因为它对大偏差惩罚更重能让模型更关注寿命中后段误差大的样本。也可以使用 Huber Loss在残差大于阈值时退化为 MAE对离群点更稳健。下面的训练配置是项目里效果较好的组合lr1e-4weight_decay1e-5batch_size64max_epochs200。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model TransformerLSTM(n_features1) criterion nn.MSELoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler CosineAnnealingLR(optimizer, T_max200, eta_min1e-6)参数说明weight_decay是 L2 正则防止全连接层过拟合T_max200是余弦周期长度这里等于最大 epoch 数让学习率从 1e-4 逐渐降到 1e-6eta_min是最低学习率。如果训练集很小可以把weight_decay提高到 1e-4。4.2 训练主循环与早停机制早停的指标是验证集 MSE。每个 epoch 结束后计算验证 loss如果比历史最优值低就保存模型参数否则连续 20 次不更新就停止训练。注意保存的是model.state_dict()不是整个模型对象方便之后在 GUI 里热插拔加载。best_loss float(inf) patience 20 trigger_times 0 for epoch in range(200): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() train_loss loss.item() * xb.size(0) scheduler.step() model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss criterion(pred, yb).item() * xb.size(0) avg_val_loss val_loss / len(val_loader.dataset) if avg_val_loss best_loss: best_loss avg_val_loss trigger_times 0 torch.save(model.state_dict(), best_model.pth) else: trigger_times 1 if trigger_times patience: print(fEarly stop at epoch {epoch1}) break逻辑说明xb进入模型前不需要额外归一化因为数据加载环节已经完成xb.size(0)是当前 batch 的真实样本数避免最后一个 batch 不足导致 loss 平均偏差。早停保存的best_model.pth会直接作为后续 GUI 默认权重。4.3 评估指标MSE、MAE、R²、RMSE、MAPE 与残差形态项目使用六个指标分别从不同角度衡量预测质量MSE 强调大误差RMSE 与原始单位一致MAE 反映平均绝对偏离R² 反映模型相对均值基线的好坏MAPE 给出百分比误差偏度和峰度则用来检验残差是否接近正态分布。残差偏度接近 0 表示预测没有系统性高估或低估峰度接近 3 说明大部分误差集中在中心区域、没有多根异常尾巴。from scipy.stats import skew, kurtosis def evaluate_metrics(y_true, y_pred): y_true np.asarray(y_true) y_pred np.asarray(y_pred) mse np.mean((y_true - y_pred) ** 2) mae np.mean(np.abs(y_true - y_pred)) rmse np.sqrt(mse) r2 1 - np.sum((y_true - y_pred)**2) / np.sum((y_true - np.mean(y_true))**2) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 residual y_true - y_pred skewness skew(residual) kurt kurtosis(residual) return { MSE: mse, MAE: mae, R2: r2, RMSE: rmse, MAPE: mape, Skewness: skewness, Kurtosis: kurt } metrics evaluate_metrics(y_test, y_pred) for k, v in metrics.items(): print(f{k}: {v:.4f})指标公式/含义数值偏好实际提示MSE均方误差越小越好对大误差敏感MAE平均绝对误差越小越好反映平均偏离程度R²决定系数越接近 1 越好表示模型解释的方差比例RMSE均方根误差越小越好单位与 RUL 相同MAPE绝对百分比误差越小越好寿命短时容易变大Skewness残差偏度接近 0正偏说明低估较多Kurtosis残差峰度接近 3大于 3 说明长尾异常4.4 可视化注意力分布、残差与三维曲线训练完成后除了指标还要确认模型“看”到了什么。将TransformerEncoderLayer的self_attn输出配合 forward hook 可以拿到注意力矩阵画出每个时间步对最后预测的贡献。LSTM 隐状态也可以沿着时间维做 L2 范数可视化观察模型在哪些循环附近发生状态切换。下面是用测试集画真实值与预测值对比和残差直方图的代码。import matplotlib.pyplot as plt # 真实值与预测值对比 plt.figure(figsize(8, 4)) plt.plot(y_test, labelTrue RUL, linewidth1.5) plt.plot(y_pred, labelPred RUL, linewidth1.5) plt.xlabel(Sample Index) plt.ylabel(Remaining Useful Life) plt.legend() plt.show() # 残差分布直方图 residual y_test - y_pred plt.figure(figsize(6, 4)) plt.hist(residual, bins30, edgecolorwhite, alpha0.7) plt.xlabel(Residual) plt.ylabel(Frequency) plt.show() # 真实与预测散点图相关性分析 plt.figure(figsize(6, 6)) plt.scatter(y_test, y_pred, alpha0.6) plt.plot([0, max(y_test)], [0, max(y_test)], r--) plt.xlabel(True RUL) plt.ylabel(Predicted RUL) plt.show()说明散点图里点越贴近对角线说明预测越准红色虚线是 yx 参考线。真实值与预测值曲线上的尖峰位置往往对应电池容量后期加速衰减的阶段也是模型最容易低估 RUL 的位置。三维曲线可以用mpl_toolkits.mplot3d绘制把真实 RUL、预测 RUL 和时间步放在三个轴上观察误差是否随循环推进而放大。注意力矩阵本身可以通过nn.MultiheadAttention的attn_weights输出获得在 GUI 里只要增加一个下拉框选择样本序号就能把矩阵画成热力图代码量不大但能显著提升结果的可解释性。5. GUI 集成与热插拔推理把模型封装成交付工具模型训练好后最终要交付给测试或产线人员使用。项目中的 GUI 采用 PySide6 实现主窗口包含文件加载区、数据预览区、预测按钮和结果显示区。核心设计是“模型热插拔”界面不负责训练只负责加载best_model.pth权重文件读取新的电池数据后用训练时保存的 scaler 做标准化再交给 Transformer-LSTM 推理。这样更换模型权重或评估不同电池数据时不需要重新启动程序。5.1 热插拔模型加载与自动标准化加载模型时有两个易错点一是必须用和训练时相同的模型结构初始化直接torch.load(best_model.pth)只能拿到权重字典二是 scaler 必须一并保存否则新的数据无法映射到同一尺度。项目里把 scaler 也存成.pkl文件GUI 启动时同时加载模型和 scaler。from PySide6.QtWidgets import QMainWindow, QFileDialog, QMessageBox from PySide6.QtCore import Qt class RULPredictorGUI(QMainWindow): def __init__(self, model_path, scaler_path): super().__init__() self.model TransformerLSTM(n_features1) self.model.load_state_dict(torch.load(model_path, map_locationcpu)) self.model.eval() import joblib self.scaler joblib.load(scaler_path) def predict_capacity_sequence(self, values): # 输入为长度超过 window_size 的容量数组 last_window values[-window_size:] last_window self.scaler.transform(last_window.reshape(1, -1)).reshape(1, window_size, 1) with torch.no_grad(): pred self.model(torch.tensor(last_window, dtypetorch.float32)) return pred.item()参数说明map_locationcpu避免模型训练在 GPU、部署在无 GPU 环境时出现设备不匹配scaler_path指向训练阶段保存的 MinMaxScaler。代码里的values可以直接来自 GUI 导入的 CSVlast_window取最后 30 个容量值保证推理使用的上下文与训练时一致。5.2 画布刷新与结果动态展示为了不让用户点一次预测就卡界面预测逻辑放在线程池中运行主线程只负责把返回的样本序号和预测值追加到matplotlib画布。GUI 中每一行数据显示“当前循环数、真实 RUL、预测 RUL、绝对误差”超过阈值会标红配合残差曲线一起刷新。这样即使是 40 节电池的批量评估界面也不会失去响应。如果后续接入实时传感器流只需要把predict_capacity_sequence的输入替换成滑动缓存再在数据源回调里调用update_plot这套 GUI 的推理链路不需要改动。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价