PyTorch模型训练经验教训之二

同上一篇,“优化”是指提高训练速度、降低训练时间 ,效果是指模型的准确率。

  1. profile会是你的好朋友。知道时间消耗在哪,是优化的第一步。
  2. 较深的网络,因为梯度过度衰减,拟合速度会很慢,尤其在训练后期。深层网络模块,需要使用残差块连接输入和输出,把梯度传播到后方网络模块。
  3. RNN和它的嫡系兄弟LSTM和GRU的序列长度,是另一个方向的深度,且RNN们不支持在该方向上并行处理。如果不是处理持续增长的序列数据,请考虑TCNAttention等可替代模块。
  4. 理性看待微调预训练模型,你的任务可能不需要那么大规模的模型。但预训练的词向量总是经济实惠的,何况还可以很方便地裁剪和降维。
  5. 一个设计良好的小型模型的效果,远胜设计一般的大型模型。这可能是整篇最重要一条。
  6. 在模型效果不能再提升时,优化几个超参数,再简单微调,也许可以把模型效果再提升一丢丢。
  7. 拟合速度很慢的模型,很可能不是一个设计良好的模型,很难或者不可能取得期望的效果。
  8. 池化层是免费(没有学习参数)的提取主要/平均特征的网络组件。
  9. 文档中的重要信息,可能很不起眼。浏览文档时,往往因为文档上太多不需要的信息,而漏掉重要的信息。
  10. 看起来“错误”的操作,可能并不影响结果。看起来“正确”的操作,可能导致模型效果意外下降。一切以最终结果为准。
Posted in Uncategorized | Leave a comment

PyTorch模型训练经验教训之一

本文为过去近一个月,我跑PyTorch模型训练所获得的经验和教训。

本文所述“优化”是指提高训练速度、降低训练时间 ,效果是指模型的准确率。

  1. 检查每一个环节的输出。如果等训练完成才发现前面的环节有问题,那就白玩了。
  2. 避免重复的、耗时的环节,以预处理缓存替代。
  3. 不要让大批量训练数据耗尽内存。可以考虑利用LMDB等把数据缓存到文件系统。
  4. 先用小批量数据验证速度和效果。
  5. 如果模型效果离你预期的相差太远,尝试把模型加宽、加深,提高学习率。
  6. 降低特征维度、降低模型复杂度是最好的优化方法。
  7. 限制变长序列数据(文本)的最大长度——序列组的最大长度直接决定了矩阵的大小,间接决定了训练所耗内存(显存)和计算量。如果训练一般情况所占内存不多,但突然out of memory,很可能就是这个原因。
  8. 文档所述的各种优化办法,很可能:不被你的硬件支持、运行不起来、没优化效果。但文档不会告诉你这些。事实上,把基本特性玩好,对大多数场景而言都足够了。
  9. 保存每一个epoch的模型状态,下次加载最佳状态继续训练。这个方法的带来利好多多:方便白剽免费的训练资源、可以中断训练以调整参数或补充训练数据、让电脑下班休息……。
  10. 小心for循环和列表生成式,这可能是最耗时的逻辑——尽可能批量处理。如果某场景不支持批量处理——很可能不是不支持,而是自己的知识储备不够。
  11. 提高准确率的更好方法是:找出准确率较低的领域,补充该领域的训练数据;剔除训练数据中有问题的部分。
Posted in Uncategorized | Leave a comment

宜家毕利书柜DIY安装经验和教训

商品:

  1. BILLY 毕利 / OXBERG 奥克伯,书柜,带玻璃门 白色/玻璃 40x30x237 厘米。货号:595.283.49。
  2. FIXA 费克沙,螺丝和螺塞12件套 银色/灰色。货号:403.075.12。

工具:

  1. T型棘轮螺丝刀。
  2. 梯子。
  3. 电锤,带6mm和8mm的钻头。

选购:

  • 毕利书柜零件含上墙固定件,不含上墙螺丝。需要自己根据墙壁类型选购固定螺丝。混凝土墙需要膨胀螺丝。我选择了工作人员介绍的费克沙螺丝。
  • 根据工作人员介绍,TRIXIG 缇克西电动螺丝刀/电钻,无论3.6V,还是12V,都不适合在混凝土墙上打孔。我选择了借用邻居的电锤。
  • 四组595.283.49,市内运费229元,安装费用300多(含上门费一百多)。我选择了DIY安装。房子6楼电梯房,师傅到约定时间送货进门。

安装:

  1. 按照说明书,把书柜(含加高件)组装起来。我把门、隔板的安装,放在上墙固定之后。
  2. 把书柜立起来。书柜会自靠在墙上。
  3. 上墙。这步最难,因为说明书上没介绍具体细节,网上也没经验分享。
    费克沙膨胀螺丝的膨胀管直径8mm、长度39mm,螺丝直径4.5mm、长度55mm。用8mm的钻头,在墙上打出深39mm的孔,再用6mm的钻头把孔加深到55mm。打孔时几个注意事项:a.注意钻头的旋转方向,错误的旋转方向会导致事倍功半,打出的孔也会过大。b.孔一定要有55mm深,即膨胀管和螺丝的最大长度。c.避开墙内的管线。
Posted in Uncategorized | Leave a comment

PaddleNLP安装

PaddleNLP可能是安装体验最差的ML程序了。安装和安装过程中,你可能会遭遇一系列错误。下面挨个介绍。

说明,我的操作系统是Fedora 38和Debian 12。当前PaddleNLP最新版本为:v2.6.1,Paddle最新版本为v2.5.0。下面只包含我当前遇到过的问题。

一、libssl.so.1.1: cannot open shared object file: No such file or directory。

系统没有OpenSSL 1.1。Fedora系统,直接安装openssl1.1即可。Debian系统,需要下载OpenSSL 1.1源码,编译安装。

二、MemoryError: (ResourceExhausted) Fail to alloc memory of XXX size。

不是电脑内存不足。切换到PaddleNLP 2.4.0和Paddle 2.4.2即可。

三、实例代码执行结果与预期不符。比如对“2月8日上午北京冬奥会自由式滑雪女子大跳台决赛中中国选手谷爱凌以188.25分获得金牌!”执行information_extraction信息抽取,结果为空。

切换到PaddleNLP 2.4.0和Paddle 2.4.2即可。

四、PaddleNLP和Paddle所依赖的protobuf版本冲突。

PaddleNLP 2.4.0和Paddle 2.4.2依赖的protobuf版本不冲突。或者可以查下PaddleNLP和Paddle各个tag下requirements.txt要求的protobuf版本。

五、pip提示:ERROR: Could not find a version that satisfies the requirement paddlepaddle==x.y.z。

换个Python版本。 我是换到Python 3.10。原因还未知。如果是Fedora系统,直接安装python3.10即可。如果是debian系统,需要下载Python 3.10源码,编译安装。

六、编译Python3.10时,make test报错:_ctypes.c:107:10: fatal error: ffi.h: No such file or directory。

安装libffi-dev后,重新编译。

七、Python3.10报错:ModuleNotFoundError: No module named ‘_bz2’。

安装libbz2-dev后,重新编译安装OpenSSL3.10。

八、Python3.10报错:ModuleNotFoundError: No module named ‘_lzma’。

安装liblzma-dev后,重新编译安装OpenSSL3.10。

综上所述,建议直接按下面的办法安装:

直接在Python3.10上安装PaddleNLP 2.4.0和Paddle 2.4.2。如果需要编译安装Python3.10,先安装依赖。依赖清单的完整版本:build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev liblzma-dev。

Posted in Uncategorized | Tagged , | Leave a comment

一种从客观数据到代码实现的编程思想

这套编程思想,从我日常工作中总结得来,用于解决一些复杂业务问题。内容偏抽象,表达能力有限,不喜请绕道。

一、调研客观数据,放弃主观思考。

以往解决问题,往往基于认知和思考。认知是有限的,主观思考易产生偏见。

解决问题,就从问题本身出发,尽可能搜集问题的表现数据,找出数据特征和规律。最后,程序要的做,就是把数据的特征和规律描述出来。

二、程序是数据的处理过程。

所有程序,最顶层的抽象,都是数据的“输入 – 处理 – 输出”。如下图所示:

实现中,中间的“处理”过程,是程序的主要复杂点。“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。对于复杂的处理,一个中间层不够,那就多个,于是可以得到:

在上面的模型中,每层都封装了数据。一层的数据,转换为下一层数据的过程,就是逻辑。

下面是一个虚构的商品请求程序模型:

这个商品请求程序模型,存在五层数据。第一层的数据是“商品请求数据”,通过逻辑“查询商品”,转换为第二层的数据“商品基础数据”……

程序也可以描述为:输入数据,经过多层转换处理,最终得到输出数据的过程。

三、数据模型和关联。

数据(模型/结构)由属性/字段组成,不带操作/逻辑(逻辑被视为数据的转换过程)。每层有一到多个/块数据。单层内的数据之前可能存在关联,这种关联在代码上表示为数据间的组合。

下面是一组虚拟的用户和钱包数据模型:

上面的数据模型,如果用代码来实现,可能是下面这样的:

type Wallet struct{
    Currency string
    Balance  string
}

type User struct {
    NickName string
    Wallet   *Wallet
    Credit   int
}

四、完整的可实现的程序模型。

一个程序模型示例(省略了数据模型细节):

在上面的示例模型中:存在多道程序逻辑(数据转换逻辑),模块间的边界清晰,部分数据和逻辑被复用,部分数据被复制而不是转换到下一层(实际代码中可能什么也没干)。

下面是一个虚拟的下单请求程序示例(只有一道程序逻辑):

最后要做的,就是用代码,把这个程序模型描述出来。

下面是一个真实案例的模型(省略了大量细节):

上面真实案例模型,是一个邮件发送程序,包含一个邮件发送的通用框架,这个框架支持不同业务(图中的1、2、3)的邮件。

Posted in Uncategorized | Tagged | Leave a comment

新的Go语言缓存中间件:restcache

我开发的第一套缓存中间件是cachex。cachex支持自定义存储后端,实现了哨兵机制,自带lru存储,支持系统故障时使用过期缓存数据。

cachex的主要问题是不支持批量处理。这也导致在上家公司,cachex未能上阵。我在上家三年时间一直不能开发出支持批量处理的缓存中间件,我也写了三年业务耦合的缓存处理代码。

这次的restcachefastrest组件库的一个组件。在写restcache之前,我创建了gox项目,实现了优化版的lru,和一套完整的支持批量处理的哨兵机制,然后再实现restcache就简单多了。解决一个复杂问题的一个有效方法,就是模块拆分。

一套完整的缓存,应该包括三个模块:存储、查询、中间件。三个名字是我取的,不准确。

  • 存储是存储缓存数据的部分,也可以称为后端,一般是redis客户端,或者进程LRU存储。存储逻辑都是透明处理业务数据,也就是存储逻辑是通用的,不用对每个业务逻辑写一套存储代码。
  • 查询是存储查不到目标数据时,提供新缓存数据的部分,一般是从MySQL等持久化数据库或者服务接口查询。很遗憾,查询逻辑是耦合业务数据的,需要对每个缓存业务写一套定制的查询代码。
  • 中间件是响应用户查询,调用存储和查询的胶水逻辑,也就是实现了典型的缓存流程。这个也是通用的。

cachex和restcache实现的就是缓存的中间件,并定义了存储和查询的接口。restcache的用户需要自己实现存储和查询。restcache已经附带了一套lrucache存储,可以直接用。但因为用户的redis库版本会不同,restcache没有实现redis存储。

典型的缓存流程如下:

  • 先查存储。如果找到,返回结果,结束;如果没找到,可能是因为没存或者已经过期,继续下一步。
  • 调用查询,查到新缓存数据,保存到存储,返回结果,结束;如果没查到,返回notfound,结束。
  • 下次查存储,查到上一步保存的新缓存数据。糟糕的情况是notfound,会重复第一、二步。

应该绝大多数缓存都是这个流程。这个流程的主要麻烦有两个:

  • 缓存失效风暴,即大面积缓存失效。缓存本意是避免执行大量重复的低效的查询,但大面积缓存实效会降低缓存的意义,严重的会导致后面的服务失去服务能力。
  • 批量查询。实际场景中,很多列表的查询。批量处理,会让流程中的每个环节变复杂。

解决方案为:

  • 对于缓存实效风暴,cachex和restcache都用上了哨兵机制。我实现的叫SentinelGroup,Go团队实现了一套singleflight。SentinelGroup不见得写得比singleflight好,但强在支持批量处理。两者核心逻辑都是一致,多个过程需要同时取得相同的结果,只放行一个去做生产者,其它作为消费者等待生产者分享结果。
  • 对于批量查询,这是cachex无法实现,但restcache实现了的特性。重复下:解决一个复杂问题的一个有效方法,就是模块拆分。一个复杂模块,由几个小模块简单地构成,每个小模块都封装了一部分复杂逻辑。然后小模块又可以继续拆分为几个更小的模块。cachex的失败,在于没划分清楚缓存和哨兵机制的边界,没能定义出清晰简单的批量存储接口,导致单个模块过于复杂。解决了实现的复杂性,简单来说,缓存的批量处理,就是要从不同的来源查询数据,再把不同来源的数据组装起来。第一步需要从存储和查询中拿到数据,第二步需要从哨兵机制的生产者和消费者拿到数据。因为这两步的边界足够清楚,也就降低了整体的复杂度。

restcache刚刚开发出来,虽然我写了很多单元测试,但经过项目的考验才能成为一个成熟稳定的组件。

Posted in Uncategorized | Tagged | Leave a comment

reloader:Go服务热升级支持

以前我在公司写过一个简单的Go热升级支持程序。但是工作时间写的,领导不准开源。后我想另写一个开源版本,但一直没想到解决多端口的问题。后来就慢慢忘却了。

直到最近一次偶然的机会,这个程序被人重新提起。又遇上待业期,正需要写点东西打发时间。突然灵感来了,想到了多端口的解决方案。

于是有了reloader

思路基本是之前版本的扩展版。一个master进程,多个worker进程,一个worker进程处理一个侦听器。这样可以实现多端口的动态侦听和关闭。我又做了些进程管理的优化,推送到github。

无法否认,我的解决方案存在很多问题,因为偷偷起了多进程。比如程序在所有侦听器和连接都关闭后需要上层逻辑结束,这对于单进程无碍,对于reloader却是可能多了N个垃圾worker进程。这些问题,得等待找到更好的解决方案。

Posted in Uncategorized | Tagged | Leave a comment

重读《黔之驴》

今天计划做点无聊的事。突然想起柳宗元的《黔之驴》。原文如下:

黔無驢,有好事者,船載以入;至則無可用,放之山下。虎見之,龐然大物也,以為神。蔽林間窺之,稍出近之,憖憖然莫相知。他日,驢一鳴,虎大駭遠遁,以為且噬已也,甚恐!然往來視之,覺無異能者,益習其聲,又近出前后,終不敢搏。稍近益狎,蕩倚衝冒。驢不勝怒,蹄之。虎因喜曰:「技止此耳!」因跳踉大闞,斷其喉,盡其肉,乃去。

噫!形之龐也,類有德;聲之宏也,類有能。向不出其技,虎雖猛,疑畏卒不敢取,今若是焉,悲夫!

大意是说:黔地没有驴,一个无聊人运了头驴过去。黔地老虎没见过驴,开始很害怕,后发现驴不过“技止此耳”,跳上去,吃了驴。

故事创作于作者永贞革新失败之后。后人一致认为,作者是在讽刺朝中外强中干的权贵。

以我今天的眼光来看,故事却是这样的:一个很有想法的人,给一个没有驴子的地方运了头驴。驴新来,遭到当地传统势力的敌视,最终不敌,被吃了。这个很有想法的人,就是指作者自己,驴指永贞革新,老虎指当时朝中掌控实权的权贵和地方的藩镇。

我们今天谋求创新,就要做很多看似无聊的尝试,敢于承担失败的风险。想法的落地,则需要短时间适应现实情况,否则很快夭折。即使最终项目失败了,也未必意味着想法是错误的。现在黔地已经没有老虎,留下的是驴。

Posted in Uncategorised | Leave a comment

Python实现数据类的方法集合

当前Python最新稳定版为3.6。主流Linux发行版官方仓库提供Python版本的有2.7和3.5,默认依然是坚强的2.7。

1、dataclass装饰器。正规军,需要Python 3.7+,还在路上。dataclass装饰器会给类添加__init__、__str__、__repr__等方法。

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

2、collections.namedtupletyping.NamedTuple。要求不多的用这两个最合适了。Python 3.6+的typing.NamedTuple才支持默认参数。

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3
Employee = namedtuple('Employee', 'name, age, title, department, paygrade')

3、autoassign装饰器,和auto_assign装饰器。算是奇技淫巧,前者支持Python2,后者支持Python 3.5+。

4、collections.namedtuple + functools.partial。通过partial实现默认参数,也是我当前采用的方案

fields = ("name", "age", "title")
defaults = OrderedDict({"title": None})
Employee = partial(namedtuple("Employee", fields), **defaults)
employee = Employee("Guido", "18")

网上还有一些相对麻烦点的方案,比如重写namedtuple的__new__方法,就不一一列举了

Posted in Uncategorised | Tagged | Leave a comment

在Linux Minecraft中输入中文——进阶:在告示牌输入中文

两年前的一篇文章,我讲到如何在Linux Minecraft中输入中文。脚本内容是转的别人的。该方法只能用于聊天,不能用于编辑告示牌。

其实只要把脚本修改下,就可以通用。只会复制粘贴,不会派生定制,做程序员的我真的不好意思。

原脚本内容为:

#!/bin/bash -e

chars=$(zenity --title 中文输入 --text 中文输入 --width 500 --entry 2>/dev/null)
sleep 0.1
xdotool key --delay 150 Escape t
sleep 0.2
xdotool type --delay 150 "$chars"
xdotool key Return

其中正文第一行为弹出对话框,接收用户输入。第三行输出t键。第五行输出对话框接收的输入。第六行输出回车。很明显,第一行和第五行才是重点。其它几行不过辅助性的,帮用户偷懒。

现在只要改下:

#!/bin/bash -e
 
chars=$(zenity --title 中文输入 --text 中文输入 --width 500 --entry 2>/dev/null)
#sleep 0.1
#xdotool key --delay 150 Escape t
sleep 0.2
xdotool type --delay 150 "$chars"
#xdotool key Return

只要注释掉辅助性的几行。

编辑告示牌时,通过快捷键执行该脚本,即可在告示牌上输入中文。如果是聊天,得先输入t打开聊天栏,再通过快捷键执行该脚本,聊天编辑栏得到中文后,敲回车——或者新脚本和旧脚本共存,绑定到不同的快捷键。

以前我在游戏中结识位朋友。他发现另一个在告示牌输入中文的办法。可惜我还没学会,就与他失去了联系。

Posted in Uncategorised | Tagged | Leave a comment