生成网络#

循环神经网络(RNNs)及其门控单元变体(如长短期记忆单元 LSTMs 和门控循环单元 GRUs)提供了一种语言建模的机制,即它们可以学习单词的排列顺序,并预测序列中下一个单词。这使得我们可以将 RNN 用于生成任务,例如普通文本生成、机器翻译,甚至图像描述生成。

在我们上一单元讨论的 RNN 架构中,每个 RNN 单元会生成下一个隐藏状态作为输出。然而,我们也可以为每个循环单元添加另一个输出,这样就可以输出一个序列(长度与原始序列相等)。此外,我们还可以使用不在每一步接受输入的 RNN 单元,而是仅接受一个初始状态向量,然后生成一系列输出。

在本笔记中,我们将专注于帮助我们生成文本的简单生成模型。为了简化起见,让我们构建一个字符级网络,逐字母生成文本。在训练过程中,我们需要获取一些文本语料库,并将其拆分为字母序列。

In [1]:
import torch
import torchtext
import numpy as np
from torchnlp import *
train_dataset,test_dataset,classes,vocab = load_dataset()
Loading dataset...
Building vocab...

构建字符词汇表#

为了构建基于字符的生成网络,我们需要将文本拆分为单个字符,而不是单词。这可以通过定义一个不同的分词器来实现:

In [2]:
def char_tokenizer(words):
    return list(words) #[word for word in words]

counter = collections.Counter()
for (label, line) in train_dataset:
    counter.update(char_tokenizer(line))
vocab = torchtext.vocab.vocab(counter)

vocab_size = len(vocab)
print(f"Vocabulary size = {vocab_size}")
print(f"Encoding of 'a' is {vocab.get_stoi()['a']}")
print(f"Character with code 13 is {vocab.get_itos()[13]}")
Vocabulary size = 82
Encoding of 'a' is 1
Character with code 13 is c

让我们看看如何对数据集中的文本进行编码的示例:

In [3]:
def enc(x):
    return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))

enc(train_dataset[0][1])
tensor([ 0,  1,  2,  2,  3,  4,  5,  6,  3,  7,  8,  1,  9, 10,  3, 11,  2,  1,
        12,  3,  7,  1, 13, 14,  3, 15, 16,  5, 17,  3,  5, 18,  8,  3,  7,  2,
         1, 13, 14,  3, 19, 20,  8, 21,  5,  8,  9, 10, 22,  3, 20,  8, 21,  5,
         8,  9, 10,  3, 23,  3,  4, 18, 17,  9,  5, 23, 10,  8,  2,  2,  8,  9,
        10, 24,  3,  0,  1,  2,  2,  3,  4,  5,  9,  8,  8,  5, 25, 10,  3, 26,
        12, 27, 16, 26,  2, 27, 16, 28, 29, 30,  1, 16, 26,  3, 17, 31,  3, 21,
         2,  5,  9,  1, 23, 13, 32, 16, 27, 13, 10, 24,  3,  1,  9,  8,  3, 10,
         8,  8, 27, 16, 28,  3, 28,  9,  8,  8, 16,  3,  1, 28,  1, 27, 16,  6])

训练生成式 RNN#

我们将通过以下方式训练 RNN 来生成文本。在每一步中,我们会取一个长度为 nchars 的字符序列,并让网络为每个输入字符生成下一个输出字符:

显示 RNN 生成单词 'HELLO' 示例的图片。

根据实际场景,我们可能还需要包含一些特殊字符,比如 序列结束符 <eos>。在我们的例子中,我们只是想训练网络进行无尽的文本生成,因此我们会将每个序列的大小固定为 nchars 个标记。因此,每个训练样本将由 nchars 个输入和 nchars 个输出组成(输出是输入序列向左偏移一个符号)。一个小批量(minibatch)将由若干这样的序列组成。

我们生成小批量的方式是取每条长度为 l 的新闻文本,并从中生成所有可能的输入-输出组合(这样的组合会有 l-nchars 个)。这些组合将形成一个小批量,而每次训练步骤中小批量的大小会有所不同。

In [4]:
nchars = 100

def get_batch(s,nchars=nchars):
    ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)
    outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)
    for i in range(len(s)-nchars):
        ins[i] = enc(s[i:i+nchars])
        outs[i] = enc(s[i+1:i+nchars+1])
    return ins,outs

get_batch(train_dataset[0][1])
(tensor([[ 0,  1,  2,  ..., 28, 29, 30],
         [ 1,  2,  2,  ..., 29, 30,  1],
         [ 2,  2,  3,  ..., 30,  1, 16],
         ...,
         [20,  8, 21,  ...,  1, 28,  1],
         [ 8, 21,  5,  ..., 28,  1, 27],
         [21,  5,  8,  ...,  1, 27, 16]]),
 tensor([[ 1,  2,  2,  ..., 29, 30,  1],
         [ 2,  2,  3,  ..., 30,  1, 16],
         [ 2,  3,  4,  ...,  1, 16, 26],
         ...,
         [ 8, 21,  5,  ..., 28,  1, 27],
         [21,  5,  8,  ...,  1, 27, 16],
         [ 5,  8,  9,  ..., 27, 16,  6]]))

现在我们来定义生成网络。它可以基于我们在上一单元中讨论过的任何循环单元(简单循环单元、LSTM 或 GRU)。在我们的示例中,我们将使用 LSTM。

由于网络以字符作为输入,并且词汇表的大小相对较小,我们不需要嵌入层,直接使用独热编码的输入即可传递给 LSTM 单元。然而,由于我们传递的是字符编号作为输入,因此在传递给 LSTM 之前需要对它们进行独热编码。这可以通过在 forward 过程中调用 one_hot 函数来完成。输出编码器将是一个线性层,用于将隐藏状态转换为独热编码的输出。

In [5]:
class LSTMGenerator(torch.nn.Module):
    def __init__(self, vocab_size, hidden_dim):
        super().__init__()
        self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)
        self.fc = torch.nn.Linear(hidden_dim, vocab_size)

    def forward(self, x, s=None):
        x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)
        x,s = self.rnn(x,s)
        return self.fc(x),s

在训练过程中,我们希望能够对生成的文本进行采样。为此,我们将定义一个 generate 函数,该函数会从初始字符串 start 开始,生成长度为 size 的输出字符串。

其工作原理如下:首先,我们将整个初始字符串 start 传递给网络,获取输出状态 s 和下一个预测字符 out。由于 out 是独热编码的,我们通过 argmax 获取字符在词汇表中的索引 nc,然后使用 itos 找到实际字符,并将其添加到结果字符列表 chars 中。生成一个字符的这个过程会重复执行 size 次,以生成所需数量的字符。

In [8]:
def generate(net,size=100,start='today '):
        chars = list(start)
        out, s = net(enc(chars).view(1,-1).to(device))
        for i in range(size):
            nc = torch.argmax(out[0][-1])
            chars.append(vocab.get_itos()[nc])
            out, s = net(nc.view(1,-1),s)
        return ''.join(chars)

现在开始训练吧!训练循环与我们之前的所有示例几乎相同,但这次我们每隔1000个epoch打印一次生成的文本样本,而不是打印准确率。

需要特别注意的是我们计算损失的方式。我们需要根据独热编码的输出 out 和预期的文本 text_out(字符索引列表)来计算损失。幸运的是,cross_entropy 函数的第一个参数需要未归一化的网络输出,第二个参数需要类别编号,这正好符合我们的需求。它还会自动对小批量的大小进行平均。

我们还通过 samples_to_train 限制训练样本数量,以避免等待时间过长。我们鼓励你进行实验,尝试更长时间的训练,可能是多个epoch(在这种情况下,你需要在这段代码外再创建一个循环)。

In [9]:
net = LSTMGenerator(vocab_size,64).to(device)

samples_to_train = 10000
optimizer = torch.optim.Adam(net.parameters(),0.01)
loss_fn = torch.nn.CrossEntropyLoss()
net.train()
for i,x in enumerate(train_dataset):
    # x[0] is class label, x[1] is text
    if len(x[1])-nchars<10:
        continue
    samples_to_train-=1
    if not samples_to_train: break
    text_in, text_out = get_batch(x[1])
    optimizer.zero_grad()
    out,s = net(text_in)
    loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)
    loss.backward()
    optimizer.step()
    if i%1000==0:
        print(f"Current loss = {loss.item()}")
        print(generate(net))
Current loss = 4.398899078369141
today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s
Current loss = 2.161320447921753
today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t
Current loss = 1.6722588539123535
today and the court to the could to the could to the could to the could to the could to the could to the c
Current loss = 2.423795223236084
today and a second to the conternation of the conternation of the conternation of the conternation of the 
Current loss = 1.702607274055481
today and the company to the company to the company to the company to the company to the company to the co
Current loss = 1.692358136177063
today and the company to the company to the company to the company to the company to the company to the co
Current loss = 1.9722288846969604
today and the control the control the control the control the control the control the control the control 
Current loss = 1.8705692291259766
today and the second to the second to the second to the second to the second to the second to the second t
Current loss = 1.7626899480819702
today and a security and a security and a security and a security and a security and a security and a secu
Current loss = 1.5574463605880737
today and the company and the company and the company and the company and the company and the company and 
Current loss = 1.5620026588439941
today and the be that the be the be that the be the be that the be the be that the be the be that the be t

这个示例已经生成了一些相当不错的文本,但仍有几个方面可以进一步改进:

  • 更好的小批量生成。我们在训练数据准备时,是从一个样本中生成一个小批量。这种方法并不理想,因为小批量的大小各不相同,有些甚至无法生成,因为文本长度小于 nchars。此外,较小的小批量无法充分利用 GPU 的性能。更明智的做法是从所有样本中获取一个较大的文本块,然后生成所有输入-输出对,打乱顺序,并生成大小相等的小批量。

  • 多层 LSTM。尝试使用 2 或 3 层的 LSTM 单元是有意义的。正如我们在前一个单元中提到的,每一层 LSTM 都会从文本中提取特定的模式。在字符级生成器的情况下,我们可以预期较低的 LSTM 层负责提取音节,而较高的层负责提取单词和单词组合。这可以通过向 LSTM 构造函数传递层数参数来简单实现。

  • 你还可以尝试使用 GRU 单元,看看哪种表现更好,以及尝试 不同的隐藏层大小。隐藏层过大可能导致过拟合(例如,网络会学习到精确的文本),而隐藏层过小可能无法生成良好的结果。

软文本生成与温度#

在之前定义的 generate 方法中,我们总是选择概率最高的字符作为生成文本中的下一个字符。这导致生成的文本经常在相同的字符序列之间“循环”,如下例所示:

today of the second the company and a second the company ...

然而,如果我们查看下一个字符的概率分布,可能会发现几个最高概率之间的差异并不大,例如,一个字符的概率可能是 0.2,而另一个是 0.19,等等。例如,当寻找序列 'play' 的下一个字符时,下一个字符可能同样可能是空格,或者是 e(如单词 player 中的情况)。

这让我们得出一个结论:选择概率最高的字符并不总是“公平”的,因为选择第二高的字符也可能生成有意义的文本。更明智的做法是从网络输出给出的概率分布中采样字符。

这种采样可以通过实现所谓多项分布multinomial 函数来完成。下面定义了一个实现这种文本生成的函数:

In [10]:
def generate_soft(net,size=100,start='today ',temperature=1.0):
        chars = list(start)
        out, s = net(enc(chars).view(1,-1).to(device))
        for i in range(size):
            #nc = torch.argmax(out[0][-1])
            out_dist = out[0][-1].div(temperature).exp()
            nc = torch.multinomial(out_dist,1)[0]
            chars.append(vocab.get_itos()[nc])
            out, s = net(nc.view(1,-1),s)
        return ''.join(chars)
    
for i in [0.3,0.8,1.0,1.3,1.8]:
    print(f"--- Temperature = {i}\n{generate_soft(net,size=300,start='Today ',temperature=i)}\n")
--- Temperature = 0.3
Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin

--- Temperature = 0.8
Today he oniis its first to Aus bomblaties the marmation a to manan  boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith

--- Temperature = 1.0
Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders

--- Temperature = 1.3
Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs 

--- Temperature = 1.8
Today plary, P.slan chly\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\eexciaishes.conBabntusmor I

我们引入了一个名为温度的参数,用于指示我们应多大程度上坚持最高概率。如果温度为1.0,我们进行公平的多项式采样,而当温度趋于无穷大时——所有概率变得相等,我们随机选择下一个字符。在下面的示例中,我们可以观察到,当我们将温度提高得过高时,文本变得毫无意义,而当温度接近0时,它类似于“循环”的硬生成文本。


免责声明
本文档使用AI翻译服务 Co-op Translator 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。我们对因使用此翻译而引起的任何误解或误读不承担责任。