building micrograd
从零搭建 micrograd:神经元、权重、bias、激活函数(ReLU)与反向传播的直观解释,配可交互函数图像。
1. 项目简介
micrograd 是 Andrej Karpathy 所开源的一个 GitHub 学习项目,项目的初衷是让读者可以更好地学习 back propagation。Andrej Karpathy 本人也录制过一段 YouTube 视频来进行讲解(url: https://youtu.be/VMj-3S1tku0?si=6qfc4FQkdIn_WmnL),那么为了理解 micrograd,我们需要先从最基础的神经网络开始讲起。
2. 神经网络基础
2.1 神经元与权重
神经元就是一个 $f(x_1,x_2,x_3,...)$,一个函数,输入了一堆数字。
然后每个 input 都会跟着一个 weight 权重,就是,最主要是一个常数。
高就代表这个输入很重要;低就代表没那么重要。
当然也要看正负,可以看出是负影响,还是正影响。
2.2 神经元的基本结构
基本结构:
2.3 bias 的作用
这里可以看到,在刚刚输入 × 权重后,还要加一个 bias。
而 bias 的作用是什么?为什么 + 一个 bias 呢?
bias 从几何意义上来理解:
c 也就是代表着这条线可以在整个空间里面平移。
机器学习,最主要是学习数据关系,也就是输入和输出的对应关系。
如果不允许平移,那么,如果真实数据里面是 $y = 2x + 100$,在输入永远 = 0 的时候,如果没有 bias,永远拟合不了线性的真实数据。
而我们最终的目的也就是,确保 x 轴上每一个 x,只要调整 w 和 bias,可以触及任意的 y。
因为我们要拟合世界上任意的对应关系,任何的数据形态。
2.4 Activation function 与 ReLU
但是单单只有一条直线,是不够的,所以我们要引入 非线性。
也就是 Activation function(激活函数)。
我们先介绍第一种 Activation function,ReLU:
其实也就是创造一个折点,在几何意义上。
2.5 神经网络的连接结构
那我们现在画一个最简单的神经网络,长这样(2 个输入、3 个隐藏神经元、1 个输出):
每条连线就是一个权重 w,每个神经元做的是加权求和,再过一个 Activation function。
2.6 线性变换的局限
为什么一定要非线性?
如果每个神经元都只是线性加权求和,那不管叠多少层,组合起来还是一条直线:
拖动左边 m、b 的滑条,怎么调都只是一条直线,永远拟合不了 $\sin(x)$。
2.7 多个 ReLU 的组合
但是只要加上 ReLU 这种非线性,把几个 ReLU 加起来,曲线就开始拐弯了:
拖动 $c_1$ 到 $c_4$ 的滑条,可以看到分段线性的曲线可以一点点逼近 $\sin(x)$。
神经元的数量越多,拐弯的点越多,就能逼近越复杂的函数。
这就是为什么神经网络一定要加 Activation function。
3. micrograd 与 backpropagation
简单介绍完基本的神经网络结构,就可以来说一下 micrograd 了。
3.1 autograd engine
micrograd 是一个 autograd engine,就是自动梯度的机器。
- 也就是 Implement backpropagation,向后传播。
3.2 从输入、输出到 Loss function
backpropagation 向后传播解释
整个 Neural Network 的目的,例如说,我们想要这个神经网络去拟合一种数据分布,对吧?
首先整个的流程就是:
Feed input -> Process -> Output
然后看看结果如何。
那如何衡量结果?我们就需要 loss function。
有了 loss function 之后,知道做得好不好了。
我们就需要去改良(也就是去调整权重和 bias)。
3.3 Mean Squared Error(MSE)
举出一个简单的例子:
Mean Squared Error - MSE
3.4 gradient 与 chain rule
有了 Loss function 之后,我们的步骤就到了如何改进。
那么改进,需要 gradient(梯度)。
梯度的意思就是在那一变量的对整个函数的影响,就是,如果增加一个无穷小量 $dx$,如何影响整个函数。
让 $L$ 作为 Loss function。
$\frac{dL}{dL}$ 是 1。
只要用微积分的法则,例如多变量微积分是将其他的变量当成常数,嵌套函数就是 chain rule,就可以算出某个具体变量的梯度。
3.5 学习率与最小化 Loss
知道梯度是 $x$ 增加一个 $dx$,$y$ 的上涨。
那么为了最小化 $L$,可以学习率乘一个负导数。
因为 $x$ 如果 $+dx$ 是增加 $L$,那么 $-dx$ 就是减少 $L$,这样可以让 Loss function 变小。
而学习率就是变小多少,防止直接跳过了 minimum。
4. Python 与 Value 类的实现
实践
在 Python 里面,我们最主要使用 OOP(Object-Oriented Programming)。
我们要从最小的部分开始(分治法)。
4.1 初始化:__init__
创造一个名为 Value 的 class。
|
在初始化一个 Value 的时候,它就是类似一种新的 data type,是用来装数值的。
4.2 加法:__add__
并且我们要定义加法减法,等等,让 Python 可以识别我们对于这种自创的 class 的操作。
|
这里 isinstance 就是判断 other 是不是 Value 这个类的,不是我们就转换成 Value,我们这个自定义的 class。
Value 的第二个参数就是提供一个可以追溯的,因为我们最后要做 Back propagation,所以就是我们会需要我之前做过每个操作的 history。
这里的 backward 也很容易理解,就是基本的微积分。
|
那么如何算 dx/da?那就是 treat other as constant。
所以是:
那 out.grad 是怎来?因为我们最终是要 dL/da。
所以 out.grad 就是 dL/dx * dx/da = dL/da,所以 dL/da = 1 * dL/dx,这里的 dL/dx 就是 out.grad。
4.3 对象表示:__repr__
|
纯粹是,我们如果直接 call x 的时候,会出现什么。
4.4 乘法:__mul__
|
mul 也是和 add 一样,不过转成了乘法。
还是推理一下:
|
4.5 取负:__neg__
|
这里是 negative,也就是 -self。
4.6 减法:__sub__
|
这里 subtract 的话就不用再写一遍了,直接套用 + 的那个方法就行。
4.7 反向减法:__rsub__
|
定义 other - self。
4.8 反向乘法:__rmul__
|
4.9 反向加法:__radd__
|
4.10 激活函数:tanh
|
然后是 tanh 这个方法,就是加个 activation function,引入非线性,稍微推理一下:
|
wiki : https://en.wikipedia.org/wiki/Hyperbolic_functions
tanh(a) 的导数 wiki 有,所以是 1 - tanh(x)^2。tanh 又是由 $e$ define 的,所以我们从 math lib 引用一些函数用来组成 tanh,当然这里转 ReLU 那些都行。
4.11 然后就是最重要的方法:backward
|
backward 首先我们创造了 topo 一个 list 和 visited 一个 set。为什么要用 set?首先我们需要获取所有的 child,也就是需要做 gradient 的 child,set 是不重复的,收录所有的 child,可以确保 child 只看了一次。
然后我们就开始进行 topological order,也就是从 output 开始,最顶端 output 的 child,再到 output child 的 child(这里使用了 recursion function calling itself)。
例如我 x = a + b,那么 topo 里面会有 a, b, x。
topo 里面是有 order 的,set 是无 order,并且只记录一次的(确保所有 node 遍历过)。
因为它是处理完 child 之后才会处理自己,topo.append(v) 在 for loop 后面,然后 for loop 里面有 recursion,是处理 for loop 里面的 recursion 先,然后就导致我们的 list,x 是在最后的。
然后我们因为要从最顶端开始,所以是 reversed,从顶端的 output,然后因为 node gradient 已经有了,我们定义过 dL/dL = 1,然后利用 local derivative + chain rule,计算 child 的 gradient,这样。
这里在做的是 topological sort,因为整个 back propagation 是有向无环图 DAG (Directed Acyclic Graph)。
以上就是整个 engine 的代码解析。
5. 那么我们如何制作一个最简单的神经网络来进行 back propagation 呢?
|
制作最基础的神经元
|
|
我们需要可以弄一个每个 layer 几层几层,因为可能我们最后只需要输出一个值这样。
|
|
|
|