当前位置: 首页 > article >正文

机器学习作业:HW2分类(Phoneme Classification音素分类)代码详解

MFCC特征:MFCC特征提取是语音识别自然语言处理领域中常用的一种技术,它的全称是梅尔频率倒谱系数(Mel-scale Frequency Cepstral Coefficients)。MFCC特征提取通过对音频信号的处理和分析,提取出反映语音特征的信息,广泛应用于语音识别、语音合成、说话人识别等领域。

MFCC特征提取的过程一般包括以下步骤:

  1. 读取音频文件,进行预处理,包括数字化、预滤波、预加重、分帧等操作;
  2. 进行快速傅里叶变换,将音频信号从时域转换为频域;
  3. 通过梅尔滤波器组对频谱进行处理,提取出反映语音特征的信息;
  4. 进行对数运算和离散余弦变换等数学运算,进一步提取出反映语音特征的信息;
  5. 提取动态特征,即求取倒谱系数的差分等操作;
  6. 将提取出的MFCC特征向量进行归一化处理,以便于后续的比较和分析。

2.1 下载数据

#这里主要是云服务器的一种写法  命令前加!
#查看当前GPU资源(确保有GPU运行)
!nvidia-smi
#下载数据
!pip install --upgrade gdown

# Main link
!gdown --id '1o6Ag-G3qItSmYhTheX6DYiuyNzWyHyTc' --output libriphone.zip

!unzip -q libriphone.zip
!ls libriphone

2.2 Preparing Data

Helper functions to pre-process the training data from raw MFCC features of each utterance.

A phoneme may span several frames and is dependent to past and future frames.
Hence we concatenate neighboring phonemes for training to achieve higher accuracy. The concat_feat function concatenates past and future k frames (total 2k+1 = n frames), and we predict the center frame.

Feel free to modify the data preprocess functions, but do not drop any frame (if you modify the functions, remember to check that the number of frames are the same as mentioned in the slides)

用于对于每个音频的MFCC特征进行数据预处理。

一个音素可能跨越多个帧,并且依赖于过去和未来的帧。因此,我们将相邻的音素进行拼接以提高训练的准确性。concat_feat函数会拼接过去和未来的k帧(总共2k+1 = n帧),我们预测的是中心帧。

随意修改数据预处理函数,但不要丢掉任何帧(如果你修改了函数,请记得检查帧的数量是否与幻灯片中提到的一样)。

import os
# 导入random模块,用于生成随机数和执行随机操作  
import random
import pandas as pd
import torch
# 显示进度条
from tqdm import tqdm

def load_feat(path):
    feat = torch.load(path)
    return feat

def shift(x, n):
  # 该函数适用于对音频和序列数据的预处理,比如此处在提取MFCC特征之前进行数据位置的调整
  # 对输入数据x进行位移操作(假设是一个多维张量,例如音频信号在时间或者帧维度上的表示)
  # 位移的大小和方向由参数n决定的
  # n<0 则向左位移 
    if n < 0:
      # 这里假设 x 的第一个维度是时间或帧,x[0] 取出第一帧的数据,
      # torch.Tensor有两个实例方法可以用来扩展某维的数据的尺寸,分别是repeat()和expand():
      # 扩展(expand)张量不会分配新的内存,只是在存在的张量上创建一个新的视图(view),一个大小(size)等于1的维度扩展到更大的尺寸。
      # 沿着特定的维度重复这个张量,和expand()不同的是,这个函数拷贝张量的数据,这也解释了下面填充重复帧以进行位移的可行性。
      # 然后使用 .repeat(-n, 1) 方法重复这帧数据 -n 次(注意 -n 因为 n 是负数,所以实际上是将绝对值 |n| 次)。
      # 这样做是为了在左侧填充足够的重复帧以进行位移。
        left = x[0].repeat(-n, 1)
      # 从 x 中取出前 -n(即绝对值 |n|)帧的数据作为右侧部分。
        right = x[:n]

    elif n > 0:
      # x[-1]:这部分代码表示获取张量x的最后一个元素(或最后一个维度上的所有元素,具体取决于x的维度)。
      # 如果x是一个一维张量,那么x[-1]就是它的最后一个元素;如果x是一个多维张量,那么x[-1]表示的是最后一个维度上的所有元素组成的张量
      # (比如,如果x的形状是(a, b, c),那么x[-1]的形状会是(a, b),假设最后一个维度是c)。
      # .repeat(n, 1):这是一个方法调用,用于将张量沿着指定的维度重复。
      # 这里,n和1分别表示沿着第一个维度(通常是行的方向)和第二个维度(通常是列的方向)重复的次数。
      # 因此,如果x[-1]是一个形状为(a, b)的张量,那么x[-1].repeat(n, 1)的结果是一个形状为(n*a, b)的张量,即x[-1]在第一个维度上被重复了n次。
        right = x[-1].repeat(n, 1)
      # x[n:]:这部分代码表示从张量x的第n个元素(或第n个维度上的元素,具体取决于x的维度)开始,直到x的末尾,截取一个子张量。
      # 如果x是一维的,那么x[n:]就是x从第n个元素到最后一个元素组成的子张量;如果x是多维的,那么x[n:]表示的是从第n个元素(在第一个维度上)开始,到x的末尾,所有元素组成的子张量。
        left = x[n:]
    else:
        return x
    # 将左侧和右侧的数据在第一维度上拼接
    return torch.cat((left, right), dim=0)

def concat_feat(x, concat_n):
  # assert断言是声明其布尔值必须为真的判定,如果发生异常就说明表达示为假。 用来测试表示式,其返回值为假,就会触发异常。
    assert concat_n % 2 == 1 # n must be odd
    if concat_n < 2:
      # 如果只有一个数据则不需要拼接
        return x
    # 获取x的序列长度和特征维度
    seq_len, feature_dim = x.size(0), x.size(1)
    x = x.repeat(1, concat_n) 
    # reshape 做维度转换
    x = x.view(seq_len, concat_n, feature_dim).permute(1, 0, 2) # concat_n, seq_len, feature_dim
    # //是整除运算符,它用于执行除法运算后只取结果的整数部分
    mid = (concat_n // 2)
    for r_idx in range(1, mid+1):
        x[mid + r_idx, :] = shift(x[mid + r_idx], r_idx)
        x[mid - r_idx, :] = shift(x[mid - r_idx], -r_idx)
    # 恢复形状
    return x.permute(1, 0, 2).view(seq_len, concat_n * feature_dim)

def preprocess_data(split, feat_dir, phone_path, concat_nframes, train_ratio=0.8, train_val_seed=1337):
    class_num = 41 # NOTE: pre-computed, should not need change
    # 根据split的值设置mode
    mode = 'train' if (split == 'train' or split == 'val') else 'test'
    # 初始化标签字典
    # 字典是一种可变容易模型,且可存储任意类型对象 字典的每个键值对(key=>value)都是用冒号:分割,每个键值对之间用逗号,分割,整个字典包括在花括号{}
    label_dict = {}
    if mode != 'test':
      # os.path.join()函数用于路径拼接文件路径,可以传入多个路径
      phone_file = open(os.path.join(phone_path, f'{mode}_labels.txt')).readlines()

      for line in phone_file:
          # 去除换行符并分割字符串 
          line = line.strip('\n').split(' ')
          # 将标签转换为整数列表并存储到字典中 
          label_dict[line[0]] = [int(p) for p in line[1:]]

    if split == 'train' or split == 'val':
        # split training and validation data
        usage_list = open(os.path.join(phone_path, 'train_split.txt')).readlines()
        random.seed(train_val_seed)
        # 将一个列表中的元素打乱顺序
        random.shuffle(usage_list)
        # 设置训练集的比例
        percent = int(len(usage_list) * train_ratio)
        # 
        usage_list = usage_list[:percent] if split == 'train' else usage_list[percent:]
    elif split == 'test':
        usage_list = open(os.path.join(phone_path, 'test_split.txt')).readlines()
    else:
        raise ValueError('Invalid \'split\' argument for dataset: PhoneDataset!')

    # 去除换行符
    usage_list = [line.strip('\n') for line in usage_list]
    # 打印数据信息
    print('[Dataset] - # phone classes: ' + str(class_num) + ', number of utterances for ' + split + ': ' + str(len(usage_list)))

    max_len = 3000000
    # 初始化特征张量
    X = torch.empty(max_len, 39 * concat_nframes)
    if mode != 'test':
      y = torch.empty(max_len, dtype=torch.long)
    # 初始化索引
    idx = 0
    for i, fname in tqdm(enumerate(usage_list)):
        feat = load_feat(os.path.join(feat_dir, mode, f'{fname}.pt'))
        # 提取当前特征长度
        cur_len = len(feat)
        # 对特征进行拼接处理
        feat = concat_feat(feat, concat_nframes)
        if mode != 'test':
          # 将标签转换为张量(非测试模式)
          label = torch.LongTensor(label_dict[fname])
        # 将特征存储到X中 
        X[idx: idx + cur_len, :] = feat
        if mode != 'test':
          # 将标签存储在y里
          y[idx: idx + cur_len] = label
        # 更新索引
        idx += cur_len
    # 截取有效特征部分
    X = X[:idx, :]
    if mode != 'test':
      # 截取有效标签部分
      y = y[:idx]

    print(f'[INFO] {split} set')
    print(X.shape)
    if mode != 'test':
      print(y.shape)
      return X, y
    else:
      return X

2.3 Define Dataset

import torch
from torch.utils.data import Dataset
from torch.utils.data import DataLoader

class LibriDataset(Dataset):
    def __init__(self, X, y=None):
        self.data = X
        if y is not None:
            self.label = torch.LongTensor(y)
        else:
            self.label = None

    def __getitem__(self, idx):
        if self.label is not None:
            return self.data[idx], self.label[idx]
        else:
            return self.data[idx]

    def __len__(self):
        return len(self.data)

2.4 Define Model

import torch
# 从torch.nn模块导入nn,该模块包含了构建神经网络所需的层和模块  
import torch.nn as nn
# 从torch.nn.functional模块导入F,该模块包含了用于构建神经网络的函数式接口  
import torch.nn.functional as F

# 定义一个基本的神经网络块(BasicBlock),它继承自nn.Module(所有神经网络模块的基类)  
class BasicBlock(nn.Module):
    # 初始化方法,接受输入维度(input_dim)和输出维度(output_dim)作为参数  
    def __init__(self, input_dim, output_dim):
        # 调用父类nn.Module的初始化方法 
        super(BasicBlock, self).__init__()
        # 定义一个序列模型(Sequential),它按照添加的顺序包含多个模块  
        # 在这个BasicBlock中,它首先包含一个线性层(Linear),然后是ReLU激活函数 
        self.block = nn.Sequential(
            # 线性层,将输入维度映射到输出维度
            nn.Linear(input_dim, output_dim),
            # ReLU激活函数,增加非线性 
            nn.ReLU(),
        )
    # 前向传播方法,定义了数据通过网络的方式  
    def forward(self, x):
        # 将输入x通过定义好的block进行处理  
        x = self.block(x)
        return x

# 定义分类器(Classifier)类,同样继承自nn.Module  
class Classifier(nn.Module):
    # 初始化方法,接受输入维度(input_dim),输出维度(output_dim,默认为41),
    # 隐藏层层数(hidden_layers,默认为1),和隐藏层维度(hidden_dim,默认为256)作为参数  
    def __init__(self, input_dim, output_dim=41, hidden_layers=1, hidden_dim=256):
        super(Classifier, self).__init__()
        # 定义一个序列模型(Sequential),用于构建整个分类器的前向传播路径  
        # 它首先包含一个BasicBlock,其输入维度为input_dim,输出维度为hidden_dim  
        # 然后根据hidden_layers的数量,添加相应数量的BasicBlock,每个Block的输入和输出维度都是hidden_dim  
        # 最后,添加一个线性层,将hidden_dim映射到output_dim  
        self.fc = nn.Sequential(
            BasicBlock(input_dim, hidden_dim),
            # 生成一个由 hidden_layers 个 BasicBlock构成的列表;*用来调取列表中内容
            *[BasicBlock(hidden_dim, hidden_dim) for _ in range(hidden_layers)],
            nn.Linear(hidden_dim, output_dim)
        )

    def forward(self, x):
        x = self.fc(x)
        return x

进一步理解


*[BasicBlock(hidden_dim, hidden_dim) for _ in range(hidden_layers)],
    # 或者,更简洁地,使用加号来合并序列(如果不需要星号解包的话,但这里需要)  
    # 由于我们不能直接在nn.Sequential中使用加号来合并多个序列,我们通常依赖nn.Sequential自身来处理多个参数  
    # 因此,在这个上下文中,星号是必要的(如果我们选择使用列表推导式的话),以确保列表被正确解包为单独的参数 
    
 *(BasicBlock(hidden_dim, hidden_dim) for _ in range(hidden_layers)),  # 使用生成器表达式,仍然需要星号解包  
    # 或者,更简单地,不使用任何特殊结构,直接列出所有层(如果数量不是动态的话)  
    # BasicBlock(hidden_dim, hidden_dim),  
    # BasicBlock(hidden_dim, hidden_dim),  
    # ...(重复上述行,直到达到hidden_layers的数量)  
    # 但这是不实际的,因为我们是动态地根据hidden_layers的数量来添加层的  
  1. for _ in range(hidden_layers): 这是一个循环,它会迭代hidden_layers次。_是一个惯用的占位符,表示循环变量在循环体中不会被使用。range(hidden_layers)生成一个从0到hidden_layers-1的整数序列。
  2. BasicBlock(hidden_dim, hidden_dim): 对于循环中的每次迭代,都会创建一个新的BasicBlock实例。这个实例的输入维度和输出维度都被设置为hidden_dim
  3. [...]: 方括号表示这是一个列表推导式,它会根据循环和条件生成一个列表。在这个例子中,生成的列表将包含多个BasicBlock实例。
  4. *: 星号在这里用作解包操作符。然而,在这个上下文中,它实际上可能是多余的,除非这行代码是作为另一个列表或nn.Sequential模型的一部分被直接包含的。**如果它是nn.Sequential中的一个元素,并且前面还有其他元素,那么星号会确保这个列表被解包成单独的参数,而不是作为一个单一的列表参数。**但是,如果这是列表推导式单独的一行,或者它是作为nn.Sequential中的唯一元素(或最后一个元素,且后面没有逗号分隔的其他元素),则星号是不必要的。

http://www.kler.cn/news/359776.html

相关文章:

  • 引领企业数字化未来:物联网与微服务架构的深度融合之道
  • 用户界面设计:视觉美学与交互逻辑的融合
  • (46)MATLAB仿真从正弦波转换为方波
  • 【重拾算法第一天】质数约数欧拉筛 埃氏筛GCD
  • NoSQL 简介
  • [枚举坤坤]二进制枚举基础
  • 【WPF】中Binding的应用
  • (已开源-ECCV2024)BEV检测模型-LabelDistill,使用真值进行知识蒸馏
  • QT关闭界面后退出线程
  • docker 数据管理,数据持久化详解 一
  • dfs排列数字(新手)c++
  • 基序对酶特异性功能的影响-文献精读67
  • 虚拟现实辅助工程技术在现代汽车制造中的重要性
  • CentOS系统Nginx的安装部署
  • HashMap如何处理Hash碰撞
  • PHP爬虫:获取数据的入门详解
  • ArcGIS 最新底图服务地址
  • git 免密的方法
  • CANoe_C#如何调用CANoe的诊断
  • jmeter学习(8)界面的使用