本节将继续就前面课程中的应用实例介绍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_()
约定神经网络结构表示如下:
考虑如下场景:在一段文本中,通过前三个字母,预测第四个。首先,三个输入字母是同质的,因此对其所做的基础操作(线性变换和非线性输出)应当是一样的;其次,三个字母有先后顺序,越靠后的字母与所需预测的字母关系越紧密,这代表着一种序列关系。因此一种合理的网络结构如下所示:
其中颜色一致的箭头代表着所做操作的参数是一样的。如三个输入字母所连接的绿线,代表着对三个字母的基础操作是一致的。另外,还约定隐含层之间的系数也一致,这样,不同的字母在不同的层输入,代表着上文所描述的时序性。
基于上述结构,循环网络实现代码如下:
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
生成的数据集。
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)
在隐含层处,我们将新输入的字符特征和已经处理过的若干字符的特征进行相加,这可能会导致信息丢失。因此一个可以改进的地方是将相加操作改为连接操作。
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
。
RNN
结构上述结构有个问题,比如我们使用三个字母预测第四个字母,若采用上述结构,则本次输入和上次输入之间重叠了两个字母,这就意味着这两个字母的相关操作是重复的。
如果我们记录各个隐含层的输入状态,那么这部分就可被复用。更进一步,我们没有必要一个字符一个字符地移动输入,而是以三个字符为步长进行移动。比如一个字符串:c1c2c3c4c5c6c7c8c9,按照之前的结构,计算过程是:输入c1c2c3,预测c4,然后输入c2c3c4,预测c5 ……而按照新结构,我们可以第一次输入c1c2c3,预测c2c3c4,第二次输入c4c5c6,预测
c5c6c7 ……即每输入一个字符,就预测一个字符。这样在输入c4预测c5时,由于已经记录c2c3的隐含层输入状态,这样其实也就是利用c2c3c4预测c5。
最终所得的网络结构如下图所示,即将输出纳入到循环中。
Fast.AI
的学习器中获取Pytorch
模型:learer.model
。其中model
是用@property
标记的属性。ColumnarModelData
定义处。RNN
中,隐含层之间的连接系数初始化:这些系数构成了结构图中黄色线条所代表的操作中的线性变换,这一变换会被循环执行,如利用3个字母预测第4个时,则对第一个字母,这一操作会被执行3次。若这些系数组成的矩阵,有明显放大或缩小输入向量的作用的话,执行多次后会导致其所作用的向量要么过大要么过小。出于这个考虑,可将这些系数所组成的矩阵初始化为单位矩阵。