第5章深度学习 本章将简要介绍深度学习的一些基本内容和算法,现阶段关于深度学习的书籍或其他资料非常多,因此这里只简要阐述一些常见的神经网络算法。本章的算法和模型实现主要通过PyTorch模块实现。 视频讲解 5.1PyTorch PyTorch模块是由Facebook在深度学习框架Torch的基础上,使用Python重写的一个全新的深度学习框架,不仅继承了NumPy模块的众多优点,同时还支持GPU加速计算,因此其计算效率明显好于NumPy,而且相比于TensorFlow,它的灵活性备受使用者喜欢。PyTorch含有非常丰富的API,可以轻松地完成深度神经网络模型的构建和实现,因此本章节的深度学习内容以PyTorch来实现。 5.1.1PyTorch安装 PyTorch也是Python的一个模块,但其安装方式与之前介绍的内容稍有不同。需要登录官方网站(https://pytorch.org/)下载,它分为不同的系统版本,有PyTorch版本、Python版本、显卡等安装形式。另外,需要根据自己的操作系统和软硬件配置进行搭配,如图5.1所示。 图5.1PyTorch安装界面(实例图) 本书选用了稳定版本(1.0)的PyTorch、Mac OS系统,通过pip安装,Python是3.6版本的,由于MacBook Air不支持CUDA,因此无法进行GPU加速。图5.1给出的安装命令如下所示。 1pip3 install torch torchvision 安装成功后,进入Notebook环境,并执行以下代码来验证是否成功,如图5.2所示。 图5.2验证PyTorch是否成功安装 若安装失败,很可能是选择的安装版本与软硬件不相符造成的。安装成功后可通过以下命令验证是否支持GPU加速。 1torch.cuda.is_available()#输出↓ 2False#表示不支持 若支持GPU加速,在编程期间仅需添加以下命令,即可实现快速计算。 1if torch.cuda.is_available(): 2x = x.cuda()#GPU加速 3y = y.cuda() 5.1.2创建tensor PyTorch与NumPy在操作上非常相似,若读者熟悉NumPy模块的相关操作,那么在短时间内就能掌握并熟练应用PyTorch。在这里简要阐述tensor(张量)的一些基本操作,其代码如下。 1#构建3×2随机浮点数类型张量 2a = torch.FloatTensor(3, 2) 3a#tensor格式输出↓ 4tensor([[ 1.4013e-45, 1.4013e-45], 5[ 3.9883e-22, 1.4013e-45], 6[-1.1426e-14, 4.5911e-41]]) 7a.numpy()#转成数组输出↓ (非常简单) 8array([[ 1.4012985e-45, 1.4012985e-45], 9[ 3.9882707e-22, 1.4012985e-45], 10[-1.1426393e-14, 4.5910742e-41]], dtype=float32) 11#列表转tensor 12b = torch.FloatTensor([1, 0.618]) 13b#输出↓ 14tensor([1.0000, 0.6180]) 15#构建3×2随机整数型 16torch.IntTensor(3, 2)#输出↓ 17tensor([[0, 1342177280], 18[0, 1342177280], 19[5, 0]], dtype=torch.int32) 20#列表转tensor,列表元素为浮点型 21torch.IntTensor([1.2, 3.2])#输出↓ 22tensor([1, 3], dtype=torch.int32) 23#构建2×3 元素全为0 的tensor 24torch.zeros(2, 3)#输出↓ 25tensor([[0., 0., 0.], 26[0., 0., 0.]]) 27#构建3×3 单位矩阵(格式: tensor) 28torch.eye(3, 3)#numpy.eye(3, 3)输出↓ 29tensor([[1., 0., 0.], 30[0., 1., 0.], 31[0., 0., 1.]]) 32#构建3×3元素全为1的矩阵 33torch.ones(3, 3)#输出↓ 34tensor([[1., 1., 1.], 35[1., 1., 1.], 36[1., 1., 1.]]) 37#NumPy 转tensor 转换非常简单直接 38torch.from_numpy(np.array([[1, 2, 3], [3, 4, 5]]))#输出↓ 39tensor([[1, 2, 3], 40[3, 4, 5]]) 5.1.3基本运算 这里对PyTorch中的tensor基本运算进行简要阐述。 1a = torch.randn(2, 3)#随机数 2a#输出↓ 3tensor([[ 0.3786, 0.8703, 0.5232], 4[ 0.2417, -0.7136, -1.2713]]) 5#绝对值|x_{i}| 运算 6torch.abs(a)#输出↓ 7tensor([[0.3786, 0.8703, 0.5232], 8[0.2417, 0.7136, 1.2713]]) 9#加法1,维度要对应一致 10a.add(b)#输出↓ 11tensor([[0.7572, 1.7407, 1.0464], 12[0.4834, 0.0000, 0.0000]]) 13#加法2 14torch.add(a, b)#输出↓ 15tensor([[0.7572, 1.7407, 1.0464], 16[0.4834, 0.0000, 0.0000]]) 17#加法3,b ← a + b 18b.add_(a)#方法含有下画线的.method_ 会更新原有数组输出↓ 19tensor([[0.7572, 1.7407, 1.0464], 20[0.4834, 0.0000, 0.0000]]) 21b#输出↓ (很强大, 方便) 22tensor([[0.7572, 1.7407, 1.0464], 23[0.4834, 0.0000, 0.0000]]) 24#减法 25b.sub_(a)#复原b输出↓ 26b#输出↓ 27tensor([[0.3786, 0.8703, 0.5232], 28[0.2417, 0.7136, 1.2713]]) 29a - b#减法,同a.sub(b) 30#乘法 31a * b#输出↓ 32tensor([[ 0.1433, 0.7575, 0.2738], 33[ 0.0584, -0.5092, -1.6162]]) 34a.mul(b)#乘法,结果同上 35#除法 36a.div(b)#输出↓ 37tensor([[ 1., 1., 1.], 38[ 1., -1., -1.]]) 39a / b#除法,结果同上 40#余数 41a % b#输出↓ 42tensor([[0., 0., 0.], 43[0., 0., 0.]]) 44a.pow(3)#所有元素的3 次方输出↓ 45tensor([[ 0.0543, 0.6593, 0.1432], 46[ 0.0141, -0.3634, -2.0546]]) 从上面的代码不难发现,PyTorch关于基本运算是非常方便的,但不局限于此,比如定义一个简单的分段函数。 f(x)=a,x≤a x,a<x<b b,x≥b (5.1) 其中,a和b是待定常数。下面通过PyTorch来实现这个函数。 1a#tensor输出↓ 2tensor([[ 0.3786, 0.8703, 0.5232], 3[ 0.2417, -0.7136, -1.2713]]) 4torch.clamp(a, 0, 0.5)#取值a = 0, b = 0.5输出↓ 5tensor([[0.3786, 0.5000, 0.5000], 6[0.2417, 0.0000, 0.0000]]) 7#只操作最大值 8torch.clamp_max(a, 0.5)#输出↓ 9tensor([[ 0.3786, 0.5000, 0.5000], 10[ 0.2417, -0.7136, -1.2713]]) 11#只操作最小值 12torch.clamp_min(a, 0)#输出↓ 13tensor([[0.3786, 0.8703, 0.5232], 14[0.2417, 0.0000, 0.0000]]) 15torch.clamp??#查看命令的用法输出↓ (notebook or ipython) 16Docstring: 17clamp(input, min, max, out=None) -> Tensor 18 19Clamp all elements in :attr:'input' into the range '[' :attr:'min', :attr:'max' ']' and return 20a resulting tensor: 21... 除了以上基本运算,还有很多其他的运算,这里不再一一阐述,下面阐述矩阵运算。 5.1.4矩阵运算 1A = torch.Tensor([[1, 2, 3]])#构建1x3矩阵 2B = torch.Tensor([[1, 2, 3], [7, 8, 9]])#构建2x3矩阵 3A.shape#1行3列输出↓ 4torch.Size([1, 3]) 5B.shape#2行3列输出↓ 6torch.Size([2, 3]) 7#矩阵乘法AB^{T} A: 1×3 B^{T}: 3×2 8A.mm(B.reshape(3, 2)) #(同torch.matmul(A, B.reshape(3, 2)))输出↓ 9tensor([[31., 43.]]) 10#矩阵与向量的乘法Ax : mv(顺序不要乱): matrixvector 11torch.mv(B, A[0])#输出↓ 12tensor([14., 50.]) 13#方阵求逆 14c = torch.randn(3, 3)#构建3×3 矩阵 15c#输出↓ 16tensor([[ 0.2634, 0.8859, -0.5662], 17[-0.5709, -0.8612, -0.3837], 18[-0.6534, -0.8777, 2.2320]]) 19c.inverse()#矩阵c 的逆输出↓ 20tensor([[-2.8565, -1.8718, -1.0465], 21[ 1.9283, 0.2755, 0.5366], 22[-0.0779,-0.4396,0.3527]]) 23#行列式 24c.det()#输出↓ 25tensor(0.7908) 26#验证逆矩阵乘法AA^{-1} = I 27torch.matmul(c.inverse(), c)#输出↓ 28tensor([[ 1.0000e+00, -1.1921e-07, 4.7684e-07], 29[ 0.0000e+00, 1.0000e+00, 0.0000e+00], 30[-3.1692e-10, -1.5118e-08, 1.0000e+00]]) 31#构建一个2x4x4 张量 32d = torch.randn(2, 4, 4) 33d#输出↓ 34tensor([[[-0.0879, -0.6460, -0.1376, -1.2186], 35[-0.9019, 1.1118, -0.0460, 2.1199], 36[-1.2170, 0.0621, -1.1434, -1.1737], 37[-1.3173, 0.6776, 0.2788, 0.4723]], 38 39[[ 0.2511, -0.1904, -0.5661, -0.4801], 40[-1.0341, -1.4300, 0.2876, 0.6154], 41[-0.6928, 1.4691, -0.7081, -0.2806], 42[-0.7519, -1.2448, 1.1138, -0.0593]]]) 43d.shape#输出↓ 44torch.Size([2, 4, 4]) 45d[0]#切片输出↓ 46tensor([[-0.0879, -0.6460, -0.1376, -1.2186], 47[-0.9019, 1.1118, -0.0460, 2.1199], 48[-1.2170, 0.0621, -1.1434, -1.1737], 49[-1.3173, 0.6776, 0.2788, 0.4723]]) 关于PyTorch更多的内容,读者可以通过相关书籍或官方文档进行深入学习,这里不再大篇幅的详细介绍。 5.2基础知识 在介绍神经网络之前,需要先对相关的思想方法和数学知识进行了解,因此需要补充一些相关的数学知识,以便能更好地理解后面的内容。 5.2.1蒙特卡洛法 蒙特卡洛法也称统计模拟方法,曾被评为20世纪最美的算法之一。本书在前面介绍过关于蒙特卡洛法的实例(求π的近似值),其思想就是通过模拟参数(采样)来求解一个复杂问题的(相对)最优解。通常模拟(采样)数量越多越接近最优解,在关于线性回归的章节中通过理论推导出参数的值(精确值),但是一个含有很多变量的函数就很难处理,蒙特卡洛法可以在一定程度上解决这个问题。举个简单的例子,给定一个一元二次函数f(x)=x2,并添加随机噪声,其结果如图5.3所示。 图5.3f(x)函数和添加随机噪声后的散点图 图5.3中的散点序列{(xi,yi)}具有非常直观的现象。现在若想构造一个函数来描述这些散点,进而逼近原函数。众所周知: 任何形式的曲线都可以通过多项式去逼近,问题在于如何构造一个合适的函数。对于一个变元问题,构建函数次数的高低会影响模型的性能: 构建函数的次数低,易造成模型的欠拟合问题; 构建函数的次数高,易造成模型的过拟合问题。 针对散点序列{(xi,yi)}(i=1,2,…,m),理论上希望散点尽可能地都落在已构造的函数上,如式(5.2)所示。 yi=a0+a1xi+a2x2i+…+anxni+εi(5.2) 其中,aj(j=0,1,2,…,n)是待定系数,εi是扰动项。通过观察散点图,不妨构造如下函数(一元二次函数)。 f^(xi)=ax2i+bxi+c(5.3) 其中,f^是拟合函数,a,b,c是待定系数(需要求解的)。通过图形和函数性质分析,不难推测出系数a>0,b和c均比较小,这样做的目的是可以在生成随机数组时进行剔除不满足条件的数,下面的实例中暂不考虑这种情况。 根据式(5.3),其损失函数如下所示。 J(a,b,c)=1m∑mi=1(f^(xi)-yi)2(5.4) 要满足J(a,b,c)(代价函数,又称损失函数)最小,即J(a*,b*,c*)=min J(a,b,c),其中(a*,b*,c*)是待求的 “最佳系数组”。式(5.4)可通过理论进行推导,现在并不打算这样做,先仅通过蒙特卡洛法来生成q个(视情况而定)随机数组 (ar,br,cr)(r=1,2,…,q),以遍历的形式(或并行计算)代入式(5.4)求解J的值,在q个结果中筛选出最小值J对应的数组(ar,br,cr),即求解的待定系数组(a*,b*,c*)。 通过以上理论分析,构造的式(5.3)针对散点序列 {(xi,yi)}结合蒙特卡洛法进行求解的代码如下所示。 1import numpy as np 2import torch 3from torch.autograd import Variable 4import matplotlib.pyplot as plt 5%matplotlib inline 6x = np.linspace(-3, 3, 30)#x 轴数据 7f = x ** 2#原函数 8f_noise = f + np.random.randn(x.__len__())#添加噪声后的数据 9#定义拟合函数 10def f_prob(x, a, b, c): 11return a * x ** 2 + b * x + c 12x_tensor = torch.from_numpy(x)#array2tensor 13f_tensor = torch.from_numpy(f_noise)#array2tensor 14tmp_result_dict = {}#创建存储计算结果字典 15for _ in range(100000):#这里模拟100000 次 16arg_value = torch.randn(3)#生成随机数 17#不含1/m 的J 函数 18J = (f_prob(x_tensor, *arg_value) - f_tensor).pow(2).sum().numpy() 19tmp_result_dict[str(arg_value.tolist())] = J 20loss_min_label = min(tmp_result_dict, key=tmp_result_dict.get) #最小J值对应的待定系数组 21tmp_result_dict[loss_min_label]#输出↓ 22array(14.71679089)#差值平方和(14.71679089/30)相当小,在可接受范围内 23#最佳系数组元素转换成数值类型str2float 24best_args_list = [round(float(i),4) for i in loss_min_label.strip('[]').split(',')] 25print(best_args_list)#输出↓ 26[1.015, 0.1198, 0.0314] 通过100000次模拟可得到一个相对最佳的解,即a=1.015,b=0.1198,c=0.0314。代入构造拟合函数(式(5.3))可得f(x)=1.015x2+0.1198x+0.0314。将数据代入函数对其进行绘图,结果如图5.4所示。 图5.4拟合函数与原函数对比图 实现图5.4的代码如下所示。 1f_fitting = f_prob(x_tensor, *best_args_list) 2fig = plt.figure(figsize=(7, 5)) 3plt.plot(x, f, 'r-', label='original')#函数图像 4plt.scatter(x, f_noise)#噪声点 5plt.plot(x, f_fitting.numpy(), 'g-.', label='fitting') #拟合函数 6plt.legend() 7plt.xlabel('$x$') 8plt.ylabel('$f(x)$') 通过蒙特卡洛法求解出一个相对最佳的结果,但是通过100000次模拟参数数组去计算的,尽管现在的计算机计算力比较强,但是相对于研究数学的人而言这并非首选方法,原因有以下几点: 尽管复杂度不大,但是计算量大; 求解的结果仅是一种近似解,且与模拟次数有很大的关系; 数学研究志在求解精确解,即使是一种近似,理论上也要确定误差范围。 读者可以尝试构造一元一次函数或一元高次(大于2)函数进行实验,看效果如何。若通过蒙特卡洛法求解多元高次函数,且在待定系数取值范围不好确定的情况下,为求解最佳待定系数需要模拟的次数规模是一件可怕的事情,显然这不是最佳方法,但是在求解极其复杂的问题时,蒙特卡洛法不失为一种有效的方法。 5.2.2梯度下降法 在前面章节过介绍过牛顿法(求解方程的根),这里再介绍另一种算法: 梯度下降法。仍然以蒙特卡洛法中的实例,针对式(5.4)定义的J(a,b,c),先计算关于a,b,c的导数。 J(a,b,c)a=2m∑mi=1(ax2i+bxi+c-yi)x2i J(a,b,c)b=2m∑mi=1(ax2i+bxi+c-yi)xi J(a,b,c)c=2m∑mi=1(ax2i+bxi+c-yi) (5.5) 根据式(5.5)构造梯度J(a,b,c)=J(a,b,c)a,J(a,b,c)b,J(a,b,c)c。不妨令θ=(a,b,c),则梯度下降法的迭代式为 θn=θn-1-ηJ(θn-1) (5.6) 其中,η(称为学习率)是指定常数,通常的范围为(0,1),θ0是给定的初值,θn是第n次迭代后的值(n=0,1,2,…)。 用梯度下降法极小化J(a,b,c)代码如下所示。在这个例子中,η=0.05。 1#梯度函数 2def gradient(x, y, a, b, c): 3x_len = len(x) 4tmp_value = a * x.pow(2) + b * x + c - y#tensor计算 5ja = 2 / x_len * (tmp_value * x.pow(2)).sum().numpy() 6jb = 2 / x_len * (tmp_value * x).sum().numpy() 7jc = 2 / x_len * tmp_value.sum().numpy() 8return torch.Tensor([ja, jb, jc]) 9#给定一组初值系数 10coeff_value = torch.Tensor([1, 1, 1]) 11loss_result_list = []#存储损失函数值 12for ix in range(20): 13coeff_value = coeff_value - 0.05 * gradient(x_tensor, f_tensor, *coeff_value) 14loss_value = (f_prob(x_tensor, *coeff_value) - f_tensor).pow(2).sum().numpy() 15loss_result_list.append(round(float(loss_value), 2)) 16print("迭代步数:{0}, 系数数组: {1}, 损失函数值: {2:.2f}".format(ix + 1, coeff_value,loss_value))#打印输出↓ 17迭代步数:1, 系数数组: tensor([0.7178, 0.7044, 0.9046]), 损失函数值: 76.06 18迭代步数:2, 系数数组: tensor([0.9878, 0.5037, 0.9092]), 损失函数值: 52.66 19迭代步数:3, 系数数组: tensor([0.7573, 0.3673, 0.8268]), 损失函数值: 40.66 20迭代步数:4, 系数数组: tensor([0.9793, 0.2746, 0.8266]), 损失函数值: 34.08 21迭代步数:5, 系数数组: tensor([0.7910, 0.2117, 0.7552]), 损失函数值: 30.16 22迭代步数:6, 系数数组: tensor([0.9737, 0.1689, 0.7513]), 损失函数值: 27.57 23迭代步数:7, 系数数组: tensor([0.8199, 0.1399, 0.6892]), 损失函数值: 25.71 24迭代步数:8, 系数数组: tensor([0.9703, 0.1202, 0.6826]), 损失函数值: 24.27 25迭代步数:9, 系数数组: tensor([0.8448, 0.1068, 0.6285]), 损失函数值: 23.10 26迭代步数:10, 系数数组: tensor([0.9686, 0.0977, 0.6200]), 损失函数值: 22.12 27迭代步数:11, 系数数组: tensor([0.8663, 0.0915, 0.5727]), 损失函数值: 21.28 28迭代步数:12, 系数数组: tensor([0.9683, 0.0873, 0.5629]), 损失函数值: 20.55 29迭代步数:13, 系数数组: tensor([0.8849, 0.0844, 0.5214]), 损失函数值: 19.92 30迭代步数:14, 系数数组: tensor([0.9689, 0.0825, 0.5108]), 损失函数值: 19.36 31迭代步数:15, 系数数组: tensor([0.9011, 0.0812, 0.4742]), 损失函数值: 18.86 32迭代步数:16, 系数数组: tensor([0.9704, 0.0803, 0.4632]), 损失函数值: 18.43 33迭代步数:17, 系数数组: tensor([0.9151, 0.0797, 0.4309]), 损失函数值: 18.04 34迭代步数:18, 系数数组: tensor([0.9723, 0.0793, 0.4197]), 损失函数值: 17.69 35迭代步数:19, 系数数组: tensor([0.9274, 0.0790, 0.3912]), 损失函数值: 17.39 36迭代步数:20, 系数数组: tensor([0.9746, 0.0788, 0.3800]), 损失函数值: 17.11 37#迭代后计算出的最佳系数 38coeff_value#输出↓ 39tensor([0.9746, 0.0788, 0.3800]) 使用最后一次迭代(第20次,这里步长η=0.05)计算的系数作为最佳的系数组代入函数,其结果如图5.5所示。 图5.5梯度下降法迭代20次的拟合函数(gradfitting)和原函数(original)图形 相应的损失函数值关于迭代步骤的曲线如图5.6所示。 图5.6损失函数迭代图 图5.5和图5.6的代码实现如下所示。 1#拟合函数图 2fig = plt.figure(figsize=(7, 5)) 3plt.plot(x, f, 'r-', label='original')#函数图像 4plt.scatter(x, f_noise)#噪声点 5plt.plot(x, f_prob(x_tensor, *coeff_value).numpy(), 'g-.', label='gradfitting') #拟合函数 6plt.legend() 7plt.xlabel('$x$') 8plt.ylabel('$f(x)$') 9#损失函数迭代图 10plt.plot(list(range(1, len(loss_result_list) +1)),loss_result_list, 'r-^') 11plt.xlabel("迭代步数") 12plt.ylabel("损失函数值") 这里简单讨论蒙特卡洛法和梯度下降法。通过以上实例不难看出,蒙特卡洛法更像是以 “猜”或 “试”的方法来找最值,但前提要确定待求系数的取值范围,这样才能保证 “猜”的可靠性 .梯度下降法是每次迭代时都找一个 “最快 ”的方式来更新上一步的计算(给定)的待求系数,因此梯度下降法在计算成本上更有优势。 这里将所有的散点序列{(xi,yi)}在每次迭代过程中都进行考虑,若每次随机采样(重复采样)来进行梯度计算,称为随机梯度下降法(stochastic gradient descent,SGD)。随机梯度下降法可以有效抑制过拟合问题,关于随机梯度法的内容不再阐述,读者可以查阅相关资料了解随机梯度法的相关定义和内容。除此之外,还有批量梯度下降法(batch gradient descent,BGD)和小批量梯度下降法(minibatch gradient descent,MBGD)。 5.2.3封装实现 前面通过自编代码来实现模型的构建和计算,那通过PyTorch模块如何实现呢?这里给出其对应的代码。 1x = x_tensor.float()#自变量 2y = torch.from_numpy(f_noise).float()#因变量 3#定义拟合函数 4def f_prob(x, w): 5return w[0]*x ** 2 + w[1] * x + w[2] 6#初始化参数 7w = torch.ones(3, requires_grad=True) 8#损失函数 9criterion = torch.nn.MSELoss() 10#优化函数 11optimizer = torch.optim.SGD([w,],lr=0.05)#随机梯度下降法学习率0.05 12#遍历 13for iter_n in range(20): 14loss = criterion(f_prob(x, w), y) 15optimizer.zero_grad()#初始化梯度 16loss.backward() 17optimizer.step() 18if iter_n % 2 == 0: 19print("迭代步数",iter_n, '损失值', loss.data, "参数变化", w.data) #输出↓ 20迭代步数0 损失值tensor(4.1230) 参数变化tensor([0.7178, 0.7045, 0.9046]) 21迭代步数2 损失值tensor(1.7554) 参数变化tensor([0.7573, 0.3673, 0.8268]) 22迭代步数4 损失值tensor(1.1360) 参数变化tensor([0.7910, 0.2117, 0.7552]) 23迭代步数6 损失值tensor(0.9190) 参数变化tensor([0.8199, 0.1399, 0.6892]) 24迭代步数8 损失值tensor(0.8090) 参数变化tensor([0.8448, 0.1068, 0.6285]) 25迭代步数10 损失值tensor(0.7373) 参数变化tensor([0.8663, 0.0915, 0.5727]) 26迭代步数12 损失值tensor(0.6851) 参数变化tensor([0.8849, 0.0844, 0.5214]) 27迭代步数14 损失值tensor(0.6452) 参数变化tensor([0.9011, 0.0812, 0.4743]) 28迭代步数16 损失值tensor(0.6142) 参数变化tensor([0.9151, 0.0797, 0.4310]) 29迭代步数18 损失值tensor(0.5898) 参数变化tensor([0.9274, 0.0790, 0.3912]) 将计算后的结果系数(参数) w=[0.9274,0.0790,0.3912]代入公式,并绘制图片,其结果如图5.7所示。 图5.7拟合函数与原函数对比图 这里通过19次迭代来求解参数,因此最终结果与上面代码运算的结果略有差别,但只要迭代次数足够大,其结果差异性就会很小。 5.2.4激活函数 激活函数(activation function)又称非线性函数(nonlinear function)。激活函数是类似于神经元与神经元传递信息时的一种信息处理手段。激活函数具有以下几种性质: ①非线性; ②单调性; ③值域有界性。 这里介绍3个常见的激活函数: sigmiod、tanh和ReLU。 1. 常见的激活函数 1) sigmoid g(x)=11+e-x (5.7) 2) tanh tanh(x)=ex-e-xex+e-x (5.8) 3) ReLU relu(x)=0x≤0 xx>0 (5.9) 通过Python对以上3个函数进行绘图,其代码如下所示,图像如图5.8所示。 1#sigmoid 函数 2def g(x): 3return 1 / (1 + torch.exp(-x)) 4#tanh 激活函数 5def tanh(x): 6a1 = torch.exp(x) 7a2 = torch.exp(-x) 8return (a1 - a2) / (a1 + a2) 9#ReLU激活函数 10def relu(x): 11return torch.clamp_min(x, 0) 12 13x = torch.linspace(-5, 5, 100) 14fig, axes = plt.subplots(1, 3, figsize=(12, 5)) 15axes[0].plot(x.numpy(), g(x).numpy(), 'r-.', label='sigmoid') 16axes[1].plot(x.numpy(), tanh(x).numpy(), 'g', label='tanh') 17axes[2].plot(x.numpy(), relu(x).numpy(), 'b', label='relu') 图5.8激活函数图 2. 激活函数的优缺点 以上3个激活函数都有各自的优缺点,这里对其简要介绍。 1) sigmiod函数 优点: 值域为(0,1),单调连续,优化稳定; 易求导。 缺点: 易造成计算的梯度消失,从而导致训练出现问题; 其输出不是以0为中心(zerocentered)。 2) tanh函数 优点: 相比于sigmiod函数,其收敛速度更快; 其输出是以0为中心。 缺点: 由于饱和性产生的梯度消失(同sigmiod函数)。 注意: 对于函数f(x),当x→-∞时,其导数f′(x)→0,则称为左饱和,相应地,还有右饱和。若左右都满足饱和,称为两端饱和。 3) ReLu函数 优点: 计算复杂度低,不需要进行指数运算; 收敛速度比sigmiod和tanh快。 缺点: 输出不是以0为中心; 不会对数据做幅度压缩,因此数据幅度会随着模型层数的增加不断扩大。 除了以上函数、还有ELU、SELU、Threshold、PReLU和Leakly ReLU等激活函数,这里不再一一阐述。 5.2.5softmax softmax函数又称归一化指数函数,是逻辑函数中的一种。Softmax函数本质上是对有限项离散概率分布的梯度对数归一化https://zh.wikipedia.org/wiki/softmax函数。。softmax函数在人工神经网络的多分类问题中有着非常广泛的应用。其意图是将一个含有任意实数的k维向量z,映射到另一个k维实向量中,但其元素范围为(0,1),k个元素的和为1。 σ(z)j=ezj∑ki=1ezi (5.10) 其Python代码实现非常简单,如下所示。 1#导入模块包 2import NumPy as np 3#构建数组 4z = np.array([1.0, 2.0, 1.0, 5.0, 6.0]) 5print(np.exp(z) / sum(np.exp(z))) 视频讲解 5.3前馈神经网络 5.3.1思想原理 人工神经网络(artificial neural network,ANN)简称神经网络https://zh.wikipedia.org/wiki/人工神经网络。。人工神经网络有很多类型,这里仅阐述一些经典的方法。下面先介绍全连接神经网络,其简单的流程如图5.9所示。 图5.9全连接神经网络 如图5.9所示,这是一个全连接神经网络(fully connected neural network),全连接神经网络是仅由全连接层组成的前馈神经网络。图5.9含有输入层、隐藏层(2个)和输出层,这里有3个特征xi(i=0,1,2)和1个常数项b,存在一个权重向量w=(w0,w1,w2),记 f(w1,x)=w0x0+w1x1+w2x2+b=∑2i=0wixi+b(5.11) 式(5.11)是第一个隐藏层(含有 3个神经元),第一个隐藏层到第二个隐藏层传递之前,需要通过激活函数h(x)计算h(f(w1,x)),然后作为“输入元 ”再进行类似于式(5.11)的处理g(w4,h(f(wi,x))),最终通过输出层来计算出输出结果。 前馈神经网络有以下特点: 每层含有不同数量的神经元,同层之间的神经元无连接; 相邻两层之间的神经元全部两两连接; 整个网络中无反馈环节,可以理解为一个有向无环图。 5.3.2手写体识别实例 这里通过经典数据集——手写体(MNIST)数据集来进行实验,不妨先随机取出 6个样本进行观察,其图像如图5.10所示。 图5.106个样本的图像数据 MNIST数据集是一个手写体数据集,共有10分类标签,每一个样本的标签值为0~9的一个数。训练集有60000个样本,测试集有10000个样本,每个样本都可以看成28×28的矩阵,因此将其展平成1×784向量,即784个变元。 下面通过PyTorch模块对其进行神经网络训练,先导入模块以及基本参数设置。 1#模块导入 2import torch 3import torch.nn as nn 4import torchvision 5import torchvision.transforms as transforms 6#基本参数设置 7input_size = 784#变量数量 8num_calsses = 10#分类数目 9num_epochs = 5#迭代次数 10batch_size = 100#训练批次 11learning_rate = 1e-3#学习率 torchvision模块作为PyTorch的辅助模块,不仅包含经典的深度学习模型,而且含有常见的数据集,现在下载并导入MNIST数据集。 1data_path = '../data/MNIST'#在data 路径下创建MNIST 文件 2#训练集若没有则下载 3train_dataset = torchvision.datasets.MNIST( 4root=data_path,#数据集路径 5train=True,#是否为训练集 6transform=transforms.ToTensor(),#将数据张量化处理 7download=True)#进行下载 8#测试集 9test_dataset = torchvision.datasets.MNIST( 10root=data_path, 11train=False, 12transform=transforms.ToTensor(), 13download=True) 14#构建训练数据加载器 15train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 16batch_size=batch_size, 17shuffle=True) 18#测试数据加载器 19test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 20batch_size=batch_size, 21shuffle=False) 通过PyTorch模块进行数据处理时,该数据集的输入格式都已处理好,其中train_loader为数据加载器,它可以很好地进行批处理运算。通过PyTorch先搭建一个极简的全连接网络,即输入层到输出层。 1#线性模型 2model = nn.Linear(input_size, num_calsses)#输出↓ 3Linear(in_features=784, out_features=10, bias=True) 这是一个非常简单的模型,即输入784个参数和1个偏置项(bias),输出为10个分类数目。截止到目前,关于该数据集的预处理和模型构造已完成,下面开始进行模型训练。 1#损失函数 2#nn.CrossEntropyLoss() 内部集成了softmax函数 3criterion = nn.CrossEntropyLoss() 4#优化方式: 随机梯度下降法 5optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate) 6#训练模型: 遍历 7for epoch in range(num_epochs): 8for i, (images, labels) in enumerate(train_loader): 9#转换数据格式 10images = images.reshape(-1, 28 * 28)#28x28 → 1x784 11#前向传播 12outputs = model(images)#导入数据,输出计算结果值 13loss = criterion(outputs, labels)#代价函数(损失函数) 14#反向传播及优化 15optimizer.zero_grad()#初始化梯度 16loss.backward()#计算代价函数的损失函数 17optimizer.step()#更新梯度 18#打印部分日志 19if (i + 1) % 300 == 0: 20print("Epoch: {0},Step: {1},loss: {2}".format(epoch + 1,i+1,loss.item())) 21#输出↓ 22Epoch: 1, Step: 300, loss: 1.999853491783142 23Epoch: 1, Step: 600, loss: 1.8600431680679321 24Epoch: 2, Step: 300, loss: 1.6646180152893066 25Epoch: 2, Step: 600, loss: 1.4261541366577148 26Epoch: 3, Step: 300, loss: 1.3930109739303589 27Epoch: 3, Step: 600, loss: 1.3381340503692627 28Epoch: 4, Step: 300, loss: 1.1542218923568726 29Epoch: 4, Step: 600, loss: 1.2540661096572876 30Epoch: 5, Step: 300, loss: 1.0619515180587769 31Epoch: 5, Step: 600, loss: 0.9185544848442078 下面通过测试集来验证模型的训练情况,其代码如下所示。 1#PyTorch 默认每一次前向传播都会计算梯度 2with torch.no_grad(): 3correct = 0#初始化正确数量 4total = 0#初始化总数 5for images, labels in test_loader: 6images = images.reshape(-1, 28 * 28)#测试集格式 7outputs = model(images)#代入训练好的模型 8_, predicted = torch.max(outputs.data, 1)#取最大值 9total += labels.size(0) 10correct += (predicted == labels).sum()#正确数 11print('Accuracy of the model on the 10000 test images:{}%'.format(100*correct/total)) 12#输出↓ 13Accuracy of the model on the 10000 test images: 83 % 这里通过这个全连接神经网络模型可以获得83%的准确率。从其代码结构上不难发现是非常简洁的,易读性很高。若读者对其感到生疏,建议查阅官方的PyTorch文档进行学习。 有时候构建的神经网络比较复杂,建议继承torch.nn.Module类来构建神经网络,其代码构建是非常方便的。 1#导入模块 2import torch.nn as nn 3import torch.nn.functional as F 4#构建一个MnistNet 类 5class MnistNet(nn.Module): 6def __init__(self, input_size, hidden_size, num_classes): 7super(MnistNet, self).__init__() 8self.fc1 = nn.Linear(input_size, hidden_size) 9self.fc2 = nn.Linear(hidden_size, num_classes) 10def forward(self, x): 11x = self.fc1(x)#全连接 12x = F.relu(x)#ReLU激活函数 13x = self.fc2(x)#全连接 14return x#输出结果 以上自定义类都做了哪些工作?下面简要阐述。 导入必要的模块; 构建名为MnistNet的类,并继承torch.nn.Module类; 定义两个线性函数fc1,fc2; 重构前馈神经网络函数forward; 前馈神经网络中先对输入数据通过第一个线性函数线性计算,然后通过激活函数ReLU处理,再通过第2个线性函数进行线性计算,将最后的计算结果输出。 现在将对象实例化,其代码如下所示。 1model1 = MnistNet(784, 20, 10)#输入层: 784, 隐含层: 20个神经元, 输出层:10 2model1#输出↓ 神经网络结构 3MnistNet( 4(fc1): Linear(in_features=784, out_features=20, bias=True) 5(fc2): Linear(in_features=20, out_features=10, bias=True) 6) 构建相应的损失函数和梯度下降法,并对模型进行训练,其代码如下所示。 1criterion = nn.CrossEntropyLoss() 2optimizer1 = torch.optim.SGD(model1.parameters(), lr=learning_rate) 3#训练模型 4total_step = len(train_loader) 5for epoch in range(num_epochs): 6for i, (images, labels) in enumerate(train_loader): 7#转换数据格式 8images = images.reshape(-1, 28 * 28) 9#前向传播 10outputs = model1(images) 11loss = criterion(outputs, labels) 12#反向传播及优化 13optimizer1.zero_grad() 14loss.backward() 15optimizer1.step() 16#打印部分日志 17if (i + 1) % 300 == 0: 18print("Epoch: {0},Step: {1},loss: {2}".format(epoch+1,i+1,loss.item()) 这里不再给出打印结果,不难看出PyTorch模块搭建神经网络是非常系统和简洁的。这也是PyTorch模块发行后备受欢迎的主要原因之一。 视频讲解 5.4卷积神经网络 卷积神经网络(convolutional neural network,CNN)是一种具有局部连接、权重共享等特性的深层前馈神经网络。CNN算法主要用于图像处理问题,因为图像可以看成含有多个颜色通道的二维矩阵以及特征处理过程中需要满足视野域的概念,但也可以用于一维数据。卷积的主要功能是在给定的图像上滑动一个卷积核(卷积核函数,又称滤波器),通过卷积计算后获得一组新的特征。 全连接前馈神经网络中,其权重矩阵的参数非常多致使训练效率低下。 5.4.1核函数 介绍卷积神经网络之前,非常有必要介绍卷积核函数。卷积核函数有很多种,这里介绍5种常见的3×3卷积核函数。 1. identity identity卷积核是非常简单的。 kernel= 000 010 000 (5.12) 该卷积核函数不改变图像的任何元素,不妨给定一个图像(单渠道或灰度)的矩阵I,如下所示。 I= 800210003 0051420030 0301300030 0063013372 016018701130 0001170011 1260200701915 20500121408 0419004500 (5.13) 对矩阵I进行卷积函数处理时,其计算方式是非常简单的(暂不考虑边界元素),Ii=1,j=1的分块矩阵 800 005 030 (5.14) Ii=1,j=1与卷积核对应元素相乘后再相加: 0×8+0×0+0×0+0×0+1×0+0×5+0×0+0×3+0×0=0 即通过卷积核kernel函数在图像Ii=1,j=1位置的值为0。 这里给定一个图像,其图像读取效果如图5.11所示。 图5.11原图(源于MATLAB内置图像) 以上图像是一个含有3个渠道(R、G、B)的256×256的方形图像,其代码如下所示。 1#模块 2import numpy as np 3import torch 4from PIL import Image, ImageShow 5from torchvision import transforms 6import matplotlib.pyplot as plt 7%matplotlib inline 8#读取图像 9path = "../chap3/girl.png" 10girl_pic = Image.open(path) 11girl_pic#输出↓ 将以上图像转换成数组,这里通过NumPy模块的array成员函数将图像数据转换成数组类型。 1np.array(girl_pic).shape#输出↓ 2(256, 256, 3) 3type(girl_pic)#输出↓ 4PIL.PngImagePlugin.PngImageFile 5#数据(width, height, channel) 转换成(channel, width, height) 6data2channel_tensor = transforms.functional.to_tensor(np.array(girl_pic)) 7data2channel_tensor.shape#输出↓ 8torch.Size([3, 256, 256]) 下面通过卷积核函数kernel1对图像的矩阵I做处理,其代码如下。 1kernel_tensor = torch.FloatTensor(3,3,3) 2for i in range(3): 3#核函数 4kernel_tensor[i] = torch.Tensor([[0,0,0],[0,1,0],[0,0,0]]) 5 6#构建一个等同, 元素全为0 的数组 7new_data_tensor = torch.zeros_like(data2channel_tensor) 8c, m, n = data2channel_tensor.size()#渠道, 宽, 高 9for layer in range(3):#遍历渠道 10tmp_data = data2channel_tensor[layer] 11kernel = kernel_tensor[layer] 12#遍历计算 13for i in range(1, m-1): 14for j in range(1, n-1): 15ele_data = tmp_data[i-1:i+2,j-1:j+2] 16#计算值结果 17result_value = float((ele_data * kernel.float()).sum().numpy()) 18new_data_tensor[layer][i, j] = result_value 2. edge detection 边缘检测(edge detection)卷积核矩阵有很多形式,常见的3种形式如下所示。 10-1 000 -101 (5.15) 010 1-41 010 (5.16) -1-1-1 -18-1 -1-1-1 (5.17) 通过以上3种边缘检测卷积核矩阵对图像的矩阵I分别处理,步长设置为1,其结果如图5.12所示。 图5.12边缘检测卷积核函数处理结果 通过图5.12不难看出,其边缘轮廓的差异非常明显。 3. sharpen 锐化(sharpen)卷积核函数如下所示。 0-10 -15-1 0-10 (5.18) 其结果如图5.13所示。 图5.13锐化卷积核函数处理结果 4. box blur 盒子模糊(box blur)是将3×3的矩阵元素全部设置为19,可以看作一种简单的加权平均方式,其卷积和函数如下所示。 191919 191919 191919 (5.19) 关于其效果图这里不再表述,因为其效果图和下面要阐述的高斯模糊比较相近。 5. Gaussian blur 高斯模糊(Gaussian blur)是一种比较常见的模糊处理方法,它考虑到了距离问题,距离中心元素最近的4个元素全部为216,剩余的4个角元素全部为116,中心元素数值为416。 116216116 216416216 116216116 (5.20) 通过高斯模糊处理好的图像相比于原图有一定的模糊,但是高斯模糊可以在一定程度上有效降低高斯噪声,其图像如图5.14所示。 图5.14高斯模糊卷积核处理结果 除了以上5种常见的3×3卷积核函数,还有其他类似的5×5和7×7等卷积核函数。读者可以自行查阅相关资料深入了解和学习。 5.4.2池化层 除了上面的卷积核函数和激活函数,卷积网络中另一个重要的结构是池化层(pooling)。其本质是将图片变小,达到一种降维的目的,从而有效提高计算效率,并能保留原图像相应的特征。池化层没有任何参数,易于实现,其形式也有多种,比如最大值池化、最小值池化、均值池化等,并且其尺寸大小以及步长可进行人为调整或设置。卷积网络中采用最大池化层来做处理。 5.4.3LeNet LeNet神经网络由深度学习三巨头之一的 Yan LeCun提出,该方法标志着 CNN的真正面世。 LeNet主要用于手写体的识别与分类,并在美国银行得到广泛应用。由于当时计算机硬件不佳,以及缺少大规模的训练集,致使 LeNet模型在处理较复杂的问题时不太理想,但是其思想方法非常值得借鉴。LeNet神经网络结构如图5.15所示。 图5.15LeNet神经网络结构 根据图5.15的神经网络,不难看出它涉及2个卷积层和2个全连接层,其PyTorch代码也是非常简单的,如下所示。 1#导入模块 2import torch.nn as nn 3import torch.nn.functional as F 4#构建一个LeNet 类 5class LeNet(nn.Module): 6def __init__(self): 7super(LeNet, self).__init__() 8self.tmp_dict = {} 9#3: 输入图片单通道, 6: 输出通道数, 5: 卷积核为5 * 5 10self.conv1 = nn.Conv2d(3, 6, 5) 11self.conv2 = nn.Conv2d(6, 16, 5) 12#全连接层y = wx + b 13self.fc1 = nn.Linear(16 * 5 * 5, 120) 14self.fc2 = nn.Linear(120, 84) 15self.fc3 = nn.Linear(84, 10) 16def forward(self, x): 17#卷积-> 激活-> 池化 18x = self.conv1(x)#5×5卷积核 19x = F.relu(x)#ReLU激活函数 20x = F.max_pool2d(x, 2)#最大池化层2×2 21x = self.conv2(x)#卷积6个神经元扩展到16个神经元,5×5卷积核 22x = F.relu(x)#激活函数 23x = F.max_pool2d(x, 2)#2×2池化层#S4 24x = x.view(x.size()[0], -1)#数据展平C5 25x = F.relu(self.fc1(x))#线性模型1 26x = F.relu(self.fc2(x))#线性模型2 27return self.fc3(x)#输出层 现在根据以上构建的神经网络代码,对LeNet算法进行阐述: 输入层: 含有3个颜色通道的图像大小为32 ×32; C1层: 6个5×5的卷积核,即6组28×28特征映射结果; S2层: 使用最大池化层2×2,生成6组14×14个神经元; C3层: 由6组扩展到16组大小为10×10的特征映射,这里采用了部分连接(非全连接),使用了60个5×5的卷积核,若是全连接则是6×16=96个卷积核; S4层: 继续采用2×2的池化层,得到16组5×5的特征映射; C5层: 对数据进行展平,得到120个1×1的特征映射,使用16×120个5×5个卷积核; F6层: 这是一个全连接层,共有84个神经元; 输出层: 输出10个类别数组结果。 5.4.4AlexNet AlexNet由Hilton的学生Alex Krizhevsky提出,它可被视为首个现代深度卷积网络模型,主要体现在使用GPU进行并行训练、利用Dropout防止过拟合。AlexNet在2012年的ImageNet图像分类大赛中获得冠军。AlexNet模型如图5.16所示。 图5.16AlexNet模型(图片来源见参考文献 [11]) 关于AlexNet模块的代码如下所示。 1import torch.nn as nn 2import torchvision.transforms as transforms 3class AlexNet(nn.Module): 4def __init__(self, num_classes): 5super(AlexNet, self).__init__() 6self.features = nn.Sequential( 7#(227 - 11 + 2 × 2) / 4 = 55 8nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), 9#输出55 × 55 × 96 10nn.ReLU(inplace=True), 11nn.MaxPool2d(kernel_size=3, stride=2), 12nn.Conv2d(64, 256, kernel_size=5, padding=2), 13nn.ReLU(inplace=True), 14nn.MaxPool2d(kernel_size=3, stride=2), 15nn.Conv2d(192,384,kernel_size=3, padding=1), 16nn.ReLU(inplace=True), 17nn.Conv2d(384,256, kernel_size=3, padding=1), 18nn.ReLU(inplace=True), 19nn.Conv2d(256,256,kernel_size=3, padding=1), 20nn.ReLU(inplace=True), 21nn.MaxPool2d(kernel_size=3, stride=2) 22) 23self.classifier = nn.Sequential( 24nn.Dropout(), 25nn.Linear(256 * 6 * 6, 4096), 26nn.ReLU(inplace=True), 27nn.Dropout(), 28nn.Linear(4096, 4096), 29nn.ReLU(inplace=True), 30nn.Linear(4096, num_classes) 31) 32def forward(self, x): 33x = self.features(x) 34x = x.view(x.size(0), 256 * 6 * 6) 35x = self.classifier(x) 36return x 5.4.5ResNet 2015年,在ImageNet竞赛中获得冠军的是由微软亚洲研究院的研究员设计的更加简单的网络 ResNet,该模型可有效地解决深度神经网络难以训练的问题网络之所以难以训练, 是因为存在梯度消失的问题。,可训练高达1000层的卷积网络。由于网络资源中有很多现成的介绍,这里不再一一赘述。 1from torch import nn 2import torch as t 3from torch.nn import functional as F 4class ResidualBlock(nn.Module): 5''' 6实现子模块: Residual Block 7''' 8def __init__(self, in_channel, out_channel, stride = 1, short_cut = None): 9super(ResidualBlock, self).__init__() 10self.left = nn.Sequential( 11nn.Conv2d(in_channel, out_channel, 3, stride, padding=1, bias=False), 12nn.BatchNorm2d(out_channel), 13nn.ReLU(inplace=True), 14nn.Conv2d(out_channel, out_channel, 3, 1, 1, bias=False), 15nn.BatchNorm2d(out_channel) 16) 17self.right = short_cut 18def forward(self, x): 19out = self.left(x) 20residual = x if self.right is None else self.right(x) 21out += residual 22return F.relu(out) 23class ResNet(nn.Module): 24''' 25主模块: ResNet34 26''' 27def __init__(self, num_classes = 1000): 28super(ResNet, self).__init__() 29#前几层图像转换 30self.pre = nn.Sequential( 31nn.Conv2d(3, 64, 7, 2, 3, bias=False), 32nn.BatchNorm2d(64), 33nn.ReLU(inplace=True), 34nn.MaxPool2d(3,2,1)) 35#重复layer: 分别为3,4,6,residual block 36self.layer1 = self._make_layer(64, 128, 3) 37self.layer2 = self._make_layer(128, 256, 4, stride = 2) 38self.layer3 = self._make_layer(256, 512, 6, stride = 2) 39self.layer4 = self._make_layer(512, 512, 3, stride = 2) 40#分类用的全连接 41self.fc = nn.Linear(512, num_classes) 42def _make_layer(self, in_channel, out_channel, block_num, stride = 1): 43''' 44构建layer, 包含多个residual block 45:param in_channel: 46:param out_channel: 47:param block_num: 48:param stride: 49:return: 50''' 51shortcut = nn.Sequential( 52nn.Conv2d(in_channel, out_channel, 1, stride, bias=False), 53nn.BatchNorm2d(out_channel) 54) 55layers = [] 56layers.append(ResidualBlock(in_channel, out_channel, stride, shortcut)) 57for i in range(1, block_num): 58layers.append(ResidualBlock(out_channel, out_channel)) 59return nn.Sequential(*layers) 60def forward(self, x): 61''' 62:param x: 样本 63:return: 前反馈数据结果 64''' 65x = self.pre(x) 66x = self.layer1(x) 67x = self.layer2(x) 68x = self.layer3(x) 69x = self.layer4(x) 70x = F.avg_pool2d(x, 7) 71x = x.view(x.size(0), -1) 72return self.fc(x) 关于ResNet神经网络的变型较多,比如ResNet18、ResNet3、ResNet50、ResNet101以及ResNet152。ResNet神经网络在处理实际问题中具有较好的表现能力,很大程度归功于Residual Block。 5.4.6GoogLeNet VGG在2014年的ImageNet比赛中获得了亚军,冠军为GoogLeNet,其模型由Google的研究人员提出。该模型在当时影响非常大,其根源在于颠覆了对卷积神经网络的常规想法。除此之外,它采用了一种非常高效的inception模块,得到了比VGG更深的网络结构,并且其参数比VGG的参数更少。这里给出高效的inception模块示意图,如图5.17所示。 图5.17inception模块示意图 图5.17含有4个并行线路: 一个1×1的卷积,一个小的感受野进行卷积提取特征; 一个1×1的卷积加上一个3×3的卷积,1×1的卷积降低输入的特征通道,减少参数计算量,然后接一个3×3的卷积,做一个较大感受野的卷积; 一个1×1的卷积加上一个5×5的卷积,做一个更大感受野的卷积; 一个3×3的最大池化加上一个1×1的卷积,最大池化改变输入的特征序列,1×1的卷积进行特征提取。 最终将4个并行线路得到的特征进行拼接,再进行下一步处理。 5.4.7垃圾分类实例 垃圾分类问题是当下研究的一个热点问题,这里通过自行构建的神经网络(与LeNet神经网络非常相似)来实验垃圾分类问题。本实例的样本数据来源于网络资源链接为https://pan.baidu.com/s/1rWl_odFKFAnNBrlNUxDo8g,密码: uhv8。,共有6个分类: 纸板(箱)(cardboard)、玻璃瓶(glass)、金属类(metal)、纸质类(paper)、塑料类(plastic)以及垃圾(trash),其训练集的各类样本数量如表5.1所示。 表5.1垃圾样本对应的样本量 labelcardboardglassmetalpaperplastictrash N403501410594482137 通过表5.1不难发现,训练集的各类样本数量并不均衡,但这里并不想深入探究这个问题。现在随机读取几个样本观察其图像,如图5.18所示。 图5.18训练集样本图像 图5.18中含有6张图像,对应的类别分别为: cardboard、glass、metal、paper、plastic以及trash,由于训练集的数据都处理成尺寸大小一致的图像,因此不需要再次处理。下面通过PyTorch来进行数据预处理。 1from torchvision import transforms,datasets 2from torch.utils.data import Dataset, DataLoader 3import torch.optim as optim 4import torch.nn.functional as F 5path = "./dataset-resized/"#训练集数据路径 6data_transform = transforms.Compose([ 7transforms.Resize(84),#重置成尺寸大小为84×84的图像 8transforms.CenterCrop(84),#剪切 9transforms.ToTensor(),#转换成张量形式 10transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #标准化处理 11]) 12train_dataset = datasets.ImageFolder(root = path, transform = data_transform) 这里借助torchvision中transforms来进一步处理数据,比如设置尺寸、剪切、张量化处理以及标准化处理等,利用dataset来导入数据集。其效果可以通过打印train_dataset来查看。其代码如下所示。 1print(train_dataset)#输出↓ 2Dataset ImageFolder 3Number of datapoints: 2527 4Root location: ./dataset-resized/ 5StandardTransform 6Transform: Compose( 7Resize(size=84, interpolation=PIL.Image.BILINEAR) 8CenterCrop(size=(84, 84)) 9ToTensor() 10Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) 11) 完成训练集数据的处理工作,下面就需要对其进行训练方式的设置,比如以什么样的方式来进行训练模型。这里选择以批次为2(batch_size=2)、每次迭代的批次样本顺序随机变动(shuffle=True),以及采用 2个进程(num_workers=2)来训练,其代码实现是非常简单的,如下所示。 1train_loader = torch.utils.data.DataLoader(train_dataset, batch_size = 2, shuffle = True,num_workers = 2) 下面给定自定义的神经网络,其代码如下所示。 1class MyNet(nn.Module): 2def __init__(self): 3super(MyNet, self).__init__() 4self.features = nn.Sequential( 5nn.Conv2d(in_channels=3, out_channels=18, kernel_size=5, stride=1, dilation=1), 6nn.ReLU(inplace=True), 7nn.MaxPool2d(kernel_size=2, stride=2), 8nn.Conv2d(in_channels=18, out_channels=30, kernel_size=5, stride=1, dilation=1), 9nn.ReLU(inplace=True), 10nn.MaxPool2d(kernel_size=2, stride=2) 11) 12self.linear = nn.Sequential( 13nn.Linear(in_features=30 * 18 * 18, out_features=1024), 14nn.ReLU(inplace=True), 15nn.Linear(in_features=1024, out_features=512), 16nn.ReLU(inplace=True), 17nn.Linear(in_features=512, out_features=6) 18) 19def forward(self, x): 20x = self.features(x) 21x = x.view(-1, 30 * 18 * 18) 22x = self.linear(x) 23return x 由于数据量较大,现在通过GPU进行加速训练,其代码如下所示。 1net = MyNet().cuda() 2#函数 3cirterion = nn.CrossEntropyLoss() 4#优化函数 5optimizer = optim.SGD(net.parameters(), lr = 0.0001) 6#迭代遍历 7for epoch in range(100): 8for i,data in enumerate(train_loader,0): 9inputs, labels = data#x, y 10outputs = net(inputs.cuda())#训练, 输出预测\hat{y} 11loss = cirterion(outputs.cpu(),labels)#损失函数 12optimizer.zero_grad() 13loss.backward() 14optimizer.step() 15loss_val = loss.data.numpy() 16if i % 10 == 0: 17print('epoch:[%d|%d], loss: %.3f' % (epoch + 1, i + 1, loss_val)) 该模型采用随机梯度下降法来进行批次训练,并且迭代次数(epoch)为100,这里不考虑这些设置是否可以获得本问题的最优解,仅考虑整个神经网络的正常运行。截至目前,即完成了整个训练过程,net中的参数即是待求系数。关于模型的验证这里不再赘述,通过PyTorch的相关模块看一下整个模型的构造,如图5.19所示。 图5.19MyNet神经网络模型 图5.19的神经网络模型结构图可以通过PyTorch相应的代码来实现,其代码也非常的简单,如下所示。 1from torchviz import make_dot 2from torch.autograd import Variable 3#随机生成一个满足图片尺寸大小和数据结构类型的随机张量 4x = Variable(torch.randn(1, 3, 84, 84)) 5vis_graph = make_dot(net(x), params=dict(net.named_parameters())) 6#将神经网络结构模型保存成pdf 文件格式 7vis_graph.view() 待模型训练完成后,需要对其进行保存,代码如下所示。 1#保存和加载整个模型, 包括网络结构、模型参数等 2torch.save(net, 'MyNet_model.pkl') 3#加载已训练好的模型 4model = torch.load('MyNet_model.pkl') 5#保存和加载网络中的参数 6torch.save(net.state_dict(), 'params.pkl') 7#加载已训练好的模型 8resnet.load_state_dict(torch.load('params.pkl')) 5.5生成对抗网络 5.5.1思想原理 生成对抗网络(generative adversarial net,GAN)作为深度学习的热门方向之一,深受研究学者和部分市场的欢迎。Ian Goodfellow(称为GAN之父)在2014年首次提出生成对抗网络,并给出了一个通过GAN实现的手写数字。GAN可以有效解决非监督学习的一个有名的问题: 指定一定规模的样本,训练一个系统能够生成类似的新样本。 下面给出GAN的流程图,如图5.20所示。 图5.20GAN流程图 根据图5.20,给出其优化目标函数: minG maxD V(D,G)=Ex~pdata(x)[log D(x)]+Ez~pz(z)[log(1-(D(G(z)))] (5.21) GAN模型在训练期间容易陷入损失函数值NaN现象,G网络和D网络在训练过程中要保持在一个平衡状态,若一方训练过好会致使另一方训练无法进行。相关研究人员在GAN的基础上提出了多种变种模型,比如WGAN、DCGAN、CGAN和LSGAN等。感兴趣的读者可以通过文献检索来进行了解和学习。 5.5.2对抗网络实例 通过对手写字段进行训练得到新的样本集,其结果如图5.21所示。 图5.21手写字体原图像和部分训练次数结果 通过图5.21的结果不难看出,GAN模型在训练过程生成的样本与原样本非常相近,并且训练次数越高其相似度越大,从而到达真假难分的目的。其代码(该代码借鉴了github资源)如下所示。 1#导入模块 2import torch 3import torch.nn as nn 4from torchvision import datasets 5from torchvision import transforms 6from torchvision.utils import save_image 7from torch.autograd import Variable 8from torch.utils.data import DataLoader 9#对图像进行处理 10def img_norm(x): 11'''分段函数''' 12out = (x + 1) / 2 13return out.clamp(0, 1) 14#标准化, 均值为0.5, 标准差为0.5 15transform = transforms.Compose([ 16transforms.ToTensor(), 17transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.05,0.05,0.05))]) 18#下载数据 19path = "../data" 20mnist = datasets.MNIST( 21root=path, 22train=True,#训练集 23transform=transform,#数据格式转换 24download=True 25) 26#数据批量,随机批次 27train_loader = DataLoader(mnist, batch_size=100, shuffle=True) 28#加载数据, 对输入数据做线性变换 29D = nn.Sequential( 30nn.Linear(784, 256), 31#max(0,x) + 0.2 * min(0,x) 32nn.LeakyReLU(0.2), 33nn.Linear(256,128), 34nn.LeakyReLU(0.2), 35nn.Linear(128,1), 36nn.Sigmoid() 37) 38#生成数据的取值范围与真实数据相似 39G = nn.Sequential( 40nn.Linear(64, 128), 41nn.LeakyReLU(0.2), 42nn.Linear(128, 256), 43nn.LeakyReLU(0.2), 44nn.Linear(256, 784), 45nn.Tanh() 46) 47#损失函数以及优化函数 48criterion = nn.BCELoss() 49d_optimizer = torch.optim.Adam(D.parameters(), lr=1e-6) 50g_optimizer = torch.optim.Adam(G.parameters(), lr=1e-6) 51#遍历 52for epoch in range(200): 53for i, (images, _) in enumerate(train_loader):#不需要label 54batch_size = images.size(0) 55#转成784 * 1 向量 56images = Variable(images.view(batch_size, -1)) 57#构建元素全为1 的矩阵 58real_labels = Variable(torch.ones(batch_size)) 59#元素全为0 的矩阵 60fake_labels = Variable(torch.zeros(batch_size)) 61outputs = D(images) 62d_loss_real = criterion(outputs.view(1,-1), real_labels) 63real_score = outputs 64#构建随机矩阵 65z = Variable(torch.randn(batch_size, 64)) 66#先G网络后D网络 67fake_images = G(z) 68outputs = D(fake_images) 69d_loss_fake = criterion(outputs.view(1,-1), fake_labels) 70fake_score = outputs 71d_loss = d_loss_real + d_loss_fake 72D.zero_grad() 73d_loss.backward() 74d_optimizer.step() 75z = Variable(torch.randn(batch_size, 64)) 76#先G网络后D网络 77fake_images = G(z) 78outputs = D(fake_images) 79g_loss = criterion(outputs.view(1,-1), real_labels) 80D.zero_grad() 81G.zero_grad() 82g_loss.backward() 83g_optimizer.step() 84if (i + 1) % 300 == 0: 85print("epoch: ", epoch, "d_loss: ", d_loss_real.data.numpy(), fake_score.data.mean()) 86#对图片进行训练 87if (epoch + 1) == 1: 88images = images.view(images.size(0), 1, 28, 28) 89save_image(img_norm(images.data), './real_images.png') 90fake_images = fake_images.view(fake_images.size(0), 1, 28,28) 91save_image(img_norm(fake_images.data), './fake_image_{0}.png'.format(epoch+1)) 5.6其他神经网络 5.6.1循环神经网络 循环神经网络(recurrent neural network,RNN)是一种具有短期记忆功能的神经网络。不同于前馈神经网络和卷积神经网络,RNN不仅可接收其他神经元传递的信息,也可接收自身的历史信息,从而形成一个具有环路的网络结构。 RNN在语音识别和自然语言领域有广泛的应用。这里只介绍一种简单的循环网络。 给定一个时间序列x=(x1,x2,…,xt),其更新并含有反馈的函数为: ht=f(ht-1,xt)(5.22) 其中,h0=0,f可以是一个非线性函数,也可以是一个前馈网络。式(5.22)可视为一种动力系统。理论上,循环神经网络在一定程度上可以视为非线性动力系统。 图5.22循环神经网络的流程图 注意: 式(5.22)形式上类似于统计学中的自回归模型(autoregressive module,AR)。 循环神经网络的流程图如图5.22所示。 关于循环神经网络的其他内容在这里不再进行过多阐述,若读者对循环神经网络深感兴趣,可以查阅其他相关的书籍或与自然语言(NLP)相关的书籍。 5.6.2风格迁移神经网络 风格迁移(neural style)神经网络是由德国图宾根大学的Bethge实验室的3个研究员Leon Gatys、Alexander Ecker和Matthias Bethge于2015年提出的一种算法。其算法实现过程非常的复杂,处理像素比较大的图片时往往需要几个小时甚至几十个小时的训练。 2016年斯坦福大学的Justin Johnson、Alexandre Alahi和李飞飞提出了一种快速风格迁移算法。该方法结合GPU可以快速完成训练。 由于网上有很多关于该算法的优质代码,读者可以通过github网络查询优质开源代码,这里不再对其进行赘述。这里给出训练的一组校园图片,如图5.23所示。 图5.23风格图像(星空) 图5.23是大画家梵高的星空。杭州师范大学(仓前校区)的图书馆照片如图5.24所示。 图5.24杭州师范大学(仓前校区)图书馆一角 通过风格迁移神经网络的算法原理,将风格图像(图5.23)迁移到图像 5.24中,其训练 100次的结果如图5.25所示。 图5.25风格图像迁移训练 100次的效果图 5.7本章小结 本章主要介绍了PyTorch模块和部分神经网络模型。PyTorch在学术研究方面要优于TensorFlow,再者它与NumPy模块可以很方便地进行转换,这也是使用它做神经网络的重要原因之一,但在大型的神经网络项目部署中,建议采用 TensorFlow。神经网络历史悠久,但是其兴盛时期也就在最近几年,其涉及领域广泛,应用前景非常大,不同的模型用于不同的实际问题,更甚者同一问题可以通过多种神经网络模型来研究。若读者对神经网络非常感兴趣,可以查阅斯坦福大学李飞飞老师的相关教程,因此这里不再详细阐述关于神经网络的相关内容。