• RNN实现字符级语言模型


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


    在这里你将学习到:

    • 如何存储文本数据以便使用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
    Aardonyx
    Abdallahsaurus...'
    chars=['o', 'm', 'k', 'v', 'w', 'b', 'j', 'd', 'x', 'a', 'h', 'i',
    'e', 'l', 's', 't', 'n', 'z', 'p', 'y', 'g', 'f', ' ', '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)
     {'
    ': 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: ' ', 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[" "] 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[" "]) 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["
    "]]
     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["
    "]]
    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()生成“
    ”字符,检查模型是否学习正确
    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("
    ")
    68     return parameters

     

    比如说某恐龙名字叫 zzh

    那么X = ['0','z','z','h']

    Y = ['z','z','h',' ']

     

    需要注意的是我们使用了

    index= j % len(examples),

    其中= 1....num_iterations,

    为了确保examples[index]总是有效的

    (index小于len(examples)),

    rnn_forward()会将X的第一个值None解释为

    x<0>=0向量.

    此外,为了确保Y等于X,会向左移动一步,

    并添加一个附加的“ ”以表示恐龙名称的结束。

     
     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', ' '] 共计有19909个字符,唯一字符有27个
     
    kl_name = open('dinos.txt','r').read().lower()
    # kl_name =kl_name.lower().split('
    ')
    # len(kl_name.split('
    '))
    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)
     
    {'
    ': 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: ' ', 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的主要模块介绍】

     

     

     

     

     

  • 相关阅读:
    VS2012快捷操作功能
    SQL Union和SQL Union All用法
    安装sql server 2008 R2出现 创建usersettings/microsoft.sqlserver.configuration.landingpage.properties.setter
    编码的来历和使用 utf-8 和GB2312比较
    Java内存机制,内存地址
    spring
    使用CMD建立指定格式的文件
    在命令提示符窗口下(cmd)使用指令操作并编译java代码,运行java编译代码
    匿名内部类的使用总结
    网页中插入FLASH(swf文件)的html代码
  • 原文地址:https://www.cnblogs.com/nxf-rabbit75/p/9945549.html
Copyright © 2020-2023  润新知