手写神经网络和反向传播,带你彻底搞清底层原理(含代码)!


如果不想抄代码或者想获得数据,详见我的github

前言

回想起我初学神经网络与反向传播,以及torch框架时的种种挫折,我发现痛苦的根源在于各种知识点非常零散,虽然理论类的博客很多(给了我很多帮助),但深入讲解代码实现的却不多。所以为了搞懂pytorch体系,我阅读了不少torch的python源码,梳理出了torch主干, 并尝试只用numpy库和python内置库实现了一个小型神经网络系统。在这里分享给大家,欢迎大家批评与指正,另外这是我的第一篇博客,真诚希望大家指出我的不足之处,我会一定会加以改进。

为了便于理解,本篇文章只实现最简单的多层感知机,使用逻辑回归解决平面点集的二分类问题,让初接触这方面的
读者能够快速入门,让有这方面基础的读者的也能巩有所收获。(我的水平也不足以去讲解整个torch体系hhhhh…)

为了不让文章的开头太突兀,我会尽量详细说明。
但在此之前需要有以下基础:
1.扎实的Python语言基础,会使用numpy,类的继承,装饰器语法
2.线性代数知识,矩阵乘法和哈达玛乘积的概念
3.函数偏导数与链式求导法则
4.了解机器学习里使用梯度下降算法求解分类回归问题的思维方法(这个很重要)
5.想不出来了…每个人不一样,也不好评估

反向传播的实现本篇文章的核心之一,在这里我附上我当时学习反向传播其中一个比较好的博客 链接

看不懂不要紧,我也会画一张图,给出一个具体例子。

关于本篇的介绍【有不少公式编辑,建议全篇用电脑查看,不然格式显示会很难受】

下面很啰嗦,不想读的话直接看图就好

1. 本篇拟仅用 n u m p y 库和 p y t h o n 内置库手搓一个小型的神经网络系统 , 对 t o r c h 最核心的几个模块进行了 p y t h o n 实现 , 本篇通过实现最基本的多层感知机 ( M u l t i l a y e r   P e r c e p t r o n ) , 神经网络的最基本训练方式 ( B a c k P r o p a g a t i o n ) , 希望能够理清 t o r c h 的主干框架结构 , 以便加深对神经网络的理解 1.本篇拟仅用numpy库和python内置库手搓一个小型的神经网络系统,\\ 对torch最核心的几个模块进行了python实现,\\ \quad \\ 本篇通过实现最基本的多层感知机(Multilayer \ Perceptron),\\ 神经网络的最基本训练方式(BackPropagation),\\ 希望能够理清torch的主干框架结构,以便加深对神经网络的理解 1.本篇拟仅用numpy库和python内置库手搓一个小型的神经网络系统,torch最核心的几个模块进行了python实现,本篇通过实现最基本的多层感知机(Multilayer Perceptron),神经网络的最基本训练方式(BackPropagation),希望能够理清torch的主干框架结构,以便加深对神经网络的理解
\quad \\ \quad
2. 本篇全部采用类来实现 , 避免多文件带来的阅读不便 , M y t o r c h 尽可能地贴合了 t o r c h 的体系 , 是使用 n u m p y 对 t o r c h 体系主干部分的简单实现 , 包括 : 网络自动求导 , 前向、反向传播机制 L i n e a r , M o d u l e 模块 , S i g m o i d , T a n h 激活函数 B C E W i t h L o g i t s L o s s 损失函数 , M i n i _ B G D 梯度下降算法 D a t a s e t , D a t a L o a d e r 数据集和数据迭代器 2.本篇全部采用类来实现,避免多文件带来的阅读不便,\\ Mytorch尽可能地贴合了torch的体系,\\ 是使用numpy对torch体系主干部分的简单实现,\\ 包括: \\ 网络自动求导,前向、反向传播机制 \\ \quad \\ Linear,Module模块, Sigmoid,Tanh激活函数 \\ \quad \\ BCEWithLogitsLoss损失函数, Mini\_BGD 梯度下降算法 \\ \quad \\ Dataset,DataLoader 数据集和数据迭代器 2.本篇全部采用类来实现,避免多文件带来的阅读不便,Mytorch尽可能地贴合了torch的体系,是使用numpytorch体系主干部分的简单实现,包括:网络自动求导,前向、反向传播机制Linear,Module模块,Sigmoid,Tanh激活函数BCEWithLogitsLoss损失函数,Mini_BGD梯度下降算法Dataset,DataLoader数据集和数据迭代器
\quad \\ \quad
3. 未实现 : 普通 t e n s o r 的求导机制 ( 所以 b a c k w a r d 会和 t o r c h 体系有一定的差异 , 比如 d e t a c h 和 i s _ l e a f 等 , 但基本原理相同 ) , C N N , R N N 等网络 , R e L U , G L U 等激活函数 , C r o s s E n t r o p y L o s s , B C E L o s s 等损失函数 , S e q u e n t i a l , M o d u l e L i s t 等辅助容器类 , S G D , A d a m 等梯度下降算法 ,   S t e p L R 等学习率调度器 等等等等 . . . . . . 3.未实现: \\ 普通tensor的求导机制(所以backward会和torch体系有一定的差异,比如detach和is\_leaf等,但基本原理相同), \\ \quad \\ CNN,RNN等网络, ReLU,GLU等激活函数, \\ \quad \\ CrossEntropyLoss,BCELoss等损失函数, Sequential,ModuleList等辅助容器类, \\ \quad \\ SGD,Adam等梯度下降算法, \ StepLR等学习率调度器 \\ \quad \\ 等等等等...... 3.未实现:普通tensor的求导机制(所以backward会和torch体系有一定的差异,比如detachis_leaf,但基本原理相同),CNN,RNN等网络,ReLU,GLU等激活函数,CrossEntropyLoss,BCELoss等损失函数,Sequential,ModuleList等辅助容器类,SGD,Adam等梯度下降算法, StepLR等学习率调度器等等等等......

[ 补充 ] 1. t o r c h 是一个含有多个模块的庞大的深度学习框架 , 其底层主要由 C / C + + 实现 , 本篇全部采用 p y t h o n , 其主要目的在于理清 t o r c h 的主干结构 , 而不是复现 t o r c h . 2. 下图是 t o r c h 主干框架 ( 的仅仅一小部分 ) 和本篇将实现的框架结构 , 相同颜色表示相互对应 , 可以看出 M y t o r c h 和 t o r c h 的架构并不一样 ( 只提取了主干 ) 3. 白框部分未实现,感兴趣的读者 ( 真的会有么 h h h h ) 可以尝试实现 [补充] \\ 1.torch是一个含有多个模块的庞大的深度学习框架,其底层主要由C/C++实现,\\ 本篇全部采用python,其主要目的在于理清torch的主干结构,而不是复现torch. \\ 2.下图是torch主干框架(的仅仅一小部分)和本篇将实现的框架结构,相同颜色表示相互对应,\\ 可以看出Mytorch和torch的架构并不一样(只提取了主干) \\ 3.白框部分未实现,感兴趣的读者(真的会有么hhhh )可以尝试实现 [补充]1.torch是一个含有多个模块的庞大的深度学习框架,其底层主要由C/C++实现,本篇全部采用python,其主要目的在于理清torch的主干结构,而不是复现torch.2.下图是torch主干框架(的仅仅一小部分)和本篇将实现的框架结构,相同颜色表示相互对应,可以看出Mytorchtorch的架构并不一样(只提取了主干)3.白框部分未实现,感兴趣的读者(真的会有么hhhh)可以尝试实现

torch-Mytorch架构图

请添加图片描述

反向传播原理公式

相信大家都看过了那篇博客,或者已经对反向传播有所了解了,这里给出所需公式,以及一张图,让大家更加直观的理解

本节主要讲述反向传播原理 , 给出本篇实现反向传播代码需要依赖的基本公式 本节主要讲述反向传播原理,给出本篇实现反向传播代码需要依赖的基本公式\quad \\ \quad 本节主要讲述反向传播原理,给出本篇实现反向传播代码需要依赖的基本公式

[ 注 ] 因为 [注] 因为 []因为导数要放在形状和原矩阵一样的矩阵中(这句话很重要), 故矩阵偏微分采用分母布局 ( 所以和雅可比不同 , 雅可比是分子布局 ) , 故矩阵偏微分采用分母布局(所以和雅可比不同,雅可比是分子布局),\quad \\ \quad 故矩阵偏微分采用分母布局(所以和雅可比不同,雅可比是分子布局),
以下公式均以分母布局给出 ( 即偏导数矩阵和被求的偏导数的原矩阵形状相同 ) : 以下公式均以分母布局给出(即偏导数矩阵和被求的偏导数的原矩阵形状相同): \quad \\ \quad 以下公式均以分母布局给出(即偏导数矩阵和被求的偏导数的原矩阵形状相同):

元素级别函数求导 ( 函数作用于矩阵等价于作用于矩阵中每个元素 , 激活函数求导需要用到 ) : 元素级别函数求导(函数作用于矩阵等价于作用于矩阵中每个元素,激活函数求导需要用到): 元素级别函数求导(函数作用于矩阵等价于作用于矩阵中每个元素,激活函数求导需要用到):

∂ f ( W ) ∂ W = [ ∂ f ( w 11 ) ∂ w 11 . . . ∂ f ( w 1 n ) ∂ w 1 n . . . . . . . . . ∂ f ( w n 1 ) ∂ w n 1 . . . ∂ f ( w n n ) ∂ w n n ] \cfrac{\partial f(W)}{\partial W}=\begin{bmatrix} \cfrac{\partial f(w_{11})}{\partial w_{11}} & ... & \cfrac{\partial f(w_{1n})}{\partial w_{1n}} \\ ... & ... & ... \\ \cfrac{\partial f(w_{n1})}{\partial w_{n1}} & ... & \cfrac{\partial f(w_{nn})}{\partial w_{nn}} \end{bmatrix} Wf(W)= w11f(w11)...wn1f(wn1).........w1nf(w1n)...wnnf(wnn)
\quad \\ \quad

标量对向量求导及链式法则 : 标量对向量求导及链式法则: 标量对向量求导及链式法则:

∂ y ∂ X = [ ∂ y ∂ x 1   ∂ y ∂ x 2   . . .   ∂ y ∂ x n ] T ,   w h e r e   y ∈ R   ,   X ∈ R n × 1 \cfrac{\partial y}{\partial X}=\begin{bmatrix}\cfrac{\partial y}{\partial x_1} \ & \cfrac{\partial y}{\partial x_2}\ & ... \ & \cfrac{\partial y}{\partial x_n}\end{bmatrix}^T , \ where \ y \in R \ , \ X \in R^{n \times 1} Xy=[x1y x2y ... xny]T, where yR , XRn×1

∂ z ∂ X = ∂ Y ∂ X ⋅ ∂ z ∂ Y = J X ( Y ) T ⋅ ∂ z ∂ Y   , w h e r e   X ∈ R n × 1   ,   Y = F ( X ) ∈ R m × 1   ,   z = G ( X ) ∈ R \cfrac{\partial z}{\partial X}=\cfrac{\partial Y}{\partial X} \cdot \cfrac{\partial z}{\partial Y}=J_X(Y)^T \cdot \cfrac{\partial z}{\partial Y} \ , where \ X \in R^{n \times 1} \ , \ Y=F(X) \in R^{m \times 1}\ , \ z=G(X) \in R Xz=XYYz=JX(Y)TYz ,where XRn×1 , Y=F(X)Rm×1 , z=G(X)R
\quad \\ \quad

推论(参数矩阵求导需要用到) , 大家不妨写几个具体的矩阵验证一下 : 推论(参数矩阵求导需要用到),大家不妨写几个具体的矩阵验证一下: 推论(参数矩阵求导需要用到),大家不妨写几个具体的矩阵验证一下:

∂ z ∂ X = A T ⋅ ∂ z ∂ ( A X ) ,   w h e r e   z = f ( A X ) ∈ R ,   A ∈ R m × n ,   X ∈ R n × p \cfrac{\partial z}{\partial X}=A^T \cdot \cfrac{\partial z}{\partial (AX)} , \ where\ z=f(AX) \in R , \ A \in R^{m \times n} , \ X \in R^{n \times p} Xz=AT(AX)z, where z=f(AX)R, ARm×n, XRn×p

∂ z ∂ X = ∂ z ∂ ( X B ) ⋅ B T ,   w h e r e   z = f ( X B ) ∈ R ,   B ∈ R p × m   ,   X ∈ R n × p \cfrac{\partial z}{\partial X}=\cfrac{\partial z}{\partial (XB)} \cdot B^T ,\ where \ z=f(XB) \in R, \ B \in R^{p \times m}\ , \ X \in R^{n \times p} Xz=(XB)zBT, where z=f(XB)R, BRp×m , XRn×p
\quad \\ \quad

有了上述基础 , 就可以来搭建整个网络体系了 , 这里给出一个具体例子 , 本篇将以与该例相同的模式搭建 : 有了上述基础,就可以来搭建整个网络体系了,这里给出一个具体例子,\\ 本篇将以与该例相同的模式搭建:\quad \\ \quad 有了上述基础,就可以来搭建整个网络体系了,这里给出一个具体例子,本篇将以与该例相同的模式搭建:
在这里插入图片描述

好了好了,差不多了,让我们进入正题,建议可以从定义数据集和本问题的网络开始看,然后往前回溯,因为日常中我们都是调库,这种方式更贴合思维,同时大家可以对比torch版代码(可以发现几乎是一样的):

【注】以下代码运行环境为python3.9 (一些语法可能3.7及以下不支持,3.8及以上应该都不会有语法不支持的情况)

'''以下是全部所需库,pickle用于载入数据集,数据集很简单,后面会提到'''
import numpy as np
import math,pickle,time
import matplotlib.pyplot as plt
from collections import defaultdict
from abc import ABC,abstractmethod,abstractproperty

使用numpy搭建Mytorch体系

1.为ndarray加上导数矩阵

装饰在前向和反向传播中需要用到的成员方法 , 使参数在传递过程中始终拥有导数 装饰在前向和反向传播中需要用到的成员方法,使参数在传递过程中始终拥有导数 装饰在前向和反向传播中需要用到的成员方法,使参数在传递过程中始终拥有导数

'''对于拷贝传递返回值的成员方法,导数也拷贝生成'''
def add_grad(func):
    def inner(self,*args,**kwargs):
        ret=func(self,*args,**kwargs)
        ret.detach=False
        ret.grad=np.zeros(ret.shape)
        return ret
    return inner
'''对于引用传递返回值的成员方法,导数也取引用'''
'''
修改了add_grad_inplace,
之前的方法其实并没有让导数的内容和原数组一一对应,本篇未用到这个方法,
但是后续会更新的"手写CNN"里会用到
'''
def add_grad_inplace(func):
    def inner(self, *args, **kwargs):
        grad = self.grad
        ret = Tensor(func(self, *args, **kwargs))
        ret.grad = getattr(grad, func.__name__)(*args, **kwargs)
        return ret

    return inner

class Tensor(np.ndarray):
    '''传入元组会生成元组形状的随机矩阵,其余参数仅进行拷贝封装'''
    '''
	修改了__new__方法的if,else条件判断,代码更简洁和通用,注释掉的是修改前的
	'''
    def __new__(cls, input_array, requires_grad=True):
        if type(input_array) == tuple:
            obj = np.random.randn(*input_array).view(cls)
        else:
            obj = np.asarray(input_array).view(cls)
        obj.grad = np.zeros(obj.shape)
        return obj
#    def __new__(cls,input_array,requires_grad=True):
#        obj=np.asarray(input_array).view(cls) \
#        if type(input_array) in (list,Tensor,np.ndarray) \
#        else np.random.randn(*input_array).view(cls)
#        obj.grad=np.zeros(obj.shape)
#        return obj
    '''我一通乱装饰,实际上真正用到的没这么多,选择自己需要的进行装饰就好'''
    @add_grad
    def mean(self,*args,**kwargs):
        return super().mean(*args,**kwargs)
    @add_grad
    def std(self,*args,**kwargs):
        return super().std(*args,**kwargs)
    @add_grad
    def sum(self,*args,**kwargs):
        return super().sum(*args,**kwargs)
    @add_grad
    def __add__(self,*args,**kwargs):
        return super().__add__(*args,**kwargs)
    @add_grad
    def __radd__(self,*args,**kwargs):
        return super().__radd__(*args,**kwargs)
    @add_grad
    def __sub__(self,*args,**kwargs):
        return super().__sub__(*args,**kwargs)
    @add_grad
    def __rsub__(self,*args,**kwargs):
        return super().__rsub__(*args,**kwargs)
    @add_grad
    def __mul__(self,*args,**kwargs):
        return super().__mul__(*args,**kwargs)
    @add_grad
    def __rmul__(self,*args,**kwargs):
        return super().__rmul__(*args,**kwargs)
    @add_grad
    def __pow__(self,*args,**kwargs):
        return super().__pow__(*args,**kwargs)
    @add_grad
    def __rtruediv__(self,*args,**kwargs):
        return super().__rtruediv__(*args,**kwargs)
    @add_grad
    def __truediv__(self,*args,**kwargs):
        return super().__truediv__(*args,**kwargs)
    @add_grad
    def __matmul__(self,*args,**kwargs):
        return super().__matmul__(*args,**kwargs)
    @add_grad
    def __rmatmul__(self,*args,**kwargs):
        return super().__rmatmul__(*args,**kwargs)
    
    @add_grad_inplace
    def reshape(self,*args,**kwargs):
        return super().reshape(*args,**kwargs)     
    @add_grad_inplace
    def __getitem__(self,*args,**kwargs):
        return super().__getitem__(*args,**kwargs)
    
    @property
    def zero_grad_(self):
        self.grad=np.zeros(self.grad.shape)
        
'''定义网络所需的函数'''
'''
现在这种写法更加简便,其实受益于继承关系,直接Tensor(np.exp(x))也可以,
但是有风险会报错
'''
def exp(x):
    return Tensor(np.exp(np.array(x)))
def log(x):
    return Tensor(np.log(np.array(x)))

2.定义抽象模块

定义一个 M y T o r c h 抽象类 , 充当该体系一切类的基类 定义一个MyTorch抽象类,充当该体系一切类的基类 定义一个MyTorch抽象类,充当该体系一切类的基类

A b s N e t 表示网络主体模块,一切在前向传播中需要经历的模块都继承自该模块 AbsNet表示网络主体模块,一切在前向传播中需要经历的模块都继承自该模块 AbsNet表示网络主体模块,一切在前向传播中需要经历的模块都继承自该模块

A b s O p t i m i z e r 表示求解算法模块,一切基于梯度下降的求解算法都继承自该模块 AbsOptimizer表示求解算法模块,一切基于梯度下降的求解算法都继承自该模块 AbsOptimizer表示求解算法模块,一切基于梯度下降的求解算法都继承自该模块

A b s A c t i v a t i o n 是激活函数模块,一切激活函数 . . . . . . AbsActivation是激活函数模块,一切激活函数...... AbsActivation是激活函数模块,一切激活函数......

A b s M o d u l e 是网络参数模块 , 一切 . . . . . . AbsModule是网络参数模块,一切...... AbsModule是网络参数模块,一切......

A b s L o s s 是损失函数模块 , 一 . . . . . AbsLoss是损失函数模块,一..... AbsLoss是损失函数模块,.....

各个模块的继承关系如上面那个 M y t o r c h 架构图 各个模块的继承关系如上面那个Mytorch架构图 各个模块的继承关系如上面那个Mytorch架构图

class MyTorch(ABC):pass
class AbsNet(MyTorch):
    @abstractmethod
    def __init__(self,*args,**kwargs):pass
    @abstractmethod
    def __call__(self,*args,**kwargs):pass
    @abstractmethod
    def forward(self,*args,**kwargs):pass
    @abstractmethod
    def backward(self,*args,**kwargs):pass

class AbsActivation(AbsNet):
    def __init__(self,*args,**kwargs):pass
    @abstractmethod
    def function(self,*args,**kwargs):pass
    def __call__(self,x):
        self.input=x
        self.output=self.forward(x)
        return self.output
    @property
    def zero_grad_(self):
        if "input" in self.__dict__.keys():
            self.input.zero_grad_
            
class AbsOptimizer(MyTorch):
    @abstractmethod
    def __init__(self,*args,**kwargs):pass
    @abstractmethod
    def step(self,*args,**kwargs):pass
    def zero_grad(self):
        self.parameters.zero_grad_       

class AbsModule(AbsNet):
    @abstractproperty
    def zero_grad_(self):pass
    @abstractmethod
    def __repr__(self):pass
    
class AbsLoss(AbsNet):
    @abstractproperty
    def outgrad(self):pass
    @abstractmethod
    def forward(self,*args,**kwargs):pass
    def __call__(self,*args,**kwargs):
        return self.forward(*args,**kwargs)
    def backward(self):
        cgrad=self.outgrad
        for block_name,block in reversed(self.net.__dict__.items()):
            if type(block).__base__.__base__ is not AbsNet:
                continue
            cgrad=block.backward(cgrad)

3.定义“内置”模块

不同类型的模块需继承各自相应的抽象模块

定义内置模块需要继承 A b s M o d u l e , 并实现 _ _ r e p r _ _ 方法方便打印 , 同时实现导数清零方法 ( 因为参数网络不一样 ) 定义内置模块需要继承AbsModule,并实现\_\_repr\_\_方法方便打印,同时实现导数清零方法\\ (因为参数网络不一样) 定义内置模块需要继承AbsModule,并实现__repr__方法方便打印,同时实现导数清零方法(因为参数网络不一样)

定义内置激活函数需要继承 A b s A c t i v a t i o n , 并需要重载 _ _ i n i t _ _ , f o r w a r d 和 b a c k w a r d 定义内置激活函数需要继承AbsActivation,并需要重载\_\_init\_\_,forward和backward 定义内置激活函数需要继承AbsActivation,并需要重载__init__,forwardbackward

定义内置损失函数需继承 A b s L o s s , 不用重载 b a c k w a r d , 但要实现 o u t g r a d 方法 ( 即损失函数对最后输出的导数 ) 定义内置损失函数需继承AbsLoss,不用重载backward,但要实现outgrad方法 \\ (即损失函数对最后输出的导数) 定义内置损失函数需继承AbsLoss,不用重载backward,但要实现outgrad方法(即损失函数对最后输出的导数)

定义内置梯度下降算法需继承 A b s O p t i m i z e r , 并实现 s t e p 方法 定义内置梯度下降算法需继承AbsOptimizer,并实现step方法 定义内置梯度下降算法需继承AbsOptimizer,并实现step方法

(1)Linear (对应于 nn.Linear)

由于torch.nn.Linear参数初始化采用了kaiming_uniform_初始化,我们也这样初始化,其实就是从一个
U(-bound,bound) 的均匀分布中随机抽取

'''定义线性层'''
class Linear(AbsModule):
    def __init__(self,in_features,out_features,bias=True):
        self.in_features=in_features
        self.out_features=out_features
        self.bias=bias
        '''使用和torch.nn.Linear中一样参数初始化:参数a=sqrt(5),mode='fan_in'的kaiming_uniform_初始化'''
        bound=1/math.sqrt(in_features)
        
        self.parameters={"weight":Tensor((np.random.rand(in_features,out_features)-0.5)*2*bound)}
        if bias:
            self.parameters["bias"]=Tensor((np.random.rand(1,out_features)-0.5)*2*bound)
            
    def __call__(self,x):
        self.input=x
        self.output=self.forward(x)
        return self.output
    
    def forward(self,x):
        out=x @ self.parameters["weight"]
        if self.bias:
            out+=self.parameters["bias"]
        return out
    
    def backward(self,cgrad):
        try:
            self.input.grad= cgrad @ self.parameters["weight"].T
        except AttributeError:
            raise AttributeError("The layer: "+self.__repr__()+" absent from FP!")
        self.parameters["weight"].grad+= self.input.T @ cgrad
        if self.bias:
            self.parameters["bias"].grad+= cgrad.sum(0,keepdims=True)
        return self.input.grad.copy()
    
    def __repr__(self):
        return f"Linear(in_features={self.in_features}, "+\
        f"out_features={self.out_features}, bias={self.bias})"
    
    @property
    def zero_grad_(self):
        if "input" in self.__dict__.keys():
            self.input.zero_grad_
        self.parameters["weight"].zero_grad_
        if self.bias:
            self.parameters["bias"].zero_grad_
(2)激活函数 (对应于 nn.Sigmoid 和 nn.Tanh )
'''定义激活函数层,这里使用Sigmoid,Tanh'''
class Sigmoid(AbsActivation):
    def function(self,x):
        return 1/(1+exp(-x))
    
    def forward(self,x):
        return self.function(x)
    
    def backward(self,cgrad):
        assert self.output.shape==cgrad.shape,"Activation Sigmoid BP Error!"
        try:
            self.input.grad=(self.output*(1-self.output))*cgrad
        except (AttributeError):
            raise AttributeError("Layer: " +self.__repr__()+" absent from FP!")
        return self.input.grad
    
    def __repr__(self):
        return "Sigmoid()"
    
        
class Tanh(AbsActivation):
    def function(self,x):
        return (1-exp(-2*x))/(1+exp(-2*x))
    
    def forward(self,x):
        return self.function(x)
    
    def backward(self,cgrad):
        assert self.output.shape==cgrad.shape,"Activation Tanh BP Error!"
        try: 
            self.input.grad=(1-self.output**2)*cgrad
        except (AttributeError):
            raise AttributeError("Layer: " +self.__repr__()+" absent from FP!")
        return self.input.grad
    def __repr__(self):
        return "Tanh()"

(3) Module (对应于 nn.Module )

自定义网络只需继承 M o d u l e , 并只需实现 _ _ i n i t _ _ 和 f o r w a r d 即可 自定义网络只需继承Module,并只需实现\_\_init\_\_和forward即可 自定义网络只需继承Module,并只需实现__init__forward即可

'''定义模块层'''
class Module(AbsModule):
    def __init__(self,*args,**kwargs):
        raise NotImplementedError("Class: \"Module\" has to be overrided!")
        
    def __call__(self,*args,**kwargs):
        return self.forward(*args,**kwargs)
    
    def forward(self,*args,**kwargs):
        raise NotImplementedError("Function: \"forward()\" has to be overloaded!")
        
    def backward(self,cgrad):
        for block_name,block in reversed(self.__dict__.items()):
            if type(block).__base__ not in (AbsActivation,AbsModule,Module):continue
            cgrad=block.backward(cgrad)
        return cgrad
    def __repr__(self):
        name="Net(\n"
        for block_name,block in self.__dict__.items():
            if type(block).__base__ not in (AbsNet,AbsActivation,AbsModule,):
                continue
            name+="  ("+str(block_name)+"): "+block.__repr__()+"\n"
        return name+")"
    @property
    def zero_grad_(self):
        for block_name,block in self.__dict__.items():
            if type(block).__base__ not in (AbsActivation,AbsModule,Module):continue
            block.zero_grad_
(4) BCEWithLogitsLoss (对应于 nn.BCEWithLogitsLoss)

这个损失函数是BCELoss和Sigmoid激活的整合,其原理可以参考这篇博客, 其导数可以查看上面我给出的反向传播那张图,另外值得一提的是,其形式上和回归问题的平方损失函数是一样的,非常优美。

'''定义损失函数,这里实现二元交叉熵+ Sigmoid 损失函数'''
class BCEWithLogitsLoss(AbsLoss):
    def __init__(self,net,reduction="none"):
        self.net=net
        self.reduction=reduction
        self.function=Sigmoid()
        
    
    def forward(self,y,y_hat):
        self.out=y
        self.hat=y_hat
        p=self.function(y)
        ret=-(y_hat*log(p)+(1-y_hat)*log(1-p))
        if self.reduction=="mean":return ret.mean()
        elif self.reduction=="sum":return ret.sum()
        return ret
    
    @property
    def outgrad(self):
        out=self.out
        hat=self.hat
        out.grad=(self.function(out)-hat)/out.shape[0]
        return out.grad
(5) Mini_BGD(小批量梯度下降算法,类似于torch.optim.SGD,因为torch貌似未实现普通的BGD)
'''定义梯度下降算法,这里使用最普通的小批量梯度下降算法,其实和SGD区别只在于遍历数据集的方式'''
class Mini_BGD(AbsOptimizer):
    def __init__(self,net,lr=0.001):
        self.parameters=net
        self.lr=lr
    def step(self):
        for block_name,block in reversed(self.parameters.__dict__.items()):
            if type(block).__base__!=AbsModule:continue
            for name,weight in block.parameters.items():
                weight-=self.lr*weight.grad
(6) 定义一个非常厉害的优化器AdamW优化算法

我知道这东西放在这里可能有些突兀, 但 A d a m 系列优化器已经是训练 t r a n s f o r m e r 的主流优化器了,有必要提一下下, 大家跑这份代码的时候,注释掉随机数种子,对比我列出的这两种算法多跑几次, 就知道 A d a m W 有多厉害了 h h h . . . 我知道这东西放在这里可能有些突兀,\\ 但Adam系列优化器已经是训练 transformer的主流优化器了,有必要提一下下,\\ 大家跑这份代码的时候,注释掉随机数种子,对比我列出的这两种算法多跑几次,\\ 就知道AdamW有多厉害了hhh... 我知道这东西放在这里可能有些突兀,Adam系列优化器已经是训练transformer的主流优化器了,有必要提一下下,大家跑这份代码的时候,注释掉随机数种子,对比我列出的这两种算法多跑几次,就知道AdamW有多厉害了hhh...

这里给出它的公式 ( 从 t o r c h 源码复制粘贴的 ) : 这里给出它的公式(从torch源码复制粘贴的): 这里给出它的公式(torch源码复制粘贴的)

input : γ (lr) ,   β 1 , β 2 (betas) ,   θ 0 (params) ,   f ( θ ) (objective) ,   ϵ  (epsilon) λ (weight decay) ,   amsgrad ,   maximize initialize : m 0 ← 0  (first moment) , v 0 ← 0  ( second moment) ,   v 0 ^ m a x ← 0 for   t = 1   to   …   do if   maximize : g t ← − ∇ θ f t ( θ t − 1 ) else g t ← ∇ θ f t ( θ t − 1 ) θ t ← θ t − 1 − γ λ θ t − 1 m t ← β 1 m t − 1 + ( 1 − β 1 ) g t v t ← β 2 v t − 1 + ( 1 − β 2 ) g t 2 m t ^ ← m t / ( 1 − β 1 t ) v t ^ ← v t / ( 1 − β 2 t ) if   a m s g r a d v t ^ m a x ← m a x ( v t ^ m a x , v t ^ ) θ t ← θ t − γ m t ^ / ( v t ^ m a x + ϵ ) else θ t ← θ t − γ m t ^ / ( v t ^ + ϵ ) r e t u r n   θ t \begin{aligned} &\rule{110mm}{0.4pt} \\ &\textbf{input} : \gamma \text{(lr)}, \: \beta_1, \beta_2 \text{(betas)}, \: \theta_0 \text{(params)}, \: f(\theta) \text{(objective)}, \: \epsilon \text{ (epsilon)} \\ &\hspace{13mm} \lambda \text{(weight decay)}, \: \textit{amsgrad}, \: \textit{maximize} \\ &\textbf{initialize} : m_0 \leftarrow 0 \text{ (first moment)}, v_0 \leftarrow 0 \text{ ( second moment)}, \: \widehat{v_0}^{max}\leftarrow 0 \\[-1.ex] &\rule{110mm}{0.4pt} \\ &\textbf{for} \: t=1 \: \textbf{to} \: \ldots \: \textbf{do} \\ &\hspace{5mm}\textbf{if} \: \textit{maximize}: \\ &\hspace{10mm}g_t \leftarrow -\nabla_{\theta} f_t (\theta_{t-1}) \\ &\hspace{5mm}\textbf{else} \\ &\hspace{10mm}g_t \leftarrow \nabla_{\theta} f_t (\theta_{t-1}) \\ &\hspace{5mm} \theta_t \leftarrow \theta_{t-1} - \gamma \lambda \theta_{t-1} \\ &\hspace{5mm}m_t \leftarrow \beta_1 m_{t-1} + (1 - \beta_1) g_t \\ &\hspace{5mm}v_t \leftarrow \beta_2 v_{t-1} + (1-\beta_2) g^2_t \\ &\hspace{5mm}\widehat{m_t} \leftarrow m_t/\big(1-\beta_1^t \big) \\ &\hspace{5mm}\widehat{v_t} \leftarrow v_t/\big(1-\beta_2^t \big) \\ &\hspace{5mm}\textbf{if} \: amsgrad \\ &\hspace{10mm}\widehat{v_t}^{max} \leftarrow \mathrm{max}(\widehat{v_t}^{max}, \widehat{v_t}) \\ &\hspace{10mm}\theta_t \leftarrow \theta_t - \gamma \widehat{m_t}/ \big(\sqrt{\widehat{v_t}^{max}} + \epsilon \big) \\ &\hspace{5mm}\textbf{else} \\ &\hspace{10mm}\theta_t \leftarrow \theta_t - \gamma \widehat{m_t}/ \big(\sqrt{\widehat{v_t}} + \epsilon \big) \\ &\rule{110mm}{0.4pt} \\[-1.ex] &\bf{return} \: \theta_t \\[-1.ex] &\rule{110mm}{0.4pt} \\[-1.ex] \end{aligned} input:γ(lr),β1,β2(betas),θ0(params),f(θ)(objective),ϵ (epsilon)λ(weight decay),amsgrad,maximizeinitialize:m00 (first moment),v00 ( second moment),v0 max0fort=1todoifmaximize:gtθft(θt1)elsegtθft(θt1)θtθt1γλθt1mtβ1mt1+(1β1)gtvtβ2vt1+(1β2)gt2mt mt/(1β1t)vt vt/(1β2t)ifamsgradvt maxmax(vt max,vt )θtθtγmt /(vt max +ϵ)elseθtθtγmt /(vt +ϵ)returnθt

'''定义另一个非常厉害的学习率调度器AdamW优化算法'''
class AdamW(AbsOptimizer):
    def __init__(self,net,lr=0.01,betas=(0.9,0.999),eps=1e-08,weight_decay=0.01):
        self.parameters=net
        self.lr=lr
        self.betas=betas
        self.eps=eps
        self.weight_decay=weight_decay
        
        '''初始化t,mt,vt'''
        self.t=0
        self.mt=defaultdict(dict)
        self.vt=defaultdict(dict)
        for block_name,block in reversed(self.parameters.__dict__.items()):
            if type(block).__base__!=AbsModule:continue
            for name,weight in block.parameters.items():
                self.mt[block_name][name]=np.zeros_like(weight)
                self.vt[block_name][name]=np.zeros_like(weight)
    def step(self):
        beta1,beta2=self.betas
        self.t+=1
        for block_name,block in self.parameters.__dict__.items():
            if type(block).__base__!=AbsModule:continue
            for name,weight in block.parameters.items():
                gt=weight.grad
                mt=self.mt[block_name][name]
                vt=self.vt[block_name][name]
                
                weight-=self.lr*gt
                
                self.mt[block_name][name]=beta1*mt+(1-beta1)*gt
                self.vt[block_name][name]=beta2*vt+(1-beta2)*(gt*gt)
                
                mt=mt/(1-np.power(beta1,self.t))
                vt=vt/(1-np.power(beta2,self.t))
                
                weight-=self.lr*mt/(np.sqrt(vt)+self.eps)
(7) DataLoader (对应于torch.utils.data.DataLoader)
'''定义批量数据迭代器,使数据集可以按照小批量传入网络'''
class DataLoader:
    def __init__(self,dataset,batch_size):
        self.dataset=dataset
        self.batch_size=batch_size
        self.num=0
        self.stop=False
        self.final=False
        
    def __iter__(self):
        return self
    '''变量self.final使其可以反复迭代'''
    def __next__(self):
        if self.final==True:
            self.num=0
            self.final=False
            
        if not self.stop:
            bs=self.batch_size
            num=self.num
            
            self.num=min(self.num+bs,len(self.dataset))
            if self.num==len(self.dataset):self.stop=True
            return [Tensor(np.stack([self.dataset[i][j] 
                                     for i in range(num,self.num)]))
                    for j in range(2)]
        
        self.stop=False
        self.final=True
        raise StopIteration
        

定义数据集和本问题的网络

从这里开始可以对比torch代码查看,另外,Dataset 对应于torch.utils.data.Dataset

'''定义数据集'''
class Dataset:
    def __init__(self,data):
        self.data=data if type(data)==list else pickle.load(open(data,"rb"))
        
    def __getitem__(self,i):
        return \
    np.array(list(self.data[i][0])),np.array([self.data[i][1]],dtype=np.int32)
    
    def __len__(self):
        return len(self.data)

'''采用多层感知机,并使用tanh作为激活函数'''
class Net(Module):
    def __init__(self,in_dim):
        self.linear1=Linear(in_dim,5)
        self.tanh1=Tanh()
        
        self.linear2=Linear(5,3)
        self.tanh2=Tanh()
        
        self.linear3=Linear(3,1)
        
    def forward(self,x):
        out=self.linear1(x)
        out=self.tanh1(out)
        
        out=self.linear2(out)
        out=self.tanh2(out)
        
        out=self.linear3(out)
        
        return out

定义绘图,训练,预测函数

这里解释一下数据集,data_path是.pkl 文件,dots=pickle.load(open(data_path,“rb”))是一个列表,
其元素格式为 (x,y),L ,其中(x,y)是坐标, L 是类别并且只有0,1取值

def draw(data_pth):
    dots=pickle.load(open(data_pth,"rb"))
    
    dots0=[[dot[0][0],dot[0][1]] for dot in dots if dot[-1]==0]
    dots0x=[k[0] for k in dots0]
    dots0y=[k[1] for k in dots0]
    
    dots1=[[dot[0][0],dot[0][1]] for dot in dots if dot[-1]==1]
    dots1x=[k[0] for k in dots1]
    dots1y=[k[1] for k in dots1]
    
    plt.scatter(dots0x,dots0y,c="g")
    plt.scatter(dots1x,dots1y,c="b")


def train(net,dataloader,epochs,lr,eps=1e-5):
    '''选择优化器(两个都可以)'''
#     optimizer=Mini_BGD(net,lr=lr)
    optimizer=AdamW(net,lr=lr)
    '''选择损失函数'''
    l=BCEWithLogitsLoss(net,reduction="mean")
    
    loss_lst=[100]
    '''开始遍历epochs次数据集'''
    for _ in range(epochs):
        '''对每次遍历取出小批量(这是必要的,尤其在数据量很大的时候)'''
        for x,y in dataloader:
            '''前向传播'''
            y_pre=net(x)
            '''计算损失函数'''
            loss=l(y_pre,y)
            '''记录误差'''
            loss_lst+=[loss]
            '''反向传播求导数'''
            l.backward()
            '''更新参数'''
            optimizer.step()
            '''导数清零'''
            optimizer.zero_grad()
            
        '''误差足够低时退出'''
        if abs(loss_lst[-1]-loss_lst[-2])<eps:
            break
            
    '''输出训练信息'''
    print("Update times:",len(loss_lst))
    print("Final_Loss:",loss_lst[-1])
    
    plt.xlabel("update_times")
    plt.ylabel("loss")
    plt.plot(loss_lst)
    plt.show()

    
    
def predict(net,test_dataset,trn_path=False,eps=0.001):
    '''初始化真阳,真阴,假阳,假阴'''
    TP,TN,FP,FN=0,0,0,0
    ALL=len(test_dataset)
    datas=[[[],[]],[[],[]]]
    
    
    for i in range(ALL):
        dot,l=test_dataset[i]
        dot=dot[None,:]
        l=l[None,:]
        out=net(dot)[0][0]
        pre=0 if out <0 else 1
        
        datas[pre][0]+=[dot[0][0]]
        datas[pre][1]+=[dot[0][1]]
        
        if l==1:
            if out<0:FP+=1
            else:TP+=1
        else:
            if out<0:TN+=1
            else: FN+=1
    if trn_path:
        draw(trn_path)
        
    '''绘出预测情况,黄色为预测负类,红色为预测正类'''
    plt.scatter(datas[0][0],datas[0][1],c="y")
    plt.scatter(datas[1][0],datas[1][1],c="r")
    plt.show()
    '''精确率'''
    print("Precision:\t",(TP+eps)/(TP+FP+eps))
    '''召回率'''
    print("Recall:\t",(TP+eps)/(TP+FN+eps))
    '''准确率'''
    print("Accuracy:\t",(TP+TN+eps)/(TP+TN+FP+FN+eps))
        

训练

if __name__=="__main__":
	'''载入训练集和测试集'''
	trn_dataset=Dataset("trn_datas.pkl")
	tst_dataset=Dataset("tst_datas.pkl")
	
	'''设置随机数种子以便复现'''
	np.random.seed(0)
	
	net=Net(in_dim=2)
	dataloader=DataLoader(trn_dataset,batch_size=50)
	
	'''查看模型结构'''
	print(net)
	
	train(net,dataloader,epochs=200,lr=0.001)
		

结果:

Net(
(linear1): Linear(in_features=2, out_features=5, bias=True)
(tanh1): Tanh()
(linear2): Linear(in_features=5, out_features=3, bias=True)
(tanh2): Tanh()
(linear3): Linear(in_features=3, out_features=1, bias=True)
)

Update times: 569
Final_Loss: 0.28847184152795263
请添加图片描述

验证

由于本任务简单,采用训练集来验证 ( 仅因以此来绘图比较简单, 实际上验证集不应与训练集和测试集相交 ) 由于本任务简单,采用训练集来验证(仅因以此来绘图比较简单,\\ 实际上验证集不应与训练集和测试集相交) 由于本任务简单,采用训练集来验证(仅因以此来绘图比较简单,实际上验证集不应与训练集和测试集相交)

predict(net,trn_dataset)

结果:
【注】红色为预测正类,黄色为预测负类, 并且由于数据集含有异常点,这是最高准确率了

请添加图片描述
Precision: 0.9404768990845347
Recall: 0.9404768990845347
Accuracy: 0.9428574693858892

测试

绘出原始训练数据集 , 蓝色为样本正类,绿色为样本负类 绘出原始训练数据集,蓝色为样本正类,绿色为样本负类 绘出原始训练数据集,蓝色为样本正类,绿色为样本负类

draw("trn_datas.pkl")

结果: 结果: 结果:

请添加图片描述

在测试集上进行预测 , 并与原始训练集绘在同一张图上 在测试集上进行预测,并与原始训练集绘在同一张图上 在测试集上进行预测,并与原始训练集绘在同一张图上

predict(net,tst_dataset,trn_path="trn_datas.pkl")

结果: 结果: 结果:

请添加图片描述
Precision: 1.0
Recall: 1.0
Accuracy: 1.0

可见其全部正确 可见其全部正确 可见其全部正确

附录


代码和数据集地址(就是本篇开头链接):链接

这里给出torch版代码:

#!/usr/bin/env python
# coding: utf-8
#author:iiGray

import torch
from torch import nn
from torch.utils import data
import pickle
import matplotlib.pyplot as plt


class Dataset(data.Dataset):
    def __init__(self,data):
        self.data=data if type(data)==list else pickle.load(open(data,"rb"))
    def __getitem__(self,i):
        return \
    torch.FloatTensor(list(self.data[i][0])),torch.FloatTensor([self.data[i][1]])
    def __len__(self):
        return len(self.data)


class Net(nn.Module):
    def __init__(self,in_dim):
        super().__init__()
        
        self.linear1=nn.Linear(in_dim,5)
        self.tanh1=nn.Tanh()
        
        self.linear2=nn.Linear(5,3)
        self.tanh2=nn.Tanh()
        
        self.linear3=nn.Linear(3,1)
        
    def forward(self,x):
        out=self.linear1(x)
        out=self.tanh1(out)
        
        out=self.linear2(out)
        out=self.tanh2(out)
        
        out=self.linear3(out)
        
        return out


def draw(data_pth):
    dots=pickle.load(open(data_pth,"rb"))
    dots0=[[dot[0][0],dot[0][1]] for dot in dots if dot[-1]==0]
    dots0x=[k[0] for k in dots0]
    dots0y=[k[1] for k in dots0]
    dots1=[[dot[0][0],dot[0][1]] for dot in dots if dot[-1]==1]
    dots1x=[k[0] for k in dots1]
    dots1y=[k[1] for k in dots1]
    plt.scatter(dots0x,dots0y,c="g")
    plt.scatter(dots1x,dots1y,c="b")


def train(net,dataloader,epochs,lr,eps=1e-5):
    
#     optimizer=torch.optim.SGD(net.parameters(),lr=lr)
    optimizer=torch.optim.AdamW(net.parameters(),lr=lr)
    l=nn.BCEWithLogitsLoss()
    
    loss_lst=[100]
    for _ in range(epochs):
        for x,y in dataloader:
            
            y_pre=net(x)
            
            loss=l(y_pre,y)
            
            loss.backward()
            
            with torch.no_grad():
                ls=loss.item()
                
            loss_lst+=[ls]
            
            optimizer.step()
            
            optimizer.zero_grad()
            
        if abs(loss_lst[-1]-loss_lst[-2])<eps:
            break
    
    print("Update times:",len(loss_lst))
    print("Final_Loss:",loss_lst[-1])
    
    plt.xlabel("update_times")
    plt.ylabel("loss")
    plt.plot(loss_lst)
    plt.show()
    
def predict(net,test_dataset,trn_path=False,eps=0.001):
    TP,TN,FP,FN=0,0,0,0
    ALL=len(test_dataset)
    datas=[[[],[]],[[],[]]]
    
    
    for i in range(ALL):
        dot,l=test_dataset[i]
        dot=dot[None,:]
        l=l[None,:]
        out=net(dot)[0][0]
        pre=0 if out <0 else 1
        
        datas[pre][0]+=[dot[0][0]]
        datas[pre][1]+=[dot[0][1]]
        
        if l==1:
            if out<0:FP+=1
            else:TP+=1
        else:
            if out<0:TN+=1
            else: FN+=1
    if trn_path:
        draw(trn_path)
        

    plt.scatter(datas[0][0],datas[0][1],c="y")
    plt.scatter(datas[1][0],datas[1][1],c="r")
    plt.show()
    
    print("Precision:\t",(TP+eps)/(TP+FP+eps))
    
    print("Recall:\t",(TP+eps)/(TP+FN+eps))
    
    print("Accuracy:\t",(TP+TN+eps)/(TP+TN+FP+FN+eps))



if __name__=="__main__":
    trn_dataset=Dataset("trn_datas.pkl")
    tst_dataset=Dataset("tst_datas.pkl")


    torch.manual_seed(0)
    
    net=Net(in_dim=2)
    dataloader=data.DataLoader(trn_dataset,batch_size=50)
    
    print(net)

    train(net,dataloader,epochs=200,lr=0.001)

    predict(net,trn_dataset)

    draw("trn_datas.pkl")

    predict(net,tst_dataset,"trn_datas.pkl")

运行结果(依次显示):

Net(
(linear1): Linear(in_features=2, out_features=5, bias=True)
(tanh1): Tanh()
(linear2): Linear(in_features=5, out_features=3, bias=True)
(tanh2): Tanh()
(linear3): Linear(in_features=3, out_features=1, bias=True)
)

Update times: 801
Final_Loss: 0.28874489665031433
请添加图片描述

请添加图片描述
Precision: 0.9404768990845347
Recall: 0.9404768990845347
Accuracy: 0.9428574693858892

请添加图片描述

请添加图片描述
Precision: 1.0
Recall: 1.0
Accuracy: 1.0

结果是一样滴,不过大家发现没有,似乎我们手搓出来(Updatetimes=569)的比torch(Updatetimes=801)收敛更快,我也不知为什么,我觉得是更可能是巧合,但也可能某个地方没注意到吧,欢迎大家指出错误。

尾注

该数据集是代码随机生成的,具体过程见 𝑔𝑒𝑛𝑒𝑟𝑎𝑡𝑒_𝑑𝑎𝑡𝑎 文件(github里),为了更专注于探索底层的逻辑,这份数据的训练很简单,基本可以预测全对,但要想让训练集精确率和召回率同时达到 93% 以上, 会稍有难度. 同时,本篇未考虑过拟合等问题.

最后的最后,这份代码只是我在学习过程中帮助理解的练习代码,本来只是图一乐hhh,但后来越发觉得是一份很宝贵的资料,因此决定整理一下,既进行了巩固,又能够分享给大家。
而其中的设计在大佬们看来可能会有不合理的地方,毕竟我写的时候也没有考虑到太多因素,也许有些地方会有笔误,不过欢迎大家交流!

【更新与勘误】
2023.10.1: 在 定义“内置”模块 部分下方添加了补充说明
2023.10.3: 更新了为ndarray加上导数 部分内容, 纠正了 add_grad_inplace的写法(本篇未用到这个,可以忽略),简化了Tensor.__new__ 方法的判断和exp,log的实现,删除了一些本篇未用到的无关函数,后续篇章若用到会特别说明

这里顺便解释一下为什么 为了实现Tensor,要继承ndarray,而不是让data和grad分别作为Tensor的两个成员变量,因为不继承的话,像shape,__len__这些属性或方法都需要重写或装饰,不如直接继承再附带一个导数方便。

2023.10.9 更新:将BCEWithLogitsLoss 的 __call__方法放在了其父类AbsLoss里, 后面实现其他损失函数不必再重写__call__方法,反向传播例子 图有误 (最后一层求导应该是 ∂ L ∂ O \cfrac{\partial L}{\partial O} OL)已更新,代码没问题

2023.11.9
注释:
1.手搓部分代码运行环境为py3.9,py3.7及以下版本可能不支持 reversed(dict_items)的语法,这么写是为了简便写法,如果语法不支持,也可以转成列表手动反向遍历,但是需要注意必须保证遍历时取出的是对象的引用,而不是对象的拷贝,否则参数更新时就会更新对象的拷贝而不是对象本身,相当于没有更新参数了

2.之前忘了提及,大家如果了解过pytorch,应该知道tensor反向传播时,应当传入一个与它形状相同的导数矩阵,这个矩阵作为误差函数从后面传过来的导数,而我们知道tensor的导数和它本身形状相同,因此在pytorch的tensor反向传播时,当前tensor ( 记为 A ) 的导数是通过它后面的那个 tensor( 记为 B ) 的backward()方法算出来的,因此 B 在backward时,必须知道 A 是如何运算得到 B 的,而我们知道tensor有一个grad_fn属性,正是用来表示前一个tensor通过什么运算得到当前tensor的, 本篇手搓代码为了简洁,让每个 ‘tensor’ 的 backward()都求它自己的导数而不是它的前一个‘tensor’的导数,这样就不必记录grad_fn了

2023.12.12
3.torch里tensor的反向传播和优化器的参数更新是分离的,通过将tensor外套一个nn.Parameter,将tensor转成可以被优化器更新的网络参数,只有Parameter类型的参数可以被网络更新,这样就解决了“如果想要在网络里使用tensor又不想被更新怎么办”的问题,而本篇处于简洁考虑,没有实现Parameter, 后续有机会会实现

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值