基于deepspeech2的android应用开发详解
摘要: DeepSpeech2是一个端到端语音识别模型,采用CNN提取音频特征,RNN处理时序信息,并通过CTC损失函数解决序列对齐问题。在Android应用中,模型推理流程包括音频特征预处理、CNN/RNN计算及CTC解码。示例代码展示了NPU加速的推理实现,包含参数校验、数据量化、模型加载、推理执行及结果后处理等步骤,最终输出识别文本。
一,语音识别: DeepSpeech2模型概述
DeepSpeech2是一个端到端自动语音识别(ASR)引擎的开源项目。其核心代码实现包括音频预处理、特征提取、模型结构定义及训练流程。
模型架构
DeepSpeech2 的模型架构主要由卷积神经网络(CNN)、循环神经网络(RNN)和全连接层(Dense Layers)组成,并采用连接主义时间分类(CTC)损失函数进行训练。
卷积神经网络(CNN):
作用:负责提取音频信号的局部特征。音频信号在时域上具有连续性,CNN通过卷积核在音频信号上滑动,提取不同时间段的特征,生成特征图。
特点:能够捕捉音频信号中的局部模式,如音素、音节等,为后续的时序建模提供基础。
循环神经网络(RNN):
作用:对CNN提取的特征图进行时序建模,捕捉语音信号中的时序依赖关系。语音信号在时间上具有连续性,RNN通过其循环结构能够处理这种连续性,捕捉语音信号中的上下文信息。
变体:DeepSpeech2中常使用双向RNN(如双向GRU或双向LSTM),能够同时考虑过去和未来的上下文信息,提高识别准确率。
全连接层(Dense Layers):
作用:将RNN的输出转化为最终的识别结果。全连接层通过非线性变换,将RNN输出的高维特征映射到低维的文本空间,生成最终的识别文本。
CTC损失函数:
作用:解决序列对齐问题。在语音识别中,输入音频和输出文本之间存在对齐问题,即音频中的某个时间段可能对应文本中的多个字符或音节。CTC损失函数通过引入空白标签和重复标签,允许模型在输出序列中插入空白或重复字符,从而解决对齐问题。
二,android应用开发示例
本程序实现了端到端推理流程,对应原理中的“音频特征输入→CNN特征提取→RNN时序建模→CTC解码→文本输出”全链路。量化步骤适配了NPU对8-bit整型数据的处理特性。
开发步骤映射:属于“模型部署”阶段,承接训练完成的nbg模型,通过NPU加速实现高效推理。输入数据预处理需与训练时的特征工程保持一致(如MFCC参数、归一化系数)。
int main(int argc, char **argv) {
printf("%s nbg input\n", argv[0]);
if(argc < 3)
{
printf("Arguments count %d is incorrect!\n", argc);
return -1;
}
const char* nbg = argv[1];
const char* input = argv[2];
printf("input tensor_file = %s\n", input);
// npu init
awnn_init();
// create network
Awnn_Context_t *context = awnn_create(nbg);
// copy input
unsigned int width = 756;
unsigned int height = 161;
unsigned int sz = width * height;
float *plant_data = (float*) malloc(sz * sizeof(float));
unsigned char* data = (unsigned char*) malloc(sz * sizeof(unsigned char));
// Read tensor data from file
FILE *file = fopen(input, "r");
if (file == NULL) {
printf("Failed to open tensor file.\n");
exit(-1);
}
for (int i = 0; i < sz; i++) {
fscanf(file, "%f", &plant_data[i]);
plant_data[i] /= 0.009404;
if (plant_data[i] < 0)
data[i] = 0;
else if (plant_data[i] > 255)
data[i] = 255;
else
data[i] = (unsigned char)round(plant_data[i]);
}
fclose(file);
unsigned char *input_buffers[1] = {data};
awnn_set_input_buffers(context, input_buffers);
// process network
awnn_run(context);
// get result
float **results = awnn_get_output_buffers(context);
// post process
deepspeech2_post_process(results[0]);
free(plant_data);
free(data);
// destroy network
// awnn_destroy(context);
// npu uninit
// awnn_uninit();
return 0;
}
这段C程序实现了基于NPU(神经网络处理单元)的DeepSpeech2模型推理流程,核心逻辑可分为以下8个详细步骤:
-
参数校验与初始化
参数检查:通过argc < 3校验命令行参数数量,确保包含程序名、nbg模型路径、输入tensor文件路径三要素。
路径获取:argv[1]指向预训练的nbg模型文件(如deepspeech2.nbg),argv[2]指向输入特征数据文件(如MFCC特征或频谱图)。 -
NPU硬件初始化
调用awnn_init()完成NPU底层驱动加载、内存分配器初始化及硬件算子注册,为后续模型推理建立硬件加速环境。这里的npu是基于全志平台A733的api接口。
-
模型加载与上下文创建
通过awnn_create(nbg)加载nbg模型文件,生成推理上下文context。该步骤会解析模型结构(如CNN卷积核、RNN层数、CTC解码器),并分配对应的权重内存。
-
输入数据预处理
内存分配:
plant_data:float数组,存储原始输入特征(尺寸756×161,总计121,716个元素)。
data:unsigned char数组,存储量化后的输入数据。
数据读取与量化:
从输入文件逐个读取浮点数至plant_data。
执行归一化:plant_data[i] /= 0.009404(将特征值缩放到0-255范围)。
量化裁剪:通过if-else逻辑将值限制在[0,255]区间,并转换为unsigned char类型存入data数组。此步骤模拟了语音特征(如MFCC)的标准化处理,适配模型输入要求。 -
模型推理执行
输入绑定:awnn_set_input_buffers(context, input_buffers)将量化后的data绑定到模型输入层。
前向推理:awnn_run(context)触发NPU加速的模型推理,依次执行CNN特征提取、RNN时序建模、CTC解码等操作。
结果获取:awnn_get_output_buffers(context)获取输出缓冲区指针results,存储模型预测的字符概率序列。 -
后处理与结果解析
调用deepspeech2_post_process(results[0])对输出结果进行后处理,典型操作包括:
CTC解码:将字符概率序列转换为文本(如贪婪解码、Beam Search)。
语言模型纠偏:结合语言模型提升识别准确率。
格式化输出:生成最终识别文本并打印或返回。 -
资源释放与清理
释放动态分配的内存:free(plant_data)和free(data)。
注释掉的awnn_destroy(context)和awnn_uninit()表明程序未完全释放NPU资源(需注意潜在内存泄漏风险,生产环境应取消注释)。 -
程序退出
返回0表示正常退出,非0值表示错误(如参数校验失败、文件读取错误)。
int deepspeech2_post_process(float *tensor_data)
{
int rows = 378;
int data_size = rows * ALPHABET_SIZE; // 数据个数计数器
// printf("data_size = %d \n", data_size);
// printf("tensor[0] = %f \n", tensor_data[0]);
float tensor[rows][ALPHABET_SIZE];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < ALPHABET_SIZE; j++) {
tensor[i][j] = tensor_data[i * ALPHABET_SIZE + j];
}
}
free(tensor_data);
// // Print first row of tensor
// for (int i = 0; i < ALPHABET_SIZE; i++) {
// printf("%f ", tensor[0][i]);
// }
// printf("\n");
// Find maximum value index in each row
int tensor_argmax[rows];
for (int i = 0; i < rows; i++) {
int max_index = 0;
for (int j = 0; j < ALPHABET_SIZE; j++) {
if (tensor[i][j] > tensor[i][max_index]) {
max_index = j;
}
}
tensor_argmax[i] = max_index;
}
// Convert maximum value indices to characters
char results_1[rows + 1];
char results[rows + 1];
int a = 0;
for (int i = 0; i < rows; i++) {
if (tensor_argmax[i] < ALPHABET_SIZE) {
results_1[i] = alphabets[tensor_argmax[i]];
if (results_1[i] != '\0') {
results[a] = results_1[i];
// printf("%c", results[a]);
a = a + 1;
}
} else {
results_1[i] = '-';
}
}
results[rows] = '\0';
printf("Original array: %s\n", results);
removeDuplicateChars(results);
printf("Modified array : %s\n", results);
return 0;
}
以上代码核心部分解析:
1,贪婪解码(Argmax),对每个时间步(共378帧)独立执行argmax操作,选取概率最高的字符索引。
2,索引到字符映射
字符映射:通过全局字符表alphabets将索引转换为实际字符(如索引0→’a’)。
空字符过滤:跳过’\0’字符(可能用于标记单词边界),仅保留有效字符到results数组。
边界处理:末尾显式添加’\0’确保字符串终止,但results数组长度固定为rows+1,存在缓冲区溢出风险(若有效字符超过378个)。
三,运行演示流程
将模型文件推送至设备
将 deepspeech2 模型文件放入设备的 /data 或 /sdcard 目录:
adb push deepspeech2 /data/
预处理音频文件生成张量,通过短时傅里叶变换(STFT)生成频谱图
使用 pre_process.py 脚本将音频文件转换为模型输入所需的张量格式:
python3 pre_process.py --wav=1188-133604-0010.flac.wav
输入:FLAC/WAV格式的音频文件(如示例中的 1188-133604-0010.flac.wav)。
输出:生成对应的张量文件(如 1188-133604-0010.flac_756_161_1.tensor),包含预处理后的音频特征数据。
推送模型和张量文件到设备
将以下文件上传至设备:
模型二进制文件:deepspeech2.nb
预处理生成的张量文件:1188-133604-0010.flac_756_161_1.tensor
adb push deepspeech2.nb /data/
adb push 1188-133604-0010.flac_756_161_1.tensor /data/
在设备上运行推理
执行以下命令启动模型推理,输入为模型文件和张量文件:
deepspeech2 deepspeech2.nb 1188-133604-0010.flac_756_161_1.tensor
参数说明:
deepspeech2:可执行程序名称。
deepspeech2.nb:优化后的模型二进制文件(如NPU加速的量化模型)。
.tensor 文件:预处理后的音频特征数据。
输出识别结果
程序会打印推理后的文本结果,格式如下:
Original array: hheelllloo
Modified array : hello
原始输出:模型直接解码的字符序列(可能包含重复字符)。
去重后结果:通过后处理(如CTC解码)合并重复字符后的最终文本。
最后,完整的示例代码可在该路径下载。包括完整的demo代码和nb模型,输入音频文件。
更多推荐

所有评论(0)