SegmentFault 上不少做者都喜歡寫一系列博客,可是並無很好的歸類,就作了一個標題類似度匹配出一系列的文章,相關原理都是 Google 的,這裏稍微紀錄一下本身從中學到的東西。php
以前也沒作過天然語言處理相關的東西,因此開始一頭霧水,查了一下文本類似度匹配,發現解決方案還不少,就開始嘗試了。第一次使用了一個叫 LD 的算法:python
LD算法(Levenshtein Distance)又成爲編輯距離算法(Edit Distance)。他是以字符串A經過插入字符、刪除字符、替換字符變成另外一個字符串B,那麼操做的過程的次數表示兩個字符串的差別。算法
參照網上的代碼弄出來以後發現匹配效果不佳,另外運行效率真也很差,最後仍是放棄了;數據結構
另外就是這裏的匹配出結果以後發現一個問題就是全部的輸出都是兩兩相配的,並無歸類起來,而後就在這裏卡了很久。app
貼代碼:機器學習
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]
又去查了一下,學習到了機器學習總的聚類和分類的概念,以前嘗試過 KNN,雖然按照教程搞出來的,可是不是很理解什麼是監督學習和非監督學習,此次算是完全搞懂了,因此無論看起來多難的東西,只要去嘗試作,必定會有收穫的。函數
這裏有瞭解了 k-means 聚類算法,是一種非監督學習算法,其實原理至關簡單:學習
隨機抽取 k 箇中心簇。this
計算每一個元素與上面中心簇的距離,取最小的歸爲一類。.net
再次計算上面新產生的一類的中心簇,更新起座標。
經過這樣一個循環,就能夠吧一些數據作出分類了。
覺得上面的 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()
代碼也是參考網上的,具體哪裏的懶得去找了,做者要是看到了須要我加參考連接的聯繫我好了。
總之,歷來沒正經學過算法的人只能可恥的到網上找解決方案了,嗯,以後會努力學習算法了,但願不要被噴。