简介:面向电池管理系统学习与研究者的一套锂电池SOC估计工程资料,聚焦Transformer与容积卡尔曼滤波(CKF)融合架构,解决多工况、多温度下荷电状态测算精度与稳定性不足的问题。资源包共59个文件,约12.52MB,以23个Python脚本(模型、训练、评估与绘图)、5个mat数据文件、16张png结果图及若干备份与说明文档为主,覆盖BJDST、DST、FUDS、US06等标准驾驶循环与0°C、25°C、45°C温度场景。已有199人学习下载。读者可获取完整的特征工程、模型训练与CKF递归融合代码,直接复现多工况多温度对比实验,并借助误差曲线、热力图、PCA方差图等可视化结果理解各模块作用;配套README与配置脚本便于快速搭建环境、按目录结构定位数据与模型文件,适合具备一定Python与深度学习基础、希望深入电池状态估计方向的学习者参考。
1. 从一条 SOC 曲线说起:Transformer 与 CKF 融合到底在解决什么
锂电池 SOC 估计这件事,实验室里跑一条 DST 工况曲线看着挺漂亮,一上车、一进低温舱就原形毕露。我最早做 BMS 算法时踩过的坑是:安时积分在 25°C 恒流下误差能压在 3% 以内,可一旦切到 0°C 的 FUDS 工况,端电压被极化压得乱七八糟,误差直接冲到 10% 以上。问题不在积分公式,而在于模型对动态极化、温度漂移和非线性迟滞的刻画太弱。
这个标题讲的就是把两件事拼起来:用 Transformer 学电池端电压、电流、温度到 SOC 之间的长时序非线性映射,再用 CKF(容积卡尔曼滤波)把这个数据驱动结果和等效电路模型的物理约束做融合,最终在 BJDST、DST、FUDS、US06 四类工况和 0/25/45°C 三个温度点上都能把 RMSE 压到 2% 以内。它适合两类人:一类是做 BMS 算法、想把 SOC 精度从「能用」推到「敢标定」的工程师;另一类是手上有电池测试数据、想找一个能复现的时序建模 + 滤波融合框架的研究者。核心词 Transformer、CKF、锂电池 SOC、多工况、多温度,后面每一章都会落到具体参数和代码上。
2. 为什么是 Transformer 加 CKF:选型理由与数据准备
2.1 纯数据驱动和纯模型的各自死穴
先说清楚为什么不能只用其中一个。纯 Transformer 做 SOC 回归,本质是学一个从历史窗口到当前 SOC 的映射函数。它在训练集覆盖的工况上表现很好,但电池是个时变系统,老化、温度、甚至同一批电芯的一致性差异都会让映射偏移。我见过最典型的翻车是:模型在 DST 上 RMSE 0.8%,换到 US06 这种高频动态工况直接飙到 4%,因为 US06 的电流变化率远超训练分布。
纯 CKF 依赖等效电路模型,比如二阶 RC 模型。它的优势是有物理约束、可解释、外推稳,但死穴是模型参数(R0、R1、C1、R2、C2)随 SOC、温度、老化变化,离线辨识的参数在线上一旦失配,滤波就会发散。低温下 R0 能比常温大两三倍,你拿 25°C 的参数去跑 0°C,CKF 的观测更新会把状态越拉越偏。
融合的思路是:Transformer 提供观测层面的强非线性修正,CKF 提供状态层面的时序平滑和物理约束。具体做法常见有两种,一种是把 Transformer 输出当作 CKF 的虚拟观测,另一种是把 Transformer 学到的极化电压作为模型参数在线修正项。我一般用第一种,工程上更好落地。
2.2 数据准备:四工况三温度的输入张量怎么搭
数据是整个方案的地基。BJDST、DST、FUDS、US06 这四类工况覆盖了从城市低速到高速急加速的动态范围,0/25/45°C 覆盖了低温、常温、高温三个典型温度区间。每个工况每个温度下,你需要采集的原始量至少包括:时间、电流(带符号,放电为正或负要统一)、端电压、环境温度或表面温度、以及作为标签的参考 SOC(一般用高精度充放电设备做安时积分标定)。
构造输入张量时,我一般用滑动窗口。窗口长度取 100 到 200 个采样点,采样周期 1s 或 0.1s 视工况而定。特征维度包括电流、电压、温度,以及它们的差分特征。下面这段是数据切片的核心逻辑:
import numpy as np def build_windows(current, voltage, temp, soc, win=150, stride=1): """ current/voltage/temp/soc: 一维数组,长度 N win: 滑动窗口长度,对应 150 秒历史 stride: 步长,训练时可取 1 做增强 返回 X: (M, win, 3) Y: (M, 1) """ feats = np.stack([current, voltage, temp], axis=1) # (N,3) X, Y = [], [] for i in range(0, len(soc) - win, stride): X.append(feats[i:i+win]) Y.append(soc[i+win]) # 用窗口末端时刻的 SOC 作标签 return np.array(X, dtype=np.float32), np.array(Y, dtype=np.float32)逻辑说明:窗口末端对齐标签是为了避免信息泄漏,训练时不能用未来时刻的 SOC 去预测当前。参数上,win=150是我在 1s 采样下比较稳的值,太短抓不到极化动态,太长会让 Transformer 注意力分散、训练变慢。stride训练时取 1 做数据增强,验证和测试时必须取win,保证窗口不重叠,否则评估结果会虚高。
归一化必须按训练集统计量来做,电流和电压量纲差一个数量级,不归一化 Transformer 收敛会很慢。温度我一般单独做 one-hot 或 embedding,因为 0/25/45 是离散的三个点,直接当连续值喂进去反而引入虚假的序关系。
提示:参考 SOC 的标定精度直接决定模型上限。如果标签本身有 2% 误差,你后面再怎么调网络也压不下去。
3. Transformer 回归网络:结构、训练与三个必调参数
3.1 编码器结构怎么定:层数、头数、维度的取舍
标题里带 Transformer,但做 SOC 回归不需要上那种几十层的大家伙。我实测下来,编码器层数 2 到 4 层、注意力头数 4 到 8、隐藏维度 64 到 128 就足够。层数再多,在几万到几十万样本量级上很容易过拟合,而且推理延迟对 BMS 嵌入式部署不友好。
位置编码这块,SOC 序列是强时序的,正弦位置编码和可学习位置编码我都试过,差别不大,但可学习位置编码在窗口长度固定时略稳。注意力机制用标准的多头自注意力即可,不需要搞什么稀疏注意力,窗口才 150 个点,计算量完全扛得住。
输出头我一般接一个两层 MLP,最后接 sigmoid 把输出压到 [0,1] 区间,对应 SOC 的 0 到 100%。这里有个细节:SOC 在充放电末端变化很慢,中间段变化快,直接回归容易出现末端误差大,可以在损失里对末端样本加权。
import torch import torch.nn as nn class SOCTransformer(nn.Module): def __init__(self, feat_dim=3, d_model=96, nhead=6, num_layers=3, win=150): super().__init__() self.proj = nn.Linear(feat_dim, d_model) # 特征投影 self.pos = nn.Parameter(torch.zeros(1, win, d_model)) # 可学习位置编码 enc_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=d_model*4, dropout=0.1, batch_first=True) self.encoder = nn.TransformerEncoder(enc_layer, num_layers=num_layers) self.head = nn.Sequential( nn.Linear(d_model, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid()) # 输出 SOC 归一化值 def forward(self, x): # x: (B, win, feat_dim) h = self.proj(x) + self.pos h = self.encoder(h) h = h[:, -1, :] # 取最后时刻表征 return self.head(h)逻辑说明:proj把 3 维原始特征升到d_model,位置编码用可学习参数,编码器输出只取最后一个时间步的表征做回归,因为标签就是窗口末端的 SOC。参数上,d_model=96、nhead=6保证 96 能被 6 整除,num_layers=3是我在四工况混合训练下比较平衡的选择。dropout=0.1对抑制过拟合有帮助,但如果你数据量很大可以降到 0.05。
3.2 训练策略:损失函数、学习率和温度泛化
损失函数用 MSE 起步,但我强烈建议加一项对 SOC 变化率的约束,让预测曲线更平滑。单纯 MSE 训出来的模型在工况切换点会有抖动,CKF 融合时这种抖动会被放大。
学习率用 1e-3 配余弦退火,batch size 取 256 到 512。训练轮数不用太多,我一般 50 到 80 轮就收敛,早停看验证集 RMSE。
温度泛化是这套方案的关键难点。如果你把三个温度的数据混在一起训一个模型,模型会学到温度作为条件;但如果你想在没见过的温度上也能用,就得做温度条件归一化,或者干脆每个温度训一个子模型再插值。我一般先混训一个主模型,再对 0°C 单独微调,因为低温是误差重灾区。
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=60) criterion = nn.MSELoss() for epoch in range(60): model.train() for xb, yb in train_loader: pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad(); loss.backward(); optimizer.step() scheduler.step() # 每个 epoch 后在验证集上算 RMSE,保存最优权重逻辑说明:AdamW的权重衰减比 Adam 更规范,CosineAnnealingLR让学习率平滑下降,避免后期震荡。T_max=60和总轮数对齐。验证集必须按工况和温度分层划分,不能随机打乱,否则同一段连续数据既在训练又在验证,RMSE 会假低。
注意:训练集和测试集的工况划分要留出至少一个完整工况做跨工况测试,比如用 BJDST+DST+FUDS 训练、US06 测试,这才是真实上车场景。
4. CKF 融合层:状态方程、观测方程与噪声整定
4.1 二阶 RC 模型离散化与 CKF 容积点
CKF 的核心是用一组容积点去近似非线性函数的积分,相比 EKF 不需要求雅可比矩阵,精度到二阶。状态量取[SOC, U1, U2],其中 U1、U2 是两个 RC 环的极化电压。状态方程由安时积分和 RC 放电方程离散化得到:
def ckf_predict(x, P, I, dt, Q, R0, R1, C1, R2, C2, Cn): """ x: [SOC, U1, U2] I: 电流,放电为正 dt: 采样周期 Cn: 额定容量(Ah),注意单位换算 """ SOC, U1, U2 = x SOC_new = SOC - I * dt / (Cn * 3600) # 安时积分 U1_new = U1 * np.exp(-dt / (R1 * C1)) + R1 * I * (1 - np.exp(-dt / (R1 * C1))) U2_new = U2 * np.exp(-dt / (R2 * C2)) + R2 * I * (1 - np.exp(-dt / (R2 * C2))) x_pred = np.array([SOC_new, U1_new, U2_new]) # 状态转移雅可比用于协方差传播,CKF 用容积点做无迹变换 F = np.diag([1.0, np.exp(-dt/(R1*C1)), np.exp(-dt/(R2*C2))]) P_pred = F @ P @ F.T + Q return x_pred, P_pred逻辑说明:SOC 更新用安时积分,两个极化电压用一阶保持离散化。Q是过程噪声协方差,SOC 那一维我一般给 1e-8 到 1e-7 量级,极化电压维给 1e-6 量级。Cn单位是安时,乘 3600 换成安秒,这一步单位错了 SOC 会整体漂移,是血泪经验。
观测方程用端电压:U = OCV(SOC) - I*R0 - U1 - U2。OCV 和 SOC 的关系用开路电压测试得到的查表函数,中间做线性插值。CKF 的观测更新用 2n 个容积点,n=3 时就是 6 个点,计算量很小。
4.2 Transformer 输出怎么进 CKF:虚拟观测与噪声自适应
融合的关键在于把 Transformer 的 SOC 预测当作一个虚拟观测。观测方程变成两路:一路是端电压观测,一路是 Transformer 的 SOC 观测。观测噪声R_trans不能给固定值,因为 Transformer 在不同工况下置信度不同。
我的做法是用 Transformer 在验证集上的残差统计来整定R_trans:在 DST 上残差小,R_trans给 1e-4;在 US06 上残差大,给 1e-3。更进一步,可以用 Transformer 输出的方差(比如 MC Dropout 多次采样)做自适应,但工程上先用分段常数就够。
def ckf_update(x_pred, P_pred, U_meas, soc_trans, R_trans, R_volt, ocv_func, I, R0): n = 3 # 生成 2n 个容积点 S = np.linalg.cholesky(P_pred) points = [x_pred + np.sqrt(n) * S[:, i] for i in range(n)] + \ [x_pred - np.sqrt(n) * S[:, i] for i in range(n)] # 端电压观测预测 z_volt = np.array([ocv_func(p[0]) - I*R0 - p[1] - p[2] for p in points]) z_volt_mean = z_volt.mean(axis=0) # SOC 虚拟观测预测 z_soc = np.array([p[0] for p in points]) z_soc_mean = z_soc.mean(axis=0) # 互协方差与卡尔曼增益(两路观测拼接) Pxz_v = sum((points[i]-x_pred) * (z_volt[i]-z_volt_mean) for i in range(2*n)) / (2*n) Pzz_v = sum((z_volt[i]-z_volt_mean)**2 for i in range(2*n)) / (2*n) + R_volt Pxz_s = sum((points[i]-x_pred) * (z_soc[i]-z_soc_mean) for i in range(2*n)) / (2*n) Pzz_s = sum((z_soc[i]-z_soc_mean)**2 for i in range(2*n)) / (2*n) + R_trans K_v = Pxz_v / Pzz_v K_s = Pxz_s / Pzz_s x_new = x_pred + K_v*(U_meas - z_volt_mean) + K_s*(soc_trans - z_soc_mean) P_new = P_pred - K_v*Pzz_v*K_v - K_s*Pzz_s*K_s return x_new, P_new逻辑说明:两路观测分别算增益再叠加,R_volt一般给 1e-4 到 1e-3(电压量测噪声),R_trans按工况给。ocv_func是 OCV-SOC 查表插值函数。注意P_new的更新用了简化形式,严格写法要减两个增益项,这里因为两路观测近似独立所以可叠加。
提示:CKF 初始协方差
P0的 SOC 维不要给太大,否则前几十秒收敛过程会震荡。我一般给 SOC 维 1e-4,极化电压维 1e-6。
5. 避坑与排查:多工况多温度下的五条踩坑记录
5.1 现象:0°C 下 SOC 估计整体偏高 5% 以上
原因:低温下电池内阻增大、极化严重,OCV-SOC 曲线在低温下和常温差异明显,用常温 OCV 表做观测更新,观测值系统性偏大。解决:分温度标定 OCV-SOC 表,0°C 单独一张表,插值时按温度做二维插值。这一步做完,低温 RMSE 能从 5% 降到 2% 以内。
5.2 现象:US06 工况下 SOC 曲线高频抖动
原因:US06 电流变化率大,Transformer 预测本身有噪声,CKF 的R_trans给得太小,虚拟观测权重过高,把噪声引进了状态。解决:按工况自适应R_trans,US06 下给 1e-3 甚至 5e-3,同时给 Transformer 输出加一个一阶低通滤波,截止频率 0.5Hz 左右。
5.3 现象:CKF 在工况切换点发散,SOC 跳到 0 或 1
原因:状态协方差P在长时间稳定工况后收缩得很小,突然切换到大电流工况时,过程噪声Q不足以让滤波器快速响应,导致新息过大、增益异常。解决:加新息卡方检测,新息超过阈值时临时放大Q,或者重置P的 SOC 维。这个后悔药我一般都会预留在代码里。
5.4 现象:Transformer 训练 RMSE 很低但测试 RMSE 翻倍
原因:数据泄漏。滑动窗口做训练集和验证集划分时,如果按随机划分,相邻窗口高度重叠,验证集信息泄漏到训练集。解决:按时间段划分,训练集和验证集之间留至少一个窗口长度的间隔,测试集用完全独立的工况段。
5.5 现象:45°C 高温下模型误差反而比 25°C 大
原因:高温下电池自放电和副反应加剧,SOC 定义本身有漂移,参考 SOC 标定精度下降。解决:高温数据做标签时缩短标定周期,或者对高温样本降权。另外高温下 R0 反而减小,CKF 参数要重新辨识,不能沿用常温参数。
6. 进阶技巧:用残差一致性做在线置信度评估
前面五章把方案跑通了,这一章讲一个我实际项目里用得最多的技巧:怎么在不依赖参考 SOC 的在线场景下,判断当前估计到底可不可信。BMS 上车后你拿不到真值,只能靠内部一致性做自检。
核心思路是看两路观测的残差是否一致。端电压残差r_volt = U_meas - U_pred,Transformer 虚拟观测残差r_soc = soc_trans - soc_ckf。正常工作时,两个残差都应该在零附近小幅波动;如果r_volt持续偏大而r_soc很小,说明等效电路模型参数失配了,可能是温度或老化导致;如果r_soc持续偏大而r_volt正常,说明 Transformer 遇到了分布外工况,置信度下降。
我一般维护一个长度为 100 的残差滑动窗口,算两个统计量:残差均值和残差标准差。当|r_volt|均值超过 20mV 持续 30 秒,就触发模型参数在线辨识;当|r_soc|均值超过 3% 持续 30 秒,就把R_trans放大 10 倍,让 CKF 更多依赖物理模型。
class ConfidenceMonitor: def __init__(self, win=100, volt_th=0.02, soc_th=0.03, persist=30): self.r_volt = [] self.r_soc = [] self.win = win self.volt_th = volt_th # 20mV self.soc_th = soc_th # 3% self.persist = persist self.volt_cnt = 0 self.soc_cnt = 0 def update(self, r_volt, r_soc): self.r_volt.append(r_volt); self.r_soc.append(r_soc) if len(self.r_volt) > self.win: self.r_volt.pop(0); self.r_soc.pop(0) mv = np.mean(np.abs(self.r_volt)) ms = np.mean(np.abs(self.r_soc)) self.volt_cnt = self.volt_cnt + 1 if mv > self.volt_th else 0 self.soc_cnt = self.soc_cnt + 1 if ms > self.soc_th else 0 return { "param_reident": self.volt_cnt > self.persist, "inflate_R_trans": self.soc_cnt > self.persist }逻辑说明:win=100对应 100 秒窗口,persist=30表示连续 30 秒超阈值才触发,避免瞬时扰动误报。volt_th=0.02是 20mV,这个值根据你的电压采样精度调,如果采样噪声本身就有 10mV,阈值要相应放大。返回的两个标志位分别驱动参数在线辨识和观测噪声放大。
验证这套置信度机制是否有效,我一般做两件事:一是人为把 CKF 的 R0 改错 30%,看param_reident是否在几十秒内触发;二是拿一段没参与训练的工况数据喂进去,看inflate_R_trans是否在 Transformer 误差变大时及时响应。两个测试都过了,才敢说这套融合方案在在线场景下站得住。
最后说个我自己的习惯:每次换电芯或换温度区间,我都会先把 Transformer 单独跑一遍看残差分布,再决定 CKF 的R_trans初值,而不是拍脑袋给一个固定数。这个习惯帮我省了很多返工。希望帮到你。
本文还有配套的精品资源,点击获取