Python 机器学习秘籍第二版(七)
原文:
annas-archive.org/md5/343c5e6c97737f77853e89eacb95df75译者:飞龙
第二十二章:神经网络用于非结构化数据
22.0 介绍
在前一章中,我们专注于适用于结构化数据的神经网络配方,即表格数据。实际上,过去几年中最大的进展大部分涉及使用神经网络和深度学习处理非结构化数据,例如文本或图像。与处理结构化数据源不同,处理这些非结构化数据集有所不同。
深度学习在非结构化数据领域尤为强大,而“经典”的机器学习技术(如提升树)通常无法捕捉文本数据、音频、图像、视频等中存在的所有复杂性和细微差别。在本章中,我们将专门探讨将深度学习用于文本和图像数据。
在文本和图像的监督学习空间中,存在许多子任务或“类型”学习。以下是一些示例(尽管这不是一个全面的列表):
-
文本或图像分类(例如:分类一张照片是否是热狗的图像)
-
迁移学习(例如:使用预训练的上下文模型如 BERT,并在一个任务上进行微调以预测电子邮件是否为垃圾邮件)
-
目标检测(例如:识别和分类图像中的特定对象)
-
生成模型(例如:基于给定输入生成文本的模型,如 GPT 模型)
随着深度学习的普及和越来越普遍,处理这些用例的开源和企业解决方案变得更加易于访问。在本章中,我们将利用几个关键库作为我们进入执行这些深度学习任务的起点。特别是,我们将使用 PyTorch、Torchvision 和 Transformers Python 库来完成跨文本和图像 ML 数据的一系列任务。
22.1 训练神经网络进行图像分类
问题
您需要训练一个图像分类神经网络。
解决方案
在 PyTorch 中使用卷积神经网络:
# Import libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# Define the convolutional neural network architecture
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(64 * 14 * 14, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = nn.functional.relu(self.conv1(x))
x = nn.functional.relu(self.conv2(x))
x = nn.functional.max_pool2d(self.dropout1(x), 2)
x = torch.flatten(x, 1)
x = nn.functional.relu(self.fc1(self.dropout2(x)))
x = self.fc2(x)
return nn.functional.log_softmax(x, dim=1)
# Set the device to run on
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Define the data preprocessing steps
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# Load the MNIST dataset
train_dataset = datasets.MNIST('./data', train=True, download=True,
transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)
# Create data loaders
batch_size = 64
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size,
shuffle=True)
# Initialize the model and optimizer
model = Net().to(device)
optimizer = optim.Adam(model.parameters())
# Compile the model using torch 2.0's optimizer
model = torch.compile(model)
# Define the training loop
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = nn.functional.nll_loss(output, target)
loss.backward()
optimizer.step()
# Define the testing loop
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
# get the index of the max log-probability
test_loss += nn.functional.nll_loss(
output, target, reduction='sum'
).item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
讨论
卷积神经网络通常用于图像识别和计算机视觉任务。它们通常包括卷积层、池化层和全连接层。
卷积层的目的是学习可以用于当前任务的重要图像特征。卷积层通过对图像的特定区域(卷积的大小)应用滤波器来工作。这一层的权重然后学习识别在分类任务中关键的特定图像特征。例如,如果我们在训练一个识别人手的模型,滤波器可能学会识别手指。
池化层的目的通常是从前一层的输入中减少维度。该层还使用应用于输入部分的滤波器,但没有激活。相反,它通过执行最大池化(选择具有最高值的滤波器中的像素)或平均池化(取输入像素的平均值来代替)来减少输入的维度。
最后,全连接层可以与类似 softmax 的激活函数一起用于创建一个二元分类任务。
参见
22.2 训练用于文本分类的神经网络
问题
您需要训练一个神经网络来对文本数据进行分类。
解决方案
使用一个 PyTorch 神经网络,其第一层是您的词汇表的大小:
# Import libraries
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Load the 20 newsgroups dataset
cats = ['alt.atheism', 'sci.space']
newsgroups_data = fetch_20newsgroups(subset='all', shuffle=True,
random_state=42, categories=cats)
# Split the dataset into training and test sets
X_train, X_test, y_train, y_test = train_test_split(newsgroups_data.data,
newsgroups_data.target, test_size=0.2, random_state=42)
# Vectorize the text data using a bag-of-words approach
vectorizer = CountVectorizer(stop_words='english')
X_train = vectorizer.fit_transform(X_train).toarray()
X_test = vectorizer.transform(X_test).toarray()
# Convert the data to PyTorch tensors
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.long)
# Define the model
class TextClassifier(nn.Module):
def __init__(self, num_classes):
super(TextClassifier, self).__init__()
self.fc1 = nn.Linear(X_train.shape[1], 128)
self.fc2 = nn.Linear(128, num_classes)
def forward(self, x):
x = nn.functional.relu(self.fc1(x))
x = self.fc2(x)
return nn.functional.log_softmax(x, dim=1)
# Instantiate the model and define the loss function and optimizer
model = TextClassifier(num_classes=len(cats))
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# Compile the model using torch 2.0's optimizer
model = torch.compile(model)
# Train the model
num_epochs = 1
batch_size = 10
num_batches = len(X_train) // batch_size
for epoch in range(num_epochs):
total_loss = 0.0
for i in range(num_batches):
# Prepare the input and target data for the current batch
start_idx = i * batch_size
end_idx = (i + 1) * batch_size
inputs = X_train[start_idx:end_idx]
targets = y_train[start_idx:end_idx]
# Zero the gradients for the optimizer
optimizer.zero_grad()
# Forward pass through the model and compute the loss
outputs = model(inputs)
loss = loss_function(outputs, targets)
# Backward pass through the model and update the parameters
loss.backward()
optimizer.step()
# Update the total loss for the epoch
total_loss += loss.item()
# Compute the accuracy on the test set for the epoch
test_outputs = model(X_test)
test_predictions = torch.argmax(test_outputs, dim=1)
test_accuracy = accuracy_score(y_test, test_predictions)
# Print the epoch number, average loss, and test accuracy
print(f"Epoch: {epoch+1}, Loss: {total_loss/num_batches}, Test Accuracy:"
"{test_accuracy}")
讨论
不像图像,文本数据本质上是非数值的。在训练模型之前,我们需要将文本转换为模型可以使用的数值表示,以便学习哪些单词和单词组合对于当前分类任务是重要的。在这个例子中,我们使用 scikit-learn 的CountVectorizer将词汇表编码为一个大小为整个词汇表的向量,其中每个单词被分配到向量中的特定索引,该位置的值是该单词在给定段落中出现的次数。在这种情况下,我们可以通过查看我们的训练集来看到词汇表的大小:
X_train.shape[1]
25150
我们在神经网络的第一层使用相同的值来确定输入层的大小:self.fc1 = nn.Linear(X_train.shape[1], 128)。这允许我们的网络学习所谓的词嵌入,即从像本配方中的监督学习任务学习到的单词的向量表示。这个任务将允许我们学习大小为 128 的词嵌入,尽管这些嵌入主要对这个特定的任务和词汇表有用。
22.3 对图像分类进行微调预训练模型
问题
您希望使用从预训练模型中学到的知识来训练图像分类模型。
解决方案
使用transformers库和torchvision在您的数据上对预训练模型进行微调:
# Import libraries
import torch
from torchvision.transforms import(
RandomResizedCrop, Compose, Normalize, ToTensor
)
from transformers import Trainer, TrainingArguments, DefaultDataCollator
from transformers import ViTFeatureExtractor, ViTForImageClassification
from datasets import load_dataset, load_metric, Image
# Define a helper function to convert the images into RGB
def transforms(examples):
examples["pixel_values"] = [_transforms(img.convert("RGB")) for img in
examples["image"]]
del examples["image"]
return examples
# Define a helper function to compute metrics
def compute_metrics(p):
return metric.compute(predictions=np.argmax(p.predictions, axis=1),
references=p.label_ids)
# Load the fashion mnist dataset
dataset = load_dataset("fashion_mnist")
# Load the processor from the VIT model
image_processor = ViTFeatureExtractor.from_pretrained(
"google/vit-base-patch16-224-in21k"
)
# Set the labels from the dataset
labels = dataset['train'].features['label'].names
# Load the pretrained model
model = ViTForImageClassification.from_pretrained(
"google/vit-base-patch16-224-in21k",
num_labels=len(labels),
id2label={str(i): c for i, c in enumerate(labels)},
label2id={c: str(i) for i, c in enumerate(labels)}
)
# Define the collator, normalizer, and transforms
collate_fn = DefaultDataCollator()
normalize = Normalize(mean=image_processor.image_mean,
std=image_processor.image_std)
size = (
image_processor.size["shortest_edge"]
if "shortest_edge" in image_processor.size
else (image_processor.size["height"], image_processor.size["width"])
)
_transforms = Compose([RandomResizedCrop(size), ToTensor(), normalize])
# Load the dataset we'll use with transformations
dataset = dataset.with_transform(transforms)
# Use accuracy as our metric
metric = load_metric("accuracy")
# Set the training args
training_args = TrainingArguments(
output_dir="fashion_mnist_model",
remove_unused_columns=False,
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=0.01,
per_device_train_batch_size=16,
gradient_accumulation_steps=4,
per_device_eval_batch_size=16,
num_train_epochs=1,
warmup_ratio=0.1,
logging_steps=10,
load_best_model_at_end=True,
metric_for_best_model="accuracy",
push_to_hub=False,
)
# Instantiate a trainer
trainer = Trainer(
model=model,
args=training_args,
data_collator=collate_fn,
compute_metrics=compute_metrics,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
tokenizer=image_processor,
)
# Train the model, log and save metrics
train_results = trainer.train()
trainer.save_model()
trainer.log_metrics("train", train_results.metrics)
trainer.save_metrics("train", train_results.metrics)
trainer.save_state()
讨论
在像文本和图像这样的非结构化数据领域,通常会使用在大型数据集上训练过的预训练模型作为起点,而不是从头开始,尤其是在我们没有太多标记数据的情况下。利用来自更大模型的嵌入和其他信息,我们可以调整我们自己的模型以适应新任务,而不需要大量标记信息。此外,预训练模型可能具有我们训练数据中未完全捕获的信息,从而导致整体性能的提升。这个过程被称为迁移学习。
在这个例子中,我们加载了来自 Google 的 ViT(Vision Transformer)模型的权重。然后,我们使用transformers库对其进行微调,以在时尚 MNIST 数据集上进行分类任务,这是一个简单的服装项目数据集。这种方法可以应用于增加任何计算机视觉数据集的性能,并且transformers库提供了一个高级接口,我们可以使用它来微调我们自己的模型,而无需编写大量代码。
参见
22.4 对预训练模型进行文本分类的微调
问题
你想使用预训练模型的学习成果来训练一个文本分类模型。
解决方案
使用transformers库:
# Import libraries
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from transformers import (
AutoModelForSequenceClassification, TrainingArguments, Trainer
)
import evaluate
import numpy as np
# Load the imdb dataset
imdb = load_dataset("imdb")
# Create a tokenizer and collator
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# Tokenize the imdb dataset
tokenized_imdb = imdb.map(
lambda example: tokenizer(
example["text"], padding="max_length", truncation=True
),
batched=True,
)
# User the accuracy metric
accuracy = evaluate.load("accuracy")
# Define a helper function to produce metrics
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=1)
return accuracy.compute(predictions=predictions, references=labels)
# Create dictionaries to map indices to labels and vice versa
id2label = {0: "NEGATIVE", 1: "POSITIVE"}
label2id = {"NEGATIVE": 0, "POSITIVE": 1}
# Load a pretrained model
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2, id2label=id2label,
label2id=label2id
)
# Specify the training arguments
training_args = TrainingArguments(
output_dir="my_awesome_model",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=2,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
# Instantiate a trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_imdb["train"],
eval_dataset=tokenized_imdb["test"],
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
# Train the model
trainer.train()
讨论
就像使用预训练图像模型一样,预训练语言模型包含了大量关于语言的上下文信息,因为它们通常是在各种开放互联网来源上进行训练的。当我们从一个预训练模型基础开始时,我们通常做的是将现有网络的分类层替换为我们自己的分类层。这使我们能够修改已经学习的网络权重以适应我们的特定任务。
在这个例子中,我们正在对一个 DistilBERT 模型进行微调,以识别 IMDB 电影评论是积极的(1)还是消极的(0)。预训练的 DistilBERT 模型为每个单词提供了大量的语境信息,以及从先前的训练任务中学到的神经网络权重。迁移学习使我们能够利用所有用于训练 DistilBERT 模型的初始工作,并将其重新用于我们的用例,即对电影评论进行分类。
参见
第二十三章:保存、加载和提供训练好的模型
23.0 简介
在过去的 22 章和大约 200 个示例中,我们已经涵盖了如何使用机器学习从原始数据创建性能良好的预测模型。然而,为了使我们所有的工作变得有价值,最终我们需要对我们的模型采取行动,比如将其集成到现有的软件应用程序中。为了实现这个目标,我们需要能够在训练后保存我们的模型,在应用程序需要时加载它们,然后请求该应用程序获取预测结果。
机器学习模型通常部署在简单的 Web 服务器上,旨在接收输入数据并返回预测结果。这使得模型能够在同一网络上的任何客户端中使用,因此其他服务(如 UI、用户等)可以实时使用 ML 模型进行预测。例如,在电子商务网站上使用 ML 进行商品搜索时,将提供一个 ML 模型,该模型接收关于用户和列表的数据,并返回用户购买该列表的可能性。搜索结果需要实时可用,并且可供负责接收用户搜索并协调用户结果的电子商务应用程序使用。
23.1 保存和加载 scikit-learn 模型
问题
您有一个训练好的 scikit-learn 模型,想要在其他地方保存和加载它。
解决方案
将模型保存为 pickle 文件:
# Load libraries
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create decision tree classifer object
classifer = RandomForestClassifier()
# Train model
model = classifer.fit(features, target)
# Save model as pickle file
joblib.dump(model, "model.pkl")
['model.pkl']
一旦模型保存完成,我们可以在目标应用程序(例如 Web 应用程序)中使用 scikit-learn 加载该模型:
# Load model from file
classifer = joblib.load("model.pkl")
并使用它进行预测:
# Create new observation
new_observation = [[ 5.2, 3.2, 1.1, 0.1]]
# Predict observation's class
classifer.predict(new_observation)
array([0])
讨论
将模型用于生产环境的第一步是将该模型保存为可以被另一个应用程序或工作流加载的文件。我们可以通过将模型保存为 pickle 文件来实现这一点,pickle 是一种 Python 特定的数据格式,使我们能够序列化 Python 对象并将其写入文件。具体来说,为了保存模型,我们使用 joblib,这是一个扩展 pickle 的库,用于处理我们在 scikit-learn 中经常遇到的大型 NumPy 数组。
在保存 scikit-learn 模型时,请注意保存的模型可能在不同版本的 scikit-learn 之间不兼容;因此,在文件名中包含使用的 scikit-learn 版本可能会有所帮助:
# Import library
import sklearn
# Get scikit-learn version
scikit_version = sklearn.__version__
# Save model as pickle file
joblib.dump(model, "model_{version}.pkl".format(version=scikit_version))
['model_1.2.0.pkl']
23.2 保存和加载 TensorFlow 模型
问题
您有一个训练好的 TensorFlow 模型,想要在其他地方保存和加载它。
解决方案
使用 TensorFlow 的 saved_model 格式保存模型:
# Load libraries
import numpy as np
from tensorflow import keras
# Set random seed
np.random.seed(0)
# Create model with one hidden layer
input_layer = keras.Input(shape=(10,))
hidden_layer = keras.layers.Dense(10)(input_layer)
output_layer = keras.layers.Dense(1)(input_layer)
model = keras.Model(input_layer, output_layer)
model.compile(optimizer="adam", loss="mean_squared_error")
# Train the model
x_train = np.random.random((1000, 10))
y_train = np.random.random((1000, 1))
model.fit(x_train, y_train)
# Save the model to a directory called `save_model`
model.save("saved_model")
32/32 [==============================] - 1s 8ms/step - loss: 0.2056
INFO:tensorflow:Assets written to: saved_model/assets
然后我们可以在另一个应用程序中加载该模型,或用于进一步的训练:
# Load neural network
model = keras.models.load_model("saved_model")
讨论
虽然在本书的整个过程中我们并没有大量使用 TensorFlow,但了解如何保存和加载 TensorFlow 模型仍然是有用的。与使用 Python 原生的 pickle 格式不同,TensorFlow 提供了自己的保存和加载模型的方法。saved_model 格式创建一个存储模型和加载所需所有信息的目录,以便以协议缓冲区格式(使用 .pb 文件扩展名)加载模型并进行预测:
ls saved_model
assets fingerprint.pb keras_metadata.pb saved_model.pb variables
虽然我们不会深入探讨这种格式,但这是在 TensorFlow 中保存、加载和提供训练模型的标准方式。
参见
23.3 保存和加载 PyTorch 模型
问题
如果你有一个训练好的 PyTorch 模型,并希望在其他地方保存和加载它。
解决方案
使用 torch.save 和 torch.load 函数:
# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 5
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# Save the model after it's been trained
torch.save(
{
'epoch': epoch,
'model_state_dict': network.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
},
"model.pt"
)
# Reinitialize neural network
network = SimpleNeuralNet()
state_dict = torch.load(
"model.pt",
map_location=torch.device('cpu')
)["model_state_dict"]
network.load_state_dict(state_dict, strict=False)
network.eval()
SimpleNeuralNet(
(sequential): Sequential(
(0): Linear(in_features=10, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
(5): Dropout(p=0.1, inplace=False)
(6): Sigmoid()
)
)
讨论
尽管我们在 第二十一章 中使用了类似的公式来检查点我们的训练进度,但在这里我们看到相同的方法如何用于将模型加载回内存以进行预测。我们保存模型的 model.pt 实际上只是一个包含模型参数的字典。我们在字典键 model_state_dict 中保存了模型状态;为了将模型加载回来,我们重新初始化我们的网络,并使用 network.load_state_dict 加载模型的状态。
参见
23.4 提供 scikit-learn 模型
问题
你想要使用 Web 服务器提供你训练好的 scikit-learn 模型。
解决方案
构建一个 Python Flask 应用程序,加载本章早期训练的模型:
# Import libraries
import joblib
from flask import Flask, request
# Instantiate a flask app
app = Flask(__name__)
# Load the model from disk
model = joblib.load("model.pkl")
# Create a predict route that takes JSON data, makes predictions, and
# returns them
@app.route("/predict", methods = ["POST"])
def predict():
print(request.json)
inputs = request.json["inputs"]
prediction = model.predict(inputs)
return {
"prediction" : prediction.tolist()
}
# Run the app
if __name__ == "__main__":
app.run()
确保已安装 Flask:
python3 -m pip install flask==2.2.3 joblib==1.2.0 scikit-learn==1.2.0
然后运行应用程序:
python3 app.py
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
现在,我们可以通过向端点提交数据点来对应用程序进行预测,并通过 curl 获取结果:
curl -X POST http://127.0.0.1:5000/predict -H 'Content-Type: application/json'
-d '{"inputs":[[5.1, 3.5, 1.4, 0.2]]}'
{"prediction":[0]}
讨论
在本例中,我们使用了 Flask,这是一个流行的用于构建 Python Web 框架的开源库。我们定义了一个路由 /predict,该路由接受 POST 请求中的 JSON 数据,并返回包含预测结果的字典。尽管这个服务器并非准备用于生产环境(请参阅 Flask 关于使用开发服务器的警告),我们可以很容易地使用更适合生产环境的 Web 框架扩展和提供此代码以将其移至生产环境。
23.5 提供 TensorFlow 模型
问题
你想要使用 Web 服务器提供你训练好的 TensorFlow 模型。
解决方案
使用开源 TensorFlow Serving 框架和 Docker:
docker run -p 8501:8501 -p 8500:8500 \
--mount type=bind,source=$(pwd)/saved_model,target=/models/saved_model/1 \
-e MODEL_NAME=saved_model -t tensorflow/serving
讨论
TensorFlow Serving 是一个针对 TensorFlow 模型优化的开源服务解决方案。通过简单提供模型路径,我们就能获得一个 HTTP 和 gRPC 服务器,并附带开发者所需的额外有用功能。
docker run 命令使用公共 tensorflow/serving 镜像运行容器,并将我们当前工作目录的 saved_model 路径 ($(pwd)/saved_model) 挂载到容器内部的 /models/saved_model/1。这样就会自动将我们之前在本章保存的模型加载到正在运行的 Docker 容器中,我们可以向其发送预测查询。
如果你在网络浏览器中转到 http://localhost:8501/v1/models/saved_model,你应该看到这里显示的 JSON 结果:
{
"model_version_status": [
{
"version": "1",
"state": "AVAILABLE",
"status": {
"error_code": "OK",
"error_message": ""
}
}
]
}
在 http://localhost:8501/v1/models/saved_model/metadata 的 /metadata 路由将返回有关模型的更多信息:
{
"model_spec":{
"name": "saved_model",
"signature_name": "",
"version": "1"
}
,
"metadata": {"signature_def": {
"signature_def": {
"serving_default": {
"inputs": {
"input_8": {
"dtype": "DT_FLOAT",
"tensor_shape": {
"dim": [
{
"size": "-1",
"name": ""
},
{
"size": "10",
"name": ""
}
],
"unknown_rank": false
},
"name": "serving_default_input_8:0"
}
},
"outputs": {
"dense_11": {
"dtype": "DT_FLOAT",
"tensor_shape": {
"dim": [
{
"size": "-1",
"name": ""
},
{
"size": "1",
"name": ""
}
],
"unknown_rank": false
},
"name": "StatefulPartitionedCall:0"
}
},
"method_name": "tensorflow/serving/predict"
},
"__saved_model_init_op": {
"inputs": {},
"outputs": {
"__saved_model_init_op": {
"dtype": "DT_INVALID",
"tensor_shape": {
"dim": [],
"unknown_rank": true
},
"name": "NoOp"
}
},
"method_name": ""
}
}
}
}
}
我们可以使用 curl 向 REST 端点进行预测,并传递变量(此神经网络使用 10 个特征):
curl -X POST http://localhost:8501/v1/models/saved_model:predict
-d '{"inputs":[[1,2,3,4,5,6,7,8,9,10]]}'
{
"outputs": [
[
5.59353495
]
]
}
参见
23.6 在 Seldon 中为 PyTorch 模型提供服务
问题
您希望为实时预测提供经过训练的 PyTorch 模型。
解决方案
使用 Seldon Core Python 包装器提供模型服务:
# Import libraries
import torch
import torch.nn as nn
import logging
# Create a PyTorch model class
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
# Create a Seldon model object with the name `MyModel`
class MyModel(object):
# Loads the model
def __init__(self):
self.network = SimpleNeuralNet()
self.network.load_state_dict(
torch.load("model.pt")["model_state_dict"],
strict=False
)
logging.info(self.network.eval())
# Makes a prediction
def predict(self, X, features_names=None):
return self.network.forward(X)
并使用 Docker 运行它:
docker run -it -v $(pwd):/app -p 9000:9000 kylegallatin/seldon-example
seldon-core-microservice MyModel --service-type MODEL
2023-03-11 14:40:52,277 - seldon_core.microservice:main:578 -
INFO: Starting microservice.py:main
2023-03-11 14:40:52,277 - seldon_core.microservice:main:579 -
INFO: Seldon Core version: 1.15.0
2023-03-11 14:40:52,279 - seldon_core.microservice:main:602 -
INFO: Parse JAEGER_EXTRA_TAGS []
2023-03-11 14:40:52,287 - seldon_core.microservice:main:605 -
INFO: Annotations: {}
2023-03-11 14:40:52,287 - seldon_core.microservice:main:609 -
INFO: Importing MyModel
2023-03-11 14:40:55,901 - root:__init__:25 - INFO: SimpleNeuralNet(
(sequential): Sequential(
(0): Linear(in_features=10, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
(5): Dropout(p=0.1, inplace=False)
(6): Sigmoid()
)
)
2023-03-11 14:40:56,024 - seldon_core.microservice:main:640 -
INFO: REST gunicorn microservice running on port 9000
2023-03-11 14:40:56,028 - seldon_core.microservice:main:655 -
INFO: REST metrics microservice running on port 6000
2023-03-11 14:40:56,029 - seldon_core.microservice:main:665 -
INFO: Starting servers
2023-03-11 14:40:56,029 - seldon_core.microservice:start_servers:80 -
INFO: Using standard multiprocessing library
2023-03-11 14:40:56,049 - seldon_core.microservice:server:432 -
INFO: Gunicorn Config: {'bind': '0.0.0.0:9000', 'accesslog': None,
'loglevel': 'info', 'timeout': 5000, 'threads': 1, 'workers': 1,
'max_requests': 0, 'max_requests_jitter': 0, 'post_worker_init':
<function post_worker_init at 0x7f5aee2c89d0>, 'worker_exit':
functools.partial(<function worker_exit at 0x7f5aee2ca170>,
seldon_metrics=<seldon_core.metrics.SeldonMetrics object at
0x7f5a769f0b20>), 'keepalive': 2}
2023-03-11 14:40:56,055 - seldon_core.microservice:server:504 -
INFO: GRPC Server Binding to 0.0.0.0:5000 with 1 processes.
2023-03-11 14:40:56,090 - seldon_core.wrapper:_set_flask_app_configs:225 -
INFO: App Config: <Config {'ENV': 'production', 'DEBUG': False,
'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None,
'JSON_SORT_KEYS': None, 'JSONIFY_PRETTYPRINT_REGULAR': None,
'JSONIFY_MIMETYPE': None, 'TEMPLATES_AUTO_RELOAD': None,
'MAX_COOKIE_SIZE': 4093}>
2023-03-11 14:40:56,091 - seldon_core.wrapper:_set_flask_app_configs:225 -
INFO: App Config: <Config {'ENV': 'production', 'DEBUG': False,
'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None,
'JSON_SORT_KEYS': None, 'JSONIFY_PRETTYPRINT_REGULAR': None,
'JSONIFY_MIMETYPE': None, 'TEMPLATES_AUTO_RELOAD': None,
'MAX_COOKIE_SIZE': 4093}>
2023-03-11 14:40:56,096 - seldon_core.microservice:_run_grpc_server:466 - INFO:
Starting new GRPC server with 1 threads.
[2023-03-11 14:40:56 +0000] [23] [INFO] Starting gunicorn 20.1.0
[2023-03-11 14:40:56 +0000] [23] [INFO] Listening at: http://0.0.0.0:6000 (23)
[2023-03-11 14:40:56 +0000] [23] [INFO] Using worker: sync
[2023-03-11 14:40:56 +0000] [30] [INFO] Booting worker with pid: 30
[2023-03-11 14:40:56 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2023-03-11 14:40:56 +0000] [1] [INFO] Listening at: http://0.0.0.0:9000 (1)
[2023-03-11 14:40:56 +0000] [1] [INFO] Using worker: sync
[2023-03-11 14:40:56 +0000] [34] [INFO] Booting worker with pid: 34
2023-03-11 14:40:56,217 - seldon_core.gunicorn_utils:load:103 - INFO:
Tracing not active
讨论
虽然我们可以使用多种方式为 PyTorch 模型提供服务,但在这里我们选择了 Seldon Core Python 包装器。Seldon Core 是一个流行的用于在生产环境中为模型提供服务的框架,具有许多有用的功能,使其比 Flask 应用程序更易于使用和更可扩展。它允许我们编写一个简单的类(上面我们使用 MyModel),而 Python 库则负责所有服务器组件和端点。然后我们可以使用 seldon-core-microservice 命令运行服务,该命令启动一个 REST 服务器、gRPC 服务器,甚至公开一个指标端点。要向服务进行预测,我们可以在端口 9000 上调用以下端点:
curl -X POST http://127.0.0.1:9000/predict -H 'Content-Type: application/json'
-d '{"data": {"ndarray":[[0, 0, 0, 0, 0, 0, 0, 0, 0]]}}'
您应该看到以下输出:
{"data":{"names":["t:0","t:1","t:2","t:3","t:4","t:5","t:6","t:7","t:8"],
"ndarray":[[0,0,0,0,0,0,0,0,0]]},"meta":{}}