☰
1D-GAN实战:用Matlab生成一维序列数据,解决样本不足与过拟合问题
2026/10/18 3:51:54 网站建设 项目流程

做信号处理和数据建模的朋友,应该都遇到过这种尴尬:手里的真实一维序列样本就那么几百条,分类模型一上就过拟合,怎么调都是一顿操作猛如虎,验证集上一看原地杵。我以前也试过加噪声、时间平移、插值这些传统增强套路,但对样本分布的理解并没有本质提升。直到系统折腾了1D-GAN,才发现生成对抗网络这条路是真的能走通。

简单说,1D-GAN就是用一维卷积网络搭一个生成器,从随机噪声里直接学出一维序列数据的分布,再把生成样本拿来扩充训练集、补全缺失段、甚至模拟罕见异常模式。这篇文章把它的原理、Matlab实现、以及我踩过的调参坑从头到尾捋一遍,代码骨架可以直接拿去改。

适合读这篇的人,我猜大概是这么三类:做机械故障诊断的研究生、在工业现场处理传感器数据的算法工程师,以及刚接触GAN、想在Matlab里快速跑通一个可解释的生成模型练手的人。那咱们就从最核心的问题开始。

1. 为什么要用1D-GAN做数据生成

1.1 一维序列数据生成的痛点

先明确一下“一维序列数据”指什么。这里说的不是文本或语音,而是时间轴上连续采样的数值序列:工业振动信号、心电信号、传感器温度曲线、设备运行状态参数,或者某一类实验曲线。这类数据在很多场景下有一个共同点——高质量、有标签的样本极少。原因很现实:故障不是天天有,昂贵设备不能随便跑,采集一次实验要搭台子、要花时间,数据量上不去是常态。

传统数据增强大概分两类:一类是加扰动,比如加噪声、平移、缩放、时间拉伸;另一类是把真实样本切段、重组、拼接。这些方法都建立在“局部变换不改变语义”的假设上,但这个假设很多时候不成立。比如一倍的时间拉伸可能还是同一个故障模式,拉过头了就变成另一个工况了。所以传统增强只能缓解过拟合,不能产生真正“没见过”的样本结构。

生成模型的思路完全不同:先估计数据分布P(x),然后从分布里直接采样新样本。听起来像是“编造”,但这些编造样本服从原有数据分布,对下游任务来说就是有效的扩充。GAN在这个场景里很能打,因为它不显式建模密度函数,而是靠生成器和判别器的博弈,绕开了高维分布估计这个老大难问题。

1.2 为什么不是VAE,也不是自回归

圈里聊生成模型,绕不开三派:GAN、VAE、自回归模型。

VAE的思路是学一个隐变量z,再让解码器从z重建x。它目标函数里有一个KL项,会强制隐变量分布趋于标准高斯,这个正则会让生成结果偏向“安全”和“平均”。反映到一维信号上就是波形变糊、细节丢失。做图像可能还行,做振动信号这种对峰值和边沿敏感的数据,糊了就等于废了。

自回归模型,比如WaveNet那种,是逐点预测下一点的条件分布。生成质量确实高,但速度就是硬伤——256个点还好,要是几万点的长序列,那就是一个一个点往外蹦,慢到怀疑人生。而且误差会累积,一个点偏了后面全跟着偏。

GAN处在中间位置:训练完生成器就是一次前向,一个batch几毫秒;生成样本不经过KL平均化,细节保留得明显更好。1D-GAN最大的特点是卷积核只沿着时间轴方向滑动,把“局部波形”这个先验直接塞进网络结构里,参数效率比全连接高很多,也比把序列硬生生当成图片去跑2D卷积要贴合得多。

1.3 典型应用场景

我在实际项目里验证过三条路,都跑得通。

第一是数据增强。把正常样本和少数类样本分别训练一个1D-GAN,然后把生成的少数类样本混入训练集,分类器对少数类的召回率能提不少。做故障诊断的同学应该最有感触:正常样本几百上千条,故障样本可能就二三十条,拿1D-GAN生成之后再训网络,效果往往比单纯调模型结构来得好。

第二是缺失段补全。序列中间有一整段传感器掉线,可以利用生成器学到的“正常模式”作为先验,结合前后端上下文把缺口补上。虽然不能保证补出来的数值绝对精确,但至少不会出现离谱的突变,对后续趋势分析是能用的。

第三是异常样本仿真。某类异常现象极度罕见,但机理上知道它大概长什么样,可以手工构造少量示例让GAN学会这个模式,再扩充生成。相当于用少量先验知识“养”出一个能批量产出的异常样本库。

2. 1D-GAN的核心原理与设计思路

2.1 对抗博弈的本质

GAN的核心思想可以类比成“假钞制造者”和“验钞机”的博弈。生成器就是那个造假钞的,目标是造出能以假乱真的钞票;判别器就是验钞机,目标是识破假钞。两个人不断对抗升级:造假者越来越厉害,验钞机也越来越挑剔。

训练到理想状态时,判别器怎么分辨都无法区分真钞和假钞,输出概率稳定在0.5附近,这时候生成器学到的分布就逼近了真实分布。对于1D-GAN来说,“钞票”就是一条信号序列,生成器输入一个随机噪声向量z,输出一条假序列;判别器输入一条序列,输出一个0到1之间的真伪概率。

这个对抗过程用数学语言写就是min-max博弈:生成器想让判别器的损失最大化,判别器想让自己的损失最小化。训练的时候不是同时凭空优化一个目标,而是交替更新两个网络。这个交替节奏把握不好,训练就会明显颠簸,后面我会专门讲。

2.2 网络结构设计:从噪声到序列的映射

生成器的核心任务是把低维噪声向量逐步映射成一条合理长度的一维序列。直接用一个巨大的全连接层把100维噪声映射成256个点,理论上可行,但参数量浪费严重,而且学不到序列的局部结构。

这里用“转置卷积+上采样”的思路更自然:把噪声经过全连接层投影到一个小尺寸的特征图,再通过几层转置卷积逐步把序列长度翻倍,同时不断调整通道数。整个过程有点像画画:先用几笔勾出轮廓,再一层层补充细节。每层转置卷积都在扩大序列长度,通道数逐渐收敛到最终需要的单通道输出。

判别器则是生成器的镜像。它把一条序列通过几层一维卷积逐步压缩,卷积层在每个局部窗口提取波形特征,中间穿插批量归一化和Leaky ReLU激活,最后接一个全连接层输出标量logit。这里注意:判别器最后一层一般不加sigmoid,直接输出未归一化的logit,在损失函数里用带logits的BCE公式计算,数值稳定性比先过sigmoid再算要好。

用Matlab实现时有个常用小技巧:把一维序列当成“高为1、宽为L、通道为1”的二维特征图,这样可以直接复用convolution2dLayer和transposedConv2dLayer。只要把卷积核设成[1 4]、步长设成[1 2],卷积就是沿宽度方向滑动,效果等价于一维卷积。这个技巧能兼容绝大多数Matlab版本,不用特意去找单独的1D卷积层。

2.3 损失函数为什么这么写

原始GAN的损失函数大家背得很熟,但实际工程里直接照着写,很容易出现数值问题。我推荐用“非饱和生成损失”这种改进写法:

  • 判别器损失:让真实样本的输出尽可能大,让生成样本的输出尽可能小。
  • 生成器损失:让生成样本的输出尽可能大,而不是“让生成样本的输出尽可能接近0”。

这两种写法区别在梯度行为上。原始写法在判别器已经很强时,生成器梯度会消失,训练完全停摆;非饱和写法在同样情况下还能提供有效梯度,训练不容易假死。

如果训练仍然不稳,可以换成LSGAN的最小二乘损失,或者WGAN-GP的梯度惩罚方案。我在Matlab里验证过,标准GAN加BN加Leaky ReLU,配合下面会说到的调参技巧,已经能覆盖大部分一维序列生成需求。LSGAN和WGAN-GP是后手,遇到标准GAN实在训不动再换,不必一开始就上复杂度。

3. Matlab实现:从零搭建一个1D-GAN

3.1 环境准备与模拟数据生成

实现1D-GAN需要Matlab R2021a及以上版本,并安装Deep Learning Toolbox。我建议用GPU训练,没有GPU也能跑,就是慢一些,256点的小序列CPU其实也能忍受。

为了不依赖外部数据文件,我写了一个模拟数据生成器,用来合成“模式不同的周期信号”。它生成的是叠加了不同频率、幅度、相位的正弦信号,再加一点高斯噪声,模拟传感器振动数据的风格。

function data = makeDemoData(numSamples, signalLength) data = zeros(numSamples, signalLength); t = (0:signalLength-1) / signalLength; for i = 1:numSamples f1 = 3 + 2*rand(); % 主频率 f2 = 8 + 3*rand(); % 次频率 amp1 = 0.3 + 0.5*rand(); % 主幅度 amp2 = 0.2*amp1 + 0.1*rand(); phase1 = 2*pi*rand(); phase2 = 2*pi*rand(); data(i,:) = amp1*sin(2*pi*f1*t + phase1) + ... amp2*sin(2*pi*f2*t + phase2) + ... 0.08*randn(1, signalLength); end % 归一化到 [0,1],匹配生成器最后的 sigmoid 输出 data = (data - min(data(:))) / (max(data(:)) - min(data(:))); end

生成500条、每条256点的数据,作为训练集:

rng(42); signalLength = 256; numReal = 500; realData = makeDemoData(numReal, signalLength);

数据归一化这里有个关键细节:生成器最后一层的激活函数要和数据范围匹配。数据归一化到[0,1],就用sigmoid;归一化到[-1,1],就用tanh。两者不匹配,生成器再怎么训也只能在错误的取值范围里打转。

3.2 生成器和判别器网络定义

下面是生成器定义。latentDim是噪声维度,我通常设100。输出长度需要能被8整除,256正好满足,因为三层转置卷积每次把长度翻倍,从32翻到256。

function dlnetGen = createGenerator(latentDim, outputLength) layers = [ featureInputLayer(latentDim, 'Normalization', 'none', 'Name', 'in') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(64 * outputLength / 8, 'Name', 'fc2') reluLayer('Name', 'relu2') % 把全连接输出重塑为 [通道, 高度, 宽度, 样本数] % 这里通道=64,高度=1,宽度=outputLength/8 functionLayer(@(X) reshape(X, 64, 1, outputLength/8, []), 'Name', 'reshape_in') transposedConv2dLayer([1 4], 32, 'Stride', [1 2], 'Cropping', 'same', 'Name', 'tconv1') reluLayer('Name', 'relu3') transposedConv2dLayer([1 4], 16, 'Stride', [1 2], 'Cropping', 'same', 'Name', 'tconv2') reluLayer('Name', 'relu4') transposedConv2dLayer([1 4], 1, 'Stride', [1 2], 'Cropping', 'same', 'Name', 'tconv3') sigmoidLayer('Name', 'output') ]; lgraph = layerGraph(layers); dlnetGen = dlnetwork(lgraph); end

判别器接收[1 1 signalLength]的单通道输入,也就是一条一维序列。三层卷积逐步压缩长度、增加通道,最后输出单个logit。

function dlnetDis = createDiscriminator(inputLength) layers = [ imageInputLayer([1 inputLength 1], 'Normalization', 'none', 'Name', 'in') convolution2dLayer([1 4], 16, 'Stride', [1 2], 'Padding', 'same', 'Name', 'conv1') leakyReluLayer(0.2, 'Name', 'lrelu1') convolution2dLayer([1 4], 32, 'Stride', [1 2], 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn1') leakyReluLayer(0.2, 'Name', 'lrelu2') convolution2dLayer([1 4], 64, 'Stride', [1 2], 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn2') leakyReluLayer(0.2, 'Name', 'lrelu3') fullyConnectedLayer(1, 'Name', 'fc_out') ]; lgraph = layerGraph(layers); dlnetDis = dlnetwork(lgraph); end

生成器第一层用了featureInputLayer,生成器的输入是纯噪声向量;判别器第一层是imageInputLayer,输入是序列形式的特征图。两者输入格式不同,这是由各自定义决定的,不要搞混。

3.3 自定义训练循环

Matlab里训练GAN最稳的方式是自定义训练循环,原因在于GAN需要单独控制两个网络的更新节奏和损失计算,trainNetwork是给标准监督学习用的,不太适合这种交替对抗过程。

模型梯度函数是整个训练循环的核心:

function [lossDis, lossGen, gradDis, gradGen] = modelGradients(dlnetGen, dlnetDis, XReal, Z) % 生成假样本 XFake = forward(dlnetGen, Z); % 判别器对真实和生成样本分别打分 YReal = forward(dlnetDis, XReal); YFake = forward(dlnetDis, XFake); % 判别器损失:让真样本得分高,假样本得分低 lossDisReal = -mean(log(1 ./ (1 + exp(-YReal)))); lossDisFake = -mean(log(1 - 1 ./ (1 + exp(-YFake)))); lossDis = lossDisReal + lossDisFake; % 生成器损失:非饱和形式 lossGen = -mean(log(1 ./ (1 + exp(-YFake)))); % 计算梯度 gradDis = dlgradient(lossDis, dlnetDis.Learnables); gradGen = dlgradient(lossGen, dlnetGen.Learnables); end

主训练循环如下。我习惯把真实数据随机打乱后按miniBatchSize切块,每个epoch内交替更新判别器和生成器:

% 超参数设置 latentDim = 100; miniBatchSize = 128; numEpochs = 500; learnRateGen = 2e-4; learnRateDis = 1e-4; gradDecay = 0.5; gradDecaySq = 0.999; % 初始化网络 dlnetGen = createGenerator(latentDim, signalLength); dlnetDis = createDiscriminator(signalLength); % 优化器状态 trailingAvgGen = []; trailingAvgSqGen = []; trailingAvgDis = []; trailingAvgSqDis = []; numItersPerEpoch = floor(numReal / miniBatchSize); for epoch = 1:numEpochs idxOrder = randperm(numReal); for iter = 1:numItersPerEpoch idxBatch = idxOrder((iter-1)*miniBatchSize + 1 : iter*miniBatchSize); XReal = realData(idxBatch, :); XReal = dlarray(reshape(XReal, 1, 1, signalLength, miniBatchSize)); Z = dlarray(randn(latentDim, miniBatchSize)); % 前向计算梯度 [lossDis, lossGen, gradDis, gradGen] = dlfeval( ... @modelGradients, dlnetGen, dlnetDis, XReal, Z); % Adam更新判别器 [dlnetDis, trailingAvgDis, trailingAvgSqDis] = adamupdate( ... dlnetDis, gradDis, trailingAvgDis, trailingAvgSqDis, ... iter + (epoch-1)*numItersPerEpoch, learnRateDis, gradDecay, gradDecaySq); % Adam更新生成器 [dlnetGen, trailingAvgGen, trailingAvgSqGen] = adamupdate( ... dlnetGen, gradGen, trailingAvgGen, trailingAvgSqGen, ... iter + (epoch-1)*numItersPerEpoch, learnRateGen, gradDecay, gradDecaySq); end % 每个epoch结束后打印损失 if mod(epoch, 20) == 0 fprintf('Epoch %d, Dis Loss: %.4f, Gen Loss: %.4f\n', ... epoch, extractdata(lossDis), extractdata(lossGen)); end end

训练结束后,生成新样本就是一次前向计算:

Ztest = dlarray(randn(latentDim, 50)); fakeData = predict(dlnetGen, Ztest); fakeData = extractdata(fakeData); fakeData = reshape(fakeData, signalLength, 50)';

这50条生成序列就是训练出来的“数据资产”,可以直接写入文件,也可以继续做下游评估。

3.4 训练结果评估与可视化

很多人训练完GAN只看loss曲线,这是不够的。loss不降不等于没学好,loss在降也不代表波形正常。我自己的习惯是“三看”:

第一,看波形。挑几条生成序列和几条真实序列画在同一张图里,对比局部形状、峰值、周期性。如果生成波形肉眼可见地和真实波形不是一个路子,那就是结构没学会。

第二,看统计量。一维信号可以算均值、均方根、峰值、峰值因子、频谱主频等若干统计特征,把真实样本和生成样本的特征分布画直方图,两条分布应该大致重合。这个是便宜有效的定性验证。

第三,看下游任务。把生成样本和真实样本混合,重新训练一个一维CNN分类器,再用纯真实样本做测试集。如果混入生成样本后分类器在真实测试集上的性能没有下降,说明生成样本在语义层面是有效的。这个方法比单纯看MMD值更贴近实际需求。

4. 训练稳定性的关键细节与调参经验

4.1 模式崩溃:症状、原因与对策

GAN最出名的毛病就是模式崩溃。表现很典型:训练到某个阶段,生成器开始“偷懒”,翻来覆去只生成同一种或少数几种波形。看起来生成器的loss在正常下降,判别器也拿它没办法,但生成样本的多样性很差。

我常用的识别方法:训练过程中定期生成一批样本,两两计算距离。如果所有生成样本之间的平均距离越来越小,那大概率就是在崩溃边缘。更直观的做法是把生成样本和真实样本一起做t-SNE降维,如果生成样本只缩在真实样本聚类中的某个角落,说明只学到了部分模式。

原因和对策要对症下药。最常见的情况是判别器太强,把生成器压得毫无梯度可以学习,生成器只能退守到几个“安全区”。对策是给判别器降学习率,或者减小判别器网络容量。另一种情况是噪声维度太低,编码空间不够,生成器没法表达足够多样性的样本,对策是加大latentDim到128或更高。第三种情况是训练batch太小,判别器梯度噪声太大,导致训练方向不稳定,对策是保证miniBatchSize至少64以上。

4.2 判别器与生成器的平衡博弈

训练GAN本质上是“拔河”,两头得势均力敌才好看。如果判别器loss掉到接近0,意味着判别器可以轻松识破所有生成样本,这对生成器非常不利。反过来,如果生成器loss掉得飞快而判别器loss一直下不来,说明判别器太弱,学到的特征不足以指导生成器。

我调平衡的经验有两条主线:第一,让判别器的学习率低于生成器。常见配置是生成器2e-4、判别器1e-4,差距不用太大,但能让生成器有更多机会去追判别器。第二,控制判别器的更新频率。每轮迭代让判别器更新一次、生成器更新一次是比较稳妥的起点。如果判别器太强,就让它每两步才更新一次,或者给判别器加一点Dropout,削弱它的“辨识能力”。

这是一个系统工程,不要指望一个参数搞定。我自己习惯每次都记录两边的loss变化,观察20个epoch内两者的相对走势,再决定下一步动哪个旋钮。光盯着“最终生成质量”而忽略“训练过程曲线”,排查起来会很被动。

4.3 生成样本质量的评估指标

定性看波形,定量也有几个指标可以用。MMD是常用的分布距离指标,用高斯核函数映射后比较真实样本和生成样本的分布差异,数值越小说明两个分布越接近。Matlab里写一个简单的高斯核MMD并不难,关键是先对样本提取统计特征再算距离,直接拿原始序列算MMD对长度和尺度太敏感。

FID在图像生成领域是标配,但直接套用到一维序列上并不理想,因为FID依赖的Inception特征是为图像设计的,对信号语义不敏感。如果确实需要类似指标,可以在一维CNN分类器的倒数第二层提取特征,再按FID公式计算,但这样做的复杂度等同自己搭一个评估网络,性价比不高。

所以我最终的评估建议很简单:特征分布直方图对比加下游任务验证。前者告诉你“分布像不像”,后者告诉你“有用没用”。两个都通过,这个1D-GAN就算达标了。

5. 常见问题排查与避坑实录

5.1 常见问题速查表

这里整理一份问题速查表,方便卡住的时候对照排查:

症状可能原因解决思路
生成器loss一直不降判别器太强降低判别器学习率;减小判别器层数;给判别器加Dropout
生成样本几乎一模一样模式崩溃加大噪声维度;增加batch;调整判别器更新频率
生成波形有明显锯齿抖动转置卷积棋盘效应把卷积核尺寸改成4的倍数;改用上采样+普通卷积
loss在震荡但波形越来越差学习率过高把两个网络学习率同时调小到1e-4或5e-5
生成序列全是常数激活函数和数据范围不匹配确认sigmoid输出对应[0,1]数据,tanh对应[-1,1]
训练很慢CPU计算、模型过大改用GPU;减小通道数;缩短序列长度
判别器loss很快降到0真实样本和生成样本差异过大先确认前置预处理一致;检查生成器最后一层输出范围

5.2 我踩过的几个坑

第一个坑是转置卷积的棋盘伪影。生成序列肉眼看还行,但放大后发现每隔几个点就有周期性的小抖动,像梳子齿一样。原因是转置卷积在步长为2时,叠加位置重叠不均造成的。我最后的解决办法是把卷积核尺寸调成4的倍数,配合Cropping, same,伪影基本消失。网上也有人说用“上采样+普通卷积”替代转置卷积能根治,我后来试过,确实稳定,只是代码麻烦一点,普通任务不必折腾。

第二个坑是数据预处理不统一。我一开始真实数据归一化到[-1,1],但生成器最后一层忘了从sigmoid换成tanh,导致生成样本全部落在[0,1]区间,和真实数据天然差了一整块。这种错误不仔细看统计量是发现不了的,因为loss看起来完全正常。从那以后我每次搭建网络前,先把真实数据和生成器输出层的激活范围写在同一张便签上贴屏幕边。

第三个坑是过度依赖loss曲线。有一次我看判别器loss稳定在0.69附近,以为是正常状态,结果生成样本是一坨乱码。后来才明白,0.69这个数是log(2),说明判别器正在“完全随机猜测”。出现这种情况可能是两个网络都太弱,或者生成器和判别器处于一个“失败的纳什均衡”,这时调任何参数都很难奏效,直接提高模型容量反而有效。

第四个坑是太大意batch大小。训练初期因为batch设置得太小,生成器每次更新看到的真实样本太少,梯度噪声巨大,loss曲线抖得像心电图,但当时没意识到。后来把batch从32提到128,训练稳定性立刻上了一个台阶。别小看这个设定,GAN对梯度质量的敏感程度比普通CNN高一个数量级。

5.3 几个实用建议

给想快速上手1D-GAN的同学几条建议。

第一,网络结构不要一上来就设计得很深。生成器三到四层转置卷积、判别器三到四层卷积,足以应付大多数256~1024点的一维序列。过深的网络在数据量不足时只会让训练更加失控。

第二,超参数优先调学习率,其次调batch,最后才动网络层数。我做过的实验中,学习率对训练稳定性的影响最大,也是最容易“力挽狂澜”的旋钮。

第三,训练过程中每隔固定epoch就保存一次生成器参数,至少保留到训练结束。GAN训练可能在中后期突然崩溃,如果只在最后保存一份,崩溃后发现之前好的版本已经没了,就只能从头再训。别问我怎么知道的。

第四,有条件的话,尽量用真实业务数据跑通整个流程,哪怕样本只有两三百条。模拟数据能验证代码逻辑,但真实数据里的噪声模式、工况变化,会让1D-GAN的价值体现得更充分。

我个人最后的体会是:1D-GAN不是那种“跑起来放着不管就能用”的模型,它更像个需要持续沟通的合作对象。你给它合理的结构、稳定的训练节奏、有针对性的调参,它就能还给你一批像模像样的序列样本;你指望一把梭跑完就交付,它大概率会给你一坨莫名其妙的波形,然后在某一天突然霸占你脑子里所有调参的空间。

我现在的做法是,拿到任何一维序列数据增强任务,都先跑一个1D-GAN作为基线,同时跑传统增强方法做对比。如果GAN的生成样本在下游任务里能追平甚至超过传统增强,再继续深入优化;如果实在训不动,也不硬磕,先回去把数据预处理和样本模式分析做扎实。数据生成这件事,模型解决的是“分布怎么学”的问题,但“分布到底是什么”的问题,往往藏在最容易被忽略的细节里。

希望这篇实战记录能帮你省下几个星期的摸索时间。训练稳定、钱包不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询