Task3:《深度学习详解》- 2 机器学习框架&实践攻略(10页+50分钟)
Part01:视频笔记
机器学习框架(Framework):
Training data + Test data(训练数据+测试数据)+ Training:适用于不同的任务
Training:
Step01:未知数函数——
y
=
f
θ
(
x
)
y=f_\theta(x)
y=fθ(x)
Step02:由训练数据定义损失——
L
(
θ
)
L(\theta)
L(θ)
Step03:优化更新——
θ
∗
=
arg min
θ
L
\theta^*=\underset{\theta}{\operatorname{arg\, min}}L
θ∗=θargminL
General Guide——如何优化训练过程:
![[Pasted image 20240830145521.png]]
Model Bias:模型过于简单(如同大海捞针但海里没有针)
重新设计模型使其更有弹性(flexible)
- 增加更多的特征(features);
- Deep Learning:更多的神经元或层;
Optimization:更新优化参数无法找到更低的损失值(大海里有针但找不到)
Gaining the insights from comparison:
从较浅神经网络(或者其他模型)——更易于优化——训练测试;
如果复杂神经网络模型不能从训练数据中获得一个较小的损失值,那就是一个Optimization问题;
Overfitting(过拟合):训练数据损失值小但测试数据损失值大;
Flexible Model如果参数过多会导致过拟合
解决方法一:增加训练数据
解决方法二:数据增强(Data Augmentation)——还可以增加数据量
解决方法三:使用Constrained Model
- 更少的参数或者是共享参数
- 更少的特征数
- 较早地停止训练
- Dropout
- Regularization(正则化)
Cross Validation(交叉验证):
将训练数据集分为训练集和验证集;
N-fold Cross Validation:
Train | Train | Val |
---|---|---|
Train | Val | Train |
Val | Train | Train |
Mismatch:训练集和测试集的数据分布不同
(选修)实践任务:HW2(DNN)深度神经网络-分类任务
Part01:网页笔记
Baseline:
导入库:
#引入 Numpy工具,pytorch工具,和随机数工具random
import numpy as np
import torch
import random
#引入提供与操作系统交互的接口的标准库os
import os
#引入在终端显示进度条的tqdm库
from tqdm import tqdm
from torch.utils.data import Dataset
import torch.nn as nn
from torch.utils.data import DataLoader
#引入垃圾回收机制库
import gc
数据准备与预处理:
固定随机种子,可以使得我们接下来训练的模型保持统一的初始参数,从而使得每次训练完成的模型性能保持一致:
def same_seeds(seed):
#为random,numpy,pytorch等设置固定下来的随机种子
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)
#确保在使用CUDA时,运算更具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
将目标帧相邻的音素连接起来作为一个样本:
通常来说,表示一个音素需要多个帧,且与其前后帧的存在息息相关,因此在进行训练集/测试集划分前,我们需要将目标帧相邻的音素连接起来作为一个样本,来提高预测的准确性。这里我们通过def concat_feat()
函数将我们指定的左右各k帧(k值将在后面由训练者自己定义)连接成一个样本,而预测中间的第k+1帧,因此每个样本的总长度是n=2k+1。随后我们使用def preprocess_data()
函数将数据集切分为训练集和测试集。
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 必须为奇数,因为我们需要预测中间帧,而前后帧数一致,所以每个样本都会是奇数帧。
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, random_seed=1213):
class_num = 41 # NOTE:这些参数包括类别数是预先定义的,不可以被修改。
if 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:]]
# 切分训练集和验证集数据
usage_list = open(os.path.join(phone_path, 'train_split.txt')).readlines()
random.seed(random_seed)
#打乱样本,防止模型记住样本顺序
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 = 3000000
X = torch.empty(max_len, 39 * concat_nframes)
if mode == 'train':
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 == 'train':
label = torch.LongTensor(label_dict[fname])
X[idx: idx + cur_len, :] = feat
if mode == 'train':
y[idx: idx + cur_len] = label
idx += cur_len
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, y
else:
return X
实现一个加载音素数据的类:
这段代码主要是在原始数据处理完及划分完训练集和测试集后,利用Pytorch库的Dataset类实现一个加载音素数据的类,在该类中我们可以对数据进行进一步的处理,并获取训练集或测试集的部分信息,同时加载给后续的DataLoader使用。
class LibriDataset(Dataset):
def __init__(self, X, y=None):
self.data = X
#将label值转化为LongTensor格式以便输入到其他方法中计算
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)
实现训练集加载类和验证集加载类:
在以下代码中我们通过继承Pytorch中DataLoader类实现了自己的训练集加载类(train_loader)
和验证集加载类(val_loader)
,加载类可以决定批量加载大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址等。同时以下代码还对刚才定义的固定种子函数、预处理函数、Dataset函数进行了实现,正式开始读取数据并进行以上数据处理和加载。
- 验证集是从训练集中划分出来的一部分数据集,它可以用于调整模型的超参数和用于对模型的能力进行初步评估。 通常用来在模型迭代训练时,用以验证当前模型泛化能力(准确率,召回率等),以决定是否停止继续训练。其和测试集是不一样的。
same_seeds(seed)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'DEVICE: {device}')
# 调用刚才定义的预处理函数对数据进行预处理
train_X, train_y = preprocess_data(split='train', feat_dir='./libriphone/feat', phone_path='./libriphone', concat_nframes=concat_nframes, train_ratio=train_ratio, random_seed=seed)
val_X, val_y = preprocess_data(split='val', feat_dir='./libriphone/feat', phone_path='./libriphone', concat_nframes=concat_nframes, train_ratio=train_ratio, random_seed=seed)
# 调用刚才定义的Dataset函数获取数据
train_set = LibriDataset(train_X, train_y)
val_set = LibriDataset(val_X, val_y)
# 调用垃圾回收函数删除已经使用过的train_X, train_y, val_X, val_y以节省内存
gc.collect()
# 定义数据加载类,其用于给模型提供以样本划分的数据及其类别标签
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False)
定义模型:
这段代码正式定义了两个模型的部分,它们都继承了PyTorch的nn.Module
类,包含了定义一个神经网络层或者模块所必须的基础方法(如线性层和激活函数等)。
其中一个是 BasicBlock
,是用于堆叠的基础神经网络块,它的结构(由 def __init__(``)
函数定义)只包括了一个线性层和ReLU
激活函数,它的数据流操作(由def forward(``)
函数定义)只包括让输入数据经过该线性层和激活函数随后输出。
class BasicBlock(nn.Module):
#定义模块结构
def __init__(self, input_dim, output_dim):
super(BasicBlock, self).__init__()
# TODO: 你可以应用批归一化和dropout方法来获得更好的模型性能,这部分内容将在Step 5进行介绍
# 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.Sequential函数将内部的结构固定为一个序列(Sequential),可以理解为封装
#一个线性层
nn.Linear(input_dim, output_dim),
#一个激活函数
nn.ReLU(),
)
#数据流操作
def forward(self, x):
#输入数据经过定义的block块并输出结果
x = self.block(x)
return x
另一个是Classifier,其是基于刚才定义的BasicBlock
神经网络块进行堆叠,它的结构(由 def __init__(``)
函数定义)包含了多个BasicBlock
神经网络块和一个线性层用于将中间层权重输出为多个类别的权重来分类,它的数据流操作(由def forward(``)
函数定义)只包括让输入数据经过这些BasicBlock
模块和线性层输出分类结果。
class Classifier(nn.Module):
#定义模块结构
def __init__(self, input_dim, output_dim=41, hidden_layers=1, hidden_dim=256):
super(Classifier, self).__init__()
#通过调用刚才定义的序列进行叠加形成深度神经网络层,堆叠数由训练者定义的hidden_layers参数决定
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
定义损失函数和优化器等其他配置:
这段代码实现了音素分类模型的初始化和训练配置,目的是准备好训练环境和参数。它选择合适的设备(GPU或CPU),设置模型、批量大小、训练轮数、提前停止策略,定义了损失函数和优化器,为后续的模型训练奠定了基础。
# 数据集参数 TODO:你可以修改concat_nframes来看不同的分类效果
concat_nframes = 3 # 我们所定义的目标帧前后连接的帧数,连接后的帧数必须为奇数
train_ratio = 0.75 # 用于训练集的数据比例,其余数据将用于验证集
# 训练参数
seed = 1213 # 随机种子数
batch_size = 512 # 每批次输入的数据量大小,即批次大小
num_epoch = 10 # 训练的轮次数
learning_rate = 1e-4 # 定义学习率,决定了参数更新的快慢
model_path = './model.ckpt' # 模型训练的结果权重存储地点
# 模型参数
# TODO: 你可以更改"hidden_layers" 或 "hidden_dim" 来观察不同的隐藏层数量或隐藏层宽度对预测结果的影响
input_dim = 39 * concat_nframes # 数据输入模型时的维度数,这里不可以修改
hidden_layers = 2 # 隐藏层的数量
hidden_dim = 64 # 隐藏层的维度宽度
#根据上面定义的模型类实现一个实例模型,同时定义所需的损失函数(这里是交叉熵损失函数)和优化器(这里是Adam优化器)
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)
训练模型与评估模型:
这段代码实现了一个图像分类模型的训练和验证循环,目的是通过多轮训练(epochs)逐步优化模型的参数,以提高其在验证集上的性能,并保存效果最好的模型。训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时(该数量由早停数决定,如果没有设置早停数则会训练到最大轮次数)提前停止训练。
训练完成后,需要在测试集上评估模型的性能。通过计算准确率来衡量模型在测试集上的表现(见第6步)。
#开始初始化训练参数,这些参数不能改动
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
# 训练中
model.train() # 这里是使用Pytorch中刚才继承的nn.Module中的train()方法确保模型处于训练模式
for i, batch in enumerate(tqdm(train_loader)):#tqdm仅用于展示训练数据的进度
#获取每个批次应有的数据及其对应的标签并将它们放在同一设备上
features, labels = batch
features = features.to(device)
labels = labels.to(device)
# 清除上一步中参数中存储的梯度
optimizer.zero_grad()
# 前向传播数据,将数据输入实现的模型实例输出结果(确保数据和模型位于同一设备上)
outputs = model(features)
#计算交叉熵损失。
#在计算交叉熵之前不需要应用softmax,因为它会自动完成。
loss = criterion(outputs, labels)
# 通过损失值计算应当下降的参数梯度
loss.backward()
# 使用计算出的参数梯度更新参数
optimizer.step()
#根据模型输出的多个类别的权重获取概率(权重)最高的类别的索引,作为预测结果进行输出
_, train_pred = torch.max(outputs, 1)
#记录损失和准确率
train_acc += (train_pred.detach() == labels.detach()).sum().item()
train_loss += loss.item()
# 验证中
model.eval() # 确保模型处于评估模式,以便某些模块如dropout能够正常工作
# 我们在验证阶段不需要更新梯度来更新参数。
# 使用 torch.no_grad() 加速前向传播过程
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(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 val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), model_path)
print(f'saving model with acc {best_acc/len(val_set):.5f}')
进行预测:
最后的代码块用于构建一个测试数据集和测试集DataLoader,以便高效地读取数据。实例化并加载预训练的分类器模型,并将其设置为评估模式。在不更新梯度及模型参数的情况下,遍历测试数据,使用训练完成后保存的模型进行预测,并将预测标签存储在列表中。将预测结果与测试集的ID生成一个DataFrame,并将其保存为prediction.csv
。
# 构建测试数据集,同样需要预处理并构建测试集Dataset类和测试集DataLoader类来处理和读取各个测试集的样本
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)
# 加载刚才保存的训练时最优的模型参数到我们新定义的模型类中,
# 加载模型参数需要保证新构建的模型架构与保存的模型架构一致,否则会出错
model = Classifier(input_dim=input_dim, hidden_layers=hidden_layers, hidden_dim=hidden_dim).to(device)
model.load_state_dict(torch.load(model_path))
# 初始化一个空array,用于存储所有预测结果
pred = np.array([], dtype=np.int32)
# 同样是使模型进入验证模式
model.eval()
# 我们在验证阶段不需要更新梯度来更新参数。
# 使用 torch.no_grad() 加速前向传播过程
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)
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))
拓展你专属的DNN分类模型:
拓展数据预处理阶段:
在数据预处理阶段,除了上面已有的数据预处理操作,还可以加入一些自己的数据预处理操作,如去除声音信号中的噪声等。
在下面代码中,我们尝试对读取的数据施加基于FIR滤波器的去噪功能
首先在终端中安装scipy库
pip install scipy
随后在load_feat函数之前增加去噪函数
#导入这个库
from scipy.signal import firwin, lfilter
def denoise_with_fir(feat, cutoff=0.2, fs=1.0, n_taps=101):
"""
使用FIR滤波器去除高频噪声
Parameters:
- feat: (seq_len, feature_dim).
- cutoff: 低通滤波器的截断频率.
- fs: 采样频率.
- n_taps: FIR滤波器的长度,必须为奇数
Returns:
- 过滤后的信号
"""
# 设定 FIR 滤波器的系数
b = firwin(n_taps, cutoff, nyq=fs / 2)
# 对输入数据进行去噪
feat_filtered = lfilter(b, 1.0, feat.numpy(), axis=0)
return torch.tensor(feat_filtered, dtype=torch.float32)
# 修改 load_feat 函数以包含去噪
def load_feat(path):
feat = torch.load(path)
feat = denoise_with_fir(feat)
return feat
拓展分类模型结构:
在baseline中我们设计的DNNs结构非常简单,其基础块仅由线性层和激活函数堆叠而成。
事实上,我们还可以通过在基础块中调整激活函数、增加批归一化方法(Batch Normalization)和dropout方法来获得更好的模型性能。
在下面代码中,我们首先通过替换激活函数实现了新的DNNs模型,同时在其中增加了批归一化方法和dropout方法来提高其性能同时避免出现过拟合现象:
class BasicBlock(nn.Module):
#定义模块结构
def __init__(self, input_dim, output_dim):
super(BasicBlock, self).__init__()
# TODO: 你可以应用批归一化和dropout方法来获得更好的模型性能,这部分内容将在Step 5进行介绍
# 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.Sequential函数将内部的结构固定为一个序列(Sequential),可以理解为封装
#一个线性层
nn.Linear(input_dim, output_dim),
nn.BatchNorm1d(output_dim), # 批归一化层
#一个激活函数
nn.Dropout(0.1) # Dropout层,你可以调整其参数从0-1之间
)
#数据流操作
def forward(self, x):
#输入数据经过定义的block块并输出结果
x = self.block(x)
x = F.gelu(x), #替换ReLU激活函数
return x
名词解析:
激活函数:
是一种添加到深度神经网络中的函数,旨在帮助网络学习数据中的复杂模式。类似于人类大脑中基于神经元的模型,激活函数最终决定了要发射给下一个神经元的内容。其主要有两种作用:一是将神经元的输出值限制在一定的范围内,二是将非线性添加到神经网络中,而非线性决定了深度神经网络可以实现复杂的功能和拟合计算。ReLU是一种常用的激活函数,即修正线性单元,其数学表达式为:f(x) = max(0,x),其可以很好地解决梯度消失的问题。而我们替换其所用的GeLU,则是另一种常用的基于高斯误差函数的激活函数。相较于 ReLU 等激活函数,GELU 更加平滑,有助于提高训练过程的收敛速度和性能。其表达式为: G E L U ( x ) = x ∗ P ( X ≤ x ) = x ∗ Φ ( x ) GELU(x)=x*P(X\leq x)=x*\Phi(x) GELU(x)=x∗P(X≤x)=x∗Φ(x)
批归一化:
是一种在深度学习中广泛使用的技术,旨在通过归一化每一批数据的分布来加速神经网络的训练并提高模型的性能。它由Google在2015年提出,并迅速成为几乎所有卷积神经网络的标配技巧。其主要作用包括:1.加速训练;2.缓解梯度消失问题;3.提高模型泛化能力。
Dropout:
是一种在深度神经网络中常用的正则化技术,旨在防止神经网络在训练过程中出现过拟合现象。该方法通过随机丢弃(或称为“丢弃”)神经网络中的一部分神经元及其连接,从而减少神经网络模型的复杂性,增加其泛化能力。具体来说,Dropout方法在训练阶段随机选择一部分神经元,将其输出设置为0,使其不参与前向和反向传播过程。这样做可以防止模型对训练数据中的噪声和特定特征过度拟合,从而提高模型在未见过的数据(测试数据)上的性能。