資料探勘基礎-1.文字相似度
一、文字相似度
相似度度量指的是計算個體間相似程度,一般使用距離來度量,相似度值越小,距離越大,相似度值越大,距離越小。在說明文字相似度概念和計算方式之前,先回顧下餘弦相似度。
1.餘弦相似度
衡量文字相似度最常用的方法是使用餘弦相似度。
– 空間中,兩個向量夾角的餘弦值作為衡量兩個個體之間差異的大小
– 餘弦值接近1,夾角趨於0,表明兩個向量越相似
– 餘弦值接近0,夾角趨於90,表明兩個向量越不相似
2.計算文字相似度
度量兩篇文文章的相似度流程如下:
思路:1、分詞;2、列出所有詞;3、分詞編碼;4、詞頻向量化;5、套用餘弦函式計量兩個句子的相似度。
下面我們介紹使用餘弦相似度計算兩段文字的相似度的具體例子。
http://www.cnblogs.com/airnew/p/9563703.html
句子A:這隻皮靴號碼大了。那隻號碼合適。
句子B:這隻皮靴號碼不小,那隻更合適。
1、分詞:
使用結巴分詞對上面兩個句子分詞後,分別得到兩個列表:
listA=[‘這‘, ‘只‘, ‘皮靴‘, ‘號碼‘, ‘大‘, ‘了‘, ‘那‘, ‘只‘, ‘號碼‘, ‘合適‘]
listB=[‘這‘, ‘只‘, ‘皮靴‘, ‘號碼‘, ‘不小‘, ‘那‘, ‘只‘, ‘更合‘, ‘合適‘]
2、列出所有詞,將listA和listB放在一個set中,得到:
set={'不小', '了', '合適', '那', '只', '皮靴', '更合', '號碼', '這', '大'}
將上述set轉換為dict,key為set中的詞,value為set中詞出現的位置,即‘這’:1這樣的形式。
dict1={'不小': 0, '了': 1, '合適': 2, '那': 3, '只': 4, '皮靴': 5, '更合': 6, '號碼': 7, '這': 8, '大': 9},可以看出“不小”這個詞在set中排第1,下標為0。
3、將listA和listB進行編碼,將每個字轉換為出現在set中的位置,轉換後為:
listAcode=[8, 4, 5, 7, 9, 1, 3, 4, 7, 2]
listBcode=[8, 4, 5, 7, 0, 3, 4, 6, 2]
我們來分析listAcode,結合dict1,可以看到8對應的字是“這”,4對應的字是“只”,9對應的字是“大”,就是句子A和句子B轉換為用數字來表示。
4、對listAcode和listBcode進行oneHot編碼,就是計算每個分詞出現的次數。oneHot編號後得到的結果如下:
listAcodeOneHot = [0, 1, 1, 1, 2, 1, 0, 2, 1, 1]
listBcodeOneHot = [1, 0, 1, 1, 2, 1, 1, 1, 1, 0]
下圖總結了句子從分詞,列出所有詞,對分詞進行編碼,計算詞頻的過程
5、得出兩個句子的詞頻向量之後,就變成了計算兩個向量之間夾角的餘弦值,值越大相似度越高。
listAcodeOneHot = [0, 1, 1, 1, 2, 1, 0, 2, 1, 1]
listBcodeOneHot = [1, 0, 1, 1, 2, 1, 1, 1, 1, 0]
根據餘弦相似度,句子A和句子B相似度很高。
下面講解如何通過一個預料庫,提取出一篇文章的關鍵詞。
二、TF-IDF
關鍵詞可以讓人快速瞭解一篇文章,根據上面分析,如果兩篇文章的關鍵詞是相似的,那麼兩篇文章就很可能是相似的。【當然,讀者可能已經發現,本篇部落格講解的是通過字面來衡量兩篇文章的相似度,而非通過字義角度】通常,使用TF-IDF值來度量一個詞的重要性,該值越大,說明詞越能描述文章的意思,下面具體講解。
1.詞頻TF
如果一個詞很重要,在文章中就會多次出現,這可以用詞頻—TF(Term Frequency)來衡量。
計算公式:
詞頻(TF) = 某個詞在文章中出現的次數/文章的總詞數
或者
詞頻(TF) = 某個詞在文章中出現的次數/該文出現次數最多的詞的出現次數
兩個公式的區別是:第二個公式可以將不同詞的TF值拉的更開。舉個例子,假設某篇文章共1000個詞,A出現了10次,B出現了11次,A和B通過公式1計算出的TF值差距很小,假設出現次數最多的詞C出現的次數是100,A和B通過公式2計算出的TF值差距相比更大一些,更有利於區分不同的詞。
在文章中,還存在“的”“是”“在”等常用詞,這些詞出現頻率較高,但是對描述文章並沒有作用,叫做停用詞(stop words),必須過濾掉。同時如果某個詞在語料庫中比較少見,但是它在某文章中卻多次出現,那麼它很可能也反映了這篇文章的特性,這也可能是關鍵詞,所以除了計算TF,還須考慮反文件頻率(idf,inverse document frequency)。
2.反文件頻率IDF
IDF的思想是:在詞頻的基礎上,賦予每個詞權重,進一步體現該詞的重要性。最常見的詞(“的”、“是”、“在”)給予最小的權重,較常見的詞(“國內”、“中國”、“報道”)給予較小的權重,較少見的詞(“養殖”、“維基”)給與較大的權重。
計算公式:
IDF = log(詞料庫的文件總數/包含該詞的文件數+1)
TF-IDF與一個詞在文件中的出現次數成正比,與包含該詞的文件數成反比。值越大就代表這個詞越關鍵。
3.應用1-相似文章
使用TF-IDF演算法,可以找出兩篇文章的關鍵詞;可以設定一個閥值,超過該值的認定為關鍵詞,或者取值排名靠前的n個詞作為關鍵詞。
每篇文章各取出若干個關鍵詞(比如20個),合併成一個集合,計算每篇文章對於這個集合中的詞的詞頻(為了避免文章長度的差異,可以使用相對詞頻,即除以對應文章的總詞數,相當於對詞頻進行了標準化處理)。
生成兩篇文章各自的詞頻向量,計算兩個向量的餘弦相似度,值越大就表示越相似。
4.應用2-自動摘要
文章的資訊都包含在句子中,有些句子包含的資訊多,有些句子包含的資訊少。"自動摘要"就是要找出包含資訊最多的句子。句子的資訊量用"關鍵詞"來衡量。如果包含的關鍵詞越多,就說明這個句子越重要。
只要關鍵詞之間的距離小於“門檻值”,就認為處於同一個簇之中,如果兩個關鍵詞距離有5個詞以上(值可調整),就把這兩個關鍵詞分在兩個不同的簇中。
對於每個簇,計算它的重要性分值。
例如:其中的某簇一共有7個詞,其中4個是關鍵詞。因此,它的重要性分值等於 ( 4 x 4 ) / 7 =2.3
簡化做法:不再區分"簇",只考慮句子包含的關鍵詞。
三、TF-IDF的Python實現
下面使用Python計算TF-IDF,前提是有一個預料庫。這裡總共有508篇文章,每篇文章中,都已經提前做好了分詞。
1.計算IDF
思路:將語料庫中的每篇文章放入各自的set集合中,再設定一個大的set,將之前各篇文章的set集合依次放入這個大的set中,得到的即每個詞以及詞出現的次數,詞對應的次數即擁有該詞的文章數。
1)convert.py
import os
import sys
files_dir = sys.argv[1] //獲得輸入的引數,即語料庫路徑
for file_name in os.listdir(files_dir): //函式會返回目錄下面的所有檔名稱
file_path = files_dir + file_name
file_in = open(file_path, 'r') //將文章內容讀取到file_in中,即獲得輸入流
tmp_list = [] //將每個文章的每一段內容都放在陣列tmp_list中
for line in file_in: //一行一行地讀取
tmp_list.append(line.strip())
print '\t'.join([file_name, ' '.join(tmp_list)]) //檔名和檔案內容按照tab符號分割,每個文章內部的每一段按照空格連線起來,最後會只形成一段。
[[email protected] 5_codes]# python convert.py /usr/local/src/code/5_codes/input_tfidf_dir/ > convert.data #將內容輸出到一個檔案中
[[email protected] 5_codes]# head -1 convert.data //可以內容,驗證結果
這個時候,即將所有文章整合到一個convert.data檔案中,每一段都代表一篇文章的詞,且詞不重複。
2)map.py
通過conver.py,獲取到了所有文章的詞彙,接下就需要將所有的詞取出來,並且儲存到一個大的set集合中,計算擁有該詞的文章數。為此,我們將通過map和reduce兩個步驟分別進行,目的是為了使程式能夠通過hadoop的MapReduce進行分散式運算(當語料庫非常大的時候,這是非常有必要的,如果僅僅是為了實踐如何計算TF-IDF,也可以將這兩步合併成一步,通過一臺電腦進行計算)。
import sys
for line in sys.stdin: //map是通過標準輸入讀到資料,將convert.data內容讀進去
ss = line.strip().split('\t') //ss為每篇文章的名稱和屬於這篇文章的所有詞
file_name = ss[0].strip()
file_context = ss[1].strip()
word_list = file_context.split(' ') //將文字內容按照空格分割
word_set = set()
for word in word_list: #這步是為了去重
word_set.add(word)
for word in word_set:
print '\t'.join([word, '1']) //這裡輸出的是每個文章的不同的字的,只統計是否有,為了給red中的計算做準備
[[email protected] 5_codes]# cat convert.data | python map.py >map.data
3)red.py
經過map後,再通過reduce計算詞的文章數。這裡需要注意的是,將map.data的資料輸入到red.py前,需要先進行排序,在hadoop的MapReduce中,這個步驟將會自動完成,但是在使用MapReduce前,我們本地驗證時候將通過sort命令進行排序。
import sys
import math
current_word = None
doc_cnt = 508 //文章總篇數
sum = 0
for line in sys.stdin:
ss = line.strip().split('\t')
if len(ss)!=2: //判斷格式是否是正確的
continue
word,val = ss
if current_word == None:
current_word = word
if current_word != word: //如果讀進來的單詞和之前的不一致,說明之前的已經讀完,可以開始計算idf值
idf_score = math.log(float(doc_cnt)/(float(sum+1)))
print '\t'.join([current_word,str(idf_score)])
current_word = word
sum = 0
sum = sum+1
//這裡要計算最後一個詞的idf詞
idf_score = math.log(float(doc_cnt)/(float(sum+1)))
print '\t'.join([current_word,str(idf_score)])
[[email protected] 5_codes]# cat map.data | sort -k1 | python red.py > myred.tmp
[[email protected] 5_codes]# cat myred.tmp | sort -k2 -nr > result.data 按照分值,從大到小排序
2.計算TF
1)mp_tf.py
import sys
word_dict = {}
idf_dict = {}
def read_idf_func(idf): #讀取idf值檔案的函式
with open(idf,'r') as fd:
for line in fd:
kv=line.strip().split('\t')
idf_dict[kv[0].strip()] =float(kv[1].strip())
return idf_dict
def mapper_func(idf):
idf_dict = read_idf_func(idf)
for line in sys.stdin:
ss = line.strip().split('\t')
fn = ss[0].strip()
fc = ss[1].strip()
word_list = fc.split(' ')
cur_word_num = len(word_list)
for word in word_list:
if word not in word_dict:
word_dict[word]=1
else:
word_dict[word]+=1
for k,v in word_dict.items():
if k!='':#判斷key是否為空格
print fn, k, float(v/float(cur_word_num)*idf_dict[k])
if __name__ == "__main__": #函式模組化,
module = sys.modules[__name__]
func = getattr(module, sys.argv[1])
args = None
if len(sys.argv) > 1:
args = sys.argv[2:]
func(*args)
[[email protected] 5_codes]# cat convert.data | python mp_tf.py mapper_func result.data
這裡需要注意,在上面的程式碼中,用 if k!='':對key進行了判斷,如果不進行判斷,則會出現如下的錯誤。
原因是在形成convert.data的時候出了問題,在某個文章中兩個單詞之間存在兩個空格。而計算出的result.data中並不包含空格的idf值,因為在計算這個idf前,通過如下程式碼將空格過濾掉了。
if len(ss)!=2: //判斷格式是否是正確的
continue
解決的辦法就是忽略文章中的空格,因此加入了if k!='':,若是空格就忽略掉。
四、LCS
1.概念
最長公共子序列(Longest Common Subsequence),一個序列S任意刪除若干個字元得到的新序列T,則T叫做S的子序列。
兩個序列X和Y的公共子序列中,長度最長的那個,定義為X和Y的最長公共子序列。
- 字串12455與245576的最長公共子序列為2455
- 字串acdfg與adfc的最長公共子序列為adf
最長公共子串(Longest Common Substring)與最長公共子序列不同的是,最長公共子串要求字元相鄰。
2.作用
1)生物學家常利用最長的公共子序列演算法進行基因序列比對,以推測序列的結構、功能和演化過程。
2)描述兩段文字之間的“相似度”。
辨別抄襲,對一段文字進行修改之後,計算改動前後文字的最長公共子序列,將除此子序列外的部分提取出來,該方法判斷修改的部分。
3)可以推薦不同型別的事物,增強使用者體驗。
3.求解—暴力窮舉法
• 假定字串X,Y的長度分別為m,n;
• X的一個子序列即下標序列{1,2,……,m}嚴格遞增子序列,因此,X共有2的m次方個不同子序列;同理,Y有2的n次方個不同子序列;(每個字元都對應著刪除或者不刪除,所以可以有如上的不同子序列個數)
• 窮舉搜尋法時間複雜度O(2的m次方 ∗ 2的n次方);
• 對X的每一個子序列,檢查它是否也是Y的子序列,從而確定它是否為X和Y的公共子序列,並且在檢查過程中選出最長的公共子序列,也就是說要遍歷所有的子序列。
• 複雜度高,不可用!
4.求解—動態規劃
• 字串X,長度為m,從1開始數;
• 字串Y,長度為n,從1開始數;
• Xi=<x1,……,xi>即X序列的前i個字元(1<=i<=m)(Xi計作“字串X的i字首” )
• Yj=<y1,……,yj>即Y序列的前i個字元(1<=j<=n)(Yj計作“字串Y的j字首” )
• LCS(X,Y)為字串X和Y的最長公共子序列,即為Z=<z1,……,zk>
• 如果xm = yn(最後一個字元相同),則:Xm與Yn的最長公共子序列Zk的最後一個字元肯定是xm(=yn),所以zk=xm=yn,因此有LCS(Xm,Yn)= LCS(Xm-1,Yn-1)+xm。
• 如果xm ≠ yn,則LCS(Xm, Yn)=LCS(Xm−1, Yn),或者LCS(Xm, Yn)=LCS(Xm, Yn−1)
• 即LCS(Xm, Yn)=max{LCS(Xm−1, Yn), LCS(Xm, Yn−1)}
使用二維陣列C[m,n],C[i,j]記錄序列Xi和Yj的最長公共子序列的長度,因此得到C[m,n]的值時,即得到最長公共子序列的長度。當i=0或j=0時,空序列是Xi和Yj的最長公共子序列,故C[i,j]=0。
舉例:計算X=<A, B, C, B, D, A, B> 和Y=<B, D, C, A, B, A>的最長公共子串。按照公式逐漸遞推到X和Y的首個字母, 接著從兩個序列的首個字母開始回溯,最終計算出結果。具體過程如下:
X0=0或Y0=0時,LCS=0,因此第一行和第一列都是0。接下來從(1,1)位置開始,按照從坐到右,上到下的順序,一行一行地判斷。
判斷X1=A和Y1=B不一樣,所以LCS(X1,Y1)=max{LCS(X0,Y1),LCS(X1,Y0)}=0。
接下來判斷(1,2)位置的LCS值,根據公式,由於A和B元素不同,因此呼叫第3個公式,即取該點左邊和上面點的最大值,由於此時最大值都是0,所以(1,2)位置的LCS值為0;同理一直到(1,4),由於A和A相同,因此呼叫第2個公式,即左上角的LCS值+1,因此可以得到C(1,2)=1。
以此類推,最終就可以得到C(7,6)的值,該值為4,即兩個序列的最長公共子序列為4。
5.LCS的Python實現
首先準備一個輸入資料,該檔案中每行有兩句,中間用製表符分隔。
1)map.py
import sys
def cal_lcs_sim(first_str, second_str):
len_vv = [[0] * 50] * 50 // 50*50的矩陣,保證夠大就行
first_str = unicode(first_str, "utf-8", errors='ignore') //設定支援中文,否則會出現亂碼
second_str = unicode(second_str, "utf-8", errors='ignore')
len_1 = len(first_str.strip())
len_2 = len(second_str.strip())
//從左到右,從上到下計算最長公共子串
for i in range(1, len_1 + 1):
for j in range(1, len_2 + 1):
if first_str[i - 1] == second_str[j - 1]: //如果相等,則對角線的值+1,這裡i,j的範圍從1到len+1,是為了防止在計算[0][0]時,出現越界。
len_vv[i][j] = 1 + len_vv[i - 1][j - 1]
else:
len_vv[i][j] = max(len_vv[i - 1][j], len_vv[i][j - 1])
return float(float(len_vv[len_1][len_2] * 2) / float(len_1 + len_2)) //相似度公式可以自定義
//計算框架的入口,接收輸入的文字資料
for line in sys.stdin:
ss = line.strip().split('\t')
if len(ss) != 2:
continue
first_str = ss[0].strip()
second_str = ss[1].strip()
sim_score = cal_lcs_sim(first_str, second_str)
print '\t'.join([first_str, second_str, str(sim_score)])
2)run.sh
HADOOP_CMD="/usr/local/src/hadoop-1.2.1/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop-1.2.1/contrib/streaming/hadoop-streaming-1.2.1.jar" #這是hadoop1.0採用的hadoop-streaming的jar包
#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" #這是hadoop2.0採用的hadoop-streaming的jar包
INPUT_FILE_PATH_1="/lcs_input.data"
OUTPUT_PATH="/lcs_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" \
-jobconf "mapred.reduce.tasks=0" \
-jobconf "mapred.job.name=mr_lcs" \
-file ./map.py
最終在hdfs上可以看到生成了/lcs_output的資料夾,檢視內部檔案,檢查結果。