本文来自CSDN博客,转载请注明出处:http://blog.csdn.net/a635661820/article/details/44730507
参考文献: A Neural Probabilistic Language Model
参照我另一篇NNLM学习介绍的博客, 这一篇是对NNLM的简要实现, 自己简化了一些,输入层到输出层没有连接(加上直连边的真在原论文中没有明显的提高),并且没有并行算法。下面贴上自己的一些核心代码。总体来说,我用了c++面向对象来设计该算法,大概分为6个类,如下:
- CLex类:用来处理文本的一个类
- CInput类:输入层类, 包含相关量
- CHidden类:隐层类,包含相关量
- COutput类:输出层类,包含相关变量
- CAlgothrim类:算法类,涉及前向算法以及反向更新算法,将前面3个类串起来
- CForeCast类:测试类,训练好模型后对模型进行测试的相关操作
关于网络中的各个手动设定的参数在macroDefinition.h,包括隐层神经元个数、特征向量维度等。这里的附带的代码只贴出了核心相关代码,即CInput, CHidden, COutput, CAlgothrim的核心代码
网络手动设定的参数在macroDefinition.h里面,定义为宏,初始设置是按照论文中设置的:
<span style="font-family:Microsoft YaHei;font-size:14px;">//以下是各个变量的宏定义,留做接口,方便调试 #define M 100 //一个单词对应的向量的维度数为M #define N 3 //输入层的单词个数,第N+1个是预测单词 #define HN 60 //Hidden Number隐层神经元的个数 #define EPSILON 0.001 //神经网络的学习率 #define CONVERGE_THRESHOLD -4 //手动设定的累加对数概率收敛值 #define FOREWORD_NUM 5 //模型预测时的输出概率最高的前5个词语</span>
在Input.h文件里面,类的结构:
<span style="font-family:Microsoft YaHei;font-size:14px;">//输入层的结构定义 class CInput { public: bool LoadCorpusfile(char *corpusfile); //将语料库文件读入 bool Computex(int k); //计算一个句子输入单词的x向量 string GetWordByID(unsigned long id); //根据ID返回单词 unsigned long GetIDByWord(string word); //根据单词返回其ID bool NextSentence(int i); //从语料库文件中读入下一个句子 CInput(); //生成词典、二维矩阵 virtual ~CInput(); //释放单词映射矩阵,释放词典 CLex *pChnLex; //指向中文词典的指针 float **vec; //输入单词对应的映射矩阵 vector<string> sentence; //一行句子 vector<string> corpus; //语料库 float *x; //保存输入层单词的特征向量 long unsigned expectedID; //训练句子时下一个输出单词ID };</span>
类的实现在Input.cpp里面:
<span style="font-family:Microsoft YaHei;font-size:14px;">CInput::CInput() { //构造函数 //生成词典、二维矩阵 pChnLex = new CLex; //动态分配词典对象 if (NULL == pChnLex) //分配失败 { printf("动态分配词典失败!\n"); exit(1); } if (!pChnLex->LoadLexicon_Null("output.voc")) //将指向中文词典的指针与词典文本库连接起来 { printf("载入中文词典失败!\n"); //载入失败输出错误信息到屏幕 } unsigned long sizeV = pChnLex->ulVocSizeC; //记录词典的大小 srand(time(NULL)); //随机数种子 vec = new float *[sizeV]; //开始分配单词映射的二维矩阵 if (NULL == vec) //分配失败 { printf("分配单词映射矩阵失败!\n"); exit(1); } for (unsigned long i=1; i<sizeV; i++) //初始化二维矩阵 { vec[i] = new float[M]; //分配满足每个单词对应M维 if (NULL == vec[i]) { printf("分配单词映射向量失败!\n"); exit(1); } for (int j=0; j<M; j++) //赋值-0.5-0.5的小数 { vec[i][j] = (float)(((rand()/32767.0)*2-1)/2); } } x = new float[M*N]; //输入层单词的特征向量 if (NULL == x) { cerr << "输入层单词的特征向量分配失败" << endl; exit(1); } memset(x, 0, M*N*sizeof(float)); //将x向量清0 } CInput::~CInput() { //析构函数 //释放单词映射矩阵 //释放中文词典 unsigned long sizeV = pChnLex->ulVocSizeC; //记录词典的大小 for (unsigned long i=1; i<sizeV; i++) //释放单词映射矩阵 { delete [] vec[i]; vec[i] = NULL; } delete []vec; vec = NULL; delete pChnLex; //释放中文词典 pChnLex = NULL; delete [] x; //释放x x = NULL; } bool CInput::NextSentence(int i) { //从预料中读取第i个句子 //并且将该句子中的单词以空格分隔存入容器 string line; string word; if (i >= corpus.size()) //错误 { cerr << "!!!!!error 下标出界" << endl; return false; } line = corpus[i]; //第i个句子 stringstream instring(line); //字符流与该句子关联 sentence.clear(); //清空之前的句子 while (instring >> word) { sentence.push_back(word); //把句子中的单词以空格隔开存入sentence } return true; } unsigned long CInput::GetIDByWord(string word) { //根据单词返回其ID return pChnLex->findword(word); } string CInput::GetWordByID(unsigned long id) { //根据ID返回单词 return pChnLex->GetLexiconByID(id); } bool CInput::Computex(int k) { //计算一个句子输入单词的x向量 //k用来控制多次训练一个句子 long unsigned id; long unsigned Vsize = pChnLex->ulVocSizeC; //词典大小 int i, j, t; for (i=k,t=0; i<N+k; i++,t++) { if (i >= sentence.size()) //该句子训练完毕 { return false; } id = GetIDByWord(sentence[i]); //得到输入层第i个单词id if (id >= Vsize) { cerr << "输入句子有误(可能是由于当前训练语料库过小,词库太少导致)" << endl; exit(1); } for (j=0; j<M; j++) { x[M*t+j] = vec[id][j]; } } if (k+N >= sentence.size()) //该句子训练完毕 { expectedID = 0; //不会出现的ID号码标注 return false; } expectedID = GetIDByWord(sentence[k+N]); //得到训练句子的输出单词ID } bool CInput::LoadCorpusfile(char *corpusfile) { //将语料库文件读入内存 ifstream infile(corpusfile); //关联欲读入的文件 if (!infile) //读入失败 { return false; } string line; string word; while (getline(infile, line)) //从语料库读入一行句子到容器 { corpus.push_back(line); } infile.close(); //关闭文件 return true; }</span>
隐层的结构定义在Hidden.h里面:
<span style="font-family:Microsoft YaHei;font-size:14px;">//隐层的结构定义 class CHidden { public: CHidden(); //生成输入层到隐层的权值矩阵,隐层神经元的输出向量,偏置向量 virtual ~CHidden(); //释放权值矩阵H、向量a,d void OutHidden(float *x); //计算隐层的输出 float **H; //由输入层到隐层的权值矩阵(变量名按照论文中,方便对照) float *a; //隐层神经元的输出向量 float *d; //隐层的偏置向量 };</span>
隐层的实现在Hidden.cpp:
<span style="font-family:Microsoft YaHei;font-size:14px;">CHidden::CHidden() { //构造函数 //生成输入层到隐层的权值矩阵 //生成隐层神经元的输出向量 //生成隐层的偏置向量 H = new float *[HN]; //输入层到隐层的权值矩阵H if (NULL == H) { cerr << "输入层到隐层的权值矩阵分配失败!" << endl; exit(0); } int i, j; for (i=0; i<HN; i++) { H[i] = new float [M*N]; if (NULL == H[i]) { cerr << "输入层到隐层的权值矩阵分配失败!" << endl; exit(0); } } for (i=0; i<HN; i++) //初始化矩阵H { for (j=0; j<M*N; j++) { H[i][j] = (float)(((rand()/32767.0)*2-1)/2); //赋值-0.5-0.5的小数 } } d = new float[HN]; //隐层偏置向量 if (NULL == d) { cerr << "隐层偏置向量分配失败" << endl; exit(1); } a = new float[HN]; //隐层输出向量 if (NULL == a) { cerr << "隐层输出向量分配失败" << endl; exit(1); } for (i=0; i<HN; i++) { d[i] = (float)(((rand()/32767.0)*2-1)/2); //赋值-0.5-0.5的小数 a[i] = 0; } } CHidden::~CHidden() { //释放权值矩阵H、向量a,d int i; for (i=0; i<HN; i++) //释放H { delete [] H[i]; H[i] = NULL; } delete []H; H = NULL; delete []d; //释放d delete []a; //释放a d = NULL; a = NULL; } void CHidden::OutHidden(float *x) { //计算隐层输出 //x是由输入层传过来的输入层特征向量 int i, j; float zigma, o; for (i=0; i<HN; i++) //根据o<-d + Hx a<-tanh(o)计算a { zigma = 0.0; for (j=0; j<M*N; j++) { zigma += x[j] * H[i][j]; } o = d[i] + zigma; a[i] = tanh(o); } }</span>
输出层的结构定义在Output.h:
<span style="font-family:Microsoft YaHei;font-size:14px;">//输出层的结构定义 class COutput { public: void Initialize(long unsigned lexiconSize); //生成隐层到输出层的权值矩阵,偏置向量、输出向量、输出单词概率向量 void SoftMax(void); //SotfMax回归层,进行归一化,计算概率 void Output(float *a, long unsigned expectedID); //计算输出层的输出 COutput(); virtual ~COutput(); //释放权值矩阵U、向量b,y,p float **U; //隐层到输出层的权值矩阵 float *y; //输出层的输出 float *b; //输出层神经元的偏置向量 float *p; //输出单词概率向量 long unsigned VSize; //语料库词典的大小 float L; //累加的对数概率 };</span>
输出层的实现在Output.cpp文件里面:
<span style="font-family:Microsoft YaHei;font-size:14px;">COutput::COutput() { //do nothing } COutput::~COutput() { //释放权值矩阵U、向量b,y,p long unsigned i; for (i=1; i<VSize; i++) //释放H { delete [] U[i]; U[i] = NULL; } delete []U; U = NULL; delete []b; //释放d delete []y; //释放a delete []p; //释放p b = NULL; y = NULL; p = NULL; } void COutput::Output(float *a, long unsigned expectedID) { //计算输出层的输出 //a,expectedID分别是隐层传的输出向量,句子的输出单词ID long unsigned i; int j; float zigma; for (i=1; i<VSize; i++) //根据y<-b + Ua { zigma = 0.0; for (j=0; j<HN; j++) { zigma += a[j] * U[i][j]; } y[i] = b[i] + zigma; p[i] = exp(y[i]); //即计算e^yi } SoftMax(); //SotfMax回归计算概率 L += log(p[expectedID]); //返回以e为底的对数值 } void COutput::SoftMax() { //SotfMax回归层,进行归一化,计算概率 float sum = 0; long unsigned i; for (i=1; i<VSize; i++) //计算SoftMax分母 { sum += p[i]; } for (i=1; i<VSize; i++) //计算每个单词的概率 { p[i] /= sum; } } void COutput::Initialize(unsigned long lexiconSize) { //初始化输出层函数,为了程序构架好看方便,没放在构造函数里面 //生成隐层到输出层的权值矩阵 //输出层偏置向量、输出向量、输出单词概率向量 VSize = lexiconSize; //保存语料库词典大小 U = new float *[lexiconSize]; //隐层到输出层的权值矩阵U,让其下标从1开始与词典ID对应 if (NULL == U) { cerr << "隐层到输出层的权值矩阵U分配失败!" << endl; exit(0); } long unsigned int i, j; //i尽量为lVsizeg型,与词典类型大小对应 for (i=1; i<lexiconSize; i++) { U[i] = new float[HN]; if (NULL == U[i]) { cerr << "隐层到输出层的权值矩阵U分配失败!" << endl; exit(0); } } for (i=1; i<lexiconSize; i++) //初始化矩阵U { for (j=0; j<HN; j++) { U[i][j] = (float)(((rand()/32767.0)*2-1)/2); //赋值-0.5-0.5的小数 } } b = new float[lexiconSize]; //输出层偏置向量,让其下标从1开始与词典ID对应 if (NULL == b) { cerr << "输出层偏置向量分配失败" << endl; exit(1); } y = new float[lexiconSize]; //输出层输出向量,让其下标从1开始与词典ID对应 if (NULL == y) { cerr << "输出层输出向量分配失败" << endl; exit(1); } p = new float[lexiconSize]; //输出层输出向量,让其下标从1开始与词典ID对应 if (NULL == p) { cerr << "输出层概率向量分配失败" << endl; exit(1); } for (i=1; i<lexiconSize; i++) //初始化输出矩阵偏置向量、输出向量、概率向量 { b[i] = (float)(((rand()/32767.0)*2-1)/2); //赋值-0.5-0.5的小数 p[i] = 0; y[i] = 0; } p[0] = 0; //对后续排序起作用 L = 0; //累加对数概率清0 }</span>
算法类的结构定义在Algothrim.h:
<span style="font-family:Microsoft YaHei;font-size:14px;">//算法层的结构定义 class CAlgothrim { public: bool SaveParameters(); //保存网络全部参数到文件weight.txt中 void PrintOverInfo(long unsigned count, int sentNum); //提示训练结束 void PrintStartInfo(char *corpusfile); //打印该神经网络版本训练的相关信息 bool WriteResult(); //将输出层的概率向量结果输出到文件 bool Run(char *corpusFile); //模型训练框架函数 void Initialize(); //生成中间梯度向量 void UpdateAll(); //反向算法更新网络所有参数 void PrintParametersAfterUpate(); //在反向更新参数后打印网络所有参数 void UpdateInput(); //更新输入层vec void UpdateHidden(); //更新隐层的H,d void PrintParametersBeforeUpdate(); //在反向更新之前输出整个网络的所有参数 void UpdateOut(); //更新输出层的参数U,b CAlgothrim(CInput *pIn, CHidden *pHi, COutput *pOu); //3个参数分别表示输入层、隐层、输出层的指针 virtual ~CAlgothrim(); //释放偏导向量 CInput *pInput; //指向输入层的指针 CHidden *pHidden; //指向隐层的指针 COutput *pOut; //指向输出层的指针 float *Lpa; //L对a的偏导向量 float *Lpx; //L对x的偏导向量 float *Lpy; //L对y的偏导向量 };</span>
算法类的实现在Algothrim.cpp,这里是程序的核心部分:
<span style="font-family:Microsoft YaHei;font-size:14px;"><pre name="code" class="cpp">CAlgothrim::CAlgothrim(CInput *pIn, CHidden *pHi, COutput *pOu) { //构造函数 //3个参数分别表示输入层、隐层、输出层的指针 //获得各层指针 pInput = pIn; pHidden = pHi; pOut = pOu; } CAlgothrim::~CAlgothrim() { //释放偏导向量 delete []Lpa; //释放Lpa Lpa = NULL; delete []Lpx; //释放Lpx Lpx = NULL; delete []Lpy; //释放Lpy Lpy = NULL; } void CAlgothrim::UpdateOut() { //更新输出层的参数U,b //计算Lpy long unsigned Vsize = pInput->pChnLex->ulVocSizeC; //词典大小 long unsigned j; long unsigned wt = pInput->expectedID; //期望单词ID int k; memset(Lpa, 0, HN*sizeof(float)); //Lpa置0 for (j=1; j<Vsize; j++) //进行更新 { if (j == wt) //根据Lpy<-1(j==wt) - pj 计算Lpy { Lpy[j] = 1 - pOut->p[j]; } else { Lpy[j] = 0 - pOut->p[j]; } pOut->b[j] += EPSILON*Lpy[j]; //根据bj <- bj + ε*Lpy更新b for (k=0; k<HN; k++) //计算Lpa(累加),根据Lpa<-Lpa+Uj*Lpyj { Lpa[k] += Lpy[j]*pOut->U[j][k]; } for (k=0; k<HN; k++) //根据Uj<-Uj+ε*Lpy*a更新U { pOut->U[j][k] += EPSILON*Lpy[j]*pHidden->a[k]; } } } void CAlgothrim::UpdateHidden() { //更新隐层的H,d float Lpo[HN] = {0.0}; //L对o的偏导向量 int k; for (k=0; k<HN; k++) //计算Lpo 根据lpo <- (1-ak2)lpak { Lpo[k] = (1 - pHidden->a[k]*pHidden->a[k]) * Lpa[k]; } //累加计算Lpx memset(Lpx, 0, N*M*sizeof(float)); //置0 int i; float temp; for (i=0; i<M*N; i++) //根据Lpx -< Lpx + H(转置)*Lpo 累加计算Lpx { temp = 0.0; for (k=0; k<HN; k++) { temp += pHidden->H[k][i] * Lpo[k]; } Lpx[i] = temp; } //更新d for (k=0; k<HN; k++) //根据d <- d + ε*Lpo更新d { pHidden->d[k] += EPSILON * Lpo[k]; } //更新H for (i=0; i<HN; i++) //根据H <- H + ε*Lpo*x(转置) { for (k=0; k<M*N; k++) { pHidden->H[i][k] += EPSILON*Lpo[i]*pInput->x[k]; } } } void CAlgothrim::UpdateInput() { //更新输入层vec int k, i; long unsigned id; for (i=0; i<N; i++) { id = pInput->GetIDByWord(pInput->sentence[i]); //得到训练句子的第i个单词ID for (k=0; k<M; k++) { pInput->vec[id][k] += EPSILON * Lpx[i*M+k]; } } } void CAlgothrim::UpdateAll() { //反向算法更新网络所有参数 UpdateOut(); //更新输出层 UpdateHidden(); //更新隐层 UpdateInput(); //更新输入层 } void CAlgothrim::Initialize() { //生成中间梯度向量 long unsigned Vsize = pInput->pChnLex->ulVocSizeC; //得到词典大小 Lpa = new float[HN]; //L对a的偏导向量 if (NULL == Lpa) { cerr << "L对a的偏导向量分配失败!" << endl; exit(1); } memset(Lpa, 0, HN*sizeof(float)); //置0 Lpx = new float[N*M]; //L对x的偏导向量 if (NULL == Lpx) { cerr << "L对x的偏导向量分配失败!" << endl; exit(1); } memset(Lpx, 0, N*M*sizeof(float)); //置0 Lpy = new float[Vsize]; //L对y的偏导向量,下标从1开始与id对应 if (NULL == Lpy) { cerr << "L对y的偏导向量分配失败!" << endl; exit(1); } memset(Lpy, 0, (Vsize)*sizeof(float)); //置0 } bool CAlgothrim::Run(char *corpusFile) { //模型训练框架函数 //参数为语料库文件名 int i = 0; //控制语料库句子的循环训练 int k = 0; //控制一个句子中循环训练 unsigned int t = 0; //控制整个语料库的训练次数 int len; //记录要训练句子的长度 long unsigned count = 0; //记录网络更新次数 PrintStartInfo(corpusFile); //打印网络功能信息 pInput->LoadCorpusfile(corpusFile); //载入语料库文件 pOut->Initialize(pInput->pChnLex->ulVocSizeC); //初始化输出层 Initialize(); //生成中间梯度向量 while (true) { t++; //记录对语料库训练次数 pOut->L = 0; //loge(Pwt) L是累加的对数概率 for (i=0; i<pInput->corpus.size(); i++) //遍历语料库的句子进行训练 { //神经网络前馈运算 pInput->NextSentence(i); //从语料库中读取第i个句子做训练 len = pInput->sentence.size(); //读入的句子长度 for (k=0; k+N<len; k++) //对一个句子反复训练N-len次 { pInput->Computex(k); //计算输入层的x的特征向量 pHidden->OutHidden(pInput->x); //计算隐层输出 pOut->Output(pHidden->a, pInput->expectedID); //计算输出层 // PrintParametersBeforeUpdate(); //打印未更新前的参数 //反向算法更新参数 UpdateOut(); //更新输出层参数 UpdateHidden(); //更新隐层参数 UpdateInput(); //更新输入层参数 // PrintParametersAfterUpate(); //打印更新后的参数 count++; } } cout << t << "次训练,累加对数和概率: " << pOut->L << endl; if (pOut->L > CONVERGE_THRESHOLD) //大于手动设定的收敛阈值后退出循环 { break; } } SaveParameters(); //保存参数 PrintOverInfo(count, pInput->corpus.size()); return true; }</span>
时间: 2024-09-29 03:09:16