(轉)非主流自然語言處理——遺忘算法系列(二):大規模語料詞庫生成

(經老憨本人允許,轉載此文,原文地址:http://blog.csdn.net/gzdmcaoyc/article/details/50001801

一、前言

  寫這篇文時,突然想到一個問題,大家的詞庫都是從哪來的?
  之所以會這麼有些意外的問,是因爲從沒把詞庫當成個事兒:平時處理微博,就用程序跑一下微博語料獲得微博詞庫;處理新聞,程序跑一下新聞語料獲得新聞詞庫。甚至沒有把跑出來的詞庫存下來的習慣,誰知道過兩天是不是又出什麼新詞,與其用可能過時的,不如隨手生成個新鮮出爐的。
  好吧,我承認我這是在顯擺。如果你也想和我一樣,想要隨用隨丟,任性它一把,那隨我來。

  如果你只想要這樣一個程序,可以直奔這裏下載。
  如果你想親手寫一個,那也沒什麼,百來行代碼的事兒。

  好,咱們言歸正傳。

二、詞庫生成

  1、算法分析,先來考慮以下幾個問題

    問:目標是從文本中抽取詞語,是否可以考慮使用遺忘的方法呢?

    答:可以,詞語具備以相對穩定週期重複再現的特徵,所以可以考慮使用遺忘的方法。這意味着,我們只需要找一種適當的方法,將句子劃分成若干子串,這些子串即爲“候選詞”。在遺忘的作用下,如果“候選詞”會週期性重現,那麼它就會被保留在詞庫中,相反如果只是偶爾或隨機出現,則會逐漸被遺忘掉。

    問:那用什麼方法來把句子劃分成子串比較合適呢?

    答:考察句中任意相鄰的兩個字,相鄰兩字有兩種可能:要麼同屬於一個共同的詞,要麼是兩個詞的邊界。我們都會有這樣一種感覺,屬於同一個詞的相鄰兩字的“關係”肯定比屬於不同詞的相鄰兩字的“關係”要強烈一些。

    數學中並不缺少刻劃“關係”的模型,這裏我們選擇公式簡單並且參數容易統計的一種:如果兩個字共現的概率大於它們隨機排列在一起的概率,那麼我們認爲這兩個字有關,反之則無關。

    如果相鄰兩字無關,就可以將兩字中間斷開。逐字掃描句子,如果相鄰兩字滿足下面的公式,則將兩字斷開,如此可將句子切成若干子串,從而獲得“候選詞”集,判斷公式如下圖所示:

    公式中所需的參數可以通過統計獲得:遍歷一次語料,即可獲得公式中所需的“單字的頻數”、“相鄰兩字共現的頻數”,以及“所有單字的頻數總和”。

    問:如何計算遺忘剩餘量?

    答:使用牛頓冷卻公式,各參數在遺忘算法中的含義,如下圖所示:

    牛頓冷卻公式的詳情說明,可以參考阮一峯老師的博文《基於用戶投票的排名算法(四):牛頓冷卻定律》。

    問:參數中時間是用現實時間嗎,遺忘係數取多少合適呢?

    答:a、關於時間:

      可以使用現實時間,遺忘的發生與現實同步。

      也可以考慮用處理語料中對象的數量來代替,這樣僅當有數據處理時,纔會發生遺忘。比如按處理的字數爲計時單位,人閱讀的速度約每秒5至7個字,當然每個人的閱讀速度並不相同,這裏的參數值要求並不需要特別嚴格。

      b、遺忘係數可以參考艾賓浩斯曲線中的實驗值,如下圖(來自互聯網)

      我們取6天記憶剩餘量約爲25.4%這個值,按每秒閱讀7個字,將其代入牛頓冷卻公式可以求得遺忘係數:

      注意艾賓浩斯曲線中的每組數值代入公式,所得的係數並不相同,會對詞庫的最大有效容量產生影響。

  2、算法的代碼實現(C#版)

    呼,算法分析完成,相比於上面的文檔,還是寫代碼要容易的多。

    2.1、候選詞生成

/// <summary>  
/// 從文本中生成候選詞  
/// </summary>  
/// <param name="text">文本行</param>  
/// <param name="objCharBondColl">相鄰字典</param>  
/// <param name="objKeyWordColl">詞庫</param>  
/// <param name="bUpdateCharBondColl">是否更新相鄰字典</param>  
/// <param name="bUpdateKeyWordColl">是否更新詞庫</param>  
public static void UpdateKeyWordColl(string text, MemoryBondColl<string> objCharBondColl, MemoryItemColl<string> objKeyWordColl,  bool bUpdateCharBondColl = true, bool bUpdateKeyWordColl = true)  
{  
    if (String.IsNullOrEmpty(text)) return;  

    StringBuilder buffer = new StringBuilder();//用於存放連續的子串  
    string keyHead = text[0].ToString(); //keyHead、keyTail分別存放相鄰的兩個字符  
    buffer.Append(keyHead);  
    for (int k = 1; k < text.Length; k++) //遍歷句子中的每一個字符  
    {  

        //從句子中取一個字作爲相鄰兩字的尾字  
        string keyTail = text[k].ToString();  
        if (bUpdateCharBondColl)  
        {  
            //更新相鄰字典  
            DictionaryDAL.UpdateMemoryBondColl<string>(keyHead, keyTail, objCharBondColl);  
        }  
        if (bUpdateKeyWordColl)  
        {  
            //判斷相鄰兩字是否有關  
            if (!DictionaryDAL.IsBondValid<string>(keyHead, keyTail, objCharBondColl ) )  
            {  
                //兩字無關,則將綏中的字串取出,此即爲候選詞  
                string keyword = buffer.ToString();  
                //將候選詞添加到詞庫中  
                DictionaryDAL.UpdateMemoryItemColl<string>(keyword, objKeyWordColl);  
                //清空緩衝  
                buffer.Clear();  
                //並開始下一個子串  
                buffer.Append(keyTail);  
            }  
            else  
            {  
                //兩個字有關,則將當前字追加至串緩衝中  
                buffer.Append(keyTail);  
            }  
        }  
        //將當前的字作爲相鄰的首字  
        keyHead = keyTail;  
    }  
}  

    2.2、相鄰字統計

/// <summary>  
/// 相鄰字統計  
/// </summary>  
/// <param name="text">文本行</param>  
/// <param name="objCharBondColl">存放相鄰結果的字典</param>  
/// <remarks>遍歷句中相鄰的字,將結果存放到字典中</remarks>  
public static void UpdateCharBondColl(string text, MemoryBondColl<string> objCharBondColl)  
{  
    if (String.IsNullOrEmpty(text)) return;  
    string keyHead = text[0].ToString();  
    for (int k = 1; k < text.Length; k++)  
    {                 
        string keyTail = text[k].ToString();  
        //存入相鄰字典中  
        DictionaryDAL.UpdateMemoryBondColl<string>(keyHead, keyTail, objCharBondColl);  
        keyHead = keyTail;  
    }  
}  

    2.3、切片算法

/// <summary>  
/// 判斷鍵是否爲有效關聯鍵  
/// </summary>  
/// <typeparam name="T">C#中的範型,具體類型由調用者傳入</typeparam>  
/// <param name="keyHead">相鄰鍵中首項</param>  
/// <param name="keyTail">相鄰鍵中尾項</param>  
/// <param name="objMemoryBondColl">相鄰字典</param>  
/// <returns>返回是否判斷的結果:true、相鄰項有關;false、相鄰項無關</returns>  
/// <remarks>判斷標準:共享鍵概率 > 單字概率之積 </remarks>  
public static bool IsBondValid<T>(T keyHead, T keyTail, MemoryBondColl<T> objMemoryBondColl )  
{  
    //如果相鄰項任何一個不在相鄰字典中,則返回false 。  
    if (!objMemoryBondColl.Contains(keyHead) || !objMemoryBondColl.Contains(keyTail)) return false;  

    //分別獲得相鄰單項的頻次  
    double dHeadValidCount = CalcRemeberValue<T>(keyHead, objMemoryBondColl);  
    double dTailValidCount = CalcRemeberValue<T>(keyTail, objMemoryBondColl);  
    //獲得相鄰字典全庫的總詞頻  
    double dTotalValidCount = objMemoryBondColl.MinuteOffsetSize;  

    if (dTotalValidCount <= 0) return false;  

    //獲得相鄰項共現的頻次  
    MemoryItemColl<T> objLinkColl = objMemoryBondColl[keyHead].LinkColl;  
    if (!objLinkColl.Contains(keyTail)) return false;  
    double dShareValidCount = CalcRemeberValue<T>(keyTail, objLinkColl);  

    //返回計算的結果  
    return dShareValidCount / dHeadValidCount > dTailValidCount / dTotalValidCount;  

}  

    2.4、牛頓冷卻公式

/// <summary>  
/// 牛頓冷卻公式  
/// </summary>  
/// <param name="parameter">冷卻係數</param>  
/// <param name="interval">時間間隔</param>          
/// <returns></returns>  
/// <remarks>  
/// 建議遺忘係數:-Math.Log(0.254, Math.E) / (6天 * 24小時 * 60分鐘 *60秒 *7每秒閱讀字數);  
/// </remarks>  
public static double CalcNetonCooling(double parameter, double interval)  
{  
    return Math.Exp(-1 * parameter * interval);  
}  

    2.5、遺忘是在詞入庫的時候計算的(其實算法核心僅此一行)

/// <summary>  
/// 將候選項添加到詞典中  
/// </summary>  
/// <typeparam name="T">C#中的泛型,具體類型由調用者傳入</typeparam>  
/// <param name="keyItem">候選項</param>  
/// <param name="objMemoryItemColl">候選項詞典</param>  
public static void UpdateMemoryItemColl<T>(T keyItem, MemoryItemColl<T> objMemoryItemColl)  
{  

    if (!objMemoryItemColl.Contains(keyItem))  
    {  
        //如果詞典中不存在該候選項  

        //聲明數據對象,用於存放候選項及其相關數據  
        MemoryItemMDL<T> mdl = new MemoryItemMDL<T>();  
        mdl.Key = keyItem;//候選項  
        mdl.TotalCount = 1;//候選項出現的物理次數  
        mdl.ValidCount = 1;//邊遺忘邊累加共同作用下的有效次數  
        mdl.ValidDegree = 1;//該詞的成熟度  
        objMemoryItemColl.Add(mdl);//添加至詞典中  
    }  
    else  
    {  
        //如果詞典中已包含該候選項  

        //從詞典中取出該候選項  
        MemoryItemMDL<T> mdl = objMemoryItemColl[keyItem];  
        //計算從最後一次入庫至現在這段時間剩餘量係數  
        double dRemeberValue =  MemoryDAL.CalcRemeberValue(objMemoryItemColl.OffsetTotalCount - mdl.UpdateOffsetCount, objMemoryItemColl.MinuteOffsetSize);  
        mdl.TotalCount +=  1;//累加總計數  
        //計算成熟度,嗯,這個公式寫的比較亂,我自己現在看都有點頭暈,看不懂算了,改天琢磨換個公式  
        mdl.ValidDegree = mdl.ValidDegree * ((mdl.ValidCount * dRemeberValue) / (mdl.ValidCount * dRemeberValue + 1)) + (1 - mdl.ValidCount * (1 - dRemeberValue)) * (1 / (mdl.ValidCount * dRemeberValue + 1));  
        mdl.ValidCount =mdl.ValidCount* dRemeberValue+ 1;// 遺忘累頻=記憶保留量+1  
        mdl.UpdateOffsetCount = objMemoryItemColl.OffsetTotalCount;//更新時的偏移量(相當於記錄本次入庫的時間)  
    }  

    objMemoryItemColl.OffsetTotalCount += 1;//處理過的數據總量(相當於一個全局的計時器)  
}  

    2.6、按詞權重排序顯示詞庫(詞的權重將在後面的文章中詳解)

/// <summary>  
/// 按權重排序輸出詞庫  
/// </summary>  
/// <param name="objMemoryItemColl">詞庫</param>  
/// <param name="nKeyWordTopCount">輸出詞的數量</param>          
/// <param name="bIsOnlyWord">是否僅輸出詞</param>  
/// <returns>輸出的結果</returns>  
public static string ShowKeyWordWeightColl(MemoryItemColl<string> objMemoryItemColl, int nKeyWordTopCount,   bool bIsOnlyWord = true)  
{  
    StringBuilder sb = new StringBuilder();  
    sb.AppendLine(String.Format(" 【{0}】 | {1} | {2} | {3}", "主詞", "遺忘詞頻", "累計詞頻", "詞權值"));  

    var tbuffer = from x in objMemoryItemColl  
                  //如果只顯示詞,則要求:長度大於1,且不包含符號  
                 where !bIsOnlyWord || x.Key.Length > 1 && !Regex.IsMatch(x.Key,@"\p{P}")   
                 //按權重排序  
                  orderby x.ValidCount <= 0 ? 0 : (x.ValidCount  )* (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount)) descending  
                 select x;  
    var buffer = (tbuffer).Take(nKeyWordTopCount);  
    sb.AppendLine(String.Format("================ 共{0} 個 ================== ", buffer.Count()));  
    //逐詞輸出,每個詞一行  
    foreach (var x in buffer)  
    {  
        sb.AppendLine(String.Format(" 【{0}】 | {1} | {2} | {3}", x.Key, Math.Round(DictionaryDAL.CalcRemeberValue<string>(x.Key, objMemoryItemColl), 2), x.TotalCount, Math.Round((x.ValidCount <= 0 ? 0 : (x.ValidCount  ) * (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount))), 4)));  
    }  
    return sb.ToString();  
}  

    2.7、清理詞頻低於閥值的詞

/// <summary>  
/// 清理集合,移除低於閾值的項  
/// </summary>  
/// <typeparam name="T">C#中的泛型,具體類型由調用者傳入</typeparam>  
/// <param name="objMemoryItemColl">詞庫</param>  
/// <param name="dMinValidValue">閾值,默認值相當於至少1個記憶週期時間內,未曾再次出現</param>  
public static void ClearMemoryItemColl<T>(MemoryItemColl<T> objMemoryItemColl,double dMinValidValue=1.25)  
{  

    for (int k = objMemoryItemColl.Count - 1; k >= 0; k--)  
    {  
        //此處使用最後一次的計數即可,無需計算當前剩餘值  
        //double dValidValue = CalcRemeberValue<T>(objMemoryItemColl[k].Key, objMemoryItemColl);  
        //if (dValidValue < dMinVaildValue) objMemoryItemColl.RemoveAt(k);  

        if (objMemoryItemColl[k].ValidCount < dMinValidValue) objMemoryItemColl.RemoveAt(k);  
    }  
}  

  3、該算法生成詞庫的特點

    3.1、無監督學習

    3.2、O(N)級時間複雜度

    3.3、訓練、執行爲同一過程,可無縫處理流式數據

    3.4、未登錄詞、新詞、登錄詞沒有區別

    3.5、領域自適應:領域變化時,詞條、詞頻自適應的隨之調整

    3.6、算法中僅使用到頻數這一語言的共性特徵,無需對任何字符做特別處理,因此原理上跨語種。

三、詞庫成熟度

  由於每個詞都具備一個相對穩定的重現週期,不難證明,當訓練語料達到一定規模後,在遺忘的作用下,每個詞的詞頻在衰減和累加會達到平衡,也即衰減的速度與增加的速度基本一致。成熟的詞庫,詞頻的波動就會比較小,利用這個特徵,我們可以衡量詞庫的成熟程度。

  詞頻的這種波動也有一些妙用,比如:微博中重複發的廣告、突然暴發的熱點事件等情況,都會因詞頻的非正常變化而導致這些詞的成熟度變低。

  鑑於目前所用的模型,屬於順手寫的,算不上滿意,也沒在這塊花精力,這裏就不再細講。源代碼中包含具體的方法,算是拋磚引玉,哪位兄弟找到更好的模型公式,記得跟俺說一聲。

  修改說明,上傳的代碼中,成熟度公式修改爲:

/// <summary>  
/// 計算當前關鍵詞的成熟度  
/// </summary>  
/// <typeparam name="T">泛型,具體類別由調用者傳入</typeparam>  
/// <param name="mdl">待計算的對象</param>  
/// <param name="dRemeberValue">記憶剩餘量係數</param>  
/// <returns>當前成熟度</returns>  
/// <remarks>  
/// 1、成熟度這裏用對象遺忘與增加的量的殘差累和來表徵;  
/// 2、已經累計的殘差之和會隨時間衰減;  
/// 3、公式的意思是: 成熟度 = 成熟度衰減剩餘量 + 本次遺忘與增加量的殘差的絕對值  
/// </remarks>  
public static double CalcValidDegree<T>(MemoryItemMDL<T> mdl, double dRemeberValue)  
{  
    return mdl.ValidDegree * dRemeberValue + Math.Abs(1 - mdl.ValidCount * (1 - dRemeberValue));  
}  
發佈了30 篇原創文章 · 獲贊 75 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章