相关参考
Dive-into-DL-PyTorch
这是关于深度学习各种算法与网络的Pytorch讲解与实现,需要一定的线性代数和高数基础,与之对应的还有TensorFlow实现,链接如下:
d2l_zh_tensorflow2.0
Pytorch中文文档
思考
推荐两本深度学习相关的入门书籍,《深度学习入门 基于Python的理论与实现》和《深度学习的数学》,都是日本人写的,不得不说在一个专业的入门领域他们做得很精致,将复杂的专业知识通过一系列简洁明了的方式呈现出来,让初学者不被各种专业名词、数学公式劝退。
在使用Pytorch时,有太多数据对我来说是“黑箱”,就算你理解了模型,理解了计算过程,如果不把数据的每一次变动和变动原因理解,最终也没法去解决现实问题和设计更好的模型,也许有人会说参数太多,反向误差传播对人来说计算每一步是不现实的,这确实是的,这也是深度学习被称为炼金术的原因,但至少要对每一步数据变化做到心中有数,张量的形状、梯度裁剪、padding等,由于数据总是多维度,一旦步骤多了,自己都会被数据的变化整晕,这也是劝退的坑之一,我会试着将案例中每一步的数据变化通过流图展现清楚,这比给公式要强多了。
对于我们初学者来说,公式是恶心的,其次是论文图例。无论多艰深的学科,给初学者的形象得是和蔼可亲的,不然怎么忽悠人进来,当韭菜发现这玩意玩起来很吃力的时候,由于他们对于自己已经投入进去的时间与精力不会轻易放弃,在经济学上这叫沉没成本,只能咬着牙继续学,然后这中间学得大成的人接着忽悠韭菜进来,当然这里面影响因素最大的还是资本的力量。接下来还是讲讲RNN吧,相比于CNN,这个网络确实不太好理解。
循环神经网络
简介
总得有个简单介绍,能学习RNN的至少已经接触过CNN了,对深度学习有了一定的了解和学习,别的博客基本是那几样图例和公式,真的不能指望专业搞科研的能不整公式,但也没办法,RNN还是得配上公式,只是公式蕴含了太多信息,展开公式后还需要对照具体模型输入参数模拟,然后理解了基础的RNN后面还有GRU、LSTM、seq2seq等着你。
循环神经网络(Recurrent Neural Networks,RNN)已经在众多自然语言处理(Natural Language Processing, NLP)中取得了巨大成功以及广泛应用,如语音识别、机器翻译、文本摘要生成、聊天机器人等,当然这些应用里面的网络是RNN的变种,但它们都贯彻了RNN的模式和思想。
相比于计算机视觉领域,自然语言处理领域的发展较为缓慢,且近些年来真正落地的项目不多,这当然和自然语言蕴含的信息与图像完全不同,想了解这块内容的推荐吴军的《数学之美》,他的另一本书《浪潮之巅》也很不错。
循环神经网络中一个序列当前的输出与前面的输出也有关,与RNN中各层相互独立不同,具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。理论上,RNN能够对任何长度的序列数据进行处理。
案例实现
这里的案例采用了Dive-into-DL-PyTorch中循环神经网络中的内容,基于字符级循环神经网络的语言模型,代码实现基本一样,我只是对其中的数据细节进行说明,中间配合公式和图例进行说明,希望可以帮助理解RNN的运行原理和具体案例中的数据细节。
数据集加载与处理
预处理一个语言模型数据集,并将其转换成字符级循环神经网络所需要旳输入格式。此处的数据集为周杰伦从第一张专辑《Jaαy》到第十张专辑《跨时代》中的歌词,应用循环神经网络来训练一个语言模型。当模型训练好后,就可以用这个模型来创作歌词。数据集不大,大家对最终生成的歌词也不要有啥期望,不过那种周杰伦的感觉还是有的。
import time
import math
import numpy as np
import random
import zipfile
import torch
from torch import nn, optim
import torch.nn.functional as F
# 设置模型训练设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
# 加载周杰伦歌词数据集
def load_data_jay_lyrics(path):
with zipfile.ZipFile(path) as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
# 将换行符换成空格
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
# 字典列表,里面为无重复字符
idx_to_char =list(set(corpus_chars))
# 为每个字符设置索引
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx) # 2582
# 将数据集文本的每个字符转化成索引
corpus_indices = [char_to_idx[char] for char in corpus_chars]
print(vocab_size)
return corpus_indices, char_to_idx, idx_to_char, vocab_size
# 加载语料数据集
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = load_data_jay_lyrics('你的数据集路径')
这里的数据集是一个压缩包,读取里面的所有文本内容,最终得到我们下面所需要的数据。
One-hot向量
就像RNN接受图像像素数值向量,传入神经网络的得是向量,在NLP对于文本向量是一个很大的课题,这里为了Dive-into-DL-PyTorch的作者为了专注于RNN的讲解,使用的是简单的one-hot向量,就是讲每一个字典中的字符映射成一个独一无二的向量,例如字典中有5个字符。则最终的one-hot向量为:
[[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]]
这种表示方法虽然简单,但弊端也很大,每个one-hot是没有联系的,不能表示词之间的相似性,这对于文本字符间本身就有联系不相符,同时当字典很大时会造成维度过高的问题,总之在NLP中基本不会用这种方式表征字符向量。这里需要词嵌入的知识,当然Dive-into-DL-PyTorch也实现了Skip_gram和COBW算法。
Pytorch现在有自带的one-hot方法,这里还是手动实现下:
def one_hot(x, n_class, dtype=torch.float32):
x = x.long()
res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)
# 填充张量,看Pytorch文档理解吧
res.scatter_(1, x.view(-1, 1), 1)
return res
x = torch.tensor([0, 2, 8, 9])
one_hot(x, 10)
输出:
tensor([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])
每次采样的小批量的形状是(批量大小,时间步数).下面的函数将这样的小批量变换成数个可以输入
进网络的形状为(批量大小,词典大小)的矩阵,矩阵个数等于时间步数。挺抽象,如果看不到具体数据,理解挺麻烦的,在下面用到这个函数的时候再看。
def to_onehot(X, n_class):
# 这种写法是大牛的常规操作
return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
X = torch.arange(10).view(2, 5)
inputs = to_onehot(X, 12)
print(inputs)
输出:
[tensor([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.]]),
tensor([[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]]),
tensor([[0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.]]),
tensor([[0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.]]),
tensor([[0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]])]
python有时候太能干也挺头疼的,一行干了别的语言20行的事,然后你自己得解读半天,Dive-into-DL-PyTorch作者很牛逼,看他代码会被他神一般的处理数据代码折服,但理解就得自己苦逼了,查看Pytorch文档,文档里还有一堆公式,你还得回去补课,不断套娃,不过看大牛的代码还是收益良多的。
准备模型
初始化模型参数
下面就是公式时间了,这块我尽量将其理清逻辑,其实只要逻辑清晰,公式只是你写代码的依据。
输入数据存在时间相关性的情况,即每个输入之间需要有东西将它们联系起来,这里联系的媒介就是隐藏变量
H
t
H_t
Ht,再假设
X
t
∈
R
n
×
d
X_t \in\mathbb R^{n \times d}
Xt∈Rn×d是序列是时间步t的小批量输入,
H
t
H_t
Ht是该时间步的隐藏变量,且
H
t
∈
R
n
×
d
H_t\in\mathbb R^{n \times d}
Ht∈Rn×d。
这里解释下上面的专业名词:
R
n
×
d
\mathbb R^{n \times d}
Rn×d是向量空间,可以看成
n
×
d
n \times d
n×d的矩阵。
时间步:每次输入到返回一次输出的过程,如下图所示:
下一个时间步不仅需要上一个时间步的输出作为输入,也需要上一个时间步的隐藏变量,所以说
H
t
H_t
Ht
表征了输入字符序列间联系。当然为了保存上一个时间步的隐藏变量
H
t
−
1
H_{t-1}
Ht−1,还要引入一个新的权重参数
W
h
h
∈
R
h
×
h
W_{hh}\in\mathbb R^{h\times h}
Whh∈Rh×h,神经网络就是这样,每当需要一个变量
a
a
a来控制或描述输入之间联系或其他过程时,需要用新的权重参数
W
W
W来描述它,最终模型都会将它们包含进去。
循环神经网络的参数:
隐藏层的权重
W
x
h
∈
R
d
×
h
W_{xh}\in\mathbb R^{d \times h}
Wxh∈Rd×h、
W
h
h
∈
R
h
×
h
W_{hh}\in\mathbb R^{h \times h}
Whh∈Rh×h和偏差
b
h
∈
R
1
×
h
b_h\in \mathbb R^{1 \times h}
bh∈R1×h
输出层的权重
W
h
q
∈
R
h
×
q
W_{hq}\in\mathbb R^{h \times q}
Whq∈Rh×q和偏差
b
h
∈
R
1
×
q
b_h\in \mathbb R^{1 \times q}
bh∈R1×q
d、h、q这些参数看案例中的数据,光看公式太抽象了,还是看案例的数据流程吧。
以下是生成这些参数的实现代码:
# num_hiddens是一个超参数,vocab_size = 2582
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
def get_params():
def _one(shape):
# 使用正态分布随机化初始参数变量
ts = torch.tensor(np.random.normal(0, 0.1, size=shape), device=device, dtype=torch.float32)
return torch.nn.Parameter(ts, requires_grad=True)
# 隐藏层参数
W_xh = _one((num_inputs, num_hiddens))
W_hh = _one((num_hiddens, num_hiddens))
b_h = torch.nn.Parameter(torch.zeros(num_hiddens, device=device, requires_grad=True))
# 输出层参数
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, requires_grad=True))
return nn.ParameterList([W_xh, W_hh, b_h, W_hq, b_q])
定义模型
def init_rnn_state(batch_size, num_hiddens, device):
'''
返回初始化的隐藏状态
返回一个形状为(批量大小,隐藏单元个数)的值为0的NDArray组成的元组
'''
return (torch.zeros((batch_size, num_hiddens), device=device), )
def rnn(inputs, state, params):
'''
该函数定义了在一个时间步里如何计算隐藏状态和输出
inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
'''
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, )
做个测试来观察输出结果的个数(时间步数),以及第一个时间步的输出层的形状和隐藏状态的形状
X = torch.arange(10).view(2, 5)
state = init_rnn_state(X.shape[0], num_hiddens, device)
inputs = to_onehot(X.to(device), vocab_size)
params = get_params()
outputs, state_new = rnn(inputs, state, params)
print(len(outputs), outputs[0].shape, state_new[0].shape)
输出结果:
5 torch.Size([2, 2582]) torch.Size([2, 256])
来看看具体的模型参数
模拟的输入为
2
×
5
2\times 5
2×5的输入向量,即两组包含五个字符的句子,通过to_onehot函数将字符索引转换成onehot向量,现在就有了五组输入,每组输入为两个onehot向量,每个字符用
1
×
2582
1\times 2582
1×2582的向量表示,同时原始输入的顺序也做了改变,可以看到在onehot向量组中,0对应5,1对应6,2对应7等等,这里主要是针对采样方法,下面会有介绍。
通过代码也能看到模型中的运算过程,用公式来表示就是:
H
t
=
t
a
n
h
(
X
t
W
x
h
+
H
t
−
1
W
h
h
+
b
h
)
H_t = tanh(X_tW_{xh}+H_{t-1}W_{hh}+b_h)
Ht=tanh(XtWxh+Ht−1Whh+bh)
Y
t
=
H
t
W
h
q
+
b
q
Y_t=H_tW_{hq}+b_q
Yt=HtWhq+bq激活函数
t
a
n
h
tanh
tanh,可以参考该博客,也讲述了为什么RNN选择该函数.
时序数据采样
随机采样
随机采样中,每个样本是原始序列上任意截取的一段序列,相邻的两个随机小批量在原始序列上的位置不一定相邻,因此无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,在训练模型是,每次随机采样前都需要重新初始化隐藏状态。
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
'''
batch_size: 每个小批量的样本数
num_steps: 每个样本包含的时间步数
'''
num_examples = (len(corpus_indices) - 1) // num_steps
epoch_size = num_examples // batch_size
example_indices = list(range(num_examples))
random.shuffle(example_indices)
def _data(pos):
return corpus_indices[pos: pos + num_steps]
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for i in range(epoch_size):
# 每次读取batch_size个随机样本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size]
X = [_data(j * num_steps) for j in batch_indices]
Y = [_data(j * num_steps + 1) for j in batch_indices]
yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device)
测试采样:
my_seq = list(range(30))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY: ', Y, '\n')
结果:
X: tensor([[12., 13., 14., 15., 16., 17.],
[ 6., 7., 8., 9., 10., 11.]], device='cuda:0')
Y: tensor([[13., 14., 15., 16., 17., 18.],
[ 7., 8., 9., 10., 11., 12.]], device='cuda:0')
X: tensor([[18., 19., 20., 21., 22., 23.],
[ 0., 1., 2., 3., 4., 5.]], device='cuda:0')
Y: tensor([[19., 20., 21., 22., 23., 24.],
[ 1., 2., 3., 4., 5., 6.]], device='cuda:0')
相邻采样
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
if device == None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
corpus_indices = torch.tensor(corpus_indices, dtype=torch.float32, device=device)
data_len = len(corpus_indices)
batch_len = data_len // batch_size
indices = corpus_indices[0: batch_size*batch_len].view(batch_size, batch_len)
epoch_size = (batch_len - 1) // num_steps
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
同样适用上面的数据进行测试:
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY: ', Y, '\n')
测试结果:
X: tensor([[ 0., 1., 2., 3., 4., 5.],
[15., 16., 17., 18., 19., 20.]], device='cuda:0')
Y: tensor([[ 1., 2., 3., 4., 5., 6.],
[16., 17., 18., 19., 20., 21.]], device='cuda:0')
X: tensor([[ 6., 7., 8., 9., 10., 11.],
[21., 22., 23., 24., 25., 26.]], device='cuda:0')
Y: tensor([[ 7., 8., 9., 10., 11., 12.],
[22., 23., 24., 25., 26., 27.]], device='cuda:0')
预测函数
该函数基于前缀prefix(含有数个字符的字符串)来预测接下来num_chars个字符:
def predict_rnn(prefix, num_chars, rnn, params,
init_rnn_state, num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
'''
基于前缀prefix来预测接下来的num_chars个字符
'''
state = init_rnn_state(1, num_hiddens, device)
output = [char_to_idx[prefix[0]]]
print(output)
for t in range(num_chars + len(prefix) -1):
# 将上一时间步的输出作为当前时间步的输入
X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
# 计算输出和更新隐藏状态
(Y, state) = rnn(X, state, params)
# 下一个时间步的输入时prefix里的字符或者当前的最佳预测字符
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(Y[0].argmax(dim=1).item()))
return ''.join([idx_to_char[i] for i in output])
测试该函数,设定一个前缀,如“分开”创作长度为10个字符(不考虑前缀长度),这时生成的预测结果应该是随机的,这是由于模型现在没有训练,里面的参数都是随机的。
predict_rnn('分开', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size, device, idx_to_char, char_to_idx)
测试结果:
'分开限刀安疤委啬秃文赚七'
这样至少证明模型参数各层设计没有问题。
裁剪梯度
循环神经网络中较容易岀现梯度衰减或梯度爆炸。为了应对梯度爆炸,我们可以裁剪梯度( clip gradient).假设我们把所有模型参数梯度的元素拼接成一个向量
g
g
g,并设裁剪的阈值是
θ
θ
θ.
g
=
m
i
n
(
θ
∥
g
∥
,
1
)
g=min(\frac{\theta}{\rVert g\rVert},1)
g=min(∥g∥θ,1)
梯度衰减或梯度爆炸涉及模型训练是的反向传播过程,这是一个数据黑洞,前向传播按公式走就可以了,反向传播是不断优化参数的过程,根据我们给模型的正确答案和模型自己预测的结果进行比对和修正,这里不做详细说明,也是一个大坑。
这里的剪裁梯度代码如下:
def grad_clipping(params, theta, device):
norm = torch.tensor([0.0], device=device)
for param in params:
norm += (param.grad.data ** 2).sum()
norm = norm.sqrt().item()
if norm > theta:
for param in params:
param.grad.data *= (theta / norm)
模型训练
# 小批量随机梯度下降算法
# lr 学习率
def sgd(params, lr, batch_size):
for param in params:
# 注意这里更改param时用的是param.data
param.data -= lr * param.grad / batch_size
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, is_random_iter, num_epochs, num_steps,
lr, clipping_theta, batch_size, pred_period, pre_len, prefixes):
# 选择采样方式
if is_random_iter:
data_iter_fn = data_iter_random
else:
data_iter_fn = data_iter_consecutive
params = get_params()
loss = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
if not is_random_iter: # 如使用相邻采样,epoch开始时初始化隐藏状态
state = init_rnn_state(batch_size, num_hiddens, device)
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
for X, Y in data_iter:
if is_random_iter: # 如使用随机采样,在每个小批量更新前初始化隐藏状态
state = init_rnn_state(batch_size, num_hiddens, device)
else: # 否则需要使用detach函数从计算图分离隐藏状态
for s in state:
s.detach_()
inputs = to_onehot(X, vocab_size)
# outputs有num_steps个形状为(batch_size, vocab_size)的矩阵
(outputs, state) = rnn(inputs, state, params)
# 拼接之后形状为(num_steps * batch_size, vocab_size)
outputs = torch.cat(outputs, dim=0)
# Y的形状为(batch_size, num_steps), 转置后再变成长度为batch*num_steps的向量,这样和输出的行一一对应
y = torch.transpose(Y, 0, 1).contiguous().view(-1)
# 使用交叉熵损失计算平均分类误差
l = loss(outputs, y.long())
# 梯度清零
if params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
# 剪裁梯度
grad_clipping(params, clipping_theta, device)
# 由于误差已经取过均值, 梯度不用再做平均
sgd(params, lr, 1)
l_sum += l.item() * y.shape[0]
n += y.shape[0]
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
一个函数的参数过多是很让人难受的,《代码整洁之道》探讨过这个问题,不过也得看具体情境,在模型设置和训练这方面,有太多的数学参数和相关变量,当然你要把它们打包简化也是可以的,只是学习知识还是得分清主次,理解模型才是主要的,虽然函数代码阅读挺困难的,一个又一个变量公式,看文档也是不断地套娃下去,这也是算法研究和程序设计的差异,关注点也不一样。
这里的反向误差传播、优化函数、学习率等没有具体介绍,相信能关注RNN的这些也问题不大。
参数设置和训练
很多参数都是超参数,自己找感觉设置吧,当然大牛早就探索了不少强大的模型,它们的参数设置是公开的,不过你的GPU估计不够搞定这些模型
num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, True, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)
测试结果如下:
epoch 50, perplexity 48.856914, time 2.62 sec
[290]
- 分开始 不能再说 我不要再想 我不要再想 我不能再想 我不能再想 我不要再想 我不能再想 我不能再想 我
[2281]
- 不分开 你的生你 我知道觉 我要不再 你的世界 你离开 让我们 一个人 在等待 回忆 不来 我手 你
epoch 100, perplexity 16.260598, time 2.68 sec
[290]
- 分开始打开 将我的世界 我用我害去 难过往也是我的过去 都没有了解 我都没有人瘦的气 我都不用 我的手
[2281]
- 不分开 把笑时间的画 只是我在空口 我用上一起 我知道冲不开 不用吵这样 我说了这些代 你知道你 如果我
epoch 150, perplexity 8.699328, time 2.66 sec
[290]
- 分开始打开了将 这个决 是色了 我手上 不好 在我 不想 我手中的微笑 不会怕你的喜 只有一样 那
[2281]
- 不分开 一身下了一直 会感的手都只不用 跟再说 爱一定 你给的 我也还不到 我知道你不要 你我在等你
epoch 200, perplexity 5.648656, time 2.66 sec
[290]
- 分开始不开了 因为我还是会有 分本了 不代 我的烦恼 你的身影 我轻轻汉起 我们愉快的梦游 我说好
[2281]
- 不分开 说不了解 就让我没有眼睛 我们 只是我的过性 再灭 只让我们夫 我有你会请我很多难想 你已经过了
epoch 250, perplexity 4.074645, time 2.66 sec
[290]
- 分开始爱的 时间 断了 只有多少去 慢女有雾爱 我爱情行 太过一次 寻的爱 你一上找慢慢的回忆 再不
[2281]
- 不分开 一路 强 Ya 铁 你在我没有 因为你笑的你 我放盘去着 有多少的无法 景在一起 我轻轻的叹息 后
时间是每一轮训练所需的时间,最终生成的歌词不忍直视,不过这只是最初级的RNN,也没用使用较好词向量。
总结
循环神经网络或者说深度学习的一些网络本身并不难理解,公式也不复杂,但在实际操作中有太多繁杂的数据操作,同时模型里面的数据变动如同黑箱,最终模型的好坏和实际中的效果很难用数学去解释说明,真正商业的项目需要依靠海量数据集和强大的运算能力,最终的模型也无法保证百分之百可靠,只能说我们还是要理智地去看待所谓的AI浪潮。