深度学习 | 吴恩达序列模型专项课程第一周编程作业实验2 --- DinosaurNameGenerator

吴恩达深度学习专项课程的所有实验均采用iPython Notebooks实现,不熟悉的朋友可以提前使用一下Notebooks。

本次实验中我们将使用NumPy编写一个RNN模型,来构建一个字符级的语言模型,来生成恐龙的名字。

完整代码(中文翻译版带答案)

目录

1.实验综述

2.导入必要的包

3. 数据集和模型结构

4.模型的构建块

5.构建语言模型

6.结论


1.实验综述

扫描二维码关注公众号,回复: 5406767 查看本文章

2.导入必要的包

import numpy as np
import random
#utils.py中定义了本次实验所需要的辅助函数
#包括朴素RNN的前向/反向传播  和我们在上一个实验中实现的差不多
from utils import *

3. 数据集和模型结构

  • 数据集和预处理

查看dinos.txt:

每一行包含一个恐龙的名字。 

data = open('dinos.txt', 'r').read() #读取dinos.txt中的所有恐龙名字 read()逐字符读取 返回一个字符串
data= data.lower()#把所有名字转为小写
chars = list(set(data))#得到字符列表并去重  
print(chars)  #'a'-'z' '\n'  27个字符    
data_size, vocab_size = len(data), len(chars) 
print('There are %d total characters and %d unique characters in your data.' % (data_size, vocab_size))

  • 模型结构

4.模型的构建块

  • 梯度剪切

### GRADED FUNCTION: clip

def clip(gradients, maxValue):
    '''
    把每个梯度值剪切到 minimum 和 maximum之间.
    
    Arguments:
    gradients -- Python梯度字典 包含 "dWaa", "dWax", "dWya", "db", "dby"
    maxValue -- 每个大于maxValue或小于-maxValue的梯度值 被设置为该值 
    
    Returns: 
    gradients -- Python梯度字典 包含剪切后的切度
    '''
    
    #取出梯度字典中存储的梯度
    dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby']
   
   
    # 对每个梯度[dWax, dWaa, dWya, db, dby]进行剪切
    for gradient in [dWax, dWaa, dWya, db, dby]:
        #gradient[gradient>maxValue] = maxValue
        #gradient[gradient<-maxValue] = -maxValue
        np.clip(gradient,-maxValue,maxValue,out=gradient)
    
    
    gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby}
    
    return gradients
  • 采样

# GRADED FUNCTION: sample

def sample(parameters, char_to_ix, seed):
    """
    根据朴素RNN输出的概率分布对字符序列进行采样
    
    Arguments:
    parameters --Python字典 包含模型参数 Waa, Wax, Wya, by, and b. 
    char_to_ix -- Python字典 把每个字符映射为索引
    seed -- .

    Returns:
    indices -- 包含采样字符索引的列表.
    """
    
    # 得到模型参数 和相关维度信息
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    vocab_size = by.shape[0] #字典大小  输出单元的数量
    n_a = Waa.shape[1] #隐藏单元数量
    

    # Step 1: 创建第一个时间步骤上输入的初始向量 初始化序列生成
    x = np.zeros((vocab_size,1))
    # Step 1': 初始化a_prev 
    a_prev = np.zeros((n_a,1))
    
    # 保存生成字符index的列表
    indices = []
    
    # 检测换行符, 初始化为 -1
    idx = -1 
    
    # 在每个时间步骤上进行循环.在每个时间步骤输出的概率分布上采样一个字符 
    # 把采样字典的index添加到indices中. 如果达到50个字符就停止 (说明模型训练有点问题)
    #  用于终止无限循环   模型如果训练的不错的话  在遇到换行符之前不会达到50个字符
    counter = 0
    newline_character = char_to_ix['\n'] #换行符索引
    
    while (idx != newline_character and counter != 50): #如果生成的字符不是换行符且循环次数小于50  就继续
        
        # Step 2: 对x进行前向传播   公式(1), (2) and (3)
        a = np.tanh(Wax.dot(x) + Waa.dot(a_prev) + b) #(n_a,1)
        z = Wya.dot(a) + by #(n_y,1)
        y = softmax(z) #(n_y,1)
        
   
        np.random.seed(counter+seed) 
        
        # Step 3:从输出的概率分布y中 采样一个字典中的字符索引
        idx = np.random.choice(range(vocab_size),p = y.ravel())
        indices.append(idx)
        
        
        # Step 4: 根据采样的索引 得到对应字符的one-hot形式 重写输入x
        x = np.zeros((vocab_size,1))
        x[idx] = 1 
        
        # 更新a_prev
        a_prev = a
        
        seed += 1
        counter +=1
        

    if (counter == 50):
        indices.append(char_to_ix['\n'])
    
    return indices

5.构建语言模型

现在我们要构建一个字符级的语言模型进行文本生成。

  • 梯度下降

查看头文件中的上述函数:

def rnn_step_forward(parameters, a_prev, x):
    '''朴素RNN单元的前行传播'''
    #从参数字典中取出参数
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    #计算当前时间步骤上的隐藏状态
    a_next = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b) 
    #计算当前时间步骤上的预测输出  通过一个输出层(使用softmax激活函数,多分类 ,类别数为字典大小)
    p_t = softmax(np.dot(Wya, a_next) + by) 
    
    return a_next, p_t

def rnn_step_backward(dy, gradients, parameters, x, a, a_prev):
    '''朴素RNN单元的反向传播'''
    gradients['dWya'] += np.dot(dy, a.T)
    gradients['dby'] += dy
    da = np.dot(parameters['Wya'].T, dy) + gradients['da_next'] # backprop into h
    daraw = (1 - a * a) * da # backprop through tanh nonlinearity
    gradients['db'] += daraw
    gradients['dWax'] += np.dot(daraw, x.T)
    gradients['dWaa'] += np.dot(daraw, a_prev.T)
    gradients['da_next'] = np.dot(parameters['Waa'].T, daraw)
    return gradients

def update_parameters(parameters, gradients, lr):

    '''
    使用随机梯度下降法更新模型参数
    parameters:模型参数字典
    gradients:对模型参数计算的梯度
    lr:学习率
    '''
    parameters['Wax'] += -lr * gradients['dWax']
    parameters['Waa'] += -lr * gradients['dWaa']
    parameters['Wya'] += -lr * gradients['dWya']
    parameters['b']  += -lr * gradients['db']
    parameters['by']  += -lr * gradients['dby']
    return parameters

def rnn_forward(X, Y, a0, parameters, vocab_size = 27):
    '''朴素RNN的前行传播
    和上一个实验实验的RNN有所不同,之前我们一次处理m个样本/序列 要求m个序列有相同的长度
    本次实验的RNN,一次只处理一个样本/序列(名字单词) 所以不用统一长度。
     X -- 整数列表,每个数字代表一个字符的索引。 X是一个训练样本 代表一个单词
     Y -- 整数列表,每个数字代表一个字符的索引。 Y是一个训练样本对应的真实标签 为X中的索引左移一位 
    '''
    
    # Initialize x, a and y_hat as empty dictionaries
    x, a, y_hat = {}, {}, {}
    
    a[-1] = np.copy(a0)
    
    # initialize your loss to 0
    loss = 0
    
    for t in range(len(X)): 
        
        # 设置x[t]为one-hot向量形式.
        # 如果 X[t] == None, 设置 x[t]=0向量. 设置第一个时间步骤的输入为0向量
        x[t] = np.zeros((vocab_size,1))  #设置每个时间步骤的输入向量
        if (X[t] != None):
            x[t][X[t]] = 1 #one-hot形式 索引位置为1 其余为0
        
        # 运行一步RNN前向传播
        a[t], y_hat[t] = rnn_step_forward(parameters, a[t-1], x[t])
        #得到当前时间步骤的隐藏状态和预测输出
        
        # 把预测输出和真实标签结合 计算交叉熵损失
        loss -= np.log(y_hat[t][Y[t],0])
        
    cache = (y_hat, a, x)
        
    return loss, cache

def rnn_backward(X, Y, parameters, cache):
    '''朴素RNN的反向传播'''
    # Initialize gradients as an empty dictionary
    gradients = {}
    
    # Retrieve from cache and parameters
    (y_hat, a, x) = cache
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    
    # each one should be initialized to zeros of the same dimension as its corresponding parameter
    gradients['dWax'], gradients['dWaa'], gradients['dWya'] = np.zeros_like(Wax), np.zeros_like(Waa), np.zeros_like(Wya)
    gradients['db'], gradients['dby'] = np.zeros_like(b), np.zeros_like(by)
    gradients['da_next'] = np.zeros_like(a[0])
    
    ### START CODE HERE ###
    # Backpropagate through time
    for t in reversed(range(len(X))):
        dy = np.copy(y_hat[t])
        dy[Y[t]] -= 1
        gradients = rnn_step_backward(dy, gradients, parameters, x[t], a[t], a[t-1])
    ### END CODE HERE ###
    
    return gradients, a

利用上述函数实现优化过程(随机梯度下降的一步迭代):

# GRADED FUNCTION: optimize

def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
    """
    执行一步优化过程(随机梯度下降,一次优化使用一个训练训练).
    
    Arguments:
    X -- 整数列表,每个数字代表一个字符的索引。 X是一个训练样本 代表一个单词
    Y -- 整数列表,每个数字代表一个字符的索引。 Y是一个训练样本对应的真实标签 为X中的索引左移一位 
    a_prev -- 上一个时间步骤产生的隐藏状态
    parameters -- Python字典包含:
                        Wax -- 与输入相乘的权重矩阵, 维度 (n_a, n_x)
                        Waa -- 与之前隐藏状态相乘的权重矩阵, 维度 (n_a, n_a)
                        Wya -- 与当前隐藏状态相乘用于产生输出的权重矩阵, 维度 (n_y, n_a)
                        ba --  计算当前隐藏状态的偏置参数  维度 (n_a, 1)
                        by --  计算当前输出的偏置参数  维度 (n_y, 1)

    learning_rate -- 学习率
    
    Returns:
    loss -- loss函数值(交叉熵)
    gradients -- python dictionary containing:
                        dWax -- Gradients of input-to-hidden weights, of shape (n_a, n_x)
                        dWaa -- Gradients of hidden-to-hidden weights, of shape (n_a, n_a)
                        dWya -- Gradients of hidden-to-output weights, of shape (n_y, n_a)
                        db -- Gradients of bias vector, of shape (n_a, 1)
                        dby -- Gradients of output bias vector, of shape (n_y, 1)
    a[len(X)-1] -- 最后一个隐藏状态 (n_a, 1)
    """
    
    
    # 通过时间前向传播
    loss, cache = rnn_forward(X,Y,a_prev,parameters,vocab_size=27)
    
    # 通过时间的反向传播 
    gradients, a = rnn_backward(X,Y,parameters,cache)
    
    # 梯度剪切 -5 (min)  5 (max) 
    gradients = clip(gradients,maxValue=5)
    
    # 更新参数
    parameters = update_parameters(parameters,gradients,lr=learning_rate)
    
    
    return loss, gradients, a[len(X)-1]
  • 训练模型

# GRADED FUNCTION: model

def model(data, ix_to_char, char_to_ix, num_iterations = 35000, n_a = 50, dino_names = 7, vocab_size = 27):
    """
    训练模型生成恐龙名字. 
    
    Arguments:
    data -- 文本语料(恐龙名字数据集)
    ix_to_char -- 从索引到字符的映射字典
    char_to_ix -- 从字符到索引的映射字典
    num_iterations -- 随机梯度下降的迭代次数  每次使用一个训练样本(一个名字)
    n_a -- RNN单元中的隐藏单元数
    dino_names -- 采样的恐龙名字数量
    vocab_size -- 字典的大小  文本语料中不同的字符数
    
    Returns:
    parameters --  训练好的参数
    """
    
    # 输入特征向量x的维度n_x, 输出预测概率向量的维度n_y  2者都为字典大小
    n_x, n_y = vocab_size, vocab_size
    
    # 初始化参数
    parameters = initialize_parameters(n_a, n_x, n_y)
    
    # 初始化loss (this is required because we want to smooth our loss, don't worry about it)
    loss = get_initial_loss(vocab_size, dino_names)
    
    # 得到所有恐龙名字的列表 (所有训练样本).
    with open("dinos.txt") as f:
        examples = f.readlines() #读取所有行 每行是一个名字 作为列表的一个元素
    examples = [x.lower().strip() for x in examples] #转换小写 去掉换行符
    
    # 随机打乱所有恐龙名字 所有训练样本
    np.random.seed(0)
    np.random.shuffle(examples)
    
    # 初始化隐藏状态为0
    a_prev = np.zeros((n_a, 1))
    
    # 优化循环
    for j in range(num_iterations):
        
        
        # 得到一个训练样本 (X,Y) 
        index = j%len(examples)   #得到随机打乱后的一个名字的索引
        X =  [None] + [char_to_ix[ch] for ch in examples[index]]#把名字中的每个字符转为对应的索引 第一个字符为None翻译为0向量
        Y =  X[1:] + [char_to_ix['\n']]
        
        # 随机梯度下降 执行一次优化: Forward-prop -> Backward-prop -> Clip -> Update parameters
        # 学习率 0.01
        curr_loss, gradients, a_prev = optimize(X,Y,a_prev,parameters,learning_rate=0.01)
        

        
        # 使用延迟技巧保持loss平稳. 加速训练
        loss = smooth(loss, curr_loss)

        # 每2000次随机梯度下降迭代, 通过sample()生成'n'个字符(1个名字)  来检查模型是否训练正确
        if j % 2000 == 0:
            
            print('Iteration: %d, Loss: %f' % (j, loss) + '\n')
            
            
            seed = 0
            for name in range(dino_names):#生成名字的数量
                
                # 得到采样字符的索引
                sampled_indices = sample(parameters, char_to_ix, seed)
                #得到索引对应的字符 生成一个名字
                print_sample(sampled_indices, ix_to_char)
                
                seed += 1  # To get the same result for grading purposed, increment the seed by one. 
      
            print('\n')
        
    return parameters

查看头文件中上述函数用到的一些辅助函数:

def softmax(x):
    ''''softmax激活函数'''
    e_x = np.exp(x - np.max(x)) #首先对输入做一个平移 减去最大值 使其最大值为0 再取exp 避免指数爆炸
    return e_x / e_x.sum(axis=0) 

def smooth(loss, cur_loss):
    return loss * 0.999 + cur_loss * 0.001

def print_sample(sample_ix, ix_to_char):
    '''
    得到采样的索引对应的字符
    sample_ix:采样字符的索引
    ix_to_char:索引到字符的映射字典
    '''
    txt = ''.join(ix_to_char[ix] for ix in sample_ix)#连接成字符串
    txt = txt[0].upper() + txt[1:]  # 首字母大写
    print ('%s' % (txt, ), end='')

def get_initial_loss(vocab_size, seq_length):
    return -np.log(1.0/vocab_size)*seq_length

def initialize_parameters(n_a, n_x, n_y):
    """
    用小随机数初始化模型参数
    
    Returns:
    parameters -- Python字典包含:
                        Wax -- 与输入相乘的权重矩阵, 维度 (n_a, n_x)
                        Waa -- 与之前隐藏状态相乘的权重矩阵, 维度 (n_a, n_a)
                        Wya -- 与当前隐藏状态相乘用于产生输出的权重矩阵, 维度(n_y,n_a)
                        ba --  计算当前隐藏状态的偏置参数  维度 (n_a, 1)
                        by --  计算当前输出的偏置参数  维度 (n_y, 1)

    """
    np.random.seed(1)
    Wax = np.random.randn(n_a, n_x)*0.01
    Waa = np.random.randn(n_a, n_a)*0.01 
    Wya = np.random.randn(n_y, n_a)*0.01 
    b = np.zeros((n_a, 1)) 
    by = np.zeros((n_y, 1)) 
    
    parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b,"by": by}
    
    return parameters

训练模型:

parameters = model(data, ix_to_char, char_to_ix) #训练模型

在第1次随机梯度下降得到后,你的模型会输出dino_names个名字,每个名字很像随机字符, 经过几千次迭代,模型生成合理的名字。

6.结论

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/87876169
今日推荐