基于Numpy构建RNN模块并进行实例应用(附代码)

简介: 基于Numpy构建RNN模块并进行实例应用(附代码)

一、写在前面

按照国际惯例,最先是免责声明:本文只是我自己学习循环神经网络RNN的理解,内容不乏不准确甚至错误的地方,希望批评指正,共同进步。


为什么写这个博客?

在一年前,写完这篇博客-基于Pytorch的机器学习Regression问题实例(附源码)最后留了一个尾巴:就是使用简单的全连接网络的时候,即使网络做的足够深,在“预测”上的表现仍然很差。


在相继学过CNN、GAN之后,看到RNN的介绍,发现这是最有希望解决“预测”问题的神经元网络模型。


二、RNN原理介绍说明

对于任何学习RNN的新手,与其上来就看一堆介绍文章,看着可能作者都似懂非懂的原理图,不如都先看看B站这个视频【循环神经网络】5分钟搞懂RNN,3D动画深入浅出


对RNN一个比较好的直观理解是:“CNN模型可以理解为人类的视觉中枢,对于单张图片来说,CNN可以很好地理解其内容。但是无论是人类的视觉神经还是听觉神经,所接受到的都是一个连续的序列(上下文),使用CNN相当于割裂了前后的联系(只注重单张图片,而忽略了上下文关联)。从而诞生了专门为处理序列的Recurrent Neural Network(RNN),每一个神经元除了当前信息的输入外,还有之前产生的记忆信息,保留序列依赖型。”


所以,上面说,RNN是最有希望解决“预测”问题的神经元网络模型,因为RNN可以关注到“上下文”之间的联系。


1. RNN架构说明

首先看一个平淡无奇的全连接网络:

x为输入值,w为输入层权重;

隐层神经元输入u=w·x+b1,隐层输出h=f(u),f为激活函数;

最终输出y=z·h+b2,z为输出层权重。


而RNN的精妙之处,就在于引进了一个新的维度:时间


RNN的设计思路就是:t-1时刻的隐层输出,接入到t时刻的隐层,这样做的目的就是让t时刻的输出y不仅取决于输入x,同时也参考了t-1时刻的隐层输出h(t-1)。同样地,t+1时刻的输出也会参考t时刻的隐层输出h(t)。


在引入时间维度后,平淡无奇的全连接网络就变成了RNN↓

2.RNN的数学模型及代码
①正向传播:输入层→隐藏层

数学模型

以下大写字母代表该变量的矩阵形式,小写字母代表该变量的单个数值。

引入上一时刻的隐层输出及权值v之后,隐层神经元输入u为:

u = w ⋅ x + v ⋅ h t − 1 + b u = w·x + v·h^{t-1} + b u=wx+vht1+b

写成矩阵形式:

指定激活函数为tanh,t时刻隐层输出 H t H^t Ht为:

H t = t a n h ( U ) H^t=tanh(U) Ht=tanh(U)

代码

def forward(self, x, h_pre):  #h_pre 上一个时刻的隐层的输出,即为h_t-1
      u = np.dot(self.w, x) + np.dot(self.v, h_pre) + self.b
      self.h_cur = np.tanh(u)   #激活函数为tanh。h_cur即为h_t
      return self.h_cur
②正向传播:隐藏层→输出层

数学模型

输出Y为:

这没什么好说的,就和平淡无奇的全连接网络一样。

代码

def forward(self, h):
        self.h = h   #这里的h即为隐层输出h_t
        self.y = np.dot(self.z, h) + self.b  #输出层不加激活函数
        return self.y
③反向传播:输出层→隐藏层

数学模型

选择损失函数为MSE(均方差),即loss值e= Σ ( y o u t p u t − y t r a i n ) 2 Σ(y_{output}-y_{train})^2 Σ(youtputytrain)2

e对权值z的偏导为:

写成矩阵形式为:

e对其他权值的偏导计算思路一致,不再赘述。

代码

def backward(self, y_real):
        delta = (self.y - y_real) * 2
        self.grad_z = np.dot(delta, self.h.T)
        self.grad_h = np.dot(self.z.T, delta)
        self.grad_b = delta
    def step(self, lr):
        self.z = self.z - lr * self.grad_z
        self.b = self.b - lr * self.grad_b

有个细节需要说明下, 上面的代码出现了一个和Pytorch思路不同的地方:

在Pytorch中,权重梯度的计算是累加的,即上面的偏导数计算应该是self.grad_z += np.dot(delta, self.h.T)

所以用Pytorch的时候需要调用zero_grad(),每个batch手动把梯度清零。这里为了避免手动梯度清零,直接使用每次求得的梯度,不进行累加

那么为什么Pytorch要“多此一举”,默认梯度累加,还要手动梯度清零呢?这是因为在实际应用中,为了提升训练效率,在每个batch中梯度都进行累加,不同batch间进行梯度清零,这个操作算是实际工程应用的技巧。

④反向传播:隐藏层→输入层

数学模型

e对权值w的偏导为:

这里引入一个中间变量 δ h i d d e n δ_{hidden} δhidden:

因为隐藏层激活函数为 t a n h ( ) tanh() tanh(),即 h = t a n h ( u ) h=tanh(u) h=tanh(u)所以:

求 y = e x − e − x e x + e − x y={{e^x-e^{-x}}\over{e^x+e^{-x}}} y=ex+exexex的导数为高中数学知识,不再赘述推导。

进而 δ h i d d e n δ_{hidden} δhidden为:

最终,E对权值W的偏导写成矩阵的形式为:

e对其他权值的偏导计算思路一致,不再赘述。

代码

def backward(self, x, h_cur, h_pre, grad_h):  #grad_h为loss对h的偏导
        delta = grad_h * (1-h_cur*h_cur)  #delta为loss对u的偏导,tanh(u)'=1-tanh(u)*tanh(u),这是一个中间参数,便于计算后面的梯度
        self.grad_w = np.dot(delta, x.T)
        self.grad_v = np.dot(delta, h_pre.T)
        self.grad_b = delta
        self.grad_x = np.dot(self.w.T, delta)
        self.grad_h_pre = np.dot(self.v.T, delta)
    def step(self, lr):
        self.w = self.w - lr * self.grad_w
        self.v = self.v - lr * self.grad_v
        self.b = self.b - lr * self.grad_b

华丽的分割线,以上RNN的理论基础的学习及基于Numpy的代码构建已经完成。下面来用搭建好的RNN模块训练数据解决实例问题。

三、RNN在实例中的应用

1.实例问题说明

仍然是这篇博客–基于Pytorch的机器学习Regression问题实例(附源码)里面的问题。

对 y = x ⋅ s i n ( x ) y = x·sin(x) y=xsin(x)函数,在 0 ≤ x ≤ 6 0≤x≤6 0x6范围等间距取600个点(并加上随机数噪声)来训练神经元网络,验证神经元网络对该函数的拟合情况,并对 x > 6 x>6 x6的范围进行预测。

2. 网络模型训练思路
①RNN训练数据

需要构建下面这样的一个csv文件,作为训练数据,共600个训练数据点:

x=[0.01,0.02,0.03…6.00]

y = x ⋅ s i n ( x ) + n o i s e y=x·sin(x)+noise y=xsin(x)+noisenoise为-0.01~0.01的随机数噪声

实在不会弄就留下邮箱吧。

再把600个点,以每6个点为一组,规划为100组数据,即:

x = [[0.01,0.02,0.03…0.06] T ^T T,[0.07,0.08,0.09…0.12] T ^T T,…[5.95,5.96,5.97…6.00] T ^T T]

与上面的数学模型保持一致,这里输入值x是竖向的。隐层输出h,最终输出y,也都是竖向的。

②RNN模型训练参数

batch size设定为5,即在每个batch中,时间维度需要从t=0,训练到t=4;

这里需要对batch的选取规则特殊说明下:第1个batch取第1组到第5组数据,第2个batch取第2组到第7组数据(而不是第6组到第10组),依此类推。这样iteration就是96(而不是100/5=20);

学习速率lr预设为0.0005,epoch预设为300;

输入层神经元数n_pre=6,隐藏层神经元数n_cur=20,输出层神经元数n_out=6。

3. 训练结果及参数调整
①学习速率lr调整

预设lr=0.0005,输出结果为↓(绿色为训练数据,红色为RNN模型输出数据)

经过多次调整,基本确定lr=0.023为比较合适的值。

lr的选择就是一点点尝试,没有什么特殊技巧,所以尝试过程略过。

输出结果为↓

数据会有1组飘离训练数据较大,其他都贴合的很好。

②epoch调整

这个其实很简单,只要看一下每个epoch输出的演变过程就可以了,从本实例来看epoch取到50就够了。

3.数据预测

按上面优化后的训练参数,取x>6.00(完全在训练数据范围外),对输出y进行预测。

x = [[6.01,6.02,6.03…6.06] T ^T T,[6.07,6.08,6.09…6.12] T ^T T,…[7.15,7.16,7.17…7.20] T ^T T]

输出结果为↓(蓝色为 y = x ⋅ s i n ( x ) y=x·sin(x) y=xsin(x)在6.01≤x≤7.20的函数曲线,即为预测目标。黄色为RNN模型输出值,即为预测值)

对预测数据局部放大可以看出,预测的第1组数据还是很准确的,后续数据偏差越来越大。

这个结果倒也符合客观逻辑:用过去的经验来预测未来的走势,对越是遥远的未来,判断的准确性就越低。

四、完整代码

import numpy as np
from pandas import read_csv
import matplotlib.pyplot as plt
filename = 'train_data_copy.csv'
data = read_csv(filename)
data = np.array(data)
train_data_x = data[0]
train_data_y = data[1]
x_train = train_data_x.reshape(100, 6, 1)
y_train = train_data_y.reshape(100, 6, 1)
#-------构建神经元模型↓--------
# np.random.seed(2)
class Hidden_layer():
    def __init__(self, n_pre, n_cur):   # n_pre:上一层神经元个数,n_cur:本层神经元个数
        self.w = np.random.randn(n_cur, n_pre)
        self.v = np.random.randn(n_cur, n_cur)
        self.b = np.zeros([n_cur, 1])   #初始化神经元参数
    def forward(self, x, h_pre):  #h_pre 上一个时刻的隐层的输出,即为h_t-1
        u = np.dot(self.w, x) + np.dot(self.v, h_pre) + self.b
        self.h_cur = np.tanh(u)   #激活函数为tanh。h_cur即为h_t
        return self.h_cur
    def backward(self, x, h_cur, h_pre, grad_h):  #grad_h为loss对h的偏导
        delta = grad_h * (1-h_cur*h_cur)  #delta为loss对u的偏导,tanh(u)'=1-tanh(u)*tanh(u),这是一个中间参数,便于计算后面的梯度
        self.grad_w = np.dot(delta, x.T)
        self.grad_v = np.dot(delta, h_pre.T)
        self.grad_b = delta
        self.grad_x = np.dot(self.w.T, delta)
        self.grad_h_pre = np.dot(self.v.T, delta)
    def step(self, lr):
        self.w = self.w - lr * self.grad_w
        self.v = self.v - lr * self.grad_v
        self.b = self.b - lr * self.grad_b
class Output_layer():
    def __init__(self, n_cur, n_out):
        self.z = np.random.randn(n_out, n_cur)
        self.b = np.zeros([n_out, 1])  # 初始化神经元参数
    def forward(self, h):
        self.h = h   #这里的h即为隐层输出h_t
        self.y = np.dot(self.z, h) + self.b  #输出层不加激活函数
        return self.y
    def backward(self, y_real):
        delta = (self.y - y_real) * 2
        self.grad_z = np.dot(delta, self.h.T)
        self.grad_h = np.dot(self.z.T, delta)
        self.grad_b = delta
    def step(self, lr):
        self.z = self.z - lr * self.grad_z
        self.b = self.b - lr * self.grad_b
#-------构建神经元模型↑--------
#-------训练模型↓---------
#指定模型参数
n_pre = 6  #以6个数据为一组输入
n_cur = 20
n_out = 6  #输出数据也为6个一组
batch_size = 5  #指定batch size
epoch = 300
iteration = 96
hidden_layer = Hidden_layer(n_pre, n_cur)
output_layer = Output_layer(n_cur, n_out)
y_rnn_out = np.zeros([len(y_train), n_out, 1])
h_t = np.zeros([len(y_train) + 1, n_cur, 1])  # 用于储存所有时刻隐层的输出h_t
h_pre = h_t[0, :]  # 初始化h_pre即h_0,因为第一个时刻的前一个时刻没有输出,这里全都初始化为0(也可以是随机数)
for e in range(epoch):
    for k in range(iteration):
        #正向传播,从t=0传播到t=4
        for j in range(batch_size):
            h_cur = hidden_layer.forward(x_train[k + j, :], h_pre)
            h_t[k + j + 1, :] = h_cur
            h_pre = h_cur
        y_rnn_out[k + batch_size - 1, :] = output_layer.forward(h_cur)
        #反向传播,从t=4传播到t=0
        output_layer.backward(y_train[k + batch_size - 1, :])
        grad_h = output_layer.grad_h  #输出层的输入x即为隐层输出h
        for j in reversed(range(batch_size)):
            hidden_layer.backward(x_train[k + j], h_t[k + j + 1], h_t[k + j], grad_h)
            grad_h = hidden_layer.grad_h_pre
        #更新权值
        hidden_layer.step(lr= 0.023)
        output_layer.step(lr= 0.023)
#-------训练模型↑---------
for k in range(100):
    plt.scatter(x_train[k, :], y_train[k, :], c="green", s=15)  # 绿色线为真实值(训练数据)
    plt.scatter(x_train[k, :], y_rnn_out[k, :], c="red", s=15)  # 红色线为学习值
plt.show()
# #-------用训练好的模型预测数据↓-------
#创建预测数据的横坐标x_predict值
x_predict = np.zeros([20, 6, 1])
for i in range(20):   #预测20个数据
    x_predict[i] = np.linspace(6.01+i*0.06, 6.01+(i+1)*0.06, 6).reshape(-1,1)
#创建真实y值
y_real = x_predict * np.sin(x_predict)
#创建隐藏层输出h_predict,隐层输出h要多一个h_0
h_predict = np.zeros([21, 20, 1])
h_predict[0, :] = h_t[100, :]
#创建预测结果数据
y_predict = np.zeros([20, 6, 1])
h_predict_pre = h_predict[0]
#前向传播
for i in range(20):
    h_predict_cur = hidden_layer.forward(x_predict[i, :], h_predict_pre)
    h_predict[i+1, :] = h_predict_cur
    h_predict_pre = h_predict_cur
    y_predict[i, :] = output_layer.forward(h_predict_cur)
#-------用训练好的模型预测数据↑-------
for i in range(20):
    plt.scatter(x_predict[i, :], y_real[i, :], c="blue", s = 15)  #蓝色线为真实值
    plt.scatter(x_predict[i, :], y_predict[i, :], c="orange", s = 15)  #黄色线为预测值
print(y_predict)
plt.show()


相关文章
|
1月前
|
BI 测试技术 索引
Python学习笔记之NumPy模块——超详细(安装、数组创建、正态分布、索引和切片、数组的复制、维度修改、拼接、分割...)-1
Python学习笔记之NumPy模块——超详细(安装、数组创建、正态分布、索引和切片、数组的复制、维度修改、拼接、分割...)
|
26天前
|
机器学习/深度学习 PyTorch 算法框架/工具
RNN、LSTM、GRU神经网络构建人名分类器(三)
这个文本描述了一个使用RNN(循环神经网络)、LSTM(长短期记忆网络)和GRU(门控循环单元)构建的人名分类器的案例。案例的主要目的是通过输入一个人名来预测它最可能属于哪个国家。这个任务在国际化的公司中很重要,因为可以自动为用户注册时提供相应的国家或地区选项。
|
24天前
|
数据采集 算法 BI
解析numpy中的iscomplex方法及实际应用
在 NumPy 中,iscomplex 函数用于检查数组中的每个元素是否为复数。这个函数在处理包含复数数据的数组时非常有用,尤其是在科学计算和工程领域,这些领域经常需要区分实数和复数。 在数学和工程领域,复数是一种基本的数值类型,它们扩展了实数系统,包含了实部和虚部。在 NumPy 中,复数由 numpy.complex128 或 numpy.complex64 类型表示。numpy.iscomplex 函数提供了一种简便的方式来检查数组中的元素是否为复数。这对于数据类型判断、数据清洗和后续的数值分析非常重要。
|
26天前
|
机器学习/深度学习 数据采集
RNN、LSTM、GRU神经网络构建人名分类器(一)
这个文本描述了一个使用RNN(循环神经网络)、LSTM(长短期记忆网络)和GRU(门控循环单元)构建的人名分类器的案例。案例的主要目的是通过输入一个人名来预测它最可能属于哪个国家。这个任务在国际化的公司中很重要,因为可以自动为用户注册时提供相应的国家或地区选项。
|
4天前
|
数据采集 机器学习/深度学习 数据处理
数据科学家的秘密武器:Pandas与NumPy高级应用实战指南
【7月更文挑战第14天】Pandas与NumPy在数据科学中扮演关键角色。Pandas的DataFrame和Series提供高效数据处理,如数据清洗、转换,而NumPy则以ndarray为基础进行数值计算和矩阵操作。两者结合,从数据预处理到数值分析,形成强大工具组合。示例展示了填充缺失值、类型转换、矩阵乘法、标准化等操作,体现其在实际项目中的协同效用。掌握这两者,能提升数据科学家的效能和分析深度。**
14 0
|
27天前
|
机器学习/深度学习 C语言 索引
数组计算模块NumPy(一)
NumPy是Python科学计算的核心库,提供高性能的数组和矩阵操作,支持大量数学函数。它包括一维、二维到多维数组,并通过C实现,优化了计算速度。
数组计算模块NumPy(一)
|
27天前
|
索引 Python
数组计算模块NumPy(二)
NumPy教程概要:介绍数组切片、二维数组索引、重塑、转置和数组操作。讨论了切片语法`[start:stop:step]`,二维数组的索引方式,以及reshape方法改变数组形状。涉及转置通过`.T`属性或`transpose()`函数实现,数组增加使用`hstack()`和`vstack()`,删除用`delete()`。还提到了矩阵运算,包括加减乘除,并展示了`numpy.dot()`和`@`运算符的使用。最后提到了排序函数`sort()`、`argsort()`和`lexsort()`,以及NumPy的统计分析函数如均值、标准差等。
|
1月前
|
存储 API C语言
Python学习笔记之NumPy模块——超详细(安装、数组创建、正态分布、索引和切片、数组的复制、维度修改、拼接、分割...)-2
Python学习笔记之NumPy模块——超详细(安装、数组创建、正态分布、索引和切片、数组的复制、维度修改、拼接、分割...)
|
1月前
|
机器学习/深度学习 人工智能 IDE
人工智能平台PAI操作报错合集之交互式建模(DSW)环境中,numpy模块如何正确安装
阿里云人工智能平台PAI (Platform for Artificial Intelligence) 是阿里云推出的一套全面、易用的机器学习和深度学习平台,旨在帮助企业、开发者和数据科学家快速构建、训练、部署和管理人工智能模型。在使用阿里云人工智能平台PAI进行操作时,可能会遇到各种类型的错误。以下列举了一些常见的报错情况及其可能的原因和解决方法。
|
2月前
|
SQL 数据采集 数据挖掘
构建高效的Python数据处理流水线:使用Pandas和NumPy优化数据分析任务
在数据科学和分析领域,Python一直是最受欢迎的编程语言之一。本文将介绍如何通过使用Pandas和NumPy库构建高效的数据处理流水线,从而加速数据分析任务的执行。我们将讨论如何优化数据加载、清洗、转换和分析的过程,以及如何利用这些库中的强大功能来提高代码的性能和可维护性。