cosine_similarity ( x , y ) = cos ⟨ x , y ⟩ = x T y ∥ x ∥ ∥ y ∥ \text{ cosine\_similarity}\left( \mathbf{x},\mathbf{y}\right)=\cos \left\langle \mathbf{x},\mathbf{y} \right\rangle=\frac{\mathbf{x}^T\mathbf{y}}{\|\mathbf{x}\|\|\mathbf{y}\|} cosine_similarity(x,y)=cos⟨x,y⟩=∥x∥∥y∥xTy
其中 ⟨ x , y ⟩ \left\langle \mathbf{x},\mathbf{y} \right\rangle ⟨x,y⟩表示 x \mathbf{x} x和 y \mathbf{y} y的角度
接下来的问题是考虑矩阵 A = ( a 1 T a 2 T ⋮ a m T ) , a i ∈ R k , B = ( b 1 T b 2 T ⋮ b n T ) , b i ∈ R k \mathbf{A}=\begin{pmatrix} \mathbf{a}_1^T\\ \mathbf{a}_2^T\\ \vdots\\ \mathbf{a}_m^T \end{pmatrix},\mathbf{a}_i\in\mathbb{R}^k,\mathbf{B}=\begin{pmatrix} \mathbf{b}_1^T\\ \mathbf{b}_2^T\\ \vdots\\ \mathbf{b}_n^T \end{pmatrix},\mathbf{b}_i\in\mathbb{R}^k A=⎝⎜⎜⎜⎛a1Ta2T⋮amT⎠⎟⎟⎟⎞,ai∈Rk,B=⎝⎜⎜⎜⎛b1Tb2T⋮bnT⎠⎟⎟⎟⎞,bi∈Rk
计算余弦相似度矩阵 C ∈ R m × n \mathbf{C}\in\mathbb{R}^{m\times n} C∈Rm×n,
其中 c i j = cosine_similarity ( a i , b j ) c_{ij}=\text{ cosine\_similarity}\left( \mathbf{a}_i,\mathbf{b}_j\right) cij= cosine_similarity(ai,bj)
先单位化,这样可以不用单位化
这里要注意防止分母为0,所以有一个eps
torch.einsum就是一种矩阵乘法
def cosine_similarity(a, b, eps=1e-7):
"""
:param a: m*k
:param b: n*k
:return: m*n cosine similarity
"""
a = a / torch.max(torch.norm(a, p=2, dim=-1, keepdim=True), torch.tensor(eps))
b = b / torch.max(torch.norm(b, p=2, dim=-1, keepdim=True), torch.tensor(eps))
return torch.einsum('ni,mi->nm', a, b)
其实就是最后一步直接矩阵乘法
def cosine_similarity(a, b, eps=1e-7):
"""
:param a: m*k
:param b: n*k
:return: m*n cosine similarity
"""
a = a / torch.max(torch.norm(a, p=2, dim=-1, keepdim=True), torch.tensor(eps))
b = b / torch.max(torch.norm(b, p=2, dim=-1, keepdim=True), torch.tensor(eps))
return torch.mm(a, b.T)
from torch.nn.functional import cosine_similarity
cosine_similarity(a.unsqueeze(1), b.unsqueeze(0), dim=-1)
可以查看这里https://pytorch.org/docs/stable/generated/torch.nn.CosineSimilarity.html?highlight=cosinesimilarity#torch.nn.CosineSimilarity
A ∈ R m × k \mathbf{A}\in\mathbb{R}^{m\times k} A∈Rm×k,经过.unsqueeze(1)会变成 m × 1 × k m\times 1\times k m×1×k的矩阵
B ∈ R n × k \mathbf{B}\in\mathbb{R}^{n\times k} B∈Rn×k,经过.unsqueeze(0)会变成 1 × n × k 1\times n\times k 1×n×k的矩阵
dim=-1,这意味着最后会变成一个 m × n m\times n m×n的矩阵
因为形状不一样,所以会广播成 m × n × k m\times n\times k m×n×k的矩阵
举个例子
将 3 × 2 3\times 2 3×2的矩阵广播成 3 × 4 × 2 3\times 4\times 2 3×4×2
a经过unsqueeze(1)会变成 3 × 1 × 2 3\times 1\times 2 3×1×2的矩阵
最后的+zeros是为了触发广播
最后计算余弦相似度的时候
a[0,0]得到的是一个2个元素的向量
就是
a[0,0]和b[0,0]计算余弦相似度
a[0,0]和b[0,1]计算余弦相似度
…
a[0,1]和b[0,0]计算余弦相似度
a[0,1]和b[0,1]计算余弦相似度
…
a[m,n]和b[m,n]计算余弦相似度
得到余弦相似度矩阵后计算 arccos \arccos arccos,对应pytorch的torch.acos
需要注意的是,pytorch的acos有点问题https://github.com/pytorch/pytorch/issues/8069
如果数值接近-1或1,则会返回nan
解决方法是 ± e p s \pm eps ±eps
eps=1e-7
matrix_cos=matrix_cos.clamp(min=-1+eps,max=1+eps)
matrix_angle=torch.acos(matrix_cos)