【AI】PaddlePaddle实现自动语音识别

文档背景

学习AI的过程中,难免会出现各种各样的问题。比如,什么样的模型需要什么样的环境。依赖与Python版本不兼容时怎么办。数据集如何自定义。构建模型的目的是什么。原本模型黑盒是如何训练并得以优化的,等等等等。基于这些问题,利用入门级PP ASR简单的语音模型,做一个详细的记录。

安装环境

Python版本

在这里插入图片描述

pip环境

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

项目目录结构

在这里插入图片描述

数据准备

  1. 可以直接使用官方提供的训练数据集。
    在这里插入图片描述
  2. 也可以直接使用自定义数据集
    在这里插入图片描述

生成数据字典

当数据集构建完成后。将所有数据进行分词。因为模型不是以音频区分的。而是将音频处理成序列的形式进行排列,最后输出时,根据生成的字典进行查找,来返回最终的结果。所以生成数据字典的目的,既是固定构建音频生成序列的规则,也是语音识别的依赖。

数据预处理

音频数据预处理通常包括以下步骤:

  1. 读入音频文件:使用库如 librosa、pydub 等加载音频文件,得到音频数据和采样率。

  2. 数据截取:按照需要的长度截取音频数据。例如,将音频数据切割成固定长度的语音片段。

  3. 数据增强:数据增强指对原始数据进行变换、扩充、合成等处理,以提高模型的鲁棒性和泛化能力。例如,随机速度变化、加入噪音、改变音高等方式对数据进行增强。

  4. 特征提取:通过对音频数据进行短时傅里叶变换(STFT)、梅尔频率倒谱系数(MFCC)等方式提取音频特征。特征提取是将高维的音频数据转换为低维度的特征表示。

  5. 数据标准化:标准化是为了使不同样本的特征具有可比性,减少由于特征维度大小造成的模型不稳定或低效问题。例如,将特征值归一化到 [0,1] 范围内、均值为 0,标准差为 1 等方式进行标准化。

  6. 数据划分:将数据划分为训练集、验证集和测试集,可使用库如 sklearn 等将样本划分为不同集合。

  7. 数据存储:将处理好的数据按照一定格式保存,以便在模型训练和测试时使用。

下面是数据预处理工具类 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

总结

  1. 训练的过程和评估详情可以查看此链接: https://aistudio.baidu.com/aistudio/projectdetail/5902290?forkThirdPart=1
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
随着人工智能和自然语言处理技术的不断发展,语音识别技术已经变得越来越普遍,并在日常生活和工作中起着越来越重要的作用。虽然现在的语音识别技术已经可以基本准确地将人们的语音转化为文本,但是其中常常缺少标点符号,导致人们在阅读时感到困惑。 为了解决这一问题,我们可以利用 PaddlePaddle 框架中的自然语言处理模型,给语音识别文本进行标点符号的添加。 首先,我们需要将语音识别文本转化为适合自然语言处理的文本格式,包括去除多余的重复词语、补全缩写、纠正拼写等。然后,我们可以使用 PaddlePaddle 框架中的分词器对文本进行分词,将文本划分为基本的语义单元。接着,我们可以利用 PaddlePaddle 中的标点生成器模型,对每个语义单元进行判断,是否应该加上标点符号。最后,将加上标点符号的文本输出为最终的识别结果。 需要注意的是,标点符号的添加要尽可能地符合语法规则和语境,以保证输出的文本准确、易读、易懂。此外,为了提高模型的准确性,我们还可以利用大量的语音识别文本数据进行训练和调优,以优化模型的性能和稳定性。 总之,通过利用 PaddlePaddle 框架中的自然语言处理模型,给语音识别文本加上标点符号可以有效提高文本的可读性和可理解性,对于提高语音识别技术的应用价值和广泛应用具有重要的意义。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值