SunDoge's Blog

What TripleZ don't know

0%

这篇文章假定读者对 python 中的装饰器有一定的了解。

我们先来设计一个最简单的 decorator:

1
2
3
4
5
6
7
8
9
10
def f1(func):
print('f1')
return func

@f1
def f0(x):
print('x:', x)

if __name__ == "__main__":
f0('asdf')

当我们执行这个脚本的时候,我们很自然会得到下面的输出

1
2
f1
x: asdf

这个时候,我们只能写 @f1,而不能写 @f1()。如果我们把 @f1 改成 @f1(),会得到以下错误:

1
TypeError: f1() missing 1 required positional argument: 'func'

因为被装饰的函数会被当成第一个参数(positional argument),而加上括号之后,相当于少了第一个参数。对于使用者来说,要记忆某个 decorator 是否要带括号是个不小的心智负担。你可能会想,在使用 python 标准库中的 dataclasses 时,@dataclass@dataclass() 都是合法的,为什么这里就不行了。其实想实现 dataclasses 类似的功能其实也很简单。我们只要判断 func 是否为 None,如果是,就返回一个新的 decorator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f1(func=None):
print('f1')
return func


def f2(func=None):
if func is None:
return f1
else:
return f1(func=func)

@f2()
def f0(x):
print('x:', x)

这样一切都正常了。

今天修改论文的时候 Latex 报了一个挺离谱的 bug

image.png

为了方便搜索,下面是文字版

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
Undefined control sequence.

The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \usepackage{...}.
Learn more

\citeauthoryear #1#2->\def \@thisauthor
{#1}\ifx \@lastauthor \@thisauthor \...
l.172 }

The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.
self-supervised, line 172

Illegal parameter number in definition of \reserved@a.

<to be read again>
}
l.172 }

You meant to type ## instead of #, right?
Or maybe a } was forgotten somewhere earlier, and things
are all screwed up? I'm going to assume that you meant ##.
self-supervised, line 172

Illegal parameter number in definition of \reserved@a.

<to be read again>

l.172 }

You meant to type ## instead of #, right?
Or maybe a } was forgotten somewhere earlier, and things
are all screwed up? I'm going to assume that you meant ##.
self-supervised, line 172

Illegal parameter number in definition of \reserved@a.

<to be read again>
2
l.172 }

You meant to type ## instead of #, right?
Or maybe a } was forgotten somewhere earlier, and things
are all screwed up? I'm going to assume that you meant ##.
self-supervised, line 172

Undefined control sequence.

The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \usepackage{...}.
Learn more

\cite ...\citeauthoryear ##1##2{\def \@thisauthor
{##1}\ifx \@lastauthor \@t...
l.172 }

The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.
self-supervised, line 172

Illegal parameter number in definition of \reserved@a.

<to be read again>
1
l.172 }

You meant to type ## instead of #, right?
Or maybe a } was forgotten somewhere earlier, and things
are all screwed up? I'm going to assume that you meant ##.
self-supervised, line 172

Argument of \caption@ydblarg has an extra }.

<inserted text>
\par
l.172 }

I've run across a `}' that doesn't seem to match anything.
For example, `\def\a#1{...}' and `\a}' would produce
this error. If you simply proceed now, the `\par' that
I've just inserted will cause me to report a runaway
argument that might be the root of the problem. But if
your `}' was spurious, just type `2' and it will go away.
self-supervised, line 172

Runaway argument?

{\@caption \@captype }{\@tempswatrue \@citex }\def \reserved@b {\@tempswafalse \ETC.
! Paragraph ended before \caption@ydblarg was complete.
<to be read again>
\par
l.172 }

I suspect you've forgotten a `}', causing me to apply this
control sequence to too much text. How can we recover?
My plan is to forget the whole thing and hope for the best.

从 log 里面基本是没法定位问题,但是通过排除法,最终把问题定位到 ~\cite{speednet} 上。我先检查了 bibtex,没有发现问题,随后我意识到可能是 \cite\caption 里面,所以才有 Illegal parameter number 的问题。上网搜了以下,找到以下信息:

https://tex.stackexchange.com/questions/227833/cite-references-in-figure-caption

答案作者的解释是 cite 会变成 caption 的 argument,解决方案是加上 \protect。也就是说变成 ~\protect\cite{speednet},问题解决。

今天用 aria2下载 s3 上的文件时,出现以下错误:

1
2
3
09/03 15:34:47 [ERROR] CUID#7 - Download aborted. URI=https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_image_2.zip
Exception: [AbstractCommand.cc:351] errorCode=1 URI=https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_image_2.zip
-> [SocketCore.cc:1018] errorCode=1 SSL/TLS handshake failure: The TLS connection was non-properly terminated.

但是用 wget 却一切正常。开始怀疑 s3 限制连接数,但是减少线程数仍然无法下载。最后在一个不相关的 issue #502里面找到了解决办法。似乎问题是 Async DNS 引起的。加上 --async-dns=false 后问题解决。

暂时不清楚 Async DNS 干了什么,如果以后看源码弄清楚了再补充。

TypedArgs v0.4.0 发布了。大部分接口与 v0.3.x 没有差别,但是少了一个 attribute,所以必须升一个 minor version。这篇博客主要记录我的几个设计失误。

argparse.ArgumentParser 不能被 pickle

TypedArgs 内部是使用 Python 标准库 argparse 实现的。一个标准的 argparse 使用流程如下:

1
2
3
4
5
6
7
import argparse

parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('data', help='path to data')
args = parser.parse_args(['data-path'])

assert args.data == 'data-path'

首先,ArgumentParser 支持指定显示的 program 名,那 TypedArgs也应该支持。那一个很自然的想法就是让 parser 成为 class TypedArgs 的一个 attribute,这样解析 arguments 的时候就能自由访问。

1
2
3
4
5
6
7
from dataclasses import dataclass, field
from typed_args import TypedArgs
import argparse

@dataclass
class Args(TypedArgs):
parser: field(default_factory=lambda: argparse.ArgumentParser(prog='PROG'))

但是这带来了一个问题,我并没有意识到 ArgumentParser 是不能被 pickle(序列化)的。虽然平时使用没有问题,但是到了多进程环境(比如 PyTorch 的 DistributedDataParallel)就会报错。为此。v0.3.x 临时打了一个 patch,在解析完 arguements 之后,将 self.parser = None。其实这个时候已经造成了 API 变动,minor version 应该 + 1,但是当时思想出了问题,只升了 patch version。当然,没有人会访问 parser,所以理论上问题不大。

v0.4.0 从根本上解决了这个问题,parser 只在解析 arguments 被生成,解析完就回收,不再作为 TypedArgs 上的一个 attribute。

如何让 IDE 正确识别出类型

PyCharm 和 VS Code 对于类型推倒总是有不同的想法。PyCharm 认为 = 后面的才是正确类型,直接忽略了我的 annotation。VS Code 就蠢一点,认为我的 annotation 是对的,但是使用的时候又自作聪明不显示我标注的类型。最后我采用了现在的方案:

1
2
3
4
foo: str = func()

def func() -> Any:
pass

这样写,两个 IDE 都能正确推导类型,而且也比较符合常识。

使用 GitHub Action 自动发布到 pypi.org

GitHub 很贴心,GitHub 整个模板都给你弄好了,你只需要去 Settings > Secrets 填两个参数(PYPI_USERNAMEPYPI_PASSWORD)。之后每次 push 带 tag,或者在 GitHub 上创建 release 的时候(创建 release 会创建一个 tag),就会触发这个 GitHub Action,将这个库发布到 pypi.org

安装 PulseAudio Volumne Control

有些主机插入耳机之后没有声音,因为系统自带的音量控制无法检测到前面板。可以安装 PulseAudio Volumne Control 来解决这个问题。

Pacman 包管理器中,这个包名叫 pavucontrol(gtk 版本)或 pavucontrol-qt(qt 版本)。安装其中一个即可。

image.png

image.png

保存 Profile

就算解决了耳机没声音的问题,重启之后配置也会丢失,下次开机还要重新配置一边。可以通过命令行将当前的 profile 保存下来,重启之后也不会丢失。

https://unix.stackexchange.com/questions/462670/set-default-profile-for-pulseaudio

先确定当前 profile 的名字

1
pacmd list-cards | grep 'active profile'

然后在 /etc/pulse/default.pa 中加一行

1
set-card-profile <cardindex> <profilename>

我的是

1
set-card-profile 0 output:analog-stereo

今天 push 代码的时候遇到一个问题:

1
2
3
4
5
6
7
8
9
Counting objects: 47, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (44/44), done.
Writing objects: 100% (47/47), 573.15 KiB | 0 bytes/s, done.
Total 47 (delta 11), reused 0 (delta 0)
remote: Resolving deltas: 100% (11/11), done.
To github.com:SunDoge/xxxx.git
! [remote rejected] master -> master (shallow update not allowed)
error: failed to push some refs to 'git@github.com:SunDoge/xxxx.git'

找到 stackoverflow 上的一个答案

https://stackoverflow.com/questions/28983842/remote-rejected-shallow-update-not-allowed-after-changing-git-remote-url

那么问题也就很明显了,我在 clone 原仓库时用了 git clone --depth 1,导致本地为 shallow repo。解决方法也很简单

1
git fetch --unshallow origin

origin 就是原仓库。fetch 完就能正常 push 了。

本文是 https://determined.ai/blog/tf-dataset-the-bad-parts/的译文。我在尝试用 TensorFlow 参照 PyTorch 代码复现一些模型的时候也产生了同样的想法。本文仅供个人研究使用。


TLDR: TensorFlow 的 tf.data API 是一个常用的将数据加载进深度模型的方法。虽然 tf.data 有很多强大的功能,但是它是围绕顺序读取数据集设计的。这个设计导致它无法高效实现一些功能:shuffle 大数据集,分布式训练时对数据进行分片,实现容错训练(fault-tolerant training)。我们认为,在构建深度学习数据读取 API 时,随机访问应该是需要考虑的关键因素。

在构建端到端的企业深度学习平台时,工程团队可能会遇到许多陷阱。最常见的问题之一涉及数据加载。训练期间的数据加载常常被忽略,但是它可能对吞吐量产生巨大影响。机器学习框架提供了一些抽象,试图让数据加载变得简单直接。但是,在看似简单接口背后可能隐藏了一些令人惊讶的问题。在这篇文章中,我们将带你了解一个常见的数据加载 API:TensorFlow Datasets

image.png

Data Loader Patterns

数据加载接口可以使用两种基本模式,随机访问(random access)顺序访问(sequential access)

Random Access

随机访问是指高效访问数据集中任何元素的能力。在 Python 中,随机访问通常通过对一个列表进行索引来完成(i.e., data[index]) ,背后实际调用了__getitem__()。PyTorch 使用这种方法来定义映射样式(map-style)的数据集接口(上面实现的那种)。随机访问数据加载器接口还可能要求用户指定数据集的整个长度(__len__())。

1
2
3
4
5
6
7
8
9
10
11
import torch.utils.data

class RandomAccessDataset(torch.utils.data.Dataset):
def __init__(self, data: List) -> None:
self.data = data

def __len__(self) -> int:
return len(self.data)

def __getitem__(self, index: int): -> Any:
return self.data[index]

支持随机访问的深度学习数据 api 包括 tfkeras.utils.Sequencetorch.utils.data.Dataset (Map Style)。

Sequential Access

顺序读取是一种元素必须按预定顺序访问的模式,通常是通过迭代器访问。在 Python 中,顺序访问通常是通过迭代器和 yield 表达式实现的。

1
2
3
def sequential_dataset(data: List) -> Iterator:
for item in data:
yield item

一些深度学习框架,比如早期版本的 keras,原生支持将数据输入管道(data input pipeline)表示为 Python 生成器。类似地,TensorFlow Datasets 是围绕顺序数据访问构建的。 Python 生成器转换为 TensorFlow Dataset 非常简单,虽然有点啰嗦:

1
2
3
4
5
6
7
8
9
10
import itertools

def gen():
for i in itertools.count(1):
yield (i, [1] * i)

dataset = tf.data.Dataset.from_generator(
gen,
(tf.int64, tf.int64),
(tf.TensorShape([]), tf.TensorShape([None])))

在下一节中,我们将讨论使用循序访问作为数据加载器的缺点。

Sequential Access in TensorFlow Datasets

TensorFlow 的 tf.data API 以一种优雅的方式简化了数据加载代码的结构:你可以使用 lazy initialization 的方式将一系列操作串起来。tf.data 还提供了 helper API 来完成预读取并行数据加载等常见任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import tensorflow as tf


dataset = tf.data.Dataset.from_tensor_slices([1,2,3])

for element in dataset:
print (element)
>>> tf.Tensor(1, shape=(), dtype=int32)
>>> tf.Tensor(2, shape=(), dtype=int32)
>>> tf.Tensor(3, shape=(), dtype=int32)

dataset = dataset.map(lambda x: x*2)
for element in dataset:
print (element)
>>> tf.Tensor(2, shape=(), dtype=int32)
>>> tf.Tensor(4, shape=(), dtype=int32)
>>> tf.Tensor(6, shape=(), dtype=int32)

然而,TensorFlow Dataset 基本上是围绕顺序访问构建的tf.data pipeline 中的每一个操作,都会迭代他的输入并生成一个顺序的输出流,供下一个操作使用。这个 API 不支持随机访问,导致在尝试实现一些常见的机器学习工作流时会出现一些重大问题。

1
2
3
4
5
dataset[0]
>>> TypeError: 'TensorSliceDataset' object does not support indexing

list(dataset.as_numpy_iterator())
>>> [1,2,3]

Data Shuffling

在训练一个深度学习模型时,训练集通常在输入模型前被 shuffle,这个操作通常会提升泛化性能。如果我们的数据 API 只支持顺序访问,要怎样实现 random shuffling 呢?一个简单但是低效的方法是将尽可能多的数据读入内存并在内存里面 shuffle。实际上,这正是 tf.data 的 shuffle 的做法!

This dataset fills a buffer with buffer_size elements, then randomly samples elements from this buffer, replacing the selected elements with new elements. For perfect shuffling, a buffer size greater than or equal to the full size of the dataset is required.

对于不能完全放入内存的数据集(深度学习中最常见的情况),**shuffle() 实际上不会 shuffle 整个数据集 **!这意味着 shuffle() 在大多数应用程序中没有达到预期的效果。包括我们在内的很多从业人员都犯了这个错误,并且看到他们模型的泛化性能因此收到影响。虽然可以通过提前将数据读进内存或 shuffle 文件名列表来实现 shuffle 整个数据集,但是很多用户可能没有意识到这个他们的代码中存在这个问题!

Data Sharding

在进行 data-parallel distributed training 时,每一个 worker(通常是 GPU)要对每个 batch 的一个部分(或者叫 shard)进行训练。为了处理这个常见的任务,tf.data 提供了一个看起来完美契合我们要求的方法:shard(n, i)将数据分为 n 个 shards,并返回第 i 个 shard,以便在当前 worker 中继续处理。

不幸的是,这里存在一个陷阱:shard() 会遍历整个输入数据集,每次返回第 n 个记录并忽略其他记录(我的理解是假设 n=4,输入数据会按 12341234 的方式分发,但是每个 worker 都要读完 1234 才能选出第 n 个)!这意味着如果在分布式训练时,对打数据集应用 shard() 操作,每个 worker 在分布式训练任务中最终将读取整个数据集。如果你正在用 64 块 GPU 训练一个模型,这意味着消耗比预期多 64 倍的磁盘 I/O。如果你在 shard() 之前进行动态(on-the-fly)数据增强,那么情况会变得更糟 —— 这些数据增强操作会被每个 worker 冗余地执行。

TensorFlow 的文档承认了这一点并指出:

Generally it is best if the shard operator is used early in the dataset pipeline.
通常来说,最好在数据集 pipeline 的早期使用 shard 操作。

TensorFlow 推荐的方法是创建 TFRecord 文件记录文件名,并在文件名列表上应用 shard()。每个 worker 接收并处理一组不相交的文件,这样就避免了任何不必要的磁盘 I/O。这种方法是有效的,但存在两个问题:

  1. 在分布式训练中,你要将数据集划分为比 worker 数还要多的文件。如果你有一个大数据集存在少数几个文件中,那你就不走运了。此外,这些文件之间的任何大小不平衡都会导致掉队,从而影响训练效果。
  2. 更有可能的是,你没有意识到这些问题!许多实际的数据加载代码只是将 Python 生成器通过 Dataset.from_generator() 转换为 TensorFlow Dataset。当处理小规模数据时,这是没问题的,但是随着数据集的增长,将很快遇到严重的性能问题。

保存和还原迭代器状态

如果你希望构建一个可以从错误中恢复的深度学习训练系统,一种常见的方法是使用 checkpoint-restart:定期将训练任务的状态保存到 checkpoint 文件中,并在发生故障时从最近的 checkpoint 中恢复任务。这对于那些可以持续几个小时甚至几天的训练工作尤其重要。但是,保存和恢复训练需要知道当前训练阶段在数据集中的位置。

例如,如果你在一个 100,000 个元素的数据集上训练,最近的 checkpoint 是在第 50,000 个记录之后执行的,那么你需要确保从第 50,001 条记录恢复,而不是从头开始。对于随机访问接口,这很简单:只需要将索引位置保存为整数,然后从中断的地方恢复训练。对于顺序访问接口,这可能非常困难。总所周知,Python 生成器很难被 pickle(序列化)。TensorFlow Dataset 实验性地支持 checkpoint 和恢复某些类型的数据集,但不支持使用 tf.data.Dataset.from_generator() 创建的数据集。

解决方案

之所以存在上述问题,是因为 tf.data 是围绕循序存取构建的。所以,帮自己个忙:不要让你的数据读取代码完全依赖顺序访问模式。如果你像使用预读取和支持顺序读取的函数式风格,你可以像下面这样包装一个随机读取的接口:

1
2
3
4
5
dataset = RandomAccessDataset()

def sequential_access_dataset() -> Iterator:
for index in range(len(dataset)):
yield dataset[index]

从随机到顺序是很容易的,但是走另一条路就难得多了。

TensorFlow Dataset 是目前推荐的在 TensorFlow 中加载数据的方式,而且这种方式看起来一段时间内都不会改变。本文的许多读者可能会发现自己处于一个不幸的位置,由于不可控因素,他们只能依赖 TensorFlow Dataset。如果你也是这样,我们一直在努力寻找一个能让你的生活更轻松的解决方案 —— 下周请继续关注!

基本流程参照 https://github.com/marketplace/actions/hexo-action

Step 1. 生成 ssh key pair

首先,我们需要生成 ssh key pair。最好是专门为这个仓库生成一个 pair,因为后面需要上传私钥,如果是本机常用的 ssh key pair,会存在一定风险。

1
ssh-keygen -t rsa -C "username@example.com"

邮箱可以修改为自己的邮箱,询问保存 key 的文件路径时,随便写一个,避免覆盖原有的 key。

1
2
3
 ± ssh-keygen -t rsa -C "username@example.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/sundoge/.ssh/id_rsa): hexo

以输入 hexo 为例,在当前目录下得到 hexohexo.pub 两个文件,前者是私钥,后者是公钥。

将公钥填入 Settings > Deploy Keys。将私钥填入 Settings > Secrets,键名设为 DEPLOY_KEY。这样 GitHub Action 就可以利用 DEPLOY_KEY 将生成好的静态页面 push 到部署分支。

Step2: 配置 GitHub Action

创建.github/workflows/xxx.yml,用你认为的 workflow 名字命名。Hexo Action里面给出了详细的设置,我这里只放我自己的

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
name: Hexo Deploy

on:
push:
branches: ['dev']

jobs:
build:
runs-on: ubuntu-latest
name: A job to deploy blog.
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Cache node modules
uses: actions/cache@v2
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci

# Deploy hexo blog website.
- name: Deploy
id: deploy
uses: sma11black/hexo-action@v1.0.2
with:
deploy_key: ${{ secrets.DEPLOY_KEY }}

- name: Get the output
run: |
echo "${{ steps.deploy.outputs.notify }}"

首先我把写作分支设在了 dev,所以触发条件是 dev branch 存在 push 操作。然后我们会检查 node_modules 的缓存,如果没有缓存就 npm install 并缓存。最后我们调用 sma11black/hexo-action,里面本质上就是帮你执行了 hexo deploy 的操作,这样编译好的静态页面就推到 master branch 了。

尝试过几次,删掉多余的 dependency 之后,GitHub Action 正常运作。

image.png

不足的地方在于调用别人的 action,可能有一些需要手动安装的依赖缺失。后续考虑研究一下 GitHub Action。

自 Hexo 2.8.2 之后 [1],Hexo 支持使用 theme_config 来配置 theme。在这之前,如果我们想要使用 git 来更新我们 theme,同时又想用 git 管理 theme 的 config,我所知道的唯一一个办法,就是 fork 一份 theme,修改其中的_config.yml,再用 git submodule 将 fork 的 theme 引入博客仓库中。这个过程比较繁琐。

现在,我们可以利用博客仓库下的_config.yml 来覆盖 theme 仓库下的 config。以我使用的 NexT 为例,默认的 Muse scheme 并不和我意,我希望将其修改成 Mist scheme,只需要添加几行配置:

1
2
theme_config:
scheme: Mist

Hexo 5.0.0 之后 [1],我们还可以为每个 theme 创建一个单独的 config 文件来进行管理,文件命名规则为_config.[theme].yml。还是以 NexT 为例,创建_config.next.yml,修改内容为:

1
scheme: Mist

theme 就配置好了。

很奇怪的一点是,目前很多关于 theme 的资料都没有提到 Hexo 已经支持覆盖 theme config,导致我使用了很长一段时间的 Hexo 3.9,却还是在用 fork+submodule 的方法来管理 theme。

下次我将尝试使用 github action 来自动 deploy 我的博客。


2020-07-31 Update

Hexo 5.0.0 支持使用 npm 配置 theme 了。以后不需要手动管理版本,只需要

1
npm i hexo-theme-next --save

目前通过 npm 安装的主题在执行 hexo clean 时会报错,是 Hexo 的已知问题,将在下个版本修复。

更新

此次更新了 post 的命名格式。

方向调整

赵神钦定,这个博客以后只写赵神不会的东西。