搜索引擎中的关联度算法|青训营笔记

408 阅读8分钟

搜索引擎中的关联度算法|青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记

本文将介绍笔者团队实现搜索引擎项目中实现关联度相关算法中学习到的知识。

1.搜索引擎中的TF-IDF算法

1.1什么是TF-IDF算法?

词频逆词频模型(TF-IDF)的出现主要是为了解决BOW仅考虑了词频而忽略了词的重要性的问题。TF-IDF是基于统计来评估文本中词对于语料库中的一份文本的重要程度的方法。

TF-IDF使得文本内的高频率词语及其在整个文件集合中的低频率文件可以得到高权重的TF-IDF。在TF-IDF中,词语的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降,这从侧面反映TF-IDF倾向于保留重要的词语,过滤掉常见的词语。 举个栗子: 想象你是新手房产中介,摆在面前的有100个楼盘的文档资料,当客户来咨询楼盘的时候,你必须快速定位到某一篇文档,你会怎么做?我们当然会想要使用某种方法将其归类,什么方法合适呢? 你会不会去找每篇文档中的关键词?那些在某篇文档中出现频率很高的词,比如每篇文档中基本都会有谈论租房或新房或二手房这样的字眼,这些高频的字眼其实就代表着这篇文章的属性, 我们大概也能通过这些字眼判断这是不是客户关心的问题。 但是还有一个问题,很多语气词,没有代表意义的词在一篇文档中同样频率很高,比如我,中介,楼盘,和这种词,几乎每篇文档中都会存在,而且提及很多次。 它们很明显,虽然词频高,但是不具有区分力,用上面的方法,这些词也会被误认为很重要。所以学者很聪明,他们知道光看局部信息(某篇文档中的词频TF)会带来统计偏差, 它们就引入了一个全局参数(IDF),来判断这个词在所有文档中,是不是垃圾信息。很明显,我,中介,和这种词在全量文档中就是这样的垃圾信息, 而租房或新房或二手房是在全局下有区分力的词。所以如果我们把局部(TF)和全局(IDF)的信息都整合起来一起看的时候,我们就能快速定位到具体的文档啦。

TF-IDF是一种统计方法,用以评估字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降

TF-IDF主要思想:如果一个单词在该文章中出现的频率(TF)高,并且在其它文章中出现频率很低,则认为该单词具有很好的区分能力,适合用来进行分类。

1)词频(Term Frequency)表示单词在该文章中出现的频率。通常归一化处理,以防止它偏向长的文件。

词频(TF) = 单词在该文章出现次数/当前文章总单词数
在这里插入图片描述2)反问档频率(Inverse Document Frequency)表示某一个特定单词IDF可以由总文章数除以包含该单词的文章数,再将得到的商取对数得到。如果包含该单词的文章越少,则IDF越大,则表明该单词具有很好的文章区分能力。

反问档频率(IDF) = log(语料库中文章总数/(包含该单词的文章数+1))
在这里插入图片描述

question:为什么分母+1?
answer:分母之所以+1是为了避免分母为0.
其实最简单的IDF公式应该是log(语料库中文章总数/(包含该单词的文章数)),但是在一些特殊的时候会出现问题,比如一些生僻词在语料库中没有,这样分母会为0,IDF就没有意义了,为了避免这种情况发生,我们需要对IDF进行一些平滑操作,使得语料库中没有出现的生僻词也可以得到一个合适的IDF值。平滑的方式有很多种,上面的将分母+1是最简单的一种方式,还有一种比较常见的IDF平滑公式是IDF=log(语料库中文章总数+1/(包含该单词的文章数+1)+1)

TF-IDF与一个词在文档中的出现次数成正比, 与包含该词的文档数成反比。

有了IDF的定义,我们就可以计算某一个词语的TF-IDF值:
TF-IDF(x)=TF(x)*IDF(x),其中TF(x)指单词x在当前文章中的词频。
在这里插入图片描述
TF-IDF算法的优点:简单快速,结果比较符合实际情况。

TF-IDF算法的缺点:单纯以"词频"衡量一个词的重要性,不够全面,有时重要的词可能出现次数并不多。而且,这种算法无法体现词的位置信息,出现位置靠前的词与出现位置靠后的词,都被视为重要性相同,这是不正确的。(一种解决方法是,对全文的第一段和每一段的第一句话,给予较大的权重。)

TF-IDF的应用场景:TF-IDF算法可用来提取文档的关键词,关键词在文本聚类、文本分类、文献检索、自动文摘等方面有着重要应用。

1.2 TF-IFD的实现


import (
	"math"
	"sort"
	"time"
)

type TfidfUtil struct{}

type wordTfidf struct {
	word      string
	frequency float64
}

type wordTfidfs []wordTfidf

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

func (wts wordTfidfs) Len() int {
	return len(wts)
}
func (wts wordTfidfs) Less(i, j int) bool {
	return wts[i].frequency < wts[j].frequency
}
func (wts wordTfidfs) Swap(i, j int) {
	wts[i], wts[j] = wts[j], wts[i]
}

func (wts *TfidfUtil) currentTimeMillis() int64 {
	return time.Now().UnixNano() / 1000000
}

func (wts *wordTfidfs) Sort() {
	sort.Sort(wts)
}

// listWords:分词后的矩阵
// 获取词频矩阵  [[key, frequency]]
func (tu *TfidfUtil) fit(listWords [][]string) wordTfidfs {
	docFrequency := make(map[string]float64, 0)
	sumWorlds := 0
	for _, wordList := range listWords {
		for _, v := range wordList {
			docFrequency[v] += 1
			sumWorlds++
		}
	}
	wordTf := make(map[string]float64)
	for k, _ := range docFrequency {
		wordTf[k] = docFrequency[k] / float64(sumWorlds)
	}
	docNum := float64(len(listWords))
	wordIdf := make(map[string]float64)
	wordDoc := make(map[string]float64, 0)
	for k, _ := range docFrequency {
		for _, v := range listWords {
			for _, vs := range v {
				if k == vs {
					wordDoc[k] += 1
					break
				}
			}
		}
	}
	for k, _ := range docFrequency {
		wordIdf[k] = math.Log(docNum / (wordDoc[k] + 1))
	}
	var words wordTfidfs
	for k, _ := range docFrequency {
		words = append(words, wordTfidf{
			word:      k,
			frequency: wordTf[k] * wordIdf[k],
		})
	}
	return words
}


2.余弦相似度计算

2.1余弦相似度原理


对于多个不同的文本或者短文本对话消息要来计算他们之间的相似度如何,一个好的做法就是将这些文本中词语,映射到向量空间,形成文本中文字和向量数据的映射关系,再通过计算几个或者多个不同的向量的差异的大小,来计算文本的相似度。下面介绍一个详细成熟的向量空间余弦相似度方法计算相似度算法。

### 原理

枯燥的原理不如示例来的简单明了,我们将以一个简单的示例来介绍余弦复杂度的原理。现在有下面这样的两句话,从我们直觉感官来看,说的是一模一样的内容,那么我们通过计算其余弦距离来看看其相似度究竟为多少。

S1: "为什么我的眼里常含泪水,因为我对这片土地爱得深沉" S2: "我深沉的爱着这片土地,所以我的眼里常含泪水"


第一步,分词:

我们对上述两段话分词分词并得到下面的词向量:

S1: [为什么 我 的 眼里 常含 泪水 因为 我 对 这片 土地 爱得 深沉 ,] S2: [我 深沉 的 爱 着 这片 土地 所以 我 的 眼里 常含 泪水 ,]


第二步,统计所有词组:

将S1和S2中出现的所有不同词组融合起来,并得到一个词向量超集,如下:

[眼里 这片 为什么 我 的 常含 因为 对 所以 爱得 深沉 爱 着 , 泪水 土地]


第三步,获取词频:

对应上述的超级词向量,我们分别就S1的分词和S2的分词计算其出现频次,并记录:




第四步:计算两个句子向量

句子A:(112111000)

和句子B:(111011111)的向量余弦值来确定两个句子的相似度。
计算相似度公式如下

image.png


计算结果中夹角的余弦值为0.81非常接近于1,所以,上面的句子A和句子B是基本相似的。

由此,我们就得到了文本相似度计算的处理流程是:

    (1)找出两篇文章的关键词;

 (2)每篇文章各取出若干个关键词,合并成一个集合,计算每篇文章对于这个集合中的词的词频

 (3)生成两篇文章各自的词频向量;

 (4)计算两个向量的余弦相似度,值越大就表示越相似。

2.2余弦相似度的实现

func CosineSimilar(srcWords, dstWords []string) float64 {
   // get all words
   allWordsMap := make(map[string]int, 0)
   for _, word := range srcWords {
      if _, found := allWordsMap[word]; !found {
         allWordsMap[word] = 1
      } else {
         allWordsMap[word] += 1
      }
   }
   for _, word := range dstWords {
      if _, found := allWordsMap[word]; !found {
         allWordsMap[word] = 1
      } else {
         allWordsMap[word] += 1
      }
   }

   // stable the sort
   allWordsSlice := make([]string, 0)
   for word, _ := range allWordsMap {
      allWordsSlice = append(allWordsSlice, word)
   }

   // assemble vector
   srcVector := make([]int, len(allWordsSlice))
   dstVector := make([]int, len(allWordsSlice))
   for _, word := range srcWords {
      if index := indexOfSclie(allWordsSlice, word); index != -1 {
         srcVector[index] += 1
      }
   }
   for _, word := range dstWords {
      if index := indexOfSclie(allWordsSlice, word); index != -1 {
         dstVector[index] += 1
      }
   }

   // calc cos
   numerator := float64(0)
   srcSq := 0
   dstSq := 0
   for i, srcCount := range srcVector {
      dstCount := dstVector[i]
      numerator += float64(srcCount * dstCount)
      srcSq += srcCount * srcCount
      dstSq += dstCount * dstCount
   }
   denominator := math.Sqrt(float64(srcSq * dstSq))

   return numerator / denominator
}
```
```