A Neural Probabilistic Language Model


A Neural Probabilistic Language Model,這篇論文是Begio等人在2003年發表的,可以說是詞表示的鼻祖。在這里給出簡要的譯文

 

A Neural Probabilistic Language Model

一個神經概率語言模型

  要

 

    統計語言模型的一個目標是學習一種語言的單詞序列的聯合概率函數。因為維數災難,這是其本質難點:將被模型測試的單詞序列很可能是與在訓練中見過的所有單詞的序列都不相同。傳統的但非常成功的基於n-gram的方法通過將出現在訓練集很短的重疊序列連接得到泛化。為了解決維數災難的問題,我們提出學習詞的分布式表示,這種方法允許每一個訓練語句給模型提供關於語義相鄰句子的指數級別數量的信息。根據剛才的描述,該模型同時學習(1)每個詞的分布式表示與(2)詞序列的概率函數。模型可以得到泛化是因為一個從未出現的詞序列,如果它是由與它相似的詞(在其附近的一個代表性的意義上)組成過已經出現的句子的話,那么它獲得較高的概率。在合理的時間內訓練這樣的大型模型(以百萬計的參數)本身就是一個顯著的挑戰。我們報告基於神經網絡的概率函數的實驗,顯示出在兩個文本語料庫,該方法顯著改進了最先進的n-gram模型,而且該方法允許利用較長的上下文優勢。

    關鍵詞:統計語言模型, 人工神經網絡, 分布式表示, 維數災難

1.  介紹

    使語言建模和其他學習問題困難的一個根本的問題是維數災難。當一個人想對很多離散隨機變量(如在句子中的單詞或者數據挖掘任務中的離散分布)建立聯合分布模型時,這個問題尤其明顯。例如,如果一個人想要對自然語言中單詞表大小為100000的10個相連的詞建立聯合分布模型,將會有100 00010  1 = 1050 – 1個自由參數。當對連續變量建立模型時,我們更容易得到泛化(如光滑的類的函數像多層神經網絡或Gaussian混合模型),因為要學習的函數可以被期望將有一些LO- CAL的平滑性。對於離散的空間,泛化結構並不明顯:這些離散隨機變量的任何變化可能對要估計的函數的值產生極大的影響,並且當每個離散的變量取值范圍很大時,大多數觀察到的對象在漢明距離上是幾乎無窮遠的。

      一個統計語言模型可以表示成給定前面詞后面一個詞出現的條件概率:

    其中,wt代表第t個詞,把子序列寫成wij。這種語言模型已經被發現在很多自然語言處理領域有作用,例如語音識別,語言翻譯,信息檢索。統計語言模型性能的提升因此能夠對這些應用有顯著的影響。

    在建立統計語言模型時,一個可以考慮的降低模型困難的方法是詞序列中更靠近的詞更加具有依賴性。因此,n-gram模型建立了一個給定前n-1個詞,第n個詞的條件概率表示:

    我們只考慮在訓練集中出現的連續詞的組合,或者出現足夠頻繁的詞。當在語料中未見過的n個詞的新組合出現時,將發生什么?我們不想為它們分配為0的概率,因為這樣的組合確實有可能發生。一個簡單的解決辦法是使用更小的上下文,即使用tri-gram或者平滑后的tri-gram。本質上來說,一個新的詞序列是通過“粘合”非常短的重疊的在訓練語料中出現頻繁的字片段組成。獲得下一個片段的概率的規則是隱式的回退或者打折后的n-gram算法。研究者使用典型的n=3的tri-gram,並且獲得了世界領先水平的結果。顯然的是直接出現在詞前面的序列攜帶的信息要比僅僅之前的一小段序列攜帶的信息多。我們在本論文中提出的方法至少在兩個特點上面顯著的提高了上面的問題。第一點,上面的方法沒有考慮超過1或2個詞的上下文;第二點,上面的方法沒有考慮詞與詞之間的相似性。例如,在語料庫中已經觀測到了序列“The cat is walking in the bedroom”,可以幫助我們生成序列“A dog was running in a room”,因為“dog”和“cat”有相似的語義和語法角色。

    有很多被提出來的方法可以解決這兩個問題,我們在1.2節給出簡潔的解釋。我們首先將討論被提出方法的基本思想。更加形式化的介紹將在2節中給出。這些思想的實現使用的是同享參數的多層神經網絡。這篇論文的另一個貢獻是介紹了對大量數據訓練如此大的神經網絡的高效方法。最后,一個重要的貢獻是說明了訓練如此大規模的模型是昂貴但是值得的。

    這片論文的很多運算使用矩陣符號,使用小寫字母v代表列向量,v’代表它的轉置,Aj表示矩陣A的第j行,x.y代表x’y。

    1.1  使用分布式表示解決維數災難

    簡單來講,本方法的思想可以被概括成以下3個步驟:

        1. 為在詞表中的每一個詞分配一個分布式的詞特征向量

        2. 詞序列中出現的詞的特征向量表示的詞序列的聯合概率函數

        3.學習詞特征向量和概率函數的參數

    詞特征向量代表了詞的不同的方面:每個詞關聯向量空間的一個點。特征的數量遠遠小於詞表的大小。概率函數被表達成給定前面的詞后面一個詞的條件概率的乘積(例如,在實驗中,使用多層神經網絡,給定前面的詞預測下一個詞)。這個函數有一些參數,可以通過迭代的方式調整這些參數來最大化對數似然函數。這些與詞關聯的特征向量可以被學習得當,但是他們可以使用先驗的語義特征知識來初始化。

    為什么這樣有效?在前面的例子中,如果我們知道 “ dog”和“cat”扮演相似的角色(語義的或者句法的),類似的對於(the,a),(bedroom,room),(is,was),(running,walking),我們自然地可以由

        The cat is walking in the bedroom

    生成

        A dog was running in a room

    或者

        The cat is runing in a room

        A dog is walking in a bedroom

        The dog was walking in the room

        …

    或者更多的其他組合。在本模型中,這些可以被生成因為相似的詞被期望有相似的特征向量,也因為概率函數是一個這些特征值的平滑的函數,在特征中的小的改變將在概率中產生小的變化。因此,上述這些句子其中一個在語料庫中的出現,將增加這些句子的概率。

    1.2  與前面工作的關系

    使用神經網絡對高維離散分布建模已經被發現可以有效的學習其聯合概率。在這個模型中,聯合概率被分解為條件概率的乘積

    其中,g(x)是被左到右結構神經網絡表示的函數。第i個輸出塊gi計算表達給定之前Z,Zi的條件概率的參數。在四個UCI數據集上的實驗證明了這個方法可以工作的很好。這里我們必須處理可變長度的數據,像句子,因此上面的方法必須被變形。

2. 一個神經模型

    訓練集是一個詞序列w1,…,wT,其中wtV,詞表V是一個大但是有限的集合。模型的目標是要學到一個好的函數來估計條件概率:

    需要滿足的約束為:

    其中,wt表示詞序列的第t個詞;V表示詞表,|V|表示詞表的大小。通過條件概率的成績可以獲得詞序列的聯合概率。

 

我們把函數分解為兩個部分:

        1. 一個映射C,從詞表中的任意元素i到實向量C(i)∈Rm。它代表關聯詞表中詞的分布特征向量。在實踐中,C被表示成一個|V|×m的自由參數矩陣。 

        2. 詞上的概率函數,用C表達:一個函數g,從輸入序列的詞的上下文特征向量,(C(wt-n+1),…,C(wt-1)),到詞表中下一個詞i的條件概率分布。g的輸出是一個向量,向量的第i個元素估計概率,如下圖

1  神經網絡語言模型結構圖

 

    函數f是這兩個映射C和g的組合。這兩個映射都關聯一些參數。映射C的參數就是特征向量本身,被表示成一個|V|×m的矩陣C,C的第i行是詞i的特征向量。函數g可以被一個前饋神經網絡或者卷積神經網絡實現或者其他的參數化函數實現。

    訓練的被實現為尋找θ使得訓練數據的對數似然函數最大化

    其中θ為參數,R(θ)為正則項,例如,在我們的實驗中,R是一個權重的懲罰,僅僅是神經網絡的權重和矩陣C。

    在上述的模型中,自由參數的數量是詞表V大小的線性函數。自由參數的數量也是序列長度n的線性函數。

    在下面的大多數實驗中,神經網絡有一個隱藏層,隱藏成在詞特征映射的前面,直接連接詞特征到輸出層。因此,實際上是由兩個隱藏層:共享詞特征層C和雙曲正切隱藏層。

    輸出層采用softmax函數:

 

    其中yi是每個輸出詞i的未歸一化log概率,計算如下:

   其中b,W,U,d和H都是參數,x為輸入,則θ=(b, W, U, d,H)。雙曲正切被一個元素接一個元素的作用域向量中。當神經網絡中隱藏單元的數目為h,詞表大小為|V|時,b是|V|維的列向量,W是|V|×(n-1)m的矩陣,U是|V|×h的矩陣,d是h維的列向量,H是h×(n-1)m的矩陣。需要注意的是,一般的神經網絡輸入是不需要優化,而在這里,x=(C(wt-1 ),C(wt-2 ),…,C(wt-n+1)),也是需要優化的參數。在圖4-1中,如果下層原始輸入 x 不直接連到輸出的話,可令W=0。

  自由參數的數量是|V|(1+nm+h)+h(1+(n-1)m).其中的主要因子是|V|(nm+h)。

    如果采用隨機梯度算法的話,梯度更新的法則為:

   其中ε為學習速度(learning rate)。需要注意的是,一般神經網絡的輸入層只是一個輸入值,而在這里,輸入層x也是參數(存在C中),也是需要優化的。優化結束之后,語言模型訓練完成。

3.  並行化的實現

    即使參數的數量是輸入窗口大小n和詞表大小|V|的線性函數,即已經被限制的很好,但是計算的總量還是遠遠大於n-gram。主要原因是在n-gram模型中,獲得特定的p(wt|wt-1,…,wt-n+1)不需要計算詞表中所有的概率,因為簡單的歸一化。神經網絡計算的瓶頸主要是在輸出層。運行模型(在訓練和測試時)在一個並行化的計算機中是減少計算時間的方法,我們在兩種平台上探索了並行化:貢獻內存處理器和Linux集群。

    3.1 數據並行處理

    在共享內存處理器的條件下,並行是很容易實現的,這歸功於非常低的通信開銷。在這種情況下,我們選擇數據並行化的實現方式,每個處理器工作在不同的數據子集。每個處理器計算它擁有的訓練樣例的梯度,執行隨機梯度下降算法更新內存中共享的參數。我們的第一個實現是很低速,因為采用了同步算法來避免寫寫沖突。處理器的大多數時間浪費在了等待其他處理器上。

    取而代之,我們選擇異步實現方式,每個處理器可以在任意時間向共享的內存中寫數據。有時一些更新因為寫寫沖突而丟失,這導致了參數更新的一些小噪聲的產生。然而,這種噪聲是很微不足道的。

    不幸的是,大型共享內存計算機是很昂貴的,並且它們的處理器的速度傾向於比CPU集群落后。因此我們可以在高速的網絡集群上得到更快的訓練。

    3.2 參數並行處理

    如果並行計算機是一個CPU的網絡,我們通常支付不起過於頻繁的參數交換的開銷,因為參數的規模是百兆級別的,這將消耗大量的時間。取而代之我們選擇參數並行處理,特別的,參數是輸出單元的參數,因為這是在我們的架構中絕大多數計算發生的地方。每個CPU負責計算一個未正則化概率的輸出子集。這種策略允許我們實現一個通信開銷微不足道的並行的隨機梯度下降算法。CPU本質上需要交換兩種數據:(1)輸出層的正則化因子,(2)隱藏層的梯度和詞特征層。所有的CPU都復制在輸出層之前的計算,然而這些計算比起總的計算量是微不足道的。

    舉例來說,考慮在AP news上的實驗:詞表大小|V|=17964,隱藏層單元數量h=60,序列長度n=6,詞特征向量維數m=100,單個訓練樣例的計算量是|V|(1+nm+h)+h(1+nm)+nm。在這個例子中,在輸出層需要的計算量占總計算量的分數為

    這個計算是近似的,因為實際的CPU時間隨着計算的種類的不同而不同,但是它顯示出並行計算輸出層是具有積極影響的。所有的CPU都要復制非常少量的因子,這對總的計算時間影響並不大。如果隱藏層單元的數據巨大,並行化計算也是有益的,我們在這里不做實驗證明了。

   下面用到的符號中“.”代表笛卡爾積,“'”代表矩陣轉置,CPUi(i取值范圍是0~M-1)負責計算輸出單元起始號為starti=i×⌈|V|/M⌉, 長度為 min(⌈|V|/M⌉,|V|-starti)的輸出層塊。

    權重懲罰正則化沒有在上面顯示,但是可以簡單的被實現。需要注意的是參數的更新是立即的而不是通過一個參數梯度向量,這樣做可以提高速度。

    在前向計算階段,會出現一些問題,其中一個問題是pj可以全部為0,或者他們的其中一個非常大而不能進行指數運算。為了避免這個問題,通常的解決方案是在計算指數運算之前,減去yj中最大的數。因此我們可以在計算pj之前加上一個Allreduce運算去在M個處理器間共享yj的最大值。

    在低速度的集群上,仍然可以獲得有效的並行化。與其在每個訓練樣例計算時通信,不如在每K個訓練樣例計算時通信。這需要保存神經網絡的K個激活和梯度。在K個訓練樣例的前向階段后,概率的和必須共享給處理器。然后K后向階段被初始化。在交換了這些梯度向量之后,每個處理器可以完成后向階段並更行參數。如果K過大,將會導致不收斂的問題。

4.  實驗結果

    在Brwon語料庫上的1181041個詞序列上進行了對比實驗。前800000個詞用來訓練,接下來的200000個詞用來調整模型的參數,剩下的181041用來測試。不同的詞的數量是47587。詞的頻率3的被合並成為一項。把詞表的大小縮小到了|V|=16383。

    一個實驗也在1995和1996的AP news的文本數據上運行。訓練集是大約1400萬的序列,發展集的大小大約是100萬的序列,測試集也是100的序列。數據有148721個不同的詞,我們把詞表縮小到|V|=17964,使用的方法是保留高頻率的詞,把大寫字母轉化為小寫字母,把數字和特殊字符合並等。

對於神經網絡,初始的學習速率被設置為ε0=0.001,並且逐漸的采用公式εt0/(1+rt)縮小,其中t代表已經被更新的參數數量,r是衰減因子,取值為10-8

  4.1  N-Gram模型

    第一個對照的對象是使用插值法和平滑法的trigram模型。模型的條件概率表示為

    其中,條件權重αi(qt)>0,∑iαi(qt)=1。p0=1/|V|,p1(wt)是unigram,p2 (wt|wt-1)是bigram, p3 (wtwt-1 ,wt-2)是trigram。αi可以通過EM算法求得,大約需要5次迭代。

    4.2  結果

    下圖為基於困惑度的對不同模型的測試結果。


    可以看到神經網絡語言模型比最好的n-gram性能要好。

5.  結論

    實驗在兩個語料庫上進行,一個具有超過一百萬的訓練樣例,另一個更大有一千五百萬詞。實驗顯示了本論文提出的方法獲得了比先進的trigram好很多的困惑度值。

    我們相信主要的原因是該方法允許學習分布表示來解決維數災難的問題。這個模型可能有更多的可以改進的地方,在模型的架構方面,計算效率方面和先驗知識的運用方面等。將來的優先研究點應該是提高訓練速度。一個簡單的想法來利用時間結構並擴展輸入窗口的大小的方法是利用卷積神經網絡。更一般的在這里介紹的工作打開了提高統計語言模型方法的大門,用基於分布表示的更加平滑的表示方法代替條件概率表。鑒於統計語言模型研究的很多努力工作都花費在了限制和總結條件變量上,來防止過擬合問題,在本論文中介紹的方法轉移了這個困難:更多的計算被需要,但是計算和內存需求規模都是線性的,而不是條件變量的指數級別。

 
下面附上我的源代碼
NPLM.h
#pragma once
#include <map>
#include <string>

using namespace std;


class NPLM
{
public:
    NPLM(double alphan = 0.001, int hn = 50, int nn = 5, int mn = 60, int sparseThresholdn = 2);
    ~NPLM();
public:
    int h;                //隱藏結點的數量
    int n;                //詞序列的長度 
    int m;                //表示詞的維數
    int v;                //表示詞的維數
    double alpha;        //學習的步長
    int sparseThreshold;//出現次數大於閾值的詞被加入詞表
public:
    void MakeVocabulary(const string &fileName);//建立詞典
    void AllocMemory();                            //為計算分配空間
    void FreeMemory();                            //回收計算空間
    void InitParameters();                        //初始化參數0~1的rand
    void ForwardPhase();
    void BackwardPhase();
    void SaveParameters(const string &path);
    void LoadParameters(const string &path);
    void SaveVocabularyAndModel(const string &path);
    void LoadVocabularyAndModel(const string &path);
    void Train(const string & inputFileName, const string & workpath);
    void Predict(const string & inputFileName, const string & workpath);
private:
    map<string, int> wordToIndex;
    double *x;            //輸入層的輸出                (n-1)m
    double **H;            //輸入層到隱藏層的參數矩陣    h * (n-1)m
    double *d;            //輸入層到隱藏層的偏斜        h
    double *b;            //隱藏層到輸出層的偏斜        v
    double *a;            //隱藏層的輸出                h
    double *y;            //輸出層的輸出                v
    double **U;            //隱藏層到輸出層的參數矩陣    v * h
    double *p;            //輸出層轉化為概率            v
    double *ly;            //對y的偏導數                v
    double *la;            //對隱藏層輸出的偏導數        h
    double *lo;            //對tanH內部的偏導數        h
    double **C;            //詞到特征向量的轉化矩陣    v * m
    double * lx;        //對x的偏導數                (n-1)m
    int *wt;            //長度為n的次序列,用下標表示
    double * lastb;        //迭代前的b向量
    double * lastd;        //迭代前的d向量
    double ** lastU;    //迭代前的U矩陣
    double ** lastC;    //迭代前的C矩陣
    double ** lastH;    //迭代前的H矩陣
private:
    inline bool IsWhiteChar(char ch);                            //判斷字符是不是空白符
    inline double GetRand(double denominator);                    //生成隨機數 0~1/denominator
    double ScalarProduct(double *aa, double *bb, int len);        //笛卡爾積
};

NPLM.cpp

#include "NPLM.h"
#include <fstream>
#include <cmath>
#include <vector>
#include <iostream>
//#define DEBUG

NPLM::NPLM(double alphan, int hn, int nn, int mn, int sparseThresholdn)
: h(hn)
, m(mn)
, n(nn)
, v(0)
, alpha(alphan)
, sparseThreshold(sparseThresholdn)
, x(nullptr)
, H(nullptr)
, d(nullptr)
, b(nullptr)
, a(nullptr)
, y(nullptr)
, U(nullptr)
, p(nullptr)
, ly(nullptr)
, la(nullptr)
, lo(nullptr)
, C(nullptr)
, lx(nullptr)
, wt(nullptr)
, lastb(nullptr)
, lastd(nullptr)
, lastU(nullptr)
, lastC(nullptr)
, lastH(nullptr)
{
}


NPLM::~NPLM()
{
}

void NPLM::MakeVocabulary(const string &fileName)
{
    ifstream input(fileName, ios::in);
    string line;
    string word;
    map<string, int> wordToCurrency;
    map<string, int>::iterator itMap;
    int i;
    int j;
    getline(input, line);
    while (input)
    {
        i = 0;
        for (j = 0; j < line.size(); ++j)
        {
            if (IsWhiteChar(line.at(j)))
            {
                if (i == j)
                {
                    ++i;
                }
                else
                {
                    //找到一個詞
                    word.assign(line.substr(i, j - i));
                    if ((itMap = wordToCurrency.find(word)) == wordToCurrency.end())
                    {
                        wordToCurrency.insert(pair<string, int>(word, 1));
                    }
                    else
                    {
                        ++itMap->second;
                    }
                    i = j + 1;
                }
            }
        }
        if (i < line.size())
        {
            //找到一個詞
            word.assign(line.substr(i, line.size() - i));
            if ((itMap = wordToCurrency.find(word)) == wordToCurrency.end())
            {
                wordToCurrency.insert(pair<string, int>(word, 1));
            }
            else
            {
                ++itMap->second;
            }
        }
        getline(input, line);
    }
    input.close();
    wordToIndex.insert(pair<string, int>(" ", 0));    //空白
    wordToIndex.insert(pair<string, int>(" S", 1)); //稀疏
    i = 2;
    for (itMap = wordToCurrency.begin(); itMap != wordToCurrency.end(); ++itMap)
    {
        if (itMap->second > sparseThreshold)
        {
            wordToIndex.insert(pair<string, int>(itMap->first, i));
            ++i;
        }
    }
}
bool NPLM::IsWhiteChar(char ch)
{
    return ch == '\b' || ch == '\n' || ch == '\t' || ch == ' ' || ch == '\r' ? true : false;
}
void NPLM::AllocMemory()
{
    int i = 0;
    v = wordToIndex.size();
    x = new double[(n - 1) * m];
    d = new double[h];
    lastd = new double[h];
    H = new double *[h];
    lastH = new double *[h];
    for (i = 0; i < h; ++i)
    {
        H[i] = new double[(n - 1) * m];
        lastH[i] = new double[(n - 1) * m];
    }
    a = new double[h];
    y = new double[v];
    U = new double *[v];
    lastU = new double *[v];
    for (i = 0; i < v; ++i)
    {
        U[i] = new double[h];
        lastU[i] = new double[h];
    }
    b = new double[v];
    lastb = new double[v];
    p = new double[v];
    ly = new double[v];
    la = new double[h];
    lo = new double[h];
    C = new double *[v];
    lastC = new double *[v];
    for (i = 0; i < v; ++i)
    {
        C[i] = new double[m];
        lastC[i] = new double[m];
    }
    lx = new double[(n - 1) * m];
    wt = new int[n];
}
void NPLM::FreeMemory()
{
    int i = 0;
    if (x != nullptr)
    {
        delete[]x;
    }
    if (d != nullptr)
    {
        delete[]d;
    }
    if (lastd != nullptr)
    {
        delete[]lastd;
    }
    if (H != nullptr)
    {
        for (i = 0; i < h; ++i)
        {
            delete[]H[i];
        }
        delete[]H;
    }
    if (lastH != nullptr)
    {
        for (i = 0; i < h; ++i)
        {
            delete[]lastH[i];
        }
        delete[]lastH;
    }
    if (a != nullptr)
    {
        delete[]a;
    }
    if (y != nullptr)
    {
        delete[]y;
    }
    if (U != nullptr)
    {
        for (i = 0; i < v; ++i)
        {
            delete[]U[i];
        }
        delete[]U;
    }
    if (lastU != nullptr)
    {
        for (i = 0; i < v; ++i)
        {
            delete[]lastU[i];
        }
        delete[]lastU;
    }
    if (b != nullptr)
    {
        delete[]b;
    }
    if (lastb != nullptr)
    {
        delete[]lastb;
    }
    if (p != nullptr)
    {
        delete[]p;
    }
    if (ly != nullptr)
    {
        delete[]ly;
    }
    if (la != nullptr)
    {
        delete[]la;
    }
    if (lo != nullptr)
    {
        delete[]lo;
    }
    if (C != nullptr)
    {
        for (i = 0; i < v; ++i)
        {
            delete[]C[i];
        }
        delete[]C;
    }
    if (lastC != nullptr)
    {
        for (i = 0; i < v; ++i)
        {
            delete[]lastC[i];
        }
        delete[]lastC;
    }
    if (lx != nullptr)
    {
        delete[]lx;
    }
    if (wt != nullptr)
    {
        delete[]wt;
    }
    x = nullptr;
    H = nullptr;
    d = nullptr;
    b = nullptr;
    a = nullptr;
    y = nullptr;
    U = nullptr;
    p = nullptr;
    ly = nullptr;
    la = nullptr;
    lo = nullptr;
    C = nullptr;
    lx = nullptr;
    wt = nullptr;
    lastb = nullptr;
    lastd = nullptr;
    lastU = nullptr;
    lastC = nullptr;
    lastH = nullptr;
}
double NPLM::GetRand(double denominator)
{
    return static_cast<double>(rand() % 10 + 1) / 10.0 / denominator;
}
void NPLM::InitParameters()
{
    int i = 0;
    int j = 0;
    int s = (n - 1) * m;
    for (i = 0; i < v; ++i)
    {
        lastb[i] = b[i] = GetRand(1);
    }
    for (i = 0; i < h; ++i)
    {
        lastd[i] = d[i] = GetRand(1);
    }
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < h; ++j)
        {
            lastU[i][j] = U[i][j] = GetRand(v * h);
        }
    }
    for (i = 0; i < h; ++i)
    {
        for (j = 0; j < s; ++j)
        {
            lastH[i][j] = H[i][j] = GetRand(h * s);
        }
    }
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < m; ++j)
        {
            lastC[i][j] = C[i][j] = GetRand(1);
        }
    }
}
void NPLM::ForwardPhase()
{
    int i = 0;
    double maxYi = 0.0;
    double s = 0.0;
    //b)
    for (i = 0; i < h; ++i)
    {
        a[i] = ScalarProduct(H[i], x, (n - 1) * m) + d[i];
        a[i] = tanh(a[i]);
    }
    //c)
    maxYi = -DBL_MAX;
    for (i = 0; i < v; ++i)
    {
        y[i] = ScalarProduct(a, U[i], h) + b[i];
        if (y[i] > maxYi)
        {
            maxYi = y[i];
        }
    }
    s = 0.0;
    for (i = 0; i < v; ++i)
    {
        y[i] = y[i] - maxYi;
        p[i] = exp(y[i]);
        s += p[i];
    }
    //e)
    for (i = 0; i < v; ++i)
    {
        p[i] = p[i] / s;
    }
}
void NPLM::BackwardPhase()
{
    int i = 0;
    int j = 0;
    int k = 0;
    int s = (n - 1) * m;
    int t = n - 1;
    double temp = 0;
    //a)
    for (i = 0; i < h; ++i)
    {
        la[i] = 0.0;
    }
    for (i = 0; i < s; ++i)
    {
        lx[i] = 0.0;
    }
    for (i = 0; i < v; ++i)
    {
        if (wt[n - 1] == i)
        {
            ly[i] = 1.0 - p[i];
        }
        else
        {
            ly[i] = -p[i];
        }
        b[i] += alpha * ly[i];
        for (j = 0; j < h; ++j)
        {
            la[j] += ly[i] * U[i][j];
            U[i][j] += alpha * ly[i] * a[j];
        }
    }
    //c)
    for (i = 0; i < h; ++i)
    {
        lo[i] = (1.0 - a[i] * a[i]) * la[i];
        d[i] += alpha * lo[i];
    }
    for (i = 0; i < s; ++i)
    {
        temp = 0.0;
        for (j = 0; j < h; ++j)
        {
            temp += H[j][i] * lo[j];
        }
        lx[i] += temp;
    }
    for (i = 0; i < h; ++i)
    {
        for (j = 0; j < s; ++j)
        {
            H[i][j] += alpha * lo[i] * x[j];
        }
    }
    //d)
    k = 0;
    for (i = 0; i < t; ++i)
    {
        for (j = 0; j < m; ++j)
        {
            C[wt[i]][j] += alpha * lx[k];
            ++k;
        }
    }
}


double NPLM::ScalarProduct(double *aa, double *bb, int len)
{
    int i = 0;
    double ret = 0;
    for (i = 0; i < len; ++i)
    {
        ret += aa[i] * bb[i];
    }
    return ret;
}
void NPLM::SaveParameters(const string& path)
{
    int i = 0;
    int j = 0;
    int s = (n - 1) * m;
    ofstream output;
    output.open(path + "\\b", ios::out);
    output << v << endl;
    for (i = 0; i < v; ++i)
    {
        output << b[i] << endl;
    }
    output.close();
    output.open(path + "\\U", ios::out);
    output << v << '\t' << h << endl;
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < h; ++j)
        {
            output << U[i][j] << endl;
        }
    }
    output.close();
    output.open(path + "\\d", ios::out);
    output << h << endl;
    for (i = 0; i < h; ++i)
    {
        output << d[i] << endl;
    }
    output.close();
    output.open(path + "\\H", ios::out);
    output << h << '\t' << s << endl;
    for (i = 0; i < h; ++i)
    {
        for (j = 0; j < s; ++j)
        {
            output << H[i][j] << endl;
        }
    }
    output.close();
    output.open(path + "\\C", ios::out);
    output << v << '\t' << m << endl;
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < m; ++j)
        {
            output << C[i][j] << endl;
        }
    }
    output.close();
}
void NPLM::LoadParameters(const string& path)
{
    int i = 0;
    int j = 0;
    int s = (n - 1) * m;
    int temp;
    ifstream input;
    input.open(path + "\\b", ios::in);
    input >> temp;
    if (b == nullptr)
    {
        b = new double[v];
    }
    for (i = 0; i < v; ++i)
    {
        input >> b[i];
    }
    input.close();
    input.open(path + "\\U", ios::in);
    input >> temp >> temp;
    if (U == nullptr)
    {
        U = new double *[v];
        for (i = 0; i < v; ++i)
        {
            U[i] = new double[h];
        }
    }
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < h; ++j)
        {
            input >> U[i][j];
        }
    }
    input.close();
    input.open(path + "\\d", ios::in);
    input >> temp;
    if (d == nullptr)
    {
        d = new double[h];
    }
    for (i = 0; i < h; ++i)
    {
        input >> d[i];
    }
    input.close();
    input.open(path + "\\H", ios::in);
    input >> temp >> temp;
    if (H == nullptr)
    {
        H = new double *[h];
        for (i = 0; i < h; ++i)
        {
            H[i] = new double[s];
        }
    }
    for (i = 0; i < h; ++i)
    {
        for (j = 0; j < s; ++j)
        {
            input >> H[i][j];
        }
    }
    input.close();
    input.open(path + "\\C", ios::in);
    input >> temp >> temp;
    if (C == nullptr)
    {
        C = new double *[v];
        for (i = 0; i < v; ++i)
        {
            C[i] = new double[m];
        }
    }
    for (i = 0; i < v; ++i)
    {
        for (j = 0; j < m; ++j)
        {
            input >> C[i][j];
        }
    }
    input.close();
}
void NPLM::SaveVocabularyAndModel(const string &path)
{
    ofstream output;
    output.open(path + "\\word2index", ios::out);
    for (map<string, int>::iterator itMap = wordToIndex.begin(); itMap != wordToIndex.end(); ++itMap)
    {
        output << itMap->first << '\t' << itMap->second << endl;
    }
    output.close();

    output.open(path + "\\parameters", ios::out);
    output << "h\t" << h << endl;
    output << "n\t" << n << endl;
    output << "m\t" << m << endl;
    output << "v\t" << v << endl;
    output << "alpha\t" << alpha << endl;
    output << "sparsethreshold\t" << sparseThreshold << endl;
    output.close();
}
void NPLM::LoadVocabularyAndModel(const string &path)
{
    ifstream input;
    string line;
    string word;
    string index;
    int i = 0;
    wordToIndex.clear();
    input.open(path + "\\word2index", ios::in);
    getline(input, line);
    while (input)
    {
        i = line.find_first_of('\t');
        word.assign(line.substr(0, i));
        index.assign(line.substr(i + 1, line.size() - i - 1));
        wordToIndex.insert(pair<string, int>(word, stoi(index)));
        getline(input, line);
    }
    input.close();
    input.open(path + "\\parameters", ios::in);
    input >> word >> h;
    input >> word >> n;
    input >> word >> m;
    input >> word >> v;
    input >> word >> alpha;
    input >> word >> sparseThreshold;
    input.close();
}
void NPLM::Train(const string &inputFileName, const string & workpath)
{
    ifstream input;
    ofstream output;
    string line;
    string word;
    int i = 0;
    int j = 0;
    int k = 0;
    int l = 0;
    int s = (n - 1) * m;
    int lineNum = 0;
    int iterateNum = 1;
    double deltad = 0.0;
    double deltab = 0.0;
    double deltaH = 0.0;
    double deltaC = 0.0;
    double deltaU = 0.0;
    double deltaAll = 0.0;
    vector<string> sentence;
    map<string, int>::iterator itMap;
    //建立詞表
    MakeVocabulary(inputFileName);
    cout << "Making vocabulary has beed finished!\n";
    //為計算分配空間
    AllocMemory();
    cout << "Allocating memory has beed finished!\n";
    //存儲單詞表
    SaveVocabularyAndModel(workpath);
    cout << "Saving vocabulary and model has beed finished!\n";
    //初始化參數0~1的rand
    InitParameters();
    cout << "Initializing parameters has beed finished!\n";


    output.open(workpath + "//log", ios::out);
    output << "**************log*************" << endl;
    output.close();
    while (true)
    {
        input.open(inputFileName, ios::in);
        getline(input, line);
        lineNum = 1;
        while (input)
        {
            //得到一個句子(詞串)
            i = 0;
            sentence.clear();
            for (j = 0; j < line.size(); ++j)
            {
                if (IsWhiteChar(line.at(j)))
                {
                    if (i == j)
                    {
                        ++i;
                    }
                    else
                    {
                        //找到一個詞
                        word.assign(line.substr(i, j - i));
                        sentence.push_back(word);
                        i = j + 1;
                    }
                }
            }
            if (i < line.size())
            {
                //找到一個詞
                word.assign(line.substr(i, line.size() - i));
                sentence.push_back(word);
            }
            //獲得詞串后建立訓練樣本
            for (i = 0; i < sentence.size(); ++i)
            {
                //重疊建立訓練樣本
                for (j = n - 1; j > -1 && i - n + 1 + j > -1; --j)
                {
                    //未登錄詞或者數量少於閾值(3)個的詞下標為1
                    if ((itMap = wordToIndex.find(sentence.at(i - n + 1 + j))) == wordToIndex.end())
                    {
                        wt[j] = 1;
                    }
                    else
                    {
                        wt[j] = itMap->second;
                    }
                }
                //超出邊界的設置為0
                for (; j > -1; --j)
                {
                    wt[j] = 0;
                }
                //建立x向量
                k = n - 1;
                for (l = 0; l < k; ++l)
                {
                    for (j = 0; j < m; ++j)
                    {
                        x[l * m + j] = C[wt[l]][j];
                    }
                }
                //隨機梯度下降
                //向前傳播
                ForwardPhase();
                //向后更新
                BackwardPhase();
            }
            //一行處理結束后打印信息
            cout << lineNum << " lines have been computed!\r";
            getline(input, line);
            ++lineNum;
        }
        input.close();
        //整個樣本迭代完成
        //保存參數
        SaveParameters(workpath);
        //把參數放到last中,並且計算更新值
        deltad = 0.0;
        deltab = 0.0;
        deltaH = 0.0;
        deltaC = 0.0;
        deltaU = 0.0;
        for (i = 0; i < v; ++i)
        {
            deltab += abs(b[i] - lastb[i]);
            lastb[i] = b[i];
        }
        for (i = 0; i < h; ++i)
        {
            deltad += abs(d[i] - lastd[i]);
            lastd[i] = d[i];
        }
        for (i = 0; i < v; ++i)
        {
            for (j = 0; j < h; ++j)
            {
                deltaU += abs(U[i][j] - lastU[i][j]);
                lastU[i][j] = U[i][j];
            }
        }
        for (i = 0; i < h; ++i)
        {
            for (j = 0; j < s; ++j)
            {
                deltaH += abs(H[i][j] - lastH[i][j]);
                lastH[i][j] = H[i][j];
            }
        }
        for (i = 0; i < v; ++i)
        {
            for (j = 0; j < m; ++j)
            {
                deltaC += abs(C[i][j] - lastC[i][j]);
                lastC[i][j] = C[i][j];
            }
        }
        deltaAll = deltab + deltad + deltaH + deltaU + deltaC;
        //一行處理結束后打印信息
        cout << "\n" << iterateNum << " times of iteration have been computed!\n";
        cout << "delta d = " << deltad << '\n';
        cout << "delta b = " << deltab << '\n';
        cout << "delta H = " << deltaH << '\n';
        cout << "delta U = " << deltaU << '\n';
        cout << "delta C = " << deltaC << '\n';
        cout << "delta all = " << deltaAll << "\n\n";
        output.open(workpath + "//log", ios::app);
        output << iterateNum << " times of iteration have been computed!\n";
        output << "delta d   = " << deltad << '\n';
        output << "delta b   = " << deltab << '\n';
        output << "delta H   = " << deltaH << '\n';
        output << "delta U   = " << deltaU << '\n';
        output << "delta C   = " << deltaC << '\n';
        output << "delta all = " << deltaAll << "\n\n";
        output.close();
        ++iterateNum;
    }
    //回收計算空間
    FreeMemory();
}

void NPLM::Predict(const string & inputFileName, const string & workpath)
{
    ifstream input;
    ofstream output;
    string line;
    string word;
    int i = 0;
    int j = 0;
    int k = 0;
    int l = 0;
    int s = (n - 1) * m;
    int lineNum = 0;
    vector<string> sentence;
    map<string, int>::iterator itMap;
    long long wordNumOfDoc = 0;
    long long wordNumOfSen = 0;
    double hpSen = 0.0;
    double hpDoc = 0.0;
    double ppSen = 0.0;
    double ppDoc = 0.0;
    //讀入詞表和模型
    LoadVocabularyAndModel(workpath);
    cout << "Loading vocabulary and model has beed finished!\n";
    //申請空間
    AllocMemory();
    cout << "Allocating memory has beed finished!\n";
    //讀入參數
    LoadParameters(workpath);
    cout << "Loading parameters has beed finished!\n";
    //讀入測試文件
    input.open(inputFileName, ios::in);
    getline(input, line);
    lineNum = 1;
    while (input)
    {
        //得到一個句子(詞串)
        i = 0;
        sentence.clear();
        for (j = 0; j < line.size(); ++j)
        {
            if (IsWhiteChar(line.at(j)))
            {
                if (i == j)
                {
                    ++i;
                }
                else
                {
                    //找到一個詞
                    word.assign(line.substr(i, j - i));
                    sentence.push_back(word);
                    i = j + 1;
                }
            }
        }
        if (i < line.size())
        {
            //找到一個詞
            word.assign(line.substr(i, line.size() - i));
            sentence.push_back(word);
        }
        //獲得詞串后建立訓練樣本
        hpSen = 0.0;
        ppSen = 0.0;
        for (i = 0; i < sentence.size(); ++i)
        {
            //重疊建立訓練樣本
            for (j = n - 1; j > -1 && i - n + 1 + j > -1; --j)
            {
                //未登錄詞或者數量少於閾值(3)個的詞下標為1
                if ((itMap = wordToIndex.find(sentence.at(i - n + 1 + j))) == wordToIndex.end())
                {
                    wt[j] = 1;
                }
                else
                {
                    wt[j] = itMap->second;
                }
            }
            //超出邊界的設置為0
            for (; j > -1; --j)
            {
                wt[j] = 0;
            }
            //建立x向量
            k = n - 1;
            for (l = 0; l < k; ++l)
            {
                for (j = 0; j < m; ++j)
                {
                    x[l * m + j] = C[wt[l]][j];
                }
            }
            //向前傳播
            ForwardPhase();
            //累積Hp值
            hpSen += log2(p[wt[n - 1]]);
        }
        wordNumOfSen = sentence.size();
        wordNumOfDoc += wordNumOfSen;
        hpDoc += hpSen;
        //一行處理結束后打印信息
        cout << lineNum << " lines have been computed!\r";
        getline(input, line);
        ++lineNum;
    }
    //預測結束,關閉文件
    input.close();

    hpDoc = -hpDoc / static_cast<double>(wordNumOfDoc);
    ppDoc = pow(2.0, hpDoc);
    output.open(workpath + "\\testresult", ios::out);
    output << "Hp = " << hpDoc << "\n";
    output << "Pp = " << ppDoc << "\n";
    output.close();
    cout << "\nHp = " << hpDoc << "\n";
    cout << "Pp = " << ppDoc << "\n";
    //回收計算空間
    FreeMemory();
}

main.cpp

#include "NPLM.h"
#include <iostream>
using namespace std;
int main()
{
    string training = R"(E:\nplm100\training)";
    string test = R"(E:\nplm100\test)";
    string pathname = R"(E:\nplm100)";
    NPLM nplm(0.001, 100, 5,120,2);
    //nplm.Train(training, pathname);
    nplm.Predict(test, pathname);
    cout << "Press any key to continue!" << endl;
    getchar();
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM