本节将继续就前面课程中的应用实例介绍Fast.AI
的具体实现,并深入介绍相关原理。主要内容包括:
- 类型变量的内置矩阵含义分析。
- 随机梯度算法的实现。
- 循环神经网络(
RNN
)的原理与实现。
一. 使用PCA
对类型变量的内置矩阵进行分析
前面课程讲述了如何将类型变量映射为连续型向量。那么这些连续型向量又都表征了数据的什么特征呢?这可通过可视化技术进行分析。但由于向量维度可能太高,而我们至多只能在三维空间中实现可视化,因此,首先需要利用主成分分析(PCA
,Principle Component Analysis
)方法进行降维。
from sklearn.decomposition import PCA
pca = PCA(n_components=3)
movie_pca = pca.fit(movie_emb.T).components_
最终获得各部电影在第一维上的最高得分与最低得分如下图。可以看出,第一维可能表征了电影是严肃性题材,还是娱乐性题材。
二. 以线性回归演示自定义的随机梯度算法
首先定义线性函数,均方误差函数,以及损失函数:
# 线性函数,其中a,b为参量
def lin(a,b,x): return a*x+b
# 均方误差函数
def mse(y_hat, y): return ((y_hat - y) ** 2).mean()
# 损失函数
def mse_loss(a, b, x, y): return mse(lin(a,b,x), y)
然后生成相应数据,定义参量,并都转换为Pytorch
的Variable
类型(这是针对Pytorch
的0.3.0
版本)。
# 产生10000组线性数据,斜率为3,常数项为8
x, y = gen_fake_data(10000, 3., 8.)
x,y = V(x),V(y)
# 初始化参数,设置梯度求解为真
a = V(np.random.randn(1), requires_grad=True)
b = V(np.random.randn(1), requires_grad=True)
然后写优化循环:
learning_rate = 1e-3
for t in range(10000):
loss = mse_loss(a,b,x,y)
# 这一句会计算requires_grad=True的参数的梯度
loss.backward()
# 更新参数值
a.data -= learning_rate * a.grad.data
b.data -= learning_rate * b.grad.data
# 梯度置零,因为可能会有多个损失函数,若再调用其他损失函数的backward()时,需要做此操作。
a.grad.data.zero_()
b.grad.data.zero_()
三. 循环神经网络的原理与实现
1. 三层循环的神经网络示例
约定神经网络结构表示如下:
- 所有的形状代表着激活层(即非线性函数)输出。
- 所有箭头代表着线性加权操作。
- 矩形为输入层,圆形为隐藏层,三角形为输出层。
考虑如下场景:在一段文本中,通过前三个字母,预测第四个。首先,三个输入字母是同质的,因此对其所做的基础操作(线性变换和非线性输出)应当是一样的;其次,三个字母有先后顺序,越靠后的字母与所需预测的字母关系越紧密,这代表着一种序列关系。因此一种合理的网络结构如下所示:
其中颜色一致的箭头代表着所做操作的参数是一样的。如三个输入字母所连接的绿线,代表着对三个字母的基础操作是一致的。另外,还约定隐含层之间的系数也一致,这样,不同的字母在不同的层输入,代表着上文所描述的时序性。
基于上述结构,循环网络实现代码如下:
class Char3Model(nn.Module):
def __init__(self, vocab_size, n_fac):
super().__init__()
# 字母表的内置矩阵
self.e = nn.Embedding(vocab_size, n_fac)
# 输入层
self.l_in = nn.Linear(n_fac, n_hidden)
# 隐含层
self.l_hidden = nn.Linear(n_hidden, n_hidden)
# 输出层
self.l_out = nn.Linear(n_hidden, vocab_size)
def forward(self, c1, c2, c3):
# 三条绿色输入线所做的操作
in1 = F.relu(self.l_in(self.e(c1)))
in2 = F.relu(self.l_in(self.e(c2)))
in3 = F.relu(self.l_in(self.e(c3)))
# 为使下面三条语句保持一致的形式,先初始化零向量。
h = V(torch.zeros(in1.size()).cuda())
# 三个激活层三角形所做的操作:非线性输出
h = F.tanh(self.l_hidden(h+in1))
h = F.tanh(self.l_hidden(h+in2))
h = F.tanh(self.l_hidden(h+in3))
# 输出层操作
return F.log_softmax(self.l_out(h))
假设构造了一个Char3Model
实例m
,接下来定义m
的优化函数:
opt = optim.Adam(m.parameters(), 1e-2)
然后进行训练:
fit(m, md, 1, opt, F.nll_loss)
其中md
是由ColumnarModelData
生成的数据集。
2. 通用RNN
的实现
观察Char3Model()
的forward()
方法,其中三条绿线和三个三角所代表的操作形式一致,因此可以用循环来表示为更紧凑的形式。事实上,这样做之后,循环的次数就可以由参数设定,这样就演变出了一个更为通用的RNN的实现,相应的结构图示也可简化为下图:
相应代码如下:
class CharLoopModel(nn.Module):
# This is an RNN!
def __init__(self, vocab_size, n_fac):
super().__init__()
self.e = nn.Embedding(vocab_size, n_fac)
self.l_in = nn.Linear(n_fac, n_hidden)
self.l_hidden = nn.Linear(n_hidden, n_hidden)
self.l_out = nn.Linear(n_hidden, vocab_size)
def forward(self, *cs):
# cs stands for character set
bs = cs[0].size(0)
h = V(torch.zeros(bs, n_hidden).cuda())
for c in cs:
inp = F.relu(self.l_in(self.e(c)))
h = F.tanh(self.l_hidden(h+inp))
return F.log_softmax(self.l_out(h), dim=-1)
在隐含层处,我们将新输入的字符特征和已经处理过的若干字符的特征进行相加,这可能会导致信息丢失。因此一个可以改进的地方是将相加操作改为连接操作。
3. 使用Pytorch
实现RNN
使用Pytorch
中的nn.RNN()
实现RNN
,Pytorch
会自动创建输出层,另外不需要自己写循环语句,Pytorch
已经做了相应操作。
class CharRnn(nn.Module):
def __init__(self, vocab_size, n_fac):
super().__init__()
self.e = nn.Embedding(vocab_size, n_fac)
self.rnn = nn.RNN(n_fac, n_hidden)
self.l_out = nn.Linear(n_hidden, vocab_size)
def forward(self, *cs):
bs = cs[0].size(0)
h = V(torch.zeros(1, bs, n_hidden))
inp = self.e(torch.stack(cs))
outp,h = self.rnn(inp, h)
return F.log_softmax(self.l_out(outp[-1]), dim=-1)
其中Pytorch
的RNN
对象,不仅会返回输出outp
,还会返回各个隐含层输入状态h
。而且,在每个隐含层处都会有输出,因此outp
是多维输出,而我们仅需最后一个,所以在进行log_softmax()
时,指定outp
的索引为-1
。
4. 更为紧凑的RNN
结构
上述结构有个问题,比如我们使用三个字母预测第四个字母,若采用上述结构,则本次输入和上次输入之间重叠了两个字母,这就意味着这两个字母的相关操作是重复的。
如果我们记录各个隐含层的输入状态,那么这部分就可被复用。更进一步,我们没有必要一个字符一个字符地移动输入,而是以三个字符为步长进行移动。比如一个字符串:c1c2c3c4c5c6c7c8c9,按照之前的结构,计算过程是:输入c1c2c3,预测c4,然后输入c2c3c4,预测c5 ……而按照新结构,我们可以第一次输入c1c2c3,预测c2c3c4,第二次输入c4c5c6,预测
c5c6c7 ……即每输入一个字符,就预测一个字符。这样在输入c4预测c5时,由于已经记录c2c3的隐含层输入状态,这样其实也就是利用c2c3c4预测c5。
最终所得的网络结构如下图所示,即将输出纳入到循环中。
备注
- 从
Fast.AI
的学习器中获取Pytorch
模型:learer.model
。其中model
是用@property
标记的属性。 - vim技巧
- tag ColumnarModelData: 跳转到
ColumnarModelData
定义处。 - ctrl + ]: 跳转到光标所在的变量的定义处。
- ctr + t: 回到上次浏览位置。
- tag ColumnarModelData: 跳转到
RNN
中,隐含层之间的连接系数初始化:这些系数构成了结构图中黄色线条所代表的操作中的线性变换,这一变换会被循环执行,如利用3个字母预测第4个时,则对第一个字母,这一操作会被执行3次。若这些系数组成的矩阵,有明显放大或缩小输入向量的作用的话,执行多次后会导致其所作用的向量要么过大要么过小。出于这个考虑,可将这些系数所组成的矩阵初始化为单位矩阵。
一些有用的链接
- RNN网络参数初始化方法。
- 课程wiki: 本节课程的一些相关资源,包括课程笔记、课上提到的博客地址等。