中文博客 on 范叶亮 | Leo Van

Recent content in 中文博客 on 范叶亮 | Leo Van

大语言模型微调 (Fine-tuning Large Language Models)

什么是微调 微调(Fine-tuning)是深度学习中迁移学习的一种方法,其将现有模型已经学到的知识作为学习新任务的起点。虽然微调表面上是模型训练中使用的一种技术,但它与传统意义上的“训练”截然不同。为了消除歧义,在这种情况下通常将传统意义上的“训练”称为预训练(Pre-training)。1 与微调类似的另一个概念称之为后训练(Post-training),两者均发生在预训练之后,其目的也都是为了进一步提升模型的效果,通常两者可以理解为相同的概念。从优化的发起人角度出发:当终端用户希望模型可以更好的适配自己的领域知识,则需要进行的操作称之为微调;当模型开发者希望可以更好的将模型与人类的价值、道德和预期保持一致,则需要进行的操作称之为后训练。2 为什么微调 从本质上讲,磨练一个预训练基础模型的能力要比从头开始训练一个新模型更容易,也更省钱,因为预训练已经获得了与当前任务相关的广泛知识。对于具有数百万甚至数十亿个参数的深度学习模型尤其如此,例如在自然语言处理(NLP)领域的大语言模型(LLM)或卷积神经网络(CNN)和视觉转换器(ViT)(用于计算机视觉任务,例如图像分类、对象检测或图像分割等)。 通过迁移学习利用先前的模型训练,微调可以减少获得适合特定用例和业务需求的大模型所需的昂贵算力和标记数据量。例如,微调可用于调整预训练大语言模型的对话语气或预训练图像生成模型的图片样式,还可用于使用专有数据或领域特定的专业知识来补充模型的原始训练数据集。 如何去微调 微调是需要进一步训练模型的技术,模型的权重已经通过先前的预训练得到了更新。使用基础模型的先前知识作为起点,通过在任务特定的较小数据集上进行训练来对模型进行微调。 虽然从理论上讲,任务特定的数据集都可以用于初始训练,但在小数据集上从头开始训练一个大模型可能会存在过拟合的风险:模型可能会在训练示例中学习得到良好的表现,但对新数据的泛化能力却很差。这会导致模型不适合给定的任务,也违背了模型训练的初衷。 因此,微调可以兼具两者的优势:利用对大量数据进行预训练所获得的广泛知识和稳定性,并训练模型对更详细、更具体概念的理解。 微调方法分类 参数角度 从参数角度,我们可以将微调分为全参数微调(Full Fine-tuning)和部分参数微调(Repurposing): 全参数微调:即对模型的所有参数进行微调。这种微调方法通常适用于任务与预训练模型之间存在较大差异的情况。全参数微调需要耗费较大的计算资源和时间,通常可以获得更好的性能,但在数据不足时容易出现过拟合问题。 部分参数微调:即仅对模型的部分参数或额外的模型参数进行微调。相比于全参数微调,部分参数微调可以在较少的计算资源和时间的情况下,在一些特定任务上提高模型的性能。 数据角度 从数据角度,我们可以将微调分为监督微调(Supervised Fine-tuning)和无监督微调(Unsupervised Fine-tuning): 监督微调:在进行微调时使用有标签的训练数据集。通过使用这些标签来指导模型的微调,可以使模型更好地适应特定任务。 无监督微调:在进行微调时使用无标签的训练数据集。通过学习数据的内在结构或生成数据来进行微调,以提取有用的特征或改进模型的表示能力。 方式角度 从方式角度,我们可以将微调划分为指令微调(Instruction Fine-tuning)和对齐微调(Alignment Fine-tuning): 指令微调:利用格式化的实例以有监督的方式微调大语言模型。通过指令微调不仅可以改善模型的性能,同时也可以增强模型的泛化能力。 对齐微调:为了避免大语言模型的幻觉问题,以及同人类的价值观和偏好对齐,提高伦理表现,将人类整合到模型的训练过程中。例如基于人类对结果的反馈,模型通过强化学习从而与人类对齐。 显存消耗 在进行模型微调时我们需要关注显存的消耗,以确保在当前的硬件环境中可以正常进行微调。在进行模型微调时,显存消耗主要同如下因素有关: 参数精度:决定了每个参数占用的实际显存大小,例如 FP32(单精度)占用 4 字节,FP16(半精度)占用 2 字节,INT8 占用 1 字节等。 模型参数:微调所需显存的基础决定因素,例如微调一个 1B 模型,其参数总量则为 10 亿。 梯度:在微调过程中需要存储反向传播计算得到的梯度信息,这部分大小同模型参数相同,精度同训练精度一致。 优化器状态:优化器所需的额外信息,例如动量、方差等,不同的优化器占用的显存大小也不尽相同,保守估计为模型参数所需显存的 4 倍。 激活值:用于储存前向传播过程中的中间结果,所需显存大小主要受到批大小和序列长度的影响,量级相比前面可以忽略不计。 以全参数微调一个 1B 模型为例,所需的显存大致为 12GB(模型参数:2GB,梯度:2GB,优化器状态:8GB)。在使用部分参数微调时,模型参数本身不变,需要微调的参数会大幅度减少,因此梯度和优化器状态所需的显存也会大幅度减少。在应用一些量化技术后,例如使用 INT8 精度进行微调,则所有部分的显存占用均会相应的减少。 参数高效微调 参数高效微调部分主要参考了 llm-action 项目。 参数高效微调(Parameter-Efficient Fine-Tuning)是指固定大部分预训练模型的参数,仅微调少量或额外的模型参数的微调方法,从而可以极大的降低计算成本。参数高效微调可以从方法的角度可以分为三类 3:引入新参数(Addition-based),微调部分参数(Selection-based)和重参数化(Reparametrization-based),其中引入新参数又可以划分为适配器方法和软提示方法两种。 在实战过程中我们可以使用 Huggingface 开源的 PEFT 扩展库进行参数高效微调,作为 Huggingface 开源项目其可以与 Transformers,Accelerate 和 Diffusers 等多个开源库无缝衔接使用。在官方文档中我们可以查看支持的微调类型和任务类型。 Prefix Tuning Prefix Tuning 4 是在输入的 token 前构造与任务相关的 virtual token 作为前缀,在微调的时候仅更新前缀部分的参数。 针对不同的模型结构,构造的前缀也有所不同: 自回归结构:添加前缀后,得到 $z = \left[\text{PREFIX}; x; y\right]$。 编码器-解码器结构:在编码器和解码器前均添加前缀,得到 $z = \left[\text{PREFIX}; x; \text{PREFIX}’; y\right]$。 作者发现直接更新前缀的参数会导致训练不稳定,因此在前缀之前添加了 MLP 结构,在训练完成后 MLP 部分的参数无需保留,仅保留前缀的参数即可。同时作者也针对前缀使用多少个 virtual token 以及前缀放置的位置对于微调的性能影响进行了相关实验,细节请参考论文原文。 Prompt Tuning Prompt Tuning 5 可以看作是 Prefix Tuning 的简化版本,其为每个任务定义不同的 Prompt,然后拼接到数据上作为输入。 Prompt Tuning 仅在输入层添加 token,不需要像 Prefix Tuning 那样添加 MLP 结构解决训练问题。通过实验发现,随着模型参数的增加,Prompt Tuning 的效果会逼近全参数微调的效果。 实验还发现,与随机和使用样本词汇表的初始化相比,采用类标签初始化的微调效果更好,但随着模型参数的增加,效果差异会消失。同时 Prompt 的 token 长度在 20 左右可以获得不错的效果,同样随着模型参数的增加,token 长度带来的增益会消失。 P-Tuning P-Tuning 6 将 Prompt 转换为可学习的 Embedding 层,再通过 MLP+LSTM 结构对 Prompt Embedding 进行处理。 对比 Prefix Tuning,P-Tuning 增加了可微的 virtual token,但仅作用于输入层,没有在每一层都添加。同时,virtual token 的位置也不一定是前缀,其插入的位置是可选的,目的是将传统人工设计的 Prompt 模板中的真实 token 替换为可微的 virtual token。 实验发现随机初始化 virtual token 容易陷入局部最优,因此通过一个 Prompt Encoder(即 MLP+LSTM 结构)对其进行编码可以获得更好的效果。 P-Tuning v2 Prompt Tuning 和 P-Tuning 存在如下问题: 缺乏规模通用性:Prompt Tuning 在模型超过 100 亿时可以与全量微调媲美,但对于较小参数的模型与全量微调的效果有很大的差距。 缺乏任务普遍性:Prompt Tuning 和 P-Tuning 在一些 NLU 基准测试中表现出优势,但对序列标注任务的有效性尚未得到验证。 缺少深度提示优化:在 Prompt Tuning 和 P-Tuning 中,提示仅被插入到输入中,后续插入提示位置的嵌入是通过之前层计算得到。这可能导致插入提示对模型最终效果的影响有限,同时由于序列长度限制可调参数的数量也是有限的。 考虑到上述问题,P-Tuning v2 7 利用深度提示优化(例如:Prefix Tuning)对 Prompt Tuning 和 P-Tuning 进行改进,类似 Prefix Tuning,其在每一层都加入了 Prompt token 作为输入。 P-Tuning v2 还通过移除重参数化编码器、针对不同任务采用不同提示词长度、引入多任务学习、使用传统分类标签范式等方法进行模型改进。实验表明 P-Tuning v2 可以在不同规模和不同任务中实现与全量微调相媲美的效果。 Adapter Tuning Adapter Tuning 8 设计了一个 Adapter 模块,并在每个 Transformer 层中插入两个 Adapter 模块,在微调时仅更新增加的 Adapter 模块和 Layer Norm 层中的参数。 Adapter 模块由两个前馈层和一个非线性层组成。第一个前馈层将 Transformer 的输入从 $d$ 维(高维)映射到 $m$ 维(低维),其中 $m \ll d$。通过控制 $m$ 的大小可以限制 Adapter 模块的参数量。中间通过非线性层后,第二个前馈层再将 $m$ 维映射回 $d$ 维作为 Adapter 模块的输出。同时通过 Skip Connection 将 Adapter 的输入也添加到 Adapter 的输出中,这样可以保证即便 Adapter 最开始的参数值接近 0,通过 Skip Connection 的设置也可以保证训练的有效性。 LoRA 神经网络包含很多全连接层,其通过矩阵乘法实现。很多全连接层的权重矩阵是满秩的,针对特定任务微调后,模型的权重矩阵其实具有很低的本征秩(intrinsic rank),因此将参数矩阵投影到更小的空间仍可以得到有效的学习。LoRA 9 的核心思想就是通过低秩分解降低特征矩阵的参数量,从而以较小的参数量来实现大模型的间接训练。 在涉及到矩阵相乘的模块中,在其旁边增加一个新的通路,通过第一个矩阵 $A$ 将维度从 $d$ 降至 $r$,在通过一个矩阵 $B$ 将维度从 $r$ 升回 $d$,其中 $r \ll d$。 在微调时,仅更新上述两个矩阵的参数,再将两条通路的结果相加作为最终的结果,即 $h = W_0 x + B A x$。训练时矩阵 $A$ 通过高斯函数初始化,矩阵 $B$ 初始化为零矩阵,这样训练开始时 $B A = 0$,从而可以确保新的通路对模型的结果没有影响。 在 Attention 模块中,权重矩阵包括用于计算 Q,K,V 的 $W_q$,$W_k$,$W_v$ 以及多头注意力的 $W_o$,实验表明保证权重矩阵的种类数量比增加秩 $r$ 的大小更为重要,通常情况下 $r$ 选择 4,8,16 即可。 IA3 IA3 10 不同于 LoRA 学习低秩权重,而是通过学习向量($l_k$,$l_v$,$l_{ff}$)对模型的部分参数进行加权实现对一些激活层的抑制或放大,同时优化损失函数以适应少样本学习。 https://www.ibm.com/cn-zh/think/topics/fine-tuning ↩︎ Huyen, Chip. AI Engineering: Building Applications with Foundation Models. O’Reilly Media, Incorporated, 2024. ↩︎ Lialin, Vladislav, Vijeta Deshpande, and Anna Rumshisky. “Scaling down to scale up: A guide to parameter-efficient fine-tuning.” arXiv preprint arXiv:2303.15647 (2023). ↩︎ Li, Xiang Lisa, and Percy Liang. “Prefix-tuning: Optimizing continuous prompts for generation.” arXiv preprint arXiv:2101.00190 (2021). ↩︎ Lester, Brian, Rami Al-Rfou, and Noah Constant. “The power of scale for parameter-efficient prompt tuning.” arXiv preprint arXiv:2104.08691 (2021). ↩︎ Liu, Xiao, et al. “GPT Understands, Too.” arXiv preprint arXiv:2103.10385 (2021). ↩︎ Liu, Xiao, et al. “P-tuning v2: Prompt tuning can be comparable to fine-tuning universally across scales and tasks.” arXiv preprint arXiv:2110.07602 (2021). ↩︎ Houlsby, Neil, et al. “Parameter-efficient transfer learning for NLP.” International conference on machine learning. PMLR, 2019. ↩︎ Hu, Edward J., et al. “Lora: Low-rank adaptation of large language models.” ICLR 1.2 (2022): 3. ↩︎ Liu, Haokun, et al. “Few-shot parameter-efficient fine-tuning is better and cheaper than in-context learning.” Advances in Neural Information Processing Systems 35 (2022): 1950-1965. ↩︎

2025/8/9
articleCard.readMore

提升图片分辨率和质量

本节将介绍如何将一张低分辨率的图片转换成一张高分辨率高质量的图片。 Bilibili YouTube 为了对比提升分辨率后的图片质量,我们先下载一张原始的高清图片 1,通过 ImageMagick 命令将其转换为一个低分辨率低质量的图片,未来基于这张转换后的图片进行分辨率和质量的提升,再与原始图片进行对比验证提升的效果。 magick toucan-raw.jpg -resize 512x -quality 10 toucan-low-res.jpg 基础操作 单击左侧 按钮进入 Upscaling 界面,将上面生成的低分辨率图片拖入 Assets 中,之后再拖入到 Upscale 面板的图片区域。 安装的 SDXL 模型包中包含一个 SwinIR 分辨率提升模型。选择该模型并添加如下的正向和负向提示词: 正向提示词 a vibrant bird, detailed, a high contrast, inviting warmth, sunlit elements, dynamic composition, 35mm lens, 1/2.8, environmental context, detailed har photography 负向提示词 blurry, out of focus, over saturated, text+++ 将 Creativity 和 Structure 保持默认值 0,单击 Invoke 按钮生成图片。生成完毕后在 Assets 中的低分辨图片上右键,单击 按钮选择进行对比,单击 Images 选项卡回到新生成的图片页面查看对比效果。 从如下的对比中可以看出,生成的图片具有更多的细节,这样我们就可以在更高的分辨率下获得更加清晰锐利的图像。 单击画布上方的 Exit Compare 退出对比,在生成的图片上右键,单击 按钮可以在新窗口中打开图片,之后则可以使用鼠标进行放大来观察图片细节。 进阶操作 除了基础的分辨率提升以外,还有一些高级选项可以用来控制分辨率提升后的图片与原始图片的相似程度以及在提升分辨率过程中的创意性。Creativity 参数用于设置提示词控制图像生成的创意性,从而控制与原始图像的差异程度,值越大表示越具有创意性。Structure 参数用于确保分辨率提升过程中图像中的元素与原始图像中的元素的形状和位置匹配,值越大表示越会严格遵守原始图片的结构。 同时也可以调整生成的模型及其相关参数,例如:Scheduler 和 CFG Scale 等。 风格调整 如果在提升分辨率的过程中还希望调整图片的风格,你可以尝试使用不同的提示词,例如下面是一个绘画风格的提示词: a painting of a vibrant bird, oil painting, deep impasto, glazed brushstrokes 我们适当增加 Creativity 的值到 8,减少 Structure 的值到 -4,这样可以给到模型更多的自由度重新构思一些细节。最后我们来综合对比原始图片、低分辨率图片、提升分辨率后的图片以及调整风格的图片: 至此,我们 Invoke AI 101 系列的 6 节课程就全部结束了,希望大家能够通过这 6 节课程对 Invoke AI 的功能有一个基础的认知。也期待大家能够进一步探索 Invoke AI 的高级功能,去创造更多自己喜欢的图片。 图片来源:https://pixabay.com/zh/photos/toucan-nature-bird-beak-costa-rica-9603854/ ↩︎

2025/7/5
articleCard.readMore

使用画布创建和组合生成新的图片

本节将介绍在使用画布进行创建和组合生成新的图片的过程中使用到的核心工具。 Bilibili YouTube 首先,我们使用 Juggernaut XL v9 模型和如下提示词生成一张基础图片留作备用: 提示词模板: Environment Art 正向提示词: futuristic terraced structure built into a mountain at dusk, twilight hues, lush greenery illuminated by soft glowing lights, multiple levels, pathways, vast mountain range, distant winding roads, glowing city lights below, towering otherworldly rock formations 为了保证可复现性,在生成这张图片时可以将随机数种子 Seed 固定,此处设置为 42。 边界框 将生成的图片拖入画布并创建一个新的 Raster Layer。在画布上单击 Bbox 按钮,此时图片的周围将显示一个边界框,使用鼠标按住可以拖动边界框的位置,放在边界框的四角可以调整边界框的大小。 将边界框移动到画布的一个完全空白的区域,此时边界框中没有任何 Raster Layer 的内容,单击 Invoke 会生成一张新的图片。 将边界框移动到画布的一个包含部分 Raster Layer 内容的区域,单击 Invoke 会将空白的部分补全,通常称之为 Out Painting 或 Infilling。 如果边界框和 Raster Layer 完全重合,单击 Invoke 将会基于当前 Raster Layer 中的内容重新生成新的图片,通常称之为图像到图像(Image2Image)。 修复蒙版 修复蒙版(Inpaint Mask)用于控制在边界框中哪些区域会被修改。在图层中单击 + 新建一个 Inpaint Mask 图层。单击画布上的 按钮后,则可以在画布上绘制所需要修改的区域。 此时可以将边界框进行缩放并移动到关注的指定区域。从左侧的 Image 面板中可以看到边界框的大小为 320x320。 但在图片生成过程中,仍然会以 1024x1024 分辨率进行生成,再通过缩放填充到边界框中。由于先生成了分辨率更高的图片,再进行的缩放,此时生成的部分可以具有更多的细节。这使得我们可以在不牺牲图片质量的前提下,对于图片的复杂区域进行优化。 修复蒙版可以让我们很方便的在图片中添加、移除和改变元素。例如,我们希望在楼梯台阶上添加两个人,可以按照如下步骤进行操作: 将边界框调整到一个合适的大小和位置。 创建一个 Inpaint Mask 图层,使用画笔勾勒出需要修改的区域。 返回 Raster Layer,选择一个颜色,使用画笔勾勒出两个人的大概位置。 在提示词的前面添加 two people。 选择一个合适的 Denoising Strength,此处设置为 0.7,单击 Invoke 按钮启动生成。 可以看出,在修复蒙版区域内,根据 Raster Layer 的修改和提示词的修改成功的在台阶上添加了两个人。

2025/6/28
articleCard.readMore

探索 AI 模型和概念适配器

本节将介绍图片生成使用到的 AI 模型和概念适配器,我们将讨论提示词和模型的训练方式是如何最终决定提示词对生成图片的有效性。 Bilibili YouTube 在图片生成过程中你需要认识到提示词并非在任何时候都是有效的。相同的提示词在一个模型上可以获得很好的生成效果,但在另一个模型上可能无法生成所需的图片。这是由于模型训练所使用的图片和图片的标注文本不同所导致的,因此模型的提示词指南会格外重要。这也是为什么训练自己的模型会更好,因为你完全清楚在训练模型时所使用的标注文本。 在本节教程中我们不会讨论如何训练模型,我们将重点介绍不同模型之间的差异,展示它们如何影响图片生成的过程。除此之外我们还会讨论概念适配器(通常也称为 LoRA),来帮助你更好的理解相关工具。 模型比较 进入模型管理页面,在添加模型选项卡处选择 HuggingFace,输入 cagliostrolab/animagine-xl-4.0 下载 Animagine XL 4.0 模型。Animagine XL 模型与其他的通用模型有很大的不同,Animagine XL 模型是一个动漫主题的 SDXL 微调模型。Animagine XL 4.0 基于 Stable Diffusion XL 1.0 进行重新训练所得,它使用了 840 万张不同来源不同动漫风格的图片进行微调。 Animagine XL 模型使用了一套自有的数据标注方法进行训练,提示词指南如下图所示: 整个提示词包含 6 个部分: 性别。例如:1girl/1boy/1other。 角色。例如:remilia scarlet。 来源作品。例如:touhou。 分级。例如:safe, sensitive, nsfw, explicit。 一般标签。 质量标签。例如:masterpiece, high score, great score, absurdres。 建议的负向提示词如下: lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry Animagine XL 模型支持一些特殊的标签用于控制图片生成。质量标签直接影响生成图像的整体质量和细节水平。可用的质量标签有:masterpiece,best quality,low quality,worst quality。 masterpiece, best quality low quality, worst quality 与质量标签相比,分数标签可以对图像质量进行更细致的控制。在 Animagine XL 模型中,它们对输出质量的影响更大。可用的分数标签有:high score,great score,good score,average score,bad score,low score。 high score, great score bad score, low score 时间标签允许根据特定时间段或年份来控制生成图片的艺术风格。这对于生成具有特定时代艺术特征的图像非常有用。支持的年份标签有:year 2005,year {n},year 2025。 year 2007 year 2023 利用如下正向和负向提示词分别使用 Animagine XL 4.0 模型和 Juggernaut XL v9 模型生成图片。 正向提示词 1boy, black hoodie, white spiky punk hair, nose piercing, standing against a brick wall, masterpiece, best quality, high score, great score 负向提示词 lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry 同时,为了比较提示词在不同模型中对生成图片的影响,再利用如下正向和负向提示词分别使用 Animagine XL 4.0 模型和 Juggernaut XL v9 模型生成图片。 正向提示词 1boy, black hoodie, white spiky punk hair, nose piercing, standing against a brick wall 负向提示词 lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, signature, watermark, username, blurry 为了确保可复现,手动将随机数种子固定设置为 42,生成的图片如下: 不难看出,从正向和负向提示词中删除关于图片质量的关键词后,Animagine XL 4.0 模型生成的图片有显著的画质降低,而 Juggernaut XL v9 模型生成的图片质量变化并不大。实验说明提示词在不同的模型中效果存在差异,你必须了解模型的训练过程才能更好的使用提示词生成所需的图片。 概念适配器 上述问题就导致了概念适配器的诞生,通过自己训练概念适配器,你可以完全掌握对于模型的修改。进入模型管理页面,在添加模型选项卡处选择 HuggingFace,输入 nerijs/pixel-art-xl 下载 Pixel Art XL 概念适配器。利用如下正向和负向提示词分别使用 Animagine XL 4.0 模型和 Juggernaut XL v9 模型生成图片。 正向提示词 1boy, black hoodie, white spiky punk hair, nose piercing, standing against a brick wall, masterpiece, best quality, high score, great score, pixel art style 负向提示词 lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry 在 Generation 中添加概念适配器 pixel-art-xl 并启用: Pixel Art XL 概念适配器用于生成像素风格的图片,生成的图片如下: 需要注意,LoRA 这类概念适配器与用于训练的原始模型之间存在关联。概念适配器可以理解为一个模型的上层封装,其扩展和增强了某些概念。将其放在另一个模型上,其仍可以将这个概念应用到新的模型上,但质量可能会有所下降。这是因为训练概念适配器的底层模型与当前应用的模型可能有着不同的架构和假设。也就是说 LoRA 这类模型并不是一个完全独立的模型,而是一个专门为某个基础模型构建的适配器,但如果两个模型本质上非常相似,则 LoRA 具有一定的可移植性。

2025/6/20
articleCard.readMore

理解图像到图像和降噪过程

本节将介绍「 图像到图像」和「降噪」两个重要概念,帮助大家更好的理解 Invoke 中的画布是如何工作的,或者说生成式 AI 图片生成是如何工作的。 Bilibili YouTube 在之前的图像生成示例中,单击 Invoke 按钮后,整个图像生成过程会从一张静态噪声图像开始,模型会将噪声逐步转化为最终图片,整个过程如下图所示: 将噪声图像转化成最终图片的过程称之为降噪(Denoising)。 将示例图片拖拽至画布上,选择 New Raster Layer,示例图片如下 1: 在图层中,可以找到 Denosing Strength 参数: Denosing Strength 用于控制初始图片或 Raster Layer 在降噪过程中影响最终输出图片的程度。设置较高的值会使降噪过程从一个具有更多噪声数据的图片开始,此时模型会具有更高的自由度根据提示词生成新的内容。 图像到图像和文本到图像的最主要区别在于图像生成的起点。文本到图像时从纯噪声开始,并根据提示词逐步细化。图像到图像则会根据 Denosing Strength 跳过前面的一些步骤,使用提供的图像作为起点。 以 a porcelain teacup on a table 作为提示词,选择 Photography (General) 作为提示词模板,分别将 Denosing Strength 设置为 0.2 和 0.8,生成的图片和原始图片对比如下: 可以看出,较高 Denosing Strength 值可以让模型具有更高的自由度生成图片,而设置较低的 Denosing Strength 值时生成的图片仅有少量的变化。除此之外,在控制层和参考图片上也可以设置 Denosing Strength 来取得不同的效果。 https://pixabay.com/zh/photos/teacup-ceramic-table-room-curtain-2605474/ ↩︎

2025/6/14
articleCard.readMore

使用控制层和指示控制图片的生成

本节将介绍如何使用控制层和指示来精确控制图片的生成。 Bilibili YouTube 控制网络 控制层通常用作控制网络(ControlNet)在图片生成中以线或结构的形式来提供参考。在 SDXL 中,常用的预训练控制网络有 1: Contour Detection (scribble) Scribble 模型可以根据输入的图片生成线条画,处理后的图片作为原始图片的简化版本可以精准的捕捉图片的轮廓。 Depth Map Depth Map 可以生成图片的深度图,在图片生成中模拟深度效果,从而创建更加逼真的 3D 图片。 Hard Edge Detection (canny) Canny 边缘检测的原理是通过寻找强度突变来识别图片中的边缘。它能够准确的检测边缘并减少噪声和伪边缘,通过降低阈值可以识别更多的信息。采用 Canny 模型时,将会生成与检测到的边缘匹配的图片。 Pose Detection (openpose) Openpose 模型可以检测身体、手部、面部等多个部位的关键点,在生成图片时可以控制人体的姿态。 Soft Edge Detection (softedge) Softedge 模型与 Scribble 模型类似,处理后的图片作为原始图片的简化版本仅保留形状的柔和边缘和一些浅阴影。 Tile Tile 模型的主要功能可以归结为如下两点: 可以重新解读图片中的特定细节,并创建全新的元素。 当全局指令与图片的局部或特定部分存在差异时,可以忽略这些指令。在这种情况下,它可以使用局部上下文指示图片生成。 Tile 模型可以作为增强图片质量和细节的工具。如果在图片中存在一些不足的地方,例如调整图片大小导致的模糊,Tile 模型可以有效的消除此类问题从而获得更加清晰锐利的图片。此外 Tile 模型还可以添加更多细节,从而提升图片的整体质量。 控制层 在图层中单击 + Control Layer 添加控制层,或者在图片库中将图片拖拽至画布上,选择 New Control Layer。 示例图片及其生成的提示词如下: 正向提示词: futuristic terraced structure built into a mountain at dusk, twilight hues, lush greenery illuminated by soft glowing lights, multiple levels, pathways, vast mountain range, distant winding roads, glowing city lights below, towering otherworldly rock formations, dreamy sky with soft clouds 负向提示词: painting, digital art, sketch, blurry 控制层 本教程以 Hard Edge Detection (canny) 模型为例,单击 Filter 中的 Advanced 可以进行更多的调整: 调整完毕各项参数后,单击 Apply 即可生成控制图片。 控制图片提供的线条可以为图片生成提供参考。在画布中,可以使用 、 和 等工具对控制图片进行添加或删除等微调操作。接下来对控制层进行设置: Weight:用于设置控制图层在图片生成过程中的权重,值越大则图片生成时越会严格遵守控制线条。 Begin/End %:用于设置在图片生成过程中在何时使用控制图层,将其设置为 0% 至 100% 表示从图片生成开始至结束一直使用控制图层。调整开始和结束的百分比可以给到模型更多的灵活性,从而获得更好的生成图片。 Control Mode:包含多种控制模式用于调节控制层的相关性(CFG scale): Balanced:提示词和控制层同等重要。 Prompt:提示词更加重要。 Control:控制层更加重要。 不同的 Control Mode 的生成示例如下 2: 单击 Invoke 开始生成图片。此时可以看到模型在控制线条的指示下开始生成图片。 由于将结束百分比设置为了 70%,生成的图片也具有一定的灵活创意。单击 会将生成的图片添加到 Raster Layer。单击 可以将画布保存到图片库中。新生成的图片和用于生成控制层的图片对比如下: 参考图片 接下来我们将添加一个参考图片,也称为 IP Adapter。模型会根据参考图片的风格和结构来生成图片。首先将参考图片添加到图片库,本教程中使用的图片如下 3: 将图片拖拽至左侧 Reference Image 上。 在左侧将会创建一个 Reference Image,并可以对其进行相关设置。 模型:在安装 SDXL 相关模型的情况下,选择 Standard Reference (IP Adapter ViT-H) 模型,这里可以选择 ViT-H、ViT-G 和 ViT-L 多种变种。 Mode:Style and Composition 表示参考样式和结构,Style 表示仅参考样式,Composition 表示仅参考结构。 Weight:类比控制层。 Begin/End %:类比控制层。 在本教程中,将参考图片的 Weight 设置为 0.7,将结束百分比设置为 70%,从而给到模型更多的自由度。删除之前生成的 Raster Layer,单击 Invoke 开始生成图片。应用参考图片生成的图片和之前生成的图片对比如下: 可以看出新生成的图片在遵循控制层的前提下同时应用了参考图片的白色风格。 局部指示 在新生成的图片中,可以看到左侧背景中的山峰也被应用了参考图片中的建筑风格。为了实现更精确的控制,可以选择局部参考图片,将图片拖拽至画布上,选择 New Regional Reference Image。 在画布上选择 ,设置合适的笔刷宽度,在画布上勾勒出需要应用参考的区域,这里我们仅勾勒出建筑所在的区域。 单击 Invoke 开始生成图片。应用局部参考图片生成的图片和应用参考图片生成的图片对比如下: 可以看出新生成的图片仅在勾勒的区域内与参考图片的白色风格保持了一致,背后的山峰仍根据提示词生成相应的风格。 基于文本的指示 除了基于图片的指示以外,还可以创建基于文本的局部指示。在图层中单击 +,选择 Reginal Guidance 创建局部指示。在画布上选择 ,设置合适的笔刷宽度,在画布上勾勒出需要应用参考的区域,这里我们勾勒出建筑后面的一座山峰。在 Reginal Guidance 中选择 + Prompt 并添加 lush greenery 提示词。 单击 Invoke 开始生成图片。应用局部基于文本的指示的图片和应用局部参考图片生成的图片对比如下: 可以看出新生成的图片在勾勒的山峰区域增加了郁郁葱葱的树木。 https://support.invoke.ai/support/solutions/articles/151000105880-control-layers ↩︎ https://github.com/Mikubill/sd-webui-controlnet ↩︎ https://pixabay.com/zh/photos/modern-building-business-district-4428919/ ↩︎

2025/6/7
articleCard.readMore

使用 Invoke 创作你的第一张图片

本节将展示如何使用 Invoke 创作你的第一张图片。 Bilibili YouTube 安装 Invoke 从 Invoke 官网下载对应系统的安装包,根据如下步骤完成安装。 运行 Invoke 社区版本,单击 Install 开始安装。 选择安装位置,单击 Next 进行下一步。 选择安装版本,单击 Next 进行下一步。 确认 GPU 情况,单击 Next 进行下一步。 确认安装选项,单击 Install 开始安装。 安装完成,单击 Finish 关闭安装向导。 安装完成后,单击 Launch 启动 Invoke。 用户界面 Invoke 启动后会打开主界面,从启动器的日志不难看出,Invoke 在后台启动了一个 HTTP 服务,用浏览器打开 http://127.0.0.1:9090 可以得到相同的界面: 主界面包含三个区域,分别是: 左侧:用于输入提示词、模型选择和参数设置等。 中间:用于显示生成的图片。 右侧:用于显示生成图片的所需的图层和历史生成的图片等。 左侧 提示词 提示词区域用于输入生成图片提示词。 在下拉菜单中可以选择预设的提示词模板。 在 Prompt 中输入正向提示词,在 Negative Prompt 中输入负向提示词。 也可以将自定义的提示词添加为模板以方便后续使用。 图像 图像区域用于控制生成图片的比例和大小。 Aspect 用于设置生成图片的宽高比,Width 和 Height 用于设置生成图片的宽高。 Seed 用于设置生成图片的随机数种子,设置相同的随机数种子将在相同的条件下得到一致的图片,这在微调的时候会派上用场。 生成 生成区域用于设置模型和相关参数。 Model 用于选择生成图片的模型。 Concepts 用于选择进行微调的 LoRA 模型。 高级选项中可以设置图片生成使用的调度器(Scheduler)。调度器负责数据采样,包括采样的步骤数(Steps)和提示词相关性(CFG Scale)等。 中间 中间区域用于查看和调整生成的图片。在后续教程中将会深度介绍如何使用该区域来优化生成的图片。 在上方选项卡中单击 Canvas 可以打开画布,单击 Image Viewer 可打开图片浏览器。 右侧 图板和图片库 图板用于组织和管理用户生成的图像,它提供了一种结构化的方式来高效地分类和访问这些图像。 在图片库中,你可以将图片拖拽到画布中来使用。此外,你还可以在图库中共享、下载和删除图片。 图层 图层区域显示了工作区中用于修改图像的所有活动图层。单击右上角的 + 图标即可添加新图层。你可以创建多个图层并对其进行操作和变换,在生成图像之前进行组合使用。 模型 单击左侧面板的 图标打开模型页面。针对新手用户 Invoke 提供了新手模型包,单击 Starter Models 选项卡,可以看到系统提供了 Stable Diffusion 1.5、SDXL 和 FLUX 三款模型包。根据官方系统环境要求和自己的设备性能选择合适的模型包。 Stable Diffusion 1.5 GPU:Nvidia 10xx 或更新,4GB+ 显存 内存:至少 8GB 磁盘:至少 30GB SDXL GPU:Nvidia 20xx 或更新,8GB+ 显存 内存:至少 18GB 磁盘:至少 100GB FLUX GPU:Nvidia 20xx 或更新,10GB+ 显存 内存:至少 32GB 磁盘:至少 200GB 在下载模型前,需在 HuggingFace 的设置页面创建 Token,并将其保存在 Invoke 模型页面的 HuggingFace 选项卡中。本教程选择 SDXL 作为模型包,单击模型包启动下载。模型下载完毕后可以在 Model Manager 中查看已下载的模型。 创作图片 在提示词模板中选择 Environment Art,在 Prompt 中输入如下提示词: futuristic urban park, neon lighting, raised highways, green spaces, modern architecture 单击模板中的 按钮会开启应用模板后的提示词预览,如下图所示: 可以看到,提示词模板在用户输入的提示词基础上添加了更多的正向和负向提示词。在 Generation 中选择 Juggernaut XL v9 作为生成模型。 在左上角 中输入生成图片的数量,单击 Invoke 开始生成图片。单击左侧面板的 图标打开队列页面,等待中、执行中和已完成的所有任务都将显示在该页面中: 生成的 3 张图片如下所示: 中文测试 为了验证 Juggernaut XL v9 模型对于中文提示词的兼容性,对上述示例和 Environment Art 提示词模板中的提示词整理中英文对照版本如下: 提示词 英文 中文 用户 - 正向 futuristic urban park, neon lighting, raised highways, green spaces, modern architecture 未来城市公园, 霓虹灯, 高架公路, 绿色空间, 现代建筑 模板 - 正向 environment artwork, hyper-realistic digital painting style with cinematic composition, atmospheric, depth and detail, voluminous. textured dry brush 2d media 环境艺术作品, 具有电影构图的超现实数字绘画风格, 大气, 深度和细节, 丰满, 纹理干笔画 2D 媒体 模板 - 负向 photo, distorted, blurry, out of focus. sketch. 照片, 扭曲, 模糊, 失焦, 草图 用户的正向提示词使用中文,选择 Environment Art 提示词模板(即提示词模板使用英文),生成的图片如下: 用户的正向提示词使用中文,不选择提示词模板,将 Environment Art 提示词模板的中文提示词补充到用户的正向和负向提示词后,生成的图片如下: 不难看出虽然图片生成的画质不错,但其并未遵循提示词的指令生成(看起来是将中文提示词作为了画风),因此可以判断 Juggernaut XL v9 模型不具备直接使用中文提示词的能力。

2025/6/1
articleCard.readMore

在 OpenWrt 和群晖中自动申请和部署证书

为了在本地局域网环境中摆脱 IP 用上域名(纯属闲来无事瞎鼓捣),购入了 leovan.dev 域名。想着把各种服务都映射到不同的二级域名上,这样就可以不用 IP 和端口了,岂不完美。然,问题这就来了。 acme.sh 域名是在 Cloudflare 上申请的,在 Cloudflare 上使用 Page 服务部署网站就可以白嫖他家的证书,还能自动帮你续期,比如当前的站点就是使用 Page 进行部署的。但你要是想生成证书下载下来使用,就会很麻烦,因为证书的有效期只有三个月,手动续期再加上各种替换操作就不太方便了。 这时候就要请出我们的 acme.sh 了,除了支持各种桌面和服务器操作系统外,还支持 OpenWrt 路由器系统。 Cloudflare 使用 acme.sh 申请免费证书需要使用 DNS 验证对域名的所有权,本文以 Cloudflare 为例,其它 DNS 请参考官方文档。Cloudflare 支持两种方式,一种是使用 API Token,另一种是使用全局 API Key,这里我们以 API Token 为例。 进入 API Token 页面,单击 创建令牌 按钮,在 API 令牌模板中选择 编辑区域 DNS,单击 使用模板 按钮。在 权限 中添加 区域 - DNS - 编辑 和 区域 - 区域 - 读取 的权限,在 区域资源 中根据你的需求选择对应的 特定区域,例如 leovan.dev,或为了省事选择 所有区域 也可以,如下图所示: 创建完毕后会生成 Token,将 Token 保存为 CF_Token="xxxxxxxxx",注意该 Token 在 Cloudflare 中不会再展示。 之后从 Cloudflare 账户主页进入对应的域名详情页面,在右下角可以找到 API 的区域 ID 和账户 ID 两个代码,如下图所示: 将区域 ID 保存为 CF_Zone_ID="xxxxxxxxx",将账户 ID 保存为 CF_Account_ID="xxxxxxxxx"。 OpenWrt Opwnert 使用 uHTTPd 作为默认的 Web 服务器。正如官网上说的,这是一个轻量极了的 Web 服务器,以至于不支持反向代理。 警告 那就安装一个 Nginx 吧,不是说不行,只是 Nginx 和 uHTTPd 存在冲突,你需要把路由器的 LuCI 也切换到 Nginx 上,麻烦不说,后续如果有更新还有可能又会变回 uHTTPd。自己搞了下,差点登录不进去 Web 页面,遂放弃。 但这不影响我们先把路由器的域名 router.leovan.dev 映射到 192.168.100.1 上先用起来。 acme.sh 通过 系统 - 软件包 或命令行安装 acme.sh 相关软件包: opkg install acme acme-acmesh-dnsapi luci-app-acme luci-i18n-acme-zh-cn luci-ssl-openssl 安装完毕后可以在 服务 菜单下找到 ACME 证书 子菜单,进入后在 ACME 全局配置 中输入 电子邮件帐户,勾选 启用调试日志记录,如下图所示: 在 证书配置 中删除默认的配置,在下方输入框中输入配置名称,例如 leovan_dev,如下图所示: 单击 添加 按钮打开配置对话框。在 常规设置 中勾选 已启用,输入所需的域名,选择验证方式为 DNS,其它保持默认,如下图所示: 在 DNS 质询验证 中选择对应的 DNS API(本文使用 CloudFlare.com),并将上文中的 CF_Token="xxxxxxxxx"、CF_Zone_ID="xxxxxxxxx" 和 CF_Account_ID="xxxxxxxxx" 填写到对应的位置,其它保持默认,如下图所示: 在 高级设置 中根据自己的需求选择 密钥长度(本文使用 ECC 256 位),其它保持默认,如下图所示: 单击 保存 按钮,并在 ACME 证书 页面单击 保存并应用 按钮。 稍等片刻后,如果运行正常则可以在 证书 中看到对应域名的证书,如下图所示: 同时,系统会启动自动续签,在 系统 - 计划任务 中可以看到添加了如下一条记录: 0 0 * * * /etc/init.d/acme renew uHTTPd 通过 系统 - 软件包 或命令行安装 uHTTPd 的管理界面: opkg install luci-app-uhttpd luci-i18n-uhttpd-zh-cn 安装完毕后可以在 服务 菜单下找到 uHTTPd 子菜单,进入后在 MAIN - 常规设置 中添加 HTTPS 监听,如下图所示: 在 HTTPS 证书 中选择上文中生成的证书(本文为 /etc/ssl/acme/leovan.dev.fullchain.crt),如下图所示: 在 HTTPS 私钥 中选择上文中生成的私钥(本文为 /etc/ssl/acme/leovan.dev.key),如下图所示: 在 uHTTPd 页面单击 保存并应用 按钮。 在 Cloudflare 中将 router.leovan.dev 解析到 192.168.100.1 上后,分别通过 http://192.168.100.1、https://192.168.100.1 和 https://router.leovan.dev 访问路由器,如下图所示: 可以看出通过 router.leovan.dev 域名进行访问已经实现了 HTTPS 安全访问。 群晖 由于在 OpenWrt 上搞 Nginx 有些麻烦,此时此刻,恰巧手里还有一台群晖的 NAS,恰巧群晖默认支持反向代理服务器,这一切的一切不就又双叒叕完美了。 acme.sh 稍显遗憾的是在群晖中没有像 OpenWrt 那样的工具可以直接使用,这里就只能用脚本的方式手搓部署了。首先通过命令行 SSH 登录群晖,并切换到 root 用户: sudo su cd /root 由于群晖没有 crontab,因此需要使用如下命令强制安装,根据实际情况修改命令中的电子邮箱: curl https://get.acme.sh | sh -s email=my@example.com --force 当控制台显示 Install success! 后表示安装成功。进入 /root/.acme.sh 目录,修改 account.conf 文件: cd /root/.acme.sh vi account.conf account.conf 文件示例如下,请根据上文中的内容修改 CF_Token、CF_Zone_ID 和 CF_Account_ID 配置项: export CF_Token="xxxxxxxxx" export CF_Zone_ID="xxxxxxxxx" export CF_Account_ID="xxxxxxxxx" LOG_FILE="/root/.acme.sh/acme.sh.log" LOG_LEVEL=1 AUTO_UPGRADE="1" ACCOUNT_EMAIL="my@example.com" UPGRADE_HASH="xxxxxxxxx" 运行如下命令申请证书: ./acme.sh --set-default-ca --server letsencrypt ./acme.sh --issue --dns dns_cf --keylength ec-256 -d leovan.dev -d *.leovan.dev 正常情况下,申请的证书将保存在 /root/.acme.sh/leovan.dev_ecc 目录下。 运行如下命令将证书部署到群晖系统中: export SYNO_USE_TEMP_ADMIN=1 ./acme.sh --deploy --deploy-hook synology_dsm -d leovan.dev -d *.leovan.dev 此时进入群晖的 控制面板 - 安全性 - 证书 中,可以看到 leovan.dev 证书已经部署到系统中并作为默认证书,如下图所示: 在群晖中创建计划任务来实现自动更新并部署证书,在 控制面板 - 计划任务 中选择 新建 - 计划的任务 - 用户自定的脚本。在 常规 中设置 任务名称,选择 用户账号 为 root,如下图所示: 在 计划 中设置执行的周期,由于 acme.sh 在证书到期前一个月会发起重新申请,因此可以将计划任务周期设置为每周,如下图所示: 在 任务设置 中设置 用户自定义的脚本: /root/.acme.sh/acme.sh --cron --home /root/.acme.sh 根据个人需要可以勾选 通过电子邮件发送运行详情,如下图所示: 反向代理 在 Cloudflare 中将 nas.leovan.dev 解析到 192.168.100.10 上后,在群晖的 登录门户 - 高级 单击 反向代理服务器 按钮打开对话框,单击 新增 按钮,根据下图添加配置: 分别通过 http://192.168.100.10:500 和 https://nas.leovan.dev 访问群晖,如下图所示: 可以看出通过 nas.leovan.dev 域名进行访问已经实现了 HTTPS 安全访问。 提示 针对局域网其它机器上的 Web 服务,可以先将域名解析到群晖的 IP 上,再利用群晖的反向代理转发到对应机器的 Web 服务上。对于非 Web 服务,将域名直接解析到对应的机器上即可。

2025/5/25
articleCard.readMore

凡人歌

最近在没有快进的情况下看完了一部剧《凡人歌》,你说有多好看吗,也不是,只是共鸣多些,剧中的种种都好似在“点”我。这一度让我陷入了第二次职业焦虑,但比第一次职业焦虑(大概 5 年前)会显得没那么严重,没那么慌张。 悲观与乐观 我是一个做事比较喜欢先考虑最坏情况的人,或者说是一个对风险敏感的保守型人,我理解这算是一种悲观。但我也乐观,因为我认为只要努力总还是能够进步的,不是对自己的能力由多自信,只是感觉在合适的地方总会有一席用武之地。说好听了这可能是乐观,说不好听了这可能是自我感觉良好,甚至有种逆风而上的狂妄自大。不过实话实说狂妄应该不至于,年纪大了,各种情绪都会收敛很多,会觉得容忍比自由更重要。为什么聊到悲观和乐观,因为看下来这可能是引起我第二次职业焦虑的根因:对宏观(不够悲观)和微观(过于乐观)的认知偏差。 大学学了七年的管理,两门经济学基础课宏观经济学考的算是所有科目中最烂的,微观经济学到着实不赖。宏观经济学确实会比微观经济学难一些,毕竟有一只看不见的手老在调控这儿调控那儿的 。后疫情时代已经有三年左右,经济确实恢复的不理想,这点大家都有目共睹。但可能我仍是一叶障目,背后到底有多少问题可能也只是片面了解。看了付鹏在凤凰湾区财经论坛上的演讲,让我这个没啥宏观认知的人也能清晰地直面市场现状。 还有就是近期在招人,骚扰了很多有段时间(短则个把月,长则一两年)没太联系的前同事。聊下来,感觉能维持现状就算不错的了,还有些是在走下坡路。或许在当前的职位供需下,对大部分人来说可能最好的就是保持不变,对于发生变化的也只有降低个人预期才不至于出现空档。 剧中与剧外 写本文前也简单看了看豆瓣上的剧评,里面有不少提到这里面哪一个都不算是“凡人”。没有错,但剧的背景在北京,再加上编剧可能也会有一些夸张的描述,所以同更多人理解的“凡人”有很大差距,但对于我们这些在一线城市打拼的人来看,距离就没有那么远了。至少就我而言,剧中每个人的好我可能粘不上,但每个人的烦我却都似曾相识。 那伟从买了 50 万的宝马,到被裁员,到最后用宝马换了哈弗是第一个触动我的点。因为我最近也再考虑买车,想买一辆越野车,所以价格上也不算便宜。在我看来,虽然车是一个消费品,但他能够带来的快乐会高于我付出的金钱,我的小摩托就是这样的。有了车至少不会被谢美蓝嫌弃沈磊用小电驴送他上班,不过北京的车牌确实是一个难受的客观因素,摇又摇不中,租又不好租。但那伟后面遭遇的一连串不幸,虽然感觉有点过了,但也确实让我会重新审视多攒些钱早点把房贷还完是不是更好的选择。 那隽是我羡慕的对象,学历好、工作好、房子好、女朋友好。但这些在他女朋友看来可能都不算好,但我很能理解那隽,因为这些会让他感觉很安全。剧中给到介绍那伟和那隽小时候家庭条件一般,那隽通过自己的努力走到这样一个位置,已经算是相当的成功了。最后没能和李晓悦走到一起倒不是工作和生活的平衡的问题,反到他身体的问题是,我认为他俩的问题在于人生观的差异过大。在我换到第二份工作之后,体重涨的速度应该超过了我挣钱的速度,Work Life Balance 这个道理谁人不懂呢,但又有哪些人能做到呢。之前会开玩笑的说:“若不是生活所迫,谁愿意把自己搞得一身才华!”,但殊不知这一切还不都是为了碎银几两。 那隽在那伟失业的时候讽刺他没有核心竞争力,看似狂妄却也实在,我感觉也也是他在发现后浪越来越优秀的时候对自己的质问。上面提到我的这次的职业焦虑没那么严重和慌张,就是当我在问自己“你的核心竞争力是什么?”时,我能够更快的发现问题并尽可能快的去改变。上周在和 HR 同学聊时,一个观点也给我带来了一些启发,他说核心竞争力应该是一个组合,上面说的我去改变具体一点是想在技术方面多下功夫,但多下功夫了就真的有核心竞争力了吗?我们总在谈短板和长板,那么核心竞争力到底是短板不那么短,还是长板要足够长?或许都不是,核心竞争力应该是你真的去思考这个问题了,然后也付诸行动去改变了,不奏效大不了再换一个思路去改变。 沈磊被谢美蓝诟病最大的就是在他母亲生命最后的阶段他没有去麻烦别人,看起来是他的理性在驱使,但如果事情发生在他自己的父母身上呢,这里不聊谁对谁错,选择是困难的,尤其是在不得不选的时候。不喜欢麻烦别人这个事我也是这样,我甚至把能不麻烦就不麻烦别人作为我的人生信条。但我现在有了些许改变,有些人值得去麻烦,因为我知道他不图有一天可以麻烦回我。我称这样的人为知己,人生有知己,三两足矣。 牛马一生 终其一生,不为牛马,能做到的就真的不是凡人了,大多数人可能是牛马一生。当下流行的说法是“牛马三件套”:房贷、车贷、传宗接代,好多鸡汤都在说让大家活出自我,其实没必要把两者放在对立面。牛马一生可能才是常态,每个人都有欲望,或大或小,但终会有两难全的事情,所以自我和解会更可取些。 每个人的情况各有不同,每个时段的情况也各有不同,审时度势,不把自己玩死,才会有追求更好的机会。所以在一个时间点大胆地假设小心地求证,事后不要以“都是为了什么或谁”的理由去懊悔,别绑架他人也别绑架自己,向前看就好。凡人,但不要烦心。 window.meting_api = "https:\/\/api.obdo.cc\/meting\/?server=:server\u0026type=:type\u0026id=:id\u0026auth=:auth\u0026r=:r";

2024/9/22
articleCard.readMore

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

在 Linux 中调用一个脚本有多种方式,例如 fork,exec 和 source。其中 fork 为 Linux 系统调用,exec 和 source 均为 bash 内部命令。下面以 parent.sh 和 child.sh 两个脚本演示不同调用方式的区别。 parent.sh 内容如下: #!/bin/bash echo "--------------------------------------------------" echo "Before calling child.sh" echo "--------------------------------------------------" echo "PID for parent.sh: $$" var="parent" export var echo "In parent.sh, set var=$var" echo "In parent.sh, variable var=$var" echo "--------------------------------------------------" case $1 in exec) echo "Call child.sh using exec" exec ./child.sh ;; source) echo "Call child.sh using source" source ./child.sh ;; *) echo "Call child.sh using fork" ./child.sh ;; esac echo "After calling child.sh" echo "--------------------------------------------------" echo "PID for parent.sh: $$" echo "In parent.sh, variable var=$var" echo "--------------------------------------------------" child.sh 内容如下: #!/bin/bash echo "--------------------------------------------------" echo "PID for child.sh: $$" echo "In child.sh, variable var=$var from parent.sh" var="child" export var echo "In child.sh, set var=$var" echo "In child.sh, variable var=$var" echo "--------------------------------------------------" 为了确保脚本可执行,需为其添加执行权限: chmod +x parent.sh child.sh fork fork 通过进程复制来创建一个新进程,新进程称为子进程,当前进程称为父进程。在 fork 之后,子进程拥有父进程的副本,但两者的 PID 不同,同时子进程也拥有父进程的所有属性,例如:环境变量、打开的文件描述符等。 通过 fork 调用是最普遍的方式。在当前终端中通过 ./run.sh 执行时,终端会新建一个子 shell 执行 run.sh,子 shell 执行时,父 shell 仍在运行,当子 shell 运行完毕后会返回父 shell。 运行如下命令进行 fork 方式调用测试: ./parent.sh fork 测试结果如下: -------------------------------------------------- Before calling child.sh -------------------------------------------------- PID for parent.sh: 7149 In parent.sh, set var=parent In parent.sh, variable var=parent -------------------------------------------------- Call child.sh using fork -------------------------------------------------- PID for child.sh: 7150 In child.sh, variable var=parent from parent.sh In child.sh, set var=child In child.sh, variable var=child -------------------------------------------------- After calling child.sh -------------------------------------------------- PID for parent.sh: 7149 In parent.sh, variable var=parent -------------------------------------------------- exec exec 与 fork 不同,其不需要开启一个新的 shell 执行子脚本。使用 exec 执行一个新脚本后,父脚本中 exec 后的内容将不再执行。 运行如下命令进行 exec 方式调用测试: ./parent.sh exec 测试结果如下: -------------------------------------------------- Before calling child.sh -------------------------------------------------- PID for parent.sh: 9629 In parent.sh, set var=parent In parent.sh, variable var=parent -------------------------------------------------- Call child.sh using exec -------------------------------------------------- PID for child.sh: 9629 In child.sh, variable var=parent from parent.sh In child.sh, set var=child In child.sh, variable var=child -------------------------------------------------- source source 同 exec 类似,也不需要开启一个新的 shell 执行子脚本。使用 source 执行一个新脚本后,父脚本中 source 后的内容可以继续执行。 运行如下命令进行 source 方式调用测试: ./parent.sh source 测试结果如下: -------------------------------------------------- Before calling child.sh -------------------------------------------------- PID for parent.sh: 10274 In parent.sh, set var=parent In parent.sh, variable var=parent -------------------------------------------------- Call child.sh using source -------------------------------------------------- PID for child.sh: 10274 In child.sh, variable var=parent from parent.sh In child.sh, set var=child In child.sh, variable var=child -------------------------------------------------- After calling child.sh -------------------------------------------------- PID for parent.sh: 10274 In parent.sh, variable var=child --------------------------------------------------

2024/5/18
articleCard.readMore

重定向和管道 (Redirect and Pipe)

输入输出文件描述符 在 Linux 启动后,init 进程会创建 3 个特殊的文件描述符分配给输入输出。 文件描述符 英文描述 中文描述 0 stdin 标准输入 1 stdout 标准输出 2 stderr 标准错误 默认情况下,程序经由标准输入(stdin)从键盘读取数据,并将标准输出(stdout)和标准错误(stderr)显示在屏幕上。 在 Linux 中,init 是所有进程的父进程,所有子进程均会继承父进程的文件描述符。因此在 Linux 中执行的所有程序都可以从 stdin 获取输入,并将结果打印到 stdout 中,同时将错误信息打印到 stderr 中。 重定向 当我们不希望从键盘获取标准输入或将标准输出和标准错误显示在屏幕上时,则需要采用重定向。 输出重定向 输出重定向的使用方式如下: cmd [1-n]> [文件/文件描述符/设备等] 假设当前目录下存在一个名为 yes.txt 的文件,且不存在名为 no.txt 的文件。执行如下命令: ls yes.txt no.txt 由于 yes.txt 存在,这部分结果将输出到 stdout,同时由于 no.txt 不存在,这部分结果将输出到 stderr。命令的输出结果为: ls: cannot access 'no.txt': No such file or directory yes.txt 执行如下命令: ls yes.txt no.txt 1> success.log 2> fail.log 此时屏幕上将不再显示任何信息,当前目录下会生成 success.log 和 fail.log 两个文件。其中 1> success.log 表示将 stdout 重定向至 success.log,2> fail.log 表示将 stderr 重定向至 fail.log。因此 success.log 中的内容为 yes.txt,fail.log 中的内容为 ls: cannot access 'no.txt': No such file or directory。 重定向过程中,stdout 的文件描述符 1 可以省略,但 stderr 的文件描述符 2 不可以省略。因此,当只重定向 stdout 时,可简写为: ls yes.txt no.txt > success.log 此时屏幕上依旧会显示 stderr 的内容 ls: cannot access 'no.txt': No such file or directory,而 stdout 的内容则被重定向至 success.log 文件中。 在 Linux 中 &- 和 /dev/null 是两个特殊的输出设备,均表示为空,输出到该设备相当于抛弃输出。因此如下两行命令分别会抛弃 stdout 和 stderr 的内容: ls yes.txt no.txt 1>&- ls yes.txt no.txt 2> /dev/null & 可以表示当前进程中已经存在的描述符,&1 表示 stdout,&2 表示 stderr。因此我们可以将 stdout 和 stderr 重定向到相同文件: ls yes.txt no.txt > out.log 2> out.log ls yes.txt no.txt > out.log 2>&1 在上述两种方式中,第一种会导致 out.log 文件被打开两次,stdout 和 stderr 内容会相互覆盖。第二种由于 stderr 重定向给了 stdout,stdout 重定向给了 out.log,因此 out.log 仅被打开了一次。 使用 > 进行输出重定向时会先判断文件是否存在,如果存在会先删除再创建,不存在则直接创建,无论命令是否执行成功均会创建。使用 >> 进行重定向时,如果文件存在则会以添加方式打开,不存在则直接创建。 输入重定向 输入重定向的使用方式如下: cmd [1-n]< [文件/文件描述符/设备等] 例如: cat > out.txt < in.txt 此时命令将从 in.txt 文件中获取输入而非 stdin,并将结果重定向到 out.txt 文件中。 Here Document Here Document 是一种特殊的重定向方式,可以用来将多行输入传递给命令,使用方式如下: cmd << delimiter ... delimiter 这会将中间的内容 ... 传递给命令。需要注意结尾处的 delimiter 的前后均不能包含任何字符,起始处的 delimiter 的前后空白字符将被忽略。最为常用的 delimiter 为 EOF,但这不是必须的,例如: wc -l << SOMETHING 第一行 第二行 第三行 SOMETHING 上述命令的输出结果为: 3 管道 管道 | 可以将一个命令的 stdout 作为下一个命令的 stdin 使用,但无法对 stderr 进行处理。因此管道也可以理解为重定向的一种特殊形式。 假设存在一个如下内容的 test.txt 文档: Here is a test in line 1. Here is another test in line 2. Here is something else in line 3. 利用如下命令可以过滤出包含 test 字符的行并显示行号: cat test.txt | grep -n "test" 上述命令的输出结果为: 1:Here is a test in line 1. 2:Here is another test in line 2. 如果希望同时将 stdout 和 stderr 重定向到下一个命令的 stdin,可以采用如下方式: ls yes.txt no.txt 2>&1 | grep "No such file or directory" 上述命令的输出结果为: ls: no.txt: No such file or directory 上述命令也可以简写为: ls yes.txt no.txt |& grep "No such file or directory"

2024/5/12
articleCard.readMore

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

随着深度神经网络模型的复杂度越来越高,除了训练阶段需要大量算力外,模型推理阶段也较多的资源。在深度学习落地应用中,受部署环境的影响,尤其是在边缘计算场景中,有限的计算资源成为了复杂模型的应用壁垒。 复杂模型的部署问题突出表现在三个方面,如下图所示: 速度:实时响应效率的要求,过长的响应耗时会严重影响用户体验。 存储:有限的内存空间要求,无法加载超大模型的权重从而无法使用模型。 能耗:移动场景的续航要求,大量的浮点计算导致移动设备耗电过快。 针对上述三类问题,可以从模型压缩和推理加速两个角度出发,在保持一定模型精度的情况下,让模型速度更快、体积更小、能耗更低。 模型压缩 常用的模型压缩方法有如下几种类型: 剪裁 剪裁(Pruning)的核心思想是在尽量保持模型精度不受影响的前提下减少网络的参数量,例如减少网络中连接或神经元的数量,如下图所示: 剪裁最常用的步骤如下: 训练:在整个剪裁过程中,该步骤主要为预训练过程,同时为后续的剪裁工作做准备。 修剪:通过具体的方法对网络进行剪裁,并对网络重新进行评估以确定是否符合要求。 微调:通过微调恢复由于剪裁对模型带来的性能损耗。 对网络进行剪裁的具体方法可以分为非结构化剪裁和结构化剪裁。 非结构化剪裁是细粒度的剪裁方法,一般通过设定一个阈值,高于该阈值的权重得以保留,低于该阈值的权重则被去除。非结构化剪裁虽然方法简单、模型压缩比高,但也存在诸多问题。例如:全局阈值设定未考虑不同层级的差异性,剪裁信息过多有损模型精度且无法还原,剪裁后的稀疏权重矩阵需要硬件层支持方可实现压缩和加速的效果等。 结构化剪裁是粗粒度的剪裁方法,例如对网络层、通道、滤波器等进行剪裁。在滤波器剪裁中,通过评估每个滤波器的重要性(例如:Lp 范数)确定是否保留。结构化剪裁算法相对复杂、控制精度较低,但剪裁策略更为有效且不需要硬件层的支持,可以在现有深度学习框架上直接应用。 量化 神经网络中的计算通常采用浮点数(FP32)进行计算,量化(Quantization)的基本思想是将浮点计算替换为更低比特(例如:FP16,INT8 等)的计算,从而降低模型体积加快模型推理速度。 数值的量化可以看做一个近似过程,主要可以分为两类: 定点近似:通过缩小浮点数表示中指数部分和小数部分的位宽实现。映射过程不需要额外的参数,实现相对简单,但针对较大数值的精度损失较大。 范围近似:通过统计分析,经过缩放和平移映射浮点数。映射过程需要存储额外的参数,计算时需要先反量化,计算相对复杂,但精度更高。 范围近似又可以分为线性映射和非线性映射两种。 线性映射将浮点数映射到量化空间时采用如下计算公式: $$ \begin{aligned} r &= S \left(q - Z\right) \\ q &= round \left(\dfrac{r}{S} + Z\right) \end{aligned} $$ 其中,$r, q$ 分别表示量化前和量化后的值,$S, Z$ 为量化系数。一般化的非对称映射如下图所示: 其中, $$ \begin{aligned} S &= \dfrac{r_{max} - r_{min}}{q_{max} - q_{min}} \\ Z &= q_{min} - \dfrac{r_{min}}{S} \end{aligned} $$ 非线性映射考虑了数据本身的分布情况。以分位量化方法为例,其基本思想是通过分位点对数据进行划分,使得各个区间之间的数据量相等,然后将同一个区间的数据映射为相同值,从而实现量化。 量化粒度是指控制多少个待量化的参数共享一组量化系数,通常粒度越大,精度损失越大。以 Transformer 模型为例,不同粒度的量化方式如下图所示: 其中,$d$ 为模型大小与隐层维度之比,$h$ 为多头自注意中的头数。 模型量化分为两种: 权重量化:即对网络中的权重执行量化操作。数值范围与输入无关,量化相对容易。 激活量化:即对网络中不含权重的激活类操作进行量化。输出与输入有关,需要统计数据动态范围,量化相对困难。 根据是否进行训练可以将量化方法分为两大类,如下图所示: 训练后量化(Post Traning Quantization,PTQ):方法简单高效,无需重新训练模型。根据是否量化激活又分为: 动态:仅量化权重,激活在推理阶段量化,无需校准数据。 静态:量化权重和激活,需要校准数据。 量化感知训练(Quantization Aware Traning,QAT):方法相对复杂,需要在模型中添加伪量化节点模拟量化,需要重新训练模型。 三种不同量化方法之间的差异如下图所示: 神经结构搜索 神经结构搜索(Network Architecture Search,NAS)旨在以一种自动化的方式,解决高难度的复杂神经网络设计问题。根据预先定义的搜索空间,神经结构搜索算法在一个庞大的神经网络集合中评估结构性能并寻找到表现最佳的网络结构。整个架构如下图所示: 近年来基于权重共享的结构搜索方法受到广泛关注,搜索策略和性能评估高度相关,因此两者往往合为一体表示。 搜索空间包含了所有可搜索的网络结构,越大的搜索空间可以评估更多结构的性能,但不利于搜索算法的收敛。搜索空间从搜索方式角度分为两种: 全局搜索空间:通过链式、跳跃链接、分支等方式搜索整个网络结构。 基于结构单元的搜索空间:仅搜索结构单元,减少搜索代价,提高结构的可迁移性。 搜索策略即如何在搜索空间根据性能评估选择最优的网络结构。具体包含随机搜索、贝叶斯优化、进化算法、强化学习和基于梯度的方法。 性能评估最简单的方法就是对数据划分验证集,针对不同的网络结构重新训练并评估其在验证集上的表现。但这种方法所需的计算成本很高,无法在实践中落地应用。一些基于权重共享的结构搜索方法能够一定程度地加速搜索,因此考虑搜索策略和性能评估任务的相关性,在当前架构中往往将这两部分统一表述为搜索策略。 知识蒸馏 知识蒸馏(Knowledge Distillation,KD)是一种教师-学生(Teacher-Student)训练结构,通常是已训练好的教师模型提供知识,学生模型通过蒸馏训练来获取教师的知识。它能够以轻微的性能损失为代价将复杂教师模型的知识迁移到简单的学生模型中。 知识蒸馏架构如下图所示: 上半部分为教师模型,下半部分为学生(蒸馏)模型。将教师模型的输出作为软标签与学生模型的软预测计算蒸馏损失,将真实的硬标签与学生模型的硬预测计算学生损失,最终将两种损失结合训练学生模型。 论文 1 给出了软标签的计算公式: $$ q_i = \dfrac{\exp \left(z_i / T\right)}{\sum_j \exp \left(z_j / T\right)} $$ 其中,$T$ 为温度系数,用来控制输出概率的软化程度。不难看出当 $T = 1$ 时,公式的输出即为网络输出 $Softmax$ 的类概率。$T$ 越大,$Softmax$ 的类概率分布越平滑,这可以让学生模型学习到教师模型对负标签的归纳信息。 参考 可供使用的模型压缩库有: PyTorch:剪裁,量化,神经网络搜索,知识蒸馏 TensorFlow:剪裁,量化 PaddleSlim:支持剪裁、量化、神经网络搜索、知识蒸馏,适配 PaddlePaddle 框架。 PocketFlow:支持剪裁、量化,适配 TensorFlow 框架。 NNI:支持剪裁、量化、神经网络搜索,适配 TensorFlow、PyTorch 等框架。 TinyNeuralNetwork:支持剪裁、量化,适配 PyTorch 框架。 DeepSpeed:支持剪裁、量化,适配 PyTorch 框架。 Intel Neural Compressor:支持剪裁、量化、知识蒸馏,适配 TensorFlow、PyTorch、ONNX 等框架。 Neural Network Compression Framework:支持剪裁、量化,适配 TensorFlow、PyTorch、ONNX、OpenVINO 等框架。 推理加速 硬件加速 硬件加速是指将计算交由专门的硬件以获得更快的速度。在深度学习领域最简单的体现就是利用 GPU 进行推理会比利用 CPU 更快。除此之外,在给定的硬件环境中,利用针对性优化的推理框架可以更充分的利用硬件特性提升预测效率。 并行计算 并行计算是指将计算的过程分解成小部分,以并发方式运行实现计算效率的提升。在模型训练和推理阶段,主流的并行方式有: 数据并行(Data Parallel):将数据集切分为多份,每个设备负责其中一部分。 流水线并行(Pipeline Parallel):将模型纵向拆分,每个设备只包含模型的部分层,数据在一个设备完成计算后传递给下一个设备。 张量并行(Tensor Parallel):将模型横向拆分,将模型的每一层拆分至不同设备,每一层计算都需要多个设备合作完成。 参考 可供使用的推理加速库有: 库 平台 CPU GPU & NPU 框架 系统 TensorRT 服务端 不支持 CUDA TensorFlow PyTorch ONNX 等 Windows Linux 等 Triton 服务端 x86 ARM CUDA TensorFlow PyTorch ONNX 等 Windows Linux 等 OpenVINO 服务端 Intel ARM OpenCL TensorFlow PyTorch PaddlePaddle ONNX 等 Windows Linux macOS 等 Paddle Inference 服务端 x86 ARM CUDA PaddlePaddle Windows Linux macOS 等 MNN 服务端 移动端 x86 ARM CUDA OpenCL Vulkan Metal HiAI CoreML TensorFlow ONNX 等 Windows Linux macOS Android iOS 等 TNN 服务端 移动端 x86 ARM CUDA OpenCL Metal HiAI CoreML TensorFlow PyTorch ONNX 等 Windows Linux macOS Android iOS 等 Tensorflow Lite 移动端 ARM OpenCL Metal NNAPI Core ML Tensorflow Android iOS 等 PyTorch Mobile 移动端 ARM Vulkan Metal NNAPI PyTorch Android iOS 等 Paddle Lite 移动端 x86 ARM OpenCL Metal NNAPI PaddlePaddle Android iOS 等 ncnn 移动端 x86 ARM Vulkan TensorFlow PyTorch ONNX 等 Android iOS 等 本文未对模型压缩和推理加速进行深入展开,仅作为工业实践的基础概念解释。本文参考了大量前人之作,在此一并引用 2 3 4 5 6 7。 Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. arXiv preprint arXiv:1503.02531. ↩︎ 模型压缩概述:https://paddlepedia.readthedocs.io/en/latest/tutorials/model_compress/model_compress.html ↩︎ Large Transformer Model Inference Optimization:https://lilianweng.github.io/posts/2023-01-10-inference-optimization/ ↩︎ 深度学习模型压缩方法:剪枝:https://zhuanlan.zhihu.com/p/609126518 ↩︎ 模型量化原理与实践:https://robot9.me/ai-model-quantization-principles-practice/ ↩︎ 李航宇, 王楠楠, 朱明瑞, 杨曦, & 高新波. (2021). 神经结构搜索的研究进展综述. 软件学报, 33(1), 129-149. ↩︎ 黄震华, 杨顺志, 林威, 倪娟, 孙圣力, 陈运文, & 汤庸. (2022). 知识蒸馏研究综述. 计算机学报, 45(3). ↩︎

2024/4/14
articleCard.readMore

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

编程语言「只是」达成目标的工具,这是我一直推崇的说法,因为我认为达成目标更重要的在于个人思考,编程语言不过是个「工具」,选哪个并没有那么重要。现在我越来越认为这个「工具」的选择还是很重要的,因为编程语言之于目标实现的可能性和效率都制约了目标的最终达成。 以个人数据科学的工作背景,结合我的编程语言学习路径,尝试回答一下我们需要多少种编程语言这个问题。我的编程语言学习路径大致如下: 小学时代:Logo 初高中时代:Basic,HTML,CSS 本科时代:C,C++ 研究生时代:Matlab,R,Python,SQL 工作时代:Markdown,Java,JavaScript,TypeScript,Rust 语言分类 在我的编程语言学习路径中,各个编程语言之间还是存在很大差异的,以最早接触的 Logo 为例,其除了教学目的之外似乎就真的没有什么大用途了。这些编程语言可以大致划分为如下三种类型: 通用编程语言(General-purpose Programming Language,GPL):Basic,C,C++,Python,JavaScript,TypeScript,Java,Rust 领域特定语言(Domain-Specific Language,DSL):Logo,CSS,Matlab,R,SQL 标记语言(Markup Language):HTML,Markdown 领域特定语言和标记语言就不用多说了,其应用范围有限。通用编程语言虽然定义为通用,但各自也有擅长和不擅长的领域,需要针对实际场景进行选择。 领域偏好 领域偏好是指你所从事的领域内大家对编程语言的使用偏好,这对于语言的选择至关重要,因为靠一己之力改变领域内大多数人的选择还是相当有难度的,从众可以极大地充分利用前人的成果降低工作的成本。 数据科学 数据科学可选的编程语言有 Python、R 和 Matlab 等,从技多不压身的角度出发肯定是掌握的越多越好,但精力终归是有限的。Matlab 在版权、仿真等方面具有一定的特殊性,Python 和 R 在数据科学中的竞争可谓是旷日持久,相关对比也数不胜数 1 2 3。 如果非要二选一我现在会选择 Python,因为其作为通用编程语言在将数据科学和工程代码结合时会展现出更多的优势。但在一些特定领域,例如生物信息学,R 的采用率会更高。一些新起之秀例如 Julia 和 Mojo 仍需要进一步观测其发展,过早地大面积使用新语言可能会面临各种风险。 后端 企业级后端应用中 Java 应该是首选,对一些高性能场景 C/C++、Rust 可能更加适合。 在实际工作中,Python 依旧可以作为一个不错的后端语言选择。Python 在 HTTP 和 RPC 接口、高并发、开源组件 SDK 支持等方面都不错的表现。Python 作为一种解释型语言,时常被诟病运行慢,这确实是解释型语言的一个问题。不过现在很多流行扩展包都是基于 C/C++ 构建,并且从 3.13 版本开始已经可选去除全局解释锁,这些都会使 Python 的性能变得越来越好。个人认为 Python 运行慢的另一个原因是使用者对其理解仍不够深入,使用的技巧仍有待提升,《流畅的 Python》可以让你对 Python 有更深入的认识。 前端 前端是程序与用户进行交互的必经之路,HTML,CSS 和 JavaScript 可以算得上前端三剑客了,分别负责元素的定义、样式和交互。随着 Node.js 的发展,JavaScript 也可以作为后端语言使用。TypeScript 作为 JavaScript 的超集,扩展了 JavaScript 的功能和特性,同时随着 React 和 Vue 框架的出现,前端的发展可谓是盛况空前。 Python 此时就真的很难插入一脚了,不过话无绝对,基于 WebAssembly 技术 Python 也可以在前端运行。WebAssembly 设计的目的是为了提升前端代码的运行效率,而在这方面从实践上 4 来看 Rust 更受青睐。 客户端 在 Apple 和 Google 两大移动阵营中,iOS(iPadOS 和 tvOS)系统的首选语言为 Swift,Android 系统的首选语言为 Kotlin。在 Apple 和 Microsoft 两大桌面阵营中,macOS 系统的首选语言为 Swift,Windows 系统的首选语言为 .Net。原生语言可以让应用更好的适配对应的系统,但引入的问题就是相同应用针对不同系统适配的成本增加。针对大公司的核心应用确实有这个必要,但是普通场景,「跨平台」则会更吸引人。 在之前的博客中有对桌面端的跨平台框架作简要分析,但结合当下移动端的市场占比,基于前端技术的跨平台解决方案会是一个不错的选择。 脚本 脚本可以说就只是程序员自己为了方便而产生的需求,不出意外它应该只会出现在命令行的黑框框中。在类 Unix 系统中,Shell 是一个通用且不需要额外安装扩展的不错之选。除此之外,什么 Python、Perl、Ruby、Lua 都在不同的场景中发光发热,相信我们将处于并将长期处于脚本语言的五代十国中。 嵌入式 早期的嵌入式我认为是一个相对专业垂直的领域,由于操作的对象更加底层,所以使用的语言也会更底层一些,例如 C/C++。随着硬件的不断发展,开发板、智能设备、机器人都在逐步走入更多程序员的视野,硬件性能的提升也使得 Python 等高级语言可以作为嵌入式开发的工具。 个人偏好 领域偏好是站在客观的角度指导我们如何选择语言,但谁还没有些小脾气呢?以自己为例,最早接触的数据分析语言是 Matlab,我的本科和研究生的论文都是用 Matlab 完成的,当时是由于实验室都在用它(领域偏好),但我个人并不很喜欢它。虽然上面我也承认在 Python 和 R 的大战中,如果非让我选一个我会选择 Python,但这也剥夺不了我对 R 的钟爱。R 的管道符 |> 用起来就是舒服,虽然 Python 第三方也提供了类似的扩展包 siuba,但模仿终归还是模仿。 在一定程度上坚持自己的个性还是可能会有些益处的。仍以自己为例,钟爱 R 的我在了解到 RMarkdown 之后让我喜欢上了可重复性研究(更多细节可参见之前的博客)。再到之后的 Quarto,最终我将这些内容融入工作之中,开发工作中适用的产品。可以说如果仅是基于领域偏好彻底拥抱 Python 而抛弃 R,那么我会错过不少优秀的项目和工具。还有不得不提的 ggplot2,图形语法的最好践行者,Python 中 Matlab 的遗留瑰宝 matplotlib 我只能说很强大但我不喜欢用,至于 Python 第三方提供的类似扩展包 plotnine,模仿终归还是模仿。 真实需要 聊了这么多,到底我们需要多少种编程语言?这取决于你到底想要啥? 如果想作为一个安分守己的数据科学工作者,Python 能撑起你 90% 的需求,多花一些时间去了解业务可能比你多学一门编程语言的收益要大得多。 如果你不够安分,不想深藏于后端,甚至有技术变现的想法,那么前端交互自然不可缺失。此时 JavaScript/TypeScript 应该是个不错的选择,毕竟你不是要成为一个专业的前端或 APP 开发人员,跨平台才应该是我们应该偏好的重点,毕竟一套代码处处可用,变现的速度就可以杠杠的了。 当然我相信除了眼前的这些苟且,大家还是有更远大抱负的,那么在这条布满荆棘的路上你会遇到更复杂的问题。性能的提升我会选择 Rust,毕竟人家除了能提升后端还能提升前端,何乐而不为呢? 综上所述,作为数据科学工作的从业者,我将以并将长期以如下四句作为我编程语言选择的重要指导方针: 人生苦短,我用 Python。 JavaScript / TypeScript。 R。 Rust。 https://www.jiqizhixin.com/articles/2018-06-30-2 ↩︎ https://www.datacamp.com/blog/python-vs-r-for-data-science-whats-the-difference ↩︎ https://www.geeksforgeeks.org/r-vs-python/ ↩︎ https://github.com/antvis/layout ↩︎

2024/2/9
articleCard.readMore

数据可视化小贴士

文本主要面向不同格式文档(HTML、PDF、Word)的动态生成,秉承规范、统一和实用的理念总结数据可视化过程中的相关问题,不过度涉及数据可视化本身细节。 一画胜千言(A picture is worth a thousand words)是我个人很推崇的一个指引,不过前提是这得是一张「好图」,否则容易过犹不及。 数据可视化是一门复杂的学问,在动态文档生成中,秉承规范、统一和实用的理念,我认为是快速提高数据可视化质量的不错之选。 设计规范 在之前的文章「设计语言初探」中探讨过产品的设计语言,在此针对各大企业的数据可视化规范并结合中文文档生成和个人偏好做简要分析。各大企业的数据可视化规范如下: 企业 数据可视化规范 Apple Human Interface Guidelines - Charts Google Material Design - Data Visualization Microsoft Data visualization style guidelines for Office Add-ins Adobe Spectrum - Data Visualization IBM Carbon Design System - Data Visualization Salesforce Lightning Design System - Data Visualization 蚂蚁金服 Ant Vision 各家的设计理念有所不同,但我相信其目标是一致的,就是让用户可以更好更快地理解数据并从数据中获取洞见。上面的大多数数据可视化规范依旧是以面向产品设计为主,不过我认为大部分理念是可以迁移到文档中的可视化,尤其是 HTML 格式的动态文档。 Apple、Google、Microsoft 三大家的规范在自家系统的平台上针对简单的可视化场景可以说是最适用的,毕竟原生设计毫无违和感。但针对复杂的可视化场景,三家并没有给出更细的指引,不过也能理解针对商业数据可视化和科技绘图等复杂场景,确实更适合由上层(例如:库、应用等)去根据实际情况作出相应规范。 所以,站在规范和统一的视角,我个人更倾向于选择适合中国宝宝体质的 Ant Vision。给出些我认为靠谱的理由: 设计体系基于具有更悠久历史的 Ant Design 衍生,具有完善的设计规范指引。 针对简单场景(例如:统计图表等)和复杂场景(例如:地图、关系图表等)都有较好的覆盖。 科学的色彩体系,在萝卜青菜各有所爱的配色之上给到了科学的指引。 最后也是我认为最重要的特点,开源,且有丰富的中文文档。 从规范和统一的角度,可以说 Ant Vision 是最优选择,有关 Ant Vision 的更多资料除了官网以外,还可以参见语雀上的 AntV 文档。 工具选择 由于是面向动态文档生成为主,基于各种可视化工具的绘图很难嵌入自动化流水线中,因此本节主要讨论相关扩展包,不涉及专用的可视化工具(例如:Tableau,Power BI 等)。常用的可视化扩展包及其支持的语言和图类型,如下表所示: 扩展包 JS/TS Python R 统计图 地图 关系图 Ant Vision ✅ ❌ ⛔️ 1 ✅ ✅ ✅ ECharts ✅ ☑️ 2 ☑️ 3 ✅ ✅ ✅ Plotly ✅ ✅ ✅ ✅ ✅ ✅ D3 ✅ ☑️ 4 5 ☑️ 6 ✅ ✅ ✅ Matplotlib ❌ ✅ ❌ ✅ ✅ ✅ seaborn ❌ ✅ ❌ ✅ ❌ ❌ plotnine ❌ ✅ ❌ ✅ ✅ ❌ ggplot2 ❌ ❌ ✅ ✅ ✅ ✅ 上述扩展包是我个人在实际项目中真实会使用到的,此时此刻就不难发现 Ant Vision 最大的问题就是对科学编程几乎 7 没有官方支持。 用于可视化的扩展包远不止上述的这 8 种,但我们不可能去学习使用所有的扩展包,这样于自己需要投入大量的学习成本,于团队也不利于项目的维护。即便就这 8 种扩展包,其语法也大不相同。 我接触的第一门绘图语言应该是 Logo,不过 Logo 是一门教学语言,所以在科学编程中使用的最早的是 Matlab 的绘图功能,Matplotlib 从名称上就不难看出是源自 Matlab。不过个人而言不是很喜欢 Matplotlib 的 API 风格,或者说我认为其 API 更偏底层一些。谈到这里就不得不谈一下「The Grammar of Graphics」,这也是我个人认为绘图「最舒服」的 ggplot2 扩展包背后的理论。在上述扩展包中,ggplot2 算是先驱者,plotnine 是 ggplot2 的 Python 复刻,Plotly 的 R 绑定可以支持直接将 ggplot2 对象绘制成 plotly 绘图,Ant Vision 则在该核心理论的基础上做了更多探索。所以当掌握了图形语法的理论基础后,对这些包的学习就相对会简单不少。由于长尾需求还是会存在,所以掌握不同可视化包的使用就成了技多不压身,我认为上述 8 种扩展包覆盖 99% 的可视化需求应该不成问题。 上述 8 种可视化包中,前 4 种都是以 JS/TS 库为基础,部分由官方或三方实现了 Python 和 R 的绑定,所以前 4 种天然就支持可交互绘图。利用 Python 的 Jupyter Widgets 和 R 的 htmlwidgets 实现在网页和 Notebook 中的可交互绘图。D3 背后的 Observable JS 也被当下流行的动态文档生成工具 quarto 所支持。后 4 种则只能创建不可交互的静态绘图,Matplotlib 作为 Python 绘图的重要可视化扩展包提供了丰富的绘图 API,seaborn 和 plotnine 均基于其开发,seaborn 简化了 API,plotnine 提供了 ggplot2 语法支持,而 ggplot2 则是 R 语言中数据可视化的不二之选。 讲了这么多并不是要在各个扩展包之间分个孰优孰劣,而是需要其特性和需求场景选择合适的扩展包。在此个人愚见如下: 动态绘图:Plotly > ECharts > D3 > Ant Vision,理由:多语言支持优先。 动态绘图(复杂场景,例如:地图、关系图等):Ant Vision > D3 > ECharts > Plotly,理由:交互性能优先。 静态绘图:ggplot2 = plotnine > seaborn > Matplotlib,理由:语法简单易于理解优先。 静态绘图(长尾需求,例如:示意图等)Matplotlib,理由:较为底层的绘图 API,使用更为灵活。 实用建议 根据规范,为了保证利用不同的工具绘图的视觉效果一致,我们需要在布局、色板、字体等多个角度进行自定义配置。下表展示了扩展包在不用语言绑定中样式的可自定义特性: 扩展包 JS/TS Python R Ant Vision ☑️ 详情 - - ECharts ☑️ 详情 ☑️ 详情 ☑️ 详情 Plotly ☑️ 详情 ✅ 详情 ☑️ 详情,✅ 复用 ggplot2 D3 ❌ 详情 ❌ ❌ Matplotlib - ✅ 详情 - seaborn - ✅ 复用 Matplotlib - plotnine - ✅ 详情 - ggplot2 - - ✅ 详情 其中,✅ 表示支持自定义样式,且支持修改全局默认样式;☑️ 表示支持自定义样式,不支持修改全局默认样式,但支持通过函数一次性设置自定义样式;❌ 表示支持自定义样式,但需要每次手动配置所有样式细节。 布局 在 Ant Vision 设计语言中一个图应该包含标题、轴、图形、标签、注解、提示信息、图例等信息,如下图所示: 大多数扩展包支持绝大多数元素,但对于一些相对特殊的元素(例如:注解,尤其是富文本的注解)支持较为有限。同时不同扩展包对于相同元素的样式控制也存在差异,这就导致很难将在不同扩展包之间做到完全统一,只能是尽可能相似。 色板 色板(配色)是影响统一的另一大重要因素,相比布局其更好做到不同扩展包之间的统一。根据 Ant Vision 的设计语言,色板分为:分类、顺序、发散、叠加、强调、语义共 6 大色板,如下图所示: 不同扩展包不一定能覆盖所有类型色板(视其绘图能力而定)。除了选择合适的统一色板之外,不同类型色板在使用时也有各自的注意事项,虽然这无关样式统一,但却会从很大程度上影响数据可视化的效果。 字体 根据 Ant Vision 的设计语言,数据可视化字体应当具备三个条件:数字等宽、识别度高、混排美观,如下图所示: 针对文档生成,个人总结了能够覆盖大部分文档场景且商用免费的字体,如下表所示: 字体名称 字体分类 语言 版权 建议使用场景 思源黑体 无衬线黑体 中英文 SIL开源,商用免费 网页正文 思源宋体 衬线宋体 中英文 SIL开源,商用免费 PDF&Word正文 方正仿宋 仿宋 中英文 商用免费 公文 方正楷体 楷体 中英文 商用免费 注释 ETbb 衬线 英文 MIT开源,商用免费 PDF&Word正文 Latin Modern Math TeX数学字体 英文 GFL开源,商用免费 数学公式 更纱黑体 等宽黑体 中英文 SIL开源,商用免费 代码 为了满足 Ant Vision 对字体的要求,在 Word 和 PDF 格式文档中的静态绘图可以使用更纱黑体。针对 HTML 格式文档,由于不会内嵌字体,为了适配不同的操作系统同时考虑不同字体的可用性,可以将交互式绘图字体设置为多个值: { font-family: Iosevka, 'Iosevka Nerd', Consolas, 'Lucida Console', Menlo, Monaco, 'Andale Mono', 'Ubuntu Mono', 'Source Han Mono SC', 'Source Han Mono TC', 'Source Han Mono', 'Noto Sans Mono SC', 'Noto Sans Mono TC', 'Noto Sans Mono', monospace, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important; } 尺寸 & 响应式 错误的尺寸选择或不良的元素位置摆放都会导致可视化效果变差。PDF 和 Word 格式文档,以及 PC 端的 HTML 格式文档,通过限制页面宽度,可以相对统一的规范到标准的可读内容宽度。此时图片的最大宽度是已知的,可视化时仅需要考虑合适的宽高比即可。 针对移动端的 HTML 文档,由于可读内容宽度较小,绘图的尺寸则需要有针对性地进行调整。此时,使用交互式绘图则比静态绘图有更有优势,利用交互式绘图本身的响应式能力,可以减少人工调整的工作量,也更容易实现一套绘图代码处处可用的效果。 在动态文档生成过程中,我们很难预估数据的真实场景,一些极端情况往往可能会导致可视化结果完全不可用。因此在编写绘图代码时应充分考虑到数据类别的数量、数据类别名称的长度、数据分布等多种因素,同时将绘图中的不同元素摆放在合适的位置。编写出适应性更好的绘图代码才能保证动态文档的高可用性,当然这个过程不是一蹴而就的,随着真实数据中极端情况的积累,绘图代码也会越来越完善越来越鲁棒。 由第三方 g2r 提供 G2 部分支持,已停止更新。 ↩︎ 由第三方 pyecharts 提供支持。 ↩︎ 由第三方 echarts4r 提供支持。 ↩︎ 由第三方 d3blocks 提供支持。 ↩︎ 由第三方 d3graph 提供关系图支持。 ↩︎ 由第三方 r2d3 提供支持。 ↩︎ L7VP 提供了 Python 绑定。 ↩︎

2024/1/7
articleCard.readMore

从 rm -rf * 说起

故事要从昨晚的事故说起,在软路由中删除了一个 Docker 容器,想着相关配置和数据目录也都用不到了就删掉吧。进入目录后「聪明」的我就执行了 rm -rf *,等回头去看命令执行情况已然为时已晚。为什么说我「聪明」呢,因为自从知道一个空格引起的 /usr 被删除的血案后,在做删除动作时我都会谨慎再谨慎,然而这次的悲剧在于目录下通过 NFS 挂载了 NAS 上的远程目录,删除前忘记取消挂载了,结果就是 NAS 上 4 块盘里面的影视资料被我一键清空了。 想着十多个 TB 的影视资料就这么没了,到也没有太伤感,毕竟技术男认为总还是可以恢复的,无非就是费些时间的问题。所以做的第一件事就是把 NAS 关机了,因为一旦再写入新的数据,被删除的数据可能就真的无法恢复了。关机后就开始找 SATA 线(NAS 里面是 3.5 寸的机械硬盘,使用 SATA 口通信),发现没有就赶紧买了一根第二天可以到的,至此第一笔 60 大洋(3.5 寸的硬盘还得单独供电,好不容易找到一个便宜的带电源的套装)损失就出去了。然后就开始各种找资料,NAS 里面的硬盘格式是 Btrfs 的,可用的恢复工具一下子就少了,翻着翻着发现就已经凌晨一点。怀着一丝丝担忧还是决定先睡了,明天早起再说吧,反正 SATA 线最快也得下午才能到。 喜新 这一切的一切要往前捯就只怪我「喜新厌旧」。搬到新家利用软路由和 NAS 搭建了一套家庭影音中心,老老实实看就得了呗,非要瞎鼓捣。在 Jellyfin 中显示的影视信息读取的元信息文件有些问题,提的 PR 也是做了各种测试才成功合并到主干,尽管只是改了一行代码的位置,但维护人员的严谨还是很让我受教的。虽然合并到了主干,但由于大版本更新发布还未确定时间,当时自己就临时针对当前版本调整了代码编译部署到了自己的软路由上先用起来了。 后续稳定版本也发布了几个修复问题的小版本更新,但合并的代码并不在更新范围内,自己懒了也就没再更新 Docker 镜像。直到昨天晚上也许就是闲来无事,想着要不就更新到非稳定版本用吧,同时一不做二不休还把刮削用的 tinyMediaManager 也更新下吧,然后就没有然后了,事故就发生了。 我是一个比较喜欢尝鲜的人,每天到公司不执行一下 brew update & brew upgrade 就不舒服。而且还不能搞成定时任务,就得手动执行,然后看着相关工具更新到最新版本就会很舒服。我承认在一些「大型」项目中,兼容和稳定才是第一追求,但我认为我还真么参与到过那种「大」到处处都要为兼容和稳定考虑的项目中过,所以我还是很喜欢尝试新的特性。当然工作中尝鲜用到的也得至少是正式发布的版本。最近的一次大迁移就是把我所有的 Python 项目都从 3.7 升级到了 3.10,为啥不是 3.11 呢,因为有些依赖包没我走得快,对 3.11 还不支持。我大部分是在做数据和算法工作,少部分时间也会用 Python 写一些工程性质的代码,在这个领域我认为「喜新」是一件好事,这会让你在面对一个问题时更有可能说出「我行」。 怀旧 我其实还挺两面派的,刚刚还在「喜新厌旧」,现在又说「怀旧」。老人经常说人要有个「念想」,但此时此刻我发现我的「念想」有点儿多了,多到可能束缚到了我的前行。NAS 是我大概 6 年前买的了,去年淘汰了里面两块老旧的小硬盘,又补上了两块 8TB 的。里面存的都是从 PT 站下载的高清影视资料,今年也终于成功的将 PT 站的账号升到了永久保号的等级,看了看上传量也近 20TB 了。文件被删掉的那一刻都是在想怎么恢复,但当发现恢复有一定的难度后,我突然有在想要不要恢复。 其实这么多年下载了这么多高清的影视资料,真正回过头再去看的次数不算多,一是没那么多时间,二是有新的内容可以选择。所以这么多「怀旧」的东西可能并不是「念想」,而只是满足「占有欲」的电子榨菜。这就和买书与读书是一回事儿,不是买了书有了书,知识就是你的了。多年前的我认识到了这个问题,但好像改进的并不理想,尤其是疫情这几年,身体没空去旅行,然而思想也没有在路上。不过感觉近期有所改善,至少近几个月快读完三本书了。 中午眯了会儿醒来还是决定把四块硬盘重新格式化了,也正好把之前一个磁盘和存储空间顺序不一致的问题做了纠正,如实让我这个强迫症选手舒服了一些。后面把原来移动硬盘中的照片重新又备份了一份到 NAS 中,「念想」这东西有点儿就够了。弄完了,心情一下子轻松了不少,毕竟不用再为恢复数据发愁了,尤其是在这种你还不知道能不能恢复的情况下。唯一还有些心疼的就是花 60 大洋买的 SATA 线,看了看快递小哥已经送上货了,就留着吧,万一将来有啥用呢。 再出发 后疫情时代的第一年马上就要过去了,这一年的种种个人感觉都不尽如人意,也正是不如人意才会让我去思考更多,改变更多。变革不一定会创造机会,哪怕创造了机会能不能抓住也难说,但对变革的思考和自身的改变是可以主动发起的。抛下一些陈旧的包袱,再去想想你真正想要的,透彻一些,会发现变革不一定是坏事。 大家都在往前走,这是我们认为的,也是我们想要的,至少是我想要的。可我真的在往前走吗?或许慢慢地陷入了舒适圈,或许被过往的「念想」不知不觉牵绊住了,所以不经意的一个变数,或好或坏,都给了我们重新审视自我和世界的机会。停下来,思考片刻,再出发,来得及,也值得。

2023/12/17
articleCard.readMore

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

色域 在当我谈修图时,我谈些什么 - 色彩篇 Part 1 中已经介绍过什么是色彩空间,在显示领域通常会使用 RGB 色彩模型,在印刷领域通常会使用 CMYK 色彩模型。而在颜色感知领域,CIE 1931 色彩空间 则是在设计之初便要求包含普通人眼可见的所有颜色的标准色彩空间。 人类眼睛有对于短、中和长波的感光细胞,色彩空间在描述颜色时则可以通过定义三种刺激值,再利用值的叠加表示各种颜色。在 CIE 1931 色彩空间中,这三种刺激值并不是指对短、中和长波的反应,而是一组约略对应红色、绿色和蓝色的 X、Y 和 Z 的值。X、Y 和 Z 的值并不是真的看起来是红色、绿色和蓝色,而是使用 CIE XYZ 颜色匹配函数计算而来。 在颜色匹配实验中,如下图 1 所示: 受试者通过观察单一光源的颜色和三原色光源的混合颜色是否相同,得到光谱三刺激值曲线如下图 2 左所示。为了消除负值对数据处理带来的不便,通过转换得到了三个新的值 $X$、$Y$ 和 $Z$ 的曲线如下图 2 右所示。 在 CIE 1931 色彩空间中,所有可视颜色的完整绘图是三维的,$Y$ 可以表示颜色的明度 3。$Y$ 表示明度的好处是在给定 $Y$ 值时,XZ 平面将包含此明度下的所有色度。通过规范化 $X$、$Y$ 和 $Z$ 的值: $$ \begin{aligned} x &= \dfrac{X}{X + Y + Z} \\ y &= \dfrac{Y}{X + Y + Z} \\ z &= \dfrac{Z}{X + Y + Z} = 1 - x - y \end{aligned} $$ 色度可以使用 $x$ 和 $y$ 来表示。CIE 1931 的相对色度图 4 如下所示: 外侧曲线边界是光谱轨迹,波长用纳米标记。不同色域(Color Gamut)标准之间的对比如下图 5 所示: 对于一个显示设备来说,不可能产生超过其色域的颜色。通常情况下,讨论一台摄影设备的色域并没有意义,但使用什么样的色彩空间进行编码则需要重点关注。 色彩深度 色彩深度,简称色深(Color Depth),即存储一个像素的颜色所需要的位数。若色彩深度为 $n$ 位,则代表一共包含 $2^n$ 种颜色。例如我们常说的真彩色,即 24 位,对应 RGB 三个通道,每个通道 8 位(即 0-255),共可以表示 16,777,216 种颜色。 从上述对比图 6 中不难看出,色深越大,图像的效果越好,图像内容之间的过度越自然,与此同时占用的存储也会越多。 在视频拍摄中,我们通常说的 8bit 和 10bit 指的是位深(Bit Depth),即每个通道的位数。设备在拍摄素材时,记录更大位数的信息会更有利于后期调色等处理。 在显示器的特性中,我们也经常会遇见 8bit 和 10bit,以及 8bit FRC 这个概念。FRC 是 Frame Rate Control 的缩写,即帧率控制,是一种时间维度的像素抖动算法。以灰度图像为例,如下图所示,当渲染一个图像包含多个帧时,可以让帧在明暗之间进行切换,从而产生中间灰度。 相应的空间维度的像素抖动算法(Dither)如下图所示: 所以,一块原生 10bit 屏幕优于 8bit FRC 10bit 的屏幕优于原生 8bit 的屏幕。 色度抽样 在拍摄视频时,除了 8bit 和 10bit 位深的区别外,我们还经常听到 4:2:2 和 4:2:0 等比值,这代表色度抽样。由于人眼对色度的敏感度不及对亮度的敏感度,图像的色度分量不需要有和亮度分量相同的清晰度,在色度上进行抽样可以在不明显降低画面质量的同时降低影像信号的总带宽。 抽样系统通常用一个三分比值表示:$J : a : b$,其中: $J$ 为水平抽样的宽度 $a$ 为第一行 $J$ 个像素中色度的抽样数量 $b$ 为第二行 $J$ 个像素中色度的抽样数量 不同的比值色度抽样对比图 7 如下所示: 动态范围 动态范围(Dynamic Range)是可变信号(例如声音或光)最大值和最小值的比值。在相机中,设置不同的 ISO 会影响到动态范围在记录高光和暗部时的噪点表现。 高动态范围(High Dynamic Range,HDR)相比与标准动态范围(Standard Dynamic Range,SDR)具有更大的动态范围,简而言之 HDR 可以让画面中亮的地方足够亮暗的的地方足够暗。HDR 需要采集设备和显示设备同时支持才能够得以正常的显示,下图 8 展示了 HDR 和 SDR 从场景采集到显示还原的过程: 最终 SDR 和 HDR 成像的区别如下图 9 所示(模拟效果): 在摄影过程中,如下两种方式都可以得到不错的 HDR 照片: 针对 RAW 格式照片,其存储的不同明暗数据已经足够多,针对高光降低一些曝光,暗部增加一些曝光即可获得 HDR 照片。 前期进行包围曝光,即在拍摄时同时拍摄多张具有不同曝光补偿的照片,后期再利用曝光合成技术得到一张 HDR 照片。 在摄像过程中,上述的两种方案就变得不太可行了,如果对于视频的每一帧都保存 RAW 信息会导致视频素材体积过大。此时我们会采用一种名为 Log 曲线的方式对视频的每一帧图像进行处理。 首先我们需要了解一下什么是曝光量(Photometric Exposure)和曝光值(Exposure Value,EV)。曝光量是指进入镜头在感光介质上的光量,其由光圈、快门和感光度组合控制,定义为: $$ H = Et $$ 其中,$E$ 为影像平面的照度,$t$ 为快门的曝光时间。影像平面照度与光圈孔径面积成正比,因此与光圈 $f$ 值的平方成反比,则有: $$ H \propto \dfrac{t}{N^2} $$ 其中,$N$ 为光圈的 $f$ 值。$\dfrac{t}{N^2}$ 这个比例值可以用于表示多个等效的曝光时间和光圈 $f$ 值组合。此比值具有较大的分母,为了方便使用反转该比值并取以 $2$ 为底的对数则可以得到曝光值的定义: $$ EV = \log_2{\dfrac{N^2}{t}} = 2 \log_2{\left(N\right)} - \log_2{\left(t\right)} $$ 在现实中,随着光线强度(类比曝光量)的成倍增加,人眼对于光的感应(类比曝光值)大约成线性增长。同时,摄像机器对于光线强度的记录是线性的,也就是说当光线强度翻倍时,转换后存储的数值也会翻倍。 以 8bit 为例,对于高光部分(7 - 8 档曝光值)会使用 128 位存储相关信息,而对于暗部(0 - 1 档曝光值)则仅使用 8 位存储相关信息,如下图左所示。此时由于高光部分看起来亮度变化并不大,使用的存储位数比暗部多得多,这种非均衡的的存储容易丢失图像的暗部细节。通过对曝光量进行 Log 处理,可以得到均衡的对应关系,如下图右所示。 在真实场景中,各个相机厂商的所搭载的 Log 曲线并不完全相同,都会为了实现某种效果进行调整修改。但整体来说其目的还是为了让每一档曝光值之间存储的信息量大致相同。颜色矫正后和原始应用 Log 曲线的对比图像 10 如下所示: Verhoeven, G. (2016). Basics of photography for cultural heritage imaging. In E. Stylianidis & F. Remondino (Eds.), 3D recording, documentation and management of cultural heritage (pp. 127–251). Caithness: Whittles Publishing. ↩︎ Patrangenaru, V., & Deng, Y. (2020). Nonparametric data analysis on the space of perceived colors. arXiv preprint arXiv:2004.03402. ↩︎ ↩︎ https://en.wikipedia.org/wiki/CIE_1931_color_space#Meaning_of_X,_Y_and_Z ↩︎ https://commons.wikimedia.org/wiki/File:CIE1931xy_blank.svg ↩︎ https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison.svg ↩︎ https://en.wikipedia.org/wiki/Color_depth ↩︎ https://en.wikipedia.org/wiki/Chroma_subsampling ↩︎ https://www.benq.com/en-my/knowledge-center/knowledge/what-is-hdr.html ↩︎ https://kmbcomm.com/demystifying-high-dynamic-range-hdr-wide-color-gamut-wcg/ ↩︎ https://postpace.io/blog/difference-between-raw-log-and-rec-709-camera-footage/ ↩︎

2023/7/30
articleCard.readMore

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

CSS 中的布局 display 和定位 position 可以说是两个最基本的属性,其控制着元素在网页中的显示方式。之前对布局和定位可谓是一知半解,最终奏不奏效全凭一顿乱试 😂,想了想还是应该细致地了解下,后面虽不妄想写起代码来事半功倍,但至少不会再暴力遍历破解了。 盒模型 在介绍布局和定位之前,首先回顾一下 CSS 的盒模型。CSS 盒模型从外到内由外边距 margin、边框 border、内边距 padding 和内容 content 共 4 部分组成,如下图所示: 元素的宽度 width 为内容的宽度 + 左边框 + 有边框 + 左内边距 + 右内边距,上例中为 $360+10+10+10+10=400$;元素的的高度 height 为内容的高度 + 上边框 + 下边框 + 上内边距 + 下内边距,上例中为 $240+10+10+20+20=300$。在实际中,我们并不能直接设定内容的宽度和高度,只能设置元素的宽度和高度,而显示区域的宽度和高度则通过计算自动设定。 在 CSS 中广泛使用的有两种盒子模型:块级盒子(block box) 和 内联盒子(inline box)1。 块级盒子有如下表现行为: 盒子会在内联方向上扩展并占据父容器在该方向上的所有可用空间,在绝大数情况下意味着盒子会和父容器一样宽。 每个盒子都会换行。 width 和 height 属性可以发挥作用。 内边距、外边距和边框会将其他元素从当前盒子周围“推开”。 除非特殊指定,诸如标题 (<h1> 等) 和段落 (<p>) 默认情况下都是块级的盒子。 内联盒子有如下表现行为: 盒子不会产生换行。 width 和 height 属性将不起作用。 垂直方向的内边距、外边距以及边框会被应用但是不会把其他处于 inline 状态的盒子推开。 水平方向的内边距、外边距以及边框会被应用且会把其他处于 inline 状态的盒子推开。 <a> 、<span>、<em> 以及 <strong> 都是默认处于 inline 状态的。 布局 在 CSS 中使用 display 属性控制元素的布局方式,上文中的 block 和 inline 是最常用的两种布局方式。除此之外还有一种介于块级盒子和内联盒子之间的布局方式,即 inline-block,其具有如下表现行为: 盒子不会产生换行。 width 和 height 属性可以发挥作用。 内边距、外边距和边框会将其他元素从当前盒子周围“推开”。 这是一段包含 span 元素的文本。display: inline 的 span 元素的 width 和 height 属性无法发挥作用。 #inline-block-span span { display: inline-block; margin: 20px; padding: 20px; width: 140px; height: 80px; background-color: #916cad; border: 2px #523874 solid; } 这是一段包含 span 元素的文本。display: inline-block 的 span 元素的 width 和 height 属性可以发挥作用。 上图分别展示了 display: inline 和 display: inline-block 两种布局 span 元素的显示差异。 弹性布局 本节内容主要参考自:A Complete Guide to Flexbox 弹性布局(Flexbox Layout,Flexible Box Layout) 旨在提供一种更加有效的方式来布局、对齐和分配容器中元素之间的空间,即使元素的大小是未知或动态的,这也就是称为“弹性”的原因。 弹性布局是一套完整的模块而非一个单一的属性,其中一些属性要设置在父元素(flex container) 上,一些属性要设置在子元素(flex items) 上。常规布局是基于块级元素和内联元素的的流向,而弹性布局是基于弹性流向(flex-flow directions)。下图展示了弹性布局的基本思想: 父元素属性 display 该属性启用弹性容器,为其子元素开启弹性上下文。 .container { display: flex; /* 或 inline-flex */ } flex-direction 该属性定义了弹性流向,即基本思想中的 main-axis。 .container { flex-direction: row | row-reverse | column | column-reverse; } row(默认):ltr 时从左至右,rtl 时从右至左 row-reverse:ltr 时从右至左,rtl 时从左至右 column:从上至下 column-reverse:从下至上 flex-wrap 默认情况下会将子元素放置在一行中,该属性用于设置换行模式。 .container { flex-wrap: nowrap | wrap | wrap-reverse; } nowarp(默认):所有子元素放置在一行中。 wrap:允许换行,从上至下。 wrap-reverse:允许换行,从下至上。 flex-flow 该属性是 flex-direction 和 flex-wrap 两个属性的简写。 .container { flex-flow: column wrap; } justify-content 该属性用于设置主轴(main axis)方向的对齐方式。 .container { justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe; } flex-start(默认):将子元素排列在 flex-direction 起始位置。 flex-end:将子元素排列在 flex-direction 结束位置。 center:将子元素沿着 flex-direction 方向居中排列。 space-between:将子元素沿着 flex-direction 方向均匀排列,第一个子元素位于起始位置,最后一个子元素位于结束位置。 space-around:将子元素沿着 flex-direction 方向均匀排列,每个子元素周围分配相同的空间。 space-evenly:将子元素沿着 flex-direction 方向均匀排列,每个子元素之间的间隔相同。 align-items 该属性用于设置交叉轴(cross axis)方向的对齐方式。 .container { align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe; } stretch(默认):拉伸并填充容器(仍遵守 min-width 和 max-width)。 flex-start / start / self-start:子元素被放置在交叉轴的起始位置。 flex-end / end / self-end:子元素被放置在交叉轴的结束位置。 center:子元素在交叉轴上居中对齐。 baseline:子元素沿着他们的基线对齐。 align-content 该属性用于设置当交叉轴上有额外的空间时容器多行的内部对齐方式,类似 justify-content 设置主轴上子元素的对齐方式。 警告 该属性仅对包含多行子元素的容器有效。 .container { align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe; } normal(默认):子元素被放置到容器的默认位置。 flex-start / start:子元素被放置到容器的起始位置。 flex-end / end:子元素被放置到容器的结束位置。 center:子元素被放置到容器的居中位置。 space-between:子元素均匀分布,第一行在容器的起始位置,最后一行在容器的结束位置。 space-around:子元素均匀分布,每行元素周围分配相同的空间。 space-evenly:子元素均匀分布,每行元素之间的间隔相同。 stretch:子元素拉伸占用剩余空间。 gap, row-gap, column-gap 该属性用于控制子元素之间的间距,其仅用于非边缘子元素之间的间距。 .container { display: flex; ... gap: 10px; gap: 10px 20px; /* row-gap column gap */ row-gap: 10px; column-gap: 20px; } 该属性产生的行为可以认为是子元素之间的最小间距。 子元素属性 order 默认情况下,子元素按照代码顺序排列。该属性可以控制子元素在容器中的顺序。 .item { order: 5; /* 默认为 0 */ } flex-grow 该属性定义了子元素在必要时的扩张能力,其接受一个整数比例值用于设定子元素占用容器的空间。如果所有子元素的 flew-grow 都设置为 1,则所有子元素将评分容器的剩余空间;如果一个子元素的 flex-grow 设置为 2,则该子元素将尝试占用其他子元素 2 倍大小的空间。 .item { flex-grow: 4; /* 默认为 0 */ } flex-shrink 该属性定义了子元素在必要时的收缩能力。 .item { flex-shrink: 3; /* 默认为 1 */ } flex-basis 该属性定义了分配剩余空间之前子元素的默认大小。其可以为例如 20%、5rem 之类的长度或一个关键字。 .item { flex-basis: | auto; /* 默认为 auto */ } flex 该属性是 flex-grow、flex-shrink 和 flex-basis 三个属性的简写。 .item { flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] } align-self 该属性可以覆盖由 align-items 指定的对齐方式。 .item { align-self: auto | flex-start | flex-end | center | baseline | stretch; } 网格布局 本节内容主要参考自:A Complete Guide to CSS Grid 网格布局(Grid Layout)是一种基于网格的布局系统,相比于沿轴线 一维布局 的弹性布局,网格布局可以看做是一种 二维布局。 核心概念 网格容器 网格容器即属性 display 为 grid 的元素,其为所有网格项目的直接父级。如下示例中,container 即为网格容器: <div class="container"> <div class="item item-1"> </div> <div class="item item-2"> </div> <div class="item item-3"> </div> </div> 网格项目 网格项目为网格容器的直接后代。如下示例中,item 即为网格项目,但 sub-item 不是: <div class="container"> <div class="item"> </div> <div class="item"> <p class="sub-item"> </p> </div> <div class="item"> </div> </div> 网格线 网格线即构成网格结构的分界线。其可以是位于行或列任意一侧的垂直或水平线。如下示例中,黄色的线为一条列网格线: 网格单元 网格单元即两个相邻行和两个相邻列之间的区域。如下示例中,黄色区域为行网格线 1 和 2 以及列网格线 2 和 3 之间的单元格: 网格轨道 网格轨道即 2 条相邻网格线之间的区域,可以将其视为网格的行或列。如下示例中,黄色区域为第 2 行和第 3 行网格线之间的网格轨道: 网格区域 网格区域即 4 条网格线包围的区域,一个网格区域可以由任意数量的网格单元组成。如下示例中,黄色区域为行网格线 1 和 3 以及列网格线 1 和 3 之间的网格区域: 父元素属性 display 该属性启用网格容器,为其子元素开启网格上下文。 .container { display: grid | inline-grid; } grid-template-columns, grid-template-rows 该属性通过空格分隔的值列表定义网格的列和行,值代表轨道的大小。值列表包括: <track-size>:轨道大小,可以为长度、百分比等。 <line-name>:网格线名称,可以为任意值。 .container { grid-template-columns: ... ...; /* 例如: 1fr 1fr minmax(10px, 1fr) 3fr repeat(5, 1fr) 50px auto 100px 1fr */ grid-template-rows: ... ...; /* 例如: min-content 1fr min-content 100px 1fr max-content */ } 网格线默认将会被分为正整数(-1 作为最后一个的替代值)。 同时也可以明确指定这些线的名称,请注意括号命名语法: .container { grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end]; grid-template-rows: [row1-start] 25% [row1-end] 100px [third-line] auto [last-line]; } 请注意,一个行或列可以有多个名称: .container { grid-template-rows: [row1-start] 25% [row1-end row2-start] 25% [row2-end]; } 使用 repeat() 可以简化重复项: .container { grid-template-columns: repeat(3, 20px [col-start]); } 上述代码等效于: .container { grid-template-columns: 20px [col-start] 20px [col-start] 20px [col-start]; } 如果多行或多列共享相同的名称,可以通过行名或列名和计数来引用它们: .item { grid-column-start: col-start 2; } fr 单位允许将轨道的大小设置为网格容器可用空间的一定比例。例如,如下示例将每个项目设置为容器宽度的三分之一: .container { grid-template-columns: 1fr 1fr 1fr; } 可用空间是在所有非弹性项目之后计算得到。在上述示例中,fr 单位的可用空间总量不包括 50px: .container { grid-template-columns: 1fr 50px 1fr 1fr; } grid-template-areas 该属性通过引用网格区域的名称 grid-area 来定义网格。重复网格区域名称会导致内容跨越这些单元格。句点表示一个空单元格。语法本身提供了网格结构的可视化。 <grid-area-name>:网格区域的名称。 .:空网格单元。 none:未定义的网格区域。 .container { grid-template-areas: "<grid-area-name> | . | none | ..." "..."; } .item-a { grid-area: header; } .item-b { grid-area: main; } .item-c { grid-area: sidebar; } .item-d { grid-area: footer; } .container { display: grid; grid-template-columns: 50px 50px 50px 50px; grid-template-rows: auto; grid-template-areas: "header header header header" "main main . sidebar" "footer footer footer footer"; } 上述示例将创建一个 4 列 3 行的网格。整个顶部为 header 区域,中间一行由 main 和 sidebar 两个区域和一个空单元格组成,最后一行为 footer。 声明中的每一行都需要有相同数量的单元格。可以使用任意数量的句点声明一个空单元格,只要句点之间没有空格,就代表一个单元格。 注意使用此语法仅可以命名区域,不可命名线。使用此语法时,区域两端的线会自动命名,如果网格区域名称为 foo,那么该区域的起始行线和起始列线名称为 foo-start,该区域的终止行线和终止列线名称为 foo-end。这意味着某些线可能有多个名称,上述示例中最左边的行线将有 3 个名称:header-start、main-start 和 footer-start。 grid-template 该属性是 grid-template-rows、grid-template-columns 和 grid-template-areas 三个属性的简写。 .container { grid-template: none | <grid-template-rows> / <grid-template-columns>; } 其接受更复杂但更方便的语法来指定这三个值,例如: .container { grid-template: [row1-start] "header header header" 25px [row1-end] [row2-start] "footer footer footer" 25px [row2-end] / auto 50px auto; } 上述代码等效于: .container { grid-template-rows: [row1-start] 25px [row1-end row2-start] 25px [row2-end]; grid-template-columns: auto 50px auto; grid-template-areas: "header header header" "footer footer footer"; } 由于 grid-template 并不会重置网格的隐含属性(grid-auto-columns、grid-auto-rows 和 grid-auto-flow)。因此,建议使用 grid 属性而非 grid-template。 column-gap, row-gap, grid-column-gap, grid-row-gap 该属性用于指定网格线的大小,你可以将其看做列和行之间的间距。 .container { /* standard */ column-gap: <line-size>; row-gap: <line-size>; /* old */ grid-column-gap: <line-size>; grid-row-gap: <line-size>; } .container { grid-template-columns: 100px 50px 100px; grid-template-rows: 80px auto 80px; column-gap: 10px; row-gap: 15px; } 间距仅在列和行之间创建,不在边缘创建。注意,带有 grid- 前缀的属性将被废弃。 gap, grid-gap 该属性为 row-gap 和 column-gap 两个属性的简写。 .container { /* standard */ gap: <grid-row-gap> <grid-column-gap>; /* old */ grid-gap: <grid-row-gap> <grid-column-gap>; } .container { grid-template-columns: 100px 50px 100px; grid-template-rows: 80px auto 80px; gap: 15px 10px; } 如果未指定 row-gap,则它将被设置为与 column-gap 相同的值。注意,带有 grid- 前缀的属性将被废弃。 justify-items 沿 inline(行)轴对齐网格项(与沿 block(列)轴对齐 align-items 相反)。该属性将应用于容器内所有网格项。 stretch(默认值):将网格项填充至整个单元格宽度。 start:将网格项与单元的起始边缘对齐。 end:将网格项与单元的结束边缘对齐。 center:将网格项与单元的中心对齐。 .container { justify-items: stretch | start | end | center; } .container { justify-items: stretch; } .container { justify-items: start; } .container { justify-items: end; } .container { justify-items: center; } align-items 沿 block(列)轴对齐网格项(与沿 inline(行)轴对齐 align-items 相反)。该属性将应用于容器内所有网格项。 stretch(默认值):将网格项填充至整个单元格高度。 start:将网格项与单元的起始边缘对齐。 end:将网格项与单元的结束边缘对齐。 center:将网格项与单元的中心对齐。 baseline:将网格项沿文本基线对齐。 .container { align-items: stretch | start | end | center; } .container { align-items: stretch; } .container { align-items: start; } .container { align-items: end; } .container { align-items: center; } 通过 align-self 属性可以在单个网格项上覆盖由 align-items 指定的对齐方式。 place-items 该属性在单次声明中同时设置 align-items 和 justify-items 属性。 <align-items> / <justify-items>:省略第二个值则将第一个值分配给两个属性。 .center { display: grid; place-items: center; } justify-content 当所有网格项均使用非弹性的单位(例如 px)来确定大小,则网格的总大小可能小于网格容器的大小。在这种情况下,可以在网格容器内设置网格的对齐方式。该属性沿 inline(行)轴(与沿 block(列)轴对齐 align-content 相反)对齐网格。 start:将网格与网格容器的起始边缘对齐。 end:将网格与网格容器的结束边缘对齐。 center:将网格与网格容器的中心对齐。 stretch :调整网格项的大小使网格填充网格容器的整个宽度。 space-around:每个网格项均匀分布,每个网格项周围分配相同的空间。 space-between:每个网格项均匀分布,第一个网格项在起始位置,最后一个网格项在结束位置。 space-evenly:每个网格项均匀分布,每个网格项之间的间隔相同。 .container { justify-content: start | end | center | stretch | space-around | space-between | space-evenly; } .container { justify-content: start; } .container { justify-content: end; } .container { justify-content: center; } .container { justify-content: stretch; } .container { justify-content: space-around; } .container { justify-content: space-between; } .container { justify-content: space-evenly; } align-content 当所有网格项均使用非弹性的单位(例如 px)来确定大小,则网格的总大小可能小于网格容器的大小。在这种情况下,可以在网格容器内设置网格的对齐方式。该属性沿 block(列)轴(与沿 inline(行)轴对齐 justify-content 相反)对齐网格。 start:将网格与网格容器的起始边缘对齐。 end:将网格与网格容器的结束边缘对齐。 center:将网格与网格容器的中心对齐。 stretch :调整网格项的大小使网格填充网格容器的整个高度。 space-around:每个网格项均匀分布,每个网格项周围分配相同的空间。 space-between:每个网格项均匀分布,第一个网格项在起始位置,最后一个网格项在结束位置。 space-evenly:每个网格项均匀分布,每个网格项之间的间隔相同。 .container { align-content: start | end | center | stretch | space-around | space-between | space-evenly; } .container { align-content: start; } .container { align-content: end; } .container { align-content: center; } .container { align-content: stretch; } .container { align-content: space-around; } .container { align-content: space-between; } .container { align-content: space-evenly; } place-content 该属性在单次声明中同时设置 align-content 和 justify-content 属性。 <align-content> / <justify-content>:省略第二个值则将第一个值分配给两个属性。 grid-auto-columns, grid-auto-rows 该属性指定自动生成的网格轨道(也称为隐式网格轨道)的大小。当网格项多于网格中的单元格或当网格项放置在显示网格之外时,将创建隐式网格轨道。 <track-size>:可以为长度、百分比或可用空间的比例(使用 fr 单位)。 .container { grid-auto-columns: <track-size> ...; grid-auto-rows: <track-size> ...; } .container { grid-template-columns: 60px 60px; grid-template-rows: 90px 90px; } 上述代码将生成一个 2x2 的网格: 使用 grid-column 和 grid-row 来定位网格项: .item-a { grid-column: 1 / 2; grid-row: 2 / 3; } .item-b { grid-column: 5 / 6; grid-row: 2 / 3; } .item-b 从第 5 列线开始到第 6 列线结束,但由于并未定义第 5 列线和第 6 列线,因此创建了宽度为 0 的隐式轨道用于填充间隙。使用 grid-auto-columns 和 grid-auto-rows 可以指定这些隐式轨道的宽度: .container { grid-auto-columns: 60px; } grid-auto-flow 如果有未明确放置在网格中的网格项目,自动放置算法会自动放置这些网格项目。此属性用于控制自动放置算法的工作方式。 row(默认):依次填充每一行,并根据需要添加新行。 column:依次填充每一列,并根据需要添加新列。 dense:将可能较晚出现的较小的网格项优先填充在网格中。 .container { grid-auto-flow: row | column | row dense | column dense; } 注意 dense 仅会改变网格项目的视觉顺序,这可能导致顺序混乱且不利于访问。 考虑如下示例: <section class="container"> <div class="item-a">item-a</div> <div class="item-b">item-b</div> <div class="item-c">item-c</div> <div class="item-d">item-d</div> <div class="item-e">item-e</div> </section> 定义一个包含 5 列和 2 行的网格,并将 grid-auto-flow 设置为 row: .container { display: grid; grid-template-columns: 60px 60px 60px 60px 60px; grid-template-rows: 30px 30px; grid-auto-flow: row; } 将网格项目放置在网格中时,只需要为其中两个指定位置: .item-a { grid-column: 1; grid-row: 1 / 3; } .item-e { grid-column: 5; grid-row: 1 / 3; } 因为将 grid-auto-flow 设置为了 row,未放置的三个网格项目(item-b、item-c 和 item-d)如下所示: .container { display: grid; grid-template-columns: 60px 60px 60px 60px 60px; grid-template-rows: 30px 30px; grid-auto-flow: column; } 如果将 grid-auto-flow 设置为 column,未放置的三个网格项目(item-b、item-c 和 item-d)如下所示: grid 该属性为 grid-template-rows、grid-template-columns、grid-template-areas、grid-auto-rows、grid-auto-columns 和 grid-auto-flow 属性的简写。 none:将所有子属性设置为初始值。 <grid-template>:同 grid-template。 <grid-template-rows> / [ auto-flow && dense? ] <grid-auto-columns>?:设置 grid-template-rows 为指定值。如果使用 auto-flow 关键字,则设置 grid-auto-flow 为 colomn。如果额外使用 dense 关键字,则自动放置算法将使用 dense 算法。如果省略 grid-auto-columns,则其被设置为 auto。 [ auto-flow && dense? ] <grid-auto-rows>? / <grid-template-columns>:设置 grid-template-columns 为指定值。如果使用 auto-flow 关键字,则设置 grid-auto-flow 为 row。如果额外使用 dense 关键字,则自动放置算法将使用 dense 算法。如果省略 grid-auto-rows,则其被设置为 auto。 如下示例中的代码是等效的: .container { grid: 100px 300px / 3fr 1fr; } .container { grid-template-rows: 100px 300px; grid-template-columns: 3fr 1fr; } .container { grid: auto-flow / 200px 1fr; } .container { grid-auto-flow: row; grid-template-columns: 200px 1fr; } .container { grid: auto-flow dense 100px / 1fr 2fr; } .container { grid-auto-flow: row dense; grid-auto-rows: 100px; grid-template-columns: 1fr 2fr; } .container { grid: 100px 300px / auto-flow 200px; } .container { grid-template-rows: 100px 300px; grid-auto-flow: column; grid-auto-columns: 200px; } 它还接受更复杂但更方便的语法来一次性设置所有内容。如下示例中的代码是等效的: .container { grid: [row1-start] "header header header" 1fr [row1-end] [row2-start] "footer footer footer" 25px [row2-end] / auto 50px auto; } .container { grid-template-areas: "header header header" "footer footer footer"; grid-template-rows: [row1-start] 1fr [row1-end row2-start] 25px [row2-end]; grid-template-columns: auto 50px auto; } 子元素属性 grid-column-start, grid-column-end, grid-row-start, grid-row-end 该属性通过网格线来设置网格项在网格中的位置。grid-column-start 和 grid-row-start 为网格项起始的线,grid-column-end 和 grid-row-end 为网格项结束的线。 <line>:指代网格线的数字编号或名称。 span <number>:该网格项跨越的网格轨道数。 span <name>:该网格项跨越直到它抵达该名称网格线的下一个网格线。 auto:表示自动放置、自动跨度或一个默认跨度。 .item { grid-column-start: <number> | <name> | span <number> | span <name> | auto; grid-column-end: <number> | <name> | span <number> | span <name> | auto; grid-row-start: <number> | <name> | span <number> | span <name> | auto; grid-row-end: <number> | <name> | span <number> | span <name> | auto; } .item-a { grid-column-start: 2; grid-column-end: five; grid-row-start: row1-start; grid-row-end: 3; } .item-b { grid-column-start: 1; grid-column-end: span col4-start; grid-row-start: 2; grid-row-end: span 2; } 如果 grid-column-end 或 grid-row-end 未声明,则该网格项将默认跨越一个轨道。网格项目之间可以相互重叠,使用 z-index 可以控制它们的重叠次序。 grid-column, grid-row 分别是 grid-column-start + grid-column-end 和 grid-row-start+ grid-row-end 的简写。 <start-line> / <end-line>:接受非简写版本相同的值,包括 span。 .item { grid-column: <start-line> / <end-line> | <start-line> / span <value>; grid-row: <start-line> / <end-line> | <start-line> / span <value>; } .item-c { grid-column: 3 / span 2; grid-row: third-line / 4; } 如果未设置结束线的值,则该网格项将默认跨越一个轨道。 grid-area 为一个网格项命名以便它可以使用 grid-template-areas 属性创建的模板引用。此属性可以作为 grid-row-start + grid-column-start + grid-row-end + grid-column-end 的简写。 <name>:选用的名称。 <row-start> / <column-start> / <row-end> / <column-end>:可以为数字编号或线名称。 用作为网格项分配名称: .item-d { grid-area: header; } 用作 grid-row-start + grid-column-start + grid-row-end + grid-column-end 的简写: .item-d { grid-area: 1 / col4-start / last-line / 6; } justify-self 沿 inline(行)轴对齐单元格内的网格项(与沿 block(列)轴对齐 align-self 相反)。该属性仅应用于单个单元格内的网格项。 stretch(默认):填充单元格的整个宽度。 start:将网格项与单元格的起始边缘对齐。 end:将网格项与单元格的结束边缘对齐。 center:将网格项与单元格的中心对齐。 .item { justify-self: stretch | start | end | center; } .item-a { justify-self: stretch; } .item-a { justify-self: start; } .item-a { justify-self: end; } .item-a { justify-self: center; } 通过 justify-items 属性可以为容器中所有的网格项设置对齐方式。 align-self 沿 block(列)轴对齐单元格内的网格项(与沿 inline(行)轴对齐 justify-self 相反)。该属性将仅应用于单个单元格内的网格项。 stretch(默认):填充单元格的整个高度。 start:将网格项与单元格的起始边缘对齐。 end:将网格项与单元格的结束边缘对齐。 center:将网格项与单元格的中心对齐。 .item { align-self: stretch | start | end | center; } .item-a { align-self: stretch; } .item-a { align-self: start; } .item-a { align-self: end; } .item-a { align-self: center; } place-self place-self 可以在单次声明中同时设置 align-self 和 justify-self。 auto:默认对齐方式。 <align-self> / <justify-self>:省略第二个值则将第一个值分配给两个属性。 .item-a { place-self: center; } .item-a { place-self: center stretch; } 定位 本节内容主要参考自:定位技术 定位允许我们将一个元素放置在网页的指定位置上。定位并非是一种用来做主要布局的方式,而是一种用于微调布局的手段。通过 position 属性在特定的布局中修改元素的定位方式,该属性有 static、relative、fixed、absolute 和 sticky 共 5 种可选值。 为了展示不同 position 的效果,在此采用相同的 HTML 进行比较: <h1>XXX 定位</h1> <p>这是一个基本块元素。</p> <p class="position">这是一个基本块元素。</p> <p>这是一个基本块元素。</p> 默认样式为: body { width: 400px; margin: 0 auto; } h1 { text-align: center; } p { margin: 10px; padding: 10px; background-color: #916cad; border: 2px #523874 solid; border-radius: 3px; } 静态定位 静态定位(static)是 position 属性的 默认值,它表示将元素放置在文档布局流的默认位置上。 静态定位样式为: .position { position: static; } 渲染效果如下: 相对定位 相对定位(relative)表示相对于 静态定位 的默认位置进行偏移,其需要搭配 top、bottom、left 和 right 四个属性使用。 相对定位样式为: .position { position: relative; top: 30px; left: 30px; background-color: #c7fba5cc; border: 2px #adf182cc solid; } 渲染效果如下: 绝对定位 绝对定位(absolute)表示相对于 上级元素 的位置进行偏移,其需要搭配 top、bottom、left 和 right 四个属性使用。绝对定位的定位基点不能为 static 定位,否则定位基点将变成网页根元素 html。 绝对定位样式为: .position { position: absolute; top: 30px; left: 30px; background-color: #c7fba5cc; border: 2px #adf182cc solid; } 渲染效果如下: 固定定位 固定定位(fixed)表示相对于 视窗(viewport,即浏览器窗口)进行偏移,其需要搭配 top、bottom、left 和 right 四个属性使用。利用固定定位可以实现元素位置不随页面滚动而发生变化。 为了演示固定定位,修改 HTML 代码如下: <h1>固定定位</h1> <p>这是一个基本块元素。</p> <p class="position">固定</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> 固定定位样式为: .position { position: fixed; top: 30px; left: 30px; background-color: #c7fba5cc; border: 2px #adf182cc solid; } 渲染效果如下: 粘性定位 粘性定位(sticky)可以理解为 静态定位(static)和 固定定位(fixed)的 混合。当指定一个元素的 position 属性为 sticky 后,它会在正常布局流中滚动,直至它出现在设定的相对于容器的位置,此时它会停止滚动,表现为固定定位。 为了演示粘性定位,修改 HTML 代码如下: <h1>粘性定位</h1> <p>这是一个基本块元素。</p> <p class="position">这是一个粘性定位元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> <p>这是一个基本块元素。</p> 粘性定位样式为: .position { position: sticky; top: 30px; left: 30px; background-color: #c7fba5cc; border: 2px #adf182cc solid; } 渲染效果如下: https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Building_blocks/The_box_model ↩︎

2023/5/3
articleCard.readMore

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

文本是「当我谈」系列的第一篇博客,后续「当我谈」系列会从程序员的视角一起科普认知未曾触及的其他领域。 色彩空间 色彩空间是对色彩的组织方式,借助色彩空间和针对物理设备的测试,可以得到色彩的固定模拟和数字表示。色彩模型是一种抽象数学模型,通过一组数字来描述颜色。由于“色彩空间”有着固定的色彩模型和映射函数组合,非正式场合下,这一词汇也被用来指代色彩模型。 RGB 红绿蓝(RGB)色彩模型,是一种加法混色模型,将红(Red)、绿(Green)、蓝(Blue)三原色的色光以不同的比例相加,以合成产生各种色彩光。三原色的原理不是出于物理原因,而是由于生理原因造成的。 RGB 色彩模型可以映射到一个立方体上,如下图所示: 红绿蓝的三原色光显示技术广泛用于电视和计算机的显示器,利用红、绿、蓝三原色作为子像素组成的真色彩像素,透过眼睛及大脑的模糊化,“人类看到”不存在于显示器上的感知色彩。 CMYK 印刷四分色模式(CMYK)是彩色印刷中采用的一种减法混色模型,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓的“全彩印刷”。四种标准颜色分别是: Cyan:青色或“水蓝” Magenta:洋红色或“紫色” Yellow:黄色 Key plate:因实务上多使用黑色,所以也可以简单视为 blacK CMY 叠色的示意图如下所示: 利用 $0$ 到 $1$ 的浮点数表示 $R, G, B$ 和 $C, M, Y, K$,从四分色向三原光转换公式如下: $$ \begin{aligned} R &= \left(1 - C\right) \left(1 - K\right) \\ G &= \left(1 - M\right) \left(1 - K\right) \\ B &= \left(1 - Y\right) \left(1 - K\right) \end{aligned} $$ 从三原光向四分色转换公式如下: $$ \begin{aligned} C &= 1 - \dfrac{R}{\max \left(R, G, B\right)} \\ M &= 1 - \dfrac{G}{\max \left(R, G, B\right)} \\ Y &= 1 - \dfrac{B}{\max \left(R, G, B\right)} \\ K &= 1 - \max \left(R, G, B\right) \\ \end{aligned} $$ HSL 和 HSV HSL 和 HSV 都是一种将 RGB 色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构 RGB 更加直观。HSL 即色相、饱和度、亮度(Hue,Saturation,Lightness),HSV 即色相、饱和度、明度(Hue,Saturation,Value),又称 HSB,其中 B 为 Brightness。另种色彩空间定义如下图所示: 色相 色相(Hue)指的是色彩的外相,是在不同波长的光照射下,人眼所感觉到的不同的颜色。在 HSL 和 HSV 色彩空间中,色相是以红色为 0 度(360 度)、黄色为 60 度、绿色为 120 度、青色为 180 度、蓝色为 240 度、洋红色为 300 度。如下图所示: 饱和度 饱和度(Saturation)指的是色彩的纯度,饱和度由光强度和它在不同波长的光谱中分布的程度共同决定。下图为红色从最小饱和度到最大饱和度的示例: 亮度和明度 明度值是与同样亮的白色物体相比,某物的亮的程度。如果我们拍摄一张图像,提取图像色相、饱和度和明度值,然后将它们与不同色彩空间的明度值进行比较,可以迅速地从视觉上得出差异。如下图所示,HSV 色彩空间中的 V 值和 HSL 色彩空间中的 L 值与感知明度值明显不同: 差异 HSV 和 HSL 两者对于色相(H)的定义一致,但对于饱和度(S)和亮度与明度(L 与 B)的定义并不一致。 在 HSL 中,饱和度独立于亮度存在,也就是说非常浅的颜色和非常深的颜色都可以在 HSL 中非常饱和。而在 HSV 中,接近于白色的颜色都具有较低的饱和度。 HSV 中的 S 控制纯色中混入白色的量,值越大,混入的白色越少,颜色越纯。 HSV 中的 V 控制纯色中混入黑色的量,值越大,混入的黑色越少,明度越高。 HSL 中的 S 和黑白没有关系,饱和度不控制颜色中混入白色和黑色的多少。 HSL 中的 L 控制纯色中混入白色和黑色的多少。 以 Photoshop 和 Afiinity Photo 两款软件的拾色器为例: 两个软件分别采用 HSV 和 HSL 色彩空间,其横轴为饱和度(S),纵轴分别为明度(V)和亮度(L)。不难看出,在 Photoshop 拾色器中,越往上混入的黑色越少,明度越高;越往右混入的白色越少,纯度越高。在 Afiinity Photo 拾色器中,下部为纯黑色,亮度最小,从下往上,混入的黑色逐渐减少,直到 50% 位置处完全没有黑色混入,继续往上走,混入的白色逐渐增加,直到 100% 位置处完全变为纯白色,亮度最高。 直方图 图像直方图是反映图像色彩亮度的直方图,其中 $x$ 轴表示亮度值,$y$ 轴表示图像中该亮度值像素点的个数。以 $8$ 位图像为例,亮度的取值范围为 $\left[0, 2^8-1\right]$,即 $\left[0, 255\right]$。以如下图片为例(原始图片:链接): 在 Lightroom 中直方图如下所示: 利用 Python 绘制的直方图如下所示: 直方图代码 import cv2 import numpy as np import matplotlib.pyplot as plt gray_img = cv2.imread('demo.jpg', cv2.IMREAD_GRAYSCALE) img = cv2.imread('demo.jpg') img_channels = cv2.split(img) height, width = gray_img.shape gray_img_hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256]) img_channels_hist = [ cv2.calcHist([img_channel], [0], None, [256], [0, 256]) for img_channel in img_channels ] fig, ax = plt.subplots(1, 1) ax.plot(gray_img_hist, color='0.6', label='灰') for img_channel_hist, color, label in zip( img_channels_hist, ['#6695ff', '#70df5f', '#f74048'], ['蓝', '绿', '红'] ): ax.plot(img_channel_hist, color=color, label=label) segments = [0, 28, 85, 170, 227, 255] segments_text = ['黑色', '阴影', '曝光', '高光', '白色'] for left_border, right_border, segment_text in zip( segments[:-1], segments[1:], segments_text ): if left_border != 0: ax.axvline(x=left_border, ymin=0, color='black') ax.annotate( segment_text, xy=((left_border + right_border) / 2, np.max(img_channels_hist) / 3), ha='center', ) ax.legend(loc='upper center') plt.xlim([0, 256]) ax.set_xticks([0, 32, 64, 96, 128, 160, 192, 224, 256]) ax.axes.get_yaxis().set_visible(False) plt.tight_layout() fig.set_size_inches(8, 4) plt.savefig('demo-image-histgram.png', dpi=100) 原始图片直方图 直方图以 $28, 85, 170, 227$ 为分界线可以划分为黑色、阴影、曝光、高光、白色共 5 个区域。其中曝光区域以适中的亮度保留了图片最多的细节,阴影和高光对应了照片中较暗和较亮的区域,黑色和白色两个部分则几乎没有任何细节。当整个直方图过于偏左时表示欠曝,过于偏右时则表示过曝。 色温 色温(Temperature)是指照片中光源发出相似的光的黑体辐射体所具有的开尔文温度。开尔文温度越低光越暖,开尔文温度越高光越冷,如下图所示: 针对图片分别应用 5000K 和 10000K 色温的对比结果如下图所示: 色温代码 import math import cv2 import numpy as np def __kelvin_to_rgb(kelvin: int) -> (int, int, int): kelvin = np.clip(kelvin, min_val=1000, max_val=40000) temperature = kelvin / 100.0 # 红色通道 if temperature < 66.0: red = 255 else: # a + b x + c Log[x] /. # {a -> 351.97690566805693`, # b -> 0.114206453784165`, # c -> -40.25366309332127 # x -> (kelvin/100) - 55} red = temperature - 55.0 red = ( 351.97690566805693 + 0.114206453784165 * red - 40.25366309332127 * math.log(red) ) # 绿色通道 if temperature < 66.0: # a + b x + c Log[x] /. # {a -> -155.25485562709179`, # b -> -0.44596950469579133`, # c -> 104.49216199393888`, # x -> (kelvin/100) - 2} green = temperature - 2 green = ( -155.25485562709179 - 0.44596950469579133 * green + 104.49216199393888 * math.log(green) ) else: # a + b x + c Log[x] /. # {a -> 325.4494125711974`, # b -> 0.07943456536662342`, # c -> -28.0852963507957`, # x -> (kelvin/100) - 50} green = temperature - 50.0 green = ( 325.4494125711974 + 0.07943456536662342 * green - 28.0852963507957 * math.log(green) ) # 蓝色通道 if temperature >= 66.0: blue = 255 elif temperature <= 20.0: blue = 0 else: # a + b x + c Log[x] /. # {a -> -254.76935184120902`, # b -> 0.8274096064007395`, # c -> 115.67994401066147`, # x -> kelvin/100 - 10} blue = temperature - 10.0 blue = ( -254.76935184120902 + 0.8274096064007395 * blue + 115.67994401066147 * math.log(blue) ) return np.clip(red, 0, 255), np.clip(green, 0, 255), np.clip(blue, 0, 255) def __mix_color(v1, v2, ratio: float): return np.array((1.0 - ratio) * v1 + 0.5).astype(np.uint8) + np.array( ratio * v2 ).astype(np.uint8) def __keep_original_lightness(original_image, image): original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1] h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS)) return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR) def apply_temperature(image, temperature, keep_original_lightness: bool = True): b, g, r = cv2.split(image) n_b = np.clip(b.astype(np.single) - temperature, 0, 255).astype(np.uint8) n_r = np.clip(r.astype(np.single) + temperature, 0, 255).astype(np.uint8) ret_image = cv2.merge([n_b, g, n_r]) return ( __keep_original_lightness(image, ret_image) if keep_original_lightness else ret_image ) def apply_kelvin( image, kelvin: int, strength: float = 0.6, keep_original_lightness: bool = True ): b, g, r = cv2.split(image) k_r, k_g, k_b = __kelvin_to_rgb(kelvin) n_r, n_g, n_b = ( __mix_color(r, k_r, strength), __mix_color(g, k_g, strength), __mix_color(b, k_b, strength), ) ret_image = cv2.merge([n_b, n_g, n_r]) return ( __keep_original_lightness(image, ret_image) if keep_original_lightness else ret_image ) img = cv2.imread('demo.jpg') cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 5000)) cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 10000)) 色调 色调(Tint)允许我们为了实现中和色偏或增加色偏的目的,而将色偏向绿色或洋红色转变。针对图片分别应用 -30 和 +30 色调的对比结果如下图所示: 色调代码 import cv2 import numpy as np def __keep_original_lightness(original_image, image): original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1] h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS)) return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR) def apply_tint(image, tint, keep_original_lightness: bool = True): b, g, r = cv2.split(image) n_g = np.clip(g.astype(np.single) + tint, 0, 255).astype(np.uint8) ret_image = cv2.merge([b, n_g, r]) return ( __keep_original_lightness(image, ret_image) if keep_original_lightness else ret_image ) img = cv2.imread('demo.jpg') cv2.imwrite('demo-color-tint-negative.jpg', apply_tint(img, -30)) cv2.imwrite('demo-color-tint-positive.jpg', apply_tint(img, +30))

2023/4/22
articleCard.readMore

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

本文主要面向希望在游戏空闲时段将显卡用于科学事业的朋友们 😎。 更新于 2024-05-19 终端 工欲善其事必先利其器,开发离不开那个黑框框,所以我们需要把这个黑框框变得更好看更好用些。Windows 终端是一个新的支持 PowerShell 和 WSL bash 的应用,通过应用商店 直接进行安装。 建议安装最新版的 PowerShell 作为命令行环境,相关下载和配置详见官网。 为了更好的在终端中显示中英文和图标,推荐使用 Sarasa Term SC Nerd 作为终端显示字体。 网络 为了方便使用,网络设置采用桥接模式。桥接模式需要在 Windows 中启用 Hyper-V(仅 Windows 专业版支持)。通过启用或关闭 Windows 功能开启 Hyper-V,然后重启电脑生效。 在 Hyper-V 中创建一个新的交换机,在连接类型中选择外部网络,并根据电脑的网络连接情况选择对应的桥接网卡。 通过 Get-VMSwitch -SwitchType External 可以查看创建的交换机: Name SwitchType NetAdapterInterfaceDescription ---- ---------- ------------------------------ WSL External Realtek Gaming 2.5GbE Family Controller 在 Home 目录创建 .wslconfig 文件,并添加如下内容: [wsl2] networkingMode=bridged vmSwitch=WSL ipv6=true 其中,vmSwitch 填写创建的交换机的名称。 WSL 以管理员模式打开 PowerShell 或 Windows 命令提示符,输入如下命令,并重启计算机: wsl --install 此命令会启用 WSL 并安装 Ubuntu 发行版 Linux。通过 wsl -l -o 可以查看所有 Linux 的发行版: 以下是可安装的有效分发的列表。 请使用“wsl --install -d <分发>”安装。 NAME FRIENDLY NAME Ubuntu Ubuntu Debian Debian GNU/Linux kali-linux Kali Linux Rolling Ubuntu-18.04 Ubuntu 18.04 LTS Ubuntu-20.04 Ubuntu 20.04 LTS Ubuntu-22.04 Ubuntu 22.04 LTS Ubuntu-24.04 Ubuntu 24.04 LTS OracleLinux_7_9 Oracle Linux 7.9 OracleLinux_8_7 Oracle Linux 8.7 OracleLinux_9_1 Oracle Linux 9.1 openSUSE-Leap-15.5 openSUSE Leap 15.5 SUSE-Linux-Enterprise-Server-15-SP4 SUSE Linux Enterprise Server 15 SP4 SUSE-Linux-Enterprise-15-SP5 SUSE Linux Enterprise 15 SP5 openSUSE-Tumbleweed openSUSE Tumbleweed 通过 wsl --install -d <发行版名称> 可以安装其他发行版 Linux,本文以 Ubuntu 22.04 为例。通过 wsl -l -v 可以查看当前运行的 WSL 版本: NAME STATE VERSION * Ubuntu-22.04 Running 2 通过 wsl 新安装的 Linux 默认已经设置为 WSL 2。 进入 Ubuntu 命令行,输入如下命令安装必要的系统依赖: sudo apt install gcc 安装 zsh 作为 Ubuntu 默认的 Shell: sudo apt install zsh 安装 Oh My Zsh 来提升 zsh 的易用性。 显卡 驱动 从 Nvidia 官网(https://www.nvidia.cn/geforce/drivers)下载适用于 Windows 的最新驱动并安装。进入 Windows 命令行,输入 nvidia-smi 命令查看显卡状态: Ubuntu 中不再需要额外安装显卡驱动,进入 Ubuntu 命令行,输入 nvidia-smi 命令查看显卡状态: 不难看出,出了 nvidia-smi 工具版本不同外,显卡驱动和 CUDA 版本均是相同的。 CUDA 从 Nvidia 官网(https://developer.nvidia.com/cuda-toolkit-archive)下载适用于 WSL Ubuntu 的 CUDA,在此选择的版本为 11.8.0(具体请参考例如 Tensorflow 等所需工具的依赖版本),相关平台选项如下: Operating System:Linux Architecture:x86_64 Distribution:WSL-Ubuntu Version:2.0 Installer Type:runfile (local) 下载完毕后运行如下命令进行安装: chmod +x cuda_11.8.0_520.61.05_linux.run sudo ./cuda_11.8.0_520.61.05_linux.run --toolkit 其中 --toolkit 表示仅安装 CUDA 工具箱。 在弹出的 EULA 界面输入 accept 进入安装选项界面: 仅保留 CUDA Toolkit 11.8 即可,切换到 Install 并按回车键进行安装。 将如下内容添加到 ~/.bashrc 文件尾部: export PATH=/usr/local/cuda/bin:$PATH export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64 通过 source ~/.bashrc 或 source ~/.zshrc 使路径立即生效。输入 nvcc -V 查看 CUDA 编译器驱动版本: cuDNN 从 Nvidia 官网(https://developer.nvidia.com/rdp/cudnn-archive)下载适用于 Linux 和上述安装 CUDA 版本的 cuDNN,在此选择的版本为 v8.8.0 for CUDA 11.x,安装包格式为 Local Installer for Linux x86_64 (Tar)。 注意:cuDNN 需要注册账户后方可进行下载。 下载完毕后运行如下命令进行解压: tar -xvf cudnn-linux-x86_64-8.9.7.29_cuda11-archive.tar.xz 运行如下命令将其移动到 CUDA 目录: sudo mv cudnn-*-archive/include/cudnn*.h /usr/local/cuda/include sudo mv cudnn-*-archive/lib/libcudnn* /usr/local/cuda/lib64 sudo chmod a+r /usr/local/cuda/include/cudnn*.h /usr/local/cuda/lib64/libcudnn* 机器学习环境 Python Ubuntu 22.04 系统已经安装了 Python 3.10 版本,Python 3.10 在常用机器学习库上具有较好的兼容性。因此,以 Python 3.10 版本为例,使用 venv 创建机器学习虚拟环境。在系统层面安装 venv 并创建虚拟环境: sudo apt install python3-venv mkdir ~/SDK python3.10 -m venv ~/SDK/python310 source ~/SDK/python310/bin/activate PyTorch 输入如下命令安装 PyTorch: pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 安装完毕后发现 PyTorch 内嵌了 CUDA 和 cuDNN。内嵌的好处是可以做到安装即用,但如果和其他包依赖的系统 CUDA 和 cuDNN 版本不一致,容易出现各种意想不到的问题。在 Python 中运行如下命令验证 PyTorch 是否可以正常调用显卡: import torch # PyTorch 版本 torch.__version__ # 2.3.0+cu118 # CUDA 是否可用 torch.cuda.is_available() # True # GPU 数量 torch.cuda.device_count() # 1 # GPU 名称 torch.cuda.get_device_name(0) # NVIDIA GeForce RTX 3070 Ti Tensorflow 输入如下命令安装 Tensorflow(2.14.1 版本支持 CUDA 11.8): pip install tensorflow==2.14.1 在 Python 中运行如下命令验证 Tensorflow 是否可以正常调用显卡: import tensorflow as tf # Tensorflow 版本 tf.__version__ # 2.15.1 # GPU 设备 tf.config.list_physical_devices('GPU') # [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] # GPU 名称 tf.test.gpu_device_name() # /device:GPU:0 PyCharm 配置 PyCharm 使用 WSL 中的 Python 请参见 Configure an interpreter using WSL。

2023/3/19
articleCard.readMore

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

文学编程 文学式编程(Literate Programming)是由高德纳提出的编程方法,希望能用来取代结构化编程范型。正如高德纳所构想的那样,文学编程范型不同于传统的由计算机强加的编写程序的方式和顺序,而代之以让程序员用他们自己思维内在的逻辑和流程所要求的顺序开发程序。文学编程自由地表达逻辑,而且它用人类日常使用的语言写出来,就好像一篇文章一样,文章里包括用来隐藏抽象的巨集和传统的源代码。文学编程工具用来从文学源文件中获得两种表达方式,一种用于计算机进一步的编译和执行,称作“绕出”(tangled)的代码,一种用于格式化文档,称作从文学源代码中“织出”(woven)。虽然第一代文学编程工具特定于计算机语言,但后来的工具可以不依赖具体语言,并且存在于比编程语言更高的层次中 1。 如高德纳在论文 2 中所示,相同的源文件经过“tangle”可以编译为机器代码,经过“weave”可以编译为文档。 文学编程历史 从高德纳提出文学编程的概念后,各家各派都在将这个编程范式付诸实践。我接触文学编程已经比较晚了,算是从 R Markdown 和 knitr 开始,开始时写写分析报告和做做幻灯片,慢慢的在更多场景我发现这很适合。 WEB, CWEB & noweb WEB 是一种计算机编程语言系统,它由高德纳设计,是第一种实现他称作“文学编程”的语言。WEB 包含了 2 个主要程序:TANGLE,从源文本生成可编译的 Pascal 代码,以及 WEAVE,使用 TeX 生成格式漂亮可打印的文档。CWEB 是 WEB 的 C 语言新版本,noweb 是另外一种借鉴了 WEB 的文学编程工具,同时与语言无关 3。 以 wc.nw 为例,其为 Unix 单词统计程序 wc 的 noweb 版本重写,原始的 CWEB 版本可以在高德纳的《文学编程》一书中找到。noweb 源代码中包含 TeX 代码和 C 语言代码,每个 C 语言代码片段都以一个 <<代码片段名称>>= 开头,以 @ 结尾,程序的入口为 <<*>>=。在某个代码片段中调用其他代码片段只需要输入 <<代码片段名称>> 即可。 安装 noweb,通过如下命令可以将 wc.nw 编译为 C 语言代码 wc.c: notangle -L wc.nw > wc.c 通过如下命令可以将 wc.nw 编译为 TeX 源代码: noweave -autodefs c -index wc.nw > wc.tex Org Mode Org Mode 是由 Carsten Dominik 于 2003 年发明的用于文本编辑器 Emacs 的一种支持内容分级显示的编辑模式。这种模式下可以创建待办列表,日志管理,做笔记,做工程计划或者写网页。Org Mode 通常启用于后缀名为 org 的纯文本文件,使用星号标记有层次的内容(如文章大纲、话题与子话题、嵌套代码),并提供一组函数用于读取并展示这类标记以及操纵内容(如折叠大纲内容、移动元素、更改待办项状态)4。 在 Org Mode 中使用 #+BEGIN_SRC 和 #+END_SRC 来标记代码块,在 #+BEGIN_SRC 后指定嵌入的代码类型,例如嵌入 C 语言源代码: #+BEGIN_SRC c int main(void) { return 0; } #+END_SRC 更多关于在 Org Mode 中的文学编程应用可以参见 lujun9972/emacs-document。 Sweave & knitr Sweave 是 R 语言的 WEB 实现,为什么是 Sweave 而不是 Rweave,没有仔细去找解释,但我猜测是由于 R 语言的前身为 S 语言吧。既然有了 Sweave 为什么没有 Stangle 呢?也是猜测,或许 Sweave 的作者在创作之初就更侧重于将 R 代码及其运行结果嵌入,“织出”最终阅读友好的文档吧。当然,由于 R 是一门统计分析语言,将所有 R 代码提取出来编译成可执行文件并不是它的优势,我猜这应该也是没有 Stangle 的一个原因吧。当然,也并不是没有人打算这么做,fusen 是一个基于 R Markdown 直接生成 R 扩展包的扩展包,从一定程度上应该算是 tangle 的理念实现吧。 Sweave 是基于 R 和 LaTeX 的实现,但 LaTeX 的学习曲线相对比较陡峭,knitr 的出现拓展了 Sweave 的功能,例如:内容方面增加支持了 Markdown 等,代码方面增加支持了 Python 等。除此之外,也衍生出了多种多样的文档格式,例如:幻灯片(xaringan),图书(bookdown)和博客(blogdown)等等。 在 R Markdown 中使用如下方式嵌入代码,在 {} 中指定嵌入代码的类型,例如嵌入并执行 R 语言源代码: ```{r} add <- function(a, b) { return(a + b) } print(add(1, 1)) ``` 在同一个 R Markdown 文件中可以同时插入 R 和 Python 等多种不同语言的源代码,通过 reticulate 甚至可以实现 R 和 Python 之间的数据交互。 Jupyter Jupyter 是从 IPython Notebook 发展而来,基于 Python 语言的强大优势,其在业界迅速占领了一大片应用市场,后来 Jupyter 也逐渐支持其他语言。虽然现在 R Markdown 也支持在 RStuido 等编辑器中逐行运行,但个人认为 Jupyter 的最大优势就在于边写边运行,这也使得 Jupyter 在教育等需要实时运行的领域应用最为广泛。 Jupyter 仍以 .ipynb 为扩展名,其底层为 JSON 格式的文本文件。原生 Jupyter 针对一个文件仅支持一种 Kernel,即运行一种类型的代码,当通过一些技巧也可以实现同时运行多种类型的代码。 Quarto Quarto 是 Posit(RStudio 的新公司名)开发的一个基于 Pandoc 的开源技术出版系统。Quarto 的目标是改进科学和技术文档的创建和协作过程,其希望将 R Markdown、bookdown、distill、xaringan 等功能统一到一个系统中。 Quarto 的工作流程同 R Markdown 类似,如下图所示: 所以,Quarto 的到来是否意味着 R Markdown 的消失呢?官方 FAQ 给到了否定的答案。不过我认为 Quarto 「一统天下」的野心还是有的,只是基于现状可能这条路还需要再走一阵子。如下是我从当先(2023 年初)现状和个人的一些需求,认为 Quarto 和 R Markdown 之间存在的一些区别: 博客方面,个人需要动态输出的场景不多,blogdown 是基于 Hugo 的实现,动态文档是利用 knitr 将 R Markdown 直接渲染为 HTML 再交由 Hugo 处理。支持 Hugo 的自动化部署(例如:Cloudflare Pages,Netlify,Vercel 等)对比 Quarto 的自动化部署选择要更多些。 幻灯片方面,xaringan 是基于 remark.js 实现的,Quarto 是基于 reveal.js。两者没有孰优孰劣,接触 remark.js 更久一些,更熟悉一些,可能就更偏好一些,不过 remark.js 目前处于非活跃开发状态,这可能是 Quarto 选择 reveal.js 的一个原因吧。 书籍方面,这个不得不说 Quarto 真的是赞了。我认为书籍输出格式是所有格式中最复杂的一个,这也使得在代码执行参数、扩展组件 等方面比 bookdown 支持更灵活的 Quarto 在实践中更好用些。 还是很希望 Quarto 在未来能够做更好的统一,这也会让我们面对不同输出场景中复用更多相同的知识和技巧。 当我谈文学编程时我谈些什么 高德纳提出了文学编程的理念,Peter Seibel 也存在不同的看法:编码并非文学。其实这两者并不是对立的,只是角度不同而已。我认为文学编程更适合数据分析型工程,针对功能系统型工程确实很难融入文学编程。以当下的实践来看,从 R Markdown,到 Jupyter,再到 Quarto,无一例外是针对技术和科学等场景提供数据分析功能,而针对系统工程开发,更多还是遵循着产品文档和工程代码分离。 文学编程是一种理念,类似一门新的语言,从客观上能解决一些特定领域的问题,也能在某些场景中提高效率。但整个生态的发展离不开真正「喜欢」的人参与,不断改善和大力推广才能保证生态的持续发展。除了商业团队的主推以外,我认为开源精神和社区参与也很重要,真正的繁华从来不是一家独大而是全民参与。 可重复性研究 可重复性研究的范畴要比文学编程更广泛,文学编程主要围绕计算机相关科学展开,可重复性研究则是面向全部科学的。可重复性研究指的是科学结果应该在其推论完全透明的方式记录下来 5。 https://github.com/mickaeltemporao/reproducible-research-in-python 上图生动地描述了可重复性研究的重要性。在此我们依旧围绕计算机相关科学讨论可重复性研究。文学编程通过将代码嵌入文档中实现了代码结果的可重复性动态生成,但除了代码之外,可重复性研究还需要关注代码的运行环境和使用的数据等,这些同样会影响研究的最终结果。 运行环境 硬件、内核、操作系统、语言、扩展包等代码运行环境都会对最终的研究结果产生影响,如下图所示: https://github.com/MozillaFoundation/2017-fellows-sf 硬件问题在苹果推出基于 arm 架构的 M1 芯片时一度带来了不少的麻烦,虽然 macOS 提供了转义工具,但在推出的早起仍出现大量软件兼容问题。不过随着这几年的发展,软件的兼容性问题已经得到了极大的改善,因此在硬件这一层几乎不再会有太多问题。 内核和操作系统可以粗略的认为是同一层级,这也是在日常研究中会经常遇到的问题。有时候在自己电脑系统上跑地好好的代码,拿到别人电脑上就会出现各种问题。在工程部署阶段,通过 docker 等虚拟化技术是可以保证代码运行的系统环境是相同的,但在分析研究阶段这并不好用。在这个层面感觉比较好的解决方案就是使用多系统兼容的软件、语言、扩展包等,如果确实需要使用指定系统的工具,在代码层面实现兼容或提示兼容问题会是不错的选择。 语言和扩展包层面的问题在真实场景中遇到的并不多,我们不必非要在 Python 和 R 中二选一,也不必非要在 PyTorch 和 Tensorflow 中二选一。但至少要保证使用相关研究领域中常用的工具、语言和扩展包,当然这些最好甚至应该是开源的,这样其他人才能够无障碍的获取相关代码依赖。 数据公开 在可重复性研究中,数据公开也很重要,没有研究的输入数据,哪怕分析代码全部公开,也无法得到相同的研究结果。最理想的情况就是完全公开所用的原始数据,但这个在涉及到私有域数据时往往又是不现实的。针对这个问题有多种可以尝试解决的方案: 数据脱敏。例如:针对涉及隐私的 ID 可以转换为无意义的 ID,一般情况不会对研究产生影响。例如:针对涉及商业机密的价格或销量可以添加扰动量或进行分箱处理,但这会对研究产生一定的影响。 人造数据。针对所需的数据格式完全人工创造虚拟数据,不过在复杂场景下其成本较高,甚至无法实现。 https://zh.wikipedia.org/zh-hans/文学编程 ↩︎ Knuth, Donald Ervin. “Literate programming.” The computer journal 27.2 (1984): 97-111. ↩︎ https://zh.wikipedia.org/zh-hans/WEB ↩︎ https://zh.wikipedia.org/zh-hans/Org-模式 ↩︎ https://en.wikipedia.org/wiki/Reproducibility ↩︎

2023/3/11
articleCard.readMore

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

历史尝试 入手 NAS 已经近 5 年的时间了,最初只是用来挂 PT 下载,然后在各种设备上通过 SMB 共享播放上面的视频。后面也尝试在利用 Plex 搭建家庭影音中心,但由于 Plex 的高级功能需要付费也就作罢。今年搬家后整体对各种硬件做了升级,换了软路由,做了基于 AC+AP 的全屋 WiFi,NAS 换了更大的硬盘,客厅和卧室各安装了一个投影机,入了 Apple TV 4K 和 Chromecast with Google TV 4K 两个盒子。购买 Apple TV 时买了有 Infuse 的套餐,果然没有花钱的不是,Infuse 无论是从 UI 还是体验上都算优秀,但由于仅限于苹果生态,且可玩性较差,最终也只是沦为了 Apple TV 上的本地播放器。 秉着「付费虽美丽,免费更开心」的原则,最终选择了基于 Jellyfin 的方案。由于 NAS 的 CPU 性能并不高,为了不给 NAS 其他功能带来过多压力,同时考虑软路由性能过剩,因此决定将 Jellyfin 安装在软路由上,再将 NAS 的资源挂载到软路由来实现整体解决方案。 在做 Jellyfin 选型时,其吸引我的最大优点就是开源,同时各个平台的客户端也都在官方应用商店有上架,这极大的简化了客户端的安装流程。付费解决方案,例如:Plex,Emby(在 3.5.3 之后闭源),由于有更多资金的支持,肯定在一些方面会优于 Jellyfin。其他的免费解决方案,例如:NAS 自带的 Video Station,Kodi(大学时代就曾在电脑上安装过)等在不同方面也各有差异。关于不同解决方案的一些差异在此就不再做深入探究,有兴趣的同学可以自行 Google,不过也要注意很多文章时间会比较久了,与当下的实际情况会有部分出入。 硬件设备 服务端设备 设备 系统 CPU 内存 用途 网络连接 位置 软路由 OpwnWrt Intel Celeron N5105 2.0-2.9 GHz 4 核心 4 线程 4GB 主路由 代理服务器 内网穿透服务器 有线 1000M 客厅 NAS DSM 7 Intel Celeron J3355 2.0-2.5 GHz 2 核心 2 线程 6GB 共享存储 PT 下载 迅雷远程下载 有线 1000M 衣帽间 NAS 通过有线网络与主路由直连,虽然主路由网口为 2.5G,但由于 NAS 网口仅为 1000M,而且又懒于把 NAS 上的双网口做链路聚合,因此实际通讯也就限制为 1000M,但对于家庭影音中心也基本够用了。主路由上游使用了运营商提供的光猫,虽然已经改了桥接模式,但由于运营商提供的光猫 LAN 口也是 1000M 的,因此外网也无法突破千兆限制,当然还是由于 10G EPON 的万兆光猫太贵,压制了我鼓捣的欲望。 NAS 自带的内存为 2G,后面加了一条 4G 的内存扩容到 6G,最初也是计划用 NAS 玩一玩 Docker 的。但碍于 J3355 这颗 CPU 性能一般,运行太多东西给 NAS 的基本功能会带来不小压力,我想这也是群辉官方并没有给 DS418play 这款 NAS 提供 Docker 应用的主要原因吧。软路由当时买了非裸机的丐版,但由于并没有用其做太多事情,空闲内存基本上还有 3.5G 左右,因此为了充分利用 N5105 这颗 CPU,最终决定将需要视频解码这类耗 CPU 的任务交给软路由了。不过买的这款软路由是被动散热,正常待机就干到 60 摄氏度左右了,CPU 占用上来了估计有望突破 100 摄氏度😂。 客户端设备 设备 系统 用途 网络/视频连接 位置 Apple TV 4K tvOS 16 主电视盒子 有线 1000M 客厅 明基 TK850 - 主投影机 HDMI 2.1 客厅 Chromecast with Google TV 4K Android 12 原生 次电视盒子 无线 WiFi 5 主卧 小明 Q2 Pro Android 9 非原生 次投影机 无线 WiFi 5 HDMI 2.1 主卧 PC Windows 11 台式机 有线 1000M 主卧 Macbook Pro macOS 13 笔记本 无线 WiFi 5 - iPhone 13 Pro iOS 16 主手机 无线 WiFi 6 - Google Pixel 6 Pro Android 13 原生 备用手机 无线 WiFi 6 - iPad Pro iPadOS 16 平板电脑 无线 WiFi 6 - 所有客户端通过 H3C 的 1000M AC+AP 采用有线或无线间接连接到主路由。综上所述,家里各种内外部线路就都是 1000M 的理论带宽了。 客户端设备几乎覆盖了所有常用的系统,Jellyfin 在各个系统上均提供了客户端,而且可以在官方商店直接安装,这也是最终确认选择 Jellyfin 的关键一点。毕竟服务端搞得再好,客户端安装费劲的不行也是很痛苦的,尤其是在苹果生态中,官方商店的支持会让你泪大喜奔的。 NAS 准备 由于 Jellyfin 安装在软路由上,因此需要将 NAS 上的媒体文件夹通过 NFS 映射到软路由上,首先需要在 NAS 上配置客户端。进入 NAS,打开 控制面板,进入 文件服务,确保 启用 NFS 服务,最大 NFS 协议选择 NFSv4.1: 进入 共享文件夹,选择需要通过 NFS 共享的文件夹: 单击 编辑 进入共享文件夹设置: 在 NFS 权限 标签页单击 新增 添加新客户端: 相关配置如图所示,其中 服务器名称或 IP 地址 为客户端 IP 地址(即软路由 IP 地址)。依次为所有需要共享的文件夹进行相同配置。 OpenWrt 准备 软路由自带了 128G 的 NVME 固态硬盘,系统采用了 eSir 编译的高大全版本。为了后续安装扩展包和 Docker,对硬盘重新进行分区。 通过 系统 > TTYD终端 在输入用户名(root)和密码后可以进入软路由命令行,输入 fdisk -l 可以查看所有可用块设备的信息: Disk /dev/nvme0n1: 119.24 GiB, 128035676160 bytes, 250069680 sectors ... Device Start End Sectors Size Type /dev/nvme0n1p1 512 33279 32768 16M Linux filesystem /dev/nvme0n1p2 33280 1057279 1024000 500M Linux filesystem /dev/nvme0n1p128 34 511 478 239K BIOS boot Partition table entries are not in disk order. 输入 cfdisk /dev/nvme0n1 进入分区工具: 使用上下键选择分区,左右键选择要操作的选项。选中 Free space,使用 [New] 选项建立新的分区,输入分区大小,例如:32G: 本例计划为 overlay 分配 32G,为 docker 分配 32G,剩余全部分配给 data: 使用 [Write] 选项将结果写入分区表,并在确认处输入 yes 提交: 提交完毕后使用 [Quit] 选项退出分区工具。再次输入 fdisk -l 可以查看所有可用块设备的信息: Disk /dev/nvme0n1: 119.24 GiB, 128035676160 bytes, 250069680 sectors ... Device Start End Sectors Size Type /dev/nvme0n1p1 512 33279 32768 16M Linux filesystem /dev/nvme0n1p2 33280 1057279 1024000 500M Linux filesystem /dev/nvme0n1p3 1058816 68167679 67108864 32G Linux filesystem /dev/nvme0n1p4 68167680 135276543 67108864 32G Linux filesystem /dev/nvme0n1p5 135276544 250068991 114792448 54.7G Linux filesystem /dev/nvme0n1p128 34 511 478 239K BIOS boot Partition table entries are not in disk order. 分别对新分区进行格式化: mkfs.ext4 /dev/nvme0n1p3 mkfs.ext4 /dev/nvme0n1p4 mkfs.ext4 /dev/nvme0n1p5 将 /dev/nvme0n1p3 挂载至 /mnt/nvme0n1p3: mount /dev/nvme0n1p3 /mnt/nvme0n1p3 将 /overlay 分区数据全部复制到 /mnt/nvme0n1p3 中: cp -R /overlay/* /mnt/nvme0n1p3/ 以上完成后,进入 OpenWrt 管理后台,在 系统 > 挂载点 菜单的 挂载点 处,单击 添加 按钮添加挂载点,将 /dev/nvme0n1p3 挂载为 /overlay,将 /dev/nvme0n1p4 挂载为 /opt,将 /dev/nvme0n1p5 挂载为 /data: 单击 保存&应用 后重启路由器,重启完毕后在命令行输入 df -h 可以看出所有分区均成功挂载: Filesystem Size Used Available Use% Mounted on ... /dev/nvme0n1p3 31.2G 87.9M 26.6G 0% /overlay overlayfs:/overlay 31.2G 87.9M 26.6G 0% / /dev/nvme0n1p4 31.2G 356.0K 29.6G 0% /opt /dev/nvme0n1p5 53.6G 24.0K 50.8G 0% /data ... 在 /data 目录中创建用于字体的目录: mkdir /data/fonts 下载 CJK 相关字体至该目录,例如:Noto Sans CJK。 在 /data 目录中创建用于 Jellyfin 的目录: mkdir /data/docker mkdir /data/docker/jellyfin mkdir /data/docker/jellyfin/config mkdir /data/docker/jellyfin/config/fonts mkdir /data/docker/jellyfin/cache mkdir /data/docker/jellyfin/media mkdir /data/docker/jellyfin/media/nas mkdir /data/docker/jellyfin/media/nas/disk1 mkdir /data/docker/jellyfin/media/nas/disk2 mkdir /data/docker/jellyfin/media/nas/disk3 mkdir /data/docker/jellyfin/media/nas/disk4 由于在 Docker 中需要使用 1000:1000 作为 UID 和 GID 运行 Jellyfin,需要将 jellyfin 目录修改为对应所有者: chown -R 1000:1000 /data/docker/jellyfin/ 进入命令行,输入如下命令将 NAS 上配置好的共享文件夹挂载到 Jellyfin 的相关目录: mount.nfs -w 192.168.5.10:/volume1/Disk1 /data/docker/jellyfin/media/nas/disk1 -o nolock mount.nfs -w 192.168.5.10:/volume2/Disk2 /data/docker/jellyfin/media/nas/disk2 -o nolock mount.nfs -w 192.168.5.10:/volume3/Disk3 /data/docker/jellyfin/media/nas/disk3 -o nolock mount.nfs -w 192.168.5.10:/volume4/Disk4 /data/docker/jellyfin/media/nas/disk4 -o nolock 为了保证每次启动软路由时能够自动挂载,请将上述内容添加至 系统 > 启动项 菜单下的 本地启动脚本 文本框的 exit 0 之前: Jellyfin 部署 在 OpenWrt 上安装 Jellyfin 需要使用 Docker 进行部署。首先在 Docker > 镜像 菜单的 拉取镜像 处填写 jellyfin/jellyfin:latest,然后单击 拉取: 拉取完毕后即可在 镜像概览 处查看已下载的镜像: 进入软路由命令行,输入 ls /dev/dri,如果输出如下则表示 CPU 支持硬件加速: card0 renderD128 为了确保在 Docker 中其他用户可以使用该设备,输入如下命令设置设备权限: chmod 777 /dev/dri/* 通过 Docker > 容器 菜单,单击 添加 按钮添加容器。单击 命令行 并复制如下内容,单击 提交 解析命令行: docker run -d \ --name=jellyfin \ --hostname=jellyfin \ --pull=always \ --privileged \ --volume /data/docker/jellyfin/config:/config \ --volume /data/docker/jellyfin/cache:/cache \ --volume /data/docker/jellyfin/media:/media \ --volume /data/fonts:/usr/local/share/fonts \ --user 1000:1000 \ --net=host \ --restart=unless-stopped \ --device /dev/dri/renderD128:/dev/dri/renderD128 \ --device /dev/dri/card0:/dev/dri/card0 \ jellyfin/jellyfin 相关参数说明如下: 参数 说明 –name=jellyfin 镜像名称 –hostname=jellyfin 主机名称 –pull=always 运行前总是先拉取镜像 –privileged 特权模式 –volume /data/docker/jellyfin/config:/config 配置文件目录 –volume /data/docker/jellyfin/cache:/cache 缓存文件目录 –volume /data/docker/jellyfin/media:/media 媒体文件目录 –volume /data/fonts:/usr/local/share/fonts 备用字体目录 –user 1000:1000 运行时用户和用户组 –net=host 网络类型:同宿主机相同网络 –restart=unless-stopped 重启策略:在容器退出时总是重启容器 –device /dev/dri/renderD128:/dev/dri/renderD128 硬件加速设备 –device /dev/dri/card0:/dev/dri/card0 硬件加速设备 如果 总是先拉取镜像 未成功自动勾选,可以手动勾选确保运行前拉取最新镜像。单击 提交 创建容器。创建完毕后容器列表即出现 Jellyfin 容器: 勾选 Jellyfin 容器,单击 启动 按钮启动容器: Jellyfin 配置 通过 http://192.168.5.1:8096 进入 Jellyfin,首选显示语言 选择 汉语(简化字): 单击 下一个,根据个人情况设置 用户名 和 密码: 单击 下一个,设置媒体库: 单击 + 添加媒体库: 根据实际情况进行配置: 选择 内容类型 并填写 显示名称。 在 文件夹 中添加所有包含当前类型媒体的文件夹。 首选下载语言 选择 Chinese。 国家/地区 选择 People's Republic of China。 取消勾选 元数据下载器 和 图片获取程序 中所有选项。 其他设置暂时保持默认。 单击 下一个,设置首选元数据语言: 单击 下一个,设置远程访问: 单击 下一个,完成设置: 单击 完成 进入登录界面: 进入系统后,单击左侧菜单按钮,选择 管理 > 控制台 菜单。进入 控制台 后,选择 服务器 > 播放 菜单。将 转码 中的 硬件加速 选择为 Video Acceleration API (VAAPI),注意确认 VA-API 设备 是否为 /dev/dri/renderD128,并在 启用硬件解码 勾选所有媒体类型。注意确认 硬件编码选项 中的 启用硬件编码 选项已勾选。 提示 根据官方文档说明,针对部分 CPU(例如:N5105)需要勾选 启用低电压模式的 Intel H.264 硬件编码器以确保硬件加速正常工作。 在 服务器 > 播放 菜单中,勾选 启用备用字体,将 备用字体文件路径 设置为 /usr/local/share/fonts。 TMM 刮削 tinyMediaManager 是一个用 Java/Swing 编写的媒体管理工具,它可以为多种媒体服务器提供元数据。TMM 提供了多个平台的客户端,但为了多客户端刮削时数据共享,本例也使用 Docker 进行安装。 在软路由 /data 目录中创建用于 TMM 的目录: mkdir /data/docker/tinymediamanager mkdir /data/docker/tinymediamanager/config mkdir /data/docker/tinymediamanager/media mkdir /data/docker/tinymediamanager/media/nas mkdir /data/docker/tinymediamanager/media/nas/disk1 mkdir /data/docker/tinymediamanager/media/nas/disk2 mkdir /data/docker/tinymediamanager/media/nas/disk3 mkdir /data/docker/tinymediamanager/media/nas/disk4 进入命令行,输入如下命令将 NAS 上配置好的共享文件夹挂载到 TMM 的相关目录: mount.nfs -w 192.168.5.10:/volume1/Disk1 /data/docker/tinymediamanager/media/nas/disk1 -o nolock mount.nfs -w 192.168.5.10:/volume2/Disk2 /data/docker/tinymediamanager/media/nas/disk2 -o nolock mount.nfs -w 192.168.5.10:/volume3/Disk3 /data/docker/tinymediamanager/media/nas/disk3 -o nolock mount.nfs -w 192.168.5.10:/volume4/Disk4 /data/docker/tinymediamanager/media/nas/disk4 -o nolock 为了保证每次启动软路由时能够自动挂载,请将上述内容添加至 系统 > 启动项 菜单下的 本地启动脚本 文本框的 exit 0 之前。 在 Docker > 镜像 菜单的 拉取镜像 处填写 romancin/tinymediamanager:latest-v4,然后单击 拉取。 通过 Docker > 容器 菜单,单击 添加 按钮添加容器。单击 命令行 并复制如下内容,单击 提交 解析命令行: docker run -d \ --name=tinymediamanager \ --hostname=tinymediamanager \ --pull=always \ --privileged \ --volume /data/docker/tinymediamanager/config:/config \ --volume /data/docker/tinymediamanager/media:/media \ --user root:root \ --env ENABLE_CJK_FONT=1 \ --publish 5800:5800 \ --restart=unless-stopped \ romancin/tinymediamanager:latest-v4 如果 总是先拉取镜像 未成功自动勾选,可以手动勾选确保运行前拉取最新镜像。单击 提交 创建容器。勾选 TMM 容器,单击 启动 按钮启动容器。 安装完毕后重启容器。通过 http://192.168.5.1:5800 进入 TMM: 根据向导进行配置,设置中文界面后需要重启容器生效。 警告 PT 用户注意,不要开启任何自动重命名,不要将 NFO 保存为与媒体文件相同的文件名,避免覆盖原始内容从而导致做种错误。 根据个人喜好配置好 TMM 后即可对媒体文件进行刮削了,在此不再详细展开刮削过程。由于原始文件的命名可能导致自动获取的信息有误,因此建议对每一个媒体文件刮削结果进行人工复核。 提示 Docker 版本 TMM 不支持输入中文,在通过 Clipboard 内外传输剪切板时中文也会出现乱码,且目前暂时无法修复。 4.0 之后版本的 TMM 免费版不再支持自动下载字幕,由于 TMM 采用 Open Subtitles,对于有需要双语字幕和特效字幕的同学并不友好。建议还是自行手动下载字幕并放置在媒体文件中,在此提供几个不错的字幕下载网站: 伪射手:https://assrt.net SubHD:https://subhd.tv/sub/new 字幕组(需注册):https://www.yysub.net/subtitle 测试 经过 TMM 刮削后,Jellyfin 即可自动识别元数据,示例电影的详细信息如下如所示: 单击播放后,通过播放信息查看,已经可以使用 Jellyfin 实现转码在线播放: 测试完成后即可在各个终端安装相应的的客户端: iPhone & iPad & Apple TV:建议使用 Swiftfin,官方应用,原生界面体验,应用商店直接下载安装。 Android 手机:建议使用 Findroid,第三方应用,原生界面体验,应用商店直接下载安装,非原生 Android 系统可以在 Github 页面下载离线 apk 文件安装。 Android TV:建议使用 Jellyfin for Android TV,官方应用,应用商店直接下载安装,非原生 Android 系统可以在 Github 页面下载离线 apk 文件安装。 可以在官方客页面探索更多官方和第三方客户端。在电视盒子等仅用于播放视频的设备上,可以尝试启用 Direct Play,当然也需要根据电视盒子的特性进行调整,避免部分格式的视频和音频无法正常解析。

2023/1/24
articleCard.readMore

自私和贪婪 (Selfish and Greedy)

适当的自私,但不应贪婪。 学了这么多年管理,除了印象最深且还能在工作中不时提起的马斯洛需求层次理论之外,还有就是亚当斯密的经济人假设了。经济人假设每个人都以自身利益最大化为目标,我称这即为自私。社会人假设人的最大动机是社会需要,我认为这也没有逃离自私的范畴,只是需求的类型有些差异而已。苟且和远方,不分贵贱,亦可兼得,但想要的太多,抑或不劳而获,甚至痴心妄想,那这就是贪婪。 自私 不像会纠结到底是人性本善还是人性本恶,我认为人天生就是自私的。社会是由一个个人组成,脱离个体谈集体是无效的,当个体都无法被满足时,集体的功能不可能正常。我无法做到囚徒困境在神学院学生所做出的选择 1,那是一种超脱的信仰的力量,我肃穆敬仰,但恕我如实做不到。 不为恶 不为恶是我毕生的信条,执行起来不易。通俗些就是不给别人添堵,但并不是麻烦别人就是添堵,有时候就是需要去麻烦别人,我称这样的人为朋友。我认为不为恶是自私的底线,切莫损人利己,损人不利己就更可恨了。正道成功很难,邪门歪道却很容易让你误入歧途,但弯道超车未尝不可,至少我认为适合我这种在一些专业领域并不是专业的人。 诗和远方的动力 自私会是一种动力,而且不与社会主流价值观相违背。例如:我想买一辆越野车(这是真话,真的想买)和我要为世界和平奋斗终身(这也不是假话)。理想不分贵贱,看起来第一个是满足我一己私欲,第二个是利他主义,很有可能两者对于当下我的要求都是努力提升个人的专业素养,但第一个会更能促进我达成提高个人能力的目的。因为从提升个人能力到诗和远方,第一个离我会更近些,来的更实在些,更会让我更有动力些。同时,我认为这些“自私”的小目标积累起来有助于我达成“宏伟”的大目标。 贪婪 我想我并不贪婪,说实话要不是最近遇到一些事情,我都从来没认真审视过“贪婪”二字。当你脑海中都没有这么一个概念,你怎么可能会有这种想法呢。遇到的那件小事告诉我,贪婪容易出现在当你获得一定成就时,或许是被喜悦冲昏了头脑,在荷尔蒙的激发下会让我们变得更胆大妄为。我认为保持冷静,时不时的多给自己泼几盆冷水,多照照镜子,会让你更清楚你是谁。 原罪 七宗罪中有贪婪,没有自私。我想是因为贪婪是一种“过分”的追求,而自私并不是那么“过分”。我有很多想要的东西,至少我认为不过分,但我也会知足,因为知足常乐,人生嘛,最重要的就是开心。我会把贪婪和机会主义并列在一起,一方面机会主义的行为常常就是不顾及他人感受的添堵或为恶,另一方面机会主义所展现出的形势并不一定是“机会”。抓住机会很重要,但审视机会也不容小觑,莫让“机会”冲昏了头脑。 量力而为 贪婪是无休止的自私,不切实际的自私,贪得无厌描述的恰到其实。士兵突击中团长对许三多说的话我觉得放在这很贴切:“想要和得到,中间还有两个字,那就是要做到,你只有做到,你才能得到嘛”。认清自己,量力而为,清楚想要什么需要付出多少,然后去奋斗,去努力,才有可能得到。也许有人会说,你不做,怎么知道做不到?这句话我也挑不出什么毛病,但我想一个人的能力除了能够洞察机会,能认清现实也是很牛的一批。 我的选择 适当的自私。有底线,通过自我认知和自我实践,拼搏奋斗,承担责任,包容利他。有底线是明确大是大非,不为恶。自我认知和实践是认清自己和所处的环境,切莫空想和冲动。拼搏奋斗是说要去做,努力去做,才有机会自我实现。承担责任是说不要什么都是“关我屁事”和“关你屁事”,我们是自己,也是社会的一员,有权利,也有义务和责任,该承担还是要承担,这才像个爷们。包容利他我想必然就会是这样做之后超越极端自私的结果。 印象中应该是在耶鲁大学公开课 Justice: What’s The Right Thing To Do 中有讲到,神学院学生的结果为均不检举对方。 ↩︎

2022/9/11
articleCard.readMore

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

在之前的博客「投票公平合理吗?」中已经得到了一个令人沮丧的结论:只有道德上的相对民主,没有制度上的绝对公平。投票是对不同选项或个体的排序,在投票中我们关注更多是相对位置这样定性的结论,例如:积分前三名的同学才能进入下一环节。但有的时候我们不光想知道不同选项之间的先后顺序,还想了解不同选项之间的差异大小,这时我们就需要设计更精细的方法进行定量分析。 基础评分和排名 直接评分 从小到大被评分最多的应该就是考试了,100,120 或是 150,这三个数字应该从小学一年级一直“陪”我们走过十几载青春。考试的评分算法简单且容易区分,整个系统设置了一个总分,根据不同的表现进行加分或扣分,统计最终得分作为最后的评分。一般情况下成绩是一个近似正态分布的偏态分布,如下图所示。 如果成绩近似正态分布(如上图-中),则说明本次考试难度分布较为均衡;如果成绩分布整体向左偏(如上图-左),则说明本次考试较为困难,学生成绩普遍偏低;如果成绩分布整体向右偏(如上图-右),则说明本次考试较为容易,学生成绩普遍偏高。 除此之外,也可能出现双峰分布,以及峰的陡峭和平缓都能反应考试的不同问题,在此就不再一一展开说明。一般情况下,考试的最终成绩已经能够很好地对学生的能力进行区分,这也正是为什么一般情况我们不会对考试分数做二次处理,而是直接使用。 加权评分 在现实生活中,不同的问题和任务难易程度不同,为了保证「公平」,我们需要赋予困难的任务更多的分数。这一点在试卷中也会有体现,一般而言判断题会比选择题分数更低,毕竟随机作答,判断题仍有 50% 的概率回答正确,但包含四个选项的选择题却仅有 25% 概率回答正确。 加权评分在问题和任务的难易程度与分值之间通过权重进行平衡,但权重的制定并不是一个容易的过程,尤其是在设置一个兼顾客观、公平、合理等多维度的权重时。 考虑时间的评分和排名 Delicious 最简单直接的方法是在一定的时间内统计投票的数量,得票数量高的则为更好的项目。在旧版的 Delicious 中,热门书签排行榜则是根据过去 60 分钟内被收藏的次数进行排名,每 60 分钟重新统计一次。 这种算法的优点是:简单、容易部署、更新快;缺点是:一方面,排名变化不够平滑,前一个小时还排名靠前的内容,往往第二个小时就一落千丈,另一方面,缺乏自动淘汰旧项目的机制,某些热门内容可能会长期占据排行榜前列。 Hacker News Hacker News 是一个可以发布帖子的网络社区,每个帖子前面有一个向上的三角形,如果用户觉得这个内容好,点击一下即可投票。根据得票数,系统自动统计出热门文章排行榜。 Hacker News 使用分数计算公式如下: $$ Score = \dfrac{P - 1}{\left(T + 2\right)^G} \label{eq:hacker-news} $$ 其中,$P$ 表示帖子的得票数,减去 $1$ 表示忽略发帖人的投票;$T$ 表示当前距离发帖的时间(单位为小时),加上 $2$ 是为了防止最新的帖子分母过小;$G$ 为重力因子,即将帖子排名被往下拉的力量,默认值为 $1.8$。 在其他条件不变的情况下,更多的票数可以获得更高的分数,如果不希望“高票数”帖子和“低票数”帖子之间差距过大,可以在式 $\ref{eq:hacker-news}$ 的分子中添加小于 $1$ 的指数,例如:$\left(P - 1\right)^{0.8}$。在其他条件不变的情况下,随着时间不断流逝,帖子的分数会不断降低,经过 24 小时后,几乎所有帖子的分数都将小于 $1$。重力因子对于分数的影响如下图所示: 不难看出,$G$ 值越大,曲线越陡峭,排名下降的速度越快,意味着排行榜的更新速度越快。 Reddit 不同于 Hacker News,Reddit 中的每个帖子前面都有向上和向下的箭头,分别表示"赞成"和"反对"。用户点击进行投票,Reddit 根据投票结果,计算出最新的热点文章排行榜。 Reddit 关于计算分数的代码可以简要总结如下: from datetime import datetime, timedelta from math import log epoch = datetime(1970, 1, 1) def epoch_seconds(date): td = date - epoch return td.days * 86400 + td.seconds + (float(td.microseconds) / 1000000) def score(ups, downs): return ups - downs def hot(ups, downs, date): s = score(ups, downs) order = log(max(abs(s), 1), 10) sign = 1 if s > 0 else -1 if s < 0 else 0 seconds = epoch_seconds(date) - 1134028003 return round(order + sign * seconds / 45000, 7) 分数的计算过程大致如下: 计算赞成票和反对票的差值,即: $$ s = ups - downs $$ 利用如下公式计算中间分数,即: $$ order = \log_{10} \max\left(\left|s\right|, 1\right) $$ 其中,取 $\left|s\right|$ 和 $1$ 的最大值是为了避免当 $s = 0$ 时,无法计算 $\log_{10}{\left|s\right|}$。赞成票与反对票差值越大,得分越高。取以 $10$ 为底的对数,表示当 $s = 10$ 时,这部分为 $1$,只有 $s = 100$ 时才为 $2$,这样设置是为了减缓差值增加对总分的影响程度。 确定分数的方向,即: $$ sign = \begin{cases} 1 & \text{如果} \ s > 0 \\ 0 & \text{如果} \ s = 0 \\ -1 & \text{如果} \ s < 0 \end{cases} $$ 计算发贴时间距离 2005 年 12 月 8 日 7:46:43(Reddit 的成立时间?)的秒数,即: $$ seconds = \text{timestamp}\left(date\right) - 1134028003 $$ 计算最终分数,即: $$ score = order + sign \times \dfrac{seconds}{45000} $$ 将时间除以 $45000$ 秒(即 12.5 个小时),也就是说当前天的帖子会比昨天的帖子多约 $2$ 分。如果昨天的帖子想要保持住之前的排名,则 $s$ 值需要增加 $100$ 倍才可以。 Reddit 评分排名算法决定了 Reddit 是一个符合大众口味的社区,而不是一个适合展示激进想法的地方。因为评分中使用的是赞成票和反对票的差值,也就是说在其他条件相同的情况下,帖子 A 有 1 票赞成,0 票反对;帖子 B 有 1000 票赞成,1000 票反对,但讨论火热的帖子 B 的得分却比 帖子 A 要少。 Stack Overflow Stack Overflow 是世界排名第一的程序员问答社区。用户可以在上面提出各种关于编程的问题,等待别人回答;可以对问题进行投票(赞成票或反对票),表示这个问题是不是有价值;也可以对这个回答投票(赞成票或反对票),表示这个回答是不是有价值。 在 Stack Overflow 的页面上,每个问题前面有三个数字,分别为问题的得分、回答的数量和问题的浏览次数。 创始人之一的 Jeff Atwood 公布的评分排名的计算公式如下: $$ \dfrac{4 \times \log_{10}{Q_{views}} + \dfrac{Q_{answers} \times Q_{score}}{5} + \sum \left(A_{scores}\right)}{\left(\left(Q_{age} + 1\right) - \left(\dfrac{Q_{age} - Q_{updated}}{2}\right)\right)^{1.5}} $$ 其中: $4 \times \log_{10}{Q_{views}}$ 表示问题的浏览次数越多,得分越高,同时利用 $\log_{10}$ 减缓了随着浏览量增大导致得分变高的程度。 $\dfrac{Q_{answers} \times Q_{score}}{5}$ 表示问题的得分(赞成票和反对票之差)越高,回答的数量越多,分数越高。采用乘积的形式意味着即使问题本身的分数再高,没有人回答的问题也算不上热门问题。 $\sum \left(A_{scores}\right)$ 表示问题回答的总分数。回答总分采用了简单的加和,但实际上一个正确的回答要胜过多个无用的回答,简答的加和无法很好的区分这两种不同的情况。 $\left(\left(Q_{age} + 1\right) - \left(\dfrac{Q_{age} - Q_{updated}}{2}\right)\right)^{1.5}$ 可以改写为 $\left(\dfrac{Q_{age}}{2} + \dfrac{Q_{updated}}{2} + 1\right)^{1.5}$,$Q_{age}$ 和 $Q_{updated}$ 分别表示问题和最近一次回答的时间(单位为小时),也就是说问题时间越久远,最近一次回答时间约久远,分母就会越大,从而得分就会越小。 Stack Overflow 的评分排名算法考虑了参与程度(问题浏览次数和回答次数)、质量(问题分数和回答分数)、时间(问题时间和最近一次回答时间)等多个维度。 不考虑时间的评分和排名 上文中介绍的评分和排名方法多适用于具有时效性的信息,但是对于图书、电影等无需考虑时间因素的情况而言,则需要其他方法进行衡量。 威尔逊区间算法 在不考虑时间的情况下,以「赞成」和「反对」两种评价方式为例,通常我们会有两种最基础的方法计算得分。第一种为绝对分数,即: $$ \text{评分} = \text{赞成票} - \text{反对票} $$ 但这种计算方式有时会存在一定问题,例如:A 获得 60 张赞成票,40 张反对票;B 获得 550 张赞成票,450 张反对票。根据上式计算可得 A 的评分为 20,B 的评分为 100,所以 B 要优于 A。但实际上,B 的好评率仅有 $\dfrac{550}{550 + 450} = 55\%$,而 A 的好评率为 $\dfrac{60}{60 + 40} = 60\%$,因此实际情况应该是 A 优于 B。 这样,我们就得到了第二种相对分数,即: $$ \text{评分} = \dfrac{\text{赞成票}}{\text{赞成票} + \text{反对票}} $$ 这种方式在总票数比较大的时候没有问题,但总票数比较小时就容易产生错误。例如:A 获得 2 张赞成票,0 张反对票;B 获得 100 张赞成票,1 张反对票。根据上式计算可得 A 的评分为 $100\%$,B 的评分为 $99\%$。但实际上 B 应该是优于 A 的,由于 A 的总票数太少,数据不太具有统计意义。 对于这个问题,我们可以抽象出来: 每个用户的投票都是独立事件。 用户只有两个选择,要么投赞成票,要么投反对票。 如果投票总人数为 $n$,其中赞成票为 $k$,则赞成票的比例 $p = \dfrac{k}{n}$。 不难看出,上述过程是一个二项实验。$p$ 越大表示评分越高,但是 $p$ 的可信性取决于投票的人数,如果人数太少,$p$ 就不可信了。因此我们可以通过计算 $p$ 的置信区间对评分算法进行调整如下: 计算每个项目的好评率。 计算每个好评率的置信区间。 根据置信区间的下限值进行排名。 置信区间的本质就是对可信度进行修正,弥补样本量过小的影响。如果样本足够多,就说明比较可信,则不需要很大的修正,所以置信区间会比较窄,下限值会比较大;如果样本比较少,就说明不一定可信,则需要进行较大的修正,所以置信区间会比较宽,下限值会比较小。 二项分布的置信区间有多种计算公式,最常见的「正态区间」方法对于小样本准确性较差。1927 年,美国数学家 Edwin Bidwell Wilson 提出了一个修正公式,被称为「威尔逊区间」,很好地解决了小样本的准确性问题。置信区间定义如下: $$ \frac{1}{1+\frac{z^{2}}{n}}\left(\hat{p}+\frac{z^{2}}{2 n}\right) \pm \frac{z}{1+\frac{z^{2}}{n}} \sqrt{\frac{\hat{p}(1-\hat{p})}{n}+\frac{z^{2}}{4 n^{2}}} $$ 其中,$\hat{p}$ 表示样本好评率,$n$ 表示样本大小,$z$ 表示某个置信水平的 z 统计量。 贝叶斯平均算法 在一些榜单中,有时候会出现排行榜前列总是那些票数最多的项目,新项目或者冷门的项目很难有出头机会,排名可能会长期靠后。以世界最大的电影数据库 IMDB 为例,观众可以对每部电影投票,最低为 1 分,最高为 10 分,系统根据投票结果,计算出每部电影的平均得分。 这就出现了一个问题:热门电影与冷门电影的平均得分,是否真的可比?例如一部好莱坞大片有 10000 个观众投票,一部小成本的文艺片可能只有 100 个观众投票。如果使用威尔逊区间算法,后者的得分将被大幅拉低,这样处理是否公平,是否能反映电影的真正质量呢?在 Top 250 榜单中,IMDB 给到的评分排名计算公式如下: $$ WR = \dfrac{v}{v + m} R + \dfrac{m}{v + m} C $$ 其中,$WR$ 为最终的加权得分,$R$ 为该电影用户投票的平均得分,$v$ 为该电影的投票人数,$m$ 为排名前 250 电影的最低投票数,$C$ 为所有电影的平均得分。 从公式中可以看出,分量 $m C$ 可以看作为每部电影增加了评分为 $C$ 的 $m$ 张选票。然后再根据电影自己的投票数量 $v$ 和投票平均分 $R$ 进行修正,得到最终的分数。随着电影投票数量的不但增加 $\dfrac{v}{v + m} R$ 占的比重将越来越大,加权得分也会越来越接近该电影用户投票的平均分。 将公式写为更一般的形式,有: $$ \bar{x}=\frac{C m+\sum_{i=1}^{n} x_{i}}{C+n} $$ 其中,$C$ 为需要扩充的投票人数规模,可以根据投票人数总量设置一个合理的常数,$n$ 为当前项目的投票人数,$x$ 为每张选票的值,$m$ 为总体的平均分。这种算法称为「贝叶斯平均」。在这个公式中,$m$ 可以视为“先验概率”,每新增一次投票,都会对最终得分进行修正,使其越来越接近真实的值。 比赛评分和排名 Kaggle 积分 Kaggle 是一个数据建模和数据分析竞赛平台。企业和研究者可在其上发布数据,统计学者和数据挖掘专家可在其上进行竞赛以产生最好的模型。用户以团队形式参加 Kaggle 的比赛,团队可以仅包含自己一人,根据在每场比赛中的排名不断获取积分,用做 Kaggle 网站中的最终排名。 早期 Kaggle 对于每场比赛的积分按如下方式计算: $$ \left[\dfrac{100000}{N_{\text {teammates }}}\right]\left[\text{Rank}^{-0.75}\right]\left[\log _{10}\left(N_{\text {teams }}\right)\right]\left[\dfrac{2 \text { years - time }}{2 \text { years }}\right] $$ 在 2015 年对新的排名系统做了调整,新的比赛积分计算公式调整为: $$ \left[\dfrac{100000}{\sqrt{N_{\text {teammates }}}}\right]\left[\text{Rank}^{-0.75}\right]\left[\log _{10}\left(1+\log _{10}\left(N_{\text {teams }}\right)\right)\right]\left[e^{-t / 500}\right] $$ 其中,$N_{\text{teammates}}$ 为团队成员的数量,$\text{Rank}$ 为比赛排名,$N_{\text{teams}}$ 为参赛的团队数量,$t$ 为从比赛结束之日起过去的时间。 第一部分可以视为基础分,团队成员越少,所获得的基础分越多。从调整的文档来看,Kaggle 认为团队合作每个人的贡献程度会大于 $1 / N_{\text {teammates}}$,为了鼓励大家团队合作,Kaggle 减少了对团队人数的基础分惩罚力度。 第二部分则是根据用户在比赛中的排名得到一个小于等于 1 的系数。下图显示了不同的指数以及 $1 / \text{Rank}$ 之间的区别: 从图中可以看出,通过调节指数的大小可以控制系数随排名下降而下降的速度。整体来说,Kaggle 更加重视前几名,对于 10 名开外的选手,系数均小于 $0.2$,且差异不大。 第三部分可以理解为通过参赛的队伍数量来衡量比赛的受欢迎程度(或是在众多参赛队伍中脱颖而出的难易程度)。以 100 和 1000 支参赛队伍对比为例,根据之前的计算公式,这一部分为: $$ \begin{equation} \begin{aligned} \log_{10} \left(100\right) &= 2 \\ \log_{10} \left(1000\right) &= 3 \end{aligned} \end{equation} $$ 但随着 Kaggle 本身比赛流行度越来越高,官方认为赢得一场 1000 人的比赛并不需要比赢得一场 100 人的比赛需要多 $50\%$ 的技能,因此通过调整后的算法,这个比例调整至大约为 $25\%$。 $$ \begin{equation} \begin{aligned} \log_{10} \left(\log_{10} \left(100\right) + 1\right) &\approx 0.47 \\ \log_{10} \left(\log_{10} \left(1000\right) + 1\right) &\approx 0.6 \end{aligned} \end{equation} $$ 第四部份为时间衰减项,调整为新的计算公式后可以消除原来通过设置 2 年时限导致的积分断崖。如果任何一对个体都没有采取任何进一步的行动,那么排名不应该在任何一对个体之间发生变化。换句话说,如果整个 Kaggle 用户群停止参加比赛,他们的相对排名应该随着时间的推移保持不变。选择 $1 / 500$ 的原因是可以将旧的 2 年断崖延长到更长的时间范围,并且永远不会变为 0。 Elo 评分系统 Elo 评分系统(Elo Rating System)是由匈牙利裔美国物理学家 Arpad Elo 创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估公认的权威标准,且被广泛用于国际象棋、围棋、足球、篮球等运动。网络游戏的竞技对战系统也常采用此评分系统。 Elo 评分系统是基于统计学的一个评估棋手水平的方法。Elo 模型原先采用正态分布,但实践显明棋手的表现并非正态分布,所以现在的评分计分系统通常使用的是逻辑分布。 假设棋手 A 和 B 的当前评分分别为 $R_A$ 和 $R_B$,则按照逻辑分布,A 对 B 的胜率期望值为: $$ E_{A}=\frac{1}{1+10^{\left(R_{B}-R_{A}\right) / 400}} $$ 类似的有 B 对 A 的胜率期望值为: $$ E_{B}=\frac{1}{1+10^{\left(R_{A}-R_{B}\right) / 400}} $$ 假如一位棋手在比赛中的真实得分 $S_{A}$(胜 1 分,和 0.5 分,负 0 分)和他的胜率期望值 $E_{A}$ 不同,则他的评分要作相应的调整: $$ R_{A}^{\prime} = R_{A} + K\left(S_{A}-E_{A}\right) $$ 公式中 $R_{A}$ 和 $R_{A}^{\prime }$ 分别为棋手调整前后的评分。$K$ 值是一个极限值,代表理论上最多可以赢一个玩家的得分和失分,$K / 2$ 就是相同等级的玩家其中一方胜利后所得的分数。国际象棋大师赛中,$K = 16$;在大部分的游戏规则中,$K = 32$。通常水平越高的比赛中其 $K$ 值越小,这样做是为了避免少数的几场比赛就能改变高端顶尖玩家的排名。$E_A$ 和 $E_B$ 中的 $400$ 是让多数玩家积分保持标准正态分布的值,在 $K$ 相同的情况下,分母位置的值越大,积分变化值越小。 Glicko 评分系统 Glicko 评分系统(Glicko Rating System)及 Glicko-2 评分系统(Glicko-2 Rating System)是评估选手在比赛中(如国际象棋及围棋)的技术能力方法之一。此方法由马克·格利克曼发明,原为国际象棋评分系统打造,后作为评分评分系统的改进版本广泛应用 1。 Elo 评分系统的问题在于无法确定选手评分的可信度,而 Glicko 评分系统正是针对此进行改进。假设两名评分均为 1700 的选手 A 和 B 在进行一场对战后 A 获得胜利,在美国国际象棋联赛的 Elo 评分系统下,A 选手评分将增长 16,对应的 B 选手评分将下降 16。但是假如 A 选手是已经很久没玩,但 B 选手每周都会玩,那么在上述情况下 A 选手的 1700 评分并不能十分可信地用于评定其实力,而 B 选手的 1700 评分则更为可信。 Glicko 算法的主要贡献是“评分可靠性”(Ratings Reliability),即评分偏差(Ratings Deviation)。若选手没有评分,则其评分通常被设为 1500,评分偏差为 350。新的评分偏差($RD$)可使用旧的评分偏差($RD_0$)计算: $$ RD = \min \left(\sqrt{RD_0^2 + c^2 t}, 350\right) $$ 其中,$t$ 为自上次比赛至现在的时间长度(评分期),常数 $c$ 根据选手在特定时间段内的技术不确定性计算而来,计算方法可以通过数据分析,或是估算选手的评分偏差将在什么时候达到未评分选手的评分偏差得来。若一名选手的评分偏差将在 100 个评分期间内达到 350 的不确定度,则评分偏差为 50 的玩家的常数 $c$ 可通过解 $350 = \sqrt{50^2 + 100 c^2}$,则有 $c = \sqrt{\left(350^2 - 50^2\right) / 100} \approx 34.6$。 在经过 $m$ 场比赛后,选手的新评分可通过下列等式计算: $$ r=r_{0}+\frac{q}{\frac{1}{R D^{2}}+\frac{1}{d^{2}}} \sum_{i=1}^{m} g\left(R D_{i}\right)\left(s_{i}-E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\right) $$ 其中: $$ \begin{equation*} \begin{aligned} & g\left(R D_{i}\right) = \frac{1}{\sqrt{1+\frac{3 q^{2}\left(R D_{i}^{2}\right)}{\pi^{2}}}} \\ & E\left(s \mid r, r_{i}, R D_{i}\right) = \frac{1}{1+10\left(\frac{g\left(R D_{i}\right)\left(r_{0}-r_{i}\right)}{-400}\right)} \\ & q = \frac{\ln (10)}{400}=0.00575646273 \\ & d^{2} = \frac{1}{q^{2} \sum_{i=1}^{m}\left(g\left(R D_{i}\right)\right)^{2} E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\left(1-E\left(s \mid r_{0}, r_{i}, R D_{i}\right)\right)} \end{aligned} \end{equation*} $$ $r_i$ 表示选手个人评分,$s_i$ 表示每场比赛后的结果。胜利为 $1$,平局为 $1 / 2$,失败为 $0$。 原先用于计算评分偏差的函数应增大标准差值,进而反应模型中一定非观察时间内,玩家的技术不确定性的增长。随后,评分偏差将在几场游戏后更新: $$ R D^{\prime}=\sqrt{\left(\frac{1}{R D^{2}}+\frac{1}{d^{2}}\right)^{-1}} $$ Glicko-2 评分系统 Glicko-2 算法与原始 Glicko 算法类似,增加了一个评分波动率 $\sigma$,它根据玩家表现的不稳定程度来衡量玩家评分的预期波动程度。例如:当一名球员的表现保持稳定时,他们的评分波动性会很低,如果他们在这段稳定期之后取得了异常强劲的成绩,那么他们的评分波动性就会增加 1。 Glicko-2 算法的简要步骤如下: 计算辅助量 在一个评分周期内,当前评分为 $\mu$ 和评分偏差为 $\phi$ 的玩家与 $m$ 个评分为 $\mu_1, \cdots, \mu_m$ 和评分偏差为 $\phi_1, \cdots, \phi_m$ 的玩家比赛,获得的分数为 $s_1, \cdots, s_m$,我们首先需要计算辅助量 $v$ 和 $\Delta$: $$ \begin{aligned} v &= \left[\sum_{j=1}^{m} g\left(\phi_{j}\right)^{2} E\left(\mu, \mu_{j}, \phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\}\right]^{-1} \\ \Delta &= v \sum_{j=1}^{m} g\left(\phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\} \end{aligned} $$ 其中: $$ \begin{equation*} \begin{aligned} &g(\phi)=\frac{1}{\sqrt{1+3 \phi^{2} / \pi^{2}}}, \\ &E\left(\mu, \mu_{j}, \phi_{j}\right)=\frac{1}{1+\exp \left\{-g\left(\phi_{j}\right)\left(\mu-\mu_{j}\right)\right\}} \end{aligned} \end{equation*} $$ 确定新的评分波动率 选择一个小的常数 $\tau$ 来限制时间的波动性,例如:$\tau = 0.2$(较小的 $\tau$ 值可以防止剧烈的评分变化),对于: $$ f(x)=\frac{1}{2} \frac{e^{x}\left(\Delta^{2}-\phi^{2}-v^{2}-e^{x}\right)}{\left(\phi^{2}+v+e^{x}\right)^{2}}-\frac{x-\ln \left(\sigma^{2}\right)}{\tau^{2}} $$ 我们需要找到满足 $f\left(A\right) = 0$ 的值 $A$。解决此问题的一种有效方法是使用 Illinois 算法,一旦这个迭代过程完成,我们将新的评级波动率 $\sigma'$ 设置为: $$ \sigma' = e^{\frac{A}{2}} $$ 确定新的评分偏差和评分 之后得到新的评分偏差: $$ \phi^{\prime} = \dfrac{1}{\sqrt{\dfrac{1}{\phi^{2}+\sigma^{\prime 2}}+\dfrac{1}{v}}} $$ 和新的评分: $$ \mu^{\prime} = \mu+\phi^{\prime 2} \sum_{j=1}^{m} g\left(\phi_{j}\right)\left\{s_{j}-E\left(\mu, \mu_{j}, \phi_{j}\right)\right\} $$ 需要注意这里的评分和评分偏差与原始 Glicko 算法的比例不同,需要进行转换才能正确比较两者。 TrueSkill 评分系统 TrueSkill 评分系统是基于贝叶斯推断的评分系统,由微软研究院开发以代替传统 Elo 评分系统,并成功应用于 Xbox Live 自动匹配系统。TrueSkill 评分系统是 Glicko 评分系统的衍伸,主要用于多人游戏中。TrueSkill 评分系统考虑到了个别玩家水平的不确定性,综合考虑了各玩家的胜率和可能的水平涨落。当各玩家进行了更多的游戏后,即使个别玩家的胜率不变,系统也会因为对个别玩家的水平更加了解而改变对玩家的评分 2。 在电子竞技游戏中,特别是当有多名选手参加比赛的时候需要平衡队伍间的水平,让游戏比赛更加有意思。这样的一个参赛选手能力平衡系统通常包含以下三个模块: 一个包含跟踪所有玩家比赛结果,记录玩家能力的模块。 一个对比赛成员进行配对的模块。 一个公布比赛中各成员能力的模块。 能力计算和更新 TrueSkill 评分系统是针对玩家能力进行设计的,以克服现有排名系统的局限性,确保比赛双方的公平性,可以在联赛中作为排名系统使用。TrueSkill 评分系统假设玩家的水平可以用一个正态分布来表示,而正态分布可以用两个参数:平均值和方差来完全描述。设 Rank 值为 $R$,代表玩家水平的正态分布的两个参数平均值和方差分别为 $\mu$ 和 $\sigma$,则系统对玩家的评分即 Rank 值为: $$ R = \mu - k \times \sigma $$ 其中,$k$ 值越大则系统的评分越保守。 上图来自 TrueSkill 网站,钟型曲线为某个玩家水平的可能分布,绿色区域是排名系统的信念,即玩家的技能在 15 到 20 级之间。 下表格给出了 8 个新手在参与一个 8 人游戏后 $\mu$ 和 $\sigma$ 的变化。 姓名 排名 赛前 $\mu$ 赛前 $\sigma$ 赛后 $\mu$ 赛后 $\sigma$ Alice 1 25 8.3 36.771 5.749 Bob 2 25 8.3 32.242 5.133 Chris 3 25 8.3 29.074 4.943 Darren 4 25 8.3 26.322 4.874 Eve 5 25 8.3 23.678 4.874 Fabien 6 25 8.3 20.926 4.943 George 7 25 8.3 17.758 5.133 Hillary 8 25 8.3 13.229 5.749 第 4 名 Darren 和第 5 名 Eve,他们的 $\sigma$ 是最小的,换句话说系统认为他们能力的可能起伏是最小的。这是因为通过这场游戏我们对他们了解得最多:他们赢了3 和 4 个人,也输给了 4 和 3 个人。而对于第 1 名 Alice,我们只知道她赢了 7 个人。 定量分析可以先考虑最简单的两人游戏情况: $$ \begin{aligned} &\mu_{\text {winner }} \longleftarrow \mu_{\text {winner }}+\frac{\sigma_{\text {winner }}^{2}}{c} * v\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right) \\ &\mu_{\text {loser }} \longleftarrow \mu_{\text {loser }}-\frac{\sigma_{\text {loser }}^{2}}{c} * v\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right) \\ &\sigma_{\text {winner }}^{2} \longleftarrow \sigma_{\text {uninner }}^{2} *\left[1-\frac{\sigma_{\text {winner }}^{2}}{c} * w\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right)\right. \\ &\sigma_{\text {loser }}^{2} \longleftarrow \sigma_{\text {loser }}^{2} *\left[1-\frac{\sigma_{\text {loser }}^{2}}{c} * w\left(\frac{\mu_{\text {winner }}-\mu_{\text {loser }}}{c}, \frac{\varepsilon}{c}\right)\right. \\ &c^{2}=2 \beta^{2}+\sigma_{\text {winner }}^{2}+\sigma_{\text {loser }}^{2} \end{aligned} $$ 其中,系数 $\beta^2$ 代表的是所有玩家的平均方差,$v$ 和 $w$ 是两个函数,比较复杂,$\epsilon$ 是平局参数。简而言之,个别玩家赢了 $\mu$ 就增加,输了 $\mu$ 减小;但不论输赢,$\sigma$ 都是在减小,所以有可能出现输了涨分的情况。 对手匹配 势均力敌的对手能带来最精彩的比赛,所以当自动匹配对手时,系统会尽可能地为个别玩家安排可能水平最为接近的对手。TrueSkill 评分系统采用了一个值域为 $(0, 1)$ 的函数来描述两个人是否势均力敌:结果越接近 0 代表差距越大,越接近 1 代表水平越接近。 假设有两个玩家 A 和 B,他们的参数为 $(\mu_A, \sigma_A)$ 和 $(\mu_B, \sigma_B)$,则函数对这两个玩家的返回值为: $$ e^{-\frac{\left(\mu_{A}-\mu_{B}\right)^{2}}{2 c^{2}}} \sqrt{\frac{2 \beta^{2}}{c^{2}}} $$ $c$ 的值由如下公式给出: $$ c^{2}=2 \beta^{2}+\mu_{A}^{2}+\mu_{B}^{2} $$ 如果两人有较大几率被匹配在一起,仅是平均值接近还不行,还需要方差也比较接近才可以。 在 Xbox Live 上,系统为每个玩家赋予的初值是 $\mu = 25$,$\sigma = \dfrac{25}{3}$ 和 $k = 3$。所以玩家的起始 Rank 值为: $$ R=25-3 \frac{25}{3}=0 $$ 相较 Elo 评价系统,TrueSkill 评价系统的优势在于: 适用于复杂的组队形式,更具一般性。 有更完善的建模体系,容易扩展。 继承了贝叶斯建模的优点,如模型选择等。 本文主要参考了阮一峰的系列文章「基于用户投票的排名算法」和钱魏的「游戏排名算法:Elo、Glicko、TrueSkill」。 https://en.wikipedia.org/wiki/Glicko_rating_system ↩︎ ↩︎ https://zh.wikipedia.org/wiki/TrueSkill评分系统 ↩︎

2022/5/22
articleCard.readMore

小记这一波裁员浪潮

一眨眼已经四月了,第一季度穿插着反反复复的疫情、说干就干的战争和悲痛万分的空难就这么过去了。还在用生活不易、活着真好的话宽慰自己的时候,互联网迎来了一大波裁员浪潮。说实话其实每年都在陆陆续续的传出各大厂的裁员消息,可今年这一波着实动静大了些。翻看已经有两三篇待完成的博客躺在草稿箱中有个把月了,工作和生活的各种乱七八糟的事情扰得自己完全没有动笔的欲望。算是为了不让博客列表页有太长断档吧,小记一下最近的心情,也让自己冷静下来思考二三。 人都去哪儿了? 在这波裁员浪潮之前,工作上除了项目本身以外,最重要的就是招聘。从最初组建团队一直到现在招聘就像一个看似永远完不成的项目。摆在我面前的招聘三大问题:我看上的人家不一定看得上我,看得上我的我不一定看得上人家,都看上了的因为各种各样的不可控因素最后还是没谈成。 有时候从源头上就捞不到人,没有简历,约不上面,不可否认一些岗位的门槛和稀缺性注定这就是一场艰难的战役。但从个人主观感受上来看,逃离互联网的人可能正在增长。这个观点没有任何调研为依据,单纯的从身边的案例有感,不具备统计显著性,但也不接受反驳。为什么要逃离,或许逃离这个词用的就不好,应该说是「离开」更好些,不然显得当下的互联网有多么水深火热一样。因为大环境、家庭、亦或是躺平的心态,都很难说,自己没处在这么一个情景中,也说不出个四五到六来。 很难说未来自己会不会离开互联网这个圈子,如果离开也很难说这个时点有多近还是多远,无论主动还是被动。只能说当下的互联网仍旧在不断给予我机会和挑战,在没有被困难打倒之前,我应该还是会在这条路上拼杀几载吧。 每天都在忙些啥? 我属于一个不太喜欢开会的人,可能我的层次还不太高吧,我认为大部分事情都没法在相对正式的会议上达成很好的决策,我更喜欢非正式的沟通和正式的记录配合达成目标。所以排除开会,每天到底都在忙些啥? 想要的太多,不舍得放手的太多 人总是贪婪的,虽说给永远比拿快乐,但有时候就喜欢圈起来个一亩三分地,当然圈的越多也越好。其实道理自己都清楚,东西太多可能哪个都做不好,但有时候执念上来了,拦都拦不住。和当年的关不掉的浏览器标签页一样,不舍得放手。还是得多学会放手,一方面是给自己减负,另一方面也是给别人机会,这怎么说得我好像有多大权力似的 😂。 时间管理 为真,是一个需要相对长远看待的问题,没有哪个真理是三两天就弄出来的。为需,才是第一重要的事情,光想着要做的多大多完美,不想想再不做点为所需的东西,团队都快没了,还谈什么理想。以一个明星项目养活俩仨探索项目,是我有时候会偷偷使用的小伎俩,先把本职的活儿干好,如果再时不时的整两个惊喜出来岂不美哉。就算没弄出来惊喜,只要没搞出来惊吓,至少我把分内的事儿做的没啥大毛病,老板也不会过来挑你啥。 但我坚信,创新才是第一动力,太本分不好。 努力不被「淘汰」? 有时候说被「淘汰」未免过分了些,组织淘汰人是「组织」认为「你」不再适合「组织」了,就像搞对象一样,「我」说「你」不是「我」喜欢的,还是有主观性的,没准儿人家到了别处还是顶梁柱嘞。怎么才能成为一代海王呢,个人观点,永无止境的学习是很重要的。 学啥?大点儿来说别学坏就好,当然好与坏,这又是个问题。我想表达的意思是,学些和工作有关的和学些和工作无关的都挺好的。两者之于工作都是有正向作用的,就比如这一年,我的这辆小摩托帮我结交了几个不错的朋友,帮我在不开心的时候变开心,开心的时候变得更开心,这还不够吗?学些和工作相关的技能那就更有益了,自己这点做的还是不太好,虽然能跟得上前沿的脚步,但也都是略知皮毛,开拓深耕些不熟悉的地方还是挺费力费时的。 怎么学?还记得之前大学时候管理学课上老师总说的一句话:读万卷书不如行万里路,行万里路不如交万名友。对于社牛症患者,Social 大可不必多说。对于有交通便利的朋友(比如有辆小摩托 🏍 的我 😎),多和大自然接触接触还是舒服的,切记,安全第一。所有里面,效果相对不足的可能就是读书了,读书真的是成本很低的事情,时间可能是最需要消耗的东西,读书又确确实实是一件性价比很高的事情。这一年读的不多,暂且就拿客观因素当当借口吧。对于读书有一点我很受用,就是总结整理,吸进来些东西,消化消化,再创造些东西,不一定非要有多深的思考,哪怕是简单的归纳整理,写出来感觉一下子就不一样了。 活到老,学到老,这话在理。 万一万一,退路在哪里? 我是一个不太喜欢承受高风险的人,所以往往做事之前我都会尽可能想到最差的情况,多想想退路,好像也没什么毛病,是吧?万一万一,命运不济,有此一劫,我又该如何度过?一些具有高不可替代性的人不知道会不会思考这个问题,他们出现万一万一的概率太小了,我自认为还不是他们那种人,换句话,公司没了我照样转,往小了说,部门没了我照样转,最多卡个把月。 真正的退路也是需要很多客观条件支持的,比如雄厚的家庭资产,那我会选择回家继承我的百亩良田。再比如另一门硬手艺,大不了我换个行业,依旧可以做的风风火火。很不幸,这俩我都不符合,之前有时会也会问自己,如果有一天不能写代码了,我还能干点啥?我的菜属实做的还不错,开个饭馆没准儿能凑合,然而你抵挡不了天灾,疫情对餐饮、旅游、文娱的冲击真的不小。 那怎么办,没能力躺平,没实力单干,就只能坐等幸运女神眷顾了吗?作为一个「普通」人,我想也还是有些法子的,至少能让我们在遇见万一万一的时候可以更加从容的面对,快速顺利的找到下家,度过去,这不也是一条好路吗。时刻保持警惕感、夯实自己的核心能力、关注行业前沿动态、拓展个人认知范围,几点看似废话的东西我认为挺有用的,重点看你怎么去落实了。 光说不练假把式。

2022/4/10
articleCard.readMore

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

本文主要参考自 SIFT meets CNN: A decade survey of instance retrieval 1 和 Deep Learning for Instance Retrieval: A Survey 2。 基于内容的图像检索(Content-based image retrieval,CBIR),属于图像分析的一个研究领域。基于内容的图像检索目的是在给定查询图像的前提下,依据内容信息或指定查询标准,在图像数据库中搜索并查找出符合查询条件的相应图片 3。 根据不同的视觉表示方法,可以将基于内容的图像检索方法分为两类:基于 SIFT 特征的和基于深度学习的。基于 SIFT 特征的方法分为如下 3 类: 使用小型编码本:视觉词汇少于几千个,紧凑向量在降维和编码之前生成。 使用中型编码本:考虑到 BoW 的稀疏性和视觉词汇的低区分度,使用倒排索引和二进制签名方法。准确率和效率之间的权衡是该算法的主要影响因素。 使用大型编码本:考虑到 BoW 直方图的稀疏性和视觉词汇的高区分度,在算法中使用了倒排索引和存储友好型的签名方式。在编码本生成和编码中使用了类似的方法。 基于深度学习的方法分为如下 3 类: 混合型方法:图像块被多次输入到 CNN 中用于特征提取。编码与索引方法和基于 SIFT 的检索方法类似。 使用预训练的模型:通过在类似 ImageNet 的大数据集预训练的 CNN 模型进行单通道特征提取,同时使用紧凑编码和池化技术。 使用微调的模型:在图像与目标数据具有相似分布的训练集上对 CNN 模型进行微调。通过端到端的方法利用 CNN 模型进行单通道特征提取。这种视觉表示方法可以提升模型的区分能力。 各类模型的异同点如下表所示: 方法类型 检测 描述 编码 维度 索引 基于 SIFT 大编码本 DoG, Hessian-Affine, dense patches 等 局部不变 描述,例如: SIFT Hard, soft 高 倒排索引 中编码本 Hard, soft, HE 中 倒排索引 小编码本 VLAD, FV 低 ANN 模型 基于深度学习 混合模型 图像块 CNN 特征 VLAD, FV, pooling 不定 ANN 模型 预训练模型 预训练 CNN 模型的列特征或全连接层 VLAD, FV, pooling 低 ANN 模型 微调模型 从预训练 CNN 模型中端到端提取的全局特征 低 ANN 模型 基于内容的图像检索里程碑节点如下图所示: 基于 SIFT 的图像检索 基于 SIFT 的图像检索流程如下图所示: 局部特征提取:假设有一个包含 $N$ 张图片的集合 $\mathcal{G}$。指定一个特征检测器,从稀疏的兴趣点或密集的图像块中提取局部描述符。我们用 $D$ 表示局部描述符,$\left\{f_{i}\right\}_{i=i}^{D}, f_{i} \in \mathbb{R}^{p}$ 表示图像中被检测的区域。 编码本训练:基于 SIFT 的方法需要离线训练一个编码本。编码本中的每个视觉词汇位于子空间的中心,称为 Voronoi Cell。一个更大的密码本对应一个更精细的划分,进而产生更有区分性的视觉词汇,反之亦然。假设存在一个用无标注数据集训练的局部描述符池 $\mathcal{F} \equiv\left\{f_{i}\right\}_{i=1}^{M}$,一个基准方法是利用 K-means 将 $M$ 个点聚类成 $K$ 个簇,这 $K$ 个视觉词汇则构成了大小为 $K$ 的编码本。 特征编码:一个局部描述符 $f_{i} \in \mathbb{R}^{p}$ 通过特征编码过程 $f_{i} \rightarrow g_{i}$ 被映射到嵌入特征 $g_{i} \in \mathbb{R}^{l}$。当使用 K-means 聚类时,$f_i$ 可以根据其与视觉词汇的距离进行编码。 局部特征提取 Harris 角点检测 特征点在图像中一般有具体的坐标,并具有某些数学特征,如局部最大或最小灰度、以及某些梯度特征等。可以通过加权的差值平方和来形式化的比较一个图像的两个区块: $$ \label{eq:e_u_v} E\left(u, v\right) = \sum_{x, y} w\left(x, y\right)\left[I\left(x + u, y + v\right)-I\left(x, y\right)\right]^{2} $$ 其中,$I$ 为待比较的图像,$\left(u, v\right)$ 为平移向量,$w\left(x, y\right)$ 是在空间上变化的权重。 根据泰勒展开,窗口平移后图像的一阶近似为: $$ \begin{aligned} I(x + u, y + v) &= I(x, y) + I_{x}(x, y) u + I_{y}(x, y) v + O\left(u^2, v^2\right) \\ & \approx I(x, y) + I_{x}(x, y) u + I_{y}(x, y) v \end{aligned} $$ 其中,$I_{x}$ 和 $I_{y}$ 是图像 $I(x, y)$ 的偏导数,那么式 $\ref{eq:e_u_v}$ 可以简化为: $$ \begin{aligned} E\left(u, v\right) & \approx \sum_{x, y} w(x, y)\left[I_{x}(x, y) u + I_{y}(x, y) v\right]^{2} \\ &=\left[\begin{array}{ll} u & v \end{array}\right] M(x, y)\left[\begin{array}{c} u \\ v \end{array}\right] \end{aligned} $$ 其中, $$ M=\sum w(x, y)\left[\begin{array}{cc} I_{x}^{2} & I_{x} I_{y} \\ I_{x} I_{y} & I_{y}^{2} \end{array}\right] $$ 通过求解 $M$ 的特征向量,我们可以获得 $E(u, v)$ 最大和最小增量的方向,对应的特征值则为实际的增量值。Harris 角点检测方法对每一个窗口定义了一个 $R$ 值: $$ R = \operatorname{det} M - k (\operatorname{trace} M)^{2} $$ 其中,$\operatorname{det} M = \lambda_1 \lambda_2$ 是矩阵 $M$ 的行列式,$\operatorname{trace} M = \lambda_1 + \lambda_2$ 是矩阵 $M$ 的迹,$\lambda_1$ 和 $\lambda_2$ 为矩阵 $M$ 特征值,$k$ 为经验常数,通常取值为 $[0.04, 0.06]$。特征值决定了当前区域是一个角、边还是平坦区域。 当 $\lvert R \rvert$ 比较小时,$\lambda_1$ 和 $\lambda_2$ 均比较小,则区域为平坦区域。 当 $R < 0$ 时,$\lambda_1 \gg \lambda_2$ 或 $\lambda_1 \ll \lambda_2$,则区域为边。 当 $R$ 较大时,$\lambda_1$ 和 $\lambda_2$ 都很大且 $\lambda_1 \sim \lambda_2$,则区域为角。 Harris 角点检测方法具备如下性质: $k$ 影响被检测角点数量:增大 $k$ 将减小 $R$,从而减少被检测角点的数量,反之亦然。 对亮度和对比度的变化不敏感:Harris 角点检测对图像进行微分运算,微分运算对图像密度的拉升或收缩和对亮度的抬高或下降不敏感。 具有旋转不变性:Harris 角点检测算子使用的是角点附近区域的灰度二阶矩矩阵。而二阶矩矩阵可以表示成一个椭圆,椭圆的长短轴正是二阶矩矩阵特征值平方根的倒数。当特征椭圆转动时,特征值并不发生变化,所以判断角点的 $R$ 值也不发生变化。 不具有尺度不变性:如下图所示,当左图被缩小时,在检测窗口尺寸不变的前提下,在窗口内所包含图像的内容是完全不同的。左侧的图像可能被检测为边缘或曲线,而右侧的图像则可能被检测为一个角点。 利用 Harris 方法检测角点的效果如下图所示(代码详见这里): 尺度空间极值检测 为了使检测到的特征点具备尺度不变性,使能够在不同尺度检测到尽可能完整的特征点或关键点,则需要借助尺度空间理论来描述图像的多尺度特征。相关研究证明高斯卷积核是实现尺度变换的唯一线性核。因此可用图像的高斯金字塔表示尺度空间,而且尺度规范化的 LoG 算子具有尺度不变性,在具体实现中,可用高斯差分( DoG)算子近似 LoG 算子,在构建的尺度空间中检测稳定的特征点。 在图像处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获取多尺度下的空间表示序列,对这些空间序列提取某些特征描述子,抽象成特征向量,实现图像在不同尺度或不同分辨率的特征提取。尺度空间中各尺度图像的模糊程度逐渐变大,模拟人在由近到远时目标在人眼视网膜上的成像过程。而且尺度空间需满足一定的不变性,包括图像灰度不变性、对比度不变性、平移不变性、尺度不变性以及旋转不变性等。在某些情况下甚至要求尺度空间算子具备仿射不变性。 图像的尺度空间 $L(x, y, \sigma)$ 可以定义为图像 $I(x, y)$ 与可变尺度的高斯函数 $G(x, y, \sigma)$ 的卷积: $$ \begin{aligned} L(x, y, \sigma) &= G(x, y, \sigma) * I(x, y) \\ G(x, y, \sigma) &= \frac{1}{2 \pi \sigma^{2}} e^{-\frac{x^{2}+y^{2}}{2 \sigma^{2}}} \end{aligned} $$ 其中,$\sigma$ 是尺度变化因子,大小决定图像的平滑程度,值越大图像越模糊。大尺度对应图像的概貌特征,小尺度对应图像的细节特征。一般根据 $3 \sigma$ 原则,高斯核矩阵的大小设为 $(6 \sigma+1) \times(6 \sigma+1)$。 尺度归一化的高斯拉普拉斯函数 $\sigma^{2} \nabla^{2} G$ 可以提取稳定的特征,高斯差分函数(Difference-of-Gaussian,DoG) 4 与尺度归一化的高斯拉普拉斯函数近似: $$ \begin{aligned} LoG &= \sigma^{2} \nabla^{2} G \\ DoG &= G(x, y, \sigma_2) - G(x, y, \sigma_1) \end{aligned} $$ 利用差分近似替代微分,有: $$ \sigma \nabla^{2} G=\frac{\partial G}{\partial \sigma} \approx \frac{G(x, y, k \sigma)-G(x, y, \sigma)}{k \sigma-\sigma} $$ 因此有: $$ G(x, y, k \sigma)-G(x, y, \sigma) \approx(k-1) \sigma^{2} \nabla^{2} G $$ 其中,$k - 1$ 是个常数,不影响极值点的检测,DoG 和 LoG 的对比图如下: 在使用高斯金字塔构建尺度空间时,主要包括两部分:对图像做下采样,以及对图像做不同尺度的高斯模糊。对图像做降采样得到不同尺度的图像,也就是不同的组(Octave),后面的 Octave(高一层的金字塔)为上一个 Octave(低一层的金字塔)下采样得到,图像宽高分别为上一个 Octave 的 1/2 。每组(Octave)又分为若干层(Interval),通过对图像做不同尺度的高斯模糊得到,如下图所示: 在由图像金字塔表示的尺度空间中,Octave 由原始图像的大小和塔顶图像的大小决定: $$ Octave = \log_2 \left(\min\left(w_b, h_b\right)\right) - \log_2 \left(\min\left(w_t, h_t\right)\right) $$ 其中,$w_b$ 和 $h_b$ 分别为原始图像的宽和高,$w_t$ 和 $h_t$ 分别为金字塔顶部图像的宽和高。 尺度参数 $\sigma$ 的取值与金字塔的组数和层数相关,设第一组第一层的尺度参数取值为 $\sigma \left(1, 1\right)$,一般取值为 $1.6$,则第 $m$ 组第 $n$ 层的取值为: $$ \sigma(m, n)=\sigma\left(1, 1\right) \cdot 2^{m-1} \cdot k^{n-1}, \quad k=2^{1/S} $$ 其中,$S$ 为金字塔中每组的有效层数,$k=2^{1/S}$ 是变化尺度因子。在检测极值点前对原始图像的高斯平滑会导致图像高频信息的丢失,所以在建立尺度空间之前,先利用双线性插值将图像扩大为原来的两倍,以保留原始图像信息,增加特征点数量。 为了寻找 DoG 尺度空间的极值点,每一个采样点要和其所有邻域像素相比较,如下图所示,中间检测点与其同尺度的 8 个邻域像素点以及上下相邻两层对应的 9×2 个像素点一共 26 个点作比较,以确保在图像空间和尺度空间都能检测到极值点。一个像素点如果在 DoG 尺度空间本层及上下两层的 26 邻域中取得最大或最小值时,就可以认为该点是图像在该尺度下的一个特征点。 在极值比较的过程中,每一组差分图像的首末两层是无法比较的,为了在每组中检测 $S$ 个尺度的极值点,则 DoG 金字塔每组须有 $S+2$ 层图像,高斯金字塔每组须有 $S+3$ 层图像。另外,在下采样时,高斯金字塔中后一组(Octive)的底层图像是由前一组图像的倒数第 3 张图像($S+1$ 层)隔点采样得到。这样也保证了尺度变化的连续性,如下图所示: 关键点定位 在 DoG 尺度空间检测到的极值点是离散的,通过拟合三元二次函数可以精确定位关键点的位置和尺度,达到亚像素精度。同时去除低对比度的检测点和不稳定的边缘点(因为 DoG 算子会产生较强的边缘响应),以增强匹配稳定性,提高抗噪声能力。 离散空间的极值点并不是真正的极值点,如下图所示一维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation)。 假设在尺度为 $\sigma$ 的尺度图像 $D(x, y, \sigma)$ 检测到一个局部极值点,空间位置为 $(x, y, \sigma)$。根据上图直观可知,它只是离散情况下的极值点,而连续情况下,极值点可能坐落在 $(x, y, \sigma)$ 附近,设连续情况的正真极值点偏离 $(x, y, \sigma)$ 的坐标为 $(\Delta x, \Delta y, \Delta \sigma)$。则对 $D(x + \Delta x, y + \Delta y, \sigma + \Delta \sigma)$ 在 $D(x, y, \sigma)$ 处进行泰勒展开(保留二阶),有: $$ \begin{split} & \ D(x+\Delta x, y+\Delta y, \sigma+\Delta \sigma) \\ \approx & \ D(x, y, \sigma)+\left[\begin{array}{lll} \frac{\partial D}{\partial x} & \frac{\partial D}{\partial y} & \frac{\partial D}{\partial \sigma} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right] \\ & +\frac{1}{2}\left[\begin{array}{lll} \Delta x & \Delta y & \Delta \sigma \end{array}\right]\left[\begin{array}{lll} \frac{\partial D^{2}}{\partial x^{2}} & \frac{\partial D^{2}}{\partial x \partial y} & \frac{\partial^{2} D}{\partial x \partial \sigma} \\ \frac{\partial D^{2}}{\partial y \partial x} & \frac{\partial D^{2}}{\partial y^{2}} & \frac{\partial D^{2}}{\partial y \partial \sigma} \\ \frac{\partial D^{2}}{\partial \sigma \partial x} & \frac{\partial D^{2}}{\partial \sigma \partial y} & \frac{\partial D^{2}}{\partial \sigma^{2}} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right] \end{split} $$ 将上式写成矢量形式如下: $$ D(X+\Delta X)=D(X)+\frac{\partial D^{\top}(X)}{\partial X} \Delta X+\frac{1}{2} \Delta X^{\top} \frac{\partial^{2} D(X)}{\partial X^{2}} \Delta X $$ 上式对 $\Delta X$ 求导,并令其等于零,可以得到极值点的偏移量: $$ \Delta X=-\frac{\partial^{2} D(X)}{\partial X^{2}}^{-1} \frac{\partial D^{\top}(X)}{\partial X} $$ 通过多次迭代(Lowe SIFT 算法里最多迭代 5 次),得到最终候选点的精确位置与尺度 $\hat{X}$。当超出所设定的迭代次数或者超出图像边界的范围时应删除该点,如果 $\lvert D(\hat{X}) \rvert$ 小于某个阈值则将该极值点也应该删除。 高斯差分函数有较强的边缘响应,对于比较像边缘的点应该去除掉。这样的点的特征为在某个方向有较大主曲率,而在垂直的方向主曲率很小。主曲率可通过一个 $2 \times 2$ 的 Hessian 矩阵求出: $$ H=\left[\begin{array}{ll} D_{x x} & D_{x y} \\ D_{x y} & D_{y y} \end{array}\right] $$ $D$ 的主曲率和 $H$ 的特征值成正比,令 $\alpha$ 为较大的特征值,$\beta$ 为较小的特征值,$\alpha = \gamma\beta$,则有: $$ \begin{aligned} \operatorname{trace}(H) &= D_{x x}+D_{y y}=\alpha+\beta \\ \operatorname{det}(H) &= D_{x x} D_{y y}-\left(D_{x y}\right)^{2}=\alpha \beta \\ \frac{\operatorname{trace}(H)^{2}}{\operatorname{det}(H)} &= \frac{(\alpha+\beta)^{2}}{\alpha \beta}=\frac{(\gamma+1)^{2}}{\gamma} \end{aligned} $$ 上式的结果只与两个特征值的比例有关,而与具体特征值无关。当两个特征值相等时,$\dfrac{(\gamma+1)^{2}}{\gamma}$ 的值最小,随着 $\gamma$ 的增加,$\dfrac{(\gamma+1)^{2}}{\gamma}$ 的值也增加。设定一个阈值 $\gamma_t$(Lowe SIFT 算法里最 $\gamma_t = 10$),若: $$ \frac{\operatorname{trace}(H)^{2}}{\operatorname{det}(H)}<\frac{\left(\gamma_{t}+1\right)^{2}}{\gamma_{t}} $$ 则认为该关键点不是边缘,否则予以剔除。 关键点方向指定 为了使特征描述符具有旋转不变性,需要利用关键点邻域像素的梯度方向分布特性为每个关键点指定方向参数。对于在 DoG 金字塔中检测出的关键点,在其邻近高斯金字塔图像的 3𝜎 邻域窗口内计算其梯度幅值和方向,公式如下: $$ \begin{aligned} &m(x, y)=\sqrt{(L(x+1, y)-L(x-1, y))^{2}+(L(x, y+1)-L(x, y-1))^{2}} \\ &\theta(x, y)=\arctan ((L(x, y+1)-L(x, y-1)) /(L(x+1, y)-L(x-1, y))) \end{aligned} $$ 其中,$L$ 为关键点所在尺度空间的灰度值,$m(x, y)$ 为梯度幅值,$\theta(x, y)$ 为梯度方向。对于模值 $m(x, y)$ 按照 $\theta = 1.5 \theta_{oct}$ 邻域窗口为 $3 \theta$ 的高斯分布加权。在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向,梯度直方图将梯度方向 $\left(0,360^{\circ}\right)$ 分为 36 个柱(bins),如下图所示(为简化,图中只画了八个方向的直方图),直方图的峰值所在的方向代表了该关键点的主方向。 梯度方向直方图的峰值代表了该特征点处邻域梯度的主方向,为了增强鲁棒性,保留峰值大于主方向峰值 80% 的方向作为该关键点的辅方向。因此,在相同位置和尺度,将会有多个关键点被创建但方向不同,这可以提高特征点匹配的稳定性。 关键点特征描述符 在经过上述流程后,检测到的每个关键点有三个信息:位置、尺度以及方向,接下来就是为每个关键点建立一个描述符,用一组向量将这个关键点描述出来。这个特征描述符不但包括关键点,还包括其邻域像素的贡献,而且需具备较高的独特性和稳定性,以提高特征点匹配的准确率。SIFT 特征描述符是关键点邻域梯度经过高斯加权后统计结果的一种表示。通过对关键点周围图像区域分块,计算块内的梯度直方图,得到表示局部特征点信息的特征向量。例如在尺度空间 $4 \times 4$ 的窗口内统计 8 个方向的梯度直方图,生成一个 $4 \times 4 \times 8 = 128$ 维的表示向量。 特征描述符与特征点所在的尺度有关,因此,对梯度的求取应在特征点对应的高斯图像上进行。将关键点附近的邻域划分为 $d \times d$ 个子区域 $(d = 4)$,每个子区域做为一个种子点,每个种子点有 8 个方向。每个子区域的大小与关键点方向分配时相同,即每个区域边长为 $3 \theta_{oct}$。考虑到实际计算时需要进行三线性插值,采样窗口区域半边长设为 $\dfrac{3 \theta_{oct} (d + 1)}{2}$,又考虑到旋转因素(坐标轴旋转至关键点主方向),这个值需要乘以 $\sqrt{2}$,最后所需的图像区域半径为: $$ \text{radius} = \dfrac{3 \sigma_{oct} \times \sqrt{2} \times(d+1)}{2} $$ 将坐标轴旋转至关键点主方向,以确保旋转不变性。如下图所示: 旋转后采样点的新坐标为: $$ \left[\begin{array}{l} x^{\prime} \\ y^{\prime} \end{array}\right]=\left[\begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array}\right]\left[\begin{array}{l} x \\ y \end{array}\right] \quad(x, y \in[\text {-radius, radius }]) $$ 在图像半径区域内对每个像素点求其梯度幅值和方向,并对每个梯度幅值乘以高斯权重: $$ w=m(u+x, b+v) \times e^{-\frac{x^{\prime 2}+y^{\prime 2}}{2 \sigma_{w}^{2}}} $$ 其中,$u$ 和 $v$ 表示关键点在高斯金字塔图中的位置坐标,$x$ 和 $y$ 为旋转坐标轴至关键点主方向之前相对关键点的偏移量,$x^{\prime}$ 和 $y^{\prime}$ 为旋转坐标轴至关键点主方向之后相对关键点的偏移量。 将旋转后的采样点坐标分配到对应的子区域,计算影响子区域的采样点的梯度和方向,分配到 8 个方向上。旋转后的采样点 $(x^{\prime}, y^{\prime})$ 落在子区域的下标为: $$ \left[\begin{array}{l} x_{d} \\ y_{d} \end{array}\right]=\frac{1}{3 \sigma_{o c t}}\left[\begin{array}{c} x^{\prime} \\ y^{\prime} \end{array}\right]+\frac{d}{2}, \quad x_{d}, y_{d} \in[0, d] $$ 将采样点在子区域的下标进行三线性插值,根据三维坐标计算与周围子区域的距离,按距离远近计算权重,最终累加在相应子区域的相关方向上的权值为: $$ w^{\prime} = w \cdot\left[d_{r}^{i} \cdot\left(1-d_{r}\right)^{1-i}\right] \cdot\left[d_{c}^{j} \cdot\left(1-d_{c}^{1-j}\right)\right] \cdot\left[d_{o}^{k} \cdot\left(1-d_{o}\right)^{1-k}\right] $$ 其中,$i, j, k$ 取值为 0 或 1,$d_r, 1- d_r$ 是对相邻两行的贡献因子,$d_c, 1- d_c$ 是对相邻两列的贡献因子,$d_o, 1- d_o$ 是对相邻两个方向的贡献因子。插值计算每个种子点八个方向的梯度,最终结果如下图所示: 得到 128 维特征向量后,为了去除光照变化的影响,需要对向量进行归一化处理。非线性光照变化仍可能导致梯度幅值的较大变化,但对梯度方向影响较小。因此对于超过阈值 0.2 的梯度幅值设为 0.2 ,然后再进行一次归一化。最后将特征向量按照对应高斯金字塔的尺度大小排序。至此,SIFT 特征描述符形成。 利用 SIFT 方法检测关键点的效果如下图所示(代码详见这里): SIFT 方法的优点如下: 局部:SIFT 特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性。 独特:信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配。 大量:即使少数的几个物体也可以产生大量的 SIFT 特征向量。 高效:经优化的 SIFT 匹配算法甚至可以达到实时的要求。 可扩展:可以很方便的与其他形式的特征向量进行联合。 SIFT 方法的缺点如下: 默认算法的实时性不高。 部分情况下特征点较少。 对边缘光滑的目标无法准确提取特征点。 对于 SIFT 的改进可以参考 SURF 5 和 CSIFT 6。 本小节参考: https://docs.opencv.org/5.x/dc/d0d/tutorial_py_features_harris.html https://docs.opencv.org/5.x/da/df5/tutorial_py_sift_intro.html https://lsxiang.github.io/Journey2SLAM/computer_vision/Harris/ https://lsxiang.github.io/Journey2SLAM/computer_vision/SIFT/ 特征编码 BoF BoF(Bag of Features, Bag of Visual Words)7 8 借鉴了文本中的 BoW(Bag of Words)模型的思路。从图像抽象出很多具有代表性的「关键词」,形成一个字典,再统计每张图片中出现的「关键词」频率,得到图片的特征向量。BoF 算法的流程如下: 局部特征提取:利用上文中的 SIFT 等类似方法提取图片的局部特征,每个图片提取的特征数量不同,但每个特征的维度是相同的(例如:128 维)。 构建视觉词典:利用 K-means 等算法将所有图片的所有特征向量进行聚类,得到 $K$ 个聚类中心,即 $K$ 个视觉词汇(Visual Word)。由于特征数量可能非常大,使用 K-means 算法聚类会相当耗时。 生成 BoF 特征:对于一个图像中的每一个特征,都可以在视觉词典中找到一个最相似的视觉词汇,因此可以对该图像统计得到一个 $K$ 维的直方图,每个值表示图像中局部特征在视觉词典中相似视觉词汇的频率。针对该步骤可以利用 TF-IDF 的思想获取加权的 BoF 特征结果。 BoF 算法的一些缺点也比较明显: 在使用 K-means 进行聚类时,$K$ 和初始聚类中心的选取对结果敏感。字典过大,词汇缺乏一般性,对噪声敏感;字典过小,词汇区分性较差,无法充分表示图片。对于海量数据计算所需的时间和空间复杂度都比较高。 将图像表示成一个无序的特征,丢失了图片中的空间信息,表示上存在一定局限。 VLAD VLAD(Vector of Local Aggregated Descriptors)9 方法同 BoF 类似,但在生成特征时采用如下公式: $$ V(j, k)=\sum_{i=1}^{N} \operatorname{sign}_{k}\left(x_{i}\right)\left(x_{i}(j)-c_{k}(j)\right), \quad k \in K, j \in D $$ 其中,$K$ 为词典大小,$N$ 为图片的局部特征数量,$D$ 为每个局部特征的维度,$x_i$ 表示第 $i$ 个局部特征,$c_k$ 表示第 $k$ 个聚类中心,$\operatorname{sign}_k$ 是一个符号函数,如果 $x_i$ 不属于聚类中心 $c_k$ 则为 $0$,反之则为 $1$。 从上式中可以看出 VLAD 累加了每个聚类的所有特征残差,通过 $x_i - c_k$ 将图像本身的局部特征分布差异转换为聚类中心的分布差异,通过归一化和降维等手段得到最终的全局特征。 FV FV(Fisher Vector)10 本质上是用似然函数的梯度向量表示一幅图像。梯度向量的物理意义就是描述能够使模型更好地适应数据的参数变化方向,也就是数据拟合中参数调优的过程。在 FV 中我们采用高斯混合模型(Gaussian Mixture Model,GMM)。 高斯混合模型是由多个高斯模型线性叠加而成,公式如下: $$ p(x)=\sum_{k=1}^{K} \pi_{k} N\left(x \mid \mu_{k}, \Sigma_{k}\right) $$ 其中,$p(x)$ 表示数据 $x$ 出现的概率,$K$ 表示高斯模型个数,$\pi_k$ 表示第 $k$ 个高斯模型的权重,$\mu_k$ 表示第 $k$ 个高斯分布的均值,$\Sigma_k$ 表示第 $k$ 个高斯分布的方差。理论上,只要 $K$ 足够大,GMM 可以逼近任意一种概率分布。 GMM 的目标是求解参数 $\pi_k, \mu_k, \Sigma_k$ 使得它确定的概率分布生成这些给定数据的概率最大,即 $\Pi_{i=1}^{N} p\left(x_{i}\right)$ 最大。假设各个数据点之间满足独立同分布,可以将其转换成对数似然函数: $$ \sum_{i=1}^{N} \log \left(p \left(x_i\right)\right) $$ 假设 GMM 模型包含 $K$ 个高斯模型,则模型的参数集合为 $\lambda = \left\{w_i, \mu_k, \Sigma_k, k = 1, \cdots, K\right\}$,假设共有 $T$ 个特征向量,则似然函数可以表示为: $$ \mathcal{L} (x \mid \lambda)=\sum_{t=1}^{T} \log \left(p\left(x_{t} \mid \lambda\right)\right) $$ 其中的高斯分布是多个基高斯分布的混合: $$ \label{eq:fv_gmm} p\left(x_{t} \mid \lambda\right)=\sum_{k=1}^{K} w_{k} * p_{k}\left(x_{t} \mid \lambda\right) $$ 每个基高斯分布又可以表示为: $$ p_{k}\left(x_{t} \mid \lambda\right) = \dfrac{1}{\left(2 \pi\right)^{\frac{D}{2}} \lvert\Sigma_k\rvert^{\frac{1}{2}}} e^{-\frac{1}{2} \left(x - \mu_k\right)^{\prime} \Sigma_k^{-1} \left(x - \mu_k\right)} $$ 由贝叶斯公式可知,描述符 $x_t$ 数据第 $i$ 个高斯模型的概率为: $$ \gamma_{t}(i)=\frac{w_{i} u_{i}\left(x_{t}\right)}{\sum_{k=1}^{K} w_{k} u_{k}\left(x_{t}\right)} $$ 则公式 $\ref{eq:fv_gmm}$ 的梯度分量为: $$ \begin{aligned} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial w_{i}} &=\sum_{t=1}^{T}\left[\frac{\gamma_{t}(i)}{w_{i}}-\frac{\gamma_{t}(1)}{w_{1}}\right] \text { for } i \geq 2, \\ \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \mu_{i}^{d}} &=\sum_{t=1}^{T} \gamma_{t}(i)\left[\frac{x_{t}^{d}-\mu_{i}^{d}}{\left(\sigma_{i}^{d}\right)^{2}}\right], \\ \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \sigma_{i}^{d}} &=\sum_{t=1}^{T} \gamma_{t}(i)\left[\frac{\left(x_{t}^{d}-\mu_{i}^{d}\right)^{2}}{\left(\sigma_{i}^{d}\right)^{3}}-\frac{1}{\sigma_{i}^{d}}\right] . \end{aligned} $$ 由于概率空间和欧氏空间的归一化方式不同,在此引入 Fisher Matrix 进行归一化: $$ \begin{aligned} f_{w_{i}} &=T\left(\frac{1}{w_{i}}+\frac{1}{w_{1}}\right) \\ f_{\mu_{i}^{d}} &=\frac{T w_{i}}{\left(\sigma_{i}^{d}\right)^{2}}, \\ f_{\sigma_{i}^{d}} &=\frac{2 T w_{i}}{\left(\sigma_{i}^{d}\right)^{2}} . \end{aligned} $$ 归一化后即为 Fisher 向量: $$ \begin{aligned} \mathscr{G}_{\alpha_{k}}^{X} &= f_{w_i}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial w_{i}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T}\left(\gamma_{t}(k)-w_{k}\right), \\ \mathscr{G}_{\mu_{k}}^{X} &= f_{\mu_i^d}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \mu_{i}^{d}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T} \gamma_{t}(k)\left(\frac{x_{t}-\mu_{k}}{\sigma_{k}}\right), \\ \mathscr{G}_{\sigma_{k}}^{X} &= f_{\sigma_i^d}^{1/2} \frac{\partial \mathcal{L}(X \mid \lambda)}{\partial \sigma_{i}^{d}} =\frac{1}{\sqrt{w_{k}}} \sum_{t=1}^{T} \gamma_{t}(k) \frac{1}{\sqrt{2}}\left[\frac{\left(x_{t}-\mu_{k}\right)^{2}}{\sigma_{k}^{2}}-1\right] \end{aligned} $$ 其中 $\mathscr{G}_{\alpha_{k}}^{X}$ 同 BoF 有类似效果,$\mathscr{G}_{\mu_{k}}^{X}$ 同 VLAD 有类似效果。 图像检索 倒排索引 倒排索引是一种提高存储和检索效率的算法,它常被用于大/中等规模的编码本中,结构如下图所示: 倒排索引是一个一维结构,其中每一个条目对应编码本中的一个视觉词汇。每一个视觉词汇都包含一个倒排表,每个倒排表中的索引被称为索引特征或者记录。倒排索引很好地发挥了大规模编码本词汇直方图稀疏性的特点。 ANN 基于近似最近邻搜索(Approximate Nearest Neighbor Search,ANN)的方法,请参考之前的博客。 基于深度学习的图像检索 基于深度学的图像检索流程如下图所示: 网络前馈方式:将图片输入 DCNN 的方式有两种:单路和多路。单路将整个图片作为输入,多路依赖区域提取,例如空间金字塔模型(Spatial Pyramid Models)和区域候选网络(Region Proposal Networks,RPN)。 深度特征提取:基于整个图像或部分区块的输入,网络的激活值可以用作原始的特征。全连接层提供了一个全局的视野,将一整个图片表示为单个向量。 特征嵌入和集成:基于图片级别或区块级别的描述符,构造全局或局部特征时包含两个重要步骤,通常为 PCA 和白化。特征嵌入将独立的局部特征映射到一个高维向量,特征集成则将多个映射的向量合并成一个单一向量。 特征匹配:特征匹配用来衡量图片之间特征的相似度。全局匹配可以通过欧式、汉明或其他距离度量进行高效计算。对于局部特征,可以通过 RANSAC 11 和近期的一些改进方法对局部特征进行相似度汇总来评估相似性。 关于卷积神经网络的细节介绍,请参考之前的博客。 网络前馈方式 单路前馈方法 单路前馈方法是将整个图片直接输入模型来提取特征。由于仅将图片输入模型一次,该方法效率较高。对于这类方法,全连接层和最后一个卷积层可以作为特征提取器。 多路前馈方法 相比于单路前馈方法,多路前馈方法由于需要将生成的多个图像块输入到模型中,因此相对耗时。这类方法通常包含两个步骤:图像块识别和图像块描述。使用不同方法可以获得不同尺度的图像块,如下图所示: 其中,(a) 为固定窗口大小划分的区块,(b) 为空间金字塔模型(Spatial Pyramid Model,SPM)划分的区块,(c) 为稠密采样的区块,(d) 为通过区域候选网络(Region Proposal Network,RPN)获得的候选区块。 深度特征选择 特征选择决定了提取特征的表达范围,例如:从全连接层可以获得全局级别特征,从卷积层可以获得区块级别特征。 从全连接层提取 将全连接层作为全局特征提取器,通过 PCA 降维和标准化后可以用于度量图像的相似性。但由于这一层是全连接的,每个神经元都会产生图像级别的描述符,这会导致两个明显的缺陷:包括无关信息和缺少局部几何不变性。 针对第一个缺陷,可以通过多路前馈方法来提取区块级别特征。针对第二个缺陷,缺乏几何不变性会影响图像变换(如:裁剪、遮挡)时的鲁棒性,通过使用中间卷积层可以来解决。 从卷积层提取 从卷积层(通常是最后一层)提取的特征保留了更多的结构细节,这对实例检索尤为有利。卷积层的神经元仅同局部区域相连接,这样较小的视野确保所生成的特征包含更多局部结构信息,同时对于图像变换更为鲁棒。 加和/平均和最大池化是用于生成全局特征的两个简单的集成方法,一些其他的集成方法,例如:MAC 12,R-MAC 13,GeM polling 14,SPoC 15,CroW 16 和 CAM+CroW 17 如下图所示: 特征融合策略 层级别融合 通过层级别融合,可以在深度网络中融合多个全连接层。全连接层的特征保留了全局高层级语义信息,卷积层的特征保留了局部中低层级的信息。因此,全局特征和局部特征在测量语义相似度时相辅相成,可以在一定程度上保证检索的效果。 模型级别融合 融合不同模型的特征也是可行的,这种融合方式关注于模型之间的互补性质,其可以分为:intra-model 和 inter-model 两类。intra-model 融合方式建议使用多个具有相似性或者结构上高度兼容的模型,而 inter-model 融合方式则使用结构上具有很大不同的模型。 inter-model 和 intra-model 融合同模型选择有关。融合候选模型的所有特征,然后根据拼接的特征学习得到一个度量,这种方式称为 early fusion 策略。或者对每个模型的特征学习各自的最优度量,然后组合这些度量用于最终的检索排名,这种方式称为 late fusion 策略。 特征嵌入和集成 特征嵌入和集成的主要目的是进一步提高从 DCNN 中提取的特征的区分能力,来生成用于检索特定实例的最终全局或局部特征。 匹配全局特征 全局特征可以从全连接层中提取,然后进行降维和标准化,通常情况下没有进一步的聚合过程。卷积特征也可以被集成到全局特征中,简单的方式是通过加和/平均或最大池化。卷积特征可以作为局部区域的描述符,因此可以利用基于 SIFT 的图像检索中提到的 BoF,VLAD,FV 等模型对其进行编码,然后再将他们聚合为一个全局描述符。 匹配局部特征 尽管全局特征匹配对于特征提取和相似度计算都具有很高的效率,但全局特征与空间验证和对应估计不兼容,这些是实例级别检索任务的重要过程。在匹配过程中,全局特征只匹配一次,而局部特征匹配通过汇总所有单个局部特征的相似性来评估(即多对多匹配)。 注意力机制 注意力机制可以看作是一种特征聚合,其核心思想是突出最相关的特征部分,通过计算注意力映射来实现。获取注意力映射的方法可以分为两类:非参数和参数,如下图所示,主要区别在于注意力映射中的重要性权重是否可学习。 非参数加权是一种突出特征重要性的直接方法,相应的注意力映射可以通过通道或空间池化获得,如 (a) 和 (b) 所示。参数注意力映射可以通过深度网络学习,其输入可以是图像块或特征映射,这些方法通常用于有监督的度量学习,如 (c) 和 (d) 所示。 哈希嵌入 深度网络提取的实值特征通常是高维的,不太适合检索。因此,将深层特征转换为更紧凑的编码具有重要意义。由于其计算和存储效率,哈希算法已被广泛用于全局和局部描述符。 哈希函数可以作为一个层插入到深度网络中,以便可以通过有监督或无监督的方式同时训练和优化哈希码和深度网络。哈希函数训练时,相似图像的哈希码嵌入会尽可能的接近,不相似图像的哈希码嵌入会尽可能的分离。 模型微调 微调方法已被广泛研究以学习更好的检索特征。在基于图像分类的数据集上预训练的 DCNN 对类间可变性非常稳健,随后将成对的监督信息引入排名损失中,通过对检索表示进行正则化来微调网络。具有清晰且定义良好的真实标签的标准数据集对于微调深度模型以执行准确的实例级检索是必不可少的,否则就需要开发无监督的微调方法。在网络微调之后,可以将特征作为全局或局部来进行检索。 有监督微调 下图展示了不同类型的有监督微调方法: 下图展示了不同类型的有监督微调损失函数: 无监督微调 由于成本等问题可能导致监督信息不足,从而监督网络微调变得不可行。因此,用于图像检索的无监督微调方法是非常必要的。对于无监督微调,两个方向是通过流形学习和聚类技术挖掘特征之间的相关性。具体细节不再展开讨论,详细内容请参见原文。 开放资源 Stanford Online Products Retrieval Leaderboard CARS196 Retrieval Leaderboard CUB-200-2011 Retrieval Leaderboard In-shop Clothes Retrieval Leaderboard Image Retrieval - Paper with code Zheng, L., Yang, Y., & Tian, Q. (2017). SIFT meets CNN: A decade survey of instance retrieval. IEEE transactions on pattern analysis and machine intelligence, 40(5), 1224-1244. ↩︎ Chen W., Liu Y., Wang W., Bakker E., Georigiou T., Fieguth P., Liu L., & Lew M. (2022). Deep Learning for Instance Retrieval: A Survey. arXiv:2101.11282 ↩︎ https://zh.wikipedia.org/wiki/基于内容的图像检索 ↩︎ Lindeberg, T. (1994). Scale-space theory: A basic tool for analyzing structures at different scales. Journal of applied statistics, 21(1-2), 225-270. ↩︎ Bay, H., Tuytelaars, T., & Van Gool, L. (2006, May). Surf: Speeded up robust features. In European conference on computer vision (pp. 404-417). Springer, Berlin, Heidelberg. ↩︎ Abdel-Hakim, A. E., & Farag, A. A. (2006, June). CSIFT: A SIFT descriptor with color invariant characteristics. In 2006 IEEE computer society conference on computer vision and pattern recognition (CVPR'06) (Vol. 2, pp. 1978-1983). Ieee. ↩︎ Sivic, J., & Zisserman, A. (2003, October). Video Google: A text retrieval approach to object matching in videos. In Computer Vision, IEEE International Conference on (Vol. 3, pp. 1470-1470). IEEE Computer Society. ↩︎ Radenović, F., Jégou, H., & Chum, O. (2015, June). Multiple measurements and joint dimensionality reduction for large scale image search with short vectors. In Proceedings of the 5th ACM on International Conference on Multimedia Retrieval (pp. 587-590). ↩︎ Jégou, H., Douze, M., Schmid, C., & Pérez, P. (2010, June). Aggregating local descriptors into a compact image representation. In 2010 IEEE computer society conference on computer vision and pattern recognition (pp. 3304-3311). IEEE. ↩︎ Perronnin, F., Sánchez, J., & Mensink, T. (2010, September). Improving the fisher kernel for large-scale image classification. In European conference on computer vision (pp. 143-156). Springer, Berlin, Heidelberg. ↩︎ Fischler, M. A., & Bolles, R. C. (1981). Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography. Communications of the ACM, 24(6), 381-395. ↩︎ Razavian, A. S., Sullivan, J., Carlsson, S., & Maki, A. (2016). Visual instance retrieval with deep convolutional networks. ITE Transactions on Media Technology and Applications, 4(3), 251-258. ↩︎ Tolias, G., Sicre, R., & Jégou, H. (2015). Particular object retrieval with integral max-pooling of CNN activations. arXiv preprint arXiv:1511.05879. ↩︎ Radenović, F., Tolias, G., & Chum, O. (2018). Fine-tuning CNN image retrieval with no human annotation. IEEE transactions on pattern analysis and machine intelligence, 41(7), 1655-1668. ↩︎ Babenko, A., & Lempitsky, V. (2015). Aggregating local deep features for image retrieval. In Proceedings of the IEEE international conference on computer vision (pp. 1269-1277). ↩︎ Kalantidis, Y., Mellina, C., & Osindero, S. (2016, October). Cross-dimensional weighting for aggregated deep convolutional features. In European conference on computer vision (pp. 685-701). Springer, Cham. ↩︎ Jimenez, A., Alvarez, J. M., & Giro-i-Nieto, X. (2017). Class-weighted convolutional features for visual instance search. arXiv preprint arXiv:1707.02581. ↩︎

2022/1/27
articleCard.readMore

你所应该知道的 A/B 测试 (A/B Test You Should Know)

什么是 A/B 测试 A/B 测试是一种随机测试,将两个不同的东西(即 A 和 B)进行假设比较。A/B 测试可以用来测试某一个变量两个不同版本的差异,一般是让 A 和 B 只有该变量不同,再测试目标对于 A 和 B 的反应差异,再判断 A 和 B 的方式何者较佳 1。A/B 测试的前身为双盲测试 2,在双盲测试中人员会被随机分为两组,受试验的对象及研究人员并不知道哪些对象属于对照组,哪些属于实验组,通过一段时间的实验后对比两组人员的结果是否有明显差异。在各种科学研究领域中,从医学、食品、心理到社会科学及法证都有使用双盲方法进行实验。 一个简单的 A/B 测试流程如下: 对目标人群进行随机划分,以进行有效的独立随机实验。 对不同分组应用不同的策略。 在确保实验有效的前提下,对不同分组的结果进行分析,以确定不同策略的优劣。 A/B 测试的主要目的是帮助我们更加科学的判断不同策略的优劣性,避免拍脑门的决策,不给杠精们互相 BATTLE 的机会。同时我们也需要认识到 A/B 测试只是一个工具,它能够帮助我们对产品和策略进行不断优化,但对产品和策略的创新更多还是需要洞察力。它可以让我们在已达到的山上越来越高,却不能用它来发现一座新的山脉。一句话:A/B 测试不是万能的,但离开 A/B 测试是万万不能的。 A/B 测试的科学性 流量分配 进行 A/B 测试的第一个问题就是如何划分用户,如果采用上面简单五五开的方式我们一次只能做一个实验,当我们需要同时做多个实验时就无法满足了。如果对用户分成多个桶,当桶的数量过多时,每个桶中的用户数量就会过少,从而会导致实验的置信度下降。 为了保证可以使用相同的流量开展不同的实验,同时各个实验之间不能相关干扰,我们需要采用正交实验。正交实验的思想如下: 每个独立实验为一层,层与层之间的流量是正交的,流量经过一层实验后会再次被随机打散。 有些情况下实验并不是独立的,例如同时对按钮和背景的颜色进行实验,按钮和背景颜色之间并不是独立的(即有些按钮和背景颜色搭配从设计角度是不可行的,没有必要进行实验),这种情况下我们需要采用互斥实验。互斥实验的思想如下: 实验在同一层进行流量拆分,不同组之间的流量是没有重叠的。 在 A/B 测试中,当多个实验内容相互影响应选择互斥方法分配流量,当多个实验内容不会相互影响应选择正交方法分配流量。更加精细的流量分类和控制可以参考 Google 的论文 Overlapping Experiment Infrastructure: More, Better, Faster Experimentation 3。 评价指标选择 在设计实验之前我们需要明确实验的目标,根据目标才能确定合理的评价指标。更多情况下我们应该从业务的视角出发选择合适的评价指标,我们以风险策略模型实验为例,我们可以从技术和业务角度选择不同的评价指标: 技术角度:准确率和召回率 业务角度:客诉率和追损金额 单纯从技术角度出发我们会忽视很多现实问题,例如两个策略的准确率和召回率差不多,但识别的结果人群不一样,这些人造成的损失也可能不一样。因此能够帮助我们追回更多损失同时有更小的客诉率才是更优的策略。 在进行实验时结果指标至关重要,但有时我们也应该关注一些过程指标。以页面优化实验为例,可能的过程指标和结果指标有: 过程指标:页面平均停留时间,页面跳出率等 结果指标:商品加购率,商品转化率等 策略和模型最终都是要为业务服务的,因此我们应常关注业务指标,一些常用的业务指标有:点击率(CTR)、转化率(CVR)、千次展示收入(RPM)等。 有效性检验 当实验完成得到结果后,我们还需要判断实验结果是否有效,这部分主要依靠统计学中的假设检验进行分析。针对两个实验在确定合理的统计量后,需要构建如下两个假设: 原假设 $H_0$:两个实验的统计量无差异 备选假设 $H_1$:两个实验的统计量有差异 对于假设检验只可能有两种结果:一个是接受原假设,一个是拒绝原假设。在进行假设检验过程中,容易犯两类错误: 是否接受原假设 \ 假设真伪 $H_0$ 为真 $H_1$ 为真 拒绝原假设 第一类错误 $\alpha$:显著水平 正确决策 $1 - \alpha$:置信度 接受原假设 正确决策 $1 - \beta$:统计功效 第二类错误 $\beta$ 第一类错误(弃真)即原假设为真时拒绝原假设,犯第一类错误的概率为 $\alpha$,即显著水平。第二类错误(取伪)即原假设为假时未拒绝原假设,犯第二类错误的概率为 $\beta$。 在进行有效性检验时我们有多个指标可以参考: P 值。P 值就是当原假设为真时,比所得到的样本观察结果更极端的结果出现的概率。如果 P 值很小,说明原假设情况的发生的概率很小,而如果出现了,根据小概率原理,我们就有理由拒绝原假设,P 值越小,我们拒绝原假设的理由越充分。 置信区间。置信区间就是分别以统计量的置信上限和置信下限为上下界构成的区间。置信水平是指包含总体平均值的概率是多大,例如:95% 的置信水平表示,如果有 100 个样本,可以构造出 100 个这样的区间,有 95% 的可能性包含总体平均值。在 A/B 测试时,如果置信区间上下限的值同为正或负,则认为存在有显著差异的可能性;如果同时有正值和负值,则认为不存在有显著差异的可能性。 统计功效。一般情况下我们希望拒绝原假设,得到新的结论,即在进行 A/B 测试时希望实验组的效果优于对照组。也就是我们希望不要出现在应该拒绝原假设时却没有拒绝的情况,即犯第二类错误。统计功效就是我们没有犯第二类错误的概率 $1 - \beta$,在进行 A/B 测试时表示当两个策略之间存在显著差异时,实验能正确做出存在差异判断的概率。 综上,我们可以认为当 A/B 测试实验数据在 95% 的置信水平区间内,P 值小于0.05,功效大于 80% 的情况下,实验结果是可信赖的。 A/A 测试 在做 A/B 测试的时候,有时尽管我们发现 A/B 两组有明显差异,但我们依旧无法确认这种差异是由于实验条件不同还是 A/B 两组用户本身的差异带来的。尽管 A/B 两组用户是随机抽样,但两组用户在空跑期(即实验条件一致)也会出现显著差异。因此为了避免这个问题,我们会选择进行 A/A 测试,即在正式开启实验之前,先进行一段时间的空跑,对 A/B 两组用户采用同样的实验条件,一段时间后,再看两组之间的差异。如果差异显著,数据弃之不用,重新选组。如果差异不显著,记录两组之间的均值差,然后在实验期结束时,用实验期的组间差异减去空跑期的组间差异得到最终实验结果。 A/A 测试也会存在一些局限,在实际情况中,组间差异是一定存在。因此在这个前提下,可以用统计方式来衡量差异大小,在计算实验效果的时候,把差异考虑在内即可。差异产生的主要原因就是“随机性”,我们同样可以利用置信度和置信区间来描述 A/A 实验的波动。在进行实验时直接进行 A/B 测试,不需要考虑 A/A 测试,在分析结果时,需要考虑 A/B 测试实验之间的差异要大于 A/A 测试实验之间的差异。 https://zh.wikipedia.org/wiki/A/B測試 ↩︎ https://zh.wikipedia.org/wiki/雙盲 ↩︎ Tang, Diane, et al. “Overlapping experiment infrastructure: More, better, faster experimentation.” Proceedings of the 16th ACM SIGKDD international conference on Knowledge discovery and data mining. 2010. ↩︎

2021/10/17
articleCard.readMore

一个人的摩旅 (Travel with My Motorcycle Alone)

之前 三十岁了,不小了,一些事情再不做就不知道会拖到什么时候了。自从之前看到朋友发的一段摩旅的视频,就一直念念不忘,有机会一定要来一次,虽然不一定如视频中那般潇洒。最后证明确实不一样,尽管谈不上是一场修行,但遇到的人事物都是独特且难以忘记的。 有了出发的勇气,但拖延症还是很严重,各种准备和规划直到出发前一晚才简单搞定,当然这也给后面的遇到的问题埋下了伏笔。本来规划了 5 天的行程,最后一天赶到大同见朋友一面再返回北京,但一场冷雨让我在第 4 天直接折返北京。 第一天 折腾了好久,费劲扒拉才把后座包绑好。出门还没上高速,碰见一个骑复古车的哥们,问了我去哪里,我说自己去内蒙古,一句注意安全,一句感谢。出发前才掉了一格油,就没有去加,距离丰宁还 20 多公里,油表灯就闪了,好在还是坚持到了丰宁,成功续满了油。路上断断续续发现成队的、独行的骑士,还有一个书包和后座包都贴了实习标志的哥们,应该都是追逐自由的人吧。 到丰宁前中途停下检查了后座包,发现有一侧绑的不合理,卡口会被后轮摩到,都快要磨断了,幸好检查了下,改造一番绑好继续上路。从丰宁出发没多久,隐约闻道一股烧焦的味道,没当回事儿,以为是路上其他汽车的尾气。中间休息的时候又检查了一下后座包,才发现从丰宁出发时有一侧绑的有点松,后座包倒向了排气那边。奈何我帅气的双排气在上面,就把包烫了一个大洞,一并倒霉的还有带了但一次还没来得及穿过的冬款骑行服和骑行裤。不过好在底下放的是衣服,如果我把相机、电脑、无人机、充电宝放在那边低下…..都不敢想会有什么事儿。这就是没有提前做好功课如何绑后座包的后果,当然闻道异味没有第一时间检查也很不好。 包和衣服被烫了之后,心情一下就不好了,虽然一再自我安慰,但还是缓了很久才勉强不伤心了。由于中途发生了不少事情,整体耽误了一个小时左右,赶到塞罕坝这边酒店的时候已经是六点半了,天已经完全黑下来了,后面一个多小时的路程真的是又累又冷。万幸的是我把我夏季骑行服里面的内胆加上了,不然估计到酒店就冻成狗了。 第二天 早起退房碰见一对儿摩旅的,男生骑了一辆 ADV,小姐姐骑了一辆 Ninja 400。一清早晴空万里,阳光明媚,昨日的不开心一下子全都消散了。虽然秋天已经来了好一阵儿了,不过路两旁的风景还是很漂亮的。 骑进乌兰布统,越往后走人越少,路上碰见了好多次牛马拦路。从乌兰布统去到多伦的路上风景很好,选择下道骑行是明智的,一路上草原、小河、树林,遇到太多美景。 赶着中午骑到了多伦湖,简单吃过饭后就开始了环湖之旅,一路上走走停停。到了一处小高峰,把摩托骑了上去,太适合给我的小摩托来一张照片了。刚拍上就碰到了俩大哥,帮我和我的小摩托来了本次摩旅的第一张合照,聊了会儿道谢之后就开始往回返。 回到环湖起点时间还早,简单搜了下发现往北一点儿就是「滦河」源,小时候家乡人口中的滦河(唐山话:láng hé)原来是从这里流下去的。往滦河源的路不是很好走,骑到一半发现一条土路,骑过去是一个小平台,望向四周很美。后面跟过来好多越野车,也纷纷下来拍照,发现我骑了摩托过来就一个个站在旁边拍起了照片。我过去车上拿我的相机,说道慢慢拍不着急,小姐姐问我可不可以坐上去,我说没问题。后来我也拜托他们帮我和我的小摩托拍了第二张合照,我也成了几个阿姨的摄影师。 快乐是什么?一路上美丽的风景,一路上美丽的人,还有多少能带给他人快乐的我和我的小摩托。果然不敢路的一天是轻松的,从滦河源下来之后就早早赶到多伦的酒店休息了。 第三天 早起的阳光不错,虽然空气有些凉,但是太阳照着还是暖洋洋的,耳机里恰巧响起了赵照的「在冬天和奶奶一起晒太阳」,有些惬意。走着走着云多了起来,中午赶到了太仆寺旗,吃上一口热乎的麻辣烫。 中午微信群里有哥们说北京下大雨了,我心底还在暗暗窃喜这几天出行的天气都还不错,然而这 Flag 还是立早了……下午在去往张北的路上碰见了一波从北京来的摩旅队伍,什么车都有,有 ADV,有踏板,可见是真友谊,ADV 还能带着小踏板一起愉快的玩耍。前半程的风景很不错,路两旁的树叶开始泛黄飘落,一条直路望不见头。 天气渐渐阴沉了下来,肉眼可见前方有积雨云,但还是朝着前方骑了下去。突然天空飘起了小雨,但很快又没了,停在一个加油站打开雷达云图,发现云应该已经从要走的路上飘过去了,简单休息了一下又继续往前走。 路上淅淅沥沥的飘落几滴雨,但路面已经湿透了,之前应该是下了一阵。走着走着发现大腿有点凉,想着应该是雨水溅落到裤子上了。找了个地方停下一看,我去,车子、衣服、后座包已经被黒黑的泥水沾满,躲过了雨水,躲不过泥巴……水越溅越多,身体也越来越冷,万幸还是撑到了张北。找了个肯德基,点了一大杯热拿铁,喝完天已经放晴,看看时间不早了,再不走估计到乌兰察布得很晚了,简单把车擦了下就又启程了。 前半程的国道一路都是风景,但从张北到乌兰察布的后半程除了大车和尘土啥都没有。天彻底放晴,一股暖意上来,还好老天没太折磨我,停下来特意给我脏了吧唧但不离不弃的小摩托照了张像,纪念有史以来最脏的一次。 上了高速就快多了,大概 6 点半多赶到了酒店,卸下行囊第一件事就是去找了个洗车店把我的小摩托好好冲一冲,拿过老板的水枪就开始了自助洗车,洗完之后,我漂亮的小摩托又回来了! 第四天 一早起来天气有些阴冷,不过早有预期,昨天看天气,相比其他地方至少没有下雨。先走了国道去火山,路不太好,一路上身体到不是很冷,冷的是手,夏季的手套还真实漏风,走走停停,冷的受不了就带着手套放在发动机上「烤烤火」。 快到火山了发现一条岔路,还有警察叔叔在摆一个大牌子,「火山旅游路线」,问了后果断选择了指向的新路,新路好走的不要不要的。来到六号火山,人还不少,阴天更是给这里增添了一份外星感。摩托的好处是能骑上平台,转到北面发现人比较少,借来的大疆终于能派上用场了,虽然最后感觉拍的并不好。 往下走看到有几辆越野车爬上了小山丘,心里想来都来了,不在土路上骑一遭怎么对得起我这「越野版」的小狮子。 转完火山发现时间不是很晚,导航了下发现应该能在 7 点前赶回北京,想了想北京这几天都有雨,应该怎么都躲不过,一番权衡之下打算今天直接回北京。上了高速很快就到了集宁服务区,已经快 2 点了,正好吃午饭,这时天也下上了小雨。吃完之后再次权衡,还是决定继续往北京走,如果下大了不行就找最近的口下。一路上,雨不算大也不算小,这就让人很纠结,每到一个服务区就停下来看看云图,最终还是在晚上 10 点前回到了北京。陪了我五年的手机最终在进入地库前因为进水太多失灵然后宕机了,万幸坚持到了回家。 这段雨中经历感觉能回忆一辈子,不是因为勇敢,也不是因为幸运。敢这么走下来,还是评估了环境没有那么恶劣,对自己的驾驶技术也有一定把握,当然最重要的是量力,人永远不要同自然和生命开玩笑。所以每一个服务区都停下来,要么吃点东西,要么喝点热水,休整好后继续往前走。如果说知道 10 点才能到北京,让我再选择一次的话,我想我会选择在中途歇上一晚,虽然第二天也躲不开雨,但能骑得更从容些。 之后 真的要感谢我靠谱的小摩托,一直不离不弃,也要感谢这一路上遇到的陌生人的祝福和帮助。服务区遇到的摩托情侣,问我衣服是不是穿得太少,我没好意思说烫坏了,就只说带少了。餐厅拼桌吃饭的一家,男主人在深圳上班,自己不玩摩托,但工作的地方挨着交管局,见到太多扣查的摩托(深圳全市禁摩),分别时一个握手,一句平安。高速路上,三车道,我在最右侧,中间车道的一辆大车超我时特意变到左侧,掀起的水花完全没溅到我,这一定是个可爱的司机大叔吧。 旅行的意义在哪里?形形色色的人,真实且普通,慢下来,你能看到自己的更多面,也能看到陌生人的一面,或好或坏,无所谓,世界不就是这样吗。

2021/10/6
articleCard.readMore

设计语言初探 (A Glimpse of Design Language)

设计语言(Design Language)或设计语言系统(Design Language System, DLS)是一套用于指导产品设计的整体风格方案 1。设计语言把设计作为一种“沟通的方式”,用于在特定的场景内,做适当的表达,进行特定的信息传递。设计语言在建筑、工业设计和数字产品等领域都有广泛的应用,本文仅围绕数字产品进行初探。 为什么构建设计语言? 统一 通过设计语言可以在整个平台中统一颜色、字体、组件、动效等各种规范,避免由于设计师的个人特点导致产品风格不一致。 体验 优秀的设计语言符合大众审美,可以提高产品的可用性和易用性。设计语言可以使用户能够与具备一致性的应用进行交互,让用户在使用过程中获得愉悦,提升用户体验。 效率 优秀的设计语言使得设计和开发团队能够快速、经济、高效地进行开发、重构和迭代产品。通过不断更新和完善的文档库,可以改善团队之间的协作,提高生产力。 品牌 设计语言的构建可以传达一个统一的公司品牌形象。设计语言让产品具有自己的身份,使其在市场上的众多产品中更容易被识别出来,加深用户对品牌的印象。 设计语言构建 在此我们借助语言学的角度来讨论数字化产品的构建 2。在语言应用中,我们通常会涉及语法、语素、语句、语义、语境、语气、语素和响度等维度,通过不同的组合达成应景的表达和适时的沟通。 语法 设计语言中的语法即设计价值观和设计原则,这是构建设计语言系统的起点,用于传达品牌主张或设计理念,它将指引业务设计执行的方向。 制定设计原则时,首先研究用户特性,聚焦产品核心价值,然后通过脑暴等形式选择有特点的维度,结合用户体验与品牌属性将其视觉化,最后用简要的语言归纳出来。 Ant Design 设计价值观 3 自然 数字世界的光速迭代使得产品日益复杂,而人类意识和注意力资源有限。面对这种设计矛盾,追求「自然」交互将是 Ant Design 持之以恒的方向。 确定性 界面是用户与系统交互的媒介,是手段而非目的。在追求「自然」交互基础上,通过 Ant Design 创造的产品界面应是高确定性、低合作熵的状态。 意义感 一个产品或功能被设计者创造出来不只是用户的需要,而更多是承载用户的某个工作使命。产品设计应充分站在工作视角,促成用户使命的达成;同时,在「自然」、「确定」之上,兼顾用户的人性需求,为工作过程创造富有意义感的人机交互。 生长性 企业级产品功能的增长与用户系统角色的演变相生相伴。设计者应为自己创造的产品负责,提升功能、价值的可发现性。用发展的眼光做设计,充分考虑人、机两端的共同生长。 Ant Design 设计原则 4 亲密性 如果信息之间关联性越高,它们之间的距离就应该越接近,也越像一个视觉单元;反之,则它们的距离就应该越远,也越像多个视觉单元。亲密性的根本目的是实现组织性,让用户对页面结构和信息层次一目了然。 对齐 正如「格式塔学派」中的连续律(Law of Continuity)所描述的,在知觉过程中人们往往倾向于使知觉对象的直线继续成为直线,使曲线继续成为曲线。在界面设计中,将元素进行对齐,既符合用户的认知特性,也能引导视觉流向,让用户更流畅地接收信息。 对比 对比是增加视觉效果最有效方法之一,同时也能在不同元素之间建立一种有组织的层次结构,让用户快速识别关键信息。 重复 相同的元素在整个界面中不断重复,不仅可以有效降低用户的学习成本,也可以帮助用户识别出这些元素之间的关联性。 直截了当 正如 Alan Cooper 所言:「需要在哪里输出,就要允许在哪里输入」。这就是直接操作的原理。eg:不要为了编辑内容而打开另一个页面,应该直接在上下文中实现编辑。 足不出户 能在这个页面解决的问题,就不要去其它页面解决,因为任何页面刷新和跳转都会引起变化盲视(Change Blindness),导致用户心流(Flow)被打断。频繁的页面刷新和跳转,就像在看戏时,演员说完一行台词就安排一次谢幕一样。 简化交互 根据费茨法则(Fitts’s Law)所描述的,如果用户鼠标移动距离越少、对象相对目标越大,那么用户越容易操作。通过运用上下文工具(即:放在内容中的操作工具),使内容和操作融合,从而简化交互。 提供邀请 很多富交互模式(eg:「拖放」、「行内编辑」、「上下文工具」)都有一个共同问题,就是缺少易发现性。所以「提供邀请」是成功完成人机交互的关键所在。 邀请就是引导用户进入下一个交互层次的提醒和暗示,通常包括意符(eg:实时的提示信息)和可供性,以表明在下一个界面可以做什么。当可供性中可感知的部分(Perceived Affordance)表现为意符时,人机交互的过程往往更加自然、顺畅。 巧用过渡 人脑灰质(Gray Matter)会对动态的事物(eg:移动、形变、色变等)保持敏感。在界面中,适当的加入一些过渡效果,能让界面保持生动,同时也能增强用户和界面的沟通。 即时反应 「提供邀请」的强大体现在 交互之前 给出反馈,解决易发现性问题;「巧用过渡」的有用体现在它能够在 交互期间 为用户提供视觉反馈;「即时反应」的重要性体现在 交互之后 立即给出反馈。 就像「牛顿第三定律」所描述作用力和反作用一样,用户进行了操作或者内部数据发生了变化,系统就应该立即有一个对应的反馈,同时输入量级越大、重要性越高,那么反馈量级越大、重要性越高。 虽然反馈太多(准确的说,错误的反馈太多)是一个问题,但是反馈太少甚至没有反馈的系统,则让人感觉迟钝和笨拙,用户体验更差。 Alibaba Fusion 设计价值观 5 化繁为简的交互模式 面对互联网产品高迭代节奏和复杂的中后台场景,将复杂的业务组件抽象为用户标准认知层的交互方式,这套组件库来自于阿里巴巴上百个中后台场景的抽象结果,试图建立中后台 web 设计标准。 驾驭技术 你用的所有设计资料,小到 sketch 样式工具中的颜色、字体、字号、投影、边框、尺寸;再到组件,大到一套完整的中后台产品系统,均能找到其对应的代码,完整的释放整个团队的前端生产力。 追求新鲜,潮流 设计风格每年都会更新换代,由于 Alibaba Fusion 设计系统中的颜色、字体、字号、投影等样式均可通过线上配置修改,这也决定了它可以快速(甚至 15 分钟内)完成整套设计系统的样式迭代。 聚变/裂变 通过在 Alibaba Fusion 设计系统原则下,变换样式、多维度定制组件交互形式,可瞬间获取属于自己业务属性的设计系统;我们期待有无数业务线能够通过 Alibaba Fusion 的设计系统原则聚变出符合各类业务场景的 Fusion 生态系统。 效率 Alibaba Fusion Design 希望构建一套提升设计与开发之间 UI 构建效率的工作方式,让 UED 的工作能够尽可能多的投入在 UE(User Experience)的用户调研、用户体验、商业思考,而在 D(Design)的过程中更多的投入与创意而非日复一日的重复绘图。 腾讯 Q 语言理念 6 统一体验 QQ 作为一个社交平台,会容纳多样性的功能与体验,为了降低用户在不同场景功能下的学习成本,并提升易用性,统一体验是提升平台易用性的关键基础。同时有助于提升各角色间的协作效率。 基因体现 当今同质化的社交应用越来越多,QQ 作为横跨多时期多平台的社交应用,一方面需紧贴时代趋势,在众多应用中脱颖而出,另一方面有足够的历史底蕴,应强化自身基因特征,提升整体品牌认知。 社交向善 社交应用会融入琳琅满目的娱乐化规则与玩法,但吸引年轻人的不应只是单纯娱乐消费,需要考虑社交娱乐的本质初心,QQ 更倡导用户在一个积极健康,安全贴心,触动情感的环境进行社交,并最终导人向善。 高效娱乐 伴随信息传播便利性提升,用户需要更高浓度的信息和更快的娱乐方式。用户时间愈加宝贵,偏向消费耗时较短的短视频、信息流等内容,希望更快找到喜欢的内容,以及更高浓度的内容。 兴趣细分 互联网使用场景更细分,兴趣爱好更加细分深入。各种兴趣圈、游戏圈、粉丝圈等年轻用户基于互联网衍生出来的圈子,需要有更细分深入的功能场景去承载。线上和线下联动是细分圈子持续活跃的关键。 社交压力 互联网信息传播的扩散效应,以及社会的复杂性给用户带来更多社交压力,原创越来越少。可以通过丰富的形象建立和维护体系增强用户的社交动力,引导产生更多原创内容和互动。 腾讯 Q 语言原则 7 活力灵动 对年轻人有吸引力,传递积极乐观情感,有怦然心动的感觉。 亲和自然 体验过程犹如与朋友打交道,亲和自然,懂我所想。 自我有范 用户能无压力表达自我,满足不同人群个性诉求。 语素 视觉基础是构成设计语言的最小单位,就像语素是语言中最小的音义结合体。在原子设计理论中,它属于最小粒度的元素,通常包括:色彩、布局、字体、图标等。 色彩 无论 UI 还是平面,颜色是视觉传达的最核心也是最基本的语言,不同的主色,会给人不同的视觉感受,同样的主色不同的配色,视觉感受也会不同。通常一款产品的色彩体系包含:品牌色、功能色、中立色三个部分: 品牌色:代表品牌形象及 VI 识别的色彩,品牌色的数量可以一个也可以多个,用于主按钮、主 icon 等需要突出品牌特征的地方。 功能色:代表明确的信息以及状态,如成功、出错、失败、提醒等。功能色的选取需要遵守用户对色彩的基本认知,如绿色代表成功,红色代表警示或失败。 中立色:灰或饱和度低的颜色,用于界面设计中的字体、背景、边框、分割线等,中立色通常是按照透明度的方式实现。 布局 空间布局是体系化视觉设计的起点,和传统的平面设计的不同之处在于,UI 界面的布局空间要基于「动态、体系化」的角度出发展开。在中后台视觉体系中定义布局系统,可以从 5 个方面出发:统一的画板尺寸、适配方案、网格单位、栅格、常用模度。 统一画板:为了尽可能减少沟通与理解的成本,有必要在组织内部统一设计板的尺寸。 适配:在设计过程中还需要建立适配的概念,根据具体情况判断系统是否需要进行适配,以及哪些区块需要考虑动态布局。 左右布局的适配方案:常被用于左右布局的设计方案中,常见的做法是将左边的导航栏固定,对右边的工作区域进行动态缩放。 上下布局的适配方案:常被用于上下布局的设计方案中,做法是对两边留白区域进行最小值的定义,当留白区域到达限定值之后再对中间的主内容区域进行动态缩放。 网格单位:通过网格体系可以实现视觉体系的秩序。网格的基数为 8,不仅符合偶数的思路同时能够匹配多数主流的显示设备。通过建立网格的思考方式,还能帮助设计者快速实现布局空间的设计决策同时也能简化设计到开发的沟通损耗。 栅格:以上下布局的结构为例,对内容区域进行 24 栅格的划分设置,如下图所示。页面中栅格的 Gutter 设定了定值,即浏览器在一定范围扩大或缩小,栅格的 Column 宽度会随之扩大或缩小,但 Gutter 的宽度值固定不变。 模度:模度是为了帮助不同设计能力的设计者们在界面布局上的一致性和韵律感,统一设计到开发的布局语言,减少还原损耗。 字体 字体是界面设计中最基本的构成之一。通过定义字体在设计上的使用规则,从而在阅读的舒适性上达到平衡。确定字体主要从下面四个方面出发:字体家族、主字体、字阶与行高、字重。 字体家族:优秀的字体系统首先是要选择合适的字体家族。提供一套利于屏显的备用字体库,来维护在不同平台以及浏览器的显示下,字体始终保持良好的易读性和可读性,体现了友好、稳定和专业的特性。在中后台系统中,数字经常需要进行纵向对比展示,将数字的字体 font-variant-numeric 设置为 tabular-nums,使其为等宽字体。 主字体:基于电脑显示器阅读距离(50 cm)以及最佳阅读角度(0.3),将自私设置为 14,以保证在多数常用显示器上的用户阅读效率最佳。 字阶与行高:字阶和行高决定着一套字体系统的动态与秩序之美。字阶是指一系列有规律的不同尺寸的字体。行高可以理解为一个包裹在字体外面的无形的盒子。 字重:字重的选择同样基于秩序、稳定、克制的原则。多数情况下,只出现 regular 以及 medium 的两种字体重量,分别对应代码中的 400 和 500。在英文字体加粗的情况下会采用 semibold 的字体重量,对应代码中的 600。 图标 图标是 UI 设计中必不可少的组成。通常我们理解图标设计的含义,是将某个概念转换成清晰易读的图形,从而降低用户的理解成本,提升界面的美观度。在我们的企业级应用设计范围中,图标在界面设计的诸多元素中往往只占了很小的比重,在调用时也会被缩到比设计稿小很多倍的尺寸,加上在图形素材极度丰富并且便于获取的今天,在产品设计体系中实现一套美观、一致、易用、便于延展的图标体系往往会被不小心忽略掉。 语句 组件就像由若干个语素组成的语句,比如一个基础按钮,通常就是由颜色、字体、边距等元素组成。而我们平时所说的组件库,其实就是一部词典,其中包含了设计系统中所需的基础组件与用法,在界面设计中也具有较高的通用性。 语义 符号是语言的载体,但符号本身没有意义,只有被赋予含义的符号才能够被使用,这时候语言就转化为信息,而语言的含义就是语义。在视觉传达设计中也一样,使用的图标或图形,需具备正确的语义属性。如果商场导视设计中非要使用「裙子」图标来代表「男厕」入口,如此混淆语义挑战公众认知,那就等着被投诉吧。 语境 语境包含 3 个维度:一是流程意义上的上下文,二是产品属性中的语境,三是用户当下所处的环境。 当设计需要对上下文进行特别处理时,有可能对话的层级次序是受限于屏幕稀缺性,通常可采用 z-depth 叠加(Material Design 属性)、步骤条、元素关联转场动效等方式。举个常见的例子,当用户发起一个删除数据的请求时,界面会弹出一个二次确认的模态会话,用户点击确认之后才会执行删除操作。 针对用户当下所处的环境来适配界面语境,常见通过界面换肤的手法来实现,比如微信读书等阅读应用为用户提供白天模式或黑夜模式的选择。用户所处的外部环境因素可以很大程度上决定界面语言的应用,就好像在菜市场买东西要靠吼,在图书馆借书仅需要用肢体语言便能达成。 语气 交互界面通常需要使用说明或提示文案来指导用户完成操作,大多数情况下都是使用第二人称,就像在与用户对话,从以用户为中心的角度上讲,建议保持谦逊、友善的语气,尽可能避免使用晦涩的专业术语,谨慎使用带有强烈情感属性的感叹号,或过于口语化的语言。另外,语气的拿捏也将直接影响到与用户的距离感,以及当下的应景度。 正确示例:使用检索可以快速搜索任务。 不良示例:你一定会爱上方便快捷的检索功能! 语速 语速在这里指的是界面的信息密度,在不同的场合对语速的控制能够提升接受者的体验,视觉设计也同样需要注意把握间距与留白,网格系统在这里可以起到「节拍器」的作用,借助节拍器可以让设计更具节奏感。而交互意义上的语速,更多体现在操作路径的长度,以及动效的速率。 下图分别展示了 QQ 音乐和富途牛牛两种不同场景的「语速」: 响度 其实就好像我们说话可以通过音量大小来控制信息的可感知程度,希望接受者听清楚的就说大声一点。汤姆奥斯本(Tom Osborne)的视觉响度指南(Visual Loudness Guide)是一个如何系统地处理按钮和链接的例子,它们不是单独列出,而是作为一个套件呈现,并且根据每个元素的视觉冲击力会相应的拥有一个「响度」值。我们在构建设计语言系统时,也同样需要设置梯级「响度」的按钮、字重等组件来满足不同场景的表达需求。 设计语言列表 企业 设计语言 Apple Human Interface Guidelines Google Material Design Microsoft Fluent Design Facebook Facebook Design Adobe Spectrum Firefox Photon Design IBM Carbon Design System Airbnb Lottie Salesforce Lightning Design System 蚂蚁金服 Ant Design 阿里巴巴 Fusion Design 腾讯 WeUI 腾讯 Q Design https://en.wikipedia.org/wiki/Design_language ↩︎ https://www.uisdc.com/design-language ↩︎ https://ant.design/docs/spec/values-cn ↩︎ https://ant.design/docs/spec/overview-cn ↩︎ https://fusion.design/pc/doc/design/设计概览/12 ↩︎ https://qq.design/design/QLanguage/Concept/ ↩︎ https://qq.design/design/QLanguage/Principles/ ↩︎

2021/8/8
articleCard.readMore

Spark 集群搭建 (Spark Cluster Setup)

文本使用的软件版本分别为: JDK:1.8.0_291,下载地址。 Scala:2.12.14,下载地址。 Hadoop:3.2.2,下载地址。 Spark:3.1.2,下载地址。 Python:3.9,Miniconda3,下载地址。 按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机。 按照 Hadoop 集群搭建 (Hadoop Cluster Setup) 搭建 Hadoop 集群。 本文以 Spark on YARN 模式介绍 Spark 集群的搭建。 Scala 配置 将 Scala 安装包解压缩到 /opt 目录并创建软链接: cd /opt tar -zxvf scala-2.12.14.tgz ln -s /opt/scala-2.12.14 /opt/scala 将如下信息添加到 /etc/profile 中: # Scala export SCALA_HOME=/opt/scala export PATH=$PATH:$SCALA_HOME/bin 方便起见可以使用 rsync 命令同步 Scala: rsync -auvp /opt/scala-2.12.14 leo@vm-02:/opt rsync -auvp /opt/scala-2.12.14 leo@vm-03:/opt Spark 配置 将 Spark 安装包解压缩到 /opt 目录并创建软链接: cd /opt tar -zxvf spark-3.1.2-bin-hadoop3.2.tgz ln -s /opt/spark-3.1.2-bin-hadoop3.2 /opt/spark 将如下信息添加到 /etc/profile 中: # Spark export SPARK_HOME=/opt/spark export PATH=$PATH:$SPARK_HOME/bin 复制环境变量文件: cp /opt/spark/conf/spark-env.sh.template /opt/spark/conf/spark-env.sh 在 spark-env.sh 结尾添加如下内容: export JAVA_HOME=/opt/jdk export SCALA_HOME=/opt/scala export HADOOP_HOME=/opt/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/conf export YARN_CONF_DIR=$HADOOP_HOME/conf export SPAKR_HOME=/opt/spark export SPARK_CONF_DIR=$SPAKR_HOME/conf export SPARK_EXECUTOR_CORES=1 export SPARK_EXECUTOR_MEMORY=1G export SPARK_DRIVER_MEMORY=1G export SPARK_HISTORY_OPTS="-Dspark.history.retainedApplications=10" 复制配置文件: cp /opt/spark/conf/spark-defaults.conf.template /opt/spark/conf/spark-defaults.conf 修改 spark-defaults.conf 文件内容如下: spark.eventLog.enabled true spark.eventLog.compress true spark.eventLog.dir hdfs://vm-01:9000/logs/spark spark.history.fs.logDirectory hdfs://vm-01:9000/logs/spark spark.yarn.historyServer.address vm-01:18080 spark.yarn.jars hdfs://vm-01:9000/spark/jars/* 复制 Worker 节点列表文件: cp /opt/spark/conf/workers.template /opt/spark/conf/workers 修改 workers 文件内容如下: vm-01 vm-02 vm-03 在 HDFS 上创建目录,并上传 Spark 相关 JAR 包: hdfs dfs -mkdir -p /spark/jars hdfs dfs -put /opt/spark/jars/* /spark/jars/ 方便起见可以使用 rsync 命令同步 Spark: rsync -auvp /opt/spark-3.1.2-bin-hadoop3.2 leo@vm-02:/opt rsync -auvp /opt/spark-3.1.2-bin-hadoop3.2 leo@vm-03:/opt 启动 Spark 在 vm-01,vm-02 和 vm-03 上启动 Zookeeper: zkServer.sh start 启动 Hadoop: /opt/hadoop/sbin/start-dfs.sh /opt/hadoop/sbin/start-yarn.sh 获取并切换 YARN Resource Manager 的状态: yarn rmadmin -getServiceState rm1 yarn rmadmin -getServiceState rm2 yarn rmadmin -transitionToActive rm1 --forcemanual 在 HDFS 上创建相关目录: hdfs dfs -mkdir /logs hdfs dfs -mkdir /logs/spark 启动 Spark: /opt/spark/sbin/start-all.sh 通过 http://vm-01:8081 可以进入 Spark Web 页面: 启动 Spark History Server: /opt/spark/sbin/start-history-server.sh 执行 PI 示例程序: spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ --executor-memory 1G \ --num-executors 3 \ /opt/spark/examples/jars/spark-examples*.jar \ 10 在 YARN 中,通过 Application ID 查看对应的 Container 的 stdout 日志,可以得到示例程序的运行结果: Pi is roughly 3.1424791424791425 通过 http://vm-01:18081 可以进入 Spark History Server 页面: NFS 配置 安装 NFS 相关软件: sudo apt install nfs-kernel-server nfs-common 在 vm-01,vm-02 和 vm-03 上创建 MFS 文件夹并设置权限: sudo mkdir /nfs sudo chown -R leo:leo /nfs 在 vm-01 上修改 /etc/exports 文件,配置 NFS 共享目录: /nfs 192.168.56.1/24(rw,sync,no_root_squash,no_subtree_check) 相关参数定义可以通过 man nfs 获取。 导出共享目录并重启 NFS 服务: sudo exportfs -a sudo service nfs-kernel-server restart 在 vm-02 和 vm-03 上挂在 NFS: sudo mount vm-01:/nfs /nfs 在 /etc/fstab 中添加如下内容实现开机自动挂载: vm-01:/nfs /nfs nfs rw Python 配置 安装 Miniconda3 到 /nfs/miniconda3 目录: sh Miniconda3-py39_4.9.2-Linux-x86_64.sh 在安装过程中安装选项如下: Miniconda3 will now be installed into this location: /home/leo/miniconda3 - Press ENTER to confirm the location - Press CTRL-C to abort the installation - Or specify a different location below [/home/leo/miniconda3] >>> /nfs/miniconda3 Do you wish the installer to initialize Miniconda3 by running conda init? [yes|no] [no] >>> yes 修改 ~/.condarc 更改 Anaconda 镜像: channels: - defaults show_channel_urls: true default_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2 custom_channels: conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud 创建用于 Spark 的 Python 环境: conda create -n spark python=3.9 分别在 vm-02 和 vm-03 中将如下信息添加到 /etc/profile 中: # >>> conda initialize >>> # !! Contents within this block are managed by 'conda init' !! __conda_setup="$('/nfs/miniconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" if [ $? -eq 0 ]; then eval "$__conda_setup" else if [ -f "/nfs/miniconda3/etc/profile.d/conda.sh" ]; then . "/nfs/miniconda3/etc/profile.d/conda.sh" else export PATH="/nfs/miniconda3/bin:$PATH" fi fi unset __conda_setup # <<< conda initialize <<< 分别在 vm-01,vm-02 和 vm-03 中将如下信息添加到 /etc/profile 中: # Python 3.9 for Spark conda activate spark PySpark 测试 输入 pyspark 进入 PySaprk Shell: Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ '_/ /__ / .__/\_,_/_/ /_/\_\ version 3.1.2 /_/ Using Python version 3.9.5 (default, Jun 4 2021 12:28:51) Spark context Web UI available at http://vm-01:4040 Spark context available as 'sc' (master = local[*], app id = local-1623876641615). SparkSession available as 'spark'. >>> 执行 PI 示例程序: from random import random from operator import add partitions = 3 n = 100000 * partitions def f(_): x = random() * 2 - 1 y = random() * 2 - 1 return 1 if x ** 2 + y ** 2 <= 1 else 0 count = spark.sparkContext.parallelize(range(1, n + 1), partitions).map(f).reduce(add) print("Pi is roughly %f" % (4.0 * count / n)) 运行结果如下: Pi is roughly 3.147160

2021/6/19
articleCard.readMore

Hive 安装和配置 (Hive Setup)

文本使用的软件版本分别为: JDK:1.8.0_291,下载地址。 Hadoop:3.2.2,下载地址。 Hive:3.2.1,下载地址。 MySQL:8.0.25,使用 apt install mysql-server 安装。 MySQL JDBC Connector:8.0.25,下载地址。 按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机列表如下: 主机名 IP 角色 vm-01 192.168.56.101 Hadoop MySQL Hive vm-02 192.168.56.102 Hadoop vm-03 192.168.56.103 Hadoop 按照 Hadoop 集群搭建 (Hadoop Cluster Setup) 搭建 Hadoop 集群。 MySQL 安装和配置 通过如下命令安装 MySQL: sudo apt install mysql-server mysql-client libmysqlclient-dev 安装完毕后,使用如下命令初始化 MySQL: sudo mysql_secure_installation 在密码安全性校验步骤,输入 N 关闭密码安全性校验: VALIDATE PASSWORD COMPONENT can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD component? Press y|Y for Yes, any other key for No: N 输入新密码: New password: ********* Re-enter new password: ********* 在删除匿名用户环节,输入 Y 删除匿名用户: By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? (Press y|Y for Yes, any other key for No) : Y 输入 N 允许远程登录 root 用户: Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? (Press y|Y for Yes, any other key for No) : N 输入 N 保留 test 数据库: By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? (Press y|Y for Yes, any other key for No) : N 输入 Y 应用设置并生效: Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? (Press y|Y for Yes, any other key for No) : Y 修改 MySQL 配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf 将绑定地址替换为 0.0.0.0: bind-address = 0.0.0.0 mysqlx-bind-address = 0.0.0.0 重启 MySQL 服务: sudo service mysql restart 在本地通过如下命令并输入密码进入 MySQL: sudo mysql -uroot -p 在 MySQL 命令行中输入如下语句为 root 用户配置允许远程访问: CREATE USER 'root'@'%' IDENTIFIED BY '**********'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; EXIT; 之后在宿主机通过如下命令即可登录虚拟机中的 MySQL: mysql -h192.168.56.101 -uroot -p 为 Hive 创建数据库和用户,并设置相关权限: CREATE DATABASE hive; CREATE USER 'hive'@'%' IDENTIFIED BY '**********'; GRANT ALL PRIVILEGES ON hive.* TO 'hive'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; EXIT; Hive 安装和配置 将 Hive 安装包解压缩到 /opt 目录并创建软链接: cd /opt tar -zxvf apache-hive-3.1.2-bin.tar.gz ln -s /opt/apache-hive-3.1.2-bin /opt/hive 将如下信息添加到 /etc/profile 中: # Hive export HIVE_HOME=/opt/hive export PATH=$PATH:$HIVE_HOME/bin 复制环境变量文件: cp /opt/hive/conf/hive-env.sh.template /opt/hive/conf/hive-env.sh 修改 hive-env.sh 内容如下: export JAVA_HOME=/opt/jdk export HADOOP_HOME=/opt/hadoop export HIVE_HOME=/opt/hive export HIVE_CONF_DIR=$HIVE_HOME/conf 创建配置文件: vi /opt/hive/conf/hive-site.xml 修改 hive-site.xml 内容如下: <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://vm-01:3306/hive</value> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.cj.jdbc.Driver</value> </property> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>hive</value> </property> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>*********</value> </property> </configuration> 将 MySQL JDBC Connector 解压缩到 /opt/hive/lib 中: cd /opt tar -zxvf mysql-connector-java-8.0.25.tar.gz mv /opt/mysql-connector-java-8.0.25/mysql-connector-java-8.0.25.jar /opt/hive/lib rm -rf /opt/mysql-connector-java-8.0.25 修正不兼容的依赖包: mv /opt/hive/lib/log4j-slf4j-impl-2.10.0.jar /opt/hive/lib/log4j-slf4j-impl-2.10.0.jar.bak mv /opt/hive/lib/guava-19.0.jar /opt/hive/lib/guava-19.0.jar.bak cp /opt/hadoop/share/hadoop/common/lib/guava-*.jar /opt/hive/lib 初始化元数据: schematool -dbType mysql -initSchema 出现如下输出时表示元数据初始化成功: Metastore connection URL: jdbc:mysql://vm-01:3306/hive Metastore Connection Driver : com.mysql.cj.jdbc.Driver Metastore connection User: hive Starting metastore schema initialization to 3.1.0 Initialization script hive-schema-3.1.0.mysql.sql ...... Initialization script completed schemaTool completed 启动 Hive 执行如下命令启动 Hive: hive 出现如下输出时表示启动成功: Hive Session ID = f3edb53b-5037-47c3-b318-75854e2328c5 Logging initialized using configuration in jar:file:/opt/apache-hive-3.1.2-bin/lib/hive-common-3.1.2.jar!/hive-log4j2.properties Async: true Hive Session ID = 3331472a-a171-47be-843a-378611233f18 Hive-on-MR is deprecated in Hive 2 and may not be available in the future versions. Consider using a different execution engine (i.e. spark, tez) or using Hive 1.X releases. hive>

2021/6/14
articleCard.readMore

Hadoop 集群搭建 (Hadoop Cluster Setup)

文本使用的软件版本分别为: JDK:1.8.0_291,下载地址。 Zookeeper:3.7.0,下载地址。 Hadoop:3.2.2,下载地址。 按照虚拟环境准备 (Virtual Environment Preparation) 准备虚拟机列表如下: 主机名 IP 角色 vm-01 192.168.56.101 HDFS Namenode HDFS Datanode YARN Resource Manager YARN Node Manager Journal Node Zookeeper vm-02 192.168.56.102 HDFS Namenode HDFS Datanode YARN Resource Manager YARN Node Manager Journal Node Zookeeper vm-03 192.168.56.103 HDFS Namenode HDFS Datanode YARN Node Manager Journal Node Zookeeper 系统配置 将 /opt 目录所有者赋予当前用户: sudo chown -R leo:leo /opt 在根目录建立 data 目录,并将其所有者赋予当前用户: sudo mkdir /data sudo chown -R leo:leo /data JDK 配置 将 JDK 安装包解压缩到 /opt 目录并创建软链接: cd /opt tar -zxvf jdk-8u291-linux-x64.tar.gz ln -s /opt/jdk1.8.0_291 /opt/jdk 将如下信息添加到 /etc/profile 中: # JDK export JAVA_HOME=/opt/jdk export PATH=$PATH:$JAVA_HOME/bin 方便起见可以使用 rsync 命令同步 JDK: rsync -auvp /opt/jdk1.8.0_291 leo@vm-02:/opt rsync -auvp /opt/jdk1.8.0_291 leo@vm-03:/opt Zookeeper 配置 将 Zookeeper 安装包解压到 /opt 目录并创建软链接: cd /opt tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz ln -s /opt/apache-zookeeper-3.7.0-bin /opt/zookeeper 将如下信息添加到 /etc/profile 中: # Zookeeper export ZOOKEEPER_HOME=/opt/zookeeper export PATH=$PATH:$ZOOKEEPER_HOME/bin 在 /data 目录下创建 zookeeper 文件夹: mkdir /data/zookeeper 在 /data/zookeeper 目录中创建 myid 文件: echo 1 > /data/zookeeper/myid # 仅在 vm-01 上执行 echo 2 > /data/zookeeper/myid # 仅在 vm-02 上执行 echo 3 > /data/zookeeper/myid # 仅在 vm-03 上执行 复制 Zookeeper 配置文件: cd /opt/zookeeper/conf mv zoo_sample.cfg zoo.cfg 修改 zoo.cfg 文件内容如下: # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=/data/zookeeper # servers server.1=vm-01:2888:3888 server.2=vm-02:2888:3888 server.3=vm-03:2888:3888 方便起见可以使用 rsync 命令同步 Zookeeper: rsync -auvp /opt/apache-zookeeper-3.7.0-bin leo@vm-02:/opt rsync -auvp /opt/apache-zookeeper-3.7.0-bin leo@vm-03:/opt Hadoop 配置 将 Hadoop 安装包解压到 /opt 目录并创建软链接: cd /opt tar -zxvf /hadoop-3.2.2.tar.gz ln -s /opt/hadoop-3.2.2 /opt/hadoop 将如下信息添加到 /etc/profile 中: # Hadoop export HADOOP_HOME=/opt/hadoop export PATH=$PATH:$HADOOP_HOME/bin 在 /data 目录下创建如下文件夹: mkdir /data/hadoop mkdir /data/hadoop/tmp mkdir /data/hadoop/pid mkdir /data/hadoop/logs mkdir /data/hadoop/hdfs mkdir /data/hadoop/hdfs/journalnode mkdir /data/hadoop/hdfs/namenode mkdir /data/hadoop/hdfs/datanode 编辑 /opt/hadoop/etc/hadoop/core-site.xml 内容如下: <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <!-- HDFS 的 nameservice --> <name>fs.defaultFS</name> <value>hdfs://ns1/</value> </property> <property> <!-- Hadoop 临时目录 --> <name>hadoop.tmp.dir</name> <value>/data/hadoop/tmp</value> </property> <property> <!-- Zookeeper 地址 --> <name>ha.zookeeper.quorum</name> <value>vm-01:2181,vm-02:2181,vm-03:2181</value> </property> </configuration> 编辑 /opt/hadoop/etc/hadoop/hdfs-site.xml 内容如下: <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <!-- HDFS 的 nameservice --> <name>dfs.nameservices</name> <value>ns1</value> </property> <property> <!-- namenode 列表 --> <name>dfs.ha.namenodes.ns1</name> <value>nn1,nn2,nn3</value> </property> <property> <!-- nn1 的 RPC 通信地址 --> <name>dfs.namenode.rpc-address.ns1.nn1</name> <value>vm-01:9000</value> </property> <property> <!-- nn2 的 RPC 通信地址 --> <name>dfs.namenode.rpc-address.ns1.nn2</name> <value>vm-02:9000</value> </property> <property> <!-- nn3 的 RPC 通信地址 --> <name>dfs.namenode.rpc-address.ns1.nn3</name> <value>vm-03:9000</value> </property> <property> <!-- nn1 的 HTTP 通信地址 --> <name>dfs.namenode.http-address.ns1.nn1</name> <value>vm-01:50070</value> </property> <property> <!-- nn2 的 HTTP 通信地址 --> <name>dfs.namenode.http-address.ns1.nn2</name> <value>vm-02:50070</value> </property> <property> <!-- nn3 的 HTTP 通信地址 --> <name>dfs.namenode.http-address.ns1.nn3</name> <value>vm-03:50070</value> </property> <property> <!-- namenode 在 journalnode 上的存放位置 --> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://vm-01:8485;vm-02:8485;vm-03:8485/ns1</value> </property> <property> <!-- journalnode 在磁盘上的存放位置 --> <name>dfs.journalnode.edits.dir</name> <value>/data/hadoop/hdfs/journalnode</value> </property> <property> <!-- 开启 namenode 失败自动切换 --> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property> <property> <!-- 配置失败自动切换实现方式 --> <name>dfs.client.failover.proxy.provider.ns1</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property> <property> <!-- 配置隔离机制方法,多个机制用换行分割,即每个机制暂用一行 --> <name>dfs.ha.fencing.methods</name> <value>sshfence</value> </property> <property> <!-- 使用 sshfence 隔离机制时需要 ssh 免密登陆 --> <name>dfs.ha.fencing.ssh.private-key-files</name> <value>/homt/leo/.ssh/id_rsa</value> </property> <property> <!-- 配置 sshfence 隔离机制超时时间 --> <name>dfs.ha.fencing.ssh.connect-timeout</name> <value>30000</value> </property> <property> <!-- journalnode HTTP 通信地址 --> <name>dfs.journalnode.http-address</name> <value>0.0.0.0:8480</value> </property> <property> <!-- journalnode RPC 通信地址 --> <name>dfs.journalnode.rpc-address</name> <value>0.0.0.0:8485</value> </property> <property> <!-- HDFS 副本数量 --> <name>dfs.replication</name> <value>1</value> </property> <property> <!-- namenode 在磁盘上的存放位置 --> <name>dfs.namenode.name.dir</name> <value>/data/hadoop/hdfs/namenode</value> </property> <property> <!-- datanode 在磁盘上的存放位置 --> <name>dfs.datanode.data.dir</name> <value>/data/hadoop/hdfs/datanode</value> </property> <property> <!--开启 webhdfs 接口访问 --> <name>dfs.webhdfs.enabled</name> <value>true</value> </property> <property> <!-- 关闭权限验证,hive 可以直连 --> <name>dfs.permissions.enabled</name> <value>false</value> </property> </configuration> 编辑 /opt/hadoop/etc/hadoop/yarn-site.xml 内容如下: <?xml version="1.0"?> <configuration> <property> <!-- 开启 resourc emanager 高可用 --> <name>yarn.resourcemanager.ha.enabled</name> <value>true</value> </property> <property> <!-- 指定 resourc emanager 的 cluster id --> <name>yarn.resourcemanager.cluster-id</name> <value>leo</value> </property> <property> <!-- 指定 resourc emanager 的名字 --> <name>yarn.resourcemanager.ha.id</name> <value>rm1</value> </property> <property> <!-- 指定 resourc emanager 的名字 --> <name>yarn.resourcemanager.ha.rm-ids</name> <value>rm1,rm2</value> </property> <property> <!-- 指定 resourc emanager 1 的地址 --> <name>yarn.resourcemanager.hostname.rm1</name> <value>vm-01</value> </property> <property> <!-- 指定 resourc emanager 2 的地址 --> <name>yarn.resourcemanager.hostname.rm2</name> <value>vm-02</value> </property> <property> <!-- 指定 zookeeper 集群地址 --> <name>yarn.resourcemanager.zk-address</name> <value>vm-01:2181,vm-02:2181,vm-03:2181</value> </property> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> </configuration> 提示 需要在 vm-02 中将 yarn.resourcemanager.ha.id 的值设置为 rm2,在 vm-03 中删除 yarn.resourcemanager.ha.id 属性。 编辑 /opt/hadoop/etc/hadoop/mapred-site.xml 内容如下: <?xml version="1.0"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <property> <name>mapreduce.application.classpath</name> <value> /opt/Hadoop/share/hadoop/common/*, /opt/hadoop/share/hadoop/common/lib/*, /opt/hadoop/share/hadoop/hdfs/*, /opt/hadoop/share/hadoop/hdfs/lib/*, /opt/hadoop/share/hadoop/mapreduce/*, /opt/hadoop/share/hadoop/mapreduce/lib/*, /opt/hadoop/share/hadoop/yarn/*, /opt/hadoop/share/hadoop/yarn/lib/* </value> </property> </configuration> 修改 /opt/hadoop/etc/hadoop/hadoop-env.sh 内容如下: export JAVA_HOME=/opt/jdk export HADOOP_LOG_DIR=/data/hadoop/logs export HADOOP_PID_DIR=/data/hadoop/pid 修改 /opt/hadoop/etc/hadoop/yarn-env.sh 内容如下: export JAVA_HOME=/opt/jdk 在 /opt/hadoop/sbin/start-dfs.sh 和 /opt/hadoop/sbin/stop-dfs.sh 开始位置添加: HDFS_NAMENODE_USER=leo HDFS_DATANODE_USER=leo HDFS_JOURNALNODE_USER=leo HDFS_ZKFC_USER=leo 在 /opt/hadoop/sbin/start-yarn.sh 和 /opt/hadoop/sbin/stop-yarn.sh 开始位置添加: YARN_RESOURCEMANAGER_USER=leo YARN_NODEMANAGER_USER=leo 修改 /opt/hadoop/etc/hadoop/workers 内容如下: vm-01 vm-02 vm-03 方便起见可以使用 rsync 命令同步 Hadoop: rsync -auvp /opt/hadoop-3.2.2 leo@vm-02:/opt rsync -auvp /opt/hadoop-3.2.2 leo@vm-03:/opt 启动集群 启动 zookeeper 分别在 vm-01,vm-02 和 vm-03 上执行如下操作: zkServer.sh start 当所有虚拟机 zookeeper 启动完毕后,执行如下操作: zkServer.sh status 可能得到如下输出: ZooKeeper JMX enabled by default Using config: /opt/zookeeper/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Client SSL: false. Mode: leader 或 ZooKeeper JMX enabled by default Using config: /opt/zookeeper/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Client SSL: false. Mode: follower 启动 journalnode 分别在 vm-01,vm-02 和 vm-03 上执行如下操作: hdfs --daemon start journalnode 格式化 namenode 在 vm-01 上执行如下操作: hdfs namenode -format 将格式化之后的元数据到其他 namenode: rsync -auvp /data/hadoop/hdfs/namenode/current vm-01:/data/hadoop/hdfs/namenode/current rsync -auvp /data/hadoop/hdfs/namenode/current vm-02:/data/hadoop/hdfs/namenode/current 在 vm-01 格式化 zookeeper: hdfs zkfc -formatZK 停止 journalnode 分别在 vm-01,vm-02 和 vm-03 上执行如下操作: hdfs --daemon stop journalnode 启动 hadoop 在 vm-01 启动 DFS: /opt/hadoop/sbin/start-dfs.sh 会得到如下输出: Starting namenodes on [vm-01 vm-02 vm-03] Starting datanodes Starting journal nodes [vm-01 vm-03 vm-02] Starting ZK Failover Controllers on NN hosts [vm-01 vm-02 vm-03] 在 vm-01 启动 YARN: /opt/hadoop/sbin/start-yarn.sh 会得到如下输出: Starting resourcemanagers on [vm-01 vm-02] Starting nodemanagers 通过 http://vm-01:50070 可以进入 Hadoop Web 页面: 通过 http://vm-01:8088 可以进入 YARN 页面:

2021/6/13
articleCard.readMore

虚拟环境准备 (Virtual Environment Preparation)

本文以 VirtualBox 和 Ubuntu Server 为例,介绍在 macOS 下搭建 3 台虚拟机集群过程。 安装 VirtualBox 从官网下载最新版本的 VirtualBox 和 VirtualBox Extension Pack,本文以 6.1.22 版本为例。根据安装向导安装 VirtualBox。 双击下载好的 .vbox-extpack 安装文件安装 VirtualBox Extension Pack。 通过 File -> Host Network Manager... 为 VirtualBox 添加一块网卡。 安装 Ubuntu Server 虚拟机配置 从官网下载最新版本的 Ubuntu Server,本文以 20.04.2 LTS 版本为例。在 VirtualBox 通过 New 按钮添加新的虚拟机,首先为虚拟机配置名称,存储路径和内存大小等基本信息: 单击 Create 后为虚拟机配置磁盘类型和大小: 单击 Create 完成创建。创建完毕后,在左侧列表中选择创建好的虚拟机,通过 Settings 按钮打开设置对话框,在 System - Motherboard 标签页去除掉软盘启动 Boot Order - Floppy,同时也可以再次调整内存大小: 在 System - Processor 标签页可以调整虚拟机使用 CPU 的数量: 在 Display - Acceleration 标签页可以调整使用的现存大小: 在 Network - Adapter 1 标签页选择 NAT 网络类型,该网卡用于虚拟机连接外部网络: 在 Network - Adapter 2 标签页选择 Host-only Adapter 网络类型,名称选择上文 VirtualBox 配置的 vboxnet0,该网卡用于虚拟机连接内部网络: 在 Network - Adapter 2 标签页为光驱选择挂载的磁盘镜像: 添加下载好的 Ubuntu Server ISO 磁盘镜像: Ubuntu Server 安装配置 单击 Start 按钮启动虚拟机,启动后等待片刻,在语言选择页面选择 English: 在键盘布局页面采用默认配置,Done 进入下一步: 在网络配置页面,上文中配置了两块网卡,在此我们需要对于 Host-only Adapter 网卡 enp0s8 进行配置,NAT 网卡 enp0s3 采用默认即可: 进入配置后选择手动 Nanual,在 Subnet 中输入 192.168.56.0/24(参考上文中 VirtualBox 添加的网卡设置),在 Address 中输入 192.168.56.1,在 Gateway 中输入 192.168.56.1,Save 进行保存: 配置完后,结果如下,Done 进入下一步: 在镜像配置页面,根据实际情况选择一个合适的镜像地址,本文采用清华大学的镜像地址 https://mirrors.tuna.tsinghua.edu.cn/ubuntu/,Done 进入下一步: 在存储配置页面,为了方便起见选择使用整个磁盘,Done 进入下一步: 确认磁盘配置信息,Done 进入下一步: 在用户信息配置页面,输入用户名和密码等信息,Done 进入下一步: 在 SSH 配置页面,选择安装 OpenSSH server,Done 进入下一步: 在软件包配置页面,跳过安装,Done 进入下一步: 安装完毕后,Reboot Now 重启虚拟机: 配置 Ubuntu Server 克隆虚拟机 重启后,输入用户名和密码即可进入系统: 通过如下命令更新系统软件到最新版本: sudo apt update sudo apt upgrade sudo apt autoremove 输入以下命令进行关机: sudo shutdown now 右键单击虚拟机 Ubuntu Server 1,选择 Clone 对虚拟机进行克隆,选择为所有网卡重新生成 MAC 地址,然后单击 Continue: 选择 Full clone 模式,单击 Continue 完成克隆: 配置网络 分别进入虚拟机 2 和 3,修改固定 IP 地址为 191.168.56.102 和 191.168.56.103。 sudo vi /etc/netplan/00-installer-config.yaml # This is the network config written by 'subiquity' network: ethernets: enp0s3: dhcp4:true enp0s8: addresses: - 192.168.56.101/24 gateway4: 192.168.56.1 nameservers: addresses: [] search: [] version: 2 将配置文件中 00-installer-config.yaml 的 192.168.56.101 修改为对应的 IP。然后执行: sudo netplan apply 让配置生效。 为三台虚拟机修改主机名为 vm-01,vm-02 和 vm-03: sudo hostnamectl set-hostname vm-0x 为三台虚拟机设置 IP 和主机名映射: sudo vi /etc/hosts 在结尾添加: # VM 192.168.56.101 vm-01 192.168.56.102 vm-02 192.168.56.103 vm-03 配置 SSH 分别进入三台虚拟机并生成密钥: ssh-keygen -t rsa 这会在 ~/.ssh 目录生成一对密钥,其中 id_rsa 是私钥,id_rsa.pub 是公钥。 将三台机器中的 id_rsa.pub 导出合并到 ~/.ssh/authorized_keys 文件中,为了方便可以将自己电脑的 id_rsa.pub 也合并到其中实现免密登录。 登录任意每台虚拟机,通过如下命令测试是否可以免密登录: ssh vm-01 ssh vm-02 ssh vm-03

2021/6/12
articleCard.readMore

大数据 SQL 性能调优 (Big Data SQL Performance Tuning)

在日常工作中,数据处理和分析在研发、产品和运营等多个领域起着重要的作用。在海量数据处理和分析中,SQL 是一项基础且重要的能力。一个优秀的 SQL Boy 和茶树姑的 SQL 代码除了保持简单、可读和易于维护的样式风格外,还需要具备良好的执行性能,准确且高效的计算出结果才能让你在工作中决胜于千里之外。 影响 SQL 执行性能的主要因素可以总结为如下几项: 计算资源量(CPU,内存,网络等) 计算数据量(输入和输出的记录数) 计算复杂度(业务逻辑复杂程度和对应的 SQL 实现和执行) 计算资源量是一个前置制约因素,理论上更多的资源能够带来更快的计算效果。计算数据量也可以认为是一个前置制约因素,理论上更大的数据量会导致计算速度降低,但对于复杂的计算逻辑,通过合理的 SQL 可以更好的控制计算过程中的数据量,从而提升 SQL 性能。计算复杂度是影响 SQL 性能的关键因素,复杂的业务逻辑必然比简单的业务逻辑处理时间要长,相同业务逻辑的不同 SQL 实现也会影响运行效率,这就要求我们对业务逻辑进行全面的理解,对实现 SQL 进行合理优化,从而提升计算速度。 执行引擎 SQL 是用于一种用于数据定义和数据操纵的特定目的的编程语言 1。SQL 虽然有 ISO 标准 2,但大部分 SQL 代码在不同的数据库系统中并不具有完全的跨平台性。不同的执行引擎也会对 SQL 的语法有相应的改动和扩展,同时对于 SQL 的执行也会进行不同的适配和优化。因此,脱离执行引擎的 SQL 性能优化是不可取的。 Hive Apache Hive 是一个建立在 Hadoop 架构之上的数据仓库。可以将结构化的数据文件映射为一张数据库表,并提供简单的 SQL 查询功能,可以将 SQL 语句转换为 MapReduce 任务进行运行。因此 MapReduce 是 Hive SQL 运行的核心和根基。 我们以 Word Count 为例简单介绍一下 MapReduce 的原理和过程,Word Count 的 MapReduce 处理过程如下图所示: Input:程序的输入数据。 Splitting:讲输入数据分割为若干部分。 Mapping:针对 Splitting 分割的每个部分,对应有一个 Map 程序处理。本例中将分割后的文本统计成 <K,V> 格式,其中 K 为单词,V 为该单词在这个 Map 中出现的次数。 Shuffling:对 Mapping 的相关输出结果进行合并。本例中将具有相同 K 的统计结果合并到一起。 Reducing:对 Shuffling 合并的结果进行汇总。本例中讲相同 K 的 V 值进行加和操作并返回单个统计结果。 Merged:对 Reducing 的结果进行融合形成最终输出。 Spark Apache Spark 是一个用于大规模数据处理的统一分析引擎,Spark SQL 则作为 Apache Spark 用于处理结构化数据的模块。 Spark 中常见的概念有: RDD:Resilient Distributed Dataset,弹性分布式数据集,是分布式内存中一个抽象概念,提供了一种高度受限的共享内存模型。 DAG:Directed Acyclic Graph,有向无环图,反应了 RDD 之间的依赖关系。 Driver Program:控制程序,负责为 Application 创建 DAG,通常用 SparkContext 代表 Driver Program。 Cluster Manager:集群管理器,负责分配计算资源。 Worker Node:工作节点,负责具体计算。 Executor:运行在 Worker Node 上的一个进程,负责运行 Task,并为 Application 存储数据。 Application:Spark 应用程序,包含多个 Executor。 Task:任务,运行在 Executor 上的工作单元,是 Executor 中的一个线程。 Stage:一组并行的 Task,Spark 一般会根据 Shuffle 类算子(例如:reduceByKey 或 join 等)划分 Stage。 Job:一组 Stage 的集合,一个 Job 包含多个 RDD 及作用于 RDD 上的操作。 相关概念构成了 Spark 的整体架构,如下图所示: 在 Spark 中,一个任务的执行过程大致分为 4 个阶段,如下图所示: 定义 RDD 的 Transformations 和 Actions 算子 3,并根据这些算子形成 DAG。 根据形成的 DAG,DAGScheduler 将其划分为多个 Stage,每个 Stage 包含多个 Task。 DAGScheduler 将 TaskSet 交由 TaskScheduler 运行,并将执行完毕后的结果返回给 DAGScheduler。 TaskScheduler 将任务分发到每一个 Worker 去执行,并将执行完毕后的结果返回给 TaskScheduler。 Spark 相比于 Hadoop 的主要改进有如下几点: Hadoop 的 MapReduce 的中间结果都会持久化到磁盘上,而 Spark 则采用基于内存的计算(内存不足时也可选持久化到磁盘上),从而减少 Shuffle 数据,进而提升计算速度。 Spark 采用的 DAG 相比于 Hadoop 的 MapReduce 具有更好的容错性和可恢复性,由于 Spark 预先计算出了整个任务的 DAG,相比于 MapReduce 中各个操作之间是独立的,这更有助于进行全局优化。 Presto Presto 是一种用于大数据的高性能分布式 SQL 查询引擎。Presto 与 Hive 执行任务过程的差异如下图所示: Presto 的优点主要有如下几点: 基于内存计算,减少了磁盘 IO,从而计算速度更快。 能够连接多个数据源,跨数据源连表查询。 虽然 Presto 能够处理 PB 级数据,但并不代表 Presto 会把 PB 级别数据都放在内存中计算。而是根据场景,例如 COUNT 和 AVG 等聚合操作,是边读数据边计算,再清理内存,再读取数据计算,这种情况消耗的内存并不高。但是连表查询,可能产生大量的临时数据,从而速度会变慢。 性能调优 本节关于 SQL 性能调优的建议主要针对 Hive,Spark 和 Presto 这类大数据 OLAP 执行引擎设计,其他执行引擎不一定完全适用。 下文性能调优中均以如下两张表为例进行说明: CREATE TABLE IF NOT EXISTS sku_order ( order_id STRING '订单 ID', sku_id STRING '商品 ID', sale_quantity BIGINT '销售数量' ) COMMENT '商品订单表' PARTITIONED BY ( dt STRING COMMENT '日期分区' ) ; CREATE TABLE IF NOT EXISTS sku_info ( sku_id STRING '商品 ID', sku_name STRING '商品名称', category_id STRING '品类 ID', category_name STRING '品类名称' ) COMMENT '商品信息表' 减少数据量 限定查询分区。对于包含分区的数据表(例如:日期分区),通过合理限定分区来减少数据量,避免全表扫描。 限定查询字段。避免使用 SELECT *,仅选择需要的字段。SELECT * 会通过查询元数据获取字段信息,同时查询所有字段会造成更大的网络开销。 在关联前过滤数据。应在进行数据表关联之前按照业务逻辑进行数据过滤,从而提升执行效率。 数据倾斜 在 Shuffle 阶段,需要将各节点上相同的 Key 拉取到某个节点(Task)上处理,如果某个 Key 对应的数据量特别大则会产生数据倾斜。结果就是该 Task 运行的时间要远远大于其他 Task 的运行时间,从而造成作业整体运行缓慢,数据量过大甚至可能导致某个 Task 出现 OOM。 在 SQL 中主要有如下几种情况会产生数据倾斜: JOIN 导致的数据倾斜:两表关联,关联字段的无效值(例如:NULL)或有效值过多,可能会导致数据倾斜。 GROUP BY 导致的数据倾斜:当 GROUP BY 的字段(或字段组合)中,Key 分布不均,可能会导致数据倾斜。 DISTINCT 导致的数据倾斜:当 DISTINCT 的字段(或字段组合)中,Key 分布不均,可能会导致数据倾斜。 对于不同的数据倾斜情况,解决方案如下: 对于 JOIN 中的无效值进行过滤。 SELECT category_name, SUM(sale_quantity) AS sale_quantity FROM ( SELECT sku_id, sale_quantity FROM sku_order WHERE dt = '20210523' AND sku_id IS NOT NULL ) AS sku_order_filtered LEFT JOIN sku_info ON sku_order_filtered.sku_id = sku_info.sku_id GROUP BY category_name ; 对于 JOIN 开启 Map Join 或 Broadcast Join 策略,将小表广播到每个 Executor 上来避免产生 Shuffle,从而使得 JOIN 能够快速完成。 set spark.sql.autoBroadcastJoinThreshold=10485760; 对于 JOIN 中存在数据倾斜的 KEY 进行打散处理。 SELECT category_name, SUM(sale_quantity) AS sale_quantity FROM ( SELECT IF(sku_id IN (0000, 9999), CONCAT(sku_id, '_', CEIL(RAND() * 10)), sku_id) AS sku_id, sale_quantity FROM sku_order WHERE dt = '20210523' ) AS sku_order_modified LEFT JOIN ( SELECT sku_id, category_name, FROM sku_info WHERE sku_id NOT IN (0000, 9999) UNION ALL SELECT CONCAT(sku_id, '_', suffix) AS sku_id, category_name FROM ( SELECT sku_id, SPLIT('1,2,3,4,5,6,7,8,9,10', ',') AS suffix_list, category_name FROM sku_info WHERE sku_id IN (0000, 9999) ) sku_info_tmp LATERAL VIEW EXPLODE(suffix_list) sku_info_suffix AS suffix ) sku_info_all ON sku_order_modified.sku_id = sku_info_all.sku_id GROUP BY category_name ; 对于 GROUP BY 导致的数据倾斜采用两步聚合。 SELECT IF(sku_is_null = 1, NULL, sku_id) AS sku_id, SUM(sale_quantity) AS sale_quantity FROM ( SELECT sku_id, sku_is_null, SUM(sale_quantity) AS sale_quantity FROM ( SELECT IF(sku_id IS NULL, CONCAT(sku_id, CEIL(RAND() * 10)), sku_id) AS sku_id, IF(sku_id IS NULL, 1, 0) AS sku_is_null, sale_quantity FROM sku_order WHERE dt = '20210523' ) sku_order_modified GROUP BY sku_id, sku_is_null ) sku_order_group GROUP BY IF(sku_is_null = 1, NULL, sku_id) ; 对于 DISTINCT 导致的数据倾斜,可以改写为 GROUP BY 实现,从而通过多个 Task 计算避免数据倾斜。 /* COUNT DISTINCT */ SELECT COUNT(DISTINCT sku_id) AS cnt FROM sku_order WHERE dt = '20210523' ; /* GROUP BY */ SELECT COUNT(1) AS cnt FROM ( SELECT sku_id FROM sku_order WHERE dt = '20210523' GROUP BY sku_id ) AS sku_stats ; 其他建议 使用 Common Table Expressions (CTEs) 而非子查询。WITH 语句产生的结果类似临时表,可以重复使用,从而避免相同逻辑业务重复计算。 使用 LEFT SEMI JOIN 而非 IN 和子查询。Hive 在 0.13 后的版本中才在 IN 和 NOT IN 中支持子查询。 /* BAD */ SELECT order_id, sku_id, sale_quantity FROM sku_order WHERE sku_id IN (SELECT sku_id FROM sku_info) ; /* GOOD */ SELECT order_id, sku_id, sale_quantity FROM sku_order LEFT SEMI JOIN sku_info ON sku_order.sku_id = sku_info.sku_id ; 参数调优 除了 SQL 本身逻辑的优化外,执行引擎的相关参数设置也会影响 SQL 的执行性能。本小节以 Spark 引擎为例,总结相关参数的设置及其影响。 动态分区 /* 以下 Hive 参数对 Spark 同样有效 */ /* 是否启用动态分区功能 */ set hive.exec.dynamic.partition=true; /* strict 表示至少需要指定一个分区,nonstrict 表示可以全部动态指定分区 */ set hive.exec.dynamic.partition.mode=nonstrict; /* 动态生成分区的最大数量 */ set hive.exec.max.dynamic.partitions=1000; 资源申请 /* 每个 Executor 中的核数 */ set spark.executor.cores=2; /* Executor 的内存总量。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */ set spark.executor.memory=4G; /* Executor 的堆外内存大小,由 YARN 控制,单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */ set spark.yarn.executor.memoryOverhead=1024; /* Driver 的内存总量,主要用于存放任务执行过程中 Shuffle 元数据,以及任务中 Collect 的数据,Broadcast 的小表也会先存放在 Driver 中。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */ set spark.driver.memory=8G; /* Driver 的堆外内存,由 YARN 控制,单位为 MB。YARN 中 Container 的内存限制为 spark.executor.memory + spark.yarn.executor.memoryOverhead <= 16G。 */ set spark.yarn.driver.memoryOverhead=1024; /* storage memory + execution memory 占总内存(java heap-reserved memory)的比例。executor jvm 中内存分为 storage、execution 和 other 内存。storage 存放缓存 RDD 数据,execution 存放 Shuffle 过程的中间数据,other 存放用户定义的数据结构或 Spark 内部元数据。如果用户自定义数据结构较少,可以将该参数比例适当上调。 */ set spark.memory.fraction=0.7; 动态分配 开启动态分配,Spark 可以根据当前作业负载动态申请和释放资源: set spark.dynamicAllocation.enabled=true; 同时需要设置同一时刻可以申请的最小和最大 Executor 数量: set spark.dynamicAllocation.minExecutors=10; set spark.dynamicAllocation.maxExecutors=100; 小文件合并 /* 小文件合并阈值,如果生成的文件平均大小低于阈值会额外启动一轮 Stage 进行小文件的合并,默认不合并小文件。 */ set spark.sql.mergeSmallFileSize=67108864; /* 设置额外的合并 Job 时的 Map 端输入大小 */ set spark.sql.targetBytesInPartitionWhenMerge=67108864; /* 设置 Map 端输入的合并文件大小 */ set spark.hadoopRDD.targetBytesInPartition=67108864; 在决定一个目录是否需要合并小文件时,会统计目录下的平均大小,然后和 spark.sql.mergeSmallFileSize 比较。在合并文件时,一个 Map Task 读取的数据量取决于下面三者的较大值:spark.sql.mergeSmallFileSize,spark.sql.targetBytesInPartitionWhenMerge,spark.hadoopRDD.targetBytesInPartition。 Shuffle 相关 当大表 JOIN 小表时,如果小表足够小,可以将小表广播到所有 Executor 中,在 Map 阶段完成 JOIN。如果该值设置太大,容易导致 Executor 出现 OOM。 /* 10 * 1024 * 1024, 10MB */ set spark.sql.autoBroadcastJoinThreshold=10485760; 设置 Reduce 阶段的分区数: set spark.sql.shuffle.partitions=1000; 设置过大可能导致很多 Reducer 同时向一个 Mapper 拉取数据,导致 Mapper 由于请求压力过大而挂掉或响应缓慢,从而 fetch failed。 一些其他 Shuffle 相关的配置如下: /* 同一时刻一个 Reducer 可以同时拉取的数据量大小 */ set spark.reducer.maxSizeInFlight=25165824; /* 同一时刻一个 Reducer 可以同时产生的请求数 */ set spark.reducer.maxReqsInFlight=10; /* 同一时刻一个 Reducer 向同一个上游 Executor 拉取的最多 Block 数 */ set spark.reducer.maxBlocksInFlightPerAddress=1; /* Shufle 请求的 Block 超过该阈值就会强制落盘,防止一大堆并发请求将内存占满 */ set spark.reducer.maxReqSizeShuffleToMem=536870911; /* Shuffle 中连接超时时间,超过该时间会 fetch failed */ set spark.shuffle.io.connectionTimeout=120; /* Shuffle 中拉取数据的最大重试次数 */ set spark.shuffle.io.maxRetries=3; /* Shuffle 重试的等待间隔 */ set spark.shuffle.io.retryWait=5; ORC 相关 ORC 文件的格式如下图所示: 其中,Postscript 为文件描述信息,包括 File Footer 和元数据长度、文件版本、压缩格式等;File Footer 是文件的元数据信息,包括数据量、每列的统计信息等;文件中的数据为 Stripe,每个 Stripe 包括索引数据、行数据和 Stripe Footer。更多有关 ORC 文件格式的信息请参见 ORC Specification v1 。 在读取 ORC 压缩表时,可以控制生成 Split 的策略,包括: BI:以文件为力度进行 Split 划分 ETL:将文件进行切分,多个 Stripe 组成一个 Split HYBRID:当文件的平均大小大于 Hadoop 最大 Split 值时使用 ETL 策略,否则使用 BI 策略 对于一些较大的 ORC 表,可能其 Footer 较大,ETL 策略可能会导致从 HDFS 拉取大量的数据来切分 Split,甚至会导致 Driver 端 OOM,因此这类表的读取建议采用 BI 策略。对于一些较小,尤其是有数据倾斜的表(即大量 Stripe 存储于少数文件中),建议使用 ETL 策略。 一些其他 ORC 相关的配置如下: /* ORC 谓词下推,默认是关闭 */ set spark.sql.orc.filterPushdown=true; /* 开启后,在 Split 划分时会使用 Footer 信息 */ set spark.sql.orc.splits.include.file.footer=true; /* 设置每个 Stripe 可以缓存的大小 */ set spark.sql.orc.cache.stripe.details.size=10000; /* 当为 true 时,Spark SQL 的谓语将被下推到 Hive Metastore 中,更早的消除不匹配的分区。 */ set spark.sql.hive.metastorePartitionPruning=true; /* 读 ORC 表时,设置小文件合并的阈值,低于该值的 Split 会合并在一个 Task 中执行 */ set spark.hadoop.mapreduce.input.fileinputformat.split.minsize=67108864; /* 读 ORC 表时,设置一个 Split 的最大阈值,大于该值的 Split 会切分成多个 Split。 */ set spark.hadoop.mapreduce.input.fileinputformat.split.maxsize=268435456; /* 文件提交到HDFS上的算法:1. version=1 是按照文件提交。2. version=2 是批量按照目录进行提交,可以极大节约文件提交到 HDFS 的时间,减轻 NameNode 压力。 */ set spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version=2; 自适应执行 /* 开启动态执行 */ set spark.sql.adaptive.enabled=true; 当自适应执行开启后,调整 spark.sql.adaptive.shuffle.targetPostShuffleInputSize,当 Mapper 端两个 Partition 的数据合并后小于该值时,Spark 会将两个 Partition 合并到一个 Reducer 进行处理。 set spark.sql.adaptive.shuffle.targetPostShuffleInputSize=67108864; 当自适应执行开启后,有时会导致过多分区被合并,为了防止分区过少影响性能,可以设置如下参数: set spark.sql.adaptive.minNumPostShufflePartitions=10; 一些其他自适应执行相关的配置如下: /* 开启动态调整 Join */ set spark.sql.adaptive.join.enabled=true; /* 设置 SortMergeJoin 转 BroadcastJoin 的阈值,如果不设置该参数,该阈值和 spark.sql.autoBroadcastJoinThreshold 值相等。 */ set spark.sql.adaptiveBroadcastJoinThreshold=33554432; /* 是否允许为了优化 Join 而增加 Shuffle,默认是 false */ set spark.sql.adaptive.allowAddititionalShuffle=false; /* 开启自动处理 Join 时的数据倾斜 */ set spark.sql.adaptive.skewedJoin.enabled=true; /* 控制处理一个倾斜 Partition 的 Task 个数上限,默认值是 5 */ set spark.sql.adaptive.skewedPartitionMaxSplits=100; /* 设置一个 Partition 被视为倾斜 Partition 的行数下限,行数低于该值的 Partition 不会被当做倾斜 Partition 处理。 */ set spark.sql.adaptive.skewedPartitionRowCountThreshold=10000000; /* 设置一个 Partition 被视为倾斜 Partition 的大小下限,大小小于该值的 Partition 不会被当做倾斜 Partition 处理。 */ set spark.sql.adaptive.skewedPartitionSizeThreshold=536870912; /* 设置倾斜因子,当一个 Partition 满足以下两个条件之一,就会被视为倾斜 Partition:1. 大小大于 spark.sql.adaptive.skewedPartitionSizeThreshold 的同时大于各 Partition 大小中位数与该因子的乘积。2. 行数大于 spark.sql.adaptive.skewedRowCountThreshold 的同时大于各 Partition 行数中位数与该因子的乘积。*/ set spark.sql.adaptive.skewedPartitionFactor=10; 推测执行 /* Spark 推测执行开关,默认是 true */ set spark.speculation=true; /* 开启推测执行后,每隔该值时间会检测是否有需要推测执行的 Task */ set spark.speculation.interval=1000ms; /* 当成功 Task 占总 Task 的比例超过 spark.speculation.quantile,统计成功 Task 运行时间中位数乘以 spark.speculation.multiplier 得到推测执行阈值,当在运行的任务超过这个阈值就会启动推测执行。当资源充足时,可以适当减小这两个值。 */ set spark.speculation.quantile=0.99; set spark.speculation.multiplier=3; https://zh.wikipedia.org/wiki/SQL ↩︎ https://www.iso.org/committee/45342.html ↩︎ https://spark.apache.org/docs/latest/rdd-programming-guide.html ↩︎

2021/5/23
articleCard.readMore

SQL 样式指南 (SQL Style Guide)

代码样式指南主要用于规范项目中代码的一致性,使得代码简单、可读和易于维护,从一定程度上也影响代码的质量。一句话概括如何评价代码的质量: 衡量代码质量的唯一有效标准:WTF/min – Robert C. Martin Google 针对大多数编程语言(例如:C/C++,Java,JavaScript,Python,R 等)都整理了相关的代码风格,但对于 SQL 这种用于数据库查询特殊目的的编程语言并没有整理对应的风格。同其他编程语言代码风格一样,没有哪种风格是最好的,只要在项目中采用统一合理的风格即可。 本文参考的 SQL 样式指南有如下几种: https://www.sqlstyle.guide/zh/ https://about.gitlab.com/handbook/business-technology/data-team/platform/sql-style-guide/ https://docs.telemetry.mozilla.org/concepts/sql_style.html https://github.com/mattm/sql-style-guide 本文给出的 SQL 样式指南基于上述几种进行整理和修改。 一般原则 使用一致的、描述性名称。 使用空格(2 个或 4 个,项目中保持一致),避免使用 TAB 缩进。 在 SQL 中加入必要的注释,块注释使用 /* */,行注释使用 --,并在末尾换行。 使用单引号 ' 作为被引号包裹的标识符。 运算符前后添加空格,逗号 , 后添加空格,避免行尾有空格。 每行代码不超过 80 个字符。 命名惯例 避免名称和保留字一样。 关键词、函数名称采用大写,字段名、表名采用小蛇式(lower snake case)命名。 名称要以字母开头,不能以下划线结尾,名称中仅可以使用字母、数字和下划线。 不要在名字中出现连续下划线 __,这样很难辨认。 尽量避免使用缩写词。使用时一定确定这个缩写简明易懂。 字段名总是使用单数。 对齐和换行 避免川流式对齐代码。 /* Good */ SELECT id FROM table_name WHERE column = "test" ; /* Bad */ SELECT id FROM talbe_name WHERE column = "test" ; 多个元素组合无法呈现在一行中时,应将第一个元素另起一行。 /* Good */ SELECT CASE postcode WHEN 'BN1' THEN 'Brighton' WHEN 'EH1' THEN 'Edinburgh' END AS city FROM table_name ; /* Bad */ SELECT CASE postcode WHEN 'BN1' THEN 'Brighton' WHEN 'EH1' THEN 'Edinburgh' END AS city FROM table_name ; 由括号构成的多行,结尾括号应单独一行。 /* Good */ SELECT id FROM table_name WHERE postcode IN ( 'looooooooooooooooooooooooong_BN1', 'loooooooooooooooooooooooooog_EH1' ) /* Bad */ SELECT id FROM table_name WHERE postcode IN ('looooooooong_BN1', 'looooooooong_EH1') 多行采用右侧逗号和左侧关键字连接。 /* Good */ SELECT id, name FROM talbe_name WHERE id > 1 AND name LIKE "%Tom%" ; /* Bad */ SELECT id , name FROM table_name WHERE id > 1 AND name LIKE "%Tom%" ; 根关键词建议单独一行,多个参数单独一行。 /* Good */ SELECT id, name FROM table_name WHERE id > 1 AND name LIKE "%Tom%" LIMIT 10 ; /* Acceptable */ SELECT id, name FROM table_name WHERE id > 1 AND name LIKE "%Tom%" LIMIT 10 ; /* Bad */ SELECT id, name FROM table_name WHERE id > 1 AND name LIKE "%Tom%" LIMIT 10 ; 明确指定 使用 AS 明确指定别名,而非隐式。 /* Good */ SELECT table_name_1.id AS user_id, table_name_2.name AS user_name FROM looooooooong_table_name_1 AS table_name_1 LEFT JOIN looooooooong_table_name_2 AS table_name_2 ON table_name_1.id = table_name_2.id ; /* Bad */ SELECT table_name_1.id user_id, table_name_2.name user_name FROM looooooooong_table_name_1 table_name_1 LEFT JOIN looooooooong_table_name_2 table_name_2 ON table_name_1.id = table_name_2.id ; 避免使用隐式关联。 /* Good */ SELECT table_name_1.id, table_name_2.name FROM table_name_1 INNER JOIN table_name_2 ON table_name_1.id = table_name_2.id ; /* Bad */ SELECT table_name_1.id, table_name_2.name FROM table_name_1, table_name_2 ON table_name_1.id = table_name_2.id ; 明确关联类型。 /* Good */ SELECT table_name_1.id, table_name_2.name FROM table_name_1 INNER JOIN table_name_2 ON table_name_1.id = table_name_2.id ; /* Bad */ SELECT table_name_1.id, table_name_2.name FROM table_name_1 JOIN table_name_2 ON table_name_1.id = table_name_2.id ; 明确指定分组列。 /* Good */ SELECT submission_date, normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease, COUNT(*) AS count FROM telemetry.clients_daily WHERE submission_date > '2019-07-01' GROUP BY submission_date, is_prerelease ; /* Bad */ SELECT submission_date, normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease, COUNT(*) AS count FROM telemetry.clients_daily WHERE submission_date > '2019-07-01' GROUP BY 1, 2 ; 子查询 尽量使用 Common Table Expressions (CTEs) 而非子查询。 /* Good */ WITH sample AS ( SELECT client_id, submission_date FROM main_summary WHERE sample_id = '42' ) SELECT * FROM sample LIMIT 10 /* Bad */ SELECT * FROM ( SELECT client_id, submission_date FROM main_summary WHERE sample_id = '42' ) LIMIT 10 尽量在 CTEs 中处理查询而非主语句中。 /* Good */ WITH backings_per_category AS ( SELECT ... ), backers AS ( SELECT backings_per_category.backer_id, COUNT(backings_per_category.id) AS projects_backed_per_category INNER JOIN ksr.users AS users ON users.id = backings_per_category.backer_id GROUP BY backings_per_category.backer_id ), backers_and_creators AS ( ... ) SELECT * FROM backers_and_creators; /* Bad */ WITH backings_per_category AS ( SELECT ... ), backers AS ( SELECT backer_id, COUNT(backings_per_category.id) AS projects_backed_per_category ), backers_and_creators AS ( ... ) SELECT * FROM backers_and_creators INNER JOIN backers ON backers_and_creators ON backers.backer_id = backers_and_creators.backer_id 其他 尽量使用 != 而不是 <> 表示不等于。 尽量使用 BETWEEN 而不是多个 AND 语句。 尽量使用 IN() 而不是多个 OR 语句。 尽量避免使用 SELECT *。 尽量避免使用无意义的别名,例如:a, b, c。

2021/5/4
articleCard.readMore

进程,线程和协程 (Process, Thread and Coroutine)

理论篇请参见:进程,线程和协程 (Process, Thread and Coroutine) - 理论篇 本文将介绍进程,线程和协程在 Python 中的实现,代码详见这里,部分参考自「Python 并发编程」 1:。 进程和线程 在 Python 中可以使用 multiprocessing.Process 和 threading.Thread 来实现进程和线程。我们采用CPU 密集型、磁盘 IO 密集型、网络 IO 密集型和模拟 IO 密集型任务类型来测试单线程,多线程和多进程之间的性能差异。 import requests # CPU 密集型 def cpu_bound_task(x=1, y=1): c = 0 while c < 1500000: c += 1 x += x y += y # 磁盘 IO 密集型 def disk_io_bound_task(): with open('tmp.log', 'w') as f: for idx in range(5000000): f.write('{}\n'.format(idx)) # 网络 IO 密集型 def web_io_bound_task(): try: requests.get('https://www.baidu.com') except Exception as e: pass # 模拟 IO 密集型 def simulation_io_bound_task(): time.sleep(2) 为了方便统计运行时间,定义如下一个运行时间装饰器: import time def timer(task_mode): def wrapper(func): def decorator(*args, **kwargs): task_type = kwargs.setdefault('task_type', None) start_time = time.time() func(*args, **kwargs) end_time = time.time() print('耗时({} - {}): {}'.format( task_mode, task_type, end_time - start_time)) return decorator return wrapper 单线程,多线程和多进程的测试代码如下: from threading import Thread from multiprocessing import Process @timer('单线程') def single_thread(func, task_type='', n=10): for idx in range(n): func() @timer('多线程') def multi_threads(func, task_type='', n=10): threads = {} for idx in range(n): t = Thread(target=func) threads[idx] = t t.start() for thread in threads.values(): thread.join() @timer('多进程') def multi_processes(func, task_type='', n=10): processes = {} for idx in range(n): p = Process(target=func) processes[idx] = p p.start() for process in processes.values(): process.join() 运行测试 # 单线程 single_thread(cpu_bound_task, task_type='CPU 密集型任务') single_thread(disk_io_bound_task, task_type='磁盘 IO 密集型任务') single_thread(web_io_bound_task, task_type='网络 IO 密集型任务') single_thread(simulation_io_bound_task, task_type='模拟 IO 密集型任务') # 多线程 multi_threads(cpu_bound_task, task_type='CPU 密集型任务') multi_threads(disk_io_bound_task, task_type='磁盘 IO 密集型任务') multi_threads(web_io_bound_task, task_type='网络 IO 密集型任务') multi_threads(simulation_io_bound_task, task_type='模拟 IO 密集型任务' # 多进程 multi_processes(cpu_bound_task, task_type='CPU 密集型任务') multi_processes(disk_io_bound_task, task_type='磁盘 IO 密集型任务') multi_processes(web_io_bound_task, task_type='网络 IO 密集型任务') multi_processes(simulation_io_bound_task, task_type='模拟 IO 密集型任务') 可以得到类似如下的结果: 单线程 多线程 多进程 CPU 密集型 83.42 93.82 9.08 磁盘 IO 密集型 15.64 13.27 1.28 网络 IO 密集型 1.13 0.18 0.13 模拟 IO 密集型 20.02 2.02 2.01 从测试结果来看,不难得出如下结论: 多线程和多进程相比单线程速度整体上有很大提升。 对于 CPU 密集型任务,由于 GIL 加锁和释放问题,多线程相比单线程更慢。 多线程更适合在 IO 密集场景下使用,例如:爬虫等。 多进程更适合在 CPU 密集场景下使用,例如:大数据处理,机器学习等。 创建线程有两种方式: 利用函数创建线程 Python 中的 threading.Thread() 接受两个参数:线程函数,用于指定线程执行的函数;线程函数参数,以元组的形式传入执行函数所需的参数。 import time from threading import Thread # 自定义函数 def func(name='Python'): for idx in range(2): print('Hello, {}'.format(name)) time.sleep(1) # 创建线程 thread_1 = Thread(target=func) thread_2 = Thread(target=func, args=('Leo', )) # 启动线程 thread_1.start() thread_2.start() 可以得到如下输出: Hello, Python Hello, Leo Hello, Python Hello, Leo 利用类创建线程 利用类创建线程需要自定义一个类并继承 threading.Thread 这个父类,同时重写 run 方法。最后通过实例化该类,并运行 start() 方法执行该线程。 # 自定义类 class MyThread(Thread): def __init__(self, name='Python'): super(MyThread, self).__init__() self.name = name def run(self): for idx in range(2): print('Hello, {}'.format(self.name)) time.sleep(1) # 创建线程 thread_1 = MyThread() thread_2 = MyThread('Leo') # 启动线程 thread_1.start() thread_2.start() 可以得到同上面一样的输出: Hello, Python Hello, Leo Hello, Python Hello, Leo 线程的一些常用方法和属性如下所示: # 创建线程 t = Thread(target=func) # 启动线程 t.start() # 阻塞线程 t.join() # 判断线程是否处于执行状态 # True: 执行中,False: 其他 t.is_alive() # 这是线程是否随主线程退出而退出 # 默认为 False t.daemon = True # 设置线程名称 t.name = 'My Thread' 锁 在一段代码中加锁表示同一时间有且仅有一个线程可以执行这段代码。在 Python 中锁分为两种:互斥锁和可重入锁。利用 threading.Lock() 可以获取全局唯一的锁对象,使用 acquire() 和 release() 方法可以获取和释放锁,注意两个需成对出现,否则可能造成死锁。 互斥锁 例如定义两个函数,并在两个线程中执行,这两个函数共用一个变量 C: import time import random from threading import Thread # 共用变量 C = 0 def job1(n=10): global C for idx in range(n): C += 1 print('Job1: {}'.format(C)) def job2(n=10): global C for idx in range(n): C += 10 print('Job2: {}'.format(C)) t1 = Thread(target=job1) t2 = Thread(target=job2) t1.start() t2.start() 运行结果如下: Job1: 1 Job2: 11 Job2: 21 Job1: 22 Job1: 23 Job2: 33 Job2: 43 Job1: 44 Job2: 54 Job1: 55 Job2: 65 Job1: 66 Job2: 76 Job2: 86 Job1: 87 Job1: 88 Job2: 98 Job1: 99 Job2: 109 Job1: 110 两个线程共用一个全局变量,两个线程根据自己执行的快慢对变量 C 进行修改。在增加锁后: import time import random from threading import Lock # 全局唯一锁 LOCK = Lock() # 共用变量 C = 0 def job1_with_lock(n=10): global C, LOCK LOCK.acquire() for idx in range(n): C += 1 print('Job1: {}'.format(C)) time.sleep(random.random()) LOCK.release() def job2_with_lock(n=10): global C, LOCK LOCK.acquire() for idx in range(n): C += 10 print('Job2: {}'.format(C)) time.sleep(random.random()) LOCK.release() t1 = Thread(target=job1_with_lock) t2 = Thread(target=job2_with_lock) t1.start() t2.start() 运行结果如下: Job1: 1 Job1: 2 Job1: 3 Job1: 4 Job1: 5 Job1: 6 Job1: 7 Job1: 8 Job1: 9 Job1: 10 Job2: 20 Job2: 30 Job2: 40 Job2: 50 Job2: 60 Job2: 70 Job2: 80 Job2: 90 Job2: 100 Job2: 110 此时,由于 job1_with_lock 先拿到了锁,所以当执行时 job2_with_lock 无法获取到锁,就无法对 C 进行修改。只有当 job1_with_lock 执行完毕释放锁后,job2_with_lock 才能执行对 C 的修改操作。为了避免忘记释放锁,可以使用 with 上下文管理器来加锁。 可重入锁 在同一个线程中,我们可能会多次请求同一个资源,这称为嵌套锁。如果使用常规的方式: from threading import Lock def lock_with_lock(n=10): c = 0 lock = Lock() with lock: for idx in range(n): c += 1 with lock: print(c) t = Thread(target=lock_with_lock) t.start() 则无法正常运行,因为第二次获取锁时,锁已经被同一线程获取,从而无法运行后续代码。由于后续代码无法运行则无法释放锁,从而上述的嵌套锁会造成死锁。 为了解决这个问题,threading 模块提供了可重入锁 RLock: from threading import RLock def rlock_with_lock(n=10): c = 0 lock = RLock() with lock: for idx in range(n): c += 1 with lock: print(c) t = Thread(target=rlock_with_lock) t.start() 运行结果如下: 1 2 3 4 5 6 7 8 9 10 全局解释器锁 全局解释器锁(Global Interpreter Lock,GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。 任何 Python 线程执行前,必须先获得 GIL 锁,然后,每执行 100 条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在 Python 中只能交替执行,即使 100 个线程跑在 100 核 CPU 上,也只能用到 1 个核。 通信 Python 中实现线程中通信有如下 3 中方法: Event 事件 threading.Event 可以创建一个事件变量,多个线程等待这个事件的发生,在事件发生后,所有线程继续运行。threading.Event 包含如下三个函数: event = threading.Event() # 重置 event,使得所有该 event 事件都处于待命状态 event.clear() # 等待接收 event 的指令,决定是否阻塞程序执行 event.wait() # 发送 event 指令,使所有设置该 event 事件的线程执行 event.set() 例如: import time from threading import Thread, Event class EventThread(Thread): def __init__(self, name, event): super(EventThread, self).__init__() self.name = name self.event = event def run(self): print('线程 {} 启动于 {}'.format(self.name, time.ctime(time.time()))) self.event.wait() print('线程 {} 结束于 {}'.format(self.name, time.ctime(time.time()))) threads = {} event = Event() for tid in range(3): threads[tid] = EventThread(str(tid), event) event.clear() for thread in threads.values(): thread.start() print('等待 3 秒钟 ...') time.sleep(3) print('唤醒所有线程 ...') event.set() 运行结果如下: 线程 0 启动于 Thu Apr 1 23:12:32 2021 线程 1 启动于 Thu Apr 1 23:12:32 2021 线程 2 启动于 Thu Apr 1 23:12:32 2021 等待 3 秒钟 ... 唤醒所有线程 ... 线程 0 结束于 Thu Apr 1 23:12:35 2021 线程 1 结束于 Thu Apr 1 23:12:35 2021 线程 2 结束于 Thu Apr 1 23:12:35 2021 可见线程启动后并未执行完成,而是卡在了 event.wait() 处,直到通过 event.set() 发送指令后,所有线程才继续向下执行。 Condition threading.Condition 与 threading.Event 类似,包含如下 4 个函数: cond = threading.Condition() # 类似 lock.acquire() cond.acquire() # 类似 lock.release() cond.release() # 等待指定触发,同时会释放对锁的获取,直到被 notify 才重新占有琐。 cond.wait() # 发送指定,触发执行 cond.notify() 以一个捉迷藏的游戏为例: import time from threading import Thread, Condition class Seeker(Thread): def __init__(self, condition, name): super(Seeker, self).__init__() self.condition = condition self.name = name def run(self): time.sleep(1) # 确保先运行 Hider 中的方法 self.condition.acquire() print('{}: 我把眼睛蒙好了'.format(self.name)) self.condition.notify() self.condition.wait() print('{}: 我找到你了'.format(self.name)) self.condition.notify() self.condition.release() print('{}: 我赢了'.format(self.name)) class Hider(Thread): def __init__(self, condition, name): super(Hider, self).__init__() self.condition = condition self.name = name def run(self): self.condition.acquire() self.condition.wait() print('{}: 我藏好了'.format(self.name)) self.condition.notify() self.condition.wait() self.condition.release() print('{}: 被你找到了'.format(self.name)) condition = Condition() seeker = Seeker(condition, 'Seeker') hider = Hider(condition, 'Hider') seeker.start() hider.start() 运行结果如下: Seeker: 我把眼睛蒙好了 Hider: 我藏好了 Seeker: 我找到你了 Seeker: 我赢了 Hider: 被你找到了 可见通过 cond.wait() 和 cond.notify() 进行阻塞和通知可以实现双方动作交替进行。 Queue 队列 从一个线程向另一个线程发送数据最安全的方式是使用 queue 库中的队列。创建一个被多个线程共享的队列对象,通过 put() 和 get() 方法向队列发送和获取元素。队列的常用方法如下: from queue import Queue # maxsize=0 表示不限大小 # maxsize>0 且消息数达到限制时,put() 方法会阻塞 q = Queue(maxsize=0) # 默认阻塞程序,等待队列消息,可设置超时时间 q.get(block=True, timeout=None) # 发送消息,默认会阻塞程序至队列中有空闲位置放入数据 q.put(item, block=True, timeout=None) # 等待所有的消息都被消费完 q.join() # 通知队列任务处理已经完成,当所有任务都处理完成时,join() 阻塞将会解除 q.task_done() # 查询当前队列的消息个数 q.qsize() # 队列消息是否都被消费完,返回 True/False q.empty() # 检测队列里消息是否已满 q.full() 以老师点名为例: import time from queue import Queue from threading import Thread class Student(object): def __init__(self, name): super(Student, self).__init__() self.name = name def speak(self): print('{}: 到'.format(self.name)) class Teacher(object): def __init__(self, queue): super(Teacher, self).__init__() self.queue = queue def call(self, student_name): if student_name == 'exit': print('老师: 点名结束,开始上课') else: print('老师: {}'.format(student_name)) self.queue.put(student_name) class CallManager(Thread): def __init__(self, queue): super(CallManager, self).__init__() self.students = {} self.queue = queue def put(self, student): self.students.setdefault(student.name, student) def run(self): while True: student_name = self.queue.get() if student_name == 'exit': break elif student_name in self.students: self.students[student_name].speak() else: print('学生: 老师,没有 {} 这个人'.format(student_name)) queue = Queue() teacher = Teacher(queue=queue) s1 = Student(name='张三') s2 = Student(name='李四') cm = CallManager(queue) cm.put(s1) cm.put(s2) cm.start() print('开始点名') teacher.call('张三') time.sleep(1) teacher.call('李四') time.sleep(1) teacher.call('王五') time.sleep(1) teacher.call('exit') 运行结果如下: 开始点名 老师: 张三 张三: 到 老师: 李四 李四: 到 老师: 王五 学生: 老师,没有 王五 这个人 老师: 点名结束,开始上课 除了先进先出队列 queue.Queue 外,还有后进先出队列 queue.LifoQueue 和优先级队列 queue.PriorityQueue。 进程池和线程池 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。 池的概念主要目的是为了重用:让线程或进程在生命周期内可以多次使用。它减少了创建创建线程和进程的开销,以空间换时间来提高了程序性能。重用不是必须的规则,但它是程序员在应用中使用池的主要原因。 Python 中利用 concurrent.futures 库中的 ThreadPoolExecutor 和 ProcessPoolExecutor 创建线程池和进程池。示例如下: import time import threading from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed def print_func(n=3): for idx in range(n): print('运行 {}-{}'.format(threading.get_ident(), idx)) time.sleep(1) def return_func(n=3): res = [] for idx in range(n): res.append('{}-{}'.format(threading.get_ident(), idx)) time.sleep(1) return res def test_thread_pool_print(n=3, m=12): with ThreadPoolExecutor(max_workers=n) as executor: for _ in range(m): executor.submit(print_func) def test_process_pool_print(n=3, m=12): with ProcessPoolExecutor(max_workers=n) as executor: for _ in range(m): executor.submit(print_func) def test_thread_pool_return(n=3, m=12): with ThreadPoolExecutor(max_workers=n) as executor: futures = [executor.submit(return_func) for _ in range(m)] for future in as_completed(futures): print(future.result()) def test_process_pool_return(n=3, m=12): with ProcessPoolExecutor(max_workers=n) as executor: futures = [executor.submit(return_func) for _ in range(m)] for future in as_completed(futures): print(future.result()) line_sep = '-' * 60 print(line_sep) print('测试线程池') print(line_sep) test_thread_pool_print() print(line_sep) print(line_sep) print('测试进程池') print(line_sep) test_process_pool_print() print(line_sep) print(line_sep) print('测试线程池') print(line_sep) test_thread_pool_return() print(line_sep) print(line_sep) print('测试进程池') print(line_sep) test_process_pool_return() print(line_sep) 运行结果如下: ------------------------------------------------------------ 测试线程池 ------------------------------------------------------------ 运行 123145462505472-0 运行 123145479294976-0 运行 123145496084480-0 运行 123145496084480-1 运行 123145462505472-1 运行 123145479294976-1 运行 123145496084480-2 运行 123145462505472-2 运行 123145479294976-2 运行 123145462505472-0 运行 123145479294976-0 运行 123145496084480-0 运行 123145479294976-1 运行 123145462505472-1 运行 123145496084480-1 运行 123145479294976-2 运行 123145462505472-2 运行 123145496084480-2 ------------------------------------------------------------ ------------------------------------------------------------ 测试进程池 ------------------------------------------------------------ 运行 4545199616-0 运行 4545199616-1 运行 4545199616-2 运行 4545199616-0 运行 4545199616-1 运行 4545199616-2 运行 4663131648-0 运行 4663131648-1 运行 4663131648-2 运行 4663131648-0 运行 4663131648-1 运行 4663131648-2 运行 4633173504-0 运行 4633173504-1 运行 4633173504-2 运行 4633173504-0 运行 4633173504-1 运行 4633173504-2 ------------------------------------------------------------ ------------------------------------------------------------ 测试线程池 ------------------------------------------------------------ ['123145496084480-0', '123145496084480-1', '123145496084480-2'] ['123145479294976-0', '123145479294976-1', '123145479294976-2'] ['123145462505472-0', '123145462505472-1', '123145462505472-2'] ['123145479294976-0', '123145479294976-1', '123145479294976-2'] ['123145496084480-0', '123145496084480-1', '123145496084480-2'] ['123145462505472-0', '123145462505472-1', '123145462505472-2'] ------------------------------------------------------------ ------------------------------------------------------------ 测试进程池 ------------------------------------------------------------ ['4791307776-0', '4791307776-1', '4791307776-2'] ['4588228096-0', '4588228096-1', '4588228096-2'] ['4654599680-0', '4654599680-1', '4654599680-2'] ['4791307776-0', '4791307776-1', '4791307776-2'] ['4588228096-0', '4588228096-1', '4588228096-2'] ['4654599680-0', '4654599680-1', '4654599680-2'] ------------------------------------------------------------ 其中,submit() 方法用于提交要执行的任务到线程池或进程池中,并返回该任务的 Future 对象。Future 对象的 done() 方法用于判断任务是否执行完毕,通过 result(timeout=None) 方法获取返回结果。利用 concurrent.futures.as_completed() 方法可以返回一个包含指定 Future 实例的迭代器,这些实例在完成时生成 Future 对象。 生成器和迭代器 容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用 in 或 not in 判断元素是否包含在容器中。常见的容器对象有: list, deque, ... set, frozensets, ... dict, defaultdict, OrderedDict, Counter, ... tuple, namedtuple, ... str 可迭代对象 很多容器都是可迭代对象,此外还有更多的对象同样也可以是可迭代对象,比如处于打开状态的 file 和 socket 等。凡是可以返回一个迭代器的对象都可称之为可迭代对象,例如: from collections import deque from collections.abc import Iterable print(isinstance('Leo Van', Iterable)) print(isinstance([1, 2, 3], Iterable)) print(isinstance({'k1': 'v1', 'k2': 'v2'}, Iterable)) print(isinstance(deque('abc'), Iterable)) 运行结果如下: True True True True 迭代器 迭代器是一个带有状态的对象,通过 next() 方法可以返回容器中的下一个值。任何实现 __iter__() 和 __next__() 方法的对象都是迭代器。其中,__iter__() 方法返回迭代器本身,__next__() 方法返回容器中的下一个值,如果容器中没有更多元素了,则抛出 StopIteration 异常。例如: class MyList(object): def __init__(self, end): super(MyList, self).__init__() self.end = end def __iter__(self): return MyListIterator(self.end) def __repr__(self): return '[{}]'.format(', '.join([str(ele) for ele in self])) class MyListIterator(object): def __init__(self, end): super(MyListIterator, self).__init__() self.data = end self.start = 0 def __iter__(self): return self def __next__(self): while self.start < self.data: self.start += 1 return self.start - 1 raise StopIteration my_list = MyList(3) print('MyList: {}'.format(my_list)) print(isinstance(my_list, Iterable)) print(isinstance(my_list, Iterator)) for ele in my_list: print(ele) my_list_iterator = MyListIterator(3) print('MyListIterator: {}'.format(my_list_iterator)) print(isinstance(my_list_iterator, Iterable)) print(isinstance(my_list_iterator, Iterator)) my_iterator = iter(my_list) print('MyIterator: {}'.format(my_iterator)) print(isinstance(my_iterator, Iterable)) print(isinstance(my_iterator, Iterator)) while True: try: print(next(my_iterator)) except StopIteration as e: return 运行结果如下: MyList: [0, 1, 2] True False 0 1 2 MyListIterator: <__main__.MyListIterator object at 0x7fc9602b2100> True True 0 1 2 MyIterator: <__main__.MyListIterator object at 0x7fc9602b2fa0> True True 0 1 2 Stop 生成器 生成器非常类似于返回数组的函数,都是具有参数、可被调用、产生一系列的值。但是生成器并不是构造出数组包含所有的值并一次性返回,而是每次产生一个值,因此生成器看起来像函数,但行为像迭代器。 Python 中创建生成器有两种方法:使用类似列表方式或 yield 关键字: from collections.abc import Generator from inspect import getgeneratorstate a_list = [x for x in range(10)] print(a_list) print(isinstance(a_list, Generator)) a_generator = (x for x in range(10)) print(a_generator) print(isinstance(a_generator, Generator)) def my_yield(n): now = 0 while now < n: yield now now += 1 raise StopIteration gen = my_yield(4) print(gen) print(isinstance(gen, Generator)) 运行结果如下: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] False <generator object <genexpr> at 0x7fdf84a8a430> True <generator object my_yield at 0x7fdf86a03f20> True 由于生成器并不是一次生成所有元素,而是每次执行后返回一个值,通过 next() 和 generator.send(None) 两个方法可以激活生成器,例如: def my_yield(n): now = 0 while now < n: yield now now += 1 raise StopIteration gen = my_yield(4) print(gen.send(None)) print(next(gen)) print(gen.send(None)) print(next(gen)) 运行结果如下: 0 1 2 3 生成器在其生命周期中共有 4 种状态: GEN_CREATED:已创建 GEN_RUNNING:正在执行(只在多线程应用中能看到该状态) GEN_SUSPENDED:暂停中 GEN_CLOSED:已关闭 例如: from collections.abc import Generator from inspect import getgeneratorstate def my_yield(n): now = 0 while now < n: yield now now += 1 raise StopIteration gen = my_yield(4) print(gen) print(getgeneratorstate(gen)) print(gen.send(None)) print(next(gen)) print(getgeneratorstate(gen)) print(gen.send(None)) print(next(gen)) print(getgeneratorstate(gen)) gen.close() print(getgeneratorstate(gen)) 运行结果如下: GEN_CREATED 0 1 GEN_SUSPENDED 2 3 GEN_SUSPENDED GEN_CLOSED 生成器在不满足生成元素的条件时,会抛出 StopIteration 异常,通过类似列表形式构建的生成器会自动实现该异常,自定的生成器则需要手动实现该异常。 协程 yield 协程通过 yield 暂停生成器,可以将程序的执行流程交给其他子程序,从而实现不同子程序之间的交替执行。例如: def jump_range(n): idx = 0 while idx < n: jump = yield idx print('[idx: {}, jump: {}]'.format(idx, jump)) if jump is None: jump = 1 idx += jump itr = jump_range(6) print(next(itr)) print(itr.send(2)) print(next(itr)) print(itr.send(-1)) print(next(itr)) print(next(itr)) 运行结果如下: 0 [idx: 0, jump: 2] 2 [idx: 2, jump: None] 3 [idx: 3, jump: -1] 2 [idx: 2, jump: None] 3 [idx: 3, jump: None] 4 yield idx 将 idx 返回给外部调用程序,jump = yield 可以接受外部程序通过 send() 发送的信息,并将其赋值给 jump。 yield from 是 Python 3.3 之后出现的新语法,后面是可迭代对象,可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。yield 和 yield from 的对比如下: a_str = 'Leo' a_list = [1, 2, 3] a_dict = {'name': 'Leo', 'gender': 'Male'} a_gen = (idx for idx in range(4, 8)) def gen(*args, **kwargs): for item in args: for ele in item: yield ele new_gen = gen(a_str, a_list, a_dict, a_gen) print(list(new_gen)) a_gen = (idx for idx in range(4, 8)) def gen_from(*args, **kwargs): for item in args: yield from item new_gen = gen_from(a_str, a_list, a_dict, a_gen) print(list(new_gen)) 运行结果如下: ['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7] ['L', 'e', 'o', 1, 2, 3, 'name', 'gender', 4, 5, 6, 7] 在实现生成器的嵌套时,使用 yield from 可以比使用 yield 避免各种意想不到的异常。使用 yield from 时,需要关注如下几个概念: 调用方:调用委托生成器的代码 委托生成器:包含 yield from 表达式的生成器函数 子生成器:yield from 后面的生成器函数 如下是一个计算平均数的例子: # 子生成器 def average_gen(): total = 0 count = 0 average = 0 while True: num = yield average if num is None: break count += 1 total += num average = total / count return total, count, average # 委托生成器 def proxy_gen(): while True: total, count, average = yield from average_gen() print('计算完毕,共输入 {} 个数值,总和 {},平均值 {}'.format( count, total, average)) # 调用方 calc_average = proxy_gen() next(calc_average) print(calc_average.send(10)) print(calc_average.send(20)) print(calc_average.send(30)) calc_average.send(None) 运行结果如下: 10.0 15.0 20.0 计算完毕,共输入 3 个数值,总和 60,平均值 20.0 委托生成器的作用是在调用方和子生成器之间建立一个双向通道,调用方通过 send() 将消息发送给子生成器,子生成器 yield 的值则返回给调用方。yield from 背后为整个过程做了很多操作,例如:捕获 StopIteration 异常等。 asyncio asyncio 是 Python 3.4 引入的标准库,直接内置了对异步 IO 的支持。只要在一个函数前面加上 async 关键字就可以将一个函数变为一个协程。例如: from collections.abc import Coroutine async def async_func(name): print('Hello, ', name) coroutine = async_func('World') print(isinstance(coroutine, Coroutine)) 运行结果如下: True 利用 asyncio.coroutine 装饰器可以将一个生成器当作协程使用,但其本质仍旧是一个生成器。例如: import asyncio from collections.abc import Generator, Coroutine @asyncio.coroutine def coroutine_func(name): print('Hello,', name) yield from asyncio.sleep(1) print('Bye,', name) coroutine = coroutine_func('World') print(isinstance(coroutine, Generator)) print(isinstance(coroutine, Coroutine)) 运行结果如下: True False asyncio 中包含如下几个重要概念: event_loop 事件循环:程序开启一个无限的循环,协程将注册到事件循环上,当满足事件发生时,调用相应的协程函数。 coroutine 协程:一个使用 async 定义的协程函数,它的调用不会立即执行,而是会返回一个协程对象。协程对象需要注册到事件循环上,由事件循环控制调用。 future 对象:代表将来执行或没有执行的对象。它和 task 对象没有本质上的区别。 task 对象:一个协程对象是一个原生可以挂起的函数,任务是对协程的进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将 coroutine 封装成为一个 Future 对象。 async / await 关键字:async 定义一个协程,await 用于挂起阻塞的异步调用接口。 协程完整的工作流程如下: import asyncio async def hello(name): print('Hello,', name) # 定义协程 coroutine = hello('World') # 定义事件循环 loop = asyncio.get_event_loop() # 创建任务 task = loop.create_task(coroutine) # 将任务交由时间循环并执行 loop.run_until_complete(task) 运行结果如下: Hello, World await 用于挂起阻塞的异步调用接口,其作用在一定程度上类似于 yield。yield from 后面可接可迭代对象,也可接 future 对象或协程对象;await 后面必须接 future 对象或协程对象。 import asyncio from asyncio.futures import Future async def hello(name): await asyncio.sleep(2) print('Hello, ', name) coroutine = hello("World") # 将协程转为 task 对象 task = asyncio.ensure_future(coroutine) print(isinstance(task, Future)) 运行结果如下: True 异步 IO 的实现原理就是在 IO 高的地方挂起,等 IO 结束后再继续执行。绝大部分情况下,后续代码的执行是需要依赖 IO 的返回值的,这就需要使用回调。 回调的实现有两种,一种是在同步编程中直接获取返回结果: import asyncio import time async def _sleep(x): time.sleep(x) return '暂停了 {} 秒'.format(x) coroutine = _sleep(2) loop = asyncio.get_event_loop() task = asyncio.ensure_future(coroutine) loop.run_until_complete(task) print('返回结果:{}'.format(task.result())) 运行结果如下: 返回结果:暂停了 2 秒 另一种是通过添加回调函数来实现: import asyncio import time async def _sleep(x): time.sleep(x) return '暂停了 {} 秒'.format(x) def callback(future): print('回调返回结果:{}'.format(future.result())) coroutine = _sleep(2) loop = asyncio.get_event_loop() task = asyncio.ensure_future(coroutine) task.add_done_callback(callback) loop.run_until_complete(task) 运行结果如下: 回调返回结果:暂停了 2 秒 asyncio 实现并发需要多个协程来完成,每当有任务阻塞时需要 await,然后让其他协程继续工作。 import asyncio async def do_some_work(x): print('等待中 ...') await asyncio.sleep(x) print('{} 秒后结束'.format(x)) return x # 协程对象 coroutine1 = do_some_work(1) coroutine2 = do_some_work(2) coroutine3 = do_some_work(4) # 任务列表 tasks = [ asyncio.ensure_future(coroutine1), asyncio.ensure_future(coroutine2), asyncio.ensure_future(coroutine3) ] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks)) for task in tasks: print('任务结果:{}'.format(task.result())) 运行结果如下: 等待中 ... 等待中 ... 等待中 ... 1 秒后结束 2 秒后结束 4 秒后结束 任务结果:1 任务结果:2 任务结果:4 协程之间可以进行嵌套,即在一个协程中 await 另一个协程: import asyncio async def do_some_work(x): print('等待中 ...') await asyncio.sleep(x) print('{} 秒后结束'.format(x)) return x async def out_do_some_work(): coroutine1 = do_some_work(1) coroutine2 = do_some_work(2) coroutine3 = do_some_work(4) tasks = [ asyncio.ensure_future(coroutine1), asyncio.ensure_future(coroutine2), asyncio.ensure_future(coroutine3) ] dones, pendings = await asyncio.wait(tasks) for task in dones: print('任务结果:{}'.format(task.result())) loop = asyncio.get_event_loop() loop.run_until_complete(out_do_some_work()) 如果使用 asyncio.gather() 来获取结果,则需要对获取结果部分做如下修改: results = await asyncio.gather(*tasks) for result in results: print('任务结果:{}'.format(result)) asyncio.wait() 返回 dones 和 pendings,分别表示已完成和未完成的任务;asyncio.gather() 则会把结果直接返回。 运行结果如下: 等待中 ... 等待中 ... 等待中 ... 1 秒后结束 2 秒后结束 4 秒后结束 任务结果:1 任务结果:4 任务结果:2 协程(准确的说是 Future 或 Task 对象)包含如下状态: Pending:已创建,未执行 Running:执行中 Done:执行完毕 Cancelled:被取消 测试代码如下: coroutine = _sleep(10) loop = asyncio.get_event_loop() task = loop.create_task(coroutine) print('Pending') try: t = Thread(target=loop.run_until_complete, args=(task, )) t.start() print('Running') t.join() except KeyboardInterrupt as e: task.cancel() print('Cancel') finally: print('Done') 执行顺利的话,运行结果如下: Pending Running Done 如果在启动后按下 Ctrl + C 则会触发 task.cancel(),运行结果如下: Pending Running Cancelled Done asyncio.wait() 可以通过参数控制何时返回: import random import asyncio async def random_sleep(): await asyncio.sleep(random.uniform(0.5, 6)) loop = asyncio.get_event_loop() tasks = [random_sleep() for _ in range(1, 10)] dones, pendings = loop.run_until_complete(asyncio.wait( tasks, return_when=asyncio.FIRST_COMPLETED)) print('第一次完成的任务数:{}'.format(len(dones))) dones, pendings = loop.run_until_complete(asyncio.wait( pendings, timeout=2)) print('第二次完成的任务数: {}'.format(len(dones))) dones, pendings = loop.run_until_complete(asyncio.wait(pendings)) print('第三次完成的任务数:{}'.format(len(dones))) 运行结果如下: 第一次完成的任务数:1 第二次完成的任务数: 4 第三次完成的任务数:4 https://iswbm.com/108.html ↩︎

2021/4/3
articleCard.readMore

进程,线程和协程 (Process, Thread and Coroutine)

Python 实现篇请参见:进程,线程和协程 (Process, Thread and Coroutine) - 实现篇 进程,线程和协程 进程(Process)是计算机中已运行的程序 1。线程(Thread)是操作系统能够进行运算调度的最小单位。大部分情况下,线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 2。 进程和线程之间的主要区别在于: 线程共享创建其进程的地址空间,进程使用自己的地址。 线程可以直接访问进程的数据,进程使用其父进程数据的副本。 线程可以同其进程中其他线程直接通信,进程必须使用进程间通讯(inter-process communicate, IPC)与同级进程通信。 线程开销较小,进程开销较大。 线程的创建较为容易,进程需要复制其父进程。 线程可以控制相同进程的其他线程,进程只能控制其子进程。 对于主线程的修改(例如:取消、优先级修改等)可能会影响进程中的其他线程,对于父进程的修改不会影响其子进程。 单线程进程和多线程进程之间的对比如下图所示: 一个关于进程和线程的形象类比如下 3: 计算机的核心是 CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。 一个车间里,可以有很多工人。他们协同完成一个任务。 线程就好比车间里的工人。一个进程可以包括多个线程。 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual Exclusion,Mutex),防止多个线程同时读写某一块内存区域。 还有些房间,可以同时容纳 $n$ 个人,比如厨房。也就是说,如果人数大于 $n$,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。 这时的解决方法,就是在门口挂 $n$ 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore),用来保证多个线程不会互相冲突。不难看出,Mutex 是 Semaphore 的一种特殊情况($n = 1$ 时)。也就是说,完全可以用后者替代前者。但是,因为 Mutex 较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。 操作系统的设计,因此可以归结为三点:(1). 以多进程形式,允许多个任务同时运行;(2). 以多线程形式,允许单个任务分成不同的部分运行;(3). 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。 协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。 子例程(Subroutine),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。 协程和子例程的执行过程对比如下: 子例程可以调用其他子例程,调用者等待被调用者结束后继续执行,故而子例程的生命期遵循后进先出,即最后一个被调用的子例程最先结束返回。协程的生命期完全由对它们的使用需要来决定。 子例程的起始处是惟一的入口点,每当子例程被调用时,执行都从被调用子例程的起始处开始。协程可以有多个入口点,协程的起始处是第一个入口点,每个 yield 返回出口点都是再次被调用执行时的入口点。 子例程只在结束时一次性的返回全部结果值。协程可以在 yield 时不调用其他协程,而是每次返回一部分的结果值,这种协程常称为生成器或迭代器。 协程类似于线程,但是协程是协作式多任务的,而线程是抢占式多任务的。这意味着协程提供并发性而非并行性。协程超过线程的好处是它们可以用于硬性实时的语境(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用),这里不需要用来守卫临界区段的同步原语比如互斥锁、信号量等,并且不需要来自操作系统的支持。 通信 进程间通信 管道 管道(Pipeline)是一系列将标准输入输出链接起来的进程,其中每一个进程的输出被直接作为下一个进程的输入。 例如: ls -l | less ls 用于在 Unix 下列出目录内容,less 是一个有搜索功能的交互式的文本分页器。这个管道使得用户可以在列出的目录内容比屏幕长时目录上下翻页。 命名管道 命名管道是计算机进程间的一种先进先出通信机制。是类 Unix 系统传统管道的扩展。传统管道属于匿名管道,其生存期不超过创建管道的进程的生存期。但命名管道的生存期可以与操作系统运行期一样长。 信号 信号(Signals)是 Unix、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。 例如,在一个运行的程序的控制终端键入特定的组合键可以向它发送某些信号:Ctrl + C 发送 INT 信号(SIGINT),这会导致进程终止;Ctrl + Z 发送 TSTP 信号(SIGTSTP),这会导致进程挂起。 消息队列 消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。 消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。 信号量 信号量(Semaphore)又称为信号标,是一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。当线程完成一次对该 Semaphore 对象的等待(wait)时,该计数值减一;当线程完成一次对 Semaphore 对象的释放(release)时,计数值加一。当计数值为 0,则线程等待该 Semaphore 对象不再能成功直至该 Semaphore 对象变成 signaled 状态。Semaphore 对象的计数值大于 0,为 signaled 状态;计数值等于 0,为 nonsignaled 状态. 共享内存 共享内存指可被多个进程存取的内存,一个进程是一段程序的单个运行实例。在这种情况下,共享内存被用作进程间的通讯。 伯克利套接字 伯克利套接字(Internet Berkeley Sockets),又称为 BSD 套接字是一种应用程序接口,主要用于实现进程间通讯,在计算机网络通讯方面被广泛使用。 线程间通信 锁机制 互斥锁:互斥锁(Mutual Exclusion,Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。 条件锁:读写锁是计算机程序的并发控制的一种同步机制,用于解决读写问题。读操作可并发重入,写操作是互斥的。 条件变量:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”而挂起;另一个线程使“条件成立”(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。 自旋锁:自旋锁是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 信号 同上文。 信号量 同上文。 https://zh.wikipedia.org/wiki/进程 ↩︎ https://zh.wikipedia.org/wiki/线程 ↩︎ https://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html ↩︎

2021/4/1
articleCard.readMore

胶囊网络 (Capsule Network)

CNN 的缺陷 卷积神经网络(CNN)在图像领域取得了很大的成功,但同时也存在一定的缺陷。卷积层中的卷积核对输入图像利用卷积运算提取其中的特征。卷积核以一个较小的尺寸并以一定的步长在图像上移动得到特征图。步长越大,特征图的尺寸就越小,过大的步长会丢失部分图像中的特征。池化层作用于产生的特征图上,使得 CNN 可以在不同形式的图像中识别出相同的物体,为 CNN 引入了空间不变性。 CNN 最大的缺陷就是忽略了不同特征之间的相对位置,从而无法从图像中识别出姿势、纹理和变化。CNN 中的池化操作使得模型具有空间不变性,因此模型就不具备等变性。以下图为例,CNN 会把第一幅和第二幅识别为人脸,而将第三幅方向翻转的图识别为不是人脸。池化操作造成了部分信息的丢失,因此需要更多的训练数据来补偿这些损失。 等变和不变 对于一个函数 $f$ 和一个变换 $g$,如果有: $$ f \left(g \left(x\right)\right) = g \left(f \left(x\right)\right) $$ 则称 $f$ 对变换 $g$ 有等变性。 例如,变换 $g$ 为将图像向左平移若干像素,函数 $f$ 表示检测一个人脸的位置。则 $f \left(g \left(x\right)\right)$ 表示先将图片左移,我们将在原图的左侧检测到人脸;$g \left(f \left(x\right)\right)$ 表示先检测人脸位置,然后将人脸位置左移。这两者的输出结果是一样的,与我们施加变换的顺序无关。CNN 中的卷积操作使得它对平移操作具有等变性。 对于一个函数 $f$ 和一个变换 $g: g \left(x\right) = x'$,如果有: $$ f \left(x\right) = f \left(x'\right) = f \left(g \left(x\right)\right) $$ 则称 $f$ 对变换 $g$ 有不变性。 例如,变换 $g$ 为旋转或平移,函数 $f$ 表示检测图中是否有黑色,那么这些变换不会对函数结果有任何影响,可以说函数对该变换具有不变性。CNN 中的池化操作对平移操作具有近似不变性。 逆图形 计算机图形学根据几何数据的内部层次结构来构造可视图像,该表示的结构将对象的相对位置考虑在内。软件采用层次的表示方式将其渲染为屏幕上的图像。人类大脑的工作原理则与渲染过程相反,我们称其为逆图形。大脑中对象的表示并不依赖于视角。 例如下图,人眼可以很容易的分辨出是自由女神像,只是角度不同,但 CNN 却很难做到,因为它不理解 3D 空间的内在。 胶囊网络 胶囊 在引入“胶囊”这个概念的第一篇文献 Transforming Auto-encoders 1 中,Hinton 等人对胶囊概念理解如下: 人工神经网络不应当追求“神经元”活动中的视角不变性(使用单一的标量输出来总结一个局部池中的重复特征检测器的活动),而应当使用局部的“胶囊”,这些胶囊对其输入执行一些相当复杂的内部计算,然后将这些计算的结果封装成一个包含信息丰富的输出的小向量。每个胶囊学习辨识一个有限的观察条件和变形范围内隐式定义的视觉实体,并输出实体在有限范围内存在的概率及一组“实例参数”,实例参数可能包括相对这个视觉实体的隐式定义的典型版本的精确的位姿、照明条件和变形信息。当胶囊工作正常时,视觉实体存在的概率具有局部不变性——当实体在胶囊覆盖的有限范围内的外观流形上移动时,概率不会改变。实例参数却是“等变的”——随着观察条件的变化,实体在外观流形上移动时,实例参数也会相应地变化,因为实例参数表示实体在外观流形上的内在坐标。 人造神经元输出单个标量。对于 CNN 卷积层中的每个卷积核,对整个输入图复制同一个内核的权重输出一个二维矩阵。矩阵中每个数字是该卷积核对输入图像一部分的卷积,这个二维矩阵看作是重复特征检测器的输出。所有卷积核的二维矩阵堆叠在一起得到卷积层的输出。CNN 利用最大池化实现不变性,但最大池化丢失了有价值的信息,也没有编码特征之间的相对空间关系。 胶囊将特征检测的概率作为其输出向量的长度进行编码,检测出的特征的状态被编码为该向量指向的方向。当检测出的特征在图像中移动或其状态发生变化时,概率仍然保持不变(向量的长度没有改变),但它的方向改变了。 下表总结了胶囊和神经元的不同: Capsule Traditional Neuron Input from low-level capsule/neuron $\text{vector}\left(\mathbf{u}_i\right)$ $\text{scalar}\left(x_i\right)$ Operration Affine Transform $\widehat{\mathbf{u}}_{j \mid i}=\mathbf{W}_{i j} \mathbf{u}_{i}$ - Weighting $\mathbf{s}_{j}=\sum_{i} c_{i j} \widehat{\mathbf{u}}_{j \mid i}$ $a_{j}=\sum_{i} w_{i} x_{i}+b$ Sum Nonlinear Activation $\mathbf{v}_{j}=\dfrac{\left\|\mathbf{s}_{j}\right\|^{2}}{1+\left\|\mathbf{s}_{j}\right\|^{2}} \dfrac{\mathbf{s}_{j}}{\left\|\mathbf{s}_{j}\right\|}$ $h_{j}=f\left(a_{j}\right)$ Output $\text{vector}\left(\mathbf{v}_j\right)$ $\text{scalar}\left(h_j\right)$ 人造神经元包含如下 3 个计算步骤: 输入标量的加权 加权标量的求和 求和标量到输出标量的非线性变换 胶囊可以理解为上述 3 个步骤的向量版,同时增加了对输入的仿射变换: 输入向量的矩阵乘法:胶囊接受的输入向量编码了低层胶囊检测出的相应对象的概率,向量的方向编码了检测出的对象的一些内部状态。接着将这些向量乘以相应的权重矩阵 $\mathbf{W}$,$\mathbf{W}$ 编码了低层特征(例如:眼睛、嘴巴和鼻子)和高层特征(例如:面部)之间的空间关系和其他重要关系。 向量的标量加权:这个步骤同人造神经元对应的步骤类似,但神经元的权重是通过反向传播学习的,而胶囊则使用动态路由。 加权向量的求和:这个步骤同人造神经元对应的步骤类似。 求和向量到输出向量的非线性变换:胶囊神经网络的非线性激活函数接受一个向量,然后在不改变方向的前提下,将其长度压缩到 1 以下。 动态路由 胶囊网络使用动态路由算法进行训练,算法过程如下 2: 第 1 行表示算法的输入为:低层 $l$ 中所有胶囊的输出 $\widehat{\mathbf{u}}$,以及路由迭代计数 $r$。 第 2 行中的 $b_{ij}$ 为一个临时变量,其值在迭代过程中更新,算法运行完毕后其值被保存在 $c_{ij}$ 中。 第 3 行表示如下步骤将会被重复 $r$ 次。 第 4 行利用 $\mathbf{b}_i$ 计算低层胶囊的权重向量 $\mathbf{c}_i$。$\text{softmax}$ 确保了所有权重为非负数,且和为一。第一次迭代后,所有系数 $c_{ij}$ 的值相等,随着算法的继续,这些均匀分布将发生改变。 第 5 行计算经前一步确定的路由系数 $c_{ij}$ 加权后的输入向量的线性组合。该步缩小输入向量并将他们相加,得到输出向量 $\mathbf{s}_j$。 第 6 行对前一步的输出向量应用 $\text{squash}$ 非线性函数。这确保了向量的方向被保留下来,而长度被限制在 1 以下。 第 7 行通过观测低层和高层的胶囊,根据公式更新相应的权重 $b_{ij}$。胶囊 $j$ 的当前输出与从低层胶囊 $i$ 处接收到的输入进行点积,再加上旧的权重作为新的权重。点积检测胶囊输入和输出之间的相似性。 重复 $r$ 次,计算出所有高层胶囊的输出,并确立路由权重。之后正向传导就可以推进到更高层的网络。 点积运算接收两个向量,并输出一个标量。对于给定长度但方向不同的两个向量而言,点积有几种情况:$a$ 最大正值;$b$ 正值;$c$ 零;$d$ 负值;$e$ 最小负值,如下图所示: 我们用紫色向量 $\mathbf{v}_1$ 和 $\mathbf{v}_2$ 表示高层胶囊,橙色向量表示低层胶囊的输入,其他黑色向量表示接收自其他低层胶囊的输入,如下图所示: 左侧的紫色输出 $\mathbf{v}_1$ 和橙色输入 $\widehat{\mathbf{u}}_{1|1}$ 指向相反的方向,这意味着他们的点积是一个负数,从而路由系数 $c_{11}$ 减少;右侧的紫色输出 $\mathbf{v}_2$ 和橙色输入 $\widehat{\mathbf{u}}_{2|1}$ 指向相同的方向,从而路由系数 $c_{12}$ 增加。在所有高层胶囊及其所有输入上重复该过程,得到一个路由系数集合,达成了来自低层胶囊的输出与高层胶囊的输出的最佳匹配。 网络架构 胶囊网络由 6 层神经网络构成,前 3 层是编码器,后 3 层是解码器: 卷积层 PrimaryCaps(主胶囊)层 DigitCaps(数字胶囊)层 第一全连接层 第二全连接层 第三全连接层 编码器 编码器部分如下图所示: 卷积层用于检测 2D 图像的基本特征。PrimaryCaps 层包含 32 个主胶囊,接收卷积层检测到的基本特征,生成特征的组合。DigitCaps 层包含 10 个数字胶囊,每个胶囊对应一个数字。 对于 $k$ 个类别的数字,我们希望最高层的胶囊当且仅当一个数字出现在图像中时具有一个长的实例化向量。为了允许多个数字,对于每个 DigitCap 使用一个独立的损失函数: $$ L_{k}=T_{k} \max \left(0, m^{+}-\left\|\mathbf{v}_{k}\right\|\right)^{2}+\lambda\left(1-T_{k}\right) \max \left(0,\left\|\mathbf{v}_{k}\right\|-m^{-}\right)^{2} $$ DigitCaps 层的输出为 10 个 16 维的向量,根据上面的公式计算每个向量的损失值,然后将 10 个损失值相加得到最终损失。 在损失函数中,当正确的标签与特定 DigitCap 的数字对应时 $T_k$ 为 1,否则为 0。加号前一项用于计算正确 DigitCap 的损失,当概率值大于 $m^{+} = 0.9$ 时为 0,当概率值小于 $m^{+} = 0.9$ 时为非零值;加号后一项用于计算错误 DigitCap 的损失,当概率值小于 $m^{-} = 0.1$ 时值为 0,当概率值大于 $m^{-} = 0.1$ 时为非零值。公式中的 $\lambda = 0.5$ 用于确保训练中数值的稳定性。 简单来说,低层的胶囊用于检测一些特定模式的出现概率和姿态,高层的胶囊用于检测更加复杂的图像,如下图所示: 解码器 解码器部分如下图所示: 解码器从正确的 DigitCap 中接收一个 16 维向量,并学习将其解码为数字图像。解码器接收正确的 DigitCap 的输出作为输入,并学习重建一张 $28 \times 28$ 像素的图像,损失函数为重建图像与输入图像之间的欧式距离。 Hinton, G. E., Krizhevsky, A., & Wang, S. D. (2011, June). Transforming auto-encoders. In International conference on artificial neural networks (pp. 44-51). Springer, Berlin, Heidelberg. ↩︎ Sabour, S., Frosst, N., & Hinton, G. E. (2017, December). Dynamic routing between capsules. In Proceedings of the 31st International Conference on Neural Information Processing Systems (pp. 3859-3869). ↩︎

2021/3/14
articleCard.readMore

投票公平合理吗?

只有道德上的相对民主,没有制度上的绝对公平,求同存异才能长治久安。 无处不在的投票 在古代雅典城邦有一项政治制度称之为陶片放逐制,是由雅典政治家克里斯提尼于公元前 510 年创立。雅典人民可以通过投票强制将某个人放逐,目的在于驱逐可能威胁雅典的民主制度的政治人物。投票者在选票——陶罐碎片较为平坦处,刻上他认为应该被放逐者的名字,投入本部落的投票箱。如果选票总数未达到 6000,此次投票即宣告无效;如果超过 6000,再按票上的名字将票分类,得票最多的人士即为当年放逐的人选。 美国总统选举的方式称为选举人团,是一种间接选举,旨在选出美国总统和副总统。根据《美国宪法》及其修正案,美国各州公民先选出该州的选举人,再由选举人代表该州投票。不采用普选制度的原因,主要是由于美国是联邦制国家,并考虑到各州的特定地理及历史条件,制宪元老决定采取选举人团制度,保障各州权益,所以美国没有公民直选的总统。 离我们最近的可能就是朋友聚会吃什么这个问题了,烧烤还是火锅,这是个问题。当然,只要你想要,还可以:蒸羊羔、蒸熊掌、蒸鹿尾儿、烧花鸭、烧雏鸡、烧子鹅 …… 投票制度 多数制 多数制(Plurality Voting System)的原则是“胜者全取”,又被称为多数代表制(Majoritarian Representation),分成相对多数(Relative Plurality)和绝对多数(Absolute Majority)。相对多数制即不论票数多少,得票最多的候选人便可当选。绝对多数制指候选人需要得到指定的票数方可当选,门槛在设定上须达有效票之过半数、三分之二、四分之三或五分之四等多数,亦可以是更高的比例或数字。 多数制的优点在于简单易行,缺点在于当选票分散的越平均,投票结果的争议性就会越大。例如:有 10 人参与投票,有 3 人选择吃火锅,有 3 人选择吃烧烤,有 4 人选择吃家常菜。根据多数制规则,最终的选择为吃家常菜,但会有 6/10 的人没有得到满意的结果。 波达计数法 波达计数法(Borda Count)是通过投票人对候选者进行排序,如果候选者在选票的排第一位,它就得某个分数,排第二位得一个较小的分数,如此类推,分数累计下来最高分的候选者便取胜。 波达计数法相比于多数制不容易选出比较有争议的选项。假设有一场选举,共 4 位候选人,共收集到有效选票 100 张,选票统计结果(理论山可能出现的选票类型为 $4! = 24$ 种可能,简单起见,示例中仅有 4 种可能)如下: # 51 票 5 票 23 票 21 票 1 张三 王五 李四 马六 2 王五 李四 王五 王五 3 李四 马六 马六 李四 4 马六 张三 张三 张三 选举采用排名第 $n$ 位得 $4 - n$ 分的准则,每个人的分数如下: 张三:$51 \times 3 + 5 \times 0 + 23 \times 0 + 21 \times 0 = 153$ 李四:$51 \times 1 + 5 \times 2 + 23 \times 3 + 21 \times 1 = 151$ 王五:$51 \times 2 + 5 \times 3 + 23 \times 2 + 21 \times 2 = 205$ 马六:$51 \times 0 + 5 \times 1 + 23 \times 1 + 21 \times 3 = 91$ 按照多数制应该是张三获胜(得到第 1 名的票数最多,共计 51 票)。不过通过波达计数法可以发现大家对张三的喜好比较极端:只有特别喜欢(排名第 1)和特别不喜欢(排名第 4)两种情况,所以张三是一个具有争议的人。但王五不同,虽然其获得第 1 的选票并不多,但是获得第 2 的选票很多,说明大部分人都是比较能接受王五的。 波达计数法同时也存在几个问题: 不同的权重的计分规则,可能得到不同结果。以下表为例: # 2 票 3 票 4 票 # 2 分 A A C 5 分 1 分 B C B 2 分 0 分 C B A 1 分 采用左边权重的计分结果为:A:10 分,B:6 分,C:11 分,则 C 胜出。采用右边权重的计分结果为:A:29 分,B:15 分,C:28 分,则 A 胜出。不难看出,多数投票制是波达计数法的一个特例,即第 1 名的权重为非零值,其余名次权重均为零。 容易出现恶意投票局面。参与的人在投票时往往会掺杂个人情感,假设存在这样一次选举:张三、李四、王五三人竞选班长,张三和李四是最有希望竞争班长的人员,而王五则表现平庸。参与投票的同学几乎近一半的人支持张三,近一半的人支持李四,很少人支持王五。参与投票的同学自认为都很“聪明”,担心自己不支持的人(假设为李四)担任班长,同时认为王五没希望竞选成功,因此在排序时会把王五排在第 2 名,把李四排在第 3 名。此时,张三和李四都变成了具有争议的人,那么选举的结果就很有可能是王五获胜。 孔多赛投票制 波达计数法还存在一个严重的问题是,当其中一个选项退出后,投票的结果会发生变化。仍然以上文中的例子为例: # 2 分 1 分 0 分 # 1 分 0 分 2 票 A B C 2 票 A C 3 票 A C B 3 票 A C 4 票 C B A 4 票 C A 在包含选项 B 时,投票结果为:A:10 分,B:6 分,C:11 分,C 获胜。当把选项 B 去掉后,投票结果为:A:5 分,C:4 分,A 获胜。去掉选项 B,规则没有发生变化(不同排名之间权重相差为 1),投票人的意愿也没有发生变化(候选人的相对名次没有发生变动),但投票结果却截然不同。 为了解决这个问题,孔多赛提供了采用两两对决的方式进行投票。投票人依旧按照类似波达计数法对候选人进行排序,但与波达计数法不同的是并不进行计分而是需要统计出所有两两对决的情况。上面这个例子中两两对决的情况有 6 种:A > B,B > A,B > C,C > B,A > C,C > A,对决的统计结果如下表所示: # 5 次 4 次 2 次 7 次 5 次 4 次 1 分 A B B C A C 0 分 B A C B C A 由于 A > B 有 5 次,B > A 有 4 次,因此在 A 和 B 的两两对决中获胜的是 A。同理可得,在 A 和 C 的两两对决中获胜的是 A,在 B 和 C 的两两对决中获胜的是 C。当存在很多选项时,假设选项 A 在与任何其它选项的两两比较中都能胜出,那 A 称为孔多塞胜利者。 如果你对 Facebook 历史有了解,早期扎克伯格在哈佛大学就读学士期间,写了一个名为 Facemash 的网站程序,根据哈佛校内报纸《The Harvard Crimson》,Facemash 会从校内的网络上收集照片,每次将两张照片并排后让用户选择“更火辣”的照片。Facemash 就采取了类似孔多塞投票的方法对女生进行投票,下图是电影社交网络中这个原型的截图: 孔多塞投票制由于统计比较麻烦,现实生活中使用的情况不多。同时,孔多塞投票也存在一个问题,即孔多塞循环。假设 A 和 B 对决的优胜者为 A,B 和 C 对决的优胜者为 B,C 和 A 对决的优胜者为 C,则没有一个选项打败其他所有选项,那么这样就无法得到投票的结果。这称之为孔多塞悖论(投票悖论),在这个假想情况中,集体倾向可以是循环性的,即使个人的倾向不是。 所以呢? 所以什么样的投票才算是公平合理的投票呢?肯尼斯·阿罗给出了一个解答。 有 $N$ 种选择,有 $m$ 个决策者,他们每个人都对这 $N$ 个选择有一个从优至劣的排序。我们要设计一种选举法则,使得将这 $m$ 个排序的信息汇总成一个新的排序,称为投票结果。我们希望这种法则满足以下条件: 一致性(Unanimity)。或称为帕累托最优(Pareto Efficiency),即如果所有的 $m$ 个决策者都认为选择 $A$ 优于 $B$,那么在投票结果中,$A$ 也优于 $B$。 非独裁(Non-Dictatorship)。不存在一个决策者 $X$,使得投票结果总是等同于 $X$ 的排序。 独立于无关选项(Independence of Irrelevant Alternatives,IIA))。如果现在一些决策者改了主意,但是在每个决策者的排序中,$A$ 和 $B$ 的相对位置不变,那么在投票结果中 $A$ 和 $B$ 的相对位置也不变。 如果选项 $N \geq 3$,投票人数 $m \geq 2$ 时,没有任何一种投票规则能够满足以上 3 点,这就是阿罗悖论。所以只有道德上的相对民主,没有制度上的绝对公平,求同存异才能长治久安。 本文参考自《投票公平合理吗?为什么没有绝对的公平?阿罗不可能定理》。

2021/1/17
articleCard.readMore

图存储与计算(Network Storage & Computing)

本文为《复杂网络系列》文章 图存储 语义网络与 RDF 存储 1968 年 Ross Quillian 在其博士论文中最先提出语义网络(Semantic Web),把它作为人类联想记忆的一个显式心理学模型,并在他设计的可教式语言理解器 TLC(Teachable Language Comprehenden)中用作知识表示方法。 语义网络的基本思想是在网络中,用“节点”代替概念,用节点间的“连接弧”(称为联想弧)代替概念之间的关系,因此,语义网络又称联想网络。它在形式上是一个带标识的有向图。由于所有的概念节点均通过联想弧彼此相连知识推导。 一个语义网络的基本构成如下: 语义网络中的节点:表示各种事物、概念、情况、属性、动作、状态等,每个节点可以带有若干属性,一般用框架或元组表示。此外,节点还可以是一个语义子网络,形成一个多层次的嵌套结构。 语义网络中的弧:表示各种语义联系,指明它所连接的节点间某种语义关系。 节点和弧都必须带有标识,以便区分各种不同对象以及对象间各种不同的语义联系。 之后 Tim Berners-Lee 又提出了语义网堆栈(Semantic Web Stack)的概念。语义网堆栈利用图示解释是不同层面的语言所构成的层级结构,其中,每一层面都将利用下游层面的能力,语义网堆栈如下图所示: 资源描述框架(Resource Description Framework,RDF)是用于描述网络资源的 W3C 标准,比如网页的标题、作者、修改日期、内容以及版权信息。 RDF 使用 Web 标识符来标识事物,并通过属性和属性值来描述资源。 对资源、属性和属性值的解释: 资源是可拥有 URI 的任何事物,比如 http://www.w3school.com.cn/rdf 属性是拥有名称的资源,比如 author 或 homepage 属性值是某个属性的值,比如 David 或 http://www.w3school.com.cn(请注意一个属性值可以是另外一个资源) 下面是一个 RDF 示例文档(这是一个简化的例子,命名空间被忽略了): <?xml version="1.0"?> <RDF> <Description about="http://www.w3school.com.cn/RDF"> <author>David</author> <homepage>http://www.w3school.com.cn</homepage> </Description> </RDF> 资源、属性和属性值的组合可形成一个陈述(被称为陈述的主体、谓语和客体)。上述的 RDF 文档包含了如下两个陈述: 陈述:The author of http://www.w3school.com.cn/rdf is David 陈述的主体是:http://www.w3school.com.cn/rdf 谓语是:author 客体是:David 陈述:The homepage of http://www.w3school.com.cn/rdf is http://www.w3school.com.cn 陈述的主体是:http://www.w3school.com.cn/rdf 谓语是:homepage 客体是:http://www.w3school.com.cn 更多 RDF 介绍请参见:https://www.w3school.com.cn/rdf/index.asp 。 Apache Jena 是一个用于构建语义网络(Semantic Web)和链接数据(Linked Data)应用的开源 Java 框架。Jena 提供了 3 大部分功能: RDF RDF API:提供构建和读取 RDF 图的核心 API,并利用 RDF/XML 或 Turtle 等数据类型序列化数据。 ARQ(SPARQL):提供一种 SPARQL 1.1 的编译引擎 ARQ 用于查询 RDF。 Triple store TDB:提供一种原生高效的 Triple 存储 TDB,全面支持 Jena APIs。 Fuseki:提供 REST 风格的 RDF 数据交互方式。 OWL Ontology API:通过 RDFS,OWL 等为 RDF 数据添加更多语义信息。 Inference API:通过内置的 OWL 和 RDFS 语义推理器 构建个性化的推理规则。 下面以 Graph of The Gods 的关系图对 Jena 的基本功能进行说明。Graph of The Gods 是一张描述希腊神话相关事物之间关系的图,其中顶点的类型有:titan(泰坦,希腊神话中曾经统治师姐的古老神族),god(神),demigod(半神),human(人),monster(怪物),location(地点);关系的类型有:father(父亲),brother(兄弟),mother(母亲),battled(战斗),lives(居住)。 以 Apache Tomcat 作为容器来安装 Apache Jena Fuseki,下载最新版的 Apache Jena Fuseki 并解压,将其中的 fuseki.war 复制到已经安装并运行的 Apache Tomcat 的 webapps 路径下。安装完毕后,进入 http://127.0.0.1:8080/fuseki 即可使用 Apache Jena Fuseki。 在导入 Graph of The Gods 数据后,执行如下查询语句可以获得 jupiter 的所有兄弟: PREFIX gods: <http://leovan.me/gods/> SELECT DISTINCT ?god WHERE { ?god gods:brother gods:jupiter } 查询结果为: god 1 gods:pluto 2 gods:neptune 图数据库 图数据库是一个使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据。不同于关系型数据库,图数据库为 NoSQL(Not Only SQL)的一种,属于联机事务处理(OLTP)的范畴,可以解决现有关系数据库的局限性。 下图展示了近年来不同类型数据库的流行度趋势,不难看出近年来越来越多的人开始关注图数据库。 https://db-engines.com/en/ranking_categories 截止到 2020 年 12 月,图数据库的排名如下图所示: https://db-engines.com/en/ranking/graph+dbms 其中,Neo4j、JanusGraph、Dgraph、TigerGraph、Nebula Graph 均为时下常用的图数据库。从下图的流行度趋势角度来看,JanusGraph、Dgraph、TigerGraph 和 Nebula Graph 等后起之秀发展迅速。 https://db-engines.com/en/ranking_trend/graph+dbms 不同的图数据库有着不同的优劣势,用户可以根据实际业务场景选择合适的图数据库。下面给到一些较新的图数据库对比和评测: 主流开源分布式图数据库 Benchmark 图数据库对比:Neo4j vs Nebula Graph vs HugeGraph 图分析系统基准测试报告 图数据平台产品测试报告 查询语言 图查询语言(Graph Query Language,GQL)是一种用于图数据库的查询语言,类比于关系型数据库的查询语言 SQL。2019 年 9 月,GQL 被提议为一种新的数据库查询语言(ISO/IEC WD 39075),目前仍处于开发当中,因此市面上还没有统一的图查询语言标准。 Gremlin Gremlin 是 Apache TinkerPop 框架下的图遍历语言。Gremlin 适用于基于 OLTP 的图数据库以及基于 OLAP 的图分析引擎,支持命令式和声明式查询。支持 Gremlin 的图数据库有:Neo4j、JanusGraph 等。 Cypher Cypher 是一种声明式图查询语言,这使得在不必编写遍历逻辑的情况下可以实现高效的查询。支持 Cypher 的图数据库有:Neo4j、RedisGraph、Nebula Graph 等。 nGQL nGQL 是一种声明式的图查询语言,支持图遍历、模式匹配、聚合运算和图计算等特性。支持 nGQL 的图数据库有:Nebula Graph。 比较 针对 3 种不同的查询语言,对于图中相关概念的表示也略有不同,如下表所示: 术语 Gremlin Cypher nGQL 点 Vertex Node Vertex 边 Edge Relationship Edge 点类型 Label Label Tag 边类型 label RelationshipType edge type 点 ID vid id(n) vid 边 ID eid id(r) 无 插入 add create insert 删除 drop delete delete / drop 更新属性 setProperty set update 更多不同查询语言之间的详细对比可以参见如下资料: 一文了解各大图数据库查询语言 | 操作入门篇 文档解读 | SQL vs. nGQL 图计算 图计算框架 GraphX GraphX 是一个基于 Spark 大规模图计算框架。GraphX 通过引入一个包含带有属性的顶点和变的有向图对 Spark 的 RDD 进行了扩展。通过 subgraph、joinVertices 和 aggregateMessages 等算子实现了 PageRank、连通子图、LPA 等图算法。 Plato Plato 是由腾讯开源的高性能图计算框架。Plato 主要提供两方面的能力:离线图计算和图表示学习,目前支持的图算法如下: 算法分类 算法 图特征 树深度/宽度;节点数/边数/密度/节点度分布;N-阶度;HyperANF 节点中心性指标 KCore;Pagerank;Closeness;Betweenness 连通图 & 社团识别 Connected-Component;LPA;HANP 图表示学习 Node2Vec-Randomwalk;Metapath-Randomwalk 聚类/分圈算法 FastUnfolding 其他图相关算法 BFS;共同类计算 待开源算法 Word2Vec;Line;GraphVite;GCN 在计算性能上,Plato 与 Spark GraphX 在 PageRank 和 LPA 两个算法上的计算耗时与内存消耗对比如下图所示: GraphScope GraphScope 由有阿里巴巴开源的一个统一的分布式图计算平台。GraphScope 提供了一个一站式环境,可以通过用户友好的 Python 接口在集群内对图进行操作。GraphScope 利用一系列开源技术使得集群上的大规模图数据的多阶段处理变得简单,这些技术包括:用于分析的 GRAPE、用于查询的 MaxGraph 、用于图神经网络计算的 Graph-Learn 和用于提供高效内存数据交换的 vineyard。GraphScope 的整体架构如下图所示: GraphScope Interactive Engine(GIE)是一个用于探索性分析大规模复杂图结构数据的引擎,它通过 Gremlin 提供高级别的图查询语言,同时提供自动并行执行功能。 GraphScope Analytical Engine(GAE)是一个基于 GRAPE 1 提供并行图算法的分析引擎。除了提供基础的内置算法以外,GAE 允许用户利用 Python 基于 PIE 1 编程模型编写自定义算法,PIE 编程模型的运行方式如下图所示: GraphScope 还提供以顶点为中心的 Pregel 模型 2,用户可以使用 Pregel 模型来实现自定义算法。 GraphScope Learning Engine(GLE)是一个用于开发和训练大规模图神经网络的分布式框架。GLE 提供基于全量图(用于 GCN、GAT 等算法)和采样子图(用于 GraphSAGE,FastGCN、GraphSAINT 等算法)两种不同方式训练图模型。整体架构如下图所示: Galileo Galileo 是由京东零售研发的图计算平台,提供离线和在线图计算和图数据服务能力。目前 Galileo 暂未开源,待开源后补充相关信息。 图神经网络 关于图神经网络内容,请参见之前的博客 图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)。 🎉🎉🎉 Happy New Year! 🎉🎉🎉 Fan, W., Yu, W., Xu, J., Zhou, J., Luo, X., Yin, Q., … & Xu, R. (2018). Parallelizing sequential graph computations. ACM Transactions on Database Systems (TODS), 43(4), 1-39. ↩︎ ↩︎ Malewicz, G., Austern, M. H., Bik, A. J., Dehnert, J. C., Horn, I., Leiser, N., & Czajkowski, G. (2010, June). Pregel: a system for large-scale graph processing. In Proceedings of the 2010 ACM SIGMOD International Conference on Management of data (pp. 135-146). ↩︎

2021/1/1
articleCard.readMore

网络算法 (Network Algorithms)

本文为《复杂网络系列》文章 1 网络基础算法 最短路径 最短路径(shortest path)算法是寻找两个顶点之间的最短路径,寻找网络中最短路径的标准算法称为广度优先搜索(breadth-first search)。算法的基本思想如下图所示: 根据广度优先搜索的基本思想,不难证明距 $s$ 最短距离为 $d$ 的每个顶点都有一个到 $s$ 的最短距离为 $d - 1$ 的邻居顶点。一个简单的实现方式是,创建一个有 $n$ 个元素的数组存储从源顶点 $s$ 到其他所有顶点的距离,同时创建一个距离变量 $d$ 来记录当前在搜索过程中所处的层数,算法的具体流程如下: 遍历距离数组,查找到 $s$ 的距离为 $d$ 的所有顶点。 查找上述顶点的所有邻居顶点,如果同 $s$ 的距离未知,则距离置为 $d + 1$。 如果距离未知的邻居顶点数量为零,则停止算法,否则将 $d$ 的值加一并重复上述过程。 这种方法在最坏的情况下时间复杂度为 $O \left(m + n^2\right)$,考虑多数网络的直径只随 $\log n$ 增长,算法运行的时间复杂度为 $O \left(m + n \log n\right)$。 上述算法中步骤 1 是最耗时的部分,通过使用队列的数据结构我们可以避免每次都遍历列表来找到距离源顶点 $s$ 距离为 $d$ 的顶点。构造一个队列,一个指针指向下一个要读取的元素,另一个指针指向要填充的空位,这样距离为 $d + 1$ 的顶点就会紧跟在距离为 $d$ 的顶点后面,队列如下图所示: 通过队列可以将算法的时间复杂度降至 $O \left(m + n\right)$,对于 $m \propto n$ 的稀疏网络而言,$O \left(m + n\right)$ 相当于 $O \left(n\right)$,所以算法的时间复杂度同顶点数量成正比。 通过对算法进行进一步修改则可以得到源顶点 $s$ 到其他任何顶点的最短路径。方法是在原来的网络上构建一个新的有向网络,该网络代表最短路径,称为最短路径树(shortest path tree),通常情况下,该网络是一个有向非循环网络,而不是树。 对于加权网络,利用广度优先搜索无法找到最短路径,这里需要用到 Dijkstra 算法 2 进行求解。算法将图中的顶点分成两组 $S$ 和 $U$,整个算法过程如下: 初始状态,$S$ 仅包含源顶点,即 $S = \left\{v\right\}$,$U$ 包含其余顶点。如果 $v$ 与 $U$ 中的顶点 $u$ 为邻居,则距离为边的权重,否则为无穷大。 从 $U$ 中选择一个距离 $v$ 最短的顶点 $k$,并把 $k$ 加入到 $S$ 中。 若从源点 $v$ 经过顶点 $k$ 到达 $u$ 的距离比之前 $v$ 到 $u$ 的距离短,则将距离修改为这个更短的距离。 重复步骤 2 和 3,直至所有顶点都包含在 $S$ 中。 整个算法过程的可视化效果如下图所示: Dijkstra 算法的时间复杂度为 $O \left(m + n^2\right)$,通过二叉堆的数据结构可以将时间复杂度优化至 $O \left(\left(m + n\right) \log n\right)$。 Dijkstra 算法虽然能够处理加权网络,但不能处理存在负权重的网络,需要利用 Floyd-Warshall 算法 3 进行求解。更多 Floyd-Warshall 算法的细节请参见之前的博客计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)。 最大流和最小割 对于连接给定顶点 $s$ 和 $t$ 的两条路径,若没有共享边,则这两条路径是边独立的;若除 $s$ 和 $t$ 外不共享任何其他顶点,则这两条路径是顶点独立的。顶点之间的边连通度和顶点连通度分别是顶点之间边独立路径数和顶点独立路径数。连通度是度量顶点之间连通鲁棒性的简单参数。假设一个网络是一个管线网络,其中每个管线的容量均为单位流量,那么边连通度等于从 $s$ 流向 $t$ 的最大流。 增广路径算法(Ford-Fulkerson Algorithm,FFA)是计算最大流最简单的算法。基本思想是:首先利用广度优先搜索算法找到一条从源 $s$ 到目标 $t$ 的路径。该步骤“消耗”了网络中的一些边,将这些边的容量填充满后,它们不再承载更多流量。之后在剩余边中找到从 $s$ 到 $t$ 的另一条路径,重复该过程直到找不到更多的路径为止。 但这还不是一个有效的算法,如下图中的 (a) 所示,如果在 $s$ 和 $t$ 之间运用广度优先搜索,可以发现黑色标记的路径。一旦这些边的容量被填充满,就不能在剩余边中找到从 $s$ 到 $t$ 的更多路径,但很明显,从 $s$ 到 $t$ 有两条边独立路径(上下各一条)。 解决该问题的一个简单修正方法是允许网络流量在一条边中能够同时在两个方向流动。更一般地,因为一条边容许承载的最大流是在任意方向的单位流量,那么一条边可以有多个单位流量,只要保证他们能够相互抵消,并且最终每条边承载不超过一个单位流量。 增广路径算法的实现利用了剩余图(residual graph),这是一个有向网络,该网络中的有向边连接原网络中相应的顶点对,并在指定方向承载一个或多个单位流量。例如上图中 (c) 和 (d) 就是对应 (a) 和 (b) 的流量状态的剩余图。算法的正确性在这里就不过多展开说明,该算法在计算两个顶点之间的最大流的平均时间复杂度为 $O \left(\left(m + n\right) m / n\right)$。 在图论中,去掉其中所有边使一张网络不再连通的边集为图的割,一张图上最小的割为最小割。通过对增广路径算法进行改动即可以寻找到边独立路径、最小边割集和顶点独立路径。 图划分和社团发现 图划分(graph partitioning)和社团发现(community detection)都是指根据网络中的边的连接模式,把网络顶点划分成群组、簇或社团。将网络顶点划分成群组后最常见的属性是,同一群组内部的顶点之间通过边紧密连接,而不同群组之间只有少数边。 图划分 最简单的图划分问题是把网络划分成两部分,有时也称其为图对分(graph bisection)。图对分是把一个网络中的顶点划分成为两个指定规模的非重叠群组,使得不同群组之间相互连接的边数最小。群组之间的边数称为割集规模(cut size)。 利用穷举搜索解决该问题是极为耗时的,通过启发式算法我们可以找到较好的网络划分。 Kernighan-Lin 算法 Kernighan-Lin 算法是由 Brian Kernighan 和 Shen Lin 在 1970 年提出的 4,是图对分问题中最简单、最知名的启发式算法之一,如下图所示。 先以任意方式将网络顶点按指定规模划分成两个群组,对于任何由分属不同群组的顶点 $i$ 和顶点 $j$ 组成的顶点对 $\left(i, j\right)$,交换 $i$ 和 $j$ 的位置,并计算交换前后两个群组之间割集规模的变化量。在所有顶点对中找到使割集规模减小最多的顶点对,或者若没有使割集规模减小的顶点对,则找到使割集规模增加最小的顶点对,交换这两个顶点。重复这个过程,同时保证网络中的每个顶点只能移动一次。 继续算法,每一步都交换最大程度减少或最小程度增加群组之间边数的顶点对,直到没有可以变换的顶点对,此时本轮算法停止。在完成所有交换后,检查网络在此过程中经过的每一个状态,然后选择割集规模最小的状态。最后,重复执行上述整个过程,每次始于上次发现的最优网络划分,直到割集规模不在出现改善。 Kernighan-Lin 算法的主要缺点是运算速度缓慢,采用一些技巧来改善算法也只能使时间复杂度降至 $O \left(n^3\right)$,因此该算法仅适用于有几百或几千个顶点的网络,而不适用于更大规模的网络。 谱划分 请先了解附录中的拉普拉斯算子和拉普拉斯矩阵等相关概念。 考虑具有 $n$ 个顶点 $m$ 条边的网络,将其划分为两个群组,称为群组 1 和群组 2。可以把该划分的割集规模,也就是两个群组之间的边数表示为: $$ \label{eq:r_1} R = \dfrac{1}{2} \sum_{i, j \text{ 属于不同群组}} A_{ij} $$ 对于每个网络划分,定义有参数 $s_i$ 组成的集合,集合中每个元素对应于一个顶点 $i$,则有: $$ s_i = \left\{\begin{array}{ll} +1 & \text{顶点 } i \text{ 在群组 1 中} \\ -1 & \text{顶点 } i \text{ 在群组 2 中} \end{array}\right. $$ 那么: $$ \dfrac{1}{2} \left(1 - s_i s_j\right) = \left\{\begin{array}{ll} 1 & \text{顶点 } i \text{ 和 } j \text{ 在不同的群组中} \\ 0 & \text{顶点 } i \text{ 和 } j \text{ 在相同的群组中} \end{array}\right. $$ 则式 \ref{eq:r_1} 可以改写为: $$ \begin{aligned} R & = \dfrac{1}{4} \sum_{ij} A_{ij} \left(1 - s_i s_j\right) \\ & = \dfrac{1}{4} \left(k_i \delta_{ij} - A_{ij}\right) s_i s_j \\ & = \dfrac{1}{4} \sum_{ij} L_{ij} s_i s_j \end{aligned} $$ 其中,$\delta_{ij}$ 是克罗内克函数,$L_{ij}$ 是图拉普拉斯矩阵的第 $ij$ 个元素。写成矩阵的形式有: $$ R = \dfrac{1}{4} \mathbf{s}^{\top} \mathbf{L} \mathbf{s} $$ 由于每个 $s_i$ 的取值只能是 $\left\{+1, -1\right\}$,所以在给定 $\mathbf{L}$ 时求解 $\mathbf{s}$ 使其割集规模最小时并不容易。具体求解方法的推导在此不再展开说明,最终谱划分算法的过程如下所示: 计算图拉普拉斯矩阵的第二小特征值 $\lambda_2$,称为网络的代数连通度(algebraic connectivity),及其对应的特征向量 $\mathbf{v}_2$。 按从大到小的顺序对特征向量的元素进行排序。 把前 $n_1$ 个最大元素对应的顶点放入群组 1,其余放入群组 2,计算割集规模。 把前 $n_1$ 个最小(注意:中文译本中有错误)元素对应的顶点放入群组 2,其余放入群组 1,并重新计算割集规模。 在两种网络划分中,选择割集规模较小的那个划分。 谱划分方法在稀疏网络上的时间复杂度为 $O \left(n^2\right)$,这比 Kernighan-Lin 算法时间复杂度少了一个因子 $n$,从而使该算法能应用于更大规模的网络。 社团发现 社团发现(社区发现,社群发现,Community Detection)的基本目的与图划分类似,即把网络分成几个节点点群组,并使节点群组之间的连接较少。主要的差别就是群组的数量和规模是不确定的。社团发现的算法分类和具体实现很多,本文仅介绍几个常用的算法,更多方法及其细节请参见如下开放资源: Community Detection in Graphs 5 Deep Learning for Community Detection: Progress, Challenges and Opportunities 6 复杂网络社团发现算法研究新进展 7 benedekrozemberczki/awesome-community-detection Fast Unfolding (Louvain) Fast Unfolding (Louvain) 8 是一种基于模块度的社团发现算法,通过模块度来衡量一个社团的紧密程度。算法包含两个阶段: 历遍网络中所有的节点,通过比较将节点给每个邻居社团带来的模块度变化,将这个节点加入到使模块度增加最大的社团中。 对于步骤 1 的结果,将属于同一个社团的节点合并成为一个大的节点,进而重型构造网络。新的节点之间边的权重是所包含的之前所有节点之间相连的边权重之和,然后重复步骤 1。 算法的两个步骤如下图所示: Label Propagation Algorithm (LPA) 标签传播算法(Label Propagation Algorithm,LPA)是一种基于半监督学习的社团发现算法。对于每个节点都有对应的标签(即节点所隶属的社团),在算法迭代过程中,节点根据其邻居节点更新自身的标签。更新的规则是选择邻居节点中最多的标签作为自身的标签。 标签传播的过程中,节点的标签更新方式分为同步更新和异步更新两种方式。同步更新是指对于节点 $x$,在第 $t$ 步时,根据其所有邻居节点在 $t - 1$ 步时的标签对其进行更新,即: $$ C_{x}(t)=f\left(C_{x_{1}}(t-1), C_{x_{2}}(t-1), \cdots, C_{x_{k}}(t-1)\right) $$ 同步更新对于一个二分或者近似二分的网络来说可能会出现标签震荡的现象。对于异步更新方式,更新公式为: $$ C_{x}(t)=f\left(C_{x_{i 1}}(t), \cdots, C_{x_{i m}}(t), C_{x_{i(m+1)}}(t-1), \cdots, C_{x_{i k}}(t-1)\right) $$ 其中,邻居节点 $x_{i1}, \cdots, x_{im}$ 的标签在第 $t$ 步时已经更新过,而 $x_{i(m+1)}, \cdots, x_{ik}$ 的标签还未更新。 附录 拉普拉斯算子(Laplace operator,Laplacian)是由欧式空间中的一个函数的梯度的散度给出的微分算子,通常写作 $\Delta$,$\nabla^2$ 或 $\nabla \cdot \nabla$。 梯度(gradient)是对多元导数的概括,函数沿着梯度的方向变化最快,变化率则为梯度的模。假设二元函数 $f \left(x, y\right)$ 在区域 $G$ 内具有一阶连续偏导数,点 $P \left(x, y\right) \in G$,则称向量: $$ \nabla f = \left(\dfrac{\partial f}{\partial x}, \dfrac{\partial f}{\partial y} \right) = \dfrac{\partial f}{\partial x} \mathbf{i} + \dfrac{\partial f}{\partial y} \mathbf{j} $$ 为函数 $f$ 在点 $P$ 处的梯度,其中 $\mathbf{i}$ 和 $\mathbf{j}$ 为单位向量,分别指向 $x$ 和 $y$ 坐标方向。 散度(divergence)将向量空间上的一个向量场对应到一个标量场上,记为 $\nabla \cdot$。散度的意义是场的有源性,当 $\nabla \cdot F > 0$ 时,表示该点是发源点;当 $\nabla \cdot F < 0$ 时,表示该点是汇聚点;当 $\nabla \cdot F = 0$ 时,表示该点无源,如下图所示。 拉普拉斯离散化后即为拉普拉斯矩阵(laplacian matrix),也称为调和矩阵(harmonic matrix)。离散化的拉普拉斯算子形式如下: $$ \begin{aligned} \Delta f & = \dfrac{\partial^2 f}{\partial x^2} + \dfrac{\partial^2 f}{\partial y^2} \\ & = f \left(x + 1, y\right) + f \left(x - 1, y\right) - 2 f \left(x, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 2 f \left(x, y\right) \\ & = f \left(x + 1, y\right) + f \left(x - 1, y\right) + f \left(x, y + 1\right) + f \left(x, y - 1\right) - 4 f \left(x, y\right) \end{aligned} $$ 从上述离散化后的拉普拉斯算子形式可以看出,拉普拉斯矩阵表示的是对矩阵进行微小扰动后获得的收益。 设图 $G$ 有 $n$ 个节点,节点的邻域为 $N$,图上的函数 $f = \left(f_1, f_2, \cdots, f_n\right)$,其中 $f_i$ 表示节点 $i$ 处的函数值。对 $i$ 进行扰动,其可能变为邻域内的任意一个节点 $j \in N_i$: $$ \Delta f_{i}=\sum_{j \in N_{i}}\left(f_{i}-f_{j}\right) $$ 设每一条边 $e_{ij}$ 的权重为 $w_{ij}$,$w_{ij} = 0$ 表示节点 $i$ 和节点 $j$ 不相邻,则有: $$ \begin{aligned} \Delta f_i & = \sum_{j \in N} w_{ij} \left(f_i - f_j\right) \\ & = \sum_{j \in N} w_{ij} f_i - \sum_{j \in N} w_{ij} f_i \\ & = d_i f_i - W_{i:} f \end{aligned} $$ 对于所有节点有: $$ \begin{aligned} \Delta f & = \left(\begin{array}{c} \Delta f_{1} \\ \vdots \\ \Delta f_{N} \end{array}\right)=\left(\begin{array}{c} d_{1} f_{1}-W_{1:} f \\ \vdots \\ d_{N} f_{N}-W_{N:} f \end{array}\right) \\ & = \left(\begin{array}{ccc} d_{1} & \cdots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \cdots & d_{N} \end{array}\right) f-\left(\begin{array}{c} W_{1:} \\ \vdots \\ W_{N:} \end{array}\right) f \\ & = diag \left(d_i\right) f - W f \\ & = \left(D - W\right) f \\ & = L f \end{aligned} $$ 令图 $G$ 的邻接矩阵为 $W$,度矩阵为 $D$,从上式可知拉普拉斯矩阵 $L = D - W$,其中: $$ L_{ij} = \left\{\begin{array}{ll} \deg \left(v_i\right) & \text{如果 } i = j \\ -1 & \text{如果 } i \neq j \text{ 且 } v_i \text{ 与 } v_j \text{ 相邻} \\ 0 & \text{其他情况} \end{array}\right. $$ 以下面的图为例: 邻接矩阵为: $$ \left(\begin{array}{llllll} 0 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 & 1 & 1 \\ 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \end{array}\right) $$ 度矩阵为: $$ \left(\begin{array}{cccccc} 2 & 0 & 0 & 0 & 0 & 0 \\ 0 & 3 & 0 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 & 0 & 0 \\ 0 & 0 & 0 & 3 & 0 & 0 \\ 0 & 0 & 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 \end{array}\right) $$ 拉普拉斯矩阵为: $$ \left(\begin{array}{rrrrrr} 2 & -1 & 0 & 0 & -1 & 0 \\ -1 & 3 & -1 & 0 & -1 & 0 \\ 0 & -1 & 2 & -1 & 0 & 0 \\ 0 & 0 & -1 & 3 & -1 & -1 \\ -1 & -1 & 0 & -1 & 3 & 0 \\ 0 & 0 & 0 & 1 & 0 & 1 \end{array}\right) $$ 开放资源 常用网络算法包 名称 语言 NetworkX Python graph-tool Python SNAP C++, Python NetworKit C++, Python igraph C, C++, Python, R lightgraphs Julia 不同扩展包之间的性能比较如下表所示 9: 数据集 算法 graph-tool igraph LightGraphs NetworKit NetworkX SNAP Amazon CC 0.08 0.22 0.07 0.09 2.22 0.31 Amazon k-core 0.08 0.15 0.04 0.15 3.63 0.37 Amazon loading 2.61 0.57 4.66 0.98 4.72 1.61 Amazon page rank 0.04 0.57 0.02 0.02 8.59 0.58 Amazon shortest path 0.03 0.05 0.01 0.04 1.37 0.12 Google CC 0.28 1.38 0.29 0.37 7.77 1.56 Google k-core 0.39 0.92 0.16 0.83 42.6 1.31 Google loading 11.02 3.87 16.75 4.38 19.24 7.56 Google page rank 0.36 2.42 0.06 0.1 33.5 2.31 Google shortest path 0.08 0.41 0.01 0.14 3.41 0.26 Pokec CC 1.83 3.96 1.5 1.75 61.74 9.75 Pokec k-core 3.6 5.99 0.95 5.05 296.26 6.91 Pokec loading 71.46 25.75 170.63 26.77 140.19 52.73 Pokec page rank 1.1 23.39 0.21 0.24 239.75 8.62 Pokec shortest path 0.48 0.6 0.05 0.56 5.65 2.3 常用网络可视化软件 软件 平台 Cytoscape Windows, macOS, Linux Gephi Windows, macOS, Linux Tulip Windows, macOS, Linux Pajek Windows 不同可视化软件之间的比较如下表所示 10: Cytoscape Tulip Gephi Pajek Scalability ★★ ★ ★★★ ★★★★ User friendliness ★★ ★★★★ ★★★ ★ Visual styles ★★★★ ★★ ★★★ ★ Edge bundling ★★★ ★★★★ ★★ - Relevance to biology ★★★★ ★★ ★★★ ★ Memory efficiency ★ ★★ ★★★ ★★★★ Clustering ★★★★ ★★★ ★ ★★ Manual node/edge editing ★★★ ★★★★ ★★★ ★ Layouts ★★★ ★★ ★★★★ ★ Network profiling ★★★★ ★★ ★★★ ★ File formats ★★ ★★★ ★★★★ ★ Plugins ★★★★ ★★ ★★★ ★ Stability ★★★ ★ ★★★★ ★★★ Speed ★★ ★ ★★★ ★★★★ Documentation ★★★★ ★ ★★ ★★★ 其中,★ 表示较弱、★★ 表示中等、★★★ 表示较好、★★★★ 表示优秀。 Newman, M. E. J. (2014) 网络科学引论. 电子工业出版社. ↩︎ https://zh.wikipedia.org/wiki/戴克斯特拉算法 ↩︎ https://zh.wikipedia.org/zh-hans/Floyd-Warshall算法 ↩︎ Kernighan, B. W., & Lin, S. (1970). An efficient heuristic procedure for partitioning graphs. The Bell system technical journal, 49(2), 291-307. ↩︎ Fortunato, S. (2010). Community detection in graphs. Physics reports, 486(3-5), 75-174. ↩︎ Liu, F., Xue, S., Wu, J., Zhou, C., Hu, W., Paris, C., … & Yu, P. S. (2020). Deep Learning for Community Detection: Progress, Challenges and Opportunities. arXiv preprint arXiv:2005.08225. ↩︎ 骆志刚, 丁凡, 蒋晓舟, & 石金龙. (2011). 复杂网络社团发现算法研究新进展. 国防科技大学学报, (1), 12. ↩︎ Blondel, V. D., Guillaume, J. L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of statistical mechanics: theory and experiment, 2008(10), P10008. ↩︎ Benchmark of popular graph/network packages v2 ↩︎ Pavlopoulos, G. A., Paez-Espino, D., Kyrpides, N. C., & Iliopoulos, I. (2017). Empirical comparison of visualization tools for larger-scale network analysis. Advances in bioinformatics, 2017. ↩︎

2020/12/12
articleCard.readMore

真实世界网络结构 (Structure of Real-World Network)

本文为《复杂网络系列》文章 1 分支 在无向网络中,一个典型的现象是很多网络都有一个分支,该分支占据了网络的绝大部分,而剩余部分则被划分为大量的小分支,这些小分支之间彼此并不相连。如下图所示: 一个网络通常不能有两个或更多占据网络大部分的大分支。如果将一个 $n$ 个顶点的网络分解为两个分支,每个分支约为 $\dfrac{1}{2} n$ 个顶点,则两个分支的顶点之间会有 $\dfrac{1}{4} n^2$ 个顶点对,这些顶点对有可能一个顶点在一个大分支中,而另一个顶点在另外一个大分支中。如果在任何一个顶点对之间有一条边,那么这两个分支就会合并为一个分支。 有向图中分支分为两种:弱连通分支和强连通分支。弱连通分支的定义与无向网络的分支定义类似,强连通分支是指网络顶点的一个最大子集,该子集中的顶点能够通过有向路径到达其余所有顶点,同时也能够通过有向路径从其余所有顶点到达。 每个连通分支拥有外向分支(即从强连通分支中的任意顶点出发,沿着有向路径能够到达的所有顶点的集合)和内向分支(即沿着有向路径能够到达强连通分支的所有顶点的集合)。利用**“领结”图**可以很好地刻画有向网络的总体情况,万维网的“领结”图如下所示: 小世界效应 小世界效应(small-world effect)是指对于大多数网络而言,网络顶点之间的测地距离都惊人的小,例如:六度分隔理论。网络的数学模型显示出网络测地路径长度的数量级通常与网络定点数 $n$ 成对数关系 ,即 $\log n$。 度分布 顶点的度是指连接到它的边的数量。度分布(degree distribution)$p_k$ 是指网络中节点度的概率分布,也可以理解为从网络中随机选择一个顶点,其度为 $k$ 的概率。度序列(degree sequence)是指所有顶点度的集合。 根据度 $k$ 描述出大型网络的度分布有着非常重要的作用,下图给出了 Internet 的度分布: 现实世界中,几乎所有网络的度分布都有类似的由度较大的核心顶点构成的尾部,统计上称为右偏(right-skewed)的。 幂律和无标度网络 以 Internet 为例,下图给出了度分布的一个有趣特征,下图使用了对数标度重新绘制了上图的直方图: 如上图所示,对数处理后,分布大致遵循一条直线。度分布 $p_k$ 的对数与度 $k$ 的对数之间具有线性函数关系: $$ \ln p_k = - \alpha \ln k + c $$ 对两侧同时做指数运算,有: $$ p_k = C k^{- \alpha} $$ 其中,$C = e^c$ 是一个常数。这种形式的分布,即按照 $k$ 的幂变化,称为幂律(power law)。在不同类型的网络中,幂律度分布是普遍存在的,常数 $\alpha$ 是幂律的指数,该值的典型取值区间为 $2 \leq \alpha \leq 3$。通常,度分布并非在整个区间都遵循幂律分布,当 $k$ 较小时,度分布并不是单调的。具有幂律度分布的网络也称为无标度网络(scale-free network)。 观察幂律分布的另外一种方式是构建累积分布函数,定义如下: $$ P_k = \sum_{k' = k}^{\infty} p_{k'} $$ 假设度分布 $p_k$ 在尾部服从幂律,确切地讲,对于某个 $k_{\min}$,当 $k \geq k_{\min}$ 时有 $p_k = C k^{- \alpha}$,则对于 $k \geq k_{\min}$,有: $$ P_{k}=C \sum_{k^{\prime}=k}^{\infty} k^{\prime-\alpha} \simeq C \int_{k}^{\infty} k^{\prime-\alpha} \mathrm{d} k^{\prime}=\frac{C}{\alpha-1} k^{-(\alpha-1)} $$ 这里通过积分来近似求和是合理的,因为当 $k$ 值较大时,幂律函数的变化率较小。所以,如果度分布 $p_k$ 服从幂律,那么 $p_k$ 的累积分布函数也服从幂律。 聚类系数 聚类系数是度量某个顶点的两个邻居顶点也互为邻居的平均概率。该测度计算值与随机条件下得到的期望值之间有较大的差异,这种巨大差异可能也显示出了真正发挥作用的社会效应。在合作网络中,与随机选择合作者相比,实际的合作网络中包含更多的三角形结构。这种现象背后有很多原因,其中一个原因可能是人们会介绍其合作者认识,而这些合作者两两之间也开始进行合作。 随着度的增加,局部聚类系数不断减少,这种现象的一个可能的解释是顶点分成紧密的群组或社团,同一个群组内部的顶点之间连接较多。在表现出此类行为的网络中,属于小型群组的顶点的度较小,因为这种群组的成员也相对较少,但在较大的群组中的顶点的度较大。同时,小型群组中的顶点的局部聚类系数较高。出现这种情况是因为将每个群组与网络的其余部分隔离开之后,每个群组大体上相当于一个小型网络,较小的网络会有更大的聚类系数。当对不同规模的网络取平均之后,会发现度小的顶点具有较高的聚类系数,如下图所示: Newman, M. E. J. (2014) 网络科学引论. 电子工业出版社. ↩︎

2020/11/28
articleCard.readMore

网络表示,测度和度量 (Network Representation, Measures & Metrics)

本文为《复杂网络系列》文章,本文内容主要参考自:《网络科学引论》 网络(network)也称为图(graph),是一个由多个顶点(vertex)及连接顶点的边(edge)组成的集合。在网络中,我们通常用 $n$ 表示顶点的数目,用 $m$ 表示边的数目。在大多数网络中两个顶点之间都只有一条边,极少数情况下,两个顶点之间有多条边,称之为重边(multiedge)。在极特殊情况下,还会存在连接到顶点自身的边,称之为自边(self-edge)。既没有自边也没有重边的图称之为简单网络(simple network)或简单图(simple graph),存在重边的网络称之为重图(multigraph)。相关概念示例如下: 网络表示 无向网络 对于一个包含 $n$ 个顶点的无向图,可以用整数 $1$ 到 $n$ 对各个顶点进行标注。如果用 $\left(i, j\right)$ 表示顶点 $i$ 和顶点 $j$ 之间的边,那么通过给定 $n$ 的值及所有边的列表就能表示一个完整的网络,这种表示方法称之为边列表(edge list)。 相比于边列表,邻接矩阵(adjacency matrix)可以更好地表示网络。一个简单图的邻接矩阵 $\mathbf{A}$ 中元素 $A_{ij}$ 的含义如下: $$ A_{ij}=\left\{\begin{array}{ll} 1 & \text{如果顶点 } i \text{ 和顶点 } j \text{ 之间存在一条边} \\ 0 & \text{其他} \end{array}\right. $$ 对于一个没有自边的网络,其邻接矩阵有两个特点: 邻接矩阵对角线上的元素取值均为零。 邻接矩阵是对称的。 加权网络 对于加权网络(weighted network)和赋值网络(valued network)可以将邻接矩阵中对应元素的值设定为相应的权重的方式来进行表示。 有向网络 有向网络(directed network)或有向图(directed graph)有时简称为 digraph,在这类网络中,每条边都有方向,从一个顶点指向另一个顶点,称之为有向边(directed edge)。 提示 有向网络的邻接矩阵中元素 $A_{ij} = 1$ 时表示存在从顶点 $j$ 到顶点 $i$ 的边。虽然表示方法有些出人意料,但在数据计算上会带来极大的方便。 超图 在某些类型的网络中,一些边会同时连接多个顶点。例如:创建一个社会网络,用来表示一个大规模社区中的各个家庭。每个家庭都可能会有两名或多名成员,因此表示这些家庭之间关系的做好方法就是使用一种广义边来同时连接多个顶点。这样的边称之为超边(hyperedge),含有超边的网络称之为超图(hypergraph)。下图 (a) 表示一个小型超图,其中超边用环的形式表示。 当一个网络中的顶点因为某种群组之间的关系被连接在一起时,可以使用超图来表示这个网络,在社会学中,这样的网络称之为隶属网络。对于超图,可于采用二分图的方式进行表示,通过引入 4 个新的顶点代表 4 个群组,在顶点及其所属群组之间通过边连接,如上图 (b) 所示。 二分网络 群组内成员之间的关系可以用超图中的超边表示,也可以等价地用更方便的二分图(bipartite network)表示。这种网络中有两类顶点,一类顶点代表原始顶点,另一类顶点则表示原始顶点所属的群组。 二分网络中,与邻接矩阵等价的是一个矩形矩阵,称之为关联矩阵(incidence matrix)。如果 $n$ 代表人数或网络中的成员数目,$g$ 是群组的数目,那么关联矩阵 $\mathbf{B}$ 是一个 $g \times n$ 的矩阵,其元素 $B_{ij}$ 的取值含义如下: $$ B_{ij}=\left\{\begin{array}{ll} 1 & \text{如果顶点 } j \text{ 属于群组 } i \\ 0 & \text{其他} \end{array}\right. $$ 研究统一类型顶点之间的直接联系可以通过对二分网络进行单模投影(one-mode projection),推导出同类顶点之间的直接联系,如下图所示。 树 树(tree)是连通的、无向的且不包含闭合循环的网络,如下图所示。 连通是指任意两个顶点之间都存在一条相互可达的路径。一个网络可能有两个或多个部分组成,每个部分相互之间不连通,如果任意单独的部分都为树,则称这个网络为森林(forest)。 由于树没有闭合循环,因此任意两个顶点之间有且只有一条相连的路径。如果一个树有 $n$ 个顶点,那么它有且仅有 $n - 1$ 条边。 度 图中顶点的度(degree)是指与其直接相连的边数目。将顶点 $i$ 的度表示为 $k_i$,对于有 $n$ 个顶点构成的无向图,可利用邻接矩阵将度表示为: $$ k_i = \sum_{j=1}^{n} A_{ij} $$ 在无向图中,每个边都有两端,如果一共有 $m$ 条边,那么就有 $2m$ 个边的端点。同时,边的端点数与所有顶点度的总和相等: $$ 2m = \sum_{j=1}^{n} k_i $$ 即 $$ m = \dfrac{1}{2} \sum_{i=1}^{n} k_i = \dfrac{1}{2} \sum_{ij} A_{ij} $$ 无向图中顶点度的均值 $c$ 为: $$ c = \dfrac{1}{n} \sum_{i=1}^{n} k_i $$ 综上可得: $$ c = \dfrac{2m}{n} $$ 在一个简单图中,可能的边数的最大值是 $\dbinom{n}{2} = \dfrac{1}{2} n \left(n - 1\right)$ 个。图的连通度(connectance)或密度(density)$\rho$ 是所有图中实际出现的边的数目与边数最大值之间的比值: $$ \rho = \dfrac{m}{\dbinom{n}{2}} = \dfrac{2m}{n \left(n - 1\right)} = \dfrac{c}{n - 1} $$ 在有向图中,每个顶点有两个度:入度(in-degree)是连接到该顶点的入边的数目,出度(out-degree)是出边数目。当从顶点 $j$ 到 $i$ 有一条边时,邻接矩阵中对应的元素 $A_{ij} = 1$,则入度和出度记为: $$ k_i^{\text{in}} = \sum_{j=1}^{n} A_{ij}, k_j^{\text{out}} = \sum_{i=1}^{n} A_{ij} $$ 在有向图中,边的数目 $m$ 等于入边的端点数总和,也等于出边的端点数总和,有: $$ m=\sum_{i=1}^{n} k_{i}^{\mathrm{in}}=\sum_{j=1}^{n} k_{j}^{\mathrm{out}}=\sum_{i j} A_{i j} $$ 每个有向图的入度的均值 $c_{\text{in}}$ 和出度的均值 $c_{\text{out}}$ 是相等的: $$ c_{\text {in }}=\frac{1}{n} \sum_{i=1}^{n} k_{i}^{\text {in }}=\frac{1}{n} \sum_{j=1}^{n} k_{j}^{\text {out }}=c_{\text {out }} $$ 简化后有: $$ c = \dfrac{m}{n} $$ 路径 网络中的路径是指由一组顶点构成的序列,序列中每两个连续顶点都通过网络中的边连接在一起,路径长度等于该路径经过的边的数目(而非顶点的数目)。从顶点 $j$ 到顶点 $i$ 存在长度为 $r$ 的路径总数为: $$ N_{ij}^{\left(r\right)} = \left[\mathbf{A}^r\right]_{ij} $$ 其中,$\left[\cdots\right]_{ij}$ 表示矩阵中的第 $i$ 行、第 $j$ 列的元素。 测地路径(geodesic path),简称为最短路径(shortest path),即两个顶点间不存在更短路径的路径。图的直径(diameter)是指图中任意一对相互连接的顶点之间的最长测地路径长度。欧拉路径(Eulerian path)是经过网络中的所有边且每条边只经过一次的路径。哈密顿路径(Hamiltonian path)是访问网络的所有顶点且每个顶点只访问一次的路径。 分支 如果一个网络中两个顶点之间不存在路径,则称这个网络是非连通(disconnected)的,如果网络中任意两个顶点之间都能找到一条路径,则称这个网络是连通(connected)的。 网络中的子群称为分支(component)。分支是网络中顶点的子集,该子集中任何两个顶点之间至少存在一条路径,在保证该性质的前提下,网络中其他顶点都不能被添加到这个子集中。在保证一个给定性质的前提下,不能再向它添加其他顶点,就称其为最大子集(maximal subset)。 连通度 如果两条路经除了起点和终点外,不共享其他任何顶点,那么这两条路径是顶点独立(vertex-independent)的。如果两条路径是顶点独立的,那么也是边独立的,反之则不成立。 两个顶点之间的独立路径数称为顶点之间的连通度(connectivity),如果明确考虑边还是顶点,则需利用边连通度(edge connectivity)及顶点连通度(vertex connectivity)的概念。 子图 令原图表示为 $G = \left(V, E\right)$,其中,$V$ 是图中所有顶点的集合,$E$ 是图中所有边的集合,有: 子图(subgraph):$G'$ 中所有顶点和边均包含于原图 $G$ 中,即 $E' \in E, V' \in V$。 生成子图(spanning subgraph):$G'$ 中顶点同原图 $G$ 相同,且 $E' \in E$。 导出子图(induced subgraph):$G'$ 中,$V' \in V$,同时对于 $V'$ 中任意一个顶点,只要在原图 $G$ 中有对应的边,则也应包含在 $E'$ 中。 Motif Motif 1 被定义为反复出现的重要连接模式。这些模式在真实的网络中要比随机网络中出现的更加频繁,如下图所示: Motif 的显著性定义为: $$ Z_i = \dfrac{N_i^{\text{real}} - \bar{N}_i^{\text{rand}}}{\text{std} \left(N_i^{\text{rand}}\right)} $$ 其中,$N_i^{\text{real}}$ 为模式在真实图中出现的次数,$N_i^{\text{rand}}$ 为模式在随机图中出现的次数。 Graphlets Graphlets 是对 Motif 的扩展,Motif 是从全局的角度发现模式,而 Graphlets 是从局部角度出发。Graphlets 是连接的非同构子图,这里要求子图为导出子图。下图展示了节点数为 2 至 5 的所有 Graphlets: 更多关于 Motif 和 Graphlets 的细节请参见 2 3 。 测度和度量 中心性 度中心性 中心性(centrality)是研究“网络中哪些顶点是最重要或最核心的?”这个问题的一个概念。网络中心性的最简单的测度是顶点的度,即与顶点相连的边的数量。有时为了强调度作为中心性测度的用途,在社会学中也称之为度中心性(degree centrality)。 特征向量中心性 度中心性可自然地扩展为特征向量中心性(eigenvector centrality)。可以将度中心性理解为给某顶点所有邻居顶点赋予一个“中心性值”,但并非所有连接顶点的值都是相同的。很多情况下,一个顶点会由于连接到一些本身很重要的点,而使自身的重要性得到提升,这就是特征向量中心性的本质。 对于每个顶点 $i$,假设其中心性为 $x_i$。对于所有 $i$,可以设其初始值 $x_i = 1$,利用该值可以计算出另一个更能体现中心性的值 $x'_i$,将 $x'_i$ 定义为 $i$ 所有邻居顶点的中心性之和: $$ x'_i = \sum_{j} A_{ij} x_j $$ 重复该过程可以得到更好的估计值,重复 $t$ 步后,中心性 $\mathbf{x} \left(t\right)$ 的计算公式如下: $$ \mathbf{x} \left(t\right) = \mathbf{A}^t \mathbf{x} \left(0\right) $$ 当 $t \to \infty$ 时,中心性向量的极限与邻接矩阵中的主特征向量成正比。因此,可以等价地认为中心性 $\mathbf{x}$ 满足: $$ \mathbf{A} \mathbf{x} = \kappa_1 \mathbf{x} $$ 其中,$\kappa_1$ 为矩阵 $\mathbf{A}$ 的特征值中的最大值。 特征向量中心性对于有向图和无向图都适用。在有向图中,邻接矩阵是非对称的,因此网络有两类特征向量,通常情况下我们选择右特征向量来定义中心性。因为在有向网络中,中心性主要是由指向顶点的顶点,而不是由顶点指向的顶点赋予的。 Katz 中心性 Katz 中心性解决了特征向量中心性中节点中心性可能为零的问题。通过为网络中每个顶点赋予少量的“免费”中心性,可以定义: $$ x_i = \alpha \sum_{j} A_{ij} x_j + \beta $$ 其中,$\alpha$ 和 $\beta$ 是正常数。使用矩阵表示可以写成: $$ \mathbf{x} = \alpha \mathbf{A} \mathbf{x} + \beta \mathbf{1} $$ 其中,$\mathbf{1}$ 代表向量 $\left(1, 1, 1, \cdots\right)$。重新整理有 $\mathbf{x} = \beta \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$,由于只关心相对值,通常可以设置 $\beta = 1$,则有: $$ \mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1} $$ PageRank Katz 中心性有一个不足,被一个 Katz 中心性较高的顶点指向的顶点具有较高的 Katz 中心性,但如果这个中心性较高的顶点指向大量顶点,那么这些大量被指向的顶点也会拥有较高的中心性,但这种估计并非总是恰当的。在新的中心性中,那些指向很多其他顶点的顶点,即使本身的中心性很高,但也只能传递给它指向的每个顶点少量的中心性,定义为: $$ x_{i}=\alpha \sum_{j} A_{i j} \frac{x_{j}}{k_{j}^{\text {out }}}+\beta $$ 其中,$k_j^{\text{out}}$ 为顶点的出度,当 $k_j^{\text{out}} = 0$ 时可以将其设定为任何一个非零值,都不会影响计算结果。利用矩阵的形式,可以表示为: $$ \mathbf{x}=\alpha \mathbf{AD}^{-1} \mathbf{x}+\beta \mathbf{1} $$ 其中,$\mathbf{D}$ 为对角矩阵,$D_{ii} = \max \left(k_j^{\text{out}}, 1\right)$。同之前一样,$\beta$ 只是整个公式的因子,设置 $\beta = 1$,有: $$ \mathbf{x}=\left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1} $$ 该中心性即为 PageRank。 上述 4 种中心性的区别和联系如下表所示: 带有常数项 不带常数项 除以出度 $\mathbf{x} = \left(\mathbf{I}-\alpha \mathbf{A} \mathbf{D}^{-1}\right)^{-1} \mathbf{1}$ PageRank $\mathbf{x} = \mathbf{A} \mathbf{D}^{-1} \mathbf{x}$ 度中心性 不除出度 $\mathbf{x} = \left(\mathbf{I} - \alpha \mathbf{A}\right)^{-1} \mathbf{1}$ Katz 中心性 $\mathbf{x} = \kappa_1^{-1} \mathbf{A} \mathbf{x}$ 特征向量中心性 接近度中心性 接近度中心性(closeness centrality)用于度量一个顶点到其他顶点的平均距离。 $$ C_{i}=\frac{1}{\ell_{i}}=\frac{n}{\sum_{j} d_{i j}} $$ 其中,$d_{i j}$ 表示从顶点 $i$ 到 $j$ 的测地路径长度,即路径中边的总数,$\ell_{i}$ 表示从 $i$ 到 $j$ 的平均测地距离。在大多数网络中,顶点之间的测地距离一般都较小,并且随着网络规模的增长,该值只是以对数级别速度缓慢增长。 在不同分支中的两个顶点之间的测地距离定义为无穷大,则 $C_i$ 为零。为了解决这个问题,最常见的方法是只计算同一分支内部的顶点的平均测地距离。新的定义使用顶点之间的调和平均测地距离: $$ C_{i}^{\prime}=\frac{1}{n-1} \sum_{j(\neq i)} \frac{1}{d_{i j}} $$ 公式中排除了 $j = i$ 的情况,因为 $d_{ii} = 0$。结果也称之为调和中心性(harmonic centrality)。 介数中心性 介数中心性(betweenness centrality)描述了一个顶点在其他顶点之间路径上的分布程度。假设在网络中每两个顶点之间,在每个单位时间内以相等的概率交换信息,信息总是沿着网络中最短测地路径传播,如果有多条最短测地路径则随机选择。由于消息是沿着最短路径以相同的速率传播,因此经过某个顶点的消息数与经过该顶点的测地路径数成正比。测地路径数就是所谓的介数中心性,简称介数。 定义 $n_{st}^i$ 为从 $s$ 到 $t$ 经过 $i$ 的测地路径数量,定义 $g_{st}$ 为从 $s$ 到 $t$ 的测地路径总数,那么顶点 $i$ 的介数中心性可以表示为: $$ x_{i}=\sum_{s t} \frac{n_{s t}^{i}}{g_{s t}} $$ 高介数中心性的顶点由于控制着其他顶点之间的消息传递,在网络中有着很强的影响力。删除介数最高的顶点,也最有可能破坏其他顶点之间的通信。 不同中心性的可视化如下图所示: By Tapiocozzo, CC BY-SA 4.0 其中,A:介数中心性;B:接近度中心性;C:特征向量中心性;D:度中心性;E:调和中心性;F:Katz 中心性。 传递性 传递性(transitivity)在社会网络中的重要性要比其他网络中重要得多。在数学上,对于关系“$\circ$”,如果 $a \circ b$ 和 $b \circ c$,若能推出 $a \circ c$,则称 $\circ$ 具有传递性。 完全传递性值出现在每一个分支都是全连通的子图或团的网络中。团(clique)是指无向图网络中的一个最大顶点子集,在该子集中任何两个顶点之间都有一条边直接连接。完全传递性没有太多的实际意义,而部分传递性却很有用。在很多网络中,$u$ 认识 $v$ 且 $v$ 认识 $w$,并不能保证 $u$ 认识 $w$,但两者之间相互认识的概率很大。 如果 $u$ 也认识 $w$,则称该路径是闭合的。在社会网络术语中,称 $u, v, w$ 这 3 个顶点形成一个闭合三元组(closed triad)。我们将聚类系数(clustering coefficient)定义为网络中所有长度为 2 的路径中闭合路径所占的比例: $$ C = \dfrac{\text{长度为 2 的路径中闭合路径数}}{\text{长度为 2 的路径数}} $$ 其取值范围在 0 到 1 之间。社会网络的聚类系数比其他网络偏高。 对于顶点 $i$,定地单个顶点的聚类系数为: $$ C_i = \dfrac{\text{顶点 i 的邻居顶点中直接相连的顶点对数}}{\text{顶点 i 的邻居顶点对总数}} $$ $C_i$ 也称为局部聚类系数(local clustering coefficient),该值代表了 $i$ 的朋友之间互为朋友的平均概率。 相互性 聚类系数观察的是长度为 3 的循环,长度为 2 的循环的频率通过相互性(reciprocity)来度量,该频率描述了两个顶点之间相互指向的概率。 相似性 社会网络分析的另一个核心概念是顶点之间的相似性。构造网络相似性的测度有两种基本方法:结构等价(structural equivalence)和规则等价(regular equivalence),如下图所示: 结构等价 针对无向网络中,最简单和最显而易见的结构等价测度就是计算两个顶点的共享邻居顶点数。在无向网络中,顶点 $i$ 和 $j$ 的共享邻居顶点数表示为 $n_{ij}$,有: $$ n_{ij} = \sum_{k} A_{ik} A_{kj} $$ 利用余弦相似度可以更好的对其进行度量。将邻接矩阵的第 $i$ 和第 $j$ 行分别看成两个向量,然后将这两个向量之间的夹角余弦值用于相似性度量,有: $$ \sigma_{i j}=\cos \theta=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{\sum_{k} A_{i k}^{2}} \sqrt{\Sigma_{k} A_{j k}^{2}}} $$ 假设网络是不带权重的简单图,上式可以化简为: $$ \sigma_{i j}=\frac{\sum_{k} A_{i k} A_{k j}}{\sqrt{k_{i}} \sqrt{k_{j}}}=\frac{n_{i j}}{\sqrt{k_{i} k_{j}}} $$ 其中,$k_i$ 是顶点 $i$ 的度。余弦相似度的取值范围为从 0 到 1,1 表示两个顶点之间拥有完全相同的邻居节点。 皮尔逊相关系数通过同随机选择邻居顶点条件下共享邻居顶点数的期望值进行比较的方式进行计算,得到的标准的皮尔逊相关系数为: $$ r_{i j}=\frac{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)\left(A_{j k}-\left\langle A_{j}\right\rangle\right)}{\sqrt{\sum_{k}\left(A_{i k}-\left\langle A_{i}\right\rangle\right)^{2}} \sqrt{\sum_{k}\left(A_{j k}-\left\langle A_{j}\right\rangle\right)^{2}}} $$ 上式的取值范围从 -1 到 1,数值越大表明两者之间越相似。 规则等价 规则等价的顶点不必共享邻居顶点,但是两个顶点的邻居顶点本身要具有相似性。一些简单的代数测度思想如下:定义一个相似性值 $\sigma_{ij}$,若顶点 $i$ 和 $j$ 各自的邻居顶点 $k$ 和 $l$ 本身具有较高的相似性,则 $i$ 和 $j$ 的相似性也较高。对于无向网络,有以下公式: $$ \sigma_{i j}=\alpha \sum_{k l} A_{i k} A_{j l} \sigma_{k l} $$ 或者利用矩阵性质表示为 $\mathbf{\sigma} = \alpha \mathbf{A \sigma A}$。 同质性 在社会网络中,人们倾向于选择那些他们认为与其自身在某些方面相似的人作为朋友,这种倾向性称为同质性(homophily)或同配混合(assortative mixing)。 依据枚举特征的同配混合 假设有一个网络,其顶点根据某个枚举特征(例如:国籍、种族、性别等)分类,且该特征的取值是一个有限集合。如果网络中连接相同类型顶点之间的边所占比例很大,那么该网络就是同配的。量化同配性简单的方法是观测这部分边占总边数的比例,但这并不是很好的度量方法,因为如果所有顶点都是同一个类型,那么测度值就是 1。 好的测度可以通过首先找出连接同类顶点的边所占的比例,然后减去在不考虑顶点类型时,随机连接的边中,连接两个同类顶点的边所占比例的期望值的方式得到。常用的测度为模块度(modularity): $$ Q=\frac{1}{2 m} \sum_{i j}\left(A_{i j}-\frac{k_{i} k_{j}}{2 m}\right) \delta_{g_{i} g_{i}} $$ 其中,$k_i$ 为顶点 $i$ 的度,$g_i$ 为顶点 $i$ 的类型,$m$ 为总边数,$\delta_{ij}$ 为克罗内克函数。该值严格小于 1,如果同类顶点之间边数的实际值大于随机条件下的期望值,则该值为正数,否则为负数,值为正说明该网络是同配混合的。 依据标量特征的同配混合 如果根据标量特征(例如:年龄、收入等)来度量网络中的同质性。由于该类特征具有确定的顺序,因此根据标量的数值,不仅可以指出两个顶点在什么情况下是完全相同的,也可以指出它们在真么情况下是近似相同的。 令 $x_i$ 为顶点 $i$ 的标量值,$\left(x_i, x_j\right)$ 为网络中每一条边 $\left(i, j\right)$ 的两个端点的值,利用协方差可以得到同配系数: $$ r=\frac{\sum_{i j}\left(A_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}}{\sum_{i j}\left(k_{i} \delta_{i j}-k_{i} k_{j} / 2 m\right) x_{i} x_{j}} $$ 该系数在全同配混合网络中取最大值 1,在全异配混合网络中取最小值 -1,值 0 意味着边两端的顶点值是非相关的。 依据度的同配混合 依据度的同配混合是依据标量特征的同配混合的一个特例。依据度的同配混合网络中,高度数顶点倾向于与其他高度数顶点相连,而低度数顶点倾向于与其他低度数顶点相连。 在同配网络中,度大的顶点倾向于聚集在一起的网络中,我们希望得到网络中这些度大的顶点构成的顶点块或核,它们周围是一些度小的顶点构成的低密度边缘(periphery)。这种核心/边缘结构(core/periphery structure)是社会网络的普遍特征。 上图 (a) 给出了一个小型的同配混合网络,其核心/边缘结构明显,上图 (b) 给出了一个小型异配混合网络,通常不具备核心/边缘结构,但顶点的分布更加均匀。 Milo, R., Shen-Orr, S., Itzkovitz, S., Kashtan, N., Chklovskii, D., & Alon, U. (2002). Network motifs: simple building blocks of complex networks. Science, 298(5594), 824-827. ↩︎ Jain, D., & Patgiri, R. (2019, April). Network Motifs: A Survey. In International Conference on Advances in Computing and Data Sciences (pp. 80-91). Springer, Singapore. ↩︎ Henderson, K., Gallagher, B., Eliassi-Rad, T., Tong, H., Basu, S., Akoglu, L., … & Li, L. (2012, August). Rolx: structural role extraction & mining in large graphs. In Proceedings of the 18th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1231-1239). ↩︎

2020/11/21
articleCard.readMore

文本相似度 (Text Similarity)

文本相似度是指衡量两个文本的相似程度,相似程度的评价有很多角度:单纯的字面相似度(例如:我和他 v.s. 我和她),语义的相似度(例如:爸爸 v.s. 父亲)和风格的相似度(例如:我喜欢你 v.s. 我好喜欢你耶)等等。 文本表示角度 统计模型 文本切分 在中文和拉丁语系中,文本的直观表示就存在一定的差异,拉丁语系中词与词之间存在天然的分隔符,而中文则没有。 I can eat glass, it doesn’t hurt me. 因此针对拉丁语系的文本切分相对中文容易许多。 N 元语法 N-gram (N 元语法) 是一种文本表示方法,指文中连续出现的 $n$ 个词语。N-gram 模型是基于 $n-1$ 阶马尔科夫链的一种概率语言模型,可以通过前 $n-1$ 个词对第 $n$ 个词进行预测。以 南京市长江大桥 为例,N-gram 的表示如下: 一元语法(unigram):南/京/市/长/江/大/桥 二元语法(bigram):南京/京市/市长/长江/江大/大桥 三元语法(trigram):南京市/京市长/市长江/长江大/江大桥 import re from nltk.util import ngrams s = '南京市长江大桥' tokens = re.sub(r'\s', '', s) list(ngrams(tokens, 1)) # [('南',), ('京',), ('市',), ('长',), ('江',), ('大',), ('桥',)] list(ngrams(tokens, 2)) # [('南', '京'), ('京', '市'), ('市', '长'), # ('长', '江'), ('江', '大'), ('大', '桥')] list(ngrams(tokens, 3, pad_left=True, pad_right=True, left_pad_symbol='<s>', right_pad_symbol='</s>')) # [('<s>', '<s>', '南'), # ('<s>', '南', '京'), # ('南', '京', '市'), # ('京', '市', '长'), # ('市', '长', '江'), # ('长', '江', '大'), # ('江', '大', '桥'), # ('大', '桥', '</s>'), # ('桥', '</s>', '</s>')] 分词 分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符,虽然英文也同样存在短语的划分问题,不过在词这一层上,中文比之英文要复杂得多、困难得多。 s = '南京市长江大桥' # jieba # https://github.com/fxsjy/jieba import jieba list(jieba.cut(s, cut_all=False)) # ['南京市', '长江大桥'] list(jieba.cut(s, cut_all=True)) # ['南京', '南京市', '京市', '市长', '长江', '长江大桥', '大桥'] list(jieba.cut_for_search(s)) # ['南京', '京市', '南京市', '长江', '大桥', '长江大桥'] # THULAC # https://github.com/thunlp/THULAC-Python import thulac thulac_ins = thulac.thulac() thulac_ins.cut(s) # [['南京市', 'ns'], ['长江', 'ns'], ['大桥', 'n']] # PKUSEG # https://github.com/lancopku/PKUSeg-python import pkuseg seg = pkuseg.pkuseg(postag=True) seg.cut(s) # [('南京市', 'ns'), ('长江', 'ns'), ('大桥', 'n')] # HanLP # https://github.com/hankcs/HanLP import hanlp tokenizer = hanlp.load('LARGE_ALBERT_BASE') tokenizer(s) # ['南京市', '长江', '大桥'] 主题模型 除了对文本进行切分将切分后结果全部用于表示文本外,还可以用部分字词表示一篇文档。主题模型(Topic Model)在机器学习和自然语言处理等领域是用来在一系列文档中发现抽象主题的一种统计模型。 直观来讲,如果一篇文章有一个中心思想,那么一些特定词语会更频繁的出现。比方说,如果一篇文章是在讲狗的,那“狗”和“骨头”等词出现的频率会高些。如果一篇文章是在讲猫的,那“猫”和“鱼”等词出现的频率会高些。而有些词例如“这个”、“和”大概在两篇文章中出现的频率会大致相等。但真实的情况是,一篇文章通常包含多种主题,而且每个主题所占比例各不相同。因此,如果一篇文章 10% 和猫有关,90% 和狗有关,那么和狗相关的关键字出现的次数大概会是和猫相关的关键字出现次数的 9 倍。 一个主题模型试图用数学框架来体现文档的这种特点。主题模型自动分析每个文档,统计文档内的词语,根据统计的信息来断定当前文档含有哪些主题,以及每个主题所占的比例各为多少 1。 TF-IDF TF-IDF 是 Term Frequency - Inverse Document Frequency 的缩写,即“词频-逆文本频率”。TF-IDF 可以用于评估一个字词在语料中的一篇文档中的重要程度,基本思想是如果某个字词在一篇文档中出现的频率较高,而在其他文档中出现频率较低,则认为这个字词更能够代表这篇文档。 形式化地,对于文档 $y$ 中的字词 $x$ 的 TF-IDF 重要程度可以表示为: $$ w_{x, y} = tf_{x, y} \times \log \left(\dfrac{N}{df_{x}}\right) $$ 其中,$tf_{x, y}$ 表示字词 $x$ 在文档 $y$ 中出现的频率,$df_x$ 为包含字词 $x$ 的文档数量,$N$ 为语料中文档的总数量。 以 14 万歌词语料 为例,通过 TF-IDF 计算周杰伦的《简单爱》中最重要的 3 个词为 ['睡着', '放开', '棒球']。 BM25 BM25 算法的全称为 Okapi BM25,是一种搜索引擎用于评估查询和文档之间相关程度的排序算法,其中 BM 是 Best Match 的缩写。 对于一个给定的查询 $Q$,包含的关键词为 $q_1, \cdots, q_n$,一个文档 $D$ 的 BM25 值定义为: $$ \operatorname{score}(D, Q)=\sum_{i=1}^{n} \operatorname{IDF}\left(q_{i}\right) \cdot \frac{f\left(q_{i}, D\right) \cdot\left(k_{1}+1\right)}{f\left(q_{i}, D\right)+k_{1} \cdot\left(1-b+b \cdot \frac{|D|}{\text { avgdl }}\right)} $$ 其中,$f\left(q_{i}, D\right)$ 表示 $q_i$ 在文档 $D$ 中的词频,$|D|$ 表示文档 $D$ 中的词数,$\text{avgdl}$ 表示语料中所有文档的平均长度。$k_1$ 和 $b$ 为自由参数,通常取值为 $k_1 \in \left[1.2, 2.0\right], b = 0.75$ 2。$\operatorname{IDF} \left(q_i\right)$ 表示词 $q_i$ 的逆文档频率,通常计算方式如下: $$ \operatorname{IDF}\left(q_{i}\right)=\ln \left(\frac{N-n\left(q_{i}\right)+0.5}{n\left(q_{i}\right)+0.5}+1\right) $$ 其中,$N$ 为语料中文档的总数量,$n \left(q_i\right)$ 表示包含 $q_i$ 的文档数量。 BM25 算法是对 TF-IDF 算法的优化,在词频的计算上,BM25 限制了文档 $D$ 中关键词 $q_i$ 的词频对评分的影响。为了防止词频过大,BM25 将这个值的上限设置为 $k_1 + 1$。 同时,BM25 还引入了平均文档长度 $\text{avgdl}$,不同的平均文档长度 $\text{avgdl}$ 对 TF 分值的影响如下图所示: TextRank TextRank 3 是基于 PageRank 4 算法的一种关键词提取算法。PageRank 最早是用于 Google 的网页排名,因此以公司创始人拉里·佩奇(Larry Page)的姓氏来命名。PageRank 的计算公式如下: $$ S\left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in I n\left(V_{i}\right)} \frac{1}{\left|O u t\left(V_{j}\right)\right|} S\left(V_{j}\right) $$ 其中,$V_i$ 表示任意一个网页,$V_j$ 表示链接到网页 $V_i$ 的网页,$S \left(V_i\right)$ 表示网页 $V_i$ 的 PageRank 值,$In \left(V_i\right)$ 表示网页 $V_i$ 所有的入链集合,$Out \left(V_j\right)$ 表示网页 $V_j$ 所有的出链集合,$|\cdot|$ 表示集合的大小,$d$ 为阻尼系数,是为了确保每个网页的 PageRank 值都大于 0。 TextRank 由 PageRank 改进而来,计算公式如下: $$ WS \left(V_{i}\right)=(1-d)+d * \sum_{V_{j} \in In\left(V_{i}\right)} \frac{w_{j i}}{\sum_{V_{k} \in Out\left(V_{j}\right)} w_{j k}} WS \left(V_{j}\right) $$ 相比于 PageRank 公式增加了权重项 $W_{ji}$,用来表示两个节点之间的边的权重。TextRank 提取关键词的算法流程如下: 将文本进行切分得到 $S_i = \left[t_{i1}, t_{i2}, \cdots, t_{in}\right]$。 将 $S_i$ 中大小为 $k$ 的滑动窗口中的词定义为共现关系,构建关键词图 $G = \left(V, E\right)$。 根据 TextRank 的计算公式对每个节点的值进行计算,直至收敛。 对节点的 TextRank 的值进行倒叙排序,获取前 $n$ 个词作为关键词。 LSA, PLSA, LDA & HDP 潜在语义分析(LSA, Latent Semantic Analysis)5 的核心思想是将文本的高维词空间映射到一个低维的向量空间,我们称之为隐含语义空间。降维可以通过奇异值分解(SVD)实现,令 $X$ 表示语料矩阵,元素 $\left(i, j\right)$ 表示词 $i$ 和文档 $j$ 的共现情况(例如:词频): $$ X = \mathbf{d}_{j} \cdot \mathbf{t}_{i}^{T} = \left[\begin{array}{c} x_{1, j} \\ \vdots \\ x_{i, j} \\ \vdots \\ x_{m, j} \end{array}\right] \cdot \left[\begin{array}{ccccc} x_{i, 1} & \ldots & x_{i, j} & \ldots & x_{i, n} \end{array}\right] = \left[\begin{array}{ccccc} x_{1,1} & \ldots & x_{1, j} & \ldots & x_{1, n} \\ \vdots & \ddots & \vdots & \ddots & \vdots \\ x_{i, 1} & \ldots & x_{i, j} & \ldots & x_{i, n} \\ \vdots & \ddots & \vdots & \ddots & \vdots \\ x_{m, 1} & \ldots & x_{m, j} & \ldots & x_{m, n} \end{array}\right] $$ 利用奇异值分解: $$ X = U \Sigma V^{T} $$ 取最大的 $K$ 个奇异值,则可以得到原始矩阵的近似矩阵: $$ \widetilde{X} =U \widetilde{\Sigma} V^{T} $$ 在处理一个新的文档时,可以利用下面的公式将原始的词空间映射到潜在语义空间: $$ \tilde{x} =\tilde{\Sigma} ^{-1} V^{T} x_{test} $$ LSA 的优点: 低维空间可以刻画同义词 无监督模型 降维可以减少噪声,使特征更加鲁棒 LSA 的缺点: 未解决多义词问题 计算复杂度高,增加新文档时需要重新训练 没有明确的物理解释 高斯分布假设不符合文本特征(词频不为负) 维度的确定是 Ad hoc 的 概率潜语义分析(Probabilistic Latent Semantic Analysis, PLSA)6 相比于 LSA 增加了概率模型,每个变量以及相应的概率分布和条件概率分布都有明确的物理解释。 PLSA 认为一篇文档可以由多个主题混合而成,而每个主题都是词上的概率分布,文章中的每个词都是由一个固定的主题生成的,如下图所示: 针对第 $m$ 篇文档 $d_m$ 中的每个词的生成概率为: $$ p\left(w \mid d_{m}\right)=\sum_{z=1}^{K} p(w \mid z) p\left(z \mid d_{m}\right)=\sum_{z=1}^{K} \varphi_{z w} \theta_{m z} $$ 因此整篇文档的生成概率为: $$ p\left(\vec{w} \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} p\left(w_{i} \mid z\right) p\left(z \mid d_{m}\right)=\prod_{i=1}^{n} \sum_{z=1}^{K} \varphi_{z w_{i}} \theta_{d z} $$ PLSA 可以利用 EM 算法求得局部最优解。 PLSA 优点: 定义了概率模型,有明确的物理解释 多项式分布假设更加符合文本特征 可以通过模型选择和复杂度控制来确定主题的维度 解决了同义词和多义词的问题 PLSA 缺点: 随着文本和词的增加,PLSA 模型参数也随之线性增加 可以生成语料中的文档的模型,但不能生成新文档的模型 EM 算法求解的计算量较大 隐含狄利克雷分布(Latent Dirichlet Allocation, LDA)7 在 PLSA 的基础上增加了参数的先验分布。在 PLSA 中,对于一个新文档,是无法获取 $p \left(d\right)$ 的,因此这个概率模型是不完备的。LDA 对于 $\vec{\theta}_m$ 和 $\vec{\phi}_k$ 都增加了多项式分布的共轭分布狄利克雷分布作为先验,整个 LDA 模型如下图所示: LDA 的参数估计可以通过吉布斯采样实现。PLSA 和 LDA 的更多细节请参见《LDA 数学八卦》8。 LDA 在使用过程中仍需要指定主题的个数,而层次狄利克雷过程(Hierarchical Dirichlet Processes, HDP)9 通过过程的构造可以自动训练出主题的个数,更多实现细节请参考论文。 LSA,PLSA,LDA 和 HDP 之间的演化关系如下图所示: 本节相关代码详见 这里。 距离度量 本节内容源自 相似性和距离度量 (Similarity & Distance Measurement)。 相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity),通常情况下我们可以利用一个距离函数定义集合 $X$ 上元素间的距离,即: $$ d: X \times X \to \mathbb{R} $$ Jaccard 系数 $$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$ Jaccard 系数的取值范围为:$\left[0, 1\right]$,0 表示两个集合没有重合,1 表示两个集合完全重合。 Dice 系数 $$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$ 与 Jaccard 系数相同,Dice 系数的取值范围为:$\left[0, 1\right]$,两者之间可以相互转换 $s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$。不同于 Jaccard 系数,Dice 系数的差异函数 $d = 1 - s$ 并不是一个合适的距离度量,因为其并不满足距离函数的三角不等式。 Tversky 系数 $$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$ 其中,$X \setminus Y$ 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化,当 $\alpha = \beta = 1$ 时为 Jaccard 系数,当 $\alpha = \beta = 0.5$ 时为 Dice 系数。 Levenshtein 距离 Levenshtein 距离是 编辑距离 (Editor Distance) 的一种,指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如将 kitten 转成 sitting,转换过程如下: $$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\ \to g\right) \end{split} \end{equation*} $$ 编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。 Jaro-Winkler 距离 对于给定的两个字符串 $s_1$ 和 $s_2$,Jaro 相似度定义为: $$ sim = \begin{cases} 0 & \text{if} \ m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) & \text{otherwise} \end{cases} $$ 其中,$\left|s_i\right|$ 为字符串 $s_i$ 的长度,$m$ 为匹配的字符的个数,$t$ 换位数目的一半。如果字符串 $s_1$ 和 $s_2$ 相差不超过 $\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$,我们则认为两个字符串是匹配的。例如,对于字符串 CRATE 和 TRACE,仅 R, A, E 三个字符是匹配的,因此 $m = 3$,尽管 C, T 均出现在两个字符串中,但是他们的距离超过了 1 (即,$\lfloor \dfrac{5}{2} \rfloor - 1$),因此 $t = 0$。 Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数,其定义为: $$ sim_w = sim_j + l p \left(1 - sim_j\right) $$ 其中,$sim_j$ 为字符串 $s_1$ 和 $s_2$ 的 Jaro 相似度,$l$ 为共同前缀的长度 (规定不超过 $4$),$p$ 为调整系数 (规定不超过 $0.25$),Winkler 将其设置为 $p = 0.1$。 汉明距离 汉明距离为两个等长字符串对应位置的不同字符的个数,也就是将一个字符串变换成另外一个字符串所需要替换的字符个数。例如:1011101 与 1001001 之间的汉明距离是 2,“toned” 与 “roses” 之间的汉明距离是 3。 import textdistance as td s1 = '南京市长江大桥' s2 = '北京市三元桥' td.jaccard(s1, s2) # 0.6666666666666666 td.sorensen_dice(s1, s2) # 0.46153846153846156 td.tversky(s1, s2) # 0.3 td.levenshtein(s1, s2) # 4 td.jaro(s1, s2) # 0.6428571428571429 td.hamming(s1, s2) # 5 表示学习 基于表示学习的文本相似度计算方法的思路如下: 利用表示学习方法将不定长的文本表示为定长的实值向量。 计算转换后的实值向量相似度,用于表示两个文本的相似度。 关于文本表示学习和实值向量相似度计算请参见之前博客:词向量 (Word Embeddings),相似性和距离度量 (Similarity & Distance Measurement),预训练自然语言模型 (Pre-trained Models for NLP)。 文本词法,句法和语义角度 本节主要参考自《基于词法、句法和语义的句子相似度计算方法》10。 一段文本的内容分析由浅及深可以分为词法,句法和语义三个层次。 词法,以词为对象,研究包括分词,词性和命名实体等。 句法,以句子为对象,研究包括句子成分和句子结构等。 语义,研究文字所表达的含义和蕴含的知识等。 词法和句法可以统一成为语法,如下图所示: 词法 词法层以单个句子作为输入,其输出为已标记(词性,命名实体等)的词汇序列。 词汇序列的相似度计算可以采用上文中的距离度量等方式实现。 句法 句法层用于研究句子各个组成部分及其排列顺序,将文本分解为句法单位,以理解句法元素的排列方式。句法层接收词法层分析后的将其转化为依存图。 对于依存图,我们可以利用三元组 $S = \left(V_1, E, V_2\right)$ 表示任意一个依存关系,然后通过统计计算两个文本的依存图的三元组集合之间的相似度来评价句法层的相似度。此外,也可以从树结构的角度直接评价依存句法的相似度,更多细节可参考相关论文 11 12。 语义 语义层用于研究文本所蕴含的意义。例如“父亲”和“爸爸”在词法层完全不同,但在语义层却具有相同的含义。针对语义相似度的两种深度学习范式如下: 第一种范式首先通过神经网络获取文本的向量表示,再通过向量之间的相似度来衡量文本的语义相似度。这种范式在提取特征时不考虑另一个文本的信息,更适合做大规模的语义相似召回,例如:DSSM 13,ARC-I 14,CNTN 15,LSTM-RNN 16 等。 第二种范式首先通过深度模型提取两个文本的交叉特征,得到匹配信号张量,再聚合为匹配分数。这种范式同时考虑两个文本的输入信息,更适合做小规模的语义相似精排,例如:ARC-II 14,MatchPyramid 17,Match-SRNN 18,Duet 19 等。 文本长度角度 从文本长度角度出发,我们可以粗略的将文本分类为短文本和长文本。短文本包括“字词”,“短语”,“句子”等相对比较短的文本形式,长文本包括“段落”,“篇章”等相对比较长的文本形式。 短文本 v.s. 短文本 短文本同短文本的常见比较形式有:关键词(字词)同文本标题(句子)的匹配,相似查询(句子)的匹配等。如果单纯的希望获取字符层面的差异,可以通过距离度量进行相似度比较。如果需要从语义的角度获取相似度,则可以利用表示学习对需要比对的文本进行表示,在通过语义向量之间的相似程度来衡量原始文本之间的相似度,详情可参见上文。 短文本 v.s. 长文本 短文本同长文本的比较多见于文档的搜索,即给定相关的查询(字词),给出最相关的文档(段落和篇章)。对于这类问题常见的解决方式是对长文本利用 TF-IDF,BM25等方法或进行主题建模后,再同查询的关键词进行匹配计算相似度度。 长文本 v.s. 长文本 长文本同长文本的比较多见于文档的匹配和去重,对于这类问题常见的解决方式是利用关键词提取获取长文本的特征向量,然后利用特征向量之间的相似度衡量对应文本的相似程度。在针对海量文本的去重,还以应用 SimHash 等技术对文本生成一个指纹,从而实现快速去重。 https://zh.wikipedia.org/wiki/主题模型 ↩︎ Manning, C. D., Schütze, H., & Raghavan, P. (2008). Introduction to information retrieval. Cambridge university press. ↩︎ Mihalcea, R., & Tarau, P. (2004, July). Textrank: Bringing order into text. In Proceedings of the 2004 conference on empirical methods in natural language processing (pp. 404-411). ↩︎ Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank citation ranking: Bringing order to the web. Stanford InfoLab. ↩︎ Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K., & Harshman, R. (1990). Indexing by latent semantic analysis. Journal of the American society for information science, 41(6), 391-407. ↩︎ Hofmann, T. (1999, August). Probabilistic latent semantic indexing. In Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval (pp. 50-57). ↩︎ Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). Latent dirichlet allocation. Journal of machine Learning research, 3(Jan), 993-1022. ↩︎ Rickjin(靳志辉). 2013. LDA数学八卦 ↩︎ Teh, Y. W., Jordan, M. I., Beal, M. J., & Blei, D. M. (2006). Hierarchical dirichlet processes. Journal of the american statistical association, 101(476), 1566-1581. ↩︎ 翟社平, 李兆兆, 段宏宇, 李婧, & 董迪迪. (2019). 基于词法, 句法和语义的句子相似度计算方法. 东南大学学报: 自然科学版, 49(6), 1094-1100. ↩︎ Zhang, K., & Shasha, D. (1989). Simple fast algorithms for the editing distance between trees and related problems. SIAM journal on computing, 18(6), 1245-1262. ↩︎ Meila, M., & Jordan, M. I. (2000). Learning with mixtures of trees. Journal of Machine Learning Research, 1(Oct), 1-48. ↩︎ Huang, P. S., He, X., Gao, J., Deng, L., Acero, A., & Heck, L. (2013, October). Learning deep structured semantic models for web search using clickthrough data. In Proceedings of the 22nd ACM international conference on Information & Knowledge Management (pp. 2333-2338). ↩︎ Hu, B., Lu, Z., Li, H., & Chen, Q. (2014). Convolutional neural network architectures for matching natural language sentences. In Advances in neural information processing systems (pp. 2042-2050). ↩︎ ↩︎ Qiu, X., & Huang, X. (2015, June). Convolutional neural tensor network architecture for community-based question answering. In Twenty-Fourth international joint conference on artificial intelligence. ↩︎ Palangi, H., Deng, L., Shen, Y., Gao, J., He, X., Chen, J., … & Ward, R. (2016). Deep sentence embedding using long short-term memory networks: Analysis and application to information retrieval. IEEE/ACM Transactions on Audio, Speech, and Language Processing, 24(4), 694-707. ↩︎ Pang, L., Lan, Y., Guo, J., Xu, J., Wan, S., & Cheng, X. (2016). Text matching as image recognition. In Proceedings of the Thirtieth AAAI Conference on Artificial Intelligence (AAAI'16). (pp. 2793–2799). ↩︎ Wan, S., Lan, Y., Xu, J., Guo, J., Pang, L., & Cheng, X. (2016, July). Match-SRNN: modeling the recursive matching structure with spatial RNN. In Proceedings of the Twenty-Fifth International Joint Conference on Artificial Intelligence (pp. 2922-2928). ↩︎ Mitra, B., Diaz, F., & Craswell, N. (2017, April). Learning to match using local and distributed representations of text for web search. In Proceedings of the 26th International Conference on World Wide Web (pp. 1291-1299). ↩︎

2020/10/31
articleCard.readMore

而立之前 (Life before 30)

没几个月就步入而立之年,古有云三十岁能自立于世,诚然不易。「立」,自当有成家立业之意,在当今社会,无论是客观因素还是主观因素,大家对成家立业的看法和行动步伐都略有差异,在此就不再发表愚见了。但我认为**「立身」**是我们都应该去思考和行动的事情,我们应该对于内在的自我和外部的社会有一个比较明确的认知,知道在社会中立足应该去做什么事、喜欢去做什么事、擅长去做什么事,然后不畏惧、不妥协、勇敢去做。 有时候会自嘲没有远大的志向和野心,但这样其实很真实,我认为平淡是人生的常态,脑海中记忆更深的才是平淡之外的苦难和幸福。虛泛的东西写多了容易变成鸡汤,简单整理几块内容省身克己,如于他人有益,实则万幸。 三观 总是在说三观,其实容易泛泛而谈,我认为三观会从很大程度上决定应该去做什么、喜欢去做什么,甚至会影响到擅长去做什么。 世界观 世界观是我们对于宇宙的根本看法,我认为这是进行沟通的一个基础,如果两个人在这个层面上就处在对立的位置,感觉无论沟通什么都会失去意义。世界观的两个根本对立即唯物主义和唯心主义,我自己不在任意一个极端,非要占个队,我应该会略微偏向唯物主义。毕竟鸡不打鸣太阳照常会升起,但如果我看不到,于我又有什么意义。 在我认识的人中,没有遇见太过极端的,更多的可能是受到民族、信仰、地域等因素的长期影响吧,或多或少都会有些差异。我认为只要不处在完全的对立面,一切都还是可以去思考、去沟通、去辩解的。 人生观 生而为赢(Born to Win)是高中年代看过的一篇文章,很鸡血,也着实影响了自己的求学年代。但随着环境的改变和自己人生的实践,对于人生态度、目的和意义的思考也在不断转变。现在来看,生而为赢依旧没错,拼搏和奋斗才能创造平淡之中的幸福。当前的我想要追求的是平淡之中不乏波澜,或好或坏,同时在为之努力的过程中也不要给别人添堵,说白了不能损人利己,损人不利己就更要唾弃了。 「认命」是我最近比较喜欢的一个词,也是最近更新了对其理解的词。其实“认命”并不消极,尽人事,听天命,积极做事,结果往往会受到多方面的影响,成则已,不成换个路子继续尽人事。人生观相比世界观可能更容易发生变化,虽然没有世界观那么基础,但我认为对待人生观也应该更多的从“功利主义”而非“个人主义”角度出发,如果从这个层面上就背离社会大多数认可的正道,那么就会容易走上邪路,危害他人,这不就真给人添堵了吗。 价值观 孰好孰坏、孰优孰劣真的不是一个很容易回答的问题。面对比较基础的问题,只要不违背世界观和人生观,还是能够比较明确的给出一个结论的,但细到很小很具体的问题上,每个人的见解就不同了。所以,对待大是大非、大善大恶同上面一样,我更倾向于功利主义,对于小事,我认为要做到有理有据、不自欺欺人,那么就无伤大雅。 价值观包含物质性和精神性两大种类,我想两者也不必割裂而谈,不必不食人间烟火,能与朋友大快朵颐,亦能与朋友谈天论地,岂不快哉? 做有意义的事,不给人添堵,什么有意义,需要自己去思考。 思考 要思考,但也别想太多,多思考做人做事自然是好的,但是天马行空的想太多,容易出现各种妄想症,我略受过其害。 思辨胜于对错 做算法久了,虽然越来越需要更深入的了解业务,但同业务掰扯的时候还是不多的,更多的是围绕抽象出来的问题在做事。做这些事情需要更多的是专业力,是非对错就是一个量化评估的过程,但真遇到一些方向上的问题,或是与人更相关的问题,对错的判断就显得不是那么容易。 即便将来一直沿着技术专家路线发展下去,我认为对上层问题的思考终将占据更重要的地位,而且也如实在这样发展。所以要有一套自己衡量评价事情的方法论,是非对错的评价标准因我们的世界观、人生观、价值观不同而有差异,但方法论还是能够有一定通用性的。方法论并不虚,就像我们做数据挖掘用的 CRISP-DM 一样,能够帮助我们更加客观、全面、系统的去做事。思考如何去评判,感觉比一味地争辩什么是对、什么是错更有意义。 永不停止 无论处于顺势还是逆势,都不要停止思考,顺势思考如何锦上添花,逆势思考如何雪中送炭。浑浑噩噩、自欺欺人、得过且过是最不可取的。读万卷书、行万里路、交万名友,成本最低的就是读万卷书,思考和读书类似,除了时间没什么太大的花销,就看你想不想。 生命不息,思考不止。 自由 自由有很多种,财务自由、意志自由,谈及大多数自由的时候都是我们所追求的。当然自由的首要前提正如《人权宣言》里面说的:「自由即有权做一切无害于他人的任何事情」。也就是自由并不是为所欲为,当然哪怕无害于他人,肆意挥霍、信口开河也是应该是被唾弃的。 读胡适的《容忍与自由》,感觉成事要「养成能够容忍谅解别人的见解的度量」,不要「以吾辈所主张这为绝对之是」。尤其进入到一个新的领域,要谦逊但不谦卑地去学习和理解,懂得尊重和敬畏即为容忍。 我是一个比较喜欢用数据说话的人,但有时也正是这种“所谓的有理有据”会让我“怼人”怼得理所当然。当想要让别人容忍谅解我们的见解,我们自己应该先做到这一点,凡事没有绝对,哪怕概率是一也不一定必然发生,数据说话是好事,只是不要被数字本身所蒙蔽就好。 我年纪越大,越感觉到容忍比自由更重要

2020/8/15
articleCard.readMore

最近邻搜索 (Nearest Neighbor Search)

最近邻搜索(Nearest Neighbor Search)是指在一个确定的距离度量和一个搜索空间内寻找与给定查询项距离最小的元素。更精确地,对于一个包含 $N$ 个元素的集合 $\mathcal{X} = \left\{\mathbf{x}_1, \mathbf{x}_2, \cdots, \mathbf{x}_n\right\}$,给定查询项 $\mathbf{q}$ 的最近邻 $NN \left(\mathbf{q}\right) = \arg\min_{\mathbf{x} \in \mathcal{X}} dist \left(\mathbf{q}, \mathbf{x}\right)$,其中 $dist \left(\mathbf{q}, \mathbf{x}\right)$ 为 $\mathbf{q}$ 和 $\mathbf{x}$ 之间的距离。由于维数灾难,我们很难在高维欧式空间中以较小的代价找到精确的最近邻。近似最近邻搜索(Approximate Nearest Neighbor Search)则是一种通过牺牲精度来换取时间和空间的方式从大量样本中获取最近邻的方法。 精确搜索 暴力查找(Brute-force Search) 最简单的最邻近搜索便是遍历整个点集,计算它们和目标点之间的距离,同时记录目前的最近点。这样的算法较为初级,可以为较小规模的点集所用,但是对于点集的尺寸和空间的维数稍大的情况则不适用。对于 $D$ 维的 $N$ 个样本而言,暴力查找方法的复杂度为 $O \left(DN\right)$。 k-D 树 k-D 树(k-Dimesion Tree)1 是一种可以高效处理 $k$ 维空间信息的数据结构。k-D 树具有二叉搜索树的形态,二叉搜索树上的每个结点都对应 $k$ 维空间内的一个点。其每个子树中的点都在一个 $k$ 维的超长方体内,这个超长方体内的所有点也都在这个子树中。k-D 树的构建过程如下: 若当前超长方体中只有一个点,返回这个点。 选择一个维度,将当前超长方体按照这个维度分割为两个超长方体。 选择一个切割点,将小于这个点的归入其中一个超长方体(左子树),其余归入另一个超长方体(右子树)。 递归地对分出的两个超长方体构建左右子树。 一个 $k = 2$ 的例子如下: 构建 k-D 树目前最优方法的时间复杂度为 $O \left(n \log n\right)$。对于单次查询,当 $2$ 维时,查询时间复杂度最优为 $O \left(\log n\right)$,最坏为 $O \left(\sqrt{n}\right)$,扩展至 $k$ 维,最坏为 $O \left(n^{1 - \frac{1}{k}}\right)$。k-D 树对于低维度最近邻搜索比较好,但当 $k$ 增长到很大时,搜索的效率就变得很低,这也是“维数灾难”的一种体现。 Ball 树 为了解决 k-D 树在高维数据上的问题,Ball 树 2 结构被提了出来。k-D 树是沿着笛卡尔积(坐标轴)方向迭代分割数据,而 Ball 树是通过一系列的超球体分割数据而非超长方体。Ball 树的构建过程如下: 若当前超球体中只有一个点,返回这个点。 定义所有点的质心为 $c$,离质心 $c$ 最远的点为 $c_1$,离 $c_1$ 最远的点为 $c_2$。 将 $c_1$ 和 $c_2$ 作为聚类中心对数据点进行聚类得到两个簇 $\left(c_1, r_1\right), \left(c_2, r_2\right)$,将其归入左子树和右子树,其中 $r$ 为超球的半径。 递归的对分出的两个超球体构建左右子树。 一个二维的例子如下: 每个点必须只能隶属于一个簇,但不同簇的超球体之间是可以相交的。在利用 Ball 树进行查询时,首先自上而下的找到包含查询点的叶子簇 $\left(c, r\right)$,在这个簇中找到距离查询点最近的观测点,这两个点的距离 $d_{upper}$ 即为最近邻的距离上界。之后检查该叶子簇的所有兄弟簇是否包含比这个上界更小的观测点,在检查时,如果查询节点距离兄弟簇圆心的距离大于兄弟簇的半径与之前计算的上界 $d_{upper}$ 之和,则这个兄弟节点不可能包含所需要的最近邻。 构建 Ball 树的时间复杂度为 $O \left(n \left(\log n\right)^2\right)$,查询时间复杂度为 $O \left(\log \left(n\right)\right)$。 近似搜索 基于哈希的算法 基于哈希的算法的目标是将一个高维数据点转换为哈希编码的表示方式,主要包含两类方法:局部敏感哈希(Local Sensitive Hash, LSH)和哈希学习(Learning to Hash, L2H)。 局部敏感哈希 局部敏感哈希采用的是与数据无关的哈希函数,也就是说整个学习处理过程不依赖于任何的数据内容信息。LSH 通过一个局部敏感哈希函数将相似的数据点以更高的概率映射到相同的哈希编码上去。这样我们在进行查询时就可以先找到查询样本落入那个哈希桶,然后再在这个哈希桶内进行遍历比较就可以找到最近邻了。 要使得相近的数据点通过哈希后落入相同的桶中,哈希函数需要满足如下条件: 如果 $d \left(x, y\right) \leq d_1$,则 $Pr \left[h \left(x\right), h \left(y\right)\right] \geq p_1$。 如果 $d \left(x, y\right) \geq d_2$,则 $Pr \left[h \left(x\right), h \left(y\right)\right] \leq p_2$。 其中,$x, y \in \mathbb{R}^n$ 表示 $n$ 维度数据点,$d \left(x, y\right)$ 表示 $x, y$ 之间的距离,$h$ 为哈希函数。满足上述两个条件的哈希函数称为是 $\left(d_1, d_2, p_1, p_2\right)$ 敏感的。 MinHash(Jaccard 距离) MinHash 算法的思路是:采用一种哈希函数将元素的位置均匀打乱,然后在新顺序下每个集合的第一个元素作为该集合的特征值。我们以 $s_1 = \left\{a, d\right\}$,$s_2 = \left\{c\right\}$,$s_3 = \left\{b, d, e\right\}$,$s_4 = \left\{a, c, d\right\}$ 为例,集合中可能的元素为 $\left\{a, b, c, d, e\right\}$,则这四个集合可以表示为: 元素 $s_1$ $s_2$ $s_3$ $s_4$ $a$ 1 0 0 1 $b$ 0 0 1 0 $c$ 0 1 0 1 $d$ 1 0 1 1 $e$ 0 0 1 0 对矩阵进行随机打乱后有: 元素 $s_1$ $s_2$ $s_3$ $s_4$ $b$ 0 0 1 0 $e$ 0 0 1 0 $a$ 1 0 0 1 $d$ 1 0 1 1 $c$ 0 1 0 1 我们利用每个集合的第一个元素作为该集合的特征值,则有 $h \left(s_1\right) = a$,$h \left(s_2\right) = c$,$h \left(s_3\right) = b$,$h \left(s_4\right) = a$,可以看出 $h \left(s_1\right) = h \left(s_4\right)$。MinHash 能够保证在哈希函数均匀分布的情况下,哈希值相等的概率等于两个集合的 Jaccard 相似度,即: $$ Pr \left(MinHash \left(s_1\right) = MinHash \left(s_2\right)\right) = Jaccard \left(s_1, s_2\right) $$ SimHash(汉明距离) SimHash 是由 Manku 等人 3 提出的一种用于用于进行网页去重的哈希算法。SimHash 作为局部敏感哈希算法的一种其主要思想是将高维特征映射到低维特征,再通过两个向量的汉明距离来确定是否存在重复或相似。算法步骤如下: 对文本进行特征抽取(例如:分词),并为每个特征赋予一定的权重(例如:词频)。 计算每个特征的二进制哈希值。 计算加权后的哈希值,当哈希值为 1 时,则对应位置为 $w_i$,否则为 $-w_i$,其中 $w_i$ 为该特征对应的权重。 将所有特征加权后的哈希值按对应的位置进行累加合并。 如果累加位置大于 0 则置为 1,否则置为 0,最终得到哈希结果。 算法流程如下图所示: 在得到 SimHash 的值后,我们可以通过比较之间的汉明距离来判断相似性。为了提高海量数据的去重效率,以 64 位指纹为例,我们可以将其切分为 4 份 16 位的数据块,根据鸽巢原理,汉明距离为 3 的两个文档必定有一个数据块是相等的。将这 4 分数据利用 KV 数据库和倒排索引进行存储,Key 为 16 位的截断指纹,Value 为剩余的指纹集合,从而提高查询的效率。同时可以选择 16,8 和 4 位进行索引,位数越小越精确,但所需的存储空间越大。 p-stable 分布(欧式距离) 当一个在 $\Re$ 上的分布 $\mathcal{D}$ 为 $p\text{-stable}$ 时,存在 $p \geq 0$ 使得对于任意 $n$ 个实数 $v_1, \cdots, v_n$ 和独立同分布 $\mathcal{D}$ 下的变量 $X_1, \cdots, X_n$,有随机变量 $\sum_{i}{v_i X_i}$ 和 $\left(\sum_{i}{\left|v_i\right|^p}\right)^{1/p} X$ 具有相同的分布,其中 $X$ 为分布 $\mathcal{D}$ 下的随机变量 4。常见的 p-stable 分布有: 柯西分布:密度函数为 $c \left(x\right) = \dfrac{1}{\pi} \dfrac{1}{1 + x^2}$,为 $1\text{-stable}$。 正态分布:密度函数为 $g \left(x\right) = \dfrac{1}{\sqrt{2 \pi}} e^{-x^2 / 2}$,为 $2\text{-stable}$。 p-stable 分布主要可以用于估计 $\left\|v\right\|_p$,对于两个相似的 $v_1, v_2$,它们应该具有更小的 $\left\|v_1 - v_2\right\|_p$,也就是对应的哈希值有更大的概率发生碰撞。对于 $v_1, v_2$,距离的映射 $a \cdot v_1 - a \cdot v_2$ 和 $\left\|v_1 - v_2\right\|_p \cdot X$ 具有相同的分布。$a \cdot v$ 将向量 $v$ 映射到实数集,如果将实轴以宽度 $w$ 进行等分,$a \cdot v$ 落在哪个区间中就将其编号赋予它,这样构造的哈希函数具有局部保持特性。构造的哈希函数族的形式为: $$ h_{a, b} \left(v\right) = \left\lfloor \dfrac{a \cdot v + b}{w} \right\rfloor $$ 其中,向量 $a$ 的元素 $a_i \sim N \left(0, 1\right)$,$b \sim U \left(0, w\right)$。令 $c = \left\|u - v \right\|_p$,则两个向量在被分配到一个桶中的概率为: $$ Pr \left[h_{a, b} \left(u\right) = h_{a, b} \left(v\right)\right] = \int_{0}^{w} \dfrac{1}{c} \cdot f_p \left(\dfrac{t}{u}\right) \left(1 - \dfrac{t}{w}\right) dt $$ 其中,$f_p$ 为概率密度函数。从上式中不难看出,随着距离 $c$ 的减小,两个向量发生碰撞的概率增加。 相关问题 局部敏感哈希可以在次线性时间内完成搜索,但缺点在于需要比较长的比特哈希码和比较多的哈希表才能达到预期的性能。 在单表哈希中,当哈希编码位数 $K$ 过小时,每个哈希桶中数据个数较多,从而会增加查询的响应时间。当哈希编码位数 $K$ 较大时,查询样本同最近邻落入同一个桶中的概率会很小。针对这个问题,我们可以通过重复 $L$ 次来增加最近邻的召回率。这个操作可以转化为构建 $L$ 个哈希表,给定一个查询样本,我们可以找到 $L$ 个哈希桶,然后再遍历这 $L$ 个哈希桶中的数据。但这样会增加内存的消耗,因此需要选择合理的 $K$ 和 $L$ 来获得更好的性能。 Multi-probe LSH 5 引入了一种新的策略解决召回的问题。Multi-probe LSH 不仅仅会遍历查询样本所在桶内的元素,同时还会查询一些其他有可能包含最近邻的桶,从而在避免构建多个哈希表的情况下增加召回率。 哈希学习 哈希学习(Learning to Hash)是由 Salakhutdinov 和 Hinton 6 引入到机器学习领域,通过机器学习机制将数据映射成二进制串的形式,能显著减少数据的存储和通信开销,从而有效提高学习系统的效率 7。从原空间中的特征表示直接学习得到二进制的哈希编码是一个 NP-Hard 问题。现在很多的哈希学习方法都采用两步学习策略: 先对原空间的样本采用度量学习(Metric Learning)进行降维,得到 1 个低维空间的实数向量表示。 对得到的实数向量进行量化(即离散化)得到二进制哈希码。 现有的方法对第二步的处理大多很简单,即通过某个阈值函数将实数转换成二进制位。通常使用的量化方法为 1 个阈值为 0 的符号函数,即如果向量中某个元素大于 0,则该元素被量化为 1,否则如果小于或等于 0,则该元素被量化为 0。 哈希学习相关的具体算法不再一一展开,更多细节请参见下文提供的相关 Survey。 矢量量化算法 矢量量化(Vector Quantization)是信息论中一种用于数据压缩的方法,其目的是减少表示空间的维度。一个量化器可以表示为由 $D$ 维向量 $x \in \mathbb{R}^D$ 到一个向量 $q \left(x\right) \in \mathcal{C} = \left\{c_i; i \in \mathcal{I}\right\}$ 的映射 $q$,其中下标集合 $\mathcal{I}$ 为有限集合,即 $\mathcal{I} = 0, \cdots, k-1$。$c_i$ 称之为形心(centroids),$\mathcal{C}$ 称之为大小为 $k$ 的码本(codebook)。映射后的向量到一个给定下标 $i$ 的集合 $\mathcal{V}_i \triangleq \left\{x \in \mathbb{R}^D: q \left(x\right) = c_i\right\}$(Voronoi),称之为一个单元(cell)。 以一个图像编码为例,我们通过 K-Means 算法得到 $k$ 个 centroids,然后用这些 centroids 的像素值来替换对应簇中所有点的像素值。当 $k = 2, 10, 100$ 时,压缩后的图像和原始图像的对比结果如下图所示: 当 $k = 100$ 时,压缩后的图像和原始图像已经非常接近了,相关代码请参见这里。 矢量量化以乘积量化(Product Quantization,PQ)最为典型,乘积量化的核心思想还是聚类,乘积量化生成码本和量化过程如下图所示: 在训练阶段,以维度为 128 的 $N$ 个样本为例,我们将其切分为 4 个子空间,则每个子空间的维度为 32 维。对每一个子空间利用 K-Means 对其进行聚类,令聚类个数为 256,这样每个子空间就能得到一个 256 大小的码本。样本的每个子段都可以用子空间的聚类中心来近似,对应的编码即为类中心的 ID。利用这种编码方式可以将样本用一个很短的编码进行表示,从而达到量化的目的。 在查询阶段,我们将查询样本分成相同的子段,然后在每个子空间中计算子段到该子空间中所有聚类中心的距离,这样我们就得到了 $4 \times 256$ 个距离。在计算某个样本到查询样本的距离时,我们仅需要从计算得到的 4 组距离中将对应编码的距离取出相加即可,所有距离计算完毕排序后即可得到结果。 乘积量化有两种计算距离的方式 8:对称距离和非对称距离,如下图所示: 对于 $x$ 和 $y$ 的距离 $d \left(x, y\right)$,对称距离利用 $d \left(q \left(x\right), q \left(y\right)\right)$ 进行估计,非对称距离利用 $d \left(x, q \left(y\right)\right)$ 进行估计。对称距离和非对称距离在不同阶段的时间复杂度如下表所示: 对称距离 非对称距离 编码 $x$ $k^* D$ 0 计算 $d \left(u_j \left(x\right), c_{j, i}\right)$ 0 $k^* D$ 对于 $y \in \mathcal{Y}$,计算 $\hat{d} \left(x, y\right)$ 或 $\tilde{d} \left(x, y\right)$ $nm$ $nm$ 查找最小 $k$ 个距离 $n + k$ $\log k \log \log n$ 其中,$k^*$ 为 centroids 个数,$D$ 为向量维度,$n$ 为样本个数,$m$ 为分割个数。通常情况下我们采用非对称距离,其更接近真实距离。 IVFADC 8 是乘积量化的的加速版本,乘积量化在计算距离时仍需逐个遍历相加计算。倒排乘积量化首先对 $N$ 个样本采用 K-Means 进行聚类,此处的聚类中心相比乘积量化应设置较小的数值。在得到聚类中心后,针对每一个样本 $x_i$ 找到距离最近的类中心 $c_i$,两者相减后得到残差 $x_i - c_i$,然后对残差再进行乘积量化的全过程。在查询阶段,通过先前较粗力度的量化快速定位隶属于哪一个 $c_i$,然后在 $c_i$ 区域利用乘积量化获取最终结果。整个流程如下图所示: Optimized Product Quantization (OPQ) 9 是乘积量化的一个优化方法。通常用于检索的原始特征维度较高,实践中利用乘积量化之前会对高维特征利用 PCA 等方法进行降维处理。这样在降低维度的时候还能够使得对向量进行子段切分的时候各个维度不相关。在利用 PCA 降维后,采用顺序切分子段仍存在一些问题,以 Iterative Quantization (ITQ) 10 中的一个二维平面例子来说明,如下图所示: 在利用乘积量化进行编码时,对于切分的各个子空间,应尽可能使得各个子空间的方差接近。上图中 $(a)$ 图在 x 和 y 轴上的方差较大,而 $(c)$ 图在两个方向上比较接近。OPQ 致力解决的问题就是对各个子空间方差上的均衡,OPQ 对于该问题的求解分为非参数求解方法和参数求解方法两种,更多算法细节请参见 ITQ 和 OPQ 原文。 基于图的算法 NSW(Navigable Small World)11 算法是一种由 Malkov 等人提出的基于图的索引的方法。我们将 Navigable Small World 网络表示为一个图 $G \left(V, E\right)$,其中数据集合 $X$ 的点被唯一映射到集合 $V$ 中的一条边,边集 $E$ 由构造算法确定。对于与一个节点 $v_i$ 共享一条边的所有节点,我们称之为该节点的“友集”。 之后我们可以利用一个贪婪搜索的变种算法实现一个基本的 KNN 搜索算法。通过选择友集中未被访问过的距离查询样本最近的节点,可以在图中一个接一个的访问不同的节点,直到达到停止准则。整个过程如下图所示: 上图中的边扮演着两种不同的角色: 短距离边的子集作为 Delaunay 图的近似用于贪婪搜索算法。 长距离边的子集用于对数尺度的贪婪搜索,负责构造图的 Navigable Small World 属性。 其中,黑色的边为短距离边,红色的边为长距离边,箭头为迭代查询路径。整个结构的构建可以通过元素的连续插入实现,对于新的元素,我们从当前结构中找到最接近的邻居集合与之相连。随着越来越多的元素插入到结构中,之前的短距离连接就变成了长距离连接。 NSW 的 KNN 查询过程如下所示: HNSW(Hierarchical Navigable Small World)12 是对 NSW 的一种改进。HNSW 的思想是根据连接的长度(距离)将连接划分为不同的层,然后就可以在多层图中进行搜索。在这种结构中,搜索从较长的连接(上层)开始,贪婪地遍历所有元素直到达到局部最小值,之后再切换到较短的连接(下层),然后重复该过程,如下图所示: 利用这种结构可以将原来 NSW 的多重对数(Polylogarithmic)计算复杂度降低至对数(Logarithmic)复杂度。更多关于数据插入和搜索的细节请参见原文。 NSG 13 提出了一种新的图结构 Monotonic Relative Neighborhood Graph (MRNG) 用于保证一个平均的低搜索时间复杂度(接近对数复杂度)。同时为了进一步降低索引复杂度,作者从确保连接性、降低平均出度、缩短搜索路径和降低索引大小 4 个方面考虑,提出了一个用于近似 MRNG 的 Spreading-out Graph (NSG)。 基于图的方法 HNSW 和基于乘积量化的方法 OPQ 之间的特性对比如下: 特点 OPQ HNSW 内存占用 小 大 召回率 较高 高 数据动态增删 灵活 不易 本文部分内容参考自 图像检索:向量索引。 算法对比 常用算法的开源实现的评测如下,更多评测结果请参见 erikbern/ann-benchmarks。 开放资源 Survey A Survey on Learning to Hash 14 A Survey on Nearest Neighbor Search Methods 15 An Investigation of Practical Approximate Nearest Neighbor Algorithms 16 Approximate Nearest Neighbor Search on High Dimensional Data - Experiments, Analyses, and Improvement 17 Binary Hashing for Approximate Nearest Neighbor Search on Big Data: A Survey 18 Hashing for Similarity Search: A Survey 19 开源库 库 API spotify/annoy C++, Python, Go vioshyvo/mrpt C++, Python, Go pixelogik/NearPy Python aaalgo/kgraph C++, Python nmslib/nmslib C++, Python nmslib/hnswlib C++, Python lyst/rpforest Python facebookresearch/faiss C++, Python ekzhu/datasketch Python lmcinnes/pynndescent Python yahoojapan/NGT C, C++, Python, Go, Ruby microsoft/SPTAG C++, Python puffinn/puffinn C++, Python kakao/n2 C++, Python, Go ZJULearning/nsg C++ 开源搜索引擎 搜索引擎 API milvus-io/milvus C, C++, Python, Java Go, Node.js, RESTful API vearch/vearch Python, Go 评测 https://github.com/erikbern/ann-benchmarks/ https://github.com/DBWangGroupUNSW/nns_benchmark Bentley, J. L. (1975). Multidimensional binary search trees used for associative searching. Communications of the ACM, 18(9), 509-517. ↩︎ Omohundro, S. M. (1989). Five balltree construction algorithms (pp. 1-22). Berkeley: International Computer Science Institute. ↩︎ Manku, G. S., Jain, A., & Das Sarma, A. (2007, May). Detecting near-duplicates for web crawling. In Proceedings of the 16th international conference on World Wide Web (pp. 141-150). ↩︎ Datar, M., Immorlica, N., Indyk, P., & Mirrokni, V. S. (2004, June). Locality-sensitive hashing scheme based on p-stable distributions. In Proceedings of the twentieth annual symposium on Computational geometry (pp. 253-262). ↩︎ Lv, Q., Josephson, W., Wang, Z., Charikar, M., & Li, K. (2007, September). Multi-probe LSH: efficient indexing for high-dimensional similarity search. In Proceedings of the 33rd international conference on Very large data bases (pp. 950-961). ↩︎ Salakhutdinov, Ruslan, and Geoffrey Hinton. “Semantic hashing.” International Journal of Approximate Reasoning 50.7 (2009): 969-978. ↩︎ 李武军, & 周志华. (2015). 大数据哈希学习: 现状与趋势. 科学通报, 60(5-6), 485-490. ↩︎ Jegou, H., Douze, M., & Schmid, C. (2010). Product quantization for nearest neighbor search. IEEE transactions on pattern analysis and machine intelligence, 33(1), 117-128. ↩︎ ↩︎ Ge, T., He, K., Ke, Q., & Sun, J. (2013). Optimized product quantization. IEEE transactions on pattern analysis and machine intelligence, 36(4), 744-755. ↩︎ Gong, Y., Lazebnik, S., Gordo, A., & Perronnin, F. (2012). Iterative quantization: A procrustean approach to learning binary codes for large-scale image retrieval. IEEE transactions on pattern analysis and machine intelligence, 35(12), 2916-2929. ↩︎ Malkov, Y., Ponomarenko, A., Logvinov, A., & Krylov, V. (2014). Approximate nearest neighbor algorithm based on navigable small world graphs. Information Systems, 45, 61-68. ↩︎ Malkov, Y. A., & Yashunin, D. A. (2018). Efficient and robust approximate nearest neighbor search using hierarchical navigable small world graphs. IEEE transactions on pattern analysis and machine intelligence. ↩︎ Fu, C., Xiang, C., Wang, C., & Cai, D. (2019). Fast approximate nearest neighbor search with the navigating spreading-out graph. Proceedings of the VLDB Endowment, 12(5), 461-474. ↩︎ Wang, J., Zhang, T., Sebe, N., & Shen, H. T. (2017). A survey on learning to hash. IEEE transactions on pattern analysis and machine intelligence, 40(4), 769-790. ↩︎ Reza, M., Ghahremani, B., & Naderi, H. (2014). A Survey on nearest neighbor search methods. International Journal of Computer Applications, 95(25), 39-52. ↩︎ Liu, T., Moore, A. W., Yang, K., & Gray, A. G. (2005). An investigation of practical approximate nearest neighbor algorithms. In Advances in neural information processing systems (pp. 825-832). ↩︎ Li, W., Zhang, Y., Sun, Y., Wang, W., Li, M., Zhang, W., & Lin, X. (2019). Approximate nearest neighbor search on high dimensional data-experiments, analyses, and improvement. IEEE Transactions on Knowledge and Data Engineering. ↩︎ Cao, Y., Qi, H., Zhou, W., Kato, J., Li, K., Liu, X., & Gui, J. (2017). Binary hashing for approximate nearest neighbor search on big data: A survey. IEEE Access, 6, 2039-2054. ↩︎ Wang, J., Shen, H. T., Song, J., & Ji, J. (2014). Hashing for similarity search: A survey. arXiv preprint arXiv:1408.2927. ↩︎

2020/8/1
articleCard.readMore

无模型策略预测和控制 - 时序差分学习 (Model-Free Policy Prediction and Control - Temporal Difference Learning)

本文为《强化学习系列》文章 1 2 3 时序差分预测 时序差分(Temporal Difference,TD)和蒙特卡洛方法都利用经验来解决预测问题。给定策略 $\pi$ 的一些经验,以及这些经验中的非终止状态 $S_t$,一个适用于非平稳环境的简单的每次访问型蒙特卡洛方法可以表示为: $$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[G_{t}-V\left(S_{t}\right)\right] \label{eq:mc-update} $$ 其中,$G_t$ 是时刻 $t$ 真实的回报,$\alpha$ 是步长参数,称之为常量 $\alpha$ MC。MC 需要等到一幕的结尾才能确定对 $V \left(S_t\right)$ 的增量(此时才能获得 $G_t$),而 TD 则只需要等到下一个时刻即可。在 $t+1$ 时刻,TD 使用观察到的收益 $R_{t+1}$ 和估计值 $V \left(S_{t+1}\right)$ 来进行一次有效更新: $$ V\left(S_{t}\right) \gets V\left(S_{t}\right)+\alpha\left[R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right] \label{eq:td-update} $$ 这种 TD 方法称之为 TD(0),算法的完整过程如下: TD(0) 的更新在某种程度上基于已存在的估计,我们称之为一种自举法。 TD 和 MC 方法都能渐进地收敛于正确的预测,但两种方法谁收敛的更快,目前暂时未能证明。但在如下的随机任务上,TD 方法通常比常量 $\alpha$ MC 方法收敛得更快。假设如下 MRP 所有阶段都同中心 C 开始,每个时刻以相同的概率向左或向右移动一个状态。幕终止于最左侧或最右侧,终止于最右侧时有 +1 的收益,除此之外收益均为零。 由于这个任务没有折扣,因此每个状态的真实价值是从这个状态开始并终止于最右侧的概率,即 A 到 E 的概率分别为 $\frac{1}{6}, \frac{2}{6}, \frac{3}{6}, \frac{4}{6}, \frac{5}{6}$。 上图左侧显示了在经历了不同数量的幕采样序列之后,运行一次 TD(0) 所得到的价值估计。在 100 幕后,估计值已经非常接近真实值了。上图右侧显示了不同的 $\alpha$ 情况下学习到的价值函数和真实价值函数的均方根(RMS)误差,对于所有的 $s$,近似价值函数都被初始化为中间值 $V \left(s\right) = 0.5$,显示的误差是 5 个状态上运行 100 次的平均误差。 给定近似价值函数 $V$,在访问非终止状态的每个时刻 $t$,使用式 $\ref{eq:mc-update}$ 和 $\ref{eq:td-update}$ 计算相应的增量经验,产生新的总增量,以此类推,直到价值函数收敛。我们称这种方法为批量更新,因为只有在处理了整批的训练数据后才进行更新。批量蒙特卡洛方法总是找出最小化训练集上均方误差的估计,而批量 TD(0) 总是找出完全符合马尔可夫过程模型的最大似然估计参数。因此,MC 在非马尔可夫环境中更加高效,而 TD 在马尔可夫环境中更加高效。 DP,MC 和 TD 的状态价值更新回溯过程如下图所示: $$ \textbf{DP} \quad V\left(S_{t}\right) \leftarrow \mathbb{E}_{\pi}\left[R_{t+1}+\gamma V\left(S_{t+1}\right)\right] $$ $$ \textbf{MC} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(G_{t}-V\left(S_{t}\right)\right) $$ $$ \textbf{TD} \quad V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) $$ 时序差分控制 利用时序差分方法解决控制问题,我们依然采用广义策略迭代(GPI),只是在评估和预测部分采用时序差分方法。同蒙特卡洛方法一样,我们需要在试探和开发之间做出权衡,因此方法又划分为同轨策略和离轨策略。 Sarsa:同轨策略下的时序差分控制 在同轨策略中,我们需要对所有状态 $s$ 以及动作 $a$ 估计出在当前的行动策略下所有对应的 $q_{\pi} \left(s, a\right)$。确保状态值在 TD(0) 下收敛的定理同样也适用于对应的动作值的算法上 $$ Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)-Q\left(S_{t}, A_{t}\right)\right] $$ 每当从非终止状态的 $S_t$ 出现一次转移之后,就进行上述的一次更新,如果 $S_{t+1}$ 是终止状态,那么 $Q \left(S_{t+1}, A_{t+1}\right)$ 则定义为 0。这个更新规则用到了描述这个事件的五元组 $\left(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1}\right)$,因此根据这五元组将这个算法命名为 Sarsa。Sarsa 控制算法的一般形式如下: Q-Learning:离轨策略下的时序差分控制 离轨策略下的时序差分控制算法被称为 Q-Learning,其定义为: $$ Q\left(S_{t}, A_{t}\right) \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \max_{a} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] $$ 在这里,待学习的动作价值函数 $Q$ 采用了对最优动作价值函数 $q_*$ 的直接近似作为学习目标,而与用于生成智能体决策序例轨迹的行动策略是什么无关。Q-Learning 算法的流程如下: 期望 Sarsa 如果将 Q-Learning 中对于下一个“状态-动作”二元组取最大值这一步换成取期望,即更新规则为: $$ \begin{aligned} Q\left(S_{t}, A_{t}\right) & \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \mathbb{E}_{\pi}\left[Q\left(S_{t+1}, A_{t+1}\right) \mid S_{t+1}\right]-Q\left(S_{t}, A_{t}\right)\right] \\ & \gets Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \sum_{a} \pi\left(a \mid S_{t+1}\right) Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] \end{aligned} $$ 给定下一个状态 $S_{t+1}$,这个算法确定地向期望意义上的 Sarsa 算法所决定的方向上移动,因此这个算法被称为期望 Sarsa。期望 Sarsa 在计算上比 Sarsa 更加复杂,但它消除了因为随机选择 $A_{t+1}$ 而产生的方差。 双学习 在上述算法中,在估计值的基础上进行最大化也可以被看做隐式地对最大值进行估计,而这会产生一个显著的正偏差。假设在状态 $s$ 下可以选择多个动作 $a$,这些动作在该状态下的真实价值 $q \left(s, a\right)$ 全为零,但他们的估计值 $Q \left(s, a\right)$ 是不确定的,可能大于零也可能小于零。真实值的最大值是零,但估计值的最大值是正数,因此就产生了正偏差,我们称其为最大化偏差。 我们用下例进行说明,在如下这个 MDP 中有两个非终止节点 A 和 B。每幕都从 A 开始并选择向左或向右的动作。向右则会立即转移到终止状态并得到值为 0 的收益和回报。向左则会是状态转移到 B,得到的收益也是 0。而在 B 这个状态下有很多种可能的动作,每种动作被选择后会立刻停止并得到一个从均值为 -0.1 方差为 1.0 的分布中采样得到的收益。 因此,任何一个以向左开始的轨迹的期望回报均为 -0.1,则在 A 这个状态中根本就不该选择向左。然而使用 $\epsilon-$ 贪心策略来选择动作的 Q-Learning 算法会在开始阶段非常明显地选择向左这个动作。即使在算法收敛到稳定时,它选择向左这个动作的概率也比最优值高了大约 5%,如下图所示: 解决该问题的一种方法为双学习。如果们将样本划分为两个集合,并用它们学习两个独立的对真实价值 $q \left(a\right), \forall a \in A$ 的估计 $Q_1 \left(a\right)$ 和 $Q_2 \left(a\right)$。则我们可以使用其中一个 $Q_1$ 来确认最大的动作 $A^* = \arg\max_a Q_1 \left(a\right)$,用另一个 $Q_2$ 来计算其价值的估计 $Q_2 \left(A^*\right) = Q_2 \left(\arg\max_a Q_1 \left(a \right)\right)$。由于 $\mathbb{E} \left[Q_2 \left(A^*\right)\right] = q \left(A^*\right)$,因此这个估计是无偏的。我们可以交换两个估计 $Q_1 \left(a\right)$ 和 $Q_2 \left(a\right)$ 的角色再执行一遍上面的过程,就可以得到另一个无偏的估计 $Q_1 \left(\arg\max_a Q_2 \left(a\right)\right)$。 双学习的 Q-Learning 版本为 Double Q-Learning。Double Q-Learning 在学习时会以 0.5 的概率进行如下更新: $$ Q_{1}\left(S_{t}, A_{t}\right) \leftarrow Q_{1}\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q_{2}\left(S_{t+1}, \underset{a}{\arg \max } Q_{1}\left(S_{t+1}, a\right)\right)-Q_{1}\left(S_{t}, A_{t}\right)\right] $$ 以 0.5 的概率交换 $Q_1$ 和 $Q_2$ 的角色进行同样的更新。使用 $\epsilon-$ 贪心策略的 Double Q-Learning 的完整算法流程如下: 0$ \STATE 对于所有 $s \in \mathcal{S}^+, a \in \mathcal{A} \left(s\right)$,任意初始化 $Q_1 \left(s, a\right), Q_2 \left(s, a\right)$,其中 $Q \left(\text{终止状态}, \cdot\right) = 0$ \FOR{每一幕} \STATE 初始化 $S$ \REPEAT \STATE 基于 $Q_1 + Q_2$ 使用 $\epsilon-$ 贪心策略在 $S$ 处选择 $A$ \STATE 执行动作 $A$,观测到 $R, S'$ \IF{$Random(0, 1] > 0.5$} \STATE $Q_{1}(S, A) \leftarrow Q_{1}(S, A)+\alpha\left(R+\gamma Q_{2}\left(S^{\prime}, \arg \max _{a} Q_{1}\left(S^{\prime}, a\right)\right)-Q_{1}(S, A)\right)$ \ELSE \STATE $Q_{2}(S, A) \leftarrow Q_{2}(S, A)+\alpha\left(R+\gamma Q_{1}\left(S^{\prime}, \arg \max _{a} Q_{2}\left(S^{\prime}, a\right)\right)-Q_{2}(S, A)\right)$ \ENDIF \STATE $S \gets S'$ \UNTIL{$S$ 为终止状态} \ENDFOR \end{algorithmic} \end{algorithm} Taxi-v3 示例 我们以 Taxi-v3 为示例来测试 Sarsa,Q-Learning 和 期望 Sarsa 三种不同的算法。Taxi-v3 包含了一个 5x5 的网格,即 25 个可能的位置,我们需要驾驶一辆出租车分别在图中的 R、G、Y、B 四个位置接送乘客。客人共计存在 4 种可能的上车点,4 种可能的下车点,同时考虑出租车的位置,整个环境共有 $5 \times 5 \times \left(4 + 1\right) \times 4 = 500$ 种可能的状态,如下图所示: 出租车需要根据当前环境采取不同的动作,共计 6 种可能的动作:向南走,向北走,向东走,向西走,接上乘客,放下乘客。由于环境中存在墙,出租车每次撞墙不会发生任何移动。每一步动作默认 -1 的回报,当选择错误的地点接上或放下乘客时获得 -10 的回报,在成功运送一个客人后获得 +20 的回报。 分别利用 Sarsa,Q-Learning 和 期望 Sarsa 三种不同的算法训练模型,我们以 100 幕作为窗口计算平均回报,前 1000 个平均回报的对比结果如下图所示: Taxi-v3 的成绩排行榜可参见 这里。利用训练好的模型执行预测的效果如下图所示: 本文示例代码实现请参见这里。 Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎ CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎ UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

2020/7/11
articleCard.readMore

无模型策略预测和控制 - 蒙特卡洛方法 (Model-Free Policy Prediction and Control - Monte-Carlo Learning)

本文为《强化学习系列》文章 1 2 3 蒙特卡洛算法仅需要经验,即从真实或者模拟的环境交互中采样得到的状态、动作、收益的序例。从真实经验中学习不需要关于环境动态变化规律的先验知识,却依然能够达到最优的行为;从模拟经验中学习尽管需要一个模型,但这个模型只需要能够生成状态转移的一些样本,而不需要像动态规划那样生成所有可能的转移概率分布。 蒙特卡洛预测 一个状态的价值是从该状态开始的期望回报,即未来的折扣收益累积值的期望。那么一个显而易见的方式是根据经验进行估计,即对所有经过这个状态之后产生的回报进行平均。随着越来越多的回报被观察到,平均值就会收敛到期望值,这就是蒙特卡洛算法的基本思想。 假设给定策略 $\pi$ 下途径状态 $s$ 的多幕数据,我们需要估计策略 $\pi$ 下状态 $s$ 的价值函数 $v_{\pi} \left(s\right)$。在同一幕中,$s$ 可能多次被访问,因此蒙特卡洛方法分为首次访问型 MC 算法和每次访问型 MC 算法,两者的区别在于更新时是否校验 $S_t$ 已经在当前幕中出现过。以首次访问型 MC 预测算法为例,算法流程如下: 以二十一点游戏为例,每一局可以看作一幕,胜、负、平分别获得收益 $+1, -1, 0$。每局游戏进行中的收益都为 $0$,并且不打折扣($\gamma = 1$),最终的收益即为整个游戏的回报。玩家的动作为要牌(Hit)或停牌(Stand),状态则取决于玩家的牌和庄家显示的牌。假设所有牌来自无穷多的一组牌(即每次取出的牌都会再放回牌堆)。如果玩家手上有一张 A,可以视作 11 而不爆掉,那么称这张 A 为可用的,此时这张牌总会被视作 11。因此,玩家做出的选择只会依赖于三个变量:他手牌的总和(12-31),庄家显示的牌(A-10),以及他是否有可用的 A,共计 200 个状态。 考虑如下策略,玩家在手牌点数之和小于 20 时均要牌,否则停牌。通过该策略多次模型二十一点游戏,并且计算每一个状态的回报的平均值。模拟结果如下: 有可用 A 的状态的估计会更不确定、不规律,因为这样的状态更加罕见。无论哪种情况,在大于约 500000 局游戏后,价值函数都能很好地近似。 蒙特卡洛控制 如果无法得到环境的模型,那么计算动作的价值(“状态-动作”二元组的价值)比计算状态的价值更加有用。动作价值函数的策略评估的目标是估计 $q_{\pi} \left(s, a\right)$,即在策略 $\pi$ 下从状态 $s$ 采取动作 $a$ 的期望回报。只需将对状态的访问改为对“状态-动作”二元组的访问,蒙特卡洛算法就可以几乎和之前完全相同的方式解决该问题,唯一复杂之处在于一些“状态-动作”二元组可能永远不会被访问到。为了实现基于动作价值函数的策略评估,我们必须保证持续的试探。一种方式是将指定的“状态-动作”二元组作为起点开始一幕采样,同时保证所有“状态-动作”二元组都有非零的概率可以被选为起点。这样就保证了在采样的幕个数趋于无穷时,每一个“状态-动作”二元组都会被访问到无数次。我们把这种假设称为试探性出发。 策略改进的方法是在当前价值函数上贪心地选择动作。由于我们有动作价值函数,所以在贪心的时候完全不需要使用任何的模型信息。对于任意的一个动作价值函数 $q$,对应的贪心策略为:对于任意一个状态 $s \in \mathcal{S}$,必定选择对应动作价值函数最大的动作: $$ \pi \left(s\right) = \arg\max_a q \left(s, a\right) $$ 策略改进可以通过将 $q_{\pi_k}$ 对应的贪心策略作为 $\pi_{k+1}$ 来进行。这样的 $\pi_k$ 和 $\pi_{k+1}$ 满足策略改进定理,因为对于所有的状态 $s \in \mathcal{S}$: $$ \begin{aligned} q_{\pi_{k}}\left(s, \pi_{k+1}(s)\right) &=q_{\pi_{k}}\left(s, \underset{a}{\arg \max } q_{\pi_{k}}(s, a)\right) \\ &=\max _{a} q_{\pi_{k}}(s, a) \\ & \geqslant q_{\pi_{k}}\left(s, \pi_{k}(s)\right) \\ & \geqslant v_{\pi_{k}}(s) \end{aligned} $$ 对于蒙特卡洛策略迭代,可以逐幕交替进行评估与改进。每一幕结束后,使用观测到的回报进行策略评估,然后在该幕序列访问到的每一个状态上进行策略改进。使用这个思路的一个简单算法称为基于试探性出发的蒙特卡洛(蒙特卡洛 ES),算法流程如下: 0$ \STATE 从 $S_0, A_0$ 开始根据 $\pi$ 生成一幕序列 $S_0, A_0, R_1, \cdots, S_{T-1}, A_{T-1}, R_T$ \STATE $G \gets 0$ \FOR{$t \in T-1, T-2, \cdots, 0$} \STATE $G \gets \gamma G + R_{t+1}$ \IF{$S_t, A_t$ 在 $S_0, A_0, S_1, A_1, \cdots, S_{t-1}, A_{t-1}$ 中出现过} \STATE $Returns \left(S_t, A_t\right) \gets Resurn \left(S_t, A_t\right) \cup G$ \STATE $Q \left(S_t, A_t\right) \gets avg \left(Returns \left(S_t, A_t\right)\right)$ \STATE $\pi \left(S_t\right) \gets \arg\max_a Q \left(S_t, a\right)$ \ENDIF \ENDFOR \ENDWHILE \end{algorithmic} \end{algorithm} 利用蒙特卡洛 ES 可以很直接地解决二十一点游戏,只需随机等概率选择庄家的扑克牌、玩家手牌的点数,以及确定是否有可用的 A 即可。令只在 20 或 21 点停牌为初始策略,初始动作价值函数全部为零,下图展示了蒙特卡洛 ES 得出的最优策略: 同轨策略和离轨策略 同轨策略 为了避免很难被满足的试探性出发假设,一般性的解法是智能体能够持续不断地选择所有可能的动作,有两种方法可以保证这一点,同轨策略(on-policy)和离轨策略(off-policy)。在同轨策略中,用于生成采样数据序列的策略和用于实际决策的待评估和改进的策略是相同的;而在离轨策略中,用于评估或改进的策略与生成采样数据的策略是不同的,即生成的数据“离开”了待优化的策略所决定的决策序列轨迹。 在同轨策略中,策略一般是“软性”的,即对于任意 $s \in \mathcal{S}$ 以及 $a \in \mathcal{A} \left(s\right)$,都有 $\pi \left(a | s\right) > 0$,但他们会逐渐地逼近一个确定性的策略。$\epsilon-$ 贪心策略是指在绝大多数时候都采取获得最大估计值的动作价值函数对应的动作,但同时以一个较小的概率 $\epsilon$ 随机选择一个动作。因此对于所有非贪心的动作都以 $\frac{\epsilon}{|\mathcal{A} \left(s\right)|}$ 的概率被选中,贪心动作则以 $1 - \epsilon + \frac{\epsilon}{|\mathcal{A} \left(s\right)|}$ 的概率被选中。同轨策略的蒙特卡洛控制 离轨策略 所有的学习控制方法都面临一个困境:它们希望学到的动作可以使随后的智能体行为是最优的,但是为了搜索所有的动作(以保证找到最优动作),它们需要采取非最优的行动。同轨策略采用一种妥协的方法,它并不学习最优策略的动作值,而是学习一个接近最优而且仍能进行试探的策略的动作值。一个更加直接的方法是采用两个策略,一个用来学习并成为最优策略,另一个更加有试探性,用来产生智能体的行动样本。用来学习的策略被称为目标策略,用于生成行动样本的策略被称为行动策略。在这种情况下,我们认为学习所用的数据“离开”了待学习的目标策略,因此整个过程称为离轨策略学习。 几乎所有的离轨策略方法都采用了重要度采样,重要度采样是一种在给定来自其他分布的样本的条件下,估计某种分布的期望值的通用方法。在离轨策略学习中,对回报值根据其轨迹在目标策略与行动策略中出现的相对概率进行加权,这个相对概率称为重要度采样比。给定起始状态 $S_t$,后续的“状态-动作”轨迹 $A_t, S_{t+1}, A_{t+1}, \cdots, S_T$ 在策略 $\pi$ 下发生的概率为: $$ \begin{aligned} \operatorname{Pr}\left\{A_{t},\right.&\left.S_{t+1}, A_{t+1}, \ldots, S_{T} \mid S_{t}, A_{t: T-1} \sim \pi\right\} \\ &=\pi\left(A_{t} \mid S_{t}\right) p\left(S_{t+1} \mid S_{t}, A_{t}\right) \pi\left(A_{t+1} \mid S_{t+1}\right) \cdots p\left(S_{T} \mid S_{T-1}, A_{T-1}\right) \\ &=\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right) \end{aligned} $$ 其中,$p$ 为状态转移概率函数。因此,在目标策略和行动策略轨迹下的相对概率(重要度采样比)为: $$ \rho_{t: T-1} \doteq \frac{\prod_{k=t}^{T-1} \pi\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}{\prod_{k=t}^{T-1} b\left(A_{k} \mid S_{k}\right) p\left(S_{k+1} \mid S_{k}, A_{k}\right)}=\prod_{k=t}^{T-1} \frac{\pi\left(A_{k} \mid S_{k}\right)}{b\left(A_{k} \mid S_{k}\right)} $$ 化简后,重要度采样比只与两个策略和样本序列数据相关,而与 MDP 的动态特性(状态转移概率)无关。我们希望估计目标策略下的期望回报(价值),但我们只有行动策略中的回报 $G_t$。直接使用行动策略中的回报进行估计是不准的,因此需要使用重要度采样比调整回报从而得到正确的期望值: $$ \mathbb{E}\left[\rho_{t: T-1} G_{t} \mid S_{t}=s\right]=v_{\pi}(s) $$ 定义所有访问过状态 $s$ 的时刻集合为 $\mathcal{T} \left(s\right)$,$T \left(t\right)$ 表示时刻 $t$ 后的首次终止,用 $G_t$ 表示在 $t$ 之后到达 $T \left(t\right)$ 时的回报值。则 $\left\{G_t\right\}_{t \in \mathcal{T} \left(s\right)}$ 就是状态 $s$ 对应的回报值,$\left\{\rho_{t:T \left(t\right) - 1}\right\}_{t \in \mathcal{T} \left(s\right)}$ 是相应的重要度采样比。则为了预测 $v_{\pi} \left(s\right)$,有: $$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{|\mathcal{T}(s)|} \label{eq:ordinary-importance-sampling} $$ 为一种简单平均实现的重要度采样,称之为普通重要度采样。 $$ V(s) \doteq \frac{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1} G_{t}}{\sum_{t \in \mathcal{T}(s)} \rho_{t: T(t)-1}} \label{eq:weighted-importance-sampling} $$ 为一种加权的重要度采样,称之为加权重要度采样,如果分母为零,则式 $\ref{eq:weighted-importance-sampling}$ 的值也为零。式 $\ref{eq:ordinary-importance-sampling}$ 得到的结果在期望上是 $v_{\pi} \left(s\right)$ 的无偏估计,但其值可能变得很极端,式 $\ref{eq:weighted-importance-sampling}$ 的估计是有偏的,但其估计的方差可以收敛到 0。 我们对二十一点游戏的状态值进行离轨策略估计。评估的状态为玩家有一张 A,一张 2(或者等价情况,有三张 A),从这个状态开始等概率选择要牌或停牌得到采样数据,目标策略只在和达到 20 或 21 时停牌。 在目标策略中,这个状态的值大概为 -0.27726(利用目标策略独立生成 1 亿幕数据后对回报进行平均得到)。两种离轨策略方法在采样随机策略经过 1000 幕离轨策略数据采样后都很好地逼近了这个值,但加权重要度采样在开始时错误率明显较低,这也是实践中的典型现象。 假设一个回报序列 $G_1, G_2, \cdots, G_{n-1}$,它们都从相同的状态开始,且每一个回报都对应一个随机权重 $W_i$,我们希望获得如下式子的估计: $$ V_{n} \doteq \frac{\sum_{k=1}^{n-1} W_{k} G_{k}}{\sum_{k=1}^{n-1} W_{k}}, \quad n \geq 2 $$ 同时在获得一个额外的回报 $G_n$ 时能保持更新。为了能不断跟踪 $V_n$ 的变化,我们必须为每一个状态维护前 $n$ 个回报对应的权值的累加和 $C_n$。$V_n$ 的更新方法如下: $$ \begin{array}{l} V_{n+1} \doteq V_{n}+\dfrac{W_{n}}{C_{n}}\left[G_{n}-V_{n}\right], \quad n \geq 1 \\ C_{n+1} \doteq C_{n}+W_{n+1} \end{array} $$ 其中,$C_0 = 0$,$V_1$ 是任意值。一个完整的用于蒙特卡洛策略评估的逐幕增量算法如下: 在离轨策略中,策略的价值评估和策略的控制是分开的,用于生成行动数据的策略被称为行动策略,行动策略可能与实际上被评估和改善的策略无关,而被评估和改善的策略称为目标策略。这样分离的好处在于当行动策略能对所有可能的动作继续进行采样时,目标策略可以是确定的(贪心的)。 离轨策略蒙特卡洛控制方法要求行动策略对目标策略可能做出的所有动作都有非零的概率被选择。为了试探所有的可能性,要求行动策略是软性的。一个基于通用迭代策略(GPI)和重要度采样的离轨策略蒙特卡洛控制方法如下: 本文示例代码实现请参见这里。 Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎ CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎ UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

2020/7/1
articleCard.readMore

利用动态规划求解马尔可夫决策过程 (Planning by Dynamic Programming)

本文为《强化学习系列》文章 1 2 3 动态规划 动态规划(Dynamic Programming,DP)是一种用于解决具有如下两个特性问题的通用算法: 优化问题可以分解为子问题。 子问题出现多次并可以被缓存和复用。 马尔可夫决策过程正符合这两个特性: 贝尔曼方程给定了迭代过程的分解。 价值函数保存并复用了解决方案。 在强化学习中,DP 的核心思想是使用价值函数来结构化地组织对最优策略的搜索。一旦得到了满足贝尔曼最优方程的价值函数 $v_*$ 或 $q_*$,得到最优策略就容易了。对于任意 $s \in \mathcal{S}$(状态集合),$a \in \mathcal{A} \left(s\right)$(动作集合)和 $s' \in \mathcal{S}^{+}$(在分幕式任务下 $\mathcal{S}$ 加上一个终止状态),有: $$ \begin{aligned} v_{*}(s) &=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{*}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{*}\left(s^{\prime}\right)\right] \end{aligned} $$ $$ \begin{aligned} q_{*}(s, a) &=\mathbb{E}\left[R_{t+1}+\gamma \max _{a^{\prime}} q_{*}\left(S_{t+1}, a^{\prime}\right) | S_{t}=s, A_{t}=a\right] \\ &\left.=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma \max _{a^{\prime}}\right] q_{*}\left(s^{\prime}, a^{\prime}\right)\right] \end{aligned} $$ 将贝尔曼方程转化成为近似逼近理想价值函数的递归更新公式,我们就得到了 DP 算法。 策略评估 对于一个策略 $\pi$,如何计算其状态价值函数 $v_{\pi}$ 被称为策略评估。对于任意 $s \in \mathcal{S}$,有: $$ \begin{aligned} v_{\pi}(s) & \doteq \mathbb{E}_{\pi}\left[G_{t} | S_{t}=s\right] \\ &=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &=\mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ &=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$ 其中 $\pi \left(a | s\right)$ 表示在环境 $s$ 中智能体在策略 $\pi$ 下采取动作 $a$ 的概率。只要 $\gamma < 1$ 或者任何状态在 $\pi$ 下都能保证最后终止,则 $v_{\pi}$ 唯一存在。 考虑一个近似的价值函数序列 $v_0, v_1, \cdots$,从 $\mathcal{S}^{+}$ 映射到 $\mathbb{R}$,初始的近似值 $v_0$ 可以任意选取(除了终止状态必须为 0 外)。下一轮迭代的近似可以使用 $v_{\pi}$ 的贝尔曼方程进行更新,对于任意 $s \in \mathcal{S}$ 有: $$ \begin{aligned} v_{k+1}(s) & \doteq \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s\right] \\ &=\sum_{a} \pi(a | s) \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$ 显然,$v_k = v_{\pi}$ 是这个更新规则的一个不动点。在保证 $v_{\pi}$ 存在的条件下,序列 $\left\{v_k\right\}$ 在 $k \to \infty$ 时将会收敛到 $v_{\pi}$,这个算法称作 迭代策略评估。 策略改进 对于任意一个确定的策略 $\pi$,我们已经确定了它的价值函数 $v_{\pi}$。对于某个状态 $s$,我们想知道是否应该选择一个不同于给定的策略的动作 $a \neq \pi \left(s\right)$。如果从状态 $s$ 继续使用现有策略,则最后的结果就是 $v \left(s\right)$,但我们并不知道换成一个新策略后是得到更好的结果还是更坏的结果。一种解决方法是在状态 $s$ 选择动作 $a$ 后,继续遵循现有的策略 $\pi$,则这种方法的价值为: $$ \begin{aligned} q_{\pi}(s, a) & \doteq \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$ 一个关键的准则就是这个值是大于还是小于 $v_{\pi} \left(s\right)$。如果这个值更大,则说明在状态 $s$ 选择动作 $a$,然后继续使用策略 $\pi$ 会比使用始终使用策略 $\pi$ 更优。 上述情况是策略改进定理的一个特例,一般来说,如果 $\pi$ 和 $\pi'$ 是任意两个确定的策略,对于任意 $s \in \mathcal{S}$: $$ q_{\pi}\left(s, \pi^{\prime}(s)\right) \geq v_{\pi}(s) $$ 则称策略 $\pi'$ 相比于 $\pi$ 一样好或更好。也就是说,对于任意状态 $s \in \mathcal{S}$,这样肯定能得到一样或更好的期望回报: $$ v_{\pi^{\prime}}(s) \geq v_{\pi}(s) $$ 延伸到所有状态和所有可能的动作,即在每个状态下根据 $q_{\pi} \left(s, a\right)$ 选择一个最优的,换言之,考虑一个新的贪心策略 $\pi'$,满足: $$ \begin{aligned} \pi^{\prime}(s) & \doteq \underset{a}{\arg \max } q_{\pi}(s, a) \\ &=\underset{a}{\arg \max } \mathbb{E}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\underset{a}{\arg \max } \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi}\left(s^{\prime}\right)\right] \end{aligned} $$ 这样构造出的贪心策略满足策略改进定理的条件,所以它和原策略相比一样好或更好。这种根据原策略的价值函数执行贪心算法,来构造一个更好策略的过程称之为策略改进。如果新的贪心策略 $\pi'$ 和原策略 $\pi$ 一样好而不是更好,则有 $v_{\pi} = v_{\pi'}$,对任意 $s \in \mathcal{S}$: $$ \begin{aligned} v_{\pi^{\prime}}(s) &=\max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{\pi^{\prime}}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{\pi^{\prime}}\left(s^{\prime}\right)\right] \end{aligned} $$ 这同贝尔曼方程完全相同,因此 $v_{\pi}$ 一定与 $v_*$ 相同,$\pi$ 与 $\pi'$ 均必须为最优策略。因此,在除了原策略即为最优策略的情况下,策略改进一定会给出一个更优的结果。 策略迭代 一个策略 $\pi$ 根据 $v_{\pi}$ 产生了一个更好的策略 $\pi'$,进而我们可以通过计算 $v_{\pi'}$ 来得到一个更优的策略 $\pi''$。这样一个链式的方法可以得到一个不断改进的策略和价值函数序列: $$ \pi_{0} \stackrel{E}{\longrightarrow} v_{\pi_{0}} \stackrel{I}{\longrightarrow} \pi_{1} \stackrel{E}{\longrightarrow} v_{\pi_{1}} \stackrel{I}{\longrightarrow} \pi_{2} \stackrel{E}{\longrightarrow} \cdots \stackrel{I}{\longrightarrow} \pi_{*} \stackrel{E}{\longrightarrow} v_{*} $$ 其中 $\stackrel{E}{\longrightarrow}$ 表示策略评估,$\stackrel{I}{\longrightarrow}$ 表示策略改进。每一个策略都能保证同前一个一样或者更优,由于一个有限 MDP 必然只有有限种策略,所以在有限次的迭代后,这种方法一定收敛到一个最优的策略与最优价值函数。这种寻找最优策略的方法叫做策略迭代。整个策略迭代算法如下: 以杰克租车(Jack’s Car)问题为例:杰克在两地运营租车公司,每租出一辆车获得 10 元收益,为了保证每个地点有车可用,杰克需要夜间在两地之间移动车辆,每辆车的移动代价为 2 元。假设每个地点租车和还车的数量符合泊松分布 $\dfrac{\lambda^n}{n!} e^{- \lambda}$,其中 $\lambda$ 为期望值,租车的 $\lambda$ 在两地分别为 3 和 4,还车的 $\lambda$ 在两地分别为 3 和 2。假设任何一个地点不超过 20 辆车,每天最多移动 5 辆车,折扣率 $\gamma = 0.9$,将问题描述为一个持续的有限 MPD,时刻按天计算,状态为每天结束时每个地点的车辆数,动作则为夜间在两个地点之间移动的车辆数。策略从不移动任何车辆开始,整个策略迭代过程如下图所示: 上例代码实现请参见这里。 价值迭代 策略迭代算法的一个缺点是每一次迭代都涉及了策略评估,这是一个需要多次遍历状态集合的迭代过程。如果策略评估是迭代进行的,那么收敛到 $v_{\pi}$ 理论上在极限处才成立,实际中不必等到其完全收敛,可以提前截断策略评估过程。有多种方式可以截断策略迭代中的策略评估步骤,并且不影响其收敛,一种重要的特殊情况是在一次遍历后即刻停止策略评估,该算法称为价值迭代。可以将此表示为结合了策略改进与阶段策略评估的简单更新公式,对任意 $s \in \mathcal{S}$: $$ \begin{aligned} v_{k+1}(s) & \doteq \max _{a} \mathbb{E}\left[R_{t+1}+\gamma v_{k}\left(S_{t+1}\right) | S_{t}=s, A_{t}=a\right] \\ &=\max _{a} \sum_{s^{\prime}, r} p\left(s^{\prime}, r | s, a\right)\left[r+\gamma v_{k}\left(s^{\prime}\right)\right] \end{aligned} $$ 可以证明,对任意 $v_0$,在 $v_*$ 存在的条件下,序列 $\left\{v_k\right\}$ 都可以收敛到 $v_*$。整个价值迭代算法如下: 以赌徒问题(Gambler’s Problem)为例:一个赌徒下注猜一系列抛硬币实验的结果,如果正面朝上则获得这一次下注的钱,如果背面朝上则失去这一次下注的钱,游戏在达到目标收益 100 元或全部输光时结束。每抛一次硬币,赌徒必须从他的赌资中选取一个整数来下注,这个问题可以表示为一个非折扣的分幕式有限 MDP。状态为赌徒的赌资 $s \in \left\{1, 2, \cdots, 99\right\}$,动作为赌徒下注的金额 $a \in \left\{0, 1, \cdots, \min \left(s, 100 - s\right)\right\}$,收益在一般情况下为 0,只有在赌徒达到获利 100 元的终止状态时为 1。 令 $p_h$ 为抛硬币正面朝上的概率,如果 $p_h$ 已知,那么整个问题可以由价值迭代或其他类似算法解决。下图为当 $p_h = 0.4$ 时,价值迭代连续遍历得到的价值函数和最后的策略。 上例代码实现请参见这里。 异步动态规划 之前讨论的 DP 方法的一个主要缺点是它们涉及对 MDP 的整个状态集的操作,如果状态集很大,即使单次遍历也会十分昂贵。异步动态规划算法是一类就地迭代的 DP 算法,其不以系统遍历状态集的形式来组织算法。这些算法使用任意可用的状态值,以任意顺序来更新状态值,在某些状态的值更新一次之前,另一些状态的值可能已经更新了好几次。然而为了正确收敛,异步算法必须要不断地更新所有状态的值:在某个计算节点后,它不能忽略任何一个状态。 广义策略迭代 策略迭代包含两个同时进行的相互作用的流程,一个使得价值函数与当前策略一致(策略评估),另一个根据当前价值函数贪心地更新策略(策略改进)。在策略迭代中,这两个流程交替进行,每个流程都在另一个开始前完成。然而这也不是必须的,在异步方法中,评估和改进流程则以更细的粒度交替进行。我们利用广义策略迭代(GPI)一词来指代策略评估和策略改进相互作用的一般思路,与这两个流程的力度和其他细节无关。 几乎所有的强化学习方法都可以被描述为 GPI,几乎所有方法都包含明确定义的策略和价值函数。策略总是基于特定的价值函数进行改进,价值函数也始终会向对应特定策略的真实价值函数收敛。 GPI 的评估和改进流程可以视为两个约束或目标之间的相互作用的流程。每个流程都把价值函数或策略推向其中的一条线,该线代表了对于两个目标中的某一个目标的解决方案,如下图所示: Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎ CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎ UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

2020/6/13
articleCard.readMore

贝叶斯优化 (Bayesian Optimization)

本文内容主要参考自: 从高斯分布到高斯过程、高斯过程回归、贝叶斯优化 A Visual Exploration of Gaussian Processes Gaussian Process Regression Exploring Bayesian Optimization 高斯分布 一元高斯分布 若随机变量 $X$ 服从一个均值为 $\mu$,方差为 $\sigma^2$ 的高斯分布,则记为: $$ X \sim N \left(\mu, \sigma^2\right) $$ 其概率密度函数为: $$ f \left(x\right) = \dfrac{1}{\sigma \sqrt{2 \pi}} e^{- \dfrac{\left(x - \mu\right)^2}{2 \sigma^2}} $$ 二元高斯分布 若随机变量 $X, Y$ 服从均值为 $\mu = \left(\mu_X, \mu_Y\right)^{\top}$,方差为 $\mu = \left(\sigma_X, \sigma_Y\right)^{\top}$ 的高斯分布,则记为: $$ \left(X, Y\right) \sim \mathcal{N} \left(\mu, \sigma\right) $$ 其概率密度函数为: $$ f(x, y)=\frac{1}{2 \pi \sigma_{X} \sigma_{Y} \sqrt{1-\rho^{2}}} e^{-\dfrac{1}{2\left(1-\rho^{2}\right)}\left[\dfrac{\left(x-\mu_{X}\right)^{2}}{\sigma_{X}^{2}}+\dfrac{\left(y-\mu_{Y}\right)^{2}}{\sigma_{Y}^{2}}-\dfrac{2 \rho\left(x-\mu_{X}\right)\left(y-\mu_{X}\right)}{\sigma_{X} \sigma_{Y}}\right]} $$ 其中,$\rho$ 是 $X$ 和 $Y$ 之间的相关系数,$\sigma_X > 0$ 且 $\sigma_Y > 0$。 多元高斯分布 若 $K$ 维随机向量 $X = \left[X_1, \cdots, X_K\right]^{\top}$ 服从多元高斯分布,则必须满足如下三个等价条件: 任何线性组合 $Y = a_1 X_1 + \cdots a_K X_K$ 均服从高斯分布。 存在随机向量 $Z = \left[Z_1, \cdots, Z_L\right]^{\top}$(每个元素服从独立标准高斯分布),向量 $\mu = \left[\mu_1, \cdots, \mu_K\right]^{\top}$ 以及 $K \times L$ 的矩阵 $A$,满足 $X = A Z + \mu$。 存在 $\mu$ 和一个对称半正定矩阵 $\Sigma$ 满足 $X$ 的特征函数 $\phi_X \left(u; \mu, \Sigma\right) = \exp \left(i \mu^{\top} u - \dfrac{1}{2} u^{\top} \Sigma u\right)$ 如果 $\Sigma$ 是非奇异的,则概率密度函数为: $$ f \left(x_1, \cdots, x_k\right) = \dfrac{1}{\sqrt{\left(2 \pi\right)^k \lvert\Sigma\rvert}} e^{- \dfrac{1}{2} \left(x - \mu\right)^{\top} \Sigma^{-1} \left(x - \mu\right)} $$ 其中 $\lvert\Sigma\rvert$ 表示协方差矩阵的行列式。 边缘化和条件化 高斯分布具有一个优秀的代数性质,即在边缘化和条件化下是闭合的,也就是说从这些操作中获取的结果分布也是高斯的。边缘化(Marginalization)和条件化(Conditioning)都作用于原始分布的子集上: $$ P_{X, Y}=\left[\begin{array}{l} X \\ Y \end{array}\right] \sim \mathcal{N}(\mu, \Sigma)=\mathcal{N}\left(\left[\begin{array}{l} \mu_{X} \\ \mu_{Y} \end{array}\right],\left[\begin{array}{l} \Sigma_{X X} \Sigma_{X Y} \\ \Sigma_{Y X} \Sigma_{Y Y} \end{array}\right]\right) $$ 其中,$X$ 和 $Y$ 表示原始随机变量的子集。 对于随机向量 $X$ 和 $Y$ 的高斯概率分布 $P \left(X, Y\right)$,其边缘概率分布为: $$ \begin{array}{l} X \sim \mathcal{N}\left(\mu_{X}, \Sigma_{X X}\right) \\ Y \sim \mathcal{N}\left(\mu_{Y}, \Sigma_{Y Y}\right) \end{array} $$ $X$ 和 $Y$ 两个子集各自只依赖于 $\mu$ 和 $\Sigma$ 中它们对应的值。因此从高斯分布中边缘化一个随机变量仅需从 $\mu$ 和 $\Sigma$ 中舍弃相应的变量即可: $$ p_{X}(x)=\int_{y} p_{X, Y}(x, y) d y=\int_{y} p_{X | Y}(x | y) p_{Y}(y) d y $$ 条件化可以用于得到一个变量在另一个变量条件下的概率分布: $$ \begin{array}{l} X | Y \sim \mathcal{N}\left(\mu_{X}+\Sigma_{X Y} \Sigma_{Y Y}^{-1}\left(Y-\mu_{Y}\right), \Sigma_{X X}-\Sigma_{X Y} \Sigma_{Y Y}^{-1} \Sigma_{Y X}\right) \\ Y | X \sim \mathcal{N}\left(\mu_{Y}+\Sigma_{Y X} \Sigma_{X X}^{-1}\left(X-\mu_{X}\right), \Sigma_{Y Y}-\Sigma_{Y X} \Sigma_{X X}^{-1} \Sigma_{X Y}\right) \end{array} $$ 需要注意新的均值仅依赖于作为条件的变量,协方差矩阵和这个变量无关。 边缘化可以理解为在高斯分布的一个维度上的累加,条件化可以理解为在多元分布上切一刀从而获得一个维数更少的高斯分布,如下图所示: 高斯过程 高斯过程(Gaussian Process)是观测值出现在一个连续域(例如时间或空间)的随机过程。在高斯过程中,连续输入空间中每个点都是与一个正态分布的随机变量相关联。此外,这些随机变量的每个有限集合都有一个多元正态分布,换句话说它们的任意有限线性组合是一个正态分布。高斯过程的分布是所有那些(无限多个)随机变量的联合分布,正因如此,它是连续域(例如时间或空间)上函数的分布。 简单而言,高斯过程即为一系列随机变量,这些随机变量的任意有限集合均为一个多元高斯分布。从一元高斯分布到多元高斯分布相当于增加了空间维度,从高斯分布到高斯过程相当于引入了时间维度。一个高斯过程可以被均值函数 $m \left(x\right)$ 和协方差函数 $K \left(x, x'\right)$ 共同唯一确定: $$ \begin{aligned} m(x) &=\mathbb{E}[f(x)] \\ K\left(x, x'\right) &=\mathbb{E}\left[(f(x)-m(x))\left(f\left(x^{\prime}\right)-m\left(x^{\prime}\right)\right)\right] \end{aligned} $$ 则高斯过程可以表示为: $$ f \left(x\right) \sim \mathcal{GP} \left(m \left(x\right), K \left(x, x'\right)\right) $$ 均值函数决定了样本出现的整体位置,如果为零则表示以 $y = 0$ 为基准线。协方差函数描述了不同点之间的关系,从而可以利用输入的训练数据预测未知点的值。常用的协方差函数有: 常数:$K_c \left(x, x'\right) = C$ 线性:$K_L \left(x, x'\right) = x^{\top} x'$ 高斯噪声:$K_{GN} \left(x, x'\right) = \sigma^2 \delta_{x, x'}$ 指数平方:$K_{\mathrm{SE}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{|d|^{2}}{2 \ell^{2}}\right)$ Ornstein-Uhlenbeck:$K_{\mathrm{OU}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{|d|}{\ell}\right)$ Matérn:$K_{\text {Matern }}\left(x, x^{\prime}\right)=\dfrac{2^{1-\nu}}{\Gamma(\nu)}\left(\dfrac{\sqrt{2 \nu}|d|}{\ell}\right)^{\nu} K_{\nu}\left(\dfrac{\sqrt{2 \nu}|d|}{\ell}\right)$ 周期:$K_{\mathrm{P}}\left(x, x^{\prime}\right)=\exp \left(-\dfrac{2 \sin ^{2}\left(\dfrac{d}{2}\right)}{\ell^{2}}\right)$ 有理平方:$K_{\mathrm{RQ}}\left(x, x^{\prime}\right)=\left(1+|d|^{2}\right)^{-\alpha}, \quad \alpha \geq 0$ 高斯过程回归 回归任务的目标是给定一个输入变量 $x \in \mathbb{R}^D$ 预测一个或多个连续目标变量 $y$ 的值。更确切的说,给定一个包含 $N$ 个观测值的训练集 $\mathbf{X} = \left\{x_n\right\}^N_1$ 和对应的目标值 $\mathbf{Y} = \left\{y_n\right\}^N_1$,回归的目标是对于一个新的 $x$ 预测对应的 $y$。目标值和观测值之间通过一个映射进行关联: $$ f: X \to Y $$ 在贝叶斯模型中,我们通过观测数据 $\mathcal{D} = \left\{\left(\mathbf{x}_n, \mathbf{y}_n\right)\right\}^N_{n=1}$ 更新先验分布 $P \left(\mathbf{\Theta}\right)$。通过贝叶斯公式我们可以利用先验概率 $P \left(\mathbf{\Theta}\right)$ 和似然函数 $P \left(\mathcal{D} | \mathbf{\Theta}\right)$ 推导出后验概率: $$ p\left(\mathbf{\Theta} | \mathcal{D}\right)=\frac{p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right)}{p\left(\mathcal{D}\right)} $$ 其中 $p\left(\mathcal{D}\right)$ 为边际似然。在贝叶斯回归中我们不仅希望获得未知输入对应的预测值 $\mathbf{y}_*$ ,还希望知道预测的不确定性。因此我们需要利用联合分布和边缘化模型参数 $\mathbf{\Theta}$ 来构造预测分布: $$ p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathcal{D}\right)=\int p\left(\mathbf{y}_{*}, \mathbf{\Theta} | \mathbf{x}_{*}, \mathcal{D}\right) \mathrm{d} \Theta=\int p\left(\mathbf{y}_{*} | \mathbf{x}_{*}, \mathbf{\Theta}, \mathcal{D}\right) p(\mathbf{\Theta} | \mathcal{D}) \mathrm{d} \mathbf{\Theta} $$ 通常情况下,由于积分形式 $p \left(\Theta | \mathcal{D}\right)$ 不具有解析可解性(Analytically Tractable): $$ p\left(\mathcal{D}\right)=\int p\left(\mathcal{D} | \mathbf{\Theta}\right) p\left(\mathbf{\Theta}\right) d \Theta $$ 但在高斯似然和高斯过程先验的前提下,后验采用函数的高斯过程的形式,同时是解析可解的。 对于高斯过程回归,我们构建一个贝叶斯模型,首先定义函数输出的先验为一个高斯过程: $$ p \left(f | \mathbf{X}, \theta\right) = \mathcal{N} \left(\mathbf{0}, K \left(\mathbf{X}, \mathbf{X}\right)\right) $$ 其中 $K \left(\cdot, \cdot\right)$ 为协方差函数,$\theta$ 为过程的超参数。假设数据已经变换为零均值,因此我们不需要在先验中设置均值函数,则令似然形式如下: $$ p \left(\mathbf{Y} | f\right) \sim \mathcal{N} \left(f, \sigma^2_n \mathbf{I}\right) $$ 假设观测值为独立同分布的高斯噪音的累加,则整个模型的联合分布为: $$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) $$ 虽然我们并不关心变量 $f$,但由于我们需要对不确定性进行建模,我们仍需考虑 $\mathbf{Y}$ 和 $f$ 以及 $f$ 和 $\mathbf{X}$ 之间的关系。高斯过程作为一个非参数模型,其先验分布构建于映射 $f$ 之上,$f$ 仅依赖于核函数的超参数 $\theta$,且这些超参数可以通过数据进行估计。我们可以将超参数作为先验,即: $$ p \left(\mathbf{Y} , f | \mathbf{X}, \theta\right) = p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) p \left(\theta\right) $$ 然后进行贝叶斯推断和模型选择,但是通常情况下这是不可解的。David MacKay 引入了一个利用最优化边际似然来估计贝叶斯平均的框架,即计算如下积分: $$ p \left(\mathbf{Y} | \mathbf{X}, \theta\right) = \int p \left(\mathbf{Y} | f\right) p \left(f | \mathbf{X}, \theta\right) df $$ 其中,高斯似然 $p \left(\mathbf{Y} | f\right)$ 表示模型拟合数据的程度,$p \left(f | \mathbf{X}, \theta\right)$ 为高斯过程先验。经过边缘化后,$\mathbf{Y}$ 不在依赖于 $f$ 而仅依赖于 $\theta$。 假设采用零均值函数,对于一个高斯过程先验,我们仅需指定一个协方差函数。以指数平方协方差函数为例,选择一系列测试输入点 $X_*$,利用协方差矩阵和测试输入点可以生成一个高斯向量: $$ \mathbf{f}_* \sim \mathcal{N} \left(\mathbf{0}, K \left(X_*, X_*\right)\right) $$ 从高斯先验中进行采样,我们首先需要利用标准正态来表示多元正态: $$ \mathbf{f}_* \sim \mu + \mathbf{B} \mathcal{N} \left(0, \mathbf{I}\right) $$ 其中,$\mathbf{BB}^{\top} = K \left(X_*, X_*\right)$,$\mathbf{B}$ 本质上是协方差矩阵的平方根,可以通过 Cholesky 分解获得。 上图(左)为从高斯先验中采样的 10 个序列,上图(右)为先验的协方差。如果输入点 $x_n$ 和 $x_m$ 接近,则对应的 $f \left(x_n\right)$ 和 $f \left(x_m\right)$ 相比于不接近的点是强相关的。 我们关注的并不是这些随机的函数,而是如何将训练数据中的信息同先验进行合并。假设观测数据为 $\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$,则训练目标 $\mathbf{f}$ 和测试目标 $\mathbf{f}_*$ 之间的联合分布为: $$ \left[\begin{array}{l} \mathbf{f} \\ \mathbf{f}_{*} \end{array}\right] \sim \mathcal{N}\left(\mathbf{0},\left[\begin{array}{ll} K(X, X) & K\left(X, X_{*}\right) \\ K\left(X_{*}, X\right) & K\left(X_{*}, X_{*}\right) \end{array}\right]\right) $$ 根据观测值对联合高斯先验分布进行条件化处理可以得到高斯过程回归的关键预测方程: $$ \mathbf{f}_{*} | X, X_{*}, \mathbf{f} \sim \mathcal{N}\left(\overline{\mathbf{f}}_{*}, \operatorname{cov}\left(\mathbf{f}_{*}\right)\right) $$ 其中 $$ \begin{aligned} \overline{\mathbf{f}}_{*} & \triangleq \mathbb{E}\left[\mathbf{f}_{*} | X, X_{*}, \mathbf{f}\right]=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$ 函数值可以通过对联合后验分布采样获得。 我们以三角函数作为给定的函数,并随机采样一些训练数据 $\left\{\left(\mathbf{x}_{i}, f_{i}\right) | i=1, \ldots, n\right\}$,如下图所示: 我们希望将训练数据和高斯过程先验进行合并得到联合后验分布,我们可以通过在观测值上条件化联合高斯先验分布,预测的均值和协方差为: $$ \begin{aligned} \overline{\mathbf{f}}_{*} &=K\left(X_{*}, X\right) K(X, X)^{-1} \mathbf{f} \\ \operatorname{cov}\left(\mathbf{f}_{*}\right) &=K\left(X_{*}, X_{*}\right)-K\left(X_{*}, X\right) K(X, X)^{-1} K\left(X, X_{*}\right) \end{aligned} $$ Rasmussen 和 Williams 给出了一个实现高斯过程回归的实用方法: 高斯过程后验和采样的序列如下图所示: 先验的协方差矩阵和后验的协方差矩阵可视化如下图所示: 本小结代码请参见这里。 贝叶斯优化 主动学习 在很多机器学习问题中,数据标注往往需要耗费很大成本。主动学习(Active Learning)在最大化模型准确率时最小化标注成本,例如对不确定性最高的数据进行标注。由于我们仅知道少量数据点,因此我们需要一个代理模型(Surrogate Model)来建模真正的模型。高斯过程因其灵活性和具有估计不确定性估计的特性不失为一个常用的代理模型。 在估计 $f \left(x\right)$ 的过程中,我们希望最小化评估的次数,因此我们可以通过主动学习来“智能”地选择下一个评估的数据点。通过不断的选择具有最高不确定性的数据点来获得 $f \left(x\right)$ 更准确的估计,直至收敛或达到停止条件。下图展示了利用主动学习估计真实数据分布的过程: 贝叶斯优化问题 贝叶斯优化的核心问题是:基于现有的已知情况,如果选择下一步评估的数据点?在主动学习中我们选择不确定性最大的点,但在贝叶斯优化中我们需要在探索不确定性区域(探索)和关注已知具有较优目标值的区域之间进行权衡(开发)。这种评价的依据称之为采集函数(Acquisition Functions),采集函数通过当前模型启发式的评估是否选择一个数据点。 贝叶斯优化的目标是找到一个函数 $f: \mathbb{R}^d \mapsto \mathbb{R}$ 最大值(或最小值)对应的位置 $x \in \mathbb{R}^d$。为了解决这个问题,我们遵循如下算法: 选择一个代理模型用于建模真实函数 $f$ 和定义其先验。 给定观测集合,利用贝叶斯公式获取后验。 利用采集函数 $\alpha \left(x\right)$ 确性下一个采样点 $x_t = \arg\max_x \alpha \left(x\right)$。 将采样的点加入观测集合,重复步骤 2 直至收敛或达到停止条件。 采集函数 Probability of Improvement (PI) Probability of Improvement (PI) 采集函数会选择具有最大可能性提高当前最大的 $f \left(x^{+}\right)$ 值的点作为下一个查询点,即: $$ x_{t+1} = \arg\max \left(\alpha_{PI} \left(x\right)\right) = \arg\max \left(P \left(f \left(x\right)\right) \geq \left(f \left(x^{+}\right) + \epsilon\right)\right) $$ 其中,$P \left(\cdot\right)$ 表示概率,$\epsilon$ 为一个较小的正数,$x^{+} = \arg\max_{x_i \in x_{1:t}} f \left(x_i\right)$,$x_i$ 为第 $i$ 步查询点的位置。如果采用高斯过程作为代理模型,上式则转变为: $$ x_{t+1} = \arg\max_x \Phi \left(\dfrac{\mu_t \left(x\right) - f \left(x^{+}\right) - \epsilon}{\sigma_t \left(x\right)}\right) $$ 其中,$\Phi \left(\cdot\right)$ 表示标准正态分布累积分布函数。PI 利用 $\epsilon$ 来权衡探索和开发,增加 $\epsilon$ 的值会更加倾向进行探索。 Expected Improvement (EI) PI 仅关注了有多大的可能性能够提高,而没有关注能够提高多少。Expected Improvement (EI) 则会选择具有最大期望提高的点作为下一个查询点,即: $$ x_{t+1} = \arg\min_x \mathbb{E} \left(\left\|h_{t+1} \left(x\right) - f \left(x^*\right)\right\| | \mathcal{D}_t\right) $$ 其中,$f$ 为真实函数,$h_{t+1}$ 为代理模型在 $t+1$ 步的后验均值,$\mathcal{D}_t = \left\{\left(x_i, f\left(x_i\right)\right)\right\}, \forall x \in x_{1:t}$ 为训练数据,$x^*$ 为 $f$ 取得最大值的真实位置。 上式中我们希望选择能够最小化与最大目标值之间距离的点,由于我们并不知道真实函数 $f$,Mockus 1 提出了一种解决办法: $$ x_{t+1} = \arg\max_x \mathbb{E} \left(\max \left\{0, h_{t+1} \left(x\right) - f \left(x^{+}\right)\right\} | \mathcal{D}_t\right) $$ 其中,$f \left(x^{+}\right)$ 为到目前为止遇见的最大函数值,如果采用高斯过程作为代理模型,上式则转变为: $$ \begin{aligned} EI(x) &= \left\{\begin{array}{ll} \left(\mu_{t}(x)-f\left(x^{+}\right)-\epsilon\right) \Phi(Z)+\sigma_{t}(x) \phi(Z), & \text { if } \sigma_{t}(x)>0 \\ 0 & \text { if } \sigma_{t}(x)=0 \end{array}\right. \\ Z &= \frac{\mu_{t}(x)-f\left(x^{+}\right)-\epsilon}{\sigma_{t}(x)} \end{aligned} $$ 其中 $\Phi \left(\cdot\right)$ 表示标准正态分布累积分布函数,$\phi \left(\cdot\right)$ 表示标准正态分布概率密度函数。类似 PI,EI 也可以利用 $\epsilon$ 来权衡探索和开发,增加 $\epsilon$ 的值会更加倾向进行探索。 对比和其他采集函数 上图展示了在仅包含一个训练观测数据 $\left(0.5, f \left(0.5\right)\right)$ 情况下不同点的采集函数值。可以看出 $\alpha_{EI}$ 和 $\alpha_{PI}$ 的最大值分别为 0.3 和 0.47。选择一个具有较小的 $\alpha_{PI}$ 和一个较大的 $\alpha_{EI}$ 的点可以理解为一个高的风险和高的回报。因此,当多个点具有相同的 $\alpha_{EI}$ 时,我们应该优先选择具有较小风险(高 $\alpha_{PI}$)的点,类似的,当多个点具有相同的 $\alpha_{PI}$ 时,我们应该优先选择具有较大回报(高 $\alpha_{EI}$)的点。 其他采集函数还有 Thompson Sampling 2,Upper Confidence Bound (UCB),Gaussian Process Upper Confidence Bound (GP-UCB) 3,Entropy Search 4,Predictive Entropy Search 5 等,细节请参见原始论文或 A Tutorial on Bayesian Optimization 6。 开放资源 scikit-optimize/scikit-optimize hyperopt/hyperopt automl/SMAC3 fmfn/BayesianOptimization pytorch/botorch GPflow/GPflowOpt keras-team/keras-tuner tobegit3hub/advisor Mockus, J. B., & Mockus, L. J. (1991). Bayesian approach to global optimization and application to multiobjective and constrained problems. Journal of Optimization Theory and Applications, 70(1), 157-172. ↩︎ Thompson, W. R. (1933). On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. Biometrika, 25(3/4), 285-294. ↩︎ Auer, P. (2002). Using confidence bounds for exploitation-exploration trade-offs. Journal of Machine Learning Research, 3(Nov), 397-422. ↩︎ Hennig, P., & Schuler, C. J. (2012). Entropy search for information-efficient global optimization. Journal of Machine Learning Research, 13(Jun), 1809-1837. ↩︎ Hernández-Lobato, J. M., Hoffman, M. W., & Ghahramani, Z. (2014). Predictive entropy search for efficient global optimization of black-box functions. In Advances in neural information processing systems (pp. 918-926). ↩︎ Frazier, P. I. (2018). A tutorial on bayesian optimization. arXiv preprint arXiv:1807.02811. ↩︎

2020/6/6
articleCard.readMore

马尔可夫决策过程 (Markov Decision Process)

本文为《强化学习系列》文章 1 2 3 马尔可夫模型 马尔可夫模型是一种用于序列数据建模的随机模型,其假设未来的状态仅取决于当前的状态,即: $$ \mathbb{P} \left[S_{t+1} | S_t\right] = \mathbb{P} \left[S_{t+1} | S_1, \cdots, S_t\right] $$ 也就是认为当前状态捕获了历史中所有相关的信息。根据系统状态是否完全可被观测以及系统是自动的还是受控的,可以将马尔可夫模型分为 4 种,如下表所示: 状态状态完全可被观测 系统状态不是完全可被观测 状态是自动的 马尔可夫链(MC) 隐马尔可夫模型(HMM) 系统是受控的 马尔可夫决策过程(MDP) 部分可观测马尔可夫决策过程(POMDP) 马尔可夫链(Markov Chain,MC)为从一个状态到另一个状态转换的随机过程,当马尔可夫链的状态只能部分被观测到时,即为隐马尔可夫模型(Hidden Markov Model,HMM),也就是说观测值与系统状态有关,但通常不足以精确地确定状态。马尔可夫决策过程(Markov Decision Process,MDP)也是马尔可夫链,但其状态转移取决于当前状态和采取的动作,通常一个马尔可夫决策过程用于计算依据期望回报最大化某些效用的行动策略。部分可观测马尔可夫决策过程(Partially Observable Markov Decision Process,POMDP)即为系统状态仅部分可见情况下的马尔可夫决策过程。 马尔可夫过程 对于一个马尔可夫状态 $s$ 和一个后继状态 $s'$,状态转移概率定义为: $$ \mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right] $$ 状态概率矩阵 $\mathcal{P}$ 定义了从所有状态 $s$ 到后继状态 $s'$ 的转移概率: $$ \mathcal{P} = \left[\begin{array}{ccc} \mathcal{P}_{11} & \cdots & \mathcal{P}_{1 n} \\ \vdots & & \\ \mathcal{P}_{n 1} & \cdots & \mathcal{P}_{n n} \end{array}\right] $$ 其中每一行的加和为 1。 马尔可夫过程(马尔可夫链)是一个无记忆的随机过程,一个马尔可夫过程可以定义为 $\langle \mathcal{S}, \mathcal{P} \rangle$,其中 $\mathcal{S}$ 是一个有限状态集合,$\mathcal{P}_{ss'} = \mathbb{P} \left[S_t = s' | S_{t-1} = s\right]$,$\mathcal{P}$ 为状态转移概率矩阵。以一个学生的日常生活为例,Class $i$ 表示第 $i$ 门课程,Facebook 表示在 Facebook 上进行社交,Pub 表示去酒吧,Pass 表示通过考试,Sleep 表示睡觉,这个马尔可夫过程如下图所示: 从而可以产生多种不同的序列,例如: C1 -> C2 -> C3 -> Pass -> Sleep C1 -> FB -> FB -> C1 -> C2 -> Sleep C1 -> C2 -> C3 -> Pub -> C2 -> C3 -> Pass -> Sleep 状态转移概率矩阵如下所示: 据此我们可以定义马尔可夫奖励过程(Markov Reward Process,MRP)为 $\langle \mathcal{S, P, R}, \gamma \rangle$,其中 $\mathcal{S}$ 和 $\mathcal{P}$ 同马尔可夫过程定义中的参数相同,$\mathcal{R}$ 为收益函数,$\mathcal{R}_s = \mathbb{E} \left[R_t | S_{t-1} = s\right]$,$\gamma \in \left[0, 1\right]$ 为折扣率。如下图所示: 期望回报 $G_t$ 定义为从时刻 $t$ 之后的所有衰减的收益之和,即: $$ G_t = R_{t+1} + \gamma R_{t+2} + \cdots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} $$ 当 $\gamma$ 接近 $0$ 时,智能体更倾向于近期收益,当 $\gamma$ 接近 $1$ 时,智能体更侧重考虑长远收益。邻接时刻的收益可以按如下递归方式表示: $$ G_t = R_{t+1} + \gamma G_{t+1} $$ 对于存在“最终时刻”的应用中,智能体和环境的交互能被自然地分成一个系列子序列,每个子序列称之为“幕(episodes)”,例如一盘游戏、一次走迷宫的过程,每幕都以一种特殊状态结束,称之为终结状态。这些幕可以被认为在同样的终结状态下结束,只是对不同的结果有不同的收益,具有这种分幕重复特性的任务称之为分幕式任务。 MRP 的状态价值函数 $v \left(s\right)$ 给出了状态 $s$ 的长期价值,定义为: $$ \begin{aligned} v(s) &=\mathbb{E}\left[G_{t} | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma\left(R_{t+2}+\gamma R_{t+3}+\ldots\right) | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma G_{t+1} | S_{t}=s\right] \\ &=\mathbb{E}\left[R_{t+1}+\gamma v\left(S_{t+1}\right) | S_{t}=s\right] \end{aligned} $$ 价值函数可以分解为两部分:即时收益 $R_{t+1}$ 和后继状态的折扣价值 $\gamma v \left(S_{t+1}\right)$。上式我们称之为贝尔曼方程(Bellman Equation),其衡量了状态价值和后继状态价值之间的关系。 马尔可夫决策过程 一个马尔可夫决策过程(Markov Decision Process,MDP)定义为包含决策的马尔可夫奖励过程 $\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$,在这个环境中所有的状态均具有马尔可夫性。其中,$\mathcal{S}$ 为有限的状态集合,$\mathcal{A}$ 为有限的动作集合,$\mathcal{P}$ 为状态转移概率矩阵,$\mathcal{P}_{s s^{\prime}}^{a}=\mathbb{P}\left[S_{t+1}=s^{\prime} | S_{t}=s, A_{t}=a\right]$,$\mathcal{R}$ 为奖励函数,$\mathcal{R}_{s}^{a}=\mathbb{E}\left[R_{t+1} | S_{t}=s, A_{t}=a\right]$,$\gamma \in \left[0, 1\right]$ 为折扣率。上例中的马尔可夫决策过程如下图所示: 策略(Policy)定义为给定状态下动作的概率分布: $$ \pi \left(a | s\right) = \mathbb{P} \left[A_t = a | S_t = s\right] $$ 一个策略完全确定了一个智能体的行为,同时 MDP 策略仅依赖于当前状态。给定一个 MDP $\mathcal{M}=\langle\mathcal{S}, \mathcal{A}, \mathcal{P}, \mathcal{R}, \gamma\rangle$ 和一个策略 $\pi$,状态序列 $S_1, S_2, \cdots$ 为一个马尔可夫过程 $\langle \mathcal{S}, \mathcal{P}^{\pi} \rangle$,状态和奖励序列 $S_1, R_2, S_2, \cdots$ 为一个马尔可夫奖励过程 $\left\langle\mathcal{S}, \mathcal{P}^{\pi}, \mathcal{R}^{\pi}, \gamma\right\rangle$,其中 $$ \begin{aligned} \mathcal{P}_{s s^{\prime}}^{\pi} &=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{P}_{s s^{\prime}}^{a} \\ \mathcal{R}_{s}^{\pi} &=\sum_{a \in \mathcal{A}} \pi(a | s) \mathcal{R}_{s}^{a} \end{aligned} $$ 在策略 $\pi$ 下,状态 $s$ 的价值函数记为 $v_{\pi} \left(s\right)$,即从状态 $s$ 开始,智能体按照策略进行决策所获得的回报的概率期望值,对于 MDP 其定义为: $$ \begin{aligned} v_{\pi} \left(s\right) &= \mathbb{E}_{\pi} \left[G_t | S_t = s\right] \\ &= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s\right] \end{aligned} $$ 在策略 $\pi$ 下,在状态 $s$ 时采取动作 $a$ 的价值记为 $q_\pi \left(s, a\right)$,即根据策略 $\pi$,从状态 $s$ 开始,执行动作 $a$ 之后,所有可能的决策序列的期望回报: $$ \begin{aligned} q_\pi \left(s, a\right) &= \mathbb{E}_{\pi} \left[G_t | S_t = s, A_t = a\right] \\ &= \mathbb{E}_{\pi} \left[\sum_{k=0}^{\infty} \gamma^k R_{t+k+1} | S_t = s, A_t = a\right] \end{aligned} $$ 状态价值函数 $v_{\pi}$ 和动作价值函数 $q_{\pi}$ 都能从经验中估计得到,两者都可以分解为当前和后继两个部分: $$ \begin{aligned} v_{\pi}(s) &= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma v_{\pi}\left(S_{t+1}\right) | S_{t}=s\right] \\ q_{\pi}(s, a) &= \mathbb{E}_{\pi}\left[R_{t+1}+\gamma q_{\pi}\left(S_{t+1}, A_{t+1}\right) | S_{t}=s, A_{t}=a\right] \end{aligned} $$ 从一个状态 $s$ 出发,采取一个行动 $a$,状态价值函数为: $$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s) q_{\pi}(s, a) $$ 从一个动作 $s$ 出发,再采取一个行动 $a$ 后,动作价值函数为: $$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right) $$ 利用后继状态价值函数表示当前状态价值函数为: $$ v_{\pi}(s)=\sum_{a \in \mathcal{A}} \pi(a | s)\left(\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{\pi}\left(s^{\prime}\right)\right) $$ 利用后继动作价值函数表示当前动作价值函数为: $$ q_{\pi}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \sum_{a^{\prime} \in \mathcal{A}} \pi\left(a^{\prime} | s^{\prime}\right) q_{\pi}\left(s^{\prime}, a^{\prime}\right) $$ 最优状态价值函数 $v_* \left(s\right)$ 定义为所有策略上最大值的状态价值函数: $$ v_* \left(s\right) = \mathop{\max}_{\pi} v_{\pi} \left(s\right) $$ 最优动作价值函数 $q_* \left(s, a\right)$ 定义为所有策略上最大值的动作价值函数: $$ q_* \left(s, a\right) = \mathop{\max}_{\pi} q_{\pi} \left(s, a\right) $$ 定义不同策略之间的大小关系为: $$ \pi \geq \pi^{\prime} \text { if } v_{\pi}(s) \geq v_{\pi^{\prime}}(s), \forall s $$ 对于任意一个马尔可夫决策过程有: 存在一个比其他策略更优或相等的策略,$\pi_* \geq \pi, \forall \pi$ 所有的最优策略均能够获得最优的状态价值函数,$v_{\pi_*} \left(s\right) = v_* \left(s\right)$ 所有的最优策略均能够获得最优的动作价值函数,$q_{\pi_*} \left(s, a\right) = q_* \left(s, a\right)$ 一个最优策略可以通过最大化 $q_* \left(s, a\right)$ 获得: $$ \pi_{*}(a | s)=\left\{\begin{array}{ll} 1 & \text { if } a=\underset{a \in \mathcal{A}}{\operatorname{argmax}} q_{*}(s, a) \\ 0 & \text { otherwise } \end{array}\right. $$ 对于任意一个 MDP 均会有一个确定的最优策略,如果已知 $q_* \left(s, a\right)$ 即可知晓最优策略。 最优状态价值函数循环依赖于贝尔曼最优方程: $$ v_{*}(s)=\max _{a} q_{*}(s, a) $$ $$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$ $$ v_{*}(s)=\max _{a} \mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} v_{*}\left(s^{\prime}\right) $$ $$ q_{*}(s, a)=\mathcal{R}_{s}^{a}+\gamma \sum_{s^{\prime} \in \mathcal{S}} \mathcal{P}_{s s^{\prime}}^{a} \max _{a^{\prime}} q_{*}\left(s^{\prime}, a^{\prime}\right) $$ 显式求解贝尔曼最优方程给出了找到一个最优策略的方法,但这种解法至少依赖于三条实际情况很难满足的假设: 准确地知道环境的动态变化特性 有足够的计算资源来求解 马尔可夫性质 尤其是假设 2 很难满足,现实问题中状态的数量一般很大,即使利用最快的计算机也需要花费难以接受的时间才能求解完成。 Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎ CS234: Reinforcement Learning http://web.stanford.edu/class/cs234/index.html ↩︎ UCL Course on RL https://www.davidsilver.uk/teaching ↩︎

2020/5/23
articleCard.readMore

多臂赌博机 (Multi-armed Bandit)

本文为《强化学习系列》文章 1 多臂赌博机问题 一个赌徒,要去摇老虎机,走进赌场一看,一排老虎机,外表一模一样,但是每个老虎机吐钱的概率可不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是多臂赌博机问题 (Multi-armed bandit problem, K- or N-armed bandit problem, MAB) 2。 $k$ 臂赌博机问题中,$k$ 个动作的每一个在被选择时都有一个期望或者平均收益,称之为这个动作的**“价值”**。令 $t$ 时刻选择的动作为 $A_t$,对应的收益为 $R_t$,任一动作 $a$ 对应的价值为 $q_* \left(a\right)$,即给定动作 $a$ 时收益的期望: $$ q_* \left(a\right) = \mathbb{E} \left[R_t | A_t = a\right] $$ 我们将对动作 $a$ 在时刻 $t$ 的价值的估计记做 $Q_t \left(a\right)$,我们希望它接近 $q_* \left(a\right)$。 如果持续对动作的价值进行估计,那么在任一时刻都会至少有一个动作的估计价值是最高的,将这些对应最高估计价值的动作成为贪心的动作。当从这些动作中选择时,称此为开发当前所知道的关于动作的价值的知识。如果不是如此,而是选择非贪心的动作,称此为试探,因为这可以让你改善对非贪心动作的价值的估计。“开发”对于最大化当前这一时刻的期望收益是正确的做法,但是“试探”从长远来看可能会带来总体收益的最大化。到底选择“试探”还是“开发”一种复杂的方式依赖于我们得到的函数估计、不确定性和剩余时刻的精确数值。 动作价值估计方法 我们以一种自然的方式,就是通过计算实际收益的平均值来估计动作的价值: $$ \begin{aligned} Q_t \left(a\right) &= \dfrac{t \text{ 时刻前执行动作 } a \text{ 得到的收益总和 }}{t \text{ 时刻前执行动作 } a \text{ 的次数}} \\ &= \dfrac{\sum_{i=1}^{t-1}{R_i \cdot \mathbb{1}_{A_i = a}}}{\sum_{i=1}^{t-1}{\mathbb{1}_{A_i = a}}} \end{aligned} $$ 其中,$\mathbb{1}_{\text{predicate}}$ 表示随机变量,当 predicate 为真时其值为 1,反之为 0。当分母为 0 时,$Q_t \left(a\right) = 0$,当分母趋向无穷大时,根据大数定律,$Q_t \left(a\right)$ 会收敛到 $q_* \left(a\right)$。这种估计动作价值的方法称为采样平均方法,因为每一次估计都是对相关收益样本的平均。 当然,这只是估计动作价值的一种方法,而且不一定是最好的方法。例如,我们也可以利用累积遗憾(Regret)来评估动作的价值: $$ \rho = T \mu^* - \sum_{t=1}^{T} \hat{r}_t $$ 其中,$\mu^* = \mathop{\max}_{k} \left\{\mu_k\right\}$ 为最大的回报,$\hat{r}_t$ 为 $t$ 时刻的回报。 多臂赌博机算法 以 10 臂赌博机为例,动作的收益分布如下图所示: 动作的真实价值 $q_* \left(a\right), a = 1, \cdots, 10$ 为从一个均值为 0 方差为 1 的标准正态分布中选择。当对于该问题的学习方法在 $t$ 时刻选择 $A_t$ 时,实际的收益 $R_t$ 则由一个均值为 $q_* \left(A_t\right)$ 方差为 1 的正态分布决定。 $\epsilon$-Greedy $\epsilon$-Greedy 采用的动作选择逻辑如下: 确定一个 $\epsilon \in \left(0, 1\right)$。 每次以 $\epsilon$ 的概率随机选择一个臂,以 $1 - \epsilon$ 选择平均收益最大的那个臂。 下图分别展示了 $\epsilon = 0$(贪婪),$\epsilon = 0.01$ 和 $\epsilon = 0.1$ 三种情况下的平均收益和最优动作占比随训练步数的变化情况。 $\epsilon$-Greedy 相比于 $\epsilon = 0$(贪婪)算法的优势如下: 对于更大方差的收益,找到最优的动作需要更多次的试探。 对于非平稳的任务,即动作的真实价值会随着时间而改变,这种情况下即使有确定性的情况下,也需要进行试探。 令 $R_i$ 表示一个动作被选择 $i$ 次后获得的收益,$Q_n$ 表示被选择 $n - 1$ 次后它的估计的动作价值,其可以表示为增量计算的形式: $$ \begin{aligned} Q_{n+1} &= \dfrac{1}{n} \sum_{i=1}^{n}{R_i} \\ &= \dfrac{1}{n} \left(R_n + \sum_{i=1}^{n-1}{R_i}\right) \\ &= \dfrac{1}{n} \left(R_n + \left(n - 1\right) \dfrac{1}{n-1} \sum_{i=1}^{n-1}{R_i}\right) \\ &= \dfrac{1}{n} \left(R_n + \left(n - 1\right) Q_n\right) \\ &= \dfrac{1}{n} \left(R_n + n Q_n - Q_n\right) \\ &= Q_n + \dfrac{1}{n} \left[R_n - Q_n\right] \end{aligned} $$ 上述我们讨论的都是平稳的问题,即收益的概率分布不随着时间变化的赌博机问题。对于非平稳的问题,给近期的收益赋予比过去更高的权值是一个合理的处理方式。则收益均值 $Q_n$ 的增量更新规则为: $$ \begin{aligned} Q_{n+1} &= Q_n + \alpha \left[R_n - Q_n\right] \\ &= \left(1 - \alpha\right)^n Q_1 + \sum_{i=1}^{n} \alpha \left(1 - \alpha\right)^{n-i} R_i \end{aligned} $$ 赋给收益 $R_i$ 的权值 $\alpha \left(1 - \alpha\right)^{n-i}$ 依赖于它被观测到的具体时刻和当前时刻的差,权值以指数形式递减,因此这个方法也称之为指数近因加权平均。 上述讨论中所有方法都在一定程度上依赖于初始动作值 $Q_1 \left(a\right)$ 的选择。从统计学角度,初始估计值是有偏的,对于平均采样来说,当所有动作都至少被选择一次时,偏差会消失;对于步长为常数的情况,偏差会随时间而减小。 下图展示了不同初始动作估计值下最优动作占比随训练步数的变化情况: 设置较大初始动作估计值会鼓励进行试探,这种方法称之为乐观初始价值,该方法在平稳问题中非常有效。 UCB $\epsilon$-Greedy 在进行尝试时是盲目地选择,因为它不大会选择接近贪心或者不确定性特别大的动作。在非贪心动作中,最好是根据它们的潜力来选择可能事实上是最优的动作,这要考虑它们的估计有多接近最大值,以及这些估计的不确定性。 一种基于置信度上界(Upper Confidence Bound,UCB)思想的选择动作依据如下: $$ A_t = \mathop{\arg\max}_{a} \left[Q_t \left(a\right) + c \sqrt{\dfrac{\ln t}{N_t \left(a\right)}}\right] $$ 其中,$N_t \left(a\right)$ 表示在时刻 $t$ 之前动作 $a$ 被选择的次数,$c > 0$ 用于控制试探的程度。平方根项是对 $a$ 动作值估计的不确定性或方差的度量,最大值的大小是动作 $a$ 的可能真实值的上限,参数 $c$ 决定了置信水平。 下图展示了 UCB 算法和 $\epsilon$-Greedy 算法平均收益随着训练步数的变化: 梯度赌博机算法 针对每个动作 $a$,考虑学习一个数值化的偏好函数 $H_t \left(a\right)$,偏好函数越大,动作就约频繁地被选择,但偏好函数的概念并不是从“收益”的意义上提出的。基于随机梯度上升的思想,在每个步骤中,在选择动作 $A_t$ 并获得收益 $R_t$ 之后,偏好函数会按如下方式更新: $$ \begin{aligned} H_{t+1} \left(A_t\right) &eq H_t \left(A_t\right) + \alpha \left(R_t - \bar{R}_t\right) \left(1 - \pi_t \left(A_t\right)\right) \\ H_{t+1} \left(a\right) &eq H_t \left(a\right) - \alpha \left(R_t - \bar{R}_t\right) \pi_t \left(a\right) \end{aligned} $$ 其中,$\alpha > 0$ 表示步长,$\bar{R}_t \in \mathbb{R}$ 表示时刻 $t$ 内所有收益的平均值。$\bar{R}_t$ 项作为比较收益的一个基准项,如果收益高于它,那么在未来选择动作 $A_t$ 的概率就会增加,反之概率就会降低,未选择的动作被选择的概率会上升。 下图展示了在 10 臂测试平台问题的变体上采用梯度赌博机算法的结果,在这个问题中,它们真实的期望收益是按照平均值为 4 而不是 0 的正态分布来选择的。所有收益的这种变化对梯度赌博机算法没有任何影响,因为收益基准项让它可以马上适应新的收益水平,如果没有基准项,那么性能将显著降低。 算法性能比较 $\epsilon$-Greedy 方法在一段时间内进行随机的动作选择;UCB 方法虽然采用确定的动作选择,但可以通过每个时刻对具有较少样本的动作进行优先选择来实现试探;梯度赌博机算法则不估计动作价值,而是利用偏好函数,使用 softmax 分布来以一种分级的、概率式的方式选择更优的动作;简单地将收益的初值进行乐观的设置,可以让贪心方法也能进行显示试探。 下图展示了上述算法在不同参数下的平均收益,每条算法性能曲线都看作一个自己参数的函数。$x$ 轴上参数值的变化是 2 的倍数,并以对数坐标轴进行表示。 在评估方法时,不仅要关注它在最佳参数设置上的表现,还要注意它对参数值的敏感性。总的来说,在本文的问题上,UCB 表现最好。 Sutton, R. S., & Barto, A. G. (2018). Reinforcement learning: An introduction. MIT press. ↩︎ https://cosx.org/2017/05/bandit-and-recommender-systems ↩︎

2020/5/16
articleCard.readMore

强化学习简介 (Introduction of Reinforcement Learning)

本文为《强化学习系列》文章 强化学习简介 强化学习(Reinforcement Learning,RL)是机器学习中的一个领域,是学习“做什么(即如何把当前的情景映射成动作)才能使得数值化的收益信号最大化”。学习者不会被告知应该采取什么动作,而是必须自己通过尝试去发现哪些动作会产生最丰厚的收益。 强化学习同机器学习领域中的有监督学习和无监督学习不同,有监督学习是从外部监督者提供的带标注训练集中进行学习(任务驱动型),无监督学习是一个典型的寻找未标注数据中隐含结构的过程(数据驱动型)。强化学习是与两者并列的第三种机器学习范式,强化学习带来了一个独有的挑战——“试探”与“开发”之间的折中权衡,智能体必须开发已有的经验来获取收益,同时也要进行试探,使得未来可以获得更好的动作选择空间(即从错误中学习)。 在强化学习中,有两个可以进行交互的对象:智能体(Agnet)和环境(Environment): 智能体:可以感知环境的状态(State),并根据反馈的奖励(Reward)学习选择一个合适的动作(Action),来最大化长期总收益。 环境:环境会接收智能体执行的一系列动作,对这一系列动作进行评价并转换为一种可量化的信号反馈给智能体。 除了智能体和环境之外,强化学习系统有四个核心要素:策略(Policy)、回报函数(收益信号,Reward Function)、价值函数(Value Function)和环境模型(Environment Model),其中环境模型是可选的。 策略:定义了智能体在特定时间的行为方式。策略是环境状态到动作的映射。 回报函数:定义了强化学习问题中的目标。在每一步中,环境向智能体发送一个称为收益的标量数值。 价值函数:表示了从长远的角度看什么是好的。一个状态的价值是一个智能体从这个状态开始,对将来累积的总收益的期望。 环境模型:是一种对环境的反应模式的模拟,它允许对外部环境的行为进行推断。 强化学习是一种对目标导向的学习与决策问题进行理解和自动化处理的计算方法。它强调智能体通过与环境的直接互动来学习,而不需要可效仿的监督信号或对周围环境的完全建模,因而与其他的计算方法相比具有不同的范式。 强化学习使用马尔可夫决策过程的形式化框架,使用状态,动作和收益定义学习型智能体与环境的互动过程。这个框架力图简单地表示人工智能问题的若干重要特征,这些特征包含了对因果关系的认知,对不确定性的认知,以及对显式目标存在性的认知。 价值与价值函数是强化学习方法的重要特征,价值函数对于策略空间的有效搜索来说十分重要。相比于进化方法以对完整策略的反复评估为引导对策略空间进行直接搜索,使用价值函数是强化学习方法与进化方法的不同之处。 示例和应用 以经典的 Flappy Bird 游戏为例,智能体就是游戏中我们操作的小鸟,整个游戏中的天空和遮挡管道即为环境,动作为玩家单击屏幕使小鸟飞起的行为,如下图所示: 目前,强化学习在包括游戏,广告和推荐,对话系统,机器人等多个领域均展开了广泛的应用。 游戏 AlphaGo 1 是于 2014 年开始由英国伦敦 Google DeepMind 开发的人工智能围棋软件。AlphaGo 使用蒙特卡洛树搜索(Monte Carlo tree search),借助估值网络(value network)与走棋网络(policy network)这两种深度神经网络,通过估值网络来评估大量选点,并通过走棋网络选择落点。 AlphaStar 2 3 是由 DeepMind 开发的玩 星际争霸 II 游戏的人工智能程序。AlphaStar 是由一个深度神经网路生成的,它接收来自原始游戏界面的输入数据,并输出一系列指令,构成游戏中的一个动作。 更具体地说,神经网路体系结构将 Transformer 框架运用于模型单元(类似于关系深度强化学习),结合一个深度 LSTM 核心、一个带有 pointer network 的自回归策略前端和一个集中的值基线。这种先进的模型将有助于解决机器学习研究中涉及长期序列建模和大输出空间(如翻译、语言建模和视觉表示)的许多其他挑战。 AlphaStar 还使用了一种新的多智能体学习算法。该神经网路最初是通过在 Blizzard 发布的匿名人类游戏中进行监督学习来训练的。这使得 AlphaStar 能够通过模仿学习星际争霸上玩家所使用的基本微观和宏观策略。这个初级智能体在 95% 的游戏中击败了内置的「精英」AI 关卡(相当于人类玩家的黄金级别)。 OpenAI Five 4 是一个由 OpenAI 开发的用于多人视频游戏 Dota 2 的人工智能程序。OpenAI Five 通过与自己进行超过 10,000 年时长的游戏进行优化学习,最终获得了专家级别的表现。 Pluribus 5 是由 Facebook 开发的第一个在六人无限注德州扑克中击败人类专家的 AI 智能程序,其首次在复杂游戏中击败两个人或两个团队。 广告和推荐 对话系统 机器人 开放资源 开源实验平台 openai/gym MuJoCo openai/mujoco-py deepmind/lab 开源框架 deepmind/trfl/ deepmind/open_spiel google/dopamine tensorflow/agents keras-rl/keras-rl tensorforce/tensorforce facebookresearch/ReAgent thu-ml/tianshou astooke/rlpyt NervanaSystems/coach PaddlePaddle/PARL 开源模型 dennybritz/reinforcement-learning openai/baselines 其他资源 ShangtongZhang/reinforcement-learning-an-introduction aikorea/awesome-rl openai/spinningup udacity/deep-reinforcement-learning https://deepmind.com/research/case-studies/alphago-the-story-so-far ↩︎ https://deepmind.com/blog/article/alphastar-mastering-real-time-strategy-game-starcraft-ii ↩︎ https://deepmind.com/blog/article/AlphaStar-Grandmaster-level-in-StarCraft-II-using-multi-agent-reinforcement-learning ↩︎ https://openai.com/projects/five/ ↩︎ https://ai.facebook.com/blog/pluribus-first-ai-to-beat-pros-in-6-player-poker/ ↩︎

2020/5/9
articleCard.readMore

在群晖 NAS 上编译安装 tmux

工具链安装 登录 NAS 控制台,在系统根目录创建 toolkit 目录: sudo mkdir /toolkit sudo chown -R username:users /toolkit 其中 username 为使用的用户名,如果后续使用过程中出现磁盘空间不足的问题,可以在其他具有较大容量的分区建立 toolkit,再在根目录建立软链进行使用: mkdir /xxx/toolkit sudo ln -s /xxx/toolkit /toolkit sudo chown -R username:users /toolkit 之后下载相关工具脚本: cd /toolkit git clone https://github.com/SynologyOpenSource/pkgscripts-ng.git 工具脚本使用 Python 3 实现,请确保 NAS 已经安装 Python 3,后续使用过程中如果提示相关 Python 扩展包未安装的情况请自行安装后重试。实验的 Synology NAS 为 DS418play,系统版本为 DSM 6.2.2 系统,处理器为 INTEL Celeron J3355 处理器(产品代号:Apollo Lake),首先利用 EnvDeploy 下载所需的编译环境: cd /toolkit/pkgscripts-ng sudo ./EnvDeploy -v 6.2 -p apollolake 请根据自己机器的系统版本和处理器类型自行调整 -v 和 -p 参数。如果下载速度较慢可以手动从 https://sourceforge.net/projects/dsgpl/files/toolkit/DSM6.2/ 下载下列文件: base_env-6.2.txz ds.apollolake-6.2.dev.txz ds.apollolake-6.2.env.txz 将其放置到 /toolkit/toolkit_tarballs 目录中,然后通过如下命令进行部署安装: sudo ./EnvDeploy -v 6.2 -p apollolake -t /toolkit/toolkit_tarballs 编译 tmux 在 /toolkit 目录下建立 source 文件夹,并将 tmux 源代码(本文以 3.1b 版本为例)下载到该文件夹中: cd /toolkit mkdir source cd source wget https://github.com/tmux/tmux/releases/download/3.1b/tmux-3.1b.tar.gz tar -zxvf tmux-3.1b.tar.gz mv tmux-3.1b tmux 在 tmux 源代码根目录中建立 SynoBuildConf 文件夹,并在文件夹中创建如下文件: cd /toolkit/source/tmux mkdir SynoBuildConf build #!/bin/bash case ${MakeClean} in [Yy][Ee][Ss]) make distclean ;; esac NCURSES_INCS="$(pkg-config ncurses --cflags)" NCURSES_LIBS="$(pkg-config ncurses --libs)" CFLAGS="${CFLAGS} ${NCURSES_INCS}" LDFLAGS="${LDFLAGS} ${NCURSES_LIBS}" env CC="${CC}" AR="${AR}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" \ ./configure ${ConfigOpt} make ${MAKE_FLAGS} depends [default] all="6.2" install #!/bin/bash PKG_NAME="tmux" TGZ_DIR="/tmp/_${PKG_NAME}_tgz" PKG_DIR="/tmp/_${PKG_NAME}_pkg" PKG_DEST="/image/packages" source /pkgscripts-ng/include/pkg_util.sh create_package_tgz() { ## clear destination directory for dir in $TGZ_DIR $PKG_DIR; do rm -rf "$dir" done for dir in $TGZ_DIR $PKG_DIR; do mkdir -p "$dir" done ## install needed file into TGZ_DIR DESTDIR="${TGZ_DIR}" make install ## create package.tgz pkg_make_package $TGZ_DIR $PKG_DIR } create_package_spk(){ ## Copy package center scripts to PKG_DIR cp -r synology/scripts/ $PKG_DIR ## Copy package icon cp -av synology/PACKAGE_ICON*.PNG $PKG_DIR ## Generate INFO file synology/INFO.sh > INFO cp INFO $PKG_DIR ## Create the final spk. mkdir -p $PKG_DEST pkg_make_spk $PKG_DIR $PKG_DEST } main() { create_package_tgz create_package_spk } main "$@" 在 tmux 源代码根目录中建立 synology 文件夹,并在文件夹中创建如下文件: cd /toolkit/source/tmux mkdir synology INFO.sh #!/bin/sh . /pkgscripts-ng/include/pkg_util.sh package="tmux" version="3.1b" displayname="tmux" arch="$(pkg_get_platform) " maintainer="tmux" maintainer_url="https://github.com/tmux" distributor="Leo Van" distributor_url="https://leovan.me" description="tmux is a terminal multiplexer: it enables a number of terminals to be created, accessed, and controlled from a single screen. tmux may be detached from a screen and continue running in the background, then later reattached." support_url="https://github.com/tmux/tmux" thirdparty="yes" startable="no" silent_install="yes" silent_upgrade="yes" silent_uninstall="yes" [ "$(caller)" != "0 NULL" ] && return 0 pkg_dump_info 并为其添加运行权限: cd /toolkit/source/tmux/scripts chmod u+x INFO.sh 下载 tmux 图标并将其重命名: cd /toolkit/source/tmux/synology wget https://raw.githubusercontent.com/tmux/tmux/master/logo/tmux-logo-huge.png convert tmux-logo-huge.png -crop 480x480+0+0 -resize 72x PACKAGE_ICON.PNG convert tmux-logo-huge.png -crop 480x480+0+0 -resize 256x PACKAGE_ICON_256.PNG 此处需要使用 ImageMagick 对图标进行裁剪和缩放,请自行安装,或在本地对图片进行处理后上传到指定目录。在 /toolkit/source/tmux/synology 目录中建立 scripts 文件夹,并在文件夹中创建如下文件: cd /toolkit/source/tmux/synology mkdir scripts postinst #!/bin/sh ln -sf "$SYNOPKG_PKGDEST/usr/local/bin/tmux" /usr/bin/ postuninst #!/bin/sh rm -f /usr/local/bin/tmux rm -f /usr/bin/tmux postupgrade #!/bin/sh exit 0 preinst #!/bin/sh exit 0 preuninst #!/bin/sh exit 0 preupgrade #!/bin/sh exit 0 start-stop-status #!/bin/sh case $1 in start) exit 0 ;; stop) exit 0 ;; status) if [ -h "/usr/bin/tmux" ]; then exit 0 else exit 1 fi ;; killall) ;; log) exit 0 ;; esac 为所有文件添加运行权限: cd /toolkit/source/tmux/synology/scripts chmod u+x * 利用 PkgCreate.py 构建 tmux 扩展包: sudo ./PkgCreate.py -v 6.2 -p apollolake tmux 最终构建完毕的扩展包位于 /toolkit/build_env/ds.apollolake-6.2/image/packages 中。 安装 tmux 在 /toolkit/build_env/ds.apollolake-6.2/image/packages 目录中有两个编译好的扩展包,分别是 tmux-apollolake-3.1b_debug.spk 和 tmux-apollolake-3.1b.spk。其中 tmux-apollolake-3.1b.spk 为 Release 版本,传输到本地,通过 NAS 的套件中心手动安装即可。安装完毕后,套件中心的“已安装”会出现 tmux,如下图所示: 进入 NAS 控制台,运行 tmux -V 可以得到安装好的 tmux 版本信息: tmux 3.1b 本文主要参考了 Synology 官方的扩展包构建指南:https://help.synology.com/developer-guide/create_package/index.html

2020/5/7
articleCard.readMore

隐马尔可夫 (Hidden Markov Model, HMM),条件随机场 (Conditional Random Fields, CRF) 和序列标注 (Sequence Labeling)

隐马尔可夫 隐马尔可夫模型(Hidden Markov Model,HMM)是一个描述包含隐含未知参数的马尔可夫过程的统计模型。马尔可夫过程(Markov Process)是因俄国数学家安德雷·安德耶维齐·马尔可夫(Андрей Андреевич Марков)而得名一个随机过程,在该随机过程中,给定当前状态和过去所有状态的条件下,其下一个状态的条件概率分布仅依赖于当前状态,通常具备离散状态的马尔可夫过程称之为马尔可夫链(Markov Chain)。因此,马尔可夫链可以理解为一个有限状态机,给定了当前状态为 $S_i$ 时,下一时刻状态为 $S_j$ 的概率,不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 $S_a, S_b, S_c$ 之间转换状态的马尔可夫链。 隐马尔可夫模型中包含两种序列:随机生成的状态构成的序列称之为状态序列(state sequence),状态序列是不可被观测到的;每个状态对应的观测值组成的序列称之为观测序列(observation sequence)。令 $I = \left(i_1, i_2, \cdots, i_T\right)$ 为状态序列,其中 $i_t$ 为第 $t$ 时刻系统的状态值,对应的有 $O = \left(o_1, o_2, \cdots, o_T\right)$ 为观测序列,其中 $o_t$ 为第 $t$ 时刻系统的观测值,系统的所有可能的状态集合为 $Q = \{q_1, q_2, \cdots, q_N\}$,所有可能的观测集合为 $V= \{v_1, v_2, \cdots, v_M\}$。 隐马尔可夫模型主要由三组参数构成: 状态转移矩阵: $$ A = \left[a_{ij}\right]_{N \times N} $$ 其中, $$ a_{ij} = P \left(i_{t+1} = q_j | i_t = q_i\right), 1 \leq i, j \leq N $$ 表示 $t$ 时刻状态为 $q_i$ 的情况下,在 $t+1$ 时刻状态转移到 $q_j$ 的概率。 观测概率矩阵: $$ B = \left[b_j \left(k\right)\right]_{N \times M} $$ 其中, $$ b_j \left(k\right) = P \left(o_t = v_k | i_t = q_j\right), k = 1, 2, \cdots, M, j = 1, 2, \cdots, N $$ 表示 $t$ 时刻状态为 $q_i$ 的情况下,观测值为 $v_k$ 的概率。 初始状态概率向量: $$ \pi = \left(\pi_i\right) $$ 其中, $$ \pi_i = P \left(i_1 = q_i\right), i = 1, 2, \cdots, N $$ 表示 $t = 1$ 时刻,系统处于状态 $q_i$ 的概率。 初始状态概率向量 $\pi$ 和状态转移矩阵 $A$ 决定了状态序列,观测概率矩阵 $B$ 决定了状态序列对应的观测序列,因此马尔可夫模型可以表示为: $$ \lambda = \left(A, B, \pi\right) $$ 对于马尔可夫模型 $\lambda = \left(A, B, \pi\right)$,通过如下步骤生成观测序列 $\{o_1, o_2, \cdots, o_T\}$: 按照初始状态分布 $\pi$ 产生状态 $i_1$. 令 $t = 1$。 按照状态 $i_t$ 的观测概率分布 $b_{i_t} \left(k\right)$ 生成 $o_t$。 按照状态 $i_t$ 的状态转移概率分布 $\left\{a_{i_t i_{t+1}}\right\}$ 产生状态 $i_{t+1}$,$i_{t+1} = 1, 2, \cdots, N$。 令 $t = t + 1$,如果 $t < T$,转步骤 3;否则,终止。 马尔可夫模型在应用过程中有 3 个基本问题 1: 概率计算问题。给定模型 $\lambda = \left(A, B, \pi\right)$ 和观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,计算在模型 $\lambda$ 下观测序列 $O$ 出现的概率 $P\left(O | \lambda \right)$。 学习问题。已知观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,估计模型 $\lambda = \left(A, B, \pi\right)$ 参数,使得在该模型下观测序列概率 $P\left(X | \lambda \right)$ 最大。即用极大似然估计的方法估计参数。 预测问题,也称为解码(decoding)问题。已知模型 $\lambda = \left(A, B, \pi\right)$ 和观测序列 $O = \{o_1, o_2, \cdots, o_T\}$,求对给定观测序列条件概率 $P \left(I | O\right)$ 最大的状态序列 $I = \{i_1, i_2, \cdots, i_T\}$。即给定观测序列,求最有可能的对应的状态序列。 概率计算 直接计算法 给定模型 $\lambda = \left(A, B, \pi \right)$ 和观测序列 $O = \{o_1, o_2, ..., o_T\}$,计算在模型 $\lambda$ 下观测序列 $O$ 出现的概率 $P\left(O | \lambda \right)$。最简单的办法就是列举出左右可能的状态序列 $I = \{i_1, i_2, ..., i_T\}$,再根据观测概率矩阵 $B$,计算每种状态序列对应的联合概率 $P \left(O, I | \lambda\right)$,对其进行求和得到概率 $P\left(O | \lambda \right)$。 状态序列 $I = \{i_1, i_2, ..., i_T\}$ 的概率是: $$ P \left(I | \lambda \right) = \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} $$ 对于固定的状态序列 $I = \{i_1, i_2, ..., i_T\}$,观测序列 $O = \{o_1, o_2, ..., o_T\}$ 的概率是: $$ P \left(O | I, \lambda \right) = \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) $$ $O$ 和 $I$ 同时出现的联合概率为: $$ \begin{split} P \left(O, I | \lambda \right) &= P \left(O | I, \lambda \right) P \left(I | \lambda \right) \\ &= \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$ 然后,对于所有可能的状态序列 $I$ 求和,得到观测序列 $O$ 的概率 $P \left(O | \lambda\right)$,即: $$ \begin{split} P\left(O | \lambda \right) &= \sum_{I} P \left(O | I, \lambda \right) P \left(I | \lambda \right) \\ &= \sum_{i_1, i_2, \cdots, i_T} \pi_{y_1} \prod_{t = 1}^{T - 1} a_{{i_t}{i_{t+1}}} \prod_{t = 1}^{T} b_{i_t} \left(o_t\right) \end{split} $$ 但利用上式的计算量很大,是 $O \left(T N^T\right)$ 阶的,这种算法不可行。 前向算法 前向概率:给定马尔可夫模型 $\lambda$,给定到时刻 $t$ 部分观测序列为 $o_1, o_2, \cdots, o_t$ 且状态为 $q_i$ 的概率为前向概率,记作: $$ \alpha_t \left(i\right) = P \left(o_1, o_2, \cdots, o_t, i_t = q_i | \lambda\right) $$ 可以递推地求得前向概率 $\alpha_t \left(i\right)$ 及观测序列概率 $P \left(O | \lambda\right)$,前向算法如下: 初值 $$ \alpha_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N $$ 递推,对 $t = 1, 2, \cdots, T-1$ $$ \alpha_{t+1}(i)=\left[\sum_{j=1}^{N} \alpha_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N $$ 终止 $$ P(O | \lambda)=\sum_{i=1}^{N} \alpha_{T}(i) $$ 后向算法 后向概率:给定隐马尔可夫模型 $\lambda$,给定在时刻 $t$ 状态为 $q_i$ 的条件下,从 $t+1$ 到 $T$ 的部分观测序列为 $o_{t+1}, o_{t+2}, \cdots, o_T$ 的概率为后向概率,记作: $$ \beta_{t}(i)=P\left(o_{t+1}, o_{t+2}, \cdots, o_{T} | i_{t}=q_{i}, \lambda\right) $$ 可以递推地求得后向概率 $\alpha_t \left(i\right)$ 及观测序列概率 $P \left(O | \lambda\right)$,后向算法如下: 初值 $$ \beta_{T}(i)=1, \quad i=1,2, \cdots, N $$ 递推,对 $t = T-1, T-2, \cdots, 1$ $$ \beta_{t}(i)=\sum_{j=1}^{N} a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j), \quad i=1,2, \cdots, N $$ 终止 $$ P(O | \lambda)=\sum_{i=1}^{N} \pi_{i} b_{i}\left(o_{1}\right) \beta_{1}(i) $$ 学习算法 监督学习算法 假设以给训练数据包含 $S$ 个长度相同的观测序列和对应的状态序列 $\left\{\left(O_1, I_1\right), \left(O_2, I_2\right), \cdots, \left(O_S, I_S\right)\right\}$,那么可以利用极大似然估计法来估计隐马尔可夫模型的参数。 设样本中时刻 $t$ 处于状态 $i$ 时刻 $t+1$ 转移到状态 $j$ 的频数为 $A_{ij}$,那么转移概率 $a_{ij}$ 的估计是: $$ \hat{a}_{i j}=\frac{A_{i j}}{\sum_{j=1}^{N} A_{i j}}, \quad i=1,2, \cdots, N ; \quad j=1,2, \cdots, N $$ 设样本中状态为 $j$ 并观测为 $k$ 的频数是 $B_{jk}$,那么状态为 $j$ 观测为 $k$ 的概率 $b_j \left(k\right)$ 的估计是: $$ \hat{b}_{j}(k)=\frac{B_{j k}}{\sum_{k=1}^{M} B_{j k}}, \quad j=1,2, \cdots, N ; \quad k=1,2, \cdots, M $$ 初始状态概率 $\pi_i$ 的估计 $\hat{\pi}_i$ 为 $S$ 个样本中初始状态为 $q_i$ 的频率。 无监督学习算法 假设给定训练数据值包含 $S$ 个长度为 $T$ 的观测序列 $\left\{O_1, O_2, \cdots, O_S\right\}$ 而没有对应的状态序例,目标是学习隐马尔可夫模型 $\lambda = \left(A, B, \pi\right)$ 的参数。我们将观测序列数据看做观测数据 $O$,状态序列数据看作不可观测的隐数据 $I$,那么马尔可夫模型事实上是一个含有隐变量的概率模型: $$ P(O | \lambda)=\sum_{I} P(O | I, \lambda) P(I | \lambda) $$ 它的参数学习可以由 EM 算法实现。EM 算法在隐马尔可夫模型学习中的具体实现为 Baum-Welch 算法: 初始化。对 $n = 0$,选取 $a_{i j}^{(0)}, b_{j}(k)^{(0)}, \pi_{i}^{(0)}$,得到模型 $\lambda^{(0)}=\left(A^{(0)}, B^{(0)}, \pi^{(0)}\right)$。 递推。对 $n = 1, 2, \cdots$: $$ \begin{aligned} a_{i j}^{(n+1)} &= \frac{\sum_{t=1}^{T-1} \xi_{t}(i, j)}{\sum_{t=1}^{T-1} \gamma_{t}(i)} \\ b_{j}(k)^{(n+1)} &= \frac{\sum_{t=1, o_{t}=v_{k}}^{T} \gamma_{t}(j)}{\sum_{t=1}^{T} \gamma_{t}(j)} \\ \pi_{i}^{(n+1)} &= \gamma_{1}(i) \end{aligned} $$ 右端各按照观测 $O=\left(o_{1}, o_{2}, \cdots, o_{T}\right)$ 和模型 $\lambda^{(n)}=\left(A^{(n)}, B^{(n)}, \pi^{(n)}\right)$ 计算, $$ \begin{aligned} \gamma_{t}(i) &= \frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} \\ \xi_{t}(i, j) &= \frac{\alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)}{\sum_{i=1}^{N} \sum_{j=1}^{N} \alpha_{t}(i) a_{i j} b_{j}\left(o_{t+1}\right) \beta_{t+1}(j)} \end{aligned} $$ 终止。得到模型参数 $\lambda^{(n+1)}=\left(A^{(n+1)}, B^{(n+1)}, \pi^{(n+1)}\right)$。 预测算法 近似算法 近似算法的思想是,在每个时刻 $t$ 选择在该时刻最有可能出现的状态 $i_t^*$,从而得到一个状态序列 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$,将它作为预测的结果。给定隐马尔可夫模型 $\lambda$ 和观测序列 $O$,在时刻 $t$ 处于状态 $q_i$ 的概率 $\gamma_t \left(i\right)$ 是: $$ \gamma_{t}(i)=\frac{\alpha_{t}(i) \beta_{t}(i)}{P(O | \lambda)}=\frac{\alpha_{t}(i) \beta_{t}(i)}{\sum_{j=1}^{N} \alpha_{t}(j) \beta_{t}(j)} $$ 在每一时刻 $t$ 最有可能的状态 $i_t^*$ 是: $$ i_{t}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\gamma_{t}(i)\right], \quad t=1,2, \cdots, T $$ 从而得到状态序列 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$。 近似算法的优点是计算简单,其缺点是不能保证预测的状态序列整体是最有可能的状态序列,因为预测的状态序列可能有实际不发生的部分。事实上,上述方法得到的状态序列中有可能存在转移概率为0的相邻状态,即对某些 $i, j, a_{ij} = 0$ 。尽管如此,近似算法仍然是有用的。 维特比算法 维特比算法(Viterbi Algorithm)实际是用动态规划(Dynamic Programming)解隐马尔可夫模型预测问题,即用动态规划求概率最大路径(最优路径)。这时一条路径对应着一个状态序列。 首先导入两个变量 $\sigma$ 和 $\Psi$。定义在时刻 $t$ 状态为 $i$ 的所有单个路径 $\left(i_1, i_2, \cdots, i_t\right)$ 中概率最大值为: $$ \delta_{t}(i)=\max _{i_{1}, i_{2}, \cdots, i_{t-1}} P\left(i_{t}=i, i_{t-1}, \cdots, i_{1}, o_{t}, \cdots, o_{1} | \lambda\right), \quad i=1,2, \cdots, N $$ 由定义可得变量 $\sigma$ 的递推公式: $$ \begin{aligned} \delta_{t+1}(i) &=\max _{i_{1}, i_{2}, \cdots, i_{t}} P\left(i_{t+1}=i, i_{t}, \cdots, i_{1}, o_{t+1}, \cdots, o_{1} | \lambda\right) \\ &=\max _{1 \leqslant j \leqslant N}\left[\delta_{t}(j) a_{j i}\right] b_{i}\left(o_{t+1}\right), \quad i=1,2, \cdots, N ; \quad t=1,2, \cdots, T-1 \end{aligned} $$ 定义在时刻 $t$ 状态为 $i$ 的所有单个路径 $\left(i_1, i_2, \cdots, i_{t-1}, i\right)$ 中概率最大的路径的第 $t - 1$ 个结点为: $$ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N $$ 维特比算法流程如下: 初始化 $$ \begin{array}{c} \delta_{1}(i)=\pi_{i} b_{i}\left(o_{1}\right), \quad i=1,2, \cdots, N \\ \Psi_{1}(i)=0, \quad i=1,2, \cdots, N \end{array} $$ 递推。对 $t = 2, 3, \cdots, T$ $$ \begin{array}{c} \delta_{t}(i)=\max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right] b_{i}\left(o_{t}\right), \quad i=1,2, \cdots, N \\ \Psi_{t}(i)=\arg \max _{1 \leqslant j \leqslant N}\left[\delta_{t-1}(j) a_{j i}\right], \quad i=1,2, \cdots, N \end{array} $$ 终止。 $$ \begin{array}{c} P^{*}=\max _{1 \leqslant i \leqslant N} \delta_{T}(i) \\ i_{T}^{*}=\arg \max _{1 \leqslant i \leqslant N}\left[\delta_{T}(i)\right] \end{array} $$ 最优路径回溯。对 $t = T - 1, T - 2, \cdots, 1$ $$ i_{t}^{*}=\Psi_{t+1}\left(i_{t+1}^{*}\right) $$ 求的最优路径 $I^{*}=\left(i_{1}^{*}, i_{2}^{*}, \cdots, i_{T}^{*}\right)$。 条件随机场 概率无向图模型(Probabilistic Undirected Graphical Model)又称为马尔可夫随机场(Markov Random Field),是一个可以由无向图表示的联合概率分布。概率图模型(Probabilistic Graphical Model)是由图表示的概率分布,设有联合概率分布 $P \left(Y\right), Y \in \mathcal{Y}$ 是一组随机变量。由无向图 $G = \left(V, E\right)$ 表示概率分布 $P \left(Y\right)$,即在图 $G$ 中,结点 $v \in V$ 表示一个随机变量 $Y_v, Y = \left(Y_v\right)_{v \in V}$,边 $e \in E$ 表示随机变量之间的概率依赖关系。 成对马尔可夫性:设 $u$ 和 $v$ 是无向图 $G$ 中任意两个没有边连接的结点,结点 $u$ 和 $v$ 分别对应随机变量 $Y_u$ 和 $Y_v$。其他所有结点为 $O$,对应的随机变量组是 $Y_O$。成对马尔可夫是指给定随机变量组 $Y_O$ 的条件下随机变量 $Y_u$ 和 $Y_v$ 是条件独立的,即: $$ P\left(Y_{u}, Y_{v} | Y_{O}\right)=P\left(Y_{u} | Y_{O}\right) P\left(Y_{v} | Y_{O}\right) $$ 局部马尔可夫性:设 $v \in V$ 是无向图 $G$ 中任意一个结点,$W$ 是与 $v$ 有边连接的所有结点,$O$ 是 $v$ 和 $W$ 以外的其他所有结点。$v$ 表示的随机变量是 $Y_v$,$W$ 表示的随机变量组是 $Y_W$,$O$ 表示的随机变量组是 $Y_O$。局部马尔可夫性是指在给定随机变量组 $Y_W$ 的条件下随机变量 $Y_v$ 与随机变量组 $Y_O$ 是独立的,即: $$ P\left(Y_{v}, Y_{O} | Y_{W}\right)=P\left(Y_{v} | Y_{W}\right) P\left(Y_{O} | Y_{W}\right) $$ 在 $P \left(Y_O | Y_W\right) > 0$ 时,等价地: $$ P\left(Y_{v} | Y_{W}\right)=P\left(Y_{v} | Y_{W}, Y_{O}\right) $$ 局部马尔可夫性如下图所示: 全局马尔可夫性:设结点结合 $A, B$ 是在无向图 $G$ 中被结点集合 $C$ 分开的任意结点集合,如下图所示。结点集合 $A, B$ 和 $C$ 所对应的随机变量组分别是 $Y_A, Y_B$ 和 $Y_C$。全局马尔可夫性是指给定随机变量组 $Y_C$ 条件下随机变量组 $Y_A$ 和 $Y_B$ 是条件独立的,即: $$ P\left(Y_{A}, Y_{B} | Y_{C}\right)=P\left(Y_{A} | Y_{C}\right) P\left(Y_{B} | Y_{C}\right) $$ 概率无向图模型定义为:设有联合概率分布 $P \left(Y\right)$,由无向图 $G = \left(V, E\right)$ 表示,在图 $G$ 中,结点表示随机变量,边表示随机变量之间的依赖关系。如果联合概率分布 $P \left(Y\right)$ 满足成对、局部或全局马尔可夫性,就称此联合概率分布为概率无向图模型(Probabilistic Undirected Graphical Model),或马尔可夫随机场(Markov Random Field)。 团与最大团:无向图 $G$ 中任何两个结点均有边连接的结点子集称为团(Clique)。若 $C$ 是无向图 $G$ 的一个团,并且不能再加进任何一个 $G$ 的结点时期成为一个更大的团,则称此 $C$ 为最大团(Maximal Clique)。 上图表示 4 个结点组成的无向图。图中有 2 个结点组成的团有 5 个:$\left\{Y_1, Y_2\right\}$,$\left\{Y_2, Y_3\right\}$,$\left\{Y_3, Y_4\right\}$,$\left\{Y_4, Y_2\right\}$ 和 $\left\{Y_1, Y_3\right\}$。有 2 个最大团:$\left\{Y_1, Y_2, Y_3\right\}$ 和 $\left\{Y_2, Y_3, Y_4\right\}$。而 $\left\{Y_1, Y_2, Y_3, Y_4\right\}$ 不是一个团,因为 $Y_1$ 和 $Y_4$ 没有边连接。 将概率无向图模型的联合概率分布表示为其最大团上的随机变量的函数的乘积形式的操作,称为概率无向图模型的因子分解。给定无向图模型,设其无向图为 $G$,$C$ 为 $G$ 上的最大团,$Y_C$ 表示 $C$ 对应的随机变量。那么概率无向图模型的联合概率分布 $P \left(Y\right)$ 可以写作图中所有最大团 $C$ 上的函数 $\Psi_C \left(Y_C\right)$ 的乘积形式,即: $$ P(Y)=\frac{1}{Z} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$ 其中,$Z$ 是规范化因子: $$ Z=\sum_{Y} \prod_{C} \Psi_{C}\left(Y_{C}\right) $$ 规范化因子保证 $P \left(Y\right)$ 构成一个概率分布。函数 $\Psi_C \left(Y_C\right)$ 称为势函数,这里要求势函数 $\Psi_C \left(Y_C\right)$ 是严格正的,通常定义为指数函数: $$ \Psi_{C}\left(Y_{C}\right)=\exp \left\{-E\left(Y_{C}\right)\right\} $$ 概率无向图模型的因子分解由这个 Hammersley-Clifford 定理来保证。 条件随机场(Conditional Random Field)是给定随机变量 $X$ 条件下,随机变量 $Y$ 的马尔可夫随机场。设 $X$ 与 $Y$ 是随机变量,$P \left(Y | X\right)$ 是给定 $X$ 的条件下 $Y$ 的条件概率分布。若随机变量 $Y$ 构成一个有无向图 $G = \left(V, E\right)$ 表示的马尔可夫随机场,即: $$ P\left(Y_{v} | X, Y_{w}, w \neq v\right)=P\left(Y_{v} | X, Y_{w}, w \sim v\right) $$ 对任意结点 $v$ 成立,则称条件概率分布 $P \left(Y | X\right)$ 为条件随机场。其中,$w \sim v$ 表示在图 $G = \left(V, E\right)$ 中与结点 $v$ 有边连接的所有结点 $w$,$w \neq v$ 表示结点 $v$ 以外的所有结点,$Y_v, Y_u$ 与 $Y_w$ 为结点 $v, u$ 和 $w$ 对应的随机变量。 定义中并没有要求 $X$ 和 $Y$ 具有相同的结构,一般假设 $X$ 和 $Y$ 有相同的图结构,下图展示了无向图的线性链情况,即: $$ G=(V=\{1,2, \cdots, n\}, E=\{(i, i+1)\}), \quad i=1,2, \cdots, n-1 $$ 此情况下,$X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$,最大团是相邻两个结点的集合。 线性链条件随机场:设 $X=\left(X_{1}, X_{2}, \cdots, X_{n}\right), Y=\left(Y_{1}, Y_{2}, \cdots, Y_{n}\right)$ 均为线性链表示的随机变量序列,若在给定随机变量序列 $X$ 的条件下,随机变量序列 $Y$ 的条件概率分布 $P \left(Y | X\right)$ 构成条件随机场,即满足马尔可夫性: $$ \begin{array}{c} P\left(Y_{i} | X, Y_{1}, \cdots, Y_{i-1}, Y_{i+1}, \cdots, Y_{n}\right)=P\left(Y_{i} | X, Y_{i-1}, Y_{i+1}\right) \\ i=1,2, \cdots, n \quad (\text { 在 } i=1 \text { 和 } n \text { 时只考虑单边 }) \end{array} $$ 则称 $P \left(Y | X\right)$ 为线性链条件随机场。在标注问题中,$X$ 表示输入观测序列,$Y$ 表示对应的输出标记序列或状态序列。 根据 Hammersley-Clifford 定理,设 $P \left(Y | X\right)$ 为线性链条件随机场,则在随机变量 $X$ 取值为 $x$ 的条件下,随机变量 $Y$ 取值为 $y$ 的条件概率有如下形式: $$ P(y | x)=\frac{1}{Z(x)} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$ 其中, $$ Z(x)=\sum_{y} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right) $$ 其中,$t_k$ 和 $s_l$ 是特征函数,$\lambda_k$ 和 $\mu_l$ 是对应的权值。$Z \left(x\right)$ 是规范化因子,求和是在所有可能的输出序列上进行的。 条件随机场的概率计算,学习算法和预测算法类似隐马尔可夫模型,在此不进行过多赘述,有兴趣的同学可以参见 1。 综上所述,隐马尔可夫模型和条件随机场的主要联系和区别如下: HMM 是概率有向图,CRF 是概率无向图 HMM 是生成模型,CRF 是判别模型 如上图所示,上面部分为生成式模型,下面部分为判别式模型,生成式模型尝试构建联合分布 $P \left(Y, X\right)$,而判别模型则尝试构建条件分布 $P \left(Y | X\right)$。 序列标注 序列标注(Sequence Labeling)是自然语言处理中的一项重要任务,对于给定的文本序列需要给出对应的标注序列。常见的序列标注任务包含:组块分析(Chunking),词性标注(Part-of-Speech,POS)和命名实体识别(Named Entity Recognition,NER)。 上图为一段文本的词性标注和命名实体识别的结果。 词性标注 词性标注是指为分词结果中的每个单词标注一个正确的词性,即确定每个词是名词、动词、形容词或其他词性的过程。 一些常用中文标注规范如下: 北京大学现代汉语语料库基本加工规范 2 北大语料库加工规范:切分·词性标注·注音 3 计算所汉语词性标记集 3.0(ICTPOS 3.0)4 The Part-Of-Speech Tagging Guidelines for the Penn Chinese Treebank (3.0) 5 中文文本标注规范(微软亚洲研究院)6 命名实体识别 命名实体识别,又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等。简单的讲,就是识别自然文本中的实体指称的边界和类别。 常用的标注标准有 IO,BIO,BIOES,BMEWO 和 BMEWO+ 等。(参考自:Coding Chunkers as Taggers: IO, BIO, BMEWO, and BMEWO+) IO 标注标准是最简单的标注方式,对于命名实体类别 X 标注为 I_X,其他则标注为 O。由于没有标签界线表示,这种方式无法表示两个相邻的同类命名实体。 BIO 标注标准将命名实体的起始部分标记为 B_X,其余部分标记为 I_X。 BIOES 标注标准将命名实体的起始部分标记为 B_X,中间部分标记为 I_X,结尾部分标记为 E_X,对于单个字符成为命名实体的情况标记为 S_X。 BMEWO 标注标准将命名实体的起始部分标记为 B_X,中间部分标记为 M_X,结尾部分标记为 E_X,对于单个字符成为命名实体的情况标记为 W_X。 BMEWO+ 标注标准在 BMEWO 的基础上针对不同情况的非命名实体标签的标注进行了扩展,同时增加了一个句外(out-of-sentence)标签 W_OOS,句子起始标签 BB_O_OOS 和句子结束标签 WW_O_OOS,如 下表 所示: 标签 描述 可能上接的标签 可能下接的标签 B_X 命名实体类型 X 的起始 E_Y, W_Y, EE_O_X, WW_O_X M_X, W_X M_X 命名实体类型 X 的中间 B_X, M_X M_X, W_X E_X 命名实体类型 X 的结尾 B_X, M_X B_Y, W_Y, BB_O_X, WW_O_X W_X 命名实体类型 X 的单个字符 E_Y, W_Y, EE_O_X, WW_O_X B_Y, W_Y, BB_O_X, WW_O_X BB_O_X 非命名实体的起始,上接命名实体类型 X E_X, W_X MM_O, EE_O_Y MM_O 非命名实体的中间 BB_O_Y, MM_O MM_O, EE_O_Y EE_O_X 非命名实体的结尾,下接命名实体类型 X BB_O_Y, MM_O B_X, W_X WW_O_X 非命名实体,上接命名实体,下接命名实体类型 X E_X, W_X B_Y, W_Y 不同标注标准的差别示例如下: 字符 IO BIO BIOES BMEWO BMEWO+ W_OOS Yesterday O O O O BB_O_OOS afternoon O O O O MM_O , O O O O EE_O_PER John I_PER B_PER B_PER B_PER B_PER J I_PER I_PER I_PER M_PER M_PER . I_PER I_PER I_PER M_PER M_PER Smith I_PER I_PER E_PER E_PER E_PER traveled O O O O BB_O_PER to O O O O EE_O_LOC Washington I_LOC B_LOC S_LOC W_LOC W_LOC . O O O O WW_O_OOS W_OOS 不同标准的标签数量如下表所示: 标注标准 标签数量 N=1 N=3 N=20 IO N+1 2 4 21 BIO 2N+1 3 7 41 BIOES 4N+1 5 13 81 BMEWO 4N+1 5 13 81 BMEWO+ 7N+3 10 24 143 其中,N 为命名实体类型的数量。 BiLSTM CRF 7 本小节内容参考和修改自 CRF-Layer-on-the-Top-of-BiLSTM。 Huang 等人提出了一种基于 BiLSTM 和 CRF 的神经网络模型用于序例标注。整个网络如下图所示: 关于模型中的 BiLSTM 部分在此不过多赘述,相关细节可以参见之前的博客:循环神经网络 (Recurrent Neural Network, RNN) 和 预训练自然语言模型 (Pre-trained Models for NLP)。BiLSTM-CRF 模型的输入是词嵌入向量,输出是对应的预测标注标签,如下图所示: BiLSTM 层的输出为每个标签的分数,对于 $w_0$,BiLSTM 的输出为 1.5 (B_PER),0.9 (I_PER),0.1 (B_ORG),0.08 (I_ORG) 和 0.05 (O),这些分数为 CRF 层的输入,如下图所示: 经过 CRF 层后,具有最高分数的预测序列被选择为最优预测结果。如果没有 CRF 层,我们可以直接选择 BiLSTM 层输出分数的最大值对应的序列为预测结果。例如,对于 $w_0$,最高分数为 1.5,对应的预测标签则为 B_PER,类似的 $w_1, w_2, w_3, w_4$ 对应的预测标签为 I_PER, O, B_ORG, O,如下图所示: 虽然我们在上例中得到了正确的结果,但通常情况下并非如此。对于如下的示例,预测结果为 I_ORG, I_PER, O, I_ORG, I_PER,这显然是不正确的。 CRF 层在进行预测时可以添加一些约束,这些约束可以在训练时被 CRF 层学习得到。可能的约束有: 句子的第一个词的标签可以是 B_X 或 O,而非 I_X。 B_X, I_X 是有效的标签,而 B_X, I_Y 是无效的标签。 一个命名实体的起始标签应为 B_X 而非 I_X。 CRF 层的损失包含两部分,这两部分构成了 CRF 层的关键: 发射分数(Emission Score) 发射分数即为 BiLSTM 层的输出分数,例如 $w_0$ 对应的标签 B_PER 的分数为 1.5。为了方便起见,对于每类标签给定一个索引: 标签 索引 B_PER 0 I_PER 1 B_ORG 2 I_ORG 3 O 4 我们利用 $x_{i y_{j}}$ 表示发射分数,$i$ 为词的索引,$y_i$ 为标注标签的索引。例如:$x_{i=1, y_{j}=2} = x_{w_1, \text{B\_ORG}} = 0.1$,表示 $w_1$ 为 B_ORG 的分数为 0.1。 转移分数(Transition Score) 我们利用 $t_{y_i, y_j}$ 表示转移分数,例如 $t_{\text{B\_PER}, \text{I\_PER}} = 0.9$ 表示由标签 B_PER 转移到 I_PER 的分数为 0.9。因此,需要一个转移分数矩阵用于存储所有标注标签之间的转移分数。为了使得转移分数矩阵更加鲁棒,需要添加两个标签 START 和 END,分别表示一个句子的开始和结束。下表为一个转移分数矩阵的示例: START B-PER I-PER B-ORG I-ORG O END START 0 0.8 0.007 0.7 0.0008 0.9 0.08 B_PER 0 0.6 0.9 0.2 0.0006 0.6 0.009 I_PER -1 0.5 0.53 0.55 0.0003 0.85 0.008 B_ORG 0.9 0.5 0.0003 0.25 0.8 0.77 0.006 I_ORG -0.9 0.45 0.007 0.7 0.65 0.76 0.2 O 0 0.65 0.0007 0.7 0.0008 0.9 0.08 END 0 0 0 0 0 0 0 转移分数矩阵作为 BiLSTM-CRF 模型的一个参数,随机初始化并通过模型的训练不断更新,最终学习得到约束条件。 CRF 层的损失函数包含两个部分:真实路径分数和所有可能路径的总分数。假设每个可能的路径有一个分数 $P_i$,共 $N$ 种可能的路径,所有路径的总分数为: $$ P_{\text {total}}=P_{1}+P_{2}+\ldots+P_{N}=e^{S_{1}}+e^{S_{2}}+\ldots+e^{S_{N}} $$ 则损失函数定义为: $$ \text{Loss} = \dfrac{P_{\text{RealPath}}}{\sum_{i=1}^{N} P_i} $$ 对于 $S_i$,共包含两部分:发射分数和转移分数。以路径 START -> B_PER -> I_PER -> O -> B_ORG -> O -> END 为例,发射分数为: $$ \begin{aligned} \text{EmissionScore} = \ &x_{0, \text{START}} + x_{1, \text{B\_PER}} + x_{2, \text{I\_PER}} \\ &+ x_{3, \text{O}} + x_{4, \text{B\_ORG}} + x_{5, \text{O}} + x_{6, \text{END}} \end{aligned} $$ 其中 $x_{i, y_j}$ 表示第 $i$ 个词标签为 $y_j$ 的分数,为 BiLSTM 的输出,$x_{0, \text{START}}$ 和 $x_{6, \text{END}}$ 可以设置为 0。转换分数为: $$ \begin{aligned} \text{TransitionScore} = \ &t_{\text{START}, \text{B\_PER}} + t_{\text{B\_PER}, \text{I\_PER}} + t_{\text{I\_PER}, \text{O}} \\ &+ t_{\text{O}, \text{B\_ORG}} + t_{\text{B\_ORG}, \text{O}} + t_{\text{O}, \text{END}} \end{aligned} $$ 其中 $t_{y_i, y_j}$ 表示标注标签由 $y_i$ 转移至 $y_j$ 的分数。 对于所有路径的总分数的计算过程采用了类似 动态规划 的思想,整个过程计算比较复杂,在此不再详细展开,详细请参见参考文章。 利用训练好的 BiLSTM-CRF 模型进行预测时,首先我们可以得到序列的发射分数和转移分数,其次用维特比算法可以得到最终的预测标注序列。 Lattice LSTM 8 Zhang 等人针对中文提出了一种基于 Lattice LSTM 的命名实体识别方法,Lattice LSTM 的结构如下图所示: 模型的基本思想是将句子中的词汇(例如:南京,长江大桥等)信息融入到基于字符的 LSTM 模型中,从而可以显性地利用词汇信息。 模型的输入为一个字符序列 $c_1, c_2, \cdots, c_m$ 和词汇表 $\mathbb{D}$ 中所有匹配的字符子序列,其中词汇表 $\mathbb{D}$ 利用大量的原始文本通过分词构建。令 $w_{b, e}^d$ 表示有以第 $b$ 个字符起始,以第 $e$ 个字符结尾的子序列,例如:$w_{1,2}^d$ 表示“南京 ”,$w_{7,8}^d$ 表示“大桥”。 不同于一般的字符级模型,LSTM 单元的状态考虑了句子中的子序列 $w_{b,e}^d$,每个子序列 $w_{b,e}^d$ 表示为: $$ \mathbf{x}_{b, e}^{w}=\mathbf{e}^{w}\left(w_{b, e}^{d}\right) $$ 其中,$\mathbf{e}^{w}$ 为词向量查询表。一个词单元 $\mathbf{c}_{b,e}^w$ 用于表示 $\mathbf{x}_{b,e}^w$ 的循环状态: $$ \begin{aligned} \left[\begin{array}{c} \mathbf{i}_{b, e}^{w} \\ \mathbf{f}_{b, e}^{w} \\ \widetilde{c}_{b, e}^{w} \end{array}\right] &=\left[\begin{array}{c} \sigma \\ \sigma \\ \tanh \end{array}\right]\left(\mathbf{W}^{w \top}\left[\begin{array}{c} \mathbf{x}_{b, e}^{w} \\ \mathbf{h}_{b}^{c} \end{array}\right]+\mathbf{b}^{w}\right) \\ \mathbf{c}_{b, e}^{w} &=\mathbf{f}_{b, e}^{w} \odot \mathbf{c}_{b}^{c}+\mathbf{i}_{b, e}^{w} \odot \widetilde{c}_{b, e}^{w} \end{aligned} $$ 其中,$\mathbf{i}_{b, e}^{w}$ 和 $\mathbf{f}_{b, e}^{w}$ 分别为输入门和遗忘门。由于仅在字符级别上进行标注,因此对于词单元来说没有输出门。 对于 $\mathbf{c}_{j}^c$ 来说可能有多条信息流,例如 $\mathbf{c}_7^c$ 的输入包括 $\mathbf{x}_7^c$(桥),$\mathbf{c}_{6,7}^w$(大桥)和 $\mathbf{c}_{4,7}^w$(长江大桥)。论文采用了一个新的门 $\mathbf{i}_{b,e}^c$ 来控制所有子序列单元 $\mathbf{c}_{b,e}^w$ 对 $\mathbf{c}_{j}^c$ 的贡献: $$ \mathbf{i}_{b, e}^{c}=\sigma\left(\mathbf{W}^{l \top}\left[\begin{array}{c} \mathbf{x}_{e}^{c} \\ \mathbf{c}_{b, e}^{w} \end{array}\right]+\mathbf{b}^{l}\right) $$ 则单元状态 $\mathbf{c}_j^c$ 的计算变为: $$ \mathbf{c}_{j}^{c}=\sum_{b \in\left\{b^{\prime} | w_{b^{\prime}, j} \in \mathbb{D}\right\}} \boldsymbol{\alpha}_{b, j}^{c} \odot \boldsymbol{c}_{b, j}^{w}+\boldsymbol{\alpha}_{j}^{c} \odot \widetilde{\boldsymbol{c}}_{j}^{c} $$ 在上式中,$\mathbf{i}_{b,j}^c$ 和 $\mathbf{i}_j^c$ 标准化为 $\boldsymbol{\alpha}_{b, j}^{c}$ 和 $\boldsymbol{\alpha}_{j}^{c}$: $$ \begin{aligned} \boldsymbol{\alpha}_{b, j}^{c} &=\frac{\exp \left(\mathbf{i}_{b, j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \\ \boldsymbol{\alpha}_{j}^{c} &=\frac{\exp \left(\mathbf{i}_{j}^{c}\right)}{\exp \left(\mathbf{i}_{j}^{c}\right)+\sum_{b^{\prime} \in\left\{b^{\prime \prime} | w_{b^{\prime \prime}, j}^{d} \in \mathbb{D}\right\}} \exp \left(\mathbf{i}_{b^{\prime}, j}^{c}\right)} \end{aligned} $$ 开放资源 标注工具 synyi/poplar nlplab/brat doccano/doccano heartexlabs/label-studio deepwel/Chinese-Annotator jiesutd/YEDDA 开源模型,框架和代码 pytorch/text flairNLP/flair PetrochukM/PyTorch-NLP allenai/allennlp fastnlp/fastNLP Stanford CoreNLP NeuroNER spaCy NLTK BrikerMan/Kashgari Hironsan/anago crownpku/Information-Extraction-Chinese thunlp/OpenNRE hankcs/HanLP jiesutd/NCRFpp 其他资源 keon/awesome-nlp crownpku/Awesome-Chinese-NLP sebastianruder/NLP-progress thunlp/NREPapers 李航. (2019). 统计学习方法(第二版). 清华大学出版社. ↩︎ ↩︎ 俞士汶, 段慧明, 朱学锋, & 孙斌. (2002). 北京大学现代汉语语料库基本加工规范. 中文信息学报, 16(5), 51-66. ↩︎ 俞士汶, 段慧明, 朱学锋, 孙斌, & 常宝宝. (2003). 北大语料库加工规范: 切分· 词性标注· 注音. 汉语语言与计算学报, 13(2), 121-158. ↩︎ http://ictclas.nlpir.org/nlpir/html/readme.htm ↩︎ Xia, F. (2000). The part-of-speech tagging guidelines for the Penn Chinese Treebank (3.0). IRCS Technical Reports Series, 38. ↩︎ Huang, C. N., Li, Y., & Zhu, X. (2006). Tokenization guidelines of Chinese text (v5.0, in Chinese). Microsoft Research Asia. ↩︎ Huang, Z., Xu, W., & Yu, K. (2015). Bidirectional LSTM-CRF models for sequence tagging. arXiv preprint arXiv:1508.01991. ↩︎ Zhang, Y., & Yang, J. (2018). Chinese NER Using Lattice LSTM. In Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (pp. 1554-1564). ↩︎

2020/5/2
articleCard.readMore

图嵌入 (Graph Embedding) 和图神经网络 (Graph Neural Network)

图(Graph / Network)数据类型可以自然地表达物体和物体之间的联系,在我们的日常生活与工作中无处不在。例如:微信和新浪微博等构成了人与人之间的社交网络;互联网上成千上万个页面构成了网页链接网络;国家城市间的运输交通构成了物流网络。 The power of relationships in data 通常定义一个图 $G = \left(V, E\right)$,其中 $V$ 为顶点(Vertices)集合,$E$ 为边(Edges)集合。对于一条边 $e = u, v$ 包含两个端点(Endpoints) $u$ 和 $v$,同时 $u$ 可以称为 $v$ 的邻居(Neighbor)。当所有的边为有向边时,图称之为有向(Directed)图,当所有边为无向边时,图称之为无向(Undirected)图。对于一个顶点 $v$,令 $d \left(v\right)$ 表示连接的边的数量,称之为度(Degree)。对于一个图 $G = \left(V, E\right)$,其邻接矩阵(Adjacency Matrix) $A \in \mathbb{A}^{|V| \times |V|}$ 定义为: $$ A_{i j}=\left\{\begin{array}{ll} 1 & \text { if }\left\{v_{i}, v_{j}\right\} \in E \text { and } i \neq j \\ 0 & \text { otherwise } \end{array}\right. $$ 作为一个典型的非欧式数据,对于图数据的分析主要集中在节点分类,链接预测和聚类等。对于图数据而言,图嵌入(Graph / Network Embedding)和图神经网络(Graph Neural Networks, GNN)是两个类似的研究领域。图嵌入旨在将图的节点表示成一个低维向量空间,同时保留网络的拓扑结构和节点信息,以便在后续的图分析任务中可以直接使用现有的机器学习算法。一些基于深度学习的图嵌入同时也属于图神经网络,例如一些基于图自编码器和利用无监督学习的图卷积神经网络等。下图描述了图嵌入和图神经网络之间的差异: 提示 本文中图嵌入和网络表示学习均表示 Graph / Network Embedding。 图嵌入 本节内容主要参考自: 1 2 3 使用邻接矩阵的网络表示存在计算效率的问题,邻接矩阵 $A$ 使用 $|V| \times |V|$ 的存储空间表示一个图,随着节点个数的增长,这种表示所需的空间成指数增长。同时,在邻接矩阵中绝大多数是 0,数据的稀疏性使得快速有效的学习方式很难被应用。 网路表示学习是指学习得到网络中节点的低维向量表示,形式化地,网络表示学习的目标是对每个节点 $v \in V$ 学习一个实值向量 $R_v \in \mathbb{R}^k$,其中 $k \ll |V|$ 表示向量的维度。经典的 Zachary’s karate club 网络的嵌入可视化如下图所示: Random Walk 基于随机游走的图嵌入通过使得图上一个短距的随机游走中共现的节点具有更相似的表示的方式来优化节点的嵌入。 DeepWalk DeepWalk 4 算法主要包含两个部分:一个随机游走序列生成器和一个更新过程。随机游走序列生成器首先在图 $G$ 中均匀地随机抽样一个随机游走 $\mathcal{W}_{v_i}$ 的根节点 $v_i$,接着从节点的邻居中均匀地随机抽样一个节点直到达到设定的最大长度 $t$。对于一个生成的以 $v_i$ 为中心左右窗口为 $w$ 的随机游走序列 $v_{i-w}, \dotsc, v_{i-1}, v_i, v_{i+1}, \dotsc, v_{i+m}$,DeepWalk 利用 SkipGram 算法通过最大化以 $v_i$ 为中心,左右 $w$ 为窗口的同其他节点共现概率来优化模型: $$ \text{Pr} \left(\left\{v_{i-w}, \dotsc, v_{i+w}\right\} \setminus v_i \mid \Phi \left(v_i\right)\right) = \prod_{j=i-w, j \neq i}^{i+w} \text{Pr} \left(v_j \mid \Phi \left(v_i\right)\right) $$ DeepWalk 和 Word2Vec 的类比如下表所示: 模型 目标 输入 输出 Word2Vec 词 句子 词嵌入 DeepWalk 节点 节点序列 节点嵌入 node2vec node2vec 5 通过改变随机游走序列生成的方式进一步扩展了 DeepWalk 算法。DeepWalk 选取随机游走序列中下一个节点的方式是均匀随机分布的,而 node2vec 通过引入两个参数 $p$ 和 $q$,将宽度优先搜索和深度优先搜索引入了随机游走序列的生成过程。 宽度优先搜索注重邻近的节点并刻画了相对局部的一种网络表示, 宽度优先中的节点一般会出现很多次,从而降低刻画中心节点的邻居节点的方差, 深度优先搜索反映了更高层面上的节点之间的同质性。 node2vec 中的两个参数 $p$ 和 $q$ 控制随机游走序列的跳转概率。假设上一步游走的边为 $\left(t, v\right)$, 那么对于节点 $v$ 的不同邻居,node2vec 根据 $p$ 和 $q$ 定义了不同的邻居的跳转概率,$p$ 控制跳向上一个节点的邻居的概率,$q$ 控制跳向上一个节点的非邻居的概率,具体的未归一的跳转概率值 $\pi_{vx} = \alpha_{pq} \left(t, x\right)$ 如下所示: $$ \alpha_{p q}(t, x)=\left\{\begin{array}{cl} \dfrac{1}{p}, & \text { if } d_{t x}=0 \\ 1, & \text { if } d_{t x}=1 \\ \dfrac{1}{q}, & \text { if } d_{t x}=2 \end{array}\right. $$ 其中,$d_{tx}$ 表示节点 $t$ 和 $x$ 之间的最短距离。为了获得最优的超参数 $p$ 和 $q$ 的取值,node2vec 通过半监督形式,利用网格搜索最合适的参数学习节点表示。 APP 之前的基于随机游走的图嵌入方法,例如:DeepWalk,node2vec 等,都无法保留图中的非对称信息。然而非对称性在很多问题,例如:社交网络中的链路预测、电商中的推荐等,中至关重要。在有向图和无向图中,非对称性如下图所示: 为了保留图的非对称性,对于每个节点 $v$ 设置两个不同的角色:源和目标,分别用 $\overrightarrow{s_{v}}$ 和 $\overrightarrow{t_{v}}$ 表示。对于每个从 $u$ 开始以 $v$ 结尾的采样序列,利用 $(u, v)$ 表示采样的节点对。则利用源节点 $u$ 预测目标节点 $v$ 的概率如下: $$ p(v | u)=\frac{\exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})}{\sum_{n \in V} \exp (\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})} $$ 通过 Skip-Gram 和负采样对模型进行优化,损失函数如下: $$ \begin{aligned} \ell &= \log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}})+k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})] \\ &= \sum_{u} \sum_{v} \# \text {Sampled}_{u}(v) \cdot \left(\log \sigma(\overrightarrow{s_{u}} \cdot \overrightarrow{t_{v}}) + k \cdot E_{t_{n} \sim P_{D}}[\log \sigma(-\overrightarrow{s_{u}} \cdot \overrightarrow{t_{n}})]\right) \end{aligned} $$ 其中,我们根据分布 $P_D \left(n\right) \sim \dfrac{1}{|V|}$ 随机负采样 $k$ 个节点对,$\# \text{Sampled}_{u}(v)$ 为采样的 $\left(u, v\right)$ 对的个数,$\sigma$ 为 sigmoid 函数。通常情况下,$\# \text{Sampled}_{u}(v) \neq \# \text{Sampled}_{v}(u)$,即 $\left(u, v\right)$ 和 $\left(v, u\right)$ 的观测数量是不同的。模型利用 Monte-Carlo End-Point 采样方法 6 随机的以 $v$ 为起点和 $\alpha$ 为停止概率采样 $p$ 条路径。这种采样方式可以用于估计任意一个节点对之间的 Rooted PageRank 7 值,模型利用这个值估计由 $v$ 到达 $u$ 的概率。 Matrix Fractorization GraRep GraRep 8 提出了一种基于矩阵分解的图嵌入方法。对于一个图 $G$,利用邻接矩阵 $S$ 定义图的度矩阵: $$ D_{i j}=\left\{\begin{array}{ll} \sum_{p} S_{i p}, & \text { if } i=j \\ 0, & \text { if } i \neq j \end{array}\right. $$ 则一阶转移概率矩阵定义如下: $$ A = D^{-1} S $$ 其中,$A_{i, j}$ 表示通过一步由 $v_i$ 转移到 $v_j$ 的概率。所谓的全局特征包含两个部分: 捕获两个节点之间的长距离特征 分别考虑按照不同转移步数的连接 下图展示了 $k = 1, 2, 3, 4$ 情况下的强(上)弱(下)关系: 利用 Skip-Gram 和 NCE(noise contrastive estimation)方法,对于一个 $k$ 阶转移,可以将模型归结到一个矩阵 $Y_{i, j}^k$ 的分解问题: $$ Y_{i, j}^{k}=W_{i}^{k} \cdot C_{j}^{k}=\log \left(\frac{A_{i, j}^{k}}{\sum_{t} A_{t, j}^{k}}\right)-\log (\beta) $$ 其中,$W$ 和 $C$ 的每一行分别为节点 $w$ 和 $c$ 的表示,$\beta = \lambda / N$,$\lambda$ 为负采样的数量,$N$ 为图中边的个数。 之后为了减少噪音,模型将 $Y^k$ 中所有的负值替换为 0,通过 SVD(方法详情见参见之前博客)得到节点的 $d$ 维表示: $$ \begin{aligned} X_{i, j}^{k} &= \max \left(Y_{i, j}^{k}, 0\right) \\ X^{k} &= U^{k} \Sigma^{k}\left(V^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &= U_{d}^{k} \Sigma_{d}^{k}\left(V_{d}^{k}\right)^{T} \\ X^{k} \approx X_{d}^{k} &= W^{k} C^{k} \\ W^{k} &= U_{d}^{k}\left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} \\ C^{k} &= \left(\Sigma_{d}^{k}\right)^{\frac{1}{2}} V_{d}^{k T} \end{aligned} $$ 最终,通过对不同 $k$ 的表示进行拼接得到节点最终的表示。 HOPE HOPE 9 对于每个节点最终生成两个嵌入表示:一个是作为源节点的嵌入表示,另一个是作为目标节点的嵌入表示。模型通过近似高阶相似性来保留非对称传递性,其优化目标为: $$ \min \left\|\mathbf{S}-\mathbf{U}^{s} \cdot \mathbf{U}^{t^{\top}}\right\|_{F}^{2} $$ 其中,$\mathbf{S}$ 为相似矩阵,$\mathbf{U}^s$ 和 $\mathbf{U}^t$ 分别为源节点和目标节点的向量表示。下图展示了嵌入向量可以很好的保留非对称传递性: 对于 $\mathbf{S}$ 有多种可选近似度量方法:Katz Index,Rooted PageRank(RPR),Common Neighbors(CN),Adamic-Adar(AA)。这些度量方法可以分为两类:全局近似(Katz Index 和 RPR)和局部近似(CN 和 AA)。 算法采用了一个广义 SVD 算法(JDGSVD)来解决使用原始 SVD 算法计算复杂度为$O \left(N^3\right)$ 过高的问题,从而使得算法可以应用在更大规模的图上。 Meta Paths metapath2vec metapath2vec 10 提出了一种基于元路径的异构网络表示学习方法。在此我们引入 3 个定义: 异构网络((Heterogeneous information network,HIN)可以定义为一个有向图 $G = \left(V, E\right)$,一个节点类型映射 $\tau: V \to A$ 和一个边类型映射 $\phi: E \to R$,其中对于 $v \in V$ 有 $\tau \left(v\right) \in A$,$e \in E$ 有 $\phi \left(e\right) \in R$,且 $|A| + |R| > 1$。 网络模式(Network schema)定义为 $T_G = \left(A, R\right)$,为一个包含节点类型映射 $\tau \left(v\right) \in A$ 和边映射 $\phi \left(e\right) \in R$ 异构网络的 $G = \left(V, E\right)$ 的元模板。 元路径(Meta-path)定义为网络模式 $T_G = \left(A, R\right)$ 上的一条路径 $P$,形式为 $A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l+1}$。 下图展示了一个学术网络和部分元路径: 其中,APA 表示一篇论文的共同作者,APVPA 表示两个作者在同一个地方发表过论文。 metapath2vec 采用了基于元路径的随机游走来生成采样序列,这样就可以保留原始网络中的语义信息。对于一个给定的元路径模板 $P: A_{1} \stackrel{R_{1}}{\longrightarrow} A_{2} \stackrel{R_{2}}{\longrightarrow} \cdots A_{t} \stackrel{R_{t}}{\longrightarrow} A_{t+1} \cdots \stackrel{R_{l}}{\longrightarrow} A_{l}$,第 $i$ 步的转移概率为: $$ p\left(v^{i+1} | v_{t}^{i}, P\right)=\left\{\begin{array}{ll} \dfrac{1}{\left|N_{t+1}\left(v_{t}^{i}\right)\right|} & \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right)=A_{t+1} \\ 0 & \left(v_{t}^{i}, v^{i+1}\right) \in E, \phi\left(v^{i+1}\right) \neq A_{t+1} \\ 0 & \left(v_{t}^{i}, v^{i+1}\right) \notin E \end{array}\right. $$ 其中,$v^i_t \in A_t$,$N_{t+1} \left(v^i_t\right)$ 表示节点 $v^i_t$ 类型为 $A_{t+1}$ 的邻居。之后,则采用了类似 DeepWalk 的方式进行训练得到节点表示。 HIN2Vec HIN2Vec 11 提出了一种利用多任务学习通过多种关系进行节点和元路径表示学习的方法。模型最初是希望通过一个多分类模型来预测任意两个节点之间所有可能的关系。假设对于任意两个节点,所有可能的关系集合为 $R = \{\text{P-P, P-A, A-P, P-P-P, P-P-A, P-A-P, A-P-P, A-P-A}\}$。假设一个实例 $P_1$ 和 $A_1$ 包含两种关系:$\text{P-A}$ 和 $\text{P-P-A}$,则对应的训练数据为 $\langle x: P_1, y: A_1, output: \left[0, 1, 0, 0, 1, 0, 0, 0\right] \rangle$。 但实际上,扫描整个网络寻找所有可能的关系是不现实的,因此 HIN2Vec 将问题简化为一个给定两个节点判断之间是否存在一个关系的二分类问题,如下图所示: 模型的三个输入分别为节点 $x$ 和 $y$,以及关系 $r$。在隐含层输入被转换为向量 $W_{X}^{\prime} \vec{x}, W_{Y}^{\prime} \vec{y}$ 和 $f_{01}\left(W_{R}^{\prime} \vec{r}\right)$。需要注意对于关系 $r$,模型应用了一个正则化函数 $f_{01} \left(\cdot\right)$ 使得 $r$ 的向量介于 $0$ 和 $1$ 之间。之后采用逐元素相乘对三个向量进行汇总 $W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)$。在最后的输出层,通过计算 $sigmoid \left(\sum W_{X}^{\prime} \vec{x} \odot W_{Y}^{\prime} \vec{y} \odot f_{01}\left(W_{R}^{\prime} \vec{r}\right)\right)$ 得到最终的预测值。 在生成训练数据时,HIN2Vec 采用了完全随机游走进行节点采样,而非 metapath2vec 中的按照给定的元路径的方式。通过随机替换 $x, y, r$ 中的任何一个可以生成负样本,但当网络中的关系数量较少,节点数量远远大于关系数量时,这种方式很可能产生错误的负样本,因此 HIN2Vec 只随机替换 $x, y$,保持 $r$ 不变。 Deep Learning SDNE SDNE 12 提出了一种利用自编码器同时优化一阶和二阶相似度的图嵌入算法,学习得到的向量能够保留局部和全局的结构信息。SDNE 使用的网络结构如下图所示: 对于二阶相似度,自编码器的目标是最小化输入和输出的重构误差。SDNE 采用邻接矩阵作为自编码器的输入,$\mathbf{x}_i = \mathbf{s}_i$,每个 $\mathbf{s}_i$ 包含了节点 $v_i$ 的邻居结构信息。模型的损失函数如下: $$ \mathcal{L}=\sum_{i=1}^{n}\left\|\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right\|_{2}^{2} $$ 由于网络的稀疏性,邻接矩阵中的非零元素远远少于零元素,因此模型采用了一个带权的损失函数: $$ \begin{aligned} \mathcal{L}_{2nd} &=\sum_{i=1}^{n}\left\|\left(\hat{\mathbf{x}}_{i}-\mathbf{x}_{i}\right) \odot \mathbf{b}_{i}\right\|_{2}^{2} \\ &=\|(\hat{X}-X) \odot B\|_{F}^{2} \end{aligned} $$ 其中,$\odot$ 表示按位乘,$\mathbf{b}_i = \left\{b_{i, j}\right\}_{j=1}^{n}$,如果 $s_{i, j} = 0$ 则 $b_{i, j} = 1$ 否则 $b_{i, j} = \beta > 1$。 对于一阶相似度,模型利用了一个监督学习模块最小化节点在隐含空间中距离。损失函数如下: $$ \begin{aligned} \mathcal{L}_{1st} &=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}^{(K)}-\mathbf{y}_{j}^{(K)}\right\|_{2}^{2} \\ &=\sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2} \end{aligned} $$ 最终,模型联合损失函数如下: $$ \begin{aligned} \mathcal{L}_{mix} &=\mathcal{L}_{2nd}+\alpha \mathcal{L}_{1st}+\nu \mathcal{L}_{reg} \\ &=\|(\hat{X}-X) \odot B\|_{F}^{2}+\alpha \sum_{i, j=1}^{n} s_{i, j}\left\|\mathbf{y}_{i}-\mathbf{y}_{j}\right\|_{2}^{2}+\nu \mathcal{L}_{reg} \end{aligned} $$ 其中,$\mathcal{L}_{reg}$ 为 L2 正则项。 DNGR DNGR 13 提出了一种利用基于 Stacked Denoising Autoencoder(SDAE)提取特征的网络表示学习算法。算法的流程如下图所示: 模型首先利用 Random Surfing 得到一个概率共现(PCO)矩阵,之后利用其计算得到 PPMI 矩阵,最后利用 SDAE 进行特征提取得到节点的向量表示。 对于传统的将图结构转换为一个线性序列方法存在几点缺陷: 采样序列边缘的节点的上下文信息很难被捕捉。 很难直接确定游走的长度和步数等超参数,尤其是对于大型网络来说。 受 PageRank 思想影响,作者采用了 Random Surfing 模型。定义转移矩阵 $A$,引入行向量 $p_k$,第 $j$ 个元素表示通过 $k$ 步转移之后到达节点 $j$ 的概率。$p_0$ 为一个初始向量,其仅第 $i$ 个元素为 1,其它均为 0。在考虑以 $1 - \alpha$ 的概率返回初始节点的情况下有: $$ p_{k}=\alpha \cdot p_{k-1} A+(1-\alpha) p_{0} $$ 在不考虑返回初始节点的情况下有: $$ p_{k}^{*}=p_{k-1}^{*} A=p_{0} A^{k} $$ 直观而言,两个节点越近,两者的关系越亲密,因此通过同当前节点的相对距离来衡量上下文节点的重要性是合理的。基于此,第 $i$ 个节点的表示可以用如下方式构造: $$ r=\sum_{k=1}^{K} w(k) \cdot p_{k}^{*} $$ 其中,$w \left(\cdot\right)$ 是一个衰减函数。 利用 PCO 计算得到 PPMI 后,再利用一个 SDAE 进行特征提取。Stacking 策略可以通过不同的网络层学习得到不同层级的表示,Denoising 策略则通过去除数据中的噪声,增加结果的鲁棒性。同时,SNGR 相比基于 SVD 的方法效率更高。 Others LINE LINE 14 提出了一个用于大规模网络嵌入的方法,其满足如下 3 个要求: 同时保留节点之间的一阶相似性(first-order proximity)和二阶相似性(second-order proximity)。 可以处理大规模网络,例如:百万级别的顶点和十亿级别的边。 可以处理有向,无向和带权的多种类型的图结构。 给定一个无向边 $\left(i, j\right)$,点 $v_i$ 和 $v_j$ 的联合概率如下: $$ p_{1}\left(v_{i}, v_{j}\right)=\frac{1}{1+\exp \left(-\vec{u}_{i}^{T} \cdot \vec{u}_{j}\right)} $$ 其中,$\vec{u}_{i} \in R^{d}$ 为节点 $v_i$ 的低维向量表示。在空间 $V \times V$ 上,分布 $p \left(\cdot, \cdot\right)$ 的经验概率为 $\hat{p}_1 \left(i, j\right) = \dfrac{w_{ij}}{V}$,其中 $W = \sum_{\left(i, j\right) \in E} w_{ij}$。通过最小化两个分布的 KL 散度来优化模型,则目标函数定义如下: $$ O_{1}=-\sum_{(i, j) \in E} w_{i j} \log p_{1}\left(v_{i}, v_{j}\right) $$ 需要注意的是一阶相似度仅可用于无向图,通过最小化上述目标函数,我们可以将任意顶点映射到一个 $d$ 维空间向量。 二阶相似度既可以用于无向图,也可以用于有向图。二阶相似度假设共享大量同其他节点连接的节点之间是相似的,每个节点被视为一个特定的上下文,则在上下文上具有类似分布的节点是相似的。在此,引入两个向量 $\vec{u}_{i}$ 和 $\vec{u}_{\prime i}$,其中 $\vec{u}_{i}$ 是 $v_i$ 做为节点的表示,$\vec{u}_{\prime i}$ 是 $v_i$ 做为上下文的表示。对于一个有向边 $\left(i, j\right)$,由 $v_i$ 生成上下文 $v_j$ 的概率为: $$ p_{2}\left(v_{j} | v_{i}\right)=\frac{\exp \left(\vec{u}_{j}^{\prime T} \cdot \vec{u}_{i}\right)}{\sum_{k=1}^{|V|} \exp \left(\vec{u}_{k}^{\prime T} \cdot \vec{u}_{i}\right)} $$ 其中,$|V|$ 为节点或上下文的数量。在此我们引入一个参数 $\lambda_i$ 用于表示节点 $v_i$ 的重要性程度,重要性程度可以利用度或者 PageRank 算法进行估计。经验分布 $\hat{p}_{2}\left(\cdot \mid v_{i}\right)$ 定义为 $\hat{p}_{2}\left(v_{j} \mid v_{i}\right)=\dfrac{w_{i j}}{d_{i}}$,其中 $w_{ij}$ 为边 $\left(i, j\right)$ 的权重,$d_i$ 为节点 $v_i$ 的出度。LINE 中采用 $d_i$ 作为节点的重要性 $\lambda_i$,利用 KL 散度同时忽略一些常量,目标函数定义如下: $$ O_{2}=-\sum_{(i, j) \in E} w_{i j} \log p_{2}\left(v_{j} \mid v_{i}\right) $$ LINE 采用负采样的方式对模型进行优化,同时利用 Alias 方法 15 16 加速采样过程。 图神经网络 本节内容主要参考自: 17 18 19 20 图神经网络(Graph Neural Network,GNN)最早由 Scarselli 等人 21 提出。图中的一个节点可以通过其特征和相关节点进行定义,GNN 的目标是学习一个状态嵌入 $\mathbf{h}_v \in \mathbb{R}^s$ 用于表示每个节点的邻居信息。状态嵌入 $\mathbf{h}_v$ 可以生成输出向量 $\mathbf{o}_v$ 用于作为预测节点标签的分布等。 下面三张图分别从图的类型,训练方法和传播过程角度列举了不同 GNN 的变种 19。 下面我们主要从模型的角度分别介绍不同种类的 GNN。 Graph Neural Networks 为了根据邻居更新节点的状态,定义一个用于所有节点的函数 $f$,称之为 local transition function。定义一个函数 $g$,用于生成节点的输出,称之为 local output function。有: $$ \begin{array}{c} \mathbf{h}_{v}=f\left(\mathbf{x}_{v}, \mathbf{x}_{co[v]}, \mathbf{h}_{ne[v]}, \mathbf{x}_{ne[v])}\right. \\ \mathbf{o}_{v}=g\left(\mathbf{h}_{v}, \mathbf{x}_{v}\right) \end{array} $$ 其中,$\mathbf{x}$ 表示输入特征,$\mathbf{h}$ 表示隐含状态。$co[v]$ 为连接到节点 $v$ 的边集,$ne[v]$ 为节点 $v$ 的邻居。 上图中,$\mathbf{x}_1$ 表示 $l_1$ 的输入特征,$co[l_1]$ 包含了边 $l_{(1, 4)}, l_{(6, 1)}, l_{(1, 2)}$ 和 $l_{(3, 1)}$,$ne[l_1]$ 包含了节点 $l_2, k_3, l_4$ 和 $l_6$。 令 $\mathbf{H}, \mathbf{O}, \mathbf{X}$ 和 $\mathbf{X}_N$ 分别表示状态、输出、特征和所有节点特征的向量,有: $$ \begin{aligned} &\mathbf{H}=F(\mathbf{H}, \mathbf{X})\\ &\mathbf{O}=G\left(\mathbf{H}, \mathbf{X}_{N}\right) \end{aligned} $$ 其中,$F$ 为 global transition function,$G$ 为 global output function,分别为图中所有节点的 local transition function $f$ 和 local output function $g$ 的堆叠版本。依据 Banach 的 Fixed Point Theorem 22,GNN 利用传统的迭代方式计算状态: $$ \mathbf{H}^{t+1}=F\left(\mathbf{H}^{t}, \mathbf{X}\right) $$ 其中,$\mathbf{H}^t$ 表示第 $t$ 论循环 $\mathbf{H}$ 的值。 介绍完 GNN 的框架后,下一个问题就是如果学习得到 local transition function $f$ 和 local output function $g$。在包含目标信息($\mathbf{t}_v$ 对于特定节点)的监督学习情况下,损失为: $$ loss = \sum_{i=1}^{p} \left(\mathbf{t}_i - \mathbf{o}_i\right) $$ 其中,$p$ 为用于监督学习的节点数量。利用基于梯度下降的学习方法优化模型后,我们可以得到针对特定任务的训练模型和图中节点的隐含状态。 尽管实验结果表明 GNN 是一个用于建模结构数据的强大模型,但对于一般的 GNN 模型仍存在如下缺陷: 对于固定点,隐含状态的更新是低效地。 GNN 在每一轮计算中共享参数,而常见的神经网络结构在不同层使用不同的参数。同时,隐含节点状态的更新可以进一步应用 RNN 的思想。 边上的一些信息特征并没有被有效的建模,同时如何学习边的隐含状态也是一个重要问题。 如果我们更关注节点的表示而非图的表示,当迭代轮数 $T$ 很大时使用固定点是不合适的。这是因为固定点表示的分布在数值上会更加平滑,从而缺少用于区分不同节点的信息。 Graph Convolutional Networks 图卷积神经网络是将用于传统数据(例如:图像)的卷积操作应用到图结构的数据中。核心思想在于学习一个函数 $f$,通过聚合节点 $v_i$ 自身的特征 $\mathbf{X}_i$ 和邻居的特征 $\mathbf{X}_j$ 获得节点的表示,其中 $j \in N\left(v_i\right)$ 为节点的邻居。 下图展示了一个用于节点表示学习的 GCN 过程: GCN 在构建更复杂的图神经网路中扮演了一个核心角色: GCN 方法可以分为两大类:基于频谱(Spectral Methods)和基于空间(Spatial Methods)的方法。 基于频谱的方法(Spectral Methods) 基于频谱的方法将图视为无向图进行处理,图的一种鲁棒的数学表示为标准化的图拉普拉斯矩阵: $$ \mathbf{L}=\mathbf{I}_{\mathbf{n}}-\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} $$ 其中,$\mathbf{A}$ 为图的邻接矩阵,$\mathbf{D}$ 为节点度的对角矩阵,$\mathbf{D}_{ii} = \sum_{j} \left(\mathbf{A}_{i, j}\right)$。标准化的拉普拉斯矩阵具有实对称半正定的性质,因此可以分解为: $$ \mathbf{L}=\mathbf{U} \mathbf{\Lambda} \mathbf{U}^{T} $$ 其中,$\mathbf{U}=\left[\mathbf{u}_{\mathbf{0}}, \mathbf{u}_{\mathbf{1}}, \cdots, \mathbf{u}_{\mathbf{n}-\mathbf{1}}\right] \in \mathbf{R}^{N \times N}$ 是由 $\mathbf{L}$ 的特征向量构成的矩阵,$\mathbf{\Lambda}$ 为特征值的对角矩阵,$\mathbf{\Lambda}_{ii} = \lambda_i$。在图信号处理过程中,一个图信号 $\mathbf{x} \in \mathbb{R}^N$ 是一个由图的节点构成的特征向量,其中 $\mathbf{x}_i$ 表示第 $i$ 个节点的值。对于信号 $\mathbf{x}$,图上的傅里叶变换可以定义为: $$ \mathscr{F}(\mathbf{x})=\mathbf{U}^{T} \mathbf{x} $$ 傅里叶反变换定义为: $$ \mathscr{F}^{-1}(\hat{\mathbf{x}})=\mathbf{U} \hat{\mathbf{x}} $$ 其中,$\hat{\mathbf{x}}$ 为傅里叶变换后的结果。 转变后信号 $\hat{\mathbf{x}}$ 的元素为新空间图信号的坐标,因此输入信号可以表示为: $$ \mathbf{x}=\sum_{i} \hat{\mathbf{x}}_{i} \mathbf{u}_{i} $$ 这正是傅里叶反变换的结果。那么对于输入信号 $\mathbf{x}$ 的图卷积可以定义为: $$ \begin{aligned} \mathbf{x} *_{G} \mathbf{g} &=\mathscr{F}^{-1}(\mathscr{F}(\mathbf{x}) \odot \mathscr{F}(\mathbf{g})) \\ &=\mathbf{U}\left(\mathbf{U}^{T} \mathbf{x} \odot \mathbf{U}^{T} \mathbf{g}\right) \end{aligned} $$ 其中,$\mathbf{g} \in \mathbb{R}^N$ 为滤波器,$\odot$ 表示逐元素乘。假设定义一个滤波器 $\mathbf{g}_{\theta}=\operatorname{diag}\left(\mathbf{U}^{T} \mathbf{g}\right)$,则图卷积可以简写为: $$ \mathbf{x} *_{G} \mathbf{g}_{\theta}=\mathbf{U} \mathbf{g}_{\theta} \mathbf{U}^{T} \mathbf{x} $$ 基于频谱的图卷积网络都遵循这样的定义,不同之处在于不同滤波器的选择。 一些代表模型及其聚合和更新方式如下表所示: 模型 聚合方式 更新方式 ChebNet 23 $\mathbf{N}_{k}=\mathbf{T}_{k}(\tilde{\mathbf{L}}) \mathbf{X}$ $\mathbf{H}=\sum_{k=0}^{K} \mathbf{N}_{k} \mathbf{\Theta}_{k}$ 1st-order model $\begin{array}{l} \mathbf{N}_{0}=\mathbf{X} \\ \mathbf{N}_{1}=\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}} \mathbf{X} \end{array}$ $\mathbf{H}=\mathbf{N}_{0} \mathbf{\Theta}_{0}+\mathbf{N}_{1} \mathbf{\Theta}_{1}$ Single parameter $\mathbf{N}=\left(\mathbf{I}_{N}+\mathbf{D}^{-\frac{1}{2}} \mathbf{A} \mathbf{D}^{-\frac{1}{2}}\right) \mathbf{X}$ $\mathbf{H}=\mathbf{N} \mathbf{\Theta}$ GCN 24 $\mathbf{N}=\tilde{\mathbf{D}}^{-\frac{1}{2}} \tilde{\mathbf{A}} \tilde{\mathbf{D}}^{-\frac{1}{2}} \mathbf{X}$ $\mathbf{H}=\mathbf{N} \mathbf{\Theta}$ 基于空间的方法(Spatial Methods) 基于空间的方法通过节点的空间关系来定义图卷积操作。为了将图像和图关联起来,可以将图像视为一个特殊形式的图,每个像素点表示一个节点,如下图所示: 每个像素同周围的像素相连,以 $3 \times 3$ 为窗口,每个节点被 8 个邻居节点所包围。通过对中心节点和周围邻居节点的像素值进行加权平均来应用一个 $3 \times 3$ 大小的滤波器。由于邻居节点的特定顺序,可以在不同位置共享权重。同样对于一般的图,基于空间的图卷积通过对中心和邻居节点的聚合得到节点新的表示。 为了使节点可以感知更深和更广的范围,通常的做法是将多个图卷积层堆叠在一起。根据堆叠方式的不同,基于空间的图卷积可以进一步分为两类:基于循环(Recurrent-based)和基于组合(Composition-based)的。基于循环的方法使用相同的图卷积层来更新隐含表示,基于组合的方式使用不同的图卷积层更新隐含表示,两者差异如下图所示: 一些代表模型及其聚合和更新方式如下表所示: 模型 聚合方式 更新方式 Neural FPs 25 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\mathbf{h}_{v}^{t-1}+\sum_{k=1}^{\mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$ $\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{h}_{\mathcal{N}_{v}}^{t} \mathbf{W}_{L}^{\mathcal{N}_{v}}\right)$ DCNN 26 Node classification: $\mathbf{N}=\mathbf{P}^{*} \mathbf{X}$ Graph classification: $\mathbf{N}=1_{N}^{T} \mathbf{P}^{*} \mathbf{X} / N$ $\mathbf{H}=f\left(\mathbf{W}^{c} \odot \mathbf{N}\right)$ GraphSAGE 27 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\text{AGGREGATE}_{t}\left(\left\{\mathbf{h}_{u}^{t-1}, \forall u \in \mathcal{N}_{v}\right\}\right)$ $\mathbf{h}_{v}^{t}=\sigma\left(\mathbf{W}^{t} \cdot\left[\mathbf{h}_{v}^{t-1} \Vert \mathbf{h}_{\mathcal{N}_{v}}^{t}\right]\right)$ Graph Recurrent Networks 一些研究尝试利用门控机制(例如:GRU 或 LSTM)用于减少之前 GNN 模型在传播过程中的限制,同时改善在图结构中信息的长距离传播。GGNN 28 提出了一种使用 GRU 进行传播的方法。它将 RNN 展开至一个固定 $T$ 步,然后通过基于时间的传导计算梯度。传播模型的基础循环方式如下: $$ \begin{aligned} &\mathbf{a}_{v}^{t}=\mathbf{A}_{v}^{T}\left[\mathbf{h}_{1}^{t-1} \ldots \mathbf{h}_{N}^{t-1}\right]^{T}+\mathbf{b}\\ &\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{a}_{v}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{a}_{v}^{t}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W} \mathbf{a}_{v}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned} $$ 节点 $v$ 首先从邻居汇总信息,其中 $\mathbf{A}_v$ 为图邻接矩阵 $\mathbf{A}$ 的子矩阵表示节点 $v$ 及其邻居的连接。类似 GRU 的更新函数,通过结合其他节点和上一时间的信息更新节点的隐状态。$\mathbf{a}$ 用于获取节点 $v$ 邻居的信息,$\mathbf{z}$ 和 $\mathbf{r}$ 分别为更新和重置门。 GGNN 模型设计用于解决序列生成问题,而之前的模型主要关注单个输出,例如:节点级别或图级别的分类问题。研究进一步提出了 Gated Graph Sequence Neural Networks(GGS-NNs),使用多个 GGNN 产生一个输出序列 $\mathbf{o}^{(1)}, \cdots, \mathbf{o}^{(K)}$,如下图所示: 上图中使用了两个 GGNN,$\mathcal{F}_o^{(k)}$ 用于从 $\mathcal{\boldsymbol{X}}^{(k)}$ 预测 $\mathbf{o}^{(k)}$,$\mathcal{F}_x^{(k)}$ 用于从 $\mathcal{\boldsymbol{X}}^{(k)}$ 预测 $\mathcal{\boldsymbol{X}}^{(k+1)}$。令 $\mathcal{\boldsymbol{H}}^{(k, t)}$ 表示第 $k$ 步输出的第 $t$ 步传播,$\mathcal{\boldsymbol{H}}^{(k, 1)}$ 在任意 $k$ 步初始化为 $\mathcal{\boldsymbol{X}}^{(k)}$,$\mathcal{\boldsymbol{H}}^{(t, 1)}$ 在任意 $t$ 步初始化为 $\mathcal{\boldsymbol{X}}^{(t)}$,$\mathcal{F}_o^{(k)}$ 和 $\mathcal{F}_x^{(k)}$ 可以为不同模型也可以共享权重。 一些代表模型及其聚合和更新方式如下表所示: 模型 聚合方式 更新方式 GGNN 28 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}+\mathbf{b}$ $\begin{aligned} &\mathbf{z}_{v}^{t}=\sigma\left(\mathbf{W}^{z} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}^{z} \mathbf{h}_{v}^{t-1}\right)\\ &\mathbf{r}_{v}^{t}=\sigma\left(\mathbf{W}^{r} \mathbf{h}_{\mathcal{N}_{v}}^{z}+\mathbf{U}^{r} \mathbf{h}_{v}^{t-1}\right)\\ &\begin{array}{l} \widetilde{\mathbf{h}}_{v}^{t}=\tanh \left(\mathbf{W h}_{\mathcal{N}_{v}}^{t}+\mathbf{U}\left(\mathbf{r}_{v}^{t} \odot \mathbf{h}_{v}^{t-1}\right)\right) \\ \mathbf{h}_{v}^{t}=\left(1-\mathbf{z}_{v}^{t}\right) \odot \mathbf{h}_{v}^{t-1}+\mathbf{z}_{v}^{t} \odot \widetilde{\mathbf{h}}_{v}^{t} \end{array} \end{aligned}$ Tree LSTM (Child sum) 29 $\mathbf{h}_{\mathcal{N}_{v}}^{t}=\sum_{k \in \mathcal{N}_{v}} \mathbf{h}_{k}^{t-1}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{U}^{i} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{U}^{o} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{U}^{u} \mathbf{h}_{\mathcal{N}_{v}}^{t}+\mathbf{b}^{u}\right)\\ &\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$ Tree LSTM (N-ary) 29 $\begin{aligned} &\mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{l=1}^{K} \mathbf{U}_{l}^{i} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v} k}^{t f}=\sum_{l=1}^{K} \mathbf{U}_{k l}^{f} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{l=1}^{K} \mathbf{U}_{l}^{o} \mathbf{h}_{v l}^{t-1}\\ &\mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{l=1}^{K} \mathbf{U}_{l}^{u} \mathbf{h}_{v l}^{t-1} \end{aligned}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v},}^{t i}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v} k}^{f f}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &\mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{l=1}^{K} \mathbf{f}_{v l}^{t} \odot \mathbf{c}_{v l}^{t-1}\\ &\mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{aligned}$ Graph LSTM 30 $\begin{aligned} \mathbf{h}_{\mathcal{N}_{v}}^{t i}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{i} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t o}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{o} \mathbf{h}_{k}^{t-1} \\ \mathbf{h}_{\mathcal{N}_{v}}^{t u}=\sum_{k \in \mathcal{N}_{v}} \mathbf{U}_{m(v, k)}^{u} \mathbf{h}_{k}^{t-1} \end{aligned}$ $\begin{aligned} &\mathbf{i}_{v}^{t}=\sigma\left(\mathbf{W}^{i} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t i}+\mathbf{b}^{i}\right)\\ &\mathbf{f}_{v k}^{t}=\sigma\left(\mathbf{W}^{f} \mathbf{x}_{v}^{t}+\mathbf{U}_{m(v, k)}^{f} \mathbf{h}_{k}^{t-1}+\mathbf{b}^{f}\right)\\ &\mathbf{o}_{v}^{t}=\sigma\left(\mathbf{W}^{o} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t o}+\mathbf{b}^{o}\right)\\ &\mathbf{u}_{v}^{t}=\tanh \left(\mathbf{W}^{u} \mathbf{x}_{v}^{t}+\mathbf{h}_{\mathcal{N}_{v}}^{t u}+\mathbf{b}^{u}\right)\\ &\begin{array}{l} \mathbf{c}_{v}^{t}=\mathbf{i}_{v}^{t} \odot \mathbf{u}_{v}^{t}+\sum_{k \in \mathcal{N}_{v}} \mathbf{f}_{v k}^{t} \odot \mathbf{c}_{k}^{t-1} \\ \mathbf{h}_{v}^{t}=\mathbf{o}_{v}^{t} \odot \tanh \left(\mathbf{c}_{v}^{t}\right) \end{array} \end{aligned}$ Graph Attention Networks 与 GCN 对于节点所有的邻居平等对待相比,注意力机制可以为每个邻居分配不同的注意力评分,从而识别更重要的邻居。 GAT 31 将注意力机制引入传播过程,其遵循自注意力机制,通过对每个节点邻居的不同关注更新隐含状态。GAT 定义了一个图注意力层(graph attentional layer),通过堆叠构建图注意力网络。对于节点对 $\left(i, j\right)$,基于注意力机制的系数计算方式如下: $$ \alpha_{i j}=\frac{\exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{j}\right]\right)\right)}{\sum_{k \in N_{i}} \exp \left(\text { LeakyReLU }\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{k}\right]\right)\right)} $$ 其中,$\alpha_{i j}$ 表示节点 $j$ 对 $i$ 的注意力系数,$N_i$ 表示节点 $i$ 的邻居。令 $\mathbf{h}=\left\{\vec{h}_{1}, \vec{h}_{2}, \ldots, \vec{h}_{N}\right\}, \vec{h}_{i} \in \mathbb{R}^{F}$ 表示输入节点特征,其中 $N$ 为节点的数量,$F$ 为特征维度,则节点的输出特征(可能为不同维度 $F^{\prime}$)为 $\mathbf{h}^{\prime}=\left\{\vec{h}_{1}^{\prime}, \vec{h}_{2}^{\prime}, \ldots, \vec{h}_{N}^{\prime}\right\}, \vec{h}_{i}^{\prime} \in \mathbb{R}^{F^{\prime}}$。$\mathbf{W} \in \mathbb{R}^{F^{\prime} \times F}$ 为所有节点共享的线性变换的权重矩阵,$a: \mathbb{R}^{F^{\prime}} \times \mathbb{R}^{F^{\prime}} \rightarrow \mathbb{R}$ 用于计算注意力系数。最后的输出特征计算方式如下: $$ \vec{h}_{i}^{\prime}=\sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j} \mathbf{W} \vec{h}_{j}\right) $$ 注意力层采用多头注意力机制来稳定学习过程,之后应用 $K$ 个独立的注意力机制计算隐含状态,最后通过拼接或平均得到输出表示: $$ \vec{h}_{i}^{\prime}=\Vert_{k=1}^{K} \sigma\left(\sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$ $$ \vec{h}_{i}^{\prime}=\sigma\left(\frac{1}{K} \sum_{k=1}^{K} \sum_{j \in \mathcal{N}_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) $$ 其中,$\Vert$ 表示连接操作,$\alpha_{ij}^k$ 表示第 $k$ 个注意力机制计算得到的标准化的注意力系数。整个模型如下图所示: GAT 中的注意力架构有如下几个特点: 针对节点对的计算是并行的,因此计算过程是高效的。 可以处理不同度的节点并对邻居分配对应的权重。 可以容易地应用到归纳学习问题中去。 应用 图神经网络已经被应用在监督、半监督、无监督和强化学习等多个领域。下图列举了 GNN 在不同领域内相关问题中的应用,具体模型论文请参考 Graph Neural Networks: A Review of Methods and Applications 原文 19。 开放资源 开源实现 项目 框架 rusty1s/pytorch_geometric PyTorch dmlc/dgl PyTorch, TF & MXNet alibaba/euler TF alibaba/graph-learn TF deepmind/graph_nets TF & Sonnet facebookresearch/PyTorch-BigGraph PyTorch tencent/plato PaddlePaddle/PGL PaddlePaddle Accenture/AmpliGraph TF danielegrattarola/spektral TF THUDM/cogdl PyTorch DeepGraphLearning/graphvite PyTorch 论文列表和评测 Must-read papers on network representation learning (NRL) / network embedding (NE) Must-read papers on graph neural networks (GNN) DeepGraphLearning/LiteratureDL4Graph nnzhan/Awesome-Graph-Neural-Networks graphdeeplearning/benchmarking-gnns Open Graph Benchmark Cai, H., Zheng, V. W., & Chang, K. C. C. (2018). A comprehensive survey of graph embedding: Problems, techniques, and applications. IEEE Transactions on Knowledge and Data Engineering, 30(9), 1616-1637. ↩︎ Goyal, P., & Ferrara, E. (2018). Graph embedding techniques, applications, and performance: A survey. Knowledge-Based Systems, 151, 78-94. ↩︎ Hamilton, W. L., Ying, R., & Leskovec, J. (2017). Representation learning on graphs: Methods and applications. arXiv preprint arXiv:1709.05584. ↩︎ Perozzi, B., Al-Rfou, R., & Skiena, S. (2014). Deepwalk: Online learning of social representations. In Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 701-710). ↩︎ Grover, A., & Leskovec, J. (2016). node2vec: Scalable feature learning for networks. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 855-864). ↩︎ Fogaras, D., Rácz, B., Csalogány, K., & Sarlós, T. (2005). Towards scaling fully personalized pagerank: Algorithms, lower bounds, and experiments. Internet Mathematics, 2(3), 333-358. ↩︎ Haveliwala, T. H. (2002). Topic-sensitive PageRank. In Proceedings of the 11th international conference on World Wide Web (pp. 517-526). ↩︎ Cao, S., Lu, W., & Xu, Q. (2015). Grarep: Learning graph representations with global structural information. In Proceedings of the 24th ACM international on conference on information and knowledge management (pp. 891-900). ↩︎ Ou, M., Cui, P., Pei, J., Zhang, Z., & Zhu, W. (2016). Asymmetric transitivity preserving graph embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1105-1114). ↩︎ Dong, Y., Chawla, N. V., & Swami, A. (2017). metapath2vec: Scalable representation learning for heterogeneous networks. In Proceedings of the 23rd ACM SIGKDD international conference on knowledge discovery and data mining (pp. 135-144). ↩︎ Fu, T. Y., Lee, W. C., & Lei, Z. (2017). Hin2vec: Explore meta-paths in heterogeneous information networks for representation learning. In Proceedings of the 2017 ACM on Conference on Information and Knowledge Management (pp. 1797-1806). ↩︎ Wang, D., Cui, P., & Zhu, W. (2016). Structural deep network embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1225-1234). ↩︎ Cao, S., Lu, W., & Xu, Q. (2016). Deep neural networks for learning graph representations. In Thirtieth AAAI conference on artificial intelligence. ↩︎ Tang, J., Qu, M., Wang, M., Zhang, M., Yan, J., & Mei, Q. (2015). Line: Large-scale information network embedding. In Proceedings of the 24th international conference on world wide web (pp. 1067-1077). ↩︎ Walker, A. J. (1974). New fast method for generating discrete random numbers with arbitrary frequency distributions. Electronics Letters, 10(8), 127-128. ↩︎ Walker, A. J. (1977). An efficient method for generating discrete random variables with general distributions. ACM Transactions on Mathematical Software (TOMS), 3(3), 253-256. ↩︎ Zhang, Z., Cui, P., & Zhu, W. (2020). Deep learning on graphs: A survey. IEEE Transactions on Knowledge and Data Engineering. ↩︎ Wu, Z., Pan, S., Chen, F., Long, G., Zhang, C., & Philip, S. Y. (2020). A comprehensive survey on graph neural networks. IEEE Transactions on Neural Networks and Learning Systems. ↩︎ Zhou, J., Cui, G., Zhang, Z., Yang, C., Liu, Z., Wang, L., … & Sun, M. (2018). Graph neural networks: A review of methods and applications. arXiv preprint arXiv:1812.08434. ↩︎ ↩︎ ↩︎ Liu, Z., & Zhou, J. (2020). Introduction to Graph Neural Networks. Synthesis Lectures on Artificial Intelligence and Machine Learning, 14(2), 1–127. ↩︎ Scarselli, F., Gori, M., Tsoi, A. C., Hagenbuchner, M., & Monfardini, G. (2008). The graph neural network model. IEEE Transactions on Neural Networks, 20(1), 61-80. ↩︎ Khamsi, M. A., & Kirk, W. A. (2011). An introduction to metric spaces and fixed point theory (Vol. 53). John Wiley & Sons. ↩︎ Defferrard, M., Bresson, X., & Vandergheynst, P. (2016). Convolutional neural networks on graphs with fast localized spectral filtering. In Advances in neural information processing systems (pp. 3844-3852). ↩︎ Kipf, T. N., & Welling, M. (2016). Semi-supervised classification with graph convolutional networks. arXiv preprint arXiv:1609.02907. ↩︎ Duvenaud, D. K., Maclaurin, D., Iparraguirre, J., Bombarell, R., Hirzel, T., Aspuru-Guzik, A., & Adams, R. P. (2015). Convolutional networks on graphs for learning molecular fingerprints. In Advances in neural information processing systems (pp. 2224-2232). ↩︎ Atwood, J., & Towsley, D. (2016). Diffusion-convolutional neural networks. In Advances in neural information processing systems (pp. 1993-2001). ↩︎ Hamilton, W., Ying, Z., & Leskovec, J. (2017). Inductive representation learning on large graphs. In Advances in neural information processing systems (pp. 1024-1034). ↩︎ Li, Y., Tarlow, D., Brockschmidt, M., & Zemel, R. (2015). Gated graph sequence neural networks. arXiv preprint arXiv:1511.05493. ↩︎ ↩︎ Tai, K. S., Socher, R., & Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers) (pp. 1556-1566). ↩︎ ↩︎ Peng, N., Poon, H., Quirk, C., Toutanova, K., & Yih, W. T. (2017). Cross-sentence n-ary relation extraction with graph lstms. Transactions of the Association for Computational Linguistics, 5, 101-115. ↩︎ Veličković, P., Cucurull, G., Casanova, A., Romero, A., Lio, P., & Bengio, Y. (2017). Graph attention networks. arXiv preprint arXiv:1710.10903. ↩︎

2020/4/11
articleCard.readMore

预训练自然语言模型 (Pre-trained Models for NLP)

本文为 Pre-trained Models for Natural Language Processing: A Survey 和相关模型的读书笔记 1。 在当下的 NLP 研究领域,随着计算机算力的不断增强,越来越多的通用语言表征的预训练模型(Pre-trained Models,PTMs)逐渐涌现出来。这对下游的 NLP 任务非常有帮助,可以避免大量从零开始训练新的模型。PTM 大致可以分为两代: 第一代 PTM 旨在学习词嵌入。由于下游任务不在需要这些模型,因此为了计算效率,这些模型往往采用浅层模型,例如 Skip-Gram 2,GloVe 3 等。尽管这些模型可以捕获词的语义,但由于未基于上下文环境,因此不能够捕捉到更深层次的概念,例如:句法结构,语义角色,指代等等。 第二代 PTM 专注于学习基于上下文的词嵌入,例如 CoVe 4,ELMo 5,OpenAI GPT 6 和 BERT 7 等。这些学习到的编码器在下游任务中仍会用于词在上下文中的语义表示。 预训练原理 语言表示学习 分布式表示的核心思想为用一个低维的实值向量表示一段文本,向量单独每个维度不具有任何实质含义,但整个向量表示了一个具体的概念。下图展示了一个 NLP 任务的一般神经网络架构: 词嵌入包含两种类型:上下文无关的词嵌入和基于上下文的词嵌入。两者的不同点在于一个词的嵌入是够会随着上下文的不同而随之改变。 上下文无关的词嵌入 为了表征语义,我们需要将离散的语言符号映射到一个分布式嵌入空间中。对于词典 $\mathcal{V}$ 中的一个词 $x$,我们将其映射为查询表 $\mathbf{E} \in \mathbb{R}^{D_e \times \|\mathcal{V}\|}$ 中的一个向量 $\mathbf{e}_x \in \mathbb{R}^{D_e}$,其中 $D_e$ 为嵌入的维度。 这种类型的嵌入主要有两个缺陷:一是嵌入是静态的,词在不同的上下文中的嵌入表示是相同的,因此无法处理一词多义;二是未登录词(out-of-vocabulary,OOV)问题,通常可以采用字符级嵌入表示解决该问题。更多上下文无关的词嵌入模型,请参见之前的博客 词向量。 基于上下文的词嵌入 为了解决上述问题,我们需要区分在不同上下文下词的含义。给定一段文本 $x_1, x_2, \dotsc, x_T$ 其中每段标记 $x_t \in \mathcal{V}$ 为一个词或子词,$x_t$ 基于上下文的表示依赖于整段文本。 $$ \left[\mathbf{h}_1, \mathbf{h}_2, \dotsc, \mathbf{h}_T\right] = f_{\text{enc}} \left(x_1, x_2, \dotsc, x_T\right) $$ 其中,$f_{\text{enc}} \left(\cdot\right)$ 为神经编码器,$\mathbf{h}_t$ 为标记 $x_t$ 的基于上下文的嵌入或动态嵌入。 神经上下文编码器 神经上下文编码器大致可以分为 3 类: 基于卷积的模型:基于卷积的模型通过卷积操作从一个词的邻居中聚合局部信息来捕获这个词的含义 8。 Convolutional model 基于序列的模型:基于序列的模型采用 RNNs(LSTM 9 和 GRU 10) 来捕获词的上下文信息。实际中,我们采用双向的 RNNs 从词的两端收集信息,不过整体效果容易收到长期依赖问题的影响。 Sequential model 基于图的模型:基于图的模型将字作为图中的一个节点来学习上下文表示,这个图通常是一个词之间预定义的语言结构,例如:语法结构 11 12 或语义关系 13。尽管基于语言学的图结构能提供有用的信息,但如何构建一个好的图结构则成为了难题。除此之外,基于语言学的图结构需要依赖专家知识和外部工具,例如:依存句法分析等。事实上,我们会采用一个更直接的方式去学习任意两个词之间的关系,通常连接的权重可以通过自注意力机制自动计算得出。Transformer 14 是一个采用了全链接自注意力架构的实现,同时也采用了位置嵌入(positional embedding),层标准化(layer normalization)和残差连接(residual connections)等网络设计理念。 Fully-connected graph-based model 为什么预训练 对于大多数的 NLP 任务,构建一个大规模的有标签的数据集是一项很大的挑战。相反,大规模的无标签语料是相对容易构建的,为了充分利用这些无标签数据,我们可以先利用它们获取一个好的语言表示,再将这些表示用于其他任务。预训练的好处如下: 预训练可以从大规模语料中学习得到通用的语言表示,并用于下游任务。 预训练提供了更优的模型初始化方法,有助于提高模型的泛化能力和加速模型收敛。 预训练可以当作是在小数据集上一种避免过拟合的正则化方法。 预训练任务 预训练任务对于学习语言的通用表示来说至关重要。通常情况下,预训练任务具有挑战性,同时需要大量训练数据。我们将预训练任务划分为 3 类: 监督学习,即从包含输入输出对的训练数据中学习一个由输入到输出的映射函数。 非监督学习,即从无标签数据获取一些固有的知识,例如:聚类,密度,潜在表征等。 自监督学习,是监督学习和非监督学习的混合体,核心思想是对于输入的一部分利用其他部分进行预测。 语言模型(Language Modeling,LM) NLP 中最常见的非监督任务为概率语言建模,这是一个经典的概率密度估计问题。给定一个文本序列 $x_{1:T} = \left[x_1, x_2, \dotsc, x_T\right]$,他的联合概率 $p \left(x_{1:T}\right)$ 可以分解为: $$ p \left(x_{1:T}\right) = \prod_{t=1}^{y}{p \left(x_t \mid x_{0:t-1}\right)} $$ 其中 $x_0$ 为序列开始的特殊标记。条件概率 $p \left(x_t \mid x_{0:t-1}\right)$ 可以通过给定的语言上下文 $x_{0:t-1}$ 词的概率分布进行建模估计。上下文 $x_{0:t-1}$ 可以通过神经编码器 $f_{\text{enc}} \left(\cdot\right)$ 进行建模,则条件概率可以表示为: $$ p \left(x_t | x_{0:t-1}\right) = g_{\text{LM}} \left(f_{\text{enc}} \left(x_{0:t-1}\right)\right) $$ 其中,$g_{\text{LM}}$ 为预测层。 遮罩语言模型(Masked Language Modeling,MLM) 大致上来说,MLM 首先将输入句子的一些词条进行遮挡处理,其次再训练模型利用剩余的部分预测遮挡的部分。这种预训练方法会导致在预训练(pre-training)阶段和微调(fine-tuning)阶段的不一致,因为在微调阶段遮挡标记并未出现,BERT 15 通过一个特殊的符号 [MASK] 对其进行处理。 Sequence-to-Sequence MLM (Seq2Seq MLM) MLM 通常以一个分类问题进行求解,我们将遮挡后的序列输入到一个神经编码器,再将输出向量传给一个 Softmax 分类器来预测遮挡的字符。我们可以采用 Encoder-Decoder(Seq2Seq)网络结构,将遮挡的序列输入到 Encoder,Decoder 则会循序的产生被遮挡的字符。MASS 16 和 T5 17 均采用了这种序列到序列的 MLM 结构,这种结构对 Seq2Seq 风格的下游任务很有帮助,例如:问答,摘要和机器翻译。 Enhanced Masked Language Modeling (E-MLM) 同时,大量研究对于 BERT 所使用的遮罩处理进行了改进。RoBERTa 18 采用了一种动态的遮罩处理。UniLM 将遮罩任务拓展到 3 种不同的类型:单向的,双向的和 Seq2Seq 类型的。 排列语言模型(Permuted Language Modeling,PLM) 在 MLM 中一些特殊字符(例如:[MASK])在下游任务中是无用的,为了解决这个问题,XLNet 19 提出了一种排列语言模型(Permuted Language Modeling,PLM)用于替代 MLM。简言之,PLM 是对输入序列的排列进行语言建模。给定一个序列,从所有可能的排列中随机抽样得到一个排列,将排列后的序列中的一些字符作为模型的预测目标,利用其他部分和目标的自然位置进行训练。需要注意的是这种排列并不会影响序列的自然位置,其仅用于定义字符预测的顺序。 去噪自编码(Denoising Autoencoder,DAE) DAE 旨在利用部分有损的输入恢复原始无损的输入。对于语言模型,例如 Seq2Seq 模型,可以采用标准的 Transformer 来重构原始文本。有多种方式可以对文本进行破坏 20: 字符遮罩:随机采样字符并将其替换为 [MASK]。 字符删除:随机的从输入中删除字符,不同于字符遮罩,模型需要确定丢失字符的位置。 文本填充:采样一段文本并将其替换为一个 [MASK],每段文本的长度服从泊松分布($\lambda = 3$),模型需要确定这段文本中缺失的字符个数。 句子重排:将文档以终止标点进行分割,再进行随机排序。 文档旋转:随机均匀地选择一个字符,对文档进行旋转使得这个字符作为文档的起始字符,模型需要确定文档真实的起始位置。 对比学习(Contrastive Learning,CTL) 对比学习 21 假设一些观测到的文本对比随机采样的文本具有更相似的语义。对于文本对 $\left(x, y\right)$ 通过最小化如下目标函数来学习评分函数 $s \left(x, y\right)$: $$ \mathbb{E}_{x, y^+, y^-} \left[- \log \dfrac{\exp \left(s \left(x, y^+\right)\right)}{\exp \left(s \left(x, y^+\right)\right) + \exp \left(s \left(x, y^-\right)\right)}\right] $$ 其中,$\left(x, y^+\right)$ 为一个相似对,$y^-$ 对于 $x$ 而言假定为不相似,$y^+$ 和 $y^-$ 通常称之为正样本和负样本。评分函数 $s \left(x, y\right)$ 通过一个神经编码器计算可得,$s \left(x, y\right) = f^{\top}_{\text{enc}} \left(x\right) f_{\text{enc}} \left(y\right)$ 或 $s \left(x, y\right) = f_{\text{enc}} \left(x \oplus y\right)$。CTL 的核心思想是“通过对比进行学习”。 下图展示了预训练模型的分类和部分代表模型: 应用于下游任务 如何迁移 选择合适的预训练任务,模型架构和语料 不同的 PTMs 在相同的下游任务上有着不同的效果,这是因为 PTMs 有着不同的预训练任务,模型架构和语料。 目前,语言模型是最流行的预训练任务,同时也可以有效地解决很多 NLP 问题。但是不同的预训练任务有着自己的侧重,在不同的任务上会有不同的效果。例如:NSP 任务使得 PTM 可以理解两句话之间的关系,因此 PTM 可以在例如问答(Question Answering,QA)和自然语言推理(Natural Language Inference,NLI)等下游任务上表现更好。 PTM 的网络架构对下游任务也至关重要。例如:尽管 BERT 可以处理大多数自然语言理解任务,对其很难生成语言。 下游任务的数据分布应该和 PTM 训练所用语料相似。目前,大量现成的 PTM 仅可以快速地用于特定领域或特定语言的下游任务上。 选择合适的网络层 给定一个预训练的模型,不同的网络层捕获了不同的信息,例如:词性标记(POS tagging),语法(parsing),长期依赖(long-term dependencies),语义角色(semantic roles),指代(coreference)等。Tenney 22 等人发现 BERT 表示方式类似传统的 NLP 流程:基础的句法信息出现在浅层的网络中,高级的语义信息出现在更高的层级中。 令 $\mathbf{H}^{\left(l\right)} \left(1 \leq l \leq L\right)$ 表示共 $L$ 层的预训练模型的第 $l$ 层表示,$g \left(\cdot\right)$ 表示用于特定任务的的模型。一般有 3 中情况选择表示: Embedding Only:一种情况是仅选用预训练模型的静态嵌入,模型的其他部分仍需作为一个任务从头训练。这种情况不能够获取到一些有用的深层信息,词嵌入仅能够捕获词的语义信息。 Top Layer:最简单有效的方式是将网络的顶层表示输入到模型中 $g \left(\mathbf{H}^{\left(L\right)}\right)$。 All Layers:另一种更灵活的方式是自动选择最合适的层,例如 ELMo: $$ \mathbf{r}_t = \gamma \sum_{l=1}^{L}{\alpha_l \mathbf{h}^{\left(l\right)}_t} $$ 其中 $\alpha_l$ 是层 $l$ 的 softmax 归一的权重,$\gamma$ 是用于缩放预训练模型输出向量的一个标量值,再将不同层的混合输出输入到后续模型中 $g \left(\mathbf{r}_t\right)$。 是否微调 目前,主要有两种方式进行模型迁移:特征提取(预训练模型的参数是固定的)和模型微调(预训练模型的参数是经过微调的)。当采用特征提取时,预训练模型可以被看作是一个特征提取器。除此之外,我们应该采用内部层作为特征,因为他们通常是最适合迁移的特征。尽管两种不同方式都能对大多数 NLP 任务效果有显著提升,但以特征提取的方式需要更复杂的特定任务的架构。因此,微调是一种更加通用和方便的处理下游任务的方式。 微调策略 随着 PTMs 网络层数的加深,其捕获的表示使得下游任务变得越来越简单,因此整个模型中用于特定任务的网络层一般比较简单,微调已经成为了采用 PTMs 的主要方式。但是微调的过程通常是比较不好预估的,即使采用相同的超参数,不同的随机数种子也可能导致差异较大的结果。除了标准的微调外,如下为一些有用的微调策略: 两步骤微调 一种方式是两阶段的迁移,在预训练和微调之间引入了一个中间阶段。在第一个阶段,PTM 通过一个中间任务或语料转换为一个微调后的模型,在第二个阶段,再利用目标任务进行微调。 多任务微调 在多任务学习框架下对其进行微调。 利用额外模块进行微调 微调的主要缺点就是其参数的低效性。每个下游模型都有其自己微调好的参数,因此一个更好的解决方案是将一些微调好的适配模块注入到 PTMs 中,同时固定原始参数。 开放资源 PTMs 开源实现: 项目 框架 PTMs word2vec - CBOW, Skip-Gram GloVe - Pre-trained word vectors FastText - Pre-trained word vectors Transformers PyTorch & TF BERT, GPT-2, RoBERTa, XLNet, etc. Fairseq PyTorch English LM, German LM, RoBERTa, etc. Flair PyTorch BERT, ELMo, GPT, RoBERTa, XLNet, etc. AllenNLP PyTorch ELMo, BERT, GPT-2, etc. FastNLP PyTorch BERT, RoBERTa, GPT, etc. Chinese-BERT - BERT, RoBERTa, etc. (for Chinese) BERT TF BERT, BERT-wwm RoBERTa PyTorch XLNet TF ALBERT TF T5 TF ERNIE(THU) PyTorch ERNIE(Baidu) PaddlePaddle Hugging Face PyTorch & TF 很多… 论文列表和 PTMs 相关资源: 资源 URL 论文列表 https://github.com/thunlp/PLMpapers 论文列表 https://github.com/tomohideshibata/BERT-related-papers 论文列表 https://github.com/cedrickchee/awesome-bert-nlp Bert Lang Street https://bertlang.unibocconi.it BertViz https://github.com/jessevig/bertviz 预训练模型 CoVe (2017) 4 首先,给定一个源语言序列 $w^x = \left[w^x_1, \dotsc, w^x_n\right]$ 和一个翻译目标语言序列 $w^z = \left[w^z_1, \dotsc, w^z_n\right]$。令 $\text{GloVe} \left(w^x\right)$ 为词 $w^x$ 对应的 GloVe 向量,$z$ 为 $w^z$ 中的词随机初始化的词向量。将 $\text{GloVe} \left(w^x\right)$ 输入到一个标准的两层 biLSTM 网络中,称之为 MT-LSTM,MT-LSTM 用于计算序列的隐含状态如下: $$ h = \text{MT-LSTM} \left(\text{GloVe} \left(w^x\right)\right) $$ 对于机器翻译,MT-LSTM 的注意力机制的解码器可以对于输出的词在每一步产生一个分布 $p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right)$。在 $t$ 步,解码器利用一个两层的单向 LSTM 基于之前目标词嵌入 $z_{t-1}$ 和一个基于上下文调整的隐含状态 $\tilde{h}_{t-1}$ 生成一个隐含状态 $h^{\text{dec}}_t$: $$ h^{\text{dec}}_t = \text{LSTM} \left(\left[z_{t-1}; \tilde{h}_{t-1}\right], h^{\text{dec}}_{t-1}\right) $$ 之后解码器计算每一步编码到当前解码状态的注意力权重 $\alpha$: $$ \alpha_t = \text{softmax} \left(H \left(W_1 h^{\text{dec}}_t + b_1\right)\right) $$ 其中 $H$ 表示 $h$ 按照时间维度的堆叠。之后解码器将这些权重作为相关性用于计算基于上下文调整的隐含状态 $\tilde{h}$: $$ \tilde{h}_t = \text{tanh} \left(W_2 \left[H^{\top} \alpha_t; h^{\text{dec}}_t\right] + b_2\right) $$ 最后,输出词的分布通过基于上下文调整的隐含状态计算可得: $$ p \left(\hat{w}^z_t \mid H, w^z_1, \dotsc, w^z_{t-1}\right) = \text{softmax} \left(W_{\text{out}} \tilde{h}_t + b_{\text{out}}\right) $$ CoVe 将 MT-LSTM 学习到的表示迁移到下游任务中,令 $w$ 表示文字序列,$\text{GloVe} \left(w\right)$ 表示对应的 GloVe 向量,则: $$ \text{CoVe} \left(w\right) = \text{MT-LSTM} \left(\text{GloVe} \left(w\right)\right) $$ 表示由 MT-LSTM 产生的上下文向量,对于分类和问答任务,有一个输入序列 $w$,我们可以将 GloVe 和 CoVe 向量进行拼接作为其嵌入表示: $$ \tilde{w} = \left[\text{GloVe} \left(w\right); \text{CoVe} \left(w\right)\right] $$ CoVe 网络架构示意图如下: ELMo (2018) 5 在 ELMo 模型中,对于每个词条 $t_k$,一个 $L$ 层的 biLM 可以计算得到 $2L + 1$ 个表示: $$ \begin{aligned} R_k &= \left\{\mathbf{x}^{LM}_k, \overrightarrow{\mathbf{h}}^{LM}_{k, j}, \overleftarrow{\mathbf{h}}^{LM}_{k, j} \mid j = 1, \dotsc, L \right\} \\ &= \left\{\mathbf{h}^{LM}_{k, j} \mid j = 0, \dotsc, L\right\} \end{aligned} $$ 其中 $\mathbf{h}^{LM}_{k, 0}$ 为词条的嵌入层,$\mathbf{h}^{LM}_{k, j} = \left[\overrightarrow{\mathbf{h}}^{LM}_{k, j}; \overleftarrow{\mathbf{h}}^{LM}_{k, j}\right]$ 为每个 biLSTM 层。 对于下游任务,ELMo 将 $R$ 中的所有层汇总成一个向量 $\mathbf{ELMo}_k = E \left(R_k; \mathbf{\Theta}_e\right)$。在一些简单的案例中,ELMo 仅选择顶层,即:$E \left(R_k\right) = \mathbf{h}^{LM}_{k, L}$。更通用的,对于一个特定的任务,我们可以计算一个所有 biLM 层的加权: $$ \mathbf{ELMo}^{task}_k = E \left(R_k; \Theta^{task}\right) = \gamma^{task} \sum_{j=0}^{L}{s^{task}_j \mathbf{h}^{LM}_{k, j}} $$ 其中,$s^{task}$ 表示 softmax 归一化后的权重,$\gamma^{task}$ 允许模型对整个 ELMo 向量进行缩放。$\gamma$ 对整个优化过程具有重要意义,考虑每个 biLM 层的激活具有不同的分布,在一些情况下这相当于在进行加权之前对每一个 biLM 层增加了层标准化。 ELMo 网络架构示意图如下 23: GPT (2018) 6 给定一个语料 $\mathcal{U} = \left\{u_1, \dotsc, u_n\right\}$,使用标准的语言建模目标来最大化如下似然: $$ L_1 \left(\mathcal{U}\right) = \sum_{i} \log P \left(u_i \mid u_{i-k}, \dotsc, u_{i-1}; \Theta\right) $$ 其中,$k$ 为上下文窗口的大小,条件概率 $P$ 通过参数为 $\Theta$ 的神经网络进行建模。GPT 中使用了一个多层的 Transformer Decoder 作为语言模型。模型首先对输入上下文词条应用多头自注意力机制,再通过按位置的前馈层产生目标词条的输出分布: $$ \begin{aligned} h_0 &= UW_e + W_p \\ h_l &= \text{transformer\_black} \left(h_{l-1}\right), \forall i \in \left[1, n\right] \\ P \left(u\right) &= \text{softmax} \left(h_n W^{\top}_e\right) \end{aligned} $$ 其中,$U = \left(u_{-k}, \dotsc, u_{-1}\right)$ 为词条的上下文向量,$n$ 为网络层数,$W_e$ 为词条的嵌入矩阵,$W_p$ 为位置嵌入矩阵。 给定一个有标签的数据集 $\mathcal{C}$,其中包含了输入词条序列 $x^1, \dotsc, x^m$ 和对应的标签 $y$。利用上述预训练的模型获得输入对应的最后一个 Transformer 的激活输出 $h^m_l$,之后再将其输入到一个参数为 $W_y$ 的线性输入层中预测 $y$: $$ P \left(y \mid x^1, \dotsc, x^m\right) = \text{softmax} \left(h^m_l W_y\right) $$ 模型通过最小化如下损失进行优化: $$ L_2 \left(\mathcal{C}\right) = \sum_{\left(x, y\right)} \log P \left(y \mid x^1, \dotsc, x^m\right) $$ 研究还发现将语言建模作为微调的附加目标可以帮助提高模型的泛化能力,同时可以加速模型收敛。GPT 中采用如下的优化目标: $$ L_3 \left(\mathcal{C}\right) = L_2 \left(\mathcal{C}\right) + \lambda L_1 \left(\mathcal{C}\right) $$ GPT 网络架构示意图如下: BERT (2018) 7 BERT 采用了一中基于 Vaswani 14 所提出模型的多层双向 Transformer 编码器。在 BERT 中,令 $L$ 为 Transformer Block 的层数,$H$ 为隐层大小,$A$ 为自注意力头的数量。在所有情况中,设置前馈层的大小为 $4H$,BERT 提供了两种不同大小的预训练模型: $\text{BERT}_{\text{BASE}}$:$L=12, H=768, A=12$,参数总量为 100 M。 $\text{BERT}_{\text{LARGE}}$:$L=24, H=1024, A=16$,参数总量为 340 M。 $\text{BERT}_{\text{BASE}}$ 采用了同 GPT 相同的模型大小用于比较,不同与 GPT,BERT 使用了双向的注意力机制。在文献中,双向 Transformer 通常称之为 Transformer 编码器,仅利用左边上下文信息的 Transformer 由于可以用于文本生成被称之为 Transformer 解码器。BERT,GPT 和 ELMo 之间的不同如下图所示: BERT 的输入表示既可以表示一个单独的文本序列,也可以表示一对文本序列(例如:问题和答案)。对于一个给定的词条,其输入表示由对应的词条嵌入,分割嵌入和位置嵌入三部分加和构成,如下图所示: 具体的有: 采用一个包含 30,000 个词条的 WordPiece 嵌入 24。 位置嵌入最大支持 512 个词条。 序列的第一字符采用特殊的分类嵌入 [CLS],其最终的隐含状态在分类任务中用于汇总整个序列的表示,对于非分类任务则忽视该向量。 句子对被整合成一个序列,首先利用一个特殊词条 [SEP] 对句子进行分割,其次对于第一个句子中的每个词条叠加一个学习到的 A 句子嵌入,对于第二个句子中的每个词条叠加一个学习到的 B 句子嵌入。 对于一个单独的句子,仅使用 A 句子嵌入。 在预训练阶段,BERT 采用了两个无监督预测任务: 遮罩的语言模型(Masked LM,MLM) [MASK] 进行遮挡,BERT 选择采用 80% 的 [MASK],10% 的随机词和 10% 保留原始词的方式对随机选择的 15% 的词条进行遮挡处理。由于编码器不知会预测哪个词或哪个词被随机替换了,这迫使其必须保留每个输入词条的分布式上下文表示。同时 1.5% 的随机替换也不会过多的损害模型的理解能力。 预测是否为下一个句子(Next Sentence Prediction) 基于 BERT 的不同下游任务的实现形式如下图所示: UniLM (2019) 25 给定一个输入序列 $x = x_1 \cdots x_{|x|}$,UniLM 通过下图的方式获取每个词条的基于上下文的向量表示。整个预训练过程利用单向的语言建模(unidirectional LM),双向的语言建模(bidirectional LM)和 Seq2Seq 语言建模(sequence-to-sequence LM)优化共享的 Transformer 网络。 输入序列 $x$ 对于单向语言模型而言是一个分割的文本,对于双向语言模型和 Seq2Seq 语言模型而言是一对打包的分割文本。UniLM 在输入的起始位置添加特殊的 [SOS] (start-of-sequence),在结尾处添加 [EOS](end-of-sequence)。[EOS] 对于自然语言理解(NLU)任务可以标记句子之间的界线,对于自然语言生成(NLG)任务可以确定解码过程停止的时间。输入的表示同 BERT 一样,文本利用 WordPiece 进行分割,对于每个输入词条,其向量表示为对应的词条嵌入,位置嵌入和分割嵌入的汇总。 对于输入向量 $\left\{\mathbf{x}_i\right\}^{|x|}_{i=1}$ 首先将其输入到隐层 $\mathbf{H}^0 = \left[\mathbf{x}_1, \dotsc, \mathbf{x}_{|x|}\right]$,之后使用一个 $L$ 层的 Transformer $\mathbf{H}^l = \text{Transformer}_l \left(\mathbf{H}^{l-1}\right), l \in \left[1, L\right]$ 对每一层 $\mathbf{H}^l = \left[\mathbf{h}^l_1, \dotsc, \mathbf{h}^l_{|x|}\right]$ 进行上下文表示编码。在每个 Tansformer 块中,使用多头自注意力机制对输出向量和上一层进行汇总,第 $l$ 层 Transformer 自注意力头 $\mathbf{A}_l$ 的输入通过如下方式计算: $$ \begin{aligned} \mathbf{Q} &= \mathbf{H}^{l-1} \mathbf{W}^Q_l, \mathbf{K} = \mathbf{H}^{l-1} \mathbf{W}^K_l, \mathbf{V} = \mathbf{H}^{l-1} \mathbf{W}^W_l \\ \mathbf{M}_{ij} &= \begin{cases} 0, & \text{allow to attend} \\ -\infty, & \text{prevent from attending} \end{cases} \\ \mathbf{A}_l &= \text{softmax} \left(\dfrac{\mathbf{Q} \mathbf{K}^{\top}}{\sqrt{d_k}} + \mathbf{M}\right) \mathbf{V}_l \end{aligned} $$ 其中,上一层的输出 $\mathbf{H}^{l-1} \in \mathbb{R}^{|x| \times d_h}$ 通过参数矩阵 $\mathbf{W}^Q_l, \mathbf{W}^K_l, \mathbf{W}^V_l \in \mathbb{R}^{d_h \times d_k}$ 线性地映射为相应的 Query,Key 和 Value,遮罩矩阵 $\mathbf{M} \in \mathbb{R}^{|x| \times |x|}$ 用于确定一对词条是否可以被相互连接。 Transformer-XL (2019) 26 将 Transformer 或注意力机制应用到语言建模中的核心问题是如何训练 Transformer 使其有效地将一个任意长文本编码为一个固定长度的表示。Transformer-XL 将整个语料拆分为较短的段落,仅利用每段进行训练并忽略之前段落的上下文信息。这种方式称之为 Vanilla Model 27,如下图所示: 在这种训练模式下,无论是前向还是后向信息都不会跨越分割的段落进行传导。利用固定长度的上下文主要有两个弊端: 这限制了最大依赖的长度,虽然自注意力机制不会像 RNN 一样受到梯度弥散的影响,但 Vanilla Model 也不能完全利用到这个优势。 虽然可以利用补全操作来实现句子或其他语义的分割,但实际上通常会简单的将一个长文本截断成一个固定长度的分割,这样会产生上下文分裂破碎的问题。 为了解决这个问题,Transformer-XL 采用了一种循环机制的 Transformer。在训练阶段,在处理新的分割段落时,之前分割分部分的隐含状态序列将被固定(fixed)和缓存(cached)下来作为一个扩展的上下文被复用参与计算,如下图所示: 虽然梯度仍仅限于这个分割段落内部,但网络可以从历史中获取信息,从而实现对长期依赖的建模。令两个长度为 $L$ 的连续分割段落为 $\mathbf{s}_{\tau} = \left[x_{\tau, 1}, \dotsc, x_{\tau, L}\right]$ 和 $\mathbf{s}_{\tau + 1} = \left[x_{\tau + 1, 1}, \dotsc, x_{\tau + 1, L}\right]$,第 $\tau$ 段分割 $\mathbf{s}_{\tau}$ 的第 $n$ 层隐含状态为 $\mathbf{h}^n_{\tau} \in \mathbb{R}^{L \times d}$,其中 $d$ 为隐含维度。则对于分割段落 $\mathbf{s}_{\tau + 1}$ 的第 $n$ 层隐含状态通过如下方式进行计算: $$ \begin{aligned} \tilde{\mathbf{h}}^{n-1}_{\tau + 1} &= \left[\text{SG} \left(\mathbf{h}^{n-1}_{\tau}\right) \circ \mathbf{h}^{n-1}_{\tau + 1} \right] \\ \mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1} &= \mathbf{h}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{q}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{k}, \tilde{\mathbf{h}}^{n-1}_{\tau + 1} \mathbf{W}^{\top}_{v} \\ \mathbf{h}^{n}_{\tau + 1} &= \text{Transformer-Layer} \left(\mathbf{q}^{n}_{\tau + 1}, \mathbf{k}^{n}_{\tau + 1}, \mathbf{v}^{n}_{\tau + 1}\right) \end{aligned} $$ 其中,$\text{SG} \left(\cdot\right)$ 表示停止梯度,$\left[\mathbf{h}_u \circ \mathbf{h}_v\right]$ 表示将两个隐含序列按照长度维度进行拼接,$\mathbf{W}$ 为模型的参数。与一般的 Transformer 相比,最大的不同在于 $\mathbf{k}^n_{\tau + 1}$ 和 $\mathbf{v}^n_{\tau + 1}$ 不仅依赖于 $\tilde{\mathbf{h}}^{n-1}_{\tau - 1}$ 还依赖于之前分割段落的 $\mathbf{h}^{n-1}_{\tau}$ 缓存。 在标准的 Transformer 中,序列的顺序信息通过位置嵌入 $\mathbf{U} \in \mathbb{R}^{L_{\max} \times d}$ 提供,其中第 $i$ 行 $\mathbf{U}_i$ 对应一个分割文本内部的第 $i$ 个绝对位置,$L_{\max}$ 为最大可能长度。在 Transformer-XL 中则是通过一种相对位置信息对其进行编码,构建一个相对位置嵌入 $\mathbf{R} \in \mathbb{R} ^{L_{\max} \times d}$,其中第 $i$ 行 $\mathbf{R}_i$ 表示两个位置之间相对距离为 $i$ 的嵌入表示。 对于一般的 Transformer,一个分割段落内部的 $q_i$ 和 $k_j$ 之间的注意力分数可以分解为: $$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{abs}} &=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(b)} \\ &+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\mathbf{U}_{i}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k} \mathbf{U}_{j}}_{(d)} \end{aligned} $$ 利用相对位置思想,变化如下: $$ \begin{aligned} \mathbf{A}_{i, j}^{\mathrm{rel}} &=\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(a)}+\underbrace{\mathbf{E}_{x_{i}}^{\top} \mathbf{W}_{q}^{\top} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(b)} \\ &+\underbrace{\textcolor{red}{u^{\top}} \mathbf{W}_{k, E} \mathbf{E}_{x_{j}}}_{(c)}+\underbrace{\textcolor{red}{v^{\top}} \mathbf{W}_{k, R} \textcolor{blue}{\mathbf{R}_{i-j}}}_{(d)} \end{aligned} $$ 首先,利用相对位置 $\textcolor{blue}{\mathbf{R}_{i-j}}$ 替代绝对位置嵌入 $\mathbf{U}_j$,这里 $\mathbf{R}$ 采用的是无需学习的 sinusoid 编码矩阵 14。 其次,引入了一个可训练的参数 $\textcolor{red}{u} \in \mathbb{R}^d$ 用于替换 $\mathbf{U}^{\top}_i \mathbf{W}^{\top}_q$。类似的,对于 $\mathbf{U}^{\top} \mathbf{W}^{\top}_q$ 使用一个可训练的 $\textcolor{red}{v} \in \mathbb{R}^d$ 替换。 最后,有意地划分了两个权重矩阵 $\mathbf{W}_{k, E}$ 和 $\mathbf{W}_{k, R}$ 用于生成基于内容的 Key 向量和基于位置的 Key 向量。 这样,$\left(a\right)$ 代表了基于内容的位置信息,$\left(b\right)$ 捕获了内容无关的位置偏置,$\left(c\right)$ 表示了一个全局的内容偏置,$\left(d\right)$ 捕获了一个全局的位置偏置。 利用一个自注意力头计算 $N$ 层的 Transformer-XL 的过程如下,对于 $n = 1, \dotsc, N$ 有: $$ \begin{aligned} \widetilde{\mathbf{h}}_{\tau}^{n-1}=&\left[\mathrm{SG}\left(\mathbf{m}_{\tau}^{n-1}\right) \circ \mathbf{h}_{\tau}^{n-1}\right] \\ \mathbf{q}_{\tau}^{n}, \mathbf{k}_{\tau}^{n}, \mathbf{v}_{\tau}^{n}=& \mathbf{h}_{\tau}^{n-1} {\mathbf{W}_{q}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{k, E}^{n}}^{\top}, \widetilde{\mathbf{h}}_{\tau}^{n-1} {\mathbf{W}_{v}^{n}}^{\top} \\ \mathbf{A}_{\tau, i, j}^{n}=& {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{k}_{\tau, j}^{n} + {\mathbf{q}_{\tau, i}^{n}}^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ &+u^{\top} \mathbf{k}_{\tau, j}+v^{\top} \mathbf{W}_{k, R}^{n} \mathbf{R}_{i-j} \\ \mathbf{a}_{\tau}^{n}=& \text { Masked-Softmax }\left(\mathbf{A}_{\tau}^{n}\right) \mathbf{v}_{\tau}^{n} \\ \mathbf{o}_{\tau}^{n}=& \text { LayerNorm } \left(\text{Linear}\left(\mathbf{a}_{\tau}^{n}\right)+\mathbf{h}_{\tau}^{n-1}\right) \\ \mathbf{h}_{\tau}^{n}=& \text { Positionwise-Feed-Forward }\left(\mathbf{o}_{\tau}^{n}\right) \end{aligned} $$ XLNet (2019) 19 给定一个序列 $\mathbf{X} = \left[x_1, \dotsc, x_T\right]$,AR 语言模型通过最大化如下似然进行预训练: $$ \max_{\theta} \quad \log p_{\theta}(\mathbf{x})=\sum_{t=1}^{T} \log p_{\theta}\left(x_{t} | \mathbf{x}_{<t}\right)=\sum_{t=1}^{T} \log \frac{\exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(h_{\theta}\left(\mathbf{x}_{1: t-1}\right)^{\top} e\left(x^{\prime}\right)\right)} $$ 其中,$h_{\theta}\left(\mathbf{x}_{1: t-1}\right)$ 是由 RNNs 或 Transformer 等神经网络网络模型生成的上下文表示,$e \left(x\right)$ 为 $x$ 的嵌入。对于一个文本序列 $\mathbf{x}$,BERT 首先构建了一个遮罩的数据集 $\hat{\mathbf{x}}$,令被遮挡的词条为 $\overline{\mathbf{x}}$,通过训练如下目标来利用 $\hat{\mathbf{x}}$ 重构 $\overline{\mathbf{x}}$: $$ \max_{\theta} \quad \log p_{\theta}(\overline{\mathbf{x}} | \hat{\mathbf{x}}) \approx \sum_{t=1}^{T} m_{t} \log p_{\theta}\left(x_{t} | \hat{\mathbf{x}}\right)=\sum_{t=1}^{T} m_{t} \log \frac{\exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(H_{\theta}(\hat{\mathbf{x}})_{t}^{\top} e\left(x^{\prime}\right)\right)} $$ 其中 $m_t = 1$ 表示 $x_t$ 是被遮挡的,$H_{\theta}$ 是一个 Transformer 将一个长度为 $T$ 的文本序列映射到一个隐含向量序列 $H_{\theta}(\mathbf{x})=\left[H_{\theta}(\mathbf{x})_{1}, H_{\theta}(\mathbf{x})_{2}, \cdots, H_{\theta}(\mathbf{x})_{T}\right]$。两种不同的预训练目标的优劣势如下 独立假设:BERT 中联合条件概率 $p(\overline{\mathbf{x}} | \hat{\mathbf{x}})$ 假设在给定的 $\hat{\mathbf{x}}$ 下,遮挡的词条 $\overline{\mathbf{x}}$ 是相关独立的,而 AR 语言模型则没有这样的假设。 输入噪声:BERT 在预训练是使用了特殊标记 [MASK],在下游任务微调时不会出现,而 AR 语言模型则不会存在这个问题。 上下文依赖:AR 语言模型仅考虑了词条左侧的上下文,而 BERT 则可以捕获两个方向的上下文。 为了利用 AR 语言模型和 BERT 的优点,XLNet 提出了排序语言模型。对于一个长度为 $T$ 序列 $\mathbf{x}$,共有 $T!$ 种不同的方式进行 AR 分解,如果模型共享不同分解顺序的参数,那么模型就能学习到两侧所有位置的信息。令 $\mathcal{Z}_T$ 为长度为 $T$ 的索引序列 $\left[1, 2, \dotsc, T\right]$ 的所有可能排列,$z_t$ 和 $\mathbf{z}_{<t}$ 分别表示一个排列 $\mathbf{z} \in \mathcal{Z}_T$ 第 $t$ 个和前 $t-1$ 个元素。则排列语言模型的优化目标为: $$ \max_{\theta} \quad \mathbb{E}_{\mathbf{z} \sim \mathcal{Z}_{T}}\left[\sum_{t=1}^{T} \log p_{\theta}\left(x_{z_{t}} | \mathbf{x}_{\mathbf{z}_{<t}}\right)\right] $$ 根据标准的 Transformer,下一个词条的分布 $p_{\theta}\left(X_{z_{t}} | \mathbf{x}_{\mathbf{z}<t}\right)$ 为: $$ p_{\theta}\left(X_{z_{t}} = x | \mathbf{x}_{\mathbf{z}<t}\right)=\frac{\exp \left(e(x)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)\right)} $$ 其中,$h_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}\right)$ 表示通过共享的 Transformer 产生的 $\mathbf{X}_{\mathbf{Z}<t}$ 的隐含表示。该表示并不依赖于所预测的位置,为了避免这个问题,我们将位置 $z_t$ 加入到模型中: $$ p_{\theta}\left(X_{z_{t}}=x | \mathbf{x}_{z_{<t}}\right)=\frac{\exp \left(e(x)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)\right)}{\sum_{x^{\prime}} \exp \left(e\left(x^{\prime}\right)^{\top} g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)\right)} $$ 对于 $g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 进行建模需要满足如下两个要求: 预测 $x_{z_t}$ 时,$g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 只能使用位置信息 $z_t$ 而不能使用内容信息 $x_{z_t}$。 在预测 $x_{z_t}$ 之后的词条时,$g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$ 又必须包含 $x_{z_t}$ 的语义信息。 为了解决这个问题,XLNet 提供了两种隐含表示: 内容隐含表示 $h_{\theta}\left(\mathbf{x}_{\mathbf{z} \leq t}\right)$,简写为 $h_{z_t}$,它和标准的 Transformer 一样,既编码上下文也编码 $x_{z_t}$ 的内容。 查询隐含表示 $g_{\theta}\left(\mathbf{x}_{\mathbf{z}<t}, z_{t}\right)$,简写为 $g_{z_t}$,它仅编码上下文信息 $\mathbf{X}_{\mathbf{Z}<t}$ 和位置信息 $z_t$,不编码内容 $x_{z_t}$。 模型的整个计算过程如下图所示: 虽然排列语言模型有很多优点,但是由于计算量很大,模型很难进行优化,因此我们通过仅预测一个句子后面的一些词条解决这个问题。将 $\mathbf{z}$ 分为两部分:非目标子序列 $\mathbf{z}_{\leq c}$ 和目标子序列 $\mathbf{z}_{>c}$,其中 $c$ 为切分点。同时会设置一个超参数 $K$,表示仅 $1 / K$ 的词条会被预测,有 $|\mathbf{z}| /(|\mathbf{z}|-c) \approx K$。对于未被选择的词条,其查询隐状态无需被计算,从而节省计算时间和资源。 MASS (2019) 16 MASS 是一个专门针对序列到序列的自然语言任务设计的预训练方法,对于一个给定的原始句子 $x \in \mathcal{X}$,令 $x^{\setminus u:v}$ 表示将 $x$ 从 $u$ 到 $v$ 位置进行遮挡处理,$k = v - u + 1$ 为被遮挡词条的个数,$x^{u:v}$ 为从 $u$ 到 $v$ 位置被遮挡的部分。MASS 利用被遮挡的序列 $x^{\setminus u:v}$ 预测被遮挡的部分 $x^{u:v}$,目标函数的对数似然如下: $$ \begin{aligned} L(\theta ; \mathcal{X}) &=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log P\left(x^{u: v} | x^{\setminus u: v} ; \theta\right) \\ &=\frac{1}{|\mathcal{X}|} \Sigma_{x \in \mathcal{X}} \log \prod_{t=u}^{v} P\left(x_{t}^{u: v} | x_{<t}^{u: v}, x^{\setminus u: v} ; \theta\right) \end{aligned} $$ 对于一个具有 8 个词条的序列,$x_3 x_4 x_5 x_6$ 被遮挡的示例如下: 模型仅预测遮挡的部分 $x_3 x_4 x_5 x_6$,对于解码器中位置 $4-6$ 利用 $x_3 x_4 x_5$ 作为输入,利用特殊遮挡符号 $\left[\mathbb{M}\right]$ 作为其他位置的输入。对于不同长度 $k$,MASS 包含了上文中提到的两种预训练模型: 长度 概率 模型 $k=1$ $P\left(x^{u} \mid x^{\setminus u} ; \theta\right)$ masked LM in BERT $k=m$ $P\left(x^{1:m} \mid x^{\setminus 1:m} ; \theta\right)$ masked LM in GPT $k \in \left(1, m\right)$ $P\left(x^{u:v} \mid x^{\setminus u:v} ; \theta\right)$ 两种之间 对于不同 $k$ 值,实验发现当 $k$ 处于 $m$ 的 $50\%$ 至 $70\%$ 之间时下游任务性能最优。 当 $k = 0.5 m$ 时,MASS 可以很好地平衡编码器和解码器的预训练。过度地偏向编码器($k=1$,masked LM in BERT)和过度地偏向解码器($k=m$,masked LM in GPT)均不能在下游的自然语言生成任务中取得很好的效果。 RoBERTa (2019) 18 RoBERTa 主要围绕 BERT 进行了如下改进: 模型采用了动态遮罩,不同于原始 BERT 中对语料预先进行遮罩处理,RoBERTa 在 40 轮训练过程中采用了 10 种不同的遮罩。 模型去掉了 NSP 任务,发现可以略微提升下游任务的性能。 模型采用了更大的训练数据和更大的 Batch 大小。 原始 BERT 采用一个 30K 的 BPE 词表,RoBERTa 采用了一个更大的 50K 的词表 28。 BART (2019) 20 BART 采用了一个标准的 Seq2Seq Transformer 结构,类似 GPT 将 ReLU 激活函数替换为 GeLUs。对于基线模型,采用了一个 6 层的编码和解码器,对于更大模型采用了 12 层的结构。相比于 BERT 的架构主要有以下两点不同: 解码器的每一层叠加了对编码器最后一个隐含层的注意力。 BERT 在预测之前采用了一个前馈的网络,而 BART 没有。 BART 采用了最小化破坏后的文档和原始文档之间的重构误差的方式进行预训练。不同于其他的一些去噪自编码器,BART 可以使用任意类型的文档破坏方式。极端情况下,当源文档的所有信息均丢失时,BART 就等价与一个语言模型。BART 中采用的文本破坏方式有:字符遮罩,字符删除,文本填充,句子重排,文档旋转,如下图所示: T5 (2019) 17 T5(Text-to-Text Transfer Transformer) 提出了一种 text-to-text 的框架,旨在利用相同的模型,损失函数和超参数等对机器翻译,文档摘要,问答和分类(例如:情感分析)等任务进行统一建模。我们甚至可以利用 T5 通过预测一个数字的文本表示而不是数字本身来建模一个回归任务。模型及其输入输出如下图所示: Google 的这项研究并不是提出一种新的方法,而是从全面的视角来概述当前 NLP 领域迁移学习的发展现状。T5 还公开了一个名为 C4(Colossal Clean Crawled Corpus)的数据集,该数据集是一个比 Wikipedia 大两个数量级的 Common Crawl 的清洗后版本的数据。更多模型的细节请参见源论文和 Google 的 官方博客。 ERNIE (Baidu, 2019) 29 30 ERNIE 1.0 29 通过建模海量数据中的词、实体及实体关系,学习真实世界的语义知识。相较于 BERT 学习原始语言信号,ERNIE 直接对先验语义知识单元进行建模,增强了模型语义表示能力。例如: BERT :哈 [mask] 滨是 [mask] 龙江的省会,[mask] 际冰 [mask] 文化名城。 ERNIE:[mask] [mask] [mask] 是黑龙江的省会,国际 [mask] [mask] 文化名城。 在 BERT 模型中,我们通过『哈』与『滨』的局部共现,即可判断出『尔』字,模型没有学习与『哈尔滨』相关的任何知识。而 ERNIE 通过学习词与实体的表达,使模型能够建模出『哈尔滨』与『黑龙江』的关系,学到『哈尔滨』是 『黑龙江』的省会以及『哈尔滨』是个冰雪城市。 训练数据方面,除百科类、资讯类中文语料外,ERNIE 还引入了论坛对话类数据,利用 DLM(Dialogue Language Model)建模 Query-Response 对话结构,将对话 Pair 对作为输入,引入 Dialogue Embedding 标识对话的角色,利用 Dialogue Response Loss 学习对话的隐式关系,进一步提升模型的语义表示能力。 ERNIE 2.0 30 是基于持续学习的语义理解预训练框架,使用多任务学习增量式构建预训练任务。ERNIE 2.0 中,新构建的预训练任务类型可以无缝的加入训练框架,持续的进行语义理解学习。 通过新增的实体预测、句子因果关系判断、文章句子结构重建等语义任务,ERNIE 2.0 语义理解预训练模型从训练数据中获取了词法、句法、语义等多个维度的自然语言信息,极大地增强了通用语义表示能力。 State-of-Art NLP 任务的 State-of-Art 模型详见: GLUE Leaderboard SuperGLUE Leaderboard SQuAD NLP-progress 中文任务基准测评 Qiu, X., Sun, T., Xu, Y., Shao, Y., Dai, N., & Huang, X. (2020). Pre-trained Models for Natural Language Processing: A Survey. ArXiv:2003.08271 [Cs]. http://arxiv.org/abs/2003.08271 ↩︎ Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119). ↩︎ Pennington, J., Socher, R., & Manning, C. D. (2014, October). Glove: Global vectors for word representation. In Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP) (pp. 1532-1543). ↩︎ McCann, B., Bradbury, J., Xiong, C., & Socher, R. (2017). Learned in translation: Contextualized word vectors. In Advances in Neural Information Processing Systems (pp. 6294-6305). ↩︎ ↩︎ Peters, M. E., Neumann, M., Iyyer, M., Gardner, M., Clark, C., Lee, K., & Zettlemoyer, L. (2018). Deep contextualized word representations. arXiv preprint arXiv:1802.05365. ↩︎ ↩︎ Radford, A., Narasimhan, K., Salimans, T., & Sutskever, I. (2018). Improving language understanding by generative pre-training. URL https://openai.com/blog/language-unsupervised/. ↩︎ ↩︎ Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2018). Bert: Pre-training of deep bidirectional transformers for language understanding. arXiv preprint arXiv:1810.04805. ↩︎ ↩︎ Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1746-1751). ↩︎ Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780. ↩︎ Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical evaluation of gated recurrent neural networks on sequence modeling. arXiv preprint arXiv:1412.3555. ↩︎ Socher, R., Perelygin, A., Wu, J., Chuang, J., Manning, C. D., Ng, A. Y., & Potts, C. (2013). Recursive deep models for semantic compositionality over a sentiment treebank. In Proceedings of the 2013 conference on empirical methods in natural language processing (pp. 1631-1642). ↩︎ Tai, K. S., Socher, R., & Manning, C. D. (2015). Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers) (pp. 1556-1566). ↩︎ Marcheggiani, D., Bastings, J., & Titov, I. (2018). Exploiting Semantics in Neural Machine Translation with Graph Convolutional Networks. In Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 2 (Short Papers) (pp. 486-492). ↩︎ Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … & Polosukhin, I. (2017). Attention is all you need. In Advances in neural information processing systems (pp. 5998-6008). ↩︎ ↩︎ ↩︎ Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. In Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers) (pp. 4171-4186). ↩︎ Song, K., Tan, X., Qin, T., Lu, J., & Liu, T. Y. (2019). MASS: Masked Sequence to Sequence Pre-training for Language Generation. In International Conference on Machine Learning (pp. 5926-5936). ↩︎ ↩︎ Raffel, C., Shazeer, N., Roberts, A., Lee, K., Narang, S., Matena, M., … & Liu, P. J. (2019). Exploring the limits of transfer learning with a unified text-to-text transformer. arXiv preprint arXiv:1910.1068 ↩︎ ↩︎ Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., … & Stoyanov, V. (2019). Roberta: A robustly optimized bert pretraining approach. arXiv preprint arXiv:1907.11692. ↩︎ ↩︎ Yang, Z., Dai, Z., Yang, Y., Carbonell, J., Salakhutdinov, R. R., & Le, Q. V. (2019). Xlnet: Generalized autoregressive pretraining for language understanding. In Advances in neural information processing systems (pp. 5754-5764). ↩︎ ↩︎ Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A., Levy, O., … & Zettlemoyer, L. (2019). Bart: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. arXiv preprint arXiv:1910.13461. ↩︎ ↩︎ Saunshi, N., Plevrakis, O., Arora, S., Khodak, M., & Khandeparkar, H. (2019). A Theoretical Analysis of Contrastive Unsupervised Representation Learning. In International Conference on Machine Learning (pp. 5628-5637). ↩︎ Tenney, I., Das, D., & Pavlick, E. (2019). BERT Rediscovers the Classical NLP Pipeline. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics (pp. 4593-4601). ↩︎ 图片来源:http://www.realworldnlpbook.com/blog/improving-sentiment-analyzer-using-elmo.html ↩︎ Wu, Y., Schuster, M., Chen, Z., Le, Q. V., Norouzi, M., Macherey, W., … & Klingner, J. (2016). Google’s neural machine translation system: Bridging the gap between human and machine translation. arXiv preprint arXiv:1609.08144. ↩︎ Dong, L., Yang, N., Wang, W., Wei, F., Liu, X., Wang, Y., … & Hon, H. W. (2019). Unified language model pre-training for natural language understanding and generation. In Advances in Neural Information Processing Systems (pp. 13042-13054). ↩︎ Dai, Z., Yang, Z., Yang, Y., Carbonell, J. G., Le, Q., & Salakhutdinov, R. (2019, July). Transformer-XL: Attentive Language Models beyond a Fixed-Length Context. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics (pp. 2978-2988). ↩︎ Al-Rfou, R., Choe, D., Constant, N., Guo, M., & Jones, L. (2019). Character-level language modeling with deeper self-attention. In Proceedings of the AAAI Conference on Artificial Intelligence (Vol. 33, pp. 3159-3166). ↩︎ Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners. URL https://openai.com/blog/better-language-models/. ↩︎ Sun, Y., Wang, S., Li, Y., Feng, S., Chen, X., Zhang, H., … & Wu, H. (2019). Ernie: Enhanced representation through knowledge integration. arXiv preprint arXiv:1904.09223. ↩︎ ↩︎ Sun, Y., Wang, S., Li, Y., Feng, S., Tian, H., Wu, H., & Wang, H. (2019). Ernie 2.0: A continual pre-training framework for language understanding. arXiv preprint arXiv:1907.12412. ↩︎ ↩︎

2020/3/28
articleCard.readMore

ToB 产品用户权限 (User Privileges of ToB Products)

用户权限在产品中是一个最基础的功能,它决定了用户在产品中能做些什么。虽然用户对权限的感知并不是很明显,但作为一个系统的基础功能,用户权限可以说会影响产品设计和实现的方方面面。对于不同类型的产品,用户权限的设计也略有差异,ToC 产品中用户之间相对独立,所需要考虑的问题会比 ToB 产品简单不少,本文仅针对 ToB 产品的用户权限给出一些思考。 Role-Based Access Control RBAC (Role-Based Access Control,基于角色的访问控制) 是一种已经广泛应用于各种管理系统的权限管理模型 1。在 RBAC 中,操作权限与角色之间建立关联,再通过在角色与用户之间建立关联来完成对用户的授权,大大提高了权限控制的灵活性。在 RBAC 中包含几个关键的概念: 资源:所有可以访问的对象,从交互角度可以理解为:页面,按钮等一切可以操作的对象,从后台接口角度可以理解为:以 Rest 接口为例,资源即为每个 URI。 权限:获取资源的方式,简单理解即为资源的增删改查。 角色:一个授权等级的工作职位或职称。 用户:一名使用者或自动代理人。 资源,权限,角色和用户之间的关系如下图所示: 「资源」和「权限」之间为多对多的关系,「权限」和「角色」之间为多对多的关系,「角色」和「用户」之间亦为多对多的关系。 RBAC0 RBAC0 为 RBAC 中最简单最基础的权限模型,包含了用户,角色,权限之间的关系,如下图所示: 用户,角色和权限之间都可以是多对多的关系,在系统分工简单权限清晰明确的情况下,用户和角色之间也可能是多对一的关系。 RBAC1 RBAC1 在 RBAC0 的基础上引入了角色继承的概念,添加了「子角色」,上级角色可以继承下级角色所有的权限。 例如,一个公司的不同人力资源副总监分管不同的职能,因此具有不同的权限,而人力资源总监则应该具有所有人力资源副总监的权限。同时,人力资源总监也可能具有其他人力资源副总监都不具有的权限。 RBAC2 RBAC2 在 RBAC0 的基础上增加了一些限制,引入了 SSD (Static Separation of Duty,静态职责分离) 和 DSD (Dynamic Separation of Duty,动态职责分离)。 SSD 主要应用于用户和角色之间,主要的约束包括: 角色互斥约束:同一个用户仅能分配到互斥角色中的一个。例如:财务系统中一个用户不能同时分配会计和审计两种互斥的角色。 基数约束:一个用户拥有的角色是有限的,一个角色拥有的权限是有限的。 先决条件约束:用户在获取更高的角色之前需先拥有低级的角色。 DSD 主要应用于会话和角色之间动态地限制用户及其拥有的角色和相应的权限。例如:一个用户在系统中拥有两个不同的角色,但在使用系统时只能激活其中一个角色。 RBAC3 RBAC3 是 RBAC1 和 RBAC2 的合集,既包含了角色分层也包含了相关约束。 权限扩展 接下来我们探讨在 RBAC 的基础上在真实的 ToB 产品中又有哪些权限扩展,这里有时我会以「种植」业务的 SaaS 为例进行简略分析。我们将权限又分为「功能权限」和「数据权限」两个部分,如下图所示: 功能权限就是指我们具体能够干些什么,例如:育苗,除草,施肥等等。数据权限是指我们是对谁做这些事情,例如:是对北京的实验大棚除草,还是河北的生产大棚施肥。 ToB 产品的销售对象并非个人,而是一个组织,在此我们引入「组织」的概念: 组织:用户所隶属的单位,从产品销售角度限定了用户所能使用的功能。 对于一个组织,我们设置有「功能池」和「数据池」。功能池即组织购买的产品的功能集合,一个组织的用户所能够使用的功能受限与此,并由组织的系统管理员分配功能池中的功能。数据池即组织自己的相关数据集合,由组织的管理员对不同的员工进行分配。 对于功能权限和数据权限,均为一个树状的层级关系,以功能权限为例,层级如下图所示: 功能的层级和菜单的层级有一定的对应关系,层级不易过深,这部分将在下文菜单部分详细介绍。数据的层级则需要根据产品涉及的具体业务做出相应的规范要求,例如:「根」-「种植场」-「种植区」-「种植大棚」-「种植地块」。同时,为了方便层级的扩充,在实现层面上不建议提前把所有层级固定死,可以采用父子层级的形式循环关联。 权限分配 在介绍如何根据上述设计进行权限分配前,我们再引入一个概念,「用户组」: 用户组:具有相同权限的一组用户。 当一个组织中有大量用户需要具有相同的权限时,为了避免给每个用户分配权限导致的繁琐操作,可以将这些具有相同权限的用户归入一个用户组。对该用户组赋予一定的权限,则该用户组下面的所有用户自动具有相应权限,当需要取消其中某些用户权限时,仅需要将其从该用户组中移除即可。理论上,「用户」和「用户组」以及「用户组」和「角色」之间都可以是多对多的关系。 下图为一个最细力度的权限分配示意图: 首先,我们暂时限定一个用户仅能够隶属于一个组织,因此该用户所能够具有的最多的权限即为该组织所购买的产品的功能权限和组织自己的全部数据权限。 其次,一个最基本的权限应该是由一个功能权限对应一个数据权限构成的。这里面的功能权限和数据权限可以是任意一个级别的,同时当具有高级别的功能权限或数据权限后,子级别的相应权限自动获得。 最后,权限、角色、用户组和用户之间均可以是多对多的关系。 虽然通过很多概念的抽象使得权限管理变得相对简单,但是由于理论上支持各种细粒度的操作,在一定程度上又会加重权限管理的运维成本。在一个 ToB 产品的前期,组织的管理员可能对整个系统不是很熟悉,这样权限管理的工作会落到内部管理员的头上。当随着产品的不断发展,组织和用户的量级不断变大,内部管理员仅需为每个组织开通一个组织管理员并赋予该组织的所有权限即可,该组织内部的权限管理则交由组织的管理员进行运维即可。 菜单,页面和组件 以上我们都是从业务逻辑的角度解释权限控制,接下来我们从用户交互的角度出发简单阐述一下权限控制在用户交互上的体现。下图为一个 ToB 产品的原型界面: 在这个界面中,我们粗略的将其中的元素划分为三大块:「菜单」,「页面」和「组件」。这个划分并不是很准确,甚至可以说一定程度上概念之间有重叠,这样划分仅仅是为了更好的对应到上文涉及的相关概念上。 上文中我们提到一个角色的权限可以是任何一个级别的功能权限和数据权限的组合,同时权限、角色、用户组和用户之间都可以是多对多的关系,在此我们假设后台已经通过查询获得到了一个用户的所有权限。 菜单 菜单对应到主要是功能权限,但不是所有的功能权限都会以菜单的形式展现出来,因此我们可能还需要维护一套菜单的层级关系以及同功能权限的对应关系,或者我们直接利用功能权限的层级关系,但需要在里面补充上是否作为菜单显示的属性。具有不同权限的用户看到的菜单也会是不一样的。 在菜单设计时,建议的原则是层级不要过深,过深的话从交互上就会导致你的菜单像极了「箭头型代码」。 理论上,所有包含子功能的父功能都不代表一个具体的操作,一般仅仅是为了维护一个分组而创建的。 页面 页面并不是浏览器里面的整个页面,这里我们特指不包含菜单的部分。当我们单击一个功能菜单时,对应的页面则会相应的显示出来。当然,我们知道单击按钮 (下文会提到的组件) 也可能会打开一个新的页面,这种情况我们在下文中再展开说明。当我们单击了一个叶子界别的菜单后,其实我们已经使用了一个权限,一般情况下对应的是对一个资源的查询。 组件 页面中包含了操作的结果信息,一般情况下这些信息是不可交互的,同时页面中也包含了大量可以交互的地方,这里我们称其中的部分为组件,例如:按钮等。当然还有一些被我们在通常情况下也称之为的组件的元素,例如:输入框,下拉框等等,虽然他们也有交互功能,但是一般情况下他们并不会触发一个权限操作,因此这里我们就先将组件特指那些可以产生权限操作的元素。 一个页面可以包含多个组件,也就是说一个页面并不一定仅对应一个权限操作,可能是多个权限操作的集合。所以简单的说我们可以将功能权限设置的比菜单多一个级别,功能权限的倒数第二级对应叶子菜单,功能权限的最后一级对应页面上的组件。 权限运维 权限的精细化管理和运维成本是相互矛盾的,想更加精细化的管理权限必然会增加运维成本。因此一个 ToB 的产品到底使用什么力度的权限管理更加合适呢?我想这没有一个准确的答案,从个人角度给出两点有参考意义的设计原则: 客户需求。ToB 产品吗,最终是要拿来卖的,跪添甲方爸爸就好,甲方爸爸的需求就是合理的需求,无需质疑。说的有点过了,意思其实就是尽可能满足合理客户需求的前提下,制定合适的权限管理系统。 成本合算。有的时候客户也不是很清楚到底需不需要精细化的权限管理,那么问题就又抛回给了产品经理,那么我们需要考虑更多的是当下的实现成本和后期的改造成本。这是一个很现实的考虑,如果业务 KPI 压力比较大,有时候我们宁可承担更高的改造成本也会先实现一套简单的权限管理先用着。但是我个人认为,在研发资源相对充足的前提下还是尽量将权限系统设计完善,因为这是一个系统很底层的部分,甚至会影响后续所有的业务功能逻辑的设计和实现。 权限运维是一个很耗费人力的工作,解决这个问题没有太好的途径。提高用户的交互体验,编写简单易懂的使用说明书,只有将一个组织的权限管理交到组织的管理员手里。才能够真正解放内部管理运维人员的工作时间。 Ferraiolo, David, Janet Cugini, and D. Richard Kuhn. “Role-based access control (RBAC): Features and motivations.” Proceedings of 11th annual computer security application conference. 1995. ↩︎

2019/11/24
articleCard.readMore

京东数科 HIPO 学习之旅 (JDD HIPO Learning Journey)

今年有幸参加了京东数科的 HIPO 培训课程,整个学习之旅可谓收货满满。由于出差没能参加 8 月份的开营仪式,看到小伙伴们各种拓展,还能自己做饭,感觉错过了展示范小厨的大好机会。同样也错过了报名的「领导力」的课程,最终换到了「情商」和「沟通」两门课程学习。 情商 自认为不是一个情商很高的人,果然开课前利用 6 秒钟情商 1 评测 (SEI) 评测之后的结果不甚理想。6 秒钟情商模型将情商划分为 3 个阶段:自我认知,包括:认识情绪、识别情绪模式;自我选择,包括:运用因果思维、驾驭情绪、运用内在动力、修炼乐观思维;自我超越,包括:增强同理心、追求超我目标。 在自我认知的认识情绪和识别情绪模式上仍处于起步阶段,其他部分基本处于稳定阶段和熟练阶段。这完全符合我对自己情商的认知,不善于发现别人情绪的变化,一旦发现还是能够用自己擅长的理性思维解决问题的。不过认识情绪是整个环节的第一步,如果这一步都不能做得很好,就算后面能力不错,也很难成为一个高情商的人。 情绪是很容易变向极端,但比较难恢复平静的,下图就是我们测试自己当前的情绪状况,颜色越紫表示越紧张压力越大,颜色越蓝表示越平静。 课程上学习到了一个很好用的情绪管理的工具,TFA 卡片。TFA 卡片是一个实践情绪管理的工具,让我们尝试从T (Think,思考),F (Feel,感受) 和 A (Act,行动) 三个角度对一个事件进行复盘实践。 情商课程让我认识到了情绪是应该去管理的,而不是去控制的,因为控制的情绪总会有爆发的一天。希望通过坚持不断地实践,努力成为一个高情商的人。 沟通 在我的认知范围内,只要将事情表达清楚就可以达成高效的沟通,然而现实并不是这样的,沟通课程告诉了我更多需要注意的地方。首先通过一个 DISC 模型 2 从两个维度将人的特质划分为四种,两个维度分别是:更关注事物本身还是人际关系和倾向快 (直接) 还是慢 (间接)。 不同特质的人的性格也截然不同: 特质 特点 D - 掌控型 爱冒险的、有竞争力的、大胆的、直接的、果断的、创新的、坚持不懈的、问题解决者、自我激励者 I - 影响型 有魅力的、自信的、有说服力的、热情的、鼓舞人心的、乐观的、令人信服的、受欢迎的、好交际的、可信赖的 S - 沉稳型 友善的、亲切的、好的倾听者、有耐心的、放松的、热诚的、稳定的、团队合作者、善解人意的、稳健的 C - 严谨型 准确的、有分析力的、谨慎的、谦恭的、圆滑的、善于发现事实、高标准、成熟的、有耐心的、严谨的 通过测评分析,我是一个内在 C/S,外在正在向 D/S 转变的特征的人。评测结果也很符合当下的我,之前作为算法工程师更习惯用数据说话,但现在向一个产品经理的方向转变,需要更多的掌控和沟通。 C 类特质的人有严谨的优势,但这个优势也需要根据不同的场景进行适当地调整,这样才能够避免让自己的优势变成劣势,反而影响与他人沟通的效率。在沟通的过程中,有时候大家的最终目标其实相差并不是很远,只是不同类型特质的人看待事情的角度不尽相同而已。如果在沟通过程中大家不考虑自己和沟通者的不同性格特质的话,就很容易触碰到大家的一些敏感区域,从而造成无效的沟通。因此无论是对上,对下还是平级沟通,我们都需要根据沟通者的性格特质采用相应的沟通技巧,进而高效地达成我们的沟通目的。 库布其沙漠 50 公里挑战 课程的最后安排了一个终极挑战「库布其沙漠 50 公里挑战」,对于我这个还比较喜欢出去浪的人是一定不能错过的。比赛之前我们做了些功课,我作为整个队伍的领航员走在最前面负责探路和制定路线,队长则负责在最后面压阵避免有同学掉队。组内的男生分担了两位女生的负重,当然最感动的是两位小姐姐帮我们买了鞋垫神奇 - 姨妈巾。 我们一路穿越山丘,峡谷和小河,还有我们巨帅的越野保障车。 挑战赛一共两天的行程,第一天路线比较固定,大家基本拼的就是体力和团队凝聚力了,队里的两位小姐姐超级给力,一路我们都保持了很好的队形,最终拿下了第一天赛程的冠军,领先第二名 20 多分钟。第二天赛程给定的建议路线有一个折线,前一晚我们计划选择走更近的直线,但中途被呼叫让我们走回预定线路上去。虽然我们已经提前确定规则并没有要求必须走预定线路,但是由于对讲机里不断呼叫,我们还是回到了预定路线上去。但比赛最后才发现,只有我们回到了预定路线上,其他队伍并没有回去,所以第二赛段我们只拿到了第二,被昨天的第二名领先了 15 分钟。 庆幸的是两个赛段的总成绩我们还是第一名,当然第一名也还是要有付出的,颁奖仪式上我们用奖杯每人豪饮了一大碗啤酒,足足有一瓶多的一碗。整个比赛下来我感觉三点很重要:目标清晰,分工明确,齐心协力。我们在一开始就朝着冠军的奖杯出发的,我负责领航,队长负责断后,所有人相互帮助,一同朝着目标努力前进。当然,在规则允许的情况下,再多一分坚持己见就更好了。 生活因你而火热 最后放一下京东数科的 MV 吧,我有出境 6 秒哦 (02:27 - 02:32),没错,打哈欠和撸猫的就是我,相信我,养猫一定比养猪容易,还有种菜 😀。 https://cn.6seconds.org/about ↩︎ https://en.wikipedia.org/wiki/DISC_assessment ↩︎

2019/10/28
articleCard.readMore

杭州和东京之旅 (Tour of Hangzhou and Tokyo)

换完工作方向后的第一个假期,决定好好调整一下,休个小长假,浪完这一圈好收收心,再全身心地投入到后面的工作中去。之前一直也没有陪家里人出去玩玩,父母年岁不小了,所以选择避开十一前面,四号错峰到了杭州,妥妥的人少了不少。 游西湖的时候下着零星的小雨,烟雨朦胧中的西湖别有一番韵味。想起有一日北京大雨,开完会坐在出租车上还哼起了“西湖的水,我的泪,我情愿和你化作一团火焰…..”。 陪着家人玩儿完,自己一个人跑去了日本,因为剩下的年假也不长了,就选择只停留在东京先。下了飞机,打车到酒店,我擦,出租车你敢不敢再贵点儿。好吧,我承认出来之前没做任何攻略,佛系浪吧 😂。 回程本想做地铁去机场的,结果各种原因又只能打车,就眼瞅着计价器越跳越多,越跳越多 😱,后面再吐槽这一段经历。住在了新宿区歌舞伎町附近,夜晚的新宿很是热闹,不得不说东京的住宿也是贵的不要不要的。 没有攻略就前一天晚上简单的查了查,第一天到了必打卡的浅草寺,顺着上野公园,又去到了东京国立博物馆。 天气还是很给力的,第二天依旧闲逛,在东京大学里面转了一圈愣是没转到安田讲堂那边,不过在三四郎池旁边抓拍到应该是一只小蜥蜴吧。 没攻略果然还是有问题的,想去晴空塔却发现距离第一天的浅草寺很近,无奈就只能朝着那个方向又跑了一遍。之前有朋友晚上去过,风景不错,我选择了白天上去,稍微有些雾蒙蒙的,视野还是很开阔的。 住的附近搜到了一家武士博物馆,下午回来早了就过去看了看,到的时间刚刚好,前面的一波英文讲解刚刚开始。小姐姐讲到刀柄上的文字时,说到日本的文字主要源自自己,英文和中文,她说她也看不懂刀柄上的文字,我过去一瞧发现这不就是繁体中文吗 😏,瞬间感觉到我中华语言和文化的优秀。最后还体验了一把武士对决的表演,感觉人家才是武士,我顶多也就是个浪人 :sweat_smile:。 来了日本还有的就是买买买,给自己买,给家人买,给朋友带,总之各种买买买。计划预留了一天的时间去购物,谁知道玩着玩着就听说台风“海贝思”要来了,据说还是近期最大的一次台风,然后 12 号各种地方就都关门了,只能老老实实地呆在酒店里。傍晚还感觉到房子晃了一下,后面查了还真的是有地震,各种情况就这样都被赶上了。 台风前天空都变成紫色的了,自己没照到,网上盗图一张。本来还很庆幸自己是 13 号下午的飞机回来,台风应该影响不大,谁知道 13 号退完房才发现航班被取消了,各种客服电话都打不进去,无奈我就只能赶紧打车到机场去看看。Anyway,经过各种买票,退票,改签终于还是赶上了 13 号的一班飞机回来。 第一次一个人出来浪还是很舒服的,自由,想去哪儿就去哪儿,唯一不爽的就是没人帮忙照相。

2019/10/14
articleCard.readMore

国际智慧温室种植挑战赛 (International Autonomous Greenhouse Challenge)

国际智慧温室种植挑战赛 是一个由 瓦赫宁根大学研究中心 (Wageningen University & Research) 主办的旨在利用自动化、信息技术和人工智能技术控制温室以实现增加产量、降低成本等目标的大赛。第一届赛事的种植作物为黄瓜,第二届赛事为樱桃西红柿。 很幸运能够在晚些时候加入到 CPlant 队伍中一同参与到这次赛事,虽然加入到队伍中比较晚,但工作之余也参与了大部分赛事的准备工作。 整个赛事分为初赛和复赛两个部分,初赛采用 Hackathon 的形式通过仿真模拟进行,初赛晋级的队伍将会在后续 6 个月的时间内通过远程控制进行真实的作物种植比赛。本次赛事吸引了全球顶级的农业与 AI 领域的企业、大学和研究机构参与,组成来自 26 个国家的 21 支团队,超过 200 名专家与学生。 初赛黑客马拉松评分主要由三部分组成:团队构成 (20%)、人工智能方法(30%),以及虚拟西红柿种植净利润(50%)。仿真部分,采用了 Venlo 类型的温室,模拟时间从 2017/12/15 日至 2018/06/01,荷兰本地的外部天气,整个模拟过程并未考虑病虫害问题 (主要受到湿度影响)。仿真模型包含三个子模型: Kaspro 温室模型 Intkam 作物模型 经济模型 Kaspro 温室模型:主要通过温室的控制器 (例如:通风口,加热管道,CO2 补充器,遮阳帘,灌溉系统等) 控制温室内的环境变量 (例如:光照,温度,湿度,CO2 浓度,水量,水 EC 值等),进而控制作物生长。环境控制模型是相对复杂的一个模型,因为控制器和环境变量之间并不是一对一的关系。 Intkam 作物模型:主要通过设置茎的密度,叶片的去留策略,去顶时间,果实个数保留策略等控制作物的生长。 经济模型:主要定义了不同时间、不同果重、不同糖分樱桃番茄的价格,不同时间段内光照、加热和 CO2 的成本,以及相关的人工成本。 最终经过 24 小时的 Hackathon,我们队伍的成绩如下,最后排名 9/21,很遗憾未能进入到决赛。 Team Composition (20%) Strategy and AI Approach for the Growing Challenge (30%) Obtained Points Following Rankings in Hackathon (50%) Obtained Final Results in Hackathon (Net Profit) Total Score 15.6 (Ranking 6/21) (Max: 17.6) (Min: 7.6) 21.6 (Ranking 4/21) (Max: 23.1) (Min: 4.8) 21 (Ranking 9/21) (Max: 50) (Min: 1) 92.0 (Ranking 9/21) (Max: 154.5) (Min: 0.7) 58.2 (Ranking 9/21) (Max: 88.8) (Min: 13.4) 所在的 CPlant 队伍是本次比赛中人数最多的一只队伍 (21 人,最少的队伍为 5 人,虽然人最多却未能进入决赛 :disappointed_relieved:),评审从国籍,研究和企业组成等多个角度对团队构成进行了评分,最终我们拿到了一个中等偏上的成绩。 人工智能方法方面是我们在准备过程中讨论比较多的内容,每个人根据自己的优势不同分别负责了 Plant Growth Model, Machine Learning, Deep Learning, Reinforcement Learning 和 Knowledge Graph 等不同部分的设计。答辩过程中多位评委对于我们的 Knowledge Graph 在整个人工智能中的应用很感兴趣,在最后点评中也提到我们是唯一一只提到 Knowledge Graph 及其在智慧农业中应用的队伍。我认为智慧农业不同于其他人工智能应用领域分支,其具有一定的特殊性,数据和实验并不像其他领域容易获取和实现,我们需要更多地结合农业科学本身的相关经验和知识。由于我之前从事过 NLP 和 Knowledge Graph 相关工作,我深信 Knowledge Graph 一定会是一个将农业和人工智能有机地结合起来的好工具,但至于如果结合和实现落地还需要进一步探索和研究。最终这部分我们拿到了一个相对不错的成绩。 分数占比最多的仿真部分我们做的有所欠缺,同时这也是我们最为陌生的一个部分。整个 Hackathon 从当地时间 12 日 13 时开始,至 13 日 13 时结束,我们通宵达旦,一整夜的 Coding 陪伴我们度过了中秋佳节。整个过程中我们几乎将全部的精力投入到了 Kaspro 温室模型参数的优化中来,Intkam 作物模型则是根据相关的农业经验进行了简单的优化配置,经济模型并没有直接的控制参数,而是通过相关投入和产出进行计算得到。通过不断的优化,净收益从 10 几分不断提高到 80 几分,后面则一直卡在了 80 几分未能进一步提高。整个 Hackathon 过程中,组委会不定时地公布一些不包含具体组名的成绩统计信息,在第一天白天就已经有队伍拿到了接近 100 分的成绩,在半夜的一次公布中有队伍已经拿到了接近 120 分的净收益。面对巨大的压力,我们仍不断地优化 Kaspro 温室模型参数,虽然成绩在稳步提高,但提高的幅度甚微。在临近比赛的时候,我们终于决定在 Intkam 作物模型做一些大胆的尝试,设置了一些现实中绝对不可能达到的参数,居然取得了很高的提升。在最后 10 几分钟内我们将成绩又提高了 10 分左右,但由于时间限制我们未能来得及进一步调整测试。 所有队伍的 Net Profit 和 Points 成绩从大到小排列结果如下: 一些与现实相差很远的参数设置却能够得到一个更好的结果,这个问题我们在最开始确实没有敢想。但其实开赛前的技术文件中有提及,整个模拟就是一个黑盒游戏,并没有任何规则可言,最终的评判准则只有净收益。虽然仿真模型与现实会有些差距,但对于这个单纯的游戏而言,先入为主的种植经验确实限制了我们的想象。而对于我这个正在朝着产品经理发展的野生程序猿而言,我正需要的就是这种想象和实践想象的能力,让我以胡适先生的一段话总结这次赛事的经验教训吧: 大胆的假设,小心的求证。 – 胡适 💪 壮志未酬,来年再战!💪

2019/9/21
articleCard.readMore

记忆中的儿时 (My Childhood in Memory)

年中自己做出了一个重大的转变,担搁了不少时间,也没太多心情能静下心来读读写写。现在开始了新的征程,本想先总结总结前段的生活和这个我认为很重要的转变,但或许是这段时间想多了,前后也想得远了,回忆到很多小时候的事儿。开始读上《龍應台的香港筆記》,读着读着也很突然地就想起了童年,真的也很难说上有什么关系,但就是突然想起来了。 然后,也就这几天,突然又听到了刘昊霖的「儿时」,之前有听过但没怎么注意,偏偏这个时候再一听,感觉所有儿时的东西就一股脑都蹦出来了。 window.meting_api = "https:\/\/api.obdo.cc\/meting\/?server=:server\u0026type=:type\u0026id=:id\u0026auth=:auth\u0026r=:r"; 能回忆起的最早的画面是小时候妈妈给买的小蛋糕,找了很久才找到这种记忆中蛋糕的照片,蛋糕里好像就只有一层奶油。 说实话记不起来是生日还是过年了,因为生日也在冬天,只是模糊地记得和妈妈冬天里赶集的时候好像也没有和妈妈开口要,但妈妈却给买了,当时应该真的是很开心。妈妈管自己比较严,从来都不太敢和妈妈要买什么,实在想要什么的时候我就会说如果我考了前几名能不能给我买个什么呀? 父母的文化水平不高,尤其是妈妈,就上了一段时间的小学,但妈妈却对我们姐弟俩在学业上要求很高。或许他们知道自己没有能够接受很好的知识教育,反而想着不能也让孩子和他们一样。爸爸不善言辞,但经常会带着我出去玩,暑假总能够在前几天就把作业写完,然后爸爸就骑着大梁自行车,我坐在大梁上,带着我去捞小鱼,去摘酸枣,真的很开心,无忧无虑。 小时候住在村里的瓦房中,一个前院,还有个后院,家里有好多的果树:梨树,葡萄树,李子树,樱桃树,柿子树,后院的黄李子真的很好吃,酸酸甜甜的。前院有一大株橙色的百合花,印象中还有一张穿着塑料凉鞋和妈妈的合照,妈妈的样子一直没怎么变,只是现在头发已经花白。整个童年都是在这个院子里面度过的,现在村里的老院子盖成了门市租了出去,爸妈现在也不在村子里住了,自己就更少回去看看了。都说物是人非,现在连老院子也变了,剩下的就只有印象中的东屋,西屋还有窗或地下1。 姐姐大我九岁,没太多印象和姐姐一起玩,但还记得好几次和姐姐打架。有一次姐姐不小心把自己从炕上推到了地上,是真的疼了,记得自己哭得比较厉害,妈妈说了姐姐。长大后回家,一家人聚在一起回忆旧时光,聊到这里,小外甥女还问我说:我妈那天把你推到地上疼吗?现在想来感觉前脚还在和姐姐打闹,后脚姐姐的孩子都上初中了。姐姐一直对我都很好,小时候播的是四驱小子,姐姐在读师范,放假回来给我带了一辆绿色的「燃烧太阳」,超级开心。 那时候的玩伴是斜对门奶奶家的外孙,当时村里的化肥厂还在,他读工厂的子弟学校,我读村里的小学。周末的时候他一般会回来,那会儿感觉最爱玩土,什么用沙子搭个城堡啦,用黄土做个小笔筒啦,还像模像样的用火烧一烧。家的旁边是村委会,里面有个大院子,那里就成了我们最大的根据地。院子里散落着一些不知道干什么用的设备器材,可以爬上爬下。当时我们还发明了一种叫做「闯关」的游戏,就是我俩假设出一关一关的剧情,然后就假装一关一关的完成任务。现在想想这不就是赤裸裸的意淫吗,不过那时候我们真的是玩得不亦乐乎。 回忆儿时的无忧无虑和欢声笑语,其实是对那种生活的向往。现在长大了,对自己的未来有了期许,也自然有了压力,遇见了更多的人和事,难免会有不对付和烦躁。停下来回味回味,感觉两个事很重要:「简单」和「感恩」。简单点,不会太累,很多时候很多事其实也没想的那么复杂,简单些反而迎刃而解,我想这就是童年快乐的源泉吧。多感恩,年轻气盛时老是记得别人的不好,现在反而更多地去发现人与事好的一面。一路走来,能相识相知都是很大的缘,感恩家人、朋友以及一路走来遇见的人和事,正是他们让自己的生活变得独一无二,也正是他们让我们有的回忆,值得回忆。 唐山话,老家的房子一般就是两间卧室在两边,中间算是大厅,做饭,吃饭都在这,我们就叫这儿窗或地下。 ↩︎

2019/7/28
articleCard.readMore

启发式算法 (Heuristic Algorithms)

启发式算法 (Heuristic Algorithms) 启发式算法 (Heuristic Algorithms) 是相对于最优算法提出的。一个问题的最优算法是指求得该问题每个实例的最优解. 启发式算法可以这样定义 1:一个基于直观或经验构造的算法,在可接受的花费 (指计算时间、占用空间等) 下给出待解决组合优化问题每一个实例的一个可行解,该可行解与最优解的偏离程度不一定事先可以预计。 在某些情况下,特别是实际问题中,最优算法的计算时间使人无法忍受或因问题的难度使其计算时间随问题规模的增加以指数速度增加,此时只能通过启发式算法求得问题的一个可行解。 利用启发式算法进行目标优化的一些优缺点如下: 优点 缺点 1. 算法简单直观,易于修改 2. 算法能够在可接受的时间内给出一个较优解 1. 不能保证为全局最优解 2. 算法不稳定,性能取决于具体问题和设计者经验 启发式算法简单的划分为如下三类:简单启发式算法 (Simple Heuristic Algorithms),元启发式算法 (Meta-Heuristic Algorithms) 和 超启发式算法 (Hyper-Heuristic Algorithms)。 简单启发式算法 (Simple Heuristic Algorithms) 贪心算法 (Greedy Algorithm) 贪心算法是指一种在求解问题时总是采取当前状态下最优的选择从而得到最优解的算法。贪心算法的基本步骤定义如下: 确定问题的最优子结构。 设计递归解,并保证在任一阶段,最优选择之一总是贪心选择。 实现基于贪心策略的递归算法,并转换成迭代算法。 对于利用贪心算法求解的问题需要包含如下两个重要的性质: 最优子结构性质。当一个问题具有最优子结构性质时,可用 动态规划 法求解,但有时用贪心算法求解会更加的简单有效。同时并非所有具有最优子结构性质的问题都可以利用贪心算法求解。 贪心选择性质。所求问题的整体最优解可以通过一系列局部最优的选择 (即贪心选择) 来达到。这是贪心算法可行的基本要素,也是贪心算法与动态规划算法的主要区别。 贪心算法和动态规划算法之间的差异如下表所示: 贪心算法 动态规划 每个阶段可以根据选择当前状态最优解快速的做出决策 每个阶段的选择建立在子问题的解之上 可以在子问题求解之前贪婪的做出选择 子问题需先进行求解 自顶向下的求解 自底向上的求解 (也可采用带备忘录的自顶向下方法) 通常情况下简单高效 效率可能比较低 局部搜索 (Local Search) 和爬山算法 (Hill Climbing) 局部搜索算法基于贪婪思想,从一个候选解开始,持续地在其邻域中搜索,直至邻域中没有更好的解。对于一个优化问题: $$ \min f \left(x\right), x \in \mathbb{R}^n $$ 其中,$f \left(x\right)$ 为目标函数。搜索可以理解为从一个解移动到另一个解的过程,令 $s \left(x\right)$ 表示通过移动得到的一个解,$S \left(x\right)$ 为从当前解出发所有可能的解的集合 (邻域),则局部搜索算法的步骤描述如下: 初始化一个可行解 $x$。 在当前解的邻域内选择一个移动后的解 $s \left(x\right)$,使得 $f \left(s \left(x\right)\right) < f \left(x\right), s \left(x\right) \in S \left(x\right)$,如果不存在这样的解,则 $x$ 为最优解,算法停止。 令 $x = s \left(x\right)$,重复步骤 2。 当我们的优化目标为最大化目标函数 $f \left(x\right)$ 时,这种局部搜索算法称之为爬山算法。 元启发式算法 (Meta-Heuristic Algorithms) 元启发式算法 (Meta-Heuristic Algorithms) 是启发式算法的改进,通常使用随机搜索技巧,可以应用在非常广泛的问题上,但不能保证效率。本节部分内容参考了《智能优化方法》2 和《现代优化计算方法》1。 禁忌搜索 (Tabu Search) 禁忌搜索 (Tabu Search) 是由 Glover 3 提出的一种优化方法。禁忌搜索通过在解邻域内搜索更优的解的方式寻找目标的最优解,在搜索的过程中将搜索历史放入禁忌表 (Tabu List) 中从而避免重复搜索。禁忌表通过模仿人类的记忆功能,禁忌搜索因此得名。 在禁忌搜索算法中,禁忌表用于防止搜索过程出现循环,避免陷入局部最优。对于一个给定长度的禁忌表,随着新的禁忌对象的不断进入,旧的禁忌对象会逐步退出,从而可以重新被访问。禁忌表是禁忌搜索算法的核心,其功能同人类的短时记忆功能相似,因此又称之为“短期表”。 在某些特定的条件下,无论某个选择是否包含在禁忌表中,我们都接受这个选择并更新当前解和历史最优解,这个选择所满足的特定条件称之为渴望水平。 一个基本的禁忌搜索算法的步骤描述如下: 给定一个初始可行解,将禁忌表设置为空。 选择候选集中的最优解,若其满足渴望水平,则更新渴望水平和当前解;否则选择未被禁忌的最优解。 更新禁忌表。 判断是否满足停止条件,如果满足,则停止算法;否则转至步骤 2。 模拟退火 (Simulated Annealing) 模拟退火 (Simulated Annealing) 是一种通过在邻域中寻找目标值相对小的状态从而求解全局最优的算法,现代的模拟退火是由 Kirkpatrick 等人于 1983 年提出 4。模拟退火算法源自于对热力学中退火过程的模拟,在给定一个初始温度下,通过不断降低温度,使得算法能够在多项式时间内得到一个近似最优解。 对于一个优化问题 $\min f \left(x\right)$,模拟退火算法的步骤描述如下: 给定一个初始可行解 $x_0$,初始温度 $T_0$ 和终止温度 $T_f$,令迭代计数为 $k$。 随机选取一个邻域解 $x_k$,计算目标函数增量 $\Delta f = f \left(x_k\right) - f \left(x\right)$。若 $\Delta f < 0$,则令 $x = x_k$;否则生成随机数 $\xi = U \left(0, 1\right)$,若随机数小于转移概率 $P \left(\Delta f, T\right)$,则令 $x = x_k$。 降低温度 $T$。 若达到最大迭代次数 $k_{max}$ 或最低温度 $T_f$,则停止算法;否则转至步骤 2。 整个算法的伪代码如下: 在进行邻域搜索的过程中,当温度较高时,搜索的空间较大,反之搜索的空间较小。类似的,当 $\Delta f > 0$ 时,转移概率的设置也同当前温度的大小成正比。常用的降温函数有两种: $T_{k+1} = T_k * r$,其中 $r \in \left(0.95, 0.99\right)$,$r$ 设置的越大,温度下降越快。 $T_{k+1} = T_k - \Delta T$,其中 $\Delta T$ 为每一步温度的减少量。 初始温度和终止温度对算法的影响较大,相关参数设置的细节请参见参考文献。 模拟退火算法是对局部搜索和爬山算法的改进,我们通过如下示例对比两者之间的差异。假设目标函数如下: $$ f \left(x, y\right) = e^{- \left(x^2 + y^2\right)} + 2 e^{- \left(\left(x - 1.7\right)^2 + \left(y - 1.7\right)^2\right)} $$ 优化问题定义为: $$ \max f \left(x, y\right), x \in \left[-2, 4\right], y \in \left[-2, 4\right] $$ 我们分别令初始解为 $\left(1.5, -1.5\right)$ 和 $\left(3.5, 0.5\right)$,下图 (上) 为爬山算法的结果,下图 (下) 为模拟退火算法的结果。 其中,白色 的大点为初始解位置,粉色 的大点为求解的最优解位置,颜色从白到粉描述了迭代次数。从图中不难看出,由于局部最大值的存在,从不同的初始解出发,爬山算法容易陷入局部最大值,而模拟退火算法则相对稳定。 遗传算法 (Genetic Algorithm) 遗传算法 (Genetic Algorithm, GA) 是由 John Holland 提出,其学生 Goldberg 对整个算法进行了进一步完善 5。算法的整个思想来源于达尔文的进化论,其基本思想是根据问题的目标函数构造一个适应度函数 (Fitness Function),对于种群中的每个个体 (即问题的一个解) 进行评估 (计算适应度),选择,交叉和变异,通过多轮的繁殖选择适应度最好的个体作为问题的最优解。算法的整个流程如下所示: 初始化种群 在初始化种群时,我们首先需要对每一个个体进行编码,常用的编码方式有二进制编码,实值编码 6,矩阵编码 7,树形编码等。以二进制为例 (如下不做特殊说明时均以二进制编码为例),对于 $p \in \left\{0, 1, \dotsc, 100\right\}$ 中 $p_i = 50$ 可以表示为: $$ x_i = 50_{10} = 0110010_{2} $$ 对于一个具体的问题,我们需要选择合适的编码方式对问题的解进行编码,编码后的个体可以称之为一个染色体。则一个染色体可以表示为: $$ x = \left(p_1, p_2, \dotsc, p_m\right) $$ 其中,$m$ 为染色体的长度或编码的位数。初始化种群个体共 $n$ 个,对于任意一个个体染色体的任意一位 $i$,随机生成一个随机数 $\text{rand} \in U \left(0, 1\right)$,若 $\text{rand} > 0.5$,则 $p_i = 1$,否则 $p_i = 0$。 计算适应度 适应度为评价个体优劣程度的函数 $f\left(x\right)$,通常为问题的目标函数,对最小化优化问题 $f\left(x\right) = - \min \sum{\mathcal{L} \left(\hat{y}, y\right)}$,对最大化优化问题 $f\left(x\right) = \max \sum{\mathcal{L} \left(\hat{y}, y\right)}$,其中 $\mathcal{L}$ 为损失函数。 选择 对于种群中的每个个体,计算其适应度,记第 $i$ 个个体的适应度为 $F_i = f\left(x_i\right)$。则个体在一次选择中被选中的概率为: $$ P_i = \dfrac{F_i}{\sum_{i=1}^{n}{F_i}} $$ 为了保证种群的数量不变,我们需要重复 $n$ 次选择过程,单次选择采用轮盘赌的方法。利用计算得到的被选中的概率计算每个个体的累积概率: $$ \begin{equation} \begin{split} CP_0 &= 0 \\ CP_i &= \sum_{j=1}^{i}{P_i} \end{split} \end{equation} $$ 对于如下一个示例: 指标 \ 个体 $x_1$ $x_2$ $x_3$ $x_4$ $x_5$ $x_6$ 适应度 (F) 100 60 60 40 30 20 概率 (P) 0.322 0.194 0.194 0.129 0.097 0.064 累积概率 (CP) 0.322 0.516 0.71 0.839 0.936 1 每次选择时,随机生成 $\text{rand} \in U \left(0, 1\right)$,当 $CP_{i-1} \leq \text{rand} \leq CP_i$ 时,选择个体 $x_i$。选择的过程如同在下图的轮盘上安装一个指针并随机旋转,每次指针停止的位置的即为选择的个体。 交叉 交叉运算类似于染色体之间的交叉,常用的方法有单点交叉,多点交叉和均匀交叉等。 单点交叉:在染色体中选择一个切点,然后将其中一部分同另一个染色体的对应部分进行交换得到两个新的个体。交叉过程如下图所示: 多点交叉:在染色体中选择多个切点,对其任意两个切点之间部分以概率 $P_c$ 进行交换,其中 $P_c$ 为一个较大的值,例如 $P_m = 0.9$。两点交叉过程如下图所示: 均匀交叉:染色体任意对应的位置以一定的概率进行交换得到新的个体。交叉过程如下图所示: 变异 变异即对于一个染色体的任意位置的值以一定的概率 $P_m$ 发生变化,对于二进制编码来说即反转该位置的值。其中 $P_m$ 为一个较小的值,例如 $P_m = 0.05$。 小结 在整个遗传运算的过程中,不同的操作发挥着不同的作用: 选择:优胜劣汰,适者生存。 交叉:丰富种群,持续优化。 变异:随机扰动,避免局部最优。 除此之外,对于基本的遗传算法还有多种优化方法,例如:精英主义,即将每一代中的最优解原封不动的复制到下一代中,这保证了最优解可以存活到整个算法结束。 示例 - 商旅问题 以 商旅问题 为例,利用 GA 算法求解中国 34 个省会城市的商旅问题。求解代码利用了 Deap 库,结果可视化如下图所示: 一个更有趣的例子是利用 GA 算法,使用不同颜色和透明度的多边形的叠加表示一张图片,在线体验详见 这里,下图为不同参数下的蒙娜丽莎图片的表示情况: 蚁群算法 (Ant Colony Optimization, ACO) 1991 年,意大利学者 Dorigo M. 等人在第一届欧洲人工生命会议 (ECAL) 上首次提出了蚁群算法。1996 年 Dorigo M. 等人发表的文章 “Ant system: optimization by a colony of cooperating agents” 8 为蚁群算法奠定了基础。在自然界中,蚂蚁会分泌一种叫做信息素的化学物质,蚂蚁的许多行为受信息素的调控。蚂蚁在运动过程中能够感知其经过的路径上信息素的浓度,蚂蚁倾向朝着信息素浓度高的方向移动。以下图为例 9: 蚂蚁从蚁巢 (N) 出发到达食物源所在地 (F),取得食物后再折返回蚁巢。整个过程中蚂蚁有多种路径可以选择,单位时间内路径上通过蚂蚁的数量越多,则该路径上留下的信息素浓度越高。因此,最短路径上走过的蚂蚁数量越多,则后来的蚂蚁选择该路径的机率就越大,从而蚂蚁通过信息的交流实现了寻找食物和蚁巢之间最短路的目的。 粒子群算法 (Particle Swarm Optimization, PSO) Eberhart, R. 和 Kennedy, J. 于 1995 年提出了粒子群优化算法 10 11。粒子群算法模仿的是自然界中鸟群和鱼群等群体的行为,其基本原理描述如下: 一个由 $m$ 个粒子 (Particle) 组成的群体 (Swarm) 在 $D$ 维空间中飞行,每个粒子在搜索时,考虑自己历史搜索到的最优解和群体内 (或邻域内) 其他粒子历史搜索到的最优解,在此基础上进行位置 (状态,也就是解) 的变化。令第 $i$ 个粒子的位置为 $x_i$,速度为 $v_i$,历史搜索的最优解对应的点为 $p_i$,群体内 (或邻域内) 所有粒子历史搜索到的最优解对应的点为 $p_g$,则粒子的位置和速度依据如下公式进行变化: $$ \begin{equation} \begin{split} v^{k+1}_i &= \omega v^k_i + c_1 \xi \left(p^k_i - x^k_i\right) + c_2 \eta \left(p^k_g - x^k_i\right) \\ x^{k+1}_i &= x^k_i + v^{k+1}_i \end{split} \end{equation} $$ 其中,$\omega$ 为惯性参数;$c_1$ 和 $c_2$ 为学习因子,其一般为正数,通常情况下等于 2;$\xi, \eta \in U \left[0, 1\right]$。学习因子使得粒子具有自我总结和向群体中优秀个体学习的能力,从而向自己的历史最优点以及群体内或邻域内的最优点靠近。同时,粒子的速度被限制在一个最大速度 $V_{max}$ 范围内。 对于 Rosenbrock 函数 $$ f \left(x, y\right) = \left(1 - x\right)^2 + 100 \left(y - x^2\right)^2 $$ 当 $x \in \left[-2, 2\right], y \in \left[-1, 3\right]$,定义优化问题为最小化目标函数,最优解为 $\left(1, 1\right)$。利用 PySwarms 扩展包的优化过程可视化如下: 其中,$m = 50, \omega = 0.8, c_1 = 0.5, c_2 = 0.3$,迭代次数为 200。 本节相关示例代码详见 这里。 超启发式算法 (Hyper-Heuristic Algorithms) 超启发式算法 (Hyper-Heuristic Algorithms) 提供了一种高层次启发式方法,通过管理或操纵一系列低层次启发式算法 (Low-Level Heuristics,LLH),以产生新的启发式算法。这些新启发式算法被用于求解各类组合优化问题 12。 下图给出了超启发式算法的概念模型。该模型分为两个层面:在问题域层面上,应用领域专家根据自己的背景知识,在智能计算专家协助下,提供一系列 LLH 和问题的定义、评估函数等信息;在高层次启发式方法层面上,智能计算专家设计高效的管理操纵机制,运用问题域所提供的 LLH 算法库和问题特征信息,构造出新的启发式算法。 邢文训, & 谢金星. (2005). 现代优化计算方法. 清华大学出版社. ↩︎ ↩︎ 汪定伟, 王俊伟, 王洪峰, 张瑞友, & 郭哲. (2007). 智能优化方法. 高等教育出版社. ↩︎ Glover, F. W., & Laguna, M. (1997). Tabu Search. Springer US. ↩︎ Kirkpatrick, S., Gelatt, C. D., & Vecchi, M. P. (1983). Optimization by Simulated Annealing. Science, 220(4598), 671–680. ↩︎ https://en.wikipedia.org/wiki/Genetic_algorithm ↩︎ Michalewicz, Z., Janikow, C. Z., & Krawczyk, J. B. (1992). A modified genetic algorithm for optimal control problems. Computers & Mathematics with Applications, 23(12), 83-94. ↩︎ Gottlieb, J., & Paulmann, L. (1998, May). Genetic algorithms for the fixed charge transportation problem. In Evolutionary Computation Proceedings, 1998. IEEE World Congress on Computational Intelligence., The 1998 IEEE International Conference on (pp. 330-335). IEEE. ↩︎ Dorigo, M., Maniezzo, V., & Colorni, A. (1996). Ant system: optimization by a colony of cooperating agents. IEEE Transactions on Systems, man, and cybernetics, Part B: Cybernetics, 26(1), 29-41. ↩︎ Toksari, M. D. (2016). A hybrid algorithm of Ant Colony Optimization (ACO) and Iterated Local Search (ILS) for estimating electricity domestic consumption: Case of Turkey. International Journal of Electrical Power & Energy Systems, 78, 776-782. ↩︎ Eberhart, R., & Kennedy, J. (1995, November). Particle swarm optimization. In Proceedings of the IEEE international conference on neural networks (Vol. 4, pp. 1942-1948). ↩︎ Eberhart, R., & Kennedy, J. (1995, October). A new optimizer using particle swarm theory. In MHS'95. Proceedings of the Sixth International Symposium on Micro Machine and Human Science (pp. 39-43). IEEE. ↩︎ 江贺. (2011). 超启发式算法:跨领域的问题求解模式. 中国计算机学会通讯, 7(2), 63-70 ↩︎

2019/4/5
articleCard.readMore

关不掉的浏览器标签页 (Browser Tabs You do not Close)

有舍,方有得。弱水三千,我只取一瓢饮。 前不久买重了一本书《断舍离》,新版的装帧挺小清新的,就买了,到家盖完私印后才发现角落里躺着另一本。去年如实有些“买书如山倒,读书如抽丝”了,工作忙是客观原因,不过更多的还是要怪自己懒散了。书不厚,其实之前也没想着买这本书,因为总感觉书名透露着机场里面琳琅满目的成功学书籍的味道,发现买重了之后,用几天零散的时间也就翻完了。书里的道理自己都清楚,但又为什么没有明白地去做呢?我想这本书对于我最重要的就是激发了我去认真理解什么是“执念”,如何放下自己的“执念”。 或许从我妈那继承了“勤俭持家”的“优良传统”,我也不是很喜欢把东西丢掉,想着总还会有用上的时候,于是就慢慢的东西越堆越多。但毕竟还是年轻的一代,读完书,按照其中的建议整理了下租住的屋子,还真丢掉了不少东西,完事后多少有种轻松的感觉。其实我还是很愿意去收拾房间的,只不过之前更多的是“整理”,而现在则开始尝试“舍弃”。 屋子整理完了,第二天上班,打开电脑,才发现,更需要收拾的地方在这里,再具体写就是我的浏览器。标签页的数量已经多到连每个标签页的关闭按钮都显示不出来了,而且还是有 3 个这样的浏览器实例在那躺着,好在本子性能不错,不然 Chrome 这种吃内存大户早就把电脑搞瘫了。 拖延症 主观之上说白了我就是“拖延症”犯了,没有什么好狡辩的。如果网页打开了,看完了,内容理解吸收了亦或开怀一笑,那么这个标签页也就自然被关闭了。但拖延症确实也不是很好能改掉,我想每个人或多或少都会有拖延症,只是程度不同而已。当然也会有很多客观因素,工作和生活的节奏都很快,真的来不及把所有事情都做完,更不要说都做好。 知识管理 做不到完全摆脱拖延症,那就从技术的角度考虑考虑如何高效的获取获取信息 (知识) 吧。信息爆炸也已经不是一天两天的事了,如何在漫天遍地的信息中准确定位你想要的内容还是有些技巧可循。有些人习惯逛门户网站,这对于一些时效性信息不失为一个选择,但这类网站反而不会成为你关不掉的标签中的一员。对于知识性的内容,我更倾向于使用 RSS 进行订阅,并且仅订阅少量的优质汇总源和个人博客,对于离线知识则使用 Zotero 进行管理。可以说在技巧和工具上我也算是走在前面了,但订阅中的一个个连接被打开,却没有一个个被关掉。 断舍离 《断舍离》中提到的“执念”让我问了自己一个问题:你想要的真的是你想要的吗?所以你认为你想要的可能并不是你想要的 (或你需要的),但清楚自己“真的”想要的,也如实不好做。所以我没有打算一下子就能把我的标签页控制在个位数,只能尝试着去做做看。《断舍离》中的一些方法是可取的,但要针对“信息”这种还比较特殊的东西进行一些变化可能才会适用。 一些尝试 重新审视你到底想干什么,这样才会更清楚想要什么或需要什么? 当不清楚取舍与否,先尝试“舍”,如果有不好的影响,再“取”回来也还来得及。 完成一件事才是真正放下了执念,不要同时干两件类似的事情,干完一件再干一件。

2019/3/9
articleCard.readMore

贝塞尔曲线 (Bézier Curve)

知道贝塞尔曲线 (Bézier Curve) 这个名字已经有很长一段时间了,但一直没有去详细了解一番。直到最近想要绘制一个比较复杂的曲线,才发现很多工具都以贝塞尔曲线为基础的,这包括 Adobe 全家桶中的钢笔工具,还有 OmniGraffle 中的曲线。迫于仅靠猜其是如何工作的但一直没猜透的无奈,只能去详细了解一下其原理再使用了。 数学表示 贝塞尔曲线 (Bézier Curve) 是由法国工程师皮埃尔·贝兹 (Pierre Bézier) 于 1962 年所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计 1。贝塞尔曲线最初由保尔·德·卡斯特里奥 (Paul de Casteljau) 于 1959 年运用德卡斯特里奥算法 (De Casteljau’s Algorithm) 开发,以稳定数值的方法求出贝塞尔曲线。 线性贝塞尔曲线 给定点 $P_0, P_1$,线性贝塞尔曲线定义为: $$ B \left(t\right) = \left(1 - t\right) P_0 + t P_1, t \in \left[0, 1\right] $$ 不难看出,线性贝塞尔曲线即为点 $P_0$ 和 $P_1$ 之间的线段。 对于 $P_0 = \left(4, 6\right), P_1 = \left(10, 0\right)$,当 $t = 0.25$ 时,线性贝塞尔曲线如下图所示: 整个线性贝塞尔曲线生成过程如下图所示: 二次贝塞尔曲线 给定点 $P_0, P_1, P_2$,二次贝塞尔曲线定义为: $$ B \left(t\right) = \left(1 - t\right)^2 P_0 + 2 t \left(1 - t\right) P_1 + t^2 P_2, t \in \left[0, 1\right] $$ 对于 $P_0 = \left(0, 0\right), P_1 = \left(4, 6\right), P_2 = \left(10, 0\right)$,当 $t = 0.25$ 时,二次贝塞尔曲线如下图所示: 整个二次贝塞尔曲线生成过程如下图所示: 三次贝塞尔曲线 给定点 $P_0, P_1, P_2, P_3$,三次贝塞尔曲线定义为: $$ B \left(t\right) = \left(1 - t\right)^3 P_0 + 3 t \left(1 - t\right)^2 P_1 + 3 t^2 \left(1 - t\right) P_2 + t^3 P_3, t \in \left[0, 1\right] $$ 对于 $P_0 = \left(0, 0\right), P_1 = \left(-1, 6\right), P_2 = \left(6, 6\right), P_3 = \left(12, 0\right)$,当 $t = 0.25$ 时,三次贝塞尔曲线如下图所示: 整个三次贝塞尔曲线生成过程如下图所示: 一般化的贝塞尔曲线 对于一般化的贝塞尔曲线,给定点 $P_0, P_1, \cdots, P_n$, $n$ 次贝塞尔曲线定义为: $$ B \left(t\right) = \sum_{i=0}^{n}{\binom{n}{i} \left(1 - t\right)^{n - i} t^{i} P_i}, t \in \left[0, 1\right] $$ 其中, $$ b_{i, n} \left(t\right) = \binom{n}{i} \left(1 - t\right)^{n - i} t^{i} $$ 称之为 $n$ 阶 Bernstein 多项式,点 $P_i$ 称为贝塞尔曲线的控制点。从生成过程来看,贝塞尔曲线是通过 $n$ 次中介点 ($Q_j, R_k, S_l$) 生成的,一个更加复杂的四次贝塞尔曲线 ($t = 0.25$) 如下图所示: 整个四次贝塞尔曲线生成过程如下图所示: 其中,$Q_0 = \left(1 - t\right) P_0 + t P_1$,$R_0 = \left(1 - t\right) Q_0 + t Q_1$,$S_0 = \left(1 - t\right) R_0 + t R_1$,$B = \left(1 - t\right) S_0 + t S_1$ 为构成贝塞尔曲线的点。 上述图形和动画的绘制代码请参见这里。 应用技巧 在很多绘图软件中,钢笔工具使用的是三次贝塞尔曲线,其中起始点和结束点分别对应 $P_0$ 和 $P_1$,起始点和结束点的控制点分别对应 $P_2$ 和 $P_3$。 在利用钢笔工具绘图时,可以参考如下建议来快速高效地完成绘图 2: 控制点尽可能在曲线的最外侧或最内侧。 除了曲线的结束处外,控制点的控制柄尽可能水平或垂直。 合理安排控制点的密度。 下面两张图分别展示了一个原始的字母图案,以及参考上述建议利用贝塞尔曲线勾勒出来的字母图案边框: 最后推荐一个网站 The Bezier Game,可以帮助更好的理解和掌握基于贝塞尔曲线的钢笔工具使用。 https://zh.wikipedia.org/wiki/贝塞尔曲线 ↩︎ So What’s the Big Deal with Horizontal & Vertical Bezier Handles Anyway? ↩︎

2019/2/19
articleCard.readMore

如何阅读一本书 (How to Read a Book)

本文为《如何阅读一本书》的简要总结,附加部分个人理解,仅供参考和快速查阅。楷体引用部分多为书中原话,其他部分多为总结和个人理解。 阅读的层次 第一提醒读者,阅读可以是一件多少主动的事。第二要指出的是,阅读越主动,效果越好。 读书,不是非做不可的事,而是想要去做的事。——《女王的教室》 阅读的目标:为获得资讯而读,以及为求的理解而读。 也就是说,阅读类似学习,知其然,知其所以然。 阅读就是学习:指导型的学习,以及自我发现型的学习之间的差异。 独立的思考与思辨。 阅读的层次分为: 基础阅读 (Elementary Reading) 检视阅读 (Inspectional Reading) 分析阅读 (Analytical Reading) 主题阅读 (Syntopical Reading) 基础阅读 基础阅读的四个阶段: 阅读准备阶段,相当于学前教育或幼稚园的学习经验。 认字,相当于一年级学生典型的学习经验。 字汇的增长及对课文的运用,通常是四年级结束时学会的方法。 “成熟”的阅读者,小学或初中毕业时的读写能力。 无限制的受教育机会是一个社会能提供给人民最有价值的服务。 检视阅读 有系统的略读或粗读 略读 (Skimming) 和粗读 (Pre-reading) 是检视阅读的第一个层次,用不着花太多时间,如何去做,建议如下: 先看书名页,然后如果有序就先看序。 研究目录页,对书的基本架构做概括性的理解。 如果书中附有索引,也要检阅一下。 读一下出版者的介绍。 挑几个看来跟主题息息相关的篇章来看。 东翻翻西翻翻,念一两段,连续读几页,不要太多。 在最多不超过一个小时的时间内对书有个大概的了解其是否包含你还想继续挖掘下去的内容,是否值的你再继续投入时间与注意。 粗浅的阅读 头一次面对一本难读的书的时候,从头到尾读完一遍,碰到不懂的地方不要停下来查询或思索。 在阅读一本书的时候,慢不该慢的不值得,快不该快到有损于满足与理解。 略读或粗读一本书总是个好主意,尤其当你并不清楚手边的一本书是否值的细心阅读时。 在第一次阅读一本难读的书时,不要企图了解每一个子句。 做一个自我要求的读者 在阅读时要提出问题来,同时自己必须尝试去回答这些问题: 整体来说,这本书到底在谈些什么?(主题) 作者细部说了什么,怎么说的?(细节) 这本书说得有道理吗?是全部有道理,还是部分有道理?(个人的评价) 这本书跟你有什么关系?(意义) 这四个问题概括了一个阅读者的责任,读书要对书负责,更要对自己负责。 你必须读出言外之意,才会有更大的收获,我们也鼓励你“写出言外之意”。 对于阅读来说,在书上做笔记是不可或缺的事。 可以让你保持清醒,不只是不昏睡,还是非常清醒。 主动的阅读是一种思考,而写出来是阅读者表达思考的好方法。 将自己的感想写下来,有助于记住作者的思想。 培养阅读的习惯,除了不断地运作练习之法,别无他法。 我们谈到一个有技术的人时,并不是在说他知道该如何去做那件事,而是他已经养成去做那件是的习惯了。 分析阅读 第一阶段 规则 1 你一定要知道自己在读的是哪一类书,而且越早知道越好。最好早在你开始阅读之前就先知道。 我们一定要超越“知道这是怎么回事”,进而明白“如果我们想做些什么,应该怎么利用它”。 也就是我们需要做到知行合一。 理论性作品是在教你这是什么,实用性的作品是在教你如何去做你想要做的事,或你认为应该做的事。 实用的书常会出现“应该”和“应当”,“好”和“坏”,“结果”和“意义”之类的字眼,相反的理论型作品却常常说“是”。 理论性作品可以分为: 历史,历史就是纪事,常以说故事的形态出现。 科学,以实验为基础,或依赖精确的观察研究,并不容易被证明。 哲学,是坐在摇椅上的思考,相对容易被观察和理解。 规则 2 使用一个单一的句子,或最多几句话(一小段文字)来叙述整本书的内容。 也就是用你的话告诉别人这本书在讲什么。 规则 3 将书中重要的篇章列举出来,说明它们如何按照顺序组成一个整体的架构。 规则 2 是在指导你注意一本书的整体性,规则 3 是在强调一本书的复杂度。 写作与阅读是一体两面的事,就像教书与被教一样。 一个作品应该有整体感,清楚明白,前后连贯。 规则 4 找出作者要问的问题。 第一阶段的目的就是掌握结构大纲。 第二阶段 规则 5 找出重要的单字,透过他们与作者达成共识。 字词只是作者表达的工具,我们需要通过这些字词探索作者想表达的本意。 一本书之所以能给你带来新的洞察力或启发,就是因为其中有一些你不能一读即懂的字句。 规则 6 将一本书中最重要的句子圈出来,找出其中的主旨。 主旨则是这些问题的答案。 阅读的一部分本质就是被困惑,而且知道自己被困惑。 惑,知惑! 规则 7 从相关文句的关联中,设法架构出一本书的基本论述。 要记住所有的论述都包含了一些声明。 要区别两种论述的不同之处。归纳法:以一个或多个特殊的事实证明某种共通的概念,演绎法:以连串的通则来证明更进一步的共通概念。 找出作者认为哪些事情是假设,哪些是能证实的或有根据的,以及哪些是不需要证实的自明之理。 规则 8 找出作者的解答。 第三阶段 遵守思维的礼节。 最能学习的读者,也就是最能批评的读者。 受教是一种美德,但受教并非是被动的顺从,而是主动的思考。 规则 9 在你说出“我同意”,“我不同意”,或“我暂缓评论”之前,你一定要能肯定的说:“我了解了”。 毫无理解便同意只是愚蠢,还不清楚便不同意也是无礼。 规则 10 当你不同意作者的观点时,要理性地表达自己的意见,不要无礼地辩驳或争论。 争议是教导与受教的一个过程。 规则 11 尊重知识与个人观点的不同,在作任何评断之前,都要找出理论基础。 事无对错,需有理有据,避免口舌之争。 当读者不只是盲目地跟从作者的论点,还能和作者的论点针锋相对时,他最后才能提出同意或反对的有意义的评论。 规则 12 说一位作者知识不足,就是说他缺少某些与他想要解决的问题相关的知识。 除非这些知识确实相关,否则就没有理由作这样的评论。 规则 13 说一位作者的知识错误,就是说他的理念不正确。 论点与事实相反。 规则 14 说一位作者是不和逻辑的,就是说他的推论荒谬。 荒谬有两种形态:一种是缺乏连贯,也就是结论冒出来了,却跟前面所说的理论连不起来。另一种是事件变化的前后不一致,也就是作者所说的两件事是前后矛盾的。 规则 15 说一位作者的分析是不完整的,就是说他并没有解决他一开始提出来的所有问题。 严格来说,规则 15 并不能作为不同意一个作者的根据。我们只能就作者的成就是有限的这一点而站在对立面上。 CHEAT SHEET 分析阅读的第一阶段:找出一本书在谈些什么的规则 (1). 依照书本的种类与主题作分类。 (2). 用最简短的句子说出整本书在谈些什么。 (3). 按照顺序与关系,列出全书的重要部分。将全书的纲要拟出来之后,再将各个部分的纲要也一一列出。 (4). 找出作者在问的问题,或作者想要解决的问题。 分析阅读的第二阶段:诠释一本书的内容规则 (5). 诠释作者使用的关键字,与作者达成共识。 (6). 从最重要的句子中抓出作者的重要主旨。 (7). 找出作者的论述,重新架构这些论述的前因后果,以明白作者的主张。 (8). 确定作者已经解决了哪些问题,还有哪些是未解决的。在未解决的问题中,确定哪些是作者认为自己无法解决的问题。 分析阅读的第三阶段:像是沟通知识一样地评论一本书的规则 A. 智慧礼节的一般规则 (9). 除非你已经完成大纲架构,也能诠释整本书了,否则不要轻易批评。(在你说出:“我读懂了”之前,不要说你同意、不同意或暂缓评论。) (10). 不要争强好胜,非辩到底不可。 (11). 在说出评论之前,你要能证明自己区别得出真正的知识与个人观点的不同。 B. 批评观点的特别标准 (12). 证明作者的知识不足。 (13). 证明作者的知识错误。 (14). 证明作者不合逻辑。 (15). 证明作者的分析与理由是不完整的。 辅助阅读 内在阅读,是指阅读书籍本身,于所有其他的书都是不相关的。外在阅读,是指借助其他的一些书籍来阅读一本书。 外在的辅助来源可以分为四个部分:相关经验,其他的书,导论与摘要,工具书。 导读和摘要要尽量少用,因为:一本书的导读并不一定都是对的,就算他们写对了,可能也不完整。 如果你在阅读全书之前,先看了他的导读手册,你就隶属于他了。 这也是为什么我不喜欢将自己看过的书借给其他人的原因,我不希望其他读者在第一次读这本书的时候就被我记录在书上的笔记所影响。 阅读不同读物的方法 阅读实用型的书 分析阅读的规则,一般来说适用于论说性的作品,也就是说任何一种传达知识的书。 任何实用性的书都不能解决该书所关心的问题。 实用性的书分为两类:其中一种,就像本书一样,或是烹饪书、驾驶指南,基本上都是在说明规则的。另一类的主要是在阐述形成规则的原理,许多伟大的经济、政治、道德巨著就属于这一类。 在读实用性的书要提出的四个问题: 这本书在谈些什么? 找出作者的共识、主旨和论述。 内容真实吗?(比前两点重要) 这本书于我何干? 赞同一本实用性的书,需要你采取行动。 阅读想象文学 想象文学的主要目的是娱乐,而非教育。 关于阅读想象文学,建议的否定指令: 不要抗拒想象文学带给你的影响力。(生活不只有眼前的苟且,还有诗和远方) 在想象文学中,不要去找共识、主旨和论述。 不要用适用于传递知识的,与真理一致的标准来批评小说。(一千个人眼中有一千个哈姆雷特) 阅读小说的规则: 架构性: 你必须将想象文学作品分类。 你要抓住整本书的大意,一篇故事的大意总是在情节之中。 你不仅能够将整本书简化为大意,还要能发现整本书各个部分是如何架构起来的。 诠释性: 小说的要素是插曲、事件、角色与他们的思想、言语、感觉及行动。 共识与主旨有关。(尝试身临其境) 任何活动就是论述的发展。 评论性 在你衷心感谢作者试着为你创造的经验之前,不要批评一本想象的作品。 不该反对或赞成,而是喜欢或不喜欢。 阅读故事、戏剧与诗 暴君并不怕唠叨的作家宣扬自由的思想,他害怕一个醉酒的诗人说了一个笑话,吸引了全民的注意力。 所谓“纯”艺术,并不是因为“精致”或“完美”,而是因为作品本身就是一个结束,不再与其他的影响有关。就如同爱默生所说的,美的本身就是存在的唯一标准。 阅读故事书的规则: 快读,并且全心全意地读。 整本书在谈些什么?一个故事的词义,存在于角色与事件之中。 批评小说时,要区分是满足个人特殊潜意识需求的小说还是大多数人潜意识的小说。 阅读抒情诗的规则: 不论你自己觉得懂不懂,都要一口气读完,不要停。 重读一遍,大声读出来。 对论说性作品所提出的问题是文法与逻辑上的问题。对抒情诗的问题却通常是修辞的问题,或是句法的问题。 要了解一首诗,一定要去读它,一遍又一遍地读。 阅读历史书 就事实而言的历史 (history of fact) 与就书写记录而言的历史 (history as a written record of the facts) 是不同的。 历史的基本是叙事的。 所以叙事应尽可能的公平,公正地描述所发生的事情。 历史比较接近小说,而非科学。这并不是说历史学家在捏造事实,就像诗人或小说家那样。 历史根本就没有模式可循。 在了解一个已经发生过的事情时,最好多听取几个不同的版本,哪怕每个人的陈述都已经尽可能的公平公正了,但也可能会存在信息的丢失。 修昔底德说过,他写历史的原因是:希望经由他所观察到的错误,以及他个人受到的灾难与国家所受的苦楚,将来的人们不会重蹈覆辙。 以铜为鉴,可以正衣冠,以人为鉴,可以知得失,以史为鉴,可以知兴替。 阅读历史书要提出的问题: 每一本历史书都有一个特殊而且有限定范围的主题。 历史书在说一个故事,而这个故事当然是发生在一个特定的时间里。 这与我何干?历史会建议一些可行性,因为那是以前的人已经做过的事。 传记包含很多类型: 定案本 (definitive) 的传记是对一个人的一生作详尽完整的学术性报告,这个人重要到够得上写这种完结篇的传记。定案本的传记决不能用来写活着的人。 授权本 (authorized) 的传记通常是由继承人,或是某个重要人物的朋友来负责的。读这种书不能像读一般的历史书一样,读者必须了解作者可能会有偏见。 自传所写的都是还未完结的生活。对于任何自传都要有一点疑心,同时别忘了,在你还不了解一本书之前,不要妄下论断。 阅读科学与数学 科学的客观不在于没有最初的偏见,而在于坦白承认。 科学基本上是归纳法,基本的论述也就是经由研究查证,建立出来的一个通则。 只要你记住,你的责任不是成为这个主题的专家,而是要去了解相关的问题,在阅读时就会轻松许多。 阅读哲学书 我想在进一步阅读或学习如何阅读哲学书之前,最好针对这个相对特殊的类别有一个简要的科普。待我对其窥见一斑后再回来补充这一章节。 阅读社会科学 社会科学不是一个完全独立的学科。诸如人类学、经济学、政治学、社会学的学科,都是组成社会科学的核心。大部分有关法律、教育、公共行政的作品,及一部分商业、社会服务的作品,再加上大量的心理学作品,也适合社会科学的定义。 阅读社会科学时,关于一个主题通常要读好几本书,而不会只读一本书。主要的着眼点在一个特殊的事件或问题上,而非一个特殊的作者或一本书。 主题阅读 在作主题阅读时,第一个要求就是知道:对一个特定的问题来说,所牵涉的绝对不是一本书而已。第二个要求则是:要知道就总的来说,应该读的是哪些书?第二个要求比第一个要求还难做到。 分析阅读的技巧只适用于单一的作品,主要的目标是要了解这本书。 在主题阅读的准备阶段包含如下步骤: 针对你要研究的主题,设计一份实验性的书目。你可以参考图书馆目录,专家的建议与书中的书目索引。 浏览这份书目上所有的书,确定哪些与你的主题相关,并就你的主题建立起清楚的概念。 主题阅读一共有五个步骤,这些步骤不能称之为规则,因为只要漏掉其中一个步骤,主题阅读就会变得很困难。 浏览所有在准备阶段被认定与你主题相关的书,找出最相关的章节。 根据主题创造出一套中立的词汇,带引作者与你达成共识,无论作者是否实际用到这些词汇,所有的作者,或至少绝大部分的作者都可以用这套词汇来诠释。 建立一个中立的主旨,列出一连串的问题,无论作者是否明白谈过这些问题,所有的作者,或者至少大多数的作者都要能解读为针对这些问题提供了他们的回答。 界定主要及次要的议题。然后将作者针对各个问题的不同意见整理陈列在各个议题之旁。你要记住,各个作者之间或之中,不见得一定存在着某个议题。有时候,你需要针对一些不是作者主要关心的范围的事情,把他的观点解读,才能建构出这种议题。 分析这些讨论。这得把问题和议题按照顺序排列,以求突显主题。比较有共通性的议题,要放在比较没有共通性的议题之前。各个议题之间的关系也要清楚得界定出来。 心智成长 对你来说最重要的是,你不只要能读得好,还有有能力分辨出哪些书能够帮助你增进阅读能力。 读一本好书,会让你的努力有所回报: 当你成功地阅读了一本难读的好书之后,你的阅读技巧必然增进了。 一本好书能教你了解这个世界以及你自己。 一本书如果是可以让你学习的书,重读的时候,你会发现书中的内容好像比你记忆中的少了许多。如果这本书属于更高层次的书,你在重读的时候会发现这本书好像与你一起成长了。 好的阅读,也就是主动的阅读,不只是对阅读本身有用,也不只是对我们的工作或事业有帮助,更能帮助我们的心智保持活力与成长。

2019/2/7
articleCard.readMore

相似性和距离度量 (Similarity & Distance Measurement)

相似性度量 (Similarity Measurement) 用于衡量两个元素之间的相似性程度或两者之间的距离 (Distance)。距离衡量的是指元素之间的不相似性 (Dissimilarity),通常情况下我们可以利用一个距离函数定义集合 $X$ 上元素间的距离,即: $$ d: X \times X \to \mathbb{R} $$ 同时,对于集合 $X$ 内的元素 $x, y, z$,距离函数一般满足如下条件: $d \left(x, y\right) \geq 0$ (非负性) $d \left(x, y\right) = 0, \text{当且仅当} \ x = y$ (同一性) $d \left(x, y\right) = d \left(y, x\right)$ (对称性) $d \left(x, z\right) \leq d \left(x, y\right) + d \left(y, z\right)$ (三角不等式) 明可夫斯基距离 (明氏距离, Minkowski Distance) 对于点 $x = \left(x_1, x_2, ..., x_n\right)$ 和点 $y = \left(y_1, y_2, ..., y_n\right)$,$p$ 阶明可夫斯基距离 定义为: $$ d \left(x, y\right) = \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} $$ 当 $p = 1$ 时,称之为 曼哈顿距离 (Manhattan Distance) 或 出租车距离: $$ d \left(x, y\right) = \sum_{i=1}^{n} |x_i - y_i| $$ 当 $p = 2$ 时,称之为 欧式距离 (Euclidean Distance) : $$ d \left(x, y\right) = \sqrt{\sum_{i=1}^{n} \left(x_i - y_i\right)^2} $$ 上图中 绿色 的直线为两点间的欧式距离,红色 黄色 蓝色 的折线均为两点间的曼哈顿距离,不难看出 3 条折线的长度是相同的。 当 $p \to \infty$ 时,称之为 切比雪夫距离 (Chebyshev Distance) : $$ d \left(x, y\right) = \lim_{p \to \infty} \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{\frac{1}{p}} = \max_{i=1}^{n} |x_i - y_i| $$ 下图展示了不同的 $p$ 值下单位圆,即 $x^p + y^p = 1$,便于大家理解不同 $p$ 值下的明可夫斯基距离: 马哈拉诺比斯距离 (马氏距离, Mahalanobis Distance) 马哈拉诺比斯距离表示数据的 协方差距离,与欧式距离不同其考虑到各种特性之间的联系是 尺度无关 (Scale Invariant) 的。对于一个协方差矩阵为 $\sum$ 的变量 $x$ 和 $y$,马氏距离定义为: $$ d \left(x, y\right) = \sqrt{\left(x - y\right)^{\top} {\sum}^{-1} \left(x - y\right)} $$ 马氏距离的最大优势就是其不受不同维度之间量纲的影响,同时引入的问题便是扩大了变化量较小的变量的影响。以下图为例 (源码详见 这里): 左侧图中根据欧式距离计算,红色 的点距离 绿色 的点更近一些,右侧图是根据马氏距离进行座标变换后的示意图,不难看出此时 红色 的点距离 蓝色 的点更近一些。 向量内积 (Inner Product of Vectors) 在欧几里得几何中,两个笛卡尔坐标向量的点积常称为内积,向量内积是两个向量的长度与它们夹角余弦的积,定义为: $$ x \cdot y = \sum_{i=1}^{n}{x_i y_i} $$ 从代数角度看,先对两个数字序列中的每组对应元素求积,再对所有积求和,结果即为点积。从几何角度看,点积则是两个向量的长度与它们夹角余弦的积。在欧几里得空间中,点积可以直观地定义为: $$ x \cdot y = \left| x \right| \left| y \right| \cos \theta $$ 余弦相似度 (Cosine Similarity) 可以利用两个向量夹角的 cos 值定义,即: $$ s \left(x, y\right) = \cos \left(\theta\right) = \dfrac{x \cdot y}{\left| x \right| \left| y \right|} = \dfrac{\sum_{i=1}^{n}{x_i y_i}}{\sqrt{\sum_{i=1}^{n}{x_i^2}} \sqrt{\sum_{i=1}^{n}{y_i^2}}} $$ 余弦相似度的取值范围为:$\left[-1, 1\right]$,1 表示两者完全正相关,-1 表示两者完全负相关,0 表示两者之间独立。余弦相似度与向量的长度无关,只与向量的方向有关,但余弦相似度会受到向量平移的影响。 皮尔逊相关系数 (Pearson Correlation) 解决了余弦相似度会收到向量平移影响的问题,其定义为: $$ \rho \left(x, y\right) = \dfrac{\text{cov} \left(x, y\right)}{\sigma_x \sigma_y} = \dfrac{E \left[\left(x - \mu_x\right) \left(y - \mu_y\right)\right]}{\sigma_x \sigma_y} $$ 其中,$\text{cov}$ 表示协方差,$E$ 表示期望,$\mu$ 表示均值,$\sigma$ 表示标准差。对于样本的皮尔逊相关系数,可以通过如下方式计算: $$ \begin{equation} \begin{split} r &= \dfrac{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right) \left(y_i - \bar{y}\right)}}{\sqrt{\sum_{i=1}^{n}{\left(x_i - \bar{x}\right)^2}} \sqrt{\sum_{i=1}^{n}{\left(y_i - \bar{y}\right)^2}}} \\ &= \dfrac{1}{n-1} \sum_{i=1}^{n}{\left(\dfrac{x_i - \bar{x}}{\sigma_x}\right) \left(\dfrac{y_i - \bar{y}}{\sigma_y}\right)} \end{split} \end{equation} $$ 皮尔逊相关系数的取值范围为:$\left[-1, 1\right]$,值的含义与余弦相似度相同。皮尔逊相关系数有一个重要的数学特性是:变量位置和尺度的变化并不会引起相关系数的改变。下图给出了不同的 $\left(x, y\right)$ 之间的皮尔逊相关系数。 集合距离 (Distance of Sets) 对于两个集合之间的相似性度量,主要有如下几种方法: Jaccard 系数 $$ s = \dfrac{\left|X \cap Y\right|}{\left| X \cup Y \right|} = \dfrac{\left|X \cap Y\right|}{\left|X\right| + \left|Y\right| - \left|X \cap Y\right|} $$ Jaccard 系数的取值范围为:$\left[0, 1\right]$,0 表示两个集合没有重合,1 表示两个集合完全重合。 Dice 系数 $$ s = \dfrac{2 \left| X \cap Y \right|}{\left|X\right| + \left|Y\right|} $$ 与 Jaccard 系数相同,Dice 系数的取值范围为:$\left[0, 1\right]$,两者之间可以相互转换 $s_d = 2 s_j / \left(1 + s_j\right), s_j = s_d / \left(2 - s_d\right)$。不同于 Jaccard 系数,Dice 系数的差异函数 $d = 1 - s$ 并不是一个合适的距离度量,因为其并不满足距离函数的三角不等式。 Tversky 系数 $$ s = \dfrac{\left| X \cap Y \right|}{\left| X \cap Y \right| + \alpha \left| X \setminus Y \right| + \beta \left| Y \setminus X \right|} $$ 其中,$X \setminus Y$ 表示集合的相对补集。Tversky 系数可以理解为 Jaccard 系数和 Dice 系数的一般化,当 $\alpha = \beta = 1$ 时为 Jaccard 系数,当 $\alpha = \beta = 0.5$ 时为 Dice 系数。 字符串距离 (Distance of Strings) 对于两个字符串之间的相似性度量,主要有如下几种方法: Levenshtein 距离 Levenshtein 距离是 编辑距离 (Editor Distance) 的一种,指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如将 kitten 转成 sitting,转换过程如下: $$ \begin{equation*} \begin{split} \text{kitten} \to \text{sitten} \left(k \to s\right) \\ \text{sitten} \to \text{sittin} \left(e \to i\right) \\ \text{sittin} \to \text{sitting} \left(\ \to g\right) \end{split} \end{equation*} $$ 编辑距离的求解可以利用动态规划的思想优化计算的时间复杂度。 Jaro-Winkler 距离 对于给定的两个字符串 $s_1$ 和 $s_2$,Jaro 相似度定义为: $$ sim = \begin{cases} 0 & \text{if} \ m = 0 \\ \dfrac{1}{3} \left(\dfrac{m}{\left|s_1\right|} + \dfrac{m}{\left|s_2\right|} + \dfrac{m-t}{m}\right) & \text{otherwise} \end{cases} $$ 其中,$\left|s_i\right|$ 为字符串 $s_i$ 的长度,$m$ 为匹配的字符的个数,$t$ 换位数目的一半。如果字符串 $s_1$ 和 $s_2$ 相差不超过 $\lfloor \dfrac{\max \left(\left|s_1\right|, \left|s_2\right|\right)}{2} \rfloor - 1$,我们则认为两个字符串是匹配的。例如,对于字符串 CRATE 和 TRACE,仅 R, A, E 三个字符是匹配的,因此 $m = 3$,尽管 C, T 均出现在两个字符串中,但是他们的距离超过了 1 (即,$\lfloor \dfrac{5}{2} \rfloor - 1$),因此 $t = 0$。 Jaro-Winkler 相似度给予了起始部分相同的字符串更高的分数,其定义为: $$ sim_w = sim_j + l p \left(1 - sim_j\right) $$ 其中,$sim_j$ 为字符串 $s_1$ 和 $s_2$ 的 Jaro 相似度,$l$ 为共同前缀的长度 (规定不超过 $4$),$p$ 为调整系数 (规定不超过 $0.25$),Winkler 将其设置为 $p = 0.1$。 汉明距离 汉明距离为两个等长字符串对应位置的不同字符的个数,也就是将一个字符串变换成另外一个字符串所需要替换的字符个数。例如:1011101 与 1001001 之间的汉明距离是 2,“toned” 与 “roses” 之间的汉明距离是 3。 信息论距离 (Information Theory Distance) 首先我们需要理解什么是 熵 (Entropy)?熵最早是用来表示物理学中一个热力系统无序的程度,后来依据香农的信息论,熵用来衡量一个随机变量的不确定性程度。对于一个随机变量 $X$,其概率分布为: $$ P \left(X = x_i\right) = p_i, \quad i = 1, 2, ..., n $$ 则随机变量 $X$ 的熵定义如下: $$ H \left(X\right) = - \sum_{i=1}^{n} P \left(x_i\right) \log P \left(x_i\right) \label{eq:entropy} $$ 例如抛一枚硬币,假设硬币正面向上 $X = 1$ 的概率为 $p$,硬币反面向上 $X = 0$ 的概率为 $1 - p$。则对于抛一枚硬币那个面朝上这个随机变量 $X$ 的熵为: $$ H \left(X\right) = - p \log p - \left(1-p\right) \log \left(1-p\right) $$ 随概率 $p$ 变化如下图所示: 从图可以看出,当 $p = 0.5$ 时熵最大,也就是说抛一枚硬币,当正反两面朝上的概率相同时,熵最大,系统最复杂。对于公式 $\ref{eq:entropy}$,当取以 2 为底的对数时,熵的单位为比特 (bit),当取自然对数时,熵的单位为纳特 (nat),当取以 10 为底的对数时,熵的单位为哈特 (hart)。 对于随机变量 $\left(X, Y\right)$,其联合概率分布为: $$ P \left(X = x_i, Y = y_i\right) = p_{i, j}, \quad i = 1,2,...,n; \quad j = 1,2,...,m $$ 条件熵 (Conditional Entropy) 表示在已知 $X$ 的条件下 $Y$ 的不确定性,定义为: $$ \begin{equation} \begin{split} H \left(Y | X\right) &= \sum_{i=i}^{n} P \left(x_i\right) H \left(Y | X = x_i\right) \\ &= \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i\right)}{P \left(x_i, y_j\right)}}} \end{split} \end{equation} $$ 联合熵 (Joint Entropy) 用于衡量多个随机变量的随机系统的信息量,定义为: $$ H \left(X, Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log P \left(x_i, y_j\right)}} $$ 互信息 (Mutual Information) 互信息用于衡量两个变量之间的关联程度,定义为: $$ I \left(X; Y\right) = \sum_{i=1}^{n}{\sum_{j=1}^{m}{P \left(x_i, y_j\right) \log \dfrac{P \left(x_i, y_i\right)}{P \left(x_i\right) P \left(y_j\right)}}} $$ 直观上,互信息度量 $X$ 和 $Y$ 共享的信息,它度量知道这两个变量其中一个,对另一个不确定度减少的程度。 相对熵 (Relative Entropy) 相对熵又称之为 KL 散度 (Kullback-Leibler Divergence),用于衡量两个分布之间的差异,定义为: $$ D_{KL} \left(P \| Q\right) = \sum_{i}{P \left(i\right) \ln \dfrac{P \left(i\right)}{Q \left(i\right)}} $$ KL 散度为非负数 $D_{KL} \left(P \| Q\right) \geq 0$,同时其不具有对称性 $D_{KL} \left(P \| Q\right) \neq D_{KL} \left(Q \| P\right)$,也不满足距离函数的三角不等式。 交叉熵 (Corss Entropy) 交叉熵定义为: $$ \begin{equation} \begin{split} H \left(P, Q\right) &= H \left(P\right) + D_{KL} \left(P \| Q\right) \\ &= - \sum_{i}{P \left(i\right) \log Q \left(i\right)} \end{split} \end{equation} $$ 交叉熵常作为机器学习中的损失函数,用于衡量模型分布和训练数据分布之间的差异性。 JS 散度 (Jensen-Shannon Divergence) JS 散度解决了 KL 散度不对称的问题,定义为: $$ D_{JS} \left(P \| Q\right) = \dfrac{1}{2} D_{KL} \left(P \| \dfrac{P + Q}{2}\right) + \dfrac{1}{2} D_{KL} \left(Q \| \dfrac{P + Q}{2}\right) $$ 当取以 2 为底的对数时,JS 散度的取值范围为:$\left[0, 1\right]$。 推土机距离 (Earth Mover Distance, Wasserstein Distance) 推土机距离用于描述两个多维分布之间相似性,之所以称为推土机距离是因为我们将分布看做空间中的泥土,两个分布之间的距离则是通过泥土的搬运将一个分布改变到另一个分布所消耗的最小能量 (即运送距离和运送重量的乘积)。 对于给定的分布 $P = \left\{\left(p_1, w_{p1}\right), \left(p_2, w_{p2}\right), \cdots, \left(p_m, w_{pm}\right)\right\}$ 和 $Q = \left\{\left(q_1, w_{q1}\right), \left(q_2, w_{q2}\right), \cdots, \left(q_n, w_{qn}\right)\right\}$,定义从 $p_i$ 到 $q_j$ 之间的距离为 $d_{i, j}$,所需运送的重量为 $f_{i, j}$。对于 $f_{i, j}$ 有如下 4 个约束: 运送需从 $p_i$ 到 $q_j$,不能反向,即 $f_{i, j} \geq 0, 1 \leq i \leq m, 1 \leq j \leq n$。 从 $p_i$ 运送出的总重量不超过原始的总重量 $w_{pi}$,即 $\sum_{j=1}^{n}{f_{i, j}} \leq w_{pi}, 1 \leq i \leq m$。 运送到 $q_j$ 的总重量不超过其总容量 $w_{qj}$,即 $\sum_{i=1}^{m}{f_{i, j}} \leq w_{qj}, 1 \leq j \leq n$。 $\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}}} = \min \left\{\sum_{i=1}^{m}{w_{pi}}, \sum_{j=1}^{n}{w_{qj}}\right\}$。 在此约束下,通过最小化损失函数: $$ \min \sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}}} $$ 得到最优解 $f_{i, j}^*$,则推土机距离定义为: $$ D_{W} \left(P, Q\right) = \dfrac{\sum_{i=1}^{m}{\sum_{j=1}^{n}{d_{i, j} f_{i, j}^*}}}{\sum_{i=1}^{m}{\sum_{j=1}^{n}{f_{i, j}^*}}} $$ 其他距离 (Other Distance) DTW (Dynamic Time Warping) 距离 DTW 距离用于衡量两个序列之间的相似性,序列的长度可能相等也可能不相等。对于两个给定的序列 $X = \left(x_1, x_2, \cdots, x_m\right)$ 和 $Y = \left(y_1, y_2, \cdots, y_n\right)$,我们可以利用动态规划的方法求解 DTW 距离。首先我们构造一个 $m \times n$ 的矩阵,矩阵中的元素 $d_{i, j}$ 表示 $x_i$ 和 $y_j$ 之间的距离。我们需要找到一条通过该矩阵的路径 $W = \left(w_1, w_2, \cdots, w_l\right)$, $\max\left(m, n\right) \leq l < m + n + 1$,假设 $w_k$ 对应的矩阵元素为 $\left(i, j\right)$,对应的距离为 $d_k$,则 DTW 的优化目标为 $\min \sum_{k=1}^{l}{d_k}$。如下图右上角部分所示: 对于路径 $W$,需要满足如下 3 个条件: 边界条件:$w_1 = \left(1, 1\right), w_k = \left(m, n\right)$,即路径须从左下角出发,在右上角终止。 连续性:对于 $w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$,需满足 $i - i' \leq 1, j - j' \leq 1$,即路径不能跨过任何一点进行匹配。 单调性:对于 $w_{l-1} = \left(i', j'\right), w_l = \left(i, j\right)$,需满足 $0 \leq i - i', 0 \leq j - j'$,即路径上的点需单调递增,不能回退进行匹配。 利用动态规划求解 DTW 的状态转移方程为: $$ dtw_{i, j} = \begin{cases} 0 & \text{if} \ i = j = 0 \\ \infty & \text{if} \ i = 0 \ \text{or} \ j = 0 \\ d_{i, j} + \min \left(dtw_{i-1, j}, dtw_{i-1, j-1}, dtw_{i, j-1}\right) & \text{otherwise} \end{cases} $$ $dtw_{m, n}$ 则为最终的 DTW 距离。在 DTW 求解的过程中还可以使用不同的 Local Warping Step 和窗口类型,更多详细信息可看见 R 中 dtw 包。下图展示了利用 DTW 求解后不同点之间的对应关系: 流形距离 (Distance of Manifold) 关于流形距离请参见之前的博客:流形学习 (Manifold Learning)。 🎉🎉🎉 Happy New Year! 🎉🎉🎉

2019/1/1
articleCard.readMore

集成学习算法 (Ensemble Learning)

传统机器学习算法 (例如:决策树,人工神经网络,支持向量机,朴素贝叶斯等) 的目标都是寻找一个最优分类器尽可能的将训练数据分开。集成学习 (Ensemble Learning) 算法的基本思想就是将多个分类器组合,从而实现一个预测效果更好的集成分类器。集成算法可以说从一方面验证了中国的一句老话:三个臭皮匠,赛过诸葛亮。 Thomas G. Dietterich 1 2 指出了集成算法在统计,计算和表示上的有效原因: 统计上的原因 一个学习算法可以理解为在一个假设空间 $\mathcal{H}$ 中选找到一个最好的假设。但是,当训练样本的数据量小到不够用来精确的学习到目标假设时,学习算法可以找到很多满足训练样本的分类器。所以,学习算法选择任何一个分类器都会面临一定错误分类的风险,因此将多个假设集成起来可以降低选择错误分类器的风险。 计算上的原因 很多学习算法在进行最优化搜索时很有可能陷入局部最优的错误中,因此对于学习算法而言很难得到一个全局最优的假设。事实上人工神经网络和决策树已经被证实为是一 个NP 问题 3 4。集成算法可以从多个起始点进行局部搜索,从而分散陷入局部最优的风险。 表示上的原因 在多数应用场景中,假设空间 $\mathcal{H}$ 中的任意一个假设都无法表示 (或近似表示) 真正的分类函数 $f$。因此,对于不同的假设条件,通过加权的形式可以扩大假设空间,从而学习算法可以在一个无法表示或近似表示真正分类函数 $f$ 的假设空间中找到一个逼近函数 $f$ 的近似值。 集成算法大致可以分为:Bagging,Boosting 和 Stacking 等类型。 Bagging Bagging (Boostrap Aggregating) 是由 Breiman 于 1996 年提出 5,基本思想如下: 每次采用有放回的抽样从训练集中取出 $n$ 个训练样本组成新的训练集。 利用新的训练集,训练得到 $M$ 个子模型 $\{h_1, h_2, ..., h_M\}$。 对于分类问题,采用投票的方法,得票最多子模型的分类类别为最终的类别;对于回归问题,采用简单的平均方法得到预测值。 Bagging 算法如下所示: 假设对于一个包含 $M$ 个样本的数据集 $T$,利用自助采样,则一个样本始终不被采用的概率是 $\left(1 - \frac{1}{M}\right)^M$,取极限有: $$ \lim_{x \to \infty} \left(1 - \dfrac{1}{M}\right)^M = \dfrac{1}{e} \approx 0.368 $$ 即每个学习器仅用到了训练集中 $63.2\%$ 的数据集,剩余的 $36.8\%$ 的训练集样本可以用作验证集对于学习器的泛化能力进行包外估计 (out-of-bag estimate)。 随机森林 (Random Forests) 随机森林 (Random Forests) 6 是一种利用决策树作为基学习器的 Bagging 集成学习算法。随机森林模型的构建过程如下: 数据采样 作为一种 Bagging 集成算法,随机森林同样采用有放回的采样,对于总体训练集 $T$,抽样一个子集 $T_{sub}$ 作为训练样本集。除此之外,假设训练集的特征个数为 $d$,每次仅选择 $k\left(k < d\right)$ 个构建决策树。因此,随机森林除了能够做到样本扰动外,还添加了特征扰动,对于特征的选择个数,推荐值为 $k = \log_2 d$ 6。 树的构建 每次根据采样得到的数据和特征构建一棵决策树。在构建决策树的过程中,会让决策树生长完全而不进行剪枝。构建出的若干棵决策树则组成了最终的随机森林。 随机森林在众多分类算法中表现十分出众 7,其主要的优点包括: 由于随机森林引入了样本扰动和特征扰动,从而很大程度上提高了模型的泛化能力,尽可能地避免了过拟合现象的出现。 随机森林可以处理高维数据,无需进行特征选择,在训练过程中可以得出不同特征对模型的重要性程度。 随机森林的每个基分类器采用决策树,方法简单且容易实现。同时每个基分类器之间没有相互依赖关系,整个算法易并行化。 Boosting Boosting 是一种提升算法,可以将弱的学习算法提升 (boost) 为强的学习算法。基本思路如下: 利用初始训练样本集训练得到一个基学习器。 提高被基学习器误分的样本的权重,使得那些被错误分类的样本在下一轮训练中可以得到更大的关注,利用调整后的样本训练得到下一个基学习器。 重复上述步骤,直至得到 $M$ 个学习器。 对于分类问题,采用有权重的投票方式;对于回归问题,采用加权平均得到预测值。 Adaboost Adaboost 8 是 Boosting 算法中有代表性的一个。原始的 Adaboost 算法用于解决二分类问题,因此对于一个训练集 $$ T = \{\left(x_1, y_1\right), \left(x_2, y_2\right), ..., \left(x_n, y_n\right)\} $$ 其中 $x_i \in \mathcal{X} \subseteq \mathbb{R}^n, y_i \in \mathcal{Y} = \{-1, +1\}$,首先初始化训练集的权重 $$ \begin{equation} \begin{split} D_1 =& \left(w_{11}, w_{12}, ..., w_{1n}\right) \\ w_{1i} =& \dfrac{1}{n}, i = 1, 2, ..., n \end{split} \end{equation} $$ 根据每一轮训练集的权重 $D_m$,对训练集数据进行抽样得到 $T_m$,再根据 $T_m$ 训练得到每一轮的基学习器 $h_m$。通过计算可以得出基学习器 $h_m$ 的误差为 $\epsilon_m$,根据基学习器的误差计算得出该基学习器在最终学习器中的权重系数 $$ \alpha_m = \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m} $$ 更新训练集的权重 $$ \begin{equation} \begin{split} D_{m+1} =& \left(w_{m+1, 1}, w_{m+1, 2}, ..., w_{m+1, n}\right) \\ w_{m+1, i} =& \dfrac{w_{m, i}}{Z_m} \exp \left(-\alpha_m y_i h_m\left(x_i\right)\right) \end{split} \end{equation} $$ 其中 $Z_m$ 为规范化因子 $$ Z_m = \sum_{i = 1}^{n} w_{m, i} \exp \left(-\alpha_m y_i h_m \left(x_i\right)\right) $$ 从而保证 $D_{m+1}$ 为一个概率分布。最终根据构建的 $M$ 个基学习器得到最终的学习器: $$ h_f\left(x\right) = \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right) $$ AdaBoost 算法过程如下所示: 0.5$} \BREAK \ENDIF \STATE $\alpha_m \gets \dfrac{1}{2} \ln \dfrac{1 - \epsilon_m}{\epsilon_m}$ \STATE $D_{m+1} \gets \dfrac{D_m \exp \left(-\alpha_m y h_m\left(x\right)\right)}{Z_m}$ \ENDFOR \STATE $h_f\left(x\right) \gets \text{sign} \left(\sum_{m=1}^{M} \alpha_m h_m\left(x\right)\right)$ \RETURN $h_f\left(x\right)$ \ENDFUNCTION \end{algorithmic} \end{algorithm} GBDT (GBM, GBRT, MART) GBDT (Gradient Boosting Decision Tree) 是另一种基于 Boosting 思想的集成算法,除此之外 GBDT 还有很多其他的叫法,例如:GBM (Gradient Boosting Machine),GBRT (Gradient Boosting Regression Tree),MART (Multiple Additive Regression Tree) 等等。GBDT 算法由 3 个主要概念构成:Gradient Boosting (GB),Regression Decision Tree (DT 或 RT) 和 Shrinkage。 从 GBDT 的众多别名中可以看出,GBDT 中使用的决策树并非我们最常用的分类树,而是回归树。分类树主要用于处理响应变量为因子型的数据,例如天气 (可以为晴,阴或下雨等)。回归树主要用于处理响应变量为数值型的数据,例如商品的价格。当然回归树也可以用于二分类问题,对于回归树预测出的数值结果,通过设置一个阈值即可以将数值型的预测结果映射到二分类问题标签上,即 $\mathcal{Y} = \{-1, +1\}$。 对于 Gradient Boosting 而言,首先,Boosting 并不是 Adaboost 中 Boost 的概念,也不是 Random Forest 中的重抽样。在 Adaboost 中,Boost 是指在生成每个新的基学习器时,根据上一轮基学习器分类对错对训练集设置不同的权重,使得在上一轮中分类错误的样本在生成新的基学习器时更被重视。GBDT 中在应用 Boost 概念时,每一轮所使用的数据集没有经过重抽样,也没有更新样本的权重,而是每一轮选择了不用的回归目标,即上一轮计算得到的残差 (Residual)。其次,Gradient 是指在新一轮中在残差减少的梯度 (Gradient) 上建立新的基学习器。 下面我们通过一个年龄预测的 示例 (较之原示例有修改) 简单介绍 GBDT 的工作流程。 假设存在 4 个人 $P = \{p_1, p_2, p_3, p_4\}$,他们的年龄分别为 $14, 16, 24, 26$。其中 $p_1, p_2$ 分别是高一和高三学生,$p_3, p_4$ 分别是应届毕业生和工作两年的员工。利用原始的决策树模型进行训练可以得到如下图所示的结果: 利用 GBDT 训练模型,由于数据量少,在此我们限定每个基学习器中的叶子节点最多为 2 个,即树的深度最大为 1 层。训练得到的结果如下图所示: 在训练第一棵树过程中,利用年龄作为预测值,根据计算可得由于 $p_1, p_2$ 年龄相近,$p_3, p_4$ 年龄相近被划分为两组。通过计算两组中真实年龄和预测的年龄的差值,可以得到第一棵树的残差 $R = \{-1, 1, -1, 1\}$。因此在训练第二棵树的过程中,利用第一棵树的残差作为标签值,最终所有人的年龄均正确被预测,即最终的残差均为 $0$。 则对于训练集中的 4 个人,利用训练得到的 GBDT 模型进行预测,结果如下: $p_1$ :14 岁高一学生。购物较少,经常问学长问题,预测年龄 $Age = 15 - 1 = 14$。 $p_2$ :16 岁高三学生。购物较少,经常回答学弟问题,预测年龄 $Age = 15 + 1 = 16$。 $p_3$ :24 岁应届毕业生。购物较多,经常问别人问题,预测年龄 $Age = 25 - 1 = 24$。 $p_4$ :26 岁 2 年工作经验员工。购物较多,经常回答别人问题,预测年龄 $Age = 25 + 1 = 26$。 整个 GBDT 算法流程如下所示: GBDT 中也应用到了 Shrinkage 的思想,其基本思想可以理解为每一轮利用残差学习得到的回归树仅学习到了一部分知识,因此我们无法完全信任一棵树的结果。Shrinkage 思想认为在新的一轮学习中,不能利用全部残差训练模型,而是仅利用其中一部分,即: $$ r_m = y - s F_m \left(x\right), 0 \leq s \leq 1 $$ 注意,这里的 Shrinkage 和学习算法中 Gradient 的步长是两个不一样的概念。Shrinkage 设置小一些可以避免发生过拟合现象;而 Gradient 中的步长如果设置太小则会陷入局部最优,如果设置过大又容易结果不收敛。 XGBoost XGBoost 是由 Chen 等人 9 提出的一种梯度提升树模型框架。XGBoost 的基本思想同 GBDT 一样,对于一个包含 $n$ 个样本和 $m$ 个特征的数据集 $\mathcal{D} = \left\{\left(\mathbf{x}_i, y_i\right)\right\}$,其中 $\left|\mathcal{D}\right| = n, \mathbf{x}_i \in \mathbb{R}^m, y_i \in \mathbb{R}$,一个集成树模型可以用 $K$ 个加法函数预测输出: $$ \hat{y}_i = \phi \left(\mathbf{x}_i\right) = \sum_{k=1}^{K}{f_k \left(\mathbf{x}_i\right)}, f_k \in \mathcal{F} $$ 其中,$\mathcal{F} = \left\{f \left(\mathbf{x}\right) = w_{q \left(\mathbf{x}\right)}\right\} \left(q: \mathbb{R}^m \to T, w \in \mathbb{R}^T\right)$ 为回归树 (CART),$q$ 表示每棵树的结构,其将一个样本映射到最终的叶子节点,$T$ 为叶子节点的数量,每个 $f_w$ 单独的对应一棵结构为 $q$ 和权重为 $w$ 的树。不同于决策树,每棵回归树的每个叶子节点上包含了一个连续的分值,我们用 $w_i$ 表示第 $i$ 个叶子节点上的分值。 XGBoost 首先对损失函数进行了改进,添加了 L2 正则项,同时进行了二阶泰勒展开。损失函数表示为: $$ \begin{equation} \begin{split} \mathcal{L} \left(\phi\right) = \sum_{i}{l \left(\hat{y}_i, y_i\right)} + \sum_{k}{\Omega \left(f_k\right)} \\ \text{where} \ \Omega \left(f\right) = \gamma T + \dfrac{1}{2} \lambda \left\| w \right\|^2 \end{split} \end{equation} $$ 其中,$l$ 为衡量预测值 $\hat{y}_i$ 和真实值 $y_i$ 之间差异的函数,$\Omega$ 为惩罚项,$\gamma$ 和 $\lambda$ 为惩罚项系数。 我们用 $\hat{y}_i^{\left(t\right)}$ 表示第 $t$ 次迭代的第 $i$ 个实例,我们需要增加 $f_t$ 来最小化如下的损失函数: $$ \mathcal{L}^{\left(t\right)} = \sum_{i=1}^{n}{l \left(y_i, \hat{y}_i^{\left(t-1\right)} + f_t \left(\mathbf{x}_i\right)\right)} + \Omega \left(f_t\right) $$ 对上式进行二阶泰勒展开有: $$ \mathcal{L}^{\left(t\right)} \simeq \sum_{i=1}^{n}{\left[l \left(y_i, \hat{y}_i^{\left(t-1\right)}\right) + g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$ 其中,$g_i = \partial_{\hat{y}^{\left(t-1\right)}} l \left(y_i, \hat{y}^{\left(t-1\right)}\right), h_i = \partial_{\hat{y}^{\left(t-1\right)}}^{2} l \left(y_i, \hat{y}^{\left(t-1\right)}\right)$ 分别为损失函数的一阶梯度和二阶梯度。去掉常数项,第 $t$ 步的损失函数可以简化为: $$ \tilde{\mathcal{L}}^{\left(t\right)} = \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \Omega \left(f_t\right) $$ 令 $I_j = \left\{i \ | \ q \left(\mathbf{x}_i\right) = j\right\}$ 表示叶子节点 $j$ 的实例集合,上式可重写为: $$ \begin{equation} \begin{split} \tilde{\mathcal{L}}^{\left(t\right)} &= \sum_{i=1}^{n}{\left[ g_i f_t \left(\mathbf{x}_i\right) + \dfrac{1}{2} h_i f_t^2 \left(\mathbf{x}_i\right)\right]} + \gamma T + \dfrac{1}{2} \lambda \sum_{j=1}^{T}{w_j^2} \\ &= \sum_{j=1}^{T}{\left[\left(\sum_{i \in I_j}{g_i}\right) w_j + \dfrac{1}{2} \left(\sum_{i \in I_j}{h_i + \lambda}\right) w_j^2\right]} + \gamma T \end{split} \end{equation} $$ 对于一个固定的结构 $q \left(\mathbf{x}\right)$,可以通过下式计算叶子节点 $j$ 的最优权重 $w_j^*$: $$ w_j^* = - \dfrac{\sum_{i \in I_j}{g_i}}{\sum_{i \in I_j}{h_i} + \lambda} $$ 进而计算对应的最优值: $$ \tilde{\mathcal{L}}^{\left(t\right)} \left(q\right) = - \dfrac{1}{2} \sum_{j=1}^{T}{\dfrac{\left(\sum_{i \in I_j}{g_i}\right)^2}{\sum_{i \in I_j}{h_i} + \lambda}} + \gamma T $$ 上式可以作为评价树的结构 $q$ 的评分函数。通常情况下很难枚举所有可能的树结构,一个贪心的算法是从一个节点出发,逐层的选择最佳的分裂节点。令 $I_L$ 和 $I_R$ 分别表示分裂后左侧和右侧的节点集合,令 $I = I_L \cup I_R$,则分裂后损失的减少量为: $$ \mathcal{L}_{\text{split}} = \dfrac{1}{2} \left[\dfrac{\left(\sum_{i \in I_L}{g_i}\right)^2}{\sum_{i \in I_L}{h_i} + \lambda} + \dfrac{\left(\sum_{i \in I_R}{g_i}\right)^2}{\sum_{i \in I_R}{h_i} + \lambda} - \dfrac{\left(\sum_{i \in I}{g_i}\right)^2}{\sum_{i \in I}{h_i} + \lambda}\right] - \gamma $$ XGBoost 也采用了 Shrinkage 的思想减少每棵树的影响,为后续树模型留下更多的改进空间。同时 XGBoost 也采用了随机森林中的特征下采样 (列采样) 方法用于避免过拟合,同时 XGBoost 也支持样本下采样 (行采样)。XGBoost 在分裂点的查找上也进行了优化,使之能够处理无法将全部数据读入内存的情况,同时能够更好的应对一些由于数据缺失,大量零值和 One-Hot 编码导致的特征稀疏问题。除此之外,XGBoost 在系统实现,包括:并行化,Cache-Aware 加速和数据的核外计算 (Out-of-Core Computation) 等方面也进行了大量优化,相关具体实现请参见论文和 文档。 LightGBM LightGBM 是由微软研究院的 Ke 等人 10 提出了一种梯度提升树模型框架。之前的 GBDT 模型在查找最优分裂点时需要扫描所有的样本计算信息增益,因此其计算复杂度与样本的数量和特征的数量成正比,这使得在处理大数据量的问题时非常耗时。LightGBM 针对这个问题提出了两个算法: Gradient-based One-Side Sampling (GOSS) Exclusive Feature Bundling (EFB) Gradient-based One-Side Sampling 在 AdaBoost 中,样本的权重很好的诠释了数据的重要性,但在 GBDT 中并没有这样的权重,因此无法直接应用 AdaBoost 的采样方法。幸运的是 GBDT 中每个样本的梯度可以为我们的数据采样提供有用的信息。当一个样本具有较小的梯度时,其训练的误差也较小,表明其已经训练好了。一个直观的想法就是丢弃这些具有较小梯度的样本,但是这样操作会影响整个数据的分布,从而对模型的精度造成损失。 GOSS 的做法是保留具有较大梯度的样本,并从具有较小梯度的样本中随机采样。同时为了补偿对数据分布的影响,在计算信息增益的时候,GOSS 针对梯度较小的样本引入了一个常数乘子。这样就保证了模型更多的关注未得到较好训练的数据,同时又不会对原始数据分布改变过多。整个算法流程如下: Exclusive Feature Bundling 高维数据往往是稀疏的,特征空间的稀疏性为我们提供了可能的近似无损的特征降维实现。进一步而言,在稀疏的特征空间中,很多特征之间是互斥的,也就是说它们不同时取非零值。因此,我们就可以将这些互斥的特征绑定成一个特征。由于 $\#bundle \ll \#feature$,因此构建直方图的复杂度就可以从 $O \left(\#data \times \#features\right)$ 减小至 $O \left(\#data \times \#bundle\right)$,从而在不损失精度的情况下加速模型的训练。这样我们就需要解决如下两个问题: 确定对哪些特征进行绑定。 如果对这些特征进行绑定。 对哪些特征进行绑定可以利用 图着色问题 进行解决。对于一个图 $G = \left(V, E\right)$,将 $G$ 的 关联矩阵 中的每一行看成特征,得到 $|V|$ 个特征,从而可以得出图中颜色相同的节点即为互斥的特征。算法如下: 上述算法的复杂度为 $O \left(\#feature^2\right)$ ,并且仅在模型训练前运行一次。对于特征数不是很大的情况是可以接受的,但当特征数量很大时算法效率并不令人满意。进一步的优化是在不构造图的情况下进行高效的排序,即根据非零值的数量进行排序,更多的非零值意味着更高的冲突概率。 合并特征的关键在于确保原始特征的值能够从合并后的特征之中识别出来。由于基于直方图的算法保存的是原始特征的离散桶,而非连续的值,因此我们可以将互斥的特征置于不同的桶内。算法如下: EFB 算法可以将大量的互斥特征合并为少量的稠密特征,从而通过避免对零值特征的计算提高算法的运行效率。 Tree Growth 大多的决策树算法通过逐层 (Level-wise / Depth-wise) 的方法生成树,如下图所示: LightGBM 采用了另外一种 Leaf-wise (或称 Best-first) 的方式生成 11,如下图所示: 该方法想选择具有最大 Delta Loss 值的叶子节点进行生长。在固定叶子节点数量的情况下,Leaf-wise 的生长方式比 Level-wise 的方式更容易获得较低的损失值。Leaf-wise 的生长方式在数据量较小时容易产生过拟合的现象,在 LightGBM 中可以通过限制树的最大深度减轻该问题。 更多有关 LightGBM 的优化请参见论文和 文档。 CatBoost CatBoost 是由俄罗斯 Yandex 公司 12 13 提出的一种梯度提升树模型框架。相比于之前的实现,CatBoost 的优化主要包括如下几点: 提出了一种处理分类特征 (Categorical Features) 的算法。 提出了一种解决预测偏移 (Prediction Shift) 问题的算法。 分类特征 分类特征是由一些离散的值构成的集合,其无法直接应用在提升树模型中,一个常用的方法是利用 One-Hot 编码对分类特征进行预处理,将其转化成值特征。 另一种方法是根据样本的标签值计算分类特征的一些统计量 (Target Statistics, TS)。令 $\mathcal{D} = \left\{\left(\mathbf{x}_k, y_k\right)\right\}_{k=1, \dotsc, n}$ 为一个数据集,其中 $\mathbf{x}_k = \left(x_k^1, \dotsc, x_k^m\right)$ 为一个包含 $m$ 个特征的向量 (包含值特征和分类特征),$y_k \in \mathbb{R}$ 为标签值。最简单的做法是将分类特征替换为全量训练数据上对应特征值相同的标签值的均值,即 $\hat{x}_k^i \approx \mathbb{E} \left(y \ | \ x^i = x_k^i\right)$。 Greedy TS 一个简单估计 $\mathbb{E} \left(y \ | \ x^i = x_k^i\right)$ 的方法是对具有相同类型 $x_k^i$ 的样本的标签值求均值。但这种估计对于低频的分类噪音较大,因此我们可以通过一个先验 $P$ 来进行平滑: $$ \hat{x}_k^i = \dfrac{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{j=1}^{n}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$ 其中,$a > 0$ 为先验系数,$\boldsymbol{1}$ 为指示函数,通常 $P$ 取整个数据集标签值的均值。 上述贪婪 (Greedy) 的做法的问题在于存在目标泄露 (Target Leakage),即特征 $\hat{x}_k^i$ 是通过 $\mathbf{x}_k$ 的目标 $y_k$ 计算所得。这会导致条件偏移 (Conditional Shift) 的问题 14,即 $\hat{x}^i \ | \ y$ 的分布在训练集和测试集上不同。因此在计算 TS 时需要满足如下特性: 特性 1 $\mathbb{E} \left(\hat{x}^i \ | \ y = v\right) = \mathbb{E} \left(\hat{x}_k^i \ | \ y_k = v\right)$,其中 $\left(x_k, y_k\right)$ 为第 $k$ 个训练样本。 一种修正方法是在计算 TS 时使用排除掉 $\mathbf{x}_k$ 的一个子集,令 $\mathcal{D}_k \subset \mathcal{D} \setminus \left\{\mathbf{x}_k\right\}$,有: $$ \hat{x}_k^i = \dfrac{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}} \cdot y_j} + a P}{\sum_{\mathbf{x}_j \in \mathcal{D}_k}{\boldsymbol{1}_{\left\{x_j^i = x_k^i\right\}}} + a} $$ Holdout TS 另一种方法是将训练集划分为两部分 $\mathcal{D} = \hat{\mathcal{D}}_0 \sqcup \hat{\mathcal{D}_1}$,利用 $\mathcal{D}_k = \hat{\mathcal{D}}_0$ 计算 TS,利用 $\hat{\mathcal{D}_1}$ 进行训练。虽然满足了 特性 1,但是这会导致计算 TS 和用于训练的数据均显著减少,因此还需要满足另一个特性: 特性 2 有效地利用所有的训练数据计算 TS 和训练模型。 Leave-one-out TS 对于训练样本 $\mathbf{x}_k$ 令 $\mathcal{D}_k = \mathcal{D} \setminus \mathbf{x}_k$,对于测试集,令 $\mathcal{D}_k = \mathcal{D}$,但这并没有解决 Target Leakage 问题。 Ordered TS Catboost 采用了一种更有效的策略:首先对于训练样本进行随机排列,得到排列下标 $\sigma$,之后对于每个训练样本仅利用“历史”样本来计算 TS,即:$\mathcal{D}_k = \left\{\mathbf{x}_j: \sigma \left(j\right) < \sigma \left(k\right)\right\}$,对于每个测试样本 $\mathcal{D}_k = \mathcal{D}$。 Prediction Shift & Ordered Boosting 类似计算 TS,Prediction Shift 是由一种特殊的 Target Leakage 所导致的。对于第 $t$ 次迭代,我们优化的目标为: $$ h^t = \mathop{\arg\min}_{h \in H} \mathbb{E} \left(-g^t \left(\mathbf{x}, y\right) - h \left(\mathbf{x}\right)\right)^2 \label{eq:catboost-obj} $$ 其中,$g^t \left(\mathbf{x}, y\right) := \dfrac{\partial L \left(y, s\right)}{\partial s} \bigg\vert_{s = F^{t-1} \left(\mathbf{x}\right)}$。通常情况下会使用相同的数据集 $\mathcal{D}$ 进行估计: $$ h^t = \mathop{\arg\min}_{h \in H} \dfrac{1}{n} \sum_{k=1}^{n}{\left(-g^t \left(\mathbf{x}_k, y_k\right) - h \left(\mathbf{x}_k\right)\right)^2} \label{eq:catboost-obj-approx} $$ 整个偏移的链条如下: 梯度的条件分布 $g^t \left(\mathbf{x}_k, y_k\right) \ | \ \mathbf{x}_k$ 同测试样本对应的分布 $g^t \left(\mathbf{x}, y\right) \ | \ \mathbf{x}$ 存在偏移。 由式 $\ref{eq:catboost-obj}$ 定义的基学习器 $h^t$ 同由式 $\ref{eq:catboost-obj-approx}$ 定义的估计方法存在偏移。 最终影响训练模型 $F^t$ 的泛化能力。 每一步梯度的估计所使用的标签值同构建当前模型 $F^{t-1}$ 使用的相同。但是,对于一个训练样本 $\mathbf{x}_k$ 而言,条件分布 $F^{t-1} \left(\mathbf{x}_k \ | \ \mathbf{x}_k\right)$ 相对一个测试样本 $\mathbf{x}$ 对应的分布 $F^{t-1} \left(\mathbf{x}\right) \ | \ \mathbf{x}$ 发生了偏移,我们称这为预测偏移 (Prediction Shift)。 CatBoost 提出了一种解决 Prediction Shift 的算法:Ordered Boosting。假设对于训练数据进行随机排序得到 $\sigma$,并有 $n$ 个不同的模型 $M_1, \dotsc, M_n$,每个模型 $M_i$ 仅利用随机排序后的前 $i$ 个样本训练得到。算法如下: 在计算 TS 和进行 Ordered Boosting 时我们均使用了随机排列并得到 $\sigma_{cat}$ 和 $\sigma_{boost}$。需要注意的是在将两部分合并为一个算法时,我们需要令 $\sigma_{cat} = \sigma_{boost}$ 避免 Prediction Shift。这样可以保证目标 $y_i$ 不用于训练模型 $M_i$ (既不参与计算 TS,也不用于梯度估计)。 更多 CatBoost 的实现细节请参见论文和 文档。 不同实现的比较 针对 scikit-learn,XGBoost,LightGBM 和 CatBoost 4 种 GBDT 的具体实现,下表汇总了各自的相关特性: scikit-learn XGBoost LightGBM CatBoost 当前版本 0.20.1 0.81 2.2.2 0.11.1 实现语言 C, C++, Python C, C++ C, C++ C++ API 语言 Python Python, R, Java, Scala, C++ and more Python, R Python, R 模型导出 JPMML 15 JPMML 16, ONNX 17 18 ONNX 17 19 CoreML, Python, C++, JSON 20 多线程 No Yes Yes Yes GPU No Yes 21 Yes 22 Yes 23 多 GPU No Yes 21 No Yes 23 Boosting 类型 Gradient Boosted Tree (GBDT) GBDT (booster: gbtree) Generalized Linear Model, GLM (booster: gbliner) Dropout Additive Regression Tree, DART (booster: dart) GBDT (boosting: gbdt) Random Forest (boosting: rf) DART (boosting: dart) Gradient-based One-Side Sampling, GOSS (bossting: goss) Ordered (boosting_type: Ordered) Plain (bossting_type: Plain) Level-wise (Depth-wise) Split Yes Yes (grow_policy: depthwise) No Yes Leaf-wise (Best-first) Split No Yes (grow_policy: lossguide) Yes No Histogram-based Split No Yes (tree_method: hist / gpu_hist) Yes Yes 过拟合控制 Yes (max_depth, …) Yes (max_depth, max_leaves, gamma, reg_alpha, reg_lamda, …) Yes (max_depth, num_leaves, gamma, reg_alpha, reg_lamda, drop_rate, …) Yes (max_depth, reg_lambda, …) 分类特征 No No Yes (categorical_feature) Yes (cat_features) 缺失值处理 No Yes Yes (use_missing) Yes 不均衡数据 No Yes (scale_pos_weight, max_delta_step) Yes (scale_pos_weight, poisson_max_delta_step) Yes (scale_pos_weight) 不同实现的性能分析和比较可参见如下文章,括号中内容为分析的实现库: GPU Accelerated XGBoost (XGBoost) Updates to the XGBoost GPU algorithms (XGBoost) LightGBM Experiments (XGBoost, LightGBM),代码 GPU Tunning Guide and Performance Comparision (LightGBM) Lessons Learned From Benchmarking Fast Machine Learning Algorithms (XGBoost, LightGBM), 代码 CatBoost Benchmarks (XGBoost, LightGBM, CatBoost, H2O) Benchmarking and Optimization of Gradient Boosted Decision Tree Algorithms (XGBoost, LightGBM, CatBoost) Laurae++: xgboost / LightGBM (XGBoost, LightGBM), 代码 GBM Performance (XGBoost, LightGBM, H2O), 代码 Stacking Stacking 本身是一种集成学习方法,同时也是一种模型组合策略,我们首先介绍一些相对简单的模型组合策略:平均法 和 投票法。 对于 数值型的输出 $h_i \left(\mathbf{x}\right) \in \mathbb{R}$, 简单平均法 (Simple Averaging) $$ H \left(\mathbf{x}\right) = \dfrac{1}{M} \sum_{i=1}^{M}{h_i \left(\mathbf{x}\right)} $$ 加权平均法 (Weighted Averaging) $$ H \left(\mathbf{x}\right) = \sum_{i=1}^{M}{w_i h_i \left(\mathbf{x}\right)} $$ 其中,$w_i$ 为学习器 $h_i$ 的权重,且 $w_i \geq 0, \sum_{i=1}^{T}{w_i} = 1$。 对于 分类型的任务,学习器 $h_i$ 从类别集合 $\left\{c_1, c_2, \dotsc, c_N\right\}$ 中预测一个标签。我们将 $h_i$ 在样本 $\mathbf{x}$ 上的预测输出表示为一个 $N$ 维向量 $\left(h_i^1 \left(\mathbf{x}\right); h_i^2 \left(\mathbf{x}\right); \dotsc, h_i^N \left(\mathbf{x}\right)\right)$,其中 $h_i^j \left(\mathbf{x}\right)$ 为 $h_i$ 在类型标签 $c_j$ 上的输出。 绝对多数投票法 (Majority Voting) $$ H \left(\mathbf{x}\right) = \begin{cases} c_j, & \displaystyle\sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right) > 0.5 \displaystyle\sum_{k=1}^{N}{\displaystyle\sum_{i=1}^{M}{h_i^k \left(\mathbf{x}\right)}}} \\ \text{refuse}, & \text{other wise} \end{cases} $$ 即如果一个类型的标记得票数过半,则预测为该类型,否则拒绝预测。 相对多数投票法 (Plurality Voting) $$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{h_i^j \left(\mathbf{x}\right)}} $$ 即预测为得票数最多的类型,如果同时有多个类型获得相同最高票数,则从中随机选取一个。 加权投票法 (Weighted Voting) $$ H \left(\mathbf{x}\right) = c_{\arg\max_j \sum_{i=1}^{M}{w_i h_i^j \left(\mathbf{x}\right)}} $$ 其中,$w_i$ 为学习器 $h_i$ 的权重,且 $w_i \geq 0, \sum_{i=1}^{M}{w_i} = 1$。 绝对多数投票提供了“拒绝预测”,这为可靠性要求较高的学习任务提供了一个很好的机制,但如果学习任务要求必须有预测结果时则只能选择相对多数投票法和加权投票法。在实际任务中,不同类型的学习器可能产生不同类型的 $h_i^j \left(\boldsymbol{x}\right)$ 值,常见的有: 类标记,$h_i^j \left(\mathbf{x}\right) \in \left\{0, 1\right\}$,若 $h_i$ 将样本 $\mathbf{x}$ 预测为类型 $c_j$ 则取值为 1,否则取值为 0。使用类型标记的投票称之为 “硬投票” (Hard Voting)。 类概率,$h_i^j \left(\mathbf{x}\right) \in \left[0, 1\right]$,相当于对后验概率 $P \left(c_j \ | \ \mathbf{x}\right)$ 的一个估计。使用类型概率的投票称之为 “软投票” (Soft Voting)。 Stacking 24 25 方法又称为 Stacked Generalization,是一种基于分层模型组合的集成算法。Stacking 算法的基本思想如下: 利用初级学习算法对原始数据集进行学习,同时生成一个新的数据集。 根据从初级学习算法生成的新数据集,利用次级学习算法学习并得到最终的输出。 对于初级学习器,可以是相同类型也可以是不同类型的。在新的数据集中,初级学习器的输出被用作次级学习器的输入特征,初始样本的标记仍被用作次级学习器学习样本的标记。Stacking 算法的流程如下图所示: Stacking 算法过程如下: 次级学习器的训练集是有初级学习器产生的,如果直接利用初级学习器的训练集生成次级学习器的训练集,过拟合风险会比较大 26。因此,一般利用在训练初级学习器中未使用过的样本来生成次级学习器的训练样本。以 $k$ 折交叉检验为例:初始的训练集 $T$ 被随机划分为 $k$ 个大小相近的集合 $T_1, T_2, ..., T_k$。令 $T_j$ 和 $\overline{T}_j = T \setminus T_j$ 表示第 $j$ 折的测试集和训练集。则对于 $M$ 个初级学习算法,学习器 $h_m^{\left(j\right)}$ 是根据训练集 $\overline{T}_j$ 生成的,对于测试集 $T_j$ 中的每个样本 $\mathbf{x}_i$,得到 $z_{im} = h_m^{\left(j\right)} \left(\mathbf{x}_i\right)$。则根据 $\mathbf{x}_i$ 所产生的次级学习器的训练样本为 $\mathbf{z}_i = \left(\left(z_{i1}, z_{i2}, ..., z_{iM}\right), y_i\right)$。最终利用 $M$ 个初级学习器产生的训练集 $T' = \{\left(\mathbf{z}_i, y_i\right)\}_{i=1}^N$ 训练次级学习器。 下图展示了一些基础分类器以及 Soft Voting 和 Stacking 两种融合策略的模型在 Iris 数据集分类任务上的决策区域。数据选取 Iris 数据集中的 Sepal Length 和 Petal Length 两个特征,Stacking 中的次级学习器选择 Logistic Regression,详细实现请参见 这里。 Dietterich, T. G. (2000, June). Ensemble methods in machine learning. In International workshop on multiple classifier systems (pp. 1-15). ↩︎ Dietterich, T. G. (2002). Ensemble Learning, The Handbook of Brain Theory and Neural Networks, MA Arbib. ↩︎ Laurent, H., & Rivest, R. L. (1976). Constructing optimal binary decision trees is NP-complete. Information processing letters, 5(1), 15-17. ↩︎ Blum, A., & Rivest, R. L. (1989). Training a 3-node neural network is NP-complete. In Advances in neural information processing systems (pp. 494-501). ↩︎ Breiman, L. (1996). Bagging predictors. Machine learning, 24(2), 123-140. ↩︎ Breiman, L. (2001). Random forests. Machine learning, 45(1), 5-32. ↩︎ ↩︎ Fernández-Delgado, M., Cernadas, E., Barro, S., & Amorim, D. (2014). Do we need hundreds of classifiers to solve real world classification problems?. The Journal of Machine Learning Research, 15(1), 3133-3181. ↩︎ Freund, Y., & Schapire, R. E. (1997). A decision-theoretic generalization of on-line learning and an application to boosting. Journal of computer and system sciences, 55(1), 119-139. ↩︎ Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. In Proceedings of the 22Nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (pp. 785–794). ↩︎ Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., … Liu, T.-Y. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 30 (pp. 3146–3154). ↩︎ Shi, H. (2007). Best-first Decision Tree Learning (Thesis). The University of Waikato. ↩︎ Dorogush, A. V., Ershov, V., & Gulin, A. (2018). CatBoost: gradient boosting with categorical features support. arXiv preprint arXiv:1810.11363 ↩︎ Prokhorenkova, L., Gusev, G., Vorobev, A., Dorogush, A. V., & Gulin, A. (2018). CatBoost: unbiased boosting with categorical features. In S. Bengio, H. Wallach, H. Larochelle, K. Grauman, N. Cesa-Bianchi, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 31 (pp. 6637–6647). ↩︎ Zhang, K., Schölkopf, B., Muandet, K., & Wang, Z. (2013, February). Domain adaptation under target and conditional shift. In International Conference on Machine Learning (pp. 819-827). ↩︎ https://github.com/jpmml/jpmml-sklearn ↩︎ https://github.com/jpmml/jpmml-xgboost ↩︎ https://github.com/onnx/onnx ↩︎ ↩︎ https://pypi.org/project/winmltools ↩︎ https://github.com/onnx/onnxmltools ↩︎ https://tech.yandex.com/catboost/doc/dg/concepts/python-reference_catboost_save_model-docpage ↩︎ https://xgboost.readthedocs.io/en/latest/gpu/index.html ↩︎ ↩︎ https://lightgbm.readthedocs.io/en/latest/GPU-Tutorial.html ↩︎ https://tech.yandex.com/catboost/doc/dg/features/training-on-gpu-docpage ↩︎ ↩︎ Wolpert, D. H. (1992). Stacked generalization. Neural networks, 5(2), 241-259. ↩︎ Breiman, L. (1996). Stacked regressions. Machine learning, 24(1), 49-64. ↩︎ 周志华. (2016). 机器学习. 清华大学出版社. ↩︎

2018/12/8
articleCard.readMore

计算复杂性 (Computational Complexity) 与动态规划 (Dynamic Programming)

计算复杂性 计算复杂性 (Computational Complexity) 是用于对一个问题求解所需的资源 (通常为 空间 和 时间) 的度量。在评估一个算法的时候,除了算法本身的准确性以外,同时需要关注算法运行的时间以及占用的内存,从而根据实际情况选择合适的算法。 函数的增长 计算复杂性中的空间和时间的评估方法类似,在此我们更多的以时间复杂度为例。算法的运行时间刻画了算法的效率,对于一个输入规模为 $n$ 的问题,定义一个算法求解该问题 最坏情况 下的运行时间为 $T \left(n\right)$,我们可以使用一些 渐进记号 更加方便地对其进行描述。 $\Theta$ 记号 对于一个给定的函数 $g \left(n\right)$,$\Theta \left(g \left(n\right)\right)$ 可以表示如下函数的集合: $$ \Theta \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c_1 > 0, c_2 > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c_1 g \left(n\right) \leq f \left(n\right) \leq c_2 g \left(n\right) \right\} $$ 也就是说当 $n$ 足够大时,函数 $f \left(n\right)$ 能够被 $c_1 g \left(n\right)$ 和 $c_2 g \left(n\right)$ 夹在中间,我们称 $g \left(n\right)$ 为 $f \left(n\right)$ 的一个 渐进紧确界 (Asymptotically Tight Bound)。 $O$ 记号 $\Theta$ 记号给出了一个函数的上界和下界,当只有一个 渐进上界 时,可使用 $O$ 记号。$O \left(g \left(n\right)\right)$ 表示的函数集合为: $$ O \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) \leq c g \left(n\right)\right\} $$ $O$ 记号描述的为函数的上界,因此可以用它来限制算法在最坏情况下的运行时间。 $\Omega$ 记号 $\Omega$ 记号提供了 渐进下界,其表示的函数集合为: $$ \Omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \exists c > 0, n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) \leq f \left(n\right)\right\} $$ 根据上面的三个渐进记号,不难证明如下定理: 定理 1 对于任意两个函数 $f \left(n\right)$ 和 $g \left(n\right)$,有 $f \left(n\right) = \Theta \left(g \left(n\right)\right)$,当且仅当 $f \left(n\right) = O \left(g \left(n\right)\right)$ 且 $f \left(n\right) = \Omega \left(g \left(n\right)\right)$。 $o$ 记号 $O$ 记号提供的渐进上界可能是也可能不是渐进紧确的,例如 $2n^2 = O \left(n^2\right)$ 是渐进紧确的,但 $2n = O \left(n^2\right)$ 是非渐进紧确的。我们使用 $o$ 记号表示非渐进紧确的上界,其表示的函数集合为: $$ o \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c > 0, \exists n_0 > 0, s.t. \forall n \geq n_0, 0 \leq f \left(n\right) < c g \left(n\right)\right\} $$ $\omega$ 记号 $\omega$ 记号与 $\Omega$ 记号的关系类似于 $o$ 记号与 $O$ 记号的关系,我们使用 $\omega$ 记号表示一个非渐进紧确的下界,其表示的函数集合为: $$ \omega \left(g \left(n\right)\right) = \left\{f \left(n\right): \forall c > 0, \exists n_0 > 0, s.t. \forall n \geq n_0, 0 \leq c g \left(n\right) < f \left(n\right)\right\} $$ NP 完全性 计算问题可以按照在不同计算模型下所需资源的不同予以分类,从而得到一个对算法问题“难度”的类别,这就是复杂性理论中复杂性类概念的来源 1。对于输入规模为 $n$ 的问题,一个算法在最坏情况下的运行时间为 $O \left(n^k\right)$,其中 $k$ 为一个确定的常数,我们称这类算法为 多项式时间的算法。 本节我们将介绍四类问题:P 类问题,NP 类问题,NPC 类问题和 NPH 类问题。 P 类问题 P 类问题 (Polynomial Problem,多项式问题) 是指能在多项式时间内 解决 的问题。 NP 类问题 NP 类问题 (Non-Deteministic Polynomial Problem,非确定性多项式问题) 是指能在多项式时间内被 证明 的问题,也就是可以在多项式时间内对于一个给定的解验证其是否正确。所有的 P 类问题都是 NP 类问题,但目前 (截至 2018 年,下文如不做特殊说明均表示截至到该时间) 人类还未证明 $P \neq NP$ 还是 $P = NP$。 NPC 类问题 (NP-Complete Problems) 在理解 NPC 类问题之前,我们需要引入如下几个概念: 最优化问题 (Optimization Problem) 与 判定问题 (Decision Problem):最优化问题是指问题的每一个可行解都关联一个值,我们希望找到具有最佳值的可行解。判定问题是指问题的答案仅为“是”或“否”的问题。NP 完全性仅适用于判定问题,但通过对最优化问题强加一个界,可以将其转换为判定问题。 归约 (Reduction):假设存在一个判定问题 A,该问题的输入称之为实例,我们希望能够在多项式时间内解决该问题。假设存在另一个不同的判定问题 B,并且已知能够在多项式时间内解决该问题,同时假设存在一个过程,它可以将 A 的任何实例 $\alpha$ 转换成 B 的某个实例 $\beta$,转换操作需要在多项式时间内完成,同时两个实例的解是相同的。则我们称这一过程为多项式 规约算法 (Reduction Algorithm)。通过这个过程,我们可以将问题 A 的求解“归约”为对问题 B 的求解,从而利用问题 B 的“易求解性”来证明 A 的“易求解性”。 从而我们可以定义 NPC 类问题为:首先 NPC 类问题是一个 NP 类问题,其次所有的 NP 类问题都可以用多项式时间归约到这类问题。因此,只要找到 NPC 类问题的一个多项式时间的解,则所有的 NP 问题都可以通过多项式时间归约到该问题,并用多项式时间解决该问题,从而使得 $NP = P$,但目前,NPC 类问题并没有找到一个多项式时间的算法。 NPH 类问题 (NP-Hard Problems) NPH 类问题定义为所有的 NP 类问题都可以通过多项式时间归约到这类问题,但 NPH 类问题不一定是 NP 类问题。NPH 类问题同样很难找到多项式时间的解,由于 NPH 类问题相比较 NPC 类问题放松了约束,因此即便 NPC 类问题找到了多项式时间的解,NPH 类问题仍可能无法在多项式时间内求解。 下图分别展示了 $P \neq NP$ 和 $P = NP$ 两种假设情况下四类问题之间的关系: 动态规划 动态规划 (Dynamic Programming, DP) 算法通常基于一个递归公式和一个或多个初始状态,并且当前子问题的解可以通过之前的子问题构造出来。动态规划算法求解问题的时间复杂度仅为多项式复杂度,相比其他解法,例如:回溯法,暴利破解法所需的时间要少。动态规划中的 “Programming” 并非表示利用计算机编程,而是一种表格法。动态规划对于每个子问题只求解一次,将解保存在一个表格中,从而避免不必要的重复计算。 动态规划算法的适用情况如下 2: 最优子结构性质,即问题的最优解由相关子问题的最优解组合而成,子问题可以独立求解。 无后效性,即每个状态均不会影响之前的状态。 子问题重叠性质,即在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。 一个动态规划算法的核心包含两个部分:状态 和 状态转移方程。状态即一个子问题的表示,同时这个表示需要具备 无后效性。状态转移方程用于描述状态之间的关系,也就是如何利用之前的状态构造出当前的状态进而求解。 动态规划有两种等价的实现方法: 带备忘的自顶向下法 (Top-Down with Memoization),该方法采用自然的递归形式编写过程,但会保留每个子问题的解,当需要一个子问题的解时会先检查是否保存过,如果有则直接返回该结果。 自底向上法 (Bottom-Up Method),该方法需要恰当的定义子问题“规模”,任何子问题的求解都值依赖于“更小”的子问题的求解,从而可以按照子问题的规模从小到大求解。 两种方法具有相同的渐进运行时间,在某些特殊的情况下,自顶向下的方法并未真正递归地考虑所有可能的子问题;自底向上的方法由于没有频繁的递归调用,时间复杂性函数通常具有更小的系数。 背包问题 背包问题 (Knapsack problem) 是一种组合优化的 NPC 类问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,合理地选择物品使得总价值最高。 形式化的定义,我们有 $n$ 种物品,物品 $j$ 的重量为 $w_j$,价值为 $p_j$,假定所有物品的重量和价值都是非负的,背包所能承受的最大重量为 $W$。如果限定每种物品只能选择 0 个或 1 个,则该问题称为 0-1 背包问题;如果限定物品 $j$ 最多只能选择 $b_j$ 个,则该问题称为 有界背包问题;如果不限定每种物品的数量,则该问题称为 无界背包问题。最优化问题可以表示为: $$ \begin{equation} \begin{split} \text{maximize} & \sum_{j=1}^{n}{p_j x_j} \\ s.t. & \sum_{j=1}^{n}{w_j x_j} \leq W, x_j \in \left\{0, 1, ..., b_j\right\} \end{split} \end{equation} $$ 以 0-1 背包问题为例,用 $d_{i, w}$ 表示取 $i$ 件商品填充一个最大承重 $w$ 的背包的最大价值,问题的最优解即为 $d_{n, W}$。不难写出 0-1 背包问题的状态转移方程如下: $$ d_{i, w} = \begin{cases} d_{i - 1, w}, & w < w_i \\ \max \left(d_{i - 1, w}, d_{i - 1, w - w_i} + p_i\right), & w \geq w_i \\ 0, & i w = 0 \end{cases} $$ 一个 0-1 背包问题的具体示例如下:背包承受的最大重量 $W = 10$,共有 $n = 5$ 种物品,编号分别为 $A, B, C, D, E$,重量分别为 $2, 2, 6, 5, 4$,价值分别为 $6, 3, 5, 4, 6$,利用 BP 求解该问题,不同 $i, w$ 情况下的状态如下表所示 (计算过程详见 这里): i \ w 1 2 3 4 5 6 7 8 9 10 1 NA (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 (A) 2 - 6 2 NA (A) 2 - 6 (A) 2 - 6 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 3 NA (A) 2 - 6 (A) 2 - 6 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, C) 8 - 11 (A, C) 8 - 11 (A, B, C) 10 - 14 4 NA (A) 2 - 6 (A) 2 - 6 (A, B) 4 - 9 (A, B) 4 - 9 (A, B) 4 - 9 (A, D) 7 - 10 (A, C) 8 - 11 (A, B, D) 9 - 13 (A, B, C) 10 - 14 5 NA (A) 2 - 6 (A) 2 - 6 (A, B) 4 - 9 (A, B) 4 - 9 (A, E) 6 - 12 (A, E) 6 - 12 (A, B, E) 8 - 15 (A, B, E) 8 - 15 (A, B, E) 8 - 15 其中,NA 表示未选取任何物品,单元格上部括号中的为选取物品的编号,单元格下部分别为选取物品的总重量和总价值。 最长公共子序列与最长公共子串 给定一个序列 $X = \left\{x_1, x_2, \dotsc, x_m\right\}$,另一个序列 $Z = \left\{z_1, z_2, \dotsc, z_k\right\}$ 在满足如下条件时称其为 $X$ 的一个 子序例 (Subsequence),即存在一个严格递增的 $X$ 的下标序列 $\left\{i_1, i_2, \dotsc, i_k\right\}$,对于所有的 $j = 1, 2, \dotsc, k$,满足 $x_{i_j} = z_j$。给定两个序例 $X$ 和 $Y$,如果 $Z$ 既是 $X$ 的子序列,也是 $Y$ 的子序列,则称它为 $X$ 和 $Y$ 的 公共子序列 (Common Subsequence)。最长公共子序列 (Longest Common Subsequence) 问题为给定两个序列 $X = \left\{x_1, x_2, \dotsc, x_m\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_n\right\}$,求 $X$ 和 $Y$ 最长的公共子序列。 我们可以按如下递归的方式求解最长公共子序列问题: 当 $x_i = y_j$ 时,求解 $X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$ 的最长公共子序列,在其尾部添加 $x_i$ 和 $y_j$ 即为当前状态下的最长公共子序列。 当 $x_i \neq y_j$ 时,我们则需求解 $X = \left\{x_1, x_2, \dotsc, x_{i-1}\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_j\right\}$ 与 $X = \left\{x_1, x_2, \dotsc, x_i\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_{j-1}\right\}$ 两种情况下最长的公共子序列作为当前状态下的最长公共子序列。 用 $c_{i, j}$ 表示$X = \left\{x_1, x_2, \dotsc, x_i\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_j\right\}$ 情况下的最长公共子序列的长度,则状态转移方程如下: $$ c_{i, w} = \begin{cases} c_{i - 1, j - 1} + i, & x_i = y_j \\ \max \left(c_{i, j - 1}, c_{i - 1, j}\right), & x_i \neq y_j \\ 0, & i j = 0 \end{cases} $$ 例如:给定序列 $X = \left\{A, B, C, B, D, A, B\right\}$ 和序列 $Y = \left\{B, D, C, A, B, A\right\}$,不同状态下最长公共子序列如下表所示 (计算过程详见 这里): $j$ 0 1 2 3 4 5 6 $i$ $y_j$ B D C A B A 0 $x_i$ 0 0 0 0 0 0 0 1 A 0 0 (↑) 0 (↑) 0 (↑) 1 (↖) 1 (←) 1 (↖) 2 B 0 1 (↖) 1 (←) 1 (←) 1 (↑) 2 (↖) 2 (←) 3 C 0 1 (↑) 1 (↑) 2 (↖) 2 (←) 2 (↑) 2 (↑) 4 B 0 1 (↖) 1 (↑) 2 (↑) 2 (↑) 3 (↖) 3 (←) 5 D 0 1 (↑) 2 (↖) 2 (↑) 2 (↑) 3 (↑) 3 (↑) 6 A 0 1 (↑) 2 (↑) 2 (↑) 3 (↖) 3 (↑) 4 (↖) 7 B 0 1 (↖) 2 (↑) 2 (↑) 3 (↑) 4 (↖) 4 (↑) 其中,每个单元格前面的数字为最长公共子序列的长度,后面的符号为还原最长公共子序列使用的备忘录符号。 最长公共子串 (Longest Common Substring) 同最长公共子序列问题略有不同,子序列不要求字符是连续的,而子串要求字符必须是连续的。例如:给定序列 $X = \left\{A, B, C, B, D, A, B\right\}$ 和序列 $Y = \left\{B, D, C, A, B, A\right\}$,最长公共子序列为 $\left\{B, C, B, A\right\}$,而最长公共子串为 $\left\{A, B\right\}$ 或 $\left\{B, D\right\}$。用 $c_{i, j}$ 表示$X = \left\{x_1, x_2, \dotsc, x_i\right\}$ 和 $Y = \left\{y_1, y_2, \dotsc, y_j\right\}$ 情况下的最长公共子串的长度,则状态转移方程如下: $$ c_{i, w} = \begin{cases} c_{i - 1, j - 1} + i, & x_i = y_j \\ 0, & x_i \neq y_j \\ 0, & i j = 0 \end{cases} $$ 利用动态规划可以在 $\Theta \left(nm\right)$ 的时间复杂度内求解,利用广义后缀树 3 可以进一步降低问题求解的时间复杂度 4。 Floyd-Warshall 算法 Floyd-Warshall 算法 是一种求解任意两点之间 最短路 的算法,相比 Dijkstra 算法 5,Floyd-Warshall 算法可以处理有向图或负权图 (但不可以存在负权回路) 的情况 6。 用 $d_{i, j}^{\left(k\right)}$ 表示从 $i$ 到 $j$ 路径上最大节点的标号为 $k$ 的最短路径的长度。有: $d_{i, j}^{\left(k\right)} = d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}$,若最短路径经过点 $k$。 $d_{i, j}^{\left(k\right)} = d_{i, j}^{\left(k-1\right)}$,若最短路径不经过点 $k$。 则状态转移方程如下: $$ d_{i, j}^{\left(k\right)} = \begin{cases} w_{i, j}, & k = 0 \\ \min \left(d_{i, j}^{\left(k-1\right)}, d_{i, k}^{\left(k-1\right)} + d_{k, j}^{\left(k-1\right)}\right), & k \leq 1 \end{cases} $$ 以下图所示的最短路问题为例: Floyd-Warshall 算法的求解伪代码如下所示: d_{i, k} + d_{k, j}$} \STATE $d_{i, j} \gets d_{i, k} + d_{k, j}$ \STATE $m_{i, j} \gets k$ \ENDIF \ENDFOR \ENDFOR \ENDFOR \ENDFUNCTION \end{algorithmic} \end{algorithm} 通过备忘录矩阵 $m$,恢复从点 $i$ 到点 $j$ 的过程如下所示: 文章部分内容参考了 Thomas H. Cormen 等人的《算法导论》 https://zh.wikipedia.org/zh/计算复杂性理论 ↩︎ https://zh.wikipedia.org/zh/动态规划 ↩︎ https://zh.wikipedia.org/zh/后缀树 ↩︎ https://zh.wikipedia.org/zh/最长公共子串 ↩︎ https://zh.wikipedia.org/zh/戴克斯特拉算法 ↩︎ https://zh.wikipedia.org/zh/Floyd-Warshall算法 ↩︎

2018/11/18
articleCard.readMore

利用 Flask 和 Google App Engine 部署模型服务

本文的配套代码请参见 这里,建议配合代码阅读本文。 模型部署和服务调用 对于做算法的同学,大家或多或少的更关心模型的性能指标多些,对于一些工程性问题考虑的较少。模型的部署是这些工程性问题中重要的一个,它直接关系到模型在生产系统的使用。一些成熟的机器学习框架会提供自己的解决方案,例如 Tensorflow 提供的 Serving 服务等。但很多情况下我们构建的工程可能不只使用了一种框架,因此一个框架自身的部署工具可能就很难满足我们的需求了。 针对此类情况,本文介绍一个 简单 的 准生产 模型部署方案。简单是指除了模型相关代码之外的工程性代码量不大,这得益于将要使用的 Flask 框架。准生产是指这种部署方案应对一般的生产环境问题不大,对于高并发的场景可以通过横向扩容并进行负载均衡解决,但对于单次调用时效性要求较高的场景则需要另寻其他解决方案。 本文方案的模型部署和服务调用框架如下图所示: 其主要特性如下: 服务端采用 Python 的 Flask 框架构建,无需使用其他外部服务。Flask 框架的 微服务 (Microframework) 特性使得服务端代码简洁高效。 利用 Gunicorn 提供的高性能 Python WSGI HTTP UNIX Server,方便在服务端运行 Flask 应用。 客户端和服务端之间采用 RESTful API 调用方式,尽管在性能上可能不及其他一些方案 (例如:基于 RPC 的解决方案等),但其较好地解决了跨语言交互的问题,不同语言之间交互仅需使用 HTTP 协议和 JSON 数据格式即可。 Flask 服务和 AJAX 调用 Flask 服务封装 为了将模型代码和 Flask 服务进行整合,首先假设你已经对模型部分代码做了完美的封装 😎,整个工程先叫做 model-serving-demo 吧。整理一下代码的目录结构,给一个我中意的 Python 目录结构风格: model-serving-demo/ # 工程根目录 ├── bin/ # 可执行命令目录 | ├─ start.sh # 启动脚本 | ├─ stop.sh # 停止脚本 | └─ ... ├── conf/ # 配置文件目录 | ├─ logging.conf # 日志配置文件 | ├─ xxx_model.conf # XXX Model 配置文件 | └─ ... ├── data/ # 数据文件目录 ├── docs/ # 文档目录 ├── model_serving/ # 模块根目录 | ├─ models/ # 模型代码目录 | | ├─ __init__.py | | ├─ xxx_model.py # XXX Model 代码 | | └─ ... | ├─ resources/ # Flask RESTful Resources 代码目录 | | ├─ __init__.py | | ├─ xxx_model_resource.py # XXX Model Flask RESTful Resources 代码 | | └─ ... | ├─ tests/ # 测试代码根目录 | | ├─ models # 模型测试代码目录 | | | ├─ __init__.py | | | ├─ test_xxx_model.py # XXX Model 测试代码 | | | └─ ... | | ├─ __init__.py | | └─ ... | ├─ tmp/ # 临时目录 | └─ ... ├── .gitignore # Git Ignore 文件 ├── app.yaml # Google App Engine 配置文件 ├── LICENSE # 授权协议 ├── main.py # 主程序代码 ├── README.md # 说明文件 └── requirements.txt # 依赖包列表 我们利用一个极简的示例介绍整个模型部署,相关的库依赖 requirements.txt 如下: Flask==1.0.2 Flask-RESTful==0.3.6 Flask-Cors==3.0.6 jsonschema==2.6.0 docopt==0.6.2 # 本地部署时需保留,GAE 部署时请删除 # gunicorn==19.9.0 其中: Flask 用于构建 Flask 服务。 Flask-RESTful 用于构建 Flask RESTful API。 Flask-Cors 用于解决 AJAX 调用时的 跨域问题。 jsonschema 用于对请求数据的 JSON 格式进行校验。 docopt 用于从代码文档自动生成命令行参数解析器。 gunicorn 用于提供的高性能 Python WSGI HTTP UNIX Server。 XXX Model 的代码 xxx_model.py 如下: from ..utils.log_utils import XXXModel_LOGGER LOGGER = XXXModel_LOGGER class XXXModel(): def __init__(self): LOGGER.info('Initializing XXX Model ...') LOGGER.info('XXX Model Initialized.') def hello(self, name:str) -> str: return 'Hello, {name}!'.format(name=name) 其中 hello() 为服务使用的方法,其接受一个类型为 str 的参数 name,并返回一个类型为 str 的结果。 XXX Model 的 Flask RESTful Resource 代码 xxx_model_resource.py 如下: from flask_restful import Resource, request from ..models.xxx_model import XXXModel from ..utils.validation_utils import validate_json xxx_model_instance = XXXModel() xxx_model_schema = { 'type': 'object', 'properties': { 'name': {'type': 'string'} }, 'required': ['name'] } class XXXModelResource(Resource): @validate_json(xxx_model_schema) def post(self): json = request.json return {'result': xxx_model_instance.hello(json['name'])} 我们需要从 Flask RESTful 的 Resource 类继承一个新的类 XXXModelResource 用于处理 XXX Model 的服务请求。如上文介绍,我们在整个模型服务调用中使用 POST 请求方式和 JSON 数据格式,因此我们需要在类 XXXModelResource 中实现 post() 方法,同时对于传入数据的 JSON 格式进行校验。 post() 方法用于处理整个模型的服务请求,xxx_model_instance 模型实例在类 XXXModelResource 外部进行实例化,避免每次处理请求时都进行初始化。post() 的返回结果无需处理成 JSON 格式的字符串,仅需返回词典数据即可,Flask RESTful 会自动对其进行转换。 为了方便对请求数据的 JSON 格式进行校验,我们将对 JSON 格式的校验封装成一个修饰器。使用时如上文代码中所示,在 post() 方法上方添加 @validate_json(xxx_model_schema) 即可,其中 xxx_model_schema 为一个符合 jsonschema 要求的 JSON Schema。示例代码中要求传入的 JSON 数据 必须 包含一个名为 name 类型为 string 的字段。 validate_json 修饰器的代码 validation_utils.py 如下: from functools import wraps from jsonschema import validate, ValidationError from flask_restful import request def validate_json(schema, force=False): def decorator(f): @wraps(f) def wrapper(*args, **kwargs): json_body = request.get_json(force=force) if json_body is None: return {'message': 'No JSON object'}, 400 try: validate(json_body, schema) except ValidationError as e: return {'message': e.message}, 400 return f(*args, **kwargs) return wrapper return decorator 首先我们需要验证请求包含一个 JSON 请求体,同时 JSON 请求体的内容需满足 schema 的要求。如果不满足这些条件,我们需要返回对应的错误信息 message,同时返回合理的 HTTP 状态码 (例如:400) 用于表示无法处理错误的请求。对于正常的请求响应 (即 HTTP 状态码为 200 的情况),状态码可以省略不写。 构建完 XXX Model 的 Flask RESTful Resource 后,我们就可以构建 Flask 的主服务了,主程序代码 main.py 如下: """ Model Serving Demo Usage: main.py [--host <host>] [--port <port>] [--debug] main.py (-h | --help) main.py --version Options: --host <host> 绑定的 Host [default: 0.0.0.0] --port <port> 绑定的 Port [default: 9999] --debug 是否开启 Debug [default: False] -h --help 显示帮助 -v --version 显示版本 """ from docopt import docopt from flask import Flask from flask_cors import CORS from flask_restful import Api from model_serving.resources.xxx_model_resource import XXXModelResource app = Flask(__name__) CORS(app) api = Api(app) api.add_resource(XXXModelResource, '/v1/XXXModel') if __name__ == '__main__': args = docopt(__doc__, version='Model Serving Demo v1.0.0') app.run(host=args['--host'], port=args['--port'], debug=args['--debug']) docopt 库用于从代码文档自动生成命令行参数解析器,具体使用方法请参见 官方文档。整个 Flask 主服务的构建比较简单,流程如下: 构建 Flask 主程序,app = Flask(__name__)。 解决 AJAX 调用的跨域问题, CORS(app)。为了方便起见,我们不加任何参数,允许任意来源的请求,详细的使用方式请参见 官方文档。 构建 Flask RESTful API,api = Api(app)。 将构建好的 XXX Model 的 Flask RESTful Resource 添加到 API 中,api.add_resource(XXXModelResource, '/v1/XXXModel')。 其中第二个参数为请求的 URL,对于这个 URL 的建议将在后续小节中详细说明。 Flask 主程序配置完毕后,我们通过 app.run() 在本地启动 Flask 服务,同时可以指定绑定的主机名,端口,以及是否开启调试模式等。通过 python main.py 启动 Flask 服务后,可以在命令行看到如下类似的日志: [2018/10/21 00:00:00] - [INFO] - [XXXModel] - Initializing XXX Model ... [2018/10/21 00:00:00] - [INFO] - [XXXModel] - XXX Model Initialized. * Serving Flask app "main" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: off [2018/10/21 00:00:00] - [INFO] - [werkzeug] - * Running on http://0.0.0.0:9999/ (Press CTRL+C to quit) 现在就可以测试调用服务了,我们用 curl 命令进行简单的测试,相关代码 request-demo.sh 如下: host=0.0.0.0 port=9999 url=/v1/XXXModel curl_url=http://${host}:${port}${url} invalid_json='{}' valid_json='{"name": "Leo"}' # No JSON object curl --request POST --url ${curl_url} --verbose # Invalid JSON object curl --header 'Content-Type: application/json; charset=UTF-8' \ --request POST --data ${invalid_json} --url ${curl_url} --verbose # Valid JSON object curl --header 'Content-Type: application/json; charset=UTF-8' \ --request POST --data ${valid_json} --url ${curl_url} --verbose 三种不同的请求返回的 HTTP 状态码和结果如下: HTTP/1.0 400 BAD REQUEST {"message": "No JSON object"} HTTP/1.0 400 BAD REQUEST {"message": "'name' is a required property"} HTTP/1.0 200 OK {"result": "Hello, Leo!"} 上文中,我们通过 python main.py 利用内置的 Server 启动了 Flask 服务,启动后日志中打印出来一条警告信息,告诉使用者不要在生产环境中使用内置的 Server。在生产环境中我们可以利用高性能 Python WSGI HTTP UNIX Server gunicorn 来启动 Flask 服务。 服务启动 (start.sh) 脚本代码如下: cd `dirname $0` cd .. base_dir=`pwd` tmp_dir=${base_dir}/tmp pid_file_path=${tmp_dir}/model-serving-demo.pid log_file_path=${tmp_dir}/model-serving-demo.log bind_host=0.0.0.0 bind_port=9999 workers=2 nohup gunicorn -b ${bind_host}:${bind_port} \ -w ${workers} -p ${pid_file_path} \ main:app > ${log_file_path} 2>&1 & 服务停止 (stop.sh) 脚本代码如下: cd `dirname $0` cd .. base_dir=`pwd` tmp_dir=${base_dir}/tmp pid_file_path=${tmp_dir}/model-serving-demo.pid kill -TERM `echo ${pid_file_path}` gunicorn 的详细参数配置和使用教程请参见 官方文档。 RESTful API 设计 RESTful API 是一种符合 REST(Representational State Transfer,表现层状态转换) 原则的框架,该框架是由 Fielding 在其博士论文 1 中提出。相关的核心概念如下: 资源 (Resources),即网络中的一个实体 (文本,图片,服务等),使用一个 URL 进行表示。 表现层 (Representation),资源具体的呈现形式即为表现层,例如图片可以表示为 PNG 文件,音乐可以表示为 MP3 文件,还有本文使用的数据格式 JSON 等。HTTP 请求的头信息中用 Accept 和 Content-Type 字段对表现层进行描述。 状态转换 (State Transfer),互联网通信协议 HTTP 协议是一个无状态协议,所有的状态都保存在服务端。因此如果客户端想要操作服务器,必须通过某种手段让服务器端发生 状态转换。客户端利用 HTTP 协议中的动作对服务器进行操作,例如:GET,POST,PUT,DELETE 等。 利用 RESTful API 构建模型服务时,需要注意如下几点: 为模型服务设置专用域名,例如:https://api.example.com,并配以负载均衡。 将 API 的版本号写入 URL 中,例如:https://api.example.com/v1。 RESTful 框架中每个 URL 表示一种资源,因此可以将模型的名称作为 URL 的终点 (Endpoint),例如:https://api.example.com/v1/XXXModel。 对于操作资源的 HTTP 方式有多种,综合考虑建议选用 POST 方式,同时建议使用 JSON 数据格式。 为请求响应设置合理的状态码,例如:200 OK 表示正常返回,400 INVALID REQUEST 表示无法处理客户端的错误请求等。 对于错误码为 4xx 的情况,建议在返回中添加键名为 message 的错误信息。 AJAX 调用 对于动态网页,我们可以很容易的在后端服务中发起 POST 请求调用模型服务,然后将结果在前端进行渲染。对于静态网页,我们可以利用 AJAX 进行相关操作,实现细节请参见 示例代码。 AJAX 服务请求代码的核心部分如下: $(document).ready(function() { $("#submit").click(function() { $.ajax({ url: "http://0.0.0.0:9999/v1/XXXModel", method: "POST", contentType: "application/json; charset=UTF-8", data: JSON.stringify({"name": $("#name").val()}), timeout: 3000, success: function (data, textStatus, jqXHR) { $("#result").html(data.result); }, error: function (jqXHR, textStatus, errorThrown) { $("#result").html(errorThrown); } }); }); }); 代码使用了 jQuery 的相关函数。JSON.stringify({"name": $("#name").val()}) 获取 ID 为 name 的元素的值,并将其转换成符合服务端要求的 JSON 格式。通过 AJAX 向远程发出请求后,如果请求成功则将返回数据 data 中对应的结果 result 填充到 ID 为 result 的元素中,否则填入返回的错误信息。 Google App Engine 部署 上文中已经介绍了如何在本地利用 Flask 部署模型服务和相关调用方法,但如果希望在自己的网站中调用时,则利用 SaaS 来部署符合会是一个不二之选。国内外多个厂商均提供了相应的 SaaS 产品,例如 Google,Amazon,Microsoft 等。Google App Engine (GAE) 提供了一个 始终免费 方案,虽然部署阶段会受到 GFW 的影响,但调用阶段测试影响并不是很大 (不同地区和服务提供商会有差异)。综合考虑,本文选择 GAE 作为 SaaS 平台部署服务,各位看官请自备梯子。 环境准备 首先,在 Google Cloud Platform Console 中建立一个新的 Project,假设项目名为 YOUR_PROJECT_ID。 其次,根据 Google Cloud SDK 文档 在本地安装相应版本的 Google Cloud SDK。MacOS 下建议通过 brew cask install google-cloud-sdk 方式安装,安装完毕后确认在命令行中可以运行 gcloud 命令。 $ gcloud version Google Cloud SDK 221.0.0 bq 2.0.35 core 2018.10.12 gsutil 4.34 构建 GAE 工程 模型服务仅作为后端应用,因此本节不介绍前端页面开发的相关部分,有兴趣的同学请参见 官方文档。GAE 部署 Python Web 应用采用了 WSGI 标准,我们构建的本地部署版本完全满足这个要求,因此仅需为项目在根目录添加一个 GAE 配置文件 app.yaml 即可,内容如下: runtime: python37 handlers: - url: /.* script: main.app skip_files: - .idea/ - .vscode/ - __pycache__/ - .hypothesis/ - .pytest_cache/ - bin/ - ^(.*/)?.*\.py[cod]$ - ^(.*/)?.*\$py\.class$ - ^(.*/)?.*\.log$ 其中,runtime 指定了服务运行的环境,handlers 指定了不同的 URL 对应的处理程序,在此所有的 URL 均由 main.py 中的 app 进行处理,skip_files 用于过滤不需要上传的文件。更多关于 app.yaml 的设置信息,请参见 官方文档。 部署 GAE 工程 在部署 GAE 工程之前我们可以利用本地的开发环境对其进行测试,测试无误后,即可运行如下命令将其部署到 GAE 上: gcloud app deploy --project [YOUR_PROJECT_ID] 然后根据命令行提示完成整个部署流程,部署完成的远程服务 URL 为 https://YOUR_PROJECT_ID.appspot.com,更多的测试和部署细节请参见 官方文档。 部署后的 GAE 服务使用了其自带的域名 appspot.com。如果你拥有自己的域名,可以根据官方文档 设置自己的域名 并 开启 SSL。 本文部分内容参考了 Genthial 的博客 Serving a model with Flask 和阮一峰的博客 理解RESTful架构 和 RESTful API 设计指南。 Fielding, Roy T., and Richard N. Taylor. Architectural styles and the design of network-based software architectures. Vol. 7. Doctoral dissertation: University of California, Irvine, 2000. ↩︎

2018/10/19
articleCard.readMore

序列到序列 (Seq2Seq) 和注意力机制 (Attention Machanism)

Encoder-Decoder & Seq2Seq Encoder-Decoder 是一种包含两个神经网络的模型,两个网络分别扮演编码器和解码器的角色。Cho 等人 1 提出了一个基于 RNN 的 Encoder-Decoder 神经网络用于机器翻译。网络结构如下图所示: 整个模型包含编码器 (Encoder) 和解码器 (Decoder) 两部分:Encoder 将一个可变长度的序列转换成为一个固定长度的向量表示,Decoder 再将这个固定长度的向量表示转换为一个可变长度的序列。这使得模型可以处理从一个可变长度序列到另一个可变长度序例的转换,即学习到对应的条件概率 $p \left(y_1, \dotsc, y_{T'} | x_1, \dotsc, x_T\right)$,其中 $T$ 和 $T'$ 可以为不同的值,也就是说输入和输出的序列的长度不一定相同。 在模型中,Encoder 为一个 RNN,逐次读入输入序列 $\mathbf{x}$ 中的每个元素,其中 RNN 隐状态的更新方式如下: $$ \mathbf{h}_{\langle t \rangle} = f \left(\mathbf{h}_{\langle t-1 \rangle}, x_t\right) $$ 在读入序列的最后一个元素后 (通常为一个结束标记),RNN 的隐状态则为整个输入序列的概括信息 $\mathbf{c}$。Decoder 为另一个 RNN,用于根据隐状态 $\mathbf{h}'_{\langle t \rangle}$ 预测下一个元素 $y_t$,从而生成整个输出序列。不同于 Encoder 中的 RNN,Decoder 中 RNN 的隐状态 $\mathbf{h}'_{\langle t \rangle}$ 除了依赖上一个隐含层的状态和之前的输出外,还依赖整个输入序列的概括信息 $\mathbf{c}$,即: $$ \mathbf{h}'_{\langle t \rangle} = f \left(\mathbf{h}'_{\langle t-1 \rangle}, y_{t-1}, \mathbf{c}\right) $$ 类似的,下一个输出元素的条件分布为: $$ P \left(y_t | y_{t-1}, y_{t-2}, \dotsc, y_1, \mathbf{c}\right) = g \left(\mathbf{h}_{\langle t \rangle}, y_{t-1}, \mathbf{c}\right) $$ RNN Encoder-Decoder 的两部分通过最大化如下的对数似然函数的联合训练进行优化: $$ \max_{\theta} \dfrac{1}{N} \sum_{n=1}^{N}{\log p_{\theta} \left(\mathbf{y}_n | \mathbf{x}_n\right)} $$ 其中,$\theta$ 为模型的参数,$\mathbf{x}_n$ 和 $\mathbf{y}_n$ 分别为输入和输出序列的成对样本。当模型训练完毕后,我们可以利用模型根据给定的输入序列生成相应的输出序列,或是根据给定的输入和输出序列对计算概率得分 $p_{\theta} \left(\mathbf{y} | \mathbf{x}\right)$。同时,作者还提出了一种新的 RNN 单元 GRU (Gated Recurrent Unit),有关 GRU 的更多介绍请参见 之前的博客。 序列到序列 (Sequence to Sequence, Seq2Seq) 模型从名称中不难看出来是一种用于处理序列数据到序列数据转换问题 (例如:机器翻译等) 的方法。Sutskever 等人 2 提出了一种基于 Encoder-Decoder 网络结构的 Seq2Seq 模型用于机器翻译,网络结构细节同 RNN Encoder-Decoder 略有不同,如下图所示: 模型的相关细节如下: 对数据进行预处理,在每个句子的结尾添加特殊字符 <EOS>,如上图所示。首先计算 A, B, C, <EOS> 的表示,再利用该表示计算 W, X, Y, Z, <EOS> 的条件概率。 利用两个不同的 LSTM,一个用于输入序列,另一个用于输出序列。 选用一个较深的 LSTM 模型 (4 层) 提升模型效果。 对输入序列进行倒置处理,例如对于输入序列 $a, b, c$ 和对应的输出序列 $\alpha, \beta, \gamma$,LSTM 需要学习的映射关系为 $c, b, a \to \alpha, \beta, \gamma$。 在模型的解码阶段,模型采用简单的从左到右的 Beam Search,该方法维护一个大小为 $B$ 的集合保存最好的结果。下图展示了 $B = 2$ 情况下 Beam Search 的具体工作方式: 其中,红色的虚线箭头表示每一步可能的搜索方向,绿色的实线箭头表示每一步概率为 Top $B$ 的方向。例如,从 S 开始搜索: 第一步搜索的可能结果为 SA 和 SB,保留 Top 2,结果为 SA 和 SB。 第二步搜索的可能结果为 SAC,SAD,SBE 和 SBF,保留 Top 2,结果为 SAC 和 SBE。 第三步搜索的可能结果为 SACG,SACH,SBEK 和 SBEL,保留 Top 2,结果为 SACH 和 SBEK。至此,整个搜索结束。 Bahdanau 等人 3 提出了一种基于双向 RNN (Bidirectional RNN, BiRNN) 结合注意力机制 (Attention Mechanism) 的网络结构用于机器翻译。网络结构如下: 模型的编码器使用了一个双向的 RNN,前向的 RNN $\overrightarrow{f}$ 以从 $x_1$ 到 $x_T$ 的顺序读取输入序列并计算前向隐状态 $\left(\overrightarrow{h}_1, \dotsc, \overrightarrow{h}_T\right)$,后向的 RNN $\overleftarrow{f}$ 以从 $x_T$ 到 $x_1$ 的顺序读取输入序列并计算后向隐状态 $\left(\overleftarrow{h}_1, \dotsc, \overleftarrow{h}_T\right)$。对于一个词 $x_j$,通过将对应的前向隐状态 $\overrightarrow{h}_j$ 和后向隐状态 $\overleftarrow{h}_j$ 进行拼接得到最终的隐状态 $h_j = \left[\overrightarrow{h}_j^{\top}; \overleftarrow{h}_j^{\top}\right]^{\top}$。这样的操作使得隐状态 $h_j$ 既包含了前面词的信息也包含了后面词的信息。 在模型的解码器中,对于一个给定的序例 $\mathbf{x}$,每一个输出的条件概率为: $$ p \left(y_i | y_1, \dotsc, y_{i-1}, \mathbf{x}\right) = g \left(y_{i-1}, s_i, c_i\right) $$ 其中,$s_i$ 为 $i$ 时刻 RNN 隐含层的状态,即: $$ s_i = f \left(s_{i-1}, y_{i-1}, c_i\right) $$ 这里需要注意的是不同于之前的 Encoder-Decoder 模型,此处每一个输出词 $y_i$ 的条件概率均依赖于一个单独的上下文向量 $c_i$。该部分的改进即结合了注意力机制,有关注意力机制的详细内容将在下个小节中展开说明。 注意力机制 (Attention Mechanism) Bahdanau 等人在文中 3 提出传统的 Encoder-Decoder 模型将输入序列压缩成一个固定长度的向量 $c$,但当输入的序例很长时,尤其是当比训练集中的语料还长时,模型的的效果会显著下降。针对这个问题,如上文所述,上下文向量 $c_i$ 依赖于 $\left(h_1, \dotsc, h_T\right)$。其中,每个 $h_i$ 都包含了整个序列的信息,同时又会更多地关注第 $i$ 个词附近的信息。对于 $c_i$,计算方式如下: $$ c_i = \sum_{j=1}^{T}{\alpha_{ij} h_j} $$ 对于每个 $h_j$ 的权重 $\alpha_{ij}$,计算方式如下: $$ \alpha_{ij} = \dfrac{\exp \left(e_{ij}\right)}{\sum_{k=1}^{T}{\exp \left(e_{ik}\right)}} $$ 其中,$e_{ij} = a \left(s_{i-1}, h_j\right)$ 为一个 Alignment 模型,用于评价对于输入的位置 $j$ 附近的信息与输出的位置 $i$ 附近的信息的匹配程度。Alignment 模型 $a$ 为一个用于评分的前馈神经网络,与整个模型进行联合训练,计算方式如下: $$ a \left(s_{i-1}, h_j\right) = v_a^{\top} \tanh \left(W_a s_{i-1} + U_a h_j\right) $$ 其中,$W_a \in \mathbb{R}^{n \times n}, U_a \in \mathbb{R}^{n \times 2n},v_a \in \mathbb{R}^n$ 为网络的参数。 Hard & Soft Attention Xu 等人 4 在图像标题生成 (Image Caption Generation) 任务中引入了注意力机制。在文中作者提出了 Hard Attenttion 和 Soft Attention 两种不同的注意力机制。 对于 Hard Attention 而言,令 $s_t$ 表示在生成第 $t$ 个词时所关注的位置变量,$s_{t, i} = 1$ 表示当第 $i$ 个位置用于提取视觉特征。将注意力位置视为一个中间潜变量,可以以一个参数为 $\left\{\alpha_i\right\}$ 的多项式分布表示,同时将上下文向量 $\hat{\mathbf{z}}_t$ 视为一个随机变量: $$ \begin{equation} \begin{split} & p \left(s_{t, i} = 1 | s_{j < t}, \mathbf{a}\right) = \alpha_{t, i} \\ & \hat{\mathbf{z}}_t = \sum_{i}{s_{t, i} \mathbf{a}_i} \end{split} \end{equation} $$ 因此 Hard Attention 可以依据概率值从隐状态中进行采样计算得到上下文向量,同时为了实现梯度的反向传播,需要利用蒙特卡罗采样的方法来估计梯度。 对于 Soft Attention 而言,则直接计算上下文向量 $\hat{\mathbf{z}}_t$ 的期望,计算方式如下: $$ \mathbb{E}_{p \left(s_t | a\right)} \left[\hat{\mathbf{z}}_t\right] = \sum_{i=1}^{L}{\alpha_{t, i} \mathbf{a}_i} $$ 其余部分的计算方式同 Bahdanau 等人 3 的论文类似。Soft Attention 模型可以利用标准的反向传播算法进行求解,直接嵌入到整个模型中一同训练,相对更加简单。 下图展示了一些图片标题生成结果的可视化示例,其中图片内 白色 为关注的区域,画线的文本 即为生成的标题中对应的词。 Global & Local Attention Luong 等人 5 提出了 Global Attention 和 Local Attention 两种不同的注意力机制用于机器翻译。Global Attention 的思想是在计算上下文向量 $c_t$ 时将编码器的所有隐状态均考虑在内。对于对齐向量 $\boldsymbol{a}_t$,通过比较当前目标的隐状态 $\boldsymbol{h}_t$ 与每一个输入的隐状态 $\bar{\boldsymbol{h}}_s$ 得到,即: $$ \begin{equation} \begin{split} \boldsymbol{a}_t &= \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \\ &= \dfrac{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right)\right)}{\sum_{s'}{\exp \left(\text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_{s'}\right)\right)}} \end{split} \end{equation} $$ 其中 $\text{score}$ 为一个基于内容 (content-based) 的函数,可选的考虑如下三种形式: $$ \text{score} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) = \begin{cases} \boldsymbol{h}_t^{\top} \bar{\boldsymbol{h}}_s & dot \\ \boldsymbol{h}_t^{\top} \boldsymbol{W}_a \bar{\boldsymbol{h}}_s & general \\ \boldsymbol{W}_a \left[\boldsymbol{h}_t; \bar{\boldsymbol{h}}_s\right] & concat \end{cases} $$ 我们利用一个基于位置 (location-based) 的函数构建注意力模型,其中对齐分数通过目标的隐状态计算得到: $$ \boldsymbol{a}_t = \text{softmax} \left(\boldsymbol{W}_a \boldsymbol{h}_t\right) $$ Global Attention 模型的网络结构如下所示: Global Attention 的一个问题在于任意一个输出都需要考虑输入端的所有隐状态,这对于很长的文本 (例如:一个段落或一篇文章) 计算量太大。Local Attention 为了解决这个问题,首先在 $t$ 时刻对于每个目标词生成一个对齐位置 $p_t$,其次上下文向量 $\boldsymbol{c}_t$ 则由以 $p_t$ 为中心前后各 $D$ 大小的窗口 $\left[p_t - D, p_t + D\right]$ 内的输入的隐状态计算得到。不同于 Global Attention,Local Attention 的对齐向量 $\boldsymbol{a}_t \in \mathbb{R}^{2D + 1}$ 为固定维度。 一个比较简单的做法是令 $p_t = t$,也就是假设输入和输出序列是差不多是单调对齐的,我们称这种做法为 Monotonic Alignment (local-m)。另一种做法是预测 $p_t$,即: $$ p_t = S \cdot \text{sigmoid} \left(\boldsymbol{v}_p^{\top} \tanh \left(\boldsymbol{W}_p \boldsymbol{h}_t\right)\right) $$ 其中,$\boldsymbol{W}_p$ 和 $\boldsymbol{h}_t$ 为预测位置模型的参数,$S$ 为输入句子的长度。我们称这种做法为 Predictive Alignment (local-p)。作为 $\text{sigmoid}$ 函数的结果,$p_t \in \left[0, S\right]$,则通过一个以 $p_t$ 为中心的高斯分布定义对齐权重: $$ \boldsymbol{a}_t \left(s\right) = \text{align} \left(\boldsymbol{h}_t, \bar{\boldsymbol{h}}_s\right) \exp \left(- \dfrac{\left(s - p_t\right)^2}{2 \sigma^2}\right) $$ 其中,根据经验设置 $\sigma = \dfrac{D}{2}$,$s$ 为在窗口大小内的一个整数。 Local Attention 模型的网络结构如下所示: Self Attention Vaswani 等人 6 提出了一种新的网络结构,称之为 Transformer,其中采用了自注意力 (Self-attention) 机制。自注意力是一种将同一个序列的不同位置进行自我关联从而计算一个句子表示的机制。Transformer 利用堆叠的 Self Attention 和全链接网络构建编码器 (下图左) 和解码器 (下图右),整个网络架构如下图所示: 编码器和解码器 编码器 是由 $N = 6$ 个相同的网络层构成,每层中包含两个子层。第一层为一个 Multi-Head Self-Attention 层,第二层为一个 Position-Wise 全链接的前馈神经网络。每一层再应用一个残差连接 (Residual Connection) 7 和一个层标准化 (Layer Normalization) 8。则每一层的输出为 $\text{LayerNorm} \left(x + \text{Sublayer} \left(x\right)\right)$,其中 $\text{Sublayer} \left(x\right)$ 为子层本身的函数实现。为了实现残差连接,模型中所有的子层包括 Embedding 层的输出维度均为 $d_{\text{model}} = 512$。 解码器 也是由 $N = 6$ 个相同的网络层构成,但每层中包含三个子层,增加的第三层用于处理编码器的输出。同编码器一样,每一层应用一个残差连接和一个层标准化。除此之外,解码器对 Self-Attention 层进行了修改,确保对于位置 $i$ 的预测仅依赖于位置在 $i$ 之前的输出。 Scaled Dot-Product & Multi-Head Attention 一个 Attention 函数可以理解为从一个序列 (Query) 和一个键值对集合 (Key-Value Pairs Set) 到一个输出的映射。文中提出了一种名为 Scaled Dot-Product Attention (如下图所示),其中输入包括 queries,维度为 $d_k$ 的 keys 和维度为 $d_v$ 的 values。通过计算 queries 和所有 keys 的点积,除以 $\sqrt{d_k}$,再应用一个 softmax 函数获取 values 的权重。 实际中,我们会同时计算一个 Queries 集合中的 Attention,并将其整合成一个矩阵 $Q$。Keys 和 Values 也相应的整合成矩阵 $K$ 和 $V$,则有: $$ \text{Attention} \left(Q, K, V\right) = \text{softmax} \left(\dfrac{Q K^{\top}}{\sqrt{d_k}}\right) V $$ 其中,$Q \in \mathbb{R}^{n \times d_k}$,$Q$ 中的每一行为一个 query,$K \in \mathbb{R}^{n \times d_k}, V \in \mathbb{R}^{n \times d_v}$。$\dfrac{1}{\sqrt{d_k}}$ 为一个归一化因子,避免点积的值过大导致 softmax 之后的梯度过小。 Multi-Head Attention 的做法并不直接对原始的 keys,values 和 queries 应用注意力函数,而是学习一个三者各自的映射再应用 Atteneion,同时将这个过程重复 $h$ 次。Multi-Head Attention 的网路结构如下图所示: Multi-Head Attention 的计算过程如下所示: $$ \begin{equation} \begin{split} \text{MultiHead} \left(Q, K, V\right) &= \text{Concat} \left(\text{head}_1, \dotsc, \text{head}_h\right) W^O \\ \textbf{where } \text{head}_i &= \text{Attention} \left(QW_i^Q, KW_i^K, VW_i^V\right) \end{split} \end{equation} $$ 其中,$W_i^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}, W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}, W_i^O \in \mathbb{R}^{h d_v \times d_{\text{model}}}, $ 为映射的参数,$h = 8$ 为重复的次数,则有 $d_k = d_v = d_{\text{model}} / h = 64$。 整个 Transformer 模型在三处使用了 Multi-Head Attention,分别是: Encoder-Decoder Attention Layers,其中 queries 来自于之前的 Decoder 层,keys 和 values 来自于 Encoder 的输出,该部分同其他 Seq2Seq 模型的 Attention 机制类似。 Encoder Self-Attention Layers,其中 queries,keys 和 values 均来自之前的 Encoder 层的输出,同时 Encoder 层中的每个位置都能够从之前层的所有位置获取到信息。 Decoder Self-Attention Layers,其中 queries,keys 和 values 均来自之前的 Decoder 层的输出,但 Decoder 层中的每个位置仅可以从之前网络层的包含当前位置之前的位置获取信息。 Position-wise Feed-Forward Networks 在 Encoder 和 Decoder 中的每一层均包含一个全链接的前馈神经网络,其使用两层线性变换和一个 ReLU 激活函数实现: $$ \text{FFN} \left(x\right) = \max \left(0, x W_1 + b_1\right) W_2 + b_2 $$ 全链接层的输入和输出的维度 $d_{\text{model}} = 512$,内层的维度 $d_{ff} = 2048$。 Positional Encoding Transformer 模型由于未使用任何循环和卷积组件,因此为了利用序列的位置信息则在模型的 Embedding 输入中添加了 Position Encoding。Position Encoding 的维度同 Embedding 的维度相同,从而可以与 Embedding 进行加和,文中使用了如下两种形式: $$ \begin{equation} \begin{split} PE_{\left(pos, 2i\right)} &= \sin \left(pos / 10000^{2i / d_{\text{model}}}\right) \\ PE_{\left(pos, 2i+1\right)} &= \cos \left(pos / 10000^{2i / d_{\text{model}}}\right) \end{split} \end{equation} $$ 其中,$pos$ 为位置,$i$ 为对应的维度,选用这种表示形式的原因是对于一个固定的偏移 $k$,$PE_{pos + k}$ 都可以利用 $PE_{pos}$ 线性表示。这是因为对于正弦和余弦函数有: $$ \begin{equation} \begin{split} \sin \left(\alpha + \beta\right) &= \sin \alpha \cos \beta + \cos \alpha \sin \beta \\ \cos \left(\alpha + \beta\right) &= \cos \alpha \sin \beta - \sin \alpha \sin \beta \end{split} \end{equation} $$ Why Self-Attention 相比于循环和卷积层,Transformer 模型利用 Self-Attention 层用于一个序列 $\left(x_1, \dotsc, x_n\right)$ 到另一个等长序例 $\left(z_1, \dotsc, z_n\right)$ 的映射,其中 $x_i, z_i \in \mathbb{R}^d$。Self-Attention 与循环和卷积的对比如下表所示: 层类型 每层的复杂度 序列操作数 长距离依赖路径长度 Self-Attention $O \left(n^2 \cdot d\right)$ $O \left(1\right)$ $O \left(1\right)$ Recurrent $O \left(n \cdot d^2\right)$ $O \left(n\right)$ $O \left(n\right)$ Convolutional $O \left(k \cdot n \cdot d^2\right)$ $O \left(1\right)$ $O \left(\log_k \left(n\right)\right)$ Self-Attention (restricted) $O \left(r \cdot n \cdot d\right)$ $O \left(1\right)$ $O \left(n/r\right)$ 对于每层的复杂度,当序例的长度 $n$ 比表示的维度 $d$ 小时,Self-Attention 要比循环结构计算复杂度小。为了改进在长序列上 Self-Attention 的计算性能,Self-Attention 可以被限制成仅考虑与输出位置对应的输入序列位置附近 $r$ 窗口大小内的信息。 Recurrent 层的最小序列操作数为 $O \left(n\right)$,其他情况为 $O \left(1\right)$,这使得 Recurrent 的并行能力较差,即上表中的 Self-Attention (restricted)。 学习到长距离依赖是很多序列任务的关键,影响该能力的一个重要因素就是前向和后向信号穿越整个网络的路径长度,这个路径长度越短,越容易学习到长距离依赖。 Attention Visualizations 第一张图展示了 Self-Attention 学到的句子内部的一个长距离依赖 “making … more diffcult”,图中不同的颜色表示不同 Head 的 Attention,颜色越深表示 Attention 的值越大。 第二张图展示了 Self-Attention 学到的一个指代消解关系 (Anaphora Resolution),its 指代的为上文中的 law。下图 (上) 为 Head 5 的所有 Attention,下图 (下) 为 Head 5 和 6 关于词 its 的 Attention,不难看出模型学习到了 its 和 law 之间的依赖关系 (指代消解关系)。 Hierarchical Attention Yang 等人 9 提出了一种层级的注意力 (Hierarchical Attention) 网络用于文档分类。Hierarchical Attention 共包含 4 层:一个词编码器 (Word Encoder),一个词级别的注意力层 (Word Attention),一个句子编码器 (Sentence Encoder) 和一个句子级别的注意力层 (Sentence Attention)。网络架构如下图所示: Word Encoder 对于一个给定的句子 $w_{it}, t \in \left[0, T\right]$,通过一个 Embedding 矩阵 $W_e$ 得到每个词的向量表示,再应用一个双向的 GRU,即: $$ \begin{equation} \begin{split} x_{it} &= W_e w_{it}, t \in \left[1, T\right] \\ \overrightarrow{h}_{it} &= \overrightarrow{\text{GRU}} \left(x_{it}\right), t \in \left[1, T\right] \\ \overleftarrow{h}_{it} &= \overleftarrow{\text{GRU}} \left(x_{it}\right), t \in \left[T, 1\right] \end{split} \end{equation} $$ 最后将前向的隐状态 $\overrightarrow{h}_{it}$ 和后向的隐状态 $\overleftarrow{h}_{it}$ 进行拼接,得到 $h_{ij} = \left[\overrightarrow{h}_{it}, \overleftarrow{h}_{it}\right]$ 为整个句子在词 $w_{ij}$ 附近的汇总信息。 Word Attention Word Attention 同一般的 Attention 机制类似,计算方式如下: $$ \begin{equation} \begin{split} u_{it} &= \tanh \left(W_w h_{it} + b_w\right) \\ a_{it} &= \dfrac{\exp \left(u_{it}^{\top} u_w\right)}{\sum_{t}{\exp \left(u_{it}^{\top} u_w\right)}} \\ s_i &= \sum_{t}{a_{it} h_{it}} \end{split} \end{equation} $$ Sentence Encoder 在 Word Attention 之后,我们得到了一个句子的表示 $s_i$,类似的我们利用一个双向的 GRU 编码文档中的 $L$ 个句子: $$ \begin{equation} \begin{split} \overrightarrow{h}_i &= \overrightarrow{\text{GRU}} \left(s_i\right), i \in \left[1, L\right] \\ \overleftarrow{h}_i &= \overleftarrow{\text{GRU}} \left(s_i\right), i \in \left[L, 1\right] \end{split} \end{equation} $$ 最后将前向的隐状态 $\overrightarrow{h}_i$ 和后向的隐状态 $\overleftarrow{h}_i$ 进行拼接,得到 $h_i = \left[\overrightarrow{h}_i, \overleftarrow{h}_i\right]$ 为整个文档关于句子 $s_i$ 的注意力汇总信息。 Sentence Attention 同理可得 Sentence Attention 的计算方式如下: $$ \begin{equation} \begin{split} u_i &= \tanh \left(W_s h_i + b_s\right) \\ a_i &= \dfrac{\exp \left(u_i^{\top} u_s\right)}{\sum_{i}{\exp \left(u_i^{\top} u_s\right)}} \\ v &= \sum_{i}{a_i h_i} \end{split} \end{equation} $$ 最终得到整个文档的向量表示 $v$。 Attention-over-Attention Cui 等人 10 提出了 Attention-over-Attention 的模型用于阅读理解 (Reading Comprehension)。网络结构如下图所示: 对于一个给定的训练集 $\langle \mathcal{D}, \mathcal{Q}, \mathcal{A} \rangle$,模型包含两个输入,一个文档 (Document) 和一个问题序列 (Query)。网络的工作流程如下: 先获取 Document 和 Query 的 Embedding 结果,再应用一个双向的 GRU 得到对应的隐状态 $h_{doc}$ 和 $h_{query}$。 计算一个 Document 和 Query 的匹配程度矩阵 $M \in \mathbb{R}^{\lvert \mathcal{D} \rvert \times \lvert \mathcal{Q} \rvert}$,其中第 $i$ 行第 $j$ 列的值计算方式如下: $$ M \left(i, j\right) = h_{doc} \left(i\right)^{\top} \cdot h_{query} \left(j\right) $$ 按照 列 的方向对矩阵 $M$ 应用 softmax 函数,矩阵中的每一列为考虑一个 Query 中的词的 Document 级别的 Attention,因此定义 $\alpha \left(t\right) \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$ 为 $t$ 时刻的 Document 级别 Attention (query-to-document attention)。计算方式如下: $$ \begin{equation} \begin{split} \alpha \left(t\right) &= \text{softmax} \left(M \left(1, t\right), \dotsc, M \left(\lvert \mathcal{D} \rvert, t\right)\right) \\ \alpha &= \left[\alpha \left(1\right), \alpha \left(2\right), \dotsc, \alpha \left(\lvert \mathcal{Q} \rvert\right)\right] \end{split} \end{equation} $$ 同理按照 行 的方向对矩阵 $M$ 应用 softmax 函数,可以得到 $\beta \left(t\right) \in \mathbb{R}^{\lvert \mathcal{Q} \rvert}$ 为 $t$ 时刻的 Query 级别的 Attention (document-to-query attention)。计算方式如下: $$ \beta \left(t\right) = \text{softmax} \left(M \left(t, 1\right), \dotsc, M \left(t, \lvert \mathcal{Q} \rvert\right)\right) $$ 对于 document-to-query attention,我们对结果进行平均得到: $$ \beta = \dfrac{1}{n} \sum_{t=1}^{\lvert \mathcal{D} \rvert}{\beta \left(t\right)} $$ 最终利用 $\alpha$ 和 $\beta$ 的点积 $s = \alpha^{\top} \beta \in \mathbb{R}^{\lvert \mathcal{D} \rvert}$ 得到 attended document-level attention (即 attention-over-attention)。 Multi-step Attention Gehring 等人 11 提出了基于 CNN 和 Multi-step Attention 的模型用于机器翻译。网络结构如下图所示: Position Embeddings 模型首先得到序列 $\mathbf{x} = \left(x_1, \dotsc, x_m\right)$ 的 Embedding $\mathbf{w} = \left(w_1, \dotsc , w_m\right), w_j \in \mathbb{R}^f$。除此之外还将输入序列的位置信息映射为 $\mathbf{p} = \left(p_1, \dotsc, p_m\right), p_j \in \mathbb{R}^f$,最终将两者进行合并得到最终的输入 $\mathbf{e} = \left(w_1 + p_1, \dotsc, w_m + p_m\right)$。同时在解码器部分也采用类似的操作,将其与解码器网络的输出表示合并之后再喂入解码器网络 $\mathbf{g} = \left(g_1, \dotsc, g_n\right)$ 中。 Convolutional Block Structure 编码器和解码器均由多个 Convolutional Block 构成,每个 Block 包含一个卷积计算和一个非线性计算。令 $\mathbf{h}^l = \left(h_1^l, \dotsc, h_n^l\right)$ 表示解码器第 $l$ 个 Block 的输出,$\mathbf{z}^l = \left(z_1^l, \dotsc, z_m^l\right)$ 表示编码器第 $l$ 个 Block 的输出。对于一个大小 $k = 5$ 的卷积核,其结果的隐状态包含了这 5 个输入,则对于一个 6 层的堆叠结构,结果的隐状态则包含了输入中的 25 个元素。 在每一个 Convolutional Block 中,卷积核的参数为 $W \in \mathbb{R}^{2d \times kd}, b_w \in \mathbb{R}^{2d}$,其中 $k$ 为卷积核的大小,经过卷积后的输出为 $Y \in \mathbb{R}^{2d}$。之后的非线性层采用了 Dauphin 等人 12 提出的 Gated Linear Units (GLU),对于卷积后的输出 $Y = \left[A, B\right]$ 有: $$ v \left(\left[A, B\right]\right) = A \otimes \sigma \left(B\right) $$ 其中,$A, B \in \mathbb{R}^d$ 为非线性单元的输入,$\otimes$ 为逐元素相乘,$\sigma \left(B\right)$ 为用于控制输入 $A$ 与当前上下文相关度的门结构。 模型中还加入了残差连接,即: $$ h_i^l = v \left(W^l \left[h_{i-k/2}^{l-1}, \dotsc, h_{i+k/2}^{l-1}\right] + b_w^l\right) + h_i^{l-1} $$ 为了确保网络卷积层的输出同输入的长度相匹配,模型对输入数据的前后填补 $k - 1$ 个零值,同时为了避免解码器使用当前预测位置之后的信息,模型删除了卷积输出尾部的 $k$ 个元素。在将 Embedding 喂给编码器网络之前,在解码器输出应用 softmax 之前以及所有解码器层计算 Attention 分数之前,建立了一个从 Embedding 维度 $f$ 到卷积输出大小 $2d$ 的线性映射。最终,预测下一个词的概率计算方式如下: $$ p \left(y_{i+1} | y_1, \dotsc, y_i, \mathbf{x}\right) = \text{softmax} \left(W_o h_i^L + b_o\right) \in \mathbb{R}^T $$ Multi-step Attention 模型的解码器网络中引入了一个分离的注意力机制,在计算 Attention 时,将解码器当前的隐状态 $h_i^l$ 同之前输出元素的 Embedding 进行合并: $$ d_i^l = W_d^l h_i^l + b_d^l + g_i $$ 对于解码器网络层 $l$ 中状态 $i$ 和输入元素 $j$ 之间的的 Attention $a_{ij}^l$ 通过解码器汇总状态 $d_i^l$ 和最后一个解码器 Block $u$ 的输出 $z_j^u$ 进行点积运算得到: $$ a_{ij}^l = \dfrac{\exp \left(d_i^l \cdot z_j^u\right)}{\sum_{t=1}^{m}{\exp \left(d_i^l \cdot z_t^u\right)}} $$ 条件输入 $c_i^l$ 的计算方式如下: $$ c_i^l = \sum_{j=1}^{m}{a_{ij}^l \left(z_j^u + e_j\right)} $$ 其中,$e_j$ 为输入元素的 Embedding。与传统的 Attention 不同,$e_j$ 的加入提供了一个有助于预测的具体输入元素信息。 最终将 $c_i^l$ 加到对应的解码器层的输出 $h_i^l$。这个过程与传统的单步 Attention 不同,被称之为 Multiple Hops 13。这种方式使得模型在计算 Attention 时会考虑之前已经注意过的输入信息。 Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1724–1734). ↩︎ Sutskever, I., Vinyals, O., & Le, Q. V. (2014). Sequence to Sequence Learning with Neural Networks. In Z. Ghahramani, M. Welling, C. Cortes, N. D. Lawrence, & K. Q. Weinberger (Eds.), Advances in Neural Information Processing Systems 27 (pp. 3104–3112). ↩︎ Bahdanau, D., Cho, K., & Bengio, Y. (2014). Neural Machine Translation by Jointly Learning to Align and Translate. arXiv preprint arXiv:1409.0473 ↩︎ ↩︎ ↩︎ Xu, K., Ba, J., Kiros, R., Cho, K., Courville, A., Salakhudinov, R., … Bengio, Y. (2015). Show, Attend and Tell: Neural Image Caption Generation with Visual Attention. In International Conference on Machine Learning (pp. 2048–2057). ↩︎ Luong, T., Pham, H., & Manning, C. D. (2015). Effective Approaches to Attention-based Neural Machine Translation. In Proceedings of the 2015 Conference on Empirical Methods in Natural Language Processing (pp. 1412–1421). ↩︎ Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … Polosukhin, I. (2017). Attention is All you Need. In I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 30 (pp. 5998–6008). ↩︎ He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep Residual Learning for Image Recognition. In 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) (pp. 770–778). ↩︎ Ba, J. L., Kiros, J. R., & Hinton, G. E. (2016). Layer Normalization. arXiv preprint arXiv:1607.06450 ↩︎ Yang, Z., Yang, D., Dyer, C., He, X., Smola, A., & Hovy, E. (2016). Hierarchical Attention Networks for Document Classification. In Proceedings of the 2016 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (pp. 1480–1489). ↩︎ Cui, Y., Chen, Z., Wei, S., Wang, S., Liu, T., & Hu, G. (2017). Attention-over-Attention Neural Networks for Reading Comprehension. In Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (pp. 593–602). ↩︎ Gehring, J., Auli, M., Grangier, D., Yarats, D., & Dauphin, Y. N. (2017). Convolutional Sequence to Sequence Learning. In International Conference on Machine Learning (pp. 1243–1252). ↩︎ Dauphin, Y. N., Fan, A., Auli, M., & Grangier, D. (2016). Language Modeling with Gated Convolutional Networks. arXiv preprint arXiv:1612.08083 ↩︎ Sukhbaatar, S., szlam, arthur, Weston, J., & Fergus, R. (2015). End-To-End Memory Networks. In C. Cortes, N. D. Lawrence, D. D. Lee, M. Sugiyama, & R. Garnett (Eds.), Advances in Neural Information Processing Systems 28 (pp. 2440–2448). ↩︎

2018/10/12
articleCard.readMore

词向量 (Word Embeddings)

文本表示 文本表示是计算机处理自然语言的核心,我们希望计算机能够同人类一样对自然语言能够实现语义层面的理解,但这并非易事。在中文和拉丁语系中,文本的直观表示就存在一定的差异,拉丁语系中词与词之间存在天然的分隔符,而中文则没有。 I can eat glass, it doesn’t hurt me. 所以,在处理中文之前我们往往需要对原始文本进行分词,在此我们不谈这部分工作,假设我们已经得到了分词完的文本,即我们后续需要处理的“词”。早期的词表示方法多采用独热编码 (One-Hot Encoding),对于每一个不同的词都使用一个单独的向量进行表示。对于一个包含 $n$ 个词的语料而言,一个词的向量表示 $\text{word}_i \in \left\{0, 1\right\}^n$ 仅在第 $i$ 的位置值为 1,其他位置的值均为 0。例如,我们可以将“父亲”表示为: $$ \left[1, 0, 0, 0, 0, 0, ...\right] \nonumber $$ One-Hot Encoding 的表示方法十分简洁,但也存在着一些问题。 维数灾难 (The Curse of Dimensionality) 在很多现实问题中,我们仅用少数的特征是很难利用一个线性模型将数据区分开来的,也就是线性不可分问题。一个有效的方法是利用核函数实现一个非线性变换,将非线性问题转化成线性问题,通过求解变换后的线性问题进而求解原来的非线性问题。 假设 $\mathcal{X}$ 是输入空间(欧式空间 $\mathbb{R}^n$ 的子集或离散结合),$\mathcal{H}$ 为特征空间(希尔伯特空间),若存在一个从 $\mathcal{X}$ 到 $ \mathcal{H}$ 的映射: $$\phi \left(x\right): \mathcal{X} \rightarrow \mathcal{H}$$ 使得对所有 $x, z \in \mathcal{X}$ ,函数 $K\left(x, z\right)$ 满足条件: $$K\left(x, z\right) = \phi \left(x\right) \cdot \phi \left(z\right)$$ 则 $K\left(x, z\right)$ 为核函数, $\phi \left(x\right)$ 为映射函数,其中 $\phi \left(x\right) \cdot \phi \left(z\right)$ 为 $\phi \left(x\right)$ 和 $\phi \left(z\right)$ 的内积。 例如,对于一个下图所示的二维数据,显然是线性不可分的。 构建一个映射 $\phi: \mathbb{R}^2 \rightarrow \mathbb{R}^3$ 经 $X$ 映射为: $x = x^2, y = y^2, z = y$ ,则通过变换后的数据通过可视化可以明显地看出,数据是可以通过一个超平面来分开的。 可以说随着维度的增加,我们更有可能找到一个超平面(线性模型)将数据划分开来。尽管看起来,随着维度的增加似乎有助于我们构建模型,但是同时数据在高维空间的分布变得越来越稀疏。因此,在构建机器学习模型时,当我们需要更好的覆盖数据的分布时,我们需要的数据量就更大,这也就会导致需要更多的时间去训练模型。例如,假设所有特征均为0到1之间连续分布的数据,针对1维的情况,当覆盖50%的数据时,仅需全体50%的样本即可;针对2维的情况,当覆盖50%的数据时,则需全体71% ( $0.71^2 \approx 0.5$ ) 的样本;针对3维的情况,当覆盖50%的数据时,则需全体79% ( $0.79^3 \approx 0.5$ ),这就是我们所说的维数灾难。 分散式表示 (Distributed Representations) 分散式表示(Distributed Representations)1 最早由 Hiton 提出,对比于传统的 One-Hot Representation ,Distributed Representations 可以将数据表示为低维,稠密,连续的向量,也就是说将原始空间中的潜在信息分散的表示在低维空间的不同维度上。 传统的 One-Hot Representation 会将数据表示成一个很长的向量,例如,在 NLP 中,利用 One-Hot Representation 表示一个单词: 父亲: [1, 0, 0, 0, 0, 0, ...] 爸爸: [0, 1, 0, 0, 0, 0, ...] 母亲: [0, 0, 1, 0, 0, 0, ...] 妈妈: [0, 0, 0, 1, 0, 0, ...] 这种表示形式很简介,但也很稀疏,相当于语料库中有多少个词,则表示空间的维度就需要多少。那么,对于传统的聚类算法,高斯混合模型,最邻近算法,决策树或高斯 SVM 需要 $O\left(N\right)$ 个参数 (或 $O\left(N\right)$ 个样本) 将能够将 $O\left(N\right)$ 的输入区分开来。而像 RBMs ,稀疏编码,Auto-Encoder 或多层神经网络则可以利用 $O\left(N\right)$ 个参数表示 $O\left(2^k\right)$ 的输入,其中 $k \leq N$ 为稀疏表示中非零元素的个数 2。 采用 Distributed Representation,则可以将单词表示为: 父亲: [0.12, 0.34, 0.65, ...] 爸爸: [0.11, 0.33, 0.58, ...] 母亲: [0.34, 0.98, 0.67, ...] 妈妈: [0.29, 0.92, 0.66, ...] 利用这种表示,我们不仅可以将稀疏的高维空间转换为稠密的低维空间,同时我们还能学习出文本间的语义相似性来,例如实例中的 父亲 和 爸爸,从语义上看其均表示 父亲 的含义,但是如果利用 One-Hot Representation 编码则 父亲 与 爸爸 的距离同其与 母亲 或 妈妈 的距离时相同的,而利用 Distributed Representation 编码,则 父亲 同 爸爸 之间的距离要远小于其同 母亲 或 妈妈 之间的距离。 Word Embedding 之路 N-gram 模型 N-gram (N 元语法) 是一种文本表示方法,指文中连续出现的 $n$ 个词语。N-gram 模型是基于 $n-1$ 阶马尔科夫链的一种概率语言模型,可以通过前 $n-1$ 个词对第 $n$ 个词进行预测。Bengio 等人 3 提出了一个三层的神经网络的概率语言模型,其网络结构如下图所示: 模型的最下面为前 $n-1$ 个词 $w_{t-n+1}, ..., w_{t-2}, w_{t-1}$,每个词 $w_i$ 通过查表的方式同输入层对应的词向量 $C \left(w_i\right)$ 相连。词表 $C$ 为一个 $\lvert V\rvert \times m$ 大小的矩阵,其中 $\lvert V\rvert$ 表示语料中词的数量,$m$ 表示词向量的维度。输入层则为前 $n-1$ 个词向量拼接成的向量 $x$,其维度为 $m \left(n-1\right) \times 1$。隐含层直接利用 $d + Hx$ 计算得到,其中 $H$ 为隐含层的权重,$d$ 为隐含层的偏置。输出层共包含 $\lvert V\rvert$ 个神经元,每个神经元 $y_i$ 表示下一个词为第 $i$ 个词的未归一化的 log 概率,即: $$ y = b + Wx + U \tanh \left(d + Hx\right) $$ 对于该问题,我们的优化目标为最大化如下的 log 似然函数: $$ L = \dfrac{1}{T} \sum_{t}{f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right) + R \left(\theta\right)} $$ 其中,$f \left(w_t, w_{t-1}, ..., w_{t-n+1}\right)$ 为利用前 $n-1$ 个词预测当前词 $w_t$ 的条件概率,$R \left(\theta\right)$ 为参数的正则项,$\theta = \left(b, d, W, U, H, C\right)$。$C$ 作为模型的参数之一,随着模型的训练不断优化,在模型训练完毕后,$C$ 中保存的即为词向量。 Continuous Bag-of-Words (CBOW) 和 Skip-gram 模型 CBOW 和 Skip-gram 均考虑一个词的上下文信息,两种模型的结构如下图所示: 两者在给定的上下文信息中 (即前后各 $m$ 个词) 忽略了上下文环境的序列信息,CBOW (上图左) 是利用上下文环境中的词预测当前的词,而 Skip-gram (上图右) 则是用当前词预测上下文中的词。 对于 CBOW,$x_{1k}, x_{2k}, ..., x_{Ck}$ 为上下文词的 One-Hot 表示,$\mathbf{W}_{V \times N}$ 为所有词向量构成的矩阵 (词汇表),$y_j$ 为利用上下文信息预测得到的当前词的 One-Hot 表示输出,其中 $C$ 为上下文词汇的数量,$V$ 为词汇表中词的总数量,$N$ 为词向量的维度。从输入层到隐含层,我们对输入层词对应的词向量进行简单的加和,即: $$ h_i = \sum_{c=1}^{C}{x_{ck} \mathbf{W}_{V \times N}} $$ 对于 Skip-gram,$x_k$ 为当前词的 One-Hot 表示,$\mathbf{W}_{V \times N}$ 为所有词向量构成的矩阵 (词汇表),$y_{1j}, y_{2j}, ..., y_{Cj}$ 为预测的上次文词汇的 One-Hot 表示输出。从输入层到隐含层,直接将 One-Hot 的输入向量转换为词向量表示即可。 除此之外两者还有一些其他的区别: CBOW 要比 Skip-gram 模型训练快。从模型中我们不难发现:从隐含层到输出层,CBOW 仅需要计算一个损失,而 Skip-gram 则需要计算 $C$ 个损失再进行平均进行参数优化。 Skip-gram 在小数量的数据集上效果更好,同时对于生僻词的表示效果更好。CBOW 在从输入层到隐含层时,对输入的词向量进行了平均 (可以理解为进行了平滑处理),因此对于生僻词,平滑后则容易被模型所忽视。 Word2Vec Mikolov 等人 4 利用上面介绍的 CBOW 和 Skip-gram 两种模型提出了经典的 Word2Vec 算法。Word2Vec 中针对 CBOW 和 Skip-gram 又提出了两种具体的实现方案 Hierarchical Softmax (层次 Softmax) 和 Negative Sampling (负采样),因此共有 4 种不同的模型。 基于 Hierarchical Softmax 的模型 基于 Hierarchical Softmax 的 CBOW 模型如下: 其中: 输入层:包含了 $C$ 个词的词向量,$\mathbf{v} \left(w_1\right), \mathbf{v} \left(w_2\right), ..., \mathbf{v} \left(w_C\right) \in \mathbb{R}^N$,$N$ 为词向量的维度。 投影层:将输入层的向量进行加和,即:$\mathbf{x}_w = \sum_{i=1}^{C}{\mathbf{v} \left(w_i\right)} \in \mathbb{R}^N$。 输出层:输出为一颗二叉树,是根据语料构建出来的 Huffman 树 5,其中每个叶子节点为词汇表中的一个词。 Hierarchical Softmax 是解决概率语言模型中计算效率的关键,CBOW 模型去掉了隐含层,同时将输出层改为了 Huffman 树。对于该模型的优化求解,我们首先引入一些符号,对于 Huffman 树的一个叶子节点 (即词汇表中的词 $w$),记: $p^w$:从根节点出发到达 $w$ 对应的叶子节点的路径。 $l^w$:路径 $p^w$ 包含的节点的个数。 $p_1^w, p_1^w, ..., p_{l^w}^w$:路径 $p^w$ 中的 $l^w$ 个节点,其中 $p_1^w$ 表示根节点,$p_{l^w}^w$ 表示词 $w$ 对应的叶子节点。 $d_2^w, d_3^w, ..., d_{l^w}^w \in \{0, 1\}$:词 $w$ 的 Huffman 编码,由 $l^w - 1$ 位编码构成,$d_j^w$ 表示路径 $p^w$ 中第 $j$ 个结点对应的编码。 $\theta_1^w, \theta_1^w, ..., \theta_{l^w - 1}^w \in \mathbb{R}^N$:路径 $p^w$ 中非叶子节点对应的向量,$\theta_j^w$ 表示路径 $p^w$ 中第 $j$ 个非叶子节点对应的向量。 首先我们需要根据向量 $\mathbf{x}_w$ 和 Huffman 树定义条件概率 $p \left(w | Context\left(w\right)\right)$。我们可以将其视为一系列的二分类问题,在到达对应的叶子节点的过程中,经过的每一个非叶子节点均为对应一个取值为 0 或 1 的 Huffman 编码。因此,我们可以将编码为 1 的节点定义为负类,将编码为 0 的节点定义为正类 (即分到左边为负类,分到右边为正类),则这条路径上对应的标签为: $$ Label \left(p_i^w\right) = 1 - d_i^w, i = 2, 3, ..., l^w $$ 则对于一个节点被分为正类的概率为 $\sigma \left(\mathbf{x}_w^{\top} \theta\right)$,被分为负类的概率为 $1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right)$。则条件概率可以表示为: $$ p \left(w | Context\left(w\right)\right) = \prod_{j=2}^{l^w}{p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right)} $$ 其中 $$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \begin{cases} \sigma \left(\mathbf{x}_w^{\top} \theta\right) & d_j^w = 0 \\ 1 - \sigma \left(\mathbf{x}_w^{\top} \theta\right) & d_j^w = 1 \end{cases} $$ 或表示为: $$ p \left(d_j^w | \mathbf{x}_w, \theta_{j-1}^w\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w} $$ 则对数似然函数为: $$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log \prod_{j=2}^{l^w}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{1 - d_j^w} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}\right)\right]^{d_j^w}\right\}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{j=2}^{l^w}{\left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\}}} \end{split} \end{equation} $$ 记上式花括号中的内容为 $\mathcal{L} \left(w, j\right)$,则 $\mathcal{L} \left(w, j\right)$ 关于 $\theta_{j-1}^w$ 的梯度为: $$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \theta_{j-1}^w} &= \dfrac{\partial}{\partial \theta_{j-1}^w} \left\{\left(1 - d_j^w\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] + d_j^w \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right]\right\} \\ &= \left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right) \mathbf{x}_w \\ &= \left\{\left(1 - d_j^w\right) \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] - d_j^w \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right\} \mathbf{x}_w \\ &= \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w \end{split} \end{equation} $$ 则 $\theta_{j-1}^w$ 的更新方式为: $$ \theta_{j-1}^w \gets \theta_{j-1}^w + \eta \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \mathbf{x}_w $$ 同理可得,$\mathcal{L} \left(w, j\right)$ 关于 $\mathbf{x}_w$ 的梯度为: $$ \dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w} = \left[1 - d_j^w - \sigma \left(\mathbf{x}_w^{\top} \theta_{j-1}^w\right)\right] \theta_{j-1}^w $$ 但 $\mathbf{x}_w$ 为上下文词汇向量的加和,Word2Vec 的做法是将梯度贡献到上下文中的每个词向量上,即: $$ \mathbf{v} \left(u\right) \gets \mathbf{v} \left(u\right) + \eta \sum_{j=2}^{l^w}{\dfrac{\partial \mathcal{L} \left(w, j\right)}{\partial \mathbf{x}_w}}, u \in Context \left(w\right) $$ 基于 Hierarchical Softmax 的 CBOW 模型的随机梯度上升算法伪代码如下: 基于 Hierarchical Softmax 的 Skip-gram 模型如下: 对于 Skip-gram 模型,是利用当前词 $w$ 对上下文 $Context \left(w\right)$ 中的词进行预测,则条件概率为: $$ p \left(Context \left(w\right) | w\right) = \prod_{u \in Context \left(w\right)}{p \left(u | w\right)} $$ 类似于 CBOW 模型的思想,有: $$ p \left(u | w\right) = \prod_{j=2}^{l^u}{p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right)} $$ 其中 $$ p \left(d_j^u | \mathbf{v} \left(w\right), \theta_{j-1}^u\right) = \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u} $$ 可得对数似然函数为: $$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log \prod_{u \in Context \left(w\right)}{\prod_{j=2}^{l^u}{\left\{\left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]^{1 - d_j^u} \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^u\right)\right]^{d_j^u}\right\}}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\}}}} \end{split} \end{equation} $$ 记上式花括号中的内容为 $\mathcal{L} \left(w, u, j\right)$,在 $\mathcal{L} \left(w, u, j\right)$ 关于 $\theta_{j-1}^u$ 的梯度为: $$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \theta_{j-1}^{u}} &= \dfrac{\partial}{\partial \theta_{j-1}^{u}} \left\{\left(1 - d_j^u\right) \cdot \log \left[\sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] + d_j^u \cdot \log \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right]\right\} \\ &= \left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right) \mathbf{v} \left(w\right) \\ &= \left\{\left(1 - d_j^u\right) \cdot \left[1 - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] - d_j^u \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right\} \mathbf{v} \left(w\right) \\ &= \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) \end{split} \end{equation} $$ 则 $\theta_{j-1}^u$ 的更新方式为: $$ \theta_{j-1}^u \gets \theta_{j-1}^u + \eta \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \mathbf{v} \left(w\right) $$ 同理可得,$\mathcal{L} \left(w, u, j\right)$ 关于 $\mathbf{v} \left(w\right)$ 的梯度为: $$ \dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)} = \left[1 - d_j^u - \sigma \left(\mathbf{v} \left(w\right)^{\top} \theta_{j-1}^{u}\right)\right] \theta_{j-1}^u $$ 则 $\mathbf{v} \left(w\right)$ 的更新方式为: $$ \mathbf{v} \left(w\right) \gets \mathbf{v} \left(w\right) + \eta \sum_{u \in Context \left(w\right)}{\sum_{j=2}^{l^u}{\dfrac{\partial \mathcal{L} \left(w, u, j\right)}{\partial \mathbf{v} \left(w\right)}}} $$ 基于 Hierarchical Softmax 的 Skip-gram 模型的随机梯度上升算法伪代码如下: 基于 Negative Sampling 的模型 基于 Negative Sampling (NEG) 的模型相比于基于 Hierarchical Softmax 的模型不再使用复杂的 Huffman 树,而是使用简单的随机负采样,从而大幅的提高了模型的性能。 基于 Negative Sampling 的 CBOW 模型如下: 对于基于 Negative Sampling CBOW 模型,已知词 $w$ 的上下文 $Context \left(w\right)$,预测词 $w$,则词 $w$ 即为一个正样本,其他词则为负样本。对于一个给定 $Context \left(w\right)$ 的负样本集合 $NEG \left(w\right) \neq \varnothing$,词典中的任意词 $\forall \tilde{w} \in \mathcal{D}$,其样本的标签定义为: $$ L^w \left(\tilde{w}\right) = \begin{cases} 1, & \tilde{w} = w \\ 0, & \tilde{w} \neq w \end{cases} $$ 则对于一个正样本 $\left(Context, \left(w\right)\right)$,我们希望最大化: $$ g \left(w\right) = \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{p \left(u | Context \left(w\right)\right)} $$ 或表示为: $$ p \left(u | Context \left(w\right)\right) = \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(w\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(w\right)} $$ 即增大正样本概率的同时减少负样本的概率。对于一个给定的语料库 $\mathcal{C}$,对数似然函数为: $$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &= \sum_{w \in \mathcal{C}}{\log \prod_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{\left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\}}} \end{split} \end{equation} $$ 记上式花括号中的内容为 $\mathcal{L} \left(w, u\right)$,则 $\mathcal{L} \left(w, u\right)$ 关于 $\theta^u$ 的梯度为: $$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \theta^u} &= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right]\right\} \\ &= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right) \mathbf{x}_w \\ &= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{x}_w^{\top} \theta^u\right)\right\} \mathbf{x}_w \\ &= \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w \end{split} \end{equation} $$ 则 $\theta^u$ 的更新方式为: $$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \mathbf{x}_w $$ 同理可得,$\mathcal{L} \left(w, u\right)$ 关于 $\mathbf{x}_w$ 的梯度为: $$ \dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w} = \left[L^w \left(u\right) - \sigma \left(\mathbf{w}_w^{\top} \theta^u\right)\right] \theta^u $$ 则 $\mathbf{v} \left(\tilde{w}\right), \tilde{w} \in Context \left(w\right)$ 的更新方式为: $$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, u\right)}{\partial \mathbf{x}_w}}, \tilde{w} \in Context \left(w\right) $$ 基于 Negative Sampling 的 CBOW 模型的随机梯度上升算法伪代码如下: 基于 Negative Sampling 的 Skip-gram 模型如下: 对于 Skip-gram 模型,利用当前词 $w$ 对上下文 $Context \left(w\right)$ 中的词进行预测,则对于一个正样本 $\left(Context, \left(w\right)\right)$,我们希望最大化: $$ g \left(w\right) = \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{p \left(u | \tilde{w}\right)}} $$ 其中,$NEG^{\tilde{w}} \left(w\right)$ 为处理词 $\tilde{w}$ 时生成的负样本集合,且: $$ p \left(u | \tilde{w}\right) = \begin{cases} \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) & L^w \left(u\right) = 1 \\ 1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right) & L^w \left(u\right) = 0 \end{cases} $$ 或表示为: $$ p \left(u | \tilde{w}\right) = \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)} $$ 对于一个给定的语料库 $\mathcal{C}$,对数似然函数为: $$ \begin{equation} \begin{split} \mathcal{L} &= \sum_{w \in \mathcal{C}}{\log g \left(w\right)} \\ &= \sum_{w \in \mathcal{C}}{\log \prod_{\tilde{w} \in Context \left(w\right)}{\prod_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{\left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{L^w \left(u\right)} \cdot \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]^{1 - L^w \left(u\right)}\right\}}}} \\ &= \sum_{w \in \mathcal{C}}{\sum_{\tilde{w} \in Context \left(w\right)}{\sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\}}}} \end{split} \end{equation} $$ 记上式花括号中的内容为 $\mathcal{L} \left(w, \tilde{w}, u\right)$,则 $\mathcal{L} \left(w, \tilde{w}, u\right)$ 关于 $\theta^u$ 的梯度为: $$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \theta^u} &= \dfrac{\partial}{\partial \theta^u} \left\{L^w \left(u\right) \cdot \log \left[\sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right] + \left[1 - L^w \left(u\right)\right] \cdot \log \left[1 - \sigma \left(\mathbf{v}\left(\tilde{w}\right)^{\top} \theta^u\right)\right]\right\} \\ &= L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right) \mathbf{v} \left(\tilde{w}\right) \\ &= \left\{L^w \left(u\right) \left[1 - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] - \left[1 - L^w \left(u\right)\right] \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right\} \mathbf{v} \left(\tilde{w}\right) \\ &= \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) \end{split} \end{equation} $$ 则 $\theta^u$ 的更新方式为: $$ \theta^u \gets \theta^u + \eta \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \mathbf{v} \left(\tilde{w}\right) $$ 同理可得,$\mathcal{L} \left(w, \tilde{w}, u\right)$ 关于 $\mathbf{v} \left(\tilde{w}\right)$ 的梯度为: $$ \dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)} = \left[L^w \left(u\right) - \sigma \left(\mathbf{v} \left(\tilde{w}\right)^{\top} \theta^u\right)\right] \theta^u $$ 则 $\mathbf{v} \left(\tilde{w}\right)$ 的更新方式为: $$ \mathbf{v} \left(\tilde{w}\right) \gets \mathbf{v} \left(\tilde{w}\right) + \eta \sum_{u \in \left\{w\right\} \cup NEG^{\tilde{w}} \left(w\right)}{\dfrac{\partial \mathcal{L} \left(w, \tilde{w}, u\right)}{\partial \mathbf{v} \left(\tilde{w}\right)}} $$ 基于 Negative Sampling 的 Skig-gram 模型的随机梯度上升算法伪代码如下: 无论是基于 Negative Sampling 的 CBOW 模型还是 Skip-gram 模型,我们都需要对于给定的词 $w$ 生成 $NEG \left(w\right)$,对于一个词典 $\mathcal{D}$ 和给定的语料 $\mathcal{C}$,一个词被选择中的概率为: $$ p_{NEG} \left(w\right) = \dfrac{\#w}{\sum_{u \in \mathcal{D}}{\#u}} $$ 其中 $\#w$ 和 $\#u$ 表示词 $w$ 和 $u$ 在语料 $\mathcal{C}$ 中出现的频次。在 Word2Vec 的 C 代码中 6,并没有使用词的原始频次,而是对其做了 0.75 次幂,即: $$ p_{NEG} \left(w\right) = \dfrac{\left(\#w\right)^{0.75}}{\sum_{u \in \mathcal{D}}{\left(\#u\right)^{0.75}}} $$ 本节内容参考了 licstar 的 博客 和 peghoty 的 博客。 其他 Embedding 方法 GloVe GloVe (Global Vector 的简写) 是由 Pennington 等人 7 提出了一种词向量生成方法,该方法利用了语料的全局统计信息。 令 $X$ 表示词与词之间的共现矩阵,$X_{ij}$ 表示词 $j$ 在词 $i$ 为上下文的情况下出现的频次。则 $X_i = \sum_{k}{X_{ik}}$ 表示在词$i$ 为上下文的情况任意词出现的总次数。令 $P_{ij} = P \left(j | i\right) = X_{ij} / X_i$ 表示词 $j$ 在词 $i$ 出现前提下出现的条件概率。 例如,我们令 $i = ice, j = steam$,则这两个词之间的关系可以利用同其他词 $k$ 共现概率的比率学习得出。则有: 与词 ice 相关,但与词 steam 不太相关,例如 $k = solid$,则比率 $P_{ik} / P_{jk}$ 应该较大;类似的当词 $k$ 与 steam 相关,但与词 ice 不太相关,则比率 $P_{ik} / P_{jk}$ 应该较小。 当与词 ice 和词 steam 均相关或者均不太相关时,例如 $k = water$ 或 $k = fashion$,则比率 $P_{ik} / P_{jk}$ 应该和 1 接近。 下表展示了在一个大量语料上的概率及其比率: 概率和比例 $k = solid$ $k = gas$ $k = water$ $k = fashion$ $P \left(k \vert ice\right)$ $1.9 \times 10^{-4}$ $6.6 \times 10^{-5}$ $3.0 \times 10^{-3}$ $1.7 \times 10^{-5}$ $P \left(k \vert steam\right)$ $2.2 \times 10^{-5}$ $7.8 \times 10^{-4}$ $2.2 \times 10^{-3}$ $1.8 \times 10^{-5}$ $P \left(k \vert ice\right) / P \left(k \vert steam\right)$ $8.9$ $8.5 \times 10^{-2}$ $1.36$ $0.96$ 根据如上的假设,我们可以得到一个最基础的模型: $$ F \left(w_i, w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$ 其中 $w \in \mathbb{R}^d$ 为词向量,$\tilde{w}_k \in \mathbb{R}^d$ 为单独的上下文词的词向量。假设向量空间是一个线性结构,因此 $F$ 仅依赖于两个向量之间的差异,则模型可以改写为: $$ F \left(w_i - w_j, \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$ 上式中右面是一个标量,如果左面的参数利用一个复杂的模型进行计算,例如神经网络,则会破坏我们希望保留的线性结构。因此,我们对参数采用点积运算,即: $$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{P_{ik}}{P_{jk}} $$ 在词之间的共现矩阵中,一个词和其上下文中的一个词之间应该是可以互换角色的。首先我们要保证 $F$ 在 $\left(\mathbb{R}, +\right)$ 和 $\left(\mathbb{R}_{>0}, \times\right)$ 上是同态的 (homomorphism),例如: $$ F \left(\left(w_i - w_j\right)^{\top} \tilde{w}_k\right) = \dfrac{F \left(w_i^{\top} \tilde{w}_k\right)}{F \left(w_j^{\top} \tilde{w}_k\right)} $$ 其中 $F \left(w_i^{\top} \tilde{w}_k\right) = P_{ik} = \dfrac{X_{ik}}{X_i}$,则上式的一个解为 $F = \exp$,或: $$ w_i^{\top} \tilde{w}_k = \log \left(P_{ik}\right) = \log \left(X_{ik}\right) - \log \left(X_i\right) $$ 其中 $\log \left(X_i\right)$ 与 $k$ 无关记为 $b_i$,同时为了对称性添加 $\tilde{b}_k$,则上式改写为: $$ w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k = \log \left(X_{ik}\right) $$ 上式中,左侧为词向量的相关运算,右侧为共现矩阵的常量信息,则给出模型的损失函数如下: $$ J = \sum_{i,j=1}^{V}{f \left(X_{ij}\right) \left(w_i^{\top} \tilde{w}_k + b_i + \tilde{b}_k - \log X_{ij}\right)^2} $$ 其中,$V$ 为词典中词的个数,$f$ 为一个权重函数,其应具有如下特点: $f \left(0\right) = 0$。如果 $f$ 为一个连续函数,则当 $x \to 0$ 时 $\lim_{x \to 0}{f \left(x\right) \log^2 x}$ 应足够快地趋近于无穷。 $f \left(x\right)$ 应为非减函数,以确保稀少的共现不会权重过大。 $f \left(x\right)$ 对于较大的 $x$ 应该相对较小,以确保过大的共现不会权重过大。 文中给出了一个符合要求的函数如下: $$ f \left(x\right) = \begin{cases} \left(x / x_{\max}\right)^{\alpha} & \text{if} \ x < x_{\max} \\ 1 & \text{otherwise} \end{cases} $$ 其中两个超参数的值建议为 $x_{\max} = 100, \alpha = 0.75$。 fastText fastText 是由 Bojanowski 和 Grave 等人 8 提出的一种词向量表示方法。原始的 Skip-gram 模型忽略了词语内部的结构信息,fastText 利用 N-gram 方法将其考虑在内。 对于一个词 $w$,利用一系列的 N-gram 进行表示,同时在词的前后添加 < 和 > 边界符号以同其他文本序列进行区分。同时还将词语本身也包含在这个 N-gram 集合中,从而学习到词语的向量表示。例如,对于词 $where$ 和 $n = 3$,则 N-gram 集合为:<wh, whe, her, ere, re>,同时包含词本身 <where>。需要注意的是,序列 <her> 与词 $where$ 中的 tri-gram her 是两个不同的概念。模型提取所有 $3 \leq n \leq 6$ 的 N-gram 序列。 假设 N-gram 词典的大小为 $G$,对于一个词 $w$,$\mathcal{G}_w \subset \left\{1, ..., G\right\}$ 表示词中出现的 N-gram 的集合。针对任意一个 N-gram $g$,用向量 $\mathbf{z}_g$ 表示,则我们利用一个词的所有 N-gram 的向量的加和表示该词。可以得到该模型的评分函数为: $$ s \left(w, c\right) = \sum_{g \in \mathcal{G}_w}{\mathbf{z}_g^{\top} \mathbf{v}_c} $$ 模型在学习不同词向量时可以共享权重 (不同词的可能包含相同的 N-gram),使得在学习低频词时也可得到可靠的向量表示。 WordRank WordRank 是由 Ji 等人 9 提出的一种词向量表示方法,其将词向量学习问题转换成一个排序问题。 我们令 $\mathbf{u}_w$ 表示当前词 $w$ 的 $k$ 维词向量,$\mathbf{v}_c$ 表示当前词上下文 $c$ 的词向量。通过两者的内积 $\langle \mathbf{u}_w, \mathbf{v}_c \rangle$ 来捕获词 $w$ 和上下文 $c$ 之间的关系,两者越相关则该内积越大。对于一个给定的词 $w$,利用上下文集合 $\mathcal{C}$ 同词的内积分数进行排序,对于一个给定的上下文 $c$,排序为: $$ \begin{equation} \begin{split} \text{rank} \left(w, c\right) &= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c \rangle - \langle \mathbf{u}_w, \mathbf{v}_{c'} \rangle \leq 0\right)} \\ &= \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{I \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle \leq 0\right)} \end{split} \end{equation} $$ 其中,$I \left(x \leq 0\right)$ 为一个 0-1 损失函数,当 $x \leq 0$ 时为 1 其他情况为 0。由于 $I \left(x \leq 0\right)$ 为一个非连续函数,因此我们可以将其替换为一个凸上限函数 $\ell \left(\cdot\right)$,其可以为任意的二分类损失函数,构建排序的凸上限如下: $$ \text{rank} \left(w, c\right) \leq \overline{\text{rank}} \left(w, c\right) = \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)} $$ 我们期望排序模型将更相关的上下文排在列表的顶部,基于此构建损失函数如下: $$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{w \in \mathcal{W}}{\sum_{c \in \Omega_w}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)}} $$ 其中,$\mathcal{W}$ 表示词典,$\mathbf{U} := \left\{\mathbf{u}_w\right\}_{w \in \mathcal{W}}$ 和 $\mathbf{V} := \left\{\mathbf{c}_w\right\}_{c \in \mathcal{C}}$ 分别表示词及其上下文词向量的参数,$\Omega_w$ 表示与词 $w$ 共现的上下文的集合,$r_{w, c}$ 为衡量 $w$ 和 $c$ 之间关系的权重,$\rho \left(\cdot\right)$ 为用于衡量排序好坏的单调递增的损失函数,$\alpha \geq 0, \beta \geq 0$ 为超参数。可选的有: $$ r_{w, c} = \begin{cases} \left(X_{w, c} / x_{\max}\right)^{\epsilon} & \text{if} \ X_{w, c} < x_{\max} \\ 1 & \text{otherwise} \end{cases} $$ 其中 $x_{\max} = 100, \epsilon = 0.75$。根据 $\rho \left(\cdot\right)$ 的要求,损失函数在排序的顶部 (rank 值小) 的地方更加敏感,同时对于 rank 值较大的地方不敏感。这可以使得模型变得更加稳健 (避免语法错误和语言的非常规使用造成干扰),因此可选的有: $$ \begin{equation} \begin{split} \rho \left(x\right) &:= \log_2 \left(1 + x\right) \\ \rho \left(x\right) &:= 1 - \dfrac{1}{\log_2 \left(2 + x\right)} \\ \rho \left(x\right) &:= \dfrac{x^{1 - t} - 1}{1 - t}, t \neq 1 \end{split} \end{equation} $$ 损失函数可以等价的定义为: $$ J \left(\mathbf{U}, \mathbf{V}\right) := \sum_{\left(w, c\right) \in \Omega}{r_{w, c} \cdot \rho \left(\dfrac{\overline{\text{rank}} \left(w, c\right) + \beta}{\alpha}\right)} $$ 在训练过程中,外层的求和符号容易利用 SDG 算法解决,但对于内层的求和符号除非 $\rho \left(\cdot\right)$ 是一个线性函数,否则难以求解。然而,$\rho \left(\cdot\right)$ 函数的性质要求其不能是一个线性函数,但我们可以利用其凹函数的特性对其进行一阶泰勒分解,有: $$ \rho \left(x\right) \leq \rho \left(\xi^{-1}\right) + \rho' \left(\xi^{-1}\right) \cdot \left(x - \xi^{-1}\right) $$ 对于任意 $x$ 和 $\xi \neq 0$ 均成立,同时当且仅当 $\xi = x^{-1}$ 时等号成立。因此,令 $\Xi := \left\{\xi_{w, c}\right\}_{\left(w, c\right) \in \Sigma}$,则可以得到 $J \left(\mathbf{U}, \mathbf{V}\right)$ 的一个上界: $$ \begin{equation} \begin{split} \overline{J} \left(\mathbf{U}, \mathbf{V}, \Xi\right) &:= \sum_{\left(w, c\right) \in \Omega}{r_{w, c} \cdot \left\{\rho \left(\xi_{wc}^{-1}\right) + \rho' \left(\xi_{wc}^{-1}\right) \cdot \left(\alpha^{-1} \beta + \alpha^{-1} \sum_{c' \in \mathcal{C} \setminus \left\{c\right\}}{\ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right) - \xi_{w, c}^{-1}}\right)\right\}} \\ &= \sum_{\left(w, c, c'\right)}{r_{w, c} \cdot \left(\dfrac{\rho \left(\xi_{w, c}^{-1}\right) + \rho' \left(\xi_{w, c}^{-1}\right) \cdot \left(\alpha^{-1} \beta - \xi_{w, c}^{-1}\right)}{\lvert \mathcal{C} \rvert - 1} + \dfrac{1}{\alpha} \rho' \left(\xi_{w, c}^{-1}\right) \cdot \ell \left(\langle \mathbf{u}_w, \mathbf{v}_c - \mathbf{v}_{c'} \rangle\right)\right)} \end{split} \end{equation} $$ 其中,$\left(w, c, c'\right) \in \Omega \times \left(\mathcal{C} \setminus \left\{c\right\}\right)$,至此我们可以通过均匀采样 $\left(w, c\right) \in \Sigma$ 和 $c' \in \mathcal{C} \setminus \left\{c\right\}$ 解决训练问题。 整个 WordRank 算法的伪代码如下: cw2vec cw2vec 是由 Cao 等人 10 提出的一种基于汉字笔画 N-gram 的中文词向量表示方法。该方法根据汉字作为象形文字具有笔画信息的特点,提出了笔画 N-gram 的概念。针对一个词的笔画 N-gram,其生成过程如下图所示: 共包含 4 个步骤: 将一个词拆解成单个的汉字,例如:“大人” 拆解为 “大” 和 “人”。 将每个汉字拆解成笔画,例如:“大” 和 “人” 拆解为 “一,丿,乀,丿,乀”。 将每个笔画映射到对应的编码序列,例如: “一,丿,乀,丿,乀” 映射为 13434。 利用编码序列生成笔画 N-gram,例如:134,343,434;1343,3434;13434。 模型中定义一个词 $w$ 及其上下文 $c$ 的相似度如下: $$ sim \left(w, c\right) = \sum_{q \in S\left(w\right)}{\vec{q} \cdot \vec{c}} $$ 其中,$S$ 为由笔画 N-gram 构成的词典,$S \left(w\right)$ 为词 $w$ 对应的笔画 N-gram 集合,$q$ 为该集合中的一个笔画 N-gram,$\vec{q}$ 为 $q$ 对应的向量。 该模型的损失函数为: $$ \mathcal{L} = \sum_{w \in D}{\sum_{c \in T \left(w\right)}{\log \sigma \left(sim \left(w, c\right)\right) + \lambda \mathbb{E}_{c' \sim P} \left[\log \sigma \left(- sim \left(w, c'\right)\right)\right]}} $$ 其中,$D$ 为语料中的全部词语,$T \left(w\right)$ 为给定的词 $w$ 和窗口内的所有上次文词,$\sigma \left(x\right) = \left(1 + \exp \left(-x\right)\right)^{-1}$,$\lambda$ 为负采样的个数,$\mathbb{E}_{c' \sim P} \left[\cdot\right]$ 表示负样本 $c'$ 按照 $D$ 中词的分布 $P$ 进行采样,该分布可以为词的一元模型的分布 $U$,同时为了避免数据的稀疏性问题,类似 Word2Vec 中的做法采用 $U^{0.75}$。 Hinton, G. E. (1986, August). Learning distributed representations of concepts. In Proceedings of the eighth annual conference of the cognitive science society (Vol. 1, p. 12). ↩︎ Bengio, Y., Courville, A., & Vincent, P. (2013). Representation learning: A review and new perspectives. IEEE transactions on pattern analysis and machine intelligence, 35(8), 1798-1828. ↩︎ Bengio, Y., Ducharme, R., Vincent, P., & Jauvin, C. (2003). A Neural Probabilistic Language Model. Journal of Machine Learning Research, 3(Feb), 1137–1155. ↩︎ Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. arXiv preprint arXiv:1301.3781 ↩︎ https://zh.wikipedia.org/zh/霍夫曼编码 ↩︎ https://code.google.com/archive/p/word2vec/ ↩︎ Pennington, J., Socher, R., & Manning, C. (2014). Glove: Global Vectors for Word Representation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1532–1543). ↩︎ Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). Enriching Word Vectors with Subword Information. Transactions of the Association for Computational Linguistics, 5, 135–146. ↩︎ Ji, S., Yun, H., Yanardag, P., Matsushima, S., & Vishwanathan, S. V. N. (2016). WordRank: Learning Word Embeddings via Robust Ranking. In Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing (pp. 658–668). ↩︎ Cao, S., Lu, W., Zhou, J., & Li, X. (2018). cw2vec: Learning Chinese Word Embeddings with Stroke n-gram Information. In Thirty-Second AAAI Conference on Artificial Intelligence. ↩︎

2018/10/1
articleCard.readMore

循环神经网络 (Recurrent Neural Network, RNN)

文章部分内容参考了 Christopher 的博客 Understanding LSTM Networks,内容翻译和图片重绘已得到原作者同意。 发展史 循环神经网络 (Recurrent Neural Network, RNN) 一般是指时间递归神经网络而非结构递归神经网络 (Recursive Neural Network),其主要用于对序列数据进行建模。Salehinejad 等人 1 的一篇综述文章列举了 RNN 发展过程中的一些重大改进,如下表所示: Year 1st Author Contribution 1990 Elman Popularized simple RNNs (Elman network) 1993 Doya Teacher forcing for gradient descent (GD) 1994 Bengio Difficulty in learning long term dependencies with gradient descend 1997 Hochreiter LSTM: long-short term memory for vanishing gradients problem 1997 Schuster BRNN: Bidirectional recurrent neural networks 1998 LeCun Hessian matrix approach for vanishing gradients problem 2000 Gers Extended LSTM with forget gates 2001 Goodman Classes for fast Maximum entropy training 2005 Morin A hierarchical softmax function for language modeling using RNNs 2005 Graves BLSTM: Bidirectional LSTM 2007 Jaeger Leaky integration neurons 2007 Graves MDRNN: Multi-dimensional RNNs 2009 Graves LSTM for hand-writing recognition 2010 Mikolov RNN based language model 2010 Neir Rectified linear unit (ReLU) for vanishing gradient problem 2011 Martens Learning RNN with Hessian-free optimization 2011 Mikolov RNN by back-propagation through time (BPTT) for statistical language modeling 2011 Sutskever Hessian-free optimization with structural damping 2011 Duchi Adaptive learning rates for each weight 2012 Gutmann Noise-contrastive estimation (NCE) 2012 Mnih NCE for training neural probabilistic language models (NPLMs) 2012 Pascanu Avoiding exploding gradient problem by gradient clipping 2013 Mikolov Negative sampling instead of hierarchical softmax 2013 Sutskever Stochastic gradient descent (SGD) with momentum 2013 Graves Deep LSTM RNNs (Stacked LSTM) 2014 Cho Gated recurrent units 2015 Zaremba Dropout for reducing Overfitting 2015 Mikolov Structurally constrained recurrent network (SCRN) to enhance learning longer memory for vanishing gradient problem 2015 Visin ReNet: A RNN-based alternative to convolutional neural networks 2015 Gregor DRAW: Deep recurrent attentive writer 2015 Kalchbrenner Grid long-short term memory 2015 Srivastava Highway network 2017 Jing Gated orthogonal recurrent units RNN 网络结构 不同于传统的前馈神经网络接受特定的输入得到输出,RNN 由人工神经元和一个或多个反馈循环构成,如下图所示: 其中,$\boldsymbol{x}_t$ 为输入层,$\boldsymbol{h}_t$ 为带有循环的隐含层,$\boldsymbol{y}_t$ 为输出层。其中隐含层包含一个循环,为了便于理解我们将循环进行展开,展开后的网络结构如下图所示: 对于展开后的网络结构,其输入为一个时间序列 $\left\{\dotsc, \boldsymbol{x}_{t-1}, \boldsymbol{x}_t, \boldsymbol{x}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{x}_t \in \mathbb{R}^n$,$n$ 为输入层神经元个数。相应的隐含层为 $\left\{\dotsc, \boldsymbol{h}_{t-1}, \boldsymbol{h}_t, \boldsymbol{h}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{h}_t \in \mathbb{R}^m$,$m$ 为隐含层神经元个数。隐含层节点使用较小的非零数据进行初始化可以提升整体的性能和网络的稳定性 2。隐含层定义了整个系统的状态空间 (state space),或称之为 memory 1: $$ \boldsymbol{h}_t = f_H \left(\boldsymbol{o}_t\right) $$ 其中 $$ \boldsymbol{o}_t = \boldsymbol{W}_{IH} \boldsymbol{x}_t + \boldsymbol{W}_{HH} \boldsymbol{h}_{t-1} + \boldsymbol{b}_h $$ $f_H \left(\cdot\right)$ 为隐含层的激活函数,$\boldsymbol{b}_h$ 为隐含层的偏置向量。对应的输出层为 $\left\{\dotsc, \boldsymbol{y}_{t-1}, \boldsymbol{y}_t, \boldsymbol{y}_{t+1}, \dotsc\right\}$,其中 $\boldsymbol{y}_t \in \mathbb{R}^p$,$p$ 为输出层神经元个数。则: $$ \boldsymbol{y}_t = f_O \left(\boldsymbol{W}_{HO} \boldsymbol{h}_t + \boldsymbol{b}_o\right) $$ 其中 $f_O \left(\cdot\right)$ 为隐含层的激活函数,$\boldsymbol{b}_o$ 为隐含层的偏置向量。 在 RNN 中常用的激活函数为双曲正切函数: $$ \tanh \left(x\right) = \dfrac{e^{2x} - 1}{e^{2x} + 1} $$ Tanh 函数实际上是 Sigmoid 函数的缩放: $$ \sigma \left(x\right) = \dfrac{1}{1 + e^{-x}} = \dfrac{\tanh \left(x / 2\right) + 1}{2} $$ 梯度弥散和梯度爆炸 原始 RNN 存在的严重的问题就是梯度弥散 (Vanishing Gradients) 和梯度爆炸 (Exploding Gradients)。我们以时间序列中的 3 个时间点 $t = 1, 2, 3$ 为例进行说明,首先假设神经元在前向传导过程中没有激活函数,则有: $$ \begin{equation} \begin{split} &\boldsymbol{h}_1 = \boldsymbol{W}_{IH} \boldsymbol{x}_1 + \boldsymbol{W}_{HH} \boldsymbol{h}_0 + \boldsymbol{b}_h, &\boldsymbol{y}_1 = \boldsymbol{W}_{HO} \boldsymbol{h}_1 + \boldsymbol{b}_o \\ &\boldsymbol{h}_2 = \boldsymbol{W}_{IH} \boldsymbol{x}_2 + \boldsymbol{W}_{HH} \boldsymbol{h}_1 + \boldsymbol{b}_h, &\boldsymbol{y}_2 = \boldsymbol{W}_{HO} \boldsymbol{h}_2 + \boldsymbol{b}_o \\ &\boldsymbol{h}_3 = \boldsymbol{W}_{IH} \boldsymbol{x}_3 + \boldsymbol{W}_{HH} \boldsymbol{h}_2 + \boldsymbol{b}_h, &\boldsymbol{y}_3 = \boldsymbol{W}_{HO} \boldsymbol{h}_3 + \boldsymbol{b}_o \end{split} \end{equation} $$ 在对于一个序列训练的损失函数为: $$ \mathcal{L} \left(\boldsymbol{y}, \boldsymbol{\hat{y}}\right) = \sum_{t=0}^{T}{\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)} $$ 其中 $\mathcal{L}_t \left(\boldsymbol{y_t}, \boldsymbol{\hat{y}_t}\right)$ 为 $t$ 时刻的损失。我们利用 $t = 3$ 时刻的损失对 $\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}, \boldsymbol{W}_{HO}$ 求偏导,有: $$ \begin{equation} \begin{split} \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HO}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{W}_{HO}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{IH}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{IH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{IH}} \\ \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{W}_{HH}} &= \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{W}_{HH}} + \dfrac{\partial \mathcal{L}_3}{\partial \boldsymbol{y}_3} \dfrac{\partial \boldsymbol{y}_3}{\partial \boldsymbol{h}_3} \dfrac{\partial \boldsymbol{h}_3}{\partial \boldsymbol{h}_2} \dfrac{\partial \boldsymbol{h}_2}{\partial \boldsymbol{h}_1} \dfrac{\partial \boldsymbol{h}_1}{\partial \boldsymbol{W}_{HH}} \end{split} \end{equation} $$ 因此,不难得出对于任意时刻 $t$,$\boldsymbol{W}_{IH}, \boldsymbol{W}_{HH}$ 的偏导为: $$ \dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{IH}} = \sum_{k=0}^{t}{\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{y}_t} \dfrac{\partial \boldsymbol{y}_t}{\partial \boldsymbol{h}_t} \left(\prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}}\right) \dfrac{\partial \boldsymbol{h}_k}{\partial \boldsymbol{W}_{IH}}} $$ $\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$ 同理可得。对于 $\dfrac{\partial \mathcal{L}_t}{\partial \boldsymbol{W}_{HH}}$,在存在激活函数的情况下,有: $$ \prod_{j=k+1}^{t}{\dfrac{\partial \boldsymbol{h}_j}{\partial \boldsymbol{h}_{j-1}}} = \prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}} $$ 假设激活函数为 $\tanh$,下图刻画了 $\tanh$ 函数及其导数的函数取值范围: 可得,$0 \leq \tanh' \leq 1$,同时当且仅当 $x = 0$ 时,$\tanh' \left(x\right) = 1$。因此: 当 $t$ 较大时,$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$ 趋近于 0,则会产生梯度弥散问题。 当 $\boldsymbol{W}_{HH}$ 较大时,$\prod_{j=k+1}^{t}{f'_H \left(h_{j-1}\right) \boldsymbol{W}_{HH}}$ 趋近于无穷,则会产生梯度爆炸问题。 长期依赖问题 RNN 隐藏节点以循环结构形成记忆,每一时刻的隐藏层的状态取决于它的过去状态,这种结构使得 RNN 可以保存、记住和处理长时期的过去复杂信号。但有的时候,我们仅需利用最近的信息来处理当前的任务。例如:考虑一个用于利用之前的文字预测后续文字的语言模型,如果我们想预测 “the clouds are in the sky” 中的最后一个词,我们不需要太远的上下信息,很显然这个词就应该是 sky。在这个情况下,待预测位置与相关的信息之间的间隔较小,RNN 可以有效的利用过去的信息。 但也有很多的情况需要更多的上下文信息,考虑需要预测的文本为 “I grew up in France … I speak fluent French”。较近的信息表明待预测的位置应该是一种语言,但想确定具体是哪种语言需要更远位置的“在法国长大”的背景信息。理论上 RNN 有能力处理这种长期依赖,但在实践中 RNN 却很难解决这个问题 3。 LSTM LSTM 网络结构 长短时记忆网络 (Long Short Term Memroy, LSTM) 是由 Hochreiter 和 Schmidhuber 4 提出一种特殊的 RNN。LSTM 的目的就是为了解决长期依赖问题,记住长时间的信息是 LSTM 的基本功能。 所有的循环神经网络都是由重复的模块构成的一个链条。在标准的 RNN 中,这个重复的模块的结构比较简单,仅包含一个激活函数为 $\tanh$ 的隐含层,如下图所示: LSTM 也是类似的链条状结构,但其重复的模块的内部结构不同。模块内部并不是一个隐含层,而是四个,并且以一种特殊的方式进行交互,如下图所示: 下面我们将一步一步的介绍 LSTM 单元 (cell) 的具体工作原理,在之前我们先对使用到的符号进行简单的说明,如下图所示: 其中,每条线都包含一个从输出节点到其他节点的整个向量,粉红色的圆圈表示逐元素的操作,黄色的矩形为学习到的神经网络层,线条的合并表示连接,线条的分叉表示内容的复制并转移到不同位置。 LSTM 单元状态和门控机制 LSTM 的关键为单元的状态 (cell state),即下图中顶部水平穿过单元的直线。单元的状态像是一条传送带,其直接运行在整个链条上,同时仅包含少量的线性操作。因此,信息可以很容易得传递下去并保持不变。 LSTM 具有向单元状态添加或删除信息的能力,这种能力被由一种称之为“门” (gates) 的结构所控制。门是一种可选择性的让信息通过的组件,其由一层以 Sigmoid 为激活函数的网络层和一个逐元素相乘操作构成的,如下图所示: Sigmoid 层的输出值介于 0 和 1 之间,代表了所允许通过的数据量。0 表示不允许任何数据通过,1 表示允许所有数据通过。一个 LSTM 单元包含 3 个门用于控制单元的状态。 LSTM 工作步骤 LSTM 的第一步是要决定从单元状态中所忘记的信息,这一步是通过一个称之为“遗忘门 (forget gate)”的 Sigmoid 网络层控制。该层以上一时刻隐含层的输出 $h_{t-1}$ 和当前这个时刻的输入 $x_t$ 作为输入,输出为一个介于 0 和 1 之间的值,1 代表全部保留,0 代表全部丢弃。回到之前的语言模型,单元状态需要包含主语的性别信息以便选择正确的代词。但当遇见一个新的主语后,则需要忘记之前主语的性别信息。 $$ f_t = \sigma \left(W_f \cdot \left[h_{t-1}, x_t\right] + b_f\right) $$ 第二步我们需要决定要在单元状态中存储什么样的新信息,这包含两个部分。第一部分为一个称之为“输入门 (input gate)” 的 Sigmoid 网络层,其决定更新那些数据。第二部分为一个 Tanh 网络层,其将产生一个新的候选值向量 $\tilde{C}_t$ 并用于添加到单元状态中。之后会将两者进行整合,并对单元状态进行更新。在我们的语言模型中,我们希望将新主语的性别信息添加到单元状态中并替代需要忘记的旧主语的性别信息。 $$ \begin{equation} \begin{split} i_t &= \sigma \left(W_i \cdot \left[h_{t-1}, x_t\right] + b_i\right) \\ \tilde{C}_t &= \tanh \left(W_C \cdot \left[h_{t-1}, x_t\right] + b_C\right) \end{split} \end{equation} $$ 接下来需要将旧的单元状态 $C_{t-1}$ 更新为 $C_t$。我们将旧的单元状态乘以 $f_t$ 以控制需要忘记多少之前旧的信息,再加上 $i_t \odot \tilde{C}_t$ 用于控制单元状态的更新。在我们的语言模型中,该操作真正实现了我们对与之前主语性别信息的遗忘和对新信息的增加。 $$ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t $$ 最后我们需要确定单元的输出,该输出将基于单元的状态,但为一个过滤版本。首先我们利用一个 Sigmoid 网络层来确定单元状态的输出,其次我们对单元状态进行 $\tanh$ 操作 (将其值缩放到 -1 和 1 之间) 并与之前 Sigmoid 层的输出相乘,最终得到需要输出的信息。 $$ \begin{equation} \begin{split} o_t &= \sigma \left(W_o \cdot \left[h_{t-1}, x_t\right] + b_o\right) \\ h_t &= o_t \odot \tanh \left(C_t\right) \end{split} \end{equation} $$ LSTM 变种 上文中介绍的基础的 LSTM 模型,事实上不同学者对 LSTM 的结构进行了或多或少的改变,其中一个比较有名的变种是由 Gers 和 Schmidhuber 提出的 5。其添加了一种“窥视孔连接 (peephole connections)”,这使得每一个门结构都能够窥视到单元的状态。 $$ \begin{equation} \begin{split} f_t &= \sigma \left(W_f \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_f\right) \\ i_t &= \sigma \left(W_i \cdot \left[\boldsymbol{C_{t-1}}, h_{t-1}, x_t\right] + b_i\right) \\ o_t &= \sigma \left(W_o \cdot \left[\boldsymbol{C_t}, h_{t-1}, x_t\right] + b_o\right) \end{split} \end{equation} $$ 另一个变种是使用了成对的遗忘门和输入门。不同于一般的 LSTM 中分别确定需要遗忘和新添加的信息,成对的遗忘门和输入门仅在需要添加新输入是才会忘记部分信息,同理仅在需要忘记信息时才会添加新的输入。 $$ C_t = f_t \odot C_{t-1} + \boldsymbol{\left(1 - f_t\right)} \odot \tilde{C}_t $$ 另外一个比较有名的变种为 Cho 等人提出的 Gated Recurrent Unit (GRU) 6,单元结构如下: GRU 将遗忘门和输入门整个成一层,称之为“更新门 (update gate)”,同时配以一个“重置门 (reset gate)”。具体的计算过程如下: 首先计算更新门 $z_t$ 和重置门 $r_t$: $$ \begin{equation} \begin{split} z_t &= \sigma \left(W_z \cdot \left[h_{t-1}, x_t\right]\right) \\ r_t &= \sigma \left(W_r \cdot \left[h_{t-1}, x_t\right]\right) \end{split} \end{equation} $$ 其次计算候选隐含层 (candidate hidden layer) $\tilde{h}_t$,与 LSTM 中计算 $\tilde{C}_t$ 类似,其中 $r_t$ 用于控制保留多少之前的信息: $$ \tilde{h}_t = \tanh \left(W \cdot \left[r_t \odot h_{t-1}, x_t\right]\right) $$ 最后计算需要从之前的隐含层 $h_{t-1}$ 遗忘多少信息,同时加入多少新的信息 $\tilde{h}_t$,$z_t$ 用于控制这个比例: $$ h_t = \left(1 - z_t\right) \odot h_{t-1} + z_t \odot \tilde{h}_t $$ 因此,对于短距离依赖的单元重置门的值较大,对于长距离依赖的单元更新门的值较大。如果 $r_t = 1$ 并且 $z_t = 0$,则 GRU 退化为一个标准的 RNN。 除此之外还有大量的 LSTM 变种,Greff 等人 7 对一些常见的变种进行了比较,Jozefowicz 等人 8 测试了大量的 RNN 结构在不同任务上的表现。 扩展与应用 循环神经网络在序列建模上有着天然的优势,其在自然语言处理,包括:语言建模,语音识别,机器翻译,对话与QA,文本生成等;计算视觉,包括:目标识别,视觉追踪,图像生成等;以及一些综合场景,包括:图像标题生成,视频字幕生成等,多个领域均有不错的表现,有代表性的论文请参见 awesome-rnn。 Google 的 Magenta 是一项利用机器学习创作艺术和音乐的研究,其中也包含了大量利用 RNN 相关模型构建的有趣项目。SketchRNN 是由 Ha 等人 9 提出了一种能够根据用户描绘的一些简单图形自动完成后续绘画的 RNN 网络。 Performance RNN 是由 Ian 等人 10 提出了一种基于时间和动态因素生成复合音乐的 LSTM 网络。 更多有趣的作品请参见 Megenta 的 Demos 页面。 Salehinejad, H., Sankar, S., Barfett, J., Colak, E., & Valaee, S. (2017). Recent Advances in Recurrent Neural Networks. arXiv preprint arXiv:1801.01078. ↩︎ ↩︎ Sutskever, I., Martens, J., Dahl, G., & Hinton, G. (2013). On the importance of initialization and momentum in deep learning. In International Conference on Machine Learning (pp. 1139–1147). ↩︎ Bengio, Y., Simard, P., & Frasconi, P. (1994). Learning long-term dependencies with gradient descent is difficult. IEEE Transactions on Neural Networks, 5(2), 157–166. ↩︎ Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural Computation, 9(8), 1735–1780. ↩︎ Gers, F. A., & Schmidhuber, J. (2000). Recurrent nets that time and count. In Proceedings of the IEEE-INNS-ENNS International Joint Conference on Neural Networks. IJCNN 2000. Neural Computing: New Challenges and Perspectives for the New Millennium (Vol. 3, pp. 189–194 vol.3). ↩︎ Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP) (pp. 1724–1734). ↩︎ Greff, K., Srivastava, R. K., Koutník, J., Steunebrink, B. R., & Schmidhuber, J. (2017). LSTM: A Search Space Odyssey. IEEE Transactions on Neural Networks and Learning Systems, 28(10), 2222–2232. ↩︎ Jozefowicz, R., Zaremba, W., & Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. In Proceedings of the 32Nd International Conference on International Conference on Machine Learning - Volume 37 (pp. 2342–2350). ↩︎ Ha, D., & Eck, D. (2017). A Neural Representation of Sketch Drawings. arXiv preprint arXiv:1704.03477 ↩︎ Ian S., & Sageev O. Performance RNN: Generating Music with Expressive Timing and Dynamics. Magenta Blog, 2017. https://magenta.tensorflow.org/performance-rnn ↩︎

2018/9/21
articleCard.readMore

泰国之行 (Tour of Thailand)

近来这大半年的工作感觉活活像一场清宫剧,对于我们这种一心只想撸代码,两耳不闻窗外事的人来说,确实太累了。从年初换了工作方向后,交接了所有之前的线上任务和系统,所以这次旅行格外的清净,没有一封报警邮件。北京已经入秋,走之前还很热,回来已凉意浓浓,去的路上天气格外的好。 进去大皇宫不能衣冠不整,牛仔裤上有个洞,在门口买了条裤子套上,还挺配我的白衬衫,感觉满满的社会人气息 😎。发现中国人在外面很难 High 起来,油轮之上小姐姐唱着不同国家的歌曲,独独唱到中文歌的时候没有人上去跳。也许我们天生没有欧美人的开放,也不像中东朋友在国家内被束缚的太紧,不过感觉出来玩还是不要闷骚,把激情都释放出来才好。 Pattaya 的水上市场虽然有商业景区的味道,但还是保留了很多当地的特色,水流两旁很多卖水果和小吃的小船。海边的日落很美,再来杯美酒,再来个佳人就更好了。 只在金沙岛上呆了小半天,天公还算作美,早起下着小雨,上岛了雨就停了。想想上次下水游泳还是上初中时候的事情了,虽然不怎么太会游,但至少还能扑腾两下。下次再有机会来,一定直奔普吉岛,舒舒服服的在岛上呆上几天,别的啥也不干,就游游泳,晒晒太阳。 这边最多的当然是各种各样的水果和超大只的海鲜,第一次吃到蛇皮果,据说这东西很壮阳。最鲜的还是螃蟹,越大只越好,泰国的酱料味道还是很特别的,不难吃但不是很习惯,不过海鲜什么都不沾,也很鲜甜。 泰国的枪械管得比较松,打了几枪后座力比较强的 .45 手枪和霰弹枪,成绩还不错,有 10 环哦。回来想一想比国内打枪便宜不少,后悔没有把所有的枪型都试一试。

2018/9/15
articleCard.readMore

媒介之战 (War of Medias)

本文为《娱乐至死》(Amusing Ourselved To Death) 的读书随想。 在这本书中,作者 Neil Postman 的基本观点为推崇铅字文化,声讨电视文化。首先,我必须承认作者对电视文化的很多现象描述确实存在,我虽然也一直知道其存在,但却从未思考过其中的问题,这是这本书对我影响最多的地方。换言之,是书籍 (铅字文化) 能够让我更深入的思考问题,这也是作者所推崇的铅字文化的益处。同时,作者并没有否定电视文化作为娱乐本身的用途,这点我也是认同的。无论是电影,电视剧还是综艺节目,电视文化确实以一种五彩斑斓的形式丰富着我们的娱乐生活。但我认为作者对于两种不同文化的观点略微有些绝对和偏激,尤其是在书籍和电视 (不同的信息载体) 内容过剩的今天,我认为两者都存在精华和糟粕 (同时包括文化内容和娱乐内容)。我认为书籍和电视中文化和娱乐的界线也不是很明显,虽然我支持作者的不要将娱乐和一些严肃的事情 (例如:政治,宗教,教育等) 混在一起,但是我也不否能不能从电视文化中获取知识。我认为更重要的是对于信息的细粒度消化,在这个过程中比较重要的是 思考 和 实践。 作者在书的最前面提到: 奥威尔担心我们憎恨的东西会毁掉我们,而赫胥黎担心的是,我们将毁于我们热爱的东西。 在文末又再次呼应了这个观点: 有两种方法可以让文化精神枯萎,一种是奥威尔式的 – 文化成为一个监狱,另一种是赫胥黎式的 – 文化成为一种滑稽戏。 最后,又引用了赫胥黎在《美丽新世界》中的观点: 人们感到痛苦的不是他们用笑声代替了思考,而是他们不知道自己为什么笑以及为什么不再思考 最后这句最符合我的观点,下面从几个方面聊聊我对书中观点的一些看法和自己的一些补充。 信息、文化和知识 作者用了两章的内容讨论 媒介 的作用,那么我就从媒介在信息的传递,文化的形成和知识的获取的三个角度来谈一谈我对媒介的认知。 信息的传递 信息,我认为可以简单的理解为我们所能感知到的一切,这里的 感知 其实就可以理解为 媒介。对于信息本身,可以将其粗略的划分为两类:有用的信息 和 无用的信息。理论一点的解释就是:从信息论的角度而言,能够帮助降低系统不确定性 (信息熵) 的事物就认为是有用的信息 (与真实概念略有差异);通俗一点的解释就是:你在过马路时,你能够感知到行人,车辆和建筑物等各种信息,此时你更关心的是那些会影响到你生命安全的车辆和行人,这些事物就是有用的信息。 无论是有用的信息还是无用的信息一定是通过某种方式 (嗅觉,听觉,视觉等) 和形态 (书籍,音乐,电视等) 传递到我们的大脑。所以我们对于事物的认知都不是透过其内涵本身,而是通过其在媒介中的具体表现形式。同一个事物本身透过不同的媒介,可能具有不同的具体形态,而我们直接去理解的正是这个具体的形态,这也就是麦克卢汉所说的 “媒介即信息”。 文化的形成 文化 同 知识 有相似之处,我认为两者的主要区别在于:文化是一个比较中性的描述,其表示一段时间内形成的某些习惯 (例如:饮食,建筑等),强调的是 习惯本身,而非 对习惯的评价 (好与坏)。而知识往往更像是一种具有 “好”的影响 的文化结晶,这个“好”可以对于集体而言也可以对于个人而言。 这些“习惯”又是如何一步步形成的呢?首先,我认为客观环境会是一个比较大的影响因素。从很底层出发,人首先需要做到同自然的抗争和共处,所以就会有人担忧干旱,有人担忧洪涝,自然会促使人们向不同方向发展。其次,就是我们人的主观因素了,我认为人的因素要更为复杂些。文化并不是一成不变的,而是在不断的演变,这种演变正是我们人自己主观选择的结果。 知识的获取 如上文中,我将 知识 定义为具有“好”的影响的文化,所以知识的获取就可以理解为对信息的选择。我认为对选择的结果产生影响的主要有两个方面:用于选择的 信息池 和 选择的方法。这个池子就像是作者所描述的不同媒介所包含的信息,铅字文化中包含更多的是知识类型的信息,而电视文化中包含的信息更多的是娱乐类型的信息。选择的方法则像一个甄别和抽象的工具,对你感知到的信息进行筛选,提取和抽象,得到最终有意义的知识。我认为两者都很重要,但在信息过剩的今天,后者对于我们自己 可控性 会更好些。 严肃性与思考 到这,我们就聊聊刚才说的可控性更好的选择方法,概括而言这个方法就是 思考。思考本身是具有 严肃性 的,在谈及思考的严肃性之前,我们先说一下信息的严肃性。在书的第六章和第七章中,对于电视文化的抨击主要有如下几点: 电视不再是为我们展示具有娱乐性的内容,而是将所有内容都以娱乐方式表现出来。 电视呈现的事件都是独立存在的,剥夺了其与过去,未来和其他事件之间的关联。 第一点其实就是在说一些严肃性的信息 (例如:宗教,教育,政治等) 不应该以电视这种形式进行展现。我很赞同这一点,从思考的角度,这些严肃的事情是需要不断思考的,也就是说思考是贯穿在这些信息的接受之中的,而电视往往没有在中途预留很多时间,很快便进入了预先设计好的后续环节。 第二点其实作者更多的描述的是 电视新闻,我也很赞同这一点,确实这些被剥离同其他事情关联的快速新闻,只能片面的向我们做出了事情本身的局部,长此以往只会让我们对其变得麻木不仁,因为他们关心的是给观众留下印象而非观点。 面对不同形式的信息,我们思考的方式也会有不同。例如读一本书,遇到不熟悉的名词,你可能需要停下来仔细调研思考之后才会继续阅读,避免影响对后续内容的理解。又例如观影,电影更擅长以视觉冲击让你对其中的某些场景留下深刻的印象,而对电影的思考往往是以事后对其内容进行反思的形式,进一步理解其深层含义。但无论是以何种形式去思考,最重要的一点是 独立思考,也就是我想表达的思考的严肃性。思考不必可避免的会涉及到对相同事物不同观点的发表和交流,独立思考让我们做到不人云亦云,同时我们也需要做到不固执己见。 思考与实践 思考不能停留在精神层面,思考可以让你不人云亦云,但是对事物的理解本身又有太多的主观性,其正确性却有待验证。实践 则可以帮助你检查你思考结果的正确性,否定自己错误的判断,避免固执己见。怎么去实践又是一门学问,但也大同小异,我感觉比较有效的几个方法如下: 反复。是说对于一个事情在不同的时点可以重复思考,比如书可以再读一篇,电影可以再看一次,每一次都会有不同的收获。 笔记。这点对于读书和观影都有效,把想法写下来,不光会让你的思路变得更加清晰,有时你还会在总结的过程发现自相矛盾的地方,有助于自我改正。 做实验。好与不好,搞一下不就知道了?拿我们这群做模型的人来说,各种深度学习算法原理掌握的再好,不放在具体的问题上,不用真实的数据试一试,都很难说孰优孰劣。 媒介之战 回到这本书的核心 – “媒介”。看起来媒介对我们像是一种 被动 的影响,而思考是一种 主动 的干预。但我认为媒介的演变其实就是我们主动选择的结果,一种对精神放纵的结果。现在不同媒介之间,甚至相同媒介内部都充满着过剩的信息,那些让你感觉获取方便,理解容易的信息所包含的知识应该不多。知识的获取一定是一件困难的事情,当你过于放纵,贪图舒适的信息获取,那必然仅能得到有限的知识。所以不要对于媒介的呈现形式所迷惑,对于铅字文化也好,对于电视文化也好,你都需要保持思辨的精神,不要让这场媒介之战影响我们对于知识的获取和未知的探索。 What you see with your eyes may not be true, see it with your heart. 一句本来之前用于讽刺自己的话,现在看来放在这也挺合适,不要浮于事物的表象,更重要的是你对事物的看法。

2018/9/1
articleCard.readMore

卷积神经网络 (Convolutional Neural Network, CNN)

发展史 卷积神经网络 (Convolutional Neural Network, CNN) 是一种目前广泛用于图像,自然语言处理等领域的深度神经网络模型。1998 年,Lecun 等人 1 提出了一种基于梯度的反向传播算法用于文档的识别。在这个神经网络中,卷积层 (Convolutional Layer) 扮演着至关重要的角色。 随着运算能力的不断增强,一些大型的 CNN 网络开始在图像领域中展现出巨大的优势,2012 年,Krizhevsky 等人 2 提出了 AlexNet 网络结构,并在 ImageNet 图像分类竞赛 3 中以超过之前 11% 的优势取得了冠军。随后不同的学者提出了一系列的网络结构并不断刷新 ImageNet 的成绩,其中比较经典的网络包括:VGG (Visual Geometry Group) 4,GoogLeNet 5 和 ResNet 6。 CNN 在图像分类问题上取得了不凡的成绩,同时一些学者也尝试将其应用在图像的其他领域,例如:物体检测 789,语义分割 10,图像摘要 11,行为识别 12 等。除此之外,在非图像领域 CNN 也取得了一定的成绩 13。 模型原理 下图为 Lecun 等人提出的 LeNet-5 的网络架构: 下面我们针对 CNN 网络中的不同类型的网络层逐一进行介绍。 输入层 LeNet-5 解决的手写数字分类问题的输入为一张 32x32 像素的灰度图像 (Gray Scale)。日常生活中计算机常用的图像的表示方式为 RGB,即将一张图片分为红色通道 (Red Channel),绿色通道 (Green Channel) 和蓝色通道 (Blue Channel),其中每个通道的每个像素点的数值范围为 $\left[0, 255\right]$。灰度图像表示该图片仅包含一个通道,也就是不具备彩色信息,每个像素点的数值范围同 RGB 图像的取值范围相同。 因此,一张图片在计算机的眼里就是一个如下图所示的数字矩阵 (示例图片来自于 MNIST 数据集 14): 在将图像输入到 CNN 网络之前,通常我们会对其进行预处理,因为每个像素点的最大取值为 $255$,因此将每个像素点的值除以 $255$ 则可以将其归一化到 $\left[0, 1\right]$ 的范围。 卷积层 在了解卷积层之前,让我们先来了解一下什么是卷积?设 $f\left(x\right), g\left(x\right)$ 是 $\mathbb{R}$ 上的两个可积函数,则卷积定义为: $$ \left(f * g\right) \left(x\right) = \int_{- \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right) d \tau} $$ 离散形式定义为: $$ \left(f * g\right) \left(x\right) = \sum_{\tau = - \infty}^{\infty}{f \left(\tau\right) g \left(x - \tau\right)} $$ 我们用一个示例来形象的理解一下卷积的含义,以离散的形式为例,假设我们有两个骰子,$f\left(x\right), g\left(x\right)$ 分别表示投两个骰子,$x$ 面朝上的概率。 $$ f \left(x\right) = g \left(x\right) = \begin{cases} 1/6 & x = 1, 2, 3, 4, 5, 6 \\ 0 & \text{otherwise} \end{cases} $$ 卷积 $\left(f * g\right) \left(x\right)$ 表示投两个骰子,朝上数字之和为 $x$ 的概率。则和为 $4$ 的概率为: $$ \begin{equation} \begin{split} \left(f * g\right) \left(4\right) &= \sum_{\tau = 1}^{6}{f \left(\tau\right) g \left(4 - \tau\right)} \\ &= f \left(1\right) g \left(4 - 1\right) + f \left(2\right) g \left(4 - 2\right) + f \left(3\right) g \left(4 - 3\right) \\ &= 1/6 \times 1/6 + 1/6 \times 1/6 + 1/6 \times 1/6 \\ &= 1/12 \end{split} \end{equation} $$ 这是一维的情况,我们处理的图像为一个二维的矩阵,因此类似的有: $$ \left(f * g\right) \left(x, y\right) = \sum_{v = - \infty}^{\infty}{\sum_{h = - \infty}^{\infty}{f \left(h, v\right) g \left(x - h, y - v\right)}} $$ 这次我们用一个抽象的例子解释二维情况下卷积的计算,设 $f, g$ 对应的概率矩阵如下: $$ f = \left[ \begin{array}{ccc} \color{red}{a_{0, 0}} & \color{orange}{a_{0, 1}} & \color{yellow}{a_{0, 2}} \\ \color{green}{a_{1, 0}} & \color{cyan}{a_{1, 1}} & \color{blue}{a_{1, 2}} \\ \color{purple}{a_{2, 0}} & \color{black}{a_{2, 1}} & \color{gray}{a_{2, 2}} \end{array} \right] , g = \left[ \begin{array}{ccc} \color{gray}{b_{-1, -1}} & \color{black}{b_{-1, 0}} & \color{purple}{b_{-1, 1}} \\ \color{blue}{b_{0, -1}} & \color{cyan}{b_{0, 0}} & \color{green}{b_{0, 1}} \\ \color{yellow}{b_{1, -1}} & \color{orange}{b_{1, 0}} & \color{red}{b_{1, 1}} \end{array} \right] $$ 则 $\left(f * g\right) \left(1, 1\right)$ 计算方式如下: $$ \left(f * g\right) \left(1, 1\right) = \sum_{v = 0}^{2}{\sum_{h = 0}^{2}{f \left(h, v\right) g \left(1 - h, 1 - v\right)}} $$ 从这个计算公式中我们就不难看出为什么上面的 $f, g$ 两个概率矩阵的角标会写成上述形式,即两个矩阵相同位置的角标之和均为 $1$。$\left(f * g\right) \left(1, 1\right)$ 即为 $f, g$ 两个矩阵中对应颜色的元素乘积之和。 在上例中,$f, g$ 两个概率矩阵的大小相同,而在 CNN 中,$f$ 为输入的图像,$g$ 一般是一个相对较小的矩阵,我们称之为卷积核。这种情况下,卷积的计算方式是类似的,只是会将 $g$ 矩阵旋转 $180^{\circ}$ 使得相乘的元素的位置也相同,同时需要 $g$ 在 $f$ 上进行滑动并计算对应位置的卷积值。下图 15 展示了一步计算的具体过程: 下图 15 形象的刻画了利用一个 3x3 大小的卷积核的整个卷积计算过程: 一些预设的卷积核对于图片可以起到不同的滤波器效果,例如下面 4 个卷积核分别会对图像产生不同的效果:不改变,边缘检测,锐化和高斯模糊。 $$ \left[ \begin{array}{ccc} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{array} \right] , \left[ \begin{array}{ccc} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{array} \right] , \left[ \begin{array}{ccc} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{array} \right] , \dfrac{1}{16} \left[ \begin{array}{ccc} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{array} \right] $$ 对 lena 图片应用这 4 个卷积核,变换后的效果如下 (从左到右,从上到下): 在上面整个计算卷积的动图中,我们不难发现,利用 3x3 大小 (我们一般将这个参数称之为 kernel_size,即卷积核的大小,其可以为一个整数表示长宽大小相同,也可以为两个不同的整数) 的卷积核对 5x5 大小的原始矩阵进行卷积操作后,结果矩阵并没有保持原来的大小,而是变为了 (5-(3-1))x(5-(3-1)) (即 3x3) 大小的矩阵。这就需要引入 CNN 网络中卷积层的两个常用参数 padding 和 strides。 padding 是指是否对图像的外侧进行补零操作,其取值一般为 VALID 和 SAME 两种。VALID 表示不进行补零操作,对于输入形状为 $\left(x, y\right)$ 的矩阵,利用形状为 $\left(m, n\right)$ 的卷积核进行卷积,得到的结果矩阵的形状则为 $\left(x-m+1, y-n+1\right)$。SAME 表示进行补零操作,在进行卷积操作前,会对图像的四个边缘分别向左右补充 $\left(m \mid 2 \right) + 1$ 个零,向上下补充 $\left(n \mid 2 \right) + 1$ 个零 ($\mid$ 表示整除),从而保证进行卷积操作后,结果的形状与原图像的形状保持相同,如下图 15 所示: strides 是指进行卷积操作时,每次卷积核移动的步长。示例中,卷积核在横轴和纵轴方向上的移动步长均为 $1$,除此之外用于也可以指定不同的步长。移动的步长同样会对卷积后的结果的形状产生影响。 除此之外,还有另一个重要的参数 filters,其表示在一个卷积层中使用的卷积核的个数。在一个卷积层中,一个卷积核可以学习并提取图像的一种特征,但往往图片中包含多种不同的特征信息,因此我们需要多个不同的卷积核提取不同的特征。下图 15 是一个利用 4 个不同的卷积核对一张图像进行卷积操作的示意图: 上面我们都是以一个灰度图像 (仅包含 1 个通道) 为示例进行的讨论,那么对于一个 RGB 图像 (包含 3 个通道),相应的,卷积核也是一个 3 维的形状,如下图 15 所示: 卷积层对于我们的神经网络的模型带来的改进主要包括如下三个方面:稀疏交互 (sparse interactions),参数共享 (parameter sharing) 和等变表示 (equivariant representations)。 在全连接的神经网络中,隐含层中的每一个节点都和上一层的所有节点相连,同时有被连接到下一层的全部节点。而卷积层不同,节点之间的连接性受到卷积核大小的制约。下图 16 分别以自下而上 (左) 和自上而下 (右) 两个角度对比了卷积层和全连接层节点之间连接性的差异。 在上图 (右) 中,我们可以看出节点 $s_3$ 受到节点 $x_2$,$x_3$ 和 $x_4$ 的影响,这些节点被称之为 $s_3$ 的接受域 (receptive field)。稀疏交互使得在 $m$ 个输入和 $n$ 个输出的情况下,参数的个数由 $m \times n$ 个减少至 $k \times n$ 个,其中 $k$ 为卷积核的大小。尽管一个节点在一个层级之间仅与其接受域内的节点相关联,但是对于深层中的节点,其与绝大部分输入之间却存在这间接交互,如下图 16 所示: 节点 $g_3$ 尽管直接的连接是稀疏的,但处于更深的层中可以间接的连接到全部或者大部分的输入节点。这就使得网络可以仅通过这种稀疏交互来高效的描述多个输入变量之间的复杂关系。 除了稀疏交互带来的参数个数减少外,参数共享也起到了类似的作用。所谓参数共享就是指在进行不同操作时使用相同的参数,具体而言也就是在我们利用卷积核在图像上滑动计算卷积时,每一步使用的卷积核都是相同的。同全连接网络的情况对比如下图 16 所示: 在全连接网络 (上图 - 下) 中,任意两个节点之间的连接 (权重) 仅用于这两个节点之间,而在卷积层中,如上图所示,其对卷积核中间节点 (黑色箭头) 的使用方式 (权重) 是相同的。参数共享虽然对于计算的时间复杂度没有带来改进,仍然是 $O \left(k \times n\right)$,但其却将参数个数降低至 $k$ 个。 正是由于参数共享机制,使得卷积层具有平移 等变 (equivariance) 的性质。对于函数 $f\left(x\right)$ 和 $g\left(x\right)$,如果满足 $f\left(g\left(x\right)\right) = g\left(f\left(x\right)\right)$,我们就称 $f\left(x\right)$ 对于变换 $g$ 具有等变性。简言之,对于图像如果我们将所有的像素点进行移动,则卷积后的输出表示也会移动同样的量。 非线性层 非线性层并不是 CNN 特有的网络层,在此我们不再详细介绍,一般情况下我们会使用 ReLU 作为我们的激活函数。 池化层 池化层 是一个利用 池化函数 (pooling function) 对网络输出进行进一步调整的网络层。池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出。常用的池化函数包括最大池化 (max pooling) 函数 (即给出邻域内的最大值) 和平均池化 (average pooling) 函数 (即给出邻域内的平均值) 等。但无论选择何种池化函数,当对输入做出少量平移时,池化对输入的表示都近似 不变 (invariant)。局部平移不变性 是一个很重要的性质,尤其是当我们关心某个特征是否出现而不关心它出现的位置时。 池化层同卷积层类似,具有三个比较重要的参数:pool_size,strides 和 padding,分别表示池化窗口的大小,步长以及是否对图像的外侧进行补零操作。下图 16 是一个 pool_size=3,strides=3,padding='valid' 的最大池化过程示例: 池化层同时也能够提高网络的计算效率,例如上图中在横轴和纵轴的步长均为 $3$,经过池化后,下一层网络节点的个数降低至前一层的 $\frac{1}{3 \times 3} = \frac{1}{9}$。 全连接层 全链接层 (Fully-connected or Dense Layer) 的目的就是将我们最后一个池化层的输出连接到最终的输出节点上。例如,最后一个池化层的输出大小为 $\left[5 \times 5 \times 16\right]$,也就是有 $5 \times 5 \times 16 = 400$ 个节点,对于手写数字识别的问题,我们的输出为 0 至 9 共 10 个数字,采用 one-hot 编码的话,输出层共 10 个节点。例如在 LeNet 中有 2 个全连接层,每层的节点数分别为 120 和 84,在实际应用中,通常全连接层的节点数会逐层递减。需要注意的是,在进行编码的时候,第一个全连接层并不是直接与最后一个池化层相连,而是先对池化层进行 flatten 操作,使其变成一个一维向量后再与全连接层相连。 输出层 输出层根据具体问题的不同会略有不同,例如对于手写数字识别问题,采用 one-hot 编码的话,输出层则包含 10 个节点。对于回归或二分类问题,输出层则仅包含 1 个节点。当然对于二分类问题,我们也可以像多分类问题一样将其利用 one-hot 进行编码,例如 $\left[1, 0\right]$ 表示类型 0,$\left[0, 1\right]$ 表示类型 1。 扩展与应用 本节我们将介绍一些经典的 CNN 网络架构及其相关的改进。 AlexNet 2 AlexNet 在整体结构上同 LeNet-5 类似,其改进大致如下: 网络包含了 5 个卷积层和 3 个全连接层,网络规模变大。 使用了 ReLU 非线性激活函数。 应用了 Data Augmentation,Dropout,Momentum,Weight Decay 等策略改进训练。 在算力有限的情况下,对模型进行划分为两部分并行计算。 增加局部响应归一化 (LRN, Local Response Normalization)。 LRN 的思想来自与生物学中侧抑制 (Lateral Inhibition) 的概念,简单来说就是相近的神经元之间会发生抑制作用。在 AlexNet 中,给出的 LRN 计算公式如下: $$ b_{x,y}^{i} = a_{x,y}^{i} / \left(k + \alpha \sum_{j = \max \left(0, i - n/2\right)}^{\min \left(N - 1, i + n/2\right)}{\left(a_{x,y}^{j}\right)^2}\right)^{\beta} $$ 其中,$a_{x,y}^{i}$ 表示第 $i$ 个卷积核在位置 $\left(x,y\right)$ 的输出,$N$ 为卷积核的个数,$k, n, \alpha, \beta$ 均为超参数,在原文中分别初值为:$k=2, n=5, \alpha=10^{-4}, \beta=0.75$。在上式中,分母为所有卷积核 (Feature Maps) 的加和,因此 LRN 可以简单理解为一个跨 Feature Maps 的像素级归一化。 开源实现: tensorflow/models, tflearn/examples pytorch/torchvision/models caffe2/models incubator-mxnet/example VGG Net 4 左图是 VGG-16 Net 的网络结构,原文中还有一个 VGG-19 Net,其差别在于后面三组卷积层中均多叠加了一个卷积层,使得网络层数由 16 增加至 19。 VGG Net 的主要改变如下: 网络层级更深,从 AlexNet 的 8 层增加至 16 层和 19 层,更深的网络层级意味着更强的学习能力,但也需要更多的算力对模型进行优化。 仅使用 3x3 大小的卷积。在 AlexNet 中,浅层部分使用了较大的卷积核,而 VGG 使用了 3x3 的小卷积核进行串联叠加,减少了参数个数。 卷积采样的步长为 1x1,Max Pooling 的步长为 2x2。 去掉了效果提升不明显的但计算耗时的 LRN。 增加了 Feature Maps 的个数。 开源实现: tensorflow/models, tflearn/examples, tensorlayer/awesome-tensorlayer tf/keras/applications, keras/applications pytorch/torchvision/models caffe2/models incubator-mxnet/example Network in Network (NIN) 17 NIN 网络的主要改变如下: 利用多层的全连接网络替换线性的卷积,即 mlpconv (Conv + MLP) 层。其中卷积层为线性的操作,而 MLP 为非线性的操作,因此具有更高的抽象能力。 去掉了全连接层,使用 Global Average Pooling,也就是将每个 Feature Maps 上所有的值求平均,直接作为输出节点,如下图所示: 相比 AlexNet 简化了网络结构,仅包含 4 个 NIN 单元和一个 Global Average Pooling,整个参数空间比 AlexNet 小了一个数量级。 在 NIN 中,在跨通道的情况下,mlpconv 层又等价于传统的 Conv 层后接一个 1x1 大小的卷积层,因此 mlpconv 层有时也称为 cccp (cascaded cross channel parametric pooling) 层。1x1 大小的卷积核可以说实现了不同通道信息的交互和整合,同时对于输入通道为 $m$ 和输出通道为 $n$,1x1 大小的卷积核在不改变分辨率的同时实现了降维 ($m > n$ 情况下) 或升维 ($m < n$ 情况下) 操作。 开源实现: tflearn/examples GoogLeNet (Inception V1) 5, Inception V3 18, Inception V4 19 除了 VGG 这种从网络深度方向进行优化的策略以外,Google 还提出了在同一层利用不同大小的卷积核同时提取不同特征的思路,对于这样的结构我们称之为 Inception。 上图 (左) 为原始的 Inception 结构,在这样一层中分别包括了 1x1 卷积,3x3 卷积,5x5 卷积和 3x3 Max Polling,使得网络在每一层都能学到不同尺度的特征。最后通过 Filter Concat 将其拼接为多个 Feature Maps。 这种方式虽然能够带来性能的提升,但同时也增加了计算量,因此为了进一步改善,其选择利用 1x1 大小的卷积进行降维操作,改进后的 Inception 模块如上图 (右) 所示。我们以 GoogLeNet 中的 inception (3a) 模块为例 (输入大小为 28x28x192),解释 1x1 卷积的降维效果。 对于原始 Inception 模块,1x1 卷积的通道为 64,3x3 卷积的通道为 128,5x5 卷积的通道为 32,卷积层的参数个数为: $$ \begin{equation} \begin{split} \# w_{\text{3a\_conv\_without\_1x1}} =& 1 \times 1 \times 192 \times 64 \\ & + 3 \times 3 \times 192 \times 128 \\ & + 5 \times 5 \times 192 \times 32 \\ =& 387072 \end{split} \end{equation} $$ 对于加上 1x1 卷积后的 Inception 模块 (通道数分别为 96 和 16) 后,卷积层的参数个数为: $$ \begin{equation} \begin{split} \# w_{\text{3a\_conv\_with\_1x1}} =& 1 \times 1 \times 192 \times 64 \\ & + 1 \times 1 \times 192 \times 96 + 3 \times 3 \times 96 \times 128 \\ & + 1 \times 1 \times 192 \times 16 + 5 \times 5 \times 16 \times 32 \\ =& 157184 \end{split} \end{equation} $$ 可以看出,在添加 1x1 大小的卷积后,参数的个数减少了 2 倍多。通过 1x1 卷积对特征进行降维的层称之为 Bottleneck Layer 或 Bottleneck Block。 在 GoogLeNet 中,作者还提出了 Auxiliary Classifiers (AC),用于辅助训练。AC 通过增加浅层的梯度来减轻深度梯度弥散的问题,从而加速整个网络的收敛。 随后 Google 在此对 Inception 进行了改进,同时提出了卷积神经网络的 4 项设计原则,概括如下: 避免表示瓶颈,尤其是在网络的浅层部分。一般来说,在到达任务的最终表示之前,表示的大小应该从输入到输出缓慢减小。 高维特征在网络的局部更容易处理。在网络中增加更多的非线性有助于获得更多的解耦特征,同时网络训练也会加快。 空间聚合可以在低维嵌入中进行,同时也不会对表征能力带来太多影响。例如,再进行尺寸较大的卷积操作之前可以先对输入进行降维处理。 在网络的宽度和深度之间进行权衡。通过增加网络的深度和宽度均能够带来性能的提升,在同时增加其深度和宽度时,需要综合考虑算力的分配。 Inception V3 的主要改进包括: 增加了 Batch Normalized 层。 将一个 5x5 的卷积替换为两个串联的 3x3 的卷积 (基于原则 3),减少了网络参数,如下图所示: 利用串联的 1xn 和 nx1 的非对称卷积 (Asymmetric Convolutions) 替代 nxn 的卷积 (基于原则 3),减少了网络参数,如下图 (左) 所示: 增加带有滤波器组 (filter bank) 的 Inception 模块 (基于原则 2),用于提升高维空间的表示能力,如下图 (右) 所示: 重新探讨了 Auxiliary Classifiers 的作用,发现其在训练初期并没有有效的提高收敛速度,尽在训练快结束时会略微提高网络的精度。 新的下采样方案。在传统的做法中,如果先进行 Pooling,在利用 Inception 模块进行操作,如下图 (左) 所示,会造成表示瓶颈 (原则 1);而先利用 Inception 模块进行操作,再进行 Pooling,则会增加参数数量。 Label Smoothing 机制。假设标签的真实分布为 $q\left(k\right)$,则对于一个真实标签 $y$ 而言,有 $q\left(y\right) = 1$,对于 $k \neq y$,有 $q\left(k\right) = 0$。这种情况会导致两个问题:一个是当模型对于每个训练样本的真实标签赋予全部的概率时,模型将会发生过拟合;另一个是其鼓励拉大最大概率标签同其他标签之间的概率差距,从而降低网络的适应性。也就是说这种情况的发生是由于网络对于其预测结果过于自信。因此,对于一个真实标签 $y$,我们将其标签的分布 $q\left(k | x\right) = \delta_{k, y}$ 替换为: $$ q' \left(k | x\right) = \left(1 - \epsilon\right) \delta_{k, y} + \epsilon u \left(k\right) $$ 其中,$u \left(k\right)$ 是一个固定的分布,文中采用了均匀分布,即 $u \left(k\right) = 1 / K$;$\epsilon$ 为权重项,试验中取为 $0.1$。 Inception V4 对于 Inception 网络做了进一步细致的调整,其主要是将 Inception V3 中的前几层网络替换为了 stem 模块,具体的 stem 模块结构就不在此详细介绍了。 开源实现: tensorflow/models, tflearn/examples, tensorlayer/awesome-tensorlayer tf/keras/applications, keras/applications pytorch/torchvision/models caffe2/models incubator-mxnet/example Deep Residual Net 6, Identity Mapping Residual Net 20, DenseNet 21 随着网络深度的不断增加啊,其效果并未如想象一般提升,甚至发生了退化,He 等人 6 发现在 CIFAR-10 数据集上,一个 56 层的神经网络的性能要比一个 20 层的神经网络要差。网络层级的不断增加,不仅导致参数的增加,同时也可能导致梯度弥散问题 (vanishing gradients)。 这对这些问题,He 等人提出了一种 Deep Residual Net,在这个网络结构中,残差 (residual) 的思想可以理解为:假设原始潜在的映射关系为 $\mathcal{H} \left(\mathbf{x}\right)$,对于新的网络层我们不再拟合原始的映射关系,而是拟合 $\mathcal{F} \left(\mathbf{x}\right) = \mathcal{H} \left(\mathbf{x}\right) - \mathbf{x}$,也就是说原始潜在的映射关系变为 $\mathcal{F} \left(\mathbf{x}\right) + \mathbf{x}$。新的映射关系可以理解为在网络前向传播中添加了一条捷径 (shortcut connections),如下图所示: 增加 Short Connections 并没有增加参数个数,也没有增加计算量,与此同时模型依旧可以利用 SGD 等算法进行优化。 从 Deep Residual Net 的实验结果 (如上图) 可以看出,在没有加入残差模块的网络中 (上图 - 左) 出现了上文中描述的问题:更多层级的网络的效果反而较差;在加入了残差模块的网络中 (上图 - 右),其整体性能均比未加入残差模块的网络要好,同时具有更多层级的网络的效果也更好。 随后 He 等人 20 又提出了 Identity Mapping Residual Net,在原始的 ResNet 中,一个残差单元可以表示为: $$ \begin{equation} \begin{split} \mathbb{y}_{\ell} = & h \left(\mathbb{x}_{\ell}\right) + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_l\right) \\ \mathbb{x}_{\ell+1} = & f \left(\mathbb{y}_{\ell}\right) \end{split} \end{equation} $$ 其中 $\mathbb{x}_{\ell}$ 和 $\mathbb{x}_{\ell+1}$ 为第 $\ell$ 个单元的输入和输出,$\mathcal{F}$ 为残差函数,$h \left(\mathbb{x}_{\ell}\right) = \mathbb{x}_{\ell}$ 为一个恒等映射,$f$ 为 ReLU 函数。在 Identity Mapping Residual Net,作者将 $f$ 由原来的 ReLU 函数也替换成一个恒定映射,即 $\mathbb{x}_{\ell+1} \equiv \mathbb{y}_{\ell}$,则上式可以改写为: $$ \mathbb{x}_{\ell+1} = \mathbb{x}_{\ell} + \mathcal{F} \left(\mathbb{x}_{\ell}, \mathcal{W}_{\ell}\right) $$ 则对于任意深度的单元 $L$,有如下表示: $$ \mathbb{x}_L = \mathbb{x}_{\ell} + \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)} $$ 上式形式的表示使得其在反向传播中具有一个很好的性质,假设损失函数为 $\mathcal{E}$,根据链式法则,对于单元 $\ell$,梯度为: $$ \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_L} \dfrac{\partial \mathbb{x}_L}{\partial \mathbb{x}_{\ell}} = \dfrac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(1 + \dfrac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right) $$ 对于上式形式的梯度,我们可以将其拆解为两部分:$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}}$ 为不通过任何权重层的直接梯度传递,$\frac{\partial \mathcal{E}}{\partial \mathbb{x}_{\ell}} \left(\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}{\mathcal{F} \left(\mathbb{x}_i, \mathcal{W}_i\right)}\right)$ 为通过权重层的梯度传递。前一项保证了梯度能够直接传回任意浅层 $\ell$,同时对于任意一个 mini-batch 的所有样本,$\frac{\partial}{\partial \mathbb{x}_{\ell}} \sum_{i=\ell}^{L-1}\mathcal{F}$ 不可能永远为 $-1$,所以保证了即使权重很小的情况下也不会出现梯度弥散。下图展示了原始的 ResNet 和 Identity Mapping Residual Net 之间残差单元的区别和网络的性能差异: Huang 等人 21 在 ResNet 的基础上又提出了 DenseNet 网络,其网络结构如下所示: DenseNet 的主要改进如下: Dense Connectivity:将网络中每一层都与其后续层进行直接连接。 Growth Rate:$H_{\ell}$ 将产生 $k$ 个 Feature Maps,因此第 $\ell$ 层将包含 $k_0 + k \times \left(\ell - 1\right)$ 个 Feature Maps,其中 $k_0$ 为输入层的通道数。DenseNet 与现有框架的不同之处就是将网络限定的比较窄,例如:$k = 12$,并将该超参数称之为网络的增长率 (Growth Rate)。 Bottleneck Layers:在 3x3 的卷积之前增加 1x1 的卷积进行降维操作。 Compression:在两个 Dense Block 之间增加过渡层 (Transition Layer),进一步减少 Feature Maps 个数。 开源实现: tensorflow/models, tflearn/examples, tensorlayer/awesome-tensorlayer tf/keras/applications, keras/applications pytorch/torchvision/models caffe2/models incubator-mxnet/example 综合比较 Canziani 等人 22 综合了模型的准确率,参数大小,内存占用,推理时间等多个角度对现有的 CNN 模型进行了对比分析。 上图 (左) 展示了在 ImageNet 挑战赛中不同 CNN 网络模型的 Top-1 的准确率。可以看出近期的 ResNet 和 Inception 架构以至少 7% 的显著优势超过了其他架构。上图 (右) 以另一种形式展现了除了准确率以外的更多信息,包括计算成本和网络的参数个数,其中横轴为计算成本,纵轴为 Top-1 的准确率,气泡的大小为网络的参数个数。可以看出 ResNet 和 Inception 架构相比 AlexNet 和 VGG 不仅有更高的准确率,其在计算成本和网络的参数个数 (模型大小) 方面也具有一定优势。 文章部分内容参考了 刘昕 的 CNN近期进展与实用技巧。CNN 除了在图像分类问题上取得很大的进展外,在例如:物体检测:R-CNN 23, SPP-Net 24, Fast R-CNN 25, Faster R-CNN 26,语义分割:FCN 27 等多个领域也取得了不俗的成绩。针对不同的应用场景,网络模型和处理方法均有一定的差异,本文就不再对其他场景一一展开说明,不同场景将在后续进行单独整理。 LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324. ↩︎ Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems (pp. 1097-1105). ↩︎ ↩︎ http://www.image-net.org/ ↩︎ Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556. ↩︎ ↩︎ Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., … & Rabinovich, A. (2015). Going deeper with convolutions. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 1-9). ↩︎ ↩︎ He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778). ↩︎ ↩︎ ↩︎ Girshick, R., Donahue, J., Darrell, T., & Malik, J. (2014). Rich feature hierarchies for accurate object detection and semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 580-587). ↩︎ Girshick, R. (2015). Fast r-cnn. In Proceedings of the IEEE international conference on computer vision (pp. 1440-1448). ↩︎ Ren, S., He, K., Girshick, R., & Sun, J. (2015). Faster r-cnn: Towards real-time object detection with region proposal networks. In Advances in neural information processing systems (pp. 91-99). ↩︎ Long, J., Shelhamer, E., & Darrell, T. (2015). Fully convolutional networks for semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3431-3440). ↩︎ Vinyals, O., Toshev, A., Bengio, S., & Erhan, D. (2015). Show and tell: A neural image caption generator. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3156-3164). ↩︎ Ji, S., Xu, W., Yang, M., & Yu, K. (2013). 3D convolutional neural networks for human action recognition. IEEE transactions on pattern analysis and machine intelligence, 35(1), 221-231. ↩︎ Kim, Y. (2014). Convolutional neural networks for sentence classification. arXiv preprint arXiv:1408.5882. ↩︎ http://yann.lecun.com/exdb/mnist ↩︎ https://mlnotebook.github.io/post/CNN1/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ Goodfellow, I., Bengio, Y., Courville, A., & Bengio, Y. (2016). Deep learning (Vol. 1). Cambridge: MIT press. ↩︎ ↩︎ ↩︎ ↩︎ Lin, M., Chen, Q., & Yan, S. (2013). Network In Network. arXiv preprint arXiv:1312.4400. ↩︎ Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., & Wojna, Z. (2016). Rethinking the Inception Architecture for Computer Vision. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (pp. 2818–2826). ↩︎ Szegedy, C., Ioffe, S., Vanhoucke, V., & Alemi, A. (2016). Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning. arXiv preprint arXiv:1602.07261. ↩︎ He, K., Zhang, X., Ren, S., & Sun, J. (2016). Identity Mappings in Deep Residual Networks. arXiv preprint arXiv:1603.05027. ↩︎ ↩︎ Huang, G., Liu, Z., van der Maaten, L., & Weinberger, K. Q. (2016). Densely Connected Convolutional Networks. arXiv preprint arXiv:1608.06993 ↩︎ ↩︎ Canziani, A., Paszke, A., & Culurciello, E. (2016). An Analysis of Deep Neural Network Models for Practical Applications. arXiv preprint arXiv:1605.07678 ↩︎ Girshick, R., Donahue, J., Darrell, T., & Malik, J. (2014). Rich Feature Hierarchies for Accurate Object Detection and Semantic Segmentation. In Proceedings of the 2014 IEEE Conference on Computer Vision and Pattern Recognition (pp. 580–587). ↩︎ He, K., Zhang, X., Ren, S., & Sun, J. (2015). Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition. IEEE Transactions on Pattern Analysis and Machine Intelligence, 37(9), 1904–1916. ↩︎ Girshick, R. (2015). Fast R-CNN. arXiv preprint arXiv:1504.08083. ↩︎ Ren, S., He, K., Girshick, R., & Sun, J. (2017). Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks. IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(6), 1137–1149. ↩︎ Shelhamer, E., Long, J., & Darrell, T. (2017). Fully Convolutional Networks for Semantic Segmentation. IEEE Transactions on Pattern Analysis and Machine Intelligence, 39(4), 640–651. ↩︎

2018/8/25
articleCard.readMore

买书,囤书,看书 (Buy Books, Hoard Books and Read Books)

写这么一篇主要是想自我分析一下和读书有关的毛病,写下来会让自己认识的深刻些。尤其是最近一年,书入库明显高于出库,导致未读完的书都快摆不下了。总结起来主要是两个问题: 书买了,不看。 书看了,看不完。 买书 先聊聊为啥买书,简单通俗的解释就是“想看”。我认为,这么回答一点毛病都没有,因为这是我们去探索这个未知的世界最容易的方式。古人云:“读万卷书,行万里路”,都是探索这个世界的方式,后来细查得知这句话源自于董其昌谈及绘画之道时所说 1: 画家六法,一曰气韵生动。气韵不可学,此生而知之,自然天授,然亦有学得处。**读万卷书,行万里路。**胸中脱去尘浊,自然丘壑内营,成立郛郭,随手写去,皆为山水传神。 为什么说读书是其中最容易的方式呢?最主要就是经济实惠,你不需要太多的成本,包括财物和时间,当然是相对行万里路而言。这里谈及的书我不想把杂志和技术书籍囊括进来,于我而言,杂志是消磨零散时间的读物,例如:如厕,等车;而技术类的书籍有需要比较系统的整理和笔记,甚至需要反复去看和理解。其他书也可以反复看,有时候我会把喜欢的书再翻一遍,而且每一遍都会有新的发现。所以,想去买书至少能够说明你还有对知识的渴望和对未知探索的兴趣。这点我认为很重要,如果你连念想都没了,那就什么都没了。 囤书 囤书,就是买得多了,而且略微上瘾,想着可能买了就等于已经把其中的知识吃进脑子了吧。这点和我办健身卡很像,感觉卡办了,八块腹肌就有了似的,不过还好,至少我把跑步坚持下来了,关于这个类比的问题我们下面再详谈。其实,囤书本身也没有错,什么 618 啊,双 11 啊,书就是便宜吗,多买点实惠。错就错在了我们只做了“囤”的前半部,丢失了后半部。“囤”的后半部我认为是计划的制定,这其实直接决定着我们后续要说的执行。就和囤粮食一样,我们是做了详细的计划去应对未来的粮食短缺的,而不是今年收成好了就一定多囤一点,不过似乎也会有这样的趋势,和上面说书的价格类似吗。 这里说的意思就是计划的重要性,不要在脑子里想,我要干什么干什么,尤其是和我脑子一样不好用的同学,还是先写下来,坚持按照计划执行一段时间,等熟悉了,习惯养成了,再靠脑子才比较好。想想我读书最“舒服” (感觉写读的最快又不太合适,毕竟不应该以读多少,读多块论好坏) 的一段时间是我研究生期间的第一份实习,当时每天翻越 4 条地铁,从昌平线的沙河高教园到中关村。每天都很规律,早起听一路的歌,在 13 号线的换乘站看着七八辆车过去,然后被后面的人成功的推上列车;晚上也带上耳机,但不放歌,为的是减少噪音,上车找个角落,不坐,站着,用手机看一路的电子书。换乘点停一停,甚至还能简单的做些思考。就这样,不算长的实习期里看完了好几本书。这段时间虽然没有形成规范的计划,但是每天规律的上下班无形之中就促成了计划的按时执行。 看书 回到最初的两个毛病,“不看”和“看不完”。好吧,先找些客观理由,时间真的不多。记得当时看《解忧杂货铺》的时候,应该是晚上十点多开始看到,一晚上看完了,结束应该已经两三点了。作为一个靠撸代码为生的人,明天还要准时上班工作,在中国这样一个竞争激烈市场中,报答还是和加班呈强正相关的。读《解忧杂货铺》的时候,其实是不知不觉就读完了,当然不知不觉也就两三点了。所以,我认为能够促使你读完的最重要的还是内容本身,是你喜欢的风格,就不会错过。尤其是书的前几章,前面写的不好,可能就会让你失去继续读下去的兴趣。还有就是书的厚度,之前买了几本很喜欢的书,喜欢是从其题材和评价先预支的结论,但碍于实在太厚,就迟迟未读,也怕看了没看完,还不如等做好准备再去读。 所以如上文提及的,读书之于健身,能把跑步坚持下来是因为时间不长,30 分钟能跑个不到 6 公里,跑完还很舒服,会感觉收益很高。而以增肌为目的的撸铁,对于我这种本来上肢力量就很弱的男生,短时间不见效果就放弃了。但无论是健身还是读书,这个想法也都是不对的,而且我是“一直”都知道是不对的。 对于我,简单思考之后,我想最主要的问题是缺乏一种强烈的约束力去推着你动起来。我算是一个自制力一般的人,有人鞭笞我几下,效果一下子就会上来,没人鞭笞我了,就容易出现不好不差的状态。不过与其说是缺乏外界的动力,不如说还是没有触及当下的痛点。我没有八块腹肌,但也没有肚子,做不到穿衣显瘦,脱衣有肉,但也还算看得过去。同时我知道,程序员不动是一定会长肚子的,身边的例子比比皆是。所以作为半个外貌协会的会员,我能够坚持跑步。同样,读书真的是我喜欢的事情,看电影也一样,两者各自有各自体验,但当下生活的压力促使我会投入更多的时间在工作相关的领域,自然留出来给读书的时间就少了。 总结起来,如何改变这个状态,还是要制定个读书计划的,各种情况难免无法严格执行,但尽量去做。还有就是当你不开心的时候,我认为是适合读书,这个想法来自于一次周末的阅读体验。五天的工作日结束,甚是不爽,为啥就不说了,总之不爽,周末来公司,本来想再敲几行代码,心情原因,效率如实不高。翻开了之前没有带回家的冯唐的《在宇宙间不易被风吹散》,冯唐的东西总是有点他特别的地方。一个下午,几杯咖啡,一本书,慢慢的就过了,然后不开心的东西就散了。还不忘发个朋友圈装下文艺青年: 人燥的时候就多做些通灵的事,无需如鼓琴,对画,临帖一般高雅,一书,一茶足矣,静下来,腐朽之气全无。 最后,再补一下我对读书这件事我们需要以何种态度去对待的看法,我认为:读书,不是非做不可的事,而是真正想要去做的事。 《画旨》- 董其昌 ↩︎

2018/7/10
articleCard.readMore

Play Safe, Smart Choice & Yuppie

最近是看了 Youtube 上 Wong Fu Productions 的一个系列视频 Yappie,一下子把脑子中包括很久之前的一些思考就全都串联回忆起来了,所以就写了本文,把这些凌乱的思考拼凑起来。截止到本文写完,Yappie 已经出了两集,视频可能需要梯子,为了不影响理解,简单勾勒一下剧情。 主角是一个名叫 Andrew 的亚裔美国人,他和女友 (当然后面知道才没交往多久,然后就分手了) 去听了一个关于美国亚裔 (Aisan Americans) 的一些特殊的境遇问题的 Talk Show。但 Andrew 似乎对这场演讲并不感冒,中途手机中球赛的声音打断了演讲,并被演讲者问及了姓名。Andrew 告知了姓名,演讲者随即用个玩笑回应了他的不礼貌打断,说一个美国亚裔中有 1/13 的概率叫 Andrew,有 1/5 的概率会是一名工程师,会来自于 San Gabriel Valley,喜欢篮球,Boyz II Men 和宝马 (然而这一系列看似玩笑的猜测,后面剧情验证,他就是这样的一个 Andrew)。演讲过后 Andrew 的女友和演讲者做着进一步的沟通,Andrew 过去为自己的不礼貌表示道歉,并想融入他们的谈话,但却完全和他们不在一个频道。然后,就没有然后了,他和女友就 OVER 了,并且女友说自己就不应该和一个 Yuppie 男约会。所以,什么是 Yuppie?其实他们的这段分手对白说的就很清楚了: Girl: I knew I shouldn’t have swiped on a Yuppie. Andrew: A what? Girl: You’re a Yuppie, Andrew. Andrew: Are we doing nicknames now or …? Girl: A young Asian professional who acts like a Yuppie? Cause all you care about is earning a nice salary, and buying a nice car, and settling down in a nice suburb. Andrew: That sounds like a nice, normal person. What’s so wrong with that? Girl: It’s not wrong. It’s just … safe. Listen, we’re just … not the right fit. I need to be with someone who you know … cares, more about the world. 后面,Andrew 回到公司后,貌似是人力的小姐姐要找他聊天,关于之前 Andrew 想进行岗位异动的事情。人力小姐姐说今年内是无法进行了,但 Andrew 也只是叹了叹气,说 that’s fine。这一次,他又选择了一个 Safe 的方式,或许他在想至少还能保留现在的职位。但在和人力小姐姐沟通的后面,他已经开始走神思考着什么了。同时,在第 2 集中得知 Andrew 与前女友 Lana 分手貌似也是因为对他这种 Safe 的生活方式的不认同,后面的剧情中 Andrew 已经开始对着这种 Safe 的生活方式进行了改变,就不在详细介绍了。 对与错,好与坏 Andrew 之所以很困惑为什么会叫他 Yuppie,是因为他认为他现在所做的事情 (赚钱,买好车,买豪宅) 是再正常不过的事情了,是对的 (Right),是好的 (Good),是明智的选择 (Smart Choice)。他不明白,为什么有人会对这些再正常不过的事情产生质疑。但是大家有没有想过: 什么是对,什么是错,什么是好,什么又是坏? 让我真正重视这些问题是源自哈佛大学的一门公开课 Justice: What’s The Right Thing To Do?,现在 Michael 教授也出了这门课程对应的书 《正義: 一場思辨之旅》。课程的一开始就抛出了经典的 电车难题 (Trolley Problem),问题如下图所示: 原始版本的两个问题都是有一辆快速行驶的电车,在电车当前行驶的轨道前面被绑了 5 个人,如果不采取任何措施的话,这 5 个人就会被电车轧死。假设你是唯一一个能够影响这件事情发生的人,在上图左面中的情况,你能够操纵一个轨道变换器,能够让电车变到另一条轨道上,但是这条轨道上也绑了一个人,电车会把这个人轧死;在上图右面中的情况,你站在一个桥上,你面前有个胖子,如果你把他退下去,靠他的身体就能逼停电车,但这个胖子会死掉,同时如果你跳下去是无法逼停电车的。问题就是,针对不同的情况你,你会如果选择? 在课堂上统计的结果是,左面的情况会有更多的人选择变换轨道,牺牲一个人,挽救 5 个人;而右面的情况则会有更少的人选择去推那个胖子。我会有同样的选择,理由是右面的情况另外一个人的死亡会和我“看似”有更直接的关系,因此导致我的负罪感也会更强,课程的后面又从功利主义和个人权利的角度对相关问题进行了讨论。 当然抛出这个问题的目的不是想让我们陷入这个哲学的问题无法自拔,而是说我们是不是应该想想是不是没有所谓的“真”的“对”,当然这有可能陷入“循环嵌套”的困境,什么又是“真”,不必纠结这个,理解这个意思就好。或许有人会说,“对”就是“对”啊,怎么还会有“真”的“对”,其实不难解释,学了这么多年数学,我们都清楚:结论是建立在一定的前提假设下的,如果假设变了,那么结果的“真”与“假”也可能会发生变化。除非,对,除非前提是公理或者公设,无需证明就是对的,好吧,那是不是又要陷进去了,“公理”就是“真”的“公理”吗?不要陷进去,就假设“公理”“真”的是“公理”,那么 Andrew 就是把太多东西当成“公理”了,我们很多人都是,也许亚裔,甚至是中国人这方面会更严重一些。那么,这个问题出在了哪儿呢? 思辨精神与教育 问题就出在了我们的思辨精神,或者说我们的教育,当然这里我们并不会过多的去批判教育的问题,因为存不存在问题需要你去思考。我们从小接触的教育是这样的,至少我是这样的:最重要的是学习,或者说考试拿高分,这是“绝对”“正确”的,因此如果考少了,你就有可能被老师或者父母揍一顿。同样,在父母的眼里“棍棒底下出孝子”可能是“绝对”“正确”的,因此才会揍我们。所以,为了避免皮肉之苦,就只能好好学习,同时不允许一切有碍我们学习的东西出现。学习本身“似乎”是“绝对”“正确”的,尤其是在一定的社会条件下,好好学习可能是一个我们实现自我实现的一个很好的途径。我只是认为,这不能成为我们不做一些事情的理由,看动画片“似乎”会让我们学习的时间变少,所以可能很多家长的做法就是不让平时看动画片。然而,在这样的情况下,我的学业也还是一般般 😂。 如果我们一昧的强调某些事情是“绝对”“正确”的,一些事情是“绝对”“错误”的,最终导致的就是我们会终将失去思辨的精神。对于个人的成长来说,在前期可能还不会发现有多大的影响,但当你脱离父母,学校等这些保护伞后,我认为思辨精神可以说是能够让我们理解生存的本质,生活的意义以及如何获得多彩人生的重要思想。 下图是我从 Birdbox Studio 的 Wildbeest 视频中截取的一段做成的动图。动画的内容比较简单,但却相当的讽刺:前面的角马坚持河里面的是鳄鱼,但后面的角马却“一再”地说是木头,无论是出于想验证自己的想法还是对自己想法的质疑,前面的角马最后被鳄鱼吃掉了,这时后面的角马认识到了这“真的”是鳄鱼,更讽刺的是再后面的角马却又说这是木头。 我感觉用这个小视频来比喻我们的问题还是很恰当的,一昧的以家长的姿态强调什么是“对的”,什么是“错的”,最终会让孩子失去思辨的精神。而且这种问题是会传递的,可能将来你的孩子对于他的孩子也会存在这种“不当”的教育,又来了,我所说的这种“不当”是不是“真”的“不当”,取决于你的思考和理解。 当下,随着父母的知识水平的不断提升,开始认识到单纯的自然科学学习已经不够了,又开始给孩子们报各种兴趣班,而且是强制报名让孩子去学习。我认为这是同样的问题,只不过是父母角度的“绝对”“正确”又多了一项,就是还必须得有个特长。对于孩子而言,父母毕竟走过了更多的人生道路,也遇见过更多的问题,有远比孩子丰富的阅历,但我认为更加合适的方式是去“引导”,去让孩子尝试不同的东西,同时也给孩子一个思考和选择的机会。 其实,我认为思辨精神其实是我们人与生俱来的,最明显的表现就是小孩子永远爱问十万个为什么?所以,我认为对待孩子的问题要有耐心,要不厌其烦的去回答,这样才不会让孩子的这种性格消失。而且这是个正向反馈的过程,问的多了,得到的解释多了,思考多了,才会在下一次提问前更多的思考,提出少但更具意义的问题,同时又不会失去这种思辨的精神。 我还没有小孩,所以说了一大堆孩子教育的问题可能会有很多不当。除了孩子的思辨教育,作为一个成年人,对于我们自己又该如何去做呢?对于 Andrew,面对两任女友的不满,又该如何去改变呢? P.S. 关于思辨精神,推荐读过的一本不错的书,是一位台湾公民课教师 黃益中 的 《思辨: 熱血教師的十堂公民課 》,我感觉于自己和于未来孩子的教育都很有帮助 。当然对于思辨,我认为《中庸》中的「博學之,審問之,慎思之,明辨之,篤行之」与其含义是相通的,所以我们老祖宗很早就认识到了思辨的重要性。 P.S. 这段里面很多引号,引起来说明这些是我的观点,之于你,请慎思明辨。 迈出改变的一步 该如何去做,答案就是迈出改变的一步。当然我们首先要认识到我们需要改变,这点其实不易,因为我们并没有接触到很好的思辨教育,至少我是这样的,在毕业后才意识到思辨的重要性。很难认识到自己需要改变,我认为有如下几个原因: 对于生活的还算满意 有时候我们会遇到不爽,但不爽也会很快过去,因为我们会说:好吧,其实整体来看,Life is not bad. 是的,就是这个 NOT BAD 会让我们习惯下去,不去改变。在 Andrew 参加的 Talk Show 中,如演讲者说的那样,作为 Model Minority (模范少数族裔),我们过的还不错。 人际不没有想象的那么美好 我们有时候是很难发现自己的问题的,就像我们这群撸代码的一样,很难找到自己代码中所有的毛病,这也是测试工程师存在的理由。但在生活中,发现你的问题的人应该不少,但能够真诚的告诉你的我认为不会很多,这也就是为什么人生得一二知己足矣。Andrew 很幸运,两任女友都直白的和他说了真实的分手理由,没有搪塞。所以一个人说你有问题,你不一定真的有问题,但也需要反思,当说你有同一个问题的人多了,你就真的要反思了。炮弹落到同一个坑里面的概率很小,当真的都精准的打击到同一个地方时,那一定是这个地方的问题才让它成为众矢之的。 顾虑太多 认识到了问题,但还是不想改变,理由是如果我这样做了,那 XXX 怎么办 (比如家庭,工作等等)?会给自己找很多很多“理所当然”“对”的理由,我就经常这样,但我在努力改变中。太多的顾虑其实只是借口,可能当你走下去的时候,会发现 XXX 根本不是问题,甚至会比当下更好。当然,也可能会真的发生一些问题,如果真的发生了,要么想办法再解决它,要么再放弃之前的抉择,我认为也还来得及。所以,也不用把年龄当作借口,since it’s nevery too late to do it. 在迈出这一步的路上,我做的并不好,但我已经认识到了,所以在努力改变中。简单而言,对于一个事情,我们可以怎么做? THINK -> CHANGE -> IF 👍 THEN 😎 ELSE GOTO THINK AGAIN

2018/6/30
articleCard.readMore

基于 PyQt5/PySide2 和 QML 的跨平台 GUI 程序开发

先聊聊写界面化程序的目的,在 B/S 结构软件盛行的今天,C/S 结构的软件还有人用吗?答案是肯定的,至少你想用 B/S 结构的软件的时候你得有个 C/S 结构的浏览器,对吧?这样说显得有点抬杠,当然,我认为最重要的还是“简单”,或者说“用户友好”。再 Geek 的人应该也喜欢有的时候偷懒,虽然我称不上 Geek,但也经常在黑框框中不用鼠标敲着各种代码,但是还是希望能够有些小工具只要能够点个几下就能帮忙干些事情的。至于对于更普通的用户而言,就应该更加希望能够用最“简单,清晰,明了”的方式“快速”的完成一项任务,有点像 Windows 用户把桌面上的快捷方式拖到回收站,然后和我说:好了,程序卸载了,我只能回答说:或许你该换个 MAC。 ❗ 更新 ❗ SciHubEVA 最新版本已经采用 PySide2 进行改写,Windows 版本安装包构建工作迁移至 Inno Setup 6,更多变更请参见 CHANGELOG。 跨平台 GUI 程序开发方案选型 所以,写个带界面的小工具就是把你的想法更好的服务自己和别人的一个好途径,那么问题来了,对于我这做算法的种业余编程选手,怎么搞定界面化应用呢?虽然是业余编程选手,也也一路从 Logo,Basic,VB,C/C++,Java,R,Python 等等走来,当然很多都是从入门到放弃,总之对于同时需要兼顾一定美感的我,总结了几种跨平台界面化的解决方案。 JavaFX,基于 JVM,一次编译处处运行,配合 Material Design 风格的 JFoenix,应该是能写出很漂亮的界面的。 Qt,一次编写处处编译,配合 Qt Quick 和 QML,可以把前后端分离。原生 C++ 语言支持,同时有 Python 绑定,对于 Python 比较熟的同学相对友好。界面风格上在较新的 Qt Quick 中也支持了 Material Design 风格。 Electron,使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,很多优秀的应用都是用这个来搞的,例如:Visual Studio Code,Hyper 等。 我不认为这 3 种方法孰优孰劣,因为毕竟我们的目的是快速的搞定一个漂亮的小工具,因此到底选哪个完全取决于个人对相关技术的熟悉程度。因此,对于我这个搞算法的,最终选择了 Qt 的 Python 绑定 PyQt。作为 R 的忠实用户,实在是没找到特别好的解决方案,只能找个借口说我们 R 就不是干这个用的…… 环境配置 当然选择 PyQt 也是有些个人的倾向在里面的,写 C++ 的年代就用过 Qt,对于原理多少有些了解。不过针对 PyQt,以及其与 Qt Quick 和 QML 的结合使用在后面开发时发现相关文档比较少,只能一步一步地趟雷了。毕竟要做跨平台的 GUI 程序开发,因此本文会针对 macOS 和 Windows 两个系统做相关说明,Linux 系统由于发行版本太多就不做说明了,大部分情况应该和 macOS 类似。 Python (开发语言) Python 的版本选择了 3.5,因为在后面选择 3.6 时发现编译打包的时候会有些错误,没有细究,简单 Google 了此类问题,发现回退到 3.5 版本就没问题了,可能需要相关打包工具的更新才能有对 3.6 更好的支持。如果使用 Conda 建立虚拟环境,建议新建一个干净的 Python 3.5 的环境。 Qt 和 PyQt (界面化) Qt 和 PyQt 均采用比较新的版本,版本号需大于 5.10。Qt 直接从官网下载安装即可,理论上不需要安装 Qt,因为 PyQt 中包含了运行时环境,安装 Qt 的目的是为了使用其可视化的 Qt Creator,设计界面的时候会比较方便。如果使用 Conda 建立 Python 虚拟环境,请使用 pip 安装 PyQt 的对应版本,Conda 中的 PyQt 的版本相对较低,一些新的 Qt 特性不支持。 PyInstaller (编译打包) PyInstaller 是一个用于打包 Python 代码到一个本地化可执行程序的工具,安装其最新版本即可:pip install PyInstaller。 appdmg 和 NSIS (安装包制作) appdmg 是 macOS 下一个用于制作 DMG 镜像的工具,使用前先安装 Node.js,再通过 npm install -g appdmg 安装最新版即可。NSIS 是 Windows 下一个用于制作安装包的工具,NSIS 的一个问题是不支持 Unicode,因此对于包含中文字符的脚本需要以 GBK 编码格式保存。Unicode 版本的 NSIS 为 Unicode NSIS,不过 Unicode NSIS 已经长时间未更新,因此本文依旧将 NSIS 作为安装包制作工具。 界面设计 通过需求分析,整个工具最核心的两个界面为程序主界面和配置信息界面: 程序主界面包含了待搜索的信息,保存的路径,相关的按钮和日志输出。 配置信息界面以配置项的分组不同分别包括通用,网络和代理等相关的配置信息更改。 整个界面设计采用了 Google 的 Material Design 风格,尤其是在没有 UI 支援的情况下,使用这个风格至少不会让你的应用太丑。在 PyQt 中,可以通过 多种方式 启用 Material Design 风格。 程序开发 本文以 Sci-Hub EVA 作为示例介绍 PyQt 的跨平台 GUI 程序开发。Sci-Hub EVA 是一个利用 Sci-Hub API 下载论文的界面化小工具,功能相对简单。首先介绍一下工程的目录: docs\ images\ translations\ ui\ BUILDING.md Info.plist LICENSE README.md SciHubEVA.conf SciHubEVA.cpp SciHubEVA.dmg.json SciHubEVA.nsi SciHubEVA.pro SciHubEVA.qrc SciHubEVA.win.version requirements.txt scihub_add_scihub_url.py scihub_api.py scihub_conf.py scihub_eva.py scihub_preferences.py scihub_resources.py scihub_utils.py version_updater.py 其中,docs 目录为项目的一些文档,images 目录为项目的相关图片文件,translations 目录为项目的 i18n 翻译文件,ui 目录为相关的界面文件 (QML 文件),Info.plist 为 macOS 程序信息文件,SciHubEVA.conf 为程序配置文件,SciHubEVA.cpp 为 Qt 生成的 C++ 主文件,SciHubEVA.dmg.json 为利用 appdmg 制作 DMG 镜像的配置文件,SciHubEVA.nsi 为利用 NSIS 制作 Windows 安装包的脚本文件,SciHubEVA.pro 为程序的 Qt 主项目文件,,SciHubEVA.qrc 为程序的资源文件,SciHubEVA.win.version 为打包 Windows 的版本信息文件,requirements.txt 为 Python 依赖包信息文件,scihu_*.py 为程序实现相关 Python 代码,version_updater.py 为版本更新的小工具。 下文中不会介绍具体的业务逻辑代码,而是对开发过程中的一些核心点和注意事项进行简单的介绍。 Python 与 QML 通信 首先,对于每一个界面 (QML 文件),我们都有一个与之对应 Python 文件 (除非该页面没有具体的业务逻辑,例如:ui\SciHubEVAAbout.qml 为关于页面,ui\SciHubEVAMenuBar.qml 为菜单栏),以主页面 (ui\SciHubEVA.qml 和 scihub_eva.py) 为例,我们为每个界面创建一个类,同时该类集成自 Qt 的一个基类: class SciHubEVA(QObject): pass Python 代码同界面交互的核心是通过 Qt 的 信号与槽,同样在 PyQt 中也是利用 相同的机制。简单的理解 PyQt 与 QML 的信号与槽,可以认为信号就是函数的定义,槽就是函数的实现。同时,信号和槽往往会位于不同的地方,例如:信号定义在 Python 中,则对应的槽会在 QML 中,反之亦然,当然这并不是一定的。两者通过 connect() 函数连接起来,当触发一个信号时,槽就会接受到信号传递的参数,并执行槽里面相应的逻辑。 i18n Qt 对于多语言支持比较完善,在 QML 中对于需要翻译的地方利用 qsTr() 函数处理待翻译的文本即可,例如: Label { id: labelQuery text: qsTr("Query: ") } 在 Python 代码中,对于继承自 QObject 的类,可以利用基类中的 tr() 函数处理待翻译的文本即可,例如: self.tr('Saved PDF as: ') 同时将具有待翻译文本的文件加入到 SciHubEVA.pro 的主工程文件中,用于后续翻译处理: lupdate_only { SOURCES += \ ui/SciHubEVA.qml \ ui/SciHubEVAAbout.qml \ ui/SciHubEVAMenuBar.qml \ ui/SciHubEVAPreferences.qml \ ui/SciHubEVAAddSciHubURL.qml \ scihub_api.py } TRANSLATIONS += \ translations/SciHubEVA_zh_CN.ts 因为 Python 代码中也有需要翻译的文件,因此我们需要运行如下命令生成翻译的源文件: lupdate SciHubEVA.pro pylupdate5 SciHubEVA.pro 这样在 translations 目录即可生成待翻译的源文件 (ts 文件),利用 Qt 自带的 Liguist 可以对其进行编辑,翻译并保存后,利用如下命令生成翻译的结果文件: lrelease SciHubEVA.pro 在 translations 目录即可生成待翻译的结果文件 (qm 文件)。 资源文件 在 GUI 编程中,我们不可避免的会使用到各种各样的资源,例如:图片,音频,字体等等。Qt 中提供了一种资源管理方案,可以在不同场景下使用 (Python 和 QML 中均可)。SciHubEVA.qrc 定义了所有使用到的资源: <RCC> <qresource prefix="/"> <file>ui/SciHubEVA.qml</file> <file>ui/SciHubEVAMenuBar.qml</file> <file>ui/SciHubEVAAbout.qml</file> <file>ui/SciHubEVAPreferences.qml</file> <file>ui/SciHubEVAAddSciHubURL.qml</file> <file>images/about.png</file> </qresource> </RCC> 在 QML 中使用示例如下: Image { id: imageAboutLogo source: "qrc:/images/about.png" } 在 Python 中使用示例如下: self._engine = QQmlApplicationEngine() self._engine.load('qrc:/ui/SciHubEVA.qml') 使用 qrc 文件管理资源文件的一个好处就是不需要担心各种相对路径和绝对路径带来的找不到文件的错误,但同时一个缺点是当资源文件更新后,需要运行 pyrcc5 SciHubEVA.qrc -o scihub_resources.py 更新资源,同时还需要在主程序代码中引入生成的 Python 资源代码。 界面线程分离 写 GUI 应用的一个重要问题就是界面线程的分离,需要把耗时的业务逻辑摘出来,单独作为一个线程运行,这样才不会造成界面的“假死”情况。scihub_api.py 中的 SciHubAPI 作为下载文章的主类,下载过程相对耗时。因为其既需要 Qt 中的 tr() 函数,也需要线程,通过 Python 的多继承,SciHubAPI 类构造如下: class SciHubAPI(QObject, threading.Thread): pass 编译打包 PyInstaller 是一个用于打包 Python 代码到一个本地化可执行程序的工具,详细的使用方法请参见官方文档。同样,我们在此仅说明打包过程中遇到的一些问题。 macOS macOS 下的编译打包命令如下: # 清理相关目录和文件 rm -rf build rm -rf dist rm -f SciHubEVA.spec # 重新生成资源文件 rm -f scihub_resources.py pyrcc5 SciHubEVA.qrc -o scihub_resources.py # 编译打包 pyinstaller -w scihub_eva.py \ --hidden-import "PyQt5.Qt" \ --hidden-import "PyQt5.QtQuick" \ --add-data "LICENSE:." \ --add-data "SciHubEVA.conf:." \ --add-data "images/SciHubEVA.png:images" \ --add-data "translations/SciHubEVA_zh_CN.qm:translations" \ --name "SciHubEVA" \ --icon "images/SciHubEVA.icns" # 拷贝程序信息 cp Info.plist dist/SciHubEVA.app/Contents 编译打包过程中的 --hidden-import 参数是因为我们使用了 Qt Quick 和 QML 相关框架,但是在 Python 代码中我们并没有显式的引入这两个包,因此我们需要告知 PyInstaller 我们使用了这两个包,这样 PyInstaller 才会把相关的动态链接库拷贝到打包的程序中。 打包好的程序 SciEvaHub.app 会保存在 dist 目录中。由于目前无论是 macOS 还是 Windows 系统,高分辨率已经比较常见,为了适应高分辨率,我们需要在代码中添加相应的支持,在入口 Python 文件中,我们需要在头部添加如下信息: if hasattr(Qt, 'AA_EnableHighDpiScaling'): QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) if hasattr(Qt, 'AA_UseHighDpiPixmaps'): QGuiApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) 同时针对 macOS 系统,我们需要在 Info.plist 中添加如下信息以支持高分辨率: <key>NSHighResolutionCapable</key> <string>True</string> <key>NSSupportsAutomaticGraphicsSwitching</key> <string>True</string> Info.plist 中的其他信息针对性进行修改即可,最后将其拷贝到打包好的程序中。 Windows Windows 下的编译打包命令如下: rem 清理相关目录和文件 rd /s /Q build rd /s /Q dist del /Q SciHubEVA.spec rem 重新生成资源文件 del /Q scihub_resources.py pyrcc5 SciHubEVA.qrc -o scihub_resources.py rem 编译打包 pyinstaller -w scihub_eva.py ^ --hidden-import "PyQt5.Qt" ^ --hidden-import "PyQt5.QtQuick" ^ --add-data "LICENSE;." ^ --add-data "SciHubEVA.conf;." ^ --add-data "images/SciHubEVA.png;images" ^ --add-data "translations/SciHubEVA_zh_CN.qm;translations" ^ --name "SciHubEVA" ^ --icon "images/SciHubEVA.ico" ^ --version-file "SciHubEVA.win.version" 编译打包过程中的 --version-file 参数是 Windows 程序的相关版本信息,具体请参见微软的 Version Information Structures。 打包好的程序会在 dist\SciHubEVA 目录中,该目录还包含了所有运行时所需的文件。 安装包制作 macOS macOS 下我们使用 appdmg 工具将编译打包好的程序制作成 DMG 镜像文件。DMG 镜像文件可以对原始的程序进行压缩,便于分发。appdmg 通过一个 JSON 文件控制 DMG 镜像的制作,详细的 JSON 格式和相关参数请参见 官方文档,Sci-Hub EVA 的 DMG 制作 JSON 文件如下: { "title": "Sci-Hub EVA", "icon": "images/SciHubEVA.icns", "icon-size": 100, "background": "images/SciHubEVA-dmg-backgroud.png", "format": "UDZO", "window": { "size": { "width": 600, "height": 400 } }, "contents": [ { "x": 100, "y": 150, "type": "file", "path": "dist/SciHubEVA.app" }, { "x": 300, "y": 150, "type": "link", "path": "/Applications" } ] } 打包好后的 DMG 镜像效果如下: Windows Windows 下我们使用 NSIS 构建安装包,同样 NSIS 也支持多语言安装包构建,但请注意,NSIS 程序本身并不支持 Unicode,因此 NSIS 安装包的脚本需使用 GBK 编码保存。构建好的安装包的安装界面如下: 整个 Sci-Hub EVA 的编译打包和安装包制作过程请参见 构建说明文档。

2018/5/27
articleCard.readMore

流形学习 (Manifold Learning)

降维 在之前的 博客 中,我们曾经介绍过 PCA 方法及其降维的作用。在原始数据中各个特征之间存在着一定的信息冗余,随着特征的不断增加就容易出现“维数灾难”的问题,因此降维的目的就是在尽可能多的保留原始信息的同时减少数据的维度。一般情况下我们将降维方法分为:线性降维方法和非线性降维方法,线性降维方法的典型算法有: 主成份分析 (PCA, Principal Component Analysis) 1 线性判别分写 (LDA, Linear Discriminant Analysis) 2 多尺度变换 (MDS, Multi-Dimensional Scaling) 3 非线性降维方法中在此我们仅列举一些基于流行学习的算法: 保距特征映射 (ISOMAP) 4 局部线性嵌入 (LLE, Locally Linear Embedding) 5 拉普拉斯特征映射 (LE, Laplacian Eigenmap) 6 在现实数据中,很多情况数据是无法通过线性的方法进行降维表示的,因此就需要非线性的降维算法出马了。 流形 在调研流形相关概念时,发现要想深一步的理解这些概念还是需要详细的了解微分几何相关的内容,鉴于本文的目的主要是介绍流形学习 (主要是降维角度) 的相关内容,因此我们对流形仅做一些粗略的介绍。 “流形”是英文单词 Manifold 的中文译名,它源于德文术语 Mannigfaltigkeit,最早出现在 Riemann 1851 年的博士论文中,用来表示某种属性所能取到的所有值 7。为了更好的理解流形,我们先引入几个概念: 拓扑结构 (拓扑) 任意给定集合 $X$ 上的一个拓扑结构 (拓扑) 是 $X$ 的某些特定子集组成的集合 $\tau \subset 2^X$,其中那些特定子集称为 $\tau$ 所声明的开集,同时满足如下性质: 空集和全集是开集,即 $\varnothing, X \in \tau$ 任意多个开集的并集是开集 有限多个开集的交集是开集 拓扑空间 指定了拓扑结构的集合就称为一个拓扑空间。 上图中给出了一些拓扑空间的示例,其中左侧 4 个为正确示例,右侧 2 个为错误示例。右上角的缺少了 {2} 和 {3} 的并集 {2, 3},右下角的缺少了 {1, 2} 和 {2, 3} 的交集 {2}。 同胚 两个拓扑空间 $\left(X, \tau_X\right)$ 和 $\left(Y, \tau_Y\right)$ 之间的函数 $f: X \to Y$ 称为同胚,如果它具有下列性质: $f$ 是双射 (单射和满射) $f$ 是连续的 反函数 $f^{−1}$ 也是连续的 ($f$ 是开映射) 如果拓扑空间是一个几何物体,同胚就是把物体连续延展和弯曲,使其成为一个新的物体。因此,正方形和圆是同胚的,但球面和环面就不是。用一幅图形象的理解同胚,例如下图所示的咖啡杯和甜甜圈 8: 最后我们回过头来解释到底什么是流形?流形并不是一个“形状”,而是一个“空间” 9。最容易定义的流形是拓扑流形,它局部看起来象一些“普通”的欧几里得空间 $\mathbb{R}^n$,一个拓扑流形是一个局部同胚于一个欧几里得空间的拓扑空间。根据 Whitney 嵌入理论 10,任何一个流形都可以嵌入到高维的欧氏空间中。例如,地球的表面可以理解为一个嵌入 3 维空间的 2 维流形,其局部同胚于 2 维的欧式空间,对于一个球体的表面,用极坐标的形式可以表示为 $$ \begin{equation} \begin{split} x &= r \sin \theta \cos \phi \\ y &= r \sin \theta \sin \phi \\ z &= r \cos \theta \end{split} \end{equation} $$ 也就是说其 3 个维度实际上是由 2 个变量控制的。 流形学习 假设 $Y$ 为一个欧式空间 $\mathbb{R}^d$ 的一个 $d$ 维流形,$f: Y \to \mathbb{R}^D$ 为一个光滑嵌入,对于 $D > d$,流形学习的目的就是根据空间 $\mathbb{R}^D$ 中的观测数据 $\{x_i\}$ 重构 $Y$ 和 $f$ 的过程。隐含数据 $\{y_i\}$ 由 $Y$ 随机生成,通过光滑嵌入 $f$ 生成观测数据,即 $\{x_i = f\left(y_i\right)\}$,所以我们可以将流形学习的问题看做是对于一个给定的观测数据一个生成模型的反向过程 11。 在介绍具体的流形学习算法前,我们先引入几个 3 维数据用于解释后续的具体算法 第一个为瑞士卷 (Swiss Roll),其形状和我们日常生活中的瑞士卷相似;第二个为 S 形曲线 (S Curve);第三个为一个被切断的球面 (Severed Sphere)。 MDS 多尺度变换 (MDS, Multi-Dimensional Scaling) 3 是一种通过保留样本在高维空间中的不相似性 (Dissimilarity) 降低数据维度的方法,在这里不相似性可以理解为样本之间的距离。因此,根据距离的度量方式不同可以将其分为度量型 (metric) MDS 和 非度量型 (non-metric) MDS。度量型 MDS 通过计算不同样本之间距离的度量值进行降维,而非度量型则仅考虑距离的排序信息,在此我们仅对度量型 MDS 做简单介绍。 MDS 的目标是保留样本在高维空间中的不相似性,假设 $x \in \mathbb{R}^D, x' \in \mathbb{R}^d, D > d$,则 MDS 的目标函数可以写为 $$ \min \sum_{i, j} \lvert dist \left(x_i, x_j\right) - dist \left(x'_i, x'_j\right) \rvert $$ 则,度量型 MDS 的算法的步骤如下: 计算样本的距离矩阵 $\boldsymbol{D} = \left[d_{i, j}\right] = \left[dist \left(x_i, x_j\right)\right]$。 构造矩阵 $\boldsymbol{A} = \left[a_{i, j}\right] = \left[- \dfrac{1}{2} d_{i, j}^2\right]$。 通过中心矫正的方法构造矩阵 $\boldsymbol{B} = \boldsymbol{J} \boldsymbol{D} \boldsymbol{J}, \boldsymbol{J} = \boldsymbol{I} - \dfrac{1}{n} \boldsymbol{O}$,其中 $\boldsymbol{I}$ 为 $n \times n$ 的单位阵,$\boldsymbol{O}$ 为 $n \times n$ 的值均为 $1$ 的矩阵。 计算矩阵 $\boldsymbol{B}$ 的特征向量 $e_1, e_2, ..., e_m$ 及其对应的特征值 $\lambda_1, \lambda_2, ..., \lambda_m$。 确定维度 $k$,重构数据 $\boldsymbol{X}' = \boldsymbol{E}_k \boldsymbol{\Lambda}_k^{1/2}$,其中 $\boldsymbol{\Lambda}_k$ 为前 $k$ 个值最大的 $k$ 个特征值构成的对角矩阵,$\boldsymbol{E}_k$ 是对应的 $k$ 个特征向量构成的矩阵。 在《多元统计分析》12一书中证明了,$\boldsymbol{X}$ 的 $k$ 维主坐标正好是将 $\boldsymbol{X}$ 中心化后 $n$ 个样本的前 $k$ 个主成份的值,由此可见 MDS 和 PCA 的作用是类似的。 我们利用中国省会的地理位置给出 MDS 的一个示例,首先我们获取中国省会共 34 个点的坐标,其次我们计算两两之间的距离,我们仅利用距离信息利用 MDS 还原出 2 维空间中的坐标,可视化结果如下所示 其中,黑色的点为省会的真实位置,蓝色的点为利用距离矩阵和 MDS 还原出来的位置,为了绘制还原出的位置我们对 MDS 的结果做出了适当的翻转和变换。从结果中不难看出,尽管每个点的坐标相比真实坐标都有一定的偏离,但是其很好的保持了相对距离,这也正是 MDS 算法的要求。 ISOMAP 对于一些非线性的流形,如果使用线性的降维方法得到的效果就不尽人意了,例如上文中提到的瑞士卷。在 ISOMAP 中,我们首先引入一个测地线的概念,在距离度量定义时,测地线可以定义为空间中两点的局域最短路径。形象的,在一个球面上,两点之间的测地线就是过这两个点的大圆的弧线 那么,对于非线性流形,ISOMAP 则是通过构建邻接图,利用图上的最短距离来近似测地线。在构造邻接图时,我们使用最近邻算法,对于一个点 $x_i$ 连接距离其最近的 $k$ 个点,两点之间的距离我们则一般使用传统的欧式距离。则任意两点之间的测地线距离则可以利用构建的邻接图上的最短路径进行估计,图上的最短路问题我们可以通过 Dijkstra 或 Floyd-Warshall 算法计算。得到样本的距离矩阵后,ISOMAP 算法则使用 MDS 方法计算得到低维空间的座标映射。 上图中,我们给出了利用 ISOMAP 对瑞士卷降至 2 维的一个格式化过程。第一幅图中,我们标注了 2 个蓝色的点,其中蓝色的直线为这 2 个点在三维空间中的欧式距离。第二幅图中,同样是相同的两个点,我们首先利用最近邻算法 ($k = 10$) 将瑞士卷所有的点连接为一个邻接图,其中红色的路径为这 2 个点在邻接图上的最短路。第三幅图是通过 ISOMAP 算法降维至 2 维的结果,其中蓝色的直线是这两个点在 2 维空间中的欧式距离,红色的路径是 3 维最短路在 2 维结果中的连线,可以看出两者是很相近的。 LLE 局部线性嵌入 (LLE, Locally Linear Embedding) 5,从这个名称上我们不难看出其不同与 ISOMAP 那种通过都建邻接图保留全局结构的,而是从局部结构出发对数据进行降维。在 LLE 方法中,主要有如下的基本假设: 一个流形的局部可以近似于一个欧式空间 每个样本均可以利用其邻居进行线性重构 基于上面的假设,LLE 算法的流程如下: 对于点 $X_i$,计算距离其最近的 $k$ 个点,$X_j, j \in N_i$。 计算权重 $W_{ij}$ 是的能够通过点 $X_i$ 的邻居节点最优的重构该点,即最小化 $$ \epsilon \left(W\right) = \sum_i \left\lVert X_i - \sum_j W_{ij} X_j \right\rVert ^2 $$ 通过权重 $W_{ij}$ 计算 $X$ 的低维最优重构 $Y$,即最小化 $$ \phi \left(Y\right) = \sum_i \left\lVert Y_i - \sum_j W_{ij} Y_j \right\rVert ^2 $$ 具体上述问题的优化求解过程在此就不在详细描述。针对 LLE 算法,后续很多人从不同方面对其进行了改进: Hessian LLE 13 在局部中不再考虑局部的线性关系,而是保持局部的 Hessian 矩阵的二次型的关系。 Modified LLE 14 则是修改了寻找最临近的 $k$ 个样本的方案,其在寻找 $k$ 近邻时希望找到的近邻尽量分布在样本的各个方向,而不是集中在一侧。 LTSA (Local Tangent Space Alignment) 15 则是除了保留了局部的几何性质,同时使用的一个从局部几何到整体性质过渡的 alignment 方法,因此可以理解为是一个局部和整体的组合。 LE LE (Laplacian Eigenmap) 6 的基本思想是认为在高维空间中距离近的点映射到低维空间中后其位置也相距很近。LE 从这个思想出发,最终将问题转化为求解图拉普拉斯算子的广义特征值问题,具体的一些证明不在这里详细展开说明,具体请参见原文,下面仅给出 LE 算法的流程: 构建邻接图。 构建邻接矩阵 $W$,构建邻接矩阵有两种方法:对于点 $i$ 和点 $j$ 相连,如果利用 Hear Kernel (参数 $t \in \mathbb{R}$),则令 $W_{ij} = \exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{t}\right)$;如果使用简介方案,则令 $W_{ij} = 1$,对于不相连的点,则令 $W_{ij} = 0$。 进行特征映射,通过上面构造的图 $G$,计算如下广义特征值和特征向量 $$ L f = \lambda D f $$ 其中 $D$ 是一个对角矩阵,$D_{ii} = \sum_{j} W_{ji}$,$L = D - W$ 即为拉普拉斯矩阵。对于上式的解 $f_0, .., f_{k-1}$ 为根据特征值从小到大的排序,其中 $0 = \lambda_0 \leq \lambda_1 \leq ... \leq \lambda_{k-1}$,则降维至 $d$ 维的后的特征即为 $\left(f_1, f_2, ..., f_d\right)$。 SNE 和 t-SNE SNE SNE (Stochastic Neighbor Embedding) 16 是由 Hinton 等人提出的一种降维算法,其方法的基本假设如下: 对象之间的相似度可以用概率进行表示,即:相似的对象有更高的概率被同时选择,不相似的对象有较低的概率被同时选择。 在高维空间中构建的这种概率分布应该尽可能的同低维空间中的概率分布相似。 对于两个点 $x_i, x_j$,假设 $x_i$ 以条件概率 $p_{j∣i}$ 选择 $x_j$ 作为它的邻近点,因此如果两者距离更近 (更相似),则概率值越大,反之概率值越小,则我们定义 $p_{j∣i}$ 如下: $$ p_{j∣i} = \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma_i^2}\right)}{\sum_{k \neq i} \exp \left(\dfrac{- \left\lVert x_i - x_k \right\rVert ^ 2}{2 \sigma_i^2}\right)} $$ 其中,$\sigma_i$ 为参数,同时我们设置 $p_{i∣i} = 0$,因为我们仅需衡量不同对象之间的相似度。 类似的,根据 SNE 的基本思想,当数据被映射到低维空间中后,其概率分布应同高维空间中的分布尽可能的相似,假设点 $x_i, x_j$ 在低维空间中的映射点为 $y_i, y_j$,则在低维空间中的条件概率 $q_{j∣i}$ 定义为: $$ q_{j∣i} = \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq i} \exp \left(- \left\lVert y_i - y_k \right\rVert ^ 2\right)} $$ 同样,我们设置 $q_{i∣i} = 0$。从 SNE 的基本假设出发,我们的目的是使得数据在高维空间中的条件概率尽可能的和其在低维空间中的条件概率相同,因此对于全部点样本点而言,就是保证高维空间的概率分布 $P_i$ 和低维空间的概率分布 $Q_i$ 尽量形同。在这里我们利用 KL 散度衡量这两个概率分布的差异,则 SNE 的损失函数可以写为: $$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{j∣i} \log \dfrac{p_{j∣i}}{q_{j∣i}} $$ 因为 KL 散度具有不对称性可知,当在原始空间中两点距离较远而降维后的空间中距离较近 (即,$q_{j|i} < p_{j|i}$) 时,会产生较大的 cost,相反则会产生较小的 cost。正是这种不对称性的损失函数导致了 SNE 算法更加关注局部结构,相比忽略了全局结构。 上文中,对于不同的点,$\sigma_i$ 具有不同的值,SNE 算法利用困惑度 (Perplexity) 对其进行优化寻找一个最佳的 $\sigma$,对于一个随机变量 $P_i$,困惑度定义如下: $$ Perp \left(P_i\right) = 2^{H \left(P_i\right)} $$ 其中,$H \left(P_i\right) = \sum_{j} p_{j|i} \log_2 p_{j|i}$ 表示 $P_i$ 的熵。困惑度可以解释为一个点附近的有效近邻点个数。SNE 对困惑度的调整比较有鲁棒性,通常选择 5-50 之间,给定之后,使用二分搜索的方式寻找合适的 $\sigma$。 SNE 的损失函数对 $y_i$ 求梯度后,可得: $$ \dfrac{\delta C}{\delta y_i} = 2 \sum_j \left(p_{j|i} - q_{j|i} + p_{i|j} - q_{i|j}\right) \left(y_i - y_j\right) $$ t-SNE SNE 为我们提供了一种很好的降维方法,但是其本身也存在一定的问题,主要有如下两点: 不对称问题:损失函数中的 KL 散度具有不对称性,导致 SNE 更加关注局部结构,相比忽略了全局结构。 拥挤问题:从高维空间映射到低维空间后,不同类别的簇容易挤在一起,无法较好地区分开。 针对这两个问题,Maaten 等人又提出了 t-SNE 算法对其进行优化 17。 针对不对称问题,Maaten 采用的方法是用联合概率分布来替代条件概率分布。高维控件中的联合概率分布为 $P$,低维空间中的联合概率分布为 $Q$,则对于任意的 $i, j$,有 $p_{ij} = p_{ji}, q_{ij} = q_{ji}$,联合概率定义为: $$ \begin{align} p_{ij} &= \dfrac{\exp \left(\dfrac{- \left\lVert x_i - x_j \right\rVert ^ 2}{2 \sigma^2}\right)}{\sum_{k \neq l} \exp \left(\dfrac{- \left\lVert x_k - x_l \right\rVert ^ 2}{2 \sigma^2}\right)} \\ q_{ij} &= \dfrac{\exp \left(- \left\lVert y_i - y_j \right\rVert ^ 2\right)}{\sum_{k \neq l} \exp \left(- \left\lVert y_k - y_l \right\rVert ^ 2\right)} \end{align} $$ 虽然这样保证了对称性,但是对于异常的情况,例如数据点 $x_i$ 在距离群簇较远,则 $\lVert x_i − x_j \rVert ^ 2$ 的值会很大,而 $p_{ij}$ 会相应变得非常小,也就是说 $x_i$ 的位置很远这件事情对损失函数影响很小 (惩罚过小),那这个点在低维空间中将无法从其他点中区分出来。因此 Maaten 提出了对称的条件概率来重新定义上述联合概率 $p_{ij}$ ,对于数量为 $n$ 的数据点,新的概率公式是: $$ p_{ij} = \dfrac{p_{j|i} + p_{i|j}}{2n} $$ 则损失函数更新为: $$ C = \sum_{i} KL \left(P_i \Vert Q_i\right) = \sum_{i} \sum_{j} p_{ij} \log \dfrac{p_{ij}}{q_{ij}} $$ 梯度更新为: $$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) $$ 拥挤问题 (Crowding) 就是从高维空间映射到低维空间后,不同类别的簇容易挤在一起,不能很好的地区分开。t-SNE 则是利用了 t 分布重新定义 $q_{ij}$,t 分布具有长尾特性,相比于高斯分布,其在尾部趋向于 0 的速度更慢,对比如图所示: 利用 t 分布重新定义的 $q_{ij}$ 为: $$ q_{ij} = \dfrac{\left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1}}{\sum_{k \neq l} \left(1 + \lVert y_k - y_l \rVert ^ 2\right) ^ {-1}} $$ 梯度更新为: $$ \dfrac{\delta C}{\delta y_i} = 4 \sum_j \left(p_{ij} - q_{ij}\right) \left(y_i - y_j\right) \left(1 + \lVert y_i - y_j \rVert ^ 2\right) ^ {-1} $$ 利用 t-SNE 对 MNIST 数据集进行降维可视化结果如下: 方法比较 针对上述的若干算法,我们简单列举一下每个算法的优缺点 方法 优点 缺点 Isomap 1. 保持流形的全局几何结构 2. 适用于学习内部平坦的低维流形 1. 对于数据量较大的情况,计算效率过低 2. 不适于学习有较大内在曲率的流形 LLE 1. 可以学习任意维的局部线性的低维流形 2. 归结为稀疏矩阵特征值计算,计算复杂度相对较小 1. 所学习的流形只能是不闭合的 2. 要求样本在流形上是稠密采样的 3.对样本中的噪声和邻域参数比较敏感 LE 1. 是局部非线性方法,与谱图理论有很紧密的联系 2. 通过求解稀疏矩阵的特征值问题解析地求出整体最优解,效率非常高 3. 使原空间中离得很近的点在低维空间也离得很近,可以用于聚类 1. 对算法参数和数据采样密度较敏感 2. 不能有效保持流形的全局几何结构 SNE, t-SNE 1. 非线性降维效果相较上述方法较好 1. 大规模高维数据时,效率显著降低 2. 参数对不同数据集较为敏感 对于瑞士卷 (Swiss Roll),S 形曲线 (S Curve) 和切断的球面 (Severed Sphere),我们利用不同的流形算法对其进行降维,可视化的对比结果如下面 3 张图所示,图中同时标注了算法的运行时间,实现主要参照了 scikit-learn 关于流形学习算法的比较 18。 文中相关图片绘制实现详见代码,本文部分内容参考了流形学习专题介绍 19, 流形学习 20,Chrispher 21 的博客和 bingo 22 的博客。 Jolliffe, Ian T. “Principal component analysis and factor analysis.” Principal component analysis. Springer, New York, NY, 1986. 115-128. ↩︎ Balakrishnama, Suresh, and Aravind Ganapathiraju. “Linear discriminant analysis-a brief tutorial.” Institute for Signal and information Processing 18 (1998): 1-8. ↩︎ Cox, Trevor F., and Michael AA Cox. Multidimensional scaling. CRC press, 2000. ↩︎ ↩︎ Tenenbaum, Joshua B., Vin De Silva, and John C. Langford. “A global geometric framework for nonlinear dimensionality reduction.” Science 290.5500 (2000): 2319-2323. ↩︎ Roweis, Sam T., and Lawrence K. Saul. “Nonlinear dimensionality reduction by locally linear embedding.” Science 290.5500 (2000): 2323-2326. ↩︎ ↩︎ Belkin, Mikhail, and Partha Niyogi. “Laplacian eigenmaps for dimensionality reduction and data representation.” Neural computation 15.6 (2003): 1373-1396. ↩︎ ↩︎ 梅加强. 流形与几何初步 ↩︎ https://zh.wikipedia.org/zh-hans/流形 ↩︎ pluskid. 浅谈流形学习 ↩︎ https://en.wikipedia.org/wiki/Whitney_embedding_theorem ↩︎ Silva, Vin D., and Joshua B. Tenenbaum. “Global versus local methods in nonlinear dimensionality reduction.” Advances in neural information processing systems. 2003. ↩︎ 何晓群. 多元统计分析 ↩︎ Donoho, David L., and Carrie Grimes. “Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data.” Proceedings of the National Academy of Sciences 100.10 (2003): 5591-5596. ↩︎ Zhang, Zhenyue, and Jing Wang. “MLLE: Modified locally linear embedding using multiple weights.” Advances in neural information processing systems. 2007. ↩︎ Zhang, Zhenyue, and Hongyuan Zha. “Principal manifolds and nonlinear dimensionality reduction via tangent space alignment.” SIAM journal on scientific computing 26.1 (2004): 313-338. ↩︎ Hinton, Geoffrey E., and Sam T. Roweis. “Stochastic neighbor embedding.” Advances in neural information processing systems. 2003. ↩︎ Maaten, Laurens van der, and Geoffrey Hinton. “Visualizing data using t-SNE.” Journal of machine learning research 9.Nov (2008): 2579-2605. ↩︎ http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html ↩︎ 王瑞平. 流形学习专题介绍 ↩︎ 何晓飞. 流形学习 ↩︎ http://www.datakit.cn/blog/2017/02/05/t_sne_full.html ↩︎ http://bindog.github.io/blog/2016/06/04/from-sne-to-tsne-to-largevis/ ↩︎

2018/3/16
articleCard.readMore

深度学习优化算法 (Optimization Methods for Deeplearning)

在构建神经网络模型的时候,除了网络结构设计以外,选取合适的优化算法也对网络起着至关重要的作用,本文将对神经网络中常用的优化算法进行简单的介绍和对比,本文部分参考了 Ruder 的关于梯度下降优化算法一文 1。首先,我们对下文中使用的符号进行同意说明:网络中的参数同一表示为 $\theta$,网络的假设函数为 $h_{\boldsymbol{\theta}}\left(\boldsymbol{x}\right)$,网络的损失函数为 $J\left(\boldsymbol{\theta}\right)$,学习率为 $\alpha$,假设训练数据中共包含 $m$ 个样本,网络参数个数为 $n$。 梯度下降 在梯度下降算法中,常用的主要包含 3 种不同的形式,分别是批量梯度下降 (Batch Gradient Descent, BGD),随机梯度下降 (Stochastic Gradient Descent, SGD) 和小批量梯度下降 (Mini-Batch Gradient Descent, MBGD)。一般情况下,我们在谈论梯度下降时,更多的是指小批量梯度下降。 BGD BGD 为梯度下降算法中最基础的一个算法,其损失函数定义如下: $$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2m} \sum_{i=1}^{m}{\left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right)} $$ 针对任意参数 $\theta_j$ 我们可以求得其梯度为: $$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$ 之后,对于任意参数 $\theta_j$ 我们按照其负梯度方向进行更新: $$ \theta_j = \theta_j + \alpha \left[\dfrac{1}{m} \sum_{i=1}^{m}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}}\right] $$ 整个算法流程可以表示如下: 从上述算法流程中我们可以看到,BGD 算法每次计算梯度都使用了整个训练集,也就是说对于给定的一个初始点,其每一步的更新都是沿着全局梯度最大的负方向。但这同样是其问题,当 $m$ 太大时,整个算法的计算开销就很高了。 SGD SGD 相比于 BGD,其最主要的区别就在于计算梯度时不再利用整个数据集,而是针对单个样本计算梯度并更新权重,因此,其损失函数定义如下: $$ J \left(\boldsymbol{\theta}\right) = \dfrac{1}{2} \left(h_{\boldsymbol{\theta}}\left(x^{\left(i\right)}\right) - y^{\left(i\right)}\right) $$ 整个算法流程可以表示如下: SGD 相比于 BGD 具有训练速度快的优势,但同时由于权重改变的方向并不是全局梯度最大的负方向,甚至相反,因此不能够保证每次损失函数都会减小。 MBGD 针对 BGD 和 SGD 的问题,MBGD 则是一个折中的方案,在每次更新参数时,MBGD 会选取 $b$ 个样本计算的梯度,设第 $k$ 批中数据的下标的集合为 $B_k$,则其损失函数定义如下: $$ \nabla_{\theta_j} = \dfrac{\partial J\left(\boldsymbol{\theta}\right)}{\partial \theta_j} = - \dfrac{1}{|B_k|} \sum_{i \in B_k}{\left(y^{\left(i\right)} - h_{\boldsymbol{\theta}} \left(x^{\left(i\right)}\right)\right) x_j^{\left(i\right)}} $$ 整个算法流程可以表示如下: Momentum 当梯度沿着一个方向要明显比其他方向陡峭,我们可以形象的称之为峡谷形梯度,这种情况多位于局部最优点附近。在这种情况下,SGD 通常会摇摆着通过峡谷的斜坡,这就导致了其到达局部最优值的速度过慢。因此,针对这种情况,Momentum 2 方法提供了一种解决方案。针对原始的 SGD 算法,参数每 $t$ 步的变化量可以表示为 $$ \boldsymbol{v}_t = - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) $$ Momentum 算法则在其变化量中添加了一个动量分量,即 $$ \begin{equation} \begin{split} \boldsymbol{v}_t &= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$ 对于添加的动量项,当第 $t$ 步和第 $t-1$ 步的梯度方向相同时,$\boldsymbol{\theta}$ 则以更快的速度更新;当第 $t$ 步和第 $t-1$ 步的梯度方向相反时,$\boldsymbol{\theta}$ 则以较慢的速度更新。利用 SGD 和 Momentum 两种方法,在峡谷行的二维梯度上更新参数的示意图如下所示 NAG NAG (Nesterov Accelerated Gradient) 3 是一种 Momentum 算法的变种,其核心思想会利用“下一步的梯度”确定“这一步的梯度”,当然这里“下一步的梯度”并非真正的下一步的梯度,而是指仅根据动量项更新后位置的梯度。Sutskever 4 给出了一种更新参数的方法: $$ \begin{equation} \begin{split} \boldsymbol{v}_t &= - \alpha \nabla_{\boldsymbol{\theta}} J \left(\boldsymbol{\theta}_t + \gamma \boldsymbol{v}_{t-1}\right) + \gamma \boldsymbol{v}_{t-1} \\ \boldsymbol{\theta}_t &= \boldsymbol{\theta}_{t-1} + \boldsymbol{v}_t \end{split} \end{equation} $$ 针对 Momentum 和 NAG 两种不同的方法,其更新权重的差异如下图所示: AdaGrad AdaGrad 5 是一种具有自适应学习率的的方法,其对于低频特征的参数选择更大的更新量,对于高频特征的参数选择更小的更新量。因此,AdaGrad算法更加适用于处理稀疏数据。Pennington 等则利用该方法训练 GloVe 6 词向量,因为对于出现次数较少的词应当获得更大的参数更新。 因为每个参数的学习速率不再一样,则在 $t$ 时刻第 $i$ 个参数的变化为 $$ \theta_{t, i} = \theta_{t-1, i} - \alpha \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$ 根据 AdaGrad 方法的更新方式,我们对学习率做出如下变化 $$ \theta_{t, i} = \theta_{t-1, i} - \dfrac{\alpha}{\sqrt{G_{t, i}} + \epsilon} \nabla_{\theta} J \left(\theta_{t-1, i}\right) $$ 其中,$G_t$ 表示截止到 $t$ 时刻梯度的平方和;$\epsilon$ 为平滑项,防止除数为零,一般设置为 $10^{-8}$。AdaGrad 最大的优势就在于其能够自动调节每个参数的学习率。 Adadelta 上文中 AdaGrad 算法存在一个缺点,即其用于调节学习率的分母中包含的是一个梯度的平方累加项,随着训练的不断进行,这个值将会越来越大,也就是说学习率将会越来越小,最终导致模型不会再学习到任何知识。Adadelta 7 方法针对 AdaGrad 的这个问题,做出了进一步改进,其不再计算历史所以梯度的平方和,而是使用一个固定长度 $w$ 的滑动窗口内的梯度。 因为存储 $w$ 的梯度平方并不高效,Adadelta 采用了一种递归的方式进行计算,定义 $t$ 时刻梯度平方的均值为 $$ E \left[g^2\right]_t = \rho E \left[g^2\right]_{t-1} + \left(1 - \rho\right) g^2_{t} $$ 其中,$g_t$ 表示 $t$ 时刻的梯度;$\rho$ 为一个衰减项,类似于 Momentum 中的衰减项。在更新参数过程中我们需要其平方根,即 $$ \text{RMS} \left[g\right]_t = \sqrt{E \left[g^2\right]_t + \epsilon} $$ 则参数的更新量为 $$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$ 除此之外,作者还考虑到上述更新中更新量和参数的假设单位不一致的情况,在上述更新公式中添加了一个关于参数的衰减项 $$ \text{RMS} \left[\Delta \theta\right]_t = \sqrt{E \left[\Delta \theta^2\right]_t + \epsilon} $$ 其中 $$ E \left[\Delta \theta^2\right]_t = \rho E \left[\Delta \theta^2\right]_{t-1} + \left(1 - \rho\right) \Delta \theta_t^2 $$ 在原始的论文中,作者直接用 $\text{RMS} \left[\Delta \theta^2\right]_t$ 替换了学习率,即 $$ \Delta \theta_t = - \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$ 而在 Keras 源码中,则保留了固定的学习率,即 $$ \Delta \theta_t = - \alpha \dfrac{\text{RMS} \left[\Delta \theta\right]_{t-1}}{\text{RMS} \left[g\right]_t} g_t $$ RMSprop RMSprop 8 是由 Hinton 提出的一种针对 AdaGrad 的改进算法。参数的更新量为 $$ \Delta \theta_t = - \dfrac{\alpha}{\text{RMS} \left[g\right]_t} g_t $$ Adam Adam (Adaptive Moment Estimation) 9 是另一种类型的自适应学习率方法,类似 Adadelta,Adam 对于每个参数都计算各自的学习率。Adam 方法中包含一个一阶梯度衰减项 $m_t$ 和一个二阶梯度衰减项 $v_t$ $$ \begin{equation} \begin{split} m_t &= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \end{split} \end{equation} $$ 算法中,$m_t$ 和 $v_t$ 初始化为零向量,作者发现两者会更加偏向 $0$,尤其是在训练的初始阶段和衰减率很小的时候 (即 $\beta_1$ 和 $\beta_2$ 趋近于1的时候)。因此,对其偏差做如下校正 $$ \begin{equation} \begin{split} \hat{m}_t &= \dfrac{m_t}{1 - \beta_1^t} \\ \hat{v}_t &= \dfrac{v_t}{1 - \beta_2^t} \end{split} \end{equation} $$ 最终得到 Adam 算法的参数更新量如下 $$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t $$ Adamax 在 Adam 中参数的更新方法利用了 $L_2$ 正则形式的历史梯度 ($v_{t-1}$) 和当前梯度 ($|g_t|^2$),因此,更一般的,我们可以使用 $L_p$ 正则形式,即 $$ \begin{equation} \begin{split} v_t &= \beta_2^p v_{t-1} + \left(1 - \beta_2^p\right) |g_t|^p \\ &= \left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p \end{split} \end{equation} $$ 这样的变换对于值较大的 $p$ 而言是很不稳定的,但对于极端的情况,当 $p$ 趋近于无穷的时候,则变为了一个简单并且稳定的算法。则在 $t$ 时刻对应的我们需要计算 $v_t^{1/p}$,令 $u_t = \lim_{p \to \infty} \left(v_t\right)^{1/p}$,则有 $$ \begin{equation} \begin{split} u_t &= \lim_{p \to \infty} \left(\left(1 - \beta_2^p\right) \sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \lim_{p \to \infty} \left(1 - \beta_2^p\right)^{1/p} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \lim_{p \to \infty} \left(\sum_{i=1}^{t} \beta_2^{p\left(t-i\right)} \cdot |g_t|^p\right)^{1/p} \\ &= \max \left(\beta_2^{t-1} |g_1|, \beta_2^{t-2} |g_2|, ..., \beta_{t-1} |g_t|\right) \end{split} \end{equation} $$ 写成递归的形式,则有 $$ u_t = \max \left(\beta_2 \cdot u_{t-1}, |g_t|\right) $$ 则 Adamax 算法的参数更新量为 $$ \Delta \theta = - \dfrac{\alpha}{u_t} \hat{m}_t $$ Nadam Adam 算法可以看做是对 RMSprop 和 Momentum 的结合:历史平方梯度的衰减项 $v_t$ (RMSprop) 和 历史梯度的衰减项 $m_t$ (Momentum)。Nadam (Nesterov-accelerated Adaptive Moment Estimation) 10 则是将 Adam 同 NAG 进行了进一步结合。我们利用 Adam 中的符号重新回顾一下 NAG 算法 $$ \begin{equation} \begin{split} g_t &= \nabla_{\theta} J \left(\theta_t - \gamma m_{t-1}\right) \\ m_t &= \gamma m_{t-1} + \alpha g_t \\ \theta_t &= \theta_{t-1} - m_t \end{split} \end{equation} $$ NAG 算法的核心思想会利用“下一步的梯度”确定“这一步的梯度”,在 Nadam 算法中,作者在考虑“下一步的梯度”时对 NAG 进行了改动,修改为 $$ \begin{equation} \begin{split} g_t &= \nabla_{\theta} J \left(\theta_t\right) \\ m_t &= \gamma m_{t-1} + \alpha g_t \\ \theta_t &= \theta_{t-1} - \left(\gamma m_t + \alpha g_t\right) \end{split} \end{equation} $$ 对于 Adam,根据 $$ \hat{m}_t = \dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t} $$ 则有 $$ \begin{equation} \begin{split} \Delta \theta &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \\ &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) \end{split} \end{equation} $$ 上式中,仅 $\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^t}$ 和动量项相关,因此我们类似上文中对 NAG 的改动,通过简单的替换加入 Nesterov 动量项,最终得到 Nadam 方法的参数的更新量 $$ \Delta \theta = - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \left(\dfrac{\beta_1 m_{t-1}}{1 - \beta_1^{t+1}} + \dfrac{\left(1 - \beta_1\right) g_t}{1 - \beta_1^t}\right) $$ AMSGrad 对于前面提到的 Adadelta,RMSprop,Adam 和 Nadam 方法,他们均采用了平方梯度的指数平滑平均值迭代产生新的梯度,但根据观察,在一些情况下这些算法并不能收敛到最优解。Reddi 等提出了一种新的 Adam 变体算法 AMSGrad 11,在文中作者解释了为什么 RMSprop 和 Adam 算法无法收敛到一个最优解的问题。通过分析表明,为了保证得到一个收敛的最优解需要保留过去梯度的“长期记忆”,因此在 AMSGrad 算法中使用了历史平方梯度的最大值而非滑动平均进行更新参数,即 $$ \begin{equation} \begin{split} m_t &= \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t \\ v_t &= \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2 \\ \hat{v}_t &= \max \left(\hat{v}_{t-1}, v_t\right) \\ \Delta \theta &= - \dfrac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} m_t \end{split} \end{equation} $$ 作者在一些小数据集和 CIFAR-10 数据集上得到了相比于 Adam 更好的效果,但与此同时一些其他的 实验 却得到了相比与 Adam 类似或更差的结果,因此对于 AMSGrad 算法的效果还有待进一步确定。 算法可视化 正所谓一图胜千言,Alec Radford 提供了 2 张图形象了描述了不同优化算法之间的区别 左图为 Beale Function 在二维平面上的等高线,从图中可以看出 AdaGrad,Adadelta 和 RMSprop 算法很快的找到正确的方向并迅速的收敛到最优解;Momentum 和 NAG 则在初期出现了偏离,但偏离之后调整了方向并收敛到最优解;而 SGD 尽管方向正确,但收敛速度过慢。 右图为包含鞍点的一个三维图像,图像函数为 $z = x^2 - y^2$,从图中可以看出 AdaGrad,Adadelta 和 RMSprop 算法能够相对很快的逃离鞍点,而 Momentum,NAG 和 SGD 则相对比较困难逃离鞍点。 很不幸没能找到 Alec Radford 绘图的原始代码,不过 Louis Tiao 在 博客 中给出了绘制类似动图的方法。因此,本文参考该博客和 Keras 源码中对不同优化算法的实现重新绘制了 2 张类似图像,详细过程参见 源代码,动图如下所示: Ruder, Sebastian. “An overview of gradient descent optimization algorithms.” arXiv preprint arXiv:1609.04747 (2016). ↩︎ Qian, Ning. “On the momentum term in gradient descent learning algorithms.” Neural networks 12.1 (1999): 145-151. ↩︎ Nesterov, Yurii. “A method for unconstrained convex minimization problem with the rate of convergence O (1/k^2).” Doklady AN USSR. Vol. 269. 1983. ↩︎ Sutskever, Ilya. “Training recurrent neural networks.” University of Toronto, Toronto, Ont., Canada (2013). ↩︎ Duchi, John, Elad Hazan, and Yoram Singer. “Adaptive subgradient methods for online learning and stochastic optimization.” Journal of Machine Learning Research 12.Jul (2011): 2121-2159. ↩︎ Pennington, Jeffrey, Richard Socher, and Christopher Manning. “Glove: Global vectors for word representation.” Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP). 2014. ↩︎ Zeiler, Matthew D. “ADADELTA: an adaptive learning rate method.” arXiv preprint arXiv:1212.5701 (2012). ↩︎ Hinton, G., Nitish Srivastava, and Kevin Swersky. “Rmsprop: Divide the gradient by a running average of its recent magnitude.” Neural networks for machine learning, Coursera lecture 6e (2012). ↩︎ Kingma, Diederik P., and Jimmy Ba. “Adam: A method for stochastic optimization.” arXiv preprint arXiv:1412.6980 (2014). ↩︎ Dozat, Timothy. “Incorporating nesterov momentum into adam.” (2016). ↩︎ Reddi, Sashank J., Satyen Kale, and Sanjiv Kumar. “On the convergence of adam and beyond.” International Conference on Learning Representations. 2018. ↩︎

2018/2/24
articleCard.readMore

生成对抗网络简介 (GAN Introduction)

Generative Adversarial Networks (GAN) 生成对抗网络 (Generative Adversarial Network, GAN) 是由 Goodfellow 1 于 2014 年提出的一种对抗网络。这个网络框架包含两个部分,一个生成模型 (generative model) 和一个判别模型 (discriminative model)。其中,生成模型可以理解为一个伪造者,试图通过构造假的数据骗过判别模型的甄别;判别模型可以理解为一个警察,尽可能甄别数据是来自于真实样本还是伪造者构造的假数据。两个模型都通过不断的学习提高自己的能力,即生成模型希望生成更真的假数据骗过判别模型,而判别模型希望能学习如何更准确的识别生成模型的假数据。 网络框架 GAN 由两部分构成,一个生成器 (Generator) 和一个判别器 (Discriminator)。对于生成器,我们需要学习关于数据 $\boldsymbol{x}$ 的一个分布 $p_g$,首先定义一个输入数据的先验分布 $p_{\boldsymbol{z}} \left(\boldsymbol{z}\right)$,其次定义一个映射 $G \left(\boldsymbol{z}; \theta_g\right): \boldsymbol{z} \to \boldsymbol{x}$。对于判别器,我们则需要定义一个映射 $D \left(\boldsymbol{x}; \theta_d\right)$ 用于表示数据 $\boldsymbol{x}$ 是来自于真实数据,还是来自于 $p_g$。GAN 的网络框架如下图所示 2: 模型训练 Goodfellow 在文献中给出了一个重要的公式用于求解最优的生成器 $$ \min_{G} \max_{D} V\left(D, G\right) = \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} $$ 上式中,在给定的 $G$ 的情况下,$\max_{D} V\left(G, D\right)$衡量的是 $p_{data}$ 和 $p_g$ 之间的“区别”,因此我们最终的优化目标就是找到最优的 $G^*$ 使得 $p_{data}$ 和 $p_g$ 之间的“区别”最小。 首先,在给定 $G$ 的时候,我们可以通过最大化 $V \left(G, D\right)$ 得到最优 $D^*$ $$ \begin{equation} \begin{split} V \left(G, D\right) &= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ &= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) dx} + \int_{\boldsymbol{z}}{p_{\boldsymbol{z}} \left(\boldsymbol{z}\right) \log \left(1 - D\left(g\left(\boldsymbol{z}\right)\right)\right) dz} \\ &= \int_{\boldsymbol{x}}{p_{data}\left(\boldsymbol{x}\right) \log D\left(\boldsymbol{x}\right) + p_g\left(\boldsymbol{x}\right) \log \left(1 - D\left(\boldsymbol{x}\right)\right) dx} \end{split} \end{equation} $$ 对于给定的任意 $a, b \in \mathbb{R}^2 \setminus \{0, 0\}$,$a \log\left(x\right) + b \log\left(1 - x\right)$在 $x = \dfrac{a}{a+b}$ 处取得最大值,$D$ 的最优值为 $$ D_{G}^{*} = \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} $$ 因此,$\max_{D} V \left(G, D\right)$ 可重写为 $$ \begin{equation} \begin{split} &C\left(G\right) \\ =& \max_{D} V \left(G, D\right) = V \left(G, D^*\right) \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}\left(\boldsymbol{z}\right)}{\left[\log \left(1 - D_{G}^{*}\left(G\left(\boldsymbol{z}\right)\right)\right)\right]} \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ =& \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)} \right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \dfrac{p_g \left(\boldsymbol{x}\right)}{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}\right]} \\ =& \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{\dfrac{1}{2} p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log \dfrac{\dfrac{1}{2} p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} \\ =& \int_{x}{p_{data} \left(\boldsymbol{x}\right) \log \dfrac{p_{data} \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + \int_{x}{p_g \left(\boldsymbol{x}\right) \log \dfrac{p_g \left(\boldsymbol{x}\right)}{\dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}} dx} + 2 \log \dfrac{1}{2} \\ =& KL \left(p_{data} \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) + KL \left(p_g \left(\boldsymbol{x}\right) \Vert \dfrac{p_{data} \left(\boldsymbol{x}\right) + p_g \left(\boldsymbol{x}\right)}{2}\right) - 2 \log 2 \\ =& 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$ 其中 $KL$ 表示 KL 散度 3,$JS$ 表示 JS 散度 4,因此在全局最优情况下 $p_g = p_{data}$。 整个 GAN 的训练过程如下所示: 在实际的训练过程中,我们通常不会直接训练 $G$ 最小化 $\log \left(1 - D \left(G \left(\boldsymbol{z}\right)\right)\right)$,因为其在学习过程中的早起处于饱和状态,因此我们通常会通过最大化 $\log \left(D \left(G \left(z\right)\right)\right)$。 存在的问题 针对 GAN,包括 Goodfellow 自己在内也提出了其中包含的很多问题 2,因此后人也提出了大量的改进,衍生出了大量的 GAN 变种。本章节仅对原始的 GAN 中存在的问题进行简略介绍,相关的改进请参见后续的具体改进算法。 JS 散度问题 我们在训练判别器的时候,其目标是最大化 JS 散度,但 JS 散度真的能够很好的帮助我们训练判别器吗? Wasserstein GAN 一文 5给出了不同生成器情况下 JS 散度的变化情况。 上图中,左边为一个基于 MLP 的生成器,右边为一个 DCGAN 6 生成器,两者均有一个 DCGAN 的判别器。根据上文我们可以知道判别器的目标是最大化 $$ \begin{equation} \begin{split} L \left(D, \theta_g\right) &= \mathbb{E}_{\boldsymbol{x} \sim p_{data}{\left(\boldsymbol{x}\right)}}{\left[\log D_{G}^{*}\left(\boldsymbol{x}\right)\right]} + \mathbb{E}_{\boldsymbol{x} \sim p_g\left(\boldsymbol{x}\right)}{\left[\log \left(1 - D_{G}^{*}\left(\boldsymbol{x}\right)\right)\right]} \\ &= 2 JS \left(p_{data} \left(\boldsymbol{x}\right) \Vert p_g \left(\boldsymbol{x}\right) \right) - 2 \log 2 \end{split} \end{equation} $$ 上图中 Y 轴绘制的为 $\dfrac{1}{2} L \left(D, \theta_g\right) + \log 2$,因为 $-2 \log 2 \leq L \left(D, \theta_g\right) \leq 0$,因此我们可得 $0 \leq \dfrac{1}{2} L \left(D, \theta_g\right) + \log 2 \leq \log 2$。从图中我们可以看出,针对两种不同的情况,其值均很快的逼近最大值 $\log 2 \approx 0.69$,当接近最大值的时候,判别器将具有接近于零的损失,此时我们可以发现,尽管 JS 散度很快趋于饱和,但 DCGAN 生成器的效果却仍在不断的变好,因此,使用 JS 散度作为判别其的目标就显得不是很合适。 多样性问题 Mode Collapse Mode Collapse 问题是指生成器更多的是生成了大量相同模式的数据,导致的结果就是生成的数据缺乏多样性,如下图所示 7: 不难看出,其中红色方框圈出来的图像十分相似,这样的问题我们就称之为 Mode Collapse。Goolfellow 曾经从不同的 KL 散度的角度解释引起 Mode Collapse 的问题,但最后发现其并非由散度的不同所导致。对于 KL 散度,其并非是对称的,即 $D_{KL} \left(p_{data} \Vert p_{model}\right)$ 与 $D_{KL} \left(p_{model} \Vert p_{data}\right)$ 是不同的。在最大化似然估计的时候使用的是前者,而在最小化 JS 散度的时候使用的更类似于后者。如下图所示 假设我们的模型 $q$ 并没有足够能能力去拟合真实数据分布 $p$,假设真实数据由两个二维的高斯分布构成,而模型需要使用一个一维的高斯分布去拟合。在左图中,模型更倾向于覆盖两个高斯分布,也就是说其更倾向与在有真实数据的地方得到更大的概率。在右图中,模型更倾向于覆盖其中一个高斯分布,也就是说其更倾向于在没有真实数据的地方取得更小的概率。这样,如果我们用 JS 散度训练模型的时候就容易出现模式缺失的问题,但尽管我们利用前者去优化模型,但结果中仍然出现了 Mode Collapse 的问题,这也就说明并非 JS 散度问题导致的 Mode Collapse。 针对 Mode Collapse 的问题,出现了大量不同角度的优化 基于正则化的优化 8 基于 Minibatch 的优化 9 基于 Unrolled Optimization 的优化 10 基于集成算法的优化 11 MNIST 示例 我们利用 MNIST 数据集测试原始的 GAN 模型的效果,代码主要参考了 Keras-GAN,最终实现代码详见 image_gan_keras.py,我们简单对其核心部分进行说明。 生成器 def build_generator(self): model = Sequential() model.add(Dense(int(self._hidden_dim / 4), input_shape=self._noise_shape)) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(int(self._hidden_dim / 2))) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(self._hidden_dim)) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(np.prod(self._input_shape), activation='tanh')) model.add(Reshape(self._input_shape)) print('Generator Summary: ') model.summary() noise = Input(shape=self._noise_shape) image = model(noise) return Model(noise, image) 在生成器中,我们使用了一个包含3个隐含层的全链接网络,其中 self._hidden_dim 是我们定义的隐含节点最多一层的节点数;self._noise_shape 为用于生成器的噪音数据的形状;self._input_shape 为输入数据形状,即图片数据的形状,中间层次采用的激活函数为 LeakyReLU,最后一层采用的激活函数为 tanh。 判别器 def build_discriminator(self): model = Sequential() model.add(Flatten(input_shape=self._input_shape)) model.add(Dense(int(self._hidden_dim / 2))) model.add(LeakyReLU(alpha=0.2)) model.add(Dense(int(self._hidden_dim / 4))) model.add(LeakyReLU(alpha=0.2)) model.add(Dense(1, activation='sigmoid')) print('Discriminator Summary: ') model.summary() image = Input(shape=self._input_shape) label = model(image) return Model(image, label) 在判别器中,我们使用了一个包含2个隐含层的全链接网络,中间层次采用的激活函数为 LeakyReLU,最后一层采用的激活函数为 sigmoid。 对抗网络 class ImageBasicGAN(): def __init__(self, width, height, channels, a_optimizer=Adam(1e-4, beta_1=0.5), g_optimizer=Adam(1e-4, beta_1=0.5), d_optimizer=Adam(1e-4, beta_1=0.5), noise_dim=100, hidden_dim=1024): ''' Args: width: 图像宽度 height: 图像高度 channels: 图像颜色通道数 a_optimizer: 对抗网络优化器 g_optimizer: 生成器优化器 d_optimizer: 判别器优化器 noise_dim: 噪音数据维度 hidden_dim: 隐含层最大维度 ''' # 省略一大坨代码 # 构建和编译判别器 self._discriminator = self.build_discriminator() self._discriminator.compile(loss='binary_crossentropy', optimizer=d_optimizer, metrics=['accuracy']) # 构建和编译生成器 self._generator = self.build_generator() self._generator.compile(loss='binary_crossentropy', optimizer=g_optimizer) # 生成器利用噪声数据作为输入 noise = Input(shape=self._noise_shape) generated_image = self._generator(noise) # 当训练整个对抗网络时,仅训练生成器 self._discriminator.trainable = False # 判别器将生成的图像作为输入 label = self._discriminator(generated_image) # 构建和编译整个对抗网络 self._adversarial = Model(noise, label) self._adversarial.compile(loss='binary_crossentropy', optimizer=a_optimizer) 在构造整个对抗网络的时候,需要注意我们训练完判别器后,通过训练整个对抗网络进而训练生成器的时候是固定住训练好的判别器的,因此在训练整个对抗网络的时候我们应该将判别器置为无需训练的状态。 训练过程 def train(self, x_train, output_dir, iters, batch_size=32, k=1, save_interval=200): ''' 训练模型 Args: x_train: 训练数据 output_dir: 相关输出路径 iters: 迭代次数 batch_size: 批大小 k: K save_interval: 结果保存间隔 ''' # 省略一大坨代码 for iter in range(iters): # 训练判别器 for _ in range(k): train_indices = np.random.randint(0, x_train.shape[0], batch_size) train_images = x_train[train_indices] noises = np.random.normal(0, 1, (batch_size, self._noise_dim)) generated_images = self._generator.predict(noises) self._discriminator.train_on_batch(train_images, np.ones((batch_size, 1))) self._discriminator.train_on_batch(generated_images, np.zeros((batch_size, 1))) # 训练生成器 noises = np.random.normal(0, 1, (batch_size, self._noise_dim)) labels = np.ones(batch_size) self._adversarial.train_on_batch(noises, labels) # 再省略一大坨代码 在训练整个对抗网络的时候,我们对于一个给定的生成器,我们将生成器生成的数据作为负样本,将从真实数据中采样的数据作为正样本训练判别器。Goodfellow 在描述 GAN 训练的过程中,对于给定的生成器,训练判别器 $k$ 次,不过通常取 $k = 1$。训练好判别器后,再随机生成噪音数据用于训练生成器,周而复始直至达到最大迭代次数。 在整个训练过程中,我们分别记录了判别器和生成器的损失的变化,以及判别器的准确率的变化,如下图所示: 从上图中我们可以看出,在训练开始阶段,判别器能够相对容易的识别出哪些数据是来自于真实数据的采样,哪些数据是来自于生成器的伪造数据。随着训练的不断进行,判别器的准确率逐渐下降,并稳定在 60% 左右,也就是说生成器伪造的数据越来越像真实的数据,判别器越来越难进行甄别。 下图中我们展示了利用 MNIST 数据集,进行 30000 次的迭代,每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像,最后合成的一张生成图片的变化动图。 Deep Convolutional GAN DCGAN (Deep Convolutional GAN) 是由 Radford 6 等人提出的一种对原始 GAN 的变种,其基本的思想就是将原始 GAN 中的全链接层用卷积神经网络代替。在文中,Radford 等人给出构建一个稳定的 DCGAN 的建议,如下: 在网络中不使用 pooling 层,而是使用多步长的卷积层 (判别器) 和多步长的反卷积层 (生成器)。 在生成器和判别器中均使用批标准化。 对于深层的框架,去掉全链接层。 在生成器中使用 ReLU 激活函数,最后一层使用 Tanh 激活函数。 在判别器中使用 LeakyReLU 激活函数。 我们利用 MNIST 数据集测试 DCGAN 模型的效果,最终实现代码详见 image_dcgan_keras.py。训练过程中判别器和生成器的损失的变化,以及判别器的准确率的变化,如下图所示: 下图中我们展示了利用 MNIST 数据集,进行 30000 次的迭代,每 1000 次截取 100 张生成器利用相同噪音数据伪造的图像,最后合成的一张生成图片的变化动图。 从生成的结果中可以看出,DCGAN 生成的图片的质量还是优于原始的 GAN 的,在原始的 GAN 中我们能够明显的看出其中仍旧包含大量的噪音点,而在 DCGAN 中这种情况几乎不存在了。 Goodfellow, Ian, et al. “Generative adversarial nets.” Advances in neural information processing systems. 2014. ↩︎ Goodfellow, Ian. “NIPS 2016 tutorial: Generative adversarial networks.” arXiv preprint arXiv:1701.00160 (2016). ↩︎ ↩︎ https://en.wikipedia.org/wiki/Kullback–Leibler_divergence ↩︎ https://en.wikipedia.org/wiki/Jensen–Shannon_divergence ↩︎ Arjovsky, Martin, Soumith Chintala, and Léon Bottou. “Wasserstein gan.” arXiv preprint arXiv:1701.07875 (2017). ↩︎ Radford, Alec, Luke Metz, and Soumith Chintala. “Unsupervised representation learning with deep convolutional generative adversarial networks.” arXiv preprint arXiv:1511.06434 (2015). ↩︎ ↩︎ http://speech.ee.ntu.edu.tw/~tlkagk/courses/MLDS_2017/Lecture/GAN%20(v11).pdf ↩︎ Che, Tong, et al. “Mode regularized generative adversarial networks.” arXiv preprint arXiv:1612.02136 (2016). ↩︎ Salimans, Tim, et al. “Improved techniques for training gans.” Advances in Neural Information Processing Systems. 2016. ↩︎ Metz, Luke, et al. “Unrolled generative adversarial networks.” arXiv preprint arXiv:1611.02163 (2016). ↩︎ Tolstikhin, Ilya O., et al. “Adagan: Boosting generative models.” Advances in Neural Information Processing Systems. 2017. ↩︎

2018/2/3
articleCard.readMore

Ising 模型,Hopfield 网络和受限的玻尔兹曼机 (Ising, Hopfield and RBM)

Ising 模型 Ising 模型最早是由物理学家威廉·冷次在 1920 年发明的,他把该模型当成是一个给他学生恩斯特·易辛的问题。易辛在他一篇 1924 年的论文 1 中求得了一维易辛模型的解析解,并且证明它不会产生相变。 二维方晶格易辛模型相对于一维的难出许多,因此其解析的描述在一段时间之后才在 1943 年由拉斯·昂萨格给出 2。 Ising 模型假设铁磁物质是由一堆规则排列的小磁针构成,每个磁针只有上下两个方向。相邻的小磁针之间通过能量约束发生相互作用,同时受到环境热噪声的干扰而发生磁性的随机转变。涨落的大小由关键的温度参数决定,温度越高,随机涨落干扰越强,小磁针越容易发生无序而剧烈地状态转变,从而让上下两个方向的磁性相互抵消,整个系统消失磁性,如果温度很低,则小磁针相对宁静,系统处于能量约束高的状态,大量的小磁针方向一致,铁磁系统展现出磁性。而当系统处于临界温度 $T_C$ 时,Ising 模型表现出一系列幂律行为和自相似现象 3。 由于 Ising 模型的高度抽象,可以很容易地将它应用到其他领域之中。例如,将每个小磁针比喻为某个村落中的村民,而将小磁针上下的两种状态比喻成个体所具备的两种政治观点,相邻小磁针之间的相互作用比喻成村民之间观点的影响,环境的温度比喻成每个村民对自己意见不坚持的程度,这样 Ising 模型就可以建模该村落中不同政治见解的动态演化。在社会科学中,人们已经将 Ising 模型应用于股票市场、种族隔离、政治选择等不同的问题。另一方面,如果将小磁针比喻成神经元细胞,向上向下的状态比喻成神经元的激活与抑制,小磁针的相互作用比喻成神经元之间的信号传导,那么,Ising 模型的变种还可以用来建模神经网络系统,从而搭建可适应环境、不断学习的机器,例如 Hopfield 网络或 Boltzmann 机。 考虑一个二维的情况 如图所示,每个节点都有两种状态 $s_i \in \{+1, -1\}$,则我们可以定义这个系统的能量为 $$ E = -H \sum_{i=1}^{N}{s_i} - J \sum_{<i, j>}{s_i s_j} $$ 其中 $H$ 为外界磁场的强度,$J$ 为能量耦合常数,$\sum_{<i, j>}$表示对于相邻的两个节点的函数值求和。因此,可以得出 当每个节点的方向同外部磁场一致时,系统能量越小;反之系统能量越大。 对于 $J > 0$,当相邻的节点方向相同时,系统能量越小;反之系统能量越大。 对于整个系统的演变,除了系统的总能量以外,还受到节点所处环境的热噪声影响。我们利用温度 $T$ 表示环境对节点的影响,当 $T$ 越高时,节点状态发生变化的可能性越大。此时,则有两种力量作用在每个节点上 节点邻居和外部磁场的影响,这种影响使得当前节点尽可能的同其邻居和外部磁场保持一致,即尽可能是系统的总能量达到最小。 环境的影响,这种影响使得每个节点的状态以一定的概率发生随机变化。 不难想像,当 $T = 0$ 时,节点状态完全受其邻居和外部磁场影响,当 $J = 0, H = 0$ 时,节点处于完全的随机状态。 对于 Ising 模型,我们利用蒙特卡罗方法进行模拟。初始化系统状态为 $s_i^{\left(0\right)}$,对于任意时刻 $t$,对其状态 $s_i^{\left(t\right)}$进行一个改变,将其中一个节点变为相反的状态,得到新的状态 $s'_i$ $$ s_i^{\left(t+1\right)} = \begin{cases} s'_i & \text{with probablity of } \mu \\ s_i^{\left(t\right)} & \text{with probablity of } 1-\mu \end{cases} $$ 其中 $\mu = \min\left\lbrace\dfrac{e^{E\left(s_i^{\left(t\right)}\right) - E\left(s'_i\right)}}{kT}, 1\right\rbrace$ 表示接受转移的概率;$k \approx 1.38 \times 10^{23}$ 为玻尔兹曼常数。我们利用蒙特卡罗方法对其进行模拟 $T = 4J/k$的情况,我们分别保留第 $0, 1, 5, 50, 500, 5000$ 步的模拟结果 # 每一轮状态转移 each_round <- function(current_matrix, ising_config) { n_row <- nrow(current_matrix) n_col <- ncol(current_matrix) for (i in 1:n_row) { for (j in 1:n_col) { current_row <- sample(1:n_row, 1) current_col <- sample(1:n_col, 1) s <- current_matrix[current_row, current_col] e <- -(current_matrix[(current_row-1-1)%%n_row+1, current_col] + current_matrix[current_row, (current_col-1-1)%%n_col+1] + current_matrix[(current_row+1)%%n_row, current_col] + current_matrix[current_row, (current_col+1)%%n_col]) * s * ising_config$j mu <- min(exp((e + e) / (ising_config$k * ising_config$t)), 1) mu_random <- runif(1) if (mu_random < mu) { s <- -1 * s } current_matrix[current_row, current_col] <- s } } current_matrix } # Ising 模拟 ising_simulation <- function(N, iter, ising_config, saved_steps) { set.seed(112358) current_matrix <- matrix(sample(0:1, N^2, replace = T), N, N)*2-1 saved_matrix <- list() if (0 %in% saved_steps) { saved_matrix <- c(saved_matrix, list(current_matrix)) } for (i in 1:iter) { if (i %in% saved_steps) { saved_matrix <- c(saved_matrix, list(current_matrix)) } current_matrix <- each_round(current_matrix, ising_config) if (i %% 1000 == 0) { cat(paste0("Steps: ", i, '\n')) } } saved_matrix } # T = 4J/K,方便模拟取 j = 1, k = 1, t = 4 ising_config <- list(j = 1, k = 1, t = 4) diff_steps_matrix <- ising_simulation(100, 5000, ising_config, c(0, 1, 5, 50, 500, 5000)) 模拟结果可视化效果如图所示 对于二维的 Ising 模型,存在一个相变点,在相变点上的温度 $T_c$ 满足 $$ \sinh\left(\dfrac{2J_1}{kT_c}\right) \sinh\left(\dfrac{2J_2}{kT_c}\right) = 1 $$ 若 $J_1 = J_2$,则 $$ T_c = \dfrac{2J}{k \ln\left(1 + \sqrt{2}\right)} \approx 2.27 \dfrac{J}{k} $$ 称之为临界温度。当温度小于临界值的时候,Ising 模型中大多数节点状态相同,系统处于较为秩序的状态。当温度大于临界值的时候,大多数节点的状态较为混乱,系统处于随机的状态。而当温度接近临界的时候,系统的运行介于随机与秩序之间,也就是进入了混沌的边缘地带,这种状态称为临界状态。 我们模拟不同温度下,系统在运行 $50$ 步时的状态 ising_config_t <- c(1, 2, 2.27, 2.5, 3, 6) diff_t_matrix <- lapply(ising_config_t, function(t) { ising_config <- list(j = 1, k = 1, t = t) ising_simulation(100, 50, ising_config, c(50)) }) 模拟结果可视化效果如图所示 Hopfield 神经网络 Hopfield 神经网络 4 是一种基于能量的反馈人工神经网络。Hopfield 神经网络分为离散型 (Discrete Hopfield Neural Network, DHNN) 和 连续性 (Continues Hopfield Neural Network, CHNN)。 离散型 Hopfield 神经网络 网络结构 对于离散型 Hopfield 神经网络,其网络结果如下 对于具有 $n$ 个神经元的网络,我们设 $t$ 时刻的网络状态为 $\boldsymbol{X}^{\left(t\right)} = \left(x_1^{\left(t\right)}, x_2^{\left(t\right)}, ..., x_n^{\left(t\right)}\right)^T$,对于 $t+1$ 时刻网络的状态 $$ x_i^{\left(t+1\right)} = f \left(net_i\right) $$ 其中,DHNN 中 $f$ 多为符号函数,即 $$ \renewcommand{\sign}{\operatorname{sign}} x_i = \sign \left(net_i\right) = \begin{cases} 1, net_i \geq 0 \\ -1, net_i < 0 \end{cases} $$ $net_i$ 为一个节点的输入,为 $$ net_i = \sum_{j=1}^{n}{\left(w_{ij}x_j - T_i\right)} $$ 其中 $T_i$ 为每个神经元的阈值,对于 DHNN,一般有 $w_{ii} = 0, w_{ij} = w_{ji}$,当反馈网络稳定后,稳定后的状态即为网络的输出。网络的更新主要有两种状态,异步方式和同步方式。 对于异步方式的更新方法,每一次仅改变一个神经元 $j$ 的状态,即 $$ x_i^{\left(t+1\right)} = \begin{cases} \sign\left(net_i^{\left(t\right)}\right), i = j \\ x_i^{\left(t\right)}, i \neq j \end{cases} $$ 对于同步方式的更新方法,每一次需改变所有神经元的状态,即 $$ x_i^{\left(t+1\right)} = \sign\left(net_i^{\left(t\right)}\right) $$ 网络稳定性 我们可以将反馈网络看做一个非线性动力学系统,因此这个系统最后可能会收敛到一个稳态,或在有限状态之间振荡,亦或是状态为无穷多个即混沌现象。对于 DHNN 因为其网络状态是有限的,因此不会出现混沌的现象。若一个反馈网络达到一个稳态状态 $\boldsymbol{X}$ 时,即 $\boldsymbol{X}^{\left(t+1\right)} = \boldsymbol{X}^{\left(t\right)}$ ,则称这个状态为一个吸引子。在 Hopfield 网络结构和权重确定的情况下,其具有 $M$ 个吸引子,因此我们可以认为这个网络具有存储 $M$ 个记忆的能力。 设 $\boldsymbol{X}$ 为网络的一个吸引子,权重矩阵 $\boldsymbol{W}$ 是一个对称阵,则定义 $t$ 时刻网络的能量函数为 $$ E\left(t\right) = -\dfrac{1}{2} \boldsymbol{X}^{\left(t\right)T} \boldsymbol{W} \boldsymbol{X}^{\left(t\right)} + \boldsymbol{X}^{\left(t\right)T} \boldsymbol{T} $$ 则定义网络能量的变化量 $$ \Delta E\left(t\right) = E\left(t+1\right) - E\left(t\right) $$ 则以异步更新方式,不难推导得出 $$ \begin{equation} \begin{split} \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} \left(\sum_{j=1}^{n}{\left(w_{ij}x_j - T_j\right)}\right) - \dfrac{1}{2} \Delta x_i^{\left(t\right)2} w_{ii} \end{split} \end{equation} $$ 由于网络中的神经元不存在自反馈,即 $w_{ii} = 0$,则上式可以化简为 $$ \Delta E\left(t\right) = -\Delta x_i^{\left(t\right)} net_i^{\left(t\right)} $$ 因此,对于如上的能量变化,可分为 3 中情况: 当 $x_i^{\left(t\right)} = -1, x_i^{\left(t+1\right)} = 1$ 时,$\Delta x_i^{\left(t\right)} = 2, net_i^{\left(t\right)} \geq 0$,则可得 $\Delta E \left(t\right) \leq 0$。 当 $x_i^{\left(t\right)} = 1, x_i^{\left(t+1\right)} = -1$ 时,$\Delta x_i^{\left(t\right)} = -2, net_i^{\left(t\right)} < 0$,则可得 $\Delta E \left(t\right) < 0$。 当 $x_i^{\left(t\right)} = x_i^{\left(t+1\right)}$ 时,$\Delta x_i^{\left(t\right)} = 0$,则可得 $\Delta E \left(t\right) = 0$。 则对于任何情况,$\Delta E \left(t\right) \leq 0$,也就是说在网络不断变化的过程中,网络的总能量是一直下降或保持不变的,因此网络的能量最终会收敛到一个常数。 设 $\boldsymbol{X}'$ 为吸引子,对于异步更新方式,若存在一个变换顺序,使得网络可以从状态 $\boldsymbol{X}$ 转移到 $\boldsymbol{X}'$,则称 $\boldsymbol{X}$ 弱吸引到 $\boldsymbol{X}'$,这些 $\boldsymbol{X}$ 的集合称之为 $\boldsymbol{X}$ 的弱吸引域;若对于任意变换顺序,都能够使得网络可以从状态 $\boldsymbol{X}$ 转移到 $\boldsymbol{X}'$,则称 $\boldsymbol{X}$ 强吸引到 $\boldsymbol{X}'$,对于这些 $\boldsymbol{X}$ 称之为 $\boldsymbol{X}$ 的强吸引域。 对于 Hopfield 网络的权重,我们利用 Hebbian 规则进行设计。Hebbian 规则认为如果两个神经元同步激发,则它们之间的权重增加;如果单独激发,则权重减少。则对于给定的 $p$ 个模式样本 $\boldsymbol{X}^k, k = 1, 2, ..., p$,其中 $x \in \{-1, 1\}^n$ 且样本之间两两正交,则权重计算公式为 $$ w_{ij} = \dfrac{1}{n} \sum_{k=1}^{p}{x_i^k x_j^k} $$ 则对于给定的样本 $\boldsymbol{X}$ 确定为网络的吸引子,但对于有些非给定的样本也可能是网络的吸引子,这些吸引子称之为伪吸引子。以上权重的计算是基于两两正交的样本得到的,但真实情况下很难保证样本两两正交,对于非正交的模式,网络的存储能力则会大大下降。根据 Abu-Mostafa5 的研究表明,当模式的数量 $p$ 大于 $0.15 n$ 时,网络的推断就很可能出错,也就是结果会收敛到伪吸引子上。 示例 我们通过一个手写数字识别的例子介绍一些 Hopfield 网络的功能,我们存在如下 10 个数字的图片,每张为像素 16*16 的二值化图片,其中背景色为白色,前景色为黑色 (每个图片的名称为 num.png,图片位于 /images/cn/2018-01-17-ising-hopfield-and-rbm 目录)。 首先我们载入每张图片的数据 library(EBImage) # 载入数据 digits <- lapply(0:9, function(num) { readImage(paste0(num, '.png')) }) # 转换图像为 16*16 的一维向量 # 将 (0, 1) 转换为 (-1, 1) digits_patterns <- lapply(digits, function(digit) { pixels <- c(digit) pixels * 2 - 1 }) 接下来利用这 10 个模式训练一个 Hopfield 网络 #' 训练 Hopfield 网络 #' #' @param n 网络节点个数 #' @param pattern_list 模式列表 #' @return 训练好的 Hopfield 网络 train_hopfield <- function(n, pattern_list) { weights <- matrix(rep(0, n*n), n, n) n_patterns <- length(pattern_list) for (i in 1:n_patterns) { weights <- weights + pattern_list[[i]] %o% pattern_list[[i]] } diag(weights) <- 0 weights <- weights / n_patterns list(weights = weights, n = n) } # 训练 Hopfield 网络 digits_hopfield_network <- train_hopfield(16*16, digits_patterns) 为了测试 Hopfiled 网络的记忆能力,我们利用 10 个模式生成一些测试数据,我们分别去掉图像的右边或下边的 5 个像素,生成新的 20 张测试图片 # 构造测试数据 digits_test_remove_right <- lapply(0:9, function(num) { digit_test <- digits[[num+1]] digit_test[12:16, ] <- 1 digit_test }) digits_test_remove_bottom <- lapply(0:9, function(num) { digit_test <- digits[[num+1]] digit_test[, 12:16] <- 1 digit_test }) digits_test <- c(digits_test_remove_right, digits_test_remove_bottom) # 转换图像为 16*16 的一维向量 # 将 (0, 1) 转换为 (-1, 1) digits_test_patterns <- lapply(digits_test, function(digit) { pixels <- c(digit) pixels * 2 - 1 }) 我们利用训练好的 Hopfield 网络运行测试数据,我们迭代 300 次并保存最后的网络输出 #' 运行 Hopfiled 网络 #' @param hopfield_network 训练好的 Hopfield 网络 #' @param pattern 输入的模式 #' @param max_iter 最大迭代次数 #' @param save_history 是否保存状态变化历史 #' @return 最终的模式 (以及历史模式) run_hopfield <- function(hopfield_network, pattern, max_iter = 100, save_history = T) { last_pattern <- pattern history_patterns <- list() for (iter in 1:max_iter) { current_pattern <- last_pattern i <- round(runif(1, 1, hopfield_network$n)) net_i <- hopfield_network$weights[i, ] %*% current_pattern current_pattern[i] <- ifelse(net_i < 0, -1, 1) if (save_history) { history_patterns[[iter]] <- last_pattern } last_pattern <- current_pattern } list(history_patterns = history_patterns, final_pattern = last_pattern) } # 运行 Hopfield 网络,获取测试数据结果 digits_test_results_patterns <- lapply(digits_test_patterns, function(pattern) { run_hopfield(digits_hopfield_network, pattern, max_iter = 300) }) # 转换测试数据结果为图片 digits_test_results <- lapply(digits_test_results_patterns, function(result) { each_dim <- sqrt(digits_hopfield_network$n) Image((result$final_pattern + 1) / 2, dim = c(each_dim, each_dim), colormode = 'Grayscale') }) 网络变换过程中,图像的变换如图所示 最终网络的输出如图所示 从结果中可以看出,部分测试图片还是得到了比较好的恢复,但如上文所说,由于我们给定的模式之间并不是两两正交的,因此,网络的推断就很可能出错 (例如:数字 5 恢复的结果更像 9 多一些),甚至结果会收敛到伪吸引子上。 连续型 Hopfield 神经网络 网络结构 连续型 Hopfield 网络相比于离散型 Hopfield 网络的主要差别在于: 网络中所有的神经元随时间 $t$ 同时更新,网络状态随时间连续变化。 神经元的状态转移函数为一个 S 型函数,例如 $$ v_i = f\left(u_i\right) = \dfrac{1}{1 + e^{\dfrac{-2 u_i}{\gamma}}} = \dfrac{1}{2} \left(1 + \tanh \dfrac{u_i}{\gamma}\right) $$ 其中,$v_i$ 表示一个神经元的输出,$u_i$ 表示一个神经元的输入。 对于理想情况,网络的能量函数可以写为6 $$ E = -\dfrac{1}{2} \sum_{i=1}^{n}{\sum_{j=1}^{n}{w_{ij} v_i v_j}} - \sum_{i=1}^{n} v_i I_i $$ 可以得出,随着网络的演变,网络的总能量是降低的,随着网络中节点的不断变化,网络最终收敛到一个稳定的状态。 TSP 问题求解 旅行推销员问题 (Travelling salesman problem, TSP) 是指给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短路径 7。TSP 问题是一个 NP-hard 问题 8。 对于 TSP 问题,我们给定一个城市指之间的距离矩阵 $$ D = \left\lgroup \begin{array}{cccc} d_{11} & d_{12} & \cdots & d_{1n} \\ d_{21} & d_{22} & \cdots & d_{2n} \\ \vdots & \vdots & & \vdots \\ d_{n1} & d_{n2} & \cdots & d_{nn} \end{array} \right\rgroup $$ 其中 $d_{ij} = d_{ji}, i \neq j$ 表示城市 $i$ 和城市 $j$ 之间的距离,$d_{ij} = 0, i = j$。TSP 问题的优化目标是找到一条路径访问每一座城市一次并回到起始城市,我们利用一个矩阵表示访问城市的路径 $$ V = \left\lgroup \begin{array}{cccc} v_{11} & v_{12} & \cdots & v_{1n} \\ v_{21} & v_{22} & \cdots & v_{2n} \\ \vdots & \vdots & & \vdots \\ v_{n1} & v_{n2} & \cdots & v_{nn} \end{array} \right\rgroup $$ 其中 $v_{xi} = 1$ 表示第 $i$ 次访问城市 $x$,因此对于矩阵 $V$,其每一行每一列仅有一个元素值为 $1$,其他元素值均为 $0$。 对于 TSP 问题,我们可以得到如下约束条件 城市约束 因为每个城市只能访问一次,因此对于第 $x$ 行仅能有一个元素是 $1$,其他均为 $0$,即任意两个相邻元素的乘积为 $0$ $$ \sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}} = 0 $$ 则对于城市约束,我们得到该约束对应的能量分量为 $$ E_1 = \dfrac{1}{2} A \sum_{x=1}^{n}{\sum_{i=1}^{n-1}{\sum_{j=i+1}^{n}{v_{xi}v_{xj}}}} $$ 时间约束 因为每一时刻仅能够访问一个城市,因此对于第 $i$ 行仅能有一个元素是 $1$,其他均为 $0$,即任意两个相邻元素的乘积为 $0$ $$ \sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}} = 0 $$ 则对于时间约束,我们得到该约束对应的能量分量为 $$ E_2 = \dfrac{1}{2} B \sum_{i=1}^{n}{\sum_{x=1}^{n-1}{\sum_{y=x+1}^{n}{v_{xi}v_{yi}}}} $$ 有效性约束 当矩阵 $V$ 中所有的元素均为 $0$ 的时候,可得 $E_1 = 0, E_2 = 0$,但显然这并不是一个有效的路径,因此我们需要保证矩阵 $V$ 中元素值为 $1$ 的个数为 $n$,即 $$ \sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} = n $$ 则对于有效性约束,我们得到该约束对应的能量分量为 $$ E_3 = \dfrac{1}{2} C \left(\sum_{x=1}^{n}{\sum_{i=1}^{n}{v_{xi}}} - n\right)^2 $$ 路径长度约束 如上三个约束仅能够保证我们的路径是有效的,但并不一定是最优的。根绝 TSP 问题的优化目标,我们需要引入一个反映路径长度的能量分量,并保证该能量分量随着路径长度的减小而减小。访问两个城市 $x, y$ 有两种形式,$x \to y$ 或 $y \to x$,如果城市 $x$ 和城市 $y$ 在旅行中顺序相邻,则 $v_{xi}v_{y,i+1} = 1, v_{xi}v_{y,i-1} = 0$,反之亦然。则反映路径长度的能量分量可以定义为 $$ E_4 = \dfrac{1}{2} D \sum_{x=1}^{n}{\sum_{y=1}^{n}{\sum_{i=1}^{n}{d_{xy}\left(v_{xi}v_{y,i+1} + v_{xi}v_{y,i-1}\right)}}} $$ 综上所述,TSP 问题的能量函数定义为 $$ E = E_1 + E_2 + E_3 + E_4 $$ 其中,$A, B, C, D > 0$ 分别为每个能量分量的权重。针对这样的能量函数,我们可得对应神经元 $x_i$ 和 $y_i$ 之间的权重为 $$ \begin{equation} \begin{split} w_{x_i, y_i} = &-2A \delta_{xy} \left(1-\delta_{xy}\right) - 2B \delta_{ij} \left(1-\delta_{xy}\right) \\ &- 2C -2D d_{xy} \left(\delta_{j, i+1} + \delta_{i, j+1}\right) \end{split} \end{equation} $$ 其中 $$ \delta_{xy} = \begin{cases} 1, x = y \\ 0, x \neq y \end{cases} , \delta_{ij} = \begin{cases} 1, i = j \\ 0, i \neq j \end{cases} $$ 因此可以得到网络关于时间的导数 $$ \begin{equation} \begin{split} \dfrac{d u_{xi}}{d t} = &-2A \sum_{j \neq i}^{n}{v_{xj}} - 2B \sum_{y \neq x}^{n}{v_{yi}} - 2C \left(\sum_{x=1}^{n}{\sum_{j=1}^{n}{v_{xj}}} - n\right) \\ &- 2D \sum_{y \neq x}^{n}{d_{xy}\left(v_{y, i+1} + v_{y, i-1}\right)} - \dfrac{u_{xi}}{\tau} \end{split} \end{equation} $$ 据此,我们以一个 10 个城市的数据为例,利用 CHNN 求解 TSP 问题,其中 10 个城市的座标为 城市 横座标 纵座标 A 0.4000 0.4439 B 0.2439 0.1463 C 0.1707 0.2293 D 0.2293 0.7610 E 0.5171 0.9414 F 0.8732 0.6536 G 0.6878 0.5219 H 0.8488 0.3609 I 0.6683 0.2536 J 0.6195 0.2634 已知的最优路线为 $A \to D \to E \to F \to G \to H \to I \to J \to B \to C \to A$,最优路线的路径长度为 $2.6907$。我们使用如下参数求解 TSP 问题,初始化 $u_{init} = -\dfrac{\gamma}{2} \ln\left(n - 1\right)$,$\gamma = 0.02$,学习率 $\alpha = 0.0001$,神经元激活阈值 $\theta = 0.7$,$\tau = 1$,能量分量权重参数 $A = 500, B = 500, C = 1000, D = 500$,单次迭代最大次数为 1000,共模拟 100 次。 # 城市座标 cities <- data.frame( l = LETTERS[1:10], x = c(0.4000, 0.2439, 0.1707, 0.2293, 0.5171, 0.8732, 0.6878, 0.8488, 0.6683, 0.6195), y = c(0.4439, 0.1463, 0.2293, 0.7610, 0.9414, 0.6536, 0.5219, 0.3609, 0.2536, 0.2634) ) # 通过城市座标构建距离矩阵 distance_matrix <- function(points) { n <- nrow(points) d <- matrix(rep(0, n^2), n, n) for (i in 1:n) { for (j in i:n) { distance <- sqrt((points[i, ]$x - points[j, ]$x)^2 + (points[i, ]$y - points[j, ]$y)^2) d[i, j] <- distance d[j, i] <- distance } } d } # 结果约束校验 check_path_valid <- function(v, n) { # 城市约束 c1 <- 0 for (x in 1:n) { for (i in 1:(n-1)) { for (j in (i+1):n) { c1 <- c1 + v[x, i] * v[x, j] } } } # 时间约束 c2 <- 0 for (i in 1:n) { for (x in 1:(n-1)) { for (y in (x+1):n) { c2 <- c2 + v[x, i] * v[y, i] } } } # 有效性约束 c3 <- sum(v) ifelse(c1 == 0 & c2 == 0 & c3 == n, T, F) } # 根据结果矩阵获取路径 v_to_path <- function(v, n) { p <- c() for (i in 1:n) { for (x in 1:n) { if (v[x, i] == 1) { p <- c(p, x) break } } } p } # 计算结果矩阵的路径长度 path_distance <- function(v, n, d) { p <- v_to_path(v, n) p <- c(p, p[1]) distance <- 0 for (i in 1:(length(p)-1)) { distance <- distance + d[p[i], p[i+1]] } distance } # 构建 Hopfield 网络 tsp_chnn <- function(d, n, gamma = 0.02, alpha = 0.0001, theta = 0.7, tau = 1, A = 500, B = 500, C = 1000, D = 500, max_iter = 1000) { v <- matrix(runif(n^2), n, n) u <- matrix(rep(1, n^2), n, n) * (-gamma * log(n-1) / 2) du <- matrix(rep(0, n^2), n, n) for (iter in 1:max_iter) { for (x in 1:n) { for (i in 1:n) { # E1 e1 <- 0 for (j in 1:n) { if (j != i) { e1 <- e1 + v[x, j] } } e1 <- -A * e1 # E2 e2 <- 0 for (y in 1:n) { if (y != x) { e2 <- e2 + v[y, i] } } e2 <- -B * e2 # E3 e3 <- -C * (sum(v) - n) # E4 e4 <- 0 for (y in 1:n) { if (y != x) { e4 <- e4 + d[x, y] * (v[y, (i+1-1)%%n+1] + v[y, (i-1-1)%%n+1]) } } e4 <- -D * e4 du[x, i] <- e1 + e2 + e3 + e4 - u[x, i] / tau } } u <- u + alpha * du v <- (1 + tanh(u / gamma)) / 2 v <- ifelse(v >= theta, 1, 0) } v } # 利用 Hopfiled 网络求解 TSP 问题 set.seed(112358) n <- 10 d <- distance_matrix(cities) # 模拟 100 次并获取最终结果 tsp_solutions <- lapply(1:100, function(round) { v <- tsp_chnn(d, n) valid <- check_path_valid(v, n) distance <- ifelse(valid, path_distance(v, n, d), NA) list(round = round, valid = valid, distance = distance, v = v) }) # 获取最优结果 best_tsp_solution <- NA for (tsp_solution in tsp_solutions) { if (tsp_solution$valid) { if (!is.na(best_tsp_solution)) { if (tsp_solution$distance < best_tsp_solution$distance) { best_tsp_solution <- tsp_solution } } else { best_tsp_solution <- tsp_solution } } } # 可视化最优结果 best_tsp_solution_path <- v_to_path(best_tsp_solution$v, n) ordered_cities <- cities[best_tsp_solution_path, ] %>% mutate(ord = seq(1:10)) best_tsp_solution_path_p <- ggplot(ordered_cities) + geom_polygon(aes(x, y), color = 'black', fill = NA) + geom_point(aes(x, y)) + geom_text(aes(x, y, label = l), vjust = -1) + geom_text(aes(x, y, label = ord), vjust = 2) + coord_fixed() + ylim(c(0, 1)) + xlim(c(0, 1)) + theme(axis.title = element_blank()) print(best_tsp_solution_path_p) 受限的玻尔兹曼机 (RBM) 网络结构及其概率表示 受限的玻尔兹曼机 (Restricted Boltzmann Machine, RBM) 或簧风琴 (harmonium) 是由 Smolensky 与 1986年在玻尔兹曼机 (Boltzmann Machine, BM) 基础上提出的一种随机神经网络 (Stochastic Neural Networks) 9。受限的玻尔兹曼机对于原始的玻尔兹曼机做了相应的限制,在其网络结构中包含可见节点和隐藏节点,并且可见节点和隐藏节点内部不允许存在连接,更加形象的可以将其理解为一个二分图。 对于二值版本的 RBM 而言,其中可见层 $\mathbf{v} = \left(v_1, v_2, ..., v_{n_v}\right)^T$ 由 $n_v$ 个二值随机变量构成;隐藏层 $\mathbf{h} = \left(h_1, h_2, ..., h_{n_h}\right)^T$ 由 $n_h$ 个二值随机变量构成。 RBM 同样作为一个基于能量的模型,其能量函数定义为: $$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\sum_{i=1}^{n_v}{b_i v_i} -\sum_{j=1}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1}^{n_h}{v_i w_{i,j} h_i}} $$ 将其表示成矩阵向量的形式,可记为: $$ E \left(\boldsymbol{v}, \boldsymbol{h}\right) = -\boldsymbol{b}^T \boldsymbol{v} - \boldsymbol{c}^T \boldsymbol{h} - \boldsymbol{v}^T \boldsymbol{W} \boldsymbol{h} $$ 其中 $\boldsymbol{b} \in \mathbb{R}^{n_v}$ 为可见层的偏置向量;$\boldsymbol{c} \in \mathbb{R}^{n_h}$ 为隐含层的偏置向量;$\boldsymbol{W} \in \mathbb{R}^{n_v \times n_h}$ 为可见层和隐含层之间的权重矩阵。根据能量函数,可得其联合概率分布为: $$ P \left(\mathbf{v} = \boldsymbol{v}, \mathbf{h} = \boldsymbol{h}\right) = \dfrac{1}{Z} e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)} $$ 其中 $Z$ 为归一化常数,成为配分函数: $$ Z = \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{e^{-E \left(\boldsymbol{v}, \boldsymbol{h}\right)}}} $$ 对于 RBM 我们更加关注的的为边缘分布,即: $$ P \left(\boldsymbol{v}\right) = \sum_{h}{P\left(\boldsymbol{v}, \boldsymbol{h}\right)} = \dfrac{1}{Z} \sum_{h}{e^{-E\left(\boldsymbol{v}, \boldsymbol{h}\right)}} $$ 因为概率中包含归一化常数,我们需要计算 $Z$,从其定义可得,当穷举左右可能性的化,我们需要计算 $2^{n_v + n_h}$ 个项,其计算复杂度很大。尽管 $P\left(\boldsymbol{v}\right)$ 计算比较困难,但是其条件概率 $P\left(\mathbf{h} | \mathbf{v}\right)$ 和 $P\left(\mathbf{v} | \mathbf{h}\right)$ 计算和采样相对容易。为了便于推导,我们定义如下记号: $$ \boldsymbol{h}_{-k} = \left(h_1, h_2, ..., h_{k-1}, h_{k+1}, ..., h_{n_h}\right)^T $$ 则 $P\left(h_k = 1 | \boldsymbol{v}\right)$ 定义如下: $$ \begin{equation} \begin{split} &P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) \\ = &\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_{-k}, \boldsymbol{v}\right)} \\ = &\dfrac{P\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}{P\left(h_k = 1 | h_{-k}, \boldsymbol{v}\right) + P\left(h_k = 0 | h_{-k}, \boldsymbol{v}\right)} \\ = &\dfrac{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{\dfrac{1}{Z} e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + \dfrac{1}{Z} e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &\dfrac{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)}}{e^{-E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right)} + e^{-E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ = &\dfrac{1}{1 + e^{E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) - E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right)}} \\ \end{split} \end{equation} $$ 其中: $$ \begin{equation} \begin{split} &E\left(h_k = 1, h_{-k}, \boldsymbol{v}\right) \\ = &E\left(h_k = 1, \boldsymbol{v}\right) \\ = &-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} - c_k - \sum_{i=1}^{n_v}{v_i W_{i, k}} \\ &E\left(h_k = 0, h_{-k}, \boldsymbol{v}\right) \\ = &E\left(h_k = 0, \boldsymbol{v}\right) \\ = &-\sum_{i=1}^{n_v}{b_i v_i} - \sum_{j=1, j \neq k}^{n_h}{c_j h_j} - \sum_{i=1}^{n_v}{\sum_{j=1, j \neq k}^{n_h}{v_i W_{i, j} h_i}} \end{split} \end{equation} $$ 因此,$P\left(h_k = 1 | \boldsymbol{v}\right)$ 可以化简为: $$ \begin{equation} \begin{split} &P\left(h_k = 1 | \boldsymbol{v}\right) \\ = &\dfrac{1}{1 + e^{-\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right)}} \\ = &\sigma\left(c_k + \sum_{i=1}^{n_v}{v_i W_{i, k}}\right) \\ = &\sigma\left(c_k + \boldsymbol{v}^T \boldsymbol{W}_{:, k}\right) \end{split} \end{equation} $$ 其中,$\sigma$ 为 sigmoid 函数。因此,我们可以将条件分布表示为连乘的形式: $$ \begin{equation} \begin{split} P\left(\boldsymbol{h} | \boldsymbol{v}\right) &= \prod_{j=1}^{n_h}{P\left(h_j | \boldsymbol{v}\right)} \\ &= \prod_{j=1}^{n_h}{\sigma\left(\left(2h - 1\right) \odot \left(\boldsymbol{c} + \boldsymbol{W}^T \boldsymbol{v}\right)\right)_j} \end{split} \end{equation} $$ 同理可得: $$ \begin{equation} \begin{split} P\left(\boldsymbol{v} | \boldsymbol{h}\right) &= \prod_{i=1}^{n_v}{P\left(v_i | \boldsymbol{h}\right)} \\ &= \prod_{i=1}^{n_v}{\sigma\left(\left(2v - 1\right) \odot \left(\boldsymbol{b} + \boldsymbol{W} \boldsymbol{h}\right)\right)_i} \end{split} \end{equation} $$ 模型训练 10 对于 RBM 模型的训练,假设训练样本集合为 $S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$,其中 $\boldsymbol{v^i} = \left(v_{1}^{i}, v_{2}^{i}, ..., v_{n_v}^{i}\right), i = 1, 2, ..., n_s$。则训练 RBM 的目标可以定义为最大化如下似然: $$ \mathcal{L}_{\theta, S} = \prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} $$ 其中 $\theta$ 为待优化的参数,为了方便计算,等价目标为最大化其对数似然: $$ \ln\mathcal{L}_{\theta, S} = \ln\prod_{i=1}^{n_s}{P\left(\boldsymbol{v}^i\right)} = \sum_{i=1}^{n_s}{\ln P\left(\boldsymbol{v}^i\right)} $$ 我们将其对数似然简写为 $\ln\mathcal{L}_S$ ,通过梯度上升方法,我们可以得到参数的更新公式: $$ \theta = \theta + \eta \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} $$ 对于单个样本 $\boldsymbol{\color{red}{v'}}$ ,有: $$ \begin{equation} \begin{split} \dfrac{\partial \ln\mathcal{L}_S}{\partial \theta} &= \dfrac{\partial \ln P\left(\boldsymbol{\color{red}{v'}}\right)}{\partial \theta} = \dfrac{\partial \ln \left(\dfrac{1}{Z} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right)}{\partial \theta} \\ &= \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln Z\right)}{\partial \theta} = \dfrac{\partial \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}} - \ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right)}{\partial \theta} \\ &= \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}\right) - \dfrac{\partial}{\partial \theta} \left(\ln \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}\right) \\ &= -\dfrac{1}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \dfrac{1}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{\dfrac{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}{Z}}{\dfrac{\sum_{\boldsymbol{h}}{e^{-E\left(\boldsymbol{\color{red}{v'}, h}\right)}}}{Z}} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{\dfrac{P\left(\boldsymbol{\color{red}{v'}, h}\right)}{P\left(\boldsymbol{\color{red}{v'}}\right)} \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{\dfrac{e^{-E\left(\boldsymbol{v, h}\right)}}{\sum_{\boldsymbol{v, h}}{e^{-E\left(\boldsymbol{v, h}\right)}}} \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \\ &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} \end{split} \end{equation} $$ 其中: $$ \begin{equation} \begin{split} \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}} &= \sum_{\boldsymbol{v}}{\sum_{\boldsymbol{h}}{P\left(\boldsymbol{v}\right) P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \\ &= \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) \sum_{\boldsymbol{h}}{P \left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial \theta}}} \end{split} \end{equation} $$ 则对于参数 $w_{i, j}$ 可得: $$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_i v_j} \\ = &-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_i v_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-i} | \boldsymbol{v}\right) h_i v_j} \\ = &-\sum_{\boldsymbol{h_i}}{\sum_{h_{-i}}{P\left(h_i | \boldsymbol{v}\right) P\left(\boldsymbol{h_{-i}} | \boldsymbol{v}\right) h_i v_j}} \\ = &-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \sum_{\boldsymbol{h_{-i}}}{P\left(h_{-i} | \boldsymbol{v}\right)} \\ = &-\sum_{\boldsymbol{h_i}}{P\left(h_i | \boldsymbol{v}\right) h_i v_j} \\ = &-\left(P\left(h_i = 0 | \boldsymbol{v}\right) \cdot 0 \cdot v_j + P\left(h_i = 1 | \boldsymbol{v}\right) \cdot 1 \cdot v_j\right) \\ = &-P\left(h_i = 1 | \boldsymbol{v}\right) v_j \end{split} \end{equation} $$ 则对于参数 $b_i$ 可得: $$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) v_i} \\ = &-v_i \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right)} \\ = &-v_i \end{split} \end{equation} $$ 则对于参数 $c_j$ 可得: $$ \begin{equation} \begin{split} &\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h|v}\right) h_j} \\ = &-\sum_{\boldsymbol{h}}{\prod_{k=1}^{n_h}{P\left(h_k | \boldsymbol{v}\right) h_j}} \\ = &-\sum_{\boldsymbol{h}}{P\left(h_j | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j} \\ = &-\sum_{h_j}{\sum_{h_{-j}}{P\left(h_i | \boldsymbol{v}\right) P\left(h_{-j} | \boldsymbol{v}\right) h_j}} \\ = &-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \sum_{h_{-j}}{P\left(h_{-j} | \boldsymbol{v}\right)} \\ = &-\sum_{h_j}{P\left(h_i | \boldsymbol{v}\right) h_j} \\ = &-\left(P\left(h_j = 0 | \boldsymbol{v}\right) \cdot 0 + P\left(h_j = 1 | \boldsymbol{v}\right) \cdot 1\right) \\ = &-P\left(h_j = 1 | \boldsymbol{v}\right) \end{split} \end{equation} $$ 综上所述,可得: $$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial w_{i, j}} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial w_{i, j}}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial w_{i, j}}} \\ &= P\left(h_i = 1 | \boldsymbol{\color{red}{v'}}\right) \color{red}{v'_j} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v}\right) v_j}\\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial b_i} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial b_i}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial b_i}} \\ &= \color{red}{v'_i} - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i} \\ \dfrac{\partial \ln P\left(\color{red}{\boldsymbol{v'}}\right)}{\partial c_j} &= -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h | \color{red}{v'}}\right) \dfrac{\partial E\left(\boldsymbol{\color{red}{v'}, h}\right)}{\partial c_j}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{h | v}\right) \dfrac{\partial E\left(\boldsymbol{v, h}\right)}{\partial c_j}} \\ &= P\left(h_j = 1 | \boldsymbol{\color{red}{v'}}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)} \\ \end{split} \end{equation} $$ 对于多个样本 $S = \left\lbrace{\boldsymbol{v^1}, \boldsymbol{v^2}, ..., \boldsymbol{v^{n_s}}}\right\rbrace$,有: $$ \begin{equation} \begin{split} \dfrac{\partial \ln \mathcal{L}_S}{\partial w_{i, j}} &= \sum_{m=1}^{n_S}{\left[P\left(h_i = 1 | \boldsymbol{v^m}\right) v_j^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_i = 1 | \boldsymbol{v} v_j\right)}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial b_i} &= \sum_{m=1}^{n_S}{\left[v_i^m - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) v_i}\right]} \\ \dfrac{\partial \ln \mathcal{L}_S}{\partial c_j} &= \sum_{m=1}^{n_S}{\left[P\left(h_j = 1 | \boldsymbol{v^m}\right) - \sum_{\boldsymbol{v}}{P\left(\boldsymbol{v}\right) P\left(h_j = 1 | \boldsymbol{v}\right)}\right]} \end{split} \end{equation} $$ 针对如上方法,我们需要计算 $\sum_{\boldsymbol{v}}$ 相关项,如上文所述,其计算复杂度为 $O\left(2^{n_v + n_h}\right)$,因为其条件概率计算比较容易,因此我们可以用 Gibbs 采样的方法进行估计,但由于 Gibbs 采样方法存在 burn-in period,因此需要足够次数的状态转移后才能够收敛到目标分布,因此这就增大了利用这种方法训练 RBM 模型的时间。 针对这个问题,Hinton 于 2002 年提出了对比散度 (Contrastive Divergence, CD) 算法 11,基本思想为将训练样本作为采样的初始值,因为目标就是让 RBM 去拟合这些样本的分布,因此这样则可以通过更少的状态转移就收敛到平稳分布。$k$ 步 CD 算法大致步骤为: 对 $\forall \boldsymbol{v} \in \boldsymbol{S}$,初始化 $\boldsymbol{v}^{\left(0\right)} = \boldsymbol{v}$。 执行 $k$ 步 Gibbs 采样,对于第 $t$ 步,分别利用 $P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(t-1\right)}\right)$ 和 $P\left(\boldsymbol{v} | \boldsymbol{h}^{\left(t-1\right)}\right)$ 采样出 $\boldsymbol{h}^{\left(t-1\right)}$ 和 $\boldsymbol{v}^{\left(t\right)}$。 利用采样得到的 $\boldsymbol{v}^{\left(k\right)}$ 近似估计 $\sum_{\boldsymbol{v}}$ 相关项: $$ \begin{equation} \begin{split} \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial w_{i, j}} &\approx P\left(h_i=1|\boldsymbol{v}^{\left(0\right)}\right) v_j^{\left(0\right)} - P\left(h_i=1|\boldsymbol{v}^{\left(k\right)}\right) v_j^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial b_i} &\approx v_i^{\left(0\right)} - v_i^{\left(k\right)} \\ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial c_j} &\approx P\left(h_j=1|\boldsymbol{v}^{\left(0\right)}\right) - P\left(h_j=1|\boldsymbol{v}^{\left(k\right)}\right) \end{split} \end{equation} $$ 近似估计可以看做是利用 $$ CDK\left(\theta, \boldsymbol{v}\right) = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(k\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(k\right)}, \boldsymbol{h}\right)}{\partial \theta}} $$ 近似 $$ \dfrac{\partial \ln P\left(\boldsymbol{v}\right)}{\partial \theta} = -\sum_{\boldsymbol{h}}{P\left(\boldsymbol{h} | \boldsymbol{v}^{\left(0\right)}\right) \dfrac{\partial E\left(\boldsymbol{v}^{\left(0\right)}, h\right)}{\partial \theta}} + \sum_{\boldsymbol{v, h}}{P\left(\boldsymbol{v, h}\right) \dfrac{\partial E\left(\boldsymbol{v}, \boldsymbol{h}\right)}{\partial \theta}} $$ 的过程。 基于对比散度的 RBM 训练算法可以描述为: 其中,sample_h_given_v 和 sample_v_given_h 分别表示在已知可见层时采样隐含层和在已知隐含层时采样可见层。对于 sample_h_given_v 其算法流程如下: 类似的,对于 sample_v_given_h 其算法流程如下: 至此,我们可以得到 RBM 模型训练的整个流程: 其中,$k$ 为 CDK 算法参数,$\text{max\_iter}$ 为最大迭代次数,$\boldsymbol{S}$ 为训练样本,$n_s = |\boldsymbol{S}|$,$\eta$ 为学习率。 对于模型的评估,最简单的是利用 RBM 模型的似然或对数似然,但由于涉及到归一化因子 $Z$ 的计算,其复杂度太高。更常用的方式是利用重构误差 (reconstruction error),即输入数据和利用 RBM 模型计算得到隐含节点再重构回可见节点之间的误差。 MNIST 示例 我们利用经典的 MNIST 数据作为示例,我们利用基于 tensorflow 的扩展包 tfrbm 12。tfrbm 实现了 Bernoulli-Bernoulli RBM 和 Gaussian-Bernoulli RBM 两种不同的 RBM,两者的比较详见 13 14。 import numpy as np from matplotlib import pyplot as plt, gridspec from tfrbm import BBRBM, GBRBM from tensorflow.examples.tutorials.mnist import input_data # 读入训练数据和测试数据 mnist = input_data.read_data_sets('MNIST', one_hot=True) mnist_train_images = mnist.train.images mnist_test_images = mnist.test.images mnist_test_labels = mnist.test.labels MNIST 数据集中,训练集共包含 55000 个样本,每个样本的维度为 784,我们构建 Bernoulli-Bernoulli RBM,设置隐含节点个数为 64,学习率为 0.01,epoches 为 30,batch size 为 10。 bbrbm = BBRBM(n_visible=784, n_hidden=64, learning_rate=0.01, use_tqdm=True) bbrbm_errs = bbrbm.fit(mnist_train_images, n_epoches=30, batch_size=10) # Epoch: 0: 100%|##########| 5500/5500 [00:11<00:00, 480.39it/s] # Train error: 0.1267 # # ...... # # Epoch: 29: 100%|##########| 5500/5500 [00:11<00:00, 482.15it/s] # Train error: 0.0347 训练误差变化如下 plt.style.use('ggplot') plt.plot(bbrbm_errs) 我们从 MNIST 的测试集中针对每个数字选取 10 个样本,共 100 个样本作为测试数据,利用训练好的 RBM 模型重构这 100 个样本 mnist_test_images_samples = np.zeros([10 * 10, 784]) mnist_test_images_samples_rec = np.zeros([10 * 10, 784]) mnist_test_images_samples_plt = np.zeros([10 * 10 * 2, 784]) digits_current_counts = np.zeros(10, dtype=np.int32) digits_total_counts = np.ones(10, dtype=np.int32) * 10 for idx in range(mnist_test_images.shape[0]): image = mnist_test_images[idx, ] label = mnist_test_labels[idx, ] for digit in range(10): digit_label = np.zeros(10) digit_label[digit] = 1 if (label == digit_label).all() and digits_current_counts[digit] < 10: nrow = digits_current_counts[digit] sample_idx = nrow * 10 + digit mnist_test_images_samples[sample_idx, ] = image mnist_test_images_samples_rec[sample_idx, ] = \ bbrbm.reconstruct(image.reshape([1, -1])) mnist_test_images_samples_plt[sample_idx * 2, ] = \ mnist_test_images_samples[sample_idx, ] mnist_test_images_samples_plt[sample_idx * 2 + 1, ] = \ mnist_test_images_samples_rec[sample_idx, ] digits_current_counts[digit] += 1 if (digits_current_counts == digits_total_counts).all(): break 对比测试输入数据和重构结果,奇数列为输入数据,偶数列为重构数据 def plot_mnist(mnist_images, nrows, ncols, cmap='gray'): fig = plt.figure(figsize=(ncols, nrows)) gs = gridspec.GridSpec(nrows, ncols) gs.update(wspace=0.025, hspace=0.025) for nrow in range(nrows): for ncol in range(ncols): ax = plt.subplot(gs[nrow, ncol]) idx = nrow * ncols + ncol minist_image = mnist_images[idx, ].reshape([28, 28]) ax.imshow(minist_image, cmap=cmap) ax.axis('off') return fig plot_mnist(mnist_test_images_samples_plt, 10, 20) 测试集上的重构误差为 gbrbm.get_err(mnist_test_images_samples) # 0.035245348 Ernest Ising, Beitrag zur Theorie des Ferround Paramagnetismus (1924) Contribution to the Theory of Ferromagnetism (English translation of “Beitrag zur Theorie des Ferromagnetismus”, 1925) Goethe as a Physicist (1950) ↩︎ Onsager, L. “A two-dimensional model with an order–disorder transition (crystal statistics I).” Phys. Rev 65 (1944): 117-49. ↩︎ http://wiki.swarma.net/index.php?title=ISING模型 ↩︎ Hopfield, John J. “Neural networks and physical systems with emergent collective computational abilities.” Spin Glass Theory and Beyond: An Introduction to the Replica Method and Its Applications. 1987. 411-415. ↩︎ Abu-Mostafa, Y. A. S. E. R., and J. St Jacques. “Information capacity of the Hopfield model.” IEEE Transactions on Information Theory 31.4 (1985): 461-464. ↩︎ 韩力群. 人工神经网络理论、设计及应用 ↩︎ https://zh.wikipedia.org/zh-hans/旅行推销员问题 ↩︎ https://zh.wikipedia.org/zh-hans/NP困难 ↩︎ Smolensky, Paul. Information processing in dynamical systems: Foundations of harmony theory. No. CU-CS-321-86. COLORADO UNIV AT BOULDER DEPT OF COMPUTER SCIENCE, 1986. ↩︎ http://blog.csdn.net/itplus/article/details/19168937 ↩︎ Hinton, Geoffrey E. “Training products of experts by minimizing contrastive divergence.” Neural computation 14.8 (2002): 1771-1800. ↩︎ https://github.com/meownoid/tensorfow-rbm ↩︎ Hinton, Geoffrey. “A practical guide to training restricted Boltzmann machines.” Momentum 9.1 (2010): 926. ↩︎ Yamashita, Takayoshi, et al. “To be Bernoulli or to be Gaussian, for a Restricted Boltzmann Machine.” Pattern Recognition (ICPR), 2014 22nd International Conference on. IEEE, 2014. ↩︎

2018/1/17
articleCard.readMore

马尔科夫链蒙特卡洛方法和吉布斯采样 (MCMC and Gibbs Sampling)

蒙特卡罗方法 (Monte Carlo, MC) 蒙特卡罗方法 (Monte Carlo) 也称为统计模拟方法,是于 20 世纪 40 年代由冯·诺伊曼,斯塔尼斯拉夫·乌拉姆和尼古拉斯·梅特罗波利斯在洛斯阿拉莫斯国家实验室为核武器计划工作时 (曼哈顿计划) 发明。因为乌拉姆的叔叔经常在摩纳哥的蒙特卡罗赌场输钱,该方法被定名为蒙特卡罗方法。蒙特卡罗方法是以概率为基础的方法,与之对应的是确定性算法。 蒙特卡罗方法最早可以追述到 18 世纪的布丰投针问题,该方法通过一个平行且等距木纹铺成的地板,随意抛一支长度比木纹之间距离小的针,求针和其中一条木纹相交的概率的方法得出了一个求 $\pi$ 的蒙特卡罗方法。我们通过另一种方式使用蒙特卡罗方法计算圆周率 $\pi$,对于一个边长为 $2r$ 的正方形,其内切圆的半径即为 $r$,因此圆形的面积 $A_c$ 与正方形的面积 $A_s$ 的比值为 $$ \dfrac{A_c}{A_s} = \dfrac{\pi r^2}{\left(2r\right)^2} = \dfrac{\pi}{4} $$ 如果我们在矩形内随机的生成均匀分布的点,则在圆内的点的个数的占比即为 $\dfrac{\pi}{4}$,因此通过模拟即可求出 $\pi$ 的近似值 library(tidyverse) # 圆的中心点和半径 r <- 2 center_x <- r center_y <- r # 距离公式 distance <- function(point_x, point_y, center_x, center_y) { sqrt((point_x - center_x)^2 + (point_y - center_y)^2) } # 点生成器 points_generator <- function(size) { set.seed(112358) points_x <- runif(size, min = 0, max = 2*r) points_y <- runif(size, min = 0, max = 2*r) tibble( x = points_x, y = points_y, in_cycle = ifelse( distance(points_x, points_y, center_x, center_y) > r, 0, 1) ) } # 点的个数 sizes <- c(1000, 10000, 100000, 1000000, 10000000) # 估计的 PI 值 estimated_pi <- sapply(sizes, function(size) { points <- points_generator(size) sum(points$in_cycle) * 4 / size }) print(estimated_pi) # [1] 3.184000 3.146400 3.137880 3.143140 3.141889 模拟 $1000$ 个随机点的结果如图所示 对于简单的分布 $p\left(x\right)$,我们可以相对容易的生成其样本,但对于复杂的分布或高维的分布,样本的生成就比较困难了1,例如: $p\left(x\right) = \dfrac{\tilde{p}\left(x\right)}{\int\tilde{p}\left(x\right) dx}$,其中 $\tilde{p}\left(x\right)$ 是可以计算的,而分母中的积分是无法显式计算的。 $p\left(x, y\right)$ 是一个二维分布函数,函数本身计算很困难,但其条件分布 $p\left(x | y\right)$ 和 $p\left(y | x\right)$ 计算相对简单。对于高维情况 $p\left(\boldsymbol{x}\right)$,这种情况则更加明显。 这时候则需要更加复杂的模拟方法来生成样本了。 马尔科夫链 (Markov Chain, MC) 马尔可夫过程 (Markov Process) 是因俄国数学家安德雷·安德耶维齐·马尔可夫 (Андрей Андреевич Марков) 而得名一个随机过程,在该随机过程中,给定当前状态和过去所有状态的条件下,其下一个状态的条件概率分布仅依赖于当前状态,通常具备离散状态的马尔科夫过程称之为马尔科夫链 (Markov Chain)。因此,马尔科夫链可以理解为一个有限状态机,给定了当前状态为 $s_i$ 时,下一时刻状态为 $s_j$ 的概率,不同状态之间变换的概率称之为转移概率。下图描述了 3 个状态 $S_a, S_b, S_c$ 之间转换状态的马尔科夫链。 对于马尔科夫链,我们设 $X_t$ 表示 $t$ 时刻随机变量 $X$ 的取值,则马尔科夫链可以表示为 $$ P\left(X_{t+1} = s_j | X_0 = s_{i0}, X_1 = s_{i1}, ..., X_t = s_i\right) = P\left(X_{t+1} | X_t = s_i\right) $$ 其中,$s_{i0}, s_{i1}, ..., s_i, s_j$ 为随机变量 $X$ 可能的状态。则定义从一个状态 $s_i$ 到另一个状态 $s_j$ 的转移概率为 $$ P\left(i \to j\right) = P_{ij} = P\left(X_{t+1} | X_t = s_i\right) $$ 设 $\pi_{k}^{\left(t\right)}$ 表示随机变量 $X$ 在 $t$ 时刻取值为 $s_k$ 的概率,则 $X$ 在 $t+1$ 时刻取值为 $s_i$ 的概率为 $$ \begin{equation} \begin{split} \pi_{i}^{\left(t+1\right)} &= P\left(X_{t+1} = s_i\right) \\ &= \sum_{k}{P\left(X_{t+1} = s_i | X_t = s_k\right) \cdot P\left(X_t = s_k\right)} \\ &= \sum_{k}{P_{ki} \cdot \pi_{k}^{\left(t\right)}} \end{split} \end{equation} $$ 我们通过一个例子来理解一下马尔科夫链,我们使用 LDA 数学八卦1一文中的例子,对于人口,我们将其经济状况分为 3 类:下层,中层和上层,其父代到子代收入阶层的转移情况如表所示 父代阶层\子代阶层 下层 中层 下层 下层 0.65 0.28 0.07 中层 0.15 0.67 0.18 上层 0.12 0.36 0.52 我们利用矩阵的形式表示转移概率 $$ P = \left\lgroup \begin{array}{cccc} P_{11} & P_{12} & \cdots & P_{1n} \\ P_{21} & P_{22} & \cdots & P_{2n} \\ \vdots & \vdots & & \vdots \\ P_{n1} & P_{n2} & \cdots & P_{nn} \end{array} \right\rgroup $$ 则 $$ \pi^{\left(t+1\right)} = \pi^{\left(t\right)} P $$ 假设初始概率分布为 $\pi_0 = \left(0.21, 0.68, 0.11\right)$,则计算前 $n$ 代人的阶层分布情况如下 # 转移矩阵 p <- matrix(c(0.65, 0.28, 0.07, 0.15, 0.67, 0.18, 0.12, 0.36, 0.52), 3, 3, byrow = T) # 初始概率 pi <- matrix(c(0.21, 0.68, 0.11), 1, 3, byrow = T) # 迭代变化 for (i in 1:10) { pi_current <- pi[i, ] pi_next <- pi_current %*% p pi <- rbind(pi, pi_next) } colnames(pi) <- c('下层', '中层', '上层') rownames(pi) <- 0:10 print(pi) # 下层 中层 上层 # 0 0.2100000 0.6800000 0.1100000 # 1 0.2517000 0.5540000 0.1943000 # 2 0.2700210 0.5116040 0.2183750 # 3 0.2784592 0.4969956 0.2245452 # 4 0.2824933 0.4917919 0.2257148 # 5 0.2844752 0.4898560 0.2256688 # 6 0.2854675 0.4890974 0.2254351 # 7 0.2859707 0.4887828 0.2252465 # 8 0.2862280 0.4886450 0.2251270 # 9 0.2863602 0.4885817 0.2250581 # 10 0.2864283 0.4885515 0.2250201 可以看出,从第 7 代人开始,分布就基本稳定下来了,如果将初值概率换成 $\pi_0 = \left(0.75, 0.15, 0.1\right)$,结果会是如何呢? pi <- matrix(c(0.75, 0.15, 0.1), 1, 3, byrow = T) for (i in 1:10) { pi_current <- pi[i, ] pi_next <- pi_current %*% p pi <- rbind(pi, pi_next) } colnames(pi) <- c('下层', '中层', '上层') rownames(pi) <- 0:10 print(pi) # 下层 中层 上层 # 0 0.7500000 0.1500000 0.1000000 # 1 0.5220000 0.3465000 0.1315000 # 2 0.4070550 0.4256550 0.1672900 # 3 0.3485088 0.4593887 0.1921025 # 4 0.3184913 0.4745298 0.2069789 # 5 0.3030363 0.4816249 0.2153388 # 6 0.2950580 0.4850608 0.2198812 # 7 0.2909326 0.4867642 0.2223032 # 8 0.2887972 0.4876223 0.2235805 # 9 0.2876912 0.4880591 0.2242497 # 10 0.2871181 0.4882830 0.2245989 可以看出从第 9 代人开始,分布又变得稳定了,这也就是说分布收敛情况是不随初始概率分布 $\pi_0$ 的变化而改变的。则对于具有如下特征的马尔科夫链 非周期性,可以简单理解为如果一个状态有自环,或者与一个非周期的状态互通,则是非周期的。 不可约性,即任意两个状态都是互通的。 则这样的马尔科夫链,无论 $\pi_0$ 取值如何,最终随机变量的分布都会收敛于 $\pi^*$,即 $$ \pi^* = \lim_{t \to \infty}{\pi^{\left(0\right)} \boldsymbol{P}^t} $$ $\pi^*$ 称之为这个马尔科夫链的平稳分布。 马尔科夫链蒙特卡洛方法 (MCMC) 构造一个转移矩阵为 $P$ 的马尔科夫链,如果其能收敛到平稳分布 $p\left(x\right)$,则可以从任意一个状态 $x_0$ 出发,得到一个状态转移序列 $x_0, x_1, ..., x_n, x_{n+1}, ...$,如果马尔科夫链在第 $n$ 部收敛,我们就可以得到服从分布 $p\left(x\right)$ 的样本 $x_n, x_{n+1}, ...$。因此,利用马尔科夫链的平稳性生成数据的样本的关键就在于如何构造一个状态转移矩阵 $P$,使得其平稳分布为 $p\left(x\right)$。 如果对于任意的 $i, j$,马尔科夫链的转移矩阵 $P$ 和分布 $\pi\left(x\right)$ 满足 $$ \pi\left(i\right) P_{ij} = \pi\left(j\right) P_{ji} $$ 则称 $\pi\left(x\right)$ 为马尔科夫链的平稳分布,这称为细致平稳条件。对于一个马尔科夫链,通常情况下 $$ p\left(i\right) q\left(i, j\right) \neq p\left(j\right) q\left(j, i\right) $$ 其中 $p\left(i, j\right)$ 表示状态从 $i$ 转移到 $j$ 的概率。因此,为了构造满足细致平稳条件,我们引入一个接受概率 $\alpha\left(i, j\right)$,使得 $$ p\left(i\right) q\left(i, j\right) \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right) \alpha\left(j, i\right) $$ 最简单的,我们取 $$ \alpha\left(i, j\right) = p\left(j\right) q\left(j, i\right), \alpha\left(j, i\right) = p\left(i\right) q\left(i, j\right) $$ 即可保证细致平稳性。通过引入接受概率,我们将原始的马尔科夫链改造为具有新的转移矩阵的马尔科夫链。在原始马尔科夫链上以概率 $q\left(i, j\right)$ 从状态 $i$ 转移到状态 $j$ 时,我们以概率 $\alpha\left(i, j\right)$ 接受这个转移,因此在新的马尔科夫链上的转移概率为 $q\left(i, j\right) \alpha\left(i, j\right)$。在新的马尔科夫链转移的过程中,如果接受概率 $\alpha\left(i, j\right)$ 过小,则可能导致存在大量的拒绝转移,马尔科夫链则很难收敛到平稳分布 $p\left(x\right)$,因此我们对 $\alpha\left(i, j\right), \alpha\left(j, i\right)$ 进行同比例放大,将其中较大的数放大至 $1$,则可以增加接受跳转的概率,从而更快的收敛到平稳分布。因此,我们可以取 $$ \alpha\left(i, j\right) = \min \left\lbrace\dfrac{p\left(j\right) q\left(j, i\right)}{p\left(i\right) q\left(i, j\right)}, 1\right\rbrace $$ 这样我们就得到了 Metropolis-Hastings 算法 吉布斯采样 (Gibbs Sampling) 对于 Metropolis-Hastings 算法,由于存在接受跳转概率 $\alpha < 1$,因此为了提高算法效率,我们尝试构建一个转移矩阵,使得 $\alpha = 1$。以二维情形为例,对于概率分布 $p\left(x, y\right)$,考虑两个点 $A\left(x_1, y_1\right)$ 和 $B\left(x_1, y_2\right)$ $$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &= p\left(x_1\right) p\left(y_1 | x_1\right) p\left(y_2 | x_1\right) \\ p\left(x_1, y_2\right) p\left(y_1 | x_1\right) &= p\left(x_1\right) p\left(y_2 | x_1\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$ 可得 $$ \begin{equation} \begin{split} p\left(x_1, y_1\right) p\left(y_2 | x_1\right) &= p\left(x_1, y_2\right) p\left(y_1 | x_1\right) \\ p\left(A\right) p\left(y_2 | x_1\right) &= p\left(B\right) p\left(y_1 | x_1\right) \end{split} \end{equation} $$ 可以得出在 $x = x_1$ 上任意两点之间进行转移均满足细致平稳条件,同理可得在 $y = y_1$上也满足。因此,对于二维情况,我们构建满足如下调价的概率转移矩阵 $Q$ $$ \begin{equation} \begin{split} &Q\left(A \to B\right) = p\left(y_B | x_1\right), \text{for} \ x_A = x_B = x_1 \\ &Q\left(A \to C\right) = p\left(x_C | y_1\right), \text{for} \ y_A = y_C = y_1 \\ &Q\left(A \to D\right) = 0, \text{others} \end{split} \end{equation} $$ 则对于平面上任意两点 $X, Y$ 满足细致平稳条件 $$ p\left(X\right) Q\left(X \to Y\right) = p\left(Y\right) Q\left(Y \to X\right) $$ 对于如上过程,我们不难推广到多维情况,将 $x_1$ 变为多维情形 $\boldsymbol{x_1}$,容易验证细致平稳条件依旧成立。 $$ p\left(\boldsymbol{x_1}, y_1\right) p\left(y_2 | \boldsymbol{x_1}\right) = p\left(\boldsymbol{x_1}, y_2\right) p\left(y_1 | \boldsymbol{x_1}\right) $$ 对于 $n$ 维的情况,通过不断的转移得到样本 $\left(x_1^{\left(1\right)}, x_2^{\left(1\right)}, ..., x_n^{\left(1\right)}\right)$, $\left(x_1^{\left(2\right)}, x_2^{\left(2\right)}, ..., x_n^{\left(2\right)}\right)$, …,当马尔科夫链收敛后,后续得到的样本即为 $p\left(x_1, x_2, ..., x_n\right)$ 的样本,收敛之前的这一阶段我们称之为 burn-in period。在进行转移的时候,坐标轴轮换的采样方法并不是必须的,可以在坐标轴轮换中引入随机性。至此,我们就得到了吉布斯采样算法 我们以二元高斯分布为例,演示如何用 Gibbs Sampling 方法进行采样,二元高斯分布定义为 $$ \left(X, Y\right) \sim \mathcal{N}\left(\boldsymbol{\mu}, \boldsymbol{\Sigma}\right) $$ 其中 $$ \boldsymbol{\mu} = \left\lgroup \begin{array}{c} \mu_X \\ \mu_Y \end{array} \right\rgroup, \boldsymbol{\Sigma} = \left\lgroup \begin{array}{cc} \sigma_X^2 & \rho \sigma_X \sigma_Y \\ \rho \sigma_X \sigma_Y & \sigma_Y^2 \end{array} \right\rgroup $$ 因此可得 $$ \begin{equation} \begin{split} \mu_{x|y} &= \mu_x + \sigma_x \rho_x\left(\dfrac{y - \mu_y}{\sigma_y}\right), \sigma_{x|y}^2 = \sigma_x^2 \left(1 - \rho^2\right) \\ \mu_{y|x} &= \mu_y + \sigma_y \rho_y\left(\dfrac{y - \mu_x}{\sigma_x}\right), \sigma_{y|x}^2 = \sigma_y^2 \left(1 - \rho^2\right) \end{split} \end{equation} $$ 则 $$ \begin{equation} \begin{split} X|Y &= \mu_{x|y} + \sigma_{x|y} \mathcal{N}\left(0, 1\right) \\ Y|X &= \mu_{y|x} + \sigma_{y|x} \mathcal{N}\left(0, 1\right) \end{split} \end{equation} $$ 对于 $\mu_x = 0, \mu_y = 0, \sigma_x = 10, \sigma_y = 1, \rho = 0.8$,采样过程如下 mu_x <- 0 mu_y <- 0 sigma_x <- 10 sigma_y <- 1 rho <- 0.8 iter <- 1000 samples <- matrix(c(mu_x, mu_y), 1, 2, byrow = T) set.seed(112358) for (i in 1:iter) { sample_x <- mu_x + sigma_x * rho * (samples[i, 2] - mu_y) / sigma_y + sigma_x * sqrt(1 - rho^2) * rnorm(1) sample_y <- mu_y + sigma_y * rho * (sample_x - mu_x) / sigma_x + sigma_y * sqrt(1 - rho^2) * rnorm(1) samples <- rbind(samples, c(sample_x, sample_y)) } 可视化结果如下 LDA 数学八卦,靳志辉,2013 ↩︎ ↩︎

2017/12/17
articleCard.readMore

特征值分解,奇异值分解和主成分分析 (EVD, SVD and PCA)

准备知识 向量与基 $\renewcommand{\diag}{\operatorname{diag}}\renewcommand{\cov}{\operatorname{cov}}$首先,定义 $\boldsymbol{\alpha}$ 为列向量,则维度相同的两个向量 $\boldsymbol{\alpha}, \boldsymbol{\beta}$ 的内积可以表示为: $$\boldsymbol{\alpha} \cdot \boldsymbol{\beta} = \boldsymbol{\alpha}^T \boldsymbol{\beta} = \sum_{i=1}^{n}{\alpha_i b_i}$$ 后续为了便于理解,我们以二维向量为例,则 $\boldsymbol{\alpha} = \left(x_1, y_1\right)^T, \boldsymbol{\beta} = \left(x_2, y_2\right)^T$,在直角座标系中可以两个向量表示如下: 我们从 $A$ 点向向量 $\boldsymbol{\beta}$ 的方向做一条垂线,交于点 $C$,则称 $OC$ 为 $OA$ 在 $OB$ 方向上的投影。设向量 $\boldsymbol{\alpha}$ 和向量 $\boldsymbol{\beta}$ 的夹角为 $\theta$,则: $$\cos \left(\theta\right) = \dfrac{\boldsymbol{\alpha} \cdot \boldsymbol{\beta}}{\lvert\boldsymbol{\alpha}\rvert \lvert\boldsymbol{\beta}\rvert}$$ 其中,$\lvert\boldsymbol{\alpha}\rvert = \sqrt{x_1^2 + y_1^2}$,则 $OC$ 的长度为 $\lvert\boldsymbol{\alpha}\rvert \cos\left(\theta\right)$。 在 $n$ 维的线性空间 $V$ 中,$n$ 个线性无关的向量 $\boldsymbol{\epsilon_1, \epsilon_2, ..., \epsilon_n}$ 称为 $V$ 的一组基。则对于 $V$ 中的任一向量 $\boldsymbol{\alpha}$ 可以由这组基线性表示出来: $$\boldsymbol{\alpha} = x_1 \boldsymbol{\epsilon_1} + x_2 \boldsymbol{\epsilon_2} + ... + x_n \boldsymbol{\epsilon_n}$$ 则对于向量 $\boldsymbol{\alpha} = \left(3, 2\right)^T$,可以表示为: $$\boldsymbol{\alpha} = 3 \left(1, 0\right)^T + 2 \left(0, 1\right)^T$$ 其中 $\left(1, 0\right)^T$ 和 $\left(0, 1\right)^T$ 为二维空间中的一组基。 因此,当我们确定好一组基之后,我们仅需利用向量在基上的投影值即可表示对应的向量。一般情况下,我们会选择由坐标轴方向上的单位向量构成的基作为默认的基来表示向量,但我们仍可选择其他的基。例如,我们选择 $\left(-\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$ 和 $\left(\dfrac{1}{\sqrt{2}}, \dfrac{1}{\sqrt{2}}\right)$ 作为一组基,则向量在这组基上的坐标为 $\left(-\dfrac{1}{\sqrt{2}}, \dfrac{5}{\sqrt{2}}\right)$,示例如下: 线性变换 以二维空间为例,定义一个如下矩阵 $$ A = \left\lgroup \begin{array}{cc} a_{11} & a_{12} \\ a_{21} & a_{22} \end{array} \right\rgroup $$ 则对于二维空间中一个向量 $\boldsymbol{\alpha} = \left(x, y\right)^T$ ,通过同上述矩阵进行乘法运算,可得 $$ \boldsymbol{\alpha'} = A \boldsymbol{\alpha} = \left\lgroup \begin{array}{cc} a_{11} & a_{12} \\ a_{21} & a_{22} \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x' \\ y' \end{array} \right\rgroup $$ (1) 通过变换将任意一个点 $x$ 变成它关于 $x$ 轴对称的点 $x'$ $$ x' = \left\lgroup \begin{array}{cc} 1 & 0 \\ 0 & -1 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x \\ -y \end{array} \right\rgroup $$ (2) 通过变换将任意一个点 $x$ 变成它关于 $y = x$ 对称的点 $x'$ $$ x' = \left\lgroup \begin{array}{cc} 0 & 1 \\ 1 & 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} y \\ x \end{array} \right\rgroup $$ (3) 变换将任意一个点 $x$ 变成在它与原点连线上,与原点距离伸缩为 $|\lambda|$ 倍的点 $x'$ $$ x' = \left\lgroup \begin{array}{cc} \lambda & 0 \\ 0 & \lambda \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} \lambda x \\ \lambda y \end{array} \right\rgroup $$ (4) 通过变换将任意一个点 $x$ 绕原点旋转了角度 $\theta$ 的点 $x'$ $$ \begin{equation} \begin{split} x'& = \left\lgroup \begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup \\ & = \left\lgroup \begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array} \right\rgroup \left\lgroup \begin{array}{c} r \cos \phi \\ r \sin \phi \end{array} \right\rgroup \\ & = \left\lgroup \begin{array}{c} r \cos \left(\phi + \theta\right) \\ r \sin \left(\phi + \theta\right) \end{array} \right\rgroup \end{split} \end{equation} $$ (5) 变换将任意一个点 $x$ 变成它在 $x$ 轴上 的投影点 $x'$ $$ x' = \left\lgroup \begin{array}{cc} 1 & 0 \\ 0 & 0 \end{array} \right\rgroup \left\lgroup \begin{array}{c} x \\ y \end{array} \right\rgroup = \left\lgroup \begin{array}{c} x \\ 0 \end{array} \right\rgroup $$ 特征值分解 设 $A$ 是线性空间 $V$ 上的一个线性变换,对于一个非零向量 $\boldsymbol{\alpha} = \left(x_1, x_2, ..., x_n\right)^T$ 使得 $$A \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$$ 则 $\lambda$ 称为 $A$ 的一个特征值,$\boldsymbol{\alpha}$ 称为 $A$ 的一个特征向量。通过 $$ \begin{equation} \begin{split} A \boldsymbol{\alpha} &= \lambda \boldsymbol{\alpha} \\ A \boldsymbol{\alpha} - \lambda \boldsymbol{\alpha} &= 0 \\ \left(A - \lambda E\right) \boldsymbol{\alpha} &= 0 \\ A - \lambda E &= 0 \end{split} \end{equation} $$ 其中 $E = \diag \left(1, 1, ..., 1\right)$ 为单位对角阵,即可求解其特征值,进而求解特征向量。若 $A$ 是一个可逆矩阵,则上式可以改写为: $$ A = Q \sum Q^{-1} $$ 这样,一个方阵 $A$ 就被一组特征值和特征向量表示了。例如,对于如下矩阵进行特征值分解 $$ A = \left\lgroup \begin{array}{cccc} 3 & -2 & -0.9 & 0 \\ -2 & 4 & 1 & 0 \\ 0 & 0 & -1 & 0 \\ -0.5 & -0.5 & 0.1 & 1 \end{array} \right\rgroup $$ A <- matrix(c(3, -2, -0.9, 0, -2, 4, 1, 0, 0, 0, -1, 0, -0.5, -0.5, 0.1, 1), 4, 4, byrow = T) A_eig <- eigen(A) print(A_eig) # eigen() decomposition # $values # [1] 5.561553 1.438447 1.000000 -1.000000 # # $vectors # [,1] [,2] [,3] [,4] # [1,] -0.61530186 0.4176225 0 0.15282144 # [2,] 0.78806410 0.3260698 0 -0.13448286 # [3,] 0.00000000 0.0000000 0 0.97805719 # [4,] -0.01893678 -0.8480979 1 -0.04431822 则利用特征值和特征向量,可以还原原矩阵 A_re <- A_eig$vectors %*% diag(A_eig$values) %*% solve(A_eig$vectors) print(A_re) # [,1] [,2] [,3] [,4] # [1,] 3.0 -2.0 -0.9 0 # [2,] -2.0 4.0 1.0 0 # [3,] 0.0 0.0 -1.0 0 # [4,] -0.5 -0.5 0.1 1 奇异值分解 特征值分解针对的是方阵,对于一个 $m*n$ 的矩阵是无法进行特征值分解的,这时我们就需要使用奇异值分解来解决这个问题。对于 $m*n$ 的矩阵 $A$,可得 $A A^T$ 是一个 $m*m$ 的方阵,则针对 $A A^T$,通过 $\left(A A^T\right) \boldsymbol{\alpha} = \lambda \boldsymbol{\alpha}$,即可求解这个方阵的特征值和特征向量。针对矩阵 $A$,奇异值分解是将原矩阵分解为三个部分 $$ A_{m*n} = U_{m*r} \sum\nolimits_{r*r} V_{r*n}^T $$ 其中 $U$ 称之为左奇异向量,即为 $A A^T$ 单位化后的特征向量;$V$ 称之为右奇异向量,即为 $A^T A$ 单位化后的特征向量;$\sum$矩阵对角线上的值称之为奇异值,即为 $A A^T$ 或 $A^T A$ 特征值的平方根。 我们利用经典的 lena 图片展示一下 SVD 的作用,lena图片为一张 $512*512$ 像素的彩色图片 我们对原始图片进行灰度处理后,进行特征值分解,下图中从左到右,从上到下分别是原始的灰度图像,利用 20 个左奇异向量和 20 个右奇异向量重构图像,利用 50 个左奇异向量和 100 个右奇异向量重构图像,利用 200 个左奇异向量和 200 个右奇异向量重构图像。 从图中可以看出,我们仅用了 200 个左奇异向量和 200 个右奇异向量重构图像与原始灰度图像已经基本看不出任何区别。因此,我们利用 SVD 可以通过仅保留较大的奇异值实现数据的压缩。 主成分分析 主成分分析1可以通俗的理解为一种降维方法。其目标可以理解为将一个 $m$ 维的数据转换称一个 $k$ 维的数据,其中 $k < m$。对于具有 $n$ 个样本的数据集,设 $\boldsymbol{x_i}$ 表示 $m$ 维的列向量,则 $$ X_{m*n} = \left(\boldsymbol{x_1}, \boldsymbol{x_2}, ..., \boldsymbol{x_n}\right) $$ 对每一个维度进行零均值化,即减去这一维度的均值 $$ X'_{m*n} = X - \boldsymbol{u}\boldsymbol{h} $$ 其中,$\boldsymbol{u}$ 是一个 $m$ 维的行向量,$\boldsymbol{u}[m] = \dfrac{1}{n} \sum_{i=1}^{n} X[m, i]$;$h$ 是一个值全为 $1$ 的 $n$ 维行向量。 对于两个随机变量,我们可以利用协方差简单表示这两个变量之间的相关性 $$ \cov \left(x, y\right) = E \left(\left(x - \mu_x\right) \left(x - \mu_x\right)\right) $$ 对于已经零均值化后的矩阵 $X'$,计算得出如下矩阵 $$ C = \dfrac{1}{n} X' X'^T = \left\lgroup \begin{array}{cccc} \dfrac{1}{n} \sum_{i=1}^{n} x_{1i}^2 & \dfrac{1}{n} \sum_{i=1}^{n} x_{1i} x_{2i} & \cdots & \dfrac{1}{n} \sum_{}^{} x_{1i} x_{ni} \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{2i} x_{1i} & \dfrac{1}{n} \sum_{i=1}^{n} x_{2i}^2 & \cdots & \dfrac{1}{n} \sum_{}^{} x_{2i} x_{ni} \\ \vdots & \vdots & & \vdots \\ \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{1i} & \dfrac{1}{n} \sum_{i=1}^{n} x_{mi} x_{2i} & \cdots & \dfrac{1}{n} \sum_{}^{} x_{mi}^2 \\ \end{array} \right\rgroup $$ 因为矩阵 $X'$ 已经经过了零均值化处理,因此矩阵 $C$ 中对角线上的元素为维度 $m$ 的方差,其他元素则为两个维度之间的协方差。 从 PCA 的目标来看,我们则可以通过求解矩阵 $C$ 的特征值和特征向量,将其特征值按照从大到小的顺序按行重排其对应的特征向量,则取前 $k$ 个,则实现了数据从 $m$ 维降至 $k$ 维。 例如,我们将二维数据 $$ \left\lgroup \begin{array}{ccccc} -1 & -1 & 0 & 0 & 2 \\ -2 & 0 & 0 & 1 & 1 \end{array} \right\rgroup $$ 降至一维 x <- matrix(c(-1, -1, 0, 0, 2, -2, 0, 0, 1, 1), 5, 2, byrow = F) x_pca <- prcomp(x) print(pca) # Standard deviations (1, .., p=2): # [1] 1.5811388 0.7071068 # # Rotation (n x k) = (2 x 2): # PC1 PC2 # [1,] 0.7071068 0.7071068 # [2,] 0.7071068 -0.7071068 summary(pca) # Importance of components: # PC1 PC2 # Standard deviation 1.5811 0.7071 # Proportion of Variance 0.8333 0.1667 # Cumulative Proportion 0.8333 1.0000 x_ <- predict(x_pca, x) print(x_) # PC1 PC2 # [1,] -2.1213203 0.7071068 # [2,] -0.7071068 -0.7071068 # [3,] 0.0000000 0.0000000 # [4,] 0.7071068 -0.7071068 # [5,] 2.1213203 0.7071068 降维的投影结果如图所示 Wold, Svante, Kim Esbensen, and Paul Geladi. “Principal component analysis.” Chemometrics and intelligent laboratory systems 2.1-3 (1987): 37-52. ↩︎

2017/12/11
articleCard.readMore

墨尔本之行 (Trip to Melbourne)

从下了飞机到酒店,一路上体验到了我国互联网对世界各地的影响,机场巴士可以用微信和支付宝,下了巴士发现这里也有共享单车,人家还配了头盔。 晚上雅拉河还是很漂亮的,天气略凉,空气不错,淡淡的云,大冷天的人们也很愿意在外面吃饭。 谈到澳大利亚,最熟知的两种动物就是精壮的袋鼠和呆萌的考拉,晚上吃饭看了菜单发现居然有袋鼠肉,味道还是很不错的,没什么奇怪的味道。后来得知,在澳洲袋鼠的数量算得上略微的泛滥,所以法律是允许吃袋鼠的,如果你在路上开车不幸撞上了一只袋鼠,那么请快速的结束他的生命就好,免得痛苦,但如果你撞了一只考拉,那好吧,估计你出不去澳大利亚了…… 当然,本次旅程最重要的还是 IJCAI 大会,让我这个半路出家搞 AI 的人感触最多的是:未知的还有很多,要学的也还有很多。这次大会期间也再次发表了呼吁禁止自主武器的公开信,我认为有时候我们更多的关注了 AI 技术的层面,而忽略了很多其他的事情,例如伦理和道德。例如现在医学图像识别技术在一定范围内已经超越了人类,那么是否我们就可以让机器直接做决定呢,如果出了问题,将如何处理,所以这个边界到底在哪里也是值的我们仔细思考的。 这边的冬天是多雨的,来的时候推算这边应该是早春,没带太厚的衣服,和当地人聊到天气,我说现在是 Early spring 怎么怎么的,结果对方说 No, No, No, still winter。尽管天气还是很凉,不过白天甚至晚上外面还是有很多路演的艺人,下面这个大哥唱的特别好,没一会儿就围了一圈人,他说他在为了下一次旅行筹钱,想想我何时才能有这样的勇气和行动力。 2017 年,墨尔本连续 7 年蝉联全球最适宜居住的城市冠军,很大一部分源自于其便利的交通。在墨尔本都是路上的有轨电车,还有一趟环城线路是免费的,晚上搭着它环城了一圈,最大的体会是,这儿真不大…… 路过市政厅,发现门口有人滑滑板,我也就敢在我们村委会门口玩一玩。环城电车有一个折返点,不像北京 10 号线那样无限循环,有点像带折返点的单程线,就在到头的时候奇迹发生了。我以为司机会下车到另一头再发动电车,不过他不是一个人离开的,还带走了方向盘,对是方向盘,劳动人民是智慧的,这车根本就不需要钥匙,拿走方向盘就行啦,不过话说有轨电车的方向盘又是干嘛用的。 作为不折不扣的伪军迷,自然也要了解一下这个城市的军事历史,说到这就不得不提大洋路 1。大洋路是位于澳洲东海岸维多利亚州南部的一条行车公路,全长约 276 公里,建于悬崖峭壁中间,起点自托尔坎 (Torquay),终点于亚伦斯福特 (Allansford)。大洋路始建于 1920 年,在 1932 年竣工,澳洲政府借此纪念第一次世界大战中牺牲的人。 一路上有十二门徒石 2 和阿德湖峡 3 等自然景点。 战争永远是残酷的,伤痛是无法消除的,我们能做的就是尊重这些为了国家奉献过的人们,缅怀逝去的,照顾残留的,这条有一战老兵参与修建的大洋路就是我们最好的纪念。去参观战争纪念馆的时候,恰巧碰上了一场纪念会,到的时候差不多结束了,不是太清楚具体是在纪念哪场战役,只远远的看到年轻的士兵持枪肃立,年老的士兵时不时的留下泪水。一瞬间想到了两段话,一段是说人的死亡分为三次,断气时,下葬时和被遗忘时,庆幸的是我们并没有选择忘记,至少现在还没有;另一段是麦克阿瑟的经典演讲,老兵不死,只是慢慢凋零。 最近恰巧看了二十二 4 和三十二两部纪录片,战争受难最多的终究还是百姓,不过越来越多的历史开始慢慢淡出人们的视野,但也有着那么一群人再帮着我们更好的铭记。其实,我们铭记不是说为了让我们去记恨一些人,去记恨一个民族,我们所需要让人们铭记的是战争本身的残酷,愿天下之安宁,以活民命。 https://zh.wikipedia.org/zh-hans/大洋路 ↩︎ https://zh.wikipedia.org/zh-hans/十二使徒岩 ↩︎ https://zh.wikipedia.org/zh-hans/阿德湖峡 ↩︎ https://zh.wikipedia.org/zh-hans/二十二_(2017年电影) ↩︎

2017/8/26
articleCard.readMore