【机器学习】使用Numpy实现神经网络训练全流程

news2024/12/30 1:22:47

文章目录

  • 网络搭建
  • 前向传播
  • 反向传播
  • 损失计算
  • 完整代码

曾经在面试一家大模型公司时遇到的面试真题,当时费力写了一个小时才写出来,自然面试也挂了。后来复盘,发现反向传播掌握程度还是太差,甚至连梯度链式传播法则都没有弄明白。

网络搭建

这里我们以训练 全连接神经网络 为例,也就是输入层,隐藏层和输出层都是 全连接层,激活函数选择Relu函数。

  • 构造Linear层来实现全连接层,其中权重参数 w w w和偏执参数 b i a s bias bias分别使用随机初始化和零初始化
  • Relu激活函数没有可学习参数
  • 整个网络写成MLP类,并设置参数控制中间隐藏层的个数
class Relu:
    ''' relu activation function
    '''
    def forward(self, x):
        self.x = x
        return np.maximum(0, x)
    
class LinearLayer:
    def __init__(self, input_c, output_c):
        # y = x @ w + b
        # self.w = np.random.rand(input_c, output_c)
        self.w = np.random.rand(input_c, output_c) * 0.001 # 这里乘上0.001是为了防止结果太大,梯度爆炸
        self.b = np.zeros(output_c)
    
class MLP:
    def __init__(self, input_c, hidden_c, output_c, layers_num):
        self.layers = []

        # 初始化网络第一层
        self.layers.append(LinearLayer(input_c, hidden_c))
        self.layers.append(Relu())

        # 初始化网络中间层
        for i in range(layers_num - 2):
            self.layers.append(LinearLayer(hidden_c, hidden_c))
            self.layers.append(Relu())
        
        # 初始化网络最后一层,注意,最后一层没有relu激活函数
        self.layers.append(LinearLayer(hidden_c, output_c))

前向传播

前向传播部分,主要包括 Linear层的前向,Relu激活函数的前向,以及MLP类的前向:

  • Relu激活函数:前向就是和0比较大小,大于零的保留,小于零的都置为0
  • Linear层:前向计算公式就是线性回归方程,要注意计算时维度需要对齐
  • MLP类:逐层调用前向传播函数
class Relu:
    ''' relu activation function
    '''
    def forward(self, x):
        self.x = x
        return np.maximum(0, x)
    
class LinearLayer:
    def __init__(self, input_c, output_c):
        # y = x @ w + b
        # self.w = np.random.rand(input_c, output_c)
        self.w = np.random.rand(input_c, output_c) * 0.001 # 这里乘上0.001是为了防止结果太大,梯度爆炸
        self.b = np.zeros(output_c)

    def forward(self, x):
        self.x = x # 这里保存输入,为了后续在反向传播中计算梯度
        # y = x @ w + b
        return np.dot(x, self.w) + self.b
    
class MLP:
    def __init__(self, input_c, hidden_c, output_c, layers_num):
        self.layers = []

        # 初始化网络第一层
        self.layers.append(LinearLayer(input_c, hidden_c))
        self.layers.append(Relu())

        # 初始化网络中间层
        for i in range(layers_num - 2):
            self.layers.append(LinearLayer(hidden_c, hidden_c))
            self.layers.append(Relu())
        
        # 初始化网络最后一层,注意,最后一层没有relu激活函数
        self.layers.append(LinearLayer(hidden_c, output_c))
        

    def forward(self, x):
        res = x
        for layer in self.layers:
            res = layer.forward(res)
        return res

反向传播

前向的输出,变为反向的输入
梯度,就是多元函数对某个变量的偏导数(变化最快的方向)
梯度下降法更新参数,就是朝着梯度的反方向更新参数
参数的梯度,意思是 损失函数对这个参数的梯度,根据求导的链式法则,可以表示为逐层求导乘积的形式

同前向一样,反向传播部分,也包括 Linear层的反向,Relu激活函数的反向,以及MLP类的反向:

  • Relu激活函数:大于0时导数为1,小于等于0时导数为0。同时由于没有可学习参数,所以只需要回传输入的梯度值即可
  • MLP类:由于是反向传播,所以起点就是 损失函数 对于 MLP最后一层的输出 的梯度值,然后从最后一层向前,逐层 回传 梯度值
  • Linear层:由于存在可学习参数需要去更新,因此不仅需要计算输入的梯度,还需要计算两个可学习参数 w w w b i a s bias bias的梯度,然后更新参数
class Relu:
    ''' relu activation function
    '''
    def forward(self, x):
        self.x = x
        return np.maximum(0, x)
    
    def backward(self, grad_output, lr):
        # 这里的lr没有用到,但是为了保持参数接口的一致性,还是保留了
        return grad_output * (self.x > 0) # relu函数的一阶导数,大于0部分为1,小于0部分为0
    
class LinearLayer:
    def __init__(self, input_c, output_c):
        # y = x @ w + b
        # self.w = np.random.rand(input_c, output_c)
        self.w = np.random.rand(input_c, output_c) * 0.001 # 这里乘上0.001是为了防止结果太大,梯度爆炸
        self.b = np.zeros(output_c)

    def forward(self, x):
        self.x = x # 这里保存输入,为了后续在反向传播中计算梯度
        # y = x @ w + b
        return np.dot(x, self.w) + self.b

    def backward(self, grad_output, lr):
        # linear层的梯度计算,涉及三个参数,x,w,b,为 dx, dw, db
        # 其中,dw和db是为了更新w和b
        # dx是为了计算下一层的梯度,链式法则

        # y = x @ w + b
        # dl / dx = dl / dy * dy / dx = grad_output * w
        # 这里要注意矩阵的维度要对齐
        grad_input = np.dot(grad_output, self.w.T)

        # dl / dw = dl / dy * dy / dw = grad_output * x
        # 这里要注意矩阵的维度要对齐
        w_grad = np.dot(self.x.T, grad_output)

        b_grad = np.sum(grad_output, axis=0)

        # 更新w和b的参数
        self.w -= lr * w_grad
        self.b -= lr * b_grad

        return grad_input
    
class MLP:
    def __init__(self, input_c, hidden_c, output_c, layers_num):
        self.layers = []

        # 初始化网络第一层
        self.layers.append(LinearLayer(input_c, hidden_c))
        self.layers.append(Relu())

        # 初始化网络中间层
        for i in range(layers_num - 2):
            self.layers.append(LinearLayer(hidden_c, hidden_c))
            self.layers.append(Relu())
        
        # 初始化网络最后一层,注意,最后一层没有relu激活函数
        self.layers.append(LinearLayer(hidden_c, output_c))
        

    def forward(self, x):
        res = x
        for layer in self.layers:
            res = layer.forward(res)
        return res

    def backward(self, grad_output, lr):
        grad = grad_output

        # 倒序遍历每一层,反向传播,计算每一层梯度
        # for layer in reversed(self.layers):
        for layer in self.layers[::-1]:
            grad = layer.backward(grad, lr)

        return grad

损失计算

这里为了简化操作,使用MSE均方误差函数,作为损失函数:

在这里插入图片描述
然后,计算损失loss对模型最后一层输出的梯度: l g r a d = 2 ∗ ( y − y ^ ) l_{grad}=2*(y-\hat{y}) lgrad=2(yy^),然后,将 l g r a d l_{grad} lgrad作为模型反向传播的起点,逐层回传梯度并使用梯度下降法更新参数。

完整代码

import numpy as np

class Relu:
    ''' relu activation function
    '''
    def forward(self, x):
        self.x = x
        return np.maximum(0, x)
    
    def backward(self, grad_output, lr):
        # 这里的lr没有用到,但是为了保持参数接口的一致性,还是保留了
        return grad_output * (self.x > 0) # relu函数的一阶导数,大于0部分为1,小于0部分为0
    
class LinearLayer:
    def __init__(self, input_c, output_c):
        # y = x @ w + b
        # self.w = np.random.rand(input_c, output_c)
        self.w = np.random.rand(input_c, output_c) * 0.001 # 这里乘上0.001是为了防止结果太大,梯度爆炸
        self.b = np.zeros(output_c)

    def forward(self, x):
        self.x = x # 这里保存输入,为了后续在反向传播中计算梯度
        # y = x @ w + b
        return np.dot(x, self.w) + self.b

    def backward(self, grad_output, lr):
        # linear层的梯度计算,涉及三个参数,x,w,b,为 dx, dw, db
        # 其中,dw和db是为了更新w和b
        # dx是为了计算下一层的梯度,链式法则

        # y = x @ w + b
        # dl / dx = dl / dy * dy / dx = grad_output * w
        # 这里要注意矩阵的维度要对齐
        grad_input = np.dot(grad_output, self.w.T)

        # dl / dw = dl / dy * dy / dw = grad_output * x
        # 这里要注意矩阵的维度要对齐
        w_grad = np.dot(self.x.T, grad_output)

        b_grad = np.sum(grad_output, axis=0)

        # 更新w和b的参数
        self.w -= lr * w_grad
        self.b -= lr * b_grad

        return grad_input
    
class MLP:
    def __init__(self, input_c, hidden_c, output_c, layers_num):
        self.layers = []

        # 初始化网络第一层
        self.layers.append(LinearLayer(input_c, hidden_c))
        self.layers.append(Relu())

        # 初始化网络中间层
        for i in range(layers_num - 2):
            self.layers.append(LinearLayer(hidden_c, hidden_c))
            self.layers.append(Relu())
        
        # 初始化网络最后一层,注意,最后一层没有relu激活函数
        self.layers.append(LinearLayer(hidden_c, output_c))
        

    def forward(self, x):
        res = x
        for layer in self.layers:
            res = layer.forward(res)
        return res

    def backward(self, grad_output, lr):
        grad = grad_output

        # 倒序遍历每一层,反向传播,计算每一层梯度
        # for layer in reversed(self.layers):
        for layer in self.layers[::-1]:
            grad = layer.backward(grad, lr)

        return grad

if __name__ == '__main__':
    input_data = np.random.rand(2, 8)
    input_c = 8
    hidden_c = 16
    output_c = 3
    layers = 5
    target = np.random.rand(2, 3)

    mlp_model = MLP(input_c, hidden_c, output_c, layers)

    # print(mlp_model.layers)

    for i in range(10):
        print(f'[Epoch: {i} / 100]', end='   ')
        res = mlp_model.forward(input_data)

        # 计算损失loss,这里使用mse,均方误差函数
        loss = ((res - target) ** 2).mean()

        # 损失对于最后一层输出res的梯度
        loss_grad = 2 * (res - target)

        # 反向传播,计算每一层梯度
        mlp_model.backward(loss_grad, lr=0.1)

        print(f'[loss: {loss}]')

输出结果:

[Epoch: 0 / 100]   [loss: 0.7094113331502839]
[Epoch: 1 / 100]   [loss: 0.2770589775342763]
[Epoch: 2 / 100]   [loss: 0.12141369105814748]
[Epoch: 3 / 100]   [loss: 0.06538216434144564]
[Epoch: 4 / 100]   [loss: 0.04521136910150728]
[Epoch: 5 / 100]   [loss: 0.037950191234703855]
[Epoch: 6 / 100]   [loss: 0.03533631186000425]
[Epoch: 7 / 100]   [loss: 0.03439537638899603]
[Epoch: 8 / 100]   [loss: 0.034056663841438094]
[Epoch: 9 / 100]   [loss: 0.033934736564443235]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2135342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

solidity-19-fallback

接收ETH receive和fallback receive和callback是solidity中两个特殊的回调函数,一个处理接收ETH,一个处理不存在的函数调用。本质上就是吧fallback拆成了两个回调函数。我暂时不知道什么是fallback fallback调用不存在的函数时会被调用也就是这个函数是不是等价于…

视频转音频,分享这六种转换操作

视频转音频,随着多媒体技术的发展,人们越来越频繁地需要将视频中的音频部分提取出来单独使用。无论是为了制作播客、获取音乐片段还是其他需求,视频转音频都是一项非常实用的技能。为了让你轻松应对各种场合的需求,下文将为你详细…

day-55 不同路径

思路 动态规划:因为只能向右或向下移动,可以得出状态转换方程:dp[i][j]dp[i-1][j]dp[i][j-1] 解题过程 直接令第一行和第一列全为1,然后通过状态转换方程进行计算,返回dp[m-1][n-1]即可 Code class Solution {publi…

Centos挂载和删除nfs

一、Centos挂载nfs 1、安装NFS客户端软件 sudo yum install nfs-utils 2、 创建一个挂载点目录 mkdir -p /mnt/nfs 注意:目录可以随意创建 3、永久挂载nfs 即系统在每次启动后自动挂载NFS共享 (1)编辑 /etc/fstab vim /etc/fstab (2)添加nfs <nfs_server_ip&…

AI算法盒如何精准守护你的安全区域

在当今智能化时代&#xff0c;安全防范已成为社会各个领域的核心需求之一。万物AI算法盒&#xff0c;作为前沿科技的集大成者&#xff0c;其内置的区域人员入侵检测视觉算法&#xff0c;以卓越的性能和广泛的应用场景&#xff0c;为各行各业提供了高效、精准的安全解决方案。 核…

使用iperf3测试局域网服务器之间带宽

文章目录 一、下载安装1、windows2、centos 二、使用0、参数详解1、centos 一、下载安装 1、windows https://iperf.fr/iperf-download.php 拉到最下面选最新版&#xff1a; 2、centos yum install iperf3二、使用 0、参数详解 服务器或客户端&#xff1a; -p, --port #…

喧嚣漫天之际,重新审视以太坊的定位与路线图

价值捕获很重要&#xff0c;但现在讨论为时尚早。 作者&#xff1a;Mike Neuder&#xff08;以太坊基金会研究员&#xff09;&#xff1b;译者&#xff1a;Azuma&#xff1b;编辑&#xff1a;郝方舟 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 编者按…

Centos7通过Docker安装openGauss5.0.2并配置用户供Navicat连接使用

下载镜像 [rootiZ2ze3qc9ouxm10ykn3cvdZ ~]# docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/enmotech/opengauss:5.0.2 5.0.2: Pulling from ddn-k8s/docker.io/enmotech/opengauss 2ec76a50fe7c: Pull complete e48b50219b49: Pull complete 512e203af4…

万龙觉醒免费脚本,自动打金挂机!VMOS云手机辅助攻略!

《万龙觉醒》作为一款策略类手游&#xff0c;玩家需要在多个方面进行资源管理和战斗部署。为了更加高效地进行游戏&#xff0c;推荐使用VMOS云手机。通过VMOS云手机&#xff0c;你可以体验到游戏专属定制版的云手机&#xff0c;它内置游戏安装包&#xff0c;省去了重新下载安装…

102.WEB渗透测试-信息收集-FOFA语法(2)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;101.WEB渗透测试-信息收集-FOFA语法&#xff08;1&#xff09; FOFA使用实例 • title&q…

关于若依flowable的安装

有个项目要使用工作流功能&#xff0c;在网上看了flowable的各种资料&#xff0c;最后选择用若依RuoYi-Vue-Flowable这个项目来迁移整合。 一、下载项目代码&#xff1a; 官方项目地址&#xff1a;https://gitee.com/shenzhanwang/Ruoyi-flowable/ 二、新建数据库&#xff…

怎么合法开除摸鱼员工?证据确凿,合法解雇摸鱼员工的全流程解析!

一家公司里&#xff0c;总会有员工选择"摸鱼"&#xff0c;以混底薪度时日。 古人云&#xff1a;"不以规矩&#xff0c;不能成方圆。" 公司的规章制度&#xff0c;便是那约束员工行为、维护职场秩序的"规矩"&#xff0c;但规矩之下&#xff0c;…

循环神经网络RNN+长短期记忆网络LSTM 学习记录

循环神经网络&#xff08;RNN) RNN的的基础单元是一个循环单元&#xff0c;前部序列的信息经处理后&#xff0c;作为输入信息传递到后部序列 x为输入向量&#xff0c;y为输出向量&#xff0c;a为上一隐藏层的a与x通过激活函数得到的值&#xff0c;简言之&#xff0c;每一层神…

LeetCode[中等] 74.搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。…

ASP.NET Core高效管理字符串集合

我们在开发 Web 项目时经常遇到需要管理各种来源的字符串集合&#xff08;例如HTTP 标头、查询字符串、设置的值等&#xff09;的情况。合理的管理这些字符串集合不仅可以减少出bug的几率&#xff0c;也能提高应用程序的性能。ASP.NET Core 为我们提供了一种特殊的只读结构体 S…

网络基础,协议,OSI分层,TCP/IP模型

网络的产生是数据交流的必然趋势&#xff0c;计算机之间的独立的个体&#xff0c;想要进行数据交互&#xff0c;一开始是使用磁盘进行数据拷贝&#xff0c;可是这样的数据拷贝效率很低&#xff0c;于是网络交互便出现了&#xff1b; 1.网络是什么 网络&#xff0c;顾名思义是…

工业网关在工厂数据采集中的核心作用-天拓四方

随着工业4.0时代的到来&#xff0c;工厂设备数据采集的重要性日益凸显。其中&#xff0c;工业网关以其独特的功能和优势&#xff0c;在工厂数据采集系统中发挥着核心作用。本文旨在深入探讨工业网关在工厂数据采集中的关键作用&#xff0c;以及它是如何助力工厂实现智能化、高效…

高性能编程:无锁队列

目录 1.无锁队列 1.1.1 阻塞&#xff08;Blocking&#xff09; 1.1.2 无锁&#xff08;Lock-Free&#xff09; 1.1.3 无等待&#xff08;Wait-Free&#xff09; 1.2 队列 1.2.1 链表实现的队列 1.2.2 数组实现的队列 1.2.3 混合实现的队列 1.3 多线程中的先进先出数据…

打破瓶颈:搭贝低代码平台助力企业数字化转型

在当今快速变化的商业环境中&#xff0c;越来越多的企业认识到数字化转型的重要性。然而&#xff0c;很多企业在追求数字化的过程中却遇到各种障碍&#xff0c;无论是信息管理的混乱、软件使用的低效&#xff0c;还是应对市场变化的迟缓&#xff0c;这些问题都在消耗企业的资源…

React学习笔记(1.0)

在使用vite创建react时&#xff0c;有一个语言选项&#xff0c;就是typescript-SWC&#xff0c;这里介绍一下SWC。 SWC&#xff1a;可扩展的Rust的平台&#xff0c;用于下一代快速开发工具&#xff0c;SWC比Babel快20倍。 简单来说&#xff0c;就是用于格式转换的&#xff0c…