一、N-gram介紹
n元語法(英語:N-gram)指文本中連續出現的n個語詞。n元語法模型是基於(n - 1)階馬爾可夫鏈的一種概率語言模型,通過n個語詞出現的概率來推斷語句的結構。這一模型被廣泛應用於概率論、通信理論、計算語言學(如基於統計的自然語言處理NLP)、計算生物學(如序列分析)、數據壓縮等領域。
N-gram文本廣泛用於文本挖掘和自然語言處理任務。它們基本上是給定窗口內的一組同時出現的單詞,在計算n元語法時,通常會將一個單詞向前移動(盡管在更高級的場景中可以使X個單詞向前移動)。
例如,對於句子"The cow jumps over the moon" ,N = 2(稱為二元組),則 ngram 為:
- the cow
- cow jumps
- jumps over
- over the
- the moon
因此,在這種情況下,有 5 個 n-gram。
再來看看 N = 3,ngram 將為:
- the cow jumps
- cow jumps over
- jumps over the
- over the moon
因此,在這種情況下,有 4 個 n-gram。
所以,在一個句子中 N-grams 的數量有:
Ngrams(K) = X - (N - 1)
其中,X 為給定句子K中的單詞數,N 為 N-gram 的N,指的是連續出現的 N 個單詞。
N-gram用於各種不同的任務。例如,在開發語言模型時,N-grams不僅用於開發unigram模型,而且還用於開發bigram和trigram模型。谷歌和微軟已經開發了網絡規模的 n-gram模型,可用於多種任務,例如拼寫校正、分詞和文本摘要。N-gram的另一個用途是為受監督的機器學習模型(例如SVM,MaxEnt模型,朴素貝葉斯等)開發功能。其想法是在特征空間中使用標記(例如雙字母組),而不是僅使用字母組合。
下面簡單介紹一下如何用 Java 生成 n-gram。
二、用 Java 生成 n-gram
這個是生成 n-gram 的主要方法,方法首先是對傳進來的句子 sentence 進行單詞拆分,這個正則表達式“\\s+”是能匹配任何空白字符,包括空格、制表符、換頁符等等, 等價於 [ \f\n\r\t\v]。拆分完后對單詞進行拼接。算法時間復雜度為 O(X - (N - 1)),X 為給定句子K中的單詞數,N 為 N-gram 的 N。
1 /** 2 * 生成n元語法 3 * <p> 4 * 一個句子中有多少個N-gram? 5 * 如果 X = 給定句子K中的單詞數,則句子K的 N-gram數為: 6 * N(grams<K>) = X - (N - 1) 7 * 8 * @param n 連續 n個單詞 9 * @param sentence 句子級別的文本 10 * @return 存着ngram的列表 11 */ 12 public static List<String> ngrams(int n, String sentence) { 13 List<String> ngrams = new ArrayList<>(); 14 String[] words = sentence.split("\\s+"); 15 for (int i = 0; i < words.length - n + 1; i++) 16 ngrams.add(concat(words, i, i + n)); 17 return ngrams; 18 }
進行單詞拼接,這里使用 StringBuilder(線程不安全,效率相對StringBuffer高點)對拆分好的單詞進行拼接並返回拼接好的字符串。
1 /** 2 * 拼接單詞 3 * 4 * @param words 單詞 5 * @param start 開始位置 6 * @param end 結束位置 7 * @return 拼接好的字符串 8 */ 9 public static String concat(String[] words, int start, int end) { 10 StringBuilder sb = new StringBuilder(); 11 for (int i = start; i < end; i++) 12 sb.append(i > start ? " " : "").append(words[i]); 13 return sb.toString(); 14 }
對 n-gram 的出現次數進行統計,使用 HashMap<String, Integer> 來存儲 n-gram 的出現次數,並且按照 value 的逆序排序 Map,次數較多的在前面先打印。這里使用 Java 8 Stream API 按照 value 降序順序進行 Map 排序。
在 Java 8 中,Map.Entry類具有靜態方法 comparingByValue() 來幫助按 value 排序,此方法返回以自然順序 Comparator 比較 Map.Entry值的。還有,你可以傳遞自定義Comparator 以用於排序。
下面是根據 value 進行排序的方法:
1 /** 2 * 按 value對 HashMap進行逆序排序 3 * <p> 4 * 使用 Java 8 Stream API按照降序對Value進行Map排序 5 * 邏輯的中心是按自然順序 Map.Entry.comparingByValue()比較 Map.Entry值的方法。 6 * 7 * @param unSortedMap 未排序的HashMap 8 * @return 按照value降序排序的HashMap 9 */ 10 public static HashMap<String, Integer> sortByValue(HashMap<String, Integer> unSortedMap) { 11 // System.out.println("Unsorted Map : " + unSortedMap); 12 13 // LinkedHashMap保留插入元素的順序 14 LinkedHashMap<String, Integer> reverseSortedMap = new LinkedHashMap<>(); 15 16 // 使用 Comparator.reverseOrder() 進行反向排序 17 unSortedMap.entrySet() 18 .stream() 19 .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) 20 .forEachOrdered(x -> reverseSortedMap.put(x.getKey(), x.getValue())); 21 22 // System.out.println("Reverse Sorted Map : " + reverseSortedMap); 23 24 return reverseSortedMap; 25 }
主函數測試代碼:
1 public static void main(String[] args) { 2 HashMap<String, Integer> count = new HashMap<>(); 3 String text = "I can go to the supermarket to buy spicy bars or go to the store to buy spicy bars."; 4 5 // 生成n為1~3的N元語法 6 // for (int n = 1; n <= 3; n++) { 7 // for (String ngram : ngrams(n, text)) { 8 // System.out.println(ngram); 9 // } 10 // System.out.println(); 11 // } 12 13 for (String ngram : ngrams(3, text)) { 14 // counting ngram by using HashMap 15 if (!count.containsKey(ngram)) { 16 count.put(ngram, 1); 17 } else if (count.containsKey(ngram)) { 18 count.replace(ngram, count.get(ngram) + 1); 19 } 20 System.out.println(ngram); 21 } 22 23 // 按出現次由多到少的順序打印ngram 24 System.out.println("\nCounting Result: "); 25 for (Map.Entry<String, Integer> entry : sortByValue(count).entrySet()) { 26 System.out.println(entry.getKey() + ": " + entry.getValue()); 27 } 28 29 }
