ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Matlab二维正态分布可视化:从密度曲面到等值线实战

Matlab二维正态分布可视化:从密度曲面到等值线实战

做二维正态分布可视化,几乎是每个接触多元统计或者机器学习的人都会遇到的一道坎。刚入坑的时候,我用Matlab画出来的图要么是一根根孤零零的曲线,要么是糊成一团色块,完全看不出“分布”的味道;后来把原理和绘图函数都吃透了,才明白这个可视化想表达的核心其实是三样东西:概率密度的高低、相关性造成的椭圆倾斜、以及样本点落在这个椭圆里的疏密关系。这篇博文就把我调试过程中积累的思路、代码和踩坑经验整理一遍,适合刚学完概率论、正在做课程实验或者准备用Matlab做数据分布展示的人参考,可以直接照着复现。

我先把话放在前面:Matlab做二维正态分布可视化,真正的核心不是背下某个绘图函数,而是搞懂均值向量和协方差矩阵如何决定了图形的“位置、胖瘦和倾斜角”。只要这个大方向对了,剩下就是调参和美化的事。

1. 二维正态分布的数学底子与可视化思路拆解

1.1 从一维扩展到二维,联合密度函数到底在描述什么

一维正态分布大家都很熟,就一个钟形曲线,用均值和方差两个参数就能完全刻画。到了二维,事情变得有意思了:我们面对的不再是一条曲线,而是一个“山丘”,横纵两个轴分别代表两个随机变量,竖轴代表概率密度值。这个山丘的数学表达式是联合概率密度函数:

f(x, y) = (1 / (2π · |Σ|^(1/2))) · exp(-0.5 · (X - μ)^T · Σ^(-1) · (X - μ))

其中X是二维列向量,μ是均值向量,Σ是2×2协方差矩阵。初看这个公式会觉得有点吓人,尤其是指数里套着矩阵求逆和转置,但拆开看并不复杂:核心部分(X - μ)^T Σ^(-1) (X - μ)其实是在计算样本点X到中心μ的一种“加权距离”,权重由协方差矩阵的逆决定。协方差矩阵越“大”,也就是方差越大,这个距离被缩放得越厉害,密度山丘就越矮越平。

很多人对二维正态分布的理解卡在“两个变量各自服从正态分布”上,这是不完整的。二维正态分布除了要求每个边缘分布是正态的,还通过协方差矩阵里的协方差项把两个变量的线性关联刻画出来了。换句话说,两个变量是否一起变大变小、关联强度多大,都藏在Σ的非对角元素里。这也是为什么可视化时不能只画两个一维直方图,必须画出联合分布的全貌。

1.2 可视化该画什么:曲面、等值线和样本点的分工

想清楚要画什么,比急着打开Matlab更重要。二维正态分布的完整信息密度太高,一张图很难全部表达清楚,所以实践中通常拆成三类视角:

第一类是三维曲面图,也就是把密度函数f(x, y)在网格点上算出数值后用surf绘制,视觉上是一个平滑的山丘,能直观看出峰顶位置、坡度变化和分布范围。适合用来建立整体直觉,也是初学时最想画出来的效果。

第二类是等值线图,也就是把同一密度值的点连成线,用contour绘制。这个视角特别适合展示协方差矩阵的影响:如果两个变量独立,等值线是正圆;如果正相关,等值线变成沿对角线方向拉伸的椭圆;如果负相关,则沿另一条对角线拉伸。等值线的疏密还对应着概率密度的变化快慢,研究相关性时比曲面图更清晰。

第三类是样本散点图,用mvnrnd直接生成服从二维正态分布的随机样本,再画成散点。散点图的价值在于展示“随机性”:虽然大部分点聚集在中心附近,但总会有少数点落在远离中心的区域。把散点图和等值线叠加在同一张图里,能直观看到样本分布与理论密度的吻合程度。我的习惯是这三类图都画一遍,或者把等值线和散点叠加,信息量最大。

1.3 为什么选Matlab而不是其他工具

做这类可视化,可选的工具确实不少,比如Python的matplotlib和seaborn也很流行。但Matlab在“探索性可视化”上有两个天然优势:一是内置了mvnrnd和mvnpdf这类多元正态相关函数,不用自己手写随机数生成或密度计算,能把精力集中在理解参数上;二是交互式绘图工具和图形句柄体系非常成熟,旋转视角、调整光照、标注峰值都特别顺手。如果你是正在做课程实验或者研究原型验证,Matlab这种“开箱即用”的特性会节省大量时间。

当然,Matlab的缺点也明显,比如闭源、授权费不便宜。但单就二维正态分布可视化这个场景,它的性价比和效率是最高的。我后来用Python复现同一套图,代码量差不多,但调试图形样式时花的时间明显更多。

2. 核心参数与绘图前的准备

2.1 均值向量和协方差矩阵到底怎么设

动手写代码之前,先把参数弄明白,否则画出来的图一定会让你怀疑人生。

均值向量mu决定山丘的中心位置。比如mu = [0, 0],峰顶就在原点;mu = [2, -1],峰顶就移到(2, -1)。这个参数最容易理解,也最容易设置,没有太多坑。

协方差矩阵Sigma才是真正的主角。2×2的协方差矩阵写成:

Sigma = [sigma1^2, rho·sigma1·sigma2; rho·sigma1·sigma2, sigma2^2]

其中sigma1和sigma2分别是两个变量的标准差,rho是两个变量的相关系数,取值范围是[-1, 1]。对角线元素决定山丘在x和y方向上的“胖瘦”,非对角线元素决定山丘是否倾斜以及倾斜的方向和程度。

这里有一个新手必踩的坑:直接把相关系数rho放到了非对角线上而不乘标准差乘积。正确的写法是Sigma(1,2) = rho * sigma1 * sigma2,Sigma(2,1)和它对称。如果标准差不是1,漏乘这个乘积项,画出来的等值线倾斜方向虽然对,但形状会完全错误。

2.2 我常用的三组典型参数场景

为了把不同参数的影响讲清楚,我习惯用三组对照实验来演示:

第一组:mu = [0, 0],Sigma = [1, 0; 0, 1]。这是标准二维正态分布,两个变量独立且方差都为1,等值线是标准圆。

第二组:mu = [0, 0],Sigma = [1, 0.7; 0.7, 1]。相关系数rho = 0.7,正相关,等值线变成沿45度方向拉伸的椭圆。这是展示“相关性导致椭圆倾斜”的经典例子。

第三组:mu = [0, 0],Sigma = [1, -0.7; -0.7, 1]。相关系数为负,等值线沿-45度方向拉伸,和上面一组形成鲜明对比。

设置这三组参数还有个好处:当某个参数的效果不直观时,可以只改一个变量就能对比出差异。比如只看sigma1从1变成2,山丘在x方向被拉宽、峰值同时降低,这个变化用动图展示会更加深刻。

2.3 网格生成的范围怎么定才算合理

绘制曲面和等值线,第一步是生成二维网格。网格范围太小会切掉山丘的“裙摆”,范围太大又会让主要区域被压缩成一个小凸包。我的经验是:以均值向量为中心,向四周伸展约四倍标准差。对于上面的第一组参数,x和y都取[-4, 4]就足够覆盖绝大部分密度区域了。如果协方差矩阵里的方差更大,或者负相关让椭圆沿对角线延伸得更远,就适当扩到五倍甚至六倍标准差。

网格间距的选择也影响成图质量。步长取0.1时,曲线足够平滑,计算量也不大;追求更细腻的效果可以取0.05,但网格点数量会变为四倍。对于测试阶段,先粗后细,确认参数合理后再加密网格,能省下不少等待时间。

3. 完整实现:三张核心图形一次画到位

3.1 准备数据:用mvnpdf计算联合密度

假设我们已经设置好mu和Sigma,下面先用meshgrid生成网格坐标,再用mvnpdf计算每个网格点的密度值:

mu = [0, 0]; Sigma = [1, 0.7; 0.7, 1]; x = -4:0.1:4; y = -4:0.1:4; [X, Y] = meshgrid(x, y); XY = [X(:), Y(:)]; % 每一行是一个二维坐标点 % mvnpdf要求输入为N×2的矩阵,返回每个点的密度值 F = mvnpdf(XY, mu, Sigma); F = reshape(F, size(X)); % 把列向量还原成网格形状

这里有一个容易出错的地方:mvnpdf的输入必须是N×D矩阵,其中D是维度数。如果你直接把X和Y分别传进去,会出现维度报错;如果忘记reshape,后面surf和contour画出来的图就是错乱的。我在初学时就在这栽过跟头,调试了半天才发现是数据形状不对。

如果要自己手写密度函数而不依赖mvnpdf,也可以用前面那个公式直接计算。但既然Matlab已经提供了成熟的实现,没必要重复造轮子,除非是为了教学演示或者验证自己对公式的理解。

3.2 三维曲面图与细节美化

计算好F之后,画三维曲面就是一行代码的事:

figure; surf(X, Y, F, 'EdgeColor', 'none'); colormap(parula); % Matlab自带的感知均匀色图,比默认jet更舒服 colorbar; xlabel('x'); ylabel('y'); zlabel('density f(x,y)'); title('二维正态分布联合密度曲面'); view(45, 30); % 调整视角,45度方位角,30度仰角

surf画出来的是网格曲面,如果不去掉EdgeColor,图面会布满黑线,看起来非常乱;加上'EdgeColor', 'none'之后就是一个平滑山丘。这个细节很多人不知道,默认状态下的surf会让人误以为自己的数据有问题。

光照设置是提升质感的关键一步:

lighting gouraud; light('Position', [1, 1, 1], 'Style', 'infinite'); material dull;

加上光照之后,山丘的明暗变化会把峰顶和坡度衬托得更立体。但要注意,如果是做论文配图,有时候反而需要不带光照、纯色映射的版本,更加简洁。我的做法是准备两个版本,一个带光照用于口头汇报,一个不带光照用于印刷。

3.3 等值线图与相关性信息提取

曲面图好看,但要精确分析相关性强弱,等值线图更有说服力。等高线画法如下:

figure; contour(X, Y, F, 8, 'LineWidth', 1.2); % 画8条等值线 xlabel('x'); ylabel('y'); axis equal; % 保持横纵比例一致,否则圆会变成扁椭圆 title('二维正态分布等值线'); grid on;

axis equal这一行容易被忽略但极其重要。如果不加,Matlab默认会把坐标轴的显示比例拉成近似的方形或长方形,导致明明是圆形的等值线看起来变成了椭圆,直接污染你的观察结论。我见过不止一个人对着图说“这不是正圆,怎么相关系数好像不为零”,结果加上axis equal之后一切正常。

等值线的条数也有讲究。太少看不出分布轮廓,太多则糊成一团。我一般设置6到10条,并且用自动选择的等高层级,让Matlab自己决定放哪些密度值。如果你想知道某条等值线内部包含了多少概率,可以用mvdensities或者数值积分的方式去算,但那就是另一个话题了。

3.4 样本散点叠加等值线,最能验证分布性质

密度曲面和等值线描述的都是“理论”,接下来用随机样本验证理论,这一步非常直观:

rng(42); % 设置随机种子,保证每次运行结果一致 N = 500; samples = mvnrnd(mu, Sigma, N); figure; contour(X, Y, F, 8, 'LineWidth', 1.0); hold on; scatter(samples(:,1), samples(:,2), 10, 'k', 'filled', 'MarkerFaceAlpha', 0.4); hold off; xlabel('x'); ylabel('y'); axis equal; title('等值线与500个随机样本叠加');

这里有两个经验点。第一,随机种子一定要设置,特别是在做多次对比实验或者写报告时。如果不设置rng,每次运行得到的散点图都不同,虽然分布性质一致,但细节上的差异可能会让你误判“这次参数变了”。第二,散点透明度要调低,也就是把MarkerFaceAlpha设成0.3到0.5之间。样本数量一大,重叠点很多,不调透明度的话中心区域会黑成一大团,什么信息都看不出来。

从这张叠加图能直观看到:样本点在椭圆中心区域最密集,向边缘逐渐稀疏,而且散点的整体形状和等值线的椭圆形状高度吻合。这就是“随机样本验证理论分布”的经典画面。

4. 常见问题与排查技巧实录

4.1 协方差矩阵非正定导致的报错

mvnrnd和mvnpdf都要求协方差矩阵是对称正定矩阵。如果你设置参数时不小心把Sigma写成非对称,或者相关系数绝对值超过1,运行时很可能会报错或者生成NaN值。

最常见的错误是Sigma不对称。例如只设置了Sigma = [1, 0.7; 0, 1],下三角写成0,这样矩阵不是对称矩阵,mvnrnd会拒绝执行。解决办法就是严格遵循Sigma(1,2) = Sigma(2,1)的规则,或者在代码里强制对称化:

Sigma = (Sigma + Sigma') / 2; % 强制对称化,避免手误

还有一个更隐蔽的问题是数值病态。当两个变量几乎完全相关,比如rho = 0.999时,协方差矩阵的行列式非常接近0,计算逆矩阵时会因为数值误差产生极大的密度值。这不是Matlab的bug,而是高维正态分布的固有性质。遇到这种情况,可以考虑用pinv代替inv计算伪逆,或者把rho限制在[-0.99, 0.99]内,避免极值情况。

4.2 曲面图出现“锯齿”或“毛刺”怎么办

这种现象通常由两个原因造成:网格太疏,或者数据里混入了NaN。检查NaN的推荐方式是在绘图前加一行断言:

assert(~any(isnan(F(:))), '密度值中出现NaN,请检查协方差矩阵');

如果确认数据没问题,那就是网格间距的问题。把步长从0.2缩小到0.05,曲面会立刻平滑很多。但要注意网格细分带来的内存消耗:网格点数从80×80变成160×160,计算量增加四倍,虽然对于二维场景完全不是问题,但在一堆循环里反复计算时还是会拖慢速度。我的习惯是先用大步长粗看,确认没问题后再加密网格输出最终图。

4.3 打印图片时等值线被裁切

常有人画出来的等值线图,边缘处线条被切断,看起来有很大一片区域没画全。这和网格范围密切相关。如果x和y范围只取了[-3, 3]而标准差是1.5,那么最外圈等值线对应的区域必然超出绘图范围,被裁剪是正常的。解决办法不是把图形拉扁,而是重新设置网格范围。

推荐的做法是动态生成范围,而不是写死:

range_limit = 3.5 * max(sqrt(eig(Sigma))); x = linspace(mu(1) - range_limit, mu(1) + range_limit, 100); y = linspace(mu(2) - range_limit, mu(2) + range_limit, 100);

用eig计算协方差矩阵的特征值再取最大值,相当于找到所有方向上的最大标准差,再乘以3.5倍覆盖主体区域。这个公式对任何协方差矩阵都适用,不用每次手动改范围。

4.4 如何让多个子图放在一起对比

参数对比实验最常用的布局是2×2子图,分别展示:标准圆、正相关椭圆、负相关椭圆、散点叠加等值线。实现时先在循环里准备一个cell数组保存每个Sigma,再用subplot逐个绘制:

Sigmas = {[1, 0; 0, 1], [1, 0.7; 0.7, 1], [1, -0.7; -0.7, 1], [2, 0.5; 0.5, 1]}; figure; for i = 1:4 subplot(2, 2, i); % 绘制当前Sigma的等值线 % 标题里把相关系数显示出来 end

子图对比时一定要统一x和y的范围以及等值线数量,否则不同子图之间没有可比性。另外一个细节是colorbar不要放到每个子图里,而是整个figure只放一个统一色标,让密度值在子图间可比。这个处理在写论文配图时特别重要,否则审稿人会质疑你的对比基准不一致。

5. 进阶玩法与个人心得

如果上面这些内容你都吃透了,可以再往前走一步:做动态可视化。把相关系数rho从0到0.95连续变化,在循环里重新计算协方差矩阵并实时更新等值线或曲面,能非常直观地看到“圆”如何被慢慢拉成“椭圆”。代码不复杂,核心是用drawnow强制刷新画面,并且预先获取图形句柄来更新数据,不要每次循环都重新创建figure。

另一个值得尝试的方向是用surfnorm可视化概率密度的梯度方向。梯度方向指向密度增大最快的方位,也就是山丘最陡的方向。把梯度箭头画在等值线上,能看出从中心向外都是指向峰顶的,且越靠近中心箭头越短,这其实是把“最大概率方向”这个概念具象化了。

这个可视化项目我做了好几轮,最深的一点体会是:不要一上来就依赖交互式工具乱拖乱看,先用纸和笔把mu和Sigma对图形形状的影响推演一遍,再上Matlab验证。推演和计算对上了,图形不论怎么调整都不会出错;对不上的时候,往往是你的数学直觉需要被修正,而不是代码需要Debug。先想清楚再动手,这套方法论在任何一个数据处理任务里都适用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进