深度学习——从入门到入坟,一篇文章理论结合实践带你掌握深度学习基本知识

一 Python入门

1.1 NumPy

在深度学习的实现中,经常出现数组和矩阵的计算。NumPy的数组类中提供了很多便捷的方法。实践内容如下:

  • 导入NumPy库
  • 生成NumPy数组
  • NumPy的算术运算
  • 广播
  • 访问元素
import numpy as np

x = np.array([1, 2, 3])
y = np.array([[0, 1, 0],
              [1, 2, 1],
              [0, 2, 1]])
print(x)
print(y)
z = x * y
print(z)
i = 10
q = y * i
print(q)
print(x[0])
[1 2 3]
[[0 1 0]
 [1 2 1]
 [0 2 1]]
[[0 2 0]
 [1 4 3]
 [0 4 3]]
[[ 0 10  0]
 [10 20 10]
 [ 0 20 10]]
1

1.2 Matplotlib

在深度学习的实现中,图形的绘制和可视化非常重要。Matplotlib是用于绘制图形的库,使用Matplotlib.pyplot可以轻松绘制图形并实现数据的可视化。实践内容如下:

  • 绘制sin&cos函数图像
  • pyplot的其它功能
  • 显示图像
import matplotlib.pyplot as plt
import numpy as np

# 生成数据
x = np.arange(0, 6, 0.1)  # 以0.1为单位,生成0到6的数据
y1 = np.sin(x)
y2 = np.cos(x)

# 绘制图形
plt.plot(x, y1, label="sin")
plt.plot(x, y2, linestyle="--", label="cos")
plt.xlabel("x")
plt.ylabel("y")
plt.title("sin & cos")
plt.legend()
plt.show()

图1 sin&cos

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.image import imread

img = imread('dataset\\lena.png')
plt.imshow(img)
plt.show()

图2 lena

二 感知机

2.1 感知机简介

感知机接收多个信号,输出一个信号,输入信号被送往感知机时,会被分别乘以固定的权重,感知机计算传送过来的信号总和,当总和超过一个界限值(阈值)时,感知机输出信号1(传递信号)。
y={0if w1x1+w2x2<θ1if w1x1+w2x2≥θy = \begin{cases} 0 & \text{if } w_{1}x_{1}+w_{2}x_{2} < \theta \\ 1 & \text{if } w_{1}x_{1}+w_{2}x_{2} \geq \theta \end{cases}y={01if w1x1+w2x2<θif w1x1+w2x2θ

其中yyy代表输出信号,x1,x2x_{1},x_{2}x1,x2分别代表两个输入信号,w1,w2w_{1},w_{2}w1,w2为输入信号的权重,θ\thetaθ为阈值。

2.2 简单逻辑电路

通过编写函数实现简单逻辑电路中的与门、非门、与非门、或门。

import numpy as np


# 与门
def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1
# 非门
def NOT(x1):
    b = 0.7
    tmp = -x1+b
    if tmp <= 0:
        return 0
    else:
        return 1
# 与非门
def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5])
    b = 0.7
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1
# 或门
def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1


对于感知机而言,异或门无法直接表示,但是感知机可以通过“叠加层”来表示异或门,通过叠加的方法,利用已有的与门、或门和非门,可以巧妙地实现异或门。
# 异或门
def XOR(x1, x2):
    x_n1 = NOT(x1)
    x_n2 = NOT(x2)
    x3 = AND(x2, x_n1)
    x4 = AND(x1, x_n2)
    tmp = OR(x3, x4)
    return tmp

print(XOR(1, 1))

Plus:异或门的实现可能会有不同方法,在此列举的为本人在Turing Complete上实现的方法。

三 神经网络

3.1 阶跃函数

在第二节感知机中我们了解到:当输入信号的加权和达到阈值时,输出信号将被激活。这时,将阈值移项不等式左边,令b=−θb=-\thetab=θ,记bbb为偏置,令a=b+w1x1+w2x2a=b+w_{1}x_{1}+w_{2}x_{2}a=b+w1x1+w2x2,则有阶跃函数:
y={0if a<01if a≥0y = \begin{cases} 0 & \text{if } a < 0 \\ 1 & \text{if } a \geq 0 \end{cases}y={01if a<0if a0

import numpy as np
import matplotlib.pyplot as plt

def step_function(a):
    return np.array(a > 0, dtype=int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)  # 指定y轴的范围
plt.show()

图3 阶跃函数

3.2 sigmoid激活函数

y=h(a)y=h(a)y=h(a),称h()h()h()为激活函数,aaa为自变量参数。对于神经网络中常用的激活函数sigmoidsigmoidsigmoid而言,其激活函数表达式为:
h(x)=11+exp(−x)h(x)=\frac{1}{1+exp(-x)}h(x)=1+exp(x)1

def sigmoid(a):
    return 1 / (1 + np.exp(-a))


x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)  # 指定y轴的范围
plt.show()

图4 sigmoid函数

3.3 softmax函数

分类问题中使用的softmax函数可以用下式表示:
y=exp(ak)∑i=1nexp(ai)y=\frac{exp(a_{k})}{\sum\limits_{i=1}^{n}exp(a_{i})}y=i=1nexp(ai)exp(ak)
从表达式中可以发现,输出层的各个神经元受到所有输入信号的影响。

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

上述softmax函数虽然可以正确描述softmax表达式,但在计算机运行过程中存在一定缺陷,即数值溢出问题,指数函数的运算呈现爆炸式增长,而 计算机处理“数”时,数值必须在4字节或8字节的有限数据宽度内。因此,可以考虑对softmax函数进行如下改进。

def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c) # 溢出对策
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

通过减去信号中的最大值,可以将exp_a控制在0到1之间,有效避免了计算机数值溢出问题。

3.4 三层神经网络实现

如下图所示,这是一个三层的神经网络,其中第一层网络包含三个输出信号作为第二层的输入信号,在信号处理过程中,选取的激活函数为sigmoidsigmoidsigmoid函数。我们需要定义各层的权重和偏置,输出最终的输出信号[y1,y2][y_{1},y_{2}][y1,y2]
图5 三层神经网络

# 初始化网络
def init_network():
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5],
                              [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4],
                              [0.2, 0.5],
                              [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3],
                              [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network


def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = a3
    return y


network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)
[0.31682708 0.69627909]
这里定义了init_network()和forward()函数。init_network()函数会进行权重和偏置的初始化,并将它们保存在字典变量network中。这个字典变量network中保存了每一层所需的参数(权重和偏置)。forward()函数中则封装了将输入信号转换为输出信号的处理过程。   

至此,神经网络的前向处理的实现就完成了。通过巧妙地使用NumPy 多维数组,我们高效地实现了神经网络。

3.5 实验任务一

:::tips
要求:(ch3)查阅相关资料,熟悉python的标准模块pickle、 gzip的使用,首先完成读取mnist数据集.npz文件的部分数据并输出,最后完成显示MNIST图像实验,并给出程序重、难点注释;利用MNIST的图像数据,以及sample_weight.pkl中训练好的权重参数, 完成如下图网络的向前计算程序的运行,并给出程序重、难点注释。
:::
实验步骤:

  1. 读取mnist数据集.npz部分数据内容。
  2. mnist图像展示。
  3. 读取sample_weight.pkl数据。
  4. 实现三层神经网络。
import numpy as np

filepath = 'dataset\\mnist数据集.npz'
data = np.load(filepath)

# 打印所有键
print("所有键:", data.files)
所有键: ['x_test', 'x_train', 'y_train', 'y_test']

根据读取的信息,识别出数据集中包含[‘x_test’, ‘x_train’, ‘y_train’, ‘y_test’]4组对象文件,利用该信息,可以对mnist图像进行部分展示。

import numpy as np
from PIL import Image


def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()


def load_mnist_plus():
    filepath = 'dataset\\mnist数据集.npz'
    with open(filepath, 'rb') as f:
        dataset = np.load(filepath)

    return (dataset['x_train'], dataset['y_train']), 
    (dataset['x_test'], dataset['y_test'])


(x_train, t_train), (x_test, t_test) = load_mnist_plus()

img = x_train[0]
label = t_train[0]
print(label)  # 5
print(img.shape)  # (28, 28)
img_show(img)

图6 mnist图像
最终会输出一个如上图的图像,mnist是手写数字图像数据合集,上述展示的是mnist数据集中的第一个手写数字5。

import numpy as np
import pickle
from mnist_show import load_mnist_plus
from functions import sigmoid, softmax


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist_plus()
    return x_test, t_test


def init_network():
    with open("dataset\\sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    w1, w2, w3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, w1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, w2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, w3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()

batch_size = 100  # 批数量
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i + batch_size]
    x_batch = x_batch.reshape(-1, 784)
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i + batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

Accuracy:0.9207

代码解释:

  1. get_data()函数:读取mnist数据集,由于不需要建立模型学习,故返回测试集数据即可。
  2. init_network()函数:初始化网络,读取权重与偏置。
  3. predict()函数:根据读取的权重与偏置,返回三层神经网络最终的输出信号。
  4. 调用:批量计算最终输出信号,同时需要注意转换x_batch的维度(从2828转换为1784)做矩阵运算,最终取y中最大的预测概率对应的类别作为预测类别,比较其和真实类别的差距,最终得到的预测准确度为0.9207。

四 神经网络学习

4.1 训练数据与测试数据

为了追求模型的泛化能力,机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。  
泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。比如,在识别手写数字的问题中,泛化能力可能会被用在自动读取明信片的邮政编码的系统上。此时,手写数字识别就必须具备较高的识别“某个人”写的字的能力。注意这里不是“特定的某个人写的特定的文字”,而是“任意一个人写的任意文字”。如果系统只能正确识别已有的训练数据,那有可能是只学习到了训练数据中的个人的习惯写法。 

4.2 损失函数

神经网络以某个指标为线索寻找最优权重参数,该指标称为损失函数。 这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

4.2.1 均方误差

均方误差公式如下:
E=12∑k(yk−tk)2E=\frac{1}{2}\sum_{k}(y_{k}-t_{k})^2E=21k(yktk)2
其中yky_{k}yk表示第k个维度上模型的输出值,tkt_{k}tk表示第k个维度上的训练数据,即实际值。

# 均方误差
def mean_squared_error(y, t):
    return 0.5 * np.sum((y - t) ** 2)
4.2.2 交叉熵误差

交叉熵误差公式如下:
E=−∑ktklog  ykE=-\sum_{k}t_{k}log\;y_{k}E=ktklogyk
这里,log表示以e为底数的自然对数。yky_{k}yk是神经网络的输出,tkt_{k}tk是正确解标签。其中,tkt_{k}tk中只有正确解标签的索引值为1,其它均为0。这种表示方法称为one-hot表示。

# 交叉熵误差
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

4.3 梯度

在高等数学中,我们学习到偏导数的概念,即函数对于特定自变量方向上的导数。由全部自变量的偏导数汇总而成的向量称为梯度。梯度实现方法如下:

# 梯度
def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x)
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h) 的计算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        # f(x-h) 的计算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = tmp_val  # 还原值
    return grad
其中参数f为函数,x为NumPy数组。由此,可以计算出函数的梯度向量。计算出了梯度向量,我们可以根据**梯度下降法**寻找函数最小值。现在,尝试用数学公式表达梯度法:

x0=x0−η∂f∂x0  x1=x1−η∂f∂x1x_{0} = x_{0}-\eta\frac{\partial f}{\partial x_{0}}\\\;\\ x_{1} = x_{1}-\eta\frac{\partial f}{\partial x_{1}}x0=x0ηx0fx1=x1ηx1f
公式中的η\etaη称为学习率。 学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x
参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。 

4.4 神经网络的梯度

神经网络学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。通过不断调整各维度上的权重参数以最小化损失函数,下面,以一个简单的神经网络为例,来实现求梯度的代码。首先,需要实现一个simpleNet的类。

import numpy as np
from functions import softmax, cross_entropy_error,numerical_gradient
class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 用高斯分布进行初始化
    def predict(self, x):
        return np.dot(x, self.W)
    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)
        return loss
simpleNet类只有 一个实例变量,即形状为2×3的权重参数。它有两个方法,一个是用于预测的predict(x),另一个是用于求损失函数值的loss(x,t)。参数x接收输入数据,t接收正确解标签。

由此,我们可以得到神经网络的梯度dW,定义损失函数f(W),再使用梯度计算的函数即可求得神经网络关于W的梯度。

net = simpleNet()
x = np.array([0.6,0.9])
t = [0,0,1]
def f(W):
    return net.loss(x,t)
dW = numerical_gradient(f, x)
print(dW)
[1.70071913 1.46941446]

4.5 实验任务二

要求:按照第四章内容,完成一个二层网络的建立(阅读two_layer_net.py),并完成其网络学习(阅读train_neuralnet.py),阅读上面二个程序,并给出程序重、难点注释,利用MNIST的图像数据给出实验结果。

首先,检查二层网络类,需要定义构造函数并初始化神经网络层的大小和权重;其次,需要定义预测函数用于输出最终信号,在其中需要调用sigmoid、softmax激活函数;然后,是损失函数和准确率的定义;最后,需要定义梯度运算法则(包括数值微分和误差反向传播)。

import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, 
                 weight_init_std=0.01):
        # 初始化权重
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(
            input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(
            hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    # x:输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x:输入数据, t:监督数据
    def numerical_gradient(self, x, t):
        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):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

代码解释:

  1. _init_函数:这是神经网络的构造函数,用于初始化神经网络的参数。参数包括:input_size、hidden_size、output_size以及weight_init_std。分别代表输入层的大小、隐藏层大小、输出层大小以及权重系数标准差(初始化为0.01)。对于权重的初始化,需要控制权重在一定范围,避免权重过大或过小导致出现梯度爆炸、梯度消失现象。
  2. predict函数:这是神经网络的预测函数,通过前向传播计算输出,隐藏层做sigmoid激活函数操作,输出层做softmax函数输出最终信号。
  3. loss函数:这是神经网络的损失函数。损失函数采用交叉熵误差,通过调用predict函数得到预测向量,利用预测向量和监督数据求出交叉熵误差。
  4. accuracy函数:这是神经网络的准确率函数。用于比较预测值与真实值的准确性。
  5. numerical_gradient函数:这是神经网络计算损失函数关于权重的梯度函数。
  6. gradient函数:这是神经网络通过反向传播计算梯度的方法,可以高速计算出损失函数关于权重的梯度。反向传播函数具体说明在下一章进行讲解。

根据定义的神经网络类,在MNIST数据集上做测试,绘制预测图像。

import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist_plus
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist_plus()
# 数据预处理
x_train = x_train.reshape(-1, 784)
x_test = x_test.reshape(-1, 784)
t_train = t_train.reshape(-1, 1)
t_test = t_test.reshape(-1, 1)

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

iters_num = 10000  # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01

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

iter_per_epoch = max(train_size / batch_size, 1)


for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]


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

    # 更新参数
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + 
              str(test_acc))
        if test_acc >= 0.98:
            break

# 绘制图形
markers = {'train': 'o', 'test': 's'}
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()

代码解释:
对于MNIST数据集,我们将其分批次进行处理,批量设置为100,迭代次数设置为10000,学习率设置为0.01。每次训练,我们从训练集中随机选择一定量的训练数据进行训练,通过梯度下降法寻找最优参数组合。

图7 准确率
从得到的准确率图像来看,当训练过程到第14个epoch时,准确率明显提升,设置当测试集上准确率达到0.98时,停止继续训练。

五 误差反向传播法

5.1 简单层的实现

了解了计算图后,我们可以对计算图中的乘法节点和加法节点加以实现,这里,将“乘法节点”称为乘法层,“加法节点”称为加法层。这里的“层”其实就是神经网络中的功能单位。比如,负责sigmoid函数的Sigmoid层、负责矩阵乘积的Affine层,都是以层作为单位进行运算。

5.1.1 乘法层的实现
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
对于乘法层类而言,其包含了构造函数x、y两个自变量,以及前向传播函数forward和反向传播函数backward。在反向传播过程中,得到的是关于x、y的偏导数,需要将结果翻转。上游传来的导数乘以正向传播的翻转值,然后传给下游。
5.1.2 加法层的实现
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

加法层不需要特意进行初始化,加法层的forward函数接收x、y两个参数,返回二者之和。backward函数将上游传来的导数原封不动地传递给下游。

5.2 激活函数层的实现

5.2.1 ReLU层实现

激活函数ReLU由下式表示。
y={xif x>00if x≤0y = \begin{cases} x & \text{if } x > 0 \\ 0 & \text{if } x \leq 0 \end{cases}y={x0if x>0if x0
通过上式,可以求出y关于x的导数,如下式所示:
y={1if x>00if x≤0y = \begin{cases} 1 & \text{if } x >0 \\ 0 & \text{if } x \leq 0 \end{cases}y={10if x>0if x0

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
对于ReLU层,Relu类含有成员变量mask,这是一个由True/False构成的NumPy数组,它会将正向传播时输入的x中小于等于0的位置保存为True,大于0的位置保存为False。

反向传播时,将上游传播过来的dout的mask中元素为True的位置设为0。

5.2.2 Sigmoid层实现

用计算图表示sigmoid函数,如下:
图8 Sigmoid函数计算图
除了上述乘法节点与加法节点之外,Sigmoid函数还出现了新的"exp"和"/"节点。分别进行y=exp(x)y=exp(x)y=exp(x)y=1xy=\frac{1}{x}y=x1计算。
根据链式求导法则和上述乘法层、加法层反向传播的转换可以得到下图(反向传播):
图9 Sigmoid反向传播
通过简化,可以将其修改为以下内容:
图10 Sigmoid简化图
下面进行Python代码实现:

class Sigmoid:
    def __init__(self):
        self.out = None
    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        self.out = out
        return out
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx

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

5.2.3 Affine层实现

神经网络正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。现在考虑以矩阵为对象的反向传播,可以得到下式:
∂L∂X=∂L∂Y⋅WT  ∂L∂W=XT⋅∂L∂Y\frac{\partial L}{\partial X}=\frac{\partial L}{\partial Y}\cdot W^T\\\;\\ \frac{\partial L}{\partial W}=X^T\cdot\frac{\partial L}{\partial Y}XL=YLWTWL=XTYL
于是,可以得到Affine层的反向传播计算图,如下:
图11 Affine计算图
下面来看Python实现,代码如下:

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None
    def forward(self, x):
        self.x = x
        out = np.dot(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)
        return dx
5.2.4 Softmax-with-Loss层

最后进行softmax层的实现,考虑到这里也包含作为损失函数的交叉熵误差,所以也称为“Softmax-with-Loss”层。计算过程省略,这里仅考虑“简易版”Softmax-with-Loss层计算图。
图12 简易版Softmax-with-Loss
可以观察到,Softmax-with-Loss层前向传播中传递过来的数值为经过softmax函数和交叉熵误差计算得到的损失函数值,反向传播中返回的导数值为softmax输出值减去监督数据值。因此,利用Python实现如下:

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 损失
        self.y = None    # softmax的输出
        self.t = None    # 监督数据(one-hot vector)
    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]
        dx = (self.y - self.t) / batch_size
        return dx

5.3 实验任务三

掌握采用mini-batch技术进行反向误差传播计算训练网络的方法,重点关注梯度计算,参数更新模块,阅读train_neuralnet.py 程序,比较它与第四章的相应程序,并给出程序重、难点注释, 利用MNIST的图像数据给出实验结果。

在进行具体的实现前,我们再来确认一下神经网络学习的全貌图。神经网络学习的步骤如下所示。

  • 前提: 神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为神经网络学习。
  • 步骤1(mini-batch):从训练数据中随机选择一部分数据。
  • 步骤2(计算梯度): 计算损失函数关于各个权重参数的梯度。
  • 步骤3(更新参数):将权重参数沿梯度方向进行微小的更新。
  • 步骤4(重复):重复步骤1、步骤2、步骤3。
import sys, os

from 深度学习.layers.layers import *

sys.path.append(os.pardir)
import numpy as np
from functions import *
from collections import OrderedDict


class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size,
                 weight_init_std=0.01):
        # 初始化权重
        self.params = {}
        self.params['W1'] = weight_init_std * \
                            np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
        # 生成层
        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'])
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

    # x: 输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def accuracy(self, x, t):
        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 gradient(self, x, t):
        # forward
        self.loss(x, t)
        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
        # 设定
        grads = {}
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        return grads

代码解释:
在生成层中,构建了一个有序字典,可以用于记录字典中添加元素的顺序,神经网络的正向传播只需要按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播 只需要按照相反的顺序调用各层即可。因为Affine层和ReLU层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。

# coding: utf-8
import sys, os

from matplotlib import pyplot as plt

sys.path.append(os.pardir)

import numpy as np
from dataset.mnist import load_mnist_plus
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist_plus()
# 数据预处理
x_train = x_train.reshape(-1, 784)
x_test = x_test.reshape(-1, 784)
t_train = t_train.reshape(-1, 1)
t_test = t_test.reshape(-1, 1)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01

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

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 梯度
    # grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)

    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)
        if test_acc >= 0.98:
            break
# 绘制图形
markers = {'train': 'o', 'test': 's'}
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()

图13 MNIST实现plus
观察对比图7与图13,发现图像趋势差不多,均呈现出在某一个epoch上准确率突然升高的现象。

六 学习技巧

写在前面:
在前面,我们通过使用随机梯度下降优化算法(SGD)用于训练机器学习模型,相较于其它优化算法,SGD的实现较为简单,只涉及简单的参数更新规则。但是, SGD 更新模型参数时使用的是随机样本或小批样本,因此会导致参数更新的方向不稳定,可能会在优化过程中出现震荡。 从实验任务二和实验任务三得到图像结果来看,图像曲线发展趋势呈现爆炸式,若不设定准确率大于等于0.98时停止训练的要求,曲线会出现明显的震荡现象。因此,在此介绍 Momentum、AdaGrad、Adam这3种方法来取代SGD。

6.1 Momentum

Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方法,如下所示。
v=αv−η∂L∂WW=W+vv=\alpha v-\eta\frac{\partial L}{\partial W}\\W=W+vv=αvηWLW=W+v
大致公式与SGD相同,Momentum优化算法多出了v变量,就是物理意义上的速度。第一个式子可以表达物体的速度在梯度方向上的受力,在该力的作用下,物体的速度发生变化。式中有αv\alpha vαv一项,在物体不受任何外力的作用下,该项承担使物体逐渐减速的任务,对应物理意义上的地面摩擦或空气阻力。下面看代码实现:

class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] -
                                        self.lr*grads[key]
            params[key] += self.v[key]

实例变量v会保存物体的速度。初始化时,v中什么都不保存,但当第一次调用update()时,v会以字典型变量的形式保存与参数结构相同的数据。更新剩余部分参照公式即可。

6.2 AdaGrad

在神经网络学习中,学习率的值十分重要。学习率过小,会导致学习时间过长;反过来,学习率过大,会导致学习发散而不能正确进行。在关于学习率的有效技巧上,有一种被称为学习率衰减的方法,即随着学习的进行,使学习率逐渐减小。 实际上,一开始“多” 学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习。用数学式表达AdaGrad如下:
h=h+∂L∂W∂L∂Wh=h+\frac{\partial L}{\partial W}\frac{\partial L}{\partial W}h=h+WLWL
W=W−η1h∂L∂WW=W-\eta\frac{1}{\sqrt{h}}\frac{\partial L}{\partial W}W=Wηh1WL
对比SGD,这里出现了新的变量h,它保存了以前所有梯度值的平方和。然后,在更新参数时,通过乘以1h\frac{1}{\sqrt{h}}h1,就可以调整学习率的大小。 这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
        for key, val in params.items():
            self.h[key] = np.zeros_like(val)
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / 
                                    (np.sqrt(self.h[key]) + 1e-7)

在代码实现中,最后一行加上了微小值1e-7。这是为了防止当 self.h[key]中有0时,将0用作除数的情况。在很多深度学习的框架中,这个微小值也可以设定为参数,但这里我们用的是1e-7这个固定值。  

6.3 Adam

Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样呢?这就是Adam方法的基本思路。直观地来说,基于Adam的更新过程就像小球在碗中滚动一样。虽然Momentun也有类似的移动,但是相比之下,Adam的小球左右摇晃的程度有所减轻。这得益于学习的更新程度被适当地调整了。代码实现过程见common/optimizer.py中的Adam类。

6.4 实验任务四

(ch6) 掌握网络训练的有关技巧,阅读optimizer_compare_naive.py,了解四种参数更新的特点, 给出程序的重、难点注释,阅读optimizer_compare_mnist.py,利用MNIST的图像数据展示实验结果; 理解参数初始值对网络训练的影响,阅读weight_init_activation_histogram.py,给出程序的重、难点注释,并展示实验结果。

6.4.1 对比参数更新路径

目前为止,我们了解了4种更新学习的方法,分别是SGD、Momentum、AdaGrad、Adam,那么对于这四种方法,我们将其应用到具体的函数当中,比较它们的参数更新路径。具体函数为f(x,y)=x220+y2f(x,y)=\frac{x^2}{20}+y^2f(x,y)=20x2+y2
图14 优化方法对比
从生成图像来看,AdaGrad方法要优于其它3中优化算法,但是,结果会根据要解决的问题而变。并且,很显然,超参数(学习率等)的设定值不同,结果也会发生变化。

6.4.2 基于手写数字识别的参数更新对比
我们以手写数字识别为例,比较前面介绍的SGD、Momentum、 AdaGrad、Adam这4种方法,并确认不同的方法在学习进展上有多大程度的差异。源代码见 ch06/optimizer_compare_ mnist.py。得到的图像如下:

图15 基于mnist优化方法对比 从mnist手写数字集中的对比图发现,与SGD对比,其它三张优化方法的损失函数值会下降得更快,其中AdaGrad方法会更快一些 。这个实验需要注意的地方是,实验结果会随学习率等超参数、神经网络的结构(几层等)的不同而发生变化。不过,一般而言,与SGD相比,其他3种方法可以学习得更快,有时最终的识别精度也更高。

6.4.3 参数初始值对于网络的影响

在神经网络中,参数的初始值对网络训练的影响非常重要,它可以影响到网络的收敛速度、收敛到的局部最优解质量以及整体的性能表现。以下是几个常见的影响方面:

  1. 收敛速度
  • 参数的初始值会影响网络开始时的梯度大小和方向,从而影响梯度下降的步长和方向。较好的初始值可以加速网络的收敛,使得在相同迭代次数下更快地接近最优解。
  1. 避免梯度消失或梯度爆炸
  • 当初始值过大或过小时,可能会导致梯度消失(梯度接近零,导致网络停止学习)或梯度爆炸(梯度变得极大,导致参数更新过大而无法稳定)。合适的初始值可以帮助避免这些问题的发生。
  1. 避免陷入局部最优解
  • 不同的初始值可能会导致网络最终收敛到不同的局部最优解。通过使用合适的初始值,可以增加网络探索到更好解决方案的可能性,而不是陷入次优解或局部最优解。
  1. 影响网络泛化能力
  • 参数的初始值也可能影响网络在测试数据集上的泛化能力。如果训练过程中网络能够更快地收敛到一种泛化能力更强的解,那么最终网络在新数据上的表现可能会更好。
附:常见的参数初始化方法

在实际应用中,有几种常见的参数初始化方法,每种方法对网络训练的影响略有不同:

  • 随机初始化:将参数初始化为随机值,通常服从某种分布,如均匀分布或正态分布。这是最常见的初始化方法之一。
  • Xavier初始化(也称为Glorot初始化):该方法通过考虑输入和输出神经元数量的平方根来初始化参数,旨在使得每一层的激活值保持在一个合理范围内。
  • He初始化:针对使用ReLU激活函数的网络,He初始化使用了ReLU函数特有的特性,通过考虑输入神经元数量的平方根来初始化参数。
  • 零初始化:将所有参数初始化为零,通常不推荐,因为这会导致每个神经元的梯度相同,最终参数也会同步更新。

选择适当的初始化方法取决于网络的结构、激活函数的选择以及具体的问题域。在实际应用中,通常会通过实验和验证来选择最佳的初始化方法,以获得更好的训练效果和性能表现。

6.4.4 隐藏层的激活值分布
观察隐藏层的激活值(激活函数的输出数据)的分布,可以获得很多启发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的激活值的分布的。这里要做的实验是,向一个5层神经网络(激活函数使用 sigmoid函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分布。代码见weight_init_activation_histogram.py。
这里假设神经网络有5层,每层有100个神经元。然后,用高斯分布随机生成1000个数据作为输入数据,并把它们传给5层神经网络。激活函数使 用sigmoid函数,各层的激活值的结果保存在activations变量中。这个代码 段中需要注意的是权重的尺度。虽然这次我们使用的是标准差为1的高斯分 布,但实验的目的是通过改变这个尺度(标准差),观察激活值的分布如何变 化。现在,我们将保存在activations中的各层数据画成直方图。  

图16 隐藏层激活值分布
从图中可知,各层的激活值呈偏向0和1的分布。这里使用的sigmoid 函数是S型函数,随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0。因此,偏向0和1的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失(gradient vanishing)。层次加深的深度学习中,梯度消失的问题可能会更加严重。

七 卷积神经网络

本章的主题是卷积神经网络(Convolutional Neural Network,CNN)。 CNN被用于图像识别、语音识别等各种场合,在图像识别的比赛中,基于深度学习的方法几乎都以CNN为基础。本章将详细介绍CNN的结构,并用 Python实现其处理内容。

7.1 概念介绍

卷积神经网络(Convolutional Neural Network, CNN)是一类专门用于处理具有类似网格结构的数据的神经网络。它在计算机视觉领域中得到了广泛的应用,因为它能有效地捕捉图像、视频等数据中的空间结构信息。

7.1.1 CNN的主要特点和组成部分:
  1. 卷积层(Convolutional Layer)
  • 卷积层是CNN的核心组成部分,它通过卷积操作提取输入数据的特征。卷积操作在输入数据和一组可学习的卷积核(filter)之间进行,生成特征图(feature map)作为下一层的输入。
  1. 池化层(Pooling Layer)
  • 池化层用于减少特征图的空间尺寸,同时保留重要的特征信息。常见的池化操作包括最大池化和平均池化,它们分别从输入的局部区域中提取最大值或平均值。
  1. 激活函数(Activation Function)
  • 激活函数在CNN中用于引入非线性特性,典型的激活函数包括ReLU(Rectified Linear Unit)、sigmoid和tanh。ReLU是最常用的激活函数,因为它能够缓解梯度消失问题,并加速网络的收敛。
  1. 全连接层(Fully Connected Layer)
  • 在CNN的最后几层通常会包含全连接层,用于将卷积层和池化层提取的特征映射转换为输出类别的概率分布。全连接层的每个神经元与前一层的所有神经元连接。
  1. 损失函数(Loss Function)
  • CNN通常使用交叉熵损失函数来衡量预测输出与真实标签之间的差异。在训练过程中,优化算法通过最小化损失函数来调整网络参数。
  1. 优化器(Optimizer)
  • 优化器负责根据损失函数的梯度更新网络的权重和偏置,常见的优化算法包括随机梯度下降(SGD)、Adam、Adagrad等。
  1. 批标准化(Batch Normalization)
  • 批标准化用于加速神经网络的训练过程,通过规范化每个特征的均值和方差来加快收敛速度,并且有助于缓解梯度消失问题。
7.1.2 CNN的训练和应用:
  • 训练过程:CNN通常通过前向传播计算损失,然后反向传播计算梯度,并利用优化算法更新参数。训练数据通常是由标记好的图像和其对应的类别标签组成。
  • 数据增强(Data Augmentation):为了增强模型的泛化能力,常常会对训练数据进行随机的旋转、缩放、平移等变换,以生成更多样化的训练样本。
  • 迁移学习(Transfer Learning):在一些情况下,可以利用预训练好的CNN模型,如在ImageNet数据集上训练的模型,然后通过微调或特征提取的方式应用于特定任务,以减少训练时间和提高性能。

CNN的设计和优化是深度学习中一个重要的研究方向,不断优化的网络结构和训练技巧使得CNN在图像分类、目标检测、图像分割等任务上取得了很大的成功和广泛的应用。

7.2 卷积层

在之前介绍的神经网络中使用了全连接层(Affine层)。在全连接层中,相邻层的神经元全部连接在一起,输出的数量可以任意决定。
全连接层存在什么问题呢?那就是数据的形状被“忽视”了。比如,输入数据是图像时,图像通常是高、长、通道方向上的3维形状。但是,向全连接层输入时,需要将3维数据拉平为1维数据。实际上,前面提到的使用 了MNIST数据集的例子中,输入图像就是1通道、高28像素、长28像素 的(1,28,28)形状,但却被排成1列,以784个数据的形式输入到最开始的 Affine层。
图像是3维形状,这个形状中应该含有重要的空间信息。比如,空间上邻近的像素为相似的值、RBG的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等,3维形状中可能隐藏有值得提取的本质模式。但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元 (同一维度的神经元)处理,所以无法利用与形状相关的信息。
而卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维 数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此, 在CNN中,可以正确理解图像等具有形状的数据。 另外,CNN中,有时将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图(input feature map), 输出数据称为输出特征图(output feature map)。本书中将“输入输出数据”和“特征图”作为含义相同的词使用。
卷积层进行的是卷积运算。卷积运算相当于图像处理中的“滤波器运算”,下面,通过图示讲解,我们深入理解卷积运算的过程。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
如图所示,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积 累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有 位置都进行一遍,就可以得到卷积运算的输出。

7.3 填充

在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如0等),这称为填充(padding),是卷积运算中经常会用到的处理。
如图17所示,在卷积运算的过程中,输入的数据大小为(4,4),滤波器大小为(3,3),最终输出的大小为(2,2),相当于输出大小比输入大小缩小了2个元素。这在反复进行多次卷积运算的深度网 络中会成为问题。为什么呢?因为如果每次进行卷积运算都会缩小 空间,那么在某个时刻输出大小就有可能变为1,导致无法再应用 卷积运算。为了避免出现这样的情况,就要使用填充。
图18 填充
如图所示,对输入数据进行幅度为1的填充,使大小为(4,4)的输入数据变成了(6,6)的维度,“幅度为1的填充”是指用幅度为1像素的0填充周围。这样一来,最终得到的输出数据仍然是(4,4)的大小。 因此,卷积运算就可以在保持空间大小不变的情况下将数据传给下一层。

7.4 步幅

应用滤波器的位置间隔称为步幅,之前的例子中的步幅都是1,如果将步幅设为2,如图所示,滤波器的窗口的间隔变为2个元素。
图19 步幅为2
比较填充和步幅,我们发现,当填充增大时,输出数据大小会增大;当步幅增大时,输出数据大小会减小。 如果将这样的关系写成算式,会如何呢?接下来,我们看一下对于填充和步幅,如何计算输出大小。
这里,假设输入大小为(H,W),滤波器大小为(FH,FW),输出大小为 (OH,OW),填充为P,步幅为S。此时,输出大小可通过如下公式进行计算。
OH=H+2P−FHS+1OH = \frac{H+2P-FH}{S}+1OH=SH+2PFH+1
OW=W+2P−FWS+1OW = \frac{W+2P-FW}{S}+1OW=SW+2PFW+1

7.5 3维数据卷积运算

之前的卷积运算的例子都是以有高、长方向的2维形状为对象的。但是,图像是3维数据,除了高、长方向之外,还需要处理通道方向。这里,我们按照与之前相同的顺序,看一下对加上了通道方向的3维数据进行卷积运算的例子。  

通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。
图20 3维数据卷积计算
需要注意的是,在3维数据的卷积运算中,输入数据和滤波器的通道数要设为相同的值。在这个例子中,输入数据和滤波器的通道数一致,均为3。

7.6 池化层

池化(Pooling)是卷积神经网络(CNN)中的一种操作,主要用于减少特征图的空间尺寸,同时保留重要的特征信息。池化操作通常在卷积层之后进行,有助于减少模型的参数数量,提高模型的计算效率,并且可以一定程度上减少过拟合。

7.6.1 池化的主要类型
  1. 最大池化(Max Pooling)
  • 最大池化是一种常见的池化操作,它从输入特征图的局部区域中选取最大值作为输出。例如,在2x2的窗口内,最大池化会选择4个像素中的最大值作为输出,以减少图像中的空间维度。
  • 最大池化有助于保留主要特征,例如在物体识别任务中,保留物体的边界信息和纹理特征。
  1. 平均池化(Average Pooling)
  • 平均池化是另一种常见的池化操作,它从输入特征图的局部区域中计算平均值作为输出。与最大池化相比,平均池化更加平滑,可以降低噪声的影响。
  • 平均池化适用于那些在图像中没有明显位置的特征,例如图像中的整体颜色分布或者纹理。
7.6.2 池化的作用和优势
  • 减少计算量:通过减少每个特征图的空间维度,池化操作可以减少后续层的参数数量和计算量,从而提高模型的效率。
  • 保持平移不变性:池化操作在局部区域内提取特征的过程中,不受物体在图像中位置的微小变化影响,从而提高模型的稳定性和泛化能力。
  • 降低过拟合风险:池化操作减少了特征图的维度,有助于减少模型在训练集上的过拟合风险,提高在测试集上的泛化能力。
7.6.3 池化的注意事项
  • 池化大小和步长:池化操作通常需要指定池化窗口的大小和步长。较大的池化窗口会进一步减少特征图的维度,而较小的池化窗口可以保留更多的细节信息。
  • 堆叠池化层:在深层网络中,可以堆叠多个池化层来进一步减少特征图的维度,但要注意不要过度降低特征图的分辨率,以免丢失重要的信息。

池化是CNN中不可或缺的一部分,它通过有效地减少数据量和提取重要特征来帮助神经网络更好地理解和处理图像数据。

7.7 实验任务五

(ch7) 掌握卷积神经网络的计算与学习,阅读simple_convnet.py和train_convnet.py, 实现如图 2 所示的简单CNN网络.阅读visualize_filter.py,展示滤波特征图,给出上述3个程序的重、难点注释, 并给出实验结果。

7.7.1 simple_convnet.py 解释:
  1. 初始化 (**__init__**** 方法)**:
  • 卷积层 (Conv1):使用指定的卷积参数初始化卷积核权重 W1 和偏置 b1
  • ReLU 激活函数 (Relu1):通过 Relu 类进行激活。
  • 池化层 (Pool1):使用指定的池化参数初始化池化层。
  • 全连接层 (Affine1, Affine2):通过 Affine 类初始化全连接层的权重 W2, W3 和偏置 b2, b3
  • SoftmaxWithLoss 层 (SoftmaxWithLoss):用于计算 softmax 输出并计算交叉熵损失。
  1. 前向传播 (**predict**** 方法)**:
  • 依次通过各层进行前向传播,得到最终的预测结果。
  1. 损失计算 (**loss**** 方法)**:
  • 根据输入数据 x 和标签 t 计算损失值,内部调用 SoftmaxWithLoss 层的前向传播方法。
  1. 梯度计算 (**numerical_gradient**** 和 **gradient** 方法)**:
  • numerical_gradient 方法使用数值微分计算每一层的梯度,适用于梯度检查。
  • gradient 方法使用误差反向传播法计算每一层的梯度,通常用于实际训练中。
  1. 参数保存和加载 (**save_params**** 和 **load_params** 方法)**:
  • save_params 方法将模型的参数保存到文件中,通常是 .pkl 格式。
  • load_params 方法从文件中加载参数,并将其设置到模型的各层中。
7.7.2 train_convnet.py解释

这段代码是一个简单的卷积神经网络(SimpleConvNet)在MNIST数据集上训练和评估的示例。以下是对代码的解释和可能的改进建议:

  1. 导入必要的库和模块
  • 使用 sys.path.append 添加父目录以便导入自定义模块。
  • 导入 numpymatplotlib.pyplot 库用于数据处理和结果可视化。
  • 导入 load_mnist 函数用于加载MNIST数据集。
  1. 加载数据集
  • 使用 load_mnist 函数加载MNIST数据集。这里的 flatten=False 参数表示保持图像的二维结构,即 (1, 28, 28) 的格式。
  1. 减少数据量(可选)
  • 如果数据集较大,可以选择减少训练和测试数据的规模以加快训练过程。在这段代码中,使用了注释掉的代码来限制训练集和测试集的样本数量。
  1. 初始化网络和训练参数
  • 定义了一个 SimpleConvNet 网络,指定了输入维度 (1,28,28),卷积层参数 {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1},隐藏层大小为 100,输出大小为 10,权重初始化标准差为 0.01
  • 创建了一个 Trainer 对象,用于管理训练过程,包括设置训练和测试数据、优化器选择(这里选择了 Adam 优化器)、学习率等参数。
  1. 训练网络
  • 调用 trainer.train() 方法进行训练,训练过程会打印每个 epoch 的训练和测试精度,并更新 train_acc_listtest_acc_list
  1. 保存模型参数
  • 使用 network.save_params("params.pkl") 方法将训练好的模型参数保存到文件中,以便之后可以加载模型进行预测或继续训练。
  1. 绘制训练曲线
  • 使用 matplotlib.pyplot 绘制训练过程中训练集和测试集精度随 epoch 变化的曲线图。
7.7.3 visualize_filter.py展示
  1. 导入库和模块
  • 导入了 numpy 用于数值计算和数组操作。
  • 导入了 matplotlib.pyplot 用于绘图和可视化。
  • 导入了自定义的 SimpleConvNet 类,这个类实现了一个简单的卷积神经网络。
  1. 定义 **filter_show** 函数
  • 这个函数用于显示卷积层的权重(也称为过滤器)。
  • 参数 filters 是一个四维的 numpy 数组,形状为 (FN, C, FH, FW)
    • FN 是过滤器的数量,
    • C 是通道数(在灰度图像中通常为1),
    • FH 是过滤器的高度,
    • FW 是过滤器的宽度。
  • nx 参数指定每行显示的过滤器数量,默认为 8。
  • margin 参数指定过滤器之间的间距,默认为 3。
  • scale 参数指定显示过滤器的缩放比例,默认为 10。
  • 函数首先计算出要显示的行数 ny,然后创建一个新的图形窗口(figure)。
  • 使用循环遍历每个过滤器,并在子图中显示过滤器的图像。
  • 最后调用 plt.show() 显示图形窗口。
  1. 创建 **SimpleConvNet** 实例
  • 使用 SimpleConvNet() 创建一个简单的卷积神经网络实例 network
  1. 显示随机初始化后的卷积层权重
  • 使用 filter_show(network.params['W1']) 显示网络中第一个卷积层的随机初始化后的权重。
  1. 加载经过训练后的模型参数
  • 使用 network.load_params("params.pkl") 加载经过训练后的模型参数。前提是 params.pkl 文件存在且包含了正确的模型参数。
  1. 显示经过训练后的卷积层权重
  • 再次调用 filter_show(network.params['W1']) 显示经过训练后的卷积层权重。

这段代码的主要目的是演示如何使用 filter_show 函数来可视化卷积神经网络中卷积层的权重,以便观察它们在训练前后的变化。
图21 随机权重特征
图22 学习后的权重特征

八 深度学习体会

通过学习深度学习课程,我深有体会,具体如下几个方面:

  1. 数学基础的重要性
    深度学习涉及大量的数学理论,特别是线性代数、微积分、概率统计等。良好的数学基础能够帮助理解深度学习模型的原理、优化算法的推导过程以及评估模型性能的方法。
  2. 编程实践的必要性
    实践是学习深度学习的关键。通过编写代码实现模型、训练数据集和评估结果,可以加深对算法和模型的理解。建议掌握 Python 编程语言及其相关的科学计算库(如 NumPy、Pandas)。
  3. 理论与实践的结合
    深度学习是理论和实践相结合的学科。在学习理论的同时,尝试实现论文中的模型或算法,并在真实数据集上进行实验,这有助于加深对理论的理解和应用能力的提升。
  4. 项目驱动学习
    参与深度学习项目是学习的重要途径。可以选择一些小规模的项目来开始,逐步积累经验和技能。例如,可以从图像分类、文本分类或者简单的生成模型等问题入手。
  5. 跨学科的视角
    深度学习通常涉及多个学科领域,包括计算机科学、数学、统计学等。保持跨学科的视角,能够更好地理解深度学习的发展和应用,同时也能够在不同领域中找到创新的机会。
  6. 社区和资源的利用
    深度学习领域有着活跃的学术和开源社区,例如 GitHub、论坛、博客等。积极参与社区讨论,阅读他人的研究成果和代码实现,可以拓展视野,获取实用的技术和方法。
  7. 持续学习和实践
    深度学习技术在快速发展,新的算法和模型不断涌现。保持持续学习的态度,通过阅读最新的研究论文、参加相关的学术会议和比赛,不断提升自己的专业能力和竞争力。
  8. 挑战与成就感
    深度学习学习曲线较陡,会遇到挑战和困难。克服难题和解决问题时的成就感是深度学习学习过程中的重要动力,要保持耐心和坚持不懈的精神。

最后, 深度学习是一个充满挑战但也充满机会的领域,希望在未来,我可以在这个领域取得令人满意的成就!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值