很常见的情况,我打赌。你有一个博客或新闻网站,你有很多文章或错误或任何你称之为的东西,你想在每个网站的底部建议其他似乎相关的内容。
让我们假设每个项目的元数据非常少。也就是说,没有标签,类别。视为一大块文本,包括标题和作者姓名。
你如何找到可能相关的文件?
我对实际算法很感兴趣,而不是现成的解决方案,虽然我可以看看在ruby或python中实现的东西,或者依赖于mysql或pgsql。
编辑:目前的答案相当不错,但我想看到更多。对于一两件事,可能有一些非常简单的示例代码。
答案 0 :(得分:38)
这是一个非常大的话题 - 除了人们在这里提出的答案之外,我建议您查看一些信息检索课程的教学大纲,并查看分配给他们的教科书和论文。也就是说,这是我自己的研究生日的简要概述:
最简单的方法称为bag of words。每个文档都缩减为{word: wordcount}
对的稀疏向量,您可以将NaiveBayes(或其他一些)分类器放在代表您的文档集的向量集上,或者计算每个包之间的相似度得分。 bag(这称为k-最近邻分类)。 KNN快速查找,但需要O(n ^ 2)存储分数矩阵;但是,对于博客来说,n不是很大。对于大型报纸大小的东西,KNN迅速变得不切实际,因此动态分类算法有时会更好。在这种情况下,您可以考虑ranking support vector machine。 SVM很整洁,因为它们不会限制您使用线性相似性度量,并且仍然非常快。
Stemming是词袋技术的常见预处理步骤;这涉及在计算单词包之前减少与形态相关的单词,例如“猫”和“猫”,“鲍勃”和“鲍勃”,或“相似”和“类似”,直到它们的根。那里有许多不同的词干算法;维基百科页面包含多个实现的链接。
如果词袋相似性不够好,您可以将它抽象为一层N-gram相似度,您可以在其中创建基于词对或三元组表示文档的向量。 (你可以使用4元组甚至更大的元组,但在实践中这没有多大帮助。)这样做的缺点是产生更大的矢量,因此分类会花费更多的工作,但你得到的匹配会更接近语法。 OTOH,你可能不需要这个用于语义相似性;对抄袭检测这样的东西更好。也可以使用Chunking,或者将文档缩减为轻量级解析树(对于树有分类算法),但这对于作者身份问题更有用(“给出一个来历不明的文档,谁写了吗?“)。
对您的用例可能更有用的是概念挖掘,其涉及将单词映射到概念(使用诸如WordNet的词库),然后基于所使用的概念之间的相似性对文档进行分类。这通常比基于单词的相似性分类更有效,因为从单词到概念的映射是还原性的,但预处理步骤可能相当耗时。
最后,有discourse parsing,它涉及解析文档的语义结构;你可以在语篇树上运行相似性分类器,就像在分块文档上一样。
这几乎涉及从非结构化文本生成元数据;在原始文本块之间进行直接比较是难以处理的,因此人们首先将文档预处理为元数据。
答案 1 :(得分:4)
这是Document Classification的典型案例,在每一类机器学习中进行了研究。如果您喜欢统计学,数学和计算机科学,我建议您查看kmeans++,Bayesian methods和LDA等无监督方法。特别是贝叶斯方法pretty good在你想要的是什么,他们唯一的问题是缓慢(但除非你运行一个非常大的网站,这不应该打扰你)。
关于更实际且理论上更少理论的方法,我建议您查看this和this other个很棒的代码示例。
答案 2 :(得分:4)
您应该阅读“编程集体智慧:构建智能Web 2.0应用程序”一书(ISBN 0596529325)!
对于某些方法和代码:首先问问自己,是否要根据单词匹配找到直接相似性,或者是否要显示可能与当前文章不直接相关的类似文章,但属于同一个集群制品
请参阅Cluster analysis / Partitional clustering。
找到直接相似性的一种非常简单(但理论和缓慢)的方法是:
预处理:
int word_matches[narticles][narticles]
(您不应该像那样存储它,A->的相似性; B与B-> A相同,因此稀疏矩阵节省了几乎一半的空间。查找类似文章:
答案 3 :(得分:3)
Ruby中的小型矢量空间模型搜索引擎。基本思想是,如果两个文档包含相同的单词,则它们是相关的。因此,我们计算每个文档中单词的出现次数,然后计算这些向量之间的余弦值(每个项都有一个固定的索引,如果它出现在该索引处有1,如果不是零)。如果两个文档的所有术语都是通用的,则余弦将为1.0;如果没有共同的术语,则余弦为0.0。您可以直接将其转换为%值。
terms = Hash.new{|h,k|h[k]=h.size}
docs = DATA.collect { |line|
name = line.match(/^\d+/)
words = line.downcase.scan(/[a-z]+/)
vector = []
words.each { |word| vector[terms[word]] = 1 }
{:name=>name,:vector=>vector}
}
current = docs.first # or any other
docs.sort_by { |doc|
# assume we have defined cosine on arrays
doc[:vector].cosine(current[:vector])
}
related = docs[1..5].collect{|doc|doc[:name]}
puts related
__END__
0 Human machine interface for Lab ABC computer applications
1 A survey of user opinion of computer system response time
2 The EPS user interface management system
3 System and human system engineering testing of EPS
4 Relation of user-perceived response time to error measurement
5 The generation of random, binary, unordered trees
6 The intersection graph of paths in trees
7 Graph minors IV: Widths of trees and well-quasi-ordering
8 Graph minors: A survey
Array#cosine
的定义留给读者一个练习(应该处理nil值和不同的长度,但是我们得到Array#zip
对吧?)
答案 4 :(得分:1)
前段时间我实现了类似的东西。也许这个想法现在已经过时了,但我希望它可以提供帮助。
我运行了一个ASP 3.0网站来编写常见任务,并从这个原则开始:用户有疑问,只要他/她能找到有关该主题的有趣内容,就会留在网站上。
当用户到达时,我启动了一个ASP 3.0 Session
对象并记录了所有用户导航,就像链接列表一样。在Session.OnEnd
事件中,我接受第一个链接,查找下一个链接并增加一个计数器列,如:
<Article Title="Cookie problem A">
<NextPage Title="Cookie problem B" Count="5" />
<NextPage Title="Cookie problem C" Count="2" />
</Article>
因此,要检查相关文章,我只需列出顶部的 n NextPage
实体,按计数器列降序排序。