mgl
MGL 是一个基于 Common Lisp 语言开发的开源机器学习库,由 Gábor Melis 主导开发。它旨在为 Lisp 社区提供一个功能全面且高效的机器学习解决方案,填补了该生态系统中在深度学习与统计建模领域的空白。
MGL 的核心优势在于其对多种经典及现代机器学习算法的支持,主要包括反向传播神经网络(涵盖前馈网络与循环神经网络 RNN)、玻尔兹曼机以及高斯过程等。除了核心的模型构建能力,MGL 还提供了一整套完善的数据处理工具,包括数据集采样、重采样、交叉验证以及特征选择与编码功能。此外,它内置了基于梯度的优化器(如梯度下降、共轭梯度法)和详细的训练监控机制,帮助开发者实时追踪模型性能,如混淆矩阵计算和各类指标测量。
在技术实现上,MGL 底层依赖于 MGL-MAT 库,这意味着它能够充分利用 BLAS(基础线性代数子程序)进行 CPU 加速,并支持 CUDA 以实现 GPU 并行计算,从而显著提升了大规模数据训练的效率。这种设计使得 MGL 不仅在算法灵活性上表现出色,在计算性能上也具备竞争力。
MGL 主要适合熟悉 Common Lisp 的软件开发人员、人工智能研究人员以及对函数式编程在机器学习中应用感兴趣的技术专家使用。对于希望在不离开 Lisp 环境的前提下,进行神经网络实验、自然语言处理(如词袋模型)或复杂统计建模的用户来说,MGL 是一个值得尝试的专业级工具库。
使用场景
某量化交易团队的核心算法工程师正致力于在现有的 Common Lisp 高频交易系统中原生集成一个基于循环神经网络(RNN)的市场情绪预测模块,以利用 Lisp 强大的宏系统和实时数据处理能力。
没有 mgl 时
- 生态割裂严重:必须通过外部进程调用 Python 或 C++ 编写的机器学习模型,导致 Lisp 主程序与模型服务间存在高昂的 IPC(进程间通信)延迟,难以满足微秒级交易需求。
- 手动实现复杂:若坚持纯 Lisp 实现,需从零编写反向传播算法、梯度下降优化器及矩阵运算底层逻辑,开发周期长达数月且极易引入数值计算错误。
- 缺乏调试监控:训练过程中无法直观监控损失函数收敛情况或混淆矩阵变化,只能依靠打印原始日志,难以快速定位模型过拟合或梯度消失问题。
- 数据预处理繁琐:缺少内置的数据重采样、交叉验证和特征编码工具,每次实验前需手写大量样板代码来处理时间序列数据的洗牌与分区。
使用 mgl 后
- 原生无缝集成:直接利用 mgl 提供的 RNN 和反向传播网络组件,在 Lisp 环境中原生构建和训练模型,消除了跨语言调用开销,显著降低推理延迟。
- 开箱即用算法:直接调用封装好的梯度下降优化器、共轭梯度法及 Boltzmann 机器等核心算法,将开发重心从底层数学实现转移到模型架构设计上,效率提升数倍。
- 可视化监控完善:借助内置的 Monitor 和 Measurer 模块,实时追踪分类准确率与损失值,并可通过集成的 gnuplot 接口即时生成训练曲线,快速迭代调优。
- 标准化数据流:利用内置的 Sampler 和 Partition 功能轻松实现数据的交叉验证与 Bagging 处理,配合特征编码工具,大幅简化了从原始行情数据到模型输入的预处理流程。
mgl 让 Common Lisp 开发者无需跳出熟悉的语言生态,即可高效构建、训练并部署高性能神经网络,实现了系统低延迟与开发高效率的完美统一。
运行环境要求
- 未说明
- 非必需
- 支持 NVIDIA GPU 加速(通过 CL-CUDA),若无 GPU 或未安装 CUDA SDK,将回退使用 BLAS 和 Lisp 代码
- 具体显卡型号、显存大小及 CUDA 版本未在文中明确指定
未说明

快速开始
MGL 手册
目录
- 1 介绍
- 2 常用内容
- 3 数据集
- 4 重采样
- 5 核心
- 6 监控
- 7 分类
- 8 特征
- 9 基于梯度的优化
- 10 可微函数
- 11 反向传播神经网络
- 12 玻尔兹曼机
- 13 高斯过程
- 14 自然语言处理
- 15 日志记录
[在 MGL 包中]
- [system] "mgl"
- 版本: 0.1.0
- 描述:
MGL是一个用于反向传播神经网络、玻尔兹曼机、高斯过程等的机器学习库。 - 许可证: MIT,详见 COPYING。
- 作者: Gábor Melis mega@retes.hu
- 邮箱: mega@retes.hu
- 主页: http://melisgl.github.io/mgl
- 问题跟踪器: https://github.com/melisgl/mgl/issues
- 源码控制: GIT
- 依赖于: alexandria, array-operations, cl-reexport, closer-mop, lla, mgl-gnuplot, mgl-mat, mgl-pax, named-readtables, num-utils, pythonic-string-reader, swank
1 介绍
1.1 概述
MGL 是由 Gábor Melis 开发的 Common Lisp 机器学习库,其中部分代码最初由 Ravenpack International 贡献。它主要专注于各种形式的神经网络(玻尔兹曼机、前馈和循环反向传播网络)。MGL 的大部分功能都建立在 MGL-MAT 之上,因此支持 BLAS 和 CUDA。
总体而言,MGL 的重点在于功能的强大和性能的高效,而非易用性。也许在未来,如果能在功能与易用性之间找到合理的平衡,就会出现一个功能受限但易于使用的标准化接口。
1.2 链接
1.3 依赖
MGL 曾经依赖 LLA 来与 BLAS 和 LAPACK 接口。如今这已经基本成为历史,但对外部库的配置仍然通过 LLA 完成。请参阅 LLA 的 README 文件以了解如何进行设置。需要注意的是,现在 OpenBLAS 的安装更加简单,且速度与 ATLAS 不相上下。
CL-CUDA 和 MGL-MAT 是两个主要的依赖项,同时也是尚未加入 Quicklisp 的库,因此只需将它们放入 quicklisp/local-projects/ 目录即可。如果系统中没有合适的 GPU 或未安装 CUDA SDK,MGL 将自动回退到使用 BLAS 和 Lisp 代码。只需将代码包裹在 MGL-MAT:WITH-CUDA* 中,即可在 GPU 上运行;而通过 MGL-MAT:CUDA-AVAILABLE-P 可以检查是否正在使用 GPU。
1.4 代码组织
MGL 由多个专门用于不同任务的包组成。例如,MGL-RESAMPLE 包负责 重采样,而 MGL-GD 包则负责 梯度下降,以此类推。一方面,多个包有助于清晰地分离 API 和实现,并便于深入研究特定任务。另一方面,过多的包也可能带来不便,因此 MGL 包本身会重新导出构成 MGL 和 MGL-MAT(参见 MAT 手册)的所有其他包中的所有外部符号,因为 MGL 严重依赖于 MGL-MAT。
不过,捆绑在一起但独立的 MGL-GNUPLOT 库是一个例外。
内置测试可以通过以下方式运行:
(ASDF:OOS 'ASDF:TEST-OP '#:MGL)
请注意,大多数测试具有一定的随机性,偶尔可能会失败。
1.5 术语表
归根结底,机器学习就是为某个领域创建 模型。模型中观察到的数据称为 实例(也称为示例或样本)。实例的集合被称为 数据集。数据集用于拟合模型或进行 预测。有时“预测”一词过于具体,因此将模型应用于某些实例后得到的结果通常被称为 结果。
2 常用内容
[在包 MGL-COMMON 中]
- [通用函数] NAME OBJECT
[函数] NAME= X Y
如果 X 和 Y 是
EQL(01),或者它们是元素为EQUAL的结构化组件,则返回T。字符串和位向量如果长度相同且各成分完全一致,则被认为是EQUAL。其他数组只有当它们是EQ时才被视为EQUAL。
- [通用函数] SIZE OBJECT
[通用函数] NODES OBJECT
返回一个
MGL-MAT:MAT对象,表示OBJECT的状态或结果。返回矩阵的第一维等于条带的数量。
- [通用函数] DEFAULT-VALUE OBJECT
- [通用函数] GROUP-SIZE OBJECT
- [通用函数] BATCH-SIZE OBJECT
- [通用函数] WEIGHTS OBJECT
- [通用函数] SCALE OBJECT
3 数据集
[在包 MGL-DATASET 中]
实例通常是用户选择的任何类型的对象。它通常由一组数字表示,称为特征向量,或者由包含特征向量、标签等信息的结构表示。数据集是由这样的实例组成的SEQUENCE,或者是产生实例的Samplers对象。
[函数] MAP-DATASET FN DATASET
对
DATASET中的每个实例调用FN。这基本上等同于遍历序列或采样器(参见Samplers)的元素。
[函数] MAP-DATASETS FN DATASETS &KEY (IMPUTE NIL IMPUTEP)
对
DATASETS中每个数据集的一个实例列表调用FN。不返回任何值。如果指定了IMPUTE,则会一直迭代到最大的数据集被消耗完,并用IMPUTE来填补缺失值。如果没有指定IMPUTE,则迭代直到最小的数据集耗尽为止。(map-datasets #'prin1 '((0 1 2) (:a :b))) .. (0 :A)(1 :B) (map-datasets #'prin1 '((0 1 2) (:a :b)) :impute nil) .. (0 :A)(1 :B)(2 NIL)当然也可以将序列与采样器混合使用:
(map-datasets #'prin1 (list '(0 1 2) (make-sequence-sampler '(:a :b) :max-n-samples 2))) .. (0 :A)(1 :B)
3.1 采样器
有些算法不需要对整个数据集进行随机访问,而是可以通过流式观察来工作。采样器是简单的生成器,提供两个函数:SAMPLE 和 FINISHEDP。
[通用函数] SAMPLE SAMPLER
如果
SAMPLER还没有用完数据(参见FINISHEDP),SAMPLE会返回一个代表来自世界样本的对象,也就是可以用于训练或预测的输入。如果SAMPLER已经FINISHEDP,则不允许调用SAMPLE。
[通用函数] FINISHEDP SAMPLER
检查
SAMPLER是否已经用完了所有样本。
[函数] LIST-SAMPLES SAMPLER MAX-SIZE
返回最多
MAX-SIZE长度的样本列表,如果SAMPLER提前用完,则返回更少的样本。
[函数] MAKE-SEQUENCE-SAMPLER SEQ &KEY MAX-N-SAMPLES
创建一个按原始顺序返回
SEQ元素的采样器。如果MAX-N-SAMPLES不为零,则最多抽取MAX-N-SAMPLES个样本。
[函数] MAKE-RANDOM-SAMPLER SEQ &KEY MAX-N-SAMPLES (REORDER #'MGL-RESAMPLE:SHUFFLE)
创建一个以随机顺序返回
SEQ元素的采样器。如果MAX-N-SAMPLES不为零,则最多抽取MAX-N-SAMPLES个样本。采样器首先遍历一次打乱顺序的SEQ副本,每当采样器到达副本末尾时,就会再次打乱顺序。打乱顺序是通过调用REORDER函数来完成的。
[变量] *INFINITELY-EMPTY-DATASET* #<FUNCTION-SAMPLER "infinitely empty" >
这是
MGL-OPT:MINIMIZE的默认数据集。它是一个无限的NIL流。
3.1.1 函数采样器
[类] FUNCTION-SAMPLER
一种带有函数作为其
GENERATOR的采样器,该函数会产生一个可能有限也可能无限的样本流,具体取决于MAX-N-SAMPLES。FINISHEDP会在MAX-N-SAMPLES不为零且不大于已生成的样本数量(N-SAMPLES)时返回T。(list-samples (make-instance 'function-sampler :generator (lambda () (random 10)) :max-n-samples 5) 10) => (3 5 2 3 3)
[读取器] GENERATOR FUNCTION-SAMPLER (:GENERATOR)
一个无参数的生成函数,用于返回下一个样本。
- [访问者] MAX-N-SAMPLES FUNCTION-SAMPLER (:MAX-N-SAMPLES = NIL)
[读取器] NAME FUNCTION-SAMPLER (:NAME = NIL)
一个任意对象,用于命名采样器。仅用于打印采样器对象。
- [读取器] N-SAMPLES FUNCTION-SAMPLER (:N-SAMPLES = 0)
4 重采样
[在 MGL-RESAMPLE 包中]
本包的核心是重采样方法,例如交叉验证和自助法,这些方法可用于模型评估、模型选择,也可作为一种简单的集成学习方式。此外,还提供了数据划分和采样函数,因为它们通常与重采样一起使用。
4.1 打乱顺序
[函数] SHUFFLE SEQ
复制
SEQ并使用费雪-耶茨算法对其进行打乱。
[函数] SHUFFLE! SEQ
使用费雪-耶茨算法对
SEQ进行原地打乱。
4.2 划分
以下函数将数据集(目前仅支持 SEQUENCE)划分为若干个子集。原始数据集中每个元素恰好属于其中一个子集。
[函数] FRACTURE FRACTIONS SEQ &KEY WEIGHT
将
SEQ划分为若干个子序列。FRACTIONS可以是正整数,也可以是非负实数列表。WEIGHT可以是NIL,或者是一个函数,当以SEQ中的元素作为参数调用时,返回非负实数。如果FRACTIONS是一个正整数,则返回该数量的子序列列表,各子序列的权重之和大致相等(可能因舍入误差而略有偏差);否则,按照FRACTIONS中各元素的比例来划分SEQ的子序列,即第 I 个子序列的权重之和与FRACTIONS中的第 I 个元素成正比。若WEIGHT为NIL,则假定所有元素的权重相同。例如,要将数据划分为 5 个序列:
(fracture 5 '(0 1 2 3 4 5 6 7 8 9)) => ((0 1) (2 3) (4 5) (6 7) (8 9))要将数据划分为两个长度比例为 2:3 的序列:
(fracture '(2 3) '(0 1 2 3 4 5 6 7 8 9)) => ((0 1 2 3) (4 5 6 7 8 9))
[函数] STRATIFY SEQ &KEY (KEY #'IDENTITY) (TEST #'EQL)
返回
SEQ的分层列表。SEQ是一个元素序列,其中每个元素通过函数KEY来确定其所属的类别。这些类别是不可见的对象,通过TEST函数进行比较以判断是否相等。一个分层是由具有相同(在TEST下)KEY值的元素组成的序列。例如:
(stratify '(0 1 2 3 4 5 6 7 8 9) :key #'evenp) => ((0 2 4 6 8) (1 3 5 7 9))
[函数] FRACTURE-STRATIFIED FRACTIONS SEQ &KEY (KEY #'IDENTITY) (TEST #'EQL) WEIGHT
类似于
FRACTURE,但同时确保各个类别的样本在各个子集中均匀分布(参见STRATIFY)。这在分类任务中非常有用,可以在保持各类别分布不变的情况下对数据集进行划分。注意,返回的集合并不是随机顺序的,实际上它们会根据
KEY值进行内部排序。例如,要将数据划分为两个子集,使偶数和奇数的数量大致相等:
(fracture-stratified 2 '(0 1 2 3 4 5 6 7 8 9) :key #'evenp) => ((0 2 1 3) (4 6 8 5 7 9))
4.3 交叉验证
[函数] CROSS-VALIDATE DATA FN &KEY (N-FOLDS 5) (FOLDS (ALEXANDRIA:IOTA N-FOLDS)) (SPLIT-FN #'SPLIT-FOLD/MOD) PASS-FOLD
将
FN映射到由SPLIT-FN划分的DATA的各个折中,并将结果收集到一个列表中。最简单的示例是:(cross-validate '(0 1 2 3 4) (lambda (test training) (list test training)) :n-folds 5) => (((0) (1 2 3 4)) ((1) (0 2 3 4)) ((2) (0 1 3 4)) ((3) (0 1 2 4)) ((4) (0 1 2 3)))当然,在实际应用中,人们通常会训练模型,并返回训练好的模型和/或其在
TEST上的得分。此外,有时也可能只执行部分折,并记录具体是哪些折:(cross-validate '(0 1 2 3 4) (lambda (fold test training) (list :fold fold test training)) :folds '(2 3) :pass-fold t) => ((:fold 2 (2) (0 1 3 4)) (:fold 3 (3) (0 1 2 4)))最后,还可以自定义数据的划分方式。默认情况下,会调用
SPLIT-FOLD/MOD,传入DATA、当前折(来自FOLDS)以及折数N-FOLDS作为参数。SPLIT-FOLD/MOD返回两个值,然后传递给FN。用户也可以使用SPLIT-FOLD/CONT或SPLIT-STRATIFIED,或其他接受这些参数的函数。唯一的限制是,FN必须接收与SPLIT-FN返回值数量相同的参数(如果设置了PASS-FOLD参数,则还需额外接收折数参数)。
[函数] SPLIT-FOLD/MOD SEQ FOLD N-FOLDS
将
SEQ划分为两个序列:一个包含索引除以N-FOLDS后余数为FOLD的元素,另一个包含其余元素。第二个序列通常是较大的那个。元素的相对顺序保持不变。此函数适合作为CROSS-VALIDATE的SPLIT-FN参数。
[函数] SPLIT-FOLD/CONT SEQ FOLD N-FOLDS
想象将
SEQ划分成N-FOLDS个大小相同(可能因四舍五入而略有差异)的子序列。将索引为FOLD的子序列作为第一个返回值,其余子序列拼接成一个整体作为第二个返回值。元素的相对顺序保持不变。此函数适合作为CROSS-VALIDATE的SPLIT-FN参数。
[函数] SPLIT-STRATIFIED SEQ FOLD N-FOLDS &KEY (KEY #'IDENTITY) (TEST #'EQL) WEIGHT
将
SEQ划分为N-FOLDS个子集(类似于FRACTURE-STRATIFIED)。将索引为FOLD的子集作为第一个返回值,其余子集拼接成一个整体作为第二个返回值。此函数适合作为CROSS-VALIDATE的SPLIT-FN参数(通常以闭包形式使用,绑定KEY、TEST和WEIGHT参数)。
4.4 装袋法
[函数] BAG SEQ FN &KEY (RATIO 1) N WEIGHT (REPLACEMENT T) KEY (TEST #'EQL) (RANDOM-STATE *RANDOM-STATE*)
使用
SAMPLE-FROM(传递RATIO、WEIGHT、REPLACEMENT)从SEQ中采样,如果KEY不为NIL,则使用SAMPLE-STRATIFIED。然后调用FN函数处理采样结果。如果N为NIL,则会不断重复此过程,直到FN执行非局部退出。否则,N必须是一个非负整数,表示要执行的迭代次数,FN返回的主要值将被收集到一个列表中并返回。示例请参见SAMPLE-FROM和SAMPLE-STRATIFIED。
[函数] SAMPLE-FROM RATIO SEQ &KEY WEIGHT REPLACEMENT (RANDOM-STATE *RANDOM-STATE*)
从
SEQ中有放回或无放回地进行采样,返回一个新的序列。结果序列中元素的权重总和大约是SEQ中权重总和乘以RATIO。如果WEIGHT为NIL,则假定所有元素具有相等的权重;否则,WEIGHT应在被调用时返回一个非负实数,该实数对应于SEQ中某个元素的权重。随机选择一半元素:
(sample-from 1/2 '(0 1 2 3 4 5)) => (5 3 2)随机选择一些元素,使其权重之和约为整个序列权重之和的一半:
(sample-from 1/2 '(0 1 2 3 4 5 6 7 8 9) :weight #'identity) => ;; 权重之和约为 45/2 的 28 (9 4 1 6 8)进行有放回采样(即允许同一个元素被多次采样):
(sample-from 1 '(0 1 2 3 4 5) :replacement t) => (1 1 5 1 4 4)
[函数] SAMPLE-STRATIFIED RATIO SEQ &KEY WEIGHT REPLACEMENT (KEY #'IDENTITY) (TEST #'EQL) (RANDOM-STATE *RANDOM-STATE*)
类似于
SAMPLE-FROM,但确保结果中各类别的加权比例与SEQ中的比例大致相同。关于KEY和TEST的说明,请参阅STRATIFY。
4.5 CV 装袋法
[函数] BAG-CV DATA FN &KEY N (N-FOLDS 5) (FOLDS (ALEXANDRIA:IOTA N-FOLDS)) (SPLIT-FN #'SPLIT-FOLD/MOD) PASS-FOLD (RANDOM-STATE *RANDOM-STATE*)
对
DATA的不同随机排列进行交叉验证N次,并收集结果。由于CROSS-VALIDATE会收集FN的返回值,因此本函数的返回值是一个包含多个FN结果列表的列表。如果N为NIL,则不收集任何结果,而是持续进行交叉验证,直到FN执行非局部退出。下面的例子简单地收集了 2 折交叉验证中测试集和训练集的结果,共重复 3 次:
;;; 这是非确定性的。 (bag-cv '(0 1 2 3 4) #'list :n 3 :n-folds 2) => ((((2 3 4) (1 0)) ((1 0) (2 3 4))) (((2 1 0) (4 3)) ((4 3) (2 1 0))) (((1 0 3) (2 4)) ((2 4) (1 0 3))))当单次交叉验证无法产生稳定结果时,CV 装袋法就显得非常有用。作为一种集成方法,CV 装袋法相比普通装袋法的优势在于:每个样本都会出现相同的次数,并且在第一次交叉验证完成后,每个样本都会有一个完整但不太可靠的估计值,随后通过进一步的交叉验证逐步完善。
4.6 其他操作
[函数] SPREAD-STRATA SEQ &KEY (KEY #'IDENTITY) (TEST #'EQL)
返回一个重新排序后的
SEQ序列,使得属于不同层(根据KEY和TEST,参见STRATIFY)的元素均匀分布。同一层内的元素顺序保持不变。例如,为了使偶数和奇数均匀分布:
(spread-strata '(0 2 4 6 8 1 3 5 7 9) :key #'evenp) => (0 1 2 3 4 5 6 7 8 9)对于类别不平衡的情况也适用:
(spread-strata (vector 0 2 3 5 6 1 4) :key (lambda (x) (if (member x '(1 4)) t nil))) => #(0 1 2 3 4 5 6)
[函数] ZIP-EVENLY SEQS &KEY RESULT-TYPE
将
SEQS中的多个序列合并成一个单一的序列,使得来自同一源序列的元素索引在整个序列中均匀分布。如果RESULT-TYPE是LIST(01),则返回一个列表;如果RESULT-TYPE是VECTOR(01),则返回一个向量。如果RESULT-TYPE为NIL,则由SEQS中第一个序列的类型决定。(zip-evenly '((0 2 4) (1 3))) => (0 1 2 3 4)
5 核心
[在 MGL-CORE 包中]
5.1 持久化
[函数] LOAD-STATE FILENAME OBJECT
从
FILENAME加载OBJECT的权重。返回OBJECT。
[函数] SAVE-STATE FILENAME OBJECT &KEY (IF-EXISTS :ERROR) (ENSURE T)
将
OBJECT的权重保存到FILENAME。如果ENSURE为真,则会对FILENAME调用ENSURE-DIRECTORIES-EXIST。IF-EXISTS会被传递给OPEN。返回OBJECT。
[函数] READ-STATE OBJECT STREAM
从双值
STREAM中读取OBJECT的权重,这里的权重指的是学习得到的参数。目前对数据尚未进行任何校验,未来随着序列化格式的变化,这一点肯定会有所改变。返回OBJECT。
[函数] WRITE-STATE OBJECT STREAM
将
OBJECT的权重写入双值STREAM。返回OBJECT。
[泛型函数] READ-STATE* OBJECT STREAM CONTEXT
这是
READ-STATE的扩展点。可以保证对于每个OBJECT(在EQ的意义上),主方法READ-STATE*只会被调用一次。CONTEXT是一个不透明的对象,必须传递给任何递归调用的READ-STATE*。
[泛型函数] WRITE-STATE* OBJECT STREAM CONTEXT
这是
WRITE-STATE的扩展点。可以保证对于每个OBJECT(在EQ的意义上),主方法WRITE-STATE*只会被调用一次。CONTEXT是一个不透明的对象,必须传递给任何递归调用的WRITE-STATE*。
5.2 批量处理
在训练或预测过程中逐个处理样本可能会很慢。支持批量处理以提高效率的模型被称为“条带化”模型。
通常,在创建模型时或之后,会为其设置 MAX-N-STRIPES,该值应为一个正整数。当一批样本需要输入到模型中时,首先会被分割成长度不超过 MAX-N-STRIPES 的子批次。对于每个子批次,会调用 SET-INPUT(FIXDOC),并且一个前置方法会负责将 N-STRIPES 设置为该子批次的实际样本数量。当 MAX-N-STRIPES 被设置时,内部数据结构可能会被重新调整大小,这是一项开销较大的操作。而设置 N-STRIPES 则相对便宜,通常通过矩阵重塑来实现。
需要注意的是,对于由不同部分组成的模型(例如,MGL-BP:BPN 由 MGL-BP:LUMP 组成),设置这些值会影响其组成部分,但不应直接更改各部分的条带数,因为那样会导致模型内部的一致性问题。
[泛型函数] MAX-N-STRIPES OBJECT
OBJECT同时能够处理的最大条带数。
[泛型函数] SET-MAX-N-STRIPES MAX-N-STRIPES OBJECT
分配必要的资源,以便在
OBJECT中同时处理MAX-N-STRIPES个条带。当MAX-N-STRIPES被SETF修改时,会调用此函数。
[泛型函数] N-STRIPES OBJECT
当前存在于
OBJECT中的条带数量。该值最多等于MAX-N-STRIPES。
[泛型函数] SET-N-STRIPES N-STRIPES OBJECT
设置
OBJECT中正在使用的条带数量(不超过MAX-N-STRIPES)。当N-STRIPES被SETF修改时,会调用此函数。
[宏] WITH-STRIPES SPECS &BODY BODY
在条带化对象中绑定条带的起始索引,并可选地绑定结束索引。
(WITH-STRIPES ((STRIPE1 OBJECT1 START1 END1) (STRIPE2 OBJECT2 START2) ...) ...)以下是如何在一个 bpn 的输入块中找到第 N 个输入对应的索引范围:
(with-stripes ((n input-lump start end)) (loop for i upfrom start below end do (setf (mref (nodes input-lump) i) 0d0)))注意,输入块是条带化的,但我们要索引的矩阵(
NODES)并不为WITH-STRIPES所知。事实上,对于块来说,相同的条带索引同样适用于NODES和MGL-BP:DERIVATIVES。
[泛型函数] STRIPE-START STRIPE OBJECT
返回
STRIPE在OBJECT的某个数组或矩阵中的起始索引。
[泛型函数] STRIPE-END STRIPE OBJECT
返回
STRIPE在OBJECT的某个数组或矩阵中的结束索引(不包括该索引)。
[泛型函数] SET-INPUT INSTANCES MODEL
将
INSTANCES设置为MODEL的输入。无论模型是否支持批量操作,INSTANCES始终是一个实例的SEQUENCE。它会在一个:BEFORE方法中将N-STRIPES设置为 (LENGTH)2f78INSTANCES的长度。
[函数] MAP-BATCHES-FOR-MODEL FN DATASET MODEL
使用适合
MODEL的DATASET中的样本批次调用FN。每个批次的样本数量不超过MODEL的MAX-N-STRIPES,或者如果没有更多样本,则少于这个数量。
[宏] DO-BATCHES-FOR-MODEL (BATCH (DATASET MODEL)) &BODY BODY
是
MAP-BATCHES-FOR-MODEL的便捷宏。
5.3 执行器
[通用函数] MAP-OVER-EXECUTORS FN INSTANCES PROTOTYPE-EXECUTOR
将
INSTANCES分配给与PROTOTYPE-EXECUTOR执行相同功能的执行器,并使用这些实例以及对应的执行器调用FN。某些对象将功能和调用混为一谈:例如,
MGL-BP:BPN的前向传播会根据输入计算输出,因此它类似于一个函数;但同时它也充当函数调用的角色——在计算输出的过程中,该 bpn(函数)对象本身的状态会发生变化。因此,即使是 bpn 的前向传播也不是线程安全的。此外,还有一个限制,即所有输入必须具有相同的大小。例如,如果我们有一个函数可以根据特定大小的输入构建 bpn a,那么我们可以创建一个工厂来为特定的调用创建 bpn。不过,这个工厂很可能希望保持权重不变。在 参数化执行器缓存 中,
MAKE-EXECUTOR-WITH-PARAMETERS就是这样一个工厂。另一种可能性是并行化执行,这也是
MAP-OVER-EXECUTORS允许的,但目前还没有现成的解决方案。默认实现只是简单地使用
INSTANCES和PROTOTYPE-EXECUTOR调用FN。
[宏] DO-EXECUTORS (INSTANCES OBJECT) &BODY BODY
基于
MAP-OVER-EXECUTORS的便捷宏。
5.3.1 参数化执行器缓存
[类] PARAMETERIZED-EXECUTOR-CACHE-MIXIN
将此混入模型中,实现
INSTANCE-TO-EXECUTOR-PARAMETERS、MAKE-EXECUTOR-WITH-PARAMETERS和DO-EXECUTORS后,即可构建适用于不同实例的执行器。最典型的例子是使用 BPN 来计算高斯过程的均值和协方差。由于每个实例由不同数量的观测值组成,输入的大小并不固定,因此我们需要为每个输入维度(即参数)创建一个相应的 BPN(执行器)。
[通用函数] MAKE-EXECUTOR-WITH-PARAMETERS PARAMETERS CACHE
为
PARAMETERS创建一个新的执行器。CACHE是一个PARAMETERIZED-EXECUTOR-CACHE-MIXIN。在 BPN 高斯过程的例子中,PARAMETERS就是输入维度的列表。
[通用函数] INSTANCE-TO-EXECUTOR-PARAMETERS INSTANCE CACHE
返回能够处理
INSTANCE的执行器所需的参数。由MAP-OVER-EXECUTORS在CACHE(即一个PARAMETERIZED-EXECUTOR-CACHE-MIXIN)上调用。返回的参数是EQUAL参数到执行器哈希表中的键。
6 监控
[在 MGL-CORE 包中]
在训练或应用模型时,人们通常希望跟踪各种统计信息。例如,在使用交叉熵损失函数训练神经网络的情况下,这些统计信息可能包括平均交叉熵损失值、分类准确率,甚至是整个混淆矩阵以及隐藏层中的稀疏性水平。此外,还有一个问题:如何处理这些测量值(记录后丢弃、添加到某个计数器或列表中)。
因此,在运行过程中,可能会有多个阶段需要我们关注。我们可以将这些阶段称为事件。针对每个事件,也可能有许多相对独立的操作可以执行。我们可以将这些操作称为监控器。有些监控器是由两个操作组成的:一个用于提取某些度量,另一个用于汇总这些度量。我们分别称这两个部分为度量器和计数器。
例如,考虑训练一个反向传播神经网络。我们希望在反向传播完成后立即查看网络的状态。MGL-BP:BP-LEARNER 提供了一个名为 MONITORS 的事件钩子,对应于梯度反向传播完成后的时刻。假设我们对训练代价的变化感兴趣:
(push (make-instance 'monitor
:measurer (lambda (instances bpn)
(declare (ignore instances))
(mgl-bp:cost bpn))
:counter (make-instance 'basic-counter))
(monitors learner))
在训练过程中,这个监控器会在后台跟踪训练样本的代价。如果我们希望定期打印并重置这个监控器,可以在 MGL-OPT:ITERATIVE-OPTIMIZER 的 MGL-OPT:ON-N-INSTANCES-CHANGED 访问器上再添加一个监控器:
(push (lambda (optimizer gradient-source n-instances)
(declare (ignore optimizer))
(when (zerop (mod n-instances 1000))
(format t "n-instances: ~S~%" n-instances)
(dolist (monitor (monitors gradient-source))
(when (counter monitor)
(format t "~A~%" (counter monitor))
(reset-counter (counter monitor)))))
(mgl-opt:on-n-instances-changed optimizer))
需要注意的是,只要实现了具有相应签名的 APPLY-MONITOR,我们推入的监控器可以是任何东西。另外,ZEROPec8b + MOD(0 1) 的逻辑比较脆弱,因此你很可能更倾向于使用 MGL-OPT:MONITOR-OPTIMIZATION-PERIODICALLY,而不是手动实现上述逻辑。
这就是总体思路。具体的事件会在其触发位置进行文档说明。通常,还会有一些特定任务的工具函数来创建一组合理的默认监控器(参见 分类监控器)。
[函数] APPLY-MONITORS MONITORS &REST ARGUMENTS
对
MONITORS和ARGUMENTS中的每个监控器调用APPLY-MONITOR。这就是触发事件的方式。
[通用函数] APPLY-MONITOR MONITOR &REST ARGUMENTS
将
MONITOR应用于ARGUMENTS。这听起来相当通用,因为它确实如此。MONITOR可以是任何东西,甚至只是一个简单的函数或符号,在这种情况下,它就等同于CL:APPLY。更多内容请参阅 监控器。
[通用函数] COUNTER MONITOR
返回表示
MONITOR状态的对象,或者如果它没有任何状态(例如,它只是一个简单的日志记录函数),则返回NIL。大多数监控器都有计数器,它们会将结果累积起来,直到被打印并重置。更多内容请参阅 计数器。
[函数] MONITOR-MODEL-RESULTS FN DATASET MODEL MONITORS
使用
DATASET中的批次数据调用FN,直到数据用完(类似于DO-BATCHES-FOR-MODEL)。FN应该将MODEL应用于当前批次,并返回某种结果(对于神经网络来说,结果就是模型本身的状态)。对每个批次以及FN为该批次返回的结果应用MONITORS。最后,返回MONITORS的计数器列表。这个函数的目的是通过仅对模型进行一次应用,高效地收集各种结果和统计信息(如误差度量),并将从模型结果中提取感兴趣量的工作交给
MONITORS来完成。请参阅针对特定模型的版本,例如
MGL-BP:MONITOR-BPN-RESULTS。
[通用函数] MONITORS OBJECT
返回与
OBJECT关联的监控器。更多文档请参阅各种方法,例如MONITORS。
6.1 监控器
[类] MONITOR
一种包含嵌套监控器
MEASURER的监控器。当此监控器被应用时,它会先应用度量器,并将返回的值传递给其COUNTER插槽上调用的ADD-TO-COUNTER。用户可以通过进一步特化APPLY-MONITOR来改变这一行为。当同一个事件监控器需要在一段时间内反复应用,并且其结果需要被汇总时,此类监控器非常有用,例如在跟踪训练统计信息或进行预测时。需要注意的是,监控器必须与其所处理的事件兼容。也就是说,嵌入的
MEASURER必须能够接受与该事件相关联的参数。
[读取器] MEASURER MONITOR (:MEASURER)
它本身必须是一个监控器,这意味着在其上定义了
APPLY-MONITOR(但请参阅 [监控] e668)。返回的值由COUNTER进行汇总。有关度量器的库,请参阅 度量器。
6.2 测量器
MEASURER 是 MONITOR 对象的一部分,是一种嵌入式监控器,它根据所应用事件的参数(例如模型结果)计算特定的度量值(例如分类准确率)。测量器通常通过将某种模型特定的提取器与通用的测量函数结合来实现。
所有通用的测量函数都会将其结果作为多个值返回,这些值与特定类型计数器的 ADD-TO-COUNTER 参数相匹配(参见 计数器),以便于在 MONITOR 中使用:
(multiple-value-call #'add-to-counter <some-counter>
<call-to-some-measurer>)
以这种方式与测量器兼容的计数器类会在每个函数中注明。
有关测量函数的列表,请参阅 分类测量器。
6.3 计数器
[通用函数] ADD-TO-COUNTER 计数器 &REST ARGS
以某种方式将
ARGS加到计数器上。具体的行为请参阅针对不同类型的专用方法文档。所支持的参数类型,正是度量函数(见 Measurers)预期与该计数器配对时返回的多值。
[通用函数] COUNTER-VALUES 计数器
返回任意数量的值,用于表示
计数器的状态。具体的行为请参阅针对不同类型的专用方法文档。
[通用函数] COUNTER-RAW-VALUES 计数器
返回任意数量的值,这些值能够精确地表示
计数器的当前状态;如果将这些返回值作为参数传递给同一类型的全新实例上的ADD-TO-COUNTER,就能完全恢复到原始状态。
[通用函数] RESET-COUNTER 计数器
将
计数器的状态重置为刚创建时的状态。
6.3.1 属性
[类] ATTRIBUTED
这是一个所有计数器都继承的工具类。其
ATTRIBUTESplist 可以存储几乎任何内容。目前,这些属性仅在打印时使用,并且可以由用户自定义。诸如 Classification Monitors 中的监控器生成函数也会在其创建的计数器中添加额外的属性。使用
:PREPEND-ATTRIBUTES初始化参数,可以轻松添加新属性,而不会覆盖:INITFORM中已有的属性(例如本例中的:TYPE"rmse")。(princ (make-instance 'rmse-counter :prepend-attributes '(:event "pred." :dataset "test"))) ;; pred. test rmse: 0.000e+0 (0) => #<RMSE-COUNTER pred. test rmse: 0.000e+0 (0)>
[访问器] ATTRIBUTES ATTRIBUTED (:ATTRIBUTES = NIL)
一个包含属性键和值的 plist。
[方法] NAME (ATTRIBUTED ATTRIBUTED)
从
ATTRIBUTED的ATTRIBUTES中提取值并拼接成字符串。如果存在多个具有相同键的条目,则它们会紧密相邻地显示。值可以根据外部包裹的
WITH-PADDED-ATTRIBUTE-PRINTING进行填充。
[宏] WITH-PADDED-ATTRIBUTE-PRINTING (ATTRIBUTEDS) &BODY BODY
记录每个属性键对应的值宽度,即该值通过
PRINC-TO-STRING转换后的字符数。在BODY中,如果打印具有相同键的属性,则强制使它们至少达到这个宽度。这样可以产生类似表格的美观输出:(let ((attributeds (list (make-instance 'basic-counter :attributes '(:a 1 :b 23 :c 456)) (make-instance 'basic-counter :attributes '(:a 123 :b 45 :c 6))))) (with-padded-attribute-printing (attributeds) (map nil (lambda (attributed) (format t "~A~%" attributed)) attributeds))) ;; 1 23 456: 0.000e+0 (0) ;; 123 45 6 : 0.000e+0 (0)
6.3.2 计数器类
除了这里介绍的基本计数器外,还可参阅 Classification Counters。
[类] BASIC-COUNTER ATTRIBUTED
一种简单的计数器,其
ADD-TO-COUNTER接受两个额外的参数:分别称为NUMERATOR和DENOMINATOR的内部累加值。COUNTER-VALUES返回两个值:NUMERATOR除以DENOMINATOR(如果DENOMINATOR为 0,则返回 0),以及DENOMINATOR
下面是一个示例,用于计算分两批接收的 5 个数值的平均值:
(let ((counter (make-instance 'basic-counter))) (add-to-counter counter 6.5 3) (add-to-counter counter 3.5 2) counter) => #<BASIC-COUNTER 2.00000e+0 (5)>
[类] RMSE-COUNTER BASIC-COUNTER
一种
BASIC-COUNTER,其分子部分累积的是某些统计量的平方。它具有属性:TYPE"rmse"。COUNTER-VALUES返回的是BASIC-COUNTER的COUNTER-VALUES所得结果的平方根。(let ((counter (make-instance 'rmse-counter))) (add-to-counter counter (+ (* 3 3) (* 4 4)) 2) counter) => #<RMSE-COUNTER rmse: 3.53553e+0 (2)>
[类] CONCAT-COUNTER ATTRIBUTED
一种简单地将序列连接起来的计数器。
(let ((counter (make-instance 'concat-counter))) (add-to-counter counter '(1 2 3) #(4 5)) (add-to-counter counter '(6 7)) (counter-values counter)) => (1 2 3 4 5 6 7)
[读取器] CONCATENATION-TYPE CONCAT-COUNTER (:CONCATENATION-TYPE = 'LIST)
一个适合作为
CONCATENATE函数RESULT-TYPE参数的类型指示符。
7 分类
[在包 MGL-CORE 中]
为了能够度量与分类相关的指标,我们需要定义实例的标签是什么。可以通过为特定类型的实例实现一个方法来进行自定义,但这些函数通常只作为默认实现出现,可以被覆盖。
[通用函数] LABEL-INDEX INSTANCE
返回
INSTANCE的标签,表示为一个非负整数。
[通用函数] LABEL-INDEX-DISTRIBUTION INSTANCE
返回一个一维概率数组,表示标签的分布。其中,标签索引为
LABEL-INDEXI的概率,即为返回数组中索引为I的元素值。
以下两个函数基本上与前两个函数相同,但以批处理模式运行:它们分别返回标签索引序列或标签分布序列。这些函数通常用于模型生成的结果上。为某个模型实现这些函数后,下面的监控器生成函数将自动生效。参见 FIXDOC: for bpn and boltzmann。
[通用函数] LABEL-INDICES RESULTS
返回由某个模型对一批实例生成的
RESULTS的标签索引序列。这类似于LABEL-INDEX。
[通用函数] LABEL-INDEX-DISTRIBUTIONS RESULT
返回由某个模型对一批实例生成的
RESULTS的标签索引分布序列。这类似于LABEL-INDEX-DISTRIBUTION。
7.1 分类监控器
以下函数会返回一组监控器列表。这些监控器适用于签名为 (INSTANCES MODEL) 的事件,例如由 MONITOR-MODEL-RESULTS 及其各种模型特定变体产生的事件。它们是与模型无关的函数,可以扩展到新的分类器类型。
[函数] MAKE-CLASSIFICATION-ACCURACY-MONITORS MODEL &KEY OPERATION-MODE ATTRIBUTES (LABEL-INDEX-FN #'LABEL-INDEX)
返回与
CLASSIFICATION-ACCURACY-COUNTER关联的一组MONITOR对象。LABEL-INDEX-FN是一个类似于LABEL-INDEX的函数。更多信息请参阅该函数。
[函数] MAKE-CROSS-ENTROPY-MONITORS MODEL &KEY OPERATION-MODE ATTRIBUTES (LABEL-INDEX-DISTRIBUTION-FN #'LABEL-INDEX-DISTRIBUTION)
返回与
CROSS-ENTROPY-COUNTER关联的一组MONITOR对象。LABEL-INDEX-DISTRIBUTION-FN是一个类似于LABEL-INDEX-DISTRIBUTION的函数。更多信息请参阅该函数。
[函数] MAKE-LABEL-MONITORS MODEL &KEY OPERATION-MODE ATTRIBUTES (LABEL-INDEX-FN #'LABEL-INDEX) (LABEL-INDEX-DISTRIBUTION-FN #'LABEL-INDEX-DISTRIBUTION)
返回分类准确率和交叉熵监控器。参数说明请参阅
MAKE-CLASSIFICATION-ACCURACY-MONITORS和MAKE-CROSS-ENTROPY-MONITORS。
上述监控器生成函数可以通过以下通用函数扩展,以支持新的分类器类型。
[通用函数] MAKE-CLASSIFICATION-ACCURACY-MONITORS* MODEL OPERATION-MODE LABEL-INDEX-FN ATTRIBUTES
与
MAKE-CLASSIFICATION-ACCURACY-MONITORS完全相同,只是缺少关键字参数。可通过专门化此函数来增加对新模型类型的支持。默认实现也允许一定的扩展性:如果MODEL上定义了LABEL-INDICES,则会使用它从模型结果中提取标签索引。
[通用函数] MAKE-CROSS-ENTROPY-MONITORS* MODEL OPERATION-MODE LABEL-INDEX-DISTRIBUTION-FN ATTRIBUTES
与
MAKE-CROSS-ENTROPY-MONITORS完全相同,只是缺少关键字参数。可通过专门化此函数来增加对新模型类型的支持。默认实现同样具有一定的扩展性:如果MODEL上定义了LABEL-INDEX-DISTRIBUTIONS,则会使用它从模型结果中提取标签分布。
7.2 分类度量
此处的函数将某个已知的良好解(也称为真值或目标)与预测或近似值进行比较,并返回它们之间[不]相似性的度量。这些函数与模型无关,因此需要先提取真值和预测值。它们很少被直接使用,通常隐藏在分类监控器之后。
[函数] MEASURE-CLASSIFICATION-ACCURACY TRUTHS PREDICTIONS &KEY (TEST #'EQL) TRUTH-KEY PREDICTION-KEY WEIGHT
返回正确分类的数量,第二个值为实例数量(在非加权情况下等于
TRUTHS的长度)。TRUTHS(由TRUTH-KEY键控)是一个不透明的类别标签序列,通过TEST与PREDICTIONS中的另一个类别标签序列(由PREDICTION-KEY键控)进行比较。如果WEIGHT非空,则它是一个函数,用于返回TRUTHS中元素的权重。在加权情况下,两个计数(分别作为第一个和第二个值返回)会加上该元素的权重,而不是像非加权情况那样只加1。注意,返回的值非常适合与
MULTIPLE-VALUE-CALL结合使用,配合#ADD-TO-COUNTER62de和一个CLASSIFICATION-ACCURACY-COUNTER。
[函数] MEASURE-CROSS-ENTROPY TRUTHS PREDICTIONS &KEY TRUTH-KEY PREDICTION-KEY (MIN-PREDICTION-PR 1.0d-15)
返回
TRUTHS和PREDICTIONS中具有相同索引的元素对之间的交叉熵之和。TRUTH-KEY是一个函数,当应用于TRUTHS中的元素时,会返回一个表示某种离散目标分布(下文中的P)的序列。TRUTH-KEY可以是NIL,这等同于IDENTITY函数。PREDICTION-KEY则是PREDICTIONS中类似的键,但它返回的序列代表了对真实分布的近似(下文中的Q)。真实分布与近似分布的交叉熵定义如下:
cross-entropy(p,q) = - sum_i p(i) * log(q(i))本函数返回的是根据
TRUTH-KEY和PREDICTION-KEY键控的TRUTHS和PREDICTIONS中元素对的交叉熵之和。由于涉及对数运算,当q(i)接近零时,可能会出现数值问题。为避免这种情况,所有小于
MIN-PREDICTION-PR的q(i)都会被视为等于MIN-PREDICTION-PR。函数返回的第二个值是所有
TRUTHS中p(i)的总和。这通常等于(LENGTH TRUTHS),因为TRUTHS中的元素应构成一个概率分布,但这一约束并未强制执行,从而允许控制各元素的相对重要性。函数返回的第三个值是一个属性列表,将分布序列中出现的每个索引映射到一个包含两个元素的列表:
sum_j p_j(i) * log(q_j(i))和
sum_j p_j(i)其中
J索引TRUTHS和PREDICTIONS。(measure-cross-entropy '((0 1 0)) '((0.1 0.7 0.2))) => 0.35667497 1 (2 (0.0 0) 1 (0.35667497 1) 0 (0.0 0))注意,返回的值非常适合与
MULTIPLE-VALUE-CALL结合使用,配合#ADD-TO-COUNTER62de和一个CROSS-ENTROPY-COUNTER。
[函数] MEASURE-ROC-AUC PREDICTIONS PRED &KEY (KEY #'IDENTITY) WEIGHT
返回代表二分类问题预测的
PREDICTIONS的ROC曲线下的面积。PRED是一个谓词函数,用于判断某个预测是否属于所谓的正类。KEY为每个元素返回一个数值,表示预测者认为该元素属于该类的可能性大小,尽管这并不一定是概率。如果
WEIGHT为NIL,则PREDICTIONS中的所有元素在AUC的未归一化求和中都按1计算。否则,WEIGHT必须是一个类似于KEY的函数,但它应返回元素的重要性(一个正实数)。如果某个预测的权重为2,则相当于在PREDICTIONS中存在另一份完全相同的副本。该算法基于汤姆·福塞特所著论文《ROC分析导论》中的算法2。
ROC AUC等于随机选择的一个正例其
KEY(得分)高于随机选择的一个负例的概率。考虑到得分可能相同的情况,更精确的说法是:AUC是上述概率在所有可能的按得分排序的序列上的期望值。
[函数] MEASURE-CONFUSION TRUTHS PREDICTIONS &KEY (TEST #'EQL) TRUTH-KEY PREDICTION-KEY WEIGHT
根据
TRUTHS和PREDICTIONS创建一个CONFUSION-MATRIX。TRUTHS(由TRUTH-KEY键控)是一个类别标签序列,通过TEST与PREDICTIONS中的另一个类别标签序列(由PREDICTION-KEY键控)进行比较。如果WEIGHT非空,则它是一个函数,用于返回TRUTHS中元素的权重。在加权情况下,两个计数(分别作为第一个和第二个值返回)会加上该元素的权重。注意,返回的混淆矩阵可以使用
ADD-TO-COUNTER添加到另一个混淆矩阵中。
7.3 分类计数器
[类] 分类准确率计数器 基础计数器
一个
BASIC-COUNTER,其:TYPE属性为 "acc.",并具有一个打印百分比的PRINT-OBJECT方法。
[类] 交叉熵计数器 基础计数器
一个
BASIC-COUNTER,其:TYPE属性为 "xent"。
7.3.1 混淆矩阵
[类] 混淆矩阵
混淆矩阵用于记录分类结果。正确的类别称为
target,分类器的输出称为prediction。
[函数] MAKE-CONFUSION-MATRIX &KEY (TEST #'EQL)
类别使用
TEST进行比较。
[泛型函数] SORT-CONFUSION-CLASSES MATRIX CLASSES
返回一个按展示目的排序的
CLASSES列表。
[泛型函数] CONFUSION-CLASS-NAME MATRIX CLASS
为展示目的命名
CLASS。
- [泛型函数] CONFUSION-COUNT MATRIX TARGET PREDICTION
[泛型函数] MAP-CONFUSION-MATRIX FN MATRIX
对混淆矩阵中的每个单元格调用
FN,传入TARGET、PREDICTION和COUNT参数。可以省略计数为零的单元格。
[泛型函数] CONFUSION-MATRIX-CLASSES MATRIX
所有类别的列表。默认是从计数中收集类别,但如果某些类别在结果中不存在,则可以覆盖此行为。
[函数] CONFUSION-MATRIX-ACCURACY MATRIX &KEY FILTER
返回
MATRIX中结果的整体准确率。计算方法是正确分类的案例数(命中)除以总案例数。同时返回命中数和总案例数作为第二个和第三个值。如果提供了FILTER函数,则将其与单元格的目标和预测一起调用;对于FILTER返回NIL的单元格,将忽略该单元格。虽然可以通过提供适当的过滤器轻松计算精确度和召回率,但这些指标也有专门的便捷函数提供。
[函数] CONFUSION-MATRIX-PRECISION MATRIX PREDICTION
返回当分类器预测为
PREDICTION时的准确率。
[函数] CONFUSION-MATRIX-RECALL MATRIX TARGET
返回当正确类别为
TARGET时的准确率。
[函数] ADD-CONFUSION-MATRIX MATRIX RESULT-MATRIX
将
MATRIX添加到RESULT-MATRIX中。
8 特征
[在 MGL-CORE 包中]
8.1 特征选择
以下所有评分函数均返回一个 EQUAL 哈希表,将特征映射到分数。
[函数] COUNT-FEATURES DOCUMENTS MAPPER &KEY (KEY #'IDENTITY)
返回加权特征,形式为一个
EQUAL哈希表,其键为DOCUMENTS的特征,值为特征出现的次数。MAPPER接受一个函数和一份文档,并用文档的特征调用该函数。(sort (alexandria:hash-table-alist (count-features '(("hello" "world") ("this" "is" "our" "world")) (lambda (fn document) (map nil fn document)))) #'string< :key #'car) => (("hello" . 1) ("is" . 1) ("our" . 1) ("this" . 1) ("world" . 2))
[函数] FEATURE-LLRS DOCUMENTS MAPPER CLASS-FN &KEY (CLASSES (ALL-DOCUMENT-CLASSES DOCUMENTS CLASS-FN))
返回加权特征,形式为一个
EQUAL哈希表,其键为DOCUMENTS的特征,值为它们的对数似然比。MAPPER接受一个函数和一份文档,并用文档的特征调用该函数。(sort (alexandria:hash-table-alist (feature-llrs '((:a "hello" "world") (:b "this" "is" "our" "world")) (lambda (fn document) (map nil fn (rest document))) #'first)) #'string< :key #'car) => (("hello" . 2.6032386) ("is" . 2.6032386) ("our" . 2.6032386) ("this" . 2.6032386) ("world" . 4.8428774e-8))
[函数] FEATURE-DISAMBIGUITIES DOCUMENTS MAPPER CLASS-FN &KEY (CLASSES (ALL-DOCUMENT-CLASSES DOCUMENTS CLASS-FN))
返回加权特征,形式为一个
EQUAL哈希表,其键为DOCUMENTS的特征,值为它们的歧义性。MAPPER接受一个函数和一份文档,并用文档的特征调用该函数。引自论文《利用歧义度量特征选择算法进行支持向量机分类》。
8.2 特征编码
特征很少能够直接输入算法,通常需要经过某种形式的转换。假设我们有一个简单的语言模型,它以单个词作为输入,并预测下一个词。然而,输入和输出都需要被编码为长度为1000的浮点向量。我们的做法是根据某种度量(参见特征选择)找出出现频率最高的1000个词,并将这些词与整数[0..999]一一对应起来(这就是ENCODE的过程)。例如,通过使用独热编码,我们在输入时会将一个词转换成一个浮点向量。当模型输出下一个词的概率分布时,我们会找到概率最大的索引,并查找出与之对应的词(这就是DECODE的过程)。
[通用函数] ENCODE ENCODER DECODED
使用
ENCODER对DECODED进行编码。这个接口足够通用,以至于几乎没有任何实际意义。有关简单示例,请参阅ENCODER/DECODER;更复杂的示例则可参考MGL-NLP:BAG-OF-WORDS-ENCODER。如果
ENCODER是一个函数标识符,则只需用DECODED对其调用FUNCALL即可。
[通用函数] DECODE DECODER ENCODED
使用
DECODER对ENCODED进行解码。对于一对DECODER和ENCODER来说,(DECODE DECODER (ENCODE ENCODER OBJECT))在某种意义上必须等于OBJECT。如果
DECODER是一个函数标识符,则只需用ENCODED对其调用FUNCALL即可。
[类] ENCODER/DECODER
通过内部维护一个从解码值到编码值以及从编码值到解码值的
EQUAL哈希表,实现O(1)时间复杂度的ENCODE和DECODE操作。只要哈希表中的元素具有读写一致性,ENCODER/DECODER对象就可以被保存和加载(参见持久化)。(let ((indexer (make-indexer (alexandria:alist-hash-table '(("I" . 3) ("me" . 2) ("mine" . 1))) 2))) (values (encode indexer "I") (encode indexer "me") (encode indexer "mine") (decode indexer 0) (decode indexer 1) (decode indexer 2))) => 0 => 1 => NIL => "I" => "me" => NIL
[函数] MAKE-INDEXER SCORED-FEATURES N &KEY (START 0) (CLASS 'ENCODER/DECODER)
从
SCORED-FEATURES中选取前N个特征(参见特征选择),并从START开始为其分配索引。返回一个ENCODER/DECODER(或其它CLASS),用于在对象和索引之间进行转换。另请参阅词袋模型。
9 基于梯度的优化
[位于MGL-OPT包中]
我们有一个实值可微函数F,任务是找到使其取最小值的参数。优化过程从F的参数空间中的某一点开始,然后基于当前点或其附近点的梯度和函数值,迭代地更新该点。
需要注意的是,尽管问题描述为全局优化,但对于非凸函数而言,大多数优化算法往往会收敛到局部最优解。
目前有两种优化算法: 梯度下降法(及其多种变体)和共轭梯度法,它们都属于一阶方法(不需要二阶导数),但可以通过扩展API添加更多算法。
[函数] MINIMIZE OPTIMIZER GRADIENT-SOURCE &KEY (WEIGHTS (LIST-SEGMENTS GRADIENT-SOURCE)) (DATASET *INFINITELY-EMPTY-DATASET*)
通过更新
GRADIENT-SOURCE所代表的实值函数的部分参数(即WEIGHTS,可以是MAT或一系列MAT),来最小化该函数的值。最后返回WEIGHTS。DATASET(参见数据集)是同一函数的一组未优化参数。例如,WEIGHTS可能是神经网络的权重,而DATASET则是由适合SET-INPUT的输入组成的训练集。默认的DATASET(*INFINITELY-EMPTY-DATASET*)适用于所有参数均已优化的情况,此时环境中已无任何可供获取的信息。当
DATASET为采样器且样本耗尽时,或者满足其他终止条件时(参见TERMINATION等),优化过程将终止。如果DATASET是SEQUENCE类型,则会反复循环使用。
9.1 迭代优化器
[读取器] N-INSTANCES 迭代优化器 (:N-INSTANCES = 0)
此优化器迄今为止已处理的实例数量。在优化过程中自动递增。
[访问器] TERMINATION 迭代优化器 (:TERMINATION = NIL)
如果是一个数字,则表示以
N-INSTANCES为单位的训练实例数。当N-INSTANCES等于或大于该值时,优化将停止。如果TERMINATION为NIL,则优化将继续进行。如果为T,则优化将停止。如果它是一个无参数的函数,则其返回值将被视为由TERMINATION返回的值来处理。
[访问器] ON-OPTIMIZATION-STARTED 迭代优化器 (:ON-OPTIMIZATION-STARTED = NIL)
一个带有参数
(OPTIMIZER GRADIENT-SOURCE N-INSTANCES)的事件钩子。在完成初始化(INITIALIZE-OPTIMIZER*、INITIALIZE-GRADIENT-SOURCE*)之后,但在优化开始之前调用。
[访问器] ON-OPTIMIZATION-FINISHED 迭代优化器 (:ON-OPTIMIZATION-FINISHED = NIL)
一个带有参数
(OPTIMIZER GRADIENT-SOURCE N-INSTANCES)的事件钩子。在优化完成后调用。
[访问器] ON-N-INSTANCES-CHANGED 迭代优化器 (:ON-N-INSTANCES-CHANGED = NIL)
一个带有参数
(OPTIMIZER GRADIENT-SOURCE N-INSTANCES)的事件钩子。在一批实例的优化完成且N-INSTANCES递增时调用。
现在让我们讨论几个实用的工具。
[函数] MONITOR-OPTIMIZATION-PERIODICALLY OPTIMIZER PERIODIC-FNS
对于
PERIODIC-FNS列表中的每个周期性函数,将其监控添加到OPTIMIZER的ON-OPTIMIZATION-STARTED、ON-OPTIMIZATION-FINISHED和ON-N-INSTANCES-CHANGED钩子中。这些监控是简单的函数,它们只是使用事件参数(OPTIMIZERGRADIENT-SOURCEN-INSTANCES)调用每个周期性函数。返回OPTIMIZER。要在
OPTIMIZER每看到1000个实例后记录并重置梯度源的监控:(monitor-optimization-periodically optimizer '((:fn log-my-test-error :period 2000) (:fn reset-optimization-monitors :period 1000 :last-eval 0)))注意,我们不需要传递
PERIODIC-FN本身,而是可以直接传递PERIODIC-FN的初始化参数。:LAST-EVAL为0的部分可以防止RESET-OPTIMIZATION-MONITORS在优化开始时被调用,因为那时监控本来就是空的。
[通用函数] RESET-OPTIMIZATION-MONITORS OPTIMIZER GRADIENT-SOURCE
报告
OPTIMIZER和GRADIENT-SOURCE的MONITORS状态,并重置它们的计数器。参见MONITOR-OPTIMIZATION-PERIODICALLY了解其使用示例。
[方法] RESET-OPTIMIZATION-MONITORS (OPTIMIZER ITERATIVE-OPTIMIZER) GRADIENT-SOURCE
记录
OPTIMIZER和GRADIENT-SOURCE的监控计数器,并将其重置。
[通用函数] REPORT-OPTIMIZATION-PARAMETERS OPTIMIZER GRADIENT-SOURCE
一种常在优化开始时调用的实用函数(来自
ON-OPTIMIZATION-STARTED)。默认实现会记录GRADIENT-SOURCE(如DESCRIBE所示)和OPTIMIZER的描述,并调用LOG-MAT-ROOM。
9.2 损失函数
通常将被最小化的函数称为成本或损失函数。
[通用函数] COST MODEL
返回正在被最小化的成本函数的值。只有在正在进行的优化过程中调用此函数才有意义(参见
MINIMIZE)。成本是指一批实例的成本。
[函数] MAKE-COST-MONITORS MODEL &KEY OPERATION-MODE ATTRIBUTES
返回一个
MONITOR对象列表,每个对象都与一个属性为:TYPE"cost"的BASIC-COUNTER相关联。基于MAKE-COST-MONITORS*实现。
[通用函数] MAKE-COST-MONITORS* MODEL OPERATION-MODE ATTRIBUTES
与
MAKE-COST-MONITORS相同,只是多了一些关键字参数。可以通过专门化此函数来支持新的模型类型。
9.3 梯度下降
[在包 MGL-GD 中]
梯度下降是一种一阶优化算法。它完全依赖于一阶导数,甚至不会评估要被最小化的函数。让我们看看如何针对某些参数最小化一个数值 Lisp 函数。
(cl:defpackage :mgl-example-sgd
(:use #:common-lisp #:mgl))
(in-package :mgl-example-sgd)
;;; 创建一个表示正弦函数的对象。
(defparameter *diff-fn-1*
(make-instance 'mgl-diffun:diffun
:fn #'sin
;; 我们将优化它的唯一参数。
:weight-indices '(0)))
;;; 最小化 SIN。注意这里没有数据集参与,因为所有参数都在被优化。
(minimize (make-instance 'sgd-optimizer :termination 1000)
*diff-fn-1*
:weights (make-mat 1))
;;; => 一个 MAT,其中只有一个值,约为 -pi/2。
;;; 创建一个可微函数 f(x,y)=(x-y)^2。其中 x 是一个参数,
;;; 其值来自传递给 MINIMIZE 的 DATASET 参数。y 是需要优化的参数(即“权重”)。
(defparameter *diff-fn-2*
(make-instance 'mgl-diffun:diffun
:fn (lambda (x y)
(expt (- x y) 2))
:parameter-indices '(0)
:weight-indices '(1)))
;;; 找到使与采样器生成的样本距离最小的 y 值。
(minimize (make-instance 'sgd-optimizer :batch-size 10)
*diff-fn-2*
:weights (make-mat 1)
:dataset (make-instance 'function-sampler
:generator (lambda ()
(list (+ 10
(gaussian-random-1))))
:max-n-samples 1000))
;;; => 一个包含单个值约为 10 的 MAT,这是数据集中样本的期望值。
;;; 数据集也可以是一个 SEQUENCE,在这种情况下我们最好设置 TERMINATION,
;;; 否则优化过程将永远不会结束。
(minimize (make-instance 'sgd-optimizer :termination 1000)
*diff-fn-2*
:weights (make-mat 1)
:dataset '((0) (1) (2) (3) (4) (5)))
;;; => 一个包含单个值约为 2.5 的 MAT。
我们将看到一些用于访问优化器参数的访问器。
一般来说,在优化过程中可以随时使用 SETF 修改可变槽访问器(与只读和只写访问器不同),
也可以在优化器子类上定义方法来以任何方式计算该值。例如,按每个小批量衰减学习率:
(defmethod learning-rate ((optimizer my-sgd-optimizer))
(* (slot-value optimizer 'learning-rate)
(expt 0.998
(/ (n-instances optimizer) 60000))))
9.3.1 基于批次的优化器
首先让我们看看所有基于批次的优化器共有的内容,
然后再讨论 SGD 优化器、Adam 优化器 和
归一化批次优化器。所有基于批次的优化器都是
ITERATIVE-OPTIMIZER,因此也请参阅
迭代优化器。
[类] BATCH-GD-OPTIMIZER ITERATIVE-OPTIMIZER
这是另一个基于梯度的优化器的抽象基类, 它会在处理完
BATCH-SIZE个输入后同时更新所有权重。其子类包括SGD-OPTIMIZER、ADAM-OPTIMIZER和NORMALIZED-BATCH-GD-OPTIMIZER。当某些权重可能由于缺少输入值而未被使用时,
PER-WEIGHT-BATCH-GD-OPTIMIZER可能是更好的选择。
[访问器] BATCH-SIZE GD-OPTIMIZER (:BATCH-SIZE = 1)
在处理完
BATCH-SIZE个输入后,权重会被更新。当BATCH-SIZE为 1 时,就得到随机梯度下降; 当BATCH-SIZE等于数据集中的实例数量时,就得到标准的“批”梯度下降; 而当BATCH-SIZE大小介于两者之间时,就得到了最实用的“小批量”折中方案。
[访问器] LEARNING-RATE GD-OPTIMIZER (:LEARNING-RATE = 0.1)
这是沿着梯度方向的步长。如果优化发散,就减小它; 如果优化没有进展,则增大它。
[访问器] MOMENTUM GD-OPTIMIZER (:MOMENTUM = 0)
取值范围为 [0, 1)。
MOMENTUM倍的前一次权重变化会加到梯度上。0 表示没有动量。
[读者] MOMENTUM-TYPE GD-OPTIMIZER (:MOMENTUM-TYPE = :NORMAL)
可以是
:NORMAL、:NESTEROV或:NONE中的一种。对于纯粹的优化问题,Nesterov 动量可能更好, 但同时也可能增加过拟合的风险。使用:NONE相当于动量为 0,而且占用更少的内存。需要注意的是, 即使MOMENTUM不为零,使用:NONE时也会忽略MOMENTUM。
[访问器] WEIGHT-DECAY GD-OPTIMIZER (:WEIGHT-DECAY = 0)
这是一种 L2 正则化惩罚项。它会抑制大权重,类似于零均值高斯先验。
WEIGHT-DECAY乘以权重会被加到梯度上, 以惩罚过大的权重。这相当于在所求极小值的函数中添加了WEIGHT-DECAY*sum_i{0.5 * WEIGHT_i^2}。
[访问器] WEIGHT-PENALTY GD-OPTIMIZER (:WEIGHT-PENALTY = 0)
这是一种 L1 正则化惩罚项。它会鼓励稀疏性。
SIGN(WEIGHT) 乘以WEIGHT-PENALTY会被加到梯度上,从而推动权重趋向于负无穷。这相当于在所求极小值的函数中添加了WEIGHT-PENALTY*sum_i{abs(WEIGHT_i)}。将其应用于特征偏置,则会对特征施加稀疏性约束。
[读者] USE-SEGMENT-DERIVATIVES-P GD-OPTIMIZER (:USE-SEGMENT-DERIVATIVES-P = NIL)
如果梯度来源(即被优化的模型)和优化器都支持此功能,则可以节省内存。其工作原理如下: 梯度来源会被要求将某个片段的导数放入一个累加器中,这个累加器就是该片段的
SEGMENT-DERIVATIVES。 这样优化器就不必再分配一个用于汇总导数的矩阵。
[访问器] AFTER-UPDATE-HOOK GD-OPTIMIZER (:AFTER-UPDATE-HOOK = NIL)
一组无参数函数,在每次权重更新后被调用。
[访问器] BEFORE-UPDATE-HOOK BATCH-GD-OPTIMIZER (:BEFORE-UPDATE-HOOK = NIL)
一组无参数函数。每个函数在权重更新之前被调用(即在累积的梯度被除以批次长度之后)。 这很方便用来附加一些额外的梯度累积代码。
SGD 优化器
- [类] SGD-OPTIMIZER BATCH-GD-OPTIMIZER
当BATCH-SIZE为1时,这就是随机梯度下降。当批量大小增大时,就变成了小批量梯度下降和批量梯度下降。
假设ACCUMULATOR包含了某个小批量的梯度之和,那么权重更新公式如下:
$$ \Delta_w^{t+1} = momentum * \Delta_w^t
- \frac{accumulator}{batchsize}
- l_2 w + l_1 sign(w) $$
$$ w^{t+1} = w^{t} - learningrate * \Delta_w, $$
这与更传统的形式相同:
$$ \Delta_w^{t+1} = momentum * \Delta_w^{t}
- learningrate * \left(\frac{\frac{df}{dw}}{batchsize} + l_2 w + l_1 sign(w)\right)
$$
$$ w^{t+1} = w^{t} - \Delta_w, $$
不过,在优化过程中批量大小、动量或学习率发生变化时,前者的效果更好。上述是使用普通动量的情况;此外,还有Nesterov动量(参见MOMENTUM-TYPE)可供选择。
有关所有基于批量的优化器共有的各种选项的说明,请参阅Batch Based Optimizers。
Adam优化器
[class] ADAM-OPTIMIZER BATCH-GD-OPTIMIZER
Adam是一种一阶随机梯度下降优化器。它通过指数移动平均法维护每个参数导数的均值和原始方差的内部估计。其步长基本上是
M/(sqrt(V)+E),其中M是估计的均值,V是估计的方差,而E是一个小调整因子,用于防止梯度爆炸。更多信息请参阅该论文的第5版(http://arxiv.org/abs/1412.6980)。需要注意的是,Adam不支持使用动量。事实上,如果动量不是
:NONE,系统会报错。有关所有基于批量的优化器共有的各种选项的说明,请参阅Batch Based Optimizers。
[accessor] LEARNING-RATE ADAM-OPTIMIZER (= 2.0e-4)
与
LEARNING-RATE相同,但采用了Adam论文中建议的默认值。
[accessor] MEAN-DECAY ADAM-OPTIMIZER (:MEAN-DECAY = 0.9)
一个介于0和1之间的数,决定了导数估计均值的更新速度。若设为0,则基本等同于
RMSPROP(前提是VARIANCE-DECAY不太大),或者AdaGrad(若VARIANCE-DECAY接近1且学习率逐渐衰减)。这在论文中对应$\beta_1$。
[accessor] MEAN-DECAY-DECAY ADAM-OPTIMIZER (:MEAN-DECAY-DECAY = (- 1 1.0d-7))
一个应接近1的值。每次更新后,
MEAN-DECAY都会乘以此值。这在论文中对应$\lambda$。
[accessor] VARIANCE-DECAY ADAM-OPTIMIZER (:VARIANCE-DECAY = 0.999)
一个介于0和1之间的数,决定了导数估计方差的更新速度。这在论文中对应$\beta_2$。
[accessor] VARIANCE-ADJUSTMENT ADAM-OPTIMIZER (:VARIANCE-ADJUSTMENT = 1.0d-7)
在Adam内部,估计的均值会被除以估计的方差的平方根(按每个权重计算),如果分母接近零,可能会导致数值问题。为了避免这种情况,会在分母上加上一个很小的正数
VARIANCE-ADJUSTMENT。这在论文中对应epsilon。
归一化批量优化器
[class] NORMALIZED-BATCH-GD-OPTIMIZER BATCH-GD-OPTIMIZER
类似于
BATCH-GD-OPTIMIZER,但它会记录每个权重在批次中被使用的次数,并用这个次数来除累积梯度,而不是除以N-INSTANCES-IN-BATCH。只有当所训练的学习器中存在缺失值时,这种做法才会产生差异。该类与PER-WEIGHT-BATCH-GD-OPTIMIZER的主要区别在于,所有权重的批次结束时间都是一致的。
[accessor] N-WEIGHT-USES-IN-BATCH NORMALIZED-BATCH-GD-OPTIMIZER
当前批次中该权重被使用的次数。
9.3.2 分段GD优化器
[class] SEGMENTED-GD-OPTIMIZER ITERATIVE-OPTIMIZER
这种优化器会将各个分段的训练委托给其他优化器。它可用于将不同分段的训练委托给不同的优化器(这些优化器必须能够处理可分段的数据),或者仅仅是为了不训练所有分段。
[reader] SEGMENTER SEGMENTED-GD-OPTIMIZER (:SEGMENTER)
当此优化器初始化时,它会遍历学习器的分段,使用
MAP-SEGMENTS。SEGMENTER是一个函数,会对每个分段调用一次,并返回一个优化器或NIL。多个分段可以映射到同一个优化器。在收集完分段到优化器的映射关系后,每个优化器都会通过INITIALIZE-OPTIMIZER方法,用分配给它的分段列表进行初始化。
- [reader] SEGMENTS SEGMENTED-GD-OPTIMIZER
SEGMENTED-GD-OPTIMIZER继承自ITERATIVE-OPTIMIZER,因此也请参阅迭代优化器。
9.3.3 按权重优化
- [class] PER-WEIGHT-BATCH-GD-OPTIMIZER ITERATIVE-OPTIMIZER
这与基于批处理的优化器非常相似,但它在何时更新权重方面更加智能。基本上,每个权重都有自己的批处理,与其他权重的批处理无关。这种设计具有理想的特点。例如,可以将两个神经网络组合在一起,而无需在它们之间添加任何连接,这样学习的结果将等同于它们各自独立训练的情况。此外,添加仅包含缺失值的输入也不会改变任何结果。
由于其完全非批处理的特性,目前没有该优化器的CUDA实现。
[访问器] N-WEIGHT-USES-IN-BATCH PER-WEIGHT-BATCH-GD-OPTIMIZER
该权重在其当前批处理中被使用的次数。
[函数] CLIP-L2-NORM MATS L2-UPPER-BOUND &KEY CALLBACK
将
MATS缩放,使其$L_2$范数不超过L2-UPPER-BOUND。将
MATS视为一个单一向量来计算其范数。如果范数大于L2-UPPER-BOUND,则以范数除以L2-UPPER-BOUND的比例对每个矩阵进行破坏性缩放;如果CALLBACK不为NIL,则使用缩放因子调用CALLBACK函数。
[函数] ARRANGE-FOR-CLIPPING-GRADIENTS BATCH-GD-OPTIMIZER L2-UPPER-BOUND &KEY CALLBACK
确保由
BATCH-GD-OPTIMIZER累积的批次归一化梯度的范数在每次更新前都被裁剪到L2-UPPER-BOUND。参见CLIP-L2-NORM。
9.4 共轭梯度法
[在MGL-CG包中]
共轭梯度法是一种一阶优化算法。它比梯度下降法更为先进,因为它会进行线搜索,但这也使得它不适合用于非确定性函数。下面我们来看看如何针对某些参数最小化一个数值Lisp函数。
;;; 创建一个表示正弦函数的对象。
(defparameter *diff-fn-1*
(make-instance 'mgl-diffun:diffun
:fn #'sin
;; 我们将优化其唯一的参数。
:weight-indices '(0)))
;;; 最小化SIN。注意这里没有数据集参与,因为所有参数都在被优化。
(minimize (make-instance 'cg-optimizer
:batch-size 1
:termination 1)
*diff-fn-1*
:weights (make-mat 1))
;;; => 一个MAT,其中只有一个值,约为-pi/2。
;;; 创建一个可微分的函数f(x,y)=(x-y)^2。x是一个来自传递给MINIMIZE的DATASET参数的值,y则是需要优化的参数(即“权重”)。
(defparameter *diff-fn-2*
(make-instance 'mgl-diffun:diffun
:fn (lambda (x y)
(expt (- x y) 2))
:parameter-indices '(0)
:weight-indices '(1)))
;;; 找出使距离最小化的y值,该距离是采样器生成的实例之间的距离。
(minimize (make-instance 'cg-optimizer :batch-size 10)
*diff-fn-2*
:weights (make-mat 1)
:dataset (make-instance 'function-sampler
:generator (lambda ()
(list (+ 10
(gaussian-random-1))))
:max-n-samples 1000))
;;; => 一个MAT,其中只有一个值,约为10,即数据集中实例的期望值。
;;; 数据集也可以是一个SEQUENCE,在这种情况下最好设置TERMINATION,否则优化将永远不会结束。请注意,只需一个epoch就足够了。
(minimize (make-instance 'cg-optimizer :termination 6)
*diff-fn-2*
:weights (make-mat 1)
:dataset '((0) (1) (2) (3) (4) (5)))
;;; => 一个MAT,其中只有一个值,约为2.5。
- [函数] CG FN W &KEY (MAX-N-LINE-SEARCHES *DEFAULT-MAX-N-LINE-SEARCHES*) (MAX-N-EVALUATIONS-PER-LINE-SEARCH *DEFAULT-MAX-N-EVALUATIONS-PER-LINE-SEARCH*) (MAX-N-EVALUATIONS *DEFAULT-MAX-N-EVALUATIONS*) (SIG *DEFAULT-SIG*) (RHO *DEFAULT-RHO*) (INT *DEFAULT-INT*) (EXT *DEFAULT-EXT*) (RATIO *DEFAULT-RATIO*) SPARE-VECTORS
CG-OPTIMIZER 会将每一批数据传递给此函数,并同时传递其
CG-ARGS。
使用共轭梯度法最小化一个可微的多元函数。采用 Polak-Ribiere 型共轭梯度来计算搜索方向,结合二次和三次多项式近似以及 Wolfe-Powell 停止准则进行线搜索,并使用斜率比方法来猜测初始步长。此外,还进行一系列检查以确保探索正在进行,且外推不会无限制地增大。
`FN` 是一个接受两个参数的函数:[`WEIGHTS`][ab3c] 和 `DERIVATIVES`。`WEIGHTS` 是一个与 `W` 大小相同的 `MAT`,表示搜索的起始点。`DERIVATIVES` 也是一个相同大小的 `MAT`,用于存放 `FN` 计算出的偏导数。`FN` 返回待最小化的函数值。
`CG` 会执行多次线搜索,并在每一步调用 `FN`。一次线搜索最多调用 `MAX-N-EVALUATIONS-PER-LINE-SEARCH` 次,可能会成功将最小值改进到足够大的幅度,也可能失败。需要注意的是,即使线搜索失败,仍有可能进一步改进结果,只是认为改进幅度太小而已。`CG` 会在以下任一情况下停止:
- 连续两次线搜索失败
- 达到 `MAX-N-LINE-SEARCHES`
- 达到 `MAX-N-EVALUATIONS`
`CG` 返回一个 `MAT`,其中包含最佳权重、最小值、已执行的线搜索次数、成功的线搜索次数以及评估次数。
使用 `MAX-N-EVALUATIONS` 时,请注意,在第一次线搜索之前还会额外评估一次 `FN`。
`SPARE-VECTORS` 是一组预先分配的与 `W` 大小相同的 `MAT`。传递 6 个即可满足当前算法的需求,从而完全避免动态分配大小为 `W` 的向量。
`注意`:如果函数在少数几次迭代内就终止,这可能表明函数值和导数值不一致(即 `FN` 函数的实现可能存在错误)。
`SIG` 和 `RHO` 是控制 Wolfe-Powell 条件的常数。`SIG` 是允许的前一次与新一次斜率(搜索方向上的导数)之间绝对值的最大比值,因此将 `SIG` 设置为较低的正值会提高线搜索的精度。`RHO` 是允许的最低分数,表示线搜索中从初始点斜率所预期的变化比例。常数必须满足 0 < `RHO` < `SIG` < 1。根据待优化函数的性质调整 `SIG` 可以加快最小化过程;而 `RHO` 则不太值得过多调整。
[变量] *DEFAULT-INT* 0.1
在当前区间极限的
INT范围内,不再重新评估。
[变量] *DEFAULT-EXT* 3
最多可以将当前步长外推
EXT倍。
[变量] *DEFAULT-SIG* 0.1
SIG和RHO是控制 Wolfe-Powell 条件的常数。SIG是允许的前一次与新一次斜率(搜索方向上的导数)之间绝对值的最大比值,因此将SIG设置为较低的正值会提高线搜索的精度。
[变量] *DEFAULT-RHO* 0.05
RHO是允许的最低分数,表示线搜索中从初始点斜率所预期的变化比例。常数必须满足 0 <RHO<SIG< 1。
[变量] *DEFAULT-RATIO* 10
允许的最大斜率比。
- [变量] *DEFAULT-MAX-N-LINE-SEARCHES* NIL
- [变量] *DEFAULT-MAX-N-EVALUATIONS-PER-LINE-SEARCH* 20
- [变量] *DEFAULT-MAX-N-EVALUATIONS* NIL
[类] CG-OPTIMIZER ITERATIVE-OPTIMIZER
在处理完
BATCH-SIZE个输入后,同时更新所有权重。
[访问器] BATCH-SIZE CG-OPTIMIZER (:BATCH-SIZE)
处理完
BATCH-SIZE个实例后,权重就会更新。通常,CG会作用于所有可用数据,但为了减少过拟合,使用较小的批次大小可以在优化过程中引入一些噪声。如果未设置BATCH-SIZE,则会在优化开始时将其初始化为数据集的大小。
- [访问器] CG-ARGS CG-OPTIMIZER (:CG-ARGS = 'NIL)
[访问器] ON-CG-BATCH-DONE CG-OPTIMIZER (:ON-CG-BATCH-DONE = NIL)
当共轭梯度批次处理完成后触发的事件钩子。该钩子上的处理器会被调用,并传入 8 个参数:
(优化器 梯度源 实例 最佳权重 最佳函数值 线搜索次数 成功线搜索次数 评估次数)其中后 5 个参数正是
CG函数的返回值。
[通用函数] LOG-CG-BATCH-DONE 优化器 梯度源 实例 最佳权重 最佳函数值 线搜索次数 成功线搜索次数 评估次数
这是一个可以添加到
ON-CG-BATCH-DONE的函数。默认实现只是简单地记录事件参数。
[读取器] SEGMENT-FILTER CG-OPTIMIZER (:SEGMENT-FILTER = (CONSTANTLY T))
一个用于筛选段落的兴趣性与否的谓词函数。由
INITIALIZE-OPTIMIZER*调用。
9.5 扩展 API
9.5.1 实现优化器
对于新的优化器类型,必须对以下通用函数进行专门化定义。
[通用函数] MINIMIZE* OPTIMIZER GRADIENT-SOURCE WEIGHTS DATASET
由
MINIMIZE在调用INITIALIZE-OPTIMIZER*和INITIALIZE-GRADIENT-SOURCE*之后调用,此通用函数是编写优化器的主要扩展点。
[通用函数] INITIALIZE-OPTIMIZER* OPTIMIZER GRADIENT-SOURCE WEIGHTS DATASET
此函数会在训练开始前自动调用,用于设置
OPTIMIZER以适于优化GRADIENT-SOURCE。通常会为梯度创建适当大小的累加器。
[通用函数] SEGMENTS OPTIMIZER
单个优化器可以优化多个被称为“段”的权重矩阵。此函数将它们作为一个列表返回。
其余部分仅作为实现优化器的实用工具。
[函数] TERMINATE-OPTIMIZATION-P N-INSTANCES TERMINATION
用于
ITERATIVE-OPTIMIZER子类的实用函数。它根据N-INSTANCES和TERMINATION(分别为ITERATIVE-OPTIMIZER相应访问器的值)来决定是否终止优化。
[函数] SET-N-INSTANCES OPTIMIZER GRADIENT-SOURCE N-INSTANCES
设置
OPTIMIZER的N-INSTANCES,并触发ON-N-INSTANCES-CHANGED。ITERATIVE-OPTIMIZER的子类必须调用此函数来递增N-INSTANCES。
[类] SEGMENT-SET
这是一个用于具有
SEGMENTS列表的优化器的工具类,该优化器能够在这几个段与一个单独的MAT(累加器)之间来回复制权重。
[读取器] SEGMENTS SEGMENT-SET (:SEGMENTS)
权重矩阵的列表。
[读取器] SIZE SEGMENT-SET
SEGMENTS中所有权重矩阵大小之和。
[宏] DO-SEGMENT-SET (SEGMENT &OPTIONAL START) SEGMENT-SET &BODY BODY
遍历
SEGMENT-SET中的SEGMENTS。如果指定了START,则将其绑定到SEGMENT在SEGMENT-SET中的起始索引。该起始索引是前面各段大小之和。
[函数] SEGMENT-SET<-MAT SEGMENT-SET MAT
将
MAT中的值复制到SEGMENT-SET的权重矩阵中,就好像它们被连接成一个单独的MAT一样。
[函数] SEGMENT-SET->MAT SEGMENT-SET MAT
将
SEGMENT-SET中的值复制到MAT中,就好像它们被连接成一个单独的MAT一样。
9.5.2 实现梯度源
权重可以以多种方式存储。优化器需要更新权重,因此假设权重存储在任意数量的称为“段”的MAT对象中。
本节中的通用函数除特别说明外,都必须针对新的梯度源进行专门化。
[通用函数] MAP-SEGMENTS FN GRADIENT-SOURCE
对
GRADIENT-SOURCE的每个段应用FN。
[通用函数] MAP-SEGMENT-RUNS FN SEGMENT
调用
FN时传入SEGMENT中连续且不缺失的索引区间的起始和结束位置。由支持部分更新的优化器调用。默认实现假定所有权重都存在。只有当计划使用能够处理未使用或缺失权重的优化器时,才需要对此进行专门化,例如MGL-GD:NORMALIZED-BATCH-GD-OPTIMIZER和OPTIMIZERMGL-GD:PER-WEIGHT-BATCH-GD-OPTIMIZER。
[通用函数] SEGMENT-WEIGHTS SEGMENT
返回
SEGMENT的权重矩阵。一个段本身不一定是一个MAT对象。例如,它可以是MGL-BM:BM的一个MGL-BM:CHUNK,或者MGL-BP:BPN5187的一个MGL-BP:LUMP,其NODES槽位保存着权重。
[方法] SEGMENT-WEIGHTS (MAT MAT)
当段确实是一个
MAT时,直接返回它。
[通用函数] SEGMENT-DERIVATIVES SEGMENT
返回
SEGMENT的导数矩阵。一个段本身不一定是一个MAT对象。例如,它可以是MGL-BM:BM的一个MGL-BM:CHUNK,或者MGL-BP:BPN5187的一个MGL-BP:LUMP,其DERIVATIVES槽位保存着梯度。
[函数] LIST-SEGMENTS GRADIENT-SOURCE
一个实用函数,通过对
GRADIENT-SOURCE调用MAP-SEGMENTS来返回段的列表。
[通用函数] INITIALIZE-GRADIENT-SOURCE* OPTIMIZER GRADIENT-SOURCE WEIGHTS DATASET
在调用
MINIMIZE*之前自动调用此函数,如果GRADIENT-SOURCE需要某种设置,则可以对其进行专门化。
[方法] INITIALIZE-GRADIENT-SOURCE* OPTIMIZER GRADIENT-SOURCE WEIGHTS DATASET
默认方法不做任何操作。
[通用函数] ACCUMULATE-GRADIENTS* GRADIENT-SOURCE SINK BATCH MULTIPLIER VALUEP
将一阶梯度之和乘以
MULTIPLIER后加到SINK的累加器中(通常通过DO-GRADIENT-SINK访问),并且如果VALUEP为真,则返回针对一批实例所优化函数的值之和。GRADIENT-SOURCE是表示所优化函数的对象,SINK则是梯度汇合处。注意,
BATCH中的实例数量可能大于GRADIENT-SOURCE一次能处理的数量(例如,根据MAX-N-STRIPES的限制),因此使用DO-BATCHES-FOR-MODEL或类似的方法(如将BATCH按MAX-N-STRIPES分组)可能会很有用。
9.5.3 实现梯度源
优化器会在梯度源上调用 ACCUMULATE-GRADIENTS*。ACCUMULATE-GRADIENTS* 的一个参数是 SINK。梯度源知道哪个分段对应哪个累加矩阵(如果有的话)。梯度源完全由 MAP-GRADIENT-SINK 定义。
[通用函数] MAP-GRADIENT-SINK FN SINK
对
SINK中的每个分段及其对应的累加矩阵MAT,调用带有 (SEGMENTACCUMULATOR) 参数列表的FN。
[宏] DO-GRADIENT-SINK ((SEGMENT ACCUMULATOR) SINK) &BODY BODY
是基于
MAP-GRADIENT-SINK的便捷宏。
10 可微函数
[在 MGL-DIFFUN 包中]
[类] DIFFUN
DIFFUN将一个 Lisp 函数(存储在其FNslot 中)包装成一个梯度源(参见 实现梯度源),从而使其能够用于MINIMIZE。请参阅 梯度下降 和 共轭梯度法 中的示例。
[读取器] FN DIFFUN (:FN)
一个实值 Lisp 函数。它可以有任意数量的参数。
[读取器] PARAMETER-INDICES DIFFUN (:PARAMETER-INDICES = NIL)
不参与优化的参数索引列表。这些参数的值将来自
MINIMIZE的DATASET参数。
11 反向传播神经网络
[在 MGL-BP 包中]
11.1 反向传播概述
反向传播神经网络只是一些具有大量称为“权重”的参数的函数,并且以层状结构呈现,可以表示为 计算图。该网络被训练用来 MINIMIZE 某种由网络计算出的 损失函数 值。
在本实现中,一个 BPN 由多个 LUMP 组成(大致对应于各层)。支持前馈神经网络和循环神经网络(分别为 FNN 和 RNN)。BPN 不仅可以包含 LUMP,还可以包含其他 BPN。正如我们所见,网络是由复合对象组成的,而复合与简单部分的抽象基类称为 CLUMP。
[类] CLUMP
CLUMP是一个LUMP或者一个BPN。它代表一个可微函数。CLUMP的参数在实例化时指定。有些参数本身就是CLUMP,因此它们会永久性地连接在一起,例如:(->v*m (->input :size 10 :name 'input) (->weight :dimensions '(10 20) :name 'weight) :name 'activation)上述代码创建了三个
CLUMP:名为ACTIVATION的向量-矩阵乘法CLUMP,它引用了其操作数:INPUT和WEIGHT。请注意,这个例子只是定义了一个函数,尚未进行实际计算。通过这种
CLUMP的连接方式,可以构建前馈网络(FNN)或循环神经网络(RNN),而这些网络本身也是CLUMP,因此可以根据需要以层次化的方式构建网络。非复合的CLUMP称为LUMP(注意去掉了代表复合的字母C)。各种LUMP子类型对应不同的层类型(->SIGMOID、->DROPOUT、->RELU、->TANH等)。
此时,您不妨先阅读 FNN 教程,以便对整个流程有一个直观的了解。
11.2 团块 API
这些主要是用于扩展目的。在正常操作中,从这里唯一需要用到的是在钳位输入或提取预测时的 NODES。
[通用函数] STRIPEDP CLUMP
为了效率,前向和反向传播阶段以批处理模式进行:将多个实例分批送入网络。因此,团块必须能够存储每个实例的值及其梯度。然而,有些团块对一个批次中的每个实例都产生相同的结果。这些团块就是权重,即网络的参数。
STRIPEDP在CLUMP不代表权重(即它不是->WEIGHT)时返回真。对于条纹团块,它们的
NODES和DERIVATIVES是MAT对象,其第一维度(二维情况下的行数)等于批次中的实例数量。非条纹团块的形状则没有限制,仅受其用途所决定。
[通用函数] NODES OBJECT
返回一个
MGL-MAT:MAT对象,表示OBJECT的状态或结果。返回矩阵的第一维度等于条纹的数量。
CLUMP 的 NODES 存储了最近一次 FORWARD 计算出的结果。对于 ->INPUT 团块,输入值应放置于此处(参见 SET-INPUT)。目前,该矩阵始终是二维的,但这一限制未来可能会取消。
[通用函数] DERIVATIVES CLUMP
返回表示
CLUMP所计算函数的偏导数的MAT对象。返回的偏导数是由之前的BACKWARD调用累积而来的。该矩阵的形状与
NODES返回的矩阵相同。
[通用函数] FORWARD CLUMP
计算由
CLUMP表示的函数在所有条纹上的值,并将结果放入CLUMP的NODES中。
[通用函数] BACKWARD CLUMP
计算由
CLUMP表示的函数的偏导数,并将其加到相应参数团块的DERIVATIVES中。CLUMP的DERIVATIVES包含所有团块对该输出的偏导数之和。此函数应在一次FORWARD之后调用。以应用网络到两个实例
x1和x2的批次时的->SIGMOID团块为例。x1和x2被设置在->INPUT团块 X 中。Sigmoid 计算1/(1+exp(-x)),其中X是其唯一的参数团块。f(x) = 1/(1+exp(-x))当对 Sigmoid 团块调用
BACKWARD时,其DERIVATIVES是一个 2x1 的MAT对象,包含损失函数的偏导数:dL(x1)/df dL(x2)/df现在,Sigmoid 的
BACKWARD方法需要将dL(x1)/dx1和dL(x2)/dx2加到X的DERIVATIVES中。由于dL(x1)/dx1 = dL(x1)/df * df(x1)/dx1,而第一项已经在 Sigmoid 的DERIVATIVES中,因此它只需要计算第二项。
此外,团块还需支持 SIZE、N-STRIPES、MAX-N-STRIPES(以及后两者的 SETF 方法),这些功能只需通过继承 BPN、FNN、RNN 或者 LUMP 即可实现。
11.3 BPNs
[读取器] N-STRIPES BPN (:N-STRIPES = 1)
网络当前拥有的实例数量。此值会自动设置为传递给
SET-INPUT的实例数量,因此很少需要直接操作,尽管也可以手动设置。当设置时,所有CLUMPS的N-STRIPES都会被设置为相同的值。
[读取器] MAX-N-STRIPES BPN (:MAX-N-STRIPES = NIL)
网络可以并行处理的最大实例数量。在
BUILD-FNN或BUILD-RNN内部,它默认为父网络的MAX-N-STRIPES,否则默认为 1。当设置时,所有CLUMPS的MAX-N-STRIPES都会被设置为相同的值。
[读取器] CLUMPS BPN (:CLUMPS = (MAKE-ARRAY 0 :ELEMENT-TYPE 'CLUMP :ADJUSTABLE T :FILL-POINTER T))
一个具有填充指针的拓扑排序可调整数组,其中保存着构成网络的团块。团块可以通过
ADD-CLUMP添加进去,或者更常见的是在BUILD-FNN或BUILD-RNN过程中自动添加。由于需求较少,大多数情况下使用FIND-CLUMP就足够了。
[函数] FIND-CLUMP NAME BPN &KEY (ERRORP T)
在
BPN的CLUMPS中查找名称为NAME的团块。如同往常一样,名称比较使用EQUAL。如果未找到,则返回NIL或根据ERRORP抛出错误。
[函数] ADD-CLUMP CLUMP BPN
将
CLUMP添加到BPN中。CLUMP的MAX-N-STRIPES会被设置为与BPN相同的值。如果尝试添加一个名称已被BPN的某个CLUMPS使用过的团块,则会引发错误。
11.3.1 训练
BPN 被训练以最小化其所计算的损失函数。在将 BPN 传递给 MINIMIZE(作为其 GRADIENT-SOURCE 参数)之前,必须先将其包装在一个 BP-LEARNER 对象中。BP-LEARNER 拥有 MONITORS 插槽,例如被 RESET-OPTIMIZATION-MONITORS 所使用。
不考虑其他复杂因素,基本的训练流程如下:
(minimize optimizer (make-instance 'bp-learner :bpn bpn)
:dataset dataset)
- [类] BP-LEARNER
[读取器] BPN BP-LEARNER (:BPN)
此
BP-LEARNER提供梯度的BPN。
[访问器] MONITORS BP-LEARNER (:MONITORS = NIL)
一个包含
MONITOR的列表。
11.3.2 监控
[函数] MONITOR-BPN-RESULTS DATASET BPN MONITORS
对于
DATASET中的每一批(大小为MAX-N-STRIPES的BPN)实例,将该批设置为下一个输入,使用SET-INPUT,执行一次FORWARD传递,并对BPN应用MONITORS(通过APPLY-MONITORS)。最后,返回MONITORS的计数器。这是基于MONITOR-MODEL-RESULTS构建的。
[函数] MAKE-STEP-MONITOR-MONITORS RNN &KEY (COUNTER-VALUES-FN #'COUNTER-RAW-VALUES) (MAKE-COUNTER #'MAKE-STEP-MONITOR-MONITOR-COUNTER)
返回一个监控器列表,每个监控器对应于
RNN中的每一个STEP-MONITORS。这些监控器使用COUNTER-VALUES-FN从其对应的“扭曲”计数器中提取结果,并将其添加到由MAKE-COUNTER创建的自身计数器中。哇。呃。其想法是,可以这样做来监控扭曲的预测:(let ((*warp-time* t)) (setf (step-monitors rnn) (make-cost-monitors rnn :attributes '(:event "warped pred."))) (monitor-bpn-results dataset rnn ;; 只需在每批实例后收集并重置扭曲监控器。 (make-step-monitor-monitors rnn)))
[通用函数] MAKE-STEP-MONITOR-MONITOR-COUNTER STEP-COUNTER
在
RNN中,STEP-COUNTER会汇总当前批次处理过程中所有时间步的结果。当批次处理完成后,返回一个新的计数器,用于累积来自STEP-COUNTER的结果。默认实现会创建STEP-COUNTER的一个副本。
11.3.3 前馈网络
FNN和RNN有很多共同之处(参见它们的共同超类BPN)。专门针对FNN的功能非常有限,所以在我们研究完整示例之前,先简单介绍一下它们吧。
[宏] BUILD-FNN (&KEY FNN (CLASS ''FNN) INITARGS MAX-N-STRIPES NAME) &BODY CLUMPS
用于从
CLUMP组装FNN的语法糖。类似于LET*,它是一系列绑定(将符号绑定到CLUMP)。默认情况下,所创建的clump名称与绑定的符号名称相同。如果某个clump没有绑定到符号(因为它是在嵌套表达式中创建的),则可以使用局部函数CLUMP在正在构建的fnn中找到具有给定名称的clump。例如:(build-fnn () (features (->input :size n-features)) (biases (->weight :size n-features)) (weights (->weight :size (* n-hiddens n-features))) (activations0 (->v*m :weights weights :x (clump 'features))) (activations (->+ :args (list biases activations0))) (output (->sigmoid :x activations)))
FNN教程
希望这个来自example/digit-fnn.lisp的例子能够说明相关概念。如果尽管有注释,仍然觉得过于密集,请先了解一下数据集、基于梯度的优化,然后再回来阅读。
(cl:defpackage :mgl-example-digit-fnn
(:use #:common-lisp #:mgl))
(in-package :mgl-example-digit-fnn)
;;; 输入中有10种可能的数字 ...
(defparameter *n-inputs* 10)
;;; 我们想学习将输入数字D映射到(MOD (1+ D) 3)的规则。
(defparameter *n-outputs* 3)
;;; 定义一个前馈网络,以便以后可以通过添加SET-INPUT方法来专门化输入的转换方式。
(defclass digit-fnn (fnn)
())
;;; 构建一个带有单层修正线性单元和softmax输出的DIGIT-FNN。
(defun make-digit-fnn (&key (n-hiddens 5))
(build-fnn (:class 'digit-fnn)
(input (->input :size *n-inputs*))
(hidden-activation (->activation input :size n-hiddens))
(hidden (->relu hidden-activation))
(output-activation (->activation hidden :size *n-outputs*))
(output (->softmax-xe-loss output-activation))))
;;; 此方法由MINIMIZE以及MONITOR-BPN-RESULTS在执行前向传递(即计算网络所表示的函数值)之前,以“实例”批次(本例中为输入数字)调用。它的任务是通过填充INPUT clump的NODES矩阵的行来编码输入。
;;;
;;; 每个输入被编码为一行全零,其中只有一个1位于由输入数字决定的索引位置。这被称为独热编码。TARGET也可以用同样的方式编码,但这里我们使用了->SOFTMAX-XE-LOSS的TARGET支持的稀疏选项。
(defmethod set-input (digits (fnn digit-fnn))
(let* ((input (nodes (find-clump 'input fnn)))
(output-lump (find-clump 'output fnn)))
(fill! 0 input)
(loop for i upfrom 0
for digit in digits
do (setf (mref input i digit) 1))
(setf (target output-lump)
(mapcar (lambda (digit)
(mod (1+ digit) *n-outputs*))
digits))))
;;; 使用随机梯度下降法最小化损失(这里为交叉熵)来训练网络。
(defun train-digit-fnn ()
(let ((optimizer
;; 首先创建用于 MINIMIZE 的优化器。
(make-instance 'segmented-gd-optimizer
:segmenter
;; 我们用相同的参数,实际上也是同一个优化器,
;; 来训练每一组权重。不过一般情况下并不一定如此。
(constantly
(make-instance 'sgd-optimizer
:learning-rate 1
:momentum 0.9
:batch-size 100))))
(fnn (make-digit-fnn)))
;; FNN 可以并行处理的实例数量。通常等于批次大小或其因数。
(setf (max-n-stripes fnn) 50)
;; 随机初始化所有权重。
(map-segments (lambda (weights)
(gaussian-random! (nodes weights) :stddev 0.01))
fnn)
;; 设置记录训练和测试误差的机制。
(monitor-optimization-periodically
optimizer '((:fn log-test-error :period 10000)
(:fn reset-optimization-monitors :period 1000)))
;; 最后,开始优化过程。
(minimize optimizer
;; 将 FNN 包装成一个 BP 学习器,并为其附加成本监控器。
;; 这些监控器会在每训练 100 个样本后由上述 RESET-OPTIMIZATION-MONITORS 重置并记录。
(make-instance 'bp-learner
:bpn fnn
:monitors (make-cost-monitors
fnn :attributes `(:event "train")))
;; 训练将在采样器用尽数据时停止(10000 个样本后)。
:dataset (make-sampler 10000))))
;;; 返回一个采样器对象,该对象会生成 MAX-N-SAMPLES 个随机输入(0 到 9 之间的数字)。
(defun make-sampler (max-n-samples)
(make-instance 'function-sampler :max-n-samples max-n-samples
:generator (lambda () (random *n-inputs*))))
;;; 记录测试误差。同时,在训练开始时描述优化器和 BPN。在训练期间定期调用(见上文)。
(defun log-test-error (optimizer learner)
(when (zerop (n-instances optimizer))
(describe optimizer)
(describe (bpn learner)))
(log-padded
(monitor-bpn-results (make-sampler 1000) (bpn learner)
(make-cost-monitors
(bpn learner) :attributes `(:event "pred.")))))
#|
;;; 以下是运行日志:
(repeatably ()
(let ((*log-time* nil))
(train-digit-fnn)))
.. 训练开始时,n-instances: 0
.. 训练成本:0.000e+0 (0)
.. #<SEGMENTED-GD-OPTIMIZER {100E112E93}>
.. SEGMENTED-GD-OPTIMIZER 描述:
.. N-INSTANCES = 0
.. OPTIMIZERS = (#<SGD-OPTIMIZER
.. #<SEGMENT-SET
.. (#<->WEIGHT # :SIZE 15 1/1 :NORM 0.04473>
.. #<->WEIGHT # :SIZE 3 1/1 :NORM 0.01850>
.. #<->WEIGHT # :SIZE 50 1/1 :NORM 0.07159>
.. #<->WEIGHT # :SIZE 5 1/1 :NORM 0.03056>)
.. {100E335B73}>
.. {100E06DF83}>)
.. SEGMENTS = (#<->WEIGHT (HIDDEN OUTPUT-ACTIVATION) :SIZE
.. 15 1/1 :NORM 0.04473>
.. #<->WEIGHT (:BIAS OUTPUT-ACTIVATION) :SIZE
.. 3 1/1 :NORM 0.01850>
.. #<->WEIGHT (INPUT HIDDEN-ACTIVATION) :SIZE
.. 50 1/1 :NORM 0.07159>
.. #<->WEIGHT (:BIAS HIDDEN-ACTIVATION) :SIZE
.. 5 1/1 :NORM 0.03056>)
..
.. #<SGD-OPTIMIZER {100E06DF83}>
.. GD-OPTIMIZER 描述:
.. N-INSTANCES = 0
.. SEGMENT-SET = #<SEGMENT-SET
.. (#<->WEIGHT (HIDDEN OUTPUT-ACTIVATION) :SIZE
.. 15 1/1 :NORM 0.04473>
.. #<->WEIGHT (:BIAS OUTPUT-ACTIVATION) :SIZE
.. 3 1/1 :NORM 0.01850>
.. #<->WEIGHT (INPUT HIDDEN-ACTIVATION) :SIZE
.. 50 1/1 :NORM 0.07159>
.. #<->WEIGHT (:BIAS HIDDEN-ACTIVATION) :SIZE
.. 5 1/1 :NORM 0.03056>)
.. {100E335B73}>
.. LEARNING-RATE = 1.00000e+0
.. MOMENTUM = 9.00000e-1
.. MOMENTUM-TYPE = :NORMAL
.. WEIGHT-DECAY = 0.00000e+0
.. WEIGHT-PENALTY = 0.00000e+0
.. N-AFTER-UPATE-HOOK = 0
.. BATCH-SIZE = 100
..
.. BATCH-GD-OPTIMIZER 描述:
.. N-BEFORE-UPATE-HOOK = 0
.. #<DIGIT-FNN {100E11A423}>
.. BPN 描述:
.. CLUMPS = #(#<->INPUT INPUT :SIZE 10 1/50 :NORM 0.00000>
.. #<->ACTIVATION
.. (HIDDEN-ACTIVATION :ACTIVATION) :STRIPES 1/50
.. :CLUMPS 4>
.. #<->RELU HIDDEN :SIZE 5 1/50 :NORM 0.00000>
.. #<->ACTIVATION
.. (OUTPUT-ACTIVATION :ACTIVATION) :STRIPES 1/50
.. :CLUMPS 4>
.. #<->SOFTMAX-XE-LOSS OUTPUT :SIZE 3 1/50 :NORM 0.00000>)
.. N-STRIPES = 1
.. MAX-N-STRIPES = 50
.. 预测成本:1.100d+0 (1000.00)
.. 训练开始时,n-instances: 1000
.. 训练成本:1.093d+0 (1000.00)
.. 训练开始时,n-instances: 2000
.. 训练成本:5.886d-1 (1000.00)
.. 训练开始时,n-instances: 3000
.. 训练成本:3.574d-3 (1000.00)
.. 训练开始时,n-instances: 4000
.. 训练成本:1.601d-7 (1000.00)
.. 训练开始时,n-instances: 5000
.. 训练成本:1.973d-9 (1000.00)
.. 训练开始时,n-instances: 6000
.. 训练成本:4.882d-10 (1000.00)
.. 训练开始时,n-instances: 7000
.. 训练成本:2.771d-10 (1000.00)
.. 训练开始时,n-instances: 8000
.. 训练成本:2.283d-10 (1000.00)
.. 训练开始时,n-instances: 9000
.. 训练成本:2.123d-10 (1000.00)
.. 训练开始时,n-instances: 10000
.. 训练成本:2.263d-10 (1000.00)
.. 预测成本:2.210d-10 (1000.00)
..
==> (#<->WEIGHT (:BIAS HIDDEN-ACTIVATION) :SIZE 5 1/1 :NORM 2.94294>
--> #<->WEIGHT (INPUT HIDDEN-ACTIVATION) :SIZE 50 1/1 :NORM 11.48995>
--> #<->WEIGHT (:BIAS OUTPUT-ACTIVATION) :SIZE 3 1/1 :NORM 3.39103>
--> #<->WEIGHT (HIDDEN OUTPUT-ACTIVATION) :SIZE 15 1/1 :NORM 11.39339>)
|#
11.3.4 循环神经网络
RNN 教程
希望这个来自 example/sum-sign-fnn.lisp 的示例能够说明相关概念。
在阅读本节之前,请确保你已经熟悉了 FNN 教程。
(cl:defpackage :mgl-example-sum-sign-rnn
(:use #:common-lisp #:mgl))
(in-package :mgl-example-sum-sign-rnn)
;;; 在每个时间步只有一个输入...
(defparameter *n-inputs* 1)
;;; 我们希望学习一种规则,该规则输出序列中到目前为止所有输入之和的符号。
(defparameter *n-outputs* 3)
;;; 生成一个训练示例,该示例是一个随机长度的序列,
;;; 长度介于 1 和 LENGTH 之间。序列中的元素是包含两个元素的列表:
;;;
;;; 1. 网络的输入(一个随机数)。
;;;
;;; 2. 到目前为止所有输入之和的符号,编码为 0、1、2(分别表示负值、零和正值)。为了增加一些变化,每当遇到一个负输入时,总和就会被重置。
(defun make-sum-sign-instance (&key (length 10))
(let ((length (max 1 (random length)))
(sum 0))
(loop for i below length
collect (let ((x (1- (* 2 (random 2)))))
(incf sum x)
(when (< x 0)
(setq sum x))
(list x (cond ((minusp sum) 0)
((zerop sum) 1)
(t 2)))))))
;;; 构建一个具有单个 LSTM 隐藏层和 softmax 输出的 RNN。
;;; 对于每个时间步,都会实例化一个 SUM-SIGN-FNN。
(defun make-sum-sign-rnn (&key (n-hiddens 1))
(build-rnn ()
(build-fnn (:class 'sum-sign-fnn)
(input (->input :size 1))
(h (->lstm input :name 'h :size n-hiddens))
(prediction (->softmax-xe-loss (->activation h :name 'prediction
:size *n-outputs*))))))
;;; 我们定义这个类是为了能够在稍后通过添加 SET-INPUT 方法来专门化输入的转换方式。
(defclass sum-sign-fnn (fnn)
())
;;; 我们有一批来自 MAKE-SUM-SIGN-INSTANCE 的 RNN 实例。此函数使用属于同一时间步(即在同一索引处)的这些实例的元素调用,并设置输入和目标。
(defmethod set-input (instances (fnn sum-sign-fnn))
(let ((input-nodes (nodes (find-clump 'input fnn))))
(setf (target (find-clump 'prediction fnn))
(loop for stripe upfrom 0
for instance in instances
collect
;; 批次中的序列长度不一致。如果某个序列已经结束,RNN 会向我们发送 NIL。
(when instance
(destructuring-bind (input target) instance
(setf (mref input-nodes stripe 0) input)
target))))))
;;; 使用 Adam 优化器最小化损失(这里是交叉熵)来训练网络。
(defun train-sum-sign-rnn ()
(let ((rnn (make-sum-sign-rnn)))
(setf (max-n-stripes rnn) 50)
;; 按照通常的 sqrt(1 / fan-in) 方式初始化权重。
(map-segments (lambda (weights)
(let* ((fan-in (mat-dimension (nodes weights) 0))
(limit (sqrt (/ 6 fan-in))))
(uniform-random! (nodes weights)
:limit (* 2 limit))
(.+! (- limit) (nodes weights))))
rnn)
(minimize (monitor-optimization-periodically
(make-instance 'adam-optimizer
:learning-rate 0.2
:mean-decay 0.9
:mean-decay-decay 0.9
:variance-decay 0.9
:batch-size 100)
'((:fn log-test-error :period 30000)
(:fn reset-optimization-monitors :period 3000)))
(make-instance 'bp-learner
:bpn rnn
:monitors (make-cost-monitors rnn))
:dataset (make-sampler 30000))))
;;; 返回一个采样器对象,该对象可以产生 MAX-N-SAMPLES 个随机输入。
(defun make-sampler (max-n-samples &key (length 10))
(make-instance 'function-sampler :max-n-samples max-n-samples
:generator (lambda ()
(make-sum-sign-instance :length length))))
;;; 记录测试误差。此外,在训练开始时描述优化器和 bpn。在训练期间定期调用(见上文)。
(defun log-test-error (optimizer learner)
(when (zerop (n-instances optimizer))
(describe optimizer)
(describe (bpn learner)))
(let ((rnn (bpn learner)))
(log-padded
(append
(monitor-bpn-results (make-sampler 1000) rnn
(make-cost-monitors
rnn :attributes '(:event "pred.")))
;; 以另一种方式得到相同的结果:监测长度不超过 20 的序列的预测结果,但不要不必要地展开 RNN,以节省内存。
(let ((*warp-time* t))
(monitor-bpn-results (make-sampler 1000 :length 20) rnn
// 只需收集并重置每次批次实例后的扭曲监控数据。
(make-cost-monitors
rnn :attributes '(:event "warped pred."))))))
// 验证没有进一步的展开发生。
(assert (<= (length (clumps rnn)) 10))
(log-mat-room))
#|
;;; 以下为记录:
(let (;; 反向传播网络不需要双精度浮点数。使用单精度浮点数速度更快且所需内存更少。
(*default-mat-ctype* :float)
;; 启用数据在 GPU 内存中的移动,使 RNN 能够处理长度超出展开后网络所能容纳的序列。
(*cuda-window-start-time* 1)
(*log-time* nil))
;; 初始化随机数生成器。
(repeatably ()
;; 如果可用,则启用 CUDA。
(with-cuda* ()
(train-sum-sign-rnn))))
.. 在 n-instances: 0 处进行训练
.. 开销:0.000e+0 (0)
.. #<ADAM-OPTIMIZER {1006CD5663}>
.. GD-OPTIMIZER 描述:
.. N-INSTANCES = 0
.. SEGMENT-SET = #<SEGMENT-SET
.. (#<->WEIGHT (H #) :SIZE 1 1/1 :NORM 1.73685>
.. #<->WEIGHT (H #) :SIZE 1 1/1 :NORM 0.31893>
.. #<->WEIGHT (#1=# #2=# :PEEPHOLE) :SIZE
.. 1 1/1 :NORM 1.81610>
.. #<->WEIGHT (H #2#) :SIZE 1 1/1 :NORM 0.21965>
.. #<->WEIGHT (#1# #3=# :PEEPHOLE) :SIZE
.. 1 1/1 :NORM 1.74939>
.. #<->WEIGHT (H #3#) :SIZE 1 1/1 :NORM 0.40377>
.. #<->WEIGHT (H PREDICTION) :SIZE
.. 3 1/1 :NORM 2.15898>
.. #<->WEIGHT (:BIAS PREDICTION) :SIZE
.. 3 1/1 :NORM 2.94470>
.. #<->WEIGHT (#1# #4=# :PEEPHOLE) :SIZE
.. 1 1/1 :NORM 0.97601>
.. #<->WEIGHT (INPUT #4#) :SIZE 1 1/1 :NORM 0.65261>
.. #<->WEIGHT (:BIAS #4#) :SIZE 1 1/1 :NORM 0.37653>
.. #<->WEIGHT (INPUT #1#) :SIZE 1 1/1 :NORM 0.92334>
.. #<->WEIGHT (:BIAS #1#) :SIZE 1 1/1 :NORM 0.01609>
.. #<->WEIGHT (INPUT #5=#) :SIZE 1 1/1 :NORM 1.09995>
.. #<->WEIGHT (:BIAS #5#) :SIZE 1 1/1 :NORM 1.41244>
.. #<->WEIGHT (INPUT #6=#) :SIZE 1 1/1 :NORM 0.40475>
.. #<->WEIGHT (:BIAS #6#) :SIZE 1 1/1 :NORM 1.75358>)
.. {1006CD8753}>
.. 学习率 = 2.00000e-1
.. 动量 = 无
.. 动量类型 = :NONE
.. 权重衰减 = 0.00000e+0
.. 权重惩罚 = 0.00000e+0
.. 更新后钩子数量 = 0
.. 批量大小 = 100
..
.. BATCH-GD-OPTIMIZER 描述:
.. 更新前钩子数量 = 0
..
.. ADAM-OPTIMIZER 描述:
.. 均值衰减率 = 1.00000e-1
.. 均值衰减率衰减 = 9.00000e-1
.. 方差衰减率 = 1.00000e-1
.. 方差调整 = 1.00000d-7
.. #<RNN {10047C77E3}>
.. BPN 描述:
.. 团块 = #(#<SUM-SIGN-FNN :条纹 1/50 :团块 4>
.. #<SUM-SIGN-FNN :条纹 1/50 :团块 4>)
.. 条纹数 = 1
.. 最大条纹数 = 50
..
.. RNN 描述:
.. 最大滞后 = 1
.. 预测成本:1.223e+0 (4455.00)
.. 矫正后的预测成本:1.228e+0 (9476.00)
.. 外部内存使用情况:
.. 外部数组:162 个(已使用字节数:39,600)
.. CUDA 内存使用情况:
.. 设备数组:114 个(已使用字节数:220,892,池化字节数:19,200)
.. 主机数组:162 个(已使用字节数:39,600)
.. 主机到设备复制:6,164 次,设备到主机复制:4,490 次
.. 在 n-instances: 3000 处进行训练
.. 成本:3.323e-1 (13726.00)
.. 在 n-instances: 6000 处进行训练
.. 成本:3.735e-2 (13890.00)
.. 在 n-instances: 9000 处进行训练
.. 成本:1.012e-2 (13872.00)
.. 在 n-instances: 12000 处进行训练
.. 成本:3.026e-3 (13953.00)
.. 在 n-instances: 15000 处进行训练
.. 成本:9.267e-4 (13948.00)
.. 在 n-instances: 18000 处进行训练
.. 成本:2.865e-4 (13849.00)
.. 在 n-instances: 21000 处进行训练
.. 成本:8.893e-5 (13758.00)
.. 在 n-instances: 24000 处进行训练
.. 成本:2.770e-5 (13908.00)
.. 在 n-instances: 27000 处进行训练
.. 成本:8.514e-6 (13570.00)
.. 在 n-instances: 30000 处进行训练
.. 成本:2.705e-6 (13721.00)
.. 预测成本:1.426e-6 (4593.00)
.. 矫正后的预测成本:1.406e-6 (9717.00)
.. 外部内存使用情况:
.. 外部数组:216 个(已使用字节数:52,800)
.. CUDA 内存使用情况:
.. 设备数组:148 个(已使用字节数:224,428,池化字节数:19,200)
.. 主机数组:216 个(已使用字节数:52,800)
.. 主机到设备复制:465,818 次,设备到主机复制:371,990 次
..
==> (#<->WEIGHT (H (H :OUTPUT)) :SIZE 1 1/1 :NORM 0.10624>
--> #<->WEIGHT (H (H :CELL)) :SIZE 1 1/1 :NORM 0.94460>
--> #<->WEIGHT ((H :CELL) (H :FORGET) :PEEPHOLE) :SIZE 1 1/1 :NORM 0.61312>
--> #<->WEIGHT (H (H :FORGET)) :SIZE 1 1/1 :NORM 0.38093>
--> #<->WEIGHT ((H :CELL) (H :INPUT) :PEEPHOLE) :SIZE 1 1/1 :NORM 1.17956>
--> #<->WEIGHT (H (H :INPUT)) :SIZE 1 1/1 :NORM 0.88011>
--> #<->WEIGHT (H PREDICTION) :SIZE 3 1/1 :NORM 49.93808>
--> #<->WEIGHT (:BIAS PREDICTION) :SIZE 3 1/1 :NORM 10.98112>
--> #<->WEIGHT ((H :CELL) (H :OUTPUT) :PEEPHOLE) :SIZE 1 1/1 :NORM 0.67996>
--> #<->WEIGHT (INPUT (H :OUTPUT)) :SIZE 1 1/1 :NORM 0.65251>
--> #<->WEIGHT (:BIAS (H :OUTPUT)) :SIZE 1 1/1 :NORM 10.23003>
--> #<->WEIGHT (INPUT (H :CELL)) :SIZE 1 1/1 :NORM 5.98116>
--> #<->WEIGHT (:BIAS (H :CELL)) :SIZE 1 1/1 :NORM 0.10681>
--> #<->WEIGHT (INPUT (H :FORGET)) :SIZE 1 1/1 :NORM 4.46301>
--> #<->WEIGHT (:BIAS (H :FORGET)) :SIZE 1 1/1 :NORM 1.57195>
--> #<->WEIGHT (INPUT (H :INPUT)) :SIZE 1 1/1 :NORM 0.36401>
--> #<->WEIGHT (:BIAS (H :INPUT)) :SIZE 1 1/1 :NORM 8.63833>)
|#
[类] RNN BPN
循环神经网络(与前馈神经网络相对)。它通常由
BUILD-RNN构建,后者不过是一个简单的便捷宏。RNN的输入是可变长度的序列。在每个时间步,这些序列中尚未处理的下一个元素会被设置为输入,直到批次中的所有输入序列都被处理完。为了能够进行反向传播,必须保留所有的中间LUMP,因此通过时间反向传播将递归连接展开。具体需要多少个团块取决于序列的长度。当创建一个
RNN时,会实例化MAX-LAG + 1个BPN,以确保所有权重都存在,并可以开始训练。
[读取器] UNFOLDER RNN (:UNFOLDER)
RNN的UNFOLDER是一个无参数函数,用于构建并返回一个BPN。该解折叠器允许创建具有任意拓扑结构的网络,甚至可以根据不同的TIME-STEP使用不同的拓扑结构,或嵌套RNNs。同名的权重会在各个折叠之间共享。也就是说,如果要创建一个->WEIGHT团块,而已经存在同名的权重团块,则现有的团块会被添加到由UNFOLDER创建的BPN中。
[读者] MAX-LAG RNN (:MAX-LAG = 1)
由
UNFOLDER构建的网络可能在时间步MAX-LAG之前包含新的权重。超过这个时间步之后,所有的权重块都必须是之前时间步中同名权重块的重复出现。大多数循环网络只引用前一个时间步的块状态(通过函数LAG),因此默认值为1。不过,也可以建立到任意时间步的连接。创建RNN时必须指定最大连接滞后。
[访问器] CUDA-WINDOW-START-TIME RNN (:CUDA-WINDOW-START-TIME = *CUDA-WINDOW-START-TIME*)
由于展开操作,
RNN的内存占用几乎与时间步数(即最大序列长度)成正比。对于预测任务,这可以通过时间扭曲来解决。而对于训练,我们不能丢弃先前时间步的结果,因为它们对反向传播至关重要,但我们至少可以在这些结果暂时不再使用时将其移出GPU内存,并在需要时再复制回来。显然,这一功能仅在使用CUDA时才有意义。如果
CUDA-WINDOW-START-TIME为NIL,则此功能关闭。否则,在训练过程中,当达到或超过CUDA-WINDOW-START-TIME的时间步时,属于非权重块的矩阵可能会被强制移出GPU内存,并在需要时再重新加载。该功能是通过
MGL-MAT:WITH-SYNCING-CUDA-FACETS实现的,它利用CUDA主机内存(也称为页锁定或固定内存)来进行异步复制,同时进行正常的计算。其结果是,现在不受限制的是主内存的使用量,再加上页锁定机制,这使得系统很容易被耗尽而停止运行。请务必注意这一点。
[变量] *CUDA-WINDOW-START-TIME* NIL
是
CUDA-WINDOW-START-TIME的默认值。
[宏] BUILD-RNN (&KEY RNN (CLASS ''RNN) NAME INITARGS MAX-N-STRIPES (MAX-LAG 1)) &BODY BODY
创建一个具有
MAX-N-STRIPES和MAX-LAG的RNN,其UNFOLDER为BODY包裹在一个lambda函数中。将作为RNN参数给出的符号绑定到RNN对象上,以便BODY可以访问它。
[函数] LAG NAME &KEY (LAG 1) RNN PATH
在
RNN中,或者如果它是NIL,则是在另一个BPN扩展的RNN中(称为展开),查找距离当前添加的BPN向前LAG个时间步的BPN中的CLUMPa4fe,其名称为NAME。如果此函数是从RNN的UNFOLDER调用的(这是在BUILD-RNN主体中幕后发生的情况),则返回一个表示延迟连接到某个块的不透明对象;否则,直接返回该CLUMP本身。FIXDOC:
PATH
[函数] TIME-STEP &KEY (RNN *RNN*)
返回
RNN当前正在执行或被展开的时间步。当RNN首次被展开时,该值为0。
[方法] SET-INPUT INSTANCES (RNN RNN)
RNNs像FNN一样,也是以实例批次为单位进行操作。但这里的实例更类似于数据集:序列或采样器,它们会通过MAP-DATASETS:IMPUTENIL转换为实例批次序列。索引为2的实例批次会通过SET-INPUT被固定到RNN的第2个时间步的BPN上。当批次中的输入序列长度不一致时,已经耗尽的序列会在上述过程中产生
NIL(由于:IMPUTE设置为NIL)。当这样的NIL通过SET-INPUT被固定到RNN的BPN上时,SET-INPUT必须将->ERROR块的IMPORTANCE038e设置为0,否则训练过程就会受到之前调用遗留下来的噪声干扰。
时间扭曲
每个时间步分配一个BPN的RNN会导致内存使用量无限制增长,这可能成为一个问题。对于训练而言,由于梯度通常需要从最后一个时间步反向传播到最开始的时间步,这个问题很难解决,但借助CUDA-WINDOW-START-TIME,限制已不再局限于GPU内存。
另一方面,对于预测任务来说,没有必要无限期地保留旧的时间步:当未来的时间步不会再引用它们时,就可以将其丢弃。
[变量] *WARP-TIME* NIL
控制是否启用时间扭曲(参见时间扭曲)。不要在训练时启用它,因为这会使反向传播无法进行。
- [函数] WARPED-TIME &KEY (RNN *RNN*) (TIME (TIME-STEP :RNN RNN)) (LAG 0)
返回 BPN 在 RNN 的 CLUMPS 中的索引,该 BPN 的任务是在 (- (TIME-STEP RNN) LAG) 时刻执行计算。通常情况下,这与 TIME-STEP 相同(忽略 LAG)。也就是说,可以通过 TIME-STEP 来索引 CLUMPS 以获取对应的 BPN。然而,当 *WARP-TIME* 为真时,执行会按照网络结构允许的方式循环进行。
假设我们有一个典型的 RNN,它只引用前一个时间步,因此其 MAX-LAG 为 1。它的 UNFOLDER 会返回结构相同但时间延迟连接有所偏移的 BPN,除了第一个之外。因此,WARP-START 和 WARP-LENGTH 都为 1。如果 *WARP-TIME* 为 NIL,那么从 TIME-STEP 到 CLUMPS 中 BPN 的映射就非常直接:
时间: | 0 | 1 | 2 | 3 | 4 | 5
--------+----+----+----+----+----+----
变形后: | 0 | 1 | 2 | 1 | 2 | 1
--------+----+----+----+----+----+----
BPN: | b0 | b1 | b2 | b1*| b2 | b1*
其中,`B1*` 与 `B1` 是同一个 `BPN`,但其由 `LAG` 创建的连接经过变形时间后,最终指向 `B2`。这样,内存消耗就与处理序列或进行预测所需的时间步数无关了。
为了实现这一技巧,在实例化 RNN 时必须指定 WARP-START 和 WARP-LENGTH。一般来说,当 *WARP-TIME* 为真时,需要 ( + WARP-START (MAX 2 WARP-LENGTH)) 个 BPN。之所以要加上 2,是因为当循环长度为 1 时,某个 BPN 就需要从自身获取输入,而这是有问题的,因为它仅为其一组值分配了 NODES。
[reader] WARP-START RNN (:WARP-START = 1)
表示从哪个
TIME-STEP开始,UNFOLDER将创建每隔WARP-LENGTH步骤就会重复的BPN。
[reader] WARP-LENGTH RNN (:WARP-LENGTH = 1)
一个整数,表示在时间步
I(其中(<= WARP-START I))时,UNFOLDER所创建的BPN与在时间步(+ WARP-START (MOD (- I WARP-START) WARP-LENGTH))时所创建的BPN完全相同,唯一的区别在于其时间延迟连接的位置有所不同。
[accessor] STEP-MONITORS RNN (:STEP-MONITORS = NIL)
在训练过程中,对应于先前时间步的展开后的
BPN可能因为不再位于 GPU 内存中而难以访问。这一考虑同样适用于预测阶段,而且还有一个额外的问题:当*WARP-TIME*为真时,先前的状态会被丢弃,因此在FORWARD结束后无法再收集统计信息。如果在此槽位中添加监控对象,它们将在训练或预测期间每次调用
FORWARD处理RNN时自动应用于该RNN。为了便于在不同的监控集合之间切换,除了可以使用监控列表外,还可以使用符号或函数。如果是符号,则表示其SYMBOL-VALUE;如果是函数,则必须无参数,并表示其返回值。
11.4 块状组件
11.4.1 块状组件基类
[class] LUMP CLUMP
LUMP是神经网络中一种简单的、类似层的组件。块状组件种类繁多,每种都执行特定的操作,或者只是存储输入和权重。按照惯例,块状组件的名称通常以->作为前缀。这些组件被定义为类,同时也有与类同名的函数,以便于创建它们。这些创建函数通常带有与类初始化参数相对应的关键词参数,其中一些(主要是输入类组件)则被转换为普通的位置参数。因此,与其写(make-instance '->tanh :x some-input :name 'my-tanh)不如直接写
(->tanh some-input :name 'my-tanh)在
BUILD-FNN或BUILD-RNN中以任何方式实例化的块状组件都会自动添加到正在构建的网络中。每个块状组件都有自己的
NODES和DERIVATIVES矩阵,用于存储前向和反向传播的结果。这与BPN不同,后者使用的NODES和DERIVATIVES是其最后一个组成成分CLUMP的。由于块状组件几乎总是存在于
BPN中,因此它们的N-STRIPES和MAX-N-STRIPES会在后台自动处理。
[reader] SIZE LUMP (:SIZE)
单条纹中的数值数量。
[reader] DEFAULT-VALUE LUMP (:DEFAULT-VALUE = 0)
在创建或调整大小时,块状组件的节点将被填充为此值。
[generic-function] DEFAULT-SIZE LUMP
如果在实例化时未提供
SIZE,则返回LUMP的默认值。该值通常根据输入的大小来计算。此函数用于实现新的块状组件类型。
[reader] NODES LUMP (= NIL)
前向传播中由块状组件计算出的值会存储在这里。对于非权重类块状组件,这是一个
N-STRIPES * SIZE的矩阵,预先分配了MAX-N-STRIPES * SIZE个元素的空间。而->WEIGHT类型的块状组件则没有条纹,也没有形状上的限制。
11.4.2 输入类组件
输入块
[类] ->INPUT ->DROPOUT
一个没有输入块、在前向传播中不改变其值(除非
DROPOUT不为零)且不计算导数的块。钳位 在SET-INPUT中对输入块的NODES进行输入限制。为了方便起见,
->INPUT本身可以执行dropout,尽管默认情况下不执行dropout。(->input :size 10 :name 'some-input) ==> #<->INPUT SOME-INPUT :SIZE 10 1/1 :NORM 0.00000>
嵌入块
这个块就像一个输入块和一个简单激活块为了效率而结合在一起。
[类] ->EMBEDDING LUMP
从
WEIGHTS中选择行,每条索引对应一行,在INPUT-ROW-INDICES中。这个块等同于添加一个带有独热编码方案的->INPUT块,以及在其上叠加一个->V*M块,但它在执行和内存使用上更加高效,因为它使用输入的稀疏表示。这个块的
SIZE是WEIGHTS的列数,会自动确定。(->embedding :weights (->weight :name 'embedding-weights :dimensions '(3 5)) :name 'embeddings) ==> #<->EMBEDDING EMBEDDINGS :SIZE 5 1/1 :NORM 0.00000>
[读取器] WEIGHTS ->EMBEDDING (:WEIGHTS)
一个权重块,其行由
INPUT-ROW-INDICES索引,并复制到该块的输出中。
[访问器] INPUT-ROW-INDICES ->EMBEDDING (:INPUT-ROW-INDICES)
一批次长度的行索引序列。将在
SET-INPUT中设置。
11.4.3 权重块
[类] ->WEIGHT LUMP
一组可优化的参数。当一个
BPN被训练时(参见训练),权重块的NODES将会被改变。权重块不进行任何计算。权重可以通过指定总大小或维度来创建:
(dimensions (->weight :size 10 :name 'w)) => (1 10) (dimensions (->weight :dimensions '(5 10) :name 'w)) => (5 10)
[读取器] DIMENSIONS ->WEIGHT (:DIMENSIONS)
该块的
NODES和DERIVATIVES将按照这些维度分配。
[宏] WITH-WEIGHTS-COPIED (FROM-BPN) &BODY BODY
在
BODY中,->WEIGHT会首先查找FROM-BPN中是否存在同名的权重块,如果存在则返回该块,否则正常创建一个新的权重块。如果FROM-BPN为NIL,则不会复制任何权重。
11.4.4 激活函数
激活子网络
我们已经有了输入。通常下一步是将输入向量与权重矩阵相乘并加上偏置。这可以直接用->+、->V*M和->WEIGHT完成,但使用激活子网络可以更方便地减少代码混乱。
[类] ->ACTIVATION BPN
激活子网络由函数
->ACTIVATION构建,内部隐藏着多个块。最终,这个子网络会计算一个类似于sum_i x_i * W_i + sum_j y_j .* V_j + biases的总和,其中x_i是输入块,W_i是代表连接的稠密矩阵,而V_j则是以元素方式与其对应的输入y_j相乘的窥孔连接向量。
[函数] ->ACTIVATION INPUTS &KEY (NAME (GENSYM)) SIZE PEEPHOLES (ADD-BIAS-P T)
创建一个属于
->ACTIVATION类的子网络,用于计算来自INPUTS中的稠密连接块的激活,以及来自PEEPHOLES中的元素级连接块的激活。必要时创建新的->WEIGHT块。INPUTS和PEEPHOLES可以是一个单独的块,也可以是一组块。最后,如果ADD-BIAS-P为真,则还会添加一个元素级偏置。SIZE必须显式指定,因为如果没有窥孔连接,就无法确定它。(->activation (->input :size 10 :name 'input) :name 'h1 :size 4) ==> #<->ACTIVATION (H1 :ACTIVATION) :STRIPES 1/1 :CLUMPS 4>这是神经网络的基本工作单元,负责线性变换,并将结果传递给非线性函数(如
->SIGMOID、->TANH等)。子网络块的名字是
(,NAME :ACTIVATION)。偏置权重块(如果有)名为(:BIAS ,NAME)。稠密连接权重块以输入名称和NAME命名:(,(NAME INPUT) ,NAME),而窥孔连接权重块则命名为(,(NAME INPUT) ,NAME :PEEPHOLE)。这一点很有用,例如在需要对它们进行不同初始化时。
批量归一化
- [类] ->BATCH-NORMALIZED LUMP
这是批量归一化论文第3版的实现。->BATCH-NORMALIZED 的输出是对其输入进行归一化后的结果,使得所有元素在条带维度上的均值为零、方差为1。也就是说,从输入中减去批次均值,并根据样本标准差对其进行缩放。实际上,在归一化步骤之后,数值会再次通过学习得到的参数进行缩放和偏移(但这次使用的是可学习的参数),以保持模型的表示能力不变。该模块的主要目的是加速训练,同时它也起到正则化的作用。详细信息请参阅论文。
要在不引入额外正则化效果的情况下对 LUMP 的输出进行归一化:
(->batch-normalized lump :batch-size :use-population)
上述代码使用指数移动平均来估计批次的均值和方差,并在训练和测试时都使用这些估计值。与此不同的是,发表版本在训练时使用当前批次的样本均值和方差,这会在过程中引入噪声。噪声的程度与批次大小成反比,批次越小,噪声越大,从而产生正则化效果。这是默认行为(等同于 :BATCH-SIZE NIL):
(->batch-normalized lump)
出于性能考虑,有时希望在一个批次中处理更多的实例(按照 N-STRIPES 的概念),同时获得较小批次大小带来的正则化效果。可以通过将 :BATCH-SIZE 设置为条带数量的因数来实现。例如,假设条带数量为128,但我们希望获得与批次大小为32时相同的正则化效果:
(->batch-normalized lump :batch-size 32)
->BATCH-NORMALIZED 的主要输入通常是 ->ACTIVATION(0 或 1),其输出会传递给激活函数(参见 Activation Functions)。
[reader] BATCH-NORMALIZATION ->BATCH-NORMALIZED (:NORMALIZATION)
此模块的
->BATCH-NORMALIZATION。可以被多个->BATCH-NORMALIZED模块共享。批量归一化的一个特殊之处在于,它除了计算结果(
NODES)及其导数(DERIVATIVES)之外,还具有状态。这些状态包括其输入的估计均值和方差,它们被封装在->BATCH-NORMALIZATION中。如果在实例化时未指定
NORMALIZATION,则会自动创建一个新的->BATCH-NORMALIZATION对象,并将:BATCH-SIZE、:VARIANCE-ADJUSTMENT和:POPULATION-DECAY参数传递给->BATCH-NORMALIZATION。请参阅BATCH-SIZE、VARIANCE-ADJUSTMENT和POPULATION-DECAY。新的尺度和偏移权重模块将以以下名称创建:`(,name :scale) `(,name :shift)其中
NAME是此模块的NAME。这种默认行为适用于
->BATCH-NORMALIZATION所保存的统计信息仅在RNN的各个时间步之间共享的情况。
[class] ->BATCH-NORMALIZATION ->WEIGHT
该类的主要用途是存储待归一化输入的估计均值和方差,并允许这些信息在多个执行计算的
->BATCH-NORMALIZED模块之间共享。这些估计值由SAVE-STATE和LOAD-STATE进行保存和加载。(->batch-normalization (->weight :name '(h1 :scale) :size 10) (->weight :name '(h1 :shift) :size 10) :name '(h1 :batch-normalization))
[reader] SCALE ->BATCH-NORMALIZATION (:SCALE)
一个与
SHIFT大小相同的权重模块。这就是论文中的 $\gamma$。
[reader] SHIFT ->BATCH-NORMALIZATION (:SHIFT)
一个与
SCALE大小相同的权重模块。这就是论文中的 $\beta$。
[reader] BATCH-SIZE ->BATCH-NORMALIZATION (:BATCH-SIZE = NIL)
通常情况下,所有条带都会参与批次计算。降低条带数量可能会增加正则化效果,但也会使计算效率降低。通过将
BATCH-SIZE设置为N-STRIPES的因数,可以将效率问题与正则化问题分开考虑。默认值NIL等同于N-STRIPES。BATCH-SIZE只影响训练过程。使用特殊值
:USE-POPULATION时,归一化将不再使用当前批次的均值和方差,而是使用总体统计信息。这将有效地消除正则化效果,只留下加速训练的作用。
[reader] VARIANCE-ADJUSTMENT ->BATCH-NORMALIZATION (:VARIANCE-ADJUSTMENT = 1.0e-4)
一个很小的正实数,会被加到样本方差上。这就是论文中的 $\epsilon$。
[reader] POPULATION-DECAY ->BATCH-NORMALIZATION (:POPULATION-DECAY = 0.99)
在训练过程中,会不断更新批次均值和标准差的指数移动平均值(称为“总体统计信息”)。在进行预测时,会使用这些统计信息来进行归一化。这些总体统计信息会通过
SAVE-STATE被持久化保存。
- [function] ->BATCH-NORMALIZED-ACTIVATION INPUTS &KEY (NAME (GENSYM)) SIZE PEEPHOLES BATCH-SIZE VARIANCE-ADJUSTMENT POPULATION-DECAY
一个工具函数,用于创建并包装一个 ->ACTIVATION(0 1),使其包含
->BATCH-NORMALIZED,并通过其BATCH-NORMALIZATION 对缩放和偏移参数的两个权重块进行归一化。
(->BATCH-NORMALIZED-ACTIVATION INPUTS :NAME 'H1 :SIZE 10) 等价于:
```commonlisp
(->batch-normalized (->activation inputs :name 'h1 :size 10 :add-bias-p nil)
:name '(h1 :batch-normalized-activation))
```
注意,偏置被关闭了,因为归一化无论如何都会将其抵消(但会添加一个偏移量,其效果与偏置相同)。
11.4.5 激活函数
现在我们进入最重要的非线性变换阶段,这些变换将应用于激活值。
Sigmoid 块
-
对其输入逐元素应用
1/(1 + e^{-x})函数。这是神经网络中经典的非线性激活函数之一。为了方便起见,
->SIGMOID可以自行执行丢弃操作,但默认情况下不进行丢弃。(->sigmoid (->activation (->input :size 10) :size 5) :name 'this) ==> #<->SIGMOID THIS :SIZE 5 1/1 :NORM 0.00000>该块的
SIZE是其输入的大小,由系统自动确定。
Tanh 块
缩放 Tanh 块
[类] ->SCALED-TANH LUMP
类似于
TANH,但其输入和输出经过缩放处理,使得当输入的方差接近 1 时,输出的方差也接近 1,这一特性有助于缓解梯度消失问题。实际函数为1.7159 * tanh(2/3 * x)。该块的SIZE是其输入的大小,由系统自动确定。
Relu 块
此时大约是 2007 年左右。
[类] ->RELU LUMP
max(0,x)激活函数。需要注意的是,Relu 单元可能会陷入关闭状态:如果它们的值变得过于负数,就很难再恢复到激活状态。该块的SIZE是其输入的大小,由系统自动确定。
Max 块
时间大约在 2011 年左右。
[类] ->MAX LUMP
这基本上就是没有丢弃操作的 maxout 层(参见 http://arxiv.org/abs/1302.4389)。它将输入按[`GROUP-SIZE`][59dd] 分组,并输出每个组的最大值。 输出的
SIZE会自动计算,等于输入大小除以GROUP-SIZE。(->max (->input :size 120) :group-size 3 :name 'my-max) ==> #<->MAX MY-MAX :SIZE 40 1/1 :NORM 0.00000 :GROUP-SIZE 3>与
->RELU相比,->MAX的优势在于梯度永远不会被阻断,因此不存在单元陷入关闭状态的问题。
[读取器] GROUP-SIZE ->MAX (:GROUP-SIZE)
每个组中的输入数量。
Min 块
[读取器] GROUP-SIZE ->MIN (:GROUP-SIZE)
每个组中的输入数量。
Max-Channel 块
[类] ->MAX-CHANNEL LUMP
在文献中被称为 LWTA(局部胜者全得)或 Channel-Out(参见 http://arxiv.org/abs/1312.1909)。它本质上是[`->MAX`][f652],但不同之处在于,它不是为每个组生成一个输出,而是只对组中数值最大的单元输出 1,其余单元输出 0。这样可以让下一层了解信息流动的路径。该块的
SIZE是其输入的大小,由系统自动确定。
[读取器] GROUP-SIZE ->MAX-CHANNEL (:GROUP-SIZE)
每个组中的输入数量。
11.4.6 损失函数
最终,我们需要告诉网络应该学习什么,这就意味着需要在网络中构建一个要最小化的损失函数。
损失块
[类] ->LOSS ->SUM
计算批次中各个样本的损失。该块的主要作用是提供训练信号。
误差块通常是块图中的叶节点(即没有其他块将其作为输入)。误差块的一个特殊之处在于,其导数会自动加上 1(但请参阅
IMPORTANCE)。误差块恰好有一个节点(每条带)的值是其输入块中所有节点值的总和。
[访问器] IMPORTANCE ->LOSS (:IMPORTANCE = NIL)
用于支持加权样本。也就是说,并非所有训练样本都同等重要。如果设置为非
NIL,将会提供一个 1 维的MAT,其中包含批次中各条带的重要性权重。当提供了IMPORTANCE(通常通过SET-INPUT设置)时,就不会简单地给所有条带的导数加上 1,而是按照IMPORTANCE的值逐元素相加。
平方差块
在回归任务中,平方误差损失是最常用的。平方误差损失可以通过将->SQUARED-DIFFERENCE与->LOSS结合来构建。
[类] ->SQUARED-DIFFERENCE LUMP
该块接受两个输入块,并以逐元素的方式计算它们的平方差
(x - y)^2。此块的SIZE会自动根据其输入的大小确定。该块通常会被送入->LOSS,后者会对平方差求和,使其成为待最小化的函数的一部分。(->loss (->squared-difference (->activation (->input :size 100) :size 10) (->input :name 'target :size 10)) :name 'squared-error) ==> #<->LOSS SQUARED-ERROR :SIZE 1 1/1 :NORM 0.00000>目前该块尚未进行CUDA加速,但如果需要,它会将数据从GPU复制过来。
Softmax交叉熵损失块
[类] ->SOFTMAX-XE-LOSS LUMP
这是一个专用块,在前向传播中计算其输入的Softmax值,并反向传播交叉熵损失。将这两步合并的好处在于数值稳定性。总交叉熵是按每组
GROUP-SIZE个元素计算的交叉熵之和:$$ XE(x) = - \sum_{i=1,g} t_i \ln(s_i), $$
其中
g是类别数(GROUP-SIZE),t_i是目标值(即该类别的真实概率,通常只有一个为1,其余为0),s_i是由输入X计算出的Softmax输出:$$ s_i = {softmax}(x_1, x_2, ..., x_g) = \frac{e^x_i}{\sum_{j=1,g} e^x_j} $$
换句话说,在前向阶段,该块接收输入
X,对其每个元素执行EXP操作,然后对每组GROUP-SIZE个元素进行归一化,使其总和为1,得到Softmax结果,该结果会传递到NODES中。在反向传播阶段,梯度来源有两个:使用该块输出作为输入的其他块(目前尚未实现,会导致错误)以及隐式的交叉熵损失。可以通过调用该块上的
COST来获取最近一次前向传播中计算出的交叉熵。这是分类任务中最常见的损失函数,几乎无处不在。有关该损失函数与
SET-INPUT如何协同工作的详细信息,请参阅FNN教程和RNN教程。
[读取器] GROUP-SIZE ->SOFTMAX-XE-LOSS (:GROUP-SIZE)
Softmax分组中的元素数量。对于分类任务来说,这即是类别数。通常情况下,
GROUP-SIZE等于SIZE(默认设置),但总体而言,唯一的约束条件是SIZE必须是GROUP-SIZE的整数倍。
[访问器] TARGET ->SOFTMAX-XE-LOSS (:TARGET = NIL)
在
SET-INPUT中设置,目标可以是与输入块X大小相同的MAT,或者如果目标非常稀疏,也可以是一个批次长度的序列,其中包含非零条目的索引值对:(;; 批次中的第一个实例有两个非零目标 (;; 类别10有30%的预期概率 (10 . 0.3) ;; 类别2有70%的预期概率 (2 . 0.7)) ;; 批次中的第二个实例将100%的概率分配给类别7 7 ;; 批次中还有更多实例… ...)实际上,在极少数情况下,如果
GROUP-SIZE不等于SIZE(即每个样本有多个Softmax归一化组),那么上述目标序列的长度将是BATCH-SIZE乘以组数。索引始终相对于该组的起始位置。如果
GROUP-SIZE很大(例如,在具有大量词汇的神经语言模型中),使用稀疏目标可以使计算速度大大加快,因为导数的计算不再是二次复杂度。隐式支持对不同训练实例赋予不同的权重。虽然一个组内的目标值之和应为1,但将所有目标值乘以权重
W等同于在同一实例上训练W次。
[函数] ENSURE-SOFTMAX-TARGET-MATRIX SOFTMAX-XE-LOSS N
将
SOFTMAX-XE-LOSS的TARGET设置为能够容纳N条带密集目标值的MAT。
11.4.7 随机性
Dropout块
[类] ->DROPOUT LUMP
该块的输出与其输入完全相同,只是在训练过程中会随机地将其中一部分置零,从而起到非常强的正则化作用。请参考Geoffrey Hinton的文章《通过防止特征检测器的协同适应来改进神经网络》。
该块的
SIZE与其输入的大小相同,且会自动确定。
[访问器] DROPOUT ->DROPOUT (:DROPOUT = 0.5)
如果不为
NIL,则在前向传播中以DROPOUT概率将该块中的每个节点置零。
高斯随机块
[类] ->GAUSSIAN-RANDOM LUMP
该块没有输入,它会生成服从正态分布的独立随机数,均值为
MEAN,方差为VARIANCE(或VARIANCE-FOR-PREDICTION)。这对于基于噪声的正则化方法来说是非常有用的构建模块。(->gaussian-random :size 10 :name 'normal :mean 1 :variance 2) ==> #<->GAUSSIAN-RANDOM NORMAL :SIZE 10 1/1 :NORM 0.00000>
[访问器] MEAN ->GAUSSIAN-RANDOM (:MEAN = 0)
正态分布的均值。
[访问器] 方差 ->高斯随机 (:方差 = 1)
正态分布的方差。
二值采样块
[类] ->SAMPLE-BINARY LUMP
将其输入的值视为概率,独立地进行二项式采样。将
true转换为 1,false转换为 0。该块的SIZE会根据其输入的大小自动确定。(->sample-binary (->input :size 10) :name 'binarized-input) ==> #<->SAMPLE-BINARY BINARIZED-INPUT :SIZE 10 1/1 :NORM 0.00000>
11.4.8 算术运算
求和块
向量-矩阵乘法块
[类] ->V*M LUMP
执行
X * WEIGHTS,其中X(输入)的大小为M,而WEIGHTS是一个->WEIGHT,其唯一的条带被视为尺寸为M x N的矩阵,按行优先顺序存储。N是该块的大小。如果TRANSPOSE-WEIGHTS-P为真,则WEIGHTS的尺寸为N x M,并计算X * WEIGHTS'。
元素级加法块
[类] ->+ LUMP
对其输入的块进行元素级加法。如果至少有一个输入,该块的
SIZE会自动从输入的大小中确定。如果其中一个输入是->WEIGHT块,则它会被加到每一个条带上。(->+ (list (->input :size 10) (->weight :size 10 :name 'bias)) :name 'plus) ==> #<->+ PLUS :SIZE 10 1/1 :NORM 0.00000>
元素级乘法块
[类] ->* LUMP
对其两个输入的块进行元素级乘法。该块的
SIZE会自动从输入的大小中确定。任一输入都可以是->WEIGHT块。(->* (->input :size 10) (->weight :size 10 :name 'scale) :name 'mult) ==> #<->* MULT :SIZE 10 1/1 :NORM 0.00000>
绝对值块
- [类] ->ABS LUMP
指数块
- [类] ->EXP LUMP
归一化块
- [类] ->NORMALIZED LUMP
正弦块
11.4.9 用于 RNN 的操作
LSTM 子网络
- [函数] ->LSTM INPUTS &KEY NAME CELL-INIT OUTPUT-INIT SIZE (ACTIVATION-FN '->ACTIVATION) (GATE-FN '->SIGMOID) (INPUT-FN '->TANH) (OUTPUT-FN '->TANH) (PEEPHOLES T)
创建一个由输入门、遗忘门和输出门组成的 LSTM 层,这些门会对输入、单元状态和输出进行缩放。会生成多个小块,其中代表 LSTM 输出的最后一个名为 NAME,其余的小块则根据 NAME 自动命名。该函数仅返回输出小块 (m),但所有生成的小块都会自动添加到正在构建的 BPN 中。
关于 LSTM 有许多论文和教程。本版本在《用于大规模声学建模的长短期记忆循环神经网络架构》(2014 年,Hasim Sak、Andrew Senior、Francoise Beaufays)中有详细描述。使用该论文中的符号:
$$ i_t = s(W_{ix} x_t + W_{im} m_{t-1} + W_{ic} \odot c_{t-1} + b_i) $$
$$ f_t = s(W_{fx} x_t + W_{fm} m_{t-1} + W_{fc} \odot c_{t-1} + b_f) $$
$$ c_t = f_t \odot c_{t-1} + i_t \odot g(W_{cx} x_t + W_{cm} m_{t-1} + b_c) $$
$$ o_t = s(W_{ox} x_t + W_{om} m_{t-1} + W_{oc} \odot c_t + b_o) $$
$$ m_t = o_t \odot h(c_t), $$
其中 i、f 和 o 分别是输入门、遗忘门和输出门。c 是单元状态,而 m 则是实际的输出。
从 c 连接的权重矩阵(W_ic、W_fc 和 W_oc)为对角矩阵,仅用对角线元素的向量表示。这些连接仅在 PEEPHOLES 为真时才会被添加。
与论文的一个显著区别在于,除了可以作为一个单独的小块外,x_t(INPUTS)也可以是一个小块列表。每当需要基于 x_t 计算激活值时,实际上都是各个激活值的总和。例如,W_ix * x_t 实际上是 sum_j W_ijx * inputs_j。
如果 CELL-INIT 不为 NIL,则它必须是一个大小为 SIZE 的 CLUMP,用来表示初始的细胞状态 (c_{-1})。若 CELL-INIT 为 NIL,则等同于所有状态均为零。
ACTIVATION-FN 默认为 ->ACTIVATION(0 1),但也可以是例如 ->BATCH-NORMALIZED-ACTIVATION。一般来说,像上述两种具有签名(INPUTS &KEY NAME SIZE PEEPHOLES)的函数都可以作为 ACTIVATION-FN 传递。
序列屏障小块
[类] ->SEQ-BARRIER LUMP
在
RNN中,处理批次中的不同条带(实例)可能需要不同的时间步数,因此条带 0 的最终状态可能位于某个小块 L 的第 7 个时间步,而条带 1 的最终状态则可能位于另一个小块 L 的第 42 个时间步。该小块将来自不同小块的每个条带状态复制到一个单一的小块中,以便后续处理能够继续进行(通常是在
RNN嵌入到其他网络中时)。此小块的
SIZE会自动设置为(FUNCALL SEQ-ELT-FN 0)返回的小块的大小。
[读取器] SEQ-ELT-FN ->SEQ-BARRIER (:SEQ-ELT-FN)
一个接受
INDEX参数的函数,用于返回序列中具有该索引的小块。
[访问者] SEQ-INDICES ->SEQ-BARRIER
一个长度等于批次大小的索引序列。索引
I处的元素是要传递给SEQ-ELT-FN的索引,以找到其条带I被复制到此小块条带I中的小块。
11.5 工具函数
[函数] RENORMALIZE-ACTIVATIONS ->V*M-LUMPS L2-UPPER-BOUND
如果某个单元的输入权重向量的 l2 范数大于
L2-UPPER-BOUND,则将其重新归一化至L2-UPPER-BOUND。假设->V*M-LUMPS列表最终会被馈送到同一个小块中。使用方法是将激活小块分组到同一个 GD-OPTIMIZER 中,并将此函数挂载到
AFTER-UPDATE-HOOK上,后者可以通过ARRANGE-FOR-RENORMALIZING-ACTIVATIONS自动完成。参见“通过防止特征检测器的协同适应来改进神经网络”(Hinton,2012),http://arxiv.org/pdf/1207.0580.pdf。
[函数] ARRANGE-FOR-RENORMALIZING-ACTIVATIONS BPN OPTIMIZER L2-UPPER-BOUND
通过将一个 lambda 函数推送到
OPTIMIZER的AFTER-UPDATE-HOOK,安排对OPTIMIZER训练的所有权重进行重新归一化(如RENORMALIZE-ACTIVATIONS所述,使用L2-UPPER-BOUND)。假设这些权重要么属于某个激活小块,要么只是简单地添加到激活中(即它们是偏置项)。
12 玻尔兹曼机
13 高斯过程
14 自然语言处理
[在包 MGL-NLP 中]
目前这只是一个包含几个实用工具的简单模块,未来可能会发展成一套更完善的自然语言处理工具集。
[函数] MAKE-N-GRAM-MAPPEE FUNCTION N
创建一个适用于映射函数作为参数的单参数函数。它会以每
N个元素为一组调用FUNCTION。(map nil (make-n-gram-mappee #'print 3) '(a b c d e)) .. .. (A B C) .. (B C D) .. (C D E)
[函数] BLEU CANDIDATES REFERENCES &KEY CANDIDATE-KEY REFERENCE-KEY (N 4)
计算双语语料库的 BLEU 分数。BLEU 用于衡量机器翻译相对于人工参考译文的质量。
CANDIDATES(由CANDIDATE-KEY索引)和REFERENCES(由REFERENCE-KEY索引)都是句子序列。句子是由单词组成的序列。单词通过EQUAL进行比较,可以是任何类型的对象(不一定是字符串)。目前尚不支持多条参考译文。
N决定了要考虑的最大 n-gram 长度。第一个返回值是
BLEU分数(介于 0 和 1 之间,不是百分比)。第二个值是CANDIDATES的总长度除以REFERENCES的总长度(如果分母为 0,则返回NIL)。第三个值是一个 n-gram 精确率列表(同样介于 0 和 1 之间或NIL),每个元素对应 [1..N] 中的一个值。这基本上是对 multi-bleu.perl 的重新实现。
(bleu '((1 2 3 4) (a b)) '((1 2 3 4) (1 2))) => 0.8408964 => 1 => (;; 1-gram precision: 4/6 2/3 ;; 2-gram precision: 3/4 3/4 ;; 3-gram precision: 2/2 1 ;; 4-gram precision: 1/1 1)
14.1 词袋模型
[类] BAG-OF-WORDS-ENCODER
使用稀疏向量对文档的所有特征进行编码。从
MAPPER获取文档的特征,并使用FEATURE-ENCODER对每个特征进行编码。如果该特征未被使用,FEATURE-ENCODER可能会返回NIL。结果是一个由编码后的特征与值组成的 cons 列表。编码后的特征在向量中是唯一的(根据ENCODED-FEATURE-TEST),但顺序并不固定。根据
KIND的不同,值的计算方式也有所不同:对于
:FREQUENCY,它是相应特征在DOCUMENT中出现的次数。对于
:BINARY,其值始终为 1。对于
:NORMALIZED-FREQUENCY和:NORMALIZED-BINARY,它们与非归一化的版本类似,只是在最终步骤中,组装好的稀疏向量中的值会被归一化,使其总和为 1。最后,
:COMPACTED-BINARY类似于:BINARY,但返回值不是 cons 列表,而是一个元素类型为ENCODED-FEATURE-TYPE的向量。
(let* ((feature-indexer (make-indexer (alexandria:alist-hash-table '(("I" . 3) ("me" . 2) ("mine" . 1))) 2)) (bag-of-words-encoder (make-instance 'bag-of-words-encoder :feature-encoder feature-indexer :feature-mapper (lambda (fn document) (map nil fn document)) :kind :frequency))) (encode bag-of-words-encoder '("All" "through" "day" "I" "me" "mine" "I" "me" "mine" "I" "me" "mine"))) => #((0 . 3.0d0) (1 . 3.0d0))
- [读取器] FEATURE-ENCODER BAG-OF-WORDS-ENCODER (:FEATURE-ENCODER)
- [读取器] FEATURE-MAPPER BAG-OF-WORDS-ENCODER (:FEATURE-MAPPER)
- [读取器] ENCODED-FEATURE-TEST BAG-OF-WORDS-ENCODER (:ENCODED-FEATURE-TEST = #'EQL)
- [读取器] ENCODED-FEATURE-TYPE BAG-OF-WORDS-ENCODER (:ENCODED-FEATURE-TYPE = T)
- [读取器] BAG-OF-WORDS-KIND BAG-OF-WORDS-ENCODER (:KIND = :BINARY)
15 日志记录
[在包 MGL-LOG 中]
- [函数] LOG-MSG FORMAT &REST ARGS
- [宏] WITH-LOGGING-ENTRY (STREAM) &BODY BODY
- [变量] *LOG-FILE* NIL
- [变量] *LOG-TIME* T
- [函数] LOG-MAT-ROOM &KEY (VERBOSE T)
[由 MGL-PAX 生成]
常见问题
相似工具推荐
stable-diffusion-webui
stable-diffusion-webui 是一个基于 Gradio 构建的网页版操作界面,旨在让用户能够轻松地在本地运行和使用强大的 Stable Diffusion 图像生成模型。它解决了原始模型依赖命令行、操作门槛高且功能分散的痛点,将复杂的 AI 绘图流程整合进一个直观易用的图形化平台。 无论是希望快速上手的普通创作者、需要精细控制画面细节的设计师,还是想要深入探索模型潜力的开发者与研究人员,都能从中获益。其核心亮点在于极高的功能丰富度:不仅支持文生图、图生图、局部重绘(Inpainting)和外绘(Outpainting)等基础模式,还独创了注意力机制调整、提示词矩阵、负向提示词以及“高清修复”等高级功能。此外,它内置了 GFPGAN 和 CodeFormer 等人脸修复工具,支持多种神经网络放大算法,并允许用户通过插件系统无限扩展能力。即使是显存有限的设备,stable-diffusion-webui 也提供了相应的优化选项,让高质量的 AI 艺术创作变得触手可及。
everything-claude-code
everything-claude-code 是一套专为 AI 编程助手(如 Claude Code、Codex、Cursor 等)打造的高性能优化系统。它不仅仅是一组配置文件,而是一个经过长期实战打磨的完整框架,旨在解决 AI 代理在实际开发中面临的效率低下、记忆丢失、安全隐患及缺乏持续学习能力等核心痛点。 通过引入技能模块化、直觉增强、记忆持久化机制以及内置的安全扫描功能,everything-claude-code 能显著提升 AI 在复杂任务中的表现,帮助开发者构建更稳定、更智能的生产级 AI 代理。其独特的“研究优先”开发理念和针对 Token 消耗的优化策略,使得模型响应更快、成本更低,同时有效防御潜在的攻击向量。 这套工具特别适合软件开发者、AI 研究人员以及希望深度定制 AI 工作流的技术团队使用。无论您是在构建大型代码库,还是需要 AI 协助进行安全审计与自动化测试,everything-claude-code 都能提供强大的底层支持。作为一个曾荣获 Anthropic 黑客大奖的开源项目,它融合了多语言支持与丰富的实战钩子(hooks),让 AI 真正成长为懂上
ComfyUI
ComfyUI 是一款功能强大且高度模块化的视觉 AI 引擎,专为设计和执行复杂的 Stable Diffusion 图像生成流程而打造。它摒弃了传统的代码编写模式,采用直观的节点式流程图界面,让用户通过连接不同的功能模块即可构建个性化的生成管线。 这一设计巧妙解决了高级 AI 绘图工作流配置复杂、灵活性不足的痛点。用户无需具备编程背景,也能自由组合模型、调整参数并实时预览效果,轻松实现从基础文生图到多步骤高清修复等各类复杂任务。ComfyUI 拥有极佳的兼容性,不仅支持 Windows、macOS 和 Linux 全平台,还广泛适配 NVIDIA、AMD、Intel 及苹果 Silicon 等多种硬件架构,并率先支持 SDXL、Flux、SD3 等前沿模型。 无论是希望深入探索算法潜力的研究人员和开发者,还是追求极致创作自由度的设计师与资深 AI 绘画爱好者,ComfyUI 都能提供强大的支持。其独特的模块化架构允许社区不断扩展新功能,使其成为当前最灵活、生态最丰富的开源扩散模型工具之一,帮助用户将创意高效转化为现实。
NextChat
NextChat 是一款轻量且极速的 AI 助手,旨在为用户提供流畅、跨平台的大模型交互体验。它完美解决了用户在多设备间切换时难以保持对话连续性,以及面对众多 AI 模型不知如何统一管理的痛点。无论是日常办公、学习辅助还是创意激发,NextChat 都能让用户随时随地通过网页、iOS、Android、Windows、MacOS 或 Linux 端无缝接入智能服务。 这款工具非常适合普通用户、学生、职场人士以及需要私有化部署的企业团队使用。对于开发者而言,它也提供了便捷的自托管方案,支持一键部署到 Vercel 或 Zeabur 等平台。 NextChat 的核心亮点在于其广泛的模型兼容性,原生支持 Claude、DeepSeek、GPT-4 及 Gemini Pro 等主流大模型,让用户在一个界面即可自由切换不同 AI 能力。此外,它还率先支持 MCP(Model Context Protocol)协议,增强了上下文处理能力。针对企业用户,NextChat 提供专业版解决方案,具备品牌定制、细粒度权限控制、内部知识库整合及安全审计等功能,满足公司对数据隐私和个性化管理的高标准要求。
ML-For-Beginners
ML-For-Beginners 是由微软推出的一套系统化机器学习入门课程,旨在帮助零基础用户轻松掌握经典机器学习知识。这套课程将学习路径规划为 12 周,包含 26 节精炼课程和 52 道配套测验,内容涵盖从基础概念到实际应用的完整流程,有效解决了初学者面对庞大知识体系时无从下手、缺乏结构化指导的痛点。 无论是希望转型的开发者、需要补充算法背景的研究人员,还是对人工智能充满好奇的普通爱好者,都能从中受益。课程不仅提供了清晰的理论讲解,还强调动手实践,让用户在循序渐进中建立扎实的技能基础。其独特的亮点在于强大的多语言支持,通过自动化机制提供了包括简体中文在内的 50 多种语言版本,极大地降低了全球不同背景用户的学习门槛。此外,项目采用开源协作模式,社区活跃且内容持续更新,确保学习者能获取前沿且准确的技术资讯。如果你正寻找一条清晰、友好且专业的机器学习入门之路,ML-For-Beginners 将是理想的起点。
ragflow
RAGFlow 是一款领先的开源检索增强生成(RAG)引擎,旨在为大语言模型构建更精准、可靠的上下文层。它巧妙地将前沿的 RAG 技术与智能体(Agent)能力相结合,不仅支持从各类文档中高效提取知识,还能让模型基于这些知识进行逻辑推理和任务执行。 在大模型应用中,幻觉问题和知识滞后是常见痛点。RAGFlow 通过深度解析复杂文档结构(如表格、图表及混合排版),显著提升了信息检索的准确度,从而有效减少模型“胡编乱造”的现象,确保回答既有据可依又具备时效性。其内置的智能体机制更进一步,使系统不仅能回答问题,还能自主规划步骤解决复杂问题。 这款工具特别适合开发者、企业技术团队以及 AI 研究人员使用。无论是希望快速搭建私有知识库问答系统,还是致力于探索大模型在垂直领域落地的创新者,都能从中受益。RAGFlow 提供了可视化的工作流编排界面和灵活的 API 接口,既降低了非算法背景用户的上手门槛,也满足了专业开发者对系统深度定制的需求。作为基于 Apache 2.0 协议开源的项目,它正成为连接通用大模型与行业专有知识之间的重要桥梁。