CNN训练循环重构
欢迎来到这个神经网络编程系列。在这一节中,我们将看到如何在保持训练循环和结果井井有条的同时轻松地对大量超参数值进行实验。
整理训练循环并提取类
当我们结束了前几节的训练循环后,我们建立了很多功能,使我们可以尝试许多不同的参数和值,并且还在训练循环中进行了必要的调用,并且使结果在TensorBoard
展示。
所有这些工作都是有用的,但是我们的训练循环现在非常的繁琐冗余。在本节中,我们将清理训练循环并通过使用上节建立RunBuilder
类和建立一个新的名为RunManager
的类来为进一步的实验打下基础。
我们的目标是能够在顶部添加参数和值,并在多次训练中测试或尝试所有值。
例如,在这种情况下,我们要使用两个参数lr
和batch_size
,对于,batch_size
我们要尝试两个不同的值。这总共给了我们两次训练。批次大小不同时,两次运行的学习率相同。
params = OrderedDict(
lr = [.01]
,batch_size = [1000, 2000]
)
对于结果,我们希望看到并能够比较两次运行。
建立两个新类
为此,我们需要建立两个新类。我们首先建立的类是上节中称为RunBuilder
的类,并在顶部进行调用。
for run in RunBuilder.get_runs(params):
接下来,我们需要构建RunManager
类,此类允许我们在运行循环中管理每个运行。该RunManager
实例将使我们能够提取许多繁琐乏味的TensorBoard
调用,并允许我们添加其他功能。
我们将看到,随着我们的参数的数量和运行次数的增加,TensorBoard将作为审查我们结果的可行方案开始分析。
RunManager
将在每个运行的不同阶段被调用。我们将在run
阶段和epoch
阶段的开始和结束时调用它。我们还将调用它来跟踪每个周期内的损失和正确预测的数量。最后,在结束时,我们将把运行结果保存到磁盘上。
让我们看看如何构建RunManager
类。
构建用于训练循环运行的RunManager
导入资源包
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from IPython.display import display, clear_output
import pandas as pd
import time
import json
from itertools import product
from collections import namedtuple
from collections import OrderedDict
首先,我们使用class关键字声明该类。
class RunManager():
接下来,我们将定义类构造函数。
def __init__(self):
self.epoch_count = 0
self.epoch_loss = 0
self.epoch_num_correct = 0
self.epoch_start_time = None
self.run_params = None
self.run_count = 0
self.run_data = []
self.run_start_time = None
self.network = None
self.loader = None
self.tb = None
现在,我们将在构造函数中不接受任何参数,我们将只定义一些属性,使我们能够跟踪跨运行和跨周期(epoch)的数据。
我们将跟踪以下内容:
- epoch数。
- 一个epoch的运行损失。
- 某个epoch的正确预测数。
- epoch的开始时间。
记住,我们看到RunManager
该类有两个名称为epoch
的方法。我们有begin_epoch
()和end_epoch
()。这两种方法将使我们能够在整个epoch生命周期中管理这些值。
现在,接下来我们有一些运行的属性。我们有一个叫做run_params
的属性。这是运行参数的运行定义。它的值将是RunBuilder
类所返回的运行参数之一。
接下来,我们有属性来跟踪run_count
和run_data
。run_count
给我们提供了运行次数,run_data
是一个列表,我们将用来跟踪每个运行的参数值和每个epoch
的结果,所以我们会看到,我们为每个epoch
添加一个值到这个列表中。然后,我们有运行开始时间,它将被用来计算运行持续时间。
好了,接下来我们将保存网络和用于运行的数据加载器,以及我们可以用来为TensorBoard
保存数据的SummaryWriter
。
什么是代码气味?
你闻到了吗?这段代码有些不对劲的味道 你以前听说过代码的味道吗?你闻过吗?代码气味是一个术语,用来描述一种情况,即我们眼前的代码似乎有些不对劲。这就像是软件开发人员的一种直觉。
代码气味并不意味着一定有问题。代码气味并不意味着代码不正确。它只是意味着可能有更好的方法。在本例中,代码的味道是我们有几个变量名都有前缀。这里使用前缀表示这些变量在某种程度上属于一起。
任何时候看到这种情况,我们都需要考虑去掉这些前缀。属于一起的数据应该在一起。这是通过将数据封装在一个类里面来实现的。毕竟,如果数据属于一起,面向对象的语言给了我们使用类来表达这个事实的能力。
通过提取类进行重构
现在把这段代码留在里面是可以的,但是以后我们可能要重构这段代码,做一个所谓的提取类。这是一种重构技术,我们把这些前缀去掉,然后创建一个叫做Epoch
的类,这个类有这些属性,count
,loss
,num_correct
和start_time
。
class Epoch():
def __init__(self):
self.count = 0
self.loss = 0
self.num_correct = 0
self.start_time = None
然后,我们会用一个Epoch
类的实例来替换这些类变量。我们甚至可以把count
变量改成一个更直观的名字,比如说number
或者id
。我们现在可以不做这个,是因为重构是一个迭代的过程,而这是我们的第一次迭代。
提取类创建抽象层
其实,我们现在构建这个类所做的事情,就是从我们的主训练循环程序中提取一个类。我们要解决的代码味是我们的循环程序变得杂乱无章,开始显得过于复杂。
当我们编写一个主程序,然后重构它时,我们可以认为这创造了抽象层,使主程序越来越可读,越来越容易理解。程序的每个部分都应该是非常容易理解的。
当我们把代码提取到它自己的类或方法中时,我们就是在创建额外的抽象层,如果我们想了解任何一个层的实现细节,我们可以说是潜入其中。
以一种迭代的方式,我们可以认为从一个单一的程序开始,然后,以后再提取代码,创建更深的层次。这个过程可以看作是一个树状的分支结构。
开始训练循环运行
无论如何,让我们看一下该类的第一个方法,该方法提取运行开始所需的代码。
def begin_run(self, run, network, loader):
self.run_start_time = time.time()
self.run_params = run
self.run_count += 1
self.network = network
self.loader = loader
self.tb = SummaryWriter(comment=f'-{run}')
images, labels = next(iter(self.loader))
grid = torchvision.utils.make_grid(images)
self.tb.add_image('images', grid)
self.tb.add_graph(self.network, images)
首先,我们捕获运行的开始时间。然后,我们保存传入的运行参数,并将运行次数递增一。之后,我们保存我们的网络和数据加载器,然后,我们为TensorBoard
初始化一个SummaryWriter
。注意我们是如何将我们的运行作为注释参数传递的。这将允许我们在TensorBoard
里面唯一地识别我们的运行。
好了,接下来我们只要在之前的训练循环中进行一些TensorBoard
调用。这些调用会将我们的网络和一批图像添加到TensorBoard
中。
当我们结束一个运行的时候,我们要做的就是关闭TensorBoard
的句柄,然后把epoch
计数设置回零,为下一次运行做好准备。
def end_run(self):
self.tb.close()
self.epoch_count = 0
为了开始一个时代,我们首先保存开始时间。然后,我们增加epoch_count
一个,并设置epoch_loss
和epoch_number_correct
为零。
def begin_epoch(self):
self.epoch_start_time = time.time()
self.epoch_count += 1
self.epoch_loss = 0
self.epoch_num_correct = 0
现在,让我们看一下在结束一个epoch
的动作发生在哪里。
def end_epoch(self):
epoch_duration = time.time() - self.epoch_start_time
run_duration = time.time() - self.run_start_time
loss = self.epoch_loss / len(self.loader.dataset)
accuracy = self.epoch_num_correct / len(self.loader.dataset)
self.tb.add_scalar('Loss', loss, self.epoch_count)
self.tb.add_scalar('Accuracy', accuracy, self.epoch_count)
for name, param in self.network.named_parameters():
self.tb.add_histogram(name, param, self.epoch_count)
self.tb.add_histogram(f'{name}.grad', param.grad, self.epoch_count)
...
我们首先计算周期持续时间和运行持续时间。由于我们处于一个周期的末尾,因此周期的持续时间是最终的,但此处的运行时长表示当前运行的运行时间。该值将一直运行,直到运行结束。但是,我们仍将在每个周期保存它。
接下来,我们计算epoch_loss
和accuracy
,并根据训练集的大小进行计算。这给了我们每个样本的平均损失。然后,我们将这两个值都传递给TensorBoard
。
接下来,我们像之前一样将网络的权重和梯度值传递给TensorBoard
。
追踪我们训练循环的表现
我们现在已经准备好了这个处理的新内容。当我们预先执行大量的运行时,这是为我们提供更多的见解额外增加的部分。我们要自己保存所有的数据,这样我们就可以在TensorBoard
之外对其进行分析。
def end_epoch(self):
...
results = OrderedDict()
results["run"] = self.run_count
results["epoch"] = self.epoch_count
results['loss'] = loss
results["accuracy"] = accuracy
results['epoch duration'] = epoch_duration
results['run duration'] = run_duration
for k,v in self.run_params._asdict().items(): results[k] = v
self.run_data.append(results)
df = pd.DataFrame.from_dict(self.run_data, orient='columns')
...
在这里,我们正在建立一个字典,其中包含了我们关心的运行的键和值。我们添加 run_count
、epoch_count
、loss
、accuracy
、epoch_duration
和 run_duration
。
然后,我们迭代运行参数中的键和值,将它们添加到结果字典中。这将允许我们看到与性能结果相关的参数。
最后,我们将结果追加到run_data
列表中。
一旦数据被添加到列表中,我们将数据列表变成一个pandas
数据框架,这样我们就可以有格式化的输出。
接下来的两行是Jupyter notebook
特有的。我们清除当前的输出,并显示新的数据框架。
clear_output(wait=True)
display(df)
好了,一个epoch结束了。有一件事你可能想知道,那就是epoch_loss
和epoch_num_correct
的值是如何被追踪的。有两种方法可以做到这一点。
def track_loss(self, loss):
self.epoch_loss += loss.item() * self.loader.batch_size
def track_num_correct(self, preds, labels):
self.epoch_num_correct += self.get_num_correct(preds, labels)
我们有一个叫做track_loss
()的方法和一个叫做track_num_correct
()的方法。这些方法在每次批处理之后都会在训练循环里面被调用。损失被传递到track_loss
()方法中,预测和标签被传递到track_num_correct
()方法中。
为了计算正确预测的数量,我们使用的是与前几集定义的相同的get_num_correct
()函数。这里的区别是,该函数现在被封装在我们的RunManager
类中。
def get_num_correct(self, preds, labels):
return preds.argmax(dim=1).eq(labels).sum().item()
最后,我们有一个名为的方法save
(),用于将run_data
保存为json
和csv
这两种格式。此输出将进入磁盘,并可供其他应用使用。例如,我们可以在excel
中打开csv
文件,甚至可以使用数据构建自己更好的TensorBoard
。
def save(self, fileName):
pd.DataFrame.from_dict(
self.run_data, orient='columns'
).to_csv(f'{fileName}.csv')
with open(f'{fileName}.json', 'w', encoding='utf-8') as f:
json.dump(self.run_data, f, ensure_ascii=False, indent=4)
现在我们可以在训练循环中使用这个RunManager
类。
如果我们使用下面的参数:
params = OrderedDict(
lr = [.01]
,batch_size = [1000, 2000]
,shuffle = [True, False]
)
这些是我们得到的结果:
欢迎来到这个神经网络编程系列。在这一集中,我们将看到如何在保持训练循环和结果井井有条的同时轻松地对大量超参数值进行实验。
英文原文链接是:https://deeplizard.com/learn/video/ozpv_peZ894