普通视图

发现新文章,点击刷新页面。
昨天以前Leo's blog

RISC-V 函数调用约定

作者 Leo
2024年12月1日 00:00

调用函数前后存在旧数据与新数据两种状态,RISC-V 对使用哪些寄存器存储前后状态做了人为规定,这样的一系列规定称为调用约定(Calling Convention)。CS 61C 的补充资料在这方面描述得最为明了,本文主要根据这篇文章对 RISC-V 调用约定的要点做了总结。

基本定义

首先将发起调用的函数称为调用者(caller),将被调用的函数称为被调用者(callee)。注意,一个函数是调用者或被调用者是由其行为决定,当它被其他函数调用时是被调用者,当它调用其他函数时是调用者,两个身份可以先后存在。

其次 RISC-V 约定在一部分寄存器中的内容在调用函数后不会被改变,称为由被调用者保存的寄存器(callee-saved registers),包括 s0 - s11(保存寄存器,saved registers)和 sp。调用函数可能更改另一部分寄存器中的内容,这些寄存器称为由调用者保存的寄存器(caller-saved registers ),包括 a0 - a7(参数寄存器,argument registers)、t0 - t6(临时寄存器,temporary registers)和 ra(返回地址,return address)。

寄存器的功能和约定可以总结如下表:

编号 寄存器 ABI 名称 描述 保存方
0 x0 zero 常数 0 -
1 x1 ra 返回地址 caller
2 x2 sp 栈指针 callee
3 x3 gp 全局指针 -
4 x4 tp 线程指针 -
5 ~ 7 x6 ~ x7 t0 ~ t2 临时 caller
8 x8 s0 / fp 保存 / 帧指针 callee
9 x9 s1 保存 callee
10 ~ 11 x10 ~ x11 a0 ~ a1 函数参数 / 返回值 caller
12 ~ 17 x12 ~ x17 a2 ~ a7 函数参数 caller
18 ~ 27 x18 ~ x27 s2 ~ s11 保存 callee
28 ~ 31 x28 ~ x31 t3 ~ t6 临时 caller

实际案例

从抽象的概念来看比较难以理解,接下来将以上概念代入到几个具体的案例中理解 RISC-V 的调用约定。

调用者的视角

当我们调用一个函数时,被调用的函数对由其保存的寄存器负责,也就是说由被调用者保存的寄存器内容在该函数调用前后不变。但「不变」并不是指该函数无法使用这些寄存器,实际上函数可以使用任何一个寄存器,RISC-V 中的寄存器并没有「使用权限」的概念,只是在函数结束前必须将修改值恢复原状——这个过程我形象地称其为对寄存器中的值负责(preserve)。

将以上过程抽象为黑盒,从调用者的视角来看,当然可以认为被调用的函数不会修改由被调用者保存寄存器中的值。

上述过程可以通过以下代码理解:

addi s0, x0, 5     # 寄存器 s0 的值为 5
jal ra, func       # 调用 func
addi s0, s0, 0     # 不论 func 是什么,s0 的值还是 5

从反面来思考,就会意识到被调用的函数不对由调用者保存的寄存器负责,也就是说在该函数结束后,由调用者保存寄存器中的值是不可靠的垃圾值:

addi t0, x0, 5     # 寄存器 s0 的值为 5
jal ra, func       # 调用 func
addi t0, t0, 0     # t0 中的值是垃圾值!

{warn}在调用函数后,不由被调用者负责寄存器中的值是否发生改变,实际取决于函数的实现,但作为负责编码的工程师,理应将这些寄存器中的值都视为垃圾,不应当依赖垃圾值执行程序。{end warn}

规避垃圾值问题的技巧是,在寄存器中的值变得不可靠前,预先将其中值保存下来:

addi t0, x0, 5     # t0 中的值为 5
addi a0, t0, 10    # a0 中的值为 15,a0 是函数参数

# 调用函数前,调用者需要做的事
addi sp, sp, -8    # 栈指针向下移动
sw t0, 0(sp)       # 将 t0 的值压入栈帧
sw a0, 4(sp)       # 将 a0 的值压入栈帧

jal ra, func       # 调用函数 func
mv s0, a0          # 将函数返回值 a0 中的值存入 s0
mv s1, a1          # 将函数返回值 a1 中的值存入 s1

# 调用函数后,调用者需要做的事
lw t0, 0(sp)       # 从栈帧中弹出原先 t0 的值,并把值写入 t0
lw a0, 4(sp)       # 从栈帧中弹出原先 a0 的值,并把值写入 a0
addi sp, sp, 8     # 栈指针向上移动

# 目前 t0 与 a0 的值都是可靠的,因为它们的值是预先存入栈帧并从中还原的

从上面的代码中可以观察出 2 点,也是调用者视角下的调用约定:

  1. 调用者函数内,在调用函数前后,必须通过栈内存手动维护由调用者保存的寄存器前后一致(如 a0t0);
  2. 调用者函数内,可以任意修改由被调用者保存寄存器而不用担心副作用(如 s0s1)。

被调用者视角

理解调用者视角下的调用约定后,就不难理解被调用者视角下的操作了。直接观察下例:

# 函数正式操作前,被调用者需要做的事
addi sp, sp, -12   # 栈指针向下移动
sw ra, 0(sp)       # 将 ra 的值压入栈帧
sw s0, 4(sp)       # 将 s0 的值压入栈帧
sw s1, 8(sp)       # 将 s1 的值压入栈帧

# 函数正式操作

# 函数正式操作后,被调用者需要做的事
lw ra, 0(sp)       # 从栈帧中弹出原先 ra 的值,并把值写入 ra
lw s0, 4(sp)       # 从栈帧中弹出原先 s0 的值,并把值写入 s0
lw s1, 8(sp)       # 从栈帧中弹出原先 s1 的值,并把值写入 s1
addi sp, sp, 12    # 栈指针向上移动

ret                # 从函数中返回

可以得出类似的 2 点调用约定:

  1. 被调用者函数内,在函数正式操作前后,必须通过栈内存手动维护由被调用者保存的寄存器前后一致(如 ras0s1);
  2. 被调用者函数内,可以任意修改由调用者保存寄存器而不用担心副作用。

总而言之,RISC-V 的调用约定中规定了寄存器的保存方(saver),函数(caller / callee)对其相应寄存器中的内容负责,caller 维护 caller-saved 寄存器,callee 维护 callee-saved 寄存器。因为需要维护寄存器内容,在函数正式操作前都要把需要维护的内容存入栈内存,并在函数操作结束后从中还原。


References

[译] Understanding Incremental Decoding in fairseq

作者 Leo
2024年8月4日 00:00

近来一直在使用 fairseq 做项目,因为其功能较多而源码也比较复杂,光靠官方文档也难以完全理解。ankur6ue 的一篇文章对 fairseq 中的增量解码(Incremental Decoding)操作做了详尽的介绍,于是我节选了其中的一部分,将其译为中文,希望对和我一样在读源码的朋友有所帮助。

推理过程中的增量解码

在语言翻译任务的推理过程,解码器逐步地输出目标语言词汇的概率分布。最简单的翻译算法通过贪心策略直接选择概率最高的目标词,这个方法和训练过程中计算损失函数的方式一致。另一种方法则是保存所有可能的目标序列,再从中选出最小化对数似然的的结果。但这种方法需要基于似然搜索所有的可能序列,同时由于词表大小通常在数百上千的规模,导致计算开销随着序列长度指数上升。

集束搜索(Beam Search)在两种极端策略间取得了平衡,由于网络上已经有许多非常好的教程,本文不在此展开介绍集束搜索的工作原理。因为集束搜索在每个步骤只考虑 \(B\) 个前缀序列,搜索空间就由 \(V\times V\) 下降至 \(B\times V\)(其中 \(B\) 为集束宽度,\(V\) 为目标词表的大小),所以相比暴力搜索的方法显著高效。解码结果缺乏多样性是集束搜索的一项缺陷,因为一条输入序列可能具有多条正确翻译,这项缺陷就会影响翻译任务。针对该问题也提出了许多解决方法,例如 Diverse Beam Search 在标准的集束分数中添加了一个差异项,通过对先前步骤已使用过的词施加惩罚并使用 top-k 随机取样在下一步的生成中随机选出前 k 个最有可能的备选项(代替了集束搜索中永远选择前 \(B\) 个备选项的取样方式),从而产生更多样的结果。

尽管集束搜索比蛮力搜索更高效,但由于每个步骤都要重新计算所有前缀 token(prefix tokens),其计算开销会随着解码序列长度的增加而线性增长。

n

在这个例子中,用 A、B、C 等字母表示 token,在推理时,集束会扩展成由翻译出句子所构成的 batch。如果输入 batch 由 2 个句子构成,同时将集束宽度设置为 3,最终得到 batch 的大小就为 6。在计算过程中,每个集束都作为 batch 中的元素并行计算。

当模型完成前面一部分的 token 的解码计算后,我们就会思考:是否可以重复利用这些计算结果?其实增量解码正是这个思路的实现。增量解码使用名为增量状态(incremental state)的数据结构保存先前计算结果,用于后续的卷积计算。在每个计算步骤中,解码器只需对当前 token 做计算,若是模型中的某些层需要先前 token 的信息(例如卷积层),则从增量状态中取出所需结果。而在编码器的计算过程中,编码器与解码出的目标序列无关,它只在一开始时计算输入序列并产生每个输入字词的编码,这些编码本就会被解码器重复使用。

增量解码如何节省计算开销?

增量解码的具体实现稍有些复杂,希望下图能够帮助读者更好地理解整个过程。我推荐读者尝试使用 Python Debugger 在以下代码中设置断点,相信能够更容易理解每一步所做的操作。

n

{caption}在第(1)步中,input_buffer 中每个卷积模块对应的值都是 None,其内存大小由 beam_size (3)、conv_kernel_width (3) 和 conv_kernel_input_dimension (512) 分配,初始化为 0。{end caption}

我们假设输入 1 条句子,那么 batch 的大小与集束宽度相等(该样例中为 3)。首先,每个集束都由开始标记构成(BOS),因而输入卷积层的嵌入向量相等。

n

{caption}在完成创建和初始化后,input_buffer 如上图所示{end caption}

然后 input_buffer 左移 1 个位置并将输入添加到最后一列。由于 input_buffer 全由 0 填充,左移后并没有明显的变化,不过我们在下一步就会看到它的作用。

n

{caption}在最右侧填入输入列后的 incremental_state 如上图所示,因为在第(1)步中的所有输入 token 都是 BOS token,填入每个集束的输入向量也都相同。{end caption}

接着,输入数据与卷积滤波器做计算,将计算结果传递给后续层——即 GLU 和注意力层。

n

现在我们考虑下一个线性卷积层模块,进入该层的输入来源于前一个模块。正如前一个部分中的卷积层,该卷积层也有自己的增量状态,同样由 0 初始化并填入输入数据。

n

n

和先前一样,输入数据与卷积核做计算并传入 GLU 和注意力层,解码器中的所有结构都重复这一过程,最后的输出就为每个集束的词表概率分布向量。集束搜索算法从该结果中得到最优的 token,用于下一步的计算,在这里用 A、B、C 表示解码结果。

接下来我们考虑在步骤(2)所做的操作,我们目前的集束为

n

再一次重复第一层的卷积操作,由于每个集束的输入 token 不同(A、B 和 C),其嵌入向量也不再相等。此外,由于步骤(1)中的初始化过程,input_buffer 也不再全为 0。

n

接着,input_buffer 左移并将新的输入添加至最后一列。

n

每个集束的 input_buffer 与卷积核做计算后传入到后续层中,与先前步骤中相同。

n

整个过程中,input_buffer 作为内存记录先前步骤给出的结果,用于计算卷积结果。input_buffer 同时节省了计算开销,这一点可以在下一个卷积操作中看出。

n

由于 input_buffer 中保存了先前步骤的输入,在当前步骤中可以直接用于完成卷积计算。最重要的是,先前步骤的输入是再前一个步骤的计算结果,保存的计算结果就避免了解码过程中的重复计算,从而节省计算开销。后续过程与前文所述一样,左移并填入输入,完成卷积计算。

n

为什么需要重排增量状态?

在每一步开始前,generator 都会重新排列解码器和编码器的 incremental_state

n

这是由于集束搜索会导致每个集束中前缀 token 的顺序发生改变,通过一个简单的样例描述这个过程。假设下图是步骤(2)得到的集束状态,其中展示了每个步骤得到的 token 和预测分数。

n

到了步骤(3)时,通过预测分数得到 N、P 和 S,箭头表示了每个结果 token 的来源。

n

(译者案:作者在这里描述得不是很清晰,需要额外补充一些说明。集束搜索过程如下图示意,其主要操作是在每一步中只取 top-k 个预测分数最高的结果作为下一步的前缀 token,其他分支中止不再计算。在该例中,输入 BOS token 后,在若干结果中取 top-3 分数最高的结果,分别为 A、B 和 C。那么下一步的输入就为 [BOS A]、[BOS B] 和 [BOS C],再取 top-3 分数最高的结果。由 A 生成的结果分数无法位于 top-3,A token 所属的分支就被中止,后续不会再计算,在 buffer 中存储其状态也是无用的了,因此要将其替换为有效的前缀 token。)

n

于是就如下图所示,重新排列每个集束。

n

当对当前 token N、P 和 S 执行解码操作(预测下一个 token)时,我们必须重排 incremental_state 使卷积操作能够使用正确的前缀 token。这个操作可能不能马上明白,需要花些时间仔细理解。

另外还有一点,fairseq 的代码也重新排列了编码器的状态,然而由于编码器状态只取决于输入 token,并不会随集束状态改变,其重排也就不是必需的,至少在本文的例子中不需要这样的操作。

为什么集束搜索返回的 token 数量是集束数量的两倍?

在 fairseq 中,集束搜索返回输出 token 的数量是集束数量的两倍。这是由于集束搜索中的部分集束可能会返回表示句子结束的 EOS token,而我们不想要集束搜索太早就停止。当 EOS token 出现在结果的前半部分时,可以将预测总分与其他已有结果的分数相比较从而完成句子。下图展示了相关代码并附上了一些注释,希望能有助读者理解。

n

{caption}(a)返回表示集束中具有 EOS token 的掩码;(b)具有 EOS token 集束对应的索引,只有在 EOS 出现在前半部分的情况下(:beam_size)。注意集束搜索返回 2 * beam_size 个结果;(c)对于前 beam_size 个具有 EOS 的集束,组合预测结果并判断是否完成句子。如果是,减少剩余句子的数量,注意我们处理的是一整个 batch 的输入句子;(d)如果剩余句子的数量是 0,完成;(e)如果能够完成一整个 batch 的目标句子,从 batch 中移除元素并调整 batch 索引。{end caption}

华为云踩坑:由 URL 编码导致 yum 安装时的 No such file or directory 错误

作者 Leo
2024年3月15日 00:00

最近想要在华为云的 BMS 上部署一个 Web 应用,咨询了华为的工程师后,得到了可行的明确答复。那么首先就需要安装 Nginx,在安装 Nginx 所需的依赖遇到了 [Errno 2] No such file or directory 的错误,一层层查找后发现这可能是一个由代理设置引起 URL 编码错误而导致的 bug。由于网络上几乎没有找到任何相关的资料,就把整个过程留下来作为记录。

首先从安装 Nginx 所需的依赖开始:

$ sudo yum -y install gcc gcc-c++ make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel
...
Error:
 Problem: package pcre-devel-8.32-15.1.h6.aarch64 requires libpcre16.so.0()(64bit), but none of the providers can be installed
  - package pcre-devel-8.32-15.1.h6.aarch64 requires libpcre32.so.0()(64bit), but none of the providers can be installed
  - package pcre-devel-8.32-15.1.h6.aarch64 requires libpcrecpp.so.0()(64bit), but none of the providers can be installed
  - cannot install both pcre-8.32-15.1.h1.aarch64 and pcre-8.42-4.h3.eulerosv2r8.aarch64
  - cannot install both pcre-8.32-15.1.h6.aarch64 and pcre-8.42-4.h3.eulerosv2r8.aarch64
  - cannot install the best candidate for the job

竟然提示没有找到可用的包,按理来说 BMS 已经配置了华为官方的源,既不会有网络问题也不会缺失 pcre-devel 这样的常见包。于是我开始排查 yum 源的问题,先检查设备的系统和架构:

$ uname -m
aarch64
$ cat /etc/os-release
NAME="EulerOS"
VERSION="2.0 (SP8)"
ID="euleros"
ID_LIKE="rhel fedora centos"
VERSION_ID="2.0"
PRETTY_NAME="EulerOS 2.0 (SP8)"
ANSI_COLOR="0;31"

可以看到操作系统是华为的 EulerOS 2.0 (SP8),架构是 aarch64。再检查默认的 yum 源:

$ sudo cat /etc/yum.repos.d/EulerOS.repo
[euler-base]
name=EulerOS-2.0SP8 base
baseurl=http://mirrors.huaweicloud.com/euler/2.3/os/aarch64/
enabled=1
gpgcheck=1
gpgkey=http://mirrors.huaweicloud.com/euler/2.3/os/RPM-GPG-KEY-EulerOS

仔细看默认的 yum 源,系统版本明明是 2.0 (SP8),URL 却指向了 2.3。测试直接替换后的链接 http://mirrors.huaweicloud.com/euler/2.8/os/aarch64/ 可达,那么就尝试改为使用该 yum 源。

更换 yum 源

这里的修改很简单,我就没有额外备份,使用 vim 直接编辑文件 /etc/yum.repos.d/EulerOS.repo 并保存,修改后的 yum 源信息为

[euler-base]
name=EulerOS-2.0SP8 base
baseurl=http://mirrors.huaweicloud.com/euler/2.8/os/aarch64/
enabled=1
gpgcheck=1
gpgkey=http://mirrors.huaweicloud.com/euler/2.8/os/RPM-GPG-KEY-EulerOS

通过以下命令更新 yum 源:

$ sudo yum clean all            # 清除旧 yum 源缓存
$ sudo yum makecache            # 生成新 yum 源缓存
$ sudo yum repolist             # 检查 yum 源连接状态
EulerOS-2.0SP8 local repo for internal use          0.0  B/s |   0  B     00:00
EulerOS-2.0SP8 base                                 7.4 MB/s |  17 MB     00:02
Failed to synchronize cache for repo 'base', ignoring this repo.
Last metadata expiration check: 0:00:06 ago on Fri 15 Mar 2024 09:54:47 AM CST.
repo id                           repo name                              status
euler-base                        EulerOS-2.0SP8 base                    16,599

上述信息中的 Fail 指示 EulerOS-2.0SP8 local repo for internal use 这个源不可用,看名字应该是内部使用的 yum 源,在此处没有影响。列举出的 repolist 中有 EulerOS-2.0SP8 base 一项,说明更改后的源已经可用。

[Errno 2] No such file or directory

再次尝试安装 Nginx 的依赖:

$ sudo yum -y install gcc gcc-c++ make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel
...
(19/29): gcc-c++-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm         6.2 MB/s | 7.4 MB     00:01
...
[Errno 2] No such file or directory: '/var/cache/dnf/euler-base-85cc05102200a8ac/packages/gcc-c++-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm'
The downloaded packages were saved in cache until the next successful transaction.
You can remove cached packages by executing 'dnf clean packages'.

提示 [Errno 2] No such file or directory,没有找到 gcc-c++ 的 RPM 文件,奇怪的是在安装输出的信息中分明提示已经成功下载了 gcc-c++

错误信息中指引了一个文件目录 /var/cache/dnf/euler-base-85cc05102200a8ac/packages/,不妨检查一下其中的文件:

$ sudo ls /var/cache/dnf/euler-base-85cc05102200a8ac/packages/
cpp-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm                  make-4.2.1-10.h3.eulerosv2r8.aarch64.rpm
gcc-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm                  openssl-1.1.1-3.h31.eulerosv2r8.aarch64.rpm
gcc-c%2b%2b-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm          openssl-devel-1.1.1-3.h31.eulerosv2r8.aarch64.rpm
gcc-gfortran-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm         openssl-libs-1.1.1-3.h31.eulerosv2r8.aarch64.rpm
keyutils-libs-devel-1.5.10-8.h4.eulerosv2r8.aarch64.rpm         pcre2-devel-10.32-3.h1.eulerosv2r8.aarch64.rpm
krb5-devel-1.16.1-21.h1.eulerosv2r8.aarch64.rpm                 pcre2-utf16-10.32-3.h1.eulerosv2r8.aarch64.rpm
libcom_err-devel-1.44.3-1.h4.eulerosv2r8.aarch64.rpm            pcre2-utf32-10.32-3.h1.eulerosv2r8.aarch64.rpm
libgfortran-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm          pcre-8.42-4.h3.eulerosv2r8.aarch64.rpm
libgomp-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm              pcre-cpp-8.42-4.h3.eulerosv2r8.aarch64.rpm
libkadm5-1.16.1-21.h1.eulerosv2r8.aarch64.rpm                   pcre-devel-8.42-4.h3.eulerosv2r8.aarch64.rpm
libselinux-devel-2.8-4.h2.eulerosv2r8.aarch64.rpm               pcre-utf16-8.42-4.h3.eulerosv2r8.aarch64.rpm
libsepol-devel-2.8-2.eulerosv2r8.aarch64.rpm                    pcre-utf32-8.42-4.h3.eulerosv2r8.aarch64.rpm
libstdc%2b%2b-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm        zlib-1.2.11-14.h4.eulerosv2r8.aarch64.rpm
libstdc%2b%2b-devel-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm  zlib-devel-1.2.11-14.h4.eulerosv2r8.aarch64.rpm
libverto-devel-0.3.0-6.h1.eulerosv2r8.aarch64.rpm

可以看到下载的 29 个文件都在其中,从中寻找报错的 gcc-c++,看到文件名时恍然大悟,gcc-c++ 被转义成了 gcc-c%2b%2b。同样带有 +libstdc++libstdc++-devel 两个安装文件也都被用 %2b 转义,用未转义前的名称自然无法寻找到这些文件。

起初以为这是 yum 在处理特殊符号时 URL 编码的 bug,但在互联网上用关键词检索找不到任何相关的信息。仔细一想,yum 是无数 Linux 平台上默认的包管理器,怎么可能犯这么低级的错误,况且在安装这么常见的依赖时就能引发的 bug 理应很快就被修复了。

在许久漫无目的地寻找后,偶然发现了 GitHub 上的一篇 Issue,大意是说代理软件应当支持识别 yum.conf 中的 URL 编码,否则会导致一些问题。这倒提醒了我,会不会是代理导致的问题呢?

修改 yum 源代理

设备可能带有华为用来日常管理维护设备的内部默认代理,不宜擅自修改,最好是仅修改 yum 源所使用的代理,不影响其他服务的运作。同样用 vim 修改 /etc/yum.repos.d/EulerOS.repo 文件的内容,仅在最后添加一行:

[euler-base]
name=EulerOS-2.0SP8 base
baseurl=http://mirrors.huaweicloud.com/euler/2.8/os/aarch64/
enabled=1
gpgcheck=1
gpgkey=http://mirrors.huaweicloud.com/euler/2.8/os/RPM-GPG-KEY-EulerOS
proxy=_none_

然后用同样的操作尝试更新 yum 源:

$ sudo yum clean all
$ sudo yum makecache
EulerOS-2.0SP8 local repo for internal use                          0.0  B/s |   0  B     00:00
EulerOS-2.0SP8 base                                                 0.0  B/s |   0  B     00:01
Failed to synchronize cache for repo 'base', ignoring this repo.
Failed to synchronize cache for repo 'euler-base', ignoring this repo.
Metadata cache created.

发现禁止源 EulerOS-2.0SP8 base 使用代理后,就无法连接上源仓库了。可以确定华为云上的 BMS 确实设置有供 yum 安装所使用的特殊代理,文件名的 URL 编码异常可能由该代理导致,从而引起 [Errno 2] No such file or directory 的错误。

解决方案

由于华为云 BMS 获取 yum 源仓库必须通过默认代理,不能通过取消代理解决该问题。那么就只能通过最朴素、最直接的方法解决这个问题了——手动改文件名。注意核对 cache 文件目录,手动将文件名中的 %2b 改回为 +,我这里有 gcc-c%2b%2b libstdc%2b%2b libstdc%2b%2b-devel 三个文件需要修改:

$ cd /var/cache/dnf/
$ sudo mv ./euler-base-85cc05102200a8ac/packages/gcc-c%2b%2b-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm ./euler-base-85cc05102200a8ac/packages/gcc-c++-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm
$ sudo mv ./euler-base-85cc05102200a8ac/packages/libstdc%2b%2b-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm ./euler-base-85cc05102200a8ac/packages/libstdc++-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm
$ sudo mv ./euler-base-85cc05102200a8ac/packages/libstdc%2b%2b-devel-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm ./euler-base-85cc05102200a8ac/packages/libstdc++-devel-7.3.0-20190804.h29.eulerosv2r8.aarch64.rpm

然后再尝试原先的安装命令,就发现先前提示无法找到的安装包能够成功安装上了。


References

为友治藏书印一方

作者 Leo
2024年1月14日 00:00

上次动刀还是在很久很久以前了。早先时候朋友向我抱怨白文的藏书印不容易干,稍不慎就把书印成了「大花脸」。我也不自揣,心想索性为他刻一方朱文的藏书印。因为日常各种事务迁延,这想法一直拖到了最近都还没开始动手,再拖下去就要到年后了,想到那时的忙闲也未可知,赶紧挤着时间操起刀来。要没有这样的「任务」记挂在心头,每天忙忙碌碌后谁又会有心思去做这些无用之事。

让我刻印不是朋友的主意,自然也没有交待我文字内容,我就自作主张以他的斋号起首,下缀以「收藏经籍记」几个字,刻一方扁章。虽说是斋号,天下广厦千万间尚没有容身之所,哪里还有这额外的书斋。

明清文人有个说法,大意是「书斋大都建在石头上」。这不是说以石料来架梁构椽,而是说凡有书斋必有斋馆印,哪怕是在心头上的书斋,也需有斋馆印作为凭信。所以我也可以戏言,有了这方印,朋友的书斋才总算是「建」起来了。

这样的重任在肩,我不敢懈怠。于是费心经营结构,又操刀几日,终于完成了这方藏书印。藏书印还是以朱文最好,不易遮盖文字,也不易污损纸面。扁章具有独特的书卷气,盖在书上也很是典雅。这方印基本能够满意,但也有几处毛病欠推敲。日前已经将藏书印转交了朋友,第一次创作这样风格的扁章,用照片给自己留个纪念。

n

n

把博客站点交给了 Cloudflare 托管

作者 Leo
2023年10月31日 00:00

因为博客域名是在阿里云购买的,先前一直顺理成章地用着阿里云的 DNS 解析。阿里云的 DNS 解析在各方面的体验都很不错,例如修改配置后就能很快更新、配置平台访问速度快、站点不会被国内的运营商污染等等,这些优点反过来可是说尽是 Cloudflare 的缺点。

但由于 Cloudflare 为网站提供的各种免费服务十分诱人,加之我想利用 Cloudflare 的 CDN 搭建博客图床,终究是把站点交给了 Cloudflare 管理。本文记录了从阿里云迁移站点的过程和一些必要的 Nginx 配置。

Cloudflare 注册站点

打开 Cloudflare 官网,注册帐号后选择添加站点,输入域名后点击继续。

Cloudflare

按需选择计划,对于普通的小站点来说,Free 计划足矣。点击继续后,Cloudflare 会检测站点目前已有的部分 DNS 记录,其余未检测出的记录日后再手动添加,最关键的是检查域名指向服务器 IP 地址的 A 记录是否正确。

DNS records

在「代理状态」一列可以选择该 DNS 记录是否使用 Cloudflare 的 CDN,激活后图标显示一朵黄色的云。Cloudflare 的 CDN 在国内速度很慢,一直被称为减速 CDN,所以我都选择「仅 DNS」。此前我也担心 Cloudflare 的 DNS 解析会不会也像其 CDN 一样龟速,幸好解析速度并不慢,我的担心是多虑了。

提交 DNS 记录后,Cloudflare 会提示删除阿里云的 DNS 服务器,以 Cloudflare 的 DNS 服务器代替之,接着就转到阿里云的控制中心操作。

更换 DNS 服务器

登录阿里云,进入控制台。在云解析 DNS - 域名解析下找到迁移的域名,在解析设置中保存了站点的 DNS 记录。将记录备份,后续要将所有记录导入 Cloudflare。站点交由 Cloudflare 解析后,阿里云中的解析设置也会失效,所以也在解析设置中将所有解析都停用。

aliyun DNS records

在阿里云控制台中来到域名控制台 - 域名列表,选择域名的管理 - DNS 管理 - DNS 修改 - 修改 DNS 服务器,将 Cloudflare 提供的两个 DNS 服务器地址填入其中。

DNS server

修改 DNS 服务器一般需要 24-48 h 生效,生效后 Cloudflare 会发送邮件通知。如果迟迟没有收到邮件,也可以到 Cloudflare 手动验证网站。验证成功后 Cloudflare 会指引是否开启 Brotli 压缩等功能,按需选择即可。至此,站点已经交由 Cloudflare 托管。如果站点是由 Nginx 搭建的,那么就还需要考虑 Nginx 的 SSL 设置是否与 Cloudflare 兼容。

Nginx 中的 SSL 相关配置

Cloudflare SSL

在 Cloudflare 的 SSL/TLS 设置界面可以看到,用户访问由 Cloudflare 托管的站点的过程中有 3 个实体,根据实体间通信安全等级的不同可以分为 4 种模式:

  1. 关闭:浏览器-Cloudflare 间和 Cloudflare-服务器间都使用 HTTP;
  2. 灵活:浏览器-Cloudflare 间使用 HTTPS,Cloudflare-服务器间使用 HTTP;
  3. 完全:浏览器-Cloudflare 间和 Cloudflare-服务器间都使用 HTTPS,需要 SSL 证书;
  4. 完全(严格):浏览器-Cloudflare 间和 Cloudflare-服务器间都使用 HTTPS,需要非自签名 SSL 证书。

现在的站点一般都使用了 HTTPS,还在使用 HTTP 的站长快去申请个 SSL 证书吧,同时通过 Nginx 将访问 80 端口的 HTTP 流量强制重定向到 HTTPS 入口。若使用这样的 Nginx 配置又开启的「灵活」模式,用户发起访问请求后,Cloudflare 使用 HTTP 交由 Nginx,Nginx 告知用户重定向为 HTTPS,但Cloudflare 仍使用 HTTP 与 Nginx 通信,该过程无限循环,出现 301 重定向次数过多

为了保证站点的安全性和避免以上问题,推荐配置好站点的 HTTPS 后,在 Cloudflare 的 SSL/TLS 中使用完全或完全(严格)两种模式。

最后附上我的 Nginx 配置供参考:

server {
    listen                              443 ssl http2;
    server_name                         leonis.cc;
    root                                /home/Leo/web/blog;

    # SSL 配置
    ssl_certificate                     /etc/nginx/cert/leonis.cc.cer;
    ssl_certificate_key                 /etc/nginx/cert/leonis.cc.key;
    ssl_session_timeout                 5m;
    ssl_ciphers                         ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols                       TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers           on;

    location / {
        index index.html;
    }
}

server {
    listen                              80;
    server_name                         leonis.cc
    # 重定向至 HTTPS,开启 Cloudflare 完全模式后不会访问 80 端口,也不会用上此处的重定向
    rewrite ^/(.*)$ https://leonis.cc:443/$1 permanent;
}

后记

Cloudflare 总体来说还是很好用的,提供了很多有意思的功能,很便利地就能体验,免去了自己动手配置的烦恼。Cloudflare 的不足仅在于在国内有时访问不畅,添加 DNS 记录后也要等比较长的时间才会更新到国内网络上,若能接受这两点,Cloudflare 的可玩性还是比其他平台更高的。

文献总结|结构诱导的预训练

作者 Leo
2023年6月23日 00:00

doi.org/10.1038/s42256-023-00647-z

本文介绍于 2023 年 MIT 研究团队在 Nature Machine Intelligence 发表上的一篇文章,文章原标题为 Structure-inducing pre-training,文章调查了目前广泛应用的多种预训练模型,设计了一种通过图结构在预训练过程中引入显式且深层结构约束的方法。

预训练-微调的学习模式在自然语言处理及其他相关领域都已经得到广泛的应用,预训练通过在隐空间中提取样本的特征,从而提升模型在下游任务上的表现。但目前的预训练模型都没能在潜变量 \(\boldsymbol{z}\) 上添加结构约束,从而获得既显式又深层的特征,这是目前预训练模型的一大缺陷。

方法

对于数据集 \(\boldsymbol{X}_\mathrm{PT}\in\mathcal{X}^{N_\mathrm{PT}}\),预训练的目标就是从学习过程中得到编码器 \(f_\theta:\mathcal{X}\rightarrow\mathcal{Z}\),然后将 \(f_\theta\) 用于各种各样的下游任务。

显式和深层结构约束

  • 显示结构约束:如果能从隐空间 \(\mathcal{Z}\) 中的两个样本 \(\boldsymbol{z}_i\)\(\boldsymbol{z}_j\) 直接推导出两者间的关系(如距离),那么该预训练过程就有显示的结构约束。
  • 深层结构约束:预训练过程中所使用的信息越多(如维数),那么预训练过程所使用的结构约束越深。

目前大部分的预训练模型都无法同时保证显式与深层的结构约束,调查目前超过 90 种的预训模型,其方法可以分为以下几类:

  1. 完全不使用样本间的关系,例如 prompt 训练,主要用于文本生成。
  2. 使用显式,但浅层的监督预训练目标,例如 BERT 的 Next Sentence Prediction 训练模式。
  3. 使用深层,但隐式的无监督或自监督预训练目标,例如通过添加噪声的数据强化方法。

模型

n

因此文章设计了一种同时使用显式与深层的结构约束的预训练框架,称这种方法为结构诱导的预训练。

首先将预训练问题表示为图 \(G_\mathrm{PT}=(V,E)\),其中结点 \(V\) 表示 \(\boldsymbol{X}_\mathrm{PT}\) 中的预训练样本,\(E\) 表示预先定义的样本间关系。

接着预训练的损失函数就定义为

$$ \mathcal{L}_\mathrm{PT}=(1-\lambda_\mathrm{SI})\mathcal{L}_\mathrm{M}+\lambda_\mathrm{SI}\mathcal{L}_{SI} $$

其中 \(\mathcal{L}_\mathrm{M}\) 为传统预训练模型所使用的损失函数,\(\mathcal{L}_\mathrm{SI}\) 是定义用于实现结构诱导目标的损失函数,使隐空间的各潜变量满足 \(G_\mathrm{PT}\) 中的边(样本间关系)。

数据

文章使用了 3 类数据用于预训练:

  • Proteins:来自 Stanford tree-of-life 数据集约 150 万条蛋白序列
  • Abstracts:来自 Microsoft Academic Graph 数据集约 650,000 篇的生物医学相关的文本摘要
  • Networks:来自文献的 70,000 条蛋白-蛋白相互作用网络的子图

Proteins 与 Abstracts 预训练的编码器是 Transformer 架构,Networks 预训练所使用的模型是具有图同构网络(Graph Isomorphism Network, GIN)编码器的图卷积神经网络(graph convolutional neural network, GNN)。

结果

n

预训练模型在下游任务上的测试结果如上图所示,Δ 一列中以 ↑ 表示相对传统预训练模型性质的提升,可以看出不管是相对于 per-token 还是 per-sample 的传统预训练策略,文中提出的结构诱导的预训练方法(structure-inducing pre-training, SIPT)在各下游任务上具有更好的表现。

分析 Networks 任务得到的各种预训练模型在下游任务中微调的过程,SIPT 方法相比其他预训练方法得到的特征能够更快收敛,且在最后得到更好的效果。

n

结论

文章调查了多种预训练模型,分析其训练目标发现大多数都没有引入显式且深层的结构约束,文章设计了一种预训练策略 SIPT,通过预训练图 \(G_\mathrm{PT}\) 在隐空间中加入了显式且深层的结构约束,相比于传统的预训练方法,这种策略在下游任务的层次上提升上模型表现。

文章借鉴了图结构来对样本与样本间的关系建模,但文中并未对得到「显式且深层」的特征做详尽的研究,只能推测这种方法更适用于蛋白-蛋白相互作用等更关注于样本间关系的任务,还不能证明 SIPT 得到的例如分子表示比传统预训练方法得到的分子表示更好。

文献总结|探测图表示

作者 Leo
2023年6月2日 00:00

doi.org/10.48550/arXiv.2303.03951

本文介绍于 2023 年 德国亥姆霍兹信息安全中心研究团队发表在 AISTATS 2023 上的一篇文章,文章原标题为 Probing Graph Representations,文章设计了多种分子表示的探测模型,并通过探测模型研究了图模型在预训练后所编码分子信息。

随着基于图的深度学习模型不断出现,亟需回答的一个问题是「图模型将什么信息编码进了表示中?」为了研究这一问题,文章构建了探测模型测试预训练图模型得到的分子表示。

探测图表示的思路很简单,如果能从图模型输出的分子表示中提取出分子性质,那么就可以认为该性质被编码进分子表示中,所以文章的工作流程是「预训练-预测」(略不同于「预训练-微调」)。通过该流程,文章测试了传统 GNN 与基于 Transformer 的图模型等不同架构、不同数据集、不同优化算法等因素对于模型编码得到的潜变量的影响。

方法

在分子性质预测中,对于分子 \(\boldsymbol{x}\) 与其性质 \(y\),完成该任务的模型就是映射 \(f:\boldsymbol{x}\mapsto y\)。取出 GNN 或图 Transformer 模型中 \(d\) 维的 \(l\) 层输出 \(f_l(\boldsymbol{x})=\boldsymbol{z}\),该潜变量 \(\boldsymbol{z}\) 可以作为输入 \(\boldsymbol{x}\) 的一种表示,进一步得到 \(y\)

文章使用不同的图模型得到分子表示,再通过另一模型测试分子表示 \(\boldsymbol{z}\) 预测分子性质 \(y\) 的性能,从而对比不同图模型提取特征信息的能力。

所构建的预测分子性质任务包括较为基础的判断是否具有某些官能团、更高层次的毒性、血脑屏障渗透性等。

探测策略

  1. 线性探测(Linear Probing):使用最简单的线性层,将分子表示映射为分子性质。
  2. 贝叶斯探测(Bayesian Probing):互信息可以用于 \(Z\)\(P\) 两个随机变量之间的依赖程度,文中通过计算潜变量与分子性质间的贝叶斯互信息进行评估。
  3. 成对探测(Pairwise Probing):将结构相近而性质差异大的分子构成一对 \((\boldsymbol{x}_i,\boldsymbol{x}'_i)\),通过主成分分析等方法分子潜变量与分子性质之间的关系。

实验

n

首先使用线性模型用 \(\boldsymbol{z}\) 预测了分子中是否具有某种子结构,结果如上图所示,基于 Transformer 的一类图模型显然具有比 GCN 和 GIN 具有更好的表现,同时 GCN 模型得到的表示又比以 Morgan 指纹作为分子表示更好。

n

在更高层次的分子性质数据集上测试各种分子表示,结果如上图所示,以 Morgan 指纹作为分子表示的任务效果比部分图模型更好,Morgan 指纹作为一种可以简单获得的分子表示,仍然适合用于许多机器学习模型中完成预测任务。

基于 Transformer 的图模型在更高层次的分子性质数据集上同样具有更好的表现,是具有潜力的新一代分子表示方式。这一点也可以从下图中看出,在左图中,基于 Transformer 图模型的结果都位于右上角,既能表示低层次的子结构信息,也能有效编码高层次的分子性质信息,而其他分子表示则位于左下角。右图使用贝叶斯互信息评估了样本数量与 \(Z\)\(Y\) 之间的依赖程度的关系,就整体趋势而言,仍然是基于 Transformer 图模型效果更好。

n

最后,文章通过主成分分析评估了相似分子间不同的分子表示,两个相似分子仅在官能团上有所不同,文中选择的官能团为硝基。结果如下图左侧一列所示,with FG 表示含硝基分子,w/o FG 表示去除该官能团的分子,可以明显看出,相比于 GCN,GraphGPS 这一基于 Transformer 的图模型所产生的特征中,两种结构相似的分子也具有较大的区分子,是更好的分子表示。

n

结论

文章设计探测模型研究了图模型在预训练后编码的分子信息,最终发现相比于使用消息传递聚合信息的传统 GNN 模型,基于 Transformer 的图模型能够学习到更多与化学相关的化学信息,得到更好的分子表示。文章中提出的分析方法为预训练模型的测试以及分子表示的评估提供了指导。

旧书市场淘书记

作者 Leo
2023年5月22日 00:00

不得不说,在北方诸多城市中,天津的二手旧物市场可以说是相当火热的。我猜测的原因有二,一则是天津的老龄人口占比多,古玩旧物收藏有很大的受众;二则是得益于近代天津经济、文化的繁荣,许多官商士绅定居在此,天津民间仍流通有十分具有价值的骨董。

我对古玩是一窍不通,再加之俚谚「多看少买」的教育,更有许多低劣到我都能看出的赝品,常引得我在心中暗笑,所以我对那些地摊上的古玩也一点不感兴趣。但旧物中有一门类却是我的心头好,那就是旧书。

我一向认为书应当是用来读的,次之才是历史等其他价值。由于许多好书由于各种原因不再出版了,或是更改了原来的版本,没有旧版本更好读了,于是有了「藏书」的群体去搜罗这些旧书。所以「藏书」的「藏」不该是像对待金银珠宝那样的「秘藏」,而是作「保存」解。这是我的藏书主张,也是我搜集旧书的信条。

之所以提及以上原则,还是因为天津旧货市场上的旧书实在太多了,近乎可以同逛新书店一般,不带任何想法去,抱回好几摞的书,为了避免这种无谓的金钱开销,必须要有筛选的准绳。前些天在反复告诉自己买书是为了读书后,终于敢大胆淘了几本书,对其中几本实在喜欢得紧,也算小有收获。

《且介亭杂文》《且介亭杂文末编》

且介亭杂文

人文社 1973 年出版的鲁迅作品集应该是最优良的鲁迅作品版本,因为印量大,价格也不贵,但有几本很少见,凑齐全套并不容易。所以我一般是遇见了品相较好且为手中所无才购买,一切都随缘,并不特意搜集。

且介亭杂文内枼

此一册《且介亭杂文末编》,是我在一堆未经整理的书堆中翻找出来的,售 5 元。人民文学出版社 1973 年 4 月北京 1 版 1 印,扉枼钤「天津市第一机械工业学校图书舘藏书」,内枼整洁,纸张坚韧泛黄。唯一不美的是封面钤「不外借」圆印且封面有墨渍污损。

且介亭杂文扉枼

此一册《且介亭杂文》,由我在另一书摊上访得,要价 4 元。人民文学出版社 1973 年 6 月山西 1 版 1 印,扉枼有 82 年的购书识记,内枼整洁,纸张洁白,可惜曾遭水浸,整册书都有湿后的压痕。鲁迅冠以「且介亭杂文」为名的集子共有 3 册,那么我还差一册《且介亭杂文二集》就可成一小帙了。

《唐诗选》《汉魏六朝诗选》

唐诗选与汉魏六朝诗选

人文社的文学类古籍也具有口碑,可这一套《中国古典文学读本丛书》让我颇为困惑。这一套丛书中的书籍都具有类似的封面和题签,古雅简洁,装帧精美,而且编者与注者都是各领域的权威,内容也很精良,我十分喜欢。可是这一套丛书中兼有简体横排本和繁体竖排本,例如《唐诗选》和《汉魏六朝诗选》就都是简体横排,对简体横排介怀者在挑选这一套书时务必留意。能翻看时一看便知,但有时书商将书用塑料纸包装起来,不允翻看内枼,这时可以根据书口方向分辨,书口向右者为简体横排,书口向左者为繁体竖排本。在读古典文学时,我当然更喜欢用繁体竖排,这套丛书夹杂的两种版本让我困惑又纠结。

此一册《唐诗选(上)》,中国社科院文学研究所编,全套为上下两册,因此仅售我 5 元,待有机会再访下册,这套《唐诗选》印量很大,可货比三家,寻找品相好,更适合翻阅的版本。人民文学出版社 1978 年 4 月北京 1 版 1 印,内枼整洁,纸张泛黄。许多人认为这套集子选的诗并不好,但我只以其简体横排为遗憾。唐诗存世量极大,唐朝诗人又如群星璀璨,无论怎么选都有顾此失彼之嫌,这套集子已经尽可能选出唐朝代表性诗人的作品,注释详略得当,限于篇幅可能未选许多代表作,但对于业余的爱好者概览唐诗完全是足够的了。

汉魏六朝诗选内枼

此一册《汉魏六朝诗选》,余冠英选注,可能由于印量少些,再加上这家书商的眼光比其他家更利,竟要价 10 元,不过余冠英的注本,加之品相不错,这个价格并不亏。人民文学出版社 1979 年 3 月北京 1 版 2 印,内枼整洁,纸张微黄。

《杜甫诗选》《宋诗选注》

杜甫诗选与宋诗选注

同样是《中国古典文学读本丛书》,《杜甫诗选》和《宋诗选注》都是繁体竖排,版式相同,老铅字实在赏心悦目,我十分钟爱这两本。

此一册《杜甫诗选》,冯至选,要价 10 元,还价不允,无奈购下。人民文学出版社 1987 年北京 1 版 11 印,内枼整洁,纸张洁白,摩挲纸面铅字凹痕明显,字画如新,真令人边不释手。《杜甫诗选》并不是最好的杜甫诗集,但又是读杜诗难以绕开的选本。

杜甫诗选内枼

此一册《宋诗选注》,钱锺书选注,售 2 元,可以说是捡到的最大漏。人民文学出版社 1982 年 7 月北京 1 版重庆 1 印,内枼整洁,纸张洁白柔韧,字画清晰,惜其封面有折痕。这本集子是由钱锺书选、钱锺书选的宋诗,读宋诗的人可能不多,但是这本集子是宋诗最好的选注本。

宋诗选注内枼

《唐宋詞選釋》

唐宋詞選釋

此一册《唐宋詞選釋》,俞平伯编,售 10 元,叹无人识此宝又恐有人抢购,立马购入。人民文学出版社 1979 年北京 1 版 1 印,扉枼钤「天津自行车二厂图书舘」,内枼整洁,纸张洁白柔韧,铅字的字画虽不如前面两本清晰,但同样令人赏玩不忍释手。俞平伯的《唐宋詞選釋》是读宋词的入门,选、释皆精良,说是读宋词必读并不为过。其实家中已有一本《唐宋詞選釋》,但已经快要脱胶散枼,遇到品相如此好的一本,真令我欣喜!

唐宋詞選釋内枼

《唐詩三百首新注》

唐詩三百首新注

此一册《唐詩三百首新注》,金性尧注,售 5 元。上海古籍出版社 1980 年上海 1 版 1 印,扉枼有 00 年购书识记,竟购于内蒙而流入我手。内枼如新,纸张洁白柔韧,适合翻阅。看多了总感觉上古的铅字整体比人文的更好,字画更清晰,字形也更优美,但若是真让我哪些细节上有差异则有些困难。《唐诗三百首》是家喻户晓的唐诗集子,中华书局前几年覆刻的《唐诗三百首》是更好的版本,版式古雅且价格低廉,现在也很容易买到,但肯定是激光排印而不是铅印了。这本《唐詩三百首新注》静静躺在一角,封面的金字闪耀动人,立马吸引了我的注意,展卷翻阅,心甚悦之,遂购入。

唐詩三百首新注扉枼

唐詩三百首新注内枼

最后以全部收获的合影作结吧,共计约 50 元,从堆积成山的书堆里挑出这几本,真可谓是如大浪淘沙一般的「淘」书。

淘书收获

文献总结|药物发现中的匹配分子对分析:方法与当前应用

作者 Leo
2023年4月15日 00:00

doi.org/10.1021/acs.jmedchem.2c01787

本文介绍 2023 年由曹东升与侯廷军研究团队发表在 Journal of Medicinal Chemistry 上的一篇展望,文章原标题为 Matched Molecular Pair Analysis in Drug Discovery: Methods and Recent Applications,文章介绍了主要介绍了匹配分子对分析的理论与目前基于匹配分子对分析的实际应用。

匹配分子对(matched molecular pair, MMP)的概念自提出以来,已成为了从化合物中提取药物化学知识并用于指导先导化合物优化的标准方法,MMP 的定义是只在局部具有较小的结构差异的一对化合物。合成化学家、药物化学家借助匹配分子对分析(molecular matched pair analysis, MMPA)的手段,可以从人类研究过的海量化合物中总结出化学改造的方法、化学改造对于化合物性质的影响等重要经验知识。

MMPA 理论

MMP 搜索算法

n

在需要对大量分子数据做 MMPA 时,首要任务就是提取出其中的 MMP,MMP 搜索算法可以分为 3 类:

  1. 预设的变换规则:使用人为设计的切分规则分割分子,寻找分子数据中的 MMP,常用规则如 retrosynthetic combinatorial analysis procedure(RECAP)和 breaking of retrosynthetically interesting chemical substructures(BRICS)。这种方法的局限性也很明显,例如忽略了预设规则以外的 MMP 并且只能处理单点的化学结构变换。
  2. 基于最大公共子结构(maximum common substructure, MCS)的方法:先寻找指定分子的的公共结构,将其设定为固定部分,只有具有公共结构的分子才能构成 MMP,分子中除去公共结构所剩余的结构就是改变部分,所以该方法通常用用于表示化学变换的 SMIRKS 存储 MMP。这种方法的问题在于计算 MCS 的计算开销很大。
  3. 片段与索引(fragmentation and indexing, F+I)方法:该方法是目前寻找 MMP 最通用的方法,主要方法是在两非氢原子间的非环单键处切断,构建 key 与 value 片段的对应索引,通过键值对间的匹配寻找 MMP,具体方法可以参看前文

影响 MMPA 的关键因素

n

MMPA 的基本假设是,分子结构中一些小的结构改变将引起特定物理性质或是生物活性的改变。然而现实中化合物性质改变的原因更为复杂,表现出更为偶然的现象,例如分子改造中的活性悬崖(对分子仅做微小的改造而生物活性变化巨大)等,所以在 MMPA 中也要考虑到许多因素的影响。

  • 分子表示:2D 与 3D 分子结构都被用于 MMPA 研究中,2D 分子描述的主要优点是处理简单,但许多实践表明 3D 分子表示方法表示了分子的空间信息,使其对于微小的结构差异更为敏感,这对 MMPA 十分重要。
  • 环境特征:在早期的研究中,人们认为只有 MMP 中的化学转换改变了分子的性质,因此只针对化学转换进行研究,而没有考虑具体分子。如今人们已经意识到,在 MMPA 还需要考虑具体分子的结构以及改造位点等环境特征,不能只研究 MMP 中的化学转化规则。目前,大部分研究使用分子图或 SMILES 来表示 MMP 中的完整分子,用于 MMPA 研究。除了分子信息以外,也有研究将蛋白口袋的信息也融入 MMPA,这有助于更深入研究 MMP 转化对受体与配体间结合作用影响。
  • 统计显著性:MMPA 的统计分析对于研究 MMP 间性质的变化十分重要,因为一种化学转换可以引起多种性质的改变,多种化学转换也可能使分子的某些性质不发生改变。MMPA 的统计学研究发现,在同一化合物上所做的两个结构改造所产生的影响远不同于单一结构改造影响的加和,这也称为「不可加和性」效应,这意味着简单的单一结构改造间存在着相互作用。不可加和性同样影响了分子的溶解度等性质,在 MMPA 中对可加和性进行统计分析,可以更好地识别药物分子的构效关系与分子中潜在的相互作用。

MMPA 实际应用

MMPA 已经广泛应用在寻找得到目标性质分子所需的化学改造中(ADMET 优化),除了应用在先导化合物优化,MMPA 也用于靶点预测、生物电子等排体替换、构效关系确定、全新药物设计等任务中,这里主要介绍 MMPA 在分子结构改造和全新药物设计中的应用。

匹配分子序列

n

将多个仅具有一个子结构区别的分子组织起来,就得到了匹配分子序列(matching molecular series, MMS),该方法最早被用于药物分子构效关系的分析,将不同 MMS 组织起来还得到形成匹配分子序列图,用于决策分子改造的路线。称为 SAR 转移的方法通过对比两个 MMS 间化合物性质的变化,可以判断替换结构的效果与。

基于 MMPA 的全新药物设计

将 MMP 化学变换规则用于分子生成是全新药物设计中的重要步骤,输入的分子首先被分割为片段,然后通过 MMP 数据库搜索找到相应的化学转换,将这些化学转换用于输入分子就得到了新分子。也有研究提出了基于片段的 MMP 分子生成方法,主要步骤是收集 MMP 片段信息,通过遗传算法等方法合理地相互组合 MMP 片段,得到新分子。

也有研究使用分子骨架和分子骨架以外的子结构来构建分子生成模型,模型是使用 SMILES 的 RNN 模型,第一步是生成正确的分子骨架,第二步在分子骨架上添加结构改造得到正确的分子。此外,MMS 方法可以很容易地将分子分为若干类的类似物,也可以很方便地用于全新药物设计。DeepSARM 模型的目标是寻找生物作用类似而化学结构新颖的类似物,就使用了 MMS 方法,模型同时还考虑了靶点信息,扩大的 MMS 方法的应用范围。

展望

n

分子设计所面临的一个重要难题是如何基于有限的实验数据决定下一步的分子改造,MMPA 有助于人们从已有的分子改造数据中得到化学转换的信息。为了能更好地利用 MMPA,文章提出了以下几点展望:

  1. 将 QSAR 与 MMPA 相结合。QSAR 模型着重于整体的结构特征,MMPA 主要用于确定局部子结构的改变,在一定程度上二者是互补的,在未来 MMPA 也可能对 QSAR 模型的预测有帮助。
  2. 将 MMPA 的概念用于蛋白质等大分子。
  3. 融合 MMPA 相关的分子优化方法,构建自动化的分子优化流程。尽管目前 MMP 已经应用于分子生成,但 MMP 数据的提取等步骤还需要人工处理。文章提出了上图所示的预期 MMPA 工作流程,希望能够实现 MMP 的自动提取、组织、应用和评估。

在明清小说中索隐:读《中国叙事学》

作者 Leo
2023年3月30日 00:00

浦安迪的《中国叙事学》是一本讨论中国叙事传统和明清小说的小书,页数并不多,不消四五天即可翻完。虽然这是一本学术著作,但语言流畅、分析丝丝入扣,读起来时并不觉得枯燥乏味,反而觉得酣畅淋漓。书题虽为《中国叙事学》,但全书中专门论述中国叙事的篇幅并不多,只是在全书的前面部分章节立起「中国叙事」的概念与分析方法,在后文中则全是基于这些概念与方法来分析中国的奇书文体,也就是范围更窄的明清小说,我在后文中即以「明清小说」这一更为通俗的文学类别指代书中所说的「奇书文体」。

全书只有前一部分中才真正讨论了「中国叙事学」,但这一部分中不乏许多精要切当的论述,让人觉得豁然开朗;作者将其作为分析方法,一以贯之用于剖析明清小说,又深让人惊异于明清小说原来还能这么读,读完这本书后再去重读明清小说,相信又会有另一番收获。基于以上的原因,本文另起了一个标题——「在明清小说中索隐」,我认为能够更准确地概括全书的主题。

中国叙事学
作者:[美] 浦安迪 (Andrew H. Plaks)
出版社:北京大学出版社
出版年:2018-8
页数:292
定价:45.00元
ISBN:9787301295960

何为叙事

任何时代,任何地方,任何社会,都少不了叙述。它从远古时代就开始存在,古往今来,哪里有人,哪里就有叙述。

——法国当代文论家 罗兰・巴特

书中认为文学作品是在传递某种人生本质,那么文学中的三大体式就也可以按传递人生本质的方式区别,即

  1. 抒情诗:直接描绘绘静态的人生本质,有叙述人但没有故事
  2. 戏剧:关注人生矛盾,通过舞台传达人生本质,有场面、故事但没有叙述人
  3. 叙事文:不直接描绘人生本质,而以传事为主要目标,从而展示延绵不断的经验流中的人生本质

我们不妨回忆我们曾经接触过的文学作品,一定是可以将大体的内容归置到上述的 3 个类别中去的。之所以说「大体的内容」,就是因为三种体式间不是完全孤立的,实际上三者相互交融渗透,抒情中存在着叙事,叙事中同样存在着抒情。这里用这种机械的分类有助于我们厘清全书讨论的对象——那也就是叙事文,在三者对比之下,相信我们对「何为叙事」已经有了朦胧的答案。

叙事文学的源流

西方叙事文学的历史可以概括为「史诗(epic)→ 罗曼史(romance)→ 小说(novel)」。

而中国的古代传统文学则是一条「三百篇 - 骚 - 赋 - 乐府 - 律诗 - 词曲 - 小说」的发展脉络,也就是说,中国传统文学的重心是抒情。

中国的叙事文学源头应当是《尚书》以及《左传》,作者认为二者深刻地影响了后世的叙事文学,并画下了一定的定式。二者都属于史文,而后世的虚构文学则是从六朝志怪发源,分化为「文言小说」和「白话小说」两大类别。

作者强调,文言小说和白话小说泾渭分明,「杂录」「志怪」等文言小说又被称为「史余」,与史文的关系更为密切,书中提到纪昀将《山海经》等书从史部抽出,纳入小说,可为一证。而白话小说则可能是由民间说书的「通俗文学」发展而来(鲁迅等持此说),也可能是由当时文人所创作的「才子书」(作者持此说)。

书中将中国的叙事文学历史总结为「神话 → 史文 → 明清奇书」,正如在对 novel 的批评不得不在 epic 的传统中一样,在批评中国的明清奇书时也脱不开神话与史文对其的影响。

史文的影响

在介绍书中分析史文与神话的内容之前,我想先引用顾颉刚先生的观点奠定一个基调,那就是应当审慎地对待传世文献,未经检验的文献是不可信的。经常有人被民族主义裹挟,把「疑古」简单地想象为「否认一切历史」并言之凿凿地抨击顾颉刚先生,我只能对此深表遗憾。在面对浩如烟海的史料时,「疑古」才是去伪存真、还原历史的科学方法。

书中的想法与顾颉刚先生的观点不谋而合,我不了解作者是否参考了顾颉刚先生的著作,如若不然,就是顾颉刚先生从历史研究的经验中总结出了「疑古」的观点,而作者从文学分析的角度告诉读者对史文的应当审慎,可以称得上合作。原书中的文字足够精彩,无需我的赘言,兹引录原文如下:

中国的史书虽然力图给我们造成一种客观记载的感觉,但实际上不外乎一种美学上的幻觉,是用各种人为的方法和手段造成的「拟客观」效果。

由于中国历代长期形成的对史近乎宗教的狂热崇拜,也由于在清亡以前史料永远只对史官开放的历史事实,中国正史叙述者总是摆出一副「全知全能者」的姿态;然而,这种全知全能却只是局限在冠冕堂皇的庙堂里。它的触角甚至伸不进皇家的后院,当然更难看见「处江湖之远」的草民百姓的众生相。一种纯客观的叙事幻觉由此产生,并且成为一种经久不坏的模式,从史官实录到虚构文本,横贯中国叙事的各种文体。

西方的史诗原则上是虚构的艺术,只与历史传说有些微弱的关联;而中国的史文对于「虚构」和「实事」却从来就没有严格的分界线。西方文学理论家一般认为,历史讲实事,小说讲虚构。中国古代批评家则强调,「历史中有小说,小说中有历史」。

西人重「模仿」,等于假定所讲述的一切都是出于虚构,中国人尚「传述」,等于宣称所述的一切都出于实事。

神话的影响

由史文上溯至远古神话,可以发现中国神话具有「人本位」倾向,也就是通常借神话来表人事,因而常常把历史与神话混作一团,譬如说我们都知道宙斯是虚构的,而禹究竟是神是人仍无定论,再例如我们都知道十个太阳是断无可能的,但后羿是远古的君主还是人格化的神呢?或许我们习于这些神话,在面对这样的疑难时会将其推作中国的神话过于琐碎繁杂而不成体系,但希腊神话何尝不零碎,却没有这种明显的「人本位」特征。

再来考查中神话的叙事,在中国古代典籍中,对神话的具体情节都是话焉不详的,这可以说是在中国的美学原动力中缺乏一种要求「头身尾」连贯的结构原型,这种「非叙述性」美学原型导致了中西叙事传统的分流。这两种不同的叙事传统在神话中表现为希腊神话以时间为轴心,故重过程而善于讲述故事;中国神话以空间为宗旨,故重本体而善于画图案。

这里要注意,神话是诞生于远古时代的故事,但其叙述未并产生于远古时代,例如《淮南子》《庄子》中的神话显然不能用于作神话叙述的分析,所以作者更花精力着眼于例如《尚书》中的神话。

中国的叙事

在神话与史文的孳乳中,中国的叙事形成了不同于西方的叙事传统,这种叙事传统的影响绵延至明清小说乃至其后。中国的叙事传统中,普遍将重点放在「事隙」上,与西方恰恰相反,也就是真正的具有动作的「事」,都被诸如宴会等「无事之事」包围。

至于为什么中国的叙事传统如此,没有形成或是拋弃了「叙述性」,甚至在远古神话中已经接受了了这一范式,作者认为这可能是因为中国人重礼的传统,因而中国人把诸如阴阳交替、四时更换等仪礼形式作为了一种基本原则,从而将其「空间化」,抹杀了其中的「时间性」。

明清小说中的叙事

今人在读明清小说时,相当大部分人会因为叙事中的时间性或因果性不强感觉琐碎或是乏味,想必很多人在中学时代读四大名著时,除了《西游记》与《三国演义》以外都觉得昏昏欲睡,这很大程度上就是因为《西游记》具有「取经」这条真正的具有动作的「事」贯穿始终,《三国演义》的时间性则明显更强,就算书中没有明显地以时间为线索,但汉末三分天下终而三家归晋的历史在开卷之前已经为读者熟知。

西人在读明清小说时亦有这种感受,他们用「缀段性」批评明清小说的叙事方法。缀段性来源于西方对诗歌的文学批评,亚里士多德曾说「缀段性的情节是所有情节中最坏的一种。我所谓的缀段性情节,是指前后毫无因果关系而串接成的情节。」

但是我们要注意,这是批评西方诗歌的方法,将这一术语用于评价明清小说时已经陷入了西方视角。西方之所以贬斥缀段性,是因为西方文学中有重视「头身尾」结构完整性的传统,这种叙述完整性的要求下,缀段性显然是一种低格。但这种结构完整性的要求并不是金科玉律,我们将眼光放回到「叙事」这一行为上,就会发现叙事天然就带有一定缀段性的特征,因为叙事就是在处理人类经验的一个个片段单元,例如中国的史文,就是由史官将一长段的时间线的人类经验分割为一个个事件片段而创作出的巨制。

明清小说在结构上的一个明显特征就是「百回」,古人刻意将一部小说分为百回或是百廿回本身就反映了一种中式的美感,一种中国人追求平衡的美学倾向。再者,明清小说的百回完帙在出版时又惯被分作十卷,这是今人难以注意到的细节,同时这也不是出于偶然。作者发现,每十回就能组成一个小故事,百回中的十个小故事又现出一种特殊的韵律感。

若以《水浒传》为为例:

  • 武(松)十回
  • 林(冲)十回
  • 宋(江)十回
  • 第七十二至八十二回:受招安
  • 第八十三至九十回:平辽
  • 第九十一至一百回:平田虎
  • 第一百一至一百十回:平王庆
  • 第一百十一至一百二十回:平方腊

若以《三国演义》为例:

  • 第一至九回:董卓传,至董卓身死
  • 第十至十九回:吕布传,至吕布战死白门楼
  • 第二十至三十一回:曹操传,至大破袁绍
  • 第三十二至三十八回:刘备传,至隆中对
  • 第三十九至五十回:诸葛亮传,至华容道

在这些十回的小故事中,又有相应的重心,这个重心大约是在三四回左右,在全书之中丝毫不乱,作者也基于这些结构上的巧妙安排而认定明清小说属于独运匠心的「才子书」,绝不可能脱胎于民间说书人口中。

再将全书的十回目故事缀合起来,对于全书而言还有一个结构上的经典范式,那就是「二十-六十-二十」的叙述程式。《金瓶梅》就是具有这种对称美的典型,在前二十回与后二十回中,书中都是在描写西门庆院墙外的故事,在中间六十回转入展开这间深宅大院中离合,同时前二十回叙述家中新添金、瓶、梅三小妾,奠定全书格局,后二十回则是西门庆死去,树倒猢孙散,这种对称的安排不能说不是作者的心思。

作者还发现,明清小说中的另一个叙事特点就是情节的高潮在终点前就已经发生了,大约在全书四分之三位置处,同时全书又可以分成上下两截相互照应。以《三国演义》为例子最为简单,第五十回赤壁之战确定了天下三分的格局,在此全书分为两截,而在第七十八回曹操死去,全书自此高潮突然收束,一路走向下坡。《金瓶梅》要更为规整,以第四十九回为上下截的分水岭,上截描述西门庆升官发财、步步高升的经历,下截则是其春风得意而加速自毁的过程,同样在第七十九回死去,达到故事的最高潮。

作者认为这种绵延不绝的故事布局是中国叙事独特的「转轮式」布局,故事的发展正像一个不断旋转的法轮,暗给人天道循环的感受。正如《三国演义》所要强调的「合久并分,分久并合」,故事开始于东汉末年的纷乱,结束于一统于晋,故事从刘、关、张等主人公手中交递给他们的后辈,正像长江后浪推前浪,呈现出一种无了局的形式,作者想表达的思想也暗含其中了。

作者在书的最后部分上述方法分析了四大奇书中的中心思想,对于只接触教科书式解读的我,其分析方法与结果都可谓是别开生面,例如《西游记》不能简单理解为明末政治的黑暗,在一定程度上它还与明末的思想主流「心学」有关,书中「心猿」等等用语肯定另有所指,作者将其解读为告诫人们「诚意正心」。但部分观点有些冒进而难免令人觉得略显穿凿,但我认为作者只是提出了他的观点,真正怎样理解和解读明清小说还是依赖于读者。我相信在看过作者的一系列剖析后,再去重新读一读四大奇书,一定会有新的理解,这些新收获才是作者浦安迪撰写此书想要传达给我们的东西。

❌
❌