反向传播算法(backpropagation)代码实现(基于python且不使用框架)

反向传播算法代码实现(基于python且不使用框架)

–作者:就是我自己

摘要

本文致力于对反向传播算法的原理代码实现,因此代码设计部分不会使用任何的神经网络框架,为读者刨析反向传播算法最底层的原理,而且对于理论部分的知识会相对粗略地进行介绍,如果你是已经完全了解反向传播算法原理的同学,可以直接跳转到附录的代码汇总部分

运行环境依赖

  • numpy
  • python3
    在这里插入图片描述

运行结果

在这里插入图片描述

通用近似定理

在这里插入图片描述

前向传播激活值

在这里插入图片描述

参数学习

在这里插入图片描述

梯度下降示意

在这里插入图片描述

反向传播算法

在这里插入图片描述
在这里插入图片描述

公式推导结论

在这里插入图片描述

激活函数设计

在这里插入图片描述

神经网络类设计

在这里插入图片描述

网络参数初始化

在这里插入图片描述

激活值与代价计算

在这里插入图片描述

梯度计算(核心部分)

在这里插入图片描述

更新权重与偏置

在这里插入图片描述

模型评估

在这里插入图片描述

模型训练

在这里插入图片描述

保存模型数据

在这里插入图片描述

附录–全代码

哎呀,不想对着上面的流程一个个敲代码?我懂,我懂,所以这里直接给出全部代码的汇总,方便读者一键复制粘贴哟😈😈

import numpy


# 将输出压缩成激活值得函数
def sigmoid(z):
    return 1 / (1 + numpy.exp(-z))


# sigmoid 的导函数
def sigmoid_prime(z):
    return sigmoid(z) * (1 - sigmoid(z))


class Network(object):
    def __init__(self, sizes):
        self.num_layers = len(sizes)  # 网络中包含的层数(包括并不存在的输入层)
        self.sizes = sizes[1:]  # 各层网络节点数(不包含不存在的输入层)
        # 第一层为输入层,没有权重和偏执
        self.biases = [numpy.random.randn(y) for y in sizes[1:]]  # 二维数组:self.biases[i][j] 为第i+2层、第j+1个节点的偏置
        self.weights = [numpy.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
        print("test,hahaha")
        # 权重为三维数组:self.weights[i][j] 为第i+2层、第j+1个节点的权重向量,向量维度为前一层的输出向量维度

    def save(self, file_name: str):
        """
        保存当前网络参数数据:以(权重向量,偏置)的二维数组方式保存
        :param file_name: 保存的文件名,不含后缀
        :return: None
        """
        data = []
        for b, w in zip(self.biases, self.weights):
            layer = []
            for bj, wj in zip(b, w):
                layer.append((wj, bj))
            data.append(layer)
        data = numpy.array(data, dtype=object)
        numpy.save(file_name, data)

    def load(self, file_name: str):
        """
        从文件中读取数据并初始化当前网络,改变网络结构并丢弃所有参数
        :param file_name: save()保存的文件不包含后缀
        :return:None
        """
        data = numpy.load(file_name + ".npy", allow_pickle=True)
        self.num_layers = len(data) + 1
        self.sizes = [len(l) for l in data]
        self.biases = []
        self.weights = []
        for layer in data:
            vb = []
            vw = []
            for w, b in layer:
                vb.append(b)
                vw.append(numpy.array(w))
            self.biases.append(numpy.array(vb))
            self.weights.append(numpy.array(vw))

    def feedforward(self, a):
        """
        :param a: 输入值:一个向量
        :return: 网络输出值:向量
        """
        for b, w in zip(self.biases, self.weights):  # 遍历所有层
            # b w 为[...]
            a = sigmoid(numpy.dot(w, a) + b)  # 计算当前层的激活数值向量,隐式的包含一个循环:遍历当前层的节点
        return a

    @staticmethod
    def cost_derivative(output_activation, y):
        """
        根据方程 (1) 计算输出层的误差:
        :param output_activation: 网络的实际输出,是一个十维的向量,0-9 的激活值
        :param y: 预期输出:在这里是一个整数值:0-9
        :return: 返回一个代表差异的向量,分别表示输出层各个节点的误差,正值表示比期望大,负值表示比期望小,
                 绝对值表示偏离期望的程度(修改的优先级)

        此处采用二次代价:
        对于单个样本:
        二次代价 C_x = \frac{(y-a)^2}{2}
                a = \sigma(z)
                \delta^L= a - y
        """
        # 由于期望输出向量是[0,0,0,0,0,0,0,0,0,0][y]=1,
        # 这里避免额外的向量计算,直接计算 output_activation - 期望输出向量
        output_activation = output_activation[:]
        output_activation[y] -= 1
        return output_activation

    # 计算梯度
    def backprop(self, x, y):
        """
        反向传播计算梯度
        :param x: 单个输入
        :param y: 期望输出
        :return: 梯度
        """
        # 这里构造一个存储权重 和 偏置 改变量(梯度)的容器,初始化为0
        nabla_b = [numpy.zeros(b.shape) for b in self.biases]
        nabla_w = [numpy.zeros(w.shape) for w in self.weights]
        activation = x  # 当前层输入的激活向量
        activations = [numpy.array(x)]  # 保存压缩后的激活值 (x 为输入层的激活值)
        zs = []  # 保存每一层的输出(未压缩)

        # 这里模拟一个前向传播的过程,将每一层的输出(未压缩)保存在zs中,压缩后的激活值(作为下一层的输入)保存在activations
        for b, w in zip(self.biases, self.weights):
            z = numpy.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)

        # 反向传播核心算法
        """
        反向传播:
        cost_derivative()计算输出层的梯度,对应输出层各个节点输出值的误差,
        为减少各个节点误差:(误差为正表示输出应当减小,反之增加,以下说明以误差为正值为前提)
            1.调整权重:减小该节点所有正输入对应权重,增加所有负输入对应权重
            2.减小偏置
            3.调整上一层的输出:减少正权重对应输出,增加负权重对应输出(反向传递)
        此外:不同误差表示调整该节点的性价比,误差很小意味着该节点输出很接近期望,即没必要调整,反之则意味着调整的性价比很高
             相应计算出的对应梯度绝对值大小表示调整的性价比
        """
        # 核心方程 README.md
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(
            zs[-1]
        )  # 根据方程 (1) 计算输出层误差向量

        nabla_b[-1] = delta  # 根据方程(3) 误差向量即偏置的梯度
        # 根据方程 (4) 计算输出层权重梯度
        # reshape()是为了将向量转化为矩阵(只有二维向量支持矩阵运算)(单行或单列),做矩阵乘法
        nabla_w[-1] = numpy.dot(
            delta.reshape(self.sizes[-1], 1),
            activations[-2].reshape(1, len(activations[-2])),
        )
        # 输出层梯度计算完毕,这里从倒数第二层反向计算所有层梯度(不包含输入层)
        for l in range(2, self.num_layers):
            z = zs[-l]
            delta = numpy.dot(self.weights[-l + 1].transpose(), delta) * sigmoid_prime(
                z
            )  # -l 层误差计算使用方程 (2) 由下一层表示当前层
            # 原理与计算输出层相同
            nabla_b[-l] = delta
            # 方程 (4)
            nabla_w[-l] = numpy.dot(
                delta.reshape(len(delta), 1),
                activations[-l - 1].reshape(1, len(activations[-l - 1])),
            )
        return nabla_b, nabla_w

    def update_mini_batch(self, mini_batch, eta):
        """
        根据小批量数据计算梯度更新权重和偏置,以减小误差
        :param mini_batch: 小批量数据,包含若干个 (x, y) 组合,x 为输入,y 为期望输出
        :param eta: 学习速率,每次更新会乘这个系数,值越大,则更改的越大
        :return: None
        """
        # 拷贝权重和偏置,填充为 0
        nabla_b = [numpy.zeros(b.shape) for b in self.biases]
        nabla_w = [numpy.zeros(w.shape) for w in self.weights]
        # 遍历样本中所有数据
        for x, y in mini_batch:
            # 计算得到权重和偏置的梯度
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            # 将所有样本中的梯度求和(相当于取平均值,但没有除以样本大小,变相增加学习速率)
            nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [
            w - (eta / len(mini_batch)) * nw for w, nw in zip(self.weights, nabla_w)
        ]
        self.biases = [
            b - (eta / len(mini_batch)) * nb for b, nb in zip(self.biases, nabla_b)
        ]

    def evaluate(self, test_data):
        """
        评估当前神经网络,返回判断正确的样本数
        :param test_data: 用于评估的数据集
        :return: 数据集中判断正确的样本数量
        """
        test_result = [(numpy.argmax(self.feedforward(x)), y) for x, y in test_data]
        return sum(int(x == y) for x, y in test_result)

    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        """
        加载训练数据集对网络进行训练,可接受测试数据集评估每一个周期的效果,但这会消耗更多时间
        :param training_data: 一个(x,y)元组的列表,x 表示输入,y 表示期望的输出
        :param epochs:   训练的周期
        :param mini_batch_size:  小批量训练数据大小
        :param eta: 学习速率
        :param test_data: 测试数据集,给出该数据会在每个周期打印评估结果,这会拖慢学习速度
        :return: None
        """
        # if test_data != None:
        n_test = len(test_data)

        n = len(training_data)  # n:测试数据集的大小
        for j in range(epochs):
            # 打乱训练数据
            numpy.random.shuffle(training_data)
            # 将训练数据分割成多个指定大小的小批量数据
            mini_batches = [
                training_data[k : k + mini_batch_size]
                for k in range(0, n, mini_batch_size)
            ]
            for mini_batch in mini_batches:
                # 小批量数据学习
                self.update_mini_batch(mini_batch, eta)
            # 打印进展
            if test_data != None:
                num = self.evaluate(test_data)
                print(
                    "周期:{0}  {1}/{2} {3}%".format(
                        j + 1, num, n_test, num / n_test * 100
                    )
                )
            else:
                print("周期 {0}  完成!".format(j + 1))


if __name__ == "__main__":
    # 加载数据
    training_data = numpy.load("training_data.npy", allow_pickle=True)
    test_data = numpy.load("test_data.npy", allow_pickle=True)
    validation_data = numpy.load("validation_data.npy", allow_pickle=True)
    # 创建神经网络
    net = Network([784, 15, 10])  # 输入层784 输出层10 固定,其他层可以任意
    # 加载训练数据训练
    net.SGD(
        training_data, 5, 5, 1, test_data
    )  # 参数依次为:训练数据集、训练周期、小批量数据大小、学习速率(省略了测试数据集)

    # 加载验证数据集对网络效果进行验证
    n_validation = len(validation_data)  # 获得验证数据集大小
    num = net.evaluate(validation_data)  # 获得评估正确样本数
    # 打印结果
    print("验证:{0}/{1} {2}%".format(num, n_validation, num / n_validation * 100))

参考文献

本文的理论部分大多数都参考了邱锡鹏老师所著的《Neural Network and Deep Learning
https://nndl.github.io/
有兴趣的同学也可以去看看这本书,难度较大,但是干货满满

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值