Advertisement

Speech Recognition using Deep Learning in Python (Tutor

阅读量:

作者:禅与计算机程序设计艺术

1.简介

语音识别(speech recognition)是一个计算机系统能够将人的声音转换为文字或命令的过程。它是很多应用场景中的基础技能,如自动驾驶、智能助手等。在过去几年里,人们逐渐从观察者变成了参与者,不仅需要懂得语言语法,还需要学会用自己的声音说话。基于此,人工智能领域兴起了一股清新之风,诞生了许多基于深度学习的方法来进行语音识别。本文将以Python及其相关库实现端到端的语音识别系统。 语音识别系统一般包括特征提取、声学模型(如HMM-GMM)、解码算法(如Viterbi搜索、Beam Search等)以及分类器等模块。其中特征提取主要负责将音频信号转换为可用于机器学习模型的数据。声学模型通常使用混合高斯模型(HMM-GMM)或循环神经网络(RNN)对声学特征进行建模。解码算法通过对声学模型生成的概率分布进行归一化并对最可能的结果进行排序,从而确定最终的输出文本或指令。

2.基本概念与术语

2.1.音频信号处理

首先,我们要理解什么是音频信号。简单地说,就是声音传播时传导的无形的电磁波的大小与方向的记录。普通人的声音可以通过各种方式传播,但在电脑中,音频信号以数字的形式存储,数字信号的采样频率为44.1kHz或48kHz,每秒钟有44100个采样点,即一共有44.1\times 10^3个采样时间步长。每一个采样时间步长对应于0.000025秒的时刻。因此,一段音频信号由多个采样时间步长构成。如下图所示:

上图是一个代表声音的正弦波的例子。在这个信号中,我们可以看到声音的大小随着时间的变化,由于声道之间的空间位置不同,声音也会产生空间上的相位。在实际中,我们无法直接捕捉到的都是数字的声音信号,但是可以用各种方法将声音转化为数字信号。如模拟抽样电路(ASIC)、麦克风阵列、数字采集卡等。这些采集设备通过接收各种来源的声音信号,然后经过电路和数字处理后输出数字信号。

2.2.时域分析

时域分析(time domain analysis),又称为时频分析(spectral analysis),是指利用时间信息对声音信号进行分析、分析得到的信息包括音高、音色、语调等。时频分析中,我们可以用频谱(Spectrogram)来表示声音的频率-时间图。频谱显示出声音信号在不同频率范围内的强度分布,同时反映出不同时间下声音强度的分布。如下图所示:

上图是声音信号的频谱图。图中x轴表示时间,y轴表示频率,颜色越深表示声音强度越强,高度越高表示声音响度越高。由于声音的频谱特性,使得在某些情况下,不同的频率可以共享同样的时间变化。例如,不同音符的发音往往有相同的时间结构。所以,我们需要注意的是,时频分析不能完全准确的还原声音的时空分布。

2.3.分帧与加窗

为了提升语音识别效果,需要对语音信号进行分帧处理。分帧是指把连续语音信号按照一定的长度切割成若干个子序列。通常情况下,分帧的长度应比瀑布状的语音信号更短一些。通常情况下,帧长为20~30ms较为合适。一帧中包含多少个数据点则取决于每秒钟的采样次数。由于语音信号的时变性比较强,一旦采用固定长度的帧,可能会造成语音信号丢失,导致声学模型失效。因此,我们需要用窗口函数(Window Function)对语音信号加窗,以消除信号边界对信号处理带来的影响。

2.4.倒谱变换(Discrete Fourier Transform, DFT)

对于时域信号,我们可以使用傅里叶变换(Fourier transform)来表示频域信号。傅里叶变换是指从时域信号到频域信号的一种离散变换。频域信号是指声音信号从低频到高频的一个线性表示。例如,语音信号的各个频率成分对应的振幅大小,而频率是空间频率,时域信号是时间频率。使用DFT可以将时域信号变换为频域信号。DFT可以用如下公式表示:

其中,N 是采样点数,k 是频率的下标,x_n 是时域信号第 n 个采样值。上式表示,通过一次DFT变换,我们就可以将时域信号变换到频域信号中。

2.5.Mel滤波器组

为了表示语音信号在频域的详细信息,我们需要将频域信号转换为 mel 频率(mel scale)。在语音识别中,我们通常使用 Mel-frequency cepstral coefficients (MFCCs) 来表示频域信号。MFCC 的计算需要依赖于 Mel 滤波器组。

Mel 滤波器组是一个三角形波形,中心频率处的一阶差分等于二阶差分,两边频率处的一阶差分等于零。为了在频域表示语音信号,我们使用三角形波形作为基底,每两个相邻的三角波形之间隔半个音调,这样可以很好的保留高频信息,同时压缩低频信息。因此,我们可以通过一系列的三角波窗函数对语音信号进行加窗,然后将加窗后的信号转换到 mel 频率上。

Mel 频率刻度(Mel Scale)是一种非线性尺度,由人耳感官发现的规律。与更常用的线性频率单位一样,人类也习惯于听到高音更响亮、低音更淡的声音。但是,由于人类的听觉系统的非线性分辨能力,频率不一定是均匀的。而人类身体频率范围的变化又受限于呼吸系统的调节。因此,人们希望采用非线性尺度来描述语音信号的频率。因此,人们设计了如图所示的 Mel 频率坐标系。

上图展示了一个三角波的 Mel 频率坐标。黑色区域表示人耳感官所能辨别的频率范围。显然,人耳感官的感知范围远远超过了人们真实的音高范围,因此,我们只能尽量模拟人耳的感觉。因此,人们设计了如上图所示的 Mel 频率坐标系。

2.6.特征选择

在进行语音识别任务之前,我们应该对输入的音频信号进行预处理,包括特征提取、加窗、变换到Mel频率、添加噪声、分帧等操作。在这一过程中,我们经常会面临着很多复杂的困难,包括噪声、静音、环境干扰、方言、说话人口音等种种因素。在进行特征提取之后,我们需要对特征进行筛选和选择。一些特征如帧移(Frame Shift),窗长(Window Length)等都可以对最终结果的质量产生决定性影响。另外,特征维度的增多通常会导致欠拟合现象的发生,而减少特征维度则会引入过拟合的风险。因此,我们需要结合任务要求和训练数据情况,选择合适的特征组合。

3. 核心算法原理与具体操作步骤

3.1.特征提取

3.1.1.MFCC 特征提取

目前主流的声音特征提取算法是 MFCC (Mel Frequency Cepstral Coefficients)。在此,我们将声音信号转换为 Mel 频率上的特征,再求取自相关函数(AutoRegressive Function)来获取信息。

对于一个语音信号,我们可以先对它进行加窗处理,然后将其变换到 Mel 频率坐标系,然后计算帧移为5 ms的 MFCC 特征。通常情况下,帧移较短的 MFCC 提供了更多的特征信息,因为相邻帧的相关性较小。对于 MFCC 的计算,我们通常使用以下公式:

其中,C_m(\text{frame}) 表示第 m 个 MFCC 特征,f[n] 表示第 n 个时频采样点的值,w_t 表示加窗函数在 t 时刻的值,M 为帧长,D 为 MFCC 维度。

3.1.2.其他特征提取方法

除了 MFCC 外,我们也可以尝试其他类型的特征提取算法。例如,我们可以试试傅立叶变换(Fourier Transform)、谱包络(Spectrum Envelope)或者倒谱系数(Discrete Cosine Transform)。对于任意一种特征提取方法,我们都会需要进行特征标准化(Feature Scaling)、过滤噪声(Noise Filtering)、降维(Dimensionality Reduction)等操作。

3.2.声学模型

3.2.1.HMM-GMM 模型

对于声学模型来说,HMM-GMM (Hidden Markov Model with Gaussian Mixture Models) 是一种常见的模型。HMM-GMM 将声学特征和隐藏状态联系起来。声学特征表示声音信号的概率密度函数,而隐藏状态表示声音系统当前的状态。HMM-GMM 模型的概率公式为:

这里,λ\theta 分别表示 HMM 参数,p(o|\lambda) 表示观测序列 o 在特征空间 \lambda 下的似然函数;q 表示隐状态序列,p(q_i|\theta) 表示状态转移概率分布,p(o_t|\lambda',q) 表示发射概率分布。

3.2.2.RNN-LSTM 模型

另一种声学模型是 RNN-LSTM (Recurrent Neural Network with Long Short-Term Memory Units)。这种模型比 HMM 更加通用,可以在各种环境中表现良好。RNN-LSTM 使用 LSTM (Long Short-Term Memory Unit) 单元来学习状态转移的模式。LSTM 提供记忆能力,能够在长期内保持有效的上下文信息。RNN-LSTM 的概率公式为:

这里,λ 表示输入特征,h 表示隐状态向量,L 表示隐层的数量。h_t^{(l)} 表示第 l 层的第 t 时刻的隐状态。

3.3.解码算法

3.3.1.Viterbi 搜索算法

Viterbi 搜索算法(Viterbi Algorithm)是语音识别中常用的解码算法。在 Viterbi 搜索算法中,我们定义一个动态规划的过程,寻找给定模型参数下观测序列的最大概率路径。在每一步,我们假设模型的当前状态是 q_i ,考虑到之前的观测 o_1,..., o_{t-1} 的发射概率分布,以及到达当前状态 q_i 的转移概率分布。我们求解如下方程:

P(q_1) = P(o_1|q_1) * P(q_2|q_1)_P(o_2|q_2)_..._P(q_t|q_1)..._ P(o_t|q_t)*P(q_{t+1}|q_t)

该方程表示从初始状态到结束状态的所有可能路径的概率乘积。我们可以找到所有 q_i 的概率最大值以及它们对应的路径。最后,我们将 q_{i_n}, i_n=1,...t 作为最终的隐状态序列。

Beam Search 算法也是一种解码算法。与 Viterbi 搜索算法不同的是,Beam Search 只搜索 K 个候选结果,而不是搜索所有的结果。Beam Search 会对每个候选结果赋予一个概率,然后只保留概率最大的 K 个结果。Beam Search 的过程如下图所示:

图中,第一行表示当前候选的概率分布。每一列表示一个候选,左侧表示到达该节点的前驱节点的概率;右侧表示从该节点到达终止节点的概率乘积。Beam Search 根据某个阈值 (Threshold) 来判断是否停止搜索。如果某次搜索的累计概率平均值超过阈值,就停止搜索;否则继续搜索。

3.4.分类器

分类器(Classifier)用来对输出结果进行整合,对最终的识别结果进行修正。通常,分类器包含一个线性层和一个非线性激活函数。线性层的输入是 HMM 或 RNN-LSTM 模型的输出结果,输出是语音信号属于哪个类别的概率。非线性激活函数如 sigmoid 函数或 softmax 函数都可以。softmax 函数的输出为 [0,1] 区间内的概率值,因此可以归一化到任意概率值。

4. 具体代码实例

为了方便读者理解和复现,我会展示几个具体的代码实例。大家可以根据自己需求,选择自己熟悉的编程语言来编写代码。

4.1. 准备数据集

复制代码
    import os
    import scipy.io.wavfile as wav
    from sklearn.model_selection import train_test_split
    
    def load_data():
    data_dir = "path/to/your/dataset"
    
    # list all wave files and their labels
    filenames = []
    labels = []
    
    for subfolder in sorted(os.listdir(data_dir)):
    folder_name = os.path.join(data_dir, subfolder)
    
    if not os.path.isdir(folder_name):
    continue
    
    label = int(subfolder) - 1
    
    for filename in sorted(os.listdir(folder_name)):
    file_path = os.path.join(folder_name, filename)
    
    if not os.path.isfile(file_path):
    continue
    
    _, signal = wav.read(file_path)
    
    # do something to preprocess the audio signal here...
    
    filenames.append(signal)
    labels.append(label)
    
    X_train, X_test, y_train, y_test = train_test_split(filenames, labels, test_size=0.2, random_state=42)
    
    return X_train, X_test, y_train, y_test
    
    if __name__ == '__main__':
    X_train, X_test, y_train, y_test = load_data()
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

4.2. 构建模型

复制代码
    import torch
    import torch.nn as nn
    from torch.autograd import Variable
    
    class CNNLSTMModel(nn.Module):
    def __init__(self, num_classes=10, input_dim=161):
    super().__init__()
    
    self.cnn = nn.Sequential(
    nn.Conv2d(1, 32, kernel_size=(41,11), stride=(2,2), padding=(20,5)),
    nn.BatchNorm2d(32),
    nn.Hardtanh(inplace=True),
    nn.MaxPool2d((3,2)),
    nn.Conv2d(32, 32, kernel_size=(21,11), stride=(2,1), padding=(10,5)),
    nn.BatchNorm2d(32),
    nn.Hardtanh(inplace=True),
    nn.MaxPool2d((3,2))
    )
    
    self.rnn = nn.LSTM(input_size=97, hidden_size=128, num_layers=2, batch_first=True)
    
    self.linear = nn.Linear(in_features=128, out_features=num_classes)
    
    def forward(self, inputs):
    features = self.cnn(inputs).squeeze()
    features = features.transpose(1,2)
    outputs, _ = self.rnn(features)
    logits = self.linear(outputs[:,-1,:])
    
    return logits
    
    if __name__ == '__main__':
    model = CNNLSTMModel().cuda()
    print(model)
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

4.3. 训练模型

复制代码
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    
    for epoch in range(10):
    running_loss = 0.0
    correct = 0
    total = 0
    
    for i, data in enumerate(trainloader, 0):
    inputs, labels = data['signals'].float().unsqueeze(1), data['labels']
    inputs, labels = Variable(inputs).cuda(), Variable(labels).long().cuda()
    
    optimizer.zero_grad()
    
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels.data).sum()
    
    loss.backward()
    optimizer.step()
    
    running_loss += loss.item()
    
    accuracy = float(correct) / total
    print('Epoch %d Loss: %.3f Accuracy: %.3f%%' %
    (epoch + 1, running_loss / len(trainset), accuracy * 100))
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

4.4. 测试模型

复制代码
    correct = 0
    total = 0
    
    with torch.no_grad():
    for data in testloader:
    inputs, labels = data['signals'].float().unsqueeze(1), data['labels']
    inputs, labels = Variable(inputs).cuda(), Variable(labels).long().cuda()
    
    outputs = model(inputs)
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels.data).sum()
    
    accuracy = float(correct) / total
    print('Test Accuracy: {:.2%}'.format(accuracy))
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

5. 未来发展与挑战

语音识别系统的研究一直在蓬勃发展。近几年,随着深度学习技术的进步,语音识别领域获得了突破性的进展。由于传统的音频信号处理方法存在严重缺陷,如分辨率低、特征稀疏等,深度学习模型通常采用更高级的特征提取方法,如 MFCC、谱包络等,提升语音识别性能。深度学习模型的训练速度更快,容易收敛,因此也越来越流行。但是,仍然还有许多挑战需要解决,如如何制作高质量的数据集、如何提升模型的鲁棒性、如何扩展到新语言等。

6. 参考资料

[1]. Speech Recognition Using Deep Learning in Python by , Tutorials Point. https://www.tutorialspoint.com/speech_recognition_using_deep_learning_in_python

全部评论 (0)

还没有任何评论哟~