#!/usr/bin/env python3 """ 电力波形故障识别 — 1D-CNN 训练脚本 (完整版) 使用方法: python train_1dcnn.py --data_dir ./data --epochs 100 数据准备: data/ train_data.npy (N_train, 128) float32 训练样本 train_labels.npy (N_train,) int64 训练标签 val_data.npy (N_val, 128) val_labels.npy (N_val,) test_data.npy (N_test, 128) test_labels.npy (N_test,) 输出: checkpoints/best_model.pth 最佳权重 checkpoints/fault_1dcnn.onnx ONNX 模型 (用于 RKNN 转换) """ import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from sklearn.metrics import classification_report import os import argparse from collections import Counter # ============================================================ # 故障类型名称 # ============================================================ FAULT_NAMES = [ 'Normal', # 0 正常 'A-G', # 1 A相单相接地 'BC', # 2 BC相间短路 'AB-G', # 3 AB两相接地 'ABC', # 4 三相短路 ] NUM_CLASSES = len(FAULT_NAMES) SEQ_LEN = 128 INPUT_CH = 1 # ============================================================ # 模型定义 # ============================================================ class Fault1DCNN(nn.Module): """ 轻量级 1D-CNN,面向 RK3568 NPU 部署 参数量 ~25K,模型 FP32 大小 ~100KB """ def __init__(self, num_classes=NUM_CLASSES, input_channels=INPUT_CH, seq_len=SEQ_LEN): super().__init__() _ = seq_len # unused, kept for API compatibility self.block1 = nn.Sequential( nn.Conv1d(input_channels, 16, kernel_size=7, stride=1, padding='same', bias=False), nn.BatchNorm1d(16), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2), # 128 → 64 ) self.block2 = nn.Sequential( nn.Conv1d(16, 32, kernel_size=5, stride=1, padding='same', bias=False), nn.BatchNorm1d(32), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2), # 64 → 32 ) self.block3 = nn.Sequential( nn.Conv1d(32, 64, kernel_size=3, stride=1, padding='same', bias=False), nn.BatchNorm1d(64), nn.ReLU(inplace=True), ) self.gap = nn.AdaptiveAvgPool1d(1) # 32 → 1 self.classifier = nn.Linear(64, num_classes) self._init_weights() def _init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv1d): nn.init.kaiming_normal_(m.weight, mode='fan_out') elif isinstance(m, nn.BatchNorm1d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) def forward(self, x): x = self.block1(x) # (B, 16, 64) x = self.block2(x) # (B, 32, 32) x = self.block3(x) # (B, 64, 32) x = self.gap(x) # (B, 64, 1) x = x.view(x.size(0), -1) x = self.classifier(x) # (B, num_classes) return x # ============================================================ # 数据集 (含数据增强) # ============================================================ class WaveformDataset(Dataset): def __init__(self, data_path, label_path, augment=False): self.data = np.load(data_path).astype(np.float32) self.labels = np.load(label_path).astype(np.int64) self.augment = augment if self.data.ndim == 2: self.data = self.data[:, np.newaxis, :] # 统计归一化参数 self.mean = self.data.mean() self.std = self.data.std() + 1e-8 def __len__(self): return len(self.data) def __getitem__(self, idx): x = self.data[idx].copy() y = self.labels[idx] # Z-score 归一化 x = (x - self.mean) / self.std if self.augment: x = self._augment(x) return torch.from_numpy(x), torch.tensor(y) @staticmethod def _augment(x): """时间域数据增强 (numpy, shape=(1, L))""" shift = np.random.randint(-8, 9) if shift != 0: x = np.roll(x, shift, axis=-1) x *= (1.0 + np.random.uniform(-0.03, 0.03)) x += np.random.normal(0, 1e-3, x.shape).astype(np.float32) return x # ============================================================ # 训练 / 验证 # ============================================================ def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0.0, 0, 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) correct += (model(x).argmax(1) == y).sum().item() # re-run for stats total += x.size(0) # Recalculate accuracy properly model.eval() correct, total = 0, 0 with torch.no_grad(): for x, y in loader: x, y = x.to(device), y.to(device) logits = model(x) correct += (logits.argmax(1) == y).sum().item() total += x.size(0) return total_loss / total, correct / total @torch.no_grad() def validate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 all_preds, all_labels = [], [] for x, y in loader: x, y = x.to(device), y.to(device) logits = model(x) loss = criterion(logits, y) total_loss += loss.item() * x.size(0) preds = logits.argmax(1) correct += (preds == y).sum().item() total += x.size(0) all_preds.extend(preds.cpu().numpy()) all_labels.extend(y.cpu().numpy()) return total_loss / total, correct / total, all_preds, all_labels # ============================================================ # ONNX 导出 # ============================================================ def export_onnx(model, checkpoint_path, output_path, device): state = torch.load(checkpoint_path, map_location=device) model.load_state_dict(state) model.eval() dummy = torch.randn(1, INPUT_CH, SEQ_LEN).to(device) torch.onnx.export( model, dummy, output_path, input_names=['waveform'], output_names=['logits'], dynamic_axes=None, opset_version=12, do_constant_folding=True, export_params=True, verbose=False, ) # 验证 ONNX try: import onnx onnx_model = onnx.load(output_path) onnx.checker.check_model(onnx_model) op_types = [n.op_type for n in onnx_model.graph.node] print(f"\nONNX 算子统计: {dict(Counter(op_types))}") print("ONNX 模型验证通过 ✓") except ImportError: print("WARNING: 未安装 onnx 库,跳过 ONNX 验证") print(f"ONNX 模型已导出: {output_path}") # ============================================================ # Main # ============================================================ def main(): parser = argparse.ArgumentParser(description='电力波形故障识别 1D-CNN') parser.add_argument('--data_dir', default='data', help='数据目录') parser.add_argument('--batch_size', type=int, default=64) parser.add_argument('--epochs', type=int, default=100) parser.add_argument('--lr', type=float, default=0.001) parser.add_argument('--device', default='auto') args = parser.parse_args() # 设备 if args.device == 'auto': device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') else: device = torch.device(args.device) print(f"设备: {device}") # 数据 train_ds = WaveformDataset( f'{args.data_dir}/train_data.npy', f'{args.data_dir}/train_labels.npy', augment=True) val_ds = WaveformDataset( f'{args.data_dir}/val_data.npy', f'{args.data_dir}/val_labels.npy', augment=False) train_loader = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=args.batch_size, shuffle=False, num_workers=2) print(f"训练集: {len(train_ds):,} 验证集: {len(val_ds):,}") print(f"归一化: mean={train_ds.mean:.3f}, std={train_ds.std:.3f}") # 数据不平衡检测 label_counts = Counter(train_ds.labels) print(f"训练集类别分布: {dict(label_counts)}") # 模型 model = Fault1DCNN().to(device) n_params = sum(p.numel() for p in model.parameters()) print(f"模型参数量: {n_params:,} (~{n_params*4/1024:.0f} KB FP32)") # 训练 optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=1e-4) scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='max', factor=0.5, patience=10, verbose=True) criterion = nn.CrossEntropyLoss() os.makedirs('checkpoints', exist_ok=True) best_acc = 0.0 for epoch in range(1, args.epochs + 1): train_loss, train_acc = train_one_epoch( model, train_loader, criterion, optimizer, device) val_loss, val_acc, preds, labels = validate( model, val_loader, criterion, device) scheduler.step(val_acc) if epoch % 10 == 0 or epoch == 1: print(f"Epoch {epoch:3d} | " f"Train: loss={train_loss:.4f} acc={train_acc:.2%} | " f"Val: loss={val_loss:.4f} acc={val_acc:.2%}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'checkpoints/best_model.pth') print(f"\n最佳验证准确率: {best_acc:.2%}") # 最终分类报告 print("\n" + "=" * 55) print(classification_report(labels, preds, target_names=FAULT_NAMES)) print("=" * 55) # 导出 ONNX export_onnx(model, 'checkpoints/best_model.pth', 'checkpoints/fault_1dcnn.onnx', device) if __name__ == '__main__': main()