深度学习概述
机器学习是实现人工智能的一种途径,深度学习是机器学习的一个子集,深度学习是实现机器学习的一种方法。与机器学习算法的主要区别如下图所示:
传统机器学习算术依赖人工设计特征,并进行特征提取,而深度学习方法不需要人工,而是依赖算法自动提取特征。深度学习模仿人类大脑的运行方式,从经验中学习获取知识。这也是深度学习被看做黑盒子,可解释性差的原因。
随着计算机软硬件的飞速发展,现阶段通过深度学习来模拟人脑来解释数据,包括图像,文本,音频等内容。目前深度学习的主要应用领域有:
- 语音识别
- 机器翻译
- 自动驾驶
当然在其他领域也能见到深度学习的身影,比如风控,安防,智能零售,医疗领域,推荐系统等。
神经网络概述
神经网络就是模拟人神经元的工作机理,并构造仿生的神经元来解决实际问题。一个简单的神经网络,包括输入层、隐藏层、输出层,其中隐藏层可以有很多层,每一层也可以包含数量众多的的神经元。以下流程就像,来源不同树突(树突都会有不同的权重)的信息进行的加权计算,输入到细胞中做加和,再通过激活函数输出细胞值。
神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:
- 输入层: 即输入 x 的那一层
- 输出层: 即输出 y 的那一层
- 隐藏层: 输入层和输出层之间都是隐藏层
同一层的神经元之间没有连接。 第 N 层的每个神经元和第 N-1层 的所有神经元相连(这就是full connected的含义), 第N-1层神经元的输出就是第N层神经元的输入。每个连接都有一个权值。
激活函数
激活函数用于对每层的输出数据进行变换,进而为整个网络结构结构注入了非线性因素。此时神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:
- 没有引入非线性因素的网络等价于使用一个线性模型来拟合
- 通过给网络输出增加激活函数,实现引入非线性因素,使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力。
- 激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。网络参数在更新时,使用的反向传播算法(BP),要求激活函数必须可微。
sigmoid 激活函数
- 从 sigmoid 函数图像可以得到,sigmoid 函数可以将任意的输入映射到 (0, 1) 之间,当输入的值大致在 <-6 或者 >6 时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入 100 和输出 10000 经过 sigmoid 的激活值几乎都是等于 1 的,但是输入的数据之间相差 100 倍的信息就丢失了。
- 对于 sigmoid 函数而言,输入值在 [-6, 6] 之间输出值才会有明显差异,输入值在 [-3, 3] 之间才会有比较好的效果。
- 通过上述导数图像,发现导数数值范围是 (0, 0.25),当输入 <-6 或者 >6 时,sigmoid 激活函数图像的导数接近为 0,此时网络参数将更新极其缓慢,或者无法更新。
- 一般来说, sigmoid 网络在 5 层之内就会产生梯度消失现象。而且,该激活函数并不是以 0 为中心的,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层。
tanh 激活函数
- 由上面的函数图像可以看到,Tanh 函数将输入映射到 (-1, 1) 之间,图像以 0 为中心,在 0 点对称,当输入 大概<-3 或者 >3 时将被映射为 -1 或者 1。其导数值范围 (0, 1),当输入的值大概 <-3 或者 > 3 时,其导数近似 0。
- 与 Sigmoid 相比,它是以 0 为中心的,使得其收敛速度要比 Sigmoid 快,减少迭代次数。然而,从图中可以看出,Tanh 两侧的导数也为 0,同样会造成梯度消失。
- 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数。
ReLU 激活函数
- 从上述函数图像可知,ReLU 激活函数将小于 0 的值映射为 0,而大于 0 的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
- 但是,如果网络的参数采用随机初始化时,很多参数可能为负数,这就使得输入的正值会被舍去,而输入的负值则会保留,这可能在大部分的情况下并不是想要的结果。
ReLU 的导数图像如下:
- ReLU是目前最常用的激活函数。 从图中可以看到,当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”。
- 与sigmoid相比,RELU的优势是:
- 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,求导涉及除法,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。 sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。 Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。
SoftMax
softmax用于多分类过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。计算方法如下图所示:
Softmax 直白来说就是将网络输出的 logits 通过 softmax 函数,就映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为预测目标类别。
其他激活函数
总结
对于隐藏层:
- 优先选择RELU激活函数
- 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
- 如果使用了Relu, 需要注意一下Dead Relu问题, 避免出现大的梯度从而导致过多的神经元死亡。
- 不要使用sigmoid激活函数,可以尝试使用tanh激活函数
对于输出层
- 二分类问题选择sigmoid激活函数
- 多分类问题选择softmax激活函数
- 回归问题选择identity激活函数
传播算法
前向传播
前向传播指的是数据输入的神经网络中,逐层向前传输,一直到运算到输出层为止。
在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)。利用反向传播算法对神经网络进行训练,该方法与梯度下降算法相结合,对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。
链式法则
反向传播算法是利用链式法则进行梯度求解及权重更新的。对于复杂的复合函数,将其拆分为一系列的加减乘除或指数,对数,三角函数等初等函数,通过链式法则完成复合函数的求导。这里以一个神经网络中常见的复合函数的例子来说明这个过程,复合函数 𝑓(𝑥) 为:
其参数为权重 w、b。需要求关于 w 和 b 的偏导,然后应用梯度下降公式就可以更新参数。将复合函数分解为一系列的初等函数导数相乘的形式:
整个复合函数 𝑓(𝑥; 𝑤, 𝑏) 关于参数 𝑤 和 𝑏 的导数可以通过 𝑓(𝑥; 𝑤, 𝑏) 与参数 𝑤 和 𝑏 之间路径上所有的导数连乘来得到,即:
以w为例,当 𝑥 = 1, 𝑤 = 0, 𝑏 = 0 时,可以得到:
反向传播算法
BP(Back Propagation)算法也叫做误差反向传播算法,它用于求解模型的参数梯度,从而使用梯度下降法来更新网络参数。它的基本工作流程如下:
- 通过正向传播得到误差,所谓正向传播指的是数据从输入到输出层,经过层层计算得到预测值,并利用损失函数得到预测值和真实值之前的误差。
- 通过反向传播把误差传递给模型的参数,从而对网络参数进行适当的调整,缩小预测值和真实值之间的误差。
- 反向传播算法是利用链式法则进行梯度求解,然后进行参数更新。对于复杂的复合函数,将其拆分为一系列的加减乘除或指数,对数,三角函数等初等函数,通过链式法则完成复合函数的求导。
通过一个例子来简单理解下 BP 算法进行网络参数更新的过程:
为了能够把计算过程描述的更详细一些,上图中一个矩形代表一个神经元,每个神经元中分别是值和激活值的计算结果和其对应的公式,最终计算出真实值和预测值之间的误差 0.2984. 其中:
- 由下向上看,最下层绿色的两个圆代表两个输入值
- 右侧的8个数字,最下面4个表示 w1、w2、w3、w4 的参数初始值,最上面的4个数字表示 w5、w6、w7、w8 的参数初始值
- b1 值为 0.35,b2 值为 0.60
- 预测结果分别为: 0.7514、0.7729
参数初始化
在构建网络之后,网络中的参数是需要初始化的。需要初始化的参数主要有权重和偏置,偏置一般初始化为 0 即可,而对权重的初始化则会更加重要。
- 均匀分布初始化,权重参数初始化从区间均匀随机取值。即在(-1/√d,1/√d)均匀分布中生成当前神经元的权重,其中d为每个神经元的输入数量。
- 正态分布初始化,随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化.
- 全0初始化,将神经网络中的所有权重参数初始化为 0.
- 全1初始化,将神经网络中的所有权重参数初始化为 1.
- 固定值初始化,将神经网络中的所有权重参数初始化为某个固定值.
- kaiming 初始化,也叫做 HE 初始化。HE 初始化分为正态分布的 HE 初始化、均匀分布的 HE 初始化.
- xavier 初始化,也叫做Glorot初始化,该方法的基本思想是各层的激活值和梯度的方差在传播过程中保持一致。它有两种,一种是正态分布的 xavier 初始化、一种是均匀分布的 xavier 初始化.
优化方法
传统的梯度下降优化算法中,可能会碰到以下情况:碰到平缓区域,梯度值较小,参数优化变慢碰到 “鞍点” ,梯度为0,参数无法优化碰到局部最小值,对于这些问题,出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam等。
指数加权平均
β 的值越大,则绘制出的折线越加平缓;β 值一般默认都是 0.9。
Momentum
AdaGrad
RMSProp
Adam
Momentum 使用指数加权平均计算当前的梯度值、AdaGrad、RMSProp 使用自适应的学习率,Adam 结合了 Momentum、RMSProp 的优点,使用:移动加权平均的梯度和移动加权平均的学习率。使得能够自适应学习率的同时,也能够使用 Momentum 的优点。
总结
对普通梯度下降算法的优化方法,主要有 Momentum、AdaGrad、RMSProp、Adam 等优化方法,其中 Momentum 使用指数加权平均参考了历史梯度,使得梯度值的变化更加平缓。AdaGrad 则是针对学习率进行了自适应优化,由于其实现可能会导致学习率下降过快,RMSProp 对 AdaGrad 的学习率自适应计算方法进行了优化,Adam 则是综合了 Momentum 和 RMSProp 的优点,在很多场景下,Adam 的表示都很不错。
正则化
在训深层练神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout 就是在神经网络中一种缓解过拟合的方法。
缓解过拟合的方式就是降低模型的复杂度,而 Dropout 就是通过减少神经元之间的连接,把稠密的神经网络神经元连接,变成稀疏的神经元连接,从而达到降低网络复杂度的目的。
dropout 层的使用,其作用用于控制网络复杂度,达到正则化的目的,类似于 L2 正则化对线性回归的作用。
批量归一化
在神经网络的搭建过程中,Batch Normalization(批量归一化)是经常使用一个网络层,其主要的作用是控制数据的分布,加快网络的收敛。
神经网络的学习其实在学习数据的分布,随着网络的深度增加、网络复杂度增加,一般流经网络的数据都是一个 mini batch,每个 mini batch 之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。
如果对每一个 mini batch 的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。
批量归一化层,该层的作用主要是用来控制每层数据的流动时的均值和方差,防止训练过程出现剧烈的波动,模型难以收敛,或者收敛较慢。批量归一化层在计算机视觉领域使用较多。
案例-价格分类
import torch
import torch.nn as nn
import torch.nn.functional as F
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.optim as optim
import numpy as np
import time
from sklearn.preprocessing import StandardScaler# 构建数据集
def create_dataset():data = pd.read_csv('data/手机价格预测.csv')# 特征值和目标值x, y = data.iloc[:, :-1], data.iloc[:, -1]x = x.astype(np.float32)y = y.astype(np.int64)# 数据集划分x_train, x_valid, y_train, y_valid = \train_test_split(x, y, train_size=0.8, random_state=88, stratify=y)# 数据标准化transfer = StandardScaler()x_train = transfer.fit_transform(x_train)x_valid = transfer.transform(x_valid)# 构建数据集train_dataset = TensorDataset(torch.from_numpy(x_train), torch.tensor(y_train.values))valid_dataset = TensorDataset(torch.from_numpy(x_valid), torch.tensor(y_valid.values))return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))train_dataset, valid_dataset, input_dim, class_num = create_dataset()# 构建网络模型
class PhonePriceModel(nn.Module):def __init__(self, input_dim, output_dim):super(PhonePriceModel, self).__init__()self.linear1 = nn.Linear(input_dim, 128)self.linear2 = nn.Linear(128, 256)self.linear3 = nn.Linear(256, 512)self.linear4 = nn.Linear(512, 128)self.linear5 = nn.Linear(128, output_dim)def _activation(self, x):return torch.sigmoid(x)def forward(self, x):x = self._activation(self.linear1(x))x = self._activation(self.linear2(x))x = self._activation(self.linear3(x))x = self._activation(self.linear4(x))output = self.linear5(x)return output# 编写训练函数
def train():# 固定随机数种子torch.manual_seed(0)# 初始化模型model = PhonePriceModel(input_dim, class_num)# 损失函数criterion = nn.CrossEntropyLoss()# 优化方法optimizer = optim.Adam(model.parameters(), lr=1e-4)# 训练轮数num_epoch = 50for epoch_idx in range(num_epoch):# 初始化数据加载器dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)# 训练时间start = time.time()# 计算损失total_loss = 0.0total_num = 1# 准确率correct = 0for x, y in dataloader:output = model(x)# 计算损失loss = criterion(output, y)# 梯度清零optimizer.zero_grad()# 反向传播loss.backward()# 参数更新optimizer.step()total_num += len(y)total_loss += loss.item() * len(y)print('epoch: %4s loss: %.2f, time: %.2fs' %(epoch_idx + 1, total_loss / total_num, time.time() - start))# 模型保存torch.save(model.state_dict(), 'model/phone-price-model.bin')def test():# 加载模型model = PhonePriceModel(input_dim, class_num)model.load_state_dict(torch.load('model/phone-price-model.bin'))# 构建加载器dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)# 评估测试集correct = 0for x, y in dataloader:output = model(x)y_pred = torch.argmax(output, dim=1)correct += (y_pred == y).sum()print('Acc: %.5f' % (correct.item() / len(valid_dataset)))if __name__ == '__main__':train()test()
网络模型调优:
- 对输入数据进行标准化
- 调整优化方法
- 调整学习率
- 增加批量归一化层
- 增加网络层数、神经元个数
- 增加训练轮数
- 等等...