☰
电池异常检测竞赛方案:特征工程与阈值调优全复盘
2026/10/11 3:03:40 网站建设 项目流程

我参加过一场能源AI挑战赛,任务落在电池异常检测上,最终排名守在第二,持续多轮没掉出头部。复盘时我经常被问到:这个第二名到底赢在哪?其实答案很朴素——不是某个神秘模型,而是把从数据解读、特征构造、模型规划到阈值调整的每个环节都尽量做对,并且少踩几个大多数队伍都会踩的坑。这篇文章把整套方案从头到尾拆开,适合正在打电池类竞赛、或者需要在产线上做电芯异常告警的工程师参考。我会讲清楚每个步骤的思路、可复现的细节以及踩过的坑,包括为什么某些特征有效、为什么最终没有硬堆大模型、为什么阈值不能直接取0.5。

1. 赛题本质与数据口径:先弄清楚异常到底怎么定义

1.1 异常不是时刻,而是一段区间

这种比赛最常见的设定是:给出一批电芯的循环充放电数据,每条样本包含电池编号、循环序号、时间戳、电压、电流、温度,少数还会附加容量、内阻和SOC估计。赛题核心任务是,基于前面若干个循环的信号,判断一颗电芯在后续运行中是否会出现偏离正常老化轨迹的异常。注意,这里说的“异常”通常不是一个精确的时间点,而是在一段时间窗口内发生某种扰动、衰减加速或内阻突变。如果从一开始就把标签和特征定义成“某一圈是否有异常”,很容易在后处理阶段卡壳。

我和许多参赛者交流时发现,第一轮就翻车的队伍大多死在这个定义上:他们把每个循环单独当作样本,却忘了最终评估的是电芯级二分类。这样特征提取、损失函数、预测输出和后处理全部围绕循环级展开,最后聚合成电芯结论时损失大量信息。正确做法是先明确输出层级:模型输出循环级异常概率,后续再通过窗口聚合得到电芯级概率,最终依据电芯级概率判断。输出层级定了,后面所有策略才不会乱。

1.2 数据文件的真实形态与评测指标

具体到数据文件,通常是每个电芯一个表格,或者一个大文件用电池编号分组。每组内部有几十到两百多个循环,循环内采样点数量差异很大,时间间隔也不均匀。训练集和测试集一般按电芯划分,因为比赛要模拟的是“新电芯来了,请判断它是否异常”,如果按行混切,同一电芯的数据同时出现在训练和验证里,指标会严重虚高。

评测指标方面,我遇到的这场赛事用F1分数,并且异常类召回率权重更高。F1和AUC的优化逻辑不同:AUC只看排序质量,F1还要看具体分界点。如果只在优化器里降低损失,然后习惯性把阈值设成0.5,最后的F1会非常难看。所以拿到规则文件的第一步,一定是确认指标、样本权重、以及提交文件的粒度——是提交电芯级标签还是循环级概率。这个信息比任何模型选型都重要。

1.3 我建议先画三张图,再写第一行代码

拿到数据后,我给自己定了一个规矩:先画图,再跑基线。第一张画正常电芯的容量-循环数曲线,看正常退化轨迹应该长什么样;第二张画异常电芯在异常区间前后的电压曲线,尝试找到异常信号在哪个维度出现;第三张画温度随循环的变化分布,排查是否可以通过温差直接识别异常。

这三张图在这次比赛中直接决定了特征方向。我们从图中发现,异常电芯的容量衰减速度整体上并没有显著加快,真正的差异是某几个循环中充电电压平台出现微小的左移,这种偏移在原始电压曲线里几乎看不出来。只有先把每个循环对齐到容量坐标,再放大电压区间,信号才变得清晰。如果当初跳过这一步直接做统计特征,我就只能得到一堆“平均电压”和“最大温度”这类无用特征,模型大概率只能靠运气。

2. 特征工程:把充放电曲线压缩成可用信号

2.1 容量对齐与IC曲线:电池诊断的招牌特征

电池诊断领域里有一个经典工具叫增量容量分析,也就是dQ/dV曲线。原理不复杂:正常电芯在充电过程中,电压平台比较平稳,容量增量在特定电压区间会出现尖峰;异常电芯的峰位会偏移、峰高会降低,甚至出现分叉。把这种曲线信息引入机器学习,就是先把每个循环的充电段按电压等分,统计每一小段内的容量增量,再除以对应的电压跨度,得到dQ/dV,然后把整条曲线作为这个循环的特征向量。

具体实现时,我会按100到150个电压区间分箱。分箱太少会抹掉峰位细节,太多会让噪声主导特征。还要注意每个电芯的起始电压不完全一样,直接按绝对电压分箱会导致曲线错位,所以要先定位充电平台起点,再做对齐。下面这段是我常用的特征生成逻辑,可以直接套用到类似数据上:

def build_ic_features(cell_df, voltage_bins=128): ic_list = [] for _, cyc in cell_df.groupby("cycle"): charge = cyc[cyc["current"] > 0].sort_values("voltage") if len(charge) < 20: continue cv = charge["voltage"].values cap = charge["capacity"].values if cv[-1] <= cv[0]: continue bins = np.linspace(cv[0], cv[-1], voltage_bins + 1) idx = np.clip(np.digitize(cv, bins), 1, voltage_bins) dq = np.bincount(idx, weights=cap, minlength=voltage_bins + 1)[1:] dv = (cv[-1] - cv[0]) / voltage_bins ic = dq / dv ic_list.append(ic) return np.vstack(ic_list)

拿到IC矩阵后,我通常会再压缩成几个关键量:峰值位置、峰值高度、平台区间的积分面积、首尾比值。这几个特征比直接用原始IC向量更抗噪声,也更容易被树模型利用。

2.2 循环窗口统计:退化和温度信号

IC曲线擅长刻画电化学状态,但对多循环之间的退化速度不敏感。因此我另外构造了一组循环级统计特征:以每10个循环为一个窗口,计算窗口内平均电压、电压标准差、最高温度、温度变化率、充电时长中位数、放电容量的线性拟合斜率。放电容量斜率这个特征尤其有用,它反映的是容量保持率的衰减快慢,异常电芯往往在某个窗口出现斜率突然变陡,而整体平均斜率并不明显。

温差特征也值得单拎出来说。正常电芯充电过程中的温升曲线平滑,异常电芯则经常伴随局部温差脉冲。我做过一个对比实验:只用“窗口内温差绝对值超过阈值的次数”和“最大温升速率”这两个特征,配合循环编号,一棵浅树就能拿到0.6以上的AUC。这说明温差信号与标签高度相关,而且特征本身实现简单,白送的分数不要白不要。

2.3 差分与内阻估算:捕捉瞬时突变

时序数据里,变化率往往比幅值更可靠。对电压序列做一阶差分dV/dt和二阶差分,再统计差分信号的绝对值均值、最大值、偏度、峰度,可以捕捉异常注入瞬间的突变。如果数据中包含电流阶跃信息,还能估算直流内阻:取电流变化前后的电压差值,除以电流差值,得到内阻估计值。内阻异常经常比容量衰减出现得更早,是电池早期失效的重要前兆。

这里有一个实际的工程坑:如果某个循环只采了几十个点,二阶差分噪声会非常大。我的处理是先对电压做滑动平均平滑,再做差分;或者干脆用三次样条把每个循环重采样到固定长度,再统一计算差分。这些预处理参数一旦定下来就要全量固定,避免不同电芯因为采样点密度不一样而产生系统性偏差。

2.4 频域与小波:锦上添花的补充特征

除了时域和容量域特征,频域信息偶尔能提供额外增益。对每个循环的电压序列做快速傅里叶变换,取前几个主频分量的能量占比以及频谱熵,这类特征对周期性扰动比较敏感。另一种做法是离散小波变换,它可以同时反映不同频带的能量分布,对“某一循环突然出现毛刺”的问题更有效。

不过要提醒一句:频域特征的计算开销大,样本量小的时候也更容易过拟合。我通常先在小验证集上检验增益,只有稳定提升才留下,否则果断去掉。整个比赛期间,我们的原则始终是“特征数量不代表质量”,一个不能稳定提升验证分数的特征,哪怕听起来再高级,也只是给过拟合添砖加瓦。

3. 模型配置:树模型打底,序列模型辅助,无监督补漏

3.1 样本量决定模型复杂度,先别上Transformer

比赛刚开始,团队里有人提议直接上Transformer,理由是时序数据天然适合序列模型。我明确反对。这个赛题的电芯数量只有几百颗,折算成训练样本不过几千级,深度模型在小样本上极易过拟合,而且调参周期长,一次实验就烧掉半天时间。相比之下,把特征工程做扎实,再跑一个梯度提升树模型,几分钟内就能得到高效基线。

这里有一个选型原则可以分享:

样本量级推荐路线理由
几千以下梯度提升树 + 强特征工程训练快、稳定、解释性强
几万级TCN/LSTM等轻量序列模型能利用顺序信息,复杂度可控
几十万以上Transformer或大规模序列模型数据量足够支撑长程依赖学习

比赛不是炫技场,稳定高效地拿到分数才是第一目标。

3.2 树模型:表格特征的强基线与默认选择

我们把所有特征横向拼接成一张大表,每一行对应一个循环或固定窗口,字段包括IC曲线压缩特征、循环统计特征、差分特征、内阻特征等。验证用分组交叉验证,以电芯编号为分组单位。在这个特征集上,LightGBM和CatBoost的离线结果差别不大,我们最终选择CatBoost,因为它的排序编码对高基数ID字段处理更方便,数值特征也无需手工标准化。

训练时有三个细节值得强调:第一,用类别权重处理正常样本远多于异常样本的不平衡问题;第二,学习率调低到0.02左右,配合更多迭代轮数,早停监控验证集F1;第三,不要只用一个验证集调参,可以多跑几个分组,取参数设置的鲁棒性作为最终依据。树模型在这个任务里有两个作用:自动筛选特征交互,同时输出一个非常稳定的基线概率。

3.3 序列模型:变分窗口下的失效趋势探查

树模型对时间顺序不敏感,而电池异常恰恰可能由顺序决定。为了补齐这个短板,我们并行训练了一个轻量序列模型。输入不是原始采样点,而是经过切片处理的循环窗口序列:每个窗口包含最近20个循环的电压曲线、温度曲线和若干统计量。模型结构是两层卷积加一层双向LSTM,输出异常概率。

从最终指标看,序列模型不如树模型,但它的价值在于提供了一条概率变化轨迹。当某颗电芯从第50个循环开始持续给出高概率,而树模型直到中后期才响应时,就说明特征工程里缺少这一时段的信号,我们据此补充了窗口特征。这种“用序列模型做信号探查、树模型做最终决策”的搭配,比单纯堆叠多个模型更有意义。

3.4 自编码器与孤立森林:无标签数据的用法

赛题附带了一批无标签数据,直接丢掉太可惜。我们用了两套无监督方法:第一套是自编码器,用正常电芯的容量-循环曲线训练重建模型,然后计算无标签电芯的重建误差,误差异常大的直接标记为疑似异常;第二套是孤立森林,基于统计特征给出异常分数。关键操作是,把这两路分数当作额外特征喂给树模型,而不是作为独立输出参与投票。

为什么不直接让无监督模型输出决策?因为无监督方法误报率高,如果直接叠加,误报会被完整传播到最终结果。而作为特征时,树模型可以学习“无监督分数高”在多大程度上值得信任,从而获得更好的权衡。这个改动最终稳定提升了约两个百分点的F1,算是投入产出比最高的一步。

3.5 融合权重与伪标签的严格准入

最终融合用的是简单的加权平均:树模型权重0.6,序列模型0.25,无监督分数归一化后占0.15。权重通过验证集上的小规模网格搜索确定,没有做复杂的Stacking——样本量少,Stacking的次级模型很容易过拟合,收益不划算。

比赛后期我们也试过伪标签,把模型置信度高于0.9和低于0.1的预测样本加入训练集。效果很不稳定,某些特征组合下提升明显,换一组特征就失效。所以我给团队定的规矩是:任何伪标签方案必须在至少两个不同特征集上同时复现增益,否则不采用。这个标准看起来很保守,但它确实帮我们挡住了很多次看似有效、实则偶然的提分。

4. 阈值与后处理:从模型概率到可靠告警的最后一公里

4.1 阈值搜索:F1得分不等于自动取0.5

模型输出的是概率,而最终评分是F1,二者之间隔着一个阈值。许多参赛者直接把0.5当成决策边界,这在类别不平衡且召回率敏感的任务里几乎必然吃亏。我们的做法是在验证集上从0.05到0.95按0.01步长扫描阈值,计算每个阈值下的F1、精确率和召回率,然后选择F1最高处的阈值,同时观察精确率-召回率曲线的陡峭程度。如果曲线在峰值附近非常尖锐,说明这个阈值稳健性差,宁可选择一个峰值略低但更平坦的区域。

这里有一个容易忽略的小技巧:验证集样本量不大时,F1曲线噪声很重,直接argmax很可能选到噪声尖峰。我通常先对F1曲线做三到五点的移动平均再取峰值,或者把多次分组交叉验证的阈值结果取平均。现实中很多队伍离线F1很高、线上崩盘,往往就是阈值选在了验证集噪声尖峰上。下面这个搜索函数可以直接拿来用:

def search_threshold(y_true, y_prob): best_th, best_f1 = 0.5, 0.0 for th in np.arange(0.05, 0.96, 0.01): preds = (y_prob >= th).astype(int) f1 = f1_score(y_true, preds) if f1 > best_f1: best_th, best_f1 = th, f1 return best_th, best_f1

再多说一句:阈值不是调一次就完事的。每次特征集变化、模型权重变化后,概率分布都会整体偏移,阈值必须重新扫描。

4.2 时序平滑与窗口聚合:把循环概率变为电芯结论

前面说过,比赛判断的是电芯级标签,但异常信号往往集中在某些循环。直接拿循环级概率做电芯判断会引入大量噪声,因为某个孤立的异常循环可能是测量误差。我们做了两类后处理。

第一类是时间平滑:对同一颗电芯的概率序列做窗口为5的滑动平均。如果某一循环概率高但前后循环都不高,平滑后会被自然压低。第二类是窗口放大:对每个循环的概率,取过去10个循环的均值,得到“近期趋势概率”,用于贴近“目标窗口内是否出现过异常”的标签语义。也就是说,判断一颗电芯是否异常,要看它在历史概率窗口中是否存在持续走高的片段,而不是只看单圈概率。

实测下来,这两步后处理让最终F1提升了约3个百分点,且对特征选择不敏感。如果赛题要求电芯级输出,我强烈建议先做循环级预测,再做窗口聚合,而不要直接训练电芯级分类器——电芯级样本太少,直接分类几乎必然过拟合。

4.3 概率校准与业务代价权衡

F1之外,还需要考虑实际业务语义:漏报一颗异常电芯可能带来整组电池包的安全风险,误报一颗只是增加一点人工复查成本。因此我们在最终决策时不刻意追求F1峰值,而是找F1峰值附近偏召回一侧的阈值,让召回率保持在一个更安全的位置。

另外,模型概率并不一定等于真实概率。我们在验证集上按分桶统计实际异常比例,发现模型在0.5到0.7区间给出的概率普遍偏高,说明概率偏乐观。针对这种情况,可以对概率做等张回归或局部校准,再重新找阈值。注意校准要只针对最终阈值附近进行,不要对整个分布做大范围调整,否则会破坏排序关系和已有阈值。这些操作都很轻量,但对最终线上的稳定性帮助很大。

5. 排位赛复盘:那些让名次稳固或崩盘的关键细节

5.1 分组交叉验证:一次修正让离线分数下降8个点

第一批特征做出来后,团队有人直接用普通交叉验证评估,离线F1很漂亮,线上却明显缩水。排查半天才发现问题出在分组上:同一颗电芯的多个循环之间存在强相关性,如果训练集和验证集混入同一颗电芯的数据,模型相当于提前见过同一个对象,验证指标自然虚高。改成按电芯分组后,离线F1直接下降了近8个百分点,但也终于和线上表现对上了。

这次修正可以说是整个比赛里最重要的一步。从那以后,我们不再信任任何非分组的验证分数。对这类任务,分组交叉验证不是可选项,而是必须项。后来我还养成了一个习惯:每次跑完实验,先检查验证集里有没有和训练集重复的电芯编号,用一条断言保证分组隔离。

5.2 三类特征泄漏的排查方法和处理标准

特征泄漏是这类比赛最隐蔽的敌人。我总结出三个常见来源。第一是全局归一化泄漏:如果使用整个数据集的均值和标准差做归一化,测试集的信息就间接进入了训练过程,正确做法是只在训练集上计算统计量再应用到其他数据。第二是未来窗口泄漏:构造“过去N个循环”特征时,如果代码不小心把当前循环之后的数据也纳入统计,模型就偷偷看到了未来。第三是标签间接泄漏:某个特征在异常发生时必然等于某个值,模型只需要记住这个映射关系就能拿高分。

遇到可疑的高增益特征,我的处理标准很简单:如果删掉后验证F1几乎不变,就删掉;如果删掉后大幅下降,必须先解释它的业务含义,确认它不是间接泄漏。宁可保守一点,也不要赌一个无法解释的特征能在线上继续灵验。为了系统排查未来窗口泄漏,我写了一个小函数,它对每个特征做随机重排后观察F1是否异常升高,从源头把这类问题暴露出来。

5.3 公开榜与私有榜:如何判断线上分数的稳定性

排位赛期间我们长期守在第二名,最担心的不是提不了分,而是私有榜重新排序后掉下去。为了降低线上波动风险,我做了两件事:一是每次提交都让答案之间保留一定差异,观察公开榜分数的波动范围;二是把重点特征控制在三组以内,保证验证集与线上分布尽量一致。只要分组交叉验证的F1和公开榜相差在1到2个百分点以内,私有榜通常不会有太大意外。

还有一个容易被忽视的经验:不要因为一次提交分高就高兴,更不要因为一次提交分低就慌。高分的单次提交可能只是匹配了公开榜的某个子集特征,连续两三次提交都稳定在同一区间,才是模型的真实能力。我们的最后一条提交规则是:连续三个版本的验证集F1都高于上一版,线上分数也稳定同向提升,才允许替换模型。这个规则很笨,但确实挡住了很多次“自我感动型优化”。

5.4 提交前必过的回归检查清单

接近比赛尾声,我把常用检查内容整理成固定清单,每次提交前逐项打勾:

  • 确认训练集、验证集、测试集按电芯分组隔离,没有交叉;
  • 确认所有归一化和统计量只基于训练集计算;
  • 确认所有特征窗口只使用历史信息,没有向未来偏移;
  • 确认阈值与融合权重来自多次分组验证的平均结果,不是单次噪声;
  • 确认特征增益在至少两个特征集上可复现;
  • 确认代码复现结果与上次完全一致,排除随机性影响。

这套清单看起来很朴素,但真正拉开名次的往往就是这些细节。我可以很负责地说,我们最后拿到的第二名,主要靠的是少犯错误,而不是某个模型直接领先对手一个身位。

复盘做完了,最后分享一点个人体会。我经常被问:这个第二名到底赢在什么地方?仔细想想,赢在比别人多画了三张图,赢在坚持了分组交叉验证,赢在愿意把阈值从0.5调到0.63,也赢在每次提交前老老实实过一遍检查清单。技术本身没有秘密,难的是把每个环节都做到位。对电池异常检测这类任务,模型复杂度永远不是第一优先级,先理解数据,再设计特征,最后谨慎地调阈值和后处理,顺序一旦颠倒,再多模型也救不回来。希望这篇方案能帮你在下一次比赛里少走一段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询