Theano-深度学习-全-
Theano 深度学习(全)
原文:
annas-archive.org/md5/39be8fc3387902d01265692ab3d9cda6译者:飞龙
前言
深入了解并实践神经网络架构设计,解决人工智能问题。理解深度学习中最先进网络背后的概念。利用 Python 语言和 Theano 技术,轻松计算导数并最小化你选择的目标函数。
本书内容概览
第一章,Theano 基础,帮助读者学习 Theano 的主要概念,以编写可以在不同硬件架构上编译的代码,并自动优化复杂的数学目标函数。
第二章,使用前馈网络分类手写数字,将介绍一个简单的、广为人知的经典例子,这是深度学习算法优越性证明的起点。最初的问题是识别手写数字。
第三章,将词编码为向量,神经网络面临的主要挑战之一是将现实世界的数据与神经网络的输入连接起来,尤其是对于类别性和离散数据。本章通过使用 Theano 进行训练,展示了如何构建一个嵌入空间的示例。
这样的嵌入非常有用,应用于机器翻译、机器人技术、图像描述等领域,因为它们将现实世界的数据转化为可以被神经网络处理的向量数组。
第四章,使用递归神经网络生成文本,通过一个简单的实践示例引入了神经网络中的递归性,来生成文本。
循环神经网络(RNN)是深度学习中的一个热门话题,为序列预测、序列生成、机器翻译、物联网等提供了更多可能性。自然语言处理(NLP)是另一个推动机器学习新技术研究的领域。
第五章,使用双向 LSTM 分析情感,将嵌入层和递归层应用于自然语言处理中的新任务——情感分析。它作为前面章节的一种验证。
同时,本章还展示了一种在 Theano 上构建神经网络的替代方法,使用一个更高级的库——Keras。
第六章, 使用空间变换网络进行定位,将递归应用于图像,以便一次读取页面上的多个数字。这一次,我们借此机会重写了手写数字图像的分类网络,以及我们的递归模型,在 Lasagne 的帮助下,Lasagne 是一个为 Theano 设计的深度学习模块库。
Lasagne 库有助于更快地设计神经网络进行实验。在这个帮助下,我们将通过空间变换模块来解决物体定位这一常见的计算机视觉挑战,以提高我们的分类得分。
第七章, 使用残差网络进行图像分类,能够以最佳精度对任何类型的图像进行分类。同时,为了更轻松地构建更复杂的网络,我们介绍了一个基于 Theano 框架的库——Lasagne,提供了许多已实现的组件,帮助加速 Theano 中的神经网络实现。
第八章, 通过编码-解码网络进行翻译与解释,介绍了编码-解码技术:应用于文本时,这些技术在机器翻译和简单的聊天机器人系统中被广泛使用。应用于图像时,它们用于场景分割和物体定位。最后,图像字幕生成是一个混合过程,编码图像并解码为文本。
本章进一步介绍了一个非常流行的高级库——Keras,它进一步简化了 Theano 中神经网络的开发。
第九章, 通过注意力机制选择相关输入或记忆,为了完成更复杂的任务,机器学习领域一直在寻求更高层次的智能,灵感来自自然:推理、注意力和记忆。本章将向读者介绍用于自然语言处理(NLP)中的人工智能的主要目的——语言理解的记忆网络。
第十章, 使用高级 RNN 预测时间序列,时间序列是机器学习被广泛应用的重要领域。本章将介绍基于循环神经网络(RNN)的高级技术,以获得最先进的结果。
第十一章, 从环境中学习与强化学习,强化学习是机器学习的一个广阔领域,主要通过训练一个智能体在一个环境中(例如视频游戏),通过在环境中执行特定动作(如按下手柄上的按钮)并观察发生的结果,来优化某一量(例如最大化游戏分数)。
强化学习这一新范式为设计算法以及计算机与现实世界之间的交互开辟了一条全新的路径。
第十二章, 使用无监督生成网络学习特征,无监督学习是新型的训练算法,无需对数据进行标签化即可进行训练。这些算法尝试从数据中推断出隐藏的标签,称为“因子”,并且其中一些算法还能够生成新的合成数据。
无监督训练在许多情况下非常有用,无论是当没有标签数据时,还是当用人工进行标签化太昂贵时,或者当数据集太小且特征工程可能导致过拟合时。在最后一种情况下,更多的无标签数据有助于训练更好的特征,作为监督学习的基础。
第十三章, 使用 Theano 扩展深度学习,扩展了 Theano 在深度学习中的应用范围。它介绍了如何为计算图创建新的操作符,可以在 Python 中为简便性实现,或者在 C 语言中以克服 Python 开销,为 CPU 或 GPU 实现。此外,还介绍了 GPU 并行编程的基本概念。最后,我们基于本书中首先开发的技能,开启了通用智能的领域,逐步开发新的技能,以进一步提升自身能力。
为什么选择 Theano?
投资时间和精力在 Theano 上是非常有价值的,要理解这一点,重要的是要解释为什么 Theano 属于最优秀的深度学习技术之一,而且它远不止是一个深度学习库。有三个原因使 Theano 成为一个值得投资的好选择:
-
它的性能与其他数值计算或深度学习库相当。
-
它融入了一个丰富的 Python 生态系统。
-
它使你能够在给定一个模型的情况下,评估任何由数据约束的函数,提供编译解决方案的自由,用于任何优化问题。
让我们首先关注技术本身的性能。目前深度学习领域最流行的库有 Theano(用于 Python)、Torch(用于 Lua)、Tensorflow(用于 Python)和 Caffe(用于 C++,并有 Python 包装器)。有很多基准测试用于比较不同的深度学习技术。
在 Bastien 等人 2012 年的论文(Theano: 新特性与速度提升,Frédéric Bastien, Pascal Lamblin, Razvan Pascanu, James Bergstra, Ian Goodfellow, Arnaud Bergeron, Nicolas Bouchard, David Warde-Farley, Yoshua Bengio, 2012 年 11 月)中,Theano 在速度上取得了显著进展,但在不同任务上的比较并未明确指出哪个技术优胜。Bahrampour 等人 2016 年的论文(深度学习软件框架的比较研究,Soheil Bahrampour, Naveen Ramakrishnan, Lukas Schott, Mohak Shah, 2016 年 3 月)得出结论:
-
对于基于 GPU 的卷积网络和全连接网络的已训练模型部署,Torch 最为适合,其次是 Theano。
-
对于基于 GPU 的卷积网络和全连接网络的训练,Theano 在小型网络中最快,而 Torch 在大型网络中最快。
-
对于基于 GPU 的递归网络(LSTM)的训练和部署,Theano 表现最佳。
-
对于基于 CPU 的任何已测试深度网络架构的训练和部署,Torch 表现最佳,其次是 Theano。
这些结果在开源的rnn-benchmarks(github.com/glample/rnn-benchmarks)中得到了验证,训练(前向和反向传播)时,Theano 优于 Torch 和 TensorFlow。此外,Theano 在较小批量大小和较大隐藏单元数的情况下压倒性地超过 Torch 和 TensorFlow。对于更大的批量大小和隐藏层大小,差异较小,因为它们更多地依赖于 CUDA 性能,这是所有框架共同使用的底层 NVIDIA 图形库。最后,在最新的soumith benchmarks(github.com/soumith/convnet-benchmarks)中,Theano 在 CPU 上的 fftconv 表现最佳,而在 GPU 上表现最佳的卷积实现,如 cuda-convnet2 和 fbfft,都是 CUDA 扩展,底层库。这些结果应该能说服读者,尽管结果不一,Theano 在速度竞争中扮演着领先角色。
第二个偏向选择 Theano 而非 Torch 的理由是,它拥有丰富的生态系统,既受益于 Python 生态系统,也受益于为 Theano 开发的大量库。本书将介绍其中两个库,Lasagne 和 Keras。Theano 和 Torch 是最具可扩展性的框架,无论是在支持各种深度架构方面,还是在支持库方面。最后,Theano 并不以调试复杂而闻名,这与其他深度学习库相反。
第三点使得 Theano 成为计算机科学家无法比拟的工具,因为它不仅仅局限于深度学习。虽然 Theano 为深度学习提供了与其他库相同的方法,但其基本原理却大不相同:事实上,Theano 在目标架构上编译计算图。这个编译步骤使得 Theano 具有其特殊性,应被定义为一个数学表达式编译器,旨在支持机器学习。符号微分是 Theano 提供的最有用的功能之一,用于实现非标准的深度体系结构。因此,Theano 能够解决更广泛范围的数值问题,并能够在给定现有数据集的情况下找到最小化任何具有可微损失或能量函数的问题的解决方案。
你需要为这本书做准备
安装 Theano 需要使用conda或pip,并且在 Windows、Mac OS 和 Linux 下的安装过程是相同的。
代码已在 Mac OS 和 Linux Ubuntu 下进行了测试。对于 Windows,可能会有一些特殊之处,如修改路径,这些问题 Windows 开发者会很容易解决。
代码示例假定您的计算机上有一个共享文件夹,用于下载、解压缩和预处理可能非常庞大的数据库文件,这些文件不应该留在代码库中。这种做法有助于节省磁盘空间,多个代码目录和用户可以使用同一个数据库的副本。该文件夹通常在用户空间之间共享:
sudo mkdir /sharedfiles
sudo chmod 777 /sharedfiles
这本书适合谁
本书旨在提供关于深度学习的广泛概述,以 Theano 作为支持技术。本书面向深度学习和人工智能的初学者,以及希望获取跨领域经验并熟悉 Theano 及其支持库的计算机程序员。本书帮助读者开始深度学习,并获取深度学习中相关且实用的信息。
需要具备一些基本的 Python 编程和计算机科学技能,以及初等代数和微积分技能。所有实验的基础技术都是 Theano,本书首先深入介绍了核心技术,然后介绍了一些库,以便重用现有模块。
本书的方法是介绍深度学习,描述不同类型的网络及其应用,同时探索 Theano 提供的可能性,这是一种深度学习技术,将支持所有的实现。本书总结了一些表现最佳的网络和最新的结果,并帮助读者全面了解深度学习,从简单到复杂的网络逐步深入。
由于 Python 已成为数据科学的主要编程语言,本书试图涵盖所有Python 程序员在使用 Python 和 Theano 进行深度学习时需要知道的内容。
本书将介绍两个基于 Theano 的抽象框架:Lasagne 和 Keras,它们可以简化更复杂网络的开发,但不会妨碍你理解底层概念。
约定
本书中,你会看到一些文本样式,用以区分不同类型的信息。以下是这些样式的一些示例以及它们的含义。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名显示如下:“该操作符由继承自通用theano.Op类的类定义。”
一段代码块以如下方式显示:
import theano, numpy
class AXPBOp(theano.Op):
"""
This creates an Op that takes x to a*x+b.
"""
__props__ = ("a", "b")
所有命令行输入或输出如下所示:
gsutil mb -l europe-west1 gs://keras_sentiment_analysis
新术语和重要词汇以粗体显示。你在屏幕上看到的词汇,例如菜单或对话框中的内容,会在文本中这样显示:“点击下一步按钮将你带到下一屏幕。”
注释
警告或重要注释会以框体形式显示,如下所示。
提示
提示和技巧像这样呈现。
读者反馈
我们始终欢迎读者反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你能够真正受益的书籍。
若想向我们提供一般反馈,请发送电子邮件至<feedback@packtpub.com>,并在邮件主题中注明书籍的标题。
如果你在某个领域拥有专业知识,并且有兴趣为书籍写作或贡献内容,请查看我们的作者指南:www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们提供了一些资源,帮助你充分利用你的购买。
下载示例代码
你可以从你的帐户下载本书的示例代码文件,网址为:www.packtpub.com。如果你是从其他地方购买本书,可以访问www.packtpub.com/support,注册后直接通过电子邮件获取文件。
你可以通过以下步骤下载代码文件:
-
使用你的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误。
-
在搜索框中输入书名。
-
选择你想下载代码文件的书籍。
-
从下拉菜单中选择你购买本书的地点。
-
点击代码下载。
你也可以通过点击本书网页上的代码文件按钮下载代码文件。你可以通过在搜索框中输入书名来访问此页面。请注意,你需要登录 Packt 账户。
下载文件后,请确保使用以下最新版本的工具解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Deep-Learning-with-Theano。我们还提供了来自我们丰富图书和视频目录的其他代码包,网址为 github.com/PacktPublishing/。快来看看吧!
勘误
虽然我们已经尽力确保内容的准确性,但错误仍然会发生。如果你发现我们书中的任何错误——无论是文本错误还是代码错误——我们将非常感激你向我们报告。通过这样做,你可以帮助其他读者避免困惑,并帮助我们改进本书的后续版本。如果你发现任何勘误,请访问 www.packtpub.com/submit-errata 提交,选择你的书籍,点击勘误提交表格链接,输入勘误详情。勘误一旦验证通过,你的提交将被接受,勘误信息将上传至我们的网站或添加至该书的勘误列表中。
要查看之前提交的勘误信息,请访问 www.packtpub.com/books/content/support,并在搜索框中输入书名。相关信息将出现在勘误部分。
盗版
网络上的版权材料盗版问题是一个跨媒体的持续性问题。在 Packt,我们非常重视版权和许可证的保护。如果你在互联网上遇到我们作品的任何非法复制品,请立即提供位置地址或网站名称,以便我们采取相应的补救措施。
请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌盗版材料的链接。
我们感谢你在保护我们的作者以及我们为你带来有价值内容方面的帮助。
问题
如果你对本书的任何部分有疑问,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章。Theano 基础
本章将 Theano 作为计算引擎介绍,并为符号计算打下基础。符号计算由构建操作图组成,这些操作图将在以后为特定架构进行优化,使用适用于该架构的计算库。
尽管这一章看起来与实际应用相距甚远,但了解这些技术对于后续章节至关重要;它能做什么,它带来了什么价值?接下来的章节将讨论在构建各种深度学习架构时,Theano 的应用。
Theano 可以定义为一个科学计算库;自 2007 年起可用,特别适用于深度学习。两个核心特性是任何深度学习库的基础:张量操作和将代码运行在 CPU 或图形计算单元(GPU)上的能力。这两个特性使我们能够处理大量的多维数据。此外,Theano 还提供自动微分,这是一个非常有用的特性,可以解决比深度学习问题更广泛的数值优化问题。
本章涵盖以下主题:
-
Theano 安装与加载
-
张量与代数
-
符号编程
-
图
-
自动微分
-
GPU 编程
-
性能分析
-
配置
张量的需求
通常,输入数据通过多维数组表示:
-
图像有三个维度:通道数、宽度和高度
-
声音和时间序列具有一个维度:持续时间
-
自然语言序列可以通过二维数组表示:持续时间和字母表长度或词汇表长度
我们将在后续章节中看到更多关于输入数据数组的例子。
在 Theano 中,多维数组通过一个名为张量的抽象类实现,比 Python 等计算机语言中的传统数组有更多的变换功能。
在神经网络的每个阶段,诸如矩阵乘法等计算涉及对这些多维数组的多个操作。
编程语言中的经典数组没有足够的内建功能,无法快速有效地处理多维计算和操作。
对多维数组的计算有着悠久的优化历史,伴随着大量的库和硬件。速度提升的一个重要因素是 GPU 的大规模并行架构,利用数百到数千个核心的计算能力。
与传统的 CPU(例如四核、12 核或 32 核处理器)相比,GPU 的加速效果可以从 5 倍到 100 倍不等,即使部分代码仍然在 CPU 上执行(数据加载、GPU 控制和结果输出)。使用 GPU 的主要瓶颈通常是 CPU 内存与 GPU 内存之间的数据传输,但如果编程得当,GPU 的使用能够显著提高计算速度,缩短实验时间,从几个月缩短到几天,甚至几天缩短到几个小时,这是实验中的一项不可忽视的优势。
Theano 引擎从一开始就设计用于解决多维数组和架构抽象的挑战。
Theano 对科学计算的另一个不可忽视的好处是:自动对多维数组的函数进行微分,这是通过目标函数最小化进行模型参数推断的一个非常合适的功能。这样的特性可以通过免去计算导数的麻烦来促进实验,尽管导数计算本身可能并不复杂,但容易出错。
安装和加载 Theano
在本节中,我们将安装 Theano,分别在 CPU 和 GPU 设备上运行它,并保存配置。
Conda 包管理和环境管理工具
安装 Theano 的最简单方法是使用 conda,一个跨平台的包和环境管理工具。
如果您的操作系统上尚未安装 conda,安装 conda 的最快方法是从 conda.io/miniconda.html 下载 miniconda 安装程序。例如,对于 Linux 64 位和 Python 2.7 下的 conda,使用以下命令:
wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh
chmod +x Miniconda2-latest-Linux-x86_64.sh
bash ./Miniconda2-latest-Linux-x86_64.sh
Conda 使我们能够创建新的环境,其中 Python(2 或 3)的版本以及安装的包可能会有所不同。conda 根环境使用与安装 conda 的系统上相同版本的 Python。
在 CPU 上安装和运行 Theano
让我们来安装 Theano:
conda install theano
启动一个 Python 会话并尝试以下命令来检查您的配置:
>>> from theano import theano
>>> theano.config.device
'cpu'
>>> theano.config.floatX
'float64'
>>> print(theano.config)
最后一个命令打印出 Theano 的所有配置信息。theano.config 对象包含许多配置选项的键。
为了推断配置选项,Theano 会首先查看 ~/.theanorc 文件,然后查看任何可用的环境变量,这些环境变量会覆盖前面的选项,最后查看代码中设置的变量,这些变量按优先级顺序排列:
>>> theano.config.floatX='float32'
有些属性可能是只读的,无法在代码中更改,但 floatX 属性可以直接在代码中更改,它设置了浮点数的默认精度。
注意
建议使用 float32,因为 GPU 在历史上并没有广泛支持 float64。在 GPU 上运行 float64 的速度较慢,有时甚至非常慢(在最新的 Pascal 硬件上,可能慢 2 倍到 32 倍),而 float32 精度在实际应用中已经足够。
GPU 驱动和库
Theano 启用 GPU 的使用,GPU 单元通常用于计算显示在计算机屏幕上的图形。
为了让 Theano 也能在 GPU 上工作,你的系统需要一个 GPU 后端库。
CUDA 库(仅适用于 NVIDIA GPU 卡)是进行 GPU 计算的主要选择。还有 OpenCL 标准,它是开源的,但远不如 CUDA 成熟,而且在 Theano 上的实现更为实验性和初步。
目前,大多数科学计算仍然发生在 NVIDIA 显卡上。如果你拥有 NVIDIA GPU 显卡,可以从 NVIDIA 官网developer.nvidia.com/cuda-downloads下载 CUDA 并安装。安装程序会首先安装最新版本的 GPU 驱动程序(如果尚未安装)。然后,它会将 CUDA 库安装到/usr/local/cuda目录。
安装 cuDNN 库,这是 NVIDIA 提供的一个库,它为 GPU 提供更快的某些操作实现。为了安装它,我通常会将/usr/local/cuda目录复制到一个新目录/usr/local/cuda-{CUDA_VERSION}-cudnn-{CUDNN_VERSION},这样我可以根据所使用的深度学习技术及其兼容性来选择 CUDA 和 cuDNN 的版本。
在你的.bashrc配置文件中,添加以下行以设置$PATH和$LD_LIBRARY_PATH变量:
export PATH=/usr/local/cuda-8.0-cudnn-5.1/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-8.0-cudnn-5.1/lib64:/usr/local/cuda-8.0-cudnn-5.1/lib:$LD_LIBRARY_PATH
在 GPU 上安装并运行 Theano
N 维 GPU 数组已经在 Python 中通过六种不同的 GPU 库实现(Theano/CudaNdarray,PyCUDA/ GPUArray,CUDAMAT/ CUDAMatrix, PYOPENCL/GPUArray, Clyther, Copperhead),它们是NumPy.ndarray的一个子集。Libgpuarray是一个后端库,它提供了一个共同的接口,具有相同的属性。
要通过conda安装libgpuarray,使用以下命令:
conda install pygpu
要在 GPU 模式下运行 Theano,需要在执行前配置config.device变量,因为该变量一旦代码运行后就为只读。可以通过设置THEANO_FLAGS环境变量来运行此命令:
THEANO_FLAGS="device=cuda,floatX=float32" python
>>> import theano
Using cuDNN version 5110 on context None
Mapped name None to device cuda: Tesla K80 (0000:83:00.0)
>>> theano.config.device
'gpu'
>>> theano.config.floatX
'float32'
第一个返回值表明 GPU 设备已被正确检测,并指定了使用的 GPU。
默认情况下,Theano 会激活 CNMeM,这是一个更快的 CUDA 内存分配器。可以使用gpuarra.preallocate选项指定初始预分配。最后,我的启动命令将如下所示:
THEANO_FLAGS="device=cuda,floatX=float32,gpuarray.preallocate=0.8" python
>>> from theano import theano
Using cuDNN version 5110 on context None
Preallocating 9151/11439 Mb (0.800000) on cuda
Mapped name None to device cuda: Tesla K80 (0000:83:00.0)
第一行确认 cuDNN 已激活,第二行确认内存预分配。第三行显示默认的上下文名称(当flag device=cuda被设置时为None),以及使用的 GPU 型号,而 CPU 的默认上下文名称始终为cpu。
可以指定与第一个 GPU 不同的 GPU,将设备设置为 cuda0、cuda1 等,用于多 GPU 计算机。在多个 GPU 上并行或顺序运行程序也是可能的(当一个 GPU 的内存不足时),尤其是在训练非常深的神经网络时,比如在第七章,使用残差网络分类图像中描述的完整图像分类的场景中。在这种情况下,contexts=dev0->cuda0;dev1->cuda1;dev2->cuda2;dev3->cuda3 标志会激活多个 GPU,而不是仅使用一个,并为代码中使用的每个 GPU 设备指定上下文名称。以下是一个 4-GPU 实例的示例:
THEANO_FLAGS="contexts=dev0->cuda0;dev1->cuda1;dev2->cuda2;dev3->cuda3,floatX=float32,gpuarray.preallocate=0.8" python
>>> import theano
Using cuDNN version 5110 on context None
Preallocating 9177/11471 Mb (0.800000) on cuda0
Mapped name dev0 to device cuda0: Tesla K80 (0000:83:00.0)
Using cuDNN version 5110 on context dev1
Preallocating 9177/11471 Mb (0.800000) on cuda1
Mapped name dev1 to device cuda1: Tesla K80 (0000:84:00.0)
Using cuDNN version 5110 on context dev2
Preallocating 9177/11471 Mb (0.800000) on cuda2
Mapped name dev2 to device cuda2: Tesla K80 (0000:87:00.0)
Using cuDNN version 5110 on context dev3
Preallocating 9177/11471 Mb (0.800000) on cuda3
Mapped name dev3 to device cuda3: Tesla K80 (0000:88:00.0)
在这种多 GPU 设置中,为了将计算分配到特定的 GPU,我们选择的名称 dev0、dev1、dev2 和 dev3 已映射到每个设备(cuda0、cuda1、cuda2、cuda3)。
这种名称映射使得我们可以编写与底层 GPU 分配和库(CUDA 或其他)无关的代码。
若要在每个 Python 会话或执行过程中保持当前配置标志处于激活状态,而不使用环境变量,请将配置保存在~/.theanorc文件中,如下所示:
[global]
floatX = float32
device = cuda0
[gpuarray]
preallocate = 1
现在,你可以简单地运行 python 命令。你现在一切就绪。
张量
在 Python 中,一些科学库如 NumPy 提供了多维数组。Theano 并不取代 NumPy,而是与它协同工作。NumPy 被用于张量的初始化。
要在 CPU 和 GPU 上执行相同的计算,变量是符号的,并由张量类表示,张量类是一种抽象,编写数值表达式的过程包括构建一个包含变量节点和应用节点的计算图。根据将要编译计算图的平台,张量将被替换为以下任意一种:
-
一个必须位于 CPU 上的
TensorType变量 -
一个必须位于 GPU 上的
GpuArrayType变量
这样一来,代码就可以在不考虑平台的情况下编写,无论其将在哪个平台上执行。
以下是几个张量对象:
| 对象类别 | 维度数 | 示例 |
|---|---|---|
theano.tensor.scalar |
零维数组 | 1, 2.5 |
theano.tensor.vector |
一维数组 | [0,3,20] |
theano.tensor.matrix |
二维数组 | [[2,3][1,5]] |
theano.tensor.tensor3 |
三维数组 | [[[2,3][1,5]],[[1,2],[3,4]]] |
在 Python shell 中操作这些 Theano 对象,可以帮助我们更好地理解:
>>> import theano.tensor as T
>>> T.scalar()
<TensorType(float32, scalar)>
>>> T.iscalar()
<TensorType(int32, scalar)>
>>> T.fscalar()
<TensorType(float32, scalar)>
>>> T.dscalar()
<TensorType(float64, scalar)>
在对象名称前加上 i、l、f 或 d,你可以初始化一个给定类型的张量,如 integer32、integer64、float32 或 float64。对于实值(浮动点)数据,建议使用直接形式 T.scalar(),而不是 f 或 d 的变体,因为直接形式会使用当前配置的浮动点数据:
>>> theano.config.floatX = 'float64'
>>> T.scalar()
<TensorType(float64, scalar)>
>>> T.fscalar()
<TensorType(float32, scalar)>
>>> theano.config.floatX = 'float32'
>>> T.scalar()
<TensorType(float32, scalar)>
符号变量执行以下任一操作:
-
扮演占位符的角色,作为构建数字运算图(例如加法、乘法)的起点:它们在图形编译后评估时接收输入数据的流。
-
表示中间或输出结果
符号变量和操作都是计算图的一部分,该图将在 CPU 或 GPU 上编译,以实现快速执行。让我们编写第一个计算图,内容是一个简单的加法操作:
>>> x = T.matrix('x')
>>> y = T.matrix('y')
>>> z = x + y
>>> theano.pp(z)
'(x + y)'
>>> z.eval({x: [[1, 2], [1, 3]], y: [[1, 0], [3, 4]]})
array([[ 2., 2.],
[ 4., 7.]], dtype=float32)
首先,创建两个符号变量或变量节点,分别命名为x和y,并在它们之间应用加法操作,即一个应用节点,从而在计算图中创建一个新的符号变量z。
美化打印函数pp打印由 Theano 符号变量表示的表达式。Eval在初始化x和y这两个变量并给它们赋值为两个二维数组时,计算输出变量z的值。
以下示例展示了变量x和y以及它们的名称x和y之间的区别:
>>> a = T.matrix()
>>> b = T.matrix()
>>> theano.pp(a + b)
'(<TensorType(float32, matrix)> + <TensorType(float32, matrix)>)'*.*
没有名称时,在大型图形中追踪节点会更加复杂。当打印计算图时,名称可以显著帮助诊断问题,而变量仅用于处理图中的对象:
>>> x = T.matrix('x')
>>> x = x + x
>>> theano.pp(x)
*'(x + x)'*
在这里,原始的符号变量x没有改变,并且仍然是计算图的一部分。x + x创建了一个新的符号变量,我们将其赋值给 Python 变量x。
还要注意,使用名称时,复数形式会同时初始化多个张量:
>>> x, y, z = T.matrices('x', 'y', 'z')
现在,让我们看看不同的函数来显示图形。
图形与符号计算
让我们回到简单加法的例子,并展示不同的方式来显示相同的信息:
>>> x = T.matrix('x')
>>> y = T.matrix('y')
>>> z = x + y
>>> z
Elemwise{add,no_inplace}.0
>>> theano.pp(z)
*'(x + y)*
>>> theano.printing.pprint(z)
*'(x + y)'*
>>> theano.printing.debugprint(z)
Elemwise{add,no_inplace} [id A] ''
|x [id B]
|y [id C]
在这里,debugprint函数打印的是预编译图形,即未优化的图形。在这种情况下,它由两个变量节点x和y组成,以及一个应用节点,即按元素加法,使用no_inplace选项。inplace选项将在优化后的图形中使用,以节省内存并重用输入的内存来存储操作结果。
如果已经安装了graphviz和pydot库,pydotprint命令将输出图形的 PNG 图像:
>>> theano.printing.pydotprint(z)
The output file is available at ~/.theano/compiledir_Linux-4.4--generic-x86_64-with-Ubuntu-16.04-xenial-x86_64-2.7.12-64/theano.pydotprint.gpu.png.

你可能已经注意到,第一次执行z.eval命令时需要一些时间。这种延迟的原因是优化数学表达式并为 CPU 或 GPU 编译代码所需的时间,然后才会进行求值。
编译后的表达式可以显式获得并作为函数使用,行为与传统的 Python 函数类似:
>>> addition = theano.function([x, y], [z])
>>> addition([[1, 2], [1, 3]], [[1, 0], [3, 4]])
[array([[ 2., 2.],
[ 4., 7.]], dtype=float32)]
函数创建中的第一个参数是一个表示图形输入节点的变量列表。第二个参数是输出变量的数组。要打印编译后的图形,可以使用此命令:
>>> theano.printing.debugprint(addition)
HostFromGpu(gpuarray) [id A] '' 3
|GpuElemwise{Add}[(0, 0)]<gpuarray> [id B] '' 2
|GpuFromHost<None> [id C] '' 1
| |x [id D]
|GpuFromHost<None> [id E] '' 0
|y [id F]
>>> theano.printing.pydotprint(addition)
The output file is available at ~/.theano/compiledir_Linux-4.4--generic-x86_64-with-Ubuntu-16.04-xenial-x86_64-2.7.12-64/theano.pydotprint.gpu.png:

在使用 GPU 时打印了这个案例。在编译过程中,每个操作都选择了可用的 GPU 实现。主程序仍然运行在 CPU 上,数据驻留在 CPU,但 GpuFromHost 指令会将数据从 CPU 传输到 GPU 作为输入,而相反的操作 HostFromGpu 会将结果取回供主程序显示:

Theano 进行了一些数学优化,比如将逐元素操作进行分组,将新的值添加到之前的加法中:
>>> z= z * x
>>> theano.printing.debugprint(theano.function([x,y],z))
HostFromGpu(gpuarray) [id A] '' 3
|GpuElemwise{Composite{((i0 + i1) * i0)}}[(0, 0)]<gpuarray> [id B] '' 2
|GpuFromHost<None> [id C] '' 1
| |x [id D]
|GpuFromHost<None> [id E] '' 0
|y [id F]
图中的节点数量没有增加:两个加法操作被合并成了一个节点。这样的优化使得调试变得更加复杂,因此我们将在本章的最后部分向你展示如何禁用优化以便调试。
最后,让我们再看一下如何用 NumPy 设置初始值:
>>> theano.config.floatX
'float32'
>>> x = T.matrix()
>>> x
<TensorType(float32, matrix)>
>>> y = T.matrix()
>>> addition = theano.function([x, y], [x+y])
>>> addition(numpy.ones((2,2)),numpy.zeros((2,2)))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/site-packages/theano/compile/function_module.py", line 786, in __call__
allow_downcast=s.allow_downcast)
File "/usr/local/lib/python2.7/site-packages/theano/tensor/type.py", line 139, in filter
raise TypeError(err_msg, data)
TypeError: ('Bad input argument to theano function with name "<stdin>:1" at index 0(0-based)', 'TensorType(float32, matrix) cannot store a value of dtype float64 without risking loss of precision. If you do not mind this loss, you can: 1) explicitly cast your data to float32, or 2) set "allow_input_downcast=True" when calling "function".', array([[ 1., 1.],
[ 1., 1.]]))
在 NumPy 数组上执行函数时,抛出了与精度丧失相关的错误,因为这里的 NumPy 数组有 float64 和 int64 的 dtype,而 x 和 y 是 float32。对此有多种解决方案;第一种是使用正确的 dtype 创建 NumPy 数组:
>>> import numpy
>>> addition(numpy.ones((2,2), dtype=theano.config.floatX),numpy.zeros((2,2), dtype=theano.config.floatX))
[array([[ 1., 1.],
[ 1., 1.]], dtype=float32)]
另外,转换 NumPy 数组(特别是对于 numpy.diag,它不允许我们直接选择 dtype):
>>> addition(numpy.ones((2,2)).astype(theano.config.floatX),numpy.diag((2,3)).astype(theano.config.floatX))
[array([[ 3., 1.],
[ 1., 4.]], dtype=float32)]
或者我们可以允许类型下转:
>>> addition = theano.function([x, y], [x+y],allow_input_downcast=True)
>>> addition(numpy.ones((2,2)),numpy.zeros((2,2)))
[array([[ 1., 1.],
[ 1., 1.]], dtype=float32)]
张量上的操作
我们已经看到了如何创建一个由符号变量和操作组成的计算图,并将结果表达式编译为评估或作为一个函数,既可以在 GPU 上也可以在 CPU 上执行。
由于张量在深度学习中非常重要,Theano 提供了许多操作符来操作张量。大多数科学计算库中的操作符,例如 NumPy 中的数值数组操作符,在 Theano 中都有对应的操作符,并且命名类似,以便让 NumPy 用户更熟悉。但与 NumPy 不同,使用 Theano 编写的表达式可以在 CPU 或 GPU 上编译执行。
例如,这就是张量创建的情况:
-
T.zeros()、T.ones()、T.eye()操作符接受一个形状元组作为输入 -
T.zeros_like()、T.one_like()、T.identity_like()使用张量参数的形状 -
T.arange()、T.mgrid()、T.ogrid()用于范围和网格数组
让我们在 Python shell 中看看:
>>> a = T.zeros((2,3))
>>> a.eval()
array([[ 0., 0., 0.],
[ 0., 0., 0.]])
>>> b = T.identity_like(a)
>>> b.eval()
array([[ 1., 0., 0.],
[ 0., 1., 0.]])
>>> c = T.arange(10)
>>> c.eval()
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
信息如维度数量,ndim,和类型,dtype,在张量创建时定义,且不能在之后修改:
>>> c.ndim
*1*
>>> c.dtype
'int64'
>>> c.type
TensorType(int64, vector)
其他信息,如形状,通过计算图进行评估:
>>> a = T.matrix()
>>> a.shape
Shape.0
>>> a.shape.eval({a: [[1, 2], [1, 3]]})
array([2, 2])
>>> shape_fct = theano.function([a],a.shape)
>>> shape_fct([[1, 2], [1, 3]])
array([2, 2])
>>> n = T.iscalar()
>>> c = T.arange(n)
>>> c.shape.eval({n:10})
array([10])
维度操作符
张量的第一个操作符是用于维度操作的。这类操作符以张量作为输入并返回一个新的张量:
| 操作符 | 描述 |
|---|---|
T.reshape |
重新调整张量的维度 |
T.fill |
用相同的值填充数组 |
T.flatten |
返回一个一维张量(向量)中的所有元素 |
T.dimshuffle |
改变维度的顺序,类似于 NumPy 的转置方法,主要区别在于可以添加或删除广播维度(长度为 1 的维度)。 |
T.squeeze |
通过删除等于 1 的维度进行重塑 |
T.transpose |
转置 |
T.swapaxes |
交换维度 |
T.sort, T.argsort |
对张量或排序索引进行排序 |
例如,重塑操作的输出表示一个新的张量,包含相同顺序的相同元素但具有不同形状:
>>> a = T.arange(10)
>>> b = T.reshape( a, (5,2) )
>>> b.eval()
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
运算符可以链式连接:
>>> T.arange(10).reshape((5,2))[::-1].T.eval()
array([[8, 6, 4, 2, 0],
[9, 7, 5, 3, 1]])
注意在 Python 中传统的 [::-1] 数组索引访问和 .T 用于 T.transpose。
逐元素操作
多维数组的第二类操作是逐元素操作。
第一类逐元素操作接受两个相同维度的输入张量,并逐元素应用函数 f,这意味着在各自张量中具有相同坐标的所有元素对上执行操作 f([a,b],[c,d]) = [ f(a,c), f(b,d)]。
>>> a, b = T.matrices('a', 'b')
>>> z = a * b
>>> z.eval({a:numpy.ones((2,2)).astype(theano.config.floatX), b:numpy.diag((3,3)).astype(theano.config.floatX)})
array([[ 3., 0.],
[ 0., 3.]])
同样的乘法可以写成:
>>> z = T.mul(a, b)
T.add 和 T.mul 接受任意数量的输入:
>>> z = T.mul(a, b, a, b)
一些逐元素操作仅接受一个输入张量 f([a,b]) = [f(a),f(b)]):
>>> a = T.matrix()
>>> z = a ** 2
>>> z.eval({a:numpy.diag((3,3)).astype(theano.config.floatX)})
array([[ 9., 0.],
[ 0., 9.]])
最后,我想介绍广播机制。当输入张量的维度不相同时,缺失的维度将被广播,这意味着张量将沿该维度重复以匹配另一个张量的维度。例如,取一个多维张量和一个标量(0 维)张量,标量将在一个与多维张量形状相同的数组中重复,从而最终形状将匹配并且逐元素操作将被应用,f([a,b], c) = [ f(a,c), f(b,c) ]:
>>> a = T.matrix()
>>> b = T.scalar()
>>> z = a * b
>>> z.eval({a:numpy.diag((3,3)).astype(theano.config.floatX),b:3})
array([[ 6., 0.],
[ 0., 6.]])
这是一些逐元素操作的列表:
| 操作符 | 其他形式 | 描述 |
|---|---|---|
T.add, T.sub, T.mul, T.truediv |
+, -, *, / |
加法、减法、乘法、除法 |
T.pow, T.sqrt |
**, T.sqrt |
幂、平方根 |
T.exp, T.log |
指数、对数 | |
T.cos, T.sin, T.tan |
余弦、正弦、正切 | |
T.cosh, T.sinh, T.tanh |
双曲三角函数 | |
T.intdiv, T.mod |
//, % |
整数除法、求余 |
T.floor, T.ceil, T.round |
舍入操作符 | |
T.sgn |
符号 | |
T.and_, T.xor, T.or_, T.invert |
&,^,|,~ |
按位操作符 |
T.gt, T.lt, T.ge, T.le |
>, <, >=, <= |
比较操作符 |
T.eq, T.neq, T.isclose |
等于、不等于或接近于某个值(带容差) | |
T.isnan |
与 NaN(不是一个数字)的比较 | |
T.abs_ |
绝对值 | |
T.minimum, T.maximum |
逐元素的最小值和最大值 | |
T.clip |
将值限制在最大值和最小值之间 | |
T.switch |
开关 | |
T.cast |
张量类型转换 |
元素级别的运算符总是返回与输入数组相同大小的数组。T.switch和T.clip接受三个输入。
特别地,T.switch会逐元素地执行传统的switch运算符:
>>> cond = T.vector('cond')
>>> x,y = T.vectors('x','y')
>>> z = T.switch(cond, x, y)
>>> z.eval({ cond:[1,0], x:[10,10], y:[3,2] })
array([ 10., 2.], dtype=float32)
当cond张量为真时,结果为x值;否则,如果为假,则为y值。
对于T.switch运算符,有一个特定的等效运算符ifelse,它接受标量条件而不是张量条件。尽管如此,它并不是元素级的操作,且支持惰性求值(如果答案在完成之前已知,则不会计算所有元素):
>>> from theano.ifelse import ifelse
>>> z=ifelse(1, 5, 4)
>>> z.eval()
array(5, dtype=int8)
降维运算符
另一种对张量的操作是降维,将所有元素缩减为标量值,在大多数情况下,计算输出时需要扫描所有张量元素:
| 运算符 | 描述 |
|---|---|
T.max, T.argmax, T.max_and_argmax |
最大值,最大值的索引 |
T.min, T.argmin |
最小值,最小值的索引 |
T.sum, T.prod |
元素的和或积 |
T.mean, T.var, T.std |
均值、方差和标准差 |
T.all, T.any |
对所有元素进行 AND 和 OR 操作 |
T.ptp |
元素范围(最小值,最大值) |
这些操作也可以按行或按列进行,通过指定轴和执行降维的维度。
>>> a = T.matrix('a')
>>> T.max(a).eval({a:[[1,2],[3,4]]})
array(4.0, dtype=float32)
>>> T.max(a,axis=0).eval({a:[[1,2],[3,4]]})
array([ 3., 4.], dtype=float32)
>>> T.max(a,axis=1).eval({a:[[1,2],[3,4]]})
array([ 2., 4.], dtype=float32)
线性代数运算符
第三类运算是线性代数运算符,如矩阵乘法:

也称为向量的内积:

| 运算符 | 描述 |
|---|---|
T.dot |
矩阵乘法/内积 |
T.outer |
外积 |
有一些广义版本(T.tensordot用来指定轴),或批量版本(batched_dot, batched_tensordot)的运算符。
最后,仍有一些运算符非常有用,但它们不属于任何前面的类别:T.concatenate沿指定维度连接张量,T.stack创建一个新维度来堆叠输入张量,T.stacklist创建新的模式将张量堆叠在一起:
>>> a = T.arange(10).reshape((5,2))
>>> b = a[::-1]
>>> b.eval()
array([[8, 9],
[6, 7],
[4, 5],
[2, 3],
[0, 1]])
>>> a.eval()
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
>>> T.concatenate([a,b]).eval()
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9],
[8, 9],
[6, 7],
[4, 5],
[2, 3],
[0, 1]])
>>> T.concatenate([a,b],axis=1).eval()
array([[0, 1, 8, 9],
[2, 3, 6, 7],
[4, 5, 4, 5],
[6, 7, 2, 3],
[8, 9, 0, 1]])
>>> T.stack([a,b]).eval()
array([[[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]],
[[8, 9],
[6, 7],
[4, 5],
[2, 3],
[0, 1]]])
NumPy 表达式a[5:] = 5和a[5:] += 5的等效运算是两个函数:
>>> a.eval()
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
>>> T.set_subtensor(a[3:], [-1,-1]).eval()
array([[ 0, 1],
[ 2, 3],
[ 4, 5],
[-1, -1],
[-1, -1]])
>>> T.inc_subtensor(a[3:], [-1,-1]).eval()
array([[0, 1],
[2, 3],
[4, 5],
[5, 6],
[7, 8]])
与 NumPy 的语法不同,原始张量不会被修改;相反,会创建一个新的变量,表示该修改的结果。因此,原始变量a仍然指向原始值,返回的变量(此处未赋值)表示更新后的值,用户应在其余计算中使用该新变量。
内存和变量
始终将浮点数组转换为theano.config.floatX类型是一个好习惯:
-
可以通过
numpy.array(array, dtype=theano.config.floatX)在创建数组时指定类型 -
或通过将数组转换为
array.as_type(theano.config.floatX),以便在 GPU 上编译时使用正确的类型。
例如,手动将数据转移到 GPU(默认上下文为 None),为此我们需要使用float32值:
>>> theano.config.floatX = 'float32'
>>> a = T.matrix()
>>> b = a.transfer(None)
>>> b.eval({a:numpy.ones((2,2)).astype(theano.config.floatX)})
gpuarray.array([[ 1\. 1.]
[ 1\. 1.]], dtype=float32)
>>> theano.printing.debugprint(b)
GpuFromHost<None> [id A] ''
|<TensorType(float32, matrix)> [id B]
transfer(device)函数,例如transfer('cpu'),使我们能够将数据从一个设备移动到另一个设备。当图的某些部分需要在不同设备上执行时,这尤其有用。否则,Theano 会在优化阶段自动向 GPU 添加转移函数:
>>> a = T.matrix('a')
>>> b = a ** 2
>>> sq = theano.function([a],b)
>>> theano.printing.debugprint(sq)
HostFromGpu(gpuarray) [id A] '' 2
|GpuElemwise{Sqr}[(0, 0)]<gpuarray> [id B] '' 1
|GpuFromHost<None> [id C] '' 0
|a [id D]
显式使用转移函数,Theano 去除了转移回 CPU 的操作。将输出张量保留在 GPU 上可以节省昂贵的传输:
>>> b = b.transfer(None)
>>> sq = theano.function([a],b)
>>> theano.printing.debugprint(sq)
GpuElemwise{Sqr}[(0, 0)]<gpuarray> [id A] '' 1
|GpuFromHost<None> [id B] '' 0
|a [id C]
CPU 的默认上下文是cpu:
>>> b = a.transfer('cpu')
>>> theano.printing.debugprint(b)
<TensorType(float32, matrix)> [id A]
数值与符号变量之间的混合概念是共享变量。它们还可以通过避免传输来提高 GPU 的性能。用标量零初始化共享变量:
>>> state = shared(0)
>>> state
<TensorType(int64, scalar)>
>>> state.get_value()
array(0)
>>> state.set_value(1)
>>> state.get_value()
array(1)
共享值设计用于在函数之间共享。它们也可以看作是内部状态。它们可以无差别地用于 GPU 或 CPU 编译代码。默认情况下,共享变量是在默认设备(此处为cuda)上创建的,除非是标量整数值(如前面的例子所示)。
可以指定另一个上下文,例如cpu。在多个 GPU 实例的情况下,您需要在 Python 命令行中定义上下文,并决定在哪个上下文中创建共享变量:
PATH=/usr/local/cuda-8.0-cudnn-5.1/bin:$PATH THEANO_FLAGS="contexts=dev0->cuda0;dev1->cuda1,floatX=float32,gpuarray.preallocate=0.8" python
>>> from theano import theano
Using cuDNN version 5110 on context dev0
Preallocating 9151/11439 Mb (0.800000) on cuda0
Mapped name dev0 to device cuda0: Tesla K80 (0000:83:00.0)
Using cuDNN version 5110 on context dev1
Preallocating 9151/11439 Mb (0.800000) on cuda1
Mapped name dev1 to device cuda1: Tesla K80 (0000:84:00.0)
>>> import theano.tensor as T
>>> import numpy
>>> theano.shared(numpy.random.random((1024, 1024)).astype('float32'),target='dev1')
<GpuArrayType<dev1>(float32, (False, False))>
函数和自动微分
前一节介绍了function指令来编译表达式。在这一节中,我们将展开其签名中的一些参数:
def theano.function(inputs,
outputs=None, updates=None, givens=None,
allow_input_downcast=None, mode=None, profile=None,
)
我们已经使用了allow_input_downcast功能将数据从float64转换为float32,int64转换为int32,以此类推。mode和profile功能也会显示,因为它们将在优化和调试部分展示。
Theano 函数的输入变量应该包含在列表中,即使只有一个输入。
对于输出,在多个输出需要并行计算的情况下,可以使用列表:
>>> a = T.matrix()
>>> ex = theano.function([a],[T.exp(a),T.log(a),a**2])
>>> ex(numpy.random.randn(3,3).astype(theano.config.floatX))
[array([[ 2.33447003, 0.30287042, 0.63557744],
[ 0.18511547, 1.34327984, 0.42203984],
[ 0.87083125, 5.01169062, 6.88732481]], dtype=float32),
array([[-0.16512829, nan, nan],
[ nan, -1.2203927 , nan],
[ nan, 0.47733498, 0.65735561]], dtype=float32),
array([[ 0.71873927, 1.42671108, 0.20540957],
[ 2.84521151, 0.08709242, 0.74417454],
[ 0.01912885, 2.59781313, 3.72367549]], dtype=float32)]
第二个有用的属性是updates属性,用于在表达式求值后设置共享变量的新值:
>>> w = shared(1.0)
>>> x = T.scalar('x')
>>> mul = theano.function([x],updates=[(w,w*x)])
>>> mul(4)
[]
>>> w.get_value()
array(4.0)
这种机制可以作为内部状态使用。共享变量w已在函数外定义。
使用givens参数,可以更改图中任何符号变量的值,而无需更改图。新值将由所有指向它的其他表达式使用。
Theano 中最后也是最重要的特性是自动微分,这意味着 Theano 会计算所有先前张量操作符的导数。这样的微分通过theano.grad操作符完成:
>>> a = T.scalar()
>>> pow = a ** 2
>>> g = theano.grad(pow,a)
>>> theano.printing.pydotprint(g)
>>> theano.printing.pydotprint(theano.function([a],g))

在优化图中,theano.grad已经计算了关于a的
的梯度,这是一个等价于2 * a的符号表达式。
请注意,只有标量的梯度可以被求取,但相对于(wrt)变量可以是任意张量。
符号计算中的循环
Python 的for循环可以在符号图外部使用,就像在普通的 Python 程序中一样。但在图外,传统的 Python for循环不会被编译,因此它不会使用并行和代数库进行优化,也不能自动微分,并且如果计算子图已经为 GPU 优化,可能会引入昂贵的数据传输。
这就是为什么符号操作符T.scan设计为在图中创建for循环作为操作符的原因。Theano 会将这个循环展开到图结构中,整个展开后的循环会与其他计算图一起在目标架构上进行编译。其签名如下:
def scan(fn,
sequences=None,
outputs_info=None,
non_sequences=None,
n_steps=None,
truncate_gradient=-1,
go_backwards=False,
mode=None,
name=None,
profile=False,
allow_gc=None,
strict=False)
scan操作符非常有用,可以实现数组循环、归约、映射、多维导数(如 Jacobian 或 Hessian)以及递归。
scan操作符会重复运行fn函数,直到n_steps。如果n_steps是None,操作符将根据序列的长度来确定:
注意
步骤fn函数是构建符号图的函数,该函数只会被调用一次。然而,该图会被编译成另一个 Theano 函数,然后会被反复调用。一些用户尝试将已编译的 Theano 函数传递给fn,这是不可能的。
序列是循环中输入变量的列表。步数将对应于列表中最短的序列。让我们来看一下:
>>> a = T.matrix()
>>> b = T.matrix()
>>> def fn(x): return x + 1
>>> results, updates = theano.scan(fn, sequences=a)
>>> f = theano.function([a], results, updates=updates)
>>> f(numpy.ones((2,3)).astype(theano.config.floatX))
array([[ 2., 2., 2.],
[ 2., 2., 2.]], dtype=float32)
scan操作符已经在输入张量a的所有元素上运行该函数,并保持与输入张量相同的形状,(2,3)。
注意
即使这些更新为空,最好还是将theano.scan返回的更新添加到theano.function中。
传递给fn函数的参数可以更加复杂。T.scan会在每一步调用fn函数,并按以下顺序传入参数列表:
fn( sequences (if any), prior results (if needed), non-sequences (if any) )
如下图所示,三个箭头指向fn步骤函数,代表循环中每个时间步的三种可能输入类型:

如果指定,outputs_info参数是用于开始递归的初始状态。该参数名听起来不太好,但初始状态也提供了最后一个状态的形状信息,以及所有其他状态。初始状态可以看作是第一个输出。最终输出将是一个状态数组。
例如,要计算一个向量的累积和,初始状态为0,可以使用以下代码:
>>> a = T.vector()
>>> s0 = T.scalar("s0")
>>> def fn( current_element, prior ):
... return prior + current_element
>>> results, updates = theano.scan(fn=fn,outputs_info=s0,sequences=a)
>>> f = theano.function([a,s0], results, updates=updates)
>>> f([0,3,5],0)
*array([ 0., 3., 8.], dtype=float32)*
当设置 outputs_info 时,outputs_info 和序列变量的第一个维度是时间步。第二个维度是每个时间步的数据维度。
特别地,outputs_info 包含计算第一步所需的先前时间步数。
这是相同的示例,但每个时间步使用一个向量,而不是标量作为输入数据:
>>> a = T.matrix()
>>> s0 = T.scalar("s0")
>>> def fn( current_element, prior ):
... return prior + current_element.sum()
>>> results, updates = theano.scan(fn=fn,outputs_info=s0,sequences=a)
>>> f = theano.function([a,s0], results, updates=updates)
>>> f(numpy.ones((20,5)).astype(theano.config.floatX),0)
array([ 5., 10., 15., 20., 25., 30., 35., 40., 45.,
50., 55., 60., 65., 70., 75., 80., 85., 90.,
95., 100.], dtype=float32)
沿着行(时间步)走了二十步,累计了所有元素的和。注意,由 outputs_info 参数给出的初始状态(此处为 0)不属于输出序列的一部分。
通过 non_sequences 扫描参数,递归函数 fn 可以在每次循环步骤中提供一些固定数据,独立于当前步骤:
>>> a = T.vector()
>>> s0 = T.scalar("s0")
>>> def fn( current_element, prior, non_seq ):
... return non_seq * prior + current_element
>>> results, updates = theano.scan(fn=fn,n_steps=10,sequences=a,outputs_info=T.constant(0.0),non_sequences=s0)
>>> f = theano.function([a,s0], results, updates=updates)
>>> f(numpy.ones((20)).astype(theano.),5)
array([ 1.00000000e+00, 6.00000000e+00, 3.10000000e+01,
1.56000000e+02, 7.81000000e+02, 3.90600000e+03,
1.95310000e+04, 9.76560000e+04, 4.88281000e+05,
2.44140600e+06], dtype=float32)
它将先前的值乘以 5 并添加新元素。
请注意,优化后的图在 GPU 上的 T.scan 不会并行执行循环的不同迭代,即使没有递归。
配置、分析和调试
为了调试目的,Theano 可以打印更详细的信息,并提供不同的优化模式:
>>> theano.config.exception_verbosity='high'
>>> theano.config.mode
'Mode'
>>> theano.config.optimizer='fast_compile'
为了让 Theano 使用 config.optimizer 值,必须将模式设置为 Mode,否则将使用 config.mode 中的值:
| config.mode / 函数模式 | config.optimizer (*) | 描述 |
|---|---|---|
FAST_RUN |
fast_run |
默认;最佳运行性能,编译较慢 |
FAST_RUN |
None |
禁用优化 |
FAST_COMPILE |
fast_compile |
减少优化次数,编译更快 |
None |
使用默认模式,相当于 FAST_RUN;optimizer=None |
|
NanGuardMode |
NaN、Inf 和异常大的值将引发错误 | |
DebugMode |
编译过程中进行自检和断言 |
在函数编译中,config.mode 中相同的参数可以用于 Mode 参数:
>>> f = theano.function([a,s0], results, updates=updates, mode='FAST_COMPILE')
禁用优化并选择高详细度输出有助于找到计算图中的错误。
对于 GPU 调试,您需要通过环境变量 CUDA_LAUNCH_BLOCKING 设置同步执行,因为 GPU 执行默认是完全异步的:
CUDA_LAUNCH_BLOCKING=1 python
要找出计算图中延迟的来源,Theano 提供了分析模式。
激活分析:
>>> theano.config.profile=True
激活内存分析:
>>> theano.config.profile_memory=True
激活优化阶段的分析:
>>> theano.config.profile_optimizer=True
或者直接在编译期间:
>>> f = theano.function([a,s0], results, profile=True)
>>> f.profile.summary()
Function profiling
==================
Message: <stdin>:1
Time in 1 calls to Function.__call__: 1.490116e-03s
Time in Function.fn.__call__: 1.251936e-03s (84.016%)
Time in thunks: 1.203537e-03s (80.768%)
Total compile time: 1.720619e-01s
Number of Apply nodes: 14
Theano Optimizer time: 1.382768e-01s
Theano validate time: 1.308680e-03s
Theano Linker time (includes C, CUDA code generation/compiling): 2.405691e-02s
Import time 1.272917e-03s
Node make_thunk time 2.329803e-02s
Time in all call to theano.grad() 0.000000e+00s
Time since theano import 520.661s
Class
---
<% time> <sum %> <apply time> <time per call> <type> <#call> <#apply> <Class name>
58.2% 58.2% 0.001s 7.00e-04s Py 1 1 theano.scan_module.scan_op.Scan
27.3% 85.4% 0.000s 1.64e-04s Py 2 2 theano.sandbox.cuda.basic_ops.GpuFromHost
6.1% 91.5% 0.000s 7.30e-05s Py 1 1 theano.sandbox.cuda.basic_ops.HostFromGpu
5.5% 97.0% 0.000s 6.60e-05s C 1 1 theano.sandbox.cuda.basic_ops.GpuIncSubtensor
1.1% 98.0% 0.000s 3.22e-06s C 4 4 theano.tensor.elemwise.Elemwise
0.7% 98.8% 0.000s 8.82e-06s C 1 1 theano.sandbox.cuda.basic_ops.GpuSubtensor
0.7% 99.4% 0.000s 7.87e-06s C 1 1 theano.sandbox.cuda.basic_ops.GpuAllocEmpty
0.3% 99.7% 0.000s 3.81e-06s C 1 1 theano.compile.ops.Shape_i
0.3% 100.0% 0.000s 1.55e-06s C 2 2 theano.tensor.basic.ScalarFromTensor
... (remaining 0 Classes account for 0.00%(0.00s) of the runtime)
Ops
---
<% time> <sum %> <apply time> <time per call> <type> <#call> <#apply> <Op name>
58.2% 58.2% 0.001s 7.00e-04s Py 1 1 forall_inplace,gpu,scan_fn}
27.3% 85.4% 0.000s 1.64e-04s Py 2 2 GpuFromHost
6.1% 91.5% 0.000s 7.30e-05s Py 1 1 HostFromGpu
5.5% 97.0% 0.000s 6.60e-05s C 1 1 GpuIncSubtensor{InplaceSet;:int64:}
0.7% 97.7% 0.000s 8.82e-06s C 1 1 GpuSubtensor{int64:int64:int16}
0.7% 98.4% 0.000s 7.87e-06s C 1 1 GpuAllocEmpty
0.3% 98.7% 0.000s 4.05e-06s C 1 1 Elemwise{switch,no_inplace}
0.3% 99.0% 0.000s 4.05e-06s C 1 1 Elemwise{le,no_inplace}
0.3% 99.3% 0.000s 3.81e-06s C 1 1 Shape_i{0}
0.3% 99.6% 0.000s 1.55e-06s C 2 2 ScalarFromTensor
0.2% 99.8% 0.000s 2.86e-06s C 1 1 Elemwise{Composite{Switch(LT(i0, i1), i0, i1)}}
0.2% 100.0% 0.000s 1.91e-06s C 1 1 Elemwise{Composite{Switch(i0, i1, minimum(i2, i3))}}[(0, 2)]
... (remaining 0 Ops account for 0.00%(0.00s) of the runtime)
Apply
------
<% time> <sum %> <apply time> <time per call> <#call> <id> <Apply name>
58.2% 58.2% 0.001s 7.00e-04s 1 12 forall_inplace,gpu,scan_fn}(TensorConstant{10}, GpuSubtensor{int64:int64:int16}.0, GpuIncSubtensor{InplaceSet;:int64:}.0, GpuFromHost.0)
21.9% 80.1% 0.000s 2.64e-04s 1 3 GpuFromHost(<TensorType(float32, vector)>)
6.1% 86.2% 0.000s 7.30e-05s 1 13 HostFromGpu(forall_inplace,gpu,scan_fn}.0)
5.5% 91.6% 0.000s 6.60e-05s 1 4 GpuIncSubtensor{InplaceSet;:int64:}(GpuAllocEmpty.0, CudaNdarrayConstant{[ 0.]}, Constant{1})
5.3% 97.0% 0.000s 6.41e-05s 1 0 GpuFromHost(s0)
0.7% 97.7% 0.000s 8.82e-06s 1 11 GpuSubtensor{int64:int64:int16}(GpuFromHost.0, ScalarFromTensor.0, ScalarFromTensor.0, Constant{1})
0.7% 98.4% 0.000s 7.87e-06s 1 1 GpuAllocEmpty(TensorConstant{10})
0.3% 98.7% 0.000s 4.05e-06s 1 8 Elemwise{switch,no_inplace}(Elemwise{le,no_inplace}.0, TensorConstant{0}, TensorConstant{0})
0.3% 99.0% 0.000s 4.05e-06s 1 6 Elemwise{le,no_inplace}(Elemwise{Composite{Switch(LT(i0, i1), i0, i1)}}.0, TensorConstant{0})
0.3% 99.3% 0.000s 3.81e-06s 1 2 Shape_i{0}(<TensorType(float32, vector)>)
0.3% 99.6% 0.000s 3.10e-06s 1 10 ScalarFromTensor(Elemwise{switch,no_inplace}.0)
0.2% 99.8% 0.000s 2.86e-06s 1 5 Elemwise{Composite{Switch(LT(i0, i1), i0, i1)}}(TensorConstant{10}, Shape_i{0}.0)
0.2% 100.0% 0.000s 1.91e-06s 1 7 Elemwise{Composite{Switch(i0, i1, minimum(i2, i3))}}(0, 2), i0, i1)}}.0, Shape_i{0}.0)
0.0% 100.0% 0.000s 0.00e+00s 1 9 ScalarFromTensor(Elemwise{Composite{Switch(i0, i1, minimum(i2, i3))}}[(0, 2)].0)
... (remaining 0 Apply instances account for 0.00%(0.00s) of the runtime)
总结
第一个概念是符号计算,它是构建可以编译并在 Python 代码中任何地方执行的图。一个编译后的图就像一个函数,可以在代码中的任何地方调用。符号计算的目的是对图将执行的架构进行抽象,以及选择哪些库来进行编译。如前所述,符号变量在编译期间会根据目标架构进行类型化。
第二个概念是张量,以及用于操作张量的运算符。这些运算符大部分已经在基于 CPU 的计算库中提供,例如 NumPy 或 SciPy。它们只是被移植到符号计算中,需要在 GPU 上提供其等价物。它们使用底层加速库,如 BLAS、Nvidia Cuda 和 cuDNN。
Theano 引入的最后一个概念是自动微分——这是深度学习中非常有用的特性,用于反向传播误差并根据梯度调整权重,这一过程被称为梯度下降。另外,scan 运算符使我们能够在 GPU 上编程循环(while...、for...),并且与其他运算符一样,也通过反向传播提供,极大简化了模型的训练。
我们现在准备在接下来的几章中将这些应用到深度学习中,看看这些知识在实际中的应用。
第二章。使用前馈网络分类手写数字
第一章介绍了 Theano 作为一个计算引擎,以及它的不同功能和特点。有了这些知识,我们将通过一个例子来介绍深度学习的一些主要概念,并构建三个神经网络,对手写数字分类问题进行训练。
深度学习是机器学习的一个领域,其中模块层被堆叠在彼此之上:本章介绍了一个简单的单线性层模型,然后在其上添加第二层来创建多层感知器(MLP),最后使用多个卷积层来创建卷积神经网络(CNN)。
与此同时,本章还为不熟悉数据科学的人回顾了基本的机器学习概念,如过拟合、验证和损失分析:
-
小型图像分类
-
手写数字识别挑战
-
层设计以构建神经网络
-
设计经典目标/损失函数
-
使用随机梯度下降的反向传播
-
在验证集上训练数据集
-
卷积神经网络
-
朝向手写数字分类的最新结果
MNIST 数据集
修改后的国家标准技术研究所(MNIST)数据集是一个非常著名的手写数字数据集 {0,1,2,3,4,5,6,7,8,9},用于训练和测试分类模型。
分类模型是一个根据输入预测类别观察概率的模型。
训练是学习参数,使模型尽可能适合数据,以便对于任何输入图像,都能预测出正确的标签。对于此训练任务,MNIST 数据集包含 60,000 个图像,每个示例有一个目标标签(介于 0 和 9 之间的数字)。
为了验证训练效果并决定何时停止训练,通常将训练数据集分成两个数据集:80%到 90%的图像用于训练,而剩余的 10%到 20%的图像则不会用于训练算法,而是用于验证模型在未观察数据上的泛化能力。
在训练过程中,应该避免看到的另一个单独的数据集,称为测试集,在 MNIST 数据集中包含 10,000 个图像。
在 MNIST 数据集中,每个示例的输入数据是一个 28x28 的归一化单色图像和一个标签,表示每个示例的简单整数介于 0 和 9 之间。让我们展示其中一些:
-
首先,下载一个预打包版本的数据集,这样可以更容易地从 Python 中加载:
wget http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz -P /sharedfiles -
然后将数据加载到 Python 会话中:
import pickle, gzip with gzip.open("/sharedfiles/mnist.pkl.gz", 'rb') as f: train_set, valid_set, test_set = pickle.load(f)对于
Python3,由于其序列化方式,我们需要使用pickle.load(f, encoding='latin1')。train_set[0].shape *(50000, 784)* train_set[1].shape *(50000,)* import matplotlib import numpy import matplotlib.pyplot as plt plt.rcParams['figure.figsize'] = (10, 10) plt.rcParams['image.cmap'] = 'gray' for i in range(9): plt.subplot(1,10,i+1) plt.imshow(train_set[0][i].reshape(28,28)) plt.axis('off') plt.title(str(train_set[1][i])) plt.show()
数据集的前九个样本显示了相应的标签(地面实况,即分类算法预期的正确答案):

为了避免过多的数据传输到 GPU,并且由于整个数据集足够小,能够适应 GPU 的内存,我们通常将整个训练集放入共享变量中:
import theano
train_set_x = theano.shared(numpy.asarray(train_set[0], dtype=theano.config.floatX))
train_set_y = theano.shared(numpy.asarray(train_set[1], dtype='int32'))
避免这些数据传输可以让我们在 GPU 上更快地进行训练,尽管最近的 GPU 和高速 PCIe 连接也在帮助提升速度。
更多关于数据集的信息请访问 yann.lecun.com/exdb/mnist/。
训练程序结构
训练程序的结构总是包含以下步骤:
-
设置脚本环境:例如包的导入、GPU 的使用等。
-
加载数据:数据加载器类,用于在训练期间访问数据,通常是随机顺序,以避免出现过多相似类别的示例,但有时也可能是精确顺序,例如在课程学习中,先使用简单示例,最后使用复杂示例。
-
预处理数据:一组转换操作,例如交换图像的维度、添加模糊或噪声。常见的做法是添加一些数据增强转换,比如随机裁剪、缩放、亮度或对比度波动,从而获得比原始数据更多的示例,并减少过拟合的风险。如果模型中自由参数的数量相对于训练数据集的大小过多,模型可能会仅从可用示例中学习。而且,如果数据集太小并且对同一数据进行了过多的迭代,模型可能会过于特定于训练示例,而在新的未见过的示例上泛化能力较差。
-
构建模型:定义模型结构,并在持久变量(共享变量)中设置参数,以便在训练过程中更新其值,从而拟合训练数据。
-
训练:有不同的算法,可以是对整个数据集进行训练,也可以是逐个示例进行逐步训练。通常,最佳的收敛效果是通过对批量进行训练实现的,批量是将一小部分示例分组在一起,数量从几十个到几百个不等。
使用批量的另一个原因是提高 GPU 的训练速度,因为单独的数据传输成本较高,并且 GPU 内存不足以容纳整个数据集。GPU 是并行架构,因此处理一个批次的示例通常比逐个处理示例更快,直到达到某个点。一次性查看更多示例可以加速收敛(以墙时间为准),直到达到某个点。即使 GPU 内存足够大,可以容纳整个数据集,这一点也是成立的:批量大小的收益递减通常使得使用较小的批量比使用整个数据集更快。需要注意的是,这对于现代 CPU 也是成立的,但最优的批量大小通常较小。
注意
一次迭代定义了对一个批次的训练。一个 epoch 是算法查看完整数据集所需的迭代次数。
-
在训练过程中,在一定数量的迭代后,通常会使用训练数据的拆分或未用于学习的验证数据集进行验证。在该验证集上计算损失。尽管算法的目标是减少给定训练数据的损失,但它并不保证能在未见数据上进行泛化。验证数据是未见过的数据,用于估算泛化性能。当训练数据不具代表性、存在异常且未正确抽样,或模型过拟合训练数据时,可能会发生泛化不足的情况。
验证数据验证一切是否正常,并在验证损失不再下降时停止训练,即使训练损失可能继续下降:进一步的训练已不再有价值,并可能导致过拟合。
-
保存模型参数并显示结果,如最佳训练/验证损失值、用于收敛分析的训练损失曲线。
在分类问题中,我们在训练过程中计算准确率(正确分类的百分比)或错误率(错误分类的百分比),以及损失。训练结束时,混淆矩阵有助于评估分类器的质量。
让我们在实践中看看这些步骤,并在 Python shell 会话中启动 Theano 会话:
from theano import theano import theano.tensor as T
分类损失函数
损失函数是一个目标函数,在训练过程中最小化它以获得最佳模型。存在许多不同的损失函数。
在分类问题中,目标是在 k 个类别中预测正确类别,交叉熵通常被用作损失函数,因为它衡量的是每个类别的真实概率分布 q 和预测概率分布 p 之间的差异:

在这里,i是数据集中的样本索引,n是数据集中的样本数量,k是类别的数量。
虽然每个类别的真实概率
是未知的,但在实践中它可以通过经验分布来简单地近似,即按数据集顺序从数据集中随机抽取样本。同样,任何预测概率p的交叉熵也可以通过经验交叉熵来近似:

在这里,
是模型为正确类别示例
估算的概率。
准确率和交叉熵都朝着相同的方向发展,但衡量的是不同的内容。准确率衡量预测类别的正确性,而交叉熵衡量概率之间的距离。交叉熵的下降说明预测正确类别的概率变得更高,但准确率可能保持不变或下降。
虽然准确率是离散的且不可微分,但交叉熵损失是一个可微分的函数,便于用于模型训练。
单层线性模型
最简单的模型是线性模型,其中对于每个类别c,输出是输入值的线性组合:

这个输出是无界的。
为了得到一个概率分布,p[i],其总和为 1,线性模型的输出被传入 softmax 函数:

因此,类c对输入x的估计概率可以用向量重写:

在 Python 中的实现如下:
batch_size = 600
n_in = 28 * 28
n_out = 10
x = T.matrix('x')
y = T.ivector('y')
W = theano.shared(
value=numpy.zeros(
(n_in, n_out),
dtype=theano.config.floatX
),
name='W',
borrow=True
)
b = theano.shared(
value=numpy.zeros(
(n_out,),
dtype=theano.config.floatX
),
name='b',
borrow=True
)
model = T.nnet.softmax(T.dot(x, W) + b)
给定输入的预测由最可能的类别(最大概率)给出:
y_pred = T.argmax(model, axis=1)
在这个单层线性模型中,信息从输入到输出流动:它是一个前馈网络。给定输入计算输出的过程称为前向传播。
这个层被称为全连接层,因为所有的输出,
,是通过一个乘法系数将所有输入值的和连接在一起:

成本函数和错误
给定模型预测的概率,成本函数如下:
cost = -T.mean(T.log(model)[T.arange(y.shape[0]), y])
错误是与真实类别不同的预测数量,按总值数量平均,可以写成一个均值:
error = T.mean(T.neq(y_pred, y))
相反,准确率是正确预测的数量除以总预测数。错误和准确率的总和为 1。
对于其他类型的问题,以下是一些其他的损失函数及实现:
| 分类交叉熵我们实现的等效版本 |
|---|
T.nnet.categorical_crossentropy(model, y_true).mean()
|
| 二元交叉熵当输出只能取两个值{0,1}时,通常在使用 sigmoid 激活预测概率 p 后使用 |
|---|
T.nnet.binary_crossentropy(model, y_true).mean()
|
| 均方误差回归问题的 L2 范数 |
|---|
T.sqr(model – y_true).mean()
|
| 均绝对误差回归问题的 L1 范数 |
|---|
T.abs_(model - y_true).mean()
|
| 平滑 L1L1 用于大值,L2 用于小值,通常作为回归问题中的抗异常值损失 |
|---|
T.switch(
T.lt(T.abs_(model - y_true) , 1\. / sigma),
0.5 * sigma * T.sqr(model - y_true),
T.abs_(model - y_true) – 0.5 / sigma )
.sum(axis=1).mean()
|
| 平方铰链损失特别用于无监督问题 |
|---|
T.sqr(T.maximum(1\. - y_true * model, 0.)).mean()
|
| 铰链损失 |
|---|
T.maximum(1\. - y_true * model, 0.).mean()
|
反向传播和随机梯度下降
反向传播,或称为误差的反向传播,是最常用的监督学习算法,用于调整连接权重。
将错误或成本视为权重W和b的函数,可以通过梯度下降接近成本函数的局部最小值,梯度下降的过程是沿着负错误梯度改变权重:

这里,
是学习率,一个正的常数,定义了下降的速度。
以下编译的函数在每次前馈运行后更新变量:
g_W = T.grad(cost=cost, wrt=W)
g_b = T.grad(cost=cost, wrt=b)
learning_rate=0.13
index = T.lscalar()
train_model = theano.function(
inputs=[index],
outputs=[cost,error],
updates=[(W, W - learning_rate * g_W),(b, b - learning_rate * g_b)],
givens={
x: train_set_x[index * batch_size: (index + 1) * batch_size],
y: train_set_y[index * batch_size: (index + 1) * batch_size]
}
)
输入变量是批次的索引,因为所有数据集已经通过一次传递转移到 GPU 中的共享变量。
训练过程包括将每个样本迭代地呈现给模型(迭代次数),并多次重复操作(训练周期):
n_epochs = 1000
print_every = 1000
n_train_batches = train_set[0].shape[0] // batch_size
n_iters = n_epochs * n_train_batches
train_loss = np.zeros(n_iters)
train_error = npzeros(n_iters)
for epoch in range(n_epochs):
for minibatch_index in range(n_train_batches):
iteration = minibatch_index + n_train_batches * epoch
train_loss[iteration], train_error[iteration] = train_model(minibatch_index)
if (epoch * train_set[0].shape[0] + minibatch_index) % print_every == 0 :
print('epoch {}, minibatch {}/{}, training error {:02.2f} %, training loss {}'.format(
epoch,
minibatch_index + 1,
n_train_batches,
train_error[iteration] * 100,
train_loss[iteration]
))
这只报告了一个小批次的损失和误差,最好还能够报告整个数据集上的平均值。
在前几次迭代中,误差率快速下降,然后逐渐放缓。
在一台 GPU GeForce GTX 980M 笔记本电脑上的执行时间是 67.3 秒,而在 Intel i7 CPU 上的时间为 3 分钟 7 秒。
经过一段时间后,模型收敛到 5.3% - 5.5% 的误差率,再进行几轮迭代可能会进一步下降,但也可能导致过拟合。过拟合是指模型很好地拟合了训练数据,但在未见过的数据上表现不佳,误差率较高。
在这种情况下,模型过于简单,无法对这些数据进行过拟合。
一个过于简单的模型无法很好地学习。深度学习的原则是添加更多的层,也就是增加深度,构建更深的网络,以获得更好的准确性。
我们将在接下来的部分看到如何计算模型准确度的更好估计以及训练停止的时机。
多层模型
多层感知器(MLP)是一个具有多层的前馈神经网络。在前面的例子中,增加了一个第二个线性层,称为隐藏层:

两个线性层紧跟在一起等同于一个线性层。
使用非线性函数、非线性或转换函数在各线性之间,模型不再简化为线性模型,并表示更多可能的函数,以捕捉数据中更复杂的模式:

激活函数有助于饱和(开-关),并重现生物神经元的激活。
Rectified Linear Unit(ReLU)的图形如下所示:
(x + T.abs_(x)) / 2.0

Leaky Rectifier Linear Unit(Leaky ReLU)的图形如下所示:
((1 + leak) * x + (1 – leak) * T.abs_(x)) / 2.0

在这里,leak 是一个参数,定义了负值部分的斜率。在泄漏整流器中,这个参数是固定的。
名为 PReLU 的激活函数考虑了需要学习的 leak 参数。
更一般地说,可以通过添加一个线性层,然后是 n_pool 单元的最大化激活,来学习分段线性激活:
T.max([x[:, n::n_pool] for n in range(n_pool)], axis=0)
这将输出 n_pool 个值或单元,代表底层学习到的线性关系:
Sigmoid(T.nnet.sigmoid)

HardSigmoid 函数表示为:
T.clip(X + 0.5, 0., 1.)

HardTanh 函数表示为:
T.clip(X, -1., 1.)

Tanh 函数定义如下:
T.tanh(x)

这个用 Python 编写的两层网络模型如下:
batch_size = 600
n_in = 28 * 28
n_hidden = 500
n_out = 10
def shared_zeros(shape, dtype=theano.config.floatX, name='', n=None):
shape = shape if n is None else (n,) + shape
return theano.shared(np.zeros(shape, dtype=dtype), name=name)
def shared_glorot_uniform(shape, dtype=theano.config.floatX, name='', n=None):
if isinstance(shape, int):
high = np.sqrt(6\. / shape)
else:
high = np.sqrt(6\. / (np.sum(shape[:2]) * np.prod(shape[2:])))
shape = shape if n is None else (n,) + shape
return theano.shared(np.asarray(
np.random.uniform(
low=-high,
high=high,
size=shape),
dtype=dtype), name=name)
W1 = shared_glorot_uniform( (n_in, n_hidden), name='W1' )
b1 = shared_zeros( (n_hidden,), name='b1' )
hidden_output = T.tanh(T.dot(x, W1) + b1)
W2 = shared_zeros( (n_hidden, n_out), name='W2' )
b2 = shared_zeros( (n_out,), name='b2' )
model = T.nnet.softmax(T.dot(hidden_output, W2) + b2)
params = [W1,b1,W2,b2]
在深度网络中,如果权重通过 shared_zeros 方法初始化为零,信号将无法从头到尾正确流过网络。如果权重初始化为过大的值,经过几步后,大多数激活函数将会饱和。所以,我们需要确保在传播过程中值能够传递到下一层,并且在反向传播过程中,梯度能够传递到上一层。
我们还需要打破神经元之间的对称性。如果所有神经元的权重都为零(或者它们都相等),它们将以完全相同的方式发展,模型将无法学习到很多东西。
研究员 Xavier Glorot 研究了一种算法来以最优方式初始化权重。该算法通过从均值为零的高斯或均匀分布中抽取权重,方差如下:

这是前述公式中的变量:
-
n[in]是该层在前向传播过程中接收到的输入数量 -
n[out]是该层在反向传播过程中接收到的梯度数量
在线性模型中,形状参数是一个元组,v 就是 numpy.sum(shape[:2])(在这种情况下,numpy.prod(shape[2:]) 为 1)。
均匀分布在 [-a, a] 上的方差为 a**2 / 3,那么可以通过以下方式计算边界 a:

成本可以像以前一样定义,但梯度下降需要适应处理参数列表 [W1,b1,W2,b2]:
g_params = T.grad(cost=cost, wrt=params)
训练循环需要一个更新的训练函数:
learning_rate = 0.01
updates = [
(param, param - learning_rate * gparam)
for param, gparam in zip(params, g_params)
]
train_model = theano.function(
inputs=[index],
outputs=cost,
updates=updates,
givens={
x: train_set_x[index * batch_size: (index + 1) * batch_size],
y: train_set_y[index * batch_size: (index + 1) * batch_size]
}
)
在这种情况下,学习率对整个网络是全局的,所有权重以相同的速率更新。学习率设置为 0.01,而不是 0.13。我们将在训练部分讨论超参数调优。
训练循环保持不变。完整的代码见2-multi.py文件。
在 GPU 上的执行时间为 5 分钟 55 秒,而在 CPU 上为 51 分钟 36 秒。
在 1000 次迭代后,误差降至 2%,比之前的 5% 错误率要好得多,但其中一部分可能是由于过拟合造成的。我们稍后会比较不同的模型。
卷积和最大池化层
在 MNIST 数据集上,卷积层的发明使得图像分类取得了巨大的进展:

与之前的全连接层对输入的所有值(图像的像素值)进行计算不同,2D 卷积层将仅考虑 2D 输入图像中 NxN 像素的小块、窗口或感受野来计算每个输出单元。该块的尺寸被称为卷积核尺寸,N 为卷积核大小,系数/参数即为卷积核。
在输入图像的每个位置,卷积核都会生成一个标量,所有位置的值将形成一个矩阵(2D 张量),称为 特征图。在输入图像上进行卷积操作,像滑动窗口一样,生成一个新的输出图像。卷积核的步幅定义了在图像上移动补丁/窗口的像素数:步幅为 2 时,卷积核每隔 2 个像素计算一次卷积。
例如,在一个 224 x 224 的输入图像上,我们得到如下结果:
-
一个 2x2 的卷积核,步幅为 1,将输出一个 223 x 223 的特征图。
-
一个 3x3 的卷积核,步幅为 1,将输出一个 222 x 222 的特征图。
为了保持输出特征图与输入图像相同的尺寸,有一种叫做 same 或 half 的零填充方法,可以实现以下效果:
-
在输入图像的末尾添加一行一列零,适用于步幅为 1 的 2x2 卷积核。
-
在输入图像的上下左右分别添加两行两列零,适用于步幅为 1 的 3x3 卷积核。
所以,输出的尺寸与原始尺寸相同,也就是一个 224 x 224 的特征图。
有零填充时:
-
一个 2x2 的卷积核,步幅为 2,并且有零填充,将输出一个 112 x 112 的特征图。
-
一个 3x3 的卷积核,步幅为 2,将输出一个 112 x 112 的特征图。
如果没有零填充,事情会变得更复杂:
-
一个 2x2 的卷积核,步幅为 2,将输出一个 112 x 112 的特征图。
-
一个 3x3 的卷积核,步幅为 2,将输出一个 111 x 111 的特征图。
请注意,卷积核的尺寸和步长在每个维度上可能不同。在这种情况下,我们说卷积核的宽度、卷积核的高度、步幅宽度或步幅高度。
在一个卷积层中,可能输出多个特征图,每个特征图是用不同的卷积核(和卷积核权重)计算出来的,表示一个特征。我们可以用输出、神经元、卷积核、特征、特征图、单元或输出通道来表示这些不同卷积的数量。严格来说,神经元通常指的是特征图中的一个特定位置。卷积核就是卷积核本身,其他的则是卷积操作的结果。它们的数量是相同的,这就是这些词常常用来描述相同内容的原因。我将使用通道、输出和特征这几个词。
常见的卷积运算符可以应用于多通道输入。这使得它们可以应用于三通道图像(例如 RGB 图像)或另一个卷积的输出,以便进行级联。
让我们在前面的 MLP 模型前面添加两个卷积层,卷积核大小为 5:

2D 卷积操作需要 4D 张量输入。第一维是批量大小,第二维是输入数量或输入通道数(采用“通道优先”格式),第三和第四维是特征图的两个维度(采用“通道后置”格式,通道是最后一个维度)。MNIST 灰度图像(一个通道)以一维向量存储,需要转换为 28x28 的矩阵,其中 28 是图像的高度和宽度:
layer0_input = x.reshape((batch_size, 1, 28, 28))
然后,在变换后的输入上添加一个 20 通道的第一个卷积层,得到如下结果:
from theano.tensor.nnet import conv2d
n_conv1 = 20
W1 = shared_glorot_uniform( (n_conv1, 1, 5, 5) )
conv1_out = conv2d(
input=layer0_input,
filters=W1,
filter_shape=(n_conv1, 1, 5, 5),
input_shape=(batch_size, 1, 28, 28)
)
在这种情况下,Xavier 初始化(以其发明者 Xavier Glorot 的名字命名)将输入/输出通道的数量与卷积核中的参数数量相乘,numpy.prod(shape[2:]) = 5 x 5 = 25,从而得到初始化公式中输入/输出梯度的总数。
在 28x28 的输入上使用大小为 5x5,步幅为 1 的 20 个卷积核将产生 20 个 24x24 的特征图。所以第一个卷积层的输出是(batch_size,20,24,24)。
最佳性能的网络使用最大池化层,以鼓励平移不变性并提高对噪声的稳定性。最大池化层在滑动窗口/补丁上执行最大操作,仅保留补丁中的一个值。除了提高速度性能外,它还减少了特征图的大小,总体计算复杂度和训练时间也因此降低:
from theano.tensor.signal import pool
pooled_out = pool.pool_2d(input=conv1_out, ws=(2, 2), ignore_border=True)
2x2 最大池化层的输出将是(batch_size,20,12,12)。批量大小和通道数保持不变,只有特征图的大小发生了变化。
在前一个卷积层之上添加一个 50 通道的第二个卷积层和最大池化层,得到的输出尺寸为(batch_size,50,4,4):
n_conv2 = 50
W2 = shared_glorot_uniform( (n_conv2, n_conv1, 5, 5) )
conv2_out = conv2d(
input=pooled_out,
filters=W2,
filter_shape=(n_conv2, n_conv1, 5, 5),
input_shape=(batch_size, n_conv1, 12, 12)
)
pooled2_out = pool.pool_2d(input=conv2_out, ds=(2, 2),ignore_border=True)
为了创建分类器,我们在上面连接一个具有两个全连接线性层和一个 softmax 的 MLP,如前所示:
hidden_input = pooled2_out.flatten(2)
n_hidden = 500
W3 = shared_zeros( (n_conv2 * 4 * 4, n_hidden), name='W3' )
b3 = shared_zeros( (n_hidden,), name='b3' )
hidden_output = T.tanh(T.dot(hidden_input, W3) + b3)
n_out = 10
W4 = shared_zeros( (n_hidden, n_out), name='W4' )
b4 = shared_zeros( (n_out,), name='b4' )
model = T.nnet.softmax(T.dot(hidden_output, W4) + b4)
params = [W1,W2,W3,b3,W4,b4]
这样的模型被称为卷积神经网络(CNN)。
完整的代码可以在3-cnn.py文件中找到。
训练速度大大变慢,因为参数数量又被乘以了,使用 GPU 变得更加有意义:在 GPU 上训练的总时间已增加至 1 小时 48 分钟 27 秒。若在 CPU 上训练,将需要数天。
经过几次迭代后,训练误差为零,部分原因是过拟合。接下来我们将看到如何计算测试损失和准确度,以更好地解释模型的效率。
训练
为了得到模型在训练过程中未见数据上的表现的良好度量,使用验证数据集来计算训练过程中的验证损失和准确度。
验证数据集使我们能够选择最佳模型,而测试数据集仅在最后用于获得模型的最终测试准确率/错误率。训练、测试和验证数据集是离散的数据集,没有共同的示例。验证数据集通常是测试数据集的十分之一,以尽量减少对训练过程的影响。测试数据集通常占训练数据集的 10%-20%。训练集和验证集都是训练程序的一部分,因为第一个用于学习,第二个则用于在训练时选择最佳模型,以便在未见数据上进行验证。
测试数据集完全独立于训练过程,仅用于获得经过训练和模型选择后的最终模型的准确性。
如果模型因在相同图像上训练次数过多而过拟合训练集,例如,那么验证集和测试集将不会受到这种行为的影响,并将提供模型准确性的真实估计。
通常,验证函数是编译时不更新模型梯度的,仅计算输入批次的成本和误差。
数据批次(x,y)通常在每次迭代时传输到 GPU,因为数据集通常太大,无法完全放入 GPU 的内存中。在这种情况下,我们仍然可以使用共享变量的技巧将整个验证数据集放入 GPU 的内存中,但让我们看看如果每步都必须将批次传输到 GPU 而不使用之前的技巧,我们该如何操作。我们将使用更常见的形式:
validate_model = theano.function(
inputs=[x,y],
outputs=[cost,error]
)
这需要批次输入的传输。验证不是在每次迭代时计算,而是在训练的for循环中的validation_interval次迭代时计算:
if iteration % validation_interval == 0 :
val_index = iteration // validation_interval
valid_loss[val_index], valid_error[val_index] = np.mean([
validate_model(
valid_set[0][i * batch_size: (i + 1) * batch_size],
numpy.asarray(valid_set[1][i * batch_size: (i + 1) * batch_size], dtype="int32")
)
for i in range(n_valid_batches)
], axis=0)
让我们看看简单的第一个模型:
epoch 0, minibatch 1/83, validation error 40.05 %, validation loss 2.16520105302
epoch 24, minibatch 9/83, validation error 8.16 %, validation loss 0.288349323906
epoch 36, minibatch 13/83, validation error 7.96 %, validation loss 0.278418215923
epoch 48, minibatch 17/83, validation error 7.73 %, validation loss 0.272948684171
epoch 60, minibatch 21/83, validation error 7.65 %, validation loss 0.269203903154
epoch 72, minibatch 25/83, validation error 7.59 %, validation loss 0.26624627877
epoch 84, minibatch 29/83, validation error 7.56 %, validation loss 0.264540277421
...
epoch 975, minibatch 76/83, validation error 7.10 %, validation loss 0.258190142922
epoch 987, minibatch 80/83, validation error 7.09 %, validation loss 0.258411859162
在完整的训练程序中,验证间隔与总的训练周期数相对应,且周期的平均验证得分更有意义。
为了更好地评估训练效果,让我们绘制训练损失和验证损失的曲线。为了显示早期迭代中的下降情况,我会在 100 次迭代时停止绘图。如果我在图中使用 1,000 次迭代,就看不到早期迭代的情况:

训练损失看起来像一条宽带,因为它在不同的值之间波动。每个值对应一个批次。该批次可能太小,无法提供稳定的损失值。通过整个周期的训练损失平均值,将提供一个更稳定的值,与验证损失进行比较,展示是否发生过拟合。
还需要注意,损失曲线提供了网络收敛情况的信息,但并不提供任何关于错误的有价值信息。因此,绘制训练误差和验证误差也非常重要。
对于第二个模型:
epoch 0, minibatch 1/83, validation error 41.25 %, validation loss 2.35665753484
epoch 24, minibatch 9/83, validation error 10.20 %, validation loss 0.438846310601
epoch 36, minibatch 13/83, validation error 9.40 %, validation loss 0.399769391865
epoch 48, minibatch 17/83, validation error 8.85 %, validation loss 0.379035864025
epoch 60, minibatch 21/83, validation error 8.57 %, validation loss 0.365624915808
epoch 72, minibatch 25/83, validation error 8.31 %, validation loss 0.355733696371
epoch 84, minibatch 29/83, validation error 8.25 %, validation loss 0.348027150147
epoch 96, minibatch 33/83, validation error 8.01 %, validation loss 0.34150374867
epoch 108, minibatch 37/83, validation error 7.91 %, validation loss 0.335878048092
...
epoch 975, minibatch 76/83, validation error 2.97 %, validation loss 0.167824191041
epoch 987, minibatch 80/83, validation error 2.96 %, validation loss 0.167092795949
再次,训练曲线提供了更好的洞察:

对于第三个模型:
epoch 0, minibatch 1/83, validation error 53.81 %, validation loss 2.29528842866
epoch 24, minibatch 9/83, validation error 1.55 %, validation loss 0.048202780541
epoch 36, minibatch 13/83, validation error 1.31 %, validation loss 0.0445762014715
epoch 48, minibatch 17/83, validation error 1.29 %, validation loss 0.0432346871821
epoch 60, minibatch 21/83, validation error 1.25 %, validation loss 0.0425786205451
epoch 72, minibatch 25/83, validation error 1.20 %, validation loss 0.0413943211024
epoch 84, minibatch 29/83, validation error 1.20 %, validation loss 0.0416557886347
epoch 96, minibatch 33/83, validation error 1.19 %, validation loss 0.0414686980075
...
epoch 975, minibatch 76/83, validation error 1.08 %, validation loss 0.0477593478863
epoch 987, minibatch 80/83, validation error 1.08 %, validation loss 0.0478142946085
请参阅下图:

这里我们看到训练和验证的损失差异,可能是由于稍微的过拟合训练数据,或者训练和测试数据集之间的差异。
过拟合的主要原因如下:
-
数据集过小:收集更多的数据。
-
学习率过高:网络在早期的样本上学习得太快。
-
缺乏正则化:添加更多的 dropout(参见下一节),或在损失函数中对权重的范数施加惩罚。
-
模型过小:增加不同层中滤波器/单元的数量。
验证损失和误差比训练损失和误差更能准确估计模型的效果,因为训练损失和误差噪声较大,并且在训练过程中,它们还用来决定哪个模型参数是最优的:
-
简单模型:在第 518 个周期时,损失为 6.96%。
-
MLP 模型:在第 987 个周期时,损失为 2.96%。
-
CNN 模型:在第 722 个周期时,损失为 1.06%。
这些结果也表明,模型在进一步训练后可能不会有太大改进。
这是三种模型验证损失的比较:

注意,MLP 模型仍在改进中,训练尚未结束,而 CNN 和简单网络已经收敛。
使用已选择的模型,您可以轻松计算测试数据集上的测试损失和误差,从而最终确定模型。
机器学习的最后一个重要概念是超参数调优。超参数定义了在训练过程中未学习到的模型参数。以下是一些示例:
learning rate
number of hidden neurons
batch size
对于学习率,下降过慢可能会阻止找到更全局的最小值,而下降过快则会破坏最终的收敛。找到最佳的初始学习率至关重要。然后,通常会在多次迭代后降低学习率,以便更精确地微调模型。
超参数选择要求我们为不同的超参数值运行多次前面的实验;测试所有超参数的组合可以通过简单的网格搜索来实现。
这里有一个供读者练习的题目:
-
使用不同的超参数训练模型,并绘制训练损失曲线,以观察超参数如何影响最终损失。
-
在模型训练完成后,您可以可视化第一层神经元的内容,以查看神经元从输入图像中提取的特征。为此任务,编写一个特定的可视化函数:
visualize_layer1 = theano.function( inputs=[x,y], outputs=conv1_out )
Dropout
Dropout 是一种广泛使用的技术,用于提高神经网络的收敛性和鲁棒性,并防止神经网络过拟合。它通过将一些随机值设置为零,应用于我们希望其作用的层。它在每个周期引入一些数据的随机性。
通常,dropout 用于全连接层之前,而在卷积层中使用得比较少。我们在两个全连接层之前添加以下代码:
dropout = 0.5
if dropout > 0 :
mask = srng.binomial(n=1, p=1-dropout, size=hidden_input.shape)
# The cast is important because
# int * float32 = float64 which make execution slower
hidden_input = hidden_input * T.cast(mask, theano.config.floatX)
完整的脚本位于5-cnn-with-dropout.py。经过 1,000 次迭代后,带有 dropout 的 CNN 的验证误差降至 1.08%,而不带 dropout 的 CNN 的验证误差则停留在 1.22%。
想要深入了解 dropout 的读者,应该看看 maxout 单元。它们与 dropout 一起工作,并替换 tanh 非线性函数,以获得更好的结果。由于 dropout 执行了一种模型平均,maxout 单元试图找到问题的最佳非线性函数。
推理
推理是使用模型进行预测的过程。
对于推理,权重参数不需要更新,因此推理函数比训练函数更为简洁:
infer_model = theano.function(
inputs=[x],
outputs=[y_pred]
)
优化和其他更新规则
学习率是一个非常重要的参数,必须正确设置。学习率过低会使得学习变得困难,且训练速度较慢;而学习率过高则会增加对异常值的敏感性,增加数据中的噪声,训练过快而无法进行有效的泛化,且可能陷入局部最小值:

当训练损失在一个或几个迭代后不再改进时,可以通过一个因子降低学习率:

它有助于网络学习数据中的细粒度差异,正如在训练残差网络时所展示的那样(第七章,使用残差网络进行图像分类):

为了检查训练过程,通常会打印参数的范数、梯度和更新,以及 NaN 值。
本章中看到的更新规则是最简单的更新形式,称为随机梯度下降法(SGD)。为了避免饱和和 NaN 值,通常会将范数裁剪。传递给theano函数的更新列表如下:
def clip_norms(gs, c):
norm = T.sqrt(sum([T.sum(g**2) for g in gs]))
return [ T.switch(T.ge(norm, c), g*c/norm, g) for g in gs]
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p,g in zip(params,grads):
updated_p = p - learning_rate * g
updates.append((p, updated_p))
一些非常简单的变体已被尝试用来改进下降,并在许多深度学习库中提出。我们来看一下它们在 Theano 中的实现。
动量
对于每个参数,都会从累积的梯度中计算动量(v,作为速度),并使用时间衰减。之前的动量值会乘以一个介于 0.5 和 0.9 之间的衰减参数(需交叉验证),然后与当前的梯度相加,得到新的动量值。
梯度的动量在更新中起到了惯性力矩的作用,从而加速学习。其思路是,连续梯度中的振荡会在动量中被抵消,使得参数朝着解决方案的更直接路径移动:

介于 0.5 和 0.9 之间的衰减参数是一个超参数,通常被称为动量,这里存在一种语言上的滥用:
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p,g in zip(params,grads):
m = theano.shared(p.get_value() * 0.)
v = (momentum * m) - (learning_rate * g)
updates.append((m, v))
updates.append((p, p + v))
Nesterov 加速梯度
不是将v加到参数上,而是将动量v - learning_rate g的未来值直接加到参数上,以便在下一次迭代中直接计算下一位置的梯度:
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p, g in zip(params, grads):
m = theano.shared(p.get_value() * 0.)
v = (momentum * m) - (learning_rate * g)
updates.append((m,v))
updates.append((p, p + momentum * v - learning_rate * g))
Adagrad
这个更新规则,以及接下来的规则,包含了逐参数(对每个参数不同)地调整学习率。梯度的元素级平方和被积累到每个参数的共享变量中,以便按元素方式衰减学习率:
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p,g in zip(params,grads):
acc = theano.shared(p.get_value() * 0.)
acc_t = acc + g ** 2
updates.append((acc, acc_t))
p_t = p - (learning_rate / T.sqrt(acc_t + 1e-6)) * g
updates.append((p, p_t))
Adagrad是一种激进的方法,接下来的两个规则,AdaDelta和RMSProp,尝试减少它的激进性。
AdaDelta
为每个参数创建两个累加器,用来积累平方梯度和移动平均中的更新,由衰减rho来参数化:
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p,g in zip(params,grads):
acc = theano.shared(p.get_value() * 0.)
acc_delta = theano.shared(p.get_value() * 0.)
acc_new = rho * acc + (1 - rho) * g ** 2
updates.append((acc,acc_new))
update = g * T.sqrt(acc_delta + 1e-6) / T.sqrt(acc_new + 1e-6)
updates.append((p, p - learning_rate * update))
updates.append((acc_delta, rho * acc_delta + (1 - rho) * update ** 2))
RMSProp
这个更新规则在许多情况下非常有效。它是Adagrad更新规则的改进,使用移动平均(由rho参数化)来获得较少的激进衰减:
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
for p,g in zip(params,grads):
acc = theano.shared(p.get_value() * 0.)
acc_new = rho * acc + (1 - rho) * g ** 2
updates.append((acc, acc_new))
updated_p = p - learning_rate * (g / T.sqrt(acc_new + 1e-6))
updates.append((p, updated_p))
Adam
这是带动量的RMSProp,是最好的学习规则选择之一。时间步长由共享变量t跟踪。计算了两个移动平均,一个用于过去的平方梯度,另一个用于过去的梯度:
b1=0.9, b2=0.999, l=1-1e-8
updates = []
grads = T.grad(cost, params)
grads = clip_norms(grads, 50)
t = theano.shared(floatX(1.))
b1_t = b1 * l **(t-1)
for p, g in zip(params, grads):
m = theano.shared(p.get_value() * 0.)
v = theano.shared(p.get_value() * 0.)
m_t = b1_t * m + (1 - b1_t) * g
v_t = b2 * v + (1 - b2) * g**2
updates.append((m, m_t))
updates.append((v, v_t))
updates.append((p, p - (learning_rate * m_t / (1 - b1**t)) / (T.sqrt(v_t / (1 - b2**t)) + 1e-6)) )
updates.append((t, t + 1.))
总结一下更新规则,许多最近的研究论文仍然更倾向于使用简单的 SGD 规则,并通过正确的学习率来调整网络架构和层的初始化。对于更复杂的网络,或者当数据稀疏时,适应性学习率方法更好,可以避免你在寻找合适学习率时的痛苦。
相关文章
你可以参考以下文档,获取更多关于本章涵盖主题的见解:
-
Deeplearning.net Theano 教程:单层(
deeplearning.net/tutorial/logreg.html),MLP(deeplearning.net/tutorial/mlp.html),卷积(deeplearning.net/tutorial/lenet.html) -
所有损失函数:用于分类、回归和联合嵌入(
christopher5106.github.io/deep/learning/2016/09/16/about-loss-functions-multinomial-logistic-logarithm-cross-entropy-square-errors-euclidian-absolute-frobenius-hinge.html) -
最后一个例子对应于 Yann Lecun 的五层网络,如在《基于梯度的学习应用于文档识别》一文中所示(
yann.lecun.com/exdb/publis/pdf/lecun-98.pdf) -
理解训练深度前馈神经网络的难度,Xavier Glorot, Yoshua Bengio,2010
-
Maxout 网络:Ian J. Goodfellow,David Warde-Farley,Mehdi Mirza,Aaron Courville,Yoshua Bengio 2013
-
CS231n 视觉识别卷积神经网络,
cs231n.github.io/neural-networks-3/ -
是的,你应该理解反向传播,Andrej Karpathy,2016,
medium.com/@karpathy/ -
追求简单:全卷积网络,Jost Tobias Springenberg,Alexey Dosovitskiy,Thomas Brox,Martin Riedmiller,2014
-
分数最大池化,Benjamin Graham,2014
-
批量归一化:通过减少内部协变量偏移加速深度网络训练,Sergey Ioffe,Christian Szegedy,2015
-
可视化与理解卷积网络,Matthew D Zeiler,Rob Fergus,2013
-
深入卷积,Christian Szegedy,Wei Liu,Yangqing Jia,Pierre Sermanet,Scott Reed,Dragomir Anguelov,Dumitru Erhan,Vincent Vanhoucke,Andrew Rabinovich,2014
摘要
分类是机器学习中一个非常广泛的话题。它包括预测一个类别或一个类,如我们在手写数字示例中所展示的那样。在第七章,使用残差网络分类图像,我们将看到如何分类更广泛的自然图像和物体。
分类可以应用于不同的问题,交叉熵/负对数似然是通过梯度下降解决这些问题的常见损失函数。对于回归问题(均方误差损失)或无监督联合学习(铰链损失)等问题,还有许多其他损失函数。
在本章中,我们使用了一个非常简单的更新规则作为梯度下降,命名为随机梯度下降,并介绍了其他一些梯度下降变体(Momentum、Nesterov、RMSprop、ADAM、ADAGRAD、ADADELTA)。有一些关于二阶优化的研究,例如 Hessian Free 或 K-FAC,它们在深度或循环网络中提供了更好的结果,但仍然复杂且代价高昂,直到现在仍未得到广泛采用。研究人员一直在寻找不需要这些优化技术的、更好的新架构。
在训练网络时,我强烈建议你使用以下两个 Linux 命令:
-
Screen:分离你的终端,运行服务器上的脚本,并稍后重新连接,因为训练通常需要几天时间。
-
Tee:将正在运行程序的输出传递给它,以便在继续在终端中显示输出的同时将显示的结果保存到文件中。这将减轻你的代码中日志功能和框架的负担。
第三章。将单词编码为向量
在上一章中,神经网络的输入是图像,也就是连续数值的向量,自然语言是神经网络的语言。但是对于许多其他机器学习领域,输入可能是类别型的和离散的。
在本章中,我们将介绍一种叫做嵌入的技术,它学会将离散的输入信号转换为向量。这种输入表示是与神经网络其他处理兼容的重要第一步。
这些嵌入技术将通过一个自然语言文本的示例来说明,这些文本由属于有限词汇表的单词组成。
我们将介绍嵌入的不同方面:
-
嵌入的原理
-
不同类型的单词嵌入
-
独热编码与索引编码
-
构建一个网络将文本转换为向量
-
训练并发现嵌入空间的特性
-
保存和加载模型的参数
-
可视化的降维
-
评估嵌入的质量
-
嵌入空间的应用
-
权重绑定
编码与嵌入
每个单词可以通过词汇表中的索引来表示:

编码单词是将每个单词表示为一个向量的过程。编码单词的最简单方法称为独热编码或 1-of-K 向量表示法。在这种方法中,每个单词都表示为一个
向量,该向量的所有元素都是 0,只有在该单词在排序词汇表中的索引位置上为 1。在这种表示法中,|V|表示词汇表的大小。对于词汇表{国王, 女王, 男人, 女人, 孩子},在这种类型的编码下,单词女王的编码示例如下:

在独热向量表示方法中,每个单词与其他单词的距离相等。然而,它无法保留它们之间的任何关系,并且会导致数据稀疏性。使用单词嵌入可以克服这些缺点。
单词嵌入是一种分布式语义方法,它将单词表示为实数向量。这样的表示具有有用的聚类属性,因为它将语义和句法相似的单词聚集在一起。
例如,单词海洋世界和海豚将在创建的空间中非常接近。这一步的主要目的是将每个单词映射到一个连续的、低维的实值向量,并将其用作模型的输入,例如循环神经网络(RNN)、卷积神经网络(CNN)等:

这种表示是稠密的。我们期望同义词和可互换的单词在该空间中接近。
本章中,我们将介绍一种非常流行的词嵌入模型——Word2Vec,它最初由 Mikolov 等人于 2013 年开发。Word2Vec 有两种不同的模型:连续词袋模型(CBOW)和跳字模型(Skip-gram)。
在 CBOW 方法中,目标是给定上下文来预测一个单词。而跳字模型则是根据单个单词预测周围的上下文(见下图):

对于本章,我们将重点介绍 CBOW 模型。我们将从展示数据集开始,然后解释该方法背后的思想。之后,我们将使用 Theano 展示它的简单实现。最后,我们将提到词嵌入的一些应用。
数据集
在解释模型部分之前,让我们先通过创建词汇表来处理文本语料库,并将文本与词汇表整合,以便每个单词都可以表示为一个整数。作为数据集,可以使用任何文本语料库,如维基百科或网页文章,或来自社交网络(如 Twitter)的帖子。常用的数据集包括 PTB、text8、BBC、IMDB 和 WMT 数据集。
本章中,我们使用text8语料库。它由维基百科转储中前 1 亿个字符的预处理版本构成。我们先来下载该语料库:
wget http://mattmahoney.net/dc/text8.zip -O /sharedfiles/text8.gz
gzip -d /sharedfiles/text8.gz -f
现在,我们构建词汇表,并用UNKNOWN替换稀有词汇。让我们先将数据读取为一个字符串列表:
-
将数据读取为字符串列表:
words = [] with open('data/text8') as fin: for line in fin: words += [w for w in line.strip().lower().split()] data_size = len(words) print('Data size:', data_size)从字符串列表中,我们现在可以构建字典。我们首先在
word_freq字典中统计单词的频率。接着,我们用符号替换那些在语料库中出现次数少于max_df的稀有单词。 -
构建字典并用
UNK符号替换稀有单词:unkown_token = '<UNK>' pad_token = '<PAD>' # for padding the context max_df = 5 # maximum number of freq word_freq = [[unkown_token, -1], [pad_token, 0]] word_freq.extend(Counter(words).most_common()) word_freq = OrderedDict(word_freq) word2idx = {unkown_token: 0, pad_token: 1} idx2word = {0: unkown_token, 1: pad_token} idx = 2 for w in word_freq: f = word_freq[w] if f >= max_df: word2idx[w] = idx idx2word[idx] = w idx += 1 else: word2idx[w] = 0 # map the rare word into the unkwon token word_freq[unkown_token] += 1 # increment the number of unknown tokens data = [word2idx[w] for w in words] del words # for reduce mem use vocabulary_size = len(word_freq) most_common_words = list(word_freq.items())[:5] print('Most common words (+UNK):', most_common_words) print('Sample data:', data[:10], [idx2word[i] for i in data[:10]]) *Data size: 17005207* *Most common words (+UNK): [('<UNK>', 182564), ('the', 1061396), ('of', 593677), ('and', 416629), ('one', 411764)]* *Sample data: [5239, 3084, 12, 6, 195, 2, 3137, 46, 59, 156] ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against']* -
现在,让我们定义创建数据集的函数(即上下文和目标):
def get_sample(data, data_size, word_idx, pad_token, c = 1): idx = max(0, word_idx - c) context = data[idx:word_idx] if word_idx + 1 < data_size: context += data[word_idx + 1 : min(data_size, word_idx + c + 1)] target = data[word_idx] context = [w for w in context if w != target] if len(context) > 0: return target, context + (2 * c - len(context)) * [pad_token] return None, None def get_data_set(data, data_size, pad_token, c=1): contexts = [] targets = [] for i in xrange(data_size): target, context = get_sample(data, data_size, i, pad_token, c) if not target is None: contexts.append(context) targets.append(target) return np.array(contexts, dtype='int32'), np.array(targets, dtype='int32')
连续词袋模型
用于预测给定上下文中单词的神经网络设计如下图所示:

输入层接收上下文,而输出层预测目标单词。我们将用于 CBOW 模型的模型有三层:输入层、隐藏层(也称为投影层或嵌入层)和输出层。在我们的设置中,词汇表大小是 V,隐藏层大小是 N。相邻的单元是完全连接的。
输入和输出可以通过索引(一个整数,0 维)或一-hot-编码向量(1 维)表示。与一-hot-编码向量v相乘仅仅是取嵌入矩阵的第 j 行:

由于索引表示在内存使用上比 one-hot 编码表示更高效,而且 Theano 支持索引符号变量,因此尽可能采用索引表示是更可取的。
因此,输入(上下文)将是二维的,由一个矩阵表示,具有两个维度:批量大小和上下文长度。输出(目标)是一维的,由一个向量表示,具有一个维度:批量大小。
让我们定义 CBOW 模型:
import theano
import theano.tensor as T
import numpy as np
import math
context = T.imatrix(name='context')
target = T.ivector('target')
上下文和目标变量是该模型的已知参数。CBOW 模型的未知参数是输入层和隐藏层之间的连接矩阵!连续词袋模型,以及隐藏层和输出层之间的连接矩阵!连续词袋模型:
vocab_size = len(idx2word)
emb_size = 128
W_in_values = np.asarray(np.random.uniform(-1.0, 1.0,
(vocab_size, emb_size)),
dtype=theano.config.floatX)
W_out_values = np.asarray(np.random.normal(
scale=1.0 / math.sqrt(emb_size),
size=(emb_size, vocab_size)),
dtype=theano.config.floatX)
W_in = theano.shared(value=W_in_values,
name='W_in',
borrow=True)
W_out = theano.shared(value=W_out_values,
name='W_out',
borrow=True)
params = [W_in, W_out]
的每一行是关联单词i在输入层的 N 维向量表示,N是隐藏层的大小。给定一个上下文,在计算隐藏层输出时,CBOW 模型会对输入上下文词的向量进行平均,然后使用input -> hidden权重矩阵与平均向量的乘积作为输出:

这里,C是上下文中的单词数,w1, w2, w3,..., wc是上下文中的单词,
是单词
的输入向量。输出层的激活函数是 softmax 层。方程 2 和 3 展示了我们如何计算输出层:

这里,
是矩阵
的第 j 列,V是词汇表大小。在我们的设置中,词汇表大小是vocab_size,隐藏层大小是emb_size。损失函数如下:

(4)
现在,让我们在 Theano 中翻译方程 1、2、3 和 4。
要计算隐藏层(投影层)输出:input -> hidden (eq. 1)
h = T.mean(W_in[context], axis=1)
For the hidden -> output layer (eq. 2)
uj = T.dot(h, W_out)
softmax 激活(eq. 3):
p_target_given_contex = T.nnet.softmax(uj).dimshuffle(1, 0)
损失函数(eq. 4):
loss = -T.mean(T.log(p_target_given_contex)[T.arange(target.shape[0]), target])
使用 SGD 更新模型的参数:
g_params = T.grad(cost=loss, wrt=params)
updates = [
(param, param - learning_rate * gparam)
for param, gparam in zip(params, g_params)
]
最后,我们需要定义训练和评估函数。
让我们将数据集设为共享,以便将其传递到 GPU。为简便起见,我们假设有一个名为get_data_set的函数,它返回目标集及其周围上下文:
contexts, targets = get_data_set(data, data_size, word2idx[pad_token], c=2)
contexts = theano.shared(contexts)
targets = theano.shared(targets)
index = T.lscalar('index')
train_model = theano.function(
inputs=[index],
outputs=[loss],
updates=updates,
givens={
context: contexts[index * batch_size: (index + 1) * batch_size],
target: targets[index * batch_size: (index + 1) * batch_size]
}
)
train_model的输入变量是批次的索引,因为整个数据集已经通过共享变量一次性传输到 GPU。
在训练过程中进行验证时,我们通过计算小批量示例与所有嵌入的余弦相似度来评估模型。
让我们使用一个theano变量来放置验证模型的输入:
valid_samples = T.ivector('valid_samples')
验证输入的标准化词嵌入:
embeddings = params[0]
norm = T.sqrt(T.sum(T.sqr(embeddings), axis=1, keepdims=True))
normalized_embeddings = W_in / norm
valid_embeddings = normalized_embeddings[valid_samples]
相似度由余弦相似度函数给出:
similarity = theano.function([valid_samples], T.dot(valid_embeddings, normalized_embeddings.T))
训练模型
现在我们可以开始训练模型了。在这个例子中,我们选择使用 SGD 进行训练,批大小为 64,训练 100 个周期。为了验证模型,我们随机选择了 16 个词,并使用相似度度量作为评估指标:
-
让我们开始训练:
valid_size = 16 # Random set of words to evaluate similarity on. valid_window = 100 # Only pick dev samples in the head of the distribution. valid_examples = np.array(np.random.choice(valid_window, valid_size, replace=False), dtype='int32') n_epochs = 100 n_train_batches = data_size // batch_size n_iters = n_epochs * n_train_batches train_loss = np.zeros(n_iters) average_loss = 0 for epoch in range(n_epochs): for minibatch_index in range(n_train_batches): iteration = minibatch_index + n_train_batches * epoch loss = train_model(minibatch_index) train_loss[iteration] = loss average_loss += loss if iteration % 2000 == 0: if iteration > 0: average_loss /= 2000 # The average loss is an estimate of the loss over the last 2000 batches. print("Average loss at step ", iteration, ": ", average_loss) average_loss = 0 # Note that this is expensive (~20% slowdown if computed every 500 steps) if iteration % 10000 == 0: sim = similarity(valid_examples) for i in xrange(valid_size): valid_word = idx2word[valid_examples[i]] top_k = 8 # number of nearest neighbors nearest = (-sim[i, :]).argsort()[1:top_k+1] log_str = "Nearest to %s:" % valid_word for k in xrange(top_k): close_word = idx2word[nearest[k]] log_str = "%s %s," % (log_str, close_word) print(log_str) -
最后,让我们创建两个通用函数,帮助我们将任何模型参数保存在可重用的
utils.py工具文件中:def save_params(outfile, params): l = [] for param in params: l = l + [ param.get_value() ] numpy.savez(outfile, *l) print("Saved model parameters to {}.npz".format(outfile)) def load_params(path, params): npzfile = numpy.load(path+".npz") for i, param in enumerate(params): param.set_value( npzfile["arr_" +str(i)] ) print("Loaded model parameters from {}.npz".format(path)) -
在 GPU 上运行时,前面的代码会打印以下结果:
*Using gpu device 1: Tesla K80 (CNMeM is enabled with initial size: 80.0% of memory, cuDNN 5105)* *Data size 17005207* *Most common words (+UNK) [('<UNK>', 182565), ('<PAD>', 0), ('the', 1061396), ('of', 593677), ('and', 416629)]* *Sample data [5240, 3085, 13, 7, 196, 3, 3138, 47, 60, 157] ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against']* *Average loss at step 0 : 11.2959747314* *Average loss at step 2000 : 8.81626828802* *Average loss at step 4000 : 7.63789177912* *Average loss at step 6000 : 7.40699760973* *Average loss at step 8000 : 7.20080085599* *Average loss at step 10000 : 6.85602856147* *Average loss at step 12000 : 6.88123817992* *Average loss at step 14000 : 6.96217652643* *Average loss at step 16000 : 6.53794862854* *...* *Average loss at step 26552000 : 4.52319500107* *Average loss at step 26554000 : 4.55709513521* *Average loss at step 26556000 : 4.62755958384* *Average loss at step 26558000 : 4.6266620369* *Average loss at step 26560000 : 4.82731778347* *Nearest to system: systems, network, device, unit, controller, schemes, vessel, scheme,* *Nearest to up: off, out, alight, forth, upwards, down, ordered, ups,* *Nearest to first: earliest, last, second, next, oldest, fourth, third, newest,* *Nearest to nine: apq, nineteenth, poz, jyutping, afd, apod, eurocents, zoolander,* *Nearest to between: across, within, involving, with, among, concerning, through, from,* *Nearest to state: states, provincial, government, nation, gaeltachta, reservation, infirmity, slates,* *Nearest to are: were, is, aren, was, include, have, weren, contain,* *Nearest to may: might, should, must, can, could, would, will, cannot,* *Nearest to zero: hundred, pounders, hadza, cest, bureaus, eight, rang, osr,* *Nearest to that: which, where, aurea, kessai, however, unless, but, although,* *Nearest to can: could, must, cannot, should, may, will, might, would,* *Nearest to s: his, whose, its, castletown, cide, codepoint, onizuka, brooklands,* *Nearest to will: would, must, should, could, can, might, shall, may,* *Nearest to their: its, his, your, our, her, my, the, whose,* *Nearest to but: however, though, although, which, while, whereas, moreover, unfortunately,* *Nearest to not: never, indeed, rarely, seldom, almost, hardly, unable, gallaecia,* *Saved model parameters to model.npz*
让我们注意到:
-
稀有词只更新少数几次,而频繁出现的词在输入和上下文窗口中更常出现。对频繁词进行下采样可以缓解这个问题。
-
所有权重都会在输出嵌入中更新,只有其中一部分,即对应于上下文窗口中的词汇,才会被正向更新。负采样有助于在更新中重新平衡正负样本。
可视化学习到的嵌入
我们将嵌入可视化为二维图形,以便了解它们如何捕捉相似性和语义。为此,我们需要将高维的嵌入降到二维,而不改变嵌入的结构。
降维称为流形学习,存在许多不同的技术,其中一些是线性的,如主成分分析(PCA)、独立成分分析(ICA)、线性判别分析(LDA)和潜在语义分析 / 索引(LSA / LSI),一些是非线性的,如Isomap、局部线性嵌入(LLE)、海森矩阵特征映射、谱嵌入、局部切空间嵌入、多维尺度法(MDS)和t-分布随机邻域嵌入(t-SNE)。
为了显示词嵌入,我们使用 t-SNE,这是一种适应高维数据的优秀技术,用于揭示局部结构和簇,而不会将点挤在一起:
-
可视化嵌入:
def plot_with_labels(low_dim_embs, labels, filename='tsne.png'): assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings" plt.figure(figsize=(18, 18)) #in inches for i, label in enumerate(labels): x, y = low_dim_embs[i,:] plt.scatter(x, y) plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom') plt.savefig(filename) from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000) plot_only = 500 low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only,:]) labels = [idx2word[i] for i in xrange(plot_only)] plot_with_labels(low_dim_embs, labels)绘制的地图显示了具有相似嵌入的词语彼此靠近:
![可视化学习到的嵌入]()
评估嵌入 – 类比推理
类比推理是一种简单有效的评估嵌入的方法,通过预测语法和语义关系,形式为a 对 b 就像 c 对 _?,记作 a : b → c : ?。任务是识别被省略的第四个单词,只有完全匹配的单词才被认为是正确的。
例如,单词女人是问题国王对女王,如同男人对?的最佳答案。假设
是单词
的表示向量,并标准化为单位范数。那么,我们可以通过找到与表示向量最接近的单词
来回答问题a : b → c : ?。

根据余弦相似度:

现在让我们使用 Theano 实现类比预测函数。首先,我们需要定义函数的输入。类比函数接收三个输入,即a、b和c的单词索引:
analogy_a = T.ivector('analogy_a')
analogy_b = T.ivector('analogy_b')
analogy_c = T.ivector('analogy_c')
然后,我们需要将每个输入映射到单词嵌入向量。a_emb、b_emb、c_emb的每一行都是一个单词的嵌入向量:
a_emb = embeddings[analogy_a] # a's embs
b_emb = embeddings[analogy_b] # b's embs
c_emb = embeddings[analogy_c] # c's embs
现在我们可以计算每个目标和词汇对之间的余弦距离。我们预期d在单位超球上的嵌入向量接近:c_emb + (b_emb - a_emb),其形状为[bsz, emb_size]。dist的形状为[bsz, vocab_size]。
dist = T.dot(target, embeddings.T)
在这个例子中,我们认为预测函数取前四个单词。因此,我们可以在 Theano 中定义函数如下:
pred_idx = T.argsort(dist, axis=1)[:, -4:]
prediction = theano.function([analogy_a, analogy_b, analogy_c], pred_idx)
要运行上述函数,我们需要加载评估数据,在本例中是由 Google 定义的类比问题集。每个问题包含四个用空格分隔的单词。第一个问题可以解释为雅典对希腊的关系,如同巴格达对 _?,正确答案应为伊拉克:
Athens Greece Baghdad Iraq
Athens Greece Bangkok Thailand
Athens Greece Beijing China
让我们使用以下代码中定义的read_analogies函数加载类比问题:
def read_analogies(fname, word2idx):
"""Reads through the analogy question file.
Returns:
questions: a [n, 4] numpy array containing the analogy question's
word ids.
questions_skipped: questions skipped due to unknown words.
"""
questions = []
questions_skipped = 0
with open(fname, "r") as analogy_f:
for line in analogy_f:
if line.startswith(":"): # Skip comments.
continue
words = line.strip().lower().split(" ")
ids = [word2idx.get(w.strip()) for w in words]
if None in ids or len(ids) != 4:
questions_skipped += 1
else:
questions.append(np.array(ids))
print("Eval analogy file: ", fname)
print("Questions: ", len(questions))
print("Skipped: ", questions_skipped)
return np.array(questions, dtype=np.int32)
现在,我们可以运行评估模型:
"""Evaluate analogy questions and reports accuracy."""
# How many questions we get right at precision@1.
correct = 0
analogy_data = read_analogies(args.eval_data, word2idx)
analogy_questions = analogy_data[:, :3]
answers = analogy_data[:, 3]
del analogy_data
total = analogy_questions.shape[0]
start = 0
while start < total:
limit = start + 200
sub_questions = analogy_questions[start:limit, :]
sub_answers = answers[start:limit]
idx = prediction(sub_questions[:,0], sub_questions[:,1], sub_questions[:,2])
start = limit
for question in xrange(sub_questions.shape[0]):
for j in xrange(4):
if idx[question, j] == sub_answers[question]:
# Bingo! We predicted correctly. E.g., [italy, rome, france, paris].
correct += 1
break
elif idx[question, j] in sub_questions[question]:
# We need to skip words already in the question.
continue
else:
# The correct label is not the precision@1
break
print()
print("Eval %4d/%d accuracy = %4.1f%%" % (correct, total,
correct * 100.0 / total))
这导致了:
*Eval analogy file: questions-words.txt*
*Questions: 17827*
*Skipped: 1717*
*Eval 831/17827 accuracy = 4.7%*
评估嵌入 - 定量分析
仅几个单词可能足以表明嵌入的定量分析也是可能的。
一些单词相似度基准提出了概念之间基于人类的距离:Simlex999(Hill 等,2016)、Verb-143(Baker 等,2014)、MEN(Bruni 等,2014)、RareWord(Luong 等,2013)和 MTurk-771(Halawi 等,2012)。
我们的嵌入之间的相似度距离可以与这些人类距离进行比较,使用 Spearman 秩相关系数来定量评估所学习嵌入的质量。
单词嵌入的应用
单词嵌入捕捉单词的含义。它们将离散输入转换为神经网络可处理的输入。
嵌入是与语言相关的许多应用的起点:
-
生成文本,正如我们将在下一章看到的那样
-
翻译系统,其中输入和目标句子是单词序列,且其嵌入可以通过端到端的神经网络处理 (第八章, 使用编码解码网络进行翻译和解释)
-
情感分析 (第五章, 使用双向 LSTM 分析情感)
-
计算机视觉中的零-shot 学习;语言中的结构使我们能够找到没有训练图像的类别
-
图像标注/说明
-
神经精神病学,其中神经网络可以 100%准确预测某些人类精神障碍
-
聊天机器人,或回答用户问题 (第九章, 使用注意力机制选择相关输入或记忆)
与单词一样,语义嵌入的原理可以用于任何具有类别变量(图像、声音、电影等类别)的任务,其中通过类别变量激活学习到的嵌入可以作为输入传递到神经网络,用于进一步的分类挑战。
由于语言塑造了我们的思维,词嵌入有助于构建或提高基于神经网络的系统性能。
权重绑定
使用了两个权重矩阵,
和
,分别用于输入或输出。虽然
的所有权重在每次反向传播迭代时都会更新,但
仅在与当前训练输入词对应的列上更新。
权重绑定 (WT) 由仅使用一个矩阵 W 来进行输入和输出嵌入组成。Theano 然后计算相对于这些新权重的新导数,并且 W 中的所有权重在每次迭代时都会更新。参数减少有助于减少过拟合。
对于 Word2Vec 模型,这种技术并没有给出更好的结果,原因很简单:在 Word2Vec 模型中,找到输入单词在上下文中出现的概率是:

它应该尽量接近零,但不能为零,除非 W = 0。
但在其他应用中,例如在神经网络语言模型(NNLM)中的第四章,“用递归神经网络生成文本”,以及神经机器翻译(NMT)中的第八章,“用编码解码网络进行翻译和解释”,它可以显示[使用输出嵌入来改进语言模型]:
-
输入嵌入通常比输出嵌入差
-
WT 解决了这个问题
-
通过 WT 学习的常见嵌入在质量上接近于没有 WT 的输出嵌入
-
在输出嵌入之前插入一个正则化的投影矩阵 P,帮助网络使用相同的嵌入,并且在 WT 下导致更好的结果
进一步阅读
请参阅以下文章:
-
在向量空间中高效估计单词表示,Tomas Mikolov,Kai Chen,Greg Corrado,Jeffrey Dean,2013
-
基于因子的组合嵌入模型,Mo Yu,2014
-
字符级卷积网络用于文本分类,Xiang Zhang,Junbo Zhao,Yann LeCun,2015
-
单词和短语的分布式表示及其组合性,Tomas Mikolov,Ilya Sutskever,Kai Chen,Greg Corrado,Jeffrey Dean,2013
-
使用输出嵌入来改进语言模型,Ofir Press,Lior Wolf,2016 年 8 月
总结
本章介绍了一种非常常见的方法,特别是将离散的文本输入转换为数值嵌入,用于自然语言处理。
使用神经网络训练这些单词表示的技术不需要我们对数据进行标记,并直接从自然文本推断其嵌入。这样的训练称为无监督学习。
深度学习的主要挑战之一是将输入和输出信号转换为可以由网络处理的表示,特别是浮点向量。然后,神经网络提供所有工具来处理这些向量,学习、决策、分类、推理或生成。
在接下来的章节中,我们将使用这些嵌入来处理文本和更高级的神经网络。下一章中介绍的第一个应用是自动文本生成。
第四章 使用递归神经网络生成文本
在上一章中,你学习了如何将离散输入表示为向量,以便神经网络能够理解离散输入以及连续输入。
许多现实世界的应用涉及可变长度的输入,例如物联网和自动化(类似卡尔曼滤波器,已经更为进化);自然语言处理(理解、翻译、文本生成和图像注释);人类行为重现(文本手写生成和聊天机器人);强化学习。
之前的网络,称为前馈网络,只能对固定维度的输入进行分类。为了将它们的能力扩展到可变长度的输入,设计了一个新的网络类别:递归神经网络(RNN),非常适合用于处理可变长度输入或序列的机器学习任务。
本章介绍了三种著名的递归神经网络(简单 RNN、GRU 和 LSTM),并以文本生成作为示例。 本章涵盖的主题如下:
-
序列的案例
-
递归网络的机制
-
如何构建一个简单的递归网络
-
时间反向传播
-
不同类型的 RNN、LSTM 和 GRU
-
困惑度和词错误率
-
在文本数据上进行训练以生成文本
-
递归网络的应用
递归神经网络的需求
深度学习网络在自然语言处理中的应用是数值化的,能够很好地处理多维数组的浮点数和整数作为输入值。对于类别值,例如字符或单词,上一章展示了一种称为嵌入(embedding)的技术,将它们转换为数值。
到目前为止,所有的输入都是固定大小的数组。在许多应用中,如自然语言处理中的文本,输入有一个语义含义,但可以通过可变长度的序列来表示。
需要处理可变长度的序列,如下图所示:

递归神经网络(RNN)是应对可变长度输入的解决方案。
递归可以看作是在不同时间步长上多次应用前馈网络,每次应用时使用不同的输入数据,但有一个重要区别,那就是存在与过去时间步长的连接,目标是通过时间不断优化输入的表示。
在每个时间步长,隐藏层的输出值代表网络的中间状态。
递归连接定义了从一个状态到另一个状态的转换,给定输入的情况下,以便不断优化表示:

递归神经网络适用于涉及序列的挑战,如文本、声音和语音、手写文字以及时间序列。
自然语言的数据集
作为数据集,可以使用任何文本语料库,例如 Wikipedia、网页文章,甚至是包含代码或计算机程序、戏剧或诗歌等符号的文本;模型将捕捉并重现数据中的不同模式。
在这种情况下,我们使用微型莎士比亚文本来预测新的莎士比亚文本,或者至少是风格上受到莎士比亚启发的新文本;有两种预测层次可以使用,但可以以相同的方式处理:
-
在字符级别:字符属于一个包含标点符号的字母表,给定前几个字符,模型从字母表中预测下一个字符,包括空格,以构建单词和句子。预测的单词不需要属于字典,训练的目标是构建接近真实单词和句子的内容。
-
在单词级别:单词属于一个包含标点符号的字典,给定前几个单词,模型从词汇表中预测下一个单词。在这种情况下,单词有强烈的约束,因为它们属于字典,但句子没有这种约束。我们期望模型更多地关注捕捉句子的语法和意义,而不是字符级别的内容。
在这两种模式下,token 表示字符/单词;字典、字母表或词汇表表示(token 的可能值的列表);
流行的 NLTK 库,一个 Python 模块,用于将文本分割成句子并将其标记化为单词:
conda install nltk
在 Python shell 中,运行以下命令以下载 book 包中的英语分词器:
import nltk
nltk.download("book")
让我们解析文本以提取单词:
from load import parse_text
X_train, y_train, index_to_word = parse_text("data/tiny-shakespear.txt", type="word")
for i in range(10):
print "x", " ".join([index_to_word[x] for x in X_train[i]])
print "y"," ".join([index_to_word[x] for x in y_train[i]])
*Vocabulary size 9000*
*Found 12349 unique words tokens.*
*The least frequent word in our vocabulary is 'a-fire' and appeared 1 times.*
*x START first citizen : before we proceed any further , hear me speak .*
*y first citizen : before we proceed any further , hear me speak . END*
*x START all : speak , speak .*
*y all : speak , speak . END*
*x START first citizen : you are all resolved rather to die than to famish ?*
*y first citizen : you are all resolved rather to die than to famish ? END*
*x START all : resolved .*
*y all : resolved . END*
*x START resolved .*
*y resolved . END*
*x START first citizen : first , you know caius marcius is chief enemy to the people .*
*y first citizen : first , you know caius marcius is chief enemy to the people . END*
*x START all : we know't , we know't .*
*y all : we know't , we know't . END*
*x START first citizen : let us kill him , and we 'll have corn at our own price .*
*y first citizen : let us kill him , and we 'll have corn at our own price . END*
*x START is't a verdict ?*
*y is't a verdict ? END*
*x START all : no more talking o n't ; let it be done : away , away !*
*y all : no more talking o n't ; let it be done : away , away ! END*
或者 char 库:
from load import parse_text
X_train, y_train, index_to_char = parse_text("data/tiny-shakespear.txt", type="char")
for i in range(10):
print "x",''.join([index_to_char[x] for x in X_train[i]])
print "y",''.join([index_to_char[x] for x in y_train[i]])
*x ^first citizen: before we proceed any further, hear me speak*
*y irst citizen: before we proceed any further, hear me speak.$*
*x ^all: speak, speak*
*y ll: speak, speak.$*
*x ^first citizen: you are all resolved rather to die than to famish*
*y irst citizen: you are all resolved rather to die than to famish?$*
*x ^all: resolved*
*y ll: resolved.$*
*x ^resolved*
*y esolved.$*
*x ^first citizen: first, you know caius marcius is chief enemy to the people*
*y irst citizen: first, you know caius marcius is chief enemy to the people.$*
*x ^all: we know't, we know't*
*y ll: we know't, we know't.$*
*x ^first citizen: let us kill him, and we'll have corn at our own price*
*y irst citizen: let us kill him, and we'll have corn at our own price.$*
*x ^is't a verdict*
*y s't a verdict?$*
*x ^all: no more talking on't; let it be done: away, away*
*y ll: no more talking on't; let it be done: away, away!$*
额外的开始标记(START 单词和 ^ 字符)避免了预测开始时产生空的隐藏状态。另一种解决方案是用
初始化第一个隐藏状态。
额外的结束标记(END 单词和 $ 字符)帮助网络学习在序列生成预测完成时预测停止。
最后,out of vocabulary 标记(UNKNOWN 单词)替换那些不属于词汇表的单词,从而避免使用庞大的词典。
在这个示例中,我们将省略验证数据集,但对于任何实际应用程序,将一部分数据用于验证是一个好的做法。
同时,请注意,第二章 中的函数,使用前馈网络分类手写数字 用于层初始化 shared_zeros 和 shared_glorot_uniform,以及来自第三章,将单词编码为向量 用于模型保存和加载的 save_params 和 load_params 已被打包到 utils 包中:
from theano import *
import theano.tensor as T
from utils import shared_zeros, shared_glorot_uniform,save_params,load_params
简单的递归网络
RNN 是在多个时间步上应用的网络,但有一个主要的区别:与前一个时间步的层状态之间的连接,称为隐状态!简单递归网络:

这可以写成以下形式:


RNN 可以展开为一个前馈网络,应用于序列!简单递归网络作为输入,并在不同时间步之间共享参数。
输入和输出的第一个维度是时间,而后续维度用于表示每个步骤中的数据维度。正如上一章所见,某一时间步的值(一个单词或字符)可以通过索引(整数,0 维)或独热编码向量(1 维)表示。前者在内存中更加紧凑。在这种情况下,输入和输出序列将是 1 维的,通过一个向量表示,且该维度为时间:
x = T.ivector()
y = T.ivector()
训练程序的结构与第二章中的用前馈网络分类手写数字相同,只是我们定义的模型与递归模块共享相同的权重,适用于不同的时间步:
embedding_size = len(index_)
n_hidden=500
让我们定义隐状态和输入权重:
U = shared_glorot_uniform(( embedding_size,n_hidden), name="U")
W = shared_glorot_uniform((n_hidden, n_hidden), name="W")
bh = shared_zeros((n_hidden,), name="bh")
以及输出权重:
V = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
by = shared_zeros((embedding_size,), name="by")
params = [U,V,W,by,bh]
def step(x_t, h_tm1):
h_t = T.tanh(U[x_t] + T.dot( h_tm1, W) + bh)
y_t = T.dot(h_t, V) + by
return h_t, y_t
初始状态可以在使用开始标记时设为零:
h0 = shared_zeros((n_hidden,), name='h0')
[h, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, None], truncate_gradient=10)
它返回两个张量,其中第一个维度是时间,第二个维度是数据值(在这种情况下为 0 维)。
通过扫描函数进行的梯度计算在 Theano 中是自动的,并遵循直接连接和递归连接到前一个时间步。因此,由于递归连接,某一特定时间步的错误会传播到前一个时间步,这种机制被称为时间反向传播(BPTT)。
已观察到,在过多时间步后,梯度会爆炸或消失。这就是为什么在这个例子中,梯度在 10 个步骤后被截断,并且错误不会反向传播到更早的时间步。
对于剩余的步骤,我们保持之前的分类方式:
model = T.nnet.softmax(y_pred)
y_out = T.argmax(model, axis=-1)
cost = -T.mean(T.log(model)[T.arange(y.shape[0]), y])
这将在每个时间步返回一个值的向量。
LSTM 网络
RNN 的主要困难之一是捕捉长期依赖关系,这是由于梯度消失/爆炸效应和截断反向传播。
为了克服这个问题,研究人员已经在寻找一长串潜在的解决方案。1997 年设计了一种新型的递归网络,带有一个记忆单元,称为细胞状态,专门用于保持和传输长期信息。
在每个时间步,单元值可以部分通过候选单元更新,并通过门控机制部分擦除。两个门,更新门和忘记门,决定如何更新单元,给定先前的隐藏状态值和当前输入值:

候选单元的计算方式相同:

新的单元状态的计算方式如下:

对于新的隐藏状态,输出门决定要输出单元值中的哪些信息:

其余部分与简单 RNN 保持相同:

该机制允许网络存储一些信息,并在未来比简单 RNN 更远的时间点使用这些信息。
许多 LSTM 设计的变体已经被设计出来,你可以根据你的问题来测试这些变体,看看它们的表现。
在这个例子中,我们将使用一种变体,其中门和候选值同时使用了先前的隐藏状态和先前的单元状态。
在 Theano 中,让我们为以下内容定义权重:
- 输入门:
W_xi = shared_glorot_uniform(( embedding_size,n_hidden))
W_hi = shared_glorot_uniform(( n_hidden,n_hidden))
W_ci = shared_glorot_uniform(( n_hidden,n_hidden))
b_i = shared_zeros((n_hidden,))
- 忘记门:
W_xf = shared_glorot_uniform(( embedding_size, n_hidden))
W_hf = shared_glorot_uniform(( n_hidden,n_hidden))
W_cf = shared_glorot_uniform(( n_hidden,n_hidden))
b_f = shared_zeros((n_hidden,))
- 输出门:
W_xo = shared_glorot_uniform(( embedding_size, n_hidden))
W_ho = shared_glorot_uniform(( n_hidden,n_hidden))
W_co = shared_glorot_uniform(( n_hidden,n_hidden))
b_o = shared_zeros((n_hidden,))
- 单元:
W_xc = shared_glorot_uniform(( embedding_size, n_hidden))
W_hc = shared_glorot_uniform(( n_hidden,n_hidden))
b_c = shared_zeros((n_hidden,))
- 输出层:
W_y = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
b_y = shared_zeros((embedding_size,), name="by")
所有可训练参数的数组:
params = [W_xi,W_hi,W_ci,b_i,W_xf,W_hf,W_cf,b_f,W_xo,W_ho,W_co,b_o,W_xc,W_hc,b_c,W_y,b_y]
要放置在循环中的步进函数:
def step(x_t, h_tm1, c_tm1):
i_t = T.nnet.sigmoid(W_xi[x_t] + T.dot(W_hi, h_tm1) + T.dot(W_ci, c_tm1) + b_i)
f_t = T.nnet.sigmoid(W_xf[x_t] + T.dot(W_hf, h_tm1) + T.dot(W_cf, c_tm1) + b_f)
c_t = f_t * c_tm1 + i_t * T.tanh(W_xc[x_t] + T.dot(W_hc, h_tm1) + b_c)
o_t = T.nnet.sigmoid(W_xo[x_t] + T.dot(W_ho, h_tm1) + T.dot(W_co, c_t) + b_o)
h_t = o_t * T.tanh(c_t)
y_t = T.dot(h_t, W_y) + b_y
return h_t, c_t, y_t
让我们使用扫描操作符创建循环神经网络:
h0 = shared_zeros((n_hidden,), name='h0')
c0 = shared_zeros((n_hidden,), name='c0')
[h, c, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, c0, None], truncate_gradient=10)
门控循环网络
GRU 是 LSTM 的替代方法,它简化了机制,不使用额外的单元:

构建门控循环网络的代码仅需定义权重和 step 函数,如前所述:
- 更新门的权重:
W_xz = shared_glorot_uniform(( embedding_size,n_hidden))
W_hz = shared_glorot_uniform(( n_hidden,n_hidden))
b_z = shared_zeros((n_hidden,))
- 重置门的权重:
W_xr = shared_glorot_uniform(( embedding_size,n_hidden))
W_hr = shared_glorot_uniform(( n_hidden,n_hidden))
b_r = shared_zeros((n_hidden,))
- 隐藏层的权重:
W_xh = shared_glorot_uniform(( embedding_size,n_hidden))
W_hh = shared_glorot_uniform(( n_hidden,n_hidden))
b_h = shared_zeros((n_hidden,))
- 输出层的权重:
W_y = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
b_y = shared_zeros((embedding_size,), name="by")
可训练参数:
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_y, b_y]
步进函数:
def step(x_t, h_tm1):
z_t = T.nnet.sigmoid(W_xz[x_t] + T.dot(W_hz, h_tm1) + b_z)
r_t = T.nnet.sigmoid(W_xr[x_t] + T.dot(W_hr, h_tm1) + b_r)
can_h_t = T.tanh(W_xh[x_t] + r_t * T.dot(W_hh, h_tm1) + b_h)
h_t = (1 - z_t) * h_tm1 + z_t * can_h_t
y_t = T.dot(h_t, W_y) + b_y
return h_t, y_t
循环神经网络:
h0 = shared_zeros((n_hidden,), name='h0')
[h, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, None], truncate_gradient=10)
介绍了主要网络后,我们将看看它们在文本生成任务中的表现。
自然语言性能指标
词错误率 (WER) 或 字符错误率 (CER) 等同于自然语言准确度错误的定义。
语言模型的评估通常通过困惑度来表示,困惑度简单地定义为:

训练损失对比
在训练过程中,学习率在经过一定数量的 epochs 后可能会变强,用于微调。当损失不再减小时,减少学习率将有助于训练的最后步骤。为了减少学习率,我们需要在编译时将其定义为输入变量:
lr = T.scalar('learning_rate')
train_model = theano.function(inputs=[x,y,lr], outputs=cost,updates=updates)
在训练过程中,我们调整学习率,如果训练损失没有改善,则减小学习率:
if (len(train_loss) > 1 and train_loss[-1] > train_loss[-2]):
learning_rate = learning_rate * 0.5
作为第一个实验,让我们看看隐藏层大小对简单 RNN 训练损失的影响:

更多的隐藏单元可以提高训练速度,最终可能表现更好。为了验证这一点,我们应该运行更多的 epochs。
比较不同网络类型的训练,在这种情况下,我们没有观察到 LSTM 和 GRU 有任何改善:

这可能是由于truncate_gradient选项,或者因为问题过于简单,不依赖于记忆。
另一个需要调整的参数是词汇出现在词典中的最小次数。更高的次数会学习到更频繁的词,这样更好。
预测示例
让我们用生成的模型预测一个句子:
sentence = [0]
while sentence[-1] != 1:
pred = predict_model(sentence)[-1]
sentence.append(pred)
print(" ".join([ index_[w] for w in sentence[1:-1]]))
请注意,我们选择最有可能的下一个词(argmax),同时,为了增加一些随机性,我们必须根据预测的概率抽取下一个词。
在 150 个 epoch 时,虽然模型仍未完全收敛到我们对莎士比亚文笔的学习上,我们可以通过初始化几个单词来玩转预测,并看到网络生成句子的结尾:
-
第一市民:一句话,我知道一句话
-
现在怎么了!
-
你不觉得这样睡着了,我说的是这个吗?
-
锡尼乌斯:什么,你是我的主人吗?
-
好吧,先生,来吧。
-
我自己已经做过
-
最重要的是你在你时光中的状态,先生
-
他将不会这样做
-
祈求你,先生
-
来吧,来吧,你
-
乌鸦?
-
我会给你
-
什么,嘿!
-
考虑你,先生
-
不再!
-
我们走吧,或者你的未知未知,我做我该做的事
-
我们现在不是
从这些例子中,我们可以看出,模型学会了正确地定位标点符号,在正确的位置添加句点、逗号、问号或感叹号,从而正确地排序直接宾语、间接宾语和形容词。
原文由短句组成,风格类似莎士比亚。更长的文章,如维基百科页面,以及通过进一步训练并使用验证集来控制过拟合,将生成更长的文本。第十章,使用先进的 RNN 预测时间序列:将教授如何使用先进的 RNN 预测时间序列,并展示本章的进阶版本。
RNN 的应用
本章介绍了简单的 RNN、LSTM 和 GRU 模型。这些模型在序列生成或序列理解中有广泛的应用:
-
文本生成,例如自动生成奥巴马的政治演讲(obama-rnn),例如使用关于工作的话题作为文本种子:
下午好。愿上帝保佑你。美国将承担起解决美国人民面临的新挑战的责任,并承认我们创造了这一问题。他们受到了攻击,因此必须说出在战争最后日子里的所有任务,我无法完成。这是那些依然在努力的人们的承诺,他们将不遗余力,确保美国人民能够保护我们的部分。这是一次齐心协力的机会,完全寻找向美国人民借鉴承诺的契机。事实上,身着制服的男女和我们国家数百万人的法律系统应该是我们所能承受的力量的强大支撑,我们可以增加美国人民的精神力量,并加强我们国家领导层在美国人民生活中的作用。非常感谢。上帝保佑你们,愿上帝保佑美利坚合众国。
你可以在
medium.com/@samim/obama-rnn-machine-generated-political-speeches-c8abd18a2ea0#.4nee5wafe.查看这个例子。 -
文本注释,例如,词性(POS)标签:名词、动词、助词、副词和形容词。
生成手写字:
www.cs.toronto.edu/~graves/handwriting.html![RNN 的应用]()
-
使用 Sketch-RNN 绘图 (
github.com/hardmaru/sketch-rnn)![RNN 的应用]()
-
语音合成:递归网络将生成用于生成每个音素的参数。在下面的图像中,时间-频率同质块被分类为音素(或字形或字母):
![RNN 的应用]()
-
音乐生成:
-
任何序列的分类,如情感分析(积极、消极或中立情感),我们将在第五章中讨论,使用双向 LSTM 分析情感。
-
序列编码或解码,我们将在第六章中讨论,使用空间变换网络进行定位。
相关文章
你可以参考以下链接以获得更多深入的见解:
-
递归神经网络的非理性有效性,Andrej Karpathy,2015 年 5 月 21 日(
karpathy.github.io/2015/05/21/rnn-effectiveness/) -
理解 LSTM 网络,Christopher Colah 的博客,2015 年(
colah.github.io/posts/2015-08-Understanding-LSTMs/) -
使用 LSTM 进行音频分类:连接时序分类与深度语音:端到端语音识别的扩展(
arxiv.org/abs/1412.5567) -
使用递归神经网络的通用序列学习教程:
www.youtube.com/watch?v=VINCQghQRuM -
关于训练递归神经网络的难点,Razvan Pascanu,Tomas Mikolov,Yoshua Bengio,2012 年
-
递归神经网络教程:
-
RNN 简介
-
使用 Python、NumPy 和 Theano 实现 RNN
-
反向传播与时间和梯度消失问题
-
使用 Python 和 Theano 实现 GRU/LSTM RNN,Denny Britz 2015 年,
www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/
-
-
长短时记忆(LONG SHORT-TERM MEMORY),Sepp Hochreiter,Jürgen Schmidhuber,1997 年
概述
递归神经网络提供了处理离散或连续数据的变长输入和输出的能力。
在之前的前馈网络只能处理单一输入到单一输出(一对一方案)的情况下,本章介绍的递归神经网络提供了在变长和定长表示之间进行转换的可能性,新增了深度学习输入/输出的新操作方案:一对多、多对多,或多对一。
RNN 的应用范围广泛。因此,我们将在后续章节中更深入地研究它们,特别是如何增强这三种模块的预测能力,或者如何将它们结合起来构建多模态、问答或翻译应用。
特别地,在下一章中,我们将通过一个实际示例,使用文本嵌入和递归网络进行情感分析。此次还将有机会在另一个库 Keras 下复习这些递归单元,Keras 是一个简化 Theano 模型编写的深度学习库。
第五章:使用双向 LSTM 分析情感
本章更具实用性,以便更好地了解在前两章中介绍的常用循环神经网络和词嵌入。
这也是一个将读者引入深度学习新应用——情感分析的机会,这也是自然语言处理(NLP)的另一个领域。这是一个多对一的方案,其中一系列可变长度的单词必须分配到一个类别。一个类似可以使用这种方案的 NLP 问题是语言检测(如英语、法语、德语、意大利语等)。
虽然上一章展示了如何从零开始构建循环神经网络,但本章将展示如何使用基于 Theano 构建的高级库 Keras,帮助实现和训练使用预构建模块的模型。通过这个示例,读者应该能够判断何时在项目中使用 Keras。
本章将讨论以下几个要点:
-
循环神经网络和词嵌入的回顾
-
情感分析
-
Keras 库
-
双向循环神经网络
自动化情感分析是识别文本中表达的意见的问题。它通常涉及将文本分类为积极、消极和中性等类别。意见是几乎所有人类活动的核心,它们是我们行为的关键影响因素。
最近,神经网络和深度学习方法被用于构建情感分析系统。这些系统能够自动学习一组特征,以克服手工方法的缺点。
循环神经网络(RNN)在文献中已被证明是表示序列输入(如文本)的非常有用的技术。循环神经网络的一种特殊扩展——双向循环神经网络(BRNN)能够捕捉文本中的前后上下文信息。
在本章中,我们将展示一个示例,演示如何使用长短时记忆(LSTM)架构的双向循环神经网络来解决情感分析问题。我们的目标是实现一个模型,给定一段文本输入(即一系列单词),该模型试图预测其是积极的、消极的还是中性的。
安装和配置 Keras
Keras 是一个高级神经网络 API,用 Python 编写,可以在 TensorFlow 或 Theano 上运行。它的开发目的是让实现深度学习模型变得尽可能快速和简单,以便于研究和开发。你可以通过 conda 轻松安装 Keras,如下所示:
conda install keras
在编写 Python 代码时,导入 Keras 会告诉你使用的是哪个后端:
>>> import keras
*Using Theano backend.*
*Using cuDNN version 5110 on context None*
*Preallocating 10867/11439 Mb (0.950000) on cuda0*
*Mapped name None to device cuda0: Tesla K80 (0000:83:00.0)*
*Mapped name dev0 to device cuda0: Tesla K80 (0000:83:00.0)*
*Using cuDNN version 5110 on context dev1*
*Preallocating 10867/11439 Mb (0.950000) on cuda1*
*Mapped name dev1 to device cuda1: Tesla K80 (0000:84:00.0)*
如果你已经安装了 TensorFlow,它可能不会使用 Theano。要指定使用哪个后端,请编写一个 Keras 配置文件 ~/.keras/keras.json:。
{
"epsilon": 1e-07,
"floatx": "float32",
"image_data_format": "channels_last",
"backend": "theano"
}
也可以直接通过环境变量指定 Theano 后端:
KERAS_BACKEND=theano python
请注意,所使用的设备是我们在 ~/.theanorc 文件中为 Theano 指定的设备。也可以通过 Theano 环境变量来修改这些变量:
KERAS_BACKEND=theano THEANO_FLAGS=device=cuda,floatX=float32,mode=FAST_RUN python
使用 Keras 编程
Keras 提供了一套数据预处理和构建模型的方法。
层和模型是对张量的可调用函数,并返回张量。在 Keras 中,层/模块和模型没有区别:一个模型可以是更大模型的一部分,并由多个层组成。这样的子模型作为模块运行,具有输入/输出。
让我们创建一个包含两个线性层、中间加入 ReLU 非线性层并输出 softmax 的网络:
from keras.layers import Input, Dense
from keras.models import Model
inputs = Input(shape=(784,))
x = Dense(64, activation='relu')(inputs)
predictions = Dense(10, activation='softmax')(x)
model = Model(inputs=inputs, outputs=predictions)
model 模块包含用于获取输入和输出形状的方法,无论是单个输入/输出还是多个输入/输出,并列出我们模块的子模块:
>>> model.input_shape
*(None, 784)*
>>> model.get_input_shape_at(0)
*(None, 784)*
>>> model.output_shape
*(None, 10)*
>>> model.get_output_shape_at(0)
*(None, 10)*
>>> model.name
*'sequential_1'*
>>> model.input
*/dense_3_input*
>>> model.output
*Softmax.0*
>>> model.get_output_at(0)
*Softmax.0*
>>> model.layers
*[<keras.layers.core.Dense object at 0x7f0abf7d6a90>, <keras.layers.core.Dense object at 0x7f0abf74af90>]*
为了避免为每一层指定输入,Keras 提出了通过 Sequential 模块编写模型的函数式方法,以构建由多个模块或模型组成的新模块或模型。
以下模型定义与之前展示的模型完全相同,使用 input_dim 来指定输入维度,否则将无法知道该维度并生成错误:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(units=64, input_dim=784, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
model 被视为可以是更大模型的一部分的模块或层:
model2 = Sequential()
model2.add(model)
model2.add(Dense(units=10, activation='softmax'))
每个模块/模型/层都可以进行编译,然后使用数据进行训练:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(data, labels)
让我们实践一下 Keras。
SemEval 2013 数据集
让我们从准备数据开始。在本章中,我们将使用在 SemEval 2013 竞赛中用于监督任务的 Twitter 情感分类(消息级别)的标准数据集。该数据集包含 3662 条推文作为训练集,575 条推文作为开发集,1572 条推文作为测试集。该数据集中的每个样本包含推文 ID、极性(正面、负面或中性)和推文内容。
让我们下载数据集:
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_train.zip
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_dev.zip
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_test_fixed.zip
unzip semeval2013_task2_train.zip
unzip semeval2013_task2_dev.zip
unzip semeval2013_task2_test_fixed.zip
A 指的是子任务 A,即消息级情感分类 我们本章研究的目标,其中 B 指的是子任务 B 的术语级情感分析。
input 目录不包含标签,仅包含推文。full 目录包含更多级别的分类,主观 或 客观。我们的关注点是 gold 或 cleansed 目录。
让我们使用脚本来转换它们:
pip install bs4
python download_tweets.py train/cleansed/twitter-train-cleansed-A.tsv > sem_eval2103.train
python download_tweets.py dev/gold/twitter-dev-gold-A.tsv > sem_eval2103.dev
python download_tweets.py SemEval2013_task2_test_fixed/gold/twitter-test-gold-A.tsv > sem_eval2103.test
文本数据预处理
正如我们所知,在 Twitter 上常常使用 URL、用户提及和话题标签。因此,我们首先需要按照以下步骤预处理推文。
确保所有的标记(tokens)之间使用空格分隔。每条推文都会被转换为小写字母。
URL、用户提及和话题标签分别被 <url>、<user> 和 <hashtag> 代替。此步骤通过 process 函数完成,它以推文为输入,使用 NLTK 的 TweetTokenizer 进行分词,进行预处理,并返回推文中的词汇(token)集合:
import re
from nltk.tokenize import TweetTokenizer
def process(tweet):
tknz = TweetTokenizer()
tokens = tknz.tokenize(tweet)
tweet = " ".join(tokens)
tweet = tweet.lower()
tweet = re.sub(r'http[s]?://(?:[a-z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-f][0-9a-f]))+', '<url>', tweet) # URLs
tweet = re.sub(r'(?:@[\w_]+)', '<user>', tweet) # user-mentions
tweet = re.sub(r'(?:\#+[\w_]+[\w\'_\-]*[\w_]+)', '<hashtag>', tweet) # hashtags
tweet = re.sub(r'(?:(?:\d+,?)+(?:\.?\d+)?)', '<number>', tweet) # numbers
return tweet.split(" ")
例如,如果我们有推文 RT @mhj: just an example! :D http://example.com #NLP,该函数的处理过程如下:
tweet = 'RT @mhj: just an example! :D http://example.com #NLP'
print(process(tweet))
返回值
[u'rt', u'\<user\>', u':', u'just', u'an', u'example', u'!', u':d', u'\<url\>', u'\<hashtag\>']
以下函数用于读取数据集,并返回一个元组列表,每个元组表示一个样本(推文,类别),其中类别是一个整数,取值为 {0, 1 或 2},定义了情感极性:
def read_data(file_name):
tweets = []
labels = []
polarity2idx = {'positive': 0, 'negative': 1, 'neutral': 2}
with open(file_name) as fin:
for line in fin:
_, _, _, _, polarity, tweet = line.strip().split("\t")
tweet = process(tweet)
cls = polarity2idx[polarity]
tweets.append(tweet)
labels.append(cls)
return tweets, labels
train_file = 'sem_eval2103.train'
dev_file = 'sem_eval2103.dev'
train_tweets, y_train = read_data(train_file)
dev_tweets, y_dev = read_data(dev_file)
现在,我们可以构建词汇表,它是一个字典,用于将每个单词映射到一个固定的索引。以下函数接收一个数据集作为输入,并返回词汇表和推文的最大长度:
def get_vocabulary(data):
max_len = 0
index = 0
word2idx = {'<unknown>': index}
for tweet in data:
max_len = max(max_len, len(tweet))
for word in tweet:
if word not in word2idx:
index += 1
word2idx[word] = index
return word2idx, max_len
word2idx, max_len = get_vocabulary(train_tweets)
vocab_size = len(word2idx)
我们还需要一个函数,将每条推文或一组推文转换为基于词汇表的索引,如果单词存在的话,否则用未知标记(索引 0)替换词汇表外(OOV)的单词,具体如下:
def transfer(data, word2idx):
transfer_data = []
for tweet in data:
tweet2vec = []
for word in tweet:
if word in word2idx:
tweet2vec.append(word2idx[word])
else:
tweet2vec.append(0)
transfer_data.append(tweet2vec)
return transfer_data
X_train = transfer(train_tweets, word2idx)
X_dev = transfer(dev_tweets, word2idx)
我们可以节省一些内存:
del train_tweets, dev_tweets
Keras 提供了一个辅助方法来填充序列,确保它们具有相同的长度,以便一批序列可以通过张量表示,并在 CPU 或 GPU 上对张量进行优化操作。
默认情况下,该方法会在序列开头进行填充,这有助于获得更好的分类结果:

from keras.preprocessing.sequence import pad_sequences
X_train = pad_sequences(X_train, maxlen=max_len, truncating='post')
X_dev = pad_sequences(X_dev, maxlen=max_len, truncating='post')
最后,Keras 提供了一个方法,通过添加一个维度,将类别转换为它们的一热编码表示:

使用 Keras 的 to_categorical 方法:
from keras.utils.np_utils import to_categorical
y_train = to_categorical(y_train)
y_dev = to_categorical(y_dev)
设计模型架构
本示例中的模型主要模块如下:
-
首先,输入句子的单词会被映射为实数向量。这个步骤称为词的向量表示或词嵌入(更多细节,请参见第三章,将单词编码为向量)。
-
然后,使用双向 LSTM 编码器将这组向量表示为一个固定长度的实值向量。这个向量总结了输入句子,并包含基于词向量的语义、句法和/或情感信息。
-
最后,这个向量通过一个 softmax 分类器,将句子分类为正面、负面或中立。
词的向量表示
词嵌入是分布式语义学的一种方法,它将单词表示为实数向量。这种表示具有有用的聚类特性,因为在语义和句法上相关的单词会被表示为相似的向量(参见第三章,将单词编码为向量)。
这一步的主要目的是将每个单词映射到一个连续的、低维的实值向量,这些向量可以作为任何模型的输入。所有单词向量被堆叠成一个矩阵
;其中,N 是词汇表大小,d 是向量维度。这个矩阵被称为嵌入层或查找表层。嵌入矩阵可以使用预训练模型(如 Word2vec 或 Glove)进行初始化。
在 Keras 中,我们可以简单地定义嵌入层,如下所示:
from keras.layers import Embedding
d = 100
emb_layer = Embedding(vocab_size + 1, output_dim=d, input_length=max_len)
第一个参数表示词汇表大小,output_dim 是向量维度,input_length 是输入序列的长度。
让我们将此层作为输入层添加到模型中,并声明模型为顺序模型:
from keras.models import Sequential
model = Sequential()
model.add(emb_layer)
使用双向 LSTM 进行句子表示
循环神经网络具有表示序列(如句子)的能力。然而,在实际应用中,由于梯度消失/爆炸问题,使用普通的 RNN 学习长期依赖关系是困难的。如前一章所述,长短期记忆(LSTM)网络被设计为具有更持久的记忆(即状态),专门用于保持和传递长期信息,这使得它们在捕捉序列中元素之间的长期依赖关系方面非常有用。
LSTM 单元是本章所用模型的基本组件。
Keras 提供了一种方法 TimeDistributed,用于在多个时间步上克隆任何模型并使其具有递归性。但对于常用的递归单元,如 LSTM,Keras 中已经存在一个模块:
from keras.layers import LSTM
rnn_size = 64
lstm = LSTM(rnn_size, input_shape=(max_len, d))
以下内容相同:
lstm = LSTM(rnn_size, input_dim=d, input_length=max_len)
对于后续层,我们无需指定输入大小(这是因为 LSTM 层位于嵌入层之后),因此我们可以简单地定义 lstm 单元,如下所示:
lstm = LSTM(rnn_size)
最后但同样重要的是,在这个模型中,我们希望使用双向 LSTM。它已经证明能够带来更好的结果,在给定前一个词的情况下捕捉当前词的含义,以及在之后出现的词:

为了让这个单元以双向方式处理输入,我们可以简单地使用 Bidirectional,这是一个针对 RNN 的双向封装器:
from keras.layers import Bidirectional
bi_lstm = Bidirectional(lstm)
model.add(bi_lstm)
使用 softmax 分类器输出概率
最后,我们可以将从 bi_lstm 获得的向量传递给 softmax 分类器,如下所示:
from keras.layers import Dense, Activation
nb_classes = 3
fc = Dense(nb_classes)
classifier = Activation('softmax')
model.add(fc)
model.add(classifier)
现在,让我们打印出模型的摘要:
print(model.summary())
Which will end with the results:
Using Theano backend:
__________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
=========================================================================================
embedding_1 (Embedding) (None, 30, 100) 10000100 embedding_input_1[0][0]
_________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 128) 84480 embedding_1[0][0]
__________________________________________________________________________________________
dense_1 (Dense) (None, 3) 387 bidirectional_1[0][0]
__________________________________________________________________________________________
activation_1 (Activation) (None, 3) 0 dense_1[0][0]
=========================================================================================
Total params: 10,084,967
Trainable params: 10,084,967
Non-trainable params: 0
__________________________________________________________________________________________
编译和训练模型
现在,模型已定义,准备好进行编译。要在 Keras 中编译模型,我们需要确定优化器、损失函数,并可选地指定评估指标。如前所述,问题是预测推文是正面、负面还是中立的。这是一个多类别分类问题。因此,在这个示例中使用的损失(或目标)函数是 categorical_crossentropy。我们将使用 rmsprop 优化器和准确率评估指标。
在 Keras 中,您可以找到实现的最先进的优化器、目标函数和评估指标。使用编译函数在 Keras 中编译模型非常简单:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
我们已经定义并编译了模型,现在它已经准备好进行训练。我们可以通过调用 fit 函数在定义的数据上训练或拟合模型。
训练过程会经过若干次数据集迭代,称为 epochs,可以通过epochs参数来指定。我们还可以使用batch_size参数设置每次训练时输入给模型的实例数。在本例中,我们将使用较小的epochs = 30,并使用较小的批次大小10。我们还可以通过显式地使用validation_data参数输入开发集来在训练过程中评估模型,或者通过validation_split参数选择训练集的一个子集。在本例中,我们将使用之前定义的开发集:
model.fit(x=X_train, y=y_train, batch_size=10, epochs=30, validation_data=[X_dev, y_dev])
评估模型
我们已经在训练集上训练了模型,现在可以评估网络在测试集上的性能。可以使用evaluation()函数来完成这一操作。该函数返回模型在测试模式下的损失值和指标值:
test_file = 'sem_eval2103.test'
test_tweets, y_test = read_data(test_file)
X_test = transfer(test_tweets, word2idx)
del test_twee
X_test = pad_sequences(X_test, maxlen=max_len, truncating='post')
y_test = to_categorical(y_test)
test_loss, test_acc = model.evaluate(X_test, y_test)
print("Testing loss: {:.5}; Testing Accuracy: {:.2%}" .format(test_loss, test_acc))
保存和加载模型
要保存 Keras 模型的权重,只需调用save函数,模型将序列化为.hdf5格式:
model.save('bi_lstm_sentiment.h5')
要加载模型,请使用 Keras 提供的load_model函数,如下所示:
from keras.models import load_model
loaded_model = load_model('bi_lstm_sentiment.h5')
它现在已经准备好进行评估,并且无需重新编译。例如,在相同的测试集上,我们必须获得相同的结果:
test_loss, test_acc = loaded_model.evaluate(X_test, y_test)
print("Testing loss: {:.5}; Testing Accuracy: {:.2%}" .format(test_loss, test_acc))
运行示例
要运行模型,我们可以执行以下命令行:
python bilstm.py
进一步阅读
请参考以下文章:
-
SemEval Sentiment Analysis in Twitter
www.cs.york.ac.uk/semeval-2013/task2.html -
Personality insights with IBM Watson demo
personality-insights-livedemo.mybluemix.net/ -
Tone analyzer
tone-analyzer-demo.mybluemix.net/ -
Keras
keras.io/ -
Deep Speech: 扩展端到端语音识别,Awni Hannun, Carl Case, Jared Casper, Bryan Catanzaro, Greg Diamos, Erich Elsen, Ryan Prenger, Sanjeev Satheesh, Shubho Sengupta, Adam Coates, Andrew Y. Ng, 2014
-
深度递归神经网络语音识别,Alex Graves, Abdel-Rahman Mohamed, Geoffrey Hinton, 2013
-
Deep Speech 2:英语和普通话的端到端语音识别,作者:Dario Amodei, Rishita Anubhai, Eric Battenberg, Carl Case, Jared Casper, Bryan Catanzaro, Jingdong Chen, Mike Chrzanowski, Adam Coates, Greg Diamos, Erich Elsen, Jesse Engel, Linxi Fan, Christopher Fougner, Tony Han, Awni Hannun, Billy Jun, Patrick LeGresley, Libby Lin, Sharan Narang, Andrew Ng, Sherjil Ozair, Ryan Prenger, Jonathan Raiman, Sanjeev Satheesh, David Seetapun, Shubho Sengupta, Yi Wang, Zhiqian Wang, Chong Wang, Bo Xiao, Dani Yogatama, Jun Zhan, Zhenyao Zhu,2015
总结
本章回顾了前几章介绍的基本概念,同时介绍了一种新应用——情感分析,并介绍了一个高层次的库 Keras,旨在简化使用 Theano 引擎开发模型的过程。
这些基本概念包括循环网络、词嵌入、批量序列填充和类别独热编码。为了提高结果,提出了双向递归。
在下一章中,我们将看到如何将递归应用于图像,使用一个比 Keras 更轻量的库 Lasagne,它能让你更顺利地将库模块与自己的 Theano 代码结合。
第六章. 使用空间变换器网络进行定位
本章将 NLP 领域留到后面再回到图像,并展示递归神经网络在图像中的应用实例。在第二章,使用前馈网络分类手写数字中,我们处理了图像分类的问题,即预测图像的类别。在这里,我们将讨论对象定位,这是计算机视觉中的一个常见任务,旨在预测图像中对象的边界框。
而第二章,使用前馈网络分类手写数字通过使用线性层、卷积层和非线性激活函数构建的神经网络解决了分类任务,而空间变换器是一个新的模块,基于非常特定的方程,专门用于定位任务。
为了在图像中定位多个对象,空间变换器是通过递归网络构成的。本章借此机会展示如何在Lasagne中使用预构建的递归网络,Lasagne 是一个基于 Theano 的库,提供额外的模块,并通过预构建的组件帮助你快速开发神经网络,同时不改变你使用 Theano 构建和处理网络的方式。
总结来说,主题列表由以下内容组成:
-
Lasagne 库简介
-
空间变换器网络
-
带有空间变换器的分类网络
-
使用 Lasagne 的递归模块
-
数字的递归读取
-
使用铰链损失函数的无监督训练
-
基于区域的对象定位神经网络
使用 Lasagne 的 MNIST CNN 模型
Lasagne 库打包了层和工具,能够轻松处理神经网络。首先,让我们安装最新版本的 Lasagne:
pip install --upgrade https://github.com/Lasagne/Lasagne/archive/master.zip
让我们使用 Lasagne 重新编写第二章,使用前馈网络分类手写数字中的 MNIST 模型:
def model(l_input, input_dim=28, num_units=256, num_classes=10, p=.5):
network = lasagne.layers.Conv2DLayer(
l_input, num_filters=32, filter_size=(5, 5),
nonlinearity=lasagne.nonlinearities.rectify,
W=lasagne.init.GlorotUniform())
network = lasagne.layers.MaxPool2DLayer(network, pool_size=(2, 2))
network = lasagne.layers.Conv2DLayer(
network, num_filters=32, filter_size=(5, 5),
nonlinearity=lasagne.nonlinearities.rectify)
network = lasagne.layers.MaxPool2DLayer(network, pool_size=(2, 2))
if num_units > 0:
network = lasagne.layers.DenseLayer(
lasagne.layers.dropout(network, p=p),
num_units=num_units,
nonlinearity=lasagne.nonlinearities.rectify)
if (num_units > 0) and (num_classes > 0):
network = lasagne.layers.DenseLayer(
lasagne.layers.dropout(network, p=p),
num_units=num_classes,
nonlinearity=lasagne.nonlinearities.softmax)
return network
层包括layer0_input、conv1_out、pooled_out、conv2_out、pooled2_out、hidden_output。它们是通过预构建的模块构建的,例如,InputLayer、Conv2DLayer、MaxPool2DLayer、DenseLayer,以及诸如修正线性单元(rectify)或 softmax 的丢弃层非线性和GlorotUniform的初始化方式。
要连接由模块组成的网络图,将输入符号var与输出var连接,使用以下代码:
input_var = T.tensor4('inputs')
l_input = lasagne.layers.InputLayer(shape=(None, 1, 28, 28), input_var=input_var)
network = mnist_cnn.model(l_input)
prediction = lasagne.layers.get_output(network)
或者使用这段代码:
l_input = lasagne.layers.InputLayer(shape=(None, 1, 28, 28))
network = mnist_cnn.model(l_input)
input_var = T.tensor4('inputs')
prediction = lasagne.layers.get_output(network, input_var)
一个非常方便的功能是,你可以打印任何模块的输出形状:
print(l_input.output_shape)
Lasagne 的get_all_params方法列出了模型的参数:
params = lasagne.layers.get_all_params(network, trainable=True)
for p in params:
print p.name
最后,Lasagne 提供了不同的学习规则,如RMSprop、Nesterov Momentum、Adam和Adagrad:
target_var = T.ivector('targets')
loss = lasagne.objectives.categorical_crossentropy(prediction, target_var)
loss = loss.mean()
updates = lasagne.updates.nesterov_momentum(
loss, params, learning_rate=0.01, momentum=0.9)
train_fn = theano.function([input_var, target_var], loss, updates=updates)
其他所有内容保持不变。
为了测试我们的 MNIST 模型,下载 MNIST 数据集:
wget http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz -P /sharedfiles
训练一个 MNIST 分类器来进行数字分类:
python 1-train-mnist.py
模型参数保存在 model.npz 中。准确率再次超过 99%。
一个定位网络
在 空间变换网络 (STN) 中,想法不是直接将网络应用于输入图像信号,而是添加一个模块来预处理图像,对其进行裁剪、旋转和缩放以适应物体,从而辅助分类:

空间变换网络
为此,STNs 使用一个定位网络来预测仿射变换参数并处理输入:

空间变换网络
在 Theano 中,仿射变换的微分是自动完成的,我们只需通过仿射变换将定位网络与分类网络的输入连接起来。
首先,我们创建一个与 MNIST CNN 模型相差不远的定位网络,用于预测仿射变换的六个参数:
l_in = lasagne.layers.InputLayer((None, dim, dim))
l_dim = lasagne.layers.DimshuffleLayer(l_in, (0, 'x', 1, 2))
l_pool0_loc = lasagne.layers.MaxPool2DLayer(l_dim, pool_size=(2, 2))
l_dense_loc = mnist_cnn.model(l_pool0_loc, input_dim=dim, num_classes=0)
b = np.zeros((2, 3), dtype=theano.config.floatX)
b[0, 0] = 1.0
b[1, 1] = 1.0
l_A_net = lasagne.layers.DenseLayer(
l_dense_loc,
num_units=6,
name='A_net',
b=b.flatten(),
W=lasagne.init.Constant(0.0),
nonlinearity=lasagne.nonlinearities.identity)
在这里,我们只需通过 DimshuffleLayer 向输入数组添加一个通道维度,该维度的值仅为 1。这样的维度添加被称为广播。
池化层将输入图像大小调整为 50x50,这足以确定数字的位置。
定位层的权重初始化为零,偏置则初始化为单位仿射参数;STN 模块在开始时不会产生任何影响,整个输入图像将被传输。
根据仿射参数进行裁剪:
l_transform = lasagne.layers.TransformerLayer(
incoming=l_dim,
localization_network=l_A_net,
downsample_factor=args.downsample)
down_sampling_factor 使我们能够根据输入定义输出图像的大小。在这种情况下,它的值是三,意味着图像将是 33x33——与我们的 MNIST 数字大小 28x28 相差不远。最后,我们简单地将 MNIST CNN 模型添加到分类输出中:
l_out = mnist_cnn.model(l_transform, input_dim=dim, p=sh_drp, num_units=400)
为了测试分类器,让我们创建一些 100x100 像素的图像,带有一些变形和一个数字:
python create_mnist_sequence.py --nb_digits=1
绘制前三个图像(对应 1、0、5):
python plot_data.py mnist_sequence1_sample_8distortions_9x9.npz

运行命令以训练模型:
python 2-stn-cnn-mnist.py
在这里,当数字没有变形时,准确率超过 99%,这通常是仅用简单的 MNIST CNN 模型无法实现的,并且在有变形的情况下,准确率超过 96.9%。
绘制裁剪图像的命令是:
python plot_crops.py res_test_2.npz
它给我们带来了以下结果:


带有变形的情况:

STN 可以被看作是一个模块,可以包含在任何网络中,位于两个层之间的任何地方。为了进一步提高分类结果,在分类网络的不同层之间添加多个 STN 有助于获得更好的结果。
这是一个包含两个分支的网络示例,每个分支都有自己的 SPN,它们在无监督的情况下将尝试捕捉图像的不同部分进行分类:

(空间变换网络论文,Jaderberg 等,2015 年)
应用于图像的递归神经网络
这个想法是使用递归来读取多个数字,而不仅仅是一个:

为了读取多个数字,我们只需将定位前馈网络替换为递归网络,它将输出多个仿射变换,分别对应于每个数字:

从前面的例子中,我们将全连接层替换为 GRU 层:
l_conv2_loc = mnist_cnn.model(l_pool0_loc, input_dim=dim, p=sh_drp, num_units=0)
class Repeat(lasagne.layers.Layer):
def __init__(self, incoming, n, **kwargs):
super(Repeat, self).__init__(incoming, **kwargs)
self.n = n
def get_output_shape_for(self, input_shape):
return tuple([input_shape[0], self.n] + list(input_shape[1:]))
def get_output_for(self, input, **kwargs):
tensors = [input]*self.n
stacked = theano.tensor.stack(*tensors)
dim = [1, 0] + range(2, input.ndim+1)
return stacked.dimshuffle(dim)
l_repeat_loc = Repeat(l_conv2_loc, n=num_steps)
l_gru = lasagne.layers.GRULayer(l_repeat_loc, num_units=num_rnn_units,
unroll_scan=True)
l_shp = lasagne.layers.ReshapeLayer(l_gru, (-1, num_rnn_units))
这将输出一个维度为(None, 3, 256)的张量,其中第一维是批量大小,3 是 GRU 中的步骤数,256 是隐藏层的大小。在这个层的上面,我们仅仅添加一个和之前一样的全连接层,输出三个初始的身份图像:
b = np.zeros((2, 3), dtype=theano.config.floatX)
b[0, 0] = 1.0
b[1, 1] = 1.0
l_A_net = lasagne.layers.DenseLayer(
l_shp,
num_units=6,
name='A_net',
b=b.flatten(),
W=lasagne.init.Constant(0.0),
nonlinearity=lasagne.nonlinearities.identity)
l_conv_to_transform = lasagne.layers.ReshapeLayer(
Repeat(l_dim, n=num_steps), [-1] + list(l_dim.output_shape[-3:]))
l_transform = lasagne.layers.TransformerLayer(
incoming=l_conv_to_transform,
localization_network=l_A_net,
downsample_factor=args.downsample)
l_out = mnist_cnn.model(l_transform, input_dim=dim, p=sh_drp, num_units=400)
为了测试分类器,我们创建一些具有100x100像素的图像,并加入一些扭曲,这次包含三个数字:
python create_mnist_sequence.py --nb_digits=3 --output_dim=100
绘制前三个图像(对应序列296、490、125):
python plot_data.py mnist_sequence3_sample_8distortions_9x9.npz



让我们运行命令来训练我们的递归模型:
python 3-recurrent-stn-mnist.py
*Epoch 0 Acc Valid 0.268833333333, Acc Train = 0.268777777778, Acc Test = 0.272466666667*
*Epoch 1 Acc Valid 0.621733333333, Acc Train = 0.611116666667, Acc Test = 0.6086*
*Epoch 2 Acc Valid 0.764066666667, Acc Train = 0.75775, Acc Test = 0.764866666667*
*Epoch 3 Acc Valid 0.860233333333, Acc Train = 0.852294444444, Acc Test = 0.859566666667*
*Epoch 4 Acc Valid 0.895333333333, Acc Train = 0.892066666667, Acc Test = 0.8977*
*Epoch 53 Acc Valid 0.980433333333, Acc Train = 0.984261111111, Acc Test = 0.97926666666*
分类准确率为 99.3%。
绘制裁剪图:
python plot_crops.py res_test_3.npz

带有共同定位的无监督学习
在第二章中训练的数字分类器的前几层,使用前馈网络分类手写数字作为编码函数,将图像表示为嵌入空间中的向量,就像对待单词一样:

通过最小化随机集的合页损失目标函数,有可能训练空间变换网络的定位网络,这些图像被认为包含相同的数字:

最小化这个和意味着修改定位网络中的权重,使得两个定位的数字比两个随机裁剪的数字更接近。
这是结果:

(空间变换网络论文,Jaderberg 等,2015 年)
基于区域的定位网络
历史上,目标定位的基本方法是使用分类网络在滑动窗口中;它的过程是将一个窗口在每个方向上逐像素滑动,并在每个位置和每个尺度上应用分类器。分类器学习判断目标是否存在且居中。这需要大量的计算,因为模型必须在每个位置和尺度上进行评估。
为了加速这一过程,Fast-R-CNN 论文中的区域提议网络(RPN)由研究员 Ross Girshick 提出,目的是将神经网络分类器的全连接层(如 MNIST CNN)转换为卷积层;事实上,在 28x28 的图像上,卷积层和线性层之间没有区别,只要卷积核的尺寸与输入相同。因此,任何全连接层都可以重写为卷积层,使用相同的权重和适当的卷积核尺寸,这使得网络能够在比 28x28 更大的图像上工作,输出在每个位置的特征图和分类得分。唯一的区别可能来自于整个网络的步幅,步幅可以设置为不同于 1,并且可以很大(例如几个 10 像素),通过将卷积核的步幅设置为不同于 1,以减少评估位置的数量,从而减少计算量。这样的转换是值得的,因为卷积非常高效:

Faster R-CNN:使用区域提议网络实现实时物体检测
已经设计了一种端到端网络,借鉴了解卷积原理,其中输出特征图一次性给出所有的边界框:你只看一次(YOLO)架构预测每个位置可能的 B 个边界框。每个边界框由其坐标(x, y, w, h)按比例表示为回归问题,并具有一个与交并比(IOU)相对应的置信度(概率),该交并比表示该框与真实框之间的重叠程度。类似的方式也提出了 SSD 模型。
最后,在第八章中介绍的分割网络,使用编码-解码网络进行翻译和解释,也可以看作是神经网络实现的目标定位方法。
进一步阅读
你可以进一步参考以下来源以获取更多信息:
-
空间变换网络,Max Jaderberg, Karen Simonyan, Andrew Zisserman, Koray Kavukcuoglu, 2015 年 6 月
-
循环空间变换网络,Søren Kaae Sønderby, Casper Kaae Sønderby, Lars Maaløe, Ole Winther, 2015 年 9 月
-
谷歌街景字符识别,Jiyue Wang, Peng Hui How
-
使用卷积神经网络在野外读取文本,Max Jaderberg, Karen Simonyan, Andrea Vedaldi, Andrew Zisserman, 2014 年
-
使用深度卷积神经网络从街景图像中进行多位数字识别,Ian J. Goodfellow, Yaroslav Bulatov, Julian Ibarz, Sacha Arnoud, Vinay Shet, 2013
-
从谷歌街景图像中识别字符,Guan Wang, Jingrui Zhang
-
《用于自然场景文本识别的合成数据与人工神经网络》,Max Jaderberg,Karen Simonyan,Andrea Vedaldi,Andrew Zisserman,2014 年
-
去掉 R 的 R-CNN,Karel Lenc,Andrea Vedaldi,2015 年
-
Fast R-CNN,Ross Girshick,2015 年
-
Faster R-CNN:基于区域提议网络的实时物体检测,Shaoqing Ren,Kaiming He,Ross Girshick,Jian Sun,2015 年
-
你只需看一次:统一的实时物体检测,Joseph Redmon,Santosh Divvala,Ross Girshick,Ali Farhadi,2015 年 6 月
-
YOLO 实时演示
pjreddie.com/darknet/yolo/ -
YOLO9000:更好、更快、更强,Joseph Redmon,Ali Farhadi,2016 年 12 月
-
SSD:单次多框检测器,Wei Liu,Dragomir Anguelov,Dumitru Erhan,Christian Szegedy,Scott Reed,Cheng-Yang Fu,Alexander C. Berg,2015 年 12 月
-
精确的物体检测和语义分割的丰富特征层次,Ross Girshick,Jeff Donahue,Trevor Darrell,Jitendra Malik,2013 年
-
文本流:一种统一的自然场景图像文本检测系统,Shangxuan Tian,Yifeng Pan,Chang Huang,Shijian Lu,Kai Yu,Chew Lim Tan,2016 年
总结
空间变换器层是一个原创模块,用于定位图像区域、裁剪并调整大小,帮助分类器集中注意图像中的相关部分,从而提高准确性。该层由可微分的仿射变换组成,参数通过另一个模型——定位网络进行计算,并且可以像往常一样通过反向传播进行学习。
使用循环神经单元可以推断出图像中读取多个数字的应用示例。为了简化工作,引入了 Lasagne 库。
空间变换器是众多定位方法中的一种;基于区域的定位方法,如 YOLO、SSD 或 Faster RCNN,提供了最先进的边界框预测结果。
在下一章中,我们将继续进行图像识别,探索如何对包含比数字更多信息的完整图像进行分类,例如室内场景和户外风景的自然图像。与此同时,我们将继续使用 Lasagne 的预构建层和优化模块。
第七章。使用残差网络分类图像
本章介绍了用于图像分类的最先进的深度网络。
残差网络已成为最新的架构,准确性大幅提高,并且更为简洁。
在残差网络之前,已经有很长时间的架构历史,比如AlexNet、VGG、Inception(GoogLeNet)、Inception v2、v3 和 v4。研究人员一直在寻找不同的概念,并发现了一些潜在的规律来设计更好的架构。
本章将涉及以下主题:
-
图像分类评估的主要数据集
-
图像分类的网络架构
-
批量归一化
-
全局平均池化
-
残差连接
-
随机深度
-
密集连接
-
多 GPU
-
数据增强技术
自然图像数据集
图像分类通常包括比 MNIST 手写数字更广泛的物体和场景。它们大多数是自然图像,意味着人类在现实世界中观察到的图像,例如风景、室内场景、道路、山脉、海滩、人类、动物和汽车,而不是合成图像或计算机生成的图像。
为了评估图像分类网络在自然图像上的表现,研究人员通常使用三个主要数据集来比较性能:
-
Cifar-10 数据集包含 60,000 张小图像(32x32),仅分为 10 类,您可以轻松下载:
wget https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz -P /sharedfiles tar xvzf /sharedfiles/cifar-10-python.tar.gz -C /sharedfiles/下面是每个类别的一些示例图像:
![自然图像数据集]()
Cifar 10 数据集类别及样本
www.cs.toronto.edu/~kriz/cifar.html -
Cifar-100 数据集包含 60,000 张图像,分为 100 类和 20 个超级类别
-
ImageNet 数据集包含 120 万张图像,标注了广泛的类别(1,000 类)。由于 ImageNet 仅供非商业用途,您可以下载 Food 101 数据集,该数据集包含 101 种餐食类别,每个类别有 1,000 张图像:
wget http://data.vision.ee.ethz.ch/cvl/food-101.tar.gz -P /sharedfiles tar xvzf food-101.tar.gz -C /sharedfiles/
在介绍残差架构之前,让我们讨论两种提高分类网络准确度的方法:批量归一化和全局平均池化。
批量归一化
更深的网络,超过 100 层,可以帮助图像分类多个类别。深度网络的主要问题是确保输入流以及梯度能够从网络的一端有效传播到另一端。
然而,网络中的非线性部分饱和,梯度变为零并不罕见。此外,网络中的每一层都必须适应其输入分布的持续变化,这一现象被称为内部协变量偏移。
已知,网络训练时,输入数据经过线性处理以使均值为零、方差为单位(称为网络输入归一化)能加速训练,并且每个输入特征应独立归一化,而不是联合归一化。
要规范化网络中每一层的输入,稍微复杂一些:将输入的均值归零会忽略前一层学习到的偏置,而当方差为单位时,问题更加严重。当该层的输入被归一化时,前一层的参数可能会无限增长,而损失保持不变。
因此,对于层输入规范化,批量归一化层在归一化后重新学习尺度和偏置:

它不使用整个数据集,而是使用批次来计算归一化的统计量,并通过移动平均来接近整个数据集的统计信息,同时进行训练。
一个批量归一化层具有以下好处:
-
它减少了不良初始化或过高学习率的影响
-
它提高了网络的准确性
-
它加速了训练
-
它减少了过拟合,起到正则化模型的作用
引入批量归一化层时,可以移除 dropout,增加学习率,并减少 L2 权重规范化。
小心将非线性激活放在 BN 层之后,并去除前一层的偏置:
l = NonlinearityLayer(
BatchNormLayer(
ConvLayer(l_in,
num_filters=n_filters[0],
filter_size=(3,3),
stride=(1,1),
nonlinearity=None,
pad='same',
W=he_norm)
),
nonlinearity=rectify
)
全局平均池化
传统上,分类网络的最后两层是全连接层和 softmax 层。全连接层输出一个等于类别数量的特征数,softmax 层将这些值归一化为概率,且它们的和为 1。
首先,可以将步幅为 2 的最大池化层替换为步幅为 2 的新的卷积层:全卷积网络的表现更好。
其次,也可以移除全连接层。如果最后一个卷积层输出的特征图数量选择为类别数,全球空间平均池化将每个特征图缩减为一个标量值,表示在不同宏观空间位置上类的得分平均值:

残差连接
虽然非常深的架构(具有许多层)表现更好,但它们更难训练,因为输入信号在层与层之间逐渐减弱。有人尝试在多个阶段训练深度网络。
这种逐层训练的替代方法是向网络中添加一个附加连接,跳过一个块的层,称为恒等连接,它将信号传递而不作任何修改,除了经典的卷积层,称为残差,形成一个残差块,如下图所示:

这样的残差块由六层组成。
残差网络是由多个残差块组成的网络。输入经过第一层卷积处理,然后是批量归一化和非线性激活:

例如,对于由两个残差块组成的残差网络,且第一卷积层有八个特征图,输入图像的大小为 28x28,层的输出形状如下:
InputLayer (None, 1, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
ElemwiseSumLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
ElemwiseSumLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
ElemwiseSumLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
ElemwiseSumLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
ElemwiseSumLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
ElemwiseSumLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
GlobalPoolLayer (None, 32)
DenseLayer (None, 10)
输出特征图的数量增加,而每个输出特征图的大小减小:这种技术通过减小特征图大小/增加维度的数量保持每层参数数量不变,这是构建网络时常见的最佳实践。
为了增加维度,在三个不同位置进行了三次维度转换,第一次在第一个残差块之前,第二次在 n 个残差块之后,第三次在 2xn 个残差块之后。每个转换之间,过滤器的数量按数组定义:
# 8 -> 8 -> 16 -> 32
n_filters = {0:8, 1:8, 2:16, 3:32}
维度增加是通过相应残差块的第一层进行的。由于输入的形状与输出不同,简单的恒等连接无法与块层的输出拼接,因此用维度投影代替,以将输出的大小调整为块输出的维度。这样的投影可以通过一个1x1的卷积核,步幅为2来实现:
def residual_block(l, transition=False, first=False, filters=16):
if transition:
first_stride = (2,2)
else:
first_stride = (1,1)
if first:
bn_pre_relu = l
else:
bn_pre_conv = BatchNormLayer(l)
bn_pre_relu = NonlinearityLayer(bn_pre_conv, rectify)
conv_1 = NonlinearityLayer(BatchNormLayer(ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(3,3), stride=first_stride,
nonlinearity=None,
pad='same',
W=he_norm)),nonlinearity=rectify)
conv_2 = ConvLayer(conv_1, num_filters=filters, filter_size=(3,3), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)
# add shortcut connections
if transition:
# projection shortcut, as option B in paper
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(2,2), nonlinearity=None, pad='same', b=None)
elif conv_2.output_shape == l.output_shape:
projection=l
else:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', b=None)
return ElemwiseSumLayer([conv_2, projection])
也有一些变种的残差块被发明出来。
一个宽版(Wide-ResNet)残差块是通过增加每个残差块的输出数量来构建的,当它们到达末端时,这个增加是通过一个倍数来实现的:
n_filters = {0:num_filters, 1:num_filters*width, 2:num_filters*2*width, 3:num_filters*4*width}
一个瓶颈版本通过减少每层的参数数量来创建一个瓶颈,它具有降维效果,实施赫布理论 共同发放的神经元会相互连接,并帮助残差块捕获信号中的特定模式:

瓶颈是同时减少特征图大小和输出数量,而不是像之前的做法那样保持每层参数数量不变:
def residual_bottleneck_block(l, transition=False, first=False, filters=16):
if transition:
first_stride = (2,2)
else:
first_stride = (1,1)
if first:
bn_pre_relu = l
else:
bn_pre_conv = BatchNormLayer(l)
bn_pre_relu = NonlinearityLayer(bn_pre_conv, rectify)
bottleneck_filters = filters / 4
conv_1 = NonlinearityLayer(BatchNormLayer(ConvLayer(bn_pre_relu, num_filters=bottleneck_filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
conv_2 = NonlinearityLayer(BatchNormLayer(ConvLayer(conv_1, num_filters=bottleneck_filters, filter_size=(3,3), stride=first_stride, nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
conv_3 = ConvLayer(conv_2, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)
if transition:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(2,2), nonlinearity=None, pad='same', b=None)
elif first:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', b=None)
else:
projection = l
return ElemwiseSumLayer([conv_3, projection])
现在,完整的三堆残差块网络已经构建完成:
def model(shape, n=18, num_filters=16, num_classes=10, width=1, block='normal'):
l_in = InputLayer(shape=(None, shape[1], shape[2], shape[3]))
l = NonlinearityLayer(BatchNormLayer(ConvLayer(l_in, num_filters=n_filters[0], filter_size=(3,3), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
l = residual_block(l, first=True, filters=n_filters[1])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[1])
l = residual_block(l, transition=True, filters=n_filters[2])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[2])
l = residual_block(l, transition=True, filters=n_filters[3])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[3])
bn_post_conv = BatchNormLayer(l)
bn_post_relu = NonlinearityLayer(bn_post_conv, rectify)
avg_pool = GlobalPoolLayer(bn_post_relu)
return DenseLayer(avg_pool, num_units=num_classes, W=HeNormal(), nonlinearity=softmax)
用于 MNIST 训练的命令:
python train.py --dataset=mnist --n=1 --num_filters=8 --batch_size=500
这带来了 98%的 top-1 精度。
在 Cifar 10 上,残差网络层数超过 100 层时,需要将批量大小减少到 64,以适应 GPU 的内存:
-
对于 ResNet-110(6 x 18 + 2):
python train.py --dataset=cifar10 --n=18 --num_filters=16 --batch_size=64 -
ResNet-164(6 x 27 + 2):
python train.py --dataset=cifar10 --n=27 --num_filters=16 --batch_size=64 -
宽版 ResNet-110:
python train.py --dataset=cifar10 --n=18 --num_filters=16 --width=4 --batch_size=64 -
使用 ResNet-bottleneck-164:
python train.py --dataset=cifar10 --n=18 --num_filters=16 --block=bottleneck --batch_size=64 -
对于 Food-101,我进一步减少了 ResNet 110 的批量大小:
python train.py --dataset=food101 --batch_size=10 --n=18 --num_filters=16
随机深度
由于信号在层间传播时可能在任何一个残差块中出现错误,随机深度的想法是通过随机移除一些残差块并用恒等连接替代,来训练网络的鲁棒性。
首先,由于参数数量较少,训练速度更快。其次,实践证明它具有鲁棒性,并且能提供更好的分类结果:

密集连接
随机深度通过创建直接连接来跳过一些随机的层。更进一步地,除了移除一些随机层外,另一种实现相同功能的方法是为之前的层添加一个身份连接:

一个密集块(密集连接卷积网络)
至于残差块,一个密集连接的卷积网络由重复的密集块组成,以创建一堆层块:

具有密集块的网络(密集连接卷积网络)
这种架构选择遵循了在第十章中看到的相同原则,使用高级 RNN 预测时间序列,带有高速公路网络:身份连接有助于信息在网络中正确传播和反向传播,从而减少了在层数较高时出现的梯度爆炸/消失问题。
在 Python 中,我们将残差块替换为一个密集连接块:
def dense_block(network, transition=False, first=False, filters=16):
if transition:
network = NonlinearityLayer(BatchNormLayer(network), nonlinearity=rectify)
network = ConvLayer(network,network.output_shape[1], 1, pad='same', W=he_norm, b=None, nonlinearity=None)
network = Pool2DLayer(network, 2, mode='average_inc_pad')
network = NonlinearityLayer(BatchNormLayer(network), nonlinearity=rectify)
conv = ConvLayer(network,filters, 3, pad='same', W=he_norm, b=None, nonlinearity=None)
return ConcatLayer([network, conv], axis=1)
另请注意,批量归一化是逐特征进行的,由于每个块的输出已经归一化,因此不需要第二次归一化。用一个简单的仿射层替代批量归一化层,学习连接归一化特征的尺度和偏置即可:
def dense_fast_block(network, transition=False, first=False, filters=16):
if transition:
network = NonlinearityLayer(BiasLayer(ScaleLayer(network)), nonlinearity=rectify)
network = ConvLayer(network,network.output_shape[1], 1, pad='same', W=he_norm, b=None, nonlinearity=None)
network = BatchNormLayer(Pool2DLayer(network, 2, mode='average_inc_pad'))
network = NonlinearityLayer(BiasLayer(ScaleLayer(network)), nonlinearity=rectify)
conv = ConvLayer(network,filters, 3, pad='same', W=he_norm, b=None, nonlinearity=None)
return ConcatLayer([network, BatchNormLayer(conv)], axis=1)
用于训练 DenseNet-40:
python train.py --dataset=cifar10 --n=13 --num_filters=16 --block=dense_fast --batch_size=64
多 GPU
Cifar 和 MNIST 图像仍然较小,低于 35x35 像素。自然图像的训练需要保留图像中的细节。例如,224x224 的输入大小就非常合适,这比 35x35 大了 40 倍。当具有如此输入大小的图像分类网络有几百层时,GPU 内存限制了批次大小,最多只能处理十几张图像,因此训练一个批次需要很长时间。
要在多 GPU 模式下工作:
-
模型参数是共享变量,意味着在 CPU / GPU 1 / GPU 2 / GPU 3 / GPU 4 之间共享,和单 GPU 模式一样。
-
批次被分成四个部分,每个部分被送到不同的 GPU 进行计算。网络输出在每个部分上计算,梯度被反向传播到每个权重。GPU 返回每个权重的梯度值。
-
每个权重的梯度从多个 GPU 拉回到 CPU 并堆叠在一起。堆叠后的梯度代表了整个初始批次的梯度。
-
更新规则应用于批次梯度,并更新共享的模型权重。
请参见下图:

Theano 稳定版本仅支持每个进程一个 GPU,因此在主程序中使用第一个 GPU,并为每个 GPU 启动子进程进行训练。请注意,前述图像中的循环需要同步模型的更新,以避免每个 GPU 在不同步的模型上进行训练。与其自己重新编程,不如使用 Platoon 框架(github.com/mila-udem/platoon),该框架专门用于在一个节点内跨多个 GPU 训练模型。
另外,值得注意的是,将多个 GPU 上的批量归一化均值和方差同步会更加准确。
数据增强
数据增强是提高分类精度的一个非常重要的技术。数据增强通过从现有样本创建新样本来实现,方法是添加一些抖动,例如:
-
随机缩放
-
随机大小裁剪
-
水平翻转
-
随机旋转
-
光照噪声
-
亮度抖动
-
饱和度抖动
-
对比抖动
这将帮助模型在现实生活中常见的不同光照条件下变得更加鲁棒。
模型每一轮都会发现不同的样本,而不是始终看到相同的数据集。
请注意,输入归一化对于获得更好的结果也很重要。
深入阅读
你可以参考以下标题以获得更多见解:
-
密集连接卷积网络,Gao Huang,Zhuang Liu,Kilian Q. Weinberger 和 Laurens van der Maaten,2016 年 12 月
-
代码灵感来源于 Lasagne 仓库:
-
Inception-v4,Inception-ResNet 和残差连接对学习的影响,Christian Szegedy,Sergey Ioffe,Vincent Vanhoucke 和 Alex Alemi,2016
-
图像识别的深度残差学习,Kaiming He,Xiangyu Zhang 和 Shaoqing Ren,Jian Sun,2015
-
重新思考计算机视觉中的 Inception 架构,Christian Szegedy,Vincent Vanhoucke,Sergey Ioffe,Jonathon Shlens 和 Zbigniew Wojna,2015
-
宽残差网络,Sergey Zagoruyko 和 Nikos Komodakis,2016
-
深度残差网络中的恒等映射,Kaiming He,Xiangyu Zhang,Shaoqing Ren 和 Jian Sun,2016 年 7 月
-
网络中的网络,Min Lin,Qiang Chen,Shuicheng Yan,2013
总结
已经提出了新技术来实现最先进的分类结果,如批量归一化、全局平均池化、残差连接和密集块。
这些技术推动了残差网络和密集连接网络的构建。
使用多个 GPU 有助于训练图像分类网络,这些网络具有多个卷积层、大的感受野,并且批量输入的图像在内存使用上较重。
最后,我们研究了数据增强技术如何增加数据集的大小,减少模型过拟合的可能性,并学习更稳健网络的权重。
在下一章中,我们将看到如何利用这些网络的早期层作为特征来构建编码器网络,以及如何反转卷积以重建输出图像,以进行像素级预测。
第八章. 使用编码解码网络进行翻译与解释
编码解码技术在输入和输出属于同一空间时应用。例如,图像分割是将输入图像转换为新图像,即分割掩码;翻译是将字符序列转换为新的字符序列;问答则是以新的字词序列回答输入的字词序列。
为了应对这些挑战,编码解码网络由两部分对称组成:编码网络和解码网络。编码器网络将输入数据编码成一个向量,解码器网络则利用该向量生成输出,例如翻译、回答输入问题、解释或输入句子或输入图像的注释。
编码器网络通常由前几层组成,这些层属于前面章节中介绍的网络类型,但没有用于降维和分类的最后几层。这样一个截断的网络会生成一个多维向量,称为特征,它为解码器提供一个内部状态表示,用于生成输出表示。
本章分解为以下关键概念:
-
序列到序列网络
-
机器翻译应用
-
聊天机器人应用
-
反卷积
-
图像分割应用
-
图像标注应用
-
解码技术的改进
自然语言处理中的序列到序列网络
基于规则的系统正在被端到端神经网络所取代,因为后者在性能上有所提升。
端到端神经网络意味着网络直接通过示例推断所有可能的规则,而无需了解潜在的规则,如语法和词形变化;单词(或字符)直接作为输入喂入网络。输出格式也是如此,输出可以直接是单词索引本身。网络架构通过其系数负责学习这些规则。
适用于自然语言处理(NLP)的端到端编码解码网络架构是序列到序列网络,如以下图所示:

单词索引通过查找表转换为其在嵌入空间中的连续多维值。这一转换,详见第三章,将单词编码为向量,是将离散的单词索引编码到神经网络能够处理的高维空间中的关键步骤。
然后,首先对输入的单词嵌入执行一堆 LSTM 操作,用以编码输入并生成思维向量。第二堆 LSTM 以这个向量作为初始内部状态,并且期望为目标句子中的每个单词生成下一个单词。
在核心部分,是我们经典的 LSTM 单元步骤函数,包含输入、遗忘、输出和单元门:
def LSTM( hidden_size):
W = shared_norm((hidden_size, 4*hidden_size))
U = shared_norm((hidden_size, 4*hidden_size))
b = shared_zeros(4*hidden_size)
params = [W, U, b]
def forward(m, X, h_, C_ ):
XW = T.dot(X, W)
h_U = T.dot(h_, U)
bfr_actv = XW + h_U + b
f = T.nnet.sigmoid( bfr_actv[:, 0:hidden_size] )
i = T.nnet.sigmoid( bfr_actv[:, 1*hidden_size:2*hidden_size] )
o = T.nnet.sigmoid( bfr_actv[:, 2*hidden_size:3*hidden_size] )
Cp = T.tanh( bfr_actv[:, 3*hidden_size:4*hidden_size] )
C = i*Cp + f*C_
h = o*T.tanh( C )
C = m[:, None]*C + (1.0 - m)[:, None]*C_
h = m[:, None]*h + (1.0 - m)[:, None]*h_
h, C = T.cast(h, theano.config.floatX), T.cast(h, theano.config.floatX)
return h, C
return forward, params
一个简单的闭包比一个类更好。没有足够的方法和参数去写一个类。编写类要求添加许多self,并且每个变量之前都要有一个__init__方法。
为了减少计算成本,整个层栈被构建成一个一步函数,并且递归性被添加到整个栈步骤函数的顶部,该步骤函数会为每个时间步生成最后一层的输出。其他一些实现让每一层都独立递归,这样效率要低得多(慢于两倍以上)。
在X输入的顶部,使用一个掩码变量m,当设置为零时,停止递归:当没有更多数据时,隐藏状态和单元状态保持不变(掩码值为零)。由于输入是批量处理的,因此每个批次中的句子可能有不同的长度,并且借助掩码,所有批次中的句子都可以并行处理,步数与最大句子长度相同。递归会在批次中每行的不同位置停止。
类的闭包是因为模型不能像先前示例那样直接应用于某些符号输入变量:实际上,模型是应用于递归循环内的序列(使用扫描操作符)。因此,在许多高级深度学习框架中,每一层都被设计为一个模块,暴露出前向/反向方法,可以添加到各种架构中(并行分支和递归),正如本示例所示。
编码器/解码器的完整栈步骤函数,放置在它们各自的递归循环内,可以设计如下:
def stack( voca_size, hidden_size, num_layers, embedding=None, target_voca_size=0):
params = []
if embedding == None:
embedding = shared_norm( (voca_size, hidden_size) )
params.append(embedding)
layers = []
for i in range(num_layers):
f, p = LSTM(hidden_size)
layers.append(f)
params += p
def forward( mask, inputs, h_, C_, until_symbol = None):
if until_symbol == None :
output = embedding[inputs]
else:
output = embedding[T.cast( inputs.argmax(axis=-1), "int32" )]
hos = []
Cos = []
for i in range(num_layers):
hs, Cs = layersi
hos.append(hs)
Cos.append(Cs)
output = hs
if target_voca_size != 0:
output_embedding = shared_norm((hidden_size, target_voca_size))
params.append(output_embedding)
output = T.dot(output, output_embedding)
outputs = (T.cast(output, theano.config.floatX),T.cast(hos, theano.config.floatX),T.cast(Cos, theano.config.floatX))
if until_symbol != None:
return outputs, theano.scan_module.until( T.eq(output.argmax(axis=-1)[0], until_symbol) )
return outputs
return forward, params
第一部分是将输入转换为嵌入空间。第二部分是 LSTM 层的堆栈。对于解码器(当target_voca_size != 0时),添加了一个线性层来计算输出。
现在我们有了我们的编码器/解码器步骤函数,让我们构建完整的编码器-解码器网络。
首先,编码器-解码器网络必须将输入编码成内部状态表示:
encoderInputs, encoderMask = T.imatrices(2)
h0,C0 = T.tensor3s(2)
encoder, encoder_params = stack(valid_data.source_size, opt.hidden_size, opt.num_layers)
([encoder_outputs, hS, CS], encoder_updates) = theano.scan(
fn = encoder,
sequences = [encoderMask, encoderInputs],
outputs_info = [None, h0, C0])
为了编码输入,编码栈步骤函数会在每个单词上递归地运行。
当outputs_info由三个变量组成时,扫描操作符认为扫描操作的输出由三个值组成。
这些输出来自编码栈步骤函数,并且对应于:
-
栈的输出
-
栈的隐藏状态,以及
-
栈的单元状态,对于输入句子的每个步骤/单词
在outputs_info中,None表示考虑到编码器将产生三个输出,但只有最后两个会被反馈到步骤函数中(h0 -> h_ 和 C0 -> C_)。
由于序列指向两个序列,scan 操作的步骤函数必须处理四个参数。
然后,一旦输入句子被编码成向量,编码器-解码器网络将其解码:
decoderInputs, decoderMask, decoderTarget = T.imatrices(3)
decoder, decoder_params = stack(valid_data.target_size, opt.hidden_size, opt.num_layers, target_voca_size=valid_data.target_size)
([decoder_outputs, h_vals, C_vals], decoder_updates) = theano.scan(
fn = decoder,
sequences = [decoderMask, decoderInputs],
outputs_info = [None, hS[-1], CS[-1]])
params = encoder_params + decoder_params
编码器网络的最后状态hS[-1]和CS[-1]将作为解码器网络的初始隐藏状态和细胞状态输入。
在输出上计算对数似然度与上一章关于序列的内容相同。
对于评估,最后预测的单词必须输入解码器中,以预测下一个单词,这与训练有所不同,在训练中输入和输出序列是已知的:

在这种情况下,outputs_info中的None可以替换为初始值prediction_start,即start标记。由于它不再是None,该初始值将被输入到解码器的步骤函数中,只要它与h0和C0一起存在。scan 操作符认为每个步骤都有三个先前的值输入到解码器函数(而不是像之前那样只有两个)。由于decoderInputs已从输入序列中移除,因此传递给解码器堆栈步骤函数的参数数量仍然是四个:先前预测的输出值将取代输入值。这样,同一个解码器函数可以同时用于训练和预测:
prediction_mask = theano.shared(np.ones(( opt.max_sent_size, 1), dtype="int32"))
prediction_start = np.zeros(( 1, valid_data.target_size), dtype=theano.config.floatX)
prediction_start[0, valid_data.idx_start] = 1
prediction_start = theano.shared(prediction_start)
([decoder_outputs, h_vals, C_vals], decoder_updates) = theano.scan(
fn = decoder,
sequences = [prediction_mask],
outputs_info = [prediction_start, hS[-1], CS[-1]],
non_sequences = valid_data.idx_stop
)
非序列参数valid_data.idx_stop告诉解码器步骤函数,它处于预测模式,这意味着输入不是单词索引,而是其先前的输出(需要找到最大索引)。
同样,在预测模式下,一次预测一个句子(批量大小为1)。当产生end标记时,循环停止,这得益于解码器堆栈步骤函数中的theano.scan_module.until输出,之后无需再解码更多单词。
用于翻译的 Seq2seq
序列到序列(Seq2seq)网络的第一个应用是语言翻译。
该翻译任务是为计算语言学协会(ACL)的会议设计的,数据集 WMT16 包含了不同语言的新闻翻译。此数据集的目的是评估新的翻译系统或技术。我们将使用德英数据集。
-
首先,预处理数据:
python 0-preprocess_translations.py --srcfile data/src-train.txt --targetfile data/targ-train.txt --srcvalfile data/src-val.txt --targetvalfile data/targ-val.txt --outputfile data/demo First pass through data to get vocab... Number of sentences in training: 10000 Number of sentences in valid: 2819 Source vocab size: Original = 24995, Pruned = 24999 Target vocab size: Original = 35816, Pruned = 35820 (2819, 2819) Saved 2819 sentences (dropped 181 due to length/unk filter) (10000, 10000) Saved 10000 sentences (dropped 0 due to length/unk filter) Max sent length (before dropping): 127 -
训练
Seq2seq网络:python 1-train.py --dataset translation初看之下,你会注意到每个周期的 GPU 时间是445.906425953,因此比 CPU 快十倍(4297.15962195)。
-
训练完成后,将英语句子翻译成德语,加载已训练的模型:
python 1-train.py --dataset translation --model model_translation_e100_n2_h500
用于聊天机器人的 Seq2seq
序列到序列网络的第二个目标应用是问答系统或聊天机器人。
为此,下载 Cornell 电影对话语料库并进行预处理:
wget http://www.mpi-sws.org/~cristian/data/cornell_movie_dialogs_corpus.zip -P /sharedfiles/
unzip /sharedfiles/cornell_movie_dialogs_corpus.zip -d /sharedfiles/cornell_movie_dialogs_corpus
python 0-preprocess_movies.py
该语料库包含大量富含元数据的虚构对话,数据来自原始电影剧本。
由于源语言和目标语言的句子使用相同的词汇表,解码网络可以使用与编码网络相同的词嵌入:
if opt.dataset == "chatbot":
embeddings = encoder_params[0]
对于chatbot数据集,相同的命令也适用:
python 1-train.py --dataset chatbot # training
python 1-train.py --dataset chatbot --model model_chatbot_e100_n2_h500 # answer my question
提高序列到序列网络的效率
在聊天机器人示例中,第一个值得注意的有趣点是输入序列的反向顺序:这种技术已被证明能改善结果。
对于翻译任务,使用双向 LSTM 来计算内部状态是非常常见的,正如在第五章中所看到的,使用双向 LSTM 分析情感:两个 LSTM,一个按正向顺序运行,另一个按反向顺序运行,两个并行处理序列,它们的输出被连接在一起:

这种机制能够更好地捕捉给定未来和过去的信息。
另一种技术是注意力机制,这是下一章的重点。
最后,精细化技术已经开发并在二维 Grid LSTM 中进行了测试,这与堆叠 LSTM 相差不大(唯一的区别是在深度/堆叠方向上的门控机制):

Grid 长短期记忆
精细化的原则是也在输入句子上按两种顺序运行堆栈,顺序进行。这个公式的思想是让编码器网络在正向编码之后重新访问或重新编码句子,并隐式地捕捉一些时间模式。此外,请注意,二维网格提供了更多可能的交互作用来进行这种重新编码,在每个预测步骤重新编码向量,使用之前输出的单词作为下一个预测单词的方向。所有这些改进与更大的计算能力有关,对于这个重新编码器网络,其复杂度为O(n m)(n和m分别表示输入和目标句子的长度),而对于编码-解码网络来说,其复杂度为O(n+m)。
所有这些技术都有助于降低困惑度。当模型训练时,还可以考虑使用束搜索算法,该算法会在每个时间步跟踪多个预测及其概率,而不是仅跟踪一个,以避免一个错误的预测排名第一时导致后续错误预测。
图像的反卷积
在图像的情况下,研究人员一直在寻找作为编码卷积逆操作的解码操作。
第一个应用是对卷积网络的分析与理解,如在第二章中所示,使用前馈网络分类手写数字,它由卷积层、最大池化层和修正线性单元组成。为了更好地理解网络,核心思想是可视化图像中对于网络某个单元最具判别性的部分:在高层特征图中的一个神经元被保持为非零,并且从该激活信号开始,信号会反向传播回二维输入。
为了通过最大池化层重建信号,核心思想是在正向传递过程中跟踪每个池化区域内最大值的位置。这种架构被称为DeConvNet,可以表示为:

可视化和理解卷积网络
信号会被反向传播到在正向传递过程中具有最大值的位置。
为了通过 ReLU 层重建信号,已提出了三种方法:
-
反向传播仅反向传播到那些在正向传递过程中为正的位置。
-
反向 DeConvNet仅反向传播正梯度
-
引导反向传播仅反向传播到满足两个先前条件的位置:在正向传递过程中输入为正,并且梯度为正。
这些方法在下图中进行了说明:

从第一层的反向传播给出了各种类型的滤波器:

然而,从网络的更高层开始,引导反向传播给出了更好的结果:

也可以将反向传播条件化为输入图像,这样将激活多个神经元,并从中应用反向传播,以获得更精确的输入可视化:

反向传播也可以应用于原始输入图像,而不是空白图像,这一过程被谷歌研究命名为Inceptionism,当反向传播用于增强输出概率时:

但反卷积的主要目的是用于场景分割或图像语义分析,其中反卷积被学习的上采样卷积所替代,如SegNet 网络中所示:

SegNet:一种用于图像分割的深度卷积编码器-解码器架构
在反卷积过程中,每一步通常会将较低输入特征与当前特征进行连接,以进行上采样。
DeepMask 网络采取一种混合方法,仅对包含对象的补丁进行反卷积。为此,它在包含对象的 224x224 输入补丁(平移误差±16 像素)上进行训练,而不是完整的图像:

学习分割对象候选
编码器(VGG-16)网络的卷积层有一个 16 倍的下采样因子,导致特征图为 14x14。
一个联合学习训练两个分支,一个用于分割,一个用于评分,判断补丁中对象是否存在、是否居中以及是否在正确的尺度上。
相关分支是语义分支,它将 14x14 特征图中的对象上采样到 56x56 的分割图。上采样是可能的,如果:
-
一个全连接层,意味着上采样图中的每个位置都依赖于所有特征,并且具有全局视野来预测值。
-
一个卷积(或局部连接层),减少了参数数量,但也通过部分视图预测每个位置的分数。
-
一种混合方法,由两个线性层组成,二者之间没有非线性,旨在执行降维操作,如前图所示
输出掩膜随后通过一个简单的双线性上采样层被上采样回原始的 224x224 补丁维度。
为了处理完整的输入图像,可以将全连接层转换为卷积层,卷积核大小等于全连接层的输入大小,并使用相同的系数,这样网络在应用到完整图像时就变成了完全卷积的网络,步长为 16。
随着序列到序列网络通过双向重新编码机制得到改进,SharpMask方法通过在等效尺度上使用输入卷积特征来改善上采样反卷积过程的锐度:

学习细化对象分割
而 SegNet 方法仅通过跟踪最大池化索引产生的上采样图来学习反卷积,SharpMask 方法直接重用输入特征图,这是一种非常常见的粗到细方法。
最后,请记住,通过应用条件随机场(CRF)后处理步骤,可以进一步改善结果,无论是对于一维输入(如文本),还是二维输入(如分割图像)。
多模态深度学习
为了进一步开放可能的应用,编码-解码框架可以应用于不同的模态,例如,图像描述。
图像描述是用文字描述图像的内容。输入是图像,通常通过深度卷积网络编码成一个思想向量。
用于描述图像内容的文本可以从这个内部状态向量中生成,解码器采用相同的 LSTM 网络堆栈,就像 Seq2seq 网络一样:

进一步阅读
请参考以下主题以获取更深入的见解:
-
基于神经网络的序列到序列学习,Ilya Sutskever,Oriol Vinyals,Quoc V. Le,2014 年 12 月
-
使用 RNN 编码器-解码器的短语表示学习用于统计机器翻译,Kyunghyun Cho,Bart van Merrienboer,Caglar Gulcehre,Dzmitry Bahdanau,Fethi Bougares,Holger Schwenk,Yoshua Bengio,2014 年 9 月
-
通过联合学习对齐与翻译的神经机器翻译,Dzmitry Bahdanau,Kyunghyun Cho,Yoshua Bengio,2016 年 5 月
-
神经对话模型,Oriol Vinyals,Quoc Le,2015 年 7 月
-
快速而强大的神经网络联合模型用于统计机器翻译,Jacob Devlin,Rabih Zbib,Zhongqiang Huang,Thomas Lamar,Richard Schwartz,John Mkahoul,2014 年
-
SYSTRAN 的纯神经机器翻译系统,Josep Crego,Jungi Kim,Guillaume Klein,Anabel Rebollo,Kathy Yang,Jean Senellart,Egor Akhanov,Patrice Brunelle,Aurelien Coquard,Yongchao Deng,Satoshi Enoue,Chiyo Geiss,Joshua Johanson,Ardas Khalsa,Raoum Khiari,Byeongil Ko,Catherine Kobus,Jean Lorieux,Leidiana Martins,Dang-Chuan Nguyen,Alexandra Priori,Thomas Riccardi,Natalia Segal,Christophe Servan,Cyril Tiquet,Bo Wang,Jin Yang,Dakun Zhang,Jing Zhou,Peter Zoldan,2016 年
-
Blue:一种自动评估机器翻译的方法,Kishore Papineni,Salim Roukos,Todd Ward,Wei-Jing Zhu,2002 年
-
ACL 2016 翻译任务
-
变色龙在假想对话中的应用:一种理解对话中文本风格协调的新方法,Cristian Danescu-NiculescuMizil 和 Lillian Lee,2011,见:
research.googleblog.com/2015/06/inceptionism-going-deeper-into-neural.html -
通过深度卷积网络和完全连接的 CRFs 进行语义图像分割,Liang-Chieh Chen,George Papandreou,Iasonas Kokkinos,Kevin Murphy,Alan L.,Yuille,2014 年
-
SegNet:一种用于图像分割的深度卷积编码器-解码器架构,Vijay Badrinarayanan,Alex Kendall,Roberto Cipolla,2016 年 10 月
-
R-FCN:基于区域的全卷积网络进行物体检测,Jifeng Dai,Yi Li,Kaiming He,Jian Sun,2016 年
-
学习分割物体候选框,Pedro O. Pinheiro,Ronan Collobert,Piotr Dollar,2015 年 6 月
-
学习优化物体分割,Pedro O. Pinheiro,Tsung-Yi Lin,Ronan Collobert,Piotr Dollàr,2016 年 3 月
-
可视化与理解卷积网络,Matthew D Zeiler,Rob Fergus,2013 年 11 月
-
展示与讲述:神经图像标题生成器,Oriol Vinyals,Alexander Toshev,Samy Bengio,Dumitru Erhan,2014 年
摘要
至于爱情,头到脚的姿势提供了令人兴奋的新可能性:编码器和解码器网络使用相同的层堆栈,但方向相反。
尽管它没有为深度学习提供新的模块,但编码-解码技术非常重要,因为它使得网络能够进行“端到端”训练,也就是说,直接将输入和相应的输出喂入网络,而不需要为网络指定任何规则或模式,也不需要将编码训练和解码训练拆分成两个独立的步骤。
虽然图像分类是一个一对一的任务,情感分析是一个多对一的任务,但编码-解码技术展示了多对多的任务,比如翻译或图像分割。
在下一章中,我们将介绍一种注意力机制,它赋予编码-解码架构专注于输入的某些部分,以便生成更准确的输出的能力。
第九章. 使用注意力机制选择相关的输入或记忆
本章介绍了一种注意力机制,通过这种机制,神经网络能够通过专注于输入或记忆的相关部分来提升其性能。
通过这种机制,翻译、注释、解释和分割等,在前一章中看到的,都能获得更高的准确性。
神经网络的输入和输出也可以与读取和写入外部记忆相关联。这些网络,记忆网络,通过外部记忆增强,并能够决定存储或检索哪些信息,以及从哪里存储或检索。
在本章中,我们将讨论:
-
注意力机制
-
对齐翻译
-
图像中的焦点
-
神经图灵机
-
记忆网络
-
动态记忆网络
可微分的注意力机制
在翻译句子、描述图像内容、注释句子或转录音频时,自然的做法是一次专注于输入句子或图像的某一部分,在理解该部分并转换后,再转向下一部分,按照一定的顺序进行全局理解。
例如,在德语中,在某些条件下,动词出现在句子的末尾。因此,在翻译成英语时,一旦主语被读取和翻译,好的机器翻译神经网络可以将注意力转向句子末尾以找到动词并将其翻译成英语。这种将输入位置与当前输出预测匹配的过程是通过注意力机制实现的。
首先,让我们回到设计了 softmax 层的分类网络(见 第二章, 使用前馈网络分类手写数字),该层输出一个非负权重向量
,对于输入 X,该向量的和为1:

然后:

分类的目标是使
尽可能接近1(对于正确的类别k),并对其他类别接近零。
但是
是一个概率分布,也可以作为一个权重向量,用来关注在位置k的记忆向量的某些值
:

如果权重集中在位置k,则返回
。根据权重的锐度,输出将更清晰或更模糊。
在特定位置处理向量m值的这个机制就是注意力机制:也就是说,它是线性的、可微的,并且具有反向传播梯度下降,用于特定任务的训练。
更好的使用注意力机制进行翻译
注意力机制的应用范围非常广泛。为了更好地理解,首先让我们通过机器翻译的例子来说明它。注意力机制对齐源句子和目标句子(预测翻译),并避免长句子的翻译退化:

在上一章中,我们讨论了使用编码器-解码器框架的机器翻译,编码器提供给解码器一个固定长度的编码向量c。有了注意力机制,如果每一步的编码循环网络产生一个隐藏状态h i,那么在每个解码时间步t提供给解码器的向量将是可变的,并由以下公式给出:

使用
通过 softmax 函数产生的对齐系数:

根据解码器的先前隐藏状态
和编码器的隐藏状态
,前一个解码器隐藏状态与每个编码器隐藏状态之间的嵌入式点积产生一个权重,描述它们应该如何匹配:

经过几个训练周期后,模型通过聚焦输入的某个部分来预测下一个词:

为了更好地学习对齐,可以使用数据集中存在的对齐注释,并为由注意力机制产生的权重添加交叉熵损失,这可以在训练的前几个周期中使用。
更好的使用注意力机制对图像进行注释
相同的注意力机制可以应用于图像注释或音频转录任务。
对于图像,注意力机制在每个预测时间步聚焦于特征的相关部分:

展示、关注和讲述:带有视觉注意力的神经图像字幕生成
让我们看一下经过训练的模型在图像上的注意力点:

(Show, Attend and Tell: Neural Image Caption Generation with Visual Attention,Kelvin Xu 等,2015 年)
在神经图灵机中存储和检索信息
注意力机制可以作为在记忆增强网络中访问部分记忆的方式。
神经图灵机中的记忆概念受到了神经科学和计算机硬件的启发。
RNN 的隐藏状态用来存储信息,但它无法存储足够大的数据量并进行检索,即使 RNN 已被增强了一个记忆单元,如 LSTM 中的情况。
为了解决这个问题,神经图灵机(NTM)首先设计了一个外部记忆库和读/写头,同时保留了通过梯度下降进行训练的神奇之处。
读取记忆库是通过对变量记忆库的注意力进行控制,类似于前面例子中对输入的注意力:

这可以通过以下方式进行说明:

而写入记忆库则通过另一个注意力机制将我们的新值分配到记忆的一部分:


描述需要存储的信息,并且
是需要删除的信息,并且它们的大小与记忆库相同:

读写头的设计类似于硬盘,其移动性由注意权重
和
来想象。
记忆
将在每个时间步演变,就像 LSTM 的单元记忆一样;但是,由于记忆库设计得很大,网络倾向于在每个时间步将传入的数据进行存储和组织,干扰比任何经典 RNN 都要小。
与记忆相关的处理过程自然是通过一个递归神经网络(RNN)在每个时间步充当控制器来驱动的:

控制器网络在每个时间步输出:
-
每个读/写头的定位或注意系数
-
写头需要存储或删除的值
原始的 NTM 提出了两种定义头部定位(也称为寻址)的方法,定义为权重
:
-
基于内容的定位,用于将相似的内容放置在记忆的同一区域,这对于检索、排序或计数任务非常有用:
![在神经图灵机中存储和检索信息]()
-
基于位置的定位,它依赖于头部的先前位置,可以在复制任务中使用。一个门控
定义了先前权重与新生成权重之间的影响,以计算头部的位置。一个偏移权重
定义了相对于该位置的位移量。
最后,一个锐化权重
减少了头部位置的模糊:


所有操作都是可微分的。
可能不止两个头,特别是在一些任务中,如对两个存储值的加法运算,单个读取头将会受到限制。
这些 NTM 在任务中表现出比 LSTM 更强的能力,比如从输入序列中检索下一个项目、重复输入序列多次或从分布中采样。
记忆网络
给定一些事实或故事来回答问题或解决问题,促使设计出一种新型网络——记忆网络。在这种情况下,事实或故事被嵌入到一个记忆库中,就像它们是输入一样。为了完成需要排序事实或在事实之间创建转换的任务,记忆网络使用递归推理过程,在多个步骤或跳跃中操作记忆库。
首先,查询或问题q被转换成常量输入嵌入:

而在每个推理步骤中,回答问题的事实X被嵌入到两个记忆库中,其中嵌入系数是时间步长的函数:

为了计算注意力权重:

并且:

选择了带有注意力机制:

每个推理时间步骤的输出随后与身份连接组合,如前所述,以提高递归效率:

一个线性层和分类 softmax 层被添加到最后的
:

具有动态记忆网络的情节记忆
另一种设计通过动态记忆网络被引入。首先,N 个事实与分隔符令牌连接在一起,然后通过 RNN 编码:RNN 在每个分隔符处的输出
被用作输入嵌入。这样的编码方式更加自然,同时也保留了时间依赖性。问题也通过 RNN 进行编码以生成向量q。
其次,记忆库被替换为情节记忆,依赖于混合了 RNN 的注意力机制,以便保留事实之间的时间依赖关系:

门控
由多层感知器提供,依赖于推理的前一个状态
、问题和输入嵌入
作为输入。
推理过程与 RNN 相同:

以下图片展示了输入和输出之间的相互作用,以计算情节记忆:

问我任何事:自然语言处理的动态记忆网络
为了对这些网络进行基准测试,Facebook 研究通过合成 bAbI 数据集,使用 NLP 工具为一些随机建模的故事创建事实、问题和答案。该数据集由不同的任务组成,用于测试不同的推理技能,例如基于时间、大小或位置的单个、两个或三个事实推理、计数、列举或理解论点之间的关系、否定、动机以及路径查找。
至于机器翻译中的引导对齐,当数据集也包含了导致答案的事实注释时,也可以使用监督训练:
-
注意力机制
-
当推理循环停止时,生成一个停止标记,判断使用的事实数量是否足够回答问题
进一步阅读
您可以参考以下主题以获取更多见解:
-
问我任何事:自然语言处理的动态记忆网络,Ankit Kumar,Ozan Irsoy,Peter Ondruska,Mohit Iyyer,James Bradbury,Ishaan Gulrajani,Victor Zhong,Romain Paulus,Richard Socher,2015 年
-
注意力与增强型循环神经网络,Chris Olah,Shan Carter,2016 年 9 月
distill.pub/2016/augmented-rnns/ -
面向话题的神经机器翻译的引导对齐训练,陈文虎,Evgeny Matusov,Shahram Khadivi,Jan-Thorsten Peter,2016 年 7 月
-
展示、注意与叙述:具有视觉注意力的神经图像字幕生成,Kelvin Xu,Jimmy Ba,Ryan Kiros,Kyunghyun Cho,Aaron Courville,Ruslan Salakhutdinov,Richard Zemel,Yoshua Bengio,2015 年 2 月
-
迈向 AI 完全问题回答:一组先决条件玩具任务,Jason Weston,Antoine Bordes,Sumit Chopra,Alexander M. Rush,Bart van Merriënboer,Armand Joulin,Tomas Mikolov,2015 年
-
记忆网络,Jason Weston,Sumit Chopra,Antoine Bordes,2014 年
-
端到端记忆网络,Sainbayar Sukhbaatar,Arthur Szlam,Jason Weston,Rob Fergus,2015 年
-
神经图灵机,Alex Graves,Greg Wayne,Ivo Danihelka,2014 年
-
深度视觉-语义对齐用于生成图像描述,Andrej Karpathy,Li Fei-Fei,2014
总结
注意力机制是帮助神经网络选择正确信息并集中精力以生成正确输出的聪明选择。它可以直接应用于输入或特征(输入经过几层处理)。在翻译、图像标注和语音识别等任务中,尤其是在输入维度很重要时,准确率得到了提升。
注意力机制引入了增强了外部记忆的新型网络,作为输入/输出,可以从中读取或写入。这些网络已被证明在问答挑战中非常强大,几乎所有自然语言处理任务都可以转化为此类任务:标注、分类、序列到序列,或问答任务。
在下一章,我们将介绍更高级的技巧及其在更一般的递归神经网络中的应用,以提高准确性。
第十章:使用高级 RNN 预测时间序列
本章涵盖了递归神经网络的高级技术。
在第二章中看到的技术,使用前馈网络分类手写数字,对于前馈网络,例如通过增加更多层次或添加 Dropout 层等,已经成为递归网络面临的挑战,并且需要一些新的设计原则。
由于增加新层会加剧消失/爆炸梯度问题,一种基于身份连接的新技术,如在第七章中所述,使用残差网络分类图像,已证明能够提供最先进的结果。
涵盖的主题包括:
-
变分 RNN
-
堆叠 RNN
-
深度过渡 RNN
-
高速公路连接及其在 RNN 中的应用
RNN 的 Dropout
Dropout 在神经网络中的应用一直是研究的主题,因为简单地将 Dropout 应用于递归连接会引入更多的不稳定性和训练 RNN 的困难。
一种解决方案已经被发现,它源自变分贝叶斯网络理论。最终的思想非常简单, consiste of 保持相同的 Dropout 掩码用于整个 RNN 训练序列,如下图所示,并在每个新序列上生成新的 Dropout 掩码:

这种技术被称为变分 RNN。对于前图中具有相同箭头的连接,我们将为整个序列保持噪声掩码不变。
为此,我们将引入符号变量_is_training和_noise_x,在训练过程中为输入、输出和递归连接添加随机(变分)噪声(Dropout):
_is_training = T.iscalar('is_training')
_noise_x = T.matrix('noise_x')
inputs = apply_dropout(_is_training, inputs, T.shape_padright(_noise_x.T))
RNN 的深度方法
深度学习的核心原理是通过增加更多层次来提升网络的表示能力。对于 RNN,增加层数有两种可能的方式:
- 第一个方法被称为堆叠或堆叠递归网络,其中第一个递归网络的隐藏层输出作为第二个递归网络的输入,依此类推,多个递归网络层叠在一起:
![RNN 的深度方法]()
对于深度d和T时间步长,输入与输出之间的最大连接数为d + T – 1:
-
第二种方法是深度过渡网络,它通过向递归连接中添加更多层次来实现:
![RNN 的深度方法]()
图 2
在这种情况下,输入与输出之间的最大连接数为d x T,已被证明更加强大。
两种方法都能提供更好的结果。
然而,在第二种方法中,随着层数的增加,训练变得更加复杂和不稳定,因为信号会更快地消失或爆炸。我们将在稍后通过处理递归高速公路连接的原理来解决这个问题。
首先,像往常一样,将作为词汇索引值数组的单词序列,维度为(batch_size, num_steps),嵌入到维度为(num_steps, batch_size, hidden_size)的输入张量中:
embedding = shared_uniform(( config.vocab_size,config.hidden_size), config.init_scale)
params = [embedding]
inputs = embedding[_input_data.T]
符号输入变量_lr使得在训练过程中可以减少学习率:
_lr = theano.shared(cast_floatX(config.learning_rate), 'lr')
我们从第一种方法开始,即堆叠递归网络。
堆叠递归网络
要堆叠递归网络,我们将下一个递归网络的隐藏层连接到前一个递归网络的输入:

当层数为一时,我们的实现就是前一章中的递归网络。
首先,我们在简单的 RNN 模型中实现了 dropout:
def model(inputs, _is_training, params, batch_size, hidden_size, drop_i, drop_s, init_scale, init_H_bias):
noise_i_for_H = get_dropout_noise((batch_size, hidden_size), drop_i)
i_for_H = apply_dropout(_is_training, inputs, noise_i_for_H)
i_for_H = linear.model(i_for_H, params, hidden_size,
hidden_size, init_scale, bias_init=init_H_bias)
# Dropout noise for recurrent hidden state.
noise_s = get_dropout_noise((batch_size, hidden_size), drop_s)
def step(i_for_H_t, y_tm1, noise_s):
s_lm1_for_H = apply_dropout(_is_training,y_tm1, noise_s)
return T.tanh(i_for_H_t + linear.model(s_lm1_for_H,
params, hidden_size, hidden_size, init_scale))
y_0 = shared_zeros((batch_size, hidden_size), name='h0')
y, _ = theano.scan(step, sequences=i_for_H, outputs_info=[y_0], non_sequences = [noise_s])
y_last = y[-1]
sticky_state_updates = [(y_0, y_last)]
return y, y_0, sticky_state_updates
我们在 LSTM 模型中做同样的事情:
def model(inputs, _is_training, params, batch_size, hidden_size, drop_i, drop_s, init_scale, init_H_bias, tied_noise):
noise_i_for_i = get_dropout_noise((batch_size, hidden_size), drop_i)
noise_i_for_f = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
noise_i_for_c = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
noise_i_for_o = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
i_for_i = apply_dropout(_is_training, inputs, noise_i_for_i)
i_for_f = apply_dropout(_is_training, inputs, noise_i_for_f)
i_for_c = apply_dropout(_is_training, inputs, noise_i_for_c)
i_for_o = apply_dropout(_is_training, inputs, noise_i_for_o)
i_for_i = linear.model(i_for_i, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_f = linear.model(i_for_f, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_c = linear.model(i_for_c, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_o = linear.model(i_for_o, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
# Dropout noise for recurrent hidden state.
noise_s = get_dropout_noise((batch_size, hidden_size), drop_s)
if not tied_noise:
noise_s = T.stack(noise_s, get_dropout_noise((batch_size, hidden_size), drop_s),
get_dropout_noise((batch_size, hidden_size), drop_s), get_dropout_noise((batch_size, hidden_size), drop_s))
def step(i_for_i_t,i_for_f_t,i_for_c_t,i_for_o_t, y_tm1, c_tm1, noise_s):
noise_s_for_i = noise_s if tied_noise else noise_s[0]
noise_s_for_f = noise_s if tied_noise else noise_s[1]
noise_s_for_c = noise_s if tied_noise else noise_s[2]
noise_s_for_o = noise_s if tied_noise else noise_s[3]
s_lm1_for_i = apply_dropout(_is_training,y_tm1, noise_s_for_i)
s_lm1_for_f = apply_dropout(_is_training,y_tm1, noise_s_for_f)
s_lm1_for_c = apply_dropout(_is_training,y_tm1, noise_s_for_c)
s_lm1_for_o = apply_dropout(_is_training,y_tm1, noise_s_for_o)
i_t = T.nnet.sigmoid(i_for_i_t + linear.model(s_lm1_for_i, params, hidden_size, hidden_size, init_scale))
f_t = T.nnet.sigmoid(i_for_o_t + linear.model(s_lm1_for_f, params, hidden_size, hidden_size, init_scale))
c_t = f_t * c_tm1 + i_t * T.tanh(i_for_c_t + linear.model(s_lm1_for_c, params, hidden_size, hidden_size, init_scale))
o_t = T.nnet.sigmoid(i_for_o_t + linear.model(s_lm1_for_o, params, hidden_size, hidden_size, init_scale))
return o_t * T.tanh(c_t), c_t
y_0 = shared_zeros((batch_size,hidden_size), name='h0')
c_0 = shared_zeros((batch_size,hidden_size), name='c0')
[y, c], _ = theano.scan(step, sequences=[i_for_i,i_for_f,i_for_c,i_for_o], outputs_info=[y_0,c_0], non_sequences = [noise_s])
y_last = y[-1]
sticky_state_updates = [(y_0, y_last)]
return y, y_0, sticky_state_updates
运行我们的堆叠网络:
python train_stacked.py --model=rnn
python train_stacked.py --model=lstm
对于 RNN,我们得到了 15,203,150 个参数,在 CPU 上的速度为 326 每秒字数(WPS),在 GPU 上的速度为 4,806 WPS。
对于 LSTM,参数数量为 35,882,600,在 GPU 上的速度为 1,445 WPS。
堆叠 RNN 没有收敛,正如我们预想的那样:随着深度增加,梯度消失/爆炸问题加剧。
LSTM,旨在减少此类问题,在堆叠时的收敛效果远好于单层网络。
深度转移递归网络
与堆叠递归网络相反,深度转移递归网络通过在递归连接中增加更多的层次或微时间步,来沿时间方向增加网络的深度。
为了说明这一点,让我们回到递归网络中转移/递归连接的定义:它以前一状态
和时间步t时的输入数据
为输入,预测其新状态
。
在深度转移递归网络(图 2)中,递归转移通过多个层次开发,直到递归深度L:初始状态被设置为最后一个转移的输出:

此外,在转移中,计算多个状态或步骤:

最终状态是转移的输出:

高速公路网络设计原理
在转移连接中增加更多层会在长时间依赖中增加梯度消失或爆炸问题,特别是在反向传播过程中。
在 第四章,使用递归神经网络生成文本 中,已经介绍了 LSTM 和 GRU 网络作为解决方案来应对这个问题。二阶优化技术也有助于克服这个问题。
一个更一般的原理,基于 恒等连接,用于改善深度网络的训练,第七章,使用残差网络分类图像,也可以应用于深度过渡网络。
这是理论上的原理:
给定一个输入 x 到隐藏层 H,并带有权重
:

一个高速公路网络设计包括将原始输入信息(通过恒等层)添加到一层或一组层的输出,作为快捷通道:
y = x
两个混合门,变换门
和 传递门,
学会调节隐藏层中变换的影响,以及允许通过的原始信息量:

通常,为了减少总参数量以便加速训练网络,carry 门被设置为 transform 门的互补:

递归高速公路网络
因此,让我们将高速公路网络设计应用于深度过渡递归网络,从而定义 递归高速公路网络(RHN),并根据给定的过渡输入预测输出
和
:

过渡是通过多个步骤的高速公路连接构建的:


这里的变换门如下:

为了减少权重数量,carry 门被作为 transform 门的互补:

为了在 GPU 上更快地计算,最好将不同时间步长的输入上的线性变换通过单次大矩阵乘法计算,即一次性计算所有时间步长的输入矩阵
和
,因为 GPU 会更好地并行化这些操作,并将这些输入提供给递归:
y_0 = shared_zeros((batch_size, hidden_size))
y, _ = theano.scan(deep_step_fn, sequences = [i_for_H, i_for_T],
outputs_info = [y_0], non_sequences = [noise_s])
每个步骤之间有深度过渡:
def deep_step_fn(i_for_H_t, i_for_T_t, y_tm1, noise_s):
s_lm1 = y_tm1
for l in range(transition_depth):
if l == 0:
H = T.tanh(i_for_H_t + linear(s_lm1, params, hidden_size, hidden_size, init_scale))
Tr = T.nnet.sigmoid(i_for_T_t + linear(s_lm1, params, hidden_size, hidden_size, init_scale))
else:
H = T.tanh(linear(s_lm1, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias))
Tr = T.nnet.sigmoid(linear(s_lm1, params, hidden_size, hidden_size, init_scale, bias_init=init_T_bias))
s_l = H * Tr + s_lm1 * ( 1 - Tr )
s_lm1 = s_l
y_t = s_l
return y_t
RHN 的递归隐藏状态是粘性的(一个批次的最后一个隐藏状态会传递到下一个批次,作为初始隐藏状态使用)。这些状态被保存在一个共享变量中。
让我们运行模式:
python train_stacked.py
堆叠的 RHN 的参数数量为84,172,000,其在 GPU 上的速度为420 wps。
该模型是当前在文本上递归神经网络准确度的最新最先进模型。
深入阅读
您可以参考以下主题以获取更多见解:
-
高速公路网络:
arxiv.org/abs/1505.00387 -
深度门控 LSTM:
arxiv.org/abs/1508.03790 -
学习递归神经网络中的长期记忆:
arxiv.org/abs/1412.7753 -
网格长短期记忆,Nal Kalchbrenner, Ivo Danihelka, Alex Graves
-
Zilly, J, Srivastava, R, Koutnik, J, Schmidhuber, J., 递归高速公路网络,2016
-
Gal, Y, 递归神经网络中丢弃法的理论基础应用,2015。
-
Zaremba, W, Sutskever, I, Vinyals, O, 递归神经网络正则化,2014。
-
Press, O, Wolf, L, 利用输出嵌入改进语言模型,2016。
-
门控反馈递归神经网络:Junyoung Chung, Caglar Gulcehre, Kyunghyun Cho, Yoshua Bengio 2015
-
时钟工作递归神经网络:Jan Koutník, Klaus Greff, Faustino Gomez, Jürgen Schmidhuber 2014
摘要
一种经典的丢弃法可用于提高网络的鲁棒性,避免递归转换的不稳定性和破坏,且可以在递归网络的序列或批次中应用。例如,当应用于单词输入/输出时,相当于从句子中去除相同的单词,将其替换为空值。
深度学习中堆叠层的原则,通过在深度方向堆叠递归网络而不产生负担,能够提高准确性。
将相同的原则应用于递归网络的转换中,会增加消失/爆炸问题,但通过引入具有身份连接的高速公路网络来抵消这一问题。
递归神经网络的高级技术在序列预测中给出了最先进的结果。
第十一章 从环境中学习强化
监督学习和无监督学习描述了训练过程中标签或目标的存在与否。对于代理来说,更自然的学习环境是在正确决策时获得奖励。在复杂环境中,这种奖励,例如正确打网球,可能是多个动作的结果,延迟或累积。
为了优化人工代理从环境中获取的奖励,强化学习(RL)领域出现了许多算法,如 Q 学习或蒙特卡洛树搜索,并且随着深度学习的出现,这些算法已经演变为新的方法,如深度 Q 网络,策略网络,值网络和策略梯度。
我们将首先介绍强化学习框架及其在虚拟环境中的潜在应用。然后,我们将发展其算法及其与深度学习的整合,后者解决了人工智能中最具挑战性的问题。
本章涵盖的要点如下:
-
强化学习
-
模拟环境
-
Q 学习
-
蒙特卡洛树搜索
-
深度 Q 网络
-
策略梯度
-
异步梯度下降
为了简化本章中神经网络的开发,我们将使用 Keras,这是基于我在第五章中介绍的 Theano 之上的高级深度学习库,使用双向 LSTM 分析情感。
强化学习任务
强化学习包括训练一个代理,它只需偶尔从环境中获得反馈,就可以学会在结束时获得最佳反馈。代理执行动作,修改环境的状态。
在环境中导航的动作可以表示为从一个状态到另一个状态的有向边,如下图所示:

一个机器人在真实环境中工作(步行机器人,电机控制等)或虚拟环境中(视频游戏,在线游戏,聊天室等),必须决定哪些动作(或按键)可以获得最大的奖励:

模拟环境
虚拟环境使得能够模拟数以万计甚至百万级的游戏过程,仅需计算成本。为了评估不同强化学习算法的性能,研究界开发了模拟环境。
为了找到能够很好地泛化的解决方案,Open-AI,一个由商业巨头埃隆·马斯克支持的非盈利人工智能研究公司,致力于以有益于全人类的方式小心推广和开发友好的人工智能,已经在其开源模拟环境Open-AI Gym(gym.openai.com/)中收集了一系列强化学习任务和环境,供我们在其中测试自己的方法。在这些环境中,你会找到:
-
来自 Atari 2600 的视频游戏,Atari 公司于 1977 年发布的家庭视频游戏机,封装了来自街机学习环境的模拟器,这是最常见的强化学习基准环境之一:
![模拟环境]()
-
MuJoCo,评估智能体在连续控制任务中的物理模拟器:
![模拟环境]()
-
其他著名游戏,如 Minecraft、足球、Doom 等:
![模拟环境]()
让我们安装 Gym 及其 Atari 2600 环境:
pip install gym
pip install gym[atari]
也可以通过以下方式安装所有环境:
pip install gym[all]
与 Gym 环境交互非常简单,只需使用env.step()方法,给定我们为智能体选择的动作,该方法返回新的状态、奖励,以及游戏是否已结束。
例如,让我们采样一个随机动作:
import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
env.render()
action = env.action_space.sample()
next_state, reward, done, info = env.step(action)
if done:
env.reset()
Gym 还提供了复杂的监控方法,可以录制视频和算法表现。这些记录可以上传到 Open-AI API,与其他算法一起评分。
你也可以看看:
-
3D 赛车模拟器 Torcs(
torcs.sourceforge.net/),比起简单的 Atari 游戏,它在动作的离散化上更小,更加真实,但奖励更稀疏,而且比 MuJoCo 中的连续电机控制动作还少:![模拟环境]()
-
一个名为迷宫的 3D 环境,用于随机生成迷宫:
![模拟环境]()
Q 学习
解决游戏的一个主要方法是 Q 学习方法。为了完全理解这一方法,以下是一个简单的例子,环境的状态数限制为 6,状态0为入口,状态5为出口。在每个阶段,一些动作可以使智能体跳到另一个状态,如下图所示:

当智能体从状态4跳到状态5时,奖励是 100。在其他状态中没有奖励,因为在这个例子中游戏的目标是找到出口。奖励是时间延迟的,智能体必须从状态 0 滚动通过多个状态到达状态 4,才能找到出口。
在这种情况下,Q 学习的任务是学习一个 Q 矩阵,表示状态-动作对的价值:
-
Q 矩阵的每一行对应智能体可能处于的一个状态
-
每一列表示从该状态出发到达的目标状态
代表选择该状态中的行动将如何使我们接近出口的价值。如果从状态i到状态j没有任何行动,我们在 Q 矩阵的位置(i,j)定义为零或负值。如果从状态i到状态j有一个或多个可能的行动,那么 Q 矩阵中的值将被选择来表示状态j如何帮助我们实现目标。
例如,离开状态 3 到状态 0 将使代理远离出口,而离开状态 3 到状态 4 将使我们更接近目标。一个常用的算法,称为贪婪算法,在离散空间中估计Q,由递归的贝尔曼方程给出,已被证明收敛:

在这里,S'是在状态S上执行动作a后的新状态;r定义了从状态S到S'路径上的奖励(在这种情况下为空),
是折扣因子,用于阻止到图中距离太远的状态的动作。多次应用该方程将导致以下 Q 值:

在 Q 学习中,Q代表质量,表示行动获得最佳奖励的能力。由于延迟奖励被折扣,这些值对应于每对(状态,行动)的最大折扣未来奖励。
注意,只要我们知道搜索子树的输出节点的状态值,完整图形的结果就不是必需的:

在这张图中,节点 1 和 3 的值为 10 是最优状态值函数 v(s);也就是说,在完美游戏/最佳路径下的游戏结果。实际上,确切的值函数是未知的,但是是近似的。
这种近似与 DeepMind 算法 AlphaGo 中的蒙特卡洛树搜索(MCTS)结合使用,以击败围棋世界冠军。MCTS 包括在给定策略下对动作进行抽样,从而仅保留当前节点到估计其 Q 值的最可能动作在贝尔曼方程中:

深度 Q 网络
尽管可能的行动数量通常有限(键盘键数或移动数),但可能的状态数量可能极其庞大,搜索空间可能非常庞大,例如,在配备摄像头的机器人在真实环境或现实视频游戏中。自然而然地,我们会使用计算机视觉神经网络,例如我们在第七章中用于分类的那些网络,来代表给定输入图像(状态)的行动价值,而不是一个矩阵:

Q 网络被称为状态-动作值网络,它根据给定的状态预测动作值。为了训练 Q 网络,一种自然的方式是通过梯度下降使其符合贝尔曼方程:

请注意,
被评估并固定,而下降是针对
中的导数计算的,且每个状态的值可以估算为所有状态-动作值的最大值。
在用随机权重初始化 Q 网络后,初始预测是随机的,但随着网络的收敛,给定特定状态的动作将变得越来越可预测,因此对新状态的探索减少。利用在线训练的模型需要强迫算法继续探索:
贪心方法包括以概率 epsilon 做一个随机动作,否则跟随 Q 网络给出的最大奖励动作。这是一种通过试错学习的方式。在经过一定数量的训练轮次后,
会衰减,以减少探索。
训练稳定性
有不同的方法可以在训练过程中改善稳定性。在线训练,即在玩游戏时训练模型,遗忘之前的经验,只考虑最后一条经验,对于深度神经网络来说是根本不稳定的:时间上接近的状态(例如最新的状态)通常是高度相似或相关的,训练时使用最新的状态不容易收敛。
为了避免这种失败,一个可能的解决方案是将经验存储在回放记忆中,或者使用人类游戏记录数据库。批量处理和打乱从回放记忆或人类游戏记录数据库中抽取的随机样本能够实现更稳定的训练,但属于离策略训练。
改善稳定性的第二个解决方案是将参数
的值固定在目标评估
中,并进行数千次更新
,以减少目标值和 Q 值之间的相关性:

通过 n 步 Q 学习,能够更高效地训练,将奖励传播到n个先前的动作,而不是一个:
Q 学习公式:

n 步 Q 学习公式:

在这里,每一步都会受益于n个后续奖励:

训练稳定性和效率的最终解决方案是异步梯度下降,通过多个代理并行执行,在环境的多个实例上进行,并采用不同的探索策略,这样每次梯度更新之间的相关性就更小:每个学习代理在同一台机器的不同线程中运行,与其他代理共享其模型和目标模型参数,但计算环境的不同部分的梯度。并行的行为体学习者具有稳定化效果,支持策略强化,减少训练时间,并在 GPU 或多核 CPU 上表现出可比的性能,这非常棒!
稳定化效果导致更好的数据效率:数据效率通过收敛到期望的训练损失或准确率所需的训练周期(一个周期是完整的训练数据集被算法展示一次)来衡量。总训练时间受数据效率、并行性(线程数或机器数)以及并行性开销(在给定核心数、机器数和算法分布效率的情况下,随着线程数增加呈亚线性增长)影响。
让我们实际看一下。为了实现多个代理探索环境的不同部分,我们将使用 Python 的多进程模块运行多个进程,其中一个进程用于更新模型(GPU),n个进程用于代理进行探索(CPU)。多进程模块的管理器对象控制一个持有 Q 网络权重的服务器进程,以便在进程之间共享。用于存储代理经验并在模型更新时一次性提供的通信通道,通过一个进程安全的队列实现:
from multiprocessing import *
manager = Manager()
weight_dict = manager.dict()
mem_queue = manager.Queue(args.queue_size)
pool = Pool(args.processes + 1, init_worker)
for i in range(args.processes):
pool.apply_async(generate_experience_proc, (mem_queue, weight_dict, i))
pool.apply_async(learn_proc, (mem_queue, weight_dict))
pool.close()
pool.join()
现在,让我们生成经验并将其排入公共队列对象中。
为了这个目的,在每个代理创建其游戏环境时,编译 Q 网络并从管理器加载权重:
env = gym.make(args.game)
load_net = build_networks(observation_shape, env.action_space.n)
load_net.compile(optimizer='rmsprop', loss='mse', loss_weights=[0.5, 1.])
while 'weights' not in weight_dict:
time.sleep(0.1)
load_net.set_weights(weight_dict['weights'])
为了生成一次经验,代理选择一个动作并在其环境中执行:
observation, reward, done, _ = env.step(action)
每个代理的经验都存储在一个列表中,直到游戏结束或列表长度超过n_step,以便使用n 步 Q 学习评估状态-动作值:
if done or counter >= args.n_step:
r = 0.
if not done:
r = value_net.predict(observations[None, ...])[0]
for i in range(counter):
r = n_step_rewards[i] + discount * r
mem_queue.put((n_step_observations[i], n_step_actions[i], r))
偶尔,代理会从学习进程中更新其权重:
load_net.set_weights(weight_dict['weights'])
现在,让我们看看如何更新学习代理中的权重。
带有 REINFORCE 算法的策略梯度
策略梯度(PG)/ REINFORCE 算法的想法非常简单:它在强化学习任务中,重新使用分类损失函数。
让我们记住,分类损失是由负对数似然给出的,使用梯度下降来最小化它时,遵循的是负对数似然相对于网络权重的导数:

这里,y 是选择的动作,
是给定输入 X 和权重
的该动作的预测概率。
REINFORCE 定理引入了强化学习中的等价物,其中 r 是奖励。以下是导数:

表示网络权重相对于预期奖励的导数的无偏估计:

因此,遵循导数将鼓励代理最大化奖励。
这样的梯度下降使我们能够优化我们代理的策略网络:策略
是一个合法动作的概率分布,用于在在线学习期间采样要执行的动作,并且可以通过参数化神经网络进行近似。
在连续案例中特别有用,例如在运动控制中,离散化的动作空间可能导致一些次优伪影,并且在无限动作空间下,无法对动作-值网络 Q 进行最大化。
此外,可以通过递归(LSTM,GRU)增强策略网络,使代理根据多个先前的状态选择其动作。
REINFORCE 定理为我们提供了一个梯度下降方法,用于优化参数化的策略网络。为了鼓励在这种基于策略的情况下进行探索,也可以向损失函数中添加正则化项——策略的熵。
在此策略下,可以计算每个状态的值
:
-
可以通过使用该策略从该状态进行游戏
-
或者,如果通过梯度下降将其参数化为状态价值网络,当前参数作为目标,就像在上一节中看到的带有折扣奖励的状态-动作价值网络一样:
![使用 REINFORCE 算法的策略梯度]()
这个值通常作为强化基准 b 来减少策略梯度估计的方差,Q 值可以作为期望奖励:

REINFORCE 导数中的第一个因子:

被称为动作 a 在状态 s 中的优势。
策略网络和价值网络的梯度下降可以通过我们的并行演员学习器异步执行。
让我们在 Keras 中创建我们的策略网络和状态价值网络,共享它们的第一层:
from keras.models import Model
from keras.layers import Input, Conv2D, Flatten, Dense
def build_networks(input_shape, output_shape):
state = Input(shape=input_shape)
h = Conv2D(16, (8, 8) , strides=(4, 4), activation='relu', data_format="channels_first")(state)
h = Conv2D(32, (4, 4) , strides=(2, 2), activation='relu', data_format="channels_first")(h)
h = Flatten()(h)
h = Dense(256, activation='relu')(h)
value = Dense(1, activation='linear', name='value')(h)
policy = Dense(output_shape, activation='softmax', name='policy')(h)
value_network = Model(inputs=state, outputs=value)
policy_network = Model(inputs=state, outputs=policy)
train_network = Model(inputs=state, outputs=[value, policy])
return value_network, policy_network, train_network
我们的学习过程还构建了模型,将权重共享给其他进程,并为训练编译它们,并计算各自的损失:
_, _, train_network = build_networks(observation_shape, env.action_space.n)
weight_dict['weights'] = train_net.get_weights()
from keras import backend as K
def policy_loss(advantage=0., beta=0.01):
def loss(y_true, y_pred):
return -K.sum(K.log(K.sum(y_true * y_pred, axis=-1) + \K.epsilon()) * K.flatten(advantage)) + \
beta * K.sum(y_pred * K.log(y_pred + K.epsilon()))
return loss
def value_loss():
def loss(y_true, y_pred):
return 0.5 * K.sum(K.square(y_true - y_pred))
return loss
train_net.compile(optimizer=RMSprop(epsilon=0.1, rho=0.99),
loss=[value_loss(), policy_loss(advantage, args.beta)])
策略损失是一个 REINFORCE 损失加上一个熵损失,以鼓励探索。值损失是一个简单的均方误差损失。
将经验队列化成一个批次,我们的学习过程会在这个批次上训练模型并更新权重字典:
loss = train_net.train_on_batch([last_obs, advantage], [rewards, targets])
运行完整代码:
pip install -r requirements.txt
python 1-train.py --game=Breakout-v0 --processes=16
python 2-play.py --game=Breakout-v0 --model=model-Breakout-v0-35750000.h5
学习大约花费了 24 小时。
基于策略的优势演员评论员通常优于基于值的方法。
相关文献
你可以参考以下文章:
-
连接主义强化学习的简单统计梯度跟踪算法,罗纳德·J·威廉姆斯,1992 年
-
带有函数逼近的强化学习的策略梯度方法,理查德·S·萨顿,戴维·麦卡勒斯特,萨廷德·辛格,伊沙·曼索尔,1999 年
-
通过深度强化学习玩 Atari 游戏,沃洛基米尔·姆尼赫,科雷·卡武克乔乌格,戴维·西尔弗,亚历克斯·格雷夫斯,伊欧尼斯·安东诺格鲁,丹·维尔斯特拉,马丁·里德米勒,2013 年
-
通过深度神经网络和树搜索掌握围棋游戏,戴维·西尔弗,阿贾·黄,克里斯·J·马迪森,阿瑟·格兹,洛朗·西弗,乔治·范登·德里斯切,朱利安·施里特维泽,伊欧尼斯·安东诺格鲁,维达·潘内谢尔瓦姆,马克·兰特托,桑德·迪尔曼,多米尼克·格雷韦,约翰·纳姆,纳尔·卡尔赫布雷纳,伊利亚·苏茨克弗,蒂莫西·利利克拉普,马德琳·利奇,科雷·卡武克乔乌格,托雷·格雷佩尔和德米斯·哈萨比斯,2016 年
-
深度强化学习的异步方法,沃洛基米尔·姆尼赫,阿德里亚·普伊格多梅内奇·巴迪亚,梅赫迪·米尔扎,亚历克斯·格雷夫斯,蒂姆·哈利,蒂莫西·P·利利克拉普,戴维·西尔弗,科雷·卡武克乔乌格,2016 年 2 月
-
使用 KeRLym 进行深度强化学习无线电控制和信号检测,Gym RL 代理蒂莫西·J·奥谢和 T·查尔斯·克兰西,2016 年
总结
强化学习描述了优化代理通过奖励偶然获得任务的过程。通过深度神经网络开发了在线、离线、基于值或基于策略的算法,应用于各种游戏和模拟环境。
策略梯度是一种强行求解方案,需要在训练过程中进行动作采样,适用于小的动作空间,尽管它们为连续搜索空间提供了初步的解决方案。
策略梯度也可以用于训练神经网络中的非可微随机层,并通过这些层进行反向传播梯度。例如,当通过一个模型的传播需要按照参数化子模型进行采样时,来自顶层的梯度可以被视为底层网络的奖励。
在更复杂的环境中,当没有明显的奖励时(例如,从环境中存在的物体推理和理解可能的动作),推理帮助人类优化他们的动作,目前的研究尚未提供任何解决方案。当前的强化学习算法特别适用于精确的操作、快速的反应,但没有长期规划和推理。此外,强化学习算法需要大量的数据集,而模拟环境能够轻松提供这些数据集。但这也引出了现实世界中扩展性的问题。
在下一章,我们将探讨最新的解决方案,用于生成与现实世界数据无法区分的新数据。
第十二章:使用无监督生成网络学习特征
本章重点介绍一种新型模型——生成模型,包括 限制玻尔兹曼机、深度信念网络、变分自编码器、自回归模型 和 生成对抗网络。对于前者,我们将介绍其理论,而后者则通过实践代码和建议详细解释。
这些网络不需要任何标签进行训练,这就是所谓的 无监督学习。无监督学习帮助从数据中计算特征,而不受标签的偏差影响。这些模型是生成式的,因为它们经过训练以生成听起来真实的新数据。
以下内容将会涵盖:
-
生成模型
-
无监督学习
-
限制玻尔兹曼机
-
深度信念网络
-
生成对抗模型
-
半监督学习
生成模型
神经处理中的生成模型是一个模型,给定一个噪声向量 z 作为输入,生成数据:

训练的目的是找到能够生成尽可能接近真实数据的数据的参数。
生成网络的应用包括数据维度减少、合成数据生成、无监督特征学习和预训练/训练效率。预训练有助于泛化,因为预训练侧重于数据中的模式,而不是数据与标签之间的关系。
限制玻尔兹曼机
限制玻尔兹曼机是最简单的生成网络,由一个完全连接的隐藏层组成,如图所示:

完全玻尔兹曼机还具有隐藏到隐藏和可见到可见的循环连接,而 限制 版本则没有任何这种连接。
在一般情况下,RBM 被定义为 基于能量的模型,这意味着它们通过能量函数定义了一个概率分布:


Z 是 配分函数,E(v) 是 自由能 函数(不依赖于隐藏状态)。
注意
最小化负对数似然等价于最小化能量函数。
RBM 定义了一个作为模型参数线性函数的能量函数:


能量与自由能之间的关系由以下公式给出:

在 RBM 的情况下:

这里
表示对第 i 个隐藏神经元的可能值进行求和。
RBM 通常考虑在特定情况下,其中 v 和 h 是 {0,1} 中的二项值,因此:

该模型是对称的,遵循模型中的对称性:隐藏层和可见层在能量函数中占据相同的位置:

RBM 在两个方向上都作为一个简单的随机完全连接层工作(从输入到隐藏,从隐藏到输入)。
RBM 的负对数似然的梯度或导数有两个项,分别定义为正相位和负相位,其中第一项增加数据的概率,第二项减少生成样本的概率:

在这里,求和是对所有可能的输入
按其概率(期望)加权。在最小值处,任何自由能的增加都会减少总数据的期望。
实际上,负相位中的这种求和可以转化为对V观察到的(v,h)的求和:

为了在实践中计算这样的求和,观察到的样本(v,h)的概率必须满足 p(v | h),由上式给出,同时满足 p(h | v)。
采样是通过对比散度算法进行的,实践中:v 从数据集中采样,而 h 则根据上述分布在给定 v 的条件下绘制。这个操作会重复进行,以产生给定 h 的新 v,然后是给定 v 的新 h。在实践中,这足以生成与真实分布非常接近的样本。这些观察到的 v 和 h 样本被称为负粒子,成本函数中的第二项减少这些生成样本的概率,而第一项则增加数据的概率。
这里是计算带有负粒子的配分函数的结果:
W = shared_glorot_uniform((n_visible, n_hidden), name='W')
hbias = shared_zeros(n_hidden, name='hbias')
vbias = shared_zeros(n_visible, name='vbias')
params = [W, hbias, vbias]
def sample_h_given_v(v0_sample):
pre_sigmoid_h1 = T.dot(v0_sample, W) + hbias
h1_mean = T.nnet.sigmoid(pre_sigmoid_h1)
h1_sample = theano_rng.binomial(size=h1_mean.shape, n=1, p=h1_mean, dtype=theano.config.floatX)
return [pre_sigmoid_h1, h1_mean, h1_sample]
def sample_v_given_h(h0_sample):
pre_sigmoid_v1 = T.dot(h0_sample, W.T) + vbias
v1_mean = T.nnet.sigmoid(pre_sigmoid_v1)
v1_sample = theano_rng.binomial(size=v1_mean.shape, n=1, p=v1_mean, dtype=theano.config.floatX)
return [pre_sigmoid_v1, v1_mean, v1_sample]
def gibbs_hvh(h0_sample):
pre_sigmoid_v1, v1_mean, v1_sample = sample_v_given_h(h0_sample)
pre_sigmoid_h1, h1_mean, h1_sample = sample_h_given_v(v1_sample)
return [pre_sigmoid_v1, v1_mean, v1_sample,
pre_sigmoid_h1, h1_mean, h1_sample]
chain_start = persistent_chain
(
[
pre_sigmoid_nvs,
nv_means,
nv_samples,
pre_sigmoid_nhs,
nh_means,
nh_samples
],
updates
) = theano.scan(
gibbs_hvh,
outputs_info=[None, None, None, None, None, chain_start],
n_steps=k,
name="gibbs_hvh"
)
chain_end = nv_samples[-1]
def free_energy(v_sample):
wx_b = T.dot(v_sample, W) + hbias
vbias_term = T.dot(v_sample, vbias)
hidden_term = T.sum(T.log(1 + T.exp(wx_b)), axis=1)
return -hidden_term - vbias_term
cost = T.mean(free_energy(x)) - T.mean(free_energy(chain_end))
在 MNIST 数据集上训练 15 轮后的过滤器图像:

还有一小批负粒子(每行之间有 1,000 步的采样):

深度信念网络
深度信念网络(DBN)是多个 RBM 堆叠在一起,旨在增强它们的表示能力,更好地捕捉数据中的模式。
训练过程是逐层进行的,首先假设只有一个 RBM,带有隐藏状态
。一旦 RBM 的权重被训练好,这些权重将保持固定,第一个 RBM 的隐藏层
被视为第二个 RBM 的可见层,第二个 RBM 有一个隐藏状态
。每个新的 RBM 将捕捉到之前的 RBM 没有捕捉到的模式,如下图所示:

可以证明,在堆叠中每增加一个新的 RBM,会减少负对数似然值。
最后一步是可以将这些权重应用到分类网络中,通过在最终的隐藏状态上简单地添加一个线性层和一个 Softmax 层,然后像往常一样通过梯度下降训练微调所有权重。
对数据维度的应用保持不变,将所有层展开以产生解码器网络,权重等于编码器网络中的权重转置(初始多层 RBM):

这种展开的网络被称为自编码器。
实际上,如果没有贪婪的逐层训练,直接通过梯度下降进行训练需要找到合适的初始化,这可能会更加棘手,因为权重初始化必须足够接近解决方案。这就是为什么常用的自编码器方法是分别训练每个 RBM。
生成对抗网络
由于之前模型中的分区函数不可解且需要使用吉布斯采样的对比散度算法,博弈论最近为学习生成模型提供了一类新方法,即生成对抗网络(GANs),并且这种方法今天取得了巨大成功。
生成对抗网络由两个模型组成,这两个模型交替训练以相互竞争。生成器网络G的优化目标是通过生成难以被判别器D与真实数据区分的数据,来重现真实数据的分布。与此同时,第二个网络 D 的优化目标是区分真实数据和由 G 生成的合成数据。总体而言,训练过程类似于一个双人博弈的最小-最大游戏,目标函数如下:

在这里,x是真实数据,来自真实数据分布,z是生成模型的噪声向量。从某种意义上来说,判别器和生成器可以看作是警察和小偷:为了确保训练正确进行,警察的训练次数是小偷的两倍。
让我们通过图像作为数据的案例来说明 GANs。特别地,仍然采用第二章的例子,使用前馈网络分类手写数字,关于 MNIST 数字,考虑训练一个生成对抗网络,根据我们想要的数字生成图像。
GAN 方法包括使用第二个模型——判别网络,来训练生成模型,判别输入数据是否为真实或伪造。在这种情况下,我们可以简单地重用我们的 MNIST 图像分类模型作为判别器,进行 real 或 fake 的预测输出,并且将其条件化为应生成的数字标签。为了将网络条件化为标签,数字标签与输入数据进行拼接:
def conv_cond_concat(x, y):
return T.concatenate([x, y*T.ones((x.shape[0], y.shape[1], x.shape[2], x.shape[3]))], axis=1)
def discrim(X, Y, w, w2, w3, wy):
yb = Y.dimshuffle(0, 1, 'x', 'x')
X = conv_cond_concat(X, yb)
h = T.nnet.relu(dnn_conv(X, w, subsample=(2, 2), border_mode=(2, 2)), alpha=0.2 )
h = conv_cond_concat(h, yb)
h2 = T.nnet.relu(batchnorm(dnn_conv(h, w2, subsample=(2, 2), border_mode=(2, 2))), alpha=0.2)
h2 = T.flatten(h2, 2)
h2 = T.concatenate([h2, Y], axis=1)
h3 = T.nnet.relu(batchnorm(T.dot(h2, w3)))
h3 = T.concatenate([h3, Y], axis=1)
y = T.nnet.sigmoid(T.dot(h3, wy))
return y
提示
注意使用了两个泄漏修正线性单元(leaky ReLU),泄漏系数为 0.2,作为前两个卷积的激活函数。
为了根据噪声和标签生成图像,生成器网络由一系列反卷积组成,使用一个包含 100 个从 0 到 1 之间的实数的输入噪声向量 z:

在 Theano 中创建反卷积时,创建一个虚拟的卷积前向传播,并将其梯度作为反卷积的使用。
def deconv(X, w, subsample=(1, 1), border_mode=(0, 0), conv_mode='conv'):
img = gpu_contiguous(T.cast(X, 'float32'))
kerns = gpu_contiguous(T.cast(w, 'float32'))
desc = GpuDnnConvDesc(border_mode=border_mode, subsample=subsample,
conv_mode=conv_mode)(gpu_alloc_empty(img.shape[0], kerns.shape[1], img.shape[2]*subsample[0], img.shape[3]*subsample[1]).shape, kerns.shape)
out = gpu_alloc_empty(img.shape[0], kerns.shape[1], img.shape[2]*subsample[0], img.shape[3]*subsample[1])
d_img = GpuDnnConvGradI()(kerns, img, out, desc)
return d_img
def gen(Z, Y, w, w2, w3, wx):
yb = Y.dimshuffle(0, 1, 'x', 'x')
Z = T.concatenate([Z, Y], axis=1)
h = T.nnet.relu(batchnorm(T.dot(Z, w)))
h = T.concatenate([h, Y], axis=1)
h2 = T.nnet.relu(batchnorm(T.dot(h, w2)))
h2 = h2.reshape((h2.shape[0], ngf*2, 7, 7))
h2 = conv_cond_concat(h2, yb)
h3 = T.nnet.relu(batchnorm(deconv(h2, w3, subsample=(2, 2), border_mode=(2, 2))))
h3 = conv_cond_concat(h3, yb)
x = T.nnet.sigmoid(deconv(h3, wx, subsample=(2, 2), border_mode=(2, 2)))
return x
真实数据由元组 (X,Y) 给出,而生成的数据则由噪声和标签 (Z,Y) 构建:
X = T.tensor4()
Z = T.matrix()
Y = T.matrix()
gX = gen(Z, Y, *gen_params)
p_real = discrim(X, Y, *discrim_params)
p_gen = discrim(gX, Y, *discrim_params)
生成器和判别器模型在对抗学习中竞争:
-
判别器被训练为将真实数据标记为真实(
1),并将生成数据标记为生成(0),从而最小化以下成本函数:d_cost = T.nnet.binary_crossentropy(p_real, T.ones(p_real.shape)).mean() \ + T.nnet.binary_crossentropy(p_gen, T.zeros(p_gen.shape)).mean() -
生成器被训练成尽可能欺骗判别器。生成器的训练信号由判别器网络(p_gen)提供给生成器:
g_cost = T.nnet.binary_crossentropy(p_gen,T.ones(p_gen.shape)).mean()
和通常一样,计算每个模型的参数成本,并交替优化每个模型的权重,判别器的训练次数是生成器的两倍。在 GANs 的情况下,判别器和生成器之间的竞争不会导致每个损失的减少。
从第一轮开始:

到第 45 轮:

生成的示例看起来更接近真实数据:

改进 GANs
GANs 是近期出现的、非常有前景的技术,但目前仍在进行深入研究。然而,仍然有方法可以改进之前的结果。
首先,和 RBM 及其他网络一样,GANs 可以通过堆叠来增加它们的生成能力。例如,StackGan 模型提出使用两个堆叠的 GANs 进行高质量图像生成:第一个 GAN 生成粗糙的低分辨率图像,而第二个 GAN 将这个生成的样本作为输入,生成更高定义和更具真实感的图像,其中物体的细节更加精确。
GAN 的一个主要问题是模型崩溃,这使得它们很难训练。模型崩溃发生在生成器开始忽视输入噪声并学习仅生成一个样本时,这个样本总是相同的。生成中的多样性崩溃了。解决这个问题的一种非常有效的方法来自 S-GAN 模型,它通过向生成器中添加一个第三个网络来进行训练。这个网络的目的是根据输入预测噪声:

为了与生成器一起优化这个第三个网络,会向生成器损失中添加一个熵损失,以鼓励生成的图像 x 足够依赖噪声 z。换句话说,条件熵 H(x | z) 必须尽可能低:

这个第三个网络预测一个辅助分布 Q,用来逼近真实后验 P(z | x),并且可以证明它是 H(x | z) 的变分上界。这样的损失函数有助于生成器不忽视输入噪声。
半监督学习
最后但同样重要的是,这种生成对抗网络可以用来增强监督学习本身。
假设目标是分类 K 类,并且有一定数量的标记数据。可以将一些来自生成模型的生成样本添加到数据集中,并将它们视为属于 (K+1)th 类,即伪数据类。
将新分类器在两个数据集(真实数据和伪数据)之间的训练交叉熵损失分解为以下公式:

这里,
是模型预测的概率:

请注意,如果我们知道数据是真实的:

而在真实数据(K 类)上的训练会导致以下损失:

因此,全球分类器的损失可以重写为:

损失的第二项对应于 GAN 的标准无监督损失:

监督损失和无监督损失之间引入的交互作用仍然不完全理解,但当分类问题不简单时,无监督损失是有帮助的。
进一步阅读
您可以参考以下主题以获取更多见解:
-
Deeplearning.net RBM 教程:
deeplearning.net/tutorial/rbm.html -
Deeplearning.net 深度信念网络教程:
deeplearning.net/tutorial/DBN.html -
Deeplearning.net 使用 RBM-RNN 生成的教程:
deeplearning.net/tutorial/rnnrbm.html -
建模高维序列中的时间依赖性:应用于多声部音乐生成与转录,Nicolas Boulanger-Lewandowski,Yoshua Bengio,Pascal Vincent,2012
-
生成对抗网络,Ian J. Goodfellow,Jean Pouget-Abadie,Mehdi Mirza,Bing Xu,David Warde-Farley,Sherjil Ozair,Aaron Courville,Yoshua Bengio,2014
-
生成对抗网络将 改变世界,Nikolai Yakovenko,2016
medium.com/@Moscow25/ -
像素递归神经网络,Aaron van den Oord,Nal Kalchbrenner,Koray Kavukcuoglu,2016
-
InfoGAN:通过信息最大化生成对抗网络进行可解释的表示学习,Xi Chen,Yan Duan,Rein Houthooft,John Schulman,Ilya Sutskever,Pieter Abbeel,2016
-
StackGAN:使用堆叠生成对抗网络将文本转换为逼真的图像合成,Han Zhang,Tao Xu,Hongsheng Li,Shaoting Zhang,Xiaolei Huang,Xiaogang Wang,Dimitris Metaxas,2016
-
堆叠生成对抗网络,Xun Huang,Yixuan Li,Omid Poursaeed,John Hopcroft,Serge Belongie,2016
-
神经对话生成的对抗学习,Jiwei Li,Will Monroe,Tianlin Shi,Sébastien Jean,Alan Ritter,Dan Jurafsky,2017
-
改进的 GAN 训练技术,Tim Salimans,Ian Goodfellow,Wojciech Zaremba,Vicki Cheung,Alec Radford,Xi Chen,2016
-
无监督表示学习与深度卷积生成对抗网络,Alec Radford,Luke Metz,Soumith Chintala,2015
摘要
生成对抗网络如今是一个非常活跃的研究领域。它们属于生成模型家族,包括 RBM 和深度置信网络。
生成模型旨在生成更多数据,或以无监督的方式学习更好的特征,用于监督学习和其他任务。
生成模型可以根据一些环境输入进行条件化,并尝试找到真实数据背后的隐藏变量。
这些模型是最先进的,完成了与 Theano 的深度学习网络概述。下一章将介绍一些高级概念,以扩展 Theano 并探讨深度学习的未来。
第十三章:使用 Theano 扩展深度学习
本章为进一步深入 Theano 和深度学习提供了线索。首先,它介绍了如何在 Python 或 C 语言中为 Theano 计算图创建新的操作符,无论是针对 CPU 还是 GPU。然后,研究了与其他深度学习框架的交互,借助代码库和库的支持,实现与其他技术的双向转换。
最后,为了完善 Theano 深度学习领域所提供的可能性,我们发展了一个新的通用人工智能领域的概念。
本章涵盖的主题如下:
-
为 Theano 计算图编写新操作符
-
针对 CPU 和 GPU 的 Python 代码
-
针对 CPU 和 GPU 的 C 语言 API
-
与其他深度学习框架共享模型
-
云 GPU
-
元学习、渐进学习和引导学习
-
一般人工智能
本章全面概述了使用 Theano 进行深度学习的内容。
Python 中的 Theano 操作(针对 CPU)
作为一个数学编译引擎,Theano 的目的是以最优的方式为目标平台编译计算图。
新操作符的开发可以在 Python 或 C 语言中进行编写,进行 CPU 或 GPU 的编译。
首先,我们解决最简单的情况,在 CPU 的 Python 中,它将使你能够非常容易和快速地添加新操作。
为了固定概念,让我们实现一个简单的仿射操作符,执行仿射变换 a * x + b,其中 x 为输入。
操作符是通过从通用的theano.Op类派生的类来定义的:
import theano, numpy
class AXPBOp(theano.Op):
"""
This creates an Op that takes x to a*x+b.
"""
__props__ = ("a", "b")
def __init__(self, a, b):
self.a = a
self.b = b
super(AXPBOp, self).__init__()
def make_node(self, x):
x = theano.tensor.as_tensor_variable(x)
return theano.Apply(self, [x], [x.type()])
def perform(self, node, inputs, output_storage):
x = inputs[0]
z = output_storage[0]
z[0] = self.a * x + self.b
def infer_shape(self, node, i0_shapes):
return i0_shapes
def grad(self, inputs, output_grads):
return [self.a * output_grads[0]]
mult4plus5op = AXPBOp(4,5)
x = theano.tensor.matrix()
y = mult4plus5op(x)
f = theano.function([x], y)
res = f(numpy.random.rand(3,2))
让我们理解这个例子。
__props__属性被设置为操作符依赖的两个参数名,a和b。它将自动为我们生成__eq__()、__hash__()和__str_()方法,因此,如果我们创建两个具有相同a和b参数值的不同对象,Theano 会将它们视为相等的操作符:
>>> mult4plus5op2 = AXPBOp(4,5)
>>> mult4plus5op == mult4plus5op2
True
>>> hash(mult4plus5op)
-292944955210390262
>>> hash(mult4plus5op2)
-292944955210390262
此外,打印操作时,参数a和b将会出现:
>>> theano.printing.pprint(y)
AXPBOp{a=4, b=5}.0
>>> theano.printing.pydotprint(y)

如果未指定__props__,则需要手动定义__eq__()、__hash__()和__str_()方法。
make_node()方法创建将被包含在计算图中的节点,并在将mult4plus5op对象应用于输入x时运行。节点创建通过theano.Apply()方法执行,该方法的参数是输入变量和输出类型。为了强制输入为变量,调用as_tensor_variable()方法,将任何 NumPy 数组转换为变量。这是我们定义输出类型的地方,给定输入时还需检查输入是否与操作符兼容,否则会引发 TypeError。
请注意,正如我们之前为 __eq__() 方法使用 __props__ 属性那样,自动生成 make_node() 方法也是可能的,但在这种情况下,itypes 和 otypes 属性用于定义输入和输出的类型:
itypes = [theano.tensor.dmatrix]
otypes = [theano.tensor.dmatrix]
perform() 方法定义了在 Python 中执行的该操作符的计算。由于可以实现多个输入并返回多个输出的操作符,因此输入和输出以列表形式给出。第二个输出将存储在 output_storage[1][0] 中。输出可能已经由前面的值分配,以便重用内存。它们将始终是合适的 dtype 对象,但不一定是正确的形状和步幅。它们不符合正确形状时,最好重新分配。
最后的两个方法,infer_shape() 和 grad(),是可选的。第一个方法用于输出无需计算时,仅需形状信息来进行计算的情况——这种情况通常发生在 Theano 优化过程中。第二个方法用于在 grad() 方法下对输出进行求导:
>>> dy=theano.tensor.grad(y.sum(), x)
>>> theano.printing.pprint(dy)
'(TensorConstant{4} * fill(AXPBOp{a=4, b=5}(<TensorType(float32, matrix)>), fill(Sum{acc_dtype=float64}(AXPBOp{a=4, b=5}(<TensorType(float32, matrix)>)), TensorConstant{1.0})))'
>>> df = theano.function([x], dy)
>>> theano.printing.debugprint(df)
Alloc [id A] '' 2
|TensorConstant{(1, 1) of 4.0} [id B]
|Shape_i{0} [id C] '' 1
| |<TensorType(float32, matrix)> [id D]
|Shape_i{1} [id E] '' 0
|<TensorType(float32, matrix)> [id D]
以相同的方式,可以定义操作符的 R-操作符函数。
用于 GPU 的 Theano 操作符(Op)
让我们看看在 GPU config 模式下运行该操作符时会发生什么:
>>> y = mult4plus5op(2 * x) + 4 * x
>>> f = theano.function([x], y)
>>> theano.printing.debugprint(f)
HostFromGpu(gpuarray) [id A] '' 6
|GpuElemwise{Composite{(i0 + (i1 * i2))}}[(0, 0)]<gpuarray> [id B] '' 5
|GpuFromHost<None> [id C] '' 4
| |AXPBOp{a=4, b=5} [id D] '' 3
| |HostFromGpu(gpuarray) [id E] '' 2
| |GpuElemwise{mul,no_inplace} [id F] '' 1
| |GpuArrayConstant{[[ 2.]]} [id G]
| |GpuFromHost<None> [id H] '' 0
| |<TensorType(float32, matrix)> [id I]
|GpuArrayConstant{[[ 4.]]} [id J]
|GpuFromHost<None> [id H] '' 0
由于我们仅在 Python 中定义了新操作符的 CPU 实现,而完整的图计算是在 GPU 上运行的,因此数据会在图的中间来回传输到 CPU,以应用我们新的 CPU 操作符:

为了避免图中传输的不效率问题,让我们为 GPU 创建相同的 Python 操作符。
为此,您只需修改操作符的 make_node() 和 perform() 方法,如下所示:
from theano.gpuarray.type import get_context
def make_node(self, x):
x = as_gpuarray_variable(x, self.context_name)
x_arg = pygpu.elemwise.arg('x', 'float32', read=True)
c_arg = pygpu.elemwise.arg('c', 'float32', read=True, write=True)
self.my_op = pygpu.elemwise.GpuElemwise(get_context(self.context_name), "c = " + str(self.a) + " * x + " + str(self.b), [x_arg, c_arg], convert_f16=True)
return Apply(self, [x], [x.type()])
def perform(self, node, inputs, output_storage):
x = inputs[0]
z = output_storage[0]
z[0] = pygpu.empty(x.shape, dtype=x.dtype, context=get_context(self.context_name))
self.my_op( x, z[0])
没有太多变化。
在 make_node() 方法中,as_tensor_variable() 被 as_gpuarray_variable() 替换,后者需要上下文,这也是 GPU 变量类型定义的一部分。get_context() 方法将我们为设备选择的上下文名称转换为 pygpu 库中的 GPUContext。
在 perform() 方法中,计算在 GPU 上执行,得益于 pygpu 库,它包含了 GPU 上的逐元素操作符以及 基本线性代数子程序 (BLAS) 方法,如 通用矩阵乘矩阵运算 (GEMM) 和 通用矩阵向量乘法 (GEMV) 操作。
现在,让我们看一下当这个新操作符在 GPU 上的更大图中时,编译后的图是什么样的:
HostFromGpu(gpuarray) [id A] '' 4
|GpuElemwise{Add}[(0, 1)]<gpuarray> [id B] '' 3
|GpuArrayConstant{[[ 4.]]} [id C]
|GpuAXPBOp{a=4, b=5, context_name='dev0'} [id D] '' 2
|GpuElemwise{Mul}[(0, 1)]<gpuarray> [id E] '' 1
|GpuArrayConstant{[[ 2.]]} [id F]
|GpuFromHost<dev0> [id G] '' 0
|<TensorType(float32, matrix)> [id H]

为了提高可读性,我们将操作符类的名称前缀加上了 Gpu,例如,GpuAXPBOp。
用于 CPU 的 Theano 操作符(Op)
另一个低效之处在于,操作符的 Python 实现每次执行计算时都会增加显著的开销,即在图中每次操作符实例化时都会发生这种情况。与图的其他部分不同,Python 代码不会像 Theano 将其他图部分用 C 编译一样进行编译,而是当 C 实现被包装到 Python 中并交换数据时,会发生这种开销。
为了弥补这一点,可以直接编写一些 C 代码,将其并入图的其他代码中并一起编译。
当直接用 C 实现操作符时,NumPy 是管理数组的底层库,NumPy-API 扩展了 Python C-API。定义新 C 操作符的 Python 类不必实现perform()方法;相反,它返回要并入c_code()、c_support_code()和c_support_code_apply()方法的 C 代码:
def c_code_cache_version(self):
return (6, 0)
def c_support_code(self):
c_support_code = """
bool same_shape(PyArrayObject* arr1, PyArrayObject* arr2)
{
if( PyArray_NDIM(arr1) != PyArray_NDIM(arr2)) {
return false;
}
for(int i = 0; i < PyArray_NDIM(arr2) ; i++) {
if (PyArray_DIMS(arr1)[0] == PyArray_DIMS(arr2)[0]) {
return false;
}
}
return true;
}
"""
return c_support_code
def c_support_code_apply(self, node, name):
dtype_x = node.inputs[0].dtype
dtype_z = node.outputs[0].dtype
a = self.a
b = self.b
c_support_code = """
void elemwise_op_%(name)s(npy_%(dtype_x)s* x_ptr, npy_intp* x_str, int itemsize_x,
npy_%(dtype_z)s* z_ptr, npy_intp* z_str, int itemsize_z,
int nbDims, npy_intp* dims)
{
npy_intp stride_x = (npy_intp)(1);
npy_intp stride_z = (npy_intp)(1);
for (int i = 0; i < nbDims; i ++) {
stride_x = stride_x * x_str[i] / itemsize_x;
stride_z = stride_z * z_str[i] / itemsize_z;
}
for (int i=0; i < dims[0]; i++)
if (nbDims==1) {
z_ptr[i * z_str[0]/itemsize_z] = x_ptr[i * x_str[0] / itemsize_x] * ((npy_%(dtype_z)s) %(a)s) + ((npy_%(dtype_z)s)%(b)s);
} else {
elemwise_op_%(name)s( x_ptr + i * stride_x , x_str + 1, itemsize_x,
z_ptr + i * stride_z , z_str + 1, itemsize_z,
nbDims - 1, dims + 1 );
}
}
"""
return c_support_code % locals()
def c_code(self, node, name, inp, out, sub):
x = inp[0]
z = out[0]
dtype_x = node.inputs[0].dtype
dtype_z = node.outputs[0].dtype
itemsize_x = numpy.dtype(dtype_x).itemsize
itemsize_z = numpy.dtype(dtype_z).itemsize
typenum_z = numpy.dtype(dtype_z).num
fail = sub['fail']
c_code = """
// Validate that the output storage exists and has the same
// dimension as x.
if (NULL == %(z)s || !(same_shape(%(x)s, %(z)s)))
{
/* Reference received to invalid output variable.
Decrease received reference's ref count and allocate new
output variable */
Py_XDECREF(%(z)s);
%(z)s = (PyArrayObject*)PyArray_EMPTY(PyArray_NDIM(%(x)s),
PyArray_DIMS(%(x)s),
%(typenum_z)s,
0);
if (!%(z)s) {
%(fail)s;
}
}
// Perform the elemwise operation
((npy_%(dtype_z)s *)PyArray_DATA(%(z)s))[0] = 0;
elemwise_op_%(name)s((npy_%(dtype_x)s*)PyArray_DATA(%(x)s), PyArray_STRIDES(%(x)s), %(itemsize_x)s,
(npy_%(dtype_z)s*)PyArray_DATA(%(z)s), PyArray_STRIDES(%(z)s), %(itemsize_z)s,
PyArray_NDIM(%(x)s), PyArray_DIMS(%(x)s) );
"""
return c_code % locals()
现在我们来讨论不同的部分:
当实现c_code_cache_version()时,Theano 会缓存已编译的代码,以便下次将操作符纳入图中时节省一些编译时间,但每当我们修改 C 操作符的代码时,版本号必须递增。
放置在c_support_code()和c_support_code_apply()方法中的代码包含在 C 程序的全局作用域中。放置在c_support_code_apply()和c_code()方法中的代码必须是特定于图中每个操作符应用的;特别是,在这种情况下,它们取决于输入的类型。由于c_support_code_apply()代码包含在全局作用域中,方法的名称与操作符名称相同。
PyArray_NDIM、PyArray_DIMS、PyArray_STRIDES和PyArray_DATA是宏,用于访问每个 NumPy 数组(C 中的PyArrayObject)的维数、维度、步长和数据。PyArray_EMPTY是 C 中等同于 Python numpy.empty()方法的函数。
NumPy 的PyArrayObject类继承自 Python C-API 中的PyObject类。Py_XDECREF宏允许我们在为新的输出数组分配内存之前减少输出的引用计数。像 Python C-API 一样,NumPy C-API 要求正确计数对象的引用。Theano 不能保证输出数组已被分配,也不能保证它是否已经按正确的形状分配。这就是为什么在c_code()开始时会进行测试。
注意数组可以是有步长的,因为它们可以是数组(张量)的视图(或子张量)。也可以实现创建视图或修改输入的操作。
还有一些其他可能的方法可以进一步优化 C 实现:c_libraries()和c_lib_dirs()用于使用外部库,c_code_cleanup()用于销毁内存分配,c_init_code()用于在初始化时执行某些代码。
最后,也可以在代码中引用一些 C 文件,以减少 Python 类的负担。我们不详细说明这三种特性。
Theano 在 C 中的 GPU 操作
正如你想象的那样,可以结合两种优化:
-
通过直接使用 C 编程减少 Python/C 开销
-
编写 GPU 代码
为 GPU 编写 CUDA 代码时,必须将要在 GPU 上多个核心并行运行的代码包装成一种特殊的函数类型,称为 内核。
为此,__init__()、make_node() 和 c_code_cache_version() 方法与我们在 GPU 上的 Python 示例相同,但新增了 gpu_kernels() 方法,用于定义新的 GPU 内核,以及 c_code() 方法(它再次替代了 perform() 方法),用于实现 C 代码,也称为 主机代码,该代码负责协调何时以及如何调用 GPU 上的不同内核:
def gpu_kernels(self, node, name):
code = """
KERNEL void axpb(GLOBAL_MEM %(ctype)s *x, GLOBAL_MEM %(ctype)s *z, ga_size n, ga_size m) {
for (ga_size i = LID_0; i < n; i += LDIM_0) {
for (ga_size j = LID_0; j < m; j += LDIM_0) {
z[i*m + j] = %(write_a)s( 2 * x[i*m + j] );
}
}
}""" % dict(ctype=pygpu.gpuarray.dtype_to_ctype(self.dtype),
name=name, write_a=write_w(self.dtype))
return [Kernel(
code=code, name="axpb",
params=[gpuarray.GpuArray, gpuarray.GpuArray, gpuarray.SIZE, gpuarray.SIZE],
flags=Kernel.get_flags(self.dtype),
objvar='k_axpb_' + name)]
def c_code(self, node, name, inp, out, sub):
n, = inp
z, = out
dtype_n = node.inputs[0].dtype
fail = sub['fail']
ctx = sub['params']
typecode = pygpu.gpuarray.dtype_to_typecode(self.dtype)
sync = bool(config.gpuarray.sync)
kname = self.gpu_kernels(node, name)[0].objvar
s = """
size_t dims[2] = {0, 0};
size_t ls, gs;
int err;
dims[0] = %(n)s->ga.dimensions[0];
dims[1] = %(n)s->ga.dimensions[1];
Py_CLEAR(%(z)s);
%(z)s = pygpu_zeros(2, dims,
%(typecode)s,
GA_C_ORDER,
%(ctx)s, Py_None);
if (%(z)s == NULL) {
%(fail)s
}
ls = 1;
gs = 256;
err = axpb_call(1, &gs, &ls, 0, %(n)s->ga.data, %(z)s->ga.data, dims[0], dims[1]);
if (err != GA_NO_ERROR) {
PyErr_Format(PyExc_RuntimeError,
"gpuarray error: kEye: %%s. n%%lu, m=%%lu.",
GpuKernel_error(&%(kname)s, err),
(unsigned long)dims[0], (unsigned long)dims[1]);
%(fail)s;
}
if(%(sync)d)
GpuArray_sync(&%(z)s->ga);
""" % locals()
return s
A new GPU computation kernel is defined under the name axpb, and it is a simple C code with special GPU types and two macros: KERNEL to designate the kernel function (hiding the CUDA __global__ declaration for kernels) and GLOBAL_MEM for the variables defined globally, available both on the CPU and the GPU (in opposition to variables inside the kernel function that, by default, are local to the thread executed on a GPU core).
请注意,我只实现了矩阵(即二维)输入的操作符,256 个线程将并行执行相同的操作,而这些操作本可以被分成不同的组,并分配给不同的线程。
在 CPU 上运行的主机代码管理 CPU 和 GPU 上的内存,并启动在 GPU 设备上执行的内核函数。
新 GPU 数组的分配是通过 pygpu_zeros() 方法执行的,使用 CUDA 时,它会在后台调用 cudamalloc() 方法,直接在 GPU 内存中分配数组。操作符实例无需管理分配给输出的内存释放以及 GPU 和 CPU 之间的数据传输,因为这是 Theano 优化的职责,决定何时插入传输操作符 HostFromGpu 和 GpuFromHost。
在 C 代码中调用内核是通过 axpb_call() 来执行的,即内核的名称后面跟着 _call()。注意,调用中有四个参数,比定义内核方法时的参数多。这四个参数定义了 libgpuarray 如何在核心上执行或部署内核。
为了理解 GPU 的并行编程执行配置,首先让我们明确一些关于 GPU 的基本概念。CUDA GPU 由 流式多处理器(SM)组成,其规格由计算能力、波大小、网格大小、块大小、每个 SM 和每个块的最大线程数、共享和本地内存大小以及最大寄存器数等参数来定义:

(来源:en.wikipedia.org/wiki/CUDA)
在执行过程中,多处理器以 单指令多数据 (SIMD) 方式执行一组 32 个线程的指令(如前表所示)。在进行并行执行编程时,你需要将线程组织成尽可能接近底层架构的块。例如,对于矩阵的逐元素操作,比如我们的 AXPBOp,可以认为每个线程将对矩阵中的一个元素执行操作。所以,对一个 224 x 224 的图像进行计算将需要 50,176 个线程。假设 GPU 有 8 个多处理器,每个多处理器有 1024 个核心。在执行配置中,你可以定义一个 256 线程的块大小,并且执行完整计算所需的块数将是 196 个。为了简化并行程序的开发,块可以组织成一个多维网格(对于 CC 大于 2.0 的情况,最多支持 3 维,如前表所示),在图像输入的情况下,使用 14 x 14 块的二维网格是自然的。你可以自行组织线程到网格上的块中,但最佳的线程组织方式是遵循底层数据的维度,因为这样更容易将数据划分并分配给不同的线程。
每个线程执行时都会提供值,用于访问它在网格中的位置,这些值可以在代码中使用:
-
gridDim.x、gridDim.y、gridDim.z线程块网格的维度 -
blockIdx.x、blockIdx.y、blockIdx.z块在网格中的坐标 -
blockDim.x、blockDim.y、blockDim.z块的维度 -
threadIdx.x、threadIdx.y、threadIdx.z线程在块中的坐标
对于我们的逐元素 AXPBOp,每个线程处理一个元素,线程可以根据以下行索引获取数据元素:
int i = blockIdx.x*blockDim.x + threadIdx.x;
部署时,内核调用中的前四个新参数对应于:
-
网格/块的维度,在本例中是 2,表示输入为图像/矩阵
-
启动网格的大小,在本例中是 {14, 14}。一旦每块线程的数量被定义(在本例中为 256),每个网格的块数就由问题的大小来决定(这里是矩阵的大小)。
-
启动块的大小,在本例中是 {16, 16},用于每块 256 个线程,通常设置为 128 或 256。最好选择一个 warp 大小的倍数,因为执行是按 warp 进行的;如果你设置为 250,那么 201 个块将表现不佳:每个块的一个 warp 将无法充分发挥并行潜力。可以尝试不同的 32 的倍数,选择最有效的执行结果。
-
分配动态共享内存的数量,这是在定义共享内存(使用
LOCAL_MEM宏)时需要的,尤其是在共享内存的大小在编译时未知的情况下。共享内存指定的是在同一线程块内共享的内存。在计算能力 2.x 和 3.x 的设备上,每个多处理器有 64KB 的片上内存,可以在 L1 缓存和共享内存之间进行分配(16K、32K 或 48K)。L1 缓存将线程在一个 warp 中的全局内存访问合并为尽可能少的缓存行。由于缓存的存在,每个线程之间的对齐差异对性能的影响可以忽略不计。第二和第三维度的跨步访问会引发效率问题;在这种情况下,使用共享内存使得你可以以合并的方式从全局内存中提取一个二维块到共享内存,并让连续的线程遍历共享内存块:![GPU 上的 Theano 操作 C 语言实现]()
通过共享内存进行合并转置,NVIDIA 并行计算
当数据的维度不能整除为块大小与网格大小的乘积时,处理边界数据的线程将比其他线程执行得更快,并且内核代码必须以检查越界内存访问的方式编写。
在并行编程时,竞态条件、共享内存中的内存银行冲突以及数据无法保留在可用的寄存器中的情况是一些新的问题需要检查。合并全局内存访问是实现良好性能的最关键方面。NVIDIA® Nsight™ 工具将帮助你开发、调试和分析在 CPU 和 GPU 上执行的代码。
模型转换
当模型被保存时,得到的数据仅仅是一个数组列表,即权重向量(用于偏置)和矩阵(用于乘法运算)以及每一层的名称。将模型从一个框架转换到另一个框架非常简单:它包括加载一个数值数组并检查层的名称。以下是一些从 Caffe 深度学习框架(用 C++编写)到其他框架的转换示例:
要在 Torch 深度学习框架(用 Lua 编写)与 Theano 之间转换变量,你只需要一个工具将数据从 Lua 转换到 Python NumPy:
github.com/imodpasteur/lutorpy
要在 Tensorflow 和 Theano 之间转换模型,我建议您使用 Keras 库,它将保持最新,并允许您在 Theano 或 Tensorflow 中训练模型。例如,要将模型从 Tensorflow 转换为 Theano,请保持您的 Keras 安装配置为 Theano,如我们在第五章中所见,使用双向 LSTM 分析情感,加载 Tensorflow 权重,并按如下方式修改层名称:
from keras import backend as K
from keras.utils.conv_utils import convert_kernel
from keras.models import Model
# build your Keras model HERE
# then
model.load_weights('my_weights_tensorflow.h5')
for layer in model.layers:
if layer.__class__.__name__ in ['Convolution1D', 'Convolution2D']:
original_w = K.get_value(layer.W)
converted_w = convert_kernel(original_w)
K.set_value(layer.W, converted_w)
model.save_weights('my_weights_theano.h5')
一个镜像操作序列使我们能够反向操作,从 Theano 转换为 Tensorflow。
使用 Keras 设计网络的另一个优点是能够直接在云中进行训练,利用 Google Cloud 机器学习引擎,内置张量处理单元(TPU),作为 GPU 的替代方案,从底层为机器学习设计。
让我们以第五章中的示例为例,使用双向 LSTM 分析情感。
为了在云中训练模型,我在 Google 控制台中创建了一个名为DeepLearning Theano的项目,地址是console.cloud.google.com/iam-admin/projects,并在项目的 API 管理器中启用了机器学习引擎 API。可能会有一些安装要求,您可以通过以下链接查看相关说明:cloud.google.com/ml-engine/docs/quickstarts/command-line,例如 Google Cloud SDK 和项目配置。通过gcloud init命令,您可以重新初始化 SDK 配置,切换到DeepLearning Theano项目。
让我们将数据上传到云中新创建的存储桶,具体取决于您选择的区域(这里是europe-west1):
gsutil mb -l europe-west1 gs://keras_sentiment_analysis
gsutil cp -r sem_eval2103.train gs://keras_sentiment_analysis/sem_eval2103.train
gsutil cp -r sem_eval2103.dev gs://keras_sentiment_analysis/sem_eval2103.dev
gsutil cp -r sem_eval2103.test gs://keras_sentiment_analysis/sem_eval2103.test
由于模型是在云中的实例上执行的,因此需要:
-
修改 Python 脚本,使其从远程存储桶加载文件流,而不是从本地目录加载,使用库
tensorflow.python.lib.io.file_io.FileIO(train_file, mode='r'),而不是标准方法open(train_file, mode='r'),两者的 mode 参数使用相同,'r'表示读取,w表示写入, -
定义
setup.py文件以配置云实例环境中所需的库:from setuptools import setup, find_packages setup(name='example5', version='0.1', packages=find_packages(), description='keras on gcloud ml-engine', install_requires=[ 'keras', 'h5py', 'nltk' ], zip_safe=False) -
定义云部署配置文件
cloudml-gpu.yaml:trainingInput: scaleTier: CUSTOM # standard_gpu provides 1 GPU. Change to complex_model_m_gpu for 4 GPUs masterType: standard_gpu runtimeVersion: "1.0"
在将训练提交给 Google ML Cloud 之前,先在本地检查训练是否正常工作,请运行以下命令:
gcloud ml-engine local train --module-name 7-google-cloud.bilstm \
--package-path ./7-google-cloud -- --job-dir ./7-google-cloud \
-t sem_eval2103.train -d sem_eval2103.dev -v sem_eval2103.test
如果本地一切正常,我们就可以将其提交到云端:
JOB_NAME="keras_sentiment_analysis_train_$(date +%Y%m%d_%H%M%S)"
gcloud ml-engine jobs submit training $JOB_NAME \
--job-dir gs://keras_sentiment_analysis/$JOB_NAME \
--runtime-version 1.0 \
--module-name 7-google-cloud.bilstm \
--package-path ./7-google-cloud \
--region europe-west1 \
--config=7-google-cloud/cloudml-gpu.yaml \
-- \
-t gs://keras_sentiment_analysis/sem_eval2103.train \
-d gs://keras_sentiment_analysis/sem_eval2103.dev \
-v gs://keras_sentiment_analysis/sem_eval2103.test
gcloud ml-engine jobs describe $JOB_NAME

注意
请注意,Google ML Cloud 使用 Tensorflow 作为后端。
人工智能的未来
第二章,使用前馈网络分类手写数字介绍了多种优化技术(如 Adam、RMSProp 等),并提到了二阶优化技术。一个推广是还要学习更新规则:

这里,
是优化器的参数,
用来从不同的问题实例中学习,类似于优化器从问题到问题的泛化或迁移学习,从而在新问题上学习得更好。在这种学习如何学习或元学习框架下,目标是最小化学习正确的时间,因此需要在多个时间步长上定义:

其中:

循环神经网络可以作为优化器模型来使用
。这种解决多目标优化问题的泛化技术能提高神经网络的学习速度。
研究人员进一步探索,寻找通用人工智能,旨在实现具有人类水平技能的人工智能,具备自我提升的能力,并能以渐进的方式获得新技能,利用其内在的、之前学到的技能来寻找新的优化问题的解决方案。
技能可以被定义为一种智能工具,用于缩小或约束搜索空间,并限制机器人在无限可能性的世界中的行为。
构建通用人工智能要求你定义具有内在技能的智能架构,这些技能将由程序员硬编码到机器人中,并帮助解决较小的子问题,还需要定义新技能将被获得的顺序,即在人工智能学校中可以教授的课程路线图。而渐进学习是通过使用更简单的技能逐步学习技能,引导学习则涉及一个已经发现技能的教师,将这些技能教授给其他人工智能。
在自然语言翻译任务中,已经证明较小的网络能从较大的网络中更快、更好地学习,后者作为导师,已经学会了翻译并生成翻译供较小的网络学习,而不是直接从真实的人类翻译集学习。

上图表示 GoodAI 路线图研究所,用于评估人工智能的学习路线图。
自我探索、与导师的沟通以及采纳负面和正面反馈是朝向自主智能发展的一些思想,这种智能将能够自我发展,而当前的深度学习网络为这一未来铺平了道路。
在朝着这一目标努力的公司中,值得一提的是 GoodAI 以及亚马逊及其 Echo 产品和其背后的语音控制助手技术 Alexa,后者已经学会了超过 10,000 项技能,帮助你组织生活。Alexa 的知识已经变得如此庞大,以至于很难深入了解并找出它的局限性。为开发者提供的测试环境使他们能够将这些技能集成到更高层次的智能工具中:

进一步阅读
你可以参考以下文章以了解更多:
-
CUDA C 和 C++的简单介绍,
devblogs.nvidia.com/parallelforall/easy-introduction-cuda-c-and-c/ -
如何高效访问 CUDA C/C++ 内核中的全局内存,
devblogs.nvidia.com/parallelforall/how-access-global-memory-efficiently-cuda-c-kernels/ -
在 CUDA C/C++ 中使用共享内存,
devblogs.nvidia.com/parallelforall/using-shared-memory-cuda-cc/ -
另一个 Tensorflow 初学者指南(第**4 部分 - Google Cloud ML + GUP + Keras),
liufuyang.github.io/2017/04/02/just-another-tensorflow-beginner-guide-4.html -
通过梯度下降学习的学习,Marcin Andrychowicz、Misha Denil、Sergio Gomez、Matthew W. Hoffman、David Pfau、Tom Schaul、Brendan Shillingford 和 Nando de Freitas,2016
-
一种搜索通用人工智能的框架,Marek Rosa 和 Jan Feyereisl,The GoodAI Collective,2016
摘要
本章总结了我们对 Theano 深度学习概述的内容。
Theano 的第一组扩展,在 Python 和 C 中为 CPU 和 GPU 开发,已经在这里公开,用于为计算图创建新操作符。
将已学习的模型从一个框架转换到另一个框架并不是一项复杂的任务。Keras,这本书中多次提到的高层次库,作为 Theano 引擎之上的抽象层,提供了一种简单的方法来同时使用 Theano 和 Tensorflow,并推动模型在 Google ML Cloud 中的训练。
最后,本书中呈现的所有网络都是通用智能的基础,这些网络可以利用这些初步技能,如视觉或语言理解与生成,去学习更广泛的技能,这些技能仍然来自于现实世界数据或生成的数据的经验。








定义了先前权重与新生成权重之间的影响,以计算头部的位置。一个偏移权重
定义了相对于该位置的位移量。








浙公网安备 33010602011771号