文章目录
- 前言
- RNN
- RNN架构图
- 前向传播公式
- 反向传播算法
- 用RNN实现退位减法
- 代码
- 变量的对应关系
- 总结
前言
最近深入学习了一下RNN,即循环神经网络。RNN是一类比较基础的神经网络,本文使用的是最基础、最简单的循环神经网络的形式。LSTM也是一种常见的循环神经网络,但本文为了降低理解难度,将不会涉及。
RNN
RNN是循环神经网络,是神经网络的一种。它跟普通神经网络有什么区别呢?下图表示了一个普通神经网络。
x
x
x表示输入层,
h
h
h表示隐藏层,
O
O
O表示输出层,
L
L
L表示损失函数,
y
y
y表示真实输出。
下图表示了一个循环神经网络。在隐藏层
h
h
h这里,多了一个指向自己的箭头。
这里的
x
x
x表示一个向量,一串带有顺序的信息。在普通的神经网络中,没有办法体现出前一个输入信息对下一个输出的影响,而这样一个循环的方式就可以体现前一个输入信息对下一个输出的影响。
RNN架构图
这里给出更为详细的RNN架构图。由于本人还不太会使用画图软件,这些参数没法以数学公式的形式展示,大家将就着看吧。
x
t
x_t
xt代表输入层。
h
t
h_t
ht代表隐藏层。
y
t
y_t
yt代表输出层。
W
h
x
W_{hx}
Whx是从输入层到隐藏层的参数。
W
h
h
W_{hh}
Whh是从隐藏层到隐藏层的参数,正因为它的存在,才使得RNN具有记忆,能够保存上一次输入的信息。
W
y
h
W_{yh}
Wyh是隐藏层到输出层的参数。
前向传播公式
{ h t = f ( x t ⋅ W h x + h t − 1 ⋅ W h h ) y t = g ( h t ⋅ W y h ) = g ( f ( x t ⋅ W h x + h t − 1 ⋅ W h h ) ⋅ W y h ) \begin{cases} h_t=f(x_t\cdot W_{hx}+h_{t-1}\cdot W_{hh})\\ y_t=g(h_t\cdot W_{yh})=g(f(x_t\cdot W_{hx}+h_{t-1}\cdot W_{hh})\cdot W_{yh}) \end{cases} {ht=f(xt⋅Whx+ht−1⋅Whh)yt=g(ht⋅Wyh)=g(f(xt⋅Whx+ht−1⋅Whh)⋅Wyh)
反向传播算法
用 E E E表示损失函数,我们要推导误差与参数改变量之间的关系,即 Δ W h x \Delta W_{hx} ΔWhx、 Δ W h h \Delta W_{hh} ΔWhh、 Δ W y h \Delta W_{yh} ΔWyh与 E E E之间的关系。
设 v t = h t ⋅ W y h , u t = x t ⋅ W h x + h t − 1 ⋅ W h h v_t=h_t\cdot W_{yh},u_t=x_t\cdot W_{hx}+h_{t-1}\cdot W_{hh} vt=ht⋅Wyh,ut=xt⋅Whx+ht−1⋅Whh
设 δ t y = ∂ E ∂ v t , δ t h = ∂ E ∂ u t \delta_t^y=\frac{\partial E}{\partial v_t},\delta_t^h=\frac{\partial E}{\partial u_t} δty=∂vt∂E,δth=∂ut∂E
为什么这么设呢?其实 δ t y \delta_t^y δty代表了从输出层到隐藏层的反向传播, δ t h \delta_t^h δth代表了从隐藏层到输入层的反向传播,这么设会让后面的公式变得更为简洁。
在本文中,取 E = 1 2 ∑ t = 1 n ( y t − y ^ t ) 2 E=\frac{1}{2}\sum_{t=1}^n(y_t-\widehat{y}_t)^2 E=21∑t=1n(yt−y t)2,其中 y ^ t \widehat{y}_t y t代表第 t t t位的真实值。
δ t y = ∂ E ∂ y t ⋅ ∂ y t ∂ v t = ( y t − y ^ t ) ⋅ g ′ ( v t ) \delta_t^y=\frac{\partial E}{\partial y_t}\cdot\frac{\partial y_t}{\partial v_t}=(y_t-\widehat{y}_t)\cdot g'(v_t) δty=∂yt∂E⋅∂vt∂yt=(yt−y t)⋅g′(vt)
δ t h = ∂ E t ∂ u t + ∂ E t + 1 ∂ u t = ∂ E t ∂ v t ⋅ ∂ v t ∂ h t ⋅ ∂ h t ∂ u t + ∂ E t + 1 ∂ u t + 1 ⋅ ∂ u t + 1 ∂ h t ⋅ ∂ h t ∂ u t = ( δ t y ⋅ W y h T + δ t + 1 h ⋅ W h h T ) ⋅ f ′ ( h t ) \delta_t^h=\frac{\partial E_t}{\partial u_t}+\frac{\partial E_{t+1}}{\partial u_t}=\frac{\partial E_t}{\partial v_t}\cdot \frac{\partial v_t}{\partial h_t}\cdot \frac{\partial h_t}{\partial u_t}+\frac{\partial E_{t+1}}{\partial u_{t+1}}\cdot \frac{\partial u_{t+1}}{\partial h_t}\cdot \frac{\partial h_t}{\partial u_t}=(\delta_t^y\cdot W_{yh}^T+\delta_{t+1}^h\cdot W_{hh}^T)\cdot f'(h_t) δth=∂ut∂Et+∂ut∂Et+1=∂vt∂Et⋅∂ht∂vt⋅∂ut∂ht+∂ut+1∂Et+1⋅∂ht∂ut+1⋅∂ut∂ht=(δty⋅WyhT+δt+1h⋅WhhT)⋅f′(ht)
∂ E ∂ W h x = ∑ t = 1 n ∂ E ∂ u t ⋅ ∂ u t ∂ W h x = ∑ t = 1 n x t T ⋅ δ t h \frac{\partial E}{\partial W_{hx}}=\sum_{t=1}^n \frac{\partial E}{\partial u_t}\cdot \frac{\partial u_t}{\partial W_{hx}}=\sum_{t=1}^n x_t^T\cdot \delta_t^h ∂Whx∂E=∑t=1n∂ut∂E⋅∂Whx∂ut=∑t=1nxtT⋅δth
∂ E ∂ W h h = ∑ t = 1 n ∂ E ∂ u t ⋅ ∂ u t ∂ W h h = ∑ t = 1 n h t − 1 T ⋅ δ t h \frac{\partial E}{\partial W_{hh}}=\sum_{t=1}^n \frac{\partial E}{\partial u_t}\cdot \frac{\partial u_t}{\partial W_{hh}}=\sum_{t=1}^n h_{t-1}^T\cdot \delta_t^h ∂Whh∂E=∑t=1n∂ut∂E⋅∂Whh∂ut=∑t=1nht−1T⋅δth
∂ E ∂ W y x = ∑ t = 1 n ∂ E ∂ v t ⋅ ∂ v t ∂ W y h = ∑ t = 1 n h t T ⋅ δ t y \frac{\partial E}{\partial W_{yx}}=\sum_{t=1}^n \frac{\partial E}{\partial v_t}\cdot \frac{\partial v_t}{\partial W_{yh}}=\sum_{t=1}^n h_t^T\cdot \delta_t^y ∂Wyx∂E=∑t=1n∂vt∂E⋅∂Wyh∂vt=∑t=1nhtT⋅δty
Δ W h x = η ∑ t = 1 n δ t h ⋅ x t \Delta W_{hx}=\eta\sum_{t=1}^n\delta_t^h\cdot x_t ΔWhx=η∑t=1nδth⋅xt
Δ W h h = η ∑ t = 1 n δ t h ⋅ h t − 1 \Delta W_{hh}=\eta\sum_{t=1}^n\delta_t^h\cdot h_{t-1} ΔWhh=η∑t=1nδth⋅ht−1
Δ W y h = η ∑ t = 1 n δ t y ⋅ h t \Delta W_{yh}=\eta\sum_{t=1}^n\delta_t^y\cdot h_t ΔWyh=η∑t=1nδty⋅ht
至此,反向传播公式推导完毕。
用RNN实现退位减法
RNN最适合用来处理输入中包含顺序的这种问题。像减法,两个数相减之后,会产生一个退位,这个退位的信息就可以包含在循环中,传递给下一个值。
在计算退位减法中,取 f ( ⋅ ) = g ( ⋅ ) = s i g m o i d ( ⋅ ) f(\cdot)=g(\cdot)=sigmoid(\cdot) f(⋅)=g(⋅)=sigmoid(⋅)
x
t
x_t
xt代表输入层,维度为2,每次输入为减数和被减数的第
t
t
t位。
h
t
h_t
ht代表隐藏层,设定维度为16维,通过各种参数,学习到减法的含义。
y
t
y_t
yt代表输出层,维度为1,代表预测减法的第
t
t
t位结果。
代码
接下来直接给出代码。代码中有非常详细的解释,相信也能帮助大家更好地理解RNN。
该代码来自《PyTorch从深度学习到图神经网络》配套代码。其中有我个人的小范围修改。
import copy, numpy as np
## np.random.seed(0) # 随机数生成器的种子,可以每次得到一样的值
# compute sigmoid nonlinearity
def sigmoid(x): # 激活函数,这里的x可以是一个数,也可以是向量或者矩阵
output = 1 / (1 + np.exp(-x))
return output
def sigmoid_output_to_derivative(output): # 激活函数的导数
return output * (1 - output)
int2binary = {} # 整数到其二进制表示的映射
binary_dim = 8 # 暂时只做256(2^8)以内的减法
## 计算0-256的二进制表示
largest_number = pow(2, binary_dim)
binary = np.unpackbits(
np.array([range(largest_number)], dtype=np.uint8).T, axis=1) # uint8表示无符号8位整数,T表示转置,axis=1表示按列解包
## binary是一个256行8列的矩阵,表示了0~255的二进制表示
for i in range(largest_number):
int2binary[i] = binary[i]
# input variables
alpha = 0.9 # 学习速率
input_dim = 2 # 输入的维度是2
hidden_dim = 16
output_dim = 1 # 输出维度为1
# initialize neural network weights
synapse_0 = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05 # 维度为2*16, 2是输入维度,16是隐藏层维度
synapse_1 = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
synapse_h = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05
# => [-0.05, 0.05),
# 用于存放反向传播的权重更新值
synapse_0_update = np.zeros_like(synapse_0) ##zeros_like函数的功能是生成一个同样大小的全零矩阵
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)
# training
for j in range(10000): # 训练10000轮
# 生成一个数字a
a_int = np.random.randint(largest_number)
# 生成一个数字b,b的最大值取的是largest_number/2,作为被减数,让它小一点。
b_int = np.random.randint(largest_number / 2)
# 如果生成的b大了,那么交换一下
if a_int < b_int:
tt = a_int
a_int = b_int
b_int = tt
a = int2binary[a_int] # binary encoding
b = int2binary[b_int] # binary encoding
# true answer
c_int = a_int - b_int
c = int2binary[c_int]
# 存储神经网络的预测值
d = np.zeros_like(c)
overallError = 0 # 每次把总误差清零
layer_2_deltas = list() # 存储每个时间点输出层的误差
layer_1_values = list() # 存储每个时间点隐藏层的值
layer_1_values.append(np.ones(hidden_dim) * 0.1) # 一开始没有隐藏层,所以初始化一下原始值为0.1
# moving along the positions in the binary encoding
for position in range(binary_dim): # 循环遍历每一个二进制位,从低位到高位
# generate input and output
X = np.array([[a[binary_dim - position - 1], b[binary_dim - position - 1]]]) # 从右到左,每次将两个数的当前位作为输入
y = np.array([[c[binary_dim - position - 1]]]).T # 正确答案
# hidden layer (input ~+ prev_hidden)
layer_1 = sigmoid(np.dot(X, synapse_0) + np.dot(layer_1_values[-1],
synapse_h)) # (输入层 + 之前的隐藏层) -> 新的隐藏层,这是体现循环神经网络的最核心的地方!!!
# output layer (new binary representation)
layer_2 = sigmoid(np.dot(layer_1, synapse_1)) # 隐藏层 * 隐藏层到输出层的转化矩阵synapse_1 -> 输出层
layer_2_error = y - layer_2 # 预测误差
layer_2_deltas.append((layer_2_error) * sigmoid_output_to_derivative(layer_2)) # 把每一个时间点的误差导数都记录下来
overallError += np.abs(layer_2_error[0]) # 总误差
d[binary_dim - position - 1] = np.round(layer_2[0][0]) # 记录下每一个预测bit位
# store hidden layer so we can use it in the next timestep
layer_1_values.append(copy.deepcopy(layer_1)) # 记录下隐藏层的值,在下一个时间点用
future_layer_1_delta = np.zeros(hidden_dim)
# 反向传播,从最后一个时间点到第一个时间点
for position in range(binary_dim):
X = np.array([[a[position], b[position]]]) # 最后一次的两个输入
layer_1 = layer_1_values[-position - 1] # 当前时间点的隐藏层
prev_layer_1 = layer_1_values[-position - 2] # 前一个时间点的隐藏层
# error at output layer
layer_2_delta = layer_2_deltas[-position - 1] # 当前时间点输出层导数
# error at hidden layer
# 通过后一个时间点(因为是反向传播)的隐藏层误差和当前时间点的输出层误差,计算当前时间点的隐藏层误差
layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(
synapse_1.T)) * sigmoid_output_to_derivative(layer_1)
# 等到完成了所有反向传播误差计算, 才会更新权重矩阵,先暂时把更新矩阵存起来。
synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
synapse_0_update += X.T.dot(layer_1_delta)
future_layer_1_delta = layer_1_delta
# 完成所有反向传播之后,更新权重矩阵。并把矩阵变量清零
synapse_0 += synapse_0_update * alpha
synapse_1 += synapse_1_update * alpha
synapse_h += synapse_h_update * alpha
synapse_0_update *= 0 # 这是一个好的清零数组的方式,值得学习
synapse_1_update *= 0
synapse_h_update *= 0
# print out progress
if (j % 800 == 0): # 每训练800次,输出一次结果,让人能够看到训练的情况
# print(synapse_0,synapse_h,synapse_1)
print("总误差:" + str(overallError))
print("Pred:" + str(d))
print("True:" + str(c))
out = 0
for index, x in enumerate(reversed(d)):
out += x * pow(2, index)
print(str(a_int) + " - " + str(b_int) + " = " + str(out))
print("------------")
变量的对应关系
代码中有很多变量,名称跟我们的RNN架构图并不完全对应。在这里给出一些关键变量与RNN架构图的对应关系。
代码中的变量 | 架构图中的变量 |
---|---|
layer_1 | h t h_t ht |
layer_2 | y t y_t yt |
layer_2_error | y ^ t − y t \widehat{y}_t-y_t y t−yt |
layer_2_deltas | δ y \delta^y δy |
layer_1_values | h h h |
prev_layer_1 | h t − 1 h_{t-1} ht−1 |
layer_2_delta | δ t y \delta_t^y δty |
layer_1_delta | δ t h \delta_t^h δth |
future_layer_1_delta | δ t + 1 h \delta_{t+1}^h δt+1h |
总结
本文先介绍了RNN,并指出了其相比普通神经网络的优势。然后给出了RNN的前向传播公式,并推导了反向传播算法。最后给出了用RNN实现退位减法的代码。
RNN也是从普通神经网络迈向人工智能的一大步。通过RNN,我们可以让代码“理解”减法,或者至少看上去对减法有了深刻的理解。通过越来越多的训练,它可以“理解”更多东西,从而让人感觉它产生了智能。