在机器学习实践中,距离计算是最基础却又最频繁的操作之一。想象你正在处理一个图像分类任务,需要比较每张测试图片与训练集中所有图片的相似度;或者你在实现一个推荐系统,要计算用户之间的偏好距离。这些场景本质上都在做同一件事——计算样本之间的距离。
平方欧氏距离(Squared Euclidean Distance)作为最常用的距离度量之一,其定义为两个向量各维度差值的平方和。相比普通欧氏距离,平方形式不仅保持了距离的单调性(因为平方函数在正数区间单调递增),还省去了耗时的开方运算,这对需要大量距离计算的算法(如k-means聚类)能带来显著的性能提升。
马氏距离(Mahalanobis Distance)则更进一步,通过引入协方差矩阵的逆来考虑各维度之间的相关性,相当于在计算距离前先对数据进行白化处理。这使得马氏距离在特征尺度差异大或存在强相关性的数据上表现更优,比如人脸识别中的特征匹配。
sqdistance函数的精妙之处在于它用一个统一的接口处理了三种常见场景:
matlab复制function D = sqdistance(A, B, M)
% 计算成对平方距离矩阵
% 输入:
% A - m×d矩阵(m个d维样本)
% B - n×d矩阵(n个d维样本,可选)
% M - d×d正定矩阵(马氏距离参数,可选)
% 输出:
% D - m×n距离矩阵,D(i,j)表示A(i,:)与B(j,:)的平方距离
模式一:单数据集自距离矩阵
当只传入A矩阵时(D = sqdistance(A)),函数计算A中所有样本两两之间的平方欧氏距离。这特别适用于需要构建距离矩阵或核矩阵的场景,比如谱聚类算法中高斯核的计算。
模式二:跨数据集距离计算
传入A和B两个矩阵(D = sqdistance(A,B))时,计算A中每个样本与B中每个样本的平方欧氏距离。这在k近邻(k-NN)分类、图像检索等需要查询样本与数据库比对的应用中非常实用。
模式三:马氏距离计算
当传入第三个参数M时(D = sqdistance(A,B,M)),函数计算基于M的平方马氏距离。这里的M通常是样本协方差矩阵的逆,也可以是任何自定义的正定矩阵,这为距离度量提供了极大的灵活性。
传统实现距离计算会使用双重循环遍历所有样本对,这在MATLAB中效率极低。sqdistance的高效来自于对距离公式的巧妙展开和向量化运算。
对于欧氏距离,核心公式展开为:
code复制|x - y|² = |x|² + |y|² - 2xᵀy
这个展开的妙处在于:
|x|²和|y|²可以分别批量计算(通过sum(A.^2, 2))xᵀy可以通过矩阵乘法A*B'一次性得到所有样本对的点积马氏距离的展开类似:
code复制(x-y)ᵀM(x-y) = xᵀMx + yᵀMy - 2xᵀMy
实现时同样先分别计算:
xᵀMx:sum((A*M).*A, 2)yᵀMy:sum((B*M).*B, 2)xᵀMy:A*M*B'注意:当M是单位矩阵时,马氏距离就退化为欧氏距离,因此模式三实际上是通用形式,模式一和二是其特例。
让我们拆解这个约20行的高效实现:
matlab复制function D = sqdistance(A, B, M)
% 参数预处理
if nargin < 2 || isempty(B)
B = A; % 模式一:B默认为A自身
end
if nargin < 3 || isempty(M)
M = eye(size(A,2)); % 默认使用欧氏距离(M为单位矩阵)
end
% 核心计算
AA = sum((A*M).*A, 2); % xᵀMx项
BB = sum((B*M).*B, 2)'; % yᵀMy项(转置为后续广播准备)
AB = A*M*B'; % xᵀMy项
% 组合结果
D = AA + BB - 2*AB;
% 确保对称性和非负性(处理浮点误差)
D = max(D, 0);
if nargin < 2 || isempty(B) || isequal(A,B)
D = (D + D')/2; % 保证对称
end
内存预分配与广播机制
MATLAB在底层对矩阵运算有深度优化。AA和BB的计算充分利用了列向量与行向量的广播机制,避免了显式的repmat操作。例如,AA + BB会自动将AA的m×1矩阵与BB的1×n矩阵扩展为m×n矩阵相加。
对称性处理
在计算自距离矩阵(A=B)时,理论上结果应该完全对称。但由于浮点误差,D可能不对称。函数通过(D + D')/2强制对称,这在后续使用中(如特征分解)很重要。
非负性保证
同样由于浮点误差,距离平方可能出现极小的负值。max(D,0)确保了结果的数学正确性。
参数灵活性
通过nargin检查和isempty判断,函数实现了灵活的输入参数处理。用户可以显式传入空矩阵[]来使用默认行为。
我们通过一个实验展示性能差异。生成1000个10维样本:
matlab复制X = randn(1000, 10);
Y = randn(800, 10);
% 向量化版本
tic; D1 = sqdistance(X, Y); t1 = toc;
% 双重循环版本
tic;
D2 = zeros(size(X,1), size(Y,1));
for i = 1:size(X,1)
for j = 1:size(Y,1)
D2(i,j) = sum((X(i,:) - Y(j,:)).^2);
end
end
t2 = toc;
fprintf('向量化版本: %.4f秒\n循环版本: %.4f秒\n加速比: %.1f倍\n',...
t1, t2, t2/t1);
在主流PC上(MATLAB R2021a),测试结果通常是:
code复制向量化版本: 0.0082秒
循环版本: 1.4265秒
加速比: 174.0倍
当数据量极大时(如维度>1000或样本数>1e5),直接计算A*M*B'可能导致内存不足。这时可以采用分块计算策略:
matlab复制blockSize = 5000; % 根据内存调整
D = zeros(size(A,1), size(B,1));
for i = 1:blockSize:size(A,1)
iEnd = min(i+blockSize-1, size(A,1));
for j = 1:blockSize:size(B,1)
jEnd = min(j+blockSize-1, size(B,1));
AAi = sum((A(i:iEnd,:)*M).*A(i:iEnd,:), 2);
BBj = sum((B(j:jEnd,:)*M).*B(j:jEnd,:), 2)';
ABij = A(i:iEnd,:)*M*B(j:jEnd,:)';
D(i:iEnd,j:jEnd) = AAi + BBj - 2*ABij;
end
end
对于支持GPU的MATLAB版本,只需将输入数据转换为gpuArray即可获得显著加速:
matlab复制A_gpu = gpuArray(A);
B_gpu = gpuArray(B);
M_gpu = gpuArray(M);
D_gpu = sqdistance(A_gpu, B_gpu, M_gpu);
D = gather(D_gpu); % 将结果传回CPU
在NVIDIA RTX 3090上测试,对于10000×100的矩阵,GPU版本可比CPU版本快5-8倍。
k-means的核心步骤是计算每个样本到所有簇中心的距离。使用sqdistance可以大幅优化:
matlab复制function [labels, centers] = kmeans(X, k, maxIter)
% 初始化簇中心
centers = X(randperm(size(X,1), k), :);
for iter = 1:maxIter
% 分配阶段:计算所有样本到中心的距离
D = sqdistance(X, centers);
[~, labels] = min(D, [], 2);
% 更新阶段:重新计算中心
for i = 1:k
centers(i,:) = mean(X(labels==i,:), 1);
end
end
技巧:在MATLAB R2019b及以上版本中,使用
vecnorm可以进一步优化范数计算:matlab复制AA = vecnorm(A*M, 2, 2).^2;
许多核函数(如高斯RBF核)基于距离矩阵构建:
matlab复制function K = rbf_kernel(X, Y, gamma)
D = sqdistance(X, Y);
K = exp(-gamma * D);
end
在马氏距离异常检测中,通常:
matlab复制mu = mean(trainX, 1);
S_inv = inv(cov(trainX));
D = sqdistance(x, mu, S_inv);
结合sqdistance和pdist2可以实现灵活的近邻搜索:
matlab复制% 批量查询
function [indices, dists] = batch_knn(query, database, k, M)
if nargin < 4
D = sqdistance(query, database);
else
D = sqdistance(query, database, M);
end
[dists, indices] = mink(D, k, 2);
end
注意事项:当k很小时(如k=1),直接使用
min比mink更高效。MATLAB R2020b引入了新的排序算法,对小k值有优化。
问题现象:当数据尺度差异大时,|x|² + |y|² - 2xᵀy可能因抵消导致精度损失。
解决方案:
matlab复制A = (A - mean(A,1)) ./ std(A,0,1);
matlab复制D = sum((A - B').^2, 2); % 对单查询样本更稳定
问题现象:当M不是严格正定时,马氏距离可能出现负数。
检测与修复:
matlab复制[V,D] = eig(M);
if any(diag(D) <= 0)
warning('M不是正定矩阵,正在调整');
D(D <= 0) = eps;
M = V*D/V;
end
问题现象:当维度d很大时,距离计算可能失去判别力。
缓解策略:
解决方案:
matlab复制A = single(A); % 使用单精度
通过将对角权重矩阵W融入马氏距离框架:
matlab复制W = diag([w1, w2, ..., wd]); % 权重对角阵
D = sqdistance(A, B, W);
利用平方欧氏距离与余弦相似度的关系:
matlab复制function D = cosdistance(A, B)
AA = sum(A.^2, 2);
BB = sum(B.^2, 2)';
AB = A*B';
D = 1 - AB ./ sqrt(AA * BB);
end
对于某些核函数,可以直接推导出更高效的计算方式。例如,对于多项式核:
matlab复制function K = poly_kernel(X, Y, c, d)
K = (X*Y' + c).^d;
end
在实际项目中,我经常将sqdistance与这些专用核函数结合使用,根据数据特性选择最合适的距离度量。比如在处理文本数据时,余弦距离通常比欧氏距离更合适;而在处理归一化后的图像特征时,平方欧氏距离因为计算简单且效果相当,往往成为首选。