条件语言模型
对于位置在i的单词,常规的word2vec模型考虑前后两个窗口的位置,这里只考虑单词左边位置的部分单词。这个公式考虑了单词周围的信息
CBOW的条件语言模型
P(W1 ,W2 ,…,Wm ) = ∏(下标t=1,上标m)P(Wt | Wt-1 ,Wt-2 )
CBOW模型取窗口大小为2,
缺点
无法考虑到远处的信息,即使增大窗口,也还是会有更远处的信息无法被记录,同时也会带来计算量和存储量增大的问题
RNN
基本因子 ht = tanh(ht-1Wh + xt Wx +b)
公式中ht-1代表上一个时间步输出状态,Wh用来将ht-1的形状转换为能输入到现在的模型中
反向传播BPTT
如果直接将整个的RNN串联为一条模型,在反向传播时需要存储从开始到最后的所有中间状态,如果是很长的一段文字,则需要很大的内存量。因此提出了BPTT。
基本思想是将RNN切分为多个小块,每个小块独立计算,独自反向传播
需要注意两点
1.按顺序输入数据,因为RNN的引入就是为了考虑序列的顺序性,所以数据的顺序是很重要的
2.平移不同batch的初始位置,因为要保证数据的顺序性,所以必须要计算每个batch开始的位置
代码实现RNN
class RNN:
def __init__(self,Wh,Wx,b):
self.params = [Wh,Wx,b]
#初始化梯度
self.grads = [np.zeros_like(Wh),np.zeros_like(Wx),np.zeros_like(b)]
self.cache = None
#前向传播
def forward(self,hprev,x):
Wh,Wx,b = self.params
h_next = np.dot(hprev,Wh) + np.dot(x,Wx) + b
h_next = np.tanh(h_next)
self.cache = [x,h_prev,h_next]
return h_next
#反向传播
def backward(self,dh_next):
Wh,Wx,b = self.params
dt = dh_next * (1-dh_next**2)#因为前向用的激活函数是tanh
db = np.sum(dt,axis=0)
x,hprev,h_next = self.cache
dW_h = np.dot(hprev.T,dt)
dh_prev = np.dot(dt,Wh)
dW_x = np.dot(dt,x.T)
dx = np.dot(dt,Wx.T)
self.grads[0][...] = dW_h
self.grads[1][...] = dW_x
self.grads[2][...] = db
return dx,dh_prev
TimeRNN
TimeRNN的基本思想是将T个RNN块组合成一块独立计算,每一块的隐藏状态h保存在成员变量中,由一个变量stateful控制是否保存隐藏状态,输入是一批数据xs
class TimeRNN:
def __init__(Wh,Wx,b,stateful=False):#stateful控制是否保存时序状态
self.params = [Wh,Wx,b]
self.grads = [np.zeros_like(Wh),np.zeros_like(Wx),np.zeros_like(b)]
self.layers = None
self.h,self.dh = 0,0#保存最后一块RNN的输出状态和传递给上一层的梯度
self.stateful = stateful
#根据statful决定是否保存状态
def save_state(self,h):
self.h = h
def reset_state(self):#重置输出状态
self.h = 0
#前向传播
#N是batch_size,T是时序数,D是数据维度,H是隐藏层大小
def forward(self,xs):
Wh,Wx,b = self.params#继承变量
N,T,D = xs.shape
D,H = Wx.shape
hs = np.empty((N,T,H),dtype='f')
self.layers = []
for i in range(T):
layer = RNN(Wh,Wx,b)#创建一个小RNN块
h = layer.forward(self.h,xs[:,i,:])
hs[:,i,:] = h#将每一层的输出状态保存
self.layers.append(layer)#保存每一层
return hs
#反向传播
def backward(self,dhs):
Wh,Wx,b = self.params
N,T,H = dhs.shape
D,H = Wx.shape
dxs = np.empty((N,T,D),dtype='f')#初始化dxs,对x的导数
dh = 0
grads = [0,0,0]
for t in reversed(T):#反向传播
layer = self.layers[t]
dx,dh_prev = layer.backward(dhs[:,t,:]+dh)#梯度求和
#将x的梯度保存
dxs[:,t,:] = dx
for i,grad in enumerate(layer.grads):#保存每一个RNN层的梯度
grads[i] += grad
for i,grad in enumerate(grads):#TRNN层最终的梯度是各个RNN层的梯度之和
grads[i][...] = grad#梯度求和覆盖
self.dh = dh
return dxs
RNNLM(RNN语言模型)
模型由一个将输入词汇嵌入的embed层,TimeRNN层,Affine层和一个softmax with loss层组成
每个层的初始权重使用Xavier初始值
Xavier初始值,即将初始权重变为标准差为1/√n的值(将值除以√n)
这里使用的RNN,Affine和loss层,embed层都是用Time型,即多个时间步层,因为每个时间步都需要一个层进行计算
class simpleRNNlm:
def __init__(self,vocab_size,embed_size,hidden_size):#分别是单词数,embedding层的维数,隐藏层的大小
V,D,H = vocab_size,embed_size,hidden_size
rn = np.random.randn#简化函数写法
#第一层是embed层
W_embed = (rn(V,D)/100).astype('f')
#第二层是RNN层,RNN层包含了Wh,Wx,b
W_h = (rn(D,H)/np.sqrt(D)).astype('f')#这里使用了Xavier初始化
W_x = (rn(H,H)/np.sqrt(H)).astype('f')#初始化除数等于神经元个数
b = (rn(H)/np.sqrt(H)).astype('f')
#第三层是affine层
W_a = (rn(H,V)/np.sqrt(H)).astype('f')
b_a = (rn(V)/np.sqrt(V)).astype('f')
#将所有层联合起来
self.layers = [
TimeEmbedding(W_embed),
TimeRNN(W_x,W_h,b,True),
TimeAffine(W_a,b_a)
]
self.loss_layer = TimeSoftmaxWithLoss()
self.rnn = self.layers[1]#取出rnn
#添加梯度和参数
self.params,self.grads = [],[]
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
#前向传播
def forward(self,xs,ts):#输出为xs
for layer in self.layers:
xs = layer.forward(xs)
loss = self.loss_layer.forward(xs,ts)
return loss
#反向传播
def backward(self,dout=1):
dx = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dx = layer.backward(dx)
return dx
#重置输出状态
def reset_state(self):
self.rnn.reset_state()#调用TimeRNN的reset函数
语言模型的评价
通常使用困惑度(perplexity)来衡量语言模型的好坏
困惑度是概率的倒数(书上是这么说的)
用困惑度衡量模型,在单词的判断中,常规的概率可以表示模型取到单词的几率
而困惑度可以表示对于下一个预测结果,语料库中有多少个备选结果
因此,困惑度可以很好地反应模型的好坏程度
通常,好的模型,取到单个准确单词的概率就大,困惑度就小