(一)余弦相似度、向量空间模型
1、相似度
举个例子,比如“歌神”和“张学友”,在有些场景上有相似度,有些场景却完全没关系。在文本相似度的场景中无论是垂类--某一行业如旅游专业推荐的网站( 参考文章),还是综合性网站都没有四海皆准的度量标准。所以我们都需要在产品初期阶段做一个小范围的测试(abTest),查看小范围测试的留存率,如果不做小范围测试,如果留存率低,那么导致见光死。• 相似度度量(从字面上和语义上两方面来度量):计算个体间相似程度(得到一个分数,通过分数来度量相似度,范围[0,1])
-文本角度(TF-IDF、LCS):这件衣服真好看,这件衣服真难看
-语义角度(协同过滤):真好玩,真有趣
-文本+语义角度(word2vec)
• 相似度值越小,距离越大,相似度值越大,距离越小
• 最常用—— 余弦相似度
– 一个向量空间中两个向量夹角的余弦值作为衡量两个个体之间差异的大小
– 余弦值接近1,夹角趋于0,表明两个向量越相似
2、例子
计算词频:如果一个词语出现一次就计分为1,0次计分为0,2次计分为2.
3、处理流程
• 得到了文本相似度计算的处理流程是:– 找出两篇文章的关键词(怎样区分词语是否为关键词?);
– 每篇文章各取出若干个关键词,合并成一个集合,计算每篇文章对于这个集合中的词的词频
– 生成两篇文章各自的词频向量;
– 计算两个向量的余弦相似度,值越大就表示越相似。
(二)TFIDF
1、词频——TF
• 假设:如果一个词很重要,应该会在文章中多次出现• 词频——TF(Term Frequency):一个词在文章中出现的次数
• 也不是绝对的!出现次数最多的是“的”“是”“在”,这类最常用的词,叫做 停用词(stop words)通常列为黑名单
• 停用词对结果毫无帮助,必须过滤掉的词
• 过滤掉停用词后就一定能接近问题么?
• 进一步调整假设:如果某个词比较少见,但是它在这篇文章中多次出现,那么它很可能反映了这篇文章的特性,正是我们所需要的关键词
2、反文档频率——IDF
• 在词频的基础上,赋予每一个词的权重,进一步 体现该词的重要性,• 最常见的词(“的”、“是”、“在”)给予最小的权重
• 较常见的词(“国内”、“中国”、“报道”)给予较小的权重
• 较少见的词(“养殖”、“维基”)
• 将TF和IDF进行相乘,就得到了一个词的TF-IDF值,某个词对文章重要性越高,该值越大,于是排在前面的几个词,就是这篇文章的关键词。
3、计算步骤
词频(TF):计算方式有两种,都有自己的意义所在没有标准作出选择,看场景选择合适的方式,用abTest来做测试。
4、相似文章
• 使用TF-IDF算法,找出两篇文章的关键词;• 每篇文章各取出若干个关键词(比如20个),合并成一个集合,计算每篇文章对于这个集合中的词的词频(为了避免文章长度的差异,可以使用相对词频);
-正排表:doc1:t1,t2,t3,... doc2:t1,t2,t3;-倒排表:ti:doc1,doc2... t2:doc1,doc2,...
通过关键词得到相似文章:doc1:doc,doc,doc... 近似值,解决问题就好=平衡计算代价和准确性
• 生成两篇文章各自的词频向量;• 计算两个向量的余弦相似度,值越大就表示越相似。
5、自动摘要
• 文章的信息都包含在句子中,有些句子包含的信息多,有些句子包含的信息少。"自动摘要"就是要找出那些包含信息最多的句子。• 句子的信息量用"关键词"来衡量。如果包含的关键词越多,就说明这个句子越重要。
• 只要关键词之间的距离小于“ 门槛值”,它们就被认为处于同一个簇之中,如果两个关键词之间有5个以上的其他词,就可以把这两个关键词分在两个簇。
• 下一步,对于每个簇,都计算它的重要性分值。
• 简化:不再区分"簇",只考虑句子包含的关键词。下面就是一个例子(采用伪码表示),只考虑关键词首先出现的句子
选择关键词首先出现的句子:多个关键词可能会选择同一个句子,此时setSummarySentences.add(firstMatchingSentence)里面需要做去重处理
6、总结
• 优点:简单快速,结果比较符合实际情况• 缺点:单纯以“词频”做衡量标准,不够全面,有时重要的词可能出现的次数并不多
– 这种算法无法体现词的位置信息,出现位置靠前的词与出现位置靠后的词,都被视为重要性相同,这是不正确的。(一种解决方法是,对全文的第一段和每一段的第一句话,给予较大的权重。)
通过10000篇已知的文章和分词工具怎样得到一个相对比较好用的停用词名单?
--TF-IDF值越小的-->set
通过以上停用词名单去对另外5000篇文章去做过滤,则上一篇又需要怎样得到停用词名单?
--通过IDF去得到停用词再来处理这5000篇文章
7、实践
数据准备
508篇文章
随机一篇文章的示例:每一句的词语都通过空格分开
将所有数据整理到一个文件,每一行均代表某一篇文章的某些信息
doc1:t1,t2,
doc2:t1,t2,
doc3:t1,t2,
convert.py
#!/usr/bin/python
import os
import sys
import gzip
#得到文件的目录,然后便利
test_dir = sys.argv[1]
def get_file_handler(f):
file_in = open(f, 'r')
return file_in
index = 0
for fd in os.listdir(test_dir): #便利目录,得到每一篇文章
txt_list = []
#得到一篇文章放进数组,因为每一篇文章数据较小所以可以直接放进内存
file_fd = get_file_handler(test_dir + '/' + fd)
for line in file_fd:
txt_list.append(line.strip())
#索引代表的是某一篇文章 文章索引+内容通过 \t分割
print '\t'.join([str(index), ' '.join(txt_list)])
index += 1
执行脚本:python convert.py ../input_tf_idf_dir/ > test_file.data 然后查看输出的文件内容
对输出数据做IDF:
#!/usr/bin/python
import sys
for line in sys.stdin:
ss = line.strip().split('\t', 1)
doc_index = ss[0].strip()
doc_context = ss[1].strip() #分割得到文章内容
word_list = doc_context.split(' ')
word_set = set()
for word in word_list:
word_set.add(word) #通过word_set做去重,因为我们是做IDF,所以需要去重,为什么?
for word in word_set:
print '\t'.join([word, "1"])
语料库文档总数:508
所以通过reduce可以到最终的IDF
#!/usr/bin/python
import sys
import math
current_word = None
count_pool = []
sum = 0
docs_cnt = 508 #这个通过公司的具体量级来定,这里是508
for line in sys.stdin:
ss = line.strip().split(' ') #这里的标准输入得用空格分割
if len(ss) != 2:
continue
word, val = ss
if current_word == None:
current_word = word
if current_word != word:
for count in count_pool:
sum += count
idf_score = math.log(float(docs_cnt) / (float(sum) + 1)) #得到IDF分数
print "%s\t%s" % (current_word, idf_score) #输出每一个单词的IDF
current_word = word
count_pool = []
sum = 0
count_pool.append(int(val))
for count in count_pool:
sum += count
idf_score = math.log(float(docs_cnt) / (float(sum) + 1))
print "%s\t%s" % (current_word, idf_score)
//cat test.data | python map.py | sort -k1 | python red.py >1.data #排序会引入乱码,需要指定分隔符
cat test.data | python map.py > 1.data #没有排序不会乱码
cat 1.data | awk -F'\t' '{print $1,$2}' | sort -k1 > 2.data
//cat 2.data | python red.py | grep --color '32' #调试时用,逐条查看包含32的乱码
cat 2.data | python red.py > 3.data
运行的脚本文件run.sh
HADOOP_CMD="/usr/local/src/hadoop-2.6.1/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop-2.6.1/share/hadoop/tools/lib/hadoop-streaming-2.6.1.jar"
INPUT_FILE_PATH_1="/tfidf_input.data"
OUTPUT_PATH="/tfidf_output"
$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_PATH
# Step 1.
$HADOOP_CMD jar $STREAM_JAR_PATH \
-input $INPUT_FILE_PATH_1 \
-output $OUTPUT_PATH \
-mapper "python map.py" \
-reducer "python red.py" \
-file ./map.py \
-file ./red.py
在输出文件1.data中,我们可以去找分数最低的几个词语,是否在其他文章中经常出现,所以相关性较低
(三)LCS
1、LCS定义
• 最长公共子序列(Longest Common Subsequence)• 一个序列S任意删除若干个字符得到的新序列T,则T叫做S的子序列
• 两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列--顺序+交集
– 字符串1 2455与 245576的最长公共子序列为2455
– 字符串 ac dfg与 adfc的最长公共子序列为adf
• 注意区别最长公共子串(Longest Common Substring)--顺序+交集+连续
– 最长公共子串要求连接
2、LCS的应用场景
• 求两个序列中最长的公共子序列算法– 生物学家常利用该算法进行基金序列比对,以推测序列的结构、功能和演化过程。
• 描述两段文字之间的“相似度”
– 辨别抄袭,对一段文字进行修改之后,计算改动前后文字的最长公共子序列,将除此子序列外的部分提取出来,该方法判断修改的部分
• 今日头条的视频推荐列表--新闻(重复推荐)
--10条有9条同样的类型,10张图片有9张是一样的,用户选择性较差,只能点击同样的类型--又进入同样的推荐-->死循环 --去重、过滤、随机处理
3、求解——暴力穷举法
• 假定字符串X,Y的长度分别为m,n;• X的一个子序列即下标序列{1,2,……,m}严格递增子序列,因此, X共有2m 个不同子序列;同理,Y有 2n 个不同子序列;
• 穷举搜索法时间复杂度O( 2 m∗ 2 n );
• 对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且在检查过程中选出最长的公共子序列;
• 复杂度高,不可用!
4、求解——动态规划法(找规律并抽象出方法就叫做动态规划法)
• 字符串X,长度为m,从1开始数;• 字符串Y,长度为n,从1开始数;
• X i =<x 1 ,……,x i >即X序列的前i个字符(1<=i<=m)(X i 计作“字符串X的i前缀”)
• Y i =<y 1 ,……,y i >即Y序列的前i个字符(1<=j<=n)(Y j 计作“字符串Y的j前缀”)
• LCS(X,Y)为字符串X和Y的最长公共子序列,即为Z=<z 1 ,……,z k >
如果xm =yn(最后一个字符相同),则:xm 与yn 的最长公共子序列Z k 的最后一个字符必定为xm (= yn)
•