Press "Enter" to skip to content

神经网络:numpy实现神经网络框架

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

欢迎访问个人博客网站获取更多文章:

 

https://beityluo.space

 

本文用numpy
从零搭建了一个类似于pytorch
的深度学习框架

 

可以用于前面文章提到的MINST
数据集的手写数字识别、也可以用于其他的方面

 

Github:

 

https://github.com/BeityLuo/Deeplearning-frame-from-scrach

 

以下的文字介绍在仓库中的README.md
文件中有相同内容

 

神经网络框架使用方法及设计思想

在框架上基本模仿pytorch
,用以学习神经网络的基本算法,如前向传播、反向传播、各种层、各种激活函数
采用面向对象的思想进行编程,思路较为清晰
想要自己手写神经网络
的同学们可以参考一下
代码大体框架较为清晰,但不否认存在丑陋的部分,以及对于pytorch
的拙劣模仿

项目介绍

 

MINST_recognition
:

 

手写数字识别,使用MINST
数据集

 

训练30轮可以达到93%准确度,训练500轮左右达到95%准确度无法继续上升

 

RNN_sin_to_cos
:

 

使用循环神经网络RNN,用\(sin\)
的曲线预测\(cos\)
的曲线

 

目前仍有bug,无法正常训练

 

框架介绍

 

与框架有关的代码都放在了mtorch
文件夹中

 

使用流程

 

pytorch
相似,需要定义自己的神经网络、损失函数、梯度下降的优化算法等等

 

在每一轮的训练中,先获取样本输入将其输入到自己的神经网络中获取输出。然后将预测结果和期望结果
交给损失函数计算loss
,并通过loss
进行梯度的计算,最后通过优化器对神经网络的参数进行更新。

 

结合代码理解更佳:point_down::

 

以下是使用MINST
数据集的手写数字识别的主体代码

 

# 定义网络 define neural network
class DigitModule(Module):
    def __init__(self):
        # 计算顺序就会按照这里定义的顺序进行
        sequential = Sequential([
            layers.Linear2(in_dim=ROW_NUM * COLUM_NUM, out_dim=16, coe=2),
            layers.Relu(16),
            layers.Linear2(in_dim=16, out_dim=16, coe=2),
            layers.Relu(16),
            layers.Linear2(in_dim=16, out_dim=CLASS_NUM, coe=1),
            layers.Sigmoid(CLASS_NUM)
        ])
        super(DigitModule, self).__init__(sequential)
module = DigitModule()  # 创建模型 create module
loss_func = SquareLoss(backward_func=module.backward)  # 定义损失函数 define loss function
optimizer = SGD(module, lr=learning_rate)  # 定义优化器 define optimizer
for i in range(EPOCH_NUM):  # 共训练EPOCH_NUM轮
    trainning_loss = 0  # 计算一下当前一轮训练的loss值,可以没有
    for data in train_loader:  # 遍历所有样本,train_loader是可迭代对象,保存了数据集中所有的数据
        imgs, targets = data  # 将数据拆分成图片和标签
        outputs = module(imgs)  # 将样本的输入值输入到自己的神经网络中
        loss = loss_func(outputs, targets, transform=True)  # 计算loss / calculate loss
        trainning_loss += loss.value
        loss.backward()  # 通过反向传播计算梯度 / calculate gradiant through back propagation
        optimizer.step()  # 通过优化器调整模型参数 / adjust the weights of network through optimizer
    if i % TEST_STEP == 0:  # 每训练TEST_STEP轮就测试一下当前训练的成果
        show_effect(i, module, loss_func, test_loader, i // TEST_STEP)
        print("{} turn finished, loss of train set = {}".format(i, trainning_loss))

 

接下来逐个介绍编写的类,这些类在pytorch
中都有同名同功能的类,是仿照pytorch
来的:

 

Module

pytorch
不同,只能有一个Sequential
类(序列),在该类中定义好神经网络的各个层和顺序,然后传给Module
类的构造函数
正向传播:
调用Sequential
的正向传播
反向传播:
调用Sequential
的反向传播
目前为止,这个类的大部分功能与Sequential
相同,只是套了个壳
保证与pytorch
相同

lossfunction

有不同的loss
函数,构造函数需要给他指定自己定义的神经网络的反向传播函数
调用loss
函数会返回一个Loss
类的对象,该类记录了loss
值。
通过调用Loss
类的.backward()
方法就可以实现反向传播计算梯度
内部机制:
内部其实就是调用了自己定义的神经网络的反向传播函数
也算是对于pytorch
的一个拙劣模仿,完全没必要
,直接通过Module
调用就好

优化器:

目前只实现了随机梯度下降SGD
构造函数的参数是
自己定义的Module
。在已经计算过梯度之后,调用optimizer.step()
改变Module
内各个层的参数值
内部机制:
目前由于只有SGD一种算法,所以暂时也只是一个拙劣模仿
就是调用了一下Module.step()
,再让Module
调用Sequential.step()
,最后由Sequential
调用内部各个层的Layer.step()
实现更新
梯度值在loss.backward
的时候计算、保存在各个层中了

Layer

 

有许多不同的层

 

共性

前向传播

接受一个输入进行前向传播计算,输出一个输出
会将输入保存起来,在反向传播中要用
反向传播

接受前向传播的输出的梯度值
,计算自身参数(如Linear中的w和b)的梯度值
并保存起来
输出值为前向传播的输入的梯度值
,用来让上一层(可能没有)继续进行反向传播计算
这样不同的层之间就可以进行任意的拼装而不妨碍前向传播、反向传播的进行了

.step
方法
更新自身的参数值(也可能没有,如激活层、池化层)

Sequential

 

这个类也是继承自Layer
,可以当作一层来使用

 

它把多个层按照顺序拼装到一起,在前向、反向传播时按照顺序进行计算

 

结合它的forward
backward
方法来理解:

 

def forward(self, x):
    out = x
    for layer in self.layers:
        out = layer(out)
    return out
def backward(self, output_gradiant):
    layer_num = len(self.layers)
    delta = output_gradiant
    for i in range(layer_num - 1, -1, -1):
        # 反向遍历各个层, 将期望改变量反向传播
        delta = self.layers[i].backward(delta)
def step(self, lr):
    for layer in self.layers:
        layer.step(lr)

 

RNN
类:循环神经网络层

 

继承自Layer
,由于内容比较复杂故单独说明一下

 

RNN
内部由一个
全连接层Linear
和一个激活层
组成

 

前向传播

 

def forward(self, inputs):
        """
        :param inputs: input = (h0, x) h0.shape == (batch, out_dim) x.shape == (seq, batch, in_dim)
        :return: outputs: outputs.shape == (seq, batch, out_dim)
        """
        h = inputs[0]  # 输入的inputs由两部分组成
        X = inputs[1]
        if X.shape[2] != self.in_dim or h.shape[1] != self.out_dim:
            # 检查输入的形状是否有问题
            raise ShapeNotMatchException(self, "forward: wrong shape: h0 = {}, X = {}".format(h.shape, X.shape))
        self.seq_len = X.shape[0]  # 时间序列的长度
        self.inputs = X  # 保存输入,之后的反向传播还要用
        output_list = []  # 保存每个时间点的输出
        for x in X:
            # 按时间序列遍历input
            # x.shape == (batch, in_dim), h.shape == (batch, out_dim)
            h = self.activation(self.linear(np.c_[h, x]))
            output_list.append(h)
        self.outputs = np.stack(output_list, axis=0)  # 将列表转换成一个矩阵保存起来
        return self.outputs

 

反向传播

 

def backward(self, output_gradiant):
    """
    :param output_gradiant: shape == (seq, batch, out_dim)
    :return: input_gradiant
    """
    if output_gradiant.shape != self.outputs.shape:
        # 期望得到(seq, batch, out_dim)形状
        raise ShapeNotMatchException(self, "__backward: expected {}, but we got "
                                           "{}".format(self.outputs.shape, output_gradiant.shape))
    input_gradients = []
    # 每个time_step上的虚拟weight_gradient, 最后求平均值就是总的weight_gradient
    weight_gradients = np.zeros(self.linear.weights_shape())
    bias_gradients = np.zeros(self.linear.bias_shape())
    batch_size = output_gradiant.shape[1]
    # total_gradient: 前向传播的时候是将x, h合成为一个矩阵,所以反向传播也先计算这个大矩阵的梯度再拆分为x_grad, h_grad
    total_gradient = np.zeros((batch_size, self.out_dim + self.in_dim))
    h_gradient = None
    
    # 反向遍历各个时间层,计算该层的梯度值
    for i in range(self.seq_len - 1, -1, -1):
        # 前向传播顺序: x, h -> z -> h
        # 所以反向传播计算顺序:h_grad -> z_grad -> x_grad, h_grad, w_grad, b_grad
        # %%%%%%%%%%%%%%计算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
        # h_gradient = (output_gradiant[i] + total_gradient[:, 0:self.out_dim]) / 2
        # %%%%%%%%%%%%%%不计算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
        #  计算h_grad: 这一时间点的h_grad包括输出的grad和之前的时间点计算所得grad两部分
        h_gradient = output_gradiant[i] + total_gradient[:, 0:self.out_dim]  
        # w_grad和b_grad是在linear.backward()内计算的,不用手动再计算了
        z_gradient = self.activation.backward(h_gradient)  # 计算z_grad
        total_gradient = self.linear.backward(z_gradient)  # 计算x_grad和h_grad合成的大矩阵的梯度
        # total_gradient 同时包含了h和x的gradient, shape == (batch, out_dim + in_dim)
        x_gradient = total_gradient[:, self.out_dim:]
        input_gradients.append(x_gradient)  
        weight_gradients += self.linear.gradients["w"]
        bias_gradients += self.linear.gradients["b"]
    # %%%%%%%%%%%%%%%%%%计算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
    # self.linear.set_gradients(w=weight_gradients / self.seq_len, b=bias_gradients / self.seq_len)
    # %%%%%%%%%%%%%%%%%%不计算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
    self.linear.set_gradients(w=weight_gradients, b=bias_gradients)  # 设置梯度值
    
    list.reverse(input_gradients)  # input_gradients是逆序的,最后输出时需要reverse一下
    print("sum(weight_gradients) = {}".format(np.sum(weight_gradients)))
    
    # np.stack的作用是将列表转变成一个矩阵
    return np.stack(input_gradients), h_gradient

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注