10

前两篇可以直接看我的专栏
或者
文本相似性计算(一)
文本相似性计算(二)
前面说了两篇了,分别介绍了TFIDF和向量空间的相关东西,然后介绍了主题模型,这一篇我们就来试试这两个东西。词向量就不在这篇试了,词向量和这两个关系不大,不好对比,不过我最后也给出了代码。

0. 工具准备

工欲善其事,必先利其器,那么我们先来利其器,这里我们使用的是python的gensim工具包,地址是:https://radimrehurek.com/gensim/index.html,这个工具包很强大,我就不一一介绍了,反正我们需要的功能都有,而且我们用得很简单,它还可以分布式部署,感兴趣可以去官网看具体介绍。

为什么不自己写?这个问题....呵呵.....呵呵....我写不出来.....

至于安装,需要先安装python 2.6以上(废话),NumPy 1.3以上,SciPy 0.7以上,后两个是python的科学计算的包。

easy_install很容易搞定,这里就不废话了,windows上安装可能有点困难,但我很久没用过windows了,我电脑上安装很轻松,三四个命令搞定,可以去看gensim的官方文档,上面也有怎么安装,如果你装都装不上,那就google,百度,总有解决办法。

除了gensim,还有个分词的包需要装一下,就是jieba分词,这个也很容易装。

1. 数据准备

数据准备可是个技术活,我的职业操守很高,没有用公司的数据,那只能自己找数据了,如果直接找网上的语料,显得太Low了。于是我自己爬了一些数据。

首先,我瞄准了目前全国最火的全栈技术网站(就是SegmentFault啦),然后瞄准了一个汽车网站,于是开始爬数据,自己写了个爬虫开始爬数据,恩,我的爬虫我觉得还可以,调度器+爬取器组成,爬取器插件化了,可以使用任意语言做编写,甚至可以直接对接chrome爬取纯JS单页面网站爬取,也支持代理池,如果大家感兴趣我也可以说说爬虫相关的东西,分布式的哦,可以随便加机器增加爬取能力。

这里说个小故事,爬取沙发站(SegmentFault.com)的时候,我先撸了一遍所有文章的列表页,本来我就想把标题爬下来就行了,结果发现SF站没有反应,瞬间就爬下来了,于是大意了,在没挂代理池的情况下,直接开始撸文章的详情页,想把文章都爬下来,结果卧槽,爬到6000多篇的时候把我的IP给封了。于是。。我现在只能挂VPN上SF站,挂VPN啊!!!去新加坡打个转来上一个杭州的网站!!如果有管理员或者运营人员看到这篇文章,麻烦给解个封吧。我真没有恶意,不然也不写在这了。

并且,作为一个老程序员,还是个后端人员,跑到一个90后遍地的前端为主的技术社区写后端东西,看的人本来就不多,应该在oschina,cnblog,csdn这种地方写文章的。这是一种什么精神??国际主义精神啊!因为SF的配色我实在是太喜欢了,而且markdown的渲染效果也很好看,并且手机上看效果也非常赞,于是就来了,看的人少就少吧,反正主要给自己看。。。。

好了,八卦完了,爬了两个网站,可以开始干活了,爬两个类型的网站是为了说明后面LDA主题模型,大家就有个认识了。

2. 数据清理

数据爬下来后,要做的就是数据清洗工作了,我之前有一篇搞机器学习的技能说了,数据的清理是一个算法工程师的必备技能,如果没有好的数据,算法怎么好都没用。

拿到数据以后,写个脚本

  • 首先把标题,作者,时间之类的提取出来,通过正则也好,xpath也好,都很容易把这些东西提取出来。

  • 然后把html标签干掉,一堆正则就行了,剩下的基本上就是正文了,另外,SF站的东西还特殊处理了一下,把中的内容干掉了,一堆代码对我来说没什么用。

  • 最后,把标点符号干掉,把特殊符号干掉,调整一下格式,最后的每一篇文章都变成下面的样子

ID(实际上是url)[TAB]TITLE(标题)[TAB]CONTENT(文章详情)

一共有11628篇文章,其中汽车类大约6000,技术类(SF站)大约6000,好了,数据也基本上清洗好了。

4. 训练数据

都觉得这一节才是重点,其实有jieba分词和gensim以后,代码非常简单,不超过50行,我们来一步一步玩。

4.1 分词--建立词典--准备数字语料

分词是基础,首先进行分词

from gensim import corpora,models,similarities,utils
import jieba
import jieba.posseg as pseg
jieba.load_userdict( "user_dic.txt" ) #载入自定义的词典,主要有一些计算机词汇和汽车型号的词汇
#定义原始语料集合
train_set=[]
f=open("./data/all.txt")
lines=f.readlines()
for line in lines:
    content = (line.lower()).split("\t")[2] + (line.lower()).split("\t")[1]
    #切词,etl函数用于去掉无用的符号,cut_all表示非全切分
    word_list = filter(lambda x: len(x)>0,map(etl,jieba.cut(content,cut_all=False)))
    train_set.append(word_list)
f.close()

得到的tain_set就是原始语料了,然后对这些语料导入到词典中,建立一个词典。

#生成字典
dictionary = corpora.Dictionary(train_set)
#去除极低频的杂质词
dictionary.filter_extremes(no_below=1,no_above=1,keep_n=None)
#将词典保存下来,方便后续使用
dictionary.save(output + "all.dic")

将语料导入词典后,每个词实际上就已经被编号成1,2,3....这种编号了,这是向量化的第一步,然后把词典保存下来。
然后生成数字语料

corpus = [dictionary.doc2bow(text) for text in train_set]

这一句表示把每一条原始数据向量化成编号,这样以后,corpus这个变量是个二维数据,每一行表示一个文档的每个词的编号和词频,每一行像这样

[(1,2),(2,4),(5,2)....] 表示编号为1的词出现了2次,编号为2的词出现了4次....

OK,前期准备OK了,原始文章通过切词-->建立词典-->生成语料后已经被我们数字化了,后面就简单了。

4.1 TFIDF模型

有了数字语料以后,我们可以生成一个TFIDF模型

#使用数字语料生成TFIDF模型
tfidfModel = models.TfidfModel(corpus)
#存储tfidfModel
tfidfModel.save(output + "allTFIDF.mdl")

这一句是关键,我们用了原始的数字语料,生成了一个TFIDF模型,这个模型能干什么呢?gensim重载了[]操作符,我们可以用类似[(1,2),(2,4),(5,2)....]的原始向量传进去,变成一个tfidf的向量,像这样[(1,0.98),(2,0.23),(5,0.56)....],这样就说明编号1的词的重要性比后面两个词都要大,这个向量可以作为后面LDA的原始向量输入。

然后我们把所有的语料都TFIDF向量化,并作为一个索引数据存起来方便以后查找的时候使用

#把全部语料向量化成TFIDF模式,这个tfidfModel可以传入二维数组
tfidfVectors = tfidfModel[corpus]
#建立索引并保存
indexTfidf = similarities.MatrixSimilarity(tfidfVectors)
indexTfidf.save(output + "allTFIDF.idx")

好了,TFIDF部分完了,先记下来,我们生成了一个模型数据(allTFIDF.mdl),生成了一份全部语料的TFIDF向量的索引数据(allTFIDF.idx),加上上面的词典数据(all.dic),我们现在有三份数据了,后面再说怎么用,现在先继续LDA部分。

4.2 LDA模型

LDA上一篇讲了那么多,在gensim看来就是下面几行代码,而且使用了传说中的机器学习哦。只能说gensim的代码封装得太简洁了。

#通过TFIDF向量生成LDA模型,id2word表示编号的对应词典,num_topics表示主题数,我们这里设定的50,主题太多时间受不了。
lda = models.LdaModel(tfidfVectors, id2word=dictionary, num_topics=50)
#把模型保存下来
lda.save(output + "allLDA50Topic.mdl")
#把所有TFIDF向量变成LDA的向量
corpus_lda = lda[tfidfVectors]
#建立索引,把LDA数据保存下来
indexLDA = similarities.MatrixSimilarity(corpus_lda)
indexLDA.save(output + "allLDA50Topic.idx")

虽然只有这三步,但是还是挺耗时的,在log打开的情况下可以看到处理过程,我随便截取了几个,像下面一样,很明显,前面几个主题都和汽车相关,后面几个主题都和技术相关,看样子还算比较靠谱的。

#38 (0.020): 0.003*新奇 + 0.003*骏 + 0.002*途安 + 0.002*配备 + 0.002*都市 + 0.001*除 + 0.001*昂科威
#27 (0.020): 0.003*配置 + 0.003*内饰 + 0.003*车型 + 0.002*气囊 + 0.002*瑞风 + 0.002*万元 + 0.002*逸致 +
#0 (0.020): 0.004*奔腾 + 0.003*加速 + 0.003*嘉年华 + 0.002*油门 + 0.002*爱丽舍 + 0.002*秒
#49 (0.020): 0.004*瑞虎 + 0.004*绅宝 + 0.004*欧诺 + 0.002*雷克萨斯 + 0.002*车型 + 0.002*乐途 
#26 (0.020): 0.011*列表 + 0.009*流 + 0.007*快捷键 + 0.006*崩溃 + 0.002*大神 + 0.002*混淆 + 0.002*邮箱
#21 (0.020): 0.035*命令 + 0.018*浏览器 + 0.007*第三方 + 0.007*安装 + 0.006*控制台 
topic #25 (0.020): 0.064*文件 + 0.004*约束 + 0.004*练习 + 0.003*复制到 + 0.003*就行了 + 0.003*反编译

好了,LDA部分也完了,又多了两个文件allLDA50Topic.mdlallLDA50Topic.idx,加上前面的3个,一共5个文件了,OK,休息一下,喝杯可乐,继续下一步。

5. 验证结果

好了,第四部分中不知不觉我们已经使用机器学习这么高端的东西了,那现在要验证一下这么高端的东西到底效果如何了。

前面的TFIDF和LDA我们都保存了模型和向量数据,那么我们就用两篇新的文章,来看看和这篇文章最相似的文章都有哪些来验证这两个模型靠谱不靠谱吧。

我随便打开一个汽车网站,选了一篇汽车的文章(宝马的评测),再找了我之前的一篇技术的文章(讲搜索引擎的),并且只随机截取了文章的一段进行测试。

看开头这应该是一篇为全新宝马X1 Li(下文简称新X1)洗地的文章,我想很多宝马死忠、车神也已经准备移步评论........

一般情况下,搜索引擎默认会认为索引是不会有太大的变化的,所以把索引分为全量索引和增量索引两部分,全量索引一般是以天.......

好,文章选好了,先载入之前保存的数据文件

#载入字典
dictionary = corpora.Dictionary.load(output + "all.dic")
#载入TFIDF模型和索引
tfidfModel = models.TfidfModel.load(output+"allTFIDF.mdl")
indexTfidf = similarities.MatrixSimilarity.load(output + "allTFIDF.idx")
#载入LDA模型和索引
ldaModel = models.LdaModel.load(output + "allLDA50Topic.mdl")
indexLDA = similarities.MatrixSimilarity.load(output + "allLDA50Topic.idx")

然后把测试数据进行切词TFIDF向量化找相似LDA向量化找相似

#query就是测试数据,先切词
query_bow = dictionary.doc2bow(filter(lambda x: len(x)>0,map(etl,jieba.cut(query,cut_all=False))))
#使用TFIDF模型向量化
tfidfvect = tfidfModel[query_bow]
#然后LDA向量化,因为我们训练时的LDA是在TFIDF基础上做的,所以用itidfvect再向量化一次
ldavec = ldaModel[tfidfvect]
#TFIDF相似性
simstfidf = indexTfidf[tfidfvect]
#LDA相似性
simlda = indexLDA[ldavec]

好了,结束,所有代码就这么些了。太简单了。。。。我们来看看效果。

6 输出结果

我们先看TFIDF的结果

  • 汽车的测试文章TFIDF结果(前10结果中随机选3个)

优惠购车推荐宝马x3优惠3.5-7万元
保时捷macan竞争力分析宝马x3
宝马2014年新车展望多达十余款新车

  • 技术的测试文章TFIDF结果(前10结果中随机选3个)

用golang写一个搜索引擎(0x06)
索引那点事
[搜索引擎] sphinx 的介绍和原理探索

很明显,结果基本都比较靠谱,第一个基本是说宝马车的,第二个基本都在说搜索引擎和索引。

我们再看看LDA的结果,LDA的主要功能是文本分类而不是关键词的匹配,就是看测试文章分类分得对不对,我们这里基本上是两类文章,一类技术文章,一类汽车文章,所以我们通过找和测试文章最相似的文章,然后看看找出来最相似的文章是不是正好都是技术类的或者汽车类的,如果是,表示模型比较好。

  • 汽车的测试文章LDA结果(前10结果中随机选3个)

编辑心中最美中级车一汽-大众新cc
25万时尚品质4款豪华紧凑车之奔驰a级
iphone手机html5上传图片方向问题解决

  • 技术的测试文章LDA结果(前10结果中随机选3个)

java 多线程核心技术梳理(附源码)
springsession原理解析
并发中的锁文件模式

从结果看,基本比较靠谱,但汽车那个出现了一个badcaseiphone手机html5上传图片方向问题解决,这是篇技术文章,但是出现在了汽车类上面。

7. 结果分析

我们来分析一下这个结果,对于TFIDF模型,在现有数据集(12000篇文章)的情况下,推荐结果强相关,让人觉得推荐结果很靠谱,这也是TFIDF这种算法简单有效的地方,他把文章中的关键词很好的提取出来了,所以推荐的结果让人觉得强相关,但是他也有自己的问题。

  • 对于比较短的文章(比如微博这类的),由于文本太短了,TFIDF比较难提取出重要的关键词或者提取得不对,导致推荐结果不靠谱。

  • 单纯以词频来说明这个词的重要性感觉不全面,比如这篇文章,人类来看的话应该是文本相似性最重要,但有可能按TFIDF算出来是模型这个词最重要。
    对于纯文本的推荐系统来说,这种文本相关性的推荐可能比较适合垂直类的网站,比如像SegmentFault这种,看某篇文章的人很可能希望看到类似的文章,更深入的了解这个领域,这种算法比较靠谱,不过据我观察,SegmentFault是使用的标签推荐,这种推荐效果更好,但人为因素更多点,要是我写文章的时候随便打标签就比较麻烦了。

再来看看LDA模型,LDA主要用在文本聚类上,而且他的基础是主题,如果把他作为推荐系统的算法来使用,要看具体场景,他的推荐结果在数据样本不太够的情况下,可能看上去不太靠谱(即便样本大可能也看上去不太好),显得粒度很粗,但正因为很粗,所以比较适合做内容发现,比如我对数码新闻感兴趣,这种感兴趣不仅仅是只对iphone感兴趣,只要是数码这个主题的我都感兴趣,所以用LDA可以很好的给我推荐数码这个主题下的东西,这比正在看iphone的文章,下面全是iphone的文章要靠谱多了。

LDA出现上一节的哪种badcase的时候怎么办呢?因为基本不太可能改模型,那么只能从几个方面入手。

  • 如果只是偶尔的一两个,可以选择忍了。

  • 如果多的话,那只能先调一调主题个数,然后LDA里面有些个参数可以调调(算法工程师的价值所在啊)

  • 还有一条路子就是把输入的数据尽可能的清洗干净,把无用的杂质去掉(算法工程师必备技能耐心和细心
    所以,不同的模型对于不同的场景是很重要的,根据你的场景选择合适的模型才能达到合适的效果。

8. 写在后面的话

这篇文章只是一个文本相似性的最最基本的文章,可以最直观的了解一下TFIDF模型和LDA模型,同时,也使用了目前最热的机器学习技术哦。

其实,像LDA,以及word2vec这种模型,已经是被数学抽象得很强的模型了,和实际场景基本上已经脱离了,已经完全数学化了,所以其实不一定要用在文本处理上,在流量分析,用户行为分析上一样有用,这就是算法工程师要想的事情,一个好的算法如何用在现有的场景中。

试想一下,如果我们想给我们的用户分个类,看看哪些用户兴趣比较相似。我们其实可以这么来做:

  • 首先,如果我们有一堆用户的浏览行为数据,每一条数据记录了用户点击某个链接,或者点击了某个按钮。

  • 把这些浏览行为按照用户维度进行合并,那么新数据中每一条数据就是一个用户的操作记录,按顺序就是他几点几分有什么行为。类似于用户A :[浏览了a页面,点击了b按钮,浏览了c页面....]

  • 好,如果我们发挥算法工程师的必备技能之一----想象力,那么我们把每个用户的行为当成一篇文章,每个行为数据当成一个词语,然后使用LDA.....呵呵
    这样算出来的主题,是不是就是用户的类别呢?有相似行为数据的用户会出现在相同的主题下,那么这样就把这些用户分类了,那么是不是可以理解为同样类别的下的用户有着相似的爱好呢?如果你觉得可行,可以拿你公司的用户数据试试,看看效果好不好:)

9. 后面的后面

最后,所有代码在github上,点击这里可以看得到,代码相当简单,整个不超过200行,核心的就是我上面列的那些,代码中也有word2vec的代码和使用,这篇文章就没提了,另外,爬取的文章就不放上来了,太大了。

如果大家想要语料自己玩,可以上wiki百科,他们开放了他们的所有数据给全世界做语料分析,其中有中文的,地址是:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2,但维基上中文语料并不多,中文语料多的是百度百科,但看看百度百科,呵呵,不但不开放,防爬虫跟防贼一样,呵呵,不过我也给大家个地址,100G的百度百科原始页面:http://pan.baidu.com/s/1i3wvfil ,接头密码:neqs,由亚洲第二爬虫天王梁斌penny友情提供。

好了,今天的文章有点长,就到这了,后面会把算法部分放一放,最近工作太忙,等这一段结束了,我会再说说算法部分,因为现在工作中会有一些比较好玩的算法要用,接下来的文章会主要写写系统架构方面的东西了,另外我自己的那个搜索引擎目前太忙没时间整,也要等一小段时间了,不好意思:)但放心,不会有头无尾啦。


欢迎关注我的公众号,主要聊聊搜索,推荐,广告技术,还有瞎扯。。文章会在这里首先发出来:)扫描或者搜索微信号XJJ267或者搜索西加加语言就行

图片描述


V典V
2.1k 声望1.4k 粉丝

不以物喜,不以己悲