去年四月份的时候,写了一些对这个repo的想法。最初并没有什么明确的目标,只是单纯的想在kaldi上写一些东西。我那时正在做enhancement相关的任务,最先在python+pytorch环境下跑出的结果,觉得在kaldi上做也不是很难,就想花一些功夫迁一下,把增强任务中一些固定的程序流程集成。最先是从最基本的(i)STFT开始,着手写了一些speech重构,谱,相位特征的一些基础命令。后来因为在nnet3上用Quadratic Loss训练mask模型的效果不错,就增加了一部分训练脚本,mask计算和后处理separation的代码,形成了一个围绕single-channel任务的工具闭环。增强相关的任务,包括特征提取,mask计算,网络训练,语音重构都可以在kaldi上做,验证一些数据的效果也是很快的(记得当时半天就把CHiME4上的一些任务做了重现)。所以当时README上写的就是”Speech Enhancement Toolkit based on Kaldi”。
后面有一段时候没有做更新,因为本身就single-channel而言,主流的方法,feature mapping和TF-mask已经被全部集成了,一时半会也没有什么额外的需求。所以后面就想着继续拓展到对multi-channel的支持上去。这时候存在的问题就是kaldi中没有complex类型的支持,如果我要在kaldi上实现相关算法,这个基础的组件必须要解决掉。开始还是比较犹豫,之后那段时间手里工作进展不顺利,就想着法子转移注意力了。我花了大约一周多的时间看了一下openblas的api,把complex matrix/vector的class写出来,支持一些常见的数学运算,在此之上实现了一版beamformer(mvdr和max-snr)。后期我和自己的python版本实现对比过,效果会差一些,但是整体问题不大,数值运算也比较稳定。其实这段时期贡献代码的目的十分单纯了,就是闲着想写点东西,包括后期实现的srp,fix beamformer等等。当时还建了sparse NMF和fastICA的branch,但是测试发现结果不太稳定,后期也没有继续做。真从我本身的做实验的角度,我更倾向于使用python版本的代码,这也是我后期主要转到python上去,更新script目录的主要原因。
再往后,前端的任务量越来越多,工具,代码也变的零碎和杂乱。matlab,python,kaldi等的数据格式和某些通用算法实现均不统一,在实验中就会遇到很多麻烦事,比如分帧的逻辑不同造成的时间维度不一致等等。因此我觉得很有必要花一段时间把手头的代码做一次整理,规范特征提取和数据IO,同时把常用的操作功能glue到脚本中去,便于任务并行,提升效率。虽然当时几乎重写了手里的现有代码,但是从后续的体验而言,还是觉得相当值得的。当时的想法主要有
- 统一数据访问。我沿用kaldi的规范考虑下面两点。第一,.scp这种key-value对的文件形式通用性很高,不管原始数据如何分布,拿到scp就可以做随机或者顺序访问,即使有新的数据格式,继承
Reader
实现一下_load
函数即可。第二,对于kaldi的archive归档文件读写,我之前写过python的接口,可以做高效的IO,也同时兼容kaldi的内置命令和脚本。基于这两点,我后来大部分程序的输入和输出都是所谓的.scp或者.ark形式。 - 统一(i)STFT。我使用的是librosa库的实现,两个原因,其一是那边的开发者项目维护的很频繁,代码规范度较高,社区也比较活跃,其二是我仔细看过它的代码,内在实现的细节比较熟悉。所有频域的特征,算法的实现均在此之上进行,包括spatial/spectra features,mask computation,speech separation/estimation, cgmm,gwpe,beamformer等等,这样可以保证不同命令之间的输入输出可以自由流动,因为底层的时频转换是一致的。
- 效率和并行。speech相关的任务数据量通常较大,做一些前端的预处理或者特征提取,可能就是几天的时间,程序跑的慢是难以容忍的事情。从优化的角度来说,其一是做并行,这里沿用kaldi的style,几乎所有的脚本都支持并行选项,其二就是优化程序本身,以cgmm为例,前后改了几版,最终效率提了大概30x,原本一天多的实验,后面几个小时就可以解决掉,这对于提升做一些验证性实验或者baseline系统搭建的效率是很有帮助。
- 通用和独立。separation/enhancement任务中有很多功能独立且固定的模块,以mask为例,单通道上直接做掩蔽,多通道做波束形成,这些脚本一次写成之后,在后续其他的任务中就不需要重复实现,只需要拿到对应的mask就可以做增强分离和性能评估,无关数据,方法和训练工具。其他的诸如特征计算,基准测试和一些signal processing的算法等等,对于新的数据而言也完全就是一行脚本的事。
当整个repo原型出来之后,我的实验代码中基本只需要写{nnet,dataset,train.py,infer}.py
等几个模型相关的文件,其他的功能在setk中用几行脚本就可以完成,而且不同模型结果之间的对比也更加严谨,毕竟底层的计算逻辑是是完全一致的。考虑到查看特征和mask在增强和分离中都是很常见的操作,后面我也加了几条可视化数据的命令,便于结果查验和特征检测。
上面有一点是我没有提到的,就是数据模拟(data simulation)。增强/分离这个任务上和ASR不太相同,大部分情况下都是在simu的数据集上进行训练,因为这样方便拿到对应的reference做训练target。而simu中的具体参数,比如信噪比,噪声类型,远场的话距离,混响时间,麦克风拓扑等等,都是根据你具体的应用或者假设场景决定的。我有一段时间写了很多的数据模拟相关的程序,考虑到功能不是十分通用(因为和具体的需求相关),除了rir_generate.py
之外,并没有public。生成rir的rir-simulate
是我参照开源的matlab版本,重新实现的,结果上和原始版本没有差别。主要考虑matlab版本不是非常好用,比如服务器上没有matlab环境等等。
18年九月之后我基本就在python层面维护代码,这时候已经背离原先“based on kaldi的初衷了,因此就更名为“integrated with kaldi”,可以用作对kaldi的一个前端扩展。同时把之前matlab版本的cgmm迁移到python上去,并做了优化,最后测试的结果,在性能和效率上都有了提升。
github上有一些机构和个人开始在python上进行signal processing的算法实现,比如pyroomacoustics。我对这些项目是非常感兴趣的。早期(可能现在也是)研究signal processing那块的人一直在matlab上做东西,波束形成,定位,降噪,去混响,盲源分离等等,大部分都不做开源。 对于很多新人而言,即使文章看懂了,代码也未必能写的出来(比如我至今写不出TF-GSC)。而有了参考的实现程序就非常有助于自己理解算法的细节,同时直观的体会到对应的实验结果。所以我在github上放出的那些repo,实验代码也好,工具也好,其实大部分并不是想让人clone下来直接用的,而是希望有做相同工作的人看见了,可以做个参考,或者帮助他们理解一下。理解之后,我觉得实现代码只是时间上的问题。反过来想一想现在用pytorch这些工具搭模型就可以复现论文,一方面没有多少理论方面的理解难度,一方面代码实现十分容易,真是赶上research的好时代了。但是也是个人能力有限,还有很多算法没有办法从matlab迁移到python,所以现在这个repo只做到目前这个样子。
最后说一下我对repo里面数据存储一个看法。特征什么的,早期为了兼容kaldi,存下的是archive格式。这个带来两个不方便的地方。其一,模型训练阶段的依赖kaldi-python-io这个包去load数据,不如pytorch,numpy这些简洁。其二,需要提前提取好特征,这个如果数据量很大的话,会很占磁盘空间。虽然kaldi中可以对特征进行压缩,但是kaldi-python-io中的解压过程很慢 (比不做压缩的慢上几十倍),因此我都是不开compress选项,牺牲磁盘空间。目前我更倾向于在dataset里面做online的特征提取,和torch-audio/torch-vision的Transform
设计类似,牺牲一点数据加载的时间。或者找到一种比较适合做大文件存储,同时python友好的数据格式进行替换等等。
未来的话,我还有些想法,其一是通过一些egs来解释脚本和命令的用法,现在我只做一个upit,也是给一个样例,怎么使用这些脚本,配合网络训练和后处理的,其二想添加一些signal方向上bss和denoise相关的算法,从个人兴趣上,还是觉得有理论支撑的东西比较优美,而搭网络则更加神奇……
希望所做工作能对同行有所帮助。
写于2019.3