CMU 10-414/714: Deep Learning Systems --hw1

本文介绍了CMU 10-414/714课程的作业1,主要涉及深度学习库Needle。Needle提供自动微分框架,包括计算图和自动求导功能。文章详细讲解了 Needle 中的关键文件和核心类,如Value、Op和Tensor,以及如何实现前向传播和反向传播。此外,还探讨了PyTorch的正向传播、反向传播和计算图概念,以及如何利用autograd进行自动微分。
摘要由CSDN通过智能技术生成

hw1

needle库中的两个重要文件
  1. autograd.py文件:实现计算图框架、自动微分框架(automatic differentiation framework)
  2. ops.py文件:实现各种算子。反向传播的粒度很细,会以一个个算子为单位,而不是以网络层为单位。其实现基础是numpy,后面会自己实现自己的运算库
needle中的一些核心类
  1. Value:计算图中的节点,代表计算过程中的数值
  2. Op:计算图的算子,即加减乘除等操作。其通过compute()函数进行前向计算,通过gradient()函数进行反向传播
  3. Tensor:是Value的子类,表示计算图中的一个多维数组。课程提供了一些功能(如操作符重载)来保证用tensor进行数学运算
  4. TensorOp:是Op的子类,表示返回Tensor的算子
实现功能
  1. 实现前向传播和反向传播
    • compute()函数实现前向运算,运算的输入都是numpy.ndarray(后面的实验会实现自己的NDArray,因此这里为了对齐用import numpy as array_api),即compute()函数只处理raw data,而不处理自动求导的Tensor
    • gradient()函数实现反向传播,其输入为Tensor,且函数内的运算都是TensorOp算子
    • 需要实现运算的算子如下。这里代码其实没有什么高深的,只要搞清楚每个算子是怎么求导的就好了,比如PowerScalar算子实现 a s c a l a r a^{scalar} ascalar,其gradient()函数就是返回 ( o u t _ g r a d × s e l f . s c a l a r × l h s s e l f . s c a l a r − 1 ) (out\_grad\times self.scalar\times lhs^{self.scalar - 1} ) (out_grad×self.scalar×lhsself.scalar1)
      • PowerScalar:乘方
      • EWiseDiv:除法
      • DivScalar:张量除以标量
      • MatMul:矩阵乘法
        • 梯度必须和输入数据的维度一致:损失函数L对输入数据X的梯度,告诉我们在每个特征维度上,改变输入数据时损失函数的变化情况。若想要计算L对每个输入特征的梯度,这些梯度将组成一个与输入数据X相同形状的张量,每个元素表示在相应特征维度上的变化率
      • Summation:在指定的维度上求和(在哪个维度上求和,导数就在哪个维度上扩展成与输入张量相同的形状)
      • BroadcastTo:broadcast成指定的形状(在哪个维度上扩展,导数就在哪个维度上求和后reshape成与输入张量相同的形状)
      • Reshape:转变成之后定的形状(导数是reshape成输入张量相同的形状)
      • Negate:取负数
      • Transpose:交换两个维度(导数仍是交换这两个维度)
  2. 拓扑排序:原理很简单,数据结构里学过。就是深度优先遍历node_list,如果一个node没有被visited,就深度优先遍历它的inputs结点,直到一个入度为0的结点,就将其加入topo_order中
def find_topo_sort(node_list: List[Value]) -> List[Value]:
    visited = set()
    topo_order = []
    for node in node_list:
      if node not in visited:
        topo_sort_dfs(node, visited, topo_order)
    return topo_order

def topo_sort_dfs(node, visited, topo_order):
    if node in visited:
      return

    for next in node.inputs:
      topo_sort_dfs(next, visited, topo_order)
    
    visited.add(node)
    topo_order.append(node)
  1. 实现reverse mode differentiation:
def compute_gradient_of_variables(output_tensor, out_grad):
  # 创建一个空字典,用于存储每个节点在计算图中的梯度信息
  node_to_output_grads_list: Dict[Tensor, List[Tensor]] = {}
  # 将输出张量的梯度存储在字典中,作为输出节点的梯度
  node_to_output_grads_list[output_tensor] = [out_grad]

  # find_topo_sort([output_tensor])从输出张量开始计算图的拓扑排序(因为输出张量通过node.inputs连着整张图,所以可以实现)
  reverse_topo_order = list(reversed(find_topo_sort([output_tensor])))

  for node in reverse_topo_order:
    # 计算当前节点的梯度:就是把所有偏导数加到一起
    sum_grad = node_to_output_grads_list[node][0]
    for t in node_to_output_grads_list[node][1:]:
      sum_grad = sum_grad + t 
    node.grad = sum_grad
	
	# 叶子节点即没有op的节点,跳过后面的for循环
    if node.is_leaf():
      continue

	# 计算节点的输入节点相对于
    for i, grad in enumerate(node.op.gradient_as_tuple(node.grad, node)):
      input_ = node.inputs[i]
      if input_ not in node_to_output_grads_list:
        node_to_output_grads_list[input_] = []
      node_to_output_grads_list[input_].append(grad)

举个例子有助于理解自动微分的过程原理:
在这里插入图片描述

  1. 重新实现softmax loss:使用Tensor且y是one-hot向量组成的 m × k m\times k m×k矩阵
def softmax_loss(Z, y_one_hot):
    a = ndl.ops.summation(Z*y_one_hot)
    b = ndl.ops.summation(ndl.ops.log(ndl.ops.summation(ndl.ops.exp(Z), axes=(1,))))
    return (b-a) / Z.shape[0]
  1. 重新为一个两次神经网络实现SGD
    与hw0的区别:W1和W2变成Tensor,X和y还是numpy array;hw0中是手动计算反向传播公式然后用代码实现,这次则是使用自动微分机制,只需算出loss值,然后调用.backward()即可自动对每个参数求导,再对W1和W2更新即可
def nn_epoch(X, y, W1, W2, lr = 0.1, batch=100):
    for i in range(X.shape[0]//batch+1):
      # 首先不变的,采样batch
      start, end = i * batch, min((i+1)*batch, X.shape[0])
      m = end - start
      if m == 0:
          break
      Xb, yb = X[start:end], y[start:end]

      # hw0这里是计算梯度,是通过手动计算得出数学公式然后实现的
      # 现在则是使用自动微分机制,算出loss后直接.backward()即可
      # 下面并没有对X和y设置自动微分,因为参数更新过程中只需要对参数矩阵W1和W2自动微分即可,不需要对输入数据和得出来的结果微分(node_to_grad第一个值是loss)
      Xb, yb = X[start:end], y[start:end]
      Xb = ndl.Tensor(Xb, requires_grad=False)
      Z1 = Xb @ W1
      A1 = ndl.ops.relu(Z1)
      Z2 = A1 @ W2
      y_one_hot = np.zeros(Z2.shape, dtype=np.float32)
      y_one_hot[np.arange(Z2.shape[0]), yb] = 1
      y_one_hot = ndl.Tensor(y_one_hot, requires_grad=False)
      loss = softmax_loss(Z2, y_one_hot)
      loss.backward()
      
      # 更新参数矩阵
      W1.data = W1.data - lr * ndl.Tensor(W1.grad.numpy().astype(np.float32)) 
      W2.data = W2.data - lr * ndl.Tensor(W2.grad.numpy().astype(np.float32))
    return W1, W2

相关知识补充

一、pytorch中的正向传播、反向传播和计算图
  1. Tensor
    pytorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

    Tensor是autograd包的核心类,若将其属性.requires_grad设为True,它将开始追踪在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可调用.backward()来完成所有的梯度计算。此Tensor的梯度将累积到.grad属性中。

    Function是另一个核心类,TensorFunction结合就可以构建一个记录有整个计算过程的有向无环图。每个Tensor.grad_fn属性记录着这个Tensor最近是通过哪一个运算得到的,若其不是通过某个运算得到的,.grad_fn属性值为None,像grad_fn属性值为None的称为叶子节点
x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
print(z, out)

输出为:

tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)
  1. 梯度

    o u t = 1 4 ∑ i = 1 4 z i = 1 4 ∑ i = 1 4 3 ( x i + 2 ) 2 out=\frac14\sum_{i=1}^4z_i=\frac14\sum_{i=1}^43(x_i+2)^2 out=41i=14zi=41i=143(xi+2)2 所以
    ∂ o u t ∂ x i ∣ x i = 1 = 9 2 = 4.5 \frac{\partial{out}}{\partial{x_i}}\bigr\rvert_{x_i=1}=\frac{9}{2}=4.5 xiout xi=1=29=4.5

    数学上,若有一个函数值和自变量都为向量的函数 y ⃗ = f ( x ⃗ ) \vec{y}=f(\vec{x}) y =f(x ),那么 y ⃗ \vec{y} y 关于 x ⃗ \vec{x} x 的梯度就是一个雅可比矩阵:
    J = ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ) J=\left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right) J= x1y1x1ymxny1xnym
    torch.autograd这个包就是用来计算一些雅可比矩阵的乘积的。如 v v v是一个标量函数 l = g ( y ⃗ ) l=g(\vec{y}) l=g(y )的梯度:
    v = ( ∂ l ∂ y 1 ⋯ ∂ l ∂ y m ) v=\left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) v=(y1lyml) 那么根据链式法则我们有关于的雅克比矩阵就为: v J = ( ∂ l ∂ y 1 ⋯ ∂ l ∂ y m ) ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ) = ( ∂ l ∂ x 1 ⋯ ∂ l ∂ x n ) v J=\left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) \left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)=\left(\begin{array}{ccc}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right) vJ=(y1lyml) x1y1x1ymxny1xnym =(x1lxnl)

    注意:grad在反向传播过程中是累加的,即在某一次前向传播和反向传播过程中计算得到了某些参数的梯度值,然后在下一次前向传播和反向传播中,会再次计算这些参数的梯度值。此时新计算得到的梯度值会将与之前的梯度值相加,而不是覆盖掉之前的梯度值。

    这个功能的好处是可以实现低显存跑大batchsize,详细可见此篇文章
  2. 正向传播计算图
    假设神经网络如下:
    样本为 x ∈ R d \mathbf{x}\in\mathbb{R}^d xRd,中间变量为 z = W ( 1 ) x \mathbf{z}=\mathbf{W}^{(1)}\mathbf{x} z=W(1)x,其中 W ( 1 ) ∈ R h × d \mathbf{W}^{(1)}\in\mathbb{R}^{h\times d} W(1)Rh×d是隐藏层的权重参数。
    将中间变量 z ∈ R h \boldsymbol{z} \in \mathbb{R}^h zRh通过激活函数 ϕ \phi ϕ后,得到隐藏激活向量 h = ϕ ( z ) \mathbf{h}=\phi(\mathbf{z}) h=ϕ(z)。隐藏层变量 h \boldsymbol{h} h也是一个中间变量。
    假设输出层参数只有权重 W ( 2 ) ∈ R q × h \boldsymbol{W}^{(2)} \in \mathbb{R}^{q \times h} W(2)Rq×h,可得到向量长度为 q q q的输出层变量 o = W ( 2 ) h \mathbf{o}=\mathbf{W}^{(2)}\mathbf{h} o=W(2)h.
    假设损失函数为 ℓ \ell ,且样本标签为 y y y,可以计算出单个数据样本的损失项 L = ℓ ( o , y ) \mathbf{L}=\ell(\mathbf{o}, y) L=(o,y).
    根据 L 2 L_2 L2范数正则化的定义,给定超参数 λ \lambda λ,正则化项即 s = λ 2 ( ∣ W ( 1 ) ∣ F 2 + ∣ W ( 2 ) ∣ F 2 ) s=\frac\lambda2\left(|\boldsymbol{W}^{(1)}|_F^2+|\boldsymbol{W}^{(2)}|_F^2\right) s=2λ(W(1)F2+W(2)F2).
    最终模型在给定的数据有样本上带正则化的损失为 J = L + s J=L+s J=L+s J J J即为给定数据样本的目标函数

    通过绘制计算图来可视化算子和变量在计算中的依赖关系,如下图。正方形表示变量、圆圈表示操作符。左下角表示输入、右上角表示输出
    在这里插入图片描述
  3. 反向传播
    反向传播就是计算神经网络参数梯度的方法。总的来说,反向传播依据链式法则,沿着输出层到输入层的顺序,依次计算并存储目标函数有关神经网络各层的中间变量以及参数的梯度。对输入或输出 X , Y , Z \mathsf{X}, \mathsf{Y}, \mathsf{Z} X,Y,Z为任意形状张量的函数 Y = f ( X ) \mathsf{Y}=f(\mathsf{X}) Y=f(X) Z = g ( Y ) \mathsf{Z}=g(\mathsf{Y}) Z=g(Y),通过链式法则,有 ∂ Z ∂ X = p r o d ( ∂ Z ∂ Y , ∂ Y ∂ X ) \frac{\partial\mathbb{Z}}{\partial\mathbf{X}}=\mathrm{prod}\left(\frac{\partial\mathbb{Z}}{\partial\mathbf{Y}},\frac{\partial\mathbf{Y}}{\partial\mathbf{X}}\right) XZ=prod(YZ,XY). 其中 prod \text{prod} prod运算符将根据两个输入的形状,在必要的操作(如转置和互换输入位置)后对两个输入做乘法.

    回顾一下本节中样例模型,它的参数是 W ( 1 ) \boldsymbol{W}^{(1)} W(1) W ( 2 ) \boldsymbol{W}^{(2)} W(2),因此反向传播的目标是计算 ∂ J ∂ W ( 1 ) \frac{\partial{J}}{\partial{\boldsymbol{W}^{(1)}}} W(1)J ∂ J ∂ W ( 2 ) \frac{\partial{J}}{\partial{\boldsymbol{W}^{(2)}}} W(2)J。应用链式法则依次计算各中间变量和参数的梯度,其计算次序与前向传播中相应中间变量的计算次序恰恰相反:

    首先,分别计算目标函数 J = L + s J=L+s J=L+s有关损失项 L L L和正则项 s s s的梯度 ∂ J ∂ L = 1 , ∂ J ∂ s = 1. \frac{\partial J}{\partial L}=1,\quad\frac{\partial J}{\partial s}=1. LJ=1,sJ=1.
    然后根据链式法则计算目标函数有关输出层变量的梯度 ∂ J ∂ o = p r o d ( ∂ J ∂ L , ∂ L ∂ o ) = ∂ L ∂ o . \frac{\partial J}{\partial\boldsymbol{o}}=\mathrm{prod}\left(\frac{\partial J}{\partial L},\frac{\partial L}{\partial\boldsymbol{o}}\right)=\frac{\partial L}{\partial\boldsymbol{o}}. oJ=prod(LJ,oL)=oL.
    然后计算正则项关于两个参数的梯度: ∂ s ∂ W ( 1 ) = λ W ( 1 ) , ∂ s ∂ W ( 2 ) = λ W ( 2 ) . \frac{\partial s}{\partial\boldsymbol{W}^{(1)}}=\lambda\boldsymbol{W}^{(1)},\quad\frac{\partial s}{\partial\boldsymbol{W}^{(2)}}=\lambda\boldsymbol{W}^{(2)}. W(1)s=λW(1),W(2)s=λW(2).
    然后计算 W ( 2 ) W^{(2)} W(2)参数的梯度 ∂ J ∂ W ( 2 ) = p r o d ( ∂ J ∂ o , ∂ o ∂ W ( 2 ) ) + p r o d ( ∂ J ∂ s , ∂ s ∂ W ( 2 ) ) = ∂ J ∂ o h ⊤ + λ W ( 2 ) . \frac{\partial J}{\partial\boldsymbol{W}^{(2)}}=\mathrm{prod}\left(\frac{\partial J}{\partial\boldsymbol{o}},\frac{\partial\boldsymbol{o}}{\partial\boldsymbol{W}^{(2)}}\right)+\mathrm{prod}\left(\frac{\partial J}{\partial s},\frac{\partial s}{\partial\boldsymbol{W}^{(2)}}\right)=\frac{\partial J}{\partial\boldsymbol{o}}\boldsymbol{h}^\top+\lambda\boldsymbol{W}^{(2)}. W(2)J=prod(oJ,W(2)o)+prod(sJ,W(2)s)=oJh+λW(2).
    沿着输出层向隐藏层继续反向传播,有 ∂ J ∂ h = p r o d ( ∂ J ∂ o , ∂ o ∂ h ) = W ( 2 ) ⊤ ∂ J ∂ o . \frac{\partial J}{\partial\boldsymbol{h}}=\mathrm{prod}\left(\frac{\partial J}{\partial\boldsymbol{o}},\frac{\partial\boldsymbol{o}}{\partial\boldsymbol{h}}\right)=\boldsymbol{W}^{(2)^\top}\frac{\partial J}{\partial\boldsymbol{o}}. hJ=prod(oJ,ho)=W(2)oJ.
    由于激活函数 ϕ \phi ϕ是按元素运算的,中间变量 z \boldsymbol{z} z的梯度 ∂ J / ∂ z ∈ R h \partial J/\partial \boldsymbol{z} \in \mathbb{R}^h J/zRh的计算需要使用按元素乘法符 ⊙ \odot ∂ J ∂ z = p r o d ( ∂ J ∂ h , ∂ h ∂ z ) = ∂ J ∂ h ⊙ ϕ ′ ( z ) . \frac{\partial J}{\partial z}=\mathrm{prod}\left(\frac{\partial J}{\partial\boldsymbol{h}},\frac{\partial\boldsymbol{h}}{\partial\boldsymbol{z}}\right)=\frac{\partial J}{\partial\boldsymbol{h}}\odot\phi^{\prime}\left(\boldsymbol{z}\right). zJ=prod(hJ,zh)=hJϕ(z).
    最终可得到第一个参数 W ( 1 ) W^{(1)} W(1)的梯度 ∂ J ∂ W ( 1 ) = prod ⁡ ( ∂ J ∂ z , ∂ z ∂ W ( 1 ) ) + prod ⁡ ( ∂ J ∂ s , ∂ s ∂ W ( 1 ) ) = ∂ J ∂ z x ⊤ + λ W ( 1 ) {\frac{\partial J}{\partial\boldsymbol{W}^{(1)}}=\operatorname{prod}\left(\frac{\partial J}{\partial\boldsymbol{z}},\frac{\partial\boldsymbol{z}}{\partial\boldsymbol{W}^{(1)}}\right)+\operatorname{prod}\left(\frac{\partial J}{\partial s},\frac{\partial s}{\partial\boldsymbol{W}^{(1)}}\right)=\frac{\partial J}{\partial\boldsymbol{z}}\boldsymbol{x}^\top+\lambda\boldsymbol{W}^{(1)}} W(1)J=prod(zJ,W(1)z)+prod(sJ,W(1)s)=zJx+λW(1)
二、autograd代码解析
各种类
  1. Op类:是TensorOp的父类,call、compute、gradient都在子类里各自实现
class Op:
    """Operator definition."""

    def __call__(self, *args):
        raise NotImplementedError()

    def compute(self, *args: Tuple[NDArray]):
        raise NotImplementedError()

    def gradient(self, out_grad: "Value", node: "Value") -> Union["Value", Tuple["Value"]]:
        raise NotImplementedError()

	# 此函数的作用是为了确保 gradient 方法的返回值始终以元组的形式返回。
	# 具体来说,如果 gradient 方法的返回值已经是元组,则直接返回该元组;如果返回值是列表,则将其转换为元组后返回;否则,将其包装成单元素的元组再返回。
	# 这个方法在某种程度上是一种封装,它保证了 gradient 方法返回值的一致性
    def gradient_as_tuple(self, out_grad: "Value", node: "Value") -> Tuple["Value"]:
        """ Convenience method to always return a tuple from gradient call"""
        output = self.gradient(out_grad, node)
        if isinstance(output, tuple):
            return output
        elif isinstance(output, list):
            return tuple(output)
        else:
            return (output,)
  1. TensorOp类:
# TensorOp是Op的子类,实现了父类里的__call__
class TensorOp(Op):
    """ Op class specialized to output tensors, will be alternate subclasses for other structures """

    def __call__(self, *args):
        # 如调用result=TensorOp()(tensor1, tensor 2),就会调用TensorOp的__call__函数
        # *args也就是输入的tensors
        # 这里make_from_op是在用算子对args里的tensor做运算,返回运算结果
        # 比如Tensor.make_from_op(EWiseAdd(), *(a, b))就是把a b加在一起返回
        return Tensor.make_from_op(self, args)  #定义在Value类中
  1. Value类:是Tensor的父类
    Value即计算图中的节点,如下代码可知一个节点中包含信息有:该节点的运算op、该节点的inputs、该节点的值(即inputs进行op后得到的值)、是否参与梯度计算
# Value是Tensor的父类
class Value:
    """A value in the computational graph."""

    # 用来追踪计算图
    op: Optional[Op] # 算子
    inputs: List["Value"] # 计算图中输入到这个结点的其他结点
    # 比如v3 = v1 + v2,那么v3.op就是加法算子EWiseAdd,v3.inputs是[v1, v2]
    
    # The following fields are cached fields for
    # dynamic computation
    cached_data: NDArray # 存储自身的数值
    requires_grad: bool # 是否参与梯度计算

    def realize_cached_data(self):
        """Run compute to realize the cached data"""
        # avoid recomputation
        if self.cached_data is not None:
            return self.cached_data
        # note: data implicitly calls realized cached data
        # 用自己的op算子和inputs输入算出自己的数值
        self.cached_data = self.op.compute(*[x.realize_cached_data() for x in self.inputs])
        return self.cached_data

    def is_leaf(self):
        # 叶节点是没有op的
        return self.op is None

    def __del__(self):
        global TENSOR_COUNTER
        TENSOR_COUNTER -= 1

    # 一个初始化函数,为op、inputs等赋初始值
    def _init(self, op: Optional[Op], inputs: List["Tensor"], *, num_outputs: int = 1, cached_data: List[object] = None, requires_grad: Optional[bool] = None):
        global TENSOR_COUNTER
        TENSOR_COUNTER += 1
        # any() 函数用于判断可迭代对象中是否存在至少一个为真的元素
        # 若可迭代对象中的任何一个元素为真,则返回 True;若所有元素都为假,则返回 False。
        # 如果用户没有明确指定requires_grad参数,那么根据inputs的情况来确定是否需要计算梯度
        # 如果至少有一个输入张量需要计算梯度,那么就将本节点requires_grad设为真;否则设为假。这样可以简化用户在使用时的输入,并且使得模型更加灵活。
        if requires_grad is None:
            requires_grad = any(x.requires_grad for x in inputs)
        self.op = op
        self.inputs = inputs
        self.num_outputs = num_outputs
        self.cached_data = cached_data
        self.requires_grad = requires_grad

    # 和子类Tensor里的一样
    # @classmethod让下面的函数不用实例化就可以调用,即直接Value.make_const()
    # 作用是创建了一个常量张量对象,并在创建时进行了初始化
    @classmethod
    def make_const(cls, data, *, requires_grad=False):
        value = cls.__new__(cls)  # 创建一个新的本类的实例对象(cls用法类似self,表示类本身,self表示实例本身)
        
        # 初始化这个新创建的实例,注意这里是上面定义的_init()而不是__init__()
        value._init(None, [], cached_data=data, requires_grad=requires_grad,)
        return value

    # 和子类Tensor里的一样
    @classmethod
    def make_from_op(cls, op: Op, inputs: List["Value"]):
        value = cls.__new__(cls)
        value._init(op, inputs)

        if not LAZY_MODE:
            if not value.requires_grad:
                return value.detach()
            value.realize_cached_data()
        return value
  1. Tensor类:
class Tensor(Value):
    grad: "Tensor"

	# 初始化张量对象
    def __init__(self, array, *, device: Optional[Device] = None, dtype=None, requires_grad=True, **kwargs):
        if isinstance(array, Tensor):
            if device is None:
                device = array.device
            if dtype is None:
                dtype = array.dtype
            if device == array.device and dtype == array.dtype:
                cached_data = array.realize_cached_data()
            else:
                # fall back, copy through numpy conversion
                # 如果dtype或device不一致,则重新创建
                cached_data = Tensor._array_from_numpy(
                    array.numpy(), device=device, dtype=dtype
                )
        else:
            device = device if device else cpu()
            cached_data = Tensor._array_from_numpy(array, device=device, dtype=dtype)

        # 给cached_data赋值,因为不是op产生的tensor,所以op和inputs都是空的
        self._init(None, [], cached_data=cached_data, requires_grad=requires_grad,)

    # 用后端生成数组
    @staticmethod
    def _array_from_numpy(numpy_array, device, dtype):
        if array_api is numpy:
            return numpy.array(numpy_array, dtype=dtype)
        return array_api.array(numpy_array, device=device, dtype=dtype)

    # 用op和inputs计算出一个新tensor
    @staticmethod
    def make_from_op(op: Op, inputs: List["Value"]):
        tensor = Tensor.__new__(Tensor)
        tensor._init(op, inputs)
        if not LAZY_MODE:
            tensor.realize_cached_data()
        return tensor

    # 返回一个无op,无inputs,只有cached_data,且默认不加入梯度计算的value
    @staticmethod
    def make_const(data, requires_grad=False):
        tensor = Tensor.__new__(Tensor)
        tensor._init(
            None,
            [],
            cached_data=data
            if not isinstance(data, Tensor)
            else data.realize_cached_data(),
            requires_grad=requires_grad,
        )
        return tensor

    # 可以通过.data获取没有梯度的数值,梯度下降更新参数时会用到
    @property
    def data(self):
        return self.detach()

    # 更新data的方法,这里要求更新前后dtype一致
    @data.setter
    def data(self, value):
        assert isinstance(value, Tensor)
        assert value.dtype == self.dtype, "%s %s" % (
            value.dtype,
            self.dtype,
        )
        self.cached_data = value.realize_cached_data()

    # 用make_const返回一个无梯度、在计算图外的tensor
    def detach(self):
        """Create a new tensor that shares the data but detaches from the graph."""
        return Tensor.make_const(self.realize_cached_data())

    # 因为目前只有numpy后端,所以shape就是numpy里的shape
    @property
    def shape(self):
        return self.realize_cached_data().shape

    @property
    def dtype(self):
        return self.realize_cached_data().dtype

    @property
    def device(self):
        data = self.realize_cached_data()
        # numpy array always sits on cpu
        # 如果后端是numpy,则只能用cpu
        if array_api is numpy:
            return cpu()
        return data.device

    # 用backward函数反向传播梯度
    def backward(self, out_grad=None):
        out_grad = out_grad if out_grad else Tensor(numpy.ones(self.shape))
        compute_gradient_of_variables(self, out_grad)

    def __repr__(self):
        return "needle.Tensor(" + str(self.realize_cached_data()) + ")"

    def __str__(self):
        return self.realize_cached_data().__str__()

    def numpy(self):
        data = self.realize_cached_data()
        if array_api is numpy:
            return data
        return data.numpy()

    # + 重载
    def __add__(self, other):
        if isinstance(other, Tensor):
            return needle.ops.EWiseAdd()(self, other)
        else:
            return needle.ops.AddScalar(other)(self)

    # * 重载
    def __mul__(self, other):
        if isinstance(other, Tensor):
            return needle.ops.EWiseMul()(self, other)
        else:
            return needle.ops.MulScalar(other)(self)

    # ** 重载
    def __pow__(self, other):
        if isinstance(other, Tensor):
            raise NotImplementedError()
        else:
            return needle.ops.PowerScalar(other)(self)

    # - 重载
    def __sub__(self, other):
        if isinstance(other, Tensor):
            return needle.ops.EWiseAdd()(self, needle.ops.Negate()(other))
        else:
            return needle.ops.AddScalar(-other)(self)

    # / 重载
    def __truediv__(self, other):
        if isinstance(other, Tensor):
            return needle.ops.EWiseDiv()(self, other)
        else:
            return needle.ops.DivScalar(other)(self)

    # @ 重载
    def __matmul__(self, other):
        return needle.ops.MatMul()(self, other)

    # matmul也可以不用运算符重载
    def matmul(self, other):
        return needle.ops.MatMul()(self, other)

    def sum(self, axes=None):
        return needle.ops.Summation(axes)(self)

    def broadcast_to(self, shape):
        return needle.ops.BroadcastTo(shape)(self)

    def reshape(self, shape):
        return needle.ops.Reshape(shape)(self)

    # 负号重载
    def __neg__(self):
        return needle.ops.Negate()(self)

    def transpose(self, axes=None):
        return needle.ops.Transpose(axes)(self)

    __radd__ = __add__
    __rmul__ = __mul__
    __rsub__ = __sub__
    __rmatmul__ = __matmul__
各种函数
  1. compute_gradient_of_variables
  2. find_topo_sort
  3. topo_sort_dfs
基于CMU 10-714 Deep Learning Systems 实现深度学习框架 needle 深度学习Deep Learning,简称DL)是机器学习(Machine Learning,简称ML)领域中一个新的研究方向,其目标是让机器能够像人一样具有分析学习能力,识别文字、图像和声音等数据。深度学习通过学习样本数据的内在规律和表示层次,使机器能够模仿视听和思考等人类活动,从而解决复杂的模式识别难题。 深度学习的核心是神经网络,它由若干个层次构成,每个层次包含若干个神经元。神经元接收上一层次神经元的输出作为输入,通过加权和转换后输出到下一层次神经元,最终生成模型的输出结果。神经网络之间的权值和偏置是神经网络的参数,决定了输入值和输出值之间的关系。 深度学习的训练过程通常涉及反向传播算法,该算法用于优化网络参数,使神经网络能够更好地适应数据。训练数据被输入到神经网络中,通过前向传播算法将数据从输入层传递到输出层,然后计算网络输出结果与实际标签之间的差异,即损失函数。通过反向传播算法,网络参数会被调整以减小损失函数值,直到误差达到一定的阈值为止。 深度学习中还包含两种主要的神经网络类型:卷积神经网络(Convolutional Neural Networks,简称CNN)和循环神经网络(Recurrent Neural Networks,简称RNN)。卷积神经网络特别擅长处理图像数据,通过逐层卷积和池化操作,逐步提取图像中的高级特征。循环神经网络则适用于处理序列数据,如文本或时间序列数据,通过捕捉序列中的依赖关系来生成模型输出。 深度学习在许多领域都取得了显著的成果,包括计算机视觉及图像识别、自然语言处理、语音识别及生成、推荐系统、游戏开发、医学影像识别、金融风控、智能制造、购物领域、基因组学等。随着技术的不断发展,深度学习将在更多领域展现出其潜力。 在未来,深度学习可能会面临一些研究热点和挑战,如自监督学习、小样本学习、联邦学习、自动机器学习、多模态学习、自适应学习、量子机器学习等。这些研究方向将推动深度学习技术的进一步发展和应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值