目录
一、数据集介绍
1、数据来源
2、数据预处理与格式
二、数据处理方面代码分析
1、下载数据集
2、为全项目过程设置随机数种子
3、预处理相关函数定义
(1)第一个函数:加载特征数据 load_feat
(2)第二个函数:移位操作 shift
(3)第三个函数:拼接特征 concat_feat
(4)第四个函数:声音数据文件预处理函数 preprocess_data
三、分类模型框架的搭建
1、创建自己的数据集类
2、创建分类模型
3、模型的超参数
4、Dataloader的创建
5、开始训练
6、总结与优化建议
总结:
优化建议:
四、模型使用过程
一、数据集介绍
1、数据来源
数据集: 文件中使用的是 LibriSpeech 数据集中的一个子集,即 train-clean-100。
-
训练集: 包含 3429 个预处理后的音频样本,所有样本共计 2116794 帧。
-
测试集: 包含 857 个预处理后的音频样本,共计 527364 帧。
注意:测试数据没有标注,因此主要用于最终评估,需要在 Kaggle 上提交结果。
2、数据预处理与格式
-
特征提取:
-
采用了 MFCCs(Mel Frequency Cepstral Coefficients)作为声学特征,经过 CMVN(Cepstral Mean and Variance Normalization)归一化。
-
每个语音样本保存为一个
.pt
文件,利用torch.load()
可加载为一个张量,形状为(T, 39)
,其中T 是帧数,每一帧有 39 维的特征。所有音素共有41个类别。
-
-
上下文帧拼接:
-
因为单独一帧(25ms 的时长)很难包含完整的音素信息,故一般会把相邻的若干帧进行拼接。例如,选取前后共 11 帧,拼接后维度为
11 * 39 = 117
。(实际代码中是选取前后共 3 帧,受超参数 concat_nframes 调控) -
这种拼接方式使得每次输入模型的向量能够携带更丰富的上下文信息,从而提高音素识别的准确性。
-
二、数据处理方面代码分析
1、下载数据集
!pip install --upgrade gdown# Main link
# !gdown --id '1N1eVIDe9hKM5uiNRGmifBlwSDGiVXPJe' --output libriphone.zip
!gdown --id '1qzCRnywKh30mTbWUEjXuNT2isOCAPdO1' --output libriphone.zip!unzip -q libriphone.zip
!ls libriphone
逐句分析:
(1)安装或升级 gdown
!pip install --upgrade gdown
-
作用说明:
这行代码用于通过 pip 安装或升级 Python 包gdown
。 -
逐句分析:
-
!
:在 Jupyter Notebook 中使用!
表示执行 shell 命令,而不是 Python 代码。 -
pip install --upgrade gdown
:-
pip install
:使用 pip 包管理器安装一个 Python 包。 -
--upgrade
:这个参数确保如果系统中已经安装了gdown
,则会将其升级到最新版本。 -
gdown
:这是一个专门用于从 Google Drive 下载文件的 Python 包。
-
-
(2)下载文件
!gdown --id '1qzCRnywKh30mTbWUEjXuNT2isOCAPdO1' --output libriphone.zip
-
作用说明:
此命令使用gdown
下载 Google Drive 上的文件。 -
逐句分析:
-
!gdown
:调用 shell 命令gdown
。 -
--id '1qzCRnywKh30mTbWUEjXuNT2isOCAPdO1'
:-
--id
参数后面跟随的是 Google Drive 文件的唯一标识符(ID)。 -
该 ID 表示需要下载的具体文件。
-
-
--output libriphone.zip
:-
--output
参数指定下载后的文件名,这里文件会被保存为libriphone.zip
。
-
-
(3)解压文件
!unzip -q libriphone.zip
-
作用说明:
这条命令用于解压刚刚下载的libriphone.zip
压缩文件。 -
逐句分析:
-
!unzip
:调用系统中的unzip
工具来解压文件。 -
-q
:这是unzip
的一个参数,表示 "quiet"(静默模式),即解压过程中不显示详细的信息。 -
libriphone.zip
:这是要被解压的压缩文件名称。
-
(4)查看解压后的目录内容
!ls libriphone
-
作用说明:
这行命令用于列出目录libriphone
中的所有文件和文件夹,帮助用户确认解压是否成功及目录内容结构。 -
逐句分析:
-
!ls
:调用 Unix/Linux 命令ls
来列出目录中的文件。 -
libriphone
:指定要列出内容的目录名称,这里假定解压操作会在当前工作目录下生成一个名为libriphone
的文件夹。
-
2、为全项目过程设置随机数种子
import numpy as np
import torch
import randomdef same_seeds(seed):random.seed(seed) np.random.seed(seed) torch.manual_seed(seed)if torch.cuda.is_available():torch.cuda.manual_seed(seed)torch.cuda.manual_seed_all(seed) torch.backends.cudnn.benchmark = Falsetorch.backends.cudnn.deterministic = True
逐句分析:
(1)导入所需的模块
import numpy as np
import torch
import random
(2)函数定义
def same_seeds(seed):
定义一个名为 same_seeds
的函数,该函数为全项目过程设置随机数种子。它接收一个参数 seed
,用于设置随机种子,从而确保实验过程中的随机性操作可重现。
(3)在 Python 标准库中设置随机种子
random.seed(seed)
使用 Python 的 random
模块设置随机种子。设置后,所有使用 random
模块生成的随机数序列将是可重复的,有助于实验结果的再现性。
(4)为 NumPy 设置随机种子
np.random.seed(seed)
调用 NumPy 提供的 np.random.seed
方法来设置随机种子。这样后续所有使用 NumPy 随机模块的函数(比如 np.random.rand
、np.random.randint
等)生成的随机数都会按照固定的序列进行生成,以实现结果可复现。
(5)为 PyTorch 设置 CPU 随机种子
torch.manual_seed(seed)
使用 torch.manual_seed
函数为 PyTorch 设置 CPU 的随机数种子。这确保在 CPU 运算中生成的随机数(如模型初始化、数据打乱等)是一致的,方便结果复现。
(6)检查并设置 CUDA 随机种子(若可用)
if torch.cuda.is_available():torch.cuda.manual_seed(seed)torch.cuda.manual_seed_all(seed)
整体作用:
-
这段代码首先使用
torch.cuda.is_available()
检查当前系统是否有可用的 CUDA GPU。若存在,则对 GPU 随机数生成进行设置。
-
逐句分析:
-
if torch.cuda.is_available():
-
判断当前是否可检测到 GPU 硬件的可用性。
-
-
torch.cuda.manual_seed(seed)
-
为当前 GPU 设置随机种子,这会影响到使用特定 GPU 的操作。
-
-
torch.cuda.manual_seed_all(seed)
-
如果有多个 GPU,这个函数将为所有的 GPU 设置相同的随机种子,确保分布式或多 GPU 环境下的随机操作也能复现。
-
-
(7)设置 PyTorch 的 cuDNN 后端参数以实现结果可重复性
torch.backends.cudnn.benchmark = Falsetorch.backends.cudnn.deterministic = True
-
作用与解释:
-
torch.backends.cudnn.benchmark = False
-
该设置关闭 cuDNN 后端的基准测试模式。cuDNN 默认会根据网络结构和硬件自动寻找最优算法,以提高训练速度,但这可能引入非确定性(即同一代码多次运行的结果可能略有不同)。
-
-
torch.backends.cudnn.deterministic = True
-
将这个参数设置为 True,可以强制使用确定性的算法,确保每次运行时的结果一致。
-
-
注意:
-
在一些情况下,可能会牺牲部分性能换取结果确定性。这个设置特别适用于需要严格复现实验结果的场景。
-
-
3、预处理相关函数定义
(1)第一个函数:加载特征数据 load_feat
1.1 导入必要的模块
import os
import torch
from tqdm import tqdm
1.2 加载特征数据
# 这个函数用于加载存储在指定路径 (path) 的特征数据。
def load_feat(path):feat = torch.load(path)return feat
-
def load_feat(path):
-
定义了函数名
load_feat
,接收参数path
,它是存储数据文件的路径。
-
-
feat = torch.load(path)
-
调用
torch.load
来加载指定路径的文件。这个函数能自动识别存储的对象格式(如张量、模型等),将文件中的数据加载到变量feat
中。
-
-
return feat
-
返回加载得到的特征数据,使得该函数调用处可以获得并使用这些数据。
-
(2)第二个函数:移位操作 shift
# 这个函数用于对输入张量 x 进行元素的“移位”操作。
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 xreturn torch.cat((left, right), dim=0)
-
函数说明:
-
函数
shift
用于对输入的二维张量x
进行移位操作,根据参数n
的值(正数、负数或 0)来决定具体的移位方式。
-
-
逐句解析:
-
def shift(x, n):
-
定义
shift
函数,参数包括张量x
和整型移位参数n
。
-
-
if n < 0:
-
当移位值
n
为负数时,说明需要将张量向左(向前)“移动”:-
left = x[0].repeat(-n, 1)
-
x[0]
取出张量x
的第一行。 -
.repeat(-n, 1)
将这行重复-n
次(因为 n 是负数,所以取负值),重复的方向只在第一维(行)上,第二维(列)保持原状。 -
作用:产生若干行填充在移位后的前部。
-
-
right = x[:n]
-
切片
x[:n]
表示取从开头到倒数第-n
行(注意:在 Python 中当 n 为负数时,切片会忽略最后几行)。 -
作用:保留剩余部分作为右侧补充。
-
-
-
-
elif n > 0:
-
当移位值
n
为正数时,表示需要将张量向右(向后)“移动”:-
right = x[-1].repeat(n, 1)
-
x[-1]
取出张量x
的最后一行。 -
.repeat(n, 1)
将这行重复n
次,形成右侧补充的部分。
-
-
left = x[n:]
-
切片
x[n:]
表示从第 n 行开始(从 0 开始计数)到最后一行。 -
作用:保留移位后的左侧部分。
-
-
-
-
else:
-
如果
n == 0
的情况,说明不需要移位,则直接返回原始张量。
-
-
return torch.cat((left, right), dim=0)
-
使用
torch.cat
将两个部分 (left
和right
) 在行(第 0 维)上拼接起来,形成移位后的新张量。
-
-
(3)第三个函数:拼接特征 concat_feat
# 该函数用于将输入的特征数据 x 进行拼接处理,并对数据进行移位操作。
def concat_feat(x, concat_n):assert concat_n % 2 == 1 # n must be oddif concat_n < 2:return xseq_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_dimmid = (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)
-
函数说明:
-
concat_feat
函数的作用是对输入的二维特征数据x
进行上下文拼接。拼接的目标是将原始特征扩展为多个上下文帧,同时对拼接的部分进行适当的移位处理。这在很多语音识别或时间序列建模中常用于数据的上下文扩充。
-
-
逐句解析:
-
def concat_feat(x, concat_n):
-
定义函数
concat_feat
,参数x
为输入张量(通常是二维 [序列长度, 特征维度]),concat_n
指定拼接的数量,即采集多少上下文帧。
-
-
assert concat_n % 2 == 1 # n must be odd
-
使用断言确保
concat_n
为奇数。 -
理由:通常拼接的上下文帧中,中间那一帧代表当前帧,两侧需要对称地各有一定数量的前帧和后帧,所以总帧数必须为奇数。
-
-
if concat_n < 2:
-
判断如果拼接帧数小于 2(实际上只有 1 帧,等价于原始特征),则不做拼接,直接返回输入张量。
-
-
seq_len, feature_dim = x.size(0), x.size(1)
-
获取输入张量
x
的形状信息:-
seq_len
:序列长度,即帧数。 -
feature_dim
:单帧特征的维度。
-
-
-
x = x.repeat(1, concat_n)
-
使用
repeat
方法将每一行的特征向量沿列方向复制concat_n
次。 -
结果形状从
[seq_len, feature_dim]
变为[seq_len, feature_dim * concat_n]
,不过目前还是没有分离出各个上下文帧。
-
-
x = x.view(seq_len, concat_n, feature_dim).permute(1, 0, 2) # concat_n, seq_len, feature_dim
-
第一部分
x.view(seq_len, concat_n, feature_dim)
将重复后的张量重新形状为[seq_len, concat_n, feature_dim]
,把拼接的部分分组为多个“帧”块。 -
第二部分
.permute(1, 0, 2)
改变张量的维度顺序,使得形状变为[concat_n, seq_len, feature_dim]
。 -
目的:将所有的上下文帧数据排列到第一维,使得后续通过索引更方便进行移位调整。
-
-
mid = (concat_n // 2)
-
计算中心帧的位置索引,由于
concat_n
是奇数,中心帧位于concat_n // 2
的位置(整数除法)。
-
-
for r_idx in range(1, mid+1):
-
开启一个循环,遍历范围从 1 到
mid
(包括 mid),逐个处理中心帧左右两侧的帧。 -
循环体内部:
-
x[mid + r_idx, :] = shift(x[mid + r_idx], r_idx)
-
对中心帧右侧的第
r_idx
帧进行移位操作。 -
调用前面定义的
shift
函数,将该帧内的序列根据正数r_idx
进行向右移位。 -
结果:右侧帧的每一行数据会被调整,使其在时间序列上“向后”偏移了对应数量的单位。
-
-
x[mid - r_idx, :] = shift(x[mid - r_idx], -r_idx)
-
对中心帧左侧的第
r_idx
帧进行移位操作。 -
调用
shift
函数,并传入负数-r_idx
进行向左移位。 -
结果:左侧帧的每一行数据相应“向前”偏移。
-
-
-
-
return x.permute(1, 0, 2).view(seq_len, concat_n * feature_dim)
-
最后一步,将调整后的张量从
[concat_n, seq_len, feature_dim]
重新转换回原始顺序:-
.permute(1, 0, 2)
交换维度顺序,恢复到[seq_len, concat_n, feature_dim]
。 -
.view(seq_len, concat_n * feature_dim)
将第二和第三维展平成一维,得到最终的拼接结果,其中每个序列帧包含了相邻多个帧拼接而成的特征向量。
-
-
-
(4)第四个函数:声音数据文件预处理函数 preprocess_data
def preprocess_data(split, feat_dir, phone_path, concat_nframes, train_ratio=0.8):class_num = 41 # NOTE: pre-computed, should not need changeif split == 'train' or split == 'val':mode = 'train'elif split == 'test':mode = 'test'else:raise ValueError('Invalid \'split\' argument for dataset: PhoneDataset!')label_dict = {}if mode == 'train':for line in open(os.path.join(phone_path, f'{mode}_labels.txt')).readlines():line = line.strip('\n').split(' ')label_dict[line[0]] = [int(p) for p in line[1:]]# split training and validation datausage_list = open(os.path.join(phone_path, 'train_split.txt')).readlines()random.shuffle(usage_list)train_len = int(len(usage_list) * train_ratio)usage_list = usage_list[:train_len] if split == 'train' else usage_list[train_len:]elif mode == 'test':usage_list = open(os.path.join(phone_path, 'test_split.txt')).readlines()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 = 3000000X = torch.empty(max_len, 39 * concat_nframes)if mode == 'train':y = torch.empty(max_len, dtype=torch.long)idx = 0for 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 == 'train':label = torch.LongTensor(label_dict[fname])X[idx: idx + cur_len, :] = featif mode == 'train':y[idx: idx + cur_len] = labelidx += cur_lenX = X[:idx, :]if mode == 'train':y = y[:idx]print(f'[INFO] {split} set')print(X.shape)if mode == 'train':print(y.shape)return X, yelse:return X
4.1 函数声明与参数
def preprocess_data(split, feat_dir, phone_path, concat_nframes, train_ratio=0.8):class_num = 41 # NOTE: pre-computed, should not need change
-
函数声明:
-
定义函数
preprocess_data
,它的参数包括:-
split
:指明数据集的用途,这里可能是'train'
,'val'
或'test'
。 -
feat_dir
:特征数据存储的根目录路径。 -
phone_path
:与“phone”(语音或音素)相关数据的路径,如标签文件和划分文件。 -
concat_nframes
:每个特征点拼接的帧数,即上下文帧扩充的数量(后续将用于concat_feat
函数)。 -
train_ratio
:训练数据划分比率(默认值为 0.8),用于训练和验证数据的切分。
-
-
-
内部变量:
-
class_num = 41
:预先定义了类别数,通常表示音素或电话类的数量,这个值已预计算好,不需要修改。
-
4.2 根据 split 参数确定数据模式
if split == 'train' or split == 'val':mode = 'train'elif split == 'test':mode = 'test'else:raise ValueError('Invalid \'split\' argument for dataset: PhoneDataset!')
-
功能说明:
-
判断
split
参数的取值,若为'train'
或'val'
,则设定mode
为'train'
;若为'test'
,则设为'test'
。 -
对于其他无效的
split
值,则抛出ValueError
异常,提示用户参数错误。
-
-
细节说明:
-
将训练和验证数据都统一放在
'train'
模式下进行处理,后续会根据训练集比例进行分割。
-
4.3 读取标签信息和划分文件
label_dict = {}if mode == 'train':for line in open(os.path.join(phone_path, f'{mode}_labels.txt')).readlines():line = line.strip('\n').split(' ')label_dict[line[0]] = [int(p) for p in line[1:]]# split training and validation datausage_list = open(os.path.join(phone_path, 'train_split.txt')).readlines()random.shuffle(usage_list)train_len = int(len(usage_list) * train_ratio)usage_list = usage_list[:train_len] if split == 'train' else usage_list[train_len:]elif mode == 'test':usage_list = open(os.path.join(phone_path, 'test_split.txt')).readlines()
读取标签(train 模式):
-
初始化空字典
label_dict
用于存储每个数据文件(通常以文件名为 key)的标签。label_dict
字典中 “键值对” 的 “键” 是存储声音数据的文件名, “键值对” 的 “值” 是存储对应文件中的各个桢的类别标签所组成的列表。 -
通过
open
和os.path.join
组合路径读取'train_labels.txt'
文件(文件名根据mode
动态拼接)。 -
对于每一行:
-
使用
strip('\n')
去除换行符,再使用split(' ')
按空格拆分。 -
第一部分(
line[0]
)作为标识符,后续部分转换成整数列表作为标签,保存到label_dict
中。
-
-
训练与验证数据划分:
-
读取
train_split.txt
文件,得到所有数据文件的列表usage_list
。 -
对列表进行随机打乱,确保训练和验证数据混合随机。
-
根据
train_ratio
计算训练数据的长度train_len
; -
如果当前
split
是'train'
,则保留前train_len
个文件;如果是'val'
,则保留剩余部分。
-
-
测试数据读取:
-
如果
mode
为'test'
,则直接读取test_split.txt
文件,usage_list
中包含测试文件的列表。
-
-
共同操作:
-
将
usage_list
中每行去掉换行符,得到干净的文件名列表。 -
usage_list
中存储的是 存储声音数据的文件名列表。 -
打印数据集统计信息,显示类别数和当前 split 下语句数量。
-
4.4 初始化张量 X 与 y(仅对训练模式)
max_len = 3000000X = torch.empty(max_len, 39 * concat_nframes)if mode == 'train':y = torch.empty(max_len, dtype=torch.long)
-
目的:
-
为存储整个数据集的特征(和标签)预先分配足够的内存。
-
-
说明:
-
max_len = 3000000
:设定一个足够大的上限,表示所有拼接后的特征数据将被保存在大小为max_len
的第一维中。 -
X = torch.empty(max_len, 39 * concat_nframes)
:-
创建一个空张量
X
,形状为[max_len, 39 * concat_nframes]
。 -
这里 39 可能代表原始特征的维度数,经过上下文拼接后,每一行的特征维度变为
39 * concat_nframes
。
-
-
如果是训练模式,则也初始化标签张量
y
,数据类型为长整型(torch.long
)。
-
4.5 遍历所有文件,加载和处理特征数据
idx = 0for 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 == 'train':label = torch.LongTensor(label_dict[fname])X[idx: idx + cur_len, :] = featif mode == 'train':y[idx: idx + cur_len] = labelidx += cur_len
-
初始化索引:
-
idx = 0
用于记录已经存入张量X
(和y
)的数据的帧数(每个文件中都有 cur_len 个桢数据)。
-
-
遍历文件列表:
-
使用
enumerate(usage_list)
配合tqdm
显示进度条,逐个处理每个文件名fname
。 -
对每个文件:
-
构造文件路径:使用
os.path.join
拼接feat_dir
、mode
和文件名(加.pt
扩展名),读取保存的特征文件,调用前面定义的load_feat
。 -
得到特征数据
feat
后,计算当前数据长度cur_len = len(feat)
(通常代表时间帧数)。 -
调用
concat_feat(feat, concat_nframes)
对原始特征进行上下文拼接,扩充后特征维度变化为39 * concat_nframes
。 -
如果为训练模式:
-
利用
label_dict[fname]
获取对应标签,并转换为torch.LongTensor
。
-
-
将处理好的特征数据存入预分配张量
X
中,从idx
到idx + cur_len
的位置。 -
若为训练模式,则同样将标签存入
y
的相应位置。 -
更新
idx
(索引增加当前文件的帧数),为下一个文件的数据腾出存储位置。
-
-
4.6 截断多余的预分配内存,并返回数据
X = X[:idx, :]if mode == 'train':y = y[:idx]print(f'[INFO] {split} set')print(X.shape)if mode == 'train':print(y.shape)return X, yelse:return X
-
截取有效部分:
-
由于事先预分配的张量尺寸可能大于实际存入的数据量,使用切片操作
X = X[:idx, :]
将张量截取为实际填充的部分。 -
若是训练模式,同样对标签张量
y
进行截取。
-
-
打印信息:
-
输出当前数据集的类型(split)以及特征数据
X
的形状,若为训练模式还输出标签y
的形状,方便检查预处理结果。
-
-
返回结果:
-
如果是训练模式,函数返回
(X, y)
,即特征数据与对应的标签; -
如果是测试模式,仅返回特征数据
X
。
-
三、分类模型框架的搭建
1、创建自己的数据集类
import torch
from torch.utils.data import Datasetclass LibriDataset(Dataset):def __init__(self, X, y=None):self.data = Xif y is not None:self.label = torch.LongTensor(y)else:self.label = Nonedef __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、创建分类模型
import torch.nn as nnclass BasicBlock(nn.Module):def __init__(self, input_dim, output_dim):super(BasicBlock, self).__init__()# TODO: apply batch normalization and dropout for strong baseline.# Reference: https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html (batch normalization)# https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html (dropout)self.block = nn.Sequential(nn.Linear(input_dim, output_dim),nn.ReLU(),)def forward(self, x):x = self.block(x)return xclass 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
在模型创建过程中,先创建一个 BasicBlock 模块,该模块包含一个隐藏层(一个线性层 + 一个激活函数)。
下面就是模型模块 Classifier,其中调用了 BasicBlock 模块 ,这样的一个模型创建架构可以灵活控制隐藏层的层数,比较灵活。
模型优化:
在注释部分,有对模型优化的建议,即在实际项目中,为了提高模型的泛化能力和稳定性,可以在激活函数之前或之后添加批归一化(BatchNorm1d)和 Dropout 层。
常见的添加方法为:
-
批归一化:通常在线性层之后、激活函数之前,作用是将特征标准化,使得输出有零均值和单位方差,从而加速训练和提高稳定性。
-
Dropout:可在激活函数后加入,防止过拟合,尤其是在网络较深或者数据量不足时效果明显。
改进示例(如果需要, BasicBlock 模块对隐藏层做如下修改):
self.block = nn.Sequential(nn.Linear(input_dim, output_dim),nn.BatchNorm1d(output_dim), # 对 output 进行归一化nn.ReLU(),nn.Dropout(p=0.5) # 设定 dropout 率,根据实际需求调整
)
3、模型的超参数
(1)数据相关参数
concat_nframes = 3 # the number of frames to concat with, n must be odd (total 2k+1 = n frames)
train_ratio = 0.75 # the ratio of data used for training, the rest will be used for validation
-
concat_nframes:
-
意义:该参数指定了在特征预处理时,每个时刻的特征旁边需要连接多少帧信息。
-
约束:参数必须为奇数,使得当前帧处在连接帧的正中间。比如
3 = 2*1 + 1
,表示当前帧以及前一帧和后一帧;可扩展到更多帧以增加上下文信息。 -
改进思考:根据任务(例如语音识别、情感分析或其他时序任务)的具体情况,可以调整此参数。较大的上下文有时能提供更多信息,但也可能使模型过于复杂或增加计算量。注释中提到可以针对 medium baseline 做调整。
-
-
train_ratio:
-
意义:定义了训练集占总数据的比例。剩余的数据将用于验证。
-
作用:平衡训练和验证数据,使得模型在训练过程中可以及时评估泛化性能。
-
调整方向:依赖于数据集大小和任务需求。一般来说,数据量较大时可以适当降低训练比率;数据量较小时则需要更多数据用于训练,但也要留出足够数据用于验证。
-
(2)训练相关参数
seed = 1213 # random seed
batch_size = 512 # batch size
num_epoch = 10 # the number of training epoch
learning_rate = 1e-4 # learning rate
model_path = './model.ckpt' # the path where the checkpoint will be saved
-
seed:
-
意义:用于设置随机数生成器的种子,保证实验的可复现性。
-
注意:不同环境中(如 CPU/GPU)可能需要分别设置,确保所有随机相关操作都有统一的种子。但由于有函数 same_seeds,seed 传入函数 same_seeds 后可实现全流程的随机数设置。
-
-
batch_size:
-
意义:每个训练批次中的样本数。
-
作用:较大的批次能稳定梯度估计,但占用更多内存;较小的批次能使模型更“嘈杂”地更新,有时有助于避免陷入局部最优。
-
调整方向:根据实际硬件资源以及数据集大小调节。
-
-
num_epoch:
-
意义:训练时整个数据集将被完整遍历的次数。
-
作用:决定模型训练的充分程度。
-
调整思考:较复杂的模型或大数据集可能需要更多轮;可以根据验证集性能动态调节。
-
-
learning_rate:
-
意义:学习率控制每次参数更新的步长。
-
作用:较低的学习率会使训练更稳定,但可能收敛较慢;较高的学习率加快收敛,但可能导致不稳定或发散。
-
调整方向:通常需要在实验中进行调参,可能结合学习率衰减策略(learning rate scheduler)以获得较好的效果。
-
-
model_path:
-
意义:模型参数保存的路径,用于保存检查点(checkpoint)。
-
作用:在训练过程中保存模型状态,便于恢复或后续推理。
-
注意:路径应确保有写入权限,并在多个实验中做好区分以免覆盖。
-
(3)模型相关参数
input_dim = 39 * concat_nframes # the input dim of the model, you should not change the value
hidden_layers = 2 # the number of hidden layers
hidden_dim = 64 # the hidden dim
-
input_dim:
-
意义:模型输入的维度。这里是固定为
39 * concat_nframes
。 -
约束:通常
39
表示原始输入特征的维数,而通过帧拼接后的总维度则依赖于concat_nframes
。 -
说明:不建议手动改变此值,因为它依赖于前期特征提取和上下文拼接的设计,如果修改不当可能导致维度不匹配。
-
-
hidden_layers:
-
意义:网络中隐含层(除了输入和输出层之外)的数量。
-
作用:影响模型深度和容量。更多隐含层理论上可以让模型捕捉更复杂的特征,但也更容易过拟合或导致训练困难。
-
改进思考:根据任务和数据复杂性来调整;中等baseline设计中可能需要适度增加深度以获得更好的表达能力,同时需搭配正则化方法。
-
-
hidden_dim:
-
意义:隐含层中每一层的神经元数。
-
作用:决定每层的表示能力以及模型整体的参数量。
-
调整思考:较小的 hidden_dim 能减少计算量和防止过拟合,但可能限制模型表达能力;较大的 hidden_dim 能捕获更多信息,但需要更多计算资源和可能更容易过拟合。
-
调参:在一些baseline实验中,可能需要对这个参数进行调整,以求在复杂度与性能之间取得平衡。
-
4、Dataloader的创建
from torch.utils.data import DataLoader
import gcsame_seeds(seed)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'DEVICE: {device}')# 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)
(1)设置随机种子和设备
same_seeds(seed)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'DEVICE: {device}')
-
same_seeds(seed):
-
该函数的作用是设置训练中的随机种子,确保实验结果具有可复现性。
seed
被传入函数中,以确保所有涉及随机操作的部分(如权重初始化、数据加载等)都能得到相同的结果。 -
通常,
same_seeds
函数内部会设置以下内容:-
torch.manual_seed(seed)
:设置 PyTorch 随机数种子。 -
np.random.seed(seed)
:设置 NumPy 随机数种子(如果在数据处理中有用到 NumPy)。 -
random.seed(seed)
:设置 Python 标准库中的随机数种子。 -
torch.backends.cudnn.deterministic = True
和torch.backends.cudnn.benchmark = False
:确保 CUDA 后端是确定性的,避免因为硬件加速而导致的不同实验结果。
-
-
-
device:
-
通过
torch.cuda.is_available()
检查是否有 GPU 可用。如果有,则使用'cuda'
;否则,回退到'cpu'
。 -
device
将会在后续的模型和数据移动过程中使用,确保模型和数据被正确加载到对应的设备上。
-
(2)调用函数 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)
-
功能:
-
这两行代码通过
preprocess_data
函数分别处理训练数据和验证数据,返回的train_X
和train_y
是训练特征和标签,val_X
和val_y
是验证特征和标签。 -
主要作用是进行数据的读取、标签获取和特征拼接(上下文帧拼接),并根据
train_ratio
划分数据集(训练集和验证集)。
-
-
参数说明:
-
split='train'
和split='val'
:指定了分别处理训练集和验证集的数据。 -
feat_dir='./libriphone/feat'
和phone_path='./libriphone'
:指定了特征数据和标签文件所在的路径。 -
concat_nframes=concat_nframes
:使用上下文帧拼接,concat_nframes
由外部参数设定。 -
train_ratio=train_ratio
:设定训练集和验证集的划分比例。
-
(3)创建数据集对象
train_set = LibriDataset(train_X, train_y)
val_set = LibriDataset(val_X, val_y)
-
功能:
-
将预处理好的训练特征和标签 (
train_X
,train_y
) 以及验证特征和标签 (val_X
,val_y
) 封装为LibriDataset
数据集对象。 -
LibriDataset
是您之前定义的自定义数据集类,它继承自 PyTorch 的Dataset
类,确保数据可以通过索引获取,同时为DataLoader
提供支持。
-
-
作用:
-
将数据封装为一个 PyTorch Dataset 对象后,可以更方便地在模型训练中使用 DataLoader 进行批量加载、打乱和多线程处理。
-
(4)释放内存
# remove raw feature to save memory
del train_X, train_y, val_X, val_y
gc.collect()
-
内存管理:
-
对原始数据的删除操作。原始的
train_X
,train_y
,val_X
,val_y
是预处理数据的变量,数据已经被封装到train_set
和val_set
中,因此可以手动删除原始数据以节省内存。 -
gc.collect()
是强制进行垃圾回收,清除内存中不再使用的对象。 -
若数据集非常庞大(例如音频数据或图像数据),释放内存可以显著提高系统的效率。通常在处理非常大的数据集时,可以通过这种方式避免内存溢出。
-
(5)创建数据加载器(DataLoader)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False)
-
功能:
-
DataLoader
是 PyTorch 用于数据加载的核心工具,它支持批量加载数据、自动化并行计算、数据打乱等功能。 -
train_loader
用于训练集的加载,val_loader
用于验证集的加载。
-
-
参数说明:
-
train_set
和val_set
:分别是训练集和验证集的数据集对象。 -
batch_size=batch_size
:每个批次的数据量,通常设置为 2 的幂次方(如 32、64、128 等)以便于 GPU 并行计算。 -
shuffle=True
:训练数据每次迭代时都会进行打乱,增加训练的随机性,防止模型过拟合。 -
shuffle=False
:验证数据通常不需要打乱,因为我们只是用它来评估模型性能。
-
5、开始训练
# 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.Adam(model.parameters(), lr=learning_rate)best_acc = 0.0
for epoch in range(num_epoch):train_acc = 0.0train_loss = 0.0val_acc = 0.0val_loss = 0.0# trainingmodel.train() # set the model to training modefor i, batch in enumerate(tqdm(train_loader)):features, labels = batchfeatures = 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 probabilitytrain_acc += (train_pred.detach() == labels.detach()).sum().item()train_loss += loss.item()# validationmodel.eval() # set the model to evaluation modewith torch.no_grad():for i, batch in enumerate(tqdm(val_loader)):features, labels = batchfeatures = 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 probabilityval_loss += loss.item()print(f'[{epoch+1:03d}/{num_epoch:03d}] Train Acc: {train_acc/len(train_set):3.5f} Loss: {train_loss/len(train_loader):3.5f} | Val Acc: {val_acc/len(val_set):3.5f} loss: {val_loss/len(val_loader):3.5f}')# if the model improves, save a checkpoint at this epochif val_acc > best_acc:best_acc = val_acctorch.save(model.state_dict(), model_path)print(f'saving model with acc {best_acc/len(val_set):.5f}')# 所有训练完毕后释放内存
del train_set, val_set
del train_loader, val_loader
gc.collect()
(1)创建模型、损失函数和优化器
model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
创建模型:
-
model = Classifier(...)
:根据之前定义的Classifier
类创建模型对象,并将其移动到计算设备(CPU 或 GPU)。 -
input_dim=input_dim
和hidden_layers=hidden_layers
等参数来自先前的配置,定义了模型的输入维度、隐藏层数和每个隐藏层的维度。 -
to(device)
确保模型在适当的设备上运行。无论是 CPU 还是 GPU,PyTorch 都能够自动将模型迁移到设备上。
定义损失函数:
-
criterion = nn.CrossEntropyLoss()
:交叉熵损失函数用于多分类任务,是分类问题中最常见的损失函数之一。它结合了softmax
激活函数和log
损失。
定义优化器:
-
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
:-
使用 Adam 优化器,这是一个自适应学习率优化器,常用于训练深度神经网络。
-
model.parameters()
提供了优化器需要更新的参数(即模型的权重和偏置)。 -
lr=learning_rate
设置了学习率,控制每次参数更新的步长。
-
(2)训练循环
model.train() # set the model to training mode
for i, batch in enumerate(tqdm(train_loader)):features, labels = batchfeatures = features.to(device)labels = labels.to(device)optimizer.zero_grad() # zero out the gradients from previous stepoutputs = model(features) # forward passloss = criterion(outputs, labels) # compute lossloss.backward() # backward pass to compute gradientsoptimizer.step() # update model parameters_, train_pred = torch.max(outputs, 1) # get predicted classtrain_acc += (train_pred.detach() == labels.detach()).sum().item() # calculate accuracytrain_loss += loss.item() # accumulate loss
-
训练模式:
-
model.train()
将模型设置为训练模式。这对于包含 BatchNorm 或 Dropout 层的网络非常重要,因为它们的行为在训练和评估模式下不同。
-
-
数据加载和设备迁移:
-
features, labels = batch
:从train_loader
获取当前批次的特征和标签。 -
features.to(device)
和labels.to(device)
:将数据移动到计算设备上。
-
-
梯度清零、前向传播、损失计算和反向传播:
-
optimizer.zero_grad()
清除上一步计算的梯度,这是优化器的标准做法。 -
outputs = model(features)
进行前向传播,得到模型预测的输出。 -
loss = criterion(outputs, labels)
计算损失。 -
loss.backward()
进行反向传播,计算梯度。 -
optimizer.step()
更新模型参数。
-
-
计算训练准确率和损失:
-
torch.max(outputs, 1)
获取预测的类别(outputs
是一个包含每个类概率的向量)。 -
train_acc += (train_pred.detach() == labels.detach()).sum().item()
:计算当前批次的准确率,并累加到train_acc
中。 -
train_loss += loss.item()
:累计每个批次的损失。
-
(3)验证循环
model.eval() # set the model to evaluation mode
with torch.no_grad(): # turn off gradient calculation for validationfor i, batch in enumerate(tqdm(val_loader)):features, labels = batchfeatures = features.to(device)labels = labels.to(device)outputs = model(features)loss = criterion(outputs, labels) # compute loss_, val_pred = torch.max(outputs, 1) # get predicted classval_acc += (val_pred.cpu() == labels.cpu()).sum().item() # calculate accuracyval_loss += loss.item() # accumulate loss
-
评估模式:
-
model.eval()
将模型设置为评估模式,关闭 Dropout 和 BatchNorm 层的训练行为。
-
-
禁用梯度计算:
-
with torch.no_grad()
在验证阶段不需要进行梯度计算,从而减少内存消耗并加快推理速度。
-
-
与训练阶段类似:
-
通过与训练阶段类似的方式计算损失和准确率,唯一不同的是我们不进行梯度计算,也不会更新模型参数。
-
(4)结果可视化
print(f'[{epoch+1:03d}/{num_epoch:03d}] Train Acc: {train_acc/len(train_set):3.5f} Loss: {train_loss/len(train_loader):3.5f} | Val Acc: {val_acc/len(val_set):3.5f} loss: {val_loss/len(val_loader):3.5f}')
- 打印每个 epoch 的训练和验证准确率以及损失,帮助您监控模型的训练进度和性能。
(5)保存最好的模型
if val_acc > best_acc:best_acc = val_acctorch.save(model.state_dict(), model_path)print(f'saving model with acc {best_acc/len(val_set):.5f}')
保存最佳模型:
-
如果当前 epoch 的验证准确率超过了历史最佳准确率
best_acc
,则保存当前模型的参数(model.state_dict()
)。 -
使用
torch.save()
将模型保存到指定路径model_path
,以便后续加载和推理。
(6)删除数据集对象(释放内存)
del train_set, val_set
del train_loader, val_loader
gc.collect()
三句分别是:
- 删除数据集对象
- 删除数据加载器对象
- 强制垃圾回收
6、总结与优化建议
总结:
-
训练过程:
-
通过逐批次处理训练数据并更新模型参数,使用交叉熵损失计算和 Adam 优化器进行训练。
-
每个 epoch 结束后,输出当前的训练和验证损失与准确率,帮助检查模型是否收敛。
-
-
验证过程:
-
在验证阶段,通过关闭梯度计算来提高推理效率,计算验证集的准确率和损失。
-
-
保存最佳模型:
-
在每个 epoch 结束时,如果验证准确率有所提高,保存当前模型状态,这样可以确保模型训练过程中始终保存最佳表现。
-
优化建议:
-
学习率衰减:
-
可以考虑添加学习率衰减策略,如使用
torch.optim.lr_scheduler.StepLR
或ReduceLROnPlateau
,使得学习率在训练过程中逐渐减小,从而帮助模型更好地收敛。
-
-
混合精度训练:
-
如果使用 GPU,启用混合精度训练(例如使用
torch.cuda.amp
)可以加速训练并减少显存使用。
-
-
Early Stopping:
-
引入早停(Early Stopping)机制,在验证性能不再提升时提前终止训练,避免浪费计算资源。
-
-
更多模型检查点:
-
除了保存验证集最优模型外,可以考虑每隔几个 epoch 保存一次模型检查点,便于后期恢复。
-
四、模型使用过程
这部分展示如何用模型进行预测一些没标签的数据集。
# 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))pred = np.array([], dtype=np.int32)
model.eval()
with torch.no_grad():for i, batch in enumerate(tqdm(test_loader)):features = batchfeatures = features.to(device)outputs = model(features)_, test_pred = torch.max(outputs, 1) # get the index of the class with the highest probabilitypred = np.concatenate((pred, test_pred.cpu().numpy()), axis=0)with open('prediction.csv', 'w') as f:f.write('Id,Class\n')for i, y in enumerate(pred):f.write('{},{}\n'.format(i, y))
(1)测试数据加载和预处理
# 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)
-
预处理数据:
-
调用
preprocess_data
函数,并传入参数split='test'
。-
feat_dir
和phone_path
分别指定了特征目录和标签文件所在路径。 -
concat_nframes
指定了特征拼接的帧数。
-
-
作用:该函数会读取测试数据的特征(这里测试集没有标签,因此只处理特征部分),并进行预处理操作(如归一化、帧拼接等)。
-
-
构建数据集对象:
-
使用
LibriDataset(test_X, None)
将测试数据封装成一个数据集对象。-
注意这里标签参数传入的是
None
,因为测试集没有标签,需要进行预测。
-
-
-
创建数据加载器:
-
通过
DataLoader
对测试数据进行批量加载,设置的batch_size
保持一致,并且不打乱数据(shuffle=False
)。 -
作用:利用 DataLoader 可以方便地在预测时逐批次处理数据,同时保证数据顺序与原始顺序一致(以便后续输出顺序对应测试样本的顺序)。
-
(2)加载模型
# 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))
-
模型构建:
-
根据之前同样的配置参数重新实例化模型对象,并调用
.to(device)
将模型加载到指定的设备(CPU 或 GPU)上。 -
参数
input_dim
,hidden_layers
,hidden_dim
需要和训练时保持一致,否则无法加载已保存的模型权重。
-
-
加载模型权重:
-
使用
torch.load(model_path)
读取保存的模型检查点,并用model.load_state_dict()
加载模型参数。 -
作用:使当前模型拥有训练期间保存下来的最佳权重,从而能在测试集上进行预测获得正确的输出。
-
(3)进行预测
pred = np.array([], dtype=np.int32)
model.eval()
with torch.no_grad():for i, batch in enumerate(tqdm(test_loader)):features = batchfeatures = features.to(device)outputs = model(features)_, test_pred = torch.max(outputs, 1) # get the index of the class with the highest probabilitypred = np.concatenate((pred, test_pred.cpu().numpy()), axis=0)
-
初始化预测数组:
-
pred = np.array([], dtype=np.int32)
用于存储预测类别的数组,数据类型设置为 32 位整数。
-
-
设置模型为评估模式:
-
model.eval()
将模型设置为评估(推理)模式。这样会关闭 Dropout 或 BatchNorm 层的训练行为,保证预测结果稳定且一致。 -
with torch.no_grad():
则禁用梯度计算,能减少内存消耗,加速推理过程。
-
-
批次预测:
-
使用
for i, batch in enumerate(tqdm(test_loader)):
遍历测试加载器。tqdm
用于显示进度条,方便监控当前进度。 -
取出每个批次
batch
,将其移动到相应设备device
上。 -
将输入特征送入模型获得
outputs
。 -
torch.max(outputs, 1)
函数返回每一行预测概率中的最大值的索引,这个索引就是模型预测的类别。 -
用
np.concatenate
将每个批次获得的预测结果与累积的pred
数组拼接在一起,最终得到所有测试样本的预测结果。
-
(4)保存预测结果到 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))
-
打开 CSV 文件:
-
使用
open('prediction.csv', 'w')
打开(或创建)一个 CSV 文件,模式为写模式。
-
-
写入表头:
-
f.write('Id,Class\n')
写入 CSV 文件的表头,定义了两列:Id
和Class
。
-
-
循环写入预测结果:
-
遍历预测数组
pred
中的每个预测结果。 -
enumerate(pred)
提供了样本的索引i
和预测类别y
。 -
使用
f.write('{},{}\n'.format(i, y))
将索引和预测类别格式化后写入文件,每行一个测试样本的预测结果。
-
-
作用:
-
生成的
prediction.csv
文件可以直接用于后续评测或结果提交,例如在比赛或项目评估中使用。
-