分詞算法的正向和逆向很是簡單,設計思路能夠參考這裏:html
中文分詞入門之最大匹配法 我愛天然語言處理 http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation算法
正向最大匹配,簡單來講,就是分詞的時候,每次讀取最大長度,好比7個字。而後去詞典看,有沒有這個詞,沒有,就減小一個字,再去詞典找有沒有這個詞。
如此循環,直到找到爲止。而後繼續從字符串讀取7個字,再去詞典匹配,如此循環。直到整個字符串處理完畢。
逆向就是反過來,從右往左匹配,思路同樣,不過度詞以後,要反轉排序,纔是正常人的閱讀順序。 api
通常來講,逆向匹配,比正向匹配準確率高很多。
可是,在使用搜狗拼音的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>
逆向分詞的時候,無數的廢詞,會致使分詞選擇比較亂。但若是是剛裝的搜狗拼音輸入法,並導出詞庫,應該就沒有這些問題了。
而雙向匹配,就是進行正向 + 逆向匹配。
若是 正反向分詞結果同樣,說明沒有歧義,就是分詞成功。
若是 正反向結果不同,說明有歧義,就要處理。 ide
雙向匹配,關鍵就在消除歧義的方法上。
常見的歧義消除方法,好比,從正反向的分詞結果中:選擇分詞數量較少的那個 或者 選擇單字較少的那個 或者 選擇分詞長度方差最小的那個。
還有個聽說效果很好的方法是:罰分策略。
簡單來講,就是有不少中文字,幾乎是永遠不會單獨出現的,老是做爲一個詞出現。那麼,就單獨作個字典,把這些字加進去。
若是分詞結果中,包含這樣的單字,說明這個分詞結果有問題,就要扣分。每出現一個,就扣一分。正反分詞結果通過扣分以後,哪一個扣分少,哪一個就是最優結果。
這個策略聽起來很憑感受,很麻煩。但我發現這個思路能夠用於解決一個很頭痛的問題:多音字。 post
多音字,按道理,就不該該單獨出現,否則鬼知道它應該讀什麼。
所以,經過檢查分詞結果中,是否存在單獨的多音字,就知道分詞結果是否優秀。
中文的多音字是如此之多,所以這個辦法效果應該會至關不錯。 學習
而我本身,則小小創新了一下,作了個貪吃蛇法,來消除歧義。 優化
思路是這樣的:要進行分詞的字符串,就是食物。有2個貪吃蛇,一個從左向右吃;另外一個從右向左吃。只要左右分詞結果有歧義,2條蛇就咬下一口。貪吃蛇吃下去的字符串,就變成分詞。
若是左右分詞一直有歧義,兩條蛇就一直吃。兩條蛇吃到字符串中間交匯時,就確定不會有歧義了。
這時候,左邊貪吃蛇肚子裏的分詞,中間沒有歧義的分詞,右邊貪吃蛇肚子裏的分詞,3個一拼,就是最終的分詞結果。 ui
程序上,是這樣的:
對字符串進行左右(正反)分詞,若是有歧義,就截取字符串兩頭的第一個分詞,做爲最終分詞結果的兩頭。
兩頭被各吃一口的字符串,就變短了。這個變短的字符串,從新分詞,再次比較左右分詞有沒有歧義。
若是一直有歧義,就一直循環。直到沒有歧義,或者字符串不斷被從兩頭截短,最後什麼也不剩下,也就不可能有歧義了。
這個方法,就是把正向分詞結果的左邊,和反向分詞結果的右邊,不斷的向中間進行拼接,最終獲得分詞結果。
算法設計緣由:
我發現,人在閱讀中文的時候,並不徹底是從左向右讀的。
當一句話有重大歧義,大腦不能立馬找到合理解釋的時候,會本能的開始嘗試從後往前去看這句話,試圖找到一個合理的解釋。
就像中學生作數學證實題時,正向推導很困難,就會嘗試逆向推導。而後發現,正向推導的中間產物 和 逆向推導的中間產物,恰好一致,因而證實題完成。
這個貪吃蛇算法,就是基於這個緣由而設計。
不過我暫時無法去驗證這個算法是否真的優越。
由於我沒有去優化詞庫。。。如今用的是我從搜狗拼音導出來的原生詞庫,裏面有我多年上網造成的無數錯別字詞條。。。並且因爲我我的的興趣偏向,我導出的詞庫,缺乏大量經常使用詞。好比「名著」這個詞。這輩子沒用到過。。。
最後,貼上這個分詞算法類的完整代碼,直接就能用:
包含了 正反向最大匹配,雙向匹配,以及貪吃蛇消除歧義法
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;
}
}
}
本文用到的庫下載:點此下載
詞庫下載:點此下載
將詞庫直接放到項目根目錄
詞庫設置以下:

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

完整的盤古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>
View Code
在全局文件中填入如下代碼
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;
}
設置過濾詞(注意這裏的過濾詞不要放在第一個上)
