博客
存放一些博客性的内容,大部分学习性内容见 笔记,但什么才是博客呢?
存放一些博客性的内容,大部分学习性内容见 笔记,但什么才是博客呢?
起初他们追杀共产主义者的时候,
我没有说话———因为我不是共产主义者;
接着他们追杀犹太人的时候,
我没有说话———因为我不是犹太人;
后来他们追杀工会成员的时候,
我没有说话———因为我不是工会成员;
此后他们追杀天主教徒的时候,
我没有说话———因为我是新教教徒;
最后他们奔我而来,
那时已经没有人能为我说话了。
近期由于一些事情的影响,一直倾向于保持中立的我开始动摇了(或许从很早就开始动摇了,但近期的影响比较大)。一是我很喜欢的游戏《无期迷途》的节奏,二是重庆胖猫跳江事件,以及对一些其他事情的思考,如以色列对巴勒斯坦的屠杀等。
《无期迷途》曾经是个好游戏,制作可以称得上精良,尤其是一周年繁花前后的剧情更是二游教科书式的典范。无期作为一款男性向游戏凭借媚男发家,男玩家也一直是氪金的中坚力量,然而极端女权在女厕(微博)大肆带节奏,致使一些角色的立绘修改甚至下架,这甚至导致官方后期微博治游,小仙女更是爆出充了三张月卡、脚刹男性的可笑言论。随着流水的日渐高涨,游戏质量开始下滑、主线几个月不更新、语音不添加、反馈装死、逼氪、甚至开始逐渐变为女性向游戏,普通玩家的权益开始得不到保障。玩家也意识到必须站队开冲才能保障自己的权益,直到昨天官方群管理带头冲锋,官方凌晨才发布了道歉信,然而还是偏向集美们,因此我不得不卸载这个游戏,不得不被迫站队,后续各类冲锋玩家也相继放弃。
重庆胖猫跳江事件则更是重量级,塔下场定性为正常恋爱,胖猫姐涉嫌造谣,整篇通告看得我心头一凉,的确是句句属实,可是避重就轻春秋笔法,这也引起了大量网友和捞女们的进一步对立。不同于二游的小圈子,男女对立是个庞大的圈子,与每个男性都息息相关,各类彩礼事件和不公平对待事件都表明的塔的偏向性,因此也被戏称为比萨斜塔。因此我们需要站队,只有声音足够大塔才不能装死。
你可以对政治毫无兴趣,可你必须确信: 政治对你却太有兴趣,政治绝不会因为你对政治不关心而放过你。所以,假如你不想作戏台上一具可怜的傀僵,给别人随心所欲地推来推去,你就别无选择,必须关心政治。这其实就是关心你自己。 ——《新华日报》1945 年 9 月 11 日。
马克思说过,社会是人类关系的总和,现代社会想要独善其身几乎成为不可能的事情。暂时性地选择中立或许是一种自我保护,让我们免于争议与冲突,但是长期的逃避会让我们失去改变和影响社会的能力,或者说让我们忽略我们曾拥有这种能力。正如开篇所描述的,等到事情无法挽回的地步,才发现自己早已无法发声。个人的选择和行为确是微不足道,但都在以某种方式塑造世界。长期的中立或许会导致麻木,因此我认为即使是对外表现为中立,内心也必须站队,明确自己的底线,才能维护自己和相似的人应有的权益不被蚕食。
站队并不与集体主义直接相关,集体主义强调团队利益高于个人利益,而站队则主要是为了维护个人利益,在这过程中可能会形成一个暂时的集体。个人主义者也可以通过站队的力量来不断接近自己最终的目标。
从某种程度上站队可以认为是一种加速主义,单个人的力量是有限的,但是可以通过站队来聚集力量,从而加速某些变化。这当然存在一定的风险,譬如微博已经沦为女厕,
是非对错往往是个人主观的判断。
配置系统是很多深度学习套件和算法库的重要组件,一个优秀的配置系统可以方便用户修改训练所需的超参数、管理实验并且增强项目可读性等。配置系统不光可以用于大型的算法库,也可以用于个人进行快速实验和迭代。然而当前深度学习社区的配置系统都或多或少存在一些 不够方便 的地方,本文将会介绍一些已有的配置系统,并尝试对其进行改进。
YACS 是一个轻量级的配置系统,Detectron 和 maskrcnn-benchmark 便是使用的 YACS,其使用可读性较好的 YAML 文件,格式为:
MODEL:
TYPE: mask_rcnn
CONV_BODY: FPN.add_fpn_ResNet50_conv5_body
NUM_CLASSES: 81
FASTER_RCNN: True
MASK_ON: True
该配置的第二行的 TYPE 为所要实例化的类,解析配置文件时若某个字典中包含 TYPE,则表示应该将其实例化。
OpenMMLab 系列的算法库提供了注册器 (Registry) 来维护了一个字符串到 类或函数的全局映射,这样便可以更加方便的通过字符串寻找到对应的类,使用方式为:
import torch.nn as nn
from mmengine import Registry
ACTIVATION = Registry('activation')
# 使用注册器管理模块
@ACTIVATION.register_module()
class Sigmoid(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
print('call Sigmoid.forward')
return x
然而随着 codebase 规模的不断增大,每次运行时会引入额外的注册开销和无关依赖。
OpenMMLab 系列的算法库支持 YAML,JSON 和 python 作为配置文件,其中 json 和 python 的可读性奇差:
train_pipeline = [
dict(type='LoadImageFromFile', backend_args={{_base_.backend_args}}),
dict(type='LoadAnnotations', with_bbox=True, with_mask=True),
dict(type='RandomFlip', prob=0.5),
dict(
type='RandomChoice',
transforms=[[
dict(
type='RandomChoiceResize',
scales=[(480, 1333), (512, 1333), (544, 1333), (576, 1333),
(608, 1333), (640, 1333), (672, 1333), (704, 1333),
(736, 1333), (768, 1333), (800, 1333)],
keep_ratio=True)
],
[
dict(
type='RandomChoiceResize',
scales=[(400, 1333), (500, 1333), (600, 1333)],
keep_ratio=True),
dict(
type='RandomCrop',
crop_type='absolute_range',
crop_size=(384, 600),
allow_negative_crop=True),
]]),
dict(type='PackDetInputs')
]
PaddleDectetion 的配置系统较为复杂且精妙,其只维护了一 个全局的注册字典,配置文件格式如下:
metric: COCO
num_classes: 80
TrainDataset:
!COCODataSet
image_dir: train2017
anno_path: annotations/instances_train2017.json
dataset_dir: dataset/coco
data_fields: ['image', 'gt_bbox', 'gt_class', 'is_crowd']
不同于前面两种配置系统使用 type,PaddleDetection 使用 yaml 的 representer 实现通过 !{} 来获取 python 对象。此外 PaddleDectection 在实现类时添加了类属性—— __shared__ 和 __inject__,如:
from ppdet.core.workspace import register
@register
class BBoxPostProcess(object):
__shared__ = ['num_classes']
__inject__ = ['decode', 'nms']
def __init__(self, num_classes=80, decode=None, nms=None):
# 省略内容
pass
def __call__(self, head_out, rois, im_shape, scale_factor):
# 省略内容
pass
__shared__ 表示这些参数是全局共享的,__inject__ 表示这些参数是全局字典中已经封装好的模块,即实现了配置文件的嵌套,可谓十分之精妙了。
然而该配置系统过于定制化,不能很好的适应各种情况。
PaddleSeg 中的配置格式并无特别之处,与前文相同,但是其解析、检查和构建对象都对进行了 硬编码,这样的好处是编写代码时有补全。
由于 YAML 和 PYTHON 配置文件一直被诟病不能跳转和补全,尤其是 PYTHON,可读性更是一坨。detectron2 提出了
LazyConfig,形式如:
import torch.nn as nnfrom detectron2.config import LazyCall, instantiate
# 通过LazyCall创建一个config对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
conv = instantiate(conv_config)
这里的 conv_config 将类对象和实例化所需参数都存储起来,等到需要实例化时才进行实例化,因此叫做 Lazy。这样编写配置文件时可以为对应的类提供补全和跳转,并且通过该方式可以直接舍弃注册器,可谓是一箭双雕。
然而该配置系统不能对类的参数进行补全和检查。
同样为了支持跳转和补全,OpenMMLab 提供了新式的 python 配置文件——即将 type 后的字符串变为类,可读性仍然是一坨,远没有 detectron2 优雅。
为了一定程度上解决上述问题,我开发了 ExCore,主要用于个人的实验。ExCore 使用 TOML 作为配置文件,并且借助 LSP(language server protocol)的力量在一定程度上弥补了配置文件和 python 文件之间的鸿沟——即为 TOML 配置文件提供了补全、跳转、查看文档、参数检查等功能,先看几个动图再介绍。
补全,查看文档和参数检查:

可以看到会有类名及其参数的补全以及查看对应类的 docstring,并且会对 required 参数进行提示。
跳转:

跳转以插件的形式实现,目前仅支持 NeoVim,见 excore.nvim
本配置系统的核心前提是将配置文件中所要创建的对象分为三类—— 主要、 中间 和 孤立 对象。
主要 对象是指在训练中 直接 使用的对象,如模型、优化器等。ExCore 会创建并返回这些对象。中间 对象是指在训练中 间接 使用的对象,如模型的主干、将要传入优化器的模型参数。孤立 对象是指 python 内建对象,会在读取配置文件时直接解析,如 int, string, list, dict 等ExCore 扩展了 toml 文件的语法,引入了一些特殊的前缀字符 —— !, @, $ 和 & 以简化配置文件的定义过程,因此配置文件格式为:
size = 224
[TrainData.ImageNet]
&train_size = "size"
!transforms = ['RandomResize', 'Pad']
data_path = 'xxx'
[Transform.Pad]
&pad_size = "size"
[TestData.ImageNet]
!transforms = 'Normalize'
&test_size = "size"
data_path = 'xxx'
这里的 TrainData,TestData 是一个字典,其元素都是上文所提到的主要对象,可以称之为 主要 模块,主要 模块需要定义为 [PrimaryFields.ModuleName]. PrimaryFields 是一些预先定义的字段, 这里即是 TrainData, TestData, ModuleName 即为注册的名称, 这里即为 ImageNet 和 Pad. 如此便可以去除 type。
第四行的 & 表示引用,即 train_size 的值最终会被解析为 224。
第五行的 ! 表示该参数并不是 python built-in 对象,即类似于配置嵌套,注意这里的 RandomResize, 如果其没有参数,则可以不在配置文件中声明。等价的 yaml 文件为:
TrainData:
type: ImageNet
train_size: 224
data_path: 'xxx'
transforms:
- type: RandomResize
pad_size: 224
- type: Pad
TestData:
type: ImageNet
test_size: 224
data_path: 'xxx'
transforms:
- type: Normalize
此外 @ 用于表示共享模块,如:
[Model.FCN]
@backbone = "ResNet"
[Model.SegNet]
@backbone = "ResNet"
[ResNet]
layers = 50
in_channel = 3
这里的 FCN 和 SegNet 的 bacbone 将会是同一个对象。
$ 则用于表示不需要实例化,这是一个类,如:
[Model.ResNet]
$block = "BasicBlock"
layers = 50
in_channel = 3
该功能上文介绍的注册系统是无法实现的。
$ 除了用于参数名之前,还可以用于参数值之前,后跟 PrimaryFields,如:
__base__ = ["./block.toml"]
[Model.ResNet]
!block="$Block"
该功能用于跨文件使用,即会在所有加载的配置文件中找到 Block 字段,将其值传入。
同样为了方便使用,本配置系统提供了一套参数级别的 HOOk,如最常见的将模型的参数传入优化器,格式为:
[Optimizer.AdamW]
@params = "$Model.parameters()"
weight_decay = 0.01
这也是上文所有配置系统所做不到的。
如果你想调用类方法或静态方法,可以:
[Model.XXX]
$backbone = "A.from_pretained()"
也可以获取属性,就像写 python 一样即可,但是不可传入参数:
[Model.XXX]
@channel = "$Block.last_conv.out_channels"
借助该功能我们可以注册模块,如:
[Model.ResNet]
$activation = "torch.nn.ReLU"
更复杂的情况可以继承 ExCore 提供的 hook 基类:
[Optimizer.SGD]
@params = "$Model@BnWeightDecayHook"
lr = 0.05
momentum = 0.9
weight_decay = 0.0001
[ConfigHook.BnWeightDecayHook]
weight_decay = 0.0001
bn_weight_decay = false
enabled = true
使用 @BnWeightDecayHook 来将 $Model 传入 hook 中
Hook 类定义:
from excore.engine.hook import ConfigArgumentHook
from . import HOOKS
@HOOKS.register()
class BnWeightDecayHook(ConfigArgumentHook):
def __init__(self, node, enabled: bool, bn_weight_decay: bool, weight_decay: float):
super().__init__(node, enabled)
self.bn_weight_decay = bn_weight_decay
self.weight_decay = weight_decay
def hook(self):
model = self.node()
if self.bn_weight_decay:
optim_params = model.parameters()
else:
p_bn = [p for n, p in model.named_parameters() if "bn" in n]
p_non_bn = [p for n, p in model.named_parameters() if "bn" not in n]
optim_params = [
{"params": p_bn, "weight_decay": 0},
{"params": p_non_bn, "weight_decay": self.weight_decay},
]
return optim_params
实际上是利用 json_schema 曲线救国,ExCore 对所有注册的类进行静态分析和类型推断,因此对 type-hinting 的要求比较高,如果没有编写 type-hinting,则会根据默认值进行推断。将结果缓存在本地,由 TOML 对应的 LSP 完成该功能。
在上一步静态分析的过程中,将类名及其文件地址缓存在本地,针对不同的编辑器实现插件。
不同于上述的配置系统需要额外的构建系统,ExCore 仅仅提供两个 API 用于加载、解析配置文件以及实例化:
from excore import config
lazy_cfg = config.load('xxx.toml')
modules, run_info = config.build_all(lazy_cfg)
这里的 modules 是一个 wrapper,可以通过预先定义的主要字段来获取构建的模块,如要获得 Model,只需要:
model = modules.Model
但是目前还不支持补全,后续可以根据主要字段自动生成一个类专门用作 type-hinting,例如:
class ModuleWrapper:
Model: Any
modules: ModuleWrapper
model = modules.Model
run_info 中存储的则是之前提到的 孤立对象,是一个字典。
ExCore 同样支持 python 形式的配置文件,与 detectron2 基本一致,不过实例化时只需调用 __call__ 方法即可,如下:
from excore.config.model import ModuleNode
from xxx import Module
cfg = ModuleNode(Module).add_params(arg1=1, arg2=2)
model = cfg() # support instantiate recursively
# or
model = ModuleNode(Module)(arg1=1, arg2=2)
实际上上文中 lazy_config = config.load('xxx.toml') 就是将配置文件解析为了一系列 LazyConfig 对象,可以对其进行一些操作(如通过命令行替换参数),再使用 build_all 进行实例化。
Registry 在当今看来并不是一个很好的设计,但出于某些考虑和原因,我实现了一种我称之为 LazyRegistry 的注册机制,同样能够避免冗余的注册和依赖。ExCore 中的 Registry 更多起的是一种标记的作用,用于实现配置文件的补全、提示和跳转。后面有空再详细介绍。
从年后发现被魔改的 DWM 到现在已经有半年多了,Linux 也成为了我日常使用的主力,但是实际使用过程中还是有一些不太舒服的地方,管中窥豹,这也是当前大部分平铺式窗口管理器的痛点。
DWM 是一个轻量级的平铺式窗口管理器,源码只有两三千行,专为 X Window System 设计(然而 X 日渐式微,终归会被 wayland 取代),可以动态地全自动平铺窗口,即所有的窗口都不重叠。并拥有大量的快捷键,可以只通过键盘在不同的窗口、工作区(workspace)之间移动、缩放窗口、在屏幕内移动窗口、移动窗口到其他工作区等。
除了平铺,DWM 同样支持浮动窗口,可以将窗口从平铺中释放出来,恢复堆叠式的窗口管理。
Windows 和 MacOS 都是基于堆叠的窗口管理,其操作逻辑简单,主要交互工具为鼠标,可以进行很细致的缩放和移动,但是堆叠的特性代表这窗口之间经常会相互遮挡,这阻碍了信息获取的流畅度,所以现在这类窗口管理器也都支持转换为平铺式的窗口,用于快速无遮挡地获取和对比信息。
总结一下其可能的缺陷(是否是缺陷还是要看具体的使用需求):
而平铺式窗口可以一定程度上解决这些问题,其有以下优点: