SegmentFault 上很多作者都喜欢写一系列博客,但是并没有很好的归类,就做了一个标题相似度匹配出一系列的文章,相关原理都是 Google 的,这里稍微纪录一下自己从中学到的东西。
LD算法
之前也没做过自然语言处理相关的东西,所以开始一头雾水,查了一下文本相似度匹配,发现解决方案还很多,就开始尝试了。第一次使用了一个叫 LD 的算法:
LD算法(Levenshtein Distance)又成为编辑距离算法(Edit Distance)。他是以字符串A通过插入字符、删除字符、替换字符变成另一个字符串B,那么操作的过程的次数表示两个字符串的差异。
参照网上的代码弄出来之后发现匹配效果不佳,另外运行效率真也不好,最后还是放弃了;
另外就是这里的匹配出结果之后发现一个问题就是所有的输出都是两两相配的,并没有归类起来,然后就在这里卡了好久。
贴代码:
import time
from numpy import *
def strcmp(s, t):
if len(s) > len(t):
s, t = t, s
n = len(s)
m = len(t)
if not m : return n
if not n : return m
v0 = [ i for i in range(0, m+1) ]
v1 = [ 0 ] * (m+1)
cost = 0
for i in range(1, n+1):
v1[0] = i
for j in range(1, m+1):
if s[i-1] == t[j-1]:
cost = 0
else:
cost = 1
a = v0[j] + 1
b = v1[j-1] + 1
c = v0[j-1] + cost
v1[j] = min(a, b, c)
v0 = v1[:]
return v1[m]
K-means
又去查了一下,学习到了机器学习总的聚类和分类的概念,之前尝试过 KNN,虽然按照教程搞出来的,但是不是很理解什么是监督学习和非监督学习,这次算是彻底搞懂了,所以不管看起来多难的东西,只要去尝试做,一定会有收获的。
这里有了解了 k-means 聚类算法,是一种非监督学习算法,其实原理相当简单:
随机抽取 k 个中心簇。
计算每个元素与上面中心簇的距离,取最小的归为一类。
再次计算上面新产生的一类的中心簇,更新起坐标。
通过这样一个循环,就可以吧一些数据做出分类了。
分词
以为上面的 LD 算法只是拿字来做向量,所以可能是匹配效果不佳的原因,所以选择使用词来做向量,第一步就是要分词了,本来打算用别人的分词库的,但是想想觉得这么点功能不值得,就直接用最简单的不基于语料的分词,直接以两个字符为单位进行分词,尝试了一下发现效果还蛮不错的。贴代码:
preg_match_all("/.{2}/u", $string, $splited_str);
嗯,你没看错,就只有一行,PHP 的字符串函数还是真多的,参考的示例时用python实现的,相比之下分词函数就没这么简洁了:
def splitContents(content,k=5):
content_split=[]
for i in range(len(content)-k):
content_split.append(content[i:i+k])
return content_split
这么看来 PHP 还真是世界上最好的语言,虽然我这开始写分词的时候喷了它一下,主要是因为开始用的str_split函数,分词之后发现中文全是乱码,这已经不记得是第多少次使用php字符串函数之后中文乱码的问题了,可能主要是因为前期php不支持unicode的原因吧,不过现在的mb_前缀的一系列函数貌似不会出乱码了,所以果断放弃原来的那套字符串函数吧。
统计词频
两两分词之后就要统计一下词频,下面分别是php的代码和python的代码,感受下:
php
$hash_content = array_count_values($array);
python
def hashContentsList(content_list):
hash_content={}
for i in content_list:
if i in hash_content:
hash_content[i]=hash_content[i]+1
else:
hash_content[i]=1
return hash_content
相似度计算
原理参考:文本相似度计算-JaccardSimilarity和哈希签名函数
这里就直接上php代码了,不在赘述原理:
Update: 这里的 calcIntersection
函数返回值其实是两个集合交集的词频之和,calcUnionSet
函数同理。
// 主函数
function calcEachSimilar($hash_contents)
{
$similar_list = array();
$all = (float)count($hash_contents);
$pos = 0.0;
foreach ($hash_contents as $key1 => $value1) {
$pos = $pos + 1;
foreach ($hash_contents as $key2 => $value2) {
if($value1[1] != $value2[1] and $key1 > $key2){
$intersection = $this -> calcIntersection($value1[0], $value2[0]); #计算交集
$union_set = $this -> calcUnionSet($value1[0], $value2[0], $intersection); #计算并集
$similar = $this -> calcSimilarity($intersection, $union_set);
array_push($similar_list, array($similar, $value1[1], $value2[1]));
}
}
}
rsort($similar_list);
return $similar_list;
}
// 计算交集
function calcIntersection($hash_a, $hash_b){
$intersection = 0;
if(count($hash_a) <= count($hash_b)){
$hash_min = $hash_a;
$hash_max = $hash_b;
} else {
$hash_min = $hash_b;
$hash_max = $hash_a;
}
foreach ($hash_min as $key => $value) {
if(array_key_exists($key, $hash_max)){
if($value <= $hash_max[$key]){
$intersection = $intersection + $value;
} else {
$intersection = $intersection + $hash_max[$key];
}
}
}
return $intersection;
}
// 计算并集
function calcUnionSet($hash_a, $hash_b, $intersection)
{
$union_set = 0;
foreach ($hash_a as $key => $value) {
$union_set = $union_set + $value;
}
foreach ($hash_b as $key => $value) {
$union_set = $union_set + $value;
}
return $union_set - $intersection;
}
// 计算相似度
function calcSimilarity($intersection, $union_set){
if($union_set > 0){
return (float)$intersection/(float)$union_set;
} else {
return 0.0;
}
}
当然这样匹配出来的结果是两两一起的,并没有把相似的分类,所以要分类的话还需要进行下一步,不过我php实现时已经知道了分类中的其中一个元素,所以只需要拿他去和其他的匹配就好了,就没有用php写,倒是用pyhton实现了一个版本:
其实原理很简单,就是最简单的数据结构:并查集:
class unionfind:
def __init__(self, groups):
self.groups=groups
self.items=[]
for g in groups:
self.items+=list(g)
self.items=set(self.items)
self.parent={}
self.rootdict={} #记住每个root下节点的数量
for item in self.items:
self.rootdict[item]=1
self.parent[item]=item
def union(self, r1, r2):
rr1=self.findroot(r1)
rr2=self.findroot(r2)
cr1=self.rootdict[rr1]
cr2=self.rootdict[rr2]
if cr1>=cr2: #将节点数量较小的树归并给节点数更大的树
self.parent[rr2]=rr1
self.rootdict.pop(rr2)
self.rootdict[rr1]=cr1+cr2
else:
self.parent[rr1]=rr2
self.rootdict.pop(rr1)
self.rootdict[rr2]=cr1+cr2
def findroot(self, r):
if r in self.rootdict.keys():
return r
else:
return self.findroot(self.parent[r])
def createtree(self):
for g in self.groups:
if len(g)< 2:
continue
else:
for i in range(0, len(g)-1):
if self.findroot(g[i]) != self.findroot(g[i+1]): #如果处于同一个集合的节点有不同的根节点,归并之
self.union(g[i], g[i+1])
def printree(self):
rs={}
for item in self.items:
root=self.findroot(item)
rs.setdefault(root,[])
rs[root]+=[item]
for key in rs.keys():
# print rs[key],
for t in rs[key]:
print t
print "============\n"
##### 调用
u=unionfind(class_contents)
u.createtree()
u.printree()
代码也是参考网上的,具体哪里的懒得去找了,作者要是看到了需要我加参考链接的联系我好了。
总之,从来没正经学过算法的人只能可耻的到网上找解决方案了,嗯,之后会努力学习算法了,希望不要被喷。