【深度学习】基于计算图的反向传播详解

计算图

计算图就是将计算过程用图形表示出来,这里所说的图形是数据结构图,通过多个节点和边表示(边是用来连接节点的)。

下面我们先来通过一个简单的例子了解计算图的计算过程

假设我们有如下需求:

  • 一个苹果100块钱,一个橘子150块钱
  • 消费税为10%
  • 买了2个苹果,3个橘子,一共需要支付多少钱?

1、根据需要构建计算图

【深度学习】基于计算图的反向传播详解

2、在计算图上从左向右进行计算

  • 按着图中箭头方向“从左向右进行计算”称为正向传播,即从计算图的出发点到结束点的传播
  • 自然,“从右往左计算”称为反向传播

3、局部计算

  • 上图中,我们对于苹果和橘子的计算是分开的,然后合并。这里的局部也就是在计算过程中只将与自己相关的信息进行计算输出结果;
  • 计算图可以集中精力于局部计算,无论全局多么复杂,各个步骤说要做的就是对象节点的局部计算,这样就可以通过局部计算,将结果传递下去,就可以获得全局的复杂计算结果

4、计算图求解的好处

  • 局部计算,可以是各个节点只致力于简单的计算,从而简化问题
  • 利用计算图可以将中间的计算结果保存起来,以免重复计算
  • 可以通过反向传播高效计算导数这一点是最重要的

我们来考虑一个问题:

  • 假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果的价格的导数”
  • 设苹果价格为xx,支付金额为LL,则相当于求Lx\frac{\partial L}{\partial x}

【深度学习】基于计算图的反向传播详解

图中,反向传播“局部导数”,将导数的值卸载箭头下方,图中红色表示
从右向左(1 -> 1.1 -> 2.2 )
这意味着,如果苹果的价格上涨1块钱,最终的支付金额会增加2.2块钱
关于如何计算的,后面会介绍
同样的我们也可以计算出“支付金额关于苹果个数的导数”、“支付金额关于消费税的导数”

链式法则

  • 关于偏导数的链式法则,在高等数学中有具体内容,如果不知道可以参考下
  • 从上面的计算我们知道正向传播计算过程就是我们日常的计算过程,所以很容易理解
  • 反向传播局部导数的原理,就是基于链式法则的

1、计算图的方向传播

【深度学习】基于计算图的反向传播详解

反向传播的计算顺序:

  • 将信号 EE 乘以节点的局部导数 yx\frac{\partial y}{\partial x}
  • 传递给下一个节点

通过这样的计算,可以高效地求出导数的值

2、链式法则和计算图

z=t2t=x+yz = t^2 \\ t = x+y

【深度学习】基于计算图的反向传播详解

反向传播的计算顺序:

  • 将节点的输入信号乘以节点的局部导数(偏导数),然后传递给下一个节点
    比如,反向传播时,“**2”节点的输入时 zz\frac{\partial z}{\partial z},将其乘以局部导数 zt\frac{\partial z}{\partial t}
  • 然后再将上一步的输出 zzzt\frac{\partial z}{\partial z} \frac{\partial z}{\partial t} 作为下一节点的输入,同样乘以局部导数 tx\frac{\partial t}{\partial x}

根据链式法则:

  • zzzttx=zttx=zx\frac{\partial z}{\partial z} \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} = \frac{\partial z}{\partial x}
    对应于“ zz 关于 xx 的导数”

反向传播

1、加法节点的反向传播实现

这里以 z=x+yz = x+y 为对象来说明

zx=1\frac{\partial z}{\partial x} = 1
zy=1\frac{\partial z}{\partial y} = 1

【深度学习】基于计算图的反向传播详解

如图,zx=1zy=1\frac{\partial z}{\partial x} = 1 ,\frac{\partial z}{\partial y} = 1
反向传播将上游传过来的导数乘以1,然后传向下游

也就是说,因为加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点

  • python实现加法层
class AddLayer:
    def __init__(self):
        pass
	# 正向传播
    def forward(self, x, y):
        out = x + y

        return out
	# 反向传播
    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

2、乘法节点的反向传播实现

这里以 z=xyz = xy 为对象来说明

Lz=y\frac{\partial L}{\partial z} = y
zy=x\frac{\partial z}{\partial y} = x

【深度学习】基于计算图的反向传播详解

乘法节点的反向传播需要正向传播时的输入信号值,因此,实现乘法节点的反向传播时,需要保存正向传播的输入信号

乘法节点的反向传播会乘以输入信号的翻转值

  • python实现乘法层
class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None
	# 正向传播
    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out
	# 反向传播
    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy

# 测试
apple = 100 # 苹果价格
apple_num = 2 # 苹果个数
tax = 1.1 # 消费税

mul_apple_layer = MulLayer() # 创建乘法器对象
mul_tax_layer = MulLayer() # 创建乘法器对象

# forward
apple_price = mul_apple_layer.forward(apple, apple_num) # 2个苹果的价格
price = mul_tax_layer.forward(apple_price, tax) # 支付金额

# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice) 
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dTax:", dtax)
输出为:
price: 220
dApple: 2.2
dApple_num: 110
dTax: 200

【深度学习】基于计算图的反向传播详解

结果与上图中的反向传播的结果一样

  • 苹果和橘子的例子的实现
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
输出为:
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650

【深度学习】基于计算图的反向传播详解

**函数层的反向传播实现

1、ReLU层

  • 数学表达式:
    y={x(x>0)0(x0)y = \begin{cases} x \quad (x > 0) \\ 0 \quad (x \leqslant0) \end{cases}
  • y关于x的导数:
    yx={1(x>0)0(x0)\frac{\partial y}{\partial x} = \begin{cases} 1 \quad (x > 0) \\ 0 \quad (x \leqslant0) \end{cases}

由导数可知,

  • 如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传递给下游(乘以1)
  • 如果正向传播时的输入x小于等于0,则反向传播传递给下游的信号将停在此处(乘以0)
  • python实现
class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx
  • 其中
    【深度学习】基于计算图的反向传播详解

会将输入中小于等于0的,改为0,大于0的保留

ReLu层的作用就像电路开关一样,正向传播时,有电流通过的话,就将开关设为ON;没有电流通过的话,就将开关设为OFF。

反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话,则不会有电流通过。

2、Sigmoid层

  • 数学表达式为:
    y=11+exy = \frac{1}{1 + e^{-x}}

  • 计算图如下:
    【深度学习】基于计算图的反向传播详解

其中,除了我们之前介绍过的“×”和“+”节点之外,还多了“exp”和“/”节点。
“exp”节点会进行y=exp(x)y = exp(x)的计算,“/”节点会进行y=1xy = \frac{1}{x}的计算

  • 对于“/”节点,y=1xy = \frac{1}{x}

  • 导数为:
    yx=1x2=y2\frac{\partial y}{\partial x} = -\frac{1}{x^2} = -y^2

  • 对于“exp”节点,y=exp(x)y = exp(x)

  • 导数为:
    yx=exp(x)\frac{\partial y}{\partial x} = exp(x)

  • 反向传播如下图:
    【深度学习】基于计算图的反向传播详解

其中

  • Lyy2exp(x)=Lyy(y1)\frac{\partial L}{\partial y}y^2exp(-x) = \frac{\partial L}{\partial y} y(y-1)
  • 上图可转化为:
    【深度学习】基于计算图的反向传播详解

  • python实现

import numpy as np
def sigmoid(x):
    return 1 / (1 + np.exp(-x)) 

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

正向传播时将输出保存在变量out中,然后,反向传播时,使用该变量进行计算

加权信号的计算图以及反向传播实现

此文中我们介绍了神经网络的内积,神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算
Y=np.dot(X,W)+BY = np.dot(X, W) + B

  • 计算图如下:
    【深度学习】基于计算图的反向传播详解

  • 以矩阵为对象的反向传播
    LX=LYWT\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \cdot W^T
    LW=XTLY\frac{\partial L}{\partial W} = X^T\cdot\frac{\partial L}{\partial Y}

其中WTW^TWW的转置

  • 计算图的反向传播

【深度学习】基于计算图的反向传播详解

  • 批量版本

【深度学习】基于计算图的反向传播详解

与前面的不同之处在于输入X的形状为(N,2),方向传播时,注意矩阵的形状,就可以和前面一样推到出LXLW\frac{\partial L}{\partial X} ,\frac{\partial L}{\partial W}
此外要注意:

  • 正向传播时,偏置B被加到X·W的各个数据上;
  • 反向传播时,各个数据的反向传播的值需要汇总为偏置的元素

【深度学习】基于计算图的反向传播详解

上例中,假设数据有2个(N=2),偏置的反向传播会对这2个数据的导数暗元素进行求和

  • python实现
class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        # 权重和偏置参数的导数
        self.dW = None
        self.db = None

    def forward(self, x):
        # 对应张量
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)  # 还原输入数据的形状(对应张量)
        return dx

Softmax-with-Loss层的反向传播实现

  • Softmax-with-Loss的计算图
  • 这里给出softmax函数和交叉熵误差的计算图:

【深度学习】基于计算图的反向传播详解

上图表示:
假定了一个3类别分类的神经网络,从前面的层输入的是(a1,a2,a3)(a_1, a_2, a_3),softmax层输出(y1,y2,y3)(y_1, y_2, y_3)。此外,监督标签是(t1,t2,t3)(t_1, t_2, t_3)
Cross-Entropy-Error层输出的是损失LL
Softmax-with-Loss层的反向传播的结果为(y1t1,y2t2,y3t3)(y_1-t_1, y_2-t_2, y_3-t_3)

下面我们来详细介绍Cross-Entropy-Error层和Softmax层

  • Cross-Entropy-Error层
  • 数学表达式:
    L=ktklog ykL = -\sum_k t_k log \ y_k
  • 计算图如下:
    【深度学习】基于计算图的反向传播详解

上图中多个“log”节点
“log”节点,y=log xy = log\ x
导数:
yx=12\frac{\partial y}{\partial x} = \frac{1}{2}

  • “×”节点:反向传播时将正向传播的输入值翻转,乘以上游传过来的导数后,传递给下游
  • “+”节点:将上游传来的导数原封不动地传递给下游

Cross-Entropy-Error层的反向传播的结果为:
(t1y1t2y2t3y3)(-\frac{t_1}{y_1},-\frac{t_2}{y_2},-\frac{t_3}{y_3})
是传递给softmax层反向传播的输入

  • Softmax层
  • 数学表达式:
    yk=eaki=1neaiy_k = \frac{e^{a_k}}{\sum_{i=1}^{n} e^{a_i}}
  • 计算图如下:

【深度学习】基于计算图的反向传播详解

  • 反向传播计算步骤:
  • 1.前面的层Cross-Entropy-Error层的反向传播的值传过来
  • (t1y1t2y2t3y3)(-\frac{t_1}{y_1},-\frac{t_2}{y_2},-\frac{t_3}{y_3})
  • 2.“×”节点将正向传播的值翻转后相乘(这里有两个分支)
  • (1)
  • (t1y1exp(a1)t2y2exp(a2)t3y3exp(a3))(-\frac{t1}{y1}exp(a_1),-\frac{t2}{y2}exp(a_2),-\frac{t3}{y3}exp(a_3))
  • 其中:
  • t1y1exp(a1)=t1Sexp(a1)exp(a1)=t1S-\frac{t1}{y1}exp(a_1) = -t1\frac{S}{exp(a_1)}exp(a_1) = -t1S
  • 故第一个分支的结果为:
  • (t1St2St3S)(-t1S,-t2S,-t3S)
  • (2)
  • (t1y11St2y21St3y31S)(-\frac{t1}{y1}\frac{1}{S},-\frac{t2}{y2}\frac{1}{S},-\frac{t3}{y3}\frac{1}{S})
  • 其中:
  • t1y11S=t1exp(a1)S1S=t1exp(a1)-\frac{t1}{y1}\frac{1}{S} = -\frac{t1}{\frac{exp(a_1)}{S}}\frac{1}{S} = -\frac{t_1}{exp(a_1)}
  • 故第二个分支的结果为:
  • (t1exp(a1)t2exp(a2)t3exp(a3))(-\frac{t_1}{exp(a_1)},-\frac{t_2}{exp(a_2)},-\frac{t_3}{exp(a_3)})
  • 3.若正向传播时有分支流出,则反向传播时它们的反向传播的值会相加,然后再进行“/”节点的反向传播
  • (t1S+t2S+t3S)×(1S2)-(t1S+t2S+t3S) \times(-\frac{1}{S^2})
  • t1+t2+t3S\frac{t1+t2+t3}{S}
  • 又因为监督标签是one-hot表示的,所以t1+t2+t3=1t_1+t_2+t_3=1,故结果为:
  • 1S\frac{1}{S}
  • 4.“+”节点原封不动地传递上游的值
  • 1S\frac{1}{S}
  • 5.“exp”节点,将两个分支的输入乘以exp(a1)exp(a_1)后的值就是我们要求的反向传播的值
  • 针对a1a_1
  • (1St1exp(a1))exp(a1)=y1t1(\frac{1}{S} - \frac{t1}{exp(a_1)})exp(a_1)=y_1-t_1
  • 故反向传播的结果为:
  • (y1t1y2t2y2t2)(y_1-t_1,y_2-t_2,y_2-t_2)

通过结果我们可以发现,Softmax层反向传播的输出是输出标签和监督标签的差分

神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax层的输出)接近监督标签,而前面的结果直截了当地表示了神经网络的输出与监督标签的误差。

神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质

  • python实现
# softmax函数
def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 溢出对策
    return np.exp(x) / np.sum(np.exp(x))
    
# 交叉熵误差    
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

    
class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmax的输出
        self.t = None # 监督数据

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            # 反向传播时,要将传播的值除以批的大小后,传递给前面的层的是单个数据的误差
            dx = dx / batch_size
        
        return dx

总结

  • 本篇主要介绍了神经网络误差反向传播中各个层的反向传播计算方式
  • 通过本篇可以很容易的了解反向传播的计算过程
  • 下篇我们将介绍神经网络的误差反向传播法的实现,并且用误差反向传播法来学习之前基于数值微分的神经网络学习案例,看看学习的效率以及正确率