ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于GMM的数据生成:用Matlab实现小样本扩充与类别不平衡处理

基于GMM的数据生成:用Matlab实现小样本扩充与类别不平衡处理 前阵子接手一个数据分析的活儿训练数据只有几百条样本分类模型怎么调都欠拟合。同事说要不“造”点数据出来我第一时间想到的就是上生成模型。对比了一圈GAN训练太娇气VAE调起来也麻烦倒是高斯混合模型GMM又稳又直观把数据当作若干高斯分布拼出来的混合体拟合完参数后直接抽样新样本立刻到手。这篇文章就把我基于GMM做数据生成的完整思路、Matlab实现细节、分量数怎么定、质量怎么验证、以及踩过的坑一次说清楚给做小样本扩充、类别不平衡处理、合成数据生成的朋友一个可以直接复现的参考。1. 为什么需要“造数据”从一次真实的小样本困境说起先说需求来源。无论是做故障诊断还是用户行为分析干净且充足的标注数据永远是稀缺资源。我手头的场景更典型设备采集到的正常样本很多故障样本只有几十条。直接用原始数据训练模型能记住这几条样本但遇到形态稍有变化的故障就识别不出来。这种时候人为构造合理的“伪样本”就成了一条必走的路。1.1 最常用的几条补数据路子数据增强的思路很多各有限制。我按自己的经验排了一下对原始样本加高斯噪声、平移、缩放操作简单但本质上是在已有样本附近“打转”产生的新样本和原样本高度相关多样性非常有限。SMOTE及其变体在少数类样本之间插值合成新样本常用于类别不平衡。问题是它默认样本间连线上都是合法区域遇到非线性分布或重叠严重的类别很容易产生位于错误类别的“假样本”。生成对抗网络GAN能学习复杂分布生成效果上限高但需要足够的真实样本供判别器训练外加超参数多、训练不稳定在小样本场景里经常训不出来或者模式崩塌只生成同一类图像。变分自编码器VAE相对稳但生成的样本偏模糊对数值型特征的多模态结构把握一般。基于统计模型的生成GMM、朴素贝叶斯、马尔可夫链等。GMM在其中的定位很有意思它假设整个数据集是K个高斯分布的加权和只要K取得合适它就能把多峰、偏态、重叠的复杂分布逼近得很好。而且它是有解析形式的生成模型拟合完参数后想生成多少条就生成多少条速度极快完全不依赖GPU。1.2 为什么最终选了GMM我选GMM有几个很现实的原因。第一小样本场景能吃下。GMM的EM算法在样本量只有几百、维度不夸张的情况下几十毫秒之内就能收敛不像深度学习模型要先搭网络再调batch size。第二可解释性强。每个分量都有直观的均值、协方差、权重你能直接看到“数据大致集中在哪几个区域、每个区域占多少比例”。这一点对向业务方解释“新样本是怎么来的”特别有用。第三Matlab工具箱支持得太完整了。fitgmdist负责拟合random负责抽样还有BIC、AIC、Gaussian mixture model的cluster功能一套流程下来代码不超过30行。对于不打算在生产环境上PyTorch的人来说Matlab这条路是最短的。第四GMM本质上也是聚类算法。它和K-means这种“硬聚类”不同给出的是每个样本属于各分量的后验概率保留了不确定性信息。这种概率化的聚类视角顺便还能做异常检测一举两得。2. 高斯混合模型的核心逻辑用若干个高斯去逼近任意分布既然要生成数据就得先弄明白模型凭什么能生成。GMM的大前提是任何连续分布都可以用足够多的高斯分布去逼近。这就好比你用一堆长短不一的积木块去拼一条曲线块越多、每块的形状调得越准拼出来的轮廓就越接近原图。2.1 多峰数据是逼着我们去扩展单高斯模型的直接原因单个高斯分布只有一个均值和一个协方差矩阵它的概率密度是单峰的呈椭圆形往四周平滑衰减。可我面对的数据几乎都是多峰的不同工况下的正常数据分布在不同区域故障数据又聚在另外几个角落整体PDF就是好几个山头。用一个单高斯去拟合这种多峰数据结果就是均值落在几个山头之间的山谷里方差被拉得巨大生成的样本会大量出现在根本没有真实数据的“真空地带”。这就像让一个只会画椭圆的人去画一片群岛他只能画出一个把所有岛屿都圈在里面的大椭圆完全丢失了群岛内部的空白区域和局部聚集结构。GMM的思路很直接一个椭圆画不出来就画K个椭圆每个椭圆负责一片区域最后把它们的密度按权重叠加起来。数学形式就是大家常见的加权高斯密度求和p(x)Σ_{k1}^{K} π_k N(x|μ_k, Σ_k)其中π_k是每个分量的混合权重满足所有权重非负且和为1。2.2 从隐变量视角理解GMM的生成过程要理解数据生成最清楚的方式是引入一个隐变量z。设z是一个取值为1到K的离散随机变量取值k的概率就是π_k。GMM的生成过程可以描述成两步第一步扔一个加权骰子掷出k第二步根据第k个高斯N(μ_k, Σ_k)产生一个样本。观测数据的分布去掉隐变量后就是所有分量按权重加权的结果。这个视角对后面讲随机抽样非常重要。因为当你从拟合好的GMM模型里random生成数据时并不是直接算一个高维联合分布然后逆采样而是按刚才说的两步走先按权重选中一个分量再从该分量的高斯分布里采样。每一步都有成熟的随机数生成算法效率极高。EM算法期望最大化就是在这个隐变量视角下工作的E步根据当前参数计算每个样本属于各分量的后验概率responsibilityM步利用这些后验概率重新估计各分量的均值、协方差和权重。两个步骤交替迭代直到对数似然的变化小于阈值。这么做的直觉是我不知道每个样本到底来自哪个高斯但我可以先猜一个归属概率然后在归属概率下把每个高斯的分量和参数更新到最优再重新算归属。反复迭代让整体对数似然单调上升。2.3 GMM和K-means、单一高斯的本质区别很多朋友一开始容易把GMM理解成“K-means加上椭圆协方差”这个说法不算错但不完整。K-means只把每个点硬划到一个簇然后用簇内均值作为聚类中心它对簇的形状几乎不做假设等价于默认簇是圆形且所有簇的方差相同实际效果对离群点非常敏感。GMM引入了概率归属一个点不需要单独属于某一个分量而是以不同概率属于多个分量。每个分量有自己的协方差矩阵能表达椭圆形的、旋转的、宽窄不一的各种簇形状。也就是说GMM在做了“聚类”的同时还保留了每个簇内部的统计结构这个统计结构正是后续生成数据时的依据。再对比一下单一高斯GMM可以看成多个单高斯的分区组合。单高斯只能给一个全局均值和全局协方差而GMM允许数据在不同区域拥有完全不同的局部统计特性。实际上当我们把K设成1时GMM就退化成普通的多元高斯拟合。这些原理看着偏理论但确定模型结构、选择协方差类型、判断生成样本质量时都得用到。比如你发现生成的样本在两簇交界处异常密集那很可能是协方差类型设成了共享型导致每个簇都套用到同一个椭圆形状。3. Matlab拟合GMM的关键操作fitgmdist的参数与选型细节到了真正写代码的部分Matlab里和GMM相关的核心函数就三个fitgmdist拟合模型、pdf计算概率密度、random生成新样本。可能再加一个cluster做软聚类。先把环境说清楚我使用的是Matlab R2021a版本Statistics and Machine Learning Toolbox是必需的其他工具箱都不依赖。如果你的Matlab版本比较老R2018b之后都支持这些核心函数只是部分参数的名字略有变化比如旧版本里RegularizationValue可能写作RegParam。3.1 构造一份可复现的两簇模拟数据为了把整个流程演示完整我先用mvnrnd生成一份二簇混合的仿真数据这样我们知道真实的“答案”可以对照GMM能不能还原。这段代码本身也可以当作造数据时的实验平台。% 清空环境并固定随机种子保证实验结果可以复现 clear; clc; rng(42); % 第一簇均值在[0,0]协方差为[1 0.4; 0.4 1]500个样本 n1 500; mu1 [0, 0]; Sigma1 [1, 0.4; 0.4, 1]; X1 mvnrnd(mu1, Sigma1, n1); % 第二簇均值在[4,5]协方差为[1.2 0; 0 1.2]300个样本 n2 300; mu2 [4, 5]; Sigma2 [1.2, 0; 0, 1.2]; X2 mvnrnd(mu2, Sigma2, n2); % 合并成训练数据 X [X1; X2]; % 快速查看数据分布形态 figure; scatter(X(:,1), X(:,2), 8, [0.6 0.6 0.9], filled); title(训练数据分布); xlabel(特征1); ylabel(特征2);这里的rng(42)是我强烈建议每个实验都养成的习惯。GMM拟合对初始值敏感固定随机种子才能保证你调参前后的对比是有效果的不然每次重新跑数据都不同很难判断改动到底是好是坏。3.2 fitgmdist的调用形式与初始化策略拟合模型最简单的写法是k 2; % 分量数量 GMModel fitgmdist(X, k);但实际调用时我几乎不会这么裸写至少会带上正则化和迭代上限因为GMM的EM算法在迭代中很容易碰到协方差矩阵奇异的问题。我的标准写法是k 2; options statset(MaxIter, 500, TolFun, 1e-4); GMModel fitgmdist(X, k, ... CovarianceType, full, ... RegularizationValue, 1e-5, ... Options, options, ... Start, plus);逐个说下为什么这么设CovarianceTypefull代表每个分量都用完整的协方差矩阵能捕捉旋转的椭圆簇diagonal则强制协方差非对角处为0计算更快、参数更少但要求数据特征之间相互独立。一般第一遍探索用full数据维度特别高或者分量数很多时再考虑diagonal作为降维式替代。RegularizationValue是专门防止协方差奇异的。EM在更新协方差时如果某一步某个分量的马氏距离极端协方差矩阵可能变得不可逆直接导致程序报错或生成出离群值。加上一个很小的正则项比如1e-5就相当于给协方差矩阵的对角线加一个很小的正值防止它退化。这个技巧在小样本、高相关特征场景下非常管用。Start参数控制初始化方式。plus是K-means式的初始化先让初始质心之间尽量远离通常比随机初始化更快收敛到较好的局部解。除此之外还有randSample从样本中随机抽点作为初始均值和cluster用K-means包含分类的结果初始化也是最贴近真实分布的做法。我一直用plus在小样本场景下一是快二是分量的初始区分度高不容易出现两个分量初始完全重合后拟合过程陷入退化。3.3 协方差类型与正则化防奇异的两板斧在实数据上跑过的人都有这种经历明明数据看着正常fitgmdist却报出“Ill-conditioned covariance”的警告最后生成的结果里出现极端值。原因通常有三个。第一是某个分量的样本量太少协方差估计不稳定第二是特征之间存在强线性相关导致协方差矩阵近似奇异第三是分量数设得太多个别分量被逼到只服务很少几个样本协方差缩成一个狭长的形状。解决方法就是刚才提到的两板斧一个是在协方差类型上做取舍当数据量大且维数高时比如超过20维用full协方差矩阵的参数数量会爆炸式增长这时候改成diagonal能大大减少待估参数代价是丢失特征间的相关性结构另一个是调大RegularizationValue从1e-6逐步试到1e-2直到拟合过程不再警告。但是注意正则化值过大会让协方差矩阵被“撑圆”即各分量趋向各向同性球形这会降低对椭圆簇细节的刻画精度。4. 生成新数据的核心random采样与“多阶抽样”实现逻辑拟合完GMModel之后生成数据的操作简单得让人怀疑是不是漏了什么步骤n_new 1000; % 想生成多少条样本 X_new random(GMModel, n_new);返回的X_new是一个n_new乘特征维数的矩阵每一行都是在同一个概率分布下独立采样出来的新样本。没有比这更干净的数据生成接口了这也是我推荐Matlab的原因之一。4.1 从GMModel对象到新样本的一行代码random函数会自动提取GMModel里已经学到的权重、均值、协方差然后按照模型定义的概率分布进行随机采样。和predict、cluster这类函数不同random不需要输入原始数据它只依赖模型参数所以即使你把训练数据删掉也没影响新样本完全是从概率模型本身推出来的。如果你生成的是多维数据还可以用一行代码把生成的样本和原始训练数据放在同一个坐标系里对比figure; scatter(X(:,1), X(:,2), 8, [0.6 0.6 0.9], filled); hold on; scatter(X_new(:,1), X_new(:,2), 8, [0.9 0.4 0.4], filled); legend(训练数据, 生成数据); xlabel(特征1); ylabel(特征2);跑下来你会直观地看到生成数据的散布形状和原始数据几乎一致第二簇的稀疏性和第一簇的密集程度都能保持。4.2 采样机制拆解先抽分量再抽高斯random函数背后的抽样逻辑就是我前面提到的两步过程可以手动实现一遍来加深理解% 手动实现GMM采样验证random函数的工作方式 function X_man sampleGMM(GMModel, n) K GMModel.NumComponents; pi_k GMModel.ComponentProportion; mu GMModel.mu; Sigma GMModel.Sigma; % 第1步按权重抽取每个样本对应的分量编号 comp_idx randsample(1:K, n, true, pi_k); % 第2步对每个样本按所属分量的高斯分布采样 d size(mu, 2); X_man zeros(n, d); for i 1:n idx comp_idx(i); X_man(i, :) mvnrnd(mu(idx, :), Sigma(:, :, idx)); end end注意randsample用true表示有放回抽样权重就是π_k。这样写出来的结果和random一模一样。可以写个断言快速验证先用random生成一批再手动生成一批比较两者的经验分布是否一致。理解了这一步你就有了一个随时可以替换成其他语言实现GMM生成器的基础不至于被工具箱锁死。4.3 为什么这种方式优于“原始数据随机噪声”这是我最想强调的一点。很多人所谓的“GMM数据生成”其实是对每条原始样本加一点高斯噪声美其名曰“模拟扰动”。这不是生成模型这只是数据增强的一种粗糙形式。真正的生成应该是从学到的概率分布中独立采样这意味着最后得到的样本和任何一条原始训练样本都不同但它们的统计特征均值、方差、相关性、多模态结构高度一致。打比方说原样本加噪声像是在“临摹”已知的画作你只是改了笔触的粗细而从模型采样则像是理解了画作的风格后重新创作笔触不同但风格一脉相承。在生成对抗样本做数据扩充、做隐私保护下的数据脱敏时这种“风格级”的生成能力才是核心需求。因为当你把生成数据用于模型训练时如果新样本只是原样本加噪声模型学到的还是原来那几条样本的模式泛化能力得不到本质提升。5. 分量数K的取舍BIC之外还要看什么GMM最有争议的参数就是K也就是分量个数。K太小模型表达力不够多峰结构被压平K太大出现过拟合每个分量只服务极少数样本生成数据的方差被低估甚至生成出远离真实区域的极端样本。我一般不指望一次选定K而是先跑一遍模型选择的量化指标再结合实际效果做判断。5.1 用BIC和AIC跑一遍模型选择Matlab的fitgmdist在返回模型对象时直接附带了BIC和AIC两个指标BIC全称贝叶斯信息准则它惩罚参数数量防止你过度加大K。AIC类似但对参数数量的惩罚更轻。下面这段代码一次跑完多个候选K% 候选分量数量 k_list 1:6; BIC_values zeros(size(k_list)); AIC_values zeros(size(k_list)); models cell(size(k_list)); options statset(MaxIter, 800, TolFun, 1e-5); for i 1:length(k_list) gm fitgmdist(X, k_list(i), ... CovarianceType, full, ... RegularizationValue, 1e-5, ... Options, options, ... Start, plus); BIC_values(i) gm.BIC; AIC_values(i) gm.AIC; models{i} gm; end % 画图BIC与AIC随K的变化 figure; plot(k_list, BIC_values, -o, LineWidth, 1.5); hold on; plot(k_list, AIC_values, -s, LineWidth, 1.5); legend(BIC, AIC); xlabel(分量数K); ylabel(信息准则值); grid on;BIC曲线的常见形态是“先急剧下降后缓慢上升”。取最低点对应的K是一种标准做法但我会额外关注“肘部位置”BIC下降幅度变得很小的那个点因为它往往是模型复杂度与拟合优度性价比最高的位置。BIC最低偶尔会选出一个K过大但改善微弱的模型视觉上多出来的一两个分量只是拟合了一些离散点没有实际意义。5.2 从实际效果反推K的合理性信息准则只能告诉你“统计上哪个K好”但它选出的K不一定符合你的业务预期。我会做三个额外的检查第一每个分量的权重是否太极端。如果某个分量的π_k小于0.05说明它只覆盖了极少数样本这个分量很可能只是在拟合离群点而不是一个真正的“群”。第二两两分量之间的马氏距离。如果两个分量的均值十分接近且它们的置信椭圆几乎重合说明增加这个K只是把同一个簇强行拆分没有产生新的模式。第三生成数据在关键业务指标上的表现。比如我们做故障样本生成会从每个K对应模型里生成一批样本用人工规则过滤如果新样本中“物理上不可能出现”的比例随K增大而飙升那就说明过拟合了需要回调。这步虽然主观但对落地很有价值。6. 生成的数据到底行不行质量验证的五个维度数据生成不是把代码跑完就结束必须验证生成样本和原始训练样本的本质一致性否则下游模型用错数据还不如不用。我把验证分成五个维度从快到慢依次推进。6.1 分布层面的快速可视化检验从图上对比是最直接的第一印象。二维数据可以画散点图一维数据可以画重叠直方图高维数据则用前两个主成分做二维嵌入后再看。% 一维特征的重叠直方图对比 figure; histogram(X(:,1), Normalization, pdf, FaceAlpha, 0.5); hold on; histogram(X_new(:,1), Normalization, pdf, FaceAlpha, 0.5); legend(训练数据, 生成数据); xlabel(特征1);正常情况下两条密度曲线应该基本重合训练数据某个位置出现高峰生成数据也该在该位置有高峰训练数据低谷的位置生成数据也不该冒出新峰。如果重合很差首先检查是不是K设小了再检查协方差类型是否太简单。6.2 用均值方差、KL散度、MMD做量化评估可视化能抓大问题但小差异还是要靠数字。我常用的几个指标均值向量和协方差矩阵直接对比直接用广播计算mean(X)与mean(X_new)的欧氏距离再看两者协方差矩阵的F范数差。这两个数能反应全局统计特性是否一致。KL散度需要先拟合两个GMM再用数值方法近似计算Matlab里没有现成的两个GMM之间KL散度函数但可以用Monte Carlo方式近似即从源模型采样计算平均对数似然差。这个指标对分布间的差异非常敏感实现也不复杂。MMD最大均值差异这是生成模型评估中常用的核方法指标适合比较两个高维分布。Matlab自带没有MMD实现只有几十行代码就能自己写。这里给一个简化的示例% 简化版MMD计算使用高斯核 function val mmd_gaussian(X, Y, sigma) Kxx exp(-pdist2(X, X).^2 / (2 * sigma^2)); Kyy exp(-pdist2(Y, Y).^2 / (2 * sigma^2)); Kxy exp(-pdist2(X, Y).^2 / (2 * sigma^2)); n size(X, 1); m size(Y, 1); val sum(Kxx(:)) / (n^2) sum(Kyy(:)) / (m^2) - 2 * sum(Kxy(:)) / (n * m); endsigma可以取训练样本特征标准差的中位数。MMD值越小两个分布越接近。6.3 用生成数据训练分类器的闭环验证最后一个维度最有说服力把生成的数据投入真实的下游任务。做法是对照组只用原始训练数据训练一个分类器并测测试集性能实验组把原始数据加上GMM生成的新数据一起训练同一个分类器再测测试集性能。如果实验组在关键指标准确率、F1、召回率上有提升或者至少不下降那这套生成方案才算真正“有用”。我之前遇到的情况是只拿200条故障样本训练分类器测试集上的故障召回率只有六成多一点加入GMM生成的800条故障样本后同一个分类器的故障召回率升到了八成。而用简单“噪声扰动”生成的数据召回率几乎没动。差距就是从“分布层面一致”和“只是局部抖动”之间来的。7. 动手前必须知道的几个坑我的排错记录最后这块我想把实操中踩过的坑集中说一下按严重程度排序。7.1 “X must have more rows than columns”样本量不足的报错这是刚上手最容易碰到的错误。GMM估计协方差矩阵时要求样本量必须大于特征维度准确说每行代表一个样本所以当样本数小于特征维数时fitgmdist直接报错。解决办法有几个方向降维是首选用PCA把高维特征压缩到十几维其次改用diagonal协方差类型因为对角矩阵的待估参数大幅减少样本量要求也随之降低再次可以考虑增大正则化值但效果有限。注意即使勉强拟合成功如果样本量/维度比值太小拟合出来的协方差也是不可信的生成的新样本质量会非常差。7.2 特征尺度差异大的数据是否要标准化如果数据的特征量纲差异极大比如特征1是数量级在0.001的精度值特征2是数量级在10000的温度值直接做GMM拟合时数值稳定性会出问题因为协方差矩阵里各个元素的量级差别太大EM算法迭代时很小幅度的更新就可能让某个特征对应的方差变成负值附近的数值病态。我习惯在拟合前先做z-score标准化让每个特征都处于同一量纲。参考代码mu_X mean(X); std_X std(X); X_norm (X - mu_X) ./ std_X; % 在标准化后的数据上拟合GMM GMModel_norm fitgmdist(X_norm, k, RegularizationValue, 1e-5); % 生成标准化空间中的样本 X_new_norm random(GMModel_norm, n_new); % 逆标准化回原始空间 X_new X_new_norm .* std_X mu_X;注意生成完一定要逆标准化否则特征量纲全乱了下游模型会直接出问题。这个“先标准化成模型空间的合理分布、生成后再还原”的手法很多实际场景都要用。7.3 高维数据GMM的能力边界在哪里GMM当维度超过几十甚至上百时full协方差矩阵的参数数量是O(d²)量级的训练数据稍微少一点拟合阶段就会过拟合或数值不稳定。我建议的边界是特征维度超过50且样本量只有几百条时尽量别用full协方差改用diagonal或者走PCA降维后再拟合。生成高维数据时也可以用“先拟合各特征分组再拼接”的思路但效果不如低维场景理想。另外一个被忽视的边界是数据本身的复杂程度。如果底层分布非常复杂比如有很强的非线性流形结构GMM强行逼近会需要极多的分量数K一旦大了又回到过拟合的坑。这个时候不如老老实实换VAE或者流模型。我的经验是GMM最适合的是“几十维以内、低到中等复杂度、有一定多峰结构”的数据分布。超出这个范围别硬上。说句总结性的体会GMM做数据生成真正考验人的不是调用函数而是对模型假设的理解和对K的判断。拟合完别忘了把验证步骤走完多花十分钟作图对比比什么都强。如果在实际使用中遇到协方差奇异或者分布对不齐优先检查正则化参数和分量数八成问题都出在这两个地方。
返回列表