飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件、丰富的工具组件于一体,是中国首个自主研发、功能丰富、开源开放的产业级深度学习平台。飞桨于2016 年正式开源,是主流深度学习框架中一款完全国产化的产品。相比国内其他产品,飞桨是一个功能完整的深度学习平台,也是唯一成熟稳定、具备大规模推广条件的深度学习开源开放平台。根据国际权威调查机构IDC报告显示,2021年飞桨已位居中国深度学习平台市场综合份额第一。
目前,飞桨已凝聚477万开发者,基于飞桨开源深度学习平台创建56万个模型,服务了18万家企事业单位。飞桨助力开发者快速实现AI想法,创新AI应用,作为基础平台支撑越来越多行业实现产业智能化升级,并已广泛应用于智慧城市、智能制造、智慧金融、泛交通、泛互联网、智慧农业等领域,如 图1 所示。
飞桨提供了产业级开源模型库,覆盖计算机视觉(PaddleCV)、自然语言处理(PaddleNLP)、推荐(PaddleRec)、语音(PaddleSpeech)四大应用领域,包含经过产业实践长期打磨的主流模型以及在国际竞赛中的夺冠模型。同时,飞桨将主流模型按照领域组织成端到端开发套件,助力快速的产业应用。
使用ndarray数组可以很方便的构建数学函数,并利用其底层的矢量计算能力快速实现计算。下面以神经网络中比较常用激活函数Sigmoid和ReLU为例,介绍代码实现过程。
图像是由像素点构成的矩阵,其数值可以用ndarray来表示。将上述介绍的操作用在图像数据对应的ndarray上,可以很轻松的实现图片的翻转、裁剪和亮度调整,具体代码和效果如下所示。
飞桨使用Tensor数据结构来表示数据,在神经网络中传递的数据均为Tensor。Tensor可以将其理解为多维数组,其可以具有任意多的维度,不同Tensor可以有不同的数据类型 (dtype) 和形状 (shape)。同一Tensor的中所有元素的数据类型均相同。如果你对 Numpy 熟悉,Tensor是类似于Numpy数组(array)的概念。
飞桨的Tensor高度兼容Numpy数组(array),在基础数据结构和方法上,增加了很多适用于深度学习任务的参数和方法,如:反向计算梯度,更灵活的指定运行硬件等。
如下述代码声明了两个Tensor类型的向量和,指定CPU为计算运行硬件,要自动反向求导。两个向量除了可以与Numpy类似的做相乘的操作之外,还可以直接获取到每个变量的导数值。
虽然Paddle的Tensor可以与Numpy的数组方便的互相转换,但在实际中两者频繁转换会性能消耗。飞桨的Tensor支持的操作已经基本覆盖Numpy并有所加强,所以推荐用户在程序中优先使用飞桨的Tensor完成各种数据处理和组网操作。具体分为如下两种场景:
1. Series 注:Series是一种类似于一维数组的对象,它由一维数组(各种numpy数据类 型)以及一组与之相关的数据标签(即索引)组成.可理解为带标签的一维数组,可存储整数、浮点数、字符串、Python 对象等类型的数据。
1 2 3 4 5 import pandas as pdimport numpy as np s = pd.Series(['a' ,'b' ,'c' ,'d' ,'e' ])print (s)
PYTHON
注:Series中可以使用index设置索引列表,与字典不同的是,Series允许索引重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 s = pd.Series(['a' ,'b' ,'c' ,'d' ,'e' ],index=[100 ,200 ,100 ,400 ,500 ])print (s) d = {'b' : 1 , 'a' : 0 , 'c' : 2 } pd.Series(d) d = {'b' : 1 , 'a' : 0 , 'c' : 2 } pd.Series(d)print (s.values)print (s.index)print (s[100 ])print (s[[400 , 500 ]])print (s+s)print (s*3 )
PYTHON
注:Series中最重要的一个功能是它会在算术运算中基于标签自动对齐不同索引的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 obj1 = pd.Series({"Ohio" : 35000 , "Oregon" : 16000 , "Texas" : 71000 , "Utah" : 5000 })print (obj1) obj2 = pd.Series({"California" : np.nan, "Ohio" : 35000 , "Oregon" : 16000 , "Texas" : 71000 })print (obj2)print (obj1 + obj2) s = pd.Series(np.array([1 ,2 ,3 ,4 ,5 ]), index=['a' , 'b' , 'c' , 'd' , 'e' ])print (s['a' ])print (s[1 :])print (s[:-1 ])print (s[1 :] + s[:-1 ])
PYTHON
2. DataFrame 注:
DataFrame是一个表格型的数据结构,类似于Excel或sql表
它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔值等)
DataFrame既有行索引也有列索引,它可以被看做由Series组成的字典(共用同一个索引)。
用多维数组字典、列表字典生成 DataFrame。
1 2 3 4 data = {'state' : ['Ohio' , 'Ohio' , 'Ohio' , 'Nevada' , 'Nevada' ], 'year' : [2000 , 2001 , 2002 , 2001 , 2002 ], 'pop' : [1.5 , 1.7 , 3.6 , 2.4 , 2.9 ]} frame = pd.DataFrame(data,columns=['year' , 'state' , 'pop' , 'debt' ])print (frame)
PYTHON
用 Series 字典或字典生成 DataFrame, 即Series可以作为DataFrame的子集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 d = {'one' : pd.Series([1. , 2. , 3. ], index=['a' , 'b' , 'c' ]), 'two' : pd.Series([1. , 2. , 3. , 4. ], index=['a' , 'b' , 'c' , 'd' ])}print (pd.DataFrame(d))print (frame2['state' ]) frame2['debt' ] = 16.5 print (frame2) frame2['new' ] = frame2['debt' ]* frame2['pop' ] print (frame2) frame2['debt' ] = np.arange(5. )print (frame2)
PYTHON
3. 索引对象常用方法
4.常用方法
PIL库 注:
PIL库是一个具有强大图像处理能力的第三方库。
在命令行下的安装方法: pip install pillow。
在使用过程中的弓|入方法: from PIL import Image。
图像的组成:由RGB三原色组成,RGB图像中,一种彩色由R、G、B三原色按照比例混合而成。0-255区分不同亮度的颜色。图像的数组表示:图像是一个由像素组成的矩阵,每个元素是一个RGB值。
Image 是 PIL 库中代表一个图像的类(对象)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from PIL import Imageimport matplotlib.pyplot as plt %matplotlib inline img = Image.open ('/home/aistudio/work/yushuxin.jpg' ) plt.imshow(img) plt.show(img) img_mode = img.modeprint (img_mode) width,height = img.sizeprint (width,height) img_rotate = img.rotate(45 ) img3_lr = img3.transpose(Image.FLIP_LEFT_RIGHT) img3_bt = img3.transpose(Image.FLIP_TOP_BOTTOM) width,height = img2.size img2_resize_result = img2.resize((int (width*0.6 ),int (height*0.6 )),Image.ANTIALIAS) img1_crop_result = img1.crop((126 ,0 ,381 ,300 )) img1_crop_result.save('path' )
PYTHON
Matplotlib库 注:
Matplotlib库由各种可视化类构成,内部结构复杂。
matplotlib.pylot是绘制各类可视化图形的命令字库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import matplotlib.pyplot as pltimport numpy as np %matplotlib inline x = np.linspace(-1 ,1 ,50 ) y1 = 2 *x + 1 y2 = x**2 plt.figure(figsize=(7 ,5 )) plt.plot(x,y1,color='red' ,linewidth=1 ) plt.plot(x,y2,color='blue' ,linewidth=5 ) plt.legend(handles=[l1,l2],labels=['aa' ,'bb' ],loc='best' ) plt.xlabel('x' ,fontsize=20 ) plt.ylabel('y' ,fontsize=20 ) plt.xlim((0 ,1 )) plt.ylim((0 ,1 )) plt.show() dots1 =np.random.rand(50 ) dots2 =np.random.rand(50 ) plt.scatter(dots1,dots2,c='red' ,alpha=0.5 ) plt.show() x = np.arange(10 ) y = 2 **x+10 plt.bar(x,y,facecolor='#9999ff' ,edgecolor='white' )for ax,ay in zip (x,y): plt.text(ax,ay,'%.1f' % ay,ha='center' ,va='bottom' ) plt.show()
PYTHON
基础图标函数
数据增强 简介:深层神经网络一般都需要大量的训练数据才能获得比较理想的结果。在数据量有限的情况下,可以通过数据增强( Data Augmentation )来增加训练样本的多样性,提高模型鲁棒性。 目的:
增加数据量
采集更多的图像特征
使网络可见更多的数据变化
提高模型的泛化能力
增强方式:
下图所示为一些基础的图像增强方法,如果我们发现数据集中的猫均是标准姿势,而真实场景中的猫时常有倾斜身姿的情况,那么在原始图片数据的基础上采用旋转的方法造一批数据加入到数据集会有助于提升模型效果。类似的,如果数据集中均是高清图片,而真实场景中经常有拍照模糊或曝光异常的情况,则采用降采样和调整饱和度的方式造一批数据,有助于提升模型的效果。 基础的图像增强方法
下图展示了一些高阶的图像增强方法,裁剪和拼接分别适合于“数据集中物体完整,但实际场景中物体存在遮挡”,以及“数据集中物体背景单一,而实际场景中物体的背景多变”的两种情况。 高阶的数据增强方法
下图展示了专门针对文本识别的数据增强方法TIA(Text Image augmentation),对应到“数据集中字体多是平面,而真实场景中的字体往往会在曲面上扭曲的情况,比如拿着相机对一张凸凹不平摆放的纸面拍摄的文字就会存在这种情况”。 TIA(Text Image augmentation):针对文本识别数据增强方法
下图展示了一种新颖的数据增强技巧,用于很多现实中的文字检测,要面临复杂多样的背景,比如店铺牌匾上的文字,周围的背景可能是非常多样的。将部分文本区域剪辑出来,随机摆放到图片的各种位置来生成新的训练数据。这样的数据会大大提高模型在复杂背景中,检测到文字内容的能力。 CopyPaste:一种新颖的数据增强技巧
1. 随机旋转 注:使用numpy+ PIL库进行图像的随机旋转
1 2 3 4 5 6 7 8 def rotate_image ( img ): """ 图像增强,增加随机旋转角度 """ angle = np.random.randint( -14 , 15 ) img = img.rotate(angle) return img
PYTHON
2. 随机亮度调整 注:使用numpy+ PIL库进行图像的随机亮度调整
1 2 3 4 5 6 7 8 9 10 11 12 def random_brightness (img ): """ 图像增强,亮度调整 :param img: :return: """ prob = np.random.uniform(0 , 1 ) if prob < train_parameters['image_enhance_strategy' ]['brightness_prob' ]: brightness_delta = train_parameters['image_enhance_strategy' ]['brightness_delta' ] delta = np.random.uniform(-brightness_delta, brightness_delta) + 1 img = ImageEnhance.Brightness(img).enhance(delta) return img
PYTHON
3. 训练过程可视化 注:使用Matplotlib库绘制深度学习训练过程中,随着数据的增加,误差与准确率的变化趋势,从而对模型效果进行评估。观察到模型的误差相对较低,而准确率较高,接下来可以使用该模型进行预测。
入门实战深度学习 1.深度学习模型的基本步骤
2.实例一“波士顿房价预测” 2.1 数据处理 数据处理包含五个部分:数据导入、数据形状变换、数据集划分、数据归一化处理和封装load data
函数。数据预处理后,才能被模型调用。
1 2 3 4 5 6 import numpy as npimport json datafile = './work/housing.data' data = np.fromfile(datafile, sep=' ' )
PYTHON
2.1.2 数据形状变换 由于读入的原始数据是1维的,所有数据都连在一起。因此需要我们将数据的形状进行变换,形成一个2维的矩阵,每行为一个数据样本(14个值),每个数据样本包含13个XXX (影响房价的特征)和一个YYY (该类型房屋的均价).
1 2 3 4 5 6 7 8 9 10 11 feature_names = [ 'CRIM' , 'ZN' , 'INDUS' , 'CHAS' , 'NOX' , 'RM' , 'AGE' ,'DIS' , 'RAD' , 'TAX' , 'PTRATIO' , 'B' , 'LSTAT' , 'MEDV' ] feature_num = len (feature_names) data = data.reshape([data.shape[0 ] // feature_num, feature_num])print (data) x = data[0 ]print (x.shape)print (x)
PYTHON
2.1.3 数据集划分 将数据集划分成训练集和测试集 ,其中训练集用于确定模型的参数,测试集用于评判模型的效果。
1 2 3 4 5 ratio = 0.8 offset = int (data.shape[0 ] * ratio) training_data = data[:offset] training_data.shape
PYTHON
2.1.4 数据归一化处理 对每个特征进行归一化处理,使得每个特征的取值缩放到0~1之间。这样做有两个好处:一是模型训练更高效,在本节的后半部分会详细说明;二是特征前的权重大小可以代表该变量对预测结果的贡献度(因为每个特征值本身的范围相同)。
1 2 3 4 5 6 7 8 9 maximums, minimums, avgs = \ training_data.max (axis=0 ), \ training_data.min (axis=0 ), \ training_data.sum (axis=0 ) / training_data.shape[0 ]for i in range (feature_num): data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
PYTHON
2.1.5 封装成load data函数 将上述几个数据处理操作封装成load data
函数,以便下一步模型的调用,实现方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 def load_data (): datafile = './work/housing.data' data = np.fromfile(datafile, sep=' ' ) feature_names = [ 'CRIM' , 'ZN' , 'INDUS' , 'CHAS' , 'NOX' , 'RM' , 'AGE' , \ 'DIS' , 'RAD' , 'TAX' , 'PTRATIO' , 'B' , 'LSTAT' , 'MEDV' ] feature_num = len (feature_names) data = data.reshape([data.shape[0 ] // feature_num, feature_num]) ratio = 0.8 offset = int (data.shape[0 ] * ratio) training_data = data[:offset] maximums, minimums, avgs = training_data.max (axis=0 ), training_data.min (axis=0 ), \ training_data.sum (axis=0 ) / training_data.shape[0 ] for i in range (feature_num): data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i]) training_data = data[:offset] test_data = data[offset:] return training_data, test_data training_data, test_data = load_data() x = training_data[:, :-1 ] y = training_data[:, -1 :]print (x[0 ])print (y[0 ])
PYTHON
2.2 模型设计 模型设计是深度学习模型关键要素之一,也称为网络结构设计,相当于模型的假设空间,即实现模型“前向计算”(从输入到输出)的过程。
如果将输入特征和输出预测值均以向量表示,输入特征有13个分量,有1个分量,那么参数权重的形状(shape)是。假设我们以如下任意数字赋值参数做初始化:
1 2 w = [0.1 , 0.2 , 0.3 , 0.4 , 0.5 , 0.6 , 0.7 , 0.8 , -0.1 , -0.2 , -0.3 , -0.4 , 0.0 ] w = np.array(w).reshape([13 , 1 ])
PYTHON
取出第1条样本数据,观察样本的特征向量与参数向量相乘的结果。
1 2 3 x1=x[0 ] t = np.dot(x1, w)print (t)
PYTHON
完整的线性回归公式,还需要初始化偏移量,同样随意赋初值-0.2。那么,线性回归模型的完整输出是,这个从特征和参数计算输出值的过程称为“前向计算”。
1 2 3 b = -0.2 z = t + bprint (z)
PYTHON
将上述计算预测输出的过程以“类和对象”的方式来描述,类成员变量有参数和。通过写一个forward
函数(代表“前向计算”)完成上述从特征和参数到输出预测值的计算过程,代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 class Network (object ): def __init__ (self, num_of_weights ): np.random.seed(0 ) self.w = np.random.randn(num_of_weights, 1 ) self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z
PYTHON
基于Network类的定义,模型的计算过程如下所示。
1 2 3 4 5 net = Network(13 ) x1 = x[0 ] y1 = y[0 ] z = net.forward(x1)print (z)
PYTHON
从上述前向计算的过程可见,线性回归也可以表示成一种简单的神经网络(只有一个神经元,且激活函数为恒等式)。这也是机器学习模型普遍为深度学习模型替代的原因:由于深度学习网络强大的表示能力,很多传统机器学习模型的学习能力等同于相对简单的深度学习模型。
2.3 训练配置 模型设计完成后,需要通过训练配置寻找模型的最优值,即通过损失函数来衡量模型的好坏。训练配置也是深度学习模型关键要素之一。
通过模型计算表示的影响因素所对应的房价应该是 但实际数据告诉我们房价是。这时我们需要有某种指标来衡量预测值跟真实值之间的差距。对于回归问题,最常采用的衡量方法是使用均方误差作为评价模型好坏的指标,具体定义如下:
上式中的(简记为: )通常也被称作损失函数,它是衡量模型好坏的指标。读者可能会奇怪:如果要衡量预测房价和真实房价之间的差距,是否将每一个样本的差距的绝对值加和即可?差距绝对值加和是更加直观和朴素的思路,为何要平方加和? 损失函数的设计不仅要考虑准确衡量问题的“合理性”,通常还要考虑“易于优化求解”。至于这个问题的答案,在介绍完优化算法后再揭示。
在回归问题中,均方误差 是一种比较常见的形式,分类问题中通常会采用交叉熵 作为损失函数,在后续的章节中会更详细的介绍。对一个样本计算损失函数值的实现如下。
1 2 Loss = (y1 - z)*(y1 - z)print (Loss)
PYTHON
因为计算损失函数时需要把每个样本的损失函数值都考虑到,所以我们需要对单个样本的损失函数进行求和,并除以样本总数。
在Network类下面添加损失函数的计算过程如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Network (object ): def __init__ (self, num_of_weights ): np.random.seed(0 ) self.w = np.random.randn(num_of_weights, 1 ) self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z def loss (self, z, y ): error = z - y cost = error * error cost = np.mean(cost) return cost
PY
使用定义的Network类,可以方便的计算预测值和损失函数。需要注意的是,类中的变量, ,, , 等均是向量。以变量为例,共有两个维度,一个代表特征数量(值为13),一个代表样本数量,代码如下所示。
1 2 3 4 5 6 7 8 9 net = Network(13 ) x1 = x[0 :3 ] y1 = y[0 :3 ]print ('actuality: ' ,y1) z = net.forward(x1)print ('predict: ' , z) loss = net.loss(z, y1)print ('loss:' , loss)
PY
2.4 训练过程 上述计算过程描述了如何构建神经网络,通过神经网络完成预测值和损失函数的计算。接下来介绍如何求解参数和的数值,这个过程也称为模型训练过程。训练过程是深度学习模型的关键要素之一,其目标是让定义的损失函数尽可能的小,也就是说找到一个参数解和,使得损失函数取得极小值。
我们先做一个小测试:如 图5 所示,基于微积分知识,求一条曲线在某个点的斜率等于函数在该点的导数值。那么大家思考下,当处于曲线的极值点时,该点的斜率是多少?
图5:曲线斜率等于导数值
这个问题并不难回答,处于曲线极值点时的斜率为0,即函数在极值点的导数为0。那么,让损失函数取极小值的和应该是下述方程组的解:
其中表示的是损失函数的值,为模型权重,为偏置项。和均为要学习的模型参数。
把损失函数表示成矩阵的形式为
(为范数,表示向量之间的距离,****)
其中为个样本的标签值构成的向量,形状为;为个样本特征向量构成的矩阵,形状为,为数据特征长度;为权重向量,形状为;为所有元素都为的向量,形状为。
计算公式7对参数的偏导数 请注意,上述公式忽略了系数,并不影响最后结果。其中为维的全1向量。
令公式8等于0,得到 其中为所有标签的平均值,为所有特征向量的平均值。将带入公式7中并对参数求偏导得到
令公式10等于0,得到最优参数
将样本数据带入上面的公式11和公式12中即可求解出和的值,但是这种方法只对线性回归这样简单的任务有效。如果模型中含有非线性变换,或者损失函数不是均方差这种简单的形式,则很难通过上式求解。为了解决这个问题,下面我们将引入更加普适的数值求解方法:梯度下降法。
2.4.1 梯度下降法 在现实中存在大量的函数正向求解容易,但反向求解较难,被称为单向函数,这种函数在密码学中有大量的应用。密码锁的特点是可以迅速判断一个密钥是否是正确的(已知,求很容易),但是即使获取到密码锁系统,也无法破解出正确得密钥(已知,求很难)。
这种情况特别类似于一位想从山峰走到坡谷的盲人,他看不见坡谷在哪(无法逆向求解出导数为0时的参数值),但可以伸脚探索身边的坡度(当前点的导数值,也称为梯度)。那么,求解Loss函数最小值可以这样实现:从当前的参数取值,一步步的按照下坡的方向下降,直到走到最低点。这种方法笔者称它为“盲人下坡法”。哦不,有个更正式的说法“梯度下降法”。
训练的关键是找到一组,使得损失函数取极小值。我们先看一下损失函数只随两个参数、变化时的简单情形,启发下寻解的思路。 这里将中除之外的参数和都固定下来,可以用图画出的形式,并在三维空间中画出损失函数随参数变化的曲面图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 net = Network(13 ) losses = [] w5 = np.arange(-160.0 , 160.0 , 1.0 ) w9 = np.arange(-160.0 , 160.0 , 1.0 ) losses = np.zeros([len (w5), len (w9)])for i in range (len (w5)): for j in range (len (w9)): net.w[5 ] = w5[i] net.w[9 ] = w9[j] z = net.forward(x) loss = net.loss(z, y) losses[i, j] = lossimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3D fig = plt.figure() ax = Axes3D(fig) w5, w9 = np.meshgrid(w5, w9) ax.plot_surface(w5, w9, losses, rstride=1 , cstride=1 , cmap='rainbow' ) plt.show()
PY
从图中可以明显观察到有些区域的函数值比周围的点小。需要说明的是:为什么选择和来画图呢?这是因为选择这两个参数的时候,可比较直观的从损失函数的曲面图上发现极值点的存在。其他参数组合,从图形上观测损失函数的极值点不够直观。
观察上述曲线呈现出“圆滑”的坡度,这正是我们选择以均方误差作为损失函数的原因之一。图6 呈现了只有一个参数维度时,均方误差和绝对值误差(只将每个样本的误差累加,不做平方处理)的损失函数曲线图。
图6:均方误差和绝对值误差损失函数曲线图
由此可见,均方误差表现的“圆滑”的坡度有两个好处:
曲线的最低点是可导的。
越接近最低点,曲线的坡度逐渐放缓,有助于通过当前的梯度来判断接近最低点的程度(是否逐渐减少步长,以免错过最低点)。
而绝对值误差是不具备这两个特性的,这也是损失函数的设计不仅仅要考虑“合理性”,还要追求“易解性”的原因。
现在我们要找出一组的值,使得损失函数最小,实现梯度下降法的方案如下:
步骤1:随机的选一组初始值,例如:
步骤2:选取下一个点,使得
步骤3:重复步骤2,直到损失函数几乎不再下降。
如何选择是至关重要的,第一要保证是下降的,第二要使得下降的趋势尽可能的快。微积分的基础知识告诉我们:沿着梯度的反方向,是函数值下降最快的方向,如 图7 所示。简单理解,函数在某一个点的梯度方向是曲线斜率最大的方向,但梯度方向是向上的,所以下降最快的是梯度的反方向。
图7:梯度下降方向示意图
#### 2.4.2 梯度计算
上文已经介绍了损失函数的计算方法,这里稍微改写。为了使梯度计算更加简洁,引入因子,定义损失函数如下:
其中是网络对第个样本的预测值:
梯度的定义:
可以计算出对和的偏导数:
从导数的计算过程可以看出,因子被消掉了,这是因为二次函数求导的时候会产生因子,这也是我们将损失函数改写的原因。
下面我们考虑只有一个样本的情况下,计算梯度:
可以计算出:
可以计算出对和的偏导数:
1 2 3 4 5 6 7 8 9 10 x1 = x[0 ] y1 = y[0 ] z1 = net.forward(x1)print ('x1 {}, shape {}' .format (x1, x1.shape))print ('y1 {}, shape {}' .format (y1, y1.shape))print ('z1 {}, shape {}' .format (z1, z1.shape)) gradient_w0 = (z1 - y1) * x1[0 ]print ('gradient_w0 {}' .format (gradient_w0))
PY
2.4.3 使用NumPy进行梯度计算 基于NumPy广播机制(对向量和矩阵计算如同对1个单一变量计算一样),可以更快速的实现梯度计算。计算梯度的代码中直接用,得到的是一个13维的向量,每个分量分别代表该维度的梯度。
1 2 3 gradient_w = (z1 - y1) * x1print ('gradient_w_by_sample1 {}, gradient.shape {}' .format (gradient_w, gradient_w.shape))
PY
此处可见,计算梯度gradient_w
的维度是,并且其第1行与上面第1个样本计算的梯度gradient_w_by_sample1一致,第2行与上面第2个样本计算的梯度gradient_w_by_sample2一致,第3行与上面第3个样本计算的梯度gradient_w_by_sample3一致。这里使用矩阵操作,可以更加方便的对3个样本分别计算各自对梯度的贡献。
那么对于有N个样本的情形,我们可以直接使用如下方式计算出所有样本对梯度的贡献,这就是使用NumPy库广播功能带来的便捷。 小结一下这里使用NumPy库的广播功能:
一方面可以扩展参数的维度,代替for循环来计算1个样本对从到的所有参数的梯度。列表征特征维度
另一方面可以扩展样本的维度,代替for循环来计算样本0到样本403对参数的梯度。行表征样本维度
1 2 3 4 z = net.forward(x) gradient_w = (z - y) * xprint ('gradient_w shape {}' .format (gradient_w.shape))print (gradient_w)
PY
上面gradient_w的每一行代表了一个样本对梯度的贡献。根据梯度的计算公式,总梯度是对每个样本对梯度贡献的平均值。
可以使用NumPy的均值函数来完成此过程,代码实现如下。
1 2 3 4 5 6 gradient_w = np.mean(gradient_w, axis=0 )print ('gradient_w ' , gradient_w.shape)print ('w ' , net.w.shape)print (gradient_w)print (net.w)
PY
使用NumPy的矩阵操作方便地完成了gradient的计算,但引入了一个问题,gradient_w
的形状是(13,),而的维度是(13, 1)。导致该问题的原因是使用np.mean
函数时消除了第0维。为了加减乘除等计算方便,gradient_w
和必须保持一致的形状。因此我们将gradient_w
的维度也设置为(13,1),代码如下:
1 2 3 gradient_w = gradient_w[:, np.newaxis]print ('gradient_w shape' , gradient_w.shape)print (gradient_w)
PY
梯度计算综合代码:
1 2 3 4 5 6 z = net.forward(x) gradient_w = (z - y) * x gradient_w = np.mean(gradient_w, axis=0 ) gradient_w = gradient_w[:, np.newaxis] gradient_w
PY
偏置计算综合代码:
1 2 3 4 gradient_b = (z - y) gradient_b = np.mean(gradient_b) gradient_b
PY
总结为OOP的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Network (object ): def __init__ (self, num_of_weights ): np.random.seed(0 ) self.w = np.random.randn(num_of_weights, 1 ) self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z def loss (self, z, y ): error = z - y num_samples = error.shape[0 ] cost = error * error cost = np.sum (cost) / num_samples return cost def gradient (self, x, y ): z = self.forward(x) gradient_w = (z-y)*x gradient_w = np.mean(gradient_w, axis=0 ) gradient_w = gradient_w[:, np.newaxis] gradient_b = (z - y) gradient_b = np.mean(gradient_b) return gradient_w, gradient_b net = Network(13 ) net.w[5 ] = -100.0 net.w[9 ] = -100.0 z = net.forward(x) loss = net.loss(z, y) gradient_w, gradient_b = net.gradient(x, y) gradient_w5 = gradient_w[5 ][0 ] gradient_w9 = gradient_w[9 ][0 ]print ('point {}, loss {}' .format ([net.w[5 ][0 ], net.w[9 ][0 ]], loss))print ('gradient {}' .format ([gradient_w5, gradient_w9]))
PY
2.4.4 梯度更新 下面研究更新梯度的方法,确定损失函数更小的点。首先沿着梯度的反方向移动一小步,找到下一个点P1,观察损失函数的变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 eta = 0.1 net.w[5 ] = net.w[5 ] - eta * gradient_w5 net.w[9 ] = net.w[9 ] - eta * gradient_w9 z = net.forward(x) loss = net.loss(z, y) gradient_w, gradient_b = net.gradient(x, y) gradient_w5 = gradient_w[5 ][0 ] gradient_w9 = gradient_w[9 ][0 ]print ('point {}, loss {}' .format ([net.w[5 ][0 ], net.w[9 ][0 ]], loss))print ('gradient {}' .format ([gradient_w5, gradient_w9]))
PY
运行上面的代码,可以发现沿着梯度反方向走一小步,下一个点的损失函数的确减少了。感兴趣的话,大家可以尝试不停的点击上面的代码块,观察损失函数是否一直在变小。
在上述代码中,每次更新参数使用的语句:net.w[5] = net.w[5] - eta * gradient_w5
相减:参数需要向梯度的反方向移动。
eta:控制每次参数值沿着梯度反方向变动的大小,即每次移动的步长,又称为学习率。
大家可以思考下,为什么之前我们要做输入特征的归一化,保持尺度一致?这是为了让统一的步长更加合适,使训练更加高效。
如 图8 所示,特征输入归一化后,不同参数输出的Loss是一个比较规整的曲线,学习率可以设置成统一的值 ;特征输入未归一化时,不同特征对应的参数所需的步长不一致,尺度较大的参数需要大步长,尺寸较小的参数需要小步长,导致无法设置统一的学习率。
图8:未归一化的特征,会导致不同特征维度的理想步长不同
#### 2.4.5 封装Train函数
将上面的循环计算过程封装在train
和update
函数中,实现方法如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 class Network (object ): def __init__ (self, num_of_weights ): np.random.seed(0 ) self.w = np.random.randn(num_of_weights,1 ) self.w[5 ] = -100. self.w[9 ] = -100. self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z def loss (self, z, y ): error = z - y num_samples = error.shape[0 ] cost = error * error cost = np.sum (cost) / num_samples return cost def gradient (self, x, y ): z = self.forward(x) gradient_w = (z-y)*x gradient_w = np.mean(gradient_w, axis=0 ) gradient_w = gradient_w[:, np.newaxis] gradient_b = (z - y) gradient_b = np.mean(gradient_b) return gradient_w, gradient_b def update (self, gradient_w5, gradient_w9, eta=0.01 ): net.w[5 ] = net.w[5 ] - eta * gradient_w5 net.w[9 ] = net.w[9 ] - eta * gradient_w9 def train (self, x, y, iterations=100 , eta=0.01 ): points = [] losses = [] for i in range (iterations): points.append([net.w[5 ][0 ], net.w[9 ][0 ]]) z = self.forward(x) L = self.loss(z, y) gradient_w, gradient_b = self.gradient(x, y) gradient_w5 = gradient_w[5 ][0 ] gradient_w9 = gradient_w[9 ][0 ] self.update(gradient_w5, gradient_w9, eta) losses.append(L) if i % 50 == 0 : print ('iter {}, point {}, loss {}' .format (i, [net.w[5 ][0 ], net.w[9 ][0 ]], L)) return points, losses train_data, test_data = load_data() x = train_data[:, :-1 ] y = train_data[:, -1 :] net = Network(13 ) num_iterations=2000 points, losses = net.train(x, y, iterations=num_iterations, eta=0.01 ) plot_x = np.arange(num_iterations) plot_y = np.array(losses) plt.plot(plot_x, plot_y) plt.show()
PY
2.4.6 训练过程扩展到全部参数 为了能给读者直观的感受,上文演示的梯度下降的过程仅包含和两个参数。但房价预测的模型必须要对所有参数和进行求解,这需要将Network中的update
和train
函数进行修改。由于不再限定参与计算的参数(所有参数均参与计算),修改之后的代码反而更加简洁。
实现逻辑:“前向计算输出、根据输出和真实值计算Loss、基于Loss和输入计算梯度、根据梯度更新参数值”四个部分反复执行,直到到损失函数最小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class Network (object ): def __init__ (self, num_of_weights ): np.random.seed(0 ) self.w = np.random.randn(num_of_weights, 1 ) self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z def loss (self, z, y ): error = z - y num_samples = error.shape[0 ] cost = error * error cost = np.sum (cost) / num_samples return cost def gradient (self, x, y ): z = self.forward(x) gradient_w = (z-y)*x gradient_w = np.mean(gradient_w, axis=0 ) gradient_w = gradient_w[:, np.newaxis] gradient_b = (z - y) gradient_b = np.mean(gradient_b) return gradient_w, gradient_b def update (self, gradient_w, gradient_b, eta = 0.01 ): self.w = self.w - eta * gradient_w self.b = self.b - eta * gradient_b def train (self, x, y, iterations=100 , eta=0.01 ): losses = [] for i in range (iterations): z = self.forward(x) L = self.loss(z, y) gradient_w, gradient_b = self.gradient(x, y) self.update(gradient_w, gradient_b, eta) losses.append(L) if (i+1 ) % 10 == 0 : print ('iter {}, loss {}' .format (i, L)) return losses train_data, test_data = load_data() x = train_data[:, :-1 ] y = train_data[:, -1 :] net = Network(13 ) num_iterations=1000 losses = net.train(x,y, iterations=num_iterations, eta=0.01 ) plot_x = np.arange(num_iterations) plot_y = np.array(losses) plt.plot(plot_x, plot_y) plt.show()
PY
2.4.7 随机梯度下降法( Stochastic Gradient Descent) 在上述程序中,每次损失函数和梯度计算都是基于数据集中的全量数据。对于波士顿房价预测任务数据集而言,样本数比较少,只有404个。但在实际问题中,数据集往往非常大,如果每次都使用全量数据进行计算,效率非常低,通俗地说就是“杀鸡焉用牛刀”。由于参数每次只沿着梯度反方向更新一点点,因此方向并不需要那么精确。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
mini-batch:每次迭代时抽取出来的一批数据被称为一个mini-batch。
batch_size:一个mini-batch所包含的样本数目称为batch_size。
epoch:当程序迭代的时候,按mini-batch逐渐抽取出样本,当把整个数据集都遍历到了的时候,则完成了一轮训练,也叫一个epoch。启动训练时,可以将训练的轮数num_epochs和batch_size作为参数传入。
下面结合程序介绍具体的实现过程,涉及到数据处理和训练过程两部分代码的修改。
数据处理需要实现拆分数据批次和样本乱序(为了实现随机抽样的效果)两个功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import numpy as npclass Network (object ): def __init__ (self, num_of_weights ): self.w = np.random.randn(num_of_weights, 1 ) self.b = 0. def forward (self, x ): z = np.dot(x, self.w) + self.b return z def loss (self, z, y ): error = z - y num_samples = error.shape[0 ] cost = error * error cost = np.sum (cost) / num_samples return cost def gradient (self, x, y ): z = self.forward(x) N = x.shape[0 ] gradient_w = 1. / N * np.sum ((z-y) * x, axis=0 ) gradient_w = gradient_w[:, np.newaxis] gradient_b = 1. / N * np.sum (z-y) return gradient_w, gradient_b def update (self, gradient_w, gradient_b, eta = 0.01 ): self.w = self.w - eta * gradient_w self.b = self.b - eta * gradient_b def train (self, training_data, num_epochs, batch_size=10 , eta=0.01 ): n = len (training_data) losses = [] for epoch_id in range (num_epochs): np.random.shuffle(training_data) mini_batches = [training_data[k:k+batch_size] for k in range (0 , n, batch_size)] for iter_id, mini_batch in enumerate (mini_batches): x = mini_batch[:, :-1 ] y = mini_batch[:, -1 :] a = self.forward(x) loss = self.loss(a, y) gradient_w, gradient_b = self.gradient(x, y) self.update(gradient_w, gradient_b, eta) losses.append(loss) print ('Epoch {:3d} / iter {:3d}, loss = {:.4f}' . format (epoch_id, iter_id, loss)) return losses train_data, test_data = load_data() net = Network(13 ) losses = net.train(train_data, num_epochs=50 , batch_size=100 , eta=0.1 ) plot_x = np.arange(len (losses)) plot_y = np.array(losses) plt.plot(plot_x, plot_y) plt.show()
PY
观察上述Loss的变化,随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降曲线会出现震荡。
说明:
由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升。
2.5 模型保存 Numpy提供了save接口,可直接将模型权重数组保存为.npy格式的文件。
1 2 np.save('w.npy' , net.w) np.save('b.npy' , net.b)
PY
2.5 小结 本节我们详细介绍了如何使用NumPy实现梯度下降算法,构建并训练了一个简单的线性模型实现波士顿房价预测,可以总结出,使用神经网络建模房价预测有三个要点:
构建网络,初始化参数和,定义预测和损失函数的计算方法。
随机选择初始点,建立梯度的计算方法和参数更新方式。
从总的数据集中抽取部分数据作为一个mini_batch,计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。
2.7 框架实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 import paddleimport paddle.fluid as fluidimport paddle.fluid.dygraph as dygraphfrom paddle.fluid.dygraph import Linearimport numpy as npimport osimport randomdef load_data (): datafile = 'housing.data' data = np.fromfile(datafile, sep=' ' , dtype=np.float32) feature_names = [ 'CRIM' , 'ZN' , 'INDUS' , 'CHAS' , 'NOX' , 'RM' , 'AGE' , \ 'DIS' , 'RAD' , 'TAX' , 'PTRATIO' , 'B' , 'LSTAT' , 'MEDV' ] feature_num = len (feature_names) data = data.reshape([data.shape[0 ] // feature_num, feature_num]) ratio = 0.8 offset = int (data.shape[0 ] * ratio) training_data = data[:offset] maximums, minimums, avgs = training_data.max (axis=0 ), training_data.min (axis=0 ), training_data.mean(axis=0 ) global max_values global min_values global avg_values max_values = maximums min_values = minimums avg_values = avgs for i in range (feature_num): data[:, i] = (data[:, i] - avg_values[i]) / (maximums[i] - minimums[i]) training_data = data[:offset] test_data = data[offset:] return training_data, test_data training_dataverify, test_dataverify = load_data()print (training_dataverify.shape)print (training_dataverify[0 ,:])class Regressor (fluid.dygraph.Layer): def __init__ (self,name_scope ): super (Regressor, self).__init__(name_scope) name_scope = self.full_name() self.fc = Linear(input_dim=13 , output_dim=1 ,act=None ) def forward (self, inputs ): x = self.fc(inputs) return x with fluid.dygraph.guard(): model = Regressor("Regressor" ) model.train() training_data, test_data = load_data() opt = fluid.optimizer.SGD(learning_rate=0.01 , parameter_list=model.parameters()) with dygraph.guard(fluid.CPUPlace()): EPOCH_NUM = 10 BATCH_SIZE = 10 for epoch_id in range (EPOCH_NUM): np.random.shuffle(training_data) mini_batches = [training_data[k:k+BATCH_SIZE] for k in range (0 , len (training_data), BATCH_SIZE)] for iter_id, mini_batch in enumerate (mini_batches): x = np.array(mini_batch[:,:-1 ]).astype('float32' ) y = np.array(mini_batch[:,-1 :]).astype('float32' ) house_features = dygraph.to_variable(x) prices = dygraph.to_variable(y) predicts = model(house_features) loss = fluid.layers.square_error_cost(predicts, label=prices) avg_loss = fluid.layers.mean(loss) if iter_id%20 ==0 : print ("epoch:{},iter_id:{},loss is:{}" .format (epoch_id, iter_id, avg_loss.numpy())) avg_loss.backward() opt.minimize(avg_loss) model.clear_gradients() fluid.save_dygraph(model.state_dict(), 'LRmodel' )def load_one_example (data_dir ): f = open (data_dir,"r" ) datas = f.readlines() tmp = datas[-9 ] tmp = tmp.strip().split() one_data = [float (v) for v in tmp] for i in range (len (one_data)-1 ): one_data[i] = (one_data[i]-avg_values[i])/(max_values[i]-min_values[i]) data = np.reshape(np.array(one_data[:-1 ]),[1 ,-1 ]).astype(np.float32) label = one_data[-1 ] return data, labelwith dygraph.guard(): model_dict, _ = fluid.load_dygraph('LR_model' ) model.load_dict(model_dict) model.eval () test_data, label = load_one_example('housing.data' ) test_data = dygraph.to_variable(test_data) results = model(test_data) results = results * (max_values[-1 ]-min_values[-1 ]) + avg_values[-1 ] print ("Inference result is {}, the corresponding label is {}" .format (results.numpy(), label))
PY
3.实例二“手写文字识别” 1.单层网络多元逻辑回归模型 模型设计 在房价预测深度学习任务中,我们使用了单层且没有非线性变换的模型,取得了理想的预测效果。在手写数字识别中,我们依然使用这个模型预测输入的图形数字值。其中,模型的输入为784维(28×28)数据,输出为1维数据,如 图5 所示。
图5:手写数字识别网络模型
输入像素的位置排布信息对理解图像内容非常重要(如将原始尺寸为28×28图像的像素按照7×112的尺寸排布,那么其中的数字将不可识别),因此网络的输入设计为28×28的尺寸,而不是1×784,以便于模型能够正确处理像素之间的空间信息。
说明:
事实上,采用只有一层的简单网络(对输入求加权和)时并没有处理位置关系信息,因此可以猜测出此模型的预测效果可能有限。在后续优化环节介绍的卷积神经网络则更好的考虑了这种位置关系信息,模型的预测效果也会有显著提升。
下面以类的方式组建手写数字识别的网络,实现方法如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 class MNIST (paddle.nn.Layer): def __init__ (self ): super (MNIST, self).__init__() self.fc = paddle.nn.Linear(in_features=784 , out_features=1 ) def forward (self, inputs ): outputs = self.fc(inputs) return outputs
PY
训练配置 训练配置需要先生成模型实例(设为“训练”状态),再设置优化算法和学习率(使用随机梯度下降SGD,学习率设置为0.001),实现方法如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 model = MNIST()def train (model ): model.train() train_loader = paddle.io.DataLoader(paddle.vision.datasets.MNIST(mode='train' ), batch_size=16 , shuffle=True ) opt = paddle.optimizer.SGD(learning_rate=0.001 , parameters=model.parameters())
PY
训练过程 训练过程采用二层循环嵌套方式,训练完成后需要保存模型参数,以便后续使用。
内层循环:负责整个数据集的一次遍历,遍历数据集采用分批次(batch)方式。
外层循环:定义遍历数据集的次数,本次训练中外层循环10次,通过参数EPOCH_NUM设置。
1 2 3 4 5 6 7 8 9 10 11 def norm_img (img ): assert len (img.shape) == 3 batch_size, img_h, img_w = img.shape[0 ], img.shape[1 ], img.shape[2 ] img = img / 255 img = paddle.reshape(img, [batch_size, img_h*img_w]) return img
PY
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import paddle paddle.vision.set_image_backend('cv2' ) model = MNIST()def train (model ): model.train() train_loader = paddle.io.DataLoader(paddle.vision.datasets.MNIST(mode='train' ), batch_size=16 , shuffle=True ) opt = paddle.optimizer.SGD(learning_rate=0.001 , parameters=model.parameters()) EPOCH_NUM = 10 for epoch in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images = norm_img(data[0 ]).astype('float32' ) labels = data[1 ].astype('float32' ) predicts = model(images) loss = F.square_error_cost(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 1000 == 0 : print ("epoch_id: {}, batch_id: {}, loss is: {}" .format (epoch, batch_id, avg_loss.numpy())) avg_loss.backward() opt.step() opt.clear_grad() train(model) paddle.save(model.state_dict(), './mnist.pdparams' )
PY
另外,从训练过程中损失所发生的变化可以发现,虽然损失整体上在降低,但到训练的最后一轮,损失函数值依然较高。可以猜测手写数字识别完全复用房价预测的代码,训练效果并不好。接下来我们通过模型测试,获取模型训练的真实效果。
模型测试 模型测试的主要目的是验证训练好的模型是否能正确识别出数字,包括如下四步:
声明实例
加载模型:加载训练过程中保存的模型参数,
灌入数据:将测试样本传入模型,模型的状态设置为校验状态(eval),显式告诉框架我们接下来只会使用前向计算的流程,不会计算梯度和梯度反向传播。
获取预测结果,取整后作为预测标签输出。
在模型测试之前,需要先从’./work/example_0.png’文件中读取样例图片,并进行归一化处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import matplotlib.pyplot as pltimport numpy as npfrom PIL import Image train_loader = paddle.io.DataLoader(paddle.vision.datasets.MNIST(mode='train' ), batch_size=16 , shuffle=True ) img_path = './data/data17152/example_0.png' im = Image.open (img_path) plt.imshow(im) plt.show() im = im.convert('L' )print ('原始图像shape: ' , np.array(im).shape) im = im.resize((28 , 28 ), Image.ANTIALIAS) plt.imshow(im) plt.show()print ("采样后图片shape: " , np.array(im).shape)
PY
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 def load_image (img_path ): im = Image.open (img_path).convert('L' ) im = im.resize((28 , 28 ), Image.ANTIALIAS) im = np.array(im).reshape(1 , -1 ).astype(np.float32) im = 1 - im / 255 return im model = MNIST() params_file_path = 'mnist.pdparams' img_path = './data/data17152/example_0.png' param_dict = paddle.load(params_file_path) model.load_dict(param_dict) model.eval () tensor_img = load_image(img_path) result = model(paddle.to_tensor(tensor_img))print ('result' ,result)print ("本次预测的数字是" , result.numpy().astype('int32' ))
PY
小结 灰度图:0是黑,255是白
重要函数:
enumerate(train_loader())
将迭代器进行迭代
np.reshape(1, -1)
将ndtype数组转为1维行向量,**-1为占位符**,列数看有多少列就多少列
2.多层网络改进模型 数据同步读取与训练 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import paddlefrom paddle.nn import Linearimport paddle.nn.functional as Fimport osimport gzipimport jsonimport randomimport numpy as npdef load_data (mode='train' ): datafile = './work/mnist.json.gz' print ('loading mnist dataset from {} ......' .format (datafile)) data = json.load(gzip.open (datafile)) print ('mnist dataset load done' ) train_set, val_set, eval_set = data if mode=='train' : imgs, labels = train_set[0 ], train_set[1 ] elif mode=='valid' : imgs, labels = val_set[0 ], val_set[1 ] elif mode=='eval' : imgs, labels = eval_set[0 ], eval_set[1 ] else : raise Exception("mode can only be one of ['train', 'valid', 'eval']" ) print ("训练数据集数量: " , len (imgs)) imgs_length = len (imgs) assert len (imgs) == len (labels), \ "length of train_imgs({}) should be the same as train_labels({})" .format (len (imgs), len (labels)) imgs_length = len (imgs) index_list = list (range (imgs_length)) BATCHSIZE = 100 def data_generator (): if mode == 'train' : random.shuffle(index_list) imgs_list = [] labels_list = [] for i in index_list: img = np.array(imgs[i]).astype('float32' ) label = np.array(labels[i]).astype('float32' ) imgs_list.append(img) labels_list.append(label) if len (imgs_list) == BATCHSIZE: yield np.array(imgs_list), np.array(labels_list) imgs_list = [] labels_list = [] if len (imgs_list) > 0 : yield np.array(imgs_list), np.array(labels_list) return data_generator
PYTHON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class MNIST (paddle.nn.Layer): def __init__ (self ): super (MNIST, self).__init__() self.fc = paddle.nn.Linear(in_features=784 , out_features=1 ) def forward (self, inputs ): outputs = self.fc(inputs) return outputsdef train (model ): model = MNIST() model.train() train_loader = load_data('train' ) opt = paddle.optimizer.SGD(learning_rate=0.001 , parameters=model.parameters()) EPOCH_NUM = 10 for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images, labels = data images = paddle.to_tensor(images) labels = paddle.to_tensor(labels) predits = model(images) loss = F.square_error_cost(predits, labels) avg_loss = paddle.mean(loss) if batch_id % 200 == 0 : print ("epoch: {}, batch: {}, loss is: {}" .format (epoch_id, batch_id, avg_loss.numpy())) avg_loss.backward() opt.step() opt.clear_grad() paddle.save(model.state_dict(), './mnist.pdparams' ) model = MNIST() train(model)
PYTHON
上面提到的数据读取采用的是同步数据读取方式。对于样本量较大、数据读取较慢的场景,建议采用异步数据读取方式。异步读取数据时,数据读取和模型训练并行执行,从而加快了数据读取速度,牺牲一小部分内存换取数据读取效率的提升,二者关系如 图4 所示。
图4:同步数据读取和异步数据读取示意图
* **同步数据读取**:数据读取与模型训练串行。当模型需要数据时,才运行数据读取函数获得当前批次的数据。在读取数据期间,模型一直等待数据读取结束才进行训练,数据读取速度相对较慢。
* **异步数据读取**:数据读取和模型训练并行。读取到的数据不断的放入缓存区,无需等待模型训练就可以启动下一轮数据读取。当模型训练完一个批次后,不用等待数据读取过程,直接从缓存区获得下一批次数据进行训练,从而加快了数据读取速度。
* **异步队列**:数据读取和模型训练交互的仓库,二者均可以从仓库中读取数据,它的存在使得两者的工作节奏可以解耦。
使用飞桨实现异步数据读取非常简单,只需要两个步骤:
构建一个继承paddle.io.Dataset类的数据读取器。
通过paddle.io.DataLoader创建异步数据读取的迭代器。
数据异步读取与训练 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import paddleimport jsonimport gzipimport numpy as npclass MnistDataset (paddle.io.Dataset): def __init__ (self, mode ): datafile = './work/mnist.json.gz' data = json.load(gzip.open (datafile)) train_set, val_set, eval_set = data if mode=='train' : imgs, labels = train_set[0 ], train_set[1 ] elif mode=='valid' : imgs, labels = val_set[0 ], val_set[1 ] elif mode=='eval' : imgs, labels = eval_set[0 ], eval_set[1 ] else : raise Exception("mode can only be one of ['train', 'valid', 'eval']" ) imgs_length = len (imgs) assert len (imgs) == len (labels), \ "length of train_imgs({}) should be the same as train_labels({})" .format (len (imgs), len (labels)) self.imgs = imgs self.labels = labels def __getitem__ (self, idx ): img = np.array(self.imgs[idx]).astype('float32' ) label = np.array(self.labels[idx]).astype('float32' ) return img, label def __len__ (self ): return len (self.imgs)
PY
1 2 3 4 5 6 7 8 9 10 11 12 train_dataset = MnistDataset(mode='train' ) data_loader = paddle.io.DataLoader(train_dataset, batch_size=100 , shuffle=True )for i, data in enumerate (data_loader()): images, labels = data print (i, images.shape, labels.shape) if i>=2 : break
PY
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def train (model ): model = MNIST() model.train() opt = paddle.optimizer.SGD(learning_rate=0.001 , parameters=model.parameters()) EPOCH_NUM = 10 for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (data_loader()): images, labels = data images = paddle.to_tensor(images) labels = paddle.to_tensor(labels).astype('float32' ) predicts = model(images) loss = F.square_error_cost(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 200 == 0 : print ("epoch: {}, batch: {}, loss is: {}" .format (epoch_id, batch_id, avg_loss.numpy())) avg_loss.backward() opt.step() opt.clear_grad() paddle.save(model.state_dict(), 'mnist' ) model = MNIST() train(model)
PY
小结
迭代器 迭代器有两个基本的方法:创建迭代器iter() 和 访问迭代器**next()**。
可以直接作用于for循环的对象统称为可迭代对象:Iterable
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
生成器
如果列表元素可以按照某种算法推算出来,可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
yield:是一个生成器函数,返回的是一个迭代器
yield的作用:返回一个可以用来迭代(for循环)的生成器,它的应用场景通常为一个需要返回一系列值的,含有循环的函数中。
assert:assert a > 0,"a超出范围"
如果a>0则顺序执行,否则抛出异常提示a超出范围
3.模型设计之损失函数 在之前的方案中,我们复用了房价预测模型的损失函数-均方误差。从预测效果来看,虽然损失不断下降,模型的预测值逐渐逼近真实值,但模型的最终效果不够理想。究其根本,不同的深度学习任务需要有各自适宜的损失函数。我们以房价预测和手写数字识别两个任务为例,详细剖析其中的缘由如下:
房价预测是回归任务,而手写数字识别是分类任务,使用均方误差作为分类任务的损失函数存在逻辑和效果上的缺欠。
房价可以是大于0的任何浮点数,而手写数字识别的输出只可能是0~9之间的10个整数,相当于一种标签。
在房价预测的案例中,由于房价本身是一个连续的实数值,因此以模型输出的数值和真实房价差距作为损失函数(Loss)是符合道理的。但对于分类问题,真实结果是分类标签,而模型输出是实数值,导致以两者相减作为损失不具备物理含义。
Softmax函数 如果模型能输出10个标签的概率,对应真实标签的概率输出尽可能接近100%,而其他标签的概率输出尽可能接近0%,且所有输出概率之和为1。这是一种更合理的假设!与此对应,真实的标签值可以转变成一个10维度的one-hot向量,在对应数字的位置上为1,其余位置为0,比如标签“6”可以转变成[0,0,0,0,0,0,1,0,0,0]。
为了实现上述思路,需要引入Softmax函数,它可以将原始输出转变成对应标签的概率,公式如下,其中是标签类别个数。 从公式的形式可见,每个输出的范围均在0~1之间,且所有输出之和等于1,这是这种变换后可被解释成概率的基本前提。对应到代码上,需要在前向计算中,对全连接网络的输出层增加一个Softmax运算,outputs = F.softmax(outputs)
。
图3 是一个三个标签的分类模型(三分类)使用的Softmax输出层,从中可见原始输出的三个数字3、1、-3,经过Softmax层后转变成加和为1的三个概率值0.88、0.12、0。
交叉熵 在模型输出为分类标签的概率时,直接以标签和概率做比较也不够合理,人们更习惯使用交叉熵误差作为分类问题的损失衡量。
交叉熵损失函数的设计是基于最大似然思想:最大概率得到观察结果的假设是真的。如何理解呢?举个例子来说,如 图7 所示。有两个外形相同的盒子,甲盒中有99个白球,1个蓝球;乙盒中有99个蓝球,1个白球。一次试验取出了一个蓝球,请问这个球应该是从哪个盒子中取出的?
图7:体会最大似然的思想
相信大家简单思考后均会得出更可能是从乙盒中取出的,因为从乙盒中取出一个蓝球的概率更高,所以观察到一个蓝球更可能是从乙盒中取出的。是观测的数据,即蓝球白球;是模型,即甲盒乙盒。这就是贝叶斯公式所表达的思想:
依据贝叶斯公式,某二分类模型“生成”个训练样本的概率:
说明:
对于二分类问题,模型为,为Sigmoid函数。当=1,概率为;当=0,概率为。
经过公式推导,使得上述概率最大等价于最小化交叉熵,得到交叉熵的损失函数。交叉熵的公式如下:
其中,表示以为底数的自然对数。代表模型输出,代表各个标签。中只有正确解的标签为1,其余均为0(one-hot表示)。
因此,交叉熵只计算对应着“正确解”标签的输出的自然对数。比如,假设正确标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是;若“2”对应的输出是0.1,则交叉熵误差为。由此可见,交叉熵误差的值是由正确标签所对应的输出结果决定的。
自然对数的函数曲线可由如下代码实现。
1 2 3 4 5 6 7 8 9 10 import matplotlib.pyplot as pltimport numpy as np x = np.arange(0.01 ,1 ,0.01 ) y = np.log(x) plt.title("y=log(x)" ) plt.xlabel("x" ) plt.ylabel("y" ) plt.plot(x,y) plt.show() plt.figure()
PY
总结
分类任务
数据处理部分:将输入的标签label数据类型改为int64型
网络定义部分:全连接层输出应该用SoftMax处理,将单一输出改为各类的输出概率,所有概率之和为1。
训练过程部分:损失函数改为交叉熵
4.训练配置之优化器 设置学习率 在深度学习神经网络模型中,通常使用标准的随机梯度下降算法更新参数,学习率代表参数更新幅度的大小,即步长。当学习率最优时,模型的有效容量最大,最终能达到的效果最好。学习率和深度学习任务类型有关,合适的学习率往往需要大量的实验和调参经验。探索学习率最优值时需要注意如下两点:
学习率不是越小越好 。学习率越小,损失函数的变化速度越慢,意味着我们需要花费更长的时间进行收敛,如 图2 左图所示。
学习率不是越大越好 。只根据总样本集中的一个批次计算梯度,抽样误差会导致计算出的梯度不是全局最优的方向,且存在波动。在接近最优解时,过大的学习率会导致参数在最优解附近震荡,损失难以收敛,如 图2 右图所示。
在训练前,我们往往不清楚一个特定问题设置成怎样的学习率是合理的,因此在训练时可以尝试调小或调大,通过观察Loss下降的情况判断合理的学习率.
学习率的主流优化算法 学习率是优化器的一个参数,调整学习率看似是一件非常麻烦的事情,需要不断的调整步长,观察训练时间和Loss的变化。经过研究员的不断的实验,当前已经形成了四种比较成熟的优化算法:SGD、Momentum、AdaGrad和Adam,效果如 图3 所示。
SGD: 随机梯度下降算法,每次训练少量数据,抽样偏差导致的参数收敛过程中震荡。
Momentum: 引入物理“动量”的概念,累积速度,减少震荡,使参数更新的方向更稳定。
每个批次的数据含有抽样误差,导致梯度更新的方向波动较大。如果我们引入物理动量的概念,给梯度下降的过程加入一定的“惯性”累积,就可以减少更新路径上的震荡,即每次更新的梯度由“历史多次梯度的累积方向”和“当次梯度”加权相加得到。历史多次梯度的累积方向往往是从全局视角更正确的方向,这与“惯性”的物理概念很像,也是为何其起名为“Momentum”的原因。类似不同品牌和材质的篮球有一定的重量差别,街头篮球队中的投手(擅长中远距离投篮)喜欢稍重篮球的比例较高。一个很重要的原因是,重的篮球惯性大,更不容易受到手势的小幅变形或风吹的影响。
AdaGrad: 根据不同参数距离最优解的远近,动态调整学习率。学习率逐渐下降,依据各参数变化大小调整学习率。
通过调整学习率的实验可以发现:当某个参数的现值距离最优解较远时(表现为梯度的绝对值较大),我们期望参数更新的步长大一些,以便更快收敛到最优解。当某个参数的现值距离最优解较近时(表现为梯度的绝对值较小),我们期望参数的更新步长小一些,以便更精细的逼近最优解。类似于打高尔夫球,专业运动员第一杆开球时,通常会大力打一个远球,让球尽量落在洞口附近。当第二杆面对离洞口较近的球时,他会更轻柔而细致的推杆,避免将球打飞。与此类似,参数更新的步长应该随着优化过程逐渐减少,减少的程度与当前梯度的大小有关。根据这个思想编写的优化算法称为“AdaGrad”,Ada是Adaptive的缩写,表示“适应环境而变化”的意思。RMSProp 是在AdaGrad基础上的改进,学习率随着梯度变化而适应,解决AdaGrad学习率急剧下降的问题。
Adam: 由于动量和自适应学习率两个优化思路是正交的,因此可以将两个思路结合起来,这就是当前广泛应用的算法。
5.调试优化 训练过程优化思路主要有如下五个关键环节:
1. 计算分类准确率,观测模型训练效果。
交叉熵损失函数只能作为优化目标,无法直接准确衡量模型的训练效果。准确率可以直接衡量训练效果,但由于其离散性质,不适合做为损失函数优化神经网络。
2. 检查模型训练过程,识别潜在问题。
如果模型的损失或者评估指标表现异常,通常需要打印模型每一层的输入和输出来定位问题,分析每一层的内容来获取错误的原因。
3. 加入校验或测试,更好评价模型效果。
理想的模型训练结果是在训练集和验证集上均有较高的准确率,如果训练集的准确率低于验证集,说明网络训练程度不够;如果训练集的准确率高于验证集,可能是发生了过拟合 现象。通过在优化目标中加入正则化项的办法,解决过拟合的问题。
4. 加入正则化项,避免模型过拟合。
飞桨框架支持为整体参数加入正则化项,这是通常的做法。此外,飞桨框架也支持为某一层或某一部分的网络单独加入正则化项,以达到精细调整参数训练的效果。
5. 可视化分析。
用户不仅可以通过打印或使用matplotlib库作图,飞桨还提供了更专业的可视化分析工具VisualDL,提供便捷的可视化分析方法。
计算模型的分类准确率 准确率是一个直观衡量分类模型效果的指标,由于这个指标是离散的,因此不适合作为损失来优化。通常情况下,交叉熵损失越小的模型,分类的准确率也越高。基于分类准确率,我们可以公平地比较两种损失函数的优劣,例如在【手写数字识别】之损失函数章节中均方误差和交叉熵的比较。
使用飞桨提供的计算分类准确率API,可以直接计算准确率。
class paddle.metric.Accuracy
该API的输入参数input为预测的分类结果predict,输入参数label为数据真实的label。飞桨还提供了更多衡量模型效果的计算指标,详细可以查看paddle.meric包下面的API。
在下述代码中,我们在模型前向计算过程forward函数中计算分类准确率,并在训练时打印每个批次样本的分类准确率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def forward (self, inputs, label ): x = self.conv1(inputs) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = paddle.reshape(x, [x.shape[0 ], 980 ]) x = self.fc(x) if label is not None : acc = paddle.metric.accuracy(input =x, label=label) return x, acc else : return x
PYTHON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def train (model ): model = MNIST() model.train() opt = paddle.optimizer.Adam(learning_rate=0.01 , parameters=model.parameters()) EPOCH_NUM = 2 for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images, labels = data images = paddle.to_tensor(images) labels = paddle.to_tensor(labels) predicts, acc = model(images, labels) loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 200 == 0 : print ("epoch: {}, batch: {}, loss is: {}, acc is {}" .format (epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) avg_loss.backward() opt.step() opt.clear_grad() paddle.save(model.state_dict(), 'mnist.pdparams' )
PYTHON
检查模型训练过程,识别潜在训练问题 使用飞桨动态图编程可以方便的查看和调试训练的执行过程。在网络定义的Forward函数中,可以打印每一层输入输出的尺寸,以及每层网络的参数。通过查看这些信息,不仅可以更好地理解训练的执行过程,还可以发现潜在问题,或者启发继续优化的思路。
在下述程序中,使用check_shape
变量控制是否打印“尺寸”,验证网络结构是否正确。使用check_content
变量控制是否打印“内容值”,验证数据分布是否合理。假如在训练中发现中间层的部分输出持续为0,说明该部分的网络结构设计存在问题,没有充分利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 def forward (self, inputs, label=None , check_shape=False , check_content=False ): outputs1 = self.conv1(inputs) outputs2 = F.relu(outputs1) outputs3 = self.max_pool1(outputs2) outputs4 = self.conv2(outputs3) outputs5 = F.relu(outputs4) outputs6 = self.max_pool2(outputs5) outputs6 = paddle.reshape(outputs6, [outputs6.shape[0 ], -1 ]) outputs7 = self.fc(outputs6) if check_shape: print ("\n########## print network layer's superparams ##############" ) print ("conv1-- kernel_size:{}, padding:{}, stride:{}" .format (self.conv1.weight.shape, self.conv1._padding, self.conv1._stride)) print ("conv2-- kernel_size:{}, padding:{}, stride:{}" .format (self.conv2.weight.shape, self.conv2._padding, self.conv2._stride)) print ("fc-- weight_size:{}, bias_size_{}" .format (self.fc.weight.shape, self.fc.bias.shape)) print ("\n########## print shape of features of every layer ###############" ) print ("inputs_shape: {}" .format (inputs.shape)) print ("outputs1_shape: {}" .format (outputs1.shape)) print ("outputs2_shape: {}" .format (outputs2.shape)) print ("outputs3_shape: {}" .format (outputs3.shape)) print ("outputs4_shape: {}" .format (outputs4.shape)) print ("outputs5_shape: {}" .format (outputs5.shape)) print ("outputs6_shape: {}" .format (outputs6.shape)) print ("outputs7_shape: {}" .format (outputs7.shape)) if check_content: print ("\n########## print convolution layer's kernel ###############" ) print ("conv1 params -- kernel weights:" , self.conv1.weight[0 ][0 ]) print ("conv2 params -- kernel weights:" , self.conv2.weight[0 ][0 ]) idx1 = np.random.randint(0 , outputs1.shape[1 ]) idx2 = np.random.randint(0 , outputs4.shape[1 ]) print ("\nThe {}th channel of conv1 layer: " .format (idx1), outputs1[0 ][idx1]) print ("The {}th channel of conv2 layer: " .format (idx2), outputs4[0 ][idx2]) print ("The output of last layer:" , outputs7[0 ], '\n' ) if label is not None : acc = paddle.metric.accuracy(input =F.softmax(outputs7), label=label) return outputs7, acc else : return outputs7
PY
加入校验或测试,更好评价模型效果 在训练过程中,我们会发现模型在训练样本集上的损失在不断减小。但这是否代表模型在未来的应用场景上依然有效?为了验证模型的有效性,通常将样本集合分成三份,训练集、校验集和测试集。
训练集 :用于训练模型的参数,即训练过程中主要完成的工作。
校验集 :用于对模型超参数的选择,比如网络结构的调整、正则化项权重的选择等。
测试集 :用于模拟模型在应用后的真实效果。因为测试集没有参与任何模型优化或参数训练的工作,所以它对模型来说是完全未知的样本。在不以校验数据优化网络结构或模型超参数时,校验数据和测试数据的效果是类似的,均更真实的反映模型效果。
如下程序读取上一步训练保存的模型参数,读取校验数据集,并测试模型在校验数据集上的效果。
加入正则化项,避免模型过拟合 过拟合现象 对于样本量有限、但需要使用强大模型的复杂任务,模型很容易出现过拟合的表现,即在训练集上的损失小,在验证集或测试集上的损失较大,如 图2 所示。
反之,如果模型在训练集和测试集上均损失较大,则称为欠拟合。过拟合表示模型过于敏感,学习到了训练数据中的一些误差,而这些误差并不是真实的泛化规律(可推广到测试集上的规律)。欠拟合表示模型还不够强大,还没有很好的拟合已知的训练样本,更别提测试样本了。因为欠拟合情况容易观察和解决,只要训练loss不够好,就不断使用更强大的模型即可,因此实际中我们更需要处理好过拟合的问题。
导致过拟合原因 造成过拟合的原因是模型过于敏感,而训练数据量太少或其中的噪音太多。
如图3 所示,理想的回归模型是一条坡度较缓的抛物线,欠拟合的模型只拟合出一条直线,显然没有捕捉到真实的规律,但过拟合的模型拟合出存在很多拐点的抛物线,显然是过于敏感,也没有正确表达真实规律。
如图4 所示,理想的分类模型是一条半圆形的曲线,欠拟合用直线作为分类边界,显然没有捕捉到真实的边界,但过拟合的模型拟合出很扭曲的分类边界,虽然对所有的训练数据正确分类,但对一些较为个例的样本所做出的妥协,高概率不是真实的规律。
过拟合的成因与防控 为了更好的理解过拟合的成因,可以参考侦探定位罪犯的案例逻辑,如 图5 所示。
对于这个案例,假设侦探也会犯错,通过分析发现可能的原因:
情况1:罪犯证据存在错误,依据错误的证据寻找罪犯肯定是缘木求鱼。
情况2:搜索范围太大的同时证据太少,导致符合条件的候选(嫌疑人)太多,无法准确定位罪犯。
那么侦探解决这个问题的方法有两种:或者缩小搜索范围(比如假设该案件只能是熟人作案),或者寻找更多的证据。
归结到深度学习中,假设模型也会犯错,通过分析发现可能的原因:
情况1:训练数据存在噪音,导致模型学到了噪音,而不是真实规律。
情况2:使用强大模型(表示空间大)的同时训练数据太少,导致在训练数据上表现良好的候选假设太多,锁定了一个“虚假正确”的假设。
对于情况1,我们使用数据清洗和修正来解决。 对于情况2,我们或者限制模型表示能力,或者收集更多的训练数据。
而清洗训练数据中的错误,或收集更多的训练数据往往是一句“正确的废话”,在任何时候我们都想获得更多更高质量的数据。在实际项目中,更快、更低成本可控制过拟合的方法,只有限制模型的表示能力。
正则化项 为了防止模型过拟合,在没有扩充样本量的可能下,只能降低模型的复杂度,可以通过限制参数的数量或可能取值(参数值尽量小)实现。
具体来说,在模型的优化目标(损失)中人为加入对参数规模的惩罚项。当参数越多或取值越大时,该惩罚项就越大。通过调整惩罚项的权重系数,可以使模型在“尽量减少训练损失”和“保持模型的泛化能力”之间取得平衡。泛化能力表示模型在没有见过的样本上依然有效。正则化项的存在,增加了模型在训练集上的损失。
飞桨支持为所有参数加上统一的正则化项,也支持为特定的参数添加正则化项。前者的实现如下代码所示,仅在优化器中设置weight_decay
参数即可实现。使用参数coeff
调节正则化项的权重,权重越大时,对模型复杂度的惩罚越高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def train (model ): model.train() opt = paddle.optimizer.Adam(learning_rate=0.01 , weight_decay=paddle.regularizer.L2Decay(coeff=1e-5 ), parameters=model.parameters()) EPOCH_NUM = 5 for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images, labels = data images = paddle.to_tensor(images) labels = paddle.to_tensor(labels) predicts, acc = model(images, labels) loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 200 == 0 : print ("epoch: {}, batch: {}, loss is: {}, acc is {}" .format (epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) avg_loss.backward() opt.step() opt.clear_grad() paddle.save(model.state_dict(), 'mnist_regul.pdparams' ) model = MNIST() train(model)
PY
6.可视化分析 训练模型时,经常需要观察模型的评价指标,分析模型的优化过程,以确保训练是有效的。可选用这两种工具:Matplotlib库和VisualDL。
Matplotlib库 :Matplotlib库是Python中使用的最多的2D图形绘图库,它有一套完全仿照MATLAB的函数形式的绘图接口,使用轻量级的PLT库(Matplotlib)作图是非常简单的。
VisualDL :如果期望使用更加专业的作图工具,可以尝试VisualDL,飞桨可视化分析工具。VisualDL能够有效地展示飞桨在运行过程中的计算图、各种指标变化趋势和数据信息。
使用Matplotlib库绘制损失随训练下降的曲线图 将训练的批次编号作为X轴坐标,该批次的训练损失作为Y轴坐标。
训练开始前,声明两个列表变量存储对应的批次编号(iters=[])和训练损失(losses=[])。
1 2 3 4 iters=[] losses=[]for epoch_id in range (EPOCH_NUM): """start to training"""
PY
随着训练的进行,将iter和losses两个列表填满。
1 2 3 4 5 6 7 8 9 10 11 12 13 import paddle.nn.functional as F iters=[] losses=[]for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images, labels = data predicts, acc = model(images, labels) loss = F.cross_entropy(predicts, label = labels.astype('int64' )) avg_loss = paddle.mean(loss) iters.append(batch_id + epoch_id*len (list (train_loader())) losses.append(avg_loss)
PY
训练结束后,将两份数据以参数形式导入PLT的横纵坐标。
1 plt.xlabel("iter" , fontsize=14 ),plt.ylabel("loss" , fontsize=14 )
PY
最后,调用plt.plot()函数即可完成作图。
1 plt.plot(iters, losses,color='red' ,label='train loss' )
PY
详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import matplotlib.pyplot as pltdef train (model ): model.train() opt = paddle.optimizer.Adam(learning_rate=0.001 , parameters=model.parameters()) EPOCH_NUM = 10 iter =0 iters=[] losses=[] for epoch_id in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images, labels = data images = paddle.to_tensor(images) labels = paddle.to_tensor(labels) predicts, acc = model(images, labels) loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 100 == 0 : print ("epoch: {}, batch: {}, loss is: {}, acc is {}" .format (epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) iters.append(iter ) losses.append(avg_loss.numpy()) iter = iter + 100 avg_loss.backward() opt.step() opt.clear_grad() paddle.save(model.state_dict(), 'mnist.pdparams' ) return iters, losses model = MNIST() iters, losses = train(model)
PY
1 2 3 4 5 6 7 8 plt.figure() plt.title("train loss" , fontsize=24 ) plt.xlabel("iter" , fontsize=14 ) plt.ylabel("loss" , fontsize=14 ) plt.plot(iters, losses,color='red' ,label='train loss' ) plt.grid() plt.show()
PY
使用VisualDL可视化分析 VisualDL是飞桨可视化分析工具,以丰富的图表呈现训练参数变化趋势、模型结构、数据样本、高维数据分布等。帮助用户清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型调优,具体代码实现如下。
步骤1:引入VisualDL库,定义作图数据存储位置(供第3步使用),本案例的路径是“log”。
1 2 from visualdl import LogWriter log_writer = LogWriter("./log" )
PY
步骤2:在训练过程中插入作图语句。当每100个batch训练完成后,将当前损失作为一个新增的数据点(iter和acc的映射对)存储到第一步设置的文件中。使用变量iter记录下已经训练的批次数,作为作图的X轴坐标。
1 2 3 log_writer.add_scalar(tag = 'acc' , step = iter , value = avg_acc.numpy()) log_writer.add_scalar(tag = 'loss' , step = iter , value = avg_loss.numpy())iter = iter + 100
PY
7.模型加载及恢复训练 在快速入门中,我们已经介绍了将训练好的模型保存到磁盘文件的方法。应用程序可以随时加载模型,完成预测任务。但是在日常训练工作中我们会遇到一些突发情况,导致训练过程主动或被动的中断。如果训练一个模型需要花费几天的训练时间,中断后从初始状态重新训练是不可接受的。
万幸的是,飞桨支持从上一次保存状态开始训练,只要我们随时保存训练过程中的模型状态,就不用从初始状态重新训练。
下面介绍恢复训练的实现方法,依然使用手写数字识别的案例,网络定义的部分保持不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import osimport randomimport paddleimport numpy as npimport matplotlib.pyplot as pltfrom PIL import Imageimport gzipimport jsonimport warnings warnings.filterwarnings('ignore' )import paddle.nn as nnfrom paddle.nn import Conv2D, MaxPool2D, Linearimport paddle.nn.functional as Fclass MnistDataset (paddle.io.Dataset): def __init__ (self, mode ): datafile = './work/mnist.json.gz' data = json.load(gzip.open (datafile)) train_set, val_set, eval_set = data self.IMG_ROWS = 28 self.IMG_COLS = 28 if mode=='train' : imgs, labels = train_set[0 ], train_set[1 ] elif mode=='valid' : imgs, labels = val_set[0 ], val_set[1 ] elif mode=='eval' : imgs, labels = eval_set[0 ], eval_set[1 ] else : raise Exception("mode can only be one of ['train', 'valid', 'eval']" ) imgs_length = len (imgs) assert len (imgs) == len (labels), \ "length of train_imgs({}) should be the same as train_labels({})" .format (len (imgs), len (labels)) self.imgs = imgs self.labels = labels def __getitem__ (self, idx ): img = np.reshape(self.imgs[idx], [1 , self.IMG_ROWS, self.IMG_COLS]).astype('float32' ) label = np.reshape(self.labels[idx], [1 ]).astype('int64' ) return img, label def __len__ (self ): return len (self.imgs)class MNIST (paddle.nn.Layer): def __init__ (self ): super (MNIST, self).__init__() nn.initializer.set_global_initializer(nn.initializer.Uniform(), nn.initializer.Constant()) self.conv1 = Conv2D(in_channels=1 , out_channels=20 , kernel_size=5 , stride=1 , padding=2 ) self.max_pool1 = MaxPool2D(kernel_size=2 , stride=2 ) self.conv2 = Conv2D(in_channels=20 , out_channels=20 , kernel_size=5 , stride=1 , padding=2 ) self.max_pool2 = MaxPool2D(kernel_size=2 , stride=2 ) self.fc = Linear(in_features=980 , out_features=10 ) def forward (self, inputs ): x = self.conv1(inputs) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = paddle.reshape(x, [x.shape[0 ], 980 ]) x = self.fc(x) return x
PY
定义训练Trainer,包含训练过程和模型保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Trainer (object ): def __init__ (self, model_path, model, optimizer ): self.model_path = model_path self.model = model self.optimizer = optimizer def save (self ): paddle.save(self.model.state_dict(), self.model_path) def train_step (self, data ): images, labels = data predicts = self.model(images) loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) avg_loss.backward() self.optimizer.step() self.optimizer.clear_grad() return avg_loss def train_epoch (self, datasets, epoch ): self.model.train() for batch_id, data in enumerate (datasets()): loss = self.train_step(data) if batch_id % 500 == 0 : print ("epoch_id: {}, batch_id: {}, loss is: {}" .format (epoch, batch_id, loss.numpy())) def train (self, train_datasets, start_epoch, end_epoch, save_path ): if not os.path.exists(save_path): os.makedirs(save_path) for i in range (start_epoch, end_epoch): self.train_epoch(train_datasets, i) paddle.save(opt.state_dict(), './{}/mnist_epoch{}' .format (save_path,i)+'.pdopt' ) paddle.save(model.state_dict(), './{}/mnist_epoch{}' .format (save_path,i)+'.pdparams' ) self.save()
PY
恢复训练 模型恢复训练,需要重新组网,所以我们需要重启AIStudio,运行MnistDataset
数据读取和MNIST
网络定义、Trainer
部分代码,再执行模型恢复代码
在上述训练代码中,我们训练了五轮(epoch)。在每轮结束时,我们均保存了模型参数和优化器相关的参数。
使用model.state_dict()
获取模型参数。
使用opt.state_dict
获取优化器和学习率相关的参数。
调用paddle.save
将参数保存到本地。
比如第一轮训练保存的文件是mnist_epoch0.pdparams,mnist_epoch0.pdopt,分别存储了模型参数和优化器参数。
使用paddle.load
分别加载模型参数和优化器参数,如下代码所示。
1 2 paddle.load (params_path+'.pdparams' ) paddle.load (params_path+'.pdopt' )
LIVECODESERVER
如何判断模型是否准确的恢复训练呢?
理想的恢复训练是模型状态回到训练中断的时刻,恢复训练之后的梯度更新走向是和恢复训练前的梯度走向完全相同的。基于此,我们可以通过恢复训练后的损失变化,判断上述方法是否能准确的恢复训练。即从epoch 0结束时保存的模型参数和优化器状态恢复训练,校验其后训练的损失变化(epoch 1)是否和不中断时的训练相差不多。
说明:
恢复训练有如下两个要点:
保存模型时分别保存模型参数和优化器参数。
恢复参数时分别恢复模型参数和优化器参数。
下面的代码将展示恢复训练的过程,并验证恢复训练是否成功。加载模型参数并从第一个epoch开始训练,以便读者可以校验恢复训练后的损失变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import warnings warnings.filterwarnings('ignore' ) paddle.seed(1024 ) epochs = 3 BATCH_SIZE = 32 model_path = './mnist_retrain.pdparams' train_dataset = MnistDataset(mode='train' ) train_loader = paddle.io.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=False , num_workers=4 ) model = MNIST() total_steps = (int (50000 //BATCH_SIZE) + 1 ) * epochs lr = paddle.optimizer.lr.PolynomialDecay(learning_rate=0.01 , decay_steps=total_steps, end_lr=0.001 ) opt = paddle.optimizer.Momentum(learning_rate=lr, parameters=model.parameters()) params_dict = paddle.load('./checkpoint/mnist_epoch0.pdparams' ) opt_dict = paddle.load('./checkpoint/mnist_epoch0.pdopt' ) model.set_state_dict(params_dict) opt.set_state_dict(opt_dict) trainer = Trainer( model_path=model_path, model=model, optimizer=opt ) trainer.train(train_datasets=train_loader,start_epoch = 1 , end_epoch = epochs, save_path='checkpoint_con' )
PY
从恢复训练的损失变化来看,加载模型参数继续训练的损失函数值和正常训练损失函数值是相差不多的,可见使用飞桨实现恢复训练是极其简单的。 总结一下:
1 2 paddle.save(opt.state_dict(), 'model.pdopt' ) paddle.save(model.state_dict(), 'model.pdparams' )
PY
1 2 3 4 5 model_dict = paddle.load("model.pdparams" ) opt_dict = paddle.load("model.pdopt" ) model.set_state_dict(model_dict) opt.set_state_dict(opt_dict)
PY
8.动静转换 动态图有诸多优点,比如易用的接口、Python风格的编程体验、友好的调试交互机制等。在动态图模式下,代码可以按照我们编写的顺序依次执行。这种机制更符合Python程序员的使用习惯,可以很方便地将脑海中的想法快速地转化为实际代码,也更容易调试。
但在性能方面,由于Python执行开销较大,与C++有一定差距,因此在工业界的许多部署场景中(如大型推荐系统、移动端)都倾向于直接使用C++进行提速。相比动态图,静态图在部署方面更具有性能的优势。静态图程序在编译执行时,先搭建模型的神经网络结构,然后再对神经网络执行计算操作。预先搭建好的神经网络可以脱离Python依赖,在C++端被重新解析执行,而且拥有整体网络结构也能进行一些网络结构的优化。
那么,有没有可能,深度学习框架实现一个新的模式,同时具备动态图高易用性与静态图高性能的特点呢?飞桨从2.0版本开始,新增新增支持动静转换功能,编程范式的选择更加灵活。用户依然使用动态图编写代码,只需添加一行装饰器 @paddle.jit.to_static,即可实现动态图转静态图模式运行,进行模型训练或者推理部署。在本章节中,将介绍飞桨动态图转静态图的基本用法和相关原理。
动态图转静态图训练 飞桨的动转静方式是基于源代码级别转换的ProgramTranslator实现,其原理是通过分析Python代码,将动态图代码转写为静态图代码,并在底层自动使用静态图执行器运行。其基本使用方法十分简便,只需要在要转化的函数(该函数也可以是用户自定义动态图Layer的forward函数)前添加一个装饰器 @paddle.jit.to_static。这种转换方式使得用户可以灵活使用Python语法及其控制流来构建神经网络模型。下面通过一个例子说明如何使用飞桨实现动态图转静态图训练。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import paddleclass MNIST (paddle.nn.Layer): def __init__ (self ): super (MNIST, self).__init__() self.fc = paddle.nn.Linear(in_features=784 , out_features=10 ) @paddle.jit.to_static def forward (self, inputs ): outputs = self.fc(inputs) return outputs
PY
上述代码构建了仅有一层全连接层的手写字符识别网络。特别注意,在forward函数之前加了装饰器@paddle.jit.to_static
,要求模型在静态图模式下运行。下面是模型的训练代码,由于飞桨实现动转静的功能是在内部完成的,对使用者来说,动态图的训练代码和动转静模型的训练代码是完全一致的。训练代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import paddleimport paddle.nn.functional as F paddle.vision.set_image_backend('cv2' )def norm_img (img ): batch_size = img.shape[0 ] img = img/127.5 - 1 img = paddle.reshape(img, [batch_size, 784 ]) return imgdef train (model ): model.train() train_loader = paddle.io.DataLoader(paddle.vision.datasets.MNIST(mode='train' ), batch_size=16 , shuffle=True ) opt = paddle.optimizer.SGD(learning_rate=0.001 , parameters=model.parameters()) EPOCH_NUM = 10 for epoch in range (EPOCH_NUM): for batch_id, data in enumerate (train_loader()): images = norm_img(data[0 ]).astype('float32' ) labels = data[1 ].astype('int64' ) predicts = model(images) loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) if batch_id % 1000 == 0 : print ("epoch_id: {}, batch_id: {}, loss is: {}" .format (epoch, batch_id, avg_loss.numpy())) avg_loss.backward() opt.step() opt.clear_grad() model = MNIST() train(model) paddle.save(model.state_dict(), './mnist.pdparams' )print ("==>Trained model saved in ./mnist.pdparams" )
PY
我们可以观察到,动转静的训练方式与动态图训练代码是完全相同的。因此,在动转静训练的时候,开发者只需要在动态图的组网前向计算函数上添加一个装饰器即可实现动转静训练。 在模型构建和训练中,飞桨更希望借用动态图的易用性优势,实际上,在加上@to_static装饰器运行的时候,飞桨内部是在静态图模式下执行OP的,但是展示给开发者的依然是动态图的使用方式。
动转静更能体现静态图的方面在于模型部署上。下面将介绍动态图转静态图的部署方式。
动态图转静态图模型保存 在推理&部署场景中,需要同时保存推理模型的结构和参数,但是动态图是即时执行即时得到结果,并不会记录模型的结构信息。动态图在保存推理模型时,需要先将动态图模型转换为静态图写法,编译得到对应的模型结构再保存,而飞桨框架2.0版本推出paddle.jit.save和paddle.jit.load接口,无需重新实现静态图网络结构,直接实现动态图模型转成静态图模型格式。paddle.jit.save接口会自动调用飞桨框架2.0推出的动态图转静态图功能,使得用户可以做到使用动态图编程调试,自动转成静态图训练部署。
这两个接口的基本关系如下图所示:
当用户使用paddle.jit.save保存Layer对象时,飞桨会自动将用户编写的动态图Layer模型转换为静态图写法,并编译得到模型结构,同时将模型结构与参数保存。paddle.jit.save需要适配飞桨沿用已久的推理模型与参数格式,做到前向完全兼容,因此其保存格式与paddle.save有所区别,具体包括三种文件:保存模型结构的*.pdmodel文件;保存推理用参数的*.pdiparams文件和保存兼容变量信息的*.pdiparams.info文件,这几个文件后缀均为paddle.jit.save保存时默认使用的文件后缀。
比如,如果保存上述手写字符识别的inference模型用于部署,可以直接用下面代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from paddle.static import InputSpec state_dict = paddle.load("./mnist.pdparams" ) model.set_state_dict(state_dict) model.eval () paddle.jit.save( layer=model, path="inference/mnist" , input_spec=[InputSpec(shape=[None , 784 ], dtype='float32' )])print ("==>Inference model saved in inference/mnist." )
PY
其中,paddle.jit.save API 将输入的网络存储为 paddle.jit.TranslatedLayer 格式的模型,载入后可用于预测推理或者fine-tune训练。 该接口会将输入网络转写后的模型结构 Program 和所有必要的持久参数变量存储至输入路径 path 。
path 是存储目标的前缀,存储的模型结构 Program 文件的后缀为 .pdmodel ,存储的持久参数变量文件的后缀为 .pdiparams ,同时这里也会将一些变量描述信息存储至文件,文件后缀为 .pdiparams.info。
通过调用对应的paddle.jit.load接口,可以把存储的模型载入为 paddle.jit.TranslatedLayer格式,用于预测推理或者fine-tune训练。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import numpy as npimport paddleimport paddle.nn.functional as F paddle.vision.set_image_backend('cv2' ) mnist_test = paddle.vision.datasets.MNIST(mode='test' ) test_image, label = mnist_test[0 ]print ("The label of readed image is : " , label) test_image = paddle.reshape(paddle.to_tensor(test_image), [1 , 784 ]) test_image = norm_img(test_image) loaded_model = paddle.jit.load("./inference/mnist" ) preds = loaded_model(test_image) pred_label = paddle.argmax(preds)print ("The predicted label is : " , pred_label.numpy()) The label of readed image is : [7 ] The predicted label is : [7 ]
PY
paddle.jit.save API 可以把输入的网络结构和参数固化到一个文件中,所以通过加载保存的模型,可以不用重新构建网络结构而直接用于预测,易于模型部署。