00 - 前言
欢迎学习《基于 Conformer 和 Transformer 模型的中文语音识别》实验。本案例旨在帮助你深入了解如何运用深度学习模型搭建一个高效精准的语音识别系统,将中文语音信号转换成文字,并利用开源数据集对模型效果加以验证。
学习目标 在本课程中,您将学习如下内容:
- 语音数据预处理的方法
- 使用AI模型对语音数据进行预测推理的流程
- AI模型推理结果后处理的方法
- 端到端的深度学习工作流
目录 本实验分为四个核心部分。第一部分主要介绍语音识别的应用场景;第二部分会详细阐述端到端的解决方案,搭建起技术实现的整体框架;第三部分会手把手指导您完成代码编写与实现;最后一部分给出测试题,帮助您巩固学习内容。
- 场景介绍
- 解决方案
- 代码实战
- 课后测试
JupyterLab
在本实操实验中,我们将使用 JupyterLab 管理我们的环境。JupyterLab 界面是一个控制面板,可供您访问交互式 iPython Notebook、所用环境的文件夹结构,以及用于进入 Ubuntu 操作系统的终端窗口,只需要点击菜单栏的小三角就可以运行代码。
尝试执行下方单元中的一条简单的 print(打印)语句。
# DO NOT CHANGE THIS CELL# activate this cell by selecting it with the mouse or arrow keys then use the keyboard shortcut [Shift+Enter] to executeprint('This is just a simple print statement')This is just a simple print statement
01 场景介绍
在现代办公环境中,智能会议系统已经成为提高工作效率和协作能力的重要工具。随着技术的不断进步,会议记录的方式也在发生着革命性的变化。传统的会议记录方法,如人工笔记或手动整理录音,不仅效率低下,而且容易遗漏关键信息,尤其是在多说话人的复杂会议场景中。此外,人工记录还存在主观性,不同记录者对信息的理解和记录方式可能存在差异,这进一步影响了会议记录的准确性和完整性。
随着NLP技术的迅速发展,AI语音识别技术应运而生。语音识别系统能够自动将会议中的语音内容转换为文本,不仅提高了记录的效率,还能确保信息的准确性和完整性。此外,使用语言大模型还能对会议内容进行总结,输出会议纪要。然而,要实现这一目标,第一步要做的事情,就是把语音信号转化为文字。截止到目前,这一项技术已经发展得比较成熟了,几乎所有人都在日常生活中用到了这项技术,本案例则通过 conformer + tranformer 的技术方案来端到端介绍中文语音识别的基本原理和流程。
02 解决方案
本实验教程使用的解决方案如下,首先对wav格式的语音数据进行预处理,提取出二维梅尔频谱特征,然后传给 Conformer 模型进行预测,得到拼音序列;接着再把拼音序列(整数数组)传给 Transformer 模型,预测得到中文的 token id,最后进行后处理,把 token id 翻译成中文。
添加图片注释,不超过 140 字(可选)
以下分别介绍这几个核心模块。
2.1 声音数据预处理模块
原始的语音数据是音频的时间序列,不能直接给AI模型处理,需要进行一系列的预处理得到二维频谱矩阵。数据预处理包括读取音频数据、特征提取和数据填充等步骤。
添加图片注释,不超过 140 字(可选)
- 读取音频数据: 首先,我们需要将wav格式的音频数据读取并转换为适合AI模型处理的格式。在本案例中,我们使用 librosa.load 读取音频文件,返回音频时间序列。
- 特征提取: 然后我们需要将音频信号归一化到 [-1, 1] 范围内,以减少不同音频之间的幅度差异,这有助于提高模型的稳定性和性能。再进行预加重,用于增强高频成分,减少高频信号在传输过程中的衰减,这有助于提高高频特征的清晰度。最后使用 Log-Fbank 方法将音频信号从时域转换到频域,生成频谱图,并且使用梅尔滤波器组将频谱图转换为梅尔频谱图。
- 数据填充: 为了使得输入数据的 shape 和模型的输入 shape 保存一致,我们还需要对上面处理得到的数据进行填充。
2.2 语音识别模型
在现代语音识别系统中,Conformer 和 Transformer 模型的结合已成为一种强大的技术组合,能够高效地将语音信号转换为文本。Conformer 模型负责识别拼音序列,而 Transformer 模型则将这些拼音序列转换为中文文本(实际上是文字对应的 token id)。这种分阶段的处理方式不仅提高了识别的准确性,还增强了系统的鲁棒性和灵活性。
1. Conformer 模型
Conformer 是一种结合了卷积神经网络(CNN)和 Transformer 结构的混合模型,专门设计用于处理序列数据,如语音信号。其核心在于能够同时利用 CNN 的局部感知能力和 Transformer 的全局注意力机制实现对语音信号的高效处理。Conformer 模型的输出是一个整形数组,如 [12, 22, 40, ..., -1, -1],非 -1 的每个整数都是一个拼音对应的 token id,如 “yun2 xu3”、“mei2 you3”,而 -1 代表不包含语音信息。
添加图片注释,不超过 140 字(可选)
2. Transformer 模型
Transformer 是一种基于自注意力机制的序列模型,广泛应用于自然语言处理任务,如机器翻译、文本生成等。在语音识别中,Transformer 模型负责将 Conformer 生成的拼音序列转换为中文字符对应的 token id。
2.3 后处理模块
经过conformer+transformer模型预测得到的是一个整数序列,每个整数代表一个中文字符在字典表中的 key 值,所以我们还需要根据字典,把整数序列翻译成中文语句。
好了,到这里我们已经学习了语音翻译的解决方案流程,接下来我们就一起动手,基于开源数据集完成端到端的实验!
03 动手实验
3.1 实验准备
数据集
实验所用的语音测试数据源自AISHELL-1_sample样例数据集,该数据集包含了79条语音数据和对应的中文文本,每条语音的长度都在5秒钟以内。
模型权重
本实验采用的 Conformer 和 Transformer 模型来自gitee社区的tensorflow模型,权重要从这个链接下载,里面包含了 pb 格式的模型,我们后续将会用到。
3.2 语音数据预处理
如 2.1 章节所述,首先我们需要使用三方库 librosa 加载 wav 格式的语音数据。由于原始数据是音频的时间序列,所以我们还需要设置一个采样率 sample_rate,指每秒从连续信号中提取并组成离散信号的采样个数。我们使用 ./test_data/BAC009S0252W0302.wav 样本数据进行测试:
import librosaimport osimport numpy as np sample_rate = 16000wav_path = "./test_data/BAC009S0252W0302.wav"wav_data, _ = librosa.load(os.path.expanduser(wav_path), sr=sample_rate)print(wav_data.shape)(45775,)
然后我们需要对采集的信息进行归一化和预加重,目的是增加有效的语音信号:
def normalize_signal(signal: np.ndarray): """ Normailize signal to [-1, 1] range """ gain = 1.0 / (np.max(np.abs(signal)) + 1e-9) return signal * gain def preemphasis(signal: np.ndarray, coeff=0.97): '''improve the high frequency data of speech''' if not coeff or coeff <= 0.0: return signal return np.append(signal[0], signal[1:] - coeff * signal[:-1])
接着 ,我们就可以使用傅里叶变换对音频序列进行变换,得到频谱信息,并进一步使用梅尔滤波器组将频谱图转换为梅尔频谱图。梅尔频谱图(Mel Spectrogram)是一种常用于音频处理和分析的特征表示方法,特别是在语音识别、音乐信息检索等领域。它通过将音频信号的频谱转换到梅尔尺度(Mel scale)上来更好地反映人耳对频率的感知特性。在这里,我们主要使用 librosa 库提供的方法:
def compute_logfbank_feature(signal: np.ndarray) -> np.ndarray: # 1. The Fourier transform is used to get the mel spectral characteristics frame_ms = 25 # 每个帧的持续时间,单位是 ms stride_ms = 10 # 表示相邻帧之间的时间间隔,单位是 ms frame_length = int(sample_rate * (frame_ms / 1000)) frame_step = int(sample_rate * (stride_ms / 1000)) num_feature_bins = 80 # 表示梅尔频谱图中梅尔滤波器的数量。每个滤波器对应一个特征 bin,用于提取特定频率范围内的能量 log_power_mel_spectrogram = np.square( np.abs( librosa.core.stft(signal, n_fft=frame_length, hop_length=frame_step, win_length=frame_length, center=True))) # 2. Use librosa tool to get mel basis mel_basis = librosa.filters.mel(sample_rate, frame_length, n_mels=num_feature_bins, fmin=0, fmax=int(sample_rate / 2)) # 3. Dot product mel_basi and log_power_mel_spectrogram to get logfbank feature return np.log(np.dot(mel_basis, log_power_mel_spectrogram) + 1e-20).T
求出梅尔特征后,我们还需要对信号进行归一化,使得数据分布和AI模型的输入空间匹配:
def normalize_audio_feature(audio_feature: np.ndarray, per_feature=False): """ mean and variance normalization """ axis = 0 if per_feature else None mean = np.mean(audio_feature, axis=axis) std_dev = np.std(audio_feature, axis=axis) + 1e-9 normalized = (audio_feature - mean) / std_dev return normalized
好的,现在我们把这些预处理过程用一个 extract 函数包装起来,方便后续调用:
def extract(signal: np.ndarray) -> np.ndarray: """feature extraction according to feature type""" # 1. Normalize signal signal = normalize_signal(signal) signal = preemphasis(signal, 0.97) # 2. Compute feature features = compute_logfbank_feature(signal) # 3. Normalize feature # mean and variance normalization features = normalize_audio_feature(features) features = np.expand_dims(features, axis=-1) return features
现在,我们传入前面 load 的语音数据,测试一下:
extract_data = extract(wav_data)print(extract_data.shape)(287, 80, 1)/tmp/ipykernel_2234331/3089051765.py:18: FutureWarning: Pass sr=16000, n_fft=400 as keyword args. From version 0.10 passing these as positional arguments will result in an error mel_basis = librosa.filters.mel(sample_rate,
由于我们后续要使用的 Conformer 模型的输入shape是 1,1001,80,1 ,所以还需要对输入进行padding,使得 shape 和模型的输入一致:
def pad_feat(feat, max_length=1001): '''padding data after feature extraction''' # Truncate data that exceeds the maximum length if feat.shape[0] > max_length: feat = feat[0:max_length] else: # Use 0 for padding to max_length at axis 0. feat = np.pad( feat, ((0, max_length-feat.shape[0]), (0, 0), (0, 0)), 'constant') return feat pad_data = pad_feat(extract_data)print(pad_data.shape)(1001, 80, 1)
此外,注意到 Conformer 模型的输入有2个,除了 pad_data,还有 length ,代表输出的拼音序列预测长度,计算公式如下:
length_data = pad_data.shape[0] // 4print(length_data)250
好了,到这里我们就完成了原始语音数据的预处理,把它转换成了AI模型可以处理的格式,接下来我们就尝试使用 Conformer 模型和 Transformer 模型进行推理。
3.3 使用 Conformer 和 Transformer 模型进行预测
如 2.2 章节所述,我们将使用 Conformer 模型预测拼音序列,再接着用 Transformer 模型对拼音序列进行预测得到中文字符。
首先,我们需要把下载的 pb 格式的模型转成能在昇腾硬件上运行的 om 格式的模型,命令如下:
# atc --model=./frozen_graph_conform.pb --framework=3 --output=./am_conform_batch_one --input_format=NHWC --input_shape="features:1,1001,80,1;length:1,1" --soc_version=Ascendxxx --log=error# atc --model=model/pb/frozen_graph_transform.pb --framework=3 --output=./lm_transform_batch_one_keep --input_format=ND --input_shape="inputs:1,251" --soc_version=Ascendxxx --log=error --precision_mode=must_keep_origin_dtype
然后我们构建 om 模型的推理类:
import acl ACL_MEM_MALLOC_HUGE_FIRST = 0ACL_MEMCPY_HOST_TO_DEVICE = 1ACL_MEMCPY_DEVICE_TO_HOST = 2class OmModel: def __init__(self, model_path): # 初始化函数 self.device_id = 5 # step1: 初始化 ret = acl.init() # 指定运算的Device ret = acl.rt.set_device(self.device_id) # step2: 加载模型 # 加载离线模型文件,返回标识模型的ID self.model_id, ret = acl.mdl.load_from_file(model_path) # 创建空白模型描述信息,获取模型描述信息的指针地址 self.model_desc = acl.mdl.create_desc() # 通过模型的ID,将模型的描述信息填充到model_desc ret = acl.mdl.get_desc(self.model_desc, self.model_id) # step3:创建输入输出数据集 # 创建输入数据集 self.input_dataset, self.input_data = self.prepare_dataset('input') # 创建输出数据集 self.output_dataset, self.output_data = self.prepare_dataset('output') def prepare_dataset(self, io_type): # 准备数据集 if io_type == "input": # 获得模型输入的个数 io_num = acl.mdl.get_num_inputs(self.model_desc) acl_mdl_get_size_by_index = acl.mdl.get_input_size_by_index else: # 获得模型输出的个数 io_num = acl.mdl.get_num_outputs(self.model_desc) acl_mdl_get_size_by_index = acl.mdl.get_output_size_by_index # 创建aclmdlDataset类型的数据,描述模型推理的输入。 dataset = acl.mdl.create_dataset() datas = [] for i in range(io_num): # 获取所需的buffer内存大小 buffer_size = acl_mdl_get_size_by_index(self.model_desc, i) # 申请buffer内存 buffer, ret = acl.rt.malloc(buffer_size, ACL_MEM_MALLOC_HUGE_FIRST) # 从内存创建buffer数据 data_buffer = acl.create_data_buffer(buffer, buffer_size) # 将buffer数据添加到数据集 _, ret = acl.mdl.add_dataset_buffer(dataset, data_buffer) datas.append({"buffer": buffer, "data": data_buffer, "size": buffer_size}) return dataset, datas def forward(self, inputs): # 执行推理任务 # 遍历所有输入,拷贝到对应的buffer内存中 input_num = len(inputs) for i in range(input_num): bytes_data = inputs[i].tobytes() bytes_ptr = acl.util.bytes_to_ptr(bytes_data) # 将图片数据从Host传输到Device。 ret = acl.rt.memcpy(self.input_data[i]["buffer"], # 目标地址 device self.input_data[i]["size"], # 目标地址大小 bytes_ptr, # 源地址 host len(bytes_data), # 源地址大小 ACL_MEMCPY_HOST_TO_DEVICE) # 模式:从host到device # 执行模型推理。 ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset) # 处理模型推理的输出数据,输出top5置信度的类别编号。 inference_result = [] for i, item in enumerate(self.output_data): buffer_host, ret = acl.rt.malloc_host(self.output_data[i]["size"]) # 将推理输出数据从Device传输到Host。 ret = acl.rt.memcpy(buffer_host, # 目标地址 host self.output_data[i]["size"], # 目标地址大小 self.output_data[i]["buffer"], # 源地址 device self.output_data[i]["size"], # 源地址大小 ACL_MEMCPY_DEVICE_TO_HOST) # 模式:从device到host # 从内存地址获取bytes对象 bytes_out = acl.util.ptr_to_bytes(buffer_host, self.output_data[i]["size"]) # 按照模型输出数据的格式将数据转为numpy数组 data = np.frombuffer(bytes_out, dtype=np.int32) inference_result.append(data) return inference_result def __del__(self): # 析构函数 按照初始化资源的相反顺序释放资源。 # 销毁输入输出数据集 for dataset in [self.input_data, self.output_data]: while dataset: item = dataset.pop() ret = acl.destroy_data_buffer(item["data"]) # 销毁buffer数据 ret = acl.rt.free(item["buffer"]) # 释放buffer内存 ret = acl.mdl.destroy_dataset(self.input_dataset) # 销毁输入数据集 ret = acl.mdl.destroy_dataset(self.output_dataset) # 销毁输出数据集 # 销毁模型描述 ret = acl.mdl.destroy_desc(self.model_desc) # 卸载模型 ret = acl.mdl.unload(self.model_id) # 释放device ret = acl.rt.reset_device(self.device_id) # acl去初始化 ret = acl.finalize()
现在测试一下模型的推理结果
# 加载conformer模型conform_om_model_path = "./am_conform_batch_one_linux_aarch64.om"conform_om_model = OmModel(conform_om_model_path)# 推理pad_data_array = np.array([pad_data], dtype='float32') length_data_array = np.array([[length_data]], dtype='int32') conform_out = conform_om_model.forward([pad_data_array, length_data_array])conform_out_reshape = conform_out[0].reshape(1, -1)print(conform_out_reshape.shape)(1, 251)
# 加载transformer模型transform_om_model_path = "./lm_transform_batch_one_keep.om"transform_om_model = OmModel(transform_om_model_path)# 推理transform_out = transform_om_model.forward([conform_out_reshape])transform_out_reshape = transform_out[0].reshape(1, -1)print(transform_out_reshape.shape)# converts the inference result to a NumPy arrayinfer_result = transform_out_reshape[0]ids = np.array(infer_result)print(ids)(1, 252)[ 1 82 132 13 307 34 803 1459 490 633 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
好的,到这里,我们使用两个AI模型对语音信号数据进行推理预测,得到了一个数组 ids,这个数组的每个值都代表字典文件 lm_tokens.txt 里面的一个序号。接下来,我们要根据 lm_tokens.txt 把 ids 翻译成中文。
3.4 后处理函数
为了把AI模型推理得到的整数数组翻译成中文,我们需要依赖 lm_tokens.txt 文件,这个文件的内容如下,每行都包含一个字符,第0行的字符是 S,第1行的字符是 /S,第2行的字符是 的,等等:
""" S /S 的 是 不 一 了 很 有 好 我 ... 我们 """
'\nS\n/S\n的\n是\n不\n一\n了\n很\n有\n好\n我\n...\n我们\n'
我们创建2个字典把它们的对应关系记录下来:
import codecs token_to_index = {}index_to_token = {}lines = []with codecs.open("./lm_tokens.txt", "r", "utf-8") as fin: lines.extend(fin.readlines()) index = 1for line in lines: # Strip the '\n' char line = line.strip() # Skip comment line, empty line if line.startswith("#") or not line or line == "\n": continue token_to_index[line] = index index_to_token[index] = line index += 1
然后翻译 ids:
def deocde_without_start_end(ids): """Convert a list of integers to a list of tokens without 'S' and '\S' """ startid = token_to_index['S'] endid = token_to_index['/S'] tokens = [] for i in ids: if i == startid: continue elif i == endid: break else: tokens.append(index_to_token[i]) return tokens text = deocde_without_start_end(ids)# convert list to string and print recognition resultprint("The recognition result: ", ''.join(text))The recognition result: 对于这类可穿戴设备
预测结果显示这段语音对应的中文是“对于这类可穿戴设备”。我们查看使用的测试数据 ./test_data/BAC009S0252W0302.wav 对应的真实标签,和我们的预测结果一致!
添加图片注释,不超过 140 字(可选)
3.5 端到端测试
现在,我们把前面的数据处理代码和后处理代码组合起来,进行批量化地预测。首先,把数据预处理的函数合成一个函数:
def make_model_input(wav_path_list): wav_data_list = [] length_data_list = [] for wav_path in wav_path_list: # Load data and extract features. wav_data, _ = librosa.load(os.path.expanduser(wav_path), sr=sample_rate) feat_data = extract(wav_data) # Padding the data after feature extraction. wav_data = pad_feat(feat_data) # Calculates the length of the text corresponding to the speech data. length_data = feat_data.shape[0] // 4 wav_data_list.append(wav_data) length_data_list.append([length_data]) # Convert list to NumPy data wav_data_batch = np.array(wav_data_list, dtype='float32') length_data_batch = np.array(length_data_list, dtype='int32') return wav_data_batch, length_data_batch
然后设置批量数据路径进行推理:
import globfrom pathlib import Path wav_file_dir = "./test_data/"wave_file_paths = glob.glob(str(Path(wav_file_dir).resolve()) + '/*.*')for wave in wave_file_paths: feat_data, len_data = make_model_input([wave]) # 推理 conform_out = conform_om_model.forward([feat_data, len_data]) conform_out_reshape = conform_out[0].reshape(1, -1) transform_out = transform_om_model.forward([conform_out_reshape]) transform_out_reshape = transform_out[0].reshape(1, -1) # get inference result infer_result = transform_out_reshape[0] if infer_result.size == 0: print("infer_result is null") exit() # converts the inference result to a NumPy array ids = np.array(infer_result) # decode text = deocde_without_start_end(ids) # convert list to string and print recognition result print("The recognition result: ", ''.join(text))The recognition result: 也就是通过统一平台 The recognition result: 对于这类可穿戴设备 The recognition result: 我们这种模式要比云公厂的概念要好一些
恭喜你!至此,你已经成功完成了基于 Conformer 和 Transformer 模型的中文语音识别的全部实验流程,希望你能够熟练掌握这套技术方案,并将其应用到实际的项目中去!
3.6 依赖软件
本实验的依赖软件版本信息如下:
- Python:为了方便开发者进行学习,本课程采用Python代码实现,您可以在服务器上安装一个Conda,用于创建Python环境,本实验使用的是 python 3.10 ;
- librosa:一个用于音频和音乐分析的Python库,提供了丰富的音频处理功能,特别适用于音乐信息检索(MIR)和音频分析任务,本实验使用的是 0.9.2 版本;
- numpy: 开源的Python科学计算库,用于进行大规模数值和矩阵运算,本实验使用的是 1.26.4 版本;
- CANN(Compute Architecture for Neural Networks):Ascend芯片的使能软件,本实验使用的是 8.0.rc2 版本。
04 课后测试
- 多尝试几个测试样本,观察预测结果是否准确
- 对于长时间的语音数据,如何进行推理预测