应优先使用 scipy.spatial.distance.cdist 计算两组点间距离,因其底层为优化C代码、支持多种距离且避免内存爆炸;纯NumPy实现需用平方和公式避免三维中间数组,并注意浮点误差、数据类型与归一化。

用 scipy.spatial.distance.cdist 而不是手写循环
直接写双层 for 循环算两组点之间的欧氏距离,哪怕只有几千个点,也会慢得明显。NumPy 自身不提供批量距离矩阵计算的顶层函数,但 scipy.spatial.distance.cdist 是专为此设计的——它底层调用优化过的 C 代码,支持多种距离类型,且自动处理广播逻辑。
常见错误是试图用 np.linalg.norm 配合 np.expand_dims 做广播减法:虽然能跑通,但会临时生成 (m, n, d) 形状的大数组,内存爆炸风险高,尤其当 m、n > 10⁴ 时。
-
cdist(XA, XB, metric='euclidean')中,XA形状为 (m, d),XB为 (n, d),返回 (m, n) 距离矩阵 - 支持
'cityblock'、'cosine'、'mahalanobis'等,比硬编码更可靠 - 若只需求最近邻(而非全矩阵),可搭配
scipy.spatial.cKDTree,构建一次后多次查询更快
自己用 NumPy 广播时必须避免 (m,n,d) 中间张量
真要纯 NumPy 实现(比如不能引入 SciPy),核心是避免显式构造三维差值数组。正确做法是利用 np.einsum 或分步平方和:
diff = XA[:, None, :] - XB[None, :, :] # 错!这一步就生成 (m,n,d)
应改用:
立即学习“Python免费学习笔记(深入)”;
sq_norm_A = np.sum(XA**2, axis=1, keepdims=True) # (m, 1) sq_norm_B = np.sum(XB**2, axis=1) # (n,) dot_AB = XA @ XB.T # (m, n) dist_sq = sq_norm_A + sq_norm_B - 2 * dot_AB # 广播完成,无中间大数组 dist = np.sqrt(np.maximum(dist_sq, 0)) # 加 maximum 防浮点误差致负
-
np.maximum(dist_sq, 0)很关键:浮点误差可能让本该为 0 的值变成 -1e-16,开根报RuntimeWarning: invalid value encountered in sqrt - 该写法在 d 较大(如 > 100)时比
cdist略快,因省去函数调用开销;但 m、n 大时仍推荐cdist - 注意
@运算对内存连续性敏感,确保XA和XB是 C-contiguous(可用.copy(order='C')强制)
大批量计算时注意 float32 和内存布局
当点集规模上十万、维度上百,float64 距离矩阵占内存极大(例如 1e5 × 1e5 × 8 字节 = 80 GB)。实际中多数场景用 float32 完全够用,且能减半内存与计算时间。
- 传入
cdist前统一转:XA = XA.astype(np.float32),否则默认按输入 dtype 计算 - 如果数据来自 pandas DataFrame 或 torch.Tensor,注意 .values 或 .numpy() 后是否保留原内存顺序;乱序会显著拖慢
@和cdist - Linux 下可用
psutil.virtual_memory()监控实时内存,避免 OOM 杀进程
别忽略距离定义本身是否匹配业务需求
“距离”不是数学概念,而是业务建模选择。用欧氏距离前先确认:特征是否同量纲?是否有缺失?是否需要加权?
- 未归一化时,数值范围大的特征(如收入 vs 年龄)会主导距离结果——先做
sklearn.preprocessing.StandardScaler或 min-max - 含类别特征?别直接丢进距离计算。要么用 Gower 距离(需额外库),要么先做 target encoding 再用欧氏
- 文本嵌入向量常用余弦距离,此时
cdist(XA, XB, 'cosine')比手动算1 - cosine_similarity更稳(后者对零向量处理不一致)
批量距离看着是小操作,但一旦嵌入 pipeline,数据类型、精度、归一化、距离语义这些点漏掉一个,后面聚类或检索的结果就全偏了。


















