PyTorch 深度学习指南(二)

原文:zh.annas-archive.org/md5/057fe0c351c5365f1188d1f44806abda

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:序列数据和文本的深度学习

在上一章中,我们介绍了如何使用卷积神经网络CNNs)处理空间数据,并构建了图像分类器。在本章中,我们将涵盖以下主题:

  • 对于构建深度学习模型有用的文本数据的不同表示形式

  • 理解循环神经网络RNNs)及其不同实现,如长短期记忆LSTM)和门控循环单元GRU),它们支持大多数文本和序列数据的深度学习模型

  • 使用一维卷积处理序列数据

可以使用 RNN 构建的一些应用包括:

  • 文档分类器:识别推文或评论的情感,分类新闻文章

  • 序列到序列学习:用于任务如语言翻译,将英语转换为法语

  • 时间序列预测:根据前几天的商店销售详情预测商店的销售情况

处理文本数据

文本是常用的序列数据类型之一。文本数据可以看作是字符序列或单词序列。对于大多数问题,将文本视为单词序列是很常见的。深度学习序列模型如 RNN 及其变体能够从文本数据中学习重要模式,可以解决以下领域的问题:

  • 自然语言理解

  • 文档分类

  • 情感分类

这些序列模型也是各种系统的重要构建块,如问答系统QA)。

尽管这些模型在构建这些应用中非常有用,但由于其固有的复杂性,它们并不理解人类语言。这些序列模型能够成功地找到有用的模式,然后用于执行不同的任务。将深度学习应用于文本是一个快速发展的领域,每个月都有许多新技术问世。我们将介绍支持大多数现代深度学习应用的基本组件。

深度学习模型和其他机器学习模型一样,不理解文本,因此我们需要将文本转换为数值表示。将文本转换为数值表示的过程称为向量化,可以用不同的方法进行,如下所述:

  • 将文本转换为单词,并将每个单词表示为一个向量

  • 将文本转换为字符,并将每个字符表示为一个向量

  • 创建n-gram 单词并将它们表示为向量

文本数据可以分解为这些表示之一。每个文本的较小单元称为标记,将文本分解为标记的过程称为标记化。Python 中有很多强大的库可以帮助我们进行标记化。一旦我们将文本数据转换为标记,我们就需要将每个标记映射到一个向量上。一热编码和词嵌入是将标记映射到向量的两种最流行的方法。以下图表总结了将文本转换为其向量表示的步骤:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们更详细地看一下标记化、n-gram 表示和向量化。

标记化

给定一个句子,将其拆分为字符或单词称为标记化。有一些库,比如 spaCy,提供了复杂的标记化解决方案。让我们使用简单的 Python 函数,比如 splitlist,将文本转换为标记。

为了演示标记化在字符和单词上的工作原理,让我们考虑电影《雷神 3:诸神黄昏》的简短评论。我们将使用以下文本:

这部电影的动作场面非常出色。在 MCU 中,Thor 从未如此史诗般。他在这部电影中表现得相当史诗,绝对不再是无能为力。Thor 在这部电影中释放了自我,我喜欢这一点。

将文本转换为字符

Python 的 list 函数接受一个字符串并将其转换为单个字符的列表。这完成了将文本转换为字符的任务。以下代码块展示了使用的代码和结果:

thor_review = "the action scenes were top notch in this movie. Thor has never been this epic in the MCU. He does some pretty epic sh*t in this movie and he is definitely not under-powered anymore. Thor in unleashed in this, I love that."

print(list(thor_review))

结果如下:


#Results
['t', 'h', 'e', ' ', 'a', 'c', 't', 'i', 'o', 'n', ' ', 's', 'c', 'e', 'n', 'e', 's', ' ', 'w', 'e', 'r', 'e', ' ', 't', 'o', 'p', ' ', 'n', 'o', 't', 'c', 'h', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ' ', 'm', 'o', 'v', 'i', 'e', '.', ' ', 'T', 'h', 'o', 'r', ' ', 'h', 'a', 's', ' ', 'n', 'e', 'v', 'e', 'r', ' ', 'b', 'e', 'e', 'n', ' ', 't', 'h', 'i', 's', ' ', 'e', 'p', 'i', 'c', ' ', 'i', 'n', ' ', 't', 'h', 'e', ' ', 'M', 'C', 'U', '.', ' ', 'H', 'e', ' ', 'd', 'o', 'e', 's', ' ', 's', 'o', 'm', 'e', ' ', 'p', 'r', 'e', 't', 't', 'y', ' ', 'e', 'p', 'i', 'c', ' ', 's', 'h', '*', 't', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ' ', 'm', 'o', 'v', 'i', 'e', ' ', 'a', 'n', 'd', ' ', 'h', 'e', ' ', 'i', 's', ' ', 'd', 'e', 'f', 'i', 'n', 'i', 't', 'e', 'l', 'y', ' ', 'n', 'o', 't', ' ', 'u', 'n', 'd', 'e', 'r', '-', 'p', 'o', 'w', 'e', 'r', 'e', 'd', ' ', 'a', 'n', 'y', 'm', 'o', 'r', 'e', '.', ' ', 'T', 'h', 'o', 'r', ' ', 'i', 'n', ' ', 'u', 'n', 'l', 'e', 'a', 's', 'h', 'e', 'd', ' ', 'i', 'n', ' ', 't', 'h', 'i', 's', ',', ' ', 'I', ' ', 'l', 'o', 'v', 'e', ' ', 't', 'h', 'a', 't', '.']

这个结果展示了我们简单的 Python 函数如何将文本转换为标记。

将文本转换为单词

我们将使用 Python 字符串对象中的 split 函数将文本分割成单词。split 函数接受一个参数,根据此参数将文本分割为标记。对于我们的示例,我们将使用空格作为分隔符。以下代码块演示了如何使用 Python 的 split 函数将文本转换为单词:

print(Thor_review.split())

#Results

['the', 'action', 'scenes', 'were', 'top', 'notch', 'in', 'this', 'movie.', 'Thor', 'has', 'never', 'been', 'this', 'epic', 'in', 'the', 'MCU.', 'He', 'does', 'some', 'pretty', 'epic', 'sh*t', 'in', 'this', 'movie', 'and', 'he', 'is', 'definitely', 'not', 'under-powered', 'anymore.', 'Thor', 'in', 'unleashed', 'in', 'this,', 'I', 'love', 'that.']

在上述代码中,我们没有使用任何分隔符;split 函数默认在空格上分割。

N-gram 表示

我们已经看到文本可以表示为字符和单词。有时候,查看两个、三个或更多单词在一起是很有用的。N-gram 是从给定文本中提取的一组单词。在 n-gram 中,n 表示可以一起使用的单词数。让我们看一个 bigramn=2)的示例。我们使用 Python 的 nltk 包为 thor_review 生成了一个 bigram。以下代码块展示了 bigram 的结果以及生成它所使用的代码:

from nltk import ngrams

print(list(ngrams(thor_review.split(),2)))

#Results
[('the', 'action'), ('action', 'scenes'), ('scenes', 'were'), ('were', 'top'), ('top', 'notch'), ('notch', 'in'), ('in', 'this'), ('this', 'movie.'), ('movie.', 'Thor'), ('Thor', 'has'), ('has', 'never'), ('never', 'been'), ('been', 'this'), ('this', 'epic'), ('epic', 'in'), ('in', 'the'), ('the', 'MCU.'), ('MCU.', 'He'), ('He', 'does'), ('does', 'some'), ('some', 'pretty'), ('pretty', 'epic'), ('epic', 'sh*t'), ('sh*t', 'in'), ('in', 'this'), ('this', 'movie'), ('movie', 'and'), ('and', 'he'), ('he', 'is'), ('is', 'definitely'), ('definitely', 'not'), ('not', 'under-powered'), ('under-powered', 'anymore.'), ('anymore.', 'Thor'), ('Thor', 'in'), ('in', 'unleashed'), ('unleashed', 'in'), ('in', 'this,'), ('this,', 'I'), ('I', 'love'), ('love', 'that.')]

ngrams 函数接受一系列单词作为第一个参数,并将要分组的单词数作为第二个参数。以下代码块展示了三元组表示法的样子,以及用于它的代码:

print(list(ngrams(thor_review.split(),3)))

#Results

[('the', 'action', 'scenes'), ('action', 'scenes', 'were'), ('scenes', 'were', 'top'), ('were', 'top', 'notch'), ('top', 'notch', 'in'), ('notch', 'in', 'this'), ('in', 'this', 'movie.'), ('this', 'movie.', 'Thor'), ('movie.', 'Thor', 'has'), ('Thor', 'has', 'never'), ('has', 'never', 'been'), ('never', 'been', 'this'), ('been', 'this', 'epic'), ('this', 'epic', 'in'), ('epic', 'in', 'the'), ('in', 'the', 'MCU.'), ('the', 'MCU.', 'He'), ('MCU.', 'He', 'does'), ('He', 'does', 'some'), ('does', 'some', 'pretty'), ('some', 'pretty', 'epic'), ('pretty', 'epic', 'sh*t'), ('epic', 'sh*t', 'in'), ('sh*t', 'in', 'this'), ('in', 'this', 'movie'), ('this', 'movie', 'and'), ('movie', 'and', 'he'), ('and', 'he', 'is'), ('he', 'is', 'definitely'), ('is', 'definitely', 'not'), ('definitely', 'not', 'under-powered'), ('not', 'under-powered', 'anymore.'), ('under-powered', 'anymore.', 'Thor'), ('anymore.', 'Thor', 'in'), ('Thor', 'in', 'unleashed'), ('in', 'unleashed', 'in'), ('unleashed', 'in', 'this,'), ('in', 'this,', 'I'), ('this,', 'I', 'love'), ('I', 'love', 'that.')]

在前述代码中唯一改变的是函数的第二个参数n-值。

许多监督学习模型,例如朴素贝叶斯,使用n-gram 来改善它们的特征空间。n-gram 也用于拼写纠正和文本摘要任务。

n-gram 表示的一个挑战是它丢失了文本的顺序性质。它通常与浅层机器学习模型一起使用。这种技术在深度学习中很少使用,因为像 RNN 和 Conv1D 这样的架构可以自动学习这些表示。

向量化

有两种流行的方法可以将生成的标记映射到数字向量中,称为单热编码词嵌入。让我们通过编写一个简单的 Python 程序来了解如何将标记转换为这些向量表示。我们还将讨论每种方法的各种优缺点。

单热编码

在单热编码中,每个标记由长度为 N 的向量表示,其中N是文档中唯一单词的数量。让我们看一个简单的句子,并观察每个标记如何表示为单热编码向量。以下是句子及其相关标记表示:

一天一个苹果,医生远离你

前述句子的单热编码可以用表格格式表示如下:

An100000000
apple010000000
a001000000
day000100000
keeps000010000
doctor000001000
away000000100
said000000010
the000000001

此表描述了标记及其单热编码表示。向量长度为 9,因为句子中有九个唯一单词。许多机器学习库已经简化了创建单热编码变量的过程。我们将编写自己的实现以便更容易理解,并可以使用相同的实现来构建后续示例所需的其他特征。以下代码包含了一个Dictionary类,其中包含创建唯一单词字典以及返回特定单词的单热编码向量的功能。让我们看一下代码,然后逐个功能进行解释:

class Dictionary(object):
    def __init__(self):
        self.word2idx = {}
        self.idx2word = []
        self.length = 0

    def add_word(self,word):
        if word not in self.idx2word:
            self.idx2word.append(word)
            self.word2idx[word] = self.length + 1
            self.length += 1
        return self.word2idx[word]

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

    def onehot_encoded(self,word):
        vec = np.zeros(self.length)
        vec[self.word2idx[word]] = 1
        return vec

前述代码提供了三个重要功能:

  • 初始化函数__init__创建了一个word2idx字典,它将存储所有唯一单词及其索引。idx2word列表存储所有唯一单词,length变量包含文档中唯一单词的总数。

  • add_word函数接受一个单词并将其添加到word2idxidx2word中,并增加词汇表的长度(假设单词是唯一的)。

  • onehot_encoded函数接受一个单词并返回一个长度为 N 的向量,其中除了单词的索引处为一之外,所有其他值都为零。如果传递的单词的索引是二,则向量在索引二处的值将为一,所有其他值将为零。

由于我们已经定义了Dictionary类,让我们在thor_review数据上使用它。以下代码演示了如何构建word2idx以及如何调用我们的onehot_encoded函数:

dic = Dictionary()

for tok in thor_review.split():
    dic.add_word(tok)

print(dic.word2idx)

前述代码的输出如下:

# Results of word2idx

{'the': 1, 'action': 2, 'scenes': 3, 'were': 4, 'top': 5, 'notch': 6, 'in': 7, 'this': 8, 'movie.': 9, 'Thor': 10, 'has': 11, 'never': 12, 'been': 13, 'epic': 14, 'MCU.': 15, 'He': 16, 'does': 17, 'some': 18, 'pretty': 19, 'sh*t': 20, 'movie': 21, 'and': 22, 'he': 23, 'is': 24, 'definitely': 25, 'not': 26, 'under-powered': 27, 'anymore.': 28, 'unleashed': 29, 'this,': 30, 'I': 31, 'love': 32, 'that.': 33}

单词were的一热编码如下:

# One-hot representation of the word 'were'
dic.onehot_encoded('were')
array([ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.])

一热表示法的一个挑战是数据过于稀疏,且随着词汇表中独特单词数量的增加,向量的大小迅速增长,这被认为是一种限制,因此在深度学习中很少使用。

词嵌入

词嵌入是在深度学习算法解决的文本数据中表示问题的非常流行的方法。词嵌入提供了一个由浮点数填充的单词的密集表示。向量维度根据词汇表的大小而变化。通常使用的词嵌入维度大小有 50、100、256、300,有时候甚至是 1,000。维度大小是我们在训练阶段需要调整的超参数之一。

如果我们试图用一热表示法表示一个大小为 20,000 的词汇表,那么我们将得到 20,000 x 20,000 个数字,其中大部分将是零。同样的词汇表可以用词嵌入表示为大小为 20,000 x 维度大小的形式,其中维度大小可以是 10、50、300 等。

创建词嵌入的一种方法是从每个标记的随机数密集向量开始,然后训练一个模型,如文档分类器或情感分类器。代表标记的浮点数将以一种使语义上更接近的单词具有类似表示的方式进行调整。为了理解它,让我们看看以下图例,其中我们在五部电影的二维图上绘制了词嵌入向量:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前述图像展示了如何调整密集向量以使语义相似的单词之间具有较小的距离。由于像SupermanThorBatman这样的电影标题是基于漫画的动作电影,它们的嵌入更接近,而电影Titanic的嵌入则远离动作电影,更接近电影Notebook,因为它们是浪漫电影。

学习词嵌入可能在数据量太少时不可行,在这种情况下,我们可以使用由其他机器学习算法训练的词嵌入。从另一个任务生成的嵌入称为预训练词嵌入。我们将学习如何构建自己的词嵌入并使用预训练词嵌入。

通过构建情感分类器训练词嵌入

在上一节中,我们简要介绍了单词嵌入的概念,但没有实现它。在本节中,我们将下载一个名为IMDB的数据集,其中包含评论,并构建一个情感分类器,用于判断评论的情感是积极的、消极的还是未知的。在构建过程中,我们还将为IMDB数据集中的单词训练单词嵌入。我们将使用一个名为torchtext的库,它通过提供不同的数据加载器和文本抽象化简化了许多与自然语言处理NLP)相关的活动。训练情感分类器将涉及以下步骤:

  1. 下载 IMDB 数据并执行文本标记化

  2. 建立词汇表

  3. 生成向量批次

  4. 创建带有嵌入的网络模型

  5. 训练模型

下载 IMDB 数据并执行文本标记化

对于与计算机视觉相关的应用程序,我们使用了torchvision库,该库为我们提供了许多实用函数,帮助构建计算机视觉应用程序。同样,还有一个名为torchtext的库,它是 PyTorch 的一部分,专门用于处理与 PyTorch 相关的许多文本活动,如下载、文本向量化和批处理。在撰写本文时,torchtext不随 PyTorch 安装而提供,需要单独安装。您可以在您的计算机命令行中运行以下代码来安装torchtext

pip install torchtext

一旦安装完成,我们将能够使用它。Torchtext 提供了两个重要的模块,称为torchtext.datatorchtext.datasets

我们可以从以下链接下载IMDB Movies数据集:

www.kaggle.com/orgesleka/imdbmovies

torchtext.data

torchtext.data实例定义了一个名为Field的类,它帮助我们定义数据的读取和标记化方式。让我们看看下面的示例,我们将用它来准备我们的IMDB数据集:

from torchtext import data
TEXT = data.Field(lower=True, batch_first=True,fix_length=20)
LABEL = data.Field(sequential=False)

在上述代码中,我们定义了两个Field对象,一个用于实际文本,另一个用于标签数据。对于实际文本,我们期望torchtext将所有文本转换为小写,标记化文本,并将其修剪为最大长度为20。如果我们为生产环境构建应用程序,可能会将长度固定为更大的数字。但是对于玩具示例,这个长度可以工作得很好。Field构造函数还接受另一个名为tokenize的参数,默认使用str.split函数。我们还可以指定 spaCy 作为参数,或任何其他的分词器。在我们的示例中,我们将坚持使用str.split

torchtext.datasets

torchtext.datasets实例提供了使用不同数据集的包装器,如 IMDB、TREC(问题分类)、语言建模(WikiText-2)和其他几个数据集。我们将使用torch.datasets下载IMDB数据集并将其分割为traintest数据集。以下代码执行此操作,当您第一次运行它时,根据您的宽带连接速度,可能需要几分钟时间从互联网上下载IMDB数据集:

train, test = datasets.IMDB.splits(TEXT, LABEL)

先前数据集的IMDB类将下载、标记和分割数据库到traintest数据集中所涉及的所有复杂性抽象化。train.fields包含一个字典,其中TEXT是键,值是LABEL。让我们来看看train.fields,而每个train元素包含:

print('train.fields', train.fields)

#Results 
train.fields {'text': <torchtext.data.field.Field object at 0x1129db160>, 'label': <torchtext.data.field.Field object at 0x1129db1d0>}

print(vars(train[0]))

#Results 

vars(train[0]) {'text': ['for', 'a', 'movie', 'that', 'gets', 'no', 'respect', 'there', 'sure', 'are', 'a', 'lot', 'of', 'memorable', 'quotes', 'listed', 'for', 'this', 'gem.', 'imagine', 'a', 'movie', 'where', 'joe', 'piscopo', 'is', 'actually', 'funny!', 'maureen', 'stapleton', 'is', 'a', 'scene', 'stealer.', 'the', 'moroni', 'character', 'is', 'an', 'absolute', 'scream.', 'watch', 'for', 'alan', '"the', 'skipper"', 'hale', 'jr.', 'as', 'a', 'police', 'sgt.'], 'label': 'pos'}

我们可以从这些结果看到,单个元素包含一个字段text,以及表示text的所有标记,还有一个包含文本标签的label字段。现在我们已经准备好对IMDB数据集进行批处理。

构建词汇表

当我们为thor_review创建一位热编码时,我们创建了一个word2idx字典,它被称为词汇表,因为它包含文档中所有唯一单词的详细信息。torchtext实例使我们更容易。一旦数据加载完毕,我们可以调用build_vocab并传递必要的参数,这些参数将负责为数据构建词汇表。以下代码显示了如何构建词汇表:

TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train)

在上述代码中,我们传递了需要构建词汇表的train对象,并要求它使用维度为300的预训练嵌入来初始化向量。build_vocab对象只是下载并创建将在以后训练情感分类器时使用的维度。max_size实例限制了词汇表中单词的数量,而min_freq则移除了出现次数不到十次的任何单词,其中10是可配置的。

一旦词汇表构建完成,我们可以获取不同的值,如频率、单词索引和每个单词的向量表示。以下代码演示了如何访问这些值:

print(TEXT.vocab.freqs)

# A sample result 
Counter({"i'm": 4174,
         'not': 28597,
         'tired': 328,
         'to': 133967,
         'say': 4392,
         'this': 69714,
         'is': 104171,
         'one': 22480,
         'of': 144462,
         'the': 322198,

下面的代码演示了如何访问结果:


print(TEXT.vocab.vectors)

#Results displaying the 300 dimension vector for each word.
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
      ...... 
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.7724 -0.1800 0.2072 ... 0.6736 0.2263 -0.2919
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
[torch.FloatTensor of size 10002x300]

print(TEXT.vocab.stoi)

# Sample results
defaultdict(<function torchtext.vocab._default_unk_index>,
            {'<unk>': 0,
             '<pad>': 1,
             'the': 2,
             'a': 3,
             'and': 4,
             'of': 5,
             'to': 6,
             'is': 7,
             'in': 8,
             'i': 9,
             'this': 10,
             'that': 11,
             'it': 12,

stoi提供了一个包含单词及其索引的字典。

生成向量的批次

Torchtext 提供了BucketIterator,它有助于对所有文本进行批处理,并用单词的索引号替换这些单词。BucketIterator实例带有许多有用的参数,如batch_sizedevice(GPU 或 CPU)和shuffle(数据是否需要洗牌)。以下代码演示了如何创建迭代器以为traintest数据集生成批次:

train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=128, device=-1,shuffle=True)
#device = -1 represents cpu , if u want gpu leave it to None.

上述代码为traintest数据集提供了一个BucketIterator对象。以下代码将展示如何创建一个批次并显示批次的结果:

batch = next(iter(train_iter))
batch.text

#Results
Variable containing:
 5128 427 19 ... 1688 0 542
   58 2 0 ... 2 0 1352
    0 9 14 ... 2676 96 9
       ...... 
  129 1181 648 ... 45 0 2
 6484 0 627 ... 381 5 2
  748 0 5052 ... 18 6660 9827
[torch.LongTensor of size 128x20]

batch.label

#Results
Variable containing:
 2
 1
 2
 1
 2
 1
 1
 1
[torch.LongTensor of size 128]

从前面的代码块的结果中,我们可以看到文本数据如何转换为大小为(batch_size * fix_len)的矩阵,即(128x20)。

创建带嵌入的网络模型

我们之前简要讨论了词嵌入。在本节中,我们将词嵌入作为网络架构的一部分,并训练整个模型来预测每个评论的情感。训练结束时,我们将得到一个情感分类器模型,以及针对IMDB数据集的词嵌入。以下代码展示了如何创建一个使用词嵌入来预测情感的网络架构:

class EmbNet(nn.Module):
    def __init__(self,emb_size,hidden_size1,hidden_size2=400):
        super().__init__()
        self.embedding = nn.Embedding(emb_size,hidden_size1)
        self.fc = nn.Linear(hidden_size2,3)

    def forward(self,x):
        embeds = self.embedding(x).view(x.size(0),-1)
        out = self.fc(embeds)
        return F.log_softmax(out,dim=-1)

在前面的代码中,EmbNet创建了用于情感分类的模型。在__init__函数内部,我们初始化了nn.Embedding类的一个对象,它接受两个参数,即词汇表的大小和我们希望为每个单词创建的维度。由于我们限制了唯一单词的数量,词汇表大小将为 10,000,并且我们可以从小的嵌入大小10开始。对于快速运行程序,小的嵌入大小是有用的,但当您尝试为生产系统构建应用程序时,请使用较大的嵌入。我们还有一个线性层,将词嵌入映射到类别(积极、消极或未知)。

forward函数确定如何处理输入数据。对于批量大小为 32 且最大长度为 20 个单词的句子,我们将得到形状为 32 x 20 的输入。第一个嵌入层充当查找表,将每个单词替换为相应的嵌入向量。对于嵌入维度为 10,输出将变为 32 x 20 x 10,因为每个单词都被其相应的嵌入替换。view()函数将扁平化嵌入层的结果。传递给view的第一个参数将保持该维度不变。在我们的情况下,我们不想组合来自不同批次的数据,因此保留第一个维度并扁平化张量中的其余值。应用view函数后,张量形状变为 32 x 200。密集层将扁平化的嵌入映射到类别数量。定义了网络架构后,我们可以像往常一样训练网络。

请记住,在这个网络中,我们失去了文本的顺序性,只是把它们作为一个词袋来使用。

训练模型

训练模型与构建图像分类器非常相似,因此我们将使用相同的函数。我们通过模型传递数据批次,计算输出和损失,然后优化模型权重,包括嵌入权重。以下代码执行此操作:

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 , batch in enumerate(data_loader):
        text , target = batch.text , batch.label
        if is_cuda:
            text,target = text.cuda(),target.cuda()

        if phase == 'training':
            optimizer.zero_grad()
        output = model(text)
        loss = F.nll_loss(output,target)

        running_loss += F.nll_loss(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 = [],[]
val_losses , val_accuracy = [],[]

train_iter.repeat = False
test_iter.repeat = False

for epoch in range(1,10):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

在上述代码中,我们通过传递我们为批处理数据创建的 BucketIterator 对象调用 fit 方法。迭代器默认不会停止生成批次,因此我们必须将 BucketIterator 对象的 repeat 变量设置为 False。如果我们不将 repeat 变量设置为 False,则 fit 函数将无限运行。在大约 10 个 epochs 的训练后,模型的验证准确率约为 70%。

使用预训练的词嵌入

在特定领域(如医学和制造业)工作时,预训练的词嵌入非常有用,因为我们有大量数据可以训练嵌入。当我们有少量数据无法进行有意义的训练时,我们可以使用在不同数据集(如维基百科、Google 新闻和 Twitter 推文)上训练的嵌入。许多团队都有使用不同方法训练的开源词嵌入。在本节中,我们将探讨 torchtext 如何简化使用不同词嵌入,并如何在我们的 PyTorch 模型中使用它们。这类似于我们在计算机视觉应用中使用的迁移学习。通常,使用预训练嵌入会涉及以下步骤:

  • 下载嵌入

  • 加载模型中的嵌入

  • 冻结嵌入层权重

让我们详细探讨每个步骤的实现方式。

下载嵌入

torchtext 库在下载嵌入并将其映射到正确单词中,抽象出了许多复杂性。Torchtext 在 vocab 模块中提供了三个类,分别是 GloVeFastTextCharNGram,它们简化了下载嵌入和映射到我们词汇表的过程。每个类别提供了在不同数据集上训练的不同嵌入,使用了不同的技术。让我们看一些提供的不同嵌入:

  • charngram.100d

  • fasttext.en.300d

  • fasttext.simple.300d

  • glove.42B.300d

  • glove.840B.300d

  • glove.twitter.27B.25d

  • glove.twitter.27B.50d

  • glove.twitter.27B.100d

  • glove.twitter.27B.200d

  • `glove.6B.50d`

  • glove.6B.100d

  • glove.6B.200d

  • glove.6B.300d

Field 对象的 build_vocab 方法接受一个用于嵌入的参数。以下代码解释了我们如何下载这些嵌入:

from torchtext.vocab import GloVe
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)

参数向量的值表示使用的嵌入类别。namedim 参数确定可以使用的嵌入。我们可以轻松地从 vocab 对象中访问嵌入。以下代码演示了它,同时展示了结果的样子:

TEXT.vocab.vectors

#Output
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
          ...... 
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
 0.7724 -0.1800 0.2072 ... 0.6736 0.2263 -0.2919
 0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
[torch.FloatTensor of size 10002x300]

现在我们已经下载并将嵌入映射到我们的词汇表中。让我们了解如何在 PyTorch 模型中使用它们。

加载模型中的嵌入

vectors变量返回一个形状为vocab_size x dimensions的 torch 张量,其中包含预训练的嵌入。我们必须将这些嵌入的权重存储到我们的嵌入层权重中。我们可以通过访问嵌入层权重来分配嵌入的权重,如下面的代码所示。

model.embedding.weight.data = TEXT.vocab.vectors

model代表我们网络的对象,embedding代表嵌入层。因为我们使用了新维度的嵌入层,线性层输入会有些变化。下面的代码展示了新的架构,与我们之前训练嵌入时使用的架构类似:

class EmbNet(nn.Module):
    def __init__(self,emb_size,hidden_size1,hidden_size2=400):
        super().__init__()
        self.embedding = nn.Embedding(emb_size,hidden_size1)
        self.fc1 = nn.Linear(hidden_size2,3)

    def forward(self,x):
        embeds = self.embedding(x).view(x.size(0),-1)
        out = self.fc1(embeds)
        return F.log_softmax(out,dim=-1)

model = EmbNet(len(TEXT.vocab.stoi),300,12000)

加载嵌入向量后,我们必须确保在训练过程中不改变嵌入层权重。让我们讨论如何实现这一点。

冻结嵌入层权重

告诉 PyTorch 不要改变嵌入层权重是一个两步骤过程:

  1. requires_grad属性设置为False,告诉 PyTorch 不需要这些权重的梯度。

  2. 移除传递给优化器的嵌入层参数。如果不执行此步骤,则优化器会抛出错误,因为它期望所有参数都有梯度。

下面的代码展示了如何轻松冻结嵌入层权重,并告知优化器不使用这些参数:

model.embedding.weight.requires_grad = False
optimizer = optim.SGD([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)

通常我们将所有模型参数传递给优化器,但在前面的代码中,我们传递了requires_gradTrue的参数。

我们可以使用这段代码训练模型,并应该获得类似的准确度。所有这些模型架构都没有利用文本的序列性质。在下一节中,我们将探讨两种流行的技术,即 RNN 和 Conv1D,它们利用数据的序列性质。

递归神经网络

RNN 是最强大的模型之一,使我们能够处理分类、序列数据标签和文本生成等应用(例如SwiftKey键盘应用可以预测下一个词),以及将一种序列转换为另一种语言,例如从法语到英语。大多数模型架构如前馈神经网络没有利用数据的序列性质。例如,我们需要数据来表示每个示例的特征向量,比如代表句子、段落或文档的所有标记。前馈网络设计只是一次性查看所有特征并将其映射到输出。让我们看一个文本示例,展示为什么文本的顺序或序列性质很重要。I had cleaned my carI had my car cleaned 是两个英语句子,包含相同的单词集合,但只有在考虑单词顺序时它们的含义不同。

人类通过从左到右阅读单词并构建一个强大的模型来理解文本数据。RNN 的工作方式略有相似,它一次查看文本中的一个单词。RNN 也是一种神经网络,其中有一个特殊的层,该层循环处理数据而不是一次性处理所有数据。由于 RNN 可以按顺序处理数据,我们可以使用不同长度的向量并生成不同长度的输出。以下图像提供了一些不同的表示形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:karpathy.github.io/2015/05/21/rnn-effectiveness/

上述图片来自 RNN 的一个著名博客(karpathy.github.io/2015/05/21/rnn-effectiveness),作者 Andrej Karpathy 讲解如何使用 Python 从头构建 RNN,并将其用作序列生成器。

通过示例理解 RNN 的工作原理

让我们从假设我们已经构建了一个 RNN 模型开始,并尝试理解它提供了哪些功能。一旦我们了解了 RNN 的功能,然后让我们探索 RNN 内部发生了什么。

让我们将《雷神》影评作为 RNN 模型的输入。我们正在查看的示例文本是这部电影的动作场面非常棒……。我们首先将第一个词the传递给我们的模型;模型生成两种不同的东西,一个状态向量和一个输出向量。状态向量在处理评论中的下一个词时传递给模型,并生成一个新的状态向量。我们只考虑在最后一个序列中模型生成的输出。下图总结了这一过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上述图示示了以下内容:

  • 如何通过展开和图像来理解 RNN 的工作方式

  • 如何递归地将状态传递给相同的模型

到现在为止,您应该已经了解了 RNN 的作用,但还不清楚它是如何工作的。在我们深入了解其工作原理之前,让我们先看一个代码片段,详细展示我们所学的内容。我们仍然将 RNN 视为一个黑匣子:

rnn = RNN(input_size, hidden_size,output_size)
for i in range(len(Thor_review):
        output, hidden = rnn(thor_review[i], hidden)

在前面的代码中,hidden变量表示状态向量,有时称为隐藏状态。到现在为止,我们应该已经了解了 RNN 的使用方式。现在,让我们看一下实现 RNN 并理解 RNN 内部发生了什么的代码。以下代码包含RNN类:

import torch.nn as nn
from torch.autograd import Variable

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()
        self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        combined = torch.cat((input, hidden), 1)
        hidden = self.i2h(combined)
        output = self.i2o(combined)
        output = self.softmax(output)
        return output, hidden

    def initHidden(self):
        return Variable(torch.zeros(1, self.hidden_size))

除了前面代码中的词语RNN外,其他内容听起来与我们在前几章中使用的内容非常相似,因为 PyTorch 隐藏了很多反向传播的复杂性。让我们逐步讲解init函数和forward函数,以理解发生了什么。

__init__函数初始化两个线性层,一个用于计算输出,另一个用于计算状态或隐藏向量。

forward函数将input向量和hidden向量结合起来,并通过两个线性层将其传递,生成一个输出向量和一个隐藏状态。对于output层,我们应用了log_softmax函数。

initHidden函数帮助创建没有状态的隐藏向量,用于第一次调用 RNN。让我们通过下图视觉地了解RNN类的作用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上述图展示了 RNN 的工作方式。

第一次遇到 RNN 这些概念有时可能会感到棘手,因此我强烈建议阅读以下链接提供的一些令人惊叹的博客:karpathy.github.io/2015/05/21/rnn-effectiveness/colah.github.io/posts/2015-08-Understanding-LSTMs/.

在接下来的部分中,我们将学习如何使用一种称为LSTM的变种 RNN 来构建一个情感分类器,用于IMDB数据集。

LSTM

RNN 在构建语言翻译、文本分类等许多实际应用中非常流行,但在现实中,我们很少会使用我们在前面部分看到的普通 RNN 版本。普通 RNN 存在梯度消失和梯度爆炸等问题,处理大序列时尤为突出。在大多数真实世界的问题中,会使用像 LSTM 或 GRU 这样的 RNN 变体,这些变体解决了普通 RNN 的局限性,并且能更好地处理序列数据。我们将尝试理解 LSTM 中发生的情况,并基于 LSTM 构建一个网络来解决IMDB数据集上的文本分类问题。

长期依赖

RNN 在理论上应该从历史数据中学习所有必要的依赖关系,以建立下文的上下文。例如,假设我们试图预测句子“the clouds are in the sky”的最后一个词。RNN 可以预测,因为信息(clouds)仅仅落后几个词。再来看一个更长的段落,依赖关系不必那么紧密,我们想预测其中的最后一个词。句子看起来像“我出生在金奈,一个坐落在泰米尔纳德邦的城市。在印度的不同州接受教育,我说…”。在实践中,普通版本的 RNN 很难记住序列前部发生的上下文。LSTM 及其他不同的 RNN 变体通过在 LSTM 内部添加不同的神经网络来解决这个问题,后者决定可以记住多少或者可以记住什么数据。

LSTM 网络

LSTM 是一种特殊类型的 RNN,能够学习长期依赖关系。它们于 1997 年引入,并随着可用数据和硬件的进展在过去几年中变得流行起来。它们在各种问题上表现出色,并被广泛使用。

LSTM 的设计旨在通过一种设计自然地记住信息以解决长期依赖问题。在 RNN 中,我们看到它们如何在序列的每个元素上重复自身。在标准 RNN 中,重复模块将具有像单个线性层的简单结构。

下图显示了一个简单的 RNN 如何重复自身:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 LSTM 内部,我们不是使用简单的线性层,而是在 LSTM 内部有较小的网络,这些网络完成独立的工作。以下图表展示了 LSTM 内部发生的情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/(由 Christopher Olah 绘制的图表)

在上述图表中,第二个框中的每个小矩形(黄色)代表一个 PyTorch 层,圆圈表示元素矩阵或向量加法,合并线表示两个向量正在连接。好消息是,我们不需要手动实现所有这些。大多数现代深度学习框架提供了一个抽象层,可以处理 LSTM 内部的所有功能。PyTorch 提供了 nn.LSTM 层的抽象,我们可以像使用任何其他层一样使用它。LSTM 中最重要的是通过所有迭代传递的细胞状态,如上述图表中横跨细胞的水平线所示。LSTM 内部的多个网络控制信息如何在细胞状态之间传播。LSTM 中的第一步(由符号 σ 表示的小网络)是决定从细胞状态中丢弃哪些信息。这个网络称为 遗忘门,具有 sigmoid 作为激活函数,为细胞状态中的每个元素输出介于 0 和 1 之间的值。该网络(PyTorch 层)由以下公式表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

网络的值决定了哪些值将保存在细胞状态中,哪些将被丢弃。下一步是决定要添加到细胞状态的信息。这有两个部分;一个称为 输入门 的 sigmoid 层,决定要更新的值;一个 tanh 层,用于创建要添加到细胞状态的新值。数学表示如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在下一步中,我们将输入门生成的两个值与 tanh 结合。现在我们可以通过以下公式更新细胞状态,即在遗忘门和 i[t ]与 C[t] 乘积之和的逐元素乘法之间,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,我们需要决定输出,这将是细胞状态的过滤版本。有不同版本的 LSTM 可用,它们大多数都基于类似的原理运作。作为开发人员或数据科学家,我们很少需要担心 LSTM 内部的运作。如果您想更多了解它们,请阅读以下博客链接,这些链接以非常直观的方式涵盖了大量理论。

查看 Christopher Olah 关于 LSTM 的精彩博客(colah.github.io/posts/2015-08-Understanding-LSTMs),以及 Brandon Rohrer 的另一篇博客(brohrer.github.io/how_rnns_lstm_work.html),他在一个很棒的视频中解释了 LSTM。

由于我们理解了 LSTM,让我们实现一个 PyTorch 网络,我们可以用来构建情感分类器。像往常一样,我们将按照以下步骤创建分类器:

  1. 准备数据

  2. 创建批次

  3. 创建网络

  4. 训练模型

准备数据

我们使用相同的 torchtext 来下载、分词和构建IMDB数据集的词汇表。在创建Field对象时,我们将batch_first参数保留为False。RNN 网络期望数据的形式是Sequence_lengthbatch_size和特征。以下用于准备数据集:

TEXT = data.Field(lower=True,fix_length=200,batch_first=False)
LABEL = data.Field(sequential=False,)
train, test = IMDB.splits(TEXT, LABEL)
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)

创建批次

我们使用 torchtext 的BucketIterator来创建批次,批次的大小将是序列长度和批次大小。对于我们的情况,大小将为[200, 32],其中200是序列长度,32是批次大小。

以下是用于分批的代码:

train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=32, device=-1)
train_iter.repeat = False
test_iter.repeat = False

创建网络

让我们看看代码,然后逐步分析代码。您可能会对代码看起来多么相似感到惊讶:

class IMDBRnn(nn.Module):

    def __init__(self,vocab,hidden_size,n_cat,bs=1,nl=2):
        super().__init__()
        self.hidden_size = hidden_size
        self.bs = bs
        self.nl = nl
        self.e = nn.Embedding(n_vocab,hidden_size)
        self.rnn = nn.LSTM(hidden_size,hidden_size,nl)
        self.fc2 = nn.Linear(hidden_size,n_cat)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self,inp):
        bs = inp.size()[1]
        if bs != self.bs:
            self.bs = bs
        e_out = self.e(inp)
        h0 = c0 = Variable(e_out.data.new(*(self.nl,self.bs,self.hidden_size)).zero_())
        rnn_o,_ = self.rnn(e_out,(h0,c0)) 
        rnn_o = rnn_o[-1]
        fc = F.dropout(self.fc2(rnn_o),p=0.8)
        return self.softmax(fc)

init方法创建一个与词汇表大小和hidden_size相同的嵌入层。它还创建一个 LSTM 和一个线性层。最后一层是一个LogSoftmax层,用于将线性层的结果转换为概率。

forward函数中,我们传递大小为[200, 32]的输入数据,它经过嵌入层处理,批次中的每个标记都被嵌入替换,大小变为[200, 32, 100],其中100是嵌入维度。LSTM 层使用嵌入层的输出以及两个隐藏变量进行处理。隐藏变量应与嵌入输出相同类型,并且它们的大小应为[num_layers, batch_size, hidden_size]。LSTM 按序列处理数据并生成形状为[Sequence_length, batch_size, hidden_size]的输出,其中每个序列索引表示该序列的输出。在本例中,我们只取最后一个序列的输出,其形状为[batch_size, hidden_dim],然后将其传递给线性层以映射到输出类别。由于模型容易过拟合,添加一个 dropout 层。您可以调整 dropout 的概率。

训练模型

创建网络后,我们可以使用与前面示例中相同的代码来训练模型。以下是训练模型的代码:

model = IMDBRnn(n_vocab,n_hidden,3,bs=32)
model = model.cuda()

optimizer = optim.Adam(model.parameters(),lr=1e-3)

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 , batch in enumerate(data_loader):
        text , target = batch.text , batch.label
        if is_cuda:
            text,target = text.cuda(),target.cuda()

        if phase == 'training':
            optimizer.zero_grad()
        output = model(text)
        loss = F.nll_loss(output,target)

        running_loss += F.nll_loss(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 = [],[]
val_losses , val_accuracy = [],[]

for epoch in range(1,5):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

以下是训练模型的结果:

#Results

training loss is   0.7 and training accuracy is 12564/25000     50.26
validation loss is   0.7 and validation accuracy is 12500/25000      50.0
training loss is  0.66 and training accuracy is 14931/25000     59.72
validation loss is  0.57 and validation accuracy is 17766/25000     71.06
training loss is  0.43 and training accuracy is 20229/25000     80.92
validation loss is   0.4 and validation accuracy is 20446/25000     81.78
training loss is   0.3 and training accuracy is 22026/25000      88.1
validation loss is  0.37 and validation accuracy is 21009/25000     84.04

对模型进行四次纪元的训练得到了 84%的准确率。训练更多的纪元导致了一个过拟合的模型,因为损失开始增加。我们可以尝试一些我们尝试过的技术,如减少隐藏维度、增加序列长度以及以更小的学习率进行训练以进一步提高准确性。

我们还将探讨如何使用一维卷积来对序列数据进行训练。

序列数据上的卷积网络

我们学习了 CNN 如何通过从图像中学习特征解决计算机视觉问题。在图像中,CNN 通过在高度和宽度上进行卷积来工作。同样,时间可以被视为一个卷积特征。一维卷积有时比 RNN 表现更好,并且计算成本更低。在过去几年中,像 Facebook 这样的公司已经展示出在音频生成和机器翻译方面的成功。在本节中,我们将学习如何使用 CNN 构建文本分类解决方案。

理解序列数据上的一维卷积

在第五章 计算机视觉的深度学习 中,我们看到了如何从训练数据中学习二维权重。这些权重在图像上移动以生成不同的激活。类似地,一维卷积激活在我们的文本分类器训练中也是学习的,这些权重通过在数据上移动来学习模式。以下图解释了一维卷积是如何工作的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了在IMDB数据集上训练文本分类器,我们将按照构建使用 LSTM 分类器的步骤进行操作。唯一不同的是,我们使用batch_first = True,而不像我们的 LSTM 网络那样。因此,让我们看看网络,训练代码以及其结果。

创建网络

让我们先看看网络架构,然后逐步分析代码:

class IMDBCnn(nn.Module):

    def __init__(self,vocab,hidden_size,n_cat,bs=1,kernel_size=3,max_len=200):
        super().__init__()
        self.hidden_size = hidden_size
        self.bs = bs
    self.e = nn.Embedding(n_vocab,hidden_size)
    self.cnn = nn.Conv1d(max_len,hidden_size,kernel_size)
    self.avg = nn.AdaptiveAvgPool1d(10)
        self.fc = nn.Linear(1000,n_cat)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self,inp):
        bs = inp.size()[0]
        if bs != self.bs:
            self.bs = bs
        e_out = self.e(inp)
        cnn_o = self.cnn(e_out) 
        cnn_avg = self.avg(cnn_o)
        cnn_avg = cnn_avg.view(self.bs,-1)
        fc = F.dropout(self.fc(cnn_avg),p=0.5)
        return self.softmax(fc)

在上面的代码中,我们有一个Conv1d层和一个AdaptiveAvgPool1d层而不是一个 LSTM 层。卷积层接受序列长度作为其输入大小,输出大小为隐藏大小,内核大小为 3。由于我们必须改变线性层的维度,每次尝试以不同的长度运行时,我们使用一个AdaptiveAvgpool1d,它接受任意大小的输入并生成给定大小的输出。因此,我们可以使用一个固定大小的线性层。其余代码与我们在大多数网络架构中看到的代码类似。

训练模型

模型的训练步骤与上一个例子相同。让我们看看调用fit方法的代码以及它生成的结果:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]

for epoch in range(1,5):

    epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

我们对模型进行了四轮训练,达到了大约 83%的准确率。以下是运行模型的结果:

training loss is  0.59 and training accuracy is 16724/25000      66.9
validation loss is  0.45 and validation accuracy is 19687/25000     78.75
training loss is  0.38 and training accuracy is 20876/25000      83.5
validation loss is   0.4 and validation accuracy is 20618/25000     82.47
training loss is  0.28 and training accuracy is 22109/25000     88.44
validation loss is  0.41 and validation accuracy is 20713/25000     82.85
training loss is  0.22 and training accuracy is 22820/25000     91.28
validation loss is  0.44 and validation accuracy is 20641/25000     82.56

由于验证损失在三轮后开始增加,我停止了模型的运行。我们可以尝试几件事情来改善结果,如使用预训练权重、添加另一个卷积层,并在卷积之间尝试MaxPool1d层。我把这些尝试留给你来测试是否能提高准确率。

概要

在本章中,我们学习了不同的技术来表示深度学习中的文本数据。我们学习了如何在处理不同领域时使用预训练的词嵌入和我们自己训练的词嵌入。我们使用 LSTM 和一维卷积构建了文本分类器。

在下一章中,我们将学习如何训练深度学习算法来生成时尚图像和新图像,并生成文本。

第七章:生成网络

在前几章中,我们看到的所有示例都集中在解决分类或回归等问题上。这一章对理解深度学习如何发展以解决无监督学习问题非常有趣和重要。

在本章中,我们将训练网络,学习如何创建:

  • 基于内容和特定艺术风格生成的图像,通常称为风格迁移

  • 使用特定类型的**生成对抗网络(GAN)**生成新人物面孔

  • 使用语言建模生成新文本

这些技术构成了深度学习领域大部分高级研究的基础。深入探讨 GAN 和语言建模等子领域的确切细节超出了本书的范围,它们值得有专门的书籍来介绍。我们将学习它们的一般工作原理以及在 PyTorch 中构建它们的过程。

神经风格迁移

我们人类以不同的准确度和复杂性生成艺术作品。尽管创作艺术的过程可能非常复杂,但它可以被视为两个最重要因素的结合,即要绘制什么和如何绘制。绘制什么受我们周围所见的启发,而如何绘制也将受到我们周围某些事物的影响。从艺术家的角度来看,这可能是一种过度简化,但对于理解如何使用深度学习算法创建艺术作品,它非常有用。我们将训练一个深度学习算法,从一幅图像中获取内容,然后按照特定的艺术风格进行绘制。如果您是艺术家或从事创意行业,您可以直接利用近年来进行的惊人研究来改进并在您工作的领域内创造出一些很酷的东西。即使您不是,它也会向您介绍生成模型领域,其中网络生成新内容。

让我们在高层次理解神经风格迁移的过程,然后深入细节,以及构建它所需的 PyTorch 代码。风格迁移算法提供了一个内容图像(C)和一个风格图像(S),算法必须生成一个新图像(O),该图像具有来自内容图像的内容和来自风格图像的风格。这一创建神经风格迁移的过程由 Leon Gates 等人于 2015 年介绍(艺术风格的神经算法)。以下是我们将使用的内容图像(C):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来源:https://arxiv.org/pdf/1508.06576.pdf

下面是样式图像(S):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来源:https://arxiv.org/pdf/1508.06576.pdf

这是我们将要生成的图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来源:https://arxiv.org/pdf/1508.06576.pdf

从理解卷积神经网络CNNs)工作方式的角度来看,样式转移的背后思想变得直观。当 CNN 用于对象识别训练时,训练的 CNN 的早期层学习非常通用的信息,如线条、曲线和形状。CNN 的最后层捕捉图像的更高级概念,如眼睛、建筑物和树木。因此,类似图像的最后层的值倾向于更接近。我们将相同的概念应用于内容损失。内容图像和生成图像的最后一层应该是相似的,我们使用均方误差MSE)来计算相似性。我们使用优化算法降低损失值。

通过称为格拉姆矩阵的技术,CNN 通常在多个层次上捕获图像的样式。格拉姆矩阵计算跨多个层次捕获的特征映射之间的相关性。格拉姆矩阵提供计算样式的一种方法。具有类似风格的图像对于格拉姆矩阵具有相似的值。样式损失也是使用样式图像和生成图像的格拉姆矩阵之间的 MSE 计算的。

我们将使用提供在 torchvision 模型中的预训练 VGG19 模型。训练样式转移模型所需的步骤与任何其他深度学习模型类似,唯一不同的是计算损失比分类或回归模型更复杂。神经风格算法的训练可以分解为以下步骤:

  1. 加载数据。

  2. 创建 VGG19 模型。

  3. 定义内容损失。

  4. 定义样式损失。

  5. 从 VGG 模型中提取跨层的损失。

  6. 创建优化器。

  7. 训练 - 生成与内容图像类似的图像,并且风格与样式图像类似。

加载数据。

加载数据与我们在第五章中解决图像分类问题所见的方式类似,《计算机视觉深度学习》。我们将使用预训练的 VGG 模型,因此必须使用与预训练模型相同的值对图像进行标准化。

以下代码展示了我们如何做到这一点。代码大部分是自解释的,因为我们已经在前几章中详细讨论过:

#Fixing the size of the image, reduce it further if you are not using a GPU.
imsize = 512 
is_cuda = torch.cuda.is_available()

#Converting image ,making it suitable for training using the VGG model.

prep = transforms.Compose([transforms.Resize(imsize),
                           transforms.ToTensor(),
                           transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]), #turn to BGR
                           transforms.Normalize(mean=[0.40760392, 0.45795686, 0.48501961], #subtract imagenet mean
                                                std=[1,1,1]),
                           transforms.Lambda(lambda x: x.mul_(255)),
                          ])

#Converting the generated image back to a format which we can visualise. 

postpa = transforms.Compose([transforms.Lambda(lambda x: x.mul_(1./255)),
                           transforms.Normalize(mean=[-0.40760392, -0.45795686, -0.48501961], #add imagenet mean
                                                std=[1,1,1]),
                           transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]), #turn to RGB
                           ])
postpb = transforms.Compose([transforms.ToPILImage()])

#This method ensures data in the image does not cross the permissible range .
def postp(tensor): # to clip results in the range [0,1]
    t = postpa(tensor)
    t[t>1] = 1 
    t[t<0] = 0
    img = postpb(t)
    return img

#A utility function to make data loading easier.
def image_loader(image_name):
    image = Image.open(image_name)
    image = Variable(prep(image))
    # fake batch dimension required to fit network's input dimensions
    image = image.unsqueeze(0)
    return image

在此代码中,我们定义了三个功能,prep执行所有所需的预处理,并使用与训练 VGG 模型相同的值进行标准化。模型的输出需要恢复到其原始值;postpa函数执行所需的处理。生成的模型可能超出接受值的范围,postp函数将所有大于 1 的值限制为 1,所有小于 0 的值限制为 0。最后,image_loader函数加载图像,应用预处理转换,并将其转换为变量。以下功能加载样式和内容图像:

style_img = image_loader("Images/vangogh_starry_night.jpg")
content_img = image_loader("Images/Tuebingen_Neckarfront.jpg")

我们可以创建一个带有噪声(随机数)的图像,也可以使用相同的内容图像。在这种情况下,我们将使用内容图像。以下代码创建内容图像:

opt_img = Variable(content_img.data.clone(),requires_grad=True)

我们将使用优化器调整 opt_img 的值,以便图像更接近内容图像和样式图像。因此,我们通过指定 requires_grad=True 要求 PyTorch 保持梯度。

创建 VGG 模型

我们将从 torchvisions.models 中加载一个预训练模型。我们将仅使用此模型来提取特征,PyTorch 的 VGG 模型被定义为所有卷积块在 features 模块中,而全连接或线性层在 classifier 模块中。由于我们不会训练 VGG 模型中的任何权重或参数,我们还将冻结该模型。以下代码演示了同样的操作:

#Creating a pretrained VGG model
vgg = vgg19(pretrained=True).features

#Freezing the layers as we will not use it for training.
for param in vgg.parameters():
    param.requires_grad = False

在这段代码中,我们创建了一个 VGG 模型,仅使用其卷积块,并冻结了模型的所有参数,因为我们只会用它来提取特征。

内容损失

内容损失 是在通过网络传递两个图像后提取的特定层输出上计算的均方误差(MSE)。我们通过使用 register_forward_hook 功能从 VGG 中提取中间层的输出来计算这些层的输出的 MSE,如下代码所述。

target_layer = dummy_fn(content_img)
noise_layer = dummy_fn(noise_img)
criterion = nn.MSELoss()
content_loss = criterion(target_layer,noise_layer)

在接下来的部分,我们将实现这段代码中的 dummy_fn 函数。目前我们只知道,dummy_fn 函数通过传递图像返回特定层的输出。我们将通过将内容图像和噪声图像传递给 MSE loss 函数来传递生成的输出。

样式损失

样式损失 在多个层次上计算。样式损失是每个特征图生成的格拉姆矩阵的均方误差(MSE)。格拉姆矩阵表示其特征的相关性值。让我们通过以下图表和代码实现来理解格拉姆矩阵的工作方式。

以下表格显示了具有列属性 Batch_sizeChannelsValues 的特征图维度为 [2, 3, 3, 3] 的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要计算格拉姆矩阵,我们将所有通道的值展平,然后通过与其转置相乘找到相关性,如下表所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们所做的只是将所有通道的值展平为单个向量或张量。以下代码实现了这一点:

class GramMatrix(nn.Module):

    def forward(self,input):
        b,c,h,w = input.size()
        features = input.view(b,c,h*w)
        gram_matrix = torch.bmm(features,features.transpose(1,2))
        gram_matrix.div_(h*w)
        return gram_matrix

我们将 GramMatrix 实现为另一个 PyTorch 模块,并具有一个 forward 函数,以便我们可以像使用 PyTorch 层一样使用它。在此行中,我们从输入图像中提取不同的维度:

b,c,h,w = input.size()

这里,b代表批量大小,c代表过滤器或通道数,h代表高度,w代表宽度。在下一步中,我们将使用以下代码保持批量和通道维度不变,并展平所有高度和宽度维度的值,如前图所示:

features = input.view(b,c,h*w)

Gram 矩阵通过将其展平的值与其转置向量相乘来计算。我们可以使用 PyTorch 提供的批量矩阵乘法函数torch.bmm()来执行此操作,如下代码所示:

gram_matrix = torch.bmm(features,features.transpose(1,2))

我们通过将 Gram 矩阵的值除以元素数量来完成 Gram 矩阵的值归一化。这可以防止具有大量值的特定特征图支配得分。一旦计算了GramMatrix,就可以简单地计算风格损失,该损失在以下代码中实现:

class StyleLoss(nn.Module):

    def forward(self,inputs,targets):
        out = nn.MSELoss()(GramMatrix()(inputs),targets)
        return (out)

StyleLoss作为另一个 PyTorch 层实现。它计算输入GramMatrix值与风格图像GramMatrix值之间的均方误差。

提取损失

就像我们在《深度学习计算机视觉》第五章中使用register_forward_hook()函数提取卷积层的激活一样,我们可以提取不同卷积层的损失,用于计算风格损失和内容损失。这种情况的一个不同之处在于,我们不是从一个层中提取,而是需要从多个层中提取输出。以下类整合了所需的变更:

class LayerActivations():
    features=[]

    def __init__(self,model,layer_nums):

        self.hooks = []
        for layer_num in layer_nums:
            self.hooks.append(model[layer_num].register_forward_hook(self.hook_fn))

    def hook_fn(self,module,input,output):
        self.features.append(output)

    def remove(self):
        for hook in self.hooks:
            hook.remove()

__init__方法接受我们需要调用register_forward_hook方法的模型以及我们需要提取输出的层编号。__init__方法中的for循环遍历层编号并注册所需的前向钩子以提取输出。

传递给register_forward_hook方法的hook_fn将在注册hook_fn函数的层之后由 PyTorch 调用。在函数内部,我们捕获输出并将其存储在features数组中。

当我们不想捕获输出时,我们需要调用remove函数一次。忘记调用remove方法可能导致内存不足异常,因为所有输出都会累积。

让我们编写另一个实用函数,它可以提取用于风格和内容图像的输出。以下函数执行相同操作:

def extract_layers(layers,img,model=None):

    la = LayerActivations(model,layers)
    #Clearing the cache 
    la.features = []
    out = model(img)
    la.remove()
    return la.features

extract_layers函数内部,我们通过传入模型和层编号来创建LayerActivations类的对象。特征列表可能包含先前运行的输出,因此我们将其重新初始化为空列表。然后我们通过模型传入图像,我们不会使用输出。我们更感兴趣的是生成在features数组中的输出。我们调用remove方法从模型中移除所有注册的钩子并返回特征。以下代码展示了我们如何提取风格和内容图像所需的目标:

content_targets = extract_layers(content_layers,content_img,model=vgg)
style_targets = extract_layers(style_layers,style_img,model=vgg)

一旦我们提取了目标,我们需要从创建它们的图中分离输出。请记住,所有这些输出都是保持它们如何创建的 PyTorch 变量。但对于我们的情况,我们只对输出值感兴趣,而不是图,因为我们不会更新style图像或content图像。以下代码说明了这种技术:

content_targets = [t.detach() for t in content_targets]
style_targets = [GramMatrix()(t).detach() for t in style_targets]

一旦我们分离了目标,让我们将所有目标添加到一个列表中。以下代码说明了这种技术:

targets = style_targets + content_targets

在计算风格损失和内容损失时,我们传递了称为内容层和风格层的两个列表。不同的层选择将影响生成图像的质量。让我们选择与论文作者提到的相同层。以下代码显示了我们在这里使用的层的选择:

style_layers = [1,6,11,20,25]
content_layers = [21]
loss_layers = style_layers + content_layers

优化器期望最小化单一标量量。为了实现单一标量值,我们将所有到达不同层的损失求和。习惯上,对这些损失进行加权求和是常见做法,我们选择的权重与 GitHub 仓库中论文实现中使用的相同(github.com/leongatys/PytorchNeuralStyleTransfer)。我们的实现是作者实现的略微修改版本。以下代码描述了正在使用的权重,这些权重由所选层中的滤波器数量计算得出:

style_weights = [1e3/n**2 for n in [64,128,256,512,512]]
content_weights = [1e0]
weights = style_weights + content_weights

要可视化这一点,我们可以打印 VGG 层。花一分钟观察我们选择了哪些层,并尝试不同的层组合。我们将使用以下代码来print VGG 层:

print(vgg)

#Results 

Sequential(
  (0): Conv2d (3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (1): ReLU(inplace)
  (2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (3): ReLU(inplace)
  (4): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (5): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (6): ReLU(inplace)
  (7): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (8): ReLU(inplace)
  (9): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (10): Conv2d (128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (11): ReLU(inplace)
  (12): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (13): ReLU(inplace)
  (14): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (15): ReLU(inplace)
  (16): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (17): ReLU(inplace)
  (18): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (19): Conv2d (256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (20): ReLU(inplace)
  (21): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (22): ReLU(inplace)
  (23): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (24): ReLU(inplace)
  (25): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (26): ReLU(inplace)
  (27): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (28): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (29): ReLU(inplace)
  (30): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (31): ReLU(inplace)
  (32): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (33): ReLU(inplace)
  (34): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (35): ReLU(inplace)
  (36): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
)

我们必须定义loss函数和optimizer来生成艺术图像。我们将在以下部分初始化它们两个。

为每一层创建损失函数

我们已经定义了 PyTorch 层作为loss函数。现在,让我们为不同的风格损失和内容损失创建损失层。以下代码定义了该函数:

loss_fns = [StyleLoss()] * len(style_layers) + [nn.MSELoss()] * len(content_layers)

loss_fns是一个包含一系列风格损失对象和内容损失对象的列表,基于创建的数组长度。

创建优化器

一般来说,我们会传递网络(如 VGG)的参数进行训练。但在这个例子中,我们将 VGG 模型作为特征提取器使用,因此不能传递 VGG 的参数。在这里,我们只会提供opt_img变量的参数,我们将优化它们以使图像具有所需的内容和风格。以下代码创建了优化器来优化它的值:

optimizer = optim.LBFGS([opt_img]);

现在我们已经有了所有的训练组件。

训练

与我们之前训练的其他模型相比,training方法有所不同。在这里,我们需要在多个层次计算损失,并且每次调用优化器时,它都会改变输入图像,使其内容和样式接近目标的内容和样式。让我们看一下用于训练的代码,然后我们将详细介绍训练的重要步骤:

max_iter = 500
show_iter = 50
n_iter=[0]

while n_iter[0] <= max_iter:

    def closure():
        optimizer.zero_grad()

        out = extract_layers(loss_layers,opt_img,model=vgg)
        layer_losses = [weights[a] * loss_fnsa for a,A in enumerate(out)]
        loss = sum(layer_losses)
        loss.backward()
        n_iter[0]+=1
        #print loss
        if n_iter[0]%show_iter == (show_iter-1):
            print('Iteration: %d, loss: %f'%(n_iter[0]+1, loss.data[0]))

        return loss

    optimizer.step(closure)

我们正在运行为期500次迭代的训练循环。对于每一次迭代,我们使用我们的extract_layers函数计算 VGG 模型不同层的输出。在这种情况下,唯一变化的是opt_img的值,它将包含我们的样式图像。一旦计算出输出,我们通过迭代输出并将它们传递给相应的loss函数及其各自的目标来计算损失。我们将所有损失相加并调用backward函数。在closure函数的末尾,返回损失。closure方法与optimizer.step方法一起调用max_iter次。如果在 GPU 上运行,可能需要几分钟;如果在 CPU 上运行,请尝试减小图像大小以加快运行速度。

运行 500 个周期后,在我的设备上生成的图像如下所示。尝试不同的内容和样式组合来生成有趣的图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在下一节中,让我们使用深度卷积生成对抗网络DCGANs)生成人脸。

生成对抗网络

GANs 在过去几年变得非常流行。每周都有一些 GAN 领域的进展。它已成为深度学习的重要子领域之一,拥有非常活跃的研究社区。GAN 是由 Ian Goodfellow 于 2014 年引入的。GAN 通过训练两个深度神经网络,称为生成器判别器,它们相互竞争来解决无监督学习的问题。在训练过程中,它们最终都变得更擅长它们所执行的任务。

用仿冒者(生成器)和警察(判别器)的案例直观理解 GAN。最初,仿冒者向警察展示假钞。警察识别出它是假的,并解释为什么是假的。仿冒者根据收到的反馈制作新的假钞。警察发现它是假的,并告知仿冒者为什么是假的。这个过程重复了很多次,直到仿冒者能够制作出警察无法辨别的假钞。在 GAN 场景中,我们最终得到一个生成器生成的假图像与真实图像非常相似,而分类器则变得擅长辨别真假。

GAN 是一个伪造网络和专家网络的组合,每个网络都被训练来击败对方。生成器网络以随机向量作为输入并生成合成图像。鉴别器网络接收输入图像并预测图像是真实的还是伪造的。我们向鉴别器网络传递的是真实图像或伪造图像。

生成器网络被训练来生成图像,并欺骗鉴别器网络认为它们是真实的。鉴别器网络也在不断提高其不被欺骗的能力,因为我们在训练中传递反馈。尽管 GAN 的理念在理论上听起来很简单,但训练一个真正有效的 GAN 模型却非常困难。训练 GAN 也很具挑战性,因为需要训练两个深度神经网络。

DCGAN 是早期展示如何构建自学习并生成有意义图像的 GAN 模型之一。您可以在此了解更多:

arxiv.org/pdf/1511.06434.pdf

下图显示了 GAN 模型的架构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们将逐步介绍这种架构的每个组件及其背后的一些理论,然后我们将在下一节中用 PyTorch 实现相同的流程。通过此实现,我们将对 DCGAN 的工作原理有基本的了解。

深度卷积 GAN

在本节中,我们将基于我在前面信息框中提到的 DCGAN 论文 来实现训练 GAN 架构的不同部分。训练 DCGAN 的一些重要部分包括:

  • 一个生成器网络,将某个固定维度的潜在向量(数字列表)映射到某种形状的图像。在我们的实现中,形状是 (3, 64, 64)。

  • 一个鉴别器网络,它将生成器生成的图像或来自实际数据集的图像作为输入,并映射到一个评分,用于估计输入图像是真实还是伪造的。

  • 为生成器和鉴别器定义损失函数。

  • 定义优化器。

  • 训练一个 GAN。

让我们详细探讨这些部分的每一个。这些实现基于 PyTorch 示例代码,可在以下位置找到:

github.com/pytorch/examples/tree/master/dcgan

定义生成器网络

生成器网络以固定维度的随机向量作为输入,并对其应用一组转置卷积、批归一化和 ReLU 激活,生成所需尺寸的图像。在查看生成器实现之前,让我们来定义一下转置卷积和批归一化。

转置卷积

转置卷积也称为分数步进卷积。它们的工作方式与卷积相反。直观地说,它们尝试计算如何将输入向量映射到更高的维度。让我们看下图以更好地理解它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此图来自 Theano(另一个流行的深度学习框架)的文档(deeplearning.net/software/theano/tutorial/conv_arithmetic.html)。如果您想深入了解步进卷积的工作原理,我强烈建议您阅读 Theano 文档中的这篇文章。对我们来说重要的是,它有助于将一个向量转换为所需维度的张量,并且我们可以通过反向传播来训练核的值。

批归一化

我们已经多次观察到,所有传递给机器学习或深度学习算法的特征都是经过归一化的;也就是说,特征的值通过减去数据的均值使其居中于零,并通过除以其标准差使数据具有单位标准差。通常我们会使用 PyTorch 的torchvision.Normalize方法来实现这一点。以下代码展示了一个示例:

transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

在我们所看到的所有示例中,数据在进入神经网络之前都是归一化的;不能保证中间层获得归一化的输入。以下图显示了神经网络中间层无法获取归一化数据的情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

批归一化就像是一个中间函数,或者一个在训练过程中当均值和方差随时间变化时规范化中间数据的层。批归一化由 Ioffe 和 Szegedy 在 2015 年提出(arxiv.org/abs/1502.03167)。批归一化在训练和验证或测试时的行为是不同的。训练时,批内数据的均值和方差被计算。验证和测试时,使用全局值。我们需要理解的是它如何规范化中间数据。使用批归一化的一些关键优势是:

  • 提升网络中的梯度流,从而帮助我们构建更深的网络

  • 允许更高的学习率

  • 减少初始化的强依赖

  • 作为一种正则化形式,减少了 dropout 的依赖性

大多数现代架构,如 ResNet 和 Inception,在其架构中广泛使用批归一化。批归一化层通常在卷积层或线性/全连接层之后引入,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

到目前为止,我们对生成网络的关键组成部分有了直观的理解。

生成器

让我们快速查看以下生成器网络代码,然后讨论生成器网络的关键特性:

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

        self.main = nn.Sequential(
            # input is Z, going into a convolution
            nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # state size. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # state size. (ngf*4) x 8 x 8
            nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # state size. (ngf*2) x 16 x 16
            nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # state size. (ngf) x 32 x 32
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # state size. (nc) x 64 x 64
        )

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

netG = Generator()
netG.apply(weights_init)
print(netG)

在我们看到的大多数代码示例中,我们使用了许多不同的层,并在forward方法中定义了流程。在生成器网络中,我们使用顺序模型在__init__方法中定义层和数据流动。

模型接受尺寸为nz的张量作为输入,然后通过转置卷积将输入映射到需要生成的图像大小。forward函数将输入传递给顺序模块并返回输出。

生成器网络的最后一层是一个tanh层,它限制了网络可以生成的值的范围。

不使用相同的随机权重,而是使用在论文中定义的权重初始化模型。以下是权重初始化代码:

def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        m.weight.data.normal_(0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        m.weight.data.normal_(1.0, 0.02)
        m.bias.data.fill_(0)

我们通过将函数传递给生成器对象netG调用weight函数。每一层都通过函数传递;如果层是卷积层,我们以不同的方式初始化权重,如果是BatchNorm,我们稍微不同地初始化它。我们使用以下代码在网络对象上调用函数:

netG.apply(weights_init)

定义鉴别器网络

让我们快速查看以下鉴别器网络代码,然后讨论鉴别器网络的关键特性:

class Discriminator(nn.Module):
    def __init__(self):
        super(_netD, self).__init__()
        self.main = nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        output = self.main(input)
        return output.view(-1, 1).squeeze(1)

netD = Discriminator()
netD.apply(weights_init)
print(netD)

前面网络中有两个重要的事情,即使用leaky ReLU作为激活函数,以及在最后一个激活层中使用 sigmoid。首先,让我们了解一下什么是 Leaky ReLU。

Leaky ReLU 是解决 ReLU 死亡问题的一种尝试。Leaky ReLU 不像在输入为负时函数返回零,而是输出一个非常小的数值,如 0.001。论文表明,使用 Leaky ReLU 可以提高鉴别器的效率。

另一个重要的区别是在鉴别器末端不使用全连接层。通常看到最后的全连接层被全局平均池化层替换。但使用全局平均池化会减少收敛速度(构建准确分类器所需的迭代次数)。最后的卷积层被展平并传递给 sigmoid 层。

除了这两个差异之外,网络的其余部分与我们在本书中看到的其他图像分类器网络非常相似。

定义损失和优化器

我们将在下面的代码中定义一个二元交叉熵损失和两个优化器,一个用于生成器,另一个用于鉴别器:

criterion = nn.BCELoss()

# setup optimizer
optimizerD = optim.Adam(netD.parameters(), lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr, betas=(beta1, 0.999))

到目前为止,这与我们在所有先前的例子中看到的非常相似。让我们探讨一下如何训练生成器和鉴别器。

训练鉴别器

鉴别器网络的损失取决于其在真实图像上的表现和在生成器网络生成的假图像上的表现。损失可以定义为:

loss = 最大化 log(D(x)) + log(1-D(G(z)))

因此,我们需要使用真实图像和生成器网络生成的假图像来训练鉴别器。

使用真实图像训练鉴别器网络

让我们传递一些真实图像作为地面实况来训练鉴别器。

首先,我们将查看执行相同操作的代码,然后探索重要特性:

output = netD(inputv)
errD_real = criterion(output, labelv)
errD_real.backward()

在前述代码中,我们计算了用于鉴别器图像的损失和梯度。inputvlabelv 表示来自 CIFAR10 数据集的输入图像和标签,其中真实图像的标签为 1。这非常直观,因为它类似于我们为其他图像分类器网络所做的工作。

使用假图像训练鉴别器

现在传递一些随机图像来训练鉴别器。

让我们查看代码,并探索重要特性:

fake = netG(noisev)
output = netD(fake.detach())
errD_fake = criterion(output, labelv)
errD_fake.backward()
optimizerD.step()

代码中的第一行传递了一个大小为 100 的向量,生成器网络 (netG) 生成一幅图像。我们将图像传递给鉴别器以确定图像是真实还是假的。我们不希望生成器被训练,因为鉴别器正在被训练。因此,我们通过在其变量上调用 detach 方法从其图中移除假图像。一旦计算出所有梯度,我们调用 optimizer 训练鉴别器。

训练生成器网络

让我们查看代码,并探索重要特性:

netG.zero_grad()
labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
output = netD(fake)
errG = criterion(output, labelv)
errG.backward()
optimizerG.step()

它看起来与我们在训练鉴别器使用假图像时所做的类似,除了一些关键区别。我们传递了由生成器创建的同样的假图像,但这次我们没有从生成图中分离它,因为我们希望训练生成器。我们计算损失 (errG) 并计算梯度。然后我们调用生成器优化器,因为我们只想训练生成器,并在生成器生成略微逼真图像之前重复整个过程多次。

训练完整网络

我们逐个分析了 GAN 的训练过程。让我们总结如下,并查看用于训练我们创建的 GAN 网络的完整代码:

  • 使用真实图像训练鉴别器网络

  • 使用假图像训练鉴别器网络

  • 优化鉴别器

  • 基于鉴别器反馈训练生成器

  • 优化生成器网络

我们将使用以下代码来训练网络:

for epoch in range(niter):
    for i, data in enumerate(dataloader, 0):
        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################
        # train with real
        netD.zero_grad()
        real, _ = data
        batch_size = real.size(0)
        if torch.cuda.is_available():
            real = real.cuda()
        input.resize_as_(real).copy_(real)
        label.resize_(batch_size).fill_(real_label)
        inputv = Variable(input)
        labelv = Variable(label)

        output = netD(inputv)
        errD_real = criterion(output, labelv)
        errD_real.backward()
        D_x = output.data.mean()

        # train with fake
        noise.resize_(batch_size, nz, 1, 1).normal_(0, 1)
        noisev = Variable(noise)
        fake = netG(noisev)
        labelv = Variable(label.fill_(fake_label))
        output = netD(fake.detach())
        errD_fake = criterion(output, labelv)
        errD_fake.backward()
        D_G_z1 = output.data.mean()
        errD = errD_real + errD_fake
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################
        netG.zero_grad()
        labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
        output = netD(fake)
        errG = criterion(output, labelv)
        errG.backward()
        D_G_z2 = output.data.mean()
        optimizerG.step()

        print('[%d/%d][%d/%d] Loss_D: %.4f Loss_G: %.4f D(x): %.4f D(G(z)): %.4f / %.4f'
              % (epoch, niter, i, len(dataloader),
                 errD.data[0], errG.data[0], D_x, D_G_z1, D_G_z2))
        if i % 100 == 0:
            vutils.save_image(real_cpu,
                    '%s/real_samples.png' % outf,
                    normalize=True)
            fake = netG(fixed_noise)
            vutils.save_image(fake.data,
                    '%s/fake_samples_epoch_%03d.png' % (outf, epoch),
                    normalize=True)

vutils.save_image 将把一个张量保存为图像。如果提供一个小批量的图像,则将它们保存为图像网格。

在接下来的部分,我们将看看生成的图像和真实图像的外观。

检查生成的图像

所以,让我们比较生成的图像和真实图像。

生成的图像如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

真实图像如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

比较两组图像,我们可以看到我们的 GAN 能够学习如何生成图像。除了训练生成新图像之外,我们还有一个判别器,可用于分类问题。当有限数量的标记数据可用时,判别器学习关于图像的重要特征,这些特征可以用于分类任务。当有限的标记数据时,我们可以训练一个 GAN,它将为我们提供一个分类器,可以用于提取特征,并且可以在其上构建一个分类器模块。

在下一节中,我们将训练一个深度学习算法来生成文本。

语言建模

我们将学习如何教授循环神经网络RNN)如何创建文本序列。简单来说,我们现在要构建的 RNN 模型将能够根据一些上下文预测下一个单词。这就像你手机上的Swift应用程序,它猜测你正在输入的下一个单词一样。生成序列数据的能力在许多不同领域都有应用,例如:

  • 图像字幕

  • 语音识别

  • 语言翻译

  • 自动电子邮件回复

我们在 第六章 中学到,使用序列数据和文本的深度学习,RNN 难以训练。因此,我们将使用一种称为长短期记忆网络LSTM)的变体。LSTM 算法的开发始于 1997 年,但在过去几年变得流行起来。它因为强大的硬件和高质量数据的可用性,以及一些进展(如 dropout),使得训练更好的 LSTM 模型比以前更容易。

使用 LSTM 模型生成字符级语言模型或单词级语言模型非常流行。在字符级语言建模中,我们提供一个字符,LSTM 模型被训练来预测下一个字符,而在单词级语言建模中,我们提供一个单词,LSTM 模型预测下一个单词。在本节中,我们将使用 PyTorch LSTM 模型构建一个单词级语言模型。就像训练任何其他模块一样,我们将遵循标准步骤:

  • 准备数据

  • 生成数据批次

  • 基于 LSTM 定义模型

  • 训练模型

  • 测试模型

本节内容灵感来自于 PyTorch 中稍微简化的词语言建模示例,详情请见 github.com/pytorch/examples/tree/master/word_language_model

准备数据

对于本例,我们使用名为WikiText2的数据集。WikiText 语言建模数据集包含从维基百科上的验证过的GoodFeatured文章中提取的 1 亿多个标记。与另一个广泛使用的数据集Penn TreebankPTB)的预处理版本相比,WikiText-2大约大两倍。WikiText数据集还具有更大的词汇表,并保留了原始大小写、标点和数字。该数据集包含完整的文章,因此非常适合利用长期依赖的模型。

该数据集是在一篇名为Pointer Sentinel 混合模型的论文中介绍的(arxiv.org/abs/1609.07843)。该论文讨论了用于解决特定问题的解决方案,其中 LSTM 与 softmax 层在预测稀有单词时存在困难,尽管上下文不清楚。暂时不要担心这个问题,因为这是一个高级概念,超出了本书的范围。

下面的屏幕截图显示了 WikiText 转储文件内部的数据样式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通常情况下,torchtext通过提供下载和读取数据集的抽象,使使用数据集变得更加容易。让我们看看执行此操作的代码:

TEXT = d.Field(lower=True, batch_first=True)
train, valid, test = datasets.WikiText2.splits(TEXT,root='data')

先前的代码负责下载WikiText2数据并将其分成trainvalidtest数据集。语言建模的关键区别在于如何处理数据。我们在WikiText2中有的所有文本数据都存储在一个长张量中。让我们看一下下面的代码和结果,以更好地理解数据的处理方式:

print(len(train[0].text))

#output
2088628

如前所述的结果显示,我们只有一个示例字段,其中包含所有文本。让我们快速看一下文本的表示方式:

print(train[0].text[:100])

#Results of first 100 tokens

'<eos>', '=', 'valkyria', 'chronicles', 'iii', '=', '<eos>', '<eos>', 'senjō', 'no', 'valkyria', '3', ':', '<unk>', 'chronicles', '(', 'japanese', ':', '3', ',', 'lit', '.', 'valkyria', 'of', 'the', 'battlefield', '3', ')', ',', 'commonly', 'referred', 'to', 'as', 'valkyria', 'chronicles', 'iii', 'outside', 'japan', ',', 'is', 'a', 'tactical', 'role', '@-@', 'playing', 'video', 'game', 'developed', 'by', 'sega', 'and', 'media.vision', 'for', 'the', 'playstation', 'portable', '.', 'released', 'in', 'january', '2011', 'in', 'japan', ',', 'it', 'is', 'the', 'third', 'game', 'in', 'the', 'valkyria', 'series', '.', '<unk>', 'the', 'same', 'fusion', 'of', 'tactical', 'and', 'real', '@-@', 'time', 'gameplay', 'as', 'its', 'predecessors', ',', 'the', 'story', 'runs', 'parallel', 'to', 'the', 'first', 'game', 'and', 'follows', 'the'

现在,快速查看显示初始文本及其如何被标记化的图像。现在我们有一个长度为2088628的长序列,表示为WikiText2。接下来重要的事情是如何对数据进行分批处理。

生成批次

让我们看一下代码,了解序列数据分批过程中涉及的两个关键要素:

train_iter, valid_iter, test_iter = data.BPTTIterator.splits(
    (train, valid, test), batch_size=20, bptt_len=35, device=0)

此方法中有两个重要的事情。一个是batch_size,另一个是称为时间反向传播backpropagation through time,简称bptt_len)。它简要说明了数据在每个阶段如何转换。

批次

整个数据作为一个序列进行处理相当具有挑战性,而且计算效率不高。因此,我们将序列数据分成多个批次,将每个批次视为单独的序列。尽管这听起来可能并不直接,但效果要好得多,因为模型可以从数据批次中更快地学习。让我们以英文字母顺序为例进行分组。

序列:a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z.

当我们将前述字母序列转换为四批时,我们得到:

a    g    m    s    y

b    h    n    t    z

c    i     o    u

d   j     p     v

e   k    q     w

f    l     r     x

在大多数情况下,我们会剔除最后多余的单词或标记,因为它对文本建模没有太大影响。

对于例子WikiText2,当我们将数据分成 20 批次时,我们会得到每个批次 104431 个元素。

时间反向传播

我们看到通过迭代器传递的另一个重要变量是时间反向传播BPTT)。它实际上是指模型需要记住的序列长度。数字越大,效果越好——但模型的复杂性和需要的 GPU 内存也会增加。

为了更好地理解它,让我们看看如何将前面批量化的字母数据分成长度为两的序列:

a    g    m    s

b    h    n    t

前面的示例将作为输入传递给模型,并且输出将是来自序列但包含下一个值的:

b    h    n    t

c    I      o    u

对于例子WikiText2,当我们将批量数据分割时,我们得到大小为 30 的数据,每个批次为 20,其中30是序列长度。

基于 LSTM 定义一个模型

我们定义了一个模型,它有点类似于我们在第六章中看到的网络,Deep Learning with Sequence Data and Text,但它有一些关键的不同之处。网络的高级架构如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

和往常一样,让我们先看看代码,然后逐步讲解其中的关键部分:

class RNNModel(nn.Module):
    def __init__(self,ntoken,ninp,nhid,nlayers,dropout=0.5,tie_weights=False):
        #ntoken represents the number of words in vocabulary.
        #ninp Embedding dimension for each word ,which is the input for the LSTM.
        #nlayer Number of layers required to be used in the LSTM .
        #Dropout to avoid overfitting.
        #tie_weights - use the same weights for both encoder and decoder. 
        super().__init__()
        self.drop = nn.Dropout()
        self.encoder = nn.Embedding(ntoken,ninp)
        self.rnn = nn.LSTM(ninp,nhid,nlayers,dropout=dropout)
        self.decoder = nn.Linear(nhid,ntoken)
        if tie_weights:
            self.decoder.weight = self.encoder.weight

        self.init_weights()
        self.nhid = nhid
        self.nlayers = nlayers

    def init_weights(self):
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange,initrange)
        self.decoder.bias.data.fill_(0)
        self.decoder.weight.data.uniform_(-initrange,initrange)

    def forward(self,input,hidden):
        emb = self.drop(self.encoder(input))
        output,hidden = self.rnn(emb,hidden)
        output = self.drop(output)
        s = output.size()
        decoded = self.decoder(output.view(s[0]*s[1],s[2]))
        return decoded.view(s[0],s[1],decoded.size(1)),hidden

    def init_hidden(self,bsz):
        weight = next(self.parameters()).data

        return (Variable(weight.new(self.nlayers,bsz,self.nhid).zero_()),Variable(weight.new(self.nlayers,bsz,self.nhid).zero_()))

__init__方法中,我们创建所有的层,如嵌入层、dropout、RNN 和解码器。在早期的语言模型中,通常不会在最后一层使用嵌入。使用嵌入,并且将初始嵌入与最终输出层的嵌入进行绑定,可以提高语言模型的准确性。这个概念是由 Press 和 Wolf 在 2016 年的论文Using the Output Embedding to Improve Language Models(arxiv.org/abs/1608.05859)以及 Inan 和他的合著者在 2016 年的论文Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling(arxiv.org/abs/1611.01462)中引入的。一旦我们将编码器和解码器的权重绑定在一起,我们调用init_weights方法来初始化层的权重。

forward函数将所有层连接在一起。最后的线性层将 LSTM 层的所有输出激活映射到与词汇表大小相同的嵌入中。forward函数的流程是通过嵌入层传递输入,然后传递给一个 RNN(在本例中是 LSTM),然后传递给解码器,另一个线性层。

定义训练和评估函数

模型的训练与本书中之前所有示例中看到的非常相似。我们需要进行一些重要的更改,以便训练后的模型运行得更好。让我们来看看代码及其关键部分:

criterion = nn.CrossEntropyLoss()

def trainf():
    # Turn on training mode which enables dropout.
    lstm.train()
    total_loss = 0
    start_time = time.time()
    hidden = lstm.init_hidden(batch_size)
    for i,batch in enumerate(train_iter):
        data, targets = batch.text,batch.target.view(-1)
        # Starting each batch, we detach the hidden state from how it was previously produced.
        # If we didn't, the model would try backpropagating all the way to start of the dataset.
        hidden = repackage_hidden(hidden)
        lstm.zero_grad()
        output, hidden = lstm(data, hidden)
        loss = criterion(output.view(-1, ntokens), targets)
        loss.backward()

        # `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
        torch.nn.utils.clip_grad_norm(lstm.parameters(), clip)
        for p in lstm.parameters():
            p.data.add_(-lr, p.grad.data)

        total_loss += loss.data

        if i % log_interval == 0 and i > 0:
            cur_loss = total_loss[0] / log_interval
            elapsed = time.time() - start_time
            (print('| epoch {:3d} | {:5d}/{:5d} batches | lr {:02.2f} | ms/batch {:5.2f} | loss {:5.2f} | ppl {:8.2f}'.format(epoch, i, len(train_iter), lr,elapsed * 1000 / log_interval, cur_loss, math.exp(cur_loss))))
            total_loss = 0
            start_time = time.time()

由于我们在模型中使用了 dropout,因此在训练和验证/测试数据集中需要以不同方式使用它。在模型上调用 train() 将确保在训练期间启用 dropout,在模型上调用 eval() 将确保在验证/测试期间以不同方式使用 dropout:

lstm.train()

对于 LSTM 模型,除了输入外,我们还需要传递隐藏变量。init_hidden 函数将批量大小作为输入,并返回一个隐藏变量,可以与输入一起使用。我们可以迭代训练数据并将输入数据传递给模型。由于我们处理序列数据,每次迭代都从新的隐藏状态(随机初始化)开始是没有意义的。因此,我们将使用前一次迭代的隐藏状态,在通过调用 detach 方法从图中移除它后使用。如果不调用 detach 方法,那么我们将计算一个非常长的序列的梯度,直到 GPU 内存耗尽。

然后我们将输入传递给 LSTM 模型,并使用 CrossEntropyLoss 计算损失。使用前一个隐藏状态的值是通过以下 repackage_hidden 函数实现的:

def repackage_hidden(h):
    """Wraps hidden states in new Variables, to detach them from their history."""
    if type(h) == Variable:
        return Variable(h.data)
    else:
        return tuple(repackage_hidden(v) for v in h)

RNN 及其变体,例如 LSTM 和 门控循环单元(GRU),存在一个称为 梯度爆炸 的问题。避免这个问题的一个简单技巧是裁剪梯度,以下是实现这个技巧的代码:

torch.nn.utils.clip_grad_norm(lstm.parameters(), clip)

我们通过以下代码手动调整参数值。手动实现优化器比使用预建优化器更灵活:

  for p in lstm.parameters():
      p.data.add_(-lr, p.grad.data)

我们正在遍历所有参数,并将梯度值乘以学习率相加。一旦更新了所有参数,我们记录所有统计信息,如时间、损失和困惑度。

对于验证,我们编写了类似的函数,在模型上调用 eval 方法。使用以下代码定义了 evaluate 函数:

def evaluate(data_source):
    # Turn on evaluation mode which disables dropout.
    lstm.eval()
    total_loss = 0 
    hidden = lstm.init_hidden(batch_size)
    for batch in data_source: 
        data, targets = batch.text,batch.target.view(-1)
        output, hidden = lstm(data, hidden)
        output_flat = output.view(-1, ntokens)
        total_loss += len(data) * criterion(output_flat, targets).data
        hidden = repackage_hidden(hidden)
    return total_loss[0]/(len(data_source.dataset[0].text)//batch_size)

大多数的训练逻辑和评估逻辑是相似的,除了调用 eval 并且不更新模型参数。

训练模型

我们对模型进行多次 epoch 的训练,并使用以下代码进行验证:

# Loop over epochs.
best_val_loss = None
epochs = 40

for epoch in range(1, epochs+1):
    epoch_start_time = time.time()
    trainf()
    val_loss = evaluate(valid_iter)
    print('-' * 89)
    print('| end of epoch {:3d} | time: {:5.2f}s | valid loss {:5.2f} | '
        'valid ppl {:8.2f}'.format(epoch, (time.time() - epoch_start_time),
                                   val_loss, math.exp(val_loss)))
    print('-' * 89)
    if not best_val_loss or val_loss < best_val_loss:
        best_val_loss = val_loss
    else:
        # Anneal the learning rate if no improvement has been seen in the validation dataset.
        lr /= 4.0

前面的代码训练模型 40 个 epoch,我们从一个较高的学习率 20 开始,并在验证损失饱和时进一步减少。模型运行 40 个 epoch 后得到的 ppl 分数约为 108.45。以下代码块包含了上次运行模型时的日志:

-----------------------------------------------------------------------------------------
| end of epoch  39 | time: 34.16s | valid loss  4.70 | valid ppl   110.01
-----------------------------------------------------------------------------------------
| epoch  40 |   200/ 3481 batches | lr 0.31 | ms/batch 11.47 | loss  4.77 | ppl   117.40
| epoch  40 |   400/ 3481 batches | lr 0.31 | ms/batch  9.56 | loss  4.81 | ppl   122.19
| epoch  40 |   600/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.73 | ppl   113.08
| epoch  40 |   800/ 3481 batches | lr 0.31 | ms/batch  9.48 | loss  4.65 | ppl   104.77
| epoch  40 |  1000/ 3481 batches | lr 0.31 | ms/batch  9.42 | loss  4.76 | ppl   116.42
| epoch  40 |  1200/ 3481 batches | lr 0.31 | ms/batch  9.55 | loss  4.70 | ppl   109.77
| epoch  40 |  1400/ 3481 batches | lr 0.31 | ms/batch  9.41 | loss  4.74 | ppl   114.61
| epoch  40 |  1600/ 3481 batches | lr 0.31 | ms/batch  9.47 | loss  4.77 | ppl   117.65
| epoch  40 |  1800/ 3481 batches | lr 0.31 | ms/batch  9.46 | loss  4.77 | ppl   118.42
| epoch  40 |  2000/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.76 | ppl   116.31
| epoch  40 |  2200/ 3481 batches | lr 0.31 | ms/batch  9.46 | loss  4.77 | ppl   117.52
| epoch  40 |  2400/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.74 | ppl   114.06
| epoch  40 |  2600/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.62 | ppl   101.72
| epoch  40 |  2800/ 3481 batches | lr 0.31 | ms/batch  9.44 | loss  4.69 | ppl   109.30
| epoch  40 |  3000/ 3481 batches | lr 0.31 | ms/batch  9.47 | loss  4.71 | ppl   111.51
| epoch  40 |  3200/ 3481 batches | lr 0.31 | ms/batch  9.43 | loss  4.70 | ppl   109.65
| epoch  40 |  3400/ 3481 batches | lr 0.31 | ms/batch  9.51 | loss  4.63 | ppl   102.43
val loss 4.686332647950745
-----------------------------------------------------------------------------------------
| end of epoch  40 | time: 34.50s | valid loss  4.69 | valid ppl   108.45
-----------------------------------------------------------------------------------------

在过去几个月中,研究人员开始探索先前的方法,创建一个语言模型来生成预训练的嵌入。如果您对这种方法更感兴趣,我强烈推荐您阅读 Jeremy Howard 和 Sebastian Ruder 撰写的论文Fine-tuned Language Models for Text Classification(https://arxiv.org/abs/1801.06146),他们在其中详细介绍了如何使用语言建模技术来准备特定领域的词嵌入,后者可以用于不同的 NLP 任务,如文本分类问题。

概要

在本章中,我们讨论了如何训练能够使用生成网络生成艺术风格转换、使用 GAN 和 DCGAN 生成新图像以及使用 LSTM 网络生成文本的深度学习算法。

在下一章中,我们将介绍一些现代架构,例如 ResNet 和 Inception,用于构建更好的计算机视觉模型,以及像序列到序列这样的模型,这些模型可以用于构建语言翻译和图像标题生成等任务。

第八章:现代网络架构

在最后一章中,我们探讨了如何使用深度学习算法创建艺术图像,基于现有数据集创建新图像以及生成文本。在本章中,我们将介绍驱动现代计算机视觉应用和自然语言系统的不同网络架构。本章我们将看到的一些架构包括:

  • ResNet

  • Inception

  • DenseNet

  • 编码器-解码器架构

现代网络架构

当深度学习模型学习失败时,我们通常会向模型中添加更多的层。随着层的增加,模型的准确性会提高,然后开始饱和。继续添加更多层之后,准确性会开始下降。超过一定数量的层会引入一些挑战,比如梯度消失或爆炸问题,这部分可以通过仔细初始化权重和引入中间归一化层来部分解决。现代架构,如残差网络ResNet)和 Inception,尝试通过引入不同的技术,如残差连接,来解决这些问题。

ResNet

ResNet 通过显式让网络中的层适应一个残差映射来解决这些问题,通过添加快捷连接。下图展示了 ResNet 的工作原理:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在我们看到的所有网络中,我们试图通过堆叠不同的层找到一个将输入(x)映射到其输出(H(x))的函数。但是 ResNet 的作者提出了一个修正方法;不再试图学习从 xH(x) 的基础映射,而是学习两者之间的差异,或者残差。然后,为了计算 H(x),我们可以将残差简单地加到输入上。假设残差为 F(x) = H(x) - x;与其直接学习 H(x),我们尝试学习 F(x) + x

每个 ResNet 块由一系列层组成,并通过快捷连接将块的输入添加到块的输出。加法操作是逐元素进行的,输入和输出需要具有相同的大小。如果它们大小不同,我们可以使用填充。以下代码展示了一个简单的 ResNet 块是如何工作的:

class ResNetBasicBlock(nn.Module):

    def __init__(self,in_channels,out_channels,stride):

        super().__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels,out_channels,kernel_size=3,stride=stride,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.stride = stride

    def forward(self,x):

        residual = x
        out = self.conv1(x)
        out = F.relu(self.bn1(out),inplace=True)
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        return F.relu(out)       

ResNetBasicBlock 包含一个 init 方法,用于初始化所有不同的层,如卷积层、批标准化层和 ReLU 层。forward 方法与我们之前看到的几乎相同,唯一不同的是在返回之前将输入重新添加到层的输出中。

PyTorch 的 torchvision 包提供了一个带有不同层的即用型 ResNet 模型。一些可用的不同模型包括:

  • ResNet-18

  • ResNet-34

  • ResNet-50

  • ResNet-101

  • ResNet-152

我们也可以使用这些模型中的任何一个进行迁移学习。torchvision 实例使我们能够简单地创建这些模型并使用它们。我们在书中已经做过几次,以下代码是对此的一次复习:

from torchvision.models import resnet18

resnet = resnet18(pretrained=False)

下图展示了 34 层 ResNet 模型的结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

34 层的 ResNet 模型

我们可以看到这个网络由多个 ResNet 块组成。有些团队进行了实验,尝试了深达 1000 层的模型。对于大多数实际应用场景,我个人推荐从一个较小的网络开始。这些现代网络的另一个关键优势是,它们与需要大量参数训练的模型(如 VGG)相比,需要很少的参数,因为它们避免使用全连接层。在计算机视觉领域解决问题时,另一种流行的架构是Inception。在继续研究 Inception 架构之前,让我们在Dogs vs. Cats数据集上训练一个 ResNet 模型。我们将使用我们在第五章深度学习计算机视觉中使用的数据,并基于从 ResNet 计算的特征快速训练一个模型。像往常一样,我们将按照以下步骤训练模型:

  • 创建 PyTorch 数据集

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

  • 创建 ResNet 模型

  • 提取卷积特征

  • 创建一个自定义的 PyTorch 数据集类,用于预处理的特征和加载器

  • 创建一个简单的线性模型

  • 训练和验证模型

完成后,我们将对 Inception 和 DenseNet 重复此步骤。最后,我们还将探讨集成技术,在其中结合这些强大的模型来构建一个新模型。

创建 PyTorch 数据集

我们创建一个包含所有基本变换的变换对象,并使用ImageFolder从我们在章节*5 中创建的数据目录中加载图像,深度学习计算机视觉。在以下代码中,我们创建数据集:

data_transform = transforms.Compose([
        transforms.Resize((299,299)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

# For Dogs & Cats dataset
train_dset = ImageFolder('../../chapter5/dogsandcats/train/',transform=data_transform)
val_dset = ImageFolder('../../chapter5/dogsandcats/valid/',transform=data_transform)
classes=2

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

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

我们使用 PyTorch 加载器以批次形式提供数据集中的数据,同时使用所有优势,如数据洗牌和多线程,以加快处理速度。以下代码演示了这一点:

train_loader = DataLoader(train_dset,batch_size=32,shuffle=False,num_workers=3)
val_loader = DataLoader(val_dset,batch_size=32,shuffle=False,num_workers=3)

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

创建一个 ResNet 模型

使用resnet34预训练模型的层,通过丢弃最后一个线性层创建 PyTorch 序列模型。我们将使用这个训练好的模型从我们的图像中提取特征。以下代码演示了这一点:

#Create ResNet model
my_resnet = resnet34(pretrained=True)

if is_cuda:
    my_resnet = my_resnet.cuda()

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

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

在前面的代码中,我们创建了一个在torchvision模型中可用的resnet34模型。在下面的代码中,我们挑选所有的 ResNet 层,但排除最后一层,并使用nn.Sequential创建一个新模型:

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

nn.Sequential实例允许我们快速创建一个使用一堆 PyTorch 层的模型。一旦模型创建完毕,不要忘记将requires_grad参数设置为False,这将允许 PyTorch 不维护任何用于保存梯度的空间。

提取卷积特征

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

#For training data

# Stores the labels of the train data
trn_labels = [] 

# Stores the pre convoluted features of the train data
trn_features = [] 

#Iterate through the train data and store the calculated features and the labels
for d,la in train_loader:
    o = m(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    trn_labels.extend(la)
    trn_features.extend(o.cpu().data)

#For validation data

#Iterate through the validation data and store the calculated features and the labels
val_labels = []
val_features = []
for d,la in val_loader:
    o = m(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_features.extend(o.cpu().data)

一旦我们计算了预卷积特征,我们需要创建一个能够从我们的预卷积特征中挑选数据的自定义数据集。让我们为预卷积特征创建一个自定义数据集和加载器。

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

我们已经看过如何创建 PyTorch 数据集。它应该是torch.utils.data数据集类的子类,并且应该实现__getitem__(self, index)__len__(self)方法,这些方法返回数据集中的数据长度。在下面的代码中,我们为预卷积特征实现一个自定义数据集:

class FeaturesDataset(Dataset):

    def __init__(self,featlst,labellst):
        self.featlst = featlst
        self.labellst = labellst

    def __getitem__(self,index):
        return (self.featlst[index],self.labellst[index])

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

创建自定义数据集类之后,创建预卷积特征的数据加载器就很简单了,如下面的代码所示:

#Creating dataset for train and validation
trn_feat_dset = FeaturesDataset(trn_features,trn_labels)
val_feat_dset = FeaturesDataset(val_features,val_labels)

#Creating data loader for train and validation
trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

现在我们需要创建一个简单的线性模型,它可以将预卷积特征映射到相应的类别。

创建一个简单的线性模型

我们将创建一个简单的线性模型,将预卷积特征映射到相应的类别。在这种情况下,类别的数量为两个:

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

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

fc_in_size = 8192

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

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

训练和验证模型

我们将使用相同的fit函数,该函数我们已经在《第五章》计算机视觉的深度学习中使用过。我没有在这里包含它,以节省空间。以下代码片段包含了训练模型和显示结果的功能:

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,fc,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果如下:

#Results
training loss is 0.082 and training accuracy is 22473/23000     97.71
validation loss is   0.1 and validation accuracy is 1934/2000      96.7
training loss is  0.08 and training accuracy is 22456/23000     97.63
validation loss is  0.12 and validation accuracy is 1917/2000     95.85
training loss is 0.077 and training accuracy is 22507/23000     97.86
validation loss is   0.1 and validation accuracy is 1930/2000      96.5
training loss is 0.075 and training accuracy is 22518/23000      97.9
validation loss is 0.096 and validation accuracy is 1938/2000      96.9
training loss is 0.073 and training accuracy is 22539/23000      98.0
validation loss is   0.1 and validation accuracy is 1936/2000      96.8
training loss is 0.073 and training accuracy is 22542/23000     98.01
validation loss is 0.089 and validation accuracy is 1942/2000      97.1
training loss is 0.071 and training accuracy is 22545/23000     98.02
validation loss is  0.09 and validation accuracy is 1941/2000     97.05
training loss is 0.068 and training accuracy is 22591/23000     98.22
validation loss is 0.092 and validation accuracy is 1934/2000      96.7
training loss is 0.067 and training accuracy is 22573/23000     98.14
validation loss is 0.085 and validation accuracy is 1942/2000      97.1

正如我们从结果中看到的那样,模型达到了 98%的训练精度和 97%的验证精度。让我们了解另一种现代架构及其如何用于计算预卷积特征并用它们来训练模型。

Inception

在我们看到的大多数计算机视觉模型的深度学习算法中,我们会选择使用卷积层,其滤波器大小为 1 x 1、3 x 3、5 x 5、7 x 7 或映射池化层。Inception 模块结合了不同滤波器大小的卷积,并将所有输出串联在一起。下图使 Inception 模型更清晰:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

在这个 Inception 块图像中,应用了不同尺寸的卷积到输入上,并将所有这些层的输出串联起来。这是一个 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 块:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

现在,让我们看一个 PyTorch 示例,展示前述 Inception 块的外观:

class BasicConv2d(nn.Module):

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

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

class InceptionBasicBlock(nn.Module):

    def __init__(self, in_channels, pool_features):
        super().__init__()
        self.branch1x1 = BasicConv2d(in_channels, 64, kernel_size=1)

        self.branch5x5_1 = BasicConv2d(in_channels, 48, kernel_size=1)
        self.branch5x5_2 = BasicConv2d(48, 64, kernel_size=5, padding=2)

        self.branch3x3dbl_1 = BasicConv2d(in_channels, 64, kernel_size=1)
        self.branch3x3dbl_2 = BasicConv2d(64, 96, kernel_size=3, padding=1)

        self.branch_pool = BasicConv2d(in_channels, pool_features, kernel_size=1)

    def forward(self, x):
        branch1x1 = self.branch1x1(x)

        branch5x5 = self.branch5x5_1(x)
        branch5x5 = self.branch5x5_2(branch5x5)

        branch3x3dbl = self.branch3x3dbl_1(x)
        branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)

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

        outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
        return torch.cat(outputs, 1)

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

InceptionBasicBlock 实现了我们在第二个 Inception 图中看到的内容。让我们逐个查看每个较小的片段,并尝试理解它们的实现方式:

branch1x1 = self.branch1x1(x)

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

branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)

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

branch3x3dbl = self.branch3x3dbl_1(x)
branch3x3dbl = self.branch3x3dbl_2(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 包含一个 Inception 网络,可以像我们使用 ResNet 网络一样使用。对初始 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

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

从 Inception 模型中提取卷积特征并不像 ResNet 那样直接,因此我们将使用 register_forward_hook 来提取激活值。

使用 register_forward_hook 提取卷积特征

我们将使用与计算样式迁移激活值相同的技术。以下是 LayerActivations 类的一些小修改,因为我们只关注提取特定层的输出:

class LayerActivations():
    features=[]

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

    def hook_fn(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.
trn_features = LayerActivations(my_inception.Mixed_7c)
trn_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:
    _ = my_inception(Variable(da.cuda()))
    trn_labels.extend(la)
trn_features.remove()

# Repeat the same process for validation dataset .

val_features = LayerActivations(my_inception.Mixed_7c)
val_labels = []
for da,la in val_loader:
    _ = my_inception(Variable(da.cuda()))
    val_labels.extend(la)
val_features.remove()

让我们创建新的数据集和加载器,以便获取新的卷积特征。

为卷积特征创建新数据集

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

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

trn_feat_dset = FeaturesDataset(trn_features.features,trn_labels)
val_feat_dset = FeaturesDataset(val_features.features,val_labels)

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

trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

让我们创建一个新的模型来在预卷积特征上进行训练。

创建一个完全连接的模型

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

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size,training=True):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

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

# 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,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

#Results

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 将其提升到一个新水平,通过引入从每一层到所有后续层的连接,即一个层可以接收来自前几层的所有特征图。符号上看,它可能是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图描述了一个五层密集块的结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:https://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, num_layers, num_input_features, bn_size, growth_rate, drop_rate):
        super(_DenseBlock, self).__init__()
        for i in range(num_layers):
            layer = _DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
            self.add_module('denselayer%d' % (i + 1), layer)

DenseBlock 是一个顺序模块,我们按顺序添加层。根据块中的层数(num_layers),我们添加相应数量的 _DenseLayer 对象以及一个名称。所有的魔法都发生在 DenseLayer 内部。让我们看看 DenseLayer 内部发生了什么。

DenseLayer

学习一个特定网络如何工作的一个好方法是查看源代码。PyTorch 实现非常清晰,大多数情况下易于阅读。让我们来看一下 DenseLayer 的实现:

class _DenseLayer(nn.Sequential):
    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):
        super(_DenseLayer, self).__init__()
        self.add_module('norm.1', nn.BatchNorm2d(num_input_features)),
        self.add_module('relu.1', nn.ReLU(inplace=True)),
        self.add_module('conv.1', nn.Conv2d(num_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 中的继承不熟悉,那么前面的代码可能看起来不直观。_DenseLayernn.Sequential 的子类;让我们看看每个方法内部发生了什么。

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

魔法发生在forward方法中。我们将输入传递给super类的forward方法,即nn.Sequential的方法。让我们看看顺序类的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 有两个模块:特征(包含密集块)和分类器(全连接块)。由于我们正在使用 DenseNet 作为图像特征提取器,我们只会使用特征模块:

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

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

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

提取 DenseNet 特征

这与我们为 Inception 所做的相似,只是我们没有使用register_forward_hook来提取特征。以下代码展示了如何提取 DenseNet 特征:

#For training data
trn_labels = []
trn_features = []

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

#For validation data
val_labels = []
val_features = []

#Code to store densenet features for validation dataset. 
for d,la in val_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_features.extend(o.cpu().data)

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

创建一个数据集和加载器

我们将使用我们为 ResNet 创建的FeaturesDataset类,并在以下代码中使用它来为trainvalidation数据集创建数据加载器:

# Create dataset for train and validation convolution features
trn_feat_dset = FeaturesDataset(trn_features,trn_labels)
val_feat_dset = FeaturesDataset(val_features,val_labels)

# Create data loaders for batching the train and validation datasets
trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True,drop_last=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

是时候创建模型并训练了。

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

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

class FullyConnectedModel(nn.Module):

    def __init__(self,in_size,out_size):
        super().__init__()
        self.fc = nn.Linear(in_size,out_size)

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

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

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

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,fc,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,fc,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果是:

# Results

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%的验证精度。由于您创建的validation数据集可能包含不同的图像,因此您的结果可能会有所不同。

DenseNet 的一些优点包括:

  • 它大大减少了所需的参数数量

  • 它缓解了梯度消失问题

  • 它鼓励特征重用

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

模型集成

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

集成模型的架构将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该图显示了我们在集成模型中要做的事情,可以总结为以下步骤:

  1. 创建三个模型

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

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

  4. 创建类似于前面图中架构的模型

  5. 训练和验证模型

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

创建模型

让我们按以下代码创建所有三个所需的模型:

#Create ResNet model
my_resnet = resnet34(pretrained=True)

if is_cuda:
    my_resnet = my_resnet.cuda()

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

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

#Create inception model

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

#Create densenet model

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

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

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

提取图像特征

这里,我们将本章中各算法的各自逻辑组合起来:

### For ResNet

trn_labels = []
trn_resnet_features = []
for d,la in train_loader:
    o = my_resnet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    trn_labels.extend(la)
    trn_resnet_features.extend(o.cpu().data)
val_labels = []
val_resnet_features = []
for d,la in val_loader:
    o = my_resnet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_labels.extend(la)
    val_resnet_features.extend(o.cpu().data)

### For Inception

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

trn_inception_features.remove()

val_inception_features = LayerActivations(my_inception.Mixed_7c)
for da,la in val_loader:
    _ = my_inception(Variable(da.cuda()))

val_inception_features.remove()

### For DenseNet

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

    trn_densenet_features.extend(o.cpu().data)

val_densenet_features = []
for d,la in val_loader:
    o = my_densenet(Variable(d.cuda()))
    o = o.view(o.size(0),-1)
    val_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)

trn_feat_dset = FeaturesDataset(trn_resnet_features,trn_inception_features.features,trn_densenet_features,trn_labels)
val_feat_dset = FeaturesDataset(val_resnet_features,val_inception_features.features,val_densenet_features,val_labels)

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

trn_feat_loader = DataLoader(trn_feat_dset,batch_size=64,shuffle=True)
val_feat_loader = DataLoader(val_feat_dset,batch_size=64)

创建一个集成模型

我们需要创建一个与之前展示的架构图类似的模型。以下代码实现了这一点:

class EnsembleModel(nn.Module):

    def __init__(self,out_size,training=True):
        super().__init__()
        self.fc1 = nn.Linear(8192,512)
        self.fc2 = nn.Linear(131072,512)
        self.fc3 = nn.Linear(82944,512)
        self.fc4 = nn.Linear(512,out_size)

    def forward(self,inp1,inp2,inp3):
        out1 = self.fc1(F.dropout(inp1,training=self.training))
        out2 = self.fc2(F.dropout(inp2,training=self.training))
        out3 = self.fc3(F.dropout(inp3,training=self.training))
        out = out1 + out2 + out3
        out = self.fc4(F.dropout(out,training=self.training))
        return out

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 = [],[]
val_losses , val_accuracy = [],[]
for epoch in range(1,10):
    epoch_loss, epoch_accuracy = fit(epoch,em,trn_feat_loader,phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch,em,val_feat_loader,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

上述代码的结果如下:

#Results 

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,它学习解码编码器生成的向量,并生成新的数据序列。下图展示了用于图像字幕系统的编码器-解码器架构的外观:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像字幕系统的编码器-解码器架构

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

让我们更详细地看一看图像字幕系统中编码器和解码器架构的内部情况。

编码器

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

解码器

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

总结

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

在下一章中,我们将总结在书籍学习旅程中取得的成就,同时讨论你接下来可以从哪里继续前行。我们将探讨关于 PyTorch 的大量资源以及一些正在使用 PyTorch 进行研究的酷炫深度学习项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值