RNN实现字符级语言模型 - 恐龙岛(RNN前向后向版本+keras版本)

问题描述:样本为所有恐龙名字,为了构建字符级语言模型来生成新的名称,你的模型将学习不同的名称模式,并随机生成新的名字。


在这里你将学习到:

  • 如何存储文本数据以便使用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',
'e', 'l', 's', 't', 'n', 'z', 'p', 'y', 'g', 'f', '\n', 'q',
'r', 'u', 'c']
共计有19909个字符,唯一字符有27个

 

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, 
'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13,
'n': 14, 'o': 15, 'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20,
'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f',
7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k', 12: 'l', 13: 'm',
14: 'n', 15: 'o', 16: 'p', 17: 'q', 18: 'r', 19: 's', 20: 't',
21: 'u', 22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

 

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'], 
parameters['by'], parameters['b'] 16 vocab_size = by.shape[0] 17 n_a = Waa.shape[1] 18 19 # 步骤1 20 ## 创建独热向量x 21 x = np.zeros((vocab_size,1)) 22 23 ## 使用0初始化a_prev 24 a_prev = np.zeros((n_a,1)) 25 26 # 创建索引的空列表,这是包含要生成的字符的索引的列表。 27 indices = [] 28 29 # IDX是检测换行符的标志,我们将其初始化为-1。 30 idx = -1 31 32 # 循环遍历时间步骤t。在每个时间步中,从概率分布中抽取一个字符, 33 # 并将其索引附加到“indices”上,如果我们达到50个字符, 34 #(我们应该不太可能有一个训练好的模型),我们将停止循环,这有助于调试并防止进入无限循环 35 counter = 0 36 newline_character = char_to_ix["\n"] 37 38 while (idx != newline_character and counter < 50): 39 # 步骤2:使用公式1、2、3进行前向传播 40 a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b) 41 z = np.dot(Wya, a) + by 42 y = cllm_utils.softmax(z) 43 44 # 设定随机种子 45 np.random.seed(counter + seed) 46 47 # 步骤3:从概率分布y中抽取词汇表中字符的索引 48 idx = np.random.choice(list(range(vocab_size)), p=y.ravel()) 49 50 # 添加到索引中 51 indices.append(idx) 52 53 # 步骤4:将输入字符重写为与采样索引对应的字符。 54 x = np.zeros((vocab_size,1)) 55 x[idx] = 1 56 57 # 更新a_prev为a 58 a_prev = a 59 60 # 累加器 61 seed += 1 62 counter +=1 63 64 if(counter == 50): 65 indices.append(char_to_ix["\n"]) 66 67 return indices

 

 

 

 

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
Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu


第2001次迭代,

损失值为:27.8841604914
Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros

 

 以上是自己定义参数来实现字符语言模型,下面用keras实现。

#获取恐龙的名称
data = open('dinos.txt','r').read()
data = data.lower()

chars = list(set(data))

data_size,vocab_size = len(data),len(chars)

print(chars)
print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))
 
['r', 'p', 'j', 'i', 't', 'z', 'q', 'o', 'd',
'x', 's', 'v', 'e', 'l', 'g', 'k', 'n', 'm',
'c', 'b', 'f', 'y', 'w', 'u', 'h', 'a', '\n'] 共计有19909个字符,唯一字符有27个

 

 
kl_name = open('dinos.txt','r').read().lower()
# kl_name =kl_name.lower().split('\n')
# len(kl_name.split('\n'))
print(kl_name[:50])
 

 


char_to_ix = {ch:i for i,ch in enumerate(sorted(chars))}
ix_to_char = {i:ch for i,ch in enumerate(sorted(chars))}

print(char_to_ix)
print(ix_to_char)
 
{'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 
'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11,
'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16,
'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21,
'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e',
6: 'f', 7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k',
12: 'l', 13: 'm', 14: 'n', 15: 'o', 16: 'p',
17: 'q', 18: 'r', 19: 's', 20: 't', 21: 'u',
22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

 

 
#将字符序列向量化
max_len = 8

names = []
next_chars = []

for i in range(0,len(kl_name)-maxlen):
    names.append(kl_name[i:i+max_len]) 
    next_chars.append(kl_name[i+max_len])
    #每句话之后的下一个字符,相当于是label
    
x = np.zeros((len(names),maxlen,len(char_to_ix)))
y = np.zeros((len(names),len(char_to_ix)))

for i,name in enumerate(names):
    for j,char in enumerate(name):
        x[i,j,char_to_ix[char]] = 1
    y[i,char_to_ix[next_chars[i]]]=1
        
print('x.shape',x.shape)
print('y.shape',y.shape)
print(names[:4])
print(next_chars[:4])

 

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',...]


#构建用于预测下一个字符的单层LSTM模型
from keras.layers import SimpleRNN,Dense
from keras.models import Sequential
model = Sequential()
model.add(SimpleRNN(128,input_shape=(maxlen,len(char_to_ix))))
model.add(Dense(len(char_to_ix),activation='softmax'))
model.summary()
 

 注意:输入model里面的input的size是不包括所有样本的,

也就是说只有一个样本的大小(时间步,oe-hot的长度),

在fit的时候x是包含所有样本的x.shape(样本个数,样本的

长度,每个字符one-hot的长度)

 

 
#模型编译配置
import keras
optimizers = keras.optimizers.RMSprop(lr=0.01)
model.compile(optimizer='rmsprop',
              loss = 'categorical_crossentropy',
              metrics = ['acc'])

#目标是经过one-hot编码的,所以训练模型需要使用categorical_crossentropy作为损失
 
   
 
#给定模型预测、采样下一个字符的函数
def sample(preds,temperature=0.1):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1,preds,1)
    return np.argmax(probas)

 每次predict之后,得到一个softmax之后的向量,该选取

哪个单词作为label呢?

(1)贪婪采样:每次都选可能性最大的下一个字符,但这种方法会

得到重复的、可预测的字符串

(2)随机采样:控制随机性的大小-->softmax温度

更高的温度得到的熵是更大的采样分布,会生成更加出人意料、

更加无结构的生成数据;更低的温度对应更小的随机性,以及更

加可预测的生成数据。

 
import sys
import numpy as np
#文本生成循环
epochs = 6
for epoch in range(1,epochs):#每次循环生成10个字符(只保留最新的10个),循环6次
    print('epoch',epoch)
    model.fit(x,y,batch_size=128,epochs=1)
    
    #随机选取一个文本片段
    start_index = np.random.randint(0,len(names)-1)
    generated_text = names[start_index]
    print('--- Generating with seed:"' + generated_text[1:] + '"')
    for temperature in [0.2,0.5,1.0,1.2]:
        print("第"+str(epoch)+"次,temperature="+str(temperature)+
"生成的文本为"+generated_text) #生成10个字符 for i in range(10): sampled = np.zeros((1,maxlen,len(char_to_ix))) for j,char in enumerate(generated_text): sampled[0,j,char_to_ix[char]] = 1
          #将生成的10个字符转成one-hot的形式 preds = model.predict(sampled,verbose=0)[0]
        #预测的结果 predict的shape为(1,27) next_index = sample(preds=preds,temperature=temperature)
        #采样 next_char = ix_to_char[next_index] generated_text += next_char generated_text = generated_text[1:]
       #更新generated_text每轮只留最新的后0个字符
 

 

 

epoch 1
Epoch 1/1
19879/19879 [==============================] - 5s 
265us/step - loss: 1.3650 - acc: 0.5864 --- Generating with seed:"amosaur" 第1次,temperature=0.2生成的文本为hamosaur (1, 27) 第1次,temperature=0.5生成的文本为hamosaur (1, 27) 第1次,temperature=1.0生成的文本为hamosaur (1, 27) 第1次,temperature=1.2生成的文本为hamosaur (1, 27) epoch 2 Epoch 1/1 19879/19879 [==============================] - 5s
273us/step - loss: 1.3569 - acc: 0.5858 --- Generating with seed:"nimanta" 第2次,temperature=0.2生成的文本为animanta (1, 27) 第2次,temperature=0.5生成的文本为animanta (1, 27) 第2次,temperature=1.0生成的文本为animanta (1, 27) 第2次,temperature=1.2生成的文本为animanta (1, 27)

 

 参考文献:

1.【用Keras开发字符级神经网络语言模型】

2.【对于LSTM输入层、隐含层及输出层参数的个人理解】

3.【keras的主要模块介绍】

 

 

 

 

 

posted @ 2018-11-12 11:35  nxf_rabbit75  阅读(719)  评论(0编辑  收藏  举报