众所周知,TensorFlow中原来是不提供单独的cross entropy loss计算函数的,只有softmax_cross_entropy_with_logitstf.nn.sigmoid_cross_entropy_with_logits两类。(不过现在Keras里有这种东西了,categorical_crossentropy可以指明输入的是logits而非softmax)。据开发者说,这是因为:

We provide optimized cross-entropy implementations that are fused with the softmax/sigmoid implementations because their performance and numerical stability are critical to efficient training.
If however you are just interested in the cross entropy itself, you can compute it directly using code from the beginners tutorial:

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

N.B. DO NOT use this code for training. Use tf.nn.softmax_cross_entropy_with_logits() instead.[1]

那么都有些什么问题呢?

softmax计算中的问题

softmax的公式是:

$$ y_i = \frac{e^{x_i}}{\sum_{i=1}^n e^{x_i}} $$

一个事实是,如果传入的值稍微大一些,结果就会溢出(因为指数运算的结果太大了)。解决方法是在分式上下除以一个$e^\alpha$:

$$ \begin{aligned} y_i = \frac{e^{x_i}}{\sum_{i=1}^n e^{x_i}} = \frac{e^{x_i - \alpha}}{\sum_{i=1}^n e^{x_i - \alpha}} \end{aligned} $$

令$\alpha = \max{(x_1, \cdots, x_n)}$,则$x_i - \alpha \leq 0$,$e^{x_i - \alpha}$的结果趋近于0,不会发生溢出。[2]

结论:不要自己直接手算softmax。

sigmoid计算中的问题

sigmoid的公式是:

$$y = \frac{1}{1 + e^{-x}}$$

这看起来还比较简单,不过仍然要注意分母溢出的问题。之前scipy的expit曾经出过这样的一个bug。在$x$为正数时,它计算的是$\frac{e^x}{e^x + 1}$,而python的math.exp在$x \geq 710$时会溢出。所以expit(710)也会溢出。[3]

结论:最好也不要自己手算sigmoid。

cross entropy计算中的问题

交叉熵的公式是:

$$L = -\sum_{i=1}^n y_i \log{\hat{y}_i}$$

其中$y_i$是正确(分类)结果(概率),$\hat{y}_i$是模型输出的分类概率。

一般来说,这个函数的输入都是softmax或者sigmoid之后的结果,从数学上说,可以保证在$(0, 1)$范围内;但是计算机的表示范围是有限的,很可能会出现$\hat{y}_i = 0$的情况。如果不管的话,结果就会直接溢出变成nan。所以至少要做一下预处理,把接近0的$\hat{y}_i$变成$\epsilon$之类的。

Keras的实现中还把接近1的$\hat{y}_i$变成了$1 - \epsilon$,这一点我还没想清楚为什么。[4]

结论:也不要自己手算交叉熵。

(我之前确实遇到过nan的情况。)

softmax + cross entropy

把softmax代入到cross entropy的公式中:

$$ \begin{aligned} L &= -\sum_{i=1}^n y_i \log{\hat{y}_i} \\ &= -\sum_{i=1}^n y_i \log{\frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}}} \\ &= -\sum_{i=1}^n y_i \left(x_i - \log{\sum_{j=1}^n e^{x_j}}\right) \\ &= -\sum_{i=1}^n x_i y_i + \left(\sum_{i=1}^n y_i\right) \left(\log{\sum_{j=1}^n e^{x_j}}\right) \end{aligned}$$

显然上式里只有$\log{\sum_{j=1}^n e^{x_j}}$会有数值稳定性问题。可以用类似的方法来处理:令$\alpha = \max{(x_1, \cdots, x_n)}$,则

$$ \log{\sum_{i=1}^n e^{x_i}} = \log{\left(e^\alpha \sum_{i=1}^n e^{x_i - \alpha}\right)} = \alpha + \log{\sum_{i=1}^n e^{x_i - \alpha}} $$

这样就可以解决直接计算$e^{x_j}$溢出的问题了。

sigmoid + cross entropy

$$ \begin{aligned} L &= -\sum_{i=1}^n y_i \log{\hat{y}_i} \\ &= -\sum_{i=1}^n y_i \log{\frac{1}{1+e^{-x_i}}} \\ &= \sum_{i=1}^n y_i \log{(1+e^{-x_i})} \end{aligned}$$

如果$e^{-x_i}$很大,那么不需要计算$\log{(1+e^{-x_i})}$(可能会溢出),直接用$-x_i$作为估计值。否则$e^{-x_i}$会被截断。

实现

可以看出softmax和cross entropy一起计算效果更好(如果先算出概率分布,由于计算精度的原因,很小的概率会舍入到0,然后直接增大到EPS,所以得到的结果变小了)。

sigmoid和cross entropy一起计算效果也更好,原因是类似的。

结论:TensorFlow的API这样设计是有原因的(虽然我还是觉得应该给一个算cross entropy的API),为了保证数值稳定性,应该尽量用API,不要自己写。


  1. TensorFlow issue - Why is there no support for directly computing cross entropy?

  2. 知乎 - Softmax函数与交叉熵

  3. scipy issue - expit does not handle large arguments well

  4. tensorflow issue - Why is there no support for directly computing cross entropy? - comment

书:深入理解TensorFlow架构设计与实现原理


本章主要介绍了TensorFlow的安装方法和基本结构。基本结构里大部分内容我也不是很明白……

TensorFlow的五种安装方法

  1. 用包管理工具Anacoda安装
  2. 用原生pip安装
  3. 用virtualenv安装
  4. 自己下载源码进行编译并得到whl包
  5. 用docker运行含有TensorFlow二进制包的容器

如果需要使用GPU版本的TensorFlow,则需要安装CUDA、cuDNN和libcupti-dev开发库。(因为这些是TensorFlow的外部依赖项。)

我是用virtualenv安装的。本来打算用原生pip,但PyCharm自动提供virtualenv环境,那就直接这么装好了。

TensorFlow的几种重要依赖项

  1. Bazel:TensorFlow软件构建
  2. Protocol Buffers数据结构序列化工具和gRPC通信库:TensorFlow的进程间通信机制,以及Serving等周边组件的交互机制所依赖的框架
  3. Eigen线性代数计算库:主要用于在CPU和OpenCL GPU设备上实现TensorFlow的计算类操作
  4. CUDA统一计算设备架构:用于并行计算(是不受Bazel管理的外部依赖项)

源代码结构

根目录

根目录是一个Bazel项目的工作空间,包含了TensorFlow的所有源代码、Bazel构建规则文件,以及一些辅助脚本。

2018.9.22的根目录截图

  • tensorflow/:TensorFlow项目自身的源代码
  • third_party/:部分第三方源代码以及针对第三方项目的Bazel构建规则文件
  • tools/:Bazel构建过程所需的环境配置脚本
  • utils/:现已删除

tensorflow目录

这一目录下的源文件几乎实现了TensorFlow的全部功能,同时体现了TensorFlow的整体模块布局。

2018.9.22的tensorflow目录截图

  • c/:C语言应用层API,亦作为C、C++以外的其他语言应用层API的实现基础
  • cc/:C++语言应用层API
  • compiler/:XLA(Accelerated Linear Algebra)编译优化组件的源代码。
  • contrib/:社区托管的第三方贡献组件
  • core/:TensorFlow核心运行时库的源代码,主要使用C++语言实现
  • doc_src/:TensorFlow软件文档(即TensorFlow官方网站文档)的Markdown源代码
  • examples/:TensorFlow应用开发示例代码
  • g3doc/:旧的文档目录,已弃用
  • go/:Go语言应用层API
  • java/:Java语言应用层API
  • python/:Python语言应用层API
  • security/显然写书的时候还没有这个文件夹
  • stream_executor/:StreamExecutor库的源代码,主要使用C++语言实现,用于管理CUDA GPU上的计算。
  • tensorboard/:TensorBoard组件的源代码,主要使用Python语言实现,用于深度学习过程可视化;现已移入单独仓库tensorboard
  • tools/:TensorFlow构建和运行时使用的工具程序或脚本
  • BUILD:Bazel构建规则文件,用于构建TensorFlow核心运行时库等组件
  • tensorflow.bzl:Bazel构建过程所需的辅助脚本,主要用于定义TensorFlow特有的构建规则
  • workspace.bzl:Bazel构建过程所需的辅助脚本,主要用于定义外部依赖项的下载规则

tensorflow/core目录

2018.9.22的tensorflow/core目录截图

  • api_def/显然写书的时候还没有这个文件夹
  • common_runtime/:核心库的公共运行时源代码,实现了TensorFlow数据流图计算的主要逻辑
  • debug/:用于核心库调试的组件
  • distributed_runtime/:核心库的分布式运行时源代码,实现了TensorFlow分布式运行模式的主要逻辑
  • example/:使用Protocol Buffers创建自定义数据结构并访问序列化文件的示例代码
  • framework/:核心库的框架性组件,包含TensorFlow编程框架中主要抽象的C++或Protocol Buffers定义
  • graph/:数据流图相关抽象和工具类的源代码
  • grappler/:Grappler优化器(一种基于硬件使用成本分析的数据流图优化器)的源代码
  • kernels/:数据流图操作(Op)针对各类计算设备实现的核函数源代码
  • lib/:公共基础库,涉及通用数据结构、常用算法的实现,以及多种图形、音频格式的访问接口类
  • ops/:数据流图操作的接口定义源代码
  • platform/:用于访问特定操作系统或云服务接口的平台相关代码
  • protobuf/:数据流图基本抽象以外的序列化数据结构的Protocol Buffers源代码,例如gRPC接口定义
  • public/:对应用层可见的公开接口的头文件
  • user_ops/:用于存放用户自行开发的数据流图操作,包含一组示例代码
  • util/:核心库内部使用的多种实用工具类或函数的集合,例如用于解析命令行参数和访问环境变量的工具

tensorflow/python目录

2018.9.22的tensorflow/python目录截图

  • autograph/:?
  • client/:TensorFlow主-从模型中的客户端组件,主要包括会话抽象,用于维护数据流图计算的生命周期
  • compat/:?
  • data/:?
  • debug/:用于Python应用程序调试的组件
  • distribute/:?
  • eager/:?
  • estimator/:各类模型评价器(estimator)
  • feature_column/:特征列(feature column)组件
  • framework/:Python API的框架型组件,包含TensorFlow编程框架中主要抽象的Python语言定义。
  • grappler/:Grappler优化器的Python语言接口。
  • keras/:?
  • kernel_tests/:数据流图操作的单元测试代码,有助于用户学习各种操作的使用方法。
  • layers/:预置的神经网络模型层(layer)组件
  • lib/:公共基础库,涉及专用数据结构访问和文件系统I/O等。
  • ops/:数据流图操作的Python语言接口
  • platform/:用于访问特定操作系统或云服务接口的平台相关代码
  • profiler/:?
  • saved_model/:用于访问TensorFlow通用模型序列化格式(SavedModel)的组件
  • summary/:用于生成TensorFlow事件汇总文件(summary)的组件,以便在TensorFlow中可视化计算过程。
  • tools/:若干可独立运行的Python脚本工具,涉及访问和优化模型文件等功能。
  • training/:与模型训练相关的组件,例如各类优化器(optimizer)、模型保存器(saver)等。
  • user_ops/:用于存放用户自行开发的数据流图操作的Python语言接口。
  • util/:用于存放用户自行开发的数据流图操作的Python语言接口
  • build_defs.bzl:Bazel构建过程所需的辅助脚本,主要用于定义Python API特有的构建规则。
  • pywrap_tensorflow.py:间接封装核心库通过C API导出的函数,以便在Python API内部调用核心库的功能。
  • tensorflow.i:对接C API的SWIG接口描述文件,用于在软件构建时为Python API生成核心层的动态链接库。

TensorFlow的安装目录

使用二进制包安装TensorFlow时,pip会将python文件、动态链接库和必要的依赖项复制到当前Python环境的site-packagesdist-packages目录中。

课:机器学习速成课程

课程内容

基本概念

首先介绍了监督式机器学习的概念并给出了一些定义。

标签:我们要预测的事物(简单线性回归中的y变量)

特征:表示数据的方式,用于描述数据的输入变量。

样本:数据的特定实例,分为有标签和无标签两类。

创建模型即从数据中学习规律的过程。

模型生命周期的两个阶段:

  • 训练:创建模型,让模型逐渐学习特征与标签之间的关系
  • 推断:将训练后的模型应用于无标签样本

模型的一种分类方法:

  • 回归模型:预测连续值
  • 分类模型:预测离散值

然后介绍了关于线性回归模型(和通用模型)的一些基本概念。线性回归模型用一条直线对样本的标签进行推断,即

$$y = wx + b$$

损失:对单个样本而言模型预测的准确程度。

给定样本的$L_2$损失(平方误差):$(\text{观察值} - \text{预测值})^2$

数据集上的$L_2$损失:$L_2 loss = \sum_{(x, y) \in D} (y - prediction(x))^2$

均方误差(MSE,Mean Squared Error):每个样本的平均平方损失

在监督式学习中,机器学习算法通过以下方式构建模型:检查多个样本并尝试找出可最大限度地减少损失的模型;这一过程称为经验风险最小化。

减少损失

下一个问题就是如何选择合适的模型参数以减小误差。

常用的方法是,通过计算梯度获得与模型参数相关的误差函数的导数,并沿着梯度指出的方向前进。这可以视为一种迭代法。

迭代法有几个需要考虑的问题。一个问题是沿着这一方向需要前进多少,这被称为学习速率(learning rate)。这是一个超参数。我们需要找到一个合适的学习速率,使得梯度下降过程高效收敛,但又不会高到使该过程永远无法收敛。

另一个问题是起始的位置。对于凸形问题,任意起始位置都是可取的;但很多问题(如神经网络)并不是凸形问题。它们有很多可能的极小值,其中一些比其他更优。

最后一个问题是如何计算梯度。为了得到正确的梯度方向,需要对数据集中所有样本的梯度进行计算。但是一般来说样本量太大,这样计算复杂度太高。所以可以转而计算小型样本的梯度,有两种方法:

  • 随机梯度下降法(SGD,Stochastic Gradient Descent):每次只抽取一个样本计算梯度。这一方法最终会收敛,但可能速度太慢,且过程比较杂乱
  • 小批量梯度下降法(small batch SGD):每批包含10-1000个样本,可以减少杂乱的过程

训练模型是一个迭代的过程:将特征输入当前的模型,返回一个预测作为输出,计算输出的损失,生成新的参数值。当总体损失不再变化或变化极其缓慢时,我们称模型收敛。

梯度是偏导数的矢量,具有两个特征:方向和大小。梯度指向函数增长速度最快的方向,负梯度指向函数下降速度最快的方向。

梯度下降法用梯度乘以一个称为学习速率(步长)的标量,以确定下一个点的位置。如果学习速率过小,则会花费过长的学习时间;否则可能无法收敛。合适的学习速率取决于损失函数的平坦程度。

理想的学习速率:

  • 一维空间:理想学习速率是$1 / f(x)''$
  • 二维或多维空间:理想学习速率是1 / Hessian矩阵
  • 广义凸函数:很难确定

编码练习:使用LinearRegressor构建模型

我参照first_steps_with_tensor_flow.ipynb中的内容在本地写了一遍并运行了一下。

我大概从这一过程中学到了:

  • PyCharm的安装和使用,以及venv虚拟环境的使用
  • 用Estimator API构建和训练模型的基本步骤:
    • 定义optimizerfeature_columns
    • 定义数据输入函数
    • 用数据输入函数对模型进行若干步训练
    • 用预测数据输入函数对模型进行测试
  • pandas.Dataframe的基本使用方法和它与numpy如何联合使用
  • describe、直方图等方法观察数据的分布,寻找离群值
  • matplotlib.pyplot对数据特征和训练过程中的RMSE变化进行可视化表示

下一个问题是如何调整超参数。简单来说,不同超参数的效果取决于数据。因此没有必须遵循的规则,需要对自己的数据进行测试。不过,仍然有一些经验法则:

  • 训练误差应该稳步减小;开始时急剧减小,最终应随着训练收敛达到平稳状态。
  • 如果训练没有收敛,尝试运行更长的时间。
  • 如果训练误差减小速度过慢,则提高学习速率可能有助于加快其减小速度。
    • 但有时如果学习速率过高,训练误差的减小速度反而会变慢。
    • 如果训练误差变化很大,尝试降低学习速率。
    • 较低的学习速率和较大的步数/较大的批量大小通常是不错的组合
  • 批量大小过小也会导致不稳定情况。不妨先尝试100或1000等较大的值,然后逐渐减小值的大小,直到出现性能降低的情况。

以及在代码中:

  • steps:指训练迭代的总次数。一步计算一批样本产生的损失,然后使用该值修改模型的权重一次。
  • batch size:是指单步的样本数量(随机选择)。例如,SGD的批量大小为1。

总被训练样本数 = batch size * steps

  • periods:控制报告的粒度。例如,如果periods设为7且steps设为70,则练习将每10步(共输出7次)输出一次损失值。

每period被训练的样本数 = batch size * steps / periods

我随便调了几组参数。我觉得目前这不是重点,所以就不写了。

完整代码见learnTensorFlow/first_linear_regression/first_steps.py

书:深入理解TensorFlow架构设计与实现原理


本章对TensorFlow进行了一些非常基本的介绍,主要包括以下内容:

  • TensorFlow的发展过程和优缺点
  • TensorFlow的设计目标和独特性
  • TensorFlow的基本架构

由于我现在对TensorFlow其实还没有太多的了解,前两部分我只是粗略地看了一下。

TensorFlow的发展过程和优缺点

TensorFlow是Google出的。其他发展过程略。

TensorFlow的优点:

• 线性代数编译器XLA;可针对不同软硬件环境优化配置
• 框架设计通用:提供高层封装API和底层元素API
• 支持生产环境部署
• 语言接口丰富:核心层是C++,应用层使用SWIG等技术封装
• 端云协同计算

以及丰富的算子库和教学资料。

TensorFlow的缺点:

  • API太丰富,太灵活,学习成本过高。
  • 需要进行一些特殊配置才能使TensorFlow达到最高性能。

TensorFlow的设计目标和独特性

TensorFlow的设计目标:面向多种应用场景和编程范式、支持异构计算平台、具备优异性能与可伸缩性的通用人工智能引擎。

TensorFlow的独特性:

  • 灵活性
  • 端云协同计算
  • 高性能的基础平台软件

(上述内容我看不太懂,因为没有什么了解。)

TensorFlow的基本架构

一般来说,平台设计模式有两种主要形态:

  • 库模式:平台层软件以静态或动态开发库形式存在,应用层开发者需要编写程序调用这些软件;程序入口和整体流程控制权把握在开发者手中
  • 框架模式:平台层软件以可执行文件的形式存在,并以前端交互式程序或后端守护进程方式独立运行。应用层开发者需要遵守接口约束,开发子程序。程序入口和整体流程控制权由框架把握。

TensorFlow采用的是库模式。

TensorFlow的组件结构示意图

上图中的“TensorFlow运行时核心库”就是平时所说的TensorFlow库。这个库是C++编写的,主要分为以下三个层次:

  • 公共运行时:实现了数据流图计算的基本逻辑
  • 分布式运行时:实现了数据流图的跨进程协同计算逻辑
  • 算子核函数:包含图上具体操作结点的算法实现代码

在运行时核心库之上是API层,它对用户屏蔽了核心库的动态链接逻辑。

运行时核心库的底层依赖包括计算库和通信库,其中有些是外部组件(如Eigen),有些是内部集成(如StreamExecutor)。