吴恩达深度学习专项课程的所有实验均采用iPython Notebooks实现,不熟悉的朋友可以提前使用一下Notebooks。
本次实验中我们将使用NumPy编写一个RNN模型,来构建一个字符级的语言模型,来生成恐龙的名字。
目录
1.实验综述
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.结论