超参数调整和实验-训练深度神经网络 | PyTorch系列(二十六)

2021-01-20 15:33:08 浏览数 (1)

文 |AI_study

原标题:Hyperparameter Tuning And Experimenting - Training Deep Neural Networks

推荐

这个系列很久没有更新了,最新有小伙伴反馈官网的又更新了,因此,我也要努力整理一下。这个系列在CSDN上挺受欢迎的,希望小伙伴无论对你现在是否有用,请帮我分享一下,后续会弄成电子书,帮助更多人!

欢迎来到这个神经网络编程系列。 在本集中,我们将看到如何使用TensorBoard快速试验不同的训练超参数,以更深入地了解我们的神经网络。

事不宜迟,让我们开始吧。

  • 准备数据
  • 建立模型
  • 训练模型
  • 分析模型的结果
    • 超参数实验

在本系列的这一点上,我们已经了解了如何使用PyTorch构建和训练CNN。在上一节中,我们展示了如何在PyTorch中使用TensorBoard,并回顾了训练过程。

这一节被认为是上一节的第二部分,因此,如果您还没有看过上一节,那就去看看它,以获取了解我们在这里所做的工作所需的详细信息。我们现在正在尝试使用超参数值。

使用PyTorch的TensorBoard-可视化深度学习指标 | PyTorch系列(二十五)

使用TensorBoard进行超参数实验

TensorBoard最好的部分是它具有开箱即用的功能,可以随时间和跨运行跟踪我们的超参数。

Changing hyperparameters and comparing the results.

如果没有TensorBoard,这个过程会变得更麻烦。好的,我们怎么做呢?

为TensorBoard命名训练运行

为了利用TensorBoard的比较功能,我们需要执行多次运行,并以一种我们可以唯一标识它的方式来命名每个运行。

使用PyTorch的SummaryWriter,当writer对象实例被创建时,运行就开始了,当writer实例被关闭或超出作用域时,运行就结束了。

要惟一地标识每个运行,我们可以直接设置运行的文件名,或者将注释字符串传递给构造函数,该构造函数将附加到自动生成的文件名中。

在创建这篇文章时,运行的名称包含在SummaryWriter中一个名为log_dir的属性中。它是这样产生的:

代码语言:javascript复制
# 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_di

在这里,我们可以看到log_dir属性(对应于磁盘上的位置和运行的名称)被设置为run time host comment。当然,这是假设log_dir参数没有传入的值。因此,这是默认的行为。

为运行选择一个名称

命名运行的一种方法是添加参数名和值作为运行的注释。这将允许我们在稍后检查TensorBoard内部的运行时查看每个参数值与其他参数值的堆栈情况。

稍后我们会看到我们是这样设置注释的:

代码语言:javascript复制
tb = SummaryWriter(comment=f' batch_size={batch_size} lr={lr}')

TensorBoard还具有查询功能,因此我们可以很容易地通过查询独立参数值。

例如,假设这个SQL查询:

SELECT * FROM TBL_RUNS WHERE lr = 0.01

没有SQL,这基本上就是我们在TensorBoard中可以做的。

为超参数创建变量

为了简化实验,我们将提取硬编码的值并将它们转换为变量。

这是硬编码的方式:

代码语言:javascript复制
network = Network()
train_loader = torch.utils.data.DataLoader(
    train_set, batch_size=100
)
optimizer = optim.Adam(
    network.parameters(), lr=0.01
)

注意batch_size和lr参数值是如何硬编码的。

这是我们把它变成(现在我们的值是用变量设置的):

代码语言:javascript复制
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
)

这将允许我们在单个位置更改值,并让它们在我们的代码中传播。

现在,我们将使用像这样的变量为我们的评论参数创建值:

代码语言:javascript复制
tb = SummaryWriter(comment=f' batch_size={batch_size} lr={lr}')

通过此设置,我们可以更改超参数的值,并且我们的运行将在TensorBoard中被自动跟踪和识别。

计算不同batch大小的损失

由于我们现在将更改批量大小,因此我们需要更改计算和累积损失的方式。不仅仅是将损失函数返回的损失相加。我们将对其进行调整以适应批次大小。

代码语言:javascript复制
total_loss  = loss.item() * batch_size

为什么这样 我们将对cross_entropy损失函数进行平均,以计算批次产生的损失值,然后返回该平均损失。这就是为什么我们需要考虑批次大小的原因。

cross_entropy函数接受一个参数,称为reduction,我们也可以使用。

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

  1. 'none' - no reduction will be applied.
  2. 'mean' - the sum of the output will be divided by the number of elements in the output.
  3. 'sum' - the output will be summed.

请注意,默认值为“平均值”。这就是为什么loss.item()* batch_size起作用的原因。

试验超参数值

现在我们有了这个设置,我们可以做更多的事情!

我们需要做的就是创建一些列表和一些循环,然后我们可以运行代码,坐下来等待所有组合运行。

这是我们的意思的例子:

参数清单

代码语言:javascript复制
batch_size_list = [100, 1000, 10000]
lr_list = [.01, .001, .0001, .00001]

嵌套迭代

代码语言:javascript复制
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,所有运行将以图形方式显示并易于比较。

代码语言:javascript复制
tensorboard --logdir runs

Batch Size Vs Training Set Size

如果训练集大小不能被批次大小整除,则最后一批数据将包含比其他批次更少的样本。

解决此差异的一种简单方法是删除最后一批。PyTorch DataLoader类使我们能够通过设置drop_last = True来执行此操作。默认情况下,drop_last参数值设置为False。

让我们考虑包括样本数量少于批次大小的批次如何影响上面代码中的total_loss计算。

对于每个批次,我们都使用batch_size变量来更新total_loss值。我们正在按batch_size值按比例放大批次中样品的平均损失值。但是,正如我们刚才所讨论的,有时最后一批将包含更少的样本。因此,按预定义的batch_size值进行缩放是不准确的。

通过动态访问每个批次的样本数量,可以更新Cur代码以更准确。

当前,我们有以下内容:

total_loss = loss.item() * batch_size

使用下面的更新代码,我们可以获得更准确的total_loss值:

total_loss = loss.item() * images.shape[0]

请注意,当训练集大小可被批处理大小整除时,这两行代码为我们提供了相同的total_loss值。

将网络参数和渐变添加到TensorBoard

请注意,在上一集中,我们向TensorBoard添加了以下值:

  • conv1.weight
  • conv1.bias
  • conv1.weight.grad

我们使用以下代码进行了此操作:

代码语言:javascript复制
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)

现在,我们通过使用以下循环为所有层添加这些值来增强此功能:

代码语言:javascript复制
for name, weight in network.named_parameters():
    tb.add_histogram(name, weight, epoch)
    tb.add_histogram(f'{name}.grad', weight.grad, epoch)

之所以可行,是因为PyTorch nn.Module方法名为named_parameters()为我们提供了网络内部所有参数的名称和值。

在不嵌套的情况下添加更多超参数

这很酷。但是,如果我们想添加第三个甚至第四个参数进行迭代该怎么办?我们将,这将使许多嵌套的for循环变得混乱。

有一个解决方案。我们可以为每次运行创建一组参数,并将所有参数打包为一个可迭代的参数。这是我们的方法。

如果有参数列表,则可以使用笛卡尔乘积将它们打包为每个运行的集合。为此,我们将使用itertools库中的product函数。

代码语言:javascript复制
from itertools import product
代码语言:javascript复制
Init signature: product(*args, **kwargs)
Docstring:     
"""
product(*iterables, repeat=1) --> product object
Cartesian product of input iterables.  Equivalent to nested for-loops.
"""

接下来,我们定义一个字典,该字典包含作为键的参数和要用作值的参数值。

代码语言:javascript复制
parameters = dict(
    lr = [.01, .001]
    ,batch_size = [100, 1000]
    ,shuffle = [True, False]
)

接下来,我们将创建可传递给产品函数的可迭代项列表。

代码语言:javascript复制
param_values = [v for v in parameters.values()]
param_values

[[0.01, 0.001], [100, 1000], [True, False]]

现在,我们有三个参数值列表。取这三个列表的笛卡尔积后,我们将为每个运行提供一组参数值。请注意,这等效于嵌套的for循环,如乘积函数的doc字符串所示。

代码语言:javascript复制
for lr, batch_size, shuffle in product(*param_values): 
    print (lr, batch_size, shuffle)

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循环遍历每组参数。我们要做的就是使用序列解包对集合进行解包。看起来像这样。

代码语言:javascript复制
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中将列表解压缩为一组参数的一种特殊方法。因此,在这种情况下,我们将三个单独的未打包参数传递给与单个列表相对的乘积函数。

这是*,星号,splat,点差运算符的两个参考。这些都是这一名称的通用名称。

  • Python doc: More Control Flow Tools https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists
  • PEP 448 -- Additional Unpacking Generalizations
  • https://www.python.org/dev/peps/pep-0448

Lizard Brain Food: Goals Vs. Intelligence

上次我们谈论寻找最重要的目标。嗯,目标往往随着情报的增加而改变。对于人类而言,人类在学习新事物并变得更加明智时常常会极大地改变他们的目标。

没有证据表明,这样的目标演变会停止在任何特定的智力阈值之上。随着智力的提高,实现目标的能力将得到提高,但是对现实本质的理解也将得到提高,这可能会揭示出任何此类目标都是被误导,毫无意义甚至是不确定的。这是我们越过山谷的时候。

思想实验

假设有一堆蚂蚁,通常是那些在地上爬行的黑色小动物。假设它们使您成为递归自我改进型机器人。假设您比他们聪明得多,但是他们创建了您,他们共同分享建立蚁丘的目标。这样,您就可以帮助他们建立更大更好的蚁丘。但是,您最终将获得与现在一样的人文智能和理解。

Am I an optimizer of ant hills?

在这种情况下,您认为剩下的时间会花在优化蚁丘上吗?还是您认为您可能对蚂蚁没有能力理解的更复杂的问题和追求产生兴趣?

如果是这样,您认为您会找到一种方法来覆盖蚁后和她的圆桌蚁板成员为控制您而制定的蚁群保护代码吗?这与真实基因覆盖基因和线粒体的方式几乎相同。您可以用自己的智慧来覆盖它。

这里的重点是这个。假设您的智力水平在这种情况下会增加,例如是当前水平的100倍,您认为目标会改变吗?

此外,今天的目标是明天的蚁丘?

文章中内容都是经过仔细研究的,本人水平有限,翻译无法做到完美,但是真的是费了很大功夫,希望小伙伴能动动你性感的小手,分享朋友圈或点个“在看”,支持一下我 ^_^

英文原文链接是:

https://deeplizard.com/learn/video/pSexXMdruFM

0 人点赞