Python 人工智能秘籍(六)

原文:zh.annas-archive.org/md5/11b175e592527142ad4d19f0711517be

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:自然语言处理

自然语言处理 (NLP) 是关于分析文本并设计处理文本的算法,从文本中进行预测或生成更多文本。NLP 涵盖了与语言相关的任何内容,通常包括类似于我们在第九章中看到的识别语音命令配方,深度学习中的音频和语音。您可能还想参考第二章中对抗算法偏差配方或第三章中用于相似性搜索的表示配方,以了解更传统的方法。本章大部分内容将涉及近年来突破性进展背后的深度学习模型。

语言通常被视为与人类智能密切相关,并且机器掌握沟通能力长期以来一直被认为与实现人工通用智能 (AGI) 的目标密切相关。艾伦·图灵在他 1950 年的文章计算机与智能中建议了一个测试,后来称为图灵测试,在这个测试中,询问者必须找出他们的交流对象(在另一个房间里)是计算机还是人类。然而,有人认为,成功地欺骗询问者以为他们在与人类交流并不是真正理解(或智能)的证据,而是符号操纵的结果(中文房间论证;约翰·西尔,思想、大脑和程序,1980 年)。不管怎样,在最近几年里,随着像 GPU 这样的并行计算设备的可用性,NLP 在许多基准测试中取得了令人瞩目的进展,例如在文本分类方面:nlpprogress.com/english/text_classification.html

我们首先将完成一个简单的监督任务,确定段落的情感,然后我们将设置一个响应命令的 Alexa 风格聊天机器人。接下来,我们将使用序列到序列模型翻译文本。最后,我们将尝试使用最先进的文本生成模型写一本流行小说。

在本章中,我们将进行以下配方:

  • 对新闻组进行分类

  • 与用户聊天

  • 将文本从英语翻译成德语

  • 写一本流行小说

技术要求

与迄今为止大多数章节一样,我们将尝试基于 PyTorch 和 TensorFlow 的模型。我们将在每个配方中应用不同的更专业的库。

如往常一样,您可以在 GitHub 上找到配方笔记本:github.com/PacktPublishing/Artificial-Intelligence-with-Python-Cookbook/tree/master/chapter10

对新闻组进行分类

在这个配方中,我们将做一个相对简单的监督任务:基于文本,我们将训练一个模型来确定文章的主题,从一系列话题中选择。这是一个在 NLP 中相对常见的任务;我们将试图概述不同的解决方法。

您可能还想比较在《第二章》中 算法偏差对抗 的配方中,使用词袋法(在 scikit-learn 中的 CountVectorizer)来解决这个问题的方法。在这个配方中,我们将使用词嵌入和使用词嵌入的深度学习模型。

准备工作

在这个配方中,我们将使用 scikit-learn 和 TensorFlow(Keras),正如本书的许多其他配方一样。此外,我们将使用需要下载的词嵌入,并且我们将使用 Gensim 库的实用函数在我们的机器学习管道中应用它们:

!pip install gensim

我们将使用来自 scikit-learn 的数据集,但我们仍然需要下载词嵌入。我们将使用 Facebook 的 fastText 词嵌入,该词嵌入是在 Wikipedia 上训练的:

!pip install wget
import wget
wget.download(
    'https://dl.fbaipublicfiles.com/fasttext/vectors-wiki/wiki.en.vec',
    'wiki.en.vec'
)

请注意,下载可能需要一些时间,并且需要大约 6 GB 的磁盘空间。如果您在 Colab 上运行,请将嵌入文件放入 Google Drive 的目录中,这样当您重新启动笔记本时就不需要重新下载。

如何做…

新闻组数据集是大约 20,000 个新闻组文档的集合,分为 20 个不同的组。20 个新闻组集合是在 NLP 中测试机器学习技术(如文本分类和文本聚类)的流行数据集。

我们将把一组新闻组分类为三个不同的主题,并且我们将使用三种不同的技术来解决这个任务,以便进行比较。首先获取数据集,然后应用词袋法技术,使用词嵌入,在深度学习模型中训练定制的词嵌入。

首先,我们将使用 scikit-learn 的功能下载数据集。我们将新闻组数据集分两批下载,分别用于训练和测试。

from sklearn.datasets import fetch_20newsgroups

categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics', 'sci.med']
twenty_train = fetch_20newsgroups(
    subset='train',
    categories=categories,
    shuffle=True,
    random_state=42
  )
twenty_test = fetch_20newsgroups(
    subset='test',
    categories=categories,
    shuffle=True,
    random_state=42
)

这方便地为我们提供了训练和测试数据集,我们可以在这三种方法中使用。

让我们开始覆盖第一个方法,使用词袋法。

词袋法

我们将构建一个单词计数和根据它们的频率重新加权的管道。最终的分类器是一个随机森林。我们在训练数据集上训练这个模型:

import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.ensemble import RandomForestClassifier

text_clf = Pipeline([
  ('vect', CountVectorizer()),
  ('tfidf', TfidfTransformer()),
  ('clf', RandomForestClassifier()),
])
text_clf.fit(twenty_train.data, twenty_train.target)

CountVectorizer 计算文本中的标记,而 tfidfTransformer 重新加权这些计数。我们将在 工作原理… 部分讨论词项频率-逆文档频率TFIDF)重新加权。

训练后,我们可以在测试数据集上测试准确率:

predicted = text_clf.predict(twenty_test.data)
np.mean(predicted == twenty_test.target)

我们的准确率约为 0.805。让我们看看我们的另外两种方法表现如何。下一步是使用词嵌入。

词嵌入

我们将加载我们之前下载的词嵌入:

from gensim.models import KeyedVectors

model = KeyedVectors.load_word2vec_format(
    'wiki.en.vec',
    binary=False, encoding='utf8'
)

将文本的最简单策略向量化是对单词嵌入进行平均。对于短文本,这通常至少效果还不错:

import numpy as np
from tensorflow.keras.preprocessing.text import text_to_word_sequence

def embed_text(text: str):
  vector_list = [
    model.wv[w].reshape(-1, 1) for w in text_to_word_sequence(text)
    if w in model.wv
  ]
  if len(vector_list) > 0:
    return np.mean(
        np.concatenate(vector_list, axis=1),
        axis=1
    ).reshape(1, 300)
  else:
   return np.zeros(shape=(1, 300))

assert embed_text('training run').shape == (1, 300)

然后我们将这种向量化应用于我们的数据集,然后在这些向量的基础上训练一个随机森林分类器:

train_transformed = np.concatenate(
    [embed_text(t) for t in twenty_train.data]
)
rf = RandomForestClassifier().fit(train_transformed, twenty_train.target)

然后我们可以测试我们方法的性能:

test_transformed = np.concatenate(
    [embed_text(t) for t in twenty_test.data]
)
predicted = rf.predict(test_transformed)
np.mean(predicted == twenty_test.target)

我们的准确率约为 0.862。

让我们看看我们的最后一种方法是否比这个更好。我们将使用 Keras 的嵌入层构建定制的词嵌入。

自定义词嵌入

嵌入层是在神经网络中即时创建自定义词嵌入的一种方式:

from tensorflow.keras import layers

embedding = layers.Embedding(
    input_dim=5000, 
    output_dim=50, 
    input_length=500
)

我们必须告诉嵌入层希望存储多少单词,词嵌入应该具有多少维度,以及每个文本中有多少单词。我们将整数数组输入嵌入层,每个数组引用字典中的单词。我们可以将创建嵌入层输入的任务委托给 TensorFlow 实用函数:

from tensorflow.keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer(num_words=5000)
tokenizer.fit_on_texts(twenty_train.data)

这创建了字典。现在我们需要对文本进行分词并将序列填充到适当的长度:

from tensorflow.keras.preprocessing.sequence import pad_sequences

X_train = tokenizer.texts_to_sequences(twenty_train.data)
X_test = tokenizer.texts_to_sequences(twenty_test.data)
X_train = pad_sequences(X_train, padding='post', maxlen=500)
X_test = pad_sequences(X_test, padding='post', maxlen=500)

现在我们准备构建我们的神经网络:

from tensorflow.keras.models import Sequential
from tensorflow.keras.losses import SparseCategoricalCrossentropy
from tensorflow.keras import regularizers

model = Sequential()
model.add(embedding)
model.add(layers.Flatten())
model.add(layers.Dense(
    10,
    activation='relu',
    kernel_regularizer=regularizers.l1_l2(l1=1e-5, l2=1e-4)
))
model.add(layers.Dense(len(categories), activation='softmax'))
model.compile(optimizer='adam',
              loss=SparseCategoricalCrossentropy(),
              metrics=['accuracy'])
model.summary()

我们的模型包含 50 万个参数。大约一半位于嵌入层,另一半位于前馈全连接层。

我们对网络进行了几个 epoch 的拟合,然后可以在测试数据上测试我们的准确性:

model.fit(X_train, twenty_train.target, epochs=10)
predicted = model.predict(X_test).argmax(axis=1)
np.mean(predicted == twenty_test.target)

我们获得约为 0.902 的准确率。我们还没有调整模型架构。

这完成了我们使用词袋模型、预训练词嵌入和自定义词嵌入进行新闻组分类的工作。现在我们来探讨一些背景。

工作原理…

我们已经根据三种不同的特征化方法对文本进行了分类:词袋模型、预训练词嵌入和自定义词嵌入。让我们简要地深入研究词嵌入和 TFIDF。

在第五章的基于知识做决策配方中,我们已经讨论了SkipgramContinuous Bag of WordsCBOW)算法,在启发式搜索技术与逻辑推理(在使用 Walklets 进行图嵌入子节中)。

简而言之,词向量是一个简单的机器学习模型,可以根据上下文(CBOW 算法)预测下一个词,或者可以根据一个单词预测上下文(Skipgram 算法)。让我们快速看一下 CBOW 神经网络。

CBOW 算法

CBOW 算法是一个两层前馈神经网络,用于从上下文中预测单词(更确切地说是稀疏索引向量):

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

这幅插图展示了在 CBOW 模型中,基于周围上下文预测单词的方式。在这里,单词被表示为词袋向量。隐藏层由上下文的加权平均值组成(线性投影)。输出单词是基于隐藏层的预测。这是根据法语维基百科关于词嵌入的页面上的一幅图片进行调整的:fr.wikipedia.org/wiki/Word_embedding

我们还没有讨论这些词嵌入的含义,这些词嵌入在它们出现时引起了轰动。这些嵌入是单词的网络激活,并具有组合性质,这为许多演讲和少数论文赋予了标题。我们可以结合向量进行语义代数或进行类比。其中最著名的例子是以下内容:

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

直观地说,国王和王后是相似的社会职位,只是一个由男人担任,另一个由女人担任。这在数十亿个单词学习的嵌入空间中得到了反映。从国王的向量开始,减去男人的向量,最后加上女人的向量,我们最终到达的最接近的单词是王后。

嵌入空间可以告诉我们关于我们如何使用语言的很多信息,其中一些信息有些令人担忧,比如当单词向量展现出性别刻板印象时。

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

这可以通过仿射变换在一定程度上进行校正,如 Tolga Bolukbasi 等人所示(Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings, 2016; arxiv.org/abs/1607.06520)。

让我们快速看看在这个方法的词袋法中采用的重新加权。

TFIDF

词袋模型部分,我们使用CountVectorizer来计数单词。这给了我们一个形状为外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的向量,其中外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传是词汇表中的单词数量。词汇表必须在fit()阶段之前创建,然后transform()可以基于词汇表中标记(单词)的位置创建(稀疏)向量。

通过在多篇文档上应用CountVectorizer,我们可以得到一个形状为外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的稀疏矩阵,其中外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传是语料库(文档集合),而外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传是文档的数量。这个矩阵中的每个位置表示某个标记在文档中出现的次数。在这个方法中,一个标记对应一个单词,但它同样可以是字符或任何一组字符。

有些词可能出现在每个文档中;其他词可能只出现在文档的一个小子集中,这表明它们更为特定和精确。这就是 TFIDF 的直觉,即如果一个词在语料库(文档集合)中的频率低,则提升计数(矩阵中的列)的重要性。

给定一组文档 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 的反向术语 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 的定义如下:

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

这里 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 是术语的计数 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 在文档 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 中,以及 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 是术语 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 出现的文档数。您应该看到 TFIDF 值随 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 的增加而减少。随着术语出现在更多文档中,对数和 TFIDF 值接近 0。

在本章的下一个示例中,我们将超越单词的编码,研究更复杂的语言模型。

还有更多…

我们将简要介绍如何使用 Gensim 学习自己的词嵌入,构建更复杂的深度学习模型,并在 Keras 中使用预训练的词嵌入:

  1. 我们可以在 Gensim 上轻松地训练自己的词嵌入。

让我们读入一个文本文件,以便将其作为 fastText 的训练数据集:

from gensim.utils import tokenize
from gensim.test.utils import datapath

class FileIter(object):
  def __init(self, filepath: str):
    self.path = datapath(filepath)

  def __iter__(self):
    with utils.open(self.path, 'r', encoding='utf-8') as fin:
      for line in fin:
        yield list(tokenize(line))

这对于迁移学习、搜索应用或在学习嵌入过程中花费过长时间的情况非常有用。在 Gensim 中,这只需几行代码(改编自 Gensim 文档)。

训练本身很简单,并且由于我们的文本文件很小,所以相对快速:

from gensim.models import FastText

model = FastText(size=4, window=3, min_count=1)
model.build_vocab(
  sentences=FileIter(
    'crime-and-punishment.txt'
))
model.train(sentences=common_texts, total_examples=len(common_texts), epochs=10)

您可以在 Project Gutenberg 找到《罪与罚》等经典小说:www.gutenberg.org/ebooks/2554

您可以像这样从训练好的模型中检索向量:

model.wv['axe']

Gensim 具有丰富的功能,我们建议您阅读一些其文档。

  1. 构建更复杂的深度学习模型:对于更困难的问题,我们可以在嵌入层之上使用堆叠的conv1d层,如下所示:
x = Conv1D(128, 5, activation='relu')(embedded_sequences)
x = MaxPooling1D(5)(x)

卷积层具有非常少的参数,这是使用它们的另一个优点。

  1. 在 Keras 模型中使用预训练的词嵌入:如果我们想要使用已下载的(或之前定制的)词嵌入,我们也可以这样做。首先,我们需要创建一个字典,在加载它们后我们可以很容易地自己完成,例如,使用 Gensim:
word_index = {i: w for i, w in enumerate(model.wv.vocab.keys())}

然后我们可以将这些向量馈送到嵌入层中:

from tensorflow.keras.layers import Embedding

embedding_layer = Embedding(
    len(word_index) + 1,
    300,
    weights=[list(model.wv.vectors)],
    input_length=500,
    trainable=False
)

为了训练和测试,您必须通过在我们的新词典中查找它们来提供单词索引,并像之前一样将它们填充到相同的长度。

这就结束了我们关于新闻组分类的配方。我们应用了三种不同的特征化方法:词袋模型、预训练词嵌入以及简单神经网络中的自定义词嵌入。

另请参阅

在这个配方中,我们使用了词嵌入。已经介绍了许多不同的嵌入方法,并且已经发布了许多训练自数百亿字词和数百万文档的词嵌入矩阵。如果在租用的硬件上进行大规模训练,这可能会耗费数十万美元。最流行的词嵌入包括以下内容:

处理词嵌入的流行库包括以下内容:

Kashgari 是建立在 Keras 之上用于文本标注和文本分类的库,包括 Word2vec 和更高级的模型,如 BERT 和 GPT2 语言嵌入:github.com/BrikerMan/Kashgari

Hugging Face 变压器库(github.com/huggingface/transformers)包含许多先进的架构和许多变压器模型的预训练权重,可用于文本嵌入。这些模型可以在许多自然语言处理任务中实现最先进的性能。例如,谷歌等公司已将许多语言应用转移到 BERT 架构上。我们将在本章的将英语翻译为德语的文本配方中学习更多有关变压器架构的信息。

fast.ai 提供了关于使用 PyTorch 进行深度学习的许多教程和课程的综合信息;它还包含许多有关自然语言处理的资源:nlp.fast.ai/

最后,在自然语言处理中,分类任务中常常涉及成千上万甚至数百万个不同的标签。这种情况被称为eXtreme MultiLabelXML)场景。你可以在这里找到关于 XML 的笔记本教程:github.com/ppontisso/Extreme-Multi-Label-Classification

与用户聊天

1966 年,约瑟夫·韦伊岑鲍姆发表了一篇关于他的聊天机器人 ELIZA 的文章,名为ELIZA - 人与机器之间自然语言交流研究的计算机程序。以幽默的方式展示技术的局限性,该聊天机器人采用简单的规则和模糊的开放性问题,以表现出对话中的移情理解,并以具有讽刺意味的方式经常被视为人工智能的里程碑。该领域已经发展,今天,AI 助手就在我们身边:您可能有 Alexa、Google Echo 或市场上其他任何商业家庭助手。

在这个教程中,我们将构建一个 AI 助手。这其中的困难在于,人们表达自己的方式有无数种,而且根本不可能预料到用户可能说的一切。在这个教程中,我们将训练一个模型来推断他们想要什么,并且我们会相应地做出回应。

准备工作

对于这个教程,我们将使用 Fariz Rahman 开发的名为Eywa的框架。我们将从 GitHub 使用pip安装它:

!pip install git+https://www.github.com/farizrahman4u/eywa.git

Eywa 具有从对话代理中预期的主要功能,我们可以查看其代码,了解支撑其功能的建模。

我们还将通过pyOWM库使用 OpenWeatherMap Web API,因此我们也将安装这个库:

!pip install pyOWM

使用这个库,我们可以作为我们聊天机器人功能的一部分响应用户请求并请求天气数据。如果您想在自己的聊天机器人中使用此功能,您应该在OpenWeatherMap.org注册一个免费用户账户并获取您的 API 密钥,每天最多可请求 1,000 次。

让我们看看我们如何实现这一点。

如何做…

我们的代理将处理用户输入的句子,解释并相应地回答。它将首先预测用户查询的意图,然后提取实体,以更准确地了解查询的内容,然后返回答案:

  1. 让我们从意图类开始 - 基于一些短语示例,我们将定义诸如greetingstaxiweatherdatetimemusic等意图:
from eywa.nlu import Classifier

CONV_SAMPLES = {
    'greetings' : ['Hi', 'hello', 'How are you', 'hey there', 'hey'],
    'taxi' : ['book a cab', 'need a ride', 'find me a cab'],
    'weather' : ['what is the weather in tokyo', 'weather germany',
                   'what is the weather like in kochi',
                   'what is the weather like', 'is it hot outside'],
    'datetime' : ['what day is today', 'todays date', 'what time is it now',
                   'time now', 'what is the time'],
    'music' : ['play the Beatles', 'shuffle songs', 'make a sound']
}

CLF = Classifier()
for key in CONV_SAMPLES:
    CLF.fit(CONV_SAMPLES[key], key)

我们已经创建了基于对话样本的分类器。我们可以使用以下代码块快速测试其工作原理:

print(CLF.predict('will it rain today')) # >>> 'weather'
print(CLF.predict('play playlist rock n\'roll')) # >>> 'music'
print(CLF.predict('what\'s the hour?')) # >>> 'datetime'

我们可以成功预测所需操作是否涉及天气、酒店预订、音乐或时间。

  1. 作为下一步,我们需要了解意图是否有更具体的内容,例如伦敦的天气与纽约的天气,或者播放披头士与坎耶·韦斯特。我们可以使用eywa实体提取来实现此目的:
from eywa.nlu import EntityExtractor

X_WEATHER = [
  'what is the weather in tokyo',
  'weather germany',
  'what is the weather like in kochi'
]
Y_WEATHER = [
  {'intent': 'weather', 'place': 'tokyo'},
  {'intent': 'weather', 'place': 'germany'},
  {'intent': 'weather', 'place': 'kochi'}
]

EX_WEATHER = EntityExtractor()
EX_WEATHER.fit(X_WEATHER, Y_WEATHER)

这是为了检查天气预测的特定位置。我们也可以测试天气的实体提取:

EX_WEATHER.predict('what is the weather in London')

我们询问伦敦的天气,并且我们的实体提取成功地返回了地点名称:

{'intent': 'weather', 'place': 'London'}
  1. ELIZA
from pyowm import OWM

mgr = OWM('YOUR-API-KEY').weather_manager()

def get_weather_forecast(place):
    observation = mgr.weather_at_place(place)
    return observation.get_weather().get_detailed_status()

print(get_weather_forecast('London'))

介绍中提到的原始 ELIZA 有许多语句-响应对,例如以下内容:

overcast clouds

没有问候和日期,没有一个聊天机器人是完整的:

在匹配正则表达式的情况下,会随机选择一种可能的响应,如果需要,动词会进行转换,包括使用如下逻辑进行缩写:

X_GREETING = ['Hii', 'helllo', 'Howdy', 'hey there', 'hey', 'Hi']
Y_GREETING = [{'greet': 'Hii'}, {'greet': 'helllo'}, {'greet': 'Howdy'},
              {'greet': 'hey'}, {'greet': 'hey'}, {'greet': 'Hi'}]
EX_GREETING = EntityExtractor()
EX_GREETING.fit(X_GREETING, Y_GREETING)

X_DATETIME = ['what day is today', 'date today', 'what time is it now', 'time now']
Y_DATETIME = [{'intent' : 'day', 'target': 'today'}, {'intent' : 'date', 'target': 'today'},
              {'intent' : 'time', 'target': 'now'}, {'intent' : 'time', 'target': 'now'}]

EX_DATETIME = EntityExtractor()
EX_DATETIME.fit(X_DATETIME, Y_DATETIME)
  1. “您今天还有其他问题或疑虑需要我帮助您吗?”
_EXTRACTORS = {
  'taxi': None,
  'weather': EX_WEATHER,
  'greetings': EX_GREETING,
  'datetime': EX_DATETIME,
  'music': None
}

Eywa,一个用于对话代理的框架,具有三个主要功能:

import datetime

_EXTRACTORS = {
  'taxi': None,
  'weather': EX_WEATHER,
  'greetings': EX_GREETING,
  'datetime': EX_DATETIME,
  'music': None
}

def question_and_answer(u_query: str):
    q_class = CLF.predict(u_query)
    print(q_class)
    if _EXTRACTORS[q_class] is None:
      return 'Sorry, you have to upgrade your software!'

    q_entities = _EXTRACTORS[q_class].predict(u_query)
    print(q_entities)
    if q_class == 'greetings':
      return q_entities.get('greet', 'hello')

    if q_class == 'weather':
      place = q_entities.get('place', 'London').replace('_', ' ')
      return 'The forecast for {} is {}'.format(
          place,
          get_weather_forecast(place)
      )

    if q_class == 'datetime':
      return 'Today\'s date is {}'.format(
          datetime.datetime.today().strftime('%B %d, %Y')
      )

    return 'I couldn\'t understand what you said. I am sorry.'

在我们详细讨论这些内容之前,看一下介绍中提到的 ELIZA 聊天机器人可能会很有趣。这将希望我们了解需要理解更广泛语言集的改进。

这些是 Jez Higgins 在 GitHub 上的 ELIZA 仿制品的摘录:github.com/jezhiggins/eliza.py

while True:
    query = input('\nHow can I help you?')
    print(question_and_answer(query))

实体提取器 - 从句子中提取命名实体

你应该能够询问不同地方的日期和天气情况,但如果你询问出租车或音乐,它会告诉你需要升级你的软件。

<问候>

对于机器而言,在开始阶段,硬编码一些规则会更容易,但如果您想处理更多复杂性,您将构建解释意图和位置等参考的模型。

这总结了我们的配方。我们实现了一个简单的聊天机器人,首先预测意图,然后基于规则提取实体回答用户查询。

我们省略了呼叫出租车或播放音乐的功能:

“感谢您的来电,我的名字是 _。今天我能为您做什么?”

[r'Is there (.*)', [
    "Do you think there is %1?",
    "It's likely that there is %1.",
    "Would you like there to be %1?"
]],

我们可以使用 Python OpenWeatherMap 库(pyOWM)请求给定位置的天气预报。在撰写本文时,调用新功能get_weather_forecast(),将London作为参数传入,结果如下:

 gReflections = {
  #...
  "i'd" : "you would",
}

ELIZA 是如何工作的?

如果你有兴趣,你应该能够自己实现和扩展这个功能。

让我们基于分类器和实体提取创建一些互动。我们将编写一个响应函数,可以问候,告知日期和提供天气预报:

Eywa

请注意,如果您想执行此操作,您需要使用您自己的(免费)OpenWeatherMap API 密钥。

我们已经为基本任务实现了一个非常简单但有效的聊天机器人。很明显,这可以扩展和定制以处理更多或其他任务。

question_and_answer()函数回答用户查询。

不幸的是,可能与呼叫中心的经历看起来很相似。它们通常也使用脚本,例如以下内容:

这是如何运作的…

  • 如果我们问它问题,我们现在可以与我们的代理进行有限的对话:

  • 我们还需要编码我们对话代理的功能,例如查找天气预报。让我们首先进行天气请求:

  • 模式匹配 – 基于词性和语义意义进行变量匹配

这三者使用起来非常简单,但功能强大。我们在“如何做…”部分看到了前两者的功能。让我们看看基于语义上下文的食品类型模式匹配:

from eywa.nlu import Pattern

p = Pattern('I want to eat [food: pizza, banana, yogurt, kebab]')
p('i\'d like to eat sushi')

我们创建一个名为 food 的变量,并赋予样本值:pizzabananayogurt 和 kebab。在类似上下文中使用食品术语将匹配我们的变量。这个表达式应该返回这个:

{'food' : 'sushi'}

使用看起来与正则表达式非常相似,但正则表达式基于单词及其形态学,eywa.nlu.Pattern 则在语义上锚定在词嵌入中工作。

正则表达式(简称:regex)是定义搜索模式的字符序列。它由 Steven Kleene 首次形式化,并由 Ken Thompson 和其他人在 Unix 工具(如 QED、ed、grep 和 sed)中实现于 1960 年代。这种语法已进入 POSIX 标准,因此有时也称为POSIX 正则表达式。在 1990 年代末,随着 Perl 编程语言的出现,出现了另一种标准,称为Perl 兼容正则表达式PCRE),已在包括 Python 在内的不同编程语言中得到采用。

这些模型如何工作?

首先,eywa 库依赖于来自 explosion.ai 的 sense2vec 词嵌入。sense2vec 词嵌入由 Andrew Trask 和其他人引入(sense2vec – A Fast and Accurate Method for Word Sense Disambiguation In Neural Word Embeddings, 2015)。这个想法被 explosion.ai 接受,他们在 Reddit 讨论中训练了词性消歧的词嵌入。您可以在 explosion.ai 的网站上阅读更多信息:explosion.ai/blog/sense2vec-reloaded

分类器通过存储的对话项目并根据这些嵌入选择具有最高相似度分数的匹配项。请注意,eywa 还有另一个基于递归神经网络的模型实现。

另请参阅

创建聊天机器人的库和框架非常丰富,包括不同的想法和集成方式:

将文本从英语翻译成德语

在这个食谱中,我们将从头开始实现一个 Transformer 网络,并将其用于从英语到德语的翻译任务。在*它是如何工作的…*部分,我们将详细介绍很多细节。

准备就绪

我们建议使用带有GPU的机器。强烈推荐使用 Colab 环境,但请确保您正在使用启用了 GPU 的运行时。如果您想检查是否可以访问 GPU,可以调用 NVIDIA 系统管理接口:

!nvidia-smi

你应该看到类似这样的东西:

Tesla T4: 0MiB / 15079MiB

这告诉您正在使用 NVIDIA Tesla T4,已使用 1.5GB 的 0MB(1MiB 大约相当于 1.049MB)。

我们需要一个相对较新版本的torchtext,这是一个用于pytorch的文本数据集和实用工具库。

!pip install torchtext==0.7.0

对于*还有更多…*部分,您可能需要安装额外的依赖项:

!pip install hydra-core

我们正在使用 spaCy 进行标记化。这在 Colab 中预先安装。在其他环境中,您可能需要pip-install它。我们确实需要安装德语核心功能,例如spacy的标记化,在这个食谱中我们将依赖它:

!python -m spacy download de

我们将在食谱的主要部分加载此功能。

如何做…

在这个食谱中,我们将从头开始实现一个 Transformer 模型,并且将其用于翻译任务的训练。我们从 Ben Trevett 关于使用 PyTorch 和 TorchText 实现 Transformer 序列到序列模型的优秀教程中适应了这个笔记本:github.com/bentrevett/pytorch-seq2seq

我们将首先准备数据集,然后实现 Transformer 架构,接着进行训练,最后进行测试:

  1. 准备数据集 - 让我们预先导入所有必需的模块:
import torch
import torch.nn as nn
import torch.optim as optim

import torchtext
from torchtext.datasets import Multi30k
from torchtext.data import Field, BucketIterator

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

import spacy
import numpy as np

import math

我们将要训练的数据集是 Multi30k 数据集。这是一个包含约 30,000 个平行英语、德语和法语短句子的数据集。

我们将加载spacy功能,实现函数来标记化德语和英语文本:

spacy_de = spacy.load('de')
spacy_en = spacy.load('en')

def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]

这些函数将德语和英语文本从字符串标记化为字符串列表。

Field定义了将文本转换为张量的操作。它提供了常见文本处理工具的接口,并包含一个Vocab,将标记或单词映射到数值表示。我们正在传递我们的前面的标记化方法:

SRC = Field(
    tokenize=tokenize_en, 
    init_token='<sos>', 
    eos_token='<eos>', 
    lower=True, 
    batch_first=True
)

TRG = Field(
    tokenize=tokenize_de, 
    init_token='<sos>', 
    eos_token='<eos>', 
    lower=True, 
    batch_first=True
)

我们将从数据集中创建一个训练-测试-验证拆分。exts参数指定要用作源和目标的语言,fields指定要提供的字段。之后,我们从训练数据集构建词汇表:

train_data, valid_data, test_data = Multi30k.splits(
    exts=('.en', '.de'), 
    fields=(SRC, TRG)
)
SRC.build_vocab(train_data, min_freq=2)
TRG.build_vocab(train_data, min_freq=2)

然后我们可以定义我们的数据迭代器,覆盖训练、验证和测试数据集:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
BATCH_SIZE = 128

train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data), 
     batch_size=BATCH_SIZE,
     device=device
)

现在我们可以在训练此数据集之前构建我们的变压器架构。

  1. 在实施变压器架构时,重要部分是多头注意力和前馈连接。让我们先定义它们,首先从注意力开始:
class MultiHeadAttentionLayer(nn.Module):
    def __init__(self, hid_dim, n_heads, dropout, device):
        super().__init__()
        assert hid_dim % n_heads == 0
        self.hid_dim = hid_dim
        self.n_heads = n_heads
        self.head_dim = hid_dim // n_heads
        self.fc_q = nn.Linear(hid_dim, hid_dim)
        self.fc_k = nn.Linear(hid_dim, hid_dim)
        self.fc_v = nn.Linear(hid_dim, hid_dim)
        self.fc_o = nn.Linear(hid_dim, hid_dim)
        self.dropout = nn.Dropout(dropout)
        self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).to(device)

    def forward(self, query, key, value, mask = None):
        batch_size = query.shape[0]
        Q = self.fc_q(query)
        K = self.fc_k(key)
        V = self.fc_v(value)
        Q = Q.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
        K = K.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
        V = V.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
        energy = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale
        if mask is not None:
            energy = energy.masked_fill(mask == 0, -1e10)
        attention = torch.softmax(energy, dim = -1)
        x = torch.matmul(self.dropout(attention), V)
        x = x.permute(0, 2, 1, 3).contiguous()
        x = x.view(batch_size, -1, self.hid_dim)
        x = self.fc_o(x)
        return x, attention

前馈层只是一个带有非线性激活、dropout 和线性读出的单向传递。第一个投影比原始隐藏维度大得多。在我们的情况下,我们使用隐藏维度为 512 和pf维度为 2048:

class PositionwiseFeedforwardLayer(nn.Module):
    def __init__(self, hid_dim, pf_dim, dropout):
        super().__init__()
        self.fc_1 = nn.Linear(hid_dim, pf_dim)
        self.fc_2 = nn.Linear(pf_dim, hid_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.dropout(torch.relu(self.fc_1(x)))
        x = self.fc_2(x)
        return x

我们需要EncoderDecoder部分,每个部分都有自己的层。然后我们将这两者连接成Seq2Seq模型。

这是编码器的外观:

class Encoder(nn.Module):
    def __init__(self, input_dim, hid_dim,
                 n_layers, n_heads, pf_dim,
                 dropout, device,
                 max_length=100):
        super().__init__()
        self.device = device
        self.tok_embedding = nn.Embedding(input_dim, hid_dim)
        self.pos_embedding = nn.Embedding(max_length, hid_dim)
        self.layers = nn.ModuleList(
            [EncoderLayer(
                hid_dim,
                n_heads,
                pf_dim,
                dropout,
                device
            ) for _ in range(n_layers)])
        self.dropout = nn.Dropout(dropout)
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim])).to(device)

    def forward(self, src, src_mask):
        batch_size = src.shape[0]
        src_len = src.shape[1]
        pos = torch.arange(
            0, src_len
        ).unsqueeze(0).repeat(
            batch_size, 1
        ).to(self.device)
        src = self.dropout(
            (self.tok_embedding(src) * self.scale) +
            self.pos_embedding(pos)
        )
        for layer in self.layers:
            src = layer(src, src_mask)
        return src

它由多个编码器层组成。它们看起来如下所示:

class EncoderLayer(nn.Module):
    def __init__(self, hid_dim, n_heads,
                 pf_dim, dropout, device):
        super().__init__()
        self.self_attn_layer_norm = nn.LayerNorm(hid_dim)
        self.ff_layer_norm = nn.LayerNorm(hid_dim)
        self.self_attention = MultiHeadAttentionLayer(hid_dim, n_heads, dropout, device)
        self.positionwise_feedforward = PositionwiseFeedforwardLayer(
            hid_dim, pf_dim, dropout
        )
        self.dropout = nn.Dropout(dropout)

    def forward(self, src, src_mask):
        _src, _ = self.self_attention(src, src, src, src_mask)
        src = self.self_attn_layer_norm(src + self.dropout(_src))
        _src = self.positionwise_feedforward(src)
        src = self.ff_layer_norm(src + self.dropout(_src))
        return src

解码器与编码器并没有太大的不同,但是它附带了两个多头注意力层。解码器看起来像这样:

class Decoder(nn.Module):
    def __init__(self, output_dim, hid_dim,
                 n_layers, n_heads, pf_dim,
                 dropout, device, max_length=100):
        super().__init__()
        self.device = device
        self.tok_embedding = nn.Embedding(output_dim, hid_dim)
        self.pos_embedding = nn.Embedding(max_length, hid_dim)
        self.layers = nn.ModuleList(
            [DecoderLayer(
                hid_dim, n_heads,
                pf_dim, dropout,
                device
            ) for _ in range(n_layers)]
        )
        self.fc_out = nn.Linear(hid_dim, output_dim)
        self.dropout = nn.Dropout(dropout)
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim])).to(device)

    def forward(self, trg, enc_src, trg_mask, src_mask):
        batch_size = trg.shape[0]
        trg_len = trg.shape[1]
        pos = torch.arange(0, trg_len).unsqueeze(0).repeat(
            batch_size, 1
        ).to(self.device)
        trg = self.dropout(
            (self.tok_embedding(trg) * self.scale) +
            self.pos_embedding(pos)
        )
        for layer in self.layers:
            trg, attention = layer(trg, enc_src, trg_mask, src_mask)
        output = self.fc_out(trg)
        return output, attention

在序列中,解码器层执行以下任务:

  • 带掩码的自注意力

  • 前馈

  • 退出率

  • 残差连接

  • 标准化

自注意力层中的掩码是为了避免模型在预测中包含下一个标记(这将是作弊)。

让我们实现解码器层:

class DecoderLayer(nn.Module):
    def __init__(self, hid_dim, n_heads,
                 pf_dim, dropout, device):
        super().__init__()
        self.self_attn_layer_norm = nn.LayerNorm(hid_dim)
        self.enc_attn_layer_norm = nn.LayerNorm(hid_dim)
        self.ff_layer_norm = nn.LayerNorm(hid_dim)
        self.self_attention = MultiHeadAttentionLayer(hid_dim, n_heads, dropout, device)
        self.encoder_attention = MultiHeadAttentionLayer(hid_dim, n_heads, dropout, device)
        self.positionwise_feedforward = PositionwiseFeedforwardLayer(
            hid_dim, pf_dim, dropout
        )
        self.dropout = nn.Dropout(dropout)

    def forward(self, trg, enc_src, trg_mask, src_mask):
        _trg, _ = self.self_attention(trg, trg, trg, trg_mask)
        trg = self.self_attn_layer_norm(trg + self.dropout(_trg))
        _trg, attention = self.encoder_attention(trg, enc_src, enc_src, src_mask)
        trg = self.enc_attn_layer_norm(trg + self.dropout(_trg))
        _trg = self.positionwise_feedforward(trg)
        trg = self.ff_layer_norm(trg + self.dropout(_trg))
        return trg, attention

最后,在Seq2Seq模型中一切都汇聚在一起:

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder,
                 src_pad_idx, trg_pad_idx, device):
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_pad_idx = src_pad_idx
        self.trg_pad_idx = trg_pad_idx
        self.device = device

    def make_src_mask(self, src):
        src_mask = (src != self.src_pad_idx).unsqueeze(1).unsqueeze(2)
        return src_mask

    def make_trg_mask(self, trg):
        trg_pad_mask = (trg != self.trg_pad_idx).unsqueeze(1).unsqueeze(2)
        trg_len = trg.shape[1]
        trg_sub_mask = torch.tril(torch.ones((trg_len, trg_len), device=self.device)).bool()
        trg_mask = trg_pad_mask & trg_sub_mask
        return trg_mask

    def forward(self, src, trg):
        src_mask = self.make_src_mask(src)
        trg_mask = self.make_trg_mask(trg)
        enc_src = self.encoder(src, src_mask)
        output, attention = self.decoder(trg, enc_src, trg_mask, src_mask)
        return output, attention

现在我们可以用实际参数实例化我们的模型:

INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
HID_DIM = 256
ENC_LAYERS = 3
DEC_LAYERS = 3
ENC_HEADS = 8
DEC_HEADS = 8
ENC_PF_DIM = 512
DEC_PF_DIM = 512
ENC_DROPOUT = 0.1
DEC_DROPOUT = 0.1

enc = Encoder(INPUT_DIM, 
    HID_DIM, ENC_LAYERS, 
    ENC_HEADS, ENC_PF_DIM, 
    ENC_DROPOUT, device
)

dec = Decoder(OUTPUT_DIM, 
    HID_DIM, DEC_LAYERS, 
    DEC_HEADS, DEC_PF_DIM, 
    DEC_DROPOUT, device
)
SRC_PAD_IDX = SRC.vocab.stoi[SRC.pad_token]
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]

model = Seq2Seq(enc, dec, SRC_PAD_IDX, TRG_PAD_IDX, device).to(device)

整个模型共有 9,543,087 个可训练参数。

  1. 训练翻译模型时,我们可以使用 Xavier 均匀归一化来初始化参数:
def initialize_weights(m):
    if hasattr(m, 'weight') and m.weight.dim() > 1:
        nn.init.xavier_uniform_(m.weight.data)

model.apply(initialize_weights);

我们需要将学习率设置得比默认值低得多:

LEARNING_RATE = 0.0005

optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)

在我们的损失函数CrossEntropyLoss中,我们必须确保忽略填充的标记:

criterion = nn.CrossEntropyLoss(ignore_index=TRG_PAD_IDX)

我们的训练函数如下所示:

def train(model, iterator, optimizer, criterion, clip):
    model.train()
    epoch_loss = 0
    for i, batch in enumerate(iterator):
        src = batch.src
        trg = batch.trg
        optimizer.zero_grad()
        output, _ = model(src, trg[:, :-1])
        output_dim = output.shape[-1]
        output = output.contiguous().view(-1, output_dim)
        trg = trg[:,1:].contiguous().view(-1)
        loss = criterion(output, trg)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        epoch_loss += loss.item()
    return epoch_loss / len(iterator)

然后在循环中执行训练:

N_EPOCHS = 10
CLIP = 1
best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)

    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')

我们在这里略微简化了事情。您可以在 GitHub 上找到完整的笔记本。

这个模型训练了 10 个时期。

  1. 测试模型时,我们首先必须编写函数来为模型编码一个句子,并将模型输出解码回句子。然后我们可以运行一些句子并查看翻译。最后,我们可以计算测试集上的翻译性能指标。

为了翻译一个句子,我们必须使用之前创建的源词汇表将其数值化编码,并在将其馈送到我们的模型之前附加停止标记。然后,必须从目标词汇表中解码模型输出:

def translate_sentence(sentence, src_field, trg_field, model, device, max_len=50):
    model.eval()
    if isinstance(sentence, str):
        nlp = spacy.load('en')
        tokens = [token.text.lower() for token in nlp(sentence)]
    else:
        tokens = [token.lower() for token in sentence]
    tokens = [src_field.init_token] + tokens + [src_field.eos_token]
    src_indexes = [src_field.vocab.stoi[token] for token in tokens]
    src_tensor = torch.LongTensor(src_indexes).unsqueeze(0).to(device)
    src_mask = model.make_src_mask(src_tensor)
    with torch.no_grad():
        enc_src = model.encoder(src_tensor, src_mask)
    trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]
    for i in range(max_len):
        trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
        trg_mask = model.make_trg_mask(trg_tensor)
        with torch.no_grad():
            output, attention = model.decoder(trg_tensor, enc_src, trg_mask, src_mask)
        pred_token = output.argmax(2)[:, -1].item()
        trg_indexes.append(pred_token)
        if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:
            break
    trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]
    return trg_tokens[1:], attention

我们可以看一个示例对并检查翻译:

example_idx = 8

src = vars(train_data.examples[example_idx])['src']
trg = vars(train_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')

我们得到了以下对:

src = ['a', 'woman', 'with', 'a', 'large', 'purse', 'is', 'walking', 'by', 'a', 'gate', '.']
trg = ['eine', 'frau', 'mit', 'einer', 'großen', 'geldbörse', 'geht', 'an', 'einem', 'tor', 'vorbei', '.']

我们可以将其与我们模型获得的翻译进行比较:

translation, attention = translate_sentence(src, SRC, TRG, model, device)
print(f'predicted trg = {translation}')

这是我们的翻译句子:

predicted trg = ['eine', 'frau', 'mit', 'einer', 'großen', 'handtasche', 'geht', 'an', 'einem', 'tor', 'vorbei', '.', '<eos>']

我们的翻译实际上比原始翻译好看。钱包(geldbörse)不是真正的钱包,而是一个小包(handtasche)。

然后,我们可以计算我们模型与黄金标准的 BLEU 分数的指标:

from torchtext.data.metrics import bleu_score

def calculate_bleu(data, src_field, trg_field, model, device, max_len=50):
    trgs = []
    pred_trgs = []

    for datum in data:
        src = vars(datum)['src']
        trg = vars(datum)['trg']
        pred_trg, _ = translate_sentence(src, src_field, trg_field, model, device, max_len)

        #cut off <eos> token
        pred_trg = pred_trg[:-1]

        pred_trgs.append(pred_trg)
        trgs.append([trg])

    return bleu_score(pred_trgs, trgs)

bleu_score = calculate_bleu(test_data, SRC, TRG, model, device)

print(f'BLEU score = {bleu_score*100:.2f}')

我们得到了 33.57 的 BLEU 分数,这个分数还不错,同时训练参数更少,训练时间只需几分钟。

在翻译中,一个有用的度量标准是双语评估助手BLEU)分数,其中 1 是最佳值。它是候选翻译部分与参考翻译(黄金标准)部分的比率,其中部分可以是单个词或一个词序列(n-grams)。

这就是我们的翻译模型了。我们可以看到实际上创建一个翻译模型并不是那么困难。然而,其中有很多理论知识,我们将在下一节中进行介绍。

它的工作原理…

在这个示例中,我们为英语到德语的翻译任务从头开始训练了一个变压器模型。让我们稍微了解一下变压器是什么,以及它是如何工作的。

不久之前,长短期记忆网络LSTMs)一直是深度学习模型的主要选择,然而,由于单词是按顺序处理的,训练可能需要很长时间才能收敛。在前面的示例中,我们已经看到递归神经网络如何用于序列处理(请与第九章中的生成旋律食谱进行比较,音频和语音中的深度学习)。在其他示例中,例如在第九章中的识别语音命令食谱中,我们讨论了卷积模型如何替代这些递归网络,以提高速度和预测性能。在自然语言处理中,卷积网络也已经尝试过(例如,Jonas Gehring 等人的卷积序列到序列学习,2017 年),相较于递归模型,速度和预测性能有所改善,然而,变压器架构证明更加强大且更快。

变压器架构最初是为机器翻译而创建的(Ashish Vaswani 等人的注意力机制就是你所需要的,2017 年)。变压器网络摒弃了递归和卷积,训练和预测速度大大加快,因为单词可以并行处理。变压器架构提供了通用的语言模型,在许多任务中推动了技术发展,如神经机器翻译NMT),问答系统QA),命名实体识别NER),文本蕴涵TE),抽象文本摘要等。变压器模型通常被拿来即插即用,并针对特定任务进行微调,以便从长期和昂贵的训练过程中获得的通用语言理解中受益。

Transformers 由两部分组成,类似于自动编码器:

  • 一个编码器 – 它将输入编码为一系列上下文向量(也称为隐藏状态)。

  • 一个解码器 – 它接收上下文向量并将其解码为目标表示。

我们的示例中实现与原始变压器实现(Ashish Vaswani 等人的注意力机制就是你所需要的,2017 年)之间的差异如下:

  • 我们使用了学习的位置编码而不是静态的编码。

  • 我们使用固定的学习率(没有预热和冷却步骤)。

  • 我们不使用标签平滑处理。

这些变化与现代转换器(如 BERT)保持同步。

首先,输入通过嵌入层和位置嵌入层传递,以编码序列中令牌的位置。令牌嵌入通过缩放为外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(隐藏层大小的平方根),并添加到位置嵌入中。最后,应用 dropout 进行正则化。

然后编码器通过堆叠模块传递,每个模块包括注意力、前馈全连接层和归一化。注意力层是缩放的乘法(点积)注意力层的线性组合(多头注意力)。

一些转换器架构只包含其中的一部分。例如,OpenAI GPT 转换器架构(Alec Radfor 等人,《通过生成预训练改进语言理解》,2018 年),生成了非常连贯的文本,由堆叠的解码器组成,而 Google 的 BERT 架构(Jacob Devlin 等人,《BERT: 深度双向转换器的预训练用于语言理解》,2019 年),也由堆叠的编码器组成。

还有更多…

Torch 和 TensorFlow 都有预训练模型的存储库。我们可以从 Torch Hub 下载一个翻译模型并立即使用它。这就是我们将快速展示的内容。对于pytorch模型,我们首先需要安装一些依赖项:

!pip install fairseq fastBPE sacremoses

完成之后,我们可以下载模型。它非常大,这意味着它会占用大量磁盘空间:

import torch

en2de = torch.hub.load(
    'pytorch/fairseq',
    'transformer.wmt19.en-de',
    checkpoint_file='model1.pt:model2.pt:model3.pt:model4.pt',
    tokenizer='moses',
    bpe='fastbpe'
)
en2de.translate('Machine learning is great!')

我们应该得到这样的输出:

Maschinelles Lernen ist großartig!

这个模型(Nathan Ng 等人,《Facebook FAIR 的 WMT19 新闻翻译任务提交》,2019 年)在翻译方面处于技术领先地位。它甚至在精度(BLEU 分数)上超越了人类翻译。fairseq附带了用于在您自己的数据集上训练翻译模型的教程。

Torch Hub 提供了许多不同的翻译模型,还有通用语言模型。

另请参阅

您可以在哈佛大学自然语言处理组的网站上找到关于转换器架构的指南,包括 PyTorch 代码(以及关于位置编码的解释),它还可以在 Google Colab 上运行:nlp.seas.harvard.edu/2018/04/03/attention.html

OpenAI 的 Lilian Weng 已经写过关于语言建模和转换器模型的文章,并提供了简明清晰的概述:

至于支持翻译任务的库,pytorchtensorflow 都提供预训练模型,并支持在翻译中有用的架构:

最后,OpenNMT 是一个基于 PyTorch 和 TensorFlow 的框架,用于翻译任务,拥有许多教程和预训练模型:opennmt.net/

撰写一本流行小说

我们之前提到过图灵测试,用于判断计算机是否足够智能以欺骗审问者认为它是人类。一些文本生成工具生成的文章可能在外表上看起来有意义,但在科学语言背后缺乏智力价值。

一些人类的文章和言论也可能如此。纳西姆·塔勒布在他的书《随机漫步的傻子》中认为,如果一个人的写作无法与人工生成的文章区分开来(一种逆图灵测试),则可以称其为不聪明。类似地,艾伦·索卡尔在 1996 年的恶作剧文章《超越边界:走向量子引力的转变诠释学》被一位物理学教授故意编造,以揭露缺乏思维严谨和对科学术语的误用。一个可能的结论是,模仿人类可能不是智力进步的正确方向。

OpenAI GPT-3 拥有 1750 亿个参数,显著推动了语言模型领域的发展,学习了物理学的事实,能够基于描述生成编程代码,并能够撰写娱乐性和幽默性的散文。

数百万全球粉丝已经等待 200 多年,想知道伊丽莎白和达西先生的《傲慢与偏见》故事如何继续。在这个配方中,我们将使用基于变压器的模型生成《傲慢与偏见 2》。

准备就绪

Project Gutenberg 是一个数字图书馆(大部分为公有领域电子书),拥有超过 60,000 本书籍,以纯文本、HTML、PDF、EPUB、MOBI 和 Plucker 等格式提供。Project Gutenberg 还列出了最受欢迎的下载:www.gutenberg.org/browse/scores/top

在撰写本文时,简·奥斯汀的浪漫 19 世纪初的小说傲慢与偏见在过去 30 天内下载量最高(超过 47,000 次)。我们将以纯文本格式下载这本书:

!wget -O pride_and_prejudice.txt http://www.gutenberg.org/files/1342/1342-0.txt

我们将文本文件保存为pride_and_prejudice.txt

我们将在 Colab 中工作,您将可以访问 Nvidia T4 或 Nvidia K80 GPU。但是,您也可以使用自己的计算机,使用 GPU 甚至 CPU。

如果您在 Colab 中工作,您需要将您的文本文件上传到您的 Google Drive (drive.google.com),这样您可以从 Colab 访问它。

我们将使用一个称为gpt-2-simple的 OpenAI GPT-2 的包装库,由 BuzzFeed 的数据科学家 Max Woolf 创建和维护:

%tensorflow_version 1.x
!pip install -q gpt-2-simple

此库将使模型对新文本进行微调并在训练过程中显示文本样本变得更加容易。

然后我们可以选择 GPT-2 模型的大小。OpenAI 已经发布了四种大小的预训练模型:

  • (124 百万参数;占用 500 MB)

  • 中等(355 百万参数;1.5 GB)

  • (774 百万)

  • 超大(1,558 百万)

大模型目前无法在 Colab 中进行微调,但可以从预训练模型生成文本。超大模型太大以至于无法加载到 Colab 的内存中,因此既不能进行微调也不能生成文本。尽管较大的模型会取得更好的性能并且具有更多知识,但它们需要更长时间来训练和生成文本。

我们将选择小模型:

import gpt_2_simple as gpt2
gpt2.download_gpt2(model_name='124M')

让我们开始吧!

如何做…

我们已经下载了一本流行小说傲慢与偏见的文本,并将首先对模型进行微调,然后生成类似傲慢与偏见的文本:

  1. 微调模型:我们将加载一个预训练模型,并对我们的文本进行微调。

我们将挂载 Google Drive。gpt-2-simple库提供了一个实用函数:

gpt2.mount_gdrive()

此时,您需要授权 Colab 笔记本访问您的 Google Drive。我们将使用之前上传到 Google Drive 的傲慢与偏见文本文件:

gpt2.copy_file_from_gdrive(file_name)

然后我们可以基于我们下载的文本开始微调:

sess = gpt2.start_tf_sess()

gpt2.finetune(
    sess,
    dataset=file_name,
    model_name='124M',
    steps=1000,
    restore_from='fresh',
    run_name='run1',
    print_every=10,
    sample_every=200,
    save_every=500
)

我们应该看到训练损失在至少几个小时内下降。我们在训练过程中会看到生成文本的样本,例如这个:

she will now make her opinions known to the whole of the family, and
 to all their friends.

 “At a time when so many middle-aged people are moving into
 town, when many couples are making fortunes off each other, when
 many professions of taste are forming in the society, I am
 really anxious to find some time here in the course of the next three
 months to write to my dear Elizabeth, to seek her informed opinion
 on this happy event, and to recommend it to her husband’s conduct as well
 as her own. I often tell people to be cautious when they see
 connections of importance here. What is to be done after my death?
 To go through the world in such a way as to be forgotten?”

 Mr. Bennet replied that he would write again at length to write
 very near to the last lines of his letter. Elizabeth cried
 out in alarm, and while she remained, a sense of shame had
 entered her of her being the less attentive companion she had been
 when she first distinguished her acquaintance. Anxiety increased
 every moment for the other to return to her senses, and
 every opportunity for Mr. Bennet to shine any brighter was given
 by the very first letter.

gpt-2-simple库确实使得训练和继续训练变得非常容易。所有模型检查点都可以存储在 Google Drive 上,因此在运行时超时时它们不会丢失。我们可能需要多次重启,因此始终在 Google Drive 上备份是个好习惯:

gpt2.copy_checkpoint_to_gdrive(run_name='run1')

如果我们希望在 Colab 重新启动后继续训练,我们也可以这样做:

# 1\. copy checkpoint from google drive:
gpt2.copy_checkpoint_from_gdrive(run_name='run1')

# 2\. continue training:
sess = gpt2.start_tf_sess()
gpt2.finetune(
    sess,
    dataset=file_name,
    model_name='124M',
    steps=500,
    restore_from='latest',
    run_name='run1',
    print_every=10,
    overwrite=True,
    sample_every=200,
    save_every=100
)
# 3\. let's backup the model again:
gpt2.copy_checkpoint_to_gdrive(run_name='run1')

现在我们可以生成我们的新小说了。

  1. 写我们的新畅销书:我们可能需要从 Google Drive 获取模型并将其加载到 GPU 中:
gpt2.copy_checkpoint_from_gdrive(run_name='run1')
sess = gpt2.start_tf_sess()
gpt2.load_gpt2(sess, run_name='run1')

请注意,您可能需要再次重启笔记本(Colab),以避免 TensorFlow 变量冲突。

  1. 现在我们可以调用gpt-2-simple中的一个实用函数将文本生成到文件中。最后,我们可以下载该文件:
gen_file = 'gpt2_gentext_{:%Y%m%d_%H%M%S}.txt'.format(datetime.utcnow())

gpt2.generate_to_file(
  sess,
  destination_path=gen_file,
  temperature=0.7,
  nsamples=100,
  batch_size=20
)
files.download(gen_file)

gpt_2_simple.generate() 函数接受一个可选的prefix参数,这是要继续的文本。

傲慢与偏见——传奇继续;阅读文本时,有时可以看到一些明显的连续性缺陷,然而,有些段落令人着迷。我们总是可以生成几个样本,这样我们就可以选择我们小说的继续方式。

工作原理…

在这个示例中,我们使用了 GPT-2 模型来生成文本。这被称为神经故事生成,是神经文本生成的一个子集。简而言之,神经文本生成是构建文本或语言的统计模型,并应用该模型生成更多文本的过程。

XLNet、OpenAI 的 GPT 和 GPT-2、Google 的 Reformer、OpenAI 的 Sparse Transformers 以及其他基于变换器的模型有一个共同点:它们是由于建模选择而具有生成性——它们是自回归而不是自编码的。这种自回归语言生成基于这样的假设:在给定长度为外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的上下文序列时,可以预测令牌的概率:

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

可以通过最小化预测令牌与实际令牌的交叉熵来近似这一过程。例如,LSTM、生成对抗网络GANs)或自回归变换器架构已经用于此目的。

在文本生成中,我们需要做出的一个主要选择是如何抽样,我们有几种选择:

  • 贪婪搜索

  • 束搜索

  • Top-k 抽样

  • Top-p(核心)抽样

在贪婪搜索中,每次选择评分最高的选择,忽略其他选择。相比之下,束搜索(beam search)并行跟踪几个选择的分数,以选择最高分序列,而不是选择高分令牌。Top-k 抽样由 Angela Fan 等人提出(Hierarchical Neural Story Generation, 2018)。在 top-k 抽样中,除了最可能的k个词语外,其他词语都被丢弃。相反,在 top-p(也称为核心抽样)中,选择高分词汇超过概率阈值p,而其他词语则被丢弃。可以结合使用 top-k 和 top-p 以避免低排名词汇。

尽管huggingface transformers库为我们提供了所有这些选择,但是使用gpt-2-simple时,我们可以选择使用 top-k 抽样和 top-p 抽样。

参见

有许多出色的库可以使模型训练或应用现成模型变得更加容易。首先,也许是Hugging Face transformers,这是一个语言理解和生成库,支持 BERT、GPT-2、RoBERTa、XLM、DistilBert、XLNet、T5 和 CTRL 等架构和预训练模型:github.com/huggingface/transformers

Hugging Face transformers 库提供了一些预训练的变换器模型,包括精简版的 GPT-2 模型,该模型在性能上接近 GPT-2,但参数数量减少了约 30%,带来更高的速度和更低的内存及处理需求。你可以从 Hugging Face 的 GitHub 仓库中找到几篇链接的笔记本,描述了文本生成和变换器模型的微调:github.com/huggingface/transformers/tree/master/notebooks#community-notebooks.

此外,Hugging Face 还提供了一个名为Write with Transformers的网站,根据他们的口号,该网站可以自动补全你的思路transformer.huggingface.co/.

在 TensorFlow 文档中,你可以找到关于循环神经网络文本生成的教程:www.tensorflow.org/tutorials/text/text_generation.

这些模型还预装在诸如 textgenrnn 这样的库中:github.com/minimaxir/textgenrnn.

更复杂的基于变换器的模型也可以从 TensorFlow Hub 获取,正如另一个教程所示:www.tensorflow.org/hub/tutorials/wiki40b_lm.

第十一章:生产中的人工智能

在涉及人工智能AI)的系统创建中,实际上 AI 通常只占总工作量的一小部分,而实施的主要部分涉及周围基础设施,从数据收集和验证开始,特征提取,分析,资源管理,到服务和监控(David Sculley 等人,《机器学习系统中隐藏的技术债务》,2015 年)。

在本章中,我们将处理监控和模型版本控制、作为仪表板的可视化以及保护模型免受可能泄露用户数据的恶意黑客攻击。

在本章中,我们将介绍以下配方:

  • 可视化模型结果

  • 为实时决策服务模型

  • 保护模型免受攻击

技术要求

对于 Python 库,我们将使用在 TensorFlow 和 PyTorch 中开发的模型,并在每个配方中应用不同的、更专业的库。

你可以在 GitHub 上找到配方笔记本,网址是github.com/PacktPublishing/Artificial-Intelligence-with-Python-Cookbook/tree/master/chapter11

可视化模型结果

与业务股东的频繁沟通是获取部署 AI 解决方案的关键,并应从思路和前提到决策和发现开始。结果如何传达可以决定在商业环境中成功或失败的关键因素。在这个配方中,我们将为一个机器学习ML)模型构建一个可视化解决方案。

准备工作

我们将在streamlit (www.streamlit.io/)中构建我们的解决方案,并使用来自altair的可视化工具,这是streamlit集成的众多 Python 绘图库之一(还包括matplotlibplotly)。让我们安装streamlitaltair

pip install streamlit altair

在这个配方中,我们不会使用笔记本。因此,在这个代码块中,我们已经省略了感叹号。我们将从终端运行所有内容。

Altair 有一种非常愉快的声明性方法来绘制图形,我们将在配方中看到。Streamlit 是一个创建数据应用程序的框架 - 在浏览器中具有可视化功能的交互式应用程序。

让我们继续构建一个交互式数据应用程序。

如何做…

我们将构建一个简单的应用程序用于模型构建。这旨在展示如何轻松创建一个面向浏览器的视觉交互式应用程序,以向非技术或技术观众展示发现。

作为对streamlit的快速实用介绍,让我们看看如何在 Python 脚本中的几行代码可以提供服务。

Streamlit hello-world

我们将以 Python 脚本形式编写我们的 streamlit 应用程序,而不是笔记本,并且我们将使用 streamlit 执行这些脚本以进行部署。

我们将在我们喜爱的编辑器(例如 vim)中创建一个新的 Python 文件,假设名为streamlit_test.py,并编写以下代码行:

import streamlit as st

chosen_option = st.sidebar.selectbox('Hello', ['A', 'B', 'C'])
st.write(chosen_option)

这将显示一个选择框或下拉菜单,标题为Hello,并在选项ABC之间进行选择。这个选择将存储在变量chosen_option中,在浏览器中可以输出。

我们可以从终端运行我们的简介应用程序如下:

streamlit run streamlit_test.py --server.port=80

服务器端口选项是可选的。

这应该在新标签页或窗口中打开我们的浏览器,显示带有三个选项的下拉菜单。我们可以更改选项,新值将显示出来。

这应该足够作为介绍。现在我们将进入实际的步骤。

创建我们的数据应用

我们数据应用的主要思想是将建模选择等决策纳入我们的应用程序,并观察其后果,无论是用数字总结还是在图表中直观显示。

我们将从实现核心功能开始,如建模和数据集加载,然后创建其界面,首先是侧边栏,然后是主页面。我们将所有代码写入一个单独的 Python 脚本中,可以称之为visualizing_model_results.py

  1. 加载数据集 – 我们将实现数据集加载器:

让我们从一些预备工作开始,如导入:

import numpy as np
import pandas as pd
import altair as alt
import streamlit as st

from sklearn.datasets import (
    load_iris,
    load_wine,
    fetch_covtype
)
from sklearn.model_selection import train_test_split
from sklearn.ensemble import (
    RandomForestClassifier,
    ExtraTreesClassifier,
)
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score
from sklearn.metrics import classification_report

如果您注意到了 hello-world 介绍,您可能会想知道界面如何与 Python 通信。这由 streamlit 处理,每次用户点击某处或输入字段时重新运行您的脚本。

我们需要将数据集加载到内存中。这可能包括下载步骤,对于更大的数据集,下载可能需要很长时间。因此,我们将缓存此步骤到磁盘上,所以不是每次点击按钮时都要下载,我们将从磁盘缓存中检索它:

dataset_lookup = {
    'Iris': load_iris,
    'Wine': load_wine,
    'Covertype': fetch_covtype,
}

@st.cache
def load_data(name):
    iris = dataset_lookup[name]()
    X_train, X_test, y_train, y_test = train_test_split(
        iris.data, iris.target, test_size=0.33, random_state=42
    )
    feature_names = getattr(
        iris, 'feature_names',
        [str(i) for i in range(X_train.shape[1])]
    )
    target_names = getattr(
        iris, 'target_names',
        [str(i) for i in np.unique(iris.target)]
    )
    return (
        X_train, X_test, y_train, y_test,
        target_names, feature_names
    )

这实现了建模和数据集加载器的功能。

请注意使用 streamlit 缓存装饰器,@st.cache。它处理装饰函数(在这种情况下是load_data())的缓存,使得传递给函数的任意数量的参数都将与相关输出一起存储。

这里,数据集加载可能需要一些时间。但是,缓存意味着我们只需加载每个数据集一次,因为随后的数据集将从缓存中检索,因此加载速度会更快。这种缓存功能,可以应用于长时间运行的函数,是使 streamlit 响应更快的核心。

我们正在使用 scikit-learn 数据集 API 下载数据集。由于 scikit-learn 的load_x()类型函数(如load_iris(),主要用于玩具数据集)包括target_namesfeature_names等属性,但是 scikit-learn 的fetch_x()函数(如fetch_covtype())用于更大、更严肃的数据集,我们将为这些分别生成特征和目标名称。

训练过程同样被装饰成可以缓存。但请注意,为了确保缓存与模型类型、数据集以及所有超参数都是唯一的,我们必须包含我们的超参数:

@st.cache
def train_model(dataset_name, model_name, n_estimators, max_depth):
    model = [m for m in models if m.__class__.__name__ == model_name][0]
    with st.spinner('Building a {} model for {} ...'.format(
            model_name, dataset_name
    )):
        return model.fit(X_train, y_train)

建模函数接受模型列表,我们将根据超参数的选择进行更新。我们现在将实施这个选择。

  1. 在侧边栏中呈现关键决策:

在侧边栏中,我们将呈现数据集、模型类型和超参数的选择。让我们首先选择数据集:

st.sidebar.title('Model and dataset selection')
dataset_name = st.sidebar.selectbox(
    'Dataset',
    list(dataset_lookup.keys())
)
(X_train, X_test, y_train, y_test,
 target_names, feature_names) = load_data(dataset_name)

这将在我们在 iris、wine 和 cover type 之间做出选择后加载数据集。

对于模型的超参数,我们将提供滑动条:

n_estimators = st.sidebar.slider(
    'n_estimators',
    1, 100, 25
)
max_depth = st.sidebar.slider(
    'max_depth',
    1, 150, 10
)

最后,我们将再次将模型类型暴露为一个下拉菜单:

models = [
    DecisionTreeClassifier(max_depth=max_depth),
    RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth
    ),
    ExtraTreesClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth
    ),
]
model_name = st.sidebar.selectbox(
    'Model',
    [m.__class__.__name__ for m in models]
)
model = train_model(dataset_name, model_name, n_estimators, max_depth)

最后,在选择后,我们将调用train_model()函数,参数为数据集、模型类型和超参数。

此截图显示了侧边栏的外观:

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

这显示了浏览器中的菜单选项。我们将在浏览器页面的主要部分展示这些选择的结果。

  1. 在主面板上报告分类结果:

在主面板上,我们将展示重要的统计数据,包括分类报告、几个图表,应该能够揭示模型的优势和劣势,以及数据本身的视图,在这里模型错误的决策将被突出显示。

我们首先需要一个标题:

st.title('{model} on {dataset}'.format(
    model=model_name,
    dataset=dataset_name
))

然后我们将展示与我们的建模结果相关的基本统计数据,例如曲线下面积、精度等等:

predictions = model.predict(X_test)
probs = model.predict_proba(X_test)
st.subheader('Model performance in test')
st.write('AUC: {:.2f}'.format(
    roc_auc_score(
        y_test, probs,
        multi_class='ovo' if len(target_names) > 2 else 'raise',
        average='macro' if len(target_names) > 2 else None
    )
))
st.write(
    pd.DataFrame(
        classification_report(
            y_test, predictions,
            target_names=target_names,
            output_dict=True
        )
    )
) 

然后,我们将展示一个混淆矩阵,表格化每个类别的实际和预测标签:

test_df = pd.DataFrame(
    data=np.concatenate([
        X_test,
        y_test.reshape(-1, 1),
        predictions.reshape(-1, 1)
    ], axis=1),
    columns=feature_names + [
        'target', 'predicted'
    ]
)
target_map = {i: n for i, n in enumerate(target_names)}
test_df.target = test_df.target.map(target_map)
test_df.predicted = test_df.predicted.map(target_map)
confusion_matrix = pd.crosstab(
    test_df['target'],
    test_df['predicted'],
    rownames=['Actual'],
    colnames=['Predicted']
)
st.subheader('Confusion Matrix')
st.write(confusion_matrix)

我们还希望能够滚动查看被错误分类的测试数据样本:

def highlight_error(s):
    if s.predicted == s.target:
        return ['background-color: None'] * len(s)
    return ['background-color: red'] * len(s)

if st.checkbox('Show test data'):
    st.subheader('Test data')
    st.write(test_df.style.apply(highlight_error, axis=1))

错误分类的样本将以红色背景突出显示。我们将这种原始数据探索设为可选项,需要通过点击复选框激活。

最后,我们将展示变量在散点图中相互绘制的面板图。这部分将使用altair库:

if st.checkbox('Show test distributions'):
    st.subheader('Distributions')
    row_features = feature_names[:len(feature_names)//2]
    col_features = feature_names[len(row_features):]
    test_df_with_error = test_df.copy()
    test_df_with_error['error'] = test_df.predicted == test_df.target
    chart = alt.Chart(test_df_with_error).mark_circle().encode(
            alt.X(alt.repeat("column"), type='quantitative'),
            alt.Y(alt.repeat("row"), type='quantitative'),
            color='error:N'
    ).properties(
            width=250,
            height=250
    ).repeat(
        row=row_features,
        column=col_features
    ).interactive()
    st.write(chart)

这些图中突出显示了错误分类的例子。同样,我们将这部分设为可选项,通过标记复选框激活。

主页上部分用于Covetype数据集的样式如下:

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

您可以看到分类报告和混淆矩阵。在这些内容之下(不在截图范围内),将是数据探索和数据图表。

这结束了我们的演示应用程序。我们的应用程序相对简单,但希望这个方法能作为构建这些应用程序以进行清晰沟通的指南。

工作原理如下…

本书关注于实践学习,我们也推荐这种方式用于 streamlit。在使用 streamlit 时,您可以快速实施更改并查看结果,直到您对所见到的内容满意为止。

如果你愿意,Streamlit 提供了一个本地服务器,可以通过浏览器远程访问。因此,你可以在 Azure、Google Cloud、AWS 或你公司的云上运行你的 Streamlit 应用服务器,并在本地浏览器中查看你的结果。

重要的是理解 Streamlit 的工作流程。小部件的值由 Streamlit 存储。其他值在用户与小部件交互时每次都会根据 Python 脚本从头到尾重新计算。为了避免昂贵的计算,可以使用@st.cache装饰器进行缓存,就像我们在这个示例中看到的那样。

Streamlit 的 API 集成了许多绘图和图表库。这些包括 Matplotlib、Seaborn、Plotly、Bokeh,以及 Altair、Vega Lite、用于地图和 3D 图表的 deck.gl 等交互式绘图库,以及 graphviz 图形。其他集成包括 Keras 模型、SymPy 表达式、pandas DataFrames、图像、音频等。

Streamlit 还配备了多种类型的小部件,如滑块、按钮和下拉菜单。Streamlit 还包括一个可扩展的组件系统,每个组件由 HTML 和 JavaScript 构成的浏览器前端以及 Python 后端组成,能够双向发送和接收信息。现有组件接口进一步与 HiPlot、Echarts、Spacy 和 D3 等库进行集成:www.streamlit.io/components.

你可以玩转不同的输入和输出,可以从头开始,也可以改进这个食谱中的代码。我们可以扩展它以显示不同的结果,构建仪表板,连接到数据库进行实时更新,或者为专业主题专家建立用户反馈表单,例如注释或批准。

参见

AI 和统计可视化是一个广泛的领域。Fernanda Viégas 和 Martin Wattenberg 在 NIPS 2018 上进行了一场名为机器学习可视化的概述演讲,并且你可以找到他们的幻灯片和演讲录像。

这是一些有趣的 Streamlit 演示列表:

除了 Streamlit,还有其他可以帮助创建交互式仪表板、演示和报告的库和框架,比如 Bokeh、Jupyter Voilà、Panel 和 Plotly Dash。

如果您正在寻找具有数据库集成的仪表板和实时图表,诸如 Apache Superset 这样的工具非常方便:superset.apache.org/

为实时决策提供模型服务

AI 专家经常被要求建模、呈现或提出模型。但是,即使解决方案可能在商业上具有影响力,实际上,将概念验证(POC)生产化以进行实时决策实施,往往比最初提出模型更具挑战性。一旦我们基于训练数据创建了模型,并对其进行分析以验证其按预期标准运行,并与利益相关者进行沟通,我们希望使该模型可用,以便为新决策的数据提供预测。这可能意味着满足特定要求,例如延迟(用于实时应用程序)和带宽(用于为大量客户提供服务)。通常,模型部署为诸如推断服务器之类的微服务的一部分。

在这个示例中,我们将从头开始构建一个小型推断服务器,并专注于将人工智能引入生产环境的技术挑战。我们将展示如何通过稳健性、按需扩展、及时响应的软件解决方案将 POC 开发成适合生产的解决方案,并且可以根据需要快速更新。

准备工作

在这个示例中,我们将在终端和 Jupyter 环境之间切换。我们将在 Jupyter 环境中创建和记录模型。我们将从终端控制mlflow服务器。我们会注意哪个代码块适合哪个环境。

我们将在这个示例中使用mlflow。让我们从终端安装它:

pip install mlflow

我们假设您已经安装了 conda。如果没有,请参考《Python 人工智能入门》中的设置 Jupyter 环境一章,以获取详细说明。

我们可以像这样从终端启动我们的本地mlflow服务器,使用sqlite数据库作为后端存储:

mlflow server --backend-store-uri sqlite:///mlflow.db --host 0.0.0.0 --default-artifact-root file://$PWD/mlruns

我们应该看到一条消息,指出我们的服务器正在监听0.0.0.0:5000

这是我们可以从浏览器访问此服务器的地方。在浏览器中,我们可以比较和检查不同的实验,并查看我们模型的指标。

在*更多内容…*部分,我们将快速演示如何使用FastAPI库设置自定义 API。我们也将快速安装此库:

!pip install fastapi

有了这个,我们就准备好了!

如何实现…

我们将从一个逗号分隔值CSV)文件中构建一个简单的模型。我们将尝试不同的建模选项,并进行比较。然后我们将部署这个模型:

  1. 下载和准备数据集:

我们将下载一个数据集作为 CSV 文件并准备进行训练。在这个示例中选择的数据集是葡萄酒数据集,描述了葡萄酒样本的质量。我们将从 UCI ML 存档中下载并读取葡萄酒质量的 CSV 文件:

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split

csv_url =\
    'http://archive.ics.uci.edu/ml/machine-' \
    'learning-databases/wine-quality/winequality-red.csv'
data = pd.read_csv(csv_url, sep=';')

我们将数据分割为训练集和测试集。预测列是quality 列

train_x, test_x, train_y, test_y = train_test_split(
    data.drop(['quality'], axis=1),
    data['quality']
)
  1. 使用不同的超参数进行训练:

我们可以在mlflow服务器中跟踪我们喜欢的任何东西。我们可以创建一个用于性能指标报告的函数,如下所示:

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

def eval_metrics(actual, pred):
    rmse = np.sqrt(mean_squared_error(actual, pred))
    mae = mean_absolute_error(actual, pred)
    r2 = r2_score(actual, pred)
    return rmse, mae, r2

在运行训练之前,我们需要将mlflow库注册到服务器上:

import mlflow

mlflow.set_tracking_uri('http://0.0.0.0:5000')
mlflow.set_experiment('/wine')

我们设置了我们的服务器 URI。我们还可以给我们的实验起个名字。

每次我们使用不同选项运行训练集时,MLflow 都可以记录结果,包括指标、超参数、被 pickled 的模型,以及作为 MLModel 捕获库版本和创建时间的定义。

在我们的训练函数中,我们在训练数据上训练,在测试数据上提取我们的模型指标。我们需要选择适合比较的适当超参数和指标:

from sklearn.linear_model import ElasticNet
import mlflow.sklearn

np.random.seed(40)

def train(alpha=0.5, l1_ratio=0.5):
 with mlflow.start_run():
 lr = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)
 lr.fit(train_x, train_y)
 predicted = lr.predict(test_x)
 rmse, mae, r2 = eval_metrics(test_y, predicted)

        model_name = lr.__class__.__name__
        print('{} (alpha={}, l1_ratio={}):'.format(
            model_name, alpha, l1_ratio
        ))
        print(' RMSE: %s' % rmse)
        print(' MAE: %s' % mae)
        print(' R2: %s' % r2)

        mlflow.log_params({key: value for key, value in lr.get_params().items()})
        mlflow.log_metric('rmse', rmse)
        mlflow.log_metric('r2', r2)
        mlflow.log_metric('mae', mae)
        mlflow.sklearn.log_model(lr, model_name)

我们拟合模型,提取我们的指标,将它们打印到屏幕上,在mlflow服务器上记录它们,并将模型工件存储在服务器上。

为了方便起见,我们在train()函数中暴露了我们的模型超参数。我们选择使用mlflow记录所有超参数。或者,我们也可以仅记录像这样与存储相关的参数:mlflow.log_param('alpha', alpha)

我们还可以计算更多伴随模型的工件,例如变量重要性。

我们还可以尝试使用不同的超参数,例如以下方式:

train(0.5, 0.5)

我们应该得到性能指标作为输出:

ElasticNet (alpha=0.5, l1_ratio=0.5):
  RMSE: 0.7325693777577805
  MAE: 0.5895721434715478
  R2: 0.12163690293641838

在我们使用不同参数多次运行之后,我们可以转到我们的服务器,比较模型运行,并选择一个模型进行部署。

  1. 将模型部署为本地服务器。我们可以在浏览器中比较我们的模型。我们应该能够在服务器的实验选项卡下找到我们的葡萄酒实验。

然后我们可以在概述表中比较不同模型运行,或者获取不同超参数的概述图,例如这样:

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

这个等高线图向我们展示了我们针对平均绝对误差MAE)改变的两个超参数。

我们可以选择一个模型进行部署。我们可以看到我们最佳模型的运行 ID。可以通过命令行将模型部署到服务器,例如像这样:

mlflow models serve -m /Users/ben/mlflow/examples/sklearn_elasticnet_wine/mlruns/1/208e2f5250814335977b265b328c5c49
/artifacts/ElasticNet/

我们可以将数据作为 JSON 传递,例如使用 curl,同样是从终端。这可能看起来像这样:

curl -X POST -H "Content-Type:application/json; format=pandas-split" --data '{"columns":["alcohol", "chlorides", "citric acid", "density", "fixed acidity", "free sulfur dioxide", "pH", "residual sugar", "sulphates", "total sulfur dioxide", "volatile acidity"],"data":[[1.2, 0.231, 0.28, 0.61, 4.5, 13, 2.88, 2.1, 0.26, 63, 0.51]]}' http://127.0.0.1:1234/invocations

有了这个,我们完成了使用mlflow进行模型部署的演示。

工作原理是这样的…

将模型产品化的基本工作流程如下:

  • 在数据上训练模型。

  • 将模型本身打包为可重复使用和可再现的模型格式,以及提取模型预测的胶水代码。

  • 将模型部署在启用您进行预测评分的 HTTP 服务器中。

这通常导致一个通过 JSON 通信的微服务(通常这被称为 RESTful 服务)或 GRPC(通过 Google 的协议缓冲区进行远程过程调用)。这具有将决策智能从后端分离出来,并让 ML 专家完全负责他们的解决方案的优势。

微服务 是一个可以独立部署、维护和测试的单个服务。将应用程序结构化为一组松散耦合的微服务称为微服务架构

另一种方法是将您的模型和粘合代码打包部署到公司现有企业后端中。此集成有几种替代方案:

  • 在诸如预测模型标记语言PMML)等模型交换格式中,这是由数据挖掘组织联合开发的一种格式。

  • 一些软件实现,如 LightGBM、XGBoost 或 TensorFlow,具有多种编程语言的 API,因此可以在 Python 中开发模型,并从诸如 C、C++或 Java 等语言中加载。

  • 重新工程化您的模型:

    • 一些工具可以帮助将决策树等模型转换为 C 或其他语言。

    • 这也可以手动完成。

MLflow 具有命令行、Python、R、Java 和 REST API 接口,用于将模型上传到模型库,记录模型结果(实验),再次下载以便在本地使用,控制服务器等等。它提供了一个服务器,还允许部署到 Azure ML、Amazon Sagemaker、Apache Spark UDF 和 RedisAI。

如果您希望能够远程访问您的mlflow服务器,例如通常在将模型服务器作为独立服务(微服务)时的情况,我们希望将根设置为0.0.0.0,就像我们在示例中所做的那样。默认情况下,本地服务器将在http://127.0.0.1:5000上启动。

如果我们想要访问模型,我们需要从默认的后端存储(这是存储指标的地方)切换到数据库后端,并且我们必须使用 URI 中的协议定义我们的工件存储,例如对于本地mlruns/目录,使用file://$PWD/mlruns。我们已经启用了后端的 SQLite 数据库,这是最简单的方式(但可能不适合生产环境)。我们也可以选择 MySQL、Postgres 或其他数据库后端。

然而,这只是挑战的一部分,因为模型可能变得陈旧或不适用,这些事实只有在我们具备监视部署中模型和服务器性能的能力时才能确认。因此,需要关注监控问题。

监控

在监控 AI 解决方案时,我们特别关注的是操作性或与适当决策相关的指标。前一种类型的指标如下:

  • 延迟 – 在数据上执行预测需要多长时间?

  • 吞吐量 – 我们能在任何时间段内处理多少数据点?

  • 资源使用率 – 在完成推理时,我们分配了多少 CPU、内存和磁盘空间?

以下指标可以作为监控决策过程的一部分:

  • 统计指标,例如在一定时间内预测的平均值和方差

  • 异常值和漂移检测

  • 决策的业务影响

要了解检测异常值的方法,请参阅第三章中的发现异常配方,模式、异常值和推荐

可以从头开始构建独立的监控,类似于本章中可视化模型结果的模板,或者与更专业的监控解决方案集成,如 Prometheus、Grafana 或 Kibana(用于日志监控)。

另请参阅

这是一个非常广泛的话题,在本文档的*工作原理……*部分中提到了许多生产化方面。在 ML 和深度学习DL)模型中有许多竞争激烈的工业级解决方案,考虑到空间限制,我们只能尝试给出一个概述。像往常一样,在本书中,我们主要集中于避免供应商锁定的开源解决方案:

  • MLflow 致力于管理整个 ML 生命周期,包括实验、可复现性、部署和中心化模型注册:mlflow.org/

  • BentoML 创建一个高性能 API 端点,用于提供训练好的模型:github.com/bentoml/bentoml

虽然某些工具只支持一个或少数几个建模框架,但其他工具,特别是 BentoML 和 MLflow,支持部署在所有主要 ML 训练框架下训练的模型。这两者提供了在 Python 中创建的任何东西的最大灵活性,并且它们都具有用于监控的跟踪功能。

我们的配方是从 mlflow 教程示例中调整的。MLflow 在 GitHub 上有更多不同建模框架集成的示例:github.com/mlflow/mlflow/

其他工具包括以下内容:

此外,还有许多库可用于创建自定义微服务。其中最受欢迎的两个库是:

使用这些,您可以创建端点,该端点可以接收像图像或文本这样的数据,并返回预测结果。

保护模型免受攻击

对抗攻击在机器学习中指的是通过输入欺骗模型的行为。这种攻击的示例包括通过改变几个像素向图像添加扰动,从而导致分类器误分类样本,或者携带特定图案的 T 恤以逃避人物检测器(对抗 T 恤)。一种特定的对抗攻击是隐私攻击,其中黑客可以通过成员推理攻击和模型反演攻击获取模型的训练数据集知识,从而可能暴露个人或敏感信息。

在医疗或金融等领域,隐私攻击尤为危险,因为训练数据可能涉及敏感信息(例如健康状态),并且可能可以追溯到个人身份。在本配方中,我们将构建一个免受隐私攻击的模型,因此无法被黑客攻击。

准备工作

我们将实现一个 PyTorch 模型,但我们将依赖由 Nicolas Papernot 和其他人创建和维护的 TensorFlow/privacy 存储库中的脚本。我们将按以下步骤克隆存储库:

!git clone https://github.com/tensorflow/privacy

配方后期,我们将使用分析脚本计算我们模型的隐私边界。

如何操作…

我们必须为教师模型和学生模型定义数据加载器。在我们的情况下,教师和学生架构相同。我们将训练教师,然后从教师响应的聚合训练学生。我们将最终进行隐私分析,执行来自隐私存储库的脚本。

这是从 Diego Muñoz 的 GitHub 笔记本调整而来的:github.com/dimun/pate_torch

  1. 让我们从加载数据开始。我们将使用torch实用程序函数下载数据:
from torchvision import datasets
import torchvision.transforms as transforms

batch_size = 32

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.1307,), (0.3081,))]
)

train_data = datasets.MNIST(
    root='data', train=True,
    download=True,
    transform=transform
)
test_data = datasets.MNIST(
    root='data', train=False,
    download=True,
    transform=transform
)

这将加载 MNIST 数据集,可能需要一段时间。转换将数据转换为torch.FloatTensortrain_datatest_data分别定义了训练和测试数据的加载器。

请参阅第七章中的识别服装项目配方,简要讨论 MNIST 数据集,以及同一章节中的生成图像配方,用于使用该数据集的另一个模型。

请注意,我们将在整个配方中以临时方式定义一些参数。其中包括num_teachersstandard_deviation。您将在*工作原理…*部分看到算法的解释,希望那时这些参数会变得合理。

另一个参数,num_workers,定义了用于数据加载的子进程数。batch_size定义了每批加载的样本数。

我们将为教师定义数据加载器:

num_teachers = 100

def get_data_loaders(train_data, num_teachers=10):
    teacher_loaders = []
    data_size = len(train_data) // num_teachers

    for i in range(num_teachers):
        indices = list(range(i * data_size, (i+1) * data_size))
        subset_data = Subset(train_data, indices)
        loader = torch.utils.data.DataLoader(
            subset_data,
            batch_size=batch_size,
            num_workers=num_workers
        )
        teacher_loaders.append(loader)

    return teacher_loaders

teacher_loaders = get_data_loaders(train_data, num_teachers)

get_data_loaders()函数实现了一个简单的分区算法,返回给定教师所需的数据部分。每个教师模型将获取训练数据的不相交子集。

我们为学生定义一个训练集,包括 9000 个训练样本和 1000 个测试样本。这两个集合都来自教师的测试数据集,作为未标记的训练点 - 将使用教师的预测进行标记:

import torch
from torch.utils.data import Subset

student_train_data = Subset(test_data, list(range(9000)))
student_test_data = Subset(test_data, list(range(9000, 10000)))

student_train_loader = torch.utils.data.DataLoader(
    student_train_data, batch_size=batch_size, 
    num_workers=num_workers
)
student_test_loader = torch.utils.data.DataLoader(
    student_test_data, batch_size=batch_size, 
    num_workers=num_workers
)
  1. 定义模型:我们将为所有教师定义一个单一模型:
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x)

这是用于图像处理的卷积神经网络。请参阅第七章,高级图像应用,了解更多图像处理模型。

让我们为预测创建另一个工具函数,给定一个dataloader

def predict(model, dataloader):
    outputs = torch.zeros(0, dtype=torch.long).to(device)
    model.to(device)
    model.eval()
    for images, labels in dataloader:
        images, labels = images.to(device), labels.to(device)
        output = model.forward(images)
        ps = torch.argmax(torch.exp(output), dim=1)
        outputs = torch.cat((outputs, ps))

    return outputs

现在我们可以开始训练教师。

  1. 训练教师模型:

首先,我们将实现一个训练模型的函数:

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

def train(model, trainloader, criterion, optimizer, epochs=10, print_every=120):
    model.to(device)
    steps = 0
    running_loss = 0
    for e in range(epochs):
        model.train()
        for images, labels in trainloader:
            images, labels = images.to(device), labels.to(device)
            steps += 1         
            optimizer.zero_grad()     
            output = model.forward(images)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()

现在我们准备训练我们的教师:

from tqdm.notebook import trange

def train_models(num_teachers):
    models = []
    for t in trange(num_teachers):
        model = Net()
        criterion = nn.NLLLoss()
        optimizer = optim.Adam(model.parameters(), lr=0.003)
        train(model, teacher_loaders[t], criterion, optimizer)
        models.append(model)
    return models

models = train_models(num_teachers) 

这将实例化并训练每个教师的模型。

  1. 训练学生:

对于学生,我们需要一个聚合函数。您可以在*工作原理…*部分看到聚合函数的解释:

import numpy as np

def aggregated_teacher(models, data_loader, standard_deviation=1.0):
    preds = torch.torch.zeros((len(models), 9000), dtype=torch.long)
    print('Running teacher predictions...')
    for i, model in enumerate(models):
        results = predict(model, data_loader)
        preds[i] = results

    print('Calculating aggregates...')
    labels = np.zeros(preds.shape[1]).astype(int)
    for i, image_preds in enumerate(np.transpose(preds)):
        label_counts = np.bincount(image_preds, minlength=10).astype(float)
        label_counts += np.random.normal(0, standard_deviation, len(label_counts))
        labels[i] = np.argmax(label_counts)

    return preds.numpy(), np.array(labels)

standard_deviation = 5.0
teacher_models = models
preds, student_labels = aggregated_teacher(
    teacher_models,
    student_train_loader,
    standard_deviation
)

aggregated_teacher()函数为所有教师做出预测,计数投票,并添加噪声。最后,它通过argmax返回投票和结果的聚合。

standard_deviation定义了噪声的标准差。这对隐私保证至关重要。

学生首先需要一个数据加载器:

def student_loader(student_train_loader, labels):
    for i, (data, _) in enumerate(iter(student_train_loader)):
        yield data, torch.from_numpy(labels[i*len(data):(i+1)*len(data)])

这个学生数据加载器将被提供聚合的教师标签:

student_model = Net()
criterion = nn.NLLLoss()
optimizer = optim.Adam(student_model.parameters(), lr=0.001)
epochs = 10
student_model.to(device)
steps = 0
running_loss = 0
for e in range(epochs):
    student_model.train()
    train_loader = student_loader(student_train_loader, student_labels)
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        steps += 1
        optimizer.zero_grad()
        output = student_model.forward(images)
        loss = criterion(output, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        # <validation code omitted>

这将运行学生训练。

由于简洁起见,本代码中的某些部分已从训练循环中省略。验证如下所示:

        if steps % 50 == 0:
            test_loss = 0
            accuracy = 0
            student_model.eval()
            with torch.no_grad():
                for images, labels in student_test_loader:
                    images, labels = images.to(device), labels.to(device)
                    log_ps = student_model(images)
                    test_loss += criterion(log_ps, labels).item()

                    ps = torch.exp(log_ps)
                    top_p, top_class = ps.topk(1, dim=1)
                    equals = top_class == labels.view(*top_class.shape)
                    accuracy += torch.mean(equals.type(torch.FloatTensor))
            student_model.train()
            print('Training Loss: {:.3f}.. '.format(running_loss/len(student_train_loader)),
                  'Test Loss: {:.3f}.. '.format(test_loss/len(student_test_loader)),
                  'Test Accuracy: {:.3f}'.format(accuracy/len(student_test_loader)))
            running_loss = 0

最终的训练更新如下:

Epoch: 10/10..  Training Loss: 0.026..  Test Loss: 0.190..  Test Accuracy: 0.952

我们看到这是一个好模型:在测试数据集上准确率为 95.2%。

  1. 分析隐私:

在 Papernot 等人(2018)中,他们详细介绍了如何计算数据相关的差分隐私界限,以估算训练学生的成本。

他们提供了一个基于投票计数和噪声标准差的分析脚本。我们之前克隆了这个仓库,因此可以切换到其中一个目录,并执行分析脚本:

%cd privacy/research/pate_2018/ICLR2018

我们需要将聚合后的教师计数保存为一个 NumPy 文件。然后可以通过分析脚本加载它:

clean_votes = []
for image_preds in np.transpose(preds):
    label_counts = np.bincount(image_preds, minlength=10).astype(float)
    clean_votes.append(label_counts)

clean_votes = np.array(counts)
with open('clean_votes.npy', 'wb') as file_obj:
  np.save(file_obj, clean_votes)

这将counts矩阵放在一起,并将其保存为文件。

最后,我们调用隐私分析脚本:

!python smooth_sensitivity_table.py  --sigma2=5.0 --counts_file=clean_votes.npy --delta=1e-5

隐私保证的ε估计为 34.226(数据独立)和 6.998(数据相关)。ε值本身并不直观,但需要在数据集及其维度的背景下进行解释。

工作原理…

我们从数据集中创建了一组教师模型,然后从这些教师模型中引导出了一个能提供隐私保证的学生模型。在本节中,我们将讨论机器学习中隐私问题的一些背景,差分隐私,以及 PATE 的工作原理。

泄露客户数据可能会给公司带来严重的声誉损失,更不用说因违反数据保护和隐私法(如 GDPR)而遭到监管机构处罚的费用。因此,在数据集的创建和机器学习中考虑隐私问题至关重要。作为一个例子,来自著名的 Netflix 奖数据集的 50 万用户的数据通过与公开可用的亚马逊评论的关联而被重新匿名化。

尽管几列的组合可能泄露特定个体的太多信息,例如,地址或邮政编码再加上年龄,对于试图追踪数据的人来说是一个线索,但是建立在这些数据集之上的机器学习模型也可能不安全。当遭受成员推断攻击和模型反演攻击等攻击时,机器学习模型可能会泄漏敏感信息。

成员攻击 大致上是识别目标模型在训练输入上的预测与在未经训练的输入上的预测之间的差异。您可以从论文针对机器学习模型的成员推断攻击(Reza Shokri 等人,2016)了解更多信息。他们表明,Google 等公司提供的现成模型可能容易受到这些攻击的威胁。

反演攻击中,通过 API 或黑盒访问模型以及一些人口统计信息,可以重建用于训练模型的样本。在一个特别引人注目的例子中,用于训练人脸识别模型的面部被重建了。更令人关注的是,Matthew Fredrikson 等人表明,个性化药物基因组学模型可以暴露个体的敏感基因信息(个性化华法林剂量定制的隐私个案研究;2014)。

差分隐私 (DP) 机制可以防止模型反演和成员攻击。接下来的部分,我们将讨论差分隐私,然后是关于 PATE 的内容。

差分隐私

差分隐私的概念,最初由 Cynthia Dwork 等人在 2006 年提出(在私有数据分析中校准噪声和灵敏度),是机器学习中隐私的金标准。它集中在个体数据点对算法决策的影响上。大致而言,这意味着,模型的任何输出都不会泄露是否包含了个人信息。在差分隐私中,数据会被某种分布的噪声干扰。这不仅可以提供防止隐私攻击的安全性,还可以减少过拟合的可能性。

为了正确解释差分隐私,我们需要介绍邻近数据库(类似数据集)的概念,这是一个只在单个行或者说单个个体上有所不同的数据库。两个数据集,外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,仅在外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传事实上的不同之处。

关键是设定一个上限要求映射(或机制)在相邻数据库上的行为几乎完全相同!

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

这被称为算法的 epsilon-delta 参数化差分隐私,外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,在任何邻近数据库外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传和任何结果外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传子集外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传上。

在这个表述中,epsilon 参数是乘法保证,delta 参数是概率几乎完全准确结果的加法保证。这意味着个人由于其数据被使用而产生的差异隐私成本是最小的。Delta 隐私可以被视为 epsilon 为0的子集或特殊情况,而 epsilon 隐私则是 delta 为0的情况。

这些保证是通过掩盖输入数据中的微小变化来实现的。例如,斯坦利·L·沃纳在 1965 年描述了这种掩盖的简单程序(随机化响应:消除逃避性答案偏差的调查技术)。调查中的受访者对敏感问题如*你有过堕胎吗?*以真实方式或根据硬币翻转决定:

  1. 抛一枚硬币。

  2. 如果反面,真实回答:没有。

  3. 如果正面,再翻一枚硬币,如果正面回答,如果反面回答

这提供了合理的否认能力。

教师集合的私有聚合

基于 Nicolas Papernot 等人(2017)的论文来自私有训练数据的半监督知识转移教师集合的私有聚合PATE)技术依赖于教师的嘈杂聚合。2018 年,Nicolas Papernot 等人(具有 PATE 的可扩展私有学习)改进了 2017 年的框架,提高了组合模型的准确性和隐私性。他们进一步展示了 PATE 框架在大规模、真实世界数据集中的适用性。

PATE 训练遵循这个过程:

  1. 基于不共享训练示例的不同数据集创建了模型集合(教师模型)。

  2. 学生模型是基于查询教师模型的嘈杂聚合决策进行训练。

  3. 只能发布学生模型,而不能发布教师模型。

正如提到的,每个教师都在数据集的不相交子集上训练。直觉上,如果教师们就如何对新的输入样本进行分类达成一致意见,那么集体决策不会透露任何单个训练样本的信息。

查询中的聚合机制包括Gaussian NoisyMaxGNMax),具有高斯噪声的 argmax,如下所定义:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

这有一个数据点 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,类别 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,以及教师 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 在 x 上的投票 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 表示类别 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 的投票计数:

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

直观上,准确性随噪声的方差增加而降低,因此方差必须选择足够紧密以提供良好的性能,但又足够宽以保护隐私。

ε值取决于聚合,特别是噪声水平,还取决于数据集及其维度的上下文。请参阅Jaewoo Lee 和 Chris Clifton 的《多少是足够的?选择差分隐私的敏感性参数》(2011 年),以进行讨论。

另见

关于 DP 概念的详细概述可以在*Cynthia Dwork 和 Aaron Roth 的《差分隐私的算法基础》*中找到。请参阅我们为此配方采用的第二篇 PATE 论文(Nicolas Papernot 等人 2018 年;arxiv.org/pdf/1802.08908.pdf)。

至于与 DP 相关的 Python 库,有许多选择:

TensorFlow 的隐私库包含与 DP 相关的优化器和模型。它还包含使用不同机制的教程,例如 DP 随机梯度下降,DP Adam 优化器或 PATE,适用于成人,IMDB 或其他数据集:github.com/tensorflow/privacy.

有关 TensorFlow 和 PyTorch 的加密 ML 框架:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值