模型
我为我的模型设置了必要的辅助函数,以便以后进行训练:
class ModelBase(nn.Module): # defines mechanism when training each batch in dl def train_step(self, batch): xb, labels = batch outs = self(xb) loss = F.cross_entropy(outs, labels) return loss # similar to `train_step`, but includes acc calculation & detach def val_step(self, batch): xb, labels = batch outs = self(xb) loss = F.cross_entropy(outs, labels) acc = accuracy(outs, labels) return {'loss': loss.detach(), 'acc': acc.detach()} # average out losses & accuracies from validation epoch def val_epoch_end(self, outputs): batch_loss = [x['loss'] for x in outputs] batch_acc = [x['acc'] for x in outputs] avg_loss = torch.stack(batch_loss).mean() avg_acc = torch.stack(batch_acc).mean() return {'avg_loss': avg_loss, 'avg_acc': avg_acc} # print all data once done def epoch_end(self, epoch, avgs, test=False): s = 'test' if test else 'val' print(f'Epoch #{epoch + 1}, {s}_loss:{avgs["avg_loss"]}, {s}_acc:{avgs["avg_acc"]}')
定义多个函数,以后可以使用这些函数训练继承这个类的PyTorch模型。
def accuracy(outs, labels): _, preds = torch.max(outs, dim=1) return torch.tensor(torch.sum(preds == labels).item() / len(preds))
这个函数在上述类的val_step函数中被用来确定验证dataloader上模型的%准确性。
并定义用于拟合/训练模型和在验证数据集上测试模型的主要功能
@torch.no_grad() def evaluate(model, val_dl): # eval mode model.eval() outputs = [model.val_step(batch) for batch in val_dl] return model.val_epoch_end(outputs) def fit(epochs, lr, model, train_dl, val_dl, opt_func=torch.optim.Adam): torch.cuda.empty_cache() history = [] # define optimizer optimizer = opt_func(model.parameters(), lr) # for each epoch... for epoch in range(epochs): # training mode model.train() # (training) for each batch in train_dl... for batch in tqdm(train_dl): # pass thru model loss = model.train_step(batch) # perform gradient descent loss.backward() optimizer.step() optimizer.zero_grad() # validation res = evaluate(model, val_dl) # print everything useful model.epoch_end(epoch, res, test=False) # append to history history.append(res) return history
最后,这是我们等待已久的简单CNN模型:
class Classifier(ModelBase): def __init__(self): super().__init__() # 1 x 128 x 24 self.conv1 = nn.Conv2d(1, 4, kernel_size=3, padding=1) # 4 x 128 x 24 self.conv2 = nn.Conv2d(4, 8, kernel_size=3, padding=1) # 8 x 128 x 24 self.bm1 = nn.MaxPool2d(2) # 8 x 64 x 12 self.conv3 = nn.Conv2d(8, 8, kernel_size=3, padding=1) # 8 x 64 x 12 self.bm2 = nn.MaxPool2d(2) # 8 x 32 x 6 self.fc1 = nn.Linear(8*32*6, 64) self.fc2 = nn.Linear(64, 2) def forward(self, xb): out = F.relu(self.conv1(xb)) out = F.relu(self.conv2(out)) out = self.bm1(out) out = F.relu(self.conv3(out)) out = self.bm2(out) out = torch.flatten(out, 1) out = F.relu(self.fc1(out)) out = self.fc2(out) return out
我使用了多个卷积层,正如我们之前的理论推断所建议的那样,我们的模型使用了一些最大池化层,然后使用一个非常简单的全连接网络来进行实际的分类。令人惊讶的是,这个架构后来表现得相当好,甚至超过了我自己的预期。
利用GPU
几乎每个人都需要GPU来训练比一般的前馈神经网络更复杂的东西。幸运的是,PyTorch让我们可以很容易地利用现有GPU的能力。首先,我们将我们的cuda设备定义为关键词设备,以便更容易访问:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
我们还确保如果没有GPU, CPU会被使用。
这里还有另一个技巧:
torch.backends.cudnn.benchmark = True
这可以帮助提高你的训练速度(如果你的输入在大小/形状上没有变化)
显然,你可以“告诉”PyTorch在一次又一次的训练中优化自己,只要训练输入在大小和形状上保持不变。它会知道为你的特定硬件(GPU)使用最快的算法。!
然后我们定义帮助函数来移动dataloaders、tensors和我们的模型到我们的GPU设备。
def to_device(data, device): """Move tensor(s) to chosen device""" if isinstance(data, (list, tuple)): return [to_device(x, device) for x in data] return data.to(device, non_blocking=True) class DeviceDataLoader(): """Wrap a dataloader to move data to a device""" def __init__(self, dl, device): self.dl = dl self.device = device def __iter__(self): """Yield a batch of data after moving it to device""" for b in self.dl: yield to_device(b, self.device) def __len__(self): """Number of batches""" return len(self.dl)
训练
使用我们的设备辅助功能,我们将一切移动到我们的GPU如下:
train_dl = DeviceDataLoader(train_dl, device) val_dl = DeviceDataLoader(val_dl, device) model = to_device(Classifier(), device)
我们还指定了我们的学习速度和我们的训练次数,我花了很长时间来找到一个好的值。
lr = 1e-5 epochs = 8
在进行任何训练之前,我们会发现模型的表现:
history = [evaluate(model, val_dl)] [{'avg_loss': tensor(0.7133, device='cuda:0'), 'avg_acc': tensor(0.6042)}]
很显然,我们在开始时的准确性上有点幸运,但经过多次试验,最终结果即使不相同,也会非常相似。
接下来,我们使用之前定义的fit函数来训练我们的简单分类器模型实例:
history += fit(epochs, lr, model, train_dl, val_dl) HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #1, val_loss:0.6354132294654846, val_acc:0.7176136374473572 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #2, val_loss:0.6065077781677246, val_acc:0.7439352869987488 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #3, val_loss:0.56722491979599, val_acc:0.77376788854599 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #4, val_loss:0.5528884530067444, val_acc:0.7751822471618652 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #5, val_loss:0.5130119323730469, val_acc:0.8004600405693054 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #6, val_loss:0.4849482774734497, val_acc:0.8157732486724854 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #7, val_loss:0.4655478596687317, val_acc:0.8293880224227905 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #8, val_loss:0.4765000343322754, val_acc:0.8155447244644165
在发现精确度有一点下降后,我决定再训练一点点,让模型重定向回到正确的方向:
history += fit(3, 1e-6, model, train_dl, val_dl) HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #1, val_loss:0.4524107873439789, val_acc:0.8329823613166809 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #2, val_loss:0.44666698575019836, val_acc:0.8373703360557556 HBox(children=(FloatProgress(value=0.0, max=352.0), HTML(value=''))) Epoch #3, val_loss:0.4442901611328125, val_acc:0.8412765860557556
忽略“HBox”工件,这是tqdm提供的,请多关注准确性!
总结一下,以下是我们过去11个时期的训练统计数据:
plt.plot([x['avg_loss'] for x in history]) plt.title('Losses over epochs') plt.xlabel('epochs') plt.ylabel('loss') plt.show()
plt.plot([x['avg_acc'] for x in history]) plt.title('Accuracy over epochs') plt.xlabel('epochs') plt.ylabel('acc') plt.show()
总的来说,我们的模型训练得相当好,从它的外观来看,我们可能已经为我们的模型的损失找到了一个相对最小的值。
等等,一个更复杂的模型或者使用不同的转换怎么样?
相信我,在我的simple Classifier()第一次尝试成功之后不久,我也尝试过这两种方法。我决定不包括这两个细节,因为我发现他们的结果实际上比我们已经取得的结果更糟糕,这很奇怪。
对于额外的谱图转换,我尝试了随机时移和噪声注入。长话短说,它似乎根本没有提高验证的准确性。此后,我认为是这样,因为数据集规范明确表示,所有啾啾将位于中间的录音,因此随机变化的光谱图的目的允许更好的模型泛化实际上可能已经作为损害的表现。然而,我还没有尝试过随机噪声注入。
我还尝试训练一个ResNet50模型,希望进一步提高验证的准确性。这是最令人难以置信的部分:我的模型从来没有超过50%的准确率!直到今天我写这篇文章的时候,我还不确定我做错了什么,所以如果其他人能看看笔记本并帮助我,我很高兴收到任何建议!
结论
总而言之,这是一个真正有趣的努力,花一些时间进行研究。首先,我得重新审视我去年夏天调查过的东西,无可否认,这有一种怀旧的感觉。更重要的是,我们学习了如何实现一个很可能用于真实场景的PyTorch数据集类,在真实场景中,数据不一定像您预期的那样设置。最后,最终的验证分数为84%,对于我即兴创建的如此简单的网络架构来说,这是相当整洁的!