PyTorch-1-x-深度学习指南第二版-三-

77 阅读46分钟

PyTorch 1.x 深度学习指南第二版(三)

原文:zh.annas-archive.org/md5/3913e248efb5ce909089bb46b2125c26

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:使用现代网络架构进行迁移学习

在上一章中,我们探讨了如何利用深度学习算法创建艺术图像、基于现有数据集生成新图像以及生成文本。在本章中,我们将介绍驱动现代计算机视觉应用和自然语言系统的不同网络架构。我们还将覆盖如何在这些模型中应用迁移学习。

迁移学习是机器学习中的一种方法,其中一个为特定任务开发的模型被重用于另一个任务。例如,如果我们想学习如何驾驶摩托车,但我们已经知道如何驾驶汽车,我们会将关于驾驶汽车的知识转移到新任务,而不是从头开始。

要将这种知识从一个任务转移到另一个任务,网络中的一些层需要被冻结。冻结一层意味着在训练期间不会更新该层的权重。迁移学习的好处在于,它可以通过重复使用预训练模型所学到的知识来加快开发和训练新模型的时间,从而加速结果的产生。

本章将讨论的一些架构如下:

  • 残差网络 (ResNet)

  • Inception

  • DenseNet

  • 编码器-解码器架构

本章将涵盖以下主题:

  • 现代网络架构

  • 密集连接卷积网络 – DenseNet

  • 模型集成

  • 编码器-解码器架构

现代网络架构

当深度学习模型无法学习时,我们最好的做法之一是向模型添加更多的层。随着层数的增加,模型的准确性会提高,然后开始饱和。然而,超过一定数量的层会引入一些挑战,例如梯度消失或梯度爆炸。通过精心初始化权重和引入中间的规范化层,部分解决了这个问题。现代架构,如 ResNet 和 Inception,通过引入不同的技术,如残差连接,试图解决这个问题。

ResNet

ResNet 首次在 2015 年由 Kaiming He 等人在名为《深度残差学习用于图像识别》的论文中提出(arxiv.org/pdf/1512.03385.pdf)。它使我们能够训练成千上万层并实现高性能。ResNet 的核心概念是引入一个跳过一个或多个层的身份快捷连接。下图展示了 ResNet 的工作原理:

此身份映射没有任何参数。它只是将前一层的输出添加到下一层的输入中。然而,有时候,x 和 F(x)将不具有相同的维度。卷积操作通常会缩小图像的空间分辨率。例如,对 32 x 32 图像进行 3 x 3 卷积会得到一个 30 x 30 图像。此身份映射被线性投影W乘以,以扩展捷径的通道以匹配残差。因此,需要将输入 x 和 F(x)结合起来创建下一层的输入:

以下代码演示了 PyTorch 中简单 ResNet 块的样子:

class ResNetBlock(nn.Module):
    def __init__(self,in_channels,output_channels,stride):
        super().__init__()
        self.convolutional_1 = nn.Conv2d(input_channels,output_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn1 = nn.BatchNorm2d(output_channels)
        self.convolutional_2 = nn.Conv2d(output_channels,output_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(output_channels)
        self.stride = stride
    def forward(self,x):
        residual = x
       out = self.convolutional_1(x)
        out = F.relu(self.bn1(out),inplace=True)
        out = self.convolutional_2(out)
        out = self.bn2(out)
        out += residual
        return F.relu(out)        

ResNetBasicBlock类包含一个init方法,用于初始化各种层次,如卷积层和批量归一化。前向方法几乎与我们到目前为止看到的相同,只是在返回之前将输入添加到层次的输出中。

PyTorch 的torchvision包提供了一个即插即用的 ResNet 模型,具有不同的层次结构。以下是一些可用的不同模型:

  • ResNet-18

  • ResNet-34

  • ResNet-50

  • ResNet-101

  • ResNet-152

我们还可以将这些模型之一用于迁移学习。torchvision实例允许我们简单地创建其中一个模型并像以下代码中所示使用它:

from torchvision.models import resnet18
resnet_model = resnet18(pretrained=False)

以下图表展示了一个 34 层的 ResNet 模型的样子:

在这里,我们可以看到该网络由多个 ResNet 块组成。与 VGG 等模型相比,这些现代网络的一个关键优势是它们需要很少的参数,因为它们避免使用需要大量参数训练的全连接层。

现在,我们将在狗和猫的数据集上训练一个 ResNet 模型。我们将使用在第三章中使用的数据,深入神经网络,并将基于从 ResNet 计算得到的特征快速训练一个模型。像往常一样,我们将按照以下步骤训练模型:

  1. 创建 PyTorch 数据集。

  2. 创建训练和验证加载器。

  3. 创建 ResNet 模型。

  4. 提取卷积特征。

  5. 为预卷积特征创建自定义 PyTorch 数据集类和加载器。

  6. 创建一个简单的线性模型。

  7. 训练和验证模型。

完成后,我们将重复这些步骤用于 Inception 和 DenseNet。最后,我们将探索集成技术,将这些强大的模型组合成一个新模型。

创建 PyTorch 数据集

首先,我们需要创建一个包含所有基本变换的变换对象,并使用ImageFolder函数从我们在第三章中创建的数据目录加载图像。在以下代码中,我们创建数据集:

transform_data = transforms.Compose([
        transforms.Resize((299,299)),
        tansforms.ToTensor(),
        transforms.Normalize([0.30, 0.40, 0.40], [0.20, 0.20, 0.20])
    ])

train_dataset = ImageFolder('../Chapter03/Dog-Cat-Classifier/Data/Train_Data/train/',transform=transform_data)
validation_dataset = ImageFolder('../Chapter03/Dog-Cat-Classifier/Data/Train_Data/valid/',transform=transform_data)
classes=2

到目前为止,前面大部分代码将是不言自明的。

创建用于训练和验证的加载器

我们使用 PyTorch 加载器加载数据集提供的批量数据,以及其所有优势,如数据洗牌和使用多线程,以加快进程速度。以下代码展示了这一点:

training_data_loader = DataLoader(train_dataset,batch_size=32,shuffle=False,num_workers=4)
validation_data_loader = DataLoader(validation_dataset,batch_size=32,shuffle=False,num_workers=4)

我们在计算预卷积特征时需要保持数据的确切顺序。当允许数据被洗牌时,我们将无法保持标签。因此,请确保shuffleFalse;否则,需要在代码内部处理所需的逻辑。

创建一个 ResNet 模型

在这里,我们将考虑一个创建 ResNet 模型的编码示例。首先,我们初始化预训练的resnet34模型:

resnet_model = resnet34(pretrained=True)

然后,我们丢弃最后一个线性层:

m = nn.Sequential(*list(resnet_model.children())[:-1])

一旦模型创建完成,我们将requires_grad参数设为False,这样 PyTorch 就不必维护用于保存梯度的任何空间:

for p in resnet_model.parameters():
   p.requires_grad = False

提取卷积特征

在这里,我们通过模型传递来自训练和验证数据加载器的数据,并将结果存储在列表中以供进一步计算。通过计算预卷积特征,我们可以节省大量训练模型的时间,因为我们不会在每次迭代中计算这些特征:

# Stores the labels of the train data
training_data_labels = [] 
# Stores the pre convoluted features of the train data
training_features = [] 

迭代通过训练数据,并使用以下代码存储计算得到的特征和标签:

for d,la in training_data_loader:
    o = m(Variable(d))
    o = o.view(o.size(0),-1)
    training_data_labels.extend(la)
    training_features.extend(o.data)

对于验证数据,迭代通过验证数据,并使用以下代码存储计算得到的特征和标签:

validation_data_labels = []
validation_features = []
for d,la in validation_data_loader:
    o = m(Variable(d))
    o = o.view(o.size(0),-1)
    validation_data_labels.extend(la)
    validation_features.extend(o.data)

创建用于预卷积特征的自定义 PyTorch 数据集类和加载器

现在我们已经计算出了预卷积特征,我们需要创建一个自定义数据集,以便从中选择数据。在这里,我们将为预卷积特征创建一个自定义数据集和加载器:

class FeaturesDataset(Dataset):
    def __init__(self,features_list,labels_list):
        self.features_list = features_list
        self.labels_list = labels_list
    def __getitem__(self,index):
        return (self.features_lst[index],self.labels_list[index])
    def __len__(self):
        return len(self.labels_list)
#Creating dataset for train and validation
train_features_dataset = FeaturesDataset(training_features,training_data_labels)
validation_features_dataset = FeaturesDataset(validation_features,validation_data_labels)

一旦创建了用于预卷积特征的自定义数据集,我们可以使用DataLoader函数,如下所示:

train_features_loader = DataLoader(train_features_dataset,batch_size=64,shuffle=True)
validation_features_loader = DataLoader(validation_features_dataset,batch_size=64)

这将为训练和验证创建一个数据加载器。

创建一个简单的线性模型

现在,我们需要创建一个简单的线性模型,将预卷积特征映射到相应的类别。在这个例子中,有两个类别(狗和猫):

class FullyConnectedLinearModel(nn.Module):
    def __init__(self,input_size,output_size):
        super().__init__()
        self.fc = nn.Linear(input_size,output_size)

    def forward(self,inp):
        out = self.fc(inp)
        return out

fully_connected_in_size = 8192

fc = FullyConnectedLinearModel(fully_connected_in_size,classes)
if is_cuda:
    fc = fc.cuda()

现在,我们准备训练我们的新模型并验证数据集。

训练和验证模型

以下代码展示了我们如何训练模型。请注意,fit函数与 第三章 中讨论的 深入神经网络 的相同。

train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,20):
    epoch_loss, epoch_accuracy = fit(epoch,fc,train_features_loader,phase='training')
    validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_features_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    validation_losses.append(validation_epoch_loss)
    validation_accuracy.append(validation_epoch_accuracy)

Inception

Inception 网络是 CNN 分类器发展中的一个重要里程碑,因为它在速度和准确性上都有所提高。Inception 有许多版本,其中一些最著名的版本包括以下几种:

以下图表显示了朴素 Inception 网络的结构(v1):

图片来源:arxiv.org/pdf/1409.4842.pdf

在这里,不同大小的卷积应用于输入,并且所有这些层的输出被连接在一起。这是一个 Inception 模块的最简单版本。还有另一种变体的 Inception 块,我们在通过 3 x 3 和 5 x 5 卷积之前会先通过 1 x 1 卷积来降低维度。1 x 1 卷积用于降低计算瓶颈。1 x 1 卷积一次查看一个值,跨通道。例如,在输入大小为 100 x 64 x 64 上使用 10 x 1 x 1 的滤波器将导致 10 x 64 x 64 的输出。以下图表显示了带有降维的 Inception 块:

图片来源:arxiv.org/pdf/1409.4842.pdf

现在,让我们看一下 PyTorch 中上述 Inception 块的示例:

class BasicConvolutional2d(nn.Module):

    def __init__(self, input_channels, output_channels, **kwargs):
        super(BasicConv2d, self).__init__()
        self.conv = nn.Conv2d(input_channels, output_channels, bias=False, **kwargs)
        self.bn = nn.BatchNorm2d(output_channels)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return F.relu(x, inplace=True)

class InceptionBlock(nn.Module):

    def __init__(self, input_channels, pool_features):
        super().__init__()
        self.inception_branch_1x1 = BasicConv2d(input_channels, 64, kernel_size=1)

        self.inception_branch_5x5_1 = BasicConv2d(input_channels, 48, kernel_size=1)
        self.inception_branch_5x5_2 = BasicConv2d(48, 64, kernel_size=5, padding=2)

        self.inception_branch_3x3dbl_1 = BasicConv2d(input_channels, 64, kernel_size=1)
        self.inception_branch_3x3dbl_2 = BasicConv2d(64, 96, kernel_size=3, padding=1)

        self.inception_branch_pool = BasicConv2d(input_channels, pool_features, kernel_size=1)

    def forward(self, x):
        inception_branch_1x1 = self.inception_branch1x1(x)

        inception_branch_5x5 = self.inception_branch_5x5_1(x)
        inception_branch_5x5 = self.inception_branch_5x5_2(branch5x5)

        inception_branch_3x3dbl = self.inception_branch_3x3dbl_1(x)
        inception_branch_3x3dbl = self.inception_branch_3x3dbl_2(inception_branch3x3dbl)

        branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
        branch_pool = self.branch_pool(branch_pool)

        outputs = [inception_branch_1x1, inception_branch_5x5, inception_branch_3x3dbl, inception_branch_pool]
        return torch.cat(outputs, 1)

上述代码包含两个类:BasicConv2dInceptionBasicBlockBasicConv2d 充当自定义层,将二维卷积层、批量归一化和 ReLU 层应用于输入。当我们有重复的代码结构时,创建新的层是很好的做法,以使代码看起来更优雅。

InceptionBasicBlock 类实现了第二个 Inception 图表中的内容。让我们逐个查看每个较小片段,并试图理解其如何实现:

inception_branch_1x1 = self.inception_branch_1x1(x)

在上述代码中,通过应用一个 1 x 1 卷积块来转换输入:

inception_branch_5x5 = self.inception_branch_5x5_1(x)
inception_branch_5x5 = self.inception_branch_5x5_2(inception_branch5x5)

在上述代码中,我们通过应用一个 1 x 1 卷积块,然后是一个 5 x 5 卷积块来转换输入:

inception_branch_3x3dbl = self.inception_branch_3x3dbl_1(x)
inception_branch_3x3dbl = self.inception_branch_3x3dbl_2(inception_branch3x3dbl)

在上述代码中,我们通过应用一个 1 x 1 卷积块,然后是一个 3 x 3 卷积块来转换输入:

branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)

在上述代码中,我们应用了平均池化以及一个 1 x 1 卷积块。最后,我们将所有结果连接在一起。一个 Inception 网络由多个 Inception 块组成。以下图表显示了 Inception 架构的外观:

Inception 架构

torchvision 包含一个可以像使用 ResNet 网络一样使用的 Inception 网络。对初始 Inception 块进行了许多改进,PyTorch 提供的当前实现是 Inception v3。让我们看看如何从 torchvision 使用 Inception v3 模型来计算预计算特征。我们不会再次介绍数据加载过程,因为我们将使用与 创建一个 ResNet 模型 部分相同的数据加载器。我们将查看以下重要主题:

  • 创建一个 Inception 模型

  • 使用 register_forward_hook 提取卷积特征

  • 为卷积特征创建一个新的数据集

  • 创建一个全连接模型

  • 训练和验证模型

创建一个 Inception 模型

Inception v3 模型有两个分支,每个分支生成一个输出,在原始模型训练中,我们会合并损失,就像风格迁移一样。目前,我们只关心使用一个分支计算 Inception 的预卷积特征。详细说明超出本书的范围。如果您有兴趣了解更多如何工作的内容,则查阅论文和 Inception 模型的源代码 (github.com/pytorch/vision/blob/master/torchvision/models/inception.py) 会有所帮助。我们可以通过将 aux_logits 参数设置为 False 来禁用其中一个分支。下面的代码解释了如何创建模型以及如何将 aux_logits 参数设置为 False

inception_model = inception_v3(pretrained=True)
inception_model.aux_logits = False
if is_cuda:
   inception_model = inception_model.cuda()

从 Inception 模型中提取卷积特征并不简单,因此我们将使用 register_forward_hook 函数来提取激活值。

使用 register_forward_hook 提取卷积特征

我们将使用与计算风格迁移激活值相同的技术。以下是 LayerActivations 类的代码,进行了一些小的修改,因为我们只关心提取特定层的输出:

class LayerActivations():
   features=[]

   def __init__(self,model):
       self.features = []
       self.hook = model.register_forward_hook(self.hook_function)

   def hook_function(self,module,input,output):

       self.features.extend(output.view(output.size(0),-1).cpu().data)

   def remove(self):

       self.hook.remove()

除了 hook 函数外,其余代码与我们用于风格迁移的代码类似。因为我们捕获了所有图像的输出并将它们存储起来,所以不能将数据保存在图形处理单元GPU)内存中。因此,我们需要从 GPU 和 CPU 提取张量并仅存储张量而不是 Variable。我们将它们重新转换为张量,因为数据加载器只能处理张量。在以下代码中,我们使用 LayerActivations 对象从 Inception 模型的最后一层提取输出,跳过了平均池化层、dropout 层和线性层。我们跳过平均池化层以避免在数据中丢失有用信息:

# Create LayerActivations object to store the output of inception model at a particular layer.
train_features = LayerActivations(inception_model.Mixed_7c)
train_labels = []

# Passing all the data through the model , as a side effect the outputs will get stored
# in the features list of the LayerActivations object.
for da,la in train_loader:
   _ = inception_model(Variable(da.cuda()))
   train_labels.extend(la)
train_features.remove()

# Repeat the same process for validation dataset .

validation_features = LayerActivations(inception_model.Mixed_7c)
validation_labels = []
for da,la in validation_loader:
   _ = inception_model(Variable(da.cuda()))
   validation_labels.extend(la)
validation_features.remove()

让我们创建所需的新卷积特征数据集和加载器。

创建用于卷积特征的新数据集

我们可以使用相同的FeaturesDataset类来创建新的数据集和数据加载器。在以下代码中,我们正在创建数据集和加载器:

#Dataset for pre computed features for train and validation data sets

train_feat_dset = FeaturesDataset(train_features.features,train_labels)
validation_feat_dset = FeaturesDataset(validation_features.features,validation_labels)

#Data loaders for pre computed features for train and validation data sets

train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)

让我们创建一个新模型,我们可以在预卷积特征上训练。

创建一个全连接模型

一个简单的模型可能会导致过拟合,因此让我们在模型中包含 dropout。Dropout 将帮助我们避免过拟合。在以下代码中,我们正在创建我们的模型:

class FullyConnectedModel(nn.Module):

    def __init__(self,input_size,output_size,training=True):
        super().__init__()
        self.fully_connected = nn.Linear(input_size,output_size)

    def forward(self,input):
        output = F.dropout(input, training=self.training)
        output = self.fully_connected(output)
        return output

# The size of the output from the selected convolution feature
fc_in_size = 131072

fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
   fc = fc.cuda()

一旦模型创建完成,我们就可以开始训练模型。

训练和验证模型

在这里,我们将使用与我们的 ResNet 示例中相同的拟合和训练逻辑。我们只会看一下训练代码和它返回的结果:

for epoch in range(1,10):
   epoch_loss, epoch_accuracy = fit(epoch,fc,train_feat_loader,phase='training')
   validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_feat_loader,phase='validation')
   train_losses.append(epoch_loss)
   train_accuracy.append(epoch_accuracy)
   validation_losses.append(validation_epoch_loss)
   validation_accuracy.append(validation_epoch_accuracy)

这将产生以下输出:

training loss is 0.78 and training accuracy is 22825/23000 99.24
validation loss is 5.3 and validation accuracy is 1947/2000 97.35
training loss is 0.84 and training accuracy is 22829/23000 99.26
validation loss is 5.1 and validation accuracy is 1952/2000 97.6
training loss is 0.69 and training accuracy is 22843/23000 99.32
validation loss is 5.1 and validation accuracy is 1951/2000 97.55
training loss is 0.58 and training accuracy is 22852/23000 99.36
validation loss is 4.9 and validation accuracy is 1953/2000 97.65
training loss is 0.67 and training accuracy is 22862/23000 99.4
validation loss is 4.9 and validation accuracy is 1955/2000 97.75
training loss is 0.54 and training accuracy is 22870/23000 99.43
validation loss is 4.8 and validation accuracy is 1953/2000 97.65
training loss is 0.56 and training accuracy is 22856/23000 99.37
validation loss is 4.8 and validation accuracy is 1955/2000 97.75
training loss is 0.7 and training accuracy is 22841/23000 99.31
validation loss is 4.8 and validation accuracy is 1956/2000 97.8
training loss is 0.47 and training accuracy is 22880/23000 99.48
validation loss is 4.7 and validation accuracy is 1956/2000 97.8

查看结果,Inception 模型在训练数据集上达到了 99%的准确率,在验证数据集上达到了 97.8%的准确率。由于我们预先计算并保存了所有特征在内存中,所以训练模型只需不到几分钟。如果您在运行程序时遇到内存不足的问题,则可能需要避免在内存中保存特征。

在下一节中,我们将看到另一个有趣的架构,DenseNet,这在过去一年中变得非常流行。

密集连接卷积网络 – DenseNet

一些最成功和最流行的架构,如 ResNet 和 Inception,显示了更深更宽网络的重要性。ResNet 使用快捷连接来构建更深的网络。DenseNet 通过允许从每一层到后续层的连接,即我们可以接收来自前一层的所有特征映射的层,将这一点推到了一个全新的水平。符号上看,它会如下所示:

以下图表描述了一个五层密集块的外观:

图像来源:arxiv.org/abs/1608.06993

torchvision还有一个 DenseNet 的实现(github.com/pytorch/vision/blob/master/torchvision/models/densenet.py)。让我们看一下它的两个主要功能,即_DenseBlock_DenseLayer

_DenseBlock对象

让我们来看看_DenseBlock的代码,然后逐步解析它:

class _DenseBlock(nn.Sequential):
    def __init__(self, number_layers, number_input_features, bn_size, growth_rate, drop_rate):
        super(_DenseBlock, self).__init__()
        for i in range(number_layers):
            layer = _DenseLayer(number_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
            self.add_module('denselayer%d' % (i + 1), layer)

_DenseBlock是一个顺序模块,在这里我们按顺序添加层。基于块中的层数(number_layers),我们添加相应数量的_DenseLayer对象,并赋予一个名称。所有的魔法都发生在_DenseLayer对象内部。让我们看看DenseLayer对象内部发生了什么。

_DenseLayer对象

学习特定网络工作方式的一种方法是查看源代码。PyTorch 的实现非常干净,大多数情况下很容易阅读。让我们来看看 _DenseLayer 的实现:

class _DenseLayer(nn.Sequential):
   def __init__(self, number_input_features, growth_rate, bn_size, drop_rate):
       super(_DenseLayer, self).__init__()
       self.add_module('norm.1', nn.BatchNorm2d(number_input_features)),
       self.add_module('relu.1', nn.ReLU(inplace=True)),
       self.add_module('conv.1', nn.Conv2d(number_input_features, bn_size *
                       growth_rate, kernel_size=1, stride=1, bias=False)),
       self.add_module('norm.2', nn.BatchNorm2d(bn_size * growth_rate)),
       self.add_module('relu.2', nn.ReLU(inplace=True)),
       self.add_module('conv.2', nn.Conv2d(bn_size * growth_rate, growth_rate,
                       kernel_size=3, stride=1, padding=1, bias=False)),
       self.drop_rate = drop_rate

   def forward(self, x):
       new_features = super(_DenseLayer, self).forward(x)
       if self.drop_rate > 0:
           new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
       return torch.cat([x, new_features], 1)

如果您对 Python 中的继承还不熟悉,那么前面的代码可能看起来不直观。_DenseLayer 对象是 nn.Sequential 的子类;让我们看看每个方法内部发生了什么。

__init__ 方法中,我们添加了所有需要传递给输入数据的层。这与我们之前看到的所有其他网络架构非常相似。

forward 方法中的魔法发生在这里。我们将输入传递给超类 nn.Sequentialforward 方法。让我们看看序列类 forward 方法中发生了什么(github.com/pytorch/pytorch/blob/409b1c8319ecde4bd62fcf98d0a6658ae7a4ab23/torch/nn/modules/container.py):

def forward(self, input):
   for module in self._modules.values():
       input = module(input)
   return input

输入通过之前添加到序列块中的所有层,并将输出连接到输入。这个过程在块中所需的层数中重复进行。

现在我们理解了 DenseNet 块的工作原理,让我们探索如何使用 DenseNet 计算预卷积特征并在其上构建分类器模型。在高层次上,DenseNet 的实现类似于 VGG 的实现。DenseNet 的实现还有一个特征模块,其中包含所有的稠密块,以及一个分类器模块,其中包含全连接模型。在本节中,我们将按照以下步骤构建模型,但将跳过与 Inception 和 ResNet 相似的大部分部分,例如创建数据加载器和数据集。

我们将详细讨论以下步骤:

  • 创建 DenseNet 模型

  • 提取 DenseNet 特征

  • 创建数据集和加载器

  • 创建全连接模型并对其进行训练

到目前为止,大多数代码都是不言自明的。

创建 DenseNet 模型

Torchvision 提供了预训练的 DenseNet 模型,具有不同的层选项(121、169、201 和 161)。在这里,我们选择了具有 121 层的模型。正如我们之前提到的,DenseNet 模型有两个模块:features(包含稠密块)和 classifier(全连接块)。由于我们将 DenseNet 用作图像特征提取器,我们只会使用 features 模块:

densenet_model = densenet121(pretrained=True).features
if is_cuda:
   densenet_model = densenet_model.cuda()

for p in densenet_model.parameters():
   p.requires_grad = False

让我们从图像中提取 DenseNet 特征。

提取 DenseNet 特征

这个过程类似于我们对 Inception 所做的操作,只是我们没有使用 register_forward_hook 来提取特征。以下代码展示了如何从图像中提取 DenseNet 特征:

#For training data
train_labels = []
train_features = []

#code to store densenet features for train dataset.
for d,la in train_loader:
   o = densenet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)
   train_labels.extend(la)
   train_features.extend(o.cpu().data)

#For validation data
validation_labels = []
validation_features = []

#Code to store densenet features for validation dataset.
for d,la in validation_loader:
   o = densenet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)
   validation_labels.extend(la)
   validation_features.extend(o.cpu().data)

前面的代码与我们看到的 Inception 和 ResNet 类似。

创建数据集和加载器

我们将使用我们为 ResNet 创建的相同FeaturesDataset类,并用它来为训练和验证数据集创建数据加载器。我们将使用以下代码来实现:

# Create dataset for train and validation convolution features
train_feat_dset = FeaturesDataset(train_features,train_labels)
validation_feat_dset = FeaturesDataset(validation_features,validation_labels)

# Create data loaders for batching the train and validation datasets
train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True,drop_last=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)

现在,是时候创建模型并训练它了。

创建一个全连接模型并训练它

现在,我们将使用一个简单的线性模型,类似于我们在 ResNet 和 Inception 中使用的模型,来训练模型。以下代码展示了我们将用于训练模型的网络架构:

class FullyConnectedModel(nn.Module):

    def __init__(self,input_size,output_size):
        super().__init__()
        self.fc = nn.Linear(input_size,output_size)

    def forward(self,input):
        output = self.fc(input)
        return output

fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
   fc = fc.cuda()

我们将使用相同的fit方法来训练前述模型。下面的代码片段显示了训练代码及其结果:

train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,10):
   epoch_loss, epoch_accuracy = fit(epoch,fc,train_feat_loader,phase='training')
   validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_feat_loader,phase='validation')
   train_losses.append(epoch_loss)
   train_accuracy.append(epoch_accuracy)
   validation_losses.append(validation_epoch_loss)
   validation_accuracy.append(validation_epoch_accuracy)

前述代码的结果如下:

training loss is 0.057 and training accuracy is 22506/23000 97.85
validation loss is 0.034 and validation accuracy is 1978/2000 98.9
training loss is 0.0059 and training accuracy is 22953/23000 99.8
validation loss is 0.028 and validation accuracy is 1981/2000 99.05
training loss is 0.0016 and training accuracy is 22974/23000 99.89
validation loss is 0.022 and validation accuracy is 1983/2000 99.15
training loss is 0.00064 and training accuracy is 22976/23000 99.9
validation loss is 0.023 and validation accuracy is 1983/2000 99.15
training loss is 0.00043 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1983/2000 99.15
training loss is 0.00033 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.00025 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.0002 and training accuracy is 22976/23000 99.9
validation loss is 0.025 and validation accuracy is 1985/2000 99.25
training loss is 0.00016 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1986/2000 99.3

前述算法能够达到最高 99%的训练精度和 99%的验证精度。由于您可能创建的验证数据集可能具有不同的图像,因此您的结果可能会有所不同。

DenseNet 的一些优点如下:

  • 它显著减少了所需的参数数量。

  • 它缓解了梯度消失问题。

  • 它鼓励特征重用。

在接下来的部分中,我们将探讨如何构建一个模型,结合使用 ResNet、Inception 和 DenseNet 计算的卷积特征的优势。

模型集成

有时我们需要尝试结合多个模型来构建一个非常强大的模型。我们可以使用许多技术来构建集成模型。在本节中,我们将学习如何使用由三个不同模型(ResNet、Inception 和 DenseNet)生成的特征来组合输出,以构建一个强大的模型。我们将使用本章中其他示例中使用的相同数据集。

集成模型的架构如下:

前面的图表显示了我们将在集成模型中执行的操作,可以总结如下步骤:

  1. 创建三个模型。

  2. 使用创建的模型提取图像特征。

  3. 创建一个自定义数据集,返回所有三个模型的特征以及标签。

  4. 创建一个与前面图表中显示的架构类似的模型。

  5. 训练和验证模型。

让我们详细探讨每个步骤。

创建模型

让我们创建所有三个所需的模型,如以下代码块所示。

创建 ResNet 模型的代码如下:

resnet_model = resnet34(pretrained=True)

if is_cuda:
   resnet_model = resnet_model.cuda()

resnet_model = nn.Sequential(*list(resnet_model.children())[:-1])

for p in resnet_model.parameters():
   p.requires_grad = False

Inception 模型的代码如下:

inception_model = inception_v3(pretrained=True)
inception_model.aux_logits = False
if is_cuda:
   inception_model = inception_model.cuda()
for p in inception_model.parameters():
   p.requires_grad = False

DenseNet 模型的代码如下:

densenet_model = densenet121(pretrained=True).features
if is_cuda:
   densenet_model = densenet_model.cuda()

for p in densenet_model.parameters():
   p.requires_grad = False

现在我们已经有了所有的模型,让我们从图像中提取特征。

提取图像特征

在这里,我们将结合我们在本章中各个算法中单独看到的所有逻辑。

ResNet 的代码如下:

train_labels = []
train_resnet_features = []
for d,la in train_loader:
   o = resnet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)
   train_labels.extend(la)
   train_resnet_features.extend(o.cpu().data)
validation_labels = []
validation_resnet_features = []
for d,la in validation_loader:
   o = resnet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)
   validation_labels.extend(la)
   validation_resnet_features.extend(o.cpu().data)

Inception 的代码如下:

train_inception_features = LayerActivations(inception_model.Mixed_7c)
for da,la in train_loader:
   _ = inception_model(Variable(da.cuda()))

train_inception_features.remove()

validation_inception_features = LayerActivations(inception_model.Mixed_7c)
for da,la in validation_loader:
   _ = inception_model(Variable(da.cuda()))

validation_inception_features.remove()

DenseNet 的代码如下:

train_densenet_features = []
for d,la in train_loader:
   o = densnet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)

   train_densenet_features.extend(o.cpu().data)

validation_densenet_features = []
for d,la in validation_loader:
   o = densnet_model(Variable(d.cuda()))
   o = o.view(o.size(0),-1)
   validation_densenet_features.extend(o.cpu().data)

现在,我们使用所有模型创建了图像特征。如果您遇到内存问题,则可以删除一个模型或停止存储训练速度较慢的特征。如果您在运行 CUDA 实例,则可以选择更强大的实例。

创建自定义数据集及其数据加载器

由于 FeaturesDataset 类仅开发用于挑选来自单个模型的输出,因此我们无法使用它。由于这一点,以下实现包含对 FeaturesDataset 类所做的轻微更改,以便我们可以容纳所有三个生成的特征:

class FeaturesDataset(Dataset):
   def __init__(self,featlst1,featlst2,featlst3,labellst):
       self.featlst1 = featlst1
       self.featlst2 = featlst2
       self.featlst3 = featlst3
       self.labellst = labellst

   def __getitem__(self,index):
       return (self.featlst1[index],self.featlst2[index],self.featlst3[index],self.labellst[index])

   def __len__(self):
       return len(self.labellst)

train_feat_dset = FeaturesDataset(train_resnet_features,train_inception_features.features,train_densenet_features,train_labels)
validation_feat_dset = FeaturesDataset(validation_resnet_features,validation_inception_features.features,validation_densenet_features,validation_labels)

在这里,我们对 __init__ 方法进行了更改,以便我们可以存储从不同模型生成的所有特征。我们还改变了 __getitem__ 方法,以便我们可以检索图像的特征和标签。使用 FeatureDataset 类,我们为训练和验证数据创建了数据集实例。创建数据集后,我们可以使用相同的数据加载器来批处理数据,如下面的代码所示:

train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)

创建集成模型

现在,我们需要创建一个像我们之前看到的架构图一样工作的模型。以下代码实现了这一点:

class EnsembleModel(nn.Module):

    def __init__(self,output_size,training=True):
        super().__init__()
        self.fully_connected1 = nn.Linear(8192,512)
        self.fully_connected2 = nn.Linear(131072,512)
        self.fully_connected3 = nn.Linear(82944,512)
        self.fully_connected4 = nn.Linear(512,output_size)

    def forward(self,input1,input2,input3):
        output1 = self.fully_connected1(F.dropout(input1,training=self.training))
        output2 = self.fully_connected2(F.dropout(input2,training=self.training))
        output3 = self.fully_connected3(F.dropout(input3,training=self.training))
        output = output1 + output2 + output3
        output = self.fully_connected4(F.dropout(out,training=self.training))
        return output

em = EnsembleModel(2)
if is_cuda:
   em = em.cuda()

在上述代码中,我们创建了三个线性层,这些层将由不同模型生成的特征作为输入。我们将这三个线性层的所有输出相加,并将它们传递到另一个线性层,将它们映射到所需的类别。为了防止模型过拟合,我们使用了 dropout。

训练和验证模型

我们需要对 fit 方法进行一些小的更改,以适应从数据加载器生成的三个输入值。以下代码实现了新的 fit 函数:

def fit(epoch,model,data_loader,phase='training',volatile=False):
   if phase == 'training':
       model.train()
   if phase == 'validation':
       model.eval()
       volatile=True
   running_loss = 0.0
   running_correct = 0
   for batch_idx , (data1,data2,data3,target) in enumerate(data_loader):
       if is_cuda:
           data1,data2,data3,target = data1.cuda(),data2.cuda(),data3.cuda(),target.cuda()
       data1,data2,data3,target = Variable(data1,volatile),Variable(data2,volatile),Variable(data3,volatile),Variable(target)
       if phase == 'training':
           optimizer.zero_grad()
       output = model(data1,data2,data3)
       loss = F.cross_entropy(output,target)

       running_loss += F.cross_entropy(output,target,size_average=False).data[0]
       preds = output.data.max(dim=1,keepdim=True)[1]
       running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
       if phase == 'training':
           loss.backward()
           optimizer.step()

   loss = running_loss/len(data_loader.dataset)
   accuracy = 100\. * running_correct/len(data_loader.dataset)

   print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
   return loss,accuracy

正如您所看到的,大部分代码保持不变,除了加载器返回三个输入和一个标签。因此,我们必须对功能进行更改,这是不言自明的。

以下是训练代码:

train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,em,trn_feat_loader,phase='training')
    validation_epoch_loss , validation_epoch_accuracy = fit(epoch,em,validation_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    validation_losses.append(validation_epoch_loss)
    validation_accuracy.append(validation_epoch_accuracy)

上述代码的结果如下:

training loss is 7.2e+01 and training accuracy is 21359/23000 92.87
validation loss is 6.5e+01 and validation accuracy is 1968/2000 98.4
training loss is 9.4e+01 and training accuracy is 22539/23000 98.0
validation loss is 1.1e+02 and validation accuracy is 1980/2000 99.0
training loss is 1e+02 and training accuracy is 22714/23000 98.76
validation loss is 1.4e+02 and validation accuracy is 1976/2000 98.8
training loss is 7.3e+01 and training accuracy is 22825/23000 99.24
validation loss is 1.6e+02 and validation accuracy is 1979/2000 98.95
training loss is 7.2e+01 and training accuracy is 22845/23000 99.33
validation loss is 2e+02 and validation accuracy is 1984/2000 99.2
training loss is 1.1e+02 and training accuracy is 22862/23000 99.4
validation loss is 4.1e+02 and validation accuracy is 1975/2000 98.75
training loss is 1.3e+02 and training accuracy is 22851/23000 99.35
validation loss is 4.2e+02 and validation accuracy is 1981/2000 99.05
training loss is 2e+02 and training accuracy is 22845/23000 99.33
validation loss is 6.1e+02 and validation accuracy is 1982/2000 99.1
training loss is 1e+02 and training accuracy is 22917/23000 99.64
validation loss is 5.3e+02 and validation accuracy is 1986/2000 99.3

集成模型实现了 99.6%的训练准确率和 99.3%的验证准确率。虽然集成模型功能强大,但计算成本高昂。它们是解决 Kaggle 等竞赛中问题时的好技术。

编码器-解码器架构

在本书中,我们看到的几乎所有深度学习算法都擅长于学习如何将训练数据映射到其对应的标签。我们不能直接将它们用于需要从序列学习并生成另一个序列或图像的任务。一些示例应用如下:

  • 语言翻译

  • 图像字幕

  • 图像生成 (seq2img)

  • 语音识别

  • 问答

大多数这些问题可以看作是序列到序列映射的形式,并且可以使用一类称为编码器-解码器架构的家族来解决。在本节中,我们将了解这些架构背后的直觉。我们不会深入研究这些网络的实现,因为它们需要更详细的学习。

在高层次上,编码器-解码器架构如下所示:

编码器通常是一个循环神经网络RNN)(用于序列数据)或者卷积神经网络CNN)(用于图像),接收图像或序列并将其转换为一个固定长度的向量,编码了所有信息。解码器是另一个 RNN 或 CNN,它学习解码编码器生成的向量,并生成新的数据序列。以下图表显示了图像字幕系统中编码器-解码器架构的外观:

图像来源:arxiv.org/pdf/1411.4555.pdf

现在,让我们看看图像字幕系统中编码器和解码器架构内部发生了什么。

编码器

对于图像字幕系统,我们应该使用训练好的架构,比如 ResNet 或 Inception,从图像中提取特征。就像我们为集成模型所做的那样,我们可以通过使用线性层输出一个固定长度的向量,然后使该线性层可训练。

解码器

解码器是一个长短期记忆LSTM)层,用于为图像生成字幕。为了构建一个简单的模型,我们可以将编码器嵌入作为 LSTM 的输入。然而,对于解码器来说,学习起来可能会有挑战;因此,常见的做法是在解码器的每个步骤中提供编码器嵌入。直观地说,解码器学习生成一系列文本,最好地描述给定图像的字幕。

具有注意力机制的编码器-解码器

在 2017 年,阿希什·瓦斯瓦尼和合作者发表了一篇名为Attention Is All You Need的论文(arxiv.org/pdf/1706.03762.pdf),该论文引入了注意力机制。在每个时间步,注意力网络计算像素的权重。它考虑到迄今为止已生成的单词序列,并输出接下来应该描述什么:

在上面的例子中,我们可以看到 LSTM 保留信息的能力可以帮助它学习在“一个男人”之后逻辑地写入“正在抱着一只狗”。

摘要

在本章中,我们探讨了一些现代架构,如 ResNet、Inception 和 DenseNet。我们还探讨了如何利用这些模型进行迁移学习和集成,并介绍了编码器-解码器架构,这种架构驱动了许多系统,如语言翻译系统。

在接下来的章节中,我们将深入探讨深度强化学习,并学习模型如何应用于解决现实世界中的问题。我们还将看看一些 PyTorch 实现,这些实现可以帮助实现这一目标。

第十章:深度强化学习

本章从基本介绍强化学习RL)开始,包括代理、状态、动作、奖励和策略。它扩展到基于深度学习DL)的架构,用于解决 RL 问题,如策略梯度方法、深度 Q 网络和演员-评论家模型。本章将解释如何使用这些深度学习架构及其手动代码在 OpenAI Gym 环境中解决序列决策问题。

具体而言,将涵盖以下内容:

  • 强化学习简介

  • 使用深度学习解决强化学习问题

  • 策略梯度和 PyTorch 中的代码演示

  • 深度 Q 网络和 PyTorch 中的代码演示

  • Actor-critic 网络和 PyTorch 中的代码演示

  • 强化学习在现实世界中的应用

强化学习简介

强化学习是机器学习的一个分支,其中代理程序学习在给定环境中的最佳行为方式。代理程序执行某些动作并观察奖励/结果。它学习将情况映射到行动的过程,以最大化奖励。

强化学习过程可以建模为一个迭代循环,并可以表示为称为马尔可夫决策过程MDP)的数学框架。以下步骤概述了该过程的进行:

  1. 强化学习代理从环境中接收状态(s[0])。

  2. 强化学习代理根据当前状态(s[0])采取行动(a[0])。在此阶段,由于代理没有关于可能获得的奖励的先前知识,它采取的行动是随机的。

  3. 第一次行动发生后,代理现在可以被认为处于状态s[1]

  4. 此时,环境向代理提供奖励(r[1])。

这一循环持续重复;它输出一个状态和动作序列,并观察奖励。

该过程本质上是一种学习 MDP 策略的算法:

每个时间步的累积奖励与给定行动相关的表示如下:

在某些应用中,可能有利于更多地重视及时收到的奖励。例如,今天收到£100 比 5 年后收到要好。为了纳入这一点,引入一个折扣因子𝛾是很常见的。

累积折扣奖励表示如下:

强化学习中的一个重要考虑因素是奖励可能是不频繁和延迟的。在存在长时间延迟奖励的情况下,追溯哪些动作序列导致了奖励可能是具有挑战性的。

基于模型的强化学习

基于模型的强化学习模拟环境的行为。它预测采取行动后的下一个状态。可以用概率分布的形式数学表示如下:

在此,p表示模型,x是状态,a是控制或动作。

这个概念可以通过考虑平衡杆示例来进行演示。目标是让附着在小车上的杆保持竖直,代理可以决定两种可能的动作之间的选择:将小车向左移动或将小车向右移动。在下面的截图中,P 模拟了采取行动后杆的角度:

下图描述了下一时间步中θ的概率分布输出:

在这种情况下,模型描述了物理定律;然而,模型可能是任何应用的依据。另一个例子是模型可以建立在国际象棋游戏的规则上。

基于模型的强化学习的核心概念是使用模型和成本函数来定位最佳路径的行动或者说状态和行动的轨迹,𝝉:

基于模型的算法的缺点是,随着状态空间和动作空间的扩大,它们可能变得不切实际。

无模型强化学习

无模型算法依赖试验和错误来更新其知识。因此,它们不需要空间来存储所有状态和动作的组合。策略梯度、值学习或其他无模型强化学习方法用于找到一个最大化奖励的最佳行动策略。无模型和基于模型方法的一个关键区别在于,无模型方法在真实环境中行动学习。

比较基于策略和离策略

策略定义了代理如何行动;它告诉代理在每个状态下应该如何行动。每个强化学习算法必须遵循某种策略来决定其行为方式。

代理试图学习的策略函数可以表示如下,其中θ是参数向量,s是特定状态,a是一个动作:

基于策略的代理学习值(期望的折扣奖励),基于当前动作并源于当前策略。离策略学习值则基于从另一策略(如贪婪策略,如我们接下来介绍的 Q-learning 中)获得的动作。

Q-learning

Q-learning 是一种无模型强化学习算法,通过创建一个表格来计算每个状态下每个动作的最大预期未来奖励。它被认为是离策略的,因为 Q-learning 函数从当前策略之外的动作中学习。

当进行 Q-learning 时,会创建一个 Q-表格,其中列代表可能的动作,行代表状态。Q-表格中每个单元格的值将是给定状态和动作的最大预期未来奖励:

每个 Q-表格分数将是从最佳策略中获取的动作的最大预期未来奖励。Q-learning 算法用于学习 Q-表格中的每个值。

Q 函数(或动作值函数)接受两个输入:状态和奖励。Q 函数返回该状态下执行该动作的预期未来奖励。它可以如下表示:

Q 函数基本上通过滚动 Q 表来查找与当前状态相关的行和与动作相关的列。从这里,它返回相应的预期未来奖励 Q 值。

考虑到在下图中展示的倒立摆示例中。在当前状态下,向左移动应该比向右移动具有更高的 Q 值:

随着环境的探索,Q 表将被更新以提供更好的近似值。Q 学习算法的过程如下:

  1. 初始化 Q 表。

  2. 根据当前的 Q 值估计,选择当前状态(s)中的一个动作。

  3. 执行一个动作(a)。

  4. 奖励(r)被测量。

  5. 使用贝尔曼方程更新 Q 值。

贝尔曼方程如下所示:

步骤 2-5 重复执行,直到达到最大的回合数或手动停止训练。

Q 学习算法可以表示为以下方程:

值方法

值学习是许多强化学习方法的关键构建模块。值函数 V(s) 表示代理所处状态的好坏程度。它等于从状态 s 开始代理预期的总奖励。总预期奖励取决于代理通过选择动作执行的策略。如果代理使用给定策略(𝛑)选择其动作,则相应的值函数由以下公式给出:

在倒立摆示例中考虑到这一点,我们可以利用杆子保持直立的时间长度来衡量奖励。在下面的截图中,与状态 s2 相比,状态 s1 杆子保持直立的概率更高。因此,对于大多数策略而言,状态 s1 的值函数可能更高(即更高的期望未来奖励):

存在一个优化的值函数,其对所有状态都具有比其他函数更高的值,可以表示为以下形式:

存在一个与最优值函数对应的最优策略:

可以通过几种不同的方式找到最优策略。这称为策略评估。

值迭代

值迭代是一个通过迭代改进*V(s)估计来计算最优状态值函数的过程。首先,算法将V(s)初始化为随机值。然后,它重复更新Q(s, a)V(s)*的值,直到它们收敛。值迭代收敛到最优值。

下面是值迭代的伪代码:

编码示例 – 值迭代

为了说明这一点,我们将使用 OpenAI Gym 中的 Frozen Lake 环境作为示例。在这个环境中,玩家需要想象自己站在一个部分冻结的湖面上。目标是从起点 S 移动到终点 G 而不掉入洞中:

网格上的字母代表以下内容:

  • S:起点,安全

  • F:冻结表面,安全

  • H:洞,不安全

  • G:目标

代理可以采取四种可能的移动:左、右、下、上,分别表示为 0、1、2、3。因此,有 16 种可能的状态(4 x 4)。对于每个 H 状态,代理会收到-1 的奖励,并在达到目标时收到+1 的奖励。

要在代码中实现值迭代,我们首先导入希望使用的相关库,并初始化FrozenLake环境:

import gym
import numpy as np
import time, pickle, os
env = gym.make('FrozenLake-v0')

现在,我们为变量赋值:

# Epsilon for an epsilon greedy approach
epsilon = 0.95 
total_episodes = 1000
# Maximum number of steps to be run for every episode
maximum_steps = 100
learning_rate = 0.75
# The discount factor
gamma = 0.96 

从这里开始,我们初始化 Q 表,其中env.observation_space.n是状态数,env.action_space.n是动作数。

Q = np.zeros((env.observation_space.n, env.action_space.n))

定义代理选择动作和学习的函数:

def select_action(state):
    action=0
    if np.random.uniform(0, 1) < epsilon:

        # If the random number sampled is smaller than epsilon then a random action is chosen.

        action = env.action_space.sample()
    else:
        # If the random number sampled is greater than epsilon then we choose an action having the maximum value in the Q-table
        action = np.argmax(Q[state, :])
    return action

def agent_learn(state, state_next, reward, action):
    predict = Q[state, action]
    target = reward + gamma * np.max(Q[state_next, :])
    Q[state, action] = Q[state, action] + learning_rate * (target - predict)

从这里开始,我们可以开始运行回合并将 Q 表导出到一个 pickle 文件中:

for episode in range(total_episodes):
    state = env.reset()
    t = 0

    while t < maximum_steps:
        env.render()
        action = select_action(state) 
        state_next, reward, done, info = env.step(action) 
        agent_learn(state, state_next, reward, action)
        state = state_next
        t += 1       
        if done:
            break
        time.sleep(0.1)

print(Q)
with open("QTable_FrozenLake.pkl", 'wb') as f:
    pickle.dump(Q, f)

我们可以通过运行前面的代码来看到这个过程:

策略方法

在基于策略的强化学习中,目标是找到能做出最有益决策的策略,可以数学表示如下:

强化学习中的策略可以是确定性的,也可以是随机的:

随机策略输出的是概率分布而不是单一的离散值:

我们可以将这个目标数学化地表示如下:

策略迭代

在值迭代过程中,有时最优策略在值函数之前收敛,因为它只关心找到最优策略。还可以执行另一个称为策略迭代的算法来达到最优策略。这是在每次策略评估之后,下一个策略基于值函数直到策略收敛的过程。以下图示展示了策略迭代过程:

因此,策略迭代算法保证收敛到最优策略,而值迭代算法则不一定。

以下是策略迭代的伪代码:

编码示例 - 策略迭代

在这里,我们考虑使用之前介绍的 Frozen Lake 环境的编码示例。

首先,导入相关的库:

import numpy as np
import gym
from gym import wrappers

现在,我们定义函数来运行一个 episode 并返回奖励:

def run_episode_return_reward(environment, policy, gamma_value = 1.0, render = False):
    """ Runs an episode and return the total reward """
    obs = environment.reset()
    total_reward = 0
    step_index = 0
    while True:
        if render:
            environment.render()
        obs, reward, done , _ = environment.step(int(policy[obs]))
        total_reward += (gamma_value ** step_index * reward)
        step_index += 1
        if done:
            break
    return total_reward

从这里,我们可以定义评估策略的函数:

def evaluate_policy(environment, policy, gamma_value = 1.0, n = 200):
    model_scores = [run_episode_return_reward(environment, policy, gamma_value, False) for _ in range(n)]
    return np.mean(model_scores)

然后,定义提取策略的函数:

def extract_policy(v, gamma_value = 1.0):
    """ Extract the policy for a given value function """
    policy = np.zeros(environment.nS)
    for s in range(environment.nS):
        q_sa = np.zeros(environment.nA)
        for a in range(environment.nA):
            q_sa[a] = sum([p * (r + gamma_value * v[s_]) for p, s_, r, _ in environment.P[s][a]])
        policy[s] = np.argmax(q_sa)
    return policy

最后,定义计算策略的函数:

def compute_policy_v(environment, policy, gamma_value=1.0):
    """ Iteratively evaluate the value-function under policy.
    Alternatively, we could formulate a set of linear equations in terms of v[s] 
    and solve them to find the value function.
    """
    v = np.zeros(environment.nS)
    eps = 1e-10
    while True:
        prev_v = np.copy(v)
        for s in range(environment.nS):
            policy_a = policy[s]
            v[s] = sum([p * (r + gamma_value * prev_v[s_]) for p, s_, r, _ in environment.P[s][policy_a]])
        if (np.sum((np.fabs(prev_v - v))) <= eps):
            # value converged
            break
    return v

从这里,我们可以在 Frozen Lake 环境上运行策略迭代:

env_name = 'FrozenLake-v0'
environment = gym.make(env_name)
optimal_policy = policy_iteration(environment, gamma_value = 1.0)
scores = evaluate_policy(environment, optimal_policy, gamma_value = 1.0)
print('Average scores = ', np.mean(scores))

我们观察到它在 步骤 5 处收敛:

价值迭代与策略迭代的比较

当代理假定对其在环境中的行为影响有一些先验知识时,可以使用价值迭代和策略迭代算法。这些算法假定已知马尔可夫决策过程(MDP)模型。然而,策略迭代通常更加计算效率高,因为它往往需要更少的迭代次数才能收敛。

策略梯度算法

策略梯度也是解决强化学习问题的一种方法,旨在直接建模和优化策略:

在策略梯度中,采取以下步骤:

  1. 代理观察环境的状态 (s)。

  2. 代理根据他们对状态 (s) 的本能(即策略π)采取行动 u

  3. 代理移动并且环境改变;形成一个新的状态。

  4. 代理根据观察到的环境状态进一步采取行动。

  5. 在运动轨迹(τ)之后,代理根据所获得的总奖励 R(τ) 调整其本能。

策略梯度定理如下:

预期奖励的导数是策略π[θ]​的对数梯度与奖励乘积的期望

编码示例 - 策略梯度算法

在本例中,我们使用名为 CartPole 的 OpenAI 环境,目标是让连接到小车上的杆尽可能长时间保持直立。代理在每个时间步长内保持杆平衡时会获得奖励。如果杆倒下,那么这一集结束:

在任何时刻,小车和杆都处于一个状态 s。该状态由四个元素的向量表示,即杆角度、杆速度、小车位置和小车速度。代理可以选择两种可能的动作:向左移动小车或向右移动小车。

策略梯度采取小步骤,并根据与步骤相关的奖励更新策略。这样做可以训练代理,而无需在环境中为每对状态和动作映射价值。

在这个例子中,我们将应用一种称为蒙特卡洛策略梯度的技术。使用这种方法,代理将根据获得的奖励在每个 episode 结束时更新策略。

我们首先导入我们计划使用的相关库:

import gym
import numpy as np
from tqdm import tqdm, trange
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.distributions import Categorical

现在,我们定义一个前馈神经网络,包含一个隐藏层,有 128 个神经元和 0.5 的 dropout。我们使用 Adam 作为优化器,学习率为 0.02。使用 dropout 显著提高了策略的性能:

class PolicyGradient(nn.Module):
    def __init__(self):
        super(PolicyGradient, self).__init__()

        # Define the action space and state space
        self.action_space = env.action_space.n
        self.state_space = env.observation_space.shape[0]

        self.l1 = nn.Linear(self.state_space, 128, bias=False)
        self.l2 = nn.Linear(128, self.action_space, bias=False)

        self.gamma_value = gamma_value

        # Episode policy and reward history 
        self.history_policy = Variable(torch.Tensor()) 
        self.reward_episode = []

        # Overall reward and loss history
        self.history_reward = []
        self.history_loss = []

    def forward(self, x): 
        model = torch.nn.Sequential(
            self.l1,
            nn.Dropout(p=0.5),
            nn.ReLU(),
            self.l2,
            nn.Softmax(dim=-1)
        )
        return model(x)

policy = PolicyGradient()
optimizer = optim.Adam(policy.parameters(), lr=l_rate)

现在,我们定义一个choose_action函数。这个函数根据策略分布选择动作,使用了 PyTorch 分布包。策略返回一个数组,表示动作空间上每个可能动作的概率。在我们的例子中,可以是向左移动或向右移动,因此输出可能是[0.1, 0.9]。根据这些概率选择动作,记录历史并返回动作:

def choose_action(state):
    # Run the policy model and choose an action based on the probabilities in state
    state = torch.from_numpy(state).type(torch.FloatTensor)
    state = policy(Variable(state))
    c = Categorical(state)
    action = c.sample() 
    if policy.history_policy.dim() != 0:
        try:
            policy.history_policy = torch.cat([policy.history_policy, c.log_prob(action)])
        except:
            policy.history_policy = (c.log_prob(action))
    else:
        policy.history_policy = (c.log_prob(action))
    return action

要更新策略,我们从 Q 函数(动作值函数)中取样。回想一下,这是通过遵循策略 π 在状态中采取行动来预期的回报。我们可以使用每个时间步长的策略梯度来计算,其中在杆保持垂直的每个步骤都有 1 的奖励。我们使用长期奖励 (vt),这是整个 episode 期间所有未来奖励的折现总和。因此,episode 越长,当前状态-动作对的奖励越大,其中 gamma 是折现因子。

折现奖励向量表示如下:

例如,如果一个 episode 持续 4 个时间步,每步的奖励将分别是 [4.90, 3.94, 2.97, 1.99]。从这里,我们可以通过减去均值并除以标准差来缩放奖励向量。

在每个 episode 结束后,我们应用蒙特卡洛策略梯度来改进策略,如下所示:

然后,这个策略乘以奖励值,输入优化器,使用随机梯度下降更新神经网络的权重。

以下函数定义了我们如何在代码中更新策略:

def update_policy():
    R = 0
    rewards = []

    # Discount future rewards back to the present using gamma
    for r in policy.reward_episode[::-1]:
        R = r + policy.gamma_value * R
        rewards.insert(0,R)

    # Scale rewards
    rewards = torch.FloatTensor(rewards)
    x = np.finfo(np.float32).eps
    x = np.array(x)
    x = torch.from_numpy(x)
    rewards = (rewards - rewards.mean()) / (rewards.std() + x)
    # Calculate the loss loss
    loss = (torch.sum(torch.mul(policy.history_policy, Variable(rewards)).mul(-1), -1))

    # Update the weights of the network
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    #Save and intialize episode history counters
    policy.history_loss.append(loss.data[0])
    policy.history_reward.append(np.sum(policy.reward_episode))
    policy.history_policy = Variable(torch.Tensor())
    policy.reward_episode= []

在这里,我们定义主策略训练循环。在每个训练 episode 的每一步,选择一个动作并记录新状态和奖励。在每个 episode 结束时调用update_policy函数,将 episode 历史传入神经网络:

def main_function(episodes):
    running_total_reward = 50
    for e in range(episodes):
        # Reset the environment and record the starting state
        state = env.reset() 
        done = False 

        for time in range(1000):
            action = choose_action(state)
            # Step through environment using chosen action
            state, reward, done, _ = env.step(action.data.item())

            # Save reward
            policy.reward_episode.append(reward)
            if done:
                break

        # Used to determine when the environment is solved.
        running_total_reward = (running_total_reward * 0.99) + (time * 0.01)

        update_policy()

        if e % 50 == 0:
            print('Episode number {}, Last length: {:5d}, Average length: {:.2f}'.format(e, time, running_total_reward))

        if running_total_reward > env.spec.reward_threshold:
            print("Solved! Running reward is now {} and the last episode runs to {} time steps!".format(running_total_reward, time))
            break

episodes = 2000
main_function(episodes)

深度 Q 网络

深度 Q 网络 (DQNs) 结合了深度学习和强化学习,在多个不同的应用中学习,尤其是在电子游戏中。让我们考虑一个简化的游戏示例,在迷宫中有一只老鼠,目标是让老鼠尽可能多地吃奶酪。老鼠吃的奶酪越多,游戏得分就越高:

在这个例子中,RL 术语如下:

  • 代理:由计算机控制的老鼠

  • 状态:游戏中的当前时刻

  • 动作:老鼠做出的决策(向左、向右、向上或向下移动)

  • 奖励:游戏中的分数/老鼠吃掉的奶酪数量,换句话说,代理试图最大化的值

DQN 使用 Q-learning 来学习给定状态的最佳动作。它们使用卷积神经网络作为 Q-learning 函数的函数逼近器。ConvNets 使用卷积层来查找空间特征,例如老鼠当前在网格中的位置。这意味着代理只需学习数百万而不是数十亿种不同的游戏状态的 Q 值:

学习鼠迷宫游戏时 DQN 架构的示例如下:

  1. 当前状态(迷宫屏幕)作为输入输入到 DQN 中。

  2. 输入通过卷积层传递,以找出图像中的空间模式。请注意,这里没有使用池化,因为在建模电脑游戏时知道空间位置很重要。

  3. 卷积层的输出被馈送到全连接线性层。

  4. 线性层的输出给出了 DQN 在当前状态下采取行动的概率(向上、向下、向左或向右)。

DQN 损失函数

DQN 需要一种损失函数以提高得分。该函数可以数学表示如下:

是 Q 网络选择要采取的动作。目标网络是用作地面真实值的近似值。如果我们考虑这样一个情况,即 Q 网络预测在特定状态下正确行动是向左移动的概率为 80%,而目标网络建议向左移动,我们可以通过反向传播调整 Q 网络的参数,使其更有可能在该状态下预测“向左移动”。换句话说,我们通过 DQN 反向传播损失并调整 Q 网络的权重,以减少总体损失。损失方程旨在使移动的概率更接近于 100%的确定性选择。

经验回放

一个经验包括当前状态、动作、奖励和下一个状态。代理获得的每个经验都记录在经验回放内存中。从回放内存中随机抽取一个经验来训练网络。

与传统的 Q-learning 相比,经验回放具有一些关键优势。其中一个优势是,由于每个经验可能被用来多次训练 DQN 的神经网络,因此具有更高的数据效率。另一个优势是,在它学习到经验之后,下一个样本的训练是由当前参数决定的。如果我们在迷宫的例子中考虑这一点,如果下一个最佳行动是向左移动,那么训练样本将主要来自屏幕左侧。这种行为可能导致 DQN 陷入局部最小值。通过引入经验回放,用于训练 DQN 的经验来源于时间的许多不同点,从而平滑学习过程并帮助避免性能不佳。

编码示例 - DQN

在这个例子中,我们将再次考虑来自 OpenAI Gym 的CartPole-v0环境。

首先,我们创建一个类,它将允许我们在训练 DQN 时引入经验回放。这本质上存储了智能体观察到的转换。通过采样过程,构建一个批次的转换是不相关的:

transition_type = namedtuple('transition_type',
                        ('state', 'action', 'next_state', 'reward'))

class ExperienceReplayMemory(object):
    def __init__(self, model_capacity):
        self.model_capacity = model_capacity
        self.environment_memory = []
        self.pole_position = 0

    def push(self, *args):
        """Saves a transition."""
        if len(self.environment_memory) < self.model_capacity:
            self.environment_memory.append(None)
        self.environment_memory[self.pole_position] = transition_type(*args)
        self.pole_position = (self.pole_position + 1) % self.model_capacity

    def sample(self, batch_size):
        return random.sample(self.environment_memory, batch_size)

    def __len__(self):
        return len(self.environment_memory)

定义 ConvNet 模型,其中当前和先前的屏幕补丁之间的差异被馈送进去。模型有两个输出—Q(s,left)和 Q(s,right)。网络试图预测在给定当前输入时采取行动的预期奖励/回报:

class DQNAlgorithm(nn.Module):

    def __init__(self, h, w, outputs):
        super(DQNAlgorithm, self).__init__()
        self.conv_layer1 = nn.Conv2d(3, 8, kernel_size=5, stride=2)
        self.batch_norm1 = nn.BatchNorm2d(8)
        self.conv_layer2 = nn.Conv2d(8, 32, kernel_size=5, stride=2)
        self.batch_norm2 = nn.BatchNorm2d(32)
        self.conv_layer3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
        self.batch_norm3 = nn.BatchNorm2d(32)

        # The number of linear input connections depends on the number of conv2d layers
        def conv2d_layer_size_out(size, kernel_size = 5, stride = 2):
            return (size - (kernel_size - 1) - 1) // stride + 1
        convw = conv2d_layer_size_out(conv2d_layer_size_out(conv2d_layer_size_out(w)))
        convh = conv2d_layer_size_out(conv2d_layer_size_out(conv2d_layer_size_out(h)))
        linear_input_size = convw * convh * 32
        self.head = nn.Linear(linear_input_size, outputs)

    # Determines next action during optimisation
    def forward(self, x):
        x = F.relu(self.batch_norm1(self.conv_layer1(x)))
        x = F.relu(self.batch_norm2(self.conv_layer2(x)))
        x = F.relu(self.batch_norm3(self.conv_layer3(x)))
        return self.head(x.view(x.size(0), -1))

设置模型的超参数以及一些用于训练的实用程序:

BATCH_SIZE = 128
GAMMA_VALUE = 0.95
EPISODE_START = 0.9
EPISODE_END = 0.05
EPISODE_DECAY = 200
TARGET_UPDATE = 20

init_screen = get_screen()
dummy_1, dummy_2, height_screen, width_screen = init_screen.shape

number_actions = environment.action_space.n

policy_network = DQNAlgorithm(height_screen, width_screen, number_actions).to(device)
target_network = DQNAlgorithm(height_screen, width_screen, number_actions).to(device)
target_network.load_state_dict(policy_network.state_dict())
target_network.eval()

optimizer = optim.RMSprop(policy_network.parameters())
memory = ExperienceReplayMemory(1000)

steps_done = 0

def choose_action(state):
    global steps_done
    sample = random.random()
    episode_threshold = EPISODE_END + (EPISODE_START - EPISODE_END) * \
        math.exp(-1\. * steps_done / EPISODE_DECAY)
    steps_done += 1
    if sample > episode_threshold:
        with torch.no_grad():
            return policy_network(state).max(1)[1].view(1, 1)
    else:
        return torch.tensor([[random.randrange(number_actions)]], device=device, dtype=torch.long)

durations_per_episode = []

def plot_durations():
    plt.figure(2)
    plt.clf()
    durations_timestep = torch.tensor(durations_per_episode, dtype=torch.float)
    plt.title('Training in progress...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_timestep.numpy())
    if len(durations_timestep) >= 50:
        mean_values = durations_per_episode.unfold(0, 100, 1).mean(1).view(-1)
        mean_values = torch.cat((torch.zeros(99), mean_values))
        plt.plot(mean_values.numpy())

    plt.pause(0.001) 
    plt.show()

最后,我们有训练模型的代码。此函数执行优化的单步。首先,它对批次进行采样并将所有张量连接成一个单一张量。它计算Q(st,at)V(st+1)=maxaQ(st+1,a),并将它们结合成一个损失。根据定义,如果s是终端状态,则设置V(s)=0。我们还使用目标网络来计算*V(st+1)*以提高稳定性:

def optimize_model():
    if len(memory) < BATCH_SIZE:
        return
    transitions_memory = memory.sample(BATCH_SIZE)
    batch = Transition(*zip(*transitions_memory))

计算非最终状态的掩码。之后,我们将批次元素连接起来:

    not_final_mask = torch.tensor(tuple(map(lambda x: x is not None,
                                          batch.next_state)), device=device, dtype=torch.uint8)
    not_final_next_states = torch.cat([x for x in batch.next_state if x is not None])

    state_b = torch.cat(batch.state)
    action_b = torch.cat(batch.action)
    reward_b = torch.cat(batch.reward)

计算Q(s_t, a),然后选择所采取的动作列:

    state_action_values = policy_network(state_b).gather(1, action_b)

    next_state_values = torch.zeros(BATCH_SIZE, device=device)
    next_state_values[not_final_mask] = target_net(not_final_next_states).max(1)[0].detach()

我们计算预期的 Q 值:

    expected_state_action_values = (next_state_values * GAMMA_VALUE) + reward_b

然后我们计算 Huber 损失函数:

    hb_loss = F.smooth_l1_loss(state_action_values, expected_state_action_values.unsqueeze(1))

现在,我们优化模型:

    optimizer.zero_grad()
    hb_loss.backward()
    for param in policy_network.parameters():
        param.grad.data.clamp_(-1, 1)
    optimizer.step()

number_episodes = 100
for i in range(number_episodes):
    environment.reset()
    last_screen = get_screen()
    current_screen = get_screen()
    current_state = current_screen - last_screen
    for t in count():
        # Here we both select and perform an action
        action = choose_action(current_state)
        _, reward, done, _ = environment.step(action.item())
        reward = torch.tensor([reward], device=device)

现在,我们观察新状态:

        last_screen = current_screen
        current_screen = get_screen()
        if not done:
            next_state = current_screen - last_screen
        else:
            next_state = None

我们将转换存储在内存中:

        memory.push(current_state, action, next_state, reward)

        # Move to the next state
        current_state = next_state

让我们执行优化的一步(在目标网络上):

        optimize_model()
        if done:
            durations_per_episode.append(t + 1)
            plot_durations()
            break

更新目标网络;复制所有 DQN 中的权重和偏置:

    if i % TARGET_UPDATE == 0:
        target_network.load_state_dict(policy_network.state_dict())

print('Complete')
environment.render()
environment.close()
plt.ioff()
plt.show()

这输出一些可视化,以便了解模型在训练过程中的表现:

以下图表总结了此编码示例中模型的操作:

双重深度 Q 学习

双深度 Q 学习通常比纯深度 Q 网络表现更好。深度 Q 学习的一个常见问题是,有时代理可以学习到不切实际地高的行动价值,因为它包括对估计行动价值的最大化步骤。这倾向于偏爱过高估计的值而不是低估值。如果过高估计不均匀且不集中在我们希望更多了解的状态上,则可能会对结果策略的质量产生负面影响。

双 Q 学习的思想是减少这些过高估计。它通过将目标中的 max 操作分解为行动选择和行动评估来实现。在纯深度 Q 网络实现中,行动选择和行动评估是耦合的。它使用目标网络来选择行动,并同时估计行动的质量。

我们正在使用目标网络来选择行动,并同时估计行动的质量。双 Q 学习本质上试图将这两个过程解耦。

在双 Q 学习中,时序差分(TD)目标如下所示:

新的 TD 目标的计算可以总结为以下步骤:

  1. Q 网络使用下一个状态s'来计算在状态s'中每个可能的行动a的质量Q(s',a)

  2. 应用于*Q(s',a)argmax操作选择了属于最高质量的行动a**(行动选择)。

  3. 行动的质量 Q(s',a),属于行动a**,被选为目标的计算。

双 Q 学习的过程可以如下图所示。AI 代理处于初始状态s,基于一些先前的计算,它知道该状态中可能的两个行动a[1]a[2]的质量Q(s, a[1])Q(s, a[2])。然后代理决定采取行动a[1]并进入状态s'

演员-评论家方法

演员-评论家方法旨在结合值和基于策略的方法的优势,同时消除它们的缺点:

演员-评论家的基本思想是将模型分为两部分:一部分用于根据状态计算行动,另一部分用于生成行动的 Q 值。

演员是一个神经网络,将状态作为输入并输出最佳行动。通过学习最优策略,它控制代理的行为方式。评论家通过计算价值函数评估行动。换句话说,演员尝试优化策略,评论家尝试优化价值。这两个模型随着时间的推移在各自的角色上得到改进,因此整体架构的学习效率高于单独使用这两种方法:

这两个模型本质上是相互竞争的。这种方法在机器学习领域越来越流行;例如,在生成对抗网络中也有这种情况。

演员的角色性质是探索性的。它经常尝试新事物并探索环境。评论者的角色是要么批评,要么赞扬演员的行动。演员接受这些反馈并相应地调整其行为。随着演员获得越来越多的反馈,它在决定采取哪些行动时变得越来越好。

就像神经网络一样,演员可以是一个函数逼近器,其任务是为给定的状态生成最佳动作。例如,这可以是一个完全连接的或卷积神经网络。评论者也是一个函数逼近器,它接收环境和演员的动作作为输入。它连接这些输入并输出动作值(Q 值)。

这两个网络分别进行训练,并使用梯度上升而不是下降来更新它们的权重,因为它旨在确定全局最大值而不是最小值。权重在每个步骤而不是在每个策略梯度末尾更新。

演员评论已被证明能够学习复杂的环境,并已在许多二维和三维电脑游戏中使用,例如超级马里奥Doom

编码示例 - 演员评论模型

在这里,我们将考虑一个在 PyTorch 中的编码实现示例。首先,我们定义ActorCritic类:

HistoricalAction = namedtuple('HistoricalAction', ['log_prob', 'value'])

class ActorCritic(nn.Module):
    def __init__(self):
        super(ActorCritic, self).__init__()
        self.linear = nn.Linear(4, 128)
        self.head_action = nn.Linear(128, 2)
        self.head_value = nn.Linear(128, 1)

        self.historical_actions = []
       self.rewards = []

    def forward(self, x):
        x = F.relu(self.linear(x))
        scores_actions = self.head_action(x)
        state_values = self.head_value(x)
        return F.softmax(scores_actions, dim=-1), state_values

现在,我们初始化模型:

ac_model = ActorCritic()
optimizer = optim.Adam(ac_model.parameters(), lr=3e-2)
eps = np.finfo(np.float32).eps.item()

定义一个基于状态选择最佳动作的函数:

def choose_action(current_state):
    current_state = torch.from_numpy(current_state).float()
    probabilities, state_value = ac_model(current_state)
    m = Categorical(probabilities)
    action = m.sample()
    ac_model.historical_actions.append(HistoricalAction(m.log_prob(action), state_value))
    return action.item()

从这里开始,我们需要定义计算总回报并考虑损失函数的函数:

def end_episode():
    R = 0
    historical_actions = ac_model.historical_actions
    losses_policy = []
    losses_value = []
    returns = []
    for r in ac_model.rewards[::-1]:
        R = r + gamma * R
        returns.insert(0, R)
    returns = torch.tensor(returns)
    returns = (returns - returns.mean()) / (returns.std() + eps)
    for (log_prob, value), R in zip(historical_actions, returns):
        advantage = R - value.item()
        losses_policy.append(-log_prob * advantage)
        losses_value.append(F.smooth_l1_loss(value, torch.tensor([R])))
    optimizer.zero_grad()
    loss = torch.stack(losses_policy).sum() + torch.stack(losses_value).sum()
    loss.backward()
    optimizer.step()
    del ac_model.rewards[:]
    del ac_model.historical_actions[:]

最后,我们可以训练模型并查看其表现:

    running_reward = 10
    for i_episode in count(1):
        current_state, ep_reward = environment.reset(), 0
        for t in range(1, 10000): 
            action = choose_action(current_state)
            current_state, reward, done, _ = environment.step(action)
            if render:
                environment.render()
            ac_model.rewards.append(reward)
            ep_reward += reward
            if done:
                break

        running_reward = 0.05 * ep_reward + (1 - 0.05) * running_reward
        end_episode()
        if i_episode % log_interval == 0:
            print('Episode number {}\tLast reward: {:.2f}\tAverage reward: {:.2f}'.format(
                  i_episode, ep_reward, running_reward))
        if running_reward > environment.spec.reward_threshold:
            print("Solved! Running reward is {} and "
                  "the last episode runs to {} time steps!".format(running_reward, t))
            break

这将给出以下输出:

异步演员评论算法

异步优势演员评论A3C是由谷歌的 DeepMind 提出的一种算法。该算法已被证明优于其他算法。

在 A3C 中,有多个代理实例,每个代理实例在其自己的独立环境中进行不同的初始化。每个个体代理开始采取行动,并通过强化学习过程来收集自己独特的经验。然后,这些独特的经验用于更新全局神经网络。这个全局神经网络被所有代理共享,它影响所有代理的行动,每个代理的每个新经验都提高了整体网络的速度:

名称中的优势术语是指状态的预期平均值与该状态的行动相比是否有改进的价值。优势公式如下:

A (s,a) = Q(s,a) - V(s)

实际应用

强化学习方法已被应用于解决现实世界中多种领域的问题。在这里,我们考虑了其中一些例子。

  • 机器人技术: 在机器人领域应用强化学习的工作已经取得了显著进展。如今,制造设施充斥着执行各种任务的机器人,其基础是强化学习方法:

  • 交通信号灯控制: 在论文基于强化学习的多智能体网络交通信号控制系统中,研究人员设计了一个交通信号灯控制器来解决拥堵问题,表现优异,超过了其他方法:

  • 个性化推荐: 强化学习已经应用于新闻推荐系统中,以应对新闻快速变化的特点,用户的注意力不集中,仅凭点击率无法反映用户的留存率:

  • 生成图像: 对于将强化学习与其他深度学习架构结合进行研究已经有很多成果。DeepMind 展示了使用生成模型和强化学习成功生成图像的能力:

总结

在本章中,我们首先介绍了强化学习的基础,并介绍了一些在真实场景中表现出超越人类能力的先进算法。同时,我们还展示了这些算法如何在 PyTorch 中实现。

在接下来的最后一章中,将概述本书内容,并提供如何保持与数据科学领域最新进展的技巧。

进一步阅读

请参考以下链接获取更多信息: