移动平台深度神经网络实战:原理、架构与优化
上QQ阅读APP看书,第一时间看更新

1.5 深度学习

如果说用很短的时间把机器学习的所有方法全部讲完,那也是完全不可能的事情。所以本书重点关注的是近几年得到广泛运用的深度学习。可以预见,深度学习不管是现在,还是在之后的一段时间内都会是最流行、最有效的机器学习方法之一。

1.5.1 深度学习的贡献

深度学习是一种思想、一种学习模式,深度神经网络是一类模型,两者在本质上是不一样的。但目前大家普遍将深度神经网络认为就是深度学习。

深度神经网络应用之前,传统的计算机视觉、语音识别方法是把特征提取和分类器设计分开来做,然后在应用时再合在一起。比如,如果输入的是一个摩托车图像的话,首先要有一个特征表达或者特征提取的过程,然后把表达出来的特征放到学习算法中进行分类学习。

因为手工设计特征需要大量的实践经验,需要对该领域和数据具有深入见解,并且在特征设计出来之后还需要大量的调试工作和一点运气。另一个难点在于,你不只需要手工设计特征,还要在此基础上有一个比较合适的分类器算法。如果想使特征设计与分类器设计两者合并且达到最优的效果,几乎是不可能完成的任务。

2012年后,深度神经网络给计算机视觉、语音识别、自然语言处理等领域带来了突破性的进展,特别是在人脸识别、机器翻译等领域应用的准确率接近甚至超过了人类的水平。深度神经网络如图1-1所示。

图1-1 深度神经网络示意图

深度神经网络最重要的是表示学习的能力,你把数据从一端扔进去,模型从另外一端就出来了,中间所有的特征完全可以通过学习自己来解决,而不再需要手工去设计特征了。

1.5.2 深度学习框架简介

作为本书的开篇章节,有必要对现今和可预见的未来流行的深度学习框架进行介绍,在后续章节当中我们将直接使用其中一部分框架进行讲解和实战。如果你对这些框架有所了解,甚至实践过,那就再好不过了。如果你不了解这些框架或没有使用过,也不必过于担心,在后续实战章节中我们会从头开始编写代码,build from scratch。

1.TensorFlow

TensorFlow读者可以通过访问https://www.tensorflow.org/learn来对TensorFlow进行更详细的了解和认识。本书会在后续内容中逐渐引入使用TensorFlow的方法。如果你还不了解TensorFlow,笔者建议先行阅读有关入门内容。(见图1-2)是一款基于Apache License 2.0协议开放源代码的软件库,用于进行高性能数值计算。借助其灵活的架构,用户可以轻松地将计算工作部署到多种平台(CPU、GPU、TPU)和设备(桌面设备、服务器集群、移动设备、边缘设备等)。TensorFlow最初是由Google Brain团队中的研究人员和工程师开发的,可为机器学习和深度学习提供强力支持,并且其灵活的数值计算核心广泛应用于许多其他科学领域。

图1-2 TensorFlow

TensorFlow属于第2代人工智能系统,也是一个通用的机器学习框架,具有良好的灵活性和可移植性等优点。TensorFlow有非常好的伸缩性,同时支持模型并行与数据并行,可以在大规模集群上进行分布式训练。

与以Caffe为代表的第1代深度学习引擎不同,TensorFlow提供了自动微分功能,当添加新的层的时候我们无须自己计算并手写微分代码,极大地方便了网络的扩展。

此外,TensorFlow提供了非常多的语言接口,从C/C++、Python、Java甚至到现在的JavaScript,支持的语言非常广泛,因此也非常受欢迎。

接下来我们详细介绍一下TensorFlow的计算模型。

TensorFlow将完整的计算任务都抽象成一张图(graph),每个小的计算步骤是一个操作(operation),因此所有的计算任务就是一张由一个个小操作组成的图。

这样讲可能比较抽象,我们使用一个实际的TensorFlow Graph来说明这些概念,如图1-3所示。

图1-3 TensorFlow计算图示例

图1-3代表了一系列的计算过程。我们先用constant操作定义一个常量,然后分成两条路,一条路先用add操作计算constant加1的结果,然后计算从外部读取一个数据ds1,和add的结果进行乘法,最后用avg操作求add和mul操作的平均值。另一条路则是先使用mul操作将constant乘以2,然后从外部读取数据ds2,并和mul的结果做加法,然后将结果赋值给一个临时变量int_result。最后使用add操作将avg的结果和int_result相加,得到最后的结果。

可以看到这个图1-3中有很多元素,比如我们将constant、add、mul称之为操作(operation)。操作是该图中的主要节点。除了操作以外还会有一些数据输入,比如ds1和ds2。我们还可以通过定义变量(variable)保存中间状态,比如int_result。

图中每一个节点负责处理一个张量(tensor)。张量是一个多维数组,表示数学里的多维向量。如果我们要处理一些平面上的散点,那么就可以将需要处理的数据看成一个二维向量(表示点的xy),我们可以将整个数据处理过程看成Tensor在不同操作节点之间的流动(Flow),这也就是为什么该框架的名字叫作TensorFlow了。

使用TensorFlow的第一步就是将计算任务构造成一张图。但不能只描述计算过程,我们需要编写可执行的任务,因此需要创建一个会话(session)。会话的作用是建立一个执行上下文(context),所有的图都需要在会话中执行,会话会初始化并保存图中需要的变量、图的执行状态、管理执行图的设备(CPU和GPU)等。

所以我们可以看到,TensorFlow的结构很简单,只需要构建一张表示计算的图,并创建会话来执行图即可,TensorFlow帮我们隐藏了其他所有细节,因此我们可以不去关心计算的那些细枝末节。

2.TensorFlow Lite

TensorFlow是目前最完善和强大的深度学习框架,在工业界服务端深度学习领域已经是无可争辩的事实标准,但在移动平台和嵌入式领域中,TensorFlow就显得过于庞大而臃肿,而且计算速度并不能满足移动平台的要求。为了解决这个问题,Google开发了TensorFlow Lite(见图1-4),实现了TensorFlow到移动平台生态体系的延续。

图1-4 TensorFlow Lite

TensorFlow Lite是一种用于设备端推断的开源深度学习框架,其目前是作为TensorFlow的一个模块发布,但我们需要知道TensorFlow Lite和TensorFlow几乎是两个独立的项目,两者之间基本没有共享代码。因此可以说TensorFlow Lite是一个完整而且独立的前向计算引擎框架。

使用TensorFlow Lite需要单独训练一些适用于移动平台的轻量级模型,减少参数数量,提升计算速度。与此同时,TensorFlow Lite还提供了模型转换工具,用于将TensorFlow的模型直接转换为TensorFlow Lite的模型,而且可以实现模型的压缩存储,还能实现模型参数的量化。这样就可以实现在服务器的TensorFlow上训练,在移动平台应用的场景。此外,TesnorFlow Lite需要我们将其转换后的tflite文件打包到App或者目标存储设备中。TensorFlow Lite启动时会将其加载到移动设备或嵌入式设备中。最后,TesnorFlow Lite对移动平台的前向计算进行了优化,可以加速浮点数运算,进行半精度浮点数运算,以及8位整数的量化计算,甚至可以通过代理方式在GPU上或者Android的NNAPI上调用。

由于TensorFlow Lite是后续内容当中优化代码讲解的重点之一,因此我们会对其进行详细阐述,围绕其进行讨论、研究以及针对性的优化与面向特定移动领域应用场景的可行性裁剪。

3.MXNet

Apache MXNet(见图1-5)是一个深度学习框架,主要目标是确保深度学习框架的灵活性与执行效率。它允许你混合符号和命令式编程,以最大限度地提高效率和生产力。MXNet的核心是一个动态依赖调度程序,可以动态地自动并行化符号和命令操作。最重要的图形优化层使符号执行更快,内存效率更高。MXNet便携且轻巧,可有效扩展到多个GPU和多台机器。

图1-5 MXNet

MXNet支持命令式和符号式两种编程模式,简单、易于上手,同时支持在多端运行,包括多CPU、多GPU、集群、服务器、工作站,甚至移动智能手机。和其他框架一样,MXNet也支持多语言接口,包括C++、Python、R、Scala、Julia、Matlab和JavaScript。最后MXNet可以非常方便地部署到云端,包括Amazon S3、HDFS和Azure。不过这里值得一提的是,MXNet很好地支持了AWS SageMaker,能够借助一系列工具有针对性地(计算平台、体系结构、网络等)进行模型优化,并非常直接地在Core ML移动平台引擎上使用。

由于本书并不直接使用MXNet,此处就不过多阐述了。

4.PyTorch

PyTorch(见图1-6)是这里最年轻的深度学习框架,也是最近发展最为迅猛的研究用深度学习框架,因为其上手简单、灵活强大,如今Caffe2也正式并入PyTorch。使用PyTorch可以非常快速地验证研究思路而为广大研究人员喜爱。

图1-6 PyTorch

PyTorch是一个以C/C++为核心实现,以Python为胶水语言,编写调用接口的框架。与TensorFlow一样,PyTorch利用Autograd模块自动计算导数,避免了复杂的手动求导。因此PyTorch非常适合深度学习。

作为深度学习领域的生力军,由于其易于使用以及高性能等特点,接下来的内容将介绍如何安装和使用PyTorch,并将其作为主要实验平台,我们在后续的产品研发实战环节都是在编写好相关代码并实验的基础之上,再针对特定平台编写高性能代码。

1.5.3 安装使用深度学习框架

本书实验部分将使用Python和PyTorch为主要实验平台。因此这里就介绍如何安装使用PyTorch解决最简单的机器学习问题。

1.安装

首先,我们需要安装最新版本的Anaconda(Python3版本)。Anaconda(见图1-7)是非常著名的Python发行版本,内部集成了相当多的Python工具包,免去了我们很多下载安装配置Python工具包的过程,有自己的软件源,同时是PyTorch官方推荐的开发包,所以推荐安装使用Anaconda。

图1-7 Anaconda

安装Anaconda非常简单,只要进入Anaconda官网下载页面目前读者可以访问https://www.anaconda.com/download来下载相关软件包。下载安装包,点击安装即可。

安装好Anaconda后,启动Anaconda控制台。此处需要注意,Anaconda在不同平台(macOS、Linux或Windows)会使用不同的依赖源提供依赖下载。而Mac上目前没有CPU包,只有GPU包。因此安装依赖的时候和Windows、*NIX有所区别。

其次,我们进入Anaconda的命令提示符环境。在这里你可以使用Anaconda预装的Python与Anaconda提供的所有工具链。

使用Anaconda安装PyTorch非常简单,只需要使用conda命令安装即可,如图1-8所示。

图1-8 PyTorch安装图

最后,安装完成后,测试下PyTorch是否安装完成,如图1-9所示。

图1-9 PyTorch测试图

我们知道怎么使用PyTorch后就可以着手解决一个比较简单的机器学习问题了。

2.第一次实战:简单训练

这个问题就是经典的手写字母识别问题,也就是我们要识别出如图1-10所示的是哪个数字?

图1-10 手写数字5

我们一眼就可以看出这是5,那么怎么让计算机识别0~9这10个手写数字呢?PyTorch可以帮助我们解决问题。

(1)进行训练

我们需要一堆手写数字的图片,这个就是训练集,每个样本是一个数字图像,每个图像都有一个标签,表示这个图像到底是几。然后我们要将这些图像与相应标签输入PyTorch中进行训练,让计算机自动学习这些图像和标签之间的关系。首先,需要定义一下网络结构,如代码清单1-1所示。

代码清单1-1 PyTorch网络定义

 1 import torch
 2 import torch.nn as nn
 3 import torch.nn.functional as F
 4 import torch.optim as optim
 5 from torchvision import datasets, transforms
 6 
 7 import argparse
 8 
 9 
10 class Net(nn.Module):
11 
12     def __init__(self):
13         super(Net, self).__init__()
14         self.conv1 = nn.Conv2d(1, 20, 5, 1)
15         self.conv2 = nn.Conv2d(20, 50, 5, 1)
16         self.fc1 = nn.Linear(4*4*50, 500)
17         self.fc2 = nn.Linear(500, 10)
18 
19     def forward(self, x):
20         x = F.relu(self.conv1(x))
21         x = F.max_pool2d(x, 2, 2)
22         x = F.relu(self.conv2(x))
23         x = F.max_pool2d(x, 2, 2)
24         x = x.view(-1, 4*4*50)
25         x = F.relu(self.fc1(x))
26         x = self.fc2(x)
27 
28         return F.log_softmax(x, dim=1)

第1~7行,导入了我们需要用到的库,包括PyTorch的基本组件、torchvision的数据集与数据变换模块以及命令行参数解析包。

第10行,定义Net类,该类继承自nn.Module类,nn.Module类是PyTorch中所有模块的基类。在PyTorch中,所有的运算符、网络结构都是一个Module,每一个Module都可以包含一系列小的Module。

第12~17行,定义了Net类的初始化方法,初始化方法中首先调用了基类的初始化方法,然后定义了4个层,分别是conv1、conv2、fc1和fc2,前两个是卷积层,后两个是全连接层。

第19行开始,定义了网络的正向传播实现,其中参数x是数据输入。

第20行,调用self.conv1将输入接入第1个卷积层,然后调用relu激活输出。

第21行,调用max_pool2d函数将relu层的输出接入第1个池化层。

第22行,调用self.conv2将输入接入第2个卷积层,然后调用relu激活输出。

第23行,调用max_pool2d函数将relu层的输出接入第2个池化层。

第24行,调用tensor的view函数对数据进行变换。

第25行,调用self.fc1将输入接入第1个全连接层,然后调用relu激活输出。

第26行,调用self.fc2将relu层输出接入第2个全连接层,最后直接输出。

第28行,最后调用softmax对fc2的输出进行多分类。

(2)编写训练代码

编写训练代码,如代码清单1-2所示。

代码清单1-2 训练实现

31 def train(args, model, device, train_loader, optimizer, epoch):
32     model.train()
33     for batch_idx, (data, target) in enumerate(train_loader):
34         data, target = data.to(device), target.to(device)
35         optimizer.zero_grad()
36         output = model(data)
37         loss = F.nll_loss(output, target)
38         loss.backward()
39         optimizer.step()
40         if batch_idx % args.log_interval == 0:
41             print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
42                 epoch, batch_idx * len(data), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item()))

第31行,定义了train函数,该函数有几个参数,分别为args、model、device、train_loader、optimizer和epoch。args是训练的命令行参数,model是网络模型,device是训练用的设备对象、train_loader是训练数据的装载类,optimizer是参数优化对象,epoch是迭代的epoch次数。

第32行,表示开始训练。

第33行,通过for in遍历训练数据中的每一批数据,循环变量batch_idx表示批次编号,data是输入数据,也就是样本特征,target是输出目标,也就是样本标签。

第34行,将数据传输到训练设备中,如果使用GPU训练,这里会将数据传输到GPU中。

第35行,将参数优化器设置为零梯度。

第36行,根据模型当前参数获取输入数据data的输出output。

第37行,根据模型的当前预测输出output和输出目标target计算预测结果和实际结果之间的loss。

第38行,调用backward并根据当前的loss通过反向传播技术实现残差传播。

第39行,调用优化器的step进行当前这轮训练的参数优化。

第40~42行,用于输出这一轮训练的误差。便于我们观察训练和参数优化过程。

(3)启动训练

调用训练函数只需要使用以下代码,如代码清单1-3所示。

代码清单1-3 启动训练

 63 def main():
 64     # Training settings
 65     parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
 66     parser.add_argument('--batch-size', type=int, default=64, metavar='N',
 67                         help='input batch size for training (default: 64)')
 68     parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
 69                         help='input batch size for testing (default: 1000)')
 70     parser.add_argument('--epochs', type=int, default=10, metavar='N',
 71                         help='number of epochs to train (default: 10)')
 72     parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
 73                         help='learning rate (default: 0.01)')
 74     parser.add_argument('--momentum', type=float, default=0.5, metavar='M',
 75                         help='SGD momentum (default: 0.5)')
 76     parser.add_argument('--no-cuda', action='store_true', default=False,
 77                         help='disables CUDA training')
 78     parser.add_argument('--seed', type=int, default=1, metavar='S',
 79                         help='random seed (default: 1)')
 80     parser.add_argument('--log-interval', type=int, default=10, metavar='N',
 81                         help='how many batches to wait before logging training status')
 82 
 83     parser.add_argument('--save-model', action='store_true', default=False,
 84                         help='For Saving the current Model')
 85     args = parser.parse_args()
 86     use_cuda = not args.no_cuda and torch.cuda.is_available()
 87 
 88     torch.manual_seed(args.seed)
 89 
 90     device = torch.device("cuda" if use_cuda else "cpu")
 91 
 92     kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
 93     train_loader = torch.utils.data.DataLoader(
 94         datasets.MNIST('../data', train=True, download=True,
 95                        transform=transforms.Compose([
 96                            transforms.ToTensor(),
 97                            transforms.Normalize((0.1307,), (0.3081,))
 98                        ])),
 99     batch_size=args.batch_size, shuffle=True, **kwargs)
100     model = Net().to(device)
101     optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
102 
103     for epoch in range(1, args.epochs + 1):
104         train(args, model, device, train_loader, optimizer, epoch)

这样我们就可以训练模型了。

第65~84行,定义了这个脚本的参数格式,这里就不对参数逐个说明了。

第85行,调用parser.parse_args根据前面定义的参数格式解析输入的命令行参数。

第86行,根据用户是否指定使用CUDA(通过no_cuda参数)和机器是否支持CUDA决定是否使用CUDA,如果用户设置了no_cuda或者机器不支持CUDA,那么将会使用CPU模式。

第88行,用于设置PyTorch的初始种子。

第90~92行,根据是否使用CUDA,初始化不同的环境变量。

第93~99行,调用PyTorch的DataLoader从TorchVision的datasets模块中初始化MNIST数据集,我们指定数据在../data目录下,如果数据不存在,PyTorch会帮助我们下载数据集。当下载完成后,我们使用transforms模块中的ToTensor将数据集转换为Tensor,然后调用Normalize对数据进行标准化。

第100行,调用Net类的to方法在指定设备上创建模型对象。Net类就是我们定义的网络模块。我们可以调用任意Module的to方法生成一个模型。

第101行,调用optim的SGD(Stochastic Gradient Descent,随机梯度下降法)类创建一个采用SGD算法的参数优化器。SGD是参数优化器的一种算法实现。

第103~104行,根据用户指定的epoch反复调用train函数进行训练,如果用户设置的epoch是1000,那么这里将会进行1000次训练。

3.预测

预测代码也就是测试代码,如代码清单1-4所示。

代码清单1-4 预测实现

 45 def test(args, model, device, test_loader):
 46     model.eval()
 47     test_loss = 0
 48     correct = 0
 49     with torch.no_grad():
 50         for data, target in test_loader:
 51             data, target = data.to(device), target.to(device)
 52             output = model(data)
 53             test_loss += F.nll_loss(output, target, reduction='sum').item()  # sum up batch loss
 54             pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
 55             correct += pred.eq(target.view_as(pred)).sum().item()
 56 
 57     test_loss /= len(test_loader.dataset)
 58 
 59     print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
 60         test_loss, correct, len(test_loader.dataset), 100. * correct / len (test_loader.dataset)))

第45行,定义了test函数,该函数有4个参数,args是命令行输入参数,model是网络模型,device是设备对象,test_loader是测试数据的数据加载器。

第46行,调用model的eval方法开始测试。

第47~48行,将测试的误差和准确率都初始化为0。

第50行,开始遍历测试数据集,训练变量data表示测试输入数据,也就是样本特征,target是测试的输出数据,也就是样本标签。

第51行,将数据传输到指定设备上,如果这里指定了CUDA设备,那么这里会将数据传输到GPU中。

第52行,调用模型对象对输入数据(样本特征)进行预测,输出返回到output中。

第53行,调用nll_loss方法计算预测的输出和样本标签之前的误差,并将loss累加到test_loss中。

第54~55行,计算所有样本预测的正确率。

第57行,将预测误差的总和除以样本数量,计算出预测的平均误差。

第59~60行,将预测的误差与准确率输出出来,便于我们观察,对模型进一步调整。

修改上一节的训练代码,我们就可以使用训练的模型来做预测了,如代码清单1-5所示。

代码清单1-5 启动预测

106     test_loader = torch.utils.data.DataLoader(
107             datasets.MNIST('../data', train=False, transform=transforms.Compose([
108                                transforms.ToTensor(),
109                                transforms.Normalize((0.1307,), (0.3081,))
110                            ])),
111             batch_size=args.test_batch_size, shuffle=True, **kwargs)
112 
113     model = Net().to(device)
114     optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
115 
116     for epoch in range(1, args.epochs + 1):
117         train(args, model, device, train_loader, optimizer, epoch)
118         test(args, model, device, test_loader)
119 
120     if (args.save_model):
121         torch.save(model.state_dict(),"mnist_cnn.pt")

第106行,调用PyTorch的DataLoader从TorchVision的datasets模块中初始化MNIST数据集,我们指定数据在../data目录下,如果数据不存在,PyTorch会帮助我们下载数据集。当下载完成后使用transforms模块中的ToTensor将数据集转换为Tensor,然后调用Normalize对数据进行标准化。

第113行,调用Net类的to方法在指定设备上创建模型对象。

第114行,调用optim的SGD类创建一个采用SGD算法的参数优化器。

第116~118行,根据用户指定的epoch反复调用train函数进行训练和测试,如果用户设置的epoch是1000,那么这里将会进行1000次训练测试,每次训练完之后都会调用test进行一次测试,观察每一轮训练的训练效果。

第120~121行,如果用户指定了希望保存模型,就调用save将模型保存到mnist_cnn.pt中。

至此,我们的所有工作就完成了。

1.5.4 深度学习进展

即便深度学习的理论早在20世纪80年代就已经奠定了基础,但这并不意味着这个领域已盖棺定论。对抗生成网络和BERT的出现又为这一领域增添了发展活力,作为补充,我们有必要在本书的开头向大家进行基本介绍,但艰深的基础理论和算法不是本书的研究主旨,因此我们在本书中将以实战为主,理论为辅向大家展现整个深度学习以及移动平台优化实战的方方面面。

1.对抗生成网络

对抗生成网络(generative adversarial network)是现阶段非常流行的一个研究方向,一般简称GAN。这是解决很多问题的关键思路,本节将介绍GAN的相关内容。

首先,我们需要明确生成的定义(generation)。生成就是模型通过学习一些数据,然后生成类似的数据。比如让机器看一些动物图片,然后自己来产生动物的图片。

在GAN之前,其实已经有一些数据生成技术,比如Auto-Encoder。我们训练一个编码器(encoder),对输入进行编码。然后训练一个解码器(decoder),将编码转换成输出。训练完后,去除整个网络的解码部分,就能通过输入随机编码生成图片。但是Auto-Encoder生成的图片质量并不好,很容易分别出真假,所以训练中很难有实际意义。后面也有人提出了VAE模型,用来解决这些问题。

但是无论是Auto-Encoder还是改进的VAE,都有一个问题——它生成的输出是希望和输入越相似越好。但是模型衡量相似的方法是计算loss,采用的大多是MSE,即每一个像素上的均方差。loss小就表示相似。但是我们并不是想生成相似的图片,而是想生成更丰富的图片,因此用来衡量生成图片好坏的标准并不能很好地完成想要实现的目的。于是就有了下面要讲的GAN。

其次,GAN是如何生成图片的呢?首先GAN由两个网络组成,一个是生成网络Generator,一个是判别网络Discriminator,从2人零和博弈中受启发,通过两个网络互相对抗来达到最好的生成效果,流程示意图如图1-11所示。

图1-11 对抗生成网络流程示意图

主要流程类似上面这个图。首先,有一个一代生成网络,它能生成一些很差的图片,然后有一个一代判别网络,它能准确地把生成的图片和真实的图片分类。简而言之,这个判别网络就是一个二分类器,对生成的图片输出0,对真实的图片输出1。

其次,开始训练出二代生成网络,它能生成稍好一点的图片,能够让一代的判别网络认为这些生成的图片是真实的图片。然后会训练出一个二代的判别网络,它能准确地识别出真实的图片和二代生成网络生成的图片。以此类推,会有三代、四代、n代生成网络和判别网络,最后当判别网络无法分辨生成的图片和真实图片时,就意味着这个网络拟合了。

GAN的运行过程就是这么简单。至于如何训练GAN就需要了解其深层次的原理,明白其证明与推导,否则没有办法进行有效训练。由于其原理较为艰涩,本书不再讨论过于深入的内容,如果读者有兴趣可以自行参阅相关资料。

2.BERT

BERT(Bidirectional Encoder Representation from Transformers)是自然语言处理领域目前效果最好的模型,基本取代了传统的word2vec和下游任务模型,彻底改变了预训练产生词向量和下游具体NLP任务的关系。

我们需要先介绍一下词向量模型。传统意义上来讲,词向量模型是一个工具,可以把真实世界抽象存在的文字转换成可以进行数学公式操作的向量,而对这些向量的操作才是NLP真正要做的任务。因而从某种意义上来说,可以将NLP任务分成两部分,即预训练产生词向量和对词向量操作(下游具体NLP任务)。

在BERT之前,最常使用的词向量模型是word2vec。这种方法有一些特点,首先,这是一种线性的模型,但是神奇的是用来说明高维空间映射的词向量可以很好体现真实世界中token之间的关系。比如有一个经典的例子:king - man = queen - woman。其次,由于训练词向量模型的目标不是为了得到一个多么精准的语言模型,而是为了获得它的副产物——词向量。所以不是在数十万个单词中艰难计算Softmax,从而获得最优的那个词(就是预测的对于给定词的下一词),而只需在几个词中找到对的那个词就行。这几个词包括一个正例(即直接给定的下一词)和随机产生的噪声词(采样抽取的几个负例),也就是说训练一个Sigmoid二分类器,只要模型能够从中找出正确的词就认为完成任务。该模型的最大缺点就是上下文无关,因而为了让句子有一个整体含义,大家会在下游具体的NLP任务中基于词向量的序列做编码操作。

2018年8月,一个名为ELMo的上下文无关模型发布了,其意味着彻底颠覆了原有算法的思路。首先就是将下游具体NLP任务放到预训练产生的词向量里面,从而达到获得一个根据上下文不同而不断变化的动态词向量,而不是和原来一样在处理词向量时是上下文无关的,具体实现方法是使用双向语言模型(BiLM)——Bi-LSTM来实现,而不是传统的LSTM。

但这里有两个潜在问题,分别是“不完全双向”和“自己看见自己”。

“不完全双向”的含义是模型的前向和后向LSTM两个模型是分别训练的,因此最后的隐藏层是由两个独立训练结果拼接起来的,最后的Loss也是两个独立训练的Loss相加,并非是完全的双向计算。

“自己看见自己”是指要预测的下一个词在给定的序列中已经出现的情况。传统语言模型的数学原理决定了它的单向性。

ELMo模型将上下文编码操作从下游具体的NLP任务转换到了预训练词向量这里,但在具体应用时要做出一些调整。当Bi-LSTM有多层时,由于每层会学到不同的特征,而这些特征在具体应用中由于侧重点不同,所以对每层的关注度也不同。ELMo给原始词向量层和每个隐藏层都设置了一个可训练参数,通过Softmax层归一化后乘到相应的层上并求和,起到了加权作用。

相比于ELMo,BERT是更强大的模型,进一步增加词向量模型的泛化能力,充分描述字符级、词级、句子级甚至句子间的关系特征。首先BERT是真正的双向编码,采用了Masked LM方法确保在训练时可以看到所有位置信息,但特殊符号代替需要预测的词,可以确保双向编码。其次,完全放弃LSTM,采用Transformer做编码器,可以有更深的层数、具有更好的并行性。再次,线性的Transformer比LSTM更易免受Mask标记影响,只需要通过减少自身关注度的Mask标记权重即可,而LSTM类似黑盒模型,很难确定其内部对于Mask标记的处理方式。最后BERT将采样模型提升到了句子的层次,而再也不是单词的级别了。相当于同样会采用负采样,只不过从单词级别变成了语句级别,这样就能获取句子间的关联关系。