任务介绍
- 数据预处理:从原始波形中提取MFCC特征(已经由TAs完成了!)
- 分类:使用预先提取的MFCC特征进行帧级音素分类
来自语音的帧级音素预测
音素:一种语言中的一种语音单位,可以区分一个词。
数据预处理
将数据转换成声学特征MFCC(下载数据集已完成处理)
数据集
训练集: 4268个预处理音频特征与标签(共2644158帧)
测试集: 1078个预处理音频特征(共646268帧)
标签: 41个类,每个类代表一个音素
每个.pt文件从一个原始的wav文件中提取的,使用torch.load()读取.pt文件作为torch张量,每个张量都有一个形状为(T,39)
Code
标题准备数据
Helper用于从每个话语的原始MFCC特征预处理训练数据。
一个音素可以跨越几个帧,并且依赖于过去和未来的帧。因此,我们将相邻的音素连接起来进行训练,以实现更高的精度。
concat_filt函数连接过去和未来的k帧(总共2k+1=n帧),我们预测中心帧。
可以随意修改数据预处理函数,但不要丢弃任何帧。
import os
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):
if n < 0:
left = x[0].repeat(-n, 1)
right = x[:n]
elif n > 0:
right = x[-1].repeat(n, 1)
left = x[n:]
else:
return x
return torch.cat((left, right), dim=0)
def concat_feat(x, concat_n):
assert concat_n % 2 == 1 # n must be odd
if concat_n < 2:
return x
seq_len, feature_dim = x.size(0), x.size(1)
x = x.repeat(1, concat_n)
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
mode = 'train' if (split == 'train' or split == 'val') else 'test'
label_dict = {}
if mode != 'test':
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[idx: idx + cur_len, :] = feat
if mode != 'test':
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
定义数据集
init:接收输入的特征数据X和可选的标签数据y。将特征数据存储在self.data中,并将标签数据转换为torch.LongTensor类型后存储在self.label中。如果没有提供标签数据,则将self.label设置为None。
getitem:用于获取指定索引idx处的数据样本。如果存在标签数据,将同时返回特征数据和对应的标签数据。如果没有标签数据,仅返回特征数据。
len:返回数据集的长度,即样本数量。
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)
定义模型
定义了一个简单的神经网络模型,包含两个类:BasicBlock和Classifier。
BasicBlock是一个基本的网络块,它包含一个线性层(nn.Linear)和一个ReLU激活函数。它的输入维度为input_dim,输出维度为output_dim。
Classifier是一个分类器模型,它由多个BasicBlock组成。构造函数中的参数包括输入维度input_dim、输出维度output_dim(默认为41)、隐藏层的数量hidden_layers(默认为1)和隐藏层的维度hidden_dim(默认为256)。在构造函数中,首先创建了一个包含一个BasicBlock的线性层(nn.Linear),然后根据hidden_layers参数的值,使用循环创建了多个BasicBlock。最后,通过一个线性层得到最终的输出。
在forward方法中,输入的数据经过网络的前向传播。首先将输入数据传递给self.fc,它是一个包含多个BasicBlock的顺序模块(nn.Sequential)。然后,通过前向传播得到输出结果。
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
def __init__(self, input_dim, output_dim):
super(BasicBlock, self).__init__()
self.block = nn.Sequential(
nn.Linear(input_dim, output_dim),
nn.ReLU(),
)
def forward(self, x):
x = self.block(x)
return x
class Classifier(nn.Module):
def __init__(self, input_dim, output_dim=41, hidden_layers=1, hidden_dim=256):
super(Classifier, self).__init__()
self.fc = nn.Sequential(
BasicBlock(input_dim, hidden_dim),
*[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
超参数
# data prarameters
concat_nframes = 1 # the number of frames to concat with, n must be odd (total 2k+1 = n frames)
train_ratio = 0.8 # the ratio of data used for training, the rest will be used for validation
# training parameters
seed = 0 # random seed
batch_size = 512 # batch size
num_epoch = 5 # the number of training epoch
learning_rate = 0.0001 # learning rate
model_path = './model.ckpt' # the path where the checkpoint will be saved
# model parameters
input_dim = 39 * concat_nframes # the input dim of the model, you should not change the value
hidden_layers = 1 # the number of hidden layers
hidden_dim = 256 # the hidden dim
准备数据集和模型
实现了数据预处理,并创建了训练集和验证集的数据加载器
import gc
# preprocess data
train_X, train_y = preprocess_data(split='train', feat_dir='./libriphone/feat', phone_path='./libriphone', concat_nframes=concat_nframes, train_ratio=train_ratio)
val_X, val_y = preprocess_data(split='val', feat_dir='./libriphone/feat', phone_path='./libriphone', concat_nframes=concat_nframes, train_ratio=train_ratio)
# get dataset
train_set = LibriDataset(train_X, train_y)
val_set = LibriDataset(val_X, val_y)
# remove raw feature to save memory
del train_X, train_y, val_X, val_y
gc.collect()
# get dataloader
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False)
对于训练集:
共有41个音素类别(phone classes)。
训练集中的语音片段数目为3428。
最后两行表示训练集的形状信息:特征数据(features)的形状为[2116368, 39],表示2116368个样本,每个样本有39个特征;标签数据(labels)的形状为[2116368],表示2116368个样本的标签。
对于验证集:
共有41个音素类别(phone classes)。
验证集中的语音片段数目为858。
最后两行输出表示验证集的形状信息:特征数据(features)的形状为[527790, 39],表示527790个样本,每个样本有39个特征;标签数据(labels)的形状为[527790],表示527790个样本的标签。
定义一个函数same_seeds(seed),用于设置随机种子以保证结果的可复现性。
import numpy as np
#fix seed
def same_seeds(seed):
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
# fix random seed
same_seeds(seed)
# create model, define a loss function, and optimizer
model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
训练
首先,通过一个循环迭代num_epoch次,进行训练和验证的过程。
在训练过程中:
将模型设置为训练模式,使用model.train()。
使用train_loader迭代训练数据集中的批次数据。
将特征数据和标签数据移动到设备上,并将梯度置零。
将特征数据输入模型,获取模型的输出。
根据输出和标签计算训练损失。
执行反向传播和参数更新。
计算训练准确率和训练损失。
在验证过程中:
如果存在验证集(len(val_set) > 0):
将模型设置为评估模式,使用model.eval()。
使用val_loader迭代验证数据集中的批次数据。
将特征数据和标签数据移动到设备上,并禁用梯度计算。
将特征数据输入模型,获取模型的输出。
根据输出和标签计算验证损失。
计算验证准确率和验证损失。
打印每个epoch的训练准确率、训练损失、验证准确率和验证损失。
如果验证准确率提高,则保存当前模型的权重到指定的model_path路径。
最后,如果没有验证集(len(val_set) == 0),则在最后一个epoch结束后保存模型的权重到指定的model_path路径。
打印每个epoch的训练准确率、训练损失、验证准确率和验证损失,并保存在验证准确率最高时的模型权重。如果没有验证集,则仅打印训练准确率和训练损失,并保存最后一个epoch的模型权重。
best_acc = 0.0
for epoch in range(num_epoch):
train_acc = 0.0
train_loss = 0.0
val_acc = 0.0
val_loss = 0.0
# training
model.train() # set the model to training mode
for i, batch in enumerate(tqdm(train_loader)):
features, labels = batch
features = features.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(features)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
_, train_pred = torch.max(outputs, 1) # get the index of the class with the highest probability
train_acc += (train_pred.detach() == labels.detach()).sum().item()
train_loss += loss.item()
# validation
if len(val_set) > 0:
model.eval() # set the model to evaluation mode
with torch.no_grad():
for i, batch in enumerate(tqdm(val_loader)):
features, labels = batch
features = features.to(device)
labels = labels.to(device)
outputs = model(features)
loss = criterion(outputs, labels)
_, val_pred = torch.max(outputs, 1)
val_acc += (val_pred.cpu() == labels.cpu()).sum().item() # get the index of the class with the highest probability
val_loss += loss.item()
print('[{:03d}/{:03d}] Train Acc: {:3.6f} Loss: {:3.6f} | Val Acc: {:3.6f} loss: {:3.6f}'.format(
epoch + 1, num_epoch, train_acc/len(train_set), train_loss/len(train_loader), val_acc/len(val_set), val_loss/len(val_loader)
))
# if the model improves, save a checkpoint at this epoch
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), model_path)
print('saving model with acc {:.3f}'.format(best_acc/len(val_set)))
else:
print('[{:03d}/{:03d}] Train Acc: {:3.6f} Loss: {:3.6f}'.format(
epoch + 1, num_epoch, train_acc/len(train_set), train_loss/len(train_loader)
))
# if not validating, save the last epoch
if len(val_set) == 0:
torch.save(model.state_dict(), model_path)
print('saving model at last epoch')
释放训练集和验证集的数据加载器(train_loader和val_loader)以及进行垃圾回收
del train_loader, val_loader
gc.collect()
测试
载测试集的数据
# load data
test_X = preprocess_data(split='test', feat_dir='./libriphone/feat', phone_path='./libriphone', concat_nframes=concat_nframes)
test_set = LibriDataset(test_X, None)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)
加载之前保存的模型
# load model
model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
model.load_state_dict(torch.load(model_path))
开始预测
test_acc = 0.0
test_lengths = 0
pred = np.array([], dtype=np.int32)
model.eval()
with torch.no_grad():
for i, batch in enumerate(tqdm(test_loader)):
features = batch
features = features.to(device)
outputs = model(features)
_, test_pred = torch.max(outputs, 1) # get the index of the class with the highest probability
pred = np.concatenate((pred, test_pred.cpu().numpy()), axis=0)
将预测写入CSV文件
with open('prediction.csv', 'w') as f:
f.write('Id,Class\n')
for i, y in enumerate(pred):
f.write('{},{}\n'.format(i, y))