超参数调整和实验-训练深度神经网络(pytorch系列-26)

超参数调整和实验

欢迎来到这个神经网络编程系列。在本节中,我们将看到如何使用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_sizelr参数值是如何硬编码的。

这就是我们将其更改为的内容(现在我们的值是使用变量设置的):

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我们也可以使用的参数。

减少参数可选地接受字符串作为参数。此参数指定要应用于损失函数的输出的减少量。

  1. none’ -不会减少费用。
  2. mean’ -输出总和将除以输出中元素的数量。
  3. 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

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值