余弦相似性介绍
有些时候,除了找到关键词,我们还希望找到与原文章相似的 其他文章。比如,百度主新闻下方,还提供多条相似的新闻。
为了找出相似的文章,需要用到余弦相似性(cosine similiarity)。下面,我举一个例子来说明,什么是"余弦相似性"。
余弦相似性通过测量两个向量的夹角的余弦值来度量它们之间的相似性。0度角的余弦值是1,而其他任何角度的余弦值都不大于1;并且其最小值是-1。从而两个向量之间的角度的余弦值确定两个向量是否大致指向相同的方向。两个向量有相同的指向时,余弦相似度的值为1;两个向量夹角为90°时,余弦相 似度的值为0;两个向量指向完全相反的方向时,余弦相似度的值为-1。这结果是与向量的长度无关 的,仅仅与向量的指向方向相关。余弦相似度通常用于正空间,因此给出的值为0到1之间。
注意这上下界对任何维度的向量空间中都适用,而且余弦相似性最常用于高维正空间。例如在信息检索中,每个词项被赋予不同的维度,而一个维度由一个向量表示,其各个维度上的值对应于该词项在文档 中出现的频率。余弦相似度因此可以给出两篇文档在其主题方面的相似度。
另外,它通常用于文本挖掘中的文件比较。此外,在数据挖掘领域中,会用到它来度量集群内部的凝聚 力。
定义
两个向量间的余弦值可以通过使用欧几里得点积公式求出:
给定两个属性向量,A和B,其余弦相似性θ由点积和向量长度给出,如下所示:
这里的
分别代表向量A和B的各分量
给出的相似性范围从-1到1:-1意味着两个向量指向的方向正好截然相反,1表示它们的指向是完全相同 的,0通常表示它们之间是独立的,而在这之间的值则表示中间的相似性或相异性。
对于文本匹配,属性向量A和B通常是文档中的词频向量。余弦相似性,可以被看作是在比较过程中把文件长度正规化的方法。
在信息检索的情况下,由于一个词的频率(TF-IDF权)不能为负数,所以这两个文档的余弦相似性范围从0到1。并且,两个词的频率向量之间的角度不能大于90°。
余弦相似度计算
为了简单起见,我们先从句子着手。
句子A:我喜欢看电视,不喜欢看电影。
句子B:我不喜欢看电视,也不喜欢看电影。
请问怎样才能计算上面两句话的相似程度?
基本思路是:如果这两句话的用词越相似,它们的内容就应该越相似。因此,可以从词频入手,计算它 们的相似程度。
1.分词。
句子A:我/喜欢/看/电视,不/喜欢/看/电影。
句子B:我/不/喜欢/看/电视,也/不/喜欢/看/电影。
2.列出所有的词。
我,喜欢,看,电视,电影,不,也。
3.计算词频。
句子A:我 1,喜欢 2,看 2,电视 1,电影 1,不 1,也 0。
句子B:我 1,喜欢 2,看 2,电视 1,电影 1,不 2,也 1。
4.写出词频向量。
句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]
到这里,问题就变成了如何计算这两个向量的相似程度。
我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, ...])出发,指向不同的方向。两条线段 之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来 判断向量的相似程度。夹角越小,就代表越相似。
以二维空间为例,上图的a和b是两个向量,我们要计算它们的夹角θ。余弦定理告诉我们,可以用下面 的公式求得:
假定a向量是[x1, y1],b向量是[x2, y2],那么可以将余弦定理改写成下面的形式:
数学家已经证明,余弦的这种计算方法对n维向量也成立。假定A和B是两个n维向量,A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,则A与B的夹角θ的余弦等于:
使用这个公式,我们就可以得到,句子A与句子B的夹角的余弦。
余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似,这就叫余弦相似性。所以,上面的 句子A和句子B是很相似的,事实上它们的夹角大约为20.3度。 由此,我们就得到了"找出相似文章"的一种算法:
(1)使用TF-IDF算法,找出两篇文章的关键词;
(2)每篇文章各取出若干个关键词(比如20个),合并成一个集合,计算每篇文章对于这个集 合中的词的词频(为了避免文章长度的差异,可以使用相对词频);
(3)生成两篇文章各自的词频向量;
(4)计算两个向量的余弦相似度,值越大就表示越相似。
"余弦相似度"是一种非常有用的算法,只要是计算两个向量的相似程度,都可以采用它。
余弦相似性之Spark实例
Maven依赖
<properties>
<scala.version>2.11.8</scala.version>
<spark.version>2.2.2</spark.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
</dependencies>
CosineSimilarity.scala
package ml
import org.apache.spark.{SparkConf, SparkContext}
/**
* @Author Daniel
* @Description Spark 余弦相似性实现
*
**/
object CosineSimilarity {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("CosineSimilarity")
.setMaster("local[*]")
val sc = new SparkContext(conf)
// 观众数据集
val users = sc.parallelize(Array("Daniel", "James", "Robert", "David", "Thomas"))
// 生成初始数据
val data: Map[String, Map[String, Int]] = generateData()
// 指定目标对象
val name = "Daniel"
// 迭代进行计算
users.foreach(user => {
println(name + "与" + user + "的相似性分数为: " + calSimilarity(data, name, user))
})
val first = users.filter(!_.equals(name)).sortBy(user => calSimilarity(data, name, user), false, 1).first()
println("相似度最高的观众为:" + first)
}
// 数据集
def generateData(): Map[String, Map[String, Int]] = {
val films = Array("Avengers: Endgame", "Avatar", "Titanic", "Star Wars: The Force Awakens", "Avengers: Infinity War")
var data = Map[String, Map[String, Int]]()
val user1 = Map(films(0) -> 5, films(1) -> 3, films(2) -> 1, films(3) -> 0, films(4) -> 1)
val user2 = Map(films(0) -> 1, films(1) -> 2, films(2) -> 2, films(3) -> 1, films(4) -> 4)
val user3 = Map(films(0) -> 2, films(1) -> 1, films(2) -> 0, films(3) -> 1, films(4) -> 4)
val user4 = Map(films(0) -> 3, films(1) -> 2, films(2) -> 0, films(3) -> 5, films(4) -> 3)
val user5 = Map(films(0) -> 5, films(1) -> 3, films(2) -> 1, films(3) -> 1, films(4) -> 2)
// 对人名进行存储
data += ("Daniel" -> user1)
data += ("James" -> user2)
data += ("Robert" -> user3)
data += ("David" -> user4)
data += ("Thomas" -> user5)
data
}
// 计算余弦相似性
def calSimilarity(data: Map[String, Map[String, Int]], user1: String, user2: String): Double = {
// 获得观众1的评分
val user1FilmSource = data(user1).values.toVector
// 获得观众2的评分
val user2FilmSource = data(user2).values.toVector
// 对欧几里得公式分子部分进行计算
val member = user1FilmSource.zip(user2FilmSource).map(num => num._1 * num._2).sum.toDouble
// 求出分母中第一个变量的值
val temp1 = math.sqrt(user1FilmSource.map(num => {
math.pow(num, 2)
}).sum)
// 求出分母中第二个变量的值
val temp2 = math.sqrt(user2FilmSource.map(num => {
math.pow(num, 2)
}).sum)
// 求出分母
val denominator = temp1 * temp2
member / denominator
}
}
结果
Daniel与Daniel的相似性分数为: 1.0
Daniel与David的相似性分数为: 0.5834599659915783
Daniel与James的相似性分数为: 0.5556623828915215
Daniel与Robert的相似性分数为: 0.6040686963408962
Daniel与Thomas的相似性分数为: 0.9750356118852502
相似度最高的观众为:Thomas