在卷积语言模型建模时,我们选取上下文长度ctx_len进行训练,预测时选取句子的最后ctx_len个分词做预测,这样句子的前0~seql-1-ctx_len个词对于预测没有任何帮助,这对于语言处理来说显然是不利的。
在词袋语言模型建模时,我们舍弃ctx_len的概念,利用前缀和,用前seql-1个词作为训练数据,后seql-1个词作为标签训练。最终用这句话所有的分词求前缀和做预测,这样句子中所有的分词都参与了预测,是更准确的。
但是,词袋语言模型没有考虑到分词的顺序带来的影响,例如:0 1 2 -> 3,将0 1 2装入词袋、张量求前缀和的结果和 0 2 1、 1 0 2、 1 2 0等等没有区别,没有办法区分分词的顺序。
在此基础上,如果考虑分词间的前后次序对预测的影响,有:
h
(
x
t
)
=
C
e
l
l
(
h
(
x
(
t
−
1
)
+
x
t
)
h(x_t) = Cell(h(x(t-1) + x_t)
h(xt)=Cell(h(x(t−1)+xt)
这样类似于二叉树结构顺序输入的模型,输入的不同次序带来的结果也不同。
如上图所示的二叉树结构,输入X_0 、X_1和X_2次序的不同,对于每层的影响也不同。这样就有效的保留了原句中分词间次序对于预测的影响。
RNN模型的搭建
#RNNLM.py
#encoding: utf-8
import torch
from torch import nn
class RNNCell(nn.Module):
def __init__(self, isize, hsize, osize, dropout, **kwargs):
super(RNNCell, self,).__init__() ##调用父类的初始化函数
self.net = nn.Sequential(nn.Linear(isize + osize, hsize),
nn.ReLU(inplace=True), #设置relu激活函数,inplace=True在原始张量上进行
nn.Dropout(p=dropout, inplace=False),#设置丢弃率防止过拟合,同时创建一个新的张量
nn.Linear(hsize, osize, bias=False))
# x_t:(bsize, isize)
def forward(self, x_t, h_p): # x_t为当前词向量,h_p是h_x(t-1)即上一步的结果
return self.net(torch.cat([h_p, x_t], dim=-1))
class RNNLayer(nn.Module):
def __init__(self, isize, hsize, dropout,norm_residual=True, **kwargs):
super(RNNLayer, self,).__init__() ##调用父类的初始化函数
self.init_hx = nn.Parameter(torch.zeros(1, isize)) #h_0即最初初始化状态
#初始化时第一维加1维,便于后续的拼接
self.cell = RNNCell(isize, hsize, isize, dropout)
self.drop = nn.Dropout(p=dropout, inplace=True)
self.normer = nn.LayerNorm(isize) #做归一化
self.norm_residual = norm_residual #设置变量存储做判断
# input: (bsize, seql-1, isize)
def forward(self, input, hx=None):#hx是None说明在训练,hx不是None是Tensor说明在解码
#解码时候就用前一步的hx隐状态
_ = self.normer(input) #稳定之后的结果
bsize = input.size(0) #存一下bsize
_hx = hx if isinstance(hx, torch.Tensor) else self.init_hx.expand(bsize, -1)
#如果hx已经是一个张量不是None说明已开始计算,否则用初始化的值
#expand函数将init_hx:(1,isize) -> (bsize, isize),设置参数为-1表示大小保留原状
rs = []
for xu in input.unbind(1): #将input在第一维展开 xu:(bsize, isize)
_hx = self.cell(xu, _hx) #xu为新分词,hx为前一步隐状态
rs.append(_hx)
rs = (_ if self.norm_residual else input) + self.drop(torch.stack(rs, dim=1))
#将rs在指定维度拼起来:rs:(bsize, seq-1, isize)
#如果参数初始化做的好,就用LayerNorm后的值,否则用原始值
if hx is None:
return rs
else:
return (rs, _hx)
class NNLM(nn.Module):
def __init__(self, vcb_size, isize, hsize, dropout,
nlayer, bindemb=True, **kwargs): #有多少个词,就分多少类,类别数为vcb_size
super(NNLM, self).__init__()
self.emb = nn.Embedding(vcb_size, isize,
padding_idx=0) #<pad>的索引为0
#self.comp = nn.Linear(ctx_len * isize, isize, bias=False) #将4个词的向量降维为isize
self.drop = nn.Dropout(p=dropout, inplace=True) #embedding后dropout
self.nets = nn.Sequential(*[RNNLayer(isize, hsize, dropout)
for _ in range(nlayer)])
self.classifier = nn.Linear(isize, vcb_size)
if bindemb:
self.classifier.weight = self.emb.weight#将emb的权重赋给分类器
self.normer = nn.LayerNorm(isize)
self.out_normer = nn.LayerNorm(isize)
# input: (bsize, seql-1) 句数,句长-1 由于最后一个词是预测不作为输入
def forward(self, input):
out = self.emb(input)
# out: (bsize, seql-1, isize)
out = self.drop(out)
out = self.normer(out) #使用归一化,使模长均匀
out = self.out_normer(self.nets(out))
return self.classifier(out) #分类产生参数
RNN模型的训练
#RNNtrain.py
#encoding: utf-8
import torch
from torch import nn
from RNNLM import NNLM #导入模型
from h5py import File as h5File #读训练数据
from math import sqrt
from random import shuffle #使输入数据乱序,使模型更均衡
from lrsch import SqrtDecayLR
from tqdm import tqdm
train_data = "train.h5"#之前已经张量转文本的h5文件
isize = 64
hsize = isize * 2 #设置初始参数
dropout = 0.3 #设置丢弃率
nlayer = 4 #设置层数
gpu_id = -1 #设置是否使用gpu
lr = 1e-3 #设置初始学习率
max_run = 8 #设置训练轮数
nreport = 5000 #每训练5000个batch打印一次
tokens_optm = 25000 #设置更新参数的词数阈值
def init_model_parameters(modin): #初始化模型参数
with torch.no_grad(): #不是训练不用求导
for para in modin.parameters():
if para.dim() > 1: #若维度大于1,说明是权重参数
_ = 1.0 / sqrt(para.size(-1))
para.uniform_(-_,_) #均匀分布初始化
for _m in modin.modules(): #遍历所有小模型
if isinstance(_m, nn.Linear):#如果小模型是linear类型
if _m.bias is not None: #初始化bias
_m.bias.zero_()
elif isinstance(_m, nn.LayerNorm):#初始化LayerNorm参数
_m.weight.fill_(1.0)
_m.bias.zero_()
elif isinstance(_m, nn.Embedding): #如果是嵌入层则进入初始化权重矩阵,将<pad>的向量初始化为零向量
if _m.padding_idx >= 0:
_m.weight[_m.padding_idx].zero_()
return modin
def train(train_data, tl, model, lossf, optm, cuda_device,
nreport=nreport, tokens_optm=tokens_optm):#nreport为每训练一部分词打一次epoch
model.train() #设置模型在训练的模式
src_grp = train_data["src"] #从输入数据中取出句子
_l = 0.0 #_l用来存当前loss
_t = 0 #_t用来存句子数
_lb = 0.0
_tb = 0
_tom = 0
for _i, _id in tqdm(enumerate(tl, 1)):
seq_batch = torch.from_numpy(src_grp[_id][()])
#seq_batch:[bsize, seql]
_seqlen = seq_batch.size(-1) #取出每个batch的句长
if cuda_device is not None:
seq_batch = seq_batch.to(cuda_device, non_blocking=True)
#将数据放在同一gpu上
seq_batch = seq_batch.long() #数据转换为long类型
seq_i = seq_batch.narrow(1, 0, _seqlen - 1) #训练数据读取前seql-1的数据
#seq_i:[bsize, seql-1]
seq_o = seq_batch.narrow(1, 1, _seqlen - 1) #预测数据读取后seql-1的数据做标签
#seq_o:[bsize, seql-1]
out = model(seq_i) #获得模型结果
#out: {bsize, seql-1, vcb_size} vcb_size即预测类别数
loss = lossf(out.view(-1, out.size(-1)), seq_o.contiguous().view(-1))
#转换out维度为[bsize*(seql-1),vcb_size],seq_o:[bsize*(seql-1)]
_lossv = loss.item()
_l += _lossv #整个训练集的loss
_lb += _lossv #每个batch的loss
_n = seq_o.ne(0).int().sum().item() #seq_o中不是<pad>的位置的数量
_t += _n #整个训练集的分词数
_tb += _n #每个batch的分词数
_tom += _n
loss.backward() #反向传播求导
if _tom > tokens_optm: #当词数大于时更新参数
optm.step() #参数的更新
optm.zero_grad(set_to_none=True)#参数更新后清空梯度
_tom = 0
if _i % nreport == 0: #每训练5000个batch打印一次
print("Average loss over %d tokens: %.2f"%(_tb, _lb/_tb))
_lb = 0.0
_tb = 0
save_model(model, "checkpoint.rnn.pt") #暂存检查点模型
return _l / _t #返回总的loss
def save_model(modin, fname): #保存模型所有内容 权重、偏移、优化
torch.save({name: para.cpu() for name, para in
model.named_parameters()}, fname)
t_data = h5File(train_data, "r")#以读的方式打开训练数据
vcb_size = t_data["nword"][()].tolist()[0] #将返回的numpy的ndarray转为list
#在我们的h5文件中存储了nword: 总词数
model = NNLM(vcb_size, isize, hsize, dropout, nlayer)
model = init_model_parameters(model) #在cpu上初始化模型
lossf = nn.CrossEntropyLoss(reduction='sum', ignore_index=0,
label_smoothing=0.1)
#设置ignore_index=0,即忽略<pad>的影响
if (gpu_id >= 0) and torch.cuda.is_available(): #如果使用gpu且设备支持cuda
cuda_device = torch.device("cuda", gpu_id) #配置gpu
torch.set_default_device(cuda_device)
else:
cuda_device = None
if cuda_device is not None: #如果要用gpu
model.to(cuda_device) #将模型和损失函数放在gpu上
lossf.to(cuda_device)
optm = torch.optim.Adam(model.parameters(), lr=lr,
betas=(0.9, 0.98), eps=1e-08)
#使用model.parameters()返回模型所有参数,
lrm = SqrtDecayLR(optm, lr) #将优化器和初始学习率传入
tl = [str(_) for _ in range(t_data["ndata"][()].item())] #获得字符串构成的训练数据的list
#save_model(model, "eva.rnn.pt")
for i in range(1, max_run + 1):
shuffle(tl) #使数据乱序
_tloss = train(t_data, tl, model, lossf, optm,
cuda_device) #获取每轮训练的损失
print("Epoch %d: train loss %.2f"%(i, _tloss)) #打印日志
save_model(model, "eva.rnn.pt")
lrm.step() #每轮训练后更新学习率
t_data.close()
在命令行输入:
:~/nlp/lm$ python RNNtrain.py
RNN模型的解码与预测
模型的解码
#RNNLM.py
#encoding: utf-8
# input: (bsize, seql)
def decode(self, input, maxlen=50): #设置最多生成50个分词
rs = [input]
bsize =input.size(0)
hxd = {} #存每一层产生的hx
out = self.normer(self.drop(self.emb(input)))
# out:(bsize, seql, isize)
for i, layer in enumerate(self.nets):
out, hxd[i] = layer(out, hx=hxd.get(i, "init")) #手动遍历每一层
#得到了每层的结果rs以及隐状态hxd[i]
# out:(bsize, seql, isize)
out = self.classifier(self.out_normer(out.narrow(
1, out.size(1) - 1, 1))).argmax(-1)
# narrow:(bsize, 1, isize) -> (bsize, 1, vcb_size)
# -> (bsize, 1)
rs.append(out)
done_trans = out.squeeze(1).eq(2) #记录是否完成生成
# done_trans:(bsize)
if not done_trans.all().item():
for i in range(maxlen - 1): #已经生成一个词并添加到rs中了
out = self.normer(self.drop(self.emb(out)))
# out:(bsize, 1, isize)
for _, layer in enumerate(self.nets): #将前一层结果给下一层做输入
out, hxd[_] = layer(out, hx=hxd[_])
#将之前的隐状态输入生成下一层的隐状态
out = self.classifier(self.out_normer(out)).argmax(-1) #最后的输出预测下一个分词
# out:(bsize, 1, vcb_size) -> (bsize, 1)
rs.append(out) #将新预测分词添加到结果
done_trans |= out.squeeze(1).eq(2)
if done_trans.all().item(): #当全都为True,说明此batch中所有句子预测都为<eos>,即解码完成
break
return torch.cat(rs, dim=1)
模型的预测
预测脚本与前面的词袋模型脚本基本一样,我们在命令行中输入:
:~/nlp/lm$ python RNNpredict.py test.h5 zh.vcb pred.txt checkpoint.rnn.pt
:~/nlp/lm$ less pred.txt
我们可以看到经过若干个epoch训练出的rnn模型对于文本的预测续写。
LSTM模型的搭建
但是,由于上图这样的结构,h_3是由h_2和X_2得出,而h_2是由h_1和X_1得出。那么对于h_3来说,X_2的影响就要比X_1大很多。于是在进行预测时,句末的分词对预测的影响就要大于句子中间或者开头的分词。
而在实际句子中,
回顾 安理会 主席 以 安理会 名义 在 1994 年 4 月 7 日 发表 的 声明 ( S / PRST / 1994 / 16 ) 和 在 1994 年 4 月 30 日 发表 的 声明 ( S / PRST / 1994 / 21 ) ,
上句中“声明”的预测很依赖于动词“发表”,这符合我们的模型的偏好即句末的分词影响更大。但是像本句句尾的“1994”是该年发表声明的时间,显然前面的“1994”对它影响更大,而不是句末的分词。
这就不符合我们模型的偏好,因此我们需要重新设计cell,使得模型的偏好更均衡,而不是句末的分词影响最大。
sigmoid激活函数
S
i
g
m
o
i
d
Sigmoid
Sigmoid 函数的公式如下所示,其可以将神经网络的输出映射到(0,1)之中,公式如下所示。
s
i
g
m
o
i
d
(
x
)
=
1
1
+
e
−
x
sigmoid(x)=\frac{1}{1+e^{-x}}
sigmoid(x)=1+e−x1
函数图像如下:
如图上图所示,当输入的值趋于正无穷或负无穷时,梯度会趋近零,神经网络学习不到特征,从而导致深度神经网络无法进行训练。
>>> import torch
>>> a=torch.randn(4,5)
>>> a.sigmoid()
tensor([[0.4421, 0.6359, 0.5781, 0.4000, 0.4741],
[0.4456, 0.4710, 0.0643, 0.3690, 0.4005],
[0.4287, 0.7616, 0.6746, 0.1963, 0.6259],
[0.4268, 0.8297, 0.4883, 0.4696, 0.6682]])
>>> b=a.exp()
>>> b/(1+b)
tensor([[0.4421, 0.6359, 0.5781, 0.4000, 0.4741],
[0.4456, 0.4710, 0.0643, 0.3690, 0.4005],
[0.4287, 0.7616, 0.6746, 0.1963, 0.6259],
[0.4268, 0.8297, 0.4883, 0.4696, 0.6682]])
我们利用sigmoid函数来使得学习的权重进行变化,通过0或1的变化有助于更新或忘记信息。
我们通过门来控制变化,要么是1则记住,要么是0则忘掉。因记忆能力有限,记住重要的,忘记无关紧要的。
LSTM的基本结构如下:
h
t
,
c
t
=
L
S
T
M
C
e
l
l
(
x
t
,
h
t
−
1
,
c
t
−
1
)
h_t, c_t = LSTMCell(x_t, h_{t-1}, c_{t-1})
ht,ct=LSTMCell(xt,ht−1,ct−1)
h
=
x
t
∣
h
t
−
1
_h = x_t | h_{t-1}
h=xt∣ht−1
h
i
d
d
e
n
=
a
c
t
(
L
i
n
e
a
r
(
h
)
)
hidden=act(Linear(_h))
hidden=act(Linear(h))
i
g
a
t
e
=
s
i
g
m
o
i
d
(
L
i
n
e
a
r
(
h
)
)
igate=sigmoid(Linear(_h))
igate=sigmoid(Linear(h))
o
g
a
t
e
=
s
i
g
m
o
i
d
(
L
i
n
e
a
r
(
h
)
)
ogate=sigmoid(Linear(_h))
ogate=sigmoid(Linear(h))
f
g
a
t
e
=
s
i
g
m
o
i
d
(
L
i
n
e
a
r
(
h
)
)
fgate=sigmoid(Linear(_h))
fgate=sigmoid(Linear(h))
c
t
=
c
t
−
1
∗
f
g
a
t
e
+
h
i
d
d
e
n
∗
i
g
a
t
e
c_t=c_{t-1}*fgate+hidden*igate
ct=ct−1∗fgate+hidden∗igate
h
t
=
c
t
∗
o
g
a
t
e
h_t=c_t*ogate
ht=ct∗ogate
#LSTMLM.py
#encoding: utf-8
import torch
from torch import nn
class LSTMCell(nn.Module):
def __init__(self, isize, osize, dropout, **kwargs):
super(LSTMCell, self,).__init__()
self.net = nn.Linear(isize + osize, 4 * osize) #4*osize后分块,分别得到hidden、igate、ogate和fgate
self.drop = nn.Dropout(p=dropout, inplace=False)
def forward(self, x_t, h_p):
hx, cx = h_p
out = self.net(torch.cat([hx, x_t], dim=-1))#将h_x和x_t向量拼接
# out:(bsize, 4, osize)
out = out.view(out.size(0), 4, -1)
hidden = out.select(1, 0).relu() #取out第1维的第0个张量过relu激活函数
igate, fgate, ogate = out.narrow(1, 1, 3).sigmoid().unbind(1)
#取out第1维的1,2,3个张量过sigmoid激活函数再按照第一维拆分
#i/f/o gate:(bsize, 1, osize) hidden(bsize, 1, osize)
cx = (cx * fgate).addcmul(hidden, igate) #cx*fgate+hidden*igate
hx = cx * ogate
return hx, cx #hx是本层输出,cx是向后传的cell
class LSTMLayer(nn.Module):
def __init__(self, isize, dropout,norm_residual=True, **kwargs):
super(LSTMLayer, self,).__init__()
self.init_hx = nn.Parameter(torch.zeros(1, isize))
self.init_cx = nn.Parameter(torch.zeros(1, isize))
self.cell = LSTMCell(isize, isize, dropout)
self.drop = nn.Dropout(p=dropout, inplace=True)
self.normer = nn.LayerNorm(isize) #做归一化
self.norm_residual = norm_residual #设置变量存储做判断
# input: (bsize, seql-1, isize)
def forward(self, input, hx=None): #hx若不为None则为(hx, cx)
_ = self.normer(input) #稳定之后的结果
bsize = input.size(0)
_hx = hx if isinstance(hx, (tuple, list)) else (self.
init_hx.expand(bsize, -1), self.init_cx.expand(bsize, -1))
rs = []
for xu in input.unbind(1):
_hx = self.cell(xu, _hx)
rs.append(_hx[0]) #只添加_hx中的hx
rs = (_ if self.norm_residual else input) + self.drop(torch.stack(rs, dim=1))
if hx is None:
return rs
else:
return (rs, _hx)
#如果参数初始化做的好,就用LayerNorm后的值,否则用原始值
class NNLM(nn.Module):
def __init__(self, vcb_size, isize, hsize, dropout,
nlayer, bindemb=True, **kwargs): #有多少个词,就分多少类,类别数为vcb_size
super(NNLM, self).__init__()
self.emb = nn.Embedding(vcb_size, isize,
padding_idx=0) #<pad>的索引为0
#self.comp = nn.Linear(ctx_len * isize, isize, bias=False) #将4个词的向量降维为isize
self.drop = nn.Dropout(p=dropout, inplace=True) #embedding后dropout
self.nets = nn.Sequential(*[LSTMLayer(isize, dropout)
for _ in range(nlayer)])
self.classifier = nn.Linear(isize, vcb_size)
if bindemb:
self.classifier.weight = self.emb.weight#将emb的权重赋给分类器
self.normer = nn.LayerNorm(isize)
self.out_normer = nn.LayerNorm(isize)
# input: (bsize, seql-1) 句数,句长-1 由于最后一个词是预测不作为输入
def forward(self, input):
out = self.emb(input)
# out: (bsize, seql-1, isize)
out = self.drop(out)
out = self.normer(out) #使用归一化,使模长均匀
out = self.out_normer(self.nets(out))
return self.classifier(out) #分类产生参数
# input: (bsize, seql)
def decode(self, input, maxlen=50): #设置最多生成50个分词
rs = [input]
bsize =input.size(0)
hxd = {}
out = self.normer(self.drop(self.emb(input)))
# out:(bsize, seql, isize)
for i, layer in enumerate(self.nets):
out, hxd[i] = layer(out, hx=hxd.get(i, "init"))
out = self.classifier(self.out_normer(out.narrow(
1, out.size(1) - 1, 1))).argmax(-1)
# narrow:(bsize, 1, isize) -> (bsize, 1, vcb_size)
# -> (bsize, 1)
rs.append(out)
done_trans = out.squeeze(1).eq(2) #记录是否完成生成
# done_trans:(bsize)
if not done_trans.all().item():
for i in range(maxlen - 1):
out = self.normer(self.drop(self.emb(out)))
# out:(bsize, 1, isize)
for _, layer in enumerate(self.nets):
out, hxd[_] = layer(out, hx=hxd[_])
out = self.classifier(self.out_normer(out)).argmax(-1)
# out:(bsize, 1, vcb_size) -> (bsize, 1)
rs.append(out)
done_trans |= out.squeeze(1).eq(2)
if done_trans.all().item(): #当全都为True,说明此batch中所有句子预测都为<eos>,即解码完成
break
return torch.cat(rs, dim=1)
LSTM模型的预测
我们可以看出,LSTM的解码与RNN一样,只是在cell的设计上不同,训练脚本与预测脚本都是相同的,我们在命令行输入:
:~/nlp/lm$ python LSTMpredict.py test.h5 zh.vcb pred.txt checkpoint.lstm.pt
:~/nlp/lm$ less pred.txt