python深度学习入门-误差反向传播法

深度学习入门-误差反向传播法

博主微信公众号(左)、Python+智能大数据+AI学习交流群(右):欢迎关注和加群,大家一起学习交流,共同进步!

python深度学习入门-误差反向传播法_第1张图片

目录

摘要

1. 计算图

1.1 用计算图求解

1.2 局部计算

1.3 为何用计算图解题

2. 链式法则

2.1 计算图的反向传播

2.2 什么是链式法则

2.3 链式法则的计算图

3. 反向传播

3.1 加法节点的反向传播

3.2 乘法节点的反向传播

3.3 当当商城购买书的例子

4. 简单层的实现

4.1 乘法层的实现

4.2 加法层的实现

5. 激活函数层的实现

5.1 ReLU 层

5.2 Sigmoid 层

6. Affine/Softmax 层的实现

6.1 Affine 层

6.2 批版本的 Affine 层

6.3 Softmax-with-Loss 层

7. 误差反向传播法的实现

7.1 神经网络学习的全貌图

7.2 对应误差反向传播法的神经网络的实现

7.3 误差反向传播法的梯度确认

7.4 使用误差反向传播法的学习

7.5 完整代码


摘要

  • 通过使用计算图,可以直观地把握计算过程。
  • 计算图的节点是由局部计算构成的。局部计算构成全局计算。
  • 计算图的正向传播进行一般的计算。通过计算图的反向传播,可以计算各个节点的导数。
  • 通过将神经网络的组成元素实现为层,可以高效地计算梯度(反向传播法)。
  • 通过比较数值微分和误差反向传播法的结果,可以确认误差反向传播法的实现是否正确(梯度确认)。

1. 计算图

    通过数值微分计算神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度),优点:简单、容易实现;缺点:计算上比较费时。

    计算图:将计算过程用数据结构图表示出来(通过多个节点和边表示,连接节点的直线称为 “边”)。

    计算图通过节点和箭头表示计算过程。节点用 \bigcirc 表示, \bigcirc 中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。

1.1 用计算图求解

    问题1:小明在当当商城买了 2 本书(《python深度学习入门》《python深度学习进阶》),每本书的价格都是 100 元,消费税是 10%,请计算支付金额。

图 4-1    基于计算图求解的问题1的答案

        题目计算图解析:

            (1) 书的价格 100 元流到 "x2" 节点,变成 200 元,然后被传递给下一个节点;

            (2) 这个 200 元流向 "x(1-10%)" 节点,变成 220 元。

python深度学习入门-误差反向传播法_第2张图片 图 4-2    基于计算图求解的问题1的答案:“书本数量”和“消费税”作为变量标在外面

        题目计算图解析:

            (1) 只用 \bigcirc 表示乘法运算 "x";

            (2) 将 "2" 和 "(1-10%)" 分别作为变量 "数的数量"、"消费税" 标在 \bigcirc 外面。

    问题2:小明在当当商城买了 2 本书(《python深度学习入门》《python深度学习进阶》)、3 支钢笔,每本书的价格是 100 元,每只钢笔的价格是 10 元。消费税是 10%,请计算支付金额。

图 4-3    基于计算图求解的问题2的答案

    计算图解题流程:

        1. 构建计算图。

        2. 在计算图上,从左向右进行计算。

    正向传播(forward propagation):从计算图出发点到结束点的传播。

    反向传播(backward propagation):从计算图结束点点到出发点的传播。

1.2 局部计算

    局部计算:无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。

    问题:小明在当当商城买了 2 本书(《python深度学习入门》《python深度学习进阶》)和其他很多东西,每本书的价格是 100 元,其他很多东西的总花费为 4000 元。消费税是 10%,请计算支付金额。

图 4-4    局部计算

    题目解析:

        这里各个节点处的计算都是局部计算。例如 书 和 其他很多东西 的求和运算 (4000+200\rightarrow4200) 并不关心 4000 这个数字是如何计算来的,只要把两个数字相加就可以了。

        各个节点处只需进行与自己相关的计算,不用考虑全局。

    总结:

        1. 计算图可以集中精力于局部计算。无论全局的计算有多么复杂,各个步骤所要做的就是对象节点的局部计算。

        2. 虽然局部计算非常简单,但是通过传递它的计算结果,可以获得全局的复杂计算的结果。

1.3 为何用计算图解题

    计算图的优点:

        1. 局部计算。无论全局是多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。

        2. 利用计算图可以将中间的计算结果全部保存起来(比如,计算进行到 2 本书时的金额是 200 元、加上消费税之前的金额 230 元等)。

        3. 可以通过正向传播和反向传播高效计算各个变量的导数值。

    题目:假设书的价格为 x,支付金额为 L,求 \frac{\partial L}{\partial x}(当书的价格稍微变化时,支付金额会变化多少)。

    通过计算图的反向传播求导数:

python深度学习入门-误差反向传播法_第3张图片 图 4-5    基于反向传播的导数的传递

    在这个例子中,反向传播从右向左(图中加粗的向左箭头)传递导数的值(1\rightarrow1.1\rightarrow2.2。

    从这个结果中可知,“支付金额关于书的价格的导数” 的值是 2.2 元(严格地讲,如果输的价格增加或减少某个微小值,则最终的支付金额将增加或减少那个微小值的 2.2 倍)。

    重点:计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。

2. 链式法则

    反向传播将局部导数向正方向的反方向(从右到左)传递。这个局部导数的原理,是基于链式法则(chain rule)的。

2.1 计算图的反向传播

    y=f(x) 计算的反向传播如图 4-6 所示。

python深度学习入门-误差反向传播法_第4张图片 图 4-6    计算图的反向传播:沿着与正方向相反的方向,乘上局部导数

    如图所示,反向传播的计算顺序是,将信号 E 乘以节点的局部导数 (\frac{\partial y}{\partial x}),然后将结果传递给下一个节点。

    这里所说的局部导数是指正向传播中 y=f(x) 的导数,也就是 y 关于 x 的导数  (\frac{\partial y}{\partial x})。

    比如,假设 y=f(x)=x^{2},则局部导数为 \frac{\partial y}{\partial x}=2x。把这个局部导数乘以上游传过来的值(本例中为E),然后再传递给前面的节点。

2.2 什么是链式法则

    复合函数:由多个函数构成的函数。比如,z=(x+y)^{2} 是由式 (4.1) 所示的两个式子构成的。

        z=t^{2}, \ t=x+y \ \ \ \ \ \ \ \(4.1)

    链式法则:链式法则是关于复合函数的导数的性质,定义如下。

        如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

    以式 (4.1) 为例,\frac{\partial z}{\partial x}z 关于 x 的导数)可以用 \frac{\partial z}{\partial t}z 关于 t 的导数)和 \frac{\partial t}{\partial x}t 关于 x 的导数)的乘积表示。用数学公式表示的话,可以写成式 (5.2)。

        \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t} \frac{\partial t}{\partial x} \ \ \ \ \ \ \ \ (4.2)

    使用链式法则,求式 (4.1) 的导数 \frac{\partial z}{\partial x}

        \frac{\partial z}{\partial t} = 2t, \ \frac{\partial t}{\partial x} =1 \ \ \ \ \ \ \ \ (4.3)

        \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t} \frac{\partial t}{\partial x}=2t \ * \ 1 = 2(x+y) \ \ \ \ \ \ \ \ (4.4)

2.3 链式法则的计算图

    用 "**2" 节点表示平方运算, 将式 (4.4) 的链式法则的计算过程用计算图表示出来,计算图如图 4-7 所示。

图 4-7    式(4.4)的链式法则的计算图:沿着与正方向相反的方向,乘上局部导数后传递

    如图所示,计算图的反向传播从右到左传播信号。

    反向传播的计算顺序:

        1. 先将节点的输入信号乘以节点的局部导数(偏导数);

        2. 传递给下一个节点。

        比如,反向传播时,"**2" 节点的输入是 \frac{\partial z}{\partial z}=1,将其乘以局部导数 \frac{\partial z}{\partial t}(因为正向传播时输入是 t,输出是 z,所以这个节点的局部导数是 \frac{\partial z}{\partial t}),然后传递给下一个节点。

    观察图 4-7 最左边反向传播的结果。根据链式法则,\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} 成立,对应 “z 关于 x 的导数”。也就是说,反向传播时基于链式法则的

    把 式(4.3) 的结果带入到 图 4-7 中,结果如图 5-8 所示,\frac{\partial z}{\partial x} 的结果为 2(x+y)

python深度学习入门-误差反向传播法_第5张图片 图 4-8    z=(x+y)**2 的计算图的反向传播

3. 反向传播

3.1 加法节点的反向传播

    题目: 以 z=x+y 为对象,观察它的反向传播。

    z=x+y 的导数可由下式(解析性地)计算出来。

        \frac{\partial z}{\partial x}=1, \ \frac{\partial z}{\partial y}=1 \ \ \ \ \ \ \ \ (4.5)

    用计算图表示加法节点的反向传播:

python深度学习入门-误差反向传播法_第6张图片 图 4-9    加法节点的反向传播

 

图 4-10    加法节点存在于某个最后输出的计算的一部分中

    

图 4-11    加法节点反向传播具体例子

    1. 加法节点的反向传播将上游的值原封不动地输出到下游。

    2. 加法节点存在于某个最后输出的计算的一部分中。反向传播时,从最右边的输出出发,局部导数从节点向节点反方向传播。

3.2 乘法节点的反向传播

    题目: 以 z=xy 为对象,观察它的反向传播。

    z=x+y 的导数可由下式(解析性地)计算出来。

        \frac{\partial z}{\partial x}=y, \ \frac{\partial z}{\partial y}=x \ \ \ \ \ \ \ \ (4.6)

    用计算图表示加法节点的反向传播:

python深度学习入门-误差反向传播法_第7张图片 图 4-12    乘法节点的反向传播

    

python深度学习入门-误差反向传播法_第8张图片 图 4-13    乘法节点反向传播的具体例子

    1. 乘法节点的反向传播将上游的值乘以正向传播时的输入信号的 “翻转值” 后传递给下游。

    2. 翻转值表示一种翻转关系,如图 4-12 所示,正向传播时信号是 x 的话,反向传播时则是 y;正向传播时信号是 y 的话,反向传播时则是 x

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

3.3 当当商城购买书的例子

    问题:求解 书的价格、书的数量、消费税 这 3 个变量各自如何影响最终的支付金额。("支付金额关于书的价格的导数"、"支付金额关于书的数量的导数"、"支付金额关于消费税的导数")

python深度学习入门-误差反向传播法_第9张图片 图 4-14    当当商城购买书的反向传播的例子

4. 简单层的实现

    用 Python 代码实现前面当当商城购买书的例子。

4.1 乘法层的实现

python深度学习入门-误差反向传播法_第10张图片 图 4-15    当当商城购买 2 本书
"""
乘法层的实现
"""


class MulLayer(object):
    def __init__(self):
        """初始化实例变量,用于保存正向传播时的输入值"""
        self.x = None
        self.y = None

    def forward(self, x, y):
        """
        正向传播
        :param x:
        :param y:
        :return:
        """
        self.x = x
        self.y = y
        out = x * y

        return out

    def backward(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 上游传来的导数与正向传播的翻转值的乘积
        """
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy


# 书的单价
book = 100
# 书的数量
book_num = 2
# 消费税
tax = 1.1

# 书的单价对应的乘法层
mul_book_layer = MulLayer()
# 消费税对应的乘法层
mul_tax_layer = MulLayer()

# forward
book_price = mul_book_layer.forward(book, book_num)
price = mul_tax_layer.forward(book_price, tax)
print(price)    # 220.00000000000003

# backword
dprice = 1
dbook_price, dtax = mul_tax_layer.backward(dprice)
dbook, dboook_num = mul_book_layer.backward(dbook_price)
print(dbook, dboook_num, dtax)  # 2.2 110.00000000000001 200

4.2 加法层的实现

python深度学习入门-误差反向传播法_第11张图片 图 4-16    当当商城购买 2 本书和 3 支钢笔

    

"""
乘法层和加法层的实现
"""


class MulLayer(object):
    def __init__(self):
        """初始化实例变量,用于保存正向传播时的输入值"""
        self.x = None
        self.y = None

    def forward(self, x, y):
        """
        正向传播
        :param x:
        :param y:
        :return:
        """
        self.x = x
        self.y = y
        out = x * y

        return out

    def backward(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 上游传来的导数与正向传播的翻转值的乘积
        """
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy


class AddLayer(object):
    def __init__(self):
        pass

    def forward(self, x, y):
        """
        正向传播
        :param x:
        :param y:
        :return:
        """
        return x + y

    def backward(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 上游传来的导数
        """
        dx = dout * 1
        dy = dout * 1

        return dx, dy


book = 100    # 书的单价
book_num = 2    # 书的数量
pen = 10    # 钢笔价格
pen_num = 3    # 钢笔数量
tax = 1.1   # 消费税

# layer
mul_book_layer = MulLayer()    # 书的单价对应的乘法层
mul_pen_layer = MulLayer()    # 钢笔的单价对应的乘法层
add_book_pen_layer = AddLayer()    # 书和钢笔金额相加和对应的加法层
mul_tax_layer = MulLayer()    # 消费税对应的乘法层

# forward
book_price = mul_book_layer.forward(book, book_num)
pen_price = mul_pen_layer.forward(pen, pen_num)
all_price = add_book_pen_layer.forward(book_price, pen_price)
price = mul_tax_layer.forward(all_price, tax)
print(price)    # 253.00000000000003

# backword
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)
dbook_price, dpen_price = add_book_pen_layer.backward(dall_price)
dbook, dboook_num = mul_book_layer.backward(dbook_price)
dpen, dpen_num = mul_pen_layer.backward(dpen_price)
# 2.2 110.00000000000001 3.3000000000000003 11.0 230
print(dbook, dboook_num, dpen, dpen_num, dtax)  

5. 激活函数层的实现

5.1 ReLU 层

    激活函数 ReLU(Rectified Linear Unit)由式 (4.7) 表示。

        y=\left\{\begin{matrix} 0 & (x> 0)\\ 0 & (x\leq 0) \end{matrix}\right. \ \ \ \ \ \ \ \ (4.7)

    通过式 (4.7),可以求出 y 关于 x 的导数,如式 (4.8) 所示。

        \frac{\partial y}{\partial x}=\left\{\begin{matrix} 1 & (x> 0)\\ 0 & (x\leq 0) \end{matrix}\right. \ \ \ \ \ \ \ \ (4.8)

    1. 正向传播时的输入 x 大于 0,则反向传播会将上游的值原封不动传给下游。

    2. 正向传播时的输入 x 小于 0,则反向传播中传给下游的信号将停在此处。

    ReLU 层的计算图:

python深度学习入门-误差反向传播法_第12张图片 图 4-17    ReLU 层的计算图

    Python 代码实现:

"""ReLU层"""


class Relu(object):
    def __init__(self):
        # 由True/False构成的NumPy数组
        # 正向传播时的输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False
        self.mask = None

    def forward(self, x):
        """
        正向传播
        :param x: 正向传播时的输入
        :return:
        """
        # 输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False
        self.mask = (x <= 0)
        # 输入x的元素中小于等于0的值变换为0
        out = x.copy()
        out[self.mask] = 0

        return out

    def backword(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return:
        """
        # 将从上游传来的dout的mask中的元素为True的地方设为0
        dout[self.mask] = 0
        dx = dout

        return dx

5.2 Sigmoid 层

    Sigmoid 函数由式 (4.9) 表示。

        y=\frac{1}{1+exp(-x)} \ \ \ \ \ \ \ \ (4.9)

    用计算图表示式 (4.9),如图 4-18 所示。

python深度学习入门-误差反向传播法_第13张图片 图 4-18    Sigmoid 层的计算图(仅正向传播)

    Sigmoid 层的反向传播:

    步骤 1

    “/” 节点表示 y=\frac{1}{x},它的导数可以解析性地表示为下式。

        \frac{\partial y}{\partial x}=-1 * x^{-2}=-\frac{1}{x^{2}}=-y^{2} \ \ \ \ \ \ \ \ (4.10)

    根据式 (4.10),反向传播时,会将上游的值乘以 -y^{2}(正向传播的输出的平方乘以 -1 后的值)后,再传给下游。计算图如下所示。

    

    步骤 2

    “+” 节点将上游的值原封不动地传给下游。计算图如下图所示。

    

    步骤 3

    “exp” 节点表示 y=exp(x),它的导数由下式表示。

        \frac{\partial y}{\partial x}=exp(x) \ \ \ \ \ \ \ \ (4.11)

    计算图中,上游的值乘以正向传播时的输出(这个例子中是 exp(-x))后,再传给下游。

    

    步骤 4

    “x” 节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以 -1。

图 4-19    Sigmoid 层的计算

    集约化的 “Sigmoid” 节点:

图 4-20    Sigmoid 层的计算图(简洁版)

        1. 简洁版的计算图可以省略反向传播中的计算过程,计算效率更高。

        2. 通过对节点进行集约化,可以不用在意 Sigmoid 层中琐碎的细节,而只需要专注于它的输入和输出。

    根据正向传播的输出 y 计算反向传播:

        由于 y=\frac{1}{1+exp(-x)},则 \frac{\partial L}{\partial y}y^{2}exp(-x) 可以进一步调整如下。

        \frac{\partial L}{\partial y}y^{2}exp(-x) \\=\frac{\partial L}{\partial y}\frac{1}{(1+exp(-x))^{2}}exp(-x) \\ =\frac{\partial L}{\partial y}\frac{1}{1+exp(-x)}\frac{exp(-x)}{1+exp(-x)} \\ =\frac{\partial L}{\partial y}y(1-y) \ \ \ \ \ \ \ \ (4.12)

python深度学习入门-误差反向传播法_第14张图片 图 4-21    Sigmoid 层的计算图:根据正向传播的输出 y 计算反向传播

    Python 实现 Sigmoid 层:

"""Sigmoid层"""
import numpy as np


class Sigmoid(object):
    def __init__(self):
        self.out = None    # 正向传播的输出

    def forword(self, x):
        """
        正向传播
        :param x:
        :return:
        """
        out = 1 / (1 + np.exp(-x))
        self.out = out
        
        return out
    
    def backword(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 
        """
        dx = dout * (1.0 - self.out) * self.out
        
        return dx

6. Affine/Softmax 层的实现

6.1 Affine 层

>>> import numpy as np
>>> X = np.random.rand(2)    # 输入
>>> W = np.random.rand(2, 3)    # 权重
>>> B = np.random.rand(3)    # 偏置
>>>
>>> X.shape
(2,)
>>> W.shape
(2, 3)
>>> B.shape
(3,)
>>>
>>> Y = np.dot(X, W) + B
>>> Y
array([1.22628425, 1.34681801, 0.62003537])
>>> Y.shape
(3,)

 

python深度学习入门-误差反向传播法_第15张图片 图 4-22    矩阵的乘积运算中对应维度的元素个数要保持一致

    神经网络的正向传播中进行的矩阵的乘积运算在几何学领域称为 “仿射变换”。因此,这里将进行仿射变换的处理实现为 “Affine” 层。

    Affine 层的计算图:

        乘积运算用 “dot” 节点表示,np.dot(X, W) + B 的运算的计算图如图 4-23 所示。

        输入 X 的形状为 (2,),权重 W 的形状为(2, 3),X * W 的形状为 (3,),偏置 B 的形状为 (3,),输出 Y 的形状为 (3,)。

图 4-23    Affine 层的计算图(注意变量是矩阵,各个变量上方标记了该变量的形状)

    Affine 层的反向传播:

        \frac{\partial L}{\partial X}=\frac{\partial L}{\partial Y} * W^{T}, \ \frac{\partial L}{\partial W}=X^{T} * \frac{\partial L}{\partial Y} \ \ \ \ \ \ \ \ (4.13)

    式 (4.13) 中 W^{T} 的 T 表示转置。转置操作会把 W 的元素 (i,j) 换成元素 (j,i)。用数学式表示的话,可以写成下面这样。

        W=\begin{pmatrix} w_{11} & w_{12} & w_{13}\\ w_{21} & w_{22} & w_{23} \end{pmatrix} 

        W^{T}=\begin{pmatrix} w_{11} & w_{21}\\ w_{12} & w_{22}\\ w_{13} & w_{23} \end{pmatrix} \ \ \ \ \ \ \ \ (4.14)

    如式 (4.14) 所示,如果 W 的形状是 (2, 3),则 W^{T} 的形状就是 (3,2)。

python深度学习入门-误差反向传播法_第16张图片 图 4-24    Affine 层的反向传播

    矩阵乘积(“dot” 节点)的反向传播推导:    

    观察图 4-24 的计算图中各个变量的形状,可以发现 X 和 \frac{\partial L}{\partial X} 形状相同,W 和 \frac{\partial L}{\partial W} 形状相同。

    从下面的数学式可以很明确第看出 X 和 \frac{\partial L}{\partial X} 形状相同。

        X=(x_{0}, x_{1},x_{2},...,x_{n})

        \frac{\partial L}{\partial X}=(\frac{\partial L}{\partial x_{0}}, \frac{\partial L}{\partial x_{1}},\frac{\partial L}{\partial x_{2}},...,\frac{\partial L}{\partial x_{n}}) \ \ \ \ \ \ \ \ (4.15)

    为什么要注意矩阵的形状呢?

        因为矩阵的乘积运算要求对应维度的元素个数保持一致,通过确认一致性,就可以推导出式 (4.13)。比如,\frac{\partial L}{\partial Y} 的形状是 (3,),W 的形状是 (2, 3) 时,思考 \frac{\partial L}{\partial Y} 和 W^{T} 的乘积,使得 \frac{\partial L}{\partial X} 的形状为 (2,)(图 4-25)。这样一来,就会自然地推导出式 (4.13)。

python深度学习入门-误差反向传播法_第17张图片 图 4-25   矩阵乘积(“dot” 节点)的反向传播推导

    矩阵的乘积(“dot”节点)的反向传播可以通过组建使矩阵对应维度的元素个数一致的乘积运算而推导出来。

6.2 批版本的 Affine 层

    批版本的 Affine 层的计算图:

python深度学习入门-误差反向传播法_第18张图片 图 4-26    批版本的 Affine 层的计算图

    Python 实现 Affine层:

"""Affine层"""

import numpy as np


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):
        """
        正向传播
        :param x: 
        :return: 
        """
        # 对应张量
        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):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 输入的导数
        """
        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

6.3 Softmax-with-Loss 层

    Softmax 层将输入值正规化(将输出值的和调整为 1)之后再输出。

    Softmax-with-Loss 层(Softmax 函数和交叉熵误差)的计算图:

python深度学习入门-误差反向传播法_第19张图片 图 4-27    Softmax-with-Loss 层(Softmax 函数和交叉熵误差)的计算图

    1. 图 4-27 的计算图中,softmax 函数标记为 Softmax 层,交叉熵误差记为 Cross Entropy Error 层。这里假设要进行 3 类分类,从前面的层接收 3 个输入(得分)。

    2. 如图 4-27 所示,Softmax 层将输入 (a_{1},a_{2},a_{3}) 正规化,输出 (y_{1},y_{2},y_{3})。Cross Entropy Error 层接收 Softmax 的输出 (y_{1},y_{2},y_{3}) 和 监督标签 (t_{1},t_{2},t_{3}),从这些数据中输出损失 L

    3. 图 4-27 中 Softmax 层的反向传播得到了 (y_{1}-t_{1},y_{2}-t-{2},y_{3}-t_{3}) 这样 “漂亮” 的结果(Softmax 层的输出和监督标签的差分)。神经网络的反向传播会把这个差分表示的误差传递给前面的层。

        例 1:监督标签是 (0, 1, 0),Softmax 层的输出是 (0.3, 0.2, 0.5)。因为正确解标签处的概率是 0.2(20%),这个时候的神经网络未能正确识别。此时,Softmax 层的反向传播传递的是 (0.3, -0.8, 0.5) 这样一个大的误差。因为这个大的误差会向前面的层传播,所以 Softmax 层前面的层会从这个大的误差中学习到 “大” 的内容。

        例 2:监督标签是 (0, 1, 0),Softmax 层的输出是 (0.01, 0.99, 0)。这个神经网络识别的相当准确。此时,Softmax 层的反向传播传递的是 (0.01, -0.01, 0) 这样一个小的误差。这个小的误差会向前面的层传播,所以 Softmax 层前面的层学习到的内容也很 “小”。

    Python 实现 Softmax-with-Loss 层:

"""Softmax-with-Loss层"""

import numpy as np 


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(object):

    def __init__(self):
        self.loss = None    # 损失
        self.y = None    # softmax 的输出
        self.t = None    # 监督数据(one-hot vector)

    def forward(self, x, t):
        """
        正向传播
        :param x: 输入
        :param t: 监督数据
        :return:
        """
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backword(self, dout=1):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 
        """
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size    # 单个数据的误差

        return dx

7. 误差反向传播法的实现

7.1 神经网络学习的全貌图

    前提

    神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分为下面 4 个步骤。

    步骤 1 (mini-batch)

    从训练数据中随机选择一部分数据。

    步骤 2(计算梯度)

    计算损失函数关于各个权重参数的梯度。

    步骤 3 (更新参数)

    将权重参数沿梯度方向进行微小的更新。

    步骤 4(重复)

   重复步骤 1、步骤 2、步骤 3。

7.2 对应误差反向传播法的神经网络的实现

    代码详见章节 7.5。

7.3 误差反向传播法的梯度确认

    梯度确认(gradient check):确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致的操作称为梯度确认(gradient check)

    使用 MNIST 数据集,使用训练数据的一部分,确认数值微分求出的梯度和误差反向传播法求出的梯度的误差

    计算方法:求各个权重参数中对应元素的差的绝对值,并计算其平均值。

W1: 4.3067913219006997e-10
b1: 2.593520548931712e-09
W2: 6.288955973284032e-09
b2: 1.4004541540463267e-07

    从这个结果可以看出,通过数值微分和误差反向传播法求出的提督查非常小。比如,第 1 层的偏置的误差是 2.59e-09(0.00000000259)。这样一来,我们就知道了通过误差反向传播法求出的梯度是正确的,误差反向传播法的实现没有错误。

    数值微分和误差反向传播法的计算结果之间的误差为 0 是很少见的。这是因为计算机的精度有限(比如,32位浮点数)。受到数值精度的限制,误差一般不会为 0,但是如果实现正确的话,可以期待这个误差是一个接近 0 的很小的值。如果这个值很大,就说明误差反向传播法的实现存在错误。

7.4 使用误差反向传播法的学习

    代码详见章节 7.5。

python深度学习入门-误差反向传播法_第20张图片 图 4-28    误差反向传播法学习过程中的 loss 变化图像

 

python深度学习入门-误差反向传播法_第21张图片 图 4-29    误差反向传播法学习过程中的识别精度变化图像

7.5 完整代码

"""对应误差反向传播法的神经网络的实现"""

import numpy as np
from collections import OrderedDict
from matplotlib import pyplot as plt

from dataset.mnist import load_mnist


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


def numerical_gradient(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)  # f(x+h)

        x[idx] = tmp_val - h
        fxh2 = f(x)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2 * h)

        x[idx] = tmp_val  # 还原值
        it.iternext()

    return grad


class SoftmaxWithLoss(object):

    def __init__(self):
        self.loss = None    # 损失
        self.y = None    # softmax 的输出
        self.t = None    # 监督数据(one-hot vector)

    def forward(self, x, t):
        """
        正向传播
        :param x: 输入
        :param t: 监督数据
        :return:
        """
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backword(self, dout=1):
        """
        反向传播
        :param dout: 上游传来的导数
        :return:
        """
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size    # 单个数据的误差

        return dx


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):
        """
        正向传播
        :param x:
        :return:
        """
        # 对应张量
        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):
        """
        反向传播
        :param dout: 上游传来的导数
        :return: 输入的导数
        """
        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


class Relu(object):
    def __init__(self):
        # 由True/False构成的NumPy数组
        # 正向传播时的输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False
        self.mask = None

    def forward(self, x):
        """
        正向传播
        :param x: 正向传播时的输入
        :return:
        """
        # 输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False
        self.mask = (x <= 0)
        # 输入x的元素中小于等于0的值变换为0
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        """
        反向传播
        :param dout: 上游传来的导数
        :return:
        """
        # 将从上游传来的dout的mask中的元素为True的地方设为0
        dout[self.mask] = 0
        dx = dout

        return dx


class TwoLayerNet(object):
    """对应误差反向传播法的神经网络的实现"""
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        """
        初始化
        :param input_size: 输入层的神经元数
        :param hidden_size: 隐藏层的神经元数
        :param output_size: 输出层的神经元数
        :param weight_init_std: 初始化权重时的高斯分布的规模
        """
        # 初始化权重
        self.params = {}
        # 第 1 层的权重
        self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size)
        # 第 1 层的偏置
        self.params["b1"] = np.zeros(hidden_size)
        # 第 2 层的权重
        self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
        # 第 2 层的偏置
        self.params["b2"] = np.zeros(output_size)

        # 生成层
        # 以layers["Affine1"]、layers["Relu1"]、layers["Affine2"]的形式,通过有序字典保存各个层
        self.layers = OrderedDict()
        self.layers["Affine1"] = Affine(self.params["W1"], self.params["b1"])
        self.layers["Relu1"] = Relu()
        self.layers["Affine2"] = Affine(self.params["W2"], self.params["b2"])

        # 神经网络的最后一层,Softmax-with-Loss层
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        """
        识别(推理)
        :param x: 输入数据
        :return:
        """
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    def loss(self, x, t):
        """
        计算损失函数的值
        :param x: 输入数据
        :param t: 监督数据
        :return:
        """
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def accuracy(self, x, t):
        """
        计算识别精度
        :param x: 输入数据
        :param t: 监督数据
        :return:
        """
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1:
            t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])

        return accuracy

    def numerical_gradient(self, x, t):
        """
        通过数值微分计算关于权重参数的梯度
        :param x: 输入数据
        :param t: 监督数据
        :return:
        """
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads["W1"] = numerical_gradient(loss_W, self.params["W1"])
        grads["b1"] = numerical_gradient(loss_W, self.params["b1"])
        grads["W2"] = numerical_gradient(loss_W, self.params["W2"])
        grads["b2"] = numerical_gradient(loss_W, self.params["b2"])

        return grads

    def gradient(self, x, t):
        """
        通过误差反向传播法计算关于权重参数的梯度
        :param x: 输入数据
        :param t: 监督数据
        :return:
        """
        # forward
        self.loss(x, t)

        # backword
        dout = 1
        dout = self.lastLayer.backword(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 设定
        grads = {}
        grads["W1"], grads["b1"] = self.layers["Affine1"].dW, self.layers["Affine1"].db
        grads["W2"], grads["b2"] = self.layers["Affine2"].dW, self.layers["Affine2"].db

        return grads


"""梯度确认"""
# 读入数据
(x_train, y_train), (x_test, y_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

x_batch = x_train[:3]
y_batch = y_train[:3]

grad_numerical = network.numerical_gradient(x_batch, y_batch)
grad_backprop = network.gradient(x_batch, y_batch)

# 求各个权重的绝对误差的平均值
for key in grad_numerical.keys():
    diff = np.average(np.abs(grad_backprop[key] - grad_numerical[key]))
    print(f"{key}: {diff}")

"""使用误差反向传播法的学习"""
# 超参数
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 平均每个epoch的重复次数
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    # 获取mini-batch,每次从60000个训练数据中随机取出100个数据
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    y_batch = y_train[batch_mask]

    # 计算梯度
    # grad = network.numerical_gradient(x_batch, y_batch)
    grad = network.gradient(x_batch, y_batch)

    # 更新权重参数和偏置参数
    for key in ("W1", "b1", "W2", "b2"):
        network.params[key] -= learning_rate * grad[key]

    #  # 计算每个epoch的loss
    loss = network.loss(x_batch, y_batch)
    train_loss_list.append(loss)

    # 计算每个epoch的识别精度
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, y_train)
        test_acc = network.accuracy(x_test, y_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)

# 绘制loss图形
x = np.arange(len(train_loss_list))
plt.plot(x, train_loss_list, label="loss")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.ylim(0, 1.0)
plt.legend(loc="upper right")
plt.show()

# 绘制acc图形
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label="train acc")
plt.plot(x, test_acc_list, label="test acc", linestyle="--")
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc="lower right")
plt.show()

 

你可能感兴趣的:(人工智能(深度学习入门))