简介:这份资源面向计算机、自动化及新能源相关专业的学生与开发者,提供一套基于深度学习方法评估锂电池健康状态(SOH)的完整Python实现方案,可用于毕业设计、期末大作业或课程设计场景,也适合希望入门时序预测与深度学习实战的新手参考。压缩包共15个文件,约1.17MB,以py源码、csv数据集、xml配置、md说明文档为主,另含txt依赖清单与gitignore等辅助文件,结构清晰、便于直接部署运行。源码中实现了1D-CNN、BiLSTM及注意力机制等模型,并配有B0005、B005放电数据集,代码注释较为完整,读者可据此理解特征提取、模型搭建与SOH评估的完整流程。目前已有130人学习下载,适合需要快速获取可运行项目、对照实验数据与排错思路的读者参考使用。
1. 锂电池 SOH 评估为什么值得用深度学习重做一遍
如果你拆过退役动力电池包,会发现同一批电芯在实验室循环柜里跑出来的容量衰减曲线,和实车跑三年后的曲线几乎对不上。实验室恒流恒压、25℃、标准倍率,实车是随机充放电、温度忽高忽低、还经常浅充浅放。这种工况差异,正是传统 SOH 评估方法最头疼的地方——安时积分法依赖完整充放电段,开路电压法要静置到电压稳定,内阻法又受温度影响极大。而深度学习做 SOH 评估的核心价值,就是绕开这些苛刻前提,直接从电压、电流、温度、时间这些原始传感器序列里学出容量衰减的映射关系。
这篇笔记围绕「基于深度学习方法去评估锂电池健康状态(SOH)python实现源码+数据集」这个方向,把从数据准备到模型训练再到验证的完整链路拆开讲。适合两类人:一类是刚接触 python 深度学习、想找一个有物理背景的真实项目练手的入门者;另一类是做 BMS 或电池数据分析、想评估深度学习方案到底能不能落地的工程师。我会把每一步的参数含义、为什么这样选、失败时看什么指标都写清楚,代码可以直接抄。
2. 从原始充放电数据到 SOH 标签:数据集怎么构造
2.1 为什么 SOH 标签不能直接用容量除以额定容量
很多人拿到 NASA 或牛津电池数据集后,第一反应是SOH = 当前容量 / 额定容量。这个公式本身没错,但问题在于「当前容量」怎么来。实验室数据里每轮循环都有完整的放电段,可以精确积分得到容量;但真实工况下你拿不到完整放电段,所以训练时如果标签依赖完整放电容量,模型上线后就没有对应的输入可用。
常见做法是:训练阶段用完整放电段积分出容量作为标签,但输入特征只取充电段或一段固定窗口的电压-电流-温度序列。这样模型学到的是「充电过程特征 → 当前容量」的映射,上线时只需要一段充电数据就能推理。我一般会把放电段积分出来的容量作为 ground truth,输入侧只保留充电段,避免信息泄漏。
具体到 NASA 数据集,每块电池有discharge、charge、impedance三类操作。放电段里Capacity字段就是当轮实际容量,额定容量取 2Ah。SOH 标签就是Capacity / 2.0。下面这段代码把原始 mat 文件解析成按循环排列的 DataFrame。
import scipy.io as sio import numpy as np import pandas as pd def parse_nasa_mat(mat_path, rated_capacity=2.0): """解析 NASA 电池 mat 文件,返回每个循环的 SOH 标签和充电段序列""" mat = sio.loadmat(mat_path) # 找到以 B 开头的电池结构体 key battery_key = [k for k in mat.keys() if k.startswith('B')][0] cycles = mat[battery_key][0, 0]['cycles'][0] records = [] for i, cyc in enumerate(cycles): op_type = cyc['type'][0] # charge / discharge / impedance if op_type != 'discharge': continue # 放电段里的 Capacity 就是当轮实际容量 cap = float(cyc['data'][0, 0]['Capacity'][0, 0]) soh = cap / rated_capacity records.append({'cycle_idx': i, 'capacity': cap, 'soh': soh}) df = pd.DataFrame(records) df['cycle_seq'] = range(len(df)) # 重新编号,去掉 impedance 造成的空洞 return df这段代码的关键点是cycle_seq重新编号。原始 mat 里 charge、discharge、impedance 混在一起,如果直接用原始索引,循环序号会有大量跳变,后面做滑动窗口时会出错。参数rated_capacity默认 2.0,NASA 的 B0005、B0006、B0007、B0018 都是 2Ah,如果你用别的数据集要改。
2.2 充电段特征提取:取哪些字段、窗口多长
输入特征的选择直接决定模型能不能学到东西。我试过三种方案:只用电压序列、电压+电流+温度三通道、再加时间差分特征。结论是第二种性价比最高,第三种在数据量少时容易过拟合。
充电段通常分两个阶段:恒流(CC)和恒压(CV)。CC 段电压从下限升到上限,这段最能反映内阻和极化变化。我一般截取 CC 段的前 N 个采样点,N 取 200 左右。如果采样点不够 200,用线性插值补齐;超过 200,等间隔下采样。这样每条样本形状固定为(200, 3),三个通道分别是电压、电流、温度。
def extract_charge_features(cyc, seq_len=200): """从单个充电循环提取固定长度三通道序列""" data = cyc['data'][0, 0] voltage = data['Voltage_measured'][0].flatten() current = data['Current_measured'][0].flatten() temp = data['Temperature_measured'][0].flatten() # 只取恒流段:电流基本恒定的部分 cc_mask = np.abs(current - np.median(current[:20])) < 0.05 v_cc = voltage[cc_mask] i_cc = current[cc_mask] t_cc = temp[cc_mask] if len(v_cc) < 10: return None # 等间隔重采样到固定长度 idx = np.linspace(0, len(v_cc) - 1, seq_len).astype(int) seq = np.stack([v_cc[idx], i_cc[idx], t_cc[idx]], axis=-1) # 按通道做标准化,用训练集统计量,这里先占位 return seq.astype(np.float32)cc_mask的判断逻辑是:取前 20 个点的电流中位数作为恒流参考值,偏差小于 0.05A 的算 CC 段。这个阈值对 NASA 数据够用,如果你的数据噪声大,可以放宽到 0.1A。seq_len=200是我在 B0005 上试出来的,再短会丢信息,再长对 LSTM 训练速度影响明显但精度提升有限。
注意:标准化统计量必须只用训练集算,验证集和测试集用同一个均值和方差。如果每块电池单独标准化,模型会学到电池个体差异而不是衰减规律,跨电池测试时精度会崩。
3. 用 LSTM 搭 SOH 回归模型:结构、训练与调参
3.1 为什么选 LSTM 而不是 CNN 或 Transformer
锂电池充电序列是典型的时间序列,电压随时间是单调上升的,但上升速率在衰减后期会变化。CNN 擅长提取局部模式,但对长程依赖的建模能力弱;Transformer 需要的数据量比 LSTM 大一个量级,NASA 数据集只有几百个循环,用 Transformer 基本训不动。
LSTM 的门控机制刚好适合这种场景:遗忘门决定保留多少历史电压信息,输入门决定当前电压变化有多重要。我对比过 1D-CNN 和 LSTM,在 B0005 上 LSTM 的 MAE 比 CNN 低约 30%。所以这个方案里我选 LSTM,隐藏层维度 64,两层堆叠,后面接全连接回归头。
import torch import torch.nn as nn class SOHLSTM(nn.Module): def __init__(self, input_dim=3, hidden_dim=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) self.regressor = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1), nn.Sigmoid() # SOH 范围 0~1 ) def forward(self, x): # x: (batch, seq_len, input_dim) out, (h_n, c_n) = self.lstm(x) last_hidden = h_n[-1] # 取最后一层最后一个时间步 return self.regressor(last_hidden).squeeze(-1)num_layers=2加dropout=0.2是我在数据量 300 左右时的配置。如果数据少于 200 条,建议降到 1 层、dropout 0.1,否则验证 loss 会震荡。Sigmoid输出是为了把预测值压到 0~1,因为 SOH 物理上不会超过 1,也不会低于 0。如果你发现模型预测值长期贴着 0.8 附近不动,大概率是 Sigmoid 饱和了,可以换成不加激活直接回归,但要在 loss 里加范围约束。
3.2 训练循环与关键超参:学习率、batch size、早停
训练部分我踩过最大的坑是学习率。一开始用 0.01,loss 直接炸成 NaN;后来用 0.001,收敛太慢;最后定在 0.0005 配 Adam,效果最稳。batch size 用 32,因为总样本才几百条,batch 太大梯度更新次数太少。
from torch.utils.data import DataLoader, TensorDataset from sklearn.model_selection import train_test_split def train_model(X, y, epochs=200, lr=5e-4, batch_size=32, patience=20): # 按 7:1.5:1.5 划分训练/验证/测试 X_train, X_tmp, y_train, y_tmp = train_test_split(X, y, test_size=0.3, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_tmp, y_tmp, test_size=0.5, random_state=42) train_ds = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True) model = SOHLSTM() optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=10, factor=0.5) criterion = nn.MSELoss() best_val_loss = float('inf') wait = 0 for epoch in range(epochs): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() # 验证 model.eval() with torch.no_grad(): val_pred = model(torch.FloatTensor(X_val)) val_loss = criterion(val_pred, torch.FloatTensor(y_val)).item() scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_soh_model.pth') wait = 0 else: wait += 1 if wait >= patience: print(f'Early stop at epoch {epoch}') break return model, (X_test, y_test)clip_grad_norm_这行是防梯度爆炸的后悔药,LSTM 在序列较长时梯度容易累积,不加这行偶尔会出 NaN。ReduceLROnPlateau在验证 loss 不降时把学习率减半,比固定学习率更容易收敛到好的局部解。patience=20配合 200 epochs,实际训练通常 80~120 轮就早停了。
提示:如果你用 GPU 训练,记得把模型和输入都
.cuda(),但小数据集上 CPU 和 GPU 速度差别不大,CPU 反而省去数据搬运开销。
3.3 评估指标:MAE、RMSE 和「跨电池」验证
SOH 回归不能只看 MAE。我见过模型在单块电池上 MAE 0.005,换一块电池直接飙到 0.05。原因是模型记住了这块电池的衰减曲线形状,而不是学到通用规律。所以验证必须做跨电池划分:用 B0005、B0006 训练,B0007 测试,B0018 再测一次。
def evaluate(model, X_test, y_test): model.eval() with torch.no_grad(): pred = model(torch.FloatTensor(X_test)).numpy() mae = np.mean(np.abs(pred - y_test)) rmse = np.sqrt(np.mean((pred - y_test) ** 2)) # 相对误差:SOH 低于 0.8 时的误差更值得关注 mask = y_test < 0.8 mae_low = np.mean(np.abs(pred[mask] - y_test[mask])) if mask.sum() > 0 else 0 return {'MAE': mae, 'RMSE': rmse, 'MAE_low_SOH': mae_low}MAE_low_SOH这个指标是我自己加的,因为电池健康状态低于 80% 时才是真正需要预警的阶段,如果模型在高 SOH 区间准、低 SOH 区间飘,实际价值就打折。跨电池测试时,MAE 能到 0.02 以内算不错,0.01 以内算优秀。
4. 避坑与排查:SOH 模型翻车的五个真实场景
4.1 现象:训练 loss 一直降,验证 loss 从第 10 轮开始涨
原因:模型在背训练集的衰减曲线。NASA 数据集每块电池只有 100~200 个循环,参数量稍大就会过拟合。
解决:先把 LSTM 隐藏维度从 128 降到 64,层数从 3 降到 2;再加 dropout 0.2;最后加 weight_decay 1e-5。三步做完验证 loss 通常能稳住。如果还不行,说明数据量真的不够,考虑用数据增强——在充电序列上加小幅高斯噪声,模拟传感器误差。
4.2 现象:预测的 SOH 曲线整体平移,形状对但数值偏高 0.05
原因:标签的额定容量设错了。不同数据集额定容量不同,NASA 是 2Ah,牛津数据集部分电池是 0.74Ah。如果额定容量填大,SOH 标签整体偏小,模型学出来的预测也整体偏小。
解决:先确认数据集的额定容量,再检查标签分布。正常 SOH 应该从 1.0 附近开始缓慢下降,如果一开始就是 0.9 以下,大概率是额定容量不对。
4.3 现象:换一块电池测试,MAE 从 0.01 跳到 0.08
原因:标准化统计量用了测试集自己的均值方差,或者训练时把不同电池的数据混在一起但没做电池级划分。
解决:标准化统计量只用训练集算,然后应用到验证和测试集。跨电池验证时,测试电池的数据在训练阶段完全没见过。如果这样 MAE 还是很大,说明特征里包含了电池个体信息(比如绝对电压值),可以改成电压差分或相对变化率。
4.4 现象:模型对早期循环预测很准,对 SOH 低于 0.8 的后期循环误差大
原因:训练集里低 SOH 样本少。电池大部分时间都在高 SOH 区间,后期衰减快但循环数少,模型见到的低 SOH 样本不够。
解决:对低 SOH 样本做加权采样,或者在 loss 里给低 SOH 样本更高权重。我一般用weight = 1 + 2 * (1 - soh),SOH 越低权重越大。这样模型会更关注后期衰减阶段的拟合。
4.5 现象:训练时 loss 正常,推理时输出全是 0.5 左右
原因:Sigmoid 饱和,或者输入数据没有做标准化,数值范围太大导致 LSTM 内部激活饱和。
解决:先检查输入数据的均值和方差,电压通常在 3~4V,电流在 1~2A,温度在 20~40℃,如果不做标准化,LSTM 的输入门会饱和。标准化后输入应该在 0 附近、方差 1 左右。如果标准化后还是输出恒定,检查 Sigmoid 前的线性层输出是不是特别大,可以把 Sigmoid 去掉换成直接回归,但要在 loss 里加torch.clamp约束范围。
5. 让 SOH 模型真正可用的三个进阶技巧
5.1 用迁移学习解决「新电池没标签」的问题
实际场景里,新电池上线时没有历史容量标签,但有一堆无标签的充电序列。这时候可以用迁移学习:先在 NASA 数据集上预训练一个 SOH 模型,然后拿新电池的前几十个循环做微调。微调时只更新全连接层,LSTM 层冻结,学习率设小一点(1e-4)。我试过用 B0005 预训练、B0018 微调,只用 30 个带标签循环就能把 MAE 压到 0.025 以内,比从零训练快很多。
def fine_tune(pretrained_path, X_new, y_new, lr=1e-4, epochs=50): model = SOHLSTM() model.load_state_dict(torch.load(pretrained_path)) # 冻结 LSTM 层 for name, param in model.named_parameters(): if 'lstm' in name: param.requires_grad = False optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=lr) # 后续训练循环与前面一致,略 return model冻结 LSTM 层的逻辑是:底层时序特征在不同电池间是通用的,没必要重新学;全连接层负责把特征映射到 SOH 数值,这部分需要适配新电池。lr=1e-4比正常训练小 5 倍,防止微调时把预训练权重带偏。
5.2 用滑动窗口做在线 SOH 估计
离线评估是拿一整段充电数据预测一个 SOH 值,但在线场景需要每来一个新采样点就更新估计。做法是用滑动窗口:固定窗口长度 200,每次新来一个点就往前滑一步,窗口内的序列送进模型推理。这样 SOH 估计值是连续更新的,不会等到充电结束才出结果。
| 窗口策略 | 延迟 | 精度 | 适用场景 |
|---|---|---|---|
| 固定窗口 200 | 200 个采样点 | 高 | 充电过程实时监控 |
| 指数加权窗口 | 低 | 中 | 快速预警 |
| 完整充电段 | 充电结束 | 最高 | 离线健康报告 |
我一般用固定窗口,因为精度和延迟平衡得最好。如果 BMS 算力有限,可以把窗口降到 100,精度损失大约 0.005 MAE,但推理速度快一倍。
5.3 验证模型是否真的学到衰减规律:看注意力权重
LSTM 没有直接的注意力权重,但可以用梯度类方法看哪些时间步对输出贡献大。简单做法是:对输入序列的每个时间步求输出对输入的梯度,梯度大的时间步就是模型关注的重点。如果模型关注的是充电中段电压平台区,说明学到了极化内阻变化;如果关注的是充电末尾,说明学到了容量衰减导致的充电时间缩短。
我自己的习惯是每次训完模型,先画三张图:预测 SOH vs 真实 SOH 的散点、误差随循环数的变化、梯度热力图。三张图看完基本能判断模型是学到了物理规律还是只记住了数据。如果梯度热力图显示模型只关注序列最后几个点,那大概率是过拟合了,得回去加正则化。
这个方案从数据解析到模型部署,最花时间的不是写代码,而是理解电池数据的物理含义和反复验证跨电池泛化能力。我踩过的坑里,一半是数据预处理没做好,一半是验证方式太乐观。如果你准备投入这个方向,建议先把 NASA 数据集跑通,再拿自己的数据做迁移,别一上来就上复杂模型。希望帮到你。
本文还有配套的精品资源,点击获取