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^{n-1}導數公式。
乘數的梯度爲:被乘數乘以結果的梯度
被乘數的梯度爲:乘數乘以結果的梯度
驗證
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