主流语音任务
- 语音数据读取基本原则
直接保存语音会将该对象保存在内存中(Dataset类调用__getitem__方法)
所以一般保存这些数据的存储路径文档(表单)而不是数据的直接copy(不然占用内存太大了)
通常用numpy将数据保存为.npy格式(比如保存mel谱)
提取表单(获取文本文档)
- 从语音文件提取出特征数据文件夹
- 以数据文件夹目录作为输入,提取出表单的同时做出一定的筛选按
- 以表单作为参数,写出符合任务要求的torch.utils.DataSet类
def generate_scp_dataset(dataset_dir):
with open('Train_Scp.txt','a',encoding='utf-8' ) as txtf :
for dirname,subdirs,files in os.walk(dataset_dir):
for f in files:
if f.split('.')[-1] == 'npy':
txtf.write(os.path.join(dirname,f) + "\n")
print("写入表单")
如果需要筛选语音,建议把筛选代码写到这个函数中,这也可以保证特征数据集是不会发生变化的
- 比较常见的筛选:
- 去掉比较短的语音
- 去掉比较长的语音
- 筛选某个人的语音或者某几类需要的语音
特征提取
- 函数将语音文件夹提取特征到另一个文件夹的同文件目录结构下。由“XXX.wav”命名为"XXX.npy"
- 提取时,对语音做出一定的预处理,比如静音消除等
# 这里用from pathlib import Path # 代替os库 处理路径问题
wavpaths = [x for x in src_wavp.rglob('*.wav') if x.is_file() ]
# 如果要写os版本的代码,文件一定要以.wav结尾,否则会报错
ttsum = len(wavpaths) # 总语音数量
mel_frames = []
k=0
for wp in wavpaths:
k+=1
the_wavpath = str(wp.reslove())
the_wavpath = str(wp.reslove().replace(hp.wav_datadir_name, hp.feature_dir_name).replace('wav','npy')
wavform,_ = librosa.load(the_wavpath)
wavform,_ = librosa.effects.trim(wavform, top_db=20) # 静音消除
wavform = torch.FloatTensor(wavform).unsequeeze(0)
mel = stftfunc.mel_spectrogram(wavform)
mel = mel.sequeeze().detach().cpu().numpy()
np.save(the_melpath, mel)
构建DataSet类
pytorch提供了DataSet类,可以当成列表
-
基础知识
- AI分类任务中,pytorch的神经网络模型通常接受一个[B,D,T]的张量,输出语音每个类别的概率[B, Num_class]
(B:几条语音,D:多少维,T:多少帧) - 以mel谱为例,每条语音提取成melspec特征后,变成一个[80, 帧数]的矩阵
- AI分类任务中,pytorch的神经网络模型通常接受一个[B,D,T]的张量,输出语音每个类别的概率[B, Num_class]
-
矛盾:不同的语音,长度不同,那不同帧数的矩阵,如何拼接成一个三维矩阵呢?
希望的数据集和神经网络希望的输入有矛盾,无法打包成[B,D,T]张量
还有如何从数据集中读取矩阵
一个不会出bug的DataLoader数据读取过程
但是实际数不是整齐的{[80,100],[80,100],[80,100],[80,100]},而是如{[80,335],[80,317]}这种,是不能被DataLoader直接打包的,会报错
需要进行处理,不同任务有不同处理
说话人识别任务的DataSet.getitem
数据处理与两点有关
- 数据处理与任务的机器学习目的有关
m a x P ( C ∣ x 1 , x 2 , . . . , x T ) max \ P(C|{x_1,x_2,...,x_T}) max P(C∣x1,x2,...,xT) 最大化一段序列的类别条件概率
花括号中是mel谱,这里表示T帧的mel谱
在mel谱给定的条件下,判别为C的概率 - 与数据本身的性质有关
- 时长分布
- 静音
- 信噪比
- 远场?近场?
- 是否人类语音分类?还是动物语音分类或者其他?
得到的结论
- 该条件概率与T无关
无论一个人发出多长、多短、什么内容的声音,模型都应该能够正确判别该语音的类别 - 训练的时候,根据不同的模型,可以有不同的getitem方式
有的模型可以接受不同长度的矩阵,给出相同维度的分类概率输出(GSTmodel)
有的模型则不能,如经典的卷积结构的神经网络,Resnet系列,以及一切基于卷积神经网络的模型
有的模型则是输入多少长度,就输出多少长度的隐矩阵,如RNN模型以及一切基于自回归的模型(如transform,不过tf不是子回归模型,但是他推理的时候是自回归的推理)
- eg.
考虑最简单的情况(如卷积神经网络):接受一个固定输入维度,输出分类概率的模型
则原语音维度不管长短,都要变成[80,L]的维度。L在每个batch的训练中是一个固定的数字,也就是一个超参数
如何确定超参数->看数据集的时长分布图
256:从图上看,256可以包含大部分数据
确定数据长度后如何写getitem——两种padding方法
1. 固定长度的padding
- Padding(将补零和随机截断,称为Padding)
- 遇到比较短的语音,将长度补0到256(接上面的例子) ->补零
- 遇到比较长的语音,随机截取片段,作为256帧的代表片段 ->随即截断
这样可以保证在训练过程中可以覆盖95%+的数据
Padding的代码很大程度上决定模型训练的效果
## 下面两个函数 ,实现,将一个二维矩阵 补零到 指定长度。 (补一列一列的零). 如果超过指定的seglen,则切掉多余的。
def pad(x, seglen, mode='wrap'):
pad_len = seglen - x.shape[1] # seglen:目标长度;pad_len:要补的长度
y = np.pad(x, ((0,0), (0,pad_len)), mode=mode)
return y
def segment(x, seglen=128):
'''
:param x: npy形式的mel [80,L]
:param seglen: padding长度
:return: padding mel
'''
## 该函数将melspec [80,len] ,padding到固定长度 seglen
if x.shape[1] < seglen: # 语音小
y = pad(x, seglen)
elif x.shape[1] == seglen:# 语音等
y = x
else:
r = np.random.randint(x.shape[1] - seglen) ## r : [0- (L-128 )],原长-seglen后随机取一个数
y = x[:,r:r+seglen]
return y
tacotron2式的padding:对每个batch进行最大序列长度padding
项目地址
相关代码在TextMelCollate类中,核心代码在__call__中
其他任务的padding方式
代码
import torch
import numpy as np
import os
from torch.utils.data import Dataset,DataLoader
# 提取表单
def generate_scp_dataset(dataset_dir):
with open('Train_Scp.txt','a',encoding='utf-8' ) as txtf :
for dirname,subdirs,files in os.walk(dataset_dir):
for f in files:
if f.split('.')[-1] == 'npy':
txtf.write(os.path.join(dirname,f) + "\n")
print("写入表单")
## 下面两个函数 ,实现,将一个二维矩阵 补零到 指定长度。 (补一列一列的零). 如果超过 指定的seglen,则切掉多余的。
def pad(x, seglen, mode='wrap'):
pad_len = seglen - x.shape[1]
y = np.pad(x, ((0,0), (0,pad_len)), mode=mode)
return y
def segment(x, seglen=256):
'''
:param x: npy形式的mel [80,L]
:param seglen: padding长度
:return: padding mel
'''
## 该函数将melspec [80,len] ,padding到固定长度 seglen
if x.shape[1] < seglen:
y = pad(x, seglen)
elif x.shape[1] == seglen:
y = x
else:
r = np.random.randint(x.shape[1] - seglen) ## r : [0- (L-128 )]
y = x[:,r:r+seglen]
return y
class MeldataSet_1(Dataset): # 继承pytorch的DataSet
def __init__(self,scp_dir,seglen): # scp_dir文本文档
self.scripts = []
self.seglen = seglen
with open(scp_dir,encoding='utf-8') as f :
for l in f.readlines():
self.scripts.append(l.strip('\n'))
self.L = len((self.scripts))
pass
def __getitem__(self,index): # 根据用户给定的索引取数据
src_path = self.scripts[index]
src_mel = np.load(src_path)## 从硬盘将数据 读入内存的一个io过程。.npy
src_mel = segment(np.load(src_path), seglen=self.seglen) # 加上padding操作
return torch.FloatTensor(src_mel) ## 【80,256】 padding后出来的数据维度统一,这样后续就可以dataloader了
def __len__(self):
return self.L
pass
def my_collection_way(batch): ## batch : tuple
print("Dataloder 中的 collection func 调用:")
print([ x.shape for x in batch]) # 此时帧数不一样
output = torch.stack([ torch.FloatTensor(segment(x,seglen=256)) for x in batch ],dim=0)# dim:对第0个维度stack
return output
pass
if __name__ == '__main__':
#generate_scp_dataset("meldata_22k_trimed")
''''
# ############################################################
# #### padding与不padding的 dataloader 演示。
Mdata = MeldataSet_1("./Audio/Train_Scp.txt",seglen=256)
print(Mdata[0].shape) ## 进行索引操作的时候,就是在调用 getitem (index)
print(Mdata[1].shape)
print("-----------")
# 输出torch.size([80,335]) torch.size([80,317])
# 如何打包成为三维矩阵
# pytorch提供打包接口:DataLoader(帮助打包的类)
#
# ## 包装这个dataset,成为datalodaer
Mdataloader = DataLoader(Mdata, batch_size=3)
for batch in Mdataloader:
print(batch.shape) ## [3,80,256]
print("-----")
# ## 为什么要读取一批数据呢 ?-> 显卡 处理一批数据 ,速度比较cpu快的~
# exit()
############################################################
############################################################
'''
### 演示collection——fn
Mdata = MeldataSet_1("Train_Scp.txt",seglen=256)
Mdataloader = DataLoader(Mdata, batch_size=3,collate_fn=my_collection_way) # batch规定一次3个数据,去除数据过程中就要调用collate_function(收集数据的函数)
for batch in Mdataloader:
print(batch.shape)
exit()
############################################################