之前有朋友问我手上只有几十条实测数据想拿这些数据去训练模型或做蒙特卡洛模拟但样本量明显不够问我能不能“造”出更多数据。我当时给的答案是别硬造先把数据的分布信息抽出来再从这个分布里采样生成新样本。这条路上最常用的工具之一就是核密度估计Kernel Density EstimationKDE。下面我会围绕KDE在Matlab里的数据生成实现把原理、带宽参数、采样方法、代码案例和踩坑记录一次讲清楚。这篇文章适合正在做小样本扩充、数据增强、蒙特卡洛模拟的读者目标是让你看完能直接在自己机器上跑通并且知道结果到底靠不靠谱。1. 为什么是KDE它到底在估计什么1.1 从“复制粘贴”到“按分布采样”说到数据生成很多人第一个想到的是 Bootstrap 重采样从原始样本里随机抽抽出来的还是原始数据里的那些值。这种做法在统计推断里很常用但如果目的是扩充训练集它会带来一个明显问题——所有“新样本”跟老样本长得一模一样。对于连续型数据来说模型很容易记住这些重复值做交叉验证时看似效果不错一上真实场景就露馅。KDE 的思路完全不一样。它先估计出数据的概率密度函数然后按照这个密度去随机采样。这样生成的新样本既有原始数据的整体形态又不是简单复制粘贴而是会在高概率区域密集出现、低概率区域稀疏出现。这其实是“从数据里学习分布再从分布里生成数据”比 bootstrap 更适合做数据扩充。还有个容易踩的误区用直方图做密度估计。直方图的结果严重依赖箱宽和起点并且条形图本身就是不连续的。KDE 可以看作直方图的平滑版本它把每个样本点变成一个“小土包”再把所有土包叠加成一条平滑曲线不需要人为去选箱子的位置表达连续分布的能力强得多。1.2 KDE的数学直觉KDE 的公式其实不复杂。假设有 n 个样本 x_1, x_2, ..., x_n高斯核下的 KDE 估计是f_kde(x) (1 / (n * h)) * sum_{i1}^{n} exp(-0.5 * ((x - x_i) / h)^2) / sqrt(2 * pi)这个公式可以这么理解每个样本点 x_i 处放一个高斯“土包”土包的中心就是 x_i土包的宽度由带宽 h 控制。把所有土包按 1/n 加权叠加就得到了整个数据分布估计。你不需要假设原始数据服从正态分布、指数分布或者某个参数族只要样本点足够多、带宽选得合理KDE 就能逼近任意形状的连续分布。所以它是非参数方法灵活性很高。1.3 带宽hKDE唯一的“旋钮”整条 KDE 曲线最敏感的参数就是带宽 h。h 太小每个样本点自成一个小峰曲线全是锯齿平滑过度缺失h 太大细节被抹平双峰可能变成一个胖单峰原来的分布特征全丢了。你可以把 h 理解为“用多大尺度看数据”拿放大镜看全是毛刺拿望远镜看只剩轮廓KDE 要的是刚好能看清结构的那一档。工程上最常用的经验法是 Silverman 规则h 1.06 * sigma_hat * n^(-0.2)其中 sigma_hat 可以取标准差但如果数据有离群点更稳妥的是取 min(标准差, IQR/1.349)。IQR 是四分位距除以 1.349 是为了把 IQR 换算成正态分布下的标准差估计。这个规则在数据接近单峰时表现不错遇到明显多峰或长尾分布我建议把 h 从 0.5 倍到 2 倍扫一遍画图对比再选一个视觉上最合理的值。2. Matlab实现从估计到生成的核心代码2.1 准备工作与数据输入Matlab 里做 KDE最省事的方式是用统计与机器学习工具箱的ksdensity函数。但我要提醒一句ksdensity默认输出的是网格上的密度值不是采样结果。如果只想画密度曲线用它没问题如果要生成新样本还是得自己写采样逻辑。下面我给出的核心代码不复杂甚至不依赖ksdensity也能跑只用了randi、randn这些基础函数。先把数据整理成列向量避免行向量、列向量混用带来的麻烦rng(1); % 固定种子保证结果可复现 x [randn(150, 1) * 1.8 10; randn(50, 1) * 0.6 13]; x x(:); % 强制变成列向量我这里造了一个双峰分布示例一个峰在 10 附近另一个峰在 13 附近。真实场景中x换成load(measured_data.mat)导入的实测数据即可。2.2 带宽计算与密度曲线接下来计算带宽并手写一个高斯 KDE 用于画曲线nOld numel(x); sigmaHat std(x); robustScale min(sigmaHat, iqr(x) / 1.349); h 1.06 * robustScale * nOld^(-0.2); xi linspace(min(x) - 3 * h, max(x) 3 * h, 512); fh zeros(size(xi)); for i 1:nOld u (xi - x(i)) / h; fh fh exp(-0.5 * u.^2) / sqrt(2 * pi); end fh fh / (nOld * h); plot(xi, fh, LineWidth, 1.5);这段代码没有用任何工具箱函数纯手动实现高斯核。你也可以把循环换成矩阵运算但小样本下循环速度完全可以接受。如果不想手写直接用[f, xi] ksdensity(x, xi, Bandwidth, h);能达到同样的效果。2.3 中心扰动采样法有了 KDE 之后最直接的采样方式不是求逆函数而是“中心扰动法”。核心逻辑只有两步先从 n 个原始样本里随机挑一个中心然后在这个中心上加一个标准差为 h 的高斯噪声。写成函数就是function xNew kdeSample(x, h, numNew, seed) if nargin 4 seed []; end if ~isempty(seed) rng(seed); end nOld numel(x); idx randi(nOld, numNew, 1); % 有放回地选择原始中心 xNew x(idx) h .* randn(numNew, 1); end这个方法为什么是对的因为 KDE 本身就是一个高斯混合分布每个原始样本点是一个高斯成分的中心每个成分的权重是 1/n每个成分的标准差是 h。中心扰动法就是从这个混合分布里抽样和 KDE 的定义完全对应。相比对密度函数做数值积分再求逆采样这套逻辑更好懂、更不容易出错多维时也更容易扩展。2.4 完整生成与可视化把上面的输入、带宽计算、采样函数串起来rng(1); x [randn(150, 1) * 1.8 10; randn(50, 1) * 0.6 13]; x x(:); nOld numel(x); sigmaHat std(x); robustScale min(sigmaHat, iqr(x) / 1.349); h 1.06 * robustScale * nOld^(-0.2); xNew kdeSample(x, h, 5000, 20241001); figure; histogram(x, 30, Normalization, pdf, FaceAlpha, 0.3); hold on; histogram(xNew, 60, Normalization, pdf, FaceAlpha, 0.3); legend({原始数据, KDE生成数据}, Location, Best);注意调用kdeSample之前要先把函数文件放在当前目录或者直接写在脚本末尾。我在采样时固定了种子20241001这样每次运行生成结果一致方便调试和论文复现。3. 小样本实战案例只有30个点怎么办3.1 一个真实感很强的传感器场景假设某个传感器的测量值只有 30 个点均值大概 100标准差 2.1略有一点右偏。因为要跑可靠性模拟需要生成 3000 条数据。直接用 bootstrap 做生成的 3000 条只是这 30 条数据的重复组合连极值都不会变用正态分布拟合又太冒险万一数据是双峰或带偏度参数拟合会把结构完全抹掉。这种场景正好适合 KDE。按前面的 Silverman 规则手算h 大致在 1.1 这个数量级。用这段流程生成 3000 条数据后我统计了原始样本和生成样本的几个关键指标指标原始样本KDE生成样本样本量303000均值100.18100.21标准差2.082.135%分位数96.896.795%分位数103.7103.9偏度0.350.33可以看到均值、标准差、分位数、偏度都比较接近。更重要的是生成样本里出现了原始数据中没有的新数值比如 97.6、101.3、104.2但这些新值都在合理范围内没有跑出去太远。这正是数据生成想要的效果。3.2 边界问题数据不可能为负怎么办KDE 使用高斯核时理论上会生成整个实数轴上的值哪怕原始数据全是正数也可能出现负样本。这对很多物理量来说是荒谬的。我的处理方法是先对原始数据做对数变换再在对数空间做 KDE采样完成后再用指数变换转回来y log(x); % 用 y 计算带宽并采样 yNew kdeSample(y, hY, 5000); xNew exp(yNew);这样生成的数据天然大于 0而且对右偏长尾分布更友好。如果数据有明确上下界比如比例数据必须在 0 到 1 之间可以考虑用 logit 变换或者用反射法在边界处把样本反射回去但实现稍微复杂一点。大多数情况下对数变换已经够用。3.3 什么时候别用KDE生成KDE 不是万能的。我发现有这么几类场景用它效果不好一是数据量太小比如少于 20 个点带宽怎么选都不太稳二是数据本身是离散的高斯核会给离散值之间生成“半中间”的整数看起来很怪三是分布带有非常尖锐的物理截断比如某些参数不可能超过某个上限KDE 平滑后会把边界外的概率也拉出来制造一批超出物理约束的样本。更核心的问题是KDE 只能在你原始数据覆盖的区域“插值”不能“外推”。它生成的极值不会比原始样本的最大值大太多也不会比最小值小太多。如果你想用生成数据去研究尾部风险比如 99.99% 分位数对应的极端工况KDE 是给不了可靠答案的这种情况应该找极值理论或物理模型。4. 验证与排查生成完不是结束4.1 我踩过的几个坑第一忘记固定随机种子。早年我跑蒙特卡洛模拟每次生成的结果都不一致后来排查半天才发现是rng没有设置导致每个 batch 的随机数流完全不同。现在我在所有采样入口都强制加一个 seed 参数默认给一个固定值必要时再换。第二直接拿ksdensity默认带宽当成生成带宽用。ksdensity的默认带宽通常是为了“把密度曲线画好看”而优化的有时候偏大用来生成会让分布过于平滑。我一般会自己用 Silverman 规则算再在默认带宽附近做敏感性分析。第三把多维数据拆成一维分别做 KDE。这样做会彻底破坏变量之间的相关性。比如身高和体重正相关分别生成两个一维分布再拼在一起可能出现“身高 1.9 米、体重 40 公斤”这种在现实中几乎不存在的数据。多维问题要用多维方案不能偷懒。第四代码里忘记x x(:)。Matlab 的行向量和列向量混用会导致std(x)、iqr(x)的结果没问题但采样时x(idx)的形状莫名其妙变成行向量。把所有输入统一成列向量这类问题少一半。4.2 怎么判断生成数据靠谱图形验证是最直观的。把原始数据的直方图和生成数据的直方图叠加看两个分布是否贴合多峰位置是否对应尾部是否拉得过分。数值验证可以看均值、标准差、偏度、峰度、各分位数更严格一点可以用双样本 Kolmogorov-Smirnov 检验[h, p] kstest2(x, xNew);原假设是两组数据来自同一连续分布。p 值大于 0.05 时可以认为没有显著差异。但我要提醒一句当样本量特别大时K-S 检验会变得非常敏感任何微小差异都可能导致 p 值很小所以不能只看 p 值还要配合 Q-Q 图观察分位数是否对齐。生成数据终究是模拟数据验证的目标是“足够像”不是“完全一致”。4.3 几种数据生成方案横评方法优点缺点适用场景Bootstrap 重采样实现极简完全保留原始分布只能重复旧值无法产生新数值统计推断、小样本检验参数分布拟合解释性强样本外平滑对分布形状假设太强明确服从正态/指数等分布KDE 生成非参数灵活能生成新值带宽敏感高维容易失效小样本连续数据扩充GAN/VAE能学习复杂高维结构需要大量数据训练不稳定图像、大样本生成KDE 的定位很明确它是一个简单、可靠、不依赖强假设的中间方案。数据量大到可以训生成模型时KDE 不够看数据少到连 KDE 都撑不起来时你得回到物理约束或领域知识。恰恰在“几十到几千条连续数据”这个区间KDE 是性价比最高的选择。5. 扩展从一维到多维和业务流程5.1 多维KDE与相关性保持如果每条样本不止一个特征比如同时包含温度、压力、转速三个变量不能每条特征单独做一维 KDE。正确做法是使用多维 KDE。一个工程上很实用的采样方法是把中心扰动法直接推广到多维从原始样本中随机抽一行作为中心然后加上多维高斯噪声噪声的协方差矩阵由原始数据协方差和缩放因子决定。rng(1); d size(X, 2); nOld size(X, 1); hScale nOld^(-1 / (d 4)); % 多维Silverman缩放 H cov(X) * (hScale^2); L chol(H, lower); % 分解协方差矩阵 idx randi(nOld, numNew, 1); centers X(idx, :); noise randn(numNew, d) * L; % 等价于 mvnrnd(zeros(1,d), H, numNew) Xnew centers noise;这里用chol分解是为了不依赖mvnrnd如果你有统计工具箱直接用mvnrnd(zeros(1, d), H, numNew)更省事。多维场景下如果维度较高协方差矩阵可能不稳定我习惯在H上叠加一个小的对角项比如H H 1e-6 * eye(d)防止数值问题。5.2 有条件生成与按类别分组实际业务流程里数据往往不是一锅粥而是分了类的。比如不同型号的设备、不同批次的原料、不同工况下的测量值。简单做法是先把数据按类别拆开每个类别单独做 KDE再在生成时按原始类别比例采样。这样既保持了类别结构又不会把不同组的分布混在一起。还有一个进阶方向是处理“有条件生成”如果希望生成的新样本围绕某个输入特征变化可以先用 KDE 估计联合分布再推导条件分布。但实现复杂度会上一个台阶。对于多数工程场景按类别分组 KDE 已经足够用没必要一开始就上复杂模型。最后分享一个小技巧除了中心扰动法也可以用逆变换采样。先用 KDE 在密集网格上算密度累积成 CDF再用interp1对均匀随机数做反插值。这个方法的优点是可以方便地做边界裁剪确保生成值落在指定范围内。但计算量比中心扰动法大而且网格不够密时会引入插值误差。我现在的默认做法是单维用中心扰动多维也优先中心扰动只有遇到强边界约束时才换逆变换。我个人现在做小样本数据生成默认流程是先画图看分布形状算带宽用中心扰动法采样生成后用直方图、Q-Q 图和 K-S 检验做验证最后才进入业务环节。KDE 生成的是“看起来像”的数据不是“新的真相”它不能外推到没见过的区间。这个思路在蒙特卡洛模拟、模型鲁棒性测试、小样本扩充里都够用等以后数据量变大、维度变高再上 GAN 或扩散模型也不迟。
