专栏名称: GiantPandaCV
专注于机器学习、深度学习、计算机视觉、图像处理等多个方向技术分享。团队由一群热爱技术且热衷于分享的小伙伴组成。我们坚持原创,每天一到两篇原创技术分享。希望在传播知识、分享知识的同时能够启发你,大家一起共同进步(・ω<)☆
目录
相关文章推荐
似水之流年  ·  你那里桃花开了吗? ·  昨天  
似水之流年  ·  你那里桃花开了吗? ·  昨天  
知乎日报  ·  好莱坞电影有哪些常见套路? ·  昨天  
出彩写作  ·  起草调研报告,关键在动静结合摸实情 ·  2 天前  
51好读  ›  专栏  ›  GiantPandaCV

LLM101n 硬核代码解读:Micrograd,一个轻量级的自动微分引擎

GiantPandaCV  · 公众号  ·  · 2024-08-06 21:00

主要观点总结

本文介绍了HandyLLM101n,这是由OpenAI联合创始人、计算机视觉教母李飞飞教授的高徒Andrej Karpathy推出的AI课程。文章详细解读了微梯度项目,该项目是一个轻量级的自动微分引擎,展示了如何从头开始构建一个简单的自动求导引擎,并使用它来训练一个简单的神经网络。项目的主要目标是帮助理解自动微分和神经网络训练的基本原理。原始代码仓库地址和中文共建仓库地址也提供在文中。文章还解读了代码中的工具封装、自动微分引擎的核心代码、神经网络模块和多层感知机的实现,并给出了训练实验。

关键观点总结

关键观点1: HandyLLM101n 课程介绍

HandyLLM101n 是由OpenAI联合创始人和计算机视觉教母李飞飞教授的高徒Andrej Karpathy推出的AI课程,提供了关于深度学习和计算机视觉的硬核知识。

关键观点2: 微梯度项目介绍

微梯度项目是一个轻量级的自动微分引擎,用于从头开始构建一个简单的自动求导引擎,并使用它来训练一个简单的神经网络。

关键观点3: 代码解读

文章详细解读了代码中的工具封装、自动微分引擎的核心代码、神经网络模块和多层感知机的实现,并给出了训练实验。

关键观点4: 实践应用

文章通过实践应用,展示了如何使用微梯度项目来训练一个简单的神经网络,并介绍了如何计算损失函数和进行参数更新。

关键观点5: 总结

文章总结了自动微分引擎(micrograd)项目的重要性和实现细节,展示了深度学习的基础原理和实现细节。


正文


作者:Handy

LLM101n 是 OpenAI 联合创始人、“计算机视觉教母”李飞飞教授的高徒Andrej Karpathy 推出的“世界上显然最好的 AI 课程”。欢迎在「机智流」公众号后台回复 “ 101n ” 加入 LLM101n 中文版共建共学计划。我们后续还会更新关于该课程核心代码的解读,欢迎关注。

全文约 5000 字,预计阅读时间 8 分钟

今天将和大家一起学习 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  
  • utils.py

工具封装:自定义的随机数生成接口RNG类 和 随机数生成方法gen_data()

  • micrograd.py

自动微分引擎的核心代码,本文重点解读对象

  • micrograd_pytorch.py

micrograd.py 的功能相同,但它使用了PyTorch的自动微分引擎。这主要是为了验证和确认代码的正确性,同时也展示了PyTorch实现相同多层感知器(MLP)的一些相似性和差异性。这块代码比较简单,不作过多解读。

utils.py代码解读

utils.py 主要封装了两个小工具:

  • RNG 类:用于生成随机数。
# 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)
  • gen_data 函数:生成随机数据集。
def gen_data(random: RNG, n=100):
    # 初始化一个空列表来存储数据点
    pts = []
    for _ in range(n):
        # 在范围[-2.0, 2.0]内生成随机的x和y坐标
        x = random.uniform(-2.02.0)
        y = random.uniform(-2.02.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 代码解读

我们可以将其逻辑结构概括如下:

  • Value 类:用于存储值及其梯度。
  • Module 类:定义了神经网络模块的基本接口。
  • Neuron 类:实现单个神经元。
  • Layer 类:实现神经网络层。
  • MLP 类:实现多层感知机。
  • cross_entropy 函数:定义交叉熵损失函数。
  • eval_split 函数:评估数据集的损失。

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 = lambdaNone  # 反向传播函数
        self._prev = set(_children)    # 前驱节点集合
        self._op = _op                 # 操作类型,用于调试和可视化

Value类的目的是在自动微分系统中表示计算图中的一个节点。每个节点包含数据、梯度、前驱节点和操作类型。通过这些信息,可以构建计算图,并在需要时进行自动微分和反向传播。

Value 类数学运算

  1. __add__ 加法
    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 的梯度。


  1. __mul__ 乘法
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


  1. __pow__ 幂运算
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


  1. relu 实现ReLU激活函数
    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() 来计算其梯度。

  1. tanh 实现 tanh 正切激活函数
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


  1. exp 实现指数函数
    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


  1. log 实现自然对数
    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


  1. 重载运算符

Value 类重载了一些常见的运算符,包括加法、减法、乘法、除法以及取负操作。这些运算符的重载使得我们可以使用自然的数学表达式来操作 Value 对象,从而使代码更加简洁和易读。

def __neg__(self):  # -self
    # 返回self乘以-1的结果,实现取负操作
    return self * -1

def __radd__(self, other):  # other + self
    # 返回self加上other的结果,实现反向加法






请到「今天看啥」查看全文