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
同时可以观察到这里梯度使用了累加,这是因为变量在计算图中存在多个影响路径,如果某个变量在计算图中通过多个路径影响输出,其梯度是各路径贡献的累加,因此需要使用 += 而非 =
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:
- Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
- Displaying External Posts on Your al-folio Blog
- Graph RAG with Milvus —— 纯向量库造图的多跳推理
- Hierarchical Indices 层级索引 —— 先粗后细的两级检索
- HyDE 与 HyPE —— 假设检索技术的两个方向
- 二叉树刷题总结
- RAG 技术体系化分类——从 Pipeline 阶段到失败模式
- MemoRAG 记忆增强型 RAG 总结
- Microsoft GraphRAG 基于知识图谱的 RAG 总结
- Multimodal RAG with Captioning 图像描述型多模态 RAG 总结