从头开始:反向模式自动区分(Python中)

2020-06-15 02:35:02

自动区分是深度学习框架的基础。深度学习模型通常使用基于梯度的技术进行训练,Autodiff使得即使是从巨大的、复杂的模型中也很容易获得梯度。“反向模式自动比较”是大多数深度学习框架使用的自动比较方法,因为它的效率和准确性。

小型autodiff框架将处理标量。我们可以(稍后)使用NumPy将其矢量化。

术语说明:从现在开始,“autodiff”将指“反向模式autodiff”。“Gradient”用法松散,但在本文中通常指“一阶偏导数”。

a=4 b=3 c=a+b#=4+3=7 d=a*c#=4*7=28。

问题1:$d$相对于$a$的梯度是多少,即$\frac{\Partial{d}}{\Partial{a}}$?(来试试这个吧!)。

有很多方法可以求解Q1,但我们使用乘积规则,即如果$y=x_1x_2$,则$y‘=x_1’x_2+x_1x_2‘$。

phew…。如果你想知道$\frac{\Partial{d}}{\Partial{b}}$,你必须重新执行这个过程。

现在我们来看一下解决Q1问题的自动求差方法。以下是一个数字:

在左边,我们看到系统用图形表示。每个变量都是一个节点;例如,$d$是最上面的节点,$a$和$b$是最下面的叶节点。

在右边,我们从autodiff的角度看系统。让我们将图边上的值称为局部导数。通过使用局部导数和简单的规则,我们将能够计算出我们想要的导数。

这是Q1的答案,用自动差分法计算。你能看出它与这个数字有什么关系吗?

我们通过查找从$d$到$a$(不与虚线箭头相反)的路由,然后应用以下规则,从图中获得此答案:

第一条路线是直接从$d$到$a$,这给我们提供了$\frac{\Partial{\bar{d}{\Partial{a}}$Term。第二条路线是从$d$到$c$再到$a$,这给出了术语$\frac{\Partial{\bar{d}{\Partial{c}}*\frac{\Partial{\bar{c}{\Partial{a}}$。

我们的autodiff实现将沿着图向下,计算$d$相对于每个子节点的导数,而不是像我们刚才对$d$所做的那样,只计算特定节点的导数。请注意,我们可以计算$d$相对于$c$和$b$的梯度,而不需要太多的工作。

我们在上面的图形边上看到了“本地导数”,其形式为:$\frac{\Partial\bar{y}}{\Partial x}$。

一般而言:要获得局部导数,请将进入节点的变量视为不是其他变量的函数。

例如,回想一下$d=a*c$。然后将$\frac{\Partial{d}}{\Partial{a}}=2a+b$与$\frac{\Partial\bar{d}}{\Partial a}=c$进行比较。局部导数$\frac{\PARTIAL\BAR{d}}{\PARTIAL a}=c$,是在对$d$的表达式进行微分之前将$c$视为常量而获得的。

定义简单函数的局部导数通常很容易,如果您知道局部导数,那么将函数添加到autodiff框架中也很容易。例如:

加法:$n=a+b$,本地导数为:$\frac{\Partial\bar{n}}{\Partial a}=1$和$\frac{\Partial\bar{n}}{\Partial b}=1$。

乘法:$n=a*b$,本地导数为:$\frac{\Partial\bar{n}}{\Partial a}=b$和$\frac{\Partial\bar{n}}{\Partial b}=a$。

函数get_gradients使用节点的渐变数据递归地遍历图形,计算渐变。它使用我们在上面看到的规则:

堆栈中的元组(在get_gradients中)类似于grad中的元组,但是它们包含当前路由值,而不是局部导数值。

从集合导入defaultdict类Var:";";";一个叶节点(没有子节点)。";";";def__init__(self,value):self。value=value#节点的标量值。类add:";";";";";";";";def__init__(self,a,b):self。值=a。值+b。重视自我。grad=[(a,1),(b,1)]#子节点&;对应';本地导数&39;class mul:";";";";";";def__init__(self,a,b):self。值=a。值*b。重视自我。梯度=[(a,b.。值)、(b,a。value)]def get_gradients(Parent_Node):";";";";往下走,计算`parent_node`对每个节点的导数。";";";";gradients=defaultdict(lambda:0)stack=parent_node。格拉德。copy()#(节点,ROUTE_VALUE)元组列表。而STACK:NODE,ROUTE_VALUE=STACK。POP()梯度[node]+=route_value#";将不同的路由相加。";如果没有isinstance(node,Var):#如果节点有子节点,则将它们放到堆栈中。对于CHILD_NODE,节点中的CHILD_ROUTE_VALUE。葛兰德:堆叠。APPEND((CHILD_NODE,CHILD_ROUTE_VALUE*ROUTE_VALUE))#";将路径的边相乘。";RETURN DICT(渐变)。

a=Var(4)b=Var(3)c=add(a,b)#=4+3=7 d=mul(a,c)#=4*7=28渐变=get_gradients(D)print(';d.value=';,d..。值)打印(";d关于a的偏导数=";,梯度[a])

类操作:";";";允许使用+、*、-等。";";";def__add__(self,ther):return add(self,Other)def__mul__(self,ther):return MUL(self,Other)def__sub__(self,Other):return add(self,Neg(Other))def__truediv__(self,Other):return Mul(self,Inv(Other))class Var(Ops):def__init_(self,value):self。value=值类添加(操作):def__init__(self,a,b):self。值=a。值+b。重视自我。grad=[(a,1),(b,1)]class mul(Ops):def__init__(self,a,b):self。值=a。值*b。重视自我。梯度=[(a,b.。值)、(b,a。value)]class neg(运算):def__init__(self,var):self。值=-1*var。重视自我。grad=[(var,-1)]类存货(操作):def__init__(self,var):self。值=1/var。重视自我。grad=[(var,-var.。值**-2)]。

我们可以从我们添加到框架中的函数中获得任意函数的梯度。例如:

a=Var(230.3)b=Var(33.2)def(a,b):return(a/b-a)*(b/a+a+b)*(a-b)y=f(a,b)梯度=获取梯度(Y)print(";y关于a的偏导数=";,梯度[a])print(";y关于b的偏导数=";,渐变[b])。

y对a的偏导数=-153284.83150602411 y对b的偏导数=3815.0389441500993。

Δ=Var(1e-8)Numerical_grad_a=(f(a+δ,b)-f(a,b))/Delta Numerical_grad_b=(f(a,b+Delta)-f(a,b))/Delta Print(";a=#34;,Numerical_grad_a的数值估计。value)打印(";b=";,Numerical_grad_b.。值)。

将numpy导入为NP类Sin(Ops):def__init__(self,var):self。值=NP。Sin(var.。值)自我。梯度=[(var,np。Cos(var.。value))]class Exp(Ops):def__init__(self,var):self。值=NP。EXP(变量。值)自我。grad=[(var,sel.。value)]类日志(Ops):def__init__(self,var):self。值=NP。LOG(变量。值)自我。grad=[(var,1./var.。值)]。

A=Var(43.。)。B=Var(3.)。C=Var(2.)。def(a,b,c):F=Sin(a*b)+Exp(c-(a/b))返回Log(f*f)*Cy=f(a,b,c)梯度=get_gradients(Y)print(";y关于a的偏导数=";,梯度[a])print(";y关于b的偏导数=";,渐变[b])打印(";y关于c的偏导数=";,渐变[c])。

y对a的偏导数=60.85353612046653 y对b的偏导数=872.2331479536114 y对c的偏导数=-3.2853671032530305

Delta=Var(1e-8)Numerical_grad_a=(f(a+Delta,b,c)-f(a,b,c))/Delta Numerical_grad_b=(f(a,b+Delta,c)-f(a,b,c))/Delta Numerical_grad_c=(f(a,b,c+Delta)-f(a,b,c))/Delta print(";a=#34;,Numerical_grad_a的数值估计。value)打印(";b=";,Numerical_grad_b.。value)打印(";c=";,Numerical_grad_c的数值估计。值)。

a=60.85352186602222的数值估计b=872.232160009645 c=-3.285367089489455的数值估计。

对我们的最小框架最有效的补充将是向量化(但不是以下面的方式执行)。

我们将研究一种计算效率极低的方法来向量化我们的autodiff框架。(不推荐将其用于任何事情,因为它太慢了。)。

将numpy导入为np to_var=np。Vector torize(lambda x:var(X))#将NumPy数组转换为Var对象数组TO_VERVIES=NP。矢量化(λ变量:变量。value)#从Var对象数组中获取值。

将matplotlib.pylot导入为PLT def update_weights(权重、渐变、速率):对于范围内的i(权重。Shape[0]):对于范围内的j(权重。Shape[1]):权重[i,j]。value-=lrate*梯度[权重[i,j]]np。随机的。SEED(0)INPUT_SIZE=50 OUTPUT_SIZE=10 LARATE=0.001 x=TO_VAR(NP。随机的。RANDOM(INPUT_SIZE))y_TRUE=to_var(np.。随机的。随机(Output_Size))权重=to_var(np.。随机的。随机((INPUT_SIZE,OUTPUT_SIZE)),对于范围(100)内的i,LASS_VALUES=[]:Y_PRED=Np。点(x,重量)损失=Np。SUM((y_true-y_pred)*(y_true-y_pred))LOSS_VALUES。追加(损失。值)渐变=get_gradients(损耗)update_weights(权重、渐变、速率)PLT。绘制(亏损)PLT。xlabel(";时间点";);PLT。ylabel(";亏损&34;);PLT。标题(#34;单线性层学习)PLT。show()。

马上就要来了:第二部分,我们将研究如何更高效地向量化我们的最小框架。

份额: