真实世界的自然语言处理(一)(5)

本文涉及的产品
NLP 自学习平台,3个模型定制额度 1个月
NLP自然语言处理_高级版,每接口累计50万次
NLP自然语言处理_基础版,每接口每天50万次
简介: 真实世界的自然语言处理(一)

真实世界的自然语言处理(一)(4)https://developer.aliyun.com/article/1519728

4.4 构建 AllenNLP 训练流程

在本节中,我们将重新审视第二章中构建的情感分析器,并详细讨论如何更详细地构建其训练流程。尽管我已经展示了使用 AllenNLP

要运行本节中的代码,您需要导入必要的类和模块,如下面的代码片段所示(本节中的代码示例也可以通过 Google Colab 访问,www.realworldnlpbook.com/ch2.html#sst-nb)。

from itertools import chain
from typing import Dict
import numpy as np
import torch
import torch.optim as optim
from allennlp.data.data_loaders import MultiProcessDataLoader
from allennlp.data.samplers import BucketBatchSampler
from allennlp.data.vocabulary import Vocabulary
from allennlp.models import Model
from allennlp.modules.seq2vec_encoders import Seq2VecEncoder, PytorchSeq2VecWrapper
from allennlp.modules.text_field_embedders import TextFieldEmbedder, BasicTextFieldEmbedder
from allennlp.modules.token_embedders import Embedding
from allennlp.nn.util import get_text_field_mask
from allennlp.training.metrics import CategoricalAccuracy, F1Measure
from allennlp.training.trainer import GradientDescentTrainer
from allennlp_models.classification.dataset_readers.stanford_sentiment_tree_bank import \
    StanfordSentimentTreeBankDatasetReader
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
• 18

4.4.1 实例和字段

如第 2.2.1 节所述,实例是机器学习算法进行预测的原子单位。数据集是同一形式实例的集合。大多数 NLP 应用的第一步是读取或接收一些数据(例如从文件或通过网络请求)并将其转换为实例,以便 NLP/ML 算法可以使用它们。

AllenNLP 支持一个称为 DatasetReader 的抽象,它的工作是读取一些输入(原始字符串、CSV 文件、来自网络请求的 JSON 数据结构等)并将其转换为实例。AllenNLP 已经为 NLP 中使用的主要格式提供了广泛的数据集读取器,例如 CoNLL 格式(在语言分析的流行共享任务中使用)和 Penn Treebank(一种流行的用于句法分析的数据集)。要读取 Standard Sentiment Treebank,可以使用内置的 StanfordSentimentTreeBankDatasetReader,我们在第二章中已经使用过了。您还可以通过覆盖 DatasetReader 的一些核心方法来编写自己的数据集阅读器。

AllenNLP 类 Instance 表示一个单独的实例。一个实例可以有一个或多个字段,这些字段保存某种类型的数据。例如,情感分析任务的实例有两个字段——文本内容和标签——可以通过将字段字典传递给其构造函数来创建,如下所示:

Instance({'tokens': TextField(tokens),
          'label': LabelField(sentiment)})
• 1
• 2

在这里,我们假设您已经创建了 tokens(一个标记列表)和 sentiment(一个与情感类别对应的字符串标签),并从读取输入文件中获取了它们。根据任务,AllenNLP 还支持其他类型的字段。

DatasetReader 的 read() 方法返回一个实例迭代器,使您能够枚举生成的实例并对其进行可视化检查,如下面的代码片段所示:

reader = StanfordSentimentTreeBankDatasetReader()
train_dataset = reader.read('path/to/sst/dataset/train.txt')
dev_dataset = reader.read('path/to/sst/dataset/dev.txt')
for inst in train_dataset + dev_dataset:
    print(inst)
• 1
• 2
• 3
• 4
• 5
• 6
• 7

在许多情况下,您可以通过数据加载器访问数据集阅读器。数据加载器是 AllenNLP 的一个抽象(实际上是 PyTorch 数据加载器的一个薄包装),它处理数据并迭代批量实例。您可以通过提供批量样本器来指定如何对实例进行排序、分组为批次并提供给训练算法。在这里,我们使用了一个 BucketBatchSampler,它通过根据实例的长度对其进行排序,并将长度相似的实例分组到一个批次中,如下所示:

reader = StanfordSentimentTreeBankDatasetReader()
sampler = BucketBatchSampler(batch_size=32, sorting_keys=["tokens"])
train_data_loader = MultiProcessDataLoader(
    reader, train_path, batch_sampler=sampler)
dev_data_loader = MultiProcessDataLoader(
    reader, dev_path, batch_sampler=sampler)
• 1
• 2
• 3
• 4
• 5
• 6
• 7

4.4.2 词汇表和标记索引器

许多 NLP 应用程序的第二个步骤是构建词汇表。在计算机科学中,词汇是一个表示语言中所有可能单词的理论概念。在 NLP 中,它通常只是指数据集中出现的所有唯一标记的集合。了解一种语言中所有可能的单词是不可能的,也不是 NLP 应用程序所必需的。词汇表中存储的内容称为词汇项目(或仅称为项目)。词汇项目通常是一个词,尽管根据手头的任务,它可以是任何形式的语言单位,包括字符、字符 n-gram 和用于语言注释的标签。

AllenNLP 提供了一个名为 Vocabulary 的类。它不仅负责存储数据集中出现的词汇项目,还保存了词汇项目和它们的 ID 之间的映射关系。如前所述,神经网络和一般的机器学习模型只能处理数字,而需要一种将诸如单词之类的离散项目映射到一些数字表示(如单词 ID)的方式。词汇还用于将 NLP 模型的结果映射回原始单词和标签,以便人类实际阅读它们。

您可以按如下方式从实例创建一个 Vocabulary 对象:

vocab = Vocabulary.from_instances(chain(train_data_loader.iter_instances(), 
                                        dev_data_loader.iter_instances()),
                                  min_count={'tokens': 3})
• 1
• 2
• 3

这里需要注意几点:首先,因为我们正在处理迭代器(由数据加载器的 iter_instances()方法返回),所以我们需要使用 itertools 的 chain 方法来枚举两个数据集中的所有实例。

其次,AllenNLP 的 Vocabulary 类支持命名空间,这是一种将不同的项目集分开的系统,以防它们混淆。这是为什么它们很有用——假设你正在构建一个机器翻译系统,并且刚刚读取了一个包含英语和法语翻译的数据集。如果没有命名空间,你将只有一个包含所有英语和法语单词的集合。在大多数情况下,这通常不是一个大问题,因为英语单词(“hi,” “thank you,” “language”)和法语单词(“bonjour,” “merci,” “langue”)在大多数情况下看起来非常不同。然而,一些单词在两种语言中看起来完全相同。例如,“chat”在英语中意思是“talk”,在法语中是“cat”,但很难想象有人想要混淆这两个词并分配相同的 ID(和嵌入)。为了避免这种冲突,Vocabulary 实现了命名空间并为不同类型的项目分配了单独的集合。

你可能注意到form_instances()函数调用有一个min_count参数。对于每个命名空间,它指定了数据集中必须出现的最小次数,以便将项目包含在词汇表中。所有出现频率低于此阈值的项目都被视为“未知”项目。这是一个好主意的原因是:在典型的语言中,很少有一些词汇会频繁出现(英语中的“the”,“a”,“of”),而有很多词汇出现的频率很低。这通常表现为词频的长尾分布。但这些频率极低的词汇不太可能对模型有任何有用的信息,并且正因为它们出现频率较低,从中学习有用的模式也很困难。此外,由于这些词汇有很多,它们会增加词汇表的大小和模型参数的数量。在这种情况下,自然语言处理中常见的做法是截去这长尾部分,并将所有出现频率较低的词汇合并为一个单一的实体(表示“未知”词汇)。

最后,令牌索引器是 AllenNLP 的一个抽象概念,它接收一个令牌并返回其索引,或者返回表示令牌的索引列表。在大多数情况下,独特令牌和其索引之间存在一对一的映射,但根据您的模型,您可能需要更高级的方式来对令牌进行索引(例如使用字符 n-gram)。

创建词汇表后,你可以告诉数据加载器使用指定的词汇表对令牌进行索引,如下代码片段所示。这意味着数据加载器从数据集中读取的令牌会根据词汇表的映射转换为整数 ID:

train_data_loader.index_with(vocab)
dev_data_loader.index_with(vocab)
• 1
• 2

4.4.3 令牌嵌入和 RNN

在使用词汇表和令牌索引器索引单词后,需要将它们转换为嵌入。一个名为 TokenEmbedder 的 AllenNLP 抽象来接收单词索引作为输入并将其转换为单词嵌入向量作为输出。你可以使用多种方式嵌入连续向量,但如果你只想将唯一的令牌映射到嵌入向量时,可以使用 Embedding 类,如下所示:

token_embedding = Embedding(
    num_embeddings=vocab.get_vocab_size('tokens'),
    embedding_dim=EMBEDDING_DIM)
• 1
• 2
• 3

这将创建一个 Embedding 实例,它接收单词 ID 并以一对一的方式将其转换为定长矢量。该实例支持的唯一单词数量由 num_embeddings 给出,它等于令牌词汇的大小。嵌入的维度(即嵌入矢量的长度)由 embedding_dim 给出。

接下来,让我们定义我们的 RNN,并将变长输入(嵌入词的列表)转换为输入的定长矢量表示。正如我们在第 4.1 节中讨论的那样,你可以将 RNN 看作是一个神经网络结构,它消耗一个序列的事物(词汇)并返回一个定长的矢量。AllenNLP 将这样的模型抽象化为 Seq2VecEncoder 类,你可以通过使用 PytorchSeq2VecWrapper 创建一个 LSTM RNN,如下所示:

encoder = PytorchSeq2VecWrapper(
    torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
• 1
• 2

这里发生了很多事情,但本质上是将 PyTorch 的 LSTM 实现(torch.nn.LSTM)包装起来,使其可以插入到 AllenNLP 流程中。torch.nn.LSTM()的第一个参数是输入向量的维度,第二个参数是 LSTM 的内部状态的维度。最后一个参数 batch_first 指定了用于批处理的输入/输出张量的结构,但只要你使用 AllenNLP,你通常不需要担心其细节。

注意:在 AllenNLP 中,一切都是以批为单位,意味着任何张量的第一个维度始终等于批中实例的数量。

4.4.4 构建你自己的模型

现在我们已经定义了所有的子组件,我们准备构建执行预测的模型了。由于 AllenNLP 的良好抽象设计,你可以通过继承 AllenNLP 的 Model 类并覆盖 forward()方法来轻松构建你的模型。通常情况下,你不需要关注张量的形状和维度等细节。以下清单定义了用于分类句子的 LSTM RNN。

清单 4.1 LSTM 句子分类器

@Model.register("lstm_classifier")
class LstmClassifier(Model):                                   ❶
    def __init__(self,
                 embedder: TextFieldEmbedder,
                 encoder: Seq2VecEncoder,
                 vocab: Vocabulary,
                 positive_label: str = '4') -> None:
        super().__init__(vocab)
        self.embedder = embedder
        self.encoder = encoder
        self.linear = torch.nn.Linear(                         ❷
            in_features=encoder.get_output_dim(),
            out_features=vocab.get_vocab_size('labels'))
        positive_index = vocab.get_token_index(
            positive_label, namespace='labels')
        self.accuracy = CategoricalAccuracy()
        self.f1_measure = F1Measure(positive_index)            ❸
        self.loss_function = torch.nn.CrossEntropyLoss()       ❹
    def forward(self,                                          ❺
                tokens: Dict[str, torch.Tensor],
                label: torch.Tensor = None) -> torch.Tensor:
        mask = get_text_field_mask(tokens)
        embeddings = self.embedder(tokens)
        encoder_out = self.encoder(embeddings, mask)
        logits = self.linear(encoder_out)
        output = {"logits": logits}                            ❻
        if label is not None:
            self.accuracy(logits, label)
            self.f1_measure(logits, label)
            output["loss"] = self.loss_function(logits, label)
        return output
    def get_metrics(self, reset: bool = False) -> Dict[str, float]:
        return {'accuracy': self.accuracy.get_metric(reset),   ❼
                **self.f1_measure.get_metric(reset)}

❶ AllenNLP 模型继承自 Model。

❷ 创建线性层将 RNN 输出转换为另一个长度的向量

❸ F1Measure()需要正类的标签 ID。'4’表示“非常积极”。

❹ 用于分类任务的交叉熵损失。CrossEntropyLoss 直接接受 logits(不需要 softmax)。

❺ 实例被解构为各个字段并传递给 forward()。

❻ forward()的输出是一个字典,其中包含一个“loss”键。

❼ 返回准确率、精确率、召回率和 F1 分数作为度量标准

每个 AllenNLP 模型都继承自 PyTorch 的 Module 类,这意味着如果需要,你可以使用 PyTorch 的低级操作。这为你在定义模型时提供了很大的灵活性,同时利用了 AllenNLP 的高级抽象。

4.4.5 把所有东西都放在一起

最后,我们通过实现整个流程来训练情感分析器,如下所示。

清单 4.2 情感分析器的训练流程

EMBEDDING_DIM = 128
HIDDEN_DIM = 128
reader = StanfordSentimentTreeBankDatasetReader()
train_path = 'path/to/sst/dataset/train.txt'
dev_path = 'path/to/sst/dataset/dev.txt'
sampler = BucketBatchSampler(batch_size=32, sorting_keys=["tokens"])
train_data_loader = MultiProcessDataLoader(                            ❶
    reader, train_path, batch_sampler=sampler)
dev_data_loader = MultiProcessDataLoader(
    reader, dev_path, batch_sampler=sampler)
vocab = Vocabulary.from_instances(chain(train_data_loader.iter_instances(), 
                                        dev_data_loader.iter_instances()),
                                  min_count={'tokens': 3})
train_data_loader.index_with(vocab)
dev_data_loader.index_with(vocab)
token_embedding = Embedding(
    num_embeddings=vocab.get_vocab_size('tokens'),
    embedding_dim=EMBEDDING_DIM)
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
encoder = PytorchSeq2VecWrapper(
    torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
model = LstmClassifier(word_embeddings, encoder, vocab)               ❷
optimizer = optim.Adam(model.parameters())                            ❸
trainer = GradientDescentTrainer(                                     ❹
    model=model,
    optimizer=optimizer,
    data_loader=train_data_loader,
    validation_data_loader=dev_data_loader,
    patience=10,
    num_epochs=20,
    cuda_device=-1)
trainer.train()

❶ 定义如何构造数据加载器

❷ 初始化模型

❸ 定义优化器

❹ 初始化训练器

当创建 Trainer 实例并调用 train()时,训练流程完成。你需要传递所有用于训练的要素,包括模型、优化器、数据加载器、数据集和一堆超参数。

优化器实现了一个调整模型参数以最小化损失的算法。在这里,我们使用一种称为Adam的优化器,这是你作为首选项的一个很好的“默认”优化器。然而,正如我在第二章中提到的,你经常需要尝试许多不同的优化器,找出对你的模型效果最好的那一个。

4.5 配置 AllenNLP 训练流程

你可能已经注意到,列表 4.2 中很少有实际针对句子分类问题的内容。事实上,加载数据集、初始化模型,并将迭代器和优化器插入训练器是几乎每个 NLP 训练管道中的常见步骤。如果您想要为许多相关任务重复使用相同的训练管道而不必从头编写训练脚本呢?另外,如果您想要尝试不同配置集(例如,不同的超参数、神经网络架构)并保存您尝试过的确切配置呢?

对于这些问题,AllenNLP 提供了一个便捷的框架,您可以在 JSON 格式的配置文件中编写配置。其思想是您在 JSON 格式文件中编写您的训练管道的具体内容——例如要使用哪个数据集读取器、要使用哪些模型及其子组件,以及用于训练的哪些超参数。然后,您将配置文件提供给 AllenNLP 可执行文件,框架会负责运行训练管道。如果您想尝试模型的不同配置,只需更改配置文件(或创建一个新文件),然后再次运行管道,而无需更改 Python 代码。这是一种管理实验并使其可重现的良好实践。您只需管理配置文件及其结果——相同的配置始终产生相同的结果。

典型的 AllenNLP 配置文件由三个主要部分组成——数据集、您的模型和训练管道。下面是第一部分,指定了要使用的数据集文件以及如何使用:

"dataset_reader": {
    "type": "sst_tokens"
  },
  "train_data_path": "https:/./s3.amazonaws.com/realworldnlpbook/data/stanfordSentimentTreebank/trees/train.txt",
  "validation_data_path": "https:/./s3.amazonaws.com/realworldnlpbook/data/stanfordSentimentTreebank/trees/dev.txt"

此部分有三个键:dataset_reader、train_data_path 和 validation_data_path。第一个键 dataset_reader 指定要使用哪个 DatasetReader 来读取文件。在 AllenNLP 中,数据集读取器、模型、预测器以及许多其他类型的模块都可以使用装饰器语法注册,并且可以在配置文件中引用。例如,如果您查看下面定义了 StanfordSentimentTreeBankDatasetReader 的代码

@DatasetReader.register("sst_tokens")
class StanfordSentimentTreeBankDatasetReader(DatasetReader): 
    ...

你注意到它被 @DatasetReader.register(“sst_tokens”) 装饰。这将 StanfordSentimentTreeBankDatasetReader 注册为 sst_tokens,使您可以通过配置文件中的 “type”: “sst_tokens” 来引用它。

在配置文件的第二部分,您可以如下指定要训练的主要模型:

"model": {
    "type": "lstm_classifier",
    "embedder": {
      "token_embedders": {
        "tokens": {
          "type": "embedding",
          "embedding_dim": embedding_dim
        }
      }
    },
    "encoder": {
      "type": "lstm",
      "input_size": embedding_dim,
      "hidden_size": hidden_dim
    }
}

如前所述,AllenNLP 中的模型可以使用装饰器语法注册,并且可以通过 type 键从配置文件中引用。例如,这里引用的 LstmClassifier 类定义如下:

@Model.register("lstm_classifier")
class LstmClassifier(Model):
    def __init__(self,
                 embedder: TextFieldEmbedder,
                 encoder: Seq2VecEncoder,
                 vocab: Vocabulary,
                 positive_label: str = '4') -> None:

模型定义 JSON 字典中的其他键对应于模型构造函数的参数名称。在前面的定义中,因为 LstmClassifier 的构造函数接受了两个参数,word_embeddings 和 encoder(除了 vocab,它是默认传递的并且可以省略,以及 positive_label,我们将使用默认值),所以模型定义有两个相应的键,它们的值也是模型定义,并且遵循相同的约定。

在配置文件的最后部分,指定了数据加载器和训练器。这里的约定与模型定义类似——你指定类的类型以及传递给构造函数的其他参数,如下所示:

"data_loader": {
    "batch_sampler": {
      "type": "bucket",
      "sorting_keys": ["tokens"],
      "padding_noise": 0.1,
      "batch_size" : 32
    }
  },
  "trainer": {
    "optimizer": "adam",
    "num_epochs": 20,
    "patience": 10
  }

你可以在代码仓库中查看完整的 JSON 配置文件(realworldnlpbook.com/ch4.html#sst-json)。一旦你定义了 JSON 配置文件,你就可以简单地将其提供给 allennlp 命令,如下所示:

allennlp train examples/sentiment/sst_classifier.jsonnet \
    --serialization-dir sst-model \
    --include-package examples.sentiment.sst_classifier

–serialization-dir 指定了训练模型(以及其他一些信息,如序列化的词汇数据)将要存储的位置。你还需要使用 --include-package 指定到 LstmClassifier 的模块路径,以便配置文件能够找到注册的类。

正如我们在第二章中所看到的,当训练完成时,你可以使用以下命令启动一个简单的基于 web 的演示界面:

$ allennlp serve \ 
    --archive-path sst-model/model.tar.gz \
    --include-package examples.sentiment.sst_classifier \
    --predictor sentence_classifier_predictor \
    --field-name sentence

4.6 案例研究:语言检测

在本章的最后一节中,我们将讨论另一个场景——语言检测,它也可以被归纳为一个句子分类任务。语言检测系统,给定一段文本,检测文本所写的语言。它在其他自然语言处理应用中有着广泛的用途。例如,一个网络搜索引擎可能会在处理和索引网页之前检测网页所写的语言。Google 翻译还会根据输入文本框中键入的内容自动切换源语言。

让我们看看这实际上是什么样子。你能告诉下面每一句话是哪种语言吗?这些句子都来自 Tatoeba 项目(tatoeba.org/)。

我们需要你的帮助。

请考虑一下。

他们讨论了离开的计划。

我不知道我能不能做到。

昨天你在家,对吗?

它是一种快速而有效的通讯工具。

他讲了一个小时。

我想去喝一杯。

Ttwaliɣ nezmer ad nili d imeddukal.

答案是:西班牙语、德语、土耳其语、法语、葡萄牙语、世界语、意大利语、匈牙利语和柏柏尔语。我从 Tatoeba 上排名前 10 的最受欢迎的使用拉丁字母表的语言中挑选了它们。你可能对这里列出的一些语言不熟悉。对于那些不熟悉的人来说,世界语是一种在 19 世纪末发明的构造辅助语言。柏柏尔语实际上是一组与阿拉伯语等闲语族语言表亲关系的在北非某些地区使用的语言。

或许你能够认出其中一些语言,尽管你实际上并不会说它们。我想让你退后一步思考你是如何做到的。很有趣的是,人们可以在不会说这种语言的情况下做到这一点,因为这些语言都是用拉丁字母表写成的,看起来可能非常相似。你可能认出了其中一些语言的独特变音符号(重音符号)——例如,德语的“ü”和葡萄牙语的“ã”。这些对于这些语言来说是一个强有力的线索。或者你只是认识一些单词——例如,西班牙语的“ayuda”(意思是“帮助”)和法语的“pas”(“ne…pas”是法语的否定句语法)。似乎每种语言都有其自己的特点——无论是一些独特的字符还是单词——使得它很容易与其他语言区分开来。这开始听起来很像是机器学习擅长解决的一类问题。我们能否构建一个能够自动执行此操作的 NLP 系统?我们应该如何构建它?

4.6.1 使用字符作为输入

语言检测器也可以以类似的方式构建情感分析器。你可以使用 RNN 读取输入文本并将其转换为一些内部表示(隐藏状态)。然后,你可以使用一个线性层将它们转换为一组分数,对应于文本写成每种语言的可能性。最后,你可以使用交叉熵损失来训练模型。

一个主要区别在于情感分析器和语言检测器如何将输入馈送到 RNN 中。构建情感分析器时,我们使用了斯坦福情感树库,并且能够假设输入文本始终为英文且已经被标记化。但是对于语言检测来说情况并非如此。实际上,你甚至不知道输入文本是否是易于标记化的语言所写成——如果句子是用中文写的呢?或者是用芬兰语写的,芬兰语以其复杂的形态而臭名昭著?如果你知道是什么语言,你可以使用特定于该语言的标记器,但我们正在构建语言检测器,因为我们一开始并不知道是什么语言。这听起来像是一个典型的先有鸡还是先有蛋的问题。

为了解决这个问题,我们将使用字符而不是标记作为 RNN 的输入。这个想法是将输入分解为单个字符,甚至包括空格和标点符号,并将它们逐个馈送给 RNN。当输入可以更好地表示为字符序列时(例如中文或未知来源的语言),或者当您希望充分利用单词的内部结构时(例如我们在第三章中提到的 fastText 模型)时,使用字符是一种常见的做法。RNN 的强大表现力仍然可以捕获先前提到的字符和一些常见单词和 n-gram 之间的交互。

创建数据集阅读器

对于这个语言检测任务,我从 Tatoeba 项目中创建了 train 和 validation 数据集,方法是选择使用罗马字母的 Tatoeba 上最受欢迎的 10 种语言,并对训练集采样 10,000 个句子,验证集采样 1,000 个句子。以下是该数据集的摘录:

por De entre os designers, ele escolheu um jovem ilustrador e deu-lhe a tarefa.
por A apresentação me fez chorar.
tur Bunu denememize gerek var mı?
tur O korkutucu bir parçaydı.
ber Tebḍamt aɣrum-nni ɣef sin, naɣ?
ber Ad teddud ad twalid taqbuct n umaḍal n tkurt n uḍar deg Brizil?
eng Tom works at Harvard.
eng They fixed the flat tire by themselves.
hun Az arca hirtelen elpirult.
hun Miért aggodalmaskodsz? Hiszen még csak egy óra van!
epo Sidiĝu sur la benko.
epo Tiu ĉi kutime funkcias.
fra Vu d'avion, cette île a l'air très belle.
fra Nous boirons à ta santé.
deu Das Abnehmen fällt ihm schwer.
deu Tom war etwas besorgt um Maria.
ita Sono rimasto a casa per potermi riposare.
ita Le due più grandi invenzioni dell'uomo sono il letto e la bomba atomica: il primo ti tiene lontano dalle noie, la seconda le elimina.
spa He visto la película.
spa Has hecho los deberes.

第一个字段是一个三个字母的语言代码,描述了文本所使用的语言。第二个字段是文本本身。字段由制表符分隔。您可以从代码存储库获取数据集(github.com/mhagiwara/realworldnlp/tree/master/data/tatoeba)。

构建语言检测器的第一步是准备一个能够读取这种格式数据集的数据集阅读器。在之前的例子(情感分析器)中,因为 AllenNLP 已经提供了 StanfordSentimentTreeBankDatasetReader,所以您只需要导入并使用它。然而,在这种情况下,您需要编写自己的数据集阅读器。幸运的是,编写一个能够读取这种特定格式的数据集阅读器并不那么困难。要编写数据集阅读器,您只需要做以下三件事:

  • 通过继承 DatasetReader 创建自己的数据集阅读器类。
  • 覆盖 text_to_instance()方法,该方法接受原始文本并将其转换为实例对象。
  • 覆盖 _read()方法,该方法读取文件的内容并通过调用上面的 text_to_instance()方法生成实例。

语言检测器的完整数据集阅读器如列表 4.3 所示。我们还假设您已经导入了必要的模块和类,如下所示:

from typing import Dict
import numpy as np
import torch
import torch.optim as optim
from allennlp.common.file_utils import cached_path
from allennlp.data.data_loaders import MultiProcessDataLoader
from allennlp.data.dataset_readers import DatasetReader
from allennlp.data.fields import LabelField, TextField
from allennlp.data.instance import Instance
from allennlp.data.samplers import BucketBatchSampler
from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer
from allennlp.data.tokenizers.character_tokenizer import CharacterTokenizer
from allennlp.data.vocabulary import Vocabulary
from allennlp.modules.seq2vec_encoders import PytorchSeq2VecWrapper
from allennlp.modules.text_field_embedders import BasicTextFieldEmbedder
from allennlp.modules.token_embedders import Embedding
from allennlp.training import GradientDescentTrainer
from overrides import overrides
from examples.sentiment.sst_classifier import LstmClassifier

列表 4.3 用于语言检测器的数据集阅读器

class TatoebaSentenceReader(DatasetReader):                    ❶
    def __init__(self,
                 token_indexers: Dict[str, TokenIndexer]=None):
        super().__init__()
        self.tokenizer = CharacterTokenizer()                  ❷
        self.token_indexers = token_indexers or {'tokens': SingleIdTokenIndexer()}
    @overrides
    def text_to_instance(self, tokens, label=None):            ❸
        fields = {}
        fields['tokens'] = TextField(tokens, self.token_indexers)
        if label:
            fields['label'] = LabelField(label)
        return Instance(fields)
    @overrides
    def _read(self, file_path: str):
        file_path = cached_path(file_path)                     ❹
        with open(file_path, "r") as text_file:
            for line in text_file:
                lang_id, sent = line.rstrip().split('\t')
                tokens = self.tokenizer.tokenize(sent)
                yield self.text_to_instance(tokens, lang_id)   ❺

❶ 每个新的数据集阅读器都继承自 DatasetReader。

❷ 使用 CharacterTokenizer()将文本标记为字符

❸ 在测试时标签将为 None。

❹ 如果 file_path 是 URL,则返回磁盘上缓存文件的实际路径

❺ 使用之前定义的 text_to_instance()生成实例

请注意,列表 4.3 中的数据集阅读器使用 CharacterTokenizer()将文本标记为字符。它的 tokenize()方法返回一个标记列表,这些标记是 AllenNLP 对象,表示标记,但实际上在这种情况下包含字符。

构建训练管道

一旦构建了数据集阅读器,训练流水线的其余部分看起来与情感分析器的类似。 实际上,我们可以在不进行任何修改的情况下重用之前定义的 LstmClassifier 类。 整个训练流水线在列表 4.4 中显示。 您可以从这里访问整个代码的 Google Colab 笔记本:realworldnlpbook.com/ch4.html#langdetect

列表 4.4 语言检测器的训练流水线

EMBEDDING_DIM = 16
HIDDEN_DIM = 16
reader = TatoebaSentenceReader()
train_path = 'https:/./s3.amazonaws.com/realworldnlpbook/data/tatoeba/sentences.top10langs.train.tsv'
dev_path = 'https:/./s3.amazonaws.com/realworldnlpbook/data/tatoeba/sentences.top10langs.dev.tsv'
sampler = BucketBatchSampler(batch_size=32, sorting_keys=["tokens"])
train_data_loader = MultiProcessDataLoader(
    reader, train_path, batch_sampler=sampler)
dev_data_loader = MultiProcessDataLoader(
    reader, dev_path, batch_sampler=sampler)
vocab = Vocabulary.from_instances(train_data_loader.iter_instances(),
                                  min_count={'tokens': 3})
train_data_loader.index_with(vocab)
dev_data_loader.index_with(vocab)
token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'),
                            embedding_dim=EMBEDDING_DIM)
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
encoder = PytorchSeq2VecWrapper(
    torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
model = LstmClassifier(word_embeddings,
                       encoder,
                       vocab,
                       positive_label='eng')
train_dataset.index_with(vocab)
dev_dataset.index_with(vocab)
optimizer = optim.Adam(model.parameters())
trainer = GradientDescentTrainer(
    model=model,
    optimizer=optimizer,
    data_loader=train_data_loader,
    validation_data_loader=dev_data_loader,
    patience=10,
    num_epochs=20,
    cuda_device=-1)
trainer.train()

运行此训练流水线时,您将获得与以下大致相当的开发集上的指标:

accuracy: 0.9461, precision: 0.9481, recall: 0.9490, f1_measure: 0.9485, loss: 0.1560

这一点一点也不糟糕! 这意味着训练过的检测器在约 20 个句子中只犯了一个错误。 0.9481 的精确度意味着在 20 个被分类为英文的实例中只有一个假阳性(非英文句子)。 0.9490 的召回率意味着在 20 个真正的英文实例中只有一个假阴性(被检测器漏掉的英文句子)。

4.6.4 在未见过的实例上运行检测器

最后,让我们尝试在一组未见过的实例(既不出现在训练集也不出现在验证集中的实例)上运行我们刚刚训练过的检测器。 尝试向模型提供少量实例并观察其行为始终是一个好主意。

将实例提供给训练过的 AllenNLP 模型的推荐方法是使用预测器,就像我们在第二章中所做的那样。 但在这里,我想做一些更简单的事情,而是编写一个方法,给定一段文本和一个模型,运行预测流水线。 要在任意实例上运行模型,可以调用模型的 forward_on_instances() 方法,如下面的代码片段所示:

def classify(text: str, model: LstmClassifier):
    tokenizer = CharacterTokenizer()
    token_indexers = {'tokens': SingleIdTokenIndexer()}
    tokens = tokenizer.tokenize(text)
    instance = Instance({'tokens': TextField(tokens, token_indexers)})
    logits = model.forward_on_instance(instance)['logits']
    label_id = np.argmax(logits)
    label = model.vocab.get_token_from_index(label_id, 'labels')
    print('text: {}, label: {}'.format(text, label))

此方法首先接受输入(文本和模型)并通过分词器将其传递以创建实例对象。 然后,它调用模型的 forward_on_instance() 方法来检索 logits,即目标标签(语言)的分数。 通过调用 np.argmax 获取对应于最大 logit 值的标签 ID,然后通过使用与模型关联的词汇表对象将其转换为标签文本。

当我对一些不在这两个数据集中的句子运行此方法时,我得到了以下结果。 请注意,由于一些随机性,您得到的结果可能与我的不同:

text: Take your raincoat in case it rains., label: fra
text: Tu me recuerdas a mi padre., label: spa
text: Wie organisierst du das Essen am Mittag?, label: deu
text: Il est des cas où cette règle ne s'applique pas., label: fra
text: Estou fazendo um passeio em um parque., label: por
text: Ve, postmorgaŭ jam estas la limdato., label: epo
text: Credevo che sarebbe venuto., label: ita
text: Nem tudja, hogy én egy macska vagyok., label: hun
text: Nella ur nli qrib acemma deg tenwalt., label: ber
text: Kurşun kalemin yok, deǧil mi?, label: tur

这些预测几乎完美,除了第一句话——它是英文,而不是法文。 令人惊讶的是,模型在预测更难的语言(如匈牙利语)时完美无误地犯了一个看似简单的错误。 但请记住,对于英语为母语者来说,语言有多难并不意味着计算机分类时有多难。 实际上,一些“困难”的语言,比如匈牙利语和土耳其语,具有非常清晰的信号(重音符号和独特的单词),这使得很容易检测它们。 另一方面,第一句话中缺乏清晰的信号可能使它更难以从其他语言中分类出来。

作为下一步,你可以尝试一些事情:例如,你可以调整一些超参数,看看评估指标和最终预测结果如何变化。你还可以尝试增加测试实例的数量,以了解错误是如何分布的(例如,在哪两种语言之间)。你还可以把注意力集中在一些实例上,看看模型为什么会犯这样的错误。这些都是在处理真实世界的自然语言处理应用时的重要实践。我将在第十章中详细讨论这些话题。

摘要

  • 循环神经网络(RNN)是一种带有循环的神经网络。它可以将可变长度的输入转换为固定长度的向量。
  • 非线性是使神经网络真正强大的关键组成部分。
  • LSTM 和 GRU 是 RNN 单元的两个变体,比原始的 RNN 更容易训练。
  • 在分类问题中,你可以使用准确率、精确度、召回率和 F-度量来评估。
  • AllenNLP 提供了有用的自然语言处理抽象,例如数据集读取器、实例和词汇表。它还提供了一种以 JSON 格式配置训练流水线的方法。
  • 你可以构建一个类似于情感分析器的句子分类应用来实现语言检测器。
相关文章
|
6月前
|
机器学习/深度学习 自然语言处理 PyTorch
真实世界的自然语言处理(二)(1)
真实世界的自然语言处理(二)
76 1
|
6月前
|
机器学习/深度学习 自然语言处理 异构计算
真实世界的自然语言处理(三)(2)
真实世界的自然语言处理(三)
50 3
|
6月前
|
机器学习/深度学习 自然语言处理 算法
真实世界的自然语言处理(三)(1)
真实世界的自然语言处理(三)
46 3
|
6月前
|
机器学习/深度学习 自然语言处理 数据可视化
真实世界的自然语言处理(一)(4)
真实世界的自然语言处理(一)
33 2
|
6月前
|
机器学习/深度学习 自然语言处理 算法
真实世界的自然语言处理(二)(4)
真实世界的自然语言处理(二)
47 1
|
6月前
|
机器学习/深度学习 人工智能 自然语言处理
真实世界的自然语言处理(一)(3)
真实世界的自然语言处理(一)
39 1
|
6月前
|
机器学习/深度学习 自然语言处理 监控
真实世界的自然语言处理(一)(2)
真实世界的自然语言处理(一)
59 1
|
6月前
|
机器学习/深度学习 自然语言处理 算法
真实世界的自然语言处理(二)(5)
真实世界的自然语言处理(二)
43 0
|
6月前
|
机器学习/深度学习 自然语言处理 算法
真实世界的自然语言处理(二)(3)
真实世界的自然语言处理(二)
47 0
|
6月前
|
机器学习/深度学习 自然语言处理 算法
真实世界的自然语言处理(二)(2)
真实世界的自然语言处理(二)
86 0
下一篇
无影云桌面