恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
GASF-CNN一维信号分类实战:从编码到GUI部署
首页
资讯中心
/
GASF-CNN一维信号分类实战:从编码到GUI部署
GASF-CNN一维信号分类实战:从编码到GUI部署
发布时间:2026/9/30 18:26:51
简介这份资源面向具备一定Python编程基础的科研人员、数据科学家与工程师聚焦时序数据分类预测这一典型难题给出基于GASF-CNN的完整项目实例。它通过格拉姆角场算法将一维时序信号转换为二维图像再交由卷积神经网络提取特征从而降低对人工特征工程的依赖提升分类准确率与模型泛化能力并兼顾可解释性输出。压缩包共1个docx文件约73KB内容涵盖项目背景、目标与意义、七项挑战及对应解决方案、六项创新点以及金融市场预测、医疗数据分析、气象与环境监测等应用领域并附完整程序、GUI设计与代码详解。目录结构按背景、目标、挑战、创新、应用逐层展开便于按模块查阅与动手复现。目前已有66人学习适合希望系统掌握GASF与CNN结合方法、并需要可运行代码与界面参考的读者。1. 从一维振动信号到二维图像GASF-CNN 到底在解决什么分类难题手里只有一列传感器采样点却想让卷积神经网络像看图一样把它认出来这是很多做设备故障诊断、生理信号识别、工况分类的工程师都会撞上的墙。一维时序直接喂 CNN 不是不行但卷积核在单行数据上滑动感受野受限纹理特征几乎提不出来模型很容易退化成多层感知机。格拉姆角场Gramian Angular Summation FieldGASF给出的思路是先把一维序列归一化到 [-1,1]再把它映射成极坐标用两两采样点夹角的余弦和构造出一张对称的二维矩阵于是时间序列的时序相关性被编码成了图像里的纹理。GASF-CNN 就是拿这张图当输入用卷积神经网络做分类预测。它适合谁适合手头有中等规模一维信号、标签干净、想用现成 CNN 结构快速拿到比 SVM、随机森林更高准确率的从业者。这篇笔记把 GASF 的数学、CNN 的搭建、GUI 封装和踩坑点一次讲透代码可以直接抄。2. GASF 编码原理与 CNN 选型为什么不是直接上 1D-CNN2.1 格拉姆角场的数学推导与坐标映射GASF 的核心动作只有三步但每一步的参数都会影响最终图像的可分性。第一步是归一化把原始序列 $x {x_1, x_2, ..., x_n}$ 缩放到 $[-1, 1]$常见做法是 min-max$$\tilde{x}_i \frac{x_i - \min(x)}{\max(x) - \min(x)} \times 2 - 1$$第二步是极坐标编码把归一化后的值当作余弦时间戳当作角度$$\phi_i \arccos(\tilde{x}_i), \quad r_i \frac{t_i}{N}$$其中 $t_i$ 是时间步$N$ 是归一化常数。第三步用角度和构造 GASF 矩阵$$GASF_{ij} \cos(\phi_i \phi_j) \tilde{x}_i \cdot \tilde{x}_j - \sqrt{1-\tilde{x}_i^2} \cdot \sqrt{1-\tilde{x}_j^2}$$这个式子展开后就是两个采样点的内积减去它们正弦分量的乘积。矩阵对角线是 $\cos(2\phi_i)$保留了每个点自身的幅值信息非对角线元素反映的是两个时间点之间的相关性。序列越长矩阵越大一张 $N \times N$ 的图就诞生了。为什么用 GASF 而不是 GADFGASF 对幅值变化更敏感适合区分能量差异明显的工况GADF 用角度差对相位偏移更敏感。做故障分类时冲击成分的幅值差异往往是关键所以 GASF 更常用。2.2 为什么把一维信号转成图像再喂 CNN直接上 1D-CNN 的问题在于一维卷积核只能沿时间轴滑动提取的是局部波形片段对长程依赖和全局形态的建模能力弱。转成 GASF 图像后原本相隔很远的时间点变成了图像上不同位置的像素二维卷积核可以同时捕捉局部纹理和全局结构。更关键的是GASF 矩阵的对称性和纹理模式与原始信号的周期性、突变点直接对应CNN 在 ImageNet 上预训练出来的边缘、角点检测能力可以迁移过来。常见做法是序列长度控制在 64 到 256 之间太短则矩阵信息量不足太长则图像尺寸爆炸训练显存吃不消。我一般会把原始信号先做滑动窗口切分每个窗口 128 个点重叠率 50%这样既扩充了样本又保证了 GASF 图像尺寸可控。2.3 CNN 主干网络的结构选择与参数设定GASF 图像是单通道灰度图尺寸通常是 $128 \times 128$ 或 $64 \times 64$。主干网络不需要太深四层卷积加两层全连接足够。第一层卷积核 $3 \times 3$通道数 32后面每层翻倍到 64、128、256每层后接 BatchNorm 和 ReLU再跟一个 $2 \times 2$ 最大池化。全连接层前用全局平均池化替代 Flatten能显著减少参数量降低过拟合风险。学习率初始设 1e-3用 Adam 优化器配合余弦退火调度。Batch size 根据显存来128 的图用 32 或 64 都行。损失函数用交叉熵如果类别不平衡就加类别权重。下面这段代码是 GASF 编码和 CNN 模型定义的完整实现import numpy as np import torch import torch.nn as nn import torch.nn.functional as F def gasf_encode(series, image_size128): 将一维序列编码为 GASF 图像 series: 一维 numpy 数组长度需 image_size image_size: 输出图像边长 # 截断或重采样到 image_size 长度 if len(series) image_size: idx np.linspace(0, len(series) - 1, image_size).astype(int) series series[idx] elif len(series) image_size: series np.interp( np.linspace(0, len(series) - 1, image_size), np.arange(len(series)), series ) # min-max 归一化到 [-1, 1] s_min, s_max series.min(), series.max() if s_max - s_min 1e-8: norm np.zeros_like(series) else: norm 2 * (series - s_min) / (s_max - s_min) - 1 norm np.clip(norm, -1, 1) # 极坐标角度 phi np.arccos(norm) # 构造 GASF 矩阵 cos_sum np.cos(phi[:, None] phi[None, :]) return cos_sum.astype(np.float32) class GASFCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.conv1 nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2) ) self.conv2 nn.Sequential( nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2) ) self.conv3 nn.Sequential( nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2) ) self.conv4 nn.Sequential( nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.fc nn.Linear(256, num_classes) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x self.conv4(x) x x.view(x.size(0), -1) return self.fc(x)gasf_encode里做了长度对齐用线性插值把任意长度序列拉到image_size这样不同采样率的信号都能统一处理。归一化时加了极小值保护防止常数序列导致除零。np.arccos输入必须严格在 [-1,1]所以前面 clip 了一次。CNN 部分用AdaptiveAvgPool2d(1)把特征图压成 1×1再接全连接参数量比 Flatten 少一个数量级。num_classes根据你的标签数改比如轴承故障四分类就设 4。3. 从原始信号到训练集数据预处理与 GASF 图像生成流水线3.1 滑动窗口切分与样本增强原始信号往往是一条长序列直接整段做 GASF 会得到一张巨大的图而且样本量只有一个。标准做法是滑动窗口切分。窗口长度取 128步长取 64重叠 50%。这样一条 10000 点的信号能切出约 150 个样本。如果样本还是不够可以加高斯噪声、幅值缩放、时间偏移做增强。注意增强要在 GASF 编码之前做因为编码后的图像做几何变换会破坏格拉姆矩阵的数学结构。下面这段代码把切分和编码串起来def make_dataset(signal, label, window128, step64): 滑动窗口切分并生成 GASF 图像数据集 返回: images (N, 1, H, W), labels (N,) images, labels [], [] for start in range(0, len(signal) - window 1, step): seg signal[start:start window] img gasf_encode(seg, image_sizewindow) images.append(img) labels.append(label) images np.stack(images)[:, None, :, :] # 加通道维 labels np.array(labels, dtypenp.int64) return images, labelswindow和step是核心参数。窗口太短GASF 矩阵分辨率低纹理模糊窗口太长单样本计算量和显存占用都上去了。我一般先用 128 试如果准确率不够再试 256。step控制样本重叠度步长越小样本越多但相邻样本高度相关验证集划分时要按时间顺序切不能随机打乱否则数据泄漏会让验证准确率虚高。3.2 训练集、验证集、测试集的正确划分方式时序数据的划分是个容易翻车的地方。很多人习惯用train_test_split随机划分结果同一段信号切出来的窗口被分到训练集和验证集模型相当于在验证集上见过训练数据准确率虚高十几个点。正确做法是按时间顺序切前 70% 做训练中间 15% 做验证最后 15% 做测试。如果有多条独立信号可以按信号文件划分同一文件的窗口只进一个集合。下面是一个按比例切分的示例def split_by_time(images, labels, ratios(0.7, 0.15, 0.15)): n len(images) n_train int(n * ratios[0]) n_val int(n * ratios[1]) train (images[:n_train], labels[:n_train]) val (images[n_train:n_train n_val], labels[n_train:n_train n_val]) test (images[n_train n_val:], labels[n_train n_val:]) return train, val, test这个函数假设images已经按时间顺序排列。如果你的数据来自多个文件先把每个文件切好的窗口按文件顺序拼接再调用这个函数。验证集的作用是调超参和早停测试集只在最后跑一次不要反复用测试集调参。3.3 用 PyTorch Dataset 封装与 DataLoader 配置数据量大了以后把图像全放内存不现实。用Dataset封装__getitem__里做 GASF 编码DataLoader开多进程加载。这样内存占用低还能在线做增强。下面是一个完整的 Dataset 实现from torch.utils.data import Dataset, DataLoader class GASFDataset(Dataset): def __init__(self, signals, labels, window128, step64, transformNone): self.samples [] self.transform transform for sig, lab in zip(signals, labels): for start in range(0, len(sig) - window 1, step): self.samples.append((sig[start:start window], lab)) def __len__(self): return len(self.samples) def __getitem__(self, idx): seg, lab self.samples[idx] img gasf_encode(seg, image_sizelen(seg)) if self.transform: img self.transform(img) return torch.from_numpy(img).unsqueeze(0), lab train_loader DataLoader( GASFDataset(train_signals, train_labels), batch_size64, shuffleTrue, num_workers4, pin_memoryTrue )num_workers设成 CPU 核数的一半左右太多反而因为进程切换拖慢速度。pin_memoryTrue在 GPU 训练时能加速数据传输。shuffleTrue只在训练集用验证集和测试集必须shuffleFalse保证评估结果可复现。4. 训练、评估与 GUI 封装把模型变成能用的工具4.1 训练循环与关键超参数配置训练循环本身不复杂但有几个参数直接决定成败。学习率用 1e-3 起步配合CosineAnnealingLR每 10 个 epoch 降一次。早停 patience 设 15验证损失连续 15 轮不降就停。梯度裁剪阈值设 1.0防止 GASF 图像里少数极端值导致梯度爆炸。下面是一个完整的训练函数def train_model(model, train_loader, val_loader, epochs100, lr1e-3, patience15): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) criterion nn.CrossEntropyLoss() best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() # 验证 model.eval() val_loss, correct, total 0, 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) val_loss criterion(out, labels).item() * imgs.size(0) correct (out.argmax(1) labels).sum().item() total imgs.size(0) val_loss / total acc correct / total print(fEpoch {epoch1}: val_loss{val_loss:.4f}, val_acc{acc:.4f}) if val_loss best_val_loss: best_val_loss val_loss wait 0 torch.save(model.state_dict(), best_gasf_cnn.pth) else: wait 1 if wait patience: print(Early stopping) break return modelweight_decay1e-4是 L2 正则防止过拟合。clip_grad_norm_放在backward之后、step之前。保存模型时只存state_dict加载时先实例化模型再load_state_dict。验证准确率比训练准确率低 5 个点以内算正常差太多就是过拟合了。4.2 混淆矩阵与分类报告评估不能只看准确率准确率在类别不平衡时会骗人。比如九成样本是正常工况模型全预测正常也能拿 90% 准确率。必须看混淆矩阵和每类的 precision、recall、F1。下面这段代码用 sklearn 输出完整报告from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt def evaluate(model, test_loader, class_names): device torch.device(cuda if torch.cuda.is_available() else cpu) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names)) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclass_names, yticklabelsclass_names) plt.ylabel(True); plt.xlabel(Predicted) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight)classification_report里的macro avg和weighted avg要一起看。macro avg对每类一视同仁weighted avg按样本数加权。如果两者差距大说明小类别表现差。混淆矩阵能看出哪两类容易混比如轴承内圈故障和外圈故障在 GASF 图上纹理接近可能需要加注意力机制或者换 GADF 试试。4.3 用 Tkinter 搭一个能选文件、能预测的 GUI模型训好了总不能每次都让人跑命令行。用 Tkinter 搭个简单界面选信号文件、选模型权重、点预测、显示结果。下面是一个最小可用的 GUIimport tkinter as tk from tkinter import filedialog, messagebox import numpy as np import torch class GASFApp: def __init__(self, root, model, class_names): self.root root self.model model self.class_names class_names root.title(GASF-CNN 分类预测) tk.Button(root, text选择信号文件, commandself.load_file).pack(pady10) self.label tk.Label(root, text未选择文件, wraplength400) self.label.pack(pady5) tk.Button(root, text预测, commandself.predict).pack(pady10) self.result tk.Label(root, text, font(Arial, 14)) self.result.pack(pady10) self.signal None def load_file(self): path filedialog.askopenfilename(filetypes[(Numpy, *.npy), (CSV, *.csv)]) if not path: return if path.endswith(.npy): self.signal np.load(path).flatten() else: self.signal np.loadtxt(path, delimiter,).flatten() self.label.config(textf已加载: {path}, 长度 {len(self.signal)}) def predict(self): if self.signal is None: messagebox.showwarning(提示, 请先选择信号文件) return img gasf_encode(self.signal, image_size128) tensor torch.from_numpy(img).unsqueeze(0).unsqueeze(0) self.model.eval() with torch.no_grad(): out self.model(tensor) prob torch.softmax(out, dim1) pred out.argmax(1).item() self.result.config( textf预测类别: {self.class_names[pred]}, 置信度: {prob[0][pred]:.4f} ) if __name__ __main__: model GASFCNN(num_classes4) model.load_state_dict(torch.load(best_gasf_cnn.pth, map_locationcpu)) root tk.Tk() app GASFApp(root, model, [正常, 内圈故障, 外圈故障, 滚动体故障]) root.mainloop()gasf_encode里对输入长度做了插值所以 GUI 里选任意长度的信号都能跑。map_locationcpu保证没有 GPU 的机器也能加载。预测时加softmax拿置信度低于 0.6 的结果建议人工复核。这个 GUI 够用想做得好看点可以换 PyQt但核心逻辑一样。5. 避坑与排查GASF-CNN 落地时最容易翻车的五个地方5.1 现象训练准确率 99%测试准确率 60%原因几乎都是数据泄漏。滑动窗口切分后相邻窗口重叠 50%如果随机划分数据集训练集和测试集里会有大量高度相似的样本。模型记住了这些样本测试时遇到没见过的就露馅。解决办法是按时间顺序切分或者按信号文件切分确保同一段信号的所有窗口只出现在一个集合里。验证集和测试集也要分开不能拿测试集调参。5.2 现象GASF 图像全灰看不出纹理原因是归一化前信号幅值范围太小或者序列里有直流分量。min-max 归一化对异常值敏感一个尖峰就能把其他点压到接近零。解决办法是先做去均值再用鲁棒归一化比如用中位数和四分位距代替最小最大值。另外如果信号本身是常数或者接近常数GASF 矩阵会退化成全 1 或全 -1这种样本要提前剔除。5.3 现象训练 loss 震荡剧烈不收敛常见原因是学习率太大或者 batch size 太小。GASF 图像像素值在 [-1,1]和常规图像 [0,255] 不同如果直接套用 ImageNet 的训练配置学习率 0.1 起步梯度会爆炸。解决办法是把学习率降到 1e-3 甚至 1e-4加梯度裁剪batch size 至少 32。另外检查一下输入有没有 NaNnp.arccos输入超出 [-1,1] 会产生 NaN前面 clip 那一步不能省。5.4 现象GPU 显存不够报 CUDA out of memoryGASF 图像尺寸是 $N \times N$序列长度 256 就是 256×256 的图一个 batch 64 张就是 64×1×256×256显存占用不小。解决办法有三个一是减小窗口长度到 128 或 64二是减小 batch size三是用混合精度训练torch.cuda.amp能省一半显存。如果还不行把模型通道数减半或者用梯度累积模拟大 batch。5.5 现象GUI 打包成 exe 后闪退用 PyInstaller 打包 Tkinter 程序时如果模型权重文件没一起打包或者路径写的是相对路径exe 换台机器就找不到文件。解决办法是用sys._MEIPASS获取临时解压目录把权重文件用--add-data参数打包进去。另外PyTorch 打包后体积很大可以用--exclude-module排除不用的库或者改用 ONNX Runtime 推理体积能小很多。6. 进阶技巧用 Grad-CAM 看 CNN 到底学到了什么模型训完准确率不错但心里没底不知道它是真学到了故障特征还是记住了噪声。Grad-CAM 能把最后一层卷积的梯度加权回特征图生成热力图告诉你模型做决策时关注 GASF 图像的哪个区域。下面这段代码实现 Grad-CAMclass GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor, class_idxNone): self.model.eval() output self.model(input_tensor) if class_idx is None: class_idx output.argmax(1).item() self.model.zero_grad() output[0, class_idx].backward() weights self.gradients.mean(dim(2, 3), keepdimTrue) cam (weights * self.activations).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizeinput_tensor.shape[2:], modebilinear, align_cornersFalse) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam, class_idxtarget_layer一般选最后一个卷积层比如model.conv4[0]。register_full_backward_hook在 PyTorch 新版本里替代了register_backward_hook别用错了。生成的热力图叠加到原 GASF 图像上红色区域就是模型关注的地方。如果热力图集中在图像边缘或者对角线以外的区域说明模型可能学到了伪相关需要检查数据预处理有没有问题。我一般会挑几个预测错误的样本看热力图往往能发现标注错误或者信号采集时的干扰。这个技巧帮我省了很多盲目调参的时间希望帮到你。本文还有配套的精品资源点击获取