问题描述:样本为所有恐龙名字,为了构建字符级语言模型来生成新的名称,你的模型将学习不同的名称模式,并随机生成新的名字。
在这里你将学习到:
- 如何存储文本数据以便使用rnn进行处理。
- 如何合成数据,通过每次采样预测,并将其传递给下一个rnn单元。
- 如何构建字符级文本生成循环神经网络。
- 为什么梯度修剪很重要?
1 import numpy as np 2 import random 3 import time 4 import cllm_utils
1 - 问题描述
1.1 - 数据集与预处理
1 # 获取名称 2 data = open("dinos.txt", "r").read() 3 4 # 转化为小写字符 5 data = data.lower() 6 7 # 转化为无序且不重复的元素列表 8 chars = list(set(data)) 9 10 # 获取大小信息 11 data_size, vocab_size = len(data), len(chars) 12 13 print(chars) 14 print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))
|
data='Aachenosaurus\nAardonyx\nAbdallahsaurus\...' chars=['o', 'm', 'k', 'v', 'w', 'b', 'j', 'd', 'x', 'a', 'h', 'i',
|
1 char_to_ix = {ch:i for i,ch in enumerate(sorted(chars))} 2 ix_to_char = {i:ch for i,ch in enumerate(sorted(chars))} 3 4 print(char_to_ix) 5 print(ix_to_char)
|
{'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6,
|
1.2 - 模型回顾
模型的结构如下:
- 初始化参数
- 循环:
- 前向传播计算损失
- 反向传播计算关于损失的梯度
- 修剪梯度以免梯度爆炸
- 用梯度下降更新规则更新参数。
- 返回学习后了的参数
2 - 构建模型中的模块
在这部分,我们将来构建整个模型中的两个重要的模块:
- 梯度修剪:避免梯度爆炸
- 取样:一种用来产生字符的技术
2.1 梯度修剪
在这里,我们将实现在优化循环中调用的clip函数.回想一下,整个循环结构通常包括前向传播、成本计算、反向传播和参数更新。
在更新参数之前,我们将在需要时执行梯度修剪,以确保我们的梯度不是“爆炸”的.
接下来我们将实现一个修剪函数,该函数输入一个梯度字典输出一个已经修剪过了的梯度.有很多的方法来修剪梯度,我们在这里
使用一个比较简单的方法.梯度向量的每一个元素都被限制在[−N,N]的范围,通俗的说,有一个maxValue(比如10),
如果梯度的任何值大于10,那么它将被设置为10,如果梯度的任何值小于-10,那么它将被设置为-10,如果它在-10与10之间,那么它将不变。
1 def clip(gradients, maxValue): 2 """ 3 使用maxValue来修剪梯度 4 5 参数: 6 gradients -- 字典类型,包含了以下参数:"dWaa", "dWax", "dWya", "db", "dby" 7 maxValue -- 阈值,把梯度值限制在[-maxValue, maxValue]内 8 9 返回: 10 gradients -- 修剪后的梯度 11 """ 12 # 获取参数 13 dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby'] 14 15 # 梯度修剪 16 for gradient in [dWaa, dWax, dWya, db, dby]: 17 np.clip(gradient, -maxValue, maxValue, out=gradient) 18 19 gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby} 20 21 return gradients | 函数接受最大阈值,并返回修剪后的梯度
|
2.2 - 采样
1 def sample(parameters, char_to_is, seed): 2 """ 3 根据RNN输出的概率分布序列对字符序列进行采样 4 5 参数: 6 parameters -- 包含了Waa, Wax, Wya, by, b的字典 7 char_to_ix -- 字符映射到索引的字典 8 seed -- 随机种子 9 10 返回: 11 indices -- 包含采样字符索引的长度为n的列表。 12 """ 13 14 # 从parameters 中获取参数 15 Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], |
|
3 - 构建语言模型
3.1 - 梯度下降
在这里,我们将实现一个执行随机梯度下降的一个步骤的函数(带有梯度修剪)。我们将一次训练一个样本,所以优化算法将是随机梯度下降,这里是RNN的一个通用的优化循环的步骤:
- 前向传播计算损失
- 反向传播计算关于参数的梯度损失
- 修剪梯度
- 使用梯度下降更新参数
我们来实现这一优化过程(单步随机梯度下降),这里我们提供了一些函数:
# 示例,可参照上一篇博客RNN的前向后向传播。 def rnn_forward(X, Y, a_prev, parameters): """ 通过RNN进行前向传播,计算交叉熵损失。 它返回损失的值以及存储在反向传播中使用的“缓存”值。 """ .... return loss, cache def rnn_backward(X, Y, parameters, cache): """ 通过时间进行反向传播,计算相对于参数的梯度损失。它还返回所有隐藏的状态 """ ... return gradients, a def update_parameters(parameters, gradients, learning_rate): """ Updates parameters using the Gradient Descent Update Rule """ ... return parameters
def optimize(X, Y, a_prev, parameters, learning_rate = 0.01): """ 执行训练模型的单步优化。 参数: X -- 整数列表,其中每个整数映射到词汇表中的字符。 Y -- 整数列表,与X完全相同,但向左移动了一个索引。 a_prev -- 上一个隐藏状态 parameters -- 字典,包含了以下参数: Wax -- 权重矩阵乘以输入,维度为(n_a, n_x) Waa -- 权重矩阵乘以隐藏状态,维度为(n_a, n_a) Wya -- 隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a) b -- 偏置,维度为(n_a, 1) by -- 隐藏状态与输出相关的权重偏置,维度为(n_y, 1) learning_rate -- 模型学习的速率 返回: loss -- 损失函数的值(交叉熵损失) gradients -- 字典,包含了以下参数: dWax -- 输入到隐藏的权值的梯度,维度为(n_a, n_x) dWaa -- 隐藏到隐藏的权值的梯度,维度为(n_a, n_a) dWya -- 隐藏到输出的权值的梯度,维度为(n_y, n_a) db -- 偏置的梯度,维度为(n_a, 1) dby -- 输出偏置向量的梯度,维度为(n_y, 1) a[len(X)-1] -- 最后的隐藏状态,维度为(n_a, 1) """ # 前向传播 loss, cache = cllm_utils.rnn_forward(X, Y, a_prev, parameters) # 反向传播 gradients, a = cllm_utils.rnn_backward(X, Y, parameters, cache) # 梯度修剪,[-5 , 5] gradients = clip(gradients,5) # 更新参数 parameters = cllm_utils.update_parameters(parameters,gradients,learning_rate) return loss, gradients, a[len(X)-1]
给定恐龙名称的数据集,我们使用数据集的每一行(一个名称)作为一个训练样本。每100步随机梯度下降,你将抽样10个随机选择的名字,看看算法是怎么做的。
3.2 - 训练模型
记住要打乱数据集,以便随机梯度下降以随机顺序访问样本。当examples[index]包含一个恐龙名称(String)时,为了创建一个样本(X,Y),你可以使用这个:
1 index = j % len(examples) 2 X = [None] + [char_to_ix[ch] for ch in examples[index]] 3 Y = X[1:] + [char_to_ix["\n"]]
1 def model(data, ix_to_char, char_to_ix, num_iterations=3500, 2 n_a=50, dino_names=7,vocab_size=27): 3 """ 4 训练模型并生成恐龙名字 5 6 参数: 7 data -- 语料库 8 ix_to_char -- 索引映射字符字典 9 char_to_ix -- 字符映射索引字典 10 num_iterations -- 迭代次数 11 n_a -- RNN单元数量 12 dino_names -- 每次迭代中采样的数量 13 vocab_size -- 在文本中的唯一字符的数量 14 15 返回: 16 parameters -- 学习后了的参数 17 """ 18 19 # 从vocab_size中获取n_x、n_y 20 n_x, n_y = vocab_size, vocab_size 21 22 # 初始化参数 23 parameters = cllm_utils.initialize_parameters(n_a, n_x, n_y) 24 25 # 初始化损失 26 loss = cllm_utils.get_initial_loss(vocab_size, dino_names) 27 28 # 构建恐龙名称列表 29 with open("dinos.txt") as f: 30 examples = f.readlines() 31 examples = [x.lower().strip() for x in examples] 32 33 # 打乱全部的恐龙名称 34 np.random.seed(0) 35 np.random.shuffle(examples) 36 37 # 初始化LSTM隐藏状态 38 a_prev = np.zeros((n_a,1)) 39 40 # 循环 41 for j in range(num_iterations): 42 # 定义一个训练样本 43 index = j % len(examples) 44 X = [None] + [char_to_ix[ch] for ch in examples[index]] 45 Y = X[1:] + [char_to_ix["\n"]] 46 47 # 执行单步优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数 48 # 选择学习率为0.01 49 curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters) 50 51 # 使用延迟来保持损失平滑,这是为了加速训练。 52 loss = cllm_utils.smooth(loss, curr_loss) 53 54 # 每2000次迭代,通过sample()生成“\n”字符,检查模型是否学习正确 55 if j % 2000 == 0: 56 print("第" + str(j+1) + "次迭代,损失值为:" + str(loss)) 57 58 seed = 0 59 for name in range(dino_names): 60 # 采样 61 sampled_indices = sample(parameters, char_to_ix, seed) 62 cllm_utils.print_sample(sampled_indices, ix_to_char) 63 64 # 为了得到相同的效果,随机种子+1 65 seed += 1 66 67 print("\n") 68 return parameters
|
比如说某恐龙名字叫 zzh 那么X = ['0','z','z','h'] Y = ['z','z','h','\n']
需要注意的是我们使用了 index= j % len(examples), 其中= 1....num_iterations, 为了确保examples[index]总是有效的 (index小于len(examples)), rnn_forward()会将X的第一个值None解释为 x<0>=0向量. 此外,为了确保Y等于X,会向左移动一步, 并添加一个附加的“\n”以表示恐龙名称的结束。
|
1 #开始时间 2 start_time = time.clock() 3 4 #开始训练 5 parameters = model(data, ix_to_char, char_to_ix, num_iterations=3500) 6 7 #结束时间 8 end_time = time.clock() 9 10 #计算时差 11 minium = end_time - start_time 12 13 print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒") | 结果如下: 第1次迭代, 损失值为:23.0873360855
损失值为:27.8841604914 |
以上是自己定义参数来实现字符语言模型,下面用keras实现。
| ['r', 'p', 'j', 'i', 't', 'z', 'q', 'o', 'd',
|
|
|
| {'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5,
|
|
x.shape (19879, 30, 27)
y.shape (19879, 27)
['aachenos', 'achenosa', 'chenosau', 'henosaur']
['a', 'u', 'r', 'u'] x是从原字符串中,每max_len个字符生成的样本 y是每个样本的后一个字符
比如说原字符串为=''abcdefghijklmn',max_len=8 x = ['abcdefgh','bcdefghi','cdefghij',...] y = ['i','j','k',...] |
| 注意:输入model里面的input的size是不包括所有样本的, 也就是说只有一个样本的大小(时间步,oe-hot的长度), 在fit的时候x是包含所有样本的x.shape(样本个数,样本的 长度,每个字符one-hot的长度)
|
| |
| 每次predict之后,得到一个softmax之后的向量,该选取 哪个单词作为label呢? (1)贪婪采样:每次都选可能性最大的下一个字符,但这种方法会 得到重复的、可预测的字符串 (2)随机采样:控制随机性的大小-->softmax温度 更高的温度得到的熵是更大的采样分布,会生成更加出人意料、 更加无结构的生成数据;更低的温度对应更小的随机性,以及更 加可预测的生成数据。 |
|
epoch 1 Epoch 1/1 19879/19879 [==============================] - 5s |
参考文献: