跳到主要内容

Python3 wheel 包生成

· 阅读需 8 分钟

使用场景

当我们开发了一个 python 库时,希望能够生成一个安装包,使得用户可以快速安装、使用该库。

此帖涉及的安装包是 .whl,实质上就是一个 .zip 压缩包,里面存储了安装该库所必须的文件,比如 source code, scripts 等。

直接在本地安装 .whl 可以 pip3 install xxx.whl --user

更常见的方式是将 .whl 传到 PyPI 上,再让用户通过在线方式 pip3 install <package_name> --user 来完成安装。

Quick Start

环境配置

pip3 install setuptools wheel --user --upgrade

setuptools.setup - 安装脚本编写

假设目前有项目 example_gamma,在该项目的根路径创建 setup.py,树状图:

example_gamma
├── example_gamma
│ └── __init__.py
├── requirements.txt
└── setup.py

一个简单的 setup.py

from setuptools import setup, find_packages

setup(
# metadata
name="example_gamma",
version="0.0.1",
description="Example library for python package tutorial.",
author="wangzhao",
author_email="[email protected]",
# options
packages=find_packages(), # 自动寻找路径下的所有包
python_requires=">=3.6", # 需要的 python 版本限制
# 读取 requirements.txt 里的所有行,以列表形式呈现
install_requires=open("requirements.txt").read().splitlines(),
)

上述 setup 函数参数定义了该库的名称、版本、简介、作者、作者 email,以及设置了安装依赖、python 依赖。

setup.py - 生成 wheel 包

在项目根目录下运行

python3 setup.py sdist bdist_wheel

即可在根目录下生成一个名为 dist 的路径,进去之后就能发现打好的 whl 包 example_gamma-0.0.1-py3-none-any.whl

Further Discussion

setuptools vs distutils

distutils 是 python 原生库,而 setuptools 是第三方库,是 distutils 的增强版。官方文档也对 setuptools 有所提及。

https://docs.python.org/zh-cn/3/library/distutils.html

更多的 setup 参数

官方文档也没有完全说明所有参数的意义,需要自己拼凑和寻找。

This document is being retained solely until the setuptools documentation at https://setuptools.pypa.io/en/latest/setuptools.html independently covers all of the relevant information currently included here.

stackoverflow 上有人把所有参数都找出来列了个表,可以用来参考。

https://stackoverflow.com/questions/58533084/what-keyword-arguments-does-setuptools-setup-accept

这里挑几个参数进行介绍。示例可见: https://git-pd.megvii-inc.com/tidbit/dev-lecture/-/blob/master/package-python/example_beta/setup.py

packages

可以用 setuptools.find_packages 来指定根目录下哪些模块需要被安装,哪些不需要。

典型用法:

setup(
...
packages=find_packages(
include=("example*"), # 想安装 example 开头的所有模块
exclude=("test", "test*"), # 不想安装 test 开头的任何模块
)
....
)

exclude 会冲掉 include 的 pattern。

scripts

需要被安装的独立脚本列表。

pip3 install xxx --user 为例,这些脚本会被 copy 到 ~/.local/bin 下,并添加权限,使得这些脚本可以直接被执行。

比如:remote_boardrun_joint.py, run_neu.py, ...

entry_points

用于自动生成脚本,安装后自动生成 ~/.local/bin 下的可执行文件(假如通过 pip3 install xxx --user 安装)。

<name> = [<package>.[<subpackage>.]]<module>[:<object>.<object>]

典型的用法:

setup(
...
entry_points={
# super_beta 指向 example_beta/__init__.py 的 test 函数
"console_scripts": ["super_beta = example_beta:test"]
}
...
)

classifiers

说明包的分类信息,所有支持的列表见 https://pypi.org/classifiers/

典型的用法[2]

setup(
...
classifiers = [
# 发展时期,常见的如下
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
'Development Status :: 3 - Alpha',

# 开发的目标用户
'Intended Audience :: Developers',

# 属于什么类型
'Topic :: Software Development :: Build Tools',

# 许可证信息
'License :: OSI Approved :: MIT License',

# 目标 Python 版本
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
]
...
)

package_data

  • 需要额外加进包里的文件,比如文档、静态图片、配置文件等

典型用法:

setup(
...
package_data={
# `example_beta` 模块中满足 'resources/data/*.dat' 的文件都会被包含在包里
'example_beta': ['resources/data/*.dat'],
},
...

更多的 setup.py 功能

sdist / bdist / bdist_egg / bdist_wheel / ...

  • sdist
    • 生成 source distribution,即包括一个所有源代码与额外数据的压缩包,可以在任意平台来重新编译。
  • bdist
    • 生成 built distribution,一般得到在指定平台上的包,安装时无需再重新编译[8]。
      • bdist_wheel: *.whl
      • bdist_rpm: *.rpm
      • bdist_wininst: *.exe
      • bdist_egg: *.egg
      • ...

install

通过 python3 setup.py install 直接在本地安装库,一般使用场景是从源代码 repo 直接安装,或者 pip install 在某些情况下也是帮用户执行了这条指令。

PyPI & devpi

  • PyPI
    • Python Package Index
    • python 的正式第三方软件包的存储库
    • ws2 用的是 brainpp 的私有 PyPI
  • devpi
    • devpi-server: PyPI 服务器
    • devpi-client: 打包/测试/发布工具

setup.cfg & pyproject.toml

Quick Start

setuptools 官方文档快速上手目前以该方式为首选:

https://setuptools.pypa.io/en/latest/userguide/quickstart.html

典型的使用方式:

pip3 install build --user  # install `PyPA build`
python3 -m build --wheel # python3 setup.py bdist_wheel

对比:

setup.cfg: https://git-pd.megvii-inc.com/tidbit/dev-lecture/-/blob/master/package-python/example_alpha/setup.cfg

setup.py: https://git-pd.megvii-inc.com/tidbit/dev-lecture/-/blob/master/package-python/example_beta/setup.py

为什么要使用这种方式来进行打包

安装环境隔离

pip 在安装包时大多数使用如下方式:

  1. 寻找包
  2. 下载、解压包
  3. 运行 python setup.py install 进行安装

问题是 pip 调用了本机的 python 的解释器及本机的各种 python 第三方库比如 setuptools。一旦某个包用到了 setuptools 的最新特性,就必须要用户手动去更新本机的第三方库,有时还会安装其他库的依赖有冲突[7]。

此时就需要一个配置文件来隔离“安装包所需要的依赖”和“用户本机上其他第三方库运行时所需的依赖”。这就是 PEP-518[5] 标准中提到的 pyproject.toml

在 PEP-518 标准下,一次包的安装则会在 virtual env 下进行,从而达到安装与运行时依赖隔离的效果。

声明式安装

虽然目前大部分 setup.py 都被写成一个声明式的程序,但只要程序员尝试使用命令式的方式来编写脚本,就有可能会导致各种各样的程序 bug 。

所以基于配置的安装方式被 PEP-517[4] 标准提出,各种安装框架比如 setuptools 就建议使用 setup.cfg 来配置各种参数。当然还有比如 filt 等其他框架,也支持各种不同的方式。

References

Convex Hull - Part 3

· 阅读需 8 分钟

这次继续介绍几个凸包算法,包括 Graham scan 和快速凸包算法。

Graham scan

Graham scan 是一个表达起来很简洁的算法,而且也没有涉及到复杂的数据结构,它仅仅需要两个栈 SSTT

首先遍历所有点,选出 lowest-then-leftmost 的点 p1p_{1},并以该点为参照,将所有其余点按照极角排序,分别为 p2,p3,,pnp_{2},p_{3},\cdots ,p_{n}

两个栈初始化为(方括号代表栈底): S=[ p1,p2>S=[\ p_{1},p_{2}> T=<p3,p4,,pn ]T=< p_{3},p_{4},\cdots ,p_{n}\ ]

扫描算法流程:当栈 TT 非空时,如果栈 TT 的栈顶在栈 SS 的栈顶与次栈顶组成的有向边的左侧,则将栈 TT 的栈顶压入栈 SS;否则弹出栈 SS 的栈顶元素。伪代码如下:

while (!T.empty())
toLeft(S[1], S[0], T[0]) ? S.push(T.pop()) : S.pop();

有兴趣的读者可以在下图所示的点集上运行一遍算法流程,我就不赘述了。

graham-scan-1

扫描效率

这一节讨论扫描一步的效率。粗浅来看,点集中除了参照点的每个点都可能被做 Ω(n)\Omega(n) toLeft 比较,那么扫描效率就是 O(n)×O(n)=O(n2)O(n)\times O(n)=O(n^{2})。但这种估计方法明显太松了,接下来用两种方法求更紧的上界。

平面图

先来复习一下图论中平面图的概念:

在图论中,平面图是可以画在平面上并且使得不同的边可以互不交叠的图。而如果一个图无论怎样都无法画在平面上,并使得不同的边互不交叠,那么这样的图不是平面图,或者称为非平面图。

而在扫描一步中,遍历的所有边都不会相交(如下图)。也就是说,这一步的搜索空间构成了一个平面图。根据欧拉定理,得知平面图中边数 E=O(3n)E=O(3n),从而说明了扫描效率为 O(n)O(n)

graham-scan-2

均摊分析

我们从另一方面看待这个问题,考虑变量 A=S+2TA=|S|+2|T|。显而易见地,每次循环中 AA 都会减少 11。扫描开始时,A=2n2A=2n-2;扫描结束时,A3A\ge 3。则循环执行了 O(2n5)=O(n)O(2n-5)=O(n) 次,也即扫描效率为 O(n)O(n)

简化

仔细想想,算法中的排序一步是不需要 toLeft 作为比较器的。

假如在点集中增加一个点作为参考点,并把它无限拉低,直到 (0,)(0,-\infty),则点集中其余点与参考点连成的线就是竖直的,也就是说,我们只需要对其余点的横轴坐标排序即可。

但由于引入了一个新的点,所以也会导致求得的凸包与原点集凸包不符。幸运的是,如果我们将新凸包中与参考点有关的两条边删掉,就可以得到原凸包的上半部分,称之为上半凸包(upper hull)。

同理,可以在点集中增加参考点 (0,+)(0,+\infty),就可以求出下半凸包(lower hull)。将两者合并,就得到了原凸包。

极点个数期望

PP 是平面上的点集,假设点的个数是 nn,这一节将讨论 CH(P)CH(P)(P的凸包)上点个数的量级。

值得注意的是,不同的采样方式会得出不同的结果。考虑在单位正方形内均匀且独立地采样(单位正方形与任意长方形是相同的,因为两者可以通过仿射变换至彼此)。

取凸包的最上/下/左/右四个点,可以将凸包分成四个部分,不失一般性,我们只考虑凸包的右上角 CHUR(P)CH_{UR}(P),如下图。

expection-complexity-1

在右上角区域,可以定义「极大点」:

以点 PP 为原点建立坐标系,如果第一象限没有点集中的其余点,则称点 PP 为极大点。

expection-complexity-2

称极大点集为 MAX(P)MAX(P),则 CHUR(P)MAX(P)|CH_{UR}(P)|\le |MAX(P)|,接下来考虑 MAX(P)|MAX(P)| 的期望。

从右到左,将点集中的点称之为 {p1,p2,,pn}\{p_{1},p_{2},\cdots ,p_{n}\}。对于点 pkp_{k} 来讲,它是极大点当且仅当它是 {p1,p2,pk}\{p_{1}, p_{2}\cdots ,p_{k}\} 中最高的。由于点是在单位正方形内均匀独立采样得到的,则 pkp_{k} 是极大点的概率为 1k\frac{1}{k}。故

E[MAX(P)]=i=1n1n=O(log(n))E[|MAX(P)|]=\sum_{i=1}^{n} \frac{1}{n}=O(\log(n))

经过上述推导,得知当在单位正方形内均匀独立采样时,凸包上点个数的期望为 O(log(n))O(\log(n))

除了在单位正方形内采样以外,在其他几何形状内采样则会得到不同的结果:

  • 单位圆 —— O(n13)O(n^{\frac{1}{3}})
  • 三角形 —— O(log(n))O(\log(n))
  • kk 多边形 —— O(klog(n))O(k\log(n))

快速凸包(Quickhull)

首先找出点集中 leftmost-then-lowest 的点 ss,以及 rightmost-then-highest 的点 tt。则求解凸包就可以分为求解上半凸包和下半凸包,如下图。

quickhull-1

由于求解上下半凸包是对称的,所以只讨论上半凸包的求解过程。

与快速排序类似,在每次求解凸包时,都将当前点集分为三部分:

  • P0P_{0} 区域是需要被剪掉的部分,这其中的点之后无需考虑;
  • P1,P2P_{1},P_{2} 区域为左、右子区域,原凸包可以由这两个子区域的凸包组合而成。

具体来说,取距离线段 stst 最远的点 rr 作为哨兵。那么 srt\triangle srt 包围的区域则为 P0P_{0}srsr 左侧的区域是 P1P_{1}rtrt 右侧的区域是 P2P_{2},如下图。

quickhull-2

当然快速凸包算法的最差情况也是 O(n2)O(n^{2}) 的,考虑这种情况:在单位圆的直径上取两个点,随后在圆心角为 902k,k=0,1,2,\frac{90^{\circ}}{2^{k}},k=0,1,2,\cdots 的位置加入点,如下图。那么每次选择哨兵时都会造成左右极不均匀,从而导致最坏情况。

quickhull-3

Convex Hull - Part 2

· 阅读需 8 分钟

先来说两个简单的计算几何小问题:

给定两个二维线段,判定它们是否相交?

当然最笨的办法就是求出两个线段方程,判断解是否满足要求。但这会引入除法操作,这不是我们希望的结果。

line-line

如上图,考虑 P3,P4P_{3}, P_{4} 是否在线段 aa 的异侧(toLeft 返回值不同则表示在异侧)。当然只做这一次异侧判断是不行的,对称地考虑 P1,P2P_{1}, P_{2} 是否在线段 bb 的异侧。如果两次异侧判断都成功,则说明两个线段相交。这种方法只需要进行 44 次 toLeft 判断,提高了效率。

用极点法求出若干极点后,如何将其排成环?

假设 leftmost-then-lowest 的极点为 P0P_0,定义 P1<P2toLeft(P0P1,P2)==falseP_{1} < P_{2} \Leftrightarrow toLeft(P_{0}P_{1}, P_{2})==false。按照上述定义的偏序关系就可以对所有极点排序,排序后从小到大即可。

从上面两道小题能够看出 toLeft 判定的重要性,这个判定函数将会贯穿计算几何的学习历程。

极边法(Extreme Edges)

从极点法我们可以自然而然地想到,如果遍历点集中所有可能成为凸包边界(极边)的线段,也能达到求解凸包的目的。事实上,我们也只需要对点集中每两个点相连的线段判断其余点是否都处于它的一侧即可。

所以极边法的时间复杂度相对极点法要低一些,遍历所有线段(Cn2=O(n2)C_{n}^{2}=O(n^2)×\times O(n)O(n) 个 toLeft 判断 =O(n3)=O(n^3) 的复杂度,虽然好一些但还不够。

增量法(Incremental Construction)

顾名思义,增量法的主要思路就是遍历点集中的所有点,每次都更新当前已遍历点集的凸包,最后得到对于所有点的凸包。

incremental-construction-1

如上图,在添加新点时会发生三种情况:

  • 该点作为新凸包上的点,且不影响原凸包上的点;
  • 该点在目前凸多边形的内部;
  • 该点作为新凸包上的点,并删除一些原凸包上的点。

那么怎么判断新加入的点属于上面哪种情况呢?我们一点一点看,先来判断新点是否属于原凸包内,也即判断点是否属于凸多边形的内部。

线性解法 —— 逆时针遍历凸多边形的所有边,执行 toLeft 判定。优势是可以应用于链表等动态内存结构,劣势是慢;

二分查找 —— 二分地判定点是否属于两条射线张成的区域之内,如下图。优势是快,劣势是只能应用于数组等静态内存结构。

point-in-convex-polygon

在增量法中,由于我们需要保证能够在常数时间内删除点,所以需要采用链表等结构,那么还是需要使用线性解法。

那么怎么应对刚刚说的第三种情况呢?我们将原凸包分成四部分:

incremental-construction-2

  • 上、下切点(t,st,s);
  • tsts 段,需要被删除的部分;
  • stst 段,需要保留的部分。

incremental-construction-3

那么怎么判断点属于哪种类别呢?如上图,

  • vv 的两个邻域点都分布在 xvxv 的左侧,则 vv 是下切点;
  • vv 的两个邻域点都分布在 xvxv 的右侧,则 vv 是上切点;
  • vv 的两个邻域点(逆时针上、逆时针下)分布在 xvxv 的左右侧,则 vv 属于 tsts 段;
  • vv 的两个邻域点(逆时针上、逆时针下)分布在 xvxv 的右左侧,则 vv 属于 stst 段;

幸运的是,可以用类似的方法判断点是否在凸多边形内,也即判断其余点是否都属于 stst 段。

Jarvis March

该算法的大致思想是逐条选出极边并加入到凸包中,如下图。

jarvis-march-1

如下图,在算法运行中,怎样选取下个点,使得它与当前点 kk 组成的边是下一条极边(ksks)呢?

jarvis-march-2

对于极点 kk,只要找到点 ss ,使得 ksks 的右侧没有任何其他点。与本文一开始提出的第二个小问题类似,以 toLeft 测试为比较函数,找出其余点中最大的那个即可。

不失一般性,第一个极点 oo 可以按照 lowest-than-leftmost 的规则选取。

凸包算法的下界

使用归约法(reduction)说明。关于归约法,维基百科有如下说明:

以直觉观之,如果存在能有效解决问题 B 的算法,也可以作为解决问题 A 的子程序,则将问题 A 称为“可归约”到问题 B,因此求解 A 并不会比求解 B 更困难。

lower-bound-1

上图表示了一个线性归约,如果对于问题 A 的任意输入都可以在线性时间内转换为某个 B 的输入,且对于问题 B 的输出都可以在线性时间内转换为 A 的输出,那么称问题 A 可以线性归约至问题 B。且问题 A 的下界也是问题 B 的下界。

lower-bound-2

考虑基于比较的排序问题,对于一维上的所有输入,可以在线性时间内投影到抛物线 y=x2y=x^2 上。而投影过后的点集的凸包投影回一维上就是排序后的结果。则基于比较的排序问题可以线性归约为二维凸包问题,那么二维凸包问题的下界就是 O(nlog(n))O(n\log(n))

下次将介绍几个 O(nlog(n))O(n\log(n)) 的凸包算法。

Convex Hull - Part 1

· 阅读需 4 分钟

这学期有幸选到了贵系邓俊辉老师的《计算几何》,这学期会随着课程进度更新一些笔记。

凸包

用邓老师的话来说,所谓凸包就是

把点集看作钉在桌子上的若干钉子,这时撑开一个橡皮筋,让它能够囊括所有钉子,松手后橡皮筋围成的多边形就是该点集的凸包

当然这只是一个凸包在二维上的解释,但通俗易懂,如下图。

convex_hull_1

那么给定一个点集 PP,如何计算出其凸包 CH(P)CH(P) 呢?接下来将介绍第一个计算凸包的算法 —— 极点法。

极点法(Extreme Points)

极点

如下图,对于点集中的某个点,若存在一条经过该点的直线,使得点集中的其余点均处于该直线的一侧,则称该点为极点(Extreme Point)。

convex-hull-extreme-points

但根据上述定义很难实现凸包的构建算法,因为对于每个点都要遍历经过它的所有直线,而这些直线是无限的。

对于非极点来说,它必然会被点集中某三个点组成的三角形完全包围(不包括边界),而极点不满足该性质,如下图。

convex-hull-extreme-points-2

所以就可以遍历点集中的所有三角形,将其覆盖的所有点设置为非极点。通过排除所有的非极点就可以得到点集中的所有极点。 该算法的时间复杂度是 O(Cn3n)=O(n4)O(C_{n}^{3}\cdot n)=O(n^4) 的。

那么如何判断点是否在三角形内呢?当然,可以使用射线法或累计角度法判定,但未免有些「杀鸡用牛刀」的意味。考虑边按逆时针排列的三角形,对于这三条有向边,若某点处于它们的左侧(toLeft 判断),则该点被该三角形覆盖,如下图。

convex-hull-in-triangle

通过计算有向面积(×2\times 2)的符号能够判定某点是否在有向边的左侧:

2S=p.xp.y1q.xq.y1s.xs.y1(1)2S=\begin{array}{|ccc|} p.x & p.y & 1 \\ q.x & q.y & 1 \\ s.x & s.y & 1 \end{array} \tag{1}

在得到点集中的所有极点后,再对它们进行排序(O(nlog(n))O(n\log(n)))就可以得到最终结果。

极点法虽然能够计算凸包,但还存在问题,其中最不能使人接受的是其较高的时间复杂度,之后将会介绍一些复杂度相对较低的算法。

Berkeley CS188 学习笔记(3)

· 阅读需 13 分钟

这次的笔记包括马尔科夫决策过程以及增强学习的相关内容。

马尔科夫决策过程

马尔科夫决策过程(Markov Decision Processes,简称MDP)的定义:

  • 状态集合 sSs\in S
  • 动作集合 aAa\in A
  • 转移函数 T(s,a,s)T(s,a,s^{\prime}),表示从状态 ss 执行动作 aa 后达到状态 ss^{\prime} 的概率
  • 收益函数 R(s,a,s)R(s,a,s^{\prime}),表示转移对应的收益
  • 初始状态
  • 终止状态(不必须)

举个例子,在一个网格迷宫中,我们操纵的 agent 将会在迷宫中进行游走。

区别于一般的游戏,它并不会完全按照我们给他的指令进行移动,它有 80% 的概率正确地移动,其余 20% 的概率将会向正确移动方向的左边或者右边(各 10%)进行移动,如果移动方向前方有墙,那么它待在原地不动。为了敦促 agent 尽快找到宝物,在游戏结束前,走的每一步都会略微扣一些分数,当找到宝物后,将会有较大的奖励;反之,如果不幸掉进陷阱,将会有较大惩罚。

在这个 MDP 中,我们可以对 MDP 定义中的一些抽象概念进行具体化,比如转移函数可以写成:

T((3,1),North,(3,2))=0.8T((3,1),North,(3,2))=0.8 T((3,1),North,(2,1))=0.1T((3,1),North,(2,1))=0.1 T((3,1),North,(4,1))=0.1T((3,1),North,(4,1))=0.1

值得注意的是,终止状态并不是可见的,也就是说,当 agent 达到 (4,3)(4,3) 时,不会马上得到分数,而是需要经过一个动作 exit 后达到终止状态。

在 MDP 中,我们需要找出一个最佳策略(policy)π:SA\pi^{*}:S\rightarrow A

  • 对于每个状态,策略 π\pi 都会给出一个动作
  • 使得效益(utilities)最大化的策略被称之为最佳策略

收益(reward)函数对最佳策略的影响见下图:

比较有趣的是,当每一步的收益亏损很大时(右下角),在陷阱附近的 agent 将会倾向于自杀,因为自杀也只不过扣1分,而继续活着将扣2分。

值得一提的是,马尔科夫通常代表,针对当前状态来讲,未来以及过往都是独立的。在MDP中具体来说,可以用下面这个式子来表达:

P(St+1=sSt=st,At=at,St1=st1,At1=at1,,S0=s0)=P(St+1=sSt=st,At=at)P(S_{t+1}=s^{\prime}|S_{t}=s_{t},A_{t}=a_{t},S_{t-1}=s_{t-1},A_{t-1}=a_{t-1},\cdots,S_{0}=s_{0})=P(S_{t+1}=s^{\prime}|S_{t}=s_{t},A_{t}=a_{t})

接下来介绍折扣(discounting)概念。在我们最大化收益的同时,我们倾向于更早地获得收益,以及更晚地获得负收益。所以可以考虑将收益进行指数性减小,每经过一步,收益都会乘以一个因子 γ(0,1)\gamma\in(0,1)

现在考虑对 MDP 的求解。首先定义几个概念:

  • V(s)=V^{*}(s)= 在状态 ss 下开始,进行最优操作所获得的收益期望
  • Q(s,a)=Q^{*}(s,a)= 从状态 ss 进行动作 aa 后,进行最优操作所获得的收益期望
  • π(s)=\pi^{*}(s)= 状态 ss 的最优操作

可以类比之前学过的 Expectimax Search ,通过构造出类似的搜索树,可以得到如下几个公式(Bellman 等式):

  • V(s)=maxaQ(s,a)V^{*}(s)=\max\limits_{a} Q^{*}(s,a)
  • Q(s,a)=sT(s,a,s)[R(s,a,s)+γV(s)]Q^{*}(s,a)=\sum\limits_{s^{'}}T(s,a,s^{'})[R(s,a,s^{'})+\gamma V^{*}(s^{'})] V(s)=maxasT(s,a,s)[R(s,a,s)+γV(s)]\Rightarrow V^{*}(s)=\max\limits_{a} \sum\limits_{s^{'}}T(s,a,s^{'})[R(s,a,s^{'})+\gamma V^{*}(s^{'})]

但如果还用之前的搜索法来对这个问题进行求解,速度就太慢了。所以引入新的算法:价值迭代(value iteration):

  • sSV0(s)=0\forall s\in S\quad V_{0}(s)=0
  • sSVk+1(s)maxasT(s,a,s)[R(s,a,s)+γVk(s)]\forall s\in S\quad V_{k+1}(s)\leftarrow \max\limits_{a}\sum\limits_{s^{'}}T(s,a,s^{'})[R(s,a,s^{'})+\gamma V_{k}(s^{'})]
  • 重做第二步,直到收敛

价值迭代法每一步的时间复杂度为 O(S2A)O(S^{2}A),而且当 γ(0,1)\gamma\in (0,1) 时,算法必然收敛。

在使用价值迭代法得到每个状态的 VV^{*} 值后,怎么得到某个状态下的最佳策略呢?这时我们需要在该状态下计算其所有 QQ^{*} 值,并取使 QQ^{*} 最大的动作作为在该状态下的最佳动作。

价值迭代算法存在着一些问题:

  • 时间复杂度较高
  • 每个状态的最佳策略往往在 VV 值收敛前就已经收敛了

针对上列问题,我们可以对策略进行迭代,该方法称为策略迭代(policy iteration):

  • 策略评估:针对固定策略 π\pi 计算出所有状态的 VV
  • 策略改进:对于每个状态,都找出当前的最优动作,更新策略
  • 重复上两步,直到策略收敛

其中策略评估可以使用迭代法(利用 Bellman 等式,时间复杂度:O(S2)O(S^{2}) 每步),也可以将Bellman等式看做一个线性系统进行求解。

策略迭代方法的运行速度比价值迭代方法快了不少,并且也会收敛到最优策略。

强化学习

强化学习的基本思想如下:

  • 环境会以收益的形式给出反馈
  • agent 的效益由收益函数定义
  • 学习能够最大化收益期望的策略
  • 所有学习过程都基于在游戏探索得到的样本

之所以我们需要探索游戏,是因为游戏(MDP)中的 TT(转移函数)和 RR(收益函数)是未知的。

基于模型的学习

基本思想:

  • 根据经验学习一个大概的模型(估计 TTRR
  • 对这个学习出的 MDP 进行求解

看个例子,

根据输入的策略 π\pi,可以得到若干遍历结果,从而可以对 T,RT,R 进行估计。

无模型学习

被动强化学习(passive reinforcement learning),类似于之前介绍的策略迭代,首先对一个输入的固定策略进行评估,之后对策略进行优化改进。那么问题就是,我们没有 T,RT,R,无法使用之前的方法直接进行策略评估,所以需要尝试新的方法。

直接评估,针对固定策略 π\pi,按照该策略进行游戏,并记录每一步的收益,并最终通过取平均来获得每个状态的 VV 值。看个例子,

比如 V(B)V(B),由于样本中从状态 BB 出发,只有向东走的情况,所以 V(B)=(11+10)+(11+10)2=8V(B)=\frac{(-1-1+10)+(-1-1+10)}{2}=8,其余类似。这个方法很直观,也不需要 T,RT,R 的任何信息,但忽略了状态间的关系,且每个状态必须分开学习,所以需要较长时间去学习。

既然这个方法不行,那么我们又想回上一节策略评估的方法:通过下式进行迭代,

Vk+1π(s)sT(s,π(s),s)[R(s,π(s),s)+γVkπ(s)]V_{k+1}^{\pi}(s)\leftarrow\sum\limits_{s^{'}}T(s,\pi (s),s^{'})[R(s,\pi (s),s^{'})+\gamma V_{k}^{\pi}(s^{'})]

虽然没有 T,RT,R 也就无法使用上式,但我们可以在游戏中进行一系列采样:

samplei=R(s,π(s),si)+γVkπ(si)Vk+1π(s)1nisampleisample_{i}=R(s,\pi (s),s_{i}^{'})+\gamma V_{k}^{\pi}(s_{i}^{'})\\ V_{k+1}^{\pi}(s)\leftarrow \frac{1}{n}\sum\limits_{i}sample_{i}

通过对采样进行平均,从而得到每个状态的 VV 值。

进一步,我们需要将算法调节成迭代算法,这样就不用针对每个状态单独进行估值,而是每次进行游戏都可以使路径中的状态 VV 值迭代逼近真正的值。这种方法被称为时间差分学习(temporal difference learning)。以固定策略进行游戏的情况下,每次迭代都将更新状态的 VV 值,公式如下:

  • sample=R(s,π(s),s)+γVπ(s)sample=R(s,\pi (s),s^{'})+\gamma V^{\pi}(s^{'})
  • Vπ(s)(1α)Vπ(s)+αsampleV^{\pi}(s)\leftarrow (1-\alpha)V^{\pi}(s)+\alpha sample

这个方法很好,但我们不能忘了目标,我们需要找到每个状态的最优动作,也即对于状态 ss,需要找到

π(s)=argmaxaQ(s,a)=argmaxasT(s,a,s)[R(s,a,s)+γV(s)]\pi (s)=\operatorname*{argmax}\limits_{a} Q(s,a)=\operatorname*{argmax}\limits_{a}\sum\limits_{s^{'}}T(s,a,s^{'})[R(s,a,s^{'})+\gamma V(s^{'})]

由于对 T,RT,R 的缺失,导致无法从已有的 VV 值中榨取出最优动作,所以我们应该考虑对 QQ 值进行学习。

主动增强学习(active reinforcement learning),其与被动增强学习的区别在于,学习者需要自己选择策略对游戏进行探索,而不是之前的按照既定策略进行探索然后更新。

Q-Learning,通过自定的策略对游戏进行探索,并更新状态 ss 及对应动作 aaQQ 值:

  • 探索得到一个样本:(s,a,s,r)(s,a,s^{'},r)
  • 考虑旧的估计值 Q(s,a)Q(s,a)
  • 考虑新样本的估计值 sample=R(s,a,s)+γmaxaQ(s,a)sample=R(s,a,s^{'})+\gamma\max_{a^{'}}Q(s^{'},a^{'})
  • 将新旧估计值做加权平均:Q(s,a)(1α)Q(s,a)+αsampleQ(s,a)\leftarrow (1-\alpha)Q(s,a)+\alpha\cdot sample

Q-Learning将会收敛至最优策略,但也有几个附加说明:

  • 需要足够多的探索
  • 学习率 α\alpha 需要逐渐变小(比如 1n\frac{1}{n},但也不能减小得太快)

在Q-Learning中,比较重要的一点是在explorationexploitation中做一个 trade-off 。

一种比较简单的做法就是设定一个阈值 ϵ\epsilon,随后每次做选择时 roll 一个 0 到 1 之间的随机数,如果小于 ϵ\epsilon,则随机选择动作(exploration);否则,选择当前的最优决策(exploitation)。

稍微复杂些的做法是设置一个 exploration function,比如:f(u,n)=u+knf(u,n)=u+\frac{k}{n} ,其中 u,nu,n 分别代表估计值和探索次数,那么改良后的 Q-Learning 中每个样本估计值的表达式将变为:

sample=R(s,a,s)+γmaxaf(Q(s,a),N(s,a))sample=R(s,a,s^{'})+\gamma\max_{a^{'}}f(Q(s^{'},a^{'}),N(s^{'},a^{'}))

这样一来,当 agent 进行探索时,也会更倾向于考虑探索次数少的动作,而且当 nn 越来越大时,kn\frac{k}{n} 一项对 QQ 的影响也越来越小,最终将不会影响最优策略的选择。

然而在实际问题中,会有很多很多的状态,当状态足够多时,我们不可能存储所有的 QQ 值。而且某些状态比较相近,但我们的 Q-Learning 方法仍然将它们视为完全不同的状态。举个例子,

前两个状态,pacman 都是被两只鬼堵在角落里,实质上 pacman 所处的情形差不多。一、三两个状态更是几乎完全相同,只是右上角的一块食物在第三个状态中被吃掉。然而它们都被视为了完全不同的状态,这一点可以被我们用来优化算法。

这样看来,优化方法也可以比较容易想到,也即将状态用特征向量表示出来:fi(s,a),i[1,n]f_{i}(s,a),i\in [1,n]。这里特征的设计就显得尤为重要,好的特征可以将状态空间较好地映射到特征空间。考虑将这些特征进行线性组合:

Q(s,a)=i=1nwifi(s,a)Q(s,a)=\sum\limits_{i=1}^{n} w_{i}f_{i}(s,a)

经过这样的处理,Q-Learning 算法也将变为 Approximate Q-Learning :

difference=[r+γmaxaQ(s,a)]Q(s,a)wiwi+αdifferencefi(s,a)difference=[r+\gamma\max_{a^{'}}Q(s^{'},a^{'})]-Q(s,a)\\ w_{i}\leftarrow w_{i}+\alpha\cdot difference\cdot f_{i}(s,a)

至此,课程的第一部分也就结束了。由于学校这边即将开学,所以博文也要停更一段时间,之后的部分随后有时间再填坑吧。。

Berkeley CS188 学习笔记(2)

· 阅读需 5 分钟

这次笔记将会总结对抗性游戏中游戏策略的决策方法。

在研究决策策略之前,我们需要对游戏的类型做一个限制。首先,这种游戏需要是确定性的,也就是说,任意一个动作都可以让玩家从某一个状态确定地转移至另一个状态。以及,这种游戏需要是一个零和游戏,即,玩家的效益是相反的。

在之前的决策搜索中,状态树一般是长成这样的:

由于只有一名玩家,所以我们只需要最大化节点的值即可。但在对抗性游戏当中,至少有两位玩家,所以对抗性游戏的状态树一般是这样的:

考虑这样一种策略,当自己控制 pacman 时,总是希望选取能让自己效益最大化的那个动作;而对方控制 ghost 时,自己总是做好最坏打算(也就是对面会选取一种让自己的效益最小化的那个动作)。这种策略被称为 Minimax Search 。用数学表达式来说明每个节点的 Minimax Value ,如下:

V(s)=V(s)=

  • maxssuccessor(s)V(s)\max_{s^{\prime}\in successor(s)}V(s^{\prime}) , if under user's control
  • minssuccessor(s)V(s)\min_{s^{\prime}\in successor(s)}V(s^{\prime}) , if under opponent's control
  • known value, if terminal state

只要理解了原理,实现起来也比较简单。伪代码:

这就像一个彻底的 DFS,把状态树彻彻底底地搜索一遍。所以复杂度也与DFS相同。但在实际情况中,几乎不可能把整个状态树全部搜索完,所以需要考虑限制搜索深度。如果搜索一定深度后,仍然没有到达树的底部,那么就需要评估当前状态是好还是坏,这就是评估函数。评估函数好坏与否对于解决问题是很重要的。

在限制搜索深度的同时,还可以对状态树进行剪枝,被称作 Alpha-Beta 剪枝。看一个例子:

当我们在计算节点 nn 的值时,将会遍历其所有子节点,计算它们的值并从中选出一个最小的值作为nn 的值。这时令 aa 为从根节点到 nn 路径中所有选项中最小的那个,如果 nn 的某个子节点的值小于 aa,那么我们将不再考虑 nn 的其他任何子节点,也即将 nn 剪掉。另外一种情况是对称的。根据这个原理,可以将 Alpha-Beta 剪枝的伪代码表示成如下形式:

经过剪枝后的搜索算法的时间复杂度从 O(bm)O(b^{m}) 降至 O(bm2)O(b^{\frac{m}{2}}),也就是说,可以搜索的深度相较于之前的 naive 算法多了一倍!

之前讨论的情况都是在对方也是一个出色的玩家(总是选择最大化自己的效益)下进行的。另外还有可能对方只是采取随机游走或者采取固定策略,如果是这种情况,使用 Minimax Search 就不是一个好的选择。所以考虑引入 Expectimax Search ,思路很简单,就是将 Minimax Search 算法中状态树中的最小值节点(min node)替换成概率节点(chance node),这种节点的值等于子节点值的期望。伪代码如下:

Berkeley CS188 学习笔记(1)

· 阅读需 7 分钟

注,本文系从 https://web.archive.org 抢救而恢复,可能会丢失一些信息。

UCB 的 CS188 课程名为 Introduce to AI,人工智能导论,在 edX 网站中可以进行在线学习,链接:CS188

本文主要是对 Lecture2&3 做一个总结,这两次课着重介绍了对搜索问题的建模方法以及搜索方法。

Search Problem

一个搜索问题由几个部分组成:状态空间(state space)、继承函数(successor function)、开始状态(start state)以及结束测试(goal test)。解决方案则是由一系列动作(action)组成,这些动作可以使开始状态变化至结束状态。

举个例子,先来看这个地图:

Imgur

对于一个旅游问题,比如某人想从城市 Arad 移动至城市 Bucharest 来说,以上提到的几个要素的建模方法如下:

  • 状态空间:地图中所有城市;
  • 继承函数:假设旅行者现在所在的城市为 TT,则继承函数为 S(T)=S(T)= 所有与 TT 邻接的城市;
  • 开始状态:城市 Arad;
  • 结束测试:旅行者是否身处于城市 Bucharest。

进而,对于搜索问题,我们可以构建一个状态空间图,其中每个状态至多只在图中出现一次,继承关系可以用有向边来表示。但对很多问题来说,把整个图存放在内存中是一件几乎不可能的事情(太大),所以转而考虑构建状态空间树,从而进行搜索。

先来看一下搜索算法的框架:

Imgur

接下来的三种搜索方法都是以上面这个框架为基础。在该算法框架中,closed 是一个 set,以保证已经扩展过的状态不会再次被遍历到;fringe 则是一个容器,用来存储搜索中遍历到的状态;而 strategy 则是会影响容器 fringe 的排列方式。

如果说刚刚介绍的那套算法流程看起来比较陌生的话,那么深度优先搜索和广度优先搜索我们就比较熟悉了。对于DFS(Depth First Search)来讲,fringe 和 strategy 所组成的数据结构就是,而对BFS(Breadth First Search)来说,其对应的数据结构则是队列。在 Uninformed Search 中,还有一种搜索方法 UCS(Uniform Cost Search),这种方法与 Dijkstra 算法比较相似,只不过是在这个算法框架内缺少了 relax 操作,也即 UCS 比 Dijkstra 算法略逊一筹。对 Dijkstra 算法比较熟悉的同学可以看出,优先级队列是在这套算法框架中需要使用的数据结构。

这三种搜索方法的一些性质可以罗列如下:

  • DFS
    • 扩张节点个数复杂度
      • O(bm)O(b^{m})mm 为搜索树的高度,bb 为树中所有节点的最大子节点数
    • 空间复杂度
      • O(bm)O(bm)
    • 能否搜索到任意一个解?
      • 如果 mm 有限,则可以
    • 搜索到的解是否最优?
      • 不一定
  • BFS
    • 扩张节点个数复杂度
      • O(bs)O(b^{s})ss 为最浅解在树中的搜索深度
    • 空间复杂度
      • O(bs)O(b^{s})
    • 能否搜索到任意一个解?
      • 可以
    • 搜索到的解是否最优?
      • 除非 cost 均为固定常数,否则不一定
  • UCS
    • 扩张节点个数复杂度
      • O(bC/ϵ)O(b^{C^{*}/\epsilon})CC^{*} 为最优解的cost,ϵ\epsilon 为边的最小cost
    • 空间复杂度
      • O(bC/ϵ)O(b^{C^{*}/\epsilon})
    • 能否搜索到任意一个解?
      • 如果 CC^{*} 有限且 ϵ\epsilon 为正数,就可以
    • 搜索到的解是否最优?
      • 可以

之前所介绍的搜索方法完全没有考虑当前搜索到的状态与最终解的状态之间的距离,比如说某人想去东边的一座城市,在搜索时可能会将西方的城市也进行扩张搜索,然而这是完全没有意义的,这是南辕北辙。所以我们需要人为引入启发式函数来引导搜索方向。

一个启发式函数对当前状态与解状态之间的距离进行估计,具体函数的表达需要每个人自己去估计,比如在寻路问题中可以利用曼哈顿距离、欧氏距离等等。

在A*(A-star)算法中,将边的 cost 与启发式函数相加,得到函数 f=g+hf=g+h。使用 ff 作为优先级队列的key,就能够使用上一节所介绍的搜索算法框架来实现 A* 算法。

为保证 A* 算法的最优性,启发式函数 hh 需要满足性质:启发式函数所估计出的 cost≤ 实际 cost,用数学表达出来即,

0h(n)h(n)0\le h(n)\le h^{*}(n)

其中 hh^{*} 是从当前状态到最近解状态的真实 cost。(admissible)以及,

h(A)h(C)cost(AC)h(A)-h(C)\le cost(A\rightarrow C)

(consistency)

A* 算法将会在扩张更少节点的情况下搜索到最优解,在Pacman游戏中的寻路问题上,A* 算法与贪心法、UCS 的比较如下图:

Imgur

可以看到,贪心法虽然扩张的节点较少,但并没有找到最优解;UCS 虽然找到了最优解,但几乎搜索了整张地图;而 A* 算法在找到了最优解的情况下,只搜索了大概半张地图。

About Ray Tracing & MCPT

· 阅读需 4 分钟

简单说一下上个学期图形学课程做的一些东西,主要是实现了 Ray Tracing 以及 Monte Carlo Path Tracing 的一部分,以及实现了一篇通过神经网络训练辐射度函数的论文。这里就先谈一谈光线追踪的相关东西吧。

我在去年这个时间自己实现了一个光线追踪的 demo,只实现了几个简单的模型,并没有纹理、景深等特性,而且代码结构比较差,不是很 OOP。上个学期我拜读了一下 ppwwyyxx 的code,从代码结构以及各种 C++11 的特性运用上,感觉自己相比于这位贵系同学在大二时的水准差的还是太多了,还是需要再学习一个。这次光线追踪程序的架构很大程度上也是参考了他的架构,但类的内部实现基本还是自己写的,在之前的基础之上增加了比如折射、纹理、景深、软阴影等等特性。效果大概是这样

这里着重说一下景深,这个特性从视觉上理解就是在对焦平面上的物体最清晰,距对焦平面越远,物体越模糊的一种现象。所谓对焦平面即满足如下条件的平面:从成像平面上的一点向光圈追踪的所有光线在经过光圈折射后都会在对焦平面上的某一点相交。也就是说,如果想要模拟这种现象,就需要利用上述性质。首先我们需要在光圈上进行采样(我直接用的随机采样,貌似pbrt有更高级些的采样方法),得到采样点 PiP_{i},作为跟踪光线的起点。再从光圈中点与虚拟屏幕上需要渲染的像素点位置进行连线并延长,找到其与对焦平面的交点 QQ,则 PiQP_{i}Q 即为跟踪光线的方向。

蒙特卡洛路径追踪这一部分呢,主要还是对算法的理解以及重要性采样比较重要(对我来讲)。理解了渲染方程会对算法的实现有很大的帮助。由于我只实现了Lambertian和Phong模型,所以重要性采样也只涉及了针对这两种模型的方法。

先来看 Phong 模型的 BRDF 函数:

fr(x,θi,θo)=kdπ+ksn+22πcosn(α)f_{r}(x,\theta_{i},\theta_{o})=\frac{k_{d}}{\pi}+k_{s}\frac{n+2}{2\pi}cos^{n}(\alpha)

其中 α\alpha 是镜面反射方向与出射方向所成的夹角,nn 是材质的 shininess。以及满足约束 kd+ks1k_{d}+k_{s}\le 1

那么可以分成两步,先根据俄罗斯轮盘赌法则确定光线接下来的采样方式(漫反射/高光反射/被吸收),之后根据不同情况进行重要性采样。对于漫反射部分,概率分布函数以及采样方法如下:

pdf(θi)=1πcos(θi)pdf(\theta_{i})=\frac{1}{\pi}cos(\theta_{i})

wi=(θ,ϕ)=(arccos(u1,2πu2))w_{i}=(\theta,\phi)=(arccos(\sqrt{u_{1}},2\pi u_{2}))

其中 u1,u2u_{1},u_{2} 为属于 [0,1][0,1] 区间的随机数。对于高光反射部分,pdf 以及采样方法:

pdf(θi)=n+12πcosn(α)pdf(\theta_{i})=\frac{n+1}{2\pi}cos^{n}(\alpha)

wi=(α,ϕ)=(arccos(u11n+1),2πu2)w_{i}=(\alpha,\phi)=(arccos(u_{1}^{\frac{1}{n+1}}),2\pi u_{2})

MCPT的效果如下:

github地址:https://www.github.com/nero19960329/RayTracer