在安卓上使用Kaldi进行模型开发

为什么有这个想法,因为自己有过体验,当初做的第一个语音增强的demo,傻乎乎的自己实现特征提取,自己实现网络前向,于是就需要将kaldi的网络参数转写成方便自己程序读取的格式,还需要不断对比自己实现的特征结果和HTK的结果是否一致,这期间花费的时间个人觉得已经远远的超出做demo本身的意义,最主要的是,由于当初实现的代码仅仅是针对当初的需求,而一旦后期的特征配置或者网络结构发生变化,之前的工作就要重复一次,如此低效率的事情我个人是不想重复做的。

所以,最简单的方法就是,将计算逻辑交给kaldi完成,自己只需要完成控制逻辑上的编码。本身使用NDK+JNI进行开发并不是很难,无非是将kaldi的依赖和自身编译成库,和自己编写的JNI接口链接,通过Android Studio支持的cmake或者ndk构建最终的动态库就行了,但是,由于网上资料过少,链接错误又比较难查,所以还是耗费了不少时间的。

我的需求是提取nnet1和feat两块,所以不需要openfst以及其他无关的代码,最终提取出的工作目录如下(实际上还可以进一步精简,test文件一律删除)

1
base  cudamatrix  feat  hmm  itf  matrix  nnet thread  tree  util

所有步骤都可以从以下链接中获取

  • 谷歌官方的NDK指南 LINK
  • AndroidStudio的用户手册 LINK
  • kaldi的交叉编译 LINK
  • OpenBlas的编译 LINK

我踩过的坑如下:

配置本地编译环境

这一步之前编译OpenBlas的时候也用过,使用ndk自带的make_standalone_toolchain.py脚本配置编译环境,安装完毕之后,将安装目录下的bin文件夹导入环境变量。脚本比较重要的参数是--api--stl。其中--stl配置NDK的运行时(gnustl/libc++/stlport),--api配置SDK的版本,这两个配置选项要和后续的Application.mk保持一致的,分别对应APP_STLAPP_PLATFORM--arm配置编译平台,安卓手机对应arm或者arm64,对应Application.mk中APP_ABIarmeabi/armeabi-v7aarm86-v8a。另外,切记,openblas和kaldi在同意一个toolchain下完成编译。

编译clapack

这个也很简单,github上有人已经构建好了工程,简单修改Android.mk执行ndk-build就行。

编译openblas

openblas编译基本不会出现问题,参照上面给出的链接即可。将clapack生成的blas,lapack,clapack,f2c四个库拷贝到openblas安装目录下的lib中,kaldi会自动配置到kaldi.mk中作为链接库。

编译kaldi

由于修改了原先的工程目录,所以需要修改一下Makefile和configure文件

  1. 去除configure文件中关于openfst的检查脚本
  2. 修改Makefile中的SUBDIR(控制编译那些目录),以及各子目录中的Makefile(ADDLIBS是本目录的依赖库,OBJFILES是要打包到静态库中的目标文件,不需要生成测试文件就将TESTFILES注释即可)
  3. 注释android_openblas.mk中openfst的宏定义,将宏ANDROIDINC修正为ANDROIDINCDIR(脚本的bug,因为configure中定义的是ANDROIDINCDIR),注意一下CXXFLAGS的参数,后面链接的时候要用到。
  4. kaldi默认在编译android平台只能生成静态库

AS的配置

上述过程倒是很少出现问题,在AS中用cmake或者ndk生成动态库是主要的头疼点(一系列让人摸不着头脑的链接错误)。首先说明kaldi的编译和链接参数如下(来自android_openblas.mk):

1
2
3
4
5
-DKALDI_DOUBLEPRECISION=0 # BaseFloat为单精度 
-DHAVE_CXXABI_H
-DHAVE_OPENBLAS # 在cblas_wrapper.h中用到,表示blas用openblas实现
-DANDROID_BUILD -ftree-vectorize
-mfloat-abi=hard -mfpu=neon -mhard-float -D_NDK_MATH_NO_SOFTFP=1 # openblas用到

以及
1
2
-Wl,--no-warn-mismatch 
-lm_hard # openblas用到

以上参数需要在Android.mk或者CmakeLists.txt中等价的实现,cmake我没有成功过,Android.mk目前配置如下:
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
LOCAL_PATH := $(call my-dir)

# 预构建静态库
include $(CLEAR_VARS)
LOCAL_MODULE := kaldi-prebuild
LOCAL_SRC_FILES := libkaldi.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := kaldi
LOCAL_SRC_FILES := impl.cpp

# 编译选项
LOCAL_CFLAGS += -DKALDI_DOUBLEPRECISION=0 -DHAVE_CXXABI_H \
-DHAVE_OPENBLAS -DANDROID_BUILD -ftree-vectorize \
-mfloat-abi=hard -mfpu=neon -mhard-float -D_NDK_MATH_NO_SOFTFP=1
# 链接选项
LOCAL_LDFLAGS += -Wl,--no-warn-mismatch -lm_hard
# 链接静态库,为什么只有一个?我把各个静态库压成一个了
LOCAL_STATIC_LIBRARIES += kaldi-prebuild
# 定义头文件路径
LOCAL_C_INCLUDES += $(LOCAL_PATH)/blas $(LOCAL_PATH)
# 动态链接log库
LOCAL_LDLIBS += -llog

include $(BUILD_SHARED_LIBRARY)

对应的Application.mk如下,之前说明过,配置要和toolchain保持一致
1
2
3
4
APP_ABI := armeabi-v7a # 用到了openblas
APP_STL := c++_static # libc++
APP_CPPFLAGS := -fexceptions # 打开异常开关
APP_PLATFORM := android-24 # SDK-24

这期间遇到的问题如下:

cmake的VFP问题

使用链接openblas如果不加-mhard-float -D_NDK_MATH_NO_SOFTFP=1 -lm_hard这些选项的话,会出现*.a use VFP arguments, but output not的链接错误,但是,我在CmakeLists.txt中的add_definitions和gradle中的cppFlags都尝试过添加这些选项,均没有解决该问题,只能转战ndk+Android.mk了。

f2c_的未定义问题

这个推测应该是和静态库的链接顺序有关系,Android.mk要链接静态库,须先预构建一下(我暂时不知道其他链接方法),然后将预构建好的模块进行连接(加到LOCAL_STATIC_LIBRARIES参数之后),有一种说法是越基本的库放的越靠后,我在kaldi-base.a kaldi-matrix.a和五个blas相关的库上做过各种顺序的尝试,但是始终没能解决该问题(要么是f2c的为定义,要么是openblas的未定义),最终用粗暴的方法,将这几个库打包成一个静态库解决该问题的(尽量保证openblas和clapack库是同一个toolchain构建的)

ndk命名空间错误

这个错误是后来做了若干修正之后解决的,现在回想,可能的原因是Application.mk中的APP_PLATFORMAPP_STL和toolchain配置的api/stl不匹配。

到这里,在jni文件夹外边使用ndk-build已经可以直接生成静态库了,但是,如果在AS中build的话,还是会出现rand_r, rand, posix_memalign这些函数的未定义,目前这些问题还没有解决,我通过gradle配置ndk的构建方法替换AS自身的逻辑来跳过这个错误,在gradle中android{}添加配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sourceSets.main {
jni.srcDirs = [] # 禁用AS默认的jni目录
}

task ndkBuild(type: org.gradle.api.tasks.Exec, description: "compile JNI by NDK") {
commandLine "/Users/wujian/Library/Android/sdk/ndk-bundle/ndk-build",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}

tasks.withType(JavaCompile) {
compileTask->compileTask.dependsOn ndkBuild
}

这种方法并不推荐,实际上个人觉得还是AS支持的ndk和cmake最佳,右击app,选择构建工具,link一下CMakeLists.txt或者Android.mk就行了。以后找到问题所在还是会切换过去的。

无非就是一个链接问题,前后折腾了五天,现在总结的话,大致也只用这么多可以被写下来的东西。不过,想到今后从模型到demo的可以省下的大把时间,还是一件值得的事情。