一只小狐狸带你解锁 炼丹术&NLP 秘籍
在合格的炼丹师面前,python可能被各种嫌弃
前不久卖萌屋的lulu写了一篇vim的分享《算法工程师的效率神器——vim篇》,突然想起来自己也有一篇攒了几年灰的稿子,在小伙伴的怂恿下跟小夕强行翻新了一下,于是有了本文。
已经习惯了IDE的小伙伴可能在炼丹时也惯性的使用pycharm了。这在windows、mac本地炼丹的时候往往没什么,但是一旦要在GPU server端,初学者就往往开始捉急了,更不必说让他all in这个server端的小黑框了。
小夕早期也试过一些笨办法和部分场景下有效的“巧办法”,比如
本地端用pycharm疯狂debug,一切测试ready后,使用scp将相关代码传到服务器起任务
通过syncthing这种同步工具来自动同步本地端和server端代码
利用pycharm内置的远端同步功能
server端启动jupyter notebook,本地端浏览器访问
直到遇到了公司跳板机:scp、pycharm、jupyter notebook等通通失联,本地端与远程GPU服务器被跳板机生生拆散了。于是我问同组的小伙伴这该怎么炼丹,ta说:
“咦?直接在服务器上写就好了呀,linux不香么,vim不香么?难道你不会?”
“我。。我会。。。一点╮( ̄▽ ̄"")╭”
回想自己的炼丹之路,其实完全断奶IDE确实不是一件容易的事情。早期一直是用pycharm连接远程GPU服务器来凑合,一直觉得效率还行。直到后来投身工业界后,发现身边的大佬都是在黑框框里裸奔,炼丹效率完全秒杀我,这才下决心彻底断奶。挣扎几个月断奶成功后,愈发觉得
黑框框真香╮(╯▽╰)╭
不过,毕竟自己不是专业玩linux的,只是炼丹之余摸索了一些自己用着舒服的不成体系的方法,所以相关玩法可能会有些“歪门邪道”,如有更优做法,请大佬们在评论区不吝赐教噢~
本文目录:
准备工作与环境迁移问题
与机器解耦
别忘写个自动配置脚本
bash function用起来
与bettertouchtool的组合技
别再无脑vim了
别再无脑python了
(其实还能写更多,太困了,还是下次吧QAQ
准备工作与环境迁移问题
其实,如果只是凑合一下,那么在黑框里炼丹基本不需要什么准备工作。换到新的服务器之后,基本就是vimrc里随手写几条tab扩展、换行自动缩进、文件编码之类的,bash里随手建几条alias就可以了,剩下就是一顿pip install。
不过,如果你是一个比较深度的炼丹师,往往python环境里存了一些自己得心应手的小工具包,还有一些自己临时hack了源码的库;可能还要随身携带hadoop客户端以便在集群上炼丹和管理实验;还可能对cuda版本、cudnn版本也有一些要求,需要随身携带一些常用版本在服务器之间穿梭;此外,更不用说vim和bash的一堆个性化配置啦。
这就导致了一个问题,如果炼丹准备阶段,丝毫不顾及炼丹环境与机器的解耦和环境的迁移性,那么一旦机器挂掉,或者你要换机器(老板给你买了新机器),这就会变得非常痛苦。所以在打磨自己的工具链之后,花点时间将工具与环境解耦是非常必要的。
与机器解耦
比较欣慰的是,炼丹依赖的大部分工具和库本身就是跟机器解耦的,比如cuda、cudnn、hadoop client等,挪到新环境后,基本只需要把各bin的路径配置到环境变量PATH
里,把各lib配置到LD_LIBRARY_PATH
里就可以用了,最多再export一下CUDA_HOME
,HADOOP_HOME
等。然而,python环境的迁移就相对麻烦一些了。
早期的时候,小夕在本地搞了一堆miniconda的环境,然而换机器的时候才发现迁移时这些环境非常容易损坏,或者迁移后发现有时出现奇奇怪怪的问题。当时一顿乱试,也没能很完美的解决,不知道今天miniconda或anaconda是否有改善(本地端的体验真的无敌赞)。
此外,也尝试过用docker管理环境。虽然docker的迁移性很赞,自带云端同步,但是,当你面对一台无法连接外网且没有安装docker的服务器时,你就感觉到绝望了。。。一顿离线安装折腾到疯,机器环境差点时,各种安装依赖会搞到怀疑人生。此外,docker也有一定的上手成本,理解概念、熟悉常用命令和提高生产力的命令方面会比miniconda明显成本高。
最终发现,python环境的迁移性方面还是pyenv+virtualenv的组合能应对的情况更多一些,使用virtualenv建好一个环境后,若要将这个环境迁移到其他机器上,分两种情况:
如果你没有修改某些库的源码,所有的库都是pip install的,那自然直接pip freeze
一下,将当前环境里的各个库和版本号直接输出重定向到命名为requirements.txt
文件中就可以啦。到新的机器的新环境里,直接pip install -r requirements.txt
就哦了。
然而,如果你已经深度定制了某些库,这就意味着你需要把整个离线环境打包带走了。这时首先要保证目标机器跟现有机器的大环境差不多(否则一些系统底层库版本差异很大,可能导致上层的库调用出错),然后借助virtualenv自带的命令就非常容易了,直接一行
virtualenv -relocatable
就可以将环境里绝对路径改成相对路径,从而完成跟当前机器的解耦。
不过亲测对有的包解耦不彻底,尤其是ipython这种有bin文件的。这就需要手动的将环境里bin目录下的这些bin文件首行的解释器路径改成相对的(比如直接/bin/env python
)。最后就是改改bin目录下的activate系列文件中的绝对路径啦。至此就可以放心的带着小环境穿梭在各大机器了,且无需考虑目标机器是不是离线机器。
别忘写个自动配置脚本
上一节将python环境,cuda、cudnn、nccl、hadoop等与机器解耦后,就可以再带上vimrc
,bashrc
, inputrc
等文件一起跑路了。这时候就发现bash script真香了,把你的配置流程直接写进脚本里,这样到了新环境直接run一下这个脚本,你的全套家当就搬过来啦。
懒人的alias
说完了环境准备和搬家的事情,然后就可以开始谈谈怎么偷懒了。
虽然精力旺盛的时候,多敲几个字母也没什么,燃鹅,当你深夜面对实验结果甚为沮丧时,你就会发现敲个nvidia-smi
都是那么无力。所以,干脆就提前把懒提前偷一下。
贴一个自己的常用懒人alias,可以考虑写入到你的~/.bashrc
function enshort() {alias l='ls -a'alias d='du -h --max-depth 1' alias n='nvidia-smi'alias vb='vi ~/.bashrc'alias sb='. ~/.bashrc'alias sc='screen'alias nvfind='fuser -v /dev/nvidia*'alias nving='watch -n 0.1 nvidia-smi'
}
不过需要强调一下,如果多人共用机器,记得像上面一样把你的私人alias封到函数里,以免与他人命令冲突,造成不便。为了进一步偷个懒,你可以把这个函数的启动封装在virtualenv的启动脚本里,或者在外面再封一层更高level的环境启动函数。这样就避免了每次登陆都要手动启动一堆函数的麻烦。
bash function用起来
虽然对算法工程师来说,bash一般用来写一些非常高level的流程,大多数只是顺序执行一堆命令外加写几个分支,不过,巧用bash中的function来封装一些高频的操作往往也可以大大提升日常炼丹效率。
例如,平时炼丹实验挂一堆之后,我们可能时不时想看看某个实验的cpu利用率,内存占用等,每次用ps
一顿操作都要敲不少东西,于是机智的小夕还是选择封装一个小function
# Usage: psfind <keywords> (RegExp supported)
function psfind() {ps aux | head -n 1ps aux | grep -E $1 | grep -v grep
}
这个函数既支持psfind <进程号>
,也同样支持匹配进程中的关键词,且支持正则表达式来支持复杂查找。最主要还是格式化了一下输出,这样打印出来会看起来很清晰。
觉得好用的话欢迎拿走(记得留个赞
与bettertouchtool的组合技
提高linux环境中的炼丹效率,除了上述这种在.bashrc
中作文章,mac用户还可以通过一个众所周知的神器APPbettertouchtool
来组合提升日常效率。大部分mac用户可能只是用它来强化触控板手势了,其实它的键位映射也是炒鸡无敌有用的鸭~
例如,众所周知,在命令行里或者vim的插入模式下,我们可以通过ctrl+w
来向前删除一个单词,这个命令虽然无敌有用,但是早已习惯了ctrl/option/cmd+delete
的我们还是会觉得这个设定比较让人精分,导致影响了它的实用性。不过,用btt完全可以把这个ctrl+w
这个键位映射到我们习惯的option+delete
来删除一个单词(mac系统中大都支持opt+del
删除一个单词的设定),如下图
当然了,如果你用的不是mac自带的终端,而是iterm等第三方的话,也可以考虑通过内置的键位映射功能来完全这个操作~或者,就适应一下ctrl+w
的设定啦╮( ̄▽ ̄"")╭
别再无脑vim了
linux新手在去查看一个(文本)数据集或日志文件时,基本就是直接用vim打开后开始上下左右的看。其实,对NLPer来说,相当多的场景是无需反复去vim的,有时反而会更加问题的复杂度,或阅读体验更糟。
先说说最简单的,关于随便瞥一眼数据集的开头。
当数据集比较大时,哪怕是用sublime、vim这种高性能、轻量级文本编辑器打开,都要等待很久很久(虽然vim可以ctrl+c打断),超大单文件数据集更是噩梦了。其实只是瞥一眼的话,完全可以使用head命令来快速取出一个文件的前多少行,比如
head -100 train.csv
就可以直接print出来train.csv
这个文件的前100行数据,完全没必要把整个文件都load进内存里。
此外,有的文件内容可能是实时变化的(比如模型训练时的训练日志文件),如果希望持续track一个文件的内容,可以通过tail命令:
tail -f log
如果不仅要瞥一眼,而且还想查看或获取到文件中某些特定的行。
有的小伙伴就觉得必须要叫vim爸爸来帮忙了,甚至要求助python。其实这种高频场景也完全没有必要的,直接用grep
就能找出你感兴趣文件中的那些你感兴趣的模式。
这方面尤其在处理训练/eval日志时非常高效
例如,这么一份训练日志
我们eval了几十上百个ckpt,每次eval都往里面打了一堆日志,但是我们希望只看一下各个step的dev evaluation的结果,那么借助管道和grep
可以非常轻松的完成
cat eval.log | grep '\[dev'
而grep
的想象力也绝不是限制在管道里面,它完全可以单独使用,直接去从若干你感兴趣的文件中寻找特定的模式(比如./*/*就把所有子目录也一起遍历了),在你批量管理、分析实验日志时,用熟之后堪称神器。
当然了,进阶一点,用好awk
或sed
后,能摆脱vim和python的情况就更多了,尤其是在快速编辑方面。不过,这块一写可能又停不下来QAQ算了算了,下次再写吧(能凑够一篇文章的话)
别再无脑python了
如上节所述,使用grep
, awk
等linux的一些神器命令可以在很多高频场景下替代文本编辑器,大大提升炼丹效率。其实在coding层面上,也是如此。
例如,如果我们想把上一节末尾的图片中的日志,按照step从小到大排列后展示出来,怎么做呢?
可能又有小伙伴要说上python了。殊不知linux中内置了很多方便易用的命令,包括排序sort
命令,去重uniq
命令等,因此,这个排序的需求,在上一节末尾的grep命令处通过管道连接一个sort
就直接完成了
cat eval.log | grep '\[dev' | sort
在数据集处理方面,还有一个NLPer必知的命令,就是cut
啦。这个命令堪称是处理csv/tsv
格式文件的神器了,当你需要从CSV数据集文件中提取出某些列时,可以直接帮你免去开python的麻烦
例如,如下众所周知的Quora Question Pairs(QQP)数据集:
从第一行可以看到这个数据集包含6列:id qid1 qid2 question1 question2 is_duplicate
但实际上我们单纯训练一个q-q文本匹配模型的话,只需要最后三列就够了。不懂linux的小伙伴可能会用python打开文件,然后一顿for循环和str.split了。
但实际上,用上cut之后配合管道只需要一行
cat train.tsv | cut -f 4,5,6 > train.tsv.cut
就把数据集的第4,5,6列提出来了,并且存储在了train.tsv.cut
文件中。如果需要的数据来自于两个文件,可以分别cut出来再将这些列拼到一起(新文件是m+n列,可以理解成cat是纵向连接两个或若干个文件,而paste则是横向连接):
paste file1.txt file2.txt
最后,训练数据的去重和shuffle,也完全可以不用python去写,分别用uniq和shuf结合管道就能一行命令搞定:
sort train.tsv.cut | uniq | shuf > train.tsv
总之,类似于cut
,wc
(统计词数、行数的神器)的这种linux中的小命令很多,很大程度上帮我们解决了一些简单高频的数据集处理的需求(尤其CSV),避免了无脑使用python导致的低效时间开销。
另外,不仅简单场景可以直接用linux命令组合替代python,一些高端场景下linux命令与python的配合更是容易让复杂的事情变得非常简单。一个很常见的就是对大型数据集的单机并行处理:
&+wait:最简单的单机并行数据预处理
当数据集比较大时,我们要跑分词、shuffle、抽文本特征之类的可能会非常非常慢(千万级以上就明显感觉不行了,过亿之后要命了),如果强行把这些逻辑都丢到训练里面,又可能明显拖慢训练速度(虽然可以通过异步数据读取来缓解下),且极大的增加了训练阶段的CPU资源消耗。然而,如果我们用单核来跑全量数据,这个核用上洪荒之力可能也要跑一晚上(处理阅读理解这种数据的会就更久了)。要迭代预处理策略就会变得非常困难,怎么办呢?
用python手撸多线性代码?确实可以,但我嫌麻烦╮(╯▽╰)╭
最简单的做法就是用类似map reduce的范式来写,让python脚本仅仅去处理一个“单位”的数据,这里的单位可以是一个文本行,也可以是一个数据集的文件part。
例如,我们一个CSV文件里有1亿条样本,那么我们可以先用split
命令把它切成100个part(先用wc -l
确定行数),然后并行处理后再merge起来,随手写下:
for i in {0..99};
dopython process.py train.tsv.part$i > train.tsv.part$i.tmp &
done
wait
cat *.tmp > train.tsv.processed
rm *part*
就坐等小服务器上所有的CPU核心被打满进行数据处理啦~
但!是!如果代码bug了怎么办?等所有进程跑完吗?nonono,当然是一键kill啦~这里一种是偷懒的办法,直接用pkill -f <xxx>来杀死所有包含某个名字的进程
pkill -f 'process.py'
不过这样可能存在误杀的情况,需要注意名字不要取得太过于危险╮( ̄▽ ̄"")╭
此外,由于深度学习框架和一些库的原因,或者我们不小心打印了过量的内容到屏幕上,这时候也容易出现进程退不出来的情况(很多小伙伴选择了断开ssh链接),这时候可以使用ctrl+z将前台进程强行丢到后台并挂起,然后kill %N来杀掉它(这里的N一般是1,除非之前已经将一些前台进程挂后台了)
按下ctrl+z之后:
[1]+ Stopped top
[xixiaoyao@xxx.com emmm]$ kill %1
好困QAQ发现篇幅有点长了,还是写到这里戛然而止吧。喜欢本文,期待后续炼丹技巧分享的小伙伴们记得持续关注小屋&星标小屋 哦。
夕小瑶的卖萌屋
关注&星标小夕,带你解锁AI秘籍
订阅号主页下方「撩一下」有惊喜哦