Feedforward Neural Network Language Model(NNLM)c++核心代码实现
2015-03-29 20:29
881 查看
本文来自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里面,定义为宏,初始设置是按照论文中设置的:
//以下是各个变量的宏定义,留做接口,方便调试
#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个词语
在Input.h文件里面,类的结构:
//输入层的结构定义
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
};
类的实现在Input.cpp里面:
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;
}
//隐层的结构定义
class CHidden
{
public:
CHidden(); //生成输入层到隐层的权值矩阵,隐层神经元的输出向量,偏置向量
virtual ~CHidden(); //释放权值矩阵H、向量a,d
void OutHidden(float *x); //计算隐层的输出
float **H; //由输入层到隐层的权值矩阵(变量名按照论文中,方便对照)
float *a; //隐层神经元的输出向量
float *d; //隐层的偏置向量
};
隐层的结构定义在Hidden.h里面:
//隐层的结构定义
class CHidden
{
public:
CHidden(); //生成输入层到隐层的权值矩阵,隐层神经元的输出向量,偏置向量
virtual ~CHidden(); //释放权值矩阵H、向量a,d
void OutHidden(float *x); //计算隐层的输出
float **H; //由输入层到隐层的权值矩阵(变量名按照论文中,方便对照)
float *a; //隐层神经元的输出向量
float *d; //隐层的偏置向量
};
隐层的实现在Hidden.cpp:
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);
}
}
输出层的结构定义在Output.h:
//输出层的结构定义
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; //累加的对数概率
};
输出层的实现在Output.cpp文件里面:
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
}
算法类的结构定义在Algothrim.h:
//算法层的结构定义
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的偏导向量
};
算法类的实现在Algothrim.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
}
15/5/6补充:原来的函数run成如下:
bool CAlgothrim::Train(char *corpusFile) { //模型训练框架函数 //参数为语料库文件名 int i = 0; //控制语料库句子的循环训练 int k = 0; //控制一个句子中循环训练 unsigned int t = 0; //控制整个语料库的训练次数 int len; //记录要训练句子的长度 long unsigned count = 0; //记录网络更新次数 pInput->LoadCorpusfile(corpusFile); //载入语料库文件 PrintStartInfo(corpusFile); //打印网络功能信息 pOut->Initialize(pInput->pChnLex->ulVocSizeC); //初始化输出层 Initialize(); //生成中间梯度向量 while (true) { t++; //记录对语料库训练次数 pOut->L = 0; //log10(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++; } } double PPL = pow(10, -pOut->L / pInput->runningWords); cout << t << "次训练,累加对数和概率: " << pOut->L << "训练集上的困惑度为" << PPL << endl; if (t % 5 == 0) //每5次保存一下 { SaveParameters(); //保存参数 } if (MAX_TRAIN_EPOCH == t) //等于最大训练遍数,停止训练 { break; } } SaveParameters(); //保存参数 PrintOverInfo(count, pInput->corpus.size()); return true; }
15/5/6补充:增加了测试PPL的函数:
double CAlgothrim::TestPPL(char *testFile) { //在测试集上计算PPL,返回PPL pInput->LoadCorpusfile(testFile); pOut->L = 0; //log10(Pwt) L是累加的对数概率 for (int i=0; i<pInput->corpus.size(); i++) //遍历语料库的句子 { //神经网络前馈运算 pInput->NextSentence(i); //从语料库中读取第i个句子做测试 int len = pInput->sentence.size(); //读入的句子长度 for (int k=0; k+N<len; k++) //对一个句子N-len次测试 { pInput->Computex(k); //计算输入层的x的特征向量 pHidden->OutHidden(pInput->x); //计算隐层输出 pOut->Output(pHidden->a, pInput->expectedID); //计算输出层 //cout << "prob: " << pOut->L << endl; } } double PPL = pow(10, -pOut->L / pInput->runningWords); cout << "在测试集上的累加对数和概率: " << pOut->L << "测试集上的困惑度为" << PPL << endl; return PPL; }
为了方便,我把这个源码打包了,可以在下面的地址下载:
http://download.csdn.net/detail/a635661820/8853783
相关文章推荐
- Feedforward Neural Network Language Model(NNLM)原理及数学推导
- 词嵌入(word2vec)-NNLM(Neural Network Language Model)
- 词嵌入(word2vec)-NNLM(Neural Network Language Model)
- 一步一步分析讲解神经网络基础-Feedforward Neural Network
- Recurrent Neural Network Based Language Model(RNNLM)原理及BPTT数学推导
- tf-idf及核心代码c++实现
- Recurrent Neural Network Language Modeling Toolkit代码学习
- Feed Forward and Backward Run in Deep Convolution Neural Network 论文阅读笔记
- 读论文《Recurrent neural network based language model 》
- 基于visual c++之windows核心编程代码分析(53)在C++中嵌入汇编实现DLL注入源代码
- C++实现搜狗浏览器加速代理客户端(HTTP/HTTPS)的核心代码
- Coursera deeplearning.ai 深度学习笔记1-2-Neural Network Basics-逻辑回归原理推导与代码实现
- 3行核心代码解决汉诺塔问题(C++递归实现)
- Deep Multi-scale Convolutional Neural Network for Dynamic Scene Deblurring代码实现注意实现
- 连连看游戏核心代码(C++实现)
- 插值法之Language和基本插值多项式的C++代码实现
- 基于visual c++之windows核心编程代码分析(53)在C++中嵌入汇编实现DLL注入源代码
- Singleton模式的C++实现研究(示例代码)
- 实现DataGrid跨行(核心代码)
- 用C/C++实现SMC动态代码加密技术