深度学习入门(鱼书)笔记(持续更新)
深度学习入门笔记
python基础知识
numpy库
import numpy as np
-
numpy数组(numpy.ndarray):
np.array(list)
-
np数组算术运算需元素个数相同,否则报错。
-
np数组间的算术运算为
element-wise
,即对应元素的运算。 -
np数组与单一数值(标量)的运算为广播,即标量自动补全与数组各元素计算。
-
numpy可以生成多维数组,
.shape
可获得数组形状,.dtype
可获得元素的数据类型。 -
一维数组为向量,二维数组为矩阵,三维及以上的数组为张量(多维数组)。
-
广播可以将不同形状的数组进行运算,可将小数组自动拓展后进行运算。
>>> A = np.array([1, 2], [3, 4]) >>> B = np.array([10, 20]) >>> A * B ''' 输出: array([[ 10, 40], [ 30, 80]]) '''
广播条件:从右到左比较维度,维度相等或其中一个为1
举例:(100,10)(100,) 不可以计算,因为一个最右边维度是10,而另一个是100 -
numpy访问元素方式同普通二维甚至多维列表,也可用通过迭代访问。
>>> X = np.array([51, 55], [14, 19], [0, 4]) >>> x[0] # 输出:array([51, 55]) >>> x[0][1] # 输出:55
-
numpy库特有
.flatten()
可将np数组转换为一维数组。 -
numpy可使用数组访问各个元素,其中作为索引的数组既可以是numpy数组也可以是普通列表。
>>> X = X.flatten() >>> X[np.array(0, 2, 4)] # 输出:array([51, 14, 0])
-
对numpy数组使用不等号运算符,结果会获得一个布尔型数组。
>>> X > 15 # 输出:array([ True, True, False, True, False, False], dtype=bool) >>> X[X > 15] # 输出:array([51, 55, 19])
-
在python中,运算量大的处理对象一般使用C或C++实现,python知只是负责调用,numpy库就是一个很好的例子。
matplotlib库
import matplotlib.pyplot as plt
import matplotlib.image import imread
-
基础演示:
-
基础演示1
import numpy as np import matplotlib.pyplot as plt x = np.arange(0, 6, 0.1) # x轴点 y1 = np.sin(x) # y轴y1图像点 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") plt.title("sin & cos") # 标题 plt.legend() # 显示图例 plt.show() # 显示内容
运行结果:
-
基础演示2
import matplotlib.pyplot as plt from matplotlib.image import imread img = imread('lean.jpeg') # 读入图像,imread(路径) plt.imshow(img) # 描绘内容,但不显示 plt.show() # 显示内容
运行结果:
-
-
imshow
是一个用于在Matplotlib
的坐标轴上显示图像的函数。它主要用于显示二维数组,如灰度图像或彩色图像。这个函数会将二维数组中的数据映射到颜色上,以可视化数据。 -
show
函数则是用于显示整个图形或图像窗口。
感知机
基础概念
-
感知机接受多个信号输出一个信号。
-
通过判断输入
多个信号*各自权重ω
之和 是否大于阈值θ
来判断是否激活神经元或输出信号1。 -
感知机权重类电流中的电阻,感知机权重越大,通过的信号越大。
简单逻辑电路
-
AND门(与门)
输入A 输入B 输出 0 0 0 0 1 0 1 0 0 1 1 1 -
OR门(或门)
输入A 输入B 输出 0 0 0 0 1 1 1 0 1 1 1 1 -
NOT门(非门)
输入 输出 0 1 1 0 -
Not AND门(与非门)
输入A 输入B 输出 0 0 1 0 1 1 1 0 1 1 1 0 -
感知机表示门,
(ω1,ω2,θ)
的值即类似于逻辑门中的输入权重与阈值,而输入信号则类似于逻辑门中的输入值。 -
感知机表示门,把与门的参数值符号
(ω1,ω2,θ)
取反即可实现与非门(-ω1,-ω2,-θ)
。 -
机器学习是将由人决定参数的工作交由计算机自动进行,学习是确定合适参数的过程,人做的则是思考感知机的构造。
-
实现不同门的感知机构造相同,参数不同,所以只需适当调整参数就可以实现不同的门。
感知机的实现
-
AND实现
def AND(x1, x2): w1, w2, theta = 0.5, 0.5, 0.7 tmp = w1*x1 + w2*x2 if tmp <= theta: return 0 elif tmp > theta: return 1 print(AND(1, 1)) # 输出:1
-
将阈值转换为偏置(类似于函数中的b),即可简化比较,只需与0比较。
-
输入信号和权重的乘积加上偏置与0比较,若大于0则输出1,否则输出0。
-
感知机偏置代替阈值的实现,把
-θ
看作b。def AND(x1, x2): x = np.array([x1, x2]) w = np.array([0.5, 0.5]) b = -0.7 tmp = np.sum(x*w)+b if tmp >= 0: return 1 else: return 0
-
偏置的权重的作用不同。权重控制输入信号,偏置决定神经元被激活的容易程度。
-
与非门和或门。
- 与非门
def NAND(x1, x2): x = np.array([x1, x2]) w = np.array([0.5, 0.5]) b = 0.7 # 仅权重和偏置与AND不同 tmp = np.sum(x*w)+b if tmp >= 0: return 1 else: return 0
- 或门
def OR(x1, x2): x = np.array([x1, x2]) w = np.array([0.5, 0.5]) b = -0.2 # 仅权重和偏置与AND不同 tmp = np.sum(x*w)+b if tmp >= 0: return 1 else: return 0
感知机的的局限
-
异或门(XOR) 或称 逻辑异或 无法用感知机实现。
输入A 输入B 输出 0 0 0 0 1 1 1 0 1 1 1 0 -
异或是拒绝其他的意思。
-
与门、或门、与非门都是线性空间可实现的,而感知机的作用是表示由一条直线分割的空间(线性空间),所以可以用感知机实现。
-
感知机的函数是线性函数,只可实现线性空间,而异或门是非线性空间才能实现的。
-
异或门可以用多层感知机实现。
多层感知机
-
异或门实现图与真值表。
-
异或门实现图:
-
真值表:
x1 x2 s1 s2 y 0 0 1 0 0 0 1 1 1 1 1 0 1 1 1 1 1 0 1 0 -
-
异或门实现。
def XOR(x1, x2): s1 = NAND(x1, x2) s2 = OR(x1, x2) y = AND(s1, s2) return y
-
异或门是一种多层结构的神经网络。
-
用感知机表示异或门。
-
图中感知机由三层组成,但只有两层权重,所以称为二层感知机,但有些文献称为三层感知机。
-
-
与门、或门是单层感知机,异或门是2层感知机,叠加了多层的感知机叫多层感知机。
-
0层的神经元接收输入信号发送给1层神经元,1层神经元再将信号发送给二层神经元,二层神经元输出y。
-
通过叠加层,感知机能更加灵活。
-
多层感知机可以实现复杂线路,加法器也可以用多层感知机实现,感知机甚至可以表示计算机。
-
二层感知机(激活函数使用了非线性的sigmoid函数感知机)可以表示任意函数。
-
感知机将权重和偏置设定为参数。
神经网络
之所以神经网络在经过许多看似夸张的处理以后得到的结果是我们所需的,其根本原因是在神经网络中,输出结果是输入结果的一种映射,在神经网络的全部过程中实际改进的是映射的准确度与稳定性,其更本原因是所有的输入值都接受了相同函数的处理,从而达到输出结果是输入结果的映射的要求。
感知机与神经网络
-
神经网络有三层,从左到右依次是输入层、中间层(亦称隐藏层)、输出层。
-
示意图
-
图中虽然由三层神经元构成,但实质只有两层神经元有权重,所以称作两层网络。有的书按层数算,称作三层网络。
-
-
可将偏置b看作输入信号恒为1且权重为b的神经元。
-
将输入信号的总和转换为输出信号的函数称为激活函数。
例如:
\(h\left( x \right)= \left\{ \begin{array}{} 0 & \left(x \leq 0\right)\\ 1 & \left(x > 0\right) \\ \end{array} \right.\)在输入超过0时返回1,否则返回0。
-
信号加权总节点(神经元)与被激活函数转换为节点(神经元)y的整体是一个神经元。
-
激活函数是连接感知机和神经网络的桥梁。
-
“朴素感知机”是指单层网络,是指激活函数使用了阶跃函数的模型,“多层感知机”是指神经网络,是激活函数使用了sigmoid函数等平滑函数的多层网络。
激活函数
-
激活函数的作用:
-
控制信息传递、选择性地传递信息,激活函数决定了神经元是否应该被激活,即该神经元的信息是否应该被传递到下一层,从而学习数据的内在表示。
-
通过引入非线性激活函数,使神经网络可以解决非线性问题。
-
阶跃函数与传统常用激活函数:sigmoid函数
-
神经网络常用激活函数:sigmoid函数。
\(h\left(x\right)= \cfrac{1} {1+\exp(-x)}\)
exp(-x) 即代表 \(e^{-x}\).
-
阶跃函数的实现。
# 基础实现 def step_function(x): if x > 0: return 1 else: return 0 # 实现对nupmy的支持 def step_function(x): y = x > 0 return y.astype(np.int64) ''' y = x > 0 实现先把np数组x与0比较使得x的数组元素变为bool类型,然后将其赋给y y.astype(np.int) 实现把bool型数组转化为int型 ''' # astype()方法通过参数指定转化期望类型。
-
制作阶跃函数图形。
-
阶跃函数
def step_function(x): return np.array(x > 0, dtype=np.int64)
-
制图代码段:
import numpy as np import matplotlib.pylab as plt def step_function(x): return np.array(x > 0, dtype=np.int64) # np.int不推荐使用(新版本numpy库使用会报错),在新版本numpy库中,应用int64或int32,因为int类型模糊 x = np.arange(-5.0, 5.0, 0.1) # 生成规律元素numpy数组 y = step_function(x) plt.plot(x, y) plt.ylim(-0.1, 1.1) # 限制或者制定y轴显示范围 plt.show()
-
图形输出
-
-
sigmoid函数实现与制图。
-
sigmoid函数
def sigmoid(x): return 1 / (1 + np.exp(-x))
-
制图代码段
import numpy as np import matplotlib.pylab as plt 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) plt.show()
-
图形输出
-
-
在阶跃函数和sigmoid函数的实现中,这两个函数都可以接受np数组作为参数,其原因是np的广播功能,而函数的计算中都是标量。
-
阶跃函数和sigmoid函数的共同点是重要信息输出较大值,不重要信息输出较小值且都在0和1之间。差异点是阶跃函数是0和1二元信号,是间断的,而sigmoid函数是连续的实数值信号。
-
阶跃函数和sigmoid函数都属于非线性函数,且激活函数必须是非线性函数。
-
只有激活函数是非线性函数才能发挥叠加层的优势。而使用线性函数加深神经网络就没有意义了。
新秀常用激活函数:ReLU函数
-
神经网络常用激活函数新秀:ReLU函数
\(h\left(x\right)=\left \{ \begin{array}{}x & \left(x > 0\right)\\0 & \left(x \leq 0\right)\\\end{array}\right.\)
-
ReLU函数的实现与制图。
-
ReLU函数
def relu(x): return np.maximun(0, x) # maximum()函数会从输入的数值里选择较大的值输出。
-
制图代码段
import numpy as np import matplotlib.pylab as plt def relu(x): return np.maximun(0, x) x = np.arange(-5.0, 5.0, 0.1) y = sigmoid(x) plt.plot(x, y) plt.ylim(-0.1, 1.1) plt.show()
-
图形输出
-
原点对称的tanh函数
-
tanh函数数学式。
\(tanh = \dfrac{e^x-e^{-x}}{e^x+e^{-x}}\)
-
tanh函数的实现与制图。
-
tanh函数
def tanh(x): return np.tanh(x)
-
制图代码段
import numpy as np import matplotlib.pylab as plt def tanh(x): return np.tanh(x) x = np.arange(-5.0, 5.0, 0.1) y = tanh(x) plt.plot(x, y) plt.ylim(-1.1, 1.1) plt.show()
-
图形输出
-
多维数组
多维数组和矩阵乘法
-
np.ndim()
函数可以获得数组的维数,返回类型是int。 -
np.shape()
函数返回的结果是tuple,所以一维数组返回的结果是(n,)
。 -
二维数组也称为矩阵,横行竖列。
-
矩阵乘法是通过左边的矩阵的行和右边矩阵的列以对应元素的方式相乘后求和得到新矩阵的相应列。
-
.dot
为矩阵的点积(乘积、内积)运算方法。 -
矩阵的点积运算操作数的顺序不同,结果也不同。
-
矩阵乘法矩阵A的第1维的元素个数(列数)必须和矩阵B的第0维(行数)的元素个数相等。
-
两个矩阵 A 和 B,其中 A 是一个 m×n 矩阵(即有 m 行和 n 列),B 是一个 p×q 矩阵(即有 p 行和 q 列),那么 A 和 B 可以相乘的条件是 A 的列数n 必须等于 B 的行数p 。其生成矩阵C是一个 m×q 矩阵。
-
矩阵C的行数由A的行数决定,列数由矩阵B的列数决定。
-
当矩阵B为一维数组时,要求原则依然成立。
矩阵乘法和广播计算不同。
神经网络的内积
-
神经网络内积必须要确定x与\(\omega\)的维度元素个数的一致,符合矩阵点积的原则。
-
简单神经网络的示例。
-
代码。
>>> x = np.array([1, 2]) >>> w = np.array([[1, 3, 5],[2, 4, 6]]) >>> Y = np.dot(x, w) >>> print(Y) # 输出:[ 5 11 17]
-
神经网络图示例。
-
-
使用np.dot可以一次性计算出Y的结果,简便了计算。
三层神经网络的实现
-
符号确认:
-
权重:通常表示为 \(w_{ij}^ l\),其中 l 表示第 l 层,i 表示后一层神经元中的第 i 个神经元,j 表示前一层神经元中的第 j 个神经元。
-
偏置:通常表示为 \(b_i^l\),其中 l 表示第 l 层,i 表示第 l 层中的第 i 个偏置神经元。
-
-
各层级的信号传递图像表达式和实现代码示例。
-
图像
-
表达式:
\(a_1^1\) = \(w_{11}^1 x_1 + w_{12}^ 1 x_2 + b_1^1\)
-
矩阵乘法表示:
\(A^1 = XW^1 + B^1\)
其中\(A^1 = \bigl( \begin{matrix}a_1^1 & b_2^1 & c_3^1 \end{matrix} \bigr)\),
\(X = \bigl( \begin{matrix} x_1 & x_2\end{matrix} \bigr)\),
\(B^1 = \bigl( \begin{matrix} b_1^1 & b_2^1 & b_3^1\end{matrix} \bigr)\),
\(W^1=\bigl( \begin{matrix}w_{11}^1&w_{21}^1&w_{31}^1\\w_{12}^1&w_{22}^1&w_{32}^1\end{matrix} \bigr)\)
-
实现代码
'''第一层''' 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]) A1 = np.dot(X, W1) + B1 Z1 = sigmoid(A1)
'''第二层''' W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) B2 = np.array([0.1, 0.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)
-
-
输出层的激活函数用\(\sigma\left(x\right)\)表示,而隐藏层激活函数用\(h\left(x\right)\)。
-
输出层用的函数根据求解问题性质决定。
-
一般地,回归问题可以使用恒等函数,二元分类问题可以使用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['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 = identity_function(a3) return y network = init_network() x = np.array([1.0, 0.5]) y = forward(network, x) print(y)
输出层的设计
softmax函数
-
机器学习的问题大致分为分类问题和回归问题,回归是预测。
-
softmax函数:
\(y_k = \cfrac{\exp\left(a_k\right)}{\sum_{k=1}^n\exp\left(a_i\right)}\)
式子表示假设输出层共有n个神经元,计算第k个神经元的输出。
-
输出层的各个神经元都受到所有输入信号的影响。
-
softmax函数的实现。
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函数(可自己推理)。
\(y_k = \cfrac{\exp\left(a_k + C\right)}{\sum^n_{i=1}\exp\left(a_i + C\right)}\)
-
改进后的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 # np.max方法针对np数组
-
改进后的softmax函数可以避免出现溢出的问题。
-
softmax函数的输出可以解释为“概率”。
-
softmax函数因为和输出层神经元数量有关,其所有输出的概率和为1(重要特征),而sigmoid函数输出相互独立,不保证和为1,所以作为概率分布的softmax函数更具优势。
-
神经网络输出层运用softmax函数的目的是将网络的输出转换为概率分布的形式,因为softmax函数是一个单调递增函数,所以输入值越大,输出值越大。
-
softmax函数输出值大小和输入值大小以及输入数组的总大小有关。所以softmax函数图像并不是固定的,而是和所有输入值有关。
-
因为神经网络只把输出值最大的神经元所对应的类别作为识别结果,并且通过softmax函数的计算后,输出值最大的神经元位置不变,所以一般输出层的softmax函数会省略。
-
求解机器学习问题的步骤分为“学习”和“推理”两个阶段。
-
对于分类问题,输出层神经元数量一般设定为类别的数量。
-
如果输出层神经元数量不符合策略,那么可能会有冗余性和复杂性、过拟合风险、精确度下降。
补充:
输入层和隐藏层用sigmoid函数,输出层用softmax函数是各取所需
手写数字识别
-
实现神经网络的推理处理为向前传播。
-
把数据限定到某个范围内的处理称为正规化(例如把位图各个像素值除以255,使其处于0~1之间),对神经网络的输入数据进行某种既定的转换称为预处理。
-
数据白化,将数据整体的分布形状均匀化。
-
打包式的输入数据称为批,批处理比分布更快。
-
源代码:
import sys, os sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定 import numpy as np import pickle # 导入pickle模块,将程序运行中的对象保存为文件,并且可以加载复原。 from dataset.mnist import load_mnist def sigmoid(x): # sigmoid激活函数 return 1 / (1 + np.exp(-x)) def softmax(a): # softmax激活函数 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 get_data(): # 加载mnist包数据 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False) # normalize 正规化,flatten 一维化, one_hot_label 对1错0化 return x_test, t_test def init_network(): # 加载network已经训练好的参数集 with open("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] y_batch = predict(network, x_batch) p = np.argmax(y_batch, axis=1) # 获取概率最高的元素的索引,axis则是沿着第一维(水平)方向找到最大的元素索引值。 accuracy_cnt += np.sum(p == t[i:i+batch_size]) # 得到的sum结果为()内True的数量。 print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
神经网络的学习
神经网络的学习的目的是减小损失函数的值。
从数据中学习
-
学习是指从训练数据中自动获取最优权重参数的过程。
-
感知机可以自动学习从而解决线性可分问题,但无法解决非线性可分问题。
-
数据是机器学习的核心,数据驱动。
-
特征量是指可以从输入数据(输入图像)中准确地提取本质数据的转换器。
-
图像的特征量通常表示为向量的形式。
-
要想高效地解决问题,必须寻找到合适的特征量(专门设计的特征量),例如CV中的SIFT、SURF、HOG等。
-
深度学习也称为端到端机器学习,即从原始数据(输入)中获得目标结果(输出)的意思。
-
传统机器学习更偏向于人工设定特征量,而神经网络、深度学习更偏向于机器从数据中学习特征量。
-
机器学习数据一般分为训练数据(也称为监督数据)和测试数据,训练数据用作学习,测试数据用于评价泛化能力。
-
泛化能力是指处理未被观察过的数据的能力,获得泛化能力是机器学习的最终目标。
-
只对某个数据集过度拟合的状态称为过拟合。
损失函数
-
损失函数是衡量模型预测结果与真实结果之间的差距的指标(或称表示神经网络性能的恶劣程度的指标)(即误差),一般以这个指标为线索寻找最优权重参数。
-
损失函数可以用任意函数,一般用均方误差和交叉熵误差等。
均方误差
-
均方误差:
\(E = \sum_{k}\left(y_k - t_k\right)^2\)
(书上的式子是变式,即\(\frac{1}{2}E\))\(y_k\)表示神经网络输出,\(t_k\)表示监督数据,\(k\)表示数据的维度。
-
one-hot表示法:正确解标签表示为1,其他表示为0。
-
均方误差的实现,均方误差越小结果越吻合。
def mean_squared_error(y, t): return np.sum((y-t)**2)
-
均方误差举例:
>>> t = [0,0,1,0,0,0,0,0,0,0] >>> y = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0] >>> mean_squared_error(np.array(y), np.array(t)) 0.0975000000000000031 >>> y = [0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0] >>> mean_squared_error(np.array(y), np.array(t)) 0.5975000000000000003
交叉熵误差
-
交叉熵误差:
\(E = - \sum_{k}t_k \log{y_k}\)\(\log\) 表示 \(\ln\),\(y_k\)表示神经网络输出(预测概率),\(t_k\)表示正确解标签(one-hot表示)。
-
交叉熵误差越接近0,模型越精确(由\(y = \log{x}\)图像可得)。
-
交叉熵误差的实现:
def cross_entropy(y, t): delta = 1e-7 return -np.sum(t * np.log(y + delta)) # delta是个微小值,避免np.log(0)时出现负无限大-inf导致无法计算
-
交叉熵误差举例:
>>> t = [0,0,1,0,0,0,0,0,0,0] >>> y = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0] >>> cross_entropy(np.array(y), np.array(t)) 0.51082545709933802 >>> y = [0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0] >>> cross_entropy(np.array(y), np.array(t)) 2.3025840929945458
-
交叉熵误差的优势:
-
与概率分布相匹配:交叉熵误差是衡量概率分布之间差异的理想选择。交叉熵误差能够直接衡量预测概率与真实概率分布之间的差异。
-
数值稳定性:交叉熵误差在数值上相对稳定,特别是在处理极端概率值时,因为它在概率值接近0或1时仍然能够产生有意义的梯度。
-
优化效率高:在优化过程中,交叉熵误差通常能够更快地收敛到最小值。因为它的梯度在大多数情况下都是非零的,并且与预测概率和真实标签之间的差异成正比。这使得优化算法能够更有效地调整模型参数,以减小预测误差。
-
对多分类问题适用:通过结合softmax函数,可以将模型的原始输出转换为概率分布,并计算与真实标签之间的交叉熵误差。这使得交叉熵误差成为处理多类别分类问题的理想选择。
-
mini-batch(小批量)
-
计算损失函数时必须将所有的训练数据作为对象。
-
平均交叉熵误差:
\(E = - \frac{1}{N}\sum_{n}\sum_{k}t_{nk} \log{y_{nk}}\) -
当数据量过大时,以全部数据计算损失函数不现实,所以从全部数据中选出一批数据(mini-batch)近似,然后对每个mini-batch进行学习。
-
选取mini-batch。
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False) x_train.shape # (60000, 784)训练数据 t_train.shape # (60000, 10)对应数据正确解标签 train_size = x_train.shape[0] # 训练数据量 batch_size = 10 # mini-batch_size batch_mask = np.random.choice(train_size, batch_size) # 从指定数字中随机选择batch_size个数字 x_batch = x_train[batch_mask] t_batch = t_train[batch_mask]
-
mini-batch版交叉熵误差(监督数据是标签形式,非one-hot表示)
-
监督数据是one-hot版
def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) # reshape成只有一行的二维数组 y = y.reshape(1, y.size) batch_size = y.shape[0] # 获得批量大小 return -np.sum(t * np.log(y + 1e-7)) / batch_size
-
监督数据是标签形式,非one-hot版
def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) # reshape成只有一行的二维数组 y = y.reshape(1, y.size) batch_size = y.shape[0] return -np.sum(np.log(y[np.arrange(batch_size), t] + 1e-7)) / batch_size # 其中np.arrange(batch_size)得到的是一共从0到batch_size-1的数组,而t则是正确解标签数组 # 最终得到的则是神经元输出结果(正确解标签对应神经元输出结果),所以不需要*t
-
兼容两个形式版(最终版)
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) # argmax返回最大值索引 batch_size = y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size # argmax函数返回数组最大元素索引,axis参数决定寻找沿哪个轴,=0时沿着行对列求,=1是沿着列对行求
-
-
\(E = -∑t_k log(y_k)\)
其中 \(t_k\) 是真实标签的 one-hot 编码,\(y_k\) 是模型预测的概率,\(t_k\) 的作用是作为掩码(mask),确保只有真实标签对应的预测概率被考虑在内。
在神经网络的实践中,t 通常不是一个 one-hot 编码的数组,而是一个包含每个样本真实类别索引的数组。因此,我们不需要显式地将 t 与对数概率相乘,因为高级索引已经为我们完成了这个工作。通过 y[np.arange(batch_size), t],我们直接选取了每个样本真实标签对应的预测概率。
-
在进行神经网络的学习时,不能将识别精度作为指标,如果以识别精度作为指标,则参数的导数在绝大多数地方都会变为0(即连续性与离散性,识别精度是离散的,微变化无法改善识别精度,从而导致导数为0,类似阶跃函数)。
数值微分
-
为避免计算机舍入误差,微小值一般设定在\(10^{-4}\)。
-
为减小真导数与数值微分(利用数值方法近似求导)之间的误差,一般计算中心差分,即\(\left(x+h\right) - \left(x-h\right)\)(取代向前差分,即\(\left(x+h\right) - \left(x\right)\))。
-
微小差分求导叫做数值微分。
-
数值微分代码示例:
def numerical_diff(f, x): h = 1e-4 # 0.0001 return (f(x+h) - f(x-h)) / (2*h)
-
有多个变量的函数称为偏导数。
-
偏导数举例。
\(f\left(x_0,x_1\right) = x_0^2 +x_1^2\) -
偏导数需要将多个变量中的一个变量定为目标变量,并将其他变量固定为某个值。
梯度
-
由全部变量汇总而成的向量称为梯度,例如上个偏导数(\(\frac{\partial{f}}{\partial{x_0}},\frac{\partial{f}}{\partial{x_1}}\))。
-
梯度代码示例:
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 # 在这其中x是np数组,是需要求梯度偏函数的自变量的集合,类似于(x,y,z) # 在梯度运算中,最终得到的是以权重w为自变量、损失函数f为因变量的偏函数的梯度
'''改进为适应多维数组的函数''' def numerical_gradient(f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x) # 生成与x形状相同的数组 it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) # flags是控制迭代器行为的标志,multi_index是迭代至当前位置的多维数组索引 # op_flags是修改数据的读写模式,readwrite代表可读写权限 # finished是nditer对象检查是否达到结尾的属性,结尾是为True while not it.finished: idx = it.multi_index tmp_val = x[idx] # f(x+h)的计算,float无需显式表达 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 # 还原值 it.iternext() return grad
-
略有误差的值在输出成np数组时会自动修改成易读的形式。
-
梯度指示的方向是函数值减小最多的方向。
-
负梯度是指函数值下降最快的方向,反之,正梯度同理。
梯度法
-
梯度表示的是各点处的函数值减小最多的方向,并不能保证梯度所指的方向就是函数的最小值或者真正应该前进的方向,所以在复杂函数中,梯度指示的方向基本上都不是函数值最小处。
-
函数的极小值和最小值和鞍点梯度都为0,梯度法寻找的梯度为0的地方,所以不一定是最小值,同时当函数复杂且为扁平状时,学习很可能会进入“学习高原”的停滞期。
-
梯度法:函数的取值从当前位置沿着梯度方向前进一段距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,通过不断地沿梯度方向前进逐渐减小函数值的过程就是梯度法。
-
一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法,但同时也有梯度上升法(寻找最大值)。
-
梯度法的数学表示:
\(x_0 = x_0 - \eta\frac{\partial{f}}{\partial{x_0}}\)
\(x_0 = x_0 - \eta\frac{\partial{f}}{\partial{x_0}}\)\(\eta\)表示更新量,在神经网络的学习中称为学习率。
-
梯度法的步骤会反复执行,逐渐减小函数值。
-
梯度法的代码实现:
def gradient_descent(f, init_x, lr=0.01, step_num=100): # 参数f是要优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数 x = init_x for i in range(step_num): grad = numerical_gradient(x, x) x -= lr*grad return x
-
梯度法举例
>>>def function_2(x): ... return x[0]**2 + x[1]**2 >>> init_x = np.array([-3.0, 4.0]) >>> gradiant descent(function_2, init_X, lr=0.1, step_num=100) # 输出:array([-6.11110793e-10, 8.14814391e-10]),十分接近正确结果(0,0)
-
设定合适的学习率很重要,过大发散,过小更新太少。
-
学习率是超参数,一种神经网络的参数,由人工设定,一般需要尝试多个值。
梯度法深究关键字:牛顿法(更精确)、Hessian矩阵
神经网络的梯度
-
神经网络的梯度数学表达式:
权重\(W\)\(W = \begin{pmatrix}w_{11} & w_{12} & w_{13}\\ w_{21} & w_{22} & w_{23}\\ \end{pmatrix}\)
损失函数L、梯度\(\frac{\partial{L}}{\partial{W}}\)
\(\cfrac{\partial{L}}{\partial{W}} = \begin{pmatrix}\cfrac{\partial{L}}{\partial{w_{11}}} & \cfrac{\partial{L}}{\partial{w_{12}}} & \cfrac{\partial{L}}{\partial{w_{13}}}\\ \\ \cfrac{\partial{L}}{\partial{w_{21}}} & \cfrac{\partial{L}}{\partial{w_{22}}} & \cfrac{\partial{L}}{\partial{w_{23}}}\\ \end{pmatrix}\)
-
在神经网络的梯度中,以损失函数\(L\)为因变量,以函数的所有权重\(w\)作为自变量来进行梯度下降(最后就可以获得在一定范围内梯度为0的损失函数)。
-
简单一层神经网络类代码实现:
class simpleNet: def __init__(self): self.W = np.random.randn(2, 3) # 利用高斯分布进行初始化权重 def predict(self, x): return np.dot(x, self.W) # 对矩阵进行点乘运算,x是输入信号n*2 def loss(self, x, t): z = self.predict(x) y = softmax(z) loss = cross_entropy_error(y, t) # cross_entropy_error是最终版本,求的损失函数值,x是运算后的输出信号,t是正确解标签 return loss
-
简单一层神经网络类示例:
>>> net = simpleNet() >>> x = np.array([0.6,0.9]) >>> p = net.predict(x) # p = [1.0541, 0.6307,1.1328] >>> t = np.array([0, 0, 1]) >>> net.loss(x,t) 0.9280568 >>> f = lambda w:net.loss(x, t) # f(W)的参数W是个伪参数,只是需要兼容numerical_gradient函数而定义f(W)。 >>> dW = numerical_gradient(f, net.W) # dW:权重的变化率/梯度,[[0.2192, 0.1435, -0.3628],[0.3288, 0.2153, -0.5442]] >>> f = lambda w:net.loss(x, t)
在上式中,如果需要校正权重只需要把numerical_gradient函数换为gradient_descent函数即可。
学习算法的实现
步骤及二层神经网络的类的实现
-
选出mini-batch
-
计算梯度,准备更新参数
-
更新参数
-
重复步骤123,结束
-
因为使用的mini-batch数据是随机选择的,所i在这里使用梯度下降法称为随机梯度下降法(SGD),在很多深度学习的框架中,随机梯度下降法一般是SGD函数实现。
-
二层神经网络的类的实现
class TwoLayerNet: # 初始化 def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # 随机取权重,偏置取0 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) # 计算识别精度,x:输入数据(图像数据),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
类的初始化参数 作用 input_size 输入层神经元数量 hidden_size 隐藏层神经元数量 ooutput_size 输出层神经元数量 -
部分应用示例:
net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) x = np.random.rand(100, 784) # 随机生成一个取值范围在[0, 1)的大小100*784的数组 y = net.predict(x) # 进行推理 t = np.random.rand(100, 10) # 随机生成(伪)正确解标签 grads = net.numerical_gradient(x, t) # 计算梯度
mini-batch的实现
-
实现与记录代码
# 加载数据集 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) # 创建损失函数值记录列表 train_loss_list = [] # 两种数据精度记录列表 train_acc_list = [] test_acc_list = [] # 超参数 iter_num = 200 # 循环次数 train_size = x_train.shape[0] # 训练数据大小 batch_size = 100 # mini-batch大小 learning_rate = 0.1 # 学习率 iter_per_epoch = max(train_size / batch_size, 1) # 平均每个epoch的重复次数 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) # 建立神经网络 for i in range(iter_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) # 更新参数 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) # 计算每个epoch的识别精度(两种数据) 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) x = np.arange(iter_num) y = np.array(train_loss_list) plt.plot(x, y) plt.show()
-
随着学习的进行,对训练数据的某个mini-batch的损失汉和速度值逐渐减小,神经网络的学习正常进行。
-
神经网络的学习必须确认是否能正确识别训练数据以外的其他数据,即确认是否会发生过拟合。
-
神经网络的最初目标是掌握泛化能力,因此要评价神经网络的泛化能力就必须要使用不包含在训练数据中的数据,所以要定期地对训练数据和测试数据记录识别精度。
-
一个epoch表示学习中所有训练数据均被使用过一次时的更新次数(即训练量达到一轮完整训练数据次数)。
-
一般会事先将所有训练数据随机打乱,然后按照批次大小,按序生成mini-batch,然后用索引遍历所有mini-batch。
误差反向传播
计算图、链式法则
-
计算图:将计算过程用图形(数据结构图:节点+边)表示出来。
-
节点和箭头表示计算过程,节点用\(\circ\)表示,其中是计算过程。
-
计算图解题过程:构建、从左到右计算。
-
计算图的特征是可以通过传递局部计算获得最终结果。
-
计算图可以通过正向传播和反向传播高效计算导数。
-
如果某个函数由符合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
-
反向传播计算先将节点的输入信号乘以节点的局部导数,然后再传递给下一个节点。
反向传播
-
加法节点反向传播导数乘1。
-
乘法节点反向传播导数乘输入信号的“翻转值”。
简单层的实现
-
乘法层(MulLayer)的构建。
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 # 反向传播,dout是正向传播时的输出变量的导数 def backward(self, dout): dx = dout * self.y # 翻转x和y dy = dout * self.x return dx, dy
-
调用backward()的顺序与调用forward()的顺序相反。
-
加法层(AddLayer)的构建。
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
激活函数层的实现
-
ReLU层。
-
原理图
-
实现代码
class Relu: # mask 是由True和False构成的np数组 def __init__(self): self.mask = None # mask 会把正向传播时输入的小于0的地方保存为True,其他为False 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
-
-
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
-
Affine/Softmax层的实现
Affine层
-
Affine层是仿射变换(一次线性变换和一次平移,即加权和运算与加偏置运算)的处理,仿射变换即正向传播中矩阵乘积运算。
-
Affine层的计算图。
-
Affine层的反向传播图。
-
批版本的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
-
考虑张量
class Affine: def __init__(self, W, b): self.W =W self.b = b self.x = None self.original_x_shape = None # 权重和偏置参数的导数 self.dW = None self.db = None def forward(self, x): # 对应张量 self.original_x_shape = x.shape x = x.reshape(x.shape[0], -1) self.x = x out = np.dot(self.x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis=0) dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量) return dx
-
Softmax-with-Loss层
-
识别过程。
-
神经网络中进行的处理有推理和学习,其中推理通常不使用softmax层,学习阶段需要softmax层。
-
SwL计算图。
-
计算式。
softmax函数
\(y_k = \cfrac{\exp\left(a_k + C\right)}{\sum^n_{i=1}\exp\left(a_i + C\right)}\)
交叉熵误差损失函数
\(E = - \frac{1}{N}\sum_{n}\sum_{k}t_{nk} \log{y_{nk}}\) -
计算图
-
-
SwL实现代码。
class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None self.t = None def forward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] dx = (self.y - self.t) / batch_size return dx
改进版。
class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None self.t = None def forward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况 dx = (self.y - self.t) / batch_size else: dx = self.y.copy() dx[np.arange(batch_size), self.t] -= 1 dx = dx / batch_size return dx
-
SwL传播的是监督数据与输出的误差,目的是使得监督数据与输出的误差变小。
-
SwL中Loss函数是交叉熵误差函数,之所以softmax函数和交叉熵误差结合使用是因为交叉熵误差接收的输入是预测概率输入,而softmax函数的作用是将神经网络的输出转化为概率输出。
误差反向传播的实现
-
优化版二层神经网络的实现。
class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # 初始化权重,随机取权重,偏置取0 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() # 进行推理,x:输入数据 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) # 计算识别精度,x:输入数据(图像数据),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 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,调用softmaxWithLoss函数的反向传播 dout = 1 dout = self.lastLayer.backward(dout) # ordered.values()可获得ordered中的有序字典列表,通过调用方法的反向传播求出导数 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
-
误差反向传播梯度确认。
(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) # 选取mini-batch 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))
-
误差反向传播法神经网络学习的实现。
(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.gradient(x_batch, t_batch) # 更新 for key in ('W1', 'W2', 'b1', '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)
已完成神经网络的改进与修改
原书讲解代码(运行中报错误,且结果错误)
import sys,os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
import numpy as np
from collections import OrderedDict
# 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
# 交叉熵误差损失函数
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
# 数值梯度,f是函数、x是自变量(输入值)
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
# Affine层
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.original_x_shape = None
# 权重和偏置参数的导数
self.dW = None
self.db = None
def forward(self, x):
# 对应张量
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.x, self.W) + self.b
return out
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量)
return dx
# Relu激活函数层计算识别精度,x:输入数据(图像数据),t:监督数据(正确解标签)
class Relu:
# mask 是由True和False构成的np数组
def __init__(self):
self.mask = None
# mask 会把正向传播时输入的小于0的地方保存为True,其他为False
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
# softmax函数与loss函数层
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None
self.t = None
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
dx = (self.y - self.t) / batch_size
else:
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx = dx / batch_size
return dx
# 二层神经网络的构建
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重,随机取权重,偏置取0
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()
# 进行推理,x:输入数据
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)
# 计算识别精度,x:输入数据(图像数据),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 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,调用softmaxWithLoss函数的反向传播
dout = 1
dout = self.lastLayer.backward(dout)
# ordered.values()可获得ordered中的有序字典列表,通过调用方法的反向传播求出导数
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
# # 梯度确认
# (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))
# 误差反向传播的实现
(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.gradient(x_batch, t_batch)
# 更新
for key in ('W1', 'W2', 'b1', '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)
在这其中,结果一直在0.09左右,后发现是softmax函数问题,需改进
改进后有注解正确代码
import sys,os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
import numpy as np
from collections import OrderedDict
# 二维数组的softmax激活函数(输出层激活函数)
def softmax(x):
if x.ndim == 2:
x = x.T # 转置,不转置将导致无法广播,
x = x - np.max(x, axis=0) # 减去每一列的最大值,从而防止溢出
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y.T
x = x - np.max(x) # 溢出对策
return np.exp(x) / np.sum(np.exp(x))
# 交叉熵误差损失函数
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
# 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
if t.size == y.size:
t = t.argmax(axis=1) # argmax是返回最大值索引
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
'''numerical_gradient的目的是梯度确认
# 数值梯度,f是函数、x是自变量(输入值)
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
'''
# Affine层
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.original_x_shape = None
# 权重和偏置参数的导数
self.dW = None
self.db = None
def forward(self, x):
# 对应张量
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.x, self.W) + self.b
return out
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量)
return dx
# Relu激活函数层计算识别精度,x:输入数据(图像数据),t:监督数据(正确解标签)
class Relu:
# mask 是由True和False构成的np数组
def __init__(self):
self.mask = None
# mask 会把正向传播时输入的小于0的地方保存为True,其他为False
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
# softmax函数与loss函数层
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None # softmax的输出
self.t = None # 监督数据
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
dx = (self.y - self.t) / batch_size
else:
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx = dx / batch_size
return dx
# 二层神经网络的构建
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重,随机取权重,偏置取0
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()
# 进行推理,x:输入数据
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)
# 计算识别精度,x:输入数据(图像数据),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 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,调用softmaxWithLoss函数的反向传播
dout = 1
dout = self.lastLayer.backward(dout)
# ordered.values()可获得ordered中的有序字典列表,通过调用方法的反向传播求出导数
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
'''梯度确认
# 梯度确认
(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))
'''
# 误差反向传播的实现
(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.gradient(x_batch, t_batch)
# 更新
for key in ('W1', 'W2', 'b1', '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)
与学习相关技巧
参数的更新
寻找最优参数的过程称为最优化,则列出一些最优化方法以下。
示例函数:\(f\left(x,y\right) = \dfrac{1}{20}x^2 + y^2\)
示例函数图像以及梯度图像:
SGD(随机梯度下降法)
-
SGD数学式表示。
\(W \gets W - \eta\dfrac{\partial{L}}{\partial{W}}\)
\(W\)是需要更新的权重参数
\(\eta\)表示学习率 -
SGD代码实现
class SGD: def __init__(self, lr=0.01): # lr学习率 self.lr = lr def update(self, params, grads): for key in params.keys(): params[key] -= self.lr * grads[key]
-
SGD应用示例。
network = TwoLayerNet(...) optimizer = SGD() for i in range(10000): ... x_batch, t_batch = get_mini_batch(...) # mini-batch grads = network.gradient(x_batch, t_batch) params = network.params optimizer.update(params, grads) ...
-
SGD缺点:
-
如果函数的形状非均向,搜索路径就会非常低效,例如呈延伸状函数\(f\left(x,y\right)=\frac{1}{20}x^2 + y^2\)。
-
根本原因是梯度的方向没有指向最小值。
-
-
SGD示例函数图像:
Momentum(动量)
-
Momentum数学式表示。
\(v \gets \alpha v - \eta\dfrac{\partial{L}}{\partial{W}}\)
\(W \gets W+v\)\(v\)是速度
\(\alpha\)是影响速度变化的因素(一般是0.9之类)
\(- \eta\dfrac{\partial{L}}{\partial{W}}\)就是梯度下降方向。 -
Momentum代码实现。
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]
-
Momentum的特点:
-
在代码实现中,实例v保存了物体的速度即上次参数更新方向与举例。
-
每次更新参数,如果和上次同向则会加速,如果反向,则会减速,所以会减弱“之”字形的变动程度。
-
-
Momentum示例函数图像:
AdaGrad(AdaptiveGrad)
-
在优化学习率的有关技巧中,有一种称为学习率衰减,即随着学习的进行,使学习率逐渐减小,其中AdaGrad方法可以该分别定制每个参数的学习率。
-
AdaGrad数学式表示。
\(h \gets h + \dfrac{\partial{L}}{\partial{W}} \bigodot \dfrac{\partial{L}}{\partial{W}}\)
\(W \gets W - \eta\dfrac{1}{\sqrt{h}}\dfrac{\partial{L}}{\partial{W}}\)
\(\bigodot\) 是矩阵乘法,得到的结果是个标量
\(\dfrac{1}{\sqrt{h}}\)是衰减尺度 -
AdaGrad代码实现。
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(): # 微小值1e-7是防止出现0作除数的情况 self.h[key] += grads[key] * grads[key] params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
-
AdaGrad特点:
AdaGrad会记录过去所有的平方和,所以学习越深入,更新的幅度越小,为了避免更新量变为0可以使用RMSProp方法(会逐渐地遗忘过去的梯度,即“指数移动平均”)。 -
AdaGrad示例函数图像:
Adam
-
将Momentum与AdaGrads融合从而组合了两种方法的优点。
-
Adam设置了3个超参数:学习率\(\alpha\)、一次momenetum系数\(\beta_1\)、二次momentum系数\(\beta_2\),原论文中是\(\beta_1 = 0.9 \beta_2=0.999\)。
-
Adam进行了超参数的“偏置校正”。
-
Adam示例函数图像:
多种方法选择与比较
-
目前并不存在能在所有问题中都表现良好的方法,各有各的特点。
-
四种方法对于同一示例函数的图像对比。
-
基于MNIST数据集的更新方法比较。
-
实验结果会随学习率等超参数、神经网络结构的不同而发生变化,一般而言与SGD相比,其他三种方法学习更快,有时精度也更高。
权重的初始值
权重为0的错误
-
将权重初始值设为0会导致每层神经元传递的值相同,从而造成反向传播后权重会更新为相同的值,并拥有了对称(重复)的值,这使得神经网络丧失了意义,从而无法正确学习。
-
为了防止“权重均一化”或瓦解权重的对称结构,必须随机生成初始值。
-
标准化:均值为0,标准差为1。
-
均一化:将数据调整到某个统一的范围或标准。
-
归一化:将数据缩放到特定范围(通常是[0,1]或[-1,1])。
高斯分布初始值(sigmoid函数)
标准差为1的高斯分布
-
通过观察隐藏层的激活值(激活函数的输出数据)的分布从而得到权重初始值产生的影响。
-
实验:向一个5层神经网络(多种函数)传入随机生成的输入数据,用直方图绘制各层激活值数据分布。
input_data = np.random.randn(1000, 100) # 1000个数据 node_num = 100 # 各隐藏层的节点(神经元)数 hidden_layer_size = 5 # 隐藏层有5层 activations = {} # 激活值的结果保存在这里 x = input_data for i in range(hidden_layer_size): if i != 0: x = activations[i-1] # 改变初始值进行实验! w = np.random.randn(node_num, node_num) * 1 # w = np.random.randn(node_num, node_num) * 0.01 # w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) # w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num) a = np.dot(x, w) # 将激活函数的种类也改变,来进行实验! z = sigmoid(a) # z = ReLU(a) # z = tanh(a) activations[i] = z # 绘制直方图 for i, a in activations.items(): plt.subplot(1, len(activations), i+1) plt.title(str(i+1) + "-layer") if i != 0: plt.yticks([], []) # plt.xlim(0.1, 1) # plt.ylim(0, 7000) plt.hist(a.flatten(), 30, range=(0,1)) plt.show()
-
激活值的分布图:
-
特点:激活值偏向0和1分布会造成反向传播梯度消失问题。
-
激活值偏向0和1分布以及梯度消失的原因:
-
在前向传播过程中,每一层的输入\(z\)是前一层输出的加权和:
$ x = \sum_i w_i a_i + b \( 由于权重\)w_i\(大概率分布在0附近,但也可能有极端值,因此加权和\)z\(的值也可能很大或很小。 考虑到sigmoid函数的性质。当\)x\(的值非常大时,\)\sigma(x)\(趋近于1;当\)x\(的值非常小时,\)\sigma(x)\(趋近于0。由于权重初始化为标准差为1的高斯分布,\)z$的值可能很容易变得很大或很小,从而导致sigmoid函数的输出偏向0或1。 -
考虑一种极端情况:如果权重中有一个或几个值特别大或特别小,那么这些权重对应的项在加权和\(z\)中就会占据主导地位,导致\(z\)的值也特别大或特别小。由于高斯分布的特性,这种情况是有可能发生的,尽管概率可能不高。
-
由于sigmoid函数的导数\(\sigma'(x)\)在\(x\)接近0或1时趋近于0,因此当激活值\(a = \sigma(z)\)偏向0或1时,梯度在反向传播过程中会迅速减小,导致梯度消失。
-
标准差为0.01的高斯分布
-
激活值的分布图:
-
特点:激活值分布偏向于0.5附近的分布,导致表现力受限。
-
各层的激活值的分布都要求有适当的广度,有所偏向就会导致梯度消失或表现力受限。
Xavier初始值(Sigmoid函数和tanh函数)
-
Xavier初始值:与前一层有n个节点连接时,初始值使用标准差为\(\dfrac{1}{\sqrt{n}}\)的高斯分布。
-
Xavier初始值的实现。
node_num = 100 # 前一层节点数 w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
-
Xavier的论文提出的设定值中,不仅考虑了前一层输入节点的数量,还考虑了下一层的输出节点数量。
-
sigmoid函数激活值的分布图:
-
tanh函数代替sigmoid函数即可改善略微歪斜的问题,使用tanh函数会呈吊钟型分布,因为tanh函数的对称性。
-
Xavier初始值适合左右对称且中央附近可以视作线性函数的激活函数使用。
He初始值(ReLU函数)
-
一般ReLU函数使用He初始值。
-
He初始值:与前一层有n个节点连接时,He初始值使用标准差为\(\sqrt{\dfrac{2}{n}}\)的高斯分布。
-
直观解释He初始值\(\sqrt{\frac{2}{n}}\)和Xavier初始值\(\frac{1}{\sqrt{n}}\)的差别为,因为ReLU的负值区域的值为0,为了使其更有广度,所以使用二倍系数。
-
比较ReLU函数使用几种初始值的结果。
-
对于ReLU函数,当std=0.01时,各层激活值非常小,以至于逆向传播时权重的梯度也十分的小,这会导致学习基本没有进展。
-
对于ReLU函数,Xavier初始值会随着层的加深,偏向会逐渐变大,从而导致梯度消失。
基于MNIST数据集的权重初始值的比较
-
比较图。
-
由图可知,std=0.01时完全无法进行学习,而Xavier初始值和He初始值学习很顺=顺利,并且He初始值的学习精度更快一些。
-
权重的初始值非常重要,很多时候权重的初始值的设定关系到神经网络的学习能否成功。
Batch Normalization
-
Batch Normalization(简称batch Norm批归一化),通过对每一层的输入数据进行归一化处理,使数据的均值接近于0,标准差接近于1,(使其拥有适当的广度)从而解决神经网络中因数据分布变化导致的“内部协变量偏移(前面层的轻微改变积累后对后面的影响十分大)”问题。
-
Batch Norm的优点:
-
可以使学习快速进行(可以增大学习率)。
-
减少依赖初始值。
-
抑制过拟合(降低Dropout等的必要性)。
-
-
Batch Nrom层应在Affine层和激活函数层之间。
-
Batch Nrom层应在Affine层和激活函数层之间,而不是在激活函数层之后的原因:
-
避免破坏非线性特征的分布。
-
避免数据落入激活函数的饱和区域,从而缓解梯度消失的问题。
-
激活函数输出值范围波动较大,所以激活函数之前应用BN层可以稳定这些输入,使它们具有更一致的分布,从而提高数值稳定性。
-
-
Batch Norm是以进行学习时的mini-batch为单位,按mini-batch进行正规化(归一化/标准化,但是正规化更为准确,因为这更偏向于通过一系列数学变换来调整数据的分布,使其具有更合适的尺度或形态,从而有助于神经网络的训练)。
-
标准化、正规化、归一化三者的区别:
- 归一化(Normalization):通常指的是将数据调整到一定的范围或分布,以便模型更好地学习和处理。Batch Normalization本质上就是在每个批次的输入数据上应用归一化操作。
- 正规化(Regularization):这是一种技术,主要用于防止模型过拟合。它通过在损失函数中添加一些额外的项(如L1、L2正则化)来惩罚模型的复杂度。
- 标准化(Standardization):指的是将数据转换为均值为0,标准差为1的分布(相对于均值接近于0,标准差接近于1的归一化更加严格)。这是一种特殊的归一化形式。
-
Batch Norm的正规化过程 数学式表示。
\(\mu_B \gets \frac{1}{m}\sum^{m}_{i=1}x_i\)
\(\sigma^2_B \gets \frac{1}{m}\sum^{m}_{i=1}\left( x_i-\mu_B \right)^2\)
\(\hat{x_i} \gets \cfrac{x_i-\mu_B}{\sqrt{\sigma^2_B + \varepsilon}}\)\(\mu_B\)是mini-batch的均值
\(\sigma^2_B\)是方差
\(\varepsilon\)是微小值,防止出现除以0的情况 -
Batch Norm对正规化后的数据进行缩放和平移的变换过程 用数学式表示。
\(y_i \gets \gamma \hat{x_i} + \beta\)
初始,\(\gamma=1\) \(\beta=0\),然后再通过学习调整到合适的值。 -
Batch Norm的计算图。
-
Batch Norm可以加快学习的原因:
-
标准化处理,从而使得数据分布更加稳定,使得优化算法更有效的找到最优解。
-
减少内部协变量偏移,使神经网络训练更加稳定。
-
标准化后的数据更加小,减少了梯度消失或爆炸,从而提高梯度传播速率。
-
减少了模型对特定初始值或权重尺度的依赖,从而有助于防止过拟合。
-
-
Batch Norm会减少对初始值依赖的原因:
-
通过标准化操作使得每一层的输入具有适当的均值和方差。
-
引入的参数允许网络根据数据的特性进行自适应调整,以恢复或调整数据的原始分布。
-
固定每一层网络输入值的分布,使得网络不再受到内部协变量偏移的影响。
-
-
Batch Norm之所以需要对数据进行缩放平移,是因为简单的标准化操作可能会改变数据的原始分布,丢失一些有用的信息。
-
Batch Norm对\(\gamma \beta\)的调整的根据是经过Affine层但未经Batch Norm层处理的数据,例如\(\beta\)所依据的则是未经处理的数据的均值。
-
Batch Norm带来的变化图像。
正则化
-
正则化通过在模型的损失函数(或目标函数)中添加一个与模型复杂度相关的惩罚项(或正则项),来约束模型参数的取值范围或结构,从而防止模型过于复杂而过度拟合训练数据。
-
正则化项通常是模型参数的某种范数,最常见的有L1范数和L2范数。L1正则化(Lasso)鼓励模型参数变为零,从而实现参数的稀疏性,有助于降低模型的复杂度;而L2正则化(Ridge)则使模型参数接近零,但不会使其完全为零,有助于平滑模型的输出。
-
正则化方法有弹性网(Elastic Net,结合了L1和L2正则化)、最大范数约束、Dropout(在训练过程中随机丢弃部分神经元连接)、权重衰减(直接对权重进行惩罚)、数据增强(通过对训练数据进行变换来增加样本多样性)等。
-
范数(norm)是数学中的一种基本概念,它定义在赋范线性空间中,并满足非负性、齐次性和三角不等式这三个条件。范数常常被用来度量某个向量空间(或矩阵)中的每个向量的长度或大小。范数的本质是距离,是具有“长度”概念的函数,常用在线性代数、泛函分析及相关的数学领域。范数的存在意义是为了实现比较,把不能比较的向量转换成可以比较的实数。
-
常见的范数类型说明、数学式及举例:
-
L0范数:
L0范数指的是向量中非零元素的个数。
数学表示:\(||x||_0 = \text{number of non-zero elements in } x\) -
L1范数:
L1范数指向量中各个元素绝对值之和。
数学表示:\(||x||_1 = \sum_{i=1}^{n} |x_i|\) -
L2范数:
L2范数,也称为欧几里得范数,指向量中各元素平方和的平方根。
数学表示:\(||x||_2 = \sqrt{\sum_{i=1}^{n} x_i^2}\) -
Lp范数:
Lp范数是一种更一般化的范数形式。
数学表示:\(||x||_p = \left( \sum_{i=1}^{n} |x_i|^p \right)^{\frac{1}{p}}\)
当p取不同的值时,Lp范数表现出不同的特性。例如,当p=1时,它退化为L1范数;当p=2时,它退化为L2范数。
-
过拟合
-
过拟合是模型在训练集上表现很好,但在测试集或新的、未见过的数据上表现较差的现象,其具体原因是模型记住了过多训练中的噪点和细节(钻牛角尖),而不是数据的内在规律和模式。
-
出现过拟合的原因:
-
训练集的数量级、事物规律的复杂性和模型的复杂度不匹配。
-
训练集和测试集的特征不一致。
-
样本中的噪点干扰过大,导致模型过分记住了噪音特征,而忽略了真实的输入输出间的关系。
-
权值学习迭代次数过多,拟合了训练数据中的噪声和训练样例中没有代表性的特征。(训练epoch过多)
-
-
过拟合现象实验。
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True) # 为了再现过拟合,减少学习数据 x_train = x_train[:300] t_train = t_train[:300] network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10) optimizer = SGD(lr=0.01) max_epochs = 201 train_size = x_train.shape[0] batch_size = 100 train_loss_list = [] train_acc_list = [] test_acc_list = [] iter_per_epoch = max(train_size / batch_size, 1) epoch_cnt = 0 for i in range(100000): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] grads = network.gradient(x_batch, t_batch) optimizer.update(network.params, grads) 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("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc)) epoch_cnt += 1 if epoch_cnt >= max_epochs: break # 绘制图形 markers = {'train': 'o', 'test': 's'} x = np.arange(max_epochs) plt.plot(x, train_acc_list, marker='o', label='train', markevery=10) plt.plot(x, test_acc_list, marker='s', label='test', markevery=10) plt.xlabel("epochs") plt.ylabel("accuracy") plt.ylim(0, 1.0) plt.legend(loc='lower right') plt.show()
权值衰减
-
权值衰减是通过对在学习过程中大的权重进行惩罚,从而限制模型的权值变得过大,以维持模型的复杂度在较低的水平,从而缓解过拟合问题,提升模型的泛化性能。
-
过大权重造成过拟合的原因:
-
过大权重会导致细小变化从而对神经网络造成大变化,从而对噪点和细节过于拟合,即神经网络敏感度过强。
-
大权重使得模型在参数空间中搜索的范围更广,增加了模型找到局部最优解而非全局最优解的风险。局部最优解往往对应于对训练数据的过度拟合,而不是对数据内在规律的准确捕捉。
-
-
权值衰减会降低模型复杂度并约束参数空间。
-
常见的权值衰减【L2正则化(权重衰减)】示例:
-
方法:为损失函数加上权重的平方范数(L2范数),从而抑制权重变大。
-
数学式:
\(Loss \gets Loss + \frac{1}{2} \lambda W^2\)
\(\lambda\)是控制正则化强度的超参数,设置越大,对大的权重施加的惩罚就越重。
\(W\)是权重和,即\(\sum_{i}w_i\)
最后得到 \(\dfrac{\partial{Loss}}{\partial{w_i}} \gets \dfrac{\partial{Loss}}{\partial{w_i}} + \lambda{W}\) -
代码实现:
def gradient(self, x, t, reg_lambda=0.01): # forward self.loss(x, t) # backward,调用softmaxWithLoss函数的反向传播 dout = 1 dout = self.lastLayer.backward(dout) # ordered.values()可获得ordered中的有序字典列表,通过调用方法的反向传播求出导数 layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) + reg_lambda * (grads['W1'] + grads['W2']) # 设定 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
-
-
经过权重衰减改进后的实验结果:
Dropout
-
Dropout是一种在学习过程中随机删除神经元的方法,训练时,随机选出隐藏层的神经元,然后将其删除(被设置值为False),最后计算各神经元输出时进行权重缩放,即乘上训练时的删除比例。这种随机性的丢弃操作可以使模型在每次迭代中都有不同的结构,从而防止模型对训练数据中的特定特征过度依赖。
-
Dropout概念图:
-
Dropout的实现:
class Dropout: # dropout_ratio是dropout神经元比例,mask是标记被丢弃的神经元的Bool列表 def __init__(self, dropout_ratio=0.5): self.dropout_ratio = dropout_ratio self.mask = None # train_flg是是否处于训练状态的标记 def forward(self, x, train_flg=True): # 训练模式 if train_flg: # mask是通过生成相应数量的随机数与dropout_ratio比较进行随机丢弃 # *x是对tuple类型的x解包 self.mask = np.random.rand(*x.shape) > self.dropout_ratio return x * self.mask # 非训练模式 else: return x * (1.0 - self.dropout_ratio) # 反向传播只需传播为丢弃的神经元信息 def backward(self, dout): return dout * self.mask
-
经过Dropout(Dropout_rate=0.15)改进后的实验结果:
-
机器学习中常用集成学习,就是让多个模型单独进行学习,推理时再取多个模型的输出的平均值,而Dropout将集成学习的效果模拟地通过一个网络实现。
-
集成学习能提高神经网络的识别精度的原因:
-
多样性增强
-
减少过拟合
-
错误纠正
-
性能提升
-
超参数的验证
- 超参数是机器学习中在训练过程之前需要设置的参数,它不是通过学习过程得出,而是需要由模型训练者根据经验和实验来指定的,例如神经元数量、学习率、batch大小、训练次数等。超参数的选择对模型的性能有着至关重要的影响,因为它们控制了模型的结构和训练的方式。
验证数据
-
不能使用测试数据评估超参数的性能,如果使用测试数据调整超参数,超参数的值会对测试数据发生过拟合。
-
调整超参数时,必须使用超参数专用的确认数据,用于调整超参数的数据一般被称为验证数据。
-
训练数据用于参数的学习,验证数据用于超参数性能的评估,最后使用测试数据确认模型的泛化能力。
-
分割数据验证数据:
# shuffle_dataset()的实现 def shuffle_dataset(x, t): permutation = np.random.permutation(x.shape[0]) x = x[permutation,:] if x.ndim == 2 else x[permutation,:,:,:] t = t[permutation] return x, t # 分割数据 (x_train, t_train), (x_test, t_test) = load_mnist() # 打乱训练数据 x_train, t_train = shuffle_dataset(x_train, t_train) # 分割验证数据 validation_rate = 0.20 validation_num = int(x_train.shape[0] * validation_rate) x_val = x_train[:validation_num] t_val = t_train[:validation_num] x_train = x_train[validation_num:] t_train = t_train[validation_num:]
超参数的最优化
-
超参数的最优化是逐渐缩小超参数的“好值”的存在范围。
-
逐渐缩小范围是指一开始先大致设定一个范围,从范围中随机选出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后多次重复,观察识别精度的结果,从而逐渐缩小范围。
-
在超参数的最优化中,相对于有规律的搜索,随机采样的搜索方式效果更好,其大致确定的范围是指以“10的阶乘”的尺度(即对数尺度)指定范围。
-
在超参数的最优化中,要减少学习的epoch,缩短一次评估所需时间来提高效率。
-
以上为超参数最优化的实践性方法,如果需要更精炼的方法,可以使用贝叶斯最优化(以贝叶斯定理为中心的数学理论)。
-
超参数最优化的实现:
# 超参数(权值衰减系数和学习率)随机采样代码,uniform是随机函数,前开后闭 # 超参数的随机搜索 optimization_trial = 100 results_val = {} results_train = {} for _ in range(optimization_trial): # 指定搜索的超参数的范围 weight_decay = 10 ** np.random.uniform(-8, -4) lr = 10 ** np.random.uniform(-6, -2) val_acc_list, train_acc_list = __train(lr, weight_decay) print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay)) key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay) results_val[key] = val_acc_list results_train[key] = train_acc_list
对比图:
卷积神经网络(Convolutional Neural Network, CNN)
CNN相对于之前的神经网络出现了卷积层(Convolution层)和池化层(Pooling层),并且将前几层的 Affine-激活函数 层用Conv-激活函数-Polling层代替。
原结构
变化后的结构
卷积层
卷积层的存在
-
Affine层将数据的形状忽视了,使得空间信息无法提取,可能丢失调重要的空间信息,例如由高、长、通道(色彩信息)的三维形状构成的图像。
-
图像的重要空间信息例如邻近像素的相似值、RGB各通道之间的关系等,三维形状中可能有隐藏有值的提取的本质模式。
-
卷积层可以(有可能)正确理解图像等具有形状的数据。
-
卷积层的输入输出数据称为特征图,卷积层的输入数据称为输入特征图,输出数据称为输出特征图。
-
对于像MNIST数据集中的灰度图像,其卷积层的通道数是1,一般RGB图像的通道数是3。
-
卷积核也需要初始化,一般用Xavier初始值(tanh函数)、kaiming初始值(ReLU与图像分类)或LeCun初始值(生成任务)。
卷积运算与填充、步幅
卷积运算
-
卷积层进行的处理就是卷积运算,卷积运算相当于图像处理中的“滤波器运算”(在信号处理、图像处理等领域中,使用特定的算法或硬件设备来对信号进行加工处理,以达到去除噪声、提取特征、增强有用信号等目的)。
-
这是卷积运算的例子,其中滤波器也称为“核”。
-
对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用,然后将各个位置上滤波器的元素和输入的对应元素相乘,然后求和(也称乘积累加运算),最后将结果保存到输出的对应位置,其中还可以对数据使用偏置,但通常只有一个。
填充(padding)
在卷积运算后数据会缩小(反复进行卷积运算会出问题),所以要通过对输入数据进行填充,也就是向输入数据的周围填入固定的数据(一般是0),从而调整输出的大小,其中幅度代表二维数据的周围其中一个方向的增幅大小。
步幅(stride)
-
应用滤波器的位置间隔称为步幅。
-
增大步幅输出大小会变小,增大填充输出大小会变大,
-
输入数据大小、填充和步幅对卷积运算输出大小的影响关系式:
输入大小为 \(\left(H, W\right)\),滤波器大小为 \(\left(FH, FW\right)\),输出大小为 \(\left(OH, OW\right)\)。填充为 \(P\),步幅为 \(S\)。
\(OH = \cfrac{H + 2P - FH}{S} + 1\)
\(OW = \cfrac{W + 2P - FW}{S} + 1\)当设定的值无法除尽时,需采取报错等对策,有些情况也会四舍五入。
三维数据的卷积运算及理解
-
当纵深(通道)方向有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果向加,从而得到输出。
-
在三维数据的卷积运算中,输入数据和滤波器的通道数需相同。
-
结合方块思考,如果想在通道方向也拥有多个卷积运算,仅需应用FN个滤波器汇集处理,其中输入数据(Channel, Height, Width),滤波器(FilterNumber, Channel, FilterHeight, FilterWidth)。
-
最后形成并传输的三维输出数据即为CNN处理流。
-
相应的卷积处理流的偏置大小为(FilterNumber, 1, 1),最后得到的结果仍是(FN, OH, OW)。
批处理
批处理与Affine层的批处理方式类似,将各层间传递的数据保存为4维数据,在各个数据的开头添加了批用的维度,即(batch_num, channel, height, width)
前一层的通道数(特征图数量)决定这一层的过滤器深度,这一层的过滤器数量决定这一层输出特征图的通道数。
池化层
-
池化是缩小高和长方向上的空间的运算。
步幅为2进行2*2(目标区域大小)的Max池化(池化类型)示例:
-
一般池化的窗口大小会和步幅设定成相同的值。
-
常用池化有Max池化、Average池化等,一个是目标区域最大值,一个是目标区域平均值。
-
池化层的特征:
-
没有要学习的参数(池化层中的卷积核权重、偏置等需要学习训练)
-
通道数不发生变化,计算与通道独立
-
对微小的位置变化具有鲁棒性(健壮),会吸收小幅输入数据的偏差
-
-
步幅和输入数据对池化输出大小的影响关系式:
输入大小为 \(\left(H, W\right)\),池化目标区域大小为 \(\left(PH, PW\right)\),输出大小为 \(\left(OH, OW\right)\)。填充为 \(P\),步幅为 \(S\)。
\(OH = \cfrac{H - PH}{S} + 1\)
\(OW = \cfrac{W - PW}{S} + 1\)
卷积层与池化层的实现
卷积运算原理
-
简单地实现卷积运算可以使用for循环,但是在np中使用for循环会使得处理变慢,所以需要依靠
im2col()
函数实现卷积运算。 -
im2col
函数将输入数据展开以适合滤波器(权重)运算的形式,对多维的输入数据应用im2col
函数后,数据会转换成二维矩阵。 -
im2col
会将应用滤波器的区域展开成1行(可快速与滤波器计算),又因为在实际的卷积运算中,滤波器的应用区域是重叠的,所以会导致展开后的元素个数会多于原方块的元素个数,从而导致im2col
函数比普通的实现消耗更多内存,但是计算机的矩阵计算优化强,可以有效地高速运算。 -
im2col
是image to column的缩写。 -
在使用
im2col
函数计算后,进行卷积运算可直接将卷积层的滤波器展开成1列后计算两个矩阵的乘积即可。 -
卷积运算图:
通过
im2col
函数将原数据转换成可以直接使dot运算代替滤波器运算的数据。
卷积层的实现
-
im2col
函数的实现:""" 参数说明: - input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据 - filter_h : 滤波器的高 - filter_w : 滤波器的长 - stride : 步幅 - pad : 填充 """ def im2col(input_data, filter_h, filter_w, stride=1, pad=0): N, C, H, W = input_data.shape out_h = (H + 2*pad - filter_h)//stride + 1 out_w = (W + 2*pad - filter_w)//stride + 1 img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant') col = np.zeros((N, C, filter_h, filter_w, out_h, out_w)) for y in range(filter_h): y_max = y + stride*out_h for x in range(filter_w): x_max = x + stride*out_w col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride] col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1) return col
-
im2col
函数实现的卷积层:class Convolution: # W是滤波器,b是偏置 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 # 根据height和width的计算公式算出结果的height和width 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 # 将输出结果转换为原形态,这里为了方便理解-1,所以写成了这样 # 写作out = out.reshape(N, -1, out_h, out_w)也可 out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) return out
-
reshape()
函数中的-1是自动计算维度大小。 -
transpose()
函数通过改变多维数组的轴的顺序将卷积运算后的结果恢复原状。 -
在卷积层进行反向传播时,必须进行
im2col
的逆处理,即col2im
函数。
池化层的实现
-
池化层与卷积层相同,也使用
im2col
函数展开输入数据。 -
池化层与卷积层不同的是池化层会将数据按通道单独展开(一个通道一列),但是卷积层不会按通道单独展开。
池化层展开示例,卷积层展开计算类似。
-
im2col
函数实现的池化层:class Polling: # pool_h和pool_w是池化目标区域的尺寸 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 # 根据池化height和width的计算公式算出结果的height和width out_h = int(1 + (H - self.pool_h) / self.stride) out_w = int(1 + (W - self.pool_w) / self.stride) # 展开输入数据,对数据进行Max池化处理 col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) # # 这里是划分Max池化区块,但是多余的内容,在im2col函数中已经划分好了池化区块,可以省略 # col = col.reshape(-1, self.pool_h*self.pool_w) out = np.max(col, axis=1) out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) return out
CNN的实现
-
简单CNN的网络结构图:
-
CNN神经网络的实现:
def im2col(input_data, filter_h, filter_w, stride=1, pad=0): N, C, H, W = input_data.shape out_h = (H + 2*pad - filter_h)//stride + 1 out_w = (W + 2*pad - filter_w)//stride + 1 img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant') col = np.zeros((N, C, filter_h, filter_w, out_h, out_w)) for y in range(filter_h): y_max = y + stride*out_h for x in range(filter_w): x_max = x + stride*out_w col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride] col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1) return col def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0): N, C, H, W = input_shape out_h = (H + 2*pad - filter_h)//stride + 1 out_w = (W + 2*pad - filter_w)//stride + 1 col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2) img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1)) for y in range(filter_h): y_max = y + stride*out_h for x in range(filter_w): x_max = x + stride*out_w img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :] return img[:, :, pad:H + pad, pad:W + pad] class Convolution: def __init__(self, W, b, stride=1, pad=0): self.W = W self.b = b self.stride = stride self.pad = pad # 中间数据(backward时使用) self.x = None self.col = None self.col_W = None # 权重和偏置参数的梯度 self.dW = None self.db = None def forward(self, x): FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = 1 + int((H + 2 * self.pad - FH) / self.stride) out_w = 1 + int((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) self.x = x self.col = col self.col_W = col_W return out def backward(self, dout): FN, C, FH, FW = self.W.shape dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN) self.db = np.sum(dout, axis=0) self.dW = np.dot(self.col.T, dout) self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW) dcol = np.dot(dout, self.col_W.T) dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) return dx 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 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 self.x = None self.arg_max = None 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) col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h * self.pool_w) arg_max = np.argmax(col, axis=1) out = np.max(col, axis=1) out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) self.x = x self.arg_max = arg_max return out def backward(self, dout): dout = dout.transpose(0, 2, 3, 1) pool_size = self.pool_h * self.pool_w dmax = np.zeros((dout.size, pool_size)) dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten() dmax = dmax.reshape(dout.shape + (pool_size,)) dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1) dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad) 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): # 对应张量 self.original_x_shape = x.shape x = x.reshape(x.shape[0], -1) self.x = x out = np.dot(self.x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis=0) dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量) return dx def softmax(x): if x.ndim == 2: x = x.T x = x - np.max(x, axis=0) y = np.exp(x) / np.sum(np.exp(x), axis=0) return y.T x = x - np.max(x) # 溢出对策 return np.exp(x) / np.sum(np.exp(x)) def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) y = y.reshape(1, y.size) # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引 if t.size == y.size: t = t.argmax(axis=1) batch_size = y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None # softmax的输出 self.t = None # 监督数据 def forward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况 dx = (self.y - self.t) / batch_size else: dx = self.y.copy() dx[np.arange(batch_size), self.t] -= 1 dx = dx / batch_size return dx class SimpleConvNet: ''' 参数: input_dim=(channels, height, width) tuple,输入数据的维度 conv_param=dict,卷积层超参数dict filter_num=int,滤波器的数量 filter_size=int,滤波器的大小 stride=int,步幅 pad=int,填充 hidden_size=int,Affine层神经元数量 output_size=int,输出层的神经元数量 weight_int_std=float,初始化权重的标准差 ''' def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01): # 初始化卷积层相关参数 filter_num = conv_param['filter_num'] filter_size = conv_param['filter_size'] filter_pad = conv_param['pad'] filter_stride = conv_param['stride'] # input_size 输入数据大小 input_size = input_dim[1] # conv_output_size 卷积层输出大小,pool_output_size 池化层输出大小 conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2)) # 初始化神经网络的权重与偏置 self.params = {} self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size) self.params['b1'] = np.zeros(filter_num) self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size) self.params['b2'] = np.zeros(hidden_size) self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size) self.params['b3'] = np.zeros(output_size) # 构建神经网络层次的有序列表 self.layers = OrderedDict() self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'], conv_param['stride'], conv_param['pad']) self.layers['Relu1'] = Relu() self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2) self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2']) self.layers['Relu2'] = Relu() self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3']) self.last_layer = SoftmaxWithLoss() # 预测 def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x # 计算损失函数 def loss(self, x, t): y = self.predict(x) return self.last_layer.forward(y, t) # 反向传播法求梯度 def gradient(self, x, t): # forward self.loss(x, t) # backward dout = 1 dout = self.last_layer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 设定 grads = {} grads['W1'] = self.layers['Conv1'].dW grads['b1'] = self.layers['Conv1'].db grads['W2'] = self.layers['Affine1'].dW grads['b2'] = self.layers['Affine1'].db grads['W3'] = self.layers['Affine2'].dW grads['b3'] = self.layers['Affine2'].db return grads # 计算识别精度,x:输入数据(图像数据),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
-
在CNN神经网络中Affine层的作用是:
-
将输入图像在经过卷积和池化操作后提取的特征进行压缩(特征整合并提取),并且根据压缩的特征完成模型的分类功能
-
决策边界:在多类分类问题中,Affine层帮助网络学习一个决策边界,将不同的输入数据分到不同的类别中。通过调整权重和偏置,优化这个决策边界,以最小化分类错误。
-
高度非线性区分:与非线性激活函数结合使用时,它可以作为更大非线性系统的一部分,使得神经网络能够捕捉和区分高度非线性的数据结构。
-
CNN的可视化
-
第一层权重转化成图像后可明显观察出滤波器的更新:
-
一个卷积核只能提取一个感受野大小的特征,要想提取更大面积的特征需要扩大感受野,要想增多特征的获取量,需要增加卷积核数量。
-
在学习后,滤波器更新成了有规律的滤波器,例如从白到黑渐变的滤波器、含有块状区域(blob)的滤波器等。
-
在第一层滤波器可视图中,滤波器在观察边缘和斑块,所以卷积层的滤波器会提取到边缘或斑块等原始信息。
-
同一层卷积层多个卷积核在经过学习后的值不同、识别的特征不同的原因:
-
初始化时每个卷积核的权重是随机初始化的
-
在学习中不同区域包含不同特征
-
各个卷积核的权重独立
-
卷积层后有激活层,会使得各个卷积核的差异变大
-
-
根据深度学习可视化相关的研究,随着层次的加深,滤波器提取的信息(反映强烈的神经元)也越来越抽象,如同人的理解一般。
-
这是对一般物体识别的8层CNN:
在这个CNN中,第一层的神经元对边缘和斑块响应;第三层的神经元对纹理响应;第5层的神经元对物体部件相应,最后的全连接层对物体的类别有相应。
具有代表性的CNN
特别重要的两个网络:LeNet和AlexNet
LeNet
-
LeNet的网络结构:
其作用是进行手写数字识别 -
LeNet的特点:拥有连续的卷积层和池化层(是只“抽选元素”的子采样层),最后经全连接层输出结果。
-
LeNet与现在的CNN对比:
-
激活函数:LeNet使用的sigmoid激活函数,现在的CNN主要使用ReLU函数
-
池化层:LeNet使用的是子采样缩小中间数据的大小,而现在的CNN主要使用Max池化
-
AlexNet
-
AlexNet的网络结构:
-
AlexNet与LeNet的区别:
-
激活函数使用ReLU函数
-
使用进行局部正规化的LRN(Local Response Normalization)层
-
使用Dropout
-
-
网络结构没有改变太多,更多的是现代的数据量以及算力带来的希望。
深度学习
加深网络
手写数字识别网络的优化与特点
-
用于进行手写数字识别的神经网络结构图:
-
其网络的特点:
-
随着层的加深,通道数变大
-
使用Adam最优化,使用He初始值作为权重初始值
-
在全连接层后使用Dropout层
-
ReLU激活函数和3*3的卷积核大小
-
-
对于比较简单的人物,没有必要使用过于复杂的神经网络将表现力提高到过高程度。
-
提高精度可以使用集成学习、学习率衰减、Data Augmentation(数据扩充)等方法,在手写数字识别中Data Augmentation效果显著。
-
Data Augmentation可以通过各种方法扩充图像,例如crop处理、flip处理等,还有处理外观(改变亮度)、处理尺度(放大缩小)等。
加深层的效果与动机
-
加深层的理论研究不够透彻,但是现实表明一般层越深识别性能越高。
-
加深层可以减少网络的参数数量,同等水平表现力的网络加深层可以用更少的参数达到:
一次5*5的卷积运算可以用两次3*3的卷积运算达到效果
5*5的网络参数有55个,而两次3*3的网络参数有23*3个,且随着层的加深,参数数量差会更大。 -
叠加小型滤波器来加深网路可以减少参数的数量,扩大 感受野(receptive filed,给神经元施加变化的某个局部区域),并且通过叠加层将激活函数夹在中间,还进一步提高了网络的“非线性”表现力。
-
加深层还可以使学习更加高效,逐渐加深的层次对抽象事物的响应逐渐加强,所以可以通过分层次传递信息,将复杂问题简单化,从而到达高效。
深度学习的历史
ImageNet
ImageNet是拥有超过100万张图像的数据集,并且每张图像都被关联了标签。
ImageNet Large Scale Visual Recognition Chanllenge 大规模图像识别大赛
VGG
-
VGG是由卷积层和池化层构成的基础CNN,其特点在于将有权重的层(卷积层或全连接层)叠加至16层(或者19层),具备了深度(根据层的深度,也称为VGG16或VGG19)。
-
在VGG中,基于小型滤波器的运算是连续进行的,即卷积层重叠2至4次,在通过池化层将大小间半,最后经由全连接层输出结果。
GoogLeNet
-
GooLeNet在横向上有宽度,被称为“Inception结构”,并以此结构为基础搭建网络。
GooLeNet结构
-
Inception结构使用了多个大小不同的滤波器(和池化),最后合并他们的结果,GooLeNet就是将Inception结构作为构件(构成元素)。
Inception结构
-
在GooLeNet中,有很多地方都使用了1*1的卷积核的卷积层,其作用是通过在通道方向上减少大小,有助于减少参数和实现高速化处理。
ResNet
-
ResNet通过导入“快捷结构”,使得神经网络不因为过度加深导致学习无法顺利进行,从而有限度地随着层的的加深提高性能。
-
快捷结构跳过了输入数据的卷积层,将输入*合计到输出。
weight layer指卷积层 -
因为快捷结构只是原封不动地传递输入数据,所以反向传播时上游的梯度原封不动地传向下游,所以可以缓解加深层而导致梯度变小的梯度消失问题。
-
ResNet网络结构:
ResNet通过以两个卷积层为间隔跳跃式地连接来加深层,使得网络即使加深到150层以上,识别精度也会持续提高。 -
将学习到的权重数据灵活使用再学习称为迁移学习,迁移学习在数据集较少时非常有效。
深度学习的高速化
-
在深度学习中,卷积运算是耗时的主要部分,这是基于AlexNet网络forward处理的时间比:
-
GPU计算是指基于GPU进行通用的数值计算的操作,其目的是将其极强的计算能力运用在各种用途。
-
CUDA是NVIDIA提供的面向GPU计算的综合开发环境,其中cuDNN库实现了为深度学习最优化过的函数。
-
之所以使用
im2col
函数,是因为im2col
的实现对GPU来说非常方便,GPU更擅长计算大规模汇总数据。 -
为了尽可能缩短学习所需时间,可以使用分布式学习,在多个GPU或者多台机器上进行分布式计算,支持分布式学习的框架有TensorFlow、CNTK(Computational Network Toolki)等,以大型数据中心的低延迟、高吞吐作为支撑。
-
分布式计算效率会在大GPU量下有提升衰减:
-
为了提高训练效率,还可以使用运算精度的数位缩减,使用16位的半精度浮点数,可以代替float和double,并且使用half float识别精度也不会下降(这将是未来很重要的一个课题)。
深度学习的应用
-
物体检测: R-CNN方法
先选取候选区域,然后在提取中的区域应用CNN分类 -
候选区域提取的常用方法有Selective Search,并且最近的Faster R-CNN方法使得高速处理成为可能。
-
图像分割: FCN方法
通过一次forward处理,对所有像素进行分类,对缩小后的图像使用双线性插值法(逆卷积)扩大。 -
图像标题生成: NIC(Neural Image Caption)方法
NIC是由CNN和RNN构成,RNN是呈递归式连接的网络(神经网络会受到之前生成的内容的影响),常被用于自然语言、时间序列数据的连续性数据上。 -
将组合图像和自然语言等多种信息进行的处理称为多模态处理。
深度学习的未来
-
图像风格变换
-
图像的生成: DCGAN方法,通过使用Generator和Discriminator两个神经网络以竞争的方式学习(GAN)提升能力。
-
自动驾驶: SetNet
-
Deep Q-Network(强化学习):使计算机在摸索试验中自主学习,这称为强化学习。
-
强化学习基本框架:
-
DQN(Deep Q-Network/Q学习的强化学习算法)方法:确定最优行动价值函数的函数(通过深度学习CNN)。
神经网络相关问题
权重
-
权重过大或过小会导致梯度爆炸或消失的证明:
首先,考虑一个简单的神经网络层,其输出可以表示为:
\[z = \sum_i w_i x_i + b \]其中,\(w_i\) 是权重,\(x_i\) 是输入,\(b\) 是偏置项,\(z\) 是该层的输出。
激活函数(以sigmoid为例)作用于输出 \(z\) 上,得到该层的激活值:
\[\sigma(z) = \frac{1}{1 + e^{-z}} \]接下来,假设损失函数是激活值 \(\sigma(z)\) 的简单函数(实际上损失函数可能涉及多个层的输出,但这里我们仅考虑一层的情况)。
在反向传播过程中,我们需要计算损失函数 \(L\) 关于权重 \(w_i\) 的梯度,即:
\[\frac{\partial L}{\partial w_i} = \frac{\partial L}{\partial \sigma(z)} \cdot \frac{\partial \sigma(z)}{\partial z} \cdot \frac{\partial z}{\partial w_i} \]\(\dfrac{\partial L}{\partial \sigma(z)}\) 是损失函数关于激活值的梯度,\(\dfrac{\partial \sigma(z)}{\partial z}\) 是激活函数的导数,\(\dfrac{\partial z}{\partial w_i}\) 是输出 \(z\) 关于权重 \(w_i\) 的偏导数。
由于 \(\dfrac{\partial z}{\partial w_i} = x_i\),我们可以将梯度表达式简化为:
\[\frac{\partial L}{\partial w_i} = x_i \cdot \frac{\partial L}{\partial \sigma(z)} \cdot \sigma(z)(1 - \sigma(z)) \]这里,\(\sigma(z)(1 - \sigma(z))\) 是sigmoid激活函数的导数。
现在,考虑权重 \(w_i\) 过大的情况。如果 \(w_i\) 很大,那么输出 \(z\) 也可能很大(假设输入 \(x_i\) 不是非常小)。对于sigmoid激活函数,当 \(z\) 很大时,\(\sigma(z)\) 接近1,而 \(\sigma(z)(1 - \sigma(z))\) 接近0。然而,这并不意味着梯度会很小,因为梯度还受到 \(\frac{\partial L}{\partial \sigma(z)}\) 和 \(x_i\) 的影响。
重要的是,如果损失函数 \(L\) 对于 \(\sigma(z)\) 的变化非常敏感(即 \(\frac{\partial L}{\partial \sigma(z)}\) 很大),那么即使 \(\sigma(z)(1 - \sigma(z))\) 较小,梯度也可能很大。此外,如果输入 \(x_i\) 本身很大,那么它也会放大梯度。
因此,权重过大并不直接导致梯度过大,但它是影响梯度大小的一个因素。梯度的大小取决于多个因素的综合作用,包括权重的大小、输入的大小、激活函数的导数以及损失函数关于激活值的梯度。在实际情况中,这些因素可能相互作用,使得权重过大时梯度的确有可能变得很大,从而导致训练过程中的不稳定性和难以收敛的问题。