
什么是SQL注入?理解、风险与防范技巧
在自然语言处理、推荐系统和数据科学中,计算向量相似度是我们最常做的任务之一。无论是比较文档、用户画像还是商品特征,我们都需要一个可靠的相似度指标。然而,一个常见但容易被忽略的操作——向量归一化(Normalization)——会彻底改变相似度的计算结果,用对事半功倍,用错南辕北辙。
今天,我们就通过几个真实的案例和Python代码示例,来彻底讲清楚:什么情况下必须归一化,什么情况下最好保持原样?
归一化,在这里特指将向量转换为单位向量(模长为1的向量)。这个过程剥离了向量的"长度",只保留其"方向"。
最常用的相似度是余弦相似度(Cosine Similarity),其公式为:
cosine_sim(A, B) = (A · B) / (||A|| * ||B||)
如果你先将向量A和B归一化为单位向量A’和B’,再计算它们的点积,那么:
A' · B' = cosine_sim(A, B)
此时,点积(Dot Product)就等于余弦相似度。
核心影响:归一化让相似度计算从关注"方向和强度"变成了只关注"纯方向"。
不归一化 | 归一化后 | |
---|---|---|
关注点 | 方向 + 强度(总量) | 纯方向(比例、形态) |
模长作用 | 是关键因素,模长大的向量占主导 | 被完全消除,所有向量平等 |
好比 | 比较两首歌的绝对音量和音色 | 只比较两首歌的音色,把音量调成一致 |
理解了这一点,我们就可以通过案例和代码来看看如何选择了。
场景:文档相似度计算
假设你有两篇文档:
我们用TF-IDF模型将它们转化为向量。
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
# 示例文档
documents = [
"machine learning deep neural network artificial intelligence", # 长文档A
"ai artificial intelligence" # 短文档B
]
# 计算TF-IDF向量
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documents)
# 转换为稠密数组以便演示
vectors = tfidf_matrix.toarray()
print("原始TF-IDF向量:")
print("文档A:", vectors[0])
print("文档B:", vectors[1])
print()
# 计算模长
norm_a = np.linalg.norm(vectors[0])
norm_b = np.linalg.norm(vectors[1])
print(f"文档A模长: {norm_a:.4f}")
print(f"文档B模长: {norm_b:.4f}")
print()
# 计算未归一化的余弦相似度
dot_product = np.dot(vectors[0], vectors[1])
cosine_sim_unnormalized = dot_product / (norm_a * norm_b)
print(f"未归一化余弦相似度: {cosine_sim_unnormalized:.4f}")
# 归一化向量
normalized_a = vectors[0] / norm_a
normalized_b = vectors[1] / norm_b
print("\n归一化后的向量:")
print("文档A:", normalized_a)
print("文档B:", normalized_b)
print()
# 计算归一化后的余弦相似度(即点积)
cosine_sim_normalized = np.dot(normalized_a, normalized_b)
print(f"归一化后余弦相似度: {cosine_sim_normalized:.4f}")
# 验证:直接使用余弦相似度公式应与归一化后点积结果一致
cosine_sim_direct = dot_product / (np.linalg.norm(vectors[0]) * np.linalg.norm(vectors[1]))
print(f"直接计算余弦相似度: {cosine_sim_direct:.4f}")
不归一化的问题:
长文档A的向量模长(A
)会远大于短文档B的模长(B
)。即使两篇文章主题高度相关,由于分母A|| * ||B
巨大,计算出的余弦相似度值也会被拉低。这显然不合理——我们不能因为一篇文章写得长,就说它和短文不相似。
归一化的效果:
归一化将两个向量的模长统一为1。这时,相似度计算只关心词频分布的相对比例。既然两篇文章都重点讨论了"人工智能"、"机器学习"等话题(即向量方向相近),那么它们的相似度就会很高,完美忽略了文档长度的干扰。
✅ 结论:在文本处理、图像颜色直方图比较、用户兴趣偏好分析等场景中,模长(文档长度、图片亮度、用户活跃度)是干扰噪声,我们真正关心的是分布形态。此时,必须归一化。
场景:推荐系统中的用户评分预测
假设在一个电影评分系统(1-5分)中,有两个用户:
# 用户评分向量示例
user_a_ratings = np.array([5, 3, 2, 1]) # 苛刻用户,但对某部电影给出5星
user_b_ratings = np.array([5, 4, 4, 5]) # 宽容用户,对同一部电影也给出5星
print("用户评分向量:")
print("用户A(苛刻):", user_a_ratings)
print("用户B(宽容):", user_b_ratings)
print()
# 计算模长
norm_a = np.linalg.norm(user_a_ratings)
norm_b = np.linalg.norm(user_b_ratings)
print(f"用户A评分向量模长: {norm_a:.4f}")
print(f"用户B评分向量模长: {norm_b:.4f}")
print()
# 计算未归一化的点积相似度
dot_product_unnormalized = np.dot(user_a_ratings, user_b_ratings)
print(f"未归一化点积相似度: {dot_product_unnormalized:.4f}")
# 计算未归一化的余弦相似度
cosine_sim_unnormalized = dot_product_unnormalized / (norm_a * norm_b)
print(f"未归一化余弦相似度: {cosine_sim_unnormalized:.4f}")
print()
# 归一化向量
normalized_a = user_a_ratings / norm_a
normalized_b = user_b_ratings / norm_b
print("归一化后的评分向量:")
print("用户A:", normalized_a)
print("用户B:", normalized_b)
print()
# 计算归一化后的点积相似度
dot_product_normalized = np.dot(normalized_a, normalized_b)
print(f"归一化后点积相似度: {dot_product_normalized:.4f}")
# 分析差异
print("\n分析:")
print("归一化前,用户A的5星是绝对值5,用户B的5星也是绝对值5")
print("归一化后,用户A的5星被缩放为{:.4f},用户B的5星被缩放为{:.4f}".format(
normalized_a[0], normalized_b[0]))
print("这意味着用户A的5星在其评分体系中占比更大,但失去了'绝对值5星'的语义")
归一化的问题:
如果我们对用户评分向量进行归一化,会发生一件微妙的事情:用户A的5星是他评分体系中的最高分,而用户B的5星只是比他的平均分略高一点。归一化拉平了他们的评分尺度,模糊了"他们都给出了绝对最高分"这一强烈信号。系统可能会错误地认为用户A对《星际穿越》的喜爱程度远超用户B。
不归一化的效果:
保持向量的原始模长,点积运算会同时考虑到评分方向和评分绝对值。两个5星是绝对相等的,这表明他们对《星际穿越》的喜爱是同等强烈的。这对于基于用户的协同过滤(User-CF)算法至关重要。
✅ 结论:在推荐系统、经济数据对比、信号强度分析等场景中,向量的模长(绝对评分、经济总量、信号振幅)是核心比较信息的一部分。此时,不应使用归一化。
下次当你犹豫是否要归一化时,问自己这三个问题:
模长差异是噪声还是信息?
我想找"同类"还是"克隆"?
我的算法核心是什么?
最后的建议:
在没有明确先验知识时,先尝试归一化通常是更安全的选择,因为它能消除很多量级带来的偏差。但最好的方法永远是基于你的业务目标,构建一个验证集,同时试验两种方案,让最终的模型效果告诉你答案。