10的35次方有多大?

这两天看到一个新闻,说俄罗斯政府对谷歌的罚款已达到约200000000000000000000000000000000000()美元。

image-20241101213324614

我的第一反应是怎么可能会达到这个量级,往下看发现有一条规则是:

如果9个月内未缴纳罚款,罚款金额将每天翻一番,没有上限。

“每天翻一番”可不就是以2为底的幂指数增长么,摊上幂指数增长最后数值不爆炸才怪.....

发现评论的很多人对没有概念,我自己本着看热闹不嫌事大的心态算了一下。

这些钱如果用100美元付的话,那么就需要张100美元。是不是还没有概念?接下来从重量和尺寸上看看。

重量(严谨起见,下边称为质量)

一张100美元质量是1.05克,也就是,那么这堆钱的总质量是

看起来还挺重的,难道比地球还重不成?还真是,地球的质量是,地球质量连这堆美元质量的小数点都算不上。

格局打开一点,太阳质量为

哎,看起来是不是接近了?不就差个0.1的零头嘛。

可是别忘了,后边还有一个,光这个零头表示的质量又抵得上16764个地球的质量之和。

也就是说: 尺寸

所有美元尺寸均为,即厚度为

如果把这堆美元一直摞起来,那么高度将达到,也就是光年,即23万亿光年。

已知银河系直径约为190万光年,那么这摞钱的高度等于1200多万个银河系排成一排的长度。

已知宇宙的直径约为930亿光年,这堆美元的高度等于247个已知宇宙排成一排的长度。

总结一下就是: 是不是相当惊人?幂指数增长带来的堪称恐怖的数字量级着实令人叹为观止。

不过话说回来,很显然这是一个不现实的罚款,克宫发言人也回应称,这一数字“充满象征意义”。

Attention-is-all-you-need

link: 1706.03762 (arxiv.org)

3. Methods

Transformer总体上沿用编码器-解码器架构,组件由自注意力和全连接层堆叠而成,核心架构如下图:

Figure1: The Transformer-model architecture.

3.1 编码器-解码器堆叠架构

3.1.1 编码器

编码器由6个相同的网络块堆叠组成。每个网络块有两个子层,第一个子层是一个多头自注意力机制(multi-head self-attention mechanism),第二个子层是一个全连接层。两个子层都采用残差连接和LayerNorm。值得注意的是,所有的子层输入输出维度都是​。

为什么使用LayerNorm?通常使用BN,BN会将所有的样本一起归一化,如果样本长度变化大,那么计算出的均值和方差抖动较严重。

3.1.2 解码器

解码器同样由6个相同的网络块堆叠组成。每个网络块有三个子层,除了两个与编码器子层相同的层外,多了一个处理编码器输出的多头注意力层,即Masked Multi-Head Attention。这一层使得位置i的输出只能依赖于位置i之前的结果,这对于自然语言处理来说是个很自然的考虑。

3.2 注意力机制

Transformer中的注意力机制使用三个特征变量描述:查询(query, Q)、键(key, K)和值(value, V)。计算Q和K之间的相似度作为权重,将权重作用于V得到的加权和即为输出结果。

3.2.1 基本注意力单元

文章中将这种基本注意力单元称为“缩放的点乘注意力”(Scaled Dot-Product Attention),点明了这种注意力的两个重要计算:点乘、缩放。

假设输入的查询和键维度为,值维度为。实际计算中,为了并行化提高速度,我们会把个查询堆叠起来,即,把个键堆叠起来,即。计算公式为: 上式中缩放体现在除以,因为当比较大的时候,点乘出的结果值也会很大,经过softmax之后很容易出现非常小的梯度,通过缩放可以将结果控制在一个不太大且基本稳定的数值范围内。

3.2.2 多头注意力

Figure2: Multi-Head Attention

实际使用时并不是直接堆叠点乘注意力单元,而是将使用线性层映射到个不同的表达空间,在每个表达空间独立做注意力计算,最后将各个表达空间的计算结果concat起来。听起来有点复杂,但是将类比于CNN中的卷积通道就很好理解了。

多头注意力计算公式如下: 其中,线性层参数分别为以及

这这篇文章中,

3.2.3 Transformer中的注意力

Transformer中多头注意力有以下三种用法:

  1. 解码器中的Multi-head Attention:Q来自于先前的解码器层;K和V来自于编码器的输出,这样Q与K和V来自于不同序列的注意力被称为cross -attention。每个位置都可以看到上一层的全部位置。
  2. 编码器中的Multi-head Attention:Q、K和V相同,是上一个编码器层的输出,这样的层被称为自注意力层。每个位置都可以看到上一层的全部位置。
  3. 解码器中的Masked Multi-Head Attention:同样是一个自注意力层,但是其中每个位置都不能看到上一层其后边位置的内容。

3.3 前馈网络

每个编码器、解码器块中除了有注意力子层还包含一个应用于每一个位置(position)的全连接前馈网络。这个网络由两个线性层和一个ReLU激活函数组成: 前馈网络在同一个编码器、解码器块内权重共享,不同块之间不共享。其输入输出维度均为,隐含层维度为

除了使用全连接网络,也可以使用核为1的卷积网络。

3.4 位置编码

Transformer中不使用RNN或CNN,为了维护输入序列顺序信息,对输入Embedding做位置编码,再将位置编码和Embedding相加起来。

4. 为什么要用自注意力?

这一节对自注意力层和RNN层、CNN层做一个量化的对比。存在一个任务,要把序列映射到一个等长序列,其中。文中分别统计了这些架构的每层计算复杂度、最小顺序操作数(越小表示计算并行度越好)和上下文路径长度(越小表示感受野越大),结果如下表所示,其中表示序列长度,表示每个序列元素的维度,表示卷积核尺寸,表示在受限自注意力中的邻域尺寸。

Layer Type Complexity per Layer Sequential Operations Maximum Path Length
Self-Attention 𝑂(1) 𝑂(1)
Recurrent 𝑂(𝑛) 𝑂(𝑛)
Convolutional 𝑂(1)
Self-Attention (restricted) 𝑂(𝑟⋅𝑛⋅𝑑) 𝑂(1) 𝑂(𝑛/𝑟)

自注意力可以直接看到全图的信息,即上下文路径长度复杂度为𝑂(1),其注意力机制避免了像RNN一样循环依赖于上一次结果,所以可以并行计算,复杂度为𝑂(1),计算复杂度根据注意力的推导公式也可以推出是。总的对比下来,自注意力相对于RNN和CNN都有一定优势。

FastInst: A Simple Query-Based Model for Real-Time Instance Segmentation

[TOC]

想写好一篇论文总结不是一件容易的事情,这需要写作者充分理解这篇论文的内容、几乎不能放过每一个细节设计。说来惭愧,上一次这么认真地写论文总结还是在读书的时候,时隔这么久再写,对于如何遣词结构更加了然于心,但是对于如何写更加捉襟见肘。固然有太久不写生疏的原因,更是因为这两年读论文都是囫囵吞枣,没有真正静下心来理解实现细节。比如对视觉Transformer架构、端到端检测或分割等都是不求甚解,写的时候难免抓耳挠腮。但是,坚持写下来会发现收益颇丰——写一篇总结会倒逼自己深入理解每一个设计细节,并进一步思考这么设计的背后含义。

CVPR2023:FastInst

link:FastInst: A Simple Query-Based Model for Real-Time Instance Segmentation

code: CVPR2023] FastInst: A Simple Query-Based Model for Real-Time Instance Segmentation

1. Motivation

Transformer已经卷到了实例分割领域,Mask2Former[1]搭建了一个良好的workflow,但是其还存在以下几个问题:

  1. Mask2Former的object 查询特征缺乏先验信息,因此需要一个很大的解码器做解码和精细化;
  2. 严重依赖于一个很重的像素解码器MSDeformAttn
  3. 掩膜注意力限制了每个查询特征的感受野,从而可能导致Transformer解码器陷入次优解;
  4. 实时性不够好。

本文着眼于Mask2Former存在的这几个问题进行改进,搭建了FastInst这一架构。显然,FastInst会主要对标Mask2Former,而其架构也基本遵循Mask2Former的架构,关键结构有3点:

  1. 改进查询特征,使用实例激活查询(instance activation-guided queries),从特征图中抽取包含富语义信息的embedding用于初始查询特征(解决问题1);
  2. 在Transformer解码器中使用双路架构,可以分别独立更新查询特征特征和像素特征,从而增强像素特征的表征能力(解决问题2和问题4);
  3. 引入使用标签掩膜做引导的学习策略(ground truth mask-guided learning)(解决问题3)。

2. Methods

2.1 总体架构

FastInst总体架构如下图,主要包含三个部分:骨干网络、像素解码器和Transformer解码器。

Figure 1:总体架构

执行流程为如下:

  1. Backbone提取特征,使用​卷积将特征通道全都映射为256,完成后送入像素解码器;
  2. 像素解码器(其实是PPM-FPN[2])融合上下文信息后输出特征
  3. 中抽取个实例激活queries,将其与个辅助的可学习queries拼接形成总的查询,其中
  4. 展平为,其中即是Transformer解码器的输入;Transformer解码器会在每一解码层分别更新这两个特征并且预测类别和掩膜。

2.2 实例级激活引导(Instance activation-guided, IA-guided)的查询特征

受Deformable DETR[3]的启发,FastInst从像素解码器的特征图中抽取富语义信息的特征点形成查询。那么,核心就在于如何感知哪里有富语义信息呢?FastInst在特征后加了一个辅助分类头(一个卷积层 + 一个卷积层 + softmax)预测类别概率,其中表示像素索引,表示个类别+背景。这样就可以获得每个像素的前景概率,据此从中抽取高前景概率的个特征点(pixel embedding)作为查询特征。

这里的抽取不是直接拿前景概率最高的点,而是有一套更精细的流程:

  1. 选取每个前景类平面上具有局部最大值(某一个点的值大于其8邻域的值即为局部最大值)的像素点;
  2. 选取其中具有最高前景概率的​个像素点作为查询特征。

这么做的好处显而易见:如果一个点的概率不是局部最大,那就意味着它的8邻域中存在更大的概率值,在如此近的距离上,我们自然会倾向于选择其邻域点。

在训练过程中,辅助分类头使用匈牙利匹配损失[4]做监督,但是这里只将其定义为一个位置损失​,当像素点落入物体区域真值为0,否则为1。

2.3 双路Transformer解码器

Transformer的查询特征 个IA-guided查询特征和个辅助可学习查询特征拼接而成。为什么要使用辅助可学习查询特征呢?简而言之,IA-guided查询特征对应于前景实例,而辅助可学习查询特征则对应于背景区域的上下文信息。

Transformer解码器的输入是查询特征 和展平的像素特征,分别为 添加位置编码后将其送入Transformer解码层更新。Transformer解码层设计为双路更新,即每个Transformer解码层包含一次像素特征更新和一次查询特征更新。整个过程和EM聚类相似,步骤如下:

  1. E step:将查询特征视为聚类中心,据此更新像素特征;
  2. M step:更新作为聚类中心的查询特征。

相对于单路更新策略,这种双路更新策略可以协同优化像素特征和查询特征,这种更高效和稳定的策略可以降低对重像素解码器的依赖。

最后,经过细化的像素特征和查询特征在每一个Transformer解码层预测物体类别和掩膜。

2.3.1 位置编码

FastInst使用可学习的位置embedding做位置编码。具体而言,初始化一个可学习的embedding ,其中。在前向过程中,将分别插值到大小从而分别为做位置编码。而对于辅助可学习特征则另外创建个可学习的位置embedding。

2.3.2 像素特征更新

如图1右侧所示,一次像素特征更新流程由一个cross-attention层和和一个前馈层组成,在cross-attention层会为查询特征和键特征做位置编码。

像素特征更新并没有使用self-attention,因为像素特征的序列长度比较长,使用self-attention会引入巨大的计算量和内存开销。

2.3.3 查询特征更新

一次特征更新流程由一个masked-attention层、self-attention层和一个前馈层组成,在masked-attention层和self-attention层会为查询特征和键特征做位置编码。

Masked-attention层非常重要,它使得每个查询特征将注意力集中在前景区域,由此导致的潜在的上下文关联缺失则可以由self-attention层弥补。

2.3.4 预测

在每个解码层中,更新后的IA-guided查询特征后接两个独立的MLP预测分支,预测包含背景类​​在内的物体类别和掩膜embedding。

掩膜:掩膜预测由两步构成。第一步更新后的像素特征经过一层线性映射层得到掩膜特征;第二步将掩膜特征和掩膜embedding相乘获得分割掩膜。

置信度:每个实例的置信度预测由其类别概率与掩膜概率的均值相乘得到。

值得一提的是,考虑到每个解码层像素特征和查询特征均被更新,不同解码器层的特征可能处于不同的表示空间,上边的MLP层和线性映射层在不同解码层之间不共享。

2.4 标签掩膜引导学习

在训练时候,每个解码层的masked-attention使用的掩膜不是预测出来的掩膜,而是最后一层二分匹配到的标签掩膜,如果没有对应的标签掩膜,则对应的掩膜为。即: 符号解释:

  • 表示标签实例的数量。表示最后一层的每个查询特征和每个标签掩膜的匹配映射;
  • :第个解码层的第个查询特征的注意力掩膜;
  • :第个标签掩膜。

2.5损失函数

整个结构的损失函数由三个部分相加构成:

  1. 实例激活损失 表示权重系数;表示交叉熵损失;如前所述,表示匹配损失。

  2. 解码层预测损失 表示Transformer解码层的数量;特殊表示IA-guided查询特征送入解码器之前的预测损失,此时已经有了像素特征和查询特征,所以可以像每个解码层一样计算损失;表示掩膜的二分类交叉熵损失和dice损失;表示分类损失。

  3. 标签掩膜引导损失 的计算方式与相同,只是不计算时的损失。

3. Reference

[1]. Bowen Cheng, Ishan Misra, Alexander G. Schwing, Alexan der Kirillov, and Rohit Girdhar. Masked-attention mask transformer for universal image segmentation. In CVPR, 2022.

[2]. Tianheng Cheng, Xinggang Wang, Shaoyu Chen, Wenqiang Zhang, Qian Zhang, Chang Huang, Zhaoxiang Zhang, and Wenyu Liu. Sparse instance activation for real-time instance segmentation. In CVPR, 2022.

[3]. Xizhou Zhu, Weijie Su, Lewei Lu, Bin Li, Xiaogang Wang, and Jifeng Dai. Deformable detr: Deformable transformers for end-to-end object detection. In ICLR, 2021.

[4]. Nicolas Carion, Francisco Massa, Gabriel Synnaeve, Nicolas Usunier, Alexander Kirillov, and Sergey Zagoruyko. End-to end object detection with transformers. In ECCV, 2020.

关于深度学习的一些总结

1. 模型

1.1 模型方案定义

实际问题是复杂的,如何从实际问题中抽象关键需求并定义为一个模型预测问题是非常重要和讲究的。这一步绝对不是拍脑袋看到个模型就用可以解决的,它要求设计者必须对深度学习有深入的了解。如果一开始的方案定义就有问题,那么后边再怎么做都是无用功。

深度学习能做什么——方案定义是否合理?

比如用卷积网络去做某件事,那么就必须了解卷积有什么特性、能做什么、不能做什么。一个common sense是卷积具有两大特性:局部连接和权值共享。从这两个特性往下推就是平移不变性,也就是说对同一个物体,不管处于图像上的哪个位置,其语义定义都应该是一致的、不能有歧义。比如同样一个物体,处于图像上的A区域时将其类别定义为,处于图像上的B区域时又将其类别定义为​,这样的定义就是有问题的,从长期来看,只会使模型confused。

另一方面,必须意识到深度学习模型的能力边界,什么样的设计会让模型学不好。比如数据不均衡导致了长尾效应,那么模型对于长尾类别的预测效果自然不好,设计的时候就要考虑是不是有更好的定义方式从而缓解长尾效应呢?比如要预测非常细小的物体,那么我给定的分辨率下模型能不能看到,如果看不到要考虑是不是输入分辨率要高一些或者使用对细小物体预测效果更好的模型呢?

什么样的模型能最好解决这个问题?

如何抽象出来关键需求同样重要,这决定了要用什么复杂度的模型解决问题。粗略地说,如果单纯想知道一片区域是什么东西,那么语义分割就足够了;如果想要粗粒度的实例级信息,那么目标检测就足够了,如果想要细粒度的实例级信息,那么实例分割就足够了;如果想要全图物体的信息并尽可能给出实例级信息,那么就要考虑全景分割等等。更进一步的,在每一个类型下还可以继续细分找最合适的模型方案,最合适的模型才是最好的模型,才能事半功倍。

1.2 数据的重要性

我认为数据集和模型是互相定义的,数据集提供先验数据分布,决定了模型的预测能力和预测方向;模型则定义了数据集的形式和接口并在实际应用过程中指明数据集的扩充方向。

数据驱动

对于深度学习应用来说非常重要的一点是建立由数据驱动的闭环迭代流程,即数据 -> 模型 -> 场景 -> 数据....这样的良性循环。

由数据训练模型、模型上线应用于场景这两点是很容易做到的,困难的在于如何从场景中获取高质量的数据,这就需要有一套完备的数据挖掘流程。只有打通这个流程,才能实现模型、数据和场景的迭代闭环。

数据集

搭建数据集的注意事项有几点:

  1. 如上边说过的,标注定义不能有歧义;
  2. 数据集应该丰富、多样、有效;
  3. 要有可靠的评估集,这意味着首先要独立,不能与训练集存在交叠。严格地讲,同一个场景不应该既出现在训练集中又出现在评估集中;其次评估集的数据应该有效,其大体上应该和训练集处于同一数据分布中。

1.3 类别不均衡 / 长尾问题

长尾问题是一个经常遇到又不好解决的问题,一方面在数据和模型设计上应该尽量避免和缓解这个问题,另一方面需要一些策略缓解。

数据集上:

  1. 借助于生成模型、仿真环境等方法扩充数据集中的长尾数据;

模型上:

  1. 重采样策略:训练时对tail类过采样,或对head类欠采样;
  2. 在loss计算时,使用Focal loss等;对不同的类别设置不同的权重,对tail类设置更大的权重;
  3. 在分割任务中使用region-based损失取代distributed-based损失
  4. 采用一些少样本问题的训练方法,如:meta-learning、few-shot learning、transfer learing;
  5. 训练时采用OHEM挖掘难负样本;
  6. Equalization Loss for Long-Tailed Object Recognition

1.4 多任务模型

对于一些数据同源或者基本同源的任务来说,可以将其合并在一个模型中,不同任务共享backbone(或backbone+FPN),每个任务有自己的head,这样做的好处有几点:

  1. 共用特征提取部分可以降低总的模型参数和运算量,减小负载;
  2. 如果任务之间没有明显冲突的话,多个任务联合训练有助于提升backbone的特征提取能力,获得更丰富的语义信息;
  3. 使用同一个模型后不同任务的结果是可以对齐到图像的,这样也有助于做一些后期的verify。

然后,要把多个任务合并在一个模型中不是一件容易的事情,处理不好的话会导致各个任务都显著降点。处理上大概有以下几个要点:

  1. 不同任务的数据量存在差异,可以考虑对不同任务过采样 / 欠采样;
  2. 使用多任务训练的策略,比如CAGradMTAN等;
  3. 调整各个任务的loss和权重,尽量使各项任务的收敛趋势基本一致,并且控制不同任务的loss量级在合理的范围内。

2. 部署

TODO

使用Hexo搭建GitHub博客

之前使用WordPress部署个人博客,但是因为自己买主机太贵不合算,所以迁到GitHub上。开始时候使用Jekyll部署,可惜Jekyll主题普遍不够美观,所以又改用Hexo部署,这里记录一下Hexo部署的过程吧。

1. 安装

需要安装 Node.jsGit,安装 Node.js之后可以用其npm工具安装Hexo

  1. 安装Git。 安装完成后可以通过Git Bash进入Terminal,在Terminal输入git --version查看版本信息;(也可以通过Windows的cmd命令行查看,不过我觉得Terminal更好,因为其和Linux使用体验完全一致)

  2. 安装Node.js。 安装完成后可以在Terminal输入node -vnpm -v查看版本信息;

  3. 安装Hexo。 创建Hexo的安装路径,比如D:\softwares\Hexo,在安装路径下打开Terminal进行安装:

    1
    2
    3
    npm install -g hexo-cli
    hexo init
    npm install

    此时即可在安装路径下看到生成的文件,结构附在后边。 注意:后续涉及到Hexo的命令行操作都需要在安装路径下的终端(Terminal或者cmd)执行

2. 配置Git

首先确保自己有GitHub账户。

  1. 创建用于博客的仓库。 仓库名字必须是username.github.io,相关步骤不赘述。

  2. 本地配置Git账户并设置免密。 Terminal输入:

    1
    2
    3
    4
    5
    git config --global user.name "your username"
    git config --global user.email "your email"

    # HTTP免密,SSH免密也可以,个人比较喜欢HTTP免密,步骤更简单
    git config --global credential.helper store

    因为Hexo部署时候会自动更新仓库内容,所以必须设置免密。

  3. (Optional)可以测试一下自己上边的设置有没有问题。

    1
    git clone https://github.com/[username]/[username].github.io.git

    如果成功拉下来就表明配置没有问题。

3. Hexo部署至GitHub Pages

3.1 部署流程

  1. (Optional)测试本地安装的Hexo是否正常。

    1
    2
    hexo g
    hexo s

    如果一切正常,此时访问http://localhost:4000就会看到Hexo的默认页面。

  2. 安装hexo-deployer-git

    1
    npm install hexo-deployer-git --save

  3. 修改Hexo安装路径下的_config.yml文件末尾的deploy部分为如下形式:

    1
    2
    3
    4
    deploy:
    type: git
    repository: https://github.com/[username]/[username].github.io.git
    branch: main

    注意两点,一是repository与上边免密方式对应,我这里是HTTP免密,如果是SSH免密,形式为git@github.com:[username]/[username].github.io.git;二是branch应该与GitHub Pages使用的分支一致,通常为main或者master

  4. 上传部署。

    1
    hexo d

    此时访问https://[username].github.io即可看到部署完成的网站了,同时也可以看到自己的仓库也对应更新了。

3.2 Hexo博客无法显示LaTeX公式

Hexo本身是不支持LaTeX的语法的,如果习惯与用LaTeX语法编辑公式,需要安装其他的插件。

根据查到的资料,方法主要有两种,一是使用hexo-renderer-kramed,但是这种方法我试过并不起效,可能因为已经放弃维护了;二是使用Pandoc,这个方法我试过是可用的,这里记录一下。

  1. 安装相关插件。需要先卸载掉原有的渲染器再安装:

    1
    2
    npm uninstall hexo-renderer-marked
    npm install hexo-renderer-pandoc --save

  2. 安装Pandoc,安装完成后重启电脑生效。

  3. 配置并生效。打开主题的_config.yml加入如下键值:

    1
    2
    3
    mathjax:
    enable: true
    per_page: true

    这里的写法是最简单的,不过我认为不是最好的,因为这里的per_page表示对每一篇文章都做渲染,如果不是每一篇文章都有LaTeX公式,那么这种做法会拖慢渲染速度。我的做法是: _config.yml加入如下键值:

    1
    2
    mathjax:
    enable: true

    在有LaTeX公式的每一篇文章的front-matter中加入mathjax: true

  4. 清楚之前的生成数据并重新生成部署:

    1
    2
    3
    hexo clean
    hexo g
    hexo d

3.3 修改主题

  1. 将自己想要的主题拷贝至themes文件夹下;
  2. 修改_config.yml中的theme为新主题名字;
  3. 发布和部署。

3.4 WordPress博客迁移至Hexo

Refer to:迁移 | Hexo

使用体验一般,主要存在的问题一是公式解析出问题;二是媒体文件需要重定向路径;三是表格、段落可能出错。如果WordPress里边的内容非常多,迁移工作将非常耗时。不过还能要啥自行车呢?

导出WordPress内容

WordPress需要导出的内容大致分两类,一类是文章,另一类是媒体。

  1. 文章导出。进入WordPress后台,工具->导出,导出需要的内容,将会以xml文件格式下载下来。
  2. 媒体导出。媒体文件存在于WordPress网站目录下的uploads文件夹中,将其下载到本地即可;如果是本地部署的话则位于WordPress目录下的wp-content\uploads中。

导入Hexo

  1. 文章导入。将上边下载下来的xml文件导入进来。

    1
    2
    npm install hexo-migrator-wordpress --save
    hexo migrate wordpress <wordpress.xml>

    此时即可在source\_posts下看到导入的文章了,如果有草稿则存在于source\_drafts下。

  2. 将媒体文件放在合适的地方。比如我倾向于放在source\assets下。

  3. 逐篇校对导入的文章,将媒体重定向为新的路径,同时注意公式等有没有解析出错。

  4. 部署上传。

4. 更多Hexo相关

4.1 Hexo安装路径下的文件信息

这里仅列出部分比较重要的文件夹/文件:

1
2
3
4
5
6
7
Hexo_dir
├── public # 将被更新到博客仓库的内容
├── scaffolds # 模版文件
├── source # 存放markdown格式的博客
├── themes # 存放Hexo主题
├── _config.yml # 网站的配置信息
└── package.json # 应用程序的信息

4.2 常用Hexo命令

Refer to:Documentation | Hexo

新建文章

1
2
hexo new [layout] <title>
hexo n [layout] <title>

layout选项如下:

Layout 保存路径 说明
post source/_posts 默认位置,该路径下的文章会被生成和发布
draft source/_drafts 作为草稿,不会被生成和发布
page source

使用实例:

1
2
3
4
5
6
# 在source\_posts下生成post-name.md,打开并编辑即可
hexo new "post-name"

hexo new draft "test" # 在source\_draft下新建草稿test.md
hexo publish "test" # 推送到source\_posts下
hexo --draft # 查看已有的草稿

文章生成和发布

1
2
3
4
5
6
# 生成博客页面
hexo g
# 启动本地预览
hexo s
# 部署和发布至GitHub Pages
hexo d

pyTorch模型转onnx

pyTorch模型转换为onnx格式相对来说是比较简单的,因为pyTorch提供了torch.onnx模块用于实现这一转换过程。

基本实现

将pyTorch模型转换为onnx格式的基本流程和转换为TorchScript格式是一样的,这是因为无论转换为TorchScript还是onnx都需要对torch.nn.Module进行tracing操作以记录模型所有的运算。

torch.onnx.export 函数说明

两者的不同之处在于实现转换功能的函数,转换为onnx格式需要用函数torch.onnx.export[2]实现。函数的用法pyTorch官方有详细的介绍,不过我觉得还是有必要将重点用更简单的语言说一下。函数格式如下:

1
2
3
4
torch.onnx.export(model, args, f, export_params=True, verbose=False, training=<TrainingMode.EVAL: 0>, \
input_names=None, output_names=None, operator_export_type=<OperatorExportTypes.ONNX: 0>, \
opset_version=None, do_constant_folding=True, dynamic_axes=None, keep_initializers_as_inputs=None,
custom_opsets=None, export_modules_as_functions=False)

看起来一堆参数,但是实际上必须的参数只有三个:

  • model: 要进行转换的模型,通常是加载好权重的torch.nn.Module实例。 多说一句,model也可以是torch.jit.ScriptModule,实际上torch.onnx.export转换时处理的就是torch.jit.ScriptModule实例,如果输入是torch.nn.Module实例,那么就会首先被转换为torch.jit.ScriptModule

  • args: 模型model的输入参数,可以是元组或者torch.Tensor

  • f: 简单理解是导出文件的名字,此时需要是一个包含文件名的字符串。

除了这三个必要的参数,还有一些非常重要、常用的参数:

  • input_names: 运算图输入节点的名字,类型是字符串列表
  • output_names : 运算图输出节点的名字,类型是字符串列表
  • dynamic_axes: 可以控制将导出的模型设置为支持输入动态维度
  • opset_version: 运算集版本

torch.onnx.export 函数使用

下边通过一个简单的例子说明一下torch.onnx.export的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import torchvision

# 创建模型实例,对于自定义的模型需要加载权重
model = torchvision.models.resnet18(pretrained=True).cuda()

# 评估模式
model.eval()

# 创建一个示例输出,维度和forward函数的输入一致
dummy_input = torch.rand(1, 3, 224, 224, device='cuda')

# 执行转换并将结果保存为`resnet18.onnx`
torch.onnx.export(
model,
dummy_input,
"resnet18.onnx",
input_names = ["image"],
output_names = ["pred"],
dynamic_axes = {"image": {0: "batch"},
"label": {0: "batch"}},
opset_version=11
)

根据我们对于这个函数的认知,我们可以知道:这段代码最后得到了一个onnx模型,名字为resnet18.onnx,转换用到的操作集版本是11,resnet18.onnx的输入节点名字是image、输出节点名字是pred,输出和输出的batch size都是动态的,但每张输入图像的维度是固定的,那就是

为了验证我们转出来的onnx模型是不是跟我们预想的一致,还可以把onnx模型上传到https://netron.app 实现可视化查看。

完整的转换实例脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import argparse
import torch

# === import your model ===
from model import MyModel

parser = argparse.ArgumentParser("export model as onnx")
parser.add_argument('--checkpoint', type=str, required=True)
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument(
"--input-shape",
type=str,
default="224,224",
help="specify the input shape for inference")
parser.add_argument("--dynamic", action="store_true", help="whether the input shape should be dynamic")
parser.add_argument("--file-name", type=str, default="model.onnx", help="onnx file name")
parser.add_argument("--input-name", type=str, default="image", help="name of input node")
parser.add_argument("--output-name", type=str, default="pred", help="name of output node")
parser.add_argument("--opset", type=int, default=11, help="onnx opset version")

args = parser.parse_args()

model = MyModel().cuda()
checkpoint = torch.load(args.checkpoint)
model.load_state_dict(checkpoint)

model.eval()

input_shape = tuple(map(int, args.input_shape.split(",")))
dummy_input = torch.randn(args.batch_size, 6, *input_shape, device="cuda")

torch.onnx.export(
model,
dummy_input,
args.file_name,
input_names = [args.input_name],
output_names = [args.output_name],
dynamic_axes = {args.input_name: {0: "batch"},
args.output_name: {0: "batch"}} if args.dynamic else None,
opset_version=args.opset,
)

print("save torchscript as " + args.file_name)

参考

[1]. https://onnxruntime.ai/docs/get-started/with-python.html

[2]. https://pytorch.org/docs/stable/onnx.html#functions

pyTorch模型转torchscript

作为一个高效好用的深度学习框架,pyTorch被广泛用于深度学习模型的搭建和训练,但是却几乎没有人会直接将pyTorch模型用于部署。对此,pyTorch官方也给出了自己的一种解决方案——TorchScript。

TorchScript[1]是一种从PyTorch代码创建可序列化、可优化的模型的方法,任何TorchScript程序都可以从Python进程中保存,并加载到没有Python依赖的进程中,从而可以实现模型的部署。

将pyTorch模型转换成TorchScript有两种方法:一种叫tracing,另一种叫scripting。通常这两种方法任意一种都可以,但在一些特定模型结构中需要将两者结合使用。

鉴于tracing对于我来说已经足够,本文只关注tracing方法。

tracing用法

tracing方法的核心是用torch.jit.trace记录一次模型推理中经过的所有运算记录,将这些记录整合成计算图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch
import torchvision

# 创建模型实例,对于自定义的模型需要加载权重
model = torchvision.models.resnet18(pretrained=True)

# 评估模式
model.eval()

# 创建一个示例输出,维度和forward函数的输入一致
dummy_input = torch.rand(1, 3, 224, 224)

# 用 torch.jit.trace 生成 torch.jit.ScriptModule
with torch.no_grad():
traced_script_module = torch.jit.trace(model, dummy_input)

# 保存 TorchScript,习惯上后缀为.pt
traced_script_module.save("traced_resnet.pt")

这样即可把pyTorch模型转换成TorchScript了。

创建一个完整的转换脚本

这里补充一个完整的转换脚本吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import argparse
import torch

# === import your model ===
from model import MyModel

parser = argparse.ArgumentParser("export model as torchscript")
parser.add_argument('--checkpoint', type=str, required=True)
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument(
"--input-shape",
type=str,
default="224,224",
help="specify the input shape for inference")

args = parser.parse_args()

model = MyModel()
checkpoint = torch.load(args.checkpoint)
model.load_state_dict(checkpoint)

model.eval()

input_shape = tuple(map(int, args.input_shape.split(",")))
dummy_input = torch.randn(args.batch_size, 6, *input_shape)

with torch.no_grad():
traced_script_module = torch.jit.trace(model, dummy_input)

traced_script_module.save("model.pt")
print("save torchscript as model.pt")

TorchScript加载

pyTorch模型转换成TorchScript后可以很方便地用python或者C++直接加载,如在python中可直接使用torch.jit.load加载序列化后的模型。

torch.jit.load函数原型如下:

1
torch.jit.load(f, map_location=None, _extra_files=None, _restore_shapes=False)
  • f: 一个文件流对象或者模型文件名字符串,如我们的model.pt;
  • map_location: 字符串(如cuda:0)或torch.device,用于将模型映射加载到指定的设备上。 这一点很重要,因为默认情况下torch.jit.load会试图将模型加载到保存时所使用的设备上,如果这个设备不存在就会报错。比如保存时模型在cuda:1上,而我们加载时候的设备只有一张卡即cuda:0,那么就会出错。而通过指定map_location我们可以重新分配用于加载模型的设备。

加载实现:

1
2
3
4
# 使用保存时的设备加载
model = torch.jit.load("model.pt")
# 使用1卡加载
model = torch.jit.load("model.pt", map_location="cuda:1")

在C++中:

1
auto model = torch::jit::load('model.pt');

参考

[1]. https://pytorch.org/docs/master/jit.html

[2]. http://djl.ai/docs/pytorch/how_to_convert_your_model_to_torchscript.html

python实现文件锁

之前写过一篇用python实现多进程与多线程的文章,在使用多进程或者多线程的情况下,使用锁来实现互斥以避免同时对资源执行读和写就成为一个不可避免的事情。 对于一般的变量资源来说实现互斥锁很简单,使用multiprocessing.Lock类或者threading.Lock即可。但是如果我们要实现互斥的是文件呢?

写这篇文章的初衷是我没有找到现成的库可以用,所以特意自己写了一个想贴出来。但是写到一半的时候发现了filelock库可以解决这个问题,使用起来要比自己写的更好一些。索性就把原来写的全都删了,分享一下用filelock实现文件锁的方法。

安装

使用pip或者conda等安装filelock即可。

1
pip install filelock

使用

这里将介绍一下用filelock模块实现文件锁的基本用法。

导入接口

1
from filelock import FileLock

文件和锁文件

1
2
file = "file.txt"
lockfile = file + ".lock"

这里的文件file是我们要实现互斥锁定的文件。 锁文件lockfile用于确定是否允许进程或者线程访问该文件,具体机制我们可以不必关心,能够正确创建的使用就可以了。

实例化锁对象

1
lock = FileLock(lockfile)

实例化出来一个锁对象lock,注意这里的参数是锁文件lockfile而不是文件filelock在上锁的时候,如果文件已经被其他对象上了锁,那么将一直等待到文件被其他对象释放锁后再上锁。如果不想在获取不到锁的时候永远等待下去,可以如下实例化锁对象:

1
lock = FileLock(lockfile, timeout = 5)

这时,lock在上锁的时候,如果文件已经被其他对象上了锁,那么在5秒内如果文件被其他对象释放锁,lock会对其上锁;否则就会因超时而报错退出。

锁的获取和释放

1
2
3
lock.acquire()
# do something...
lock.release()

使用方法很直观,acquire()方法用于获取锁,release()方法用于释放锁。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
from filelock import FileLock

file = "file.txt"
lockfile = file + ".lock"

lock = FileLock(lockfile)
lock.acquire()
try:
print("acquire the lock!")
# do something to the file...
finally:
lock.release()
print("release the lock!")

例子

这里,我们将分别跑两个相同的脚本main1.pymain2.py以模拟多进程,这两个脚本均试图排他的获取文件file.txtmain1.pymain2.py中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from filelock import FileLock

file = "file.txt"
lockfile = "file.txt.lock"

lock = FileLock(lockfile)

lock.acquire()
try:
# do something to the file...
time_now = time.strftime('%H:%M:%S', time.localtime())
print(time_now + ": acquire the lock!")
time.sleep(5)
finally:
lock.release()
time_now = time.strftime('%H:%M:%S', time.localtime())
print(time_now + ": release the lock!")

先后将main1.pymain2.py跑起来,输出如下: file_lock_result

显然,main1.py先完成了对文件上锁,main2.py就一直处于等待中,一直到main1.py释放了锁,main2.py实现了文件上锁,从而实现了对文件的互斥性访问。

python多进程与多线程

多进程和多线程在编程中是一个重要的特性,本文主要讲解在python中如何实现多线程和多进程。

多线程

python中多线程速度慢,不建议使用!

python中的多线程机制不够完善,用起来效果很鸡肋。以我的实现为例,在不使用多线程的情况下运行一次需要5s左右,使用多线程之后反而需要14s之久。这显然是不可接受的,所以不推荐在python中使用多线程,如果真的需要并发,建议使用多进程代替。

python中的多线程速度慢究其原因在于python中的Global Interpreter Lock(GIL),线程只有获得GIL才可运行,导致的结果是每次最多只有一个线程在运行。GIL导致运行慢的原因不在本文讨论范围,感兴趣可自行搜索。

不过python中的多进程基于多线程实现,两者的接口使用方式几乎是一样的,所以本文还是对多线程实现做出讲解。

实现方法

python中多线程通过threading库实现,创建线程有两种方法,不过根本上都是需要用到threading.Thread类。

方法1:自定义线程的运行内容

这种方法需要自己提前以函数的形式定义好要在线程中运行的程序。为直观起见,这里我只使用了两个线程,两者均运行同一个程序函数do_something。此外,我这里将输出用日志打印,这样可以显示运行时间,便于理解线程的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
import threading
# 日志配置
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')

# 要在线程中运行的程序
def do_something(name):
logging.info(name + ' is playing toy.')
time.sleep(1)
logging.info(name + ' leaves.')

if __name__ == '__main__':
children = ['Tom', 'John']
# 创建线程
threads = [threading.Thread(target=do_something, args=(child,)) for child in children]

# 开启进程
for thread in threads:
thread.start()
# 等待进程结束
for thread in threads:
thread.join()

运行上述代码,我得到的输出是:

1
2
3
4
18:50:31 Tom have access to toy: beer
18:50:31 John have access to toy: beer
18:50:32 Tom leaves!
18:50:32 John leaves!

可见,两个线程同时开始运行,并在执行结束后分别退出。

方法2:自定义线程

相比于方法1以函数的形式定义线程要运行的程序,方法2直接定义一个要运行的线程,该线程需继承threading.Thread类,并重写其run方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
import threading
# 日志配置
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')

# 线程类
class DoSomething(threading.Thread):
def __init__(self, name, *args, **kargs):
super().__init__(*args, **kargs)
self.name = name

def run(self):
'''运行程序写在这里'''
logging.info(self.name + ' is playing toy.')
time.sleep(1)
logging.info(self.name + ' leaves.')

if __name__ == '__main__':
children = ['Tom', 'John']
threads = [DoSomething(name=child) for child in children]

for thread in threads:
thread.start()

for thread in threads:
thread.join()

运行该程序输出和方法1类似。

共享资源

在运行多线程的时候经常需要多个线程共享资源,要实现这一点只需对上边的程序稍微改进即可,这里以方法1为例说明,我额外创建了一个Shared类用于多线程共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import threading
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')

# 共享类
class Shared(object):
toy = 'beer'

def do_something(resource, name):
logging.info(name + ' have access to toy: ' + resource.toy)
time.sleep(1)
logging.info(name + ' leaves!')

if __name__ == '__main__':
# 共享资源
resource = Shared()

children = ['Tom', 'John']
threads = [threading.Thread(target=do_something, args=(resource, child)) for child in children]

for thread in threads:
thread.start()

for thread in threads:
thread.join()

使用方法2共享资源也是同理。

多进程

相对于python中多线程存在的硬伤,多进程则靠谱很多。python任何希望用多线程实现的程序都可以用多进程代替,而且多进程提供了和多线程非常相似的API接口,迁移起来非常方便。

python中多进程通过multiprocessing库实现,实现的类是multiprocessing.Process。由于和多线程接口一致,多进程也有两种实现方式,而且多进程也可以像多线程一样共享资源。

以方法1为例,要想将多线程改写为多进程,只需要导入multiprocessing库并将threading.Thread替换为multiprocessing.Process即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
# 导入多进程库
import multiprocessing

import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')

class Shared(object):
toy = 'beer'

def do_something(resource, name):
logging.info(name + ' have access to toy: ' + resource.toy)
time.sleep(1)
logging.info(name + ' leaves!')

if __name__ == '__main__':
resource = Shared()

children = ['Tom', 'John']
# 替换
threads = [multiprocessing.Process(target=do_something, args=(resource, child)) for child in children]

for thread in threads:
thread.start()

for thread in threads:
thread.join()