【DL】第8章 序列到序列的映射

  🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

在本章中,我们将研究使用序列到序列网络来学习文本片段之间的转换。这是一种相对较新的技术,具有诱人的可能性。谷歌声称已经使用这种技术对其谷歌翻译产品进行了巨大改进。此外,它还开源了一个可以完全基于平行文本学习语言翻译的版本。

我们一开始不会走那么远。相反,我们将从一个学习英语复数规则的简单模型开始。之后,我们将从 Project Gutenberg 的 19 世纪小说中提取对话,并在这些小说上训练一个聊天机器人。对于最后一个项目,我们将不得不放弃在笔记本中运行 Keras 的安全性,并将使用 Google 的开源 seq2seq 工具包。

以下笔记本包含与本章相关的代码:

08.1 Sequence to sequence mapping
08.2 Import Gutenberg
08.3 Subword tokenizing

8.1 训练一个简单的序列到序列模型

问题

您如何训练模型以对转换进行逆向工程?

解决方案

使用序列到序列映射器。

第 5 章中,我们看到了如何使用循环网络来“学习”序列的规则。该模型学习如何最好地表示一个序列,以便它可以预测下一个元素将是什么。序列到序列的映射建立在此之上,但现在模型学会了根据第一个序列来预测不同的序列。

我们可以用它来学习各种变换。让我们考虑将单数名词转换为英语中的复数名词。乍一看,这似乎只是在一个单词上附加一个s的问题,但当你仔细观察时,会发现规则实际上要复杂得多。

该模型与我们在第 5 章中使用的模型非常相似,但现在不仅仅是输入是一个序列,还有输出。这是使用RepeatVector层实现的,它允许我们从输入映射到输出向量:

def create_seq2seq(num_nodes, num_layers):
    question = Input(shape=(max_question_len, len(chars),
                     name='question'))
    repeat = RepeatVector(max_expected_len)(question)
    prev = input
    for _ in range(num_layers)::
        lstm = LSTM(num_nodes, return_sequences=True,
                    name='lstm_layer_%d' % (i + 1))(prev)
        prev = lstm
    dense = TimeDistributed(Dense(num_chars, name='dense',
                            activation='softmax'))(prev)
    model = Model(inputs=[input], outputs=[dense])
    optimizer = RMSprop(lr=0.01)
    model.compile(loss='categorical_crossentropy',
                  optimizer=optimizer,
                  metrics=['accuracy'])
    return model

数据的预处理和以前一样。我们从文件data/plurals.txt中读取数据并将其矢量化。要考虑的一个技巧是是否反转输入中的字符串。如果输入是相反的,那么生成输出就像展开处理一样,这可能更容易。

该模型需要相当长的时间才能达到 99% 左右的精度。然而,大部分时间都花在学习复制单词的单数和复数形式共享的前缀上。事实上,当我们检查模型的性能时,它已经达到了 99% 以上的精度,我们看到大部分错误仍然在那个区域。

讨论

序列到序列模型是强大的工具,只要有足够的资源,几乎可以学习任何转换。学习英语从单数到复数的规则只是一个简单的例子。这些模型是领先科技公司提供的最先进机器翻译解决方案的基本要素。

像本节中的模型这样更简单的模型可以学习如何在罗马符号中添加数字或学习在书面英语和语音英语之间进行翻译,这是构建文本到语音系统时有用的第一步。

在接下来的几个食谱中,我们将看到如何使用这种技术来训练基于从 19 世纪小说中提取的对话的聊天机器人。

8.2 从文本中提取对话

问题

您如何获得大量的对话语料库?

解决方案

解析 Project Gutenberg 提供的一些文本并提取所有对话。

让我们从从 Project Gutenberg 下载一套书籍开始。我们可以下载所有这些,但在这里我们将关注作者出生于 1835 年之后的作品。这使对话保持了一些现代性。data/books.json文档包含相关参考:

with open('data/gutenberg_index.json') as fin:
    authors = json.load(fin)
recent = [x for x in authors
          if 'birthdate' in x and x['birthdate'] > 1830]
[(x['name'], x['birthdate'], x['english_books']) for x in recent[:5]]
[('Twain, Mark', 1835, 210),
 ('Ebers, Georg', 1837, 164),
 ('Parker, Gilbert', 1862, 135),
 ('Fenn, George Manville', 1831, 128),
 ('Jacobs, W. W. (William Wymark)', 1863, 112)]

这些书大多以一致的 ASCII 格式排列。段落由双换行符分隔,对话几乎总是使用双引号。一小部分书籍也使用单引号,但我们将忽略这些,因为单引号也出现在文本的其他地方。我们假设只要引号之外的文本长度少于 100 个字符,对话就会继续(如“嗨,”他说,“你好吗?”):

def extract_conversations(text, quote='"'):
    paragraphs = PARAGRAPH_SPLIT_RE.split(text.strip())
    conversations = [['']]
    for paragraph in paragraphs:
        chunks = paragraph.replace('\n', ' ').split(quote)
        for i in range((len(chunks) + 1) // 2):
            if (len(chunks[i * 2]) > 100
                or len(chunks) == 1) and conversations[-1] != ['']:
                if conversations[-1][-1] == '':
                    del conversations[-1][-1]
                conversations.append([''])
            if i * 2 + 1 < len(chunks):
                chunk = chunks[i * 2 + 1]
                if chunk:
                    if conversations[-1][-1]:
                        if chunk[0] >= 'A' and chunk[0] <= 'Z':
                            if conversations[-1][-1].endswith(','):
                                conversations[-1][-1] = \
                                     conversations[-1][-1][:-1]
                            conversations[-1][-1] += '.'
                        conversations[-1][-1] += ' '
                    conversations[-1][-1] += chunk
        if conversations[-1][-1]:
            conversations[-1].append('')

    return [x for x in conversations if len(x) > 1]

对前 1000 位作者进行处理,为我们提供了一组很好的对话数据:

for author in recent[:1000]:
    for book in author['books']:
        txt = strip_headers(load_etext(int(book[0]))).strip()
        conversations += extract_conversations(txt)

这需要一些时间,所以我们最好将结果保存到文件中:

with open('gutenberg.txt', 'w') as fout:
    for conv in conversations:
        fout.write('\n'.join(conv) + '\n\n')

讨论

正如我们在第 5 章中看到的,古腾堡计划是一个很好的可自由使用文本的来源,只要我们不介意它们有点旧,因为它们必须不受版权保护。

该项目是在对布局和插图的关注发挥作用之前开始的,因此所有文档都是以纯 ASCII 生成的。虽然这不是实际书籍的最佳格式,但它使解析相对容易。段落由双换行符分隔,并且没有使用智能引号或任何标记。

8.3 处理开放词汇表

问题

如何仅使用固定数量的标记对文本进行完全标记?

解决方案

使用子词单元进行标记。

在上一章中,我们只是跳过了前 50,000 个单词的词汇表中没有的单词。通过子词单元标记​​,我们将不经常出现的词分解成出现的子单元。我们继续这样做,直到所有单词和子单元都适合我们的固定大小的词汇表。

例如,如果我们有单词working和working ,我们可以将它们分解work--ed-ing。这三个标记很可能与我们词汇表中的其他标记重叠,因此这可能会减少我们整个词汇表的大小。使用的算法很简单。我们将所有标记拆分为它们各自的字母。在这一点上,每个字母都是一个子词标记,大概我们的标记数量少于我们的最大数量。然后我们找出哪对子词标记出现在我们的标记中最多。在英语中,通常是 ( t , h)。然后我们加入这些子词标记。这通常会将子词标记的数量增加一个,除非我们对中的一个项目现在已用尽。我们一直这样做,直到我们拥有所需数量的子词和词标记。

尽管代码并不复杂,但使用该算法的开源版本是有意义的。标记化是一个三步过程。

第一步是标记我们的语料库。默认分词器只是拆分文本,这意味着它保留所有标点符号,通常附加到前一个单词。我们想要更先进的东西。我们希望去掉除问号之外的所有标点符号。我们还将所有内容都转换为小写并用空格替换下划线:

RE_TOKEN = re.compile('(\w+|\?)', re.UNICODE)
token_counter = Counter()
with open('gutenberg.txt') as fin:
    for line in fin:
        line = line.lower().replace('_', ' ')
        token_counter.update(RE_TOKEN.findall(line))
with open('gutenberg.tok', 'w') as fout:
    for token, count in token_counter.items():
        fout.write('%s\t%d\n' % (token, count))

现在我们可以学习子词标记:

./learn_bpe.py -s 25000 < gutenberg.tok > gutenberg.bpe

然后我们可以将它们应用于任何文本:

./apply_bpe.py -c gutenberg.bpe < some_text.txt > some_text.bpe.txt

生成的some_text.bpe.txt看起来像我们的原始语料库,除了稀有标记被分解并以@@结尾表示继续。

讨论

将文本标记为单词是减小文档大小的有效方法。正如我们在第 7 章中看到的,它还允许我们通过加载预训练的词嵌入来启动我们的学习。但是有一个缺点:较大的文本包含太多不同的单词,我们不能指望将它们全部覆盖。一种解决方案是跳过不在我们词汇表中的单词,或者用固定的UNKNOWN标记替换它们。这对于情感分析来说并不算太糟糕,但是对于我们想要生成输出文本的任务来说,它就相当不令人满意了。在这种情况下,子词单元标记​​是一个很好的解决方案。

最近获得一些关注的另一个选择是训练一个字符级模型来为不在词汇表中的单词生成嵌入。

8.4 训练一个 seq2seq 聊天机器人

问题

你想训练一个深度学习模型来重现对话语料库的特征。

解决方案

使用谷歌的 seq2seq 框架。

8.1节中的模型能够学习序列之间的关系——甚至是相当复杂的序列。然而,序列到序列模型很难调整性能。2017 年初,谷歌发布了 seq2seq,这是一个专门为此类应用开发的库,可直接在 TensorFlow 上运行。它让我们专注于模型超参数,而不是代码的细节。

seq2seq 框架希望将其输入分成训练集、评估集和开发集。每个集合都应该包含一个源文件和一个目标文件,其中匹配的行定义了模型的输入和输出。在我们的例子中,源应该包含对话的提示,目标是答案。然后,该模型将尝试学习如何从提示转换为回答,从而有效地学习如何进行对话。

第一步是将我们的对话分成(源,目标)对。对于对话中的每一对连续的行,我们提取第一句和最后一句作为源和目标:

RE_TOKEN = re.compile('(\w+|\?)', re.UNICODE)
def tokenize(st):
    st = st.lower().replace('_', ' ')
    return ' '.join(RE_TOKEN.findall(st))

pairs = []
prev = None
with open('data/gutenberg.txt') as fin:
    for line in fin:
        line = line.strip()
        if line:
            sentences = nltk.sent_tokenize(line)
            if prev:
                pairs.append((prev, tokenize(sentences[0])))
            prev = tokenize(sentences[-1])
        else:
            prev = None

现在让我们打乱我们的配对并将它们分成我们的三组,其中devtest集分别代表我们数据的 5%:

random.shuffle(pairs)
ss = len(pairs) // 20

data = {'dev': pairs[:ss],
        'test': pairs[ss:ss * 2],
        'train': pairs[ss * 2:]}

接下来我们需要解压pairs并将它们放入正确的目录结构中:

for tag, pairs2 in data.items():
    path = 'seq2seq/%s' % tag
    if not os.path.isdir(path):
        os.makedirs(path)
    with open(path + '/sources.txt', 'wt') as sources:
        with open(path + '/targets.txt', 'wt') as targets:
            for source, target in pairs2:
                sources.write(source + '\n')
                targets.write(target + '\n')

是时候训练网络了。克隆 seq2seq 存储库并安装依赖项。您可能希望单独执行此操作virtualenv

git clone https://github.com/google/seq2seq.git
cd seq2seq
pip install-e。

现在让我们设置一个指向我们放在一起的数据的环境变量:

Export SEQ2SEQROOT=/path/to/data/seq2seq

seq2seq 库包含许多配置文件,我们可以在example_configs目录中混合搭配。在这种情况下,我们要训练一个大型模型:

python -m bin.train \                                                                                                               --config_paths="                                                                                                                                                ./example_configs/nmt_large.yml,
      ./example_configs/train_seq2seq.yml" \
  --model_params "
      vocab_source: $SEQ2SEQROOT/gutenberg.tok
      vocab_target: $SEQ2SEQROOT/gutenberg.tok" \
  --input_pipeline_train "
    class: ParallelTextInputPipeline
    params:
      source_files:
        - $SEQ2SEQROOT/train/sources.txt
      target_files:
        - $SEQ2SEQROOT/train/targets.txt" \
  --input_pipeline_dev "
    class: ParallelTextInputPipeline
    params:
       source_files:
        - $SEQ2SEQROOT/dev/sources.txt
       target_files:
        - $SEQ2SEQROOT/dev/targets.txt" \
  --batch_size 1024  --eval_every_n_steps 5000 \
  --train_steps 5000000 \
  --output_dir $SEQ2SEQROOT/model_large

不幸的是,即使在具有强大 GPU 的系统上,我们也需要几天和几天的时间才能获得一些不错的结果。笔记本中的zoo文件夹包含一个预训练模型,如果你等不及了。

该库不提供交互式运行模型的方法。在第 16 章中,我们将研究如何做到这一点,但现在我们可以通过将测试问题添加到文件(例如,/tmp/test_questions.txt)并运行:

python -m bin.infer \
  --tasks "
    - class: DecodeText" \
  --model_dir $SEQ2SEQROOT/model_large \
  --input_pipeline "
    class: ParallelTextInputPipeline
    params:
      source_files:
        - '/tmp/test_questions.txt'"

一个简单的对话有效:

> hi
hi
> what is your name ?
sam barker
> how do you feel ?
Fine
> good night
good night

对于更复杂的句子,它有点命中或错过。

讨论

seq2seq 模型的主要用例似乎是自动翻译,尽管它在为图像添加字幕和总结文本方面也很有效。该文档包含有关如何训练模型的教程,该模型可在数周或数月内学习体面的英德翻译,具体取决于您的硬件。谷歌声称,将序列到序列模型作为其机器翻译工作的核心已经大大提高了质量。

考虑序列到序列映射的一种有趣方式是将其视为嵌入过程。对于翻译,源句子和目标句子都被投影到多维空间中,并且模型学习一个投影,使得具有相同含义的句子最终围绕该空间中的同一点结束。这导致了“零样本”翻译的有趣可能性;如果一个模型学习在芬兰语和英语之间进行翻译,然后在英语和希腊语之间进行翻译,并且它使用相同的语义空间,那么它也可以用于直接在芬兰语和希腊语之间进行翻译。这就开启了“思想向量”的可能性,即嵌入相对复杂的思想,具有与我们在第 3 章中看到的“词向量”相似的属性.

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值