microPytorch

实现迷你Pytorch

这是教学视频:

这里面主要讲解他自己写的库Micrograd,基本上是一个_autograd_(自动梯度)引擎,它的真正作用是实现反向传播。 反向传播是一种算法,可以有效地计算神经网络中某种损失函数相对于权重的梯度。这让我们能够迭代地调整神经网络的权重,以最小化损失函数,从而提高网络的准确性。因此,反向传播将成为任何现代深度神经网络库(如PyTorch、JAX)的数学核心。 如果详细来讲,神经网络是一个计算的式子(更加类似与公式?)不过它可以自动迭代并且更新,了解导数可以更加方便了解神经网络(维度的多少跟核心的数学思想并无关联)。

导数,就跟我们之前在中学里面学习的一样 \(f'(x_0) = \lim_{\Delta x \to 0} \frac{f(x_0 + \Delta x) - f(x_0)}{\Delta x}\) 神经元就像作一个个小单元小积木一样,我们像搭乐高一样搭建,但我们需要清楚其中的运算,至少得追根溯源吧

class Value:
	def __init__(self, data, _children=(), _op=''):
	        self.data = data
	        self._prev = set(_children)
	        self._op = _op
			self.grad = 0

    def __repr__(self):
        return f"Value({self.data})"

    def __add__(self, other):
        out = Value(self.data + other.data, (self, other), '+')
        return out

    def __mul__(self, other):
        out = Value(self.data * other.data, (self, other), '*')
        return out

作者通过维护这么一个类,记录下一个对象所作的操作_op,从而做到追根溯源 作者同时也做了个可视化的界面

from graphviz import Digraph

def trace(root):
    # builds a set of all node and edges in the graph
    nodes, edges = set(), set()
    def build(v):
        if v not in nodes:
            nodes.add(v)
            for child in v._prev:
                edges.add((child, v))
                build(child)
    build(root)
    return nodes, edges

def draw_dot(root):
    dot = Digraph(format='svg', graph_attr={'rankdir': 'LR'}) # LR = left to right

    nodes, edges = trace(root)
    for n in nodes:
        uid = str(id(n))
        # for many value in graph, create a rectangular ('record') node for it
        dot.node(name = uid, label = "{ %s | data %.4f}" % (n.label, n.data), shape = 'record')
        if n._op:
            # if this value is a result of some operation, create an op node for it
            dot.node(name = uid + n._op, label = n._op)
            # and connect this node for it
            dot.edge(uid + n._op, uid)

    for n1, n2 in edges:
        # connect n1 to the op node of n2
        dot.edge(str(id(n1)), str(id(n2)) + n2._op)

    return dot

draw_dot(d)

反向传播

在导数方面中链式法则可以帮助我们更好的理解反向传播 链式法则更像是找到一个中间量,从而帮助我们去测量,度量另外两个变量之间的斜率 \(\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} \quad \text{或} \quad (f \circ g)'(x) = f'(g(x)) \cdot g'(x)\) 在神经网络中,每个节点的操作都会对梯度的传播方式产生影响。对于加法节点,假设有两个输入 a 和 b,它们相加得到 c = a + b 。在反向传播过程中,c 相对于 a 和 b 的梯度都分别是 1。

在加法节点中,梯度会被“原样”传递给所有输入节点。这意味着加法节点不会改变梯度的大小,而是将梯度均匀分配给所有输入。这是因为加法操作对于每个输入来说是线性的,并且每个输入对输出的贡献是独立的。 乘法也是一样的,也是取导数,然后进行计算,就类似于代值进去运算。 通过反向传播,可以知道变量的梯度,在优化问题中可以通过梯度来改进模型

神经网络

就像我们学习的一样 输入,求和,激活函数 在视频中

x1 = Value(2.0, label='x1')
x2 = Value(0.0, label='x2')

# weights w1,w2, 每个输入的突触强度
w1 = Value(-3.0, label='w1')
w2 = Value(1.0, label='w2')

# bias b 偏置
b = Value(6.8813735870195432, label='b')

# x1*w1 + x2*w2 + b
x1w1 = x1 * w1; x1w1.label = 'x1w1'
x2w2 = x2 * w2; x2w2.label = 'x2w2'
x1w1x2w2 = x1w1 + x2w2; x1w1x2w2.label = 'x1w1x2w2'
n = x1w1x2w2 + b; n.label = 'n' # 细胞体的原始激活值

激活函数使用的是tanh

# 现在用一个激活函数来处理,这里使用tanh,在先前的类中实现。
def tanh(self):
        n = self.data
        t = (math.exp(2*n) - 1)/(math.exp(2*n) + 1)
        out = Value(t, (self,), 'tanh')
        return out

后面再通过导数计算求出各个神经元的导数值 例如视频中的n节点是求和以后的点,而o节点是经过激活函数tanh的节点 那么求出n节点的导数就是

\[\begin{aligned} o &= \tanh(n), \\ \frac{do}{dn} &= 1 - \tanh(n)^2 \\ &= 1 - o^2 \\ &= 0.5 \end{aligned}\]

自动化

现实里不可能让你手动更新这些梯度的,所以需要编写让其自动更新 根据上文的导数的相关概念,可以清楚的明白 对于加法来说,导数是1 对于乘法来说,可以看作 \(\frac{dL}{da} = \frac{dL}{dc} \cdot \frac{dc}{da} \quad\) 其中\(\frac{dL}{dc}\)可以看作是是out的梯度 \(\frac{dc}{da}\) 可以看作ab=c中a的导数b 类中定义一个

class Value:
	def __init__(self, data, _children=(), _op='', label =''):
	        self.data = data
	        self._prev = set(_children)
	        self._op = _op
			self.grad = 0
			self._backward = lamada: None
			self.label = label

    def __repr__(self):
        return f"Value({self.data})"

    def __add__(self, other):
		other = other if isInstance(other,Value) else Value(other)
        out = Value(self.data + other.data, (self, other), '+')
		    def backward():
			    self.grad += 1
			    other.grad += 1
		self._backward=backward
        return out

    def __sub__(self, other):
        return self + (-other)

    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
		self._backward=backward
		return out

	def __truediv__(self, other):
        return self * other**-1

	def __pow__(self, other):
	        assert isinstance(other, (int, float))
	        out = Value(self.data**other, (self,), f'**{other}')

	def Backward(self):
		out.grad = 1
		topo = []
		visited = set()
		def build_topo(v):
			if v not in visited:
				visited.add(v)
				for child in v._prev:
					build_topo(child) #DFS search
				topo.append()
		build_topo(self)
		for node in reversed(topo):
			node._backward()

    def __rmul__(self, other):
        return self.__mul__(other)
    def exp(self):
		out = Value(math.exp(self.data(),(self,),'exp')
		def backward():
			    self.grad = out.data * out.grad
		out._backward=backward
		return out

    def __rmul__(self, other):
        return self.__mul__(other)

可以观察到代码里面

if v not in visited:
    visited.add(v)
    for child in v._prev:  # 遍历所有前驱节点(输入节点)
        build_topo(child)  # 递归搜索枝叶
    topo.append(v)        # 后序加入拓扑列表

是一种常见的拓扑排序的实现:DFS+后序遍历,让根节点(最后的输出)在集合的最后 确保反向传播时按 ​​计算图的依赖逆序​​(从输出到输入)处理节点,避免梯度计算顺序错误

def backward():
	self.grad += other.data * out.grad
	other.grad += self.data * out.grad

同时可以观察到这里梯度使用了累加,这是因为​​变量在计算图中存在多个影响路径​​,如果某个变量在计算图中通过多个路径影响输出,其梯度是各路径贡献的累加,因此需要使用 += 而非 =

\[\frac{\partial L}{\partial \text{self}} = \sum_{\text{path}} \frac{\partial L}{\partial \text{out}} \cdot \frac{\partial \text{out}}{\partial \text{self}}\]

Pytorch中对应的操作

import torch

# 初始化张量并设置requires_grad=True用于自动求导
x1 = torch.tensor([2.0], dtype=torch.double, requires_grad=True)
x2 = torch.tensor([0.0], dtype=torch.double, requires_grad=True)
w1 = torch.tensor([-3.0], dtype=torch.double, requires_grad=True)
w2 = torch.tensor([1.0], dtype=torch.double, requires_grad=True)
b = torch.tensor([6.8813735870195432], dtype=torch.double, requires_grad=True)

# 前向传播计算
n = x1 * w1 + x2 * w2 + b
o = torch.tanh(n)

# 打印输出值
print(o.item())

# 反向传播计算梯度
o.backward()

# 打印各变量的梯度值
print('x2', x2.grad.item())
print('w2', w2.grad.item())
print('x1', x1.grad.item())
print('w1', w1.grad.item())



Enjoy Reading This Article?

Here are some more articles you might like to read next: