关于 Leo Van | 范叶亮

京东、美团。现从事数据科学在安全风险领域的技术应用和产品设计。

RSS 地址: https://leovan.me/cn/index.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

Leo Van | 范叶亮 RSS 预览

Shell 调用方式 fork,exec 和 source (Run Shell with fork, exec and source)

2024-05-18 08:00:00

在 Linux 中调用一个脚本有多种方式,例如 fork,exec 和 source。其中 fork 为 Linux 系统调用,exec 和 source 均为 bash 内部命令。下面以 parent.shchild.sh 两个脚本演示不同调用方式的区别。

parent.sh 内容如下:

#!/bin/bash

echo "--------------------------------------------------"
echo "Before calling child.sh"
echo "--------------------------------------------------"
echo "PID for parent.sh: $$"

var="parent"
export var

echo "In parent.sh, set var=$var"
echo "In parent.sh, variable var=$var"

echo "--------------------------------------------------"
case $1 in
    exec)
        echo "Call child.sh using exec"
        exec ./child.sh ;;
    source)
        echo "Call child.sh using source"
        source ./child.sh ;;
    *)
        echo "Call child.sh using fork"
        ./child.sh ;;
esac

echo "After calling child.sh"
echo "--------------------------------------------------"
echo "PID for parent.sh: $$"
echo "In parent.sh, variable var=$var"
echo "--------------------------------------------------"

child.sh 内容如下:

#!/bin/bash

echo "--------------------------------------------------"
echo "PID for child.sh: $$"
echo "In child.sh, variable var=$var from parent.sh"

var="child"
export var

echo "In child.sh, set var=$var"
echo "In child.sh, variable var=$var"
echo "--------------------------------------------------"

为了确保脚本可执行,需为其添加执行权限:

chmod +x parent.sh child.sh

fork

fork 通过进程复制来创建一个新进程,新进程称为子进程,当前进程称为父进程。在 fork 之后,子进程拥有父进程的副本,但两者的 PID 不同,同时子进程也拥有父进程的所有属性,例如:环境变量、打开的文件描述符等。

通过 fork 调用是最普遍的方式。在当前终端中通过 ./run.sh 执行时,终端会新建一个子 shell 执行 run.sh,子 shell 执行时,父 shell 仍在运行,当子 shell 运行完毕后会返回父 shell。

运行如下命令进行 fork 方式调用测试:

./parent.sh fork

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 7149
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using fork
--------------------------------------------------
PID for child.sh: 7150
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------
After calling child.sh
--------------------------------------------------
PID for parent.sh: 7149
In parent.sh, variable var=parent
--------------------------------------------------

exec

exec 与 fork 不同,其不需要开启一个新的 shell 执行子脚本。使用 exec 执行一个新脚本后,父脚本中 exec 后的内容将不再执行。

运行如下命令进行 exec 方式调用测试:

./parent.sh exec

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 9629
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using exec
--------------------------------------------------
PID for child.sh: 9629
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------

source

source 同 exec 类似,也不需要开启一个新的 shell 执行子脚本。使用 source 执行一个新脚本后,父脚本中 source 后的内容可以继续执行。

运行如下命令进行 source 方式调用测试:

./parent.sh source

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 10274
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using source
--------------------------------------------------
PID for child.sh: 10274
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------
After calling child.sh
--------------------------------------------------
PID for parent.sh: 10274
In parent.sh, variable var=child
--------------------------------------------------

重定向和管道 (Redirect and Pipe)

2024-05-12 08:00:00

输入输出文件描述符

在 Linux 启动后,init 进程会创建 3 个特殊的文件描述符分配给输入输出。

文件描述符 英文描述 中文描述
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误

默认情况下,程序经由标准输入(stdin)从键盘读取数据,并将标准输出(stdout)和标准错误(stderr)显示在屏幕上。

在 Linux 中,init 是所有进程的父进程,所有子进程均会继承父进程的文件描述符。因此在 Linux 中执行的所有程序都可以从 stdin 获取输入,并将结果打印到 stdout 中,同时将错误信息打印到 stderr 中。

重定向

当我们不希望从键盘获取标准输入或将标准输出和标准错误显示在屏幕上时,则需要采用重定向。

输出重定向

输出重定向的使用方式如下:

cmd [1-n]> [文件/文件描述符/设备等]

假设当前目录下存在一个名为 yes.txt 的文件,且不存在名为 no.txt 的文件。执行如下命令:

ls yes.txt no.txt

由于 yes.txt 存在,这部分结果将输出到 stdout,同时由于 no.txt 不存在,这部分结果将输出到 stderr。命令的输出结果为:

ls: cannot access 'no.txt': No such file or directory
yes.txt

执行如下命令:

ls yes.txt no.txt 1> success.log 2> fail.log

此时屏幕上将不再显示任何信息,当前目录下会生成 success.logfail.log 两个文件。其中 1> success.log 表示将 stdout 重定向至 success.log2> fail.log 表示将 stderr 重定向至 fail.log。因此 success.log 中的内容为 yes.txtfail.log 中的内容为 ls: cannot access 'no.txt': No such file or directory

重定向过程中,stdout 的文件描述符 1 可以省略,但 stderr 的文件描述符 2 不可以省略。因此,当只重定向 stdout 时,可简写为:

ls yes.txt no.txt > success.log

此时屏幕上依旧会显示 stderr 的内容 ls: cannot access 'no.txt': No such file or directory,而 stdout 的内容则被重定向至 success.log 文件中。

在 Linux 中 &-/dev/null 是两个特殊的输出设备,均表示为空,输出到该设备相当于抛弃输出。因此如下两行命令分别会抛弃 stdout 和 stderr 的内容:

ls yes.txt no.txt 1>&-
ls yes.txt no.txt 2> /dev/null

& 可以表示当前进程中已经存在的描述符,&1 表示 stdout,&2 表示 stderr。因此我们可以将 stdout 和 stderr 重定向到相同文件:

ls yes.txt no.txt > out.log 2> out.log
ls yes.txt no.txt > out.log 2>&1

在上述两种方式中,第一种会导致 out.log 文件被打开两次,stdout 和 stderr 内容会相互覆盖。第二种由于 stderr 重定向给了 stdout,stdout 重定向给了 out.log,因此 out.log 仅被打开了一次。

使用 > 进行输出重定向时会先判断文件是否存在,如果存在会先删除再创建,不存在则直接创建,无论命令是否执行成功均会创建。使用 >> 进行重定向时,如果文件存在则会以添加方式打开,不存在则直接创建。

输入重定向

输入重定向的使用方式如下:

cmd [1-n]< [文件/文件描述符/设备等]

例如:

cat > out.txt < in.txt

此时命令将从 in.txt 文件中获取输入而非 stdin,并将结果重定向到 out.txt 文件中。

Here Document

Here Document 是一种特殊的重定向方式,可以用来将多行输入传递给命令,使用方式如下:

cmd << delimiter
    ...
delimiter

这会将中间的内容 ... 传递给命令。需要注意结尾处的 delimiter 的前后均不能包含任何字符,起始处的 delimiter 的前后空白字符将被忽略。最为常用的 delimiterEOF,但这不是必须的,例如:

wc -l << SOMETHING
第一行
第二行
第三行
SOMETHING

上述命令的输出结果为:

3

管道

管道 | 可以将一个命令的 stdout 作为下一个命令的 stdin 使用,但无法对 stderr 进行处理。因此管道也可以理解为重定向的一种特殊形式。

假设存在一个如下内容的 test.txt 文档:

Here is a test in line 1.
Here is another test in line 2.
Here is something else in line 3.

利用如下命令可以过滤出包含 test 字符的行并显示行号:

cat test.txt | grep -n "test"

上述命令的输出结果为:

1:Here is a test in line 1.
2:Here is another test in line 2.

如果希望同时将 stdout 和 stderr 重定向到下一个命令的 stdin,可以采用如下方式:

ls yes.txt no.txt 2>&1 | grep "No such file or directory"

上述命令的输出结果为:

ls: no.txt: No such file or directory

上述命令也可以简写为:

ls yes.txt no.txt |& grep "No such file or directory"

模型压缩和推理加速 (Model Compression & Inference Acceleration)

2024-04-14 08:00:00

随着深度神经网络模型的复杂度越来越高,除了训练阶段需要大量算力外,模型推理阶段也较多的资源。在深度学习落地应用中,受部署环境的影响,尤其是在边缘计算场景中,有限的计算资源成为了复杂模型的应用壁垒。

复杂模型的部署问题突出表现在三个方面,如下图所示:

  1. 速度:实时响应效率的要求,过长的响应耗时会严重影响用户体验。
  2. 存储:有限的内存空间要求,无法加载超大模型的权重从而无法使用模型。
  3. 能耗:移动场景的续航要求,大量的浮点计算导致移动设备耗电过快。

针对上述三类问题,可以从模型压缩推理加速两个角度出发,在保持一定模型精度的情况下,让模型速度更快、体积更小、能耗更低。

模型压缩

常用的模型压缩方法有如下几种类型:

剪裁

剪裁Pruning)的核心思想是在尽量保持模型精度不受影响的前提下减少网络的参数量,例如减少网络中连接或神经元的数量,如下图所示:

剪裁最常用的步骤如下:

  1. 训练:在整个剪裁过程中,该步骤主要为预训练过程,同时为后续的剪裁工作做准备。
  2. 修剪:通过具体的方法对网络进行剪裁,并对网络重新进行评估以确定是否符合要求。
  3. 微调:通过微调恢复由于剪裁对模型带来的性能损耗。

对网络进行剪裁的具体方法可以分为非结构化剪裁结构化剪裁

非结构化剪裁是细粒度的剪裁方法,一般通过设定一个阈值,高于该阈值的权重得以保留,低于该阈值的权重则被去除。非结构化剪裁虽然方法简单、模型压缩比高,但也存在诸多问题。例如:全局阈值设定未考虑不同层级的差异性,剪裁信息过多有损模型精度且无法还原,剪裁后的稀疏权重矩阵需要硬件层支持方可实现压缩和加速的效果等。

结构化剪裁是粗粒度的剪裁方法,例如对网络层、通道、滤波器等进行剪裁。在滤波器剪裁中,通过评估每个滤波器的重要性(例如:Lp 范数)确定是否保留。结构化剪裁算法相对复杂、控制精度较低,但剪裁策略更为有效且不需要硬件层的支持,可以在现有深度学习框架上直接应用。

量化

神经网络中的计算通常采用浮点数(FP32)进行计算,量化Quantization)的基本思想是将浮点计算替换为更低比特(例如:FP16,INT8 等)的计算,从而降低模型体积加快模型推理速度。

数值的量化可以看做一个近似过程,主要可以分为两类:

  1. 定点近似:通过缩小浮点数表示中指数部分和小数部分的位宽实现。映射过程不需要额外的参数,实现相对简单,但针对较大数值的精度损失较大。
  2. 范围近似:通过统计分析,经过缩放和平移映射浮点数。映射过程需要存储额外的参数,计算时需要先反量化,计算相对复杂,但精度更高。

范围近似又可以分为线性映射非线性映射两种。

线性映射将浮点数映射到量化空间时采用如下计算公式:

$$ \begin{aligned} r &= S \left(q - Z\right) \\ q &= round \left(\dfrac{r}{S} + Z\right) \end{aligned} $$

其中,$r, q$ 分别表示量化前和量化后的值,$S, Z$ 为量化系数。一般化的非对称映射如下图所示:

其中,

$$ \begin{aligned} S &= \dfrac{r_{max} - r_{min}}{q_{max} - q_{min}} \\ Z &= q_{min} - \dfrac{r_{min}}{S} \end{aligned} $$

非线性映射考虑了数据本身的分布情况。以分位量化方法为例,其基本思想是通过分位点对数据进行划分,使得各个区间之间的数据量相等,然后将同一个区间的数据映射为相同值,从而实现量化。

量化粒度是指控制多少个待量化的参数共享一组量化系数,通常粒度越大,精度损失越大。以 Transformer 模型为例,不同粒度的量化方式如下图所示:

其中,$d$ 为模型大小与隐层维度之比,$h$ 为多头自注意中的头数。

模型量化分为两种:

  1. 权重量化:即对网络中的权重执行量化操作。数值范围与输入无关,量化相对容易。
  2. 激活量化:即对网络中不含权重的激活类操作进行量化。输出与输入有关,需要统计数据动态范围,量化相对困难。

根据是否进行训练可以将量化方法分为两大类,如下图所示:

  1. 训练后量化(Post Traning Quantization,PTQ):方法简单高效,无需重新训练模型。根据是否量化激活又分为:
    • 动态:仅量化权重,激活在推理阶段量化,无需校准数据。
    • 静态:量化权重和激活,需要校准数据。
  2. 量化感知训练(Quantization Aware Traning,QAT):方法相对复杂,需要在模型中添加伪量化节点模拟量化,需要重新训练模型。

三种不同量化方法之间的差异如下图所示:

神经结构搜索

神经结构搜索Network Architecture Search,NAS)旨在以一种自动化的方式,解决高难度的复杂神经网络设计问题。根据预先定义的搜索空间,神经结构搜索算法在一个庞大的神经网络集合中评估结构性能并寻找到表现最佳的网络结构。整个架构如下图所示:

近年来基于权重共享的结构搜索方法受到广泛关注,搜索策略和性能评估高度相关,因此两者往往合为一体表示。

搜索空间包含了所有可搜索的网络结构,越大的搜索空间可以评估更多结构的性能,但不利于搜索算法的收敛。搜索空间从搜索方式角度分为两种:

  1. 全局搜索空间:通过链式、跳跃链接、分支等方式搜索整个网络结构。
  2. 基于结构单元的搜索空间:仅搜索结构单元,减少搜索代价,提高结构的可迁移性。

搜索策略即如何在搜索空间根据性能评估选择最优的网络结构。具体包含随机搜索贝叶斯优化进化算法强化学习基于梯度的方法

性能评估最简单的方法就是对数据划分验证集,针对不同的网络结构重新训练并评估其在验证集上的表现。但这种方法所需的计算成本很高,无法在实践中落地应用。一些基于权重共享的结构搜索方法能够一定程度地加速搜索,因此考虑搜索策略和性能评估任务的相关性,在当前架构中往往将这两部分统一表述为搜索策略。

知识蒸馏

知识蒸馏Knowledge Distillation,KD)是一种教师-学生(Teacher-Student)训练结构,通常是已训练好的教师模型提供知识,学生模型通过蒸馏训练来获取教师的知识。它能够以轻微的性能损失为代价将复杂教师模型的知识迁移到简单的学生模型中。

知识蒸馏架构如下图所示:

上半部分为教师模型,下半部分为学生(蒸馏)模型。将教师模型的输出作为软标签与学生模型的软预测计算蒸馏损失,将真实的硬标签与学生模型的硬预测计算学生损失,最终将两种损失结合训练学生模型。

论文 1 给出了软标签的计算公式:

$$ q_i = \dfrac{\exp \left(z_i / T\right)}{\sum_j \exp \left(z_j / T\right)} $$

其中,$T$ 为温度系数,用来控制输出概率的软化程度。不难看出当 $T = 1$ 时,公式的输出即为网络输出 $Softmax$ 的类概率。$T$ 越大,$Softmax$ 的类概率分布越平滑,这可以让学生模型学习到教师模型对负标签的归纳信息。

参考

可供使用的模型压缩库有:

  1. PyTorch:剪裁量化神经网络搜索知识蒸馏
  2. TensorFlow:剪裁量化
  3. PaddleSlim:支持剪裁、量化、神经网络搜索、知识蒸馏,适配 PaddlePaddle 框架。
  4. PocketFlow:支持剪裁、量化,适配 TensorFlow 框架。
  5. NNI:支持剪裁、量化、神经网络搜索,适配 TensorFlow、PyTorch 等框架。
  6. TinyNeuralNetwork:支持剪裁、量化,适配 PyTorch 框架。
  7. DeepSpeed:支持剪裁、量化,适配 PyTorch 框架。
  8. Intel Neural Compressor:支持剪裁、量化、知识蒸馏,适配 TensorFlow、PyTorch、ONNX 等框架。
  9. Neural Network Compression Framework:支持剪裁、量化,适配 TensorFlow、PyTorch、ONNX、OpenVINO 等框架。

推理加速

硬件加速

硬件加速是指将计算交由专门的硬件以获得更快的速度。在深度学习领域最简单的体现就是利用 GPU 进行推理会比利用 CPU 更快。除此之外,在给定的硬件环境中,利用针对性优化的推理框架可以更充分的利用硬件特性提升预测效率。

并行计算

并行计算是指将计算的过程分解成小部分,以并发方式运行实现计算效率的提升。在模型训练和推理阶段,主流的并行方式有:

  1. 数据并行Data Parallel):将数据集切分为多份,每个设备负责其中一部分。
  2. 流水线并行Pipeline Parallel):将模型纵向拆分,每个设备只包含模型的部分层,数据在一个设备完成计算后传递给下一个设备。
  3. 张量并行Tensor Parallel):将模型横向拆分,将模型的每一层拆分至不同设备,每一层计算都需要多个设备合作完成。

参考

可供使用的推理加速库有:

平台 CPU GPU & NPU 框架 系统
TensorRT 服务端 不支持 CUDA TensorFlow
PyTorch
ONNX 等
Windows
Linux 等
Triton 服务端 x86
ARM
CUDA TensorFlow
PyTorch
ONNX 等
Windows
Linux 等
OpenVINO 服务端 Intel
ARM
OpenCL TensorFlow
PyTorch
PaddlePaddle
ONNX 等
Windows
Linux
macOS 等
Paddle Inference 服务端 x86
ARM
CUDA PaddlePaddle Windows
Linux
macOS 等
MNN 服务端
移动端
x86
ARM
CUDA
OpenCL
Vulkan
Metal
HiAI
CoreML
TensorFlow
ONNX 等
Windows
Linux
macOS
Android
iOS 等
TNN 服务端
移动端
x86
ARM
CUDA
OpenCL
Metal
HiAI
CoreML
TensorFlow
PyTorch
ONNX 等
Windows
Linux
macOS
Android
iOS 等
Tensorflow Lite 移动端 ARM OpenCL
Metal
NNAPI
Core ML
Tensorflow Android
iOS 等
PyTorch Mobile 移动端 ARM Vulkan
Metal
NNAPI
PyTorch Android
iOS 等
Paddle Lite 移动端 x86
ARM
OpenCL
Metal
NNAPI
PaddlePaddle Android
iOS 等
ncnn 移动端 x86
ARM
Vulkan TensorFlow
PyTorch
ONNX 等
Android
iOS 等

本文未对模型压缩和推理加速进行深入展开,仅作为工业实践的基础概念解释。本文参考了大量前人之作,在此一并引用 2 3 4 5 6 7


  1. Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. arXiv preprint arXiv:1503.02531↩︎

  2. 模型压缩概述:https://paddlepedia.readthedocs.io/en/latest/tutorials/model_compress/model_compress.html ↩︎

  3. Large Transformer Model Inference Optimization:https://lilianweng.github.io/posts/2023-01-10-inference-optimization/ ↩︎

  4. 深度学习模型压缩方法:剪枝:https://zhuanlan.zhihu.com/p/609126518 ↩︎

  5. 模型量化原理与实践:https://robot9.me/ai-model-quantization-principles-practice/ ↩︎

  6. 李航宇, 王楠楠, 朱明瑞, 杨曦, & 高新波. (2021). 神经结构搜索的研究进展综述. 软件学报, 33(1), 129-149. ↩︎

  7. 黄震华, 杨顺志, 林威, 倪娟, 孙圣力, 陈运文, & 汤庸. (2022). 知识蒸馏研究综述. 计算机学报, 45(3). ↩︎

我们需要多少种编程语言 (How Many Programming Languages do We Need)

2024-02-09 08:00:00

编程语言「只是」达成目标的工具,这是我一直推崇的说法,因为我认为达成目标更重要的在于个人思考,编程语言不过是个「工具」,选哪个并没有那么重要。现在我越来越认为这个「工具」的选择还是很重要的,因为编程语言之于目标实现的可能性和效率都制约了目标的最终达成。

以个人数据科学的工作背景,结合我的编程语言学习路径,尝试回答一下我们需要多少种编程语言这个问题。我的编程语言学习路径大致如下:

语言分类

在我的编程语言学习路径中,各个编程语言之间还是存在很大差异的,以最早接触的 Logo 为例,其除了教学目的之外似乎就真的没有什么大用途了。这些编程语言可以大致划分为如下三种类型:

领域特定语言和标记语言就不用多说了,其应用范围有限。通用编程语言虽然定义为通用,但各自也有擅长和不擅长的领域,需要针对实际场景进行选择。

领域偏好

领域偏好是指你所从事的领域内大家对编程语言的使用偏好,这对于语言的选择至关重要,因为靠一己之力改变领域内大多数人的选择还是相当有难度的,从众可以极大地充分利用前人的成果降低工作的成本。

数据科学

数据科学可选的编程语言有 Python、R 和 Matlab 等,从技多不压身的角度出发肯定是掌握的越多越好,但精力终归是有限的。Matlab 在版权、仿真等方面具有一定的特殊性,Python 和 R 在数据科学中的竞争可谓是旷日持久,相关对比也数不胜数 1 2 3

如果非要二选一我现在会选择 Python,因为其作为通用编程语言在将数据科学和工程代码结合时会展现出更多的优势。但在一些特定领域,例如生物信息学,R 的采用率会更高。一些新起之秀例如 Julia 和 Mojo 仍需要进一步观测其发展,过早地大面积使用新语言可能会面临各种风险。

后端

企业级后端应用中 Java 应该是首选,对一些高性能场景 C/C++、Rust 可能更加适合。

在实际工作中,Python 依旧可以作为一个不错的后端语言选择。Python 在 HTTP 和 RPC 接口、高并发、开源组件 SDK 支持等方面都不错的表现。Python 作为一种解释型语言,时常被诟病运行慢,这确实是解释型语言的一个问题。不过现在很多流行扩展包都是基于 C/C++ 构建,并且从 3.13 版本开始已经可选去除全局解释锁,这些都会使 Python 的性能变得越来越好。个人认为 Python 运行慢的另一个原因是使用者对其理解仍不够深入,使用的技巧仍有待提升,《流畅的 Python》可以让你对 Python 有更深入的认识。

前端

前端是程序与用户进行交互的必经之路,HTML,CSS 和 JavaScript 可以算得上前端三剑客了,分别负责元素的定义、样式和交互。随着 Node.js 的发展,JavaScript 也可以作为后端语言使用。TypeScript 作为 JavaScript 的超集,扩展了 JavaScript 的功能和特性,同时随着 React 和 Vue 框架的出现,前端的发展可谓是盛况空前。

Python 此时就真的很难插入一脚了,不过话无绝对,基于 WebAssembly 技术 Python 也可以在前端运行。WebAssembly 设计的目的是为了提升前端代码的运行效率,而在这方面从实践上 4 来看 Rust 更受青睐。

客户端

在 Apple 和 Google 两大移动阵营中,iOS(iPadOS 和 tvOS)系统的首选语言为 Swift,Android 系统的首选语言为 Kotlin。在 Apple 和 Microsoft 两大桌面阵营中,macOS 系统的首选语言为 Swift,Windows 系统的首选语言为 .Net。原生语言可以让应用更好的适配对应的系统,但引入的问题就是相同应用针对不同系统适配的成本增加。针对大公司的核心应用确实有这个必要,但是普通场景,「跨平台」则会更吸引人。

在之前的博客中有对桌面端的跨平台框架作简要分析,但结合当下移动端的市场占比,基于前端技术的跨平台解决方案会是一个不错的选择。

脚本

脚本可以说就只是程序员自己为了方便而产生的需求,不出意外它应该只会出现在命令行的黑框框中。在类 Unix 系统中,Shell 是一个通用且不需要额外安装扩展的不错之选。除此之外,什么 Python、Perl、Ruby、Lua 都在不同的场景中发光发热,相信我们将处于并将长期处于脚本语言的五代十国中。

嵌入式

早期的嵌入式我认为是一个相对专业垂直的领域,由于操作的对象更加底层,所以使用的语言也会更底层一些,例如 C/C++。随着硬件的不断发展,开发板、智能设备、机器人都在逐步走入更多程序员的视野,硬件性能的提升也使得 Python 等高级语言可以作为嵌入式开发的工具。

个人偏好

领域偏好是站在客观的角度指导我们如何选择语言,但谁还没有些小脾气呢?以自己为例,最早接触的数据分析语言是 Matlab,我的本科和研究生的论文都是用 Matlab 完成的,当时是由于实验室都在用它(领域偏好),但我个人并不很喜欢它。虽然上面我也承认在 Python 和 R 的大战中,如果非让我选一个我会选择 Python,但这也剥夺不了我对 R 的钟爱。R 的管道符 |> 用起来就是舒服,虽然 Python 第三方也提供了类似的扩展包 siuba,但模仿终归还是模仿。

在一定程度上坚持自己的个性还是可能会有些益处的。仍以自己为例,钟爱 R 的我在了解到 RMarkdown 之后让我喜欢上了可重复性研究(更多细节可参见之前的博客)。再到之后的 Quarto,最终我将这些内容融入工作之中,开发工作中适用的产品。可以说如果仅是基于领域偏好彻底拥抱 Python 而抛弃 R,那么我会错过不少优秀的项目和工具。还有不得不提的 ggplot2,图形语法的最好践行者,Python 中 Matlab 的遗留瑰宝 matplotlib 我只能说很强大但我不喜欢用,至于 Python 第三方提供的类似扩展包 plotnine,模仿终归还是模仿。

真实需要

聊了这么多,到底我们需要多少种编程语言?这取决于你到底想要啥?

如果想作为一个安分守己的数据科学工作者,Python 能撑起你 90% 的需求,多花一些时间去了解业务可能比你多学一门编程语言的收益要大得多。

如果你不够安分,不想深藏于后端,甚至有技术变现的想法,那么前端交互自然不可缺失。此时 JavaScript/TypeScript 应该是个不错的选择,毕竟你不是要成为一个专业的前端或 APP 开发人员,跨平台才应该是我们应该偏好的重点,毕竟一套代码处处可用,变现的速度就可以杠杠的了。

当然我相信除了眼前的这些苟且,大家还是有更远大抱负的,那么在这条布满荆棘的路上你会遇到更复杂的问题。性能的提升我会选择 Rust,毕竟人家除了能提升后端还能提升前端,何乐而不为呢?

综上所述,作为数据科学工作的从业者,我将以并将长期以如下四句作为我编程语言选择的重要指导方针:

人生苦短,我用 Python

想要甜些,得上 JavaScript / TypeScript

开心的话,就用 R

再想更屌,恶补 Rust

数据可视化小贴士

2024-01-07 08:00:00

文本主要面向不同格式文档(HTML、PDF、Word)的动态生成,秉承规范、统一和实用的理念总结数据可视化过程中的相关问题,不过度涉及数据可视化本身细节。

一画胜千言(A picture is worth a thousand words)是我个人很推崇的一个指引,不过前提是这得是一张「好图」,否则容易过犹不及。

一画胜千言

数据可视化是一门复杂的学问,在动态文档生成中,秉承规范、统一和实用的理念,我认为是快速提高数据可视化质量的不错之选。

设计规范

在之前的文章「设计语言初探」中探讨过产品的设计语言,在此针对各大企业的数据可视化规范并结合中文文档生成和个人偏好做简要分析。各大企业的数据可视化规范如下:

企业 数据可视化规范
Apple Human Interface Guidelines - Charts
Google Material Design - Data Visualization
Microsoft Data visualization style guidelines for Office Add-ins
Adobe Spectrum - Data Visualization
IBM Carbon Design System - Data Visualization
Salesforce Lightning Design System - Data Visualization
蚂蚁金服 Ant Vision

各家的设计理念有所不同,但我相信其目标是一致的,就是让用户可以更好更快地理解数据并从数据中获取洞见。上面的大多数数据可视化规范依旧是以面向产品设计为主,不过我认为大部分理念是可以迁移到文档中的可视化,尤其是 HTML 格式的动态文档。

Apple、Google、Microsoft 三大家的规范在自家系统的平台上针对简单的可视化场景可以说是最适用的,毕竟原生设计毫无违和感。但针对复杂的可视化场景,三家并没有给出更细的指引,不过也能理解针对商业数据可视化和科技绘图等复杂场景,确实更适合由上层(例如:库、应用等)去根据实际情况作出相应规范。

所以,站在规范统一的视角,我个人更倾向于选择适合中国宝宝体质的 Ant Vision。给出些我认为靠谱的理由:

  1. 设计体系基于具有更悠久历史的 Ant Design 衍生,具有完善的设计规范指引。
  2. 针对简单场景(例如:统计图表等)和复杂场景(例如:地图、关系图表等)都有较好的覆盖。
  3. 科学的色彩体系,在萝卜青菜各有所爱的配色之上给到了科学的指引。
  4. 最后也是我认为最重要的特点,开源,且有丰富的中文文档。

从规范和统一的角度,可以说 Ant Vision 是最优选择,有关 Ant Vision 的更多资料除了官网以外,还可以参见语雀上的 AntV 文档

工具选择

由于是面向动态文档生成为主,基于各种可视化工具的绘图很难嵌入自动化流水线中,因此本节主要讨论相关扩展包,不涉及专用的可视化工具(例如:Tableau,Power BI 等)。常用的可视化扩展包及其支持的语言和图类型,如下表所示:

扩展包 JS/TS Python R 统计图 地图 关系图
Ant Vision ⛔️1
ECharts ☑️2 ☑️3
Plotly
D3 ☑️4 5 ☑️6
Matplotlib
seaborn
plotnine
ggplot2

上述扩展包是我个人在实际项目中真实会使用到的,此时此刻就不难发现 Ant Vision 最大的问题就是对科学编程几乎 7 没有官方支持。

用于可视化的扩展包远不止上述的这 8 种,但我们不可能去学习使用所有的扩展包,这样于自己需要投入大量的学习成本,于团队也不利于项目的维护。即便就这 8 种扩展包,其语法也大不相同。

我接触的第一门绘图语言应该是 Logo,不过 Logo 是一门教学语言,所以在科学编程中使用的最早的是 Matlab 的绘图功能,Matplotlib 从名称上就不难看出是源自 Matlab。不过个人而言不是很喜欢 Matplotlib 的 API 风格,或者说我认为其 API 更偏底层一些。谈到这里就不得不谈一下「The Grammar of Graphics」,这也是我个人认为绘图「最舒服」的 ggplot2 扩展包背后的理论。在上述扩展包中,ggplot2 算是先驱者,plotnine 是 ggplot2 的 Python 复刻,Plotly 的 R 绑定可以支持直接将 ggplot2 对象绘制成 plotly 绘图,Ant Vision 则在该核心理论的基础上做了更多探索。所以当掌握了图形语法的理论基础后,对这些包的学习就相对会简单不少。由于长尾需求还是会存在,所以掌握不同可视化包的使用就成了技多不压身,我认为上述 8 种扩展包覆盖 99% 的可视化需求应该不成问题。

上述 8 种可视化包中,前 4 种都是以 JS/TS 库为基础,部分由官方或三方实现了 Python 和 R 的绑定,所以前 4 种天然就支持可交互绘图。利用 Python 的 Jupyter Widgets 和 R 的 htmlwidgets 实现在网页和 Notebook 中的可交互绘图。D3 背后的 Observable JS 也被当下流行的动态文档生成工具 quarto支持。后 4 种则只能创建不可交互的静态绘图,Matplotlib 作为 Python 绘图的重要可视化扩展包提供了丰富的绘图 API,seaborn 和 plotnine 均基于其开发,seaborn 简化了 API,plotnine 提供了 ggplot2 语法支持,而 ggplot2 则是 R 语言中数据可视化的不二之选。

讲了这么多并不是要在各个扩展包之间分个孰优孰劣,而是需要其特性和需求场景选择合适的扩展包。在此个人愚见如下:

  1. 动态绘图:Plotly > ECharts > D3 > Ant Vision,理由:多语言支持优先。
  2. 动态绘图(复杂场景,例如:地图、关系图等):Ant Vision > D3 > ECharts > Plotly,理由:交互性能优先。
  3. 静态绘图:ggplot2 = plotnine > seaborn > Matplotlib,理由:语法简单易于理解优先。
  4. 静态绘图(长尾需求,例如:示意图等)Matplotlib,理由:较为底层的绘图 API,使用更为灵活。

实用建议

根据规范,为了保证利用不同的工具绘图的视觉效果一致,我们需要在布局、色板、字体等多个角度进行自定义配置。下表展示了扩展包在不用语言绑定中样式的可自定义特性:

扩展包 JS/TS Python R
Ant Vision ☑️详情 - -
ECharts ☑️详情 ☑️详情 ☑️详情
Plotly ☑️详情 详情 ☑️详情,✅复用 ggplot2
D3 详情
Matplotlib - 详情 -
seaborn - ✅复用 Matplotlib -
plotnine - 详情 -
ggplot2 - - 详情

其中,✅表示支持自定义样式,且支持修改全局默认样式;☑️表示支持自定义样式,不支持修改全局默认样式,但支持通过函数一次性设置自定义样式;❌表示支持自定义样式,但需要每次手动配置所有样式细节。

布局

在 Ant Vision 设计语言中一个图应该包含标题、轴、图形、标签、注解、提示信息、图例等信息,如下图所示:

布局

大多数扩展包支持绝大多数元素,但对于一些相对特殊的元素(例如:注解,尤其是富文本的注解)支持较为有限。同时不同扩展包对于相同元素的样式控制也存在差异,这就导致很难将在不同扩展包之间做到完全统一,只能是尽可能相似。

色板

色板(配色)是影响统一的另一大重要因素,相比布局其更好做到不同扩展包之间的统一。根据 Ant Vision 的设计语言,色板分为:分类、顺序、发散、叠加、强调、语义共 6 大色板,如下图所示:

色板

不同扩展包不一定能覆盖所有类型色板(视其绘图能力而定)。除了选择合适的统一色板之外,不同类型色板在使用时也有各自的注意事项,虽然这无关样式统一,但却会从很大程度上影响数据可视化的效果。

字体

根据 Ant Vision 的设计语言,数据可视化字体应当具备三个条件:数字等宽、识别度高、混排美观,如下图所示:

字体

针对文档生成,个人总结了能够覆盖大部分文档场景且商用免费的字体,如下表所示:

字体名称 字体分类 语言 版权 建议使用场景
思源黑体 无衬线黑体 中英文 SIL开源,商用免费 网页正文
思源宋体 衬线宋体 中英文 SIL开源,商用免费 PDF&Word正文
方正仿宋 仿宋 中英文 商用免费 公文
方正楷体 楷体 中英文 商用免费 注释
ETbb 衬线 英文 MIT开源,商用免费 PDF&Word正文
Latin Modern Math TeX数学字体 英文 GFL开源,商用免费 数学公式
更纱黑体 等宽黑体 中英文 SIL开源,商用免费 代码

为了满足 Ant Vision 对字体的要求,在 Word 和 PDF 格式文档中的静态绘图可以使用更纱黑体。针对 HTML 格式文档,由于不会内嵌字体,为了适配不同的操作系统同时考虑不同字体的可用性,可以将交互式绘图字体设置为多个值:

{
  font-family: Iosevka, 'Iosevka Nerd', Consolas, 'Lucida Console', Menlo, Monaco, 'Andale Mono', 'Ubuntu Mono', 'Source Han Mono SC', 'Source Han Mono TC', 'Source Han Mono', 'Noto Sans Mono SC', 'Noto Sans Mono TC', 'Noto Sans Mono', monospace, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important;
}

尺寸 & 响应式

错误的尺寸选择或不良的元素位置摆放都会导致可视化效果变差。PDF 和 Word 格式文档,以及 PC 端的 HTML 格式文档,通过限制页面宽度,可以相对统一的规范到标准的可读内容宽度。此时图片的最大宽度是已知的,可视化时仅需要考虑合适的宽高比即可。

针对移动端的 HTML 文档,由于可读内容宽度较小,绘图的尺寸则需要有针对性地进行调整。此时,使用交互式绘图则比静态绘图有更有优势,利用交互式绘图本身的响应式能力,可以减少人工调整的工作量,也更容易实现一套绘图代码处处可用的效果。

在动态文档生成过程中,我们很难预估数据的真实场景,一些极端情况往往可能会导致可视化结果完全不可用。因此在编写绘图代码时应充分考虑到数据类别的数量、数据类别名称的长度、数据分布等多种因素,同时将绘图中的不同元素摆放在合适的位置。编写出适应性更好的绘图代码才能保证动态文档的高可用性,当然这个过程不是一蹴而就的,随着真实数据中极端情况的积累,绘图代码也会越来越完善越来越鲁棒。


  1. 由第三方 g2r 提供 G2 部分支持,已停止更新。 ↩︎

  2. 由第三方 pyecharts 提供支持。 ↩︎

  3. 由第三方 echarts4r 提供支持。 ↩︎

  4. 由第三方 d3blocks 提供支持。 ↩︎

  5. 由第三方 d3graph 提供关系图支持。 ↩︎

  6. 由第三方 r2d3 提供支持。 ↩︎

  7. L7VP 提供了 Python 绑定。 ↩︎

从 rm -rf * 说起

2023-12-17 08:00:00

故事要从昨晚的事故说起,在软路由中删除了一个 Docker 容器,想着相关配置和数据目录也都用不到了就删掉吧。进入目录后「聪明」的我就执行了 rm -rf *,等回头去看命令执行情况已然为时已晚。为什么说我「聪明」呢,因为自从知道一个空格引起的 /usr 被删除的血案后,在做删除动作时我都会谨慎再谨慎,然而这次的悲剧在于目录下通过 NFS 挂载了 NAS 上的远程目录,删除前忘记取消挂载了,结果就是 NAS 上 4 块盘里面的影视资料被我一键清空了。

想着十多个 TB 的影视资料就这么没了,到也没有太伤感,毕竟技术男认为总还是可以恢复的,无非就是费些时间的问题。所以做的第一件事就是把 NAS 关机了,因为一旦再写入新的数据,被删除的数据可能就真的无法恢复了。关机后就开始找 SATA 线(NAS 里面是 3.5 寸的机械硬盘,使用 SATA 口通信),发现没有就赶紧买了一根第二天可以到的,至此第一笔 60 大洋(3.5 寸的硬盘还得单独供电,好不容易找到一个便宜的带电源的套装)损失就出去了。然后就开始各种找资料,NAS 里面的硬盘格式是 Btrfs 的,可用的恢复工具一下子就少了,翻着翻着发现就已经凌晨一点。怀着一丝丝担忧还是决定先睡了,明天早起再说吧,反正 SATA 线最快也得下午才能到。

喜新

这一切的一切要往前捯就只怪我「喜新厌旧」。搬到新家利用软路由和 NAS 搭建了一套家庭影音中心,老老实实看就得了呗,非要瞎鼓捣。在 Jellyfin 中显示的影视信息读取的元信息文件有些问题,提的 PR 也是做了各种测试才成功合并到主干,尽管只是改了一行代码的位置,但维护人员的严谨还是很让我受教的。虽然合并到了主干,但由于大版本更新发布还未确定时间,当时自己就临时针对当前版本调整了代码编译部署到了自己的软路由上先用起来了。

后续稳定版本也发布了几个修复问题的小版本更新,但合并的代码并不在更新范围内,自己懒了也就没再更新 Docker 镜像。直到昨天晚上也许就是闲来无事,想着要不就更新到非稳定版本用吧,同时一不做二不休还把刮削用的 tinyMediaManager 也更新下吧,然后就没有然后了,事故就发生了。

我是一个比较喜欢尝鲜的人,每天到公司不执行一下 brew update & brew upgrade 就不舒服。而且还不能搞成定时任务,就得手动执行,然后看着相关工具更新到最新版本就会很舒服。我承认在一些「大型」项目中,兼容和稳定才是第一追求,但我认为我还真么参与到过那种「大」到处处都要为兼容和稳定考虑的项目中过,所以我还是很喜欢尝试新的特性。当然工作中尝鲜用到的也得至少是正式发布的版本。最近的一次大迁移就是把我所有的 Python 项目都从 3.7 升级到了 3.10,为啥不是 3.11 呢,因为有些依赖包没我走得快,对 3.11 还不支持。我大部分是在做数据和算法工作,少部分时间也会用 Python 写一些工程性质的代码,在这个领域我认为「喜新」是一件好事,这会让你在面对一个问题时更有可能说出「我行」。

怀旧

我其实还挺两面派的,刚刚还在「喜新厌旧」,现在又说「怀旧」。老人经常说人要有个「念想」,但此时此刻我发现我的「念想」有点儿多了,多到可能束缚到了我的前行。NAS 是我大概 6 年前买的了,去年淘汰了里面两块老旧的小硬盘,又补上了两块 8TB 的。里面存的都是从 PT 站下载的高清影视资料,今年也终于成功的将 PT 站的账号升到了永久保号的等级,看了看上传量也近 20TB 了。文件被删掉的那一刻都是在想怎么恢复,但当发现恢复有一定的难度后,我突然有在想要不要恢复。

其实这么多年下载了这么多高清的影视资料,真正回过头再去看的次数不算多,一是没那么多时间,二是有新的内容可以选择。所以这么多「怀旧」的东西可能并不是「念想」,而只是满足「占有欲」的电子榨菜。这就和买书与读书是一回事儿,不是买了书有了书,知识就是你的了。多年前的我认识到了这个问题,但好像改进的并不理想,尤其是疫情这几年,身体没空去旅行,然而思想也没有在路上。不过感觉近期有所改善,至少近几个月快读完三本书了。

中午眯了会儿醒来还是决定把四块硬盘重新格式化了,也正好把之前一个磁盘和存储空间顺序不一致的问题做了纠正,如实让我这个强迫症选手舒服了一些。后面把原来移动硬盘中的照片重新又备份了一份到 NAS 中,「念想」这东西有点儿就够了。弄完了,心情一下子轻松了不少,毕竟不用再为恢复数据发愁了,尤其是在这种你还不知道能不能恢复的情况下。唯一还有些心疼的就是花 60 大洋买的 SATA 线,看了看快递小哥已经送上货了,就留着吧,万一将来有啥用呢。

再出发

后疫情时代的第一年马上就要过去了,这一年的种种个人感觉都不尽如人意,也正是不如人意才会让我去思考更多,改变更多。变革不一定会创造机会,哪怕创造了机会能不能抓住也难说,但对变革的思考和自身的改变是可以主动发起的。抛下一些陈旧的包袱,再去想想你真正想要的,透彻一些,会发现变革不一定是坏事。

大家都在往前走,这是我们认为的,也是我们想要的,至少是我想要的。可我真的在往前走吗?或许慢慢地陷入了舒适圈,或许被过往的「念想」不知不觉牵绊住了,所以不经意的一个变数,或好或坏,都给了我们重新审视自我和世界的机会。停下来,思考片刻,再出发,来得及,也值得。

当我谈摄影时,我谈些什么

2023-07-30 08:00:00

色域

当我谈修图时,我谈些什么 - 色彩篇 Part 1 中已经介绍过什么是色彩空间,在显示领域通常会使用 RGB 色彩模型,在印刷领域通常会使用 CMYK 色彩模型。而在颜色感知领域CIE 1931 色彩空间 则是在设计之初便要求包含普通人眼可见的所有颜色的标准色彩空间。

人类眼睛有对于短、中和长波的感光细胞,色彩空间在描述颜色时则可以通过定义三种刺激值,再利用值的叠加表示各种颜色。在 CIE 1931 色彩空间中,这三种刺激值并不是指对短、中和长波的反应,而是一组约略对应红色、绿色和蓝色的 X、Y 和 Z 的值。X、Y 和 Z 的值并不是真的看起来是红色、绿色和蓝色,而是使用 CIE XYZ 颜色匹配函数计算而来。

颜色匹配实验中,如下图 1 所示:

受试者通过观察单一光源的颜色和三原色光源的混合颜色是否相同,得到光谱三刺激值曲线如下图 2 左所示。为了消除负值对数据处理带来的不便,通过转换得到了三个新的值 $X$、$Y$ 和 $Z$ 的曲线如下图 2 右所示。

在 CIE 1931 色彩空间中,所有可视颜色的完整绘图是三维的,$Y$ 可以表示颜色的明度 3。$Y$ 表示明度的好处是在给定 $Y$ 值时,XZ 平面将包含此明度下的所有色度。通过规范化 $X$、$Y$ 和 $Z$ 的值:

$$ \begin{aligned} x &= \dfrac{X}{X + Y + Z} \\ y &= \dfrac{Y}{X + Y + Z} \\ z &= \dfrac{Z}{X + Y + Z} = 1 - x - y \end{aligned} $$

色度可以使用 $x$ 和 $y$ 来表示。CIE 1931 的相对色度图 4 如下所示:

外侧曲线边界是光谱轨迹,波长用纳米标记。不同色域(Color Gamut)标准之间的对比如下图 5 所示:

对于一个显示设备来说,不可能产生超过其色域的颜色。通常情况下,讨论一台摄影设备的色域并没有意义,但使用什么样的色彩空间进行编码则需要重点关注。

色彩深度

色彩深度,简称色深(Color Depth),即存储一个像素的颜色所需要的位数。若色彩深度为 $n$ 位,则代表一共包含 $2^n$ 种颜色。例如我们常说的真彩色,即 24 位,对应 RGB 三个通道,每个通道 8 位(即 0-255),共可以表示 16,777,216 种颜色。

24bit(98KB)
8bit(37KB -62%)
4bit(13KB -87%)
2bit(6KB -94%)

从上述对比图 6 中不难看出,色深越大,图像的效果越好,图像内容之间的过度越自然,与此同时占用的存储也会越多。

在视频拍摄中,我们通常说的 8bit 和 10bit 指的是位深(Bit Depth),即每个通道的位数。设备在拍摄素材时,记录更大位数的信息会更有利于后期调色等处理。

在显示器的特性中,我们也经常会遇见 8bit 和 10bit,以及 8bit FRC 这个概念。FRCFrame Rate Control 的缩写,即帧率控制,是一种时间维度的像素抖动算法。以灰度图像为例,如下图所示,当渲染一个图像包含多个帧时,可以让帧在明暗之间进行切换,从而产生中间灰度。

相应的空间维度的像素抖动算法(Dither)如下图所示:

所以,一块原生 10bit 屏幕优于 8bit FRC 10bit 的屏幕优于原生 8bit 的屏幕。

色度抽样

在拍摄视频时,除了 8bit 和 10bit 位深的区别外,我们还经常听到 4:2:2 和 4:2:0 等比值,这代表色度抽样。由于人眼对色度的敏感度不及对亮度的敏感度,图像的色度分量不需要有和亮度分量相同的清晰度,在色度上进行抽样可以在不明显降低画面质量的同时降低影像信号的总带宽。

抽样系统通常用一个三分比值表示:$J : a : b$,其中:

不同的比值色度抽样对比图 7 如下所示:

动态范围

动态范围Dynamic Range)是可变信号(例如声音或光)最大值和最小值的比值。在相机中,设置不同的 ISO 会影响到动态范围在记录高光和暗部时的噪点表现。

高动态范围High Dynamic RangeHDR)相比与标准动态范围Standard Dynamic RangeSDR)具有更大的动态范围,简而言之 HDR 可以让画面中亮的地方足够亮暗的的地方足够暗。HDR 需要采集设备和显示设备同时支持才能够得以正常的显示,下图 8 展示了 HDR 和 SDR 从场景采集到显示还原的过程:

最终 SDR 和 HDR 成像的区别如下图 9 所示(模拟效果):

在摄影过程中,如下两种方式都可以得到不错的 HDR 照片:

  1. 针对 RAW 格式照片,其存储的不同明暗数据已经足够多,针对高光降低一些曝光,暗部增加一些曝光即可获得 HDR 照片。
  2. 前期进行包围曝光,即在拍摄时同时拍摄多张具有不同曝光补偿的照片,后期再利用曝光合成技术得到一张 HDR 照片。

在摄像过程中,上述的两种方案就变得不太可行了,如果对于视频的每一帧都保存 RAW 信息会导致视频素材体积过大。此时我们会采用一种名为 Log 曲线的方式对视频的每一帧图像进行处理。

首先我们需要了解一下什么是曝光量Photometric Exposure)和曝光值Exposure ValueEV)。曝光量是指进入镜头在感光介质上的光量,其由光圈、快门和感光度组合控制,定义为:

$$ H = Et $$

其中,$E$ 为影像平面的照度,$t$ 为快门的曝光时间。影像平面照度与光圈孔径面积成正比,因此与光圈 $f$ 值的平方成反比,则有:

$$ H \propto \dfrac{t}{N^2} $$

其中,$N$ 为光圈的 $f$ 值。$\dfrac{t}{N^2}$ 这个比例值可以用于表示多个等效的曝光时间和光圈 $f$ 值组合。此比值具有较大的分母,为了方便使用反转该比值并取以 $2$ 为底的对数则可以得到曝光值的定义:

$$ EV = \log_2{\dfrac{N^2}{t}} = 2 \log_2{\left(N\right)} - \log_2{\left(t\right)} $$

在现实中,随着光线强度(类比曝光量)的成倍增加,人眼对于光的感应(类比曝光值)大约成线性增长。同时,摄像机器对于光线强度的记录是线性的,也就是说当光线强度翻倍时,转换后存储的数值也会翻倍。

以 8bit 为例,对于高光部分(7 - 8 档曝光值)会使用 128 位存储相关信息,而对于暗部(0 - 1 档曝光值)则仅使用 8 位存储相关信息,如下图左所示。此时由于高光部分看起来亮度变化并不大,使用的存储位数比暗部多得多,这种非均衡的的存储容易丢失图像的暗部细节。通过对曝光量进行 Log 处理,可以得到均衡的对应关系,如下图右所示。

EV 和曝光量关系
EV 和曝光量 $\log$ 值关系

在真实场景中,各个相机厂商的所搭载的 Log 曲线并不完全相同,都会为了实现某种效果进行调整修改。但整体来说其目的还是为了让每一档曝光值之间存储的信息量大致相同。颜色矫正后和原始应用 Log 曲线的对比图像 10 如下所示:


  1. Verhoeven, G. (2016). Basics of photography for cultural heritage imaging. In E. Stylianidis & F. Remondino (Eds.), 3D recording, documentation and management of cultural heritage (pp. 127–251). Caithness: Whittles Publishing. ↩︎

  2. Patrangenaru, V., & Deng, Y. (2020). Nonparametric data analysis on the space of perceived colors. arXiv preprint arXiv:2004.03402. ↩︎ ↩︎

  3. https://en.wikipedia.org/wiki/CIE_1931_color_space#Meaning_of_X,_Y_and_Z ↩︎

  4. https://commons.wikimedia.org/wiki/File:CIE1931xy_blank.svg ↩︎

  5. https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison.svg ↩︎

  6. https://en.wikipedia.org/wiki/Color_depth ↩︎

  7. https://en.wikipedia.org/wiki/Chroma_subsampling ↩︎

  8. https://www.benq.com/en-my/knowledge-center/knowledge/what-is-hdr.html ↩︎

  9. https://kmbcomm.com/demystifying-high-dynamic-range-hdr-wide-color-gamut-wcg/ ↩︎

  10. https://postpace.io/blog/difference-between-raw-log-and-rec-709-camera-footage/ ↩︎

CSS 布局和定位 (CSS Display & Position)

2023-05-03 08:00:00

CSS 中的布局 display 和定位 position 可以说是两个最基本的属性,其控制着元素在网页中的显示方式。之前对布局和定位可谓是一知半解,最终奏不奏效全凭一顿乱试😂,想了想还是应该细致地了解下,后面虽不妄想写起代码来事半功倍,但至少不会再暴力遍历破解了。

盒模型

在介绍布局和定位之前,首先回顾一下 CSS 的盒模型。CSS 盒模型从外到内由外边距 margin边框 border内边距 padding内容 content 共 4 部分组成,如下图所示:

CSS 盒模型

元素的宽度 width 为内容的宽度 + 左边框 + 有边框 + 左内边距 + 右内边距,上例中为 $360+10+10+10+10=400$;元素的的高度 height 为内容的高度 + 上边框 + 下边框 + 上内边距 + 下内边距,上例中为 $240+10+10+20+20=300$。在实际中,我们并不能直接设定内容的宽度和高度,只能设置元素的宽度和高度,而显示区域的宽度和高度则通过计算自动设定。

在 CSS 中广泛使用的有两种盒子模型:块级盒子(block box)内联盒子(inline box)1

块级盒子有如下表现行为:

除非特殊指定,诸如标题 (<h1> 等) 和段落 (<p>) 默认情况下都是块级的盒子。

内联盒子有如下表现行为:

<a><span><em> 以及 <strong> 都是默认处于 inline 状态的。

布局

在 CSS 中使用 display 属性控制元素的布局方式,上文中的 blockinline 是最常用的两种布局方式。除此之外还有一种介于块级盒子和内联盒子之间的布局方式,即 inline-block,其具有如下表现行为:

这是一段包含 span 元素的文本。display: inline 的 span 元素的 width 和 height 属性无法发挥作用。

这是一段包含 span 元素的文本。display: inline-block 的 span 元素的 width 和 height 属性可以发挥作用。

上图分别展示了 display: inlinedisplay: inline-block 两种布局 span 元素的显示差异。

弹性布局

本节内容主要参考自:A Complete Guide to Flexbox

弹性布局(Flexbox Layout,Flexible Box Layout) 旨在提供一种更加有效的方式来布局、对齐和分配容器中元素之间的空间,即使元素的大小是未知或动态的,这也就是称为“弹性”的原因。

弹性布局是一套完整的模块而非一个单一的属性,其中一些属性要设置在父元素(flex container) 上,一些属性要设置在子元素(flex items) 上。常规布局是基于块级元素和内联元素的的流向,而弹性布局是基于弹性流向(flex-flow directions)。下图展示了弹性布局的基本思想:

Flexbox 基本思想

父元素属性

display

该属性启用弹性容器,为其子元素开启弹性上下文。

.container {
  display: flex; /* 或 inline-flex */
}

flex-direction

该属性定义了弹性流向,即基本思想中的 main-axis

.container {
  flex-direction: row | row-reverse | column | column-reverse;
}

flex-wrap

默认情况下会将子元素放置在一行中,该属性用于设置换行模式。

.container {
  flex-wrap: nowrap | wrap | wrap-reverse;
}

flex-flow

该属性是 flex-directionflex-wrap 两个属性的简写。

.container {
  flex-flow: column wrap;
}

justify-content

该属性用于设置主轴(main axis)方向的对齐方式。

.container {
  justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}

align-items

该属性用于设置交叉轴(cross axis)方向的对齐方式。

.container {
  align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}

align-content

该属性用于设置当交叉轴上有额外的空间时容器多行的内部对齐方式,类似 justify-content 设置主轴上子元素的对齐方式。

注意

该属性仅对包含多行子元素的容器有效。
.container {
  align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}

gap, row-gap, column-gap

该属性用于控制子元素之间的间距,其仅用于非边缘子元素之间的间距。

.container {
  display: flex;
  ...
  gap: 10px;
  gap: 10px 20px; /* row-gap column gap */
  row-gap: 10px;
  column-gap: 20px;
}

该属性产生的行为可以认为是子元素之间的最小间距。

子元素属性

order

默认情况下,子元素按照代码顺序排列。该属性可以控制子元素在容器中的顺序。

.item {
  order: 5; /* 默认为 0 */
}

flex-grow

该属性定义了子元素在必要时的扩张能力,其接受一个整数比例值用于设定子元素占用容器的空间。如果所有子元素的 flew-grow 都设置为 1,则所有子元素将评分容器的剩余空间;如果一个子元素的 flex-grow 设置为 2,则该子元素将尝试占用其他子元素 2 倍大小的空间。

.item {
  flex-grow: 4; /* 默认为 0 */
}

flex-shrink

该属性定义了子元素在必要时的收缩能力。

.item {
  flex-shrink: 3; /* 默认为 1 */
}

flex-basis

该属性定义了分配剩余空间之前子元素的默认大小。其可以为例如 20%5rem 之类的长度或一个关键字。

.item {
  flex-basis:  | auto; /* 默认为 auto */
}

flex

该属性是 flex-growflex-shrinkflex-basis 三个属性的简写。

.item {
  flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

align-self

该属性可以覆盖由 align-items 指定的对齐方式。

.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

网格布局

本节内容主要参考自:A Complete Guide to CSS Grid

网格布局(Grid Layout) 是一种基于网格的布局系统,相比于沿轴线 一维布局 的弹性布局,网格布局可以看做是一种 二维布局

核心概念

网格容器

网格容器即属性 displaygrid 的元素,其为所有网格项目的直接父级。如下示例中,container 即为网格容器:

<div class="container">
  <div class="item item-1"> </div>
  <div class="item item-2"> </div>
  <div class="item item-3"> </div>
</div>

网格项目

网格项目为网格容器的直接后代。如下示例中,item 即为网格项目,但 sub-item 不是:

<div class="container">
  <div class="item"> </div>
  <div class="item">
    <p class="sub-item"> </p>
  </div>
  <div class="item"> </div>
</div>

网格线

网格线即构成网格结构的分界线。其可以是位于行或列任意一侧的垂直或水平线。如下示例中,黄色的线为一条列网格线:

网格单元

网格单元即两个相邻行和两个相邻列之间的区域。如下示例中,黄色区域为行网格线 1 和 2 以及列网格线 2 和 3 之间的单元格:

网格轨道

网格轨道即 2 条相邻网格线之间的区域,可以将其视为网格的行或列。如下示例中,黄色区域为第 2 行和第 3 行网格线之间的网格轨道:

网格区域

网格区域即 4 条网格线包围的区域,一个网格区域可以由任意数量的网格单元组成。如下示例中,黄色区域为行网格线 1 和 3 以及列网格线 1 和 3 之间的网格区域:

父元素属性

display

该属性启用网格容器,为其子元素开启网格上下文。

.container {
  display: grid | inline-grid;
}

grid-template-columns, grid-template-rows

该属性通过空格分隔的值列表定义网格的列和行,值代表轨道的大小。值列表包括:

.container {
  grid-template-columns: ...  ...;
  /* 例如:
      1fr 1fr
      minmax(10px, 1fr) 3fr
      repeat(5, 1fr)
      50px auto 100px 1fr
  */
  grid-template-rows: ... ...;
  /* 例如:
      min-content 1fr min-content
      100px 1fr max-content
  */
}

网格线默认将会被分为正整数(-1 作为最后一个的替代值)。

同时也可以明确指定这些线的名称,请注意括号命名语法:

.container {
  grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];
  grid-template-rows: [row1-start] 25% [row1-end] 100px [third-line] auto [last-line];
}

请注意,一个行或列可以有多个名称:

.container {
  grid-template-rows: [row1-start] 25% [row1-end row2-start] 25% [row2-end];
}

使用 repeat() 可以简化重复项:

.container {
  grid-template-columns: repeat(3, 20px [col-start]);
}

上述代码等效于:

.container {
  grid-template-columns: 20px [col-start] 20px [col-start] 20px [col-start];
}

如果多行或多列共享相同的名称,可以通过行名或列名和计数来引用它们:

.item {
  grid-column-start: col-start 2;
}

fr 单位允许将轨道的大小设置为网格容器可用空间的一定比例。例如,如下示例将每个项目设置为容器宽度的三分之一:

.container {
  grid-template-columns: 1fr 1fr 1fr;
}

可用空间是在所有非弹性项目之后计算得到。在上述示例中,fr 单位的可用空间总量不包括 50px

.container {
  grid-template-columns: 1fr 50px 1fr 1fr;
}

grid-template-areas

该属性通过引用网格区域的名称 grid-area 来定义网格。重复网格区域名称会导致内容跨越这些单元格。句点表示一个空单元格。语法本身提供了网格结构的可视化。

.container {
  grid-template-areas: 
    "<grid-area-name> | . | none | ..."
    "...";
}
.item-a {
  grid-area: header;
}
.item-b {
  grid-area: main;
}
.item-c {
  grid-area: sidebar;
}
.item-d {
  grid-area: footer;
}

.container {
  display: grid;
  grid-template-columns: 50px 50px 50px 50px;
  grid-template-rows: auto;
  grid-template-areas: 
    "header header header header"
    "main main . sidebar"
    "footer footer footer footer";
}

上述示例将创建一个 4 列 3 行的网格。整个顶部为 header 区域,中间一行由 mainsidebar 两个区域和一个空单元格组成,最后一行为 footer

声明中的每一行都需要有相同数量的单元格。可以使用任意数量的句点声明一个空单元格,只要句点之间没有空格,就代表一个单元格。

注意使用此语法仅可以命名区域,不可命名线。使用此语法时,区域两端的线会自动命名,如果网格区域名称为 foo,那么该区域的起始行线和起始列线名称为 foo-start,该区域的终止行线和终止列线名称为 foo-end。这意味着某些线可能有多个名称,上述示例中最左边的行线将有 3 个名称:header-startmain-startfooter-start

grid-template

该属性是 grid-template-rowsgrid-template-columnsgrid-template-areas 三个属性的简写。

.container {
  grid-template: none | <grid-template-rows> / <grid-template-columns>;
}

其接受更复杂但更方便的语法来指定这三个值,例如:

.container {
  grid-template:
    [row1-start] "header header header" 25px [row1-end]
    [row2-start] "footer footer footer" 25px [row2-end]
    / auto 50px auto;
}

上述代码等效于:

.container {
  grid-template-rows: [row1-start] 25px [row1-end row2-start] 25px [row2-end];
  grid-template-columns: auto 50px auto;
  grid-template-areas: 
    "header header header" 
    "footer footer footer";
}

由于 grid-template 并不会重置网格的隐含属性(grid-auto-columnsgrid-auto-rowsgrid-auto-flow)。因此,建议使用 grid 属性而非 grid-template

column-gap, row-gap, grid-column-gap, grid-row-gap

该属性用于指定网格线的大小,你可以将其看做列和行之间的间距。

.container {
  /* standard */
  column-gap: <line-size>;
  row-gap: <line-size>;

  /* old */
  grid-column-gap: <line-size>;
  grid-row-gap: <line-size>;
}
.container {
  grid-template-columns: 100px 50px 100px;
  grid-template-rows: 80px auto 80px; 
  column-gap: 10px;
  row-gap: 15px;
}

间距仅在列和行之间创建,不在边缘创建。注意,带有 grid- 前缀的属性将被废弃。

gap, grid-gap

该属性为 row-gapcolumn-gap 两个属性的简写。

.container {
  /* standard */
  gap: <grid-row-gap> <grid-column-gap>;

  /* old */
  grid-gap: <grid-row-gap> <grid-column-gap>;
}
.container {
  grid-template-columns: 100px 50px 100px;
  grid-template-rows: 80px auto 80px; 
  gap: 15px 10px;
}

如果未指定 row-gap,则它将被设置为与 column-gap 相同的值。注意,带有 grid- 前缀的属性将被废弃。

justify-items

沿 inline(行)轴对齐网格项(与沿 block(列)轴对齐 align-items 相反)。该属性将应用于容器内所有网格项。

.container {
  justify-items: stretch | start | end | center;
}
.container {
  justify-items: stretch;
}
.container {
  justify-items: start;
}
.container {
  justify-items: end;
}
.container {
  justify-items: center;
}

align-items

沿 block(列)轴对齐网格项(与沿 inline(行)轴对齐 align-items 相反)。该属性将应用于容器内所有网格项。

.container {
  align-items: stretch | start | end | center;
}
.container {
  align-items: stretch;
}
.container {
  align-items: start;
}
.container {
  align-items: end;
}
.container {
  align-items: center;
}

通过 align-self 属性可以在单个网格项上覆盖由 align-items 指定的对齐方式。

place-items

该属性在单次声明中同时设置 align-itemsjustify-items 属性。

.center {
  display: grid;
  place-items: center;
}

justify-content

当所有网格项均使用非弹性的单位(例如 px)来确定大小,则网格的总大小可能小于网格容器的大小。在这种情况下,可以在网格容器内设置网格的对齐方式。该属性沿 inline(行)轴(与沿 block(列)轴对齐 align-content 相反)对齐网格。

.container {
  justify-content: start | end | center | stretch | space-around | space-between | space-evenly;    
}
.container {
  justify-content: start;
}
.container {
  justify-content: end;
}
.container {
  justify-content: center;
}
.container {
  justify-content: stretch;
}
.container {
  justify-content: space-around;
}
.container {
  justify-content: space-between;
}
.container {
  justify-content: space-evenly;
}

align-content

当所有网格项均使用非弹性的单位(例如 px)来确定大小,则网格的总大小可能小于网格容器的大小。在这种情况下,可以在网格容器内设置网格的对齐方式。该属性沿 block(列)轴(与沿 inline(行)轴对齐 justify-content 相反)对齐网格。

.container {
  align-content: start | end | center | stretch | space-around | space-between | space-evenly;    
}
.container {
  align-content: start;    
}
.container {
  align-content: end;    
}
.container {
  align-content: center;    
}
.container {
  align-content: stretch;    
}
.container {
  align-content: space-around;    
}
.container {
  align-content: space-between;    
}
.container {
  align-content: space-evenly;    
}

place-content

该属性在单次声明中同时设置 align-contentjustify-content 属性。

grid-auto-columns, grid-auto-rows

该属性指定自动生成的网格轨道(也称为隐式网格轨道)的大小。当网格项多于网格中的单元格或当网格项放置在显示网格之外时,将创建隐式网格轨道。

.container {
  grid-auto-columns: <track-size> ...;
  grid-auto-rows: <track-size> ...;
}
.container {
  grid-template-columns: 60px 60px;
  grid-template-rows: 90px 90px;
}

上述代码将生成一个 2x2 的网格:

使用 grid-columngrid-row 来定位网格项:

.item-a {
  grid-column: 1 / 2;
  grid-row: 2 / 3;
}
.item-b {
  grid-column: 5 / 6;
  grid-row: 2 / 3;
}

.item-b 从第 5 列线开始到第 6 列线结束,但由于并未定义第 5 列线和第 6 列线,因此创建了宽度为 0 的隐式轨道用于填充间隙。使用 grid-auto-columnsgrid-auto-rows 可以指定这些隐式轨道的宽度:

.container {
  grid-auto-columns: 60px;
}

grid-auto-flow

如果有未明确放置在网格中的网格项目,自动放置算法会自动放置这些网格项目。此属性用于控制自动放置算法的工作方式。

.container {
  grid-auto-flow: row | column | row dense | column dense;
}

注意 dense 仅会改变网格项目的视觉顺序,这可能导致顺序混乱且不利于访问。

考虑如下示例:

<section class="container">
  <div class="item-a">item-a</div>
  <div class="item-b">item-b</div>
  <div class="item-c">item-c</div>
  <div class="item-d">item-d</div>
  <div class="item-e">item-e</div>
</section>

定义一个包含 5 列和 2 行的网格,并将 grid-auto-flow 设置为 row

.container {
  display: grid;
  grid-template-columns: 60px 60px 60px 60px 60px;
  grid-template-rows: 30px 30px;
  grid-auto-flow: row;
}

将网格项目放置在网格中时,只需要为其中两个指定位置:

.item-a {
  grid-column: 1;
  grid-row: 1 / 3;
}
.item-e {
  grid-column: 5;
  grid-row: 1 / 3;
}

因为将 grid-auto-flow 设置为了 row,未放置的三个网格项目(item-bitem-citem-d)如下所示:

.container {
  display: grid;
  grid-template-columns: 60px 60px 60px 60px 60px;
  grid-template-rows: 30px 30px;
  grid-auto-flow: column;
}

如果将 grid-auto-flow 设置为 column,未放置的三个网格项目(item-bitem-citem-d)如下所示:

grid

该属性为 grid-template-rowsgrid-template-columnsgrid-template-areasgrid-auto-rowsgrid-auto-columnsgrid-auto-flow 属性的简写。

如下示例中的代码是等效的:

.container {
  grid: 100px 300px / 3fr 1fr;
}

.container {
  grid-template-rows: 100px 300px;
  grid-template-columns: 3fr 1fr;
}
.container {
  grid: auto-flow / 200px 1fr;
}

.container {
  grid-auto-flow: row;
  grid-template-columns: 200px 1fr;
}
.container {
  grid: auto-flow dense 100px / 1fr 2fr;
}

.container {
  grid-auto-flow: row dense;
  grid-auto-rows: 100px;
  grid-template-columns: 1fr 2fr;
}
.container {
  grid: 100px 300px / auto-flow 200px;
}

.container {
  grid-template-rows: 100px 300px;
  grid-auto-flow: column;
  grid-auto-columns: 200px;
}

它还接受更复杂但更方便的语法来一次性设置所有内容。如下示例中的代码是等效的:

.container {
  grid: [row1-start] "header header header" 1fr [row1-end]
        [row2-start] "footer footer footer" 25px [row2-end]
        / auto 50px auto;
}

.container {
  grid-template-areas: 
    "header header header"
    "footer footer footer";
  grid-template-rows: [row1-start] 1fr [row1-end row2-start] 25px [row2-end];
  grid-template-columns: auto 50px auto;    
}

子元素属性

grid-column-start, grid-column-end, grid-row-start, grid-row-end

该属性通过网格线来设置网格项在网格中的位置。grid-column-startgrid-row-start 为网格项起始的线,grid-column-endgrid-row-end 为网格项结束的线。

.item {
  grid-column-start: <number> | <name> | span <number> | span <name> | auto;
  grid-column-end: <number> | <name> | span <number> | span <name> | auto;
  grid-row-start: <number> | <name> | span <number> | span <name> | auto;
  grid-row-end: <number> | <name> | span <number> | span <name> | auto;
}
.item-a {
  grid-column-start: 2;
  grid-column-end: five;
  grid-row-start: row1-start;
  grid-row-end: 3;
}
.item-b {
  grid-column-start: 1;
  grid-column-end: span col4-start;
  grid-row-start: 2;
  grid-row-end: span 2;
}

如果 grid-column-endgrid-row-end 未声明,则该网格项将默认跨越一个轨道。网格项目之间可以相互重叠,使用 z-index 可以控制它们的重叠次序。

grid-column, grid-row

分别是 grid-column-start + grid-column-endgrid-row-start+ grid-row-end 的简写。

.item {
  grid-column: <start-line> / <end-line> | <start-line> / span <value>;
  grid-row: <start-line> / <end-line> | <start-line> / span <value>;
}
.item-c {
  grid-column: 3 / span 2;
  grid-row: third-line / 4;
}

如果未设置结束线的值,则该网格项将默认跨越一个轨道。

grid-area

为一个网格项命名以便它可以使用 grid-template-areas 属性创建的模板引用。此属性可以作为 grid-row-start + grid-column-start + grid-row-end + grid-column-end 的简写。

用作为网格项分配名称:

.item-d {
  grid-area: header;
}

用作 grid-row-start + grid-column-start + grid-row-end + grid-column-end 的简写:

.item-d {
  grid-area: 1 / col4-start / last-line / 6;
}

justify-self

沿 inline(行)轴对齐单元格内的网格项(与沿 block(列)轴对齐 align-self 相反)。该属性仅应用于单个单元格内的网格项。

.item {
  justify-self: stretch | start | end | center;
}
.item-a {
  justify-self: stretch;
}
.item-a {
  justify-self: start;
}
.item-a {
  justify-self: end;
}
.item-a {
  justify-self: center;
}

通过 justify-items 属性可以为容器中所有的网格项设置对齐方式。

align-self

沿 block(列)轴对齐单元格内的网格项(与沿 inline(行)轴对齐 justify-self 相反)。该属性将仅应用于单个单元格内的网格项。

.item {
  align-self: stretch | start | end | center;
}
.item-a {
  align-self: stretch;
}
.item-a {
  align-self: start;
}
.item-a {
  align-self: end;
}
.item-a {
  align-self: center;
}

place-self

place-self 可以在单次声明中同时设置 align-selfjustify-self

.item-a {
  place-self: center;
}
.item-a {
  place-self: center stretch;
}

定位

本节内容主要参考自:定位技术

定位允许我们将一个元素放置在网页的指定位置上。定位并非是一种用来做主要布局的方式,而是一种用于微调布局的手段。通过 position 属性在特定的布局中修改元素的定位方式,该属性有 staticrelativefixedabsolutesticky 共 5 种可选值。

为了展示不同 position 的效果,在此采用相同的 HTML 进行比较:

<h1>XXX 定位</h1>

<p>这是一个基本块元素。</p>
<p class="position">这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>

默认样式为:

body {
  width: 400px;
  margin: 0 auto;
}

h1 {
  text-align: center;
}
  
p {
  margin: 10px;
  padding: 10px;
  background-color: #916cad;
  border: 2px #523874 solid;
  border-radius: 3px;
}

静态定位

静态定位(staticposition 属性的 默认值,它表示将元素放置在文档布局流的默认位置上。

静态定位样式为:

.position {
  position: static;
}

渲染效果如下:

相对定位

相对定位(relative 表示相对于 静态定位 的默认位置进行偏移,其需要搭配 topbottomleftright 四个属性使用。

相对定位样式为:

.position {
  position: relative;
  top: 30px;
  left: 30px;
  background-color: #c7fba5cc;
  border: 2px #adf182cc solid;
}

渲染效果如下:

绝对定位

绝对定位(absolute 表示相对于 上级元素 的位置进行偏移,其需要搭配 topbottomleftright 四个属性使用。绝对定位的定位基点不能为 static 定位,否则定位基点将变成网页根元素 html

绝对定位样式为:

.position {
  position: absolute;
  top: 30px;
  left: 30px;
  background-color: #c7fba5cc;
  border: 2px #adf182cc solid;
}

渲染效果如下:

固定定位

固定定位(fixed 表示相对于 视窗(viewport,即浏览器窗口) 进行偏移,其需要搭配 topbottomleftright 四个属性使用。利用固定定位可以实现元素位置不随页面滚动而发生变化。

为了演示固定定位,修改 HTML 代码如下:

<h1>固定定位</h1>

<p>这是一个基本块元素。</p>
<p class="position">固定</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>

固定定位样式为:

.position {
  position: fixed;
  top: 30px;
  left: 30px;
  background-color: #c7fba5cc;
  border: 2px #adf182cc solid;
}

渲染效果如下:

粘性定位

粘性定位(sticky 可以理解为 静态定位(static固定定位(fixed混合。当指定一个元素的 position 属性为 sticky 后,它会在正常布局流中滚动,直至它出现在设定的相对于容器的位置,此时它会停止滚动,表现为固定定位。

为了演示粘性定位,修改 HTML 代码如下:

<h1>粘性定位</h1>

<p>这是一个基本块元素。</p>
<p class="position">这是一个粘性定位元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>
<p>这是一个基本块元素。</p>

粘性定位样式为:

.position {
  position: sticky;
  top: 30px;
  left: 30px;
  background-color: #c7fba5cc;
  border: 2px #adf182cc solid;
}

渲染效果如下:

当我谈修图时,我谈些什么

2023-04-22 08:00:00

文本是「当我谈」系列的第一篇博客,后续「当我谈」系列会从程序员的视角一起科普认知未曾触及的其他领域。

色彩空间

色彩空间是对色彩的组织方式,借助色彩空间和针对物理设备的测试,可以得到色彩的固定模拟和数字表示。色彩模型是一种抽象数学模型,通过一组数字来描述颜色。由于“色彩空间”有着固定的色彩模型和映射函数组合,非正式场合下,这一词汇也被用来指代色彩模型。

RGB

红绿蓝(RGB)色彩模型,是一种加法混色模型,将红(Red)绿(Green)蓝(Blue)三原色的色光以不同的比例相加,以合成产生各种色彩光。三原色的原理不是出于物理原因,而是由于生理原因造成的。

RGB 色彩模型可以映射到一个立方体上,如下图所示:

红绿蓝的三原色光显示技术广泛用于电视和计算机的显示器,利用红、绿、蓝三原色作为子像素组成的真色彩像素,透过眼睛及大脑的模糊化,“人类看到”不存在于显示器上的感知色彩。

CMYK

印刷四分色模式(CMYK)是彩色印刷中采用的一种减法混色模型,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓的“全彩印刷”。四种标准颜色分别是:

CMY 叠色的示意图如下所示:

利用 $0$ 到 $1$ 的浮点数表示 $R, G, B$ 和 $C, M, Y, K$,从四分色向三原光转换公式如下:

$$ \begin{aligned} R &= \left(1 - C\right) \left(1 - K\right) \\ G &= \left(1 - M\right) \left(1 - K\right) \\ B &= \left(1 - Y\right) \left(1 - K\right) \end{aligned} $$

从三原光向四分色转换公式如下:

$$ \begin{aligned} C &= 1 - \dfrac{R}{\max \left(R, G, B\right)} \\ M &= 1 - \dfrac{G}{\max \left(R, G, B\right)} \\ Y &= 1 - \dfrac{B}{\max \left(R, G, B\right)} \\ K &= 1 - \max \left(R, G, B\right) \\ \end{aligned} $$

HSL 和 HSV

HSL 和 HSV 都是一种将 RGB 色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构 RGB 更加直观。HSL 即色相、饱和度、亮度(Hue,Saturation,Lightness),HSV 即色相、饱和度、明度(Hue,Saturation,Value),又称 HSB,其中 B 为 Brightness。另种色彩空间定义如下图所示:

HSL 和 HSV 色彩空间

色相

色相(Hue)指的是色彩的外相,是在不同波长的光照射下,人眼所感觉到的不同的颜色。在 HSL 和 HSV 色彩空间中,色相是以红色为 0 度(360 度)、黄色为 60 度、绿色为 120 度、青色为 180 度、蓝色为 240 度、洋红色为 300 度。如下图所示:

饱和度

饱和度(Saturation)指的是色彩的纯度,饱和度由光强度和它在不同波长的光谱中分布的程度共同决定。下图为红色从最小饱和度到最大饱和度的示例:

亮度和明度

明度值是与同样亮的白色物体相比,某物的亮的程度。如果我们拍摄一张图像,提取图像色相、饱和度和明度值,然后将它们与不同色彩空间的明度值进行比较,可以迅速地从视觉上得出差异。如下图所示,HSV 色彩空间中的 V 值和 HSL 色彩空间中的 L 值与感知明度值明显不同:

原始图片
HSL 中的 L
HSV 中的 V

差异

HSV 和 HSL 两者对于色相(H)的定义一致,但对于饱和度(S)和亮度与明度(L 与 B)的定义并不一致。

在 HSL 中,饱和度独立于亮度存在,也就是说非常浅的颜色和非常深的颜色都可以在 HSL 中非常饱和。而在 HSV 中,接近于白色的颜色都具有较低的饱和度。

以 Photoshop 和 Afiinity Photo 两款软件的拾色器为例:

Photoshop 拾色器(HSV)
Afiinity Photo 拾色器(HSL)

两个软件分别采用 HSV 和 HSL 色彩空间,其横轴为饱和度(S),纵轴分别为明度(V)和亮度(L)。不难看出,在 Photoshop 拾色器中,越往上混入的黑色越少,明度越高;越往右混入的白色越少,纯度越高。在 Afiinity Photo 拾色器中,下部为纯黑色,亮度最小,从下往上,混入的黑色逐渐减少,直到 50% 位置处完全没有黑色混入,继续往上走,混入的白色逐渐增加,直到 100% 位置处完全变为纯白色,亮度最高。

直方图

图像直方图是反映图像色彩亮度的直方图,其中 $x$ 轴表示亮度值,$y$ 轴表示图像中该亮度值像素点的个数。以 $8$ 位图像为例,亮度的取值范围为 $\left[0, 2^8-1\right]$,即 $\left[0, 255\right]$。以如下图片为例(原始图片:链接):

原始图片

在 Lightroom 中直方图如下所示:

原始图片 Lightroom 直方图

利用 Python 绘制的直方图如下所示:

直方图代码
import cv2

import numpy as np
import matplotlib.pyplot as plt

gray_img = cv2.imread('demo.jpg', cv2.IMREAD_GRAYSCALE)
img = cv2.imread('demo.jpg')
img_channels = cv2.split(img)
height, width = gray_img.shape
gray_img_hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256])
img_channels_hist = [cv2.calcHist([img_channel], [0], None, [256], [0, 256])
                     for img_channel in img_channels]

fig, ax = plt.subplots(1, 1)

ax.plot(gray_img_hist, color='0.6', label='灰')

for (img_channel_hist, color, label) in zip(
  img_channels_hist, ['#6695ff', '#70df5f', '#f74048'], ['蓝', '绿', '红']):
    ax.plot(img_channel_hist, color=color, label=label)

segments = [0, 28, 85, 170, 227, 255]
segments_text = ['黑色', '阴影', '曝光', '高光', '白色']

for (left_border, right_border, segment_text) in \
        zip(segments[:-1], segments[1:], segments_text):
  if left_border != 0:
    ax.axvline(x=left_border, ymin=0, color='black')
  
  ax.annotate(
      segment_text,
      xy=((left_border + right_border) / 2, np.max(img_channels_hist) / 3),
      ha='center')

ax.legend(loc='upper center')
plt.xlim([0, 256])
ax.set_xticks([0, 32, 64, 96, 128, 160, 192, 224, 256])
ax.axes.get_yaxis().set_visible(False)

plt.tight_layout()
fig.set_size_inches(8, 4)
plt.savefig('demo-image-histgram.png', dpi=100)
原始图片直方图

直方图以 $28, 85, 170, 227$ 为分界线可以划分为黑色阴影曝光高光白色共 5 个区域。其中曝光区域以适中的亮度保留了图片最多的细节,阴影和高光对应了照片中较暗和较亮的区域,黑色和白色两个部分则几乎没有任何细节。当整个直方图过于偏左时表示欠曝过于偏右时则表示过曝

色温

色温(Temperature)是指照片中光源发出相似的光的黑体辐射体所具有的开尔文温度。开尔文温度越光越,开尔文温度越光越,如下图所示:

针对图片分别应用 5000K 和 10000K 色温的对比结果如下图所示:

色温代码
import math
import cv2

import numpy as np


def __kelvin_to_rgb(kelvin: int) -> (int, int, int):
  kelvin = np.clip(kelvin, min_val=1000, max_val=40000)
  temperature = kelvin / 100.0

  # 红色通道
  if temperature < 66.0:
      red = 255
  else:
      # a + b x + c Log[x] /.
      # {a -> 351.97690566805693`,
      # b -> 0.114206453784165`,
      # c -> -40.25366309332127
      # x -> (kelvin/100) - 55}
      red = temperature - 55.0
      red = 351.97690566805693 + 0.114206453784165 * red \
            - 40.25366309332127 * math.log(red)

  # 绿色通道
  if temperature < 66.0:
      # a + b x + c Log[x] /.
      # {a -> -155.25485562709179`,
      # b -> -0.44596950469579133`,
      # c -> 104.49216199393888`,
      # x -> (kelvin/100) - 2}
      green = temperature - 2
      green = -155.25485562709179 - 0.44596950469579133 * green \
              + 104.49216199393888 * math.log(green)
  else:
      # a + b x + c Log[x] /.
      # {a -> 325.4494125711974`,
      # b -> 0.07943456536662342`,
      # c -> -28.0852963507957`,
      # x -> (kelvin/100) - 50}
      green = temperature - 50.0
      green = 325.4494125711974 + 0.07943456536662342 * green \
              - 28.0852963507957 * math.log(green)

  # 蓝色通道
  if temperature >= 66.0:
      blue = 255
  elif temperature <= 20.0:
      blue = 0
  else:
      # a + b x + c Log[x] /.
      # {a -> -254.76935184120902`,
      # b -> 0.8274096064007395`,
      # c -> 115.67994401066147`,
      # x -> kelvin/100 - 10}
      blue = temperature - 10.0
      blue = -254.76935184120902 + 0.8274096064007395 * blue \
             + 115.67994401066147 * math.log(blue)

  return np.clip(red, 0, 255), np.clip(green, 0, 255), np.clip(blue, 0, 255)


def __mix_color(v1, v2, ratio: float):
  return np.array((1.0 - ratio) * v1 + 0.5).astype(np.uint8) \
      + np.array(ratio * v2).astype(np.uint8)


def __keep_original_lightness(original_image, image):
  original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1]
  h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS))

  return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR)


def apply_temperature(
        image,
        temperature,
        keep_original_lightness: bool = True):
  b, g, r = cv2.split(image)
  n_b = np.clip(b.astype(np.single) - temperature, 0, 255).astype(np.uint8)
  n_r = np.clip(r.astype(np.single) + temperature, 0, 255).astype(np.uint8)
  ret_image = cv2.merge([n_b, g, n_r])

  return __keep_original_lightness(image, ret_image) \
      if keep_original_lightness else ret_image


def apply_kelvin(
        image,
        kelvin: int,
        strength: float = 0.6,
        keep_original_lightness: bool = True):
  b, g, r = cv2.split(image)
  k_r, k_g, k_b = __kelvin_to_rgb(kelvin)
  n_r, n_g, n_b = __mix_color(r, k_r, strength), \
      __mix_color(g, k_g, strength), __mix_color(b, k_b, strength)
  ret_image = cv2.merge([n_b, n_g, n_r])

  return __keep_original_lightness(image, ret_image) \
      if keep_original_lightness else ret_image


img = cv2.imread('demo.jpg')
cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 5000))
cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 10000))

色调

色调(Tint)允许我们为了实现中和色偏或增加色偏的目的,而将色偏向绿色或洋红色转变。针对图片分别应用 -30 和 +30 色调的对比结果如下图所示:

色调代码
import cv2

import numpy as np


def __keep_original_lightness(original_image, image):
  original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1]
  h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS))

  return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR)


def apply_tint(image, tint, keep_original_lightness: bool = True):
  b, g, r = cv2.split(image)
  n_g = np.clip(g.astype(np.single) + tint, 0, 255).astype(np.uint8)
  ret_image = cv2.merge([b, n_g, r])

  return __keep_original_lightness(image, ret_image) \
    if keep_original_lightness else ret_image


img = cv2.imread('demo.jpg')
cv2.imwrite('demo-color-tint-negative.jpg', apply_tint(img, -30))
cv2.imwrite('demo-color-tint-positive.jpg', apply_tint(img, +30))

在 Windows 下利用 WSL2 和 Ubuntu 配置 GPU 机器学习环境 (GPU Machine Leanring Environment Configuration under Windows with WSL2 and Ubuntu)

2023-03-19 08:00:00

本文主要面向希望在游戏空闲时段将显卡用于科学事业的朋友们😎。

更新于 2024-05-19

终端

工欲善其事必先利其器,开发离不开那个黑框框,所以我们需要把这个黑框框变得更好看更好用些。Windows 终端是一个新的支持 PowerShell 和 WSL bash 的应用,通过应用商店 直接进行安装。

建议安装最新版的 PowerShell 作为命令行环境,相关下载和配置详见官网

为了更好的在终端中显示中英文和图标,推荐使用 Sarasa Term SC Nerd 作为终端显示字体。

网络

为了方便使用,网络设置采用桥接模式。桥接模式需要在 Windows 中启用 Hyper-V(仅 Windows 专业版支持)。通过启用或关闭 Windows 功能开启 Hyper-V,然后重启电脑生效。

在 Hyper-V 中创建一个新的交换机,在连接类型中选择外部网络,并根据电脑的网络连接情况选择对应的桥接网卡。

通过 Get-VMSwitch -SwitchType External 可以查看创建的交换机:

Name SwitchType NetAdapterInterfaceDescription
---- ---------- ------------------------------
WSL  External   Realtek Gaming 2.5GbE Family Controller

在 Home 目录创建 .wslconfig 文件,并添加如下内容:

[wsl2]
networkingMode=bridged
vmSwitch=WSL
ipv6=true

其中,vmSwitch 填写创建的交换机的名称。

WSL

以管理员模式打开 PowerShell 或 Windows 命令提示符,输入如下命令,并重启计算机:

wsl --install

此命令会启用 WSL 并安装 Ubuntu 发行版 Linux。通过 wsl -l -o 可以查看所有 Linux 的发行版:

以下是可安装的有效分发的列表。
请使用“wsl --install -d <分发>”安装。

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
Ubuntu-24.04                           Ubuntu 24.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
openSUSE-Leap-15.5                     openSUSE Leap 15.5
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
SUSE-Linux-Enterprise-15-SP5           SUSE Linux Enterprise 15 SP5
openSUSE-Tumbleweed                    openSUSE Tumbleweed

通过 wsl --install -d <发行版名称> 可以安装其他发行版 Linux,本文以 Ubuntu 22.04 为例。通过 wsl -l -v 可以查看当前运行的 WSL 版本:

  NAME                   STATE           VERSION
* Ubuntu-22.04           Running         2

通过 wsl 新安装的 Linux 默认已经设置为 WSL 2。

进入 Ubuntu 命令行,输入如下命令安装必要的系统依赖:

sudo apt install gcc

安装 zsh 作为 Ubuntu 默认的 Shell:

sudo apt install zsh

安装 Oh My Zsh 来提升 zsh 的易用性。

显卡

驱动

从 Nvidia 官网(https://www.nvidia.cn/geforce/drivers)下载适用于 Windows 的最新驱动并安装。进入 Windows 命令行,输入 nvidia-smi 命令查看显卡状态:

Ubuntu 中不再需要额外安装显卡驱动,进入 Ubuntu 命令行,输入 nvidia-smi 命令查看显卡状态:

不难看出,出了 nvidia-smi 工具版本不同外,显卡驱动和 CUDA 版本均是相同的。

CUDA

从 Nvidia 官网(https://developer.nvidia.com/cuda-toolkit-archive)下载适用于 WSL Ubuntu 的 CUDA,在此选择的版本为 11.8.0(具体请参考例如 Tensorflow 等所需工具的依赖版本),相关平台选项如下:

下载完毕后运行如下命令进行安装:

chmod +x cuda_11.8.0_520.61.05_linux.run
sudo ./cuda_11.8.0_520.61.05_linux.run --toolkit

其中 --toolkit 表示仅安装 CUDA 工具箱。

在弹出的 EULA 界面输入 accept 进入安装选项界面:

仅保留 CUDA Toolkit 11.8 即可,切换到 Install 并按回车键进行安装。

将如下内容添加到 ~/.bashrc 文件尾部:

export PATH=/usr/local/cuda/bin:$PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64

通过 source ~/.bashrcsource ~/.zshrc 使路径立即生效。输入 nvcc -V 查看 CUDA 编译器驱动版本:

cuDNN

从 Nvidia 官网(https://developer.nvidia.com/rdp/cudnn-archive)下载适用于 Linux 和上述安装 CUDA 版本的 cuDNN,在此选择的版本为 v8.8.0 for CUDA 11.x,安装包格式为 Local Installer for Linux x86_64 (Tar)

注意:cuDNN 需要注册账户后方可进行下载。

下载完毕后运行如下命令进行解压:

tar -xvf cudnn-linux-x86_64-8.9.7.29_cuda11-archive.tar.xz

运行如下命令将其移动到 CUDA 目录:

sudo mv cudnn-*-archive/include/cudnn*.h /usr/local/cuda/include
sudo mv cudnn-*-archive/lib/libcudnn* /usr/local/cuda/lib64
sudo chmod a+r /usr/local/cuda/include/cudnn*.h /usr/local/cuda/lib64/libcudnn*

机器学习环境

Python

Ubuntu 22.04 系统已经安装了 Python 3.10 版本,Python 3.10 在常用机器学习库上具有较好的兼容性。因此,以 Python 3.10 版本为例,使用 venv 创建机器学习虚拟环境。在系统层面安装 venv 并创建虚拟环境:

sudo apt install python3-venv
mkdir ~/SDK
python3.10 -m venv ~/SDK/python310
source ~/SDK/python310/bin/activate

PyTorch

输入如下命令安装 PyTorch:

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

安装完毕后发现 PyTorch 内嵌了 CUDA 和 cuDNN。内嵌的好处是可以做到安装即用,但如果和其他包依赖的系统 CUDA 和 cuDNN 版本不一致,容易出现各种意想不到的问题。在 Python 中运行如下命令验证 PyTorch 是否可以正常调用显卡:

import torch

# PyTorch 版本
torch.__version__
# 2.3.0+cu118

# CUDA 是否可用
torch.cuda.is_available()
# True

# GPU 数量
torch.cuda.device_count()
# 1

# GPU 名称
torch.cuda.get_device_name(0)
# NVIDIA GeForce RTX 3070 Ti

Tensorflow

输入如下命令安装 Tensorflow(2.14.1 版本支持 CUDA 11.8):

pip install tensorflow==2.14.1

在 Python 中运行如下命令验证 Tensorflow 是否可以正常调用显卡:

import tensorflow as tf

# Tensorflow 版本
tf.__version__
# 2.15.1

# GPU 设备
tf.config.list_physical_devices('GPU')
# [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]

# GPU 名称
tf.test.gpu_device_name()
# /device:GPU:0

PyCharm

配置 PyCharm 使用 WSL 中的 Python 请参见 Configure an interpreter using WSL

文学编程和可重复性研究 (Literate Programming and Reproducible Research)

2023-03-11 08:00:00

文学编程

文学式编程(Literate Programming)是由高德纳提出的编程方法,希望能用来取代结构化编程范型。正如高德纳所构想的那样,文学编程范型不同于传统的由计算机强加的编写程序的方式和顺序,而代之以让程序员用他们自己思维内在的逻辑和流程所要求的顺序开发程序。文学编程自由地表达逻辑,而且它用人类日常使用的语言写出来,就好像一篇文章一样,文章里包括用来隐藏抽象的巨集和传统的源代码。文学编程工具用来从文学源文件中获得两种表达方式,一种用于计算机进一步的编译和执行,称作“绕出”(tangled)的代码,一种用于格式化文档,称作从文学源代码中“织出”(woven)。虽然第一代文学编程工具特定于计算机语言,但后来的工具可以不依赖具体语言,并且存在于比编程语言更高的层次中 1

如高德纳在论文 2 中所示,相同的源文件经过“tangle”可以编译为机器代码,经过“weave”可以编译为文档。

文学编程历史

从高德纳提出文学编程的概念后,各家各派都在将这个编程范式付诸实践。我接触文学编程已经比较晚了,算是从 R Markdown 和 knitr 开始,开始时写写分析报告和做做幻灯片,慢慢的在更多场景我发现这很适合。

WEB, CWEB & noweb

WEB 是一种计算机编程语言系统,它由高德纳设计,是第一种实现他称作“文学编程”的语言。WEB 包含了 2 个主要程序:TANGLE,从源文本生成可编译的 Pascal 代码,以及 WEAVE,使用 TeX 生成格式漂亮可打印的文档。CWEB 是 WEB 的 C 语言新版本,noweb 是另外一种借鉴了 WEB 的文学编程工具,同时与语言无关 3

wc.nw 为例,其为 Unix 单词统计程序 wc 的 noweb 版本重写,原始的 CWEB 版本可以在高德纳的《文学编程》一书中找到。noweb 源代码中包含 TeX 代码和 C 语言代码,每个 C 语言代码片段都以一个 <<代码片段名称>>= 开头,以 @ 结尾,程序的入口为 <<*>>=。在某个代码片段中调用其他代码片段只需要输入 <<代码片段名称>> 即可。

安装 noweb,通过如下命令可以将 wc.nw 编译为 C 语言代码 wc.c

notangle -L wc.nw > wc.c

通过如下命令可以将 wc.nw 编译为 TeX 源代码:

noweave -autodefs c -index wc.nw > wc.tex

Org Mode

Org Mode 是由 Carsten Dominik 于 2003 年发明的用于文本编辑器 Emacs 的一种支持内容分级显示的编辑模式。这种模式下可以创建待办列表,日志管理,做笔记,做工程计划或者写网页。Org Mode 通常启用于后缀名为 org 的纯文本文件,使用星号标记有层次的内容(如文章大纲、话题与子话题、嵌套代码),并提供一组函数用于读取并展示这类标记以及操纵内容(如折叠大纲内容、移动元素、更改待办项状态)4

在 Org Mode 中使用 #+BEGIN_SRC#+END_SRC 来标记代码块,在 #+BEGIN_SRC 后指定嵌入的代码类型,例如嵌入 C 语言源代码:

#+BEGIN_SRC c
int main(void) {
  return 0;
}
#+END_SRC

更多关于在 Org Mode 中的文学编程应用可以参见 lujun9972/emacs-document

Sweave & knitr

Sweave 是 R 语言的 WEB 实现,为什么是 Sweave 而不是 Rweave,没有仔细去找解释,但我猜测是由于 R 语言的前身为 S 语言吧。既然有了 Sweave 为什么没有 Stangle 呢?也是猜测,或许 Sweave 的作者在创作之初就更侧重于将 R 代码及其运行结果嵌入,“织出”最终阅读友好的文档吧。当然,由于 R 是一门统计分析语言,将所有 R 代码提取出来编译成可执行文件并不是它的优势,我猜这应该也是没有 Stangle 的一个原因吧。当然,也并不是没有人打算这么做,fusen 是一个基于 R Markdown 直接生成 R 扩展包的扩展包,从一定程度上应该算是 tangle 的理念实现吧。

Sweave 是基于 R 和 LaTeX 的实现,但 LaTeX 的学习曲线相对比较陡峭,knitr 的出现拓展了 Sweave 的功能,例如:内容方面增加支持了 Markdown 等,代码方面增加支持了 Python 等。除此之外,也衍生出了多种多样的文档格式,例如:幻灯片(xaringan),图书(bookdown)和博客(blogdown)等等。

在 R Markdown 中使用如下方式嵌入代码,在 {} 中指定嵌入代码的类型,例如嵌入并执行 R 语言源代码:

```{r}
add <- function(a, b) {
  return(a + b)
}

print(add(1, 1))
```

在同一个 R Markdown 文件中可以同时插入 R 和 Python 等多种不同语言的源代码,通过 reticulate 甚至可以实现 R 和 Python 之间的数据交互。

Jupyter

Jupyter 是从 IPython Notebook 发展而来,基于 Python 语言的强大优势,其在业界迅速占领了一大片应用市场,后来 Jupyter 也逐渐支持其他语言。虽然现在 R Markdown 也支持在 RStuido 等编辑器中逐行运行,但个人认为 Jupyter 的最大优势就在于边写边运行,这也使得 Jupyter 在教育等需要实时运行的领域应用最为广泛。

Jupyter 仍以 .ipynb 为扩展名,其底层为 JSON 格式的文本文件。原生 Jupyter 针对一个文件仅支持一种 Kernel,即运行一种类型的代码,当通过一些技巧也可以实现同时运行多种类型的代码。

Quarto

Quarto 是 Posit(RStudio 的新公司名)开发的一个基于 Pandoc 的开源技术出版系统。Quarto 的目标是改进科学和技术文档的创建和协作过程,其希望将 R Markdown、bookdowndistillxaringan 等功能统一到一个系统中。

Quarto 的工作流程同 R Markdown 类似,如下图所示:

所以,Quarto 的到来是否意味着 R Markdown 的消失呢?官方 FAQ 给到了否定的答案。不过我认为 Quarto 「一统天下」的野心还是有的,只是基于现状可能这条路还需要再走一阵子。如下是我从当先(2023 年初)现状和个人的一些需求,认为 Quarto 和 R Markdown 之间存在的一些区别:

还是很希望 Quarto 在未来能够做更好的统一,这也会让我们面对不同输出场景中复用更多相同的知识和技巧。

当我谈文学编程时我谈些什么

高德纳提出了文学编程的理念,Peter Seibel 也存在不同的看法:编码并非文学。其实这两者并不是对立的,只是角度不同而已。我认为文学编程更适合数据分析型工程,针对功能系统型工程确实很难融入文学编程。以当下的实践来看,从 R Markdown,到 Jupyter,再到 Quarto,无一例外是针对技术和科学等场景提供数据分析功能,而针对系统工程开发,更多还是遵循着产品文档和工程代码分离。

文学编程是一种理念,类似一门新的语言,从客观上能解决一些特定领域的问题,也能在某些场景中提高效率。但整个生态的发展离不开真正「喜欢」的人参与,不断改善和大力推广才能保证生态的持续发展。除了商业团队的主推以外,我认为开源精神和社区参与也很重要,真正的繁华从来不是一家独大而是全民参与。

可重复性研究

可重复性研究的范畴要比文学编程更广泛,文学编程主要围绕计算机相关科学展开,可重复性研究则是面向全部科学的。可重复性研究指的是科学结果应该在其推论完全透明的方式记录下来 5

图片来源:https://github.com/mickaeltemporao/reproducible-research-in-python

上图生动地描述了可重复性研究的重要性。在此我们依旧围绕计算机相关科学讨论可重复性研究。文学编程通过将代码嵌入文档中实现了代码结果的可重复性动态生成,但除了代码之外,可重复性研究还需要关注代码的运行环境和使用的数据等,这些同样会影响研究的最终结果。

运行环境

硬件、内核、操作系统、语言、扩展包等代码运行环境都会对最终的研究结果产生影响,如下图所示:

图片来源:https://github.com/MozillaFoundation/2017-fellows-sf

硬件问题在苹果推出基于 arm 架构的 M1 芯片时一度带来了不少的麻烦,虽然 macOS 提供了转义工具,但在推出的早起仍出现大量软件兼容问题。不过随着这几年的发展,软件的兼容性问题已经得到了极大的改善,因此在硬件这一层几乎不再会有太多问题。

内核和操作系统可以粗略的认为是同一层级,这也是在日常研究中会经常遇到的问题。有时候在自己电脑系统上跑地好好的代码,拿到别人电脑上就会出现各种问题。在工程部署阶段,通过 docker 等虚拟化技术是可以保证代码运行的系统环境是相同的,但在分析研究阶段这并不好用。在这个层面感觉比较好的解决方案就是使用多系统兼容的软件、语言、扩展包等,如果确实需要使用指定系统的工具,在代码层面实现兼容或提示兼容问题会是不错的选择。

语言和扩展包层面的问题在真实场景中遇到的并不多,我们不必非要在 Python 和 R 中二选一,也不必非要在 PyTorch 和 Tensorflow 中二选一。但至少要保证使用相关研究领域中常用的工具、语言和扩展包,当然这些最好甚至应该是开源的,这样其他人才能够无障碍的获取相关代码依赖。

数据公开

在可重复性研究中,数据公开也很重要,没有研究的输入数据,哪怕分析代码全部公开,也无法得到相同的研究结果。最理想的情况就是完全公开所用的原始数据,但这个在涉及到私有域数据时往往又是不现实的。针对这个问题有多种可以尝试解决的方案:

  1. 数据脱敏。例如:针对涉及隐私的 ID 可以转换为无意义的 ID,一般情况不会对研究产生影响。例如:针对涉及商业机密的价格或销量可以添加扰动量或进行分箱处理,但这会对研究产生一定的影响。
  2. 人造数据。针对所需的数据格式完全人工创造虚拟数据,不过在复杂场景下其成本较高,甚至无法实现。

  1. https://zh.wikipedia.org/zh-hans/文学编程 ↩︎

  2. Knuth, Donald Ervin. “Literate programming.” The computer journal 27.2 (1984): 97-111. ↩︎

  3. https://zh.wikipedia.org/zh-hans/WEB ↩︎

  4. https://zh.wikipedia.org/zh-hans/Org-模式 ↩︎

  5. https://en.wikipedia.org/wiki/Reproducibility ↩︎

在 OpenWrt 中安装 Jellyfin 搭建家庭影音中心

2023-01-24 08:00:00

历史尝试

入手 NAS 已经近 5 年的时间了,最初只是用来挂 PT 下载,然后在各种设备上通过 SMB 共享播放上面的视频。后面也尝试在利用 Plex 搭建家庭影音中心,但由于 Plex 的高级功能需要付费也就作罢。今年搬家后整体对各种硬件做了升级,换了软路由,做了基于 AC+AP 的全屋 WiFi,NAS 换了更大的硬盘,客厅和卧室各安装了一个投影机,入了 Apple TV 4K 和 Chromecast with Google TV 4K 两个盒子。购买 Apple TV 时买了有 Infuse 的套餐,果然没有花钱的不是,Infuse 无论是从 UI 还是体验上都算优秀,但由于仅限于苹果生态,且可玩性较差,最终也只是沦为了 Apple TV 上的本地播放器。

秉着「付费虽美丽,免费更开心」的原则,最终选择了基于 Jellyfin 的方案。由于 NAS 的 CPU 性能并不高,为了不给 NAS 其他功能带来过多压力,同时考虑软路由性能过剩,因此决定将 Jellyfin 安装在软路由上,再将 NAS 的资源挂载到软路由来实现整体解决方案。

在做 Jellyfin 选型时,其吸引我的最大优点就是开源,同时各个平台的客户端也都在官方应用商店有上架,这极大的简化了客户端的安装流程。付费解决方案,例如:Plex,Emby(在 3.5.3 之后闭源),由于有更多资金的支持,肯定在一些方面会优于 Jellyfin。其他的免费解决方案,例如:NAS 自带的 Video Station,Kodi(大学时代就曾在电脑上安装过)等在不同方面也各有差异。关于不同解决方案的一些差异在此就不再做深入探究,有兴趣的同学可以自行 Google,不过也要注意很多文章时间会比较久了,与当下的实际情况会有部分出入。

硬件设备

服务端设备

设备 系统 CPU 内存 用途 网络连接 位置
软路由 OpwnWrt Intel Celeron N5105
2.0-2.9 GHz
4 核心 4 线程
4GB 主路由
代理服务器
内网穿透服务器
有线 1000M 客厅
NAS DSM 7 Intel Celeron J3355
2.0-2.5 GHz
2 核心 2 线程
6GB 共享存储
PT 下载
迅雷远程下载
有线 1000M 衣帽间

NAS 通过有线网络与主路由直连,虽然主路由网口为 2.5G,但由于 NAS 网口仅为 1000M,而且又懒于把 NAS 上的双网口做链路聚合,因此实际通讯也就限制为 1000M,但对于家庭影音中心也基本够用了。主路由上游使用了运营商提供的光猫,虽然已经改了桥接模式,但由于运营商提供的光猫 LAN 口也是 1000M 的,因此外网也无法突破千兆限制,当然还是由于 10G EPON 的万兆光猫太贵,压制了我鼓捣的欲望。

NAS 自带的内存为 2G,后面加了一条 4G 的内存扩容到 6G,最初也是计划用 NAS 玩一玩 Docker 的。但碍于 J3355 这颗 CPU 性能一般,运行太多东西给 NAS 的基本功能会带来不小压力,我想这也是群辉官方并没有给 DS418play 这款 NAS 提供 Docker 应用的主要原因吧。软路由当时买了非裸机的丐版,但由于并没有用其做太多事情,空闲内存基本上还有 3.5G 左右,因此为了充分利用 N5105 这颗 CPU,最终决定将需要视频解码这类耗 CPU 的任务交给软路由了。不过买的这款软路由是被动散热,正常待机就干到 60 摄氏度左右了,CPU 占用上来了估计有望突破 100 摄氏度😂。

客户端设备

设备 系统 用途 网络/视频连接 位置
Apple TV 4K tvOS 16 主电视盒子 有线 1000M 客厅
明基 TK850 - 主投影机 HDMI 2.1 客厅
Chromecast with Google TV 4K Android 12 原生 次电视盒子 无线 WiFi 5 主卧
小明 Q2 Pro Android 9 非原生 次投影机 无线 WiFi 5
HDMI 2.1
主卧
PC Windows 11 台式机 有线 1000M 主卧
Macbook Pro macOS 13 笔记本 无线 WiFi 5 -
iPhone 13 Pro iOS 16 主手机 无线 WiFi 6 -
Google Pixel 6 Pro Android 13 原生 备用手机 无线 WiFi 6 -
iPad Pro iPadOS 16 平板电脑 无线 WiFi 6 -

所有客户端通过 H3C 的 1000M AC+AP 采用有线或无线间接连接到主路由。综上所述,家里各种内外部线路就都是 1000M 的理论带宽了。

客户端设备几乎覆盖了所有常用的系统,Jellyfin 在各个系统上均提供了客户端,而且可以在官方商店直接安装,这也是最终确认选择 Jellyfin 的关键一点。毕竟服务端搞得再好,客户端安装费劲的不行也是很痛苦的,尤其是在苹果生态中,官方商店的支持会让你泪大喜奔的。

NAS 准备

由于 Jellyfin 安装在软路由上,因此需要将 NAS 上的媒体文件夹通过 NFS 映射到软路由上,首先需要在 NAS 上配置客户端。进入 NAS,打开 控制面板,进入 文件服务,确保 启用 NFS 服务,最大 NFS 协议选择 NFSv4.1

进入 共享文件夹,选择需要通过 NFS 共享的文件夹:

单击 编辑 进入共享文件夹设置:

NFS 权限 标签页单击 新增 添加新客户端:

相关配置如图所示,其中 服务器名称或 IP 地址 为客户端 IP 地址(即软路由 IP 地址)。依次为所有需要共享的文件夹进行相同配置。

OpenWrt 准备

软路由自带了 128G 的 NVME 固态硬盘,系统采用了 eSir 编译的高大全版本。为了后续安装扩展包和 Docker,对硬盘重新进行分区。

通过 系统 > TTYD终端 在输入用户名(root)和密码后可以进入软路由命令行,输入 fdisk -l 可以查看所有可用块设备的信息:

Disk /dev/nvme0n1: 119.24 GiB, 128035676160 bytes, 250069680 sectors
...

Device               Start       End   Sectors  Size Type
/dev/nvme0n1p1         512     33279     32768   16M Linux filesystem
/dev/nvme0n1p2       33280   1057279   1024000  500M Linux filesystem
/dev/nvme0n1p128        34       511       478  239K BIOS boot

Partition table entries are not in disk order.

输入 cfdisk /dev/nvme0n1 进入分区工具:

使用上下键选择分区,左右键选择要操作的选项。选中 Free space,使用 [New] 选项建立新的分区,输入分区大小,例如:32G

本例计划为 overlay 分配 32G,为 docker 分配 32G,剩余全部分配给 data:

使用 [Write] 选项将结果写入分区表,并在确认处输入 yes 提交:

提交完毕后使用 [Quit] 选项退出分区工具。再次输入 fdisk -l 可以查看所有可用块设备的信息:

Disk /dev/nvme0n1: 119.24 GiB, 128035676160 bytes, 250069680 sectors
...

Device               Start       End   Sectors  Size Type
/dev/nvme0n1p1         512     33279     32768   16M Linux filesystem
/dev/nvme0n1p2       33280   1057279   1024000  500M Linux filesystem
/dev/nvme0n1p3     1058816  68167679  67108864   32G Linux filesystem
/dev/nvme0n1p4    68167680 135276543  67108864   32G Linux filesystem
/dev/nvme0n1p5   135276544 250068991 114792448 54.7G Linux filesystem
/dev/nvme0n1p128        34       511       478  239K BIOS boot

Partition table entries are not in disk order.

分别对新分区进行格式化:

mkfs.ext4 /dev/nvme0n1p3
mkfs.ext4 /dev/nvme0n1p4
mkfs.ext4 /dev/nvme0n1p5

/dev/nvme0n1p3 挂载至 /mnt/nvme0n1p3

mount /dev/nvme0n1p3 /mnt/nvme0n1p3

/overlay 分区数据全部复制到 /mnt/nvme0n1p3 中:

cp -R /overlay/* /mnt/nvme0n1p3/

以上完成后,进入 OpenWrt 管理后台,在 系统 > 挂载点 菜单的 挂载点 处,单击 添加 按钮添加挂载点,将 /dev/nvme0n1p3 挂载为 /overlay,将 /dev/nvme0n1p4 挂载为 /opt,将 /dev/nvme0n1p5 挂载为 /data

单击 保存&应用 后重启路由器,重启完毕后在命令行输入 df -h 可以看出所有分区均成功挂载:

Filesystem                Size      Used Available Use% Mounted on
...
/dev/nvme0n1p3           31.2G     87.9M     26.6G   0% /overlay
overlayfs:/overlay       31.2G     87.9M     26.6G   0% /
/dev/nvme0n1p4           31.2G    356.0K     29.6G   0% /opt
/dev/nvme0n1p5           53.6G     24.0K     50.8G   0% /data
...

/data 目录中创建用于字体的目录:

mkdir /data/fonts

下载 CJK 相关字体至该目录,例如:Noto Sans CJK

/data 目录中创建用于 Jellyfin 的目录:

mkdir /data/docker
mkdir /data/docker/jellyfin
mkdir /data/docker/jellyfin/config
mkdir /data/docker/jellyfin/config/fonts
mkdir /data/docker/jellyfin/cache
mkdir /data/docker/jellyfin/media
mkdir /data/docker/jellyfin/media/nas
mkdir /data/docker/jellyfin/media/nas/disk1
mkdir /data/docker/jellyfin/media/nas/disk2
mkdir /data/docker/jellyfin/media/nas/disk3
mkdir /data/docker/jellyfin/media/nas/disk4

由于在 Docker 中需要使用 1000:1000 作为 UID 和 GID 运行 Jellyfin,需要将 jellyfin 目录修改为对应所有者:

chown -R 1000:1000 /data/docker/jellyfin/

进入命令行,输入如下命令将 NAS 上配置好的共享文件夹挂载到 Jellyfin 的相关目录:

mount.nfs -w 192.168.5.10:/volume1/Disk1 /data/docker/jellyfin/media/nas/disk1 -o nolock
mount.nfs -w 192.168.5.10:/volume2/Disk2 /data/docker/jellyfin/media/nas/disk2 -o nolock
mount.nfs -w 192.168.5.10:/volume3/Disk3 /data/docker/jellyfin/media/nas/disk3 -o nolock
mount.nfs -w 192.168.5.10:/volume4/Disk4 /data/docker/jellyfin/media/nas/disk4 -o nolock

为了保证每次启动软路由时能够自动挂载,请将上述内容添加至 系统 > 启动项 菜单下的 本地启动脚本 文本框的 exit 0 之前:

Jellyfin 部署

在 OpenWrt 上安装 Jellyfin 需要使用 Docker 进行部署。首先在 Docker > 镜像 菜单的 拉取镜像 处填写 jellyfin/jellyfin:latest,然后单击 拉取

拉取完毕后即可在 镜像概览 处查看已下载的镜像:

进入软路由命令行,输入 ls /dev/dri,如果输出如下则表示 CPU 支持硬件加速:

card0       renderD128

为了确保在 Docker 中其他用户可以使用该设备,输入如下命令设置设备权限:

chmod 777 /dev/dri/*

通过 Docker > 容器 菜单,单击 添加 按钮添加容器。单击 命令行 并复制如下内容,单击 提交 解析命令行:

docker run -d \
 --name=jellyfin \
 --hostname=jellyfin \
 --pull=always \
 --privileged \
 --volume /data/docker/jellyfin/config:/config \
 --volume /data/docker/jellyfin/cache:/cache \
 --volume /data/docker/jellyfin/media:/media \
 --volume /data/fonts:/usr/local/share/fonts \
 --user 1000:1000 \
 --net=host \
 --restart=unless-stopped \
 --device /dev/dri/renderD128:/dev/dri/renderD128 \
 --device /dev/dri/card0:/dev/dri/card0 \
 jellyfin/jellyfin

相关参数说明如下:

参数 说明
–name=jellyfin 镜像名称
–hostname=jellyfin 主机名称
–pull=always 运行前总是先拉取镜像
–privileged 特权模式
–volume /data/docker/jellyfin/config:/config 配置文件目录
–volume /data/docker/jellyfin/cache:/cache 缓存文件目录
–volume /data/docker/jellyfin/media:/media 媒体文件目录
–volume /data/fonts:/usr/local/share/fonts 备用字体目录
–user 1000:1000 运行时用户和用户组
–net=host 网络类型:同宿主机相同网络
–restart=unless-stopped 重启策略:在容器退出时总是重启容器
–device /dev/dri/renderD128:/dev/dri/renderD128 硬件加速设备
–device /dev/dri/card0:/dev/dri/card0 硬件加速设备

如果 总是先拉取镜像 未成功自动勾选,可以手动勾选确保运行前拉取最新镜像。单击 提交 创建容器。创建完毕后容器列表即出现 Jellyfin 容器:

勾选 Jellyfin 容器,单击 启动 按钮启动容器:

Jellyfin 配置

通过 http://192.168.5.1:8096 进入 Jellyfin,首选显示语言 选择 汉语(简化字)

单击 下一个,根据个人情况设置 用户名密码

单击 下一个,设置媒体库:

单击 + 添加媒体库:

根据实际情况进行配置:

  1. 选择 内容类型 并填写 显示名称
  2. 文件夹 中添加所有包含当前类型媒体的文件夹。
  3. 首选下载语言 选择 Chinese
  4. 国家/地区 选择 People's Republic of China
  5. 取消勾选 元数据下载器图片获取程序 中所有选项。
  6. 其他设置暂时保持默认。

单击 下一个,设置首选元数据语言:

单击 下一个,设置远程访问:

单击 下一个,完成设置:

单击 完成 进入登录界面:

进入系统后,单击左侧菜单按钮,选择 管理 > 控制台 菜单。进入 控制台 后,选择 服务器 > 播放 菜单。将 转码 中的 硬件加速 选择为 Video Acceleration API (VAAPI),注意确认 VA-API 设备 是否为 /dev/dri/renderD128,并在 启用硬件解码 勾选所有媒体类型。注意确认 硬件编码选项 中的 启用硬件编码 选项已勾选。

提示

根据官方文档说明,针对部分 CPU(例如:N5105)需要勾选 启用低电压模式的 Intel H.264 硬件编码器以确保硬件加速正常工作。

服务器 > 播放 菜单中,勾选 启用备用字体,将 备用字体文件路径 设置为 /usr/local/share/fonts

TMM 刮削

tinyMediaManager 是一个用 Java/Swing 编写的媒体管理工具,它可以为多种媒体服务器提供元数据。TMM 提供了多个平台的客户端,但为了多客户端刮削时数据共享,本例也使用 Docker 进行安装。

在软路由 /data 目录中创建用于 TMM 的目录:

mkdir /data/docker/tinymediamanager
mkdir /data/docker/tinymediamanager/config
mkdir /data/docker/tinymediamanager/media
mkdir /data/docker/tinymediamanager/media/nas
mkdir /data/docker/tinymediamanager/media/nas/disk1
mkdir /data/docker/tinymediamanager/media/nas/disk2
mkdir /data/docker/tinymediamanager/media/nas/disk3
mkdir /data/docker/tinymediamanager/media/nas/disk4

进入命令行,输入如下命令将 NAS 上配置好的共享文件夹挂载到 TMM 的相关目录:

mount.nfs -w 192.168.5.10:/volume1/Disk1 /data/docker/tinymediamanager/media/nas/disk1 -o nolock
mount.nfs -w 192.168.5.10:/volume2/Disk2 /data/docker/tinymediamanager/media/nas/disk2 -o nolock
mount.nfs -w 192.168.5.10:/volume3/Disk3 /data/docker/tinymediamanager/media/nas/disk3 -o nolock
mount.nfs -w 192.168.5.10:/volume4/Disk4 /data/docker/tinymediamanager/media/nas/disk4 -o nolock

为了保证每次启动软路由时能够自动挂载,请将上述内容添加至 系统 > 启动项 菜单下的 本地启动脚本 文本框的 exit 0 之前。

Docker > 镜像 菜单的 拉取镜像 处填写 romancin/tinymediamanager:latest-v4,然后单击 拉取

通过 Docker > 容器 菜单,单击 添加 按钮添加容器。单击 命令行 并复制如下内容,单击 提交 解析命令行:

docker run -d \
 --name=tinymediamanager \
 --hostname=tinymediamanager \
 --pull=always \
 --privileged \
 --volume /data/docker/tinymediamanager/config:/config \
 --volume /data/docker/tinymediamanager/media:/media \
 --user root:root \
 --env ENABLE_CJK_FONT=1 \
 --publish 5800:5800 \
 --restart=unless-stopped \
 romancin/tinymediamanager:latest-v4

如果 总是先拉取镜像 未成功自动勾选,可以手动勾选确保运行前拉取最新镜像。单击 提交 创建容器。勾选 TMM 容器,单击 启动 按钮启动容器。

安装完毕后重启容器。通过 http://192.168.5.1:5800 进入 TMM:

根据向导进行配置,设置中文界面后需要重启容器生效。

警告

PT 用户注意,不要开启任何自动重命名,不要将 NFO 保存为与媒体文件相同的文件名,避免覆盖原始内容从而导致做种错误。

根据个人喜好配置好 TMM 后即可对媒体文件进行刮削了,在此不再详细展开刮削过程。由于原始文件的命名可能导致自动获取的信息有误,因此建议对每一个媒体文件刮削结果进行人工复核。

提示

Docker 版本 TMM 不支持输入中文,在通过 Clipboard 内外传输剪切板时中文也会出现乱码,且目前暂时无法修复。

4.0 之后版本的 TMM 免费版不再支持自动下载字幕,由于 TMM 采用 Open Subtitles,对于有需要双语字幕和特效字幕的同学并不友好。建议还是自行手动下载字幕并放置在媒体文件中,在此提供几个不错的字幕下载网站:

测试

经过 TMM 刮削后,Jellyfin 即可自动识别元数据,示例电影的详细信息如下如所示:

单击播放后,通过播放信息查看,已经可以使用 Jellyfin 实现转码在线播放:

测试完成后即可在各个终端安装相应的的客户端:

  1. iPhone & iPad & Apple TV:建议使用 Swiftfin,官方应用,原生界面体验,应用商店直接下载安装。
  2. Android 手机:建议使用 Findroid,第三方应用,原生界面体验,应用商店直接下载安装,非原生 Android 系统可以在 Github 页面下载离线 apk 文件安装。
  3. Android TV:建议使用 Jellyfin for Android TV,官方应用,应用商店直接下载安装,非原生 Android 系统可以在 Github 页面下载离线 apk 文件安装。

可以在官方客页面探索更多官方和第三方客户端。在电视盒子等仅用于播放视频的设备上,可以尝试启用 Direct Play,当然也需要根据电视盒子的特性进行调整,避免部分格式的视频和音频无法正常解析。

自私和贪婪 (Selfish and Greedy)

2022-09-11 08:00:00

适当的自私,但不应贪婪。

学了这么多年管理,除了印象最深且还能在工作中不时提起的马斯洛需求层次理论之外,还有就是亚当斯密的经济人假设了。经济人假设每个人都以自身利益最大化为目标,我称这即为自私。社会人假设人的最大动机是社会需要,我认为这也没有逃离自私的范畴,只是需求的类型有些差异而已。苟且和远方,不分贵贱,亦可兼得,但想要的太多,抑或不劳而获,甚至痴心妄想,那这就是贪婪

自私

不像会纠结到底是人性本善还是人性本恶,我认为人天生就是自私的。社会是由一个个人组成,脱离个体谈集体是无效的,当个体都无法被满足时,集体的功能不可能正常。我无法做到囚徒困境在神学院学生所做出的选择 1,那是一种超脱的信仰的力量,我肃穆敬仰,但恕我如实做不到。

不为恶

不为恶是我毕生的信条,执行起来不易。通俗些就是不给别人添堵,但并不是麻烦别人就是添堵,有时候就是需要去麻烦别人,我称这样的人为朋友。我认为不为恶是自私的底线,切莫损人利己,损人不利己就更可恨了。正道成功很难,邪门歪道却很容易让你误入歧途,但弯道超车未尝不可,至少我认为适合我这种在一些专业领域并不是专业的人。

诗和远方的动力

自私会是一种动力,而且不与社会主流价值观相违背。例如:我想买一辆越野车(这是真话,真的想买)和我要为世界和平奋斗终身(这也不是假话)。理想不分贵贱,看起来第一个是满足我一己私欲,第二个是利他主义,很有可能两者对于当下我的要求都是努力提升个人的专业素养,但第一个会更能促进我达成提高个人能力的目的。因为从提升个人能力到诗和远方,第一个离我会更近些,来的更实在些,更会让我更有动力些。同时,我认为这些“自私”的小目标积累起来有助于我达成“宏伟”的大目标。

贪婪

我想我并不贪婪,说实话要不是最近遇到一些事情,我都从来没认真审视过“贪婪”二字。当你脑海中都没有这么一个概念,你怎么可能会有这种想法呢。遇到的那件小事告诉我,贪婪容易出现在当你获得一定成就时,或许是被喜悦冲昏了头脑,在荷尔蒙的激发下会让我们变得更胆大妄为。我认为保持冷静,时不时的多给自己泼几盆冷水,多照照镜子,会让你更清楚你是谁。

原罪

七宗罪中有贪婪,没有自私。我想是因为贪婪是一种“过分”的追求,而自私并不是那么“过分”。我有很多想要的东西,至少我认为不过分,但我也会知足,因为知足常乐,人生嘛,最重要的就是开心。我会把贪婪和机会主义并列在一起,一方面机会主义的行为常常就是不顾及他人感受的添堵或为恶,另一方面机会主义所展现出的形势并不一定是“机会”。抓住机会很重要,但审视机会也不容小觑,莫让“机会”冲昏了头脑。

量力而为

贪婪是无休止的自私,不切实际的自私,贪得无厌描述的恰到其实。士兵突击中团长对许三多说的话我觉得放在这很贴切:“想要和得到,中间还有两个字,那就是要做到,你只有做到,你才能得到嘛”。认清自己,量力而为,清楚想要什么需要付出多少,然后去奋斗,去努力,才有可能得到。也许有人会说,你不做,怎么知道做不到?这句话我也挑不出什么毛病,但我想一个人的能力除了能够洞察机会,能认清现实也是很牛的一批。

我的选择

适当的自私。有底线,通过自我认知和自我实践,拼搏奋斗,承担责任,包容利他。有底线是明确大是大非,不为恶。自我认知和实践是认清自己和所处的环境,切莫空想和冲动。拼搏奋斗是说要去做,努力去做,才有机会自我实现。承担责任是说不要什么都是“关我屁事”和“关你屁事”,我们是自己,也是社会的一员,有权利,也有义务和责任,该承担还是要承担,这才像个爷们。包容利他我想必然就会是这样做之后超越极端自私的结果。


  1. 印象中应该是在耶鲁大学公开课 Justice: What’s The Right Thing To Do 中有讲到,神学院学生的结果为均不检举对方。 ↩︎

评分和排名算法 (Rating & Ranking Algorithms)

2022-05-22 08:00:00

在之前的博客「投票公平合理吗?」中已经得到了一个令人沮丧的结论:只有道德上的相对民主,没有制度上的绝对公平。投票是对不同选项或个体的排序,在投票中我们关注更多是相对位置这样定性的结论,例如:积分前三名的同学才能进入下一环节。但有的时候我们不光想知道不同选项之间的先后顺序,还想了解不同选项之间的差异大小,这时我们就需要设计更精细的方法进行定量分析。

基础评分和排名

直接评分

从小到大被评分最多的应该就是考试了,100,120 或是 150,这三个数字应该从小学一年级一直“陪”我们走过十几载青春。考试的评分算法简单且容易区分,整个系统设置了一个总分,根据不同的表现进行加分或扣分,统计最终得分作为最后的评分。一般情况下成绩是一个近似正态分布的偏态分布,如下图所示。

如果成绩近似正态分布(如上图-中),则说明本次考试难度分布较为均衡;如果成绩分布整体向左偏(如上图-左),则说明本次考试较为困难,学生成绩普遍偏低;如果成绩分布整体向右偏(如上图-右),则说明本次考试较为容易,学生成绩普遍偏高。

除此之外,也可能出现双峰分布,以及峰的陡峭和平缓都能反应考试的不同问题,在此就不再一一展开说明。一般情况下,考试的最终成绩已经能够很好地对学生的能力进行区分,这也正是为什么一般情况我们不会对考试分数做二次处理,而是直接使用。

加权评分

在现实生活中,不同的问题和任务难易程度不同,为了保证「公平」,我们需要赋予困难的任务更多的分数。这一点在试卷中也会有体现,一般而言判断题会比选择题分数更低,毕竟随机作答,判断题仍有 50% 的概率回答正确,但包含四个选项的选择题却仅有 25% 概率回答正确。

加权评分在问题和任务的难易程度与分值之间通过权重进行平衡,但权重的制定并不是一个容易的过程,尤其是在设置一个兼顾客观、公平、合理等多维度的权重时。

考虑时间的评分和排名

Delicious

最简单直接的方法是在一定的时间内统计投票的数量,得票数量高的则为更好的项目。在旧版的 Delicious 中,热门书签排行榜则是根据过去 60 分钟内被收藏的次数进行排名,每 60 分钟重新统计一次。

这种算法的优点是:简单、容易部署、更新快;缺点是:一方面,排名变化不够平滑,前一个小时还排名靠前的内容,往往第二个小时就一落千丈,另一方面,缺乏自动淘汰旧项目的机制,某些热门内容可能会长期占据排行榜前列。

Hacker News

Hacker News 是一个可以发布帖子的网络社区,每个帖子前面有一个向上的三角形,如果用户觉得这个内容好,点击一下即可投票。根据得票数,系统自动统计出热门文章排行榜。

Hacker News 使用分数计算公式如下:

$$ Score = \dfrac{P - 1}{\left(T + 2\right)^G} \label{eq:hacker-news} $$

其中,$P$ 表示帖子的得票数,减去 $1$ 表示忽略发帖人的投票;$T$ 表示当前距离发帖的时间(单位为小时),加上 $2$ 是为了防止最新的帖子分母过小;$G$ 为重力因子,即将帖子排名被往下拉的力量,默认值为 $1.8$。

在其他条件不变的情况下,更多的票数可以获得更高的分数,如果不希望“高票数”帖子和“低票数”帖子之间差距过大,可以在式 $\ref{eq:hacker-news}$ 的分子中添加小于 $1$ 的指数,例如:$\left(P - 1\right)^{0.8}$。在其他条件不变的情况下,随着时间不断流逝,帖子的分数会不断降低,经过 24 小时后,几乎所有帖子的分数都将小于 $1$。重力因子对于分数的影响如下图所示:

不难看出,$G$ 值越大,曲线越陡峭,排名下降的速度越快,意味着排行榜的更新速度越快。

Reddit

不同于 Hacker News,Reddit 中的每个帖子前面都有向上和向下的箭头,分别表示"赞成"和"反对"。用户点击进行投票,Reddit 根据投票结果,计算出最新的热点文章排行榜。

Reddit 关于计算分数的代码可以简要总结如下:

from datetime import datetime, timedelta
from math import log

epoch = datetime(1970, 1, 1)

def epoch_seconds(date):
    td = date - epoch
    return td.days * 86400 + td.seconds + (float(td.microseconds) / 1000000)

def score(ups, downs):
    return ups - downs

def hot(ups, downs, date):
    s = score(ups, downs)
    order = log(max(abs(s), 1), 10)
    sign = 1 if s > 0 else -1 if s < 0 else 0
    seconds = epoch_seconds(date) - 1134028003
    return round(order + sign * seconds / 45000, 7)

分数的计算过程大致如下:

  1. 计算赞成票和反对票的差值,即: $$ s = ups - downs $$
  2. 利用如下公式计算中间分数,即: $$ order = \log_{10} \max\left(\left|s\right|, 1\right) $$ 其中,取 $\left|s\right|$ 和 $1$ 的最大值是为了避免当 $s = 0$ 时,无法计算 $\log_{10}{\left|s\right|}$。赞成票与反对票差值越大,得分越高。取以 $10$ 为底的对数,表示当 $s = 10$ 时,这部分为 $1$,只有 $s = 100$ 时才为 $2$,这样设置是为了减缓差值增加对总分的影响程度。
  3. 确定分数的方向,即: $$ sign = \begin{cases} 1 & \text{如果} \ s > 0 \\ 0 & \text{如果} \ s = 0 \\ -1 & \text{如果} \ s < 0 \end{cases} $$
  4. 计算发贴时间距离 2005 年 12 月 8 日 7:46:43(Reddit 的成立时间?)的秒数,即: $$ seconds = \text{timestamp}\left(date\right) - 1134028003 $$
  5. 计算最终分数,即: $$ score = order + sign \times \dfrac{seconds}{45000} $$ 将时间除以 $45000$ 秒(即 12.5 个小时),也就是说当前天的帖子会比昨天的帖子多约 $2$ 分。如果昨天的帖子想要保持住之前的排名,则 $s$ 值需要增加 $100$ 倍才可以。

Reddit 评分排名算法决定了 Reddit 是一个符合大众口味的社区,而不是一个适合展示激进想法的地方。因为评分中使用的是赞成票和反对票的差值,也就是说在其他条件相同的情况下,帖子 A 有 1 票赞成,0 票反对;帖子 B 有 1000 票赞成,1000 票反对,但讨论火热的帖子 B 的得分却比 帖子 A 要少。

Stack Overflow

Stack Overflow 是世界排名第一的程序员问答社区。用户可以在上面提出各种关于编程的问题,等待别人回答;可以对问题进行投票(赞成票或反对票),表示这个问题是不是有价值;也可以对这个回答投票(赞成票或反对票),表示这个回答是不是有价值。

在 Stack Overflow 的页面上,每个问题前面有三个数字,分别为问题的得分、回答的数量和问题的浏览次数。

创始人之一的 Jeff Atwood 公布的评分排名的计算公式如下:

$$ \dfrac{4 \times \log_{10}{Q_{views}} + \dfrac{Q_{answers} \times Q_{score}}{5} + \sum \left(A_{scores}\right)}{\left(\left(Q_{age} + 1\right) - \left(\dfrac{Q_{age} - Q_{updated}}{2}\right)\right)^{1.5}} $$

其中:

  1. $4 \times \log_{10}{Q_{views}}$ 表示问题的浏览次数越多,得分越高,同时利用 $\log_{10}$ 减缓了随着浏览量增大导致得分变高的程度。
  2. $\dfrac{Q_{answers} \times Q_{score}}{5}$ 表示问题的得分(赞成票和反对票之差)越高,回答的数量越多,分数越高。采用乘积的形式意味着即使问题本身的分数再高,没有人回答的问题也算不上热门问题。
  3. $\sum \left(A_{scores}\right)$ 表示问题回答的总分数。回答总分采用了简单的加和,但实际上一个正确的回答要胜过多个无用的回答,简答的加和无法很好的区分这两种不同的情况。
  4. $\left(\left(Q_{age} + 1\right) - \left(\dfrac{Q_{age} - Q_{updated}}{2}\right)\right)^{1.5}$ 可以改写为 $\left(\dfrac{Q_{age}}{2} + \dfrac{Q_{updated}}{2} + 1\right)^{1.5}$$Q_{age}$$Q_{updated}$ 分别表示问题和最近一次回答的时间(单位为小时),也就是说问题时间越久远,最近一次回答时间约久远,分母就会越大,从而得分就会越小。

Stack Overflow 的评分排名算法考虑了参与程度(问题浏览次数和回答次数)、质量(问题分数和回答分数)、时间(问题时间和最近一次回答时间)等多个维度。

不考虑时间的评分和排名

上文中介绍的评分和排名方法多适用于具有时效性的信息,但是对于图书、电影等无需考虑时间因素的情况而言,则需要其他方法进行衡量。

威尔逊区间算法

在不考虑时间的情况下,以「赞成」和「反对」两种评价方式为例,通常我们会有两种最基础的方法计算得分。第一种为绝对分数,即:

$$ \text{评分} = \text{赞成票} - \text{反对票} $$

但这种计算方式有时会存在一定问题,例如:A 获得 60 张赞成票,40 张反对票;B 获得 550 张赞成票,450 张反对票。根据上式计算可得 A 的评分为 20,B 的评分为 100,所以 B 要优于 A。但实际上,B 的好评率仅有 $\dfrac{550}{550 + 450} = 55\%$,而 A 的好评率为 $\dfrac{60}{60 + 40} = 60\%$,因此实际情况应该是 A 优于 B。

这样,我们就得到了第二种相对分数,即:

$$ \text{评分} = \dfrac{\text{赞成票}}{\text{赞成票} + \text{反对票}} $$

这种方式在总票数比较大的时候没有问题,但总票数比较小时就容易产生错误。例如:A 获得 2 张赞成票,0 张反对票;B 获得 100 张赞成票,1 张反对票。根据上式计算可得 A 的评分为 $100\%$,B 的评分为 $99\%$。但实际上 B 应该是优于 A 的,由于 A 的总票数太少,数据不太具有统计意义。

对于这个问题,我们可以抽象出来:

  1. 每个用户的投票都是独立事件。
  2. 用户只有两个选择,要么投赞成票,要么投反对票。
  3. 如果投票总人数为 $n$,其中赞成票为 $k$,则赞成票的比例 $p = \dfrac{k}{n}$

不难看出,上述过程是一个二项实验。$p$ 越大表示评分越高,但是 $p$ 的可信性取决于投票的人数,如果人数太少,$p$ 就不可信了。因此我们可以通过计算 $p$ 的置信区间对评分算法进行调整如下:

  1. 计算每个项目的好评率。
  2. 计算每个好评率的置信区间。
  3. 根据置信区间的下限值进行排名。

置信区间的本质就是对可信度进行修正,弥补样本量过小的影响。如果样本足够多,就说明比较可信,则不需要很大的修正,所以置信区间会比较窄,下限值会比较大;如果样本比较少,就说明不一定可信,则需要进行较大的修正,所以置信区间会比较宽,下限值会比较小。

二项分布的置信区间有多种计算公式,最常见的「正态区间」方法对于小样本准确性较差。1927 年,美国数学家 Edwin Bidwell Wilson 提出了一个修正公式,被称为「威尔逊区间」,很好地解决了小样本的准确性问题。置信区间定义如下:

$$ \frac{1}{1+\frac{z^{2}}{n}}\left(\hat{p}+\frac{z^{2}}{2 n}\right) \pm \frac{z}{1+\frac{z^{2}}{n}} \sqrt{\frac{\hat{p}(1-\hat{p})}{n}+\frac{z^{2}}{4 n^{2}}} $$

其中,$\hat{p}$ 表示样本好评率,$n$ 表示样本大小,$z$ 表示某个置信水平的 z 统计量。

贝叶斯平均算法

在一些榜单中,有时候会出现排行榜前列总是那些票数最多的项目,新项目或者冷门的项目很难有出头机会,排名可能会长期靠后。以世界最大的电影数据库 IMDB 为例,观众可以对每部电影投票,最低为 1 分,最高为 10 分,系统根据投票结果,计算出每部电影的平均得分。

这就出现了一个问题:热门电影与冷门电影的平均得分,是否真的可比?例如一部好莱坞大片有 10000 个观众投票,一部小成本的文艺片可能只有 100 个观众投票。如果使用威尔逊区间算法,后者的得分将被大幅拉低,这样处理是否公平,是否能反映电影的真正质量呢?在 Top 250 榜单中,IMDB 给到的评分排名计算公式如下:

$$ WR = \dfrac{v}{v + m} R + \dfrac{m}{v + m} C $$

其中,$WR$ 为最终的加权得分,$R$ 为该电影用户投票的平均得分,$v$ 为该电影的投票人数,$m$ 为排名前 250 电影的最低投票数,$C$ 为所有电影的平均得分。

从公式中可以看出,分量 $m C$ 可以看作为每部电影增加了评分为 $C$$m$ 张选票。然后再根据电影自己的投票数量 $v$ 和投票平均分 $R$ 进行修正,得到最终的分数。随着电影投票数量的不但增加 $\dfrac{v}{v + m} R$ 占的比重将越来越大,加权得分也会越来越接近该电影用户投票的平均分。

将公式写为更一般的形式,有:

$$ \bar{x}=\frac{C m+\sum_{i=1}^{n} x_{i}}{C+n} $$

其中,$C$ 为需要扩充的投票人数规模,可以根据投票人数总量设置一个合理的常数,$n$ 为当前项目的投票人数,$x$ 为每张选票的值,$m$ 为总体的平均分。这种算法称为「贝叶斯平均」。在这个公式中,$m$ 可以视为“先验概率”,每新增一次投票,都会对最终得分进行修正,使其越来越接近真实的值。

比赛评分和排名

Kaggle 积分

Kaggle 是一个数据建模和数据分析竞赛平台。企业和研究者可在其上发布数据,统计学者和数据挖掘专家可在其上进行竞赛以产生最好的模型。用户以团队形式参加 Kaggle 的比赛,团队可以仅包含自己一人,根据在每场比赛中的排名不断获取积分,用做 Kaggle 网站中的最终排名

早期 Kaggle 对于每场比赛的积分按如下方式计算:

$$ \left[\dfrac{100000}{N_{\text {teammates }}}\right]\left[\text{Rank}^{-0.75}\right]\left[\log _{10}\left(N_{\text {teams }}\right)\right]\left[\dfrac{2 \text { years - time }}{2 \text { years }}\right] $$

在 2015 年对新的排名系统做了调整,新的比赛积分计算公式调整为:

$$ \left[\dfrac{100000}{\sqrt{N_{\text {teammates }}}}\right]\left[\text{Rank}^{-0.75}\right]\left[\log _{10}\left(1+\log _{10}\left(N_{\text {teams }}\right)\right)\right]\left[e^{-t / 500}\right] $$

其中,$N_{\text{teammates}}$ 为团队成员的数量,$\text{Rank}$ 为比赛排名,$N_{\text{teams}}$ 为参赛的团队数量,$t$ 为从比赛结束之日起过去的时间。

第一部分可以视为基础分,团队成员越少,所获得的基础分越多。从调整的文档来看,Kaggle 认为团队合作每个人的贡献程度会大于 $1 / N_{\text {teammates}}$,为了鼓励大家团队合作,Kaggle 减少了对团队人数的基础分惩罚力度。

第二部分则是根据用户在比赛中的排名得到一个小于等于 1 的系数。下图显示了不同的指数以及 $1 / \text{Rank}$ 之间的区别:

从图中可以看出,通过调节指数的大小可以控制系数随排名下降而下降的速度。整体来说,Kaggle 更加重视前几名,对于 10 名开外的选手,系数均小于 $0.2$,且差异不大。

第三部分可以理解为通过参赛的队伍数量来衡量比赛的受欢迎程度(或是在众多参赛队伍中脱颖而出的难易程度)。以 100 和 1000 支参赛队伍对比为例,根据之前的计算公式,这一部分为:

$$ \begin{equation} \begin{aligned} \log_{10} \left(100\right) &= 2 \\ \log_{10} \left(1000\right) &= 3 \end{aligned} \end{equation} $$

但随着 Kaggle 本身比赛流行度越来越高,官方认为赢得一场 1000 人的比赛并不需要比赢得一场 100 人的比赛需要多 $50\%$ 的技能,因此通过调整后的算法,这个比例调整至大约为 $25\%$

$$ \begin{equation} \begin{aligned} \log_{10} \left(\log_{10} \left(100\right) + 1\right) &\approx 0.47 \\ \log_{10} \left(\log_{10} \left(1000\right) + 1\right) &\approx 0.6 \end{aligned} \end{equation} $$

第四部份为时间衰减项,调整为新的计算公式后可以消除原来通过设置 2 年时限导致的积分断崖。如果任何一对个体都没有采取任何进一步的行动,那么排名不应该在任何一对个体之间发生变化。换句话说,如果整个 Kaggle 用户群停止参加比赛,他们的相对排名应该随着时间的推移保持不变。选择 $1 / 500$ 的原因是可以将旧的 2 年断崖延长到更长的时间范围,并且永远不会变为 0。

Elo 评分系统

Elo 评分系统(Elo Rating System)是由匈牙利裔美国物理学家 Arpad Elo 创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估公认的权威标准,且被广泛用于国际象棋、围棋、足球、篮球等运动。网络游戏的竞技对战系统也常采用此评分系统。

Elo 评分系统是基于统计学的一个评估棋手水平的方法。Elo 模型原先采用正态分布,但实践显明棋手的表现并非正态分布,所以现在的评分计分系统通常使用的是逻辑分布。

假设棋手 A 和 B 的当前评分分别为 $R_A$$R_B$,则按照逻辑分布,A 对 B 的胜率期望值为:

$$ E_{A}=\frac{1}{1+10^{\left(R_{B}-R_{A}\right) / 400}} $$

类似的有 B 对 A 的胜率期望值为:

$$ E_{B}=\frac{1}{1+10^{\left(R_{A}-R_{B}\right) / 400}} $$

假如一位棋手在比赛中的真实得分 $S_{A}$(胜 1 分,和 0.5 分,负 0 分)和他的胜率期望值 $E_{A}$ 不同,则他的评分要作相应的调整:

$$ R_{A}^{\prime} = R_{A} + K\left(S_{A}-E_{A}\right) $$

公式中 $R_{A}$$R_{A}^{\prime }$ 分别为棋手调整前后的评分。$K$ 值是一个极限值,代表理论上最多可以赢一个玩家的得分和失分,$K / 2$ 就是相同等级的玩家其中一方胜利后所得的分数。国际象棋大师赛中,$K = 16$;在大部分的游戏规则中,$K = 32$。通常水平越高的比赛中其 $K$ 值越小,这样做是为了避免少数的几场比赛就能改变高端顶尖玩家的排名。$E_A$$E_B$ 中的 $400$ 是让多数玩家积分保持标准正态分布的值,在 $K$ 相同的情况下,分母位置的值越大,积分变化值越小。

Glicko 评分系统

Glicko 评分系统(Glicko Rating System)及 Glicko-2 评分系统(Glicko-2 Rating System)是评估选手在比赛中(如国际象棋及围棋)的技术能力方法之一。此方法由马克·格利克曼发明,原为国际象棋评分系统打造,后作为评分评分系统的改进版本广泛应用 1

Elo 评分系统的问题在于无法确定选手评分的可信度,而 Glicko 评分系统正是针对此进行改进。假设两名评分均为 1700 的选手 A 和 B 在进行一场对战后 A 获得胜利,在美国国际象棋联赛的 Elo 评分系统下,A 选手评分将增长 16,对应的 B 选手评分将下降 16。但是假如 A 选手是已经很久没玩,但 B 选手每周都会玩,那么在上述情况下 A 选手的 1700 评分并不能十分可信地用于评定其实力,而 B 选手的 1700 评分则更为可信。

Glicko 算法的主要贡献是“评分可靠性”(Ratings Reliability),即评分偏差(Ratings Deviation)。若选手没有评分,则其评分通常被设为 1500,评分偏差为 350。新的评分偏差($RD$)可使用旧的评分偏差($RD_0$)计算:

$$ RD = \min \left(\sqrt{RD_0^2 + c^2 t}, 350\right) $$

其中,$t$ 为自上次比赛至现在的时间长度(评分期),常数 $c$ 根据选手在特定时间段内的技术不确定性计算而来,计算方法可以通过数据分析,或是估算选手的评分偏差将在什么时候达到未评分选手的评分偏差得来。若一名选手的评分偏差将在 100 个评分期间内达到 350 的不确定度,则评分偏差为 50 的玩家的常数 $c$ 可通过解 $350 = \sqrt{50^2 + 100 c^2}$,则有 $c = \sqrt{\left(350^2 - 50^2\right) / 100} \approx 34.6$

在经过 $m$ 场比赛后,选手的新评分可通过下列等式计算:

$$ r=r_{0}+\frac{q}{\frac{1}{R D^{2}}+\frac{1}{d^{2}}} \sum_{i=1}^{m} g\left(R D_{i}\right)\left(s_{i}-E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\right) $$

其中:

$$ \begin{equation*} \begin{aligned} & g\left(R D_{i}\right) = \frac{1}{\sqrt{1+\frac{3 q^{2}\left(R D_{i}^{2}\right)}{\pi^{2}}}} \\ & E\left(s \mid r, r_{i}, R D_{i}\right) = \frac{1}{1+10\left(\frac{g\left(R D_{i}\right)\left(r_{0}-r_{i}\right)}{-400}\right)} \\ & q = \frac{\ln (10)}{400}=0.00575646273 \\ & d^{2} = \frac{1}{q^{2} \sum_{i=1}^{m}\left(g\left(R D_{i}\right)\right)^{2} E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\left(1-E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\right)} \end{aligned} \end{equation*} $$

$r_i$ 表示选手个人评分,$s_i$ 表示每场比赛后的结果。胜利为 $1$,平局为 $1 / 2$,失败为 $0$

原先用于计算评分偏差的函数应增大标准差值,进而反应模型中一定非观察时间内,玩家的技术不确定性的增长。随后,评分偏差将在几场游戏后更新:

$$ R D^{\prime}=\sqrt{\left(\frac{1}{R D^{2}}+\frac{1}{d^{2}}\right)^{-1}} $$

Glicko-2 评分系统

Glicko-2 算法与原始 Glicko 算法类似,增加了一个评分波动率 $\sigma$,它根据玩家表现的不稳定程度来衡量玩家评分的预期波动程度。例如:当一名球员的表现保持稳定时,他们的评分波动性会很低,如果他们在这段稳定期之后取得了异常强劲的成绩,那么他们的评分波动性就会增加 1

Glicko-2 算法的简要步骤如下:

计算辅助量

在一个评分周期内,当前评分为 $\mu$ 和评分偏差为 $\phi$ 的玩家与 $m$ 个评分为 $\mu_1, \cdots, \mu_m$ 和评分偏差为 $\phi_1, \cdots, \phi_m$ 的玩家比赛,获得的分数为 $s_1, \cdots, s_m$,我们首先需要计算辅助量 $v$$\Delta$

$$ \begin{aligned} v &= \left[\sum_{j=1}^{m} g\left(\phi_{j}\right)^{2} E\left(\mu, \mu_{j}, \phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\}\right]^{-1} \\ \Delta &= v \sum_{j=1}^{m} g\left(\phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\} \end{aligned} $$

其中:

$$ \begin{equation*} \begin{aligned} &g(\phi)=\frac{1}{\sqrt{1+3 \phi^{2} / \pi^{2}}}, \\ &E\left(\mu, \mu_{j}, \phi_{j}\right)=\frac{1}{1+\exp \left\{-g\left(\phi_{j}\right)\left(\mu-\mu_{j}\right)\right\}} \end{aligned} \end{equation*} $$

确定新的评分波动率

选择一个小的常数 $\tau$ 来限制时间的波动性,例如:$\tau = 0.2$(较小的 $\tau$ 值可以防止剧烈的评分变化),对于:

$$ f(x)=\frac{1}{2} \frac{e^{x}\left(\Delta^{2}-\phi^{2}-v^{2}-e^{x}\right)}{\left(\phi^{2}+v+e^{x}\right)^{2}}-\frac{x-\ln \left(\sigma^{2}\right)}{\tau^{2}} $$

我们需要找到满足 $f\left(A\right) = 0$ 的值 $A$。解决此问题的一种有效方法是使用 Illinois 算法,一旦这个迭代过程完成,我们将新的评级波动率 $\sigma'$ 设置为:

$$ \sigma' = e^{\frac{A}{2}} $$

确定新的评分偏差和评分

之后得到新的评分偏差:

$$ \phi^{\prime} = \dfrac{1}{\sqrt{\dfrac{1}{\phi^{2}+\sigma^{\prime 2}}+\dfrac{1}{v}}} $$

和新的评分:

$$ \mu^{\prime} = \mu+\phi^{\prime 2} \sum_{j=1}^{m} g\left(\phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\} $$

需要注意这里的评分和评分偏差与原始 Glicko 算法的比例不同,需要进行转换才能正确比较两者。

TrueSkill 评分系统

TrueSkill 评分系统是基于贝叶斯推断的评分系统,由微软研究院开发以代替传统 Elo 评分系统,并成功应用于 Xbox Live 自动匹配系统。TrueSkill 评分系统是 Glicko 评分系统的衍伸,主要用于多人游戏中。TrueSkill 评分系统考虑到了个别玩家水平的不确定性,综合考虑了各玩家的胜率和可能的水平涨落。当各玩家进行了更多的游戏后,即使个别玩家的胜率不变,系统也会因为对个别玩家的水平更加了解而改变对玩家的评分 2

在电子竞技游戏中,特别是当有多名选手参加比赛的时候需要平衡队伍间的水平,让游戏比赛更加有意思。这样的一个参赛选手能力平衡系统通常包含以下三个模块:

  1. 一个包含跟踪所有玩家比赛结果,记录玩家能力的模块。
  2. 一个对比赛成员进行配对的模块。
  3. 一个公布比赛中各成员能力的模块。

能力计算和更新

TrueSkill 评分系统是针对玩家能力进行设计的,以克服现有排名系统的局限性,确保比赛双方的公平性,可以在联赛中作为排名系统使用。TrueSkill 评分系统假设玩家的水平可以用一个正态分布来表示,而正态分布可以用两个参数:平均值和方差来完全描述。设 Rank 值为 $R$,代表玩家水平的正态分布的两个参数平均值和方差分别为 $\mu$$\sigma$,则系统对玩家的评分即 Rank 值为:

$$ R = \mu - k \times \sigma $$

其中,$k$ 值越大则系统的评分越保守。

上图来自 TrueSkill 网站,钟型曲线为某个玩家水平的可能分布,绿色区域是排名系统的信念,即玩家的技能在 15 到 20 级之间。

下表格给出了 8 个新手在参与一个 8 人游戏后 $\mu$$\sigma$ 的变化。

姓名 排名 赛前 $\mu$ 赛前 $\sigma$ 赛后 $\mu$ 赛后 $\sigma$
Alice 1 25 8.3 36.771 5.749
Bob 2 25 8.3 32.242 5.133
Chris 3 25 8.3 29.074 4.943
Darren 4 25 8.3 26.322 4.874
Eve 5 25 8.3 23.678 4.874
Fabien 6 25 8.3 20.926 4.943
George 7 25 8.3 17.758 5.133
Hillary 8 25 8.3 13.229 5.749

第 4 名 Darren 和第 5 名 Eve,他们的 $\sigma$ 是最小的,换句话说系统认为他们能力的可能起伏是最小的。这是因为通过这场游戏我们对他们了解得最多:他们赢了3 和 4 个人,也输给了 4 和 3 个人。而对于第 1 名 Alice,我们只知道她赢了 7 个人。

定量分析可以先考虑最简单的两人游戏情况:

$$ \begin{aligned} &\mu_{\text {winner }} \longleftarrow \mu_{\text {winner }}+\frac{\sigma_{\text {winner }}^{2}}{c} * v\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right) \\ &\mu_{\text {loser }} \longleftarrow \mu_{\text {loser }}-\frac{\sigma_{\text {loser }}^{2}}{c} * v\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right) \\ &\sigma_{\text {winner }}^{2} \longleftarrow \sigma_{\text {uninner }}^{2} *\left[1-\frac{\sigma_{\text {winner }}^{2}}{c} * w\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right)\right. \\ &\sigma_{\text {loser }}^{2} \longleftarrow \sigma_{\text {loser }}^{2} *\left[1-\frac{\sigma_{\text {loser }}^{2}}{c} * w\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right)\right. \\ &c^{2}=2 \beta^{2}+\sigma_{\text {winner }}^{2}+\sigma_{\text {loser }}^{2} \end{aligned} $$

其中,系数 $\beta^2$ 代表的是所有玩家的平均方差,$v$$w$ 是两个函数,比较复杂,$\epsilon$ 是平局参数。简而言之,个别玩家赢了 $\mu$ 就增加,输了 $\mu$ 减小;但不论输赢,$\sigma$ 都是在减小,所以有可能出现输了涨分的情况。

对手匹配

势均力敌的对手能带来最精彩的比赛,所以当自动匹配对手时,系统会尽可能地为个别玩家安排可能水平最为接近的对手。TrueSkill 评分系统采用了一个值域为 $(0, 1)$ 的函数来描述两个人是否势均力敌:结果越接近 0 代表差距越大,越接近 1 代表水平越接近。

假设有两个玩家 A 和 B,他们的参数为 $(\mu_A, \sigma_A)$$(\mu_B, \sigma_B)$,则函数对这两个玩家的返回值为:

$$ e^{-\frac{\left(\mu_{A}-\mu_{B}\right)^{2}}{2 c^{2}}} \sqrt{\frac{2 \beta^{2}}{c^{2}}} $$

$c$ 的值由如下公式给出:

$$ c^{2}=2 \beta^{2}+\mu_{A}^{2}+\mu_{B}^{2} $$

如果两人有较大几率被匹配在一起,仅是平均值接近还不行,还需要方差也比较接近才可以。

在 Xbox Live 上,系统为每个玩家赋予的初值是 $\mu = 25$$\sigma = \dfrac{25}{3}$$k = 3$。所以玩家的起始 Rank 值为:

$$ R=25-3 \frac{25}{3}=0 $$

相较 Elo 评价系统,TrueSkill 评价系统的优势在于:

  1. 适用于复杂的组队形式,更具一般性。
  2. 有更完善的建模体系,容易扩展。
  3. 继承了贝叶斯建模的优点,如模型选择等。

本文主要参考了阮一峰的系列文章「基于用户投票的排名算法」和钱魏的「游戏排名算法:Elo、Glicko、TrueSkill」。

小记这一波裁员浪潮

2022-04-10 08:00:00

一眨眼已经四月了,第一季度穿插着反反复复的疫情、说干就干的战争和悲痛万分的空难就这么过去了。还在用生活不易、活着真好的话宽慰自己的时候,互联网迎来了一大波裁员浪潮。说实话其实每年都在陆陆续续的传出各大厂的裁员消息,可今年这一波着实动静大了些。翻看已经有两三篇待完成的博客躺在草稿箱中有个把月了,工作和生活的各种乱七八糟的事情扰得自己完全没有动笔的欲望。算是为了不让博客列表页有太长断档吧,小记一下最近的心情,也让自己冷静下来思考二三。

人都去哪儿了?

在这波裁员浪潮之前,工作上除了项目本身以外,最重要的就是招聘。从最初组建团队一直到现在招聘就像一个看似永远完不成的项目。摆在我面前的招聘三大问题:我看上的人家不一定看得上我,看得上我的我不一定看得上人家,都看上了的因为各种各样的不可控因素最后还是没谈成。

有时候从源头上就捞不到人,没有简历,约不上面,不可否认一些岗位的门槛和稀缺性注定这就是一场艰难的战役。但从个人主观感受上来看,逃离互联网的人可能正在增长。这个观点没有任何调研为依据,单纯的从身边的案例有感,不具备统计显著性,但也不接受反驳。为什么要逃离,或许逃离这个词用的就不好,应该说是「离开」更好些,不然显得当下的互联网有多么水深火热一样。因为大环境、家庭、亦或是躺平的心态,都很难说,自己没处在这么一个情景中,也说不出个四五到六来。

很难说未来自己会不会离开互联网这个圈子,如果离开也很难说这个时点有多近还是多远,无论主动还是被动。只能说当下的互联网仍旧在不断给予我机会和挑战,在没有被困难打倒之前,我应该还是会在这条路上拼杀几载吧。

每天都在忙些啥?

我属于一个不太喜欢开会的人,可能我的层次还不太高吧,我认为大部分事情都没法在相对正式的会议上达成很好的决策,我更喜欢非正式的沟通和正式的记录配合达成目标。所以排除开会,每天到底都在忙些啥?

想要的太多,不舍得放手的太多

人总是贪婪的,虽说给永远比拿快乐,但有时候就喜欢圈起来个一亩三分地,当然圈的越多也越好。其实道理自己都清楚,东西太多可能哪个都做不好,但有时候执念上来了,拦都拦不住。和当年的关不掉的浏览器标签页一样,不舍得放手。还是得多学会放手,一方面是给自己减负,另一方面也是给别人机会,这怎么说得我好像有多大权力似的😂。

时间管理

为真,是一个需要相对长远看待的问题,没有哪个真理是三两天就弄出来的。为需,才是第一重要的事情,光想着要做的多大多完美,不想想再不做点为所需的东西,团队都快没了,还谈什么理想。以一个明星项目养活俩仨探索项目,是我有时候会偷偷使用的小伎俩,先把本职的活儿干好,如果再时不时的整两个惊喜出来岂不美哉。就算没弄出来惊喜,只要没搞出来惊吓,至少我把分内的事儿做的没啥大毛病,老板也不会过来挑你啥。

但我坚信,创新才是第一动力,太本分不好。

努力不被「淘汰」?

有时候说被「淘汰」未免过分了些,组织淘汰人是「组织」认为「你」不再适合「组织」了,就像搞对象一样,「我」说「你」不是「我」喜欢的,还是有主观性的,没准儿人家到了别处还是顶梁柱嘞。怎么才能成为一代海王呢,个人观点,永无止境的学习是很重要的。

学啥?大点儿来说别学坏就好,当然好与坏,这又是个问题。我想表达的意思是,学些和工作有关的和学些和工作无关的都挺好的。两者之于工作都是有正向作用的,就比如这一年,我的这辆小摩托帮我结交了几个不错的朋友,帮我在不开心的时候变开心,开心的时候变得更开心,这还不够吗?学些和工作相关的技能那就更有益了,自己这点做的还是不太好,虽然能跟得上前沿的脚步,但也都是略知皮毛,开拓深耕些不熟悉的地方还是挺费力费时的。

怎么学?还记得之前大学时候管理学课上老师总说的一句话:读万卷书不如行万里路,行万里路不如交万名友。对于社牛症患者,Social 大可不必多说。对于有交通便利的朋友(比如有辆小摩托🏍的我😎),多和大自然接触接触还是舒服的,切记,安全第一。所有里面,效果相对不足的可能就是读书了,读书真的是成本很低的事情,时间可能是最需要消耗的东西,读书又确确实实是一件性价比很高的事情。这一年读的不多,暂且就拿客观因素当当借口吧。对于读书有一点我很受用,就是总结整理,吸进来些东西,消化消化,再创造些东西,不一定非要有多深的思考,哪怕是简单的归纳整理,写出来感觉一下子就不一样了。

活到老,学到老,这话在理。

万一万一,退路在哪里?

我是一个不太喜欢承受高风险的人,所以往往做事之前我都会尽可能想到最差的情况,多想想退路,好像也没什么毛病,是吧?万一万一,命运不济,有此一劫,我又该如何度过?一些具有高不可替代性的人不知道会不会思考这个问题,他们出现万一万一的概率太小了,我自认为还不是他们那种人,换句话,公司没了我照样转,往小了说,部门没了我照样转,最多卡个把月。

真正的退路也是需要很多客观条件支持的,比如雄厚的家庭资产,那我会选择回家继承我的百亩良田。再比如另一门硬手艺,大不了我换个行业,依旧可以做的风风火火。很不幸,这俩我都不符合,之前有时会也会问自己,如果有一天不能写代码了,我还能干点啥?我的菜属实做的还不错,开个饭馆没准儿能凑合,然而你抵挡不了天灾,疫情对餐饮、旅游、文娱的冲击真的不小。

那怎么办,没能力躺平,没实力单干,就只能坐等幸运女神眷顾了吗?作为一个「普通」人,我想也还是有些法子的,至少能让我们在遇见万一万一的时候可以更加从容的面对,快速顺利的找到下家,度过去,这不也是一条好路吗。时刻保持警惕感、夯实自己的核心能力、关注行业前沿动态、拓展个人认知范围,几点看似废话的东西我认为挺有用的,重点看你怎么去落实了。

光说不练假把式。

基于内容的图像检索 (Content Based Image Retrieval, CBIR)

2022-01-27 08:00:00

本文主要参考自 SIFT meets CNN: A decade survey of instance retrieval 1Deep Learning for Instance Retrieval: A Survey 2

基于内容的图像检索(Content-based image retrieval,CBIR),属于图像分析的一个研究领域。基于内容的图像检索目的是在给定查询图像的前提下,依据内容信息或指定查询标准,在图像数据库中搜索并查找出符合查询条件的相应图片 3

根据不同的视觉表示方法,可以将基于内容的图像检索方法分为两类:基于 SIFT 特征的基于深度学习的。基于 SIFT 特征的方法分为如下 3 类:

  1. 使用小型编码本:视觉词汇少于几千个,紧凑向量在降维和编码之前生成。
  2. 使用中型编码本:考虑到 BoW 的稀疏性和视觉词汇的低区分度,使用倒排索引和二进制签名方法。准确率和效率之间的权衡是该算法的主要影响因素。
  3. 使用大型编码本:考虑到 BoW 直方图的稀疏性和视觉词汇的高区分度,在算法中使用了倒排索引和存储友好型的签名方式。在编码本生成和编码中使用了类似的方法。

基于深度学习的方法分为如下 3 类:

  1. 混合型方法:图像块被多次输入到 CNN 中用于特征提取。编码与索引方法和基于 SIFT 的检索方法类似。
  2. 使用预训练的模型:通过在类似 ImageNet 的大数据集预训练的 CNN 模型进行单通道特征提取,同时使用紧凑编码和池化技术。
  3. 使用微调的模型:在图像与目标数据具有相似分布的训练集上对 CNN 模型进行微调。通过端到端的方法利用 CNN 模型进行单通道特征提取。这种视觉表示方法可以提升模型的区分能力。

各类模型的异同点如下表所示:

方法类型 检测 描述 编码 维度 索引
基于 SIFT 大编码本 DoG,
Hessian-Affine,
dense patches 等
局部不变
描述,例如:
SIFT
Hard, soft 倒排索引
中编码本 Hard, soft, HE 倒排索引
小编码本 VLAD, FV ANN 模型
基于深度学习 混合模型 图像块 CNN 特征 VLAD, FV, pooling 不定 ANN 模型
预训练模型 预训练 CNN 模型的列特征或全连接层 VLAD, FV, pooling ANN 模型
微调模型 从预训练 CNN 模型中端到端提取的全局特征 ANN 模型

基于内容的图像检索里程碑节点如下图所示:

基于 SIFT 的图像检索

基于 SIFT 的图像检索流程如下图所示:

  1. 局部特征提取:假设有一个包含 $N$ 张图片的集合 $\mathcal{G}$。指定一个特征检测器,从稀疏的兴趣点或密集的图像块中提取局部描述符。我们用 $D$ 表示局部描述符,$\left\{f_{i}\right\}_{i=i}^{D}, f_{i} \in \mathbb{R}^{p}$ 表示图像中被检测的区域。
  2. 编码本训练:基于 SIFT 的方法需要离线训练一个编码本。编码本中的每个视觉词汇位于子空间的中心,称为 Voronoi Cell。一个更大的密码本对应一个更精细的划分,进而产生更有区分性的视觉词汇,反之亦然。假设存在一个用无标注数据集训练的局部描述符池 $\mathcal{F} \equiv\left\{f_{i}\right\}_{i=1}^{M}$,一个基准方法是利用 K-means 将 $M$ 个点聚类成 $K$ 个簇,这 $K$ 个视觉词汇则构成了大小为 $K$ 的编码本。
  3. 特征编码:一个局部描述符 $f_{i} \in \mathbb{R}^{p}$ 通过特征编码过程 $f_{i} \rightarrow g_{i}$ 被映射到嵌入特征 $g_{i} \in \mathbb{R}^{l}$。当使用 K-means 聚类时,$f_i$ 可以根据其与视觉词汇的距离进行编码。

局部特征提取

Harris 角点检测

特征点在图像中一般有具体的坐标,并具有某些数学特征,如局部最大或最小灰度、以及某些梯度特征等。可以通过加权的差值平方和来形式化的比较一个图像的两个区块:

$$ \label{eq:e_u_v} E\left(u, v\right) = \sum_{x, y} w\left(x, y\right)\left[I\left(x + u, y + v\right)-I\left(x, y\right)\right]^{2} $$

其中,$I$ 为待比较的图像,$\left(u, v\right)$ 为平移向量,$w\left(x, y\right)$ 是在空间上变化的权重。

根据泰勒展开,窗口平移后图像的一阶近似为:

$$ \begin{aligned} I(x + u, y + v) &= I(x, y) + I_{x}(x, y) u + I_{y}(x, y) v + O\left(u^2, v^2\right) \\ & \approx I(x, y) + I_{x}(x, y) u + I_{y}(x, y) v \end{aligned} $$

其中,$I_{x}$$I_{y}$ 是图像 $I(x, y)$ 的偏导数,那么式 $\ref{eq:e_u_v}$ 可以简化为:

$$ \begin{aligned} E\left(u, v\right) & \approx \sum_{x, y} w(x, y)\left[I_{x}(x, y) u + I_{y}(x, y) v\right]^{2} \\ &=\left[\begin{array}{ll} u & v \end{array}\right] M(x, y)\left[\begin{array}{c} u \\ v \end{array}\right] \end{aligned} $$

其中,

$$ M=\sum w(x, y)\left[\begin{array}{cc} I_{x}^{2} & I_{x} I_{y} \\ I_{x} I_{y} & I_{y}^{2} \end{array}\right] $$

通过求解 $M$ 的特征向量,我们可以获得 $E(u, v)$ 最大和最小增量的方向,对应的特征值则为实际的增量值。Harris 角点检测方法对每一个窗口定义了一个 $R$ 值:

$$ R = \operatorname{det} M - k (\operatorname{trace} M)^{2} $$

其中,$\operatorname{det} M = \lambda_1 \lambda_2$ 是矩阵 $M$ 的行列式,$\operatorname{trace} M = \lambda_1 + \lambda_2$ 是矩阵 $M$ 的迹,$\lambda_1$$\lambda_2$ 为矩阵 $M$ 特征值,$k$ 为经验常数,通常取值为 $[0.04, 0.06]$。特征值决定了当前区域是一个角、边还是平坦区域。

  1. $\lvert R \rvert$ 比较小时,$\lambda_1$$\lambda_2$ 均比较小,则区域为平坦区域。
  2. $R < 0$ 时,$\lambda_1 \gg \lambda_2$$\lambda_1 \ll \lambda_2$,则区域为边。
  3. $R$ 较大时,$\lambda_1$$\lambda_2$ 都很大且 $\lambda_1 \sim \lambda_2$,则区域为角。

Harris 角点检测方法具备如下性质:

  1. $k$ 影响被检测角点数量:增大 $k$ 将减小 $R$,从而减少被检测角点的数量,反之亦然。
  2. 对亮度和对比度的变化不敏感:Harris 角点检测对图像进行微分运算,微分运算对图像密度的拉升或收缩和对亮度的抬高或下降不敏感。
  3. 具有旋转不变性:Harris 角点检测算子使用的是角点附近区域的灰度二阶矩矩阵。而二阶矩矩阵可以表示成一个椭圆,椭圆的长短轴正是二阶矩矩阵特征值平方根的倒数。当特征椭圆转动时,特征值并不发生变化,所以判断角点的 $R$ 值也不发生变化。
  4. 不具有尺度不变性:如下图所示,当左图被缩小时,在检测窗口尺寸不变的前提下,在窗口内所包含图像的内容是完全不同的。左侧的图像可能被检测为边缘或曲线,而右侧的图像则可能被检测为一个角点。

利用 Harris 方法检测角点的效果如下图所示(代码详见这里):

尺度空间极值检测

为了使检测到的特征点具备尺度不变性,使能够在不同尺度检测到尽可能完整的特征点或关键点,则需要借助尺度空间理论来描述图像的多尺度特征。相关研究证明高斯卷积核是实现尺度变换的唯一线性核。因此可用图像的高斯金字塔表示尺度空间,而且尺度规范化的 LoG 算子具有尺度不变性,在具体实现中,可用高斯差分( DoG)算子近似 LoG 算子,在构建的尺度空间中检测稳定的特征点。

在图像处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获取多尺度下的空间表示序列,对这些空间序列提取某些特征描述子,抽象成特征向量,实现图像在不同尺度或不同分辨率的特征提取。尺度空间中各尺度图像的模糊程度逐渐变大,模拟人在由近到远时目标在人眼视网膜上的成像过程。而且尺度空间需满足一定的不变性,包括图像灰度不变性、对比度不变性、平移不变性、尺度不变性以及旋转不变性等。在某些情况下甚至要求尺度空间算子具备仿射不变性。

图像的尺度空间 $L(x, y, \sigma)$ 可以定义为图像 $I(x, y)$ 与可变尺度的高斯函数 $G(x, y, \sigma)$ 的卷积:

$$ \begin{aligned} L(x, y, \sigma) &= G(x, y, \sigma) * I(x, y) \\ G(x, y, \sigma) &= \frac{1}{2 \pi \sigma^{2}} e^{-\frac{x^{2}+y^{2}}{2 \sigma^{2}}} \end{aligned} $$

其中,$\sigma$ 是尺度变化因子,大小决定图像的平滑程度,值越大图像越模糊。大尺度对应图像的概貌特征,小尺度对应图像的细节特征。一般根据 $3 \sigma$ 原则,高斯核矩阵的大小设为 $(6 \sigma+1) \times(6 \sigma+1)$

尺度归一化的高斯拉普拉斯函数 $\sigma^{2} \nabla^{2} G$ 可以提取稳定的特征,高斯差分函数(Difference-of-Gaussian,DoG) 4 与尺度归一化的高斯拉普拉斯函数近似:

$$ \begin{aligned} LoG &= \sigma^{2} \nabla^{2} G \\ DoG &= G(x, y, \sigma_2) - G(x, y, \sigma_1) \end{aligned} $$

利用差分近似替代微分,有:

$$ \sigma \nabla^{2} G=\frac{\partial G}{\partial \sigma} \approx \frac{G(x, y, k \sigma)-G(x, y, \sigma)}{k \sigma-\sigma} $$

因此有:

$$ G(x, y, k \sigma)-G(x, y, \sigma) \approx(k-1) \sigma^{2} \nabla^{2} G $$

其中,$k - 1$ 是个常数,不影响极值点的检测,DoG 和 LoG 的对比图如下:

在使用高斯金字塔构建尺度空间时,主要包括两部分:对图像做下采样,以及对图像做不同尺度的高斯模糊。对图像做降采样得到不同尺度的图像,也就是不同的组(Octave),后面的 Octave(高一层的金字塔)为上一个 Octave(低一层的金字塔)下采样得到,图像宽高分别为上一个 Octave 的 1/2 。每组(Octave)又分为若干层(Interval),通过对图像做不同尺度的高斯模糊得到,如下图所示:

在由图像金字塔表示的尺度空间中,Octave 由原始图像的大小和塔顶图像的大小决定:

$$ Octave = \log_2 \left(\min\left(w_b, h_b\right)\right) - \log_2 \left(\min\left(w_t, h_t\right)\right) $$

其中,$w_b$$h_b$ 分别为原始图像的宽和高,$w_t$$h_t$ 分别为金字塔顶部图像的宽和高。

尺度参数 $\sigma$ 的取值与金字塔的组数和层数相关,设第一组第一层的尺度参数取值为 $\sigma \left(1, 1\right)$,一般取值为 $1.6$,则第 $m$ 组第 $n$ 层的取值为:

$$ \sigma(m, n)=\sigma\left(1, 1\right) \cdot 2^{m-1} \cdot k^{n-1}, \quad k=2^{1/S} $$

其中,$S$ 为金字塔中每组的有效层数,$k=2^{1/S}$ 是变化尺度因子。在检测极值点前对原始图像的高斯平滑会导致图像高频信息的丢失,所以在建立尺度空间之前,先利用双线性插值将图像扩大为原来的两倍,以保留原始图像信息,增加特征点数量。

为了寻找 DoG 尺度空间的极值点,每一个采样点要和其所有邻域像素相比较,如下图所示,中间检测点与其同尺度的 8 个邻域像素点以及上下相邻两层对应的 9×2 个像素点一共 26 个点作比较,以确保在图像空间和尺度空间都能检测到极值点。一个像素点如果在 DoG 尺度空间本层及上下两层的 26 邻域中取得最大或最小值时,就可以认为该点是图像在该尺度下的一个特征点。

在极值比较的过程中,每一组差分图像的首末两层是无法比较的,为了在每组中检测 $S$ 个尺度的极值点,则 DoG 金字塔每组须有 $S+2$ 层图像,高斯金字塔每组须有 $S+3$ 层图像。另外,在下采样时,高斯金字塔中后一组(Octive)的底层图像是由前一组图像的倒数第 3 张图像($S+1$ 层)隔点采样得到。这样也保证了尺度变化的连续性,如下图所示:

关键点定位

在 DoG 尺度空间检测到的极值点是离散的,通过拟合三元二次函数可以精确定位关键点的位置和尺度,达到亚像素精度。同时去除低对比度的检测点和不稳定的边缘点(因为 DoG 算子会产生较强的边缘响应),以增强匹配稳定性,提高抗噪声能力。

离散空间的极值点并不是真正的极值点,如下图所示一维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation)。

假设在尺度为 $\sigma$ 的尺度图像 $D(x, y, \sigma)$ 检测到一个局部极值点,空间位置为 $(x, y, \sigma)$。根据上图直观可知,它只是离散情况下的极值点,而连续情况下,极值点可能坐落在 $(x, y, \sigma)$ 附近,设连续情况的正真极值点偏离 $(x, y, \sigma)$ 的坐标为 $(\Delta x, \Delta y, \Delta \sigma)$。则对 $D(x + \Delta x, y + \Delta y, \sigma + \Delta \sigma)$$D(x, y, \sigma)$ 处进行泰勒展开(保留二阶),有:

$$ \begin{split} & \ D(x+\Delta x, y+\Delta y, \sigma+\Delta \sigma) \\ \approx & \ D(x, y, \sigma)+\left[\begin{array}{lll} \frac{\partial D}{\partial x} & \frac{\partial D}{\partial y} & \frac{\partial D}{\partial \sigma} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right] \\ & +\frac{1}{2}\left[\begin{array}{lll} \Delta x & \Delta y & \Delta \sigma \end{array}\right]\left[\begin{array}{lll} \frac{\partial D^{2}}{\partial x^{2}} & \frac{\partial D^{2}}{\partial x \partial y} & \frac{\partial^{2} D}{\partial x \partial \sigma} \\ \frac{\partial D^{2}}{\partial y \partial x} & \frac{\partial D^{2}}{\partial y^{2}} & \frac{\partial D^{2}}{\partial y \partial \sigma} \\ \frac{\partial D^{2}}{\partial \sigma \partial x} & \frac{\partial D^{2}}{\partial \sigma \partial y} & \frac{\partial D^{2}}{\partial \sigma^{2}} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right] \end{split} $$

将上式写成矢量形式如下:

$$ D(X+\Delta X)=D(X)+\frac{\partial D^{\top}(X)}{\partial X} \Delta X+\frac{1}{2} \Delta X^{\top} \frac{\partial^{2} D(X)}{\partial X^{2}} \Delta X $$

上式对 $\Delta X$ 求导,并令其等于零,可以得到极值点的偏移量:

$$ \Delta X=-\frac{\partial^{2} D(X)}{\partial X^{2}}^{-1} \frac{\partial D^{\top}(X)}{\partial X} $$

通过多次迭代(Lowe SIFT 算法里最多迭代 5 次),得到最终候选点的精确位置与尺度 $\hat{X}$。当超出所设定的迭代次数或者超出图像边界的范围时应删除该点,如果 $\lvert D(\hat{X}) \rvert$ 小于某个阈值则将该极值点也应该删除。

高斯差分函数有较强的边缘响应,对于比较像边缘的点应该去除掉。这样的点的特征为在某个方向有较大主曲率,而在垂直的方向主曲率很小。主曲率可通过一个 $2 \times 2$ 的 Hessian 矩阵求出:

$$ H=\left[\begin{array}{ll} D_{x x} & D_{x y} \\ D_{x y} & D_{y y} \end{array}\right] $$

$D$ 的主曲率和 $H$ 的特征值成正比,令 $\alpha$ 为较大的特征值,$\beta$ 为较小的特征值,$\alpha = \gamma\beta$,则有:

$$ \begin{aligned} \operatorname{trace}(H) &= D_{x x}+D_{y y}=\alpha+\beta \\ \operatorname{det}(H) &= D_{x x} D_{y y}-\left(D_{x y}\right)^{2}=\alpha \beta \\ \frac{\operatorname{trace}(H)^{2}}{\operatorname{det}(H)} &= \frac{(\alpha+\beta)^{2}}{\alpha \beta}=\frac{(\gamma+1)^{2}}{\gamma} \end{aligned} $$

上式的结果只与两个特征值的比例有关,而与具体特征值无关。当两个特征值相等时,$\dfrac{(\gamma+1)^{2}}{\gamma}$ 的值最小,随着 $\gamma$ 的增加,$\dfrac{(\gamma+1)^{2}}{\gamma}$ 的值也增加。设定一个阈值 $\gamma_t$(Lowe SIFT 算法里最 $\gamma_t = 10$),若:

$$ \frac{\operatorname{trace}(H)^{2}}{\operatorname{det}(H)}<\frac{\left(\gamma_{t}+1\right)^{2}}{\gamma_{t}} $$

则认为该关键点不是边缘,否则予以剔除。

关键点方向指定

为了使特征描述符具有旋转不变性,需要利用关键点邻域像素的梯度方向分布特性为每个关键点指定方向参数。对于在 DoG 金字塔中检测出的关键点,在其邻近高斯金字塔图像的 3𝜎 邻域窗口内计算其梯度幅值和方向,公式如下:

$$ \begin{aligned} &m(x, y)=\sqrt{(L(x+1, y)-L(x-1, y))^{2}+(L(x, y+1)-L(x, y-1))^{2}} \\ &\theta(x, y)=\arctan ((L(x, y+1)-L(x, y-1)) /(L(x+1, y)-L(x-1, y))) \end{aligned} $$

其中,$L$ 为关键点所在尺度空间的灰度值,$m(x, y)$ 为梯度幅值,$\theta(x, y)$ 为梯度方向。对于模值 $m(x, y)$ 按照 $\theta = 1.5 \theta_{oct}$ 邻域窗口为 $3 \theta$ 的高斯分布加权。在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向,梯度直方图将梯度方向 $\left(0,360^{\circ}\right)$ 分为 36 个柱(bins),如下图所示(为简化,图中只画了八个方向的直方图),直方图的峰值所在的方向代表了该关键点的主方向。

梯度方向直方图的峰值代表了该特征点处邻域梯度的主方向,为了增强鲁棒性,保留峰值大于主方向峰值 80% 的方向作为该关键点的辅方向。因此,在相同位置和尺度,将会有多个关键点被创建但方向不同,这可以提高特征点匹配的稳定性。

关键点特征描述符

在经过上述流程后,检测到的每个关键点有三个信息:位置、尺度以及方向,接下来就是为每个关键点建立一个描述符,用一组向量将这个关键点描述出来。这个特征描述符不但包括关键点,还包括其邻域像素的贡献,而且需具备较高的独特性和稳定性,以提高特征点匹配的准确率。SIFT 特征描述符是关键点邻域梯度经过高斯加权后统计结果的一种表示。通过对关键点周围图像区域分块,计算块内的梯度直方图,得到表示局部特征点信息的特征向量。例如在尺度空间 $4 \times 4$ 的窗口内统计 8 个方向的梯度直方图,生成一个 $4 \times 4 \times 8 = 128$ 维的表示向量。

特征描述符与特征点所在的尺度有关,因此,对梯度的求取应在特征点对应的高斯图像上进行。将关键点附近的邻域划分为 $d \times d$ 个子区域 $(d = 4)$,每个子区域做为一个种子点,每个种子点有 8 个方向。每个子区域的大小与关键点方向分配时相同,即每个区域边长为 $3 \theta_{oct}$。考虑到实际计算时需要进行三线性插值,采样窗口区域半边长设为 $\dfrac{3 \theta_{oct} (d + 1)}{2}$,又考虑到旋转因素(坐标轴旋转至关键点主方向),这个值需要乘以 $\sqrt{2}$,最后所需的图像区域半径为:

$$ \text{radius} = \dfrac{3 \sigma_{oct} \times \sqrt{2} \times(d+1)}{2} $$

将坐标轴旋转至关键点主方向,以确保旋转不变性。如下图所示:

旋转后采样点的新坐标为:

$$ \left[\begin{array}{l} x^{\prime} \\ y^{\prime} \end{array}\right]=\left[\begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array}\right]\left[\begin{array}{l} x \\ y \end{array}\right] \quad(x, y \in[\text {-radius, radius }]) $$

在图像半径区域内对每个像素点求其梯度幅值和方向,并对每个梯度幅值乘以高斯权重:

$$ w=m(u+x, b+v) \times e^{-\frac{x^{\prime 2}+y^{\prime 2}}{2 \sigma_{w}^{2}}} $$

其中,$u$$v$ 表示关键点在高斯金字塔图中的位置坐标,$x$$y$ 为旋转坐标轴至关键点主方向之前相对关键点的偏移量,$x^{\prime}$$y^{\prime}$ 为旋转坐标轴至关键点主方向之后相对关键点的偏移量。

将旋转后的采样点坐标分配到对应的子区域,计算影响子区域的采样点的梯度和方向,分配到 8 个方向上。旋转后的采样点 $(x^{\prime}, y^{\prime})$ 落在子区域的下标为:

$$ \left[\begin{array}{l} x_{d} \\ y_{d} \end{array}\right]=\frac{1}{3 \sigma_{o c t}}\left[\begin{array}{c} x^{\prime} \\ y^{\prime} \end{array}\right]+\frac{d}{2}, \quad x_{d}, y_{d} \in[0, d] $$

将采样点在子区域的下标进行三线性插值,根据三维坐标计算与周围子区域的距离,按距离远近计算权重,最终累加在相应子区域的相关方向上的权值为:

$$ w^{\prime} = w \cdot\left[d_{r}^{i} \cdot\left(1-d_{r}\right)^{1-i}\right] \cdot\left[d_{c}^{j} \cdot\left(1-d_{c}^{1-j}\right)\right] \cdot\left[d_{o}^{k} \cdot\left(1-d_{o}\right)^{1-k}\right] $$

其中,$i, j, k$ 取值为 0 或 1,$d_r, 1- d_r$ 是对相邻两行的贡献因子,$d_c, 1- d_c$ 是对相邻两列的贡献因子,$d_o, 1- d_o$ 是对相邻两个方向的贡献因子。插值计算每个种子点八个方向的梯度,最终结果如下图所示:

得到 128 维特征向量后,为了去除光照变化的影响,需要对向量进行归一化处理。非线性光照变化仍可能导致梯度幅值的较大变化,但对梯度方向影响较小。因此对于超过阈值 0.2 的梯度幅值设为 0.2 ,然后再进行一次归一化。最后将特征向量按照对应高斯金字塔的尺度大小排序。至此,SIFT 特征描述符形成。

利用 SIFT 方法检测关键点的效果如下图所示(代码详见这里):

SIFT 方法的优点如下:

  1. 局部:SIFT 特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性。
  2. 独特:信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配。
  3. 大量:即使少数的几个物体也可以产生大量的 SIFT 特征向量。
  4. 高效:经优化的 SIFT 匹配算法甚至可以达到实时的要求。
  5. 可扩展:可以很方便的与其他形式的特征向量进行联合。

SIFT 方法的缺点如下:

  1. 默认算法的实时性不高。
  2. 部分情况下特征点较少。
  3. 对边缘光滑的目标无法准确提取特征点。

对于 SIFT 的改进可以参考 SURF 5 和 CSIFT 6

本小节参考:

  1. https://docs.opencv.org/5.x/dc/d0d/tutorial_py_features_harris.html
  2. https://docs.opencv.org/5.x/da/df5/tutorial_py_sift_intro.html
  3. https://lsxiang.github.io/Journey2SLAM/computer_vision/Harris/
  4. https://lsxiang.github.io/Journey2SLAM/computer_vision/SIFT/

特征编码

BoF

BoF(Bag of Features, Bag of Visual Words)7 8 借鉴了文本中的 BoW(Bag of Words)模型的思路。从图像抽象出很多具有代表性的「关键词」,形成一个字典,再统计每张图片中出现的「关键词」频率,得到图片的特征向量。BoF 算法的流程如下:

  1. 局部特征提取:利用上文中的 SIFT 等类似方法提取图片的局部特征,每个图片提取的特征数量不同,但每个特征的维度是相同的(例如:128 维)。
  2. 构建视觉词典:利用 K-means 等算法将所有图片的所有特征向量进行聚类,得到 $K$ 个聚类中心,即 $K$ 个视觉词汇(Visual Word)。由于特征数量可能非常大,使用 K-means 算法聚类会相当耗时。
  3. 生成 BoF 特征:对于一个图像中的每一个特征,都可以在视觉词典中找到一个最相似的视觉词汇,因此可以对该图像统计得到一个 $K$ 维的直方图,每个值表示图像中局部特征在视觉词典中相似视觉词汇的频率。针对该步骤可以利用 TF-IDF 的思想获取加权的 BoF 特征结果。

BoF 算法的一些缺点也比较明显:

  1. 在使用 K-means 进行聚类时,$K$ 和初始聚类中心的选取对结果敏感。字典过大,词汇缺乏一般性,对噪声敏感;字典过小,词汇区分性较差,无法充分表示图片。对于海量数据计算所需的时间和空间复杂度都比较高。
  2. 将图像表示成一个无序的特征,丢失了图片中的空间信息,表示上存在一定局限。

VLAD

VLAD(Vector of Local Aggregated Descriptors)9 方法同 BoF 类似,但在生成特征时采用如下公式:

$$ V(j, k)=\sum_{i=1}^{N} \operatorname{sign}_{k}\left(x_{i}\right)\left(x_{i}(j)-c_{k}(j)\right), \quad k \in K, j \in D $$

其中,$K$ 为词典大小,$N$ 为图片的局部特征数量,$D$ 为每个局部特征的维度,$x_i$ 表示第 $i$ 个局部特征,$c_k$ 表示第 $k$ 个聚类中心,$\operatorname{sign}_k$ 是一个符号函数,如果 $x_i$ 不属于聚类中心 $c_k$ 则为 $0$,反之则为 $1$

从上式中可以看出 VLAD 累加了每个聚类的所有特征残差,通过 $x_i - c_k$ 将图像本身的局部特征分布差异转换为聚类中心的分布差异,通过归一化和降维等手段得到最终的全局特征。

FV

FV(Fisher Vector)10 本质上是用似然函数的梯度向量表示一幅图像。梯度向量的物理意义就是描述能够使模型更好地适应数据的参数变化方向,也就是数据拟合中参数调优的过程。在 FV 中我们采用高斯混合模型(Gaussian Mixture Model,GMM)。

高斯混合模型是由多个高斯模型线性叠加而成,公式如下:

$$ p(x)=\sum_{k=1}^{K} \pi_{k} N\left(x \mid \mu_{k}, \Sigma_{k}\right) $$

其中,$p(x)$ 表示数据 $x$ 出现的概率,$K$ 表示高斯模型个数,$\pi_k$ 表示第 $k$ 个高斯模型的权重,$\mu_k$ 表示第 $k$ 个高斯分布的均值,$\Sigma_k$ 表示第 $k$ 个高斯分布的方差。理论上,只要 $K$ 足够大,GMM 可以逼近任意一种概率分布。

GMM 的目标是求解参数 $\pi_k, \mu_k, \Sigma_k$ 使得它确定的概率分布生成这些给定数据的概率最大,即 $\Pi_{i=1}^{N} p\left(x_{i}\right)$ 最大。假设各个数据点之间满足独立同分布,可以将其转换成对数似然函数:

$$ \sum_{i=1}^{N} \log \left(p \left(x_i\right)\right) $$

假设 GMM 模型包含 $K$ 个高斯模型,则模型的参数集合为 $\lambda = \left\{w_i, \mu_k, \Sigma_k, k = 1, \cdots, K\right\}$,假设共有 $T$ 个特征向量,则似然函数可以表示为:

$$ \mathcal{L} (x \mid \lambda)=\sum_{t=1}^{T} \log \left(p\left(x_{t} \mid \lambda\right)\right) $$

其中的高斯分布是多个基高斯分布的混合:

$$ \label{eq:fv_gmm} p\left(x_{t} \mid \lambda\right)=\sum_{k=1}^{K} w_{k} * p_{k}\left(x_{t} \mid \lambda\right) $$

每个基高斯分布又可以表示为:

$$ p_{k}\left(x_{t} \mid \lambda\right) = \dfrac{1}{\left(2 \pi\right)^{\frac{D}{2}} \lvert\Sigma_k\rvert^{\frac{1}{2}}} e^{-\frac{1}{2} \left(x - \mu_k\right)^{\prime} \Sigma_k^{-1} \left(x - \mu_k\right)} $$

由贝叶斯公式可知,描述符 $x_t$ 数据第 $i$ 个高斯模型的概率为:

$$ \gamma_{t}(i)=\frac{w_{i} u_{i}\left(x_{t}\right)}{\sum_{k=1}^{K} w_{k} u_{k}\left(x_{t}\right)} $$

则公式 $\ref{eq:fv_gmm}$ 的梯度分量为:

$$ \begin{aligned} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial w_{i}} &=\sum_{t=1}^{T}\left[\frac{\gamma_{t}(i)}{w_{i}}-\frac{\gamma_{t}(1)}{w_{1}}\right] \text { for } i \geq 2, \\ \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \mu_{i}^{d}} &=\sum_{t=1}^{T} \gamma_{t}(i)\left[\frac{x_{t}^{d}-\mu_{i}^{d}}{\left(\sigma_{i}^{d}\right)^{2}}\right], \\ \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \sigma_{i}^{d}} &=\sum_{t=1}^{T} \gamma_{t}(i)\left[\frac{\left(x_{t}^{d}-\mu_{i}^{d}\right)^{2}}{\left(\sigma_{i}^{d}\right)^{3}}-\frac{1}{\sigma_{i}^{d}}\right] . \end{aligned} $$

由于概率空间和欧氏空间的归一化方式不同,在此引入 Fisher Matrix 进行归一化:

$$ \begin{aligned} f_{w_{i}} &=T\left(\frac{1}{w_{i}}+\frac{1}{w_{1}}\right) \\ f_{\mu_{i}^{d}} &=\frac{T w_{i}}{\left(\sigma_{i}^{d}\right)^{2}}, \\ f_{\sigma_{i}^{d}} &=\frac{2 T w_{i}}{\left(\sigma_{i}^{d}\right)^{2}} . \end{aligned} $$

归一化后即为 Fisher 向量:

$$ \begin{aligned} \mathscr{G}_{\alpha_{k}}^{X} &= f_{w_i}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial w_{i}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T}\left(\gamma_{t}(k)-w_{k}\right), \\ \mathscr{G}_{\mu_{k}}^{X} &= f_{\mu_i^d}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \mu_{i}^{d}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T} \gamma_{t}(k)\left(\frac{x_{t}-\mu_{k}}{\sigma_{k}}\right), \\ \mathscr{G}_{\sigma_{k}}^{X} &= f_{\sigma_i^d}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \sigma_{i}^{d}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T} \gamma_{t}(k) \frac{1}{\sqrt{2}}\left[\frac{\left(x_{t}-\mu_{k}\right)^{2}}{\sigma_{k}^{2}}-1\right] \end{aligned} $$

其中 $\mathscr{G}_{\alpha_{k}}^{X}$ 同 BoF 有类似效果,$\mathscr{G}_{\mu_{k}}^{X}$ 同 VLAD 有类似效果。

图像检索

倒排索引

倒排索引是一种提高存储和检索效率的算法,它常被用于大/中等规模的编码本中,结构如下图所示:

倒排索引是一个一维结构,其中每一个条目对应编码本中的一个视觉词汇。每一个视觉词汇都包含一个倒排表,每个倒排表中的索引被称为索引特征或者记录。倒排索引很好地发挥了大规模编码本词汇直方图稀疏性的特点。

ANN

基于近似最近邻搜索(Approximate Nearest Neighbor Search,ANN)的方法,请参考之前的博客

基于深度学习的图像检索

基于深度学的图像检索流程如下图所示:

  1. 网络前馈方式:将图片输入 DCNN 的方式有两种:单路和多路。单路将整个图片作为输入,多路依赖区域提取,例如空间金字塔模型(Spatial Pyramid Models)和区域候选网络(Region Proposal Networks,RPN)。
  2. 深度特征提取:基于整个图像或部分区块的输入,网络的激活值可以用作原始的特征。全连接层提供了一个全局的视野,将一整个图片表示为单个向量。
  3. 特征嵌入和集成:基于图片级别或区块级别的描述符,构造全局或局部特征时包含两个重要步骤,通常为 PCA 和白化。特征嵌入将独立的局部特征映射到一个高维向量,特征集成则将多个映射的向量合并成一个单一向量。
  4. 特征匹配:特征匹配用来衡量图片之间特征的相似度。全局匹配可以通过欧式、汉明或其他距离度量进行高效计算。对于局部特征,可以通过 RANSAC 11 和近期的一些改进方法对局部特征进行相似度汇总来评估相似性。

关于卷积神经网络的细节介绍,请参考之前的博客

网络前馈方式

单路前馈方法

单路前馈方法是将整个图片直接输入模型来提取特征。由于仅将图片输入模型一次,该方法效率较高。对于这类方法,全连接层和最后一个卷积层可以作为特征提取器。

多路前馈方法

相比于单路前馈方法,多路前馈方法由于需要将生成的多个图像块输入到模型中,因此相对耗时。这类方法通常包含两个步骤:图像块识别和图像块描述。使用不同方法可以获得不同尺度的图像块,如下图所示:

其中,(a) 为固定窗口大小划分的区块,(b) 为空间金字塔模型(Spatial Pyramid Model,SPM)划分的区块,(c) 为稠密采样的区块,(d) 为通过区域候选网络(Region Proposal Network,RPN)获得的候选区块。

深度特征选择

特征选择决定了提取特征的表达范围,例如:从全连接层可以获得全局级别特征,从卷积层可以获得区块级别特征

从全连接层提取

将全连接层作为全局特征提取器,通过 PCA 降维和标准化后可以用于度量图像的相似性。但由于这一层是全连接的,每个神经元都会产生图像级别的描述符,这会导致两个明显的缺陷:包括无关信息和缺少局部几何不变性。

针对第一个缺陷,可以通过多路前馈方法来提取区块级别特征。针对第二个缺陷,缺乏几何不变性会影响图像变换(如:裁剪、遮挡)时的鲁棒性,通过使用中间卷积层可以来解决。

从卷积层提取

从卷积层(通常是最后一层)提取的特征保留了更多的结构细节,这对实例检索尤为有利。卷积层的神经元仅同局部区域相连接,这样较小的视野确保所生成的特征包含更多局部结构信息,同时对于图像变换更为鲁棒。

加和/平均和最大池化是用于生成全局特征的两个简单的集成方法,一些其他的集成方法,例如:MAC 12,R-MAC 13,GeM polling 14,SPoC 15,CroW 16 和 CAM+CroW 17 如下图所示:

特征融合策略

层级别融合

通过层级别融合,可以在深度网络中融合多个全连接层。全连接层的特征保留了全局高层级语义信息,卷积层的特征保留了局部中低层级的信息。因此,全局特征和局部特征在测量语义相似度时相辅相成,可以在一定程度上保证检索的效果。

模型级别融合

融合不同模型的特征也是可行的,这种融合方式关注于模型之间的互补性质,其可以分为:intra-model 和 inter-model 两类。intra-model 融合方式建议使用多个具有相似性或者结构上高度兼容的模型,而 inter-model 融合方式则使用结构上具有很大不同的模型。

inter-model 和 intra-model 融合同模型选择有关。融合候选模型的所有特征,然后根据拼接的特征学习得到一个度量,这种方式称为 early fusion 策略。或者对每个模型的特征学习各自的最优度量,然后组合这些度量用于最终的检索排名,这种方式称为 late fusion 策略。

特征嵌入和集成

特征嵌入和集成的主要目的是进一步提高从 DCNN 中提取的特征的区分能力,来生成用于检索特定实例的最终全局或局部特征。

匹配全局特征

全局特征可以从全连接层中提取,然后进行降维和标准化,通常情况下没有进一步的聚合过程。卷积特征也可以被集成到全局特征中,简单的方式是通过加和/平均或最大池化。卷积特征可以作为局部区域的描述符,因此可以利用基于 SIFT 的图像检索中提到的 BoF,VLAD,FV 等模型对其进行编码,然后再将他们聚合为一个全局描述符。

匹配局部特征

尽管全局特征匹配对于特征提取和相似度计算都具有很高的效率,但全局特征与空间验证和对应估计不兼容,这些是实例级别检索任务的重要过程。在匹配过程中,全局特征只匹配一次,而局部特征匹配通过汇总所有单个局部特征的相似性来评估(即多对多匹配)。

注意力机制

注意力机制可以看作是一种特征聚合,其核心思想是突出最相关的特征部分,通过计算注意力映射来实现。获取注意力映射的方法可以分为两类:非参数和参数,如下图所示,主要区别在于注意力映射中的重要性权重是否可学习。

非参数加权是一种突出特征重要性的直接方法,相应的注意力映射可以通过通道或空间池化获得,如 (a) 和 (b) 所示。参数注意力映射可以通过深度网络学习,其输入可以是图像块或特征映射,这些方法通常用于有监督的度量学习,如 (c) 和 (d) 所示。

哈希嵌入

深度网络提取的实值特征通常是高维的,不太适合检索。因此,将深层特征转换为更紧凑的编码具有重要意义。由于其计算和存储效率,哈希算法已被广泛用于全局和局部描述符。

哈希函数可以作为一个层插入到深度网络中,以便可以通过有监督或无监督的方式同时训练和优化哈希码和深度网络。哈希函数训练时,相似图像的哈希码嵌入会尽可能的接近,不相似图像的哈希码嵌入会尽可能的分离。

模型微调

微调方法已被广泛研究以学习更好的检索特征。在基于图像分类的数据集上预训练的 DCNN 对类间可变性非常稳健,随后将成对的监督信息引入排名损失中,通过对检索表示进行正则化来微调网络。具有清晰且定义良好的真实标签的标准数据集对于微调深度模型以执行准确的实例级检索是必不可少的,否则就需要开发无监督的微调方法。在网络微调之后,可以将特征作为全局或局部来进行检索。

有监督微调

下图展示了不同类型的有监督微调方法:

下图展示了不同类型的有监督微调损失函数:

无监督微调

由于成本等问题可能导致监督信息不足,从而监督网络微调变得不可行。因此,用于图像检索的无监督微调方法是非常必要的。对于无监督微调,两个方向是通过流形学习和聚类技术挖掘特征之间的相关性。具体细节不再展开讨论,详细内容请参见原文。

开放资源


  1. Zheng, L., Yang, Y., & Tian, Q. (2017). SIFT meets CNN: A decade survey of instance retrieval. IEEE transactions on pattern analysis and machine intelligence, 40(5), 1224-1244. ↩︎

  2. Chen W., Liu Y., Wang W., Bakker E., Georigiou T., Fieguth P., Liu L., & Lew M. (2022). Deep Learning for Instance Retrieval: A Survey. arXiv:2101.11282 ↩︎

  3. https://zh.wikipedia.org/wiki/基于内容的图像检索 ↩︎

  4. Lindeberg, T. (1994). Scale-space theory: A basic tool for analyzing structures at different scales. Journal of applied statistics, 21(1-2), 225-270. ↩︎

  5. Bay, H., Tuytelaars, T., & Van Gool, L. (2006, May). Surf: Speeded up robust features. In European conference on computer vision (pp. 404-417). Springer, Berlin, Heidelberg. ↩︎

  6. Abdel-Hakim, A. E., & Farag, A. A. (2006, June). CSIFT: A SIFT descriptor with color invariant characteristics. In 2006 IEEE computer society conference on computer vision and pattern recognition (CVPR'06) (Vol. 2, pp. 1978-1983). Ieee. ↩︎

  7. Sivic, J., & Zisserman, A. (2003, October). Video Google: A text retrieval approach to object matching in videos. In Computer Vision, IEEE International Conference on (Vol. 3, pp. 1470-1470). IEEE Computer Society. ↩︎

  8. Radenović, F., Jégou, H., & Chum, O. (2015, June). Multiple measurements and joint dimensionality reduction for large scale image search with short vectors. In Proceedings of the 5th ACM on International Conference on Multimedia Retrieval (pp. 587-590). ↩︎

  9. Jégou, H., Douze, M., Schmid, C., & Pérez, P. (2010, June). Aggregating local descriptors into a compact image representation. In 2010 IEEE computer society conference on computer vision and pattern recognition (pp. 3304-3311). IEEE. ↩︎

  10. Perronnin, F., Sánchez, J., & Mensink, T. (2010, September). Improving the fisher kernel for large-scale image classification. In European conference on computer vision (pp. 143-156). Springer, Berlin, Heidelberg. ↩︎

  11. Fischler, M. A., & Bolles, R. C. (1981). Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography. Communications of the ACM, 24(6), 381-395. ↩︎

  12. Razavian, A. S., Sullivan, J., Carlsson, S., & Maki, A. (2016). Visual instance retrieval with deep convolutional networks. ITE Transactions on Media Technology and Applications, 4(3), 251-258. ↩︎

  13. Tolias, G., Sicre, R., & Jégou, H. (2015). Particular object retrieval with integral max-pooling of CNN activations. arXiv preprint arXiv:1511.05879↩︎

  14. Radenović, F., Tolias, G., & Chum, O. (2018). Fine-tuning CNN image retrieval with no human annotation. IEEE transactions on pattern analysis and machine intelligence, 41(7), 1655-1668. ↩︎

  15. Babenko, A., & Lempitsky, V. (2015). Aggregating local deep features for image retrieval. In Proceedings of the IEEE international conference on computer vision (pp. 1269-1277). ↩︎

  16. Kalantidis, Y., Mellina, C., & Osindero, S. (2016, October). Cross-dimensional weighting for aggregated deep convolutional features. In European conference on computer vision (pp. 685-701). Springer, Cham. ↩︎

  17. Jimenez, A., Alvarez, J. M., & Giro-i-Nieto, X. (2017). Class-weighted convolutional features for visual instance search. arXiv preprint arXiv:1707.02581↩︎

你所应该知道的 A/B 测试 (A/B Test You Should Know)

2021-10-17 08:00:00

什么是 A/B 测试

A/B 测试是一种随机测试,将两个不同的东西(即 A 和 B)进行假设比较。A/B 测试可以用来测试某一个变量两个不同版本的差异,一般是让 A 和 B 只有该变量不同,再测试目标对于 A 和 B 的反应差异,再判断 A 和 B 的方式何者较佳 1。A/B 测试的前身为双盲测试 2,在双盲测试中人员会被随机分为两组,受试验的对象及研究人员并不知道哪些对象属于对照组,哪些属于实验组,通过一段时间的实验后对比两组人员的结果是否有明显差异。在各种科学研究领域中,从医学、食品、心理到社会科学及法证都有使用双盲方法进行实验。

一个简单的 A/B 测试流程如下:

  1. 对目标人群进行随机划分,以进行有效的独立随机实验。
  2. 对不同分组应用不同的策略。
  3. 在确保实验有效的前提下,对不同分组的结果进行分析,以确定不同策略的优劣。

A/B 测试的主要目的是帮助我们更加科学的判断不同策略的优劣性,避免拍脑门的决策,不给杠精们互相 BATTLE 的机会。同时我们也需要认识到 A/B 测试只是一个工具,它能够帮助我们对产品和策略进行不断优化,但对产品和策略的创新更多还是需要洞察力。它可以让我们在已达到的山上越来越高,却不能用它来发现一座新的山脉。一句话:A/B 测试不是万能的,但离开 A/B 测试是万万不能的。

A/B 测试的科学性

流量分配

进行 A/B 测试的第一个问题就是如何划分用户,如果采用上面简单五五开的方式我们一次只能做一个实验,当我们需要同时做多个实验时就无法满足了。如果对用户分成多个桶,当桶的数量过多时,每个桶中的用户数量就会过少,从而会导致实验的置信度下降。

为了保证可以使用相同的流量开展不同的实验,同时各个实验之间不能相关干扰,我们需要采用正交实验。正交实验的思想如下:

每个独立实验为一层,层与层之间的流量是正交的,流量经过一层实验后会再次被随机打散。

有些情况下实验并不是独立的,例如同时对按钮和背景的颜色进行实验,按钮和背景颜色之间并不是独立的(即有些按钮和背景颜色搭配从设计角度是不可行的,没有必要进行实验),这种情况下我们需要采用互斥实验。互斥实验的思想如下:

实验在同一层进行流量拆分,不同组之间的流量是没有重叠的。

在 A/B 测试中,当多个实验内容相互影响应选择互斥方法分配流量,当多个实验内容不会相互影响应选择正交方法分配流量。更加精细的流量分类和控制可以参考 Google 的论文 Overlapping Experiment Infrastructure: More, Better, Faster Experimentation 3

评价指标选择

在设计实验之前我们需要明确实验的目标,根据目标才能确定合理的评价指标。更多情况下我们应该从业务的视角出发选择合适的评价指标,我们以风险策略模型实验为例,我们可以从技术和业务角度选择不同的评价指标:

  1. 技术角度:准确率和召回率
  2. 业务角度:客诉率和追损金额

单纯从技术角度出发我们会忽视很多现实问题,例如两个策略的准确率和召回率差不多,但识别的结果人群不一样,这些人造成的损失也可能不一样。因此能够帮助我们追回更多损失同时有更小的客诉率才是更优的策略。

在进行实验时结果指标至关重要,但有时我们也应该关注一些过程指标。以页面优化实验为例,可能的过程指标和结果指标有:

  1. 过程指标:页面平均停留时间,页面跳出率等
  2. 结果指标:商品加购率,商品转化率等

策略和模型最终都是要为业务服务的,因此我们应常关注业务指标,一些常用的业务指标有:点击率(CTR)、转化率(CVR)、千次展示收入(RPM)等。

有效性检验

当实验完成得到结果后,我们还需要判断实验结果是否有效,这部分主要依靠统计学中的假设检验进行分析。针对两个实验在确定合理的统计量后,需要构建如下两个假设:

对于假设检验只可能有两种结果:一个是接受原假设,一个是拒绝原假设。在进行假设检验过程中,容易犯两类错误:

是否接受原假设 \ 假设真伪 $H_0$ 为真 $H_1$ 为真
拒绝原假设 第一类错误
$\alpha$:显著水平
正确决策
$1 - \alpha$:置信度
接受原假设 正确决策
$1 - \beta$:统计功效
第二类错误
$\beta$

第一类错误(弃真)即原假设为真时拒绝原假设,犯第一类错误的概率为 $\alpha$,即显著水平。第二类错误(取伪)即原假设为假时未拒绝原假设,犯第二类错误的概率为 $\beta$。

在进行有效性检验时我们有多个指标可以参考:

  1. P 值。P 值就是当原假设为真时,比所得到的样本观察结果更极端的结果出现的概率。如果 P 值很小,说明原假设情况的发生的概率很小,而如果出现了,根据小概率原理,我们就有理由拒绝原假设,P 值越小,我们拒绝原假设的理由越充分。
  2. 置信区间。置信区间就是分别以统计量的置信上限和置信下限为上下界构成的区间。置信水平是指包含总体平均值的概率是多大,例如:95% 的置信水平表示,如果有 100 个样本,可以构造出 100 个这样的区间,有 95% 的可能性包含总体平均值。在 A/B 测试时,如果置信区间上下限的值同为正或负,则认为存在有显著差异的可能性;如果同时有正值和负值,则认为不存在有显著差异的可能性。
  3. 统计功效。一般情况下我们希望拒绝原假设,得到新的结论,即在进行 A/B 测试时希望实验组的效果优于对照组。也就是我们希望不要出现在应该拒绝原假设时却没有拒绝的情况,即犯第二类错误。统计功效就是我们没有犯第二类错误的概率 $1 - \beta$,在进行 A/B 测试时表示当两个策略之间存在显著差异时,实验能正确做出存在差异判断的概率。

综上,我们可以认为当 A/B 测试实验数据在 95% 的置信水平区间内,P 值小于0.05,功效大于 80% 的情况下,实验结果是可信赖的。

A/A 测试

在做 A/B 测试的时候,有时尽管我们发现 A/B 两组有明显差异,但我们依旧无法确认这种差异是由于实验条件不同还是 A/B 两组用户本身的差异带来的。尽管 A/B 两组用户是随机抽样,但两组用户在空跑期(即实验条件一致)也会出现显著差异。因此为了避免这个问题,我们会选择进行 A/A 测试,即在正式开启实验之前,先进行一段时间的空跑,对 A/B 两组用户采用同样的实验条件,一段时间后,再看两组之间的差异。如果差异显著,数据弃之不用,重新选组。如果差异不显著,记录两组之间的均值差,然后在实验期结束时,用实验期的组间差异减去空跑期的组间差异得到最终实验结果。

A/A 测试也会存在一些局限,在实际情况中,组间差异是一定存在。因此在这个前提下,可以用统计方式来衡量差异大小,在计算实验效果的时候,把差异考虑在内即可。差异产生的主要原因就是“随机性”,我们同样可以利用置信度和置信区间来描述 A/A 实验的波动。在进行实验时直接进行 A/B 测试,不需要考虑 A/A 测试,在分析结果时,需要考虑 A/B 测试实验之间的差异要大于 A/A 测试实验之间的差异。


  1. https://zh.wikipedia.org/wiki/A/B測試 ↩︎

  2. https://zh.wikipedia.org/wiki/雙盲 ↩︎

  3. Tang, Diane, et al. “Overlapping experiment infrastructure: More, better, faster experimentation.” Proceedings of the 16th ACM SIGKDD international conference on Knowledge discovery and data mining. 2010. ↩︎

一个人的摩旅 (Travel with My Motorcycle Alone)

2021-10-06 08:00:00

之前

三十岁了,不小了,一些事情再不做就不知道会拖到什么时候了。自从之前看到朋友发的一段摩旅的视频,就一直念念不忘,有机会一定要来一次,虽然不一定如视频中那般潇洒。最后证明确实不一样,尽管谈不上是一场修行,但遇到的人事物都是独特且难以忘记的。

有了出发的勇气,但拖延症还是很严重,各种准备和规划直到出发前一晚才简单搞定,当然这也给后面的遇到的问题埋下了伏笔。本来规划了 5 天的行程,最后一天赶到大同见朋友一面再返回北京,但一场冷雨让我在第 4 天直接折返北京。

第一天

折腾了好久,费劲扒拉才把后座包绑好。出门还没上高速,碰见一个骑复古车的哥们,问了我去哪里,我说自己去内蒙古,一句注意安全,一句感谢。出发前才掉了一格油,就没有去加,距离丰宁还 20 多公里,油表灯就闪了,好在还是坚持到了丰宁,成功续满了油。路上断断续续发现成队的、独行的骑士,还有一个书包和后座包都贴了实习标志的哥们,应该都是追逐自由的人吧。

到丰宁前中途停下检查了后座包,发现有一侧绑的不合理,卡口会被后轮摩到,都快要磨断了,幸好检查了下,改造一番绑好继续上路。从丰宁出发没多久,隐约闻道一股烧焦的味道,没当回事儿,以为是路上其他汽车的尾气。中间休息的时候又检查了一下后座包,才发现从丰宁出发时有一侧绑的有点松,后座包倒向了排气那边。奈何我帅气的双排气在上面,就把包烫了一个大洞,一并倒霉的还有带了但一次还没来得及穿过的冬款骑行服和骑行裤。不过好在底下放的是衣服,如果我把相机、电脑、无人机、充电宝放在那边低下…..都不敢想会有什么事儿。这就是没有提前做好功课如何绑后座包的后果,当然闻道异味没有第一时间检查也很不好。

包和衣服被烫了之后,心情一下就不好了,虽然一再自我安慰,但还是缓了很久才勉强不伤心了。由于中途发生了不少事情,整体耽误了一个小时左右,赶到塞罕坝这边酒店的时候已经是六点半了,天已经完全黑下来了,后面一个多小时的路程真的是又累又冷。万幸的是我把我夏季骑行服里面的内胆加上了,不然估计到酒店就冻成狗了。

第二天

早起退房碰见一对儿摩旅的,男生骑了一辆 ADV,小姐姐骑了一辆 Ninja 400。一清早晴空万里,阳光明媚,昨日的不开心一下子全都消散了。虽然秋天已经来了好一阵儿了,不过路两旁的风景还是很漂亮的。

骑进乌兰布统,越往后走人越少,路上碰见了好多次牛马拦路。从乌兰布统去到多伦的路上风景很好,选择下道骑行是明智的,一路上草原、小河、树林,遇到太多美景。

赶着中午骑到了多伦湖,简单吃过饭后就开始了环湖之旅,一路上走走停停。到了一处小高峰,把摩托骑了上去,太适合给我的小摩托来一张照片了。刚拍上就碰到了俩大哥,帮我和我的小摩托来了本次摩旅的第一张合照,聊了会儿道谢之后就开始往回返。

回到环湖起点时间还早,简单搜了下发现往北一点儿就是「滦河」源,小时候家乡人口中的滦河(唐山话:láng hé)原来是从这里流下去的。往滦河源的路不是很好走,骑到一半发现一条土路,骑过去是一个小平台,望向四周很美。后面跟过来好多越野车,也纷纷下来拍照,发现我骑了摩托过来就一个个站在旁边拍起了照片。我过去车上拿我的相机,说道慢慢拍不着急,小姐姐问我可不可以坐上去,我说没问题。后来我也拜托他们帮我和我的小摩托拍了第二张合照,我也成了几个阿姨的摄影师。

快乐是什么?一路上美丽的风景,一路上美丽的人,还有多少能带给他人快乐的我和我的小摩托。果然不敢路的一天是轻松的,从滦河源下来之后就早早赶到多伦的酒店休息了。

第三天

早起的阳光不错,虽然空气有些凉,但是太阳照着还是暖洋洋的,耳机里恰巧响起了赵照的「在冬天和奶奶一起晒太阳」,有些惬意。走着走着云多了起来,中午赶到了太仆寺旗,吃上一口热乎的麻辣烫。

中午微信群里有哥们说北京下大雨了,我心底还在暗暗窃喜这几天出行的天气都还不错,然而这 Flag 还是立早了……下午在去往张北的路上碰见了一波从北京来的摩旅队伍,什么车都有,有 ADV,有踏板,可见是真友谊,ADV 还能带着小踏板一起愉快的玩耍。前半程的风景很不错,路两旁的树叶开始泛黄飘落,一条直路望不见头。

天气渐渐阴沉了下来,肉眼可见前方有积雨云,但还是朝着前方骑了下去。突然天空飘起了小雨,但很快又没了,停在一个加油站打开雷达云图,发现云应该已经从要走的路上飘过去了,简单休息了一下又继续往前走。

路上淅淅沥沥的飘落几滴雨,但路面已经湿透了,之前应该是下了一阵。走着走着发现大腿有点凉,想着应该是雨水溅落到裤子上了。找了个地方停下一看,我去,车子、衣服、后座包已经被黒黑的泥水沾满,躲过了雨水,躲不过泥巴……水越溅越多,身体也越来越冷,万幸还是撑到了张北。找了个肯德基,点了一大杯热拿铁,喝完天已经放晴,看看时间不早了,再不走估计到乌兰察布得很晚了,简单把车擦了下就又启程了。

前半程的国道一路都是风景,但从张北到乌兰察布的后半程除了大车和尘土啥都没有。天彻底放晴,一股暖意上来,还好老天没太折磨我,停下来特意给我脏了吧唧但不离不弃的小摩托照了张像,纪念有史以来最脏的一次。

上了高速就快多了,大概 6 点半多赶到了酒店,卸下行囊第一件事就是去找了个洗车店把我的小摩托好好冲一冲,拿过老板的水枪就开始了自助洗车,洗完之后,我漂亮的小摩托又回来了!

第四天

一早起来天气有些阴冷,不过早有预期,昨天看天气,相比其他地方至少没有下雨。先走了国道去火山,路不太好,一路上身体到不是很冷,冷的是手,夏季的手套还真实漏风,走走停停,冷的受不了就带着手套放在发动机上「烤烤火」。

快到火山了发现一条岔路,还有警察叔叔在摆一个大牌子,「火山旅游路线」,问了后果断选择了指向的新路,新路好走的不要不要的。来到六号火山,人还不少,阴天更是给这里增添了一份外星感。摩托的好处是能骑上平台,转到北面发现人比较少,借来的大疆终于能派上用场了,虽然最后感觉拍的并不好。

往下走看到有几辆越野车爬上了小山丘,心里想来都来了,不在土路上骑一遭怎么对得起我这「越野版」的小狮子。

转完火山发现时间不是很晚,导航了下发现应该能在 7 点前赶回北京,想了想北京这几天都有雨,应该怎么都躲不过,一番权衡之下打算今天直接回北京。上了高速很快就到了集宁服务区,已经快 2 点了,正好吃午饭,这时天也下上了小雨。吃完之后再次权衡,还是决定继续往北京走,如果下大了不行就找最近的口下。一路上,雨不算大也不算小,这就让人很纠结,每到一个服务区就停下来看看云图,最终还是在晚上 10 点前回到了北京。陪了我五年的手机最终在进入地库前因为进水太多失灵然后宕机了,万幸坚持到了回家。

这段雨中经历感觉能回忆一辈子,不是因为勇敢,也不是因为幸运。敢这么走下来,还是评估了环境没有那么恶劣,对自己的驾驶技术也有一定把握,当然最重要的是量力,人永远不要同自然和生命开玩笑。所以每一个服务区都停下来,要么吃点东西,要么喝点热水,休整好后继续往前走。如果说知道 10 点才能到北京,让我再选择一次的话,我想我会选择在中途歇上一晚,虽然第二天也躲不开雨,但能骑得更从容些。

之后

真的要感谢我靠谱的小摩托,一直不离不弃,也要感谢这一路上遇到的陌生人的祝福和帮助。服务区遇到的摩托情侣,问我衣服是不是穿得太少,我没好意思说烫坏了,就只说带少了。餐厅拼桌吃饭的一家,男主人在深圳上班,自己不玩摩托,但工作的地方挨着交管局,见到太多扣查的摩托(深圳全市禁摩),分别时一个握手,一句平安。高速路上,三车道,我在最右侧,中间车道的一辆大车超我时特意变到左侧,掀起的水花完全没溅到我,这一定是个可爱的司机大叔吧。

旅行的意义在哪里?形形色色的人,真实且普通,慢下来,你能看到自己的更多面,也能看到陌生人的一面,或好或坏,无所谓,世界不就是这样吗。

设计语言初探 (A Glimpse of Design Language)

2021-08-08 08:00:00

设计语言(Design Language)或设计语言系统(Design Language System, DLS)是一套用于指导产品设计的整体风格方案 1。设计语言把设计作为一种“沟通的方式”,用于在特定的场景内,做适当的表达,进行特定的信息传递。设计语言在建筑、工业设计和数字产品等领域都有广泛的应用,本文仅围绕数字产品进行初探。

为什么构建设计语言?

统一

通过设计语言可以在整个平台中统一颜色、字体、组件、动效等各种规范,避免由于设计师的个人特点导致产品风格不一致。

体验

优秀的设计语言符合大众审美,可以提高产品的可用性和易用性。设计语言可以使用户能够与具备一致性的应用进行交互,让用户在使用过程中获得愉悦,提升用户体验。

效率

优秀的设计语言使得设计和开发团队能够快速、经济、高效地进行开发、重构和迭代产品。通过不断更新和完善的文档库,可以改善团队之间的协作,提高生产力。

品牌

设计语言的构建可以传达一个统一的公司品牌形象。设计语言让产品具有自己的身份,使其在市场上的众多产品中更容易被识别出来,加深用户对品牌的印象。

设计语言构建

在此我们借助语言学的角度来讨论数字化产品的构建 2。在语言应用中,我们通常会涉及语法、语素、语句、语义、语境、语气、语素和响度等维度,通过不同的组合达成应景的表达和适时的沟通。

语法

设计语言中的语法即设计价值观和设计原则,这是构建设计语言系统的起点,用于传达品牌主张或设计理念,它将指引业务设计执行的方向。

制定设计原则时,首先研究用户特性,聚焦产品核心价值,然后通过脑暴等形式选择有特点的维度,结合用户体验与品牌属性将其视觉化,最后用简要的语言归纳出来。

Ant Design 设计价值观 3

自然

数字世界的光速迭代使得产品日益复杂,而人类意识和注意力资源有限。面对这种设计矛盾,追求「自然」交互将是 Ant Design 持之以恒的方向。

确定性

界面是用户与系统交互的媒介,是手段而非目的。在追求「自然」交互基础上,通过 Ant Design 创造的产品界面应是高确定性、低合作熵的状态。

意义感

一个产品或功能被设计者创造出来不只是用户的需要,而更多是承载用户的某个工作使命。产品设计应充分站在工作视角,促成用户使命的达成;同时,在「自然」、「确定」之上,兼顾用户的人性需求,为工作过程创造富有意义感的人机交互。

生长性

企业级产品功能的增长与用户系统角色的演变相生相伴。设计者应为自己创造的产品负责,提升功能、价值的可发现性。用发展的眼光做设计,充分考虑人、机两端的共同生长。

Ant Design 设计原则 4

亲密性

如果信息之间关联性越高,它们之间的距离就应该越接近,也越像一个视觉单元;反之,则它们的距离就应该越远,也越像多个视觉单元。亲密性的根本目的是实现组织性,让用户对页面结构和信息层次一目了然。

对齐

正如「格式塔学派」中的连续律(Law of Continuity)所描述的,在知觉过程中人们往往倾向于使知觉对象的直线继续成为直线,使曲线继续成为曲线。在界面设计中,将元素进行对齐,既符合用户的认知特性,也能引导视觉流向,让用户更流畅地接收信息。

对比

对比是增加视觉效果最有效方法之一,同时也能在不同元素之间建立一种有组织的层次结构,让用户快速识别关键信息。

重复

相同的元素在整个界面中不断重复,不仅可以有效降低用户的学习成本,也可以帮助用户识别出这些元素之间的关联性。

直截了当

正如 Alan Cooper 所言:「需要在哪里输出,就要允许在哪里输入」。这就是直接操作的原理。eg:不要为了编辑内容而打开另一个页面,应该直接在上下文中实现编辑。

足不出户

能在这个页面解决的问题,就不要去其它页面解决,因为任何页面刷新和跳转都会引起变化盲视(Change Blindness),导致用户心流(Flow)被打断。频繁的页面刷新和跳转,就像在看戏时,演员说完一行台词就安排一次谢幕一样。

简化交互

根据费茨法则(Fitts’s Law)所描述的,如果用户鼠标移动距离越少、对象相对目标越大,那么用户越容易操作。通过运用上下文工具(即:放在内容中的操作工具),使内容和操作融合,从而简化交互。

提供邀请

很多富交互模式(eg:「拖放」、「行内编辑」、「上下文工具」)都有一个共同问题,就是缺少易发现性。所以「提供邀请」是成功完成人机交互的关键所在。

邀请就是引导用户进入下一个交互层次的提醒和暗示,通常包括意符(eg:实时的提示信息)和可供性,以表明在下一个界面可以做什么。当可供性中可感知的部分(Perceived Affordance)表现为意符时,人机交互的过程往往更加自然、顺畅。

巧用过渡

人脑灰质(Gray Matter)会对动态的事物(eg:移动、形变、色变等)保持敏感。在界面中,适当的加入一些过渡效果,能让界面保持生动,同时也能增强用户和界面的沟通。

即时反应

「提供邀请」的强大体现在 交互之前 给出反馈,解决易发现性问题;「巧用过渡」的有用体现在它能够在 交互期间 为用户提供视觉反馈;「即时反应」的重要性体现在 交互之后 立即给出反馈。

就像「牛顿第三定律」所描述作用力和反作用一样,用户进行了操作或者内部数据发生了变化,系统就应该立即有一个对应的反馈,同时输入量级越大、重要性越高,那么反馈量级越大、重要性越高。

虽然反馈太多(准确的说,错误的反馈太多)是一个问题,但是反馈太少甚至没有反馈的系统,则让人感觉迟钝和笨拙,用户体验更差。

Alibaba Fusion 设计价值观 5

化繁为简的交互模式

面对互联网产品高迭代节奏和复杂的中后台场景,将复杂的业务组件抽象为用户标准认知层的交互方式,这套组件库来自于阿里巴巴上百个中后台场景的抽象结果,试图建立中后台 web 设计标准。

驾驭技术

你用的所有设计资料,小到 sketch 样式工具中的颜色、字体、字号、投影、边框、尺寸;再到组件,大到一套完整的中后台产品系统,均能找到其对应的代码,完整的释放整个团队的前端生产力。

追求新鲜,潮流

设计风格每年都会更新换代,由于 Alibaba Fusion 设计系统中的颜色、字体、字号、投影等样式均可通过线上配置修改,这也决定了它可以快速(甚至 15 分钟内)完成整套设计系统的样式迭代。

聚变/裂变

通过在 Alibaba Fusion 设计系统原则下,变换样式、多维度定制组件交互形式,可瞬间获取属于自己业务属性的设计系统;我们期待有无数业务线能够通过 Alibaba Fusion 的设计系统原则聚变出符合各类业务场景的 Fusion 生态系统。

效率

Alibaba Fusion Design 希望构建一套提升设计与开发之间 UI 构建效率的工作方式,让 UED 的工作能够尽可能多的投入在 UE(User Experience)的用户调研、用户体验、商业思考,而在 D(Design)的过程中更多的投入与创意而非日复一日的重复绘图。

腾讯 Q 语言理念 6

统一体验

QQ 作为一个社交平台,会容纳多样性的功能与体验,为了降低用户在不同场景功能下的学习成本,并提升易用性,统一体验是提升平台易用性的关键基础。同时有助于提升各角色间的协作效率。

基因体现

当今同质化的社交应用越来越多,QQ 作为横跨多时期多平台的社交应用,一方面需紧贴时代趋势,在众多应用中脱颖而出,另一方面有足够的历史底蕴,应强化自身基因特征,提升整体品牌认知。

社交向善

社交应用会融入琳琅满目的娱乐化规则与玩法,但吸引年轻人的不应只是单纯娱乐消费,需要考虑社交娱乐的本质初心,QQ 更倡导用户在一个积极健康,安全贴心,触动情感的环境进行社交,并最终导人向善。

高效娱乐

伴随信息传播便利性提升,用户需要更高浓度的信息和更快的娱乐方式。用户时间愈加宝贵,偏向消费耗时较短的短视频、信息流等内容,希望更快找到喜欢的内容,以及更高浓度的内容。

兴趣细分

互联网使用场景更细分,兴趣爱好更加细分深入。各种兴趣圈、游戏圈、粉丝圈等年轻用户基于互联网衍生出来的圈子,需要有更细分深入的功能场景去承载。线上和线下联动是细分圈子持续活跃的关键。

社交压力

互联网信息传播的扩散效应,以及社会的复杂性给用户带来更多社交压力,原创越来越少。可以通过丰富的形象建立和维护体系增强用户的社交动力,引导产生更多原创内容和互动。

腾讯 Q 语言原则 7

活力灵动

对年轻人有吸引力,传递积极乐观情感,有怦然心动的感觉。

亲和自然

体验过程犹如与朋友打交道,亲和自然,懂我所想。

自我有范

用户能无压力表达自我,满足不同人群个性诉求。

语素

视觉基础是构成设计语言的最小单位,就像语素是语言中最小的音义结合体。在原子设计理论中,它属于最小粒度的元素,通常包括:色彩、布局、字体、图标等。

色彩

无论 UI 还是平面,颜色是视觉传达的最核心也是最基本的语言,不同的主色,会给人不同的视觉感受,同样的主色不同的配色,视觉感受也会不同。通常一款产品的色彩体系包含:品牌色、功能色、中立色三个部分:

品牌色:代表品牌形象及 VI 识别的色彩,品牌色的数量可以一个也可以多个,用于主按钮、主 icon 等需要突出品牌特征的地方。

功能色:代表明确的信息以及状态,如成功、出错、失败、提醒等。功能色的选取需要遵守用户对色彩的基本认知,如绿色代表成功,红色代表警示或失败。

中立色:灰或饱和度低的颜色,用于界面设计中的字体、背景、边框、分割线等,中立色通常是按照透明度的方式实现。

布局

空间布局是体系化视觉设计的起点,和传统的平面设计的不同之处在于,UI 界面的布局空间要基于「动态、体系化」的角度出发展开。在中后台视觉体系中定义布局系统,可以从 5 个方面出发:统一的画板尺寸、适配方案、网格单位、栅格、常用模度。

统一画板:为了尽可能减少沟通与理解的成本,有必要在组织内部统一设计板的尺寸。

适配:在设计过程中还需要建立适配的概念,根据具体情况判断系统是否需要进行适配,以及哪些区块需要考虑动态布局。

左右布局的适配方案:常被用于左右布局的设计方案中,常见的做法是将左边的导航栏固定,对右边的工作区域进行动态缩放。

上下布局的适配方案:常被用于上下布局的设计方案中,做法是对两边留白区域进行最小值的定义,当留白区域到达限定值之后再对中间的主内容区域进行动态缩放。

网格单位:通过网格体系可以实现视觉体系的秩序。网格的基数为 8,不仅符合偶数的思路同时能够匹配多数主流的显示设备。通过建立网格的思考方式,还能帮助设计者快速实现布局空间的设计决策同时也能简化设计到开发的沟通损耗。

栅格:以上下布局的结构为例,对内容区域进行 24 栅格的划分设置,如下图所示。页面中栅格的 Gutter 设定了定值,即浏览器在一定范围扩大或缩小,栅格的 Column 宽度会随之扩大或缩小,但 Gutter 的宽度值固定不变。

模度:模度是为了帮助不同设计能力的设计者们在界面布局上的一致性和韵律感,统一设计到开发的布局语言,减少还原损耗。

字体

字体是界面设计中最基本的构成之一。通过定义字体在设计上的使用规则,从而在阅读的舒适性上达到平衡。确定字体主要从下面四个方面出发:字体家族、主字体、字阶与行高、字重。

字体家族:优秀的字体系统首先是要选择合适的字体家族。提供一套利于屏显的备用字体库,来维护在不同平台以及浏览器的显示下,字体始终保持良好的易读性和可读性,体现了友好、稳定和专业的特性。在中后台系统中,数字经常需要进行纵向对比展示,将数字的字体 font-variant-numeric 设置为 tabular-nums,使其为等宽字体。

主字体:基于电脑显示器阅读距离(50 cm)以及最佳阅读角度(0.3),将自私设置为 14,以保证在多数常用显示器上的用户阅读效率最佳。

字阶与行高:字阶和行高决定着一套字体系统的动态与秩序之美。字阶是指一系列有规律的不同尺寸的字体。行高可以理解为一个包裹在字体外面的无形的盒子。

字重:字重的选择同样基于秩序、稳定、克制的原则。多数情况下,只出现 regular 以及 medium 的两种字体重量,分别对应代码中的 400 和 500。在英文字体加粗的情况下会采用 semibold 的字体重量,对应代码中的 600。

图标

图标是 UI 设计中必不可少的组成。通常我们理解图标设计的含义,是将某个概念转换成清晰易读的图形,从而降低用户的理解成本,提升界面的美观度。在我们的企业级应用设计范围中,图标在界面设计的诸多元素中往往只占了很小的比重,在调用时也会被缩到比设计稿小很多倍的尺寸,加上在图形素材极度丰富并且便于获取的今天,在产品设计体系中实现一套美观、一致、易用、便于延展的图标体系往往会被不小心忽略掉。

语句

组件就像由若干个语素组成的语句,比如一个基础按钮,通常就是由颜色、字体、边距等元素组成。而我们平时所说的组件库,其实就是一部词典,其中包含了设计系统中所需的基础组件与用法,在界面设计中也具有较高的通用性。

语义

符号是语言的载体,但符号本身没有意义,只有被赋予含义的符号才能够被使用,这时候语言就转化为信息,而语言的含义就是语义。在视觉传达设计中也一样,使用的图标或图形,需具备正确的语义属性。如果商场导视设计中非要使用「裙子」图标来代表「男厕」入口,如此混淆语义挑战公众认知,那就等着被投诉吧。

语境

语境包含 3 个维度:一是流程意义上的上下文,二是产品属性中的语境,三是用户当下所处的环境。

当设计需要对上下文进行特别处理时,有可能对话的层级次序是受限于屏幕稀缺性,通常可采用 z-depth 叠加(Material Design 属性)、步骤条、元素关联转场动效等方式。举个常见的例子,当用户发起一个删除数据的请求时,界面会弹出一个二次确认的模态会话,用户点击确认之后才会执行删除操作。

针对用户当下所处的环境来适配界面语境,常见通过界面换肤的手法来实现,比如微信读书等阅读应用为用户提供白天模式或黑夜模式的选择。用户所处的外部环境因素可以很大程度上决定界面语言的应用,就好像在菜市场买东西要靠吼,在图书馆借书仅需要用肢体语言便能达成。

语气

交互界面通常需要使用说明或提示文案来指导用户完成操作,大多数情况下都是使用第二人称,就像在与用户对话,从以用户为中心的角度上讲,建议保持谦逊、友善的语气,尽可能避免使用晦涩的专业术语,谨慎使用带有强烈情感属性的感叹号,或过于口语化的语言。另外,语气的拿捏也将直接影响到与用户的距离感,以及当下的应景度。

正确示例:使用检索可以快速搜索任务。
不良示例:你一定会爱上方便快捷的检索功能!

语速

语速在这里指的是界面的信息密度,在不同的场合对语速的控制能够提升接受者的体验,视觉设计也同样需要注意把握间距与留白,网格系统在这里可以起到「节拍器」的作用,借助节拍器可以让设计更具节奏感。而交互意义上的语速,更多体现在操作路径的长度,以及动效的速率。

下图分别展示了 QQ 音乐和富途牛牛两种不同场景的「语速」:

响度

其实就好像我们说话可以通过音量大小来控制信息的可感知程度,希望接受者听清楚的就说大声一点。汤姆奥斯本(Tom Osborne)的视觉响度指南(Visual Loudness Guide)是一个如何系统地处理按钮和链接的例子,它们不是单独列出,而是作为一个套件呈现,并且根据每个元素的视觉冲击力会相应的拥有一个「响度」值。我们在构建设计语言系统时,也同样需要设置梯级「响度」的按钮、字重等组件来满足不同场景的表达需求。

设计语言列表

企业 设计语言
Apple Human Interface Guidelines
Google Material Design
Microsoft Fluent Design
Facebook Facebook Design
Adobe Spectrum
Firefox Photon Design
IBM Carbon Design System
Airbnb Lottie
Salesforce Lightning Design System
蚂蚁金服 Ant Design
阿里巴巴 Fusion Design
腾讯 WeUI
腾讯 Q Design

Spark 集群搭建 (Spark Cluster Setup)

2021-06-19 08:00:00

文本使用的软件版本分别为:

  1. JDK:1.8.0_291,下载地址
  2. Scala:2.12.14,下载地址
  3. Hadoop:3.2.2,下载地址
  4. Spark:3.1.2,下载地址
  5. Python:3.9,Miniconda3,下载地址

按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机。

按照 Hadoop 集群搭建 (Hadoop Cluster Setup) 搭建 Hadoop 集群。

本文以 Spark on YARN 模式介绍 Spark 集群的搭建。

Scala 配置

将 Scala 安装包解压缩到 /opt 目录并创建软链接:

cd /opt
tar -zxvf scala-2.12.14.tgz
ln -s /opt/scala-2.12.14 /opt/scala

将如下信息添加到 /etc/profile 中:

# Scala
export SCALA_HOME=/opt/scala
export PATH=$PATH:$SCALA_HOME/bin

方便起见可以使用 rsync 命令同步 Scala:

rsync -auvp /opt/scala-2.12.14 leo@vm-02:/opt
rsync -auvp /opt/scala-2.12.14 leo@vm-03:/opt

Spark 配置

将 Spark 安装包解压缩到 /opt 目录并创建软链接:

cd /opt
tar -zxvf spark-3.1.2-bin-hadoop3.2.tgz
ln -s /opt/spark-3.1.2-bin-hadoop3.2 /opt/spark

将如下信息添加到 /etc/profile 中:

# Spark
export SPARK_HOME=/opt/spark
export PATH=$PATH:$SPARK_HOME/bin

复制环境变量文件:

cp /opt/spark/conf/spark-env.sh.template /opt/spark/conf/spark-env.sh

spark-env.sh 结尾添加如下内容:

export JAVA_HOME=/opt/jdk
export SCALA_HOME=/opt/scala
export HADOOP_HOME=/opt/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/conf
export YARN_CONF_DIR=$HADOOP_HOME/conf
export SPAKR_HOME=/opt/spark
export SPARK_CONF_DIR=$SPAKR_HOME/conf

export SPARK_EXECUTOR_CORES=1
export SPARK_EXECUTOR_MEMORY=1G
export SPARK_DRIVER_MEMORY=1G
export SPARK_HISTORY_OPTS="-Dspark.history.retainedApplications=10"

复制配置文件:

cp /opt/spark/conf/spark-defaults.conf.template /opt/spark/conf/spark-defaults.conf

修改 spark-defaults.conf 文件内容如下:

spark.eventLog.enabled              true
spark.eventLog.compress             true
spark.eventLog.dir                  hdfs://vm-01:9000/logs/spark
spark.history.fs.logDirectory       hdfs://vm-01:9000/logs/spark
spark.yarn.historyServer.address    vm-01:18080
spark.yarn.jars                     hdfs://vm-01:9000/spark/jars/*

复制 Worker 节点列表文件:

cp /opt/spark/conf/workers.template /opt/spark/conf/workers

修改 workers 文件内容如下:

vm-01
vm-02
vm-03

在 HDFS 上创建目录,并上传 Spark 相关 JAR 包:

hdfs dfs -mkdir -p /spark/jars
hdfs dfs -put /opt/spark/jars/* /spark/jars/

方便起见可以使用 rsync 命令同步 Spark:

rsync -auvp /opt/spark-3.1.2-bin-hadoop3.2 leo@vm-02:/opt
rsync -auvp /opt/spark-3.1.2-bin-hadoop3.2 leo@vm-03:/opt

启动 Spark

在 vm-01,vm-02 和 vm-03 上启动 Zookeeper:

zkServer.sh start

启动 Hadoop:

/opt/hadoop/sbin/start-dfs.sh
/opt/hadoop/sbin/start-yarn.sh

获取并切换 YARN Resource Manager 的状态:

yarn rmadmin -getServiceState rm1
yarn rmadmin -getServiceState rm2

yarn rmadmin -transitionToActive rm1 --forcemanual

在 HDFS 上创建相关目录:

hdfs dfs -mkdir /logs
hdfs dfs -mkdir /logs/spark

启动 Spark:

/opt/spark/sbin/start-all.sh

通过 http://vm-01:8081 可以进入 Spark Web 页面:

启动 Spark History Server:

/opt/spark/sbin/start-history-server.sh

执行 PI 示例程序:

spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master yarn \
  --deploy-mode cluster \
  --executor-memory 1G \
  --num-executors 3 \
  /opt/spark/examples/jars/spark-examples*.jar \
  10

在 YARN 中,通过 Application ID 查看对应的 Container 的 stdout 日志,可以得到示例程序的运行结果:

Pi is roughly 3.1424791424791425

通过 http://vm-01:18081 可以进入 Spark History Server 页面:

NFS 配置

安装 NFS 相关软件:

sudo apt install nfs-kernel-server nfs-common

在 vm-01,vm-02 和 vm-03 上创建 MFS 文件夹并设置权限:

sudo mkdir /nfs
sudo chown -R leo:leo /nfs

在 vm-01 上修改 /etc/exports 文件,配置 NFS 共享目录:

/nfs 192.168.56.1/24(rw,sync,no_root_squash,no_subtree_check)

相关参数定义可以通过 man nfs 获取。

导出共享目录并重启 NFS 服务:

sudo exportfs -a
sudo service nfs-kernel-server restart

在 vm-02 和 vm-03 上挂在 NFS:

sudo mount vm-01:/nfs /nfs

/etc/fstab 中添加如下内容实现开机自动挂载:

vm-01:/nfs /nfs nfs rw

Python 配置

安装 Miniconda3 到 /nfs/miniconda3 目录:

sh Miniconda3-py39_4.9.2-Linux-x86_64.sh

在安装过程中安装选项如下:

Miniconda3 will now be installed into this location:
/home/leo/miniconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/home/leo/miniconda3] >>> /nfs/miniconda3
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes

修改 ~/.condarc 更改 Anaconda 镜像:

channels:
  - defaults
show_channel_urls: true
default_channels:
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2
custom_channels:
  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud

创建用于 Spark 的 Python 环境:

conda create -n spark python=3.9

分别在 vm-02 和 vm-03 中将如下信息添加到 /etc/profile 中:

# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/nfs/miniconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
    eval "$__conda_setup"
else
    if [ -f "/nfs/miniconda3/etc/profile.d/conda.sh" ]; then
        . "/nfs/miniconda3/etc/profile.d/conda.sh"
    else
        export PATH="/nfs/miniconda3/bin:$PATH"
    fi
fi
unset __conda_setup
# <<< conda initialize <<<

分别在 vm-01,vm-02 和 vm-03 中将如下信息添加到 /etc/profile 中:

# Python 3.9 for Spark
conda activate spark

PySpark 测试

输入 pyspark 进入 PySaprk Shell:

Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.2
      /_/

Using Python version 3.9.5 (default, Jun  4 2021 12:28:51)
Spark context Web UI available at http://vm-01:4040
Spark context available as 'sc' (master = local[*], app id = local-1623876641615).
SparkSession available as 'spark'.
>>>

执行 PI 示例程序:

from random import random
from operator import add

partitions = 3
n = 100000 * partitions

def f(_):
    x = random() * 2 - 1
    y = random() * 2 - 1
    return 1 if x ** 2 + y ** 2 <= 1 else 0

count = spark.sparkContext.parallelize(range(1, n + 1), partitions).map(f).reduce(add)
print("Pi is roughly %f" % (4.0 * count / n))

运行结果如下:

Pi is roughly 3.147160

Hive 安装和配置 (Hive Setup)

2021-06-14 08:00:00

文本使用的软件版本分别为:

  1. JDK:1.8.0_291,下载地址
  2. Hadoop:3.2.2,下载地址
  3. Hive:3.2.1,下载地址
  4. MySQL:8.0.25,使用 apt install mysql-server 安装。
  5. MySQL JDBC Connector:8.0.25,下载地址

按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机列表如下:

主机名 IP 角色
vm-01 192.168.56.101 Hadoop
MySQL
Hive
vm-02 192.168.56.102 Hadoop
vm-03 192.168.56.103 Hadoop

按照 Hadoop 集群搭建 (Hadoop Cluster Setup) 搭建 Hadoop 集群。

MySQL 安装和配置

通过如下命令安装 MySQL:

sudo apt install mysql-server mysql-client libmysqlclient-dev

安装完毕后,使用如下命令初始化 MySQL:

sudo mysql_secure_installation

在密码安全性校验步骤,输入 N 关闭密码安全性校验:

VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD component?

Press y|Y for Yes, any other key for No: N

输入新密码:

New password: *********
Re-enter new password: *********

在删除匿名用户环节,输入 Y 删除匿名用户:

By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.

Remove anonymous users? (Press y|Y for Yes, any other key for No) : Y

输入 N 允许远程登录 root 用户:

Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : N

输入 N 保留 test 数据库:

By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.

Remove test database and access to it? (Press y|Y for Yes, any other key for No) : N

输入 Y 应用设置并生效:

Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : Y

修改 MySQL 配置文件:

sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf

将绑定地址替换为 0.0.0.0

bind-address            = 0.0.0.0
mysqlx-bind-address     = 0.0.0.0

重启 MySQL 服务:

sudo service mysql restart

在本地通过如下命令并输入密码进入 MySQL:

sudo mysql -uroot -p

在 MySQL 命令行中输入如下语句为 root 用户配置允许远程访问:

CREATE USER 'root'@'%' IDENTIFIED BY '**********';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

之后在宿主机通过如下命令即可登录虚拟机中的 MySQL:

mysql -h192.168.56.101 -uroot -p

为 Hive 创建数据库和用户,并设置相关权限:

CREATE DATABASE hive;
CREATE USER 'hive'@'%' IDENTIFIED BY '**********';
GRANT ALL PRIVILEGES ON hive.* TO 'hive'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

Hive 安装和配置

将 Hive 安装包解压缩到 /opt 目录并创建软链接:

cd /opt
tar -zxvf apache-hive-3.1.2-bin.tar.gz
ln -s /opt/apache-hive-3.1.2-bin /opt/hive

将如下信息添加到 /etc/profile 中:

# Hive
export HIVE_HOME=/opt/hive
export PATH=$PATH:$HIVE_HOME/bin

复制环境变量文件:

cp /opt/hive/conf/hive-env.sh.template /opt/hive/conf/hive-env.sh

修改 hive-env.sh 内容如下:

export JAVA_HOME=/opt/jdk
export HADOOP_HOME=/opt/hadoop
export HIVE_HOME=/opt/hive
export HIVE_CONF_DIR=$HIVE_HOME/conf

创建配置文件:

vi /opt/hive/conf/hive-site.xml

修改 hive-site.xml 内容如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <name>javax.jdo.option.ConnectionURL</name>
    <value>jdbc:mysql://vm-01:3306/hive</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionDriverName</name>
    <value>com.mysql.cj.jdbc.Driver</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionUserName</name>
    <value>hive</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionPassword</name>
    <value>*********</value>
  </property>
</configuration>

将 MySQL JDBC Connector 解压缩到 /opt/hive/lib 中:

cd /opt
tar -zxvf mysql-connector-java-8.0.25.tar.gz
mv /opt/mysql-connector-java-8.0.25/mysql-connector-java-8.0.25.jar /opt/hive/lib
rm -rf /opt/mysql-connector-java-8.0.25

修正不兼容的依赖包:

mv /opt/hive/lib/log4j-slf4j-impl-2.10.0.jar /opt/hive/lib/log4j-slf4j-impl-2.10.0.jar.bak
mv /opt/hive/lib/guava-19.0.jar /opt/hive/lib/guava-19.0.jar.bak
cp /opt/hadoop/share/hadoop/common/lib/guava-*.jar /opt/hive/lib

初始化元数据:

schematool -dbType mysql -initSchema

出现如下输出时表示元数据初始化成功:

Metastore connection URL:	 jdbc:mysql://vm-01:3306/hive
Metastore Connection Driver :	 com.mysql.cj.jdbc.Driver
Metastore connection User:	 hive
Starting metastore schema initialization to 3.1.0
Initialization script hive-schema-3.1.0.mysql.sql
......
Initialization script completed
schemaTool completed

启动 Hive

执行如下命令启动 Hive:

hive

出现如下输出时表示启动成功:

Hive Session ID = f3edb53b-5037-47c3-b318-75854e2328c5

Logging initialized using configuration in jar:file:/opt/apache-hive-3.1.2-bin/lib/hive-common-3.1.2.jar!/hive-log4j2.properties Async: true
Hive Session ID = 3331472a-a171-47be-843a-378611233f18
Hive-on-MR is deprecated in Hive 2 and may not be available in the future versions. Consider using a different execution engine (i.e. spark, tez) or using Hive 1.X releases.
hive>

Hadoop 集群搭建 (Hadoop Cluster Setup)

2021-06-13 08:00:00

文本使用的软件版本分别为:

  1. JDK:1.8.0_291,下载地址
  2. Zookeeper:3.7.0,下载地址
  3. Hadoop:3.2.2,下载地址

按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机列表如下:

主机名 IP 角色
vm-01 192.168.56.101 HDFS Namenode
HDFS Datanode
YARN Resource Manager
YARN Node Manager
Journal Node
Zookeeper
vm-02 192.168.56.102 HDFS Namenode
HDFS Datanode
YARN Resource Manager
YARN Node Manager
Journal Node
Zookeeper
vm-03 192.168.56.103 HDFS Namenode
HDFS Datanode
YARN Node Manager
Journal Node
Zookeeper

系统配置

/opt 目录所有者赋予当前用户:

sudo chown -R leo:leo /opt

在根目录建立 data 目录,并将其所有者赋予当前用户:

sudo mkdir /data
sudo chown -R leo:leo /data

JDK 配置

将 JDK 安装包解压缩到 /opt 目录并创建软链接:

cd /opt
tar -zxvf jdk-8u291-linux-x64.tar.gz
ln -s /opt/jdk1.8.0_291 /opt/jdk

将如下信息添加到 /etc/profile 中:

# JDK
export JAVA_HOME=/opt/jdk
export PATH=$PATH:$JAVA_HOME/bin

方便起见可以使用 rsync 命令同步 JDK:

rsync -auvp /opt/jdk1.8.0_291 leo@vm-02:/opt 
rsync -auvp /opt/jdk1.8.0_291 leo@vm-03:/opt 

Zookeeper 配置

将 Zookeeper 安装包解压到 /opt 目录并创建软链接:

cd /opt
tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz
ln -s /opt/apache-zookeeper-3.7.0-bin /opt/zookeeper

将如下信息添加到 /etc/profile 中:

# Zookeeper
export ZOOKEEPER_HOME=/opt/zookeeper
export PATH=$PATH:$ZOOKEEPER_HOME/bin

/data 目录下创建 zookeeper 文件夹:

mkdir /data/zookeeper

/data/zookeeper 目录中创建 myid 文件:

echo 1 > /data/zookeeper/myid    # 仅在 vm-01 上执行
echo 2 > /data/zookeeper/myid    # 仅在 vm-02 上执行
echo 3 > /data/zookeeper/myid    # 仅在 vm-03 上执行

复制 Zookeeper 配置文件:

cd /opt/zookeeper/conf
mv zoo_sample.cfg zoo.cfg

修改 zoo.cfg 文件内容如下:

# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/data/zookeeper

# servers
server.1=vm-01:2888:3888
server.2=vm-02:2888:3888
server.3=vm-03:2888:3888

方便起见可以使用 rsync 命令同步 Zookeeper:

rsync -auvp /opt/apache-zookeeper-3.7.0-bin leo@vm-02:/opt 
rsync -auvp /opt/apache-zookeeper-3.7.0-bin leo@vm-03:/opt 

Hadoop 配置

将 Hadoop 安装包解压到 /opt 目录并创建软链接:

cd /opt
tar -zxvf /hadoop-3.2.2.tar.gz
ln -s /opt/hadoop-3.2.2 /opt/hadoop

将如下信息添加到 /etc/profile 中:

# Hadoop
export HADOOP_HOME=/opt/hadoop
export PATH=$PATH:$HADOOP_HOME/bin

/data 目录下创建如下文件夹:

mkdir /data/hadoop
mkdir /data/hadoop/tmp
mkdir /data/hadoop/pid
mkdir /data/hadoop/logs
mkdir /data/hadoop/hdfs
mkdir /data/hadoop/hdfs/journalnode
mkdir /data/hadoop/hdfs/namenode
mkdir /data/hadoop/hdfs/datanode

编辑 /opt/hadoop/etc/hadoop/core-site.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <!-- HDFS 的 nameservice -->
    <name>fs.defaultFS</name>
    <value>hdfs://ns1/</value>
  </property>
  <property>
    <!-- Hadoop 临时目录 -->
    <name>hadoop.tmp.dir</name>
    <value>/data/hadoop/tmp</value>
  </property>
  <property>
    <!-- Zookeeper 地址 -->
    <name>ha.zookeeper.quorum</name>
    <value>vm-01:2181,vm-02:2181,vm-03:2181</value>
  </property>
</configuration>

编辑 /opt/hadoop/etc/hadoop/hdfs-site.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <!-- HDFS 的 nameservice -->
    <name>dfs.nameservices</name>
    <value>ns1</value>
  </property>
  <property>
    <!-- namenode 列表 -->
    <name>dfs.ha.namenodes.ns1</name>
    <value>nn1,nn2,nn3</value>
  </property>
  <property>
    <!-- nn1 的 RPC 通信地址 -->
    <name>dfs.namenode.rpc-address.ns1.nn1</name>
    <value>vm-01:9000</value>
  </property>
  <property>
    <!-- nn2 的 RPC 通信地址 -->
    <name>dfs.namenode.rpc-address.ns1.nn2</name>
    <value>vm-02:9000</value>
  </property>
  <property>
    <!-- nn3 的 RPC 通信地址 -->
    <name>dfs.namenode.rpc-address.ns1.nn3</name>
    <value>vm-03:9000</value>
  </property>
  <property>
    <!-- nn1 的 HTTP 通信地址 -->
    <name>dfs.namenode.http-address.ns1.nn1</name>
    <value>vm-01:50070</value>
  </property>
  <property>
    <!-- nn2 的 HTTP 通信地址 -->
    <name>dfs.namenode.http-address.ns1.nn2</name>
    <value>vm-02:50070</value>
  </property>
  <property>
    <!-- nn3 的 HTTP 通信地址 -->
    <name>dfs.namenode.http-address.ns1.nn3</name>
    <value>vm-03:50070</value>
  </property>
  <property>
    <!-- namenode 在 journalnode 上的存放位置 -->
    <name>dfs.namenode.shared.edits.dir</name>
    <value>qjournal://vm-01:8485;vm-02:8485;vm-03:8485/ns1</value>
  </property>
  <property>
    <!-- journalnode 在磁盘上的存放位置 -->
    <name>dfs.journalnode.edits.dir</name>
    <value>/data/hadoop/hdfs/journalnode</value>
  </property>
  <property>
    <!-- 开启 namenode 失败自动切换 -->
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
  </property>
  <property>
    <!-- 配置失败自动切换实现方式 -->
    <name>dfs.client.failover.proxy.provider.ns1</name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <property>
    <!-- 配置隔离机制方法,多个机制用换行分割,即每个机制暂用一行 -->
    <name>dfs.ha.fencing.methods</name>
    <value>sshfence</value>
  </property>
  <property>
    <!-- 使用 sshfence 隔离机制时需要 ssh 免密登陆 -->
    <name>dfs.ha.fencing.ssh.private-key-files</name>
    <value>/homt/leo/.ssh/id_rsa</value>
  </property>
  <property>
    <!-- 配置 sshfence 隔离机制超时时间 -->
    <name>dfs.ha.fencing.ssh.connect-timeout</name>
    <value>30000</value>
  </property>
  <property>
    <!-- journalnode HTTP 通信地址 -->
    <name>dfs.journalnode.http-address</name>
    <value>0.0.0.0:8480</value>
  </property>
  <property>
    <!-- journalnode RPC 通信地址 -->
    <name>dfs.journalnode.rpc-address</name>
    <value>0.0.0.0:8485</value>
  </property>
  <property>
    <!-- HDFS 副本数量 -->
    <name>dfs.replication</name>
    <value>1</value>
  </property>
  <property>
    <!-- namenode 在磁盘上的存放位置 -->
    <name>dfs.namenode.name.dir</name>
    <value>/data/hadoop/hdfs/namenode</value>
  </property>
  <property>
    <!-- datanode 在磁盘上的存放位置 -->
    <name>dfs.datanode.data.dir</name>
    <value>/data/hadoop/hdfs/datanode</value>
  </property>
  <property>
    <!--开启 webhdfs 接口访问 -->
    <name>dfs.webhdfs.enabled</name>
    <value>true</value>
  </property>
  <property>
    <!-- 关闭权限验证,hive 可以直连 -->
    <name>dfs.permissions.enabled</name>
    <value>false</value>
  </property>
</configuration>

编辑 /opt/hadoop/etc/hadoop/yarn-site.xml 内容如下:

<?xml version="1.0"?>
<configuration>
  <property>
    <!-- 开启 resourc emanager 高可用 -->
    <name>yarn.resourcemanager.ha.enabled</name>
    <value>true</value>
  </property>
  <property>
    <!-- 指定 resourc emanager 的 cluster id -->
    <name>yarn.resourcemanager.cluster-id</name>
    <value>leo</value>
  </property>
  <property>
    <!-- 指定 resourc emanager 的名字 -->
    <name>yarn.resourcemanager.ha.id</name>
    <value>rm1</value>
  </property>
  <property>
    <!-- 指定 resourc emanager 的名字 -->
    <name>yarn.resourcemanager.ha.rm-ids</name>
    <value>rm1,rm2</value>
  </property>
  <property>
    <!-- 指定 resourc emanager 1 的地址 -->
    <name>yarn.resourcemanager.hostname.rm1</name>
    <value>vm-01</value>
  </property>
  <property>
    <!-- 指定 resourc emanager 2 的地址 -->
    <name>yarn.resourcemanager.hostname.rm2</name>
    <value>vm-02</value>
  </property>
  <property>
    <!-- 指定 zookeeper 集群地址 -->
    <name>yarn.resourcemanager.zk-address</name>
    <value>vm-01:2181,vm-02:2181,vm-03:2181</value>
  </property>
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
</configuration>

注意

需要在 vm-02 中将 yarn.resourcemanager.ha.id 的值设置为 rm2,在 vm-03 中删除 yarn.resourcemanager.ha.id 属性。

编辑 /opt/hadoop/etc/hadoop/mapred-site.xml 内容如下:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
  </property>
  <property>
    <name>mapreduce.application.classpath</name>
    <value>
        /opt/Hadoop/share/hadoop/common/*,
        /opt/hadoop/share/hadoop/common/lib/*,
        /opt/hadoop/share/hadoop/hdfs/*,
        /opt/hadoop/share/hadoop/hdfs/lib/*,
        /opt/hadoop/share/hadoop/mapreduce/*,
        /opt/hadoop/share/hadoop/mapreduce/lib/*,
        /opt/hadoop/share/hadoop/yarn/*,
        /opt/hadoop/share/hadoop/yarn/lib/*
    </value>
  </property>
</configuration>

修改 /opt/hadoop/etc/hadoop/hadoop-env.sh 内容如下:

export JAVA_HOME=/opt/jdk
export HADOOP_LOG_DIR=/data/hadoop/logs
export HADOOP_PID_DIR=/data/hadoop/pid

修改 /opt/hadoop/etc/hadoop/yarn-env.sh 内容如下:

export JAVA_HOME=/opt/jdk

/opt/hadoop/sbin/start-dfs.sh/opt/hadoop/sbin/stop-dfs.sh 开始位置添加:

HDFS_NAMENODE_USER=leo
HDFS_DATANODE_USER=leo
HDFS_JOURNALNODE_USER=leo
HDFS_ZKFC_USER=leo

/opt/hadoop/sbin/start-yarn.sh/opt/hadoop/sbin/stop-yarn.sh 开始位置添加:

YARN_RESOURCEMANAGER_USER=leo
YARN_NODEMANAGER_USER=leo

修改 /opt/hadoop/etc/hadoop/workers 内容如下:

vm-01
vm-02
vm-03

方便起见可以使用 rsync 命令同步 Hadoop:

rsync -auvp /opt/hadoop-3.2.2 leo@vm-02:/opt 
rsync -auvp /opt/hadoop-3.2.2 leo@vm-03:/opt 

启动集群

启动 zookeeper

分别在 vm-01,vm-02 和 vm-03 上执行如下操作:

zkServer.sh start

当所有虚拟机 zookeeper 启动完毕后,执行如下操作:

zkServer.sh status

可能得到如下输出:

ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader

ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower

启动 journalnode

分别在 vm-01,vm-02 和 vm-03 上执行如下操作:

hdfs --daemon start journalnode

格式化 namenode

在 vm-01 上执行如下操作:

hdfs namenode -format

将格式化之后的元数据到其他 namenode:

rsync -auvp /data/hadoop/hdfs/namenode/current vm-01:/data/hadoop/hdfs/namenode/current
rsync -auvp /data/hadoop/hdfs/namenode/current vm-02:/data/hadoop/hdfs/namenode/current

在 vm-01 格式化 zookeeper:

hdfs zkfc -formatZK

停止 journalnode

分别在 vm-01,vm-02 和 vm-03 上执行如下操作:

hdfs --daemon stop journalnode

启动 hadoop

在 vm-01 启动 DFS:

/opt/hadoop/sbin/start-dfs.sh

会得到如下输出:

Starting namenodes on [vm-01 vm-02 vm-03]
Starting datanodes
Starting journal nodes [vm-01 vm-03 vm-02]
Starting ZK Failover Controllers on NN hosts [vm-01 vm-02 vm-03]

在 vm-01 启动 YARN:

/opt/hadoop/sbin/start-yarn.sh

会得到如下输出:

Starting resourcemanagers on [vm-01 vm-02]
Starting nodemanagers

通过 http://vm-01:50070 可以进入 Hadoop Web 页面:

通过 http://vm-01:8088 可以进入 YARN 页面:

虚拟环境准备 (Virtual Environment Preparation)

2021-06-12 08:00:00

本文以 VirtualBox 和 Ubuntu Server 为例,介绍在 macOS 下搭建 3 台虚拟机集群过程。

安装 VirtualBox

官网下载最新版本的 VirtualBox 和 VirtualBox Extension Pack,本文以 6.1.22 版本为例。根据安装向导安装 VirtualBox。

双击下载好的 .vbox-extpack 安装文件安装 VirtualBox Extension Pack。

通过 File -> Host Network Manager... 为 VirtualBox 添加一块网卡。

安装 Ubuntu Server

虚拟机配置

官网下载最新版本的 Ubuntu Server,本文以 20.04.2 LTS 版本为例。在 VirtualBox 通过 New 按钮添加新的虚拟机,首先为虚拟机配置名称,存储路径和内存大小等基本信息:

单击 Create 后为虚拟机配置磁盘类型和大小:

单击 Create 完成创建。创建完毕后,在左侧列表中选择创建好的虚拟机,通过 Settings 按钮打开设置对话框,在 System - Motherboard 标签页去除掉软盘启动 Boot Order - Floppy,同时也可以再次调整内存大小:

System - Processor 标签页可以调整虚拟机使用 CPU 的数量:

Display - Acceleration 标签页可以调整使用的现存大小:

Network - Adapter 1 标签页选择 NAT 网络类型,该网卡用于虚拟机连接外部网络:

Network - Adapter 2 标签页选择 Host-only Adapter 网络类型,名称选择上文 VirtualBox 配置的 vboxnet0,该网卡用于虚拟机连接内部网络:

Network - Adapter 2 标签页为光驱选择挂载的磁盘镜像:

添加下载好的 Ubuntu Server ISO 磁盘镜像:

Ubuntu Server 安装配置

单击 Start 按钮启动虚拟机,启动后等待片刻,在语言选择页面选择 English

在键盘布局页面采用默认配置,Done 进入下一步:

在网络配置页面,上文中配置了两块网卡,在此我们需要对于 Host-only Adapter 网卡 enp0s8 进行配置,NAT 网卡 enp0s3 采用默认即可:

进入配置后选择手动 Nanual,在 Subnet 中输入 192.168.56.0/24(参考上文中 VirtualBox 添加的网卡设置),在 Address 中输入 192.168.56.1,在 Gateway 中输入 192.168.56.1Save 进行保存:

配置完后,结果如下,Done 进入下一步:

在镜像配置页面,根据实际情况选择一个合适的镜像地址,本文采用清华大学的镜像地址 https://mirrors.tuna.tsinghua.edu.cn/ubuntu/Done 进入下一步:

在存储配置页面,为了方便起见选择使用整个磁盘,Done 进入下一步:

确认磁盘配置信息,Done 进入下一步:

在用户信息配置页面,输入用户名和密码等信息,Done 进入下一步:

在 SSH 配置页面,选择安装 OpenSSH server,Done 进入下一步:

在软件包配置页面,跳过安装,Done 进入下一步:

安装完毕后,Reboot Now 重启虚拟机:

配置 Ubuntu Server

克隆虚拟机

重启后,输入用户名和密码即可进入系统:

通过如下命令更新系统软件到最新版本:

sudo apt update
sudo apt upgrade
sudo apt autoremove

输入以下命令进行关机:

sudo shutdown now

右键单击虚拟机 Ubuntu Server 1,选择 Clone 对虚拟机进行克隆,选择为所有网卡重新生成 MAC 地址,然后单击 Continue

选择 Full clone 模式,单击 Continue 完成克隆:

配置网络

分别进入虚拟机 2 和 3,修改固定 IP 地址为 191.168.56.102191.168.56.103

sudo vi /etc/netplan/00-installer-config.yaml
# This is the network config written by 'subiquity'
network:
  ethernets:
    enp0s3:
      dhcp4:true
    enp0s8:
      addresses:
      - 192.168.56.101/24
      gateway4: 192.168.56.1
      nameservers:
        addresses: []
        search: []
  version: 2

将配置文件中 00-installer-config.yaml192.168.56.101 修改为对应的 IP。然后执行:

sudo netplan apply

让配置生效。

为三台虚拟机修改主机名为 vm-01vm-02vm-03

sudo hostnamectl set-hostname vm-0x

为三台虚拟机设置 IP 和主机名映射:

sudo vi /etc/hosts

在结尾添加:

# VM
192.168.56.101 vm-01
192.168.56.102 vm-02
192.168.56.103 vm-03

配置 SSH

分别进入三台虚拟机并生成密钥:

ssh-keygen -t rsa

这会在 ~/.ssh 目录生成一对密钥,其中 id_rsa 是私钥,id_rsa.pub 是公钥。

将三台机器中的 id_rsa.pub 导出合并到 ~/.ssh/authorized_keys 文件中,为了方便可以将自己电脑的 id_rsa.pub 也合并到其中实现免密登录。

登录任意每台虚拟机,通过如下命令测试是否可以免密登录:

ssh vm-01
ssh vm-02
ssh vm-03

大数据 SQL 性能调优 (Big Data SQL Performance Tuning)

2021-05-23 08:00:00

在日常工作中,数据处理和分析在研发、产品和运营等多个领域起着重要的作用。在海量数据处理和分析中,SQL 是一项基础且重要的能力。一个优秀的 SQL Boy 和茶树姑的 SQL 代码除了保持简单、可读和易于维护的样式风格外,还需要具备良好的执行性能,准确且高效的计算出结果才能让你在工作中决胜于千里之外。

影响 SQL 执行性能的主要因素可以总结为如下几项:

  1. 计算资源量(CPU,内存,网络等)
  2. 计算数据量(输入和输出的记录数)
  3. 计算复杂度(业务逻辑复杂程度和对应的 SQL 实现和执行)

计算资源量是一个前置制约因素,理论上更多的资源能够带来更快的计算效果。计算数据量也可以认为是一个前置制约因素,理论上更大的数据量会导致计算速度降低,但对于复杂的计算逻辑,通过合理的 SQL 可以更好的控制计算过程中的数据量,从而提升 SQL 性能。计算复杂度是影响 SQL 性能的关键因素,复杂的业务逻辑必然比简单的业务逻辑处理时间要长,相同业务逻辑的不同 SQL 实现也会影响运行效率,这就要求我们对业务逻辑进行全面的理解,对实现 SQL 进行合理优化,从而提升计算速度。

执行引擎

SQL 是用于一种用于数据定义和数据操纵的特定目的的编程语言 1。SQL 虽然有 ISO 标准 2,但大部分 SQL 代码在不同的数据库系统中并不具有完全的跨平台性。不同的执行引擎也会对 SQL 的语法有相应的改动和扩展,同时对于 SQL 的执行也会进行不同的适配和优化。因此,脱离执行引擎的 SQL 性能优化是不可取的。

Hive

Apache Hive 是一个建立在 Hadoop 架构之上的数据仓库。可以将结构化的数据文件映射为一张数据库表,并提供简单的 SQL 查询功能,可以将 SQL 语句转换为 MapReduce 任务进行运行。因此 MapReduce 是 Hive SQL 运行的核心和根基。

我们以 Word Count 为例简单介绍一下 MapReduce 的原理和过程,Word Count 的 MapReduce 处理过程如下图所示:

  1. Input:程序的输入数据。
  2. Splitting:讲输入数据分割为若干部分。
  3. Mapping:针对 Splitting 分割的每个部分,对应有一个 Map 程序处理。本例中将分割后的文本统计成 <K,V> 格式,其中 K 为单词,V 为该单词在这个 Map 中出现的次数。
  4. Shuffling:对 Mapping 的相关输出结果进行合并。本例中将具有相同 K 的统计结果合并到一起。
  5. Reducing:对 Shuffling 合并的结果进行汇总。本例中讲相同 KV 值进行加和操作并返回单个统计结果。
  6. Merged:对 Reducing 的结果进行融合形成最终输出。

Spark

Apache Spark 是一个用于大规模数据处理的统一分析引擎,Spark SQL 则作为 Apache Spark 用于处理结构化数据的模块。

Spark 中常见的概念有:

  1. RDD:Resilient Distributed Dataset,弹性分布式数据集,是分布式内存中一个抽象概念,提供了一种高度受限的共享内存模型。
  2. DAG:Directed Acyclic Graph,有向无环图,反应了 RDD 之间的依赖关系。
  3. Driver Program:控制程序,负责为 Application 创建 DAG,通常用 SparkContext 代表 Driver Program。
  4. Cluster Manager:集群管理器,负责分配计算资源。
  5. Worker Node:工作节点,负责具体计算。
  6. Executor:运行在 Worker Node 上的一个进程,负责运行 Task,并为 Application 存储数据。
  7. Application:Spark 应用程序,包含多个 Executor。
  8. Task:任务,运行在 Executor 上的工作单元,是 Executor 中的一个线程
  9. Stage:一组并行的 Task,Spark 一般会根据 Shuffle 类算子(例如:reduceByKeyjoin 等)划分 Stage。
  10. Job:一组 Stage 的集合,一个 Job 包含多个 RDD 及作用于 RDD 上的操作。

相关概念构成了 Spark 的整体架构,如下图所示:

在 Spark 中,一个任务的执行过程大致分为 4 个阶段,如下图所示:

  1. 定义 RDD 的 Transformations 和 Actions 算子 3,并根据这些算子形成 DAG。
  2. 根据形成的 DAG,DAGScheduler 将其划分为多个 Stage,每个 Stage 包含多个 Task。
  3. DAGScheduler 将 TaskSet 交由 TaskScheduler 运行,并将执行完毕后的结果返回给 DAGScheduler。
  4. TaskScheduler 将任务分发到每一个 Worker 去执行,并将执行完毕后的结果返回给 TaskScheduler。

Spark 相比于 Hadoop 的主要改进有如下几点:

  1. Hadoop 的 MapReduce 的中间结果都会持久化到磁盘上,而 Spark 则采用基于内存的计算(内存不足时也可选持久化到磁盘上),从而减少 Shuffle 数据,进而提升计算速度。
  2. Spark 采用的 DAG 相比于 Hadoop 的 MapReduce 具有更好的容错性和可恢复性,由于 Spark 预先计算出了整个任务的 DAG,相比于 MapReduce 中各个操作之间是独立的,这更有助于进行全局优化。

Presto

Presto 是一种用于大数据的高性能分布式 SQL 查询引擎。Presto 与 Hive 执行任务过程的差异如下图所示:

Presto 的优点主要有如下几点:

  1. 基于内存计算,减少了磁盘 IO,从而计算速度更快。
  2. 能够连接多个数据源,跨数据源连表查询。

虽然 Presto 能够处理 PB 级数据,但并不代表 Presto 会把 PB 级别数据都放在内存中计算。而是根据场景,例如 COUNTAVG 等聚合操作,是边读数据边计算,再清理内存,再读取数据计算,这种情况消耗的内存并不高。但是连表查询,可能产生大量的临时数据,从而速度会变慢。

性能调优

本节关于 SQL 性能调优的建议主要针对 Hive,Spark 和 Presto 这类大数据 OLAP 执行引擎设计,其他执行引擎不一定完全适用。

下文性能调优中均以如下两张表为例进行说明:

CREATE TABLE IF NOT EXISTS sku_order
(
  order_id STRING '订单 ID',
  sku_id STRING '商品 ID',
  sale_quantity BIGINT '销售数量' 
)
COMMENT '商品订单表'
PARTITIONED BY
(
  dt STRING COMMENT '日期分区'
)
;
CREATE TABLE IF NOT EXISTS sku_info
(
  sku_id STRING '商品 ID',
  sku_name STRING '商品名称',
  category_id STRING '品类 ID',
  category_name STRING '品类名称'
)
COMMENT '商品信息表'

减少数据量

数据倾斜

在 Shuffle 阶段,需要将各节点上相同的 Key 拉取到某个节点(Task)上处理,如果某个 Key 对应的数据量特别大则会产生数据倾斜。结果就是该 Task 运行的时间要远远大于其他 Task 的运行时间,从而造成作业整体运行缓慢,数据量过大甚至可能导致某个 Task 出现 OOM。

在 SQL 中主要有如下几种情况会产生数据倾斜:

对于不同的数据倾斜情况,解决方案如下:

set spark.sql.autoBroadcastJoinThreshold=10485760;

其他建议

参数调优

除了 SQL 本身逻辑的优化外,执行引擎的相关参数设置也会影响 SQL 的执行性能。本小节以 Spark 引擎为例,总结相关参数的设置及其影响。

动态分区

/* 以下 Hive 参数对 Spark 同样有效 */

/* 是否启用动态分区功能 */
set hive.exec.dynamic.partition=true;

/* strict 表示至少需要指定一个分区,nonstrict 表示可以全部动态指定分区 */
set hive.exec.dynamic.partition.mode=nonstrict;

/* 动态生成分区的最大数量 */
set hive.exec.max.dynamic.partitions=1000;

资源申请

/* 每个 Executor 中的核数 */
set spark.executor.cores=2;

/* Executor 的内存总量。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */
set spark.executor.memory=4G;

/* Executor 的堆外内存大小,由 YARN 控制,单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */
set spark.yarn.executor.memoryOverhead=1024;

/* Driver 的内存总量,主要用于存放任务执行过程中 Shuffle 元数据,以及任务中 Collect 的数据,Broadcast 的小表也会先存放在 Driver 中。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */
set spark.driver.memory=8G;

/* Driver 的堆外内存,由 YARN 控制,单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */
set spark.yarn.driver.memoryOverhead=1024;

/* storage memory + execution memory 占总内存(java heap-reserved memory)的比例。executor jvm 中内存分为 storage、execution 和 other 内存。storage 存放缓存 RDD 数据,execution 存放 Shuffle 过程的中间数据,other 存放用户定义的数据结构或 Spark 内部元数据。如果用户自定义数据结构较少,可以将该参数比例适当上调。 */
set spark.memory.fraction=0.7;

动态分配

开启动态分配,Spark 可以根据当前作业负载动态申请和释放资源:

set spark.dynamicAllocation.enabled=true;

同时需要设置同一时刻可以申请的最小和最大 Executor 数量:

set spark.dynamicAllocation.minExecutors=10;
set spark.dynamicAllocation.maxExecutors=100;

小文件合并

/* 小文件合并阈值,如果生成的文件平均大小低于阈值会额外启动一轮 Stage 进行小文件的合并,默认不合并小文件。 */
set spark.sql.mergeSmallFileSize=67108864;

/* 	设置额外的合并 Job 时的 Map 端输入大小 */
set spark.sql.targetBytesInPartitionWhenMerge=67108864;

/* 设置 Map 端输入的合并文件大小 */
set spark.hadoopRDD.targetBytesInPartition=67108864;

在决定一个目录是否需要合并小文件时,会统计目录下的平均大小,然后和 spark.sql.mergeSmallFileSize 比较。在合并文件时,一个 Map Task 读取的数据量取决于下面三者的较大值:spark.sql.mergeSmallFileSizespark.sql.targetBytesInPartitionWhenMergespark.hadoopRDD.targetBytesInPartition

Shuffle 相关

当大表 JOIN 小表时,如果小表足够小,可以将小表广播到所有 Executor 中,在 Map 阶段完成 JOIN。如果该值设置太大,容易导致 Executor 出现 OOM。

/* 10 * 1024 * 1024, 10MB */
set spark.sql.autoBroadcastJoinThreshold=10485760;

设置 Reduce 阶段的分区数:

set spark.sql.shuffle.partitions=1000;

设置过大可能导致很多 Reducer 同时向一个 Mapper 拉取数据,导致 Mapper 由于请求压力过大而挂掉或响应缓慢,从而 fetch failed。

一些其他 Shuffle 相关的配置如下:

/* 同一时刻一个 Reducer 可以同时拉取的数据量大小 */
set spark.reducer.maxSizeInFlight=25165824;

/* 同一时刻一个 Reducer 可以同时产生的请求数 */
set spark.reducer.maxReqsInFlight=10;

/* 同一时刻一个 Reducer 向同一个上游 Executor 拉取的最多 Block 数 */
set spark.reducer.maxBlocksInFlightPerAddress=1;

/* Shufle 请求的 Block 超过该阈值就会强制落盘,防止一大堆并发请求将内存占满 */
set spark.reducer.maxReqSizeShuffleToMem=536870911;

/* Shuffle 中连接超时时间,超过该时间会 fetch failed */
set spark.shuffle.io.connectionTimeout=120;

/* Shuffle 中拉取数据的最大重试次数 */
set spark.shuffle.io.maxRetries=3;

/* Shuffle 重试的等待间隔 */
set spark.shuffle.io.retryWait=5;

ORC 相关

ORC 文件的格式如下图所示:

其中,Postscript 为文件描述信息,包括 File Footer 和元数据长度、文件版本、压缩格式等;File Footer 是文件的元数据信息,包括数据量、每列的统计信息等;文件中的数据为 Stripe,每个 Stripe 包括索引数据、行数据和 Stripe Footer。更多有关 ORC 文件格式的信息请参见 ORC Specification v1

在读取 ORC 压缩表时,可以控制生成 Split 的策略,包括:

对于一些较大的 ORC 表,可能其 Footer 较大,ETL 策略可能会导致从 HDFS 拉取大量的数据来切分 Split,甚至会导致 Driver 端 OOM,因此这类表的读取建议采用 BI 策略。对于一些较小,尤其是有数据倾斜的表(即大量 Stripe 存储于少数文件中),建议使用 ETL 策略。

一些其他 ORC 相关的配置如下:

/* ORC 谓词下推,默认是关闭 */
set spark.sql.orc.filterPushdown=true;

/* 	开启后,在 Split 划分时会使用 Footer 信息 */
set spark.sql.orc.splits.include.file.footer=true;

/* 设置每个 Stripe 可以缓存的大小 */
set spark.sql.orc.cache.stripe.details.size=10000;

/* 当为 true 时,Spark SQL 的谓语将被下推到 Hive Metastore 中,更早的消除不匹配的分区。 */
set spark.sql.hive.metastorePartitionPruning=true;

/* 读 ORC 表时,设置小文件合并的阈值,低于该值的 Split 会合并在一个 Task 中执行 */
set spark.hadoop.mapreduce.input.fileinputformat.split.minsize=67108864;

/* 读 ORC 表时,设置一个 Split 的最大阈值,大于该值的 Split 会切分成多个 Split。 */
set spark.hadoop.mapreduce.input.fileinputformat.split.maxsize=268435456;

/* 文件提交到HDFS上的算法:1. version=1 是按照文件提交。2. version=2 是批量按照目录进行提交,可以极大节约文件提交到 HDFS 的时间,减轻 NameNode 压力。 */
set spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version=2;

自适应执行

/* 开启动态执行 */
set spark.sql.adaptive.enabled=true;

当自适应执行开启后,调整 spark.sql.adaptive.shuffle.targetPostShuffleInputSize,当 Mapper 端两个 Partition 的数据合并后小于该值时,Spark 会将两个 Partition 合并到一个 Reducer 进行处理。

set spark.sql.adaptive.shuffle.targetPostShuffleInputSize=67108864;

当自适应执行开启后,有时会导致过多分区被合并,为了防止分区过少影响性能,可以设置如下参数:

set spark.sql.adaptive.minNumPostShufflePartitions=10;

一些其他自适应执行相关的配置如下:

/* 开启动态调整 Join */
set spark.sql.adaptive.join.enabled=true;

/* 设置 SortMergeJoin 转 BroadcastJoin 的阈值,如果不设置该参数,该阈值和 spark.sql.autoBroadcastJoinThreshold 值相等。 */
set spark.sql.adaptiveBroadcastJoinThreshold=33554432;

/* 是否允许为了优化 Join 而增加 Shuffle,默认是 false */
set spark.sql.adaptive.allowAddititionalShuffle=false;

/* 开启自动处理 Join 时的数据倾斜 */
set spark.sql.adaptive.skewedJoin.enabled=true;

/* 控制处理一个倾斜 Partition 的 Task 个数上限,默认值是 5 */
set spark.sql.adaptive.skewedPartitionMaxSplits=100;

/* 设置一个 Partition 被视为倾斜 Partition 的行数下限,行数低于该值的 Partition 不会被当做倾斜 Partition 处理。 */
set spark.sql.adaptive.skewedPartitionRowCountThreshold=10000000;

/* 设置一个 Partition 被视为倾斜 Partition 的大小下限,大小小于该值的 Partition 不会被当做倾斜 Partition 处理。 */
set spark.sql.adaptive.skewedPartitionSizeThreshold=536870912;

/* 设置倾斜因子,当一个 Partition 满足以下两个条件之一,就会被视为倾斜 Partition:1. 大小大于 spark.sql.adaptive.skewedPartitionSizeThreshold 的同时大于各 Partition 大小中位数与该因子的乘积。2. 行数大于 spark.sql.adaptive.skewedRowCountThreshold 的同时大于各 Partition 行数中位数与该因子的乘积。*/
set spark.sql.adaptive.skewedPartitionFactor=10;

推测执行

/* Spark 推测执行开关,默认是 true */
set spark.speculation=true;

/* 开启推测执行后,每隔该值时间会检测是否有需要推测执行的 Task */
set spark.speculation.interval=1000ms;

/* 当成功 Task 占总 Task 的比例超过 spark.speculation.quantile,统计成功 Task 运行时间中位数乘以 spark.speculation.multiplier 得到推测执行阈值,当在运行的任务超过这个阈值就会启动推测执行。当资源充足时,可以适当减小这两个值。 */
set spark.speculation.quantile=0.99;
set spark.speculation.multiplier=3;

SQL 样式指南 (SQL Style Guide)

2021-05-04 08:00:00

代码样式指南主要用于规范项目中代码的一致性,使得代码简单、可读和易于维护,从一定程度上也影响代码的质量。一句话概括如何评价代码的质量:

衡量代码质量的唯一有效标准:WTF/min – Robert C. Martin

Google 针对大多数编程语言(例如:C/C++,Java,JavaScript,Python,R 等)都整理了相关的代码风格,但对于 SQL 这种用于数据库查询特殊目的的编程语言并没有整理对应的风格。同其他编程语言代码风格一样,没有哪种风格是最好的,只要在项目中采用统一合理的风格即可。

本文参考的 SQL 样式指南有如下几种:

  1. https://www.sqlstyle.guide/zh/
  2. https://about.gitlab.com/handbook/business-technology/data-team/platform/sql-style-guide/
  3. https://docs.telemetry.mozilla.org/concepts/sql_style.html
  4. https://github.com/mattm/sql-style-guide

本文给出的 SQL 样式指南基于上述几种进行整理和修改。

一般原则

命名惯例

对齐和换行

明确指定

子查询

其他

进程,线程和协程 (Process, Thread and Coroutine)

2021-04-03 08:00:00

理论篇请参见:进程,线程和协程 (Process, Thread and Coroutine) - 理论篇

本文将介绍进程,线程和协程在 Python 中的实现,代码详见这里,部分参考自「Python 并发编程」 1:。

进程和线程

在 Python 中可以使用 multiprocessing.Processthreading.Thread 来实现进程和线程。我们采用CPU 密集型磁盘 IO 密集型网络 IO 密集型模拟 IO 密集型任务类型来测试单线程,多线程和多进程之间的性能差异。

import requests

# CPU 密集型
def cpu_bound_task(x=1, y=1):
    c = 0

    while c < 1500000:
        c += 1
        x += x
        y += y

# 磁盘 IO 密集型
def disk_io_bound_task():
    with open('tmp.log', 'w') as f:
        for idx in range(5000000):
            f.write('{}\n'.format(idx))

# 网络 IO 密集型
def web_io_bound_task():
    try:
        requests.get('https://www.baidu.com')
    except Exception as e:
        pass

# 模拟 IO 密集型
def simulation_io_bound_task():
    time.sleep(2)

为了方便统计运行时间,定义如下一个运行时间装饰器:

import time

def timer(task_mode):
    def wrapper(func):
        def decorator(*args, **kwargs):
            task_type = kwargs.setdefault('task_type', None)
            start_time = time.time()
            func(*args, **kwargs)
            end_time = time.time()
            print('耗时({} - {}): {}'.format(
                task_mode, task_type, end_time - start_time))
        return decorator
    return wrapper

单线程,多线程和多进程的测试代码如下:

from threading import Thread
from multiprocessing import Process

@timer('单线程')
def single_thread(func, task_type='', n=10):
    for idx in range(n):
        func()


@timer('多线程')
def multi_threads(func, task_type='', n=10):
    threads = {}

    for idx in range(n):
        t = Thread(target=func)
        threads[idx] = t
        t.start()

    for thread in threads.values():
        thread.join()


@timer('多进程')
def multi_processes(func, task_type='', n=10):
    processes = {}

    for idx in range(n):
        p = Process(target=func)
        processes[idx] = p
        p.start()

    for process in processes.values():
        process.join()

运行测试

# 单线程
single_thread(cpu_bound_task, task_type='CPU 密集型任务')
single_thread(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
single_thread(web_io_bound_task, task_type='网络 IO 密集型任务')
single_thread(simulation_io_bound_task, task_type='模拟 IO 密集型任务')

# 多线程
multi_threads(cpu_bound_task, task_type='CPU 密集型任务')
multi_threads(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
multi_threads(web_io_bound_task, task_type='网络 IO 密集型任务')
multi_threads(simulation_io_bound_task, task_type='模拟 IO 密集型任务'

# 多进程
multi_processes(cpu_bound_task, task_type='CPU 密集型任务')
multi_processes(disk_io_bound_task, task_type='磁盘 IO 密集型任务')
multi_processes(web_io_bound_task, task_type='网络 IO 密集型任务')
multi_processes(simulation_io_bound_task, task_type='模拟 IO 密集型任务')

可以得到类似如下的结果:

单线程 多线程 多进程
CPU 密集型 83.42 93.82 9.08
磁盘 IO 密集型 15.64 13.27 1.28
网络 IO 密集型 1.13 0.18 0.13
模拟 IO 密集型 20.02 2.02 2.01

从测试结果来看,不难得出如下结论:

  1. 多线程和多进程相比单线程速度整体上有很大提升。
  2. 对于 CPU 密集型任务,由于 GIL 加锁和释放问题,多线程相比单线程更慢。
  3. 多线程更适合在 IO 密集场景下使用,例如:爬虫等。
  4. 多进程更适合在 CPU 密集场景下使用,例如:大数据处理,机器学习等。

创建线程有两种方式:

利用函数创建线程

Python 中的 threading.Thread() 接受两个参数:线程函数,用于指定线程执行的函数;线程函数参数,以元组的形式传入执行函数所需的参数。

import time
from threading import Thread

# 自定义函数
def func(name='Python'):
    for idx in range(2):
        print('Hello, {}'.format(name))
        time.sleep(1)

# 创建线程
thread_1 = Thread(target=func)
thread_2 = Thread(target=func, args=('Leo', ))

# 启动线程
thread_1.start()
thread_2.start()

可以得到如下输出:

Hello, Python
Hello, Leo
Hello, Python
Hello, Leo

利用类创建线程

利用类创建线程需要自定义一个类并继承 threading.Thread 这个父类,同时重写 run 方法。最后通过实例化该类,并运行 start() 方法执行该线程。

# 自定义类
class MyThread(Thread):
    def __init__(self, name='Python'):
        super(MyThread, self).__init__()
        self.name = name

    def run(self):
        for idx in range(2):
            print('Hello, {}'.format(self.name))
            time.sleep(1)

# 创建线程
thread_1 = MyThread()
thread_2 = MyThread('Leo')

# 启动线程
thread_1.start()
thread_2.start()

可以得到同上面一样的输出:

Hello, Python
Hello, Leo
Hello, Python
Hello, Leo

线程的一些常用方法和属性如下所示:

# 创建线程
t = Thread(target=func)

# 启动线程
t.start()

# 阻塞线程
t.join()

# 判断线程是否处于执行状态
# True: 执行中,False: 其他
t.is_alive()

# 这是线程是否随主线程退出而退出
# 默认为 False
t.daemon = True

# 设置线程名称
t.name = 'My Thread'

在一段代码中加锁表示同一时间有且仅有一个线程可以执行这段代码。在 Python 中锁分为两种:互斥锁可重入锁。利用 threading.Lock() 可以获取全局唯一的锁对象,使用 acquire()release() 方法可以获取和释放锁,注意两个需成对出现,否则可能造成死锁。

互斥锁

例如定义两个函数,并在两个线程中执行,这两个函数共用一个变量 C

import time
import random

from threading import Thread

# 共用变量
C = 0

def job1(n=10):
    global C

    for idx in range(n):
        C += 1
        print('Job1: {}'.format(C))

def job2(n=10):
    global C

    for idx in range(n):
        C += 10
        print('Job2: {}'.format(C))

t1 = Thread(target=job1)
t2 = Thread(target=job2)

t1.start()
t2.start()

运行结果如下:

Job1: 1
Job2: 11
Job2: 21
Job1: 22
Job1: 23
Job2: 33
Job2: 43
Job1: 44
Job2: 54
Job1: 55
Job2: 65
Job1: 66
Job2: 76
Job2: 86
Job1: 87
Job1: 88
Job2: 98
Job1: 99
Job2: 109
Job1: 110

两个线程共用一个全局变量,两个线程根据自己执行的快慢对变量 C 进行修改。在增加锁后:

import time
import random

from threading import Lock

# 全局唯一锁
LOCK = Lock()

# 共用变量
C = 0

def job1_with_lock(n=10):
    global C, LOCK

    LOCK.acquire()

    for idx in range(n):
        C += 1
        print('Job1: {}'.format(C))
        time.sleep(random.random())

    LOCK.release()


def job2_with_lock(n=10):
    global C, LOCK

    LOCK.acquire()

    for idx in range(n):
        C += 10
        print('Job2: {}'.format(C))
        time.sleep(random.random())

    LOCK.release()

t1 = Thread(target=job1_with_lock)
t2 = Thread(target=job2_with_lock)

t1.start()
t2.start()

运行结果如下:

Job1: 1
Job1: 2
Job1: 3
Job1: 4
Job1: 5
Job1: 6
Job1: 7
Job1: 8
Job1: 9
Job1: 10
Job2: 20
Job2: 30
Job2: 40
Job2: 50
Job2: 60
Job2: 70
Job2: 80
Job2: 90
Job2: 100
Job2: 110

此时,由于 job1_with_lock 先拿到了锁,所以当执行时 job2_with_lock 无法获取到锁,就无法对 C 进行修改。只有当 job1_with_lock 执行完毕释放锁后,job2_with_lock 才能执行对 C 的修改操作。为了避免忘记释放锁,可以使用 with 上下文管理器来加锁。

可重入锁

在同一个线程中,我们可能会多次请求同一个资源,这称为嵌套锁。如果使用常规的方式:

from threading import Lock

def lock_with_lock(n=10):
    c = 0
    lock = Lock()

    with lock:
        for idx in range(n):
            c += 1
            with lock:
                print(c)

t = Thread(target=lock_with_lock)
t.start()

则无法正常运行,因为第二次获取锁时,锁已经被同一线程获取,从而无法运行后续代码。由于后续代码无法运行则无法释放锁,从而上述的嵌套锁会造成死锁

为了解决这个问题,threading 模块提供了可重入锁 RLock

from threading import RLock

def rlock_with_lock(n=10):
    c = 0
    lock = RLock()

    with lock:
        for idx in range(n):
            c += 1
            with lock:
                print(c)
  
t = Thread(target=rlock_with_lock)
t.start()

运行结果如下:

1
2
3
4
5
6
7
8
9
10

全局解释器锁

全局解释器锁(Global Interpreter Lock,GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。

任何 Python 线程执行前,必须先获得 GIL 锁,然后,每执行 100 条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在 Python 中只能交替执行,即使 100 个线程跑在 100 核 CPU 上,也只能用到 1 个核。

通信

Python 中实现线程中通信有如下 3 中方法:

Event 事件

threading.Event 可以创建一个事件变量,多个线程等待这个事件的发生,在事件发生后,所有线程继续运行。threading.Event 包含如下三个函数:

event = threading.Event()

# 重置 event,使得所有该 event 事件都处于待命状态
event.clear()

# 等待接收 event 的指令,决定是否阻塞程序执行
event.wait()

# 发送 event 指令,使所有设置该 event 事件的线程执行
event.set()

例如:

import time

from threading import Thread, Event

class EventThread(Thread):
    def __init__(self, name, event):
        super(EventThread, self).__init__()

        self.name = name
        self.event = event

    def run(self):
        print('线程 {} 启动于 {}'.format(self.name, time.ctime(time.time())))
        self.event.wait()
        print('线程 {} 结束于 {}'.format(self.name, time.ctime(time.time())))

threads = {}
event = Event()

for tid in range(3):
    threads[tid] = EventThread(str(tid), event)

event.clear()

for thread in threads.values():
    thread.start()

print('等待 3 秒钟 ...')
time.sleep(3)

print('唤醒所有线程 ...')
event.set() 

运行结果如下:

线程 0 启动于 Thu Apr  1 23:12:32 2021
线程 1 启动于 Thu Apr  1 23:12:32 2021
线程 2 启动于 Thu Apr  1 23:12:32 2021
等待 3 秒钟 ...
唤醒所有线程 ...
线程 0 结束于 Thu Apr  1 23:12:35 2021
线程 1 结束于 Thu Apr  1 23:12:35 2021
线程 2 结束于 Thu Apr  1 23:12:35 2021

可见线程启动后并未执行完成,而是卡在了 event.wait() 处,直到通过 event.set() 发送指令后,所有线程才继续向下执行。

Condition

threading.Conditionthreading.Event 类似,包含如下 4 个函数:

cond = threading.Condition()

# 类似 lock.acquire()
cond.acquire()

# 类似 lock.release()
cond.release()

# 等待指定触发,同时会释放对锁的获取,直到被 notify 才重新占有琐。
cond.wait()

# 发送指定,触发执行
cond.notify()

以一个捉迷藏的游戏为例:

import time

from threading import Thread, Condition

class Seeker(Thread):
    def __init__(self, condition, name):
        super(Seeker, self).__init__()

        self.condition = condition
        self.name = name

    def run(self):
        time.sleep(1) # 确保先运行 Hider 中的方法

        self.condition.acquire()

        print('{}: 我把眼睛蒙好了'.format(self.name))
        self.condition.notify()
        self.condition.wait()
        print('{}: 我找到你了'.format(self.name))
        self.condition.notify()

        self.condition.release()
        print('{}: 我赢了'.format(self.name))


class Hider(Thread):
    def __init__(self, condition, name):
        super(Hider, self).__init__()

        self.condition = condition
        self.name = name

    def run(self):
        self.condition.acquire()

        self.condition.wait()
        print('{}: 我藏好了'.format(self.name))
        self.condition.notify()
        self.condition.wait()
        self.condition.release()
        print('{}: 被你找到了'.format(self.name))

condition = Condition()

seeker = Seeker(condition, 'Seeker')
hider = Hider(condition, 'Hider')

seeker.start()
hider.start()

运行结果如下:

Seeker: 我把眼睛蒙好了
Hider: 我藏好了
Seeker: 我找到你了
Seeker: 我赢了
Hider: 被你找到了

可见通过 cond.wait()cond.notify() 进行阻塞和通知可以实现双方动作交替进行。

Queue 队列

从一个线程向另一个线程发送数据最安全的方式是使用 queue 库中的队列。创建一个被多个线程共享的队列对象,通过 put()get() 方法向队列发送和获取元素。队列的常用方法如下:

from queue import Queue

# maxsize=0 表示不限大小
# maxsize>0 且消息数达到限制时,put() 方法会阻塞
q = Queue(maxsize=0)

# 默认阻塞程序,等待队列消息,可设置超时时间
q.get(block=True, timeout=None)

# 发送消息,默认会阻塞程序至队列中有空闲位置放入数据
q.put(item, block=True, timeout=None)

# 等待所有的消息都被消费完
q.join()

# 通知队列任务处理已经完成,当所有任务都处理完成时,join() 阻塞将会解除
q.task_done()

# 查询当前队列的消息个数
q.qsize()

# 队列消息是否都被消费完,返回 True/False
q.empty()

# 检测队列里消息是否已满
q.full()

以老师点名为例:

import time

from queue import Queue
from threading import Thread

class Student(object):
    def __init__(self, name):
        super(Student, self).__init__()

        self.name = name

    def speak(self):
        print('{}: 到'.format(self.name))


class Teacher(object):
    def __init__(self, queue):
        super(Teacher, self).__init__()

        self.queue = queue

    def call(self, student_name):
        if student_name == 'exit':
            print('老师: 点名结束,开始上课')
        else:
            print('老师: {}'.format(student_name))

        self.queue.put(student_name)


class CallManager(Thread):
    def __init__(self, queue):
        super(CallManager, self).__init__()

        self.students = {}
        self.queue = queue

    def put(self, student):
        self.students.setdefault(student.name, student)

    def run(self):
        while True:
            student_name = self.queue.get()

            if student_name == 'exit':
                break
            elif student_name in self.students:
                self.students[student_name].speak()
            else:
                print('学生: 老师,没有 {} 这个人'.format(student_name))

queue = Queue()

teacher = Teacher(queue=queue)
s1 = Student(name='张三')
s2 = Student(name='李四')

cm = CallManager(queue)
cm.put(s1)
cm.put(s2)

cm.start()

print('开始点名')
teacher.call('张三')
time.sleep(1)
teacher.call('李四')
time.sleep(1)
teacher.call('王五')
time.sleep(1)
teacher.call('exit')

运行结果如下:

开始点名
老师: 张三
张三: 到
老师: 李四
李四: 到
老师: 王五
学生: 老师,没有 王五 这个人
老师: 点名结束,开始上课

除了先进先出队列 queue.Queue 外,还有后进先出队列 queue.LifoQueue 和优先级队列 queue.PriorityQueue

进程池和线程池

是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。

池的概念主要目的是为了重用:让线程或进程在生命周期内可以多次使用。它减少了创建创建线程和进程的开销,以空间换时间来提高了程序性能。重用不是必须的规则,但它是程序员在应用中使用池的主要原因。

Python 中利用 concurrent.futures 库中的 ThreadPoolExecutorProcessPoolExecutor 创建线程池进程池。示例如下:

import time
import threading

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed


def print_func(n=3):
    for idx in range(n):
        print('运行 {}-{}'.format(threading.get_ident(), idx))
        time.sleep(1)


def return_func(n=3):
    res = []

    for idx in range(n):
        res.append('{}-{}'.format(threading.get_ident(), idx))
        time.sleep(1)

    return res


def test_thread_pool_print(n=3, m=12):
    with ThreadPoolExecutor(max_workers=n) as executor:
        for _ in range(m):
            executor.submit(print_func)


def test_process_pool_print(n=3, m=12):
    with ProcessPoolExecutor(max_workers=n) as executor:
        for _ in range(m):
            executor.submit(print_func)


def test_thread_pool_return(n=3, m=12):
    with ThreadPoolExecutor(max_workers=n) as executor:
        futures = [executor.submit(return_func) for _ in range(m)]

        for future in as_completed(futures):
            print(future.result())


def test_process_pool_return(n=3, m=12):
    with ProcessPoolExecutor(max_workers=n) as executor:
        futures = [executor.submit(return_func) for _ in range(m)]

        for future in as_completed(futures):
            print(future.result())


line_sep = '-' * 60

print(line_sep)
print('测试线程池')
print(line_sep)
test_thread_pool_print()
print(line_sep)

print(line_sep)
print('测试进程池')
print(line_sep)
test_process_pool_print()
print(line_sep)

print(line_sep)
print('测试线程池')
print(line_sep)
test_thread_pool_return()
print(line_sep)

print(line_sep)
print('测试进程池')
print(line_sep)
test_process_pool_return()
print(line_sep)

运行结果如下:

------------------------------------------------------------
测试线程池
------------------------------------------------------------
运行 123145462505472-0
运行 123145479294976-0
运行 123145496084480-0
运行 123145496084480-1
运行 123145462505472-1
运行 123145479294976-1
运行 123145496084480-2
运行 123145462505472-2
运行 123145479294976-2
运行 123145462505472-0
运行 123145479294976-0
运行 123145496084480-0
运行 123145479294976-1
运行 123145462505472-1
运行 123145496084480-1
运行 123145479294976-2
运行 123145462505472-2
运行 123145496084480-2
------------------------------------------------------------
------------------------------------------------------------
测试进程池
------------------------------------------------------------
运行 4545199616-0
运行 4545199616-1
运行 4545199616-2
运行 4545199616-0
运行 4545199616-1
运行 4545199616-2
运行 4663131648-0
运行 4663131648-1
运行 4663131648-2
运行 4663131648-0
运行 4663131648-1
运行 4663131648-2
运行 4633173504-0
运行 4633173504-1
运行 4633173504-2
运行 4633173504-0
运行 4633173504-1
运行 4633173504-2
------------------------------------------------------------
------------------------------------------------------------
测试线程池
------------------------------------------------------------
['123145496084480-0', '123145496084480-1', '123145496084480-2']
['123145479294976-0', '123145479294976-1', '123145479294976-2']
['123145462505472-0', '123145462505472-1', '123145462505472-2']
['123145479294976-0', '123145479294976-1', '123145479294976-2']
['123145496084480-0', '123145496084480-1', '123145496084480-2']
['123145462505472-0', '123145462505472-1', '123145462505472-2']
------------------------------------------------------------
------------------------------------------------------------
测试进程池
------------------------------------------------------------
['4791307776-0', '4791307776-1', '4791307776-2']
['4588228096-0', '4588228096-1', '4588228096-2']
['4654599680-0', '4654599680-1', '4654599680-2']
['4791307776-0', '4791307776-1', '4791307776-2']
['4588228096-0', '4588228096-1', '4588228096-2']
['4654599680-0', '4654599680-1', '4654599680-2']
------------------------------------------------------------

其中,submit() 方法用于提交要执行的任务到线程池或进程池中,并返回该任务的 Future 对象。Future 对象的 done() 方法用于判断任务是否执行完毕,通过 result(timeout=None) 方法获取返回结果。利用 concurrent.futures.as_completed() 方法可以返回一个包含指定 Future 实例的迭代器,这些实例在完成时生成 Future 对象。

生成器和迭代器

图片来源:https://nvie.com/posts/iterators-vs-generators/

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用 innot in 判断元素是否包含在容器中。常见的容器对象有:

list, deque, ...
set, frozensets, ...
dict, defaultdict, OrderedDict, Counter, ...
tuple, namedtuple, ...
str

可迭代对象

很多容器都是可迭代对象,此外还有更多的对象同样也可以是可迭代对象,比如处于打开状态的 filesocket 等。凡是可以返回一个迭代器的对象都可称之为可迭代对象,例如:

from collections import deque
from collections.abc import Iterable

print(isinstance('Leo Van', Iterable))
print(isinstance([1, 2, 3], Iterable))
print(isinstance({'k1': 'v1', 'k2': 'v2'}, Iterable))
print(isinstance(deque('abc'), Iterable))

运行结果如下:

True
True
True
True

迭代器

迭代器是一个带有状态的对象,通过 next() 方法可以返回容器中的下一个值。任何实现 __iter__()__next__() 方法的对象都是迭代器。其中,__iter__() 方法返回迭代器本身,__next__() 方法返回容器中的下一个值,如果容器中没有更多元素了,则抛出 StopIteration 异常。例如:

class MyList(object):
    def __init__(self, end):
        super(MyList, self).__init__()

        self.end = end

    def __iter__(self):
        return MyListIterator(self.end)

    def __repr__(self):
        return '[{}]'.format(', '.join([str(ele) for ele in self]))


class MyListIterator(object):
    def __init__(self, end):
        super(MyListIterator, self).__init__()

        self.data = end
        self.start = 0

    def __iter__(self):
        return self

    def __next__(self):
        while self.start < self.data:
            self.start += 1
            return self.start - 1

        raise StopIteration

my_list = MyList(3)

print('MyList: {}'.format(my_list))
print(isinstance(my_list, Iterable))
print(isinstance(my_list, Iterator))

for ele in my_list:
    print(ele)

my_list_iterator = MyListIterator(3)

print('MyListIterator: {}'.format(my_list_iterator))
print(isinstance(my_list_iterator, Iterable))
print(isinstance(my_list_iterator, Iterator))

my_iterator = iter(my_list)

print('MyIterator: {}'.format(my_iterator))
print(isinstance(my_iterator, Iterable))
print(isinstance(my_iterator, Iterator))

while True:
    try:
        print(next(my_iterator))
    except StopIteration as e:
        return

运行结果如下:

MyList: [0, 1, 2]
True
False
0
1
2
MyListIterator: <__main__.MyListIterator object at 0x7fc9602b2100>
True
True
0
1
2
MyIterator: <__main__.MyListIterator object at 0x7fc9602b2fa0>
True
True
0
1
2
Stop

生成器

生成器非常类似于返回数组的函数,都是具有参数、可被调用、产生一系列的值。但是生成器并不是构造出数组包含所有的值并一次性返回,而是每次产生一个值,因此生成器看起来像函数,但行为像迭代器。

Python 中创建生成器有两种方法:使用类似列表方式或 yield 关键字:

from collections.abc import Generator
from inspect import getgeneratorstate

a_list = [x for x in range(10)]
print(a_list)
print(isinstance(a_list, Generator))

a_generator = (x for x in range(10))
print(a_generator)
print(isinstance(a_generator, Generator))

def my_yield(n):
    now = 0

    while now < n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen)
print(isinstance(gen, Generator))

运行结果如下:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
False
<generator object <genexpr> at 0x7fdf84a8a430>
True
<generator object my_yield at 0x7fdf86a03f20>
True

由于生成器并不是一次生成所有元素,而是每次执行后返回一个值,通过 next()generator.send(None) 两个方法可以激活生成器,例如:

def my_yield(n):
    now = 0

    while now < n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen.send(None))
print(next(gen))
print(gen.send(None))
print(next(gen))

运行结果如下:

0
1
2
3

生成器在其生命周期中共有 4 种状态:

例如:

from collections.abc import Generator
from inspect import getgeneratorstate

def my_yield(n):
    now = 0

    while now < n:
        yield now
        now += 1

    raise StopIteration

gen = my_yield(4)
print(gen)
print(getgeneratorstate(gen))

print(gen.send(None))
print(next(gen))
print(getgeneratorstate(gen))

print(gen.send(None))
print(next(gen))
print(getgeneratorstate(gen))

gen.close()
print(getgeneratorstate(gen))

运行结果如下:

GEN_CREATED
0
1
GEN_SUSPENDED
2
3
GEN_SUSPENDED
GEN_CLOSED

生成器在不满足生成元素的条件时,会抛出 StopIteration 异常,通过类似列表形式构建的生成器会自动实现该异常,自定的生成器则需要手动实现该异常。

协程

yield

协程通过 yield 暂停生成器,可以将程序的执行流程交给其他子程序,从而实现不同子程序之间的交替执行。例如:

def jump_range(n):
    idx = 0

    while idx < n:
        jump = yield idx
        print('[idx: {}, jump: {}]'.format(idx, jump))

        if jump is None:
            jump = 1

        idx += jump

itr = jump_range(6)
print(next(itr))
print(itr.send(2))
print(next(itr))
print(itr.send(-1))
print(next(itr))
print(next(itr))

运行结果如下:

0
[idx: 0, jump: 2]
2
[idx: 2, jump: None]
3
[idx: 3, jump: -1]
2
[idx: 2, jump: None]
3
[idx: 3, jump: None]
4

yield idxidx 返回给外部调用程序,jump = yield 可以接受外部程序通过 send() 发送的信息,并将其赋值给 jump

yield from 是 Python 3.3 之后出现的新语法,后面是可迭代对象,可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。yieldyield from 的对比如下:

a_str = 'Leo'
a_list = [1, 2, 3]
a_dict = {'name': 'Leo', 'gender': 'Male'}
a_gen = (idx for idx in range(4, 8))

def gen(*args, **kwargs):
    for item in args:
        for ele in item:
            yield ele

new_gen = gen(a_str, a_list, a_dict, a_gen)
print(list(new_gen))

a_gen = (idx for idx in range(4, 8))

def gen_from(*args, **kwargs):
    for item in args:
        yield from item

new_gen = gen_from(a_str, a_list, a_dict, a_gen)
print(list(new_gen))

运行结果如下:

['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7]
['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7]

在实现生成器的嵌套时,使用 yield from 可以比使用 yield 避免各种意想不到的异常。使用 yield from 时,需要关注如下几个概念:

如下是一个计算平均数的例子:

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0

    while True:
        num = yield average

        if num is None:
            break

        count += 1
        total += num
        average = total / count

    return total, count, average

# 委托生成器
def proxy_gen():
    while True:
        total, count, average = yield from average_gen()
        print('计算完毕,共输入 {} 个数值,总和 {},平均值 {}'.format(
            count, total, average))

# 调用方
calc_average = proxy_gen()
next(calc_average)
print(calc_average.send(10))
print(calc_average.send(20))
print(calc_average.send(30))
calc_average.send(None)

运行结果如下:

10.0
15.0
20.0
计算完毕,共输入 3 个数值,总和 60,平均值 20.0

委托生成器的作用是在调用方和子生成器之间建立一个双向通道,调用方通过 send() 将消息发送给子生成器,子生成器 yield 的值则返回给调用方。yield from 背后为整个过程做了很多操作,例如:捕获 StopIteration 异常等。

asyncio

asyncio 是 Python 3.4 引入的标准库,直接内置了对异步 IO 的支持。只要在一个函数前面加上 async 关键字就可以将一个函数变为一个协程。例如:

from collections.abc import Coroutine

async def async_func(name):
    print('Hello, ', name)

coroutine = async_func('World')
print(isinstance(coroutine, Coroutine))

运行结果如下:

True

利用 asyncio.coroutine 装饰器可以将一个生成器当作协程使用,但其本质仍旧是一个生成器。例如:

import asyncio

from collections.abc import Generator, Coroutine

@asyncio.coroutine
def coroutine_func(name):
    print('Hello,', name)
    yield from asyncio.sleep(1)
    print('Bye,', name)


coroutine = coroutine_func('World')
print(isinstance(coroutine, Generator))
print(isinstance(coroutine, Coroutine))

运行结果如下:

True
False

asyncio 中包含如下几个重要概念:

协程完整的工作流程如下:

import asyncio

async def hello(name):
    print('Hello,', name)
    
# 定义协程
coroutine = hello('World')

# 定义事件循环
loop = asyncio.get_event_loop()

# 创建任务
task = loop.create_task(coroutine)

# 将任务交由时间循环并执行
loop.run_until_complete(task)

运行结果如下:

Hello, World

await 用于挂起阻塞的异步调用接口,其作用在一定程度上类似于 yieldyield from 后面可接可迭代对象,也可接 future 对象或协程对象;await 后面必须接 future 对象或协程对象。

import asyncio
from asyncio.futures import Future

async def hello(name):
    await asyncio.sleep(2)
    print('Hello, ', name)

coroutine = hello("World")

# 将协程转为 task 对象
task = asyncio.ensure_future(coroutine)

print(isinstance(task, Future))

运行结果如下:

True

异步 IO 的实现原理就是在 IO 高的地方挂起,等 IO 结束后再继续执行。绝大部分情况下,后续代码的执行是需要依赖 IO 的返回值的,这就需要使用回调

回调的实现有两种,一种是在同步编程中直接获取返回结果:

import asyncio
import time

async def _sleep(x):
    time.sleep(x)
    return '暂停了 {} 秒'.format(x)

coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)

loop.run_until_complete(task)
print('返回结果:{}'.format(task.result()))

运行结果如下:

返回结果:暂停了 2 秒

另一种是通过添加回调函数来实现:

import asyncio
import time

async def _sleep(x):
    time.sleep(x)
    return '暂停了 {} 秒'.format(x)

def callback(future):
    print('回调返回结果:{}'.format(future.result()))

coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)

task.add_done_callback(callback)
loop.run_until_complete(task)

运行结果如下:

回调返回结果:暂停了 2 秒

asyncio 实现并发需要多个协程来完成,每当有任务阻塞时需要 await,然后让其他协程继续工作。

import asyncio

async def do_some_work(x):
    print('等待中 ...')
    await asyncio.sleep(x)
    print('{} 秒后结束'.format(x))
    return x

# 协程对象
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

# 任务列表
tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('任务结果:{}'.format(task.result()))

运行结果如下:

等待中 ...
等待中 ...
等待中 ...
1 秒后结束
2 秒后结束
4 秒后结束
任务结果:1
任务结果:2
任务结果:4

协程之间可以进行嵌套,即在一个协程中 await 另一个协程:

import asyncio

async def do_some_work(x):
    print('等待中 ...')
    await asyncio.sleep(x)
    print('{} 秒后结束'.format(x))
    return x

async def out_do_some_work():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(4)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]

    dones, pendings = await asyncio.wait(tasks)

    for task in dones:
        print('任务结果:{}'.format(task.result()))

loop = asyncio.get_event_loop()
loop.run_until_complete(out_do_some_work())

如果使用 asyncio.gather() 来获取结果,则需要对获取结果部分做如下修改:

results = await asyncio.gather(*tasks)
for result in results:
    print('任务结果:{}'.format(result))

asyncio.wait() 返回 donespendings,分别表示已完成和未完成的任务;asyncio.gather() 则会把结果直接返回。

运行结果如下:

等待中 ...
等待中 ...
等待中 ...
1 秒后结束
2 秒后结束
4 秒后结束
任务结果:1
任务结果:4
任务结果:2

协程(准确的说是 FutureTask 对象)包含如下状态:

测试代码如下:

coroutine = _sleep(10)
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)

print('Pending')

try:
    t = Thread(target=loop.run_until_complete, args=(task, ))
    t.start()
    print('Running')
    t.join()
except KeyboardInterrupt as e:
    task.cancel()
    print('Cancel')
finally:
    print('Done')

执行顺利的话,运行结果如下:

Pending
Running
Done

如果在启动后按下 Ctrl + C 则会触发 task.cancel(),运行结果如下:

Pending
Running
Cancelled
Done

asyncio.wait() 可以通过参数控制何时返回:

import random
import asyncio

async def random_sleep():
    await asyncio.sleep(random.uniform(0.5, 6))

loop = asyncio.get_event_loop()
tasks = [random_sleep() for _ in range(1, 10)]

dones, pendings = loop.run_until_complete(asyncio.wait(
    tasks, return_when=asyncio.FIRST_COMPLETED))
print('第一次完成的任务数:{}'.format(len(dones)))

dones, pendings = loop.run_until_complete(asyncio.wait(
    pendings, timeout=2))
print('第二次完成的任务数: {}'.format(len(dones)))

dones, pendings = loop.run_until_complete(asyncio.wait(pendings))
print('第三次完成的任务数:{}'.format(len(dones)))

运行结果如下:

第一次完成的任务数:1
第二次完成的任务数: 4
第三次完成的任务数:4

进程,线程和协程 (Process, Thread and Coroutine)

2021-04-01 08:00:00

Python 实现篇请参见:进程,线程和协程 (Process, Thread and Coroutine) - 实现篇

进程,线程和协程

**进程(Process)**是计算机中已运行的程序 1。**线程(Thread)**是操作系统能够进行运算调度的最小单位。大部分情况下,线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 2

进程和线程之间的主要区别在于:

  1. 线程共享创建其进程的地址空间,进程使用自己的地址。
  2. 线程可以直接访问进程的数据,进程使用其父进程数据的副本。
  3. 线程可以同其进程中其他线程直接通信,进程必须使用**进程间通讯(inter-process communicate, IPC)**与同级进程通信。
  4. 线程开销较小,进程开销较大。
  5. 线程的创建较为容易,进程需要复制其父进程。
  6. 线程可以控制相同进程的其他线程,进程只能控制其子进程。
  7. 对于主线程的修改(例如:取消、优先级修改等)可能会影响进程中的其他线程,对于父进程的修改不会影响其子进程。

单线程进程和多线程进程之间的对比如下图所示:

一个关于进程和线程的形象类比如下 3

  1. 计算机的核心是 CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
  2. 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。
  3. 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。
  4. 一个车间里,可以有很多工人。他们协同完成一个任务。
  5. 线程就好比车间里的工人。一个进程可以包括多个线程。
  6. 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
  7. 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
  8. 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual Exclusion,Mutex),防止多个线程同时读写某一块内存区域。
  9. 还有些房间,可以同时容纳 $n$ 个人,比如厨房。也就是说,如果人数大于 $n$,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
  10. 这时的解决方法,就是在门口挂 $n$ 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore),用来保证多个线程不会互相冲突。不难看出,Mutex 是 Semaphore 的一种特殊情况($n = 1$ 时)。也就是说,完全可以用后者替代前者。但是,因为 Mutex 较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
  11. 操作系统的设计,因此可以归结为三点:(1). 以多进程形式,允许多个任务同时运行;(2). 以多线程形式,允许单个任务分成不同的部分运行;(3). 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。

子例程(Subroutine),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。

协程和子例程的执行过程对比如下:

协程类似于线程,但是协程是协作式多任务的,而线程是抢占式多任务的。这意味着协程提供并发性而非并行性。协程超过线程的好处是它们可以用于硬性实时的语境(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用),这里不需要用来守卫临界区段同步原语比如互斥锁、信号量等,并且不需要来自操作系统的支持。

通信

进程间通信

管道

管道(Pipeline)是一系列将标准输入输出链接起来的进程,其中每一个进程的输出被直接作为下一个进程的输入。 例如:

ls -l | less

ls 用于在 Unix 下列出目录内容,less 是一个有搜索功能的交互式的文本分页器。这个管道使得用户可以在列出的目录内容比屏幕长时目录上下翻页。

命名管道

命名管道是计算机进程间的一种先进先出通信机制。是类 Unix 系统传统管道的扩展。传统管道属于匿名管道,其生存期不超过创建管道的进程的生存期。但命名管道的生存期可以与操作系统运行期一样长。

信号

信号(Signals)是 Unix、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

例如,在一个运行的程序的控制终端键入特定的组合键可以向它发送某些信号:Ctrl + C 发送 INT 信号(SIGINT),这会导致进程终止;Ctrl + Z 发送 TSTP 信号(SIGTSTP),这会导致进程挂起。

消息队列

消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。

消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。

信号量

信号量(Semaphore)又称为信号标,是一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。当线程完成一次对该 Semaphore 对象的等待(wait)时,该计数值减一;当线程完成一次对 Semaphore 对象的释放(release)时,计数值加一。当计数值为 0,则线程等待该 Semaphore 对象不再能成功直至该 Semaphore 对象变成 signaled 状态。Semaphore 对象的计数值大于 0,为 signaled 状态;计数值等于 0,为 nonsignaled 状态.

共享内存

共享内存指可被多个进程存取的内存,一个进程是一段程序的单个运行实例。在这种情况下,共享内存被用作进程间的通讯。

伯克利套接字

伯克利套接字(Internet Berkeley Sockets),又称为 BSD 套接字是一种应用程序接口,主要用于实现进程间通讯,在计算机网络通讯方面被广泛使用。

线程间通信

锁机制

信号

同上文。

信号量

同上文。

胶囊网络 (Capsule Network)

2021-03-14 08:00:00

CNN 的缺陷

卷积神经网络(CNN)在图像领域取得了很大的成功,但同时也存在一定的缺陷。卷积层中的卷积核对输入图像利用卷积运算提取其中的特征。卷积核以一个较小的尺寸并以一定的步长在图像上移动得到特征图。步长越大,特征图的尺寸就越小,过大的步长会丢失部分图像中的特征。池化层作用于产生的特征图上,使得 CNN 可以在不同形式的图像中识别出相同的物体,为 CNN 引入了空间不变性。

CNN 最大的缺陷就是忽略了不同特征之间的相对位置,从而无法从图像中识别出姿势、纹理和变化。CNN 中的池化操作使得模型具有空间不变性,因此模型就不具备等变性。以下图为例,CNN 会把第一幅和第二幅识别为人脸,而将第三幅方向翻转的图识别为不是人脸。池化操作造成了部分信息的丢失,因此需要更多的训练数据来补偿这些损失。

图片来源:https://www.spiria.com/en/blog/artificial-intelligence/deep-learning-capsule-network-revolution/

等变和不变

对于一个函数 $f$ 和一个变换 $g$,如果有:

$$ f \left(g \left(x\right)\right) = g \left(f \left(x\right)\right) $$

则称 $f$ 对变换 $g$等变性

例如,变换 $g$ 为将图像向左平移若干像素,函数 $f$ 表示检测一个人脸的位置。则 $f \left(g \left(x\right)\right)$ 表示先将图片左移,我们将在原图的左侧检测到人脸;$g \left(f \left(x\right)\right)$ 表示先检测人脸位置,然后将人脸位置左移。这两者的输出结果是一样的,与我们施加变换的顺序无关。CNN 中的卷积操作使得它对平移操作具有等变性。

对于一个函数 $f$ 和一个变换 $g: g \left(x\right) = x'$,如果有:

$$ f \left(x\right) = f \left(x'\right) = f \left(g \left(x\right)\right) $$

则称 $f$ 对变换 $g$不变性

例如,变换 $g$ 为旋转或平移,函数 $f$ 表示检测图中是否有黑色,那么这些变换不会对函数结果有任何影响,可以说函数对该变换具有不变性。CNN 中的池化操作对平移操作具有近似不变性。

逆图形

计算机图形学根据几何数据的内部层次结构来构造可视图像,该表示的结构将对象的相对位置考虑在内。软件采用层次的表示方式将其渲染为屏幕上的图像。人类大脑的工作原理则与渲染过程相反,我们称其为逆图形。大脑中对象的表示并不依赖于视角。

例如下图,人眼可以很容易的分辨出是自由女神像,只是角度不同,但 CNN 却很难做到,因为它不理解 3D 空间的内在。

胶囊网络

胶囊

在引入“胶囊”这个概念的第一篇文献 Transforming Auto-encoders 1 中,Hinton 等人对胶囊概念理解如下:

人工神经网络不应当追求“神经元”活动中的视角不变性(使用单一的标量输出来总结一个局部池中的重复特征检测器的活动),而应当使用局部的“胶囊”,这些胶囊对其输入执行一些相当复杂的内部计算,然后将这些计算的结果封装成一个包含信息丰富的输出的小向量。每个胶囊学习辨识一个有限的观察条件和变形范围内隐式定义的视觉实体,并输出实体在有限范围内存在的概率及一组“实例参数”,实例参数可能包括相对这个视觉实体的隐式定义的典型版本的精确的位姿、照明条件和变形信息。当胶囊工作正常时,视觉实体存在的概率具有局部不变性——当实体在胶囊覆盖的有限范围内的外观流形上移动时,概率不会改变。实例参数却是“等变的”——随着观察条件的变化,实体在外观流形上移动时,实例参数也会相应地变化,因为实例参数表示实体在外观流形上的内在坐标。

人造神经元输出单个标量。对于 CNN 卷积层中的每个卷积核,对整个输入图复制同一个内核的权重输出一个二维矩阵。矩阵中每个数字是该卷积核对输入图像一部分的卷积,这个二维矩阵看作是重复特征检测器的输出。所有卷积核的二维矩阵堆叠在一起得到卷积层的输出。CNN 利用最大池化实现不变性,但最大池化丢失了有价值的信息,也没有编码特征之间的相对空间关系。

胶囊将特征检测的概率作为其输出向量的长度进行编码,检测出的特征的状态被编码为该向量指向的方向。当检测出的特征在图像中移动或其状态发生变化时,概率仍然保持不变(向量的长度没有改变),但它的方向改变了。

下表总结了胶囊和神经元的不同:

Capsule Traditional Neuron
Input from low-level capsule/neuron $\text{vector}\left(\mathbf{u}_i\right)$ $\text{scalar}\left(x_i\right)$
Operration Affine Transform $\widehat{\mathbf{u}}_{j \mid i}=\mathbf{W}_{i j} \mathbf{u}_{i}$ -
Weighting $\mathbf{s}_{j}=\sum_{i} c_{i j} \widehat{\mathbf{u}}_{j \mid i}$ $a_{j}=\sum_{i} w_{i} x_{i}+b$
Sum
Nonlinear Activation $\mathbf{v}_{j}=\dfrac{\left\|\mathbf{s}_{j}\right\|^{2}}{1+\left\|\mathbf{s}_{j}\right\|^{2}} \dfrac{\mathbf{s}_{j}}{\left\|\mathbf{s}_{j}\right\|}$ $h_{j}=f\left(a_{j}\right)$
Output $\text{vector}\left(\mathbf{v}_j\right)$ $\text{scalar}\left(h_j\right)$

人造神经元包含如下 3 个计算步骤:

  1. 输入标量的加权
  2. 加权标量的求和
  3. 求和标量到输出标量的非线性变换

胶囊可以理解为上述 3 个步骤的向量版,同时增加了对输入的仿射变换:

  1. 输入向量的矩阵乘法:胶囊接受的输入向量编码了低层胶囊检测出的相应对象的概率,向量的方向编码了检测出的对象的一些内部状态。接着将这些向量乘以相应的权重矩阵 $\mathbf{W}$$\mathbf{W}$ 编码了低层特征(例如:眼睛、嘴巴和鼻子)和高层特征(例如:面部)之间的空间关系和其他重要关系。
  2. 向量的标量加权:这个步骤同人造神经元对应的步骤类似,但神经元的权重是通过反向传播学习的,而胶囊则使用动态路由。
  3. 加权向量的求和:这个步骤同人造神经元对应的步骤类似。
  4. 求和向量到输出向量的非线性变换:胶囊神经网络的非线性激活函数接受一个向量,然后在不改变方向的前提下,将其长度压缩到 1 以下。

动态路由

胶囊网络使用动态路由算法进行训练,算法过程如下 2

\begin{algorithm}
\caption{Routing 算法}
\begin{algorithmic}
\PROCEDURE{Routing}{$\widehat{\mathbf{u}}_{j | i}, r, l$}
\STATE for all capsule $i$ in layer $l$ and capsule $j$ in layer $\left(l + 1\right)$: $b_{ij} \gets 0$
\FOR{$r$ iterations}
  \STATE for all capsule $i$ in layer $l$: $\mathbf{c}_i \gets \text{softmax} \left(\mathbf{b}_i\right)$
  \STATE for all capsule $j$ in layer $\left(l + 1\right)$: $\mathbf{s}_j \gets \sum_{i} c_{ij} \widehat{\mathbf{u}}_{j | i}$
  \STATE for all capsule $j$ in layer $\left(l + 1\right)$: $\mathbf{v}_j \gets \text{squash} \left(\mathbf{s}_j\right)$
  \STATE for all capsule $i$ in layer $l$ and capsule $j$ in layer $\left(l + 1\right)$: $b_{ij} \gets b_{ij} + \widehat{\mathbf{u}}_{j | i} \cdot \mathbf{v}_j$
\ENDFOR
\RETURN $\mathbf{v}_j$
\ENDPROCEDURE
\end{algorithmic}
\end{algorithm}
  1. 第 1 行表示算法的输入为:低层 $l$ 中所有胶囊的输出 $\widehat{\mathbf{u}}$,以及路由迭代计数 $r$
  2. 第 2 行中的 $b_{ij}$ 为一个临时变量,其值在迭代过程中更新,算法运行完毕后其值被保存在 $c_{ij}$ 中。
  3. 第 3 行表示如下步骤将会被重复 $r$ 次。
  4. 第 4 行利用 $\mathbf{b}_i$ 计算低层胶囊的权重向量 $\mathbf{c}_i$$\text{softmax}$ 确保了所有权重为非负数,且和为一。第一次迭代后,所有系数 $c_{ij}$ 的值相等,随着算法的继续,这些均匀分布将发生改变。
  5. 第 5 行计算经前一步确定的路由系数 $c_{ij}$ 加权后的输入向量的线性组合。该步缩小输入向量并将他们相加,得到输出向量 $\mathbf{s}_j$
  6. 第 6 行对前一步的输出向量应用 $\text{squash}$ 非线性函数。这确保了向量的方向被保留下来,而长度被限制在 1 以下。
  7. 第 7 行通过观测低层和高层的胶囊,根据公式更新相应的权重 $b_{ij}$。胶囊 $j$ 的当前输出与从低层胶囊 $i$ 处接收到的输入进行点积,再加上旧的权重作为新的权重。点积检测胶囊输入和输出之间的相似性。
  8. 重复 $r$ 次,计算出所有高层胶囊的输出,并确立路由权重。之后正向传导就可以推进到更高层的网络。

点积运算接收两个向量,并输出一个标量。对于给定长度但方向不同的两个向量而言,点积有几种情况:$a$ 最大正值;$b$ 正值;$c$ 零;$d$ 负值;$e$ 最小负值,如下图所示:

我们用紫色向量 $\mathbf{v}_1$$\mathbf{v}_2$ 表示高层胶囊,橙色向量表示低层胶囊的输入,其他黑色向量表示接收自其他低层胶囊的输入,如下图所示:

左侧的紫色输出 $\mathbf{v}_1$ 和橙色输入 $\widehat{\mathbf{u}}_{1|1}$ 指向相反的方向,这意味着他们的点积是一个负数,从而路由系数 $c_{11}$ 减少;右侧的紫色输出 $\mathbf{v}_2$ 和橙色输入 $\widehat{\mathbf{u}}_{2|1}$ 指向相同的方向,从而路由系数 $c_{12}$ 增加。在所有高层胶囊及其所有输入上重复该过程,得到一个路由系数集合,达成了来自低层胶囊的输出与高层胶囊的输出的最佳匹配。

网络架构

胶囊网络由 6 层神经网络构成,前 3 层是编码器,后 3 层是解码器:

  1. 卷积层
  2. PrimaryCaps(主胶囊)层
  3. DigitCaps(数字胶囊)层
  4. 第一全连接层
  5. 第二全连接层
  6. 第三全连接层

编码器

编码器部分如下图所示:

卷积层用于检测 2D 图像的基本特征。PrimaryCaps 层包含 32 个主胶囊,接收卷积层检测到的基本特征,生成特征的组合。DigitCaps 层包含 10 个数字胶囊,每个胶囊对应一个数字。

对于 $k$ 个类别的数字,我们希望最高层的胶囊当且仅当一个数字出现在图像中时具有一个长的实例化向量。为了允许多个数字,对于每个 DigitCap 使用一个独立的损失函数:

$$ L_{k}=T_{k} \max \left(0, m^{+}-\left\|\mathbf{v}_{k}\right\|\right)^{2}+\lambda\left(1-T_{k}\right) \max \left(0,\left\|\mathbf{v}_{k}\right\|-m^{-}\right)^{2} $$

DigitCaps 层的输出为 10 个 16 维的向量,根据上面的公式计算每个向量的损失值,然后将 10 个损失值相加得到最终损失。

在损失函数中,当正确的标签与特定 DigitCap 的数字对应时 $T_k$ 为 1,否则为 0。加号前一项用于计算正确 DigitCap 的损失,当概率值大于 $m^{+} = 0.9$ 时为 0,当概率值小于 $m^{+} = 0.9$ 时为非零值;加号后一项用于计算错误 DigitCap 的损失,当概率值小于 $m^{-} = 0.1$ 时值为 0,当概率值大于 $m^{-} = 0.1$ 时为非零值。公式中的 $\lambda = 0.5$ 用于确保训练中数值的稳定性。

简单来说,低层的胶囊用于检测一些特定模式的出现概率和姿态,高层的胶囊用于检测更加复杂的图像,如下图所示:

解码器

解码器部分如下图所示:

解码器从正确的 DigitCap 中接收一个 16 维向量,并学习将其解码为数字图像。解码器接收正确的 DigitCap 的输出作为输入,并学习重建一张 $28 \times 28$ 像素的图像,损失函数为重建图像与输入图像之间的欧式距离。


  1. Hinton, G. E., Krizhevsky, A., & Wang, S. D. (2011, June). Transforming auto-encoders. In International conference on artificial neural networks (pp. 44-51). Springer, Berlin, Heidelberg. ↩︎

  2. Sabour, S., Frosst, N., & Hinton, G. E. (2017, December). Dynamic routing between capsules. In Proceedings of the 31st International Conference on Neural Information Processing Systems (pp. 3859-3869). ↩︎

投票公平合理吗?

2021-01-17 08:00:00

只有道德上的相对民主,没有制度上的绝对公平,求同存异才能长治久安。

无处不在的投票

在古代雅典城邦有一项政治制度称之为陶片放逐制,是由雅典政治家克里斯提尼于公元前 510 年创立。雅典人民可以通过投票强制将某个人放逐,目的在于驱逐可能威胁雅典的民主制度的政治人物。投票者在选票——陶罐碎片较为平坦处,刻上他认为应该被放逐者的名字,投入本部落的投票箱。如果选票总数未达到 6000,此次投票即宣告无效;如果超过 6000,再按票上的名字将票分类,得票最多的人士即为当年放逐的人选。

美国总统选举的方式称为选举人团,是一种间接选举,旨在选出美国总统和副总统。根据《美国宪法》及其修正案,美国各州公民先选出该州的选举人,再由选举人代表该州投票。不采用普选制度的原因,主要是由于美国是联邦制国家,并考虑到各州的特定地理及历史条件,制宪元老决定采取选举人团制度,保障各州权益,所以美国没有公民直选的总统。

离我们最近的可能就是朋友聚会吃什么这个问题了,烧烤还是火锅,这是个问题。当然,只要你想要,还可以:蒸羊羔、蒸熊掌、蒸鹿尾儿、烧花鸭、烧雏鸡、烧子鹅 ……

投票制度

多数制

多数制(Plurality Voting System)的原则是“胜者全取”,又被称为多数代表制(Majoritarian Representation),分成相对多数(Relative Plurality)和绝对多数(Absolute Majority)。相对多数制即不论票数多少,得票最多的候选人便可当选。绝对多数制指候选人需要得到指定的票数方可当选,门槛在设定上须达有效票之过半数、三分之二、四分之三或五分之四等多数,亦可以是更高的比例或数字。

多数制的优点在于简单易行,缺点在于当选票分散的越平均,投票结果的争议性就会越大。例如:有 10 人参与投票,有 3 人选择吃火锅,有 3 人选择吃烧烤,有 4 人选择吃家常菜。根据多数制规则,最终的选择为吃家常菜,但会有 6/10 的人没有得到满意的结果。

波达计数法

波达计数法(Borda Count)是通过投票人对候选者进行排序,如果候选者在选票的排第一位,它就得某个分数,排第二位得一个较小的分数,如此类推,分数累计下来最高分的候选者便取胜。

波达计数法相比于多数制不容易选出比较有争议的选项。假设有一场选举,共 4 位候选人,共收集到有效选票 100 张,选票统计结果(理论山可能出现的选票类型为 $4! = 24$ 种可能,简单起见,示例中仅有 4 种可能)如下:

# 51 票 5 票 23 票 21 票
1 张三 王五 李四 马六
2 王五 李四 王五 王五
3 李四 马六 马六 李四
4 马六 张三 张三 张三

选举采用排名第 $n$ 位得 $4 - n$ 分的准则,每个人的分数如下:

按照多数制应该是张三获胜(得到第 1 名的票数最多,共计 51 票)。不过通过波达计数法可以发现大家对张三的喜好比较极端:只有特别喜欢(排名第 1)和特别不喜欢(排名第 4)两种情况,所以张三是一个具有争议的人。但王五不同,虽然其获得第 1 的选票并不多,但是获得第 2 的选票很多,说明大部分人都是比较能接受王五的。

波达计数法同时也存在几个问题:

  1. 不同的权重的计分规则,可能得到不同结果。以下表为例:

    # 2 票 3 票 4 票 #
    2 分 A A C 5 分
    1 分 B C B 2 分
    0 分 C B A 1 分

    采用左边权重的计分结果为:A:10 分,B:6 分,C:11 分,则 C 胜出。采用右边权重的计分结果为:A:29 分,B:15 分,C:28 分,则 A 胜出。不难看出,多数投票制是波达计数法的一个特例,即第 1 名的权重为非零值,其余名次权重均为零。

  2. 容易出现恶意投票局面。参与的人在投票时往往会掺杂个人情感,假设存在这样一次选举:张三、李四、王五三人竞选班长,张三和李四是最有希望竞争班长的人员,而王五则表现平庸。参与投票的同学几乎近一半的人支持张三,近一半的人支持李四,很少人支持王五。参与投票的同学自认为都很“聪明”,担心自己不支持的人(假设为李四)担任班长,同时认为王五没希望竞选成功,因此在排序时会把王五排在第 2 名,把李四排在第 3 名。此时,张三和李四都变成了具有争议的人,那么选举的结果就很有可能是王五获胜。

孔多赛投票制

波达计数法还存在一个严重的问题是,当其中一个选项退出后,投票的结果会发生变化。仍然以上文中的例子为例:

# 2 分 1 分 0 分 # 1 分 0 分
2 票 A B C 2 票 A C
3 票 A C B 3 票 A C
4 票 C B A 4 票 C A

在包含选项 B 时,投票结果为:A:10 分,B:6 分,C:11 分,C 获胜。当把选项 B 去掉后,投票结果为:A:5 分,C:4 分,A 获胜。去掉选项 B,规则没有发生变化(不同排名之间权重相差为 1),投票人的意愿也没有发生变化(候选人的相对名次没有发生变动),但投票结果却截然不同。

为了解决这个问题,孔多赛提供了采用两两对决的方式进行投票。投票人依旧按照类似波达计数法对候选人进行排序,但与波达计数法不同的是并不进行计分而是需要统计出所有两两对决的情况。上面这个例子中两两对决的情况有 6 种:A > BB > AB > CC > BA > CC > A,对决的统计结果如下表所示:

# 5 次 4 次 2 次 7 次 5 次 4 次
1 分 A B B C A C
0 分 B A C B C A

由于 A > B 有 5 次,B > A 有 4 次,因此在 A 和 B 的两两对决中获胜的是 A。同理可得,在 A 和 C 的两两对决中获胜的是 A,在 B 和 C 的两两对决中获胜的是 C。当存在很多选项时,假设选项 A 在与任何其它选项的两两比较中都能胜出,那 A 称为孔多塞胜利者

如果你对 Facebook 历史有了解,早期扎克伯格在哈佛大学就读学士期间,写了一个名为 Facemash 的网站程序,根据哈佛校内报纸《The Harvard Crimson》,Facemash 会从校内的网络上收集照片,每次将两张照片并排后让用户选择“更火辣”的照片。Facemash 就采取了类似孔多塞投票的方法对女生进行投票,下图是电影社交网络中这个原型的截图:

孔多塞投票制由于统计比较麻烦,现实生活中使用的情况不多。同时,孔多塞投票也存在一个问题,即孔多塞循环。假设 A 和 B 对决的优胜者为 A,B 和 C 对决的优胜者为 B,C 和 A 对决的优胜者为 C,则没有一个选项打败其他所有选项,那么这样就无法得到投票的结果。这称之为孔多塞悖论(投票悖论),在这个假想情况中,集体倾向可以是循环性的,即使个人的倾向不是。

所以呢?

所以什么样的投票才算是公平合理的投票呢?肯尼斯·阿罗给出了一个解答。

$N$ 种选择,有 $m$ 个决策者,他们每个人都对这 $N$ 个选择有一个从优至劣的排序。我们要设计一种选举法则,使得将这 $m$ 个排序的信息汇总成一个新的排序,称为投票结果。我们希望这种法则满足以下条件

  1. 一致性(Unanimity)。或称为帕累托最优(Pareto Efficiency),即如果所有的 $m$ 个决策者都认为选择 $A$ 优于 $B$,那么在投票结果中,$A$ 也优于 $B$
  2. 非独裁(Non-Dictatorship)。不存在一个决策者 $X$,使得投票结果总是等同于 $X$ 的排序。
  3. 独立于无关选项(Independence of Irrelevant Alternatives,IIA))。如果现在一些决策者改了主意,但是在每个决策者的排序中,$A$$B$ 的相对位置不变,那么在投票结果中 $A$$B$ 的相对位置也不变。

如果选项 $N \geq 3$,投票人数 $m \geq 2$ 时,没有任何一种投票规则能够满足以上 3 点,这就是阿罗悖论。所以只有道德上的相对民主,没有制度上的绝对公平,求同存异才能长治久安。

本文参考自《投票公平合理吗?为什么没有绝对的公平?阿罗不可能定理》。

图存储与计算(Network Storage & Computing)

2021-01-01 08:00:00

本文为《复杂网络系列》文章

图存储

语义网络与 RDF 存储

1968 年 Ross Quillian 在其博士论文中最先提出语义网络(Semantic Web),把它作为人类联想记忆的一个显式心理学模型,并在他设计的可教式语言理解器 TLC(Teachable Language Comprehenden)中用作知识表示方法。

语义网络的基本思想是在网络中,用“节点”代替概念,用节点间的“连接弧”(称为联想弧)代替概念之间的关系,因此,语义网络又称联想网络。它在形式上是一个带标识的有向图。由于所有的概念节点均通过联想弧彼此相连知识推导。

一个语义网络的基本构成如下:

之后 Tim Berners-Lee 又提出了语义网堆栈(Semantic Web Stack)的概念。语义网堆栈利用图示解释是不同层面的语言所构成的层级结构,其中,每一层面都将利用下游层面的能力,语义网堆栈如下图所示:

资源描述框架(Resource Description Framework,RDF)是用于描述网络资源的 W3C 标准,比如网页的标题、作者、修改日期、内容以及版权信息。

RDF 使用 Web 标识符来标识事物,并通过属性和属性值来描述资源。

对资源、属性和属性值的解释:

下面是一个 RDF 示例文档(这是一个简化的例子,命名空间被忽略了):

<?xml version="1.0"?>

<RDF>
  <Description about="http://www.w3school.com.cn/RDF">
    <author>David</author>
    <homepage>http://www.w3school.com.cn</homepage>
  </Description>
</RDF>

资源属性属性值的组合可形成一个陈述(被称为陈述的主体、谓语和客体)。上述的 RDF 文档包含了如下两个陈述:

更多 RDF 介绍请参见:https://www.w3school.com.cn/rdf/index.asp 。

Apache Jena 是一个用于构建语义网络(Semantic Web)和链接数据(Linked Data)应用的开源 Java 框架。Jena 提供了 3 大部分功能:

  1. RDF
    • RDF API:提供构建和读取 RDF 图的核心 API,并利用 RDF/XMLTurtle 等数据类型序列化数据。
    • ARQ(SPARQL):提供一种 SPARQL 1.1 的编译引擎 ARQ 用于查询 RDF。
  2. Triple store
    • TDB:提供一种原生高效的 Triple 存储 TDB,全面支持 Jena APIs。
    • Fuseki:提供 REST 风格的 RDF 数据交互方式。
  3. OWL
    • Ontology API:通过 RDFS,OWL 等为 RDF 数据添加更多语义信息。
    • Inference API:通过内置的 OWL 和 RDFS 语义推理器 构建个性化的推理规则。

下面以 Graph of The Gods 的关系图对 Jena 的基本功能进行说明。Graph of The Gods 是一张描述希腊神话相关事物之间关系的图,其中顶点的类型有:titan(泰坦,希腊神话中曾经统治师姐的古老神族),god(神),demigod(半神),human(人),monster(怪物),location(地点);关系的类型有:father(父亲),brother(兄弟),mother(母亲),battled(战斗),lives(居住)。

以 Apache Tomcat 作为容器来安装 Apache Jena Fuseki,下载最新版的 Apache Jena Fuseki 并解压,将其中的 fuseki.war 复制到已经安装并运行的 Apache Tomcat 的 webapps 路径下。安装完毕后,进入 http://127.0.0.1:8080/fuseki 即可使用 Apache Jena Fuseki。

在导入 Graph of The Gods 数据后,执行如下查询语句可以获得 jupiter 的所有兄弟:

PREFIX gods: <http://leovan.me/gods/>

SELECT DISTINCT ?god
WHERE {
  ?god gods:brother gods:jupiter
}

查询结果为:

god
1 gods:pluto
2 gods:neptune

图数据库

图数据库是一个使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据。不同于关系型数据库,图数据库为 NoSQL(Not Only SQL)的一种,属于联机事务处理(OLTP)的范畴,可以解决现有关系数据库的局限性。

下图展示了近年来不同类型数据库的流行度趋势,不难看出近年来越来越多的人开始关注图数据库。

数据库流行度趋势 https://db-engines.com/en/ranking_categories

截止到 2020 年 12 月,图数据库的排名如下图所示:

图数据库排名 https://db-engines.com/en/ranking/graph+dbms

其中,Neo4jJanusGraphDgraphTigerGraphNebula Graph 均为时下常用的图数据库。从下图的流行度趋势角度来看,JanusGraph、Dgraph、TigerGraph 和 Nebula Graph 等后起之秀发展迅速。

图数据库流行度趋势 https://db-engines.com/en/ranking_trend/graph+dbms

不同的图数据库有着不同的优劣势,用户可以根据实际业务场景选择合适的图数据库。下面给到一些较新的图数据库对比和评测:

  1. 主流开源分布式图数据库 Benchmark
  2. 图数据库对比:Neo4j vs Nebula Graph vs HugeGraph
  3. 图分析系统基准测试报告
  4. 图数据平台产品测试报告

查询语言

图查询语言(Graph Query Language,GQL)是一种用于图数据库的查询语言,类比于关系型数据库的查询语言 SQL。2019 年 9 月,GQL 被提议为一种新的数据库查询语言(ISO/IEC WD 39075),目前仍处于开发当中,因此市面上还没有统一的图查询语言标准。

Gremlin

GremlinApache TinkerPop 框架下的图遍历语言。Gremlin 适用于基于 OLTP 的图数据库以及基于 OLAP 的图分析引擎,支持命令式和声明式查询。支持 Gremlin 的图数据库有:Neo4j、JanusGraph 等。

Cypher

Cypher 是一种声明式图查询语言,这使得在不必编写遍历逻辑的情况下可以实现高效的查询。支持 Cypher 的图数据库有:Neo4j、RedisGraph、Nebula Graph 等。

nGQL

nGQL 是一种声明式的图查询语言,支持图遍历、模式匹配、聚合运算和图计算等特性。支持 nGQL 的图数据库有:Nebula Graph。

比较

针对 3 种不同的查询语言,对于图中相关概念的表示也略有不同,如下表所示:

术语 Gremlin Cypher nGQL
Vertex Node Vertex
Edge Relationship Edge
点类型 Label Label Tag
边类型 label RelationshipType edge type
点 ID vid id(n) vid
边 ID eid id(r)
插入 add create insert
删除 drop delete delete / drop
更新属性 setProperty set update

更多不同查询语言之间的详细对比可以参见如下资料:

  1. 一文了解各大图数据库查询语言 | 操作入门篇
  2. 文档解读 | SQL vs. nGQL

图计算

图计算框架

GraphX

GraphX 是一个基于 Spark 大规模图计算框架。GraphX 通过引入一个包含带有属性的顶点和变的有向图对 Spark 的 RDD 进行了扩展。通过 subgraph、joinVertices 和 aggregateMessages 等算子实现了 PageRank、连通子图、LPA 等图算法。

Plato

Plato 是由腾讯开源的高性能图计算框架。Plato 主要提供两方面的能力:离线图计算和图表示学习,目前支持的图算法如下:

算法分类 算法
图特征 树深度/宽度;节点数/边数/密度/节点度分布;N-阶度;HyperANF
节点中心性指标 KCore;Pagerank;Closeness;Betweenness
连通图 & 社团识别 Connected-Component;LPA;HANP
图表示学习 Node2Vec-Randomwalk;Metapath-Randomwalk
聚类/分圈算法 FastUnfolding
其他图相关算法 BFS;共同类计算
待开源算法 Word2Vec;Line;GraphVite;GCN

在计算性能上,Plato 与 Spark GraphX 在 PageRank 和 LPA 两个算法上的计算耗时与内存消耗对比如下图所示:

Plato & Spark GraphX Benchmark

GraphScope

GraphScope 由有阿里巴巴开源的一个统一的分布式图计算平台。GraphScope 提供了一个一站式环境,可以通过用户友好的 Python 接口在集群内对图进行操作。GraphScope 利用一系列开源技术使得集群上的大规模图数据的多阶段处理变得简单,这些技术包括:用于分析的 GRAPE、用于查询的 MaxGraph 、用于图神经网络计算的 Graph-Learn 和用于提供高效内存数据交换的 vineyard。GraphScope 的整体架构如下图所示:

Architecture of GraphScope

GraphScope Interactive Engine(GIE)是一个用于探索性分析大规模复杂图结构数据的引擎,它通过 Gremlin 提供高级别的图查询语言,同时提供自动并行执行功能。

GraphScope Analytical Engine(GAE)是一个基于 GRAPE 1 提供并行图算法的分析引擎。除了提供基础的内置算法以外,GAE 允许用户利用 Python 基于 PIE 1 编程模型编写自定义算法,PIE 编程模型的运行方式如下图所示:

Execution Model in GAE

GraphScope 还提供以顶点为中心的 Pregel 模型 2,用户可以使用 Pregel 模型来实现自定义算法。

GraphScope Learning Engine(GLE)是一个用于开发和训练大规模图神经网络的分布式框架。GLE 提供基于全量图(用于 GCN、GAT 等算法)和采样子图(用于 GraphSAGE,FastGCN、GraphSAINT 等算法)两种不同方式训练图模型。整体架构如下图所示:

GLE

Galileo

Galileo 是由京东零售研发的图计算平台,提供离线和在线图计算和图数据服务能力。目前 Galileo 暂未开源,待开源后补充相关信息。

图神经网络

关于图神经网络内容,请参见之前的博客 图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)

:tada::tada::tada: Happe New Year! :tada::tada::tada:


  1. Fan, W., Yu, W., Xu, J., Zhou, J., Luo, X., Yin, Q., … & Xu, R. (2018). Parallelizing sequential graph computations. ACM Transactions on Database Systems (TODS), 43(4), 1-39. ↩︎ ↩︎

  2. Malewicz, G., Austern, M. H., Bik, A. J., Dehnert, J. C., Horn, I., Leiser, N., & Czajkowski, G. (2010, June). Pregel: a system for large-scale graph processing. In Proceedings of the 2010 ACM SIGMOD International Conference on Management of data (pp. 135-146). ↩︎

网络算法 (Network Algorithms)

2020-12-12 08:00:00

本文为《复杂网络系列》文章
本文内容主要参考自:《网络科学引论》1

网络基础算法

最短路径

最短路径(shortest path)算法是寻找两个顶点之间的最短路径,寻找网络中最短路径的标准算法称为广度优先搜索(breadth-first search)。算法的基本思想如下图所示:

根据广度优先搜索的基本思想,不难证明距 $s$ 最短距离为 $d$ 的每个顶点都有一个到 $s$ 的最短距离为 $d - 1$ 的邻居顶点。一个简单的实现方式是,创建一个有 $n$ 个元素的数组存储从源顶点 $s$ 到其他所有顶点的距离,同时创建一个距离变量 $d$ 来记录当前在搜索过程中所处的层数,算法的具体流程如下:

  1. 遍历距离数组,查找到 $s$ 的距离为 $d$ 的所有顶点。
  2. 查找上述顶点的所有邻居顶点,如果同 $s$ 的距离未知,则距离置为 $d + 1$
  3. 如果距离未知的邻居顶点数量为零,则停止算法,否则将 $d$ 的值加一并重复上述过程。

这种方法在最坏的情况下时间复杂度为 $O \left(m + n^2\right)$,考虑多数网络的直径只随 $\log n$ 增长,算法运行的时间复杂度为 $O \left(m + n \log n\right)$

上述算法中步骤 1 是最耗时的部分,通过使用队列的数据结构我们可以避免每次都遍历列表来找到距离源顶点 $s$ 距离为 $d$ 的顶点。构造一个队列,一个指针指向下一个要读取的元素,另一个指针指向要填充的空位,这样距离为 $d + 1$ 的顶点就会紧跟在距离为 $d$ 的顶点后面,队列如下图所示:

通过队列可以将算法的时间复杂度降至 $O \left(m + n\right)$,对于 $m \propto n$ 的稀疏网络而言,$O \left(m + n\right)$ 相当于 $O \left(n\right)$,所以算法的时间复杂度同顶点数量成正比。

通过对算法进行进一步修改则可以得到源顶点 $s$ 到其他任何顶点的最短路径。方法是在原来的网络上构建一个新的有向网络,该网络代表最短路径,称为最短路径树(shortest path tree),通常情况下,该网络是一个有向非循环网络,而不是树。

对于加权网络,利用广度优先搜索无法找到最短路径,这里需要用到 Dijkstra 算法 2 进行求解。算法将图中的顶点分成两组 $S$$U$,整个算法过程如下:

  1. 初始状态,$S$ 仅包含源顶点,即 $S = \left\{v\right\}$$U$ 包含其余顶点。如果 $v$$U$ 中的顶点 $u$ 为邻居,则距离为边的权重,否则为无穷大。
  2. $U$ 中选择一个距离 $v$ 最短的顶点 $k$,并把 $k$ 加入到 $S$ 中。
  3. 若从源点 $v$ 经过顶点 $k$ 到达 $u$ 的距离比之前 $v$$u$ 的距离短,则将距离修改为这个更短的距离。
  4. 重复步骤 2 和 3,直至所有顶点都包含在 $S$ 中。

整个算法过程的可视化效果如下图所示:

Dijkstra 算法的时间复杂度为 $O \left(m + n^2\right)$,通过二叉堆的数据结构可以将时间复杂度优化至 $O \left(\left(m + n\right) \log n\right)$

Dijkstra 算法虽然能够处理加权网络,但不能处理存在负权重的网络,需要利用 Floyd-Warshall 算法 3 进行求解。更多 Floyd-Warshall 算法的细节请参见之前的博客计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)

最大流和最小割

对于连接给定顶点 $s$$t$ 的两条路径,若没有共享边,则这两条路径是边独立的;若除 $s$$t$ 外不共享任何其他顶点,则这两条路径是顶点独立的。顶点之间的边连通度顶点连通度分别是顶点之间边独立路径数和顶点独立路径数。连通度是度量顶点之间连通鲁棒性的简单参数。假设一个网络是一个管线网络,其中每个管线的容量均为单位流量,那么边连通度等于从 $s$ 流向 $t$最大流

增广路径算法(Ford-Fulkerson Algorithm,FFA)是计算最大流最简单的算法。基本思想是:首先利用广度优先搜索算法找到一条从源 $s$ 到目标 $t$ 的路径。该步骤“消耗”了网络中的一些边,将这些边的容量填充满后,它们不再承载更多流量。之后在剩余边中找到从 $s$$t$ 的另一条路径,重复该过程直到找不到更多的路径为止。

但这还不是一个有效的算法,如下图中的 (a) 所示,如果在 $s$$t$ 之间运用广度优先搜索,可以发现黑色标记的路径。一旦这些边的容量被填充满,就不能在剩余边中找到从 $s$$t$ 的更多路径,但很明显,从 $s$$t$ 有两条边独立路径(上下各一条)。

解决该问题的一个简单修正方法是允许网络流量在一条边中能够同时在两个方向流动。更一般地,因为一条边容许承载的最大流是在任意方向的单位流量,那么一条边可以有多个单位流量,只要保证他们能够相互抵消,并且最终每条边承载不超过一个单位流量。

增广路径算法的实现利用了剩余图(residual graph),这是一个有向网络,该网络中的有向边连接原网络中相应的顶点对,并在指定方向承载一个或多个单位流量。例如上图中 (c) 和 (d) 就是对应 (a) 和 (b) 的流量状态的剩余图。算法的正确性在这里就不过多展开说明,该算法在计算两个顶点之间的最大流的平均时间复杂度为 $O \left(\left(m + n\right) m / n\right)$

在图论中,去掉其中所有边使一张网络不再连通的边集为图的,一张图上最小的割为最小割。通过对增广路径算法进行改动即可以寻找到边独立路径、最小边割集和顶点独立路径。

图划分和社团发现

图划分(graph partitioning)和社团发现(community detection)都是指根据网络中的边的连接模式,把网络顶点划分成群组、簇或社团。将网络顶点划分成群组后最常见的属性是,同一群组内部的顶点之间通过边紧密连接,而不同群组之间只有少数边。

图划分

最简单的图划分问题是把网络划分成两部分,有时也称其为图对分(graph bisection)。图对分是把一个网络中的顶点划分成为两个指定规模的非重叠群组,使得不同群组之间相互连接的边数最小。群组之间的边数称为割集规模(cut size)。 利用穷举搜索解决该问题是极为耗时的,通过启发式算法我们可以找到较好的网络划分。

Kernighan-Lin 算法

Kernighan-Lin 算法是由 Brian Kernighan 和 Shen Lin 在 1970 年提出的 4,是图对分问题中最简单、最知名的启发式算法之一,如下图所示。

先以任意方式将网络顶点按指定规模划分成两个群组,对于任何由分属不同群组的顶点 $i$ 和顶点 $j$ 组成的顶点对 $\left(i, j\right)$,交换 $i$$j$ 的位置,并计算交换前后两个群组之间割集规模的变化量。在所有顶点对中找到使割集规模减小最多的顶点对,或者若没有使割集规模减小的顶点对,则找到使割集规模增加最小的顶点对,交换这两个顶点。重复这个过程,同时保证网络中的每个顶点只能移动一次。

继续算法,每一步都交换最大程度减少或最小程度增加群组之间边数的顶点对,直到没有可以变换的顶点对,此时本轮算法停止。在完成所有交换后,检查网络在此过程中经过的每一个状态,然后选择割集规模最小的状态。最后,重复执行上述整个过程,每次始于上次发现的最优网络划分,直到割集规模不在出现改善。

Kernighan-Lin 算法的主要缺点是运算速度缓慢,采用一些技巧来改善算法也只能使时间复杂度降至 $O \left(n^3\right)$,因此该算法仅适用于有几百或几千个顶点的网络,而不适用于更大规模的网络。

谱划分

请先了解附录中的拉普拉斯算子和拉普拉斯矩阵等相关概念。

考虑具有 $n$ 个顶点 $m$ 条边的网络,将其划分为两个群组,称为群组 1 和群组 2。可以把该划分的割集规模,也就是两个群组之间的边数表示为:

$$ \label{eq:r_1} R = \dfrac{1}{2} \sum_{i, j \text{ 属于不同群组}} A_{ij} $$

对于每个网络划分,定义有参数 $s_i$ 组成的集合,集合中每个元素对应于一个顶点 $i$,则有:

$$ s_i = \left\{\begin{array}{ll} +1 & \text{顶点 } i \text{ 在群组 1 中} \\ -1 & \text{顶点 } i \text{ 在群组 2 中} \end{array}\right. $$

那么:

$$ \dfrac{1}{2} \left(1 - s_i s_j\right) = \left\{\begin{array}{ll} 1 & \text{顶点 } i \text{ 和 } j \text{ 在不同的群组中} \\ 0 & \text{顶点 } i \text{ 和 } j \text{ 在相同的群组中} \end{array}\right. $$

则式 \ref{eq:r_1} 可以改写为:

$$ \begin{aligned} R & = \dfrac{1}{4} \sum_{ij} A_{ij} \left(1 - s_i s_j\right) \\ & = \dfrac{1}{4} \left(k_i \delta_{ij} - A_{ij}\right) s_i s_j \\ & = \dfrac{1}{4} \sum_{ij} L_{ij} s_i s_j \end{aligned} $$

其中,$\delta_{ij}$ 是克罗内克函数,$L_{ij}$ 是图拉普拉斯矩阵的第 $ij$ 个元素。写成矩阵的形式有:

$$ R = \dfrac{1}{4} \mathbf{s}^{\top} \mathbf{L} \mathbf{s} $$

由于每个 $s_i$ 的取值只能是 $\left\{+1, -1\right\}$,所以在给定 $\mathbf{L}$ 时求解 $\mathbf{s}$ 使其割集规模最小时并不容易。具体求解方法的推导在此不再展开说明,最终谱划分算法的过程如下所示:

  1. 计算图拉普拉斯矩阵的第二小特征值 $\lambda_2$,称为网络的代数连通度(algebraic connectivity),及其对应的特征向量 $\mathbf{v}_2$
  2. 按从大到小的顺序对特征向量的元素进行排序。
  3. 把前 $n_1$ 个最大元素对应的顶点放入群组 1,其余放入群组 2,计算割集规模。
  4. 把前 $n_1$ 个最小(注意:中文译本中有错误)元素对应的顶点放入群组 2,其余放入群组 1,并重新计算割集规模。
  5. 在两种网络划分中,选择割集规模较小的那个划分。

谱划分方法在稀疏网络上的时间复杂度为 $O \left(n^2\right)$,这比 Kernighan-Lin 算法时间复杂度少了一个因子 $n$,从而使该算法能应用于更大规模的网络。

社团发现

社团发现(社区发现,社群发现,Community Detection)的基本目的与图划分类似,即把网络分成几个节点点群组,并使节点群组之间的连接较少。主要的差别就是群组的数量和规模是不确定的。社团发现的算法分类和具体实现很多,本文仅介绍几个常用的算法,更多方法及其细节请参见如下开放资源:

  1. Community Detection in Graphs 5
  2. Deep Learning for Community Detection: Progress, Challenges and Opportunities 6
  3. 复杂网络社团发现算法研究新进展 7
  4. benedekrozemberczki/awesome-community-detection

Fast Unfolding (Louvain)

Fast Unfolding (Louvain) 8 是一种基于模块度的社团发现算法,通过模块度来衡量一个社团的紧密程度。算法包含两个阶段:

  1. 历遍网络中所有的节点,通过比较将节点给每个邻居社团带来的模块度变化,将这个节点加入到使模块度增加最大的社团中。
  2. 对于步骤 1 的结果,将属于同一个社团的节点合并成为一个大的节点,进而重型构造网络。新的节点之间边的权重是所包含的之前所有节点之间相连的边权重之和,然后重复步骤 1。

算法的两个步骤如下图所示:

Label Propagation Algorithm (LPA)

标签传播算法(Label Propagation Algorithm,LPA)是一种基于半监督学习的社团发现算法。对于每个节点都有对应的标签(即节点所隶属的社团),在算法迭代过程中,节点根据其邻居节点更新自身的标签。更新的规则是选择邻居节点中最多的标签作为自身的标签。

标签传播的过程中,节点的标签更新方式分为同步更新异步更新两种方式。同步更新是指对于节点 $x$,在第 $t$ 步时,根据其所有邻居节点在 $t - 1$ 步时的标签对其进行更新,即:

$$ C_{x}(t)=f\left(C_{x_{1}}(t-1), C_{x_{2}}(t-1), \cdots, C_{x_{k}}(t-1)\right) $$

同步更新对于一个二分或者近似二分的网络来说可能会出现标签震荡的现象。对于异步更新方式,更新公式为:

$$ C_{x}(t)=f\left(C_{x_{i 1}}(t), \cdots, C_{x_{i m}}(t), C_{x_{i(m+1)}}(t-1), \cdots, C_{x_{i k}}(t-1)\right) $$

其中,邻居节点 $x_{i1}, \cdots, x_{im}$ 的标签在第 $t$ 步时已经更新过,而 $x_{i(m+1)}, \cdots, x_{ik}$ 的标签还未更新。

附录

拉普拉斯算子(Laplace operator,Laplacian)是由欧式空间中的一个函数的梯度的散度给出的微分算子,通常写作 $\Delta$$\nabla^2$$\nabla \cdot \nabla$

梯度(gradient)是对多元导数的概括,函数沿着梯度的方向变化最快,变化率则为梯度的模。假设二元函数 $f \left(x, y\right)$ 在区域 $G$ 内具有一阶连续偏导数,点 $P \left(x, y\right) \in G$,则称向量:

$$ \nabla f = \left(\dfrac{\partial f}{\partial x}, \dfrac{\partial f}{\partial y} \right) = \dfrac{\partial f}{\partial x} \mathbf{i} + \dfrac{\partial f}{\partial y} \mathbf{j} $$

为函数 $f$ 在点 $P$ 处的梯度,其中 $\mathbf{i}$$\mathbf{j}$ 为单位向量,分别指向 $x$$y$ 坐标方向。

散度(divergence)将向量空间上的一个向量场对应到一个标量场上,记为 $\nabla \cdot$。散度的意义是场的有源性,当 $\nabla \cdot F > 0$ 时,表示该点是发源点;当 $\nabla \cdot F < 0$ 时,表示该点是汇聚点;当 $\nabla \cdot F = 0$ 时,表示该点无源,如下图所示。

拉普拉斯离散化后即为拉普拉斯矩阵(laplacian matrix),也称为调和矩阵(harmonic matrix)。离散化的拉普拉斯算子形式如下:

$$ \begin{aligned} \Delta f & = \dfrac{\partial^2 f}{\partial x^2} + \dfrac{\partial^2 f}{\partial y^2} \\ & = f \left(x + 1, y\right) + f \left(x - 1, y\right) - 2 f \left(x, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 2 f \left(x, y\right) \\ & = f \left(x + 1, y\right) + f \left(x - 1, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 4 f \left(x, y\right) \end{aligned} $$

从上述离散化后的拉普拉斯算子形式可以看出,拉普拉斯矩阵表示的是对矩阵进行微小扰动后获得的收益。

设图 $G$$n$ 个节点,节点的邻域为 $N$,图上的函数 $f = \left(f_1, f_2, \cdots, f_n\right)$,其中 $f_i$ 表示节点 $i$ 处的函数值。对 $i$ 进行扰动,其可能变为邻域内的任意一个节点 $j \in N_i$

$$ \Delta f_{i}=\sum_{j \in N_{i}}\left(f_{i}-f_{j}\right) $$

设每一条边 $e_{ij}$ 的权重为 $w_{ij}$$w_{ij} = 0$ 表示节点 $i$ 和节点 $j$ 不相邻,则有:

$$ \begin{aligned} \Delta f_i & = \sum_{j \in N} w_{ij} \left(f_i - f_j\right) \\ & = \sum_{j \in N} w_{ij} f_i - \sum_{j \in N} w_{ij} f_i \\ & = d_i f_i - W_{i:} f \end{aligned} $$

对于所有节点有:

$$ \begin{aligned} \Delta f & = \left(\begin{array}{c} \Delta f_{1} \\ \vdots \\ \Delta f_{N} \end{array}\right)=\left(\begin{array}{c} d_{1} f_{1}-W_{1:} f \\ \vdots \\ d_{N} f_{N}-W_{N:} f \end{array}\right) \\ & = \left(\begin{array}{ccc} d_{1} & \cdots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \cdots & d_{N} \end{array}\right) f-\left(\begin{array}{c} W_{1:} \\ \vdots \\ W_{N:} \end{array}\right) f \\ & = diag \left(d_i\right) f - W f \\ & = \left(D - W\right) f \\ & = L f \end{aligned} $$

令图 $G$ 的邻接矩阵为 $W$,度矩阵为 $D$,从上式可知拉普拉斯矩阵 $L = D - W$,其中:

$$ L_{ij} = \left\{\begin{array}{ll} \deg \left(v_i\right) & \text{如果 } i = j \\ -1 & \text{如果 } i \neq j \text{ 且 } v_i \text{ 与 } v_j \text{ 相邻} \\ 0 & \text{其他情况} \end{array}\right. $$

以下面的图为例:

邻接矩阵为:

$$ \left(\begin{array}{llllll} 0 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 & 1 & 1 \\ 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \end{array}\right) $$

度矩阵为:

$$ \left(\begin{array}{cccccc} 2 & 0 & 0 & 0 & 0 & 0 \\ 0 & 3 & 0 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 & 0 & 0 \\ 0 & 0 & 0 & 3 & 0 & 0 \\ 0 & 0 & 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 \end{array}\right) $$

拉普拉斯矩阵为:

$$ \left(\begin{array}{rrrrrr} 2 & -1 & 0 & 0 & -1 & 0 \\ -1 & 3 & -1 & 0 & -1 & 0 \\ 0 & -1 & 2 & -1 & 0 & 0 \\ 0 & 0 & -1 & 3 & -1 & -1 \\ -1 & -1 & 0 & -1 & 3 & 0 \\ 0 & 0 & 0 & 1 & 0 & 1 \end{array}\right) $$

开放资源

常用网络算法包

名称 语言
NetworkX Python
graph-tool Python
SNAP C++, Python
NetworKit C++, Python
igraph C, C++, Python, R
lightgraphs Julia

不同扩展包之间的性能比较如下表所示 9

数据集 算法 graph-tool igraph LightGraphs NetworKit NetworkX SNAP
Amazon CC 0.08 0.22 0.07 0.09 2.22 0.31
Amazon k-core 0.08 0.15 0.04 0.15 3.63 0.37
Amazon loading 2.61 0.57 4.66 0.98 4.72 1.61
Amazon page rank 0.04 0.57 0.02 0.02 8.59 0.58
Amazon shortest path 0.03 0.05 0.01 0.04 1.37 0.12
Google CC 0.28 1.38 0.29 0.37 7.77 1.56
Google k-core 0.39 0.92 0.16 0.83 42.6 1.31
Google loading 11.02 3.87 16.75 4.38 19.24 7.56
Google page rank 0.36 2.42 0.06 0.1 33.5 2.31
Google shortest path 0.08 0.41 0.01 0.14 3.41 0.26
Pokec CC 1.83 3.96 1.5 1.75 61.74 9.75
Pokec k-core 3.6 5.99 0.95 5.05 296.26 6.91
Pokec loading 71.46 25.75 170.63 26.77 140.19 52.73
Pokec page rank 1.1 23.39 0.21 0.24 239.75 8.62
Pokec shortest path 0.48 0.6 0.05 0.56 5.65 2.3

常用网络可视化软件

软件 平台
Cytoscape Windows, macOS, Linux
Gephi Windows, macOS, Linux
Tulip Windows, macOS, Linux
Pajek Windows

不同可视化软件之间的比较如下表所示 10

Cytoscape Tulip Gephi Pajek
Scalability ⭑⭑ ⭑⭑⭑ ⭑⭑⭑⭑
User friendliness ⭑⭑ ⭑⭑⭑⭑ ⭑⭑⭑
Visual styles ⭑⭑⭑⭑ ⭑⭑ ⭑⭑⭑
Edge bundling ⭑⭑⭑ ⭑⭑⭑⭑ ⭑⭑ -
Relevance to biology ⭑⭑⭑⭑ ⭑⭑ ⭑⭑⭑
Memory efficiency ⭑⭑ ⭑⭑⭑ ⭑⭑⭑⭑
Clustering ⭑⭑⭑⭑ ⭑⭑⭑ ⭑⭑
Manual node/edge editing ⭑⭑⭑ ⭑⭑⭑⭑ ⭑⭑⭑
Layouts ⭑⭑⭑ ⭑⭑ ⭑⭑⭑⭑
Network profiling ⭑⭑⭑⭑ ⭑⭑ ⭑⭑⭑
File formats ⭑⭑ ⭑⭑⭑ ⭑⭑⭑⭑
Plugins ⭑⭑⭑⭑ ⭑⭑ ⭑⭑⭑
Stability ⭑⭑⭑ ⭑⭑⭑⭑ ⭑⭑⭑
Speed ⭑⭑ ⭑⭑⭑ ⭑⭑⭑⭑
Documentation ⭑⭑⭑⭑ ⭑⭑ ⭑⭑⭑

其中,⭑ 表示较弱、⭑⭑ 表示中等、⭑⭑⭑ 表示较好、⭑⭑⭑⭑ 表示优秀。


  1. Newman, M. E. J. (2014) 网络科学引论. 电子工业出版社. ↩︎

  2. https://zh.wikipedia.org/wiki/戴克斯特拉算法 ↩︎

  3. https://zh.wikipedia.org/zh-hans/Floyd-Warshall算法 ↩︎

  4. Kernighan, B. W., & Lin, S. (1970). An efficient heuristic procedure for partitioning graphs. The Bell system technical journal, 49(2), 291-307. ↩︎

  5. Fortunato, S. (2010). Community detection in graphs. Physics reports, 486(3-5), 75-174. ↩︎

  6. Liu, F., Xue, S., Wu, J., Zhou, C., Hu, W., Paris, C., … & Yu, P. S. (2020). Deep Learning for Community Detection: Progress, Challenges and Opportunities. arXiv preprint arXiv:2005.08225↩︎

  7. 骆志刚, 丁凡, 蒋晓舟, & 石金龙. (2011). 复杂网络社团发现算法研究新进展. 国防科技大学学报, (1), 12. ↩︎

  8. Blondel, V. D., Guillaume, J. L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of statistical mechanics: theory and experiment, 2008(10), P10008. ↩︎

  9. Benchmark of popular graph/network packages v2 ↩︎

  10. Pavlopoulos, G. A., Paez-Espino, D., Kyrpides, N. C., & Iliopoulos, I. (2017). Empirical comparison of visualization tools for larger-scale network analysis. Advances in bioinformatics, 2017. ↩︎

真实世界网络结构 (Structure of Real-World Network)

2020-11-28 08:00:00

本文为《复杂网络系列》文章
本文内容主要参考自:《网络科学引论》1

分支

在无向网络中,一个典型的现象是很多网络都有一个分支,该分支占据了网络的绝大部分,而剩余部分则被划分为大量的小分支,这些小分支之间彼此并不相连。如下图所示:

一个网络通常不能有两个或更多占据网络大部分的大分支。如果将一个 $n$ 个顶点的网络分解为两个分支,每个分支约为 $\dfrac{1}{2} n$ 个顶点,则两个分支的顶点之间会有 $\dfrac{1}{4} n^2$ 个顶点对,这些顶点对有可能一个顶点在一个大分支中,而另一个顶点在另外一个大分支中。如果在任何一个顶点对之间有一条边,那么这两个分支就会合并为一个分支。

有向图中分支分为两种:弱连通分支强连通分支。弱连通分支的定义与无向网络的分支定义类似,强连通分支是指网络顶点的一个最大子集,该子集中的顶点能够通过有向路径到达其余所有顶点,同时也能够通过有向路径从其余所有顶点到达。

每个连通分支拥有外向分支(即从强连通分支中的任意顶点出发,沿着有向路径能够到达的所有顶点的集合)和内向分支(即沿着有向路径能够到达强连通分支的所有顶点的集合)。利用**“领结”图**可以很好地刻画有向网络的总体情况,万维网的“领结”图如下所示:

小世界效应

小世界效应(small-world effect)是指对于大多数网络而言,网络顶点之间的测地距离都惊人的小,例如:六度分隔理论。网络的数学模型显示出网络测地路径长度的数量级通常与网络定点数 $n$ 成对数关系 ,即 $\log n$

度分布

顶点的度是指连接到它的边的数量。度分布(degree distribution)$p_k$ 是指网络中节点度的概率分布,也可以理解为从网络中随机选择一个顶点,其度为 $k$ 的概率。度序列(degree sequence)是指所有顶点度的集合。

根据度 $k$ 描述出大型网络的度分布有着非常重要的作用,下图给出了 Internet 的度分布:

现实世界中,几乎所有网络的度分布都有类似的由度较大的核心顶点构成的尾部,统计上称为右偏(right-skewed)的。

幂律和无标度网络

以 Internet 为例,下图给出了度分布的一个有趣特征,下图使用了对数标度重新绘制了上图的直方图:

如上图所示,对数处理后,分布大致遵循一条直线。度分布 $p_k$ 的对数与度 $k$ 的对数之间具有线性函数关系:

$$ \ln p_k = - \alpha \ln k + c $$

对两侧同时做指数运算,有:

$$ p_k = C k^{- \alpha} $$

其中,$C = e^c$ 是一个常数。这种形式的分布,即按照 $k$ 的幂变化,称为幂律(power law)。在不同类型的网络中,幂律度分布是普遍存在的,常数 $\alpha$ 是幂律的指数,该值的典型取值区间为 $2 \leq \alpha \leq 3$。通常,度分布并非在整个区间都遵循幂律分布,当 $k$ 较小时,度分布并不是单调的。具有幂律度分布的网络也称为无标度网络(scale-free network)。

观察幂律分布的另外一种方式是构建累积分布函数,定义如下:

$$ P_k = \sum_{k' = k}^{\infty} p_{k'} $$

假设度分布 $p_k$ 在尾部服从幂律,确切地讲,对于某个 $k_{\min}$,当 $k \geq k_{\min}$ 时有 $p_k = C k^{- \alpha}$,则对于 $k \geq k_{\min}$,有:

$$ P_{k}=C \sum_{k^{\prime}=k}^{\infty} k^{\prime-\alpha} \simeq C \int_{k}^{\infty} k^{\prime-\alpha} \mathrm{d} k^{\prime}=\frac{C}{\alpha-1} k^{-(\alpha-1)} $$

这里通过积分来近似求和是合理的,因为当 $k$ 值较大时,幂律函数的变化率较小。所以,如果度分布 $p_k$ 服从幂律,那么 $p_k$ 的累积分布函数也服从幂律。

聚类系数

聚类系数是度量某个顶点的两个邻居顶点也互为邻居的平均概率。该测度计算值与随机条件下得到的期望值之间有较大的差异,这种巨大差异可能也显示出了真正发挥作用的社会效应。在合作网络中,与随机选择合作者相比,实际的合作网络中包含更多的三角形结构。这种现象背后有很多原因,其中一个原因可能是人们会介绍其合作者认识,而这些合作者两两之间也开始进行合作。

随着度的增加,局部聚类系数不断减少,这种现象的一个可能的解释是顶点分成紧密的群组或社团,同一个群组内部的顶点之间连接较多。在表现出此类行为的网络中,属于小型群组的顶点的度较小,因为这种群组的成员也相对较少,但在较大的群组中的顶点的度较大。同时,小型群组中的顶点的局部聚类系数较高。出现这种情况是因为将每个群组与网络的其余部分隔离开之后,每个群组大体上相当于一个小型网络,较小的网络会有更大的聚类系数。当对不同规模的网络取平均之后,会发现度小的顶点具有较高的聚类系数,如下图所示:


  1. Newman, M. E. J. (2014) 网络科学引论. 电子工业出版社. ↩︎

网络表示,测度和度量 (Network Representation, Measures & Metrics)

2020-11-21 08:00:00

本文为《复杂网络系列》文章,本文内容主要参考自:《网络科学引论》

网络(network)也称为(graph),是一个由多个顶点(vertex)及连接顶点的(edge)组成的集合。在网络中,我们通常用 $n$ 表示顶点的数目,用 $m$ 表示边的数目。在大多数网络中两个顶点之间都只有一条边,极少数情况下,两个顶点之间有多条边,称之为重边(multiedge)。在极特殊情况下,还会存在连接到顶点自身的边,称之为自边(self-edge)。既没有自边也没有重边的图称之为简单网络(simple network)或简单图(simple graph),存在重边的网络称之为重图(multigraph)。相关概念示例如下:

网络表示

无向网络

对于一个包含 $n$ 个顶点的无向图,可以用整数 $1$$n$ 对各个顶点进行标注。如果用 $\left(i, j\right)$ 表示顶点 $i$ 和顶点 $j$ 之间的边,那么通过给定 $n$ 的值及所有边的列表就能表示一个完整的网络,这种表示方法称之为边列表(edge list)。

相比于边列表,邻接矩阵(adjacency matrix)可以更好地表示网络。一个简单图的邻接矩阵 $\mathbf{A}$ 中元素 $A_{ij}$ 的含义如下:

$$ A_{ij}=\left\{\begin{array}{ll} 1 & \text{如果顶点 } i \text{ 和顶点 } j \text{ 之间存在一条边} \\ 0 & \text{其他} \end{array}\right. $$

对于一个没有自边的网络,其邻接矩阵有两个特点:

  1. 邻接矩阵对角线上的元素取值均为零。
  2. 邻接矩阵是对称的。

加权网络

对于加权网络(weighted network)和赋值网络(valued network)可以将邻接矩阵中对应元素的值设定为相应的权重的方式来进行表示。

有向网络

有向网络(directed network)或有向图(directed graph)有时简称为 digraph,在这类网络中,每条边都有方向,从一个顶点指向另一个顶点,称之为有向边(directed edge)。

注意

有向网络的邻接矩阵中元素 $A_{ij} = 1$ 时表示存在从顶点 $j$ 到顶点 $i$ 的边。虽然表示方法有些出人意料,但在数据计算上会带来极大的方便。

超图

在某些类型的网络中,一些边会同时连接多个顶点。例如:创建一个社会网络,用来表示一个大规模社区中的各个家庭。每个家庭都可能会有两名或多名成员,因此表示这些家庭之间关系的做好方法就是使用一种广义边来同时连接多个顶点。这样的边称之为超边(hyperedge),含有超边的网络称之为超图(hypergraph)。下图 (a) 表示一个小型超图,其中超边用环的形式表示。

当一个网络中的顶点因为某种群组之间的关系被连接在一起时,可以使用超图来表示这个网络,在社会学中,这样的网络称之为隶属网络。对于超图,可于采用二分图的方式进行表示,通过引入 4 个新的顶点代表 4 个群组,在顶点及其所属群组之间通过边连接,如上图 (b) 所示。

二分网络

群组内成员之间的关系可以用超图中的超边表示,也可以等价地用更方便的二分图(bipartite network)表示。这种网络中有两类顶点,一类顶点代表原始顶点,另一类顶点则表示原始顶点所属的群组。

二分网络中,与邻接矩阵等价的是一个矩形矩阵,称之为关联矩阵(incidence matrix)。如果 $n$ 代表人数或网络中的成员数目,$g$ 是群组的数目,那么关联矩阵 $\mathbf{B}$ 是一个 $g \times n$ 的矩阵,其元素 $B_{ij}$ 的取值含义如下:

$$ B_{ij}=\left\{\begin{array}{ll} 1 & \text{如果顶点 } j \text{ 属于群组 } i \\ 0 & \text{其他} \end{array}\right. $$

研究统一类型顶点之间的直接联系可以通过对二分网络进行单模投影(one-mode projection),推导出同类顶点之间的直接联系,如下图所示。

**树(tree)**是连通的、无向的且不包含闭合循环的网络,如下图所示。

连通是指任意两个顶点之间都存在一条相互可达的路径。一个网络可能有两个或多个部分组成,每个部分相互之间不连通,如果任意单独的部分都为树,则称这个网络为森林(forest)。

由于树没有闭合循环,因此任意两个顶点之间有且只有一条相连的路径。如果一个树有 $n$ 个顶点,那么它有且仅有 $n - 1$ 条边。

图中顶点的(degree)是指与其直接相连的边数目。将顶点 $i$ 的度表示为 $k_i$,对于有 $n$ 个顶点构成的无向图,可利用邻接矩阵将度表示为:

$$ k_i = \sum_{j=1}^{n} A_{ij} $$

在无向图中,每个边都有两端,如果一共有 $m$ 条边,那么就有 $2m$ 个边的端点。同时,边的端点数与所有顶点度的总和相等:

$$ 2m = \sum_{j=1}^{n} k_i $$

$$ m = \dfrac{1}{2} \sum_{i=1}^{n} k_i = \dfrac{1}{2} \sum_{ij} A_{ij} $$

无向图中顶点度的均值 $c$ 为:

$$ c = \dfrac{1}{n} \sum_{i=1}^{n} k_i $$

综上可得:

$$ c = \dfrac{2m}{n} $$

在一个简单图中,可能的边数的最大值是 $\dbinom{n}{2} = \dfrac{1}{2} n \left(n - 1\right)$ 个。图的连通度(connectance)或密度(density)$\rho$ 是所有图中实际出现的边的数目与边数最大值之间的比值:

$$ \rho = \dfrac{m}{\dbinom{n}{2}} = \dfrac{2m}{n \left(n - 1\right)} = \dfrac{c}{n - 1} $$

在有向图中,每个顶点有两个度:入度(in-degree)是连接到该顶点的入边的数目,出度(out-degree)是出边数目。当从顶点 $j$$i$ 有一条边时,邻接矩阵中对应的元素 $A_{ij} = 1$,则入度和出度记为:

$$ k_i^{\text{in}} = \sum_{j=1}^{n} A_{ij}, k_j^{\text{out}} = \sum_{i=1}^{n} A_{ij} $$

在有向图中,边的数目 $m$ 等于入边的端点数总和,也等于出边的端点数总和,有:

$$ m=\sum_{i=1}^{n} k_{i}^{\mathrm{in}}=\sum_{j=1}^{n} k_{j}^{\mathrm{out}}=\sum_{i j} A_{i j} $$

每个有向图的入度的均值 $c_{\text{in}}$ 和出度的均值 $c_{\text{out}}$ 是相等的:

$$ c_{\text {in }}=\frac{1}{n} \sum_{i=1}^{n} k_{i}^{\text {in }}=\frac{1}{n} \sum_{j=1}^{n} k_{j}^{\text {out }}=c_{\text {out }} $$

简化后有:

$$ c = \dfrac{m}{n} $$

路径

网络中的路径是指由一组顶点构成的序列,序列中每两个连续顶点都通过网络中的边连接在一起,路径长度等于该路径经过的边的数目(而非顶点的数目)。从顶点 $j$ 到顶点 $i$ 存在长度为 $r$ 的路径总数为:

$$ N_{ij}^{\left(r\right)} = \left[\mathbf{A}^r\right]_{ij} $$

其中,$\left[\cdots\right]_{ij}$ 表示矩阵中的第 $i$ 行、第 $j$ 列的元素。

测地路径(geodesic path),简称为最短路径(shortest path),即两个顶点间不存在更短路径的路径。图的直径(diameter)是指图中任意一对相互连接的顶点之间的最长测地路径长度。欧拉路径(Eulerian path)是经过网络中的所有边且每条边只经过一次的路径。哈密顿路径(Hamiltonian path)是访问网络的所有顶点且每个顶点只访问一次的路径。

分支

如果一个网络中两个顶点之间不存在路径,则称这个网络是非连通(disconnected)的,如果网络中任意两个顶点之间都能找到一条路径,则称这个网络是连通(connected)的。

网络中的子群称为分支(component)。分支是网络中顶点的子集,该子集中任何两个顶点之间至少存在一条路径,在保证该性质的前提下,网络中其他顶点都不能被添加到这个子集中。在保证一个给定性质的前提下,不能再向它添加其他顶点,就称其为最大子集(maximal subset)。

连通度

如果两条路经除了起点和终点外,不共享其他任何顶点,那么这两条路径是顶点独立(vertex-independent)的。如果两条路径是顶点独立的,那么也是边独立的,反之则不成立。

两个顶点之间的独立路径数称为顶点之间的连通度(connectivity),如果明确考虑边还是顶点,则需利用边连通度(edge connectivity)及顶点连通度(vertex connectivity)的概念。

子图

令原图表示为 $G = \left(V, E\right)$,其中,$V$ 是图中所有顶点的集合,$E$ 是图中所有边的集合,有:

  1. 子图(subgraph):$G'$ 中所有顶点和边均包含于原图 $G$ 中,即 $E' \in E, V' \in V$
  2. 生成子图(spanning subgraph):$G'$ 中顶点同原图 $G$ 相同,且 $E' \in E$
  3. 导出子图(induced subgraph):$G'$ 中,$V' \in V$,同时对于 $V'$ 中任意一个顶点,只要在原图 $G$ 中有对应的边,则也应包含在 $E'$ 中。

Motif

Motif 1 被定义为反复出现的重要连接模式。这些模式在真实的网络中要比随机网络中出现的更加频繁,如下图所示:

Motif 的显著性定义为:

$$ Z_i = \dfrac{N_i^{\text{real}} - \bar{N}_i^{\text{rand}}}{\text{std} \left(N_i^{\text{rand}}\right)} $$

其中,$N_i^{\text{real}}$ 为模式在真实图中出现的次数,$N_i^{\text{rand}}$ 为模式在随机图中出现的次数。

Graphlets

Graphlets 是对 Motif 的扩展,Motif 是从全局的角度发现模式,而 Graphlets 是从局部角度出发。Graphlets 是连接的非同构子图,这里要求子图为导出子图。下图展示了节点数为 2 至 5 的所有 Graphlets:

更多关于 Motif 和 Graphlets 的细节请参见 2 3

测度和度量

中心性

度中心性

中心性(centrality)是研究“网络中哪些顶点是最重要或最核心的?”这个问题的一个概念。网络中心性的最简单的测度是顶点的度,即与顶点相连的边的数量。有时为了强调度作为中心性测度的用途,在社会学中也称之为度中心性(degree centrality)。

特征向量中心性

度中心性可自然地扩展为特征向量中心性(eigenvector centrality)。可以将度中心性理解为给某顶点所有邻居顶点赋予一个“中心性值”,但并非所有连接顶点的值都是相同的。很多情况下,一个顶点会由于连接到一些本身很重要的点,而使自身的重要性得到提升,这就是特征向量中心性的本质。

对于每个顶点 $i$,假设其中心性为 $x_i$。对于所有 $i$,可以设其初始值 $x_i = 1$,利用该值可以计算出另一个更能体现中心性的值 $x'_i$,将 $x'_i$ 定义为 $i$ 所有邻居顶点的中心性之和:

$$ x'_i = \sum_{j} A_{ij} x_j $$

重复该过程可以得到更好的估计值,重复 $t$ 步后,中心性 $\mathbf{x} \left(t\right)$ 的计算公式如下:

$$ \mathbf{x} \left(t\right) = \mathbf{A}^t \mathbf{x} \left(0\right) $$

$t \to \infty$ 时,中心性向量的极限与邻接矩阵中的主特征向量成正比。因此,可以等价地认为中心性 $\mathbf{x}$ 满足:

$$ \mathbf{A} \mathbf{x} = \kappa_1 \mathbf{x} $$

其中,$\kappa_1$ 为矩阵 $\mathbf{A}$ 的特征值中的最大值。

特征向量中心性对于有向图和无向图都适用。在有向图中,邻接矩阵是非对称的,因此网络有两类特征向量,通常情况下我们选择右特征向量来定义中心性。因为在有向网络中,中心性主要是由指向顶点的顶点,而不是由顶点指向的顶点赋予的。

Katz 中心性

Katz 中心性解决了特征向量中心性中节点中心性可能为零的问题。通过为网络中每个顶点赋予少量的“免费”中心性,可以定义:

$$ x_i = \alpha \sum_{j} A_{ij} x_j + \beta $$

其中,$\alpha$$\beta$ 是正常数。使用矩阵表示可以写成:

$$ \mathbf{x} = \alpha \mathbf{A} \mathbf{x} + \beta \mathbf{1} $$

其中,$\mathbf{1}$ 代表向量 $\left(1, 1, 1, \cdots\right)$。重新整理有 $\mathbf{x} = \beta \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$,由于只关心相对值,通常可以设置 $\beta = 1$,则有:

$$ \mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1} $$

PageRank

Katz 中心性有一个不足,被一个 Katz 中心性较高的顶点指向的顶点具有较高的 Katz 中心性,但如果这个中心性较高的顶点指向大量顶点,那么这些大量被指向的顶点也会拥有较高的中心性,但这种估计并非总是恰当的。在新的中心性中,那些指向很多其他顶点的顶点,即使本身的中心性很高,但也只能传递给它指向的每个顶点少量的中心性,定义为:

$$ x_{i}=\alpha \sum_{j} A_{i j} \frac{x_{j}}{k_{j}^{\text {out }}}+\beta $$

其中,$k_j^{\text{out}}$ 为顶点的出度,当 $k_j^{\text{out}} = 0$ 时可以将其设定为任何一个非零值,都不会影响计算结果。利用矩阵的形式,可以表示为:

$$ \mathbf{x}=\alpha \mathbf{AD}^{-1} \mathbf{x}+\beta \mathbf{1} $$

其中,$\mathbf{D}$ 为对角矩阵,$D_{ii} = \max \left(k_j^{\text{out}}, 1\right)$。同之前一样,$\beta$ 只是整个公式的因子,设置 $\beta = 1$,有:

$$ \mathbf{x}=\left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1} $$

该中心性即为 PageRank

上述 4 种中心性的区别和联系如下表所示:

带有常数项 不带常数项
除以出度 $\mathbf{x} = \left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1}$
PageRank
$\mathbf{x} = \mathbf{A} \mathbf{D}^{-1} \mathbf{x}$
度中心性
不除出度 $\mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$
Katz 中心性
$\mathbf{x} = \kappa_1^{-1} \mathbf{A} \mathbf{x}$
特征向量中心性

接近度中心性

接近度中心性(closeness centrality)用于度量一个顶点到其他顶点的平均距离。

$$ C_{i}=\frac{1}{\ell_{i}}=\frac{n}{\sum_{j} d_{i j}} $$

其中,$d_{i j}$ 表示从顶点 $i$$j$ 的测地路径长度,即路径中边的总数,$\ell_{i}$ 表示从 $i$$j$ 的平均测地距离。在大多数网络中,顶点之间的测地距离一般都较小,并且随着网络规模的增长,该值只是以对数级别速度缓慢增长。

在不同分支中的两个顶点之间的测地距离定义为无穷大,则 $C_i$ 为零。为了解决这个问题,最常见的方法是只计算同一分支内部的顶点的平均测地距离。新的定义使用顶点之间的调和平均测地距离:

$$ C_{i}^{\prime}=\frac{1}{n-1} \sum_{j(\neq i)} \frac{1}{d_{i j}} $$

公式中排除了 $j = i$ 的情况,因为 $d_{ii} = 0$。结果也称之为调和中心性(harmonic centrality)。

介数中心性

介数中心性(betweenness centrality)描述了一个顶点在其他顶点之间路径上的分布程度。假设在网络中每两个顶点之间,在每个单位时间内以相等的概率交换信息,信息总是沿着网络中最短测地路径传播,如果有多条最短测地路径则随机选择。由于消息是沿着最短路径以相同的速率传播,因此经过某个顶点的消息数与经过该顶点的测地路径数成正比。测地路径数就是所谓的介数中心性,简称介数

定义 $n_{st}^i$ 为从 $s$$t$ 经过 $i$ 的测地路径数量,定义 $g_{st}$ 为从 $s$$t$ 的测地路径总数,那么顶点 $i$ 的介数中心性可以表示为:

$$ x_{i}=\sum_{s t} \frac{n_{s t}^{i}}{g_{s t}} $$

高介数中心性的顶点由于控制着其他顶点之间的消息传递,在网络中有着很强的影响力。删除介数最高的顶点,也最有可能破坏其他顶点之间的通信。

不同中心性的可视化如下图所示:

不同中心性可视化 By Tapiocozzo, CC BY-SA 4.0

其中,A:介数中心性;B:接近度中心性;C:特征向量中心性;D:度中心性;E:调和中心性;F:Katz 中心性。

传递性

传递性(transitivity)在社会网络中的重要性要比其他网络中重要得多。在数学上,对于关系“$\circ$”,如果 $a \circ b$$b \circ c$,若能推出 $a \circ c$,则称 $\circ$ 具有传递性。

完全传递性值出现在每一个分支都是全连通的子图或团的网络中。(clique)是指无向图网络中的一个最大顶点子集,在该子集中任何两个顶点之间都有一条边直接连接。完全传递性没有太多的实际意义,而部分传递性却很有用。在很多网络中,$u$ 认识 $v$$v$ 认识 $w$,并不能保证 $u$ 认识 $w$,但两者之间相互认识的概率很大。

如果 $u$ 也认识 $w$,则称该路径是闭合的。在社会网络术语中,称 $u, v, w$ 这 3 个顶点形成一个闭合三元组(closed triad)。我们将聚类系数(clustering coefficient)定义为网络中所有长度为 2 的路径中闭合路径所占的比例:

$$ C = \dfrac{\text{长度为 2 的路径中闭合路径数}}{\text{长度为 2 的路径数}} $$

其取值范围在 0 到 1 之间。社会网络的聚类系数比其他网络偏高。

对于顶点 $i$,定地单个顶点的聚类系数为:

$$ C_i = \dfrac{\text{顶点 i 的邻居顶点中直接相连的顶点对数}}{\text{顶点 i 的邻居顶点对总数}} $$

$C_i$ 也称为局部聚类系数(local clustering coefficient),该值代表了 $i$ 的朋友之间互为朋友的平均概率。

相互性

聚类系数观察的是长度为 3 的循环,长度为 2 的循环的频率通过相互性(reciprocity)来度量,该频率描述了两个顶点之间相互指向的概率。

相似性

社会网络分析的另一个核心概念是顶点之间的相似性。构造网络相似性的测度有两种基本方法:结构等价(structural equivalence)和规则等价(regular equivalence),如下图所示:

结构等价

针对无向网络中,最简单和最显而易见的结构等价测度就是计算两个顶点的共享邻居顶点数。在无向网络中,顶点 $i$$j$ 的共享邻居顶点数表示为 $n_{ij}$,有:

$$ n_{ij} = \sum_{k} A_{ik} A_{kj} $$

利用余弦相似度可以更好的对其进行度量。将邻接矩阵的第 $i$ 和第 $j$ 行分别看成两个向量,然后将这两个向量之间的夹角余弦值用于相似性度量,有:

$$ \sigma_{i j}=\cos \theta=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{\sum_{k} A_{i k}^{2}} \sqrt{\Sigma_{k} A_{j k}^{2}}} $$

假设网络是不带权重的简单图,上式可以化简为:

$$ \sigma_{i j}=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{k_{i}} \sqrt{k_{j}}}=\frac{n_{i j}}{\sqrt{k_{i} k_{j}}} $$

其中,$k_i$ 是顶点 $i$ 的度。余弦相似度的取值范围为从 0 到 1,1 表示两个顶点之间拥有完全相同的邻居节点。

皮尔逊相关系数通过同随机选择邻居顶点条件下共享邻居顶点数的期望值进行比较的方式进行计算,得到的标准的皮尔逊相关系数为:

$$ r_{i j}=\frac{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)\left(A_{j k}-\left\langle A_{j}\right\rangle\right)}{\sqrt{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)^{2}} \sqrt{\sum_{k}\left(A_{j k}-\left\langle A_{j}\right\rangle\right)^{2}}} $$

上式的取值范围从 -1 到 1,数值越大表明两者之间越相似。

规则等价

规则等价的顶点不必共享邻居顶点,但是两个顶点的邻居顶点本身要具有相似性。一些简单的代数测度思想如下:定义一个相似性值 $\sigma_{ij}$,若顶点 $i$$j$ 各自的邻居顶点 $k$$l$ 本身具有较高的相似性,则 $i$$j$ 的相似性也较高。对于无向网络,有以下公式:

$$ \sigma_{i j}=\alpha \sum_{k l} A_{i k} A_{j l} \sigma_{k l} $$

或者利用矩阵性质表示为 $\mathbf{\sigma} = \alpha \mathbf{A \sigma A}$

同质性

在社会网络中,人们倾向于选择那些他们认为与其自身在某些方面相似的人作为朋友,这种倾向性称为同质性(homophily)或同配混合(assortative mixing)。

依据枚举特征的同配混合

假设有一个网络,其顶点根据某个枚举特征(例如:国籍、种族、性别等)分类,且该特征的取值是一个有限集合。如果网络中连接相同类型顶点之间的边所占比例很大,那么该网络就是同配的。量化同配性简单的方法是观测这部分边占总边数的比例,但这并不是很好的度量方法,因为如果所有顶点都是同一个类型,那么测度值就是 1。

好的测度可以通过首先找出连接同类顶点的边所占的比例,然后减去在不考虑顶点类型时,随机连接的边中,连接两个同类顶点的边所占比例的期望值的方式得到。常用的测度为模块度(modularity):

$$ Q=\frac{1}{2 m} \sum_{i j}\left(A_{i j}-\frac{k_{i} k_{j}}{2 m}\right) \delta_{g_{i} g_{i}} $$

其中,$k_i$ 为顶点 $i$ 的度,$g_i$ 为顶点 $i$ 的类型,$m$ 为总边数,$\delta_{ij}$克罗内克函数。该值严格小于 1,如果同类顶点之间边数的实际值大于随机条件下的期望值,则该值为正数,否则为负数,值为正说明该网络是同配混合的。

依据标量特征的同配混合

如果根据标量特征(例如:年龄、收入等)来度量网络中的同质性。由于该类特征具有确定的顺序,因此根据标量的数值,不仅可以指出两个顶点在什么情况下是完全相同的,也可以指出它们在真么情况下是近似相同的。

$x_i$ 为顶点 $i$ 的标量值,$\left(x_i, x_j\right)$ 为网络中每一条边 $\left(i, j\right)$ 的两个端点的值,利用协方差可以得到同配系数

$$ r=\frac{\sum_{i j}\left(A_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}}{\sum_{i j}\left(k_{i} \delta_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}} $$

该系数在全同配混合网络中取最大值 1,在全异配混合网络中取最小值 -1,值 0 意味着边两端的顶点值是非相关的。

依据度的同配混合

依据度的同配混合是依据标量特征的同配混合的一个特例。依据度的同配混合网络中,高度数顶点倾向于与其他高度数顶点相连,而低度数顶点倾向于与其他低度数顶点相连。

在同配网络中,度大的顶点倾向于聚集在一起的网络中,我们希望得到网络中这些度大的顶点构成的顶点块或核,它们周围是一些度小的顶点构成的低密度边缘(periphery)。这种核心/边缘结构(core/periphery structure)是社会网络的普遍特征。

上图 (a) 给出了一个小型的同配混合网络,其核心/边缘结构明显,上图 (b) 给出了一个小型异配混合网络,通常不具备核心/边缘结构,但顶点的分布更加均匀。


  1. Milo, R., Shen-Orr, S., Itzkovitz, S., Kashtan, N., Chklovskii, D., & Alon, U. (2002). Network motifs: simple building blocks of complex networks. Science, 298(5594), 824-827. ↩︎

  2. Jain, D., & Patgiri, R. (2019, April). Network Motifs: A Survey. In International Conference on Advances in Computing and Data Sciences (pp. 80-91). Springer, Singapore. ↩︎

  3. Henderson, K., Gallagher, B., Eliassi-Rad, T., Tong, H., Basu, S., Akoglu, L., … & Li, L. (2012, August). Rolx: structural role extraction & mining in large graphs. In Proceedings of the 18th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1231-1239). ↩︎

文本相似度 (Text Similarity)

2020-10-31 08:00:00

文本相似度是指衡量两个文本的相似程度,相似程度的评价有很多角度:单纯的字面相似度(例如:我和他 v.s. 我和她),语义的相似度(例如:爸爸 v.s. 父亲)和风格的相似度(例如:我喜欢你 v.s. 我好喜欢你耶)等等。

文本表示角度

统计模型

文本切分

在中文和拉丁语系中,文本的直观表示就存在一定的差异,拉丁语系中词与词之间存在天然的分隔符,而中文则没有。

I can eat glass, it doesn’t hurt me.
我能吞下玻璃而不伤身体。

因此针对拉丁语系的文本切分相对中文容易许多。

N-gram (N 元语法) 是一种文本表示方法,指文中连续出现的 $n$ 个词语。N-gram 模型是基于 $n-1$ 阶马尔科夫链的一种概率语言模型,可以通过前 $n-1$ 个词对第 $n$ 个词进行预测。以 南京市长江大桥 为例,N-gram 的表示如下:

一元语法(unigram):南/京/市/长/江/大/桥
二元语法(bigram):南京/京市/市长/长江/江大/大桥
三元语法(trigram):南京市/京市长/市长江/长江大/江大桥
import re
from nltk.util import ngrams

s = '南京市长江大桥'
tokens = re.sub(r'\s', '', s)

list(ngrams(tokens, 1))
# [('南',), ('京',), ('市',), ('长',), ('江',), ('大',), ('桥',)]

list(ngrams(tokens, 2))
# [('南', '京'), ('京', '市'), ('市', '长'),
#  ('长', '江'), ('江', '大'), ('大', '桥')]

list(ngrams(tokens, 3, pad_left=True, pad_right=True, left_pad_symbol='<s>', right_pad_symbol='</s>'))
# [('<s>', '<s>', '南'),
#  ('<s>', '南', '京'),
#  ('南', '京', '市'),
#  ('京', '市', '长'),
#  ('市', '长', '江'),
#  ('长', '江', '大'),
#  ('江', '大', '桥'),
#  ('大', '桥', '</s>'),
#  ('桥', '</s>', '</s>')]

分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符,虽然英文也同样存在短语的划分问题,不过在词这一层上,中文比之英文要复杂得多、困难得多。

s = '南京市长江大桥'

# jieba
# https://github.com/fxsjy/jieba
import jieba

list(jieba.cut(s, cut_all=False))
# ['南京市', '长江大桥']

list(jieba.cut(s, cut_all=True))
# ['南京', '南京市', '京市', '市长', '长江', '长江大桥', '大桥']

list(jieba.cut_for_search(s))
# ['南京', '京市', '南京市', '长江', '大桥', '长江大桥']

# THULAC
# https://github.com/thunlp/THULAC-Python
import thulac

thulac_ins = thulac.thulac()

thulac_ins.cut(s)
# [['南京市', 'ns'], ['长江', 'ns'], ['大桥', 'n']]

# PKUSEG
# https://github.com/lancopku/PKUSeg-python
import pkuseg

seg = pkuseg.pkuseg(postag=True)

seg.cut(s)
# [('南京市', 'ns'), ('长江', 'ns'), ('大桥', 'n')]

# HanLP
# https://github.com/hankcs/HanLP
import hanlp

tokenizer = hanlp.load('LARGE_ALBERT_BASE')

tokenizer(s)
# ['南京市', '长江', '大桥']

主题模型

除了对文本进行切分将切分后结果全部用于表示文本外,还可以用部分字词表示一篇文档。主题模型(Topic Model)在机器学习和自然语言处理等领域是用来在一系列文档中发现抽象主题的一种统计模型。

What is Topic Model

直观来讲,如果一篇文章有一个中心思想,那么一些特定词语会更频繁的出现。比方说,如果一篇文章是在讲狗的,那“狗”和“骨头”等词出现的频率会高些。如果一篇文章是在讲猫的,那“猫”和“鱼”等词出现的频率会高些。而有些词例如“这个”、“和”大概在两篇文章中出现的频率会大致相等。但真实的情况是,一篇文章通常包含多种主题,而且每个主题所占比例各不相同。因此,如果一篇文章 10% 和猫有关,90% 和狗有关,那么和狗相关的关键字出现的次数大概会是和猫相关的关键字出现次数的 9 倍。

一个主题模型试图用数学框架来体现文档的这种特点。主题模型自动分析每个文档,统计文档内的词语,根据统计的信息来断定当前文档含有哪些主题,以及每个主题所占的比例各为多少 1

TF-IDF 是 Term Frequency - Inverse Document Frequency 的缩写,即“词频-逆文本频率”。TF-IDF 可以用于评估一个字词在语料中的一篇文档中的重要程度,基本思想是如果某个字词在一篇文档中出现的频率较高,而在其他文档中出现频率较低,则认为这个字词更能够代表这篇文档。

形式化地,对于文档 $y$ 中的字词 $x$ 的 TF-IDF 重要程度可以表示为:

$$ w_{x, y} = tf_{x, y} \times \log \left(\dfrac{N}{df_{x}}\right) $$

其中,$tf_{x, y}$ 表示字词 $x$ 在文档 $y$ 中出现的频率,$df_x$ 为包含字词 $x$ 的文档数量,$N$ 为语料中文档的总数量。

14 万歌词语料 为例,通过 TF-IDF 计算周杰伦的《简单爱》中最重要的 3 个词为 ['睡着', '放开', '棒球']

BM25 算法的全称为 Okapi BM25,是一种搜索引擎用于评估查询和文档之间相关程度的排序算法,其中 BM 是 Best Match 的缩写。

对于一个给定的查询 $Q$,包含的关键词为 $q_1, \cdots, q_n$,一个文档 $D$ 的 BM25 值定义为:

$$ \operatorname{score}(D, Q)=\sum_{i=1}^{n} \operatorname{IDF}\left(q_{i}\right) \cdot \frac{f\left(q_{i}, D\right) \cdot\left(k_{1}+1\right)}{f\left(q_{i}, D\right)+k_{1} \cdot\left(1-b+b \cdot \frac{|D|}{\text { avgdl }}\right)} $$

其中,$f\left(q_{i}, D\right)$ 表示 $q_i$ 在文档 $D$ 中的词频,$|D|$ 表示文档 $D$ 中的词数,$\text{avgdl}$ 表示语料中所有文档的平均长度。$k_1$$b$ 为自由参数,通常取值为 $k_1 \in \left[1.2, 2.0\right], b = 0.75$ 2$\operatorname{IDF} \left(q_i\right)$ 表示词 $q_i$ 的逆文档频率,通常计算方式如下:

$$ \operatorname{IDF}\left(q_{i}\right)=\ln \left(\frac{N-n\left(q_{i}\right)+0.5}{n\left(q_{i}\right)+0.5}+1\right) $$

其中,$N$ 为语料中文档的总数量,$n \left(q_i\right)$ 表示包含 $q_i$ 的文档数量。

BM25 算法是对 TF-IDF 算法的优化,在词频的计算上,BM25 限制了文档 $D$ 中关键词 $q_i$ 的词频对评分的影响。为了防止词频过大,BM25 将这个值的上限设置为 $k_1 + 1$

同时,BM25 还引入了平均文档长度 $\text{avgdl}$,不同的平均文档长度 $\text{avgdl}$ 对 TF 分值的影响如下图所示:

TextRank 3 是基于 PageRank 4 算法的一种关键词提取算法。PageRank 最早是用于 Google 的网页排名,因此以公司创始人拉里·佩奇(Larry Page)的姓氏来命名。PageRank 的计算公式如下:

$$ S\left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in I n\left(V_{i}\right)} \frac{1}{\left|O u t\left(V_{j}\right)\right|} S\left(V_{j}\right) $$

其中,$V_i$ 表示任意一个网页,$V_j$ 表示链接到网页 $V_i$ 的网页,$S \left(V_i\right)$ 表示网页 $V_i$ 的 PageRank 值,$In \left(V_i\right)$ 表示网页 $V_i$ 所有的入链集合,$Out \left(V_j\right)$ 表示网页 $V_j$ 所有的出链集合,$|\cdot|$ 表示集合的大小,$d$ 为阻尼系数,是为了确保每个网页的 PageRank 值都大于 0。

TextRank 由 PageRank 改进而来,计算公式如下:

$$ WS \left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in In\left(V_{i}\right)} \frac{w_{j i}}{\sum_{V_{k} \in Out\left(V_{j}\right)} w_{j k}} WS \left(V_{j}\right) $$

相比于 PageRank 公式增加了权重项 $W_{ji}$,用来表示两个节点之间的边的权重。TextRank 提取关键词的算法流程如下:

  1. 将文本进行切分得到 $S_i = \left[t_{i1}, t_{i2}, \cdots, t_{in}\right]$
  2. $S_i$ 中大小为 $k$ 的滑动窗口中的词定义为共现关系,构建关键词图 $G = \left(V, E\right)$
  3. 根据 TextRank 的计算公式对每个节点的值进行计算,直至收敛。
  4. 对节点的 TextRank 的值进行倒叙排序,获取前 $n$ 个词作为关键词。

潜在语义分析(LSA, Latent Semantic Analysis)5 的核心思想是将文本的高维词空间映射到一个低维的向量空间,我们称之为隐含语义空间。降维可以通过奇异值分解(SVD)实现,令 $X$ 表示语料矩阵,元素 $\left(i, j\right)$ 表示词 $i$ 和文档 $j$ 的共现情况(例如:词频):

$$ X = \mathbf{d}_{j} \cdot \mathbf{t}_{i}^{T} = \left[\begin{array}{c} x_{1, j} \\ \vdots \\ x_{i, j} \\ \vdots \\ x_{m, j} \end{array}\right] \cdot \left[\begin{array}{ccccc} x_{i, 1} & \ldots & x_{i, j} & \ldots & x_{i, n} \end{array}\right] = \left[\begin{array}{ccccc} x_{1,1} & \ldots & x_{1, j} & \ldots & x_{1, n} \\ \vdots & \ddots & \vdots & \ddots & \vdots \\ x_{i, 1} & \ldots & x_{i, j} & \ldots & x_{i, n} \\ \vdots & \ddots & \vdots & \ddots & \vdots \\ x_{m, 1} & \ldots & x_{m, j} & \ldots & x_{m, n} \end{array}\right] $$

利用奇异值分解:

$$ X = U \Sigma V^{T} $$

取最大的 $K$ 个奇异值,则可以得到原始矩阵的近似矩阵:

$$ \widetilde{X} =U \widetilde{\Sigma} V^{T} $$

在处理一个新的文档时,可以利用下面的公式将原始的词空间映射到潜在语义空间:

$$ \tilde{x} =\tilde{\Sigma} ^{-1} V^{T} x_{test} $$

LSA 的优点:

  1. 低维空间可以刻画同义词
  2. 无监督模型
  3. 降维可以减少噪声,使特征更加鲁棒

LSA 的缺点:

  1. 未解决多义词问题
  2. 计算复杂度高,增加新文档时需要重新训练
  3. 没有明确的物理解释
  4. 高斯分布假设不符合文本特征(词频不为负)
  5. 维度的确定是 Ad hoc 的

概率潜语义分析(Probabilistic Latent Semantic Analysis, PLSA)6 相比于 LSA 增加了概率模型,每个变量以及相应的概率分布和条件概率分布都有明确的物理解释。

PLSA 认为一篇文档可以由多个主题混合而成,而每个主题都是词上的概率分布,文章中的每个词都是由一个固定的主题生成的,如下图所示:

针对第 $m$ 篇文档 $d_m$ 中的每个词的生成概率为:

$$ p\left(w \mid d_{m}\right)=\sum_{z=1}^{K} p(w \mid z) p\left(z \mid d_{m}\right)=\sum_{z=1}^{K} \varphi_{z w} \theta_{m z} $$

因此整篇文档的生成概率为:

$$ p\left(\vec{w} \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} p\left(w_{i} \mid z\right) p\left(z \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} \varphi_{z w_{i}} \theta_{d z} $$

PLSA 可以利用 EM 算法求得局部最优解。

PLSA 优点:

  1. 定义了概率模型,有明确的物理解释
  2. 多项式分布假设更加符合文本特征
  3. 可以通过模型选择和复杂度控制来确定主题的维度
  4. 解决了同义词和多义词的问题

PLSA 缺点:

  1. 随着文本和词的增加,PLSA 模型参数也随之线性增加
  2. 可以生成语料中的文档的模型,但不能生成新文档的模型
  3. EM 算法求解的计算量较大

隐含狄利克雷分布(Latent Dirichlet Allocation, LDA)7 在 PLSA 的基础上增加了参数的先验分布。在 PLSA 中,对于一个新文档,是无法获取 $p \left(d\right)$ 的,因此这个概率模型是不完备的。LDA 对于 $\vec{\theta}_m$$\vec{\phi}_k$ 都增加了多项式分布的共轭分布狄利克雷分布作为先验,整个 LDA 模型如下图所示:

LDA 的参数估计可以通过吉布斯采样实现。PLSA 和 LDA 的更多细节请参见《LDA 数学八卦》8

LDA 在使用过程中仍需要指定主题的个数,而层次狄利克雷过程(Hierarchical Dirichlet Processes, HDP)9 通过过程的构造可以自动训练出主题的个数,更多实现细节请参考论文。

LSA,PLSA,LDA 和 HDP 之间的演化关系如下图所示:

本节相关代码详见 这里

距离度量

本节内容源自 相似性和距离度量 (Similarity & Distance Measurement)

相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity),通常情况下我们可以利用一个距离函数定义集合 $X$ 上元素间的距离,即:

$$ d: X \times X \to \mathbb{R} $$

$$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$

Jaccard 系数的取值范围为:$\left[0, 1\right]$,0 表示两个集合没有重合,1 表示两个集合完全重合。

$$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$

与 Jaccard 系数相同,Dice 系数的取值范围为:$\left[0, 1\right]$,两者之间可以相互转换 $s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$。不同于 Jaccard 系数,Dice 系数的差异函数 $d = 1 - s$ 并不是一个合适的距离度量,因为其并不满足距离函数的三角不等式。

$$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$

其中,$X \setminus Y$ 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化,当 $\alpha = \beta = 1$ 时为 Jaccard 系数,当 $\alpha = \beta = 0.5$ 时为 Dice 系数。

Levenshtein 距离是 编辑距离 (Editor Distance) 的一种,指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如将 kitten 转成 sitting,转换过程如下:

$$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\ \to g\right) \end{split} \end{equation*} $$

编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。

对于给定的两个字符串 $s_1$$s_2$,Jaro 相似度定义为:

$$ sim = \begin{cases} 0 & \text{if} \ m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) & \text{otherwise} \end{cases} $$

其中,$\left|s_i\right|$ 为字符串 $s_i$ 的长度,$m$ 为匹配的字符的个数,$t$ 换位数目的一半。如果字符串 $s_1$$s_2$ 相差不超过 $\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$,我们则认为两个字符串是匹配的。例如,对于字符串 CRATETRACE,仅 R, A, E 三个字符是匹配的,因此 $m = 3$,尽管 C, T 均出现在两个字符串中,但是他们的距离超过了 1 (即,$\lfloor \dfrac{5}{2} \rfloor - 1$),因此 $t = 0$

Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数,其定义为:

$$ sim_w = sim_j + l p \left(1 - sim_j\right) $$

其中,$sim_j$ 为字符串 $s_1$$s_2$ 的 Jaro 相似度,$l$ 为共同前缀的长度 (规定不超过 $4$),$p$ 为调整系数 (规定不超过 $0.25$),Winkler 将其设置为 $p = 0.1$

汉明距离为两个等长字符串对应位置的不同字符的个数,也就是将一个字符串变换成另外一个字符串所需要替换的字符个数。例如:10111011001001 之间的汉明距离是 2,tonedroses 之间的汉明距离是 3。

import textdistance as td

s1 = '南京市长江大桥'
s2 = '北京市三元桥'

td.jaccard(s1, s2)
# 0.6666666666666666

td.sorensen_dice(s1, s2)
# 0.46153846153846156

td.tversky(s1, s2)
# 0.3

td.levenshtein(s1, s2)
# 4

td.jaro(s1, s2)
# 0.6428571428571429

td.hamming(s1, s2)
# 5

表示学习

基于表示学习的文本相似度计算方法的思路如下:

  1. 利用表示学习方法将不定长的文本表示为定长的实值向量。
  2. 计算转换后的实值向量相似度,用于表示两个文本的相似度。

关于文本表示学习和实值向量相似度计算请参见之前博客:词向量 (Word Embeddings)相似性和距离度量 (Similarity & Distance Measurement)预训练自然语言模型 (Pre-trained Models for NLP)

文本词法,句法和语义角度

本节主要参考自《基于词法、句法和语义的句子相似度计算方法》10

一段文本的内容分析由浅及深可以分为词法,句法和语义三个层次。

  1. 词法,以词为对象,研究包括分词,词性和命名实体等。
  2. 句法,以句子为对象,研究包括句子成分和句子结构等。
  3. 语义,研究文字所表达的含义和蕴含的知识等。

词法和句法可以统一成为语法,如下图所示:

词法

词法层以单个句子作为输入,其输出为已标记(词性,命名实体等)的词汇序列。

词汇序列的相似度计算可以采用上文中的距离度量等方式实现。

句法

句法层用于研究句子各个组成部分及其排列顺序,将文本分解为句法单位,以理解句法元素的排列方式。句法层接收词法层分析后的将其转化为依存图。

对于依存图,我们可以利用三元组 $S = \left(V_1, E, V_2\right)$ 表示任意一个依存关系,然后通过统计计算两个文本的依存图的三元组集合之间的相似度来评价句法层的相似度。此外,也可以从树结构的角度直接评价依存句法的相似度,更多细节可参考相关论文 11 12

语义

语义层用于研究文本所蕴含的意义。例如“父亲”和“爸爸”在词法层完全不同,但在语义层却具有相同的含义。针对语义相似度的两种深度学习范式如下:

第一种范式首先通过神经网络获取文本的向量表示,再通过向量之间的相似度来衡量文本的语义相似度。这种范式在提取特征时不考虑另一个文本的信息,更适合做大规模的语义相似召回,例如:DSSM 13,ARC-I 14,CNTN 15,LSTM-RNN 16 等。

第二种范式首先通过深度模型提取两个文本的交叉特征,得到匹配信号张量,再聚合为匹配分数。这种范式同时考虑两个文本的输入信息,更适合做小规模的语义相似精排,例如:ARC-II 14,MatchPyramid 17,Match-SRNN 18,Duet 19 等。

文本长度角度

从文本长度角度出发,我们可以粗略的将文本分类为短文本长文本短文本包括“字词”,“短语”,“句子”等相对比较短的文本形式,长文本包括“段落”,“篇章”等相对比较长的文本形式。

短文本 v.s. 短文本

短文本同短文本的常见比较形式有:关键词(字词)同文本标题(句子)的匹配,相似查询(句子)的匹配等。如果单纯的希望获取字符层面的差异,可以通过距离度量进行相似度比较。如果需要从语义的角度获取相似度,则可以利用表示学习对需要比对的文本进行表示,在通过语义向量之间的相似程度来衡量原始文本之间的相似度,详情可参见上文。

短文本 v.s. 长文本

短文本同长文本的比较多见于文档的搜索,即给定相关的查询(字词),给出最相关的文档(段落和篇章)。对于这类问题常见的解决方式是对长文本利用 TF-IDF,BM25等方法或进行主题建模后,再同查询的关键词进行匹配计算相似度度。

长文本 v.s. 长文本

长文本同长文本的比较多见于文档的匹配和去重,对于这类问题常见的解决方式是利用关键词提取获取长文本的特征向量,然后利用特征向量之间的相似度衡量对应文本的相似程度。在针对海量文本的去重,还以应用 SimHash 等技术对文本生成一个指纹,从而实现快速去重。


  1. https://zh.wikipedia.org/wiki/主题模型 ↩︎

  2. Manning, C. D., Schütze, H., & Raghavan, P. (2008). Introduction to information retrieval. Cambridge university press. ↩︎

  3. Mihalcea, R., & Tarau, P. (2004, July). Textrank: Bringing order into text. In Proceedings of the 2004 conference on empirical methods in natural language processing (pp. 404-411). ↩︎

  4. Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank citation ranking: Bringing order to the web. Stanford InfoLab. ↩︎

  5. Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K., & Harshman, R. (1990). Indexing by latent semantic analysis. Journal of the American society for information science, 41(6), 391-407. ↩︎

  6. Hofmann, T. (1999, August). Probabilistic latent semantic indexing. In Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval (pp. 50-57). ↩︎

  7. Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). Latent dirichlet allocation. Journal of machine Learning research, 3(Jan), 993-1022. ↩︎

  8. Rickjin(靳志辉). 2013. LDA数学八卦 ↩︎

  9. Teh, Y. W., Jordan, M. I., Beal, M. J., & Blei, D. M. (2006). Hierarchical dirichlet processes. Journal of the american statistical association, 101(476), 1566-1581. ↩︎

  10. 翟社平, 李兆兆, 段宏宇, 李婧, & 董迪迪. (2019). 基于词法, 句法和语义的句子相似度计算方法. 东南大学学报: 自然科学版, 49(6), 1094-1100. ↩︎

  11. Zhang, K., & Shasha, D. (1989). Simple fast algorithms for the editing distance between trees and related problems. SIAM journal on computing, 18(6), 1245-1262. ↩︎

  12. Meila, M., & Jordan, M. I. (2000). Learning with mixtures of trees. Journal of Machine Learning Research, 1(Oct), 1-48. ↩︎

  13. Huang, P. S., He, X., Gao, J., Deng, L., Acero, A., & Heck, L. (2013, October). Learning deep structured semantic models for web search using clickthrough data. In Proceedings of the 22nd ACM international conference on Information & Knowledge Management (pp. 2333-2338). ↩︎

  14. Hu, B., Lu, Z., Li, H., & Chen, Q. (2014). Convolutional neural network architectures for matching natural language sentences. In Advances in neural information processing systems (pp. 2042-2050). ↩︎ ↩︎

  15. Qiu, X., & Huang, X. (2015, June). Convolutional neural tensor network architecture for community-based question answering. In Twenty-Fourth international joint conference on artificial intelligence↩︎

  16. Palangi, H., Deng, L., Shen, Y., Gao, J., He, X., Chen, J., … & Ward, R. (2016). Deep sentence embedding using long short-term memory networks: Analysis and application to information retrieval. IEEE/ACM Transactions on Audio, Speech, and Language Processing, 24(4), 694-707. ↩︎

  17. Pang, L., Lan, Y., Guo, J., Xu, J., Wan, S., & Cheng, X. (2016). Text matching as image recognition. In Proceedings of the Thirtieth AAAI Conference on Artificial Intelligence (AAAI'16). (pp. 2793–2799). ↩︎

  18. Wan, S., Lan, Y., Xu, J., Guo, J., Pang, L., & Cheng, X. (2016, July). Match-SRNN: modeling the recursive matching structure with spatial RNN. In Proceedings of the Twenty-Fifth International Joint Conference on Artificial Intelligence (pp. 2922-2928). ↩︎

  19. Mitra, B., Diaz, F., & Craswell, N. (2017, April). Learning to match using local and distributed representations of text for web search. In Proceedings of the 26th International Conference on World Wide Web (pp. 1291-1299). ↩︎

而立之前 (Life before 30)

2020-08-15 08:00:00

没几个月就步入而立之年,古有云三十岁能自立于世,诚然不易。「立」,自当有成家立业之意,在当今社会,无论是客观因素还是主观因素,大家对成家立业的看法和行动步伐都略有差异,在此就不再发表愚见了。但我认为**「立身」**是我们都应该去思考和行动的事情,我们应该对于内在的自我和外部的社会有一个比较明确的认知,知道在社会中立足应该去做什么事、喜欢去做什么事、擅长去做什么事,然后不畏惧、不妥协、勇敢去做。

有时候会自嘲没有远大的志向和野心,但这样其实很真实,我认为平淡是人生的常态,脑海中记忆更深的才是平淡之外的苦难和幸福。虛泛的东西写多了容易变成鸡汤,简单整理几块内容省身克己,如于他人有益,实则万幸。

三观

总是在说三观,其实容易泛泛而谈,我认为三观会从很大程度上决定应该去做什么、喜欢去做什么,甚至会影响到擅长去做什么。

世界观

世界观是我们对于宇宙的根本看法,我认为这是进行沟通的一个基础,如果两个人在这个层面上就处在对立的位置,感觉无论沟通什么都会失去意义。世界观的两个根本对立即唯物主义和唯心主义,我自己不在任意一个极端,非要占个队,我应该会略微偏向唯物主义。毕竟鸡不打鸣太阳照常会升起,但如果我看不到,于我又有什么意义。

在我认识的人中,没有遇见太过极端的,更多的可能是受到民族、信仰、地域等因素的长期影响吧,或多或少都会有些差异。我认为只要不处在完全的对立面,一切都还是可以去思考、去沟通、去辩解的。

人生观

生而为赢(Born to Win)是高中年代看过的一篇文章,很鸡血,也着实影响了自己的求学年代。但随着环境的改变和自己人生的实践,对于人生态度、目的和意义的思考也在不断转变。现在来看,生而为赢依旧没错,拼搏和奋斗才能创造平淡之中的幸福。当前的我想要追求的是平淡之中不乏波澜,或好或坏,同时在为之努力的过程中也不要给别人添堵,说白了不能损人利己,损人不利己就更要唾弃了。

「认命」是我最近比较喜欢的一个词,也是最近更新了对其理解的词。其实“认命”并不消极,尽人事,听天命,积极做事,结果往往会受到多方面的影响,成则已,不成换个路子继续尽人事。人生观相比世界观可能更容易发生变化,虽然没有世界观那么基础,但我认为对待人生观也应该更多的从“功利主义”而非“个人主义”角度出发,如果从这个层面上就背离社会大多数认可的正道,那么就会容易走上邪路,危害他人,这不就真给人添堵了吗。

价值观

孰好孰坏、孰优孰劣真的不是一个很容易回答的问题。面对比较基础的问题,只要不违背世界观和人生观,还是能够比较明确的给出一个结论的,但细到很小很具体的问题上,每个人的见解就不同了。所以,对待大是大非、大善大恶同上面一样,我更倾向于功利主义,对于小事,我认为要做到有理有据、不自欺欺人,那么就无伤大雅。

价值观包含物质性和精神性两大种类,我想两者也不必割裂而谈,不必不食人间烟火,能与朋友大快朵颐,亦能与朋友谈天论地,岂不快哉?

做有意义的事,不给人添堵,什么有意义,需要自己去思考。

思考

要思考,但也别想太多,多思考做人做事自然是好的,但是天马行空的想太多,容易出现各种妄想症,我略受过其害。

思辨胜于对错

做算法久了,虽然越来越需要更深入的了解业务,但同业务掰扯的时候还是不多的,更多的是围绕抽象出来的问题在做事。做这些事情需要更多的是专业力,是非对错就是一个量化评估的过程,但真遇到一些方向上的问题,或是与人更相关的问题,对错的判断就显得不是那么容易

即便将来一直沿着技术专家路线发展下去,我认为对上层问题的思考终将占据更重要的地位,而且也如实在这样发展。所以要有一套自己衡量评价事情的方法论,是非对错的评价标准因我们的世界观、人生观、价值观不同而有差异,但方法论还是能够有一定通用性的。方法论并不虚,就像我们做数据挖掘用的 CRISP-DM 一样,能够帮助我们更加客观、全面、系统的去做事。思考如何去评判,感觉比一味地争辩什么是对、什么是错更有意义。

永不停止

无论处于顺势还是逆势,都不要停止思考,顺势思考如何锦上添花,逆势思考如何雪中送炭。浑浑噩噩、自欺欺人、得过且过是最不可取的。读万卷书、行万里路、交万名友,成本最低的就是读万卷书,思考和读书类似,除了时间没什么太大的花销,就看你想不想。

生命不息,思考不止。

自由

自由有很多种,财务自由、意志自由,谈及大多数自由的时候都是我们所追求的。当然自由的首要前提正如《人权宣言》里面说的:「自由即有权做一切无害于他人的任何事情」。也就是自由并不是为所欲为,当然哪怕无害于他人,肆意挥霍、信口开河也是应该是被唾弃的。

读胡适的《容忍与自由》,感觉成事要「养成能够容忍谅解别人的见解的度量」,不要「以吾辈所主张这为绝对之是」。尤其进入到一个新的领域,要谦逊但不谦卑地去学习和理解,懂得尊重和敬畏即为容忍。

我是一个比较喜欢用数据说话的人,但有时也正是这种“所谓的有理有据”会让我“怼人”怼得理所当然。当想要让别人容忍谅解我们的见解,我们自己应该先做到这一点,凡事没有绝对,哪怕概率是一也不一定必然发生,数据说话是好事,只是不要被数字本身所蒙蔽就好。

我年纪越大,越感觉到容忍比自由更重要

最近邻搜索 (Nearest Neighbor Search)

2020-08-01 08:00:00

**最近邻搜索(Nearest Neighbor Search)**是指在一个确定的距离度量和一个搜索空间内寻找与给定查询项距离最小的元素。更精确地,对于一个包含 $N$ 个元素的集合 $\mathcal{X} = \left\{\mathbf{x}_1, \mathbf{x}_2, \cdots, \mathbf{x}_n\right\}$,给定查询项 $\mathbf{q}$ 的最近邻 $NN \left(\mathbf{q}\right) = \arg\min_{\mathbf{x} \in \mathcal{X}} dist \left(\mathbf{q}, \mathbf{x}\right)$,其中 $dist \left(\mathbf{q}, \mathbf{x}\right)$$\mathbf{q}$$\mathbf{x}$ 之间的距离。由于维数灾难,我们很难在高维欧式空间中以较小的代价找到精确的最近邻。**近似最近邻搜索(Approximate Nearest Neighbor Search)**则是一种通过牺牲精度来换取时间和空间的方式从大量样本中获取最近邻的方法。

精确搜索

最简单的最邻近搜索便是遍历整个点集,计算它们和目标点之间的距离,同时记录目前的最近点。这样的算法较为初级,可以为较小规模的点集所用,但是对于点集的尺寸和空间的维数稍大的情况则不适用。对于 $D$ 维的 $N$ 个样本而言,暴力查找方法的复杂度为 $O \left(DN\right)$

k-D 树

k-D 树(k-Dimesion Tree)1 是一种可以高效处理 $k$ 维空间信息的数据结构。k-D 树具有二叉搜索树的形态,二叉搜索树上的每个结点都对应 $k$ 维空间内的一个点。其每个子树中的点都在一个 $k$ 维的超长方体内,这个超长方体内的所有点也都在这个子树中。k-D 树的构建过程如下:

  1. 若当前超长方体中只有一个点,返回这个点。
  2. 选择一个维度,将当前超长方体按照这个维度分割为两个超长方体。
  3. 选择一个切割点,将小于这个点的归入其中一个超长方体(左子树),其余归入另一个超长方体(右子树)。
  4. 递归地对分出的两个超长方体构建左右子树。

一个 $k = 2$ 的例子如下:

构建 k-D 树目前最优方法的时间复杂度为 $O \left(n \log n\right)$。对于单次查询,当 $2$ 维时,查询时间复杂度最优为 $O \left(\log n\right)$,最坏为 $O \left(\sqrt{n}\right)$,扩展至 $k$ 维,最坏为 $O \left(n^{1 - \frac{1}{k}}\right)$。k-D 树对于低维度最近邻搜索比较好,但当 $k$ 增长到很大时,搜索的效率就变得很低,这也是“维数灾难”的一种体现。

Ball 树

为了解决 k-D 树在高维数据上的问题,Ball 树 2 结构被提了出来。k-D 树是沿着笛卡尔积(坐标轴)方向迭代分割数据,而 Ball 树是通过一系列的超球体分割数据而非超长方体。Ball 树的构建过程如下:

  1. 若当前超球体中只有一个点,返回这个点。
  2. 定义所有点的质心为 $c$,离质心 $c$ 最远的点为 $c_1$,离 $c_1$ 最远的点为 $c_2$
  3. $c_1$$c_2$ 作为聚类中心对数据点进行聚类得到两个簇 $\left(c_1, r_1\right), \left(c_2, r_2\right)$,将其归入左子树和右子树,其中 $r$ 为超球的半径。
  4. 递归的对分出的两个超球体构建左右子树。

一个二维的例子如下:

每个点必须只能隶属于一个簇,但不同簇的超球体之间是可以相交的。在利用 Ball 树进行查询时,首先自上而下的找到包含查询点的叶子簇 $\left(c, r\right)$,在这个簇中找到距离查询点最近的观测点,这两个点的距离 $d_{upper}$ 即为最近邻的距离上界。之后检查该叶子簇的所有兄弟簇是否包含比这个上界更小的观测点,在检查时,如果查询节点距离兄弟簇圆心的距离大于兄弟簇的半径与之前计算的上界 $d_{upper}$ 之和,则这个兄弟节点不可能包含所需要的最近邻。

构建 Ball 树的时间复杂度为 $O \left(n \left(\log n\right)^2\right)$,查询时间复杂度为 $O \left(\log \left(n\right)\right)$

近似搜索

基于哈希的算法

基于哈希的算法的目标是将一个高维数据点转换为哈希编码的表示方式,主要包含两类方法:局部敏感哈希(Local Sensitive Hash, LSH)哈希学习(Learning to Hash, L2H)

局部敏感哈希

局部敏感哈希采用的是与数据无关的哈希函数,也就是说整个学习处理过程不依赖于任何的数据内容信息。LSH 通过一个局部敏感哈希函数将相似的数据点以更高的概率映射到相同的哈希编码上去。这样我们在进行查询时就可以先找到查询样本落入那个哈希桶,然后再在这个哈希桶内进行遍历比较就可以找到最近邻了。

要使得相近的数据点通过哈希后落入相同的桶中,哈希函数需要满足如下条件:

  1. 如果 $d \left(x, y\right) \leq d_1$,则 $Pr \left[h \left(x\right), h \left(y\right)\right] \geq p_1$
  2. 如果 $d \left(x, y\right) \geq d_2$,则 $Pr \left[h \left(x\right), h \left(y\right)\right] \leq p_2$

其中,$x, y \in \mathbb{R}^n$ 表示 $n$ 维度数据点,$d \left(x, y\right)$ 表示 $x, y$ 之间的距离,$h$ 为哈希函数。满足上述两个条件的哈希函数称为是 $\left(d_1, d_2, p_1, p_2\right)$ 敏感的。

MinHash 算法的思路是:采用一种哈希函数将元素的位置均匀打乱,然后在新顺序下每个集合的第一个元素作为该集合的特征值。我们以 $s_1 = \left\{a, d\right\}$$s_2 = \left\{c\right\}$$s_3 = \left\{b, d, e\right\}$$s_4 = \left\{a, c, d\right\}$ 为例,集合中可能的元素为 $\left\{a, b, c, d, e\right\}$,则这四个集合可以表示为:

元素 $s_1$ $s_2$ $s_3$ $s_4$
$a$ 1 0 0 1
$b$ 0 0 1 0
$c$ 0 1 0 1
$d$ 1 0 1 1
$e$ 0 0 1 0

对矩阵进行随机打乱后有:

元素 $s_1$ $s_2$ $s_3$ $s_4$
$b$ 0 0 1 0
$e$ 0 0 1 0
$a$ 1 0 0 1
$d$ 1 0 1 1
$c$ 0 1 0 1

我们利用每个集合的第一个元素作为该集合的特征值,则有 $h \left(s_1\right) = a$$h \left(s_2\right) = c$$h \left(s_3\right) = b$$h \left(s_4\right) = a$,可以看出 $h \left(s_1\right) = h \left(s_4\right)$。MinHash 能够保证在哈希函数均匀分布的情况下,哈希值相等的概率等于两个集合的 Jaccard 相似度,即:

$$ Pr \left(MinHash \left(s_1\right) = MinHash \left(s_2\right)\right) = Jaccard \left(s_1, s_2\right) $$

SimHash 是由 Manku 等人 3 提出的一种用于用于进行网页去重的哈希算法。SimHash 作为局部敏感哈希算法的一种其主要思想是将高维特征映射到低维特征,再通过两个向量的汉明距离来确定是否存在重复或相似。算法步骤如下:

  1. 对文本进行特征抽取(例如:分词),并为每个特征赋予一定的权重(例如:词频)。
  2. 计算每个特征的二进制哈希值。
  3. 计算加权后的哈希值,当哈希值为 1 时,则对应位置为 $w_i$,否则为 $-w_i$,其中 $w_i$ 为该特征对应的权重。
  4. 将所有特征加权后的哈希值按对应的位置进行累加合并。
  5. 如果累加位置大于 0 则置为 1,否则置为 0,最终得到哈希结果。

算法流程如下图所示:

在得到 SimHash 的值后,我们可以通过比较之间的汉明距离来判断相似性。为了提高海量数据的去重效率,以 64 位指纹为例,我们可以将其切分为 4 份 16 位的数据块,根据鸽巢原理,汉明距离为 3 的两个文档必定有一个数据块是相等的。将这 4 分数据利用 KV 数据库和倒排索引进行存储,Key 为 16 位的截断指纹,Value 为剩余的指纹集合,从而提高查询的效率。同时可以选择 16,8 和 4 位进行索引,位数越小越精确,但所需的存储空间越大。

当一个在 $\Re$ 上的分布 $\mathcal{D}$$p\text{-stable}$ 时,存在 $p \geq 0$ 使得对于任意 $n$ 个实数 $v_1, \cdots, v_n$ 和独立同分布 $\mathcal{D}$ 下的变量 $X_1, \cdots, X_n$,有随机变量 $\sum_{i}{v_i X_i}$$\left(\sum_{i}{\left|v_i\right|^p}\right)^{1/p} X$ 具有相同的分布,其中 $X$ 为分布 $\mathcal{D}$ 下的随机变量 4。常见的 p-stable 分布有:

  1. 柯西分布:密度函数为 $c \left(x\right) = \dfrac{1}{\pi} \dfrac{1}{1 + x^2}$,为 $1\text{-stable}$
  2. 正态分布:密度函数为 $g \left(x\right) = \dfrac{1}{\sqrt{2 \pi}} e^{-x^2 / 2}$,为 $2\text{-stable}$

p-stable 分布主要可以用于估计 $\left\|v\right\|_p$,对于两个相似的 $v_1, v_2$,它们应该具有更小的 $\left\|v_1 - v_2\right\|_p$,也就是对应的哈希值有更大的概率发生碰撞。对于 $v_1, v_2$,距离的映射 $a \cdot v_1 - a \cdot v_2$$\left\|v_1 - v_2\right\|_p \cdot X$ 具有相同的分布。$a \cdot v$ 将向量 $v$ 映射到实数集,如果将实轴以宽度 $w$ 进行等分,$a \cdot v$ 落在哪个区间中就将其编号赋予它,这样构造的哈希函数具有局部保持特性。构造的哈希函数族的形式为:

$$ h_{a, b} \left(v\right) = \left\lfloor \dfrac{a \cdot v + b}{w} \right\rfloor $$

其中,向量 $a$ 的元素 $a_i \sim N \left(0, 1\right)$$b \sim U \left(0, w\right)$。令 $c = \left\|u - v \right\|_p$,则两个向量在被分配到一个桶中的概率为:

$$ Pr \left[h_{a, b} \left(u\right) = h_{a, b} \left(v\right)\right] = \int_{0}^{w} \dfrac{1}{c} \cdot f_p \left(\dfrac{t}{u}\right) \left(1 - \dfrac{t}{w}\right) dt $$

其中,$f_p$ 为概率密度函数。从上式中不难看出,随着距离 $c$ 的减小,两个向量发生碰撞的概率增加。

局部敏感哈希可以在次线性时间内完成搜索,但缺点在于需要比较长的比特哈希码和比较多的哈希表才能达到预期的性能。

在单表哈希中,当哈希编码位数 $K$ 过小时,每个哈希桶中数据个数较多,从而会增加查询的响应时间。当哈希编码位数 $K$ 较大时,查询样本同最近邻落入同一个桶中的概率会很小。针对这个问题,我们可以通过重复 $L$ 次来增加最近邻的召回率。这个操作可以转化为构建 $L$ 个哈希表,给定一个查询样本,我们可以找到 $L$ 个哈希桶,然后再遍历这 $L$ 个哈希桶中的数据。但这样会增加内存的消耗,因此需要选择合理的 $K$$L$ 来获得更好的性能。

Multi-probe LSH 5 引入了一种新的策略解决召回的问题。Multi-probe LSH 不仅仅会遍历查询样本所在桶内的元素,同时还会查询一些其他有可能包含最近邻的桶,从而在避免构建多个哈希表的情况下增加召回率。

哈希学习

哈希学习(Learning to Hash)是由 Salakhutdinov 和 Hinton 6 引入到机器学习领域,通过机器学习机制将数据映射成二进制串的形式,能显著减少数据的存储和通信开销,从而有效提高学习系统的效率 7。从原空间中的特征表示直接学习得到二进制的哈希编码是一个 NP-Hard 问题。现在很多的哈希学习方法都采用两步学习策略:

  1. 先对原空间的样本采用度量学习(Metric Learning)进行降维,得到 1 个低维空间的实数向量表示。
  2. 对得到的实数向量进行量化(即离散化)得到二进制哈希码。

现有的方法对第二步的处理大多很简单,即通过某个阈值函数将实数转换成二进制位。通常使用的量化方法为 1 个阈值为 0 的符号函数,即如果向量中某个元素大于 0,则该元素被量化为 1,否则如果小于或等于 0,则该元素被量化为 0。

哈希学习相关的具体算法不再一一展开,更多细节请参见下文提供的相关 Survey。

矢量量化算法

**矢量量化(Vector Quantization)**是信息论中一种用于数据压缩的方法,其目的是减少表示空间的维度。一个量化器可以表示为由 $D$ 维向量 $x \in \mathbb{R}^D$ 到一个向量 $q \left(x\right) \in \mathcal{C} = \left\{c_i; i \in \mathcal{I}\right\}$ 的映射 $q$,其中下标集合 $\mathcal{I}$ 为有限集合,即 $\mathcal{I} = 0, \cdots, k-1$$c_i$ 称之为形心(centroids),$\mathcal{C}$ 称之为大小为 $k$ 的码本(codebook)。映射后的向量到一个给定下标 $i$ 的集合 $\mathcal{V}_i \triangleq \left\{x \in \mathbb{R}^D: q \left(x\right) = c_i\right\}$(Voronoi),称之为一个单元(cell)。

以一个图像编码为例,我们通过 K-Means 算法得到 $k$ 个 centroids,然后用这些 centroids 的像素值来替换对应簇中所有点的像素值。当 $k = 2, 10, 100$ 时,压缩后的图像和原始图像的对比结果如下图所示:

$k = 100$ 时,压缩后的图像和原始图像已经非常接近了,相关代码请参见这里

矢量量化以乘积量化(Product Quantization,PQ)最为典型,乘积量化的核心思想还是聚类,乘积量化生成码本和量化过程如下图所示:

在训练阶段,以维度为 128 的 $N$ 个样本为例,我们将其切分为 4 个子空间,则每个子空间的维度为 32 维。对每一个子空间利用 K-Means 对其进行聚类,令聚类个数为 256,这样每个子空间就能得到一个 256 大小的码本。样本的每个子段都可以用子空间的聚类中心来近似,对应的编码即为类中心的 ID。利用这种编码方式可以将样本用一个很短的编码进行表示,从而达到量化的目的。

在查询阶段,我们将查询样本分成相同的子段,然后在每个子空间中计算子段到该子空间中所有聚类中心的距离,这样我们就得到了 $4 \times 256$ 个距离。在计算某个样本到查询样本的距离时,我们仅需要从计算得到的 4 组距离中将对应编码的距离取出相加即可,所有距离计算完毕排序后即可得到结果。

乘积量化有两种计算距离的方式 8对称距离非对称距离,如下图所示:

对于 $x$$y$ 的距离 $d \left(x, y\right)$,对称距离利用 $d \left(q \left(x\right), q \left(y\right)\right)$ 进行估计,非对称距离利用 $d \left(x, q \left(y\right)\right)$ 进行估计。对称距离和非对称距离在不同阶段的时间复杂度如下表所示:

对称距离 非对称距离
编码 $x$ $k^* D$ 0
计算 $d \left(u_j \left(x\right), c_{j, i}\right)$ 0 $k^* D$
对于 $y \in \mathcal{Y}$,计算 $\hat{d} \left(x, y\right)$$\tilde{d} \left(x, y\right)$ $nm$ $nm$
查找最小 $k$ 个距离 $n + k$ $\log k \log \log n$

其中,$k^*$ 为 centroids 个数,$D$ 为向量维度,$n$ 为样本个数,$m$ 为分割个数。通常情况下我们采用非对称距离,其更接近真实距离。

IVFADC 8 是乘积量化的的加速版本,乘积量化在计算距离时仍需逐个遍历相加计算。倒排乘积量化首先对 $N$ 个样本采用 K-Means 进行聚类,此处的聚类中心相比乘积量化应设置较小的数值。在得到聚类中心后,针对每一个样本 $x_i$ 找到距离最近的类中心 $c_i$,两者相减后得到残差 $x_i - c_i$,然后对残差再进行乘积量化的全过程。在查询阶段,通过先前较粗力度的量化快速定位隶属于哪一个 $c_i$,然后在 $c_i$ 区域利用乘积量化获取最终结果。整个流程如下图所示:

Optimized Product Quantization (OPQ) 9 是乘积量化的一个优化方法。通常用于检索的原始特征维度较高,实践中利用乘积量化之前会对高维特征利用 PCA 等方法进行降维处理。这样在降低维度的时候还能够使得对向量进行子段切分的时候各个维度不相关。在利用 PCA 降维后,采用顺序切分子段仍存在一些问题,以 Iterative Quantization (ITQ) 10 中的一个二维平面例子来说明,如下图所示:

在利用乘积量化进行编码时,对于切分的各个子空间,应尽可能使得各个子空间的方差接近。上图中 $(a)$ 图在 x 和 y 轴上的方差较大,而 $(c)$ 图在两个方向上比较接近。OPQ 致力解决的问题就是对各个子空间方差上的均衡,OPQ 对于该问题的求解分为非参数求解方法和参数求解方法两种,更多算法细节请参见 ITQ 和 OPQ 原文。

基于图的算法

NSW(Navigable Small World)11 算法是一种由 Malkov 等人提出的基于图的索引的方法。我们将 Navigable Small World 网络表示为一个图 $G \left(V, E\right)$,其中数据集合 $X$ 的点被唯一映射到集合 $V$ 中的一条边,边集 $E$ 由构造算法确定。对于与一个节点 $v_i$ 共享一条边的所有节点,我们称之为该节点的“友集”。

之后我们可以利用一个贪婪搜索的变种算法实现一个基本的 KNN 搜索算法。通过选择友集中未被访问过的距离查询样本最近的节点,可以在图中一个接一个的访问不同的节点,直到达到停止准则。整个过程如下图所示:

上图中的边扮演着两种不同的角色:

  1. 短距离边的子集作为 Delaunay 图的近似用于贪婪搜索算法。
  2. 长距离边的子集用于对数尺度的贪婪搜索,负责构造图的 Navigable Small World 属性。

其中,黑色的边为短距离边,红色的边为长距离边,箭头为迭代查询路径。整个结构的构建可以通过元素的连续插入实现,对于新的元素,我们从当前结构中找到最接近的邻居集合与之相连。随着越来越多的元素插入到结构中,之前的短距离连接就变成了长距离连接。

NSW 的 KNN 查询过程如下所示:

\begin{algorithm}
\caption{NSW 的 KNN 查询}
\begin{algorithmic}
\REQUIRE 查询样本 $q$,查询结果数量 $k$,最大迭代次数 $m$
\STATE $V_{cand} \gets \varnothing, V_{visited} \gets \varnothing, V_{res} \gets \varnothing$
\FOR{$i \gets 1$ \TO $m$}
  \STATE $V_{tmp} \gets \varnothing$
  \STATE $v_{rand} \gets $ 随机初始节点
  \STATE $V_{cand} \gets V_{cand} \cup v_{rand}$
  \WHILE{True}
    \STATE $v_c \gets V_{cand}$ 中距离 $q$ 最近的元素
    \STATE $V_{cand} \gets V_{cand} \setminus v_c$
    \IF{$v_c$ 比结果中的 $k$ 个元素距离 $q$ 还远}
      \BREAK
    \ENDIF
    \FOR{$v_e \in v_c$ 的“友集”}
      \IF{$v_e \notin V_{visited}$}
        \STATE $V_{visited} \gets V_{visited} \cup v_e, V_{cand} \gets V_{cand} \cup v_e, V_{tmp} \gets V_{tmp} \cup v_e$
      \ENDIF
    \ENDFOR
  \ENDWHILE
  \STATE $V_{res} \gets V_{res} \cup V_{tmp}$
\ENDFOR
\RETURN{$V_{res}$ 中与查询样本最近的 $k$ 个元素}
\end{algorithmic}
\end{algorithm}

HNSW(Hierarchical Navigable Small World)12 是对 NSW 的一种改进。HNSW 的思想是根据连接的长度(距离)将连接划分为不同的层,然后就可以在多层图中进行搜索。在这种结构中,搜索从较长的连接(上层)开始,贪婪地遍历所有元素直到达到局部最小值,之后再切换到较短的连接(下层),然后重复该过程,如下图所示:

利用这种结构可以将原来 NSW 的多重对数(Polylogarithmic)计算复杂度降低至对数(Logarithmic)复杂度。更多关于数据插入和搜索的细节请参见原文。

NSG 13 提出了一种新的图结构 Monotonic Relative Neighborhood Graph (MRNG) 用于保证一个平均的低搜索时间复杂度(接近对数复杂度)。同时为了进一步降低索引复杂度,作者从确保连接性、降低平均出度、缩短搜索路径和降低索引大小 4 个方面考虑,提出了一个用于近似 MRNG 的 Spreading-out Graph (NSG)。

基于图的方法 HNSW 和基于乘积量化的方法 OPQ 之间的特性对比如下:

特点 OPQ HNSW
内存占用
召回率 较高
数据动态增删 灵活 不易

本文部分内容参考自 图像检索:向量索引

算法对比

常用算法的开源实现的评测如下,更多评测结果请参见 erikbern/ann-benchmarks

Glove-100-Angular (K=10)
SIFT-128-Euclidean (K=10)

开放资源

Survey

开源库

API
spotify/annoy C++, Python, Go
vioshyvo/mrpt C++, Python, Go
pixelogik/NearPy Python
aaalgo/kgraph C++, Python
nmslib/nmslib C++, Python
nmslib/hnswlib C++, Python
lyst/rpforest Python
facebookresearch/faiss C++, Python
ekzhu/datasketch Python
lmcinnes/pynndescent Python
yahoojapan/NGT C, C++, Python, Go, Ruby
microsoft/SPTAG C++, Python
puffinn/puffinn C++, Python
kakao/n2 C++, Python, Go
ZJULearning/nsg C++

开源搜索引擎

搜索引擎 API
milvus-io/milvus C, C++, Python, Java
Go, Node.js, RESTful API
vearch/vearch Python, Go

评测


  1. Bentley, J. L. (1975). Multidimensional binary search trees used for associative searching. Communications of the ACM, 18(9), 509-517. ↩︎

  2. Omohundro, S. M. (1989). Five balltree construction algorithms (pp. 1-22). Berkeley: International Computer Science Institute. ↩︎

  3. Manku, G. S., Jain, A., & Das Sarma, A. (2007, May). Detecting near-duplicates for web crawling. In Proceedings of the 16th international conference on World Wide Web (pp. 141-150). ↩︎

  4. Datar, M., Immorlica, N., Indyk, P., & Mirrokni, V. S. (2004, June). Locality-sensitive hashing scheme based on p-stable distributions. In Proceedings of the twentieth annual symposium on Computational geometry (pp. 253-262). ↩︎

  5. Lv, Q., Josephson, W., Wang, Z., Charikar, M., & Li, K. (2007, September). Multi-probe LSH: efficient indexing for high-dimensional similarity search. In Proceedings of the 33rd international conference on Very large data bases (pp. 950-961). ↩︎

  6. Salakhutdinov, Ruslan, and Geoffrey Hinton. “Semantic hashing.” International Journal of Approximate Reasoning 50.7 (2009): 969-978. ↩︎

  7. 李武军, & 周志华. (2015). 大数据哈希学习: 现状与趋势. 科学通报, 60(5-6), 485-490. ↩︎

  8. Jegou, H., Douze, M., & Schmid, C. (2010). Product quantization for nearest neighbor search. IEEE transactions on pattern analysis and machine intelligence, 33(1), 117-128. ↩︎ ↩︎

  9. Ge, T., He, K., Ke, Q., & Sun, J. (2013). Optimized product quantization. IEEE transactions on pattern analysis and machine intelligence, 36(4), 744-755. ↩︎

  10. Gong, Y., Lazebnik, S., Gordo, A., & Perronnin, F. (2012). Iterative quantization: A procrustean approach to learning binary codes for large-scale image retrieval. IEEE transactions on pattern analysis and machine intelligence, 35(12), 2916-2929. ↩︎

  11. Malkov, Y., Ponomarenko, A., Logvinov, A., & Krylov, V. (2014). Approximate nearest neighbor algorithm based on navigable small world graphs. Information Systems, 45, 61-68. ↩︎

  12. Malkov, Y. A., & Yashunin, D. A. (2018). Efficient and robust approximate nearest neighbor search using hierarchical navigable small world graphs. IEEE transactions on pattern analysis and machine intelligence↩︎

  13. Fu, C., Xiang, C., Wang, C., & Cai, D. (2019). Fast approximate nearest neighbor search with the navigating spreading-out graph. Proceedings of the VLDB Endowment, 12(5), 461-474. ↩︎

  14. Wang, J., Zhang, T., Sebe, N., & Shen, H. T. (2017). A survey on learning to hash. IEEE transactions on pattern analysis and machine intelligence, 40(4), 769-790. ↩︎

  15. Reza, M., Ghahremani, B., & Naderi, H. (2014). A Survey on nearest neighbor search methods. International Journal of Computer Applications, 95(25), 39-52. ↩︎

  16. Liu, T., Moore, A. W., Yang, K., & Gray, A. G. (2005). An investigation of practical approximate nearest neighbor algorithms. In Advances in neural information processing systems (pp. 825-832). ↩︎

  17. Li, W., Zhang, Y., Sun, Y., Wang, W., Li, M., Zhang, W., & Lin, X. (2019). Approximate nearest neighbor search on high dimensional data-experiments, analyses, and improvement. IEEE Transactions on Knowledge and Data Engineering↩︎

  18. Cao, Y., Qi, H., Zhou, W., Kato, J., Li, K., Liu, X., & Gui, J. (2017). Binary hashing for approximate nearest neighbor search on big data: A survey. IEEE Access, 6, 2039-2054. ↩︎

  19. Wang, J., Shen, H. T., Song, J., & Ji, J. (2014). Hashing for similarity search: A survey. arXiv preprint arXiv:1408.2927↩︎

无模型策略预测和控制 - 时序差分学习 (Model-Free Policy Prediction and Control - Temporal Difference Learning)

2020-07-11 08:00:00

本文为《强化学习系列》文章
本文内容主要参考自:
1.《强化学习》1
2. CS234: Reinforcement Learning 2
3. UCL Course on RL 3

时序差分预测

**时序差分(Temporal Difference,TD)**和蒙特卡洛方法都利用经验来解决预测问题。给定策略 $\pi$ 的一些经验,以及这些经验中的非终止状态 $S_t$,一个适用于非平稳环境的简单的每次访问型蒙特卡洛方法可以表示为:

$$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[G_{t}-V\left(S_{t}\right)\right] \label{eq:mc-update} $$

其中,$G_t$ 是时刻 $t$ 真实的回报,$\alpha$ 是步长参数,称之为常量 $\alpha$ MC。MC 需要等到一幕的结尾才能确定对 $V \left(S_t\right)$ 的增量(此时才能获得 $G_t$),而 TD 则只需要等到下一个时刻即可。在 $t+1$ 时刻,TD 使用观察到的收益 $R_{t+1}$ 和估计值 $V \left(S_{t+1}\right)$ 来进行一次有效更新:

$$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right] \label{eq:td-update} $$

这种 TD 方法称之为 TD(0),算法的完整过程如下:

\begin{algorithm}
\caption{表格型 TD(0) 算法,用于估计 $V \approx v_{\pi}$}
\begin{algorithmic}
\REQUIRE 待评估策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}^+$,任意初始化 $V \left(s\right)$,其中 $V \left(\text{终止状态}\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE $A \gets$ 策略 $\pi$ 在状态 $S$ 下做出的决策动作
    \STATE 执行动作 $A$,观测到 $R, S'$
    \STATE $V\left(S\right) \gets V\left(S\right)+\alpha\left[R+\gamma V\left(S^{\prime}\right)-V\left(S\right)\right]$
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}

TD(0) 的更新在某种程度上基于已存在的估计,我们称之为一种自举法

TD 和 MC 方法都能渐进地收敛于正确的预测,但两种方法谁收敛的更快,目前暂时未能证明。但在如下的随机任务上,TD 方法通常比常量 $\alpha$ MC 方法收敛得更快。假设如下 MRP 所有阶段都同中心 C 开始,每个时刻以相同的概率向左或向右移动一个状态。幕终止于最左侧或最右侧,终止于最右侧时有 +1 的收益,除此之外收益均为零。

由于这个任务没有折扣,因此每个状态的真实价值是从这个状态开始并终止于最右侧的概率,即 A 到 E 的概率分别为 $\frac{1}{6}, \frac{2}{6}, \frac{3}{6}, \frac{4}{6}, \frac{5}{6}$

上图左侧显示了在经历了不同数量的幕采样序列之后,运行一次 TD(0) 所得到的价值估计。在 100 幕后,估计值已经非常接近真实值了。上图右侧显示了不同的 $\alpha$ 情况下学习到的价值函数和真实价值函数的均方根(RMS)误差,对于所有的 $s$,近似价值函数都被初始化为中间值 $V \left(s\right) = 0.5$,显示的误差是 5 个状态上运行 100 次的平均误差。

给定近似价值函数 $V$,在访问非终止状态的每个时刻 $t$,使用式 $\ref{eq:mc-update}$$\ref{eq:td-update}$ 计算相应的增量经验,产生新的总增量,以此类推,直到价值函数收敛。我们称这种方法为批量更新,因为只有在处理了整批的训练数据后才进行更新。批量蒙特卡洛方法总是找出最小化训练集上均方误差的估计,而批量 TD(0) 总是找出完全符合马尔可夫过程模型的最大似然估计参数。因此,MC 在非马尔可夫环境中更加高效,而 TD 在马尔可夫环境中更加高效。

DP,MC 和 TD 的状态价值更新回溯过程如下图所示:

$$ \textbf{DP} \quad V\left(S_{t}\right) \leftarrow \mathbb{E}_{\pi}\left[R_{t+1}+\gamma V\left(S_{t+1}\right)\right] $$

$$ \textbf{MC} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(G_{t}-V\left(S_{t}\right)\right) $$

$$ \textbf{TD} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) $$

时序差分控制

利用时序差分方法解决控制问题,我们依然采用广义策略迭代(GPI),只是在评估和预测部分采用时序差分方法。同蒙特卡洛方法一样,我们需要在试探和开发之间做出权衡,因此方法又划分为同轨策略和离轨策略。

Sarsa:同轨策略下的时序差分控制

在同轨策略中,我们需要对所有状态 $s$ 以及动作 $a$ 估计出在当前的行动策略下所有对应的 $q_{\pi} \left(s, a\right)$。确保状态值在 TD(0) 下收敛的定理同样也适用于对应的动作值的算法上

$$ Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)-Q\left(S_{t}, A_{t}\right)\right] $$

每当从非终止状态的 $S_t$ 出现一次转移之后,就进行上述的一次更新,如果 $S_{t+1}$ 是终止状态,那么 $Q \left(S_{t+1}, A_{t+1}\right)$ 则定义为 0。这个更新规则用到了描述这个事件的五元组 $\left(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1}\right)$,因此根据这五元组将这个算法命名为 Sarsa。Sarsa 控制算法的一般形式如下:

\begin{algorithm}
\caption{Sarsa(同轨策略下的 TD 控制)算法,用于估计 $Q \approx q_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right)$,其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \STATE 使用从 $Q$ 得到的策略(例如 $\epsilon-$ 贪心),在 $S$ 处选择 $A$
  \REPEAT
    \STATE 执行动作 $A$,观测到 $R, S'$
    \STATE 使用从 $Q$ 得到的策略(例如 $\epsilon-$ 贪心),在 $S'$ 处选择 $A'$
    \STATE $Q\left(S, A\right) \gets Q\left(S, A\right)+\alpha\left[R+\gamma Q\left(S', A'\right)-Q\left(S, A\right)\right]$
    \STATE $S \gets S'$
    \STATE $A \gets A'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}

Q-Learning:离轨策略下的时序差分控制

离轨策略下的时序差分控制算法被称为 Q-Learning,其定义为:

$$ Q\left(S_{t}, A_{t}\right) \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \max_{a} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] $$

在这里,待学习的动作价值函数 $Q$ 采用了对最优动作价值函数 $q_*$ 的直接近似作为学习目标,而与用于生成智能体决策序例轨迹的行动策略是什么无关。Q-Learning 算法的流程如下:

\begin{algorithm}
\caption{Q-Learning(离轨策略下的 TD 控制)算法,用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right)$,其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE 使用从 $Q$ 得到的策略(例如 $\epsilon-$ 贪心),在 $S$ 处选择 $A$
    \STATE 执行动作 $A$,观测到 $R, S'$
    \STATE $Q\left(S, A\right) \gets Q\left(S, A\right)+\alpha\left[R+\gamma \max_{a} Q\left(S', a\right)-Q\left(S, A\right)\right]$
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}

期望 Sarsa

如果将 Q-Learning 中对于下一个“状态-动作”二元组取最大值这一步换成取期望,即更新规则为:

$$ \begin{aligned} Q\left(S_{t}, A_{t}\right) & \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \mathbb{E}_{\pi}\left[Q\left(S_{t+1}, A_{t+1}\right) \mid S_{t+1}\right]-Q\left(S_{t}, A_{t}\right)\right] \\ & \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \sum_{a} \pi\left(a \mid S_{t+1}\right) Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] \end{aligned} $$

给定下一个状态 $S_{t+1}$,这个算法确定地向期望意义上的 Sarsa 算法所决定的方向上移动,因此这个算法被称为期望 Sarsa。期望 Sarsa 在计算上比 Sarsa 更加复杂,但它消除了因为随机选择 $A_{t+1}$ 而产生的方差。

双学习

在上述算法中,在估计值的基础上进行最大化也可以被看做隐式地对最大值进行估计,而这会产生一个显著的正偏差。假设在状态 $s$ 下可以选择多个动作 $a$,这些动作在该状态下的真实价值 $q \left(s, a\right)$ 全为零,但他们的估计值 $Q \left(s, a\right)$ 是不确定的,可能大于零也可能小于零。真实值的最大值是零,但估计值的最大值是正数,因此就产生了正偏差,我们称其为最大化偏差

我们用下例进行说明,在如下这个 MDP 中有两个非终止节点 A 和 B。每幕都从 A 开始并选择向左或向右的动作。向右则会立即转移到终止状态并得到值为 0 的收益和回报。向左则会是状态转移到 B,得到的收益也是 0。而在 B 这个状态下有很多种可能的动作,每种动作被选择后会立刻停止并得到一个从均值为 -0.1 方差为 1.0 的分布中采样得到的收益。

因此,任何一个以向左开始的轨迹的期望回报均为 -0.1,则在 A 这个状态中根本就不该选择向左。然而使用 $\epsilon-$ 贪心策略来选择动作的 Q-Learning 算法会在开始阶段非常明显地选择向左这个动作。即使在算法收敛到稳定时,它选择向左这个动作的概率也比最优值高了大约 5%,如下图所示:

解决该问题的一种方法为双学习。如果们将样本划分为两个集合,并用它们学习两个独立的对真实价值 $q \left(a\right), \forall a \in A$ 的估计 $Q_1 \left(a\right)$$Q_2 \left(a\right)$。则我们可以使用其中一个 $Q_1$ 来确认最大的动作 $A^* = \arg\max_a Q_1 \left(a\right)$,用另一个 $Q_2$ 来计算其价值的估计 $Q_2 \left(A^*\right) = Q_2 \left(\arg\max_a Q_1 \left(a \right)\right)$。由于 $\mathbb{E} \left[Q_2 \left(A^*\right)\right] = q \left(A^*\right)$,因此这个估计是无偏的。我们可以交换两个估计 $Q_1 \left(a\right)$$Q_2 \left(a\right)$ 的角色再执行一遍上面的过程,就可以得到另一个无偏的估计 $Q_1 \left(\arg\max_a Q_2 \left(a\right)\right)$

双学习的 Q-Learning 版本为 Double Q-Learning。Double Q-Learning 在学习时会以 0.5 的概率进行如下更新:

$$ Q_{1}\left(S_{t}, A_{t}\right) \leftarrow Q_{1}\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q_{2}\left(S_{t+1}, \underset{a}{\arg \max } Q_{1}\left(S_{t+1}, a\right)\right)-Q_{1}\left(S_{t}, A_{t}\right)\right] $$

以 0.5 的概率交换 $Q_1$$Q_2$ 的角色进行同样的更新。使用 $\epsilon-$ 贪心策略的 Double Q-Learning 的完整算法流程如下:

\begin{algorithm}
\caption{Double Q-Learning,用于估计 $Q_1 \approx Q_2 \approx q_*$}
\begin{algorithmic}
\REQUIRE 步长 $\alpha \in \left(0, 1\right]$,很小的 $\epsilon > 0$
\STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q_1 \left(s, a\right), Q_2 \left(s, a\right)$,其中 $Q \left(\text{终止状态}, \cdot\right) = 0$
\FOR{每一幕}
  \STATE 初始化 $S$
  \REPEAT
    \STATE 基于 $Q_1 + Q_2$ 使用 $\epsilon-$ 贪心策略在 $S$ 处选择 $A$
    \STATE 执行动作 $A$,观测到 $R, S'$
    \IF{$Random(0, 1] > 0.5$}
      \STATE $Q_{1}(S, A) \leftarrow Q_{1}(S, A)+\alpha\left(R+\gamma Q_{2}\left(S^{\prime}, \arg \max _{a} Q_{1}\left(S^{\prime}, a\right)\right)-Q_{1}(S, A)\right)$
    \ELSE
      \STATE $Q_{2}(S, A) \leftarrow Q_{2}(S, A)+\alpha\left(R+\gamma Q_{1}\left(S^{\prime}, \arg \max _{a} Q_{2}\left(S^{\prime}, a\right)\right)-Q_{2}(S, A)\right)$
    \ENDIF
    \STATE $S \gets S'$
  \UNTIL{$S$ 为终止状态}
\ENDFOR
\end{algorithmic}
\end{algorithm}

Taxi-v3 示例

我们以 Taxi-v3 为示例来测试 Sarsa,Q-Learning 和 期望 Sarsa 三种不同的算法。Taxi-v3 包含了一个 5x5 的网格,即 25 个可能的位置,我们需要驾驶一辆出租车分别在图中的 R、G、Y、B 四个位置接送乘客。客人共计存在 4 种可能的上车点,4 种可能的下车点,同时考虑出租车的位置,整个环境共有 $5 \times 5 \times \left(4 + 1\right) \times 4 = 500$ 种可能的状态,如下图所示:

图片来源:https://www.learndatasci.com/tutorials/reinforcement-q-learning-scratch-python-openai-gym/

出租车需要根据当前环境采取不同的动作,共计 6 种可能的动作:向南走,向北走,向东走,向西走,接上乘客,放下乘客。由于环境中存在墙,出租车每次撞墙不会发生任何移动。每一步动作默认 -1 的回报,当选择错误的地点接上或放下乘客时获得 -10 的回报,在成功运送一个客人后获得 +20 的回报。

分别利用 Sarsa,Q-Learning 和 期望 Sarsa 三种不同的算法训练模型,我们以 100 幕作为窗口计算平均回报,前 1000 个平均回报的对比结果如下图所示:

Taxi-v3 的成绩排行榜可参见 这里。利用训练好的模型执行预测的效果如下图所示:

本文示例代码实现请参见这里


  1. Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎

  2. CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎

  3. UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

无模型策略预测和控制 - 蒙特卡洛方法 (Model-Free Policy Prediction and Control - Monte-Carlo Learning)

2020-07-01 08:00:00

本文为《强化学习系列》文章
本文内容主要参考自:
1.《强化学习》1
2. CS234: Reinforcement Learning 2
3. UCL Course on RL 3

蒙特卡洛算法仅需要经验,即从真实或者模拟的环境交互中采样得到的状态、动作、收益的序例。从真实经验中学习不需要关于环境动态变化规律的先验知识,却依然能够达到最优的行为;从模拟经验中学习尽管需要一个模型,但这个模型只需要能够生成状态转移的一些样本,而不需要像动态规划那样生成所有可能的转移概率分布。

蒙特卡洛预测

一个状态的价值是从该状态开始的期望回报,即未来的折扣收益累积值的期望。那么一个显而易见的方式是根据经验进行估计,即对所有经过这个状态之后产生的回报进行平均。随着越来越多的回报被观察到,平均值就会收敛到期望值,这就是蒙特卡洛算法的基本思想。

假设给定策略 $\pi$ 下途径状态 $s$ 的多幕数据,我们需要估计策略 $\pi$ 下状态 $s$ 的价值函数 $v_{\pi} \left(s\right)$。在同一幕中,$s$ 可能多次被访问,因此蒙特卡洛方法分为首次访问型 MC 算法每次访问型 MC 算法,两者的区别在于更新时是否校验 $S_t$ 已经在当前幕中出现过。以首次访问型 MC 预测算法为例,算法流程如下:

\begin{algorithm}
\caption{首次访问型 MC 预测算法,用于估计 $V \approx v_{\pi}$}
\begin{algorithmic}
\REQUIRE 待评估策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}$,任意初始化 $V \left(s\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}$,$Returns \left(s\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t$ 在 $S_0, S_1, \cdots, S_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t\right) \gets Resurn \left(S_t\right) \cup G$
      \STATE $V \left(S_t\right) \gets avg \left(Returns \left(S_t\right)\right)$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

二十一点游戏为例,每一局可以看作一幕,胜、负、平分别获得收益 $+1, -1, 0$。每局游戏进行中的收益都为 $0$,并且不打折扣($\gamma = 1$),最终的收益即为整个游戏的回报。玩家的动作为要牌(Hit)或停牌(Stand),状态则取决于玩家的牌和庄家显示的牌。假设所有牌来自无穷多的一组牌(即每次取出的牌都会再放回牌堆)。如果玩家手上有一张 A,可以视作 11 而不爆掉,那么称这张 A 为可用的,此时这张牌总会被视作 11。因此,玩家做出的选择只会依赖于三个变量:他手牌的总和(12-31),庄家显示的牌(A-10),以及他是否有可用的 A,共计 200 个状态。

考虑如下策略,玩家在手牌点数之和小于 20 时均要牌,否则停牌。通过该策略多次模型二十一点游戏,并且计算每一个状态的回报的平均值。模拟结果如下:

有可用 A 的状态的估计会更不确定、不规律,因为这样的状态更加罕见。无论哪种情况,在大于约 500000 局游戏后,价值函数都能很好地近似。

蒙特卡洛控制

如果无法得到环境的模型,那么计算动作的价值(“状态-动作”二元组的价值)比计算状态的价值更加有用。动作价值函数的策略评估的目标是估计 $q_{\pi} \left(s, a\right)$,即在策略 $\pi$ 下从状态 $s$ 采取动作 $a$ 的期望回报。只需将对状态的访问改为对“状态-动作”二元组的访问,蒙特卡洛算法就可以几乎和之前完全相同的方式解决该问题,唯一复杂之处在于一些“状态-动作”二元组可能永远不会被访问到。为了实现基于动作价值函数的策略评估,我们必须保证持续的试探。一种方式是将指定的“状态-动作”二元组作为起点开始一幕采样,同时保证所有“状态-动作”二元组都有非零的概率可以被选为起点。这样就保证了在采样的幕个数趋于无穷时,每一个“状态-动作”二元组都会被访问到无数次。我们把这种假设称为试探性出发

策略改进的方法是在当前价值函数上贪心地选择动作。由于我们有动作价值函数,所以在贪心的时候完全不需要使用任何的模型信息。对于任意的一个动作价值函数 $q$,对应的贪心策略为:对于任意一个状态 $s \in \mathcal{S}$,必定选择对应动作价值函数最大的动作:

$$ \pi \left(s\right) = \arg\max_a q \left(s, a\right) $$

策略改进可以通过将 $q_{\pi_k}$ 对应的贪心策略作为 $\pi_{k+1}$ 来进行。这样的 $\pi_k$$\pi_{k+1}$ 满足策略改进定理,因为对于所有的状态 $s \in \mathcal{S}$

$$ \begin{aligned} q_{\pi_{k}}\left(s, \pi_{k+1}(s)\right) &=q_{\pi_{k}}\left(s, \underset{a}{\arg \max } q_{\pi_{k}}(s, a)\right) \\ &=\max _{a} q_{\pi_{k}}(s, a) \\ & \geqslant q_{\pi_{k}}\left(s, \pi_{k}(s)\right) \\ & \geqslant v_{\pi_{k}}(s) \end{aligned} $$

对于蒙特卡洛策略迭代,可以逐幕交替进行评估与改进。每一幕结束后,使用观测到的回报进行策略评估,然后在该幕序列访问到的每一个状态上进行策略改进。使用这个思路的一个简单算法称为基于试探性出发的蒙特卡洛(蒙特卡洛 ES),算法流程如下:

\begin{algorithm}
\caption{蒙特卡洛 ES(试探性出发),用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}$,任意初始化 $\pi \left(s\right) \in \mathcal{A} \left(s\right)$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,$Returns \left(s, a\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 选择 $S_0 \in \mathcal{S}$ 和 $A_0 \in \mathcal{A} \left(S_0\right)$ 以使得所有“状态-动作”二元组的概率都 $> 0$
  \STATE 从 $S_0, A_0$ 开始根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t, A_t$ 在 $S_0, A_0, S_1, A_1, \cdots, S_{t-1}, A_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t, A_t\right) \gets Resurn \left(S_t, A_t\right) \cup G$
      \STATE $Q \left(S_t, A_t\right) \gets avg \left(Returns \left(S_t, A_t\right)\right)$
      \STATE $\pi \left(S_t\right) \gets \arg\max_a Q \left(S_t, a\right)$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

利用蒙特卡洛 ES 可以很直接地解决二十一点游戏,只需随机等概率选择庄家的扑克牌、玩家手牌的点数,以及确定是否有可用的 A 即可。令只在 20 或 21 点停牌为初始策略,初始动作价值函数全部为零,下图展示了蒙特卡洛 ES 得出的最优策略:

同轨策略和离轨策略

同轨策略

为了避免很难被满足的试探性出发假设,一般性的解法是智能体能够持续不断地选择所有可能的动作,有两种方法可以保证这一点,同轨策略(on-policy)离轨策略(off-policy)。在同轨策略中,用于生成采样数据序列的策略和用于实际决策的待评估和改进的策略是相同的;而在离轨策略中,用于评估或改进的策略与生成采样数据的策略是不同的,即生成的数据“离开”了待优化的策略所决定的决策序列轨迹。

在同轨策略中,策略一般是“软性”的,即对于任意 $s \in \mathcal{S}$ 以及 $a \in \mathcal{A} \left(s\right)$,都有 $\pi \left(a | s\right) > 0$,但他们会逐渐地逼近一个确定性的策略。$\epsilon-$ 贪心策略是指在绝大多数时候都采取获得最大估计值的动作价值函数对应的动作,但同时以一个较小的概率 $\epsilon$ 随机选择一个动作。因此对于所有非贪心的动作都以 $\frac{\epsilon}{|\mathcal{A} \left(s\right)|}$ 的概率被选中,贪心动作则以 $1 - \epsilon + \frac{\epsilon}{|\mathcal{A} \left(s\right)|}$ 的概率被选中。同轨策略的蒙特卡洛控制

\begin{algorithm}
\caption{同轨策略的首次访问型 MC 控制算法(对于 $\epsilon-$ 软性策略),用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE $\pi \gets$ 一个任意的 $\epsilon-$ 软性策略
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,$Returns \left(s, a\right) \gets \varnothing$
\WHILE{TRUE}
  \STATE 根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \IF{$S_t, A_t$ 在 $S_0, A_0, S_1, A_1, \cdots, S_{t-1}, A_{t-1}$ 中出现过}
      \STATE $Returns \left(S_t, A_t\right) \gets Resurn \left(S_t, A_t\right) \cup G$
      \STATE $Q \left(S_t, A_t\right) \gets avg \left(Returns \left(S_t, A_t\right)\right)$
      \STATE $A^* \gets \arg\max_a Q \left(S_t, a\right)$
      \STATE 对于所有 $a \in \mathcal{A} \left(S_t\right)$,$\pi\left(a \mid S_{t}\right) \leftarrow\left\{\begin{array}{ll}
1-\varepsilon+\varepsilon /\left|\mathcal{A}\left(S_{t}\right)\right| & \text { if } a=A^{*} \\
\varepsilon /\left|\mathcal{A}\left(S_{t}\right)\right| & \text { if } a \neq A^{*}
\end{array}\right.$
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

离轨策略

所有的学习控制方法都面临一个困境:它们希望学到的动作可以使随后的智能体行为是最优的,但是为了搜索所有的动作(以保证找到最优动作),它们需要采取非最优的行动。同轨策略采用一种妥协的方法,它并不学习最优策略的动作值,而是学习一个接近最优而且仍能进行试探的策略的动作值。一个更加直接的方法是采用两个策略,一个用来学习并成为最优策略,另一个更加有试探性,用来产生智能体的行动样本。用来学习的策略被称为目标策略,用于生成行动样本的策略被称为行动策略。在这种情况下,我们认为学习所用的数据“离开”了待学习的目标策略,因此整个过程称为离轨策略学习

几乎所有的离轨策略方法都采用了重要度采样,重要度采样是一种在给定来自其他分布的样本的条件下,估计某种分布的期望值的通用方法。在离轨策略学习中,对回报值根据其轨迹在目标策略与行动策略中出现的相对概率进行加权,这个相对概率称为重要度采样比。给定起始状态 $S_t$,后续的“状态-动作”轨迹 $A_t, S_{t+1}, A_{t+1}, \cdots, S_T$ 在策略 $\pi$ 下发生的概率为:

$$ \begin{aligned} \operatorname{Pr}\left\{A_{t},\right.&\left.S_{t+1}, A_{t+1}, \ldots, S_{T} \mid S_{t}, A_{t: T-1} \sim \pi\right\} \\ &=\pi\left(A_{t} \mid S_{t}\right) p\left(S_{t+1} \mid S_{t}, A_{t}\right) \pi\left(A_{t+1} \mid S_{t+1}\right) \cdots p\left(S_{T} \mid S_{T-1}, A_{T-1}\right) \\ &=\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right) \end{aligned} $$

其中,$p$ 为状态转移概率函数。因此,在目标策略和行动策略轨迹下的相对概率(重要度采样比)为:

$$ \rho_{t: T-1} \doteq \frac{\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}{\prod_{k=t}^{T-1} b\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}=\prod_{k=t}^{T-1} \frac{\pi\left(A_{k} \mid S_{k}\right)}{b\left(A_{k} \mid S_{k}\right)} $$

化简后,重要度采样比只与两个策略和样本序列数据相关,而与 MDP 的动态特性(状态转移概率)无关。我们希望估计目标策略下的期望回报(价值),但我们只有行动策略中的回报 $G_t$。直接使用行动策略中的回报进行估计是不准的,因此需要使用重要度采样比调整回报从而得到正确的期望值:

$$ \mathbb{E}\left[\rho_{t: T-1} G_{t} \mid S_{t}=s\right]=v_{\pi}(s) $$

定义所有访问过状态 $s$ 的时刻集合为 $\mathcal{T} \left(s\right)$$T \left(t\right)$ 表示时刻 $t$ 后的首次终止,用 $G_t$ 表示在 $t$ 之后到达 $T \left(t\right)$ 时的回报值。则 $\left\{G_t\right\}_{t \in \mathcal{T} \left(s\right)}$ 就是状态 $s$ 对应的回报值,$\left\{\rho_{t:T \left(t\right) - 1}\right\}_{t \in \mathcal{T} \left(s\right)}$ 是相应的重要度采样比。则为了预测 $v_{\pi} \left(s\right)$,有:

$$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{|\mathcal{T}(s)|} \label{eq:ordinary-importance-sampling} $$

为一种简单平均实现的重要度采样,称之为普通重要度采样

$$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1}} \label{eq:weighted-importance-sampling} $$

为一种加权的重要度采样,称之为加权重要度采样,如果分母为零,则式 $\ref{eq:weighted-importance-sampling}$ 的值也为零。式 $\ref{eq:ordinary-importance-sampling}$ 得到的结果在期望上是 $v_{\pi} \left(s\right)$ 的无偏估计,但其值可能变得很极端,式 $\ref{eq:weighted-importance-sampling}$ 的估计是有偏的,但其估计的方差可以收敛到 0。

我们对二十一点游戏的状态值进行离轨策略估计。评估的状态为玩家有一张 A,一张 2(或者等价情况,有三张 A),从这个状态开始等概率选择要牌或停牌得到采样数据,目标策略只在和达到 20 或 21 时停牌。

在目标策略中,这个状态的值大概为 -0.27726(利用目标策略独立生成 1 亿幕数据后对回报进行平均得到)。两种离轨策略方法在采样随机策略经过 1000 幕离轨策略数据采样后都很好地逼近了这个值,但加权重要度采样在开始时错误率明显较低,这也是实践中的典型现象。

假设一个回报序列 $G_1, G_2, \cdots, G_{n-1}$,它们都从相同的状态开始,且每一个回报都对应一个随机权重 $W_i$,我们希望获得如下式子的估计:

$$ V_{n} \doteq \frac{\sum_{k=1}^{n-1} W_{k} G_{k}}{\sum_{k=1}^{n-1} W_{k}}, \quad n \geq 2 $$

同时在获得一个额外的回报 $G_n$ 时能保持更新。为了能不断跟踪 $V_n$ 的变化,我们必须为每一个状态维护前 $n$ 个回报对应的权值的累加和 $C_n$$V_n$ 的更新方法如下:

$$ \begin{array}{l} V_{n+1} \doteq V_{n}+\dfrac{W_{n}}{C_{n}}\left[G_{n}-V_{n}\right], \quad n \geq 1 \\ C_{n+1} \doteq C_{n}+W_{n+1} \end{array} $$

其中,$C_0 = 0$$V_1$ 是任意值。一个完整的用于蒙特卡洛策略评估的逐幕增量算法如下:

\begin{algorithm}
\caption{离轨策略 MC 预测算法(策略评估),用于估计 $Q \approx q_{\pi}$}
\begin{algorithmic}
\REQUIRE 一个任意的目标策略 $\pi$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,$C \left(s, a\right) \gets 0$
\WHILE{TRUE}
  \STATE $b \gets$ 任何能包括 $\pi$ 的策略
  \STATE 根据 $b$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \STATE $W \gets 1$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \STATE $C \left(S_t, A_t\right) \gets C \left(S_t, A_t\right) + W$
    \STATE $Q \left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\frac{W}{C\left(S_{t}, A_{t}\right)}\left[G-Q\left(S_{t}, A_{t}\right)\right]$
    \STATE $W \leftarrow W \frac{\pi\left(A_{t} \mid S_{t}\right)}{b\left(A_{t} \mid S_{t}\right)}$
    \IF{$W = 0$}
      \BREAK
    \ENDIF
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

在离轨策略中,策略的价值评估和策略的控制是分开的,用于生成行动数据的策略被称为行动策略,行动策略可能与实际上被评估和改善的策略无关,而被评估和改善的策略称为目标策略。这样分离的好处在于当行动策略能对所有可能的动作继续进行采样时,目标策略可以是确定的(贪心的)。

离轨策略蒙特卡洛控制方法要求行动策略对目标策略可能做出的所有动作都有非零的概率被选择。为了试探所有的可能性,要求行动策略是软性的。一个基于通用迭代策略(GPI)和重要度采样的离轨策略蒙特卡洛控制方法如下:

\begin{algorithm}
\caption{离轨策略 MC 控制算法,用于估计 $\pi \approx \pi_*$}
\begin{algorithmic}
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q \left(s, a\right) \in \mathbb{R}$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,$C \left(s, a\right) \gets 0$
\STATE 对于所有 $s \in \mathcal{S}, a \in \mathcal{A} \left(s\right)$,$\pi \left(s\right) \gets \arg\max_a Q \left(s, a\right)$
\STATE \COMMENT{出现平分情况选取方法应保持一致}
\WHILE{TRUE}
  \STATE $b \gets$ 任意软性策略
  \STATE 根据 $b$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$
  \STATE $G \gets 0$
  \STATE $W \gets 1$
  \FOR{$t \in T-1, T-2, \cdots, 0$}
    \STATE $G \gets \gamma G + R_{t+1}$
    \STATE $C \left(S_t, A_t\right) \gets C \left(S_t, A_t\right) + W$
    \STATE $Q \left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\frac{W}{C\left(S_{t}, A_{t}\right)}\left[G-Q\left(S_{t}, A_{t}\right)\right]$
    \STATE $\pi\left(S_{t}\right) \leftarrow \arg \max _{a} Q\left(S_{t}, a\right)$
    \STATE \COMMENT{出现平分情况选取方法应保持一致}
    \IF{$A_t \neq \pi \left(S_t\right)$}
      \BREAK
    \ENDIF
    \STATE $W \leftarrow W \frac{1}{b\left(A_{t} \mid S_{t}\right)}$
  \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

本文示例代码实现请参见这里


  1. Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎

  2. CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎

  3. UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

利用动态规划求解马尔可夫决策过程 (Planning by Dynamic Programming)

2020-06-13 08:00:00

本文为《强化学习系列》文章
本文内容主要参考自:
1.《强化学习》1
2. CS234: Reinforcement Learning 2
3. UCL Course on RL 3

动态规划

动态规划(Dynamic Programming,DP)是一种用于解决具有如下两个特性问题的通用算法:

  1. 优化问题可以分解为子问题。
  2. 子问题出现多次并可以被缓存和复用。

马尔可夫决策过程正符合这两个特性:

  1. 贝尔曼方程给定了迭代过程的分解。
  2. 价值函数保存并复用了解决方案。

在强化学习中,DP 的核心思想是使用价值函数来结构化地组织对最优策略的搜索。一旦得到了满足贝尔曼最优方程的价值函数 $v_*$$q_*$,得到最优策略就容易了。对于任意 $s \in \mathcal{S}$(状态集合),$a \in \mathcal{A} \left(s\right)$(动作集合)和 $s' \in \mathcal{S}^{+}$(在分幕式任务下 $\mathcal{S}$ 加上一个终止状态),有:

$$ \begin{aligned} v_{*}(s) &=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{*}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{*}\left(s^{\prime}\right)\right] \end{aligned} $$

$$ \begin{aligned} q_{*}(s, a) &=\mathbb{E}\left[R_{t+1}+\gamma \max _{a^{\prime}} q_{*}\left(S_{t+1}, a^{\prime}\right) | S_{t}=s, A_{t}=a\right] \\ &\left.=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma \max _{a^{\prime}}\right] q_{*}\left(s^{\prime}, a^{\prime}\right)\right] \end{aligned} $$

将贝尔曼方程转化成为近似逼近理想价值函数的递归更新公式,我们就得到了 DP 算法。

策略评估

对于一个策略 $\pi$,如何计算其状态价值函数 $v_{\pi}$ 被称为策略评估。对于任意 $s \in \mathcal{S}$,有:

$$ \begin{aligned} v_{\pi}(s) & \doteq \mathbb{E}_{\pi}\left[G_{t} | S_{t}=s\right] \\ &=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ &=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$

其中 $\pi \left(a | s\right)$ 表示在环境 $s$ 中智能体在策略 $\pi$ 下采取动作 $a$ 的概率。只要 $\gamma < 1$ 或者任何状态在 $\pi$ 下都能保证最后终止,则 $v_{\pi}$ 唯一存在。

考虑一个近似的价值函数序列 $v_0, v_1, \cdots$,从 $\mathcal{S}^{+}$ 映射到 $\mathbb{R}$,初始的近似值 $v_0$ 可以任意选取(除了终止状态必须为 0 外)。下一轮迭代的近似可以使用 $v_{\pi}$ 的贝尔曼方程进行更新,对于任意 $s \in \mathcal{S}$ 有:

$$ \begin{aligned} v_{k+1}(s) & \doteq \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s\right] \\ &=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$

显然,$v_k = v_{\pi}$ 是这个更新规则的一个不动点。在保证 $v_{\pi}$ 存在的条件下,序列 $\left\{v_k\right\}$$k \to \infty$ 时将会收敛到 $v_{\pi}$,这个算法称作 迭代策略评估

策略改进

对于任意一个确定的策略 $\pi$,我们已经确定了它的价值函数 $v_{\pi}$。对于某个状态 $s$,我们想知道是否应该选择一个不同于给定的策略的动作 $a \neq \pi \left(s\right)$。如果从状态 $s$ 继续使用现有策略,则最后的结果就是 $v \left(s\right)$,但我们并不知道换成一个新策略后是得到更好的结果还是更坏的结果。一种解决方法是在状态 $s$ 选择动作 $a$ 后,继续遵循现有的策略 $\pi$,则这种方法的价值为:

$$ \begin{aligned} q_{\pi}(s, a) & \doteq \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$

一个关键的准则就是这个值是大于还是小于 $v_{\pi} \left(s\right)$。如果这个值更大,则说明在状态 $s$ 选择动作 $a$,然后继续使用策略 $\pi$ 会比使用始终使用策略 $\pi$ 更优。

上述情况是策略改进定理的一个特例,一般来说,如果 $\pi$$\pi'$ 是任意两个确定的策略,对于任意 $s \in \mathcal{S}$

$$ q_{\pi}\left(s, \pi^{\prime}(s)\right) \geq v_{\pi}(s) $$

则称策略 $\pi'$ 相比于 $\pi$ 一样好或更好。也就是说,对于任意状态 $s \in \mathcal{S}$,这样肯定能得到一样或更好的期望回报:

$$ v_{\pi^{\prime}}(s) \geq v_{\pi}(s) $$

延伸到所有状态和所有可能的动作,即在每个状态下根据 $q_{\pi} \left(s, a\right)$ 选择一个最优的,换言之,考虑一个新的贪心策略 $\pi'$,满足:

$$ \begin{aligned} \pi^{\prime}(s) & \doteq \underset{a}{\arg \max } q_{\pi}(s, a) \\ &=\underset{a}{\arg \max } \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\underset{a}{\arg \max } \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$

这样构造出的贪心策略满足策略改进定理的条件,所以它和原策略相比一样好或更好。这种根据原策略的价值函数执行贪心算法,来构造一个更好策略的过程称之为策略改进。如果新的贪心策略 $\pi'$ 和原策略 $\pi$ 一样好而不是更好,则有 $v_{\pi} = v_{\pi'}$,对任意 $s \in \mathcal{S}$

$$ \begin{aligned} v_{\pi^{\prime}}(s) &=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{\pi^{\prime}}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi^{\prime}}\left(s^{\prime}\right)\right] \end{aligned} $$

这同贝尔曼方程完全相同,因此 $v_{\pi}$ 一定与 $v_*$ 相同,$\pi$$\pi'$ 均必须为最优策略。因此,在除了原策略即为最优策略的情况下,策略改进一定会给出一个更优的结果。

策略迭代

一个策略 $\pi$ 根据 $v_{\pi}$ 产生了一个更好的策略 $\pi'$,进而我们可以通过计算 $v_{\pi'}$ 来得到一个更优的策略 $\pi''$。这样一个链式的方法可以得到一个不断改进的策略和价值函数序列:

$$ \pi_{0} \stackrel{E}{\longrightarrow} v_{\pi_{0}} \stackrel{I}{\longrightarrow} \pi_{1} \stackrel{E}{\longrightarrow} v_{\pi_{1}} \stackrel{I}{\longrightarrow} \pi_{2} \stackrel{E}{\longrightarrow} \cdots \stackrel{I}{\longrightarrow} \pi_{*} \stackrel{E}{\longrightarrow} v_{*} $$

其中 $\stackrel{E}{\longrightarrow}$ 表示策略评估,$\stackrel{I}{\longrightarrow}$ 表示策略改进。每一个策略都能保证同前一个一样或者更优,由于一个有限 MDP 必然只有有限种策略,所以在有限次的迭代后,这种方法一定收敛到一个最优的策略与最优价值函数。这种寻找最优策略的方法叫做策略迭代。整个策略迭代算法如下:

\begin{algorithm}
\caption{迭代策略算法}
\begin{algorithmic}
\FUNCTION{PolicyIteration}{}
\STATE \COMMENT{初始化}
\FOR{$s \in \mathcal{S}$}
  \STATE 初始化 $V \left(s\right) \in \mathbb{R}$
  \STATE 初始化 $\pi \left(s\right) \in \mathcal{A} \left(s\right)$
\ENDFOR
\WHILE{true}
  \STATE \COMMENT{策略评估}
  \REPEAT
    \STATE $\Delta \gets 0$
    \FOR{$s \in \mathcal{S}$}
      \STATE $v \gets V \left(s\right)$
      \STATE $V \left(s\right) \gets \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, \pi \left(s\right)\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
      \STATE $\Delta \gets \max\left(\Delta, \left|v - V \left(s\right)\right|\right)$
    \ENDFOR
  \UNTIL{$\Delta < \theta$}
  \STATE \COMMENT{策略改进}
  \STATE policy-stable $\gets$ true
  \FOR{$s \in \mathcal{S}$}
    \STATE $\pi' \left(s\right) \gets \pi \left(s\right)$
    \STATE $\pi \left(s\right) \gets \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
    \IF{$\pi' \left(s\right) \neq \pi \left(s\right)$}
      \STATE policy-stable $\gets$ flase
    \ENDIF
  \ENDFOR
  \IF{policy-stable $=$ true}
    \BREAK
  \ENDIF
\ENDWHILE
\RETURN $V, \pi$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

杰克租车(Jack’s Car)问题为例:杰克在两地运营租车公司,每租出一辆车获得 10 元收益,为了保证每个地点有车可用,杰克需要夜间在两地之间移动车辆,每辆车的移动代价为 2 元。假设每个地点租车和还车的数量符合泊松分布 $\dfrac{\lambda^n}{n!} e^{- \lambda}$,其中 $\lambda$ 为期望值,租车的 $\lambda$ 在两地分别为 3 和 4,还车的 $\lambda$ 在两地分别为 3 和 2。假设任何一个地点不超过 20 辆车,每天最多移动 5 辆车,折扣率 $\gamma = 0.9$,将问题描述为一个持续的有限 MPD,时刻按天计算,状态为每天结束时每个地点的车辆数,动作则为夜间在两个地点之间移动的车辆数。策略从不移动任何车辆开始,整个策略迭代过程如下图所示:

上例代码实现请参见这里

价值迭代

策略迭代算法的一个缺点是每一次迭代都涉及了策略评估,这是一个需要多次遍历状态集合的迭代过程。如果策略评估是迭代进行的,那么收敛到 $v_{\pi}$ 理论上在极限处才成立,实际中不必等到其完全收敛,可以提前截断策略评估过程。有多种方式可以截断策略迭代中的策略评估步骤,并且不影响其收敛,一种重要的特殊情况是在一次遍历后即刻停止策略评估,该算法称为价值迭代。可以将此表示为结合了策略改进与阶段策略评估的简单更新公式,对任意 $s \in \mathcal{S}$

$$ \begin{aligned} v_{k+1}(s) & \doteq \max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$

可以证明,对任意 $v_0$,在 $v_*$ 存在的条件下,序列 $\left\{v_k\right\}$ 都可以收敛到 $v_*$。整个价值迭代算法如下:

\begin{algorithm}
\caption{价值迭代算法}
\begin{algorithmic}
\FUNCTION{ValueIteration}{}
\STATE \COMMENT{初始化}
\FOR{$s \in \mathcal{S}^{+}$}
  \STATE 初始化 $V \left(s\right)$,其中 $V \left(\text{终止状态}\right) = 0$
\ENDFOR
\STATE \COMMENT{价值迭代}
\REPEAT
  \STATE $\Delta \gets 0$
  \FOR{$s \in \mathcal{S}$}
    \STATE $v \gets V \left(s\right)$
    \STATE $V \left(s\right) \gets\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
    \STATE $\Delta \gets \max\left(\Delta, \left|v - V \left(s\right)\right|\right)$
  \ENDFOR
\UNTIL{$\Delta < \theta$}
\STATE 输出一个确定的策略 $\pi \approx \pi_*$ 使得 $\pi(s)=\arg \max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right]$
\RETURN $V, \pi$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

以**赌徒问题(Gambler’s Problem)**为例:一个赌徒下注猜一系列抛硬币实验的结果,如果正面朝上则获得这一次下注的钱,如果背面朝上则失去这一次下注的钱,游戏在达到目标收益 100 元或全部输光时结束。每抛一次硬币,赌徒必须从他的赌资中选取一个整数来下注,这个问题可以表示为一个非折扣的分幕式有限 MDP。状态为赌徒的赌资 $s \in \left\{1, 2, \cdots, 99\right\}$,动作为赌徒下注的金额 $a \in \left\{0, 1, \cdots, \min \left(s, 100 - s\right)\right\}$,收益在一般情况下为 0,只有在赌徒达到获利 100 元的终止状态时为 1。

$p_h$ 为抛硬币正面朝上的概率,如果 $p_h$ 已知,那么整个问题可以由价值迭代或其他类似算法解决。下图为当 $p_h = 0.4$ 时,价值迭代连续遍历得到的价值函数和最后的策略。

上例代码实现请参见这里

异步动态规划

之前讨论的 DP 方法的一个主要缺点是它们涉及对 MDP 的整个状态集的操作,如果状态集很大,即使单次遍历也会十分昂贵。异步动态规划算法是一类就地迭代的 DP 算法,其不以系统遍历状态集的形式来组织算法。这些算法使用任意可用的状态值,以任意顺序来更新状态值,在某些状态的值更新一次之前,另一些状态的值可能已经更新了好几次。然而为了正确收敛,异步算法必须要不断地更新所有状态的值:在某个计算节点后,它不能忽略任何一个状态。

广义策略迭代

策略迭代包含两个同时进行的相互作用的流程,一个使得价值函数与当前策略一致(策略评估),另一个根据当前价值函数贪心地更新策略(策略改进)。在策略迭代中,这两个流程交替进行,每个流程都在另一个开始前完成。然而这也不是必须的,在异步方法中,评估和改进流程则以更细的粒度交替进行。我们利用**广义策略迭代(GPI)**一词来指代策略评估和策略改进相互作用的一般思路,与这两个流程的力度和其他细节无关。

几乎所有的强化学习方法都可以被描述为 GPI,几乎所有方法都包含明确定义的策略和价值函数。策略总是基于特定的价值函数进行改进,价值函数也始终会向对应特定策略的真实价值函数收敛。

GPI 的评估和改进流程可以视为两个约束或目标之间的相互作用的流程。每个流程都把价值函数或策略推向其中的一条线,该线代表了对于两个目标中的某一个目标的解决方案,如下图所示:


  1. Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎

  2. CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎

  3. UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

贝叶斯优化 (Bayesian Optimization)

2020-06-06 08:00:00

本文内容主要参考自:

  1. 从高斯分布到高斯过程、高斯过程回归、贝叶斯优化
  2. A Visual Exploration of Gaussian Processes
  3. Gaussian Process Regression
  4. Exploring Bayesian Optimization

高斯分布

一元高斯分布

若随机变量 $X$ 服从一个均值为 $\mu$,方差为 $\sigma^2$ 的高斯分布,则记为:

$$ X \sim N \left(\mu, \sigma^2\right) $$

其概率密度函数为:

$$ f \left(x\right) = \dfrac{1}{\sigma \sqrt{2 \pi}} e^{- \dfrac{\left(x - \mu\right)^2}{2 \sigma^2}} $$

图片来源:https://zh.wikipedia.org/wiki/正态分布

二元高斯分布

若随机变量 $X, Y$ 服从均值为 $\mu = \left(\mu_X, \mu_Y\right)^{\top}$,方差为 $\mu = \left(\sigma_X, \sigma_Y\right)^{\top}$ 的高斯分布,则记为:

$$ \left(X, Y\right) \sim \mathcal{N} \left(\mu, \sigma\right) $$

其概率密度函数为:

$$ f(x, y)=\frac{1}{2 \pi \sigma_{X} \sigma_{Y} \sqrt{1-\rho^{2}}} e^{-\dfrac{1}{2\left(1-\rho^{2}\right)}\left[\dfrac{\left(x-\mu_{X}\right)^{2}}{\sigma_{X}^{2}}+\dfrac{\left(y-\mu_{Y}\right)^{2}}{\sigma_{Y}^{2}}-\dfrac{2 \rho\left(x-\mu_{X}\right)\left(y-\mu_{X}\right)}{\sigma_{X} \sigma_{Y}}\right]} $$

其中,$\rho$$X$$Y$ 之间的相关系数,$\sigma_X > 0$$\sigma_Y > 0$

图片来源:Bayesian tracking of multiple point targets using expectation maximization

多元高斯分布

$K$ 维随机向量 $X = \left[X_1, \cdots, X_K\right]^{\top}$ 服从多元高斯分布,则必须满足如下三个等价条件:

  1. 任何线性组合 $Y = a_1 X_1 + \cdots a_K X_K$ 均服从高斯分布。
  2. 存在随机向量 $Z = \left[Z_1, \cdots, Z_L\right]^{\top}$(每个元素服从独立标准高斯分布),向量 $\mu = \left[\mu_1, \cdots, \mu_K\right]^{\top}$ 以及 $K \times L$ 的矩阵 $A$,满足 $X = A Z + \mu$
  3. 存在 $\mu$ 和一个对称半正定矩阵 $\Sigma$ 满足 $X$ 的特征函数 $\phi_X \left(u; \mu, \Sigma\right) = \exp \left(i \mu^{\top} u - \dfrac{1}{2} u^{\top} \Sigma u\right)$

如果 $\Sigma$ 是非奇异的,则概率密度函数为:

$$ f \left(x_1, \cdots, x_k\right) = \dfrac{1}{\sqrt{\left(2 \pi\right)^k \lvert\Sigma\rvert}} e^{- \dfrac{1}{2} \left(x - \mu\right)^{\top} \Sigma^{-1} \left(x - \mu\right)} $$

其中 $\lvert\Sigma\rvert$ 表示协方差矩阵的行列式。

边缘化和条件化

高斯分布具有一个优秀的代数性质,即在边缘化和条件化下是闭合的,也就是说从这些操作中获取的结果分布也是高斯的。**边缘化(Marginalization)条件化(Conditioning)**都作用于原始分布的子集上:

$$ P_{X, Y}=\left[\begin{array}{l} X \\ Y \end{array}\right] \sim \mathcal{N}(\mu, \Sigma)=\mathcal{N}\left(\left[\begin{array}{l} \mu_{X} \\ \mu_{Y} \end{array}\right],\left[\begin{array}{l} \Sigma_{X X} \Sigma_{X Y} \\ \Sigma_{Y X} \Sigma_{Y Y} \end{array}\right]\right) $$

其中,$X$$Y$ 表示原始随机变量的子集。

对于随机向量 $X$$Y$ 的高斯概率分布 $P \left(X, Y\right)$,其边缘概率分布为:

$$ \begin{array}{l} X \sim \mathcal{N}\left(\mu_{X}, \Sigma_{X X}\right) \\ Y \sim \mathcal{N}\left(\mu_{Y}, \Sigma_{Y Y}\right) \end{array} $$

$X$$Y$ 两个子集各自只依赖于 $\mu$$\Sigma$ 中它们对应的值。因此从高斯分布中边缘化一个随机变量仅需从 $\mu$$\Sigma$ 中舍弃相应的变量即可:

$$ p_{X}(x)=\int_{y} p_{X, Y}(x, y) d y=\int_{y} p_{X | Y}(x | y) p_{Y}(y) d y $$

条件化可以用于得到一个变量在另一个变量条件下的概率分布:

$$ \begin{array}{l} X | Y \sim \mathcal{N}\left(\mu_{X}+\Sigma_{X Y} \Sigma_{Y Y}^{-1}\left(Y-\mu_{Y}\right), \Sigma_{X X}-\Sigma_{X Y} \Sigma_{Y Y}^{-1} \Sigma_{Y X}\right) \\ Y | X \sim \mathcal{N}\left(\mu_{Y}+\Sigma_{Y X} \Sigma_{X X}^{-1}\left(X-\mu_{X}\right), \Sigma_{Y Y}-\Sigma_{Y X} \Sigma_{X X}^{-1} \Sigma_{X Y}\right) \end{array} $$

需要注意新的均值仅依赖于作为条件的变量,协方差矩阵和这个变量无关。

边缘化可以理解为在高斯分布的一个维度上的累加,条件化可以理解为在多元分布上切一刀从而获得一个维数更少的高斯分布,如下图所示:

高斯过程

**高斯过程(Gaussian Process)**是观测值出现在一个连续域(例如时间或空间)的随机过程。在高斯过程中,连续输入空间中每个点都是与一个正态分布的随机变量相关联。此外,这些随机变量的每个有限集合都有一个多元正态分布,换句话说它们的任意有限线性组合是一个正态分布。高斯过程的分布是所有那些(无限多个)随机变量的联合分布,正因如此,它是连续域(例如时间或空间)上函数的分布。

简单而言,高斯过程即为一系列随机变量,这些随机变量的任意有限集合均为一个多元高斯分布。从一元高斯分布多元高斯分布相当于增加了空间维度,从高斯分布高斯过程相当于引入了时间维度。一个高斯过程可以被均值函数 $m \left(x\right)$ 和协方差函数 $K \left(x, x'\right)$ 共同唯一确定:

$$ \begin{aligned} m(x) &=\mathbb{E}[f(x)] \\ K\left(x, x'\right) &=\mathbb{E}\left[(f(x)-m(x))\left(f\left(x^{\prime}\right)-m\left(x^{\prime}\right)\right)\right] \end{aligned} $$

则高斯过程可以表示为:

$$ f \left(x\right) \sim \mathcal{GP} \left(m \left(x\right), K \left(x, x'\right)\right) $$

均值函数决定了样本出现的整体位置,如果为零则表示以 $y = 0$ 为基准线。协方差函数描述了不同点之间的关系,从而可以利用输入的训练数据预测未知点的值。常用的协方差函数有:

高斯过程回归

回归任务的目标是给定一个输入变量 $x \in \mathbb{R}^D$ 预测一个或多个连续目标变量 $y$ 的值。更确切的说,给定一个包含 $N$ 个观测值的训练集 $\mathbf{X} = \left\{x_n\right\}^N_1$ 和对应的目标值 $\mathbf{Y} = \left\{y_n\right\}^N_1$,回归的目标是对于一个新的 $x$ 预测对应的 $y$。目标值和观测值之间通过一个映射进行关联:

$$ f: X \to Y $$

在贝叶斯模型中,我们通过观测数据 $\mathcal{D} = \left\{\left(\mathbf{x}_n, \mathbf{y}_n\right)\right\}^N_{n=1}$ 更新先验分布 $P \left(\mathbf{\Theta}\right)$。通过贝叶斯公式我们可以利用先验概率 $P \left(\mathbf{\Theta}\right)$ 和似然函数 $P \left(\mathcal{D} | \mathbf{\Theta}\right)$ 推导出后验概率:

$$ p\left(\mathbf{\Theta} | \mathcal{D}\right)=\frac{p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right)}{p\left(\mathcal{D}\right)} $$

其中 $p\left(\mathcal{D}\right)$ 为边际似然。在贝叶斯回归中我们不仅希望获得未知输入对应的预测值 $\mathbf{y}_*$ ,还希望知道预测的不确定性。因此我们需要利用联合分布和边缘化模型参数 $\mathbf{\Theta}$ 来构造预测分布:

$$ p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathcal{D}\right)=\int p\left(\mathbf{y}_{*}, \mathbf{\Theta} | \mathbf{x}_{*}, \mathcal{D}\right) \mathrm{d} \Theta=\int p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathbf{\Theta}, \mathcal{D}\right) p(\mathbf{\Theta} | \mathcal{D}) \mathrm{d} \mathbf{\Theta} $$

通常情况下,由于积分形式 $p \left(\Theta | \mathcal{D}\right)$ 不具有解析可解性(Analytically Tractable):

$$ p\left(\mathcal{D}\right)=\int p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right) d \Theta $$

但在高斯似然和高斯过程先验的前提下,后验采用函数的高斯过程的形式,同时是解析可解的。

对于高斯过程回归,我们构建一个贝叶斯模型,首先定义函数输出的先验为一个高斯过程:

$$ p \left(f | \mathbf{X}, \theta\right) = \mathcal{N} \left(\mathbf{0}, K \left(\mathbf{X}, \mathbf{X}\right)\right) $$

其中 $K \left(\cdot, \cdot\right)$ 为协方差函数,$\theta$ 为过程的超参数。假设数据已经变换为零均值,因此我们不需要在先验中设置均值函数,则令似然形式如下:

$$ p \left(\mathbf{Y} | f\right) \sim \mathcal{N} \left(f, \sigma^2_n \mathbf{I}\right) $$

假设观测值为独立同分布的高斯噪音的累加,则整个模型的联合分布为:

$$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) $$

虽然我们并不关心变量 $f$,但由于我们需要对不确定性进行建模,我们仍需考虑 $\mathbf{Y}$$f$ 以及 $f$$\mathbf{X}$ 之间的关系。高斯过程作为一个非参数模型,其先验分布构建于映射 $f$ 之上,$f$ 仅依赖于核函数的超参数 $\theta$,且这些超参数可以通过数据进行估计。我们可以将超参数作为先验,即:

$$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) p \left(\theta\right) $$

然后进行贝叶斯推断和模型选择,但是通常情况下这是不可解的。David MacKay 引入了一个利用最优化边际似然来估计贝叶斯平均的框架,即计算如下积分:

$$ p \left(\mathbf{Y} | \mathbf{X}, \theta\right) = \int p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) df $$

其中,高斯似然 $p \left(\mathbf{Y} | f\right)$ 表示模型拟合数据的程度,$p \left(f | \mathbf{X}, \theta\right)$ 为高斯过程先验。经过边缘化后,$\mathbf{Y}$ 不在依赖于 $f$ 而仅依赖于 $\theta$

假设采用零均值函数,对于一个高斯过程先验,我们仅需指定一个协方差函数。以指数平方协方差函数为例,选择一系列测试输入点 $X_*$,利用协方差矩阵和测试输入点可以生成一个高斯向量:

$$ \mathbf{f}_* \sim \mathcal{N} \left(\mathbf{0}, K \left(X_*, X_*\right)\right) $$

从高斯先验中进行采样,我们首先需要利用标准正态来表示多元正态:

$$ \mathbf{f}_* \sim \mu + \mathbf{B} \mathcal{N} \left(0, \mathbf{I}\right) $$

其中,$\mathbf{BB}^{\top} = K \left(X_*, X_*\right)$$\mathbf{B}$ 本质上是协方差矩阵的平方根,可以通过 Cholesky 分解获得。

上图(左)为从高斯先验中采样的 10 个序列,上图(右)为先验的协方差。如果输入点 $x_n$$x_m$ 接近,则对应的 $f \left(x_n\right)$$f \left(x_m\right)$ 相比于不接近的点是强相关的。

我们关注的并不是这些随机的函数,而是如何将训练数据中的信息同先验进行合并。假设观测数据为 $\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$,则训练目标 $\mathbf{f}$ 和测试目标 $\mathbf{f}_*$ 之间的联合分布为:

$$ \left[\begin{array}{l} \mathbf{f} \\ \mathbf{f}_{*} \end{array}\right] \sim \mathcal{N}\left(\mathbf{0},\left[\begin{array}{ll} K(X, X) & K\left(X, X_{*}\right) \\ K\left(X_{*}, X\right) & K\left(X_{*}, X_{*}\right) \end{array}\right]\right) $$

根据观测值对联合高斯先验分布进行条件化处理可以得到高斯过程回归的关键预测方程:

$$ \mathbf{f}_{*} | X, X_{*}, \mathbf{f} \sim \mathcal{N}\left(\overline{\mathbf{f}}_{*}, \operatorname{cov}\left(\mathbf{f}_{*}\right)\right) $$

其中

$$ \begin{aligned} \overline{\mathbf{f}}_{*} & \triangleq \mathbb{E}\left[\mathbf{f}_{*} | X, X_{*}, \mathbf{f}\right]=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$

函数值可以通过对联合后验分布采样获得。

我们以三角函数作为给定的函数,并随机采样一些训练数据 $\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$,如下图所示:

我们希望将训练数据和高斯过程先验进行合并得到联合后验分布,我们可以通过在观测值上条件化联合高斯先验分布,预测的均值和协方差为:

$$ \begin{aligned} \overline{\mathbf{f}}_{*} &=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$

Rasmussen 和 Williams 给出了一个实现高斯过程回归的实用方法:

\begin{algorithm}
\caption{高斯过程回归算法}
\begin{algorithmic}
\REQUIRE \\
    输入 $\mathbf{X}$ \\
    目标 $\mathbf{y}$ \\
    协方差函数 $k$ \\
    噪音水平 $\sigma^2_n$ \\
    测试输入 $\mathbf{x}_*$
\ENSURE \\
    均值 $\bar{f}_*$ \\
    方差 $\mathbb{V}\left[f_{*}\right]$
\FUNCTION{GaussianProcessRegression}{$\mathbf{X}, \mathbf{y}, k, \sigma^2_n, \mathbf{x}_*$}
\STATE $L \gets \text{cholesky} \left(K + \sigma^2_n I\right)$
\STATE $\alpha \gets L^{\top} \setminus \left(L \setminus \mathbf{y}\right)$
\STATE $\bar{f}_* \gets \mathbf{k}^{\top}_* \alpha$
\STATE $\mathbf{v} \gets L \setminus \mathbf{k}_*$
\STATE $\mathbb{V}\left[f_{*}\right] \gets k \left(\mathbf{x}_*, \mathbf{x}_*\right) - \mathbf{v}^{\top} \mathbf{v}$
\RETURN $\bar{f}_*, \mathbb{V}\left[f_{*}\right]$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

高斯过程后验和采样的序列如下图所示:

先验的协方差矩阵和后验的协方差矩阵可视化如下图所示:

本小结代码请参见这里

贝叶斯优化

主动学习

在很多机器学习问题中,数据标注往往需要耗费很大成本。**主动学习(Active Learning)**在最大化模型准确率时最小化标注成本,例如对不确定性最高的数据进行标注。由于我们仅知道少量数据点,因此我们需要一个代理模型(Surrogate Model)来建模真正的模型。高斯过程因其灵活性和具有估计不确定性估计的特性不失为一个常用的代理模型。

在估计 $f \left(x\right)$ 的过程中,我们希望最小化评估的次数,因此我们可以通过主动学习来“智能”地选择下一个评估的数据点。通过不断的选择具有最高不确定性的数据点来获得 $f \left(x\right)$ 更准确的估计,直至收敛或达到停止条件。下图展示了利用主动学习估计真实数据分布的过程:

/images/cn/2020-06-06-bayesian-optimization/ active-gp- png 300

贝叶斯优化问题

贝叶斯优化的核心问题是:基于现有的已知情况,如果选择下一步评估的数据点?在主动学习中我们选择不确定性最大的点,但在贝叶斯优化中我们需要在探索不确定性区域(探索)和关注已知具有较优目标值的区域之间进行权衡(开发)。这种评价的依据称之为采集函数(Acquisition Functions),采集函数通过当前模型启发式的评估是否选择一个数据点。

贝叶斯优化的目标是找到一个函数 $f: \mathbb{R}^d \mapsto \mathbb{R}$ 最大值(或最小值)对应的位置 $x \in \mathbb{R}^d$。为了解决这个问题,我们遵循如下算法:

  1. 选择一个代理模型用于建模真实函数 $f$ 和定义其先验。
  2. 给定观测集合,利用贝叶斯公式获取后验。
  3. 利用采集函数 $\alpha \left(x\right)$ 确性下一个采样点 $x_t = \arg\max_x \alpha \left(x\right)$
  4. 将采样的点加入观测集合,重复步骤 2 直至收敛或达到停止条件。

采集函数

Probability of Improvement (PI) 采集函数会选择具有最大可能性提高当前最大的 $f \left(x^{+}\right)$ 值的点作为下一个查询点,即:

$$ x_{t+1} = \arg\max \left(\alpha_{PI} \left(x\right)\right) = \arg\max \left(P \left(f \left(x\right)\right) \geq \left(f \left(x^{+}\right) + \epsilon\right)\right) $$

其中,$P \left(\cdot\right)$ 表示概率,$\epsilon$ 为一个较小的正数,$x^{+} = \arg\max_{x_i \in x_{1:t}} f \left(x_i\right)$$x_i$ 为第 $i$ 步查询点的位置。如果采用高斯过程作为代理模型,上式则转变为:

$$ x_{t+1} = \arg\max_x \Phi \left(\dfrac{\mu_t \left(x\right) - f \left(x^{+}\right) - \epsilon}{\sigma_t \left(x\right)}\right) $$

其中,$\Phi \left(\cdot\right)$ 表示标准正态分布累积分布函数。PI 利用 $\epsilon$ 来权衡探索和开发,增加 $\epsilon$ 的值会更加倾向进行探索。

PI 仅关注了有多大的可能性能够提高,而没有关注能够提高多少。Expected Improvement (EI) 则会选择具有最大期望提高的点作为下一个查询点,即:

$$ x_{t+1} = \arg\min_x \mathbb{E} \left(\left\|h_{t+1} \left(x\right) - f \left(x^*\right)\right\| | \mathcal{D}_t\right) $$

其中,$f$ 为真实函数,$h_{t+1}$ 为代理模型在 $t+1$ 步的后验均值,$\mathcal{D}_t = \left\{\left(x_i, f\left(x_i\right)\right)\right\}, \forall x \in x_{1:t}$ 为训练数据,$x^*$$f$ 取得最大值的真实位置。

上式中我们希望选择能够最小化与最大目标值之间距离的点,由于我们并不知道真实函数 $f$,Mockus 1 提出了一种解决办法:

$$ x_{t+1} = \arg\max_x \mathbb{E} \left(\max \left\{0, h_{t+1} \left(x\right) - f \left(x^{+}\right)\right\} | \mathcal{D}_t\right) $$

其中,$f \left(x^{+}\right)$ 为到目前为止遇见的最大函数值,如果采用高斯过程作为代理模型,上式则转变为:

$$ \begin{aligned} EI(x) &= \left\{\begin{array}{ll} \left(\mu_{t}(x)-f\left(x^{+}\right)-\epsilon\right) \Phi(Z)+\sigma_{t}(x) \phi(Z), & \text { if } \sigma_{t}(x)>0 \\ 0 & \text { if } \sigma_{t}(x)=0 \end{array}\right. \\ Z &= \frac{\mu_{t}(x)-f\left(x^{+}\right)-\epsilon}{\sigma_{t}(x)} \end{aligned} $$

其中 $\Phi \left(\cdot\right)$ 表示标准正态分布累积分布函数,$\phi \left(\cdot\right)$ 表示标准正态分布概率密度函数。类似 PI,EI 也可以利用 $\epsilon$ 来权衡探索和开发,增加 $\epsilon$ 的值会更加倾向进行探索。

上图展示了在仅包含一个训练观测数据 $\left(0.5, f \left(0.5\right)\right)$ 情况下不同点的采集函数值。可以看出 $\alpha_{EI}$$\alpha_{PI}$ 的最大值分别为 0.3 和 0.47。选择一个具有较小的 $\alpha_{PI}$ 和一个较大的 $\alpha_{EI}$ 的点可以理解为一个高的风险和高的回报。因此,当多个点具有相同的 $\alpha_{EI}$ 时,我们应该优先选择具有较小风险(高 $\alpha_{PI}$)的点,类似的,当多个点具有相同的 $\alpha_{PI}$ 时,我们应该优先选择具有较大回报(高 $\alpha_{EI}$)的点。

其他采集函数还有 Thompson Sampling 2,Upper Confidence Bound (UCB),Gaussian Process Upper Confidence Bound (GP-UCB) 3,Entropy Search 4,Predictive Entropy Search 5 等,细节请参见原始论文或 A Tutorial on Bayesian Optimization 6

开放资源


  1. Mockus, J. B., & Mockus, L. J. (1991). Bayesian approach to global optimization and application to multiobjective and constrained problems. Journal of Optimization Theory and Applications, 70(1), 157-172. ↩︎

  2. Thompson, W. R. (1933). On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. Biometrika, 25(3/4), 285-294. ↩︎

  3. Auer, P. (2002). Using confidence bounds for exploitation-exploration trade-offs. Journal of Machine Learning Research, 3(Nov), 397-422. ↩︎

  4. Hennig, P., & Schuler, C. J. (2012). Entropy search for information-efficient global optimization. Journal of Machine Learning Research, 13(Jun), 1809-1837. ↩︎

  5. Hernández-Lobato, J. M., Hoffman, M. W., & Ghahramani, Z. (2014). Predictive entropy search for efficient global optimization of black-box functions. In Advances in neural information processing systems (pp. 918-926). ↩︎

  6. Frazier, P. I. (2018). A tutorial on bayesian optimization. arXiv preprint arXiv:1807.02811↩︎

马尔可夫决策过程 (Markov Decision Process)

2020-05-23 08:00:00

本文为《强化学习系列》文章
本文内容主要参考自:
1.《强化学习》1
2. CS234: Reinforcement Learning 2
3. UCL Course on RL 3

马尔可夫模型

马尔可夫模型是一种用于序列数据建模的随机模型,其假设未来的状态仅取决于当前的状态,即:

$$ \mathbb{P} \left[S_{t+1} | S_t\right] = \mathbb{P} \left[S_{t+1} | S_1, \cdots, S_t\right] $$

也就是认为当前状态捕获了历史中所有相关的信息。根据系统状态是否完全可被观测以及系统是自动的还是受控的,可以将马尔可夫模型分为 4 种,如下表所示:

状态状态完全可被观测 系统状态不是完全可被观测
状态是自动的 马尔可夫链(MC) 隐马尔可夫模型(HMM)
系统是受控的 马尔可夫决策过程(MDP) 部分可观测马尔可夫决策过程(POMDP)

马尔可夫链(Markov Chain,MC)为从一个状态到另一个状态转换的随机过程,当马尔可夫链的状态只能部分被观测到时,即为隐马尔可夫模型(Hidden Markov Model,HMM),也就是说观测值与系统状态有关,但通常不足以精确地确定状态。马尔可夫决策过程(Markov Decision Process,MDP)也是马尔可夫链,但其状态转移取决于当前状态和采取的动作,通常一个马尔可夫决策过程用于计算依据期望回报最大化某些效用的行动策略。部分可观测马尔可夫决策过程(Partially Observable Markov Decision Process,POMDP)即为系统状态仅部分可见情况下的马尔可夫决策过程。

马尔可夫过程

对于一个马尔可夫状态 $s$ 和一个后继状态 $s'$,状态转移概率定义为:

$$ \mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right] $$

状态概率矩阵 $\mathcal{P}$ 定义了从所有状态 $s$ 到后继状态 $s'$ 的转移概率:

$$ \mathcal{P} = \left[\begin{array}{ccc} \mathcal{P}_{11} & \cdots & \mathcal{P}_{1 n} \\ \vdots & & \\ \mathcal{P}_{n 1} & \cdots & \mathcal{P}_{n n} \end{array}\right] $$

其中每一行的加和为 1。

马尔可夫过程马尔可夫链)是一个无记忆的随机过程,一个马尔可夫过程可以定义为 $\langle \mathcal{S}, \mathcal{P} \rangle$,其中 $\mathcal{S}$ 是一个有限状态集合,$\mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right]$$\mathcal{P}$ 为状态转移概率矩阵。以一个学生的日常生活为例,Class $i$ 表示第 $i$ 门课程,Facebook 表示在 Facebook 上进行社交,Pub 表示去酒吧,Pass 表示通过考试,Sleep 表示睡觉,这个马尔可夫过程如下图所示:

从而可以产生多种不同的序列,例如:

C1 -> C2 -> C3 -> Pass -> Sleep
C1 -> FB -> FB -> C1 -> C2 -> Sleep
C1 -> C2 -> C3 -> Pub -> C2 -> C3 -> Pass -> Sleep

状态转移概率矩阵如下所示:

据此我们可以定义马尔可夫奖励过程Markov Reward Process,MRP)为 $\langle \mathcal{S, P, R}, \gamma \rangle$,其中 $\mathcal{S}$$\mathcal{P}$ 同马尔可夫过程定义中的参数相同,$\mathcal{R}$ 为收益函数,$\mathcal{R}_s = \mathbb{E} \left[R_t | S_{t-1} = s\right]$$\gamma \in \left[0, 1\right]$折扣率。如下图所示:

期望回报 $G_t$ 定义为从时刻 $t$ 之后的所有衰减的收益之和,即:

$$ G_t = R_{t+1} + \gamma R_{t+2} + \cdots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} $$

$\gamma$ 接近 $0$ 时,智能体更倾向于近期收益,当 $\gamma$ 接近 $1$ 时,智能体更侧重考虑长远收益。邻接时刻的收益可以按如下递归方式表示:

$$ G_t = R_{t+1} + \gamma G_{t+1} $$

对于存在“最终时刻”的应用中,智能体和环境的交互能被自然地分成一个系列子序列,每个子序列称之为“episodes)”,例如一盘游戏、一次走迷宫的过程,每幕都以一种特殊状态结束,称之为终结状态。这些幕可以被认为在同样的终结状态下结束,只是对不同的结果有不同的收益,具有这种分幕重复特性的任务称之为分幕式任务

MRP 的状态价值函数 $v \left(s\right)$ 给出了状态 $s$ 的长期价值,定义为:

$$ \begin{aligned} v(s) &=\mathbb{E}\left[G_{t} | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma\left(R_{t+2}+\gamma R_{t+3}+\ldots\right) | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma v\left(S_{t+1}\right) | S_{t}=s\right] \end{aligned} $$

价值函数可以分解为两部分:即时收益 $R_{t+1}$ 和后继状态的折扣价值 $\gamma v \left(S_{t+1}\right)$。上式我们称之为贝尔曼方程Bellman Equation),其衡量了状态价值和后继状态价值之间的关系。

马尔可夫决策过程

一个马尔可夫决策过程Markov Decision Process,MDP)定义为包含决策的马尔可夫奖励过程 $\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$,在这个环境中所有的状态均具有马尔可夫性。其中,$\mathcal{S}$ 为有限的状态集合,$\mathcal{A}$ 为有限的动作集合,$\mathcal{P}$ 为状态转移概率矩阵,$\mathcal{P}_{s s^{\prime}}^{a}=\mathbb{P}\left[S_{t+1}=s^{\prime} | S_{t}=s, A_{t}=a\right]$$\mathcal{R}$ 为奖励函数,$\mathcal{R}_{s}^{a}=\mathbb{E}\left[R_{t+1} | S_{t}=s, A_{t}=a\right]$$\gamma \in \left[0, 1\right]$ 为折扣率。上例中的马尔可夫决策过程如下图所示:

策略Policy)定义为给定状态下动作的概率分布:

$$ \pi \left(a | s\right) = \mathbb{P} \left[A_t = a | S_t = s\right] $$

一个策略完全确定了一个智能体的行为,同时 MDP 策略仅依赖于当前状态。给定一个 MDP $\mathcal{M}=\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$ 和一个策略 $\pi$,状态序列 $S_1, S_2, \cdots$ 为一个马尔可夫过程 $\langle \mathcal{S}, \mathcal{P}^{\pi} \rangle$,状态和奖励序列 $S_1, R_2, S_2, \cdots$ 为一个马尔可夫奖励过程 $\left\langle\mathcal{S}, \mathcal{P}^{\pi}, \mathcal{R}^{\pi}, \gamma\right\rangle$,其中

$$ \begin{aligned} \mathcal{P}_{s s^{\prime}}^{\pi} &=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{P}_{s s^{\prime}}^{a} \\ \mathcal{R}_{s}^{\pi} &=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{R}_{s}^{a} \end{aligned} $$

在策略 $\pi$ 下,状态 $s$ 的价值函数记为 $v_{\pi} \left(s\right)$,即从状态 $s$ 开始,智能体按照策略进行决策所获得的回报的概率期望值,对于 MDP 其定义为:

$$ \begin{aligned} v_{\pi} \left(s\right) &= \mathbb{E}_{\pi} \left[G_t | S_t = s\right] \\ &= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s\right] \end{aligned} $$

在策略 $\pi$ 下,在状态 $s$ 时采取动作 $a$ 的价值记为 $q_\pi \left(s, a\right)$,即根据策略 $\pi$,从状态 $s$ 开始,执行动作 $a$ 之后,所有可能的决策序列的期望回报:

$$ \begin{aligned} q_\pi \left(s, a\right) &= \mathbb{E}_{\pi} \left[G_t | S_t = s, A_t = a\right] \\ &= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s, A_t = a\right] \end{aligned} $$

状态价值函数 $v_{\pi}$ 和动作价值函数 $q_{\pi}$ 都能从经验中估计得到,两者都可以分解为当前和后继两个部分:

$$ \begin{aligned} v_{\pi}(s) &= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ q_{\pi}(s, a) &= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma q_{\pi}\left(S_{t+1}, A_{t+1}\right) | S_{t}=s, A_{t}=a\right] \end{aligned} $$

从一个状态 $s$ 出发,采取一个行动 $a$,状态价值函数为:

$$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s) q_{\pi}(s, a) $$

从一个动作 $s$ 出发,再采取一个行动 $a$ 后,动作价值函数为:

$$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right) $$

利用后继状态价值函数表示当前状态价值函数为:

$$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s)\left(\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right)\right) $$

利用后继动作价值函数表示当前动作价值函数为:

$$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \sum_{a^{\prime} \in \mathcal{A}} \pi\left(a^{\prime} | s^{\prime}\right) q_{\pi}\left(s^{\prime}, a^{\prime}\right) $$

最优状态价值函数 $v_* \left(s\right)$ 定义为所有策略上最大值的状态价值函数:

$$ v_* \left(s\right) = \mathop{\max}_{\pi} v_{\pi} \left(s\right) $$

最优动作价值函数 $q_* \left(s, a\right)$ 定义为所有策略上最大值的动作价值函数:

$$ q_* \left(s, a\right) = \mathop{\max}_{\pi} q_{\pi} \left(s, a\right) $$

定义不同策略之间的大小关系为:

$$ \pi \geq \pi^{\prime} \text { if } v_{\pi}(s) \geq v_{\pi^{\prime}}(s), \forall s $$

对于任意一个马尔可夫决策过程有:

一个最优策略可以通过最大化 $q_* \left(s, a\right)$ 获得:

$$ \pi_{*}(a | s)=\left\{\begin{array}{ll} 1 & \text { if } a=\underset{a \in \mathcal{A}}{\operatorname{argmax}} q_{*}(s, a) \\ 0 & \text { otherwise } \end{array}\right. $$

对于任意一个 MDP 均会有一个确定的最优策略,如果已知 $q_* \left(s, a\right)$ 即可知晓最优策略。

最优状态价值函数循环依赖于贝尔曼最优方程:

$$ v_{*}(s)=\max _{a} q_{*}(s, a) $$

$$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$

$$ v_{*}(s)=\max _{a} \mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$

$$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \max _{a^{\prime}} q_{*}\left(s^{\prime}, a^{\prime}\right) $$

显式求解贝尔曼最优方程给出了找到一个最优策略的方法,但这种解法至少依赖于三条实际情况很难满足的假设:

  1. 准确地知道环境的动态变化特性
  2. 有足够的计算资源来求解
  3. 马尔可夫性质

尤其是假设 2 很难满足,现实问题中状态的数量一般很大,即使利用最快的计算机也需要花费难以接受的时间才能求解完成。


  1. Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎

  2. CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎

  3. UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

多臂赌博机 (Multi-armed Bandit)

2020-05-16 08:00:00

本文为《强化学习系列》文章
本文内容主要参考自《强化学习》1

多臂赌博机问题

一个赌徒,要去摇老虎机,走进赌场一看,一排老虎机,外表一模一样,但是每个老虎机吐钱的概率可不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是多臂赌博机问题 (Multi-armed bandit problem, K- or N-armed bandit problem, MAB) 2

图片来源:http://hagencartoons.com/cartoons_166_170.html

$k$ 臂赌博机问题中,$k$ 个动作的每一个在被选择时都有一个期望或者平均收益,称之为这个动作的**“价值”**。令 $t$ 时刻选择的动作为 $A_t$,对应的收益为 $R_t$,任一动作 $a$ 对应的价值为 $q_* \left(a\right)$,即给定动作 $a$ 时收益的期望:

$$ q_* \left(a\right) = \mathbb{E} \left[R_t | A_t = a\right] $$

我们将对动作 $a$ 在时刻 $t$ 的价值的估计记做 $Q_t \left(a\right)$,我们希望它接近 $q_* \left(a\right)$

如果持续对动作的价值进行估计,那么在任一时刻都会至少有一个动作的估计价值是最高的,将这些对应最高估计价值的动作成为贪心的动作。当从这些动作中选择时,称此为开发当前所知道的关于动作的价值的知识。如果不是如此,而是选择非贪心的动作,称此为试探,因为这可以让你改善对非贪心动作的价值的估计。“开发”对于最大化当前这一时刻的期望收益是正确的做法,但是“试探”从长远来看可能会带来总体收益的最大化。到底选择“试探”还是“开发”一种复杂的方式依赖于我们得到的函数估计、不确定性和剩余时刻的精确数值。

动作价值估计方法

我们以一种自然的方式,就是通过计算实际收益的平均值来估计动作的价值:

$$ \begin{aligned} Q_t \left(a\right) &= \dfrac{t \text{ 时刻前执行动作 } a \text{ 得到的收益总和 }}{t \text{ 时刻前执行动作 } a \text{ 的次数}} \\ &= \dfrac{\sum_{i=1}^{t-1}{R_i \cdot \mathbb{1}_{A_i = a}}}{\sum_{i=1}^{t-1}{\mathbb{1}_{A_i = a}}} \end{aligned} $$

其中,$\mathbb{1}_{\text{predicate}}$ 表示随机变量,当 predicate 为真时其值为 1,反之为 0。当分母为 0 时,$Q_t \left(a\right) = 0$,当分母趋向无穷大时,根据大数定律,$Q_t \left(a\right)$ 会收敛到 $q_* \left(a\right)$。这种估计动作价值的方法称为采样平均方法,因为每一次估计都是对相关收益样本的平均。

当然,这只是估计动作价值的一种方法,而且不一定是最好的方法。例如,我们也可以利用累积遗憾(Regret)来评估动作的价值:

$$ \rho = T \mu^* - \sum_{t=1}^{T} \hat{r}_t $$

其中,$\mu^* = \mathop{\max}_{k} \left\{\mu_k\right\}$ 为最大的回报,$\hat{r}_t$$t$ 时刻的回报。

多臂赌博机算法

以 10 臂赌博机为例,动作的收益分布如下图所示:

动作的真实价值 $q_* \left(a\right), a = 1, \cdots, 10$ 为从一个均值为 0 方差为 1 的标准正态分布中选择。当对于该问题的学习方法在 $t$ 时刻选择 $A_t$ 时,实际的收益 $R_t$ 则由一个均值为 $q_* \left(A_t\right)$ 方差为 1 的正态分布决定。

$\epsilon$-Greedy

$\epsilon$-Greedy 采用的动作选择逻辑如下:

下图分别展示了 $\epsilon = 0$(贪婪),$\epsilon = 0.01$$\epsilon = 0.1$ 三种情况下的平均收益和最优动作占比随训练步数的变化情况。

$\epsilon$-Greedy 相比于 $\epsilon = 0$(贪婪)算法的优势如下:

$R_i$ 表示一个动作被选择 $i$ 次后获得的收益,$Q_n$ 表示被选择 $n - 1$ 次后它的估计的动作价值,其可以表示为增量计算的形式:

$$ \begin{aligned} Q_{n+1} &= \dfrac{1}{n} \sum_{i=1}^{n}{R_i} \\ &= \dfrac{1}{n} \left(R_n + \sum_{i=1}^{n-1}{R_i}\right) \\ &= \dfrac{1}{n} \left(R_n + \left(n - 1\right) \dfrac{1}{n-1} \sum_{i=1}^{n-1}{R_i}\right) \\ &= \dfrac{1}{n} \left(R_n + \left(n - 1\right) Q_n\right) \\ &= \dfrac{1}{n} \left(R_n + n Q_n - Q_n\right) \\ &= Q_n + \dfrac{1}{n} \left[R_n - Q_n\right] \end{aligned} $$

上述我们讨论的都是平稳的问题,即收益的概率分布不随着时间变化的赌博机问题。对于非平稳的问题,给近期的收益赋予比过去更高的权值是一个合理的处理方式。则收益均值 $Q_n$ 的增量更新规则为:

$$ \begin{aligned} Q_{n+1} &= Q_n + \alpha \left[R_n - Q_n\right] \\ &= \left(1 - \alpha\right)^n Q_1 + \sum_{i=1}^{n} \alpha \left(1 - \alpha\right)^{n-i} R_i \end{aligned} $$

赋给收益 $R_i$ 的权值 $\alpha \left(1 - \alpha\right)^{n-i}$ 依赖于它被观测到的具体时刻和当前时刻的差,权值以指数形式递减,因此这个方法也称之为指数近因加权平均

上述讨论中所有方法都在一定程度上依赖于初始动作值 $Q_1 \left(a\right)$ 的选择。从统计学角度,初始估计值是有偏的,对于平均采样来说,当所有动作都至少被选择一次时,偏差会消失;对于步长为常数的情况,偏差会随时间而减小。

下图展示了不同初始动作估计值下最优动作占比随训练步数的变化情况:

设置较大初始动作估计值会鼓励进行试探,这种方法称之为乐观初始价值,该方法在平稳问题中非常有效。

UCB

$\epsilon$-Greedy 在进行尝试时是盲目地选择,因为它不大会选择接近贪心或者不确定性特别大的动作。在非贪心动作中,最好是根据它们的潜力来选择可能事实上是最优的动作,这要考虑它们的估计有多接近最大值,以及这些估计的不确定性。

一种基于置信度上界(Upper Confidence Bound,UCB)思想的选择动作依据如下:

$$ A_t = \mathop{\arg\max}_{a} \left[Q_t \left(a\right) + c \sqrt{\dfrac{\ln t}{N_t \left(a\right)}}\right] $$

其中,$N_t \left(a\right)$ 表示在时刻 $t$ 之前动作 $a$ 被选择的次数,$c > 0$ 用于控制试探的程度。平方根项是对 $a$ 动作值估计的不确定性或方差的度量,最大值的大小是动作 $a$ 的可能真实值的上限,参数 $c$ 决定了置信水平。

下图展示了 UCB 算法和 $\epsilon$-Greedy 算法平均收益随着训练步数的变化:

梯度赌博机算法

针对每个动作 $a$,考虑学习一个数值化的偏好函数 $H_t \left(a\right)$,偏好函数越大,动作就约频繁地被选择,但偏好函数的概念并不是从“收益”的意义上提出的。基于随机梯度上升的思想,在每个步骤中,在选择动作 $A_t$ 并获得收益 $R_t$ 之后,偏好函数会按如下方式更新:

$$ \begin{aligned} H_{t+1} \left(A_t\right) &eq H_t \left(A_t\right) + \alpha \left(R_t - \bar{R}_t\right) \left(1 - \pi_t \left(A_t\right)\right) \\ H_{t+1} \left(a\right) &eq H_t \left(a\right) - \alpha \left(R_t - \bar{R}_t\right) \pi_t \left(a\right) \end{aligned} $$

其中,$\alpha > 0$ 表示步长,$\bar{R}_t \in \mathbb{R}$ 表示时刻 $t$ 内所有收益的平均值。$\bar{R}_t$ 项作为比较收益的一个基准项,如果收益高于它,那么在未来选择动作 $A_t$ 的概率就会增加,反之概率就会降低,未选择的动作被选择的概率会上升。

下图展示了在 10 臂测试平台问题的变体上采用梯度赌博机算法的结果,在这个问题中,它们真实的期望收益是按照平均值为 4 而不是 0 的正态分布来选择的。所有收益的这种变化对梯度赌博机算法没有任何影响,因为收益基准项让它可以马上适应新的收益水平,如果没有基准项,那么性能将显著降低。

算法性能比较

$\epsilon$-Greedy 方法在一段时间内进行随机的动作选择;UCB 方法虽然采用确定的动作选择,但可以通过每个时刻对具有较少样本的动作进行优先选择来实现试探;梯度赌博机算法则不估计动作价值,而是利用偏好函数,使用 softmax 分布来以一种分级的、概率式的方式选择更优的动作;简单地将收益的初值进行乐观的设置,可以让贪心方法也能进行显示试探。

下图展示了上述算法在不同参数下的平均收益,每条算法性能曲线都看作一个自己参数的函数。$x$ 轴上参数值的变化是 2 的倍数,并以对数坐标轴进行表示。

在评估方法时,不仅要关注它在最佳参数设置上的表现,还要注意它对参数值的敏感性。总的来说,在本文的问题上,UCB 表现最好。


  1. Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎

  2. https://cosx.org/2017/05/bandit-and-recommender-systems ↩︎

强化学习简介 (Introduction of Reinforcement Learning)

2020-05-09 08:00:00

本文为《强化学习系列》文章

强化学习简介

**强化学习(Reinforcement Learning,RL)**是机器学习中的一个领域,是学习“做什么(即如何把当前的情景映射成动作)才能使得数值化的收益信号最大化”。学习者不会被告知应该采取什么动作,而是必须自己通过尝试去发现哪些动作会产生最丰厚的收益。

强化学习同机器学习领域中的有监督学习无监督学习不同,有监督学习是从外部监督者提供的带标注训练集中进行学习(任务驱动型),无监督学习是一个典型的寻找未标注数据中隐含结构的过程(数据驱动型)。强化学习是与两者并列的第三种机器学习范式,强化学习带来了一个独有的挑战——**“试探”“开发”**之间的折中权衡,智能体必须开发已有的经验来获取收益,同时也要进行试探,使得未来可以获得更好的动作选择空间(即从错误中学习)。

在强化学习中,有两个可以进行交互的对象:智能体(Agnet)环境(Environment)

图片来源:https://en.wikipedia.org/wiki/Reinforcement_learning

除了智能体和环境之外,强化学习系统有四个核心要素:策略(Policy)回报函数(收益信号,Reward Function)价值函数(Value Function)环境模型(Environment Model),其中环境模型是可选的。

强化学习是一种对目标导向的学习与决策问题进行理解和自动化处理的计算方法。它强调智能体通过与环境的直接互动来学习,而不需要可效仿的监督信号或对周围环境的完全建模,因而与其他的计算方法相比具有不同的范式。

强化学习使用马尔可夫决策过程的形式化框架,使用状态动作收益定义学习型智能体与环境的互动过程。这个框架力图简单地表示人工智能问题的若干重要特征,这些特征包含了对因果关系的认知,对不确定性的认知,以及对显式目标存在性的认知。

价值与价值函数是强化学习方法的重要特征,价值函数对于策略空间的有效搜索来说十分重要。相比于进化方法以对完整策略的反复评估为引导对策略空间进行直接搜索,使用价值函数是强化学习方法与进化方法的不同之处。

示例和应用

以经典的 Flappy Bird 游戏为例,智能体就是游戏中我们操作的小鸟,整个游戏中的天空和遮挡管道即为环境,动作为玩家单击屏幕使小鸟飞起的行为,如下图所示:

图片来源:https://easyai.tech/ai-definition/reinforcement-learning

目前,强化学习在包括游戏广告和推荐对话系统机器人等多个领域均展开了广泛的应用。

游戏

AlphaGo 1 是于 2014 年开始由英国伦敦 Google DeepMind 开发的人工智能围棋软件。AlphaGo 使用蒙特卡洛树搜索(Monte Carlo tree search),借助估值网络(value network)与走棋网络(policy network)这两种深度神经网络,通过估值网络来评估大量选点,并通过走棋网络选择落点。

AlphaStar 2 3 是由 DeepMind 开发的玩 星际争霸 II 游戏的人工智能程序。AlphaStar 是由一个深度神经网路生成的,它接收来自原始游戏界面的输入数据,并输出一系列指令,构成游戏中的一个动作。

更具体地说,神经网路体系结构将 Transformer 框架运用于模型单元(类似于关系深度强化学习),结合一个深度 LSTM 核心、一个带有 pointer network 的自回归策略前端和一个集中的值基线。这种先进的模型将有助于解决机器学习研究中涉及长期序列建模和大输出空间(如翻译、语言建模和视觉表示)的许多其他挑战。

AlphaStar 还使用了一种新的多智能体学习算法。该神经网路最初是通过在 Blizzard 发布的匿名人类游戏中进行监督学习来训练的。这使得 AlphaStar 能够通过模仿学习星际争霸上玩家所使用的基本微观和宏观策略。这个初级智能体在 95% 的游戏中击败了内置的「精英」AI 关卡(相当于人类玩家的黄金级别)。

OpenAI Five 4 是一个由 OpenAI 开发的用于多人视频游戏 Dota 2 的人工智能程序。OpenAI Five 通过与自己进行超过 10,000 年时长的游戏进行优化学习,最终获得了专家级别的表现。

Pluribus 5 是由 Facebook 开发的第一个在六人无限注德州扑克中击败人类专家的 AI 智能程序,其首次在复杂游戏中击败两个人或两个团队。

广告和推荐

图片来源:A Reinforcement Learning Framework for Explainable Recommendation

对话系统

图片来源:End-to-End Task-Completion Neural Dialogue Systems

机器人

图片来源:Learning Synergies between Pushing and Grasping with Self-supervised Deep Reinforcement Learning

开放资源

开源实验平台

开源框架

开源模型

其他资源

在群晖 NAS 上编译安装 tmux

2020-05-07 08:00:00

工具链安装

登录 NAS 控制台,在系统根目录创建 toolkit 目录:

sudo mkdir /toolkit
sudo chown -R username:users /toolkit

其中 username 为使用的用户名,如果后续使用过程中出现磁盘空间不足的问题,可以在其他具有较大容量的分区建立 toolkit,再在根目录建立软链进行使用:

mkdir /xxx/toolkit
sudo ln -s /xxx/toolkit /toolkit
sudo chown -R username:users /toolkit

之后下载相关工具脚本:

cd /toolkit
git clone https://github.com/SynologyOpenSource/pkgscripts-ng.git

工具脚本使用 Python 3 实现,请确保 NAS 已经安装 Python 3,后续使用过程中如果提示相关 Python 扩展包未安装的情况请自行安装后重试。实验的 Synology NAS 为 DS418play,系统版本为 DSM 6.2.2 系统,处理器为 INTEL Celeron J3355 处理器(产品代号:Apollo Lake),首先利用 EnvDeploy 下载所需的编译环境:

cd /toolkit/pkgscripts-ng
sudo ./EnvDeploy -v 6.2 -p apollolake

请根据自己机器的系统版本和处理器类型自行调整 -v-p 参数。如果下载速度较慢可以手动从 https://sourceforge.net/projects/dsgpl/files/toolkit/DSM6.2/ 下载下列文件:

base_env-6.2.txz
ds.apollolake-6.2.dev.txz
ds.apollolake-6.2.env.txz

将其放置到 /toolkit/toolkit_tarballs 目录中,然后通过如下命令进行部署安装:

sudo ./EnvDeploy -v 6.2 -p apollolake -t /toolkit/toolkit_tarballs

编译 tmux

/toolkit 目录下建立 source 文件夹,并将 tmux 源代码(本文以 3.1b 版本为例)下载到该文件夹中:

cd /toolkit
mkdir source
cd source
wget https://github.com/tmux/tmux/releases/download/3.1b/tmux-3.1b.tar.gz
tar -zxvf tmux-3.1b.tar.gz
mv tmux-3.1b tmux

在 tmux 源代码根目录中建立 SynoBuildConf 文件夹,并在文件夹中创建如下文件:

cd /toolkit/source/tmux
mkdir SynoBuildConf

build

#!/bin/bash

case ${MakeClean} in
	[Yy][Ee][Ss])
		make distclean
		;;
esac

NCURSES_INCS="$(pkg-config ncurses --cflags)"
NCURSES_LIBS="$(pkg-config ncurses --libs)"

CFLAGS="${CFLAGS} ${NCURSES_INCS}"
LDFLAGS="${LDFLAGS} ${NCURSES_LIBS}"

env CC="${CC}" AR="${AR}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" \
./configure ${ConfigOpt}

make ${MAKE_FLAGS}

depends

[default]
all="6.2"

install

#!/bin/bash

PKG_NAME="tmux"
TGZ_DIR="/tmp/_${PKG_NAME}_tgz"
PKG_DIR="/tmp/_${PKG_NAME}_pkg"
PKG_DEST="/image/packages"

source /pkgscripts-ng/include/pkg_util.sh

create_package_tgz() {
	## clear destination directory
	for dir in $TGZ_DIR $PKG_DIR; do
		rm -rf "$dir"
	done
	for dir in $TGZ_DIR $PKG_DIR; do
		mkdir -p "$dir"
	done

	## install needed file into TGZ_DIR
	DESTDIR="${TGZ_DIR}" make install

	## create package.tgz
	pkg_make_package $TGZ_DIR $PKG_DIR
}

create_package_spk(){
	## Copy package center scripts to PKG_DIR
	cp -r synology/scripts/ $PKG_DIR

	## Copy package icon
	cp -av synology/PACKAGE_ICON*.PNG $PKG_DIR

	## Generate INFO file
	synology/INFO.sh > INFO
	cp INFO $PKG_DIR

	## Create the final spk.
	mkdir -p $PKG_DEST
	pkg_make_spk $PKG_DIR $PKG_DEST
}

main() {
	create_package_tgz
	create_package_spk
}

main "$@"

在 tmux 源代码根目录中建立 synology 文件夹,并在文件夹中创建如下文件:

cd /toolkit/source/tmux
mkdir synology

INFO.sh

#!/bin/sh

. /pkgscripts-ng/include/pkg_util.sh

package="tmux"
version="3.1b"
displayname="tmux"
arch="$(pkg_get_platform) "
maintainer="tmux"
maintainer_url="https://github.com/tmux"
distributor="Leo Van"
distributor_url="https://leovan.me"
description="tmux is a terminal multiplexer: it enables a number of terminals to be created, accessed, and controlled from a single screen. tmux may be detached from a screen and continue running in the background, then later reattached."
support_url="https://github.com/tmux/tmux"
thirdparty="yes"
startable="no"
silent_install="yes"
silent_upgrade="yes"
silent_uninstall="yes"

[ "$(caller)" != "0 NULL" ] && return 0

pkg_dump_info

并为其添加运行权限:

cd /toolkit/source/tmux/scripts
chmod u+x INFO.sh

下载 tmux 图标并将其重命名:

cd /toolkit/source/tmux/synology
wget https://raw.githubusercontent.com/tmux/tmux/master/logo/tmux-logo-huge.png
convert tmux-logo-huge.png -crop 480x480+0+0 -resize 72x PACKAGE_ICON.PNG
convert tmux-logo-huge.png -crop 480x480+0+0 -resize 256x PACKAGE_ICON_256.PNG

此处需要使用 ImageMagick 对图标进行裁剪和缩放,请自行安装,或在本地对图片进行处理后上传到指定目录。在 /toolkit/source/tmux/synology 目录中建立 scripts 文件夹,并在文件夹中创建如下文件:

cd /toolkit/source/tmux/synology
mkdir scripts

postinst

#!/bin/sh

ln -sf "$SYNOPKG_PKGDEST/usr/local/bin/tmux" /usr/bin/

postuninst

#!/bin/sh

rm -f /usr/local/bin/tmux
rm -f /usr/bin/tmux

postupgrade

#!/bin/sh

exit 0

preinst

#!/bin/sh

exit 0

preuninst

#!/bin/sh

exit 0

preupgrade

#!/bin/sh

exit 0

start-stop-status

#!/bin/sh

case $1 in
	start)
		exit 0
	;;
	stop)
		exit 0
	;;
	status)
		if [ -h "/usr/bin/tmux" ]; then
			exit 0
		else
			exit 1
		fi
	;;
	killall)
        ;;
	log)
		exit 0
	;;
esac

为所有文件添加运行权限:

cd /toolkit/source/tmux/synology/scripts
chmod u+x *

利用 PkgCreate.py 构建 tmux 扩展包:

sudo ./PkgCreate.py -v 6.2 -p apollolake tmux

最终构建完毕的扩展包位于 /toolkit/build_env/ds.apollolake-6.2/image/packages 中。

安装 tmux

/toolkit/build_env/ds.apollolake-6.2/image/packages 目录中有两个编译好的扩展包,分别是 tmux-apollolake-3.1b_debug.spktmux-apollolake-3.1b.spk。其中 tmux-apollolake-3.1b.spk 为 Release 版本,传输到本地,通过 NAS 的套件中心手动安装即可。安装完毕后,套件中心的“已安装”会出现 tmux,如下图所示:

进入 NAS 控制台,运行 tmux -V 可以得到安装好的 tmux 版本信息:

tmux 3.1b

在此放出编译好的 tmux 扩展包,方便和 DS418play 具有相同系统的 CPU 架构的小伙伴直接使用。

本文主要参考了 Synology 官方的扩展包构建指南:https://help.synology.com/developer-guide/create_package/index.html

隐马尔可夫 (Hidden Markov Model, HMM),条件随机场 (Conditional Random Fields, CRF) 和序列标注 (Sequence Labeling)

2020-05-02 08:00:00

隐马尔可夫

隐马尔可夫模型(Hidden Markov Model,HMM)是一个描述包含隐含未知参数的马尔可夫过程的统计模型。马尔可夫过程(Markov Process)是因俄国数学家安德雷·安德耶维齐·马尔可夫(Андрей Андреевич Марков)而得名一个随机过程,在该随机过程中,给定当前状态和过去所有状态的条件下,其下一个状态的条件概率分布仅依赖于当前状态,通常具备离散状态的马尔可夫过程称之为马尔可夫链(Markov Chain)。因此,马尔可夫链可以理解为一个有限状态机,给定了当前状态为 $S_i$ 时,下一时刻状态为 $S_j$ 的概率,不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 $S_a, S_b, S_c$ 之间转换状态的马尔可夫链。

隐马尔可夫模型中包含两种序列:随机生成的状态构成的序列称之为状态序列(state sequence),状态序列是不可被观测到的;每个状态对应的观测值组成的序列称之为观测序列(observation sequence)。令 $I = \left(i_1, i_2, \cdots, i_T\right)$ 为状态序列,其中 $i_t$ 为第 $t$ 时刻系统的状态值,对应的有 $O = \left(o_1, o_2, \cdots, o_T\right)$ 为观测序列,其中 $o_t$ 为第 $t$ 时刻系统的观测值,系统的所有可能的状态集合为 $Q = \{q_1, q_2, \cdots, q_N\}$,所有可能的观测集合为 $V= \{v_1, v_2, \cdots, v_M\}$

隐马尔可夫模型主要由三组参数构成:

  1. 状态转移矩阵: $$ A = \left[a_{ij}\right]_{N \times N} $$ 其中, $$ a_{ij} = P \left(i_{t+1} = q_j | i_t = q_i\right), 1 \leq i, j \leq N $$ 表示 $t$ 时刻状态为 $q_i$ 的情况下,在 $t+1$ 时刻状态转移到 $q_j$ 的概率。
  2. 观测概率矩阵: $$ B = \left[b_j \left(k\right)\right]_{N \times M} $$ 其中, $$ b_j \left(k\right) = P \left(o_t = v_k | i_t = q_j\right), k = 1, 2, \cdots, M, j = 1, 2, \cdots, N $$ 表示 $t$ 时刻状态为 $q_i$ 的情况下,观测值为 $v_k$ 的概率。
  3. 初始状态概率向量: $$ \pi = \left(\pi_i\right) $$ 其中, $$ \pi_i = P \left(i_1 = q_i\right), i = 1, 2, \cdots, N $$ 表示 $t = 1$ 时刻,系统处于状态 $q_i$ 的概率。

初始状态概率向量 $\pi$ 和状态转移矩阵 $A$ 决定了状态序列,观测概率矩阵 $B$ 决定了状态序列对应的观测序列,因此马尔可夫模型可以表示为:

$$ \lambda = \left(A, B, \pi\right) $$

对于马尔可夫模型 $\lambda = \left(A, B, \pi\right)$,通过如下步骤生成观测序列 $\{o_1, o_2, \cdots, o_T\}$

  1. 按照初始状态分布 $\pi$ 产生状态 $i_1$.
  2. $t = 1$
  3. 按照状态 $i_t$ 的观测概率分布 $b_{i_t} \left(k\right)$ 生成 $o_t$
  4. 按照状态 $i_t$ 的状态转移概率分布 $\left\{a_{i_t i_{t+1}}\right\}$ 产生状态 $i_{t+1}$$i_{t+1} = 1, 2, \cdots, N$
  5. $t = t + 1$,如果 $t < T$,转步骤 3;否则,终止。

马尔可夫模型在应用过程中有 3 个基本问题 1

  1. 概率计算问题。给定模型 $\lambda = \left(A, B, \pi\right)$ 和观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,计算在模型 $\lambda$ 下观测序列 $O$ 出现的概率 $P\left(O | \lambda \right)$
  2. 学习问题。已知观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,估计模型 $\lambda = \left(A, B, \pi\right)$ 参数,使得在该模型下观测序列概率 $P\left(X | \lambda \right)$ 最大。即用极大似然估计的方法估计参数。
  3. 预测问题,也称为解码(decoding)问题。已知模型 $\lambda = \left(A, B, \pi\right)$ 和观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,求对给定观测序列条件概率 $P \left(I | O\right)$ 最大的状态序列 $I = \{i_1, i_2, \cdots, i_T\}$。即给定观测序列,求最有可能的对应的状态序列。

概率计算

直接计算法

给定模型 $\lambda = \left(A, B, \pi \right)$ 和观测序列 $O = \{o_1, o_2, ..., o_T\}$,计算在模型 $\lambda$ 下观测序列 $O$ 出现的概率 $P\left(O | \lambda \right)$。最简单的办法就是列举出左右可能的状态序列 $I = \{i_1, i_2, ..., i_T\}$,再根据观测概率矩阵 $B$,计算每种状态序列对应的联合概率 $P \left(O, I | \lambda\right)$,对其进行求和得到概率 $P\left(O | \lambda \right)$

状态序列 $I = \{i_1, i_2, ..., i_T\}$ 的概率是:

$$ P \left(I | \lambda \right) = \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} $$

对于固定的状态序列 $I = \{i_1, i_2, ..., i_T\}$,观测序列 $O = \{o_1, o_2, ..., o_T\}$ 的概率是:

$$ P \left(O | I, \lambda \right) = \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) $$

$O$$I$ 同时出现的联合概率为:

$$ \begin{split} P \left(O, I | \lambda \right) &= P \left(O | I, \lambda \right) P \left(I | \lambda \right) \\ &= \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$

然后,对于所有可能的状态序列 $I$ 求和,得到观测序列 $O$ 的概率 $P \left(O | \lambda\right)$,即:

$$ \begin{split} P\left(O | \lambda \right) &= \sum_{I} P \left(O | I, \lambda \right) P \left(I | \lambda \right) \\ &= \sum_{i_1, i_2, \cdots, i_T} \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$

但利用上式的计算量很大,是 $O \left(T N^T\right)$ 阶的,这种算法不可行。

前向算法

前向概率:给定马尔可夫模型 $\lambda$,给定到时刻 $t$ 部分观测序列为 $o_1, o_2, \cdots, o_t$ 且状态为 $q_i$ 的概率为前向概率,记作:

$$ \alpha_t \left(i\right) = P \left(o_1, o_2, \cdots, o_t, i_t = q_i | \lambda\right) $$

可以递推地求得前向概率 $\alpha_t \left(i\right)$ 及观测序列概率 $P \left(O | \lambda\right)$,前向算法如下:

  1. 初值 $$ \alpha_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N $$
  2. 递推,对 $t = 1, 2, \cdots, T-1$ $$ \alpha_{t+1}(i)=\left[\sum_{j=1}^{N} \alpha_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N $$
  3. 终止 $$ P(O | \lambda)=\sum_{i=1}^{N} \alpha_{T}(i) $$

后向算法

后向概率:给定隐马尔可夫模型 $\lambda$,给定在时刻 $t$ 状态为 $q_i$ 的条件下,从 $t+1$$T$ 的部分观测序列为 $o_{t+1}, o_{t+2}, \cdots, o_T$ 的概率为后向概率,记作:

$$ \beta_{t}(i)=P\left(o_{t+1}, o_{t+2}, \cdots, o_{T} | i_{t}=q_{i}, \lambda\right) $$

可以递推地求得后向概率 $\alpha_t \left(i\right)$ 及观测序列概率 $P \left(O | \lambda\right)$,后向算法如下:

  1. 初值 $$ \beta_{T}(i)=1, \quad i=1,2, \cdots, N $$
  2. 递推,对 $t = T-1, T-2, \cdots, 1$ $$ \beta_{t}(i)=\sum_{j=1}^{N} a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j), \quad i=1,2, \cdots, N $$
  3. 终止 $$ P(O | \lambda)=\sum_{i=1}^{N} \pi_{i} b_{i}\left(o_{1}\right) \beta_{1}(i) $$

学习算法

监督学习算法

假设以给训练数据包含 $S$ 个长度相同的观测序列和对应的状态序列 $\left\{\left(O_1, I_1\right), \left(O_2, I_2\right), \cdots, \left(O_S, I_S\right)\right\}$,那么可以利用极大似然估计法来估计隐马尔可夫模型的参数。

设样本中时刻 $t$ 处于状态 $i$ 时刻 $t+1$ 转移到状态 $j$ 的频数为 $A_{ij}$,那么转移概率 $a_{ij}$ 的估计是:

$$ \hat{a}_{i j}=\frac{A_{i j}}{\sum_{j=1}^{N} A_{i j}}, \quad i=1,2, \cdots, N ; \quad j=1,2, \cdots, N $$

设样本中状态为 $j$ 并观测为 $k$ 的频数是 $B_{jk}$,那么状态为 $j$ 观测为 $k$ 的概率 $b_j \left(k\right)$ 的估计是:

$$ \hat{b}_{j}(k)=\frac{B_{j k}}{\sum_{k=1}^{M} B_{j k}}, \quad j=1,2, \cdots, N ; \quad k=1,2, \cdots, M $$

初始状态概率 $\pi_i$ 的估计 $\hat{\pi}_i$$S$ 个样本中初始状态为 $q_i$ 的频率。

无监督学习算法

假设给定训练数据值包含 $S$ 个长度为 $T$ 的观测序列 $\left\{O_1, O_2, \cdots, O_S\right\}$ 而没有对应的状态序例,目标是学习隐马尔可夫模型 $\lambda = \left(A, B, \pi\right)$ 的参数。我们将观测序列数据看做观测数据 $O$,状态序列数据看作不可观测的隐数据 $I$,那么马尔可夫模型事实上是一个含有隐变量的概率模型:

$$ P(O | \lambda)=\sum_{I} P(O | I, \lambda) P(I | \lambda) $$

它的参数学习可以由 EM 算法实现。EM 算法在隐马尔可夫模型学习中的具体实现为 Baum-Welch 算法:

  1. 初始化。对 $n = 0$,选取 $a_{i j}^{(0)}, b_{j}(k)^{(0)}, \pi_{i}^{(0)}$,得到模型 $\lambda^{(0)}=\left(A^{(0)}, B^{(0)}, \pi^{(0)}\right)$
  2. 递推。对 $n = 1, 2, \cdots$$$ \begin{aligned} a_{i j}^{(n+1)} &= \frac{\sum_{t=1}^{T-1} \xi_{t}(i, j)}{\sum_{t=1}^{T-1} \gamma_{t}(i)} \\ b_{j}(k)^{(n+1)} &= \frac{\sum_{t=1, o_{t}=v_{k}}^{T} \gamma_{t}(j)}{\sum_{t=1}^{T} \gamma_{t}(j)} \\ \pi_{i}^{(n+1)} &= \gamma_{1}(i) \end{aligned} $$ 右端各按照观测 $O=\left(o_{1}, o_{2}, \cdots, o_{T}\right)$ 和模型 $\lambda^{(n)}=\left(A^{(n)}, B^{(n)}, \pi^{(n)}\right)$ 计算, $$ \begin{aligned} \gamma_{t}(i) &= \frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} \\ \xi_{t}(i, j) &= \frac{\alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)}{\sum_{i=1}^{N} \sum_{j=1}^{N} \alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)} \end{aligned} $$
  3. 终止。得到模型参数 $\lambda^{(n+1)}=\left(A^{(n+1)}, B^{(n+1)}, \pi^{(n+1)}\right)$

预测算法

近似算法

近似算法的思想是,在每个时刻 $t$ 选择在该时刻最有可能出现的状态 $i_t^*$,从而得到一个状态序列 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$,将它作为预测的结果。给定隐马尔可夫模型 $\lambda$ 和观测序列 $O$,在时刻 $t$ 处于状态 $q_i$ 的概率 $\gamma_t \left(i\right)$ 是:

$$ \gamma_{t}(i)=\frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} $$

在每一时刻 $t$ 最有可能的状态 $i_t^*$ 是:

$$ i_{t}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\gamma_{t}(i)\right], \quad t=1,2, \cdots, T $$

从而得到状态序列 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$

近似算法的优点是计算简单,其缺点是不能保证预测的状态序列整体是最有可能的状态序列,因为预测的状态序列可能有实际不发生的部分。事实上,上述方法得到的状态序列中有可能存在转移概率为0的相邻状态,即对某些 $i, j, a_{ij} = 0$ 。尽管如此,近似算法仍然是有用的。

维特比算法

维特比算法(Viterbi Algorithm)实际是用动态规划(Dynamic Programming)解隐马尔可夫模型预测问题,即用动态规划求概率最大路径(最优路径)。这时一条路径对应着一个状态序列。

首先导入两个变量 $\sigma$$\Psi$。定义在时刻 $t$ 状态为 $i$ 的所有单个路径 $\left(i_1, i_2, \cdots, i_t\right)$ 中概率最大值为:

$$ \delta_{t}(i)=\max _{i_{1}, i_{2}, \cdots, i_{t-1}} P\left(i_{t}=i, i_{t-1}, \cdots, i_{1}, o_{t}, \cdots, o_{1} | \lambda\right), \quad i=1,2, \cdots, N $$

由定义可得变量 $\sigma$ 的递推公式:

$$ \begin{aligned} \delta_{t+1}(i) &=\max _{i_{1}, i_{2}, \cdots, i_{t}} P\left(i_{t+1}=i, i_{t}, \cdots, i_{1}, o_{t+1}, \cdots, o_{1} | \lambda\right) \\ &=\max _{1 \leqslant j \leqslant N}\left[\delta_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N ; \quad t=1,2, \cdots, T-1 \end{aligned} $$

定义在时刻 $t$ 状态为 $i$ 的所有单个路径 $\left(i_1, i_2, \cdots, i_{t-1}, i\right)$ 中概率最大的路径的第 $t - 1$ 个结点为:

$$ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N $$

维特比算法流程如下:

  1. 初始化 $$ \begin{array}{c} \delta_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N \\ \Psi_{1}(i)=0, \quad i=1,2, \cdots, N \end{array} $$
  2. 递推。对 $t = 2, 3, \cdots, T$ $$ \begin{array}{c} \delta_{t}(i)=\max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right] b_{i}\left(o_{t}\right), \quad i=1,2, \cdots, N \\ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N \end{array} $$
  3. 终止。 $$ \begin{array}{c} P^{*}=\max _{1 \leqslant i \leqslant N} \delta_{T}(i) \\ i_{T}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\delta_{T}(i)\right] \end{array} $$
  4. 最优路径回溯。对 $t = T - 1, T - 2, \cdots, 1$ $$ i_{t}^{*}=\Psi_{t+1}\left(i_{t+1}^{*}\right) $$

求的最优路径 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$

条件随机场

概率无向图模型(Probabilistic Undirected Graphical Model)又称为马尔可夫随机场(Markov Random Field),是一个可以由无向图表示的联合概率分布。概率图模型(Probabilistic Graphical Model)是由图表示的概率分布,设有联合概率分布 $P \left(Y\right), Y \in \mathcal{Y}$ 是一组随机变量。由无向图 $G = \left(V, E\right)$ 表示概率分布 $P \left(Y\right)$,即在图 $G$ 中,结点 $v \in V$ 表示一个随机变量 $Y_v, Y = \left(Y_v\right)_{v \in V}$,边 $e \in E$ 表示随机变量之间的概率依赖关系。

成对马尔可夫性:设 $u$$v$ 是无向图 $G$ 中任意两个没有边连接的结点,结点 $u$$v$ 分别对应随机变量 $Y_u$$Y_v$。其他所有结点为 $O$,对应的随机变量组是 $Y_O$。成对马尔可夫是指给定随机变量组 $Y_O$ 的条件下随机变量 $Y_u$$Y_v$ 是条件独立的,即:

$$ P\left(Y_{u}, Y_{v} | Y_{O}\right)=P\left(Y_{u} | Y_{O}\right) P\left(Y_{v} | Y_{O}\right) $$

局部马尔可夫性:设 $v \in V$ 是无向图 $G$ 中任意一个结点,$W$ 是与 $v$ 有边连接的所有结点,$O$$v$$W$ 以外的其他所有结点。$v$ 表示的随机变量是 $Y_v$$W$ 表示的随机变量组是 $Y_W$$O$ 表示的随机变量组是 $Y_O$。局部马尔可夫性是指在给定随机变量组 $Y_W$ 的条件下随机变量 $Y_v$ 与随机变量组 $Y_O$ 是独立的,即:

$$ P\left(Y_{v}, Y_{O} | Y_{W}\right)=P\left(Y_{v} | Y_{W}\right) P\left(Y_{O} | Y_{W}\right) $$

$P \left(Y_O | Y_W\right) > 0$ 时,等价地:

$$ P\left(Y_{v} | Y_{W}\right)=P\left(Y_{v} | Y_{W}, Y_{O}\right) $$

局部马尔可夫性如下图所示:

全局马尔可夫性:设结点结合 $A, B$ 是在无向图 $G$ 中被结点集合 $C$ 分开的任意结点集合,如下图所示。结点集合 $A, B$$C$ 所对应的随机变量组分别是 $Y_A, Y_B$$Y_C$。全局马尔可夫性是指给定随机变量组 $Y_C$ 条件下随机变量组 $Y_A$$Y_B$ 是条件独立的,即:

$$ P\left(Y_{A}, Y_{B} | Y_{C}\right)=P\left(Y_{A} | Y_{C}\right) P\left(Y_{B} | Y_{C}\right) $$

概率无向图模型定义为:设有联合概率分布 $P \left(Y\right)$,由无向图 $G = \left(V, E\right)$ 表示,在图 $G$ 中,结点表示随机变量,边表示随机变量之间的依赖关系。如果联合概率分布 $P \left(Y\right)$ 满足成对、局部或全局马尔可夫性,就称此联合概率分布为概率无向图模型(Probabilistic Undirected Graphical Model),或马尔可夫随机场(Markov Random Field)。

团与最大团:无向图 $G$ 中任何两个结点均有边连接的结点子集称为团(Clique)。若 $C$ 是无向图 $G$ 的一个团,并且不能再加进任何一个 $G$ 的结点时期成为一个更大的团,则称此 $C$ 为最大团(Maximal Clique)。

无向图的团和最大团

上图表示 4 个结点组成的无向图。图中有 2 个结点组成的团有 5 个:$\left\{Y_1, Y_2\right\}$$\left\{Y_2, Y_3\right\}$$\left\{Y_3, Y_4\right\}$$\left\{Y_4, Y_2\right\}$$\left\{Y_1, Y_3\right\}$。有 2 个最大团:$\left\{Y_1, Y_2, Y_3\right\}$$\left\{Y_2, Y_3, Y_4\right\}$。而 $\left\{Y_1, Y_2, Y_3, Y_4\right\}$ 不是一个团,因为 $Y_1$$Y_4$ 没有边连接。

将概率无向图模型的联合概率分布表示为其最大团上的随机变量的函数的乘积形式的操作,称为概率无向图模型的因子分解。给定无向图模型,设其无向图为 $G$$C$$G$ 上的最大团,$Y_C$ 表示 $C$ 对应的随机变量。那么概率无向图模型的联合概率分布 $P \left(Y\right)$ 可以写作图中所有最大团 $C$ 上的函数 $\Psi_C \left(Y_C\right)$ 的乘积形式,即:

$$ P(Y)=\frac{1}{Z} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$

其中,$Z$ 是规范化因子:

$$ Z=\sum_{Y} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$

规范化因子保证 $P \left(Y\right)$ 构成一个概率分布。函数 $\Psi_C \left(Y_C\right)$ 称为势函数,这里要求势函数 $\Psi_C \left(Y_C\right)$ 是严格正的,通常定义为指数函数:

$$ \Psi_{C}\left(Y_{C}\right)=\exp \left\{-E\left(Y_{C}\right)\right\} $$

概率无向图模型的因子分解由这个 Hammersley-Clifford 定理来保证。

条件随机场(Conditional Random Field)是给定随机变量 $X$ 条件下,随机变量 $Y$ 的马尔可夫随机场。设 $X$$Y$ 是随机变量,$P \left(Y | X\right)$ 是给定 $X$ 的条件下 $Y$ 的条件概率分布。若随机变量 $Y$ 构成一个有无向图 $G = \left(V, E\right)$ 表示的马尔可夫随机场,即:

$$ P\left(Y_{v} | X, Y_{w}, w \neq v\right)=P\left(Y_{v} | X, Y_{w}, w \sim v\right) $$

对任意结点 $v$ 成立,则称条件概率分布 $P \left(Y | X\right)$ 为条件随机场。其中,$w \sim v$ 表示在图 $G = \left(V, E\right)$ 中与结点 $v$ 有边连接的所有结点 $w$$w \neq v$ 表示结点 $v$ 以外的所有结点,$Y_v, Y_u$$Y_w$ 为结点 $v, u$$w$ 对应的随机变量。

定义中并没有要求 $X$$Y$ 具有相同的结构,一般假设 $X$$Y$ 有相同的图结构,下图展示了无向图的线性链情况,即:

$$ G=(V=\{1,2, \cdots, n\}, E=\{(i, i+1)\}), \quad i=1,2, \cdots, n-1 $$

线性链条件随机场
X 和 Y 有相同的图结构的线性链条件随机场

此情况下,$X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$,最大团是相邻两个结点的集合。

线性链条件随机场:设 $X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$ 均为线性链表示的随机变量序列,若在给定随机变量序列 $X$ 的条件下,随机变量序列 $Y$ 的条件概率分布 $P \left(Y | X\right)$ 构成条件随机场,即满足马尔可夫性:

$$ \begin{array}{c} P\left(Y_{i} | X, Y_{1}, \cdots, Y_{i-1}, Y_{i+1}, \cdots, Y_{n}\right)=P\left(Y_{i} | X, Y_{i-1}, Y_{i+1}\right) \\ i=1,2, \cdots, n \quad (\text { 在 } i=1 \text { 和 } n \text { 时只考虑单边 }) \end{array} $$

则称 $P \left(Y | X\right)$ 为线性链条件随机场。在标注问题中,$X$ 表示输入观测序列,$Y$ 表示对应的输出标记序列或状态序列。

根据 Hammersley-Clifford 定理,设 $P \left(Y | X\right)$ 为线性链条件随机场,则在随机变量 $X$ 取值为 $x$ 的条件下,随机变量 $Y$ 取值为 $y$ 的条件概率有如下形式:

$$ P(y | x)=\frac{1}{Z(x)} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$

其中,

$$ Z(x)=\sum_{y} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$

其中,$t_k$$s_l$ 是特征函数,$\lambda_k$$\mu_l$ 是对应的权值。$Z \left(x\right)$ 是规范化因子,求和是在所有可能的输出序列上进行的。

条件随机场的概率计算,学习算法和预测算法类似隐马尔可夫模型,在此不进行过多赘述,有兴趣的同学可以参见 1

综上所述,隐马尔可夫模型和条件随机场的主要联系和区别如下:

  1. HMM 是概率有向图,CRF 是概率无向图
  2. HMM 是生成模型,CRF 是判别模型
图片来源:An Introduction to Conditional Random Fields

如上图所示,上面部分为生成式模型,下面部分为判别式模型,生成式模型尝试构建联合分布 $P \left(Y, X\right)$,而判别模型则尝试构建条件分布 $P \left(Y | X\right)$

序列标注

序列标注(Sequence Labeling)是自然语言处理中的一项重要任务,对于给定的文本序列需要给出对应的标注序列。常见的序列标注任务包含:组块分析(Chunking),词性标注(Part-of-Speech,POS)和命名实体识别(Named Entity Recognition,NER)。

上图为一段文本的词性标注和命名实体识别的结果。

词性标注

词性标注是指为分词结果中的每个单词标注一个正确的词性,即确定每个词是名词、动词、形容词或其他词性的过程。

一些常用中文标注规范如下:

  1. 北京大学现代汉语语料库基本加工规范 2
  2. 北大语料库加工规范:切分·词性标注·注音 3
  3. 计算所汉语词性标记集 3.0(ICTPOS 3.0)4
  4. The Part-Of-Speech Tagging Guidelines for the Penn Chinese Treebank (3.0) 5
  5. 中文文本标注规范(微软亚洲研究院)6

命名实体识别

命名实体识别,又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等。简单的讲,就是识别自然文本中的实体指称的边界和类别。

常用的标注标准有 IO,BIO,BIOES,BMEWO 和 BMEWO+ 等。(参考自:Coding Chunkers as Taggers: IO, BIO, BMEWO, and BMEWO+

  1. IO 标注标准是最简单的标注方式,对于命名实体类别 X 标注为 I_X,其他则标注为 O。由于没有标签界线表示,这种方式无法表示两个相邻的同类命名实体。
  2. BIO 标注标准将命名实体的起始部分标记为 B_X,其余部分标记为 I_X
  3. BIOES 标注标准将命名实体的起始部分标记为 B_X,中间部分标记为 I_X,结尾部分标记为 E_X,对于单个字符成为命名实体的情况标记为 S_X
  4. BMEWO 标注标准将命名实体的起始部分标记为 B_X,中间部分标记为 M_X,结尾部分标记为 E_X,对于单个字符成为命名实体的情况标记为 W_X
  5. BMEWO+ 标注标准在 BMEWO 的基础上针对不同情况的非命名实体标签的标注进行了扩展,同时增加了一个句外(out-of-sentence)标签 W_OOS,句子起始标签 BB_O_OOS 和句子结束标签 WW_O_OOS,如 下表 所示:
标签 描述 可能上接的标签 可能下接的标签
B_X 命名实体类型 X 的起始 E_Y, W_Y, EE_O_X, WW_O_X M_X, W_X
M_X 命名实体类型 X 的中间 B_X, M_X M_X, W_X
E_X 命名实体类型 X 的结尾 B_X, M_X B_Y, W_Y, BB_O_X, WW_O_X
W_X 命名实体类型 X 的单个字符 E_Y, W_Y, EE_O_X, WW_O_X B_Y, W_Y, BB_O_X, WW_O_X
BB_O_X 非命名实体的起始,上接命名实体类型 X E_X, W_X MM_O, EE_O_Y
MM_O 非命名实体的中间 BB_O_Y, MM_O MM_O, EE_O_Y
EE_O_X 非命名实体的结尾,下接命名实体类型 X BB_O_Y, MM_O B_X, W_X
WW_O_X 非命名实体,上接命名实体,下接命名实体类型 X E_X, W_X B_Y, W_Y

不同标注标准的差别示例如下:

字符 IO BIO BIOES BMEWO BMEWO+
W_OOS
Yesterday O O O O BB_O_OOS
afternoon O O O O MM_O
, O O O O EE_O_PER
John I_PER B_PER B_PER B_PER B_PER
J I_PER I_PER I_PER M_PER M_PER
. I_PER I_PER I_PER M_PER M_PER
Smith I_PER I_PER E_PER E_PER E_PER
traveled O O O O BB_O_PER
to O O O O EE_O_LOC
Washington I_LOC B_LOC S_LOC W_LOC W_LOC
. O O O O WW_O_OOS
W_OOS

不同标准的标签数量如下表所示:

标注标准 标签数量 N=1 N=3 N=20
IO N+1 2 4 21
BIO 2N+1 3 7 41
BIOES 4N+1 5 13 81
BMEWO 4N+1 5 13 81
BMEWO+ 7N+3 10 24 143

其中,N 为命名实体类型的数量。

BiLSTM CRF 7

本小节内容参考和修改自 CRF-Layer-on-the-Top-of-BiLSTM

Huang 等人提出了一种基于 BiLSTM 和 CRF 的神经网络模型用于序例标注。整个网络如下图所示:

关于模型中的 BiLSTM 部分在此不过多赘述,相关细节可以参见之前的博客:循环神经网络 (Recurrent Neural Network, RNN)预训练自然语言模型 (Pre-trained Models for NLP)。BiLSTM-CRF 模型的输入是词嵌入向量,输出是对应的预测标注标签,如下图所示:

BiLSTM 层的输出为每个标签的分数,对于 $w_0$,BiLSTM 的输出为 1.5 (B_PER),0.9 (I_PER),0.1 (B_ORG),0.08 (I_ORG) 和 0.05 (O),这些分数为 CRF 层的输入,如下图所示:

经过 CRF 层后,具有最高分数的预测序列被选择为最优预测结果。如果没有 CRF 层,我们可以直接选择 BiLSTM 层输出分数的最大值对应的序列为预测结果。例如,对于 $w_0$,最高分数为 1.5,对应的预测标签则为 B_PER,类似的 $w_1, w_2, w_3, w_4$ 对应的预测标签为 I_PER, O, B_ORG, O,如下图所示:

虽然我们在上例中得到了正确的结果,但通常情况下并非如此。对于如下的示例,预测结果为 I_ORG, I_PER, O, I_ORG, I_PER,这显然是不正确的。

CRF 层在进行预测时可以添加一些约束,这些约束可以在训练时被 CRF 层学习得到。可能的约束有:

CRF 层的损失包含两部分,这两部分构成了 CRF 层的关键:

发射分数即为 BiLSTM 层的输出分数,例如 $w_0$ 对应的标签 B_PER 的分数为 1.5。为了方便起见,对于每类标签给定一个索引:

标签 索引
B_PER 0
I_PER 1
B_ORG 2
I_ORG 3
O 4

我们利用 $x_{i y_{j}}$ 表示发射分数,$i$ 为词的索引,$y_i$ 为标注标签的索引。例如:$x_{i=1, y_{j}=2} = x_{w_1, \text{B_ORG}} = 0.1$,表示 $w_1$B_ORG 的分数为 0.1。

我们利用 $t_{y_i, y_j}$ 表示转移分数,例如 $t_{\text{B_PER}, \text{I_PER}} = 0.9$ 表示由标签 B_PER 转移到 I_PER 的分数为 0.9。因此,需要一个转移分数矩阵用于存储所有标注标签之间的转移分数。为了使得转移分数矩阵更加鲁棒,需要添加两个标签 STARTEND,分别表示一个句子的开始和结束。下表为一个转移分数矩阵的示例:

START B-PER I-PER B-ORG I-ORG O END
START 0 0.8 0.007 0.7 0.0008 0.9 0.08
B_PER 0 0.6 0.9 0.2 0.0006 0.6 0.009
I_PER -1 0.5 0.53 0.55 0.0003 0.85 0.008
B_ORG 0.9 0.5 0.0003 0.25 0.8 0.77 0.006
I_ORG -0.9 0.45 0.007 0.7 0.65 0.76 0.2
O 0 0.65 0.0007 0.7 0.0008 0.9 0.08
END 0 0 0 0 0 0 0

转移分数矩阵作为 BiLSTM-CRF 模型的一个参数,随机初始化并通过模型的训练不断更新,最终学习得到约束条件。

CRF 层的损失函数包含两个部分:真实路径分数和所有可能路径的总分数。假设每个可能的路径有一个分数 $P_i$,共 $N$ 种可能的路径,所有路径的总分数为:

$$ P_{\text {total}}=P_{1}+P_{2}+\ldots+P_{N}=e^{S_{1}}+e^{S_{2}}+\ldots+e^{S_{N}} $$

则损失函数定义为:

$$ \text{Loss} = \dfrac{P_{\text{RealPath}}}{\sum_{i=1}^{N} P_i} $$

对于 $S_i$,共包含两部分:发射分数和转移分数。以路径 START -> B_PER -> I_PER -> O -> B_ORG -> O -> END 为例,发射分数为:

$$ \begin{aligned} \text{EmissionScore} = \ &x_{0, \text{START}} + x_{1, \text{B_PER}} + x_{2, \text{I_PER}} \\ &+ x_{3, \text{O}} + x_{4, \text{B_ORG}} + x_{5, \text{O}} + x_{6, \text{END}} \end{aligned} $$

其中 $x_{i, y_j}$ 表示第 $i$ 个词标签为 $y_j$ 的分数,为 BiLSTM 的输出,$x_{0, \text{START}}$$x_{6, \text{END}}$ 可以设置为 0。转换分数为:

$$ \begin{aligned} \text{TransitionScore} = \ &t_{\text{START}, \text{B_PER}} + t_{\text{B_PER}, \text{I_PER}} + t_{\text{I_PER}, \text{O}} \\ &+ t_{\text{O}, \text{B_ORG}} + t_{\text{B_ORG}, \text{O}} + t_{\text{O}, \text{END}} \end{aligned} $$

其中 $t_{y_i, y_j}$ 表示标注标签由 $y_i$ 转移至 $y_j$ 的分数。

对于所有路径的总分数的计算过程采用了类似 动态规划 的思想,整个过程计算比较复杂,在此不再详细展开,详细请参见参考文章。

利用训练好的 BiLSTM-CRF 模型进行预测时,首先我们可以得到序列的发射分数和转移分数,其次用维特比算法可以得到最终的预测标注序列。

Lattice LSTM 8

Zhang 等人针对中文提出了一种基于 Lattice LSTM 的命名实体识别方法,Lattice LSTM 的结构如下图所示:

模型的基本思想是将句子中的词汇(例如:南京,长江大桥等)信息融入到基于字符的 LSTM 模型中,从而可以显性地利用词汇信息。

模型的输入为一个字符序列 $c_1, c_2, \cdots, c_m$ 和词汇表 $\mathbb{D}$ 中所有匹配的字符子序列,其中词汇表 $\mathbb{D}$ 利用大量的原始文本通过分词构建。令 $w_{b, e}^d$ 表示有以第 $b$ 个字符起始,以第 $e$ 个字符结尾的子序列,例如:$w_{1,2}^d$ 表示“南京 ”,$w_{7,8}^d$ 表示“大桥”。

不同于一般的字符级模型,LSTM 单元的状态考虑了句子中的子序列 $w_{b,e}^d$,每个子序列 $w_{b,e}^d$ 表示为:

$$ \mathbf{x}_{b, e}^{w}=\mathbf{e}^{w}\left(w_{b, e}^{d}\right) $$

其中,$\mathbf{e}^{w}$ 为词向量查询表。一个词单元 $\mathbf{c}_{b,e}^w$ 用于表示 $\mathbf{x}_{b,e}^w$ 的循环状态:

$$ \begin{aligned} \left[\begin{array}{c} \mathbf{i}_{b, e}^{w} \\ \mathbf{f}_{b, e}^{w} \\ \widetilde{c}_{b, e}^{w} \end{array}\right] &=\left[\begin{array}{c} \sigma \\ \sigma \\ \tanh \end{array}\right]\left(\mathbf{W}^{w \top}\left[\begin{array}{c} \mathbf{x}_{b, e}^{w} \\ \mathbf{h}_{b}^{c} \end{array}\right]+\mathbf{b}^{w}\right) \\ \mathbf{c}_{b, e}^{w} &=\mathbf{f}_{b, e}^{w} \odot \mathbf{c}_{b}^{c}+\mathbf{i}_{b, e}^{w} \odot \widetilde{c}_{b, e}^{w} \end{aligned} $$

其中,$\mathbf{i}_{b, e}^{w}$$\mathbf{f}_{b, e}^{w}$ 分别为输入门和遗忘门。由于仅在字符级别上进行标注,因此对于词单元来说没有输出门。

对于 $\mathbf{c}_{j}^c$ 来说可能有多条信息流,例如 $\mathbf{c}_7^c$ 的输入包括 $\mathbf{x}_7^c$(桥),$\mathbf{c}_{6,7}^w$(大桥)和 $\mathbf{c}_{4,7}^w$(长江大桥)。论文采用了一个新的门 $\mathbf{i}_{b,e}^c$ 来控制所有子序列单元 $\mathbf{c}_{b,e}^w$$\mathbf{c}_{j}^c$ 的贡献:

$$ \mathbf{i}_{b, e}^{c}=\sigma\left(\mathbf{W}^{l \top}\left[\begin{array}{c} \mathbf{x}_{e}^{c} \\ \mathbf{c}_{b, e}^{w} \end{array}\right]+\mathbf{b}^{l}\right) $$

则单元状态 $\mathbf{c}_j^c$ 的计算变为:

$$ \mathbf{c}_{j}^{c}=\sum_{b \in\left\{b^{\prime} | w_{b^{\prime}, j} \in \mathbb{D}\right\}} \boldsymbol{\alpha}_{b, j}^{c} \odot \boldsymbol{c}_{b, j}^{w}+\boldsymbol{\alpha}_{j}^{c} \odot \widetilde{\boldsymbol{c}}_{j}^{c} $$

在上式中,$\mathbf{i}_{b,j}^c$$\mathbf{i}_j^c$ 标准化为 $\boldsymbol{\alpha}_{b, j}^{c}$$\boldsymbol{\alpha}_{j}^{c}$

$$ \begin{aligned} \boldsymbol{\alpha}_{b, j}^{c} &=\frac{\exp \left(\mathbf{i}_{b, j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \\ \boldsymbol{\alpha}_{j}^{c} &=\frac{\exp \left(\mathbf{i}_{j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \end{aligned} $$

开放资源

标注工具

  1. synyi/poplar
  2. nlplab/brat
  3. doccano/doccano
  4. heartexlabs/label-studio
  5. deepwel/Chinese-Annotator
  6. jiesutd/YEDDA

开源模型,框架和代码

  1. pytorch/text
  2. flairNLP/flair
  3. PetrochukM/PyTorch-NLP
  4. allenai/allennlp
  5. fastnlp/fastNLP
  6. Stanford CoreNLP
  7. NeuroNER
  8. spaCy
  9. NLTK
  10. BrikerMan/Kashgari
  11. Hironsan/anago
  12. crownpku/Information-Extraction-Chinese
  13. thunlp/OpenNRE
  14. hankcs/HanLP
  15. jiesutd/NCRFpp

其他资源

  1. keon/awesome-nlp
  2. crownpku/Awesome-Chinese-NLP
  3. sebastianruder/NLP-progress
  4. thunlp/NREPapers

  1. 李航. (2019). 统计学习方法(第二版). 清华大学出版社. ↩︎ ↩︎

  2. 俞士汶, 段慧明, 朱学锋, & 孙斌. (2002). 北京大学现代汉语语料库基本加工规范. 中文信息学报, 16(5), 51-66. ↩︎

  3. 俞士汶, 段慧明, 朱学锋, 孙斌, & 常宝宝. (2003). 北大语料库加工规范: 切分· 词性标注· 注音. 汉语语言与计算学报, 13(2), 121-158. ↩︎

  4. http://ictclas.nlpir.org/nlpir/html/readme.htm ↩︎

  5. Xia, F. (2000). The part-of-speech tagging guidelines for the Penn Chinese Treebank (3.0). IRCS Technical Reports Series, 38. ↩︎

  6. Huang, C. N., Li, Y., & Zhu, X. (2006). Tokenization guidelines of Chinese text (v5.0, in Chinese). Microsoft Research Asia↩︎

  7. Huang, Z., Xu, W., & Yu, K. (2015). Bidirectional LSTM-CRF models for sequence tagging. arXiv preprint arXiv:1508.01991↩︎

  8. Zhang, Y., & Yang, J. (2018). Chinese NER Using Lattice LSTM. In Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (pp. 1554-1564). ↩︎

图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)

2020-04-11 08:00:00

图(Graph / Network)数据类型可以自然地表达物体和物体之间的联系,在我们的日常生活与工作中无处不在。例如:微信和新浪微博等构成了人与人之间的社交网络;互联网上成千上万个页面构成了网页链接网络;国家城市间的运输交通构成了物流网络。

图片来源:The power of relationships in data

通常定义一个图 $G = \left(V, E\right)$,其中 $V$顶点(Vertices)集合,$E$边(Edges)集合。对于一条边 $e = u, v$ 包含两个端点(Endpoints) $u$$v$,同时 $u$ 可以称为 $v$邻居(Neighbor)。当所有的边为有向边时,图称之为有向(Directed)图,当所有边为无向边时,图称之为无向(Undirected)图。对于一个顶点 $v$,令 $d \left(v\right)$ 表示连接的边的数量,称之为度(Degree)。对于一个图 $G = \left(V, E\right)$,其邻接矩阵(Adjacency Matrix) $A \in \mathbb{A}^{|V| \times |V|}$ 定义为:

$$ A_{i j}=\left\{\begin{array}{ll} 1 & \text { if }\left\{v_{i}, v_{j}\right\} \in E \text { and } i \neq j \\ 0 & \text { otherwise } \end{array}\right. $$

作为一个典型的非欧式数据,对于图数据的分析主要集中在节点分类,链接预测和聚类等。对于图数据而言,**图嵌入(Graph / Network Embedding)图神经网络(Graph Neural Networks, GNN)**是两个类似的研究领域。图嵌入旨在将图的节点表示成一个低维向量空间,同时保留网络的拓扑结构和节点信息,以便在后续的图分析任务中可以直接使用现有的机器学习算法。一些基于深度学习的图嵌入同时也属于图神经网络,例如一些基于图自编码器和利用无监督学习的图卷积神经网络等。下图描述了图嵌入和图神经网络之间的差异:

本文中图嵌入网络表示学习均表示 Graph / Network Embedding。

图嵌入

本节内容主要参考自:
A Comprehensive Survey of Graph Embedding: Problems, Techniques and Applications 1
Graph Embedding Techniques, Applications, and Performance: A Survey 2
Representation Learning on Graphs: Methods and Applications 3

使用邻接矩阵的网络表示存在计算效率的问题,邻接矩阵 $A$ 使用 $|V| \times |V|$ 的存储空间表示一个图,随着节点个数的增长,这种表示所需的空间成指数增长。同时,在邻接矩阵中绝大多数是 0,数据的稀疏性使得快速有效的学习方式很难被应用。

网路表示学习是指学习得到网络中节点的低维向量表示,形式化地,网络表示学习的目标是对每个节点 $v \in V$ 学习一个实值向量 $R_v \in \mathbb{R}^k$,其中 $k \ll |V|$ 表示向量的维度。经典的 Zachary’s karate club 网络的嵌入可视化如下图所示:

Random Walk

基于随机游走的图嵌入通过使得图上一个短距的随机游走中共现的节点具有更相似的表示的方式来优化节点的嵌入。

DeepWalk

DeepWalk 4 算法主要包含两个部分:一个随机游走序列生成器和一个更新过程。随机游走序列生成器首先在图 $G$ 中均匀地随机抽样一个随机游走 $\mathcal{W}_{v_i}$ 的根节点 $v_i$,接着从节点的邻居中均匀地随机抽样一个节点直到达到设定的最大长度 $t$。对于一个生成的以 $v_i$ 为中心左右窗口为 $w$ 的随机游走序列 $v_{i-w}, \dotsc, v_{i-1}, v_i, v_{i+1}, \dotsc, v_{i+m}$,DeepWalk 利用 SkipGram 算法通过最大化以 $v_i$ 为中心,左右 $w$ 为窗口的同其他节点共现概率来优化模型:

$$ \text{Pr} \left(\left\{v_{i-w}, \dotsc, v_{i+w}\right\} \setminus v_i \mid \Phi \left(v_i\right)\right) = \prod_{j=i-w, j \neq i}^{i+w} \text{Pr} \left(v_j \mid \Phi \left(v_i\right)\right) $$

DeepWalk 和 Word2Vec 的类比如下表所示:

模型 目标 输入 输出
Word2Vec 句子 词嵌入
DeepWalk 节点 节点序列 节点嵌入

node2vec

node2vec 5 通过改变随机游走序列生成的方式进一步扩展了 DeepWalk 算法。DeepWalk 选取随机游走序列中下一个节点的方式是均匀随机分布的,而 node2vec 通过引入两个参数 $p$$q$,将宽度优先搜索深度优先搜索引入了随机游走序列的生成过程。 宽度优先搜索注重邻近的节点并刻画了相对局部的一种网络表示, 宽度优先中的节点一般会出现很多次,从而降低刻画中心节点的邻居节点的方差, 深度优先搜索反映了更高层面上的节点之间的同质性。

node2vec 中的两个参数 $p$$q$ 控制随机游走序列的跳转概率。假设上一步游走的边为 $\left(t, v\right)$, 那么对于节点 $v$ 的不同邻居,node2vec 根据 $p$$q$ 定义了不同的邻居的跳转概率,$p$ 控制跳向上一个节点的邻居的概率,$q$ 控制跳向上一个节点的非邻居的概率,具体的未归一的跳转概率值 $\pi_{vx} = \alpha_{pq} \left(t, x\right)$ 如下所示:

$$ \alpha_{p q}(t, x)=\left\{\begin{array}{cl} \dfrac{1}{p}, & \text { if } d_{t x}=0 \\ 1, & \text { if } d_{t x}=1 \\ \dfrac{1}{q}, & \text { if } d_{t x}=2 \end{array}\right. $$

其中,$d_{tx}$ 表示节点 $t$$x$ 之间的最短距离。为了获得最优的超参数 $p$$q$ 的取值,node2vec 通过半监督形式,利用网格搜索最合适的参数学习节点表示。

APP

之前的基于随机游走的图嵌入方法,例如:DeepWalk,node2vec 等,都无法保留图中的非对称信息。然而非对称性在很多问题,例如:社交网络中的链路预测、电商中的推荐等,中至关重要。在有向图和无向图中,非对称性如下图所示:

为了保留图的非对称性,对于每个节点 $v$ 设置两个不同的角色:源和目标,分别用 $\overrightarrow{s_{v}}$$\overrightarrow{t_{v}}$ 表示。对于每个从 $u$ 开始以 $v$ 结尾的采样序列,利用 $(u, v)$ 表示采样的节点对。则利用源节点 $u$ 预测目标节点 $v$ 的概率如下:

$$ p(v | u)=\frac{\exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})}{\sum_{n \in V} \exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})} $$

通过 Skip-Gram 和负采样对模型进行优化,损失函数如下:

$$ \begin{aligned} \ell &= \log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})+k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})] \\ &= \sum_{u} \sum_{v} \# \text {Sampled}_{u}(v) \cdot \left(\log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}}) + k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})]\right) \end{aligned} $$

其中,我们根据分布 $P_D \left(n\right) \sim \dfrac{1}{|V|}$ 随机负采样 $k$ 个节点对,$\# \text{Sampled}_{u}(v)$ 为采样的 $\left(u, v\right)$ 对的个数,$\sigma$ 为 sigmoid 函数。通常情况下,$\# \text{Sampled}_{u}(v) \neq \# \text{Sampled}_{v}(u)$,即 $\left(u, v\right)$$\left(v, u\right)$ 的观测数量是不同的。模型利用 Monte-Carlo End-Point 采样方法 6 随机的以 $v$ 为起点和 $\alpha$ 为停止概率采样 $p$ 条路径。这种采样方式可以用于估计任意一个节点对之间的 Rooted PageRank 7 值,模型利用这个值估计由 $v$ 到达 $u$ 的概率。

Matrix Fractorization

GraRep

GraRep 8 提出了一种基于矩阵分解的图嵌入方法。对于一个图 $G$,利用邻接矩阵 $S$ 定义图的度矩阵:

$$ D_{i j}=\left\{\begin{array}{ll} \sum_{p} S_{i p}, & \text { if } i=j \\ 0, & \text { if } i \neq j \end{array}\right. $$

则一阶转移概率矩阵定义如下:

$$ A = D^{-1} S $$

其中,$A_{i, j}$ 表示通过一步由 $v_i$ 转移到 $v_j$ 的概率。所谓的全局特征包含两个部分:

  1. 捕获两个节点之间的长距离特征
  2. 分别考虑按照不同转移步数的连接

下图展示了 $k = 1, 2, 3, 4$ 情况下的强(上)弱(下)关系:

利用 Skip-Gram 和 NCE(noise contrastive estimation)方法,对于一个 $k$ 阶转移,可以将模型归结到一个矩阵 $Y_{i, j}^k$ 的分解问题:

$$ Y_{i, j}^{k}=W_{i}^{k} \cdot C_{j}^{k}=\log \left(\frac{A_{i, j}^{k}}{\sum_{t} A_{t, j}^{k}}\right)-\log (\beta) $$

其中,$W$$C$ 的每一行分别为节点 $w$$c$ 的表示,$\beta = \lambda / N$$\lambda$ 为负采样的数量,$N$ 为图中边的个数。

之后为了减少噪音,模型将 $Y^k$ 中所有的负值替换为 0,通过 SVD(方法详情见参见之前博客)得到节点的 $d$ 维表示:

$$ \begin{aligned} X_{i, j}^{k} &= \max \left(Y_{i, j}^{k}, 0\right) \\ X^{k} &= U^{k} \Sigma^{k}\left(V^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &= U_{d}^{k} \Sigma_{d}^{k}\left(V_{d}^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &= W^{k} C^{k} \\ W^{k} &= U_{d}^{k}\left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} \\ C^{k} &= \left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} V_{d}^{k T} \end{aligned} $$

最终,通过对不同 $k$ 的表示进行拼接得到节点最终的表示。

HOPE

HOPE 9 对于每个节点最终生成两个嵌入表示:一个是作为源节点的嵌入表示,另一个是作为目标节点的嵌入表示。模型通过近似高阶相似性来保留非对称传递性,其优化目标为:

$$ \min \left\|\mathbf{S}-\mathbf{U}^{s} \cdot \mathbf{U}^{t^{\top}}\right\|_{F}^{2} $$

其中,$\mathbf{S}$ 为相似矩阵,$\mathbf{U}^s$$\mathbf{U}^t$ 分别为源节点和目标节点的向量表示。下图展示了嵌入向量可以很好的保留非对称传递性:

对于 $\mathbf{S}$ 有多种可选近似度量方法:Katz Index,Rooted PageRank(RPR),Common Neighbors(CN),Adamic-Adar(AA)。这些度量方法可以分为两类:全局近似(Katz Index 和 RPR)和局部近似(CN 和 AA)。

算法采用了一个广义 SVD 算法(JDGSVD)来解决使用原始 SVD 算法计算复杂度为$O \left(N^3\right)$ 过高的问题,从而使得算法可以应用在更大规模的图上。

Meta Paths

metapath2vec

metapath2vec 10 提出了一种基于元路径的异构网络表示学习方法。在此我们引入 3 个定义:

  1. **异构网络((Heterogeneous information network,HIN)**可以定义为一个有向图 $G = \left(V, E\right)$,一个节点类型映射 $\tau: V \to A$ 和一个边类型映射 $\phi: E \to R$,其中对于 $v \in V$$\tau \left(v\right) \in A$$e \in E$$\phi \left(e\right) \in R$,且 $|A| + |R| > 1$
  2. **网络模式(Network schema)**定义为 $T_G = \left(A, R\right)$,为一个包含节点类型映射 $\tau \left(v\right) \in A$ 和边映射 $\phi \left(e\right) \in R$ 异构网络的 $G = \left(V, E\right)$ 的元模板。
  3. **元路径(Meta-path)**定义为网络模式 $T_G = \left(A, R\right)$ 上的一条路径 $P$,形式为 $A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l+1}$

下图展示了一个学术网络和部分元路径:

其中,APA 表示一篇论文的共同作者,APVPA 表示两个作者在同一个地方发表过论文。

metapath2vec 采用了基于元路径的随机游走来生成采样序列,这样就可以保留原始网络中的语义信息。对于一个给定的元路径模板 $P: A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots A_{t} \stackrel{R_{t}}{\longrightarrow} A_{t+1} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l}$,第 $i$ 步的转移概率为:

$$ p\left(v^{i+1} | v_{t}^{i}, P\right)=\left\{\begin{array}{ll} \dfrac{1}{\left|N_{t+1}\left(v_{t}^{i}\right)\right|} & \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right)=A_{t+1} \\ 0 & \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right) \neq A_{t+1} \\ 0 & \left(v_{t}^{i}, v^{i+1}\right) \notin E \end{array}\right. $$

其中,$v^i_t \in A_t$$N_{t+1} \left(v^i_t\right)$ 表示节点 $v^i_t$ 类型为 $A_{t+1}$ 的邻居。之后,则采用了类似 DeepWalk 的方式进行训练得到节点表示。

HIN2Vec

HIN2Vec 11 提出了一种利用多任务学习通过多种关系进行节点和元路径表示学习的方法。模型最初是希望通过一个多分类模型来预测任意两个节点之间所有可能的关系。假设对于任意两个节点,所有可能的关系集合为 $R = \{\text{P-P, P-A, A-P, P-P-P, P-P-A, P-A-P, A-P-P, A-P-A}\}$。假设一个实例 $P_1$$A_1$ 包含两种关系:$\text{P-A}$$\text{P-P-A}$,则对应的训练数据为 $\langle x: P_1, y: A_1, output: \left[0, 1, 0, 0, 1, 0, 0, 0\right] \rangle$

但实际上,扫描整个网络寻找所有可能的关系是不现实的,因此 HIN2Vec 将问题简化为一个给定两个节点判断之间是否存在一个关系的二分类问题,如下图所示:

模型的三个输入分别为节点 $x$$y$,以及关系 $r$。在隐含层输入被转换为向量 $W_{X}^{\prime} \vec{x}, W_{Y}^{\prime} \vec{y}$$f_{01}\left(W_{R}^{\prime} \vec{r}\right)$。需要注意对于关系 $r$,模型应用了一个正则化函数 $f_{01} \left(\cdot\right)$ 使得 $r$ 的向量介于 $0$$1$ 之间。之后采用逐元素相乘对三个向量进行汇总 $W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)$。在最后的输出层,通过计算 $sigmoid \left(\sum W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)\right)$ 得到最终的预测值。

在生成训练数据时,HIN2Vec 采用了完全随机游走进行节点采样,而非 metapath2vec 中的按照给定的元路径的方式。通过随机替换 $x, y, r$ 中的任何一个可以生成负样本,但当网络中的关系数量较少,节点数量远远大于关系数量时,这种方式很可能产生错误的负样本,因此 HIN2Vec 只随机替换 $x, y$,保持 $r$ 不变。

Deep Learning

SDNE

SDNE 12 提出了一种利用自编码器同时优化一阶和二阶相似度的图嵌入算法,学习得到的向量能够保留局部和全局的结构信息。SDNE 使用的网络结构如下图所示:

对于二阶相似度,自编码器的目标是最小化输入和输出的重构误差。SDNE 采用邻接矩阵作为自编码器的输入,$\mathbf{x}_i = \mathbf{s}_i$,每个 $\mathbf{s}_i$ 包含了节点 $v_i$ 的邻居结构信息。模型的损失函数如下:

$$ \mathcal{L}=\sum_{i=1}^{n}\left\|\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right\|_{2}^{2} $$

由于网络的稀疏性,邻接矩阵中的非零元素远远少于零元素,因此模型采用了一个带权的损失函数:

$$ \begin{aligned} \mathcal{L}_{2nd} &=\sum_{i=1}^{n}\left\|\left(\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right) \odot \mathbf{b}_{i}\right\|_{2}^{2} \\ &=\|(\hat{X}-X) \odot B\|_{F}^{2} \end{aligned} $$

其中,$\odot$ 表示按位乘,$\mathbf{b}_i = \left\{b_{i, j}\right\}_{j=1}^{n}$,如果 $s_{i, j} = 0$$b_{i, j} = 1$ 否则 $b_{i, j} = \beta > 1$

对于一阶相似度,模型利用了一个监督学习模块最小化节点在隐含空间中距离。损失函数如下:

$$ \begin{aligned} \mathcal{L}_{1st} &=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}^{(K)}-\mathbf{y}_{j}^{(K)}\right\|_{2}^{2} \\ &=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2} \end{aligned} $$

最终,模型联合损失函数如下:

$$ \begin{aligned} \mathcal{L}_{mix} &=\mathcal{L}_{2nd}+\alpha \mathcal{L}_{1st}+\nu \mathcal{L}_{reg} \\ &=\|(\hat{X}-X) \odot B\|_{F}^{2}+\alpha \sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2}+\nu \mathcal{L}_{reg} \end{aligned} $$

其中,$\mathcal{L}_{reg}$ 为 L2 正则项。

DNGR

DNGR 13 提出了一种利用基于 Stacked Denoising Autoencoder(SDAE)提取特征的网络表示学习算法。算法的流程如下图所示:

模型首先利用 Random Surfing 得到一个概率共现(PCO)矩阵,之后利用其计算得到 PPMI 矩阵,最后利用 SDAE 进行特征提取得到节点的向量表示。

对于传统的将图结构转换为一个线性序列方法存在几点缺陷:

  1. 采样序列边缘的节点的上下文信息很难被捕捉。
  2. 很难直接确定游走的长度和步数等超参数,尤其是对于大型网络来说。

受 PageRank 思想影响,作者采用了 Random Surfing 模型。定义转移矩阵 $A$,引入行向量 $p_k$,第 $j$ 个元素表示通过 $k$ 步转移之后到达节点 $j$ 的概率。$p_0$ 为一个初始向量,其仅第 $i$ 个元素为 1,其它均为 0。在考虑以 $1 - \alpha$ 的概率返回初始节点的情况下有:

$$ p_{k}=\alpha \cdot p_{k-1} A+(1-\alpha) p_{0} $$

在不考虑返回初始节点的情况下有:

$$ p_{k}^{*}=p_{k-1}^{*} A=p_{0} A^{k} $$

直观而言,两个节点越近,两者的关系越亲密,因此通过同当前节点的相对距离来衡量上下文节点的重要性是合理的。基于此,第 $i$ 个节点的表示可以用如下方式构造:

$$ r=\sum_{k=1}^{K} w(k) \cdot p_{k}^{*} $$

其中,$w \left(\cdot\right)$ 是一个衰减函数。

利用 PCO 计算得到 PPMI 后,再利用一个 SDAE 进行特征提取。Stacking 策略可以通过不同的网络层学习得到不同层级的表示,Denoising 策略则通过去除数据中的噪声,增加结果的鲁棒性。同时,SNGR 相比基于 SVD 的方法效率更高。

Others

LINE

LINE 14 提出了一个用于大规模网络嵌入的方法,其满足如下 3 个要求:

  1. 同时保留节点之间的一阶相似性(first-order proximity)和二阶相似性(second-order proximity)。
  2. 可以处理大规模网络,例如:百万级别的顶点和十亿级别的边。
  3. 可以处理有向,无向和带权的多种类型的图结构。

给定一个无向边 $\left(i, j\right)$,点 $v_i$$v_j$ 的联合概率如下:

$$ p_{1}\left(v_{i}, v_{j}\right)=\frac{1}{1+\exp \left(-\vec{u}_{i}^{T} \cdot \vec{u}_{j}\right)} $$

其中,$\vec{u}_{i} \in R^{d}$ 为节点 $v_i$ 的低维向量表示。在空间 $V \times V$ 上,分布 $p \left(\cdot, \cdot\right)$ 的经验概率为 $\hat{p}_1 \left(i, j\right) = \dfrac{w_{ij}}{V}$,其中 $W = \sum_{\left(i, j\right) \in E} w_{ij}$。通过最小化两个分布的 KL 散度来优化模型,则目标函数定义如下:

$$ O_{1}=-\sum_{(i, j) \in E} w_{i j} \log p_{1}\left(v_{i}, v_{j}\right) $$

需要注意的是一阶相似度仅可用于无向图,通过最小化上述目标函数,我们可以将任意顶点映射到一个 $d$ 维空间向量。

二阶相似度既可以用于无向图,也可以用于有向图。二阶相似度假设共享大量同其他节点连接的节点之间是相似的,每个节点被视为一个特定的上下文,则在上下文上具有类似分布的节点是相似的。在此,引入两个向量 $\vec{u}_{i}$$\vec{u}_{\prime i}$,其中 $\vec{u}_{i}$$v_i$ 做为节点的表示,$\vec{u}_{\prime i}$$v_i$ 做为上下文的表示。对于一个有向边 $\left(i, j\right)$,由 $v_i$ 生成上下文 $v_j$ 的概率为:

$$ p_{2}\left(v_{j} | v_{i}\right)=\frac{\exp \left(\vec{u}_{j}^{\prime T} \cdot \vec{u}_{i}\right)}{\sum_{k=1}^{|V|} \exp \left(\vec{u}_{k}^{\prime T} \cdot \vec{u}_{i}\right)} $$

其中,$|V|$ 为节点或上下文的数量。在此我们引入一个参数 $\lambda_i$ 用于表示节点 $v_i$ 的重要性程度,重要性程度可以利用度或者 PageRank 算法进行估计。经验分布 $\hat{p}_{2}\left(\cdot \mid v_{i}\right)$ 定义为 $\hat{p}_{2}\left(v_{j} \mid v_{i}\right)=\dfrac{w_{i j}}{d_{i}}$,其中 $w_{ij}$ 为边 $\left(i, j\right)$ 的权重,$d_i$ 为节点 $v_i$ 的出度。LINE 中采用 $d_i$ 作为节点的重要性 $\lambda_i$,利用 KL 散度同时忽略一些常量,目标函数定义如下:

$$ O_{2}=-\sum_{(i, j) \in E} w_{i j} \log p_{2}\left(v_{j} \mid v_{i}\right) $$

LINE 采用负采样的方式对模型进行优化,同时利用 Alias 方法 15 16 加速采样过程。

图神经网络

本节内容主要参考自:
Deep Learning on Graphs: A Survey 17
A Comprehensive Survey on Graph Neural Networks 18
Graph Neural Networks: A Review of Methods and Applications 19
Introduction to Graph Neural Networks 20

图神经网络(Graph Neural Network,GNN)最早由 Scarselli 等人 21 提出。图中的一个节点可以通过其特征和相关节点进行定义,GNN 的目标是学习一个状态嵌入 $\mathbf{h}_v \in \mathbb{R}^s$ 用于表示每个节点的邻居信息。状态嵌入 $\mathbf{h}_v$ 可以生成输出向量 $\mathbf{o}_v$ 用于作为预测节点标签的分布等。

下面三张图分别从图的类型,训练方法和传播过程角度列举了不同 GNN 的变种 19

下面我们主要从模型的角度分别介绍不同种类的 GNN。

Graph Neural Networks

为了根据邻居更新节点的状态,定义一个用于所有节点的函数 $f$,称之为 local transition function。定义一个函数 $g$,用于生成节点的输出,称之为 local output function。有:

$$ \begin{array}{c} \mathbf{h}_{v}=f\left(\mathbf{x}_{v}, \mathbf{x}_{co[v]}, \mathbf{h}_{ne[v]}, \mathbf{x}_{ne[v])}\right. \\ \mathbf{o}_{v}=g\left(\mathbf{h}_{v}, \mathbf{x}_{v}\right) \end{array} $$

其中,$\mathbf{x}$ 表示输入特征,$\mathbf{h}$ 表示隐含状态。$co[v]$ 为连接到节点 $v$ 的边集,$ne[v]$ 为节点 $v$ 的邻居。

上图中,$\mathbf{x}_1$ 表示 $l_1$ 的输入特征,$co[l_1]$ 包含了边 $l_{(1, 4)}, l_{(6, 1)}, l_{(1, 2)}$$l_{(3, 1)}$$ne[l_1]$ 包含了节点 $l_2, k_3, l_4$$l_6$

$\mathbf{H}, \mathbf{O}, \mathbf{X}$$\mathbf{X}_N$ 分别表示状态、输出、特征和所有节点特征的向量,有:

$$ \begin{aligned} &\mathbf{H}=F(\mathbf{H}, \mathbf{X})\\ &\mathbf{O}=G\left(\mathbf{H}, \mathbf{X}_{N}\right) \end{aligned} $$

其中,$F$global transition function$G$global output function,分别为图中所有节点的 local transition function $f$ 和 local output function $g$ 的堆叠版本。依据 Banach 的 Fixed Point Theorem 22,GNN 利用传统的迭代方式计算状态:

$$ \mathbf{H}^{t+1}=F\left(\mathbf{H}^{t}, \mathbf{X}\right) $$

其中,$\mathbf{H}^t$ 表示第 $t$ 论循环 $\mathbf{H}$ 的值。

介绍完 GNN 的框架后,下一个问题就是如果学习得到 local transition function $f$ 和 local output function $g$。在包含目标信息($\mathbf{t}_v$ 对于特定节点)的监督学习情况下,损失为:

$$ loss = \sum_{i=1}^{p} \left(\mathbf{t}_i - \mathbf{o}_i\right) $$

其中,$p$ 为用于监督学习的节点数量。利用基于梯度下降的学习方法优化模型后,我们可以得到针对特定任务的训练模型和图中节点的隐含状态。

尽管实验结果表明 GNN 是一个用于建模结构数据的强大模型,但对于一般的 GNN 模型仍存在如下缺陷:

  1. 对于固定点,隐含状态的更新是低效地。
  2. GNN 在每一轮计算中共享参数,而常见的神经网络结构在不同层使用不同的参数。同时,隐含节点状态的更新可以进一步应用 RNN 的思想。
  3. 边上的一些信息特征并没有被有效的建模,同时如何学习边的隐含状态也是一个重要问题。
  4. 如果我们更关注节点的表示而非图的表示,当迭代轮数 $T$ 很大时使用固定点是不合适的。这是因为固定点表示的分布在数值上会更加平滑,从而缺少用于区分不同节点的信息。

Graph Convolutional Networks

图卷积神经网络是将用于传统数据(例如:图像)的卷积操作应用到图结构的数据中。核心思想在于学习一个函数 $f$,通过聚合节点 $v_i$ 自身的特征 $\mathbf{X}_i$ 和邻居的特征 $\mathbf{X}_j$ 获得节点的表示,其中 $j \in N\left(v_i\right)$ 为节点的邻居。

下图展示了一个用于节点表示学习的 GCN 过程:

GCN 在构建更复杂的图神经网路中扮演了一个核心角色:

包含 Pooling 模块用于图分类的 GCN
包含 GCN 的图自编码器
包含 GCN 的图时空网络

GCN 方法可以分为两大类:基于频谱(Spectral Methods)和基于空间(Spatial Methods)的方法。

基于频谱的方法(Spectral Methods)

基于频谱的方法将图视为无向图进行处理,图的一种鲁棒的数学表示为标准化的图拉普拉斯矩阵:

$$ \mathbf{L}=\mathbf{I}_{\mathbf{n}}-\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} $$

其中,$\mathbf{A}$ 为图的邻接矩阵,$\mathbf{D}$ 为节点度的对角矩阵,$\mathbf{D}_{ii} = \sum_{j} \left(\mathbf{A}_{i, j}\right)$。标准化的拉普拉斯矩阵具有实对称半正定的性质,因此可以分解为:

$$ \mathbf{L}=\mathbf{U} \mathbf{\Lambda} \mathbf{U}^{T} $$

其中,$\mathbf{U}=\left[\mathbf{u}_{\mathbf{0}}, \mathbf{u}_{\mathbf{1}}, \cdots, \mathbf{u}_{\mathbf{n}-\mathbf{1}}\right] \in \mathbf{R}^{N \times N}$ 是由 $\mathbf{L}$ 的特征向量构成的矩阵,$\mathbf{\Lambda}$ 为特征值的对角矩阵,$\mathbf{\Lambda}_{ii} = \lambda_i$。在图信号处理过程中,一个图信号 $\mathbf{x} \in \mathbb{R}^N$ 是一个由图的节点构成的特征向量,其中 $\mathbf{x}_i$ 表示第 $i$ 个节点的值。对于信号 $\mathbf{x}$,图上的傅里叶变换可以定义为:

$$ \mathscr{F}(\mathbf{x})=\mathbf{U}^{T} \mathbf{x} $$

傅里叶反变换定义为:

$$ \mathscr{F}^{-1}(\hat{\mathbf{x}})=\mathbf{U} \hat{\mathbf{x}} $$

其中,$\hat{\mathbf{x}}$ 为傅里叶变换后的结果。

转变后信号 $\hat{\mathbf{x}}$ 的元素为新空间图信号的坐标,因此输入信号可以表示为:

$$ \mathbf{x}=\sum_{i} \hat{\mathbf{x}}_{i} \mathbf{u}_{i} $$

这正是傅里叶反变换的结果。那么对于输入信号 $\mathbf{x}$ 的图卷积可以定义为:

$$ \begin{aligned} \mathbf{x} *_{G} \mathbf{g} &=\mathscr{F}^{-1}(\mathscr{F}(\mathbf{x}) \odot \mathscr{F}(\mathbf{g})) \\ &=\mathbf{U}\left(\mathbf{U}^{T} \mathbf{x} \odot \mathbf{U}^{T} \mathbf{g}\right) \end{aligned} $$

其中,$\mathbf{g} \in \mathbb{R}^N$ 为滤波器,$\odot$ 表示逐元素乘。假设定义一个滤波器 $\mathbf{g}_{\theta}=\operatorname{diag}\left(\mathbf{U}^{T} \mathbf{g}\right)$,则图卷积可以简写为:

$$ \mathbf{x} *_{G} \mathbf{g}_{\theta}=\mathbf{U} \mathbf{g}_{\theta} \mathbf{U}^{T} \mathbf{x} $$

基于频谱的图卷积网络都遵循这样的定义,不同之处在于不同滤波器的选择。

一些代表模型及其聚合和更新方式如下表所示:

模型 聚合方式 更新方式
ChebNet 23 $\mathbf{N}_{k}=\mathbf{T}_{k}(\tilde{\mathbf{L}}) \mathbf{X}$ $\mathbf{H}=\sum_{k=0}^{K} \mathbf{N}_{k} \mathbf{\Theta}_{k}$
1st-order model $\begin{array}{l} \mathbf{N}_{0}=\mathbf{X} \\ \mathbf{N}_{1}=\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} \mathbf{X} \end{array}$ $\mathbf{H}=\mathbf{N}_{0} \mathbf{\Theta}_{0}+\mathbf{N}_{1} \mathbf{\Theta}_{1}$
Single parameter $\mathbf{N}=\left(\mathbf{I}_{N}+\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}}\right) \mathbf{X}$ $\mathbf{H}=\mathbf{N} \mathbf{\Theta}$
GCN 24 $\mathbf{N}=\tilde{\mathbf{D}}^{-\frac{1}{2}} \tilde{\mathbf{A}} \tilde{\mathbf{D}}^{-\frac{1}{2}} \mathbf{X}$ $\mathbf{H}=\mathbf{N} \mathbf{\Theta}$

基于空间的方法(Spatial Methods)

基于空间的方法通过节点的空间关系来定义图卷积操作。为了将图像和图关联起来,可以将图像视为一个特殊形式的图,每个像素点表示一个节点,如下图所示:

每个像素同周围的像素相连,以 $3 \times 3$ 为窗口,每个节点被 8 个邻居节点所包围。通过对中心节点和周围邻居节点的像素值进行加权平均来应用一个 $3 \times 3$ 大小的滤波器。由于邻居节点的特定顺序,可以在不同位置共享权重。同样对于一般的图,基于空间的图卷积通过对中心和邻居节点的聚合得到节点新的表示。

为了使节点可以感知更深和更广的范围,通常的做法是将多个图卷积层堆叠在一起。根据堆叠方式的不同,基于空间的图卷积可以进一步分为两类:基于循环(Recurrent-based)和基于组合(Composition-based)的。基于循环的方法使用相同的图卷积层来更新隐含表示,基于组合的方式使用不同的图卷积层更新隐含表示,两者差异如下图所示:

一些代表模型及其聚合和更新方式如下表所示:

模型 聚合方式 更新方式
Neural FPs 25 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\mathbf{h}_{v}^{t-1}+\sum_{k=1}^{\mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$ $\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{h}_{\mathcal{N}_{v}}^{t} \mathbf{W}_{L}^{\mathcal{N}_{v}}\right)$
DCNN 26 Node classification:
$\mathbf{N}=\mathbf{P}^{*} \mathbf{X}$
Graph classification:
$\mathbf{N}=1_{N}^{T} \mathbf{P}^{*} \mathbf{X} / N$
$\mathbf{H}=f\left(\mathbf{W}^{c} \odot \mathbf{N}\right)$
GraphSAGE 27 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\text{AGGREGATE}_{t}\left(\left\{\mathbf{h}_{u}^{t-1}, \forall u \in \mathcal{N}_{v}\right\}\right)$ $\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{W}^{t} \cdot\left[\mathbf{h}_{v}^{t-1} \Vert \mathbf{h}_{\mathcal{N}_{v}}^{t}\right]\right)$

Graph Recurrent Networks

一些研究尝试利用门控机制(例如:GRU 或 LSTM)用于减少之前 GNN 模型在传播过程中的限制,同时改善在图结构中信息的长距离传播。GGNN 28 提出了一种使用 GRU 进行传播的方法。它将 RNN 展开至一个固定 $T$ 步,然后通过基于时间的传导计算梯度。传播模型的基础循环方式如下:

$$ \begin{aligned} &\mathbf{a}_{v}^{t}=\mathbf{A}_{v}^{T}\left[\mathbf{h}_{1}^{t-1} \ldots \mathbf{h}_{N}^{t-1}\right]^{T}+\mathbf{b}\\ &\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{a}_{v}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{a}_{v}^{t}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W} \mathbf{a}_{v}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned} $$

节点 $v$ 首先从邻居汇总信息,其中 $\mathbf{A}_v$ 为图邻接矩阵 $\mathbf{A}$ 的子矩阵表示节点 $v$ 及其邻居的连接。类似 GRU 的更新函数,通过结合其他节点和上一时间的信息更新节点的隐状态。$\mathbf{a}$ 用于获取节点 $v$ 邻居的信息,$\mathbf{z}$$\mathbf{r}$ 分别为更新和重置门。

GGNN 模型设计用于解决序列生成问题,而之前的模型主要关注单个输出,例如:节点级别或图级别的分类问题。研究进一步提出了 Gated Graph Sequence Neural Networks(GGS-NNs),使用多个 GGNN 产生一个输出序列 $\mathbf{o}^{(1)}, \cdots, \mathbf{o}^{(K)}$,如下图所示:

上图中使用了两个 GGNN,$\mathcal{F}_o^{(k)}$ 用于从 $\mathcal{\boldsymbol{X}}^{(k)}$ 预测 $\mathbf{o}^{(k)}$$\mathcal{F}_x^{(k)}$ 用于从 $\mathcal{\boldsymbol{X}}^{(k)}$ 预测 $\mathcal{\boldsymbol{X}}^{(k+1)}$。令 $\mathcal{\boldsymbol{H}}^{(k, t)}$ 表示第 $k$ 步输出的第 $t$ 步传播,$\mathcal{\boldsymbol{H}}^{(k, 1)}$ 在任意 $k$ 步初始化为 $\mathcal{\boldsymbol{X}}^{(k)}$$\mathcal{\boldsymbol{H}}^{(t, 1)}$ 在任意 $t$ 步初始化为 $\mathcal{\boldsymbol{X}}^{(t)}$$\mathcal{F}_o^{(k)}$$\mathcal{F}_x^{(k)}$ 可以为不同模型也可以共享权重。

一些代表模型及其聚合和更新方式如下表所示:

模型 聚合方式 更新方式
GGNN 28 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}+\mathbf{b}$ $\begin{aligned} &\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{h}_{\mathcal{N}_{v}}^{z}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned}$
Tree LSTM (Child sum) 29 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{U}^{i} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{U}^{o} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{U}^{u} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{u}\right)\\ &\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$
Tree LSTM (N-ary) 29 $\begin{aligned} &\mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{l=1}^{K} \mathbf{U}_{l}^{i} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v} k}^{t f}=\sum_{l=1}^{K} \mathbf{U}_{k l}^{f} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{l=1}^{K} \mathbf{U}_{l}^{o} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{l=1}^{K} \mathbf{U}_{l}^{u} \mathbf{h}_{v l}^{t-1} \end{aligned}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v},}^{t i}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v} k}^{f f}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &\mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{l=1}^{K} \mathbf{f}_{v l}^{t} \odot \mathbf{c}_{v l}^{t-1}\\ &\mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{aligned}$
Graph LSTM 30 $\begin{aligned} \mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{i} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{o} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{u} \mathbf{h}_{k}^{t-1} \end{aligned}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t i}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}_{m(v, k)}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$

Graph Attention Networks

与 GCN 对于节点所有的邻居平等对待相比,注意力机制可以为每个邻居分配不同的注意力评分,从而识别更重要的邻居。

GAT 31 将注意力机制引入传播过程,其遵循自注意力机制,通过对每个节点邻居的不同关注更新隐含状态。GAT 定义了一个图注意力层(graph attentional layer),通过堆叠构建图注意力网络。对于节点对 $\left(i, j\right)$,基于注意力机制的系数计算方式如下:

$$ \alpha_{i j}=\frac{\exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{j}\right]\right)\right)}{\sum_{k \in N_{i}} \exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{k}\right]\right)\right)} $$

其中,$\alpha_{i j}$ 表示节点 $j$$i$ 的注意力系数,$N_i$ 表示节点 $i$ 的邻居。令 $\mathbf{h}=\left\{\vec{h}_{1}, \vec{h}_{2}, \ldots, \vec{h}_{N}\right\}, \vec{h}_{i} \in \mathbb{R}^{F}$ 表示输入节点特征,其中 $N$ 为节点的数量,$F$ 为特征维度,则节点的输出特征(可能为不同维度 $F^{\prime}$)为 $\mathbf{h}^{\prime}=\left\{\vec{h}_{1}^{\prime}, \vec{h}_{2}^{\prime}, \ldots, \vec{h}_{N}^{\prime}\right\}, \vec{h}_{i}^{\prime} \in \mathbb{R}^{F^{\prime}}$$\mathbf{W} \in \mathbb{R}^{F^{\prime} \times F}$ 为所有节点共享的线性变换的权重矩阵,$a: \mathbb{R}^{F^{\prime}} \times \mathbb{R}^{F^{\prime}} \rightarrow \mathbb{R}$ 用于计算注意力系数。最后的输出特征计算方式如下:

$$ \vec{h}_{i}^{\prime}=\sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j} \mathbf{W} \vec{h}_{j}\right) $$

注意力层采用多头注意力机制来稳定学习过程,之后应用 $K$ 个独立的注意力机制计算隐含状态,最后通过拼接或平均得到输出表示:

$$ \vec{h}_{i}^{\prime}=\Vert_{k=1}^{K} \sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$

$$ \vec{h}_{i}^{\prime}=\sigma\left(\frac{1}{K} \sum_{k=1}^{K} \sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$

其中,$\Vert$ 表示连接操作,$\alpha_{ij}^k$ 表示第 $k$ 个注意力机制计算得到的标准化的注意力系数。整个模型如下图所示:

GAT 中的注意力架构有如下几个特点:

  1. 针对节点对的计算是并行的,因此计算过程是高效的。
  2. 可以处理不同度的节点并对邻居分配对应的权重。
  3. 可以容易地应用到归纳学习问题中去。

应用

图神经网络已经被应用在监督、半监督、无监督和强化学习等多个领域。下图列举了 GNN 在不同领域内相关问题中的应用,具体模型论文请参考 Graph Neural Networks: A Review of Methods and Applications 原文 19

开放资源

开源实现

项目 框架
rusty1s/pytorch_geometric PyTorch
dmlc/dgl PyTorch, TF & MXNet
alibaba/euler TF
alibaba/graph-learn TF
deepmind/graph_nets TF & Sonnet
facebookresearch/PyTorch-BigGraph PyTorch
tencent/plato
PaddlePaddle/PGL PaddlePaddle
Accenture/AmpliGraph TF
danielegrattarola/spektral TF
THUDM/cogdl PyTorch
DeepGraphLearning/graphvite PyTorch

论文列表和评测


  1. Cai, H., Zheng, V. W., & Chang, K. C. C. (2018). A comprehensive survey of graph embedding: Problems, techniques, and applications. IEEE Transactions on Knowledge and Data Engineering, 30(9), 1616-1637. ↩︎

  2. Goyal, P., & Ferrara, E. (2018). Graph embedding techniques, applications, and performance: A survey. Knowledge-Based Systems, 151, 78-94. ↩︎

  3. Hamilton, W. L., Ying, R., & Leskovec, J. (2017). Representation learning on graphs: Methods and applications. arXiv preprint arXiv:1709.05584↩︎

  4. Perozzi, B., Al-Rfou, R., & Skiena, S. (2014). Deepwalk: Online learning of social representations. In Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 701-710). ↩︎

  5. Grover, A., & Leskovec, J. (2016). node2vec: Scalable feature learning for networks. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 855-864). ↩︎

  6. Fogaras, D., Rácz, B., Csalogány, K., & Sarlós, T. (2005). Towards scaling fully personalized pagerank: Algorithms, lower bounds, and experiments. Internet Mathematics, 2(3), 333-358. ↩︎

  7. Haveliwala, T. H. (2002). Topic-sensitive PageRank. In Proceedings of the 11th international conference on World Wide Web (pp. 517-526). ↩︎

  8. Cao, S., Lu, W., & Xu, Q. (2015). Grarep: Learning graph representations with global structural information. In Proceedings of the 24th ACM international on conference on information and knowledge management (pp. 891-900). ↩︎

  9. Ou, M., Cui, P., Pei, J., Zhang, Z., & Zhu, W. (2016). Asymmetric transitivity preserving graph embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1105-1114). ↩︎

  10. Dong, Y., Chawla, N. V., & Swami, A. (2017). metapath2vec: Scalable representation learning for heterogeneous networks. In Proceedings of the 23rd ACM SIGKDD international conference on knowledge discovery and data mining (pp. 135-144). ↩︎

  11. Fu, T. Y., Lee, W. C., & Lei, Z. (2017). Hin2vec: Explore meta-paths in heterogeneous information networks for representation learning. In Proceedings of the 2017 ACM on Conference on Information and Knowledge Management (pp. 1797-1806). ↩︎

  12. Wang, D., Cui, P., & Zhu, W. (2016). Structural deep network embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1225-1234). ↩︎

  13. Cao, S., Lu, W., & Xu, Q. (2016). Deep neural networks for learning graph representations. In Thirtieth AAAI conference on artificial intelligence↩︎

  14. Tang, J., Qu, M., Wang, M., Zhang, M., Yan, J., & Mei, Q. (2015). Line: Large-scale information network embedding. In Proceedings of the 24th international conference on world wide web (pp. 1067-1077). ↩︎

  15. Walker, A. J. (1974). New fast method for generating discrete random numbers with arbitrary frequency distributions. Electronics Letters, 10(8), 127-128. ↩︎

  16. Walker, A. J. (1977). An efficient method for generating discrete random variables with general distributions. ACM Transactions on Mathematical Software (TOMS), 3(3), 253-256. ↩︎

  17. Zhang, Z., Cui, P., & Zhu, W. (2020). Deep learning on graphs: A survey. IEEE Transactions on Knowledge and Data Engineering↩︎

  18. Wu, Z., Pan, S., Chen, F., Long, G., Zhang, C., & Philip, S. Y. (2020). A comprehensive survey on graph neural networks. IEEE Transactions on Neural Networks and Learning Systems↩︎

  19. Zhou, J., Cui, G., Zhang, Z., Yang, C., Liu, Z., Wang, L., … & Sun, M. (2018). Graph neural networks: A review of methods and applications. arXiv preprint arXiv:1812.08434↩︎ ↩︎ ↩︎

  20. Liu, Z., & Zhou, J. (2020). Introduction to Graph Neural Networks. Synthesis Lectures on Artificial Intelligence and Machine Learning, 14(2), 1–127. ↩︎

  21. Scarselli, F., Gori, M., Tsoi, A. C., Hagenbuchner, M., & Monfardini, G. (2008). The graph neural network model. IEEE Transactions on Neural Networks, 20(1), 61-80. ↩︎

  22. Khamsi, M. A., & Kirk, W. A. (2011). An introduction to metric spaces and fixed point theory (Vol. 53). John Wiley & Sons. ↩︎

  23. Defferrard, M., Bresson, X., & Vandergheynst, P. (2016). Convolutional neural networks on graphs with fast localized spectral filtering. In Advances in neural information processing systems (pp. 3844-3852). ↩︎

  24. Kipf, T. N., & Welling, M. (2016). Semi-supervised classification with graph convolutional networks. arXiv preprint arXiv:1609.02907↩︎

  25. Duvenaud, D. K., Maclaurin, D., Iparraguirre, J., Bombarell, R., Hirzel, T., Aspuru-Guzik, A., & Adams, R. P. (2015). Convolutional networks on graphs for learning molecular fingerprints. In Advances in neural information processing systems (pp. 2224-2232). ↩︎

  26. Atwood, J., & Towsley, D. (2016). Diffusion-convolutional neural networks. In Advances in neural information processing systems (pp. 1993-2001). ↩︎

  27. Hamilton, W., Ying, Z., & Leskovec, J. (2017). Inductive representation learning on large graphs. In Advances in neural information processing systems (pp. 1024-1034). ↩︎

  28. Li, Y., Tarlow, D., Brockschmidt, M., & Zemel, R. (2015). Gated graph sequence neural networks. arXiv preprint arXiv:1511.05493. ↩︎ ↩︎

  29. Tai, K. S., Socher, R., & Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers) (pp. 1556-1566). ↩︎ ↩︎

  30. Peng, N., Poon, H., Quirk, C., Toutanova, K., & Yih, W. T. (2017). Cross-sentence n-ary relation extraction with graph lstms. Transactions of the Association for Computational Linguistics, 5, 101-115. ↩︎

  31. Veličković, P., Cucurull, G., Casanova, A., Romero, A., Lio, P., & Bengio, Y. (2017). Graph attention networks. arXiv preprint arXiv:1710.10903. ↩︎

预训练自然语言模型 (Pre-trained Models for NLP)

2020-03-28 08:00:00

本文为 Pre-trained Models for Natural Language Processing: A Survey 和相关模型的读书笔记 1

在当下的 NLP 研究领域,随着计算机算力的不断增强,越来越多的通用语言表征的预训练模型(Pre-trained Models,PTMs)逐渐涌现出来。这对下游的 NLP 任务非常有帮助,可以避免大量从零开始训练新的模型。PTM 大致可以分为两代:

预训练原理

语言表示学习

分布式表示的核心思想为用一个低维的实值向量表示一段文本,向量单独每个维度不具有任何实质含义,但整个向量表示了一个具体的概念。下图展示了一个 NLP 任务的一般神经网络架构:

NLP 任务的一般神经网络架构

词嵌入包含两种类型:上下文无关的词嵌入基于上下文的词嵌入。两者的不同点在于一个词的嵌入是够会随着上下文的不同而随之改变。

为了表征语义,我们需要将离散的语言符号映射到一个分布式嵌入空间中。对于词典 $\mathcal{V}$ 中的一个词 $x$,我们将其映射为查询表 $\mathbf{E} \in \mathbb{R}^{D_e \times \|\mathcal{V}\|}$ 中的一个向量 $\mathbf{e}_x \in \mathbb{R}^{D_e}$,其中 $D_e$ 为嵌入的维度。

这种类型的嵌入主要有两个缺陷:一是嵌入是静态的,词在不同的上下文中的嵌入表示是相同的,因此无法处理一词多义;二是未登录词(out-of-vocabulary,OOV)问题,通常可以采用字符级嵌入表示解决该问题。更多上下文无关的词嵌入模型,请参见之前的博客 词向量

为了解决上述问题,我们需要区分在不同上下文下词的含义。给定一段文本 $x_1, x_2, \dotsc, x_T$ 其中每段标记 $x_t \in \mathcal{V}$ 为一个词或子词,$x_t$ 基于上下文的表示依赖于整段文本。

$$ \left[\mathbf{h}_1, \mathbf{h}_2, \dotsc, \mathbf{h}_T\right] = f_{\text{enc}} \left(x_1, x_2, \dotsc, x_T\right) $$

其中,$f_{\text{enc}} \left(\cdot\right)$ 为神经编码器,$\mathbf{h}_t$ 为标记 $x_t$基于上下文的嵌入动态嵌入

神经上下文编码器

神经上下文编码器大致可以分为 3 类:

  1. 基于卷积的模型:基于卷积的模型通过卷积操作从一个词的邻居中聚合局部信息来捕获这个词的含义 8
    Convolutional model
  2. 基于序列的模型:基于序列的模型采用 RNNs(LSTM 9 和 GRU 10) 来捕获词的上下文信息。实际中,我们采用双向的 RNNs 从词的两端收集信息,不过整体效果容易收到长期依赖问题的影响。
    Sequential model
  3. 基于图的模型:基于图的模型将字作为图中的一个节点来学习上下文表示,这个图通常是一个词之间预定义的语言结构,例如:语法结构 11 12 或语义关系 13。尽管基于语言学的图结构能提供有用的信息,但如何构建一个好的图结构则成为了难题。除此之外,基于语言学的图结构需要依赖专家知识和外部工具,例如:依存句法分析等。事实上,我们会采用一个更直接的方式去学习任意两个词之间的关系,通常连接的权重可以通过自注意力机制自动计算得出。Transformer 14 是一个采用了全链接自注意力架构的实现,同时也采用了位置嵌入(positional embedding),层标准化(layer normalization)和残差连接(residual connections)等网络设计理念。
    Fully-connected graph-based model

为什么预训练

对于大多数的 NLP 任务,构建一个大规模的有标签的数据集是一项很大的挑战。相反,大规模的无标签语料是相对容易构建的,为了充分利用这些无标签数据,我们可以先利用它们获取一个好的语言表示,再将这些表示用于其他任务。预训练的好处如下:

  1. 预训练可以从大规模语料中学习得到通用的语言表示,并用于下游任务。
  2. 预训练提供了更优的模型初始化方法,有助于提高模型的泛化能力和加速模型收敛。
  3. 预训练可以当作是在小数据集上一种避免过拟合的正则化方法。

预训练任务

预训练任务对于学习语言的通用表示来说至关重要。通常情况下,预训练任务具有挑战性,同时需要大量训练数据。我们将预训练任务划分为 3 类:

  1. 监督学习,即从包含输入输出对的训练数据中学习一个由输入到输出的映射函数。
  2. 非监督学习,即从无标签数据获取一些固有的知识,例如:聚类,密度,潜在表征等。
  3. 自监督学习,是监督学习和非监督学习的混合体,核心思想是对于输入的一部分利用其他部分进行预测。

语言模型(Language Modeling,LM)

NLP 中最常见的非监督任务为概率语言建模,这是一个经典的概率密度估计问题。给定一个文本序列 $x_{1:T} = \left[x_1, x_2, \dotsc, x_T\right]$,他的联合概率 $p \left(x_{1:T}\right)$ 可以分解为:

$$ p \left(x_{1:T}\right) = \prod_{t=1}^{y}{p \left(x_t \mid x_{0:t-1}\right)} $$

其中 $x_0$ 为序列开始的特殊标记。条件概率 $p \left(x_t \mid x_{0:t-1}\right)$ 可以通过给定的语言上下文 $x_{0:t-1}$ 词的概率分布进行建模估计。上下文 $x_{0:t-1}$ 可以通过神经编码器 $f_{\text{enc}} \left(\cdot\right)$ 进行建模,则条件概率可以表示为:

$$ p \left(x_t | x_{0:t-1}\right) = g_{\text{LM}} \left(f_{\text{enc}} \left(x_{0:t-1}\right)\right) $$

其中,$g_{\text{LM}}$ 为预测层。

遮罩语言模型(Masked Language Modeling,MLM)

大致上来说,MLM 首先将输入句子的一些词条进行遮挡处理,其次再训练模型利用剩余的部分预测遮挡的部分。这种预训练方法会导致在预训练(pre-training)阶段和微调(fine-tuning)阶段的不一致,因为在微调阶段遮挡标记并未出现,BERT 15 通过一个特殊的符号 [MASK] 对其进行处理。

Sequence-to-Sequence MLM (Seq2Seq MLM)

MLM 通常以一个分类问题进行求解,我们将遮挡后的序列输入到一个神经编码器,再将输出向量传给一个 Softmax 分类器来预测遮挡的字符。我们可以采用 Encoder-Decoder(Seq2Seq)网络结构,将遮挡的序列输入到 Encoder,Decoder 则会循序的产生被遮挡的字符。MASS 16 和 T5 17 均采用了这种序列到序列的 MLM 结构,这种结构对 Seq2Seq 风格的下游任务很有帮助,例如:问答,摘要和机器翻译。

Enhanced Masked Language Modeling (E-MLM)

同时,大量研究对于 BERT 所使用的遮罩处理进行了改进。RoBERTa 18 采用了一种动态的遮罩处理。UniLM 将遮罩任务拓展到 3 种不同的类型:单向的,双向的和 Seq2Seq 类型的。

排列语言模型(Permuted Language Modeling,PLM)

在 MLM 中一些特殊字符(例如:[MASK])在下游任务中是无用的,为了解决这个问题,XLNet 19 提出了一种排列语言模型(Permuted Language Modeling,PLM)用于替代 MLM。简言之,PLM 是对输入序列的排列进行语言建模。给定一个序列,从所有可能的排列中随机抽样得到一个排列,将排列后的序列中的一些字符作为模型的预测目标,利用其他部分和目标的自然位置进行训练。需要注意的是这种排列并不会影响序列的自然位置,其仅用于定义字符预测的顺序。

去噪自编码(Denoising Autoencoder,DAE)

DAE 旨在利用部分有损的输入恢复原始无损的输入。对于语言模型,例如 Seq2Seq 模型,可以采用标准的 Transformer 来重构原始文本。有多种方式可以对文本进行破坏 20

  1. 字符遮罩:随机采样字符并将其替换为 [MASK]
  2. 字符删除:随机的从输入中删除字符,不同于字符遮罩,模型需要确定丢失字符的位置。
  3. 文本填充:采样一段文本并将其替换为一个 [MASK],每段文本的长度服从泊松分布($\lambda = 3$),模型需要确定这段文本中缺失的字符个数。
  4. 句子重排:将文档以终止标点进行分割,再进行随机排序。
  5. 文档旋转:随机均匀地选择一个字符,对文档进行旋转使得这个字符作为文档的起始字符,模型需要确定文档真实的起始位置。

对比学习(Contrastive Learning,CTL)

对比学习 21 假设一些观测到的文本对比随机采样的文本具有更相似的语义。对于文本对 $\left(x, y\right)$ 通过最小化如下目标函数来学习评分函数 $s \left(x, y\right)$

$$ \mathbb{E}_{x, y^+, y^-} \left[- \log \dfrac{\exp \left(s \left(x, y^+\right)\right)}{\exp \left(s \left(x, y^+\right)\right) + \exp \left(s \left(x, y^-\right)\right)}\right] $$

其中,$\left(x, y^+\right)$ 为一个相似对,$y^-$ 对于 $x$ 而言假定为不相似,$y^+$$y^-$ 通常称之为正样本和负样本。评分函数 $s \left(x, y\right)$ 通过一个神经编码器计算可得,$s \left(x, y\right) = f^{\top}_{\text{enc}} \left(x\right) f_{\text{enc}} \left(y\right)$$s \left(x, y\right) = f_{\text{enc}} \left(x \oplus y\right)$。CTL 的核心思想是“通过对比进行学习”。

下图展示了预训练模型的分类和部分代表模型:

预训练模型分类及代表性模型

应用于下游任务

如何迁移

选择合适的预训练任务,模型架构和语料

不同的 PTMs 在相同的下游任务上有着不同的效果,这是因为 PTMs 有着不同的预训练任务,模型架构和语料。

  1. 目前,语言模型是最流行的预训练任务,同时也可以有效地解决很多 NLP 问题。但是不同的预训练任务有着自己的侧重,在不同的任务上会有不同的效果。例如:NSP 任务使得 PTM 可以理解两句话之间的关系,因此 PTM 可以在例如问答(Question Answering,QA)和自然语言推理(Natural Language Inference,NLI)等下游任务上表现更好。
  2. PTM 的网络架构对下游任务也至关重要。例如:尽管 BERT 可以处理大多数自然语言理解任务,对其很难生成语言。
  3. 下游任务的数据分布应该和 PTM 训练所用语料相似。目前,大量现成的 PTM 仅可以快速地用于特定领域或特定语言的下游任务上。

选择合适的网络层

给定一个预训练的模型,不同的网络层捕获了不同的信息,例如:词性标记(POS tagging),语法(parsing),长期依赖(long-term dependencies),语义角色(semantic roles),指代(coreference)等。Tenney 22 等人发现 BERT 表示方式类似传统的 NLP 流程:基础的句法信息出现在浅层的网络中,高级的语义信息出现在更高的层级中。

$\mathbf{H}^{\left(l\right)} \left(1 \leq l \leq L\right)$ 表示共 $L$ 层的预训练模型的第 $l$ 层表示,$g \left(\cdot\right)$ 表示用于特定任务的的模型。一般有 3 中情况选择表示:

  1. Embedding Only:一种情况是仅选用预训练模型的静态嵌入,模型的其他部分仍需作为一个任务从头训练。这种情况不能够获取到一些有用的深层信息,词嵌入仅能够捕获词的语义信息。
  2. Top Layer:最简单有效的方式是将网络的顶层表示输入到模型中 $g \left(\mathbf{H}^{\left(L\right)}\right)$
  3. All Layers:另一种更灵活的方式是自动选择最合适的层,例如 ELMo: $$ \mathbf{r}_t = \gamma \sum_{l=1}^{L}{\alpha_l \mathbf{h}^{\left(l\right)}_t} $$ 其中 $\alpha_l$ 是层 $l$ 的 softmax 归一的权重,$\gamma$ 是用于缩放预训练模型输出向量的一个标量值,再将不同层的混合输出输入到后续模型中 $g \left(\mathbf{r}_t\right)$

是否微调

目前,主要有两种方式进行模型迁移:特征提取(预训练模型的参数是固定的)和模型微调(预训练模型的参数是经过微调的)。当采用特征提取时,预训练模型可以被看作是一个特征提取器。除此之外,我们应该采用内部层作为特征,因为他们通常是最适合迁移的特征。尽管两种不同方式都能对大多数 NLP 任务效果有显著提升,但以特征提取的方式需要更复杂的特定任务的架构。因此,微调是一种更加通用和方便的处理下游任务的方式。

微调策略

随着 PTMs 网络层数的加深,其捕获的表示使得下游任务变得越来越简单,因此整个模型中用于特定任务的网络层一般比较简单,微调已经成为了采用 PTMs 的主要方式。但是微调的过程通常是比较不好预估的,即使采用相同的超参数,不同的随机数种子也可能导致差异较大的结果。除了标准的微调外,如下为一些有用的微调策略:

两步骤微调

一种方式是两阶段的迁移,在预训练和微调之间引入了一个中间阶段。在第一个阶段,PTM 通过一个中间任务或语料转换为一个微调后的模型,在第二个阶段,再利用目标任务进行微调。

多任务微调

在多任务学习框架下对其进行微调。

利用额外模块进行微调

微调的主要缺点就是其参数的低效性。每个下游模型都有其自己微调好的参数,因此一个更好的解决方案是将一些微调好的适配模块注入到 PTMs 中,同时固定原始参数。

开放资源

PTMs 开源实现:

项目 框架 PTMs
word2vec - CBOW, Skip-Gram
GloVe - Pre-trained word vectors
FastText - Pre-trained word vectors
Transformers PyTorch & TF BERT, GPT-2, RoBERTa, XLNet, etc.
Fairseq PyTorch English LM, German LM, RoBERTa, etc.
Flair PyTorch BERT, ELMo, GPT, RoBERTa, XLNet, etc.
AllenNLP PyTorch ELMo, BERT, GPT-2, etc.
FastNLP PyTorch BERT, RoBERTa, GPT, etc.
Chinese-BERT - BERT, RoBERTa, etc. (for Chinese)
BERT TF BERT, BERT-wwm
RoBERTa PyTorch
XLNet TF
ALBERT TF
T5 TF
ERNIE(THU) PyTorch
ERNIE(Baidu) PaddlePaddle
Hugging Face PyTorch & TF 很多…

论文列表和 PTMs 相关资源:

资源 URL
论文列表 https://github.com/thunlp/PLMpapers
论文列表 https://github.com/tomohideshibata/BERT-related-papers
论文列表 https://github.com/cedrickchee/awesome-bert-nlp
Bert Lang Street https://bertlang.unibocconi.it
BertViz https://github.com/jessevig/bertviz

预训练模型

CoVe (2017) 4

首先,给定一个源语言序列 $w^x = \left[w^x_1, \dotsc, w^x_n\right]$ 和一个翻译目标语言序列 $w^z = \left[w^z_1, \dotsc, w^z_n\right]$。令 $\text{GloVe} \left(w^x\right)$ 为词 $w^x$ 对应的 GloVe 向量,$z$$w^z$ 中的词随机初始化的词向量。将 $\text{GloVe} \left(w^x\right)$ 输入到一个标准的两层 biLSTM 网络中,称之为 MT-LSTM,MT-LSTM 用于计算序列的隐含状态如下:

$$ h = \text{MT-LSTM} \left(\text{GloVe} \left(w^x\right)\right) $$

对于机器翻译,MT-LSTM 的注意力机制的解码器可以对于输出的词在每一步产生一个分布 $p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right)$。在 $t$ 步,解码器利用一个两层的单向 LSTM 基于之前目标词嵌入 $z_{t-1}$ 和一个基于上下文调整的隐含状态 $\tilde{h}_{t-1}$ 生成一个隐含状态 $h^{\text{dec}}_t$

$$ h^{\text{dec}}_t = \text{LSTM} \left(\left[z_{t-1}; \tilde{h}_{t-1}\right], h^{\text{dec}}_{t-1}\right) $$

之后解码器计算每一步编码到当前解码状态的注意力权重 $\alpha$

$$ \alpha_t = \text{softmax} \left(H \left(W_1 h^{\text{dec}}_t + b_1\right)\right) $$

其中 $H$ 表示 $h$ 按照时间维度的堆叠。之后解码器将这些权重作为相关性用于计算基于上下文调整的隐含状态 $\tilde{h}$

$$ \tilde{h}_t = \text{tanh} \left(W_2 \left[H^{\top} \alpha_t; h^{\text{dec}}_t\right] + b_2\right) $$

最后,输出词的分布通过基于上下文调整的隐含状态计算可得:

$$ p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right) = \text{softmax} \left(W_{\text{out}} \tilde{h}_t + b_{\text{out}}\right) $$

CoVe 将 MT-LSTM 学习到的表示迁移到下游任务中,令 $w$ 表示文字序列,$\text{GloVe} \left(w\right)$ 表示对应的 GloVe 向量,则:

$$ \text{CoVe} \left(w\right) = \text{MT-LSTM} \left(\text{GloVe} \left(w\right)\right) $$

表示由 MT-LSTM 产生的上下文向量,对于分类和问答任务,有一个输入序列 $w$,我们可以将 GloVe 和 CoVe 向量进行拼接作为其嵌入表示:

$$ \tilde{w} = \left[\text{GloVe} \left(w\right); \text{CoVe} \left(w\right)\right] $$

CoVe 网络架构示意图如下:

ELMo (2018) 5

在 ELMo 模型中,对于每个词条 $t_k$,一个 $L$ 层的 biLM 可以计算得到 $2L + 1$ 个表示:

$$ \begin{aligned} R_k &= \left\{\mathbf{x}^{LM}_k, \overrightarrow{\mathbf{h}}^{LM}_{k, j}, \overleftarrow{\mathbf{h}}^{LM}_{k, j} \mid j = 1, \dotsc, L \right\} \\ &= \left\{\mathbf{h}^{LM}_{k, j} \mid j = 0, \dotsc, L\right\} \end{aligned} $$

其中 $\mathbf{h}^{LM}_{k, 0}$ 为词条的嵌入层,$\mathbf{h}^{LM}_{k, j} = \left[\overrightarrow{\mathbf{h}}^{LM}_{k, j}; \overleftarrow{\mathbf{h}}^{LM}_{k, j}\right]$ 为每个 biLSTM 层。

对于下游任务,ELMo 将 $R$ 中的所有层汇总成一个向量 $\mathbf{ELMo}_k = E \left(R_k; \mathbf{\Theta}_e\right)$。在一些简单的案例中,ELMo 仅选择顶层,即:$E \left(R_k\right) = \mathbf{h}^{LM}_{k, L}$。更通用的,对于一个特定的任务,我们可以计算一个所有 biLM 层的加权:

$$ \mathbf{ELMo}^{task}_k = E \left(R_k; \Theta^{task}\right) = \gamma^{task} \sum_{j=0}^{L}{s^{task}_j \mathbf{h}^{LM}_{k, j}} $$

其中,$s^{task}$ 表示 softmax 归一化后的权重,$\gamma^{task}$ 允许模型对整个 ELMo 向量进行缩放。$\gamma$ 对整个优化过程具有重要意义,考虑每个 biLM 层的激活具有不同的分布,在一些情况下这相当于在进行加权之前对每一个 biLM 层增加了层标准化。

ELMo 网络架构示意图如下 23

GPT (2018) 6

给定一个语料 $\mathcal{U} = \left\{u_1, \dotsc, u_n\right\}$,使用标准的语言建模目标来最大化如下似然:

$$ L_1 \left(\mathcal{U}\right) = \sum_{i} \log P \left(u_i \mid u_{i-k}, \dotsc, u_{i-1}; \Theta\right) $$

其中,$k$ 为上下文窗口的大小,条件概率 $P$ 通过参数为 $\Theta$ 的神经网络进行建模。GPT 中使用了一个多层的 Transformer Decoder 作为语言模型。模型首先对输入上下文词条应用多头自注意力机制,再通过按位置的前馈层产生目标词条的输出分布:

$$ \begin{aligned} h_0 &= UW_e + W_p \\ h_l &= \text{transformer_black} \left(h_{l-1}\right), \forall i \in \left[1, n\right] \\ P \left(u\right) &= \text{softmax} \left(h_n W^{\top}_e\right) \end{aligned} $$

其中,$U = \left(u_{-k}, \dotsc, u_{-1}\right)$ 为词条的上下文向量,$n$ 为网络层数,$W_e$ 为词条的嵌入矩阵,$W_p$ 为位置嵌入矩阵。

给定一个有标签的数据集 $\mathcal{C}$,其中包含了输入词条序列 $x^1, \dotsc, x^m$ 和对应的标签 $y$。利用上述预训练的模型获得输入对应的最后一个 Transformer 的激活输出 $h^m_l$,之后再将其输入到一个参数为 $W_y$ 的线性输入层中预测 $y$

$$ P \left(y \mid x^1, \dotsc, x^m\right) = \text{softmax} \left(h^m_l W_y\right) $$

模型通过最小化如下损失进行优化:

$$ L_2 \left(\mathcal{C}\right) = \sum_{\left(x, y\right)} \log P \left(y \mid x^1, \dotsc, x^m\right) $$

研究还发现将语言建模作为微调的附加目标可以帮助提高模型的泛化能力,同时可以加速模型收敛。GPT 中采用如下的优化目标:

$$ L_3 \left(\mathcal{C}\right) = L_2 \left(\mathcal{C}\right) + \lambda L_1 \left(\mathcal{C}\right) $$

GPT 网络架构示意图如下:

BERT (2018) 7

BERT 采用了一中基于 Vaswani 14 所提出模型的多层双向 Transformer 编码器。在 BERT 中,令 $L$ 为 Transformer Block 的层数,$H$ 为隐层大小,$A$ 为自注意力头的数量。在所有情况中,设置前馈层的大小为 $4H$,BERT 提供了两种不同大小的预训练模型:

$\text{BERT}_{\text{BASE}}$ 采用了同 GPT 相同的模型大小用于比较,不同与 GPT,BERT 使用了双向的注意力机制。在文献中,双向 Transformer 通常称之为 Transformer 编码器,仅利用左边上下文信息的 Transformer 由于可以用于文本生成被称之为 Transformer 解码器。BERT,GPT 和 ELMo 之间的不同如下图所示:

BERT 的输入表示既可以表示一个单独的文本序列,也可以表示一对文本序列(例如:问题和答案)。对于一个给定的词条,其输入表示由对应的词条嵌入,分割嵌入和位置嵌入三部分加和构成,如下图所示:

具体的有:

在预训练阶段,BERT 采用了两个无监督预测任务:

  1. 遮罩的语言模型(Masked LM,MLM)
    不同于一般的仅利用 [MASK] 进行遮挡,BERT 选择采用 80% 的 [MASK],10% 的随机词和 10% 保留原始词的方式对随机选择的 15% 的词条进行遮挡处理。由于编码器不知会预测哪个词或哪个词被随机替换了,这迫使其必须保留每个输入词条的分布式上下文表示。同时 1.5% 的随机替换也不会过多的损害模型的理解能力。
  2. 预测是否为下一个句子(Next Sentence Prediction)
    一些重要的下游任务,例如问答(Question Answering,QA)和自然语言推断(Natural Language Inference,NLI)是基于两个句子之间关系的理解,这是语言建模无法直接捕获的。BERT 通过训练一个预测是否为下一个句子的二分类任务来实现,对于一个句子对 A 和 B,50% 的 B 是句子 A 真实的下一句,剩余 50% 为随机抽取的。

基于 BERT 的不同下游任务的实现形式如下图所示:

UniLM (2019) 25

给定一个输入序列 $x = x_1 \cdots x_{|x|}$,UniLM 通过下图的方式获取每个词条的基于上下文的向量表示。整个预训练过程利用单向的语言建模(unidirectional LM),双向的语言建模(bidirectional LM)和 Seq2Seq 语言建模(sequence-to-sequence LM)优化共享的 Transformer 网络。

输入序列 $x$ 对于单向语言模型而言是一个分割的文本,对于双向语言模型和 Seq2Seq 语言模型而言是一对打包的分割文本。UniLM 在输入的起始位置添加特殊的 [SOS] (start-of-sequence),在结尾处添加 [EOS](end-of-sequence)。[EOS] 对于自然语言理解(NLU)任务可以标记句子之间的界线,对于自然语言生成(NLG)任务可以确定解码过程停止的时间。输入的表示同 BERT 一样,文本利用 WordPiece 进行分割,对于每个输入词条,其向量表示为对应的词条嵌入,位置嵌入和分割嵌入的汇总。

对于输入向量 $\left\{\mathbf{x}_i\right\}^{|x|}_{i=1}$ 首先将其输入到隐层 $\mathbf{H}^0 = \left[\mathbf{x}_1, \dotsc, \mathbf{x}_{|x|}\right]$,之后使用一个 $L$ 层的 Transformer $\mathbf{H}^l = \text{Transformer}_l \left(\mathbf{H}^{l-1}\right), l \in \left[1, L\right]$ 对每一层 $\mathbf{H}^l = \left[\mathbf{h}^l_1, \dotsc, \mathbf{h}^l_{|x|}\right]$ 进行上下文表示编码。在每个 Tansformer 块中,使用多头自注意力机制对输出向量和上一层进行汇总,第 $l$ 层 Transformer 自注意力头 $\mathbf{A}_l$ 的输入通过如下方式计算:

`$$ \begin{aligned} \mathbf{Q} &= \mathbf{H}^{l-1} \mathbf{W}^Q_l, \mathbf{K} = \mathbf{H}^{l-1} \mathbf{W}^K_l, \mathbf{V} = \mathbf{H}^{l-1} \mathbf{W}^W_l \ \mathbf{M}_{ij} &= \begin{cases} 0, & \text{allow to attend} \

其中,上一层的输出 $\mathbf{H}^{l-1} \in \mathbb{R}^{|x| \times d_h}$ 通过参数矩阵 $\mathbf{W}^Q_l, \mathbf{W}^K_l, \mathbf{W}^V_l \in \mathbb{R}^{d_h \times d_k}$ 线性地映射为相应的 Query,Key 和 Value,遮罩矩阵 $\mathbf{M} \in \mathbb{R}^{|x| \times |x|}$ 用于确定一对词条是否可以被相互连接。

Transformer-XL (2019) 26

将 Transformer 或注意力机制应用到语言建模中的核心问题是如何训练 Transformer 使其有效地将一个任意长文本编码为一个固定长度的表示。Transformer-XL 将整个语料拆分为较短的段落,仅利用每段进行训练并忽略之前段落的上下文信息。这种方式称之为 Vanilla Model 27,如下图所示:

在这种训练模式下,无论是前向还是后向信息都不会跨越分割的段落进行传导。利用固定长度的上下文主要有两个弊端:

  1. 这限制了最大依赖的长度,虽然自注意力机制不会像 RNN 一样受到梯度弥散的影响,但 Vanilla Model 也不能完全利用到这个优势。
  2. 虽然可以利用补全操作来实现句子或其他语义的分割,但实际上通常会简单的将一个长文本截断成一个固定长度的分割,这样会产生上下文分裂破碎的问题。

为了解决这个问题,Transformer-XL 采用了一种循环机制的 Transformer。在训练阶段,在处理新的分割段落时,之前分割分部分的隐含状态序列将被**固定(fixed)缓存(cached)**下来作为一个扩展的上下文被复用参与计算,如下图所示:

虽然梯度仍仅限于这个分割段落内部,但网络可以从历史中获取信息,从而实现对长期依赖的建模。令两个长度为 $L$ 的连续分割段落为 $\mathbf{s}_{\tau} = \left[x_{\tau, 1}, \dotsc, x_{\tau, L}\right]$$\mathbf{s}_{\tau + 1} = \left[x_{\tau + 1, 1}, \dotsc, x_{\tau + 1, L}\right]$,第 $\tau$ 段分割 $\mathbf{s}_{\tau}$ 的第 $n$ 层隐含状态为 $\mathbf{h}^n_{\tau} \in \mathbb{R}^{L \times d}$,其中 $d$ 为隐含维度。则对于分割段落 $\mathbf{s}_{\tau + 1}$ 的第 $n$ 层隐含状态通过如下方式进行计算:

$$ \begin{aligned} \tilde{\mathbf{h}}^{n-1}_{\tau + 1} &= \left[\text{SG} \left(\mathbf{h}^{n-1}_{\tau}\right) \circ \mathbf{h}^{n-1}_{\tau + 1} \right] \\ \mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1} &= \mathbf{h}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{q}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{k}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{v} \\ \mathbf{h}^{n}_{\tau + 1} &= \text{Transformer-Layer} \left(\mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1}\right) \end{aligned} $$

其中,$\text{SG} \left(\cdot\right)$ 表示停止梯度,$\left[\mathbf{h}_u \circ \mathbf{h}_v\right]$ 表示将两个隐含序列按照长度维度进行拼接,$\mathbf{W}$ 为模型的参数。与一般的 Transformer 相比,最大的不同在于 $\mathbf{k}^n_{\tau + 1}$$\mathbf{v}^n_{\tau + 1}$ 不仅依赖于 $\tilde{\mathbf{h}}^{n-1}_{\tau - 1}$ 还依赖于之前分割段落的 $\mathbf{h}^{n-1}_{\tau}$ 缓存。

在标准的 Transformer 中,序列的顺序信息通过位置嵌入 $\mathbf{U} \in \mathbb{R}^{L_{\max} \times d}$ 提供,其中第 $i$$\mathbf{U}_i$ 对应一个分割文本内部的第 $i$绝对位置,$L_{\max}$ 为最大可能长度。在 Transformer-XL 中则是通过一种相对位置信息对其进行编码,构建一个相对位置嵌入 $\mathbf{R} \in \mathbb{R} ^{L_{\max} \times d}$,其中第 $i$$\mathbf{R}_i$ 表示两个位置之间相对距离为 $i$ 的嵌入表示。

对于一般的 Transformer,一个分割段落内部的 $q_i$$k_j$ 之间的注意力分数可以分解为:

$$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{abs}} &=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(b)} \\ &+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(d)} \end{aligned} $$

利用相对位置思想,变化如下:

$$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{rel}} &=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(b)} \\ &+\underbrace{\textcolor{red}{u^{\top}} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\textcolor{red}{v^{\top}} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(d)} \end{aligned} $$

  1. 首先,利用相对位置 $\textcolor{blue}{\mathbf{R}_{i-j}}$ 替代绝对位置嵌入 $\mathbf{U}_j$,这里 $\mathbf{R}$ 采用的是无需学习的 sinusoid 编码矩阵 14
  2. 其次,引入了一个可训练的参数 $\textcolor{red}{u} \in \mathbb{R}^d$ 用于替换 $\mathbf{U}^{\top}_i \mathbf{W}^{\top}_q$。类似的,对于 $\mathbf{U}^{\top} \mathbf{W}^{\top}_q$ 使用一个可训练的 $\textcolor{red}{v} \in \mathbb{R}^d$ 替换。
  3. 最后,有意地划分了两个权重矩阵 $\mathbf{W}_{k, E}$$\mathbf{W}_{k, R}$ 用于生成基于内容的 Key 向量和基于位置的 Key 向量。

这样,$\left(a\right)$ 代表了基于内容的位置信息,$\left(b\right)$ 捕获了内容无关的位置偏置,$\left(c\right)$ 表示了一个全局的内容偏置,$\left(d\right)$ 捕获了一个全局的位置偏置。

利用一个自注意力头计算 $N$ 层的 Transformer-XL 的过程如下,对于 $n = 1, \dotsc, N$ 有:

$$ \begin{aligned} \widetilde{\mathbf{h}}_{\tau}^{n-1}=&\left[\mathrm{SG}\left(\mathbf{m}_{\tau}^{n-1}\right) \circ \mathbf{h}_{\tau}^{n-1}\right] \\ \mathbf{q}_{\tau}^{n}, \mathbf{k}_{\tau}^{n}, \mathbf{v}_{\tau}^{n}=& \mathbf{h}_{\tau}^{n-1} {\mathbf{W}_{q}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{k, E}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{v}^{n}}^{\top} \\ \mathbf{A}_{\tau, i, j}^{n}=& {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{k}_{\tau, j}^{n} + {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ &+u^{\top} \mathbf{k}_{\tau, j}+v^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ \mathbf{a}_{\tau}^{n}=& \text { Masked-Softmax }\left(\mathbf{A}_{\tau}^{n}\right) \mathbf{v}_{\tau}^{n} \\ \mathbf{o}_{\tau}^{n}=& \text { LayerNorm } \left(\text{Linear}\left(\mathbf{a}_{\tau}^{n}\right)+\mathbf{h}_{\tau}^{n-1}\right) \\ \mathbf{h}_{\tau}^{n}=& \text { Positionwise-Feed-Forward }\left(\mathbf{o}_{\tau}^{n}\right) \end{aligned} $$

XLNet (2019) 19

给定一个序列 $\mathbf{X} = \left[x_1, \dotsc, x_T\right]$,AR 语言模型通过最大化如下似然进行预训练:

$$ \max_{\theta} \quad \log p_{\theta}(\mathbf{x})=\sum_{t=1}^{T} \log p_{\theta}\left(x_{t} | \mathbf{x}_{<t}\right)=\sum_{t=1}^{T} \log \frac{\exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x^{\prime}\right)\right)} $$

其中,$h_{\theta}\left(\mathbf{x}_{1: t-1}\right)$ 是由 RNNs 或 Transformer 等神经网络网络模型生成的上下文表示,$e \left(x\right)$$x$ 的嵌入。对于一个文本序列 $\mathbf{x}$,BERT 首先构建了一个遮罩的数据集 $\hat{\mathbf{x}}$,令被遮挡的词条为 $\overline{\mathbf{x}}$,通过训练如下目标来利用 $\hat{\mathbf{x}}$ 重构 $\overline{\mathbf{x}}$

$$ \max_{\theta} \quad \log p_{\theta}(\overline{\mathbf{x}} | \hat{\mathbf{x}}) \approx \sum_{t=1}^{T} m_{t} \log p_{\theta}\left(x_{t} | \hat{\mathbf{x}}\right)=\sum_{t=1}^{T} m_{t} \log \frac{\exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x^{\prime}\right)\right)} $$

其中 $m_t = 1$ 表示 $x_t$ 是被遮挡的,$H_{\theta}$ 是一个 Transformer 将一个长度为 $T$ 的文本序列映射到一个隐含向量序列 $H_{\theta}(\mathbf{x})=\left[H_{\theta}(\mathbf{x})_{1}, H_{\theta}(\mathbf{x})_{2}, \cdots, H_{\theta}(\mathbf{x})_{T}\right]$。两种不同的预训练目标的优劣势如下

  1. 独立假设:BERT 中联合条件概率 $p(\overline{\mathbf{x}} | \hat{\mathbf{x}})$ 假设在给定的 $\hat{\mathbf{x}}$ 下,遮挡的词条 $\overline{\mathbf{x}}$ 是相关独立的,而 AR 语言模型则没有这样的假设。
  2. 输入噪声:BERT 在预训练是使用了特殊标记 [MASK],在下游任务微调时不会出现,而 AR 语言模型则不会存在这个问题。
  3. 上下文依赖:AR 语言模型仅考虑了词条左侧的上下文,而 BERT 则可以捕获两个方向的上下文。

为了利用 AR 语言模型和 BERT 的优点,XLNet 提出了排序语言模型。对于一个长度为 $T$ 序列 $\mathbf{x}$,共有 $T!$ 种不同的方式进行 AR 分解,如果模型共享不同分解顺序的参数,那么模型就能学习到两侧所有位置的信息。令 $\mathcal{Z}_T$ 为长度为 $T$ 的索引序列 $\left[1, 2, \dotsc, T\right]$ 的所有可能排列,$z_t$$\mathbf{z}_{<t}$ 分别表示一个排列 $\mathbf{z} \in \mathcal{Z}_T$$t$ 个和前 $t-1$ 个元素。则排列语言模型的优化目标为:

$$ \max_{\theta} \quad \mathbb{E}_{\mathbf{z} \sim \mathcal{Z}_{T}}\left[\sum_{t=1}^{T} \log p_{\theta}\left(x_{z_{t}} | \mathbf{x}_{\mathbf{z}_{<t}}\right)\right] $$

根据标准的 Transformer,下一个词条的分布 $p_{\theta}\left(X_{z_{t}} | \mathbf{x}_{\mathbf{z}<t}\right)$ 为:

$$ p_{\theta}\left(X_{z_{t}} = x | \mathbf{x}_{\mathbf{z}<t}\right)=\frac{\exp \left(e(x)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)\right)} $$

其中,$h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)$ 表示通过共享的 Transformer 产生的 $\mathbf{X}_{\mathbf{Z}<t}$ 的隐含表示。该表示并不依赖于所预测的位置,为了避免这个问题,我们将位置 $z_t$ 加入到模型中:

$$ p_{\theta}\left(X_{z_{t}}=x | \mathbf{x}_{z_{<t}}\right)=\frac{\exp \left(e(x)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)\right)} $$

对于 $g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 进行建模需要满足如下两个要求:

  1. 预测 $x_{z_t}$ 时,$g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 只能使用位置信息 $z_t$ 而不能使用内容信息 $x_{z_t}$
  2. 在预测 $x_{z_t}$ 之后的词条时,$g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 又必须包含 $x_{z_t}$ 的语义信息。

为了解决这个问题,XLNet 提供了两种隐含表示:

  1. 内容隐含表示 $h_{\theta}\left(\mathbf{x}_{\mathbf{z} \leq t}\right)$,简写为 $h_{z_t}$,它和标准的 Transformer 一样,既编码上下文也编码 $x_{z_t}$ 的内容。
  2. 查询隐含表示 $g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$,简写为 $g_{z_t}$,它仅编码上下文信息 $\mathbf{X}_{\mathbf{Z}<t}$ 和位置信息 $z_t$,不编码内容 $x_{z_t}$

模型的整个计算过程如下图所示:

虽然排列语言模型有很多优点,但是由于计算量很大,模型很难进行优化,因此我们通过仅预测一个句子后面的一些词条解决这个问题。将 $\mathbf{z}$ 分为两部分:非目标子序列 $\mathbf{z}_{\leq c}$ 和目标子序列 $\mathbf{z}_{>c}$,其中 $c$ 为切分点。同时会设置一个超参数 $K$,表示仅 $1 / K$ 的词条会被预测,有 $|\mathbf{z}| /(|\mathbf{z}|-c) \approx K$。对于未被选择的词条,其查询隐状态无需被计算,从而节省计算时间和资源。

MASS (2019) 16

MASS 是一个专门针对序列到序列的自然语言任务设计的预训练方法,对于一个给定的原始句子 $x \in \mathcal{X}$,令 $x^{\setminus u:v}$ 表示将 $x$$u$$v$ 位置进行遮挡处理,$k = v - u + 1$ 为被遮挡词条的个数,$x^{u:v}$ 为从 $u$$v$ 位置被遮挡的部分。MASS 利用被遮挡的序列 $x^{\setminus u:v}$ 预测被遮挡的部分 $x^{u:v}$,目标函数的对数似然如下:

$$ \begin{aligned} L(\theta ; \mathcal{X}) &=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log P\left(x^{u: v} | x^{\setminus u: v} ; \theta\right) \\ &=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log \prod_{t=u}^{v} P\left(x_{t}^{u: v} | x_{<t}^{u: v}, x^{\setminus u: v} ; \theta\right) \end{aligned} $$

对于一个具有 8 个词条的序列,$x_3 x_4 x_5 x_6$ 被遮挡的示例如下:

模型仅预测遮挡的部分 $x_3 x_4 x_5 x_6$,对于解码器中位置 $4-6$ 利用 $x_3 x_4 x_5$ 作为输入,利用特殊遮挡符号 $\left[\mathbb{M}\right]$ 作为其他位置的输入。对于不同长度 $k$,MASS 包含了上文中提到的两种预训练模型:

长度 概率 模型
$k=1$ $P\left(x^{u} \mid x^{\setminus u} ; \theta\right)$ masked LM in BERT
$k=m$ $P\left(x^{1:m} \mid x^{\setminus 1:m} ; \theta\right)$ masked LM in GPT
$k \in \left(1, m\right)$ $P\left(x^{u:v} \mid x^{\setminus u:v} ; \theta\right)$ 两种之间

对于不同 $k$ 值,实验发现当 $k$ 处于 $m$$50\%$$70\%$ 之间时下游任务性能最优。

$k = 0.5 m$ 时,MASS 可以很好地平衡编码器和解码器的预训练。过度地偏向编码器($k=1$,masked LM in BERT)和过度地偏向解码器($k=m$,masked LM in GPT)均不能在下游的自然语言生成任务中取得很好的效果。

RoBERTa (2019) 18

RoBERTa 主要围绕 BERT 进行了如下改进:

  1. 模型采用了动态遮罩,不同于原始 BERT 中对语料预先进行遮罩处理,RoBERTa 在 40 轮训练过程中采用了 10 种不同的遮罩。
  2. 模型去掉了 NSP 任务,发现可以略微提升下游任务的性能。
  3. 模型采用了更大的训练数据和更大的 Batch 大小。
  4. 原始 BERT 采用一个 30K 的 BPE 词表,RoBERTa 采用了一个更大的 50K 的词表 28

BART (2019) 20

BART 采用了一个标准的 Seq2Seq Transformer 结构,类似 GPT 将 ReLU 激活函数替换为 GeLUs。对于基线模型,采用了一个 6 层的编码和解码器,对于更大模型采用了 12 层的结构。相比于 BERT 的架构主要有以下两点不同:

  1. 解码器的每一层叠加了对编码器最后一个隐含层的注意力。
  2. BERT 在预测之前采用了一个前馈的网络,而 BART 没有。

BART 采用了最小化破坏后的文档和原始文档之间的重构误差的方式进行预训练。不同于其他的一些去噪自编码器,BART 可以使用任意类型的文档破坏方式。极端情况下,当源文档的所有信息均丢失时,BART 就等价与一个语言模型。BART 中采用的文本破坏方式有:字符遮罩,字符删除,文本填充,句子重排,文档旋转,如下图所示:

T5 (2019) 17

T5(Text-to-Text Transfer Transformer) 提出了一种 text-to-text 的框架,旨在利用相同的模型,损失函数和超参数等对机器翻译,文档摘要,问答和分类(例如:情感分析)等任务进行统一建模。我们甚至可以利用 T5 通过预测一个数字的文本表示而不是数字本身来建模一个回归任务。模型及其输入输出如下图所示:

Google 的这项研究并不是提出一种新的方法,而是从全面的视角来概述当前 NLP 领域迁移学习的发展现状。T5 还公开了一个名为 C4(Colossal Clean Crawled Corpus)的数据集,该数据集是一个比 Wikipedia 大两个数量级的 Common Crawl 的清洗后版本的数据。更多模型的细节请参见源论文和 Google 的 官方博客

ERNIE (Baidu, 2019) 29 30

ERNIE 1.0 29 通过建模海量数据中的词、实体及实体关系,学习真实世界的语义知识。相较于 BERT 学习原始语言信号,ERNIE 直接对先验语义知识单元进行建模,增强了模型语义表示能力。例如:

BERT :哈 [mask] 滨是 [mask] 龙江的省会,[mask] 际冰 [mask] 文化名城。
ERNIE:[mask] [mask] [mask] 是黑龙江的省会,国际 [mask] [mask] 文化名城。

在 BERT 模型中,我们通过『哈』与『滨』的局部共现,即可判断出『尔』字,模型没有学习与『哈尔滨』相关的任何知识。而 ERNIE 通过学习词与实体的表达,使模型能够建模出『哈尔滨』与『黑龙江』的关系,学到『哈尔滨』是 『黑龙江』的省会以及『哈尔滨』是个冰雪城市。

训练数据方面,除百科类、资讯类中文语料外,ERNIE 还引入了论坛对话类数据,利用 DLM(Dialogue Language Model)建模 Query-Response 对话结构,将对话 Pair 对作为输入,引入 Dialogue Embedding 标识对话的角色,利用 Dialogue Response Loss 学习对话的隐式关系,进一步提升模型的语义表示能力。

ERNIE 2.0 30 是基于持续学习的语义理解预训练框架,使用多任务学习增量式构建预训练任务。ERNIE 2.0 中,新构建的预训练任务类型可以无缝的加入训练框架,持续的进行语义理解学习。 通过新增的实体预测、句子因果关系判断、文章句子结构重建等语义任务,ERNIE 2.0 语义理解预训练模型从训练数据中获取了词法、句法、语义等多个维度的自然语言信息,极大地增强了通用语义表示能力。

State-of-Art

NLP 任务的 State-of-Art 模型详见:


  1. Qiu, X., Sun, T., Xu, Y., Shao, Y., Dai, N., & Huang, X. (2020). Pre-trained Models for Natural Language Processing: A Survey. ArXiv:2003.08271 [Cs]. http://arxiv.org/abs/2003.08271 ↩︎

  2. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119). ↩︎

  3. Pennington, J., Socher, R., & Manning, C. D. (2014, October). Glove: Global vectors for word representation. In Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP) (pp. 1532-1543). ↩︎

  4. McCann, B., Bradbury, J., Xiong, C., & Socher, R. (2017). Learned in translation: Contextualized word vectors. In Advances in Neural Information Processing Systems (pp. 6294-6305). ↩︎ ↩︎

  5. Peters, M. E., Neumann, M., Iyyer, M., Gardner, M., Clark, C., Lee, K., & Zettlemoyer, L. (2018). Deep contextualized word representations. arXiv preprint arXiv:1802.05365. ↩︎ ↩︎

  6. Radford, A., Narasimhan, K., Salimans, T., & Sutskever, I. (2018). Improving language understanding by generative pre-training. URL https://openai.com/blog/language-unsupervised/↩︎ ↩︎

  7. Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2018). Bert: Pre-training of deep bidirectional transformers for language understanding. arXiv preprint arXiv:1810.04805. ↩︎ ↩︎

  8. Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1746-1751). ↩︎

  9. Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780. ↩︎

  10. Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical evaluation of gated recurrent neural networks on sequence modeling. arXiv preprint arXiv:1412.3555. ↩︎

  11. Socher, R., Perelygin, A., Wu, J., Chuang, J., Manning, C. D., Ng, A. Y., & Potts, C. (2013). Recursive deep models for semantic compositionality over a sentiment treebank. In Proceedings of the 2013 conference on empirical methods in natural language processing (pp. 1631-1642). ↩︎

  12. Tai, K. S., Socher, R., & Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers) (pp. 1556-1566). ↩︎

  13. Marcheggiani, D., Bastings, J., & Titov, I. (2018). Exploiting Semantics in Neural Machine Translation with Graph Convolutional Networks. In Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 2 (Short Papers) (pp. 486-492). ↩︎

  14. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … & Polosukhin, I. (2017). Attention is all you need. In Advances in neural information processing systems (pp. 5998-6008). ↩︎ ↩︎ ↩︎

  15. Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. In Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers) (pp. 4171-4186). ↩︎

  16. Song, K., Tan, X., Qin, T., Lu, J., & Liu, T. Y. (2019). MASS: Masked Sequence to Sequence Pre-training for Language Generation. In International Conference on Machine Learning (pp. 5926-5936). ↩︎ ↩︎

  17. Raffel, C., Shazeer, N., Roberts, A., Lee, K., Narang, S., Matena, M., … & Liu, P. J. (2019). Exploring the limits of transfer learning with a unified text-to-text transformer. arXiv preprint arXiv:1910.1068 ↩︎ ↩︎

  18. Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., … & Stoyanov, V. (2019). Roberta: A robustly optimized bert pretraining approach. arXiv preprint arXiv:1907.11692. ↩︎ ↩︎

  19. Yang, Z., Dai, Z., Yang, Y., Carbonell, J., Salakhutdinov, R. R., & Le, Q. V. (2019). Xlnet: Generalized autoregressive pretraining for language understanding. In Advances in neural information processing systems (pp. 5754-5764). ↩︎ ↩︎

  20. Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A., Levy, O., … & Zettlemoyer, L. (2019). Bart: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. arXiv preprint arXiv:1910.13461. ↩︎ ↩︎

  21. Saunshi, N., Plevrakis, O., Arora, S., Khodak, M., & Khandeparkar, H. (2019). A Theoretical Analysis of Contrastive Unsupervised Representation Learning. In International Conference on Machine Learning (pp. 5628-5637). ↩︎

  22. Tenney, I., Das, D., & Pavlick, E. (2019). BERT Rediscovers the Classical NLP Pipeline. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics (pp. 4593-4601). ↩︎

  23. 图片来源:http://www.realworldnlpbook.com/blog/improving-sentiment-analyzer-using-elmo.html ↩︎

  24. Wu, Y., Schuster, M., Chen, Z., Le, Q. V., Norouzi, M., Macherey, W., … & Klingner, J. (2016). Google’s neural machine translation system: Bridging the gap between human and machine translation. arXiv preprint arXiv:1609.08144. ↩︎

  25. Dong, L., Yang, N., Wang, W., Wei, F., Liu, X., Wang, Y., … & Hon, H. W. (2019). Unified language model pre-training for natural language understanding and generation. In Advances in Neural Information Processing Systems (pp. 13042-13054). ↩︎

  26. Dai, Z., Yang, Z., Yang, Y., Carbonell, J. G., Le, Q., & Salakhutdinov, R. (2019, July). Transformer-XL: Attentive Language Models beyond a Fixed-Length Context. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics (pp. 2978-2988). ↩︎

  27. Al-Rfou, R., Choe, D., Constant, N., Guo, M., & Jones, L. (2019). Character-level language modeling with deeper self-attention. In Proceedings of the AAAI Conference on Artificial Intelligence (Vol. 33, pp. 3159-3166). ↩︎

  28. Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners. URL https://openai.com/blog/better-language-models/↩︎

  29. Sun, Y., Wang, S., Li, Y., Feng, S., Chen, X., Zhang, H., … & Wu, H. (2019). Ernie: Enhanced representation through knowledge integration. arXiv preprint arXiv:1904.09223. ↩︎ ↩︎

  30. Sun, Y., Wang, S., Li, Y., Feng, S., Tian, H., Wu, H., & Wang, H. (2019). Ernie 2.0: A continual pre-training framework for language understanding. arXiv preprint arXiv:1907.12412. ↩︎ ↩︎

ToB 产品用户权限 (User Privileges of ToB Products)

2019-11-24 08:00:00

用户权限在产品中是一个最基础的功能,它决定了用户在产品中能做些什么。虽然用户对权限的感知并不是很明显,但作为一个系统的基础功能,用户权限可以说会影响产品设计和实现的方方面面。对于不同类型的产品,用户权限的设计也略有差异,ToC 产品中用户之间相对独立,所需要考虑的问题会比 ToB 产品简单不少,本文仅针对 ToB 产品的用户权限给出一些思考。

Role-Based Access Control

RBAC (Role-Based Access Control,基于角色的访问控制) 是一种已经广泛应用于各种管理系统的权限管理模型 1。在 RBAC 中,操作权限与角色之间建立关联,再通过在角色与用户之间建立关联来完成对用户的授权,大大提高了权限控制的灵活性。在 RBAC 中包含几个关键的概念:

资源:所有可以访问的对象,从交互角度可以理解为:页面,按钮等一切可以操作的对象,从后台接口角度可以理解为:以 Rest 接口为例,资源即为每个 URI。
权限:获取资源的方式,简单理解即为资源的增删改查。
角色:一个授权等级的工作职位或职称。
用户:一名使用者或自动代理人。

资源,权限,角色和用户之间的关系如下图所示:

资源-权限-角色-用户关系

「资源」和「权限」之间为多对多的关系,「权限」和「角色」之间为多对多的关系,「角色」和「用户」之间亦为多对多的关系。

RBAC0

RBAC0 为 RBAC 中最简单最基础的权限模型,包含了用户,角色,权限之间的关系,如下图所示:

RBAC0

用户,角色和权限之间都可以是多对多的关系,在系统分工简单权限清晰明确的情况下,用户和角色之间也可能是多对一的关系。

RBAC1

RBAC1 在 RBAC0 的基础上引入了角色继承的概念,添加了「子角色」,上级角色可以继承下级角色所有的权限。

RBAC1

例如,一个公司的不同人力资源副总监分管不同的职能,因此具有不同的权限,而人力资源总监则应该具有所有人力资源副总监的权限。同时,人力资源总监也可能具有其他人力资源副总监都不具有的权限。

RBAC2

RBAC2 在 RBAC0 的基础上增加了一些限制,引入了 SSD (Static Separation of Duty,静态职责分离) 和 DSD (Dynamic Separation of Duty,动态职责分离)。

SSD 主要应用于用户和角色之间,主要的约束包括:

DSD 主要应用于会话和角色之间动态地限制用户及其拥有的角色和相应的权限。例如:一个用户在系统中拥有两个不同的角色,但在使用系统时只能激活其中一个角色。

RBAC3

RBAC3 是 RBAC1 和 RBAC2 的合集,既包含了角色分层也包含了相关约束。

权限扩展

接下来我们探讨在 RBAC 的基础上在真实的 ToB 产品中又有哪些权限扩展,这里有时我会以「种植」业务的 SaaS 为例进行简略分析。我们将权限又分为「功能权限」和「数据权限」两个部分,如下图所示:

功能权限和数据权限

功能权限就是指我们具体能够干些什么,例如:育苗,除草,施肥等等。数据权限是指我们是对谁做这些事情,例如:是对北京的实验大棚除草,还是河北的生产大棚施肥。

ToB 产品的销售对象并非个人,而是一个组织,在此我们引入「组织」的概念:

组织:用户所隶属的单位,从产品销售角度限定了用户所能使用的功能。

对于一个组织,我们设置有「功能池」和「数据池」。功能池即组织购买的产品的功能集合,一个组织的用户所能够使用的功能受限与此,并由组织的系统管理员分配功能池中的功能。数据池即组织自己的相关数据集合,由组织的管理员对不同的员工进行分配。

组织功能池和数据池

对于功能权限和数据权限,均为一个树状的层级关系,以功能权限为例,层级如下图所示:

功能权限层级关系树

功能的层级和菜单的层级有一定的对应关系,层级不易过深,这部分将在下文菜单部分详细介绍。数据的层级则需要根据产品涉及的具体业务做出相应的规范要求,例如:「根」-「种植场」-「种植区」-「种植大棚」-「种植地块」。同时,为了方便层级的扩充,在实现层面上不建议提前把所有层级固定死,可以采用父子层级的形式循环关联。

权限分配

在介绍如何根据上述设计进行权限分配前,我们再引入一个概念,「用户组」:

用户组:具有相同权限的一组用户。

当一个组织中有大量用户需要具有相同的权限时,为了避免给每个用户分配权限导致的繁琐操作,可以将这些具有相同权限的用户归入一个用户组。对该用户组赋予一定的权限,则该用户组下面的所有用户自动具有相应权限,当需要取消其中某些用户权限时,仅需要将其从该用户组中移除即可。理论上,「用户」和「用户组」以及「用户组」和「角色」之间都可以是多对多的关系。

下图为一个最细力度的权限分配示意图:

权限分配

首先,我们暂时限定一个用户仅能够隶属于一个组织,因此该用户所能够具有的最多的权限即为该组织所购买的产品的功能权限和组织自己的全部数据权限。

其次,一个最基本的权限应该是由一个功能权限对应一个数据权限构成的。这里面的功能权限和数据权限可以是任意一个级别的,同时当具有高级别的功能权限或数据权限后,子级别的相应权限自动获得。

最后,权限、角色、用户组和用户之间均可以是多对多的关系。

虽然通过很多概念的抽象使得权限管理变得相对简单,但是由于理论上支持各种细粒度的操作,在一定程度上又会加重权限管理的运维成本。在一个 ToB 产品的前期,组织的管理员可能对整个系统不是很熟悉,这样权限管理的工作会落到内部管理员的头上。当随着产品的不断发展,组织和用户的量级不断变大,内部管理员仅需为每个组织开通一个组织管理员并赋予该组织的所有权限即可,该组织内部的权限管理则交由组织的管理员进行运维即可。

菜单,页面和组件

以上我们都是从业务逻辑的角度解释权限控制,接下来我们从用户交互的角度出发简单阐述一下权限控制在用户交互上的体现。下图为一个 ToB 产品的原型界面:

菜单,页面和组件

在这个界面中,我们粗略的将其中的元素划分为三大块:「菜单」,「页面」和「组件」。这个划分并不是很准确,甚至可以说一定程度上概念之间有重叠,这样划分仅仅是为了更好的对应到上文涉及的相关概念上。

上文中我们提到一个角色的权限可以是任何一个级别的功能权限和数据权限的组合,同时权限、角色、用户组和用户之间都可以是多对多的关系,在此我们假设后台已经通过查询获得到了一个用户的所有权限。

菜单

菜单对应到主要是功能权限,但不是所有的功能权限都会以菜单的形式展现出来,因此我们可能还需要维护一套菜单的层级关系以及同功能权限的对应关系,或者我们直接利用功能权限的层级关系,但需要在里面补充上是否作为菜单显示的属性。具有不同权限的用户看到的菜单也会是不一样的。

在菜单设计时,建议的原则是层级不要过深,过深的话从交互上就会导致你的菜单像极了「箭头型代码」

箭头型代码

理论上,所有包含子功能的父功能都不代表一个具体的操作,一般仅仅是为了维护一个分组而创建的。

页面

页面并不是浏览器里面的整个页面,这里我们特指不包含菜单的部分。当我们单击一个功能菜单时,对应的页面则会相应的显示出来。当然,我们知道单击按钮 (下文会提到的组件) 也可能会打开一个新的页面,这种情况我们在下文中再展开说明。当我们单击了一个叶子界别的菜单后,其实我们已经使用了一个权限,一般情况下对应的是对一个资源的查询。

组件

页面中包含了操作的结果信息,一般情况下这些信息是不可交互的,同时页面中也包含了大量可以交互的地方,这里我们称其中的部分为组件,例如:按钮等。当然还有一些被我们在通常情况下也称之为的组件的元素,例如:输入框,下拉框等等,虽然他们也有交互功能,但是一般情况下他们并不会触发一个权限操作,因此这里我们就先将组件特指那些可以产生权限操作的元素。

一个页面可以包含多个组件,也就是说一个页面并不一定仅对应一个权限操作,可能是多个权限操作的集合。所以简单的说我们可以将功能权限设置的比菜单多一个级别,功能权限的倒数第二级对应叶子菜单,功能权限的最后一级对应页面上的组件。

权限运维

权限的精细化管理和运维成本是相互矛盾的,想更加精细化的管理权限必然会增加运维成本。因此一个 ToB 的产品到底使用什么力度的权限管理更加合适呢?我想这没有一个准确的答案,从个人角度给出两点有参考意义的设计原则:

  1. 客户需求。ToB 产品吗,最终是要拿来卖的,跪添甲方爸爸就好,甲方爸爸的需求就是合理的需求,无需质疑。说的有点过了,意思其实就是尽可能满足合理客户需求的前提下,制定合适的权限管理系统。
  2. 成本合算。有的时候客户也不是很清楚到底需不需要精细化的权限管理,那么问题就又抛回给了产品经理,那么我们需要考虑更多的是当下的实现成本和后期的改造成本。这是一个很现实的考虑,如果业务 KPI 压力比较大,有时候我们宁可承担更高的改造成本也会先实现一套简单的权限管理先用着。但是我个人认为,在研发资源相对充足的前提下还是尽量将权限系统设计完善,因为这是一个系统很底层的部分,甚至会影响后续所有的业务功能逻辑的设计和实现。

权限运维是一个很耗费人力的工作,解决这个问题没有太好的途径。提高用户的交互体验,编写简单易懂的使用说明书,只有将一个组织的权限管理交到组织的管理员手里。才能够真正解放内部管理运维人员的工作时间。


  1. Ferraiolo, David, Janet Cugini, and D. Richard Kuhn. “Role-based access control (RBAC): Features and motivations.” Proceedings of 11th annual computer security application conference. 1995. ↩︎

京东数科 HIPO 学习之旅 (JDD HIPO Learning Journey)

2019-10-28 08:00:00

今年有幸参加了京东数科的 HIPO 培训课程,整个学习之旅可谓收货满满。由于出差没能参加 8 月份的开营仪式,看到小伙伴们各种拓展,还能自己做饭,感觉错过了展示范小厨的大好机会。同样也错过了报名的「领导力」的课程,最终换到了「情商」和「沟通」两门课程学习。

情商

自认为不是一个情商很高的人,果然开课前利用 6 秒钟情商 1 评测 (SEI) 评测之后的结果不甚理想。6 秒钟情商模型将情商划分为 3 个阶段:自我认知,包括:认识情绪、识别情绪模式;自我选择,包括:运用因果思维、驾驭情绪、运用内在动力、修炼乐观思维;自我超越,包括:增强同理心、追求超我目标。

6 秒钟情商

在自我认知的认识情绪和识别情绪模式上仍处于起步阶段,其他部分基本处于稳定阶段和熟练阶段。这完全符合我对自己情商的认知,不善于发现别人情绪的变化,一旦发现还是能够用自己擅长的理性思维解决问题的。不过认识情绪是整个环节的第一步,如果这一步都不能做得很好,就算后面能力不错,也很难成为一个高情商的人。

情绪是很容易变向极端,但比较难恢复平静的,下图就是我们测试自己当前的情绪状况,颜色越紫表示越紧张压力越大,颜色越蓝表示越平静。

情绪测试

课程上学习到了一个很好用的情绪管理的工具,TFA 卡片。TFA 卡片是一个实践情绪管理的工具,让我们尝试从T (Think,思考),F (Feel,感受) 和 A (Act,行动) 三个角度对一个事件进行复盘实践。

TFA 卡片

情商课程让我认识到了情绪是应该去管理的,而不是去控制的,因为控制的情绪总会有爆发的一天。希望通过坚持不断地实践,努力成为一个高情商的人。

沟通

在我的认知范围内,只要将事情表达清楚就可以达成高效的沟通,然而现实并不是这样的,沟通课程告诉了我更多需要注意的地方。首先通过一个 DISC 模型 2 从两个维度将人的特质划分为四种,两个维度分别是:更关注事物本身还是人际关系和倾向快 (直接) 还是慢 (间接)。

DISC

不同特质的人的性格也截然不同:

特质 特点
D - 掌控型 爱冒险的、有竞争力的、大胆的、直接的、果断的、创新的、坚持不懈的、问题解决者、自我激励者
I - 影响型 有魅力的、自信的、有说服力的、热情的、鼓舞人心的、乐观的、令人信服的、受欢迎的、好交际的、可信赖的
S - 沉稳型 友善的、亲切的、好的倾听者、有耐心的、放松的、热诚的、稳定的、团队合作者、善解人意的、稳健的
C - 严谨型 准确的、有分析力的、谨慎的、谦恭的、圆滑的、善于发现事实、高标准、成熟的、有耐心的、严谨的

通过测评分析,我是一个内在 C/S,外在正在向 D/S 转变的特征的人。评测结果也很符合当下的我,之前作为算法工程师更习惯用数据说话,但现在向一个产品经理的方向转变,需要更多的掌控和沟通。

沟通课程

C 类特质的人有严谨的优势,但这个优势也需要根据不同的场景进行适当地调整,这样才能够避免让自己的优势变成劣势,反而影响与他人沟通的效率。在沟通的过程中,有时候大家的最终目标其实相差并不是很远,只是不同类型特质的人看待事情的角度不尽相同而已。如果在沟通过程中大家不考虑自己和沟通者的不同性格特质的话,就很容易触碰到大家的一些敏感区域,从而造成无效的沟通。因此无论是对上,对下还是平级沟通,我们都需要根据沟通者的性格特质采用相应的沟通技巧,进而高效地达成我们的沟通目的。

库布其沙漠 50 公里挑战

课程的最后安排了一个终极挑战「库布其沙漠 50 公里挑战」,对于我这个还比较喜欢出去浪的人是一定不能错过的。比赛之前我们做了些功课,我作为整个队伍的领航员走在最前面负责探路和制定路线,队长则负责在最后面压阵避免有同学掉队。组内的男生分担了两位女生的负重,当然最感动的是两位小姐姐帮我们买了鞋垫神奇 - 姨妈巾。

我们一路穿越山丘,峡谷和小河,还有我们巨帅的越野保障车。

挑战赛一共两天的行程,第一天路线比较固定,大家基本拼的就是体力和团队凝聚力了,队里的两位小姐姐超级给力,一路我们都保持了很好的队形,最终拿下了第一天赛程的冠军,领先第二名 20 多分钟。第二天赛程给定的建议路线有一个折线,前一晚我们计划选择走更近的直线,但中途被呼叫让我们走回预定线路上去。虽然我们已经提前确定规则并没有要求必须走预定线路,但是由于对讲机里不断呼叫,我们还是回到了预定路线上去。但比赛最后才发现,只有我们回到了预定路线上,其他队伍并没有回去,所以第二赛段我们只拿到了第二,被昨天的第二名领先了 15 分钟。

庆幸的是两个赛段的总成绩我们还是第一名,当然第一名也还是要有付出的,颁奖仪式上我们用奖杯每人豪饮了一大碗啤酒,足足有一瓶多的一碗。整个比赛下来我感觉三点很重要:目标清晰,分工明确,齐心协力。我们在一开始就朝着冠军的奖杯出发的,我负责领航,队长负责断后,所有人相互帮助,一同朝着目标努力前进。当然,在规则允许的情况下,再多一分坚持己见就更好了。

生活因你而火热

最后放一下京东数科的 MV 吧,我有出境 6 秒哦 (02:27 - 02:32),没错,打哈欠和撸猫的就是我,相信我,养猫一定比养猪容易,还有种菜 😃。

杭州和东京之旅 (Tour of Hangzhou and Tokyo)

2019-10-14 08:00:00

换完工作方向后的第一个假期,决定好好调整一下,休个小长假,浪完这一圈好收收心,再全身心地投入到后面的工作中去。之前一直也没有陪家里人出去玩玩,父母年岁不小了,所以选择避开十一前面,四号错峰到了杭州,妥妥的人少了不少。

游西湖的时候下着零星的小雨,烟雨朦胧中的西湖别有一番韵味。想起有一日北京大雨,开完会坐在出租车上还哼起了“西湖的水,我的泪,我情愿和你化作一团火焰…..”。

陪着家人玩儿完,自己一个人跑去了日本,因为剩下的年假也不长了,就选择只停留在东京先。下了飞机,打车到酒店,我擦,出租车你敢不敢再贵点儿。好吧,我承认出来之前没做任何攻略,佛系浪吧 😂。

死贵的出租车

回程本想做地铁去机场的,结果各种原因又只能打车,就眼瞅着计价器越跳越多,越跳越多 😱,后面再吐槽这一段经历。住在了新宿区歌舞伎町附近,夜晚的新宿很是热闹,不得不说东京的住宿也是贵的不要不要的。

新宿区的夜晚

没有攻略就前一天晚上简单的查了查,第一天到了必打卡的浅草寺,顺着上野公园,又去到了东京国立博物馆。

天气还是很给力的,第二天依旧闲逛,在东京大学里面转了一圈愣是没转到安田讲堂那边,不过在三四郎池旁边抓拍到应该是一只小蜥蜴吧。

没攻略果然还是有问题的,想去晴空塔却发现距离第一天的浅草寺很近,无奈就只能朝着那个方向又跑了一遍。之前有朋友晚上去过,风景不错,我选择了白天上去,稍微有些雾蒙蒙的,视野还是很开阔的。

住的附近搜到了一家武士博物馆,下午回来早了就过去看了看,到的时间刚刚好,前面的一波英文讲解刚刚开始。小姐姐讲到刀柄上的文字时,说到日本的文字主要源自自己,英文和中文,她说她也看不懂刀柄上的文字,我过去一瞧发现这不就是繁体中文吗 😏,瞬间感觉到我中华语言和文化的优秀。最后还体验了一把武士对决的表演,感觉人家才是武士,我顶多也就是个浪人 😅。

来了日本还有的就是买买买,给自己买,给家人买,给朋友带,总之各种买买买。计划预留了一天的时间去购物,谁知道玩着玩着就听说台风“海贝思”要来了,据说还是近期最大的一次台风,然后 12 号各种地方就都关门了,只能老老实实地呆在酒店里。傍晚还感觉到房子晃了一下,后面查了还真的是有地震,各种情况就这样都被赶上了。

台风前紫色的天空

台风前天空都变成紫色的了,自己没照到,网上盗图一张。本来还很庆幸自己是 13 号下午的飞机回来,台风应该影响不大,谁知道 13 号退完房才发现航班被取消了,各种客服电话都打不进去,无奈我就只能赶紧打车到机场去看看。Anyway,经过各种买票,退票,改签终于还是赶上了 13 号的一班飞机回来。

第一次一个人出来浪还是很舒服的,自由,想去哪儿就去哪儿,唯一不爽的就是没人帮忙照相。

国际智慧温室种植挑战赛 (International Autonomous Greenhouse Challenge)

2019-09-21 08:00:00

国际智慧温室种植挑战赛 是一个由 瓦赫宁根大学研究中心 (Wageningen University & Research) 主办的旨在利用自动化、信息技术和人工智能技术控制温室以实现增加产量、降低成本等目标的大赛。第一届赛事的种植作物为黄瓜,第二届赛事为樱桃西红柿。

很幸运能够在晚些时候加入到 CPlant 队伍中一同参与到这次赛事,虽然加入到队伍中比较晚,但工作之余也参与了大部分赛事的准备工作。

整个赛事分为初赛和复赛两个部分,初赛采用 Hackathon 的形式通过仿真模拟进行,初赛晋级的队伍将会在后续 6 个月的时间内通过远程控制进行真实的作物种植比赛。本次赛事吸引了全球顶级的农业与 AI 领域的企业、大学和研究机构参与,组成来自 26 个国家的 21 支团队,超过 200 名专家与学生。

初赛黑客马拉松评分主要由三部分组成:团队构成 (20%)、人工智能方法(30%),以及虚拟西红柿种植净利润(50%)。仿真部分,采用了 Venlo 类型的温室,模拟时间从 2017/12/15 日至 2018/06/01,荷兰本地的外部天气,整个模拟过程并未考虑病虫害问题 (主要受到湿度影响)。仿真模型包含三个子模型:

  1. Kaspro 温室模型
  2. Intkam 作物模型
  3. 经济模型

Kaspro 温室模型:主要通过温室的控制器 (例如:通风口,加热管道,CO2 补充器,遮阳帘,灌溉系统等) 控制温室内的环境变量 (例如:光照,温度,湿度,CO2 浓度,水量,水 EC 值等),进而控制作物生长。环境控制模型是相对复杂的一个模型,因为控制器和环境变量之间并不是一对一的关系。

Intkam 作物模型:主要通过设置茎的密度,叶片的去留策略,去顶时间,果实个数保留策略等控制作物的生长。

经济模型:主要定义了不同时间、不同果重、不同糖分樱桃番茄的价格,不同时间段内光照、加热和 CO2 的成本,以及相关的人工成本。

最终经过 24 小时的 Hackathon,我们队伍的成绩如下,最后排名 9/21,很遗憾未能进入到决赛。

Team Composition (20%) Strategy and AI Approach for the Growing Challenge (30%) Obtained Points Following Rankings in Hackathon (50%) Obtained Final Results in Hackathon (Net Profit) Total Score
15.6
(Ranking 6/21)
(Max: 17.6)
(Min: 7.6)
21.6
(Ranking 4/21)
(Max: 23.1)
(Min: 4.8)
21
(Ranking 9/21)
(Max: 50)
(Min: 1)
92.0
(Ranking 9/21)
(Max: 154.5)
(Min: 0.7)
58.2
(Ranking 9/21)
(Max: 88.8)
(Min: 13.4)

所在的 CPlant 队伍是本次比赛中人数最多的一只队伍 (21 人,最少的队伍为 5 人,虽然人最多却未能进入决赛 😥),评审从国籍,研究和企业组成等多个角度对团队构成进行了评分,最终我们拿到了一个中等偏上的成绩。

人工智能方法方面是我们在准备过程中讨论比较多的内容,每个人根据自己的优势不同分别负责了 Plant Growth Model, Machine Learning, Deep Learning, Reinforcement Learning 和 Knowledge Graph 等不同部分的设计。答辩过程中多位评委对于我们的 Knowledge Graph 在整个人工智能中的应用很感兴趣,在最后点评中也提到我们是唯一一只提到 Knowledge Graph 及其在智慧农业中应用的队伍。我认为智慧农业不同于其他人工智能应用领域分支,其具有一定的特殊性,数据和实验并不像其他领域容易获取和实现,我们需要更多地结合农业科学本身的相关经验和知识。由于我之前从事过 NLP 和 Knowledge Graph 相关工作,我深信 Knowledge Graph 一定会是一个将农业和人工智能有机地结合起来的好工具,但至于如果结合和实现落地还需要进一步探索和研究。最终这部分我们拿到了一个相对不错的成绩。

分数占比最多的仿真部分我们做的有所欠缺,同时这也是我们最为陌生的一个部分。整个 Hackathon 从当地时间 12 日 13 时开始,至 13 日 13 时结束,我们通宵达旦,一整夜的 Coding 陪伴我们度过了中秋佳节。整个过程中我们几乎将全部的精力投入到了 Kaspro 温室模型参数的优化中来,Intkam 作物模型则是根据相关的农业经验进行了简单的优化配置,经济模型并没有直接的控制参数,而是通过相关投入和产出进行计算得到。通过不断的优化,净收益从 10 几分不断提高到 80 几分,后面则一直卡在了 80 几分未能进一步提高。整个 Hackathon 过程中,组委会不定时地公布一些不包含具体组名的成绩统计信息,在第一天白天就已经有队伍拿到了接近 100 分的成绩,在半夜的一次公布中有队伍已经拿到了接近 120 分的净收益。面对巨大的压力,我们仍不断地优化 Kaspro 温室模型参数,虽然成绩在稳步提高,但提高的幅度甚微。在临近比赛的时候,我们终于决定在 Intkam 作物模型做一些大胆的尝试,设置了一些现实中绝对不可能达到的参数,居然取得了很高的提升。在最后 10 几分钟内我们将成绩又提高了 10 分左右,但由于时间限制我们未能来得及进一步调整测试。

所有队伍的 Net Profit 和 Points 成绩从大到小排列结果如下:

Net Profit & Points Result

一些与现实相差很远的参数设置却能够得到一个更好的结果,这个问题我们在最开始确实没有敢想。但其实开赛前的技术文件中有提及,整个模拟就是一个黑盒游戏,并没有任何规则可言,最终的评判准则只有净收益。虽然仿真模型与现实会有些差距,但对于这个单纯的游戏而言,先入为主的种植经验确实限制了我们的想象。而对于我这个正在朝着产品经理发展的野生程序猿而言,我正需要的就是这种想象和实践想象的能力,让我以胡适先生的一段话总结这次赛事的经验教训吧:

大胆的假设,小心的求证。 – 胡适

💪 壮志未酬,来年再战!💪

记忆中的儿时 (My Childhood in Memory)

2019-07-28 08:00:00

年中自己做出了一个重大的转变,担搁了不少时间,也没太多心情能静下心来读读写写。现在开始了新的征程,本想先总结总结前段的生活和这个我认为很重要的转变,但或许是这段时间想多了,前后也想得远了,回忆到很多小时候的事儿。开始读上《龍應台的香港筆記》,读着读着也很突然地就想起了童年,真的也很难说上有什么关系,但就是突然想起来了。

然后,也就这几天,突然又听到了刘昊霖的「儿时」,之前有听过但没怎么注意,偏偏这个时候再一听,感觉所有儿时的东西就一股脑都蹦出来了。

能回忆起的最早的画面是小时候妈妈给买的小蛋糕,找了很久才找到这种记忆中蛋糕的照片,蛋糕里好像就只有一层奶油。

蛋糕

说实话记不起来是生日还是过年了,因为生日也在冬天,只是模糊地记得和妈妈冬天里赶集的时候好像也没有和妈妈开口要,但妈妈却给买了,当时应该真的是很开心。妈妈管自己比较严,从来都不太敢和妈妈要买什么,实在想要什么的时候我就会说如果我考了前几名能不能给我买个什么呀?

父母的文化水平不高,尤其是妈妈,就上了一段时间的小学,但妈妈却对我们姐弟俩在学业上要求很高。或许他们知道自己没有能够接受很好的知识教育,反而想着不能也让孩子和他们一样。爸爸不善言辞,但经常会带着我出去玩,暑假总能够在前几天就把作业写完,然后爸爸就骑着大梁自行车,我坐在大梁上,带着我去捞小鱼,去摘酸枣,真的很开心,无忧无虑。

小时候住在村里的瓦房中,一个前院,还有个后院,家里有好多的果树:梨树,葡萄树,李子树,樱桃树,柿子树,后院的黄李子真的很好吃,酸酸甜甜的。前院有一大株橙色的百合花,印象中还有一张穿着塑料凉鞋和妈妈的合照,妈妈的样子一直没怎么变,只是现在头发已经花白。整个童年都是在这个院子里面度过的,现在村里的老院子盖成了门市租了出去,爸妈现在也不在村子里住了,自己就更少回去看看了。都说物是人非,现在连老院子也变了,剩下的就只有印象中的东屋,西屋还有窗或地下1

姐姐大我九岁,没太多印象和姐姐一起玩,但还记得好几次和姐姐打架。有一次姐姐不小心把自己从炕上推到了地上,是真的疼了,记得自己哭得比较厉害,妈妈说了姐姐。长大后回家,一家人聚在一起回忆旧时光,聊到这里,小外甥女还问我说:我妈那天把你推到地上疼吗?现在想来感觉前脚还在和姐姐打闹,后脚姐姐的孩子都上初中了。姐姐一直对我都很好,小时候播的是四驱小子,姐姐在读师范,放假回来给我带了一辆绿色的「燃烧太阳」,超级开心。

燃烧太阳

那时候的玩伴是斜对门奶奶家的外孙,当时村里的化肥厂还在,他读工厂的子弟学校,我读村里的小学。周末的时候他一般会回来,那会儿感觉最爱玩土,什么用沙子搭个城堡啦,用黄土做个小笔筒啦,还像模像样的用火烧一烧。家的旁边是村委会,里面有个大院子,那里就成了我们最大的根据地。院子里散落着一些不知道干什么用的设备器材,可以爬上爬下。当时我们还发明了一种叫做「闯关」的游戏,就是我俩假设出一关一关的剧情,然后就假装一关一关的完成任务。现在想想这不就是赤裸裸的意淫吗,不过那时候我们真的是玩得不亦乐乎。

回忆儿时的无忧无虑和欢声笑语,其实是对那种生活的向往。现在长大了,对自己的未来有了期许,也自然有了压力,遇见了更多的人和事,难免会有不对付和烦躁。停下来回味回味,感觉两个事很重要:「简单」和「感恩」。简单点,不会太累,很多时候很多事其实也没想的那么复杂,简单些反而迎刃而解,我想这就是童年快乐的源泉吧。多感恩,年轻气盛时老是记得别人的不好,现在反而更多地去发现人与事好的一面。一路走来,能相识相知都是很大的缘,感恩家人、朋友以及一路走来遇见的人和事,正是他们让自己的生活变得独一无二,也正是他们让我们有的回忆,值得回忆。


  1. 唐山话,老家的房子一般就是两间卧室在两边,中间算是大厅,做饭,吃饭都在这,我们就叫这儿窗或地下。 ↩︎

启发式算法 (Heuristic Algorithms)

2019-04-05 08:00:00

启发式算法 (Heuristic Algorithms)

启发式算法 (Heuristic Algorithms) 是相对于最优算法提出的。一个问题的最优算法是指求得该问题每个实例的最优解. 启发式算法可以这样定义 1:一个基于直观或经验构造的算法,在可接受的花费 (指计算时间、占用空间等) 下给出待解决组合优化问题每一个实例的一个可行解,该可行解与最优解的偏离程度不一定事先可以预计。

在某些情况下,特别是实际问题中,最优算法的计算时间使人无法忍受或因问题的难度使其计算时间随问题规模的增加以指数速度增加,此时只能通过启发式算法求得问题的一个可行解。

利用启发式算法进行目标优化的一些优缺点如下:

优点 缺点
1. 算法简单直观,易于修改
2. 算法能够在可接受的时间内给出一个较优解
1. 不能保证为全局最优解
2. 算法不稳定,性能取决于具体问题和设计者经验

启发式算法简单的划分为如下三类:简单启发式算法 (Simple Heuristic Algorithms)元启发式算法 (Meta-Heuristic Algorithms)超启发式算法 (Hyper-Heuristic Algorithms)

Heuristic-Algorithms

简单启发式算法 (Simple Heuristic Algorithms)

贪心算法 (Greedy Algorithm)

贪心算法是指一种在求解问题时总是采取当前状态下最优的选择从而得到最优解的算法。贪心算法的基本步骤定义如下:

  1. 确定问题的最优子结构。
  2. 设计递归解,并保证在任一阶段,最优选择之一总是贪心选择。
  3. 实现基于贪心策略的递归算法,并转换成迭代算法。

对于利用贪心算法求解的问题需要包含如下两个重要的性质:

  1. 最优子结构性质。当一个问题具有最优子结构性质时,可用 动态规划 法求解,但有时用贪心算法求解会更加的简单有效。同时并非所有具有最优子结构性质的问题都可以利用贪心算法求解。
  2. 贪心选择性质。所求问题的整体最优解可以通过一系列局部最优的选择 (即贪心选择) 来达到。这是贪心算法可行的基本要素,也是贪心算法与动态规划算法的主要区别。

贪心算法和动态规划算法之间的差异如下表所示:

贪心算法 动态规划
每个阶段可以根据选择当前状态最优解快速的做出决策 每个阶段的选择建立在子问题的解之上
可以在子问题求解之前贪婪的做出选择 子问题需先进行求解
自顶向下的求解 自底向上的求解 (也可采用带备忘录的自顶向下方法)
通常情况下简单高效 效率可能比较低

局部搜索 (Local Search) 和爬山算法 (Hill Climbing)

局部搜索算法基于贪婪思想,从一个候选解开始,持续地在其邻域中搜索,直至邻域中没有更好的解。对于一个优化问题:

$$ \min f \left(x\right), x \in \mathbb{R}^n $$

其中,$f \left(x\right)$ 为目标函数。搜索可以理解为从一个解移动到另一个解的过程,令 $s \left(x\right)$ 表示通过移动得到的一个解,$S \left(x\right)$ 为从当前解出发所有可能的解的集合 (邻域),则局部搜索算法的步骤描述如下:

  1. 初始化一个可行解 $x$
  2. 在当前解的邻域内选择一个移动后的解 $s \left(x\right)$,使得 $f \left(s \left(x\right)\right) < f \left(x\right), s \left(x\right) \in S \left(x\right)$,如果不存在这样的解,则 $x$ 为最优解,算法停止。
  3. $x = s \left(x\right)$,重复步骤 2。

当我们的优化目标为最大化目标函数 $f \left(x\right)$ 时,这种局部搜索算法称之为爬山算法。

元启发式算法 (Meta-Heuristic Algorithms)

元启发式算法 (Meta-Heuristic Algorithms) 是启发式算法的改进,通常使用随机搜索技巧,可以应用在非常广泛的问题上,但不能保证效率。本节部分内容参考了《智能优化方法》2 和《现代优化计算方法》1

禁忌搜索 (Tabu Search) 是由 Glover 3 提出的一种优化方法。禁忌搜索通过在解邻域内搜索更优的解的方式寻找目标的最优解,在搜索的过程中将搜索历史放入禁忌表 (Tabu List) 中从而避免重复搜索。禁忌表通过模仿人类的记忆功能,禁忌搜索因此得名。

在禁忌搜索算法中,禁忌表用于防止搜索过程出现循环,避免陷入局部最优。对于一个给定长度的禁忌表,随着新的禁忌对象的不断进入,旧的禁忌对象会逐步退出,从而可以重新被访问。禁忌表是禁忌搜索算法的核心,其功能同人类的短时记忆功能相似,因此又称之为“短期表”。

在某些特定的条件下,无论某个选择是否包含在禁忌表中,我们都接受这个选择并更新当前解和历史最优解,这个选择所满足的特定条件称之为渴望水平。

一个基本的禁忌搜索算法的步骤描述如下:

  1. 给定一个初始可行解,将禁忌表设置为空。
  2. 选择候选集中的最优解,若其满足渴望水平,则更新渴望水平和当前解;否则选择未被禁忌的最优解。
  3. 更新禁忌表。
  4. 判断是否满足停止条件,如果满足,则停止算法;否则转至步骤 2。

模拟退火 (Simulated Annealing)

模拟退火 (Simulated Annealing) 是一种通过在邻域中寻找目标值相对小的状态从而求解全局最优的算法,现代的模拟退火是由 Kirkpatrick 等人于 1983 年提出 4。模拟退火算法源自于对热力学中退火过程的模拟,在给定一个初始温度下,通过不断降低温度,使得算法能够在多项式时间内得到一个近似最优解。

对于一个优化问题 $\min f \left(x\right)$,模拟退火算法的步骤描述如下:

  1. 给定一个初始可行解 $x_0$,初始温度 $T_0$ 和终止温度 $T_f$,令迭代计数为 $k$
  2. 随机选取一个邻域解 $x_k$,计算目标函数增量 $\Delta f = f \left(x_k\right) - f \left(x\right)$。若 $\Delta f < 0$,则令 $x = x_k$;否则生成随机数 $\xi = U \left(0, 1\right)$,若随机数小于转移概率 $P \left(\Delta f, T\right)$,则令 $x = x_k$
  3. 降低温度 $T$
  4. 若达到最大迭代次数 $k_{max}$ 或最低温度 $T_f$,则停止算法;否则转至步骤 2。

整个算法的伪代码如下:

\begin{algorithm}
\caption{模拟退火算法}
\begin{algorithmic}
\STATE $x \gets x_0$
\STATE $T \gets T_0$
\STATE $k \gets 0$
\WHILE{$k \leq k_{max}$ \AND $T \geq T_f$}
    \STATE $x_k \gets $ \CALL{neighbor}{$s$}
    \STATE $\Delta f = f \left(x_k\right) - f \left(x\right)$
    \IF{$\Delta f < 0$ \OR \CALL{random}{$0, 1$} $ \leq P \left(\Delta f, T\right)$}
        \STATE $x \gets x_k$
    \ENDIF
    \STATE $T \gets $ \CALL{cooling}{$T, k, k_{max}$}
    \STATE $k \gets k + 1$
\ENDWHILE
\end{algorithmic}
\end{algorithm}

在进行邻域搜索的过程中,当温度较高时,搜索的空间较大,反之搜索的空间较小。类似的,当 $\Delta f > 0$ 时,转移概率的设置也同当前温度的大小成正比。常用的降温函数有两种:

  1. $T_{k+1} = T_k * r$,其中 $r \in \left(0.95, 0.99\right)$$r$ 设置的越大,温度下降越快。
  2. $T_{k+1} = T_k - \Delta T$,其中 $\Delta T$ 为每一步温度的减少量。

初始温度和终止温度对算法的影响较大,相关参数设置的细节请参见参考文献。

模拟退火算法是对局部搜索和爬山算法的改进,我们通过如下示例对比两者之间的差异。假设目标函数如下:

$$ f \left(x, y\right) = e^{- \left(x^2 + y^2\right)} + 2 e^{- \left(\left(x - 1.7\right)^2 + \left(y - 1.7\right)^2\right)} $$

优化问题定义为:

$$ \max f \left(x, y\right), x \in \left[-2, 4\right], y \in \left[-2, 4\right] $$

我们分别令初始解为 $\left(1.5, -1.5\right)$$\left(3.5, 0.5\right)$,下图 (上) 为爬山算法的结果,下图 (下) 为模拟退火算法的结果。

Hill Climbing

Simulated Annealing

其中,白色 的大点为初始解位置,粉色 的大点为求解的最优解位置,颜色从白到粉描述了迭代次数。从图中不难看出,由于局部最大值的存在,从不同的初始解出发,爬山算法容易陷入局部最大值,而模拟退火算法则相对稳定。

遗传算法 (Genetic Algorithm)

遗传算法 (Genetic Algorithm, GA) 是由 John Holland 提出,其学生 Goldberg 对整个算法进行了进一步完善 5。算法的整个思想来源于达尔文的进化论,其基本思想是根据问题的目标函数构造一个适应度函数 (Fitness Function),对于种群中的每个个体 (即问题的一个解) 进行评估 (计算适应度),选择,交叉和变异,通过多轮的繁殖选择适应度最好的个体作为问题的最优解。算法的整个流程如下所示:

GA-Process

初始化种群

在初始化种群时,我们首先需要对每一个个体进行编码,常用的编码方式有二进制编码,实值编码 6,矩阵编码 7,树形编码等。以二进制为例 (如下不做特殊说明时均以二进制编码为例),对于 $p \in \left\{0, 1, \dotsc, 100\right\}$$p_i = 50$ 可以表示为:

$$ x_i = 50_{10} = 0110010_{2} $$

对于一个具体的问题,我们需要选择合适的编码方式对问题的解进行编码,编码后的个体可以称之为一个染色体。则一个染色体可以表示为:

$$ x = \left(p_1, p_2, \dotsc, p_m\right) $$

其中,$m$ 为染色体的长度或编码的位数。初始化种群个体共 $n$ 个,对于任意一个个体染色体的任意一位 $i$,随机生成一个随机数 $\text{rand} \in U \left(0, 1\right)$,若 $\text{rand} > 0.5$,则 $p_i = 1$,否则 $p_i = 0$

计算适应度

适应度为评价个体优劣程度的函数 $f\left(x\right)$,通常为问题的目标函数,对最小化优化问题 $f\left(x\right) = - \min \sum{\mathcal{L} \left(\hat{y}, y\right)}$,对最大化优化问题 $f\left(x\right) = \max \sum{\mathcal{L} \left(\hat{y}, y\right)}$,其中 $\mathcal{L}$ 为损失函数。

选择

对于种群中的每个个体,计算其适应度,记第 $i$ 个个体的适应度为 $F_i = f\left(x_i\right)$。则个体在一次选择中被选中的概率为:

$$ P_i = \dfrac{F_i}{\sum_{i=1}^{n}{F_i}} $$

为了保证种群的数量不变,我们需要重复 $n$ 次选择过程,单次选择采用轮盘赌的方法。利用计算得到的被选中的概率计算每个个体的累积概率:

$$ \begin{equation} \begin{split} CP_0 &= 0 \\ CP_i &= \sum_{j=1}^{i}{P_i} \end{split} \end{equation} $$

对于如下一个示例:

指标 \ 个体 $x_1$ $x_2$ $x_3$ $x_4$ $x_5$ $x_6$
适应度 (F) 100 60 60 40 30 20
概率 (P) 0.322 0.194 0.194 0.129 0.097 0.064
累积概率 (CP) 0.322 0.516 0.71 0.839 0.936 1

每次选择时,随机生成 $\text{rand} \in U \left(0, 1\right)$,当 $CP_{i-1} \leq \text{rand} \leq CP_i$ 时,选择个体 $x_i$。选择的过程如同在下图的轮盘上安装一个指针并随机旋转,每次指针停止的位置的即为选择的个体。

GA-Roulette-Wheel

交叉

交叉运算类似于染色体之间的交叉,常用的方法有单点交叉,多点交叉和均匀交叉等。

GA-Crossover-One-Point

GA-Crossover-Two-Points

GA-Crossover-Uniform

变异

变异即对于一个染色体的任意位置的值以一定的概率 $P_m$ 发生变化,对于二进制编码来说即反转该位置的值。其中 $P_m$ 为一个较小的值,例如 $P_m = 0.05$

小结

在整个遗传运算的过程中,不同的操作发挥着不同的作用:

  1. 选择:优胜劣汰,适者生存。
  2. 交叉:丰富种群,持续优化。
  3. 变异:随机扰动,避免局部最优。

除此之外,对于基本的遗传算法还有多种优化方法,例如:精英主义,即将每一代中的最优解原封不动的复制到下一代中,这保证了最优解可以存活到整个算法结束。

示例 - 商旅问题

商旅问题 为例,利用 GA 算法求解中国 34 个省会城市的商旅问题。求解代码利用了 Deap 库,结果可视化如下图所示:

GA-TSP

一个更有趣的例子是利用 GA 算法,使用不同颜色和透明度的多边形的叠加表示一张图片,在线体验详见 这里,下图为不同参数下的蒙娜丽莎图片的表示情况:

GA-Mona-Lisa

蚁群算法 (Ant Colony Optimization, ACO)

1991 年,意大利学者 Dorigo M. 等人在第一届欧洲人工生命会议 (ECAL) 上首次提出了蚁群算法。1996 年 Dorigo M. 等人发表的文章 “Ant system: optimization by a colony of cooperating agents” 8 为蚁群算法奠定了基础。在自然界中,蚂蚁会分泌一种叫做信息素的化学物质,蚂蚁的许多行为受信息素的调控。蚂蚁在运动过程中能够感知其经过的路径上信息素的浓度,蚂蚁倾向朝着信息素浓度高的方向移动。以下图为例 9

ACO Shortest Path

蚂蚁从蚁巢 (N) 出发到达食物源所在地 (F),取得食物后再折返回蚁巢。整个过程中蚂蚁有多种路径可以选择,单位时间内路径上通过蚂蚁的数量越多,则该路径上留下的信息素浓度越高。因此,最短路径上走过的蚂蚁数量越多,则后来的蚂蚁选择该路径的机率就越大,从而蚂蚁通过信息的交流实现了寻找食物和蚁巢之间最短路的目的。

粒子群算法 (Particle Swarm Optimization, PSO)

Eberhart, R. 和 Kennedy, J. 于 1995 年提出了粒子群优化算法 10 11。粒子群算法模仿的是自然界中鸟群和鱼群等群体的行为,其基本原理描述如下:

一个由 $m$ 个粒子 (Particle) 组成的群体 (Swarm) 在 $D$ 维空间中飞行,每个粒子在搜索时,考虑自己历史搜索到的最优解和群体内 (或邻域内) 其他粒子历史搜索到的最优解,在此基础上进行位置 (状态,也就是解) 的变化。令第 $i$ 个粒子的位置为 $x_i$,速度为 $v_i$,历史搜索的最优解对应的点为 $p_i$,群体内 (或邻域内) 所有粒子历史搜索到的最优解对应的点为 $p_g$,则粒子的位置和速度依据如下公式进行变化:

$$ \begin{equation} \begin{split} v^{k+1}_i &= \omega v^k_i + c_1 \xi \left(p^k_i - x^k_i\right) + c_2 \eta \left(p^k_g - x^k_i\right) \\ x^{k+1}_i &= x^k_i + v^{k+1}_i \end{split} \end{equation} $$

其中,$\omega$ 为惯性参数;$c_1$$c_2$ 为学习因子,其一般为正数,通常情况下等于 2;$\xi, \eta \in U \left[0, 1\right]$。学习因子使得粒子具有自我总结和向群体中优秀个体学习的能力,从而向自己的历史最优点以及群体内或邻域内的最优点靠近。同时,粒子的速度被限制在一个最大速度 $V_{max}$ 范围内。

对于 Rosenbrock 函数

$$ f \left(x, y\right) = \left(1 - x\right)^2 + 100 \left(y - x^2\right)^2 $$

$x \in \left[-2, 2\right], y \in \left[-1, 3\right]$,定义优化问题为最小化目标函数,最优解为 $\left(1, 1\right)$。利用 PySwarms 扩展包的优化过程可视化如下:

Rosenbrock PSO

其中,$m = 50, \omega = 0.8, c_1 = 0.5, c_2 = 0.3$,迭代次数为 200。

本节相关示例代码详见 这里

超启发式算法 (Hyper-Heuristic Algorithms)

超启发式算法 (Hyper-Heuristic Algorithms) 提供了一种高层次启发式方法,通过管理或操纵一系列低层次启发式算法 (Low-Level Heuristics,LLH),以产生新的启发式算法。这些新启发式算法被用于求解各类组合优化问题 12

下图给出了超启发式算法的概念模型。该模型分为两个层面:在问题域层面上,应用领域专家根据自己的背景知识,在智能计算专家协助下,提供一系列 LLH 和问题的定义、评估函数等信息;在高层次启发式方法层面上,智能计算专家设计高效的管理操纵机制,运用问题域所提供的 LLH 算法库和问题特征信息,构造出新的启发式算法。

Hyper-Heuristic-Algorithms


  1. 邢文训, & 谢金星. (2005). 现代优化计算方法. 清华大学出版社. ↩︎ ↩︎

  2. 汪定伟, 王俊伟, 王洪峰, 张瑞友, & 郭哲. (2007). 智能优化方法. 高等教育出版社. ↩︎

  3. Glover, F. W., & Laguna, M. (1997). Tabu Search. Springer US. ↩︎

  4. Kirkpatrick, S., Gelatt, C. D., & Vecchi, M. P. (1983). Optimization by Simulated Annealing. Science, 220(4598), 671–680. ↩︎

  5. https://en.wikipedia.org/wiki/Genetic_algorithm ↩︎

  6. Michalewicz, Z., Janikow, C. Z., & Krawczyk, J. B. (1992). A modified genetic algorithm for optimal control problems. Computers & Mathematics with Applications, 23(12), 83-94. ↩︎

  7. Gottlieb, J., & Paulmann, L. (1998, May). Genetic algorithms for the fixed charge transportation problem. In Evolutionary Computation Proceedings, 1998. IEEE World Congress on Computational Intelligence., The 1998 IEEE International Conference on (pp. 330-335). IEEE. ↩︎

  8. Dorigo, M., Maniezzo, V., & Colorni, A. (1996). Ant system: optimization by a colony of cooperating agents. IEEE Transactions on Systems, man, and cybernetics, Part B: Cybernetics, 26(1), 29-41. ↩︎

  9. Toksari, M. D. (2016). A hybrid algorithm of Ant Colony Optimization (ACO) and Iterated Local Search (ILS) for estimating electricity domestic consumption: Case of Turkey. International Journal of Electrical Power & Energy Systems, 78, 776-782. ↩︎

  10. Eberhart, R., & Kennedy, J. (1995, November). Particle swarm optimization. In Proceedings of the IEEE international conference on neural networks (Vol. 4, pp. 1942-1948). ↩︎

  11. Eberhart, R., & Kennedy, J. (1995, October). A new optimizer using particle swarm theory. In MHS'95. Proceedings of the Sixth International Symposium on Micro Machine and Human Science (pp. 39-43). IEEE. ↩︎

  12. 江贺. (2011). 超启发式算法:跨领域的问题求解模式. 中国计算机学会通讯, 7(2), 63-70 ↩︎

关不掉的浏览器标签页 (Browser Tabs You do not Close)

2019-03-09 08:00:00

有舍,方有得。弱水三千,我只取一瓢饮。

前不久买重了一本书《断舍离》,新版的装帧挺小清新的,就买了,到家盖完私印后才发现角落里躺着另一本。去年如实有些“买书如山倒,读书如抽丝”了,工作忙是客观原因,不过更多的还是要怪自己懒散了。书不厚,其实之前也没想着买这本书,因为总感觉书名透露着机场里面琳琅满目的成功学书籍的味道,发现买重了之后,用几天零散的时间也就翻完了。书里的道理自己都清楚,但又为什么没有明白地去做呢?我想这本书对于我最重要的就是激发了我去认真理解什么是“执念”,如何放下自己的“执念”

或许从我妈那继承了“勤俭持家”的“优良传统”,我也不是很喜欢把东西丢掉,想着总还会有用上的时候,于是就慢慢的东西越堆越多。但毕竟还是年轻的一代,读完书,按照其中的建议整理了下租住的屋子,还真丢掉了不少东西,完事后多少有种轻松的感觉。其实我还是很愿意去收拾房间的,只不过之前更多的是“整理”,而现在则开始尝试“舍弃”。

屋子整理完了,第二天上班,打开电脑,才发现,更需要收拾的地方在这里,再具体写就是我的浏览器。标签页的数量已经多到连每个标签页的关闭按钮都显示不出来了,而且还是有 3 个这样的浏览器实例在那躺着,好在本子性能不错,不然 Chrome 这种吃内存大户早就把电脑搞瘫了。

拖延症

主观之上说白了我就是“拖延症”犯了,没有什么好狡辩的。如果网页打开了,看完了,内容理解吸收了亦或开怀一笑,那么这个标签页也就自然被关闭了。但拖延症确实也不是很好能改掉,我想每个人或多或少都会有拖延症,只是程度不同而已。当然也会有很多客观因素,工作和生活的节奏都很快,真的来不及把所有事情都做完,更不要说都做好。

知识管理

做不到完全摆脱拖延症,那就从技术的角度考虑考虑如何高效的获取获取信息 (知识) 吧。信息爆炸也已经不是一天两天的事了,如何在漫天遍地的信息中准确定位你想要的内容还是有些技巧可循。有些人习惯逛门户网站,这对于一些时效性信息不失为一个选择,但这类网站反而不会成为你关不掉的标签中的一员。对于知识性的内容,我更倾向于使用 RSS 进行订阅,并且仅订阅少量的优质汇总源和个人博客,对于离线知识则使用 Zotero 进行管理。可以说在技巧和工具上我也算是走在前面了,但订阅中的一个个连接被打开,却没有一个个被关掉。

断舍离

《断舍离》中提到的“执念”让我问了自己一个问题:你想要的真的是你想要的吗?所以你认为你想要的可能并不是你想要的 (或你需要的),但清楚自己“真的”想要的,也如实不好做。所以我没有打算一下子就能把我的标签页控制在个位数,只能尝试着去做做看。《断舍离》中的一些方法是可取的,但要针对“信息”这种还比较特殊的东西进行一些变化可能才会适用。

一些尝试

贝塞尔曲线 (Bézier Curve)

2019-02-19 08:00:00

知道贝塞尔曲线 (Bézier Curve) 这个名字已经有很长一段时间了,但一直没有去详细了解一番。直到最近想要绘制一个比较复杂的曲线,才发现很多工具都以贝塞尔曲线为基础的,这包括 Adobe 全家桶中的钢笔工具,还有 OmniGraffle 中的曲线。迫于仅靠猜其是如何工作的但一直没猜透的无奈,只能去详细了解一下其原理再使用了。

数学表示

贝塞尔曲线 (Bézier Curve) 是由法国工程师皮埃尔·贝兹 (Pierre Bézier) 于 1962 年所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计 1。贝塞尔曲线最初由保尔·德·卡斯特里奥 (Paul de Casteljau) 于 1959 年运用德卡斯特里奥算法 (De Casteljau’s Algorithm) 开发,以稳定数值的方法求出贝塞尔曲线。

线性贝塞尔曲线

给定点 $P_0, P_1$,线性贝塞尔曲线定义为:

$$ B \left(t\right) = \left(1 - t\right) P_0 + t P_1, t \in \left[0, 1\right] $$

不难看出,线性贝塞尔曲线即为点 $P_0$$P_1$ 之间的线段。

对于 $P_0 = \left(4, 6\right), P_1 = \left(10, 0\right)$,当 $t = 0.25$ 时,线性贝塞尔曲线如下图所示:

线性贝塞尔曲线

整个线性贝塞尔曲线生成过程如下图所示:

线性贝塞尔曲线生成过程

二次贝塞尔曲线

给定点 $P_0, P_1, P_2$,二次贝塞尔曲线定义为:

$$ B \left(t\right) = \left(1 - t\right)^2 P_0 + 2 t \left(1 - t\right) P_1 + t^2 P_2, t \in \left[0, 1\right] $$

对于 $P_0 = \left(0, 0\right), P_1 = \left(4, 6\right), P_2 = \left(10, 0\right)$,当 $t = 0.25$ 时,二次贝塞尔曲线如下图所示:

二次贝塞尔曲线

整个二次贝塞尔曲线生成过程如下图所示:

二次贝塞尔曲线生成过程

三次贝塞尔曲线

给定点 $P_0, P_1, P_2, P_3$,三次贝塞尔曲线定义为:

$$ B \left(t\right) = \left(1 - t\right)^3 P_0 + 3 t \left(1 - t\right)^2 P_1 + 3 t^2 \left(1 - t\right) P_2 + t^3 P_3, t \in \left[0, 1\right] $$

对于 $P_0 = \left(0, 0\right), P_1 = \left(-1, 6\right), P_2 = \left(6, 6\right), P_3 = \left(12, 0\right)$,当 $t = 0.25$ 时,三次贝塞尔曲线如下图所示:

三次贝塞尔曲线

整个三次贝塞尔曲线生成过程如下图所示:

三次贝塞尔曲线生成过程

一般化的贝塞尔曲线

对于一般化的贝塞尔曲线,给定点 $P_0, P_1, \cdots, P_n$$n$ 次贝塞尔曲线定义为:

$$ B \left(t\right) = \sum_{i=0}^{n}{\binom{n}{i} \left(1 - t\right)^{n - i} t^{i} P_i}, t \in \left[0, 1\right] $$

其中,

$$ b_{i, n} \left(t\right) = \binom{n}{i} \left(1 - t\right)^{n - i} t^{i} $$

称之为 $n$Bernstein 多项式,点 $P_i$ 称为贝塞尔曲线的控制点。从生成过程来看,贝塞尔曲线是通过 $n$中介点 ($Q_j, R_k, S_l$) 生成的,一个更加复杂的四次贝塞尔曲线 ($t = 0.25$) 如下图所示:

四次贝塞尔曲线

整个四次贝塞尔曲线生成过程如下图所示:

四次贝塞尔曲线生成过程

其中,$Q_0 = \left(1 - t\right) P_0 + t P_1$$R_0 = \left(1 - t\right) Q_0 + t Q_1$$S_0 = \left(1 - t\right) R_0 + t R_1$$B = \left(1 - t\right) S_0 + t S_1$ 为构成贝塞尔曲线的点。

上述图形和动画的绘制代码请参见这里

应用技巧

在很多绘图软件中,钢笔工具使用的是三次贝塞尔曲线,其中起始点结束点分别对应 $P_0$$P_1$,起始点和结束点的控制点分别对应 $P_2$$P_3$

在利用钢笔工具绘图时,可以参考如下建议来快速高效地完成绘图 2

  1. 控制点尽可能在曲线的最外侧或最内侧。
  2. 除了曲线的结束处外,控制点的控制柄尽可能水平或垂直。
  3. 合理安排控制点的密度。

下面两张图分别展示了一个原始的字母图案,以及参考上述建议利用贝塞尔曲线勾勒出来的字母图案边框:

Letters

Letters Buzier Curve

最后推荐一个网站 The Bezier Game,可以帮助更好的理解和掌握基于贝塞尔曲线的钢笔工具使用。

如何阅读一本书 (How to Read a Book)

2019-02-07 08:00:00

本文为《如何阅读一本书》的简要总结,附加部分个人理解,仅供参考和快速查阅。楷体引用部分多为书中原话,其他部分多为总结和个人理解。

阅读的层次

第一提醒读者,阅读可以是一件多少主动的事。第二要指出的是,阅读越主动,效果越好。

读书,不是非做不可的事,而是想要去做的事。——《女王的教室

阅读的目标:为获得资讯而读,以及为求的理解而读。

也就是说,阅读类似学习,知其然,知其所以然。

阅读就是学习:指导型的学习,以及自我发现型的学习之间的差异。

独立的思考与思辨。

阅读的层次分为:

  1. 基础阅读 (Elementary Reading)
  2. 检视阅读 (Inspectional Reading)
  3. 分析阅读 (Analytical Reading)
  4. 主题阅读 (Syntopical Reading)

基础阅读

基础阅读的四个阶段:

  1. 阅读准备阶段,相当于学前教育或幼稚园的学习经验。
  2. 认字,相当于一年级学生典型的学习经验。
  3. 字汇的增长及对课文的运用,通常是四年级结束时学会的方法。
  4. “成熟”的阅读者,小学或初中毕业时的读写能力。

无限制的受教育机会是一个社会能提供给人民最有价值的服务。

检视阅读

有系统的略读或粗读

略读 (Skimming) 和粗读 (Pre-reading) 是检视阅读的第一个层次,用不着花太多时间,如何去做,建议如下:

  1. 先看书名页,然后如果有序就先看序。
  2. 研究目录页,对书的基本架构做概括性的理解。
  3. 如果书中附有索引,也要检阅一下。
  4. 读一下出版者的介绍。
  5. 挑几个看来跟主题息息相关的篇章来看。
  6. 东翻翻西翻翻,念一两段,连续读几页,不要太多。

在最多不超过一个小时的时间内对书有个大概的了解其是否包含你还想继续挖掘下去的内容,是否值的你再继续投入时间与注意。

粗浅的阅读

头一次面对一本难读的书的时候,从头到尾读完一遍,碰到不懂的地方不要停下来查询或思索。

在阅读一本书的时候,慢不该慢的不值得,快不该快到有损于满足与理解。

略读或粗读一本书总是个好主意,尤其当你并不清楚手边的一本书是否值的细心阅读时。

在第一次阅读一本难读的书时,不要企图了解每一个子句。

做一个自我要求的读者

在阅读时要提出问题来,同时自己必须尝试去回答这些问题:

  1. 整体来说,这本书到底在谈些什么?(主题)
  2. 作者细部说了什么,怎么说的?(细节)
  3. 这本书说得有道理吗?是全部有道理,还是部分有道理?(个人的评价)
  4. 这本书跟你有什么关系?(意义)

这四个问题概括了一个阅读者的责任,读书要对书负责,更要对自己负责

你必须读出言外之意,才会有更大的收获,我们也鼓励你“写出言外之意”。

对于阅读来说,在书上做笔记是不可或缺的事。

  1. 可以让你保持清醒,不只是不昏睡,还是非常清醒。
  2. 主动的阅读是一种思考,而写出来是阅读者表达思考的好方法。
  3. 将自己的感想写下来,有助于记住作者的思想。

培养阅读的习惯,除了不断地运作练习之法,别无他法。

我们谈到一个有技术的人时,并不是在说他知道该如何去做那件事,而是他已经养成去做那件是的习惯了。

分析阅读

第一阶段

规则 1

你一定要知道自己在读的是哪一类书,而且越早知道越好。最好早在你开始阅读之前就先知道。

我们一定要超越“知道这是怎么回事”,进而明白“如果我们想做些什么,应该怎么利用它”。

也就是我们需要做到知行合一

理论性作品是在教你这是什么,实用性的作品是在教你如何去做你想要做的事,或你认为应该做的事。

实用的书常会出现“应该”和“应当”,“”和“”,“结果”和“意义”之类的字眼,相反的理论型作品却常常说“”。

理论性作品可以分为:

  1. 历史,历史就是纪事,常以说故事的形态出现。
  2. 科学,以实验为基础,或依赖精确的观察研究,并不容易被证明。
  3. 哲学,是坐在摇椅上的思考,相对容易被观察和理解。

规则 2

使用一个单一的句子,或最多几句话(一小段文字)来叙述整本书的内容。

也就是用你的话告诉别人这本书在讲什么。

规则 3

将书中重要的篇章列举出来,说明它们如何按照顺序组成一个整体的架构。

规则 2 是在指导你注意一本书的整体性,规则 3 是在强调一本书的复杂度。

写作与阅读是一体两面的事,就像教书与被教一样。

一个作品应该有整体感,清楚明白,前后连贯

规则 4

找出作者要问的问题。

第一阶段的目的就是掌握结构大纲。

第二阶段

规则 5

找出重要的单字,透过他们与作者达成共识。

字词只是作者表达的工具,我们需要通过这些字词探索作者想表达的本意。

一本书之所以能给你带来新的洞察力或启发,就是因为其中有一些你不能一读即懂的字句。

规则 6

将一本书中最重要的句子圈出来,找出其中的主旨。

主旨则是这些问题的答案。

阅读的一部分本质就是被困惑,而且知道自己被困惑

惑,知惑!

规则 7

从相关文句的关联中,设法架构出一本书的基本论述。
  1. 要记住所有的论述都包含了一些声明。
  2. 要区别两种论述的不同之处。归纳法:以一个或多个特殊的事实证明某种共通的概念,演绎法:以连串的通则来证明更进一步的共通概念。
  3. 找出作者认为哪些事情是假设,哪些是能证实的或有根据的,以及哪些是不需要证实的自明之理

规则 8

找出作者的解答。

第三阶段

遵守思维的礼节。

最能学习的读者,也就是最能批评的读者。

受教是一种美德,但受教并非是被动的顺从,而是主动的思考。

规则 9

在你说出“我同意”,“我不同意”,或“我暂缓评论”之前,你一定要能肯定的说:“我了解了”。

毫无理解便同意只是愚蠢,还不清楚便不同意也是无礼。

规则 10

当你不同意作者的观点时,要理性地表达自己的意见,不要无礼地辩驳或争论。

争议是教导与受教的一个过程。

规则 11

尊重知识与个人观点的不同,在作任何评断之前,都要找出理论基础。

事无对错,需有理有据,避免口舌之争。

当读者不只是盲目地跟从作者的论点,还能和作者的论点针锋相对时,他最后才能提出同意或反对的有意义的评论。

规则 12

说一位作者知识不足,就是说他缺少某些与他想要解决的问题相关的知识。

除非这些知识确实相关,否则就没有理由作这样的评论。

规则 13

说一位作者的知识错误,就是说他的理念不正确。

论点与事实相反。

规则 14

说一位作者是不和逻辑的,就是说他的推论荒谬。

荒谬有两种形态:一种是缺乏连贯,也就是结论冒出来了,却跟前面所说的理论连不起来。另一种是事件变化的前后不一致,也就是作者所说的两件事是前后矛盾的。

规则 15

说一位作者的分析是不完整的,就是说他并没有解决他一开始提出来的所有问题。

严格来说,规则 15 并不能作为不同意一个作者的根据。我们只能就作者的成就是有限的这一点而站在对立面上。

CHEAT SHEET

辅助阅读

内在阅读,是指阅读书籍本身,于所有其他的书都是不相关的。外在阅读,是指借助其他的一些书籍来阅读一本书。

外在的辅助来源可以分为四个部分:相关经验,其他的书,导论与摘要,工具书。

导读和摘要要尽量少用,因为:一本书的导读并不一定都是对的,就算他们写对了,可能也不完整。

如果你在阅读全书之前,先看了他的导读手册,你就隶属于他了。

这也是为什么我不喜欢将自己看过的书借给其他人的原因,我不希望其他读者在第一次读这本书的时候就被我记录在书上的笔记所影响。

阅读不同读物的方法

阅读实用型的书

分析阅读的规则,一般来说适用于论说性的作品,也就是说任何一种传达知识的书。

任何实用性的书都不能解决该书所关心的问题。

实用性的书分为两类:其中一种,就像本书一样,或是烹饪书、驾驶指南,基本上都是在说明规则的。另一类的主要是在阐述形成规则的原理,许多伟大的经济、政治、道德巨著就属于这一类。

在读实用性的书要提出的四个问题:

  1. 这本书在谈些什么?
  2. 找出作者的共识、主旨和论述。
  3. 内容真实吗?(比前两点重要)
  4. 这本书于我何干?

赞同一本实用性的书,需要你采取行动。

阅读想象文学

想象文学的主要目的是娱乐,而非教育

关于阅读想象文学,建议的否定指令:

  1. 不要抗拒想象文学带给你的影响力。(生活不只有眼前的苟且,还有诗和远方)
  2. 在想象文学中,不要去找共识、主旨和论述。
  3. 不要用适用于传递知识的,与真理一致的标准来批评小说。(一千个人眼中有一千个哈姆雷特)

阅读小说的规则:

  1. 架构性:
  1. 诠释性:
  1. 评论性

阅读故事、戏剧与诗

暴君并不怕唠叨的作家宣扬自由的思想,他害怕一个醉酒的诗人说了一个笑话,吸引了全民的注意力。

所谓“纯”艺术,并不是因为“精致”或“完美”,而是因为作品本身就是一个结束,不再与其他的影响有关。就如同爱默生所说的,美的本身就是存在的唯一标准

阅读故事书的规则:

  1. 快读,并且全心全意地读。
  2. 整本书在谈些什么?一个故事的词义,存在于角色与事件之中。
  3. 批评小说时,要区分是满足个人特殊潜意识需求的小说还是大多数人潜意识的小说。

阅读抒情诗的规则:

  1. 不论你自己觉得懂不懂,都要一口气读完,不要停。
  2. 重读一遍,大声读出来。

对论说性作品所提出的问题是文法与逻辑上的问题。对抒情诗的问题却通常是修辞的问题,或是句法的问题。

要了解一首诗,一定要去它,一遍又一遍地读。

阅读历史书

就事实而言的历史 (history of fact) 与就书写记录而言的历史 (history as a written record of the facts) 是不同的。

历史的基本是叙事的。

所以叙事应尽可能的公平,公正地描述所发生的事情。

历史比较接近小说,而非科学。这并不是说历史学家在捏造事实,就像诗人或小说家那样。

历史根本就没有模式可循。

在了解一个已经发生过的事情时,最好多听取几个不同的版本,哪怕每个人的陈述都已经尽可能的公平公正了,但也可能会存在信息的丢失。

修昔底德说过,他写历史的原因是:希望经由他所观察到的错误,以及他个人受到的灾难与国家所受的苦楚,将来的人们不会重蹈覆辙。

以铜为鉴,可以正衣冠,以人为鉴,可以知得失,以史为鉴,可以知兴替。

阅读历史书要提出的问题:

  1. 每一本历史书都有一个特殊而且有限定范围的主题。
  2. 历史书在说一个故事,而这个故事当然是发生在一个特定的时间里。
  3. 这与我何干?历史会建议一些可行性,因为那是以前的人已经做过的事。

传记包含很多类型:

  1. 定案本 (definitive) 的传记是对一个人的一生作详尽完整的学术性报告,这个人重要到够得上写这种完结篇的传记。定案本的传记决不能用来写活着的人。
  2. 授权本 (authorized) 的传记通常是由继承人,或是某个重要人物的朋友来负责的。读这种书不能像读一般的历史书一样,读者必须了解作者可能会有偏见
  3. 自传所写的都是还未完结的生活。对于任何自传都要有一点疑心,同时别忘了,在你还不了解一本书之前,不要妄下论断。

阅读科学与数学

科学的客观不在于没有最初的偏见,而在于坦白承认

科学基本上是归纳法,基本的论述也就是经由研究查证,建立出来的一个通则。

只要你记住,你的责任不是成为这个主题的专家,而是要去了解相关的问题,在阅读时就会轻松许多。

阅读哲学书

我想在进一步阅读或学习如何阅读哲学书之前,最好针对这个相对特殊的类别有一个简要的科普。待我对其窥见一斑后再回来补充这一章节。

阅读社会科学

社会科学不是一个完全独立的学科。诸如人类学、经济学、政治学、社会学的学科,都是组成社会科学的核心。大部分有关法律、教育、公共行政的作品,及一部分商业、社会服务的作品,再加上大量的心理学作品,也适合社会科学的定义。

阅读社会科学时,关于一个主题通常要读好几本书,而不会只读一本书。主要的着眼点在一个特殊的事件或问题上,而非一个特殊的作者或一本书

主题阅读

在作主题阅读时,第一个要求就是知道:对一个特定的问题来说,所牵涉的绝对不是一本书而已。第二个要求则是:要知道就总的来说,应该读的是哪些书?第二个要求比第一个要求还难做到。

分析阅读的技巧只适用于单一的作品,主要的目标是要了解这本书。

在主题阅读的准备阶段包含如下步骤:

  1. 针对你要研究的主题,设计一份实验性的书目。你可以参考图书馆目录,专家的建议与书中的书目索引。
  2. 浏览这份书目上所有的书,确定哪些与你的主题相关,并就你的主题建立起清楚的概念。

主题阅读一共有五个步骤,这些步骤不能称之为规则,因为只要漏掉其中一个步骤,主题阅读就会变得很困难。

  1. 浏览所有在准备阶段被认定与你主题相关的书,找出最相关的章节。
  2. 根据主题创造出一套中立的词汇,带引作者与你达成共识,无论作者是否实际用到这些词汇,所有的作者,或至少绝大部分的作者都可以用这套词汇来诠释。
  3. 建立一个中立的主旨,列出一连串的问题,无论作者是否明白谈过这些问题,所有的作者,或者至少大多数的作者都要能解读为针对这些问题提供了他们的回答。
  4. 界定主要及次要的议题。然后将作者针对各个问题的不同意见整理陈列在各个议题之旁。你要记住,各个作者之间或之中,不见得一定存在着某个议题。有时候,你需要针对一些不是作者主要关心的范围的事情,把他的观点解读,才能建构出这种议题。
  5. 分析这些讨论。这得把问题和议题按照顺序排列,以求突显主题。比较有共通性的议题,要放在比较没有共通性的议题之前。各个议题之间的关系也要清楚得界定出来。

心智成长

对你来说最重要的是,你不只要能读得好,还有有能力分辨出哪些书能够帮助你增进阅读能力。

读一本好书,会让你的努力有所回报:

  1. 当你成功地阅读了一本难读的好书之后,你的阅读技巧必然增进了。
  2. 一本好书能教你了解这个世界以及你自己。

一本书如果是可以让你学习的书,重读的时候,你会发现书中的内容好像比你记忆中的少了许多。如果这本书属于更高层次的书,你在重读的时候会发现这本书好像与你一起成长了。

好的阅读,也就是主动的阅读,不只是对阅读本身有用,也不只是对我们的工作或事业有帮助,更能帮助我们的心智保持活力与成长。

相似性和距离度量 (Similarity & Distance Measurement)

2019-01-01 08:00:00

相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity),通常情况下我们可以利用一个距离函数定义集合 $X$ 上元素间的距离,即:

$$ d: X \times X \to \mathbb{R} $$

同时,对于集合 $X$ 内的元素 $x, y, z$,距离函数一般满足如下条件:

  1. $d \left(x, y\right) \geq 0$ (非负性)
  2. $d \left(x, y\right) = 0, \text{当且仅当} \ x = y$ (同一性)
  3. $d \left(x, y\right) = d \left(y, x\right)$ (对称性)
  4. $d \left(x, z\right) \leq d \left(x, y\right) + d \left(y, z\right)$ (三角不等式)

明可夫斯基距离 (明氏距离, Minkowski Distance)

对于点 $x = \left(x_1, x_2, ..., x_n\right)$ 和点 $y = \left(y_1, y_2, ..., y_n\right)$$p$ 阶明可夫斯基距离 定义为:

$$ d \left(x, y\right) = \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} $$

$p = 1$ 时,称之为 曼哈顿距离 (Manhattan Distance)出租车距离

$$ d \left(x, y\right) = \sum_{i=1}^{n} |x_i - y_i| $$

$p = 2$ 时,称之为 欧式距离 (Euclidean Distance)

$$ d \left(x, y\right) = \sqrt{\sum_{i=1}^{n} \left(x_i - y_i\right)^2} $$

Manhattan Distance

上图中 绿色 的直线为两点间的欧式距离,红色 黄色 蓝色 的折线均为两点间的曼哈顿距离,不难看出 3 条折线的长度是相同的。

$p \to \infty$ 时,称之为 切比雪夫距离 (Chebyshev Distance)

$$ d \left(x, y\right) = \lim_{p \to \infty} \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} = \max_{i=1}^{n} |x_i - y_i| $$

下图展示了不同的 $p$ 值下单位圆,即 $x^p + y^p = 1$,便于大家理解不同 $p$ 值下的明可夫斯基距离:

2D Unit Balls

马哈拉诺比斯距离 (马氏距离, Mahalanobis Distance)

马哈拉诺比斯距离表示数据的 协方差距离,与欧式距离不同其考虑到各种特性之间的联系是 尺度无关 (Scale Invariant) 的。对于一个协方差矩阵为 $\sum$ 的变量 $x$$y$,马氏距离定义为:

$$ d \left(x, y\right) = \sqrt{\left(x - y\right)^{\top} {\sum}^{-1} \left(x - y\right)} $$

马氏距离的最大优势就是其不受不同维度之间量纲的影响,同时引入的问题便是扩大了变化量较小的变量的影响。以下图为例 (源码详见 这里):

Mahalanobis Distance

左侧图中根据欧式距离计算,红色 的点距离 绿色 的点更近一些,右侧图是根据马氏距离进行座标变换后的示意图,不难看出此时 红色 的点距离 蓝色 的点更近一些。

向量内积 (Inner Product of Vectors)

在欧几里得几何中,两个笛卡尔坐标向量的点积常称为内积,向量内积是两个向量的长度与它们夹角余弦的积,定义为:

$$ x \cdot y = \sum_{i=1}^{n}{x_i y_i} $$

从代数角度看,先对两个数字序列中的每组对应元素求积,再对所有积求和,结果即为点积。从几何角度看,点积则是两个向量的长度与它们夹角余弦的积。在欧几里得空间中,点积可以直观地定义为:

$$ x \cdot y = \left| x \right| \left| y \right| \cos \theta $$

余弦相似度 (Cosine Similarity) 可以利用两个向量夹角的 cos 值定义,即:

$$ s \left(x, y\right) = \cos \left(\theta\right) = \dfrac{x \cdot y}{\left| x \right| \left| y \right|} = \dfrac{\sum_{i=1}^{n}{x_i y_i}}{\sqrt{\sum_{i=1}^{n}{x_i^2}} \sqrt{\sum_{i=1}^{n}{y_i^2}}} $$

余弦相似度的取值范围为:$\left[-1, 1\right]$,1 表示两者完全正相关,-1 表示两者完全负相关,0 表示两者之间独立。余弦相似度与向量的长度无关,只与向量的方向有关,但余弦相似度会受到向量平移的影响。

皮尔逊相关系数 (Pearson Correlation) 解决了余弦相似度会收到向量平移影响的问题,其定义为:

$$ \rho \left(x, y\right) = \dfrac{\text{cov} \left(x, y\right)}{\sigma_x \sigma_y} = \dfrac{E \left[\left(x - \mu_x\right) \left(y - \mu_y\right)\right]}{\sigma_x \sigma_y} $$

其中,$\text{cov}$ 表示协方差,$E$ 表示期望,$\mu$ 表示均值,$\sigma$ 表示标准差。对于样本的皮尔逊相关系数,可以通过如下方式计算:

$$ \begin{equation} \begin{split} r &= \dfrac{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right) \left(y_i - \bar{y}\right)}}{\sqrt{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right)^2}} \sqrt{\sum_{i=1}^{n}{\left(y_i - \bar{y}\right)^2}}} \\ &= \dfrac{1}{n-1} \sum_{i=1}^{n}{\left(\dfrac{x_i - \bar{x}}{\sigma_x}\right) \left(\dfrac{y_i - \bar{y}}{\sigma_y}\right)} \end{split} \end{equation} $$

皮尔逊相关系数的取值范围为:$\left[-1, 1\right]$,值的含义与余弦相似度相同。皮尔逊相关系数有一个重要的数学特性是:变量位置和尺度的变化并不会引起相关系数的改变。下图给出了不同的 $\left(x, y\right)$ 之间的皮尔逊相关系数。

Correlation Examples

集合距离 (Distance of Sets)

对于两个集合之间的相似性度量,主要有如下几种方法:

$$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$

Jaccard 系数的取值范围为:$\left[0, 1\right]$,0 表示两个集合没有重合,1 表示两个集合完全重合。

$$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$

与 Jaccard 系数相同,Dice 系数的取值范围为:$\left[0, 1\right]$,两者之间可以相互转换 $s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$。不同于 Jaccard 系数,Dice 系数的差异函数 $d = 1 - s$ 并不是一个合适的距离度量,因为其并不满足距离函数的三角不等式。

$$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$

其中,$X \setminus Y$ 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化,当 $\alpha = \beta = 1$ 时为 Jaccard 系数,当 $\alpha = \beta = 0.5$ 时为 Dice 系数。

字符串距离 (Distance of Strings)

对于两个字符串之间的相似性度量,主要有如下几种方法:

Levenshtein 距离是 编辑距离 (Editor Distance) 的一种,指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如将 kitten 转成 sitting,转换过程如下:

$$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\ \to g\right) \end{split} \end{equation*} $$

编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。

对于给定的两个字符串 $s_1$$s_2$,Jaro 相似度定义为:

$$ sim = \begin{cases} 0 & \text{if} \ m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) & \text{otherwise} \end{cases} $$

其中,$\left|s_i\right|$ 为字符串 $s_i$ 的长度,$m$ 为匹配的字符的个数,$t$ 换位数目的一半。如果字符串 $s_1$$s_2$ 相差不超过 $\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$,我们则认为两个字符串是匹配的。例如,对于字符串 CRATETRACE,仅 R, A, E 三个字符是匹配的,因此 $m = 3$,尽管 C, T 均出现在两个字符串中,但是他们的距离超过了 1 (即,$\lfloor \dfrac{5}{2} \rfloor - 1$),因此 $t = 0$

Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数,其定义为:

$$ sim_w = sim_j + l p \left(1 - sim_j\right) $$

其中,$sim_j$ 为字符串 $s_1$$s_2$ 的 Jaro 相似度,$l$ 为共同前缀的长度 (规定不超过 $4$),$p$ 为调整系数 (规定不超过 $0.25$),Winkler 将其设置为 $p = 0.1$

汉明距离为两个等长字符串对应位置的不同字符的个数,也就是将一个字符串变换成另外一个字符串所需要替换的字符个数。例如:10111011001001 之间的汉明距离是 2,tonedroses 之间的汉明距离是 3。

信息论距离 (Information Theory Distance)

首先我们需要理解什么是 熵 (Entropy)?熵最早是用来表示物理学中一个热力系统无序的程度,后来依据香农的信息论,熵用来衡量一个随机变量的不确定性程度。对于一个随机变量 $X$,其概率分布为:

$$ P \left(X = x_i\right) = p_i, \quad i = 1, 2, ..., n $$

则随机变量 $X$ 的熵定义如下:

$$ H \left(X\right) = - \sum_{i=1}^{n} P \left(x_i\right) \log P \left(x_i\right) \label{eq:entropy} $$

例如抛一枚硬币,假设硬币正面向上 $X = 1$ 的概率为 $p$,硬币反面向上 $X = 0$ 的概率为 $1 - p$。则对于抛一枚硬币那个面朝上这个随机变量 $X$ 的熵为:

$$ H \left(X\right) = - p \log p - \left(1-p\right) \log \left(1-p\right) $$

随概率 $p$ 变化如下图所示:

Entropy Demo

从图可以看出,当 $p = 0.5$ 时熵最大,也就是说抛一枚硬币,当正反两面朝上的概率相同时,熵最大,系统最复杂。对于公式 $\ref{eq:entropy}$,当取以 2 为底的对数时,熵的单位为比特 (bit),当取自然对数时,熵的单位为纳特 (nat),当取以 10 为底的对数时,熵的单位为哈特 (hart)。

对于随机变量 $\left(X, Y\right)$,其联合概率分布为:

$$ P \left(X = x_i, Y = y_i\right) = p_{i, j}, \quad i = 1,2,...,n; \quad j = 1,2,...,m $$

条件熵 (Conditional Entropy) 表示在已知 $X$ 的条件下 $Y$ 的不确定性,定义为:

$$ \begin{equation} \begin{split} H \left(Y | X\right) &= \sum_{i=i}^{n} P \left(x_i\right) H \left(Y | X = x_i\right) \\ &= \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i\right)}{P \left(x_i, y_j\right)}}} \end{split} \end{equation} $$

联合熵 (Joint Entropy) 用于衡量多个随机变量的随机系统的信息量,定义为:

$$ H \left(X, Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log P \left(x_i, y_j\right)}} $$

互信息用于衡量两个变量之间的关联程度,定义为:

$$ I \left(X; Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i, y_i\right)}{P \left(x_i\right) P \left(y_j\right)}}} $$

直观上,互信息度量 $X$$Y$ 共享的信息,它度量知道这两个变量其中一个,对另一个不确定度减少的程度。

相对熵又称之为 KL 散度 (Kullback-Leibler Divergence),用于衡量两个分布之间的差异,定义为:

$$ D_{KL} \left(P \| Q\right) = \sum_{i}{P \left(i\right) \ln \dfrac{P \left(i\right)}{Q \left(i\right)}} $$

KL 散度为非负数 $D_{KL} \left(P \| Q\right) \geq 0$,同时其不具有对称性 $D_{KL} \left(P \| Q\right) \neq D_{KL} \left(Q \| P\right)$,也不满足距离函数的三角不等式。

交叉熵定义为:

$$ \begin{equation} \begin{split} H \left(P, Q\right) &= H \left(P\right) + D_{KL} \left(P \| Q\right) \\ &= - \sum_{i}{P \left(i\right) \log Q \left(i\right)} \end{split} \end{equation} $$

交叉熵常作为机器学习中的损失函数,用于衡量模型分布和训练数据分布之间的差异性。

JS 散度解决了 KL 散度不对称的问题,定义为:

$$ D_{JS} \left(P \| Q\right) = \dfrac{1}{2} D_{KL} \left(P \| \dfrac{P + Q}{2}\right) + \dfrac{1}{2} D_{KL} \left(Q \| \dfrac{P + Q}{2}\right) $$

当取以 2 为底的对数时,JS 散度的取值范围为:$\left[0, 1\right]$

推土机距离用于描述两个多维分布之间相似性,之所以称为推土机距离是因为我们将分布看做空间中的泥土,两个分布之间的距离则是通过泥土的搬运将一个分布改变到另一个分布所消耗的最小能量 (即运送距离和运送重量的乘积)。

对于给定的分布 $P = \left\{\left(p_1, w_{p1}\right), \left(p_2, w_{p2}\right), \cdots, \left(p_m, w_{pm}\right)\right\}$$Q = \left\{\left(q_1, w_{q1}\right), \left(q_2, w_{q2}\right), \cdots, \left(q_n, w_{qn}\right)\right\}$,定义从 $p_i$$q_j$ 之间的距离为 $d_{i, j}$,所需运送的重量为 $f_{i, j}$。对于 $f_{i, j}$ 有如下 4 个约束:

  1. 运送需从 $p_i$$q_j$,不能反向,即 $f_{i, j} \geq 0, 1 \leq i \leq m, 1 \leq j \leq n$
  2. $p_i$ 运送出的总重量不超过原始的总重量 $w_{pi}$,即 $\sum_{j=1}^{n}{f_{i, j}} \leq w_{pi}, 1 \leq i \leq m$
  3. 运送到 $q_j$ 的总重量不超过其总容量 $w_{qj}$,即 $\sum_{i=1}^{m}{f_{i, j}} \leq w_{qj}, 1 \leq j \leq n$
  4. $\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}}} = \min \left\{\sum_{i=1}^{m}{w_{pi}}, \sum_{j=1}^{n}{w_{qj}}\right\}$

在此约束下,通过最小化损失函数:

$$ \min \sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}}} $$

得到最优解 $f_{i, j}^*$,则推土机距离定义为:

$$ D_{W} \left(P, Q\right) = \dfrac{\sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}^*}}}{\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}^*}}} $$

其他距离 (Other Distance)

DTW 距离用于衡量两个序列之间的相似性,序列的长度可能相等也可能不相等。对于两个给定的序列 $X = \left(x_1, x_2, \cdots, x_m\right)$$Y = \left(y_1, y_2, \cdots, y_n\right)$,我们可以利用动态规划的方法求解 DTW 距离。首先我们构造一个 $m \times n$ 的矩阵,矩阵中的元素 $d_{i, j}$ 表示 $x_i$$y_j$ 之间的距离。我们需要找到一条通过该矩阵的路径 $W = \left(w_1, w_2, \cdots, w_l\right)$, $\max\left(m, n\right) \leq l < m + n + 1$,假设 $w_k$ 对应的矩阵元素为 $\left(i, j\right)$,对应的距离为 $d_k$,则 DTW 的优化目标为 $\min \sum_{k=1}^{l}{d_k}$。如下图右上角部分所示:

DTW Three-Way

对于路径 $W$,需要满足如下 3 个条件:

  1. 边界条件$w_1 = \left(1, 1\right), w_k = \left(m, n\right)$,即路径须从左下角出发,在右上角终止。
  2. 连续性:对于 $w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$,需满足 $i - i' \leq 1, j - j' \leq 1$,即路径不能跨过任何一点进行匹配。
  3. 单调性:对于 $w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$,需满足 $0 \leq i - i', 0 \leq j - j'$,即路径上的点需单调递增,不能回退进行匹配。

利用动态规划求解 DTW 的状态转移方程为:

$$ dtw_{i, j} = \begin{cases} 0 & \text{if} \ i = j = 0 \\ \infty & \text{if} \ i = 0 \ \text{or} \ j = 0 \\ d_{i, j} + \min \left(dtw_{i-1, j}, dtw_{i-1, j-1}, dtw_{i, j-1}\right) & \text{otherwise} \end{cases} $$

$dtw_{m, n}$ 则为最终的 DTW 距离。在 DTW 求解的过程中还可以使用不同的 Local Warping Step 和窗口类型,更多详细信息可看见 R 中 dtw 包。下图展示了利用 DTW 求解后不同点之间的对应关系:

DTW Two-Way

关于流形距离请参见之前的博客:流形学习 (Manifold Learning)

:tada::tada::tada: Happe New Year! :tada::tada::tada:

集成学习算法 (Ensemble Learning)

2018-12-08 08:00:00

传统机器学习算法 (例如:决策树,人工神经网络,支持向量机,朴素贝叶斯等) 的目标都是寻找一个最优分类器尽可能的将训练数据分开。集成学习 (Ensemble Learning) 算法的基本思想就是将多个分类器组合,从而实现一个预测效果更好的集成分类器。集成算法可以说从一方面验证了中国的一句老话:三个臭皮匠,赛过诸葛亮。

Thomas G. Dietterich 1 2 指出了集成算法在统计,计算和表示上的有效原因:

一个学习算法可以理解为在一个假设空间 $\mathcal{H}$ 中选找到一个最好的假设。但是,当训练样本的数据量小到不够用来精确的学习到目标假设时,学习算法可以找到很多满足训练样本的分类器。所以,学习算法选择任何一个分类器都会面临一定错误分类的风险,因此将多个假设集成起来可以降低选择错误分类器的风险。

很多学习算法在进行最优化搜索时很有可能陷入局部最优的错误中,因此对于学习算法而言很难得到一个全局最优的假设。事实上人工神经网络和决策树已经被证实为是一 个NP 问题 3 4。集成算法可以从多个起始点进行局部搜索,从而分散陷入局部最优的风险。

在多数应用场景中,假设空间 $\mathcal{H}$ 中的任意一个假设都无法表示 (或近似表示) 真正的分类函数 $f$。因此,对于不同的假设条件,通过加权的形式可以扩大假设空间,从而学习算法可以在一个无法表示或近似表示真正分类函数 $f$ 的假设空间中找到一个逼近函数 $f$ 的近似值。

集成算法大致可以分为:Bagging,Boosting 和 Stacking 等类型。

Bagging

Bagging (Boostrap Aggregating) 是由 Breiman 于 1996 年提出 5,基本思想如下:

  1. 每次采用有放回的抽样从训练集中取出 $n$ 个训练样本组成新的训练集。
  2. 利用新的训练集,训练得到 $M$ 个子模型 $\{h_1, h_2, ..., h_M\}$
  3. 对于分类问题,采用投票的方法,得票最多子模型的分类类别为最终的类别;对于回归问题,采用简单的平均方法得到预测值。

Bagging 算法如下所示:

\begin{algorithm}
\caption{Bagging 算法}
\begin{algorithmic}
\REQUIRE \\
    学习算法 $L$ \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    Bagging 算法 $h_f\left(x\right)$
\FUNCTION{Bagging}{$L, M, T$}
\FOR{$m = 1$ \TO $M$}
    \STATE $T_m \gets $ boostrap sample from training set $T$
    \STATE $h_m \gets L\left(T_m\right)$
\ENDFOR
\STATE $h_f\left(x\right) \gets \text{sign} \left(\sum_{m=1}^{M} h_m\left(x\right)\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

假设对于一个包含 $M$ 个样本的数据集 $T$,利用自助采样,则一个样本始终不被采用的概率是 $\left(1 - \frac{1}{M}\right)^M$,取极限有:

$$ \lim_{x \to \infty} \left(1 - \dfrac{1}{M}\right)^M = \dfrac{1}{e} \approx 0.368 $$

即每个学习器仅用到了训练集中 $63.2\%$ 的数据集,剩余的 $36.8\%$ 的训练集样本可以用作验证集对于学习器的泛化能力进行包外估计 (out-of-bag estimate)。

随机森林 (Random Forests)

随机森林 (Random Forests) 6 是一种利用决策树作为基学习器的 Bagging 集成学习算法。随机森林模型的构建过程如下:

作为一种 Bagging 集成算法,随机森林同样采用有放回的采样,对于总体训练集 $T$,抽样一个子集 $T_{sub}$ 作为训练样本集。除此之外,假设训练集的特征个数为 $d$,每次仅选择 $k\left(k < d\right)$ 个构建决策树。因此,随机森林除了能够做到样本扰动外,还添加了特征扰动,对于特征的选择个数,推荐值为 $k = \log_2 d$ 6

每次根据采样得到的数据和特征构建一棵决策树。在构建决策树的过程中,会让决策树生长完全而不进行剪枝。构建出的若干棵决策树则组成了最终的随机森林。

随机森林在众多分类算法中表现十分出众 7,其主要的优点包括:

  1. 由于随机森林引入了样本扰动和特征扰动,从而很大程度上提高了模型的泛化能力,尽可能地避免了过拟合现象的出现。
  2. 随机森林可以处理高维数据,无需进行特征选择,在训练过程中可以得出不同特征对模型的重要性程度。
  3. 随机森林的每个基分类器采用决策树,方法简单且容易实现。同时每个基分类器之间没有相互依赖关系,整个算法易并行化。

Boosting

Boosting 是一种提升算法,可以将弱的学习算法提升 (boost) 为强的学习算法。基本思路如下:

  1. 利用初始训练样本集训练得到一个基学习器。
  2. 提高被基学习器误分的样本的权重,使得那些被错误分类的样本在下一轮训练中可以得到更大的关注,利用调整后的样本训练得到下一个基学习器。
  3. 重复上述步骤,直至得到 $M$ 个学习器。
  4. 对于分类问题,采用有权重的投票方式;对于回归问题,采用加权平均得到预测值。

Adaboost

Adaboost 8 是 Boosting 算法中有代表性的一个。原始的 Adaboost 算法用于解决二分类问题,因此对于一个训练集

$$ T = \{\left(x_1, y_1\right), \left(x_2, y_2\right), ..., \left(x_n, y_n\right)\} $$

其中 $x_i \in \mathcal{X} \subseteq \mathbb{R}^n, y_i \in \mathcal{Y} = \{-1, +1\}$,首先初始化训练集的权重

$$ \begin{equation} \begin{split} D_1 =& \left(w_{11}, w_{12}, ..., w_{1n}\right) \\ w_{1i} =& \dfrac{1}{n}, i = 1, 2, ..., n \end{split} \end{equation} $$

根据每一轮训练集的权重 $D_m$,对训练集数据进行抽样得到 $T_m$,再根据 $T_m$ 训练得到每一轮的基学习器 $h_m$。通过计算可以得出基学习器 $h_m$ 的误差为 $\epsilon_m$,根据基学习器的误差计算得出该基学习器在最终学习器中的权重系数

$$ \alpha_m = \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m} $$

更新训练集的权重

$$ \begin{equation} \begin{split} D_{m+1} =& \left(w_{m+1, 1}, w_{m+1, 2}, ..., w_{m+1, n}\right) \\ w_{m+1, i} =& \dfrac{w_{m, i}}{Z_m} \exp \left(-\alpha_m y_i h_m\left(x_i\right)\right) \end{split} \end{equation} $$

其中 $Z_m$ 为规范化因子

$$ Z_m = \sum_{i = 1}^{n} w_{m, i} \exp \left(-\alpha_m y_i h_m \left(x_i\right)\right) $$

从而保证 $D_{m+1}$ 为一个概率分布。最终根据构建的 $M$ 个基学习器得到最终的学习器:

$$ h_f\left(x\right) = \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right) $$

AdaBoost 算法过程如下所示:

\begin{algorithm}
\caption{AdaBoost 算法}
\begin{algorithmic}
\REQUIRE \\
    学习算法 $L$ \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    AdaBoost 算法 $h_f\left(x\right)$
\FUNCTION{AdaBoost}{$L, M, T$}
\STATE $D_1\left(x\right) \gets 1 / n$
\FOR{$m = 1$ \TO $M$}
    \STATE $T_{sub} \gets $ sample from training set $T$ with weights
    \STATE $h_m \gets L\left(T_{sub}\right)$
    \STATE $\epsilon_m\gets Error\left(h_m\right)$
    \IF{$\epsilon_m > 0.5$}
        \BREAK
    \ENDIF
    \STATE $\alpha_m \gets \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m}$
    \STATE $D_{m+1} \gets \dfrac{D_m \exp \left(-\alpha_m y h_m\left(x\right)\right)}{Z_m}$
\ENDFOR
\STATE $h_f\left(x\right) \gets \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

GBDT (GBM, GBRT, MART)

GBDT (Gradient Boosting Decision Tree) 是另一种基于 Boosting 思想的集成算法,除此之外 GBDT 还有很多其他的叫法,例如:GBM (Gradient Boosting Machine),GBRT (Gradient Boosting Regression Tree),MART (Multiple Additive Regression Tree) 等等。GBDT 算法由 3 个主要概念构成:Gradient Boosting (GB),Regression Decision Tree (DT 或 RT) 和 Shrinkage。

从 GBDT 的众多别名中可以看出,GBDT 中使用的决策树并非我们最常用的分类树,而是回归树。分类树主要用于处理响应变量为因子型的数据,例如天气 (可以为晴,阴或下雨等)。回归树主要用于处理响应变量为数值型的数据,例如商品的价格。当然回归树也可以用于二分类问题,对于回归树预测出的数值结果,通过设置一个阈值即可以将数值型的预测结果映射到二分类问题标签上,即 $\mathcal{Y} = \{-1, +1\}$

对于 Gradient Boosting 而言,首先,Boosting 并不是 Adaboost 中 Boost 的概念,也不是 Random Forest 中的重抽样。在 Adaboost 中,Boost 是指在生成每个新的基学习器时,根据上一轮基学习器分类对错对训练集设置不同的权重,使得在上一轮中分类错误的样本在生成新的基学习器时更被重视。GBDT 中在应用 Boost 概念时,每一轮所使用的数据集没有经过重抽样,也没有更新样本的权重,而是每一轮选择了不用的回归目标,即上一轮计算得到的残差 (Residual)。其次,Gradient 是指在新一轮中在残差减少的梯度 (Gradient) 上建立新的基学习器。

下面我们通过一个年龄预测的 示例 (较之原示例有修改) 简单介绍 GBDT 的工作流程。

假设存在 4 个人 $P = \{p_1, p_2, p_3, p_4\}$,他们的年龄分别为 $14, 16, 24, 26$。其中 $p_1, p_2$ 分别是高一和高三学生,$p_3, p_4$ 分别是应届毕业生和工作两年的员工。利用原始的决策树模型进行训练可以得到如下图所示的结果:

GBDT-Descision-Tree-1

利用 GBDT 训练模型,由于数据量少,在此我们限定每个基学习器中的叶子节点最多为 2 个,即树的深度最大为 1 层。训练得到的结果如下图所示:

GBDT-Descision-Tree-2

在训练第一棵树过程中,利用年龄作为预测值,根据计算可得由于 $p_1, p_2$ 年龄相近,$p_3, p_4$ 年龄相近被划分为两组。通过计算两组中真实年龄和预测的年龄的差值,可以得到第一棵树的残差 $R = \{-1, 1, -1, 1\}$。因此在训练第二棵树的过程中,利用第一棵树的残差作为标签值,最终所有人的年龄均正确被预测,即最终的残差均为 $0$

则对于训练集中的 4 个人,利用训练得到的 GBDT 模型进行预测,结果如下:

  1. $p_1$ :14 岁高一学生。购物较少,经常问学长问题,预测年龄 $Age = 15 - 1 = 14$
  2. $p_2$ :16 岁高三学生。购物较少,经常回答学弟问题,预测年龄 $Age = 15 + 1 = 16$
  3. $p_3$ :24 岁应届毕业生。购物较多,经常问别人问题,预测年龄 $Age = 25 - 1 = 24$
  4. $p_4$ :26 岁 2 年工作经验员工。购物较多,经常回答别人问题,预测年龄 $Age = 25 + 1 = 26$

整个 GBDT 算法流程如下所示:

\begin{algorithm}
\caption{GBDT 算法}
\begin{algorithmic}
\REQUIRE \\
    子模型个数 $M$ \\
    训练数据集 $T = \{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)\}$
\ENSURE \\
    GBDT 算法 $h_f\left(x\right)$
\FUNCTION{GBDT}{$M, T$}
\STATE $F_1\left(x\right) \gets \sum_{i = 1}^{N} y_i / N$
\FOR{$m = 1$ \TO $M$}
\STATE $r_m \gets y - F_m \left(x\right)$
\STATE $T_m \gets \left(x, r_m\right)$
\STATE $h_m \gets RegressionTree \left(T_m\right)$
\STATE $\alpha_m \gets \dfrac{\sum_{i = 1}^{N} r_{im} h_m \left(x_i\right)}{\sum_{i = 1}^{N} h_m \left(x_i\right)^2}$
\STATE $F_m \left(x\right) = F_{m-1} \left(x\right) + \alpha_m h_m \left(x\right)$
\ENDFOR
\STATE $h_f\left(x\right) =  F_M \left(x\right)$
\RETURN $h_f\left(x\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

GBDT 中也应用到了 Shrinkage 的思想,其基本思想可以理解为每一轮利用残差学习得到的回归树仅学习到了一部分知识,因此我们无法完全信任一棵树的结果。Shrinkage 思想认为在新的一轮学习中,不能利用全部残差训练模型,而是仅利用其中一部分,即:

$$ r_m = y - s F_m \left(x\right), 0 \leq s \leq 1 $$

注意,这里的 Shrinkage 和学习算法中 Gradient 的步长是两个不一样的概念。Shrinkage 设置小一些可以避免发生过拟合现象;而 Gradient 中的步长如果设置太小则会陷入局部最优,如果设置过大又容易结果不收敛。

XGBoost

XGBoost 是由 Chen 等人 9 提出的一种梯度提升树模型框架。XGBoost 的基本思想同 GBDT 一样,对于一个包含 $n$ 个样本和 $m$ 个特征的数据集 $\mathcal{D} = \left\{\left(\mathbf{x}_i, y_i\right)\right\}$,其中 $\left|\mathcal{D}\right| = n, \mathbf{x}_i \in \mathbb{R}^m, y_i \in \mathbb{R}$,一个集成树模型可以用 $K$ 个加法函数预测输出:

$$ \hat{y}_i = \phi \left(\mathbf{x}_i\right) = \sum_{k=1}^{K}{f_k \left(\mathbf{x}_i\right)}, f_k \in \mathcal{F} $$

其中,$\mathcal{F} = \left\{f \left(\mathbf{x}\right) = w_{q \left(\mathbf{x}\right)}\right\} \left(q: \mathbb{R}^m \to T, w \in \mathbb{R}^T\right)$ 为回归树 (CART),$q$ 表示每棵树的结构,其将一个样本映射到最终的叶子节点,$T$ 为叶子节点的数量,每个 $f_w$ 单独的对应一棵结构为 $q$ 和权重为 $w$ 的树。不同于决策树,每棵回归树的每个叶子节点上包含了一个连续的分值,我们用 $w_i$ 表示第 $i$ 个叶子节点上的分值。

XGBoost 首先对损失函数进行了改进,添加了 L2 正则项,同时进行了二阶泰勒展开。损失函数表示为:

$$ \begin{equation} \begin{split} \mathcal{L} \left(\phi\right) = \sum_{i}{l \left(\hat{y}_i, y_i\right)} + \sum_{k}{\Omega \left(f_k\right)} \\ \text{where} \ \Omega \left(f\right) = \gamma T + \dfrac{1}{2} \lambda \left\| w \right\|^2 \end{split} \end{equation} $$

其中,$l$ 为衡量预测值 $\hat{y}_i$ 和真实值 $y_i$ 之间差异的函数,$\Omega$ 为惩罚项,$\gamma$$\lambda$ 为惩罚项系数。

我们用 $\hat{y}_i^{\left(t\right)}$ 表示第 $t$ 次迭代的第 $i$ 个实例,我们需要增加 $f_t$ 来最小化如下的损失函数:

$$ \mathcal{L}^{\left(t\right)} = \sum_{i=1}^{n}{l \left(y_i, \hat{y}_i^{\left(t-1\right)} + f_t \left(\mathbf{x}_i\right)\right)} + \Omega \left(f_t\right) $$

对上式进行二阶泰勒展开有:

$$ \mathcal{L}^{\left(t\right)} \simeq \sum_{i=1}^{n}{\left[l \left(y_i, \hat{y}_i^{\left(t-1\right)}\right) + g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$

其中,$g_i = \partial_{\hat{y}^{\left(t-1\right)}} l \left(y_i, \hat{y}^{\left(t-1\right)}\right), h_i = \partial_{\hat{y}^{\left(t-1\right)}}^{2} l \left(y_i, \hat{y}^{\left(t-1\right)}\right)$ 分别为损失函数的一阶梯度和二阶梯度。去掉常数项,第 $t$ 步的损失函数可以简化为:

$$ \tilde{\mathcal{L}}^{\left(t\right)} = \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$

$I_j = \left\{i \ | \ q \left(\mathbf{x}_i\right) = j\right\}$ 表示叶子节点 $j$ 的实例集合,上式可重写为:

$$ \begin{equation} \begin{split} \tilde{\mathcal{L}}^{\left(t\right)} &= \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \gamma T + \dfrac{1}{2} \lambda \sum_{j=1}^{T}{w_j^2} \\ &= \sum_{j=1}^{T}{\left[\left(\sum_{i \in I_j}{g_i}\right) w_j + \dfrac{1}{2} \left(\sum_{i \in I_j}{h_i + \lambda}\right) w_j^2\right]} + \gamma T \end{split} \end{equation} $$

对于一个固定的结构 $q \left(\mathbf{x}\right)$,可以通过下式计算叶子节点 $j$ 的最优权重 $w_j^*$

$$ w_j^* = - \dfrac{\sum_{i \in I_j}{g_i}}{\sum_{i \in I_j}{h_i} + \lambda} $$

进而计算对应的最优值:

$$ \tilde{\mathcal{L}}^{\left(t\right)} \left(q\right) = - \dfrac{1}{2} \sum_{j=1}^{T}{\dfrac{\left(\sum_{i \in I_j}{g_i}\right)^2}{\sum_{i \in I_j}{h_i} + \lambda}} + \gamma T $$

上式可以作为评价树的结构 $q$ 的评分函数。通常情况下很难枚举所有可能的树结构,一个贪心的算法是从一个节点出发,逐层的选择最佳的分裂节点。令 $I_L$$I_R$ 分别表示分裂后左侧和右侧的节点集合,令 $I = I_L \cup I_R$,则分裂后损失的减少量为:

$$ \mathcal{L}_{\text{split}} = \dfrac{1}{2} \left[\dfrac{\left(\sum_{i \in I_L}{g_i}\right)^2}{\sum_{i \in I_L}{h_i} + \lambda} + \dfrac{\left(\sum_{i \in I_R}{g_i}\right)^2}{\sum_{i \in I_R}{h_i} + \lambda} - \dfrac{\left(\sum_{i \in I}{g_i}\right)^2}{\sum_{i \in I}{h_i} + \lambda}\right] - \gamma $$

XGBoost 也采用了 Shrinkage 的思想减少每棵树的影响,为后续树模型留下更多的改进空间。同时 XGBoost 也采用了随机森林中的特征下采样 (列采样) 方法用于避免过拟合,同时 XGBoost 也支持样本下采样 (行采样)。XGBoost 在分裂点的查找上也进行了优化,使之能够处理无法将全部数据读入内存的情况,同时能够更好的应对一些由于数据缺失,大量零值和 One-Hot 编码导致的特征稀疏问题。除此之外,XGBoost 在系统实现,包括:并行化,Cache-Aware 加速和数据的核外计算 (Out-of-Core Computation) 等方面也进行了大量优化,相关具体实现请参见论文和 文档

LightGBM

LightGBM 是由微软研究院的 Ke 等人 10 提出了一种梯度提升树模型框架。之前的 GBDT 模型在查找最优分裂点时需要扫描所有的样本计算信息增益,因此其计算复杂度与样本的数量和特征的数量成正比,这使得在处理大数据量的问题时非常耗时。LightGBM 针对这个问题提出了两个算法:

  1. Gradient-based One-Side Sampling (GOSS)
  2. Exclusive Feature Bundling (EFB)

Gradient-based One-Side Sampling

在 AdaBoost 中,样本的权重很好的诠释了数据的重要性,但在 GBDT 中并没有这样的权重,因此无法直接应用 AdaBoost 的采样方法。幸运的是 GBDT 中每个样本的梯度可以为我们的数据采样提供有用的信息。当一个样本具有较小的梯度时,其训练的误差也较小,表明其已经训练好了。一个直观的想法就是丢弃这些具有较小梯度的样本,但是这样操作会影响整个数据的分布,从而对模型的精度造成损失。

GOSS 的做法是保留具有较大梯度的样本,并从具有较小梯度的样本中随机采样。同时为了补偿对数据分布的影响,在计算信息增益的时候,GOSS 针对梯度较小的样本引入了一个常数乘子。这样就保证了模型更多的关注未得到较好训练的数据,同时又不会对原始数据分布改变过多。整个算法流程如下:

\begin{algorithm}
\caption{GOSS 算法}
\begin{algorithmic}
\INPUT \\
    训练数据 $I$ \\
    迭代次数 $d$ \\
    具有较大梯度数据的采样比例 $a$ \\
    具有较小梯度数据的采样比例 $b$ \\
    损失函数 $loss$ \\
    基学习器 $L$
\FUNCTION{GOSS}{$I, d, a, b, loss, L$}
\STATE $\text{models} \gets \varnothing$
\STATE $\text{fact} \gets \dfrac{1-a}{b}$
\STATE $\text{topN} \gets a \times \text{len} \left(I\right)$
\STATE $\text{randN} \gets b \times \text{len} \left(I\right)$
\FOR{$i = 1$ \TO $d$}
    \STATE $\text{preds} \gets \text{models.predict} \left(I\right)$
    \STATE $\text{g} \gets loss \left(I, \text{preds}\right)$
    \STATE $\text{w} \gets \left\{1, 1, \dotsc\right\}$
    \STATE $\text{sorted} \gets \text{GetSortedIndices} \left(\text{abs} \left(\text{g}\right)\right)$
    \STATE $\text{topSet} \gets \text{sorted[1:topN]}$
    \STATE $\text{randSet} \gets \text{RandomPick} \left(\text{sorted[topN:len}\left(I\right)\text{]}, \text{randN}\right)$
    \STATE $\text{usedSet} \gets \text{topSet} \cup \text{randSet}$
    \STATE $\text{w[randSet]} \gets \text{w[randSet]} \times \text{fact}$
    \STATE $\text{newModel} \gets L \left(I \text{[usedSet]}, - \text{g[usedSet]}, \text{w[usedSet]}\right)$
    \STATE $\text{models} \gets \text{models} \cup \text{newModel}$
\ENDFOR
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

Exclusive Feature Bundling

高维数据往往是稀疏的,特征空间的稀疏性为我们提供了可能的近似无损的特征降维实现。进一步而言,在稀疏的特征空间中,很多特征之间是互斥的,也就是说它们不同时取非零值。因此,我们就可以将这些互斥的特征绑定成一个特征。由于 $\#bundle \ll \#feature$,因此构建直方图的复杂度就可以从 $O \left(\#data \times \#features\right)$ 减小至 $O \left(\#data \times \#bundle\right)$,从而在不损失精度的情况下加速模型的训练。这样我们就需要解决如下两个问题:

  1. 确定对哪些特征进行绑定。
  2. 如果对这些特征进行绑定。

对哪些特征进行绑定可以利用 图着色问题 进行解决。对于一个图 $G = \left(V, E\right)$,将 $G$关联矩阵 中的每一行看成特征,得到 $|V|$ 个特征,从而可以得出图中颜色相同的节点即为互斥的特征。算法如下:

\begin{algorithm}
\caption{Greedy Bundling}
\begin{algorithmic}
\INPUT \\
    特征 $F$ \\
    最大冲突数量 $K$
\OUTPUT \\
    需要绑定的特征 $bundles$
\FUNCTION{GreedyBundling}{$F, K$}
\STATE Construct graph $G$
\STATE $\text{searchOrder} \gets G.\text{sortByDegree}()$
\STATE $\text{bundles} \gets \varnothing$
\STATE $\text{bundlesConflict} \gets \varnothing$
\FOR{$i$ $\in$ searchOrder}
    \STATE $\text{needNew} \gets$ \TRUE
    \FOR{$j = 1$ \TO len(bundles)}
        \STATE $\text{cnt} \gets$ ConflictCnt(bundles[$j$],F[$i$])
        \IF{cnt $+$ bundlesConflict[$i$] $\leq K$}
            \STATE bundles[$j$].add($F[i]$)
            \STATE $\text{needNew} \gets$ \FALSE
            \BREAK
        \ENDIF
    \ENDFOR
    \IF{needNew}
        \STATE $bundles \gets bundles \cup F[i]$
    \ENDIF
\ENDFOR
\RETURN $bundles$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

上述算法的复杂度为 $O \left(\#feature^2\right)$ ,并且仅在模型训练前运行一次。对于特征数不是很大的情况是可以接受的,但当特征数量很大时算法效率并不令人满意。进一步的优化是在不构造图的情况下进行高效的排序,即根据非零值的数量进行排序,更多的非零值意味着更高的冲突概率。

合并特征的关键在于确保原始特征的值能够从合并后的特征之中识别出来。由于基于直方图的算法保存的是原始特征的离散桶,而非连续的值,因此我们可以将互斥的特征置于不同的桶内。算法如下:

\begin{algorithm}
\caption{Merge Exclusive Features}
\begin{algorithmic}
\REQUIRE \\
    数据数量 $numData$ \\
    一组互斥特征 $F$
\ENSURE \\
    新的分箱 $newBin$ \\
    分箱范围 $binRanges$
\FUNCTION{MergeExclusiveFeatures}{$numData, F$}
\STATE $\text{binRages} \gets \left\{0\right\}$
\STATE $\text{totalBin} \gets 0$
\FOR{$f$ $\in$ $F$}
    \STATE $\text{totalBin} \gets \text{totalBin} + \text{f.numBin}$
    \STATE $\text{binRanges} \gets \text{binRanges} \cup \text{totalBin}$
\ENDFOR
\STATE $\text{newBin} \gets \text{Bin} \left(numData\right)$
\FOR{$i = 1$ \TO $numData$}
    \STATE $\text{newBin}[i] \gets 0$
    \FOR{$j = 1$ \TO $\text{len} \left(F\right)$}
        \IF{$F[j].\text{bin}[i] \neq 0$}
            \STATE $\text{newBin}[i] \gets F[j].\text{bin}[i] + \text{binRanges}[j]$
        \ENDIF
    \ENDFOR
\ENDFOR
\RETURN $newBin, binRanges$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

EFB 算法可以将大量的互斥特征合并为少量的稠密特征,从而通过避免对零值特征的计算提高算法的运行效率。

Tree Growth

大多的决策树算法通过逐层 (Level-wise / Depth-wise) 的方法生成树,如下图所示:

Level-Wise-Tree-Growth

LightGBM 采用了另外一种 Leaf-wise (或称 Best-first) 的方式生成 11,如下图所示:

Leaf-Wise-Tree-Growth

该方法想选择具有最大 Delta Loss 值的叶子节点进行生长。在固定叶子节点数量的情况下,Leaf-wise 的生长方式比 Level-wise 的方式更容易获得较低的损失值。Leaf-wise 的生长方式在数据量较小时容易产生过拟合的现象,在 LightGBM 中可以通过限制树的最大深度减轻该问题。

更多有关 LightGBM 的优化请参见论文和 文档

CatBoost

CatBoost 是由俄罗斯 Yandex 公司 12 13 提出的一种梯度提升树模型框架。相比于之前的实现,CatBoost 的优化主要包括如下几点:

  1. 提出了一种处理分类特征 (Categorical Features) 的算法。
  2. 提出了一种解决预测偏移 (Prediction Shift) 问题的算法。

分类特征

分类特征是由一些离散的值构成的集合,其无法直接应用在提升树模型中,一个常用的方法是利用 One-Hot 编码对分类特征进行预处理,将其转化成值特征。

另一种方法是根据样本的标签值计算分类特征的一些统计量 (Target Statistics, TS)。令 $\mathcal{D} = \left\{\left(\mathbf{x}_k, y_k\right)\right\}_{k=1, \dotsc, n}$ 为一个数据集,其中 $\mathbf{x}_k = \left(x_k^1, \dotsc, x_k^m\right)$ 为一个包含 $m$ 个特征的向量 (包含值特征和分类特征),$y_k \in \mathbb{R}$ 为标签值。最简单的做法是将分类特征替换为全量训练数据上对应特征值相同的标签值的均值,即 $\hat{x}_k^i \approx \mathbb{E} \left(y \ | \ x^i = x_k^i\right)$

一个简单估计 $\mathbb{E} \left(y \ | \ x^i = x_k^i\right)$ 的方法是对具有相同类型 $x_k^i$ 的样本的标签值求均值。但这种估计对于低频的分类噪音较大,因此我们可以通过一个先验 $P$ 来进行平滑:

$$ \hat{x}_k^i = \dfrac{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$

其中,$a > 0$ 为先验系数,$\boldsymbol{1}$ 为指示函数,通常 $P$ 取整个数据集标签值的均值。

上述贪婪 (Greedy) 的做法的问题在于存在目标泄露 (Target Leakage),即特征 $\hat{x}_k^i$ 是通过 $\mathbf{x}_k$ 的目标 $y_k$ 计算所得。这会导致条件偏移 (Conditional Shift) 的问题 14,即 $\hat{x}^i \ | \ y$ 的分布在训练集和测试集上不同。因此在计算 TS 时需要满足如下特性:

特性 1

$\mathbb{E} \left(\hat{x}^i \ | \ y = v\right) = \mathbb{E} \left(\hat{x}_k^i \ | \ y_k = v\right)$,其中 $\left(x_k, y_k\right)$ 为第 $k$ 个训练样本。

一种修正方法是在计算 TS 时使用排除掉 $\mathbf{x}_k$ 的一个子集,令 $\mathcal{D}_k \subset \mathcal{D} \setminus \left\{\mathbf{x}_k\right\}$,有:

$$ \hat{x}_k^i = \dfrac{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$

另一种方法是将训练集划分为两部分 $\mathcal{D} = \hat{\mathcal{D}}_0 \sqcup \hat{\mathcal{D}_1}$,利用 $\mathcal{D}_k = \hat{\mathcal{D}}_0$ 计算 TS,利用 $\hat{\mathcal{D}_1}$ 进行训练。虽然满足了 特性 1,但是这会导致计算 TS 和用于训练的数据均显著减少,因此还需要满足另一个特性:

特性 2

有效地利用所有的训练数据计算 TS 和训练模型。

对于训练样本 $\mathbf{x}_k$$\mathcal{D}_k = \mathcal{D} \setminus \mathbf{x}_k$,对于测试集,令 $\mathcal{D}_k = \mathcal{D}$,但这并没有解决 Target Leakage 问题。

Catboost 采用了一种更有效的策略:首先对于训练样本进行随机排列,得到排列下标 $\sigma$,之后对于每个训练样本仅利用“历史”样本来计算 TS,即:$\mathcal{D}_k = \left\{\mathbf{x}_j: \sigma \left(j\right) < \sigma \left(k\right)\right\}$,对于每个测试样本 $\mathcal{D}_k = \mathcal{D}$

Prediction Shift & Ordered Boosting

类似计算 TS,Prediction Shift 是由一种特殊的 Target Leakage 所导致的。对于第 $t$ 次迭代,我们优化的目标为:

$$ h^t = \mathop{\arg\min}_{h \in H} \mathbb{E} \left(-g^t \left(\mathbf{x}, y\right) - h \left(\mathbf{x}\right)\right)^2 \label{eq:catboost-obj} $$

其中,$g^t \left(\mathbf{x}, y\right) := \dfrac{\partial L \left(y, s\right)}{\partial s} \bigg\vert_{s = F^{t-1} \left(\mathbf{x}\right)}$。通常情况下会使用相同的数据集 $\mathcal{D}$ 进行估计:

$$ h^t = \mathop{\arg\min}_{h \in H} \dfrac{1}{n} \sum_{k=1}^{n}{\left(-g^t \left(\mathbf{x}_k, y_k\right) - h \left(\mathbf{x}_k\right)\right)^2} \label{eq:catboost-obj-approx} $$

整个偏移的链条如下:

  1. 梯度的条件分布 $g^t \left(\mathbf{x}_k, y_k\right) \ | \ \mathbf{x}_k$ 同测试样本对应的分布 $g^t \left(\mathbf{x}, y\right) \ | \ \mathbf{x}$ 存在偏移。
  2. 由式 $\ref{eq:catboost-obj}$ 定义的基学习器 $h^t$ 同由式 $\ref{eq:catboost-obj-approx}$ 定义的估计方法存在偏移。
  3. 最终影响训练模型 $F^t$ 的泛化能力。

每一步梯度的估计所使用的标签值同构建当前模型 $F^{t-1}$ 使用的相同。但是,对于一个训练样本 $\mathbf{x}_k$ 而言,条件分布 $F^{t-1} \left(\mathbf{x}_k \ | \ \mathbf{x}_k\right)$ 相对一个测试样本 $\mathbf{x}$ 对应的分布 $F^{t-1} \left(\mathbf{x}\right) \ | \ \mathbf{x}$ 发生了偏移,我们称这为预测偏移 (Prediction Shift)。

CatBoost 提出了一种解决 Prediction Shift 的算法:Ordered Boosting。假设对于训练数据进行随机排序得到 $\sigma$,并有 $n$ 个不同的模型 $M_1, \dotsc, M_n$,每个模型 $M_i$ 仅利用随机排序后的前 $i$ 个样本训练得到。算法如下:

\begin{algorithm}
\caption{Ordered Boosting}
\begin{algorithmic}
\INPUT \\
    训练集 $\left\{\left(\mathbf{x}_k, y_k\right)\right\}_{k=1}^{n}$ \\
    树的个数 $I$
\OUTPUT \\
    模型 $M_n$
\FUNCTION{OrderedBoosting}{$numData, F$}
\STATE $\sigma \gets \text{random permutation of} \left[1, n\right]$
\STATE $M_i \gets 0$ for $i = 1, \dotsc, n$
\FOR{$t = 1$ \TO $I$}
    \FOR{$i = 1$ \TO $n$}
        \STATE $r_i \gets y_i - M_{\sigma \left(i\right) -1} \left(\mathbf{x}_i\right)$
    \ENDFOR
    \FOR{$i = 1$ \TO $n$}
        \STATE $\Delta M \gets \text{LearnModel} \left(\left(\mathbf{x}_j, r_j\right): \sigma \left(j\right) \leq i\right)$
        \STATE $M_i \gets M_i + \Delta M$
    \ENDFOR
\ENDFOR
\RETURN $M_n$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

在计算 TS 和进行 Ordered Boosting 时我们均使用了随机排列并得到 $\sigma_{cat}$$\sigma_{boost}$。需要注意的是在将两部分合并为一个算法时,我们需要令 $\sigma_{cat} = \sigma_{boost}$ 避免 Prediction Shift。这样可以保证目标 $y_i$ 不用于训练模型 $M_i$ (既不参与计算 TS,也不用于梯度估计)。

更多 CatBoost 的实现细节请参见论文和 文档

不同实现的比较

针对 scikit-learnXGBoostLightGBMCatBoost 4 种 GBDT 的具体实现,下表汇总了各自的相关特性:

scikit-learn XGBoost LightGBM CatBoost
当前版本 0.20.1 0.81 2.2.2 0.11.1
实现语言 C, C++, Python C, C++ C, C++ C++
API 语言 Python Python, R, Java, Scala, C++ and more Python, R Python, R
模型导出 JPMML 15 JPMML 16, ONNX 17 18 ONNX 17 19 CoreML, Python, C++, JSON 20
多线程 No Yes Yes Yes
GPU No Yes 21 Yes 22 Yes 23
多 GPU No Yes 21 No Yes 23
Boosting 类型 Gradient Boosted Tree (GBDT) GBDT (booster: gbtree)
Generalized Linear Model, GLM (booster: gbliner)
Dropout Additive Regression Tree, DART (booster: dart)
GBDT (boosting: gbdt)
Random Forest (boosting: rf)
DART (boosting: dart)
Gradient-based One-Side Sampling, GOSS (bossting: goss)
Ordered (boosting_type: Ordered)
Plain (bossting_type: Plain)
Level-wise (Depth-wise) Split Yes Yes
(grow_policy: depthwise)
No Yes
Leaf-wise (Best-first) Split No Yes
(grow_policy: lossguide)
Yes No
Histogram-based Split No Yes
(tree_method: hist / gpu_hist)
Yes Yes
过拟合控制 Yes
(max_depth, …)
Yes
(max_depth, max_leaves, gamma, reg_alpha, reg_lamda, …)
Yes
(max_depth, num_leaves, gamma, reg_alpha, reg_lamda, drop_rate, …)
Yes
(max_depth, reg_lambda, …)
分类特征 No No Yes
(categorical_feature)
Yes
(cat_features)
缺失值处理 No Yes Yes
(use_missing)
Yes
不均衡数据 No Yes
(scale_pos_weight, max_delta_step)
Yes
(scale_pos_weight, poisson_max_delta_step)
Yes
(scale_pos_weight)

不同实现的性能分析和比较可参见如下文章,括号中内容为分析的实现库:

Stacking

Stacking 本身是一种集成学习方法,同时也是一种模型组合策略,我们首先介绍一些相对简单的模型组合策略:平均法投票法

对于 数值型的输出 $h_i \left(\mathbf{x}\right) \in \mathbb{R}$

$$ H \left(\mathbf{x}\right) = \dfrac{1}{M} \sum_{i=1}^{M}{h_i \left(\mathbf{x}\right)} $$

$$ H \left(\mathbf{x}\right) = \sum_{i=1}^{M}{w_i h_i \left(\mathbf{x}\right)} $$

其中,$w_i$ 为学习器 $h_i$ 的权重,且 $w_i \geq 0, \sum_{i=1}^{T}{w_i} = 1$

对于 分类型的任务,学习器 $h_i$ 从类别集合 $\left\{c_1, c_2, \dotsc, c_N\right\}$ 中预测一个标签。我们将 $h_i$ 在样本 $\mathbf{x}$ 上的预测输出表示为一个 $N$ 维向量 $\left(h_i^1 \left(\mathbf{x}\right); h_i^2 \left(\mathbf{x}\right); \dotsc, h_i^N \left(\mathbf{x}\right)\right)$,其中 $h_i^j \left(\mathbf{x}\right)$$h_i$ 在类型标签 $c_j$ 上的输出。

$$ H \left(\mathbf{x}\right) = \begin{cases} c_j, & \displaystyle\sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right) > 0.5 \displaystyle\sum_{k=1}^{N}{\displaystyle\sum_{i=1}^{M}{h_i^k \left(\mathbf{x}\right)}}} \\ \text{refuse}, & \text{other wise} \end{cases} $$

即如果一个类型的标记得票数过半,则预测为该类型,否则拒绝预测。

$$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right)}} $$

即预测为得票数最多的类型,如果同时有多个类型获得相同最高票数,则从中随机选取一个。

$$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{w_i h_i^j \left(\mathbf{x}\right)}} $$

其中,$w_i$ 为学习器 $h_i$ 的权重,且 $w_i \geq 0, \sum_{i=1}^{M}{w_i} = 1$

绝对多数投票提供了“拒绝预测”,这为可靠性要求较高的学习任务提供了一个很好的机制,但如果学习任务要求必须有预测结果时则只能选择相对多数投票法和加权投票法。在实际任务中,不同类型的学习器可能产生不同类型的 $h_i^j \left(\boldsymbol{x}\right)$ 值,常见的有:

Stacking 24 25 方法又称为 Stacked Generalization,是一种基于分层模型组合的集成算法。Stacking 算法的基本思想如下:

  1. 利用初级学习算法对原始数据集进行学习,同时生成一个新的数据集。
  2. 根据从初级学习算法生成的新数据集,利用次级学习算法学习并得到最终的输出。

对于初级学习器,可以是相同类型也可以是不同类型的。在新的数据集中,初级学习器的输出被用作次级学习器的输入特征,初始样本的标记仍被用作次级学习器学习样本的标记。Stacking 算法的流程如下图所示:

Stacking

Stacking 算法过程如下:

\begin{algorithm}
\caption{Stacking 算法}
\begin{algorithmic}
\REQUIRE \\
    初级学习算法 $L = \{L_1, L_2, ... L_M\}$ \\
    次级学习算法 $L'$ \\
    训练数据集 $T = \{(\mathbf{x}_1, y_1), (\mathbf{x}_2, y_2), ..., (\mathbf{x}_N, y_N)\}$
\ENSURE \\
    Stacking 算法 $h_f\left(x\right)$
\FUNCTION{Stacking}{$L, L', T$}
\FOR{$m$ = $1$ to $M$}
  \STATE $h_t \gets L_m \left(T\right)$
\ENDFOR
\STATE $T' \gets \varnothing$
\FOR{$i$ = $1$ to $N$}
  \FOR{$m$ = $1$ to $M$}
    \STATE $z_{im} \gets h_m(\mathbf{x}_i)$
  \ENDFOR
  \STATE $T' \gets T' \cup \left(\left(z_{i1}, z_{i2}, ..., z_{iM}\right), y_i\right)$
\ENDFOR
\STATE $h' \gets L' \left(T'\right)$
\STATE $h_f\left(\mathbf{x}\right) \gets h' \left(h_1\left(\mathbf{x}\right), h_2\left(\mathbf{x}\right), ..., h_M\left(\mathbf{x}\right)\right)$
\RETURN $h_f\left(\mathbf{x}\right)$
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

次级学习器的训练集是有初级学习器产生的,如果直接利用初级学习器的训练集生成次级学习器的训练集,过拟合风险会比较大 26。因此,一般利用在训练初级学习器中未使用过的样本来生成次级学习器的训练样本。以 $k$ 折交叉检验为例:初始的训练集 $T$ 被随机划分为 $k$ 个大小相近的集合 $T_1, T_2, ..., T_k$。令 $T_j$$\overline{T}_j = T \setminus T_j$ 表示第 $j$ 折的测试集和训练集。则对于 $M$ 个初级学习算法,学习器 $h_m^{\left(j\right)}$ 是根据训练集 $\overline{T}_j$ 生成的,对于测试集 $T_j$ 中的每个样本 $\mathbf{x}_i$,得到 $z_{im} = h_m^{\left(j\right)} \left(\mathbf{x}_i\right)$。则根据 $\mathbf{x}_i$ 所产生的次级学习器的训练样本为 $\mathbf{z}_i = \left(\left(z_{i1}, z_{i2}, ..., z_{iM}\right), y_i\right)$。最终利用 $M$ 个初级学习器产生的训练集 $T' = \{\left(\mathbf{z}_i, y_i\right)\}_{i=1}^N$ 训练次级学习器。

下图展示了一些基础分类器以及 Soft Voting 和 Stacking 两种融合策略的模型在 Iris 数据集分类任务上的决策区域。数据选取 Iris 数据集中的 Sepal Length 和 Petal Length 两个特征,Stacking 中的次级学习器选择 Logistic Regression,详细实现请参见 这里

Classifiers-Decision-Regions


  1. Dietterich, T. G. (2000, June). Ensemble methods in machine learning. In International workshop on multiple classifier systems (pp. 1-15). ↩︎

  2. Dietterich, T. G. (2002). Ensemble Learning, The Handbook of Brain Theory and Neural Networks, MA Arbib. ↩︎

  3. Laurent, H., & Rivest, R. L. (1976). Constructing optimal binary decision trees is NP-complete. Information processing letters, 5(1), 15-17. ↩︎

  4. Blum, A., & Rivest, R. L. (1989). Training a 3-node neural network is NP-complete. In Advances in neural information processing systems (pp. 494-501). ↩︎

  5. Breiman, L. (1996). Bagging predictors. Machine learning, 24(2), 123-140. ↩︎

  6. Breiman, L. (2001). Random forests. Machine learning, 45(1), 5-32. ↩︎ ↩︎

  7. Fernández-Delgado, M., Cernadas, E., Barro, S., & Amorim, D. (2014). Do we need hundreds of classifiers to solve real world classification problems?. The Journal of Machine Learning Research, 15(1), 3133-3181. ↩︎

  8. Freund, Y., & Schapire, R. E. (1997). A decision-theoretic generalization of on-line learning and an application to boosting. Journal of computer and system sciences, 55(1), 119-139. ↩︎

  9. Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. In Proceedings of the 22Nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (pp. 785–794). ↩︎

  10. Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., … Liu, T.-Y. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 30 (pp. 3146–3154). ↩︎

  11. Shi, H. (2007). Best-first Decision Tree Learning (Thesis). The University of Waikato. ↩︎

  12. Dorogush, A. V., Ershov, V., & Gulin, A. (2018). CatBoost: gradient boosting with categorical features support. arXiv preprint arXiv:1810.11363 ↩︎

  13. Prokhorenkova, L., Gusev, G., Vorobev, A., Dorogush, A. V., & Gulin, A. (2018). CatBoost: unbiased boosting with categorical features. In S. Bengio, H. Wallach, H. Larochelle, K. Grauman, N. Cesa-Bianchi, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 31 (pp. 6637–6647). ↩︎

  14. Zhang, K., Schölkopf, B., Muandet, K., & Wang, Z. (2013, February). Domain adaptation under target and conditional shift. In International Conference on Machine Learning (pp. 819-827). ↩︎

  15. https://github.com/jpmml/jpmml-sklearn ↩︎

  16. https://github.com/jpmml/jpmml-xgboost ↩︎

  17. https://github.com/onnx/onnx ↩︎ ↩︎

  18. https://pypi.org/project/winmltools ↩︎

  19. https://github.com/onnx/onnxmltools ↩︎

  20. https://tech.yandex.com/catboost/doc/dg/concepts/python-reference_catboost_save_model-docpage ↩︎

  21. https://xgboost.readthedocs.io/en/latest/gpu/index.html ↩︎ ↩︎

  22. https://lightgbm.readthedocs.io/en/latest/GPU-Tutorial.html ↩︎

  23. https://tech.yandex.com/catboost/doc/dg/features/training-on-gpu-docpage ↩︎ ↩︎

  24. Wolpert, D. H. (1992). Stacked generalization. Neural networks, 5(2), 241-259. ↩︎

  25. Breiman, L. (1996). Stacked regressions. Machine learning, 24(1), 49-64. ↩︎

  26. 周志华. (2016). 机器学习. 清华大学出版社. ↩︎

计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)

2018-11-18 08:00:00

计算复杂性

计算复杂性 (Computational Complexity) 是用于对一个问题求解所需的资源 (通常为 空间时间) 的度量。在评估一个算法的时候,除了算法本身的准确性以外,同时需要关注算法运行的时间以及占用的内存,从而根据实际情况选择合适的算法。

函数的增长

计算复杂性中的空间和时间的评估方法类似,在此我们更多的以时间复杂度为例。算法的运行时间刻画了算法的效率,对于一个输入规模为 $n$ 的问题,定义一个算法求解该问题 最坏情况 下的运行时间为 $T \left(n\right)$,我们可以使用一些 渐进记号 更加方便地对其进行描述。

对于一个给定的函数 $g \left(n\right)$$\Theta \left(g \left(n\right)\right)$ 可以表示如下函数的集合:

$$ \Theta \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c_1 > 0, c_2 > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c_1 g \left(n\right) \leq f \left(n\right) \leq c_2 g \left(n\right) \right\} $$

也就是说当 $n$ 足够大时,函数 $f \left(n\right)$ 能够被 $c_1 g \left(n\right)$$c_2 g \left(n\right)$ 夹在中间,我们称 $g \left(n\right)$$f \left(n\right)$ 的一个 渐进紧确界 (Asymptotically Tight Bound)

$\Theta$ 记号给出了一个函数的上界和下界,当只有一个 渐进上界 时,可使用 $O$ 记号。$O \left(g \left(n\right)\right)$ 表示的函数集合为:

$$ O \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) \leq c g \left(n\right)\right\} $$

$O$ 记号描述的为函数的上界,因此可以用它来限制算法在最坏情况下的运行时间。

$\Omega$ 记号提供了 渐进下界,其表示的函数集合为:

$$ \Omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) \leq f \left(n\right)\right\} $$

根据上面的三个渐进记号,不难证明如下定理:

定理 1

对于任意两个函数 $f \left(n\right)$$g \left(n\right)$,有 $f \left(n\right) = \Theta \left(g \left(n\right)\right)$,当且仅当 $f \left(n\right) = O \left(g \left(n\right)\right)$$f \left(n\right) = \Omega \left(g \left(n\right)\right)$

$O$ 记号提供的渐进上界可能是也可能不是渐进紧确的,例如 $2n^2 = O \left(n^2\right)$ 是渐进紧确的,但 $2n = O \left(n^2\right)$ 是非渐进紧确的。我们使用 $o$ 记号表示非渐进紧确的上界,其表示的函数集合为:

$$ o \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c > 0, \exists n_0 > 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) < c g \left(n\right)\right\} $$

$\omega$ 记号与 $\Omega$ 记号的关系类似于 $o$ 记号与 $O$ 记号的关系,我们使用 $\omega$ 记号表示一个非渐进紧确的下界,其表示的函数集合为:

$$ \omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c > 0, \exists n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) < f \left(n\right)\right\} $$

NP 完全性

计算问题可以按照在不同计算模型下所需资源的不同予以分类,从而得到一个对算法问题“难度”的类别,这就是复杂性理论中复杂性类概念的来源 1。对于输入规模为 $n$ 的问题,一个算法在最坏情况下的运行时间为 $O \left(n^k\right)$,其中 $k$ 为一个确定的常数,我们称这类算法为 多项式时间的算法

本节我们将介绍四类问题:P 类问题,NP 类问题,NPC 类问题和 NPH 类问题。

P 类问题 (Polynomial Problem,多项式问题) 是指能在多项式时间内 解决 的问题。

NP 类问题 (Non-Deteministic Polynomial Problem,非确定性多项式问题) 是指能在多项式时间内被 证明 的问题,也就是可以在多项式时间内对于一个给定的解验证其是否正确。所有的 P 类问题都是 NP 类问题,但目前 (截至 2018 年,下文如不做特殊说明均表示截至到该时间) 人类还未证明 $P \neq NP$ 还是 $P = NP$

在理解 NPC 类问题之前,我们需要引入如下几个概念:

  1. 最优化问题 (Optimization Problem)判定问题 (Decision Problem):最优化问题是指问题的每一个可行解都关联一个值,我们希望找到具有最佳值的可行解。判定问题是指问题的答案仅为“是”或“否”的问题。NP 完全性仅适用于判定问题,但通过对最优化问题强加一个界,可以将其转换为判定问题。
  2. 归约 (Reduction):假设存在一个判定问题 A,该问题的输入称之为实例,我们希望能够在多项式时间内解决该问题。假设存在另一个不同的判定问题 B,并且已知能够在多项式时间内解决该问题,同时假设存在一个过程,它可以将 A 的任何实例 $\alpha$ 转换成 B 的某个实例 $\beta$,转换操作需要在多项式时间内完成,同时两个实例的解是相同的。则我们称这一过程为多项式 规约算法 (Reduction Algorithm)。通过这个过程,我们可以将问题 A 的求解“归约”为对问题 B 的求解,从而利用问题 B 的“易求解性”来证明 A 的“易求解性”。

从而我们可以定义 NPC 类问题为:首先 NPC 类问题是一个 NP 类问题,其次所有的 NP 类问题都可以用多项式时间归约到这类问题。因此,只要找到 NPC 类问题的一个多项式时间的解,则所有的 NP 问题都可以通过多项式时间归约到该问题,并用多项式时间解决该问题,从而使得 $NP = P$,但目前,NPC 类问题并没有找到一个多项式时间的算法。

NPH 类问题定义为所有的 NP 类问题都可以通过多项式时间归约到这类问题,但 NPH 类问题不一定是 NP 类问题。NPH 类问题同样很难找到多项式时间的解,由于 NPH 类问题相比较 NPC 类问题放松了约束,因此即便 NPC 类问题找到了多项式时间的解,NPH 类问题仍可能无法在多项式时间内求解。

下图分别展示了 $P \neq NP$$P = NP$ 两种假设情况下四类问题之间的关系:

动态规划

动态规划 (Dynamic Programming, DP) 算法通常基于一个递归公式和一个或多个初始状态,并且当前子问题的解可以通过之前的子问题构造出来。动态规划算法求解问题的时间复杂度仅为多项式复杂度,相比其他解法,例如:回溯法,暴利破解法所需的时间要少。动态规划中的 “Programming” 并非表示利用计算机编程,而是一种表格法。动态规划对于每个子问题只求解一次,将解保存在一个表格中,从而避免不必要的重复计算。

动态规划算法的适用情况如下 2

  1. 最优子结构性质,即问题的最优解由相关子问题的最优解组合而成,子问题可以独立求解。
  2. 无后效性,即每个状态均不会影响之前的状态。
  3. 子问题重叠性质,即在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。

一个动态规划算法的核心包含两个部分:状态状态转移方程。状态即一个子问题的表示,同时这个表示需要具备 无后效性。状态转移方程用于描述状态之间的关系,也就是如何利用之前的状态构造出当前的状态进而求解。

动态规划有两种等价的实现方法:

  1. 带备忘的自顶向下法 (Top-Down with Memoization),该方法采用自然的递归形式编写过程,但会保留每个子问题的解,当需要一个子问题的解时会先检查是否保存过,如果有则直接返回该结果。
  2. 自底向上法 (Bottom-Up Method),该方法需要恰当的定义子问题“规模”,任何子问题的求解都值依赖于“更小”的子问题的求解,从而可以按照子问题的规模从小到大求解。

两种方法具有相同的渐进运行时间,在某些特殊的情况下,自顶向下的方法并未真正递归地考虑所有可能的子问题;自底向上的方法由于没有频繁的递归调用,时间复杂性函数通常具有更小的系数。

背包问题

背包问题 (Knapsack problem) 是一种组合优化的 NPC 类问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,合理地选择物品使得总价值最高。

形式化的定义,我们有 $n$ 种物品,物品 $j$ 的重量为 $w_j$,价值为 $p_j$,假定所有物品的重量和价值都是非负的,背包所能承受的最大重量为 $W$。如果限定每种物品只能选择 0 个或 1 个,则该问题称为 0-1 背包问题;如果限定物品 $j$ 最多只能选择 $b_j$ 个,则该问题称为 有界背包问题;如果不限定每种物品的数量,则该问题称为 无界背包问题。最优化问题可以表示为:

$$ \begin{equation} \begin{split} \text{maximize} & \sum_{j=1}^{n}{p_j x_j} \\ s.t. & \sum_{j=1}^{n}{w_j x_j} \leq W, x_j \in \left\{0, 1, ..., b_j\right\} \end{split} \end{equation} $$

以 0-1 背包问题为例,用 $d_{i, w}$ 表示取 $i$ 件商品填充一个最大承重 $w$ 的背包的最大价值,问题的最优解即为 $d_{n, W}$。不难写出 0-1 背包问题的状态转移方程如下:

$$ d_{i, w} = \begin{cases} d_{i - 1, w}, & w < w_i \\ \max \left(d_{i - 1, w}, d_{i - 1, w - w_i} + p_i\right), & w \geq w_i \\ 0, & i w = 0 \end{cases} $$

一个 0-1 背包问题的具体示例如下:背包承受的最大重量 $W = 10$,共有 $n = 5$ 种物品,编号分别为 $A, B, C, D, E$,重量分别为 $2, 2, 6, 5, 4$,价值分别为 $6, 3, 5, 4, 6$,利用 BP 求解该问题,不同 $i, w$ 情况下的状态如下表所示 (计算过程详见 这里):

i \ w 1 2 3 4 5 6 7 8 9 10
1 NA (A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
(A)
2 - 6
2 NA (A)
2 - 6
(A)
2 - 6
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
3 NA (A)
2 - 6
(A)
2 - 6
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, C)
8 - 11
(A, C)
8 - 11
(A, B, C)
10 - 14
4 NA (A)
2 - 6
(A)
2 - 6
(A, B)
4 - 9
(A, B)
4 - 9
(A, B)
4 - 9
(A, D)
7 - 10
(A, C)
8 - 11
(A, B, D)
9 - 13
(A, B, C)
10 - 14
5 NA (A)
2 - 6
(A)
2 - 6
(A, B)
4 - 9
(A, B)
4 - 9
(A, E)
6 - 12
(A, E)
6 - 12
(A, B, E)
8 - 15
(A, B, E)
8 - 15
(A, B, E)
8 - 15

其中,NA 表示未选取任何物品,单元格上部括号中的为选取物品的编号,单元格下部分别为选取物品的总重量和总价值。

最长公共子序列与最长公共子串

给定一个序列 $X = \left\{x_1, x_2, \dotsc, x_m\right\}$,另一个序列 $Z = \left\{z_1, z_2, \dotsc, z_k\right\}$ 在满足如下条件时称其为 $X$ 的一个 子序例 (Subsequence),即存在一个严格递增的 $X$ 的下标序列 $\left\{i_1, i_2, \dotsc, i_k\right\}$,对于所有的 $j = 1, 2, \dotsc, k$,满足 $x_{i_j} = z_j$。给定两个序例 $X$$Y$,如果 $Z$ 既是 $X$ 的子序列,也是 $Y$ 的子序列,则称它为 $X$$Y$公共子序列 (Common Subsequence)最长公共子序列 (Longest Common Subsequence) 问题为给定两个序列 $X = \left\{x_1, x_2, \dotsc, x_m\right\}$$Y = \left\{y_1, y_2, \dotsc, y_n\right\}$,求 $X$$Y$ 最长的公共子序列。

我们可以按如下递归的方式求解最长公共子序列问题:

  1. $x_i = y_j$ 时,求解 $X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$$Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$ 的最长公共子序列,在其尾部添加 $x_i$$y_j$ 即为当前状态下的最长公共子序列。
  2. $x_i \neq y_j$ 时,我们则需求解 $X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$$X = \left\{x_1, x_2, \dotsc, x_i\right\}$$Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$ 两种情况下最长的公共子序列作为当前状态下的最长公共子序列。

$c_{i, j}$ 表示$X = \left\{x_1, x_2, \dotsc, x_i\right\}$$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$ 情况下的最长公共子序列的长度,则状态转移方程如下:

$$ c_{i, w} = \begin{cases} c_{i - 1, j - 1} + i, & x_i = y_j \\ \max \left(c_{i, j - 1}, c_{i - 1, j}\right), & x_i \neq y_j \\ 0, & i j = 0 \end{cases} $$

例如:给定序列 $X = \left\{A, B, C, B, D, A, B\right\}$ 和序列 $Y = \left\{B, D, C, A, B, A\right\}$,不同状态下最长公共子序列如下表所示 (计算过程详见 这里):

$j$ 0 1 2 3 4 5 6
$i$ $y_j$ B D C A B A
0 $x_i$ 0 0 0 0 0 0 0
1 A 0 0 (↑) 0 (↑) 0 (↑) 1 (↖) 1 (←) 1 (↖)
2 B 0 1 (↖) 1 (←) 1 (←) 1 (↑) 2 (↖) 2 (←)
3 C 0 1 (↑) 1 (↑) 2 (↖) 2 (←) 2 (↑) 2 (↑)
4 B 0 1 (↖) 1 (↑) 2 (↑) 2 (↑) 3 (↖) 3 (←)
5 D 0 1 (↑) 2 (↖) 2 (↑) 2 (↑) 3 (↑) 3 (↑)
6 A 0 1 (↑) 2 (↑) 2 (↑) 3 (↖) 3 (↑) 4 (↖)
7 B 0 1 (↖) 2 (↑) 2 (↑) 3 (↑) 4 (↖) 4 (↑)

其中,每个单元格前面的数字为最长公共子序列的长度,后面的符号为还原最长公共子序列使用的备忘录符号。

最长公共子串 (Longest Common Substring) 同最长公共子序列问题略有不同,子序列不要求字符是连续的,而子串要求字符必须是连续的。例如:给定序列 $X = \left\{A, B, C, B, D, A, B\right\}$ 和序列 $Y = \left\{B, D, C, A, B, A\right\}$,最长公共子序列为 $\left\{B, C, B, A\right\}$,而最长公共子串为 $\left\{A, B\right\}$$\left\{B, D\right\}$。用 $c_{i, j}$ 表示$X = \left\{x_1, x_2, \dotsc, x_i\right\}$$Y = \left\{y_1, y_2, \dotsc, y_j\right\}$ 情况下的最长公共子串的长度,则状态转移方程如下:

$$ c_{i, w} = \begin{cases} c_{i - 1, j - 1} + i, & x_i = y_j \\ 0, & x_i \neq y_j \\ 0, & i j = 0 \end{cases} $$

利用动态规划可以在 $\Theta \left(nm\right)$ 的时间复杂度内求解,利用广义后缀树 3 可以进一步降低问题求解的时间复杂度 4

Floyd-Warshall 算法

Floyd-Warshall 算法 是一种求解任意两点之间 最短路 的算法,相比 Dijkstra 算法 5,Floyd-Warshall 算法可以处理有向图或负权图 (但不可以存在负权回路) 的情况 6

$d_{i, j}^{\left(k\right)}$ 表示从 $i$$j$ 路径上最大节点的标号为 $k$ 的最短路径的长度。有:

  1. $d_{i, j}^{\left(k\right)} = d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}$,若最短路径经过点 $k$
  2. $d_{i, j}^{\left(k\right)} = d_{i, j}^{\left(k-1\right)}$,若最短路径不经过点 $k$

则状态转移方程如下:

$$ d_{i, j}^{\left(k\right)} = \begin{cases} w_{i, j}, & k = 0 \\ \min \left(d_{i, j}^{\left(k-1\right)}, d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}\right), & k \leq 1 \end{cases} $$

以下图所示的最短路问题为例:

Floyd-Warshall 算法的求解伪代码如下所示:

\begin{algorithm}
\caption{Floyd-Warshall 算法}
\begin{algorithmic}
\REQUIRE \\
    边集合 $w$ \\
    顶点数量 $c$
\ENSURE \\
    距离矩阵 $d$ \\
    备忘录矩阵 $m$
\FUNCTION{Floyd-Warshall}{$w, c$}
\FOR{$i$ = $1$ to $c$}
    \FOR{$j$ = $1$ to $c$}
        \STATE $d_{i, j} \gets \infty$
    \ENDFOR
\ENDFOR
\FOR{$i$ = $1$ to $c$}
    \STATE $d_{i, i} \gets 0$
\ENDFOR
\FORALL{$w_{i, j}$}
    \STATE $d_{i, j} \gets w_{i, j}$
\ENDFOR
\FOR{$k$ = $1$ to $c$}
    \FOR{$i$ = $1$ to $c$}
        \FOR{$j$ = $1$ to $c$}
            \IF{$d_{i, j} > d_{i, k} + d_{k, j}$}
                \STATE $d_{i, j} \gets d_{i, k} + d_{k, j}$
                \STATE $m_{i, j} \gets k$
            \ENDIF
        \ENDFOR
    \ENDFOR
\ENDFOR
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}

通过备忘录矩阵 $m$,恢复从点 $i$ 到点 $j$ 的过程如下所示:

\begin{algorithm}
\caption{Floyd-Warshall-Path 算法}
\begin{algorithmic}
\REQUIRE \\
    备忘录矩阵 $m$ \\
    起点 $i$ \\
    终点 $j$ \\
    路径 $p$
\FUNCTION{Floyd-Warshall-Path}{$m, i, j, p$}
\IF{$i == j$}
    \RETURN
\ENDIF
\IF{$m_{i, j} == 0$}
    \STATE $p \gets p \cup j$
\ELSE
    \STATE Floyd-Warshall-Path($m, i, m_{i, j}, p$)
    \STATE Floyd-Warshall-Path($m, m_{i, j}, j, p$)
\ENDIF
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
文章部分内容参考了 Thomas H. Cormen 等人的《算法导论》

利用 Flask 和 Google App Engine 部署模型服务

2018-10-19 08:00:00

本文的配套代码请参见 这里,建议配合代码阅读本文。

模型部署和服务调用

对于做算法的同学,大家或多或少的更关心模型的性能指标多些,对于一些工程性问题考虑的较少。模型的部署是这些工程性问题中重要的一个,它直接关系到模型在生产系统的使用。一些成熟的机器学习框架会提供自己的解决方案,例如 Tensorflow 提供的 Serving 服务等。但很多情况下我们构建的工程可能不只使用了一种框架,因此一个框架自身的部署工具可能就很难满足我们的需求了。

针对此类情况,本文介绍一个 简单准生产 模型部署方案。简单是指除了模型相关代码之外的工程性代码量不大,这得益于将要使用的 Flask 框架。准生产是指这种部署方案应对一般的生产环境问题不大,对于高并发的场景可以通过横向扩容并进行负载均衡解决,但对于单次调用时效性要求较高的场景则需要另寻其他解决方案。

本文方案的模型部署和服务调用框架如下图所示:

Model-Serving

其主要特性如下:

  1. 服务端采用 Python 的 Flask 框架构建,无需使用其他外部服务。Flask 框架的 微服务 (Microframework) 特性使得服务端代码简洁高效。
  2. 利用 Gunicorn 提供的高性能 Python WSGI HTTP UNIX Server,方便在服务端运行 Flask 应用。
  3. 客户端和服务端之间采用 RESTful API 调用方式,尽管在性能上可能不及其他一些方案 (例如:基于 RPC 的解决方案等),但其较好地解决了跨语言交互的问题,不同语言之间交互仅需使用 HTTP 协议和 JSON 数据格式即可。

Flask 服务和 AJAX 调用

Flask 服务封装

为了将模型代码和 Flask 服务进行整合,首先假设你已经对模型部分代码做了完美的封装 😎,整个工程先叫做 model-serving-demo 吧。整理一下代码的目录结构,给一个我中意的 Python 目录结构风格:

model-serving-demo/                # 工程根目录
├── bin/                           # 可执行命令目录
|   ├─ start.sh                    # 启动脚本
|   ├─ stop.sh                     # 停止脚本
|   └─ ...
├── conf/                          # 配置文件目录
|   ├─ logging.conf                # 日志配置文件
|   ├─ xxx_model.conf              # XXX Model 配置文件
|   └─ ...
├── data/                          # 数据文件目录
├── docs/                          # 文档目录
├── model_serving/                 # 模块根目录
|   ├─ models/                     # 模型代码目录
|   |   ├─ __init__.py
|   |   ├─ xxx_model.py            # XXX Model 代码
|   |   └─ ...
|   ├─ resources/                  # Flask RESTful Resources 代码目录
|   |   ├─ __init__.py
|   |   ├─ xxx_model_resource.py   # XXX Model Flask RESTful Resources 代码
|   |   └─ ...
|   ├─ tests/                      # 测试代码根目录
|   |   ├─ models                  # 模型测试代码目录
|   |   |   ├─ __init__.py
|   |   |   ├─ test_xxx_model.py   # XXX Model 测试代码
|   |   |   └─ ...
|   |   ├─ __init__.py
|   |   └─ ...
|   ├─ tmp/                        # 临时目录
|   └─ ...
├── .gitignore                     # Git Ignore 文件
├── app.yaml                       # Google App Engine 配置文件
├── LICENSE                        # 授权协议
├── main.py                        # 主程序代码
├── README.md                      # 说明文件
└── requirements.txt               # 依赖包列表

我们利用一个极简的示例介绍整个模型部署,相关的库依赖 requirements.txt 如下:

Flask==1.0.2
Flask-RESTful==0.3.6
Flask-Cors==3.0.6
jsonschema==2.6.0
docopt==0.6.2

# 本地部署时需保留,GAE 部署时请删除
# gunicorn==19.9.0

其中:

  1. Flask 用于构建 Flask 服务。
  2. Flask-RESTful 用于构建 Flask RESTful API。
  3. Flask-Cors 用于解决 AJAX 调用时的 跨域问题
  4. jsonschema 用于对请求数据的 JSON 格式进行校验。
  5. docopt 用于从代码文档自动生成命令行参数解析器。
  6. gunicorn 用于提供的高性能 Python WSGI HTTP UNIX Server。

XXX Model 的代码 xxx_model.py 如下:

from ..utils.log_utils import XXXModel_LOGGER


LOGGER = XXXModel_LOGGER


class XXXModel():
    def __init__(self):
        LOGGER.info('Initializing XXX Model ...')

        LOGGER.info('XXX Model Initialized.')

    def hello(self, name:str) -> str:
        return 'Hello, {name}!'.format(name=name)

其中 hello() 为服务使用的方法,其接受一个类型为 str 的参数 name,并返回一个类型为 str 的结果。

XXX Model 的 Flask RESTful Resource 代码 xxx_model_resource.py 如下:

from flask_restful import Resource, request

from ..models.xxx_model import XXXModel
from ..utils.validation_utils import validate_json


xxx_model_instance = XXXModel()
xxx_model_schema = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'}
    },
    'required': ['name']
}


class XXXModelResource(Resource):
    @validate_json(xxx_model_schema)
    def post(self):
        json = request.json

        return {'result': xxx_model_instance.hello(json['name'])}

我们需要从 Flask RESTful 的 Resource 类继承一个新的类 XXXModelResource 用于处理 XXX Model 的服务请求。如上文介绍,我们在整个模型服务调用中使用 POST 请求方式和 JSON 数据格式,因此我们需要在类 XXXModelResource 中实现 post() 方法,同时对于传入数据的 JSON 格式进行校验。

post() 方法用于处理整个模型的服务请求,xxx_model_instance 模型实例在类 XXXModelResource 外部进行实例化,避免每次处理请求时都进行初始化。post() 的返回结果无需处理成 JSON 格式的字符串,仅需返回词典数据即可,Flask RESTful 会自动对其进行转换。

为了方便对请求数据的 JSON 格式进行校验,我们将对 JSON 格式的校验封装成一个修饰器。使用时如上文代码中所示,在 post() 方法上方添加 @validate_json(xxx_model_schema) 即可,其中 xxx_model_schema 为一个符合 jsonschema 要求的 JSON Schema。示例代码中要求传入的 JSON 数据 必须 包含一个名为 name 类型为 string 的字段。

validate_json 修饰器的代码 validation_utils.py 如下:

from functools import wraps
from jsonschema import validate, ValidationError
from flask_restful import request


def validate_json(schema, force=False):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            json_body = request.get_json(force=force)

            if json_body is None:
                return {'message': 'No JSON object'}, 400

            try:
                validate(json_body, schema)
            except ValidationError as e:
                return {'message': e.message}, 400

            return f(*args, **kwargs)
        return wrapper
    return decorator

首先我们需要验证请求包含一个 JSON 请求体,同时 JSON 请求体的内容需满足 schema 的要求。如果不满足这些条件,我们需要返回对应的错误信息 message,同时返回合理的 HTTP 状态码 (例如:400) 用于表示无法处理错误的请求。对于正常的请求响应 (即 HTTP 状态码为 200 的情况),状态码可以省略不写。

构建完 XXX Model 的 Flask RESTful Resource 后,我们就可以构建 Flask 的主服务了,主程序代码 main.py 如下:

"""
Model Serving Demo

Usage:
    main.py [--host <host>] [--port <port>] [--debug]
    main.py (-h | --help)
    main.py --version

Options:
    --host <host>                     绑定的 Host [default: 0.0.0.0]
    --port <port>                     绑定的 Port [default: 9999]
    --debug                           是否开启 Debug [default: False]
    -h --help                         显示帮助
    -v --version                      显示版本

"""

from docopt import docopt

from flask import Flask
from flask_cors import CORS
from flask_restful import Api

from model_serving.resources.xxx_model_resource import XXXModelResource


app = Flask(__name__)
CORS(app)

api = Api(app)
api.add_resource(XXXModelResource, '/v1/XXXModel')


if __name__ == '__main__':
    args = docopt(__doc__, version='Model Serving Demo v1.0.0')
    app.run(host=args['--host'], port=args['--port'], debug=args['--debug'])

docopt 库用于从代码文档自动生成命令行参数解析器,具体使用方法请参见 官方文档。整个 Flask 主服务的构建比较简单,流程如下:

  1. 构建 Flask 主程序,app = Flask(__name__)
  2. 解决 AJAX 调用的跨域问题, CORS(app)。为了方便起见,我们不加任何参数,允许任意来源的请求,详细的使用方式请参见 官方文档
  3. 构建 Flask RESTful API,api = Api(app)
  4. 将构建好的 XXX Model 的 Flask RESTful Resource 添加到 API 中,api.add_resource(XXXModelResource, '/v1/XXXModel')。 其中第二个参数为请求的 URL,对于这个 URL 的建议将在后续小节中详细说明。

Flask 主程序配置完毕后,我们通过 app.run() 在本地启动 Flask 服务,同时可以指定绑定的主机名,端口,以及是否开启调试模式等。通过 python main.py 启动 Flask 服务后,可以在命令行看到如下类似的日志:

[2018/10/21 00:00:00] - [INFO] - [XXXModel] - Initializing XXX Model ...
[2018/10/21 00:00:00] - [INFO] - [XXXModel] - XXX Model Initialized.
 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
[2018/10/21 00:00:00] - [INFO] - [werkzeug] -  * Running on http://0.0.0.0:9999/ (Press CTRL+C to quit)

现在就可以测试调用服务了,我们用 curl 命令进行简单的测试,相关代码 request-demo.sh 如下:

host=0.0.0.0
port=9999
url=https://leovan.me/v1/XXXModel
curl_url=http://${host}:${port}${url}

invalid_json='{}'
valid_json='{"name": "Leo"}'

# No JSON object
curl --request POST --url ${curl_url} --verbose

# Invalid JSON object
curl --header 'Content-Type: application/json; charset=UTF-8' \
    --request POST --data ${invalid_json} --url ${curl_url} --verbose

# Valid JSON object
curl --header 'Content-Type: application/json; charset=UTF-8' \
    --request POST --data ${valid_json} --url ${curl_url} --verbose

三种不同的请求返回的 HTTP 状态码和结果如下:

HTTP/1.0 400 BAD REQUEST
{"message": "No JSON object"}

HTTP/1.0 400 BAD REQUEST
{"message": "'name' is a required property"}

HTTP/1.0 200 OK
{"result": "Hello, Leo!"}

上文中,我们通过 python main.py 利用内置的 Server 启动了 Flask 服务,启动后日志中打印出来一条警告信息,告诉使用者不要在生产环境中使用内置的 Server。在生产环境中我们可以利用高性能 Python WSGI HTTP UNIX Server gunicorn 来启动 Flask 服务。

服务启动 (start.sh) 脚本代码如下:

cd `dirname $0`
cd ..

base_dir=`pwd`
tmp_dir=${base_dir}/tmp
pid_file_path=${tmp_dir}/model-serving-demo.pid
log_file_path=${tmp_dir}/model-serving-demo.log

bind_host=0.0.0.0
bind_port=9999
workers=2

nohup gunicorn -b ${bind_host}:${bind_port} \
  -w ${workers} -p ${pid_file_path} \
  main:app > ${log_file_path} 2>&1 &

服务停止 (stop.sh) 脚本代码如下:

cd `dirname $0`
cd ..

base_dir=`pwd`
tmp_dir=${base_dir}/tmp
pid_file_path=${tmp_dir}/model-serving-demo.pid

kill -TERM `echo ${pid_file_path}`

gunicorn 的详细参数配置和使用教程请参见 官方文档

RESTful API 设计

RESTful API 是一种符合 REST(Representational State Transfer,表现层状态转换) 原则的框架,该框架是由 Fielding 在其博士论文 1 中提出。相关的核心概念如下:

  1. 资源 (Resources),即网络中的一个实体 (文本,图片,服务等),使用一个 URL 进行表示。
  2. 表现层 (Representation),资源具体的呈现形式即为表现层,例如图片可以表示为 PNG 文件,音乐可以表示为 MP3 文件,还有本文使用的数据格式 JSON 等。HTTP 请求的头信息中用 Accept 和 Content-Type 字段对表现层进行描述。
  3. 状态转换 (State Transfer),互联网通信协议 HTTP 协议是一个无状态协议,所有的状态都保存在服务端。因此如果客户端想要操作服务器,必须通过某种手段让服务器端发生 状态转换。客户端利用 HTTP 协议中的动作对服务器进行操作,例如:GET,POST,PUT,DELETE 等。

利用 RESTful API 构建模型服务时,需要注意如下几点:

  1. 为模型服务设置专用域名,例如:https://api.example.com,并配以负载均衡。
  2. 将 API 的版本号写入 URL 中,例如:https://api.example.com/v1
  3. RESTful 框架中每个 URL 表示一种资源,因此可以将模型的名称作为 URL 的终点 (Endpoint),例如:https://api.example.com/v1/XXXModel
  4. 对于操作资源的 HTTP 方式有多种,综合考虑建议选用 POST 方式,同时建议使用 JSON 数据格式。
  5. 为请求响应设置合理的状态码,例如:200 OK 表示正常返回,400 INVALID REQUEST 表示无法处理客户端的错误请求等。
  6. 对于错误码为 4xx 的情况,建议在返回中添加键名为 message 的错误信息。

AJAX 调用

对于动态网页,我们可以很容易的在后端服务中发起 POST 请求调用模型服务,然后将结果在前端进行渲染。对于静态网页,我们可以利用 AJAX 进行相关操作,实现细节请参见 示例代码

AJAX 服务请求代码的核心部分如下:

$(document).ready(function() {
    $("#submit").click(function() {
        $.ajax({
            url: "http://0.0.0.0:9999/v1/XXXModel",
            method: "POST",
            contentType: "application/json; charset=UTF-8",
            data: JSON.stringify({"name": $("#name").val()}),
            timeout: 3000,

            success: function (data, textStatus, jqXHR) {
                $("#result").html(data.result);
            },
            error: function (jqXHR, textStatus, errorThrown) {
                $("#result").html(errorThrown);
            }
        });
    });
});

代码使用了 jQuery 的相关函数。JSON.stringify({"name": $("#name").val()}) 获取 ID 为 name 的元素的值,并将其转换成符合服务端要求的 JSON 格式。通过 AJAX 向远程发出请求后,如果请求成功则将返回数据 data 中对应的结果 result 填充到 ID 为 result 的元素中,否则填入返回的错误信息。

Google App Engine 部署

上文中已经介绍了如何在本地利用 Flask 部署模型服务和相关调用方法,但如果希望在自己的网站中调用时,则利用 SaaS 来部署符合会是一个不二之选。国内外多个厂商均提供了相应的 SaaS 产品,例如 GoogleAmazonMicrosoft 等。Google App Engine (GAE) 提供了一个 始终免费 方案,虽然部署阶段会受到 GFW 的影响,但调用阶段测试影响并不是很大 (不同地区和服务提供商会有差异)。综合考虑,本文选择 GAE 作为 SaaS 平台部署服务,各位看官请自备梯子。

环境准备

首先,在 Google Cloud Platform Console 中建立一个新的 Project,假设项目名为 YOUR_PROJECT_ID

其次,根据 Google Cloud SDK 文档 在本地安装相应版本的 Google Cloud SDK。MacOS 下建议通过 brew cask install google-cloud-sdk 方式安装,安装完毕后确认在命令行中可以运行 gcloud 命令。

$ gcloud version
Google Cloud SDK 221.0.0
bq 2.0.35
core 2018.10.12
gsutil 4.34

构建 GAE 工程

模型服务仅作为后端应用,因此本节不介绍前端页面开发的相关部分,有兴趣的同学请参见 官方文档。GAE 部署 Python Web 应用采用了 WSGI 标准,我们构建的本地部署版本完全满足这个要求,因此仅需为项目在根目录添加一个 GAE 配置文件 app.yaml 即可,内容如下:

runtime: python37

handlers:
  - url: /.*
    script: main.app

skip_files:
  - .idea/
  - .vscode/
  - __pycache__/
  - .hypothesis/
  - .pytest_cache/
  - bin/
  - ^(.*/)?.*\.py[cod]$
  - ^(.*/)?.*\$py\.class$
  - ^(.*/)?.*\.log$

其中,runtime 指定了服务运行的环境,handlers 指定了不同的 URL 对应的处理程序,在此所有的 URL 均由 main.py 中的 app 进行处理,skip_files 用于过滤不需要上传的文件。更多关于 app.yaml 的设置信息,请参见 官方文档

部署 GAE 工程

在部署 GAE 工程之前我们可以利用本地的开发环境对其进行测试,测试无误后,即可运行如下命令将其部署到 GAE 上:

gcloud app deploy --project [YOUR_PROJECT_ID]

然后根据命令行提示完成整个部署流程,部署完成的远程服务 URL 为 https://YOUR_PROJECT_ID.appspot.com,更多的测试和部署细节请参见 官方文档

部署后的 GAE 服务使用了其自带的域名 appspot.com。如果你拥有自己的域名,可以根据官方文档 设置自己的域名开启 SSL

本文部分内容参考了 Genthial 的博客 Serving a model with Flask 和阮一峰的博客 理解RESTful架构RESTful API 设计指南

  1. Fielding, Roy T., and Richard N. Taylor. Architectural styles and the design of network-based software architectures. Vol. 7. Doctoral dissertation: University of California, Irvine, 2000. ↩︎

序列到序列 (Seq2Seq) 和注意力机制 (Attention Machanism)

2018-10-12 08:00:00

Encoder-Decoder & Seq2Seq

Encoder-Decoder 是一种包含两个神经网络的模型,两个网络分别扮演编码器和解码器的角色。Cho 等人 1 提出了一个基于 RNN 的 Encoder-Decoder 神经网络用于机器翻译。网络结构如下图所示:

RNN-Encoder-Decoder

整个模型包含编码器 (Encoder) 和解码器 (Decoder) 两部分:Encoder 将一个可变长度的序列转换成为一个固定长度的向量表示,Decoder 再将这个固定长度的向量表示转换为一个可变长度的序列。这使得模型可以处理从一个可变长度序列到另一个可变长度序例的转换,即学习到对应的条件概率 $p \left(y_1, \dotsc, y_{T'} | x_1, \dotsc, x_T\right)$,其中 $T$$T'$ 可以为不同的值,也就是说输入和输出的序列的长度不一定相同。

在模型中,Encoder 为一个 RNN,逐次读入输入序列 $\mathbf{x}$ 中的每个元素,其中 RNN 隐状态的更新方式如下:

$$ \mathbf{h}_{\langle t \rangle} = f \left(\mathbf{h}_{\langle t-1 \rangle}, x_t\right) $$

在读入序列的最后一个元素后 (通常为一个结束标记),RNN 的隐状态则为整个输入序列的概括信息 $\mathbf{c}$。Decoder 为另一个 RNN,用于根据隐状态 $\mathbf{h}'_{\langle t \rangle}$ 预测下一个元素 $y_t$,从而生成整个输出序列。不同于 Encoder 中的 RNN,Decoder 中 RNN 的隐状态 $\mathbf{h}'_{\langle t \rangle}$ 除了依赖上一个隐含层的状态和之前的输出外,还依赖整个输入序列的概括信息 $\mathbf{c}$,即:

$$ \mathbf{h}'_{\langle t \rangle} = f \left(\mathbf{h}'_{\langle t-1 \rangle}, y_{t-1}, \mathbf{c}\right) $$

类似的,下一个输出元素的条件分布为:

$$ P \left(y_t | y_{t-1}, y_{t-2}, \dotsc, y_1, \mathbf{c}\right) = g \left(\mathbf{h}_{\langle t \rangle}, y_{t-1}, \mathbf{c}\right) $$

RNN Encoder-Decoder 的两部分通过最大化如下的对数似然函数的联合训练进行优化:

$$ \max_{\theta} \dfrac{1}{N} \sum_{n=1}^{N}{\log p_{\theta} \left(\mathbf{y}_n | \mathbf{x}_n\right)} $$

其中,$\theta$ 为模型的参数,$\mathbf{x}_n$$\mathbf{y}_n$ 分别为输入和输出序列的成对样本。当模型训练完毕后,我们可以利用模型根据给定的输入序列生成相应的输出序列,或是根据给定的输入和输出序列对计算概率得分 $p_{\theta} \left(\mathbf{y} | \mathbf{x}\right)$。同时,作者还提出了一种新的 RNN 单元 GRU (Gated Recurrent Unit),有关 GRU 的更多介绍请参见 之前的博客

序列到序列 (Sequence to Sequence, Seq2Seq) 模型从名称中不难看出来是一种用于处理序列数据到序列数据转换问题 (例如:机器翻译等) 的方法。Sutskever 等人 2 提出了一种基于 Encoder-Decoder 网络结构的 Seq2Seq 模型用于机器翻译,网络结构细节同 RNN Encoder-Decoder 略有不同,如下图所示:

Seq2Seq

模型的相关细节如下:

  1. 对数据进行预处理,在每个句子的结尾添加特殊字符 <EOS>,如上图所示。首先计算 A, B, C, <EOS> 的表示,再利用该表示计算 W, X, Y, Z, <EOS> 的条件概率。
  2. 利用两个不同的 LSTM,一个用于输入序列,另一个用于输出序列。
  3. 选用一个较深的 LSTM 模型 (4 层) 提升模型效果。
  4. 对输入序列进行倒置处理,例如对于输入序列 $a, b, c$ 和对应的输出序列 $\alpha, \beta, \gamma$,LSTM 需要学习的映射关系为 $c, b, a \to \alpha, \beta, \gamma$

在模型的解码阶段,模型采用简单的从左到右的 Beam Search,该方法维护一个大小为 $B$ 的集合保存最好的结果。下图展示了 $B = 2$ 情况下 Beam Search 的具体工作方式:

Beam-Search

其中,红色的虚线箭头表示每一步可能的搜索方向,绿色的实线箭头表示每一步概率为 Top $B$ 的方向。例如,从 S 开始搜索:

  1. 第一步搜索的可能结果为 SA 和 SB,保留 Top 2,结果为 SA 和 SB。
  2. 第二步搜索的可能结果为 SAC,SAD,SBE 和 SBF,保留 Top 2,结果为 SAC 和 SBE。
  3. 第三步搜索的可能结果为 SACG,SACH,SBEK 和 SBEL,保留 Top 2,结果为 SACH 和 SBEK。至此,整个搜索结束。

Bahdanau 等人 3 提出了一种基于双向 RNN (Bidirectional RNN, BiRNN) 结合注意力机制 (Attention Mechanism) 的网络结构用于机器翻译。网络结构如下:

Seq2Seq-BiRNN-Attention

模型的编码器使用了一个双向的 RNN,前向的 RNN $\overrightarrow{f}$ 以从 $x_1$$x_T$ 的顺序读取输入序列并计算前向隐状态 $\left(\overrightarrow{h}_1, \dotsc, \overrightarrow{h}_T\right)$,后向的 RNN $\overleftarrow{f}$ 以从 $x_T$$x_1$ 的顺序读取输入序列并计算后向隐状态 $\left(\overleftarrow{h}_1, \dotsc, \overleftarrow{h}_T\right)$。对于一个词 $x_j$,通过将对应的前向隐状态 $\overrightarrow{h}_j$ 和后向隐状态 $\overleftarrow{h}_j$ 进行拼接得到最终的隐状态 $h_j = \left[\overrightarrow{h}_j^{\top}; \overleftarrow{h}_j^{\top}\right]^{\top}$。这样的操作使得隐状态 $h_j$ 既包含了前面词的信息也包含了后面词的信息。

在模型的解码器中,对于一个给定的序例 $\mathbf{x}$,每一个输出的条件概率为:

$$ p \left(y_i | y_1, \dotsc, y_{i-1}, \mathbf{x}\right) = g \left(y_{i-1}, s_i, c_i\right) $$

其中,$s_i$$i$ 时刻 RNN 隐含层的状态,即:

$$ s_i = f \left(s_{i-1}, y_{i-1}, c_i\right) $$

这里需要注意的是不同于之前的 Encoder-Decoder 模型,此处每一个输出词 $y_i$ 的条件概率均依赖于一个单独的上下文向量 $c_i$。该部分的改进即结合了注意力机制,有关注意力机制的详细内容将在下个小节中展开说明。

注意力机制 (Attention Mechanism)

Bahdanau 等人在文中 3 提出传统的 Encoder-Decoder 模型将输入序列压缩成一个固定长度的向量 $c$,但当输入的序例很长时,尤其是当比训练集中的语料还长时,模型的的效果会显著下降。针对这个问题,如上文所述,上下文向量 $c_i$ 依赖于 $\left(h_1, \dotsc, h_T\right)$。其中,每个 $h_i$ 都包含了整个序列的信息,同时又会更多地关注第 $i$ 个词附近的信息。对于 $c_i$,计算方式如下:

$$ c_i = \sum_{j=1}^{T}{\alpha_{ij} h_j} $$

对于每个 $h_j$ 的权重 $\alpha_{ij}$,计算方式如下:

$$ \alpha_{ij} = \dfrac{\exp \left(e_{ij}\right)}{\sum_{k=1}^{T}{\exp \left(e_{ik}\right)}} $$

其中,$e_{ij} = a \left(s_{i-1}, h_j\right)$ 为一个 Alignment 模型,用于评价对于输入的位置 $j$ 附近的信息与输出的位置 $i$ 附近的信息的匹配程度。Alignment 模型 $a$ 为一个用于评分的前馈神经网络,与整个模型进行联合训练,计算方式如下:

$$ a \left(s_{i-1}, h_j\right) = v_a^{\top} \tanh \left(W_a s_{i-1} + U_a h_j\right) $$

其中,$W_a \in \mathbb{R}^{n \times n}, U_a \in \mathbb{R}^{n \times 2n},v_a \in \mathbb{R}^n$ 为网络的参数。

Hard & Soft Attention

Xu 等人 4 在图像标题生成 (Image Caption Generation) 任务中引入了注意力机制。在文中作者提出了 Hard Attenttion 和 Soft Attention 两种不同的注意力机制。

对于 Hard Attention 而言,令 $s_t$ 表示在生成第 $t$ 个词时所关注的位置变量,$s_{t, i} = 1$ 表示当第 $i$ 个位置用于提取视觉特征。将注意力位置视为一个中间潜变量,可以以一个参数为 $\left\{\alpha_i\right\}$ 的多项式分布表示,同时将上下文向量 $\hat{\mathbf{z}}_t$ 视为一个随机变量:

$$ \begin{equation} \begin{split} & p \left(s_{t, i} = 1 | s_{j < t}, \mathbf{a}\right) = \alpha_{t, i} \\ & \hat{\mathbf{z}}_t = \sum_{i}{s_{t, i} \mathbf{a}_i} \end{split} \end{equation} $$

因此 Hard Attention 可以依据概率值从隐状态中进行采样计算得到上下文向量,同时为了实现梯度的反向传播,需要利用蒙特卡罗采样的方法来估计梯度。

对于 Soft Attention 而言,则直接计算上下文向量 $\hat{\mathbf{z}}_t$ 的期望,计算方式如下:

$$ \mathbb{E}_{p \left(s_t | a\right)} \left[\hat{\mathbf{z}}_t\right] = \sum_{i=1}^{L}{\alpha_{t, i} \mathbf{a}_i} $$

其余部分的计算方式同 Bahdanau 等人 3 的论文类似。Soft Attention 模型可以利用标准的反向传播算法进行求解,直接嵌入到整个模型中一同训练,相对更加简单。

下图展示了一些图片标题生成结果的可视化示例,其中图片内 白色 为关注的区域,画线的文本 即为生成的标题中对应的词。

Image-Caption-Generation-Visual-Attention

Global & Local Attention

Luong 等人 5 提出了 Global Attention 和 Local Attention 两种不同的注意力机制用于机器翻译。Global Attention 的思想是在计算上下文向量 $c_t$ 时将编码器的所有隐状态均考虑在内。对于对齐向量 $\boldsymbol{a}_t$,通过比较当前目标的隐状态 $\boldsymbol{h}_t$ 与每一个输入的隐状态 $\bar{\boldsymbol{h}}_s$ 得到,即:

$$ \begin{equation} \begin{split} \boldsymbol{a}_t &= \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \\ &= \dfrac{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right)\right)}{\sum_{s'}{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_{s'}\right)\right)}} \end{split} \end{equation} $$

其中 $\text{score}$ 为一个基于内容 (content-based) 的函数,可选的考虑如下三种形式:

$$ \text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) = \begin{cases} \boldsymbol{h}_t^{\top} \bar{\boldsymbol{h}}_s & dot \\ \boldsymbol{h}_t^{\top} \boldsymbol{W}_a \bar{\boldsymbol{h}}_s & general \\ \boldsymbol{W}_a \left[\boldsymbol{h}_t; \bar{\boldsymbol{h}}_s\right] & concat \end{cases} $$

我们利用一个基于位置 (location-based) 的函数构建注意力模型,其中对齐分数通过目标的隐状态计算得到:

$$ \boldsymbol{a}_t = \text{softmax} \left(\boldsymbol{W}_a \boldsymbol{h}_t\right) $$

Global Attention 模型的网络结构如下所示:

Global-Attention

Global Attention 的一个问题在于任意一个输出都需要考虑输入端的所有隐状态,这对于很长的文本 (例如:一个段落或一篇文章) 计算量太大。Local Attention 为了解决这个问题,首先在 $t$ 时刻对于每个目标词生成一个对齐位置 $p_t$,其次上下文向量 $\boldsymbol{c}_t$ 则由以 $p_t$ 为中心前后各 $D$ 大小的窗口 $\left[p_t - D, p_t + D\right]$ 内的输入的隐状态计算得到。不同于 Global Attention,Local Attention 的对齐向量 $\boldsymbol{a}_t \in \mathbb{R}^{2D + 1}$ 为固定维度。

一个比较简单的做法是令 $p_t = t$,也就是假设输入和输出序列是差不多是单调对齐的,我们称这种做法为 Monotonic Alignment (local-m)。另一种做法是预测 $p_t$,即:

$$ p_t = S \cdot \text{sigmoid} \left(\boldsymbol{v}_p^{\top} \tanh \left(\boldsymbol{W}_p \boldsymbol{h}_t\right)\right) $$

其中,$\boldsymbol{W}_p$$\boldsymbol{h}_t$ 为预测位置模型的参数,$S$ 为输入句子的长度。我们称这种做法为 Predictive Alignment (local-p)。作为 $\text{sigmoid}$ 函数的结果,$p_t \in \left[0, S\right]$,则通过一个以 $p_t$ 为中心的高斯分布定义对齐权重:

$$ \boldsymbol{a}_t \left(s\right) = \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \exp \left(- \dfrac{\left(s - p_t\right)^2}{2 \sigma^2}\right) $$

其中,根据经验设置 $\sigma = \dfrac{D}{2}$$s$ 为在窗口大小内的一个整数。

Local Attention 模型的网络结构如下所示:

Local-Attention

Self Attention

Vaswani 等人 6 提出了一种新的网络结构,称之为 Transformer,其中采用了自注意力 (Self-attention) 机制。自注意力是一种将同一个序列的不同位置进行自我关联从而计算一个句子表示的机制。Transformer 利用堆叠的 Self Attention 和全链接网络构建编码器 (下图左) 和解码器 (下图右),整个网络架构如下图所示:

Self-Attention

编码器和解码器

编码器 是由 $N = 6$ 个相同的网络层构成,每层中包含两个子层。第一层为一个 Multi-Head Self-Attention 层,第二层为一个 Position-Wise 全链接的前馈神经网络。每一层再应用一个残差连接 (Residual Connection) 7 和一个层标准化 (Layer Normalization) 8。则每一层的输出为 $\text{LayerNorm} \left(x + \text{Sublayer} \left(x\right)\right)$,其中 $\text{Sublayer} \left(x\right)$ 为子层本身的函数实现。为了实现残差连接,模型中所有的子层包括 Embedding 层的输出维度均为 $d_{\text{model}} = 512$

解码器 也是由 $N = 6$ 个相同的网络层构成,但每层中包含三个子层,增加的第三层用于处理编码器的输出。同编码器一样,每一层应用一个残差连接和一个层标准化。除此之外,解码器对 Self-Attention 层进行了修改,确保对于位置 $i$ 的预测仅依赖于位置在 $i$ 之前的输出。

Scaled Dot-Product & Multi-Head Attention

一个 Attention 函数可以理解为从一个序列 (Query) 和一个键值对集合 (Key-Value Pairs Set) 到一个输出的映射。文中提出了一种名为 Scaled Dot-Product Attention (如下图所示),其中输入包括 queries,维度为 $d_k$ 的 keys 和维度为 $d_v$ 的 values。通过计算 queries 和所有 keys 的点积,除以 $\sqrt{d_k}$,再应用一个 softmax 函数获取 values 的权重。

Scaled-Dot-Product-Attention

实际中,我们会同时计算一个 Queries 集合中的 Attention,并将其整合成一个矩阵 $Q$。Keys 和 Values 也相应的整合成矩阵 $K$$V$,则有:

$$ \text{Attention} \left(Q, K, V\right) = \text{softmax} \left(\dfrac{Q K^{\top}}{\sqrt{d_k}}\right) V $$

其中,$Q \in \mathbb{R}^{n \times d_k}$$Q$ 中的每一行为一个 query,$K \in \mathbb{R}^{n \times d_k}, V \in \mathbb{R}^{n \times d_v}$$\dfrac{1}{\sqrt{d_k}}$ 为一个归一化因子,避免点积的值过大导致 softmax 之后的梯度过小。

Multi-Head Attention 的做法并不直接对原始的 keys,values 和 queries 应用注意力函数,而是学习一个三者各自的映射再应用 Atteneion,同时将这个过程重复 $h$ 次。Multi-Head Attention 的网路结构如下图所示:

Multi-Head-Attention

Multi-Head Attention 的计算过程如下所示:

$$ \begin{equation} \begin{split} \text{MultiHead} \left(Q, K, V\right) &= \text{Concat} \left(\text{head}_1, \dotsc, \text{head}_h\right) W^O \\ \textbf{where } \text{head}_i &= \text{Attention} \left(QW_i^Q, KW_i^K, VW_i^V\right) \end{split} \end{equation} $$

其中,$W_i^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}, W_i^O \in \mathbb{R}^{h d_v \times d_{\text{model}}}, $ 为映射的参数,$h = 8$ 为重复的次数,则有 $d_k = d_v = d_{\text{model}} / h = 64$

整个 Transformer 模型在三处使用了 Multi-Head Attention,分别是:

  1. Encoder-Decoder Attention Layers,其中 queries 来自于之前的 Decoder 层,keys 和 values 来自于 Encoder 的输出,该部分同其他 Seq2Seq 模型的 Attention 机制类似。
  2. Encoder Self-Attention Layers,其中 queries,keys 和 values 均来自之前的 Encoder 层的输出,同时 Encoder 层中的每个位置都能够从之前层的所有位置获取到信息。
  3. Decoder Self-Attention Layers,其中 queries,keys 和 values 均来自之前的 Decoder 层的输出,但 Decoder 层中的每个位置仅可以从之前网络层的包含当前位置之前的位置获取信息。

Position-wise Feed-Forward Networks

在 Encoder 和 Decoder 中的每一层均包含一个全链接的前馈神经网络,其使用两层线性变换和一个 ReLU 激活函数实现:

$$ \text{FFN} \left(x\right) = \max \left(0, x W_1 + b_1\right) W_2 + b_2 $$

全链接层的输入和输出的维度 $d_{\text{model}} = 512$,内层的维度 $d_{ff} = 2048$

Positional Encoding

Transformer 模型由于未使用任何循环和卷积组件,因此为了利用序列的位置信息则在模型的 Embedding 输入中添加了 Position Encoding。Position Encoding 的维度同 Embedding 的维度相同,从而可以与 Embedding 进行加和,文中使用了如下两种形式:

$$ \begin{equation} \begin{split} PE_{\left(pos, 2i\right)} &= \sin \left(pos / 10000^{2i / d_{\text{model}}}\right) \\ PE_{\left(pos, 2i+1\right)} &= \cos \left(pos / 10000^{2i / d_{\text{model}}}\right) \end{split} \end{equation} $$

其中,$pos$ 为位置,$i$ 为对应的维度,选用这种表示形式的原因是对于一个固定的偏移 $k$$PE_{pos + k}$ 都可以利用 $PE_{pos}$ 线性表示。这是因为对于正弦和余弦函数有:

$$ \begin{equation} \begin{split} \sin \left(\alpha + \beta\right) &= \sin \alpha \cos \beta + \cos \alpha \sin \beta \\ \cos \left(\alpha + \beta\right) &= \cos \alpha \sin \beta - \sin \alpha \sin \beta \end{split} \end{equation} $$

Why Self-Attention

相比于循环和卷积层,Transformer 模型利用 Self-Attention 层用于一个序列 $\left(x_1, \dotsc, x_n\right)$ 到另一个等长序例 $\left(z_1, \dotsc, z_n\right)$ 的映射,其中 $x_i, z_i \in \mathbb{R}^d$。Self-Attention 与循环和卷积的对比如下表所示:

层类型 每层的复杂度 序列操作数 长距离依赖路径长度
Self-Attention $O \left(n^2 \cdot d\right)$ $O \left(1\right)$ $O \left(1\right)$
Recurrent $O \left(n \cdot d^2\right)$ $O \left(n\right)$ $O \left(n\right)$
Convolutional $O \left(k \cdot n \cdot d^2\right)$ $O \left(1\right)$ $O \left(\log_k \left(n\right)\right)$
Self-Attention (restricted) $O \left(r \cdot n \cdot d\right)$ $O \left(1\right)$ $O \left(n/r\right)$
  1. 对于每层的复杂度,当序例的长度 $n$ 比表示的维度 $d$ 小时,Self-Attention 要比循环结构计算复杂度小。为了改进在长序列上 Self-Attention 的计算性能,Self-Attention 可以被限制成仅考虑与输出位置对应的输入序列位置附近 $r$ 窗口大小内的信息。
  2. Recurrent 层的最小序列操作数为 $O \left(n\right)$,其他情况为 $O \left(1\right)$,这使得 Recurrent 的并行能力较差,即上表中的 Self-Attention (restricted)。
  3. 学习到长距离依赖是很多序列任务的关键,影响该能力的一个重要因素就是前向和后向信号穿越整个网络的路径长度,这个路径长度越短,越容易学习到长距离依赖。

Attention Visualizations

第一张图展示了 Self-Attention 学到的句子内部的一个长距离依赖 “making … more diffcult”,图中不同的颜色表示不同 Head 的 Attention,颜色越深表示 Attention 的值越大。

Self-Attention-Long-Distance-Dependencies

第二张图展示了 Self-Attention 学到的一个指代消解关系 (Anaphora Resolution),its 指代的为上文中的 law。下图 (上) 为 Head 5 的所有 Attention,下图 (下) 为 Head 5 和 6 关于词 its 的 Attention,不难看出模型学习到了 its 和 law 之间的依赖关系 (指代消解关系)。

Self-Attention-Long-Anaphora-Resolution

Hierarchical Attention

Yang 等人 9 提出了一种层级的注意力 (Hierarchical Attention) 网络用于文档分类。Hierarchical Attention 共包含 4 层:一个词编码器 (Word Encoder),一个词级别的注意力层 (Word Attention),一个句子编码器 (Sentence Encoder) 和一个句子级别的注意力层 (Sentence Attention)。网络架构如下图所示:

Hierarchical-Attention

Word Encoder

对于一个给定的句子 $w_{it}, t \in \left[0, T\right]$,通过一个 Embedding 矩阵 $W_e$ 得到每个词的向量表示,再应用一个双向的 GRU,即:

$$ \begin{equation} \begin{split} x_{it} &= W_e w_{it}, t \in \left[1, T\right] \\ \overrightarrow{h}_{it} &= \overrightarrow{\text{GRU}} \left(x_{it}\right), t \in \left[1, T\right] \\ \overleftarrow{h}_{it} &= \overleftarrow{\text{GRU}} \left(x_{it}\right), t \in \left[T, 1\right] \end{split} \end{equation} $$

最后将前向的隐状态 $\overrightarrow{h}_{it}$ 和后向的隐状态 $\overleftarrow{h}_{it}$ 进行拼接,得到 $h_{ij} = \left[\overrightarrow{h}_{it}, \overleftarrow{h}_{it}\right]$ 为整个句子在词 $w_{ij}$ 附近的汇总信息。

Word Attention

Word Attention 同一般的 Attention 机制类似,计算方式如下:

$$ \begin{equation} \begin{split} u_{it} &= \tanh \left(W_w h_{it} + b_w\right) \\ a_{it} &= \dfrac{\exp \left(u_{it}^{\top} u_w\right)}{\sum_{t}{\exp \left(u_{it}^{\top} u_w\right)}} \\ s_i &= \sum_{t}{a_{it} h_{it}} \end{split} \end{equation} $$

Sentence Encoder

在 Word Attention 之后,我们得到了一个句子的表示 $s_i$,类似的我们利用一个双向的 GRU 编码文档中的 $L$ 个句子:

$$ \begin{equation} \begin{split} \overrightarrow{h}_i &= \overrightarrow{\text{GRU}} \left(s_i\right), i \in \left[1, L\right] \\ \overleftarrow{h}_i &= \overleftarrow{\text{GRU}} \left(s_i\right), i \in \left[L, 1\right] \end{split} \end{equation} $$

最后将前向的隐状态 $\overrightarrow{h}_i$ 和后向的隐状态 $\overleftarrow{h}_i$ 进行拼接,得到 $h_i = \left[\overrightarrow{h}_i, \overleftarrow{h}_i\right]$ 为整个文档关于句子 $s_i$ 的注意力汇总信息。

Sentence Attention

同理可得 Sentence Attention 的计算方式如下:

$$ \begin{equation} \begin{split} u_i &= \tanh \left(W_s h_i + b_s\right) \\ a_i &= \dfrac{\exp \left(u_i^{\top} u_s\right)}{\sum_{i}{\exp \left(u_i^{\top} u_s\right)}} \\ v &= \sum_{i}{a_i h_i} \end{split} \end{equation} $$

最终得到整个文档的向量表示 $v$

Attention-over-Attention

Cui 等人 10 提出了 Attention-over-Attention 的模型用于阅读理解 (Reading Comprehension)。网络结构如下图所示:

Attention-over-Attention

对于一个给定的训练集 $\langle \mathcal{D}, \mathcal{Q}, \mathcal{A} \rangle$,模型包含两个输入,一个文档 (Document) 和一个问题序列 (Query)。网络的工作流程如下:

  1. 先获取 Document 和 Query 的 Embedding 结果,再应用一个双向的 GRU 得到对应的隐状态 $h_{doc}$$h_{query}$
  2. 计算一个 Document 和 Query 的匹配程度矩阵 $M \in \mathbb{R}^{\lvert \mathcal{D} \rvert \times \lvert \mathcal{Q} \rvert}$,其中第 $i$ 行第 $j$ 列的值计算方式如下: $$ M \left(i, j\right) = h_{doc} \left(i\right)^{\top} \cdot h_{query} \left(j\right) $$
  3. 按照 的方向对矩阵 $M$ 应用 softmax 函数,矩阵中的每一列为考虑一个 Query 中的词的 Document 级别的 Attention,因此定义 $\alpha \left(t\right) \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$$t$ 时刻的 Document 级别 Attention (query-to-document attention)。计算方式如下: $$ \begin{equation} \begin{split} \alpha \left(t\right) &= \text{softmax} \left(M \left(1, t\right), \dotsc, M \left(\lvert \mathcal{D} \rvert, t\right)\right) \\ \alpha &= \left[\alpha \left(1\right), \alpha \left(2\right), \dotsc, \alpha \left(\lvert \mathcal{Q} \rvert\right)\right] \end{split} \end{equation} $$
  4. 同理按照 的方向对矩阵 $M$ 应用 softmax 函数,可以得到 $\beta \left(t\right) \in \mathbb{R}^{\lvert \mathcal{Q} \rvert}$$t$ 时刻的 Query 级别的 Attention (document-to-query attention)。计算方式如下: $$ \beta \left(t\right) = \text{softmax} \left(M \left(t, 1\right), \dotsc, M \left(t, \lvert \mathcal{Q} \rvert\right)\right) $$
  5. 对于 document-to-query attention,我们对结果进行平均得到: $$ \beta = \dfrac{1}{n} \sum_{t=1}^{\lvert \mathcal{D} \rvert}{\beta \left(t\right)} $$
  6. 最终利用 $\alpha$$\beta$ 的点积 $s = \alpha^{\top} \beta \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$ 得到 attended document-level attention (即 attention-over-attention)。

Multi-step Attention

Gehring 等人 11 提出了基于 CNN 和 Multi-step Attention 的模型用于机器翻译。网络结构如下图所示:

Multi-step-Attention

Position Embeddings

模型首先得到序列 $\mathbf{x} = \left(x_1, \dotsc, x_m\right)$ 的 Embedding $\mathbf{w} = \left(w_1, \dotsc , w_m\right), w_j \in \mathbb{R}^f$。除此之外还将输入序列的位置信息映射为 $\mathbf{p} = \left(p_1, \dotsc, p_m\right), p_j \in \mathbb{R}^f$,最终将两者进行合并得到最终的输入 $\mathbf{e} = \left(w_1 + p_1, \dotsc, w_m + p_m\right)$。同时在解码器部分也采用类似的操作,将其与解码器网络的输出表示合并之后再喂入解码器网络 $\mathbf{g} = \left(g_1, \dotsc, g_n\right)$ 中。

Convolutional Block Structure

编码器和解码器均由多个 Convolutional Block 构成,每个 Block 包含一个卷积计算和一个非线性计算。令 $\mathbf{h}^l = \left(h_1^l, \dotsc, h_n^l\right)$ 表示解码器第 $l$ 个 Block 的输出,$\mathbf{z}^l = \left(z_1^l, \dotsc, z_m^l\right)$ 表示编码器第 $l$ 个 Block 的输出。对于一个大小 $k = 5$ 的卷积核,其结果的隐状态包含了这 5 个输入,则对于一个 6 层的堆叠结构,结果的隐状态则包含了输入中的 25 个元素。

在每一个 Convolutional Block 中,卷积核的参数为 $W \in \mathbb{R}^{2d \times kd}, b_w \in \mathbb{R}^{2d}$,其中 $k$ 为卷积核的大小,经过卷积后的输出为 $Y \in \mathbb{R}^{2d}$。之后的非线性层采用了 Dauphin 等人 12 提出的 Gated Linear Units (GLU),对于卷积后的输出 $Y = \left[A, B\right]$ 有:

$$ v \left(\left[A, B\right]\right) = A \otimes \sigma \left(B\right) $$

其中,$A, B \in \mathbb{R}^d$ 为非线性单元的输入,$\otimes$ 为逐元素相乘,$\sigma \left(B\right)$ 为用于控制输入 $A$ 与当前上下文相关度的门结构。

模型中还加入了残差连接,即:

$$ h_i^l = v \left(W^l \left[h_{i-k/2}^{l-1}, \dotsc, h_{i+k/2}^{l-1}\right] + b_w^l\right) + h_i^{l-1} $$

为了确保网络卷积层的输出同输入的长度相匹配,模型对输入数据的前后填补 $k - 1$ 个零值,同时为了避免解码器使用当前预测位置之后的信息,模型删除了卷积输出尾部的 $k$ 个元素。在将 Embedding 喂给编码器网络之前,在解码器输出应用 softmax 之前以及所有解码器层计算 Attention 分数之前,建立了一个从 Embedding 维度 $f$ 到卷积输出大小 $2d$ 的线性映射。最终,预测下一个词的概率计算方式如下:

$$ p \left(y_{i+1} | y_1, \dotsc, y_i, \mathbf{x}\right) = \text{softmax} \left(W_o h_i^L + b_o\right) \in \mathbb{R}^T $$

Multi-step Attention

模型的解码器网络中引入了一个分离的注意力机制,在计算 Attention 时,将解码器当前的隐状态 $h_i^l$ 同之前输出元素的 Embedding 进行合并:

$$ d_i^l = W_d^l h_i^l + b_d^l + g_i $$

对于解码器网络层 $l$ 中状态 $i$ 和输入元素 $j$ 之间的的 Attention $a_{ij}^l$ 通过解码器汇总状态 $d_i^l$ 和最后一个解码器 Block $u$ 的输出 $z_j^u$ 进行点积运算得到:

$$ a_{ij}^l = \dfrac{\exp \left(d_i^l \cdot z_j^u\right)}{\sum_{t=1}^{m}{\exp \left(d_i^l \cdot z_t^u\right)}} $$

条件输入 $c_i^l$ 的计算方式如下:

$$ c_i^l = \sum_{j=1}^{m}{a_{ij}^l \left(z_j^u + e_j\right)} $$

其中,$e_j$ 为输入元素的 Embedding。与传统的 Attention 不同,$e_j$ 的加入提供了一个有助于预测的具体输入元素信息。

最终将 $c_i^l$ 加到对应的解码器层的输出 $h_i^l$。这个过程与传统的单步 Attention 不同,被称之为 Multiple Hops 13。这种方式使得模型在计算 Attention 时会考虑之前已经注意过的输入信息。


  1. Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1724–1734). ↩︎

  2. Sutskever, I., Vinyals, O., & Le, Q. V. (2014). Sequence to Sequence Learning with Neural Networks. In Z. Ghahramani, M. Welling, C. Cortes, N. D. Lawrence, & K. Q. Weinberger (Eds.), Advances in Neural Information Processing Systems 27 (pp. 3104–3112). ↩︎

  3. Bahdanau, D., Cho, K., & Bengio, Y. (2014). Neural Machine Translation by Jointly Learning to Align and Translate. arXiv preprint arXiv:1409.0473 ↩︎ ↩︎ ↩︎

  4. Xu, K., Ba, J., Kiros, R., Cho, K., Courville, A., Salakhudinov, R., … Bengio, Y. (2015). Show, Attend and Tell: Neural Image Caption Generation with Visual Attention. In International Conference on Machine Learning (pp. 2048–2057). ↩︎

  5. Luong, T., Pham, H., & Manning, C. D. (2015). Effective Approaches to Attention-based Neural Machine Translation. In Proceedings of the 2015 Conference on Empirical Methods in Natural Language Processing (pp. 1412–1421). ↩︎

  6. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … Polosukhin, I. (2017). Attention is All you Need. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 30 (pp. 5998–6008). ↩︎

  7. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep Residual Learning for Image Recognition. In 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) (pp. 770–778). ↩︎

  8. Ba, J. L., Kiros, J. R., & Hinton, G. E. (2016). Layer Normalization. arXiv preprint arXiv:1607.06450 ↩︎

  9. Yang, Z., Yang, D., Dyer, C., He, X., Smola, A., & Hovy, E. (2016). Hierarchical Attention Networks for Document Classification. In Proceedings of the 2016 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (pp. 1480–1489). ↩︎

  10. Cui, Y., Chen, Z., Wei, S., Wang, S., Liu, T., & Hu, G. (2017). Attention-over-Attention Neural Networks for Reading Comprehension. In Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (pp. 593–602). ↩︎

  11. Gehring, J., Auli, M., Grangier, D., Yarats, D., & Dauphin, Y. N. (2017). Convolutional Sequence to Sequence Learning. In International Conference on Machine Learning (pp. 1243–1252). ↩︎

  12. Dauphin, Y. N., Fan, A., Auli, M., & Grangier, D. (2016). Language Modeling with Gated Convolutional Networks. arXiv preprint arXiv:1612.08083 ↩︎

  13. Sukhbaatar, S., szlam, arthur, Weston, J., & Fergus, R. (2015). End-To-End Memory Networks. In C. Cortes, N. D. Lawrence, D. D. Lee, M. Sugiyama, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 28 (pp. 2440–2448). ↩︎

词向量 (Word Embeddings)

2018-10-01 08:00:00

文本表示

文本表示是计算机处理自然语言的核心,我们希望计算机能够同人类一样对自然语言能够实现语义层面的理解,但这并非易事。在中文和拉丁语系中,文本的直观表示就存在一定的差异,拉丁语系中词与词之间存在天然的分隔符,而中文则没有。

I can eat glass, it doesn’t hurt me.
我能吞下玻璃而不伤身体。

所以,在处理中文之前我们往往需要对原始文本进行分词,在此我们不谈这部分工作,假设我们已经得到了分词完的文本,即我们后续需要处理的“”。早期的词表示方法多采用独热编码 (One-Hot Encoding),对于每一个不同的词都使用一个单独的向量进行表示。对于一个包含 $n$ 个词的语料而言,一个词的向量表示 $\text{word}_i \in \left\{0, 1\right\}^n$ 仅在第 $i$ 的位置值为 1,其他位置的值均为 0。例如,我们可以将“父亲”表示为:

$$ \left[1, 0, 0, 0, 0, 0, ...\right] \nonumber $$

One-Hot Encoding 的表示方法十分简洁,但也存在着一些问题。

维数灾难 (The Curse of Dimensionality)

在很多现实问题中,我们仅用少数的特征是很难利用一个线性模型将数据区分开来的,也就是线性不可分问题。一个有效的方法是利用核函数实现一个非线性变换,将非线性问题转化成线性问题,通过求解变换后的线性问题进而求解原来的非线性问题。

假设 $\mathcal{X}$ 是输入空间(欧式空间 $\mathbb{R}^n$ 的子集或离散结合),$\mathcal{H}$ 为特征空间(希尔伯特空间),若存在一个从 $\mathcal{X}$$ \mathcal{H}$ 的映射:

$$\phi \left(x\right): \mathcal{X} \rightarrow \mathcal{H}$$

使得对所有 $x, z \in \mathcal{X}$ ,函数 $K\left(x, z\right)$ 满足条件:

$$K\left(x, z\right) = \phi \left(x\right) \cdot \phi \left(z\right)$$

$K\left(x, z\right)$ 为核函数, $\phi \left(x\right)$ 为映射函数,其中 $\phi \left(x\right) \cdot \phi \left(z\right)$$\phi \left(x\right)$$\phi \left(z\right)$ 的内积。

例如,对于一个下图所示的二维数据,显然是线性不可分的。

2d-Points

构建一个映射 $\phi: \mathbb{R}^2 \rightarrow \mathbb{R}^3$$X$ 映射为: $x = x^2, y = y^2, z = y$ ,则通过变换后的数据通过可视化可以明显地看出,数据是可以通过一个超平面来分开的。

3d-Points

可以说随着维度的增加,我们更有可能找到一个超平面(线性模型)将数据划分开来。尽管看起来,随着维度的增加似乎有助于我们构建模型,但是同时数据在高维空间的分布变得越来越稀疏。因此,在构建机器学习模型时,当我们需要更好的覆盖数据的分布时,我们需要的数据量就更大,这也就会导致需要更多的时间去训练模型。例如,假设所有特征均为0到1之间连续分布的数据,针对1维的情况,当覆盖50%的数据时,仅需全体50%的样本即可;针对2维的情况,当覆盖50%的数据时,则需全体71% ( $0.71^2 \approx 0.5$ ) 的样本;针对3维的情况,当覆盖50%的数据时,则需全体79% ( $0.79^3 \approx 0.5$ ),这就是我们所说的维数灾难。

分散式表示 (Distributed Representations)

分散式表示(Distributed Representations)1 最早由 Hiton 提出,对比于传统的 One-Hot Representation ,Distributed Representations 可以将数据表示为低维,稠密,连续的向量,也就是说将原始空间中的潜在信息分散的表示在低维空间的不同维度上。

传统的 One-Hot Representation 会将数据表示成一个很长的向量,例如,在 NLP 中,利用 One-Hot Representation 表示一个单词:

父亲: [1, 0, 0, 0, 0, 0, ...]
爸爸: [0, 1, 0, 0, 0, 0, ...]
母亲: [0, 0, 1, 0, 0, 0, ...]
妈妈: [0, 0, 0, 1, 0, 0, ...]

这种表示形式很简介,但也很稀疏,相当于语料库中有多少个词,则表示空间的维度就需要多少。那么,对于传统的聚类算法,高斯混合模型,最邻近算法,决策树或高斯 SVM 需要 $O\left(N\right)$ 个参数 (或 $O\left(N\right)$ 个样本) 将能够将 $O\left(N\right)$ 的输入区分开来。而像 RBMs ,稀疏编码,Auto-Encoder 或多层神经网络则可以利用 $O\left(N\right)$ 个参数表示 $O\left(2^k\right)$ 的输入,其中 $k \leq N$ 为稀疏表示中非零元素的个数 2

采用 Distributed Representation,则可以将单词表示为:

父亲: [0.12, 0.34, 0.65, ...]
爸爸: [0.11, 0.33, 0.58, ...]
母亲: [0.34, 0.98, 0.67, ...]
妈妈: [0.29, 0.92, 0.66, ...]

利用这种表示,我们不仅可以将稀疏的高维空间转换为稠密的低维空间,同时我们还能学习出文本间的语义相似性来,例如实例中的 父亲爸爸,从语义上看其均表示 父亲 的含义,但是如果利用 One-Hot Representation 编码则 父亲爸爸 的距离同其与 母亲妈妈 的距离时相同的,而利用 Distributed Representation 编码,则 父亲爸爸 之间的距离要远小于其同 母亲妈妈 之间的距离。

Word Embedding 之路

N-gram 模型

N-gram (N 元语法) 是一种文本表示方法,指文中连续出现的 $n$ 个词语。N-gram 模型是基于 $n-1$ 阶马尔科夫链的一种概率语言模型,可以通过前 $n-1$ 个词对第 $n$ 个词进行预测。Bengio 等人 3 提出了一个三层的神经网络的概率语言模型,其网络结构如下图所示:

NPLM-Network

模型的最下面为前 $n-1$ 个词 $w_{t-n+1}, ..., w_{t-2}, w_{t-1}$,每个词 $w_i$ 通过查表的方式同输入层对应的词向量 $C \left(w_i\right)$ 相连。词表 $C$ 为一个 $\lvert V\rvert \times m$ 大小的矩阵,其中 $\lvert V\rvert$ 表示语料中词的数量,$m$ 表示词向量的维度。输入层则为前 $n-1$ 个词向量拼接成的向量 $x$,其维度为 $m \left(n-1\right) \times 1$。隐含层直接利用 $d + Hx$ 计算得到,其中 $H$ 为隐含层的权重,$d$ 为隐含层的偏置。输出层共包含 $\lvert V\rvert$ 个神经元,每个神经元 $y_i$ 表示下一个词为第 $i$ 个词的未归一化的 log 概率,即:

$$ y = b + Wx + U \tanh \left(d + Hx\right) $$

对于该问题,我们的优化目标为最大化如下的 log 似然函数:

$$ L = \dfrac{1}{T} \sum_{t}{f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right) + R \left(\theta\right)} $$

其中,$f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right)$ 为利用前 $n-1$ 个词预测当前词 $w_t$ 的条件概率,$R \left(\theta\right)$ 为参数的正则项,$\theta = \left(b, d, W, U, H, C\right)$$C$ 作为模型的参数之一,随着模型的训练不断优化,在模型训练完毕后,$C$ 中保存的即为词向量。

Continuous Bag-of-Words (CBOW) 和 Skip-gram 模型

CBOW 和 Skip-gram 均考虑一个词的上下文信息,两种模型的结构如下图所示:

CBOW-Skipgram

两者在给定的上下文信息中 (即前后各 $m$ 个词) 忽略了上下文环境的序列信息,CBOW (上图左) 是利用上下文环境中的词预测当前的词,而 Skip-gram (上图右) 则是用当前词预测上下文中的词。

对于 CBOW,$x_{1k}, x_{2k}, ..., x_{Ck}$ 为上下文词的 One-Hot 表示,$\mathbf{W}_{V \times N}$ 为所有词向量构成的矩阵 (词汇表),$y_j$ 为利用上下文信息预测得到的当前词的 One-Hot 表示输出,其中 $C$ 为上下文词汇的数量,$V$ 为词汇表中词的总数量,$N$ 为词向量的维度。从输入层到隐含层,我们对输入层词对应的词向量进行简单的加和,即:

$$ h_i = \sum_{c=1}^{C}{x_{ck} \mathbf{W}_{V \times N}} $$

对于 Skip-gram,$x_k$ 为当前词的 One-Hot 表示,$\mathbf{W}_{V \times N}$ 为所有词向量构成的矩阵 (词汇表),$y_{1j}, y_{2j}, ..., y_{Cj}$ 为预测的上次文词汇的 One-Hot 表示输出。从输入层到隐含层,直接将 One-Hot 的输入向量转换为词向量表示即可。

除此之外两者还有一些其他的区别:

  1. CBOW 要比 Skip-gram 模型训练快。从模型中我们不难发现:从隐含层到输出层,CBOW 仅需要计算一个损失,而 Skip-gram 则需要计算 $C$ 个损失再进行平均进行参数优化。
  2. Skip-gram 在小数量的数据集上效果更好,同时对于生僻词的表示效果更好。CBOW 在从输入层到隐含层时,对输入的词向量进行了平均 (可以理解为进行了平滑处理),因此对于生僻词,平滑后则容易被模型所忽视。

Word2Vec

Mikolov 等人 4 利用上面介绍的 CBOW 和 Skip-gram 两种模型提出了经典的 Word2Vec 算法。Word2Vec 中针对 CBOW 和 Skip-gram 又提出了两种具体的实现方案 Hierarchical Softmax (层次 Softmax) 和 Negative Sampling (负采样),因此共有 4 种不同的模型。

基于 Hierarchical Softmax 的 CBOW 模型如下

Hierarchical-Softmax-CBOW

其中:

  1. 输入层:包含了 $C$ 个词的词向量,$\mathbf{v} \left(w_1\right), \mathbf{v} \left(w_2\right), ..., \mathbf{v} \left(w_C\right) \in \mathbb{R}^N$$N$ 为词向量的维度。
  2. 投影层:将输入层的向量进行加和,即:$\mathbf{x}_w = \sum_{i=1}^{C}{\mathbf{v} \left(w_i\right)} \in \mathbb{R}^N$
  3. 输出层:输出为一颗二叉树,是根据语料构建出来的 Huffman 树 5,其中每个叶子节点为词汇表中的一个词。

Hierarchical Softmax 是解决概率语言模型中计算效率的关键,CBOW 模型去掉了隐含层,同时将输出层改为了 Huffman 树。对于该模型的优化求解,我们首先引入一些符号,对于 Huffman 树的一个叶子节点 (即词汇表中的词 $w$),记:

首先我们需要根据向量 $\mathbf{x}_w$ 和 Huffman 树定义条件概率 $p \left(w | Context\left(w\right)\right)$。我们可以将其视为一系列的二分类问题,在到达对应的叶子节点的过程中,经过的每一个非叶子节点均为对应一个取值为 0 或 1 的 Huffman 编码。因此,我们可以将编码为 1 的节点定义为负类,将编码为 0 的节点定义为正类 (即分到左边为负类,分到右边为正类),则这条路径上对应的标签为:

$$ Label \left(p_i^w\right) = 1 - d_i^w, i = 2, 3, ..., l^w $$

则对于一个节点被分为正类的概率为 $\sigma \left(\mathbf{x}_w^{\top} \theta\right)$,被分为负类的概率为 $1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right)$。则条件概率可以表示为:

$$ p \left(w | Context\left(w\right)\right) = \prod_{j=2}^{l^w}{p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right)} $$

其中

$$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \begin{cases} \sigma \left(\mathbf{x}_w^{\top} \theta\right) & d_j^w = 0 \\ 1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right) & d_j^w = 1 \end{cases} $$

或表示为:

$$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w} $$

则对数似然函数为:

$$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log \prod_{j=2}^{l^w}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w}\right\}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{j=2}^{l^w}{\left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\}}} \end{split} \end{equation} $$

记上式花括号中的内容为 $\mathcal{L} \left(w, j\right)$,则 $\mathcal{L} \left(w, j\right)$ 关于 $\theta_{j-1}^w$ 的梯度为:

$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \theta_{j-1}^w} &= \dfrac{\partial}{\partial \theta_{j-1}^w} \left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\} \\ &= \left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right) \mathbf{x}_w \\ &= \left\{\left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right\} \mathbf{x}_w \\ &= \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w \end{split} \end{equation} $$

$\theta_{j-1}^w$ 的更新方式为:

$$ \theta_{j-1}^w \gets \theta_{j-1}^w + \eta \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w $$

同理可得,$\mathcal{L} \left(w, j\right)$ 关于 $\mathbf{x}_w$ 的梯度为:

$$ \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w} = \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \theta_{j-1}^w $$

$\mathbf{x}_w$ 为上下文词汇向量的加和,Word2Vec 的做法是将梯度贡献到上下文中的每个词向量上,即:

$$ \mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \eta \sum_{j=2}^{l^w}{\dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w}}, u \in Context \left(w\right) $$

基于 Hierarchical Softmax 的 CBOW 模型的随机梯度上升算法伪代码如下:

\begin{algorithm}
\caption{基于 Hierarchical Softmax 的 CBOW 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\STATE $\mathbf{x}_w = \sum_{u \in Context \left(w\right)}{\mathbf{v} \left(u\right)}$
\FOR{$j = 2, 3, ..., l^w$}
    \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)$
    \STATE $g = \eta \left(1 - d_j^w - q\right)$
    \STATE $\mathbf{e} \gets \mathbf{e} + g \theta_{j-1}^w$
    \STATE $\theta_{j-1}^w \gets \theta_{j-1}^w + g \mathbf{x}_w$
\ENDFOR
\FOR{$u \in Context \left(w\right)$}
    \STATE $\mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \mathbf{e}$
\ENDFOR
\end{algorithmic}
\end{algorithm}

基于 Hierarchical Softmax 的 Skip-gram 模型如下

Hierarchical-Softmax-Skipgram

对于 Skip-gram 模型,是利用当前词 $w$ 对上下文 $Context \left(w\right)$ 中的词进行预测,则条件概率为:

$$ p \left(Context \left(w\right) | w\right) = \prod_{u \in Context \left(w\right)}{p \left(u | w\right)} $$

类似于 CBOW 模型的思想,有:

$$ p \left(u | w\right) = \prod_{j=2}^{l^u}{p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right)} $$

其中

$$ p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right) = \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u} $$

可得对数似然函数为:

$$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log \prod_{u \in Context \left(w\right)}{\prod_{j=2}^{l^u}{\left\{\left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u}\right\}}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\}}}} \end{split} \end{equation} $$

记上式花括号中的内容为 $\mathcal{L} \left(w, u, j\right)$,在 $\mathcal{L} \left(w, u, j\right)$ 关于 $\theta_{j-1}^u$ 的梯度为:

$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \theta_{j-1}^{u}} &= \dfrac{\partial}{\partial \theta_{j-1}^{u}} \left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\} \\ &= \left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right) \mathbf{v} \left(w\right) \\ &= \left\{\left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right\} \mathbf{v} \left(w\right) \\ &= \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) \end{split} \end{equation} $$

$\theta_{j-1}^u$ 的更新方式为:

$$ \theta_{j-1}^u \gets \theta_{j-1}^u + \eta \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) $$

同理可得,$\mathcal{L} \left(w, u, j\right)$ 关于 $\mathbf{v} \left(w\right)$ 的梯度为:

$$ \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)} = \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \theta_{j-1}^u $$

$\mathbf{v} \left(w\right)$ 的更新方式为:

$$ \mathbf{v} \left(w\right) \gets \mathbf{v} \left(w\right) + \eta \sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)}}} $$

基于 Hierarchical Softmax 的 Skip-gram 模型的随机梯度上升算法伪代码如下:

\begin{algorithm}
\caption{基于 Hierarchical Softmax 的 Skig-gram 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\FOR{$u \in Context \left(w\right)$}
    \FOR{$j = 2, 3, ..., l^u$}
        \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^u\right)$
        \STATE $g = \eta \left(1 - d_j^u - q\right)$
        \STATE $\mathbf{e} \gets \mathbf{e} + g \theta_{j-1}^u$
        \STATE $\theta_{j-1}^u \gets \theta_{j-1}^u + g \mathbf{v} \left(w\right)$
    \ENDFOR
\ENDFOR
\STATE $\mathbf{v} \left(w\right) \gets \mathbf{v} \left(w\right) + \mathbf{e}$
\end{algorithmic}
\end{algorithm}

基于 Negative Sampling (NEG) 的模型相比于基于 Hierarchical Softmax 的模型不再使用复杂的 Huffman 树,而是使用简单的随机负采样,从而大幅的提高了模型的性能。

基于 Negative Sampling 的 CBOW 模型如下

对于基于 Negative Sampling CBOW 模型,已知词 $w$ 的上下文 $Context \left(w\right)$,预测词 $w$,则词 $w$ 即为一个正样本,其他词则为负样本。对于一个给定 $Context \left(w\right)$ 的负样本集合 $NEG \left(w\right) \neq \varnothing$,词典中的任意词 $\forall \tilde{w} \in \mathcal{D}$,其样本的标签定义为:

$$ L^w \left(\tilde{w}\right) = \begin{cases} 1, & \tilde{w} = w \\ 0, & \tilde{w} \neq w \end{cases} $$

则对于一个正样本 $\left(Context, \left(w\right)\right)$,我们希望最大化:

$$ g \left(w\right) = \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{p \left(u | Context \left(w\right)\right)} $$

或表示为:

$$ p \left(u | Context \left(w\right)\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(w\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(w\right)} $$

即增大正样本概率的同时减少负样本的概率。对于一个给定的语料库 $\mathcal{C}$,对数似然函数为:

$$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &= \sum_{w \in \mathcal{C}}{\log \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\}}} \end{split} \end{equation} $$

记上式花括号中的内容为 $\mathcal{L} \left(w, u\right)$,则 $\mathcal{L} \left(w, u\right)$ 关于 $\theta^u$ 的梯度为:

$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \theta^u} &= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\} \\ &= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right) \mathbf{x}_w \\ &= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right\} \mathbf{x}_w \\ &= \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w \end{split} \end{equation} $$

$\theta^u$ 的更新方式为:

$$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w $$

同理可得,$\mathcal{L} \left(w, u\right)$ 关于 $\mathbf{x}_w$ 的梯度为:

$$ \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w} = \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \theta^u $$

$\mathbf{v} \left(\tilde{w}\right), \tilde{w} \in Context \left(w\right)$ 的更新方式为:

$$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w}}, \tilde{w} \in Context \left(w\right) $$

基于 Negative Sampling 的 CBOW 模型的随机梯度上升算法伪代码如下:

\begin{algorithm}
\caption{基于 Negative Sampling 的 CBOW 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\STATE $\mathbf{x}_w = \sum_{u \in Context \left(w\right)}{\mathbf{v} \left(u\right)}$
\FOR{$u \in Context \left\{w\right\} \cup NEG \left(w\right)$}
    \STATE $q = \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)$
    \STATE $g = \eta \left(L^w \left(u\right) - q\right)$
    \STATE $\mathbf{e} \gets \mathbf{e} + g \theta^u$
    \STATE $\theta^u \gets \theta^u + g \mathbf{x}_w$
\ENDFOR
\FOR{$u \in Context \left(w\right)$}
    \STATE $\mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \mathbf{e}$
\ENDFOR
\end{algorithmic}
\end{algorithm}

基于 Negative Sampling 的 Skip-gram 模型如下

对于 Skip-gram 模型,利用当前词 $w$ 对上下文 $Context \left(w\right)$ 中的词进行预测,则对于一个正样本 $\left(Context, \left(w\right)\right)$,我们希望最大化:

$$ g \left(w\right) = \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{p \left(u | \tilde{w}\right)}} $$

其中,$NEG^{\tilde{w}} \left(w\right)$ 为处理词 $\tilde{w}$ 时生成的负样本集合,且:

$$ p \left(u | \tilde{w}\right) = \begin{cases} \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) & L^w \left(u\right) = 1 \\ 1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) & L^w \left(u\right) = 0 \end{cases} $$

或表示为:

$$ p \left(u | \tilde{w}\right) = \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)} $$

对于一个给定的语料库 $\mathcal{C}$,对数似然函数为:

$$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &= \sum_{w \in \mathcal{C}}{\log \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{\left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{\tilde{w} \in Context \left(w\right)}{\sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\}}}} \end{split} \end{equation} $$

记上式花括号中的内容为 $\mathcal{L} \left(w, \tilde{w}, u\right)$,则 $\mathcal{L} \left(w, \tilde{w}, u\right)$ 关于 $\theta^u$ 的梯度为:

$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \theta^u} &= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\} \\ &= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right) \mathbf{v} \left(\tilde{w}\right) \\ &= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right\} \mathbf{v} \left(\tilde{w}\right) \\ &= \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) \end{split} \end{equation} $$

$\theta^u$ 的更新方式为:

$$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) $$

同理可得,$\mathcal{L} \left(w, \tilde{w}, u\right)$ 关于 $\mathbf{v} \left(\tilde{w}\right)$ 的梯度为:

$$ \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)} = \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \theta^u $$

$\mathbf{v} \left(\tilde{w}\right)$ 的更新方式为:

$$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)}} $$

基于 Negative Sampling 的 Skig-gram 模型的随机梯度上升算法伪代码如下:

\begin{algorithm}
\caption{基于 Negative Sampling 的 Skig-gram 随机梯度上升算法}
\begin{algorithmic}
\STATE $\mathbf{e} = 0$
\FOR{$\tilde{w} \in Context \left(w\right)$}
    \FOR{$u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)$}
        \STATE $q = \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)$
        \STATE $g = \eta \left(L^w \left(u\right) - q\right)$
        \STATE $\mathbf{e} \gets \mathbf{e} + g \theta^u$
        \STATE $\theta^u \gets \theta^u + g \mathbf{v} \left(\tilde{w}\right)$
    \ENDFOR
\ENDFOR
\STATE $\mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \mathbf{e}$
\end{algorithmic}
\end{algorithm}

无论是基于 Negative Sampling 的 CBOW 模型还是 Skip-gram 模型,我们都需要对于给定的词 $w$ 生成 $NEG \left(w\right)$,对于一个词典 $\mathcal{D}$ 和给定的语料 $\mathcal{C}$,一个词被选择中的概率为:

$$ p_{NEG} \left(w\right) = \dfrac{\#w}{\sum_{u \in \mathcal{D}}{\#u}} $$

其中 $\#w$$\#u$ 表示词 $w$$u$ 在语料 $\mathcal{C}$ 中出现的频次。在 Word2Vec 的 C 代码中 6,并没有使用词的原始频次,而是对其做了 0.75 次幂,即:

$$ p_{NEG} \left(w\right) = \dfrac{\left(\#w\right)^{0.75}}{\sum_{u \in \mathcal{D}}{\left(\#u\right)^{0.75}}} $$

本节内容参考了 licstar 的 博客 和 peghoty 的 博客

其他 Embedding 方法

GloVe

GloVe (Global Vector 的简写) 是由 Pennington 等人 7 提出了一种词向量生成方法,该方法利用了语料的全局统计信息。

$X$ 表示词与词之间的共现矩阵,$X_{ij}$ 表示词 $j$ 在词 $i$ 为上下文的情况下出现的频次。则 $X_i = \sum_{k}{X_{ik}}$ 表示在词$i$ 为上下文的情况任意词出现的总次数。令 $P_{ij} = P \left(j | i\right) = X_{ij} / X_i$ 表示词 $j$ 在词 $i$ 出现前提下出现的条件概率。

例如,我们令 $i = ice, j = steam$,则这两个词之间的关系可以利用同其他词 $k$ 共现概率的比率学习得出。则有:

  1. 与词 ice 相关,但与词 steam 不太相关,例如 $k = solid$,则比率 $P_{ik} / P_{jk}$ 应该较大;类似的当词 $k$steam 相关,但与词 ice 不太相关,则比率 $P_{ik} / P_{jk}$ 应该较小。
  2. 当与词 ice 和词 steam 均相关或者均不太相关时,例如 $k = water$$k = fashion$,则比率 $P_{ik} / P_{jk}$ 应该和 1 接近。

下表展示了在一个大量语料上的概率及其比率:

概率和比例 $k = solid$ $k = gas$ $k = water$ $k = fashion$
$P \left(k \vert ice\right)$ $1.9 \times 10^{-4}$ $6.6 \times 10^{-5}$ $3.0 \times 10^{-3}$ $1.7 \times 10^{-5}$
$P \left(k \vert steam\right)$ $2.2 \times 10^{-5}$ $7.8 \times 10^{-4}$ $2.2 \times 10^{-3}$ $1.8 \times 10^{-5}$
$P \left(k \vert ice\right) / P \left(k \vert steam\right)$ $8.9$ $8.5 \times 10^{-2}$ $1.36$ $0.96$

根据如上的假设,我们可以得到一个最基础的模型:

$$ F \left(w_i, w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$

其中 $w \in \mathbb{R}^d$ 为词向量,$\tilde{w}_k \in \mathbb{R}^d$ 为单独的上下文词的词向量。假设向量空间是一个线性结构,因此 $F$ 仅依赖于两个向量之间的差异,则模型可以改写为:

$$ F \left(w_i - w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$

上式中右面是一个标量,如果左面的参数利用一个复杂的模型进行计算,例如神经网络,则会破坏我们希望保留的线性结构。因此,我们对参数采用点积运算,即:

$$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$

在词之间的共现矩阵中,一个词和其上下文中的一个词之间应该是可以互换角色的。首先我们要保证 $F$$\left(\mathbb{R}, +\right)$$\left(\mathbb{R}_{>0}, \times\right)$ 上是同态的 (homomorphism),例如:

$$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{F \left(w_i^{\top} \tilde{w}_k\right)}{F \left(w_j^{\top} \tilde{w}_k\right)} $$

其中 $F \left(w_i^{\top} \tilde{w}_k\right) = P_{ik} = \dfrac{X_{ik}}{X_i}$,则上式的一个解为 $F = \exp$,或:

$$ w_i^{\top} \tilde{w}_k = \log \left(P_{ik}\right) = \log \left(X_{ik}\right) - \log \left(X_i\right) $$

其中 $\log \left(X_i\right)$$k$ 无关记为 $b_i$,同时为了对称性添加 $\tilde{b}_k$,则上式改写为:

$$ w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k = \log \left(X_{ik}\right) $$

上式中,左侧为词向量的相关运算,右侧为共现矩阵的常量信息,则给出模型的损失函数如下:

$$ J = \sum_{i,j=1}^{V}{f \left(X_{ij}\right) \left(w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k - \log X_{ij}\right)^2} $$

其中,$V$ 为词典中词的个数,$f$ 为一个权重函数,其应具有如下特点:

  1. $f \left(0\right) = 0$。如果 $f$ 为一个连续函数,则当 $x \to 0$$\lim_{x \to 0}{f \left(x\right) \log^2 x}$ 应足够快地趋近于无穷。
  2. $f \left(x\right)$ 应为非减函数,以确保稀少的共现不会权重过大。
  3. $f \left(x\right)$ 对于较大的 $x$ 应该相对较小,以确保过大的共现不会权重过大。

文中给出了一个符合要求的函数如下:

$$ f \left(x\right) = \begin{cases} \left(x / x_{\max}\right)^{\alpha} & \text{if} \ x < x_{\max} \\ 1 & \text{otherwise} \end{cases} $$

其中两个超参数的值建议为 $x_{\max} = 100, \alpha = 0.75$

fastText

fastText 是由 Bojanowski 和 Grave 等人 8 提出的一种词向量表示方法。原始的 Skip-gram 模型忽略了词语内部的结构信息,fastText 利用 N-gram 方法将其考虑在内。

对于一个词 $w$,利用一系列的 N-gram 进行表示,同时在词的前后添加 <> 边界符号以同其他文本序列进行区分。同时还将词语本身也包含在这个 N-gram 集合中,从而学习到词语的向量表示。例如,对于词 $where$$n = 3$,则 N-gram 集合为:<wh, whe, her, ere, re>,同时包含词本身 <where>。需要注意的是,序列 <her> 与词 $where$ 中的 tri-gram her 是两个不同的概念。模型提取所有 $3 \leq n \leq 6$ 的 N-gram 序列。

假设 N-gram 词典的大小为 $G$,对于一个词 $w$$\mathcal{G}_w \subset \left\{1, ..., G\right\}$ 表示词中出现的 N-gram 的集合。针对任意一个 N-gram $g$,用向量 $\mathbf{z}_g$ 表示,则我们利用一个词的所有 N-gram 的向量的加和表示该词。可以得到该模型的评分函数为:

$$ s \left(w, c\right) = \sum_{g \in \mathcal{G}_w}{\mathbf{z}_g^{\top} \mathbf{v}_c} $$

模型在学习不同词向量时可以共享权重 (不同词的可能包含相同的 N-gram),使得在学习低频词时也可得到可靠的向量表示。

WordRank

WordRank 是由 Ji 等人 9 提出的一种词向量表示方法,其将词向量学习问题转换成一个排序问题。

我们令 $\mathbf{u}_w$ 表示当前词 $w$$k$ 维词向量,$\mathbf{v}_c$ 表示当前词上下文 $c$ 的词向量。通过两者的内积 $\langle \mathbf{u}_w, \mathbf{v}_c \rangle$ 来捕获词 $w$ 和上下文 $c$ 之间的关系,两者越相关则该内积越大。对于一个给定的词 $w$,利用上下文集合 $\mathcal{C}$ 同词的内积分数进行排序,对于一个给定的上下文 $c$,排序为:

$$ \begin{equation} \begin{split} \text{rank} \left(w, c\right) &= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c \rangle - \langle \mathbf{u}_w, \mathbf{v}_{c'} \rangle \leq 0\right)} \\ &= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle \leq 0\right)} \end{split} \end{equation} $$

其中,$I \left(x \leq 0\right)$ 为一个 0-1 损失函数,当 $x \leq 0$ 时为 1 其他情况为 0。由于 $I \left(x \leq 0\right)$ 为一个非连续函数,因此我们可以将其替换为一个凸上限函数 $\ell \left(\cdot\right)$,其可以为任意的二分类损失函数,构建排序的凸上限如下:

$$ \text{rank} \left(w, c\right) \leq \overline{\text{rank}} \left(w, c\right) = \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)} $$

我们期望排序模型将更相关的上下文排在列表的顶部,基于此构建损失函数如下:

$$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{w \in \mathcal{W}}{\sum_{c \in \Omega_w}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)}} $$

其中,$\mathcal{W}$ 表示词典,$\mathbf{U} := \left\{\mathbf{u}_w\right\}_{w \in \mathcal{W}}$$\mathbf{V} := \left\{\mathbf{c}_w\right\}_{c \in \mathcal{C}}$ 分别表示词及其上下文词向量的参数,$\Omega_w$ 表示与词 $w$ 共现的上下文的集合,$r_{w, c}$ 为衡量 $w$$c$ 之间关系的权重,$\rho \left(\cdot\right)$ 为用于衡量排序好坏的单调递增的损失函数,$\alpha \geq 0, \beta \geq 0$ 为超参数。可选的有:

$$ r_{w, c} = \begin{cases} \left(X_{w, c} / x_{\max}\right)^{\epsilon} & \text{if} \ X_{w, c} < x_{\max} \\ 1 & \text{otherwise} \end{cases} $$

其中 $x_{\max} = 100, \epsilon = 0.75$。根据 $\rho \left(\cdot\right)$ 的要求,损失函数在排序的顶部 (rank 值小) 的地方更加敏感,同时对于 rank 值较大的地方不敏感。这可以使得模型变得更加稳健 (避免语法错误和语言的非常规使用造成干扰),因此可选的有:

$$ \begin{equation} \begin{split} \rho \left(x\right) &:= \log_2 \left(1 + x\right) \\ \rho \left(x\right) &:= 1 - \dfrac{1}{\log_2 \left(2 + x\right)} \\ \rho \left(x\right) &:= \dfrac{x^{1 - t} - 1}{1 - t}, t \neq 1 \end{split} \end{equation} $$

损失函数可以等价的定义为:

$$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{\left(w, c\right) \in \Omega}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)} $$

在训练过程中,外层的求和符号容易利用 SDG 算法解决,但对于内层的求和符号除非 $\rho \left(\cdot\right)$ 是一个线性函数,否则难以求解。然而,$\rho \left(\cdot\right)$ 函数的性质要求其不能是一个线性函数,但我们可以利用其凹函数的特性对其进行一阶泰勒分解,有:

$$ \rho \left(x\right) \leq \rho \left(\xi^{-1}\right) + \rho' \left(\xi^{-1}\right) \cdot \left(x - \xi^{-1}\right) $$

对于任意 $x$$\xi \neq 0$ 均成立,同时当且仅当 $\xi = x^{-1}$ 时等号成立。因此,令 $\Xi := \left\{\xi_{w, c}\right\}_{\left(w, c\right) \in \Sigma}$,则可以得到 $J \left(\mathbf{U}, \mathbf{V}\right)$ 的一个上界:

$$ \begin{equation} \begin{split} \overline{J} \left(\mathbf{U}, \mathbf{V}, \Xi\right) &:= \sum_{\left(w, c\right) \in \Omega}{r_{w, c} \cdot \left\{\rho \left(\xi_{wc}^{-1}\right) + \rho' \left(\xi_{wc}^{-1}\right) \cdot \left(\alpha^{-1} \beta + \alpha^{-1} \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) - \xi_{w, c}^{-1}}\right)\right\}} \\ &= \sum_{\left(w, c, c'\right)}{r_{w, c} \cdot \left(\dfrac{\rho \left(\xi_{w, c}^{-1}\right) + \rho' \left(\xi_{w, c}^{-1}\right) \cdot \left(\alpha^{-1} \beta - \xi_{w, c}^{-1}\right)}{\lvert \mathcal{C} \rvert - 1} + \dfrac{1}{\alpha} \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)\right)} \end{split} \end{equation} $$

其中,$\left(w, c, c'\right) \in \Omega \times \left(\mathcal{C} \setminus \left\{c\right\}\right)$,至此我们可以通过均匀采样 $\left(w, c\right) \in \Sigma$$c' \in \mathcal{C} \setminus \left\{c\right\}$ 解决训练问题。

整个 WordRank 算法的伪代码如下:

\begin{algorithm}
\caption{WordRank 算法}
\begin{algorithmic}
\STATE $\eta$ 为学习率
\WHILE{$\mathbf{U}$,$\mathbf{V}$ 和 $\Xi$ 未收敛}
    \STATE \COMMENT{阶段1:更新 $\mathbf{U}$ 和 $\mathbf{V}$}
    \WHILE{$\mathbf{U}$ 和 $\mathbf{V}$ 未收敛}
        \STATE 从 $\Omega$ 中均匀采样 $\left(w, c\right)$
        \STATE 从 $\mathcal{C} \setminus \left\{c\right\}$ 中均匀采样 $c'$
        \STATE \COMMENT{同时更新如下 3 个参数}
        \STATE $\mathbf{u}_w \gets \mathbf{u}_w - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \left(\mathbf{v}_c - \mathbf{v}_{c'}\right)$
        \STATE $\mathbf{v}_c \gets \mathbf{v}_c - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \mathbf{u}_w$
        \STATE $\mathbf{v}_{c'} \gets \mathbf{v}_{c'} - \eta \cdot r_{w, c} \cdot \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell' \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) \cdot \mathbf{u}_w$
    \ENDWHILE
    \STATE \COMMENT{阶段2:更新 $\Xi$}
    \FOR{$w \in \mathcal{W}$}
        \FOR{$c \in \mathcal{C}$}
            \STATE $\xi_{w, c} = \alpha / \left(\sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) + \beta}\right)$
        \ENDFOR
    \ENDFOR
\ENDWHILE
\end{algorithmic}
\end{algorithm}

cw2vec

cw2vec 是由 Cao 等人 10 提出的一种基于汉字笔画 N-gram 的中文词向量表示方法。该方法根据汉字作为象形文字具有笔画信息的特点,提出了笔画 N-gram 的概念。针对一个词的笔画 N-gram,其生成过程如下图所示:

cw2vec-Stroke-N-gram-Generation

共包含 4 个步骤:

  1. 将一个词拆解成单个的汉字,例如:“大人” 拆解为 “大” 和 “人”。
  2. 将每个汉字拆解成笔画,例如:“大” 和 “人” 拆解为 “一,丿,乀,丿,乀”。
  3. 将每个笔画映射到对应的编码序列,例如: “一,丿,乀,丿,乀” 映射为 13434。
  4. 利用编码序列生成笔画 N-gram,例如:134,343,434;1343,3434;13434。

模型中定义一个词 $w$ 及其上下文 $c$ 的相似度如下:

$$ sim \left(w, c\right) = \sum_{q \in S\left(w\right)}{\vec{q} \cdot \vec{c}} $$

其中,$S$ 为由笔画 N-gram 构成的词典,$S \left(w\right)$ 为词 $w$ 对应的笔画 N-gram 集合,$q$ 为该集合中的一个笔画 N-gram,$\vec{q}$$q$ 对应的向量。

该模型的损失函数为:

$$ \mathcal{L} = \sum_{w \in D}{\sum_{c \in T \left(w\right)}{\log \sigma \left(sim \left(w, c\right)\right) + \lambda \mathbb{E}_{c' \sim P} \left[\log \sigma \left(- sim \left(w, c'\right)\right)\right]}} $$

其中,$D$ 为语料中的全部词语,$T \left(w\right)$ 为给定的词 $w$ 和窗口内的所有上次文词,$\sigma \left(x\right) = \left(1 + \exp \left(-x\right)\right)^{-1}$$\lambda$ 为负采样的个数,$\mathbb{E}_{c' \sim P} \left[\cdot\right]$ 表示负样本 $c'$ 按照 $D$ 中词的分布 $P$ 进行采样,该分布可以为词的一元模型的分布 $U$,同时为了避免数据的稀疏性问题,类似 Word2Vec 中的做法采用 $U^{0.75}$


  1. Hinton, G. E. (1986, August). Learning distributed representations of concepts. In Proceedings of the eighth annual conference of the cognitive science society (Vol. 1, p. 12). ↩︎

  2. Bengio, Y., Courville, A., & Vincent, P. (2013). Representation learning: A review and new perspectives. IEEE transactions on pattern analysis and machine intelligence, 35(8), 1798-1828. ↩︎

  3. Bengio, Y., Ducharme, R., Vincent, P., & Jauvin, C. (2003). A Neural Probabilistic Language Model. Journal of Machine Learning Research, 3(Feb), 1137–1155. ↩︎

  4. Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. arXiv preprint arXiv:1301.3781 ↩︎

  5. https://zh.wikipedia.org/zh/霍夫曼编码 ↩︎

  6. https://code.google.com/archive/p/word2vec/ ↩︎

  7. Pennington, J., Socher, R., & Manning, C. (2014). Glove: Global Vectors for Word Representation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1532–1543). ↩︎

  8. Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). Enriching Word Vectors with Subword Information. Transactions of the Association for Computational Linguistics, 5, 135–146. ↩︎

  9. Ji, S., Yun, H., Yanardag, P., Matsushima, S., & Vishwanathan, S. V. N. (2016). WordRank: Learning Word Embeddings via Robust Ranking. In Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing (pp. 658–668). ↩︎

  10. Cao, S., Lu, W., Zhou, J., & Li, X. (2018). cw2vec: Learning Chinese Word Embeddings with Stroke n-gram Information. In Thirty-Second AAAI Conference on Artificial Intelligence↩︎

循环神经网络 (Recurrent Neural Network, RNN)

2018-09-21 08:00:00

文章部分内容参考了 Christopher 的博客 Understanding LSTM Networks,内容翻译和图片重绘已得到原作者同意,重绘后的图片源文件请参见 这里

发展史

循环神经网络 (Recurrent Neural Network, RNN) 一般是指时间递归神经网络而非结构递归神经网络 (Recursive Neural Network),其主要用于对序列数据进行建模。Salehinejad 等人 1 的一篇综述文章列举了 RNN 发展过程中的一些重大改进,如下表所示:

Year 1st Author Contribution
1990 Elman Popularized simple RNNs (Elman network)
1993 Doya Teacher forcing for gradient descent (GD)
1994 Bengio Difficulty in learning long term dependencies with gradient descend
1997 Hochreiter LSTM: long-short term memory for vanishing gradients problem
1997 Schuster BRNN: Bidirectional recurrent neural networks
1998 LeCun Hessian matrix approach for vanishing gradients problem
2000 Gers Extended LSTM with forget gates
2001 Goodman Classes for fast Maximum entropy training
2005 Morin A hierarchical softmax function for language modeling using RNNs
2005 Graves BLSTM: Bidirectional LSTM
2007 Jaeger Leaky integration neurons
2007 Graves MDRNN: Multi-dimensional RNNs
2009 Graves LSTM for hand-writing recognition
2010 Mikolov RNN based language model
2010 Neir Rectified linear unit (ReLU) for vanishing gradient problem
2011 Martens Learning RNN with Hessian-free optimization
2011 Mikolov RNN by back-propagation through time (BPTT) for statistical language modeling
2011 Sutskever Hessian-free optimization with structural damping
2011 Duchi Adaptive learning rates for each weight
2012 Gutmann Noise-contrastive estimation (NCE)
2012 Mnih NCE for training neural probabilistic language models (NPLMs)
2012 Pascanu Avoiding exploding gradient problem by gradient clipping
2013 Mikolov Negative sampling instead of hierarchical softmax
2013 Sutskever Stochastic gradient descent (SGD) with momentum
2013 Graves Deep LSTM RNNs (Stacked LSTM)
2014 Cho Gated recurrent units
2015 Zaremba Dropout for reducing Overfitting
2015 Mikolov Structurally constrained recurrent network (SCRN) to enhance learning longer memory for vanishing gradient problem
2015 Visin ReNet: A RNN-based alternative to convolutional neural networks
2015 Gregor DRAW: Deep recurrent attentive writer
2015 Kalchbrenner Grid long-short term memory
2015 Srivastava Highway network
2017 Jing Gated orthogonal recurrent units

RNN

网络结构

不同于传统的前馈神经网络接受特定的输入得到输出,RNN 由人工神经元和一个或多个反馈循环构成,如下图所示:

RNN-Loops

其中,$\boldsymbol{x}_t$ 为输入层,$\boldsymbol{h}_t$ 为带有循环的隐含层,$\boldsymbol{y}_t$ 为输出层。其中隐含层包含一个循环,为了便于理解我们将循环进行展开,展开后的网络结构如下图所示:

RNN-Loops-Unrolled

对于展开后的网络结构,其输入为一个时间序列 $\left\{\dotsc, \boldsymbol{x}_{t-1}, \boldsymbol{x}_t, \boldsymbol{x}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{x}_t \in \mathbb{R}^n$$n$ 为输入层神经元个数。相应的隐含层为 $\left\{\dotsc, \boldsymbol{h}_{t-1}, \boldsymbol{h}_t, \boldsymbol{h}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{h}_t \in \mathbb{R}^m$$m$ 为隐含层神经元个数。隐含层节点使用较小的非零数据进行初始化可以提升整体的性能和网络的稳定性 2。隐含层定义了整个系统的状态空间 (state space),或称之为 memory 1

$$ \boldsymbol{h}_t = f_H \left(\boldsymbol{o}_t\right) $$

其中

$$ \boldsymbol{o}_t = \boldsymbol{W}_{IH} \boldsymbol{x}_t + \boldsymbol{W}_{HH} \boldsymbol{h}_{t-1} + \boldsymbol{b}_h $$

$f_H \left(\cdot\right)$ 为隐含层的激活函数,$\boldsymbol{b}_h$ 为隐含层的偏置向量。对应的输出层为 $\left\{\dotsc, \boldsymbol{y}_{t-1}, \boldsymbol{y}_t, \boldsymbol{y}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{y}_t \in \mathbb{R}^p$$p$ 为输出层神经元个数。则:

$$ \boldsymbol{y}_t = f_O \left(\boldsymbol{W}_{HO} \boldsymbol{h}_t + \boldsymbol{b}_o\right) $$

其中 $f_O \left(\cdot\right)$ 为隐含层的激活函数,$\boldsymbol{b}_o$ 为隐含层的偏置向量。

在 RNN 中常用的激活函数为双曲正切函数:

$$ \tanh \left(x\right) = \dfrac{e^{2x} - 1}{e^{2x} + 1} $$

Tanh 函数实际上是 Sigmoid 函数的缩放:

$$ \sigma \left(x\right) = \dfrac{1}{1 + e^{-x}} = \dfrac{\tanh \left(x / 2\right) + 1}{2} $$

梯度弥散和梯度爆炸

原始 RNN 存在的严重的问题就是梯度弥散 (Vanishing Gradients)梯度爆炸 (Exploding Gradients)。我们以时间序列中的 3 个时间点 $t = 1, 2, 3$ 为例进行说明,首先假设神经元在前向传导过程中没有激活函数,则有:

$$ \begin{equation} \begin{split} &\boldsymbol{h}_1 = \boldsymbol{W}_{IH} \boldsymbol{x}_1 + \boldsymbol{W}_{HH} \boldsymbol{h}_0 + \boldsymbol{b}_h, &\boldsymbol{y}_1 = \boldsymbol{W}_{HO} \boldsymbol{h}_1 + \boldsymbol{b}_o \\ &\boldsymbol{h}_2 = \boldsymbol{W}_{IH} \boldsymbol{x}_2 + \boldsymbol{W}_{HH} \boldsymbol{h}_1 + \boldsymbol{b}_h, &\boldsymbol{y}_2 = \boldsymbol{W}_{HO} \boldsymbol{h}_2 + \boldsymbol{b}_o \\ &\boldsymbol{h}_3 = \boldsymbol{W}_{IH} \boldsymbol{x}_3 + \boldsymbol{W}_{HH} \boldsymbol{h}_2 + \boldsymbol{b}_h, &\boldsymbol{y}_3 = \boldsymbol{W}_{HO} \boldsymbol{h}_3 + \boldsymbol{b}_o \end{split} \end{equation} $$

在对于一个序列训练的损失函数为:

$$ \mathcal{L} \left(\boldsymbol{y}, \boldsymbol{\hat{y}}\right) = \sum_{t=0}^{T}{\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)} $$

其中 $\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)$$t$ 时刻的损失。我们利用 $t = 3$ 时刻的损失对 $\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}, \boldsymbol{W}_{HO}$ 求偏导,有:

$$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HO}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{W}_{HO}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{IH}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{IH}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HH}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{HH}} \end{split} \end{equation} $$

因此,不难得出对于任意时刻 $t$$\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}$ 的偏导为:

$$ \dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{IH}} = \sum_{k=0}^{t}{\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{y}_t} \dfrac{\partial \boldsymbol{y}_t}{\partial \boldsymbol{h}_t} \left(\prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}}\right) \dfrac{\partial \boldsymbol{h}_k}{\partial \boldsymbol{W}_{IH}}} $$

$\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$ 同理可得。对于 $\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$,在存在激活函数的情况下,有:

$$ \prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}} = \prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}} $$

假设激活函数为 $\tanh$,下图刻画了 $\tanh$ 函数及其导数的函数取值范围:

Tanh-Function

可得,$0 \leq \tanh' \leq 1$,同时当且仅当 $x = 0$ 时,$\tanh' \left(x\right) = 1$。因此:

  1. $t$ 较大时,$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$ 趋近于 0,则会产生梯度弥散问题。
  2. $\boldsymbol{W}_{HH}$ 较大时,$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$ 趋近于无穷,则会产生梯度爆炸问题。

长期依赖问题

RNN 隐藏节点以循环结构形成记忆,每一时刻的隐藏层的状态取决于它的过去状态,这种结构使得 RNN 可以保存、记住和处理长时期的过去复杂信号。但有的时候,我们仅需利用最近的信息来处理当前的任务。例如:考虑一个用于利用之前的文字预测后续文字的语言模型,如果我们想预测 “the clouds are in the sky” 中的最后一个词,我们不需要太远的上下信息,很显然这个词就应该是 sky。在这个情况下,待预测位置与相关的信息之间的间隔较小,RNN 可以有效的利用过去的信息。

RNN-Long-Term-Dependencies-Short

但也有很多的情况需要更多的上下文信息,考虑需要预测的文本为 “I grew up in France … I speak fluent French”。较近的信息表明待预测的位置应该是一种语言,但想确定具体是哪种语言需要更远位置的“在法国长大”的背景信息。理论上 RNN 有能力处理这种长期依赖,但在实践中 RNN 却很难解决这个问题 3

RNN-Long-Term-Dependencies-Long

LSTM

LSTM 网络结构

长短时记忆网络 (Long Short Term Memroy, LSTM) 是由 Hochreiter 和 Schmidhuber 4 提出一种特殊的 RNN。LSTM 的目的就是为了解决长期依赖问题,记住长时间的信息是 LSTM 的基本功能。

所有的循环神经网络都是由重复的模块构成的一个链条。在标准的 RNN 中,这个重复的模块的结构比较简单,仅包含一个激活函数为 $\tanh$ 的隐含层,如下图所示:

RNN

LSTM 也是类似的链条状结构,但其重复的模块的内部结构不同。模块内部并不是一个隐含层,而是四个,并且以一种特殊的方式进行交互,如下图所示:

LSTM

下面我们将一步一步的介绍 LSTM 单元 (cell) 的具体工作原理,在之前我们先对使用到的符号进行简单的说明,如下图所示:

LSTM-Operations-Symbols

其中,每条线都包含一个从输出节点到其他节点的整个向量,粉红色的圆圈表示逐元素的操作,黄色的矩形为学习到的神经网络层,线条的合并表示连接,线条的分叉表示内容的复制并转移到不同位置。

LSTM 单元状态和门控机制

LSTM 的关键为单元的状态 (cell state),即下图中顶部水平穿过单元的直线。单元的状态像是一条传送带,其直接运行在整个链条上,同时仅包含少量的线性操作。因此,信息可以很容易得传递下去并保持不变。

LSTM-Cell-State

LSTM 具有向单元状态添加或删除信息的能力,这种能力被由一种称之为“门” (gates) 的结构所控制。门是一种可选择性的让信息通过的组件,其由一层以 Sigmoid 为激活函数的网络层和一个逐元素相乘操作构成的,如下图所示:

LSTM-Pointwise-Operation

Sigmoid 层的输出值介于 0 和 1 之间,代表了所允许通过的数据量。0 表示不允许任何数据通过,1 表示允许所有数据通过。一个 LSTM 单元包含 3 个门用于控制单元的状态。

LSTM 工作步骤

LSTM 的第一步是要决定从单元状态中所忘记的信息,这一步是通过一个称之为“遗忘门 (forget gate)”的 Sigmoid 网络层控制。该层以上一时刻隐含层的输出 $h_{t-1}$ 和当前这个时刻的输入 $x_t$ 作为输入,输出为一个介于 0 和 1 之间的值,1 代表全部保留,0 代表全部丢弃。回到之前的语言模型,单元状态需要包含主语的性别信息以便选择正确的代词。但当遇见一个新的主语后,则需要忘记之前主语的性别信息。

LSTM-Cell-Forget-Gate

$$ f_t = \sigma \left(W_f \cdot \left[h_{t-1}, x_t\right] + b_f\right) $$

第二步我们需要决定要在单元状态中存储什么样的新信息,这包含两个部分。第一部分为一个称之为“输入门 (input gate)” 的 Sigmoid 网络层,其决定更新那些数据。第二部分为一个 Tanh 网络层,其将产生一个新的候选值向量 $\tilde{C}_t$ 并用于添加到单元状态中。之后会将两者进行整合,并对单元状态进行更新。在我们的语言模型中,我们希望将新主语的性别信息添加到单元状态中并替代需要忘记的旧主语的性别信息。

LSTM-Cell-Input-Gate

$$ \begin{equation} \begin{split} i_t &= \sigma \left(W_i \cdot \left[h_{t-1}, x_t\right] + b_i\right) \\ \tilde{C}_t &= \tanh \left(W_C \cdot \left[h_{t-1}, x_t\right] + b_C\right) \end{split} \end{equation} $$

接下来需要将旧的单元状态 $C_{t-1}$ 更新为 $C_t$。我们将旧的单元状态乘以 $f_t$ 以控制需要忘记多少之前旧的信息,再加上 $i_t \odot \tilde{C}_t$ 用于控制单元状态的更新。在我们的语言模型中,该操作真正实现了我们对与之前主语性别信息的遗忘和对新信息的增加。

LSTM-Cell-State-Update

$$ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t $$

最后我们需要确定单元的输出,该输出将基于单元的状态,但为一个过滤版本。首先我们利用一个 Sigmoid 网络层来确定单元状态的输出,其次我们对单元状态进行 $\tanh$ 操作 (将其值缩放到 -1 和 1 之间) 并与之前 Sigmoid 层的输出相乘,最终得到需要输出的信息。

LSTM-Cell-Output-Gate

$$ \begin{equation} \begin{split} o_t &= \sigma \left(W_o \cdot \left[h_{t-1}, x_t\right] + b_o\right) \\ h_t &= o_t \odot \tanh \left(C_t\right) \end{split} \end{equation} $$

LSTM 变种

上文中介绍的基础的 LSTM 模型,事实上不同学者对 LSTM 的结构进行了或多或少的改变,其中一个比较有名的变种是由 Gers 和 Schmidhuber 提出的 5。其添加了一种“窥视孔连接 (peephole connections)”,这使得每一个门结构都能够窥视到单元的状态。

Peephole-Cell

$$ \begin{equation} \begin{split} f_t &= \sigma \left(W_f \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_f\right) \\ i_t &= \sigma \left(W_i \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_i\right) \\ o_t &= \sigma \left(W_o \cdot \left[\boldsymbol{C_t}, h_{t-1}, x_t\right] + b_o\right) \end{split} \end{equation} $$

另一个变种是使用了成对的遗忘门和输入门。不同于一般的 LSTM 中分别确定需要遗忘和新添加的信息,成对的遗忘门和输入门仅在需要添加新输入是才会忘记部分信息,同理仅在需要忘记信息时才会添加新的输入。

CFIG-Cell

$$ C_t = f_t \odot C_{t-1} + \boldsymbol{\left(1 - f_t\right)} \odot \tilde{C}_t $$

另外一个比较有名的变种为 Cho 等人提出的 Gated Recurrent Unit (GRU) 6,单元结构如下:

GRU-Cell

GRU 将遗忘门和输入门整个成一层,称之为“更新门 (update gate)”,同时配以一个“重置门 (reset gate)”。具体的计算过程如下:

首先计算更新门 $z_t$ 和重置门 $r_t$

$$ \begin{equation} \begin{split} z_t &= \sigma \left(W_z \cdot \left[h_{t-1}, x_t\right]\right) \\ r_t &= \sigma \left(W_r \cdot \left[h_{t-1}, x_t\right]\right) \end{split} \end{equation} $$

其次计算候选隐含层 (candidate hidden layer) $\tilde{h}_t$,与 LSTM 中计算 $\tilde{C}_t$ 类似,其中 $r_t$ 用于控制保留多少之前的信息:

$$ \tilde{h}_t = \tanh \left(W \cdot \left[r_t \odot h_{t-1}, x_t\right]\right) $$

最后计算需要从之前的隐含层 $h_{t-1}$ 遗忘多少信息,同时加入多少新的信息 $\tilde{h}_t$$z_t$ 用于控制这个比例:

$$ h_t = \left(1 - z_t\right) \odot h_{t-1} + z_t \odot \tilde{h}_t $$

因此,对于短距离依赖的单元重置门的值较大,对于长距离依赖的单元更新门的值较大。如果 $r_t = 1$ 并且 $z_t = 0$,则 GRU 退化为一个标准的 RNN。

除此之外还有大量的 LSTM 变种,Greff 等人 7 对一些常见的变种进行了比较,Jozefowicz 等人 8 测试了大量的 RNN 结构在不同任务上的表现。

扩展与应用

循环神经网络在序列建模上有着天然的优势,其在自然语言处理,包括:语言建模,语音识别,机器翻译,对话与QA,文本生成等;计算视觉,包括:目标识别,视觉追踪,图像生成等;以及一些综合场景,包括:图像标题生成,视频字幕生成等,多个领域均有不错的表现,有代表性的论文请参见 awesome-rnn

Google 的 Magenta 是一项利用机器学习创作艺术和音乐的研究,其中也包含了大量利用 RNN 相关模型构建的有趣项目。SketchRNN 是由 Ha 等人 9 提出了一种能够根据用户描绘的一些简单图形自动完成后续绘画的 RNN 网络。

SketchRNN-Demo

Performance RNN 是由 Ian 等人 10 提出了一种基于时间和动态因素生成复合音乐的 LSTM 网络。

Performance-RNN-Demo

更多有趣的作品请参见 Megenta 的 Demos 页面。


  1. Salehinejad, H., Sankar, S., Barfett, J., Colak, E., & Valaee, S. (2017). Recent Advances in Recurrent Neural Networks. arXiv preprint arXiv:1801.01078. ↩︎ ↩︎

  2. Sutskever, I., Martens, J., Dahl, G., & Hinton, G. (2013). On the importance of initialization and momentum in deep learning. In International Conference on Machine Learning (pp. 1139–1147). ↩︎

  3. Bengio, Y., Simard, P., & Frasconi, P. (1994). Learning long-term dependencies with gradient descent is difficult. IEEE Transactions on Neural Networks, 5(2), 157–166. ↩︎

  4. Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural Computation, 9(8), 1735–1780. ↩︎

  5. Gers, F. A., & Schmidhuber, J. (2000). Recurrent nets that time and count. In Proceedings of the IEEE-INNS-ENNS International Joint Conference on Neural Networks. IJCNN 2000. Neural Computing: New Challenges and Perspectives for the New Millennium (Vol. 3, pp. 189–194 vol.3). ↩︎

  6. Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1724–1734). ↩︎

  7. Greff, K., Srivastava, R. K., Koutník, J., Steunebrink, B. R., & Schmidhuber, J. (2017). LSTM: A Search Space Odyssey. IEEE Transactions on Neural Networks and Learning Systems, 28(10), 2222–2232. ↩︎

  8. Jozefowicz, R., Zaremba, W., & Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. In Proceedings of the 32Nd International Conference on International Conference on Machine Learning - Volume 37 (pp. 2342–2350). ↩︎

  9. Ha, D., & Eck, D. (2017). A Neural Representation of Sketch Drawings. arXiv preprint arXiv:1704.03477 ↩︎

  10. Ian S., & Sageev O. Performance RNN: Generating Music with Expressive Timing and Dynamics. Magenta Blog, 2017. https://magenta.tensorflow.org/performance-rnn ↩︎

泰国之行 (Tour of Thailand)

2018-09-15 08:00:00

近来这大半年的工作感觉活活像一场清宫剧,对于我们这种一心只想撸代码,两耳不闻窗外事的人来说,确实太累了。从年初换了工作方向后,交接了所有之前的线上任务和系统,所以这次旅行格外的清净,没有一封报警邮件。北京已经入秋,走之前还很热,回来已凉意浓浓,去的路上天气格外的好。

进去大皇宫不能衣冠不整,牛仔裤上有个洞,在门口买了条裤子套上,还挺配我的白衬衫,感觉满满的社会人气息 😎。发现中国人在外面很难 High 起来,油轮之上小姐姐唱着不同国家的歌曲,独独唱到中文歌的时候没有人上去跳。也许我们天生没有欧美人的开放,也不像中东朋友在国家内被束缚的太紧,不过感觉出来玩还是不要闷骚,把激情都释放出来才好。

Pattaya 的水上市场虽然有商业景区的味道,但还是保留了很多当地的特色,水流两旁很多卖水果和小吃的小船。海边的日落很美,再来杯美酒,再来个佳人就更好了。

只在金沙岛上呆了小半天,天公还算作美,早起下着小雨,上岛了雨就停了。想想上次下水游泳还是上初中时候的事情了,虽然不怎么太会游,但至少还能扑腾两下。下次再有机会来,一定直奔普吉岛,舒舒服服的在岛上呆上几天,别的啥也不干,就游游泳,晒晒太阳。

这边最多的当然是各种各样的水果和超大只的海鲜,第一次吃到蛇皮果,据说这东西很壮阳。最鲜的还是螃蟹,越大只越好,泰国的酱料味道还是很特别的,不难吃但不是很习惯,不过海鲜什么都不沾,也很鲜甜。

泰国的枪械管得比较松,打了几枪后座力比较强的 .45 手枪和霰弹枪,成绩还不错,有 10 环哦。回来想一想比国内打枪便宜不少,后悔没有把所有的枪型都试一试。

媒介之战 (War of Medias)

2018-09-01 08:00:00

本文为《娱乐至死》(Amusing Ourselved To Death) 的读书随想。

在这本书中,作者 Neil Postman 的基本观点为推崇铅字文化,声讨电视文化。首先,我必须承认作者对电视文化的很多现象描述确实存在,我虽然也一直知道其存在,但却从未思考过其中的问题,这是这本书对我影响最多的地方。换言之,是书籍 (铅字文化) 能够让我更深入的思考问题,这也是作者所推崇的铅字文化的益处。同时,作者并没有否定电视文化作为娱乐本身的用途,这点我也是认同的。无论是电影,电视剧还是综艺节目,电视文化确实以一种五彩斑斓的形式丰富着我们的娱乐生活。但我认为作者对于两种不同文化的观点略微有些绝对和偏激,尤其是在书籍和电视 (不同的信息载体) 内容过剩的今天,我认为两者都存在精华和糟粕 (同时包括文化内容和娱乐内容)。我认为书籍和电视中文化和娱乐的界线也不是很明显,虽然我支持作者的不要将娱乐和一些严肃的事情 (例如:政治,宗教,教育等) 混在一起,但是我也不否能不能从电视文化中获取知识。我认为更重要的是对于信息的细粒度消化,在这个过程中比较重要的是 思考实践

作者在书的最前面提到:

奥威尔担心我们憎恨的东西会毁掉我们,而赫胥黎担心的是,我们将毁于我们热爱的东西。

在文末又再次呼应了这个观点:

有两种方法可以让文化精神枯萎,一种是奥威尔式的 – 文化成为一个监狱,另一种是赫胥黎式的 – 文化成为一种滑稽戏。

最后,又引用了赫胥黎在《美丽新世界》中的观点:

人们感到痛苦的不是他们用笑声代替了思考,而是他们不知道自己为什么笑以及为什么不再思考

最后这句最符合我的观点,下面从几个方面聊聊我对书中观点的一些看法和自己的一些补充。

信息、文化和知识

作者用了两章的内容讨论 媒介 的作用,那么我就从媒介在信息的传递,文化的形成和知识的获取的三个角度来谈一谈我对媒介的认知。

信息的传递

信息,我认为可以简单的理解为我们所能感知到的一切,这里的 感知 其实就可以理解为 媒介。对于信息本身,可以将其粗略的划分为两类:有用的信息无用的信息。理论一点的解释就是:从信息论的角度而言,能够帮助降低系统不确定性 (信息熵) 的事物就认为是有用的信息 (与真实概念略有差异);通俗一点的解释就是:你在过马路时,你能够感知到行人,车辆和建筑物等各种信息,此时你更关心的是那些会影响到你生命安全的车辆和行人,这些事物就是有用的信息。

无论是有用的信息还是无用的信息一定是通过某种方式 (嗅觉,听觉,视觉等) 和形态 (书籍,音乐,电视等) 传递到我们的大脑。所以我们对于事物的认知都不是透过其内涵本身,而是通过其在媒介中的具体表现形式。同一个事物本身透过不同的媒介,可能具有不同的具体形态,而我们直接去理解的正是这个具体的形态,这也就是麦克卢汉所说的 “媒介即信息”

文化的形成

文化知识 有相似之处,我认为两者的主要区别在于:文化是一个比较中性的描述,其表示一段时间内形成的某些习惯 (例如:饮食,建筑等),强调的是 习惯本身,而非 对习惯的评价 (好与坏)。而知识往往更像是一种具有 “好”的影响 的文化结晶,这个“好”可以对于集体而言也可以对于个人而言。

这些“习惯”又是如何一步步形成的呢?首先,我认为客观环境会是一个比较大的影响因素。从很底层出发,人首先需要做到同自然的抗争和共处,所以就会有人担忧干旱,有人担忧洪涝,自然会促使人们向不同方向发展。其次,就是我们人的主观因素了,我认为人的因素要更为复杂些。文化并不是一成不变的,而是在不断的演变,这种演变正是我们人自己主观选择的结果。

知识的获取

如上文中,我将 知识 定义为具有“好”的影响的文化,所以知识的获取就可以理解为对信息的选择。我认为对选择的结果产生影响的主要有两个方面:用于选择的 信息池选择的方法。这个池子就像是作者所描述的不同媒介所包含的信息,铅字文化中包含更多的是知识类型的信息,而电视文化中包含的信息更多的是娱乐类型的信息。选择的方法则像一个甄别和抽象的工具,对你感知到的信息进行筛选,提取和抽象,得到最终有意义的知识。我认为两者都很重要,但在信息过剩的今天,后者对于我们自己 可控性 会更好些。

严肃性与思考

到这,我们就聊聊刚才说的可控性更好的选择方法,概括而言这个方法就是 思考。思考本身是具有 严肃性 的,在谈及思考的严肃性之前,我们先说一下信息的严肃性。在书的第六章和第七章中,对于电视文化的抨击主要有如下几点:

  1. 电视不再是为我们展示具有娱乐性的内容,而是将所有内容都以娱乐方式表现出来。
  2. 电视呈现的事件都是独立存在的,剥夺了其与过去,未来和其他事件之间的关联。

第一点其实就是在说一些严肃性的信息 (例如:宗教,教育,政治等) 不应该以电视这种形式进行展现。我很赞同这一点,从思考的角度,这些严肃的事情是需要不断思考的,也就是说思考是贯穿在这些信息的接受之中的,而电视往往没有在中途预留很多时间,很快便进入了预先设计好的后续环节。

第二点其实作者更多的描述的是 电视新闻,我也很赞同这一点,确实这些被剥离同其他事情关联的快速新闻,只能片面的向我们做出了事情本身的局部,长此以往只会让我们对其变得麻木不仁,因为他们关心的是给观众留下印象而非观点。

面对不同形式的信息,我们思考的方式也会有不同。例如读一本书,遇到不熟悉的名词,你可能需要停下来仔细调研思考之后才会继续阅读,避免影响对后续内容的理解。又例如观影,电影更擅长以视觉冲击让你对其中的某些场景留下深刻的印象,而对电影的思考往往是以事后对其内容进行反思的形式,进一步理解其深层含义。但无论是以何种形式去思考,最重要的一点是 独立思考,也就是我想表达的思考的严肃性。思考不必可避免的会涉及到对相同事物不同观点的发表和交流,独立思考让我们做到不人云亦云,同时我们也需要做到不固执己见。

思考与实践

思考不能停留在精神层面,思考可以让你不人云亦云,但是对事物的理解本身又有太多的主观性,其正确性却有待验证。实践 则可以帮助你检查你思考结果的正确性,否定自己错误的判断,避免固执己见。怎么去实践又是一门学问,但也大同小异,我感觉比较有效的几个方法如下:

  1. 反复。是说对于一个事情在不同的时点可以重复思考,比如书可以再读一篇,电影可以再看一次,每一次都会有不同的收获。
  2. 笔记。这点对于读书和观影都有效,把想法写下来,不光会让你的思路变得更加清晰,有时你还会在总结的过程发现自相矛盾的地方,有助于自我改正。
  3. 做实验。好与不好,搞一下不就知道了?拿我们这群做模型的人来说,各种深度学习算法原理掌握的再好,不放在具体的问题上,不用真实的数据试一试,都很难说孰优孰劣。

媒介之战

回到这本书的核心 – “媒介”。看起来媒介对我们像是一种 被动 的影响,而思考是一种 主动 的干预。但我认为媒介的演变其实就是我们主动选择的结果,一种对精神放纵的结果。现在不同媒介之间,甚至相同媒介内部都充满着过剩的信息,那些让你感觉获取方便,理解容易的信息所包含的知识应该不多。知识的获取一定是一件困难的事情,当你过于放纵,贪图舒适的信息获取,那必然仅能得到有限的知识。所以不要对于媒介的呈现形式所迷惑,对于铅字文化也好,对于电视文化也好,你都需要保持思辨的精神,不要让这场媒介之战影响我们对于知识的获取和未知的探索。

What you see with your eyes may not be true, see it with your heart.

一句本来之前用于讽刺自己的话,现在看来放在这也挺合适,不要浮于事物的表象,更重要的是你对事物的看法。

卷积神经网络 (Convolutional Neural Network, CNN)

2018-08-25 08:00:00

发展史

卷积神经网络 (Convolutional Neural Network, CNN) 是一种目前广泛用于图像,自然语言处理等领域的深度神经网络模型。1998 年,Lecun 等人 1 提出了一种基于梯度的反向传播算法用于文档的识别。在这个神经网络中,卷积层 (Convolutional Layer) 扮演着至关重要的角色。

随着运算能力的不断增强,一些大型的 CNN 网络开始在图像领域中展现出巨大的优势,2012 年,Krizhevsky 等人 2 提出了 AlexNet 网络结构,并在 ImageNet 图像分类竞赛 3 中以超过之前 11% 的优势取得了冠军。随后不同的学者提出了一系列的网络结构并不断刷新 ImageNet 的成绩,其中比较经典的网络包括:VGG (Visual Geometry Group) 4,GoogLeNet 5 和 ResNet 6

CNN 在图像分类问题上取得了不凡的成绩,同时一些学者也尝试将其应用在图像的其他领域,例如:物体检测 789,语义分割 10,图像摘要 11,行为识别 12 等。除此之外,在非图像领域 CNN 也取得了一定的成绩 13

模型原理

下图为 Lecun 等人提出的 LeNet-5 的网络架构:

LeNet-5

下面我们针对 CNN 网络中的不同类型的网络层逐一进行介绍。

输入层

LeNet-5 解决的手写数字分类问题的输入为一张 32x32 像素的灰度图像 (Gray Scale)。日常生活中计算机常用的图像的表示方式为 RGB,即将一张图片分为红色通道 (Red Channel),绿色通道 (Green Channel) 和蓝色通道 (Blue Channel),其中每个通道的每个像素点的数值范围为 $\left[0, 255\right]$。灰度图像表示该图片仅包含一个通道,也就是不具备彩色信息,每个像素点的数值范围同 RGB 图像的取值范围相同。

因此,一张图片在计算机的眼里就是一个如下图所示的数字矩阵 (示例图片来自于 MNIST 数据集 14):

Digit-Pixels

在将图像输入到 CNN 网络之前,通常我们会对其进行预处理,因为每个像素点的最大取值为 $255$,因此将每个像素点的值除以 $255$ 则可以将其归一化到 $\left[0, 1\right]$ 的范围。

卷积层

在了解卷积层之前,让我们先来了解一下什么是卷积?设 $f\left(x\right), g\left(x\right)$$\mathbb{R}$ 上的两个可积函数,则卷积定义为:

$$ \left(f * g\right) \left(x\right) = \int_{- \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right) d \tau} $$

离散形式定义为:

$$ \left(f * g\right) \left(x\right) = \sum_{\tau = - \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right)} $$

我们用一个示例来形象的理解一下卷积的含义,以离散的形式为例,假设我们有两个骰子,$f\left(x\right), g\left(x\right)$ 分别表示投两个骰子,$x$ 面朝上的概率。

$$ f \left(x\right) = g \left(x\right) = \begin{cases} 1/6 & x = 1, 2, 3, 4, 5, 6 \\ 0 & \text{otherwise} \end{cases} $$

卷积 $\left(f * g\right) \left(x\right)$ 表示投两个骰子,朝上数字之和为 $x$ 的概率。则和为 $4$ 的概率为:

$$ \begin{equation} \begin{split} \left(f * g\right) \left(4\right) &= \sum_{\tau = 1}^{6}{f \left(\tau\right) g \left(4 - \tau\right)} \\ &= f \left(1\right) g \left(4 - 1\right) + f \left(2\right) g \left(4 - 2\right) + f \left(3\right) g \left(4 - 3\right) \\ &= 1/6 \times 1/6 + 1/6 \times 1/6 + 1/6 \times 1/6 \\ &= 1/12 \end{split} \end{equation} $$

这是一维的情况,我们处理的图像为一个二维的矩阵,因此类似的有:

$$ \left(f * g\right) \left(x, y\right) = \sum_{v = - \infty}^{\infty}{\sum_{h = - \infty}^{\infty}{f \left(h, v\right) g \left(x - h, y - v\right)}} $$

这次我们用一个抽象的例子解释二维情况下卷积的计算,设 $f, g$ 对应的概率矩阵如下:

$$ f = \left[ \begin{array}{ccc} \color{red}{a_{0, 0}} & \color{orange}{a_{0, 1}} & \color{yellow}{a_{0, 2}} \\ \color{green}{a_{1, 0}} & \color{cyan}{a_{1, 1}} & \color{blue}{a_{1, 2}} \\ \color{purple}{a_{2, 0}} & \color{black}{a_{2, 1}} & \color{gray}{a_{2, 2}} \end{array} \right] , g = \left[ \begin{array}{ccc} \color{gray}{b_{-1, -1}} & \color{black}{b_{-1, 0}} & \color{purple}{b_{-1, 1}} \\ \color{blue}{b_{0, -1}} & \color{cyan}{b_{0, 0}} & \color{green}{b_{0, 1}} \\ \color{yellow}{b_{1, -1}} & \color{orange}{b_{1, 0}} & \color{red}{b_{1, 1}} \end{array} \right] $$

$\left(f * g\right) \left(1, 1\right)$ 计算方式如下:

$$ \left(f * g\right) \left(1, 1\right) = \sum_{v = 0}^{2}{\sum_{h = 0}^{2}{f \left(h, v\right) g \left(1 - h, 1 - v\right)}} $$

从这个计算公式中我们就不难看出为什么上面的 $f, g$ 两个概率矩阵的角标会写成上述形式,即两个矩阵相同位置的角标之和均为 $1$$\left(f * g\right) \left(1, 1\right)$ 即为 $f, g$ 两个矩阵中对应颜色的元素乘积之和。

在上例中,$f, g$ 两个概率矩阵的大小相同,而在 CNN 中,$f$ 为输入的图像,$g$ 一般是一个相对较小的矩阵,我们称之为卷积核。这种情况下,卷积的计算方式是类似的,只是会将 $g$ 矩阵旋转 $180^{\circ}$ 使得相乘的元素的位置也相同,同时需要 $g$$f$ 上进行滑动并计算对应位置的卷积值。下图 15 展示了一步计算的具体过程:

Conv-Example

下图 15 形象的刻画了利用一个 3x3 大小的卷积核的整个卷积计算过程:

Conv-Sobel

一些预设的卷积核对于图片可以起到不同的滤波器效果,例如下面 4 个卷积核分别会对图像产生不同的效果:不改变,边缘检测,锐化和高斯模糊。

$$ \left[ \begin{array}{ccc} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{array} \right] , \left[ \begin{array}{ccc} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{array} \right] , \left[ \begin{array}{ccc} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{array} \right] , \dfrac{1}{16} \left[ \begin{array}{ccc} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{array} \right] $$

对 lena 图片应用这 4 个卷积核,变换后的效果如下 (从左到右,从上到下):

Lena-Filters

在上面整个计算卷积的动图中,我们不难发现,利用 3x3 大小 (我们一般将这个参数称之为 kernel_size,即卷积核的大小,其可以为一个整数表示长宽大小相同,也可以为两个不同的整数) 的卷积核对 5x5 大小的原始矩阵进行卷积操作后,结果矩阵并没有保持原来的大小,而是变为了 (5-(3-1))x(5-(3-1)) (即 3x3) 大小的矩阵。这就需要引入 CNN 网络中卷积层的两个常用参数 paddingstrides

padding 是指是否对图像的外侧进行补零操作,其取值一般为 VALIDSAME 两种。VALID 表示不进行补零操作,对于输入形状为 $\left(x, y\right)$ 的矩阵,利用形状为 $\left(m, n\right)$ 的卷积核进行卷积,得到的结果矩阵的形状则为 $\left(x-m+1, y-n+1\right)$SAME 表示进行补零操作,在进行卷积操作前,会对图像的四个边缘分别向左右补充 $\left(m \mid 2 \right) + 1$ 个零,向上下补充 $\left(n \mid 2 \right) + 1$ 个零 ($\mid$ 表示整除),从而保证进行卷积操作后,结果的形状与原图像的形状保持相同,如下图 15 所示:

Conv2d-Zero-Padding

strides 是指进行卷积操作时,每次卷积核移动的步长。示例中,卷积核在横轴和纵轴方向上的移动步长均为 $1$,除此之外用于也可以指定不同的步长。移动的步长同样会对卷积后的结果的形状产生影响。

除此之外,还有另一个重要的参数 filters,其表示在一个卷积层中使用的卷积核的个数。在一个卷积层中,一个卷积核可以学习并提取图像的一种特征,但往往图片中包含多种不同的特征信息,因此我们需要多个不同的卷积核提取不同的特征。下图 15 是一个利用 4 个不同的卷积核对一张图像进行卷积操作的示意图:

Conv2d-Kernels

上面我们都是以一个灰度图像 (仅包含 1 个通道) 为示例进行的讨论,那么对于一个 RGB 图像 (包含 3 个通道),相应的,卷积核也是一个 3 维的形状,如下图 15 所示:

Conv3d-Kernels

卷积层对于我们的神经网络的模型带来的改进主要包括如下三个方面:稀疏交互 (sparse interactions)参数共享 (parameter sharing)等变表示 (equivariant representations)

在全连接的神经网络中,隐含层中的每一个节点都和上一层的所有节点相连,同时有被连接到下一层的全部节点。而卷积层不同,节点之间的连接性受到卷积核大小的制约。下图 16 分别以自下而上 (左) 和自上而下 (右) 两个角度对比了卷积层和全连接层节点之间连接性的差异。

Sparse-Interactions

在上图 (右) 中,我们可以看出节点 $s_3$ 受到节点 $x_2$$x_3$$x_4$ 的影响,这些节点被称之为 $s_3$接受域 (receptive field)。稀疏交互使得在 $m$ 个输入和 $n$ 个输出的情况下,参数的个数由 $m \times n$ 个减少至 $k \times n$ 个,其中 $k$ 为卷积核的大小。尽管一个节点在一个层级之间仅与其接受域内的节点相关联,但是对于深层中的节点,其与绝大部分输入之间却存在这间接交互,如下图 16 所示:

Indirect-Interactions

节点 $g_3$ 尽管直接的连接是稀疏的,但处于更深的层中可以间接的连接到全部或者大部分的输入节点。这就使得网络可以仅通过这种稀疏交互来高效的描述多个输入变量之间的复杂关系。

除了稀疏交互带来的参数个数减少外,参数共享也起到了类似的作用。所谓参数共享就是指在进行不同操作时使用相同的参数,具体而言也就是在我们利用卷积核在图像上滑动计算卷积时,每一步使用的卷积核都是相同的。同全连接网络的情况对比如下图 16 所示:

Parameter-Sharing

在全连接网络 (上图 - 下) 中,任意两个节点之间的连接 (权重) 仅用于这两个节点之间,而在卷积层中,如上图所示,其对卷积核中间节点 (黑色箭头) 的使用方式 (权重) 是相同的。参数共享虽然对于计算的时间复杂度没有带来改进,仍然是 $O \left(k \times n\right)$,但其却将参数个数降低至 $k$ 个。

正是由于参数共享机制,使得卷积层具有平移 等变 (equivariance) 的性质。对于函数 $f\left(x\right)$$g\left(x\right)$,如果满足 $f\left(g\left(x\right)\right) = g\left(f\left(x\right)\right)$,我们就称 $f\left(x\right)$ 对于变换 $g$ 具有等变性。简言之,对于图像如果我们将所有的像素点进行移动,则卷积后的输出表示也会移动同样的量。

非线性层

非线性层并不是 CNN 特有的网络层,在此我们不再详细介绍,一般情况下我们会使用 ReLU 作为我们的激活函数。

池化层

池化层 是一个利用 池化函数 (pooling function) 对网络输出进行进一步调整的网络层。池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出。常用的池化函数包括最大池化 (max pooling) 函数 (即给出邻域内的最大值) 和平均池化 (average pooling) 函数 (即给出邻域内的平均值) 等。但无论选择何种池化函数,当对输入做出少量平移时,池化对输入的表示都近似 不变 (invariant)局部平移不变性 是一个很重要的性质,尤其是当我们关心某个特征是否出现而不关心它出现的位置时。

池化层同卷积层类似,具有三个比较重要的参数:pool_sizestridespadding,分别表示池化窗口的大小,步长以及是否对图像的外侧进行补零操作。下图 16 是一个 pool_size=3strides=3padding='valid' 的最大池化过程示例:

Max-Pooling

池化层同时也能够提高网络的计算效率,例如上图中在横轴和纵轴的步长均为 $3$,经过池化后,下一层网络节点的个数降低至前一层的 $\frac{1}{3 \times 3} = \frac{1}{9}$

全连接层

全链接层 (Fully-connected or Dense Layer) 的目的就是将我们最后一个池化层的输出连接到最终的输出节点上。例如,最后一个池化层的输出大小为 $\left[5 \times 5 \times 16\right]$,也就是有 $5 \times 5 \times 16 = 400$ 个节点,对于手写数字识别的问题,我们的输出为 0 至 9 共 10 个数字,采用 one-hot 编码的话,输出层共 10 个节点。例如在 LeNet 中有 2 个全连接层,每层的节点数分别为 120 和 84,在实际应用中,通常全连接层的节点数会逐层递减。需要注意的是,在进行编码的时候,第一个全连接层并不是直接与最后一个池化层相连,而是先对池化层进行 flatten 操作,使其变成一个一维向量后再与全连接层相连。

输出层

输出层根据具体问题的不同会略有不同,例如对于手写数字识别问题,采用 one-hot 编码的话,输出层则包含 10 个节点。对于回归或二分类问题,输出层则仅包含 1 个节点。当然对于二分类问题,我们也可以像多分类问题一样将其利用 one-hot 进行编码,例如 $\left[1, 0\right]$ 表示类型 0,$\left[0, 1\right]$ 表示类型 1。

扩展与应用

本节我们将介绍一些经典的 CNN 网络架构及其相关的改进。

AlexNet 2

AlexNet

AlexNet 在整体结构上同 LeNet-5 类似,其改进大致如下:

LRN 的思想来自与生物学中侧抑制 (Lateral Inhibition) 的概念,简单来说就是相近的神经元之间会发生抑制作用。在 AlexNet 中,给出的 LRN 计算公式如下:

$$ b_{x,y}^{i} = a_{x,y}^{i} / \left(k + \alpha \sum_{j = \max \left(0, i - n/2\right)}^{\min \left(N - 1, i + n/2\right)}{\left(a_{x,y}^{j}\right)^2}\right)^{\beta} $$

其中,$a_{x,y}^{i}$ 表示第 $i$ 个卷积核在位置 $\left(x,y\right)$ 的输出,$N$ 为卷积核的个数,$k, n, \alpha, \beta$ 均为超参数,在原文中分别初值为:$k=2, n=5, \alpha=10^{-4}, \beta=0.75$。在上式中,分母为所有卷积核 (Feature Maps) 的加和,因此 LRN 可以简单理解为一个跨 Feature Maps 的像素级归一化。

开源实现

VGG Net 4

左图是 VGG-16 Net 的网络结构,原文中还有一个 VGG-19 Net,其差别在于后面三组卷积层中均多叠加了一个卷积层,使得网络层数由 16 增加至 19。

VGG Net 的主要改变如下:

开源实现

Network in Network (NIN) 17

NIN

NIN 网络的主要改变如下:

Global-Average-Pooling

在 NIN 中,在跨通道的情况下,mlpconv 层又等价于传统的 Conv 层后接一个 1x1 大小的卷积层,因此 mlpconv 层有时也称为 cccp (cascaded cross channel parametric pooling) 层。1x1 大小的卷积核可以说实现了不同通道信息的交互和整合,同时对于输入通道为 $m$ 和输出通道为 $n$,1x1 大小的卷积核在不改变分辨率的同时实现了降维 ($m > n$ 情况下) 或升维 ($m < n$ 情况下) 操作。

开源实现

GoogLeNet (Inception V1) 5, Inception V3 18, Inception V4 19

GoogLeNet

除了 VGG 这种从网络深度方向进行优化的策略以外,Google 还提出了在同一层利用不同大小的卷积核同时提取不同特征的思路,对于这样的结构我们称之为 Inception。

Inception-V1

上图 (左) 为原始的 Inception 结构,在这样一层中分别包括了 1x1 卷积,3x3 卷积,5x5 卷积和 3x3 Max Polling,使得网络在每一层都能学到不同尺度的特征。最后通过 Filter Concat 将其拼接为多个 Feature Maps。

这种方式虽然能够带来性能的提升,但同时也增加了计算量,因此为了进一步改善,其选择利用 1x1 大小的卷积进行降维操作,改进后的 Inception 模块如上图 (右) 所示。我们以 GoogLeNet 中的 inception (3a) 模块为例 (输入大小为 28x28x192),解释 1x1 卷积的降维效果。

对于原始 Inception 模块,1x1 卷积的通道为 64,3x3 卷积的通道为 128,5x5 卷积的通道为 32,卷积层的参数个数为:

$$ \begin{equation} \begin{split} \# w_{\text{3a_conv_without_1x1}} =& 1 \times 1 \times 192 \times 64 \\ & + 3 \times 3 \times 192 \times 128 \\ & + 5 \times 5 \times 192 \times 32 \\ =& 387072 \end{split} \end{equation} $$

对于加上 1x1 卷积后的 Inception 模块 (通道数分别为 96 和 16) 后,卷积层的参数个数为:

$$ \begin{equation} \begin{split} \# w_{\text{3a_conv_with_1x1}} =& 1 \times 1 \times 192 \times 64 \\ & + 1 \times 1 \times 192 \times 96 + 3 \times 3 \times 96 \times 128 \\ & + 1 \times 1 \times 192 \times 16 + 5 \times 5 \times 16 \times 32 \\ =& 157184 \end{split} \end{equation} $$

可以看出,在添加 1x1 大小的卷积后,参数的个数减少了 2 倍多。通过 1x1 卷积对特征进行降维的层称之为 Bottleneck Layer 或 Bottleneck Block。

在 GoogLeNet 中,作者还提出了 Auxiliary Classifiers (AC),用于辅助训练。AC 通过增加浅层的梯度来减轻深度梯度弥散的问题,从而加速整个网络的收敛。

随后 Google 在此对 Inception 进行了改进,同时提出了卷积神经网络的 4 项设计原则,概括如下:

  1. 避免表示瓶颈,尤其是在网络的浅层部分。一般来说,在到达任务的最终表示之前,表示的大小应该从输入到输出缓慢减小。
  2. 高维特征在网络的局部更容易处理。在网络中增加更多的非线性有助于获得更多的解耦特征,同时网络训练也会加快。
  3. 空间聚合可以在低维嵌入中进行,同时也不会对表征能力带来太多影响。例如,再进行尺寸较大的卷积操作之前可以先对输入进行降维处理。
  4. 在网络的宽度和深度之间进行权衡。通过增加网络的深度和宽度均能够带来性能的提升,在同时增加其深度和宽度时,需要综合考虑算力的分配。

Inception V3 的主要改进包括:

Inception-V3-3x3

Inception-V3-1xn-nx1

Inception-V3-reducing-grid-size-new

Inception V4 对于 Inception 网络做了进一步细致的调整,其主要是将 Inception V3 中的前几层网络替换为了 stem 模块,具体的 stem 模块结构就不在此详细介绍了。

开源实现

Deep Residual Net 6, Identity Mapping Residual Net 20, DenseNet 21

随着网络深度的不断增加啊,其效果并未如想象一般提升,甚至发生了退化,He 等人 6 发现在 CIFAR-10 数据集上,一个 56 层的神经网络的性能要比一个 20 层的神经网络要差。网络层级的不断增加,不仅导致参数的增加,同时也可能导致梯度弥散问题 (vanishing gradients)。

这对这些问题,He 等人提出了一种 Deep Residual Net,在这个网络结构中,残差 (residual) 的思想可以理解为:假设原始潜在的映射关系为 $\mathcal{H} \left(\mathbf{x}\right)$,对于新的网络层我们不再拟合原始的映射关系,而是拟合 $\mathcal{F} \left(\mathbf{x}\right) = \mathcal{H} \left(\mathbf{x}\right) - \mathbf{x}$,也就是说原始潜在的映射关系变为 $\mathcal{F} \left(\mathbf{x}\right) + \mathbf{x}$。新的映射关系可以理解为在网络前向传播中添加了一条捷径 (shortcut connections),如下图所示:

Residual-Block

增加 Short Connections 并没有增加参数个数,也没有增加计算量,与此同时模型依旧可以利用 SGD 等算法进行优化。

Residual-Results

从 Deep Residual Net 的实验结果 (如上图) 可以看出,在没有加入残差模块的网络中 (上图 - 左) 出现了上文中描述的问题:更多层级的网络的效果反而较差;在加入了残差模块的网络中 (上图 - 右),其整体性能均比未加入残差模块的网络要好,同时具有更多层级的网络的效果也更好。

随后 He 等人 20 又提出了 Identity Mapping Residual Net,在原始的 ResNet 中,一个残差单元可以表示为:

$$ \begin{equation} \begin{split} \mathbb{y}_{\ell} = & h \left(\mathbb{x}_{\ell}\right) + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_l\right) \\ \mathbb{x}_{\ell+1} = & f \left(\mathbb{y}_{\ell}\right) \end{split} \end{equation} $$

其中 $\mathbb{x}_{\ell}$$\mathbb{x}_{\ell+1}$ 为第 $\ell$ 个单元的输入和输出,$\mathcal{F}$ 为残差函数,$h \left(\mathbb{x}_{\ell}\right) = \mathbb{x}_{\ell}$ 为一个恒等映射,$f$ 为 ReLU 函数。在 Identity Mapping Residual Net,作者将 $f$ 由原来的 ReLU 函数也替换成一个恒定映射,即 $\mathbb{x}_{\ell+1} \equiv \mathbb{y}_{\ell}$,则上式可以改写为:

$$ \mathbb{x}_{\ell+1} = \mathbb{x}_{\ell} + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_{\ell}\right) $$

则对于任意深度的单元 $L$,有如下表示:

$$ \mathbb{x}_L = \mathbb{x}_{\ell} + \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)} $$

上式形式的表示使得其在反向传播中具有一个很好的性质,假设损失函数为 $\mathcal{E}$,根据链式法则,对于单元 $\ell$,梯度为:

$$ \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_L} \dfrac{\partial \mathbb{x}_L}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(1 + \dfrac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right) $$

对于上式形式的梯度,我们可以将其拆解为两部分:$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}}$ 为不通过任何权重层的直接梯度传递,$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right)$ 为通过权重层的梯度传递。前一项保证了梯度能够直接传回任意浅层 $\ell$,同时对于任意一个 mini-batch 的所有样本,$\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}\mathcal{F}$ 不可能永远为 $-1$,所以保证了即使权重很小的情况下也不会出现梯度弥散。下图展示了原始的 ResNet 和 Identity Mapping Residual Net 之间残差单元的区别和网络的性能差异:

Identity-Mapping-Residual-Net-Unit

Huang 等人 21 在 ResNet 的基础上又提出了 DenseNet 网络,其网络结构如下所示:

DenseNet

DenseNet 的主要改进如下:

开源实现

综合比较

Canziani 等人 22 综合了模型的准确率,参数大小,内存占用,推理时间等多个角度对现有的 CNN 模型进行了对比分析。

CNN-Accuracy-and-Parameters

上图 (左) 展示了在 ImageNet 挑战赛中不同 CNN 网络模型的 Top-1 的准确率。可以看出近期的 ResNet 和 Inception 架构以至少 7% 的显著优势超过了其他架构。上图 (右) 以另一种形式展现了除了准确率以外的更多信息,包括计算成本和网络的参数个数,其中横轴为计算成本,纵轴为 Top-1 的准确率,气泡的大小为网络的参数个数。可以看出 ResNet 和 Inception 架构相比 AlexNet 和 VGG 不仅有更高的准确率,其在计算成本和网络的参数个数 (模型大小) 方面也具有一定优势。

文章部分内容参考了 刘昕CNN近期进展与实用技巧。CNN 除了在图像分类问题上取得很大的进展外,在例如:物体检测:R-CNN 23, SPP-Net 24, Fast R-CNN 25, Faster R-CNN 26,语义分割:FCN 27 等多个领域也取得了不俗的成绩。针对不同的应用场景,网络模型和处理方法均有一定的差异,本文就不再对其他场景一一展开说明,不同场景将在后续进行单独整理。


  1. LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324. ↩︎

  2. Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems (pp. 1097-1105). ↩︎ ↩︎

  3. http://www.image-net.org/ ↩︎

  4. Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556. ↩︎ ↩︎

  5. Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., … & Rabinovich, A. (2015). Going deeper with convolutions. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 1-9). ↩︎ ↩︎

  6. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778). ↩︎ ↩︎ ↩︎

  7. Girshick, R., Donahue, J., Darrell, T., & Malik, J. (2014). Rich feature hierarchies for accurate object detection and semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 580-587). ↩︎

  8. Girshick, R. (2015). Fast r-cnn. In Proceedings of the IEEE international conference on computer vision (pp. 1440-1448). ↩︎

  9. Ren, S., He, K., Girshick, R., & Sun, J. (2015). Faster r-cnn: Towards real-time object detection with region proposal networks. In Advances in neural information processing systems (pp. 91-99). ↩︎

  10. Long, J., Shelhamer, E., & Darrell, T. (2015). Fully convolutional networks for semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3431-3440). ↩︎

  11. Vinyals, O., Toshev, A., Bengio, S., & Erhan, D. (2015). Show and tell: A neural image caption generator. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3156-3164). ↩︎

  12. Ji, S., Xu, W., Yang, M., & Yu, K. (2013). 3D convolutional neural networks for human action recognition. IEEE transactions on pattern analysis and machine intelligence, 35(1), 221-231. ↩︎

  13. Kim, Y. (2014). Convolutional neural networks for sentence classification. arXiv preprint arXiv:1408.5882. ↩︎

  14. http://yann.lecun.com/exdb/mnist ↩︎

  15. https://mlnotebook.github.io/post/CNN1/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  16. Goodfellow, I., Bengio, Y., Courville, A., & Bengio, Y. (2016). Deep learning (Vol. 1). Cambridge: MIT press. ↩︎ ↩︎ ↩︎ ↩︎

  17. Lin, M., Chen, Q., & Yan, S. (2013). Network In Network. arXiv preprint arXiv:1312.4400. ↩︎

  18. Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., & Wojna, Z. (2016). Rethinking the Inception Architecture for Computer Vision. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (pp. 2818–2826). ↩︎

  19. Szegedy, C., Ioffe, S., Vanhoucke, V., & Alemi, A. (2016). Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning. arXiv preprint arXiv:1602.07261. ↩︎

  20. He, K., Zhang, X., Ren, S., & Sun, J. (2016). Identity Mappings in Deep Residual Networks. arXiv preprint arXiv:1603.05027. ↩︎ ↩︎

  21. Huang, G., Liu, Z., van der Maaten, L., & Weinberger, K. Q. (2016). Densely Connected Convolutional Networks. arXiv preprint arXiv:1608.06993 ↩︎ ↩︎

  22. Canziani, A., Paszke, A., & Culurciello, E. (2016). An Analysis of Deep Neural Network Models for Practical Applications. arXiv preprint arXiv:1605.07678 ↩︎

  23. Girshick, R., Donahue, J., Darrell, T., & Malik, J. (2014). Rich Feature Hierarchies for Accurate Object Detection and Semantic Segmentation. In Proceedings of the 2014 IEEE Conference on Computer Vision and Pattern Recognition (pp. 580–587). ↩︎

  24. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition. IEEE Transactions on Pattern Analysis and Machine Intelligence, 37(9), 1904–1916. ↩︎

  25. Girshick, R. (2015). Fast R-CNN. arXiv preprint arXiv:1504.08083. ↩︎

  26. Ren, S., He, K., Girshick, R., & Sun, J. (2017). Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks. IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(6), 1137–1149. ↩︎

  27. Shelhamer, E., Long, J., & Darrell, T. (2017). Fully Convolutional Networks for Semantic Segmentation. IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(4), 640–651. ↩︎

买书,囤书,看书 (Buy Books, Hoard Books and Read Books)

2018-07-10 08:00:00

写这么一篇主要是想自我分析一下和读书有关的毛病,写下来会让自己认识的深刻些。尤其是最近一年,书入库明显高于出库,导致未读完的书都快摆不下了。总结起来主要是两个问题:

买书

先聊聊为啥买书,简单通俗的解释就是“想看”。我认为,这么回答一点毛病都没有,因为这是我们去探索这个未知的世界最容易的方式。古人云:“读万卷书,行万里路”,都是探索这个世界的方式,后来细查得知这句话源自于董其昌谈及绘画之道时所说 1

画家六法,一曰气韵生动。气韵不可学,此生而知之,自然天授,然亦有学得处。**读万卷书,行万里路。**胸中脱去尘浊,自然丘壑内营,成立郛郭,随手写去,皆为山水传神。

为什么说读书是其中最容易的方式呢?最主要就是经济实惠,你不需要太多的成本,包括财物和时间,当然是相对行万里路而言。这里谈及的书我不想把杂志和技术书籍囊括进来,于我而言,杂志是消磨零散时间的读物,例如:如厕,等车;而技术类的书籍有需要比较系统的整理和笔记,甚至需要反复去看和理解。其他书也可以反复看,有时候我会把喜欢的书再翻一遍,而且每一遍都会有新的发现。所以,想去买书至少能够说明你还有对知识的渴望和对未知探索的兴趣。这点我认为很重要,如果你连念想都没了,那就什么都没了。

囤书

囤书,就是买得多了,而且略微上瘾,想着可能买了就等于已经把其中的知识吃进脑子了吧。这点和我办健身卡很像,感觉卡办了,八块腹肌就有了似的,不过还好,至少我把跑步坚持下来了,关于这个类比的问题我们下面再详谈。其实,囤书本身也没有错,什么 618 啊,双 11 啊,书就是便宜吗,多买点实惠。错就错在了我们只做了“囤”的前半部,丢失了后半部。“囤”的后半部我认为是计划的制定,这其实直接决定着我们后续要说的执行。就和囤粮食一样,我们是做了详细的计划去应对未来的粮食短缺的,而不是今年收成好了就一定多囤一点,不过似乎也会有这样的趋势,和上面说书的价格类似吗。

这里说的意思就是计划的重要性,不要在脑子里想,我要干什么干什么,尤其是和我脑子一样不好用的同学,还是先写下来,坚持按照计划执行一段时间,等熟悉了,习惯养成了,再靠脑子才比较好。想想我读书最“舒服” (感觉写读的最快又不太合适,毕竟不应该以读多少,读多块论好坏) 的一段时间是我研究生期间的第一份实习,当时每天翻越 4 条地铁,从昌平线的沙河高教园到中关村。每天都很规律,早起听一路的歌,在 13 号线的换乘站看着七八辆车过去,然后被后面的人成功的推上列车;晚上也带上耳机,但不放歌,为的是减少噪音,上车找个角落,不坐,站着,用手机看一路的电子书。换乘点停一停,甚至还能简单的做些思考。就这样,不算长的实习期里看完了好几本书。这段时间虽然没有形成规范的计划,但是每天规律的上下班无形之中就促成了计划的按时执行。

看书

回到最初的两个毛病,“不看”和“看不完”。好吧,先找些客观理由,时间真的不多。记得当时看《解忧杂货铺》的时候,应该是晚上十点多开始看到,一晚上看完了,结束应该已经两三点了。作为一个靠撸代码为生的人,明天还要准时上班工作,在中国这样一个竞争激烈市场中,报答还是和加班呈强正相关的。读《解忧杂货铺》的时候,其实是不知不觉就读完了,当然不知不觉也就两三点了。所以,我认为能够促使你读完的最重要的还是内容本身,是你喜欢的风格,就不会错过。尤其是书的前几章,前面写的不好,可能就会让你失去继续读下去的兴趣。还有就是书的厚度,之前买了几本很喜欢的书,喜欢是从其题材和评价先预支的结论,但碍于实在太厚,就迟迟未读,也怕看了没看完,还不如等做好准备再去读。

所以如上文提及的,读书之于健身,能把跑步坚持下来是因为时间不长,30 分钟能跑个不到 6 公里,跑完还很舒服,会感觉收益很高。而以增肌为目的的撸铁,对于我这种本来上肢力量就很弱的男生,短时间不见效果就放弃了。但无论是健身还是读书,这个想法也都是不对的,而且我是“一直”都知道是不对的。

对于我,简单思考之后,我想最主要的问题是缺乏一种强烈的约束力去推着你动起来。我算是一个自制力一般的人,有人鞭笞我几下,效果一下子就会上来,没人鞭笞我了,就容易出现不好不差的状态。不过与其说是缺乏外界的动力,不如说还是没有触及当下的痛点。我没有八块腹肌,但也没有肚子,做不到穿衣显瘦,脱衣有肉,但也还算看得过去。同时我知道,程序员不动是一定会长肚子的,身边的例子比比皆是。所以作为半个外貌协会的会员,我能够坚持跑步。同样,读书真的是我喜欢的事情,看电影也一样,两者各自有各自体验,但当下生活的压力促使我会投入更多的时间在工作相关的领域,自然留出来给读书的时间就少了。

总结起来,如何改变这个状态,还是要制定个读书计划的,各种情况难免无法严格执行,但尽量去做。还有就是当你不开心的时候,我认为是适合读书,这个想法来自于一次周末的阅读体验。五天的工作日结束,甚是不爽,为啥就不说了,总之不爽,周末来公司,本来想再敲几行代码,心情原因,效率如实不高。翻开了之前没有带回家的冯唐的《在宇宙间不易被风吹散》,冯唐的东西总是有点他特别的地方。一个下午,几杯咖啡,一本书,慢慢的就过了,然后不开心的东西就散了。还不忘发个朋友圈装下文艺青年:

人燥的时候就多做些通灵的事,无需如鼓琴,对画,临帖一般高雅,一书,一茶足矣,静下来,腐朽之气全无。

最后,再补一下我对读书这件事我们需要以何种态度去对待的看法,我认为:读书,不是非做不可的事,而是真正想要去做的事。


  1. 《画旨》- 董其昌 ↩︎

Play Safe, Smart Choice & Yuppie

2018-06-30 08:00:00

最近是看了 Youtube 上 Wong Fu Productions 的一个系列视频 Yappie,一下子把脑子中包括很久之前的一些思考就全都串联回忆起来了,所以就写了本文,把这些凌乱的思考拼凑起来。截止到本文写完,Yappie 已经出了两集,视频可能需要梯子,为了不影响理解,简单勾勒一下剧情。

主角是一个名叫 Andrew 的亚裔美国人,他和女友 (当然后面知道才没交往多久,然后就分手了) 去听了一个关于美国亚裔 (Aisan Americans) 的一些特殊的境遇问题的 Talk Show。但 Andrew 似乎对这场演讲并不感冒,中途手机中球赛的声音打断了演讲,并被演讲者问及了姓名。Andrew 告知了姓名,演讲者随即用个玩笑回应了他的不礼貌打断,说一个美国亚裔中有 1/13 的概率叫 Andrew,有 1/5 的概率会是一名工程师,会来自于 San Gabriel Valley,喜欢篮球,Boyz II Men 和宝马 (然而这一系列看似玩笑的猜测,后面剧情验证,他就是这样的一个 Andrew)。演讲过后 Andrew 的女友和演讲者做着进一步的沟通,Andrew 过去为自己的不礼貌表示道歉,并想融入他们的谈话,但却完全和他们不在一个频道。然后,就没有然后了,他和女友就 OVER 了,并且女友说自己就不应该和一个 Yuppie 男约会。所以,什么是 Yuppie?其实他们的这段分手对白说的就很清楚了:

Girl: I knew I shouldn’t have swiped on a Yuppie.
Andrew: A what?
Girl: You’re a Yuppie, Andrew.
Andrew: Are we doing nicknames now or …?
Girl: A young Asian professional who acts like a Yuppie? Cause all you care about is earning a nice salary, and buying a nice car, and settling down in a nice suburb.
Andrew: That sounds like a nice, normal person. What’s so wrong with that?
Girl: It’s not wrong. It’s just … safe. Listen, we’re just … not the right fit. I need to be with someone who you know … cares, more about the world.

yappie-ep1-screen-shot

后面,Andrew 回到公司后,貌似是人力的小姐姐要找他聊天,关于之前 Andrew 想进行岗位异动的事情。人力小姐姐说今年内是无法进行了,但 Andrew 也只是叹了叹气,说 that’s fine。这一次,他又选择了一个 Safe 的方式,或许他在想至少还能保留现在的职位。但在和人力小姐姐沟通的后面,他已经开始走神思考着什么了。同时,在第 2 集中得知 Andrew 与前女友 Lana 分手貌似也是因为对他这种 Safe 的生活方式的不认同,后面的剧情中 Andrew 已经开始对着这种 Safe 的生活方式进行了改变,就不在详细介绍了。

对与错,好与坏

Andrew 之所以很困惑为什么会叫他 Yuppie,是因为他认为他现在所做的事情 (赚钱,买好车,买豪宅) 是再正常不过的事情了,是对的 (Right),是好的 (Good),是明智的选择 (Smart Choice)。他不明白,为什么有人会对这些再正常不过的事情产生质疑。但是大家有没有想过:

什么是对,什么是错,什么是好,什么又是坏?

让我真正重视这些问题是源自哈佛大学的一门公开课 Justice: What’s The Right Thing To Do?,现在 Michael 教授也出了这门课程对应的书 《正義: 一場思辨之旅》。课程的一开始就抛出了经典的 电车难题 (Trolley Problem),问题如下图所示:

trolley-problem

原始版本的两个问题都是有一辆快速行驶的电车,在电车当前行驶的轨道前面被绑了 5 个人,如果不采取任何措施的话,这 5 个人就会被电车轧死。假设你是唯一一个能够影响这件事情发生的人,在上图左面中的情况,你能够操纵一个轨道变换器,能够让电车变到另一条轨道上,但是这条轨道上也绑了一个人,电车会把这个人轧死;在上图右面中的情况,你站在一个桥上,你面前有个胖子,如果你把他退下去,靠他的身体就能逼停电车,但这个胖子会死掉,同时如果你跳下去是无法逼停电车的。问题就是,针对不同的情况你,你会如果选择?

在课堂上统计的结果是,左面的情况会有更多的人选择变换轨道,牺牲一个人,挽救 5 个人;而右面的情况则会有更少的人选择去推那个胖子。我会有同样的选择,理由是右面的情况另外一个人的死亡会和我“看似”有更直接的关系,因此导致我的负罪感也会更强,课程的后面又从功利主义和个人权利的角度对相关问题进行了讨论。

当然抛出这个问题的目的不是想让我们陷入这个哲学的问题无法自拔,而是说我们是不是应该想想是不是没有所谓的“真”的“对”,当然这有可能陷入“循环嵌套”的困境,什么又是“真”,不必纠结这个,理解这个意思就好。或许有人会说,“对”就是“对”啊,怎么还会有“真”的“对”,其实不难解释,学了这么多年数学,我们都清楚:结论是建立在一定的前提假设下的,如果假设变了,那么结果的“真”与“假”也可能会发生变化。除非,对,除非前提是公理或者公设,无需证明就是对的,好吧,那是不是又要陷进去了,“公理”就是“真”的“公理”吗?不要陷进去,就假设“公理”“真”的是“公理”,那么 Andrew 就是把太多东西当成“公理”了,我们很多人都是,也许亚裔,甚至是中国人这方面会更严重一些。那么,这个问题出在了哪儿呢?

思辨精神与教育

问题就出在了我们的思辨精神,或者说我们的教育,当然这里我们并不会过多的去批判教育的问题,因为存不存在问题需要你去思考。我们从小接触的教育是这样的,至少我是这样的:最重要的是学习,或者说考试拿高分,这是“绝对”“正确”的,因此如果考少了,你就有可能被老师或者父母揍一顿。同样,在父母的眼里“棍棒底下出孝子”可能是“绝对”“正确”的,因此才会揍我们。所以,为了避免皮肉之苦,就只能好好学习,同时不允许一切有碍我们学习的东西出现。学习本身“似乎”是“绝对”“正确”的,尤其是在一定的社会条件下,好好学习可能是一个我们实现自我实现的一个很好的途径。我只是认为,这不能成为我们不做一些事情的理由,看动画片“似乎”会让我们学习的时间变少,所以可能很多家长的做法就是不让平时看动画片。然而,在这样的情况下,我的学业也还是一般般 😂。

如果我们一昧的强调某些事情是“绝对”“正确”的,一些事情是“绝对”“错误”的,最终导致的就是我们会终将失去思辨的精神。对于个人的成长来说,在前期可能还不会发现有多大的影响,但当你脱离父母,学校等这些保护伞后,我认为思辨精神可以说是能够让我们理解生存的本质,生活的意义以及如何获得多彩人生的重要思想。

下图是我从 Birdbox StudioWildbeest 视频中截取的一段做成的动图。动画的内容比较简单,但却相当的讽刺:前面的角马坚持河里面的是鳄鱼,但后面的角马却“一再”地说是木头,无论是出于想验证自己的想法还是对自己想法的质疑,前面的角马最后被鳄鱼吃掉了,这时后面的角马认识到了这“真的”是鳄鱼,更讽刺的是再后面的角马却又说这是木头。

wildebeest

我感觉用这个小视频来比喻我们的问题还是很恰当的,一昧的以家长的姿态强调什么是“对的”,什么是“错的”,最终会让孩子失去思辨的精神。而且这种问题是会传递的,可能将来你的孩子对于他的孩子也会存在这种“不当”的教育,又来了,我所说的这种“不当”是不是“真”的“不当”,取决于你的思考和理解。

当下,随着父母的知识水平的不断提升,开始认识到单纯的自然科学学习已经不够了,又开始给孩子们报各种兴趣班,而且是强制报名让孩子去学习。我认为这是同样的问题,只不过是父母角度的“绝对”“正确”又多了一项,就是还必须得有个特长。对于孩子而言,父母毕竟走过了更多的人生道路,也遇见过更多的问题,有远比孩子丰富的阅历,但我认为更加合适的方式是去“引导”,去让孩子尝试不同的东西,同时也给孩子一个思考和选择的机会。

其实,我认为思辨精神其实是我们人与生俱来的,最明显的表现就是小孩子永远爱问十万个为什么?所以,我认为对待孩子的问题要有耐心,要不厌其烦的去回答,这样才不会让孩子的这种性格消失。而且这是个正向反馈的过程,问的多了,得到的解释多了,思考多了,才会在下一次提问前更多的思考,提出少但更具意义的问题,同时又不会失去这种思辨的精神。

我还没有小孩,所以说了一大堆孩子教育的问题可能会有很多不当。除了孩子的思辨教育,作为一个成年人,对于我们自己又该如何去做呢?对于 Andrew,面对两任女友的不满,又该如何去改变呢?

P.S. 关于思辨精神,推荐读过的一本不错的书,是一位台湾公民课教师 黃益中《思辨: 熱血教師的十堂公民課 》,我感觉于自己和于未来孩子的教育都很有帮助 。当然对于思辨,我认为《中庸》中的「博學之,審問之,慎思之,明辨之,篤行之」与其含义是相通的,所以我们老祖宗很早就认识到了思辨的重要性。

P.S. 这段里面很多引号,引起来说明这些是我的观点,之于你,请慎思明辨。

迈出改变的一步

该如何去做,答案就是迈出改变的一步。当然我们首先要认识到我们需要改变,这点其实不易,因为我们并没有接触到很好的思辨教育,至少我是这样的,在毕业后才意识到思辨的重要性。很难认识到自己需要改变,我认为有如下几个原因:

有时候我们会遇到不爽,但不爽也会很快过去,因为我们会说:好吧,其实整体来看,Life is not bad. 是的,就是这个 NOT BAD 会让我们习惯下去,不去改变。在 Andrew 参加的 Talk Show 中,如演讲者说的那样,作为 Model Minority (模范少数族裔),我们过的还不错。

我们有时候是很难发现自己的问题的,就像我们这群撸代码的一样,很难找到自己代码中所有的毛病,这也是测试工程师存在的理由。但在生活中,发现你的问题的人应该不少,但能够真诚的告诉你的我认为不会很多,这也就是为什么人生得一二知己足矣。Andrew 很幸运,两任女友都直白的和他说了真实的分手理由,没有搪塞。所以一个人说你有问题,你不一定真的有问题,但也需要反思,当说你有同一个问题的人多了,你就真的要反思了。炮弹落到同一个坑里面的概率很小,当真的都精准的打击到同一个地方时,那一定是这个地方的问题才让它成为众矢之的。

认识到了问题,但还是不想改变,理由是如果我这样做了,那 XXX 怎么办 (比如家庭,工作等等)?会给自己找很多很多“理所当然”“对”的理由,我就经常这样,但我在努力改变中。太多的顾虑其实只是借口,可能当你走下去的时候,会发现 XXX 根本不是问题,甚至会比当下更好。当然,也可能会真的发生一些问题,如果真的发生了,要么想办法再解决它,要么再放弃之前的抉择,我认为也还来得及。所以,也不用把年龄当作借口,since it’s nevery too late to do it.

在迈出这一步的路上,我做的并不好,但我已经认识到了,所以在努力改变中。简单而言,对于一个事情,我们可以怎么做?

THINK -> CHANGE -> IF 👍 THEN 😎 ELSE GOTO THINK AGAIN

基于 PyQt5/PySide2 和 QML 的跨平台 GUI 程序开发

2018-05-27 08:00:00

先聊聊写界面化程序的目的,在 B/S 结构软件盛行的今天,C/S 结构的软件还有人用吗?答案是肯定的,至少你想用 B/S 结构的软件的时候你得有个 C/S 结构的浏览器,对吧?这样说显得有点抬杠,当然,我认为最重要的还是“简单”,或者说“用户友好”。再 Geek 的人应该也喜欢有的时候偷懒,虽然我称不上 Geek,但也经常在黑框框中不用鼠标敲着各种代码,但是还是希望能够有些小工具只要能够点个几下就能帮忙干些事情的。至于对于更普通的用户而言,就应该更加希望能够用最“简单,清晰,明了”的方式“快速”的完成一项任务,有点像 Windows 用户把桌面上的快捷方式拖到回收站,然后和我说:好了,程序卸载了,我只能回答说:或许你该换个 MAC。

❗ 更新 ❗

SciHubEVA 最新版本已经采用 PySide2 进行改写,Windows 版本安装包构建工作迁移至 Inno Setup 6,更多变更请参见 CHANGELOG

跨平台 GUI 程序开发方案选型

所以,写个带界面的小工具就是把你的想法更好的服务自己和别人的一个好途径,那么问题来了,对于我这做算法的种业余编程选手,怎么搞定界面化应用呢?虽然是业余编程选手,也也一路从 Logo,Basic,VB,C/C++,Java,R,Python 等等走来,当然很多都是从入门到放弃,总之对于同时需要兼顾一定美感的我,总结了几种跨平台界面化的解决方案。

  1. JavaFX,基于 JVM,一次编译处处运行,配合 Material Design 风格的 JFoenix,应该是能写出很漂亮的界面的。
  2. Qt,一次编写处处编译,配合 Qt QuickQML,可以把前后端分离。原生 C++ 语言支持,同时有 Python 绑定,对于 Python 比较熟的同学相对友好。界面风格上在较新的 Qt Quick 中也支持了 Material Design 风格
  3. Electron,使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,很多优秀的应用都是用这个来搞的,例如:Visual Studio CodeHyper 等。

我不认为这 3 种方法孰优孰劣,因为毕竟我们的目的是快速的搞定一个漂亮的小工具,因此到底选哪个完全取决于个人对相关技术的熟悉程度。因此,对于我这个搞算法的,最终选择了 Qt 的 Python 绑定 PyQt。作为 R 的忠实用户,实在是没找到特别好的解决方案,只能找个借口说我们 R 就不是干这个用的……

环境配置

当然选择 PyQt 也是有些个人的倾向在里面的,写 C++ 的年代就用过 Qt,对于原理多少有些了解。不过针对 PyQt,以及其与 Qt Quick 和 QML 的结合使用在后面开发时发现相关文档比较少,只能一步一步地趟雷了。毕竟要做跨平台的 GUI 程序开发,因此本文会针对 macOS 和 Windows 两个系统做相关说明,Linux 系统由于发行版本太多就不做说明了,大部分情况应该和 macOS 类似。

Python 的版本选择了 3.5,因为在后面选择 3.6 时发现编译打包的时候会有些错误,没有细究,简单 Google 了此类问题,发现回退到 3.5 版本就没问题了,可能需要相关打包工具的更新才能有对 3.6 更好的支持。如果使用 Conda 建立虚拟环境,建议新建一个干净的 Python 3.5 的环境。

Qt 和 PyQt 均采用比较新的版本,版本号需大于 5.10。Qt 直接从官网下载安装即可,理论上不需要安装 Qt,因为 PyQt 中包含了运行时环境,安装 Qt 的目的是为了使用其可视化的 Qt Creator,设计界面的时候会比较方便。如果使用 Conda 建立 Python 虚拟环境,请使用 pip 安装 PyQt 的对应版本,Conda 中的 PyQt 的版本相对较低,一些新的 Qt 特性不支持。

PyInstaller 是一个用于打包 Python 代码到一个本地化可执行程序的工具,安装其最新版本即可:pip install PyInstaller

appdmg 是 macOS 下一个用于制作 DMG 镜像的工具,使用前先安装 Node.js,再通过 npm install -g appdmg 安装最新版即可。NSIS 是 Windows 下一个用于制作安装包的工具,NSIS 的一个问题是不支持 Unicode,因此对于包含中文字符的脚本需要以 GBK 编码格式保存。Unicode 版本的 NSIS 为 Unicode NSIS,不过 Unicode NSIS 已经长时间未更新,因此本文依旧将 NSIS 作为安装包制作工具。

界面设计

通过需求分析,整个工具最核心的两个界面为程序主界面和配置信息界面:

APP

程序主界面包含了待搜索的信息,保存的路径,相关的按钮和日志输出。

PREFERENCES

配置信息界面以配置项的分组不同分别包括通用,网络和代理等相关的配置信息更改。

整个界面设计采用了 Google 的 Material Design 风格,尤其是在没有 UI 支援的情况下,使用这个风格至少不会让你的应用太丑。在 PyQt 中,可以通过 多种方式 启用 Material Design 风格。

程序开发

本文以 Sci-Hub EVA 作为示例介绍 PyQt 的跨平台 GUI 程序开发。Sci-Hub EVA 是一个利用 Sci-Hub API 下载论文的界面化小工具,功能相对简单。首先介绍一下工程的目录:

docs\
images\
translations\
ui\
BUILDING.md
Info.plist
LICENSE
README.md
SciHubEVA.conf
SciHubEVA.cpp
SciHubEVA.dmg.json
SciHubEVA.nsi
SciHubEVA.pro
SciHubEVA.qrc
SciHubEVA.win.version
requirements.txt
scihub_add_scihub_url.py
scihub_api.py
scihub_conf.py
scihub_eva.py
scihub_preferences.py
scihub_resources.py
scihub_utils.py
version_updater.py

其中,docs 目录为项目的一些文档,images 目录为项目的相关图片文件,translations 目录为项目的 i18n 翻译文件,ui 目录为相关的界面文件 (QML 文件),Info.plist 为 macOS 程序信息文件,SciHubEVA.conf 为程序配置文件,SciHubEVA.cpp 为 Qt 生成的 C++ 主文件,SciHubEVA.dmg.json 为利用 appdmg 制作 DMG 镜像的配置文件,SciHubEVA.nsi 为利用 NSIS 制作 Windows 安装包的脚本文件,SciHubEVA.pro 为程序的 Qt 主项目文件,,SciHubEVA.qrc 为程序的资源文件,SciHubEVA.win.version 为打包 Windows 的版本信息文件,requirements.txt 为 Python 依赖包信息文件,scihu_*.py 为程序实现相关 Python 代码,version_updater.py 为版本更新的小工具。

下文中不会介绍具体的业务逻辑代码,而是对开发过程中的一些核心点和注意事项进行简单的介绍。

Python 与 QML 通信

首先,对于每一个界面 (QML 文件),我们都有一个与之对应 Python 文件 (除非该页面没有具体的业务逻辑,例如:ui\SciHubEVAAbout.qml 为关于页面,ui\SciHubEVAMenuBar.qml 为菜单栏),以主页面 (ui\SciHubEVA.qmlscihub_eva.py) 为例,我们为每个界面创建一个类,同时该类集成自 Qt 的一个基类:

class SciHubEVA(QObject):
    pass

Python 代码同界面交互的核心是通过 Qt 的 信号与槽,同样在 PyQt 中也是利用 相同的机制。简单的理解 PyQt 与 QML 的信号与槽,可以认为信号就是函数的定义就是函数的实现。同时,信号和槽往往会位于不同的地方,例如:信号定义在 Python 中,则对应的槽会在 QML 中,反之亦然,当然这并不是一定的。两者通过 connect() 函数连接起来,当触发一个信号时,槽就会接受到信号传递的参数,并执行槽里面相应的逻辑。

i18n

Qt 对于多语言支持比较完善,在 QML 中对于需要翻译的地方利用 qsTr() 函数处理待翻译的文本即可,例如:

Label {
    id: labelQuery
    text: qsTr("Query: ")
}

在 Python 代码中,对于继承自 QObject 的类,可以利用基类中的 tr() 函数处理待翻译的文本即可,例如:

self.tr('Saved PDF as: ')

同时将具有待翻译文本的文件加入到 SciHubEVA.pro 的主工程文件中,用于后续翻译处理:

lupdate_only {
SOURCES += \
    ui/SciHubEVA.qml \
    ui/SciHubEVAAbout.qml \
    ui/SciHubEVAMenuBar.qml \
    ui/SciHubEVAPreferences.qml \
    ui/SciHubEVAAddSciHubURL.qml \
    scihub_api.py
}

TRANSLATIONS += \
    translations/SciHubEVA_zh_CN.ts

因为 Python 代码中也有需要翻译的文件,因此我们需要运行如下命令生成翻译的源文件:

lupdate SciHubEVA.pro
pylupdate5 SciHubEVA.pro

这样在 translations 目录即可生成待翻译的源文件 (ts 文件),利用 Qt 自带的 Liguist 可以对其进行编辑,翻译并保存后,利用如下命令生成翻译的结果文件:

lrelease SciHubEVA.pro

translations 目录即可生成待翻译的结果文件 (qm 文件)。

资源文件

在 GUI 编程中,我们不可避免的会使用到各种各样的资源,例如:图片,音频,字体等等。Qt 中提供了一种资源管理方案,可以在不同场景下使用 (Python 和 QML 中均可)。SciHubEVA.qrc 定义了所有使用到的资源:

<RCC>
    <qresource prefix="/">
        <file>ui/SciHubEVA.qml</file>
        <file>ui/SciHubEVAMenuBar.qml</file>
        <file>ui/SciHubEVAAbout.qml</file>
        <file>ui/SciHubEVAPreferences.qml</file>
        <file>ui/SciHubEVAAddSciHubURL.qml</file>
        <file>images/about.png</file>
    </qresource>
</RCC>

在 QML 中使用示例如下:

Image {
    id: imageAboutLogo
    source: "qrc:/images/about.png"
}

在 Python 中使用示例如下:

self._engine = QQmlApplicationEngine()
self._engine.load('qrc:/ui/SciHubEVA.qml')

使用 qrc 文件管理资源文件的一个好处就是不需要担心各种相对路径和绝对路径带来的找不到文件的错误,但同时一个缺点是当资源文件更新后,需要运行 pyrcc5 SciHubEVA.qrc -o scihub_resources.py 更新资源,同时还需要在主程序代码中引入生成的 Python 资源代码。

界面线程分离

写 GUI 应用的一个重要问题就是界面线程的分离,需要把耗时的业务逻辑摘出来,单独作为一个线程运行,这样才不会造成界面的“假死”情况。scihub_api.py 中的 SciHubAPI 作为下载文章的主类,下载过程相对耗时。因为其既需要 Qt 中的 tr() 函数,也需要线程,通过 Python 的多继承,SciHubAPI 类构造如下:

class SciHubAPI(QObject, threading.Thread):
    pass

编译打包

PyInstaller 是一个用于打包 Python 代码到一个本地化可执行程序的工具,详细的使用方法请参见官方文档。同样,我们在此仅说明打包过程中遇到的一些问题。

macOS

macOS 下的编译打包命令如下:

# 清理相关目录和文件
rm -rf build
rm -rf dist
rm -f SciHubEVA.spec

# 重新生成资源文件
rm -f scihub_resources.py
pyrcc5 SciHubEVA.qrc -o scihub_resources.py

# 编译打包
pyinstaller -w scihub_eva.py \
  --hidden-import "PyQt5.Qt" \
  --hidden-import "PyQt5.QtQuick" \
  --add-data "LICENSE:." \
  --add-data "SciHubEVA.conf:." \
  --add-data "images/SciHubEVA.png:images" \
  --add-data "translations/SciHubEVA_zh_CN.qm:translations" \
  --name "SciHubEVA" \
  --icon "images/SciHubEVA.icns"

# 拷贝程序信息
cp Info.plist dist/SciHubEVA.app/Contents

编译打包过程中的 --hidden-import 参数是因为我们使用了 Qt Quick 和 QML 相关框架,但是在 Python 代码中我们并没有显式的引入这两个包,因此我们需要告知 PyInstaller 我们使用了这两个包,这样 PyInstaller 才会把相关的动态链接库拷贝到打包的程序中。

打包好的程序 SciEvaHub.app 会保存在 dist 目录中。由于目前无论是 macOS 还是 Windows 系统,高分辨率已经比较常见,为了适应高分辨率,我们需要在代码中添加相应的支持,在入口 Python 文件中,我们需要在头部添加如下信息:

if hasattr(Qt, 'AA_EnableHighDpiScaling'):
    QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
    QGuiApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

同时针对 macOS 系统,我们需要在 Info.plist 中添加如下信息以支持高分辨率:

<key>NSHighResolutionCapable</key>
<string>True</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<string>True</string>

Info.plist 中的其他信息针对性进行修改即可,最后将其拷贝到打包好的程序中。

Windows

Windows 下的编译打包命令如下:

rem 清理相关目录和文件
rd /s /Q build
rd /s /Q dist
del /Q SciHubEVA.spec

rem 重新生成资源文件
del /Q scihub_resources.py
pyrcc5 SciHubEVA.qrc -o scihub_resources.py

rem 编译打包
pyinstaller -w scihub_eva.py ^
  --hidden-import "PyQt5.Qt" ^
  --hidden-import "PyQt5.QtQuick" ^
  --add-data "LICENSE;." ^
  --add-data "SciHubEVA.conf;." ^
  --add-data "images/SciHubEVA.png;images" ^
  --add-data "translations/SciHubEVA_zh_CN.qm;translations" ^
  --name "SciHubEVA" ^
  --icon "images/SciHubEVA.ico" ^
  --version-file "SciHubEVA.win.version"

编译打包过程中的 --version-file 参数是 Windows 程序的相关版本信息,具体请参见微软的 Version Information Structures

打包好的程序会在 dist\SciHubEVA 目录中,该目录还包含了所有运行时所需的文件。

安装包制作

macOS

macOS 下我们使用 appdmg 工具将编译打包好的程序制作成 DMG 镜像文件。DMG 镜像文件可以对原始的程序进行压缩,便于分发。appdmg 通过一个 JSON 文件控制 DMG 镜像的制作,详细的 JSON 格式和相关参数请参见 官方文档,Sci-Hub EVA 的 DMG 制作 JSON 文件如下:

{
    "title": "Sci-Hub EVA",
    "icon": "images/SciHubEVA.icns",
    "icon-size": 100,
    "background": "images/SciHubEVA-dmg-backgroud.png",
    "format": "UDZO",
    "window": {
        "size": {
            "width": 600,
            "height": 400
        }
    },
    "contents": [
        {
            "x": 100,
            "y": 150,
            "type": "file",
            "path": "dist/SciHubEVA.app"
        },
        {
            "x": 300,
            "y": 150,
            "type": "link",
            "path": "/Applications"
        }
    ]
}

打包好后的 DMG 镜像效果如下:

DMG

Windows

Windows 下我们使用 NSIS 构建安装包,同样 NSIS 也支持多语言安装包构建,但请注意,NSIS 程序本身并不支持 Unicode,因此 NSIS 安装包的脚本需使用 GBK 编码保存。构建好的安装包的安装界面如下:

NSIS

整个 Sci-Hub EVA 的编译打包和安装包制作过程请参见 构建说明文档

流形学习 (Manifold Learning)

2018-03-16 08:00:00

降维

在之前的 博客 中,我们曾经介绍过 PCA 方法及其降维的作用。在原始数据中各个特征之间存在着一定的信息冗余,随着特征的不断增加就容易出现“维数灾难”的问题,因此降维的目的就是在尽可能多的保留原始信息的同时减少数据的维度。一般情况下我们将降维方法分为:线性降维方法非线性降维方法,线性降维方法的典型算法有:

非线性降维方法中在此我们仅列举一些基于流行学习的算法:

在现实数据中,很多情况数据是无法通过线性的方法进行降维表示的,因此就需要非线性的降维算法出马了。

流形

在调研流形相关概念时,发现要想深一步的理解这些概念还是需要详细的了解微分几何相关的内容,鉴于本文的目的主要是介绍流形学习 (主要是降维角度) 的相关内容,因此我们对流形仅做一些粗略的介绍。

流形”是英文单词 Manifold 的中文译名,它源于德文术语 Mannigfaltigkeit,最早出现在 Riemann 1851 年的博士论文中,用来表示某种属性所能取到的所有值 7。为了更好的理解流形,我们先引入几个概念:

拓扑结构 (拓扑) 任意给定集合 $X$ 上的一个拓扑结构 (拓扑)$X$ 的某些特定子集组成的集合 $\tau \subset 2^X$,其中那些特定子集称为 $\tau$ 所声明的开集,同时满足如下性质:

  1. 空集和全集是开集,即 $\varnothing, X \in \tau$
  2. 任意多个开集的并集是开集
  3. 有限多个开集的交集是开集

拓扑空间 指定了拓扑结构的集合就称为一个拓扑空间

上图中给出了一些拓扑空间的示例,其中左侧 4 个为正确示例,右侧 2 个为错误示例。右上角的缺少了 {2} 和 {3} 的并集 {2, 3},右下角的缺少了 {1, 2} 和 {2, 3} 的交集 {2}。

同胚 两个拓扑空间 $\left(X, \tau_X\right)$$\left(Y, \tau_Y\right)$ 之间的函数 $f: X \to Y$ 称为同胚,如果它具有下列性质:

  1. $f$ 是双射 (单射和满射)
  2. $f$ 是连续的
  3. 反函数 $f^{−1}$ 也是连续的 ($f$ 是开映射)

如果拓扑空间是一个几何物体,同胚就是把物体连续延展和弯曲,使其成为一个新的物体。因此,正方形和圆是同胚的,但球面和环面就不是。用一幅图形象的理解同胚,例如下图所示的咖啡杯甜甜圈 8

最后我们回过头来解释到底什么是流形?流形并不是一个“形状”,而是一个“空间” 9。最容易定义的流形是拓扑流形,它局部看起来象一些“普通”的欧几里得空间 $\mathbb{R}^n$,一个拓扑流形是一个局部同胚于一个欧几里得空间的拓扑空间。根据 Whitney 嵌入理论 10,任何一个流形都可以嵌入到高维的欧氏空间中。例如,地球的表面可以理解为一个嵌入 3 维空间的 2 维流形,其局部同胚于 2 维的欧式空间,对于一个球体的表面,用极坐标的形式可以表示为

$$ \begin{equation} \begin{split} x &= r \sin \theta \cos \phi \\ y &= r \sin \theta \sin \phi \\ z &= r \cos \theta \end{split} \end{equation} $$

也就是说其 3 个维度实际上是由 2 个变量控制的。

流形学习

假设 $Y$ 为一个欧式空间 $\mathbb{R}^d$ 的一个 $d$ 维流形,$f: Y \to \mathbb{R}^D$ 为一个光滑嵌入,对于 $D > d$,流形学习的目的就是根据空间 $\mathbb{R}^D$ 中的观测数据 $\{x_i\}$ 重构 $Y$$f$ 的过程。隐含数据 $\{y_i\}$$Y$ 随机生成,通过光滑嵌入 $f$ 生成观测数据,即 $\{x_i = f\left(y_i\right)\}$,所以我们可以将流形学习的问题看做是对于一个给定的观测数据一个生成模型的反向过程 11

在介绍具体的流形学习算法前,我们先引入几个 3 维数据用于解释后续的具体算法

第一个为瑞士卷 (Swiss Roll),其形状和我们日常生活中的瑞士卷相似;第二个为 S 形曲线 (S Curve);第三个为一个被切断的球面 (Severed Sphere)

MDS

多尺度变换 (MDS, Multi-Dimensional Scaling) 3 是一种通过保留样本在高维空间中的不相似性 (Dissimilarity) 降低数据维度的方法,在这里不相似性可以理解为样本之间的距离。因此,根据距离的度量方式不同可以将其分为度量型 (metric) MDS 和 非度量型 (non-metric) MDS。度量型 MDS 通过计算不同样本之间距离的度量值进行降维,而非度量型则仅考虑距离的排序信息,在此我们仅对度量型 MDS 做简单介绍。

MDS 的目标是保留样本在高维空间中的不相似性,假设 $x \in \mathbb{R}^D, x' \in \mathbb{R}^d, D > d$,则 MDS 的目标函数可以写为

$$ \min \sum_{i, j} \lvert dist \left(x_i, x_j\right) - dist \left(x'_i, x'_j\right) \rvert $$

则,度量型 MDS 的算法的步骤如下:

  1. 计算样本的距离矩阵 $\boldsymbol{D} = \left[d_{i, j}\right] = \left[dist \left(x_i, x_j\right)\right]$
  2. 构造矩阵 $\boldsymbol{A} = \left[a_{i, j}\right] = \left[- \dfrac{1}{2} d_{i, j}^2\right]$
  3. 通过中心矫正的方法构造矩阵 $\boldsymbol{B} = \boldsymbol{J} \boldsymbol{D} \boldsymbol{J}, \boldsymbol{J} = \boldsymbol{I} - \dfrac{1}{n} \boldsymbol{O}$,其中 $\boldsymbol{I}$$n \times n$ 的单位阵,$\boldsymbol{O}$$n \times n$ 的值均为 $1$ 的矩阵。
  4. 计算矩阵 $\boldsymbol{B}$ 的特征向量 $e_1, e_2, ..., e_m$ 及其对应的特征值 $\lambda_1, \lambda_2, ..., \lambda_m$
  5. 确定维度 $k$,重构数据 $\boldsymbol{X}' = \boldsymbol{E}_k \boldsymbol{\Lambda}_k^{1/2}$,其中 $\boldsymbol{\Lambda}_k$ 为前 $k$ 个值最大的 $k$ 个特征值构成的对角矩阵,$\boldsymbol{E}_k$ 是对应的 $k$ 个特征向量构成的矩阵。

在《多元统计分析》12一书中证明了,$\boldsymbol{X}$$k$ 维主坐标正好是将 $\boldsymbol{X}$ 中心化后 $n$ 个样本的前 $k$ 个主成份的值,由此可见 MDS 和 PCA 的作用是类似的。

我们利用中国省会的地理位置给出 MDS 的一个示例,首先我们获取中国省会共 34 个点的坐标,其次我们计算两两之间的距离,我们仅利用距离信息利用 MDS 还原出 2 维空间中的坐标,可视化结果如下所示

其中,黑色的点为省会的真实位置,蓝色的点为利用距离矩阵和 MDS 还原出来的位置,为了绘制还原出的位置我们对 MDS 的结果做出了适当的翻转和变换。从结果中不难看出,尽管每个点的坐标相比真实坐标都有一定的偏离,但是其很好的保持了相对距离,这也正是 MDS 算法的要求。

ISOMAP

对于一些非线性的流形,如果使用线性的降维方法得到的效果就不尽人意了,例如上文中提到的瑞士卷。在 ISOMAP 中,我们首先引入一个测地线的概念,在距离度量定义时,测地线可以定义为空间中两点的局域最短路径。形象的,在一个球面上,两点之间的测地线就是过这两个点的大圆的弧线

那么,对于非线性流形,ISOMAP 则是通过构建邻接图,利用图上的最短距离来近似测地线。在构造邻接图时,我们使用最近邻算法,对于一个点 $x_i$ 连接距离其最近的 $k$ 个点,两点之间的距离我们则一般使用传统的欧式距离。则任意两点之间的测地线距离则可以利用构建的邻接图上的最短路径进行估计,图上的最短路问题我们可以通过 Dijkstra 或 Floyd-Warshall 算法计算。得到样本的距离矩阵后,ISOMAP 算法则使用 MDS 方法计算得到低维空间的座标映射。

上图中,我们给出了利用 ISOMAP 对瑞士卷降至 2 维的一个格式化过程。第一幅图中,我们标注了 2 个蓝色的点,其中蓝色的直线为这 2 个点在三维空间中的欧式距离。第二幅图中,同样是相同的两个点,我们首先利用最近邻算法 ($k = 10$) 将瑞士卷所有的点连接为一个邻接图,其中红色的路径为这 2 个点在邻接图上的最短路。第三幅图是通过 ISOMAP 算法降维至 2 维的结果,其中蓝色的直线是这两个点在 2 维空间中的欧式距离,红色的路径是 3 维最短路在 2 维结果中的连线,可以看出两者是很相近的。

LLE

局部线性嵌入 (LLE, Locally Linear Embedding) 5,从这个名称上我们不难看出其不同与 ISOMAP 那种通过都建邻接图保留全局结构的,而是从局部结构出发对数据进行降维。在 LLE 方法中,主要有如下的基本假设:

基于上面的假设,LLE 算法的流程如下:

  1. 对于点 $X_i$,计算距离其最近的 $k$ 个点,$X_j, j \in N_i$
  2. 计算权重 $W_{ij}$ 是的能够通过点 $X_i$ 的邻居节点最优的重构该点,即最小化 $$ \epsilon \left(W\right) = \sum_i \left\lVert X_i - \sum_j W_{ij} X_j \right\rVert ^2 $$
  3. 通过权重 $W_{ij}$ 计算 $X$ 的低维最优重构 $Y$,即最小化 $$ \phi \left(Y\right) = \sum_i \left\lVert Y_i - \sum_j W_{ij} Y_j \right\rVert ^2 $$

具体上述问题的优化求解过程在此就不在详细描述。针对 LLE 算法,后续很多人从不同方面对其进行了改进:

  1. Hessian LLE 13 在局部中不再考虑局部的线性关系,而是保持局部的 Hessian 矩阵的二次型的关系。
  2. Modified LLE 14 则是修改了寻找最临近的 $k$ 个样本的方案,其在寻找 $k$ 近邻时希望找到的近邻尽量分布在样本的各个方向,而不是集中在一侧。
  3. LTSA (Local Tangent Space Alignment) 15 则是除了保留了局部的几何性质,同时使用的一个从局部几何到整体性质过渡的 alignment 方法,因此可以理解为是一个局部和整体的组合。

LE

LE (Laplacian Eigenmap) 6 的基本思想是认为在高维空间中距离近的点映射到低维空间中后其位置也相距很近。LE 从这个思想出发,最终将问题转化为求解图拉普拉斯算子的广义特征值问题,具体的一些证明不在这里详细展开说明,具体请参见原文,下面仅给出 LE 算法的流程:

  1. 构建邻接图。
  2. 构建邻接矩阵 $W$,构建邻接矩阵有两种方法:对于点 $i$ 和点 $j$ 相连,如果利用 Hear Kernel (参数 $t \in \mathbb{R}$),则令 $W_{ij} = \exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{t}\right)$;如果使用简介方案,则令 $W_{ij} = 1$,对于不相连的点,则令 $W_{ij} = 0$
  3. 进行特征映射,通过上面构造的图 $G$,计算如下广义特征值和特征向量 $$ L f = \lambda D f $$ 其中 $D$ 是一个对角矩阵,$D_{ii} = \sum_{j} W_{ji}$$L = D - W$ 即为拉普拉斯矩阵。对于上式的解 $f_0, .., f_{k-1}$ 为根据特征值从小到大的排序,其中 $0 = \lambda_0 \leq \lambda_1 \leq ... \leq \lambda_{k-1}$,则降维至 $d$ 维的后的特征即为 $\left(f_1, f_2, ..., f_d\right)$

SNE 和 t-SNE

SNE

SNE (Stochastic Neighbor Embedding) 16 是由 Hinton 等人提出的一种降维算法,其方法的基本假设如下:

  1. 对象之间的相似度可以用概率进行表示,即:相似的对象有更高的概率被同时选择,不相似的对象有较低的概率被同时选择。
  2. 在高维空间中构建的这种概率分布应该尽可能的同低维空间中的概率分布相似。

对于两个点 $x_i, x_j$,假设 $x_i$ 以条件概率 $p_{j∣i}$ 选择 $x_j$ 作为它的邻近点,因此如果两者距离更近 (更相似),则概率值越大,反之概率值越小,则我们定义 $p_{j∣i}$ 如下:

$$ p_{j∣i} = \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma_i^2}\right)}{\sum_{k \neq i} \exp \left(\dfrac{- \left\lVert x_i - x_k \right\rVert ^ 2}{2 \sigma_i^2}\right)} $$

其中,$\sigma_i$ 为参数,同时我们设置 $p_{i∣i} = 0$,因为我们仅需衡量不同对象之间的相似度。

类似的,根据 SNE 的基本思想,当数据被映射到低维空间中后,其概率分布应同高维空间中的分布尽可能的相似,假设点 $x_i, x_j$ 在低维空间中的映射点为 $y_i, y_j$,则在低维空间中的条件概率 $q_{j∣i}$ 定义为:

$$ q_{j∣i} = \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq i} \exp \left(- \left\lVert y_i - y_k \right\rVert ^ 2\right)} $$

同样,我们设置 $q_{i∣i} = 0$。从 SNE 的基本假设出发,我们的目的是使得数据在高维空间中的条件概率尽可能的和其在低维空间中的条件概率相同,因此对于全部点样本点而言,就是保证高维空间的概率分布 $P_i$ 和低维空间的概率分布 $Q_i$ 尽量形同。在这里我们利用 KL 散度衡量这两个概率分布的差异,则 SNE 的损失函数可以写为:

$$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{j∣i} \log \dfrac{p_{j∣i}}{q_{j∣i}} $$

因为 KL 散度具有不对称性可知,当在原始空间中两点距离较远而降维后的空间中距离较近 (即,$q_{j|i} < p_{j|i}$) 时,会产生较大的 cost,相反则会产生较小的 cost。正是这种不对称性的损失函数导致了 SNE 算法更加关注局部结构,相比忽略了全局结构。

上文中,对于不同的点,$\sigma_i$ 具有不同的值,SNE 算法利用困惑度 (Perplexity) 对其进行优化寻找一个最佳的 $\sigma$,对于一个随机变量 $P_i$,困惑度定义如下:

$$ Perp \left(P_i\right) = 2^{H \left(P_i\right)} $$

其中,$H \left(P_i\right) = \sum_{j} p_{j|i} \log_2 p_{j|i}$ 表示 $P_i$ 的熵。困惑度可以解释为一个点附近的有效近邻点个数。SNE 对困惑度的调整比较有鲁棒性,通常选择 5-50 之间,给定之后,使用二分搜索的方式寻找合适的 $\sigma$

SNE 的损失函数对 $y_i$ 求梯度后,可得:

$$ \dfrac{\delta C}{\delta y_i} = 2 \sum_j \left(p_{j|i} - q_{j|i} + p_{i|j} - q_{i|j}\right) \left(y_i - y_j\right) $$

t-SNE

SNE 为我们提供了一种很好的降维方法,但是其本身也存在一定的问题,主要有如下两点:

针对这两个问题,Maaten 等人又提出了 t-SNE 算法对其进行优化 17

针对不对称问题,Maaten 采用的方法是用联合概率分布来替代条件概率分布。高维控件中的联合概率分布为 $P$,低维空间中的联合概率分布为 $Q$,则对于任意的 $i, j$,有 $p_{ij} = p_{ji}, q_{ij} = q_{ji}$,联合概率定义为:

$$ \begin{align} p_{ij} &= \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma^2}\right)}{\sum_{k \neq l} \exp \left(\dfrac{- \left\lVert x_k - x_l \right\rVert ^ 2}{2 \sigma^2}\right)} \\ q_{ij} &= \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq l} \exp \left(- \left\lVert y_k - y_l \right\rVert ^ 2\right)} \end{align} $$

虽然这样保证了对称性,但是对于异常的情况,例如数据点 $x_i$ 在距离群簇较远,则 $\lVert x_i − x_j \rVert ^ 2$ 的值会很大,而 $p_{ij}$ 会相应变得非常小,也就是说 $x_i$ 的位置很远这件事情对损失函数影响很小 (惩罚过小),那这个点在低维空间中将无法从其他点中区分出来。因此 Maaten 提出了对称的条件概率来重新定义上述联合概率 $p_{ij}$ ,对于数量为 $n$ 的数据点,新的概率公式是:

$$ p_{ij} = \dfrac{p_{j|i} + p_{i|j}}{2n} $$

则损失函数更新为:

$$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{ij} \log \dfrac{p_{ij}}{q_{ij}} $$

梯度更新为:

$$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) $$

拥挤问题 (Crowding) 就是从高维空间映射到低维空间后,不同类别的簇容易挤在一起,不能很好的地区分开。t-SNE 则是利用了 t 分布重新定义 $q_{ij}$,t 分布具有长尾特性,相比于高斯分布,其在尾部趋向于 0 的速度更慢,对比如图所示:

利用 t 分布重新定义的 $q_{ij}$ 为:

$$ q_{ij} = \dfrac{\left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1}}{\sum_{k \neq l} \left(1 + \lVert y_k - y_l \rVert ^ 2\right) ^ {-1}} $$

梯度更新为:

$$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) \left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1} $$

利用 t-SNE 对 MNIST 数据集进行降维可视化结果如下:

方法比较

针对上述的若干算法,我们简单列举一下每个算法的优缺点

方法 优点 缺点
Isomap 1. 保持流形的全局几何结构
2. 适用于学习内部平坦的低维流形
1. 对于数据量较大的情况,计算效率过低
2. 不适于学习有较大内在曲率的流形
LLE 1. 可以学习任意维的局部线性的低维流形
2. 归结为稀疏矩阵特征值计算,计算复杂度相对较小
1. 所学习的流形只能是不闭合的
2. 要求样本在流形上是稠密采样的
3.对样本中的噪声和邻域参数比较敏感
LE 1. 是局部非线性方法,与谱图理论有很紧密的联系
2. 通过求解稀疏矩阵的特征值问题解析地求出整体最优解,效率非常高
3. 使原空间中离得很近的点在低维空间也离得很近,可以用于聚类
1. 对算法参数和数据采样密度较敏感
2. 不能有效保持流形的全局几何结构
SNE, t-SNE 1. 非线性降维效果相较上述方法较好 1. 大规模高维数据时,效率显著降低
2. 参数对不同数据集较为敏感

对于瑞士卷 (Swiss Roll)S 形曲线 (S Curve)切断的球面 (Severed Sphere),我们利用不同的流形算法对其进行降维,可视化的对比结果如下面 3 张图所示,图中同时标注了算法的运行时间,实现主要参照了 scikit-learn 关于流形学习算法的比较 18

文中相关图片绘制实现详见代码,本文部分内容参考了流形学习专题介绍 19流形学习 20Chrispher 21 的博客和 bingo 22 的博客。


  1. Jolliffe, Ian T. “Principal component analysis and factor analysis.” Principal component analysis. Springer, New York, NY, 1986. 115-128. ↩︎

  2. Balakrishnama, Suresh, and Aravind Ganapathiraju. “Linear discriminant analysis-a brief tutorial.” Institute for Signal and information Processing 18 (1998): 1-8. ↩︎

  3. Cox, Trevor F., and Michael AA Cox. Multidimensional scaling. CRC press, 2000. ↩︎ ↩︎

  4. Tenenbaum, Joshua B., Vin De Silva, and John C. Langford. “A global geometric framework for nonlinear dimensionality reduction.” Science 290.5500 (2000): 2319-2323. ↩︎

  5. Roweis, Sam T., and Lawrence K. Saul. “Nonlinear dimensionality reduction by locally linear embedding.” Science 290.5500 (2000): 2323-2326. ↩︎ ↩︎

  6. Belkin, Mikhail, and Partha Niyogi. “Laplacian eigenmaps for dimensionality reduction and data representation.” Neural computation 15.6 (2003): 1373-1396. ↩︎ ↩︎

  7. 梅加强. 流形与几何初步 ↩︎

  8. https://zh.wikipedia.org/zh-hans/流形 ↩︎

  9. pluskid. 浅谈流形学习 ↩︎

  10. https://en.wikipedia.org/wiki/Whitney_embedding_theorem ↩︎

  11. Silva, Vin D., and Joshua B. Tenenbaum. “Global versus local methods in nonlinear dimensionality reduction.” Advances in neural information processing systems. 2003. ↩︎

  12. 何晓群. 多元统计分析 ↩︎

  13. Donoho, David L., and Carrie Grimes. “Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data.” Proceedings of the National Academy of Sciences 100.10 (2003): 5591-5596. ↩︎

  14. Zhang, Zhenyue, and Jing Wang. “MLLE: Modified locally linear embedding using multiple weights.” Advances in neural information processing systems. 2007. ↩︎

  15. Zhang, Zhenyue, and Hongyuan Zha. “Principal manifolds and nonlinear dimensionality reduction via tangent space alignment.” SIAM journal on scientific computing 26.1 (2004): 313-338. ↩︎

  16. Hinton, Geoffrey E., and Sam T. Roweis. “Stochastic neighbor embedding.” Advances in neural information processing systems. 2003. ↩︎

  17. Maaten, Laurens van der, and Geoffrey Hinton. “Visualizing data using t-SNE.” Journal of machine learning research 9.Nov (2008): 2579-2605. ↩︎

  18. http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html ↩︎

  19. 王瑞平. 流形学习专题介绍 ↩︎

  20. 何晓飞. 流形学习 ↩︎

  21. http://www.datakit.cn/blog/2017/02/05/t_sne_full.html ↩︎

  22. http://bindog.github.io/blog/2016/06/04/from-sne-to-tsne-to-largevis/ ↩︎

深度学习优化算法 (Optimization Methods for Deeplearning)

2018-02-24 08:00:00

在构建神经网络模型的时候,除了网络结构设计以外,选取合适的优化算法也对网络起着至关重要的作用,本文将对神经网络中常用的优化算法进行简单的介绍和对比,本文部分参考了 Ruder 的关于梯度下降优化算法一文 1。首先,我们对下文中使用的符号进行同意说明:网络中的参数同一表示为 $\theta$,网络的假设函数为 $h_{\boldsymbol{\theta}}\left(\boldsymbol{x}\right)$,网络的损失函数为 $J\left(\boldsymbol{\theta}\right)$,学习率为 $\alpha$,假设训练数据中共包含 $m$ 个样本,网络参数个数为 $n$

梯度下降

在梯度下降算法中,常用的主要包含 3 种不同的形式,分别是批量梯度下降 (Batch Gradient Descent, BGD),随机梯度下降 (Stochastic Gradient Descent, SGD) 和小批量梯度下降 (Mini-Batch Gradient Descent, MBGD)。一般情况下,我们在谈论梯度下降时,更多的是指小批量梯度下降。

BGD

BGD 为梯度下降算法中最基础的一个算法,其损失函数定义如下:

$$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2m} \sum_{i=1}^{m}{\left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)} $$

针对任意参数 $\theta_j$ 我们可以求得其梯度为:

$$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$

之后,对于任意参数 $\theta_j$ 我们按照其负梯度方向进行更新:

$$ \theta_j = \theta_j + \alpha \left[\dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}}\right] $$

整个算法流程可以表示如下:

\begin{algorithm}
\caption{BGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \FOR{$j = 1, 2, ..., n$}
        \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2m} \sum_{i=1}^{m}{\left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)}$
        \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}

从上述算法流程中我们可以看到,BGD 算法每次计算梯度都使用了整个训练集,也就是说对于给定的一个初始点,其每一步的更新都是沿着全局梯度最大的负方向。但这同样是其问题,当 $m$ 太大时,整个算法的计算开销就很高了。

SGD

SGD 相比于 BGD,其最主要的区别就在于计算梯度时不再利用整个数据集,而是针对单个样本计算梯度并更新权重,因此,其损失函数定义如下:

$$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2} \left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right) $$

整个算法流程可以表示如下:

\begin{algorithm}
\caption{SGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \STATE Randomly shuffle dataset
    \FOR{$i = 1, 2, ..., m$}
        \FOR{$j = 1, 2, ..., n$}
            \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2} \left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)$
            \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
        \ENDFOR
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}

SGD 相比于 BGD 具有训练速度快的优势,但同时由于权重改变的方向并不是全局梯度最大的负方向,甚至相反,因此不能够保证每次损失函数都会减小。

MBGD

针对 BGD 和 SGD 的问题,MBGD 则是一个折中的方案,在每次更新参数时,MBGD 会选取 $b$ 个样本计算的梯度,设第 $k$ 批中数据的下标的集合为 $B_k$,则其损失函数定义如下:

$$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{|B_k|} \sum_{i \in B_k}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$

整个算法流程可以表示如下:

\begin{algorithm}
\caption{MBGD 算法}
\begin{algorithmic}
\FOR{$epoch = 1, 2, ...$}
    \FOR{$k = 1, 2, ..., m / b$}
        \FOR{$j = 1, 2, ..., n$}
            \STATE $J \left(\boldsymbol{\theta}\right) = \dfrac{1}{|B_k|} \sum_{i \in B_k}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}}$
            \STATE $\theta_j = \theta_j - \alpha \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j}$
        \ENDFOR
    \ENDFOR
\ENDFOR
\end{algorithmic}
\end{algorithm}

Momentum

当梯度沿着一个方向要明显比其他方向陡峭,我们可以形象的称之为峡谷形梯度,这种情况多位于局部最优点附近。在这种情况下,SGD 通常会摇摆着通过峡谷的斜坡,这就导致了其到达局部最优值的速度过慢。因此,针对这种情况,Momentum 2 方法提供了一种解决方案。针对原始的 SGD 算法,参数每 $t$ 步的变化量可以表示为

$$ \boldsymbol{v}_t = - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) $$

Momentum 算法则在其变化量中添加了一个动量分量,即

$$ \begin{equation} \begin{split} \boldsymbol{v}_t &= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$

对于添加的动量项,当第 $t$ 步和第 $t-1$ 步的梯度方向相同时,$\boldsymbol{\theta}$ 则以更快的速度更新;当第 $t$ 步和第 $t-1$ 步的梯度方向相反时,$\boldsymbol{\theta}$ 则以较慢的速度更新。利用 SGD 和 Momentum 两种方法,在峡谷行的二维梯度上更新参数的示意图如下所示

NAG

NAG (Nesterov Accelerated Gradient) 3 是一种 Momentum 算法的变种,其核心思想会利用“下一步的梯度”确定“这一步的梯度”,当然这里“下一步的梯度”并非真正的下一步的梯度,而是指仅根据动量项更新后位置的梯度。Sutskever 4 给出了一种更新参数的方法:

$$ \begin{equation} \begin{split} \boldsymbol{v}_t &= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t + \gamma \boldsymbol{v}_{t-1}\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$

针对 Momentum 和 NAG 两种不同的方法,其更新权重的差异如下图所示:

AdaGrad

AdaGrad 5 是一种具有自适应学习率的的方法,其对于低频特征的参数选择更大的更新量,对于高频特征的参数选择更小的更新量。因此,AdaGrad算法更加适用于处理稀疏数据。Pennington 等则利用该方法训练 GloVe 6 词向量,因为对于出现次数较少的词应当获得更大的参数更新。

因为每个参数的学习速率不再一样,则在 $t$ 时刻第 $i$ 个参数的变化为

$$ \theta_{t, i} = \theta_{t-1, i} - \alpha \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$

根据 AdaGrad 方法的更新方式,我们对学习率做出如下变化

$$ \theta_{t, i} = \theta_{t-1, i} - \dfrac{\alpha}{\sqrt{G_{t, i}} + \epsilon} \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$

其中,$G_t$ 表示截止到 $t$ 时刻梯度的平方和;$\epsilon$ 为平滑项,防止除数为零,一般设置为 $10^{-8}$。AdaGrad 最大的优势就在于其能够自动调节每个参数的学习率。

Adadelta

上文中 AdaGrad 算法存在一个缺点,即其用于调节学习率的分母中包含的是一个梯度的平方累加项,随着训练的不断进行,这个值将会越来越大,也就是说学习率将会越来越小,最终导致模型不会再学习到任何知识。Adadelta 7 方法针对 AdaGrad 的这个问题,做出了进一步改进,其不再计算历史所以梯度的平方和,而是使用一个固定长度 $w$ 的滑动窗口内的梯度。

因为存储 $w$ 的梯度平方并不高效,Adadelta 采用了一种递归的方式进行计算,定义 $t$ 时刻梯度平方的均值为

$$ E \left[g^2\right]_t = \rho E \left[g^2\right]_{t-1} + \left(1 - \rho\right) g^2_{t} $$

其中,$g_t$ 表示 $t$ 时刻的梯度;$\rho$ 为一个衰减项,类似于 Momentum 中的衰减项。在更新参数过程中我们需要其平方根,即

$$ \text{RMS} \left[g\right]_t = \sqrt{E \left[g^2\right]_t + \epsilon} $$

则参数的更新量为

$$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$

除此之外,作者还考虑到上述更新中更新量和参数的假设单位不一致的情况,在上述更新公式中添加了一个关于参数的衰减项

$$ \text{RMS} \left[\Delta \theta\right]_t = \sqrt{E \left[\Delta \theta^2\right]_t + \epsilon} $$

其中

$$ E \left[\Delta \theta^2\right]_t = \rho E \left[\Delta \theta^2\right]_{t-1} + \left(1 - \rho\right) \Delta \theta_t^2 $$

在原始的论文中,作者直接用 $\text{RMS} \left[\Delta \theta^2\right]_t$ 替换了学习率,即

$$ \Delta \theta_t = - \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$

而在 Keras 源码中,则保留了固定的学习率,即

$$ \Delta \theta_t = - \alpha \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$

RMSprop

RMSprop 8 是由 Hinton 提出的一种针对 AdaGrad 的改进算法。参数的更新量为

$$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$

Adam

Adam (Adaptive Moment Estimation) 9 是另一种类型的自适应学习率方法,类似 Adadelta,Adam 对于每个参数都计算各自的学习率。Adam 方法中包含一个一阶梯度衰减项 $m_t$ 和一个二阶梯度衰减项 $v_t$

$$ \begin{equation} \begin{split} m_t &= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \end{split} \end{equation} $$

算法中,$m_t$$v_t$ 初始化为零向量,作者发现两者会更加偏向 $0$,尤其是在训练的初始阶段和衰减率很小的时候 (即 $\beta_1$$\beta_2$ 趋近于1的时候)。因此,对其偏差做如下校正

$$ \begin{equation} \begin{split} \hat{m}_t &= \dfrac{m_t}{1 - \beta_1^t} \\ \hat{v}_t &= \dfrac{v_t}{1 - \beta_2^t} \end{split} \end{equation} $$

最终得到 Adam 算法的参数更新量如下

$$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t $$

Adamax

在 Adam 中参数的更新方法利用了 $L_2$ 正则形式的历史梯度 ($v_{t-1}$) 和当前梯度 ($|g_t|^2$),因此,更一般的,我们可以使用 $L_p$ 正则形式,即

$$ \begin{equation} \begin{split} v_t &= \beta_2^p v_{t-1} + \left(1 - \beta_2^p\right) |g_t|^p \\ &= \left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p \end{split} \end{equation} $$

这样的变换对于值较大的 $p$ 而言是很不稳定的,但对于极端的情况,当 $p$ 趋近于无穷的时候,则变为了一个简单并且稳定的算法。则在 $t$ 时刻对应的我们需要计算 $v_t^{1/p}$,令 $u_t = \lim_{p \to \infty} \left(v_t\right)^{1/p}$,则有

$$ \begin{equation} \begin{split} u_t &= \lim_{p \to \infty} \left(\left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \lim_{p \to \infty} \left(1 - \beta_2^p\right)^{1/p} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \lim_{p \to \infty} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \max \left(\beta_2^{t-1} |g_1|, \beta_2^{t-2} |g_2|, ..., \beta_{t-1} |g_t|\right) \end{split} \end{equation} $$

写成递归的形式,则有

$$ u_t = \max \left(\beta_2 \cdot u_{t-1}, |g_t|\right) $$

则 Adamax 算法的参数更新量为

$$ \Delta \theta = - \dfrac{\alpha}{u_t} \hat{m}_t $$

Nadam

Adam 算法可以看做是对 RMSprop 和 Momentum 的结合:历史平方梯度的衰减项 $v_t$ (RMSprop) 和 历史梯度的衰减项 $m_t$ (Momentum)。Nadam (Nesterov-accelerated Adaptive Moment Estimation) 10 则是将 Adam 同 NAG 进行了进一步结合。我们利用 Adam 中的符号重新回顾一下 NAG 算法

$$ \begin{equation} \begin{split} g_t &= \nabla_{\theta} J \left(\theta_t - \gamma m_{t-1}\right) \\ m_t &= \gamma m_{t-1} + \alpha g_t \\ \theta_t &= \theta_{t-1} - m_t \end{split} \end{equation} $$

NAG 算法的核心思想会利用“下一步的梯度”确定“这一步的梯度”,在 Nadam 算法中,作者在考虑“下一步的梯度”时对 NAG 进行了改动,修改为

$$ \begin{equation} \begin{split} g_t &= \nabla_{\theta} J \left(\theta_t\right) \\ m_t &= \gamma m_{t-1} + \alpha g_t \\ \theta_t &= \theta_{t-1} - \left(\gamma m_t + \alpha g_t\right) \end{split} \end{equation} $$

对于 Adam,根据

$$ \hat{m}_t = \dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t} $$

则有

$$ \begin{equation} \begin{split} \Delta \theta &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \\ &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) \end{split} \end{equation} $$

上式中,仅 $\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t}$ 和动量项相关,因此我们类似上文中对 NAG 的改动,通过简单的替换加入 Nesterov 动量项,最终得到 Nadam 方法的参数的更新量

$$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^{t+1}} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) $$

AMSGrad

对于前面提到的 Adadelta,RMSprop,Adam 和 Nadam 方法,他们均采用了平方梯度的指数平滑平均值迭代产生新的梯度,但根据观察,在一些情况下这些算法并不能收敛到最优解。Reddi 等提出了一种新的 Adam 变体算法 AMSGrad 11,在文中作者解释了为什么 RMSprop 和 Adam 算法无法收敛到一个最优解的问题。通过分析表明,为了保证得到一个收敛的最优解需要保留过去梯度的“长期记忆”,因此在 AMSGrad 算法中使用了历史平方梯度的最大值而非滑动平均进行更新参数,即

$$ \begin{equation} \begin{split} m_t &= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \\ \hat{v}_t &= \max \left(\hat{v}_{t-1}, v_t\right) \\ \Delta \theta &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} m_t \end{split} \end{equation} $$

作者在一些小数据集和 CIFAR-10 数据集上得到了相比于 Adam 更好的效果,但与此同时一些其他的 实验 却得到了相比与 Adam 类似或更差的结果,因此对于 AMSGrad 算法的效果还有待进一步确定。

算法可视化

正所谓一图胜千言,Alec Radford 提供了 2 张图形象了描述了不同优化算法之间的区别

左图为 Beale Function 在二维平面上的等高线,从图中可以看出 AdaGrad,Adadelta 和 RMSprop 算法很快的找到正确的方向并迅速的收敛到最优解;Momentum 和 NAG 则在初期出现了偏离,但偏离之后调整了方向并收敛到最优解;而 SGD 尽管方向正确,但收敛速度过慢。

右图为包含鞍点的一个三维图像,图像函数为 $z = x^2 - y^2$,从图中可以看出 AdaGrad,Adadelta 和 RMSprop 算法能够相对很快的逃离鞍点,而 Momentum,NAG 和 SGD 则相对比较困难逃离鞍点。

很不幸没能找到 Alec Radford 绘图的原始代码,不过 Louis Tiao 在 博客 中给出了绘制类似动图的方法。因此,本文参考该博客和 Keras 源码中对不同优化算法的实现重新绘制了 2 张类似图像,详细过程参见 源代码,动图如下所示:


  1. Ruder, Sebastian. “An overview of gradient descent optimization algorithms.” arXiv preprint arXiv:1609.04747 (2016). ↩︎

  2. Qian, Ning. “On the momentum term in gradient descent learning algorithms.” Neural networks 12.1 (1999): 145-151. ↩︎

  3. Nesterov, Yurii. “A method for unconstrained convex minimization problem with the rate of convergence O (1/k^2).” Doklady AN USSR. Vol. 269. 1983. ↩︎

  4. Sutskever, Ilya. “Training recurrent neural networks.” University of Toronto, Toronto, Ont., Canada (2013). ↩︎

  5. Duchi, John, Elad Hazan, and Yoram Singer. “Adaptive subgradient methods for online learning and stochastic optimization.” Journal of Machine Learning Research 12.Jul (2011): 2121-2159. ↩︎

  6. Pennington, Jeffrey, Richard Socher, and Christopher Manning. “Glove: Global vectors for word representation.” Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP). 2014. ↩︎

  7. Zeiler, Matthew D. “ADADELTA: an adaptive learning rate method.” arXiv preprint arXiv:1212.5701 (2012). ↩︎

  8. Hinton, G., Nitish Srivastava, and Kevin Swersky. “Rmsprop: Divide the gradient by a running average of its recent magnitude.” Neural networks for machine learning, Coursera lecture 6e (2012). ↩︎

  9. Kingma, Diederik P., and Jimmy Ba. “Adam: A method for stochastic optimization.” arXiv preprint arXiv:1412.6980 (2014). ↩︎

  10. Dozat, Timothy. “Incorporating nesterov momentum into adam.” (2016). ↩︎

  11. Reddi, Sashank J., Satyen Kale, and Sanjiv Kumar. “On the convergence of adam and beyond.” International Conference on Learning Representations. 2018. ↩︎

生成对抗网络简介 (GAN Introduction)

2018-02-03 08:00:00

Generative Adversarial Networks (GAN)

生成对抗网络 (Generative Adversarial Network, GAN) 是由 Goodfellow 1 于 2014 年提出的一种对抗网络。这个网络框架包含两个部分,一个生成模型 (generative model) 和一个判别模型 (discriminative model)。其中,生成模型可以理解为一个伪造者,试图通过构造假的数据骗过判别模型的甄别;判别模型可以理解为一个警察,尽可能甄别数据是来自于真实样本还是伪造者构造的假数据。两个模型都通过不断的学习提高自己的能力,即生成模型希望生成更真的假数据骗过判别模型,而判别模型希望能学习如何更准确的识别生成模型的假数据。

网络框架

GAN 由两部分构成,一个生成器 (Generator) 和一个判别器 (Discriminator)。对于生成器,我们需要学习关于数据 $\boldsymbol{x}$ 的一个分布 $p_g$,首先定义一个输入数据的先验分布 $p_{\boldsymbol{z}} \left(\boldsymbol{z}\right)$,其次定义一个映射 $G \left(\boldsymbol{z}; \theta_g\right): \boldsymbol{z} \to \boldsymbol{x}$。对于判别器,我们则需要定义一个映射 $D \left(\boldsymbol{x}; \theta_d\right)$ 用于表示数据 $\boldsymbol{x}$ 是来自于真实数据,还是来自于 $p_g$。GAN 的网络框架如下图所示 2

模型训练

Goodfellow 在文献中给出了一个重要的公式用于求解最优的生成器

$$ \min_{G} \max_{D} V\left(D, G\right) = \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} $$

上式中,在给定的 $G$ 的情况下,$\max_{D} V\left(G, D\right)$衡量的是 $p_{data}$$p_g$ 之间的“区别”,因此我们最终的优化目标就是找到最优的 $G^*$ 使得 $p_{data}$$p_g$ 之间的“区别”最小。

首先,在给定 $G$ 的时候,我们可以通过最大化 $V \left(G, D\right)$ 得到最优 $D^*$

$$ \begin{equation} \begin{split} V \left(G, D\right) &= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ &= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) dx} + \int_{\boldsymbol{z}}{p_{\boldsymbol{z}} \left(\boldsymbol{z}\right) \log \left(1 - D\left(g\left(\boldsymbol{z}\right)\right)\right) dz} \\ &= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) + p_g\left(\boldsymbol{x}\right) \log \left(1 - D\left(\boldsymbol{x}\right)\right) dx} \end{split} \end{equation} $$

对于给定的任意 $a, b \in \mathbb{R}^2 \setminus \{0, 0\}$$a \log\left(x\right) + b \log\left(1 - x\right)$$x = \dfrac{a}{a+b}$ 处取得最大值,$D$ 的最优值为

$$ D_{G}^{*} = \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} $$

因此,$\max_{D} V \left(G, D\right)$ 可重写为

$$ \begin{equation} \begin{split} &C\left(G\right) \\ =& \max_{D} V \left(G, D\right) = V \left(G, D^*\right) \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D_{G}^{*}\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} \right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \dfrac{p_g \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}\right]} \\ =& \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{\dfrac{1}{2} p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log \dfrac{\dfrac{1}{2} p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} \\ =& \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log \dfrac{p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + 2 \log \dfrac{1}{2} \\ =& KL \left(p_{data} \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) + KL \left(p_g \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) - 2 \log 2 \\ =& 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$

其中 $KL$ 表示 KL 散度 3$JS$ 表示 JS 散度 4,因此在全局最优情况下 $p_g = p_{data}$

整个 GAN 的训练过程如下所示:

\begin{algorithm}
\caption{Minibatch SGD for GAN 算法}
\begin{algorithmic}
\REQUIRE $iter, k, m$
\ENSURE $\theta_d, \theta_g$
\FOR{$i = 1, 2, ..., iter$}
    \FOR{$j = 1, 2, ..., k$}
        \STATE Sample minibatch of $m$ noise samples $\{z^{\left(1\right)}, ..., z^{\left(m\right)}\}$ from $p_g \left(\boldsymbol{z}\right)$
        \STATE Sample minibatch of $m$ examples $\{x^{\left(1\right)}, ..., x^{\left(m\right)}\}$ from $p_{data} \left(\boldsymbol{z}\right)$
        \STATE $\theta_d \gets \theta_d \textcolor{red}{+} \nabla_{\theta_d} \dfrac{1}{m} \sum_{i=1}^{m}{\left[\log D \left(x^{\left(i\right)}\right) + \log \left(1 - D \left(G \left(z^{\left(i\right)}\right)\right)\right)\right]}$
    \ENDFOR
    \STATE Sample minibatch of $m$ noise samples $\{z^{\left(1\right)}, ..., z^{\left(m\right)}\}$ from $p_g \left(\boldsymbol{z}\right)$
    \STATE $\theta_g \gets \theta_g \textcolor{red}{-} \nabla_{\theta_g} \dfrac{1}{m} \sum_{i=1}^{m}{\log \left(1 - D \left(G \left(z^{\left(i\right)}\right)\right)\right)}$
\ENDFOR
\end{algorithmic}
\end{algorithm}

在实际的训练过程中,我们通常不会直接训练 $G$ 最小化 $\log \left(1 - D \left(G \left(\boldsymbol{z}\right)\right)\right)$,因为其在学习过程中的早起处于饱和状态,因此我们通常会通过最大化 $\log \left(D \left(G \left(z\right)\right)\right)$

存在的问题

针对 GAN,包括 Goodfellow 自己在内也提出了其中包含的很多问题 2,因此后人也提出了大量的改进,衍生出了大量的 GAN 变种。本章节仅对原始的 GAN 中存在的问题进行简略介绍,相关的改进请参见后续的具体改进算法。

JS 散度问题

我们在训练判别器的时候,其目标是最大化 JS 散度,但 JS 散度真的能够很好的帮助我们训练判别器吗? Wasserstein GAN 一文 5给出了不同生成器情况下 JS 散度的变化情况。

上图中,左边为一个基于 MLP 的生成器,右边为一个 DCGAN 6 生成器,两者均有一个 DCGAN 的判别器。根据上文我们可以知道判别器的目标是最大化

$$ \begin{equation} \begin{split} L \left(D, \theta_g\right) &= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ &= 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$

上图中 Y 轴绘制的为 $\dfrac{1}{2} L \left(D, \theta_g\right) + \log 2$,因为 $-2 \log 2 \leq L \left(D, \theta_g\right) \leq 0$,因此我们可得 $0 \leq \dfrac{1}{2} L \left(D, \theta_g\right) + \log 2 \leq \log 2$。从图中我们可以看出,针对两种不同的情况,其值均很快的逼近最大值 $\log 2 \approx 0.69$,当接近最大值的时候,判别器将具有接近于零的损失,此时我们可以发现,尽管 JS 散度很快趋于饱和,但 DCGAN 生成器的效果却仍在不断的变好,因此,使用 JS 散度作为判别其的目标就显得不是很合适。

多样性问题 Mode Collapse

Mode Collapse 问题是指生成器更多的是生成了大量相同模式的数据,导致的结果就是生成的数据缺乏多样性,如下图所示 7:

不难看出,其中红色方框圈出来的图像十分相似,这样的问题我们就称之为 Mode Collapse。Goolfellow 曾经从不同的 KL 散度的角度解释引起 Mode Collapse 的问题,但最后发现其并非由散度的不同所导致。对于 KL 散度,其并非是对称的,即 $D_{KL} \left(p_{data} \Vert p_{model}\right)$$D_{KL} \left(p_{model} \Vert p_{data}\right)$ 是不同的。在最大化似然估计的时候使用的是前者,而在最小化 JS 散度的时候使用的更类似于后者。如下图所示

假设我们的模型 $q$ 并没有足够能能力去拟合真实数据分布 $p$,假设真实数据由两个二维的高斯分布构成,而模型需要使用一个一维的高斯分布去拟合。在左图中,模型更倾向于覆盖两个高斯分布,也就是说其更倾向与在有真实数据的地方得到更大的概率。在右图中,模型更倾向于覆盖其中一个高斯分布,也就是说其更倾向于在没有真实数据的地方取得更小的概率。这样,如果我们用 JS 散度训练模型的时候就容易出现模式缺失的问题,但尽管我们利用前者去优化模型,但结果中仍然出现了 Mode Collapse 的问题,这也就说明并非 JS 散度问题导致的 Mode Collapse。

针对 Mode Collapse 的问题,出现了大量不同角度的优化

MNIST 示例

我们利用 MNIST 数据集测试原始的 GAN 模型的效果,代码主要参考了 Keras-GAN,最终实现代码详见 image_gan_keras.py,我们简单对其核心部分进行说明。

在生成器中,我们使用了一个包含3个隐含层的全链接网络,其中 self._hidden_dim 是我们定义的隐含节点最多一层的节点数;self._noise_shape 为用于生成器的噪音数据的形状;self._input_shape 为输入数据形状,即图片数据的形状,中间层次采用的激活函数为 LeakyReLU,最后一层采用的激活函数为 tanh

在判别器中,我们使用了一个包含2个隐含层的全链接网络,中间层次采用的激活函数为 LeakyReLU,最后一层采用的激活函数为 sigmoid

在构造整个对抗网络的时候,需要注意我们训练完判别器后,通过训练整个对抗网络进而训练生成器的时候是固定住训练好的判别器的,因此在训练整个对抗网络的时候我们应该将判别器置为无需训练的状态。

在训练整个对抗网络的时候,我们对于一个给定的生成器,我们将生成器生成的数据作为负样本,将从真实数据中采样的数据作为正样本训练判别器。Goodfellow 在描述 GAN 训练的过程中,对于给定的生成器,训练判别器 $k$ 次,不过通常取 $k = 1$。训练好判别器后,再随机生成噪音数据用于训练生成器,周而复始直至达到最大迭代次数。

在整个训练过程中,我们分别记录了判别器和生成器的损失的变化,以及判别器的准确率的变化,如下图所示:

从上图中我们可以看出,在训练开始阶段,判别器能够相对容易的识别出哪些数据是来自于真实数据的采样,哪些数据是来自于生成器的伪造数据。随着训练的不断进行,判别器的准确率逐渐下降,并稳定在 60% 左右,也就是说生成器伪造的数据越来越像真实的数据,判别器越来越难进行甄别。

下图中我们展示了利用 MNIST 数据集,进行 30000 次的迭代,每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像,最后合成的一张生成图片的变化动图。

Deep Convolutional GAN

DCGAN (Deep Convolutional GAN) 是由 Radford 6 等人提出的一种对原始 GAN 的变种,其基本的思想就是将原始 GAN 中的全链接层用卷积神经网络代替。在文中,Radford 等人给出构建一个稳定的 DCGAN 的建议,如下:

我们利用 MNIST 数据集测试 DCGAN 模型的效果,最终实现代码详见 image_dcgan_keras.py。训练过程中判别器和生成器的损失的变化,以及判别器的准确率的变化,如下图所示:

下图中我们展示了利用 MNIST 数据集,进行 30000 次的迭代,每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像,最后合成的一张生成图片的变化动图。

从生成的结果中可以看出,DCGAN 生成的图片的质量还是优于原始的 GAN 的,在原始的 GAN 中我们能够明显的看出其中仍旧包含大量的噪音点,而在 DCGAN 中这种情况几乎不存在了。


  1. Goodfellow, Ian, et al. “Generative adversarial nets.” Advances in neural information processing systems. 2014. ↩︎

  2. Goodfellow, Ian. “NIPS 2016 tutorial: Generative adversarial networks.” arXiv preprint arXiv:1701.00160 (2016). ↩︎ ↩︎

  3. https://en.wikipedia.org/wiki/Kullback–Leibler_divergence ↩︎

  4. https://en.wikipedia.org/wiki/Jensen–Shannon_divergence ↩︎

  5. Arjovsky, Martin, Soumith Chintala, and Léon Bottou. “Wasserstein gan.” arXiv preprint arXiv:1701.07875 (2017). ↩︎

  6. Radford, Alec, Luke Metz, and Soumith Chintala. “Unsupervised representation learning with deep convolutional generative adversarial networks.” arXiv preprint arXiv:1511.06434 (2015). ↩︎ ↩︎

  7. http://speech.ee.ntu.edu.tw/~tlkagk/courses/MLDS_2017/Lecture/GAN%20(v11).pdf ↩︎

  8. Che, Tong, et al. “Mode regularized generative adversarial networks.” arXiv preprint arXiv:1612.02136 (2016). ↩︎

  9. Salimans, Tim, et al. “Improved techniques for training gans.” Advances in Neural Information Processing Systems. 2016. ↩︎

  10. Metz, Luke, et al. “Unrolled generative adversarial networks.” arXiv preprint arXiv:1611.02163 (2016). ↩︎

  11. Tolstikhin, Ilya O., et al. “Adagan: Boosting generative models.” Advances in Neural Information Processing Systems. 2017. ↩︎

Ising 模型,Hopfield 网络和受限的玻尔兹曼机 (Ising, Hopfield and RBM)

2018-01-17 08:00:00

Ising 模型

$\renewcommand{\sign}{\operatorname{sign}}$Ising 模型最早是由物理学家威廉·冷次在 1920 年发明的,他把该模型当成是一个给他学生恩斯特·易辛的问题。易辛在他一篇 1924 年的论文 1 中求得了一维易辛模型的解析解,并且证明它不会产生相变。 二维方晶格易辛模型相对于一维的难出许多,因此其解析的描述在一段时间之后才在 1943 年由拉斯·昂萨格给出 2

Ising 模型假设铁磁物质是由一堆规则排列的小磁针构成,每个磁针只有上下两个方向。相邻的小磁针之间通过能量约束发生相互作用,同时受到环境热噪声的干扰而发生磁性的随机转变。涨落的大小由关键的温度参数决定,温度越高,随机涨落干扰越强,小磁针越容易发生无序而剧烈地状态转变,从而让上下两个方向的磁性相互抵消,整个系统消失磁性,如果温度很低,则小磁针相对宁静,系统处于能量约束高的状态,大量的小磁针方向一致,铁磁系统展现出磁性。而当系统处于临界温度 $T_C$ 时,Ising 模型表现出一系列幂律行为和自相似现象 3

由于 Ising 模型的高度抽象,可以很容易地将它应用到其他领域之中。例如,将每个小磁针比喻为某个村落中的村民,而将小磁针上下的两种状态比喻成个体所具备的两种政治观点,相邻小磁针之间的相互作用比喻成村民之间观点的影响,环境的温度比喻成每个村民对自己意见不坚持的程度,这样 Ising 模型就可以建模该村落中不同政治见解的动态演化。在社会科学中,人们已经将 Ising 模型应用于股票市场、种族隔离、政治选择等不同的问题。另一方面,如果将小磁针比喻成神经元细胞,向上向下的状态比喻成神经元的激活与抑制,小磁针的相互作用比喻成神经元之间的信号传导,那么,Ising 模型的变种还可以用来建模神经网络系统,从而搭建可适应环境、不断学习的机器,例如 Hopfield 网络或 Boltzmann 机。

考虑一个二维的情况

如图所示,每个节点都有两种状态 $s_i \in \{+1, -1\}$,则我们可以定义这个系统的能量为

$$ E = -H \sum_{i=1}^{N}{s_i} - J \sum_{<i, j>}{s_i s_j} $$

其中 $H$ 为外界磁场的强度,$J$ 为能量耦合常数,$\sum_{<i, j>}$表示对于相邻的两个节点的函数值求和。因此,可以得出

  1. 当每个节点的方向同外部磁场一致时,系统能量越小;反之系统能量越大。
  2. 对于 $J > 0$,当相邻的节点方向相同时,系统能量越小;反之系统能量越大。

对于整个系统的演变,除了系统的总能量以外,还受到节点所处环境的热噪声影响。我们利用温度 $T$ 表示环境对节点的影响,当 $T$ 越高时,节点状态发生变化的可能性越大。此时,则有两种力量作用在每个节点上

  1. 节点邻居和外部磁场的影响,这种影响使得当前节点尽可能的同其邻居和外部磁场保持一致,即尽可能是系统的总能量达到最小。
  2. 环境的影响,这种影响使得每个节点的状态以一定的概率发生随机变化。

不难想像,当 $T = 0$ 时,节点状态完全受其邻居和外部磁场影响,当 $J = 0, H = 0$ 时,节点处于完全的随机状态。

对于 Ising 模型,我们利用蒙特卡罗方法进行模拟。初始化系统状态为 $s_i^{\left(0\right)}$,对于任意时刻 $t$,对其状态 $s_i^{\left(t\right)}$进行一个改变,将其中一个节点变为相反的状态,得到新的状态 $s'_i$

$$ s_i^{\left(t+1\right)} = \begin{cases} s'_i & \text{with probablity of } \mu \\ s_i^{\left(t\right)} & \text{with probablity of } 1-\mu \end{cases} $$

其中 $\mu = \min\left\lbrace\dfrac{e^{E\left(s_i^{\left(t\right)}\right) - E\left(s'_i\right)}}{kT}, 1\right\rbrace$ 表示接受转移的概率;$k \approx 1.38 \times 10^{23}$ 为玻尔兹曼常数。我们利用蒙特卡罗方法对其进行模拟 $T = 4J/k$的情况,我们分别保留第 $0, 1, 5, 50, 500, 5000$ 步的模拟结果

# 每一轮状态转移
each_round <- function(current_matrix, ising_config) {
    n_row <- nrow(current_matrix)
    n_col <- ncol(current_matrix)
    
    for (i in 1:n_row) {
        for (j in 1:n_col) {
            current_row <- sample(1:n_row, 1)
            current_col <- sample(1:n_col, 1)
            s <- current_matrix[current_row, current_col]
            e <- -(current_matrix[(current_row-1-1)%%n_row+1, current_col] +
                current_matrix[current_row, (current_col-1-1)%%n_col+1] +
                current_matrix[(current_row+1)%%n_row, current_col] +
                current_matrix[current_row, (current_col+1)%%n_col]) *
                s * ising_config$j
            mu <- min(exp((e + e) / (ising_config$k * ising_config$t)), 1)
            mu_random <- runif(1)
            
            if (mu_random < mu) {
                s <- -1 * s
            }
            
            current_matrix[current_row, current_col] <- s
        }
    }
    
    current_matrix
}

# Ising 模拟
ising_simulation <- function(N, iter, ising_config, saved_steps) {
    set.seed(112358)
    current_matrix <- matrix(sample(0:1, N^2, replace = T), N, N)*2-1
    saved_matrix <- list()
    
    if (0 %in% saved_steps) {
        saved_matrix <- c(saved_matrix, list(current_matrix))
    }
    
    for (i in 1:iter) {
        if (i %in% saved_steps) {
            saved_matrix <- c(saved_matrix, list(current_matrix))
        }
        
        current_matrix <- each_round(current_matrix, ising_config)
        
        if (i %% 1000 == 0) {
            cat(paste0("Steps: ", i, '\n'))
        }
    }
    
    saved_matrix
}

# T = 4J/K,方便模拟取 j = 1, k = 1, t = 4
ising_config <- list(j = 1, k = 1, t = 4)
diff_steps_matrix <- ising_simulation(100, 5000, ising_config,
                                      c(0, 1, 5, 50, 500, 5000))

模拟结果可视化效果如图所示

对于二维的 Ising 模型,存在一个相变点,在相变点上的温度 $T_c$ 满足

$$ \sinh\left(\dfrac{2J_1}{kT_c}\right) \sinh\left(\dfrac{2J_2}{kT_c}\right) = 1 $$

$J_1 = J_2$,则

$$ T_c = \dfrac{2J}{k \ln\left(1 + \sqrt{2}\right)} \approx 2.27 \dfrac{J}{k} $$

称之为临界温度。当温度小于临界值的时候,Ising 模型中大多数节点状态相同,系统处于较为秩序的状态。当温度大于临界值的时候,大多数节点的状态较为混乱,系统处于随机的状态。而当温度接近临界的时候,系统的运行介于随机与秩序之间,也就是进入了混沌的边缘地带,这种状态称为临界状态。

我们模拟不同温度下,系统在运行 $50$ 步时的状态

ising_config_t <- c(1, 2, 2.27, 2.5, 3, 6)
diff_t_matrix <- lapply(ising_config_t, function(t) {
    ising_config <- list(j = 1, k = 1, t = t)
    ising_simulation(100, 50, ising_config, c(50))
})

模拟结果可视化效果如图所示

Hopfield 神经网络

Hopfield 神经网络 4 是一种基于能量的反馈人工神经网络。Hopfield 神经网络分为离散型 (Discrete Hopfield Neural Network, DHNN) 和 连续性 (Continues Hopfield Neural Network, CHNN)。

离散型 Hopfield 神经网络

网络结构

对于离散型 Hopfield 神经网络,其网络结果如下

对于具有 $n$ 个神经元的网络,我们设 $t$ 时刻的网络状态为 $\boldsymbol{X}^{\left(t\right)} = \left(x_1^{\left(t\right)}, x_2^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)^T$,对于 $t+1$ 时刻网络的状态

$$ x_i^{\left(t+1\right)} = f \left(net_i\right) $$

其中,DHNN 中 $f$ 多为符号函数,即

$$ x_i = \sign \left(net_i\right) = \begin{cases} 1, net_i \geq 0 \\ -1, net_i < 0 \end{cases} $$

$net_i$ 为一个节点的输入,为

$$ net_i = \sum_{j=1}^{n}{\left(w_{ij}x_j - T_i\right)} $$

其中 $T_i$ 为每个神经元的阈值,对于 DHNN,一般有 $w_{ii} = 0, w_{ij} = w_{ji}$,当反馈网络稳定后,稳定后的状态即为网络的输出。网络的更新主要有两种状态,异步方式同步方式

对于异步方式的更新方法,每一次仅改变一个神经元 $j$ 的状态,即

$$ x_i^{\left(t+1\right)} = \begin{cases} \sign\left(net_i^{\left(t\right)}\right), i = j \\ x_i^{\left(t\right)}, i \neq j \end{cases} $$

对于同步方式的更新方法,每一次需改变所有神经元的状态,即

$$ x_i^{\left(t+1\right)} = \sign\left(net_i^{\left(t\right)}\right) $$

网络稳定性

我们可以将反馈网络看做一个非线性动力学系统,因此这个系统最后可能会收敛到一个稳态,或在有限状态之间振荡,亦或是状态为无穷多个即混沌现象。对于 DHNN 因为其网络状态是有限的,因此不会出现混沌的现象。若一个反馈网络达到一个稳态状态 $\boldsymbol{X}$ 时,即 $\boldsymbol{X}^{\left(t+1\right)} = \boldsymbol{X}^{\left(t\right)}$ ,则称这个状态为一个吸引子。在 Hopfield 网络结构和权重确定的情况下,其具有 $M$ 个吸引子,因此我们可以认为这个网络具有存储 $M$ 个记忆的能力。

$\boldsymbol{X}$ 为网络的一个吸引子,权重矩阵 $\boldsymbol{W}$ 是一个对称阵,则定义 $t$ 时刻网络的能量函数为

$$ E\left(t\right) = -\dfrac{1}{2} \boldsymbol{X}^{\left(t\right)T} \boldsymbol{W} \boldsymbol{X}^{\left(t\right)} + \boldsymbol{X}^{\left(t\right)T} \boldsymbol{T} $$

则定义网络能量的变化量

$$ \Delta E\left(t\right) = E\left(t+1\right) - E\left(t\right) $$

则以异步更新方式,不难推导得出

$$ \begin{equation} \begin{split} \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} \left(\sum_{j=1}^{n}{\left(w_{ij}x_j - T_j\right)}\right) - \dfrac{1}{2} \Delta x_i^{\left(t\right)2} w_{ii} \end{split} \end{equation} $$

由于网络中的神经元不存在自反馈,即 $w_{ii} = 0$,则上式可以化简为

$$ \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} net_i^{\left(t\right)} $$

因此,对于如上的能量变化,可分为 3 中情况:

  1. $x_i^{\left(t\right)} = -1, x_i^{\left(t+1\right)} = 1$ 时,$\Delta x_i^{\left(t\right)} = 2, net_i^{\left(t\right)} \geq 0$,则可得 $\Delta E \left(t\right) \leq 0$
  2. $x_i^{\left(t\right)} = 1, x_i^{\left(t+1\right)} = -1$ 时,$\Delta x_i^{\left(t\right)} = -2, net_i^{\left(t\right)} < 0$,则可得 $\Delta E \left(t\right) < 0$
  3. $x_i^{\left(t\right)} = x_i^{\left(t+1\right)}$ 时,$\Delta x_i^{\left(t\right)} = 0$,则可得 $\Delta E \left(t\right) = 0$

则对于任何情况,$\Delta E \left(t\right) \leq 0$,也就是说在网络不断变化的过程中,网络的总能量是一直下降或保持不变的,因此网络的能量最终会收敛到一个常数。

$\boldsymbol{X}'$ 为吸引子,对于异步更新方式,若存在一个变换顺序,使得网络可以从状态 $\boldsymbol{X}$ 转移到 $\boldsymbol{X}'$,则称 $\boldsymbol{X}$ 弱吸引到 $\boldsymbol{X}'$,这些 $\boldsymbol{X}$ 的集合称之为 $\boldsymbol{X}$ 的弱吸引域;若对于任意变换顺序,都能够使得网络可以从状态 $\boldsymbol{X}$ 转移到 $\boldsymbol{X}'$,则称 $\boldsymbol{X}$ 强吸引到 $\boldsymbol{X}'$,对于这些 $\boldsymbol{X}$ 称之为 $\boldsymbol{X}$ 的强吸引域。

对于 Hopfield 网络的权重,我们利用 Hebbian 规则进行设计。Hebbian 规则认为如果两个神经元同步激发,则它们之间的权重增加;如果单独激发,则权重减少。则对于给定的 $p$ 个模式样本 $\boldsymbol{X}^k, k = 1, 2, ..., p$,其中 $x \in \{-1, 1\}^n$ 且样本之间两两正交,则权重计算公式为

$$ w_{ij} = \dfrac{1}{n} \sum_{k=1}^{p}{x_i^k x_j^k} $$

则对于给定的样本 $\boldsymbol{X}$ 确定为网络的吸引子,但对于有些非给定的样本也可能是网络的吸引子,这些吸引子称之为伪吸引子。以上权重的计算是基于两两正交的样本得到的,但真实情况下很难保证样本两两正交,对于非正交的模式,网络的存储能力则会大大下降。根据 Abu-Mostafa5 的研究表明,当模式的数量 $p$ 大于 $0.15 n$ 时,网络的推断就很可能出错,也就是结果会收敛到伪吸引子上。

示例

我们通过一个手写数字识别的例子介绍一些 Hopfield 网络的功能,我们存在如下 10 个数字的图片,每张为像素 16*16 的二值化图片,其中背景色为白色,前景色为黑色 (每个图片的名称为 num.png,图片位于 /images/cn/2018-01-17-ising-hopfield-and-rbm 目录)。

首先我们载入每张图片的数据

library(EBImage)

# 载入数据
digits <- lapply(0:9, function(num) {
    readImage(paste0(num, '.png'))
})

# 转换图像为 16*16 的一维向量
# 将 (0, 1) 转换为 (-1, 1)
digits_patterns <- lapply(digits, function(digit) {
    pixels <- c(digit)
    pixels * 2 - 1
})

接下来利用这 10 个模式训练一个 Hopfield 网络

#' 训练 Hopfield 网络
#' 
#' @param n 网络节点个数
#' @param pattern_list 模式列表
#' @return 训练好的 Hopfield 网络
train_hopfield <- function(n, pattern_list) {
    weights <- matrix(rep(0, n*n), n, n)
    n_patterns <- length(pattern_list)
    
    for (i in 1:n_patterns) {
        weights <- weights + pattern_list[[i]] %o% pattern_list[[i]]
    }
    diag(weights) <- 0
    weights <- weights / n_patterns
    
    list(weights = weights, n = n)
}

# 训练 Hopfield 网络
digits_hopfield_network <- train_hopfield(16*16, digits_patterns)

为了测试 Hopfiled 网络的记忆能力,我们利用 10 个模式生成一些测试数据,我们分别去掉图像的右边或下边的 5 个像素,生成新的 20 张测试图片

# 构造测试数据
digits_test_remove_right <- lapply(0:9, function(num) {
    digit_test <- digits[[num+1]]
    digit_test[12:16, ] <- 1
    digit_test
})
digits_test_remove_bottom <- lapply(0:9, function(num) {
    digit_test <- digits[[num+1]]
    digit_test[, 12:16] <- 1
    digit_test
})
digits_test <- c(digits_test_remove_right, digits_test_remove_bottom)

# 转换图像为 16*16 的一维向量
# 将 (0, 1) 转换为 (-1, 1)
digits_test_patterns <- lapply(digits_test, function(digit) {
    pixels <- c(digit)
    pixels * 2 - 1
})

我们利用训练好的 Hopfield 网络运行测试数据,我们迭代 300 次并保存最后的网络输出

#' 运行 Hopfiled 网络
#' @param hopfield_network 训练好的 Hopfield 网络
#' @param pattern 输入的模式
#' @param max_iter 最大迭代次数
#' @param save_history 是否保存状态变化历史
#' @return 最终的模式 (以及历史模式)
run_hopfield <- function(hopfield_network, pattern,
                         max_iter = 100, save_history = T) {
    last_pattern <- pattern
    history_patterns <- list()
    
    for (iter in 1:max_iter) {
        current_pattern <- last_pattern
        
        i <- round(runif(1, 1, hopfield_network$n))
        net_i <- hopfield_network$weights[i, ] %*% current_pattern
        current_pattern[i] <- ifelse(net_i < 0, -1, 1)
        
        if (save_history) {
            history_patterns[[iter]] <- last_pattern
        }
        
        last_pattern <- current_pattern
    }
    
    list(history_patterns = history_patterns,
         final_pattern = last_pattern)
}

# 运行 Hopfield 网络,获取测试数据结果
digits_test_results_patterns <- lapply(digits_test_patterns,
                                       function(pattern) {
    run_hopfield(digits_hopfield_network, pattern, max_iter = 300)
})

# 转换测试数据结果为图片
digits_test_results <- lapply(digits_test_results_patterns,
                              function(result) {
    each_dim <- sqrt(digits_hopfield_network$n)
    Image((result$final_pattern + 1) / 2,
          dim = c(each_dim, each_dim),
          colormode = 'Grayscale')
})

网络变换过程中,图像的变换如图所示

最终网络的输出如图所示

从结果中可以看出,部分测试图片还是得到了比较好的恢复,但如上文所说,由于我们给定的模式之间并不是两两正交的,因此,网络的推断就很可能出错 (例如:数字 5 恢复的结果更像 9 多一些),甚至结果会收敛到伪吸引子上。

连续型 Hopfield 神经网络

网络结构

连续型 Hopfield 网络相比于离散型 Hopfield 网络的主要差别在于:

  1. 网络中所有的神经元随时间 $t$ 同时更新,网络状态随时间连续变化。
  2. 神经元的状态转移函数为一个 S 型函数,例如 $$ v_i = f\left(u_i\right) = \dfrac{1}{1 + e^{\dfrac{-2 u_i}{\gamma}}} = \dfrac{1}{2} \left(1 + \tanh \dfrac{u_i}{\gamma}\right) $$ 其中,$v_i$ 表示一个神经元的输出,$u_i$ 表示一个神经元的输入。

对于理想情况,网络的能量函数可以写为6

$$ E = -\dfrac{1}{2} \sum_{i=1}^{n}{\sum_{j=1}^{n}{w_{ij} v_i v_j}} - \sum_{i=1}^{n} v_i I_i $$

可以得出,随着网络的演变,网络的总能量是降低的,随着网络中节点的不断变化,网络最终收敛到一个稳定的状态。

TSP 问题求解

旅行推销员问题 (Travelling salesman problem, TSP) 是指给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短路径 7。TSP 问题是一个 NP-hard 问题 8

对于 TSP 问题,我们给定一个城市指之间的距离矩阵

$$ D = \left\lgroup \begin{array}{cccc} d_{11} & d_{12} & \cdots & d_{1n} \\ d_{21} & d_{22} & \cdots & d_{2n} \\ \vdots & \vdots & & \vdots \\ d_{n1} & d_{n2} & \cdots & d_{nn} \end{array} \right\rgroup $$

其中 $d_{ij} = d_{ji}, i \neq j$ 表示城市 $i$ 和城市 $j$ 之间的距离,$d_{ij} = 0, i = j$。TSP 问题的优化目标是找到一条路径访问每一座城市一次并回到起始城市,我们利用一个矩阵表示访问城市的路径

$$ V = \left\lgroup \begin{array}{cccc} v_{11} & v_{12} & \cdots & v_{1n} \\ v_{21} & v_{22} & \cdots & v_{2n} \\ \vdots & \vdots & & \vdots \\ v_{n1} & v_{n2} & \cdots & v_{nn} \end{array} \right\rgroup $$

其中 $v_{xi} = 1$ 表示第 $i$ 次访问城市 $x$,因此对于矩阵 $V$,其每一行每一列仅有一个元素值为 $1$,其他元素值均为 $0$

对于 TSP 问题,我们可以得到如下约束条件

因为每个城市只能访问一次,因此对于第 $x$ 行仅能有一个元素是 $1$,其他均为 $0$,即任意两个相邻元素的乘积为 $0$

$$ \sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}} = 0 $$

则对于城市约束,我们得到该约束对应的能量分量为

$$ E_1 = \dfrac{1}{2} A \sum_{x=1}^{n}{\sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}}} $$

因为每一时刻仅能够访问一个城市,因此对于第 $i$ 行仅能有一个元素是 $1$,其他均为 $0$,即任意两个相邻元素的乘积为 $0$

$$ \sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}} = 0 $$

则对于时间约束,我们得到该约束对应的能量分量为

$$ E_2 = \dfrac{1}{2} B \sum_{i=1}^{n}{\sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}}} $$

当矩阵 $V$ 中所有的元素均为 $0$ 的时候,可得 $E_1 = 0, E_2 = 0$,但显然这并不是一个有效的路径,因此我们需要保证矩阵 $V$ 中元素值为 $1$ 的个数为 $n$,即

$$ \sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} = n $$

则对于有效性约束,我们得到该约束对应的能量分量为

$$ E_3 = \dfrac{1}{2} C \left(\sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} - n\right)^2 $$

如上三个约束仅能够保证我们的路径是有效的,但并不一定是最优的。根绝 TSP 问题的优化目标,我们需要引入一个反映路径长度的能量分量,并保证该能量分量随着路径长度的减小而减小。访问两个城市 $x, y$ 有两种形式,$x \to y$$y \to x$,如果城市 $x$ 和城市 $y$ 在旅行中顺序相邻,则 $v_{xi}v_{y,i+1} = 1, v_{xi}v_{y,i-1} = 0$,反之亦然。则反映路径长度的能量分量可以定义为

$$ E_4 = \dfrac{1}{2} D \sum_{x=1}^{n}{\sum_{y=1}^{n}{\sum_{i=1}^{n}{d_{xy}\left(v_{xi}v_{y,i+1} + v_{xi}v_{y,i-1}\right)}}} $$

综上所述,TSP 问题的能量函数定义为

$$ E = E_1 + E_2 + E_3 + E_4 $$

其中,$A, B, C, D > 0$ 分别为每个能量分量的权重。针对这样的能量函数,我们可得对应神经元 $x_i$$y_i$ 之间的权重为

$$ \begin{equation} \begin{split} w_{x_i, y_i} = &-2A \delta_{xy} \left(1-\delta_{xy}\right) - 2B \delta_{ij} \left(1-\delta_{xy}\right) \\ &- 2C -2D d_{xy} \left(\delta_{j, i+1} + \delta_{i, j+1}\right) \end{split} \end{equation} $$

其中

$$ \delta_{xy} = \begin{cases} 1, x = y \\ 0, x \neq y \end{cases} , \delta_{ij} = \begin{cases} 1, i = j \\ 0, i \neq j \end{cases} $$

因此可以得到网络关于时间的导数

$$ \begin{equation} \begin{split} \dfrac{d u_{xi}}{d t} = &-2A \sum_{j \neq i}^{n}{v_{xj}} - 2B \sum_{y \neq x}^{n}{v_{yi}} - 2C \left(\sum_{x=1}^{n}{\sum_{j=1}^{n}{v_{xj}}} - n\right) \\ &- 2D \sum_{y \neq x}^{n}{d_{xy}\left(v_{y, i+1} + v_{y, i-1}\right)} - \dfrac{u_{xi}}{\tau} \end{split} \end{equation} $$

据此,我们以一个 10 个城市的数据为例,利用 CHNN 求解 TSP 问题,其中 10 个城市的座标为

城市 横座标 纵座标
A 0.4000 0.4439
B 0.2439 0.1463
C 0.1707 0.2293
D 0.2293 0.7610
E 0.5171 0.9414
F 0.8732 0.6536
G 0.6878 0.5219
H 0.8488 0.3609
I 0.6683 0.2536
J 0.6195 0.2634

已知的最优路线为 $A \to D \to E \to F \to G \to H \to I \to J \to B \to C \to A$,最优路线的路径长度为 $2.6907$。我们使用如下参数求解 TSP 问题,初始化 $u_{init} = -\dfrac{\gamma}{2} \ln\left(n - 1\right)$$\gamma = 0.02$,学习率 $\alpha = 0.0001$,神经元激活阈值 $\theta = 0.7$$\tau = 1$,能量分量权重参数 $A = 500, B = 500, C = 1000, D = 500$,单次迭代最大次数为 1000,共模拟 100 次。

# 城市座标
cities <- data.frame(
    l = LETTERS[1:10],
    x = c(0.4000, 0.2439, 0.1707, 0.2293, 0.5171,
          0.8732, 0.6878, 0.8488, 0.6683, 0.6195),
    y = c(0.4439, 0.1463, 0.2293, 0.7610, 0.9414,
          0.6536, 0.5219, 0.3609, 0.2536, 0.2634)
)

# 通过城市座标构建距离矩阵
distance_matrix <- function(points) {
    n <- nrow(points)
    d <- matrix(rep(0, n^2), n, n)
    
    for (i in 1:n) {
        for (j in i:n) {
            distance <- sqrt((points[i, ]$x - points[j, ]$x)^2 +
                                 (points[i, ]$y - points[j, ]$y)^2)
            d[i, j] <- distance
            d[j, i] <- distance
        }
    }
    
    d
}

# 结果约束校验
check_path_valid <- function(v, n) {
    # 城市约束
    c1 <- 0
    for (x in 1:n) {
        for (i in 1:(n-1)) {
            for (j in (i+1):n) {
                c1 <- c1 + v[x, i] * v[x, j]
            }
        }
    }
    
    # 时间约束
    c2 <- 0
    for (i in 1:n) {
        for (x in 1:(n-1)) {
            for (y in (x+1):n) {
                c2 <- c2 + v[x, i] * v[y, i]
            }
        }
    }
    
    # 有效性约束
    c3 <- sum(v)
    
    ifelse(c1 == 0 & c2 == 0 & c3 == n, T, F)
}

# 根据结果矩阵获取路径
v_to_path <- function(v, n) {
    p <- c()
    
    for (i in 1:n) {
        for (x in 1:n) {
            if (v[x, i] == 1) {
                p <- c(p, x)
                break
            }
        }
    }
    
    p
}

# 计算结果矩阵的路径长度
path_distance <- function(v, n, d) {
    p <- v_to_path(v, n)
    p <- c(p, p[1])
    distance <- 0 
    for (i in 1:(length(p)-1)) {
        distance <- distance + d[p[i], p[i+1]]
    }
    
    distance
}

# 构建 Hopfield 网络
tsp_chnn <- function(d, n, gamma = 0.02, alpha = 0.0001,
                     theta = 0.7, tau = 1,
                     A = 500, B = 500, C = 1000, D = 500,
                     max_iter = 1000) {
    v <- matrix(runif(n^2), n, n)
    u <- matrix(rep(1, n^2), n, n) * (-gamma * log(n-1) / 2)
    du <- matrix(rep(0, n^2), n, n)
    
    for (iter in 1:max_iter) {
        for (x in 1:n) {
            for (i in 1:n) {
                # E1
                e1 <- 0
                for (j in 1:n) {
                    if (j != i) {
                        e1 <- e1 + v[x, j]
                    }
                }
                e1 <- -A * e1
                
                # E2
                e2 <- 0
                for (y in 1:n) {
                    if (y != x) {
                        e2 <- e2 + v[y, i]
                    }
                }
                e2 <- -B * e2
                
                # E3
                e3 <- -C * (sum(v) - n)
                
                # E4
                e4 <- 0
                for (y in 1:n) {
                    if (y != x) {
                        e4 <- e4 + d[x, y] *
                            (v[y, (i+1-1)%%n+1] + v[y, (i-1-1)%%n+1])
                    }
                }
                e4 <- -D * e4
                
                du[x, i] <- e1 + e2 + e3 + e4 - u[x, i] / tau
            }
        }
        
        u <- u + alpha * du
        v <- (1 + tanh(u / gamma)) / 2
        v <- ifelse(v >= theta, 1, 0)
    }
    
    v
}

# 利用 Hopfiled 网络求解 TSP 问题
set.seed(112358)

n <- 10
d <- distance_matrix(cities)

# 模拟 100 次并获取最终结果
tsp_solutions <- lapply(1:100, function(round) {
    v <- tsp_chnn(d, n)
    valid <- check_path_valid(v, n)
    distance <- ifelse(valid, path_distance(v, n, d), NA)
    
    list(round = round, valid = valid,
         distance = distance, v = v)
})

# 获取最优结果
best_tsp_solution <- NA
for (tsp_solution in tsp_solutions) {
    if (tsp_solution$valid) {
        if (!is.na(best_tsp_solution)) {
            if (tsp_solution$distance < best_tsp_solution$distance) {
                best_tsp_solution <- tsp_solution
            }
        } else {
            best_tsp_solution <- tsp_solution
        }
    }
}

# 可视化最优结果
best_tsp_solution_path <- v_to_path(best_tsp_solution$v, n)
ordered_cities <- cities[best_tsp_solution_path, ] %>%
    mutate(ord = seq(1:10))

best_tsp_solution_path_p <- ggplot(ordered_cities) +
    geom_polygon(aes(x, y), color = 'black', fill = NA) +
    geom_point(aes(x, y)) +
    geom_text(aes(x, y, label = l), vjust = -1) +
    geom_text(aes(x, y, label = ord), vjust = 2) +
    coord_fixed() + ylim(c(0, 1)) + xlim(c(0, 1)) +
    theme(axis.title = element_blank())
print(best_tsp_solution_path_p)

受限的玻尔兹曼机 (RBM)

网络结构及其概率表示

受限的玻尔兹曼机 (Restricted Boltzmann Machine, RBM) 或簧风琴 (harmonium) 是由 Smolensky 与 1986年在玻尔兹曼机 (Boltzmann Machine, BM) 基础上提出的一种随机神经网络 (Stochastic Neural Networks) 9。受限的玻尔兹曼机对于原始的玻尔兹曼机做了相应的限制,在其网络结构中包含可见节点隐藏节点,并且可见节点隐藏节点内部不允许存在连接,更加形象的可以将其理解为一个二分图。

对于二值版本的 RBM 而言,其中可见层 $\mathbf{v} = \left(v_1, v_2, ..., v_{n_v}\right)^T$$n_v$ 个二值随机变量构成;隐藏层 $\mathbf{h} = \left(h_1, h_2, ..., h_{n_h}\right)^T$$n_h$ 个二值随机变量构成。

RBM 同样作为一个基于能量的模型,其能量函数定义为:

$$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\sum_{i=1}^{n_v}{b_i v_i} -\sum_{j=1}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1}^{n_h}{v_i w_{i,j} h_i}} $$

将其表示成矩阵向量的形式,可记为:

$$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\boldsymbol{b}^T \boldsymbol{v} - \boldsymbol{c}^T \boldsymbol{h} - \boldsymbol{v}^T \boldsymbol{W} \boldsymbol{h} $$

其中 $\boldsymbol{b} \in \mathbb{R}^{n_v}$ 为可见层的偏置向量;$\boldsymbol{c} \in \mathbb{R}^{n_h}$ 为隐含层的偏置向量;$\boldsymbol{W} \in \mathbb{R}^{n_v \times n_h}$ 为可见层和隐含层之间的权重矩阵。根据能量函数,可得其联合概率分布为:

$$ P \left(\mathbf{v} = \boldsymbol{v}, \mathbf{h} = \boldsymbol{h}\right) = \dfrac{1}{Z} e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)} $$

其中 $Z$ 为归一化常数,成为配分函数:

$$ Z = \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)}}} $$

对于 RBM 我们更加关注的的为边缘分布,即:

$$ P \left(\boldsymbol{v}\right) = \sum_{h}{P\left(\boldsymbol{v}, \boldsymbol{h}\right)} = \dfrac{1}{Z} \sum_{h}{e^{-E\left(\boldsymbol{v}, \boldsymbol{h}\right)}} $$

因为概率中包含归一化常数,我们需要计算 $Z$,从其定义可得,当穷举左右可能性的化,我们需要计算 $2^{n_v + n_h}$ 个项,其计算复杂度很大。尽管 $P\left(\boldsymbol{v}\right)$ 计算比较困难,但是其条件概率 $P\left(\mathbf{h} | \mathbf{v}\right)$$P\left(\mathbf{v} | \mathbf{h}\right)$ 计算和采样相对容易。为了便于推导,我们定义如下记号:

$$ \boldsymbol{h}_{-k} = \left(h_1, h_2, ..., h_{k-1}, h_{k+1}, ..., h_{n_h}\right)^T $$

$P\left(h_k = 1 | \boldsymbol{v}\right)$ 定义如下:

$$ \begin{equation} \begin{split} &P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) \\ = &\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_{-k}, \boldsymbol{v}\right)} \\ = &\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) + P\left(h_k = 0 | h_{-k}, \boldsymbol{v}\right)} \\ = &\dfrac{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + \dfrac{1}{Z} e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &\dfrac{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &\dfrac{1}{1 + e^{E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) - E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ \end{split} \end{equation} $$

其中:

$$ \begin{equation} \begin{split} &E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) \\ = &E\left(h_k = 1, \boldsymbol{v}\right) \\ = &-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} - c_k - \sum_{i=1}^{n_v}{v_i W_{i, k}} \\ &E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right) \\ = &E\left(h_k = 0, \boldsymbol{v}\right) \\ = &-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} \end{split} \end{equation} $$

因此,$P\left(h_k = 1 | \boldsymbol{v}\right)$ 可以化简为:

$$ \begin{equation} \begin{split} &P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &\dfrac{1}{1 + e^{-\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right)}} \\ = &\sigma\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right) \\ = &\sigma\left(c_k + \boldsymbol{v}^T \boldsymbol{W}_{:, k}\right) \end{split} \end{equation} $$

其中,$\sigma$ 为 sigmoid 函数。因此,我们可以将条件分布表示为连乘的形式:

$$ \begin{equation} \begin{split} P\left(\boldsymbol{h} | \boldsymbol{v}\right) &= \prod_{j=1}^{n_h}{P\left(h_j | \boldsymbol{v}\right)} \\ &= \prod_{j=1}^{n_h}{\sigma\left(\left(2h - 1\right) \odot \left(\boldsymbol{c} + \boldsymbol{W}^T \boldsymbol{v}\right)\right)_j} \end{split} \end{equation} $$

同理可得:

$$ \begin{equation} \begin{split} P\left(\boldsymbol{v} | \boldsymbol{h}\right) &= \prod_{i=1}^{n_v}{P\left(v_i | \boldsymbol{h}\right)} \\ &= \prod_{i=1}^{n_v}{\sigma\left(\left(2v - 1\right) \odot \left(\boldsymbol{b} + \boldsymbol{W} \boldsymbol{h}\right)\right)_i} \end{split} \end{equation} $$

模型训练 10

对于 RBM 模型的训练,假设训练样本集合为 $S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$,其中 $\boldsymbol{v^i} = \left(v_{1}^{i}, v_{2}^{i}, ..., v_{n_v}^{i}\right), i = 1, 2, ..., n_s$。则训练 RBM 的目标可以定义为最大化如下似然:

$$ \mathcal{L}_{\theta, S} = \prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} $$

其中 $\theta$ 为待优化的参数,为了方便计算,等价目标为最大化其对数似然:

$$ \ln\mathcal{L}_{\theta, S} = \ln\prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} = \sum_{i=1}^{n_s}{\ln P\left(\boldsymbol{v}^i\right)} $$

我们将其对数似然简写为 $\ln\mathcal{L}_S$ ,通过梯度上升方法,我们可以得到参数的更新公式:

$$ \theta = \theta + \eta \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} $$

对于单个样本 $\boldsymbol{\color{red}{v'}}$ ,有:

$$ \begin{equation} \begin{split} \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} &= \dfrac{\partial \ln P\left(\boldsymbol{\color{red}{v'}}\right)}{\partial \theta} = \dfrac{\partial \ln \left(\dfrac{1}{Z} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right)}{\partial \theta} \\ &= \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln Z\right)}{\partial \theta} = \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right)}{\partial \theta} \\ &= \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right) - \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right) \\ &= -\dfrac{1}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \dfrac{1}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{Z}}{\dfrac{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}}{Z}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{P\left(\boldsymbol{\color{red}{v'}, h}\right)}{P\left(\boldsymbol{\color{red}{v'}}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \end{split} \end{equation} $$

其中:

$$ \begin{equation} \begin{split} \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} &= \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{P\left(\boldsymbol{v}\right) P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \\ &= \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) \sum_{\boldsymbol{h}}{P \left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \end{split} \end{equation} $$

则对于参数 $w_{i, j}$ 可得:

$$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_i v_j} \\ = &-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_i v_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-i} | \boldsymbol{v}\right) h_i v_j} \\ = &-\sum_{\boldsymbol{h_i}}{\sum_{h_{-i}}{P\left(h_i | \boldsymbol{v}\right) P\left(\boldsymbol{h_{-i}} | \boldsymbol{v}\right) h_i v_j}} \\ = &-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \sum_{\boldsymbol{h_{-i}}}{P\left(h_{-i} | \boldsymbol{v}\right)} \\ = &-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \\ = &-\left(P\left(h_i = 0 | \boldsymbol{v}\right) \cdot 0 \cdot v_j + P\left(h_i = 1 | \boldsymbol{v}\right) \cdot 1 \cdot v_j\right) \\ = &-P\left(h_i = 1 | \boldsymbol{v}\right) v_j \end{split} \end{equation} $$

则对于参数 $b_i$ 可得:

$$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) v_i} \\ = &-v_i \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right)} \\ = &-v_i \end{split} \end{equation} $$

则对于参数 $c_j$ 可得:

$$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_j} \\ = &-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(h_j | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j} \\ = &-\sum_{h_j}{\sum_{h_{-j}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j}} \\ = &-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \sum_{h_{-j}}{P\left(h_{-j} | \boldsymbol{v}\right)} \\ = &-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \\ = &-\left(P\left(h_j = 0 | \boldsymbol{v}\right) \cdot 0 + P\left(h_j = 1 | \boldsymbol{v}\right) \cdot 1\right) \\ = &-P\left(h_j = 1 | \boldsymbol{v}\right) \end{split} \end{equation} $$

综上所述,可得:

$$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial w_{i, j}} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial w_{i, j}}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ &= P\left(h_i = 1 | \boldsymbol{\color{red}{v'}}\right) \color{red}{v'_j} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v}\right) v_j}\\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial b_i} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial b_i}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ &= \color{red}{v'_i} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i} \\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial c_j} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial c_j}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ &= P\left(h_j = 1 | \boldsymbol{\color{red}{v'}}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)} \\ \end{split} \end{equation} $$

对于多个样本 $S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$,有:

$$ \begin{equation} \begin{split} \dfrac{\partial \ln \mathcal{L}_S}{\partial w_{i, j}} &= \sum_{m=1}^{n_S}{\left[P\left(h_i = 1 | \boldsymbol{v^m}\right) v_j^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v} v_j\right)}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial b_i} &= \sum_{m=1}^{n_S}{\left[v_i^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial c_j} &= \sum_{m=1}^{n_S}{\left[P\left(h_j = 1 | \boldsymbol{v^m}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)}\right]} \end{split} \end{equation} $$

针对如上方法,我们需要计算 $\sum_{\boldsymbol{v}}$ 相关项,如上文所述,其计算复杂度为 $O\left(2^{n_v + n_h}\right)$,因为其条件概率计算比较容易,因此我们可以用 Gibbs 采样的方法进行估计,但由于 Gibbs 采样方法存在 burn-in period,因此需要足够次数的状态转移后才能够收敛到目标分布,因此这就增大了利用这种方法训练 RBM 模型的时间。

针对这个问题,Hinton 于 2002 年提出了对比散度 (Contrastive Divergence, CD) 算法 11,基本思想为将训练样本作为采样的初始值,因为目标就是让 RBM 去拟合这些样本的分布,因此这样则可以通过更少的状态转移就收敛到平稳分布。$k$ 步 CD 算法大致步骤为:

  1. $\forall \boldsymbol{v} \in \boldsymbol{S}$,初始化 $\boldsymbol{v}^{\left(0\right)} = \boldsymbol{v}$
  2. 执行 $k$ 步 Gibbs 采样,对于第 $t$ 步,分别利用 $P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(t-1\right)}\right)$$P\left(\boldsymbol{v} | \boldsymbol{h}^{\left(t-1\right)}\right)$ 采样出 $\boldsymbol{h}^{\left(t-1\right)}$$\boldsymbol{v}^{\left(t\right)}$
  3. 利用采样得到的 $\boldsymbol{v}^{\left(k\right)}$ 近似估计 $\sum_{\boldsymbol{v}}$ 相关项: $$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial w_{i, j}} &\approx P\left(h_i=1|\boldsymbol{v}^{\left(0\right)}\right) v_j^{\left(0\right)} - P\left(h_i=1|\boldsymbol{v}^{\left(k\right)}\right) v_j^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial b_i} &\approx v_i^{\left(0\right)} - v_i^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial c_j} &\approx P\left(h_j=1|\boldsymbol{v}^{\left(0\right)}\right) - P\left(h_j=1|\boldsymbol{v}^{\left(k\right)}\right) \end{split} \end{equation} $$

近似估计可以看做是利用

$$ CDK\left(\theta, \boldsymbol{v}\right) = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(k\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(k\right)}, \boldsymbol{h}\right)}{\partial \theta}} $$

近似

$$ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial \theta} = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{v, h}\right) \dfrac{\partial E\left(\boldsymbol{v}, \boldsymbol{h}\right)}{\partial \theta}} $$

的过程。

基于对比散度的 RBM 训练算法可以描述为:

\begin{algorithm}
\caption{CDK 算法}
\begin{algorithmic}
\REQUIRE $k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)$
\ENSURE $\Delta \boldsymbol{W}, \Delta \boldsymbol{b}, \Delta \boldsymbol{c}$
\PROCEDURE{CDK}{$k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)$}
    \STATE $\Delta \boldsymbol{W} \gets 0, \Delta \boldsymbol{b} \gets 0, \Delta \boldsymbol{c} \gets 0$
    \FORALL{$\boldsymbol{v \in S}$}
        \STATE $\boldsymbol{v}^{\left(0\right)} \gets \boldsymbol{v}$
        \FOR{$t = 0, 1, ..., k-1$}
            \STATE $\boldsymbol{h}^{\left(t\right)} \gets \text{sample_h_given_v} \left(\boldsymbol{v}^{\left(t\right)}, \text{RBM}\left(W, b, c\right)\right)$
            \STATE $\boldsymbol{v}^{\left(t+1\right)} \gets \text{sample_v_given_h} \left(\boldsymbol{h}^{\left(t\right)}, \text{RBM}\left(W, b, c\right)\right)$
        \ENDFOR
        \FOR{$i = 1, 2, ..., n_h; j = 1, 2, ..., n_v$}
            \STATE $\Delta w_{i, j} \gets \Delta w_{i, j} + \left[P\left(h_i=1|\boldsymbol{v}^{\left(0\right)}\right) v_j^{\left(0\right)} - P\left(h_i=1|\boldsymbol{v}^{\left(k\right)}\right) v_j^{\left(k\right)}\right]$
            \STATE $\Delta b_i \gets \Delta b_i = \left[v_i^{\left(0\right)} - v_i^{\left(k\right)}\right]$
            \STATE $\Delta c_j \gets \Delta c_j = \left[P\left(h_j=1|\boldsymbol{v}^{\left(0\right)}\right) - P\left(h_j=1|\boldsymbol{v}^{\left(k\right)}\right)\right]$
        \ENDFOR
    \ENDFOR
\ENDPROCEDURE
\end{algorithmic}
\end{algorithm}

其中,sample_h_given_vsample_v_given_h 分别表示在已知可见层时采样隐含层和在已知隐含层时采样可见层。对于 sample_h_given_v 其算法流程如下:

\begin{algorithm}
\caption{sample\_h\_given\_v 算法}
\begin{algorithmic}
\FOR{$j = 1, 2, ..., n_h$}
    \STATE sample $r_j \sim Uniform[0, 1]$
    \IF{$r_j < P\left(h_j = 1 | \boldsymbol{v}\right)$}
        \STATE $h_j \gets 1$
    \ELSE
        \STATE $h_j \gets 0$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}

类似的,对于 sample_v_given_h 其算法流程如下:

\begin{algorithm}
\caption{sample\_v\_given\_h 算法}
\begin{algorithmic}
\FOR{$j = 1, 2, ..., n_h$}
    \STATE sample $r_j \sim Uniform[0, 1]$
    \IF{$r_i < P\left(v_i = 1 | \boldsymbol{h}\right)$}
        \STATE $v_i \gets 1$
    \ELSE
        \STATE $v_i \gets 0$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}

至此,我们可以得到 RBM 模型训练的整个流程:

\begin{algorithm}
\caption{RBM 训练算法}
\begin{algorithmic}
\FOR{$iter = 1, 2, ..., \text{max\_iter}$}
    \STATE $\Delta \boldsymbol{W}, \Delta \boldsymbol{b}, \Delta \boldsymbol{c} \gets \text{CDK} \left(k, \boldsymbol{S}, \text{RBM}\left(\boldsymbol{W, b, c}\right)\right)$
    \STATE $\boldsymbol{W} \gets \boldsymbol{W} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{W}\right)$
    \STATE $\boldsymbol{b} \gets \boldsymbol{b} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{b}\right)$
    \STATE $\boldsymbol{c} \gets \boldsymbol{c} + \eta \left(\dfrac{1}{n_s} \Delta \boldsymbol{c}\right)$
\ENDFOR
\end{algorithmic}
\end{algorithm}

其中,$k$ 为 CDK 算法参数,$\text{max_iter}$ 为最大迭代次数,$\boldsymbol{S}$ 为训练样本,$n_s = |\boldsymbol{S}|$$\eta$ 为学习率。

对于模型的评估,最简单的是利用 RBM 模型的似然或对数似然,但由于涉及到归一化因子 $Z$ 的计算,其复杂度太高。更常用的方式是利用重构误差 (reconstruction error),即输入数据和利用 RBM 模型计算得到隐含节点再重构回可见节点之间的误差。

MNIST 示例

我们利用经典的 MNIST 数据作为示例,我们利用基于 tensorflow 的扩展包 tfrbm 12。tfrbm 实现了 Bernoulli-Bernoulli RBM 和 Gaussian-Bernoulli RBM 两种不同的 RBM,两者的比较详见 13 14

import numpy as np
from matplotlib import pyplot as plt, gridspec
from tfrbm import BBRBM, GBRBM
from tensorflow.examples.tutorials.mnist import input_data

# 读入训练数据和测试数据
mnist = input_data.read_data_sets('MNIST', one_hot=True)
mnist_train_images = mnist.train.images
mnist_test_images = mnist.test.images
mnist_test_labels = mnist.test.labels

MNIST 数据集中,训练集共包含 55000 个样本,每个样本的维度为 784,我们构建 Bernoulli-Bernoulli RBM,设置隐含节点个数为 64,学习率为 0.01,epoches 为 30,batch size 为 10。

bbrbm = BBRBM(n_visible=784,
              n_hidden=64,
              learning_rate=0.01,
              use_tqdm=True)

bbrbm_errs = bbrbm.fit(mnist_train_images, n_epoches=30, batch_size=10)

# Epoch: 0: 100%|##########| 5500/5500 [00:11<00:00, 480.39it/s]
# Train error: 0.1267
# 
# ......
# 
# Epoch: 29: 100%|##########| 5500/5500 [00:11<00:00, 482.15it/s]
# Train error: 0.0347

训练误差变化如下

plt.style.use('ggplot')
plt.plot(bbrbm_errs)

我们从 MNIST 的测试集中针对每个数字选取 10 个样本,共 100 个样本作为测试数据,利用训练好的 RBM 模型重构这 100 个样本

mnist_test_images_samples = np.zeros([10 * 10, 784])
mnist_test_images_samples_rec = np.zeros([10 * 10, 784])
mnist_test_images_samples_plt = np.zeros([10 * 10 * 2, 784])

digits_current_counts = np.zeros(10, dtype=np.int32)
digits_total_counts = np.ones(10, dtype=np.int32) * 10

for idx in range(mnist_test_images.shape[0]):
    image = mnist_test_images[idx, ]
    label = mnist_test_labels[idx, ]

    for digit in range(10):
        digit_label = np.zeros(10)
        digit_label[digit] = 1

        if (label == digit_label).all() and
               digits_current_counts[digit] < 10:
            nrow = digits_current_counts[digit]
            sample_idx = nrow * 10 + digit
            mnist_test_images_samples[sample_idx, ] = image
            mnist_test_images_samples_rec[sample_idx, ] = \
                bbrbm.reconstruct(image.reshape([1, -1]))
            mnist_test_images_samples_plt[sample_idx * 2, ] = \
                mnist_test_images_samples[sample_idx, ]
            mnist_test_images_samples_plt[sample_idx * 2 + 1, ] = \
                mnist_test_images_samples_rec[sample_idx, ]
            digits_current_counts[digit] += 1

    if (digits_current_counts == digits_total_counts).all():
        break

对比测试输入数据和重构结果,奇数列为输入数据,偶数列为重构数据

def plot_mnist(mnist_images, nrows, ncols, cmap='gray'):
    fig = plt.figure(figsize=(ncols, nrows))
    gs = gridspec.GridSpec(nrows, ncols)
    gs.update(wspace=0.025, hspace=0.025)

    for nrow in range(nrows):
        for ncol in range(ncols):
            ax = plt.subplot(gs[nrow, ncol])
            idx = nrow * ncols + ncol
            minist_image = mnist_images[idx, ].reshape([28, 28])
            ax.imshow(minist_image, cmap=cmap)
            ax.axis('off')

    return fig
    
plot_mnist(mnist_test_images_samples_plt, 10, 20)

测试集上的重构误差为

gbrbm.get_err(mnist_test_images_samples)

# 0.035245348

  1. Ernest Ising, Beitrag zur Theorie des Ferround Paramagnetismus (1924) Contribution to the Theory of Ferromagnetism (English translation of “Beitrag zur Theorie des Ferromagnetismus”, 1925) Goethe as a Physicist (1950) ↩︎

  2. Onsager, L. “A two-dimensional model with an order–disorder transition (crystal statistics I).” Phys. Rev 65 (1944): 117-49. ↩︎

  3. http://wiki.swarma.net/index.php?title=ISING模型 ↩︎

  4. Hopfield, John J. “Neural networks and physical systems with emergent collective computational abilities.” Spin Glass Theory and Beyond: An Introduction to the Replica Method and Its Applications. 1987. 411-415. ↩︎

  5. Abu-Mostafa, Y. A. S. E. R., and J. St Jacques. “Information capacity of the Hopfield model.” IEEE Transactions on Information Theory 31.4 (1985): 461-464. ↩︎

  6. 韩力群. 人工神经网络理论、设计及应用 ↩︎

  7. https://zh.wikipedia.org/zh-hans/旅行推销员问题 ↩︎

  8. https://zh.wikipedia.org/zh-hans/NP困难 ↩︎

  9. Smolensky, Paul. Information processing in dynamical systems: Foundations of harmony theory. No. CU-CS-321-86. COLORADO UNIV AT BOULDER DEPT OF COMPUTER SCIENCE, 1986. ↩︎

  10. http://blog.csdn.net/itplus/article/details/19168937 ↩︎

  11. Hinton, Geoffrey E. “Training products of experts by minimizing contrastive divergence.” Neural computation 14.8 (2002): 1771-1800. ↩︎

  12. https://github.com/meownoid/tensorfow-rbm ↩︎

  13. Hinton, Geoffrey. “A practical guide to training restricted Boltzmann machines.” Momentum 9.1 (2010): 926. ↩︎

  14. Yamashita, Takayoshi, et al. “To be Bernoulli or to be Gaussian, for a Restricted Boltzmann Machine.” Pattern Recognition (ICPR), 2014 22nd International Conference on. IEEE, 2014. ↩︎

马尔科夫链蒙特卡洛方法和吉布斯采样 (MCMC and Gibbs Sampling)

2017-12-17 08:00:00

蒙特卡罗方法 (Monte Carlo, MC)

蒙特卡罗方法 (Monte Carlo) 也称为统计模拟方法,是于 20 世纪 40 年代由冯·诺伊曼,斯塔尼斯拉夫·乌拉姆和尼古拉斯·梅特罗波利斯在洛斯阿拉莫斯国家实验室为核武器计划工作时 (曼哈顿计划) 发明。因为乌拉姆的叔叔经常在摩纳哥的蒙特卡罗赌场输钱,该方法被定名为蒙特卡罗方法。蒙特卡罗方法是以概率为基础的方法,与之对应的是确定性算法。

蒙特卡罗方法最早可以追述到 18 世纪的布丰投针问题,该方法通过一个平行且等距木纹铺成的地板,随意抛一支长度比木纹之间距离小的针,求针和其中一条木纹相交的概率的方法得出了一个求 $\pi$ 的蒙特卡罗方法。我们通过另一种方式使用蒙特卡罗方法计算圆周率 $\pi$,对于一个边长为 $2r$ 的正方形,其内切圆的半径即为 $r$,因此圆形的面积 $A_c$ 与正方形的面积 $A_s$ 的比值为

$$ \dfrac{A_c}{A_s} = \dfrac{\pi r^2}{\left(2r\right)^2} = \dfrac{\pi}{4} $$

如果我们在矩形内随机的生成均匀分布的点,则在圆内的点的个数的占比即为 $\dfrac{\pi}{4}$,因此通过模拟即可求出 $\pi$ 的近似值

library(tidyverse)

# 圆的中心点和半径
r <- 2
center_x <- r
center_y <- r

# 距离公式
distance <- function(point_x, point_y, center_x, center_y) {
    sqrt((point_x - center_x)^2 + (point_y - center_y)^2)
}

# 点生成器
points_generator <- function(size) {
    set.seed(112358)
    points_x <- runif(size, min = 0, max = 2*r)
    points_y <- runif(size, min = 0, max = 2*r)
    
    tibble(
        x = points_x,
        y = points_y,
        in_cycle = ifelse(
            distance(points_x, points_y, center_x, center_y) > r, 0, 1)
    )
}

# 点的个数
sizes <- c(1000, 10000, 100000, 1000000, 10000000)

# 估计的 PI 值
estimated_pi <- sapply(sizes, function(size) {
    points <- points_generator(size)
    sum(points$in_cycle) * 4 / size
})
print(estimated_pi)
# [1] 3.184000 3.146400 3.137880 3.143140 3.141889

模拟 $1000$ 个随机点的结果如图所示

对于简单的分布 $p\left(x\right)$,我们可以相对容易的生成其样本,但对于复杂的分布或高维的分布,样本的生成就比较困难了1,例如:

  1. $p\left(x\right) = \dfrac{\tilde{p}\left(x\right)}{\int\tilde{p}\left(x\right) dx}$,其中 $\tilde{p}\left(x\right)$ 是可以计算的,而分母中的积分是无法显式计算的。
  2. $p\left(x, y\right)$ 是一个二维分布函数,函数本身计算很困难,但其条件分布 $p\left(x | y\right)$$p\left(y | x\right)$ 计算相对简单。对于高维情况 $p\left(\boldsymbol{x}\right)$,这种情况则更加明显。

这时候则需要更加复杂的模拟方法来生成样本了。

马尔科夫链 (Markov Chain, MC)

马尔可夫过程 (Markov Process) 是因俄国数学家安德雷·安德耶维齐·马尔可夫 (Андрей Андреевич Марков) 而得名一个随机过程,在该随机过程中,给定当前状态和过去所有状态的条件下,其下一个状态的条件概率分布仅依赖于当前状态,通常具备离散状态的马尔科夫过程称之为马尔科夫链 (Markov Chain)。因此,马尔科夫链可以理解为一个有限状态机,给定了当前状态为 $s_i$ 时,下一时刻状态为 $s_j$ 的概率,不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 $S_a, S_b, S_c$ 之间转换状态的马尔科夫链。

对于马尔科夫链,我们设 $X_t$ 表示 $t$ 时刻随机变量 $X$ 的取值,则马尔科夫链可以表示为

$$ P\left(X_{t+1} = s_j | X_0 = s_{i0}, X_1 = s_{i1}, ..., X_t = s_i\right) = P\left(X_{t+1} | X_t = s_i\right) $$

其中,$s_{i0}, s_{i1}, ..., s_i, s_j$ 为随机变量 $X$ 可能的状态。则定义从一个状态 $s_i$ 到另一个状态 $s_j$ 的转移概率为

$$ P\left(i \to j\right) = P_{ij} = P\left(X_{t+1} | X_t = s_i\right) $$

$\pi_{k}^{\left(t\right)}$ 表示随机变量 $X$$t$ 时刻取值为 $s_k$ 的概率,则 $X$$t+1$ 时刻取值为 $s_i$ 的概率为

$$ \begin{equation} \begin{split} \pi_{i}^{\left(t+1\right)} &= P\left(X_{t+1} = s_i\right) \\ &= \sum_{k}{P\left(X_{t+1} = s_i | X_t = s_k\right) \cdot P\left(X_t = s_k\right)} \\ &= \sum_{k}{P_{ki} \cdot \pi_{k}^{\left(t\right)}} \end{split} \end{equation} $$

我们通过一个例子来理解一下马尔科夫链,我们使用 LDA 数学八卦1一文中的例子,对于人口,我们将其经济状况分为 3 类:下层,中层和上层,其父代到子代收入阶层的转移情况如表所示

父代阶层\子代阶层 下层 中层 下层
下层 0.65 0.28 0.07
中层 0.15 0.67 0.18
上层 0.12 0.36 0.52

我们利用矩阵的形式表示转移概率

$$ P = \left\lgroup \begin{array}{cccc} P_{11} & P_{12} & \cdots & P_{1n} \\ P_{21} & P_{22} & \cdots & P_{2n} \\ \vdots & \vdots & & \vdots \\ P_{n1} & P_{n2} & \cdots & P_{nn} \end{array} \right\rgroup $$

$$ \pi^{\left(t+1\right)} = \pi^{\left(t\right)} P $$

假设初始概率分布为 $\pi_0 = \left(0.21, 0.68, 0.11\right)$,则计算前 $n$ 代人的阶层分布情况如下

# 转移矩阵
p <- matrix(c(0.65, 0.28, 0.07,
              0.15, 0.67, 0.18,
              0.12, 0.36, 0.52),
            3, 3, byrow = T)
# 初始概率
pi <- matrix(c(0.21, 0.68, 0.11), 1, 3, byrow = T)

# 迭代变化
for (i in 1:10) {
    pi_current <- pi[i, ]
    pi_next <- pi_current %*% p
    pi <- rbind(pi, pi_next)
}

colnames(pi) <- c('下层', '中层', '上层')
rownames(pi) <- 0:10
print(pi)
#         下层      中层      上层
# 0  0.2100000 0.6800000 0.1100000
# 1  0.2517000 0.5540000 0.1943000
# 2  0.2700210 0.5116040 0.2183750
# 3  0.2784592 0.4969956 0.2245452
# 4  0.2824933 0.4917919 0.2257148
# 5  0.2844752 0.4898560 0.2256688
# 6  0.2854675 0.4890974 0.2254351
# 7  0.2859707 0.4887828 0.2252465
# 8  0.2862280 0.4886450 0.2251270
# 9  0.2863602 0.4885817 0.2250581
# 10 0.2864283 0.4885515 0.2250201

可以看出,从第 7 代人开始,分布就基本稳定下来了,如果将初值概率换成 $\pi_0 = \left(0.75, 0.15, 0.1\right)$,结果会是如何呢?

pi <- matrix(c(0.75, 0.15, 0.1), 1, 3, byrow = T)

for (i in 1:10) {
    pi_current <- pi[i, ]
    pi_next <- pi_current %*% p
    pi <- rbind(pi, pi_next)
}

colnames(pi) <- c('下层', '中层', '上层')
rownames(pi) <- 0:10
print(pi)
#         下层      中层      上层
# 0  0.7500000 0.1500000 0.1000000
# 1  0.5220000 0.3465000 0.1315000
# 2  0.4070550 0.4256550 0.1672900
# 3  0.3485088 0.4593887 0.1921025
# 4  0.3184913 0.4745298 0.2069789
# 5  0.3030363 0.4816249 0.2153388
# 6  0.2950580 0.4850608 0.2198812
# 7  0.2909326 0.4867642 0.2223032
# 8  0.2887972 0.4876223 0.2235805
# 9  0.2876912 0.4880591 0.2242497
# 10 0.2871181 0.4882830 0.2245989

可以看出从第 9 代人开始,分布又变得稳定了,这也就是说分布收敛情况是不随初始概率分布 $\pi_0$ 的变化而改变的。则对于具有如下特征的马尔科夫链

  1. 非周期性,可以简单理解为如果一个状态有自环,或者与一个非周期的状态互通,则是非周期的。
  2. 不可约性,即任意两个状态都是互通的。

则这样的马尔科夫链,无论 $\pi_0$ 取值如何,最终随机变量的分布都会收敛于 $\pi^*$,即

$$ \pi^* = \lim_{t \to \infty}{\pi^{\left(0\right)} \boldsymbol{P}^t} $$

$\pi^*$ 称之为这个马尔科夫链的平稳分布。

马尔科夫链蒙特卡洛方法 (MCMC)

构造一个转移矩阵为 $P$ 的马尔科夫链,如果其能收敛到平稳分布 $p\left(x\right)$,则可以从任意一个状态 $x_0$ 出发,得到一个状态转移序列 $x_0, x_1, ..., x_n, x_{n+1}, ...$,如果马尔科夫链在第 $n$ 部收敛,我们就可以得到服从分布 $p\left(x\right)$ 的样本 $x_n, x_{n+1}, ...$。因此,利用马尔科夫链的平稳性生成数据的样本的关键就在于如何构造一个状态转移矩阵 $P$,使得其平稳分布为 $p\left(x\right)$

如果对于任意的 $i, j$,马尔科夫链的转移矩阵 $P$ 和分布 $\pi\left(x\right)$ 满足

$$ \pi\left(i\right) P_{ij} = \pi\left(j\right) P_{ji} $$

则称 $\pi\left(x\right)$ 为马尔科夫链的平稳分布,这称为细致平稳条件。对于一个马尔科夫链,通常情况下

$$ p\left(i\right) q\left(i, j\right) \neq p\left(j\right) q\left(j, i\right) $$

其中 $p\left(i, j\right)$ 表示状态从 $i$ 转移到 $j$ 的概率。因此,为了构造满足细致平稳条件,我们引入一个接受概率 $\alpha\left(i, j\right)$,使得

$$ p\left(i\right) q\left(i, j\right) \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right) \alpha\left(j, i\right) $$

最简单的,我们取

$$ \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right), \alpha\left(j, i\right) = p\left(i\right) q\left(i, j\right) $$

即可保证细致平稳性。通过引入接受概率,我们将原始的马尔科夫链改造为具有新的转移矩阵的马尔科夫链。在原始马尔科夫链上以概率 $q\left(i, j\right)$ 从状态 $i$ 转移到状态 $j$ 时,我们以概率 $\alpha\left(i, j\right)$ 接受这个转移,因此在新的马尔科夫链上的转移概率为 $q\left(i, j\right) \alpha\left(i, j\right)$。在新的马尔科夫链转移的过程中,如果接受概率 $\alpha\left(i, j\right)$ 过小,则可能导致存在大量的拒绝转移,马尔科夫链则很难收敛到平稳分布 $p\left(x\right)$,因此我们对 $\alpha\left(i, j\right), \alpha\left(j, i\right)$ 进行同比例放大,将其中较大的数放大至 $1$,则可以增加接受跳转的概率,从而更快的收敛到平稳分布。因此,我们可以取

$$ \alpha\left(i, j\right) = \min \left\lbrace\dfrac{p\left(j\right) q\left(j, i\right)}{p\left(i\right) q\left(i, j\right)}, 1\right\rbrace $$

这样我们就得到了 Metropolis-Hastings 算法

\begin{algorithm}
\caption{Metropolis-Hastings 算法}
\begin{algorithmic}
\STATE $X_0 \gets x_0$
\FOR{$t = 0, 1, 2, ...$}
    \STATE $X_t = x_t$
    \STATE sample $y \sim q\left(x | x_t\right)$
    \STATE sample $u \sim Uniform[0, 1]$
    \IF{$u < \alpha\left(x_t, y\right) = \min\left\lbrace\dfrac{p\left(j\right) q\left(j, i\right)}{p\left(i\right) q\left(i, j\right)}, 1\right\rbrace$}
        \STATE $X_{t+1} \gets y$
    \ELSE
        \STATE $X_{t+1} \gets x_t$
    \ENDIF
\ENDFOR
\end{algorithmic}
\end{algorithm}

吉布斯采样 (Gibbs Sampling)

对于 Metropolis-Hastings 算法,由于存在接受跳转概率 $\alpha < 1$,因此为了提高算法效率,我们尝试构建一个转移矩阵,使得 $\alpha = 1$。以二维情形为例,对于概率分布 $p\left(x, y\right)$,考虑两个点 $A\left(x_1, y_1\right)$$B\left(x_1, y_2\right)$

$$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &= p\left(x_1\right) p\left(y_1 | x_1\right) p\left(y_2 | x_1\right) \\ p\left(x_1, y_2\right) p\left(y_1 | x_1\right) &= p\left(x_1\right) p\left(y_2 | x_1\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$

可得

$$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &= p\left(x_1, y_2\right) p\left(y_1 | x_1\right) \\ p\left(A\right) p\left(y_2 | x_1\right) &= p\left(B\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$

可以得出在 $x = x_1$ 上任意两点之间进行转移均满足细致平稳条件,同理可得在 $y = y_1$上也满足。因此,对于二维情况,我们构建满足如下调价的概率转移矩阵 $Q$

$$ \begin{equation} \begin{split} &Q\left(A \to B\right) = p\left(y_B | x_1\right), \text{for} \ x_A = x_B = x_1 \\ &Q\left(A \to C\right) = p\left(x_C | y_1\right), \text{for} \ y_A = y_C = y_1 \\ &Q\left(A \to D\right) = 0, \text{others} \end{split} \end{equation} $$

则对于平面上任意两点 $X, Y$ 满足细致平稳条件

$$ p\left(X\right) Q\left(X \to Y\right) = p\left(Y\right) Q\left(Y \to X\right) $$

对于如上过程,我们不难推广到多维情况,将 $x_1$ 变为多维情形 $\boldsymbol{x_1}$,容易验证细致平稳条件依旧成立。

$$ p\left(\boldsymbol{x_1}, y_1\right) p\left(y_2 | \boldsymbol{x_1}\right) = p\left(\boldsymbol{x_1}, y_2\right) p\left(y_1 | \boldsymbol{x_1}\right) $$

对于 $n$ 维的情况,通过不断的转移得到样本 $\left(x_1^{\left(1\right)}, x_2^{\left(1\right)}, ..., x_n^{\left(1\right)}\right)$, $\left(x_1^{\left(2\right)}, x_2^{\left(2\right)}, ..., x_n^{\left(2\right)}\right)$, …,当马尔科夫链收敛后,后续得到的样本即为 $p\left(x_1, x_2, ..., x_n\right)$ 的样本,收敛之前的这一阶段我们称之为 burn-in period。在进行转移的时候,坐标轴轮换的采样方法并不是必须的,可以在坐标轴轮换中引入随机性。至此,我们就得到了吉布斯采样算法

\begin{algorithm}
\caption{Gibbs Sampling 算法}
\begin{algorithmic}
\STATE initialize $x^{\left(0\right)}, \text{for} \ i = 1, 2, ..., n$
\FOR{$t = 0, 1, 2, ...$}
    \STATE $x_1^{\left(t+1\right)} \sim p\left(x_1 | x_2^{\left(t\right)}, x_3^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)$
    \STATE $x_2^{\left(t+1\right)} \sim p\left(x_2 | x_1^{\left(t\right)}, x_3^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)$
    \STATE $...$
    \STATE $x_n^{\left(t+1\right)} \sim p\left(x_n | x_1^{\left(t\right)}, x_2^{\left(t\right)}, ..., x_{n-1}^{\left(t\right)}\right)$
\ENDFOR
\end{algorithmic}
\end{algorithm}

我们以二元高斯分布为例,演示如何用 Gibbs Sampling 方法进行采样,二元高斯分布定义为

$$ \left(X, Y\right) \sim \mathcal{N}\left(\boldsymbol{\mu}, \boldsymbol{\Sigma}\right) $$

其中

$$ \boldsymbol{\mu} = \left\lgroup \begin{array}{c} \mu_X \\ \mu_Y \end{array} \right\rgroup, \boldsymbol{\Sigma} = \left\lgroup \begin{array}{cc} \sigma_X^2 & \rho \sigma_X \sigma_Y \\ \rho \sigma_X \sigma_Y & \sigma_Y^2 \end{array} \right\rgroup $$

因此可得

$$ \begin{equation} \begin{split} \mu_{x|y} &= \mu_x + \sigma_x \rho_x\left(\dfrac{y - \mu_y}{\sigma_y}\right), \sigma_{x|y}^2 = \sigma_x^2 \left(1 - \rho^2\right) \\ \mu_{y|x} &= \mu_y + \sigma_y \rho_y\left(\dfrac{y - \mu_x}{\sigma_x}\right), \sigma_{y|x}^2 = \sigma_y^2 \left(1 - \rho^2\right) \end{split} \end{equation} $$

$$ \begin{equation} \begin{split} X|Y &= \mu_{x|y} + \sigma_{x|y} \mathcal{N}\left(0, 1\right) \\ Y|X &= \mu_{y|x} + \sigma_{y|x} \mathcal{N}\left(0, 1\right) \end{split} \end{equation} $$

对于 $\mu_x = 0, \mu_y = 0, \sigma_x = 10, \sigma_y = 1, \rho = 0.8$,采样过程如下

mu_x <- 0
mu_y <- 0
sigma_x <- 10
sigma_y <- 1
rho <- 0.8

iter <- 1000
samples <- matrix(c(mu_x, mu_y), 1, 2, byrow = T)

set.seed(112358)
for (i in 1:iter) {
    sample_x <- mu_x +
        sigma_x * rho * (samples[i, 2] - mu_y) / sigma_y +
        sigma_x * sqrt(1 - rho^2) * rnorm(1)
    sample_y <- mu_y +
        sigma_y * rho * (sample_x - mu_x) / sigma_x +
        sigma_y * sqrt(1 - rho^2) * rnorm(1)
    samples <- rbind(samples, c(sample_x, sample_y))
}

可视化结果如下


  1. LDA 数学八卦,靳志辉,2013 ↩︎ ↩︎

特征值分解,奇异值分解和主成分分析 (EVD, SVD and PCA)

2017-12-11 08:00:00

准备知识

向量与基

$\renewcommand{\diag}{\operatorname{diag}}\renewcommand{\cov}{\operatorname{cov}}$首先,定义 $\boldsymbol{\alpha}$ 为列向量,则维度相同的两个向量 $\boldsymbol{\alpha}, \boldsymbol{\beta}$ 的内积可以表示为:

$$\boldsymbol{\alpha} \cdot \boldsymbol{\beta} = \boldsymbol{\alpha}^T \boldsymbol{\beta} = \sum_{i=1}^{n}{\alpha_i b_i}$$

后续为了便于理解,我们以二维向量为例,则 $\boldsymbol{\alpha} = \left(x_1, y_1\right)^T, \boldsymbol{\beta} = \left(x_2, y_2\right)^T$,在直角座标系中可以两个向量表示如下:

我们从 $A$ 点向向量 $\boldsymbol{\beta}$ 的方向做一条垂线,交于点 $C$,则称 $OC$$OA$$OB$ 方向上的投影。设向量 $\boldsymbol{\alpha}$ 和向量 $\boldsymbol{\beta}$ 的夹角为 $\theta$,则:

$$\cos \left(\theta\right) = \dfrac{\boldsymbol{\alpha} \cdot \boldsymbol{\beta}}{\lvert\boldsymbol{\alpha}\rvert \lvert\boldsymbol{\beta}\rvert}$$

其中,$\lvert\boldsymbol{\alpha}\rvert = \sqrt{x_1^2 + y_1^2}$,则 $OC$ 的长度为 $\lvert\boldsymbol{\alpha}\rvert \cos\left(\theta\right)$

$n$ 维的线性空间 $V$ 中,$n$ 个线性无关的向量 $\boldsymbol{\epsilon_1, \epsilon_2, ..., \epsilon_n}$ 称为 $V$ 的一组。则对于 $V$ 中的任一向量 $\boldsymbol{\alpha}$ 可以由这组基线性表示出来:

$$\boldsymbol{\alpha} = x_1 \boldsymbol{\epsilon_1} + x_2 \boldsymbol{\epsilon_2} + ... + x_n \boldsymbol{\epsilon_n}$$

则对于向量 $\boldsymbol{\alpha} = \left(3, 2\right)^T$,可以表示为:

$$\boldsymbol{\alpha} = 3 \left(1, 0\right)^T + 2 \left(0, 1\right)^T$$

其中 $\left(1, 0\right)^T$$\left(0, 1\right)^T$ 为二维空间中的一组基。

因此,当我们确定好一组基之后,我们仅需利用向量在基上的投影值即可表示对应的向量。一般情况下,我们会选择由坐标轴方向上的单位向量构成的基作为默认的基来表示向量,但我们仍可选择其他的基。例如,我们选择 $\left(-\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$$\left(\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$ 作为一组基,则向量在这组基上的坐标为 $\left(-\dfrac{1}{\sqrt{2}}, \dfrac{5}{\sqrt{2}}\right)$,示例如下:

线性变换

以二维空间为例,定义一个如下矩阵

$$ A = \left\lgroup \begin{array}{cc} a_{11} & a_{12} \\ a_{21} & a_{22} \end{array} \right\rgroup $$

则对于二维空间中一个向量 $\boldsymbol{\alpha} = \left(x, y\right)^T$ ,通过同上述矩阵进行乘法运算,可得

$$ \boldsymbol{\alpha'} = A \boldsymbol{\alpha} = \left\lgroup \begin{array}{cc} a_{11} & a_{12} \\ a_{21} & a_{22} \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x' \\ y' \end{array} \right\rgroup $$

(1) 通过变换将任意一个点 $x$ 变成它关于 $x$ 轴对称的点 $x'$

$$ x' = \left\lgroup \begin{array}{cc} 1 & 0 \\ 0 & -1 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x \\ -y \end{array} \right\rgroup $$

(2) 通过变换将任意一个点 $x$ 变成它关于 $y = x$ 对称的点 $x'$

$$ x' = \left\lgroup \begin{array}{cc} 0 & 1 \\ 1 & 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} y \\ x \end{array} \right\rgroup $$

(3) 变换将任意一个点 $x$ 变成在它与原点连线上,与原点距离伸缩为 $|\lambda|$ 倍的点 $x'$

$$ x' = \left\lgroup \begin{array}{cc} \lambda & 0 \\ 0 & \lambda \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} \lambda x \\ \lambda y \end{array} \right\rgroup $$

(4) 通过变换将任意一个点 $x$ 绕原点旋转了角度 $\theta$ 的点 $x'$

$$ \begin{equation} \begin{split} x'& = \left\lgroup \begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup \\ & = \left\lgroup \begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} r \cos \phi \\ r \sin \phi \end{array} \right\rgroup \\ & = \left\lgroup \begin{array}{c} r \cos \left(\phi + \theta\right) \\ r \sin \left(\phi + \theta\right) \end{array} \right\rgroup \end{split} \end{equation} $$

(5) 变换将任意一个点 $x$ 变成它在 $x$ 轴上 的投影点 $x'$

$$ x' = \left\lgroup \begin{array}{cc} 1 & 0 \\ 0 & 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x \\ 0 \end{array} \right\rgroup $$

特征值分解

$A$ 是线性空间 $V$ 上的一个线性变换,对于一个非零向量 $\boldsymbol{\alpha} = \left(x_1, x_2, ..., x_n\right)^T$ 使得

$$A \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$$

$\lambda$ 称为 $A$ 的一个特征值$\boldsymbol{\alpha}$ 称为 $A$ 的一个特征向量。通过

$$ \begin{equation} \begin{split} A \boldsymbol{\alpha} &= \lambda \boldsymbol{\alpha} \\ A \boldsymbol{\alpha} - \lambda \boldsymbol{\alpha} &= 0 \\ \left(A - \lambda E\right) \boldsymbol{\alpha} &= 0 \\ A - \lambda E &= 0 \end{split} \end{equation} $$

其中 $E = \diag \left(1, 1, ..., 1\right)$ 为单位对角阵,即可求解其特征值,进而求解特征向量。若 $A$ 是一个可逆矩阵,则上式可以改写为:

$$ A = Q \sum Q^{-1} $$

这样,一个方阵 $A$ 就被一组特征值和特征向量表示了。例如,对于如下矩阵进行特征值分解

$$ A = \left\lgroup \begin{array}{cccc} 3 & -2 & -0.9 & 0 \\ -2 & 4 & 1 & 0 \\ 0 & 0 & -1 & 0 \\ -0.5 & -0.5 & 0.1 & 1 \end{array} \right\rgroup $$

A <- matrix(c(3, -2, -0.9, 0,
              -2, 4, 1, 0,
              0, 0, -1, 0,
              -0.5, -0.5, 0.1, 1),
            4, 4, byrow = T)
A_eig <- eigen(A)
print(A_eig)

# eigen() decomposition
# $values
# [1]  5.561553  1.438447  1.000000 -1.000000
# 
# $vectors
#             [,1]       [,2] [,3]        [,4]
# [1,] -0.61530186  0.4176225    0  0.15282144
# [2,]  0.78806410  0.3260698    0 -0.13448286
# [3,]  0.00000000  0.0000000    0  0.97805719
# [4,] -0.01893678 -0.8480979    1 -0.04431822

则利用特征值和特征向量,可以还原原矩阵

A_re <- A_eig$vectors %*%
    diag(A_eig$values) %*%
    solve(A_eig$vectors)
print(A_re)

#      [,1] [,2] [,3] [,4]
# [1,]  3.0 -2.0 -0.9    0
# [2,] -2.0  4.0  1.0    0
# [3,]  0.0  0.0 -1.0    0
# [4,] -0.5 -0.5  0.1    1

奇异值分解

特征值分解针对的是方阵,对于一个 $m*n$ 的矩阵是无法进行特征值分解的,这时我们就需要使用奇异值分解来解决这个问题。对于 $m*n$ 的矩阵 $A$,可得 $A A^T$ 是一个 $m*m$ 的方阵,则针对 $A A^T$,通过 $\left(A A^T\right) \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$,即可求解这个方阵的特征值和特征向量。针对矩阵 $A$,奇异值分解是将原矩阵分解为三个部分

$$ A_{m*n} = U_{m*r} \sum\nolimits_{r*r} V_{r*n}^T $$

其中 $U$ 称之为左奇异向量,即为 $A A^T$ 单位化后的特征向量;$V$ 称之为右奇异向量,即为 $A^T A$ 单位化后的特征向量;$\sum$矩阵对角线上的值称之为奇异值,即为 $A A^T$$A^T A$ 特征值的平方根。

我们利用经典的 lena 图片展示一下 SVD 的作用,lena图片为一张 $512*512$ 像素的彩色图片

我们对原始图片进行灰度处理后,进行特征值分解,下图中从左到右,从上到下分别是原始的灰度图像,利用 20 个左奇异向量和 20 个右奇异向量重构图像,利用 50 个左奇异向量和 100 个右奇异向量重构图像,利用 200 个左奇异向量和 200 个右奇异向量重构图像。

从图中可以看出,我们仅用了 200 个左奇异向量和 200 个右奇异向量重构图像与原始灰度图像已经基本看不出任何区别。因此,我们利用 SVD 可以通过仅保留较大的奇异值实现数据的压缩。

主成分分析

主成分分析1可以通俗的理解为一种降维方法。其目标可以理解为将一个 $m$ 维的数据转换称一个 $k$ 维的数据,其中 $k < m$。对于具有 $n$ 个样本的数据集,设 $\boldsymbol{x_i}$ 表示 $m$ 维的列向量,则

$$ X_{m*n} = \left(\boldsymbol{x_1}, \boldsymbol{x_2}, ..., \boldsymbol{x_n}\right) $$

对每一个维度进行零均值化,即减去这一维度的均值

$$ X'_{m*n} = X - \boldsymbol{u}\boldsymbol{h} $$

其中,$\boldsymbol{u}$ 是一个 $m$ 维的行向量,$\boldsymbol{u}[m] = \dfrac{1}{n} \sum_{i=1}^{n} X[m, i]$$h$ 是一个值全为 $1$$n$ 维行向量。

对于两个随机变量,我们可以利用协方差简单表示这两个变量之间的相关性

$$ \cov \left(x, y\right) = E \left(\left(x - \mu_x\right) \left(x - \mu_x\right)\right) $$

对于已经零均值化后的矩阵 $X'$,计算得出如下矩阵

$$ C = \dfrac{1}{n} X' X'^T = \left\lgroup \begin{array}{cccc} \dfrac{1}{n} \sum_{i=1}^{n} x_{1i}^2 & \dfrac{1}{n} \sum_{i=1}^{n} x_{1i} x_{2i} & \cdots & \dfrac{1}{n} \sum_{}^{} x_{1i} x_{ni} \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{2i} x_{1i} & \dfrac{1}{n} \sum_{i=1}^{n} x_{2i}^2 & \cdots & \dfrac{1}{n} \sum_{}^{} x_{2i} x_{ni} \\ \vdots & \vdots & & \vdots \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{1i} & \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{2i} & \cdots & \dfrac{1}{n} \sum_{}^{} x_{mi}^2 \\ \end{array} \right\rgroup $$

因为矩阵 $X'$ 已经经过了零均值化处理,因此矩阵 $C$ 中对角线上的元素为维度 $m$ 的方差,其他元素则为两个维度之间的协方差。

从 PCA 的目标来看,我们则可以通过求解矩阵 $C$ 的特征值和特征向量,将其特征值按照从大到小的顺序按行重排其对应的特征向量,则取前 $k$ 个,则实现了数据从 $m$ 维降至 $k$ 维。

例如,我们将二维数据

$$ \left\lgroup \begin{array} -1 & -1 & 0 & 0 & 2 \\ -2 & 0 & 0 & 1 & 1 \end{array} \right\rgroup $$

降至一维

x <- matrix(c(-1, -1, 0, 0, 2,
              -2, 0, 0, 1, 1),
            5, 2, byrow = F)
x_pca <- prcomp(x)

print(pca)
# Standard deviations (1, .., p=2):
# [1] 1.5811388 0.7071068
# 
# Rotation (n x k) = (2 x 2):
#            PC1        PC2
# [1,] 0.7071068  0.7071068
# [2,] 0.7071068 -0.7071068

summary(pca)
# Importance of components:
#                           PC1    PC2
# Standard deviation     1.5811 0.7071
# Proportion of Variance 0.8333 0.1667
# Cumulative Proportion  0.8333 1.0000

x_ <- predict(x_pca, x)
print(x_)
#             PC1        PC2
# [1,] -2.1213203  0.7071068
# [2,] -0.7071068 -0.7071068
# [3,]  0.0000000  0.0000000
# [4,]  0.7071068 -0.7071068
# [5,]  2.1213203  0.7071068

降维的投影结果如图所示


  1. Wold, Svante, Kim Esbensen, and Paul Geladi. “Principal component analysis.” Chemometrics and intelligent laboratory systems 2.1-3 (1987): 37-52. ↩︎

墨尔本之行 (Trip to Melbourne)

2017-08-26 08:00:00

从下了飞机到酒店,一路上体验到了我国互联网对世界各地的影响,机场巴士可以用微信和支付宝,下了巴士发现这里也有共享单车,人家还配了头盔。

晚上雅拉河还是很漂亮的,天气略凉,空气不错,淡淡的云,大冷天的人们也很愿意在外面吃饭。

谈到澳大利亚,最熟知的两种动物就是精壮的袋鼠和呆萌的考拉,晚上吃饭看了菜单发现居然有袋鼠肉,味道还是很不错的,没什么奇怪的味道。后来得知,在澳洲袋鼠的数量算得上略微的泛滥,所以法律是允许吃袋鼠的,如果你在路上开车不幸撞上了一只袋鼠,那么请快速的结束他的生命就好,免得痛苦,但如果你撞了一只考拉,那好吧,估计你出不去澳大利亚了……

当然,本次旅程最重要的还是 IJCAI 大会,让我这个半路出家搞 AI 的人感触最多的是:未知的还有很多,要学的也还有很多。这次大会期间也再次发表了呼吁禁止自主武器的公开信,我认为有时候我们更多的关注了 AI 技术的层面,而忽略了很多其他的事情,例如伦理和道德。例如现在医学图像识别技术在一定范围内已经超越了人类,那么是否我们就可以让机器直接做决定呢,如果出了问题,将如何处理,所以这个边界到底在哪里也是值的我们仔细思考的。

这边的冬天是多雨的,来的时候推算这边应该是早春,没带太厚的衣服,和当地人聊到天气,我说现在是 Early spring 怎么怎么的,结果对方说 No, No, No, still winter。尽管天气还是很凉,不过白天甚至晚上外面还是有很多路演的艺人,下面这个大哥唱的特别好,没一会儿就围了一圈人,他说他在为了下一次旅行筹钱,想想我何时才能有这样的勇气和行动力。

2017 年,墨尔本连续 7 年蝉联全球最适宜居住的城市冠军,很大一部分源自于其便利的交通。在墨尔本都是路上的有轨电车,还有一趟环城线路是免费的,晚上搭着它环城了一圈,最大的体会是,这儿真不大…… 路过市政厅,发现门口有人滑滑板,我也就敢在我们村委会门口玩一玩。环城电车有一个折返点,不像北京 10 号线那样无限循环,有点像带折返点的单程线,就在到头的时候奇迹发生了。我以为司机会下车到另一头再发动电车,不过他不是一个人离开的,还带走了方向盘,对是方向盘,劳动人民是智慧的,这车根本就不需要钥匙,拿走方向盘就行啦,不过话说有轨电车的方向盘又是干嘛用的。

作为不折不扣的伪军迷,自然也要了解一下这个城市的军事历史,说到这就不得不提大洋路 1。大洋路是位于澳洲东海岸维多利亚州南部的一条行车公路,全长约 276 公里,建于悬崖峭壁中间,起点自托尔坎 (Torquay),终点于亚伦斯福特 (Allansford)。大洋路始建于 1920 年,在 1932 年竣工,澳洲政府借此纪念第一次世界大战中牺牲的人。

一路上有十二门徒石 2 和阿德湖峡 3 等自然景点。

战争永远是残酷的,伤痛是无法消除的,我们能做的就是尊重这些为了国家奉献过的人们,缅怀逝去的,照顾残留的,这条有一战老兵参与修建的大洋路就是我们最好的纪念。去参观战争纪念馆的时候,恰巧碰上了一场纪念会,到的时候差不多结束了,不是太清楚具体是在纪念哪场战役,只远远的看到年轻的士兵持枪肃立,年老的士兵时不时的留下泪水。一瞬间想到了两段话,一段是说人的死亡分为三次,断气时,下葬时和被遗忘时,庆幸的是我们并没有选择忘记,至少现在还没有;另一段是麦克阿瑟的经典演讲,老兵不死,只是慢慢凋零。

最近恰巧看了二十二 4 和三十二两部纪录片,战争受难最多的终究还是百姓,不过越来越多的历史开始慢慢淡出人们的视野,但也有着那么一群人再帮着我们更好的铭记。其实,我们铭记不是说为了让我们去记恨一些人,去记恨一个民族,我们所需要让人们铭记的是战争本身的残酷,愿天下之安宁,以活民命。