今天将和大家一起学习 LLM101n 课程中 Micrograd 部分,该部分是Andrej Karpathy 构建的一个轻量级的自动微分引擎,他通过手动实现梯度计算和反向传播来训练简单的神经网络。该项目的主要目标是帮助理解自动微分和神经网络训练的基本原理。
https://github.com/EurekaLabsAI/micrograd
https://github.com/SmartFlowAI/LLM101n-CN/tree/master/
micrograd
阅读 Tips:阅读本文需要准备一些高数微积分知识。
引言
micrograd 项目展示了如何从头开始构建一个简单的自动求导引擎,并使用它来训练一个简单的神经网络。这不仅是对深度学习基础的一个很好的介绍,也是对自动求导机制的深入理解。micrograd 的代码是神经网络训练的核心 - 它使我们能够计算如何更新神经网络的参数,以使其在某些任务上表现更好,例如自回归语言模型中的下一个标记预测。所有现代深度学习库(例如 PyTorch、TensorFlow、JAX 等)都使用完全相同的算法,只是这些库更加优化且功能丰富。下面我们开始一步步开始对代码解读。
代码目录结构树
micrograd |-- README.md |-- utils.py |-- micrograd.py |-- micrograd_pytorch.py
工具封装:自定义的随机数生成接口RNG类 和 随机数生成方法gen_data()
自动微分引擎的核心代码,本文重点解读对象
与
micrograd.py
的功能相同,但它使用了PyTorch的自动微分引擎。这主要是为了验证和确认代码的正确性,同时也展示了PyTorch实现相同多层感知器(MLP)的一些相似性和差异性。这块代码比较简单,不作过多解读。
utils.py代码解读
utils.py 主要封装了两个小工具:
# class that mimics the random interface in Python, fully deterministic, # and in a way that we also control fully, and can also use in C, etc. class RNG : def __init__ (self, seed) : self.state = seed def random_u32 (self) : # xorshift rng: https://en.wikipedia.org/wiki/Xorshift#xorshift.2A # doing & 0xFFFFFFFFFFFFFFFF is the same as cast to uint64 in C # doing & 0xFFFFFFFF is the same as cast to uint32 in C self.state ^= (self.state >> 12 ) & 0xFFFFFFFFFFFFFFFF self.state ^= (self.state << 25 ) & 0xFFFFFFFFFFFFFFFF self.state ^= (self.state >> 27 ) & 0xFFFFFFFFFFFFFFFF return ((self.state * 0x2545F4914F6CDD1D ) >> 32 ) & 0xFFFFFFFF def random (self) : # random float32 in [0, 1) return (self.random_u32() >> 8 ) / 16777216.0 def uniform (self, a=0.0 , b=1.0
) : # random float32 in [a, b) return a + (b-a) * self.random()
这段代码定义了一个名为
RNG
的类,它模拟了 Python 的随机数生成接口,但它是完全确定性的(如果使用相同的种子初始化),并且我们可以完全控制它,方便我们重复调试。
random_u32
方法生成的随机数是 32 位无符号整数。
random
方法生成的随机数是浮点数,范围在
[0, 1)
。
uniform
方法生成的随机数是浮点数,范围在
[a, b)
。
def gen_data (random: RNG, n=100 ) : # 初始化一个空列表来存储数据点 pts = [] for _ in range(n): # 在范围[-2.0, 2.0]内生成随机的x和y坐标 x = random.uniform(-2.0 , 2.0 ) y = random.uniform(-2.0 , 2.0 ) # 生成同心圆的数据标签 # label = 0 if x**2 + y**2 < 1 else 1 if x**2 + y**2 < 2 else 2 # 生成非常简单的数据集 label = 0 if x < 0 else 1 if y < 0 else 2 # 将坐标和标签作为一个元组添加到数据点列表中 pts.append(([x, y], label)) # 创建训练/验证/测试数据集,按80%、10%、10%比例划分 tr = pts[:int(0.8 *n)] val = pts[int(0.8 *n):int(0.9 *n)] te = pts[int(0.9 *n):] # 返回训练集、验证集和测试集 return tr, val, te
该函数主要用于生成一个简单的二维数据集,用于机器学习模型的训练、验证和测试。通过调整标签生成逻辑,可以生成不同类型的数据集,如同心圆数据集或简单的一维数据集。
通过以上解读,读者可以更好地理解微梯度项目的实现细节和工作原理。
micrograd.py 代码解读
我们可以将其逻辑结构概括如下:
cross_entropy
函数:定义交叉熵损失函数。
Value 类
Value
类是整个自动求导引擎的核心。它存储一个标量值及其梯度,并定义了基本的数学运算。
Value 类构造函数
class Value : """ stores a single scalar value and its gradient """ def __init__ (self, data, _children=() , _op='' ) : self.data = data # 存储数值 self.grad = 0 # 存储梯度 # internal variables used for autograd graph construction self._backward = lambda : None # 反向传播函数 self._prev = set(_children) # 前驱节点集合 self._op = _op # 操作类型,用于调试和可视化
Value类的目的是在自动微分系统中表示计算图中的一个节点。每个节点包含数据、梯度、前驱节点和操作类型。通过这些信息,可以构建计算图,并在需要时进行自动微分和反向传播。
Value 类数学运算
def __add__ (self, other) : #重载 `+` 运算符,实现两个 `Value` 对象或一个 `Value` 对象和一个标量值的加法。 other = other if isinstance(other, Value) else Value(other) #创建一个新的 `Value` 对象 `out`,其值为两个输入值的和。 out = Value(self.data + other.data, (self, other), '+' ) #定义 `_backward` 函数,用于反向传播,更新输入值的梯度。 def _backward () : self.grad += out.grad other.grad += out.grad out._backward = _backward #返回新的 `Value` 对象 `out`。 return out
加法梯度计算
在微积分中,加法的梯度是一个简单的线性操作。对于两个标量a 和 b,它们的和 (c = a + b) 的梯度可以表示为:
这意味着,当我们对 c求导时,c相对于 a和 b的变化率都是1。无论 a或 b变化多少,c的变化等于 a和 b的变化之和。因此,在计算 c的梯度时,我们需要将 a和 b的梯度都加到 c的梯度上。
链式法则
在微积分中,链式法则告诉我们,如果 f(x) 和 g(x) 是两个函数,那么 h(x) = f(g(x)) 的梯度可以表示为:
在上述代码中,
out
是
a + b
的结果,
self
和
other
是
a
和
b
。
out.grad
是
out
的梯度,
self.grad
和
other.grad
是
a
和
b
的梯度。因此,
self.grad += out.grad
other.grad += out.grad
是根据加法梯度和链式法则计算
a
和
b
的梯度。
def __mul__ (self, other) : other = other if isinstance(other, Value) else Value(other) out = Value(self.data * other.data, (self, other), '*' ) def _backward () : self.grad += other.data * out.grad other.grad += self.data * out.grad out._backward = _backward return
out
乘法梯度计算
在微积分中,乘法的梯度是一个基本的二元操作。对于两个标量 a和 b,它们的积 c=a×b 的梯度可以表示为:
和
当我们计算 c的梯度时,a和 b的变化会影响 c,并且影响的大小分别是 b和 a。
因此,根据链式法则当我们计算 c的梯度时,我们需要将 b乘以c的梯度加到 a的梯度上,反之亦然。
所以:self.grad += other.data * out.grad other.grad += self.data * out.grad
def __pow__ (self, other) : assert isinstance(other, (int, float)), "only supporting int/float powers for now" out = Value(self.data**other, (self,), f'**{other} ' ) def _backward () : self.grad += (other * self.data**(other-1 )) * out.grad out._backward = _backward return out
幂运算的梯度规则基于以下公式:
对于一个标量 a和一个指数 b,它们的幂
的梯度为:
当我们计算 c的梯度时,a的变化会影响 c,并且影响的大小是
所以 self.grad += (other * self.data**(other-1)) * out.grad
def relu (self) : out = Value(0 if self.data < 0 else self.data, (self,), 'ReLU' ) def _backward () : self.grad += (out.data > 0 ) * out.grad out._backward = _backward return out
这段代码定义了一个名为
relu
的方法,用于实现ReLU(Rectified Linear Unit)激活函数。ReLU是一种常用的神经网络激活函数,其公式为:
,即对于输入 ( x ),如果 ( x ) 大于0,则输出 ( x );如果 ( x ) 小于等于0,则输出0。
relu
方法通常用于神经网络的前向传播和反向传播过程中。在前向传播中,它将输入数据通过ReLU函数进行激活,得到新的输出值。在反向传播中,它根据ReLU函数的特性,正确地计算并传递梯度。
梯度计算
在反向传播过程中,梯度是通过将
out.data > 0
与
out.grad
相乘,然后加到
self.grad
上来累积的。这意味着
self.grad
应该初始化为0,否则累积的梯度会不正确。
ReLU函数的反向传播需要遵循链式法则,即如果
out
是多个节点的输出,那么每个依赖
out
的节点都需要调用
out._backward()
来计算其梯度。
def tanh (self) : out = Value(math.tanh(self.data), (self,), 'tanh' ) def _backward () : self.grad += (1 - out.data**2 ) * out.grad out._backward = _backward return out
tanh
方法,用于计算一个张量(tensor)的
tanh
激活函数值,并返回一个新的张量,同时支持自动求导。
这个方法主要用于深度学习中的神经网络计算。在神经网络中,激活函数(如
tanh
)用于引入非线性,帮助模型更好地拟合数据。通过自动求导,可以方便地计算梯度,用于参数更新。
梯度计算
tanh
函数的梯度是
。这个公式可以用来计算
tanh
函数在任意点 ( x ) 的导数。
根据链式法则,self.grad += (1 - out.data**2) * out.grad
def exp (self) : out = Value(math.exp(self.data), (self,), 'exp' ) def _backward () : self.grad += math.exp(self.data) * out.grad out._backward = _backward return out
exp
方法,用于计算一个数值的指数函数值:
,并返回一个新的
Value
对象。这个方法通常用于自动微分系统中,用于计算梯度。
梯度计算
exp
函数的梯度是:
在反向传播过程中,我们需要计算指数函数相对于其输入的梯度,并将其存储到
self.grad
中:
self.grad += math.exp(self.data) * out.grad
def log (self) : # (this is the natural log) out = Value(math.log(self.data), (self,), 'log' ) def _backward () : self.grad += (1 /self.data) * out.grad out._backward = _backward return out
log
方法,用于计算一个数值的自然对数(即以 e 为底的对数)
。通常用于自动微分系统中,特别是在实现神经网络或其他需要计算梯度的算法时。通过定义
log
方法,可以方便地计算一个数值的自然对数,并自动计算其梯度,这对于反向传播算法至关重要。
梯度计算
log 函数的梯度是:
在反向传播过程中,利用自然对数函数的导数公式,将梯度正确传递回前一个节点。
self.grad += (1/self.data) * out.grad
Value
类重载了一些常见的运算符,包括加法、减法、乘法、除法以及取负操作。这些运算符的重载使得我们可以使用自然的数学表达式来操作
Value
对象,从而使代码更加简洁和易读。
def __neg__ (self) : # -self # 返回self乘以-1的结果,实现取负操作 return self * -1 def __radd__ (self, other) : # other + self # 返回self加上other的结果,实现反向加法