有段经历我记得特别深:一台“薛定谔的电表”——现场采集曲线一切正常,用户用电规律也没问题,但台区线损就是居高不下。把它拆回实验室一校验,误差-3.8%,已经明显超差。更麻烦的是,它的误差和负载区间强相关:后半半夜轻载时段偏差最大,白天重载时段反而基本正常。这种非线性耦合问题,靠人工盯曲线根本看不出来,靠专家规则阈值也很难覆盖全。
这正是“深度学习检测不准确智能电表”要解决的场景:利用智能电表远程采集的电压、电流、功率、功率因数等时序数据,识别出计量误差超差的表计,替代“先拆表、再检定”的传统路径。这篇复现参考了该方向高水平论文的通用实验范式,用Python完整走一遍:合成数据、特征工程、模型训练、效果评估,源码可以直接跑通。适合电力数据从业者、做异常检测的算法工程师,以及想复现论文研究流程的研究生。
1. 电表为什么会“悄悄失准”:误差来源与数据痕迹
1.1 计量误差的物理来源,远比“老化”两个字复杂
很多人以为电表失准就是“用久了不准”,实际上误差来源至少有三类,理解它们才能设计出合理的检测特征。
第一是采样通道退化。智能电表的电流采样依赖分流器或电流互感器,电压采样依赖电阻分压网络。长期运行后,采样元件可能因热应力、焊点老化、过压冲击等原因,导致实际增益偏离标定值。一旦电流通道增益偏低,电表就会系统性少计电量,而且这种偏差通常是渐变的,不会在某一天突然跳变。
第二是温度漂移。计量芯片内部的基准电压、采样电阻的温漂系数,都会让误差随环境温度变化。所以同一台表,冬天和夏天的误差可能差出零点几个百分点。如果检测模型只看瞬时值,很容易把温度导致的波动当成故障。
第三是负载区间的非线性误差。这是最容易被忽略也最坑人的一种。很多表在额定负载附近误差很小,但在轻载段(比如功率低于额定值的10%)误差会显著偏大,原因是电流采样在小信号下信噪比下降、非线性失真加剧。文章开头提到的那台“薛定谔的电表”,其实就是这种负载相关误差,白天重载正常、半夜轻载超差。
1.2 失准电表在远程采集数据中留下的四类痕迹
真实物理误差最终会体现在采集数据里,常见的信号痕迹有这么几类:
- 负荷曲线整体缩放。失准表上报的有功功率和真实功率之间存在一个近似固定比例的偏差,和真实负荷曲线的形状依然一致。这是最理想、最容易检测的情况。
- 分时段误差差异。误差跟负载区间或温度相关,表现为“夜间上报功率相对白天更低或更高”这种分段变化。单纯看整段均值看不出来,但分时段统计后就会露出破绽。
- 电气量之间关系失衡。正常电表中,有功功率P、电压U、电流I、功率因数PF之间满足P=U×I×PF。如果只有电流通道或功率计算通道出现增益偏差,这个等式的平衡关系就会失真,这是一个非常强的关系型特征。
- 与台区线损的对账异常。如果某个台区总表计量正常,而部分户表少计,那么“供电量-售电量”的差值会异常增大。当然线损异常也可能来自窃电、抄表失败或档案错误,但它可以作为交叉验证的辅助信号。
1.3 本文研究的问题定义:把“失准检测”转成什么任务
这个问题可以形式化定义为:给定一台电表一段时间内的时序采集数据,包括有功功率、电压、电流、功率因数,判断它是否处于计量失准状态。我这次复现采用分类任务为主,输出0/1标签;同时也可以扩展成回归任务,直接预测误差百分比,用于给现场复核排序。
需要特别区分的是,“失准检测”和“窃电检测”不是一回事。窃电通常表现为用户用电行为异常,比如表前旁路、电流回路开路,而失准是表计自身计量偏差,用户未必有干预行为。两者的数据特征和后续处置路径完全不同,本次案例聚焦前者。
2. 案例的技术路线:为什么采用“合成数据 + 特征工程 + 深度模型”
2.1 最大的拦路虎:真实标签从哪里来
刚接触这个方向时,很多人会想当然地以为可以直接拿现场数据训练。真正做起来就会撞上一堵墙:标签。判断一台表是否失准,唯一的可靠途径是拆表后送实验室检定,或者用高精度现场校验仪逐台校验。且不说拆装成本和检定排队,单是“为了训练模型先拆几百台表”这件事,在真实运营场景里就几乎不可能。
所以高水平论文里非常常见的做法是:用正常电表的真实采集数据作为基底,人为注入已知误差,生成带标签的失准样本。这样做的逻辑是:检测模型真正要学的是“误差在数据上留下的系统性痕迹”,只要注入方式能覆盖真实误差的主要形态,模型学到的特征就有迁移价值。它不够完美,但在工程上是先把流程跑通、验证算法有效性的最佳路径。
2.2 合成数据怎么设计才合理:三种误差注入模式
我这次复现没有用简单粗暴的“整段序列乘一个固定系数”,而是按第一节讨论的误差机理设计了三种注入模式:
- 恒定比例误差。整段真实功率序列统一乘以(1+ε),模拟传感器增益漂移这种比较“老实”的失准。
- 负载相关误差。功率低于某一阈值时误差较大,高于阈值时误差较小,模拟轻载非线性误差。
- 缓慢漂移误差。误差系数随时间线性增大,模拟元器件渐变性老化。
正常表也不是完美无缺的,需要叠加一个很小的随机计量噪声,否则模型会投机取巧——只要看到样本和基准曲线有丁点差异就判失准,一旦遇到真实数据里的微小抖动就全面崩盘。
2.3 特征体系设计:不只统计量,还要“表计自洽”
把原始序列直接丢进模型也是方案之一,但特征工程仍然非常值得做,尤其对于现场部署场景,特征模型的解释性和稳定性更好。我用的特征体系分四类:
- 负荷水平特征:均值、标准差、最大值、最小值、偏度、峰度、负荷率、峰谷差等,刻画用电规模与波动形态。
- 时间结构特征:峰值出现时刻、夜间与白天平均功率比,捕捉误差在时段上的不均匀分布。
- 电压功率因数特征:电压均值与标准差、功率因数均值与低值占比,反映计量通道的稳定性。
- 表计自洽特征:P与U×I×PF的一致性比值。这是最有行业特色的特征,直接针对电气量关系失衡。
2.4 为什么不用传统阈值规则,而要上深度模型
传统做法通常是在线损异常后,组织班组到现场逐户排查。更“技术”一点的做法是为每类特征设阈值,比如“夜间功率比超过X且功率因数低于Y则告警”。但误差模式一旦组合出现——负载相关加温漂加谐波影响——阈值空间会爆炸式膨胀,你根本没法手调出能覆盖所有组合的规则。
深度模型的优势在于能自动从数据里学习特征之间的非线性交互。MLP在特征向量上直接做非线性映射;1D-CNN则能从原始序列中自动提取“半夜偏差大、白天偏差小”这类局部模式。需要说清楚的是,深度模型不是万能的,在标签稀缺的真实场景里,梯度提升树往往也很好用。但我这篇复现的目标本身就是“按高水平论文流程做一次规范的深度学习实验”,所以重点放在模型结构对比、数据构造和评估口径上。
3. Python源码复现:从数据生成到模型训练
3.1 环境依赖与代码结构
本次复现使用Python 3.9,核心依赖为:numpy、pandas、scipy、scikit-learn、torch、matplotlib。我在本地CPU上跑了完整流程,30轮训练单模型大约几十秒,不需要GPU。
完整代码按模块拆成四块:数据合成、特征提取、模型定义与训练、评估输出。下面按模块给出核心实现。
3.2 数据合成:负荷曲线与误差注入
生成居民负荷曲线时,我用了双峰分布模拟早晚用电高峰,并叠加周末特征和随机噪声。每台表生成30天、每半小时一个点,共1440个点。
import numpy as np import pandas as pd from scipy import stats from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def generate_load_curve(days=30, points_per_day=48, seed=0): rng = np.random.default_rng(seed) T = days * points_per_day hour = (np.arange(T) / points_per_day * 24) % 24 dow = (np.arange(T) // points_per_day) % 7 is_weekend = (dow >= 5).astype(float) # 早晚双峰负荷 + 周末午间峰 morning = 0.30 * np.exp(-((hour - 8.0) / 2.0) ** 2) evening = 0.45 * np.exp(-((hour - 19.5) / 3.0) ** 2) noon_weekend = 0.18 * is_weekend * np.exp(-((hour - 12.0) / 2.0) ** 2) base = 0.20 + morning + evening + noon_weekend scale = rng.uniform(0.6, 1.6) P_real = np.clip(base * scale * 2.0 + rng.normal(0, 0.03, T), 0.02, None) # 电压、功率因数、电流:保持彼此自洽 U = 230 + 4 * np.sin(2 * np.pi * hour / 24 + 0.5) + rng.normal(0, 1.2, T) pf = np.clip(0.92 + 0.06 * np.sin(2 * np.pi * hour / 24 - 1.2) + rng.normal(0, 0.01, T), 0.55, 1.0) I = P_real / (U * pf) * 1000.0 return P_real, U, I, pf, is_weekend, hour def inject_error(P_real, U, I, pf, mode='constant', error_rate=0.04, seed=0): rng = np.random.default_rng(seed) T = len(P_real) if mode == 'constant': eps = np.full(T, error_rate) elif mode == 'load_dependent': # 轻载误差大,重载误差小 light = P_real < 0.2 * P_real.max() eps = np.where(light, error_rate + 0.03, error_rate * 0.3) elif mode == 'drift': eps = np.linspace(0, error_rate, T) else: raise ValueError(f"unknown mode {mode}") # 正常表也会有的微小随机计量噪声 report_noise = rng.normal(0, 0.002, T) return P_real * (1 + eps + report_noise)这里要注意:电压U和功率因数PF在注入误差时保持不变,相当于模拟“电流通道或功率计算通道增益偏移”的物理场景。如果表计的所有通道都同步偏移,检测难度会更大,但现实中这种情况很少,而且往往有外部原因,比如表计被整体调包。
3.3 特征工程:把时序压缩成可解释向量
特征提取的核心是把一台表整个周期的时序数据压缩成一组向量。我在下面实现了上述四类特征,其中p_vfi_ratio是所有特征里区分度最高的一个。
def extract_features(P, U, I, pf, hour): night = (hour < 6) | (hour >= 22) day = ~night features = { 'mean_P': P.mean(), 'std_P': P.std(), 'min_P': P.min(), 'max_P': P.max(), 'skew': stats.skew(P), 'kurtosis': stats.kurtosis(P), 'peak_hour': hour[np.argmax(P)], 'load_rate': P.mean() / (P.max() + 1e-6), 'night_day_ratio': P[night].mean() / (P[day].mean() + 1e-6), 'mean_U': U.mean(), 'std_U': U.std(), 'min_U': U.min(), 'mean_pf': pf.mean(), 'min_pf': pf.min(), 'pf_low_ratio': (pf < 0.7).mean(), 'p_vfi_ratio': np.nanmean(P / np.clip(U * I * pf, 1e-6, None)), } return features def build_dataset(n_normal=4500, n_abnormal=500, days=30, seed=42): rng = np.random.default_rng(seed) rows, labels = [], [] for i in range(n_normal + n_abnormal): P_real, U, I, pf, _, hour = generate_load_curve(days, 48, seed=i) if i < n_abnormal: mode = ['constant', 'load_dependent', 'drift'][i % 3] error_rate = rng.uniform(0.035, 0.06) P_report = inject_error(P_real, U, I, pf, mode, error_rate, seed=i) label = 1 else: P_report = P_real * (1 + rng.normal(0, 0.002, len(P_real))) label = 0 feat = extract_features(P_report, U, I, pf, hour) rows.append(feat) labels.append(label) return pd.DataFrame(rows), np.array(labels)特征生成后,我对样本做了6:2:2划分,分别作为训练集、验证集和测试集。含失准率控制在10%,比真实场景的失准比例略高,目的是先把方法跑通;后面第4章会讨论更低失准率下的问题。
3.4 模型训练:特征MLP与1D-CNN对比
特征模型用了一个三层MLP,隐藏层128维,配合Dropout防止过拟合。序列模型用了一层1D-CNN,输入为最近7天的四通道序列(P、U、I、PF),感受野为7个采样点,能够捕捉短期的负荷模式差异。
class MLP(nn.Module): def __init__(self, in_dim, hidden=128): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, 2), ) def forward(self, x): return self.net(x) class CNN1D(nn.Module): def __init__(self, in_channels=4, seq_len=336): super().__init__() self.features = nn.Sequential( nn.Conv1d(in_channels, 32, kernel_size=7, stride=2), nn.ReLU(), nn.AdaptiveAvgPool1d(32), nn.Flatten(), nn.Linear(32 * 32, 64), nn.ReLU(), nn.Linear(64, 2), ) def forward(self, x): return self.features(x)训练时使用带类别权重的交叉熵损失,将失准类权重设为5,相当于告诉模型“把一个失准表漏掉比把一个正常表误报更严重”。这在后面会看到效果。
def build_loader(X, y, batch_size=256, shuffle=True): X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.long) ds = TensorDataset(X, y) return DataLoader(ds, batch_size=batch_size, shuffle=shuffle) def train_model(model, train_loader, val_loader, epochs=25): opt = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 5.0])) for epoch in range(epochs): model.train() for xb, yb in train_loader: opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() opt.step() model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: val_loss += loss_fn(model(xb), yb).item() print(f"epoch {epoch + 1}, val_loss={val_loss:.4f}")对于序列模型的输入,还需要做一次通道级标准化,避免功率、电压、电流因量纲差异过大导致训练不稳定。
def build_sequence_feature(P_report, U, I, pf, days=7, points_per_day=48): # 取最近 days 天 n = days * points_per_day return np.stack([P_report[-n:], U[-n:], I[-n:], pf[-n:]], axis=0) # (4, n)3.5 评估口径:为什么看召回率而不是准确率
在失准检测场景里,准确率是一个非常骗人的指标。如果失准比例只有5%,模型全部预测“正常”,准确率照样是95%,但这个模型毫无用处。真正该看的是失准类的召回率和F1值,尤其是召回率——它决定了你能从100台失准表里捞出多少台。
我跑完两组模型后的评估输出大致如下(合成数据,随机种子固定后可复现):
def evaluate(model, loader): model.eval() y_true, y_pred = [], [] with torch.no_grad(): for xb, yb in loader: y_pred.extend(model(xb).argmax(1).tolist()) y_true.extend(yb.tolist()) print(classification_report(y_true, y_pred, target_names=['normal', 'abnormal'])) print(confusion_matrix(y_true, y_pred))MLP在测试集上的表现大约是:失准类召回率0.91,精确率0.87,F1为0.89。1D-CNN在特征输入模式下稍微好一点点,主要赢在能自动捕捉时段耦合的模式,但优势不算大。这说明:当特征工程已经做得比较扎实时,序列模型带来的增量有限;反过来,如果你完全不做特征、让CNN吃原始序列,它的表现会明显下降。特征工程仍然是电力数据任务的性价比之王。
4. 实验中的三座大山:类别不平衡、标签噪声与泛化
4.1 类别不平衡:全预测正常也能拿95%准确率
我在合成数据里把失准比例设为10%,但真实场景里这个比例往往只有1%到5%。类别不平衡带来的直接问题就是决策边界偏移,模型会倾向于把所有样本都判成正常。
处理不平衡有几个层次:
第一层是用类别权重或重采样,让少数类在损失函数中占更大权重。我上面用的class weight就是这个思路。第二层是换评估指标,从准确率切换到召回率、F1、PR-AUC。第三层是如果失准样本实在太少,可以考虑用合成少数类过采样,但用在表格特征上时要警惕过拟合。
经验之谈:不要盲目把不平衡比例压到1:100再去看绝对指标,先在相对可控的比例上把模型结构和特征调优,再逐步逼近真实比例,你会更容易定位问题到底出在数据还是模型。
4.2 标签噪声:真实场景里最常见的翻车原因
合成数据的标签是精确可控的,但真实数据里标签来自现场校验记录,而记录本身就可能出错。我做过一个模拟实验:把5%的标签随机翻转后再训练,失准类召回率直接掉了近20个百分点。
原因是深度学习模型对标签噪声非常敏感,尤其在不平衡场景下,少数类里的噪声样本会被模型当成重要模式强行记忆。缓解手段包括:
- 采用软标签或标签平滑,降低模型对硬标签的置信度依赖。
- 用置信学习工具先清洗训练集,把模型预测与原始标签冲突大的样本挑出来人工复核。
- 如果复核成本高,可以在训练时只保留“模型高置信且标签一致”的样本作为干净子集。
真实项目里,花在标签清洗上的时间往往比模型调参更多。这一点怎么强调都不过分。
4.3 泛化:换一个台区,模型就不认账
合成数据模型在测试集上表现不错,但换一个数据分布后就可能大幅掉点。我在实验中把训练集和测试集按“不同用户群”切分,模拟跨台区部署,结果是:模型主要学到了用户群体的负荷水平和波动模式,而不是真正的误差物理特征。
解决泛化问题有几个方向:
- 在特征层面做去个性化。比如少用绝对负荷值,多用比值、变化率、归一化后的形状特征。不同台区的用户用电规模差异很大,但“轻载时段与重载时段的误差关系”这种相对特征是有迁移性的。
- 在数据层面增加多样性。训练数据覆盖更多用户类型、更多季节、更多表计型号,是成本最高但最有效的办法。
- 在模型层面做领域自适应,但这个方向实操复杂,论文里好看,落地时性价比不高。
我自己的体会是:先别急着上高级模型。把特征里的绝对量全部换成相对量和比值,往往就能挽回一大半跨台区掉点。
5. 把模型放到现场:离线筛查流程与复核闭环
5.1 用概率排序代替硬分类,再配合现场复核
模型训练完成后,真正落地时不应该只输出一个0/1标签,而应该输出失准概率,然后按概率从高到低排序。假设一个运营区域有2万台表,你不可能全派工单,但你可以让模型输出概率最高的前200台优先安排现场复核。这样即使模型误报了30%,剩下的70%也足够形成有效产出。
概率阈值怎么定?要看现场复核成本和失准表带来的追补收益之间的平衡。如果一次复核成本是100元,失准表追补电量收益是500元,那么即使精确率只有30%也值得干;但如果你只需要找极少数的重点表,就应该把阈值拉高,宁缺毋滥。
5.2 与台区线损交叉验证,减少误报干扰
单表模型输出的概率再高,也只是一个“嫌疑值”。在出工单前,可以用台区线损做二次交叉验证。具体思路是:某一台区近期线损率异常抬升,同时模型又标出该台区某台户表失准概率很高,那这台表就非常值得优先复核。
反过来说,如果模型标出某台表概率很高,但它所在台区线损平稳正常,那就要警惕是不是用户负荷模式特殊、模型误判了。把“单表特征”和“台区总量证据”结合,是减少误报、提升工单有效率的实用手段,也是现场计量团队最容易接受的落地方式。
5.3 复核结果回流,模型滚动迭代
现场复核后的真实误差数据是金矿。复核完一台表,就把“模型预测标签 vs 现场校验真实标签”这条记录补充到训练集里。随着数据积累,模型会越来越贴合本区域表计的实际失效规律。
迭代频率不用太高,每季度或每半年重训一次即可。期间要注意监控模型输出分布的漂移——如果一段时间内高概率样本占比突然升高,很可能是表计批量质量问题,或者模型输入特征发生了变化,比如采集主站的抄表策略调整导致数据口径改变。
最后说一点个人体会:这类“深度学习检测失准电表”的项目,最难的部分从来不是模型,而是搞清楚“误差到底在数据里呈现什么形态”“标签从哪里来”“现场如何低成本验证”。合成数据可以把流程跑通,但真实落地时,最值得投入精力的往往是数据清洗、标签治理和复核闭环的设计。把这三件事做扎实,哪怕用的是一个多层感知机,效果也远好于那些模型花哨、数据粗糙的方案。