2026/10/11 3:03:56

电池异常检测竞赛方案:特征工程与阈值调优全复盘

电池异常检测竞赛方案:特征工程与阈值调优全复盘 我参加过一场能源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_bins128): 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, weightscap, minlengthvoltage_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也赢在每次提交前老老实实过一遍检查清单。技术本身没有秘密难的是把每个环节都做到位。对电池异常检测这类任务模型复杂度永远不是第一优先级先理解数据再设计特征最后谨慎地调阈值和后处理顺序一旦颠倒再多模型也救不回来。希望这篇方案能帮你在下一次比赛里少走一段弯路。