推荐系统协同过滤中Python余弦相似度函数的范数计算原理问询
先把你提到的这段实现代码贴出来:
import numpy as np def similarity(ratings, kind='user', epsilon=1e-9): if kind == 'user' : sim = ratings.dot(ratings.T) elif (kind=='item'): sim = ratings.T.dot(ratings) + epsilon norms = np.array([np.sqrt(np.diagonal(sim))]) return (sim / norms / norms.T )
接下来我一步步给你拆解范数计算背后的数学逻辑,其实核心就是紧扣余弦相似度的定义:
1. 先回忆余弦相似度的核心公式
两个向量 $\boldsymbol{u}$ 和 $\boldsymbol{v}$ 的余弦相似度,本质是它们的点积除以两个向量的L2范数(欧几里得范数)的乘积,公式是:
$$
\text{cosine similarity}(\boldsymbol{u}, \boldsymbol{v}) = \frac{\boldsymbol{u} \cdot \boldsymbol{v}}{||\boldsymbol{u}|| \times ||\boldsymbol{v}||}
$$
其中 $||\boldsymbol{u}|| = \sqrt{u_1^2 + u_2^2 + ... + u_n^2}$,也就是向量自身点积的平方根:$||\boldsymbol{u}|| = \sqrt{\boldsymbol{u} \cdot \boldsymbol{u}}$。
2. 先搞懂sim矩阵的意义
- 当
kind='user'时,ratings是用户-物品评分矩阵(假设形状是$U \times I$,U是用户数,I是物品数),ratings.dot(ratings.T)会生成一个$U \times U$的矩阵,其中第$(i,j)$个元素就是用户i的评分向量和用户j的评分向量的点积 $\boldsymbol{u}_i \cdot \boldsymbol{u}_j$。 - 当
kind='item'时,ratings.T.dot(ratings)生成$I \times I$的矩阵,第$(m,n)$个元素是物品m的评分向量和物品n的评分向量的点积 $\boldsymbol{v}_m \cdot \boldsymbol{v}_n$。
3. 范数norms的计算逻辑
np.diagonal(sim)会提取sim矩阵的对角线元素:
- 对用户模式来说,对角线元素是$\boldsymbol{u}_i \cdot \boldsymbol{u}_i$,也就是用户i评分向量自身的点积,这正好等于$||\boldsymbol{u}_i||^2$(L2范数的平方)。
- 对物品模式同理,对角线元素是$\boldsymbol{v}_m \cdot \boldsymbol{v}_m = ||\boldsymbol{v}_m||^2$。
然后np.sqrt(...)把这些平方值开根号,就得到了每个用户(或物品)向量的L2范数。最后用np.array([...])把结果转成行向量(形状是$1 \times U$或$1 \times I$),方便后续的广播运算。
4. 最后一步的广播除法是关键
sim / norms / norms.T利用了Numpy的广播机制,完美对应余弦相似度的计算:
- 第一步
sim / norms:把$U \times U$的sim矩阵的每一行,都除以行向量norms中对应的元素(也就是每个用户i的范数$||\boldsymbol{u}_i||$),此时矩阵的$(i,j)$元素变成$\frac{\boldsymbol{u}_i \cdot \boldsymbol{u}_j}{||\boldsymbol{u}_i||}$。 - 第二步再除以
norms.T(转成列向量,形状$U \times 1$):把每一列再除以对应的用户j的范数$||\boldsymbol{u}_j||$,最终$(i,j)$元素就变成$\frac{\boldsymbol{u}_i \cdot \boldsymbol{u}_j}{||\boldsymbol{u}_i|| \times ||\boldsymbol{u}_j||}$,完全符合余弦相似度的公式!
另外提一句,物品模式里加了epsilon是为了避免某个物品没有任何评分时,范数为0导致除零错误,这个小细节是为了提升代码的鲁棒性。
内容的提问来源于stack exchange,提问作者Nithin Varghese

