超参数调整和实验
欢迎来到这个神经网络编程系列。在本节中,我们将看到如何使用TensorBoard快速试验不同的训练超参数,以更深入地了解我们的神经网络。
- 准备数据
- 建立模型
- 训练模型
- 分析模型的结果
- 超参数实验
在本系列的这一点上,我们已经了解了如何使用PyTorch构建和训练CNN。在上一集中,我们展示了如何在PyTorch中使用TensorBoard,并回顾了训练过程。
这一节被认为是上一节的第二部分,因此,如果您还没有看过上一节,请补学上一节的内容,以获取了解我们在这里所做的工作所需的详细信息。接下来我们要试验我们的超参数值。
使用TensorBoard进行超参数实验
TensorBoard的优点是它具有开箱即用的功能,可以随时间和跨运行跟踪我们的超参数。(改变超参数并对比多个结果)
没有TensorBoard,此过程将变得更加繁琐。那么我们该怎么做呢?
1、为TensorBoard命名每次的训练运行
要利用TensorBoard比较功能,我们需要进行多次运行,并以可以唯一标识它的方式命名每个运行。
使用PyTorch的SummaryWriter
时,运行将在创建writer对象实例时开始,并在writer实例关闭或超出范围时结束。
为了唯一地标识每个运行,我们可以直接设置运行的文件名,或者将注释字符串传递给构造函数,该构造函数将附加到自动生成的文件名之后。
在创建此帖子时,运行名称包含在SummaryWriter
名为的属性中log_dir
。它是这样创建的:
# PyTorch version 1.1.0 SummaryWriter class
if not log_dir:
import socket
from datetime import datetime
current_time = datetime.now().strftime('%b%d_%H-%M-%S')
log_dir = os.path.join(
'runs',
current_time + '_' + socket.gethostname() + comment
)
self.log_dir = log_dir
在这里,我们可以看到log_dir
对应于磁盘位置和运行名称的属性设置为runs + time + host + comment
。当然,这是假设log_dir
参数没有传入的值。因此,这是默认行为。
命名运行的一种方法是添加参数名称和值作为运行的注释。这将使我们能够在稍后查看TensorBoard内部的运行时查看每个参数值如何与其他参数值叠加。
我们将看到这是我们稍后设置注释的方式:
tb = SummaryWriter(comment = f'batch_size = {batch_size} lr = {lr}')
TensorBoard还具有查询功能,因此我们可以通过查询轻松隔离参数值。
例如,假设此SQL查询:SELECT * FROM TBL_RUNS WHERE lr = 0.01
如果没有SQL,基本上这就是我们在TensorBoard中可以做的事情。
2、为我们的超参数创建变量
为了简化实验,我们将提取硬编码的值并将其转换为变量。
这是硬编码的方式:
network = Network()
train_loader = torch.utils.data.DataLoader(
train_set, batch_size=100
)
optimizer = optim.Adam(
network.parameters(), lr=0.01
)
请注意batch_size
和lr
参数值是如何硬编码的。
这就是我们将其更改为的内容(现在我们的值是使用变量设置的):
batch_size = 100
lr = 0.01
network = Network()
train_loader = torch.utils.data.DataLoader(
train_set, batch_size=batch_size
)
optimizer = optim.Adam(
network.parameters(), lr=lr
)
这将使我们能够在单个位置更改值,并使它们在我们的代码中传播。
现在,我们将使用如下变量为comment参数创建值:
tb = SummaryWriter(comment = f'batch_size = {batch_size} lr = {lr}')
通过此设置,我们可以更改超参数的值,并且我们的运行将在TensorBoard中自动跟踪和识别。
3、计算不同批次大小的损失
由于我们现在将更改批量大小,因此我们需要更改计算和累积损失的方式。不仅仅是对损失函数返回的损失求和。我们将对其进行调整以适应批次大小。
total_loss + = loss.item()* batch_size
为什么这样 我们将cross_entropy
损失函数平均该批次产生的损失值,然后返回该平均损失。这就是为什么我们需要考虑批次大小的原因。
该cross_entropy
函数接受的参数称为reduction
我们也可以使用的参数。
减少参数可选地接受字符串作为参数。此参数指定要应用于损失函数的输出的减少量。
- ‘
none
’ -不会减少费用。 - ‘
mean
’ -输出总和将除以输出中元素的数量。 - ‘
sum
’ -将对输出求和。
请注意,默认值为’mean
’。这就是为什么loss.item() * batch_size
可行。
4、试验超参数值
现在我们有了此设置,我们可以做更多的事情!
我们要做的就是创建一些列表和一些循环,然后我们可以运行代码并坐下来,等待所有组合运行。
这是我们的意思的例子:
参数清单
batch_size_list = [100,1000,10000]
lr_list = [.01,.001,.0001,.00001]
嵌套迭代
for batch_size in batch_size_list:
for lr in lr_list:
network = Network()
train_loader = torch.utils.data.DataLoader(
train_set, batch_size=batch_size
)
optimizer = optim.Adam(
network.parameters(), lr=lr
)
images, labels = next(iter(train_loader))
grid = torchvision.utils.make_grid(images)
comment=f' batch_size={batch_size} lr={lr}'
tb = SummaryWriter(comment=comment)
tb.add_image('images', grid)
tb.add_graph(network, images)
for epoch in range(5):
total_loss = 0
total_correct = 0
for batch in train_loader:
images, labels = batch # Get Batch
preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss
optimizer.zero_grad() # Zero Gradients
loss.backward() # Calculate Gradients
optimizer.step() # Update Weights
total_loss += loss.item() * batch_size
total_correct += get_num_correct(preds, labels)
tb.add_scalar(
'Loss', total_loss, epoch
)
tb.add_scalar(
'Number Correct', total_correct, epoch
)
tb.add_scalar(
'Accuracy', total_correct / len(train_set), epoch
)
for name, param in network.named_parameters():
tb.add_histogram(name, param, epoch)
tb.add_histogram(f'{name}.grad', param.grad, epoch)
print(
"epoch", epoch
,"total_correct:", total_correct
,"loss:", total_loss
)
tb.close()
这段代码完成后,我们将运行TensorBoard,所有运行将以图形方式显示并易于比较。
tensorboard --logdir runs
批次大小 VS 训练集大小
如果训练集大小不能被批次大小整除,则最后一批数据将包含比其他批次更少的样本。
解决此差异的一种简单方法是删除最后一批。PyTorch DataLoader
类使我们能够通过设置做到这一点drop_last=True
。默认情况下, drop_last
参数值设置为False
。
让我们考虑包括一个样本数量少于批次大小的批次如何影响total_loss
上面代码中的计算。
对于每个批次,我们都使用batch_size
变量来更新total_loss
值。我们正在按该batch_size
值按比例放大批次中样品的平均损失值。但是,正如我们刚刚讨论的那样,有时最后一批将包含更少的样本。因此,以预定batch_size
值进行缩放是不准确的。
通过动态访问每个批次的样本数量,可以将代码更新为更准确。
当前,我们有以下内容:
total_loss += loss.item() * batch_size
使用下面的更新代码,我们可以获得更准确的total_loss值:
total_loss += loss.item() * images.shape[0]
请注意,total_loss
当训练集大小可被批处理大小整除时,这两行代码为我们提供相同的值。感谢Alireza Abedin Varamin在YouTube上的评论中指出了这一点。
5、向TensorBoard添加网络参数和渐变
请注意,在上一节中,我们向TensorBoard添加了以下值:
conv1.weight
conv1.bias
conv1.weight.grad
我们使用以下代码进行了此操作:
tb.add_histogram('conv1.bias', network.conv1.bias, epoch)
tb.add_histogram('conv1.weight', network.conv1.weight, epoch)
tb.add_histogram('conv1.weight.grad', network.conv1.weight.grad, epoch)
现在,我们通过使用以下循环为所有图层添加这些值来增强此功能:
for name, weight in network.named_parameters():
tb.add_histogram(name, weight, epoch)
tb.add_histogram(f'{name}.grad', weight.grad, epoch)
之所以可行,是因为nn.Module
调用的PyTorch 方法named_parameters
()为我们提供了网络内部所有参数的名称和值。
在不嵌套的情况下添加更多超参数
这很酷。但是,如果我们要添加第三个甚至第四个参数进行迭代该怎么办?这将使许多嵌套的for循环变得混乱。
有一个解决方案。我们可以为每次运行创建一组参数,并将所有参数打包为一个可迭代的参数。这是我们的方法。
如果我们有参数列表,则可以使用笛卡尔积
将它们打包为每个运行的集合 。为此,我们将使用itertools
库中的product
函数。
from itertools import product
# product函数将允许我们计算所有参数类型的笛卡尔积
Init signature: product(*args, **kwargs)
Docstring:
"""
product(*iterables, repeat=1) --> product object
Cartesian product of input iterables. Equivalent to nested for-loops.
"""
接下来,我们定义一个字典,其中包含作为键的参数和要用作值的参数值。
parameters = dict(
lr = [.01, .001]
,batch_size = [100, 1000]
,shuffle = [True, False]
)
接下来,我们将创建可传递给product
函数的可迭代列表。
param_values = [v for v in parameters.values()]
param_values
[[0.01, 0.001], [100, 1000], [True, False]]
现在,我们有三个参数值列表。取这三个列表的笛卡尔积后,我们将为每个运行提供一组参数值。请注意,这等效于嵌套的for
循环,如该product
函数的doc
字符串所示。
for lr, batch_size, shuffle in product(*param_values):
print (lr, batch_size, shuffle)
# 这里的 `*` 号是告诉乘积函数把列表中每个值作为参数,而不是把列表本身当做参数来对待
2 * 2 * 2 = 8种参数组合
0.01 100 True
0.01 100 False
0.01 1000 True
0.01 1000 False
0.001 100 True
0.001 100 False
0.001 1000 True
0.001 1000 False
好了,现在我们可以使用单个for循环遍历每组参数。我们要做的就是使用序列解包对集合进行解包。看起来像这样。
for lr, batch_size, shuffle in product(*param_values):
comment = f' batch_size={batch_size} lr={lr} shuffle={shuffle}'
train_loader = torch.utils.data.DataLoader(
train_set
,batch_size=batch_size
,shuffle=shuffle
)
optimizer = optim.Adam(
network.parameters(), lr=lr
)
# Rest of training process given the set of parameters
注意:我们构建注释字符串以标识运行的方式。我们只是插入值。另外,请注意*
操作元。这是Python中将列表解压缩为一组参数的一种特殊方法。因此,在这种情况下,我们将三个单独的未打包参数传递给与product
单个列表相对的函数。
这是*
,星号,splat
,点差运算符的两个参考。这些都是这一名称的通用名称。
欢迎来到这个神经网络编程系列。在本节中,我们将看到如何使用TensorBoard快速试验不同的训练超参数,以更深入地了解我们的神经网络。
我们将学习如何通过构建注释字符串并将其传递给SummeryWriter
构造函数并将其附加到自动生成的文件名之后,来唯一标识每次运行。
我们将学习如何使用笛卡尔积来创建一组超级参数以进行尝试,最后,我们将考虑目标与智能之间的关系。
英文原文链接是:https://deeplizard.com/learn/video/ycxulUVoNbk