1 TF-IDF算法
2 代碼實現
3 余弦相似性
4 代碼實現
1 TF-IDF算法
舉個例子
假定現在有一篇長文《中國的蜜蜂養殖》,我們準備用計算機提取它的關鍵詞。
一個容易想到的思路,就是找到出現次數最多的詞。如果某個詞很重要,它應該在這篇文章中多次出現。于是,我們進行"詞頻"(Term Frequency,縮寫為TF)統計。
結果你肯定猜到了,出現次數最多的詞是----"的"、"是"、"在"----這一類最常用的詞。它們叫做"停用詞"(stop words),表示對找到結果毫無幫助、必須過濾掉的詞。
假設我們把它們都過濾掉了,只考慮剩下的有實際意義的詞。這樣又會遇到了另一個問題,我們可能發現"中國"、"蜜蜂"、"養殖"這三個詞的出現次數一樣多。這是不是意味著,作為關鍵詞,它們的重要性是一樣的?
顯然不是這樣。因為"中國"是很常見的詞,相對而言,"蜜蜂"和"養殖"不那么常見。如果這三個詞在一篇文章的出現次數一樣多,有理由認為,"蜜蜂"和"養殖"的重要程度要大于"中國",也就是說,在關鍵詞排序上面,"蜜蜂"和"養殖"應該排在"中國"的前面。
所以,我們需要一個重要性調整系數,衡量一個詞是不是常見詞。如果某個詞比較少見,但是它在這篇文章中多次出現,那么它很可能就反映了這篇文章的特性,正是我們所需要的關鍵詞。
用統計學語言表達,就是在詞頻的基礎上,要對每個詞分配一個"重要性"權重。最常見的詞("的"、"是"、"在")給予最小的權重,較常見的詞("中國")給予較小的權重,較少見的詞("蜜蜂"、"養殖")給予較大的權重。這個權重叫做"逆文檔頻率"(Inverse Document Frequency,縮寫為IDF),它的大小與一個詞的常見程度成反比。
知道了"詞頻"(TF)和"逆文檔頻率"(IDF)以后,將這兩個值相乘,就得到了一個詞的TF-IDF值。某個詞對文章的重要性越高,它的TF-IDF值就越大。所以,排在最前面的幾個詞,就是這篇文章的關鍵詞。
下面就是這個算法的細節。
-
第一步,計算詞頻。
image.png
考慮到文章有長短之分,為了便于不同文章的比較,進行"詞頻"標準化。
image.png
或者
image.png -
第二步,計算逆文檔頻率。
這時,需要一個語料庫(corpus),用來模擬語言的使用環境。
image.png
如果一個詞越常見,那么分母就越大,逆文檔頻率就越小越接近0。分母之所以要加1,是為了避免分母為0(即所有文檔都不包含該詞)。log表示對得到的值取對數。
-
第三步,計算TF-IDF。
image.png
可以看到,TF-IDF與一個詞在文檔中的出現次數成正比,與該詞在整個語言中的出現次數成反比。所以,自動提取關鍵詞的算法就很清楚了,就是計算出文檔的每個詞的TF-IDF值,然后按降序排列,取排在最前面的幾個詞。
TF-IDF算法的優點是簡單快速,結果比較符合實際情況。缺點是,單純以"詞頻"衡量一個詞的重要性,不夠全面,有時重要的詞可能出現次數并不多。而且,這種算法無法體現詞的位置信息,出現位置靠前的詞與出現位置靠后的詞,都被視為重要性相同,這是不正確的。
2 代碼實現
實現思路:
- 通過中文分詞器分詞
- 統計詞頻
- 統計擬文檔頻率
- 計算結果,排序輸出
通過中文分詞器分詞
/**
* 調用IKSegmenter切詞,智能切詞
* @param 讀取的文本內容
* @return returnStr 切詞結果,末尾加上""
* */
private static String segStr(String text) throws IOException{
String returnStr = "";
IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(text), true);
Lexeme lexeme;
while ((lexeme = ikSegmenter.next()) != null) {
returnStr += lexeme.getLexemeText()+" ";
}
return returnStr;
}
統計詞頻TF
public static HashMap<String, Double> tf(String[] cutWordResult) {
HashMap<String, Double> tf = new HashMap<String, Double>();// 正規化
int wordNum = cutWordResult.length;
int wordtf = 0;
for (int i = 0; i < wordNum; i++) {
wordtf = 0;
for (int j = 0; j < wordNum; j++) {
if (cutWordResult[i] != " " && i != j) {
if (cutWordResult[i].equals(cutWordResult[j])) {
cutWordResult[j] = " ";
wordtf++;
}
}
}
if (cutWordResult[i] != " ") {
tf.put(cutWordResult[i], (new Double(++wordtf)) / wordNum);
cutWordResult[i] = " ";
}
}
return tf;
}
計算IDF
public static Map<String, Double> idf(String dir) throws FileNotFoundException, UnsupportedEncodingException,
IOException {
// 公式IDF=log((1+|D|)/|Dt|),其中|D|表示文檔總數,|Dt|表示包含關鍵詞t的文檔數量。
Map<String, Double> idf = new HashMap<String, Double>();
List<String> located = new ArrayList<String>();
float Dt = 1;
float D = allTheNormalTF.size();// 文檔總數
List<String> key = fileList;// 存儲各個文檔名的List
Map<String, HashMap<String, Integer>> tfInIdf = allTheNormalTF;// 存儲各個文檔tf的Map
for (int i = 0; i < D; i++) {
HashMap<String, Integer> temp = tfInIdf.get(key.get(i));
for (String word : temp.keySet()) {
Dt = 1;
if (!(located.contains(word))) {
for (int k = 0; k < D; k++) {
if (k != i) {
HashMap<String, Integer> temp2 = tfInIdf.get(key.get(k));
if (temp2.keySet().contains(word)) {
located.add(word);
Dt = Dt + 1;
continue;
}
}
}
idf.put(word, (Double) Math.log((1.0 + D) / Dt));
}
}
}
return idf;
}
計算TF-IDF = TF*IDF
public static Map<String, HashMap<String, Double>> tfidf(String RootURL_exSelectedWeb) throws IOException {
//Map<String, Float> singelFile = new TreeMap<String,Float>();
Map<String, Double> idf = TfIdf.idf(RootURL_exSelectedWeb);
Map<String, HashMap<String, Double>> tf = TfIdf.tfOfAll(RootURL_exSelectedWeb);
Map<String, HashMap<String, Double>> tfidf = new TreeMap<String,HashMap<String,Double>>();;
for (String file : tf.keySet()) {//1 獲取tf的鍵(文件名)
HashMap<String, Double> singelFile = tf.get(file);//2獲取tf的值 (詞語,詞頻)
for (String word : singelFile.keySet()) {//3 獲取詞語,通過詞語遍歷整個 文檔詞語,并逐一計算TF-IDF
singelFile.put(word, (idf.get(word)) * singelFile.get(word));
}
tfidf.put(file,singelFile);
}
return tfidf;
}
/**
* 將詞頻和文件名稱聯系起來
* @param dir
* @return allTheTf(文件名稱,詞語,正規化詞頻)
* @throws IOException
*/
public static Map<String, HashMap<String, Double>> tfOfAll(String dir) throws IOException {
List<String> fileList = TfIdf.readDirs(dir);
for (String file : fileList) {
HashMap<String, Double> dict = new HashMap<String, Double>();
dict = TfIdf.tf(TfIdf.cutWord(file));
allTheTf.put(file, dict);
}
return allTheTf;
}
3 余弦相似性
有些時候,除了找到關鍵詞,我們還希望找到與原文章相似的其他文章。
為了找出相似的文章,需要用到"余弦相似性"(cosine similiarity)。下面,我舉一個例子來說明,什么是"余弦相似性"。
為了簡單起見,我們先從句子著手。
句子A:我喜歡看電視,不喜歡看電影。
句子B:我不喜歡看電視,也不喜歡看電影。
請問怎樣才能計算上面兩句話的相似程度?
基本思路是:如果這兩句話的用詞越相似,它們的內容就應該越相似。因此,可以從詞頻入手,計算它們的相似程度。
- 第一步,分詞。
句子A:我/喜歡/看/電視,不/喜歡/看/電影。
句子B:我/不/喜歡/看/電視,也/不/喜歡/看/電影。
- 第二步,列出所有的詞。
我,喜歡,看,電視,電影,不,也。
- 第三步,計算詞頻。
句子A:我 1,喜歡 2,看 2,電視 1,電影 1,不 1,也 0。
句子B:我 1,喜歡 2,看 2,電視 1,電影 1,不 2,也 1。
- 第四步,寫出詞頻向量。
句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]
到這里,問題就變成了如何計算這兩個向量的相似程度。
我們可以把它們想象成空間中的兩條線段,都是從原點([0, 0, ...])出發,指向不同的方向。兩條線段之間形成一個夾角,如果夾角為0度,意味著方向相同、線段重合;如果夾角為90度,意味著形成直角,方向完全不相似;如果夾角為180度,意味著方向正好相反。
因此,我們可以通過夾角的大小,來判斷向量的相似程度。夾角越小,就代表越相似。
以二維空間為例,上圖的a和b是兩個向量,我們要計算它們的夾角θ。余弦定理告訴我們,可以用下面的公式求得:
假定a向量是[x1, y1],b向量是[x2, y2],那么可以將余弦定理改寫成下面的形式:
數學家已經證明,余弦的這種計算方法對n維向量也成立。假定A和B是兩個n維向量,A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,則A與B的夾角θ的余弦等于:
使用這個公式,我們就可以得到,句子A與句子B的夾角的余弦。
余弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,這就叫"余弦相似性"。所以,上面的句子A和句子B是很相似的,事實上它們的夾角大約為20.3度。
由此,我們就得到了"找出相似文章"的一種算法:
(1)使用TF-IDF算法,找出兩篇文章的關鍵詞;
(2)每篇文章各取出若干個關鍵詞(比如20個),合并成一個集合,計算每篇文章對于這個集合中的詞的詞頻(為了避免文章長度的差異,可以使用相對詞頻);
(3)生成兩篇文章各自的詞頻向量;
(4)計算兩個向量的余弦相似度,值越大就表示越相似。
"余弦相似度"是一種非常有用的算法,只要是計算兩個向量的相似程度,都可以采用它。
4 代碼實現
private double computeSimTest_COS(Map<String, Double> testWordTFMap,
Map<String, Double> trainWordTFMap) {
double mul = 0, reslut=0, aMul=0, bMul=0,amulPow=0,bmulPow=0;
ArrayList<Double> aVector = new ArrayList<Double>();
ArrayList<Double> bVector = new ArrayList<Double>();
Set<Map.Entry<String, Double>> testWordTFMapSet = testWordTFMap.entrySet();//除K文檔外所有文檔
Set<Map.Entry<String, Double>> testTrainWordSet = trainWordTFMap.entrySet();//初始k文檔
//分別遍歷兩個文本向量,取出其權值
for(Iterator<Map.Entry<String, Double>> it = testWordTFMapSet.iterator(); it.hasNext();){
Map.Entry<String, Double> me = it.next();
aVector.add(me.getValue());
}
for(Iterator<Map.Entry<String, Double>> it = testTrainWordSet.iterator(); it.hasNext();){
Map.Entry<String, Double> me = it.next();
bVector.add(me.getValue());
}
for(int i=0;i<aVector.size();i++){
aMul=aVector.get(i);
bMul=bVector.get(i);
mul+=aMul*bMul;
amulPow+=aMul*aMul;
bmulPow+=bMul*bMul;
}
reslut=mul/(Math.sqrt(amulPow)*Math.sqrt(bmulPow));
return reslut ;
}
END