《深度学习入门》学习笔记

1. Python入门

Python解释器也被称为“对话模式”,用户能够以和Python对话的方式进行编程。

Python是属于“动态类型语言”的编程语言,所谓动态,是指变量的类型是根据情况自动决定的。

1.1 NumPy

在深度学习的实现中,经常出现数组和矩阵的计算。NumPy的数组类(numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使用这些方法

NumPy 数组(np.array)可以生成N维数组,即可以生成一维数组、二维数组、三维数组等任意维数的数组。数学上将一维数组称为向量,将二维数组称为矩阵。另外,可以将一般化之后的向量或矩阵等统称为张量(tensor)。

使用步骤:

  1. 导入NumPy
import numpy as np

Python 中使用import 语句来导入库。这里的import numpy as np,直译的话就是“将numpy作为np 导入”的意思。通过写成这样的形式,之后NumPy相关的方法均可通过np来调用。

  1. 生成NumPy 数组

要生成NumPy数组,需要使用np.array()方法。np.array()接收Python列表作为参数,生成NumPy数组(numpy.ndarray)。

>>> x = np.array([1.0, 2.0, 3.0])
>>> print(x)
[ 1. 2. 3.]
>>> type(x)
<class 'numpy.ndarray'>
  1. NumPy 的算术运算
>>> x = np.array([1.0, 2.0, 3.0])
>>> y = np.array([2.0, 4.0, 6.0])
>>> x + y # 对应元素的加法
array([ 3., 6., 9.])
>>> x - y
array([ -1., -2., -3.])
>>> x * y # element-wise product
array([ 2., 8., 18.])
>>> x / y
array([ 0.5, 0.5, 0.5])

这里需要注意的是,数组x和数组y的元素个数是相同的(两者均是元素个数为3 的一维数组)。当x和y的元素个数相同时,可以对各个元素进行算术运算。如果元素个数不同,程序就会报错,所以元素个数保持一致非常重要

>>> x = np.array([1.0, 2.0, 3.0])
>>> x / 2.0
array([ 0.5, 1. , 1.5])

此时,需要在NumPy数组的各个元素和标量之间进行运算。这个功能被称为广播.

  1. NumPy 的N维数组
>>> A = np.array([[1, 2], [3, 4]])
>>> print(A)
[[1 2]
[3 4]]
>>> A.shape
(2, 2)
>>> A.dtype
dtype('int64')

这里生成了一个2 × 2 的矩阵A。另外,矩阵A的形状可以通过shape查看,矩阵元素的数据类型可以通过dtype查看

>>> B = np.array([[3, 0],[0, 6]])
>>> A + B
array([[ 4, 2],
		[ 3, 10]])
>>> A * B
array([[ 3, 0],
		[ 0, 24]])

和数组的算术运算一样,矩阵的算术运算也可以在相同形状的矩阵间以对应元素的方式进行。并且,也可以通过标量(单一数值)对矩阵进行算术运算。这也是基于广播的功能。

>>> print(A)
[[1 2]
[3 4]]
>>> A * 10
array([[ 10, 20],
		[ 30, 40]])
  1. 广播

NumPy中,形状不同的数组之间也可以进行*运算。

>>> A = np.array([[1, 2], [3, 4]])
>>> B = np.array([10, 20])
>>> A * B
array([[ 10, 40],
		[ 30, 80]])

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjSqtNo3-1654612519093)(深度学习入门.assets/image-20220519225741290.png)]

>>> A = np.array([[1,2],[3,4]])
>>> B = np.array([10,20,30])
>>> A * B
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (2,2) (3,)
>>> B = np.array([[10],[20]])
>>> A * B
array([[10, 20],
       [60, 80]])

由此可见广播必须得满足扩展成大数组的特性

  1. 访问元素

可以使用索引的方式访问

>>> X = np.array([[51, 55], [14, 19], [0, 4]])
>>> print(X)
[[51 55]
[14 19]
[ 0 4]]
>>> X[0] # 第0行
array([51, 55])
>>> X[0][1] # (0,1)的元素
55

也可以使用for语句访问各个元素。

>>> for row in X:
...     print(row)
...
[51 55]
[14 19]
[0 4]

NumPy还可以使用数组访问各个元素。

>>> X = X.flatten() # 将X转换为一维数组
>>> print(X)
[51 55 14 19 0 4]
>>> X[np.array([0, 2, 4])] # 获取索引为0、2、4的元素
array([51, 14, 0])
>>> X[[0,2]]
array([51, 14])

运用这个标记法,可以获取满足一定条件的元素。例如,要从X中抽出大于15 的元素,可以写成如下形式。只能有一个判断元素?

>>> X > 15
array([ True, True, False, True, False, False], dtype=bool)
>>> X[X>15]
array([51, 55, 19])
>>> X[X>15 and X < 20]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

1.2 Matplotlib

在深度学习的实验中,图形的绘制和数据的可视化非常重要。Matplotlib是用于绘制图形的库,使用Matplotlib 可以轻松地绘制图形和实现数据的可视化。这里,我们来介绍一下图形的绘制方法和图像的显示方法。

绘制简单图形

import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(0, 6, 0.1) # 以0.1为单位,生成0到6的数据
y = np.sin(x)

# 绘制图形
plt.plot(x, y)
plt.show()

这里使用NumPy的arange方法生成了[0, 0.1, 0.2, … , 5.8, 5.9]的数据,将其设为x。对x的各个元素,应用NumPy的sin 函数np.sin(),将x、y的数据传给plt.plot方法,然后绘制图形。最后,通过plt.show()显示图形。

在这里插入图片描述

pyplot的功能

在刚才的sin 函数的图形中,我们尝试追加cos 函数的图形,并尝试使用pyplot的添加标题和x轴标签名等其他功能。

import numpy as np
import matplotlib.pyplot as plt
# 生成数据
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") # label添加标题
plt.plot(x, y2, linestyle = "--", label="cos") # linestyle用虚线绘制
plt.xlabel("x") # x轴标签
plt.ylabel("y") # y轴标签
plt.title('sin & cos') # 标题
plt.legend()
plt.show()

在这里插入图片描述

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Ellipse

fig, ax = plt.subplots(1, 2, subplot_kw={"aspect": "equal"})

angles = np.linspace(0, 180, 8)

ellipse = [Ellipse((3, 3), 4, 1, a) for a in angles]

for elle in ellipse:
    ax[0].add_patch(elle)
    elle.set_alpha(0.4)
    elle.set_color("#FF5511")

ax[0].axis([0, 6, 0, 6])

num = np.arange(0, 100, 1)

ellipse = [Ellipse(xy=np.random.rand(2)*10+1, width=np.random.rand(1), height=np.random.rand(1),
                   angle=np.random.rand(1)*360) for i in num]

for elle in ellipse:
    ax[1].add_patch(elle)
    elle.set_alpha(np.random.rand(1))
    elle.set_color(np.random.rand(3))

ax[1].axis([0, 12, 0, 12])

plt.tight_layout()

plt.show()

在这里插入图片描述

显示图像

pyplot 中还提供了用于显示图像的方法imshow()。另外,可以使用matplotlib.image模块的imread()方法读入图像。下面我们来看一个例子。

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

img = imread('fate.png') # 读入图像(设定合适的路径!)
plt.imshow(img)
plt.show()

在这里插入图片描述

1.3 小结

  • Python是一种简单易记的编程语言。
  • Python是开源的,可以自由使用。
  • 使用NumPy和Matplotlib 这两种外部库。
  • Python有“解释器”和“脚本文件”两种运行模式。
  • Python能够将一系列处理集成为函数或类等模块。
  • NumPy中有很多用于操作多维数组的便捷方法。
  • Matplotlib 中有很多操作图像的方法

2. 感知机

在这里插入图片描述

​ 感知机就是一个多输入,单输出的一个算法,x是节点的值只能为0或1,每个输入过程都有一个权值(w是weight 的首字母),图中的○称为**“神经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重(w1x1、w2x2)神经元会计算传送过来的信号的总和,当这个总和超过了某个界限值时会输出1(1 对应“传递信号”)否则输出0(0对应“不传递信号”)这也称为“神经元被激活” 。这里将这个界限值称为阈值**,用符号θ 表示。

在这里插入图片描述

​ 感知机的多个输入信号都有各自固有的权重,这些权重发挥着控制各个信号的重要性的作用。也就是说,权重越大,对应该权重的信号的重要性就越高。

2.1 简单逻辑电路

  1. 与门

(w1, w2, θ)=(0.5, 0.5, 0.7)
w1 <= θ && w2 <= θ && θ > 0

  1. 与非门

(w1, w2, θ)= (−0.5, −0.5, −0.7)
只要把实现与门的参数值的符号取反,就可以实现与非门。

  1. 或门

(w1, w2, θ)=(0.5,0.5,0.3)
w1 > θ && w2 > θ && θ > 0

这里决定感知机参数的并不是计算机,而是我们人。我们看着真值表这种“训练数据”,人工考虑(想到)了参数的值。而机器学习的课题就是将这个决定参数值的工作交由计算机自动进行。学习是确定合适的参数的过程,而人要做的是思考感知机的构造(模型),并把训练数据交给计算机。

也就是说,相同构造的感知机,只需通过适当地调整参数的值,就可以像“变色龙演员”表演不同的角色一样,变身为与门、与非门、或门。

2.2 感知机的实现

简单实现

  1. 与门
def AND(x1, x2):
    w1 = 0.5
    w2 = 0.5
    theta = 0.7
    if w1 * x1 + w2 * x2 > theta:
        return 1
    else:
        return 0


print(AND(0, 0))
print(AND(0, 1))
print(AND(1, 0))
print(AND(1, 1))
  1. 非门
def NOT(x):
    return -x + 1
  1. 与非门
def NAND(x1, x2):
    return NOT(AND(x1, x2))
  1. 或门
def OR(x1, x2):
    w1 = 0.5
    w2 = 0.5
    theta = 0.4
    if w1 * x1 + w2 * x2 > theta:
        return 1
    else:
        return 0

导入权重和偏置

刚才的与门的实现比较直接、容易理解,但是考虑到以后的事情,我们将其修改为另外一种实现形式。在此之前,首先把式子的θ 换成−b,于是就可以用下式来表示感知机的行为。

在这里插入图片描述

两式虽然有一个符号不同,但表达的内容是完全相同的。此处,b 称为偏置,w1 和w2 称为权重。如上式所示,感知机会计算输入信号和权重的乘积,然后加上偏置,如果这个值大于0 则输出1,否则输出0。

>>> import numpy as np
>>> x = np.array([0, 1]) # 输入
>>> w = np.array([0.5, 0.5]) # 权重
>>> b = -0.7 # 偏置
>>> w*x
array([ 0. , 0.5])
>>> np.sum(w*x)
0.5
>>> np.sum(w*x) + b
-0.19999999999999996 # 大约为-0.2(由浮点小数造成的运算误差)

使用权重和偏置的实现

  1. 与门
def AND(x1, x2):
    w = np.array([0.5, 0.5])
    x = np.array([x1, x2])
    theta = 0.7
    b = -theta
    flag = np.sum(w * x) + b
    if flag > 0:
        return 1
    else:
        return 0
def AND(x1, x2):
    w1 = 0.5
    w2 = 0.5
    b = -0.7
    flag = w1 * x1 + w2 * x2 + b
    if flag > 0:
        return 1
    else:
        return 0


print(AND(0, 0))
print(AND(0, 1))
print(AND(1, 0))
print(AND(1, 1))
  1. 非门
def NOT(x):
    return -x + 1
  1. 与非门
def NAND(x1, x2):
    return NOT(AND(x1, x2))
  1. 或门
def OR(x1, x2):
    w = np.array([0.5, 0.5])
    x = np.array([x1, x2])
    theta = 0.3
    b = -theta
    flag = np.sum(w * x) + b
    if flag > 0:
        return 1
    else:
        return 0

与门、与非门、或门是具有相同构造的感知机,区别只在于权重参数的值。因此,在与非门和或门的实现中,仅设置权重和偏置的值这一点和与门的实现不同。

2.3 感知机的局限性

异或门也被称为逻辑异或电路。感知机是无法实现这个异或门。

判断能否通过单层感知机直接实现的方式是判断感知机表达式是否是线性的

例如

这样的一个或门(b, w1, w2) = (−0.5, 1.0, 1.0)

在这里插入图片描述

最终的图像会被直线分割成上下两部分,上为1下为0

在这里插入图片描述

而异或的图像则是

在这里插入图片描述

这显然是一个非线性的图像故不能由单层感知机直接实现,但可以由多层感知机间接实现

2.4 多层感知机

叠加了多层的感知机称为多层感知机。

门电路的基本组成是与或非,任何的门电路均可有有这三项组合完成

在这里插入图片描述

代码实现

def XOR(x1, x2):
    flag = OR(AND(NOT(x1), x2), AND(x1, NOT(x2)))
    if flag > 0:
        return 1
    else:
        return 0

2.5 小结

  • 感知机是具有输入和输出的算法。给定一个输入后,将输出一个既定的值。
  • 感知机将权重和偏置设定为参数。
  • 使用感知机可以表示与门和或门等逻辑电路。
  • 异或门无法通过单层感知机来表示。
  • 使用2层感知机可以表示异或门。
  • 单层感知机只能表示线性空间,而多层感知机可以表示非线性空间。
  • 多层感知机(在理论上)可以表示计算机。

3. 神经网络

设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,现在还是由人工进行的。神经网络的出现就是为了解决这个问题。

我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层。中间层有时也称为隐藏层。“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。另外,可以把输入层到输出层依次称为第0 层、第1 层、第2 层。

在这里插入图片描述

复习感知机

感知机表达式

在这里插入图片描述

可以用函数h(x)表示为y = h(b + w1x1 + w2x2)

在这里插入图片描述

3.1 激活函数

刚才登场的h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。

激活函数以阈值为界,一旦输入超过阈值,就切换输出。这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。

实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了

sigmoid 函数

神经网络中经常使用的一个激活函数就是式(3.6)表示的sigmoid 函数(sigmoid function)。

在这里插入图片描述

式中的exp(−x)表示e^−x 的意思。e是纳皮尔常数2.7182 . . .

阶跃函数的实现

def step_function(x):
	if x > 0:
		return 1
	else:
		return 0

为了能够让跃迁函数支持数组输入输出故使用NumPy库

def step_function(x):
	y = x > 0
	return y.astype(np.int)

阶跃函数的图形

显示图形需要使用matplotlib库。

import numpy as np
import matplotlib.pylab as plt

def step_function(x):
	return np.array(x > 0, dtype=np.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()

在这里插入图片描述

如图所示,阶跃函数以0 为界,输出从0 切换为1(或者从1 切换为0)。它的值呈阶梯式变化,所以称为阶跃函数。

sigmoid 函数的实现

def sigmoid(x):
	return 1 / (1 + np.exp(-x))
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()

在这里插入图片描述

sigmoid 函数和阶跃函数的比较

在这里插入图片描述

  • 不同点:
    • 平滑性:sigmoid是平滑的阶跃函数是不平滑的
    • 连续性:sigmoid是连续的阶跃函数是不连续的
  • 相同点:
    • y的范围均在0,1之间
    • y均随着x增大而增大
    • 两者均为非线性函数

非线性函数

神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。

线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”

说白了就是加深层数的线性函数可以由另一个单层的线性函数表示

例如:

h(x) = cx

y(x) = h(h(h(x)))=c * c * c * x = a * x (a = c^3)

ReLU函数

ReLU函数在输入大于0 时,直接输出该值;在输入小于等于0 时,输出0

在这里插入图片描述

def relu(x):
	return np.maximum(0, x)

在这里插入图片描述

3.2 多维数组的运算

>>> import numpy as np
>>> A = np.array([1, 2, 3, 4])
>>> print(A)
[1 2 3 4]
>>> np.ndim(A)
1
>>> A.shape
(4,)
>>> A.shape[0]
4

数组的维数可以通过np.dim()函数获得
数组的形状可以通过实例变量shape获得
A.shape的结果是个元组(tuple)

矩阵乘法

>>> A = np.array([[1,2], [3,4]])
>>> A.shape
(2, 2)
>>> B = np.array([[5,6], [7,8]])
>>> B.shape
(2, 2)
>>> np.dot(A, B)
array([[19, 22],
		[43, 50]])

矩阵乘法使用dot函数

神经网络的内积

>>> X = np.array([1, 2])
>>> X.shape
(2,)
>>> W = np.array([[1, 3, 5], [2, 4, 6]])
>>> print(W)
[[1 3 5]
[2 4 6]]
>>> W.shape
(2, 3)
>>> Y = np.dot(X, W)
>>> print(Y)
[ 5 11 17]

在这里插入图片描述

3.3 三层神经网络的实现

在这里插入图片描述

X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1

Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66818777, 0.75026011]

在这里插入图片描述

W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)

在这里插入图片描述

def identity_function(x):
	return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或者Y = A3

在这里插入图片描述

输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid 函数,多元分类问题可以使用softmax 函数。

代码实现小结

import numpy as np


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


def identity_function(x):
    return x


def init_network():
    network = {}
    network['x'] = np.array([1, 2])
    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 forword():
    network = init_network()
    network['a1'] = np.dot(network['x'], network['W1']) + network['b1']
    network['z1'] = sigmoid(network['a1'])
    network['a2'] = np.dot(network['z1'], network['W2']) + network['b2']
    network['z2'] = sigmoid(network['a2'])
    network['a3'] = np.dot(network['z2'], network['W3']) + network['b3']
    network['y'] = identity_function(network['a3'])

    return network['y']


print(forword())

3.4 输出层的设计

​ 神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用softmax 函数

恒等函数

def identity_function(x):
    return x

y = x

softmax函数

在这里插入图片描述

说白了就是这个输出神经元占所有输出神经元的比例,这样就可以反映出这个结果出现的概率了

>>> a = np.array([0.3, 2.9, 4.0])
>>>
>>> exp_a = np.exp(a) # 指数函数
>>> print(exp_a)
[ 1.34985881 18.17414537 54.59815003]
>>>
>>> sum_exp_a = np.sum(exp_a) # 指数函数的和
>>> print(sum_exp_a)
74.1221542102
>>>
>>> y = exp_a / sum_exp_a
>>> print(y)
[ 0.01821127 0.24519181 0.73659691]
def softmax(a):
	exp_a = np.exp(a)
	sum_exp_a = np.sum(exp_a)
	y = exp_a / sum_exp_a
	return y

实现softmax函数时的注意事项

要注意内存溢出的问题,所以采用一下方式进行改进

在这里插入图片描述

这里的C’可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。

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

softmax函数的特征

  • softmax函数的输出是0.0 到1.0之间的实数。
  • 输出总和为1
  • 反映出结果出现的概率

输出层的神经元数量

输出层的神经元数量需要根据待解决的问题来决定。
对于分类问题,输出层的神经元数量一般设定为类别的数量。

3.5 手写数字识别

MNIST 数据集

MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。

load_mnist(normalize=True,flatten=True, one_hot_label=False) :load_mnist函数以“( 训练图像, 训练标签),( 测试图像,测试标签)”的形式返回读入的MNIST数据。

  • normalize:设置是否将输入图像正规化为0.0~1.0 的值。如果将该参数设置为False,则输入图像的像素会保持原来的0~255。
  • flatten:设置是否展开输入图像(变成一维数组)。如果将该参数设置为False,则输入图像为1 × 28 × 28 的三维数组;若设置为True,则输入图像会保存为由784 个元素构成的一维数组。
  • one_hot_label:设置是否将标签保存为one-hot表示,one-hot 表示是仅正确解标签为1,其余皆为0 的数组

神经网络的推理处理

from dataset.mnist import load_mnist
import pickle
import numpy as np


# 隐藏层激活函数
def sigmoid_function(x):
    return 1 / (1 + np.exp(-x))


# 输出层激活函数
def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y


# 1.准备一个获取数据集的函数
def get_data():
    # 通过这个函数,获得训练集和测试集
    (x_train, t_train), (x_test, t_test) = \
        load_mnist(normalize=True, flatten=True, one_hot_label=False)
    # 后面只用到测试集,所以只返回这俩(因为模型是训练好的,所以用不到训练集)
    return x_test, t_test


# 2.准备一个获取权重信息的函数
def get_weight():
    # 你应该保证sample_weight.pkl文件的路径是正确的
    # 这个文件就是实例经过pickle模块的dumps打包之后的文件(这里我们直接拿来用)
    with open("dataset/sample_weight.pkl", 'rb') as f:
        # 把这个文件重新还原成了实例
        network = pickle.load(f)
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    # 返回了各层的权重和偏置
    return W1, W2, W3, b1, b2, b3


# 3.准备一个通过数据集及权重信息进行推理的神经网络函数
# 这个过程也叫前向传播(forward propagation)
def forward_propagation(x, network):
    W1, W2, W3, b1, b2, b3 = network
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid_function(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid_function(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)
    return y


# 4.准备一个统计真实结果与预测结果相同数量的函数
def get_accuracy():
    network = get_weight()
    x, t = get_data()
    # 这个变量用于记录正确的预测数量
    accuracy_cnt = 0
    for i in range(len(x)):
        # 逐个预测结果
        y = forward_propagation(x[i], network)
        p = np.argmax(y)  # 获取概率最高的元素的索引
        # 对预测结果与真实结果对比
        if p == t[i]:
            accuracy_cnt += 1
    # 返回正确比例
    return str(float(accuracy_cnt) / len(x))


# 5.运行主函数,查看效果
def main():
    Accuracy = get_accuracy()
    print(Accuracy)  # 输出0.9352


if __name__ == '__main__':
    main()

批处理

打包式的输入数据称为批(batch)。批处理对计算机的运算大有利处,可以大幅缩短每张图像的处理时间。

from dataset.mnist import load_mnist
import pickle
import numpy as np


# 隐藏层激活函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))


# 输出层激活函数
def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


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


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


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]
    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)))

3.6 小结

  • 神经网络中的激活函数使用平滑变化的sigmoid 函数或ReLU函数。
  • 通过巧妙地使用NumPy多维数组,可以高效地实现神经网络。
  • 机器学习的问题大体上可以分为回归问题和分类问题。
  • 关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用softmax 函数。
  • 分类问题中,输出层的神经元的数量设置为要分类的类别数。
  • 输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。

4. 神经网络的学习

“学习”是指从训练数据中自动获取最优权重参数的过程。为了使神经网络能进行学习,将导入损失函数这一指标。而学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值。

4.1 从数据中学习

神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。

对于线性可分问题,第2 章的感知机是可以利用数据自动学习的。根据“感知机收敛定理”,通过有限次数的学习,线性可分问题是可解的。但是,非线性可分问题则无法通过(自动)学习来解决。

数据驱动

机器学习的方法中,由机器从收集到的数据中找出规律性。

神经网络的优点是对所有的问题都可以用同样的流程来解决。

训练数据和测试数据

机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。

为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。

泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。

因此,仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。顺便说一下,只对某个数据集过度拟合的状态称为过拟合(over fitting)。避免过拟合也是机器学习的一个重要课题。

4.2 损失函数

神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。

神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。

均方误差

可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squarederror)。

在这里插入图片描述

这里,yk 是表示神经网络的输出,tk 表示监督数据(标准答案),k 表示数据的维数。

将正确解标签表示为1,其他标签表示为0 的表示方法称为one-hot 表示。

均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。

def mean_squared_error(y, t):
	return 0.5 * np.sum((y-t)**2)

交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。

在这里插入图片描述

这里,log表示以e为底数的自然对数(log e)。yk是神经网络的输出,tk是正确解标签。

def cross_entropy_error(y, t):
	delta = 1e-7
	return -np.sum(t * np.log(y + delta))

个微小值delta的作用是保护程序防止当出现np.log(0)时出现一个无穷大导致程序崩溃

mini-batch学习

就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。

以交叉熵误差为例求单个数据的“平均损失函数”:

在这里插入图片描述

这里, 假设数据有N个,tnk 表示第n个数据的第k 个元素的值(ynk 是神经网络的输出,tnk 是监督数据)。其实只是把求单个数据的损失函数的式扩大到了N份数据

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
(x_train, t_train), (x_test, t_test) = \
	load_mnist(normalize=True, one_hot_label=True)
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000, 10)

mini-batch版交叉熵误差的实现

def cross_entropy_error(y, t):
	if y.ndim == 1:
		t = t.reshape(1, t.size)
		y = y.reshape(1, y.size)
	batch_size = y.shape[0]
	return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

为何要设定损失函数

之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数都会变为0,导致参数无法更新。

在进行神经网络的学习时,不能将识别精度作为指标。因为如果以识别精度为指标,则参数的导数在绝大多数地方都会变为0。

4.3 数值微分

导数

在这里插入图片描述

为了减小这个误差,我们可以计算函数f 在(x + h) 和(x − h) 之间的差分。因为这种计算方法以x 为中心,计算它左右两边的差分,所以也称为中心差分(而(x + h) 和x之间的差分称为前向差分)。

def numerical_diff(f, x):
	h = 1e-4 # 0.0001
	return (f(x+h) - f(x-h)) / (2*h)

偏导数

对有两个变量的函数求其中某一个变量的导数在这里插入图片描述

梯度

由全部变量的偏导数汇总而成的向量称为梯度(gradient)。

def numerical_gradient(f, x):
	h = 1e-4 # 0.0001
	grad = np.zeros_like(x) # 生成和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

梯度法

在这里插入图片描述

η 表示更新量,在神经网络的学习中,称为学习率(learningrate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。

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 指定重复的次数。

学习率过大的话,会发散成一个很大的值;学率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。

神经网络的梯度

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import 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

4.5 学习算法的实现

步骤:

  1. mini-batch
  2. 计算梯度
  3. 更新参数
  4. 重复

2 层神经网络的类

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
变量说明
params保存神经网络的参数的字典型变量(实例变量)。
params[‘W1’]是第1 层的权重,params[‘b1’]是第1 层的偏置。
params[‘W2’]是第2 层的权重,params[‘b2’]是第2 层的偏置
grads保存梯度的字典型变量(numerical_gradient()方法的返回值)。
grads[‘W1’]是第1 层权重的梯度,grads[‘b1’]是第1 层偏置的梯度。
grads[‘W2’]是第2 层权重的梯度,grads[‘b2’]是第2 层偏置的梯度
方法说明
init(self, input_size,hidden_size, output_size)进行初始化。
参数从头开始依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数
predict(self, x)进行识别(推理)。
参数x是图像数据
loss(self, x, t)计算损失函数的值。
参数x 是图像数据,t 是正确解标签(后面3 个方法的参数也一样)
accuracy(self, x, t)计算识别精度
numerical_gradient(self, x, t)计算权重参数的梯度
gradient(self, x, t)计算权重参数的梯度。
numerical_gradient()的高速版,将在下一章实现

mini-batch的实现

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
(x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_hot_
laobel = True)
train_loss_list = []
# 超参数
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
for i in range(iters_num):
    # 获取mini-batch
    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)

基于测试数据的评价

# coding: utf-8
import sys, os

sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

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.1

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))

# 绘制图形
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()

在这里插入图片描述

在上面的例子中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个epoch 的识别精度,是因为如果在for语句的循环中一直计算识别精度,会花费太多时间。并且,也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别精度。

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000 笔训练数据,用大小为100笔数据的mini-batch 进行学习时,重复随机梯度下降法100 次,所有的训练数据就都被“看过”了A。此时,100次就是一个epoch。

4.6 小结

  • 机器学习中使用的数据集分为训练数据和测试数据。
  • 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力。
  • 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。
  • 利用某个给定的微小值的差分求导数的过程,称为数值微分。
  • 利用数值微分,可以计算权重参数的梯度。
  • 数值微分虽然费时间,但是实现起来很简单。下一章中要实现的稍微复杂一些的误差反向传播法可以高速地计算梯度。

5. 误差反向传播法

5.1 计算图

  • 既可以正向传播也可以方向传播
  • 局部计算的特性

5.2 反向传播

反向传播时一个求导的过程,输出结果是这个变量对于最终结果的影响力

以加和乘为例:

z = x + y

在这里插入图片描述

所以加相当于将上游的值原封不动地输出到下游

在这里插入图片描述

z = xy

在这里插入图片描述

所以乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游

在这里插入图片描述

5.3 简单层的实现

乘法层的实现

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 # 翻转x和y
        dy = dout * self.x
        return dx, dy

加法层的实现

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

5.4 激活函数层的实现

ReLU层

在这里插入图片描述
在这里插入图片描述

如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传给下游。反过来,如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停在此处。

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 层的作用就像电路中的开关一样。正向传播时,有电流通过的话,就将开关设为ON;没有电流通过的话,就将开关设为OFF。反向传播时,开关为ON 的话,电流会直接通过;开关为OFF 的话,则不会有电流通过。

Sigmoid 层

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

最终化简得:

在这里插入图片描述

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

5.5 Affine/Softmax 层的实现

Affine 层

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

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

Softmax-with-Loss 层

softmax 函数会将输入值正规化之后再输出。

在这里插入图片描述

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.6 误差反向传播法的实现

神经网络学习的步骤

  1. mini-batch:从训练数据中随机选择一部分数据。
  2. 计算梯度):计算损失函数关于各个权重参数的梯度。
  3. 更新参数:将权重参数沿梯度方向进行微小的更新。
  4. 重复:重复步骤1、步骤2、步骤3。

误差反向传播法会在步骤2 中出现,误差反向传播法可以快速高效地计算梯度。

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

实例变量说明
params保存神经网络的参数的字典型变量。
params[‘W1’]是第1 层的权重,params[‘b1’]是第1 层的偏置。
params[‘W2’]是第2 层的权重,params[‘b2’]是第2 层的偏置
layers保存神经网络的层的有序字典型变量。
以layers[‘Affine1’]、layers[‘ReLu1’]、layers[‘Affine2’]的形式,
通过有序字典保存各个层
lastLayer神经网络的最后一层。
本例中为SoftmaxWithLoss层
方法说明
init(self, input_size,hidden_size, output_size,weight_init_std)进行初始化。
参数从头开始依次是输入层的神经元数、隐藏层的神经元数、输出层的神经元数、初始化权重时的高斯分布的规模
predict(self, x)进行识别(推理)。
参数x是图像数据
loss(self, x, t)计算损失函数的值。
参数X是图像数据、t是正确解标签
accuracy(self, x, t)计算识别精度
numerical_gradient(self, x, t)通过数值微分计算关于权重参数的梯度
gradient(self, x, t)通过误差反向传播法计算关于权重参数的梯度
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
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
			
        # 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):
            # 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    

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

数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差反向传播法的实现很复杂,容易出错。所以,经常会比较数值微分的结果和误差反向传播法的结果,以确认误差反向传播法的实现是否正确。确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是非常相近)的操作称为梯度确认(gradient check)

import sys, os

sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_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]
t_batch = t_train[:3]

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

for key in grad_numerical.keys():
    diff = np.average(np.abs(grad_backprop[key] - grad_numerical[key]))
    print(key + ":" + str(diff))

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

import sys, os

sys.path.append(os.pardir)

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

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

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.1

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)

5.7 小结

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

6. 与学习相关的技巧

6.1 参数的更新

神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)

SGD

为了找到最优参数,我们将参数的梯度(导数)作为了线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),简称SGD

SGD 的缺点

SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。

例如:

在这里插入图片描述

这个函数图像是一个碗,SGD会呈“之”字形移动。

在这里插入图片描述

Momentum

Momentum是“动量”的意思,和物理有关。

在这里插入图片描述

在这里插入图片描述

类似小球在一个弧面上运动速度越快摩擦力越大,减数越快,相较于SGD的之字形能更快到达最低点

在这里插入图片描述

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]

AdaGrad

在关于学习率的有效技巧中,有一种被称为学习率衰减(learning ratedecay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。

在这里插入图片描述

在这里插入图片描述

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)

Adam

Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。Adam是将这两个方法融合在一起。

在这里插入图片描述

6.2 权重的初始值

通过减小权重参数的值来抑制过拟合的发生。如果想减小权重的值,一开始就将初始值设为较小的值才是正途。权重初始值都是像0.01 * np.random.randn(10, 100)这样,但又不能设置成0,而且不能相同。因为如果相同那么在误差反向传播法中,所有的权重值都会进行相同的更新。权重被更新为相同的值,并拥有了对称的值(重复的值)。这使得神经网络拥有许多不同的权重的意义丧失了。

6.3 Batch Normalization

  • 可以使学习快速进行(可以增大学习率)。
  • 不那么依赖初始值(对于初始值不用那么神经质)。
  • 抑制过拟合(降低Dropout等的必要性)。

6.4 正则化

过拟合

原因:

  • 模型拥有大量参数、表现力强。
  • 训练数据少。

权值衰减

权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是因为权重参数取值过大才发生的。

Dropout

权值衰减在模型负责的情况下难以解决过拟合的情况,Dropout是作为抑制过拟合辅助方法。

Dropout理解为,通过在学习过程中随机删除神经元,从而每一次都让不同的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比例(比如,0.5 等),可以取得模型的平均值。也就是说,可以理解成,Dropout将集成学习的效果(模拟地)通过一个网络实现了。

6.5 超参数的验证

神经网络中,除了权重和偏置等参数,超参数(hyper-parameter)也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch 大小、参数更新时的学习率或权值衰减等。

验证数据

不能使用测试数据评估超参数的性能,因为如果使用测试数据调整超参数,超参数的值会对测试数据发生过拟合。因此,调整超参数时,必须使用超参数专用的确认数据。用于调整超参数的数据,一般称为验证数据(validation data)。

根据不同的数据集,有的会事先分成训练数据、验证数据、测试数据三部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。

超参数的最优化

超参数的范围只要“大致地指定”就可以了。

步骤:

  1. 设定超参数的范围。
  2. 从设定的超参数范围中随机采样。
  3. 使用步骤1 中采样到的超参数的值进行学习,通过验证数据评估识别精度(但是要将epoch 设置得很小)。
  4. 重复步骤2 和步骤3(100 次等),根据它们的识别精度的结果,缩小超参数的范围。

6.6 小结

  • 参数的更新方法,除了SGD 之外,还有Momentum、AdaGrad、Adam等方法。
  • 权重初始值的赋值方法对进行正确的学习非常重要。
  • 作为权重初始值,Xavier 初始值、He初始值等比较有效。
  • 通过使用Batch Normalization,可以加速学习,并且对初始值变得健壮。
  • 抑制过拟合的正则化技术有权值衰减、Dropout等。
  • 逐渐缩小“好值”存在的范围是搜索超参数的一个有效方法。

7. 卷积神经网络

7.1 整体结构

在这里插入图片描述

在这里插入图片描述

可以理解为之前的“Affi ne - ReLU”连接被替换成了“Convolution -ReLU -(Pooling)”连接。

此外,靠近输出的层中使用了之前的“Affi ne - ReLU”组合,最后的输出层中使用了之前的“Affine -Softmax”组合。

7.2 卷积层

全连接层存在的问题

数据的形状被“忽视”了。比如,输入数据是图像时,图像通常是高、长、通道方向上的3 维形状。但是,向全连接层输入时,需要将3 维数据拉平为1 维数据。实际上,前面提到的使用了MNIST数据集的例子中,输入图像就是1 通道、高28 像素、长28 像素的(1, 28, 28)形状,但却被排成1 列,以784 个数据的形式输入到最开始的Affine层。

因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。

CNN中,有时将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)。

卷积运算

卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波器运算”。

乘积累加运算

在这里插入图片描述

填充

在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如0 等),这称为填充(padding)

在这里插入图片描述

使用填充主要是为了调整输出的大小。比如,对大小为(4, 4) 的输入数据应用(3, 3) 的滤波器时,输出大小变为(2, 2),相当于输出大小比输入大小缩小了2 个元素。这在反复进行多次卷积运算的深度网络中会成为问题.

步幅

应用滤波器的位置间隔称为步幅(stride)。

在这里插入图片描述

3维数据的卷积运算

通道数只能设定为和输入数据的通道数相同的值

在这里插入图片描述

7.3 池化层

池化是缩小高、长方向上的空间的运算。比如,如图7-14 所示,进行将2 × 2 的区域集约成1 个元素的处理,缩小空间大小。

除了Max 池化之外,还有Average 池化等。相对于Max 池化是从目标区域中取出最大值,Average 池化则是计算目标区域的平均值。在图像识别领域,主要使用Max 池化。

池化层的特征

  • 没有要学习的参数
    池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
  • 通道数不发生变化
    经过池化运算,输入数据和输出数据的通道数不会发生变化。如图7-15所示,计算是按通道独立进行的。
  • 对微小的位置变化具有鲁棒性(健壮)
    输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。

7.4 卷积层和池化层的实现

基于im2col 的展开

im2col是一个函数,将输入数据展开以适合滤波器(权重),空间换时间

im2col (input_data, filter_h, filter_w, stride=1, pad=0)

  • input_data―由(数据量,通道,高,长)的4维数组构成的输入数据
  • filter_h―滤波器的高
  • filter_w―滤波器的长
  • stride―步幅
  • pad―填充

卷积层的实现

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
        out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
        col = im2col(x, FH, FW, self.stride, self.pad)
        col_W = self.W.reshape(FN, -1).T # 滤波器的展开
        out = np.dot(col, col_W) + self.b
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
        return out

池化层的实现

  1. 展开输入数据。
  2. 求各行的最大值。
  3. 转换为合适的输出大小。
class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        self.pool_h = pool_h
        self.pool_w = pool_w
        self.stride = stride
        self.pad = pad
        
    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)
        # 展开(1)
        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)
        # 最大值(2)
        out = np.max(col, axis=1)
        # 转换(3)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
        return out

7.6 CNN的可视化

第1层权重的可视化

在这里插入图片描述

学习前的滤波器是随机进行初始化的,所以在黑白的浓淡上没有规律可循,但学习后的滤波器变成了有规律的图像。我们发现,通过学习,滤波器被更新成了有规律的滤波器,比如从白到黑渐变的滤波器、含有块状区域(称为blob)的滤波器等。

由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现的CNN会将这些原始信息传递给后面的层。

基于分层结构的信息提取

随着层次加深,提取的信息(正确地讲,是反映强烈的神经元)也越来越抽象

7.7 具有代表性的CNN

LeNet

在这里插入图片描述

LeNet 中使用sigmoid 函数,而现在的CNN中主要使用ReLU函数。
LeNet 中使用子采样(subsampling)缩小中间数据的大小,而现在的CNN中Max池化是主流。

AlexNet

在这里插入图片描述

  • 激活函数使用ReLU。
  • 使用进行局部正规化的LRN(Local Response Normalization)层。
  • 使用Dropout。

7.8 小结

  • CNN在此前的全连接层的网络中新增了卷积层和池化层。
  • 使用im2col函数可以简单、高效地实现卷积层和池化层。
  • 通过CNN的可视化,可知随着层次变深,提取的信息愈加高级。
  • LeNet和AlexNet是CNN的代表性网络。
  • 在深度学习的发展中,大数据和GPU做出了很大的贡献。

8. 深度学习

8.1 加深网络

在这里插入图片描述

  • 基于3×3 的小型滤波器的卷积层。
  • 激活函数是ReLU。
  • 全连接层的后面使用Dropout层。
  • 基于Adam的最优化。
  • 使用He初始值作为权重初始值。

8.2 著名的网络

VGG

在这里插入图片描述

GoogLeNet

在这里插入图片描述

在这里插入图片描述

ResNet

在这里插入图片描述

在这里插入图片描述

8.3 深度学习的高速化

  • 基于GPU 的高速化
  • 分布式学习
  • 运算精度的位数缩减

8.4 小结

  • 对于大多数的问题,都可以期待通过加深网络来提高性能。
  • 在最近的图像识别大赛ILSVRC中,基于深度学习的方法独占鳌头,使用的网络也在深化。
  • VGG、GoogLeNet、ResNet等是几个著名的网络。
  • 基于GPU、分布式学习、位数精度的缩减,可以实现深度学习的高速化。
  • 深度学习(神经网络)不仅可以用于物体识别,还可以用于物体检测、图像分割。
  • 深度学习的应用包括图像标题的生成、图像的生成、强化学习等。最近,深度学习在自动驾驶上的应用也备受期待。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值