学习AI的过程中,难免会出现各种各样的问题。比如,什么样的模型需要什么样的环境。依赖与Python版本不兼容时怎么办。数据集如何自定义。构建模型的目的是什么。原本模型黑盒是如何训练并得以优化的,等等等等。基于这些问题,利用入门级PP ASR简单的语音模型,做一个详细的记录。
pip install --upgrade pip
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple/ numpy scipy tqdm pytest-runner librosa python-Levenshtein==0.12.2 visualdl --upgrade --user
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple/ SoundFile --upgrade --user
# 执行create_mainfest.py依赖下载
!pip uninstall -y numba
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple/ --upgrade numba
!pip uninstall -y resampy
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple/ --upgrade resampy
!pip uninstall -y librosa
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple/ --upgrade librosa
当数据集构建完成后。将所有数据进行分词。因为模型不是以音频区分的。而是将音频处理成序列的形式进行排列,最后输出时,根据生成的字典进行查找,来返回最终的结果。所以生成数据字典的目的,既是固定构建音频生成序列的规则,也是语音识别的依赖。
音频数据预处理通常包括以下步骤:
读入音频文件:使用库如 librosa、pydub 等加载音频文件,得到音频数据和采样率。
数据截取:按照需要的长度截取音频数据。例如,将音频数据切割成固定长度的语音片段。
数据增强:数据增强指对原始数据进行变换、扩充、合成等处理,以提高模型的鲁棒性和泛化能力。例如,随机速度变化、加入噪音、改变音高等方式对数据进行增强。
特征提取:通过对音频数据进行短时傅里叶变换(STFT)、梅尔频率倒谱系数(MFCC)等方式提取音频特征。特征提取是将高维的音频数据转换为低维度的特征表示。
数据标准化:标准化是为了使不同样本的特征具有可比性,减少由于特征维度大小造成的模型不稳定或低效问题。例如,将特征值归一化到 [0,1] 范围内、均值为 0,标准差为 1 等方式进行标准化。
数据划分:将数据划分为训练集、验证集和测试集,可使用库如 sklearn 等将样本划分为不同集合。
数据存储:将处理好的数据按照一定格式保存,以便在模型训练和测试时使用。
下面是数据预处理工具类 data.py
注意! 需要前面的环境才可以使用
import json
import wave
import librosa
import numpy as np
import soundfile
from paddle.io import Dataset
# 加载二进制音频文件,转成短时傅里叶变换
def load_audio_stft(wav_path, mean=None, std=None):
with wave.open(wav_path) as wav:
wav = np.frombuffer(wav.readframes(wav.getnframes()), dtype="int16").astype("float32")
stft = librosa.stft(wav, n_fft=255, hop_length=160, win_length=200, window="hamming")
spec, phase = librosa.magphase(stft)
spec = np.log1p(spec)
if mean is not None and std is not None:
spec = (spec - mean) / std
return spec
# 读取音频文件转成梅尔频率倒谱系数(MFCCs)
def load_audio_mfcc(wav_path, mean=None, std=None):
wav, sr = librosa.load(wav_path, sr=16000)
mfccs = librosa.feature.mfcc(y=wav, sr=sr, n_mfcc=128, n_fft=512, hop_length=128).astype("float32")
if mean is not None and std is not None:
mfccs = (mfccs - mean) / std
return mfccs
# 改变音频采样率为16000Hz
def change_rate(audio_path):
data, sr = soundfile.read(audio_path)
if sr != 16000:
# librosa最新版本在调用参数时必须明确指定参数名,避免出现参数传递错误
data = librosa.resample(y=data, orig_sr=sr, target_sr=16000)
soundfile.write(audio_path, data, samplerate=16000)
# 音频数据加载器
class PPASRDataset(Dataset):
def __init__(self, data_list, dict_path, mean=None, std=None, min_duration=0, max_duration=-1):
super(PPASRDataset, self).__init__()
self.mean = mean
self.std = std
# 获取数据列表
with open(data_list, 'r', encoding='utf-8') as f:
lines = f.readlines()
self.data_list = []
for line in lines:
line = json.loads(line)
# 跳过超出长度限制的音频
if line["duration"] < min_duration:
continue
if max_duration != -1 and line["duration"] > max_duration:
continue
self.data_list.append([line["audio_path"], line["text"]])
# 加载数据字典
with open(dict_path, 'r', encoding='utf-8') as f:
labels = eval(f.read())
self.vocabulary = dict([(labels[i], i) for i in range(len(labels))])
def __getitem__(self, idx):
# 分割音频路径和标签
wav_path, transcript = self.data_list[idx]
# 读取音频并转换为梅尔频率倒谱系数(MFCCs)
mfccs = load_audio_mfcc(wav_path, self.mean, self.std)
# 将字符标签转换为int数据
transcript = list(filter(None, [self.vocabulary.get(x) for x in transcript]))
transcript = np.array(transcript, dtype='int32')
return mfccs, transcript
def __len__(self):
return len(self.data_list)
# 对一个batch的数据处理
def collate_fn(batch):
# 找出音频长度最长的
batch = sorted(batch, key=lambda sample: sample[0].shape[1], reverse=True)
freq_size = batch[0][0].shape[0]
max_audio_length = batch[0][0].shape[1]
batch_size = len(batch)
# 找出标签最长的
batch_temp = sorted(batch, key=lambda sample: len(sample[1]), reverse=True)
max_label_length = len(batch_temp[0][1])
# 以最大的长度创建0张量
inputs = np.zeros((batch_size, freq_size, max_audio_length), dtype='float32')
labels = np.zeros((batch_size, max_label_length), dtype='int32')
input_lens = []
label_lens = []
for x in range(batch_size):
sample = batch[x]
tensor = sample[0]
target = sample[1]
seq_length = tensor.shape[1]
label_length = target.shape[0]
# 将数据插入都0张量中,实现了padding
inputs[x, :, :seq_length] = tensor[:, :]
labels[x, :label_length] = target[:]
input_lens.append(seq_length)
label_lens.append(len(target))
input_lens = np.array(input_lens, dtype='int64')
label_lens = np.array(label_lens, dtype='int64')
return inputs, labels, input_lens, label_lens
PPASR模型是一个只使用卷积层的模型,并没有使用更加复杂的RNN模型,以下就是使用PaddlePaddle实现的一个语音识别模型。使用动态图自定义网络模型非常简单。
在语音识别的过程中,构建模型的目的是为了将语音信号转化为文字(文本)信息。这个过程可以分为两个主要步骤:语音特征提取和声学模型训练。
在语音特征提取过程中,将来自音频数据的语音信号转化为机器学习算法能够处理的形式,例如用梅尔频率倒谱系数(Mel Frequency Cepstral Coefficients,MFCCs)等特征来描述语音信号。
在声学模型训练过程中,使用大量的带有标注的语音数据来训练语音识别模型。常用的模型包括隐马尔可夫模型(Hidden Markov Model,HMM)、深度学习模型等。机器学习算法通过学习这些带有标注的语音样本的特征,来建立模型,以将音频信号转化为文字(文本)信息。
因此,构建模型的目的是为了使机器能够对人类语音进行自动识别,实现语音识别应用,例如语音助手、语音输入等。
原本的黑盒指的是模型的内部结构和工作流程难以被理解和解释的情况。在语音识别的过程中,原本的黑盒主要存在于模型内部,由于模型参数众多且相互作用复杂,难以解释和理解模型内部的具体工作流程。
在构建的模型中,原本的黑盒逐渐被打破,因为随着机器学习技术的发展,一些新的方法和技术被引入来提高模型的解释性和可理解性。例如,利用深度神经网络模型中的可视化方法来可视化模型的神经网络结构,可以更好地理解模型的工作原理。同时,通过对模型进行逐层解析,可以逐步了解模型对不同特征以及语音信号的处理方式。
总之,在构建的模型中,原本的黑盒逐渐被打破,通过不断的优化和改进,模型的解释性和可理解性得到了提高,使得我们更加深入地理解语音识别的工作原理。
所以,原本的模型黑盒,我们就可以将它看做成一个已经存在的智能AI对象。
import paddle
import paddle.nn as nn
from paddle.nn.initializer import KaimingNormal
# 门控线性单元 Gated Linear Units (GLU)
class GLU(nn.Layer):
def __init__(self, axis):
super(GLU, self).__init__()
self.sigmoid = nn.Sigmoid()
self.axis = axis
def forward(self, x):
a, b = paddle.split(x, num_or_sections=2, axis=self.axis)
act_b = self.sigmoid(b)
out = paddle.multiply(x=a, y=act_b)
return out
# 基本卷积块
class ConvBlock(nn.Layer):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding=0, p=0.5):
super(ConvBlock, self).__init__()
self.conv = nn.Conv1D(in_channels, out_channels, kernel_size, stride, padding, weight_attr=KaimingNormal())
self.conv = nn.utils.weight_norm(self.conv)
self.act = GLU(axis=1)
self.dropout = nn.Dropout(p)
def forward(self, x):
x = self.conv(x)
x = self.act(x)
x = self.dropout(x)
return x
# PPASR模型
class PPASR(nn.Layer):
def __init__(self, vocabulary, data_mean=None, data_std=None, name="PPASR"):
super(PPASR, self).__init__(name_scope=name)
# 数据均值和标准值到模型中,方便以后推理使用
if data_mean is None:
data_mean = paddle.to_tensor(1.0)
if data_std is None:
data_std = paddle.to_tensor(1.0)
self.register_buffer("data_mean", data_mean, persistable=True)
self.register_buffer("data_std", data_std, persistable=True)
# 模型的输出大小,字典大小+1
self.output_units = len(vocabulary) + 1
self.conv1 = ConvBlock(128, 500, 48, 2, padding=97, p=0.2)
self.conv2 = ConvBlock(250, 500, 7, 1, p=0.3)
self.conv3 = ConvBlock(250, 2000, 32, 1, p=0.3)
self.conv4 = ConvBlock(1000, 2000, 1, 1, p=0.3)
self.out = nn.utils.weight_norm(nn.Conv1D(1000, self.output_units, 1, 1))
def forward(self, x, input_lens=None):
x = self.conv1(x)
for i in range(7):
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.out(x)
if input_lens is not None:
return x, paddle.to_tensor(input_lens / 2 + 1, dtype='int64')
return x
import Levenshtein as Lev
import paddle
class GreedyDecoder(object):
def __init__(self, vocabulary, blank_index=0):
self.int_to_char = dict([(i, c) for (i, c) in enumerate(vocabulary)])
self.blank_index = blank_index
# 给定一个数字序列列表,返回相应的字符串
def convert_to_strings(self, sequences, sizes=None, remove_repetitions=False, return_offsets=False):
strings = []
offsets = [] if return_offsets else None
for x in range(len(sequences)):
seq_len = sizes[x] if sizes is not None else len(sequences[x])
string, string_offsets = self.process_string(sequences[x], seq_len, remove_repetitions)
strings.append([string])
if return_offsets:
offsets.append([string_offsets])
if return_offsets:
return strings, offsets
else:
return strings
# 获取字符,并删除重复的字符
def process_string(self, sequence, size, remove_repetitions=False):
string = ""
offsets = []
sequence = sequence.numpy()
for i in range(size):
char = self.int_to_char[sequence[i].item()]
if char != self.int_to_char[self.blank_index]:
# 是否删除重复的字符
if remove_repetitions and i != 0 and char == self.int_to_char[sequence[i - 1].item()]:
pass
else:
string = string + char
offsets.append(i)
return string, paddle.to_tensor(offsets, dtype='int64')
def cer(self, s1, s2):
"""
通过计算两个字符串的距离,得出字错率
"""
s1, s2, = s1.replace(" ", ""), s2.replace(" ", "")
return Lev.distance(s1, s2)
def decode(self, probs, sizes=None):
"""
解码,传入结果的概率解码得到字符串,删除序列中的重复元素和空格。
"""
max_probs = paddle.argmax(probs, 2)
strings, offsets = self.convert_to_strings(
max_probs,
sizes,
remove_repetitions=True,
return_offsets=True)
return strings, offsets