从零开始开发自己的类keras深度学习框架2 :实现全连接层

认真学习,佛系更博。

上一章简单介绍了如何实现数据的读取功能,本章将详细介绍如何实现神经网络最基础的层:全连接层。

全连接层的原理想必很多读者都接触过很多资料,比如链式法则,反向传播,梯度下降法等等。说来惭愧,博主也早早地接触过,确一直没有仔细推敲其中的原理,以至于一直对该网络层困惑了很久,其实静下心来仔细去研究一下,会发现内部原理也很简单直观,我们先来了解一下链式法则:

我们知道,神经网络的很多操作都可以当作一个个独立的层,比如卷积层、全连接层、sigmoid激活层等,其原因在于,这些操作都可以当作一系列对应的函数映射,比如全连接层可以表示为:

y=X*W+b

我们进行一下封装,表示为y=f(x),其中f表示矩阵运算,如果在后面再接一个激活层,比如sigmoid,可以表示为:

y=sigmoid(X*W+b)

可以封装为y=g(f(x)),其中g表示sigmoid运算;

前向运算很好理解,直接带入x就可以计算到y,那么反向传播该怎样计算呢?

我们先来了解一个叫gradient_check的思想,对于每个网络层,其参数(W,b)的梯度定义为结果误差变化相对于变量变化的程度,比如某一层的W,给予其一个极小的变化,误差函数发生变化的值就是该W的梯度,基于该思想,我们可以对网络模型的每个参数依次施加微小的变化,从而计算每个参数的梯度,最后运用梯度下降法更新参数。

上诉方法被验证为一种可行的方法,但也存在明显的缺点,比如效率极低,因此,引出了链式法则的思想:

我们知道,反向传播就是求y对x的导数,复合函数求导满足下图(截自百度知道),这是在高中或大学的知识:

神经网络层就相当于上面的f、p、g操作,若我们想对某一层的参数进行求导,则可以利用该求导公式,该计算也被称为链式法则;

于是乎,我们可以想象,在反向传播过程中,对于某一个全连接层,先接收来自上层的梯度,然后对本层参数进行求导,最后计算针对输入数据的梯度,作为前一层的更新梯度,这便是全连接层(卷积层同理)的参数更新过程;

我们先定义一个父类网络层,实现一些基本操作,在enet下新建一个新的模块layers,并新建文件bas_layer.py:

import numpy as np


class Layer(object):
    """
    基础网络层
    """

    def __init__(self, layer_type="layer"):

        self.input_shape = None
        self.output_shape = None
        self.weight_shape = None
        self.layer_type = layer_type
        self.activation = None
        self.name = None

        self.cache = None

    @staticmethod
    def add_weight(shape=None,
                   dtype=np.float,
                   initializer="normal",
                   node_num=None):
        """
        初始化网络参数
        :param node_num: 上一层神经网络节点的数量
        :param shape: 参数的shape
        :param dtype: 参数的dtype
        :param initializer: 初始化方法
        :return: 初始化后的数据
        """

        if initializer == "zero":
            return np.zeros(shape=shape, dtype=dtype)
        if initializer == "normal":
            return np.random.normal(size=shape) * np.sqrt(1 / np.prod(node_num))
            # return np.random.normal(size=shape)

        raise TypeError("initializer must be normal or zero")

    def build(self, *args, **k_args):
        """
        编译网络层
        :param k_args:
        :return:
        """
        pass

    def forward(self, *args, **k_args):
        """
        前向运算
        :param k_args:
        :return:
        """
        pass

    def backward(self, *args, **k_args):
        """
        反向传播,只计算梯度而不更新参数
        :param k_args:
        :return:
        """
        pass

    def update(self, *args, **k_args):
        """
        更新参数
        :param k_args:
        :return:
        """
        pass

    def get_input_shape(self):
        """
        获取网络输入形状
        :return:
        """
        return self.input_shape

    def get_output_shape(self):
        """
        获取网络输出形状
        :return:
        """
        return self.output_shape

    def get_layer_type(self):
        """
        获取网络类型
        :return:
        """
        return self.layer_type

    def get_activation_layer(self):
        """
        获取激活函数类型
        :return:
        """
        return self.activation

    def get_weight_shape(self):
        """
        获取权重形状
        :return:
        """
        return self.weight_shape

    def get_name(self):
        """
        获取网络层名
        :return:
        """
        return self.name

    def set_name(self, name):
        """
        设置网络层名字
        :param name: 名字
        :return:
        """
        if not self.name:
            self.name = name

定义了一些公公变量,这些变量在以后会用到,另外添加了一个add_weight方法,该方法用于初始化网络的参数;

然后新建dense.py文件,dense的实现稍微复杂,因为我们要考虑神经单元的个数,是否使用激活函数等;另外,我们这里做一下说明,一般的神经网络框架都单独把优化器提出来作为统一的控制,但是神经网络的每层参数都可以使用不同的优化方法,adam、momentum,因此,我们为每个层建立一个优化控制器;

下面为dense的代码,将在下面做详细说明:

from enet.layers.base_layer import Layer

import numpy as np

from enet.optimizer import optimizer_dict


class Dense(Layer):
    """
    全连接神经网络类
    """

    def __init__(self, kernel_size=None, activation=None, input_shape=None, optimizer="sgd", name=None, **k_args):
        """
        :param kernel_size: 神经元个数
        :param activation: 激活函数
        :param input_shape: 输入shape,只在输入层有效;
        :param optimizer: 优化器;
        :param name: 网络层名字;
        """
        super(Dense, self).__init__(layer_type="dense")

        assert activation in {None, "sigmoid", "relu", "softmax"}
        assert optimizer in {"sgd", "momentum", "adagrad", "adam", "rmsprop"}

        self.output_shape = kernel_size
        self.activation = activation

        self.name = name

        # 该处的input_shape只在输入层有效,input_shape样式为(784,)
        if input_shape:
            self.input_shape = input_shape[0]

        self.weight = None
        self.bias = None

        # self.use_bias = use_bias
        self.optimizer = optimizer_dict[optimizer](**k_args)

    def build(self, input_shape):
        """
        根据input_shape来构建网络模型参数
        :param input_shape: 输入形状
        :return: 无返回值
        """

        last_dim = input_shape
        self.input_shape = input_shape

        shape = (last_dim, self.output_shape)
        self.weight_shape = shape

        self.weight = self.add_weight(shape=shape, initializer="normal", node_num=input_shape)
        self.bias = self.add_weight(shape=(self.output_shape,), initializer="zero")

    def forward(self, input_signal, *args, **k_args):
        """
        前向传播
        :param input_signal: 输入信息
        :return: 输出信号
        """
        self.cache = input_signal

        return np.dot(input_signal, self.weight) + self.bias

    def backward(self, delta):
        """
        反向传播
        :param delta: 输入梯度
        :return: 误差回传
        """

        # if self.use_bias:
        #     delta_b = np.mean(delta, axis=0)
        # else:
        #     delta_b = 0
        delta_b = np.sum(delta, axis=0)
        delta_w = np.dot(self.cache.transpose(), delta)

        self.optimizer.grand(delta_w=delta_w, delta_b=delta_b)

        # 回传给前一层的梯度
        return np.dot(delta, self.weight.transpose())

    def update(self, lr):
        """
        更新参数
        :param lr: 学习率
        :return:
        """
        delta_w, delta_b = self.optimizer.get_delta_and_reset(lr, "delta_w", "delta_b")

        self.weight += delta_w
        self.bias += delta_b



首先在初始化阶段,我们定义了关键参数,kernel_size,该参数表示该层神经元的个数,另外定义了优化器,激活函数等,这部分代码看不懂的不要着急,在后面实现优化器部分将详细介绍;

build函数完成了函数的初始化,将在网络模型的compile函数进行调用,我们需要关注的是forward和backward函数,实现了网络层的前向传播和反向传播;

forward容易理解,直接用y=W*X+b即可计算结果,难点在于backward,我们先来弄清楚矩阵的反向传播公式;

假设矩阵运算为:

y=W*X+b

上层关于y的求导为\frac{\partial L}{\partial y},则关于X,W, b的求导为:

\frac{\partial y}{\partial b} = \frac{\partial L}{\partial y}\\ \frac{\partial y}{\partial W} = X^{T}*\frac{\partial L}{\partial y}\\ \frac{\partial y}{\partial X} = \frac{\partial L}{\partial y}*W^{T}

关于公式的推导,这里不做过多证明,可以直接拿来用,感兴趣的可以自己展开矩阵推导下,过程并不难,或者用几个小矩阵模拟一下,很容易得出答案;

于是乎,我们对于权重和偏执的求导便可以写成如下关键部分:

        delta_b = np.sum(delta, axis=0)
        delta_w = np.dot(self.cache.transpose(), delta)

注意,这里使用sum而不是mean,原因是我们计算误差梯度时已经做了平均的操作,下一章将会介绍;

回传给上一层的梯度为:

        # 回传给前一层的梯度
        return np.dot(delta, self.weight.transpose())

然后更新参数,回传梯度即可;

好了,本篇文章先写到这里,下一篇将介绍如何实现优化器类,以及激活函数的实现;

整个代码的github网址为:https://github.com/darkwhale/neural_network,不断更新中;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Keras深度学习实战(15)——从零开始实现YOLO目标检测是一篇非常实用的教程。YOLO(You Only Look Once)是一种流行的实时目标检测算法,其核心思想是将目标检测任务视为回归问题,并通过卷积神经网络实现端到端的检测。这篇教程提供了一步一步的实现代码,让读者能够快速了解并实践YOLO目标检测的方法。 首先,教程介绍了YOLO的工作原理和网络结构。YOLO将输入图像划分为多个网格,每个网格负责预测包含在该网格中的目标。每个网格预测包含目标的方框的位置和别,以及目标的置信度。 接下来,教程详细介绍了如何实现YOLO的网络结构。使用Keras库,创建了一个具有卷积和池化层的卷积神经网络。还使用了Anchor Boxes,用来预测不同比例和宽高比的目标。 教程还介绍了如何预处理输入图像,包括将图像调整为适当的大小,并将目标边界框转换为YOLO需要的格式。然后,选择了合适的损失函数,训练了模型,以及进行了模型评估和预测。 最后,教程提供了一些改进和扩展的思路,包括使用更大的数据集进行训练、调整网络结构和超参数等等。 通过这篇教程,读者可以了解到YOLO目标检测的基本原理和实现步骤。并且,使用Keras库可以很方便地实现和训练自己的YOLO模型。无论是对于已经有一定深度学习基础的读者,还是对于刚刚开始学习的读者,这篇教程都是非常有价值的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值