linux_learn/learn/NPU/code/train_1dcnn.py

318 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()