Python手撸机器学习系列(十六):循环神经网络RNN的实现

目录

  • 循环神经网络RNN
    • 1.公式推导
    • 2.代码实现

循环神经网络RNN

1.公式推导

Python手撸机器学习系列(十六):循环神经网络RNN的实现_第1张图片

对于该循环神经网络,以中间的RNN单元为例,推导前向传播:

对于Layer-1:

z h = w i x + w h a h − 1 z^h = w^ix+w^ha^{h-1} zh=wix+whah1

a h = σ ( z h ) a^h = \sigma(z^h) ah=σ(zh)

这里 σ \sigma σ s i g m o i d ( ⋅ ) sigmoid(·) sigmoid()激活函数

对于Layer-2:

z o = w o a h z^o = w^oa^h zo=woah

p r e d = a o = σ ( z o ) pred = a^o=\sigma(z^o) pred=ao=σ(zo)

这里pred即RNN的输出

为了更新三个权值参数 w i , w h , w o w^i,w^h,w^o wi,wh,wo的值,需要求梯度进行反向传播:

首先确立损失函数为平方误差,加上 1 2 \frac{1}{2} 21是为了便于求导。
l o s s = 1 2 ( y − p r e d ) 2 loss = \frac12(y-pred)^2 loss=21(ypred)2
w o w^o wo进行求导:
∂ l o s s ∂ w o = ∂ l o s s ∂ p r e d × ∂ p r e d ∂ z o × ∂ z o ∂ w o = − ( y − p r e d ) × ( a o ) ( 1 − a o ) × ( a h ) \frac{\partial loss}{\partial w^o} = \frac{\partial loss}{\partial pred}\times \frac{\partial pred}{\partial z^o} \times \frac{\partial z^o}{\partial w^o} = -(y-pred)\times(a^o)(1-a^o)\times(a^h) woloss=predloss×zopred×wozo=(ypred)×(ao)(1ao)×(ah)
注意这里 s i g m o i d ( ) sigmoid() sigmoid()函数 f ( x ) f(x) f(x)的求导为 f ( x ) ( 1 − f ( x ) ) f(x)(1-f(x)) f(x)(1f(x))

再对 w h w^h wh进行求导:
∂ l o s s ∂ w h = ∂ l o s s n o w ∂ w h + ∂ l o s s n e x t ∂ w h \frac{\partial loss}{\partial w^h} = \frac{\partial loss^{now}}{\partial w^h}+ \frac{\partial loss^{next}}{\partial w^h} whloss=whlossnow+whlossnext
由于 w h w^h wh不仅影响到当前RNN单元的损失,还影响到下一个RNN单元的损失,所以梯度要分两部分算
∂ l o s s n o w ∂ w h = ∂ l o s s n o w ∂ p r e d × ∂ p r e d ∂ z o × ∂ z o ∂ a h × ∂ a h ∂ z h × ∂ z h ∂ w h = − ( y − p r e d ) × ( a o ) ( 1 − a o ) × w o a h ( 1 − a h ) × a h − 1 \frac{\partial loss^{now}}{\partial w^h} = \frac{\partial loss^{now}}{\partial pred}\times \frac{\partial pred}{\partial z^o} \times \frac{\partial z^o}{\partial a^h}\times\frac{\partial a^h}{\partial z^h}\times \frac{\partial z^h}{\partial w^h} \\= -(y-pred)\times(a^o)(1-a^o)\times w^oa^h(1-a^h)\times a^{h-1} whlossnow=predlossnow×zopred×ahzo×zhah×whzh=(ypred)×(ao)(1ao)×woah(1ah)×ah1

∂ l o s s n e x t ∂ w h = ∂ l o s s n e x t ∂ p r e d n e x t × ∂ p r e d n e x t ∂ z o + 1 × ∂ z o + 1 ∂ a h + 1 × ∂ a h + 1 ∂ z h + 1 × ∂ z h + 1 ∂ a h × ∂ a h ∂ z h × ∂ z h ∂ w h = w h δ ( n e x t ) × a h ( 1 − a h ) × a h − 1 \frac{\partial loss^{next}}{\partial w^h} = \frac{\partial loss^{next}}{\partial pred^{next}}\times \frac{\partial pred^{next}}{\partial z^{o+1}} \times \frac{\partial z^{o+1}}{\partial a^{h+1}}\times \frac{\partial a^{h+1}}{\partial z^{h+1}}\times\frac{\partial z^{h+1}}{\partial a^h} \times\frac{\partial a^h}{\partial z^h}\times \frac{\partial z^h}{\partial w^h}\\=w^h\delta(next)\times a^h(1-a^h)\times a^{h-1} whlossnext=prednextlossnext×zo+1prednext×ah+1zo+1×zh+1ah+1×ahzh+1×zhah×whzh=whδ(next)×ah(1ah)×ah1

为了简写公式,这里的 δ ( n e x t ) \delta(next) δ(next)指的是后面一个RNN单元对自己 z h + 1 z^{h+1} zh+1的求导过程,下面的 δ ( n o w ) \delta(now) δ(now)同理

两者相加即可得 w h w^h wh的梯度,对矩阵求导不自信的话,在推的时候可以拟定一下维度确定要不要转置矩阵(我的公式可能存在笔误,欢迎指出):
∂ l o s s ∂ w h = [ ( w h ) T δ ( n e x t ) + ( w o ) T δ ( n o w ) ] × a h ( 1 − a h ) × a h − 1 \frac{\partial loss}{\partial w^h} = \left[{(w^h)}^T\delta(next) + (w^o)^T\delta(now)\right]\times a^h(1-a^h)\times a^{h-1} whloss=[(wh)Tδ(next)+(wo)Tδ(now)]×ah(1ah)×ah1
最后求 w i w^i wi,连续偏导部分与 ∂ l o s s n o w ∂ w h \large\frac{\partial loss^{now}}{\partial w^h} whlossnow基本一致,最后一项改为 x x x
∂ l o s s ∂ w i = − ( y − p r e d ) × ( a o ) ( 1 − a o ) × w o a h ( 1 − a h ) × x T \frac{\partial loss}{\partial w^i} = -(y-pred)\times(a^o)(1-a^o)\times w^oa^h(1-a^h)\times x^T wiloss=(ypred)×(ao)(1ao)×woah(1ah)×xT

2.代码实现

我们的基本认为是给定两个八位二进制的数,让RNN预测两个相加的结果,输出八位二进制数,在两数相加中涉及到一个进位的问题,所以RNN学习的就是怎么在循环中进位。如下图所示,注意在该情况下,前向传播是从低位数开始的,所以是从八位二进制数的右边往左边走,而反向传播则是从左往右走。

Python手撸机器学习系列(十六):循环神经网络RNN的实现_第2张图片

根据之前的公式推理,要实现这个简单的RNN,我们需要做这些事:

  • 存储上一个RNN单元的 a h a^h ah值,即公式中的 a h − 1 a^{h-1} ah1
  • 存储未来的RNN单元求导至 z h z^h zh的值,即公式中的 δ ( n e x t ) \delta(next) δ(next)
  • 创建一些列表,存储每个单元的值,便于使用

我们发现,在公式中有很多重复的乘法计算,我们将其中的重复计算存储起来,就可以便于理解和代码实现,定义如下:

  • layer_2_deltas :表示输出层的误差,存储的是 ( y − p r e d ) × ( a o ) ( 1 − a o ) (y-pred)\times(a^o)(1-a^o) (ypred)×(ao)(1ao)这一段
  • layer_2_values:表示的输出层的值,即 p r e d pred pred
  • layer_1_values:表示的是第一层的值,即 a h a^h ah
  • future_layer_1_delta:表示的是下一个RNN单元求导至 z h + 1 z^{h+1} zh+1的值,即 δ ( n e x t ) \delta(next) δ(next)
  • layer_1_delta:表示的是当前RNN单元求导至 z h z^h zh的值,即 δ ( n e x t ) \delta(next) δ(next),我们将这个值存储起来作为上一个RNN反向传播时的future_layer_1_delta

你将在代码中看到这些变量,如果不明白这些变量的意思,随时回来看看

代码如下,参考自博客csdn博客,在他的基础上做了修改,并添加了非常详细的注释,相信大家都能看懂。

import copy
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0) #随机种子,固定的话每个人得到的结果都一致

# sigmoid函数
def sigmoid(x):
    output = 1 / (1 + np.exp(-x))
    return output

# sigmoid导数
def sigmoid_output_to_derivative(output):
    return output * (1 - output)


# 训练数据生成
int2binary = {}
binary_dim = 8

largest_number = pow(2, binary_dim) #2的8次方,共256个数
binary = np.unpackbits(
    np.array([range(largest_number)], dtype=np.uint8).T, axis=1)
# unpackbits函数可以把整数转化成2进制数
for i in range(largest_number):
    int2binary[i] = binary[i] #形成一个映射,将0-255的数映射到对应的二进制

# 初始化一些变量
alpha = 0.1 #学习率
input_dim = 2   #输入的大小,因为是2个数相加,所以是2
hidden_dim = 16  #隐含层的大小,代表记忆的维度,这里可以设置任意值
output_dim = 1  #输出层的大小,输出一个结果,所以是1
EPOCHS = 10000 #迭代次数,即共训练多少次
Eval = 100 #每迭代多少次验证一次

# 随机初始化权重
w_i = 2 * np.random.random((hidden_dim, input_dim)) - 1   #输入的权值矩阵,维度为(16, 2)
w_o = 2 * np.random.random((output_dim, hidden_dim)) - 1  #输出的权值矩阵,维度为(1, 16)
w_h = 2 * np.random.random((hidden_dim, hidden_dim)) - 1  #循环rnn的关键,隐藏层与隐藏层之间的权值矩阵,维度为(16, 16)

w_i_update = np.zeros_like(w_i) #w_i的梯度,维度(16, 2)
w_o_update = np.zeros_like(w_o) #w_o的梯度,维度(1, 16)
w_h_update = np.zeros_like(w_h) #w_h的梯度,维度(16, 16)

# 开始训练
error_num_list = []
for j in range(EPOCHS):
    #每次随机生成两个128以内的数进行相加(防止溢出),并将相加的结果作为标签
    # 二进制相加
    a_int = np.random.randint(largest_number / 2)  # 随机生成相加的数
    a = int2binary[a_int]  # 映射成二进制值

    b_int = np.random.randint(largest_number / 2)  # 随机生成相加的数
    b = int2binary[b_int]  # 映射成二进制值

    # 真实的答案
    label_int = a_int + b_int   #结果
    label = int2binary[label_int]   #映射成二进制值

    # 待存放预测值,这里我们要输出8位二进制,所以维度是8,即rnn输出8次
    prediction = np.zeros_like(label)

    overallError = 0 # rnn输出的8个值错了几个

    layer_2_deltas = list() #输出层的误差
    layer_2_values = list() #第二层的值(输出的结果)
    layer_1_values = list() #第一层的值(隐含状态)
    layer_1_values.append(copy.deepcopy(np.zeros((hidden_dim, 1)))) #第一个隐含状态需要0作为它的上一个隐含状态

    #前向传播
    for i in range(binary_dim):
        X = np.array([[a[binary_dim - i - 1], b[binary_dim - i - 1]]]).T    #将两个输入并起来变为矩阵,维度(2,1)
        y = np.array([[label[binary_dim - i - 1]]]).T   #将y也变为矩阵,维度(1,1)
        layer_1 = sigmoid(np.dot(w_h, layer_1_values[-1]) + np.dot(w_i, X)) #先算第一层,算到ah,维度(1,1)
        layer_1_values.append(copy.deepcopy(layer_1))   #将第一层的值存储起来,方便反向传播用
        layer_2 = sigmoid(np.dot(w_o, layer_1))   #算输出层的值,维度(1,1)
        #loss = 1/2(y-pred)^2 没有必要写了,直接写梯度就行
        error = -(y-layer_2)    #损失对pred求导,记得这里pred = layer_2
        layer_delta2 = error * sigmoid_output_to_derivative(layer_2)    # 这里是输出层求导至zo的那一段,(1,1)
        layer_2_deltas.append(copy.deepcopy(layer_delta2)) #存储起来反向传播用
        prediction[binary_dim - i - 1] = np.round(layer_2[0][0]) #预测值,[0][0]是为了把1*1矩阵变成数
    future_layer_1_delta = np.zeros((hidden_dim, 1)) #这个是未来一个RNN单元求导至zh的δ(next),存起来求wh用的
    #反向传播

    for i in range(binary_dim): #对于8位数,之前是从右往左前向传播,所以现在是从左往右反向传播
        X = np.array([[a[i], b[i]]]).T
        prev_layer_1 = layer_1_values[-i-2] #前一个RNN单元的值,因为包含初始化的0值层(第一个RNN单元的pre是0),所以-2
        layer_1 = layer_1_values[-i-1]  #当前的隐藏层值
        layer_delta2 = layer_2_deltas[-i-1] #将之前存着的输出层的误差求导取出来
        layer_delta1 = np.multiply(np.add(np.dot(w_h.T, future_layer_1_delta),np.dot(w_o.T, layer_delta2)), sigmoid_output_to_derivative(layer_1)) #根据当前的误差以及未来一层的误差求当前的wh
        w_i_update += np.dot(layer_delta1, X.T) #根据公式还要再乘上一项才是梯度
        w_h_update += np.dot(layer_delta1, prev_layer_1.T)
        w_o_update += np.dot(layer_delta2, layer_1.T)
        future_layer_1_delta = layer_delta1
    w_i -= alpha * w_i_update
    w_h -= alpha * w_h_update
    w_o -= alpha * w_o_update
    w_i_update *= 0
    w_o_update *= 0
    w_h_update *= 0
    # 验证结果

    if (j % Eval == 0): #每100次验证一次
        overallError = sum([1 if prediction[i] != label[i] else 0 for i in range(binary_dim)])#错了几个
        error_num_list.append(overallError)
        print("Error:" + str(overallError))
        print("Pred:" + str(prediction))
        print("True:" + str(label))
        out = 0
        for index, x in enumerate(reversed(prediction)): #二进制还原为10进制
            out += x * pow(2, index)
        print(str(a_int) + " + " + str(b_int) + " = " + str(out))
        print("------------")

plt.plot(np.arange(len(error_num_list))*Eval, error_num_list)
plt.ylabel('Error numbers')
plt.xlabel('Epochs')
plt.show()

得到的部分结果:
Python手撸机器学习系列(十六):循环神经网络RNN的实现_第3张图片
预测error的下降趋势,这里表示的是8位二进制数中错误的个数

Python手撸机器学习系列(十六):循环神经网络RNN的实现_第4张图片

你可能感兴趣的:(机器学习,rnn,机器学习,python)