从零搭建——基于HMM-GMM的语音识别模型构建
HMM-GMM(Hidden Markov Model - Gaussian Mixture Model)是语音识别中的经典模型之一。它结合了隐马尔可夫模型(HMM)和高斯混合模型(GMM)的优点,用于建模语音信号的时间序列特性和观测值的概率分布。
原理
隐马尔可夫模型(HMM)
隐马尔可夫模型(HMM, Hidden Markov Model)是一种统计模型,用于描述一个系统通过一系列隐状态(hidden states)生成观测数据(observations)的过程。在HMM中,我们假设:
- 系统在任何时刻 的状态 是一个离散的随机变量,取值范围为 。
- 在时刻 的状态 只依赖于前一时刻的状态 (即一阶马尔可夫性质)。
- 在时刻 的观测 只依赖于当前的状态 。
-
状态集合 :表示系统可能的状态。
-
观测集合 :表示系统可能的观测值。
-
状态转移概率矩阵 :表示从状态 转移到状态 的概率。
-
观测概率矩阵 :表示在状态 下生成观测值 的概率。
-
初始状态概率向量 :表示系统初始处于状态 的概率。
高斯混合模型(GMM)
高斯混合模型(GMM, Gaussian Mixture Model)是一种概率模型,用于表示一个由多个高斯分布(正态分布)混合而成的复杂分布。在语音识别中,我们通常使用GMM来建模在某一状态下的观测概率。
一个GMM由以下参数定义:
- 混合权重 ,其中 表示第 个高斯分量的权重,满足 。
- 高斯分量的均值向量 和协方差矩阵 。
GMM的概率密度函数(pdf)表示为:
其中 表示第 个高斯分量的pdf,其形式为:
其中 是观测向量 的维数。
对于每个状态,其观测概率 可表示为多个高斯分布的线性组合:
其中, 是混合权重,满足 ; 是均值为 、协方差为 的多维高斯分布。
HMM-GMM语音识别模型构建
1. 数据预处理
1.1 语音信号分帧
语音信号是一个连续的时间序列,首先需要将其分割成短时帧。每帧通常包含10到30毫秒的语音数据,并且相邻帧之间有部分重叠(通常为50%重叠)。
假设语音信号为 ,我们可以用一个滑动窗口函数 对其分帧:
其中 是帧移。
1.2 特征提取
对每一帧提取特征向量,常用的方法是梅尔频率倒谱系数(MFCC)或线性预测倒谱系数(LPCC)。
以MFCC为例,其计算步骤如下:
-
傅里叶变换:计算每一帧的快速傅里叶变换(FFT):
其中 是帧长, 是频率索引。
-
梅尔滤波器组:应用梅尔滤波器组,将频域信号转换到梅尔频率尺度。
-
对数能量:对每个滤波器输出取对数能量。
-
离散余弦变换(DCT) :对对数能量进行离散余弦变换,得到MFCC。
2. 模型构建
2.1 HMM模型结构
选择适当的HMM结构,例如每个音素用一个HMM模型表示,每个HMM有多个状态(通常是3到5个状态)。
2.2 GMM参数
在每个HMM状态下,用GMM表示观测概率密度函数。设第 个状态的观测概率密度函数为:
其中 是高斯分量的个数, 是第 个高斯分量的权重, 是均值向量, 是协方差矩阵。
3. 模型训练
3.1 Baum-Welch算法
使用Baum-Welch算法(EM算法)来训练HMM-GMM模型。具体步骤如下:
E步(期望步) : 计算前向概率 和后向概率 :
计算状态概率 和状态转移概率 :
M步(最大化步) : 更新模型参数 :
4. 语言模型与字典
4.1 语言模型
语言模型(LM, Language Model)用来描述词序列的概率分布,常用的是n-gram模型。n-gram模型的形式为:
4.2 字典
字典(Lexicon)是词汇表,包括每个词的发音。每个词由音素序列表示。
5. 解码
解码是将观测序列转化为词序列的过程。通常使用Viterbi算法结合语言模型进行解码。
5.1 Viterbi算法
Viterbi算法用于寻找最可能的状态序列 :
其递推关系为:
最终路径通过回溯得到:
5.2 结合语言模型的解码
在解码过程中,结合语言模型对路径概率进行调整。设语言模型的概率为 ,则解码目标为:
可以使用加权组合的方式:
其中 是调节参数。
系统实现
要求:
-
Python 3 并安装相应库
-
kenlm 语言模型安装应用
-
CMU 发音词典
-
LibriSpeech 数据集训练
-
Linux OS
-
数据预处理:
-
分帧并提取MFCC特征。
-
将特征存储为文件以便后续使用。
使用
python_speech_features库提取梅尔频率倒谱系数(MFCC)特征:
-
import os
from scipy.io import wavfile
import numpy as np
from python_speech_features import mfcc
from sklearn.preprocessing import StandardScaler
import soundfile as sf
import pickle
def extract_mfcc_from_flac(file_path, numcep=13):
signal, sample_rate = sf.read(file_path)
mfcc_features = mfcc(signal, samplerate=sample_rate, numcep=numcep)
return mfcc_features
def get_all_flac_files(data_dir):
flac_files = []
for root, dirs, files in os.walk(data_dir):
for file in files:
if file.endswith('.flac'):
flac_files.append(os.path.join(root, file))
return flac_files
def load_librispeech_data(data_dir):
flac_files = get_all_flac_files(data_dir)
mfcc_features_list = [extract_mfcc_from_flac(file) for file in flac_files]
return mfcc_features_list
LIBRISPEECH_DIR = '/path/to/LibriSpeech/train-clean-100/'
mfcc_features_list = load_librispeech_data(LIBRISPEECH_DIR)
# 保存 mfcc_features_list 到文件
with open('mfcc_features_list.pkl', 'wb') as f:
pickle.dump(mfcc_features_list, f)
# 加载特征和长度信息
X = np.concatenate(mfcc_features_list)
lengths = [len(x) for x in mfcc_features_list]
-
语言模型与字典:
-
加载预训练的语言模型(KenLM)。
import kenlm
# 加载预训练的语言模型
lm = kenlm.Model('3-gram.pruned.1e-7.arpa')
- 加载CMU发音词典,并生成状态到音素的映射。
# 加载CMU发音词典
def load_cmudict(file_path):
cmudict = {}
phonemes = set()
with open(file_path, 'r', encoding='ISO-8859-1') as f:
for line in f:
if line.startswith(';;;'):
continue
parts = line.strip().split(' ')
word = parts[0]
phoneme_seq = parts[1].split()
cmudict[word] = phoneme_seq
phonemes.update(phoneme_seq)
return cmudict, list(phonemes)
# 提取音素集合
cmudict, phonemes = load_cmudict('cmudict-0.7b')
# 生成状态到音素的映射
def generate_state_to_phoneme_mapping(phonemes):
state_to_phoneme = {i: phoneme for i, phoneme in enumerate(phonemes)}
return state_to_phoneme
state_to_phoneme = generate_state_to_phoneme_mapping(phonemes)
-
模型构建:
- 初始化HMM模型,设置状态数和混合高斯数。
from hmmlearn import hmm
# 设置模型参数
num_mixtures = 3 # 初始混合数,可以根据需要调整
num_states = len(phonemes) # 使用音素的数量作为状态数
# 初始化并训练HMM-GMM模型
model = hmm.GMMHMM(n_components=num_states, n_mix=num_mixtures, covariance_type='diag', n_iter=200, init_params='mct')
-
模型训练:
- 使用Baum-Welch算法训练HMM-GMM模型。
model.fit(X, lengths)
-
解码:
- 使用Viterbi算法结合语言模型进行解码。
# 改进后的解码函数
def decode(features, hmm_model, lm_model, cmudict, state_to_phoneme):
log_likelihood, state_sequence = hmm_model.decode(features, algorithm='viterbi')
phoneme_sequence = [state_to_phoneme[state] for state in state_sequence]
best_sequence = []
phoneme_str = ' '.join(phoneme_sequence)
word_candidates = []
for word, phonemes in cmudict.items():
phoneme_pattern = ' '.join(phonemes)
if phoneme_pattern in phoneme_str:
word_candidates.append((word, phoneme_pattern))
word_candidates.sort(key=lambda x: len(x[1]), reverse=True)
used_phonemes = set()
for word, phoneme_pattern in word_candidates:
if all(phoneme in phoneme_str for phoneme in phoneme_pattern.split()) and not used_phonemes.intersection(phoneme_pattern.split()):
best_sequence.append(word)
used_phonemes.update(phoneme_pattern.split())
sequence_str = ' '.join(best_sequence)
score = lm_model.score(sequence_str)
return best_sequence, score
# 进行解码
decoded_sequence, decoded_score = decode(mfcc_features_list[0], model, lm, cmudict, state_to_phoneme)
print("Decoded sequence:", decoded_sequence)
print("Decoded score:", decoded_score)
补充
KenLM语言模型
KenLM 是一个用于快速语言模型训练和查询的工具,特别适用于自然语言处理和语音识别任务。
1. 安装 KenLM
安装依赖项: 在安装 KenLM 之前,需要确保系统上安装了一些必要的依赖项。可以使用以下命令安装:
sudo apt-get update
sudo apt-get install build-essential cmake libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-test-dev libboost-all-dev
克隆 KenLM 仓库: 从 GitHub 克隆 KenLM 的源码:
git clone https://github.com/kpu/kenlm.git
cd kenlm
编译 KenLM:
mkdir build
cd build
cmake ..
make -j4
-j4 选项用于并行编译,可以根据 CPU 核心数进行调整。
2. 确认编译成功及使用
lmplz是否可用
首先,请确认 KenLM 已成功编译。检查 build/bin 目录下是否有可执行文件 lmplz。
cd ~/ASR/kenlm/build/bin
ls
样例1:
cd ~/ASR/kenlm/build/bin
./lmplz -o 5 < ../../data.txt > ../../model.arpa
-
bin/lmplz是可执行文件的路径。 -
-o 5指定了 n-gram 模型的阶数,这里是 5-gram 模型。 -
< data.txt表示将data.txt文件的内容作为输入。 -
> model.arpa表示将输出写入model.arpa文件。
样例2:
cd ~/ASR/kenlm/build/bin
echo "hello world hello world hello" | ./lmplz -o 5 --discount_fallback > ../../model.arpa
检查 build_binary 可执行文件
确认 build_binary 可执行文件是否存在:
ls ~/ASR/kenlm/build/bin
如果 build_binary 文件存在,可以运行以下命令将 ARPA 模型转换为二进制格式:
cd ~/ASR/kenlm/build/bin
./build_binary ../../model.arpa ../../model.binary
使用 query 工具查询模型
如果 query 工具存在,可以用以下命令查询模型:
cd ~/ASR/kenlm/build/bin
./query ../../model.binary
回到kenlm文件夹,选择需要的python环境进行安装
python setup.py install
安装完成后可以在vscode 中 调用 kenlm
import kenlm
# 加载二进制模型
model = kenlm.Model('path/to/model.binary')
# 查询句子的对数概率
sentence = "hello world"
log_prob = model.score(sentence)
print(f"Log probability: {log_prob}")
随后,在OpenSLR上选择一个简单的预训练语言模型:openslr.org
下载这个 3-gram 语言模型(3-gram.pruned.1e-7.arpa.gz)
下载完成后解压,转成kenlm二进制格式:
build/bin/build_binary 3-gram.pruned.1e-7.arpa 3-gram.pruned.1e-7.binary
在python中加载:
# 加载二进制格式的语言模型
lm = kenlm.Model('path/to/3-gram.pruned.1e-7.binary')
# 或者加载 ARPA 文件
# lm = kenlm.Model('path/to/3-gram.pruned.1e-7.arpa')
CMU发音词典
由于语音识别的最终结果应该是对应的文字而不是数字序列。为了将解码后的状态序列映射到文字,需要使用一个词典或发音词典(Lexicon)将HMM的状态或音素序列转换为文字。
在此处下载CMU发音词典(一个英文发音词典,广泛用于语音识别和合成):The CMU Pronouncing Dictionary
下载这个简单的词典:CMUdict/cmudict-0.7b at master · Alexir/CMUdict · GitHub
使用这个词典的demo:
def load_cmudict(file_path):
cmudict = {}
phonemes = set()
with open(file_path, 'r', encoding='ISO-8859-1') as f:
for line in f:
if line.startswith(';;;'):
continue
parts = line.strip().split(' ')
word = parts[0]
phoneme_seq = parts[1].split()
cmudict[word] = phoneme_seq
phonemes.update(phoneme_seq)
return cmudict, list(phonemes)
# 提取音素集合
cmudict, phonemes = load_cmudict('cmudict-0.7b')
# 打印示例词的发音
print(cmudict.get('HELLO', 'Word not found'))
使用 load_cmudict 函数加载 CMU 发音词典,并将其存储在 cmudict 字典中。
LibriSpeech数据集
在语音识别的研究和应用中,常用的大规模开源数据集可以显著提升模型的性能和稳定性。这些数据集通常包含大量的多样化语音样本,涵盖不同的说话人、环境和背景噪音,能够为模型提供丰富的训练数据。
常用开源数据集
以下是一些主流的开源语音数据集,适用于训练和评估语音识别系统:
-
LibriSpeech:
- 描述:包含1000小时的英语语音数据,源自LibriVox的有声读物。
- 链接:LibriSpeech ASR Corpus
-
Common Voice:
- 描述:由Mozilla组织的众包语音数据集,包含多种语言和方言的语音数据。
- 链接:Common Voice
-
TED-LIUM:
- 描述:包含从TED演讲中提取的音频和相应的文本转录。
- 链接:TED-LIUM Corpus
-
WSJ (Wall Street Journal) :
- 描述:包含朗读自《华尔街日报》的语音数据,是语音识别领域的经典数据集。
- 链接:WSJ Corpus
LibriSpeech数据集的使用
从 LibriSpeech 数据集页面 下载 train-clean-100.tar.gz。这个数据集包含100小时的干净语音,适合用于初步模型的训练。
关于该数据集的解压与使用,我写了一个脚本用于处理,递归遍历
train-clean-100目录中的所有子目录,找到所有的音频文件并提取它们的MFCC特征,然后将这些特征存储到一个列表中。:
tar -xzvf train-clean-100.tar.gz
import os
from scipy.io import wavfile
import numpy as np
from python_speech_features import mfcc
from hmmlearn import hmm
from sklearn.preprocessing import StandardScaler
import soundfile as sf
import kenlm
#定义处理音频文件的函数
def extract_mfcc_from_flac(file_path, numcep=13):
# 使用soundfile库读取flac文件
signal, sample_rate = sf.read(file_path)
# 提取MFCC特征
mfcc_features = mfcc(signal, samplerate=sample_rate, numcep=numcep)
return mfcc_features
#定义递归函数来获取所有音频文件
def get_all_flac_files(data_dir):
flac_files = []
for root, dirs, files in os.walk(data_dir):
for file in files:
if file.endswith('.flac'):
flac_files.append(os.path.join(root, file))
return flac_files
#加载数据并提取MFCC特征
def load_librispeech_data(data_dir):
flac_files = get_all_flac_files(data_dir)
mfcc_features_list = [extract_mfcc_from_flac(file) for file in flac_files]
return mfcc_features_list
通过上述方式,得到了该训练集内的音频数据的MFCC特征,用于训练HMM-GMM模型。