分詞算法的正向和逆向非常簡單,設計思路可以參考這里:
中文分詞入門之最大匹配法 我愛自然語言處理 http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation
正向最大匹配,簡單來說,就是分詞的時候,每次讀取最大長度,比如7個字。然后去詞典看,有沒有這個詞,沒有,就減少一個字,再去詞典找有沒有這個詞。
如此循環,直到找到為止。然后繼續從字符串讀取7個字,再去詞典匹配,如此循環。直到整個字符串處理完畢。
逆向就是反過來,從右往左匹配,思路一樣,不過分詞之后,要反轉排序,才是正常人的閱讀順序。
一般來說,逆向匹配,比正向匹配准確率高不少。
但是,在使用搜狗拼音的17萬詞庫作為詞典后,正向匹配的准確率,明顯比逆向匹配高。
原因是,搜狗拼音的詞庫,為了處理生活中非常隨意的文字輸入,每次輸入一個隨意的短語或者錯別字,就創建了一個新詞。因此,這個詞庫有大量廢詞和錯詞。
比如:
<ns1:InputString>yao4 zhuang1</ns1:InputString> <ns1:OutputString>要裝</ns1:OutputString> </ns1:DictionaryEntry> <ns1:DictionaryEntry> <ns1:InputString>yao4 zhuang1 qu1 dong4</ns1:InputString> <ns1:OutputString>要裝驅動</ns1:OutputString> </ns1:DictionaryEntry> <ns1:DictionaryEntry> <ns1:InputString>yao4 zhuang4 tai4</ns1:InputString> <ns1:OutputString>要狀態</ns1:OutputString> </ns1:DictionaryEntry> <ns1:DictionaryEntry> <ns1:InputString>yao4 zhui1 cha2</ns1:InputString> <ns1:OutputString>要追查</ns1:OutputString> </ns1:DictionaryEntry>
而雙向匹配,就是進行正向 + 逆向匹配。
如果 正反向分詞結果一樣,說明沒有歧義,就是分詞成功。
如果 正反向結果不一樣,說明有歧義,就要處理。
雙向匹配,關鍵就在消除歧義的方法上。
常見的歧義消除方法,比如,從正反向的分詞結果中:選擇分詞數量較少的那個 或者 選擇單字較少的那個 或者 選擇分詞長度方差最小的那個。
還有個據說效果很好的方法是:罰分策略。
簡單來說,就是有很多中文字,幾乎是永遠不會單獨出現的,總是作為一個詞出現。那么,就單獨做個字典,把這些字加進去。
如果分詞結果中,包含這樣的單字,說明這個分詞結果有問題,就要扣分。每出現一個,就扣一分。正反分詞結果經過扣分之后,哪個扣分少,哪個就是最優結果。
這個策略聽起來很憑感覺,很麻煩。但我發現這個思路可以用於解決一個很頭痛的問題:多音字。
多音字,按道理,就不應該單獨出現,不然鬼知道它應該讀什么。
因此,通過檢查分詞結果中,是否存在單獨的多音字,就知道分詞結果是否優秀。
中文的多音字是如此之多,因此這個辦法效果應該會相當不錯。
而我自己,則小小創新了一下,做了個貪吃蛇法,來消除歧義。
思路是這樣的:要進行分詞的字符串,就是食物。有2個貪吃蛇,一個從左向右吃;另一個從右向左吃。只要左右分詞結果有歧義,2條蛇就咬下一口。貪吃蛇吃下去的字符串,就變成分詞。
如果左右分詞一直有歧義,兩條蛇就一直吃。兩條蛇吃到字符串中間交匯時,就肯定不會有歧義了。
這時候,左邊貪吃蛇肚子里的分詞,中間沒有歧義的分詞,右邊貪吃蛇肚子里的分詞,3個一拼,就是最終的分詞結果。
程序上,是這樣的:
對字符串進行左右(正反)分詞,如果有歧義,就截取字符串兩頭的第一個分詞,作為最終分詞結果的兩頭。
兩頭被各吃一口的字符串,就變短了。這個變短的字符串,重新分詞,再次比較左右分詞有沒有歧義。
如果一直有歧義,就一直循環。直到沒有歧義,或者字符串不斷被從兩頭截短,最后什么也不剩下,也就不可能有歧義了。
這個方法,就是把正向分詞結果的左邊,和反向分詞結果的右邊,不斷的向中間進行拼接,最終得到分詞結果。
算法設計原因:
我發現,人在閱讀中文的時候,並不完全是從左向右讀的。
當一句話有重大歧義,大腦不能立馬找到合理解釋的時候,會本能的開始嘗試從后往前去看這句話,試圖找到一個合理的解釋。
就像中學生做數學證明題時,正向推導很困難,就會嘗試逆向推導。然后發現,正向推導的中間產物 和 逆向推導的中間產物,剛好一致,於是證明題完成。
這個貪吃蛇算法,就是基於這個原因而設計。
不過我暫時沒法去驗證這個算法是否真的優越。
因為我沒有去優化詞庫。。。現在用的是我從搜狗拼音導出來的原生詞庫,里面有我多年上網形成的無數錯別字詞條。。。而且由於我個人的興趣偏向,我導出的詞庫,缺少大量常用詞。比如“名著”這個詞。這輩子沒用到過。。。
最后,貼上這個分詞算法類的完整代碼,直接就能用:
包含了 正反向最大匹配,雙向匹配,以及貪吃蛇消除歧義法
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TextToPinyin.Helper { /// <summary> /// 分詞算法 /// </summary> static class Segmentation { /// <summary> /// 用最大匹配算法進行分詞,正反向均可。 /// 為了節約內存,詞典參數是引用傳遞 /// </summary> /// <param name="inputStr">要進行分詞的字符串</param> /// <param name="wordList">詞典</param> /// <param name="leftToRight">true為從左到右分詞,false為從右到左分詞</param> /// <param name="maxLength">每個分詞的最大長度</param> /// <returns>儲存了分詞結果的字符串數組</returns> public static List<string> SegMM(string inputStr, ref List<string> wordList, bool leftToRight, int maxLength) { //指定詞典不能為空 if (wordList == null) return null; //指定要分詞的字符串也不能為空 if (string.IsNullOrEmpty(inputStr)) return null; //取詞的最大長度,必須大於0 if (!(maxLength > 0)) return null; //分詞的方向,true=從左到右,false=從右到左 //用於儲存正向分詞的字符串數組 List<string> segWords = new List<string>(); //用於儲存逆向分詞的字符串數組 List<string> segWordsReverse = new List<string>(); //用於嘗試分詞的取詞字符串 string word = ""; //取詞的當前長度 int wordLength = maxLength; //分詞操作中,處於字符串中的當前位置 int position = 0; //分詞操作中,已經處理的字符串總長度 int segLength = 0; //開始分詞,循環以下操作,直到全部完成 while (segLength < inputStr.Length) { //如果還沒有進行分詞的字符串長度,小於取詞的最大長度,則只在字符串長度內處理 if ((inputStr.Length - segLength) < maxLength) wordLength = inputStr.Length - segLength; //否則,按最大長度處理 else wordLength = maxLength; //從左到右 和 從右到左截取時,起始位置不同 //剛開始,截取位置是字符串兩頭,隨着不斷循環分詞,截取位置會不斷推進 if (leftToRight) position = segLength; else position = inputStr.Length - segLength - wordLength; //按照指定長度,從字符串截取一個詞 word = inputStr.Substring(position, wordLength); //在字典中查找,是否存在這樣一個詞 //如果不包含,就減少一個字符,再次在字典中查找 //如此循環,直到只剩下一個字為止 while (!wordList.Contains(word)) { //如果只剩下一個單字,就直接退出循環 if (word.Length == 1) break; //把截取的字符串,最邊上的一個字去掉 //注意,從左到右 和 從右到左時,截掉的字符的位置不同 if (leftToRight) word = word.Substring(0, word.Length - 1); else word = word.Substring(1); } //將分出的詞,加入到分詞字符串數組中,正向和逆向不同 if (leftToRight) segWords.Add(word); else segWordsReverse.Add(word); //已經完成分詞的字符串長度,要相應增加 segLength += word.Length; } //如果是逆向分詞,還需要對分詞結果反轉排序 if (!leftToRight) { for (int i = 0; i < segWordsReverse.Count; i++) { //將反轉的結果,保存在正向分詞數組中 segWords.Add(segWordsReverse[segWordsReverse.Count - 1 - i]); } } //返回儲存着正向分詞的字符串數組 return segWords; } /// <summary> /// 用最大匹配算法進行分詞,正反向均可,每個分詞最大長度是7。 /// 為了節約內存,詞典參數是引用傳遞 /// </summary> /// <param name="inputStr">要進行分詞的字符串</param> /// <param name="wordList">詞典</param> /// <param name="leftToRight">true為從左到右分詞,false為從右到左分詞</param> /// <returns>儲存了分詞結果的字符串數組</returns> public static List<string> SegMM(string inputStr, ref List<string> wordList, bool leftToRight) { return SegMM(inputStr, ref wordList, leftToRight, 7); } /// <summary> /// 用最大匹配算法進行分詞,正向,每個分詞最大長度是7。 /// 為了節約內存,詞典參數是引用傳遞 /// </summary> /// <param name="inputStr">要進行分詞的字符串</param> /// <param name="wordList">詞典</param> /// <returns>儲存了分詞結果的字符串數組</returns> public static List<string> SegMMLeftToRight(string inputStr, ref List<string> wordList) { return SegMM(inputStr, ref wordList, true, 7); } /// <summary> /// 用最大匹配算法進行分詞,反向,每個分詞最大長度是7。 /// 為了節約內存,詞典參數是引用傳遞 /// </summary> /// <param name="inputStr">要進行分詞的字符串</param> /// <param name="wordList">詞典</param> /// <returns>儲存了分詞結果的字符串數組</returns> public static List<string> SegMMRightToLeft(string inputStr, ref List<string> wordList) { return SegMM(inputStr, ref wordList, false, 7); } /// <summary> /// 比較兩個字符串數組,是否所有內容完全相等。 /// 為了節約內存,參數是引用傳遞 /// </summary> /// <param name="strList1">待比較字符串數組01</param> /// <param name="strList2">待比較字符串數組02</param> /// <returns>完全相同返回true</returns> private static bool CompStringList(ref List<string> strList1, ref List<string> strList2) { //待比較的字符串數組不能為空 if (strList1 == null || strList2 == null) return false; //待比較的字符串數組長度不同,就說明不相等 if (strList1.Count != strList2.Count) return false; else { //逐個比較數組中,每個字符串是否相同 for (int i = 0; i < strList1.Count; i++) { //只要有一個不同,就說明字符串不同 if (strList1[i] != strList2[i]) return false; } } return true; } /// <summary> /// 用最大匹配算法進行分詞,雙向,每個分詞最大長度是7。 /// 為了節約內存,字典參數是引用傳遞 /// </summary> /// <param name="inputStr">要進行分詞的字符串</param> /// <param name="wordList">詞典</param> /// <returns>儲存了分詞結果的字符串數組</returns> public static List<string> SegMMDouble(string inputStr, ref List<string> wordList) { //用於儲存分詞的字符串數組 //正向 List<string> segWordsLeftToRight = new List<string>(); //逆向 List<string> segWordsRightToLeft = new List<string>(); //定義拼接后的分詞數組 List<string> segWordsFinal = new List<string>(); //用於保存需要拼接的左、右、中間分詞碎塊 List<string> wordsFromLeft = new List<string>(); List<string> wordsFromRight = new List<string>(); List<string> wordsAtMiddle = new List<string>(); //通過循環,進行正反向分詞,如果有歧義,就截短字符串兩頭,繼續分詞,直到消除歧義,才結束 //整個思路就像貪吃蛇,從兩頭,一直吃到中間,把一個字符串吃完。 // //每次循環,得到正反向分詞后,進行比較,判斷是否有歧義 //如果沒有歧義,貪吃蛇就不用繼續吃了,把分詞結果保存,待會用於拼接 //如果有歧義,就取正向分詞的第一個詞,和反向分詞的最后一個詞,拼接到最終分詞結果的頭尾 //而輸入字符串,則相應的揭短掉頭尾,得到的子字符串,重新進行正反向分詞 //如此循環,直到完成整個輸入字符串 // //循環結束之后,就是把上面"貪吃蛇"吃掉的左、右分詞結果以及沒有歧義的中間分詞結果,拼接起來。 //進行正反向分詞 //正向 segWordsLeftToRight = SegMMLeftToRight(inputStr, ref wordList); //逆向 segWordsRightToLeft = SegMMRightToLeft(inputStr, ref wordList); //判斷兩頭的分詞拼接,是否已經在輸入字符串的中間交匯,只要沒有交匯,就不停循環 while ((segWordsLeftToRight[0].Length + segWordsRightToLeft[segWordsRightToLeft.Count-1].Length) < inputStr.Length) { //如果正反向的分詞結果相同,就說明沒有歧義,可以退出循環了 //正反向分詞中,隨便取一個,復制給middle的臨時變量即可 if (CompStringList(ref segWordsLeftToRight, ref segWordsRightToLeft)) { wordsAtMiddle = segWordsLeftToRight.ToList<string>(); break; } //如果正反向分詞結果不同,則取分詞數量較少的那個,不用再循環 if (segWordsLeftToRight.Count < segWordsRightToLeft.Count) { wordsAtMiddle = segWordsLeftToRight.ToList<string>(); break; } else if (segWordsLeftToRight.Count > segWordsRightToLeft.Count) { wordsAtMiddle = segWordsRightToLeft.ToList<string>(); break; } //如果正反分詞數量相同,則返回其中單字較少的那個,也不用再循環 { //計算正向分詞結果中,單字的個數 int singleCharLeftToRight = 0; for (int i = 0; i < segWordsLeftToRight.Count; i++) { if (segWordsLeftToRight[i].Length == 1) singleCharLeftToRight++; } //計算反向分詞結果中,單字的個數 int singleCharRightToLeft = 0; for (int j = 0; j < segWordsRightToLeft.Count; j++) { if (segWordsRightToLeft[j].Length == 1) singleCharRightToLeft++; } //比較單字個數多少,返回單字較少的那個 if (singleCharLeftToRight < singleCharRightToLeft) { wordsAtMiddle = segWordsLeftToRight.ToList<string>(); break; } else if (singleCharLeftToRight > singleCharRightToLeft) { wordsAtMiddle = segWordsRightToLeft.ToList<string>(); break; } } //如果以上措施都不能消除歧義,就需要繼續循環 //將正向"貪吃蛇"的第一個分詞,放入臨時變量中,用於結束循環后拼接 wordsFromLeft.Add(segWordsLeftToRight[0]); //將逆向"貪吃蛇"的最后一個分詞,放入臨時變量,用於結束循環后拼接 wordsFromRight.Add(segWordsRightToLeft[segWordsRightToLeft.Count-1]); //將要處理的字符串從兩頭去掉已經分好的詞 inputStr = inputStr.Substring(segWordsLeftToRight[0].Length); inputStr = inputStr.Substring(0, inputStr.Length - segWordsRightToLeft[segWordsRightToLeft.Count - 1].Length); //繼續次循環分詞 //分詞之前,清理保存正反分詞的變量,防止出錯 segWordsLeftToRight.Clear(); segWordsRightToLeft.Clear(); //進行正反向分詞 //正向 segWordsLeftToRight = SegMMLeftToRight(inputStr, ref wordList); //逆向 segWordsRightToLeft = SegMMRightToLeft(inputStr, ref wordList); } //循環結束,說明要么分詞沒有歧義了,要么"貪吃蛇"從兩頭吃到中間交匯了 //如果是在中間交匯,交匯時的分詞結果,還要進行以下判斷: //如果中間交匯有重疊了: // 正向第一個分詞的長度 + 反向最后一個分詞的長度 > 輸入字符串總長度,就直接取正向的 // 因為剩下的字符串太短了,2個分詞就超出長度了 if ((segWordsLeftToRight[0].Length + segWordsRightToLeft[segWordsRightToLeft.Count-1].Length) > inputStr.Length) { wordsAtMiddle = segWordsLeftToRight.ToList<string>(); } //如果中間交匯,剛好吃完,沒有重疊: // 正向第一個分詞 + 反向最后一個分詞的長度 = 輸入字符串總長度,那么正反向一拼即可 else if ((segWordsLeftToRight[0].Length + segWordsRightToLeft[segWordsRightToLeft.Count - 1].Length) == inputStr.Length) { wordsAtMiddle.Add(segWordsLeftToRight[0]); wordsAtMiddle.Add(segWordsRightToLeft[segWordsRightToLeft.Count - 1]); } //將之前"貪吃蛇"正反向得到的分詞,以及中間沒有歧義的分詞,進行合並。 //將左邊的貪吃蛇的分詞,添加到最終分詞詞組 foreach (string wordLeft in wordsFromLeft) { segWordsFinal.Add(wordLeft); } //將中間沒有歧義的分詞,添加到最終分詞詞組 foreach (string wordMiddle in wordsAtMiddle) { segWordsFinal.Add(wordMiddle); } //將右邊的貪吃蛇的分詞,添加到最終分詞詞組,注意,右邊的添加是逆向的 for (int i = 0; i < wordsFromRight.Count; i++ ) { segWordsFinal.Add(wordsFromRight[wordsFromRight.Count-1-i]); } //返回完成的最終分詞 return segWordsFinal; } } }
出處:https://my.oschina.net/butaixianran/blog/164042
====================================================
本文用到的庫下載:點此下載
詞庫下載:點此下載
將詞庫直接放到項目根目錄
詞庫設置如下:

類庫說明
詞庫查看程序:點此下載
可以在上面的程序中添加常用行業詞庫 還可以通過下面的類在程序中實現

完整的盤古release:點此下載
最新字典文件下載位置
http://pangusegment.codeplex.com/releases/view/47411
默認字典位置為 ..\Dictionaries 你可以通過設置PanGu.xml 文件來修改字典的位置
Demo.exe 分詞演示程序
DictManage.exe 字典管理程序
PanGu.xml 分詞配置文件
PanGu.HighLight.dll 高亮組件
Lucene.Net
Lucene.net是Lucene的.net移植版本,是一個開源的全文檢索引擎開發包,即它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,是一個Library.你也可以把它理解為一個將索引,搜索功能封裝的很好的一套簡單易用的API(提供了完整的查詢引擎和索引引擎)。利用這套API你可以做很多有關搜索的事情,而且很方便.。開發人員可以基於Lucene.net實現全文檢索的功能。
注意:Lucene.Net只能對文本信息進行檢索。如果不是文本信息,要轉換為文本信息,比如要檢索Excel文件,就要用NPOI把Excel讀取成字符串,然后把字符串扔給Lucene.Net。Lucene.Net會把扔給它的文本切詞保存,加快檢索速度。

ok,接下來就細細詳解下士怎樣一步一步實現這個效果的。
Lucene.Net 核心——分詞算法(Analyzer)
學習Lucune.Net,分詞是核心。當然最理想狀態下是能自己擴展分詞,但這要很高的算法要求。Lucene.Net中不同的分詞算法就是不同的類。所有分詞算法類都從Analyzer類繼承,不同的分詞算法有不同的優缺點。
內置的StandardAnalyzer是將英文按照空格、標點符號等進行分詞,將中文按照單個字進行分詞,一個漢字算一個詞
namespace EazyCMS.Common { /// <summary> /// 分詞類 /// </summary> public class Participle { public List<string> list = new List<string>(); public void get_participle() { Analyzer analyzer = new StandardAnalyzer(); TokenStream tokenStream = analyzer.TokenStream("",new StringReader("Hello Lucene.Net,我1愛1你China")); Lucene.Net.Analysis.Token token = null; while ((token = tokenStream.Next()) != null) { //Console.WriteLine(token.TermText()); string s = token.TermText(); } } } }

二元分詞算法,每兩個漢字算一個單詞,“我愛你China”會分詞為“我愛 愛你 china”,點擊查看二元分詞算法CJKAnalyzer。
namespace EazyCMS.Common { /// <summary> /// 分詞類 /// </summary> public class Participle { public List<string> list = new List<string>(); public void get_participle() { //Er Analyzer analyzer = new CJKAnalyzer(); TokenStream tokenStream = analyzer.TokenStream("", new StringReader("我愛你中國China中華人名共和國")); Lucene.Net.Analysis.Token token = null; while ((token = tokenStream.Next()) != null) { Response.Write(token.TermText() + "<br/>"); } } } }

這時,你肯定在想,上面沒有一個好用的,二元分詞算法亂槍打鳥,很想自己擴展Analyzer,但並不是算法上的專業人士。怎么辦?
天降聖器,盤古分詞,
盤古分詞的用法 首先引用以上的盤古dll 文件
將xml文件放在項目的根目錄下
<?xml version="1.0" encoding="utf-8"?> <PanGuSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.codeplex.com/pangusegment"> <DictionaryPath>Dict</DictionaryPath> <MatchOptions> <ChineseNameIdentify>true</ChineseNameIdentify> <FrequencyFirst>false</FrequencyFirst> <MultiDimensionality>false</MultiDimensionality> <EnglishMultiDimensionality>true</EnglishMultiDimensionality> <FilterStopWords>true</FilterStopWords> <IgnoreSpace>true</IgnoreSpace> <ForceSingleWord>false</ForceSingleWord> <TraditionalChineseEnabled>false</TraditionalChineseEnabled> <OutputSimplifiedTraditional>false</OutputSimplifiedTraditional> <UnknownWordIdentify>true</UnknownWordIdentify> <FilterEnglish>false</FilterEnglish> <FilterNumeric>false</FilterNumeric> <IgnoreCapital>false</IgnoreCapital> <EnglishSegment>false</EnglishSegment> <SynonymOutput>false</SynonymOutput> <WildcardOutput>false</WildcardOutput> <WildcardSegment>false</WildcardSegment> <CustomRule>false</CustomRule> </MatchOptions> <Parameters> <UnknowRank>1</UnknowRank> <BestRank>5</BestRank> <SecRank>3</SecRank> <ThirdRank>2</ThirdRank> <SingleRank>1</SingleRank> <NumericRank>1</NumericRank> <EnglishRank>5</EnglishRank> <EnglishLowerRank>3</EnglishLowerRank> <EnglishStemRank>2</EnglishStemRank> <SymbolRank>1</SymbolRank> <SimplifiedTraditionalRank>1</SimplifiedTraditionalRank> <SynonymRank>1</SynonymRank> <WildcardRank>1</WildcardRank> <FilterEnglishLength>0</FilterEnglishLength> <FilterNumericLength>0</FilterNumericLength> <CustomRuleAssemblyFileName>CustomRuleExample.dll</CustomRuleAssemblyFileName> <CustomRuleFullClassName>CustomRuleExample.PickupVersion</CustomRuleFullClassName> <Redundancy>0</Redundancy> </Parameters> </PanGuSettings>
在全局文件中填入以下代碼
protected void Application_Start(object sender, EventArgs e)
{
//log4net.Config.XmlConfigurator.Configure();
//logger.Debug("程序開始");
Segment.Init(HttpContext.Current.Server.MapPath("~/PanGu.xml"));
}
分詞方法
Segment segment = new Segment(); var ss = segment.DoSegment("海信的,家就看到"); foreach (var s in ss) { string sa = s.Word; }
設置過濾詞(注意這里的過濾詞不要放在第一個上)


