1. 相似度算法:从几何直觉到工程实践
在信息爆炸的时代,我们每天都在与各种形式的向量数据打交道——从搜索引擎中的文档匹配,到电商平台的商品推荐,再到社交媒体的内容分发。这些场景背后都依赖一个核心问题:如何量化两个事物之间的相似程度?从业十年,我发现很多工程师能够熟练调用sklearn中的相似度计算函数,却对算法选择背后的数学原理和工程考量缺乏深刻理解。今天我们就来深入探讨最常用的两种相似度度量:余弦相似度与欧氏距离。
我第一次意识到这个问题的重要性是在优化一个新闻推荐系统时。最初直接使用欧氏距离计算文章相似度,结果发现热门文章总是被过度推荐,而一些优质长文却被系统忽略。直到将度量方式切换为余弦相似度,才真正捕捉到内容本身的语义相关性。这个教训让我明白:相似度算法的选择不是简单的API调用问题,而是需要对数据特性和业务目标有深刻认知的架构决策。
2. 余弦相似度:方向比大小更重要
2.1 从几何角度理解余弦相似度
想象你在一个多维空间中,每个维度代表一个特征(比如词频、用户评分等)。余弦相似度关注的是向量之间的夹角而非长度。数学上,它计算两个向量夹角的余弦值:
code复制cosθ = (A·B) / (||A|| * ||B||)
这个公式的分子是向量点积,反映对应维度乘积之和;分母是向量模长的乘积,起到归一化作用。当向量经过L2归一化后,余弦相似度就简化为简单的点积运算。
提示:在文本处理中,TF-IDF向量天然适合使用余弦相似度,因为不同文档的长度差异很大,但我们更关心词频分布的相似性而非绝对词频数。
2.2 余弦相似度的五大核心特性
-
方向敏感性:完全相同的向量得分为1,完全相反的为-1,正交向量为0。在推荐系统中,正值表示正向关联,负值可能暗示排斥关系。
-
尺度不变性:向量长度变化不影响结果。比如[1,2,3]和[2,4,6]的相似度为1,这对用户行为分析特别重要——活跃用户与轻度用户的行为模式可以直接比较。
-
高维适应性:在稀疏特征空间(如词袋模型)中表现优异,因为零值维度不会影响计算结果。
-
计算效率:现代优化库如Facebook的FAISS可以高效计算大规模向量的余弦相似度,支持十亿级向量检索。
-
语义捕捉能力:在词嵌入空间(如Word2Vec)中,余弦相似度能有效捕捉语义相关性,比如"国王"与"王后"的相似度高于"国王"与"苹果"。
2.3 工程实践中的Python实现
实际项目中我们通常会做这些优化:
python复制import numpy as np
from numba import njit # 使用JIT加速
@njit
def batch_cosine_similarity(matrix, vector):
"""计算矩阵每行与目标向量的余弦相似度"""
norms_matrix = np.sqrt((matrix ** 2).sum(axis=1))
norm_vector = np.sqrt((vector ** 2).sum())
dot_products = matrix @ vector
return dot_products / (norms_matrix * norm_vector)
# 带稀疏矩阵优化的版本
from scipy.sparse import csr_matrix
def sparse_cosine(a: csr_matrix, b: csr_matrix):
"""稀疏矩阵的余弦相似度计算"""
numerator = a.dot(b.T)
denominator = np.sqrt(a.multiply(a).sum(1)) * np.sqrt(b.multiply(b).sum(1))
return numerator / denominator
在推荐系统实践中,我们还会使用调整余弦相似度(Adjusted Cosine)来消除用户评分偏差:
python复制def adjusted_cosine(user_ratings, item1, item2):
"""考虑用户平均评分的改进版本"""
common_users = np.where((user_ratings[:,item1] > 0) & (user_ratings[:,item2] > 0))[0]
if len(common_users) == 0: return 0
mean_ratings = user_ratings[common_users].mean(axis=1)
item1_ratings = user_ratings[common_users, item1] - mean_ratings
item2_ratings = user_ratings[common_users, item2] - mean_ratings
return np.dot(item1_ratings, item2_ratings) / (
np.linalg.norm(item1_ratings) * np.linalg.norm(item2_ratings))
3. 欧氏距离:空间几何的直观度量
3.1 欧氏距离的数学本质
欧氏距离就是我们熟悉的多维空间直线距离公式:
code复制d = √Σ(x_i - y_i)²
这个看似简单的公式在实际应用中却有许多精妙之处。比如在图像处理中,两个256维像素向量的欧氏距离可以直接反映图像的视觉差异;在GPS定位中,经纬度坐标的欧氏距离(需投影变换后)能计算实际物理距离。
3.2 欧氏距离的工程特性
-
尺度敏感性:受向量长度影响显著。在用户画像分析中,活跃用户的行为向量模长大,可能导致距离计算偏差。
-
维度诅咒:在高维空间中,所有点对的距离会趋同,这使得聚类效果下降。解决方法包括特征选择或降维。
-
计算优化:对于大规模数据,可以使用KD-Tree或Ball-Tree加速近邻搜索:
python复制from sklearn.neighbors import NearestNeighbors
nbrs = NearestNeighbors(n_neighbors=10, algorithm='ball_tree', metric='euclidean')
nbrs.fit(vectors)
distances, indices = nbrs.kneighbors(query_vector)
3.3 实际应用案例
在异常检测系统中,我们常用欧氏距离计算样本到聚类中心的距离:
python复制def anomaly_detection(samples, center, threshold):
distances = np.sqrt(((samples - center) ** 2).sum(axis=1))
return distances > threshold
# 带马氏距离的改进版本(考虑特征相关性)
def mahalanobis_distance(x, mean, cov_inv):
delta = x - mean
return np.sqrt(delta.T @ cov_inv @ delta)
在图像搜索引擎中,结合欧氏距离与颜色直方图特征:
python复制def image_similarity(hist1, hist2):
# 归一化直方图
hist1 = hist1 / hist1.sum()
hist2 = hist2 / hist2.sum()
# 计算巴氏距离(欧氏距离的变种)
return np.sqrt(np.sum((np.sqrt(hist1) - np.sqrt(hist2)) ** 2))
4. 深入对比:何时用哪个?
4.1 决策树:选择合适度量的方法论
code复制是否需要考虑向量长度?
├── 是 → 欧氏距离
└── 否 → 数据是否稀疏?
├── 是 → 余弦相似度
└── 否 → 两者都可以,需实验验证
4.2 典型场景对照表
| 场景特征 | 推荐算法 | 注意事项 |
|---|---|---|
| 文本TF-IDF向量 | 余弦相似度 | 需做停用词过滤和词干提取 |
| 用户行为日志 | 调整余弦相似度 | 考虑用户活跃度归一化 |
| 图像像素值 | 欧氏距离 | 建议先做直方图均衡化 |
| 地理位置数据 | 欧氏距离 | 需转换为UTM坐标或使用Haversine公式 |
| 词嵌入向量(Word2Vec) | 余弦相似度 | 注意embedding是否已归一化 |
| 时间序列数据 | DTW距离 | 欧氏距离对相位偏移敏感 |
4.3 混合策略实践案例
在电商混合推荐系统中,我们这样组合两种度量:
python复制def hybrid_recommend(user_vector, item_matrix):
# 第一阶段:余弦相似度粗筛
cos_scores = cosine_similarity(user_vector, item_matrix)
candidate_indices = np.argsort(cos_scores)[-1000:] # 取Top1000
# 第二阶段:欧氏距离精排
normalized_user = user_vector / np.linalg.norm(user_vector)
normalized_items = item_matrix[candidate_indices] / np.linalg.norm(
item_matrix[candidate_indices], axis=1, keepdims=True)
euclidean_dists = np.sqrt(((normalized_items - normalized_user) ** 2).sum(axis=1))
# 综合得分
combined_scores = 0.6 * cos_scores[candidate_indices] + 0.4 * (1 - euclidean_dists)
return candidate_indices[np.argsort(combined_scores)[-10:]] # 返回Top10
5. 高级优化与工程实践
5.1 计算性能优化技巧
- SIMD并行化:使用Intel MKL或OpenBLAS加速矩阵运算
- 近似计算:对于十亿级数据,采用局部敏感哈希(LSH)
- 量化压缩:将float32转为int8,牺牲少量精度换取计算速度
- 分布式计算:Spark实现的大规模相似度计算示例:
python复制from pyspark.ml.linalg import Vectors
from pyspark.sql.functions import udf
from pyspark.sql.types import FloatType
def cosine_udf(v1, v2):
return float(v1.dot(v2) / (v1.norm(2) * v2.norm(2)))
spark.udf.register("cosine_sim", cosine_udf, FloatType())
# 在DataFrame中使用
df.withColumn("similarity", cosine_sim(col("vec1"), col("vec2")))
5.2 算法变种与创新应用
- 软余弦相似度:考虑词与词之间的相关性
- Temporal Cosine Similarity:用于时间序列分析
- Angular Similarity:基于角度直接计算的变种
- Pearson Correlation:中心化后的余弦相似度
在跨模态检索中的创新应用:
python复制def cross_modal_similarity(text_vec, image_vec):
# 文本向量经过BERT编码
text_embedding = bert_model.encode(text_vec)
# 图像向量经过ResNet提取
image_embedding = resnet_model.encode(image_vec)
# 在共享空间计算余弦相似度
return cosine_similarity(text_embedding, image_embedding)
6. 避坑指南与经验总结
6.1 常见陷阱
- 未归一化陷阱:在KNN中使用欧氏距离时,不同尺度特征会导致距离计算偏向大数值特征
- 稀疏数据陷阱:对全零向量计算余弦相似度会产生除零错误
- 高维灾难:当维度超过50时,应考虑降维或改用其他度量
- 语义鸿沟:在视觉搜索中,低层特征(像素)与高层语义(概念)可能不一致
6.2 最佳实践
- 总是先做EDA分析数据分布
- 对欧氏距离考虑特征标准化(Z-score或MinMax)
- 对余弦相似度处理零向量边界情况
- 在推荐系统中加入热度惩罚项
- 使用Elasticsearch等专业引擎处理大规模相似度搜索
6.3 性能对比实验
在我的一个A/B测试中,比较了两种算法在新闻推荐中的效果:
| 指标 | 余弦相似度 | 欧氏距离 | 混合方法 |
|---|---|---|---|
| CTR | 2.8% | 1.5% | 3.2% |
| 停留时长 | 85s | 62s | 92s |
| 多样性 | 0.65 | 0.38 | 0.72 |
| 计算耗时 | 120ms | 150ms | 180ms |
这个实验证实了在文本内容推荐中,余弦相似度的显著优势,同时也展示了混合策略的潜力。