Ziyu Li's Homepage

Back

CS189 Assignment 3#

项目描述#

这个lab主要聚焦于实现优化器和反向传播(逐项求导), 和CS336的lab1当中的实现类似

例子: 多元微分#

我们用课程组给出的例子来复习一下多元微分

label_plot

中间变量是从v1到v7

∂v7∂v7=1\frac{\partial v_7}{\partial v_7} = 1 v7=v6−v5  ⟹  ∂v7∂v6=1,∂v7∂v5=−1v_7 = v_6 - v_5 \implies \frac{\partial v_7}{\partial v_6} = 1, \quad \frac{\partial v_7}{\partial v_5} = -1 v6=v4+v3  ⟹  ∂v6∂v4=1,∂v6∂v3=1v_6 = v_4 + v_3 \implies \frac{\partial v_6}{\partial v_4} = 1, \quad \frac{\partial v_6}{\partial v_3} = 1 ∂v7∂v4=∂v7∂v6⋅∂v6∂v4=1⋅1=1\frac{\partial v_7}{\partial v_4} = \frac{\partial v_7}{\partial v_6} \cdot \frac{\partial v_6}{\partial v_4} = 1 \cdot 1 = 1 ∂v7∂v3=∂v7∂v6⋅∂v6∂v3\frac{\partial v_7}{\partial v_3} = \frac{\partial v_7}{\partial v_6} \cdot \frac{\partial v_6}{\partial v_3} ∂v6∂v3=∂v6∂v3+∂v6∂v4⋅∂v4∂v3=1+ev3\frac{\partial v_6}{\partial v_3} = \frac{\partial v_6}{\partial v_3} + \frac{\partial v_6}{\partial v_4} \cdot \frac{\partial v_4}{\partial v_3} = 1 + e^{v_3} ∂v7∂v3=1+ev3\frac{\partial v_7}{\partial v_3} = 1 + e^{v_3} ∂v7∂v2=∂v7∂v6⋅∂v6∂v2+∂v7∂v5⋅∂v5∂v2=∂v6∂v2−∂v5∂v2=∂v6∂v2−cos⁡(v2)\frac{\partial v_7}{\partial v_2} = \frac{\partial v_7}{\partial v_6} \cdot \frac{\partial v_6}{\partial v_2} + \frac{\partial v_7}{\partial v_5} \cdot \frac{\partial v_5}{\partial v_2} = \frac{\partial v_6}{\partial v_2} - \frac{\partial v_5}{\partial v_2} = \frac{\partial v_6}{\partial v_2} - \cos(v_2) ∂v6∂v2=∂v6∂v4⋅∂v4∂v2+∂v6∂v3⋅∂v3∂v2=∂v4∂v2+∂v3∂v2=∂v4∂v3⋅∂v3∂v2+v1=ev3⋅v1+v1\frac{\partial v_6}{\partial v_2} = \frac{\partial v_6}{\partial v_4} \cdot \frac{\partial v_4}{\partial v_2} + \frac{\partial v_6}{\partial v_3} \cdot \frac{\partial v_3}{\partial v_2} = \frac{\partial v_4}{\partial v_2} + \frac{\partial v_3}{\partial v_2} = \frac{\partial v_4}{\partial v_3} \cdot \frac{\partial v_3}{\partial v_2} + v_1 = e^{v_3} \cdot v_1 + v_1 ∂v7∂v1=∂v7∂v6⋅∂v6∂v1=∂v6∂v1=∂v6∂v4⋅∂v4∂v1+∂v6∂v3⋅∂v3∂v1=∂v4∂v1+∂v3∂v1=∂v4∂v3⋅∂v3∂v1+∂v3∂v1=ev3⋅v2+v2\frac{\partial v_7}{\partial v_1} = \frac{\partial v_7}{\partial v_6} \cdot \frac{\partial v_6}{\partial v_1} = \frac{\partial v_6}{\partial v_1} = \frac{\partial v_6}{\partial v_4} \cdot \frac{\partial v_4}{\partial v_1} + \frac{\partial v_6}{\partial v_3} \cdot \frac{\partial v_3}{\partial v_1} = \frac{\partial v_4}{\partial v_1} + \frac{\partial v_3}{\partial v_1} = \frac{\partial v_4}{\partial v_3} \cdot \frac{\partial v_3}{\partial v_1} + \frac{\partial v_3}{\partial v_1} \\ = e^{v_3} \cdot v_2 + v_2

所谓反向传播就是把每一次算出来的中间结果记住, 然后用链式法则一步步的向前求导,比如说最初的输入是x,y, 输出是z, 通过计算z对一大堆中间变量的偏导数, 最终计算得到

∂z∂x  and  ∂z∂y\frac{\partial z}{\partial x} \,\,and \,\, \frac{\partial z}{\partial y}

如果对上面的求偏导过程有疑问, 建议去看一看微积分(下)或者数学分析(三)

Problem 1#

既然要求微分, 首先要实现Tensor之间的运算, 注意到正如上面那个图所示, 我们需要三个类:

— class BearGrad 来给出一个可调用的函数, 根据上游梯度来计算下游梯度 — class BearParent 来记录图的边, 即每个节点的parent节点是什么 — class BearTensor 来提供各种基础运算, 例如加减乘除

@dataclass
class BearGrad:
    '''Stores how to compute the downstream gradient from the upstream gradient 
    - `op_str` is just a string describing the operation; you don't need to use this for this HW, but it may help in debugging if you print out what operations create your computation graph.
    - `fn` is a function that takes in the upstream loss gradient and outputs the loss gradient that should be passed downstream. This essentially applies the chain rule at the current node in the computation graph.
    '''
    fn: callable
    op_str: str | None = None
python

这是梯度计算器, fn函数用于计算下游梯度, op_str用来描述这个操作

例如我们有一个中间变量c = a + b

a ────┐
      ├──→ c = a + b ──→ 上游梯度 (∂L/∂c)
b ────┘
plaintext

这个BearGrad类需要给出L对a和b的偏导数, 即∂L/∂a和∂L/∂b, 这种情况下fn大概是一个加法的梯度函数:

def add_grad_fn(upstream_grad):
    # ∂L/∂a = ∂L/∂c * ∂c/∂a = ∂L/∂c * 1
    # ∂L/∂b = ∂L/∂c * ∂c/∂b = ∂L/∂c * 1
    return upstream_grad  # 因为 ∂c/∂a = 1
python

父节点追踪器:

@dataclass
class BearParent:
    '''This class represents a parent of a node; a node must track its parents so that it knows who to propagate its gradients to.
    - `grad` is the `BearGrad` object for this parent; we can apply the `fn` from this to compute the gradient that should be passed downstream.
    - `parent` is the `BearTensor` object for the parent
    '''
    parent: BearTensor
    grad: BearGrad

    def __hash__(self):
        return id(self)

    def __eq__(self, other):
        return self is other
python

要记录父节点和grad, grad代表了”在已知上游梯度的情况下, 如何算出下游梯度”

顺便解释一下类装饰器@dataclass, 用了这个装饰器就会自动生成一些方法, 比如:

__init__
__repr__ (字符串表示)
__eq__ (相等比较)
plaintext

等等

class BearTensor:
    '''`BearTensor`: This represents a node in our computation graph
    - `value` is the underlying data in the Tensor; this is computed during the forward pass
    - `parents` keeps track of the parents in the computation graph to whom we pass our gradient to
    - `adjoint` is the gradient (the transpose of it technically) that we compute in the backward pass
    '''
    def __init__(self, name: str, value: np.ndarray, parents: list[BearParent] | None = None):
        self.name = name
        self.value = value
        self.parents = parents if parents is not None else []
        self.adjoint: float | np.ndarray = 0.0
python

我们要实现各种运算和相对于那种运算的梯度公式

- Addition (`__add__`)
- Subtraction (`__sub__`)
- Multiplication (`__mul__`)
- Power (`__pow__`)
- Matrix multiplication (`__matmul__`)
- Dot product (`dot`)
- Sum (`sum`)
- Mean (`mean`)
- ReLU (`relu`)
- Sigmoid (`sigmoid`)
plaintext

先来看__add__的实现, 这个运算要对self.value和other.value进行加法运算, 新BearTensor.value就是self.value + other.value

接下来思考这个问题: 返回的新的BearTensor的parents应该是什么?

return BearTensor(name=f"{self.name} + {other.name}", value=new_value, parents=parents)
python

新的parents应该有两个节点, 分别为self和other, 但是parent必须为BearParent类, 而构造这个类的时候需要两个参数: BearTensor和BearGrad, 显然BearTensor已经有了, BearGrad需要我们手动演算一下求导的函数

设有两个 Tensor:

  • aa: BearTensor, 值为 aa
  • bb: BearTensor, 值为 bb
  • c=a+bc = a + b: BearTensor, 值为 c=a+bc = a + b

我们需要计算 ∂L∂a\frac{\partial L}{\partial a} 和 ∂L∂b\frac{\partial L}{\partial b},其中 LL 是最终损失函数:

∂L∂a=∂L∂c⋅∂c∂a∂L∂b=∂L∂c⋅∂c∂b∂c∂a=∂(a+b)∂a=1∂c∂b=∂(a+b)∂b=1∂L∂a=∂L∂c⋅1=∂L∂c∂L∂b=∂L∂c⋅1=∂L∂c\begin{align*} \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial a} \\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial b} \\ \frac{\partial c}{\partial a} = \frac{\partial (a + b)}{\partial a} = 1 \\ \frac{\partial c}{\partial b} = \frac{\partial (a + b)}{\partial b} = 1\\ \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot 1 = \frac{\partial L}{\partial c}\\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot 1 = \frac{\partial L}{\partial c} \end{align*}

换言之, 这里的两个下游梯度都等于传过来的上游梯度, 所以两个fn函数的返回值保持不变

减法也一样

∂L∂a=∂L∂c⋅∂c∂a∂L∂b=∂L∂c⋅∂c∂b∂c∂a=∂(a−b)∂a=1∂c∂b=∂(a−b)∂b=−1∂L∂a=∂L∂c⋅1=∂L∂c∂L∂b=∂L∂c⋅−1=−∂L∂c\begin{align*} \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial a} \\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial b} \\ \frac{\partial c}{\partial a} = \frac{\partial (a - b)}{\partial a} = 1 \\ \frac{\partial c}{\partial b} = \frac{\partial (a - b)}{\partial b} = -1\\ \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot 1 = \frac{\partial L}{\partial c}\\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot -1 = -\frac{\partial L}{\partial c} \end{align*}

乘法

∂L∂a=∂L∂c⋅∂c∂a∂L∂b=∂L∂c⋅∂c∂b∂c∂a=∂(a⋅b)∂a=b∂c∂b=∂(a⋅b)∂b=a∂L∂a=∂L∂c⋅b∂L∂b=∂L∂c⋅a\begin{align*} \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial a} \\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial b} \\ \frac{\partial c}{\partial a} = \frac{\partial (a \cdot b)}{\partial a} = b \\ \frac{\partial c}{\partial b} = \frac{\partial (a \cdot b)}{\partial b} = a \\ \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot b \\ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \cdot a \end{align*}

幂运算, 此时另外一个输入变成固定的数power, 记作n

∂L∂a=∂L∂c⋅∂c∂a∂c∂a=∂(an)∂a=n⋅an−1∂L∂a=∂L∂c⋅n⋅an−1\begin{align*} \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot \frac{\partial c}{\partial a} \\ \frac{\partial c}{\partial a} = \frac{\partial (a^n)}{\partial a} = n \cdot a^{n-1} \\ \frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \cdot n \cdot a^{n-1} \\ \end{align*}
    def __pow__(self, power: float) -> BearTensor:
        
        new_value=self.value**power

        def grad_fn(upstream_grad):
            return upstream_grad*power*self.value**(power-1)

        parents=[
            BearParent(parent=self, grad=BearGrad(fn=grad_fn,op_str='text("pow")'))
        ]

        return BearTensor(name=f"{self.name}**{power}", value=new_value, parents=parents)
python

矩阵乘法, 注意这里要用向量微积分

∂L∂A:(m,n)∂L∂C:(m,p)C=A⋅B:(m,p)A:(m,n),B:(n,p)∂L∂A=∂L∂C⋅BT(m,n)=(m,p)⋅(p,n)∂L∂B=AT⋅∂L∂C(n,p)=(n,m)⋅(m,p)\begin{align*} \frac{\partial L}{\partial A} &: (m,n) \\ \frac{\partial L}{\partial C} &: (m,p) \\ C &= A \cdot B : (m,p) \\ A &: (m,n), \quad B : (n,p) \\[8pt] \frac{\partial L}{\partial A} &= \frac{\partial L}{\partial C} \cdot B^T \\ (m,n) &= (m,p) \cdot (p,n) \\[8pt] \frac{\partial L}{\partial B} &= A^T \cdot \frac{\partial L}{\partial C} \\ (n,p) &= (n,m) \cdot (m,p) \end{align*}

记住向量求导的尺寸约定: 偏导数(矩阵)的尺寸保持为”分母”的尺寸, 比如∂L∂A\frac{\partial L}{\partial A}的尺寸为(m,n)(m,n), 因为AA的尺寸为(m,n)(m,n)

点积运算

c=a⋅b=∑i=1naibi,a(n),b(n)∂c∂a=b∂L∂a(n)=∂L∂c(1)⋅b(n)∂c∂b=a∂L∂b(n)=∂L∂c(1)⋅a(n)\begin{align*} c &= \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^n a_i b_i, \quad \mathbf{a}_{(n)}, \mathbf{b}_{(n)} \\[8pt] \frac{\partial c}{\partial \mathbf{a}} &= \mathbf{b} \\[6pt] \frac{\partial L}{\partial \mathbf{a}}_{(n)} &= \frac{\partial L}{\partial c}_{(1)} \cdot \mathbf{b}_{(n)} \\[12pt] \frac{\partial c}{\partial \mathbf{b}} &= \mathbf{a} \\[6pt] \frac{\partial L}{\partial \mathbf{b}}_{(n)} &= \frac{\partial L}{\partial c}_{(1)} \cdot \mathbf{a}_{(n)} \end{align*}

自求和

s=∑i=1nxi=sum(x),x(n)  ⟹  s(1)∂s∂x=[11⋯1](n)∂L∂x(n)=∂L∂s(1)⋅1(n)\begin{align*} s &= \sum_{i=1}^n x_i = \text{sum}(\mathbf{x}), \quad \mathbf{x}_{(n)} \implies s_{(1)} \\[8pt] \frac{\partial s}{\partial \mathbf{x}} &= \begin{bmatrix} 1 & 1 & \cdots & 1 \end{bmatrix}_{(n)} \\[6pt] \frac{\partial L}{\partial \mathbf{x}}_{(n)} &= \frac{\partial L}{\partial s}_{(1)} \cdot \mathbf{1}_{(n)} \end{align*}
    def sum(self) -> BearTensor:
        
        new_value=np.array([np.sum(self.value)])

        def grad_fn(upstream_grad):
            return np.ones_like(self.value)*upstream_grad

        parents=[
            BearParent(parent=self, grad=BearGrad(fn=grad_fn,op_str='text("sum")'))
        ]

        return BearTensor(name=f"sum({self.name})", value=new_value, parents=parents)
python

自平均运算

μ=mean(x)=1n∑i=1nxi,x(n)  ⟹  μ(1)∂μ∂x=1n[11⋯1](n)∂L∂x(n)=∂L∂μ(1)⋅1n⋅1(n)\begin{align*} \mu &= \text{mean}(\mathbf{x}) = \frac{1}{n} \sum_{i=1}^n x_i, \quad \mathbf{x}_{(n)} \implies \mu_{(1)} \\[8pt] \frac{\partial \mu}{\partial \mathbf{x}} &= \frac{1}{n} \begin{bmatrix} 1 & 1 & \cdots & 1 \end{bmatrix}_{(n)} \\[6pt] \frac{\partial L}{\partial \mathbf{x}}_{(n)} &= \frac{\partial L}{\partial \mu}_{(1)} \cdot \frac{1}{n} \cdot \mathbf{1}_{(n)} \end{align*}
    def mean(self) -> BearTensor:
        
        new_value=np.array([np.mean(self.value)])

        def grad_fn(upstream_grad):
            return np.ones_like(self.value)*upstream_grad/self.value.size

        parents=[
            BearParent(parent=self, grad=BearGrad(fn=grad_fn,op_str='text("mean")'))
        ]

        return BearTensor(name=f"mean({self.name})", value=new_value, parents=parents)
python

ReLU运算

y=ReLU(x)=max⁡(0,x)∂y∂x={1if x>00if x≤0=1(x>0)∂L∂x=∂L∂y⋅∂y∂x=∂L∂y⋅1(x>0)\begin{align*} y &= \text{ReLU}(x) = \max(0, x) \\[8pt] \frac{\partial y}{\partial x} &= \begin{cases} 1 & \text{if } x > 0 \\ 0 & \text{if } x \leq 0 \end{cases} = \mathbb{1}(x > 0) \\[8pt] \frac{\partial L}{\partial x} &= \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \cdot \mathbb{1}(x > 0) \end{align*}
    def relu(self) -> BearTensor:
        
        new_value=np.maximum(0, self.value)

        def grad_fn(upstream_grad):
            return (self.value>0)*upstream_grad

        parents=[
            BearParent(parent=self, grad=BearGrad(fn=grad_fn,op_str='text("relu")'))
        ]

        return BearTensor(name=f"relu({self.name})", value=new_value, parents=parents)
python

注意这个求导在数学上是有瑕疵的, ReLU函数在0处并不可导

Sigmoid函数

y=σ(x)=11+e−xdydx=σ(x)(1−σ(x))=y(1−y)∂L∂x=∂L∂y⋅dydx=∂L∂y⋅y(1−y)\begin{align*} y &= \sigma(x) = \frac{1}{1 + e^{-x}} \\[8pt] \frac{dy}{dx} &= \sigma(x)(1 - \sigma(x)) = y(1 - y) \\[8pt] \frac{\partial L}{\partial x} &= \frac{\partial L}{\partial y} \cdot \frac{dy}{dx} = \frac{\partial L}{\partial y} \cdot y(1 - y) \end{align*}
    def sigmoid(self) -> BearTensor:
        
        new_value=1 / (1 + np.exp(-self.value))

        def grad_fn(upstream_grad):
            return upstream_grad*new_value*(1-new_value)

        parents=[
            BearParent(parent=self, grad=BearGrad(fn=grad_fn,op_str='text("sigmoid")'))
        ]

        return BearTensor(name=f"sigmoid({self.name})", value=new_value, parents=parents)
python

课程组给了一个链式图的demo绘图代码, 图如下

label_plot

当然, 我们可以修改里面的代码, 比如说绘制一个更复杂的多元函数的链式图

# # Example code
# a = BearTensor("a", np.array([2, 3]))
# b = BearTensor("b", np.array([1, 1]))
# c = a + b
# draw_graph(c, "demo_graph.typ")

a = BearTensor("a", np.array([2, 3]))
b = BearTensor("b", np.array([1, 1]))
c = BearTensor("c", np.array([1, 4]))
d = a * b + b * c + a * c
draw_graph(d, "demo_graph.typ")
python
label_plot

Problem 2#

要实现反向传播, 首先要对所有的节点进行拓扑排序, 这样才能搞清楚传播路径

搞清楚这个结点添加的顺序, 举一个简单的例子:

这里x是最初的输入, 没有任何依赖, 我们最终需要的梯度就是loss对x的梯度, 为了得到这个梯度, 需要计算一系列loss对中间变量(x1, x2, ReLU)的梯度, 然后通过链式法则得到loss对x的梯度

还需要从某个节点开始, 重置他和他上游(父)节点的所有梯度, 这也需要用递归实现

def reset_children(self):
    """Resets the gradient in the current node to zero and all nodes before it in the computation graph."""
    
    def reset_gradient(node):
        if node.adjoint is not None:
            node.adjoint = 0

        for parent in node.parents:
            reset_gradient(parent.parent)

    reset_gradient(self)
python

现在实现反向传播, 思考一下, 以上面那个例子当中的[x, x1, x2, ReLU, loss], 遍历的顺序应该是从loss开始, 而不是从x开始

遍历到每个节点x, 都需要再遍历x的父节点, 并且更新父节点的梯度

Problem 3#

实现SGD, Momentum, 实现AdamW优化器

首先考虑优化器的基类, 需要有一个学习率和一堆的参数, 参数是保存为list[BearTensor], value属性保存参数值, adjoint属性保存梯度

class Optimizer:
    def __init__(self, params: list[BearTensor], lr: float):
        self.params = params
        self.lr = lr

    def zero_grad(self):
        for p in self.params:
            p.adjoint = np.zeros_like(p.value)

    def step(self):
        # raise NotImplementedError
        for p in self.params:
            p.value -= self.lr * p.adjoint
python

SGD直接照抄基类就行

class SGD(Optimizer):
    def __init__(self, params: list[BearTensor], lr: float):
        super().__init__(params, lr)

    def step(self):
        
        for p in self.params:
            p.value -= self.lr * p.adjoint
        # pass
python

注意Momentum优化器的公式:

mt=β⋅mt−1+gt(动量项)vt=−η⋅mt(速度/增量)wt+1=wt+vt=wt−η⋅mt(参数更新)\boxed{ \begin{align*} m_t &= \beta \cdot m_{t-1} + g_t \quad \text{(动量项)} \\[6pt] v_t &= -\eta \cdot m_t \quad \text{(速度/增量)} \\[6pt] w_{t+1} &= w_t + v_t = w_t - \eta \cdot m_t \quad \text{(参数更新)} \end{align*} }

先把这些notation映射到我们类里的属性:

符号代码对应含义gtp.adjoint当前梯度mtmomentum_term动量项vtself.velocities[i]速度/参数增量βself.beta动量系数ηself.lr学习率wtp.value参数值\begin{array}{c|c|c} \text{符号} & \text{代码对应} & \text{含义} \\ \hline g_t & \texttt{p.adjoint} & 当前梯度 \\ m_t & \texttt{momentum\_term} & 动量项 \\ v_t & \texttt{self.velocities[i]} & 速度/参数增量 \\ \beta & \texttt{self.beta} & 动量系数 \\ \eta & \texttt{self.lr} & 学习率 \\ w_t & \texttt{p.value} & 参数值 \end{array}

接下来没有任何难度了, 在step方法里遍历参数然后逐个更新就行

对Adam优化器也做同样的理解

1. 一阶矩:mt=β1⋅mt−1+(1−β1)⋅gt2. 二阶矩:vt=β2⋅vt−1+(1−β2)⋅gt23. 偏差校正:m^t=mt1−β1t,v^t=vt1−β2t4. 参数更新:wt+1=wt−η⋅m^tv^t+ε\boxed{ \begin{align*} \text{1. 一阶矩:} \quad & m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t \\[6pt] \text{2. 二阶矩:} \quad & v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 \\[6pt] \text{3. 偏差校正:} \quad & \hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \\[6pt] \text{4. 参数更新:} \quad & w_{t+1} = w_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \varepsilon} \end{align*} } 符号代码对应含义gtp.adjoint当前梯度mtself.ms[i]一阶矩估计(动量)vtself.vs[i]二阶矩估计(方差)m^tm_hat校正后一阶矩v^tv_hat校正后二阶矩β1self.beta1一阶矩衰减系数(0.9)β2self.beta2二阶矩衰减系数(0.999)εself.eps防止除零(1e−8)tself.t时间步ηself.lr学习率\begin{array}{c|c|c} \text{符号} & \text{代码对应} & \text{含义} \\ \hline g_t & \texttt{p.adjoint} & 当前梯度 \\ m_t & \texttt{self.ms[i]} & 一阶矩估计(动量) \\ v_t & \texttt{self.vs[i]} & 二阶矩估计(方差) \\ \hat{m}_t & \texttt{m\_hat} & 校正后一阶矩 \\ \hat{v}_t & \texttt{v\_hat} & 校正后二阶矩 \\ \beta_1 & \texttt{self.beta1} & 一阶矩衰减系数 (0.9) \\ \beta_2 & \texttt{self.beta2} & 二阶矩衰减系数 (0.999) \\ \varepsilon & \texttt{self.eps} & 防止除零 (1e-8) \\ t & \texttt{self.t} & 时间步 \\ \eta & \texttt{self.lr} & 学习率 \end{array}

运行课程组的代码查看三个优化器的训练曲线

label_plot

Problem 4#

这里要我们自己写一个训练循环, 唯一要注意的就是把所有的参数注册成BearTensor类, 其他没有什么难点了

Epoch 10/100, Train Loss: 11.8084, Test Loss: 10.9215
Epoch 20/100, Train Loss: 4.0232, Test Loss: 3.5952
Epoch 30/100, Train Loss: 0.9623, Test Loss: 0.8673
Epoch 40/100, Train Loss: 0.4591, Test Loss: 0.5058
Epoch 50/100, Train Loss: 0.5181, Test Loss: 0.5702
Epoch 60/100, Train Loss: 0.4653, Test Loss: 0.4989
Epoch 70/100, Train Loss: 0.4054, Test Loss: 0.4402
Epoch 80/100, Train Loss: 0.3933, Test Loss: 0.4299
Epoch 90/100, Train Loss: 0.3896, Test Loss: 0.4261
Epoch 100/100, Train Loss: 0.3834, Test Loss: 0.4217

Final Test MSE: 0.4217
plaintext
label_plot
UC Berkeley CS189 Assignment 3
https://astro-pure.js.org/blog/cs189_assignment3
Author Ziyu(Albert) Li 李子煜
Published at February 7, 2026
Comment seems to stuck. Try to refresh?✨