micrograd为一个自动梯度引擎,其实现了反向传播算法,用于学习理解深度学习中的自动求导原理。自动求导无论再传统的机器学习中还是深度学习或是目前非常热门的大语言模型GPT中其都是非常重要基础部分。
反向传播算法可以高效计算出神经网络中损失函数关于训练权重的梯度,使得可通过快速迭代调整权重已得到损失函数最小值,从而求得最佳权重;反向传播为各类深度学习神经网络框架的数学基础无论是PyTorch还是Tensorflow等其都是必不可少的。
micrograd的核心实现在于engine.py中,Value类定义了反向传播自动求导的具体实现,由于此框架是学习用的,只实现了标量的运算并没有涉及到矩阵运算;
原理
如有函数: y = x * w + b
前向传播:通过带入数据求出y,在将所计算的值带入损失函数求得误差值。
X为数据,w为权重,b为偏置(学习率)
平均损失函数为:loss = (y_t-y) **2
通过前向传播计算完后得到了:预测值y,损失值:loss
反向传播
在正向传播遍历计算图求出每个节点的值后通过反向遍历整个图,计算出每个节点的偏导,其原理为微积分链式法则,只需要一个前向传播、一个反向传播就可以求得所有参数的导数,所以性能很高。
根据前向传播所得到的损失值loss,计算得出loss关于模型参数w、b的梯度,然后调整模型参数w、b。
参数调整为:参数减去(梯度*学习率)
需关注重点为参数的梯度如何取得,这就是偏导数、链式法则的应用。
zero_grad(w,b)
loss.backward()
step(w,brate)
反复迭代,再误差达到指定精度或epochs时停止;
具体实现
def __init__(self, data, _children=(), _op=''):
self.data = data
self.grad = 0
# internal variables used for autograd graph construction
self._backward = lambda: None
self._prev = set(_children)
self._op = _op # the op that produced this node, for graphviz / debugging / etc
Value初始化中最重要两个参数,data保存元素原始数据,grad保存当前元素梯度。
_backward() 方法保存反向传播的方法,用于计算反向传播得到梯度
_prev保存当前节点前置节点。通过遍历获取_prev节点,可得到完整的运算链路。
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 += out.grad
other.grad += out.grad
out._backward = _backward
return out
在加法中元素其梯度为其结果的梯度。
某个元素可能涉及到多个运算链路,所以其梯度为不同链路所确定的梯度之和。所以此处为 += out.grad。
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
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
def backward(self):
# topological order all of the children in the graph
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v)
build_topo(self)
# go one variable at a time and apply the chain rule to get its gradient
self.grad = 1
for v in reversed(topo):
v._backward()
乘法与次方实现也分别使用了链式法则与f'(x)=nx^导数公式。
乘数的梯度为:被乘数乘以结果的梯度
被乘数的梯度为:乘数乘以结果的梯度
验证
a = Value(2,'a')
b = Value(3,'b')
c = a * b**2
#c =a + b
c.backward()
draw_dot(c)
∂c/∂a=9
∂c/∂b=12