PaperSpace-博客中文翻译-二-
PaperSpace 博客中文翻译(二)
使用庞氏人工智能在多 GPU 梯度笔记本上训练深度学习分类器
委婉地说,确定运行分布式多 gpu 深度学习脚本的理想框架可能是一个挑战。有各种各样的选择,如 DeepSpeed 、 Horovod 或 Apache Singha ,每一种都有不同的优缺点。许多这些库仍在积极开发和改进中,因此任何超越竞争对手的更好选择都为时过早。因此,选择使用哪个库既需要时间和实践,也需要预知适应库通常不同的方法带来的总体复杂性。
虽然开始可能很难,但是并行和分布式计算库是一个非常强大的工具,其通用性不可低估。处理大数据在计算上非常昂贵,只有在使用深度学习技术时,成本才会增加。特别是,使用大型神经网络架构(如图像生成 GANs)的 DL 任务,在不耗尽内存的情况下,不可能在单个 GPU 上运行。因此,在一台机器上执行许多基线 DL 任务越来越低效。
我们建议您查看的分布式深度学习库之一是来自 HPC-AI Tech 的庞氏人工智能,该库于 2021 年 10 月由卞等人在“庞氏人工智能:用于大规模并行训练的统一深度学习系统”中首次介绍。庞氏人工智能是“一个统一的并行训练系统,旨在无缝集成不同范式的并行化技术,包括数据并行、流水线并行、多张量并行和序列并行” (1) 。庞氏人工智能在简化和促进 PyTorch 深度学习代码适应分布式系统方面做得非常好。
在这篇博文中,我们将看看一个来自巨型人工智能示例报告的样本演示的改编,该报告在梯度笔记本上运行的 CIFAR10 数据集上训练 ResNet34 分类器。所提供的用于执行分布式培训的方法是高度可修改的,因此读者可以期望能够学习本教程并将相同的思想应用到许多不同的应用程序中。
入门指南
转到您选择的团队和项目空间中的渐变笔记本创建页面。在那里,从推荐的选项中选择 PyTorch 运行时,然后向下滚动到机器选择部分。如果您有增长计划,您可以访问几个多 gpu 笔记本,包括 A4000 x 2、A6000 x 2、A6000 x 4 和 A100 GPU x 2 实例的选项。如果你不是,不要担心!庞大的人工智能在单 GPU 和多 GPU 环境中都可以工作。因此,免费用户甚至可以在免费的 GPU 笔记本上学习本教程。
我们用的是 A6000 x 2。完成机器选择后,在切换“高级选项”设置之前,滚动到页面底部。将工作区的 URL 设置为相关的 GitHub repo:https://github.com/hpcaitech/ColossalAI-examples
粘贴完网址后,您可以使用左下角的按钮启动笔记本。这将使你进入跑步笔记本。
一旦进入运行中的笔记本,先去终端(或者在笔记本中使用 line magic)新建一个目录CIFAR10
。我们以后会需要它。接下来,导航到文件夹image/resnet
,点击文件 run_resnet_cifar10_with_engine.py
。当我们调用训练循环时,我们将进行一些编辑以使其运行顺畅(稍后)。
庞大的人工智能方法论
from pathlib import Path
from colossalai.logging import get_dist_logger
import colossalai
import torch
import os
from colossalai.core import global_context as gpc
from colossalai.utils import get_dataloader
from torchvision import transforms
from colossalai.nn.lr_scheduler import CosineAnnealingLR
from torchvision.datasets import CIFAR10
from torchvision.models import resnet34
from tqdm import tqdm
The imports for the ResNet CIFAR10 training script
现在我们已经在脚本中了,我们可以开始浏览代码了。让我们先走一遍用于训练的每个步骤,然后评估文件中描述的数据集。上面是运行这个脚本的相关导入。在运行下面的代码之前,一定要安装庞 AI 和 Torchvision。您可以通过在终端中运行以下命令来轻松安装它们:pip install torchvision colossalai
def main():
colossalai.launch_from_torch(config='./config.py')
logger = get_dist_logger()
# build resnet
model = resnet34(num_classes=10)
从第 15 行开始,我们有了main()
函数,它运行整个训练和评估代码。我们的函数首先通过从 PyTorch 和config.py
文件设置的环境变量中读取 rank 和 world size 来为torch.distributed.launch
创建一个colossalai.launch
的包装器。该配置通过将BATCH_SIZE
指定为 128、NUM_EPOCHS
指定为 2 进行训练,并使用自动混合训练。
然后,在构建 ResNet34 模型之前,我们使用提供的get_dist_logger()
实例化一个记录器。由于我们将在 CIFAR10 上训练数据集,因此类的数量设置为 10。
# build dataloaders
train_dataset = CIFAR10(
root=Path('~/../CIFAR10/'),
download=False,
transform=transforms.Compose(
[
transforms.RandomCrop(size=32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[
0.2023, 0.1994, 0.2010]),
]
)
)
test_dataset = CIFAR10(
root=Path('~/../CIFAR10/'),
train=False,
transform=transforms.Compose(
[
transforms.ToTensor(),
transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[
0.2023, 0.1994, 0.2010]),
]
)
)
train_dataloader = get_dataloader(dataset=train_dataset,
shuffle=True,
batch_size=gpc.config.BATCH_SIZE,
num_workers=1,
pin_memory=True,
)
test_dataloader = get_dataloader(dataset=test_dataset,
add_sampler=False,
batch_size=gpc.config.BATCH_SIZE,
num_workers=1,
pin_memory=True,
)
接下来,我们加载 CIFAR10 的测试和培训部分的数据集。我们选择将 CIFAR10 的root
变量(以及它的目录位置)更改为/notebooks
目录,以便于交互。
在每种情况下,我们将图像转换为张量,并对它们进行归一化。提供的平均值和标准偏差是之前根据 PyTorch CIFAR repo 计算的。完成后,get_dataloader
函数实例化我们将在训练循环中使用的数据加载器。图像被混洗,从前面使用的配置文件读取的批量大小是 128。
# build criterion
criterion = torch.nn.CrossEntropyLoss()
# optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
# lr_scheduler
lr_scheduler = CosineAnnealingLR(optimizer, total_steps=gpc.config.NUM_EPOCHS)
engine, train_dataloader, test_dataloader, _ = colossalai.initialize(model,optimizer,criterion,train_dataloader,test_dataloader,)
我们将使用CrossEntropyLoss()
损失函数作为我们的标准,随机梯度下降作为我们的优化器,初始学习率相对较低,为 0.1。然后,我们将实例化我们的 LR_scheduler,以便在余弦调度上适当地衰减每个时期的学习速率。
最后,我们通过使用 ResNet34 模型、随机梯度下降优化器、交叉熵损失标准以及我们的训练和测试数据加载器来初始化庞大的 AI,从而完成设置。这将输出代表我们的模型和相关数据加载器的引擎,所有这些都在庞大的 AI 分发代码中运行。
for epoch in range(gpc.config.NUM_EPOCHS):
engine.train()
if gpc.get_global_rank() == 0:
train_dl = tqdm(train_dataloader)
else:
train_dl = train_dataloader
for img, label in train_dl:
img = img.cuda()
label = label.cuda()
engine.zero_grad()
output = engine(img)
train_loss = engine.criterion(output, label)
engine.backward(train_loss)
engine.step()
lr_scheduler.step()
engine.eval()
correct = 0
total = 0
for img, label in test_dataloader:
img = img.cuda()
label = label.cuda()
with torch.no_grad():
output = engine(img)
test_loss = engine.criterion(output, label)
pred = torch.argmax(output, dim=-1)
correct += torch.sum(pred == label)
total += img.size(0)
对于训练循环,我们在配置中设置的时期数决定了训练循环将经历的时期数。对于每个时期,我们必须将引擎设置为训练模式。然后,我们将循环设置为显示引擎在该节点上的训练进度,我们可以在它处理混合训练数据时跟踪该进度。对于训练数据加载器中的每个图像和标签,每个图像和标签首先被转换为 CUDA,以便我们可以利用 GPU。
下一步,我们将使用torch.no_grad()
作为我们的上下文管理器,因为禁用梯度计算对推断是有用的,从这我们可以为图像生成引擎的预测输出。这和它的原始标签一起,可以用来计算test_loss
。从那里,我们可以使用argmax
得到我们的预测。最后,我们向正确的张量添加一个反映预测是否等同于标签的布尔值和一个用于准确度计算的总数。
logger.info(
f"Epoch {epoch} - train loss: {train_loss:.5}, test loss: {test_loss:.5}, acc: {correct / total:.5}, lr: {lr_scheduler.get_last_lr()[0]:.5g}", ranks=[0])
为了方便起见,在测试数据加载器中的图像结束时,记录器会将时期、列车损失、测试损失、精确度、学习率和等级输出到日志中。然后重复配置文件中设置的历元数,默认值为 2。
现在,我们已经通读了代码,我们可以运行训练循环,并完全理解在引擎盖下发生了什么!
运行训练循环
A two epoch training cycle leaves us with a mediocre train loss of 1.8014, test loss of 1.4863, acc of 0.3589, and final learning rate of 0
要运行run_resnet_cifar10_with_engine.py
脚本,我们需要做的就是在终端中导航到notebooks/ColossalAI-Examples/image/resnet
并输入以下代码:
python -m torch.distributed.launch --nproc_per_node <number of GPU nodes you want to use> --master_addr localhost --master_port 29500 run_resnet_cifar10_with_engine.py
这将在输出准确度和相关损失度量之前,运行您的训练循环预定次数。我选择为两个运行我的,因为我有两个可用的,但这也将在单个 GPU 实例上运行得很好。
总结想法
由于我们在这里使用的代码非常通用,因此很容易看到相同的方法如何应用于计算机视觉中的许多不同的深度学习应用,如视觉转换器,甚至是不同领域的,如用巨型人工智能和 GPT 进行 NLP。遵循这些脚本中演示的相同工作流,很容易看到庞氏人工智能对 PyTorch 分布式包的原生拥抱如何真正简化了各种深度学习任务适应他们的分布式计算管理系统。
一定要尝试一些其他的例子,或者用渐变笔记本将你自己的个人作品改编成巨大的人工智能!
使用 Keras Functional API 组合多种功能和多种输出
原文:https://blog.paperspace.com/combining-multiple-features-outputs-keras/
我们都写了自己的第一个深度学习代码,用于回归、分类等。使用公共数据集,如 CIFAR-10 、 MNIST 或皮马印第安人糖尿病。
我们可以注意到的一个共同点是,给定项目中每个特性的数据类型都是相同的。我们要么将图像分类,要么将数值输入回归模型。
你有没有想过我们如何组合各种类型的数据,如文本、图像和数字,以获得不止一个输出,而是多个输出,如分类和回归?
现实生活中的问题在形式上不是连续的或同质的。在实践中,您可能需要将多个输入和输出合并到您的深度学习模型中。
本文深入构建一个深度学习模型,该模型接受文本和数字输入,并返回回归和分类输出。
概述
- 数据清理
- 文本预处理
- 神奇的模型
- 结论
数据清理
当我们看一个有多个文本和数字输入的问题,并且要生成一个回归和分类输出时,我们应该首先清理我们的数据集。
首先,我们应该避免包含大量 Null 或 NaN 有值特征的数据。这样的值应该用平均值、中间值等来代替。使得这些记录可以被使用而不会对整体数据产生太大影响。
我们可以将与其他特征相比通常较大的数值转换为较小的值,以确保对神经网络的权重没有影响。为了抵消这种影响,可以使用标准化或最小-最大缩放等技术将数据转换为更小范围的值,同时仍然保持它们之间的关系。
from sklearn.preprocessing import MinMaxScaler
data = [[-1010, 20000], [-50000, 6345], [10000, 1000], [19034, 18200]]
# Instantiate the MinMaxScaler object
scaler = MinMaxScaler()
# Fit it to the data
scaler.fit(data)
# Use transform to output the transformed data
print(scaler.transform(data))
# Output:
[[0.70965032 1\. ]
[0\. 0.28131579]
[0.86913695 0\. ]
[1\. 0.90526316]]
文本预处理
我们可以用两种方式处理多个文本输入
- 为每个文本特征建立专用的 LSTMs(长短期记忆网络),并随后组合来自它的数字输出
- 首先组合文本特征,然后用单个 LSTM 进行训练
在本文中,我们将探讨第二种方法,因为它在处理大量不同长度的文本特征时非常有效。
组合文本特征
原始数据集有多个需要连接的文本要素。仅仅把字符串相加是没有效率的。我们必须与模型沟通,在单个字符串中有不同的特征。
我们处理这种情况的方法是,在它们之间加入一个特殊的<eof>
标签,表示特征结束。这并没有累加任何值,但是,最终,LSTM 会知道这个标签代表了一个特性的结束。最后,所有文本特征将被转换成单个输入。
"Feature 1 <EOF> Feature 2 <EOF> ….. <EOF> Feature N"
现在我们有一个文本输入和一组数字输入。
下部外壳和停止字移除
以下技术在预处理过程中很有用。
小写是将单词转换为小写以提供更好的清晰度的过程。这在预处理过程中以及在我们进行解析的后期阶段是很有帮助的。
停用词去除是去除常用词的过程,以更加关注文本内容的更多特征。我们可以用 NLTK 去掉常规停用词。
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
# Example sentence
text = "there is a stranger out there who seems to be the owner of this mansion"
# Split our sentence at each space using .split()
words = text.split()
text = ""
# For each word, if the word isnt in stop words, then add to our new text variable
for word in words:
if word not in stop_words:
text += (word+" ")
print(text)
Output>> stranger seems owner mansion
词干化和词汇化
这些技术用于改进语义分析。
词干意味着从单词中去掉前缀和后缀以简化单词。这种技术用于在领域分析中确定领域词汇表。它有助于减少一个单词的变体,将它们转换成它们的词根形式。
举个例子,程序、程序、程序员都是程序的变种。
下面是一个使用 NLTK 进行词干提取的示例:
from nltk.stem import PorterStemmer
# Instantiate our stemmer as ps
ps = PorterStemmer()
# Example sentence
sentence = "he is likely to have more likes for the post he posted recently"
# Split our sentence at each space using .split()
words = sentence.split()
sentence = ""
# For each word, get the stem and add to our new sentence variable
for w in words:
sentence += (ps.stem(w) + " ")
print(sentence)
Output >> he is like to have more like for the post he post recent
词汇化是将单词的屈折形式分组的过程。词汇化不是将一个单词简化为它的词干,而是决定该单词对应的词典形式。这有助于模型确定单个单词的含义。
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
sentence = "It is dangerous to jump to feet on rocky surfaces"
words = sentence.split()
sentence = ""
for word in words:
sentence+= (lemmatizer.lemmatize(word) + " ")
print(sentence)
Output >> It is dangerous to jump to foot on rocky surface
列车测试拆分
重要的一步是确保我们对数据集进行适当的采样,并在每个时期后获得足够的数据来测试我们的模型。目前,我们有两个输入和输出,每个都有文本和一个数字输入数组。我们将把它们分为训练集和验证集,如下所示。
from sklearn.model_selection import train_test_split
y_reg_train, y_reg_val, y_clf_train, y_clf_val, X_text_train, X_text_val, X_num_train, X_num_val = train_test_split(y_reg,y_clf,X_text,X_num,test_size=0.25,random_state=42)
符号化
在实际进行任何 NLP 建模之前,我们需要向机器输入数值,让它执行所有这些数学运算。例如,应该将字符串“cat”转换为数字或有意义的张量,以便模型进行处理。记号化通过用数字表示每个单词来帮助我们做到这一点。
from keras.preprocessing.text import Tokenizer
# instantiate our tokenizer
tokenizer = Tokenizer(num_words = 10)
# Create a sample list of words to tokenize
text = ["python","is","cool","but","C++","is","the","classic"]
# Fit the Tokenizer to the words
tokenizer.fit_on_texts(text)
# Create a word_index to track tokens
word_index = tokenizer.word_index
print(word_index['python'])
Output >> 2
在我们的解决方案中,我们必须在训练文本特征上安装标记器。这是预处理中的一个关键点,因为如果我们想防止过度拟合,就不应该让模型或标记器知道我们的测试输入。
我们将把它作为字典存储在 word_index 中。我们可以使用 pickle 保存标记化器,以备将来使用,比如只使用模型进行预测。
让我们看看如何在我们的例子中使用 tokenizer。
# Tokenize and sequence training data
X_text_train_sequences = tokenizer.texts_to_sequences(X_text_train)
# Use pad_sequences to transforms a list (of length num_samples) of sequences (lists of integers) into a 2D Numpy array of shape (num_samples, num_timesteps)
X_text_train_padded = pad_sequences(X_text_train_sequences,maxlen=max_length,
padding=padding_type, truncating=trunction_type)
# Tokenize and sequence validation data
X_text_val_sequences = tokenizer.texts_to_sequences(X_text_val)
# pad_sequences for validation set
X_text_val_padded = pad_sequences(X_text_val_sequences,maxlen=max_length,
padding=padding_type, truncating=trunction_type)
带手套的包埋层
嵌入赋予每个单词维度。想象“King”在我们的记号赋予器中存储为 102。这意味着什么吗?我们需要表达一个单词的维度,嵌入层在这方面帮助了我们。
通常最好使用预先训练好的嵌入层,如 GloVe 来充分利用我们的数据。给定单词的 N 个维度,嵌入将 tokenizer 中的一个单词索引转换成一个大小为(1,N)的矩阵。
注:从这里 下载手套 。
import numpy as np
# Download glove beforehand
# Here we are using the 100 Dimensional embedding of GloVe
f = open('glove.6B/glove.6B.100d.txt')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
# Creating embedding matrix for each word in Our corpus
embedding_matrix = np.zeros((len(word_index) + 1, 100))
for word, i in word_index.items():
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
# words not found in the embedding index will be all-zeros.
embedding_matrix[i] = embedding_vector
现在我们有一个嵌入矩阵作为权重输入到我们的嵌入层中。
神奇的模型
我们已经完成了所有需要的预处理,现在我们有了输入到模型中的 X 和 Y 值。我们将在这里使用 Keras Functional API 来构建这个特殊的模型。
在我们直接进入代码之前,理解为什么顺序模型是不够的是很重要的。初学者应该熟悉序列模型,因为它们可以帮助我们快速构建线性流动模型。
from keras.layers import Dense, Embedding, LSTM
from keras.models import Model
from keras.models import Sequential
# Instantiate a sequential NN
model = Sequential([
Embedding(len(word_index) + 1,
100,
weights=[matrix_embedding],
input_length=max_length,
trainable=False)
LSTM(embedding_dim,),
Dense(100, activation='relu'),
Dense(5, activation='sigmoid')
])
在顺序模型中,我们无法控制输入、输出或流程。顺序模型不能共享层或层的分支,也不能有多个输入或输出。如果我们想要处理多个输入和输出,那么我们必须使用 Keras functional API。
Keras 功能 API
Keras functional API 允许我们精细地构建每一层,部分或全部输入直接连接到输出层,并且能够将任何层连接到任何其他层。像连接值、共享层、分支层以及提供多个输入和输出这样的特性是选择函数式 api 而不是顺序式 API 的最有力的理由。
这里,我们有一个文本输入和一个由九个数字特征组成的数组作为模型的输入,还有两个输出,如前几节所述。 max_length 是我们可以设置的文本输入的最大长度。嵌入矩阵是我们之前得到的嵌入层的权重。
双向 LSTMs
递归神经网络(RNN)是具有内部记忆的前馈神经网络。因为它对每个数据输入执行相同的功能,所以 RNN 本质上是递归的,而当前输入的输出依赖于过去的输入。
双向 LSTM 是一种 RNN,对于长序列具有更好的结果和更好的记忆,保留了时间序列的上下文。双向 lstm 训练输入序列上的两个 lstm,而不是一个 lstm,在如下所示的问题中,通过从两个方向遍历,输入序列的所有时间步长都是可用的。
This is an oversimplified explanation, so I encourage you to read more on this for better clarity. I hope this helps you build a small idea about LSTMs. Source: Bidirectional LSTM-CRF Models for Sequence Tagging
这是我们解决问题的模型架构。
from keras.layers import Dense, Embedding, LSTM, Bidirectional, Input
from keras.models import Model
def make_model(max_length,embedding_matrix):
# Defining the embedding layer
embedding_dim = 64
input1=Input(shape=(max_length,))
embedding_layer = Embedding(len(word_index) + 1,
100,
weights=[embedding_matrix],
input_length=max_length,
trainable=False)(input1)
# Building LSTM for text features
bi_lstm_1 = Bidirectional(LSTM(embedding_dim,return_sequences=True))(embedding_layer)
bi_lstm_2 = Bidirectional(LSTM(embedding_dim))(bi_lstm_1)
lstm_output = Model(inputs = input1,outputs = bi_lstm_2)
#Inputting Number features
input2=Input(shape=(9,))
# Merging inputs
merge = concatenate([lstm_output.output,input2])
# Building dense layers for regression with number features
reg_dense1 = Dense(64, activation='relu')(merge)
reg_dense2 = Dense(16, activation='relu')(reg_dense1)
output1 = Dense(1, activation='sigmoid')(reg_dense2)
# Building dense layers for classification with number features
clf_dense1 = Dense(64, activation='relu')(merge)
clf_dense2 = Dense(16, activation='relu')(clf_dense1)
# 5 Categories in classification
output2 = Dense(5, activation='softmax')(clf_dense2)
model = Model(inputs=[lstm_output.input,input2], outputs=[output1,output2])
return model
模型摘要
Layer (type) Output Shape Param #
Connected to
==============================================================================
input_1 (InputLayer) [(None, 2150)] 0
______________________________________________________________________________
embedding (Embedding) (None, 2150, 100) 1368500
input_1[0][0]
______________________________________________________________________________
bidirectional (Bidirectional) (None, 2150, 128) 84480
embedding[0][0]
______________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 128) 98816
bidirectional[0][0]
______________________________________________________________________________
input_2 (InputLayer) [(None, 9)] 0
______________________________________________________________________________
concatenate (Concatenate) (None, 137) 0
bidirectional_1[0][0]
input_2[0][0]
______________________________________________________________________________
dense (Dense) (None, 64) 8832
concatenate[0][0]
______________________________________________________________________________
dense_3 (Dense) (None, 64) 8832
concatenate[0][0]
______________________________________________________________________________
dense_1 (Dense) (None, 16) 1040
dense[0][0]
______________________________________________________________________________
dense_4 (Dense) (None, 16) 1040
dense_3[0][0]
______________________________________________________________________________
dense_2 (Dense) (None, 1) 17
dense_1[0][0]
______________________________________________________________________________
dense_5 (Dense) (None, 5) 85 dense_4[0][0]
==============================================================================
Total params: 1,571,642
Trainable params: 203,142
Non-trainable params: 1,368,500
考虑到我们有多个输入和输出,模型摘要可能看起来有点吓人。让我们将模型可视化,以获得更全面的了解。
模型可视化
Visualization of Model Architecture
现在,我们剩下要做的就是编译和拟合模型。让我们看看它与正常情况有什么不同。我们可以为模型的输入和输出值输入数组。
import keras
from keras.optimizers import Adam
model = make_model(max_length,embedding_matrix)
# Defining losses for each output
losses ={ 'dense_2':keras.losses.MeanSquaredError(),
'dense_5':keras.losses.CategoricalCrossentropy()}
opt = Adam(lr=0.01)
# Model Compiling
model.compile(optimizer=opt, loss=losses,metrics="accuracy")
# Model Fitting
H = model.fit(x=[X_text_train_padded, X_num_train],
y={'dense_2': y_reg_train, 'dense_5': y_clf_train},
validation_data=([X_text_val_padded, X_num_val],
{'dense_2': y_reg_val, 'dense_5': y_clf_val}), epochs=10,verbose=1)
这就是全部了!
结论
Keras Functional API 帮助我们构建这样健壮而强大的模型,因此可能性是巨大而令人兴奋的。更好地控制输入、输出、层和流有助于人们以高精度和灵活性设计模型。我鼓励大家尝试不同的层、参数和一切可能的东西,以便使用超调充分利用这些特性。
祝你自己的实验好运,感谢你的阅读!
生成对抗网络(GANs)完全指南
机器学习、深度学习和神经网络方面的技术进步和发展已经引领了一个革命性的时代。创建和复制照片、文本、图像和图片仅仅基于一些例子,对一些人来说可能是令人震惊的,对另一些人来说可能是不可思议的。
我们现在正处于一个技术如此先进的阶段,深度学习和神经网络甚至可以从零开始生成逼真的人脸。生成的人脸不属于任何人,无论是活着的还是死去的,然而它们却惊人地逼真。
对于这些成就,我们必须感谢一个特殊的深度学习网络,即生成对抗网络(GAN),这是本文的主题。让我们简要浏览一下目录,以理解我们将涉及的主要主题。
目录
- 甘斯简介
- 理解生成模型和判别模型
- 生成模型的类型
1。变分自动编码器(VAE)2。生成对抗网络 - 鉴别器
- 发电机
- 对培训的深入理解
- 应用程序
- 结论
甘斯简介
生成对抗网络(GANs)是由 Ian Goodfellow 和他的队友在 2014 年开发的。GANs 是一种独特的深度神经网络,可以生成与训练数据相似的新数据。GANs 有两个主要模块,它们相互竞争产生有远见的创作。在本文中,我们将详细了解 gan 是如何工作的。
称为发生器和鉴别器的两个模块之间竞争的更复杂的细节,将在本文后面的章节中进一步讨论。首先,让我们了解一下在科技公司中,生成性对抗网络(GANs)看到的各种现实生活中的用例,强调它们在今天的相关性。
Adobe 将 GANs 用于下一代 Photoshop。Google 利用 GANs 的能力来生成文本和图像。IBM 有效地使用 GANs 进行数据扩充。Snapchat 将它们用于高效的图像过滤器,迪士尼用于超高分辨率。天然气水合物具有多种用途,在当今的世界市场上具有许多优势,其需求在未来几年只会增加。
在本文中,我们的主要目的是直观地理解生成敌对网络的概念。除了覆盖 GANs 的理论方面,我们还将考虑 PyTorch 代码用于每个生成器和鉴别器模型的构建。
理解生成模型和判别模型
机器学习和深度学习中的判别模型起到了分类器的作用。它们通常用于区分两个类别或一组标签。这类任务的几个例子包括区分狗和猫,区分各种狗的品种,或分类各种水果(如苹果、葡萄、橙子等)。).
另一方面,生成模型的工作方式不同于它们的判别模型。在生成模型中,您考虑随机样本(通常是噪声)并从该噪声生成新的真实图像。举个例子,一个生成模型可以从真实的狗的图像中学习,然后创建自己的假的——然而是真实的——狗的图像。有两种主要类型的生成模型,我们将在下一节中进一步讨论。
生成模型的类型
可变自动编码器
Image By Author
有两种主要类型的生成模型,即变分自动编码器和生成对抗网络。虽然我们在本文中的主要焦点仍然是 GANs,但是我们也将在这一节中简要地理解变分自动编码器的工作。
变型自动编码器还利用编码器和解码器,它们通常是独立的神经网络。如上面的框图所示,一个真实的图像通过编码器。编码器的主要功能是在潜在空间中以矢量的形式表示这些真实的图像。
解码器接受这些解释并产生真实图像的副本。最初,产生的图像质量可能很低,但是一旦解码器完全起作用,编码器就可以完全被忽略。可以在潜在空间中引入一些随机噪声样本,并且解码器将生成逼真的图像。
生成对抗网络
Image By Author
这就把我们带到了本文的主要焦点:GANs。首先,让我们对 GANs 有一个直观的了解,并准确理解这些深度学习算法是如何工作的。
GAN 中的生成器和鉴别器相互竞争(因此有术语“对抗性”)。生成器就像一个小偷,其目的是复制和产生真实的数据来欺骗鉴别者;生成器想要绕过它将执行的大量检查。另一方面,鉴别器的作用是充当警察,捕捉生成器出售的人工生成的数据。鉴别器试图捕捉异常并检测由发生器产生的假图像。
换句话说,发生器接受一个嘈杂的输入,并试图从中产生逼真的图像。尽管一开始它非常失败,但它慢慢学会了如何制造更有说服力的赝品。鉴别器块然后试图确定哪些图像是真实的,哪些是伪造的。因此,这些模型相互竞争,直到达到“完美”的程度。
当生成器开始生成多个高质量、逼真的图像并绕过鉴别器的测试阶段时,就达到了完美的程度。一旦图像成功逼真,鉴别器就不能区分真实图像和伪造图像。此后,您可以单独使用生成器,并且不再需要鉴别器。
因为我们已经知道了 GANs 主要由这两个构件组成,即生成器和鉴别器,现在让我们在接下来的章节中更详细地了解它们。总之,这两个模块是独立的神经网络:
- “生成器”是学习生成看起来逼真的假图像(或数据点)的神经网络,当输入到鉴别器网络时,可以作为真实图像传递。
- “鉴别器”是区分真实和虚假图像(或任何类型的数据)的神经网络。鉴别器在生成器的学习过程中起着重要的作用。
鉴别器
Image By Author
鉴别器的作用是区分真假图像。鉴别器基本上充当分类器。
例如,让我们考虑一个分类器,它的作用是识别一个图像是否是一只狗(分类本质上是:“狗”和“不是狗”)。分类器会给我们一个介于 0 和 1 之间的值,表示它对图像是一只狗的置信度。该值越接近 1,图像是狗的概率越高,而接近 0 的值表示相反。
比如:\(P(Y|X) = 0.90\)。
\(P(Y|X)\)格式表示条件概率,其中标签\(Y\)的预测是在给定特征\(X\)的情况下做出的。以上示例中的概率 0.90 美元表示分类器 90%确信图像包含狗。可以将类似的模式应用于鉴别器,以确定(或分类)图像是真的还是假的。
继续分析鉴频器的框图,我们可以简单地确定其工作背后的方法。要素(\(X\))通过鉴别器传递,鉴别器充当分类器,尝试对所提供的要素进行成功预测。鉴别器预测用于计算总成本的结果(\(ŷ\))。
计算成本的过程非常简单,因为我们通过考虑预测产量(在这种情况下为\(ŷ\))和实际结果值或标签(y 美元)来确定成本值(或相应的损失)。在成功计算成本函数之后,我们可以更新鉴别器的相应参数。当我们讨论鉴别器的训练时,我们将讨论这些阶段的确切工作。
让我们看看 PyTorch 中的一些鉴别器代码。这段代码只是一个基本的例子,可以根据用户的选择进行修改。代码参考摘自本课程关于构建基本 GANs 的。我强烈推荐给对自己构建 GANs 感兴趣的读者。
def get_discriminator_block(input_dim, output_dim):
'''
Discriminator Block
Function for returning a neural network of the discriminator given input and output dimensions.
Parameters:
input_dim: the dimension of the input vector, a scalar
output_dim: the dimension of the output vector, a scalar
Returns:
a discriminator neural network layer, with a linear transformation
followed by an nn.LeakyReLU activation with negative slope of 0.2
(https://pytorch.org/docs/master/generated/torch.nn.LeakyReLU.html)
'''
return nn.Sequential(
nn.Linear(input_dim, output_dim),
nn.LeakyReLU(0.2)
)
class Discriminator(nn.Module):
'''
Discriminator Class
Values:
im_dim: the dimension of the images, fitted for the dataset used, a scalar
(MNIST images are 28x28 = 784 so that is your default)
hidden_dim: the inner dimension, a scalar
'''
def __init__(self, im_dim=784, hidden_dim=128):
super(Discriminator, self).__init__()
self.disc = nn.Sequential(
get_discriminator_block(im_dim, hidden_dim * 4),
get_discriminator_block(hidden_dim * 4, hidden_dim * 2),
get_discriminator_block(hidden_dim * 2, hidden_dim),
nn.Linear(hidden_dim, 1)
)
def forward(self, image):
'''
Function for completing a forward pass of the discriminator: Given an image tensor,
returns a 1-dimension tensor representing fake/real.
Parameters:
image: a flattened image tensor with dimension (im_dim)
'''
return self.disc(image)
def get_disc(self):
'''
Returns:
the sequential model
'''
return self.disc
发电机
Image By Author
发生器的作用是创建看起来如此逼真的假图像,以至于鉴别器无法区分真图像和假图像。最初,由于随机噪声变量的引入,并且由于生成器不知道要生成什么,生成器在生成真实图像的工作上非常失败。随着时间的推移,在其参数更新后,它可以慢慢地学习模式,以绕过鉴别器。
从上图所示的框图中,我们可以观察到噪声是通过生成器神经网络传递的,它试图产生一个逼真的输出示例。生成的输出由以下生成的图像的一组特征\(\hat{X}\)组成。
这些特征被馈送到鉴别器,鉴别器预测或分类由发生器产生的当前图像的真假程度。生成器希望输出(\(ŷ\))接近真实图像的 1 美元(这里,\(ŷ\)指的是鉴别器做出的预测)。使用实际输出和鉴别器分类器输出之间的差异,我们可以计算成本,其中\(1\)用于真实图像,而\(0\)用于虚假图像。
计算的成本函数用于更新参数和改进模型。一旦可以实现生成高质量真实图像的模型,就可以保存模型的参数。您可以通过加载并利用这个保存的模型来生成各种输出。每当引入新的噪声向量时,生成器将为训练数据生成更新的图像。
让我们看看 PyTorch 中的一些生成器代码。这些只是示例代码块,可以根据用户的选择进行修改。代码参考摘自本课程关于构建基本 GANs 的。我强烈推荐给对学习如何自己构建 gan 感兴趣的读者。
def get_generator_block(input_dim, output_dim):
'''
Function for returning a block of the generator's neural network
given input and output dimensions.
Parameters:
input_dim: the dimension of the input vector, a scalar
output_dim: the dimension of the output vector, a scalar
Returns:
a generator neural network layer, with a linear transformation
followed by a batch normalization and then a relu activation
'''
return nn.Sequential(
nn.Linear(input_dim, output_dim),
nn.BatchNorm1d(output_dim),
nn.ReLU(inplace=True),
)
class Generator(nn.Module):
'''
Generator Class
Values:
z_dim: the dimension of the noise vector, a scalar
im_dim: the dimension of the images, fitted for the dataset used, a scalar
(MNIST images are 28 x 28 = 784 so that is your default)
hidden_dim: the inner dimension, a scalar
'''
def __init__(self, z_dim=10, im_dim=784, hidden_dim=128):
super(Generator, self).__init__()
# Build the neural network
self.gen = nn.Sequential(
get_generator_block(z_dim, hidden_dim),
get_generator_block(hidden_dim, hidden_dim * 2),
get_generator_block(hidden_dim * 2, hidden_dim * 4),
get_generator_block(hidden_dim * 4, hidden_dim * 8),
nn.Linear(hidden_dim * 8, im_dim),
nn.Sigmoid()
)
def forward(self, noise):
'''
Function for completing a forward pass of the generator: Given a noise tensor,
returns generated images.
Parameters:
noise: a noise tensor with dimensions (n_samples, z_dim)
'''
return self.gen(noise)
def get_gen(self):
'''
Returns:
the sequential model
'''
return self.gen
对培训的深入理解
Image By Author
上面的框图代表了完整的培训结构。在本节的后面,我们将分别关注鉴别器和生成器的训练阶段。然而,现在我们将分析整体结构,并更详细地讨论二元交叉熵(BCE)函数,这将是本文剩余部分的一个基本方面。
二元交叉熵(BCE)对于训练 GANs 非常有用。该函数的主要用途是用于预测真实或虚假数据的分类任务。让我们看看 BCE 的总成本函数,并通过将其分解为两部分来进一步分析。总和(\(σ\))的范围是从\(1\)到\(m\)。在求和符号之后和加法符号之前考虑左边项的计算。右侧包括加法符号之后的所有术语。
价值函数
另类观点
J(Θ) = -1/m * Σ [y(i) logh(x(i), Θ) + (1-y(i)) log(1-h(x(i),Θ))]
在哪里,
- \(-1/m *σ\)代表整批的平均损失。等式开头的负号用于符号化,并始终确保计算的成本大于或等于零。我们的主要目标是降低成本函数以产生更好的结果。
- \(h\) 是所做预测的表示。
- \(y(i)\) 表示计算的标签。\(y(0)\)可以代表伪图像,而\(y(1)\)可以代表真实图像。
- \(x(i)\) 是被计算的特征。
- \(θ\)是需要计算的参数的表示。
LHS: $ y(i) logh(x(i),θ)$
当标签的值为 1 时,即当\(y(i)\)的值为实数(在我们的例子中为\(1\))时,预测的左侧最相关。当\(y(i)\)的值为零时,标签和对数函数的乘积的输出将始终为零。然而,当输出标签的值为 1 时,会出现两种情况。现在让我们分别分析这两种情况。
当日志中的组件产生一个好的预测时,我们会收到一个从\(0\)到\(1\)的正值(通常是一个像\(0.99\)这样的高值)。因此,由于 log 函数,最终输出为\(0\)。与\(y(i)\)值相乘后的值也是\(0\)。因此,我们可以确定,对于一个好的预测,左侧产生的结果是\(0\)。
在错误预测的第二种情况下,即当日志中的组件导致错误预测时,我们会收到一个接近\(0\)的值。因此,由于对数函数,最终输入接近负无穷大。\(y(i)\)与对数函数的乘积也产生一个很大的负值。因此,我们可以确定,对于一个不好的预测,左侧产生一个高负数(无穷大)的结果。
RHS:\((1—y(I))log(1—h(x(I),θ))\)
与左侧类似,当标签的值为零时,即当\(y(i)\)的值为假时(在我们的例子中为\(0\)),预测的右侧最相关。当\(y(i)\)的值为 1 时,标签和对数函数的乘积的输出将总是为 0。然而,当输出标签的值为零时,会出现两种情况。现在让我们分别分析这两种情况。
当日志中的组件产生一个好的预测时,我们会收到一个从\(0\)到\(1\)的正值(通常是一个像\(0.01\)这样的低值)。因此,由于 log 函数,最终输出为\(0\)。与\((1-y(i))\)值相乘后的值也是\(0\)。因此,对于一个好的预测,我们可以确定右边产生的结果是 0 美元。
在错误预测的第二种情况下,即当日志中的组件导致错误预测时,我们会收到一个接近\(0\)的值。由于对数函数,最终输入接近负无穷大。\((1-y(i))\)值与 log 函数的乘积也会产生一个很大的负值。因此,我们可以确定,对于一个不好的预测,右边产生一个高负数(无穷大)。
简单地总结一下这些概念,当做出正确的预测时,我们会得到 0 美元的值,而错误的预测会给出负值(通常为负无穷大),但是等式中的\(-1/m\)部分总是会将这些负值变为正值。
鉴频器相位
Image By Author
上面的框图是生成性对抗网络的鉴别器阶段的训练的表示。通常,鉴别器的参数最初被更新,然后我们进入生成器。我们可以注意到,一个随机噪声样本通过了发生器模块。由于生成器最初不知道要产生的实际值或输出,所以我们会收到一些垃圾值,这些垃圾值被称为生成器的特性\(\hat{X}\)。
在下一步中,生成器\(\hat{X}\)的功能和实际功能\(X\)都通过鉴别器。最初,生成器生成的特征与实际特征相比表现糟糕。鉴别器在训练程序的开始阶段表现也很差。因此,必须相应地更新鉴别器的参数。
将发生器的特性与实际特性进行比较后,鉴频器模块会产生输出\(ŷ\)。如前所述,鉴别器的作用类似于分类器。还有另一个比较步骤,我们再次从鉴频器接收输出,并将其与实际输出进行比较,以计算总成本函数。最后,相应地更新鉴别器的参数。
注意:需要特别注意的是,在鉴频器的训练阶段,只更新鉴频器的参数。在此阶段,发电机的参数保持不变。
换句话说,真实图像和虚假图像都通过 GAN 的鉴别器来计算输出预测,而无需判断哪些图像是真实的或虚假的。预测输出与预测类别的二进制交叉熵(BCE)标签进行比较(真实或虚假——通常,\(1\)用于表示真实图像,而\(0\)表示虚假图像)。最后,在所有这些步骤的计算之后,可以相应地更新鉴别器的参数。
发电机相位
Image By Author
上图代表了发电机阶段的培训。我们可以注意到,在这个阶段,一些噪声被提供给发生器,但是对发生器的参数进行了等效的更新。考虑到从鉴别器接收的反馈,相应地更新参数。在这里,我们还可以注意到,只有特性\(\hat{X}\)用于评估,而实际的特性\(X\)没有。
生成器只对真实图像进行计算。没有假图像被传递给生成器。计算后的成本函数被评估,并且发生器的参数被更新。在这个训练阶段,只有发生器的参数被相应地更新,而鉴别器的参数被忽略。
当 BCE 值等于 real(或\(1\))时,只有在那时才更新生成的参数。发生器和鉴别器都是一次训练一个,它们都是交替训练的。生成器从获得的反馈中学习它的分类是对还是错。
鉴别器和生成器模型应该一起改进。从训练开始,他们就应该保持相似的技能水平。你不会想要一个学习太快的高级鉴别器,因为它在区分真假方面变得好得多,而生成器永远无法学习得足够快,以产生令人信服的假图像。当生成器以更快的速度被训练时,会发生类似的情况,因为这时鉴别器在其任务中失败,并且允许任何随机图像被生成。因此,确保以一致的速度训练发生器和鉴别器至关重要。
氮化镓的应用
- 通过开发基于 GANs 的应用程序(例如 FaceApp )让自己或他人看起来更年轻或更老
- 生成从未存在过的人的真实图片。
- 创作新颖独特的音乐。
- 将低分辨率图像和视频转换为高分辨率。
- 如果缺少数据,为医学扫描和图像创建 X 射线的副本。
- 素描和艺术草图。
- 来自普通图像的动画 gif。
这些敌对网络有无数更多的应用,它们的受欢迎程度目前正在上升,这将导致许多更壮观的应用到来。思考和推测 GANs 将发现或创造的未来创新是令人兴奋的。
Photo by Brigitta Schneiter on Unsplash
结论
在这篇文章中,我们的主要目标是获得对生成性敌对网络(GANs)如何工作的直观理解。GANs 是现代深度学习时代的一个非凡壮举。它们提供了一种独特的方法来创建和生成图像和文本等数据,还可以执行自然图像合成、数据扩充等功能。
让我们快速回顾一下本文中讨论的众多主题。我们做了一个简短的介绍,并了解了对 gan 的实际期望,包括它们在工业中的应用。然后,我们开始理解不同类型的建模,包括判别模型和生成模型。之后,我们重点讨论了两种主要的生成模型,即变分自动编码器和生成对抗网络。
我们分别详细讨论了鉴别器和生成器模块。我们确定了它们在生成新数据中各自的角色和目的。他们本质上扮演了小偷和警察的角色,其中生成器试图创建完美的(尽管是假的)模型,而鉴别器试图区分和分类真实和虚假的数据。我们还查看了使用 PyTorch 实现它们的示例,以便对它们的机制有一个简要的了解。
然后,我们独立理解了发生器和鉴别器的深入培训过程。我们了解了它们各自的参数是如何等效更新的,从而使发生器和鉴别器都达到平衡。他们需要以这样一种方式进行训练,使他们相互制衡,我们需要确保他们都能平等地提高。最后,我们总结了 gan 在现实世界中的各种应用,并发现了它们对我们日常生活的影响。
在 GANs 系列的下一部分,我们将了解更多不同类型的 GANs,并深入研究一类特殊的 GANs,称为深度卷积生成对抗网络(DCGANs)。在那之前,享受练习和学习的乐趣吧!
构建一个 Flask Web 应用程序,使用可变自动编码器压缩图像
原文:https://blog.paperspace.com/compress-images-using-variational-autoencoders-and-flask/
在本教程中,我们将使用 Flask 构建一个 web 应用程序,该应用程序允许用户上传要使用预训练的变分自动编码器(VAE)编码(即压缩)的图像。编码后,用户得到一个由两个元素组成的矢量,代表整个图像。该应用程序还允许用户基于这样的矢量解码(即解压缩)图像。
本教程的大纲如下:
- 预训练变分自动编码器
- 在 Flask 中构建一个简单的 Web 应用程序
- 构建 VAE App 主架构
- App 主界面
- 用于编码图像的 HTML 页面
- 上传和编码图像
- 用于解码图像的 HTML 页面
- 解码图像
- 完全码
预训练的变分自动编码器
变分自动编码器(VAE)在之前名为如何在 Keras 中构建变分自动编码器的教程中介绍过,其中使用 Keras 构建了一个模型来压缩 MNIST 数据集中的图像。编码器网络接受形状为(28, 28)
的整个图像,并将其编码为长度为 2 的潜在向量,从而将每个图像压缩为 2 个元素。然后使用解码器网络对编码器的输出进行解码,该解码器网络接受潜在向量作为输入,并返回图像的重建版本。
上一教程保存了代表以下 3 种模型的 3 个文件,但我们只对编码器和解码器的模型感兴趣:
- 编码器
- 解码器
- VAE
有必要知道如何使用这些模型来编码和解码图像。
给定一个名为test.jpg
的图像,第一步是根据下面几行代码读取它。通过将as_gray
参数设置为True
,确保它被读取为灰度图像。
import skimage.io
img = skimage.io.imread(fname="test.jpg", as_gray=True)
因为我们正在处理形状为(28, 28)
的 MNIST 数据集的图像,所以确保图像形状为(28, 28)
很重要。这里有一个if
的声明。
if img.shape[0] != 28 or img.shape[1] != 28:
print("Image shape must be (28, 28)")
在读取图像之后和编码之前,有一个额外的步骤。编码器模型期望输入是 4D 数组,其中维数按顺序表示以下内容:
- 样本数目
- 样本宽度
- 样本高度
- 频道数量
在我们的应用程序中,我们只对编码单个图像感兴趣,所以样本数是1
。宽度和高度都等于28
。通道的数量是1
,因为 MNIST 图像是二进制的。因此,前 4 个维度的值如下:
- 样本数量:
1
- 样本宽度:
28
- 样品高度:
28
- 通道数量:
1
可以使用 NumPy 创建 4D 数组,如下所示:
test_sample = numpy.zeros(shape=(1, 28, 28, 1))
然后,将先前读取的图像分配给该阵列,如下所示:
test_sample[0, :, :, 0] = img
最后要做的一件事是重新调整像素值,使其落在0-1
范围内,因为这是用来训练模型的。
test_sample = test_sample.astype("float32") / 255.0
现在我们准备读取保存的编码器模型,命名为VAE_encoder.h5
。
encoder = tensorflow.keras.models.load_model("VAE_encoder.h5")
要对图像进行编码,只需调用接受 4D 数组并返回潜在向量的predict()
方法。
latent_vector = encoder.predict(test_sample)
此时,我们能够读取图像,加载编码器,并使用编码器对图像进行编码。下面是完成这些步骤的完整代码。
import skimage.io
import numpy
import tensorflow.keras.models
img = skimage.io.imread(fname="test.jpg", as_gray=True)
if img.shape[0] != 28 or img.shape[1] != 28:
print("Image shape must be (28, 28)")
exit()
test_sample = numpy.zeros(shape=(1, 28, 28, 1))
test_sample[0, :, :, 0] = img
test_sample = test_sample.astype("float32") / 255.0
encoder = tensorflow.keras.models.load_model("VAE_encoder.h5")
latent_vector = encoder.predict(test_sample)
下一步是使用解码器解码图像。解码器希望其输入是以下维度的 2D 数组:
- 样本数量。
- 样本长度。
与编码器类似,只有一个样本需要解码,因此样本数为1
。因为每个编码图像被表示为长度为 2 的向量,所以样本长度为2
。前两个维度的值如下:
- 样本数量:
1
- 样本长度:
2
下面是如何创建一个空的 NumPy 数组来表示解码器的输入。
latent_vector = numpy.zeros(shape=(1, 2))
假设向量的 2 个元素是 0.1 和 4.2,下面是如何将它们赋给向量。
latent_vector[0, 0] = 0.1
latent_vector[0, 1] = 4.2
现在我们可以读取解码器模型,它保存在一个名为VAE_decoder.h5
的文件中。
decoder = tensorflow.keras.models.load_model("VAE_decoder.h5")
通过调用解码器的predict()
方法,我们可以重建图像。
decoded_image = decoder.predict(latent_vector)
注意,解码器的结果是类似于编码器输入的 4D 张量。因此,我们需要提取该数组的图像,如下所示:
decoded_image = decoded_image[0, :, :, 0]
现在我们有了这个图像,我们要么保存它,要么显示它。下面是如何将其保存为名为decoder_image.jpg
的文件。
skimage.io.imsave(fname="decoder_image.jpg", arr=decoded_image)
以下是准备解码器输入、加载解码器、解码图像并保存结果的完整代码。
import skimage.io
import numpy
import tensorflow.keras.models
latent_vector = numpy.zeros(shape=(1, 2))
latent_vector[0, 0] = 0.1
latent_vector[0, 1] = 4.2
decoder = tensorflow.keras.models.load_model("VAE_decoder.h5")
decoded_image = decoder.predict(latent_vector)
decoded_image = decoded_image[0, :, :, 0]
skimage.io.imsave(fname="decoder_image.jpg", arr=decoded_image)
目前,我们已经回顾了基于预训练的编码器和解码器网络对来自 MNIST 数据集的图像进行编码和解码的步骤。下一节讨论使用 Flask 构建一个简单的 web 应用程序。
在 Flask 中构建一个简单的 Web 应用程序
最简单的 Flask 应用程序可以根据下面的代码块实现。通过实例化Flask
类并将实例保存在app
变量中来创建 flask 应用程序。之后,一个名为vae()
的函数监听服务器/
的主目录,并通过测试Hello
做出响应。
为了运行应用程序,调用了run()
方法,该方法有 3 个参数:
host
:保存服务器将被激活的主机名或 IP 地址。它被设置为0.0.0.0
来监听所有公共 IP,或者您可以在您的本地网络中指定确切的 IP 地址。port
:端口号,设置为5000
。debug
:设置为True
以调试模式运行服务器,这提供了一些关于服务器的附加信息以供调试。
import flask
app = flask.app.Flask(__name__)
@app.route("/", methods=["POST", "GET"])
def vae():
return "Hello"
app.run(host="0.0.0.0", port=5000, debug=True)
假设前面的代码保存在一个名为test_flask.py
的文件中,然后发出下面的终端命令来运行服务器。
python test_flask.py
下图显示了从 URL http://192.168.43.177:5000/
访问服务器后的结果,其中我的本地 IP 地址是 192.168.43.177。
有了一个运行的 Flask app 之后,我们就可以开始讨论我们的项目了。下一节只是总结了项目的结构,以便对我们将要使用的不同文件和文件夹有一个总体的了解。
项目结构
下面给出了项目的结构,假设所有的文件和文件夹都保存在名为VAE_Project
的根目录下。在这个根目录下,test_flask.py
文件保存了 Flask 应用程序代码。
该目录有两个文件夹,分别是:
static
templates
static
文件夹有 3 个文件和 1 个文件夹。这些文件是:
main.html
encode.html
decode.html
在static
文件夹中还有一个名为imgs
的文件夹,它只是一个空文件夹,用于保存解码后的图像。
templates
文件夹有两个文件:
decode_result.html
encode_result.html
VAE_Project:
static:
main.html
encode.html
decode.html
imgs:
templates:
encode_result.html
decode_result.html
test_flask.py
下一节构建应用程序的主要结构,以便它加载编码器和解码器模型,并为请求编码、解码或其他内容的请求提供服务。
搭建 VAE App 主体架构
如果用户需要对图像进行编码或解码,那么必须加载编码器和解码器模型。每次使用这些模型时都加载它们(根本)不是一个好主意。相反,只需加载一次,就可以重复使用。因此,在运行 Flask 应用程序之前加载模型是一个好时机。下面是模型在应用程序中的加载方式。
import flask
app = flask.app.Flask(__name__)
encoder = tensorflow.keras.models.load_model("VAE_encoder.h5")
decoder = tensorflow.keras.models.load_model("VAE_decoder.h5")
@app.route("/", methods=["POST", "GET"])
def vae():
return "Hello"
app.run(host="0.0.0.0", port=5000, debug=True)
为了控制所有到达服务器的请求,将使用vae()
函数来服务所有请求。在这个函数中,将根据请求的目的是编码、解码还是其他什么来调用其他函数。下面是vae()
功能的主要结构。
根据传入请求中的subject
参数,决定该请求是要求对图像进行编码或解码,还是只访问服务器的主页。
@app.route("/", methods=["POST", "GET"])
def vae():
subject = flask.request.args.get("subject")
print(subject)
if subject == "encode":
return upload_encode_img(flask.request)
elif subject == "decode":
return decode_img(flask.request)
else:
return flask.redirect(flask.url_for("static", filename="main.html"))
def upload_encode_img():
return "Encoder"
def decode_img():
return "Decoder"
以下是该应用程序可能的行为:
- 如果
subject
参数在请求中可用,并且其值为encode
,则请求的目的是编码图像。结果,请求被转发到另一个名为upload_encode_image()
的函数,该函数负责对图像进行编码。 - 如果
subject
参数中的值是decode
,那么它要求解码图像,并且该请求被转发给decode_img()
函数。 - 如果
subject
参数根本不可用,那么这意味着请求既不要求编码也不要求解码图像,因此加载了一个名为main.html
的 HTML 页面。
此时,upload_encode_img()
和decode_img()
函数除了返回一些文本外什么也不做。
在 Flask 中,最好将 HTML 文件添加到主应用程序目录中名为static
的文件夹中。通过这样做,Flask 应用程序可以轻松定位这些文件,并避免静态键入这些文件的 URL。例如,如果您想获得一个名为main.html
的文件的 URL,那么只需发出这个命令:
flask.url_for("static", filename="main.html")
获得页面的 URL 后,您可以使用flask.redirect()
函数请求服务器重定向到该页面,如下所示:
flask.redirect(flask.url_for("static", filename="main.html"))
下一节将讨论main.html
页面的实现。
App 主界面
如果用户访问了服务器的主页http://192.168.43.177:5000/
,则会显示一个 HTML 页面,询问用户是否想要对图像进行编码或解码。下面的代码给出了该页面的实现。它的主体有 2 个<a>
元素:一个引用用于编码图像的encode.html
页面,另一个引用用于解码图像的decode.html
页面。
<html>
<head>
<title>Vartiational Autoencoder for MNIST Dataset</title>
</head>
<body>
<h1><a href="http://192.168.43.177:5000/static/encode.html">Encode</a></h1>
<h1><a href="http://192.168.43.177:5000/static/decode.html">Decode</a></h1>
</body>
</html>
下图显示了main.html
在访问服务器的根目录后的样子。
下一节将讨论encode.html
页面的实现。
用于编码图像的 HTML 页面
下面列出了encode.html
页面的实现。该页面只有一个提交给服务器的地址为http://192.168.43.177:5000?subject=encode
的form
。请注意,subject
参数是可用的,并被设置为encode
以通知服务器这是一个关于编码图像的请求。
<html>
<head>
<title>Vartiational Autoencoder</title>
</head>
<body>
<form action="http://192.168.43.177:5000?subject=encode" method="post" enctype="multipart/form-data">
</form>
</body>
</html>
该表单只有两个元素:
- 类型为
file
的输入,允许用户选择要上传的图像。这个元素被命名为imageToUpload
,它将在服务器上被用来获取所选择的文件。 - 类型为
submit
的输入,这是一个按钮,用户单击它将表单提交给服务器。
这就是关于encode.html
页面的一切。下图显示了它的样子。
在从 MNIST 数据集中选择一幅图像并提交表单后,服务器将在vae()
函数中接收请求,该请求将被转发给upload_encode_img()
函数。下一节将讨论这个函数是如何工作的。
上传并编码图像
用户在encode.html
页面提交表单后,表单将被发送到upload_encode_img()
功能。根据下一个if
语句,该函数要做的第一件事是确保文件已经存在。它检查在请求的files
对象中是否有 ID 为imageToUpload
的文件。如果它不存在,那么服务器会显示一个 HTML 页面,声明没有上传任何文件。
if "imageToUpload" not in encode_request.files:
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
如果文件已经存在,则从files
对象中获取,如下所示:
img = encode_request.files["imageToUpload"]
为了再次检查文件是否已经上传,需要检查文件名,看它是否为空。如果为空,那么服务器将使用与前一种情况相同的 HTML 页面进行响应。
if img.filename == '':
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
如果文件名不为空,则按如下方式返回:
filename = werkzeug.utils.secure_filename(img.filename)
因为服务器需要一个图像文件,所以根据支持的图像扩展名列表检查上传的文件扩展名,这些扩展名是JPG
、JPEG
和PNG
。如果文件扩展名不受支持,则会显示一个 HTML 页面来通知用户。
_, file_ext = filename.split(".")
if file_ext.lower() not in ["jpg", "jpeg", "png"]:
return "<html><body><h1>Wrong file extension. The supported extensions are JPG, JPEG, and PNG.</h1><a href=" + app_url + ">Try Again</a></body></html>"
如果上传的文件是受支持扩展名的图像,则根据以下代码读取图像并检查其形状。如果图像尺寸不是(28, 28)
,将显示一个 HTML 页面。
read_image = skimage.io.imread(fname=filename, as_gray=True)
if read_image.shape[0] != 28 or read_image.shape[1] != 28:
return "<html><body><h1>Image size must be 28x28 ...</h1><a href=" + app_url + ">Try Again</a></body></html>"
最后,通过调用名为encode_img()
的函数对图像进行编码。
encode_img(read_image)
至此,这里是upload_encode_image()
函数的实现。
def upload_encode_image(encode_request):
if "imageToUpload" not in encode_request.files:
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
img = encode_request.files["imageToUpload"]
if img.filename == '':
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
filename = werkzeug.utils.secure_filename(img.filename)
_, file_ext = filename.split(".")
if file_ext.lower() not in ["jpg", "jpeg", "png"]:
return "<html><body><h1>Wrong file extension. The supported extensions are JPG, JPEG, and PNG.</h1><a href=" + app_url + ">Try Again</a></body></html>"
read_image = skimage.io.imread(fname=filename, as_gray=True)
if read_image.shape[0] != 28 or read_image.shape[1] != 28:
return "<html><body><h1>Image size must be 28x28 ...</h1><a href=" + app_url + ">Try Again</a></body></html>"
return encode_img(read_image)
下面给出了encode_img()
功能的实现。它接受要编码的图像作为参数。在其中,准备好 4D 数组,然后通过调用predict()
方法,使用之前加载的encoder
模型对图像进行编码。返回的潜在向量用于填充一个名为encode_result.html
的 HTML 模板。最后,通过调用render_template()
函数来呈现 HTML 模板。
def encode_img(img):
test_sample = numpy.zeros(shape=(1, 28, 28, 1))
test_sample[0, :, :, 0] = img
test_sample = test_sample.astype("float32") / 255.0
latent_vector = encoder.predict(test_sample)
return flask.render_template("encode_result.html", num1=latent_vector[0, 0], num2 = latent_vector[0, 1])
render_template()
函数接受 HTML 模板的名称作为参数,此外还接受列出了名称和值的其他参数(num1
和num2
代表潜在向量的两个值)。
名称-值参数用于填充 HTML 模板中的某些位置。下面给出了encode_result.html
文件的实现。其中有一个{{num1}}
,它将被分配给render_template()
函数中num1
参数的值所替代。这同样适用于{{num2}}
。
注意,HTML 模板保存在名为templates
的文件夹中。关于 Flask 中模板的更多信息,请查看这个链接。
<html>
<head>
<title>Vartiational Autoencoder</title>
</head>
<body>
<h1>Variational Autoencoder for Compressing and Reconstructing MNIST Images</h1>
<h1>Latent vector of the encoded image</h1>
<h3>{{num1}}, {{num2}}</h3>
<h1><a href="http://192.168.43.177:5000">Go to Main Page</a></h1>
</body>
</html>
在选定的图像被编码并且模板 HTML encode_result.html
被填充后,下图显示了结果。用户应该复制打印的值,因为它们代表编码的图像,以后用于解码。下一节讨论应用程序如何解码图像。
用于解码图像的 HTML 页面
在服务器的主页中,有 2 个<a>
元素,它们将用户带到encode
页面或decode
图像。之前讨论了编码部分。本节讨论解码部分。下面给出了用户点击Decode
链接后呈现的 HTML 页面。
页面有一个包含 3 个input
元素的form
。前两个是number
类型,允许用户输入潜在向量的值。他们的名字是num1
和num2
。服务器将使用这些名称来访问它们的值。第三个元素的类型是submit
,用于将表单提交到这个 URL: http://192.168.43.177:5000?subject=decode
。请注意,subject
参数被赋予值decode
以告知服务器上的vae()
函数该请求是关于解码图像的。
<html>
<head>
<title>Vartiational Autoencoder</title>
</head>
<body>
<h1>Enter latent vector to decode</h1>
<form action="http://192.168.43.177:5000?subject=decode" method="post">
</form>
</body>
</html>
下图显示了decode.html
页面的外观。
下一节讨论表单提交后服务器的行为。
解码图像
在用户输入潜在向量的值并在decode.html
页面提交表单后,请求将被转发给服务器上的vae()
函数,该函数将依次调用decode_img()
函数。下面列出了该函数的实现。它首先获取表单中传递的名为num1
和num2
的两个数值。然后,它准备一个空的 NumPy 数组,将由这两个值填充。
def decode_img(decode_request):
global im_id
num1, num2 = decode_request.form["num1"], decode_request.form["num2"]
latent_vector = numpy.zeros(shape=(1, 2))
latent_vector[0, 0] = num1
latent_vector[0, 1] = num2
print(latent_vector)
decoded_image = decoder.predict(latent_vector)
decoded_image = decoded_image[0, :, :, 0]
saved_im_name = os.path.join(app.config['UPLOAD_FOLDER'], "vae_result_" + str(im_id) + ".jpg")
im_id = im_id + 1
skimage.io.imsave(fname=saved_im_name, arr=decoded_image)
return flask.render_template("decode_result.html", img_name=saved_im_name)
解码器通过将数组传递给predict()
方法将这样的向量解码成图像。解码后的图像然后被保存在服务器端。保存图像的位置是将上传文件夹的目录与图像名称连接起来的结果。可以在运行服务器之前指定上传文件夹的位置,如下所示。在static
目录下有一个名为imgs
的文件夹,用来保存上传的文件。
IMGS_FOLDER = os.path.join('static', 'imgs')
app.config['UPLOAD_FOLDER'] = IMGS_FOLDER
根据im_id
变量,每个上传图像的名称被赋予一个唯一的 ID。它是一个全局变量,在运行服务器之前声明,初始化为0
。
保存图像后,服务器在将参数img_name
传递给render_template()
函数后呈现decode_result.html
模板。下面给出了decode_result.html
模板的实现。注意,这个文件应该保存在templates
目录下。
模板有一个<img>
元素,它的src
属性被设置为{{img_name}}
,它将被分配给render_template()
函数中img_name
参数的值所替换。
<html>
<head>
<title>Vartiational Autoencoder</title>
</head>
<body>
<h1>Variational Autoencoder for Compressing and Reconstructing MNIST Images</h1>
<h1>Reconstructed Image</h1>
<img src="{{img_name}}" width="56" height="56">
</body>
</html>
下图显示了呈现模板后的结果。
完整代码
Flask 应用程序的完整代码如下所示。
import flask
import werkzeug, os
import tensorflow.keras.models
import numpy
import skimage.io
IMGS_FOLDER = os.path.join('static', 'imgs')
app_url = "http://192.168.43.177:5000" #"https://hiai.website/vae_mnist"
app = flask.app.Flask(__name__)
app.config['UPLOAD_FOLDER'] = IMGS_FOLDER
encoder = tensorflow.keras.models.load_model("VAE_encoder.h5")
decoder = tensorflow.keras.models.load_model("VAE_decoder.h5")
im_id = 0
@app.route("/", methods=["POST", "GET"])
def vae():
subject = flask.request.args.get("subject")
print(subject)
if subject == "encode":
return upload_encode_image(flask.request)
elif subject == "decode":
return decode_img(flask.request)
else:
return flask.redirect(flask.url_for("static", filename="main.html"))
def upload_encode_image(encode_request):
if "imageToUpload" not in encode_request.files:
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
img = encode_request.files["imageToUpload"]
if img.filename == '':
return "<html><body><h1>No file uploaded.</h1><a href=" + app_url + ">Try Again</a></body></html>"
filename = werkzeug.utils.secure_filename(img.filename)
_, file_ext = filename.split(".")
if file_ext.lower() not in ["jpg", "jpeg", "png"]:
return "<html><body><h1>Wrong file extension. The supported extensions are JPG, JPEG, and PNG.</h1><a href=" + app_url + ">Try Again</a></body></html>"
read_image = skimage.io.imread(fname=filename, as_gray=True)
if read_image.shape[0] != 28 or read_image.shape[1] != 28:
return "<html><body><h1>Image size must be 28x28 ...</h1><a href=" + app_url + ">Try Again</a></body></html>"
return encode_img(read_image)
def encode_img(img):
test_sample = numpy.zeros(shape=(1, 28, 28, 1))
test_sample[0, :, :, 0] = img
test_sample = test_sample.astype("float32") / 255.0
latent_vector = encoder.predict(test_sample)
return flask.render_template("encode_result.html", num1=latent_vector[0, 0], num2 = latent_vector[0, 1])
def decode_img(decode_request):
global im_id
num1, num2 = decode_request.form["num1"], decode_request.form["num2"]
latent_vector = numpy.zeros(shape=(1, 2))
latent_vector[0, 0] = num1
latent_vector[0, 1] = num2
print(latent_vector)
decoded_image = decoder.predict(latent_vector)
decoded_image = decoded_image[0, :, :, 0]
saved_im_name = os.path.join(app.config['UPLOAD_FOLDER'], "vae_result_" + str(im_id) + ".jpg")
im_id = im_id + 1
skimage.io.imsave(fname=saved_im_name, arr=decoded_image)
return flask.render_template("decode_result.html", img_name=saved_im_name)
app.run(host="192.168.43.177", port=5000, debug=True)
结论
本教程使用了一个预训练的 variable auto encoder 来构建 Flask web 应用程序,该应用程序允许用户对 MNIST 数据集中的图像进行编码和解码。本教程通过构建一个简单的应用程序概述了 Flask,然后讨论了在 web 上编码和解码图像的细节。
实现条件生成对抗网络
原文:https://blog.paperspace.com/conditional-generative-adversarial-networks/
Photo by Avel Chuklanov / Unsplash
生成对抗网络是现代深度学习中最有用的概念之一。它们有广泛的应用,包括用户可以对将要生成的数据类型有更多的控制。虽然简单的生成网络完全能够处理不同类型的问题并实现必要的解决方案,但是这些生成网络有许多变体来完成更具体的任务,以便实现可能的最佳结果。我们将在本文中探索的一个类似的生成网络架构是条件生成对抗网络(CGANs)的概念。
在我们之前的博客中,我们已经讨论了许多类型的生成敌对网络(GAN),如 DCGAN 、 WGAN 、 SRGAN 等等。在本文中,我们将重点关注条件性 GANs,它允许我们对生成的数据类型进行更多的控制。
我们将首先看一下这个概念的简要介绍,并进而理解对条件句的直观理解。我们将用 CGAN 架构构建一个项目,最后,看看这些生成网络的一些应用。我建议使用 Paperspace Gradient 平台来运行这个程序和其他类似的代码,以实现高端结果。
简介:
当运行简单的生成对抗网络(GAN)时,经过训练的生成器网络可以生成其被开发来创建的各种图像。没有一种方法可以完全控制我们旨在通过这些简单网络生成的数据类型,如果您在较大的数据集上训练数据,但需要生成您正在寻找的特定类型的数据(或图像),有时可能需要这种方法。GANs 的一个变体是条件生成对抗网络(cgan ),它使您可以指定想要生成的数据或图像的确切类型成为可能。
理解有条件的 GANs:
在我们之前的工作中,我们探索了我们无法控制产出类型的 gan。与大多数生成式网络架构不同,CGANs 在训练方法上并非完全不受监督。这些 CGAN 网络架构需要某种类别标签或带标签的数据来执行期望的动作。让我们用一些数学公式来理解简单 GAN 架构和 CGAN 架构的区别。首先,让我们探索 GAN 结构的数学表达式,如下所示。
公式表达式表示最小-最大类型的算法,其中我们试图减少(或最小化)鉴别器和发生器网络中的变化,同时同时训练发生器和鉴别器模型。我们的目标是实现一种能够产生高质量输出的发生器,它可以绕过高效的鉴频器网络。然而,我们无法控制这种架构中生成的输出类型。现在让我们来看看 CGAN 数学表达式,它可以让我们获得想要的结果。
通过对我们之前简单 GAN 架构公式的微小修改,我们现在为鉴频器和发生器网络添加了 y 标签。通过将先前的概率转换为添加了“y”标签的条件概率,我们可以确保训练生成器和鉴别器网络现在仅针对相应的标签进行训练。因此,一旦训练过程完成,我们可以发送特定的输入标签并从生成网络接收期望的输出。
在训练过程中,发生器和鉴别器网络都将被分配这些标签。因此,CGAN 架构的这两个网络都被有条件地训练,使得生成器仅生成类似于预期标签输出的输出,而鉴别器模型确保检查所生成的输出是真的还是假的,同时检查图像是否匹配特定标签。现在我们已经对条件 GAN 有了一个简单直观的理解,我们可以继续构建 CGAN 架构来解决 MNIST 项目。
构建 CGAN 架构:
在本文的这一部分中,我们将重点讨论使用生成器和鉴别器结构来构建条件 GAN 架构。对于 MNIST 任务,我们将从 0 到 9 的每个数字指定为条件 gan 的相应标签。一旦分配了标签,有条件的 GAN 将为分配的数字产生适当的图像。这种生成不同于由简单 GAN 网络生成的图像,在简单 GAN 网络中会生成没有固定条件的随机图像。我们将利用 TensorFlow 和 Keras 深度学习框架来构建 CGAN 架构网络。
在继续本节的其余部分之前,我建议查看下面的文章,从这里了解更多关于 TensorFlow 和 Keras 的文章。
现在让我们开始库导入。
导入所需的库:
from tensorflow.keras.layers import UpSampling2D, Reshape, Activation, Conv2D, BatchNormalization, LeakyReLU, Input, Flatten, multiply
from tensorflow.keras.layers import Dense, Embedding
from tensorflow.keras.layers import Dropout, Concatenate
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt
import numpy as np
import warnings
warnings.filterwarnings('ignore')
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())
首先,让我们导入构建条件 GAN (CGAN)架构所需的所有基本库和模块。大多数层将用于构建 CGAN 模型网络。如果你了解过我之前的 GAN 文章,这些网络应该是相当熟悉的。大多数 CGAN 架构将遵循基本的 GAN 结构,并添加标签来训练模型。为了将标签转换成矢量格式,我们将使用嵌入层。所有层都将从 TensorFlow 和 Keras 深度学习框架中加载。下面是所有必要导入的代码片段。
分配基本要求:
(X_train,y_train),(X_test,y_test) = mnist.load_data()
img_width, img_height =28,28
img_channel = 1
img_shape = (img_width, img_height, img_channel)
num_classes = 10
z_dim = 100
X_train.shape
输出:
(60000, 28, 28)
因为我们将在这个项目中使用 MNIST 数据集,所以让我们将数据加载到训练和测试样本中。我们将声明所需的参数,如图像高度、图像宽度和通道数量。MNIST 数据集中的每个图像的大小都是 28 x 28,并且是具有一个通道的灰度图像。我们一共十个类,这些类将作为我们 CGAN 模型学习的标签。定义了默认的 z 维空间 100。以下代码片段如下所示。
构建发电机架构:
def build_generator():
model = Sequential()
model.add(Dense(128*7*7, activation = 'relu', input_shape = (z_dim, )))
model.add(Reshape((7,7,128)))
model.add(UpSampling2D())
model.add(Conv2D(128, kernel_size = 3, strides = 2, padding = 'same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha = 0.02))
model.add(UpSampling2D())
model.add(Conv2D(64, kernel_size = 3, strides = 1, padding = 'same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha = 0.02))
model.add(UpSampling2D())
model.add(Conv2D(1, kernel_size = 3 , strides = 1, padding='same'))
model.add(Activation('tanh'))
z = Input(shape= (z_dim,))
label = Input(shape=(1,), dtype = 'int32')
label_embedding = Embedding(num_classes, z_dim, input_length = 1)(label)
label_embedding = Flatten()(label_embedding)
joined = multiply([z, label_embedding])
img = model(joined)
return Model([z, label], img)
generator = build_generator()
generator.summary()
输出:
Model: "model"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_2 (InputLayer) [(None, 1)] 0
__________________________________________________________________________________________________
embedding (Embedding) (None, 1, 100) 1000 input_2[0][0]
__________________________________________________________________________________________________
input_1 (InputLayer) [(None, 100)] 0
__________________________________________________________________________________________________
flatten (Flatten) (None, 100) 0 embedding[0][0]
__________________________________________________________________________________________________
multiply (Multiply) (None, 100) 0 input_1[0][0]
flatten[0][0]
__________________________________________________________________________________________________
sequential (Sequential) (None, 28, 28, 1) 856193 multiply[0][0]
==================================================================================================
Total params: 857,193
Trainable params: 856,809
Non-trainable params: 384
__________________________________________________________________________________________________
为了构建发生器和鉴别器架构,我们将遵循与简单 GAN 架构相似的方案。我们将首先使用顺序模型原型,并开始构建我们的模型层。我们将使用 128 个矩阵,每个矩阵的维数为 7×7。然后,我们将开始使用上采样层,然后是卷积层。还利用了批量标准化层,以及阿尔法值为 0.02 的泄漏 ReLU 层。我们将继续使用这些构建模块,直到我们获得与 MNIST 数据相似的最终所需形状。最后,我们将以 tanh 激活函数结束,函数的这一部分包含将由带有随机分配噪声的发生器生成的图像。
在这个函数的后半部分,我们将添加一个嵌入来为必须相应分配的标签(或类)创建向量映射。MNIST 数据的十位数字中的每一位都必须被视为独立且唯一的标签,并且模型必须相应地生成它们中的每一位。我们可以创建这些标签嵌入,并继续将这些信息与我们之前的生成器模型相结合。一旦合并,我们将返回整体模型并将其存储在生成器变量中。
构建鉴别器架构:
def build_discriminator():
model = Sequential()
model.add(Conv2D(32, kernel_size = 3, strides = 2, input_shape = (28,28,2), padding = 'same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha = 0.02))
model.add(Conv2D(64, kernel_size = 3, strides = 2, padding = 'same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha = 0.02))
model.add(Conv2D(128, kernel_size = 3, strides = 2, padding = 'same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha = 0.02))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1, activation = 'sigmoid'))
img = Input(shape= (img_shape))
label = Input(shape= (1,), dtype = 'int32')
label_embedding = Embedding(input_dim = num_classes, output_dim = np.prod(img_shape), input_length = 1)(label)
label_embedding = Flatten()(label_embedding)
label_embedding = Reshape(img_shape)(label_embedding)
concat = Concatenate(axis = -1)([img, label_embedding])
prediction = model(concat)
return Model([img, label], prediction)
discriminator = build_discriminator()
discriminator.summary()
输出:
Model: "model_1"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_4 (InputLayer) [(None, 1)] 0
__________________________________________________________________________________________________
embedding_1 (Embedding) (None, 1, 784) 7840 input_4[0][0]
__________________________________________________________________________________________________
flatten_2 (Flatten) (None, 784) 0 embedding_1[0][0]
__________________________________________________________________________________________________
input_3 (InputLayer) [(None, 28, 28, 1)] 0
__________________________________________________________________________________________________
reshape_1 (Reshape) (None, 28, 28, 1) 0 flatten_2[0][0]
__________________________________________________________________________________________________
concatenate (Concatenate) (None, 28, 28, 2) 0 input_3[0][0]
reshape_1[0][0]
__________________________________________________________________________________________________
sequential_1 (Sequential) (None, 1) 95905 concatenate[0][0]
==================================================================================================
Total params: 103,745
Trainable params: 103,297
Non-trainable params: 448
__________________________________________________________________________________________________
一旦我们完成了生成器网络的构建,我们就可以继续构建鉴别器架构,它将充当分类器。首先,让我们构建分类器网络,它利用步长为 2 的卷积层来不断调整网络的大小。利用批量标准化层以及漏失来避免网络的过拟合。我们还利用了泄漏 ReLU 激活函数。
最后,具有 sigmoid 激活函数的密集层会将输出分类为真或假。鉴频器网络使用类似的逻辑。标签通过鉴别器网络传递,我们需要确保这些嵌入对生成器和鉴别器架构都可用。
计算结果:
现在我们已经完成了 CGAN 模型的生成器和鉴别器架构的构建,我们终于可以开始训练过程了。我们将这一部分分为三个不同的部分,即模型的编译、训练和拟合模型,以及最后在训练过程完成后保存和显示所获得的结果。让我们从每一部分开始。
编译模型:
from tensorflow.keras.optimizers import Adam
discriminator.compile(loss = 'binary_crossentropy', optimizer = Adam(0.001, 0.5), metrics = ['accuracy'])
z = Input(shape=(z_dim,))
label = Input(shape= (1,))
img = generator([z,label])
# discriminator.trainable = False
prediction = discriminator([img, label])
cgan = Model([z, label], prediction)
cgan.compile(loss= 'binary_crossentropy', optimizer = Adam(0.001, 0.5))
对于 CGAN 模型的编译,我们将使用二进制交叉熵损失函数、Adam 优化器、默认学习率 0.001 和β1 值 0.5。我们还将设置鉴别器来测量每个时期结束时的精度。然后,我们将分配默认的参数来通过生成器模型,这些参数是 z 维空间和标签。然后,我们将介绍条件 GAN 网络的发生器和鉴别器架构,并最终编译该模型。
定义发生器和鉴别器训练:
def train(epochs, batch_size, save_interval):
(X_train, y_train), (_, _) = mnist.load_data()
X_train = (X_train - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)
real = np.ones(shape=(batch_size, 1))
fake = np.zeros(shape=(batch_size, 1))
for iteration in range(epochs):
idx = np.random.randint(0, X_train.shape[0], batch_size)
imgs, labels = X_train[idx], y_train[idx]
z = np.random.normal(0, 1, size=(batch_size, z_dim))
gen_imgs = generator.predict([z, labels])
d_loss_real = discriminator.train_on_batch([imgs, labels], real)
d_loss_fake = discriminator.train_on_batch([gen_imgs, labels], fake)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
z = np.random.normal(0, 1, size=(batch_size, z_dim))
labels = np.random.randint(0, num_classes, batch_size).reshape(-1, 1)
g_loss = cgan.train_on_batch([z, labels], real)
if iteration % save_interval == 0:
print('{} [D loss: {}, accuracy: {:.2f}] [G loss: {}]'.format(iteration, d_loss[0], 100 * d_loss[1], g_loss))
save_image(iteration)
一旦模型编译完成,我们就可以开始定义生成器和鉴别器训练机制。我们定义的训练函数将接受设定的时期数、设定的批量大小和输入的保存生成图像的时间间隔的输入值。
在训练方法中,我们会传递随机数和值来接收假图像。鉴别器和生成器网络将被同时训练,如上面的代码所示。
保存和计算 CGAN 体系结构的训练;
确保您创建了一个图像目录,其中可以存储由 CGAN 架构生成的所有图像。你可以在 Gradient GUI 中通过在你的工作目录中创建一个新的文件夹来手动完成,或者使用下面提供的代码片段。
import os
os.mkdir("images/")
最后,让我们定义这个项目需要的最后一个功能。我们将创建一个函数来保存指定位置和指定间隔结束时的图像。matplotlib 库也用于绘制图像并将它们保存在图像目录中。下面是执行以下操作的代码块。
def save_image(epoch):
r, c = 2,5
z = np.random.normal(0,1,(r*c, z_dim))
labels = np.arange(0,10).reshape(-1,1)
gen_image = generator.predict([z,labels])
gen_image = 0.5 * gen_image + 0.5
fig, axes = plt.subplots(r,c, figsize = (10,10))
count = 0
for i in range(r):
for j in range(c):
axes[i,j].imshow(gen_image[count,:,:,0],cmap = 'gray')
axes[i,j].axis('off')
axes[i,j].set_title("Digit: %d" % labels[count])
count+=1
plt.savefig('images/cgan_%d.jpg' % epoch)
plt.close()
我们将以 128 的批量训练该模型大约 5000 个时期,并且在每 1000 个时期的训练之后保存一次图像。下面是对 5000 个时期执行以下操作的代码片段,它将 5 个图像保存到目录中。
# training the network
train(5000, 128, 1000)
在训练过程中,您应该能够注意到,在 1000 个间隔中的每一个,我们都在保存一个图像,并且 CGAN 模型学习如何为给定标签生成特定图像的过程逐渐增加。我建议从提到的链接中查看下面这个 CGAN 项目的 GitHub 参考资料。除了 MNIST 项目,CGANs 还有许多应用。我们将在下一节中详细介绍这些内容。
CGAN 的应用:
乍一看,很容易怀疑 CGANs 是否能满足大多数现代需求。现在,我们通过构建这个网络对条件 gan 有了直观和概念性的了解,我们可以继续探索一些最受欢迎的应用程序,您可以利用这些条件 gan。这些生成式架构对于一些重要的应用非常有用。让我们讨论一些 CGANs 帮助解决的重要用例。
- 图像到图像翻译: 在图像到图像翻译的应用中,我们利用输入图像将其映射到其各自的输出图像。在条件 GANs 的帮助下,我们可以指定我们想要的输入图像的类型作为特定的条件,并相应地根据输入图像训练输出图像。给定特定的输入条件,可以产生相应输出的一个或多个变化。看待这种应用的另一种方式是将图像从一个输入域转换到另一个域。我建议查阅我以前的一篇文章,研究一个使用神经风格转移的类似应用,其中我们利用内容和风格图像来构建一个生成的图像。
- 文本到图像的合成: 文本到图像合成的应用类似于我们在本文中所做的。给定文本描述,CGAN 的生成网络可以为特定描述生成图像。在本博客讨论的例子中,我们能够生成给定相应标签的范围从 0 到 9 的数字。还有其他一些例子,您可以指定一个条件,例如文本中的特定动物,并训练 CGAN 网络生成文本中提到的动物的特定图像。
- 卷积人脸生成: 在我之前的一篇文章中,我们已经非常详细地讨论了用生成网络构建一个人脸生成项目。在有条件的 GANs 的帮助下,该应用程序可以在面部生成模式方面有额外的改进。我们可以训练模型利用外部因素来实现同一张脸的不同模式。这可能涉及到针对特定面部状况产生不同种类的情绪(请参考本节开头显示的图像)。为了进一步了解和理解卷积人脸生成,我建议查看下面的研究论文。
- 视频生成: 类似于前面讨论的应用,我们可以利用有条件的 GANs 进行视频生成。我们可以指定特定的标题或视频标题,并根据条件生成整个视频。所有生成的面的组合可以组合在一起,以创建移动面的移动,以及使用更结构化的方法。如果你有兴趣了解这种应用程序的更多细节,我建议查看下面的研究论文,其中作者开发了一个通过编码字幕来生成视频的生成网络。
对于有条件的 GANs 有很多更实际的应用,开发人员、研究人员和 AI 爱好者可以自由探索他们自己的特定用例和实际应用。由于我们可以用有条件的 GANs 开发各种各样独特的项目,所以我们建议查看者查看更多这样的实验,并相应地实现它们。
结论:
Photo by Sincerely Media / Unsplash
生成性对抗网络为神经网络提供了从所提供的资源中学习和创建内容的一些最佳方式。在我们可以用这些生成网络构建的许多不同的结构中,我们可以通过添加标签来增加一定程度的监督。给定一个特定的标签,当生成网络能够为相应的类(或条件)生成所需的图像时,我们可以创建更多的可复制副本。这种条件网络有广泛的用途,开发人员可以利用它来创建独特的项目。
在这篇文章中,我们在条件生成对抗网络(CGAN)中看到了生成网络的另一个重要变化。在条件 GANs 的帮助下,我们看到了用户如何指定一个特定的条件,并为指定的标签生成特定的图像。我们对条件 gan 有了直观的理解,并学习了如何用标签构建它们的生成式和鉴别式架构。我们还讨论了这些有条件的 GANs 的一些更流行的应用,以及它们在。
在接下来的文章中,我们将研究更多的 gan 变体,以及它们在 Cycle GANs 和 Pix-To-Pix gan 中提供的大量效用。我们还将在未来的博客中查看 BERT 的一些自然语言处理应用以及如何从头构建神经网络。在此之前,继续探索和构建新的项目!
从头开始构建神经网络:第 1 部分
原文:https://blog.paperspace.com/constructing-neural-networks-from-scratch/
Photo by Nastya Dulhiier / Unsplash
随着 TensorFlow、Keras、PyTorch 和其他类似库等流行深度学习框架的出现,该领域的新手以更快的速度学习神经网络主题变得容易得多。尽管这些框架为您提供了在几分钟内解决最复杂计算的途径,但它们并不要求您理解所有需求背后真正的核心概念和直觉。如果您知道某个特定函数是如何工作的,以及如何在您的代码块中准确地使用该函数,那么您将会毫不费力地解决大多数问题。然而,对于任何人来说,要真正理解神经网络的概念并理解完整的工作过程,从头开始学习这些人工神经网络是如何工作的变得至关重要。这些神经网络如何解决这些复杂的问题?
对于任何对人工智能和深度学习感兴趣的人来说,理解神经网络是如何工作的,以及随后它们是如何构造的,是一项值得努力的工作。虽然我们限制自己使用任何类型的深度学习框架,如 TensorFlow、Keras 或 PyTorch,但我们仍然会使用其他有用的库,如 NumPy,用于数值矩阵计算。通过 NumPy 阵列,我们可以执行大量复杂的计算,模拟深度学习的效果,并使用它来建立对这些神经网络的程序工作流的理解。我们将实现一些神经网络,旨在借助这些从头构建的神经网络来解决一个相当简单的任务。
简介:
神经网络的话题是深度学习领域和人工智能未来最有趣的话题之一。虽然人工神经网络这一术语只是从生物神经元的概念中大致得到启发,但在对它们进行概念化时,有一些值得注意的相似之处需要记住。与人类神经元非常相似,使用人工神经网络的一个有趣方面是,我们通常可以确定他们正在做什么,但是通常没有明确的方法来确定他们如何工作来实现目标。虽然我们有可能回答一些“是什么”的方面,但要完全理解我们需要知道的关于模型行为的一切,还有许多发现要做。这被称为深度学习的“黑盒”隐喻,它可以应用于许多深度学习系统。也就是说,许多神经网络是足够可解释的,我们可以很容易地解释它们的目的和方法,这取决于你的用例。
人工智能是一个庞大的领域,深度学习和神经网络只是其中的两个子领域。在本文中,我们的主要目标是更深入地研究神经网络的概念,我们将继续展示如何在不使用一些著名和流行的深度学习框架的情况下从头构建一个架构。在我们从头开始详述神经网络的实现之前,让我们对它们的工作过程有一个直观的了解。
理解神经元的概念:
大多数数学运算可以通过一个函数联系起来。函数是最基本的概念之一,通过它神经网络可以学习几乎任何东西。不管该函数做什么,都可以创建一个神经网络来逼近该函数。这就是众所周知的通用逼近定理,这一定律允许神经网络处理如此多种不同的挑战,同时也赋予了它们黑盒的性质。
诸如计算机视觉任务和自然语言处理任务的大多数现实世界问题也可以以函数的形式彼此关联。例如,通过函数,我们可以将几个单词的输入链接到特定的输出单词,并将一组图像链接到它们各自的输出图像。数学和现实世界问题中的大多数概念可以被重新构造为一个函数,以构建一个问题,期望的神经网络可以找到该问题的适当解决方案。
什么是人工神经网络,它是如何工作的?
术语人工神经网络现在通常被称为神经网络、神经网络或 nns。这些神经网络松散地受到生物神经元的启发。重要的是再次注意到,在活体中的神经元和用于构建神经网络架构的神经元之间实际上几乎没有相关性。虽然这两种元素的基本工作过程非常不同,但它们确实有一个共同的特点,即当这些神经网络结合在一起时,它们可以相对容易地解决复杂的任务。
为了理解神经网络如何工作的基本概念,我们可以用来帮助理解神经网络的关键数学概念之一是线方程,它是“y = mx + c”。方程的“y = mx”部分有助于操纵线来实现所需的形状和值。另一个值是截距 c,它通过在 y 轴上移动截距来改变直线的位置。参考这两幅图,可以更清楚地理解这个基本概念。
在神经网络方面,Y = WX +B 可以用来表示这个方程。y 表示输出值,“w”表示需要调整的权重,“x”表示输入值,“b”表示值。通过使用这个简单的逻辑,神经网络可以使用已知的信息“b”和“w”来确定“x”的值。
为了更好地理解权重和偏差这一特定概念,让我们探索如下所示的简单代码片段和结果输出。使用一些输入值、权重和偏差,我们可以用权重转置的输入的点积来计算输出。将相应的偏差加到该结果值上,以计算期望值。下面的例子很简单,但足以让你有一个基本的了解。然而,我们将在下一节和后续文章中讨论更复杂的概念。
import numpy as np
inputs = [1, 2, 3, 2.5]
weights = [[ 0.2, 0.8, - 0.5, 1 ],
[ 0.5, - 0.91, 0.26, - 0.5 ],
[ - 0.26, - 0.27, 0.17, 0.87 ]]
biases = [2, 3, 0.5]
outputs = np.dot(weights, inputs) + biases
# Or Use this method
# np.dot(inputs, weights.T) + biases
print (outputs)
[4.8 1.21 2.385]
当神经网络集体结合在一起时,它们能够在反向传播的帮助下通过训练过程进行学习。第一步是前向传播,通过使用随机权重计算每层的必要信息,直到输出像元。然而,这些随机权重通常永远不会接近完美,需要调整权重以达到更理想的结果。因此,神经网络中的反向传播是其功能的更重要的方面之一。反向传播是对权重进行操作和调整的地方,通常是将手头的输出与预期的输出进行比较。我们将在下一节和后续文章中进一步研究这些概念。
从零开始构建神经网络;
在本节中,我们将看到如何从零开始借助神经网络的构造来解决一些任务。在我们从头开始构建我们的神经网络之前,让我们先了解一下本文中我们试图解决的问题类型。我们的目标是构建能够理解和解决逻辑门功能的神经网络,例如 and、OR、NOT、XOR 和其他类似的逻辑门。对于这个具体的例子,我们将看看如何通过从头构建我们的神经网络来解决 XOR 门问题。
逻辑门是电子元件的一些最基本的构件。我们使用这些逻辑门是因为,正如它们的名字所暗示的,每一个逻辑门都按照特定的逻辑运行。例如,XOR 门仅在两个输入值不同时提供高输出。如果两个输入值相似,则产生的输出为低。这些逻辑表示通常以真值表的形式表示。上图显示了异或门的符号和真值表表示。我们可以使用数组形式的输入和输出值来训练我们构建的神经网络,以获得理想的结果。
让我们首先导入必要的库,我们将利用这些库从头开始构建神经网络。本节我们不会用到任何深度学习框架。我们将只使用 NumPy 库来简化一些复杂的张量计算和整体的数学计算。即使没有 NumPy 库,您也可以选择构建神经网络,但是这会更加耗时。我们还将在 matplotlib 中导入本节所需的唯一的其他库。当我们为特定数量的时期训练模型时,我们将使用这个库来可视化和绘制损失。
import numpy as np
import matplotlib.pyplot as plt
让我们描述真值表的输入和异或门的预期输出。读者可以选择在不同的门上工作,并相应地进行实验(注意,有时您可能得不到想要的结果)。下面是 XOR 门的输入变量和预期结果的代码片段。
a = np.array([0, 0, 1, 1])
b = np.array([0, 1, 0, 1])
# y_and = np.array([[0, 0, 0, 1]])
y_xor = np.array([[0,1,1,0]])
让我们将输入组合到一个数组实体中,这样我们就有一个总输入数组和一个输出数组供神经网络学习。这个组合过程可以用几种方式来完成。在下面的代码块中,我们使用一个列表来组合两个数组,然后将最终的列表转换回 numpy 数组格式。在下一节中,我还提到了另一种整理输入数据的方法。
total_input = []
total_input = [a, b]
total_input = np.array(total_input)
产生的输入数组如下。
array([[0, 0, 1, 1],
[0, 1, 0, 1]])
对于神经网络的大多数问题,阵列的形状是最关键的概念。形状不匹配是解决此类任务时最有可能出现的错误。因此,让我们打印并分析输入数组的形状。
(2, 4)
现在让我们定义一些从零开始构建神经网络所需的基本参数。给定一个输入或一组输入,节点的激活函数定义该节点的输出。我们将首先定义 sigmoid 函数,这将是我们在此任务中的主要激活函数。然后,我们将继续定义一些基本参数,如输入神经元、隐藏神经元、输出神经元的数量、总训练样本以及我们将训练神经网络的学习速率。
# Define the sigmoid activation function:
def sigmoid (x):
return 1/(1 + np.exp(-x))
# Define the number of neurons
input_neurons, hidden_neurons, output_neurons = 2, 2, 1
# Total training examples
samples = total_input.shape[1]
# Learning rate
lr = 0.1
# Define random seed to replicate the outputs
np.random.seed(42)
在下一步中,我们将初始化将通过隐藏层和输出层传递的权重,如下面的代码片段所示。将权重随机化通常是一个好主意,而不是将它们赋值为零,因为神经网络有时可能无法学习到所需的结果。
# Initializing the weights for hidden and output layers
w1 = np.random.rand(hidden_neurons, input_neurons)
w2 = np.random.rand(output_neurons, hidden_neurons)
在下一个代码块中,我们将定义神经网络模型的工作结构。首先,我们将使函数通过神经网络结构执行前向传播。我们将从计算隐藏层中的权重和输入值开始,然后将它们传递给我们的 sigmoid 激活函数。然后,我们也将对输出层执行类似的传播,其中我们将利用之前定义的第二个权重。随机生成的权重显然不能达到预期的效果,需要进行微调。因此,我们还将实现反向传播机制,以帮助我们的模型更有效地训练。该操作的执行方式与我们在上一节中讨论的方式类似。
# Forward propagation
def forward_prop(w1, w2, x):
z1 = np.dot(w1, x)
a1 = sigmoid(z1)
z2 = np.dot(w2, a1)
a2 = sigmoid(z2)
return z1, a1, z2, a2
# Backward propagation
def back_prop(m, w1, w2, z1, a1, z2, a2, y):
dz2 = a2-y
dw2 = np.dot(dz2, a1.T)/m
dz1 = np.dot(w2.T, dz2) * a1*(1-a1)
dw1 = np.dot(dz1, total_input.T)/m
dw1 = np.reshape(dw1, w1.shape)
dw2 = np.reshape(dw2,w2.shape)
return dz2,dw2,dz1,dw1
既然我们已经定义了前向传播和反向传播机制,我们可以继续训练神经网络。让我们创建一个训练循环,运行预定义的迭代次数。首先,我们将利用正向传播来接收输出值,然后通过将其与预期输出进行比较,开始相应地计算损耗。一旦我们开始执行神经网络的反向传播,我们就可以开始微调权重,以获得与预期结果相当的最终结果。下面是培训过程的代码块。我们还确保损失不断减少,神经网络正在学习如何预测预期结果以及以下图表。
losses = []
iterations = 10000
for i in range(iterations):
z1, a1, z2, a2 = forward_prop(w1, w2, total_input)
loss = -(1/samples)*np.sum(y_xor*np.log(a2)+(1-y_xor)*np.log(1-a2))
losses.append(loss)
da2, dw2, dz1, dw1 = back_prop(samples, w1, w2, z1, a1, z2, a2, y_xor)
w2 = w2-lr*dw2
w1 = w1-lr*dw1
# We plot losses to see how our network is doing
plt.plot(losses)
plt.xlabel("EPOCHS")
plt.ylabel("Loss value")
让我们定义预测函数,通过它我们可以利用我们训练的神经网络来计算一些预测。我们将执行前向传播并压缩获得的结果。由于我们在训练过程中对权重进行了微调,因此我们应该能够在阈值为 0.5 的情况下达到预期的结果。
# Creating the predict function
def predict(w1,w2,input):
z1, a1, z2, a2 = forward_prop(w1,w2,test)
a2 = np.squeeze(a2)
if a2>=0.5:
print("For input", [i[0] for i in input], "output is 1")
else:
print("For input", [i[0] for i in input], "output is 0")
现在,我们已经完成了预测函数的定义,我们可以测试您构建的神经网络所做的预测,并在训练模型约 10000 次迭代后查看其性能。我们将测试四种可能情况的预测结果,并将它们与 XOR 门的预期结果进行比较。
test = np.array([[0],[0]])
predict(w1,w2,test)
test = np.array([[0],[1]])
predict(w1,w2,test)
test = np.array([[1],[0]])
predict(w1,w2,test)
test = np.array([[1],[1]])
predict(w1,w2,test)
For input [0, 0] output is 0
For input [0, 1] output is 1
For input [1, 0] output is 1
For input [1, 1] output is 0
我们可以注意到,神经网络预测后得到的结果与预期的结果相似。因此,我们可以得出结论,我们从头构建的神经网络能够成功地对 XOR 门任务做出准确的预测。下面的 GitHub 引用用于本节的大部分代码。如果你想要另外的学习资源,我建议你去看看。还建议读者通过从头构建神经网络来解决这些问题,尝试不同类型门的其他变体。
使用深度学习框架的构建比较:
深度学习和人工神经网络领域非常广阔。虽然可以从零开始构建神经网络来解决复杂的问题,但是由于需要大量的时间以及需要构建的网络的固有复杂性,这通常是不可行的。因此,我们利用深度学习框架,如 TensorFlow、PyTorch、MXNet、Caffe 和其他类似的库(或工具)来设计、训练和验证神经网络模型。
这些深度学习框架允许开发人员和研究人员快速构建他们想要的模型来解决特定任务,而无需对复杂和不必要的细节的底层工作进行太多投资。在众多可用的深度学习框架中,两个最受欢迎的用于构建神经网络的现有工具是 TensorFlow 和 PyTorch。在这一节中,我们将借助深度学习框架来重建我们在前面几节中构建的项目。
对于这个重建项目,我将使用 TensorFlow 和 Keras 库。我建议查看下面的文章,通过此链接了解更多关于 TensorFlow 和 Keras 文章的信息(此处)。本文这一部分的代码结构非常简单,您可以在 PyTorch 等任何其他深度学习框架中轻松复制它。如果你想用 PyTorch 而不是 TensorFlow 来构建这个项目,可以从下面的链接查看 PyTorch 的最终指南。下面是我们将用于构建我们的神经网络来求解与门和异或门的输入列表。
import tensorflow as tf
from tensorflow import keras
import numpy as np
一旦我们导入了必要的库,我们就可以定义一些所需的参数,用于构建神经网络来学习与门的输出。类似于“与”门,我们还将构建“异或”门,正如我们在上一节中所做的那样。首先,让我们看看与门的结构。下面是与门的输入和预期结果。与门的逻辑是,只有当两个(或所有)输入都为高电平时,输出才为高电平。否则,当任一输入为低电平时,输出也为低电平。
a = np.array([0, 0, 1, 1])
b = np.array([0, 1, 0, 1])
y_and = np.array([0, 0, 0, 1])
一旦我们声明了输入和预期的输出,就该把两个输入数组合并成一个实体了。正如上一节所讨论的,我们可以用几种方法做到这一点。对于这个代码片段,我们将把它们添加到一个包含四个独立元素的列表中,每个列表有两个元素。组合输入元素后得到的最终数组将存储在一个新的数组中。
total_input = []
for i, j in zip(a, b):
input1 = []
input1.append(i)
input1.append(j)
total_input.append(input1)
total_input = np.array(total_input)
组合两个初始输入列表后获得的输入数组如下所示。
array([[0, 0],
[0, 1],
[1, 0],
[1, 1]])
这个最终输入数组的形状如下。
(4, 2)
在下一步中,我们将创建训练数据及其各自的输出标签。首先,我们将创建列表来存储输入和输出的训练数据。一旦我们完成了这些元素的循环,我们可以将这些列表保存为数组,并使用它们进行进一步的计算和神经网络训练。
x_train = []
y_train = []
for i, j in zip(total_input, y_and):
x_train.append(i)
y_train.append(j)
x_train = np.array(x_train)
y_train = np.array(y_train)
这种任务的训练过程非常简单。我们可以定义所需的库,即输入层、隐藏层和输出节点的密集层,以及最后的顺序模型,以便我们可以构建顺序类型模型来解决所需的与门任务。首先,我们将定义模型的类型,然后继续添加输入层,该层将按照我们之前定义的方式接收输入。我们有两个隐藏层,每个层有 10 个节点,都有 ReLU 激活功能。最终输出层包含一个节点的 Sigmoid 激活函数,为我们提供所需的结果。根据提供的输入,最终输出是零或一。
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Sequential
model = Sequential()
model.add(Input(shape = x_train[0].shape))
model.add(Dense(10, activation = "relu"))
model.add(Dense(10, activation = "relu"))
model.add(Dense(1, activation = "sigmoid"))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense (Dense) (None, 10) 30
_________________________________________________________________
dense_1 (Dense) (None, 10) 110
_________________________________________________________________
dense_2 (Dense) (None, 1) 11
=================================================================
Total params: 151
Trainable params: 151
Non-trainable params: 0
_________________________________________________________________
上表显示了包含隐藏层和输出节点及其各自参数的序列类型网络的概要。既然我们已经构建了模型架构来解决所需的 AND 门任务,我们可以继续编译模型并相应地训练它。我们将利用 Adam 优化器、二进制交叉熵损失函数,并计算二进制准确性来验证我们的模型有多准确。
model.compile(optimizer = "adam", loss = "binary_crossentropy", metrics = "binary_accuracy")
一旦模型的编译完成,让我们开始训练程序,看看模型是否能够达到预期的结果。注意,从零开始的神经网络的损失函数和优化器等内容尚未涵盖。我们将在以后的文章中讨论这些概念。下面是训练所需模型的代码片段。
model.fit(x_train, y_train, epochs = 500)
我们将对该模型进行大约 500 个时期的训练,以确保它按照预期学习需求。由于我们对于这些门任务的数据较少,因此模型将需要更多的训练来学习并相应地优化结果。训练完成后,大约需要几分钟时间,我们可以继续使用预测功能来验证获得的结果。让我们对数据集执行预测,如下面的代码片段所示。
model.predict(x_train)
array([[0.00790971],
[0.02351646],
[0.00969902],
[0.93897456]], dtype=float32)
我们可以注意到 AND 门的结果似乎和预期的差不多。对于必须为零的输出值,预测得到的结果能够预测接近于零的值,而对于必须为一的输出值,我们得到的结果接近于一。我们还可以对这些值进行舍入,以获得想要的结果。除了我们刚刚完成的与门之外,让我们探索另一个门。
类似地,对于异或门,我们也可以按照与与门相似的工作流程进行。首先,我们将定义 XOR 门所需的输入。它也是一个双通道输入,利用变量 a 和 b 存储输入值。y 变量将在 NumPy 数组中存储预期的结果值。我们将组合两个输入数组,类似于本节中使用的方法。一旦输入被组合,并且我们得到期望的组合,我们将把我们的数据分成训练输入信息和它们的结果标签输出。
a = np.array([0, 0, 1, 1])
b = np.array([0, 1, 0, 1])
y_xor = np.array([0, 1, 1, 0])
total_input = []
for i, j in zip(a, b):
input1 = []
input1.append(i)
input1.append(j)
total_input.append(input1)
total_input = np.array(total_input)
x_train = []
y_train = []
for i, j in zip(total_input, y_xor):
x_train.append(i)
y_train.append(j)
x_train = np.array(x_train)
y_train = np.array(y_train)
我们将使用与本文前面部分类似的顺序类型架构。模型的代码片段和结果摘要如下所示。
model1 = Sequential()
model1.add(Input(shape = x_train[0].shape))
model1.add(Dense(10, activation = "relu"))
model1.add(Dense(10, activation = "relu"))
model1.add(Dense(1, activation = "sigmoid"))
model1.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_3 (Dense) (None, 10) 30
_________________________________________________________________
dense_4 (Dense) (None, 10) 110
_________________________________________________________________
dense_5 (Dense) (None, 1) 11
=================================================================
Total params: 151
Trainable params: 151
Non-trainable params: 0
_________________________________________________________________
我们将定义用于编译模型的参数,例如优化器和损失函数,然后拟合模型。对于训练过程,我们将使用第二模型和新的训练数据输入和输出用于训练过程。在得到最佳预测之前,我们将训练我们的模型一千个时期。由于需要训练的数据样本相对较少,因此训练应该只需要几分钟。
model1.compile(optimizer = "adam", loss = "binary_crossentropy", metrics = "binary_accuracy")
model1.fit(x_train, y_train, epochs = 1000)
让我们对训练输入数据执行预测,并在训练过程完成后查看模型能够预测的输出。
model1.predict(x_train)
array([[0.01542357],
[0.995468 ],
[0.99343044],
[0.00554709]], dtype=float32)
我们可以注意到,输出值与各自的预期结果相当精确。当预期结果为 0 时,这些值更接近于 0,当预期结果为 1 时,这些值更接近于 1。最后,让我们分别通过与门和异或门的两个模型对两个预测的值进行舍入。按照预期输出的要求,执行这一步将帮助我们获得单个整数值。
model.predict(x_train).round()
model1.predict(x_train).round()
array([[0.],
[0.],
[0.],
[1.]], dtype=float32)
array([[0.],
[1.],
[1.],
[0.]], dtype=float32)
我们可以注意到,经过训练的两个模型都能够利用所提供的输入生成所需的输出。即使我们的数据量较少,但经过长时间的训练,该模型能够在减少损失的情况下达到预期的结果。从头开始学习神经网络所有要素的工作是相当漫长的。优化器、损失函数、各种损失函数和其他类似主题等复杂概念将在以后关于从头构建神经网络的文章中讨论。
结论:
Photo by Denny Müller / Unsplash
在本文中,我们展示了从头构建神经网络的大多数基本概念。在简单介绍之后,我们探索了理解人工神经网络如何工作所需的一些必要元素。一旦我们完成了基本的主题,我们就使用 NumPy 从头开始构建神经网络。我们试验了异或门,并建立了一个可以解决这个问题的人工神经网络。最后,我们还学习了如何借助深度学习框架,即 TensorFlow 和 Keras,构造 AND 和 XOR 等众多门的解决方案。
人工神经网络(ANN)和深度学习是一场革命,能够完成一些曾经被认为机器不可能完成的最复杂的任务。成功的人工智能和神经网络的旅程始于卑微的开端,从简单的感知器模型到复杂的 n 隐藏层架构构建。随着 GPU 的出现和经济计算的广泛普及,任何有兴趣了解如何创建这种模型和框架的人都变得越来越容易。神经网络的复杂性和概念数不胜数,尤其是当我们试图从零开始构建这些网络时,就像我们在本文中所做的那样。在以后的部分中,我们将探索更多从头构建神经网络的基本要素。
在接下来的文章中,我们将看到更多的生成性对抗性网络的变体,如 pix-2-pix GAN,BERT transformers,当然还有从头构建神经网络的第二部分。在那之前,继续探索和学习新的东西!
将完整的 ImageNet 预训练模型从 MXNet 转换为 PyTorch
原文:https://blog.paperspace.com/convert-full-imagenet-pre-trained-model-from-mxnet-to-pytorch/
目前有许多可用的深度学习框架,供研究人员和工程师实现他们想要的深度模型。每个深度学习框架都有自己的优缺点。例如,TensorFlow 有一个很好的社区,PyTorch 是一个很好的框架,可以在短时间内轻松开发模型,它还为生产级任务提供了一个很棒的 C++ API,MXNet 是一个很好的框架,可以用于非常大规模的培训(即,它是一个超可扩展的框架,可以在分布式系统和多个 GPU 上加速培训时间),等等。
作为一名深度学习研究人员/工程师,经常会发现一个奇妙的 GitHub 知识库,它在一个你不熟悉的框架上共享一个预先训练好的模型。例如,你是 PyTorch 深度学习代码开发专家,同时你在 MXNet 上发现了一个伟大的代码及其预训练模型;而你想根据自己的需求修改这个模型。此时此刻,深度学习模型转换工具将帮助你在短时间内做到这一点。
作为一个高层次的观点,深度学习框架中的每个模型都包含一些层(例如、卷积、全连接等)。)及其相关权重,并且在不同框架之间转换预训练模型是可行的任务。然而,由于每个框架都有自己的结构,在两个不同的框架之间转换模型需要对它们都有很好的了解。为了加快这一过程,一些工程师和公司提供了帮助者深度学习模型转换工具,通过这些工具,代码开发人员可以更容易地解决这一问题。
有很多模型转换工具,如 ONNX 、 MMdnn 等。你可以在这个 GitHub 知识库上找到大量的深度学习模型转换器:
https://github.com/ysh329/deep-learning-model-convertor
在可用的模型转换器中,微软支持的 MMdnn(模型管理深度神经网络)提供了在广泛的框架集合之间转换和可视化深度模型的奇妙工具。通过使用 MMdnn,可以将每个模型从原始框架转换为标准的中间表示 ( a.k.a. ,IR),然后将 IR 格式转换为目标框架结构。在本教程中,我想通过 MMdnn 转换器将完整的 ImageNet 预训练模型从 MXNet 转换为 PyTorch。这是熟悉 MMdnn 的一个合适的例子。
ImageNet 是根据 WordNet 层次结构组织的图像数据库,其中层次结构的每个节点由成百上千的图像描述。目前,它平均每个节点有超过 500 张图片。完整版本的 ImageNet 数据集的词典(,即,其标签和词的集合)包含 21,841 个标签(,又名,同义词集(synset))及其关联的 14,197,122 个图像。自 2010 年以来,一年一度的 ImageNet 大规模视觉识别挑战 (ILSVRC)是一项竞赛,研究团队在给定的数据集上评估他们的算法,并竞争在几项视觉识别任务上实现更高的准确性(参考文献)。ILSVRC 使用一个只有 1,000 个图像类别的“修剪”列表,其中有 1,281,167 个训练图像(参考)。换句话说,ILSVRC 引入了 ImageNet 完整版本的子集。
在 ImageNet 数据上训练一个网络的一个常见原因是用它来进行迁移学习(包括特征提取或者微调其他模型)(参考)。在这方面,许多深度学习框架,对于著名的和最先进的卷积神经网络(例如、ResNet、DenseNet 等。),在 ImageNet ILSVRC 数据集上提供预训练模型(参考)。据我所知,除了 MXNet,其他深度学习框架都没有在完整的 ImageNet 数据集上提供预训练模型。幸运的是,MXNet 团队推出了一个很好的教程,用于在完整的 ImageNet 数据集上训练 ResNet 模型。您可以参考下面的链接了解更多详情:
https://mxnet . incubator . Apache . org/versions/master/tutorials/vision/large _ scale _ classification . html
值得注意的是,拥有一个在如此庞大的训练数据集上训练的预训练模型(即,full ImageNet),将是一个真正有价值的网络。它可以在训练阶段早期加速收敛,也可以在某些场景下提高目标任务精度(例如,微调或特征提取)。换句话说,在不同的框架中具有所提到的有价值的模型(即,完整的 ImageNet 预训练模型),将是用于进一步修改的很好的初始化网络。
先决条件
为了跟进本教程,首先,您应该安装 MMdnn 工具。安装方法有几种(可以参考其 GitHub 页面)。在不同的安装方式中,我更喜欢通过 pip 来安装。为此,启动您的终端并输入命令:
sudo pip3 install --upgrade mmdnn
您还需要 PyTorch 来测试转换后的模型。您还可以使用下面的命令安装 PyTorch 的稳定版本:
sudo pip3 install --upgrade torch torchvision
您需要的另一个东西是 MXNet 上原始的完整 ImageNet 预训练模型,以及它的同义词集( a.k.a. ,synset 或它的类别列表)。您可以通过下面的 python 代码下载这些文件:
import os
import errno
_base_model_url = 'http://data.mxnet.io/models/'
_default_model_info = {
'imagenet11k-resnet-152': {'symbol':_base_model_url+'imagenet-11k/resnet-152/resnet-152-symbol.json',
'params':_base_model_url+'imagenet-11k/resnet-152/resnet-152-0000.params'},
}
def download_file(url, local_fname=None, force_write=False):
# requests is not default installed
import requests
if local_fname is None:
local_fname = url.split('/')[-1]
if not force_write and os.path.exists(local_fname):
return local_fname
dir_name = os.path.dirname(local_fname)
if dir_name != "":
if not os.path.exists(dir_name):
try: # try to create the directory if it doesn't exists
os.makedirs(dir_name)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
r = requests.get(url, stream=True)
assert r.status_code == 200, "failed to open %s" % url
with open(local_fname, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
return local_fname
def download_model(model_name, dst_dir='./', meta_info=None):
if meta_info is None:
meta_info = _default_model_info
meta_info = dict(meta_info)
if model_name not in meta_info:
return (None, 0)
if not os.path.isdir(dst_dir):
os.mkdir(dst_dir)
meta = dict(meta_info[model_name])
assert 'symbol' in meta, "missing symbol url"
model_name = os.path.join(dst_dir, model_name)
download_file(meta['symbol'], model_name+'-symbol.json')
assert 'params' in meta, "mssing parameter file url"
download_file(meta['params'], model_name+'-0000.params')
return (model_name, 0)
if __name__ == "__main__":
# ***** Download synset (i.e., Synonym Set):
synset_url = 'http://data.mxnet.io.s3-website-us-west-1.amazonaws.com/models/imagenet-11k/synset.txt'
download_file(synset_url, 'synset.txt')
# ***** Download Model:
download_model('imagenet11k-resnet-152', dst_dir='./')
将完整的 ImageNet 预训练模型从 MXNet 转换为 PyTorch
为了将下载的完整 ImageNet 预训练模型从 MXNet 转换到 PyTorch,您需要移动到下载模型的目录中,然后输入以下 3 个命令(我也分享了每个步骤的输出):
命令 1[需要几分钟(~ 3-5 分钟)]:
python3 -m mmdnn.conversion._script.convertToIR -f mxnet -n imagenet11k-resnet-152-symbol.json -w imagenet11k-resnet-152-0000.params -d resnet152 --inputShape 3,224,224
命令 1 的输出:
IR network structure is saved as [resnet152.json].
IR network structure is saved as [resnet152.pb].
IR weights are saved as [resnet152.npy].
命令 2:
python3 -m mmdnn.conversion._script.IRToCode -f pytorch --IRModelPath resnet152.pb --dstModelPath kit_imagenet.py --IRWeightPath resnet152.npy -dw kit_pytorch.npy
命令 2 的输出:
Parse file [resnet152.pb] with binary format successfully.
Target network code snippet is saved as [kit_imagenet.py].
Target weights are saved as [kit_pytorch.npy].
命令 3:
python3 -m mmdnn.conversion.examples.pytorch.imagenet_test --dump resnet152Full.pth -n kit_imagenet.py -w kit_pytorch.npy
命令 3 的输出:
PyTorch model file is saved as [resnet152Full.pth], generated by [kit_imagenet.py] and [kit_pytorch.npy].
测试转换后的模型
现在,我们在 PyTorch 上有了完整的 ImageNet 预训练 ResNet-152 转换模型。为了使用它(即,用它来分类图像),你可以使用下面实现的代码。请注意,它的图像预处理有点棘手(即,当您使用模型转换器时,源框架和目标框架之间的预处理必须相同)。
import torch
import numpy as np
from tensorflow.contrib.keras.api.keras.preprocessing import image
# ************** Parameters:
num_predictions = 5 # Top-k Results
model_address = 'resnet152Full.pth' # for loading models
lexicon_address = 'synset.txt'
test_image_address = 'seagull.jpg'
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Load Converted Model:
model = torch.load(model_address).to(device)
model.eval()
# Read Input Image and Apply Pre-process:
img = image.load_img(test_image_address, target_size=(224, 224))
x = image.img_to_array(img)
x = x[..., ::-1] # transform image from RGB to BGR
x = np.transpose(x, (2, 0, 1))
x = np.expand_dims(x, 0).copy()
x = torch.from_numpy(x)
x = x.to(device)
# Load Full-ImageNet Dictionary (i.e., lexicon):
with open(lexicon_address, 'r') as f:
labels = [l.rstrip() for l in f]
# Make prediction (forward pass):
with torch.no_grad():
output = model(x)
max, argmax = output.data.squeeze().max(0)
class_id = argmax.item()
class_name = labels[class_id]
# Print the top-5 Results:
h_x = output.data.squeeze()
probs, idx = h_x.sort(0, True)
print('Top-5 Results: ')
for i in range(0, num_predictions):
print('{:.2f}% -> {}'.format(probs[i] * 100.0, labels[idx[i]]))
str_final_label = 'The Image is a ' + class_name[10:] + '.'
print(str_final_label)
以下是一些示例及其相关结果:
样本 1:
前 5 名成绩:
48.38% - > n02041246 海鸥、海鸥、海鸥
26.61% - > n02041085 拉里德
10.03% - > n01517966 隆脊鸟、隆脊鸟、飞鸟
2.76% - > n02021795 海鸟、海鸟、海鸟
样本 2:
前 5 名成绩:
86.02% - > n02510455 大熊猫、熊猫、熊猫、浣熊、大熊猫
3.74% - > n02131653 熊
1.82% - > n01322983 小熊崽
1.19% - > n02075612 杂食
1.01
样本 3:
前 5 名成绩:
21.54% - > n04026813 推式自行车
21.42% - > n03792782 山地车、越野车
17.56% - > n04126066 安全自行车、安全自行车
13.79% - > n02836035 自行车轮
7.77
结论
在本教程中,您已经熟悉了深度学习模型转换工具,尤其是 MMdnn。您已经将有价值的完整 ImageNet 预训练模型从 MXNet 转换为 PyTorch,并且现在在 PyTorch 中拥有它!
下一步
下一步,我鼓励您尝试转换后的完整 ImageNet 模型,通过 Paperspace 机器对您将遇到的问题进行微调或特征提取。
关于作者:
我是高级深度学习研究员,巴彦视界集团CTO。我在机器学习、深度学习和计算机视觉方面经验丰富。我为这个行业在这些领域做了很多项目。
可以关注我的
(我的 LinkedIn 简介 )
(我的 GitHub 简介 )
(我的谷歌学术简介)
卷积自动编码器
卷积神经网络接收二维空间结构化数据实例(图像),并对其进行处理,直到产生某种类型的一维向量表示。这就引出了一个问题,如果从图像矩阵到向量表示的映射可以被学习,也许从向量表示到图像的映射也可以被学习。在这篇演示文章中,我们将探索这一点。
内容作为特征提取器
在以前的文章中,我提到了 convnet 中卷积层的作用是从图像中提取特征。然后,这些特征被传递给执行实际分类的线性层(利用 1 x 1 卷积层进行下采样的架构除外)。
考虑具有上述架构的 VGG-16,从输入层直到汇集的7 x 7 x 512
特征地图被展平以创建大小为 25,088 个元素的向量的点:网络的该部分充当特征提取器。从本质上来说,一个总共有 50,176 个像素的224 x 224
图像被处理以创建一个 25,088 个元素的特征向量,然后这个特征向量被传递到线性层用于分类。
由于这些特征是由一个 convnet 提取的,因此有理由认为另一个 convnet 可能理解这些特征,并将这些特征所属的原始图像放回一起,基本上与特征提取过程相反。这基本上就是一个 自动编码器 所做的。
自动编码器的结构
如前所述,自动编码器是深度学习架构,能够从其特征向量重建数据实例。它们处理各种数据,但本文主要关注它们在图像数据上的应用。自动编码器由 3 个主要组件组成;即一个 编码器 ,一个 瓶颈 和一个 解码器 。
编码器
自动编码器的第一部分,编码器是专门充当特征提取器的 convnet。它的主要功能是帮助从图像中提取最显著的特征,并将它们作为向量返回。
瓶颈
位于编码器之后的瓶颈,也称为代码层,作为一个额外的层,有助于将提取的特征压缩成一个更小的矢量表示。这样做是为了使解码器更难理解这些特征,并迫使它学习更复杂的映射。
解码器
自动编码器的最后一部分,解码器是 convnet,它试图理解来自编码器的特征,这些特征随后在瓶颈中被压缩,以便重建原始图像。
训练自动编码器
在本节中,我们将在 PyTorch 中从头开始实现一个自动编码器,并在一个特定的数据集上对其进行训练。
让我们从快速导入我们需要的包开始。
# article dependencies
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
from tqdm import tqdm as tqdm_regular
import seaborn as sns
from torchvision.utils import make_grid
import random
# configuring device
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
准备数据
出于本文的目的,我们将利用 CIFAR-10 数据集来训练卷积自动编码器。如下面的代码单元格所示,可以加载它。
# loading training data
training_set = Datasets.CIFAR10(root='./', download=True,
transform=transforms.ToTensor())
# loading validation data
validation_set = Datasets.CIFAR10(root='./', download=True, train=False,
transform=transforms.ToTensor())
接下来,我们只需要从数据集中提取图像。由于我们试图教一个自动编码器重建图像,目标将不是类标签,而是实际的图像本身。还从每个类中提取一个图像,并存储在对象“test_images”中,这只是为了可视化的目的,稍后将详细介绍。
def extract_each_class(dataset):
"""
This function searches for and returns
one image per class
"""
images = []
ITERATE = True
i = 0
j = 0
while ITERATE:
for label in tqdm_regular(dataset.targets):
if label==j:
images.append(dataset.data[i])
print(f'class {j} found')
i+=1
j+=1
if j==10:
ITERATE = False
else:
i+=1
return images
# extracting training images
training_images = [x for x in training_set.data]
# extracting validation images
validation_images = [x for x in validation_set.data]
# extracting test images for visualization purposes
test_images = extract_each_class(validation_set)
现在我们需要定义一个 PyTorch 数据集类,以便能够将图像用作张量。这与类实例化一起在下面的代码单元中完成。
# defining dataset class
class CustomCIFAR10(Dataset):
def __init__(self, data, transforms=None):
self.data = data
self.transforms = transforms
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
image = self.data[idx]
if self.transforms!=None:
image = self.transforms(image)
return image
# creating pytorch datasets
training_data = CustomCIFAR10(training_images, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))
validation_data = CustomCIFAR10(validation_images, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))
test_data = CustomCIFAR10(test_images, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))
自动编码器架构
本文定义了一个自定义卷积自动编码器架构,如下图所示。该架构旨在与 CIFAR-10 数据集一起工作,因为其编码器通过 3 个通道接收32 x 32
像素图像,并对其进行处理,直到生成 64 个8 x 8
特征地图。
这些特征图随后被展平以产生 4096 个元素的向量,该向量随后在瓶颈中被压缩到仅 200 个元素。解码器采用这个 200 个元素的向量表示,并通过转置卷积对其进行处理,直到返回一个3 x 32 x 32
图像作为输出。
上面定义的架构在下面的代码单元中实现。在这种情况下,参数“latent_dim”指的是瓶颈的大小,我们将它指定为 200。
# defining encoder
class Encoder(nn.Module):
def __init__(self, in_channels=3, out_channels=16, latent_dim=200, act_fn=nn.ReLU()):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1), # (32, 32)
act_fn,
nn.Conv2d(out_channels, out_channels, 3, padding=1),
act_fn,
nn.Conv2d(out_channels, 2*out_channels, 3, padding=1, stride=2), # (16, 16)
act_fn,
nn.Conv2d(2*out_channels, 2*out_channels, 3, padding=1),
act_fn,
nn.Conv2d(2*out_channels, 4*out_channels, 3, padding=1, stride=2), # (8, 8)
act_fn,
nn.Conv2d(4*out_channels, 4*out_channels, 3, padding=1),
act_fn,
nn.Flatten(),
nn.Linear(4*out_channels*8*8, latent_dim),
act_fn
)
def forward(self, x):
x = x.view(-1, 3, 32, 32)
output = self.net(x)
return output
# defining decoder
class Decoder(nn.Module):
def __init__(self, in_channels=3, out_channels=16, latent_dim=200, act_fn=nn.ReLU()):
super().__init__()
self.out_channels = out_channels
self.linear = nn.Sequential(
nn.Linear(latent_dim, 4*out_channels*8*8),
act_fn
)
self.conv = nn.Sequential(
nn.ConvTranspose2d(4*out_channels, 4*out_channels, 3, padding=1), # (8, 8)
act_fn,
nn.ConvTranspose2d(4*out_channels, 2*out_channels, 3, padding=1,
stride=2, output_padding=1), # (16, 16)
act_fn,
nn.ConvTranspose2d(2*out_channels, 2*out_channels, 3, padding=1),
act_fn,
nn.ConvTranspose2d(2*out_channels, out_channels, 3, padding=1,
stride=2, output_padding=1), # (32, 32)
act_fn,
nn.ConvTranspose2d(out_channels, out_channels, 3, padding=1),
act_fn,
nn.ConvTranspose2d(out_channels, in_channels, 3, padding=1)
)
def forward(self, x):
output = self.linear(x)
output = output.view(-1, 4*self.out_channels, 8, 8)
output = self.conv(output)
return output
# defining autoencoder
class Autoencoder(nn.Module):
def __init__(self, encoder, decoder):
super().__init__()
self.encoder = encoder
self.encoder.to(device)
self.decoder = decoder
self.decoder.to(device)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
通常,我们现在需要定义一个类来帮助训练和验证更加无缝。在这种情况下,由于我们正在训练一个生成模型,损失可能不会携带太多信息。一般来说,我们希望减少损失,我们还可以使用损失值来查看自动编码器在每个时期重建图像的效果。出于这个原因,我包含了一个可视化块,如下所示。
class ConvolutionalAutoencoder():
def __init__(self, autoencoder):
self.network = autoencoder
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set, test_set):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'visualizations': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
elif isinstance(module, nn.Linear):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
test_loader = DataLoader(test_set, 10)
# setting convnet to training mode
self.network.train()
self.network.to(device)
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
#------------
# TRAINING
#------------
print('training...')
for images in tqdm(train_loader):
# zeroing gradients
self.optimizer.zero_grad()
# sending images to device
images = images.to(device)
# reconstructing images
output = self.network(images)
# computing loss
loss = loss_function(output, images.view(-1, 3, 32, 32))
# calculating gradients
loss.backward()
# optimizing weights
self.optimizer.step()
#--------------
# LOGGING
#--------------
log_dict['training_loss_per_batch'].append(loss.item())
#--------------
# VALIDATION
#--------------
print('validating...')
for val_images in tqdm(val_loader):
with torch.no_grad():
# sending validation images to device
val_images = val_images.to(device)
# reconstructing images
output = self.network(val_images)
# computing validation loss
val_loss = loss_function(output, val_images.view(-1, 3, 32, 32))
#--------------
# LOGGING
#--------------
log_dict['validation_loss_per_batch'].append(val_loss.item())
#--------------
# VISUALISATION
#--------------
print(f'training_loss: {round(loss.item(), 4)} validation_loss: {round(val_loss.item(), 4)}')
for test_images in test_loader:
# sending test images to device
test_images = test_images.to(device)
with torch.no_grad():
# reconstructing test images
reconstructed_imgs = self.network(test_images)
# sending reconstructed and images to cpu to allow for visualization
reconstructed_imgs = reconstructed_imgs.cpu()
test_images = test_images.cpu()
# visualisation
imgs = torch.stack([test_images.view(-1, 3, 32, 32), reconstructed_imgs],
dim=1).flatten(0,1)
grid = make_grid(imgs, nrow=10, normalize=True, padding=1)
grid = grid.permute(1, 2, 0)
plt.figure(dpi=170)
plt.title('Original/Reconstructed')
plt.imshow(grid)
log_dict['visualizations'].append(grid)
plt.axis('off')
plt.show()
return log_dict
def autoencode(self, x):
return self.network(x)
def encode(self, x):
encoder = self.network.encoder
return encoder(x)
def decode(self, x):
decoder = self.network.decoder
return decoder(x)
设置好一切之后,我们就可以使用下面代码单元中指定的参数,将我们的 autoencoder 实例化为卷积 autoencoder 类的成员。
# training model
model = ConvolutionalAutoencoder(Autoencoder(Encoder(), Decoder()))
log_dict = model.train(nn.MSELoss(), epochs=10, batch_size=64,
training_set=training_data, validation_set=validation_data,
test_set=test_data)
从第一个时期结束时起,很明显,我们的解码器已经开始了解如何重建输入编码器的图像,尽管它只能访问压缩的 200 个元素的特征向量表示。直到第 10 个纪元,重建图像的细节也在不断增加。
Epoch 1 (top) vs Epoch 10 (bottom).
看看训练和验证的损失,自动编码器仍然可以从更多的训练中稍微受益,因为它的损失仍然呈下降趋势。验证损失的情况比训练损失的情况更严重,而训练损失似乎处于平稳状态。
瓶颈和细节
在前面的一节中,我提到了瓶颈代码层如何进一步压缩特征向量,从而迫使解码器学习更复杂和更一般化的映射。另一方面,由于编码层中的压缩幅度也会影响解码器重建图像的效果,因此需要寻求一种精细的平衡。
传递给解码器的矢量表示越小,解码器可访问的图像特征就越少,其重建就越不详细。同样,传递给解码器的矢量表示越大,它可以访问的图像特征就越多,重建的图像就越详细。按照这种思路,让我们训练相同的 autoencoder 架构,但是这次使用大小为 1000 的瓶颈。
# training model
model = ConvolutionalAutoencoder(Autoencoder(Encoder(latent_dim=1000), Decoder(latent_dim=1000)))
log_dict = model.train(nn.MSELoss(), epochs=10, batch_size=64,
training_set=training_data, validation_set=validation_data,
test_set=test_data)
根据每个时期生成的可视化,很明显,解码器在细节和视觉精度方面在重建图像方面做得更好。这归结于这样一个事实,即新的解码器可以访问更多的特征,因为 4096 个元素的原始特征向量现在被下采样到 1000 个元素,而不是 200 个元素。
Epoch 1 (top) vs Epoch 10 (bottom).
同样,自动编码器可以从更多的训练中受益。它的训练和验证损失仍然呈下降趋势,其斜率比我们在只有 200 个元素的瓶颈下训练我们的自动编码器时观察到的斜率更陡。
在第 10 个时期比较大小为 200 和 1000 的瓶颈清楚地表明,用 1000 个元素的瓶颈生成的图像比用 200 个元素的瓶颈生成的图像更清晰/更详细。
Bottleneck 200 (top) vs bottleneck 1000 (bottom) both at the 10th epoch.
训练到极致
卷积自动编码器的最佳训练点是什么?从我们训练的两个自动编码器中,我们可以观察到重建在第 10 个时期仍然是模糊的,即使我们的损失图已经开始变平。增加瓶颈大小只会在一定程度上改善这个问题,但不会完全解决它。
这部分归因于在这种情况下使用的损失函数,均方差,因为它在测量生成模型中的损失时做得不太好。在很大程度上,这些模糊的重建是卷积自动编码器任务的祸根。如果一个人的目标是重建或生成图像,生成性对抗网络(GAN)或扩散模型可能是一个更安全的赌注。然而,这并不是说卷积自动编码器没有用,因为它们可以用于异常检测、图像去噪等等。
结束语
在本文中,我们讨论了图像数据环境中的自动编码器。我们接着来看看卷积自动编码器到底是做什么的,以及它是如何工作的,目的是培养对其工作原理的直觉。此后,在进一步定义我们自己的自定义自动编码器、训练它并讨论模型训练的结果之前,我们触及了它的不同部分。
卷积神经网络维度和模型性能
原文:https://blog.paperspace.com/convolutional-neural-network-dimensions-model-performance/
许多因素会影响深度学习模型泛化功能或为特定数据集和预期目标创建映射的效果。在本文中,我们将看看宽度和深度等维度意味着什么,并研究它们如何影响卷积神经网络架构的整体性能。
# import dependencies
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
import seaborn as sns
from torchvision.utils import make_grid
规模
这听起来可能有点荒谬,但是 convnets 可以被认为是盒子。正如一个盒子通常具有宽度和深度一样,相同的约定也适用于 convnets。在本节中,我们将了解这些与 convnets 相关的维度。
宽度
也称为容量,卷积神经网络的宽度是指其层中存在的通道数量。在典型的 convnet 架构中,随着我们从一层到另一层,每层中的通道数量逐渐增加,按照相同的思路,这意味着网络会随着我们从一层到另一层而变得越来越宽。
至于宽度对卷积神经网络性能的影响,在这一点上,我们知道卷积层中的每个通道只是提取特征的特征图。具有更大宽度(每层通道数)的 convnet 将能够从输入图像中了解更多不同的特征。
用不太专业的术语来说,更宽的 convnet 更适合处理各种输入,尤其是在类有些相似的情况下。举例来说,考虑一个负责区分轿车和轿跑的 convnet。这是两种看起来非常相似的汽车,除了轿车有四个门,而轿车有两个门。为了了解两辆车之间的细微差异,在每一层提取大量特征将是有益的,因此可以通过网络学习区分功能。
虽然更宽的卷积神经网络可能更有益,但必须注意的是,网络越宽,它拥有的参数数量就越多。当参数过多时,过拟合就成为一种非常可能的可能性。
考虑下面的两种定制 convnet 架构,与 convnet_1 相比,convnet_2 更宽,因为其层中的通道数量更多。它从第 1 层的 16 个信道开始,在第 3 层的 64 层终止;而 convnet_1 从第 1 层的 8 个通道开始,在第 3 层的 32 个通道中达到顶点。
class ConvNet_1(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
self.pool1 = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(16, 32, 3, padding=1)
self.pool3 = nn.MaxPool2d(2)
self.conv4 = nn.Conv2d(32, 10, 1)
self.pool4 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 1, 28, 28)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
output_1 = self.pool1(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
output_3 = self.pool3(output_3)
#--------------
# OUTPUT LAYER
#--------------
output_4 = self.conv4(output_3)
output_4 = self.pool4(output_4)
output_4 = output_4.view(-1, 10)
return torch.sigmoid(output_4)
ConvNet_1
class ConvNet_2(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.pool1 = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
self.pool3 = nn.MaxPool2d(2)
self.conv4 = nn.Conv2d(64, 10, 1)
self.pool4 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 1, 28, 28)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
output_1 = self.pool1(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
output_3 = self.pool3(output_3)
#--------------
# OUTPUT LAYER
#--------------
output_4 = self.conv4(output_3)
output_4 = self.pool4(output_4)
output_4 = output_4.view(-1, 10)
return torch.sigmoid(output_4)
ConvNet_2
深度
卷积神经网络的深度是指它拥有的层数。深度决定了 convnet 可以学习的结构类型。
通常,较浅的层学习高级特征,而较深的层学习低级特征。用更专业的术语来说,使用人脸进行说明,虽然 convnet 的前几层将提取与人脸的整体结构相关的边缘,但更深的层将提取与眼睛、耳朵、鼻子、嘴等相关的边缘。
正如 convnet width 的情况一样,深度越大意味着参数越多,因此不受控制的深度也会导致网络过度拟合训练数据。下面的 Convnet_3 是 convnet_1 的复制品,但深度有所增加。请注意,虽然深度增加了,但宽度保持不变,因为额外的卷积层与其前面的层具有相同数量的通道,因此网络没有变宽。
class ConvNet_3(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
self.conv2 = nn.Conv2d(8, 8, 3, padding=1)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(8, 16, 3, padding=1)
self.conv4 = nn.Conv2d(16, 16, 3, padding=1)
self.pool4 = nn.MaxPool2d(2)
self.conv5 = nn.Conv2d(16, 32, 3, padding=1)
self.conv6 = nn.Conv2d(32, 32, 3, padding=1)
self.pool6 = nn.MaxPool2d(2)
self.conv7 = nn.Conv2d(32, 10, 1)
self.pool7 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 1, 28, 28)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
#-------------
# LAYER 4
#-------------
output_4 = self.conv4(output_3)
output_4 = F.relu(output_4)
output_4 = self.pool4(output_4)
#-------------
# LAYER 5
#-------------
output_5 = self.conv5(output_4)
output_5 = F.relu(output_5)
#-------------
# LAYER 6
#-------------
output_6 = self.conv6(output_5)
output_6 = F.relu(output_6)
output_6 = self.pool6(output_6)
#--------------
# OUTPUT LAYER
#--------------
output_7 = self.conv7(output_6)
output_7 = self.pool7(output_7)
output_7 = output_7.view(-1, 10)
return torch.sigmoid(output_7)
Convnet_3
基于维度的 Convnet 性能基准测试
在本节中,我们将基于宽度和深度来比较 convnet 的性能。Convnet_1 将用作基线,而 convnet_2 将用作宽度增加的 convnet_1 版本,convnet_3 将用作深度增加的版本。下面的 Convnet_4 结合了 convnet_2 和 con vnet _ 3 中分别增加的宽度和深度。
class ConvNet_4(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.conv2 = nn.Conv2d(16, 16, 3, padding=1)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(16, 32, 3, padding=1)
self.conv4 = nn.Conv2d(32, 32, 3, padding=1)
self.pool4 = nn.MaxPool2d(2)
self.conv5 = nn.Conv2d(32, 64, 3, padding=1)
self.conv6 = nn.Conv2d(64, 64, 3, padding=1)
self.pool6 = nn.MaxPool2d(2)
self.conv7 = nn.Conv2d(64, 10, 1)
self.pool7 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 1, 28, 28)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
#-------------
# LAYER 4
#-------------
output_4 = self.conv4(output_3)
output_4 = F.relu(output_4)
output_4 = self.pool4(output_4)
#-------------
# LAYER 5
#-------------
output_5 = self.conv5(output_4)
output_5 = F.relu(output_5)
#-------------
# LAYER 6
#-------------
output_6 = self.conv6(output_5)
output_6 = F.relu(output_6)
output_6 = self.pool6(output_6)
#--------------
# OUTPUT LAYER
#--------------
output_7 = self.conv7(output_6)
output_7 = self.pool7(output_7)
output_7 = output_7.view(-1, 10)
return torch.sigmoid(output_7)
基准数据集:FashionMNIST
FashionMNIST 数据集将用于我们的基准目标。这是一个数据集,包含 28 像素 x 28 像素的常见时尚配饰图像,如套头衫、包、衬衫、连衣裙等。该数据集预装了 PyTorch,可以这样导入:
# laoding training data
training_set = Datasets.FashionMNIST(root='./', download=True,
transform=transforms.ToTensor())
# loading validation data
validation_set = Datasets.FashionMNIST(root='./', download=True, train=False,
transform=transforms.ToTensor())
为了可视化数据集,让我们快速创建一个数据加载器,然后从一个批处理中提取图像进行可视化。注意:可能有更好的方法,如果你找到了一个,你应该遵循它,我出于习惯选择了这样做。
val_loader = DataLoader(validation_set, 32)
for images, labels in val_loader:
print(images.shape)
break
# visualising images
plt.figure(dpi=150)
plt.title('images')
plt.imshow(np.transpose(make_grid(images, padding=4, normalize=True),
(1,2,0)))
plt.axis('off')
plt.savefig('fmnist.png', dpi=1000)
Images in the FashionMNIST dataset.
标签 | 描述 |
---|---|
Zero | t 恤 |
one | 裤子 |
Two | 套衫 |
three | 连衣裙 |
four | 外套 |
five | 凉鞋 |
six | 衬衫 |
seven | 运动鞋 |
eight | 包 |
nine | 踝靴 |
卷积神经网络类
为了训练和利用我们的 convnets,让我们定义一个包含训练和预测的类,如下面的代码块所示。train()方法接受损失函数、要训练的时期数、批量大小、训练集和验证集等参数。
根据训练集和验证集,在函数本身中创建数据加载器。这样做是为了在实例化该类的一个成员时允许改变批量大小的可能性。请注意,数据加载器中的数据没有被打乱或随机采样,这是为了实现训练的一致性,这在我们将要比较几个模型时是必不可少的。
此外,在 train()方法中定义了两个内部函数,它们是 init_weights()和 accuracy()。Init_weights()用于确保所有要比较的模型的权重以相同的方式初始化,这将再次允许训练的一致性。除此之外,这也是一种可以让神经网络训练更快的措施。accuracy()函数顾名思义就是计算 convnet 的精度。
# setup device
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
class ConvolutionalNeuralNet():
def __init__(self, network):
self.network = network.to(device)
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=3e-4)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'training_accuracy_per_epoch': [],
'validation_accuracy_per_epoch': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# defining accuracy function
def accuracy(network, dataloader):
total_correct = 0
total_instances = 0
for images, labels in tqdm(dataloader):
images, labels = images.to(device), labels.to(device)
predictions = torch.argmax(network(images), dim=1)
correct_predictions = sum(predictions==labels).item()
total_correct+=correct_predictions
total_instances+=len(images)
return round(total_correct/total_instances, 3)
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
# training
print('training...')
for images, labels in tqdm(train_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# resetting gradients
self.optimizer.zero_grad()
# making predictions
predictions = self.network(images)
# computing loss
loss = loss_function(predictions, labels)
log_dict['training_loss_per_batch'].append(loss.item())
train_losses.append(loss.item())
# computing gradients
loss.backward()
# updating weights
self.optimizer.step()
with torch.no_grad():
print('deriving training accuracy...')
# computing training accuracy
train_accuracy = accuracy(self.network, train_loader)
log_dict['training_accuracy_per_epoch'].append(train_accuracy)
# validation
print('validating...')
val_losses = []
with torch.no_grad():
for images, labels in tqdm(val_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# making predictions
predictions = self.network(images)
# computing loss
val_loss = loss_function(predictions, labels)
log_dict['validation_loss_per_batch'].append(val_loss.item())
val_losses.append(val_loss.item())
# computing accuracy
print('deriving validation accuracy...')
val_accuracy = accuracy(self.network, val_loader)
log_dict['validation_accuracy_per_epoch'].append(val_accuracy)
train_losses = np.array(train_losses).mean()
val_losses = np.array(val_losses).mean()
print(f'training_loss: {round(train_losses, 4)} training_accuracy: '+
f'{train_accuracy} validation_loss: {round(val_losses, 4)} '+
f'validation_accuracy: {val_accuracy}\n')
return log_dict
def predict(self, x):
return self.network(x)
Model class.
基准测试结果
ConvNet_1
再次记住,convnet_1 是这个基准测试任务的基线。让我们使用交叉熵损失函数将 convnet_1 实例化为 ConvolutionalNeuralNet 类的成员,批处理大小为 64,然后训练它 10 个时期,如下所示。
# instantiating convnet_1
model_1 = ConvolutionalNeuralNet(ConvNet_1())
# training convnet_1
log_dict_1 = model_1.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
从获得的日志中,我们可以看到,总体而言,在整个训练过程中,训练和验证准确性都呈上升趋势,训练准确性高于验证准确性。仅在一个时期的训练之后,convnet 就达到了大约 54%的验证准确度,并且持续稳定地上升,直到在第十个时期终止在略低于 68%的水平。
# visualizing accuracies
sns.lineplot(y=log_dict_1['training_accuracy_per_epoch'], x=range(len(log_dict_1['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_1['validation_accuracy_per_epoch'], x=range(len(log_dict_1['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
ConvNet_2
Convnet_2 是 convnet_1 的一个版本,宽度有所增加。本质上,在层数相同的情况下,convnet_2 的宽度是 convnet_1 的两倍。保持所有参数不变,让我们训练 conv net _ 2 10 个时期,并可视化结果。
# instantiating convnet_2
model_2 = ConvolutionalNeuralNet(ConvNet_2())
# training convnet_2
log_dict_2 = model_2.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
总体而言,在整个培训过程中,培训和验证的准确性都有所提高。仅在一个历元之后,验证准确度约为 62%,比相同情况下的 convnet_1 高 8%。到第二个历元时,验证准确度确实下降到略低于 60%,但在第 10 个历元时,它恢复并继续增加到略低于 72%的顶点,此时比 convnet_1 高约 5%。
# visualizing accuracies
sns.lineplot(y=log_dict_2['training_accuracy_per_epoch'], x=range(len(log_dict_2['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_2['validation_accuracy_per_epoch'], x=range(len(log_dict_2['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
ConvNet_3
Convnet_3 是 convnet_1 的升级版。本质上它的深度是两倍,但宽度相同。保持所有参数相等,让我们训练 conv net _ 3 10 个时期并探索结果。
# instantiating convnet_3
model_3 = ConvolutionalNeuralNet(ConvNet_3())
# training convnet_3
log_dict_3 = model_3.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
就像前两个 convnets 一样,在整个模型训练过程中,观察到训练和验证准确性的总体提高。一个历元后达到的验证精度约为 49%,比同期的 convnet_1 低 5 个百分点。然而,到第三个时期,性能反弹并急剧增加到大约 72%,此后它波动,最终在第 10 个时期稳定在略低于 80%,完全盖过了 convnet_1 的性能。
# visualizing accuracies
sns.lineplot(y=log_dict_3['training_accuracy_per_epoch'], x=range(len(log_dict_3['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_3['validation_accuracy_per_epoch'], x=range(len(log_dict_3['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
ConvNet_4
Convnet_4 是 convnet_1 的版本,深度和宽度都有所增加。本质上,它的宽度是两倍,深度也是两倍。保持所有参数不变,让我们对 convnet_4 进行 10 个时期的训练,并总结其结果。
# instantiating convnet_4
model_4 = ConvolutionalNeuralNet(ConvNet_4())
# training convnet_4
log_dict_4 = model_4.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
总的来说,在模型训练过程中,训练和验证的准确性都有所提高。第一个历元后达到的验证精度约为 62%,比 convnet_1 高 7 个百分点。从第二个时期到第四个时期,验证准确性显著增加到略低于 76%的值,然后在下降之前略有增加,然后增加到略低于 78%。
# visualizing accuracies
sns.lineplot(y=log_dict_4['training_accuracy_per_epoch'], x=range(len(log_dict_4['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_4['validation_accuracy_per_epoch'], x=range(len(log_dict_4['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
比较性能
比较所有四个 convnets 的性能,我们可以推断,增加维度与模型性能正相关。在至少一个维度增加的所有情况下,convnet 性能都有显著提升。
基线 | 宽度增加(x2) | 增加的深度(x2) | 增加的深度(x2)和宽度(x2) |
---|---|---|---|
Fifty-four | Sixty-two | Forty-nine | Sixty-two |
Fifty-seven point five | Fifty-nine | Sixty-six point five | Sixty-eight |
Fifty-nine | Sixty-one | Seventy-two | Seventy-one point five |
Fifty-nine | Sixty-two point five | Seventy-one point five | Seventy-four point five |
Fifty-nine point five | Sixty-four point five | Seventy-one | Seventy-five point five |
Sixty-one | Sixty-five point five | Seventy-three | Seventy-six |
Sixty-one point five | Sixty-seven point five | Seventy-four | Seventy-six point five |
Sixty-three point five | Sixty-eight point five | Seventy-four | Seventy-six |
Sixty-six point five | Seventy point five | Seventy-eight | Seventy-six point five |
Sixty-seven | Seventy-one point five | Seventy-eight | Seventy-seven point five |
ConvNet_1 | ConvNet_2 | ConvNet_3 | ConvNet_4 |
所有单位均为百分比(%)
总体而言,convnet_3(增加的深度)似乎优于所有其他 convnet,convnet_4(增加的深度和宽度)紧随其后。Convnet_2(宽度增加)远远排在第三位,而基线 convnet_1 在所有四个中表现最差。然而,应该注意的是,convnets 只被训练了 10 个时期,为了得到更有结论性的结果,它们都应该被训练到最佳性能。
Comparing performances across all four convnets.
结束语
在本文中,我们探讨了卷积神经网络环境中的维度含义。我们创建了一个定制的 convnet 架构作为基准模型,然后继续创建其版本,一个增加了宽度,另一个增加了深度,最后一个同时增加了深度和宽度。
所有四个修道院都接受了 FashionMNIST 数据集的训练,以便相互比较。从获得的结果可以看出,当深度增加时性能最佳,而从基线模型观察到的性能最差。这些结果表明,卷积神经网络的维数实际上在 convnet 的性能方面起着至关重要的作用。
坐标注意解释
最佳注意力机制的竞赛仍在继续,因为今年最大的计算机视觉大会 CVPR 2021 又增加了一个注意力机制。这一个被称为坐标注意力,是在论文中提出的高效移动网络设计的坐标注意力。乍一看,注意力机制似乎是 T2 三重注意力和条带池的混合体,但更专门针对轻量级移动部署网络。
我们将首先看看工作背后的动机,然后跟进三重注意(旋转出席:卷积三重注意模块)和条带池(条带池:重新思考场景解析的空间池)的简明背景。然后,我们将分析所提出的机制的结构,并以本文给出的结果结束本文。
目录
- 动机
- 协调注意力
- PyTorch Code
- 结果
- 结论
- 参考
摘要
最近关于移动网络设计的研究已经证明了信道注意(例如,挤压和激发注意)对于提升模型性能的显著有效性,但是它们通常忽略了位置信息,而位置信息对于生成空间选择性注意地图是重要的。本文通过在信道注意中嵌入位置信息,提出了一种新的移动网络注意机制,我们称之为“坐标注意”。与通过 2D 全局池将特征张量转换为单个特征向量的通道注意力不同,坐标注意力将通道注意力分解为两个 1D 特征编码过程,这两个过程分别沿着两个空间方向聚集特征。这样,可以沿着一个空间方向捕获长程相关性,同时可以沿着另一个空间方向保留精确的位置信息。然后,将得到的特征图分别编码成一对方向感知和位置敏感的注意力图,它们可以互补地应用于输入特征图,以增强感兴趣对象的表示。我们的协调注意力很简单,可以灵活地插入到传统的移动网络中,如 MobileNetV2、MobileNeXt 和 EfficientNet,几乎没有计算开销。大量实验表明,我们的协调注意不仅有利于图像网络分类,而且更有趣的是,在下游任务中表现更好,如对象检测和语义分割。
动机
建筑设计已经成为计算机视觉领域中最近研究的主要领域。许多人可能会认为这一研究方向正在饱和,但研究正在挑战这些论点,在定制层和注意力机制的设计方面取得了一些新的进展。这篇文章的重点是后者——注意力机制。基于所提供的输入数据,注意机制本质上提供了关于关注“哪里”和“什么”的附加信息。一些注意方法,如挤压和激励(SE)、卷积块注意模块(CBAM)、三元组注意、全局上下文(GC)等,已经证明了这种插件模块的效率,以最小的计算复杂度增加显著提高了传统基线模型的性能。然而,对“最小”这个词要有所保留,因为模型一直在努力以尽可能低的开销实现注意力模块提高性能的好处。然而,设计这些注意机制的方法主要集中在大规模网络上,因为这些方法引入的计算开销使得它们不适用于容量有限的移动网络。此外,大多数注意机制只关注通道信息,由于否定了空间信息的存在而失去了表现力。
基于这些缺点:
在本文中,除了第一个工作,我们提出了一个新的和有效的注意机制,通过将位置信息嵌入到信道注意中,使移动网络能够覆盖大的区域,同时避免产生大量的计算开销。
作者将这种新颖的注意机制称为坐标注意,因为它的操作区分空间方向(即坐标)并生成坐标感知的注意地图。
协调注意力有以下优点。首先,它不仅捕获跨通道信息,还捕获方向感知和位置敏感信息,这有助于模型更准确地定位和识别感兴趣的对象。第二,该方法是灵活和轻量的,并且可以容易地插入到移动网络的经典构建块中,例如在 MobileNetV2 中提出的反向剩余块和在 MobileNeXt 中提出的沙漏块,以通过强调信息表示来增加特征。第三,作为一个预训练的模型,协调注意可以为移动网络的下游任务带来显著的性能增益,特别是对于那些具有密集预测的任务(例如,语义分割)。
协调注意力
正如文章前言中所暗示的,上图(c)所示的协调注意与 WACV 2021 和 CVPR 2020 分别发表的三联注意和条形池的结构有些相似。(剧透:这两篇论文也与《坐标注意》出自同一作者)。
条形池(CVPR 2020 年)
虽然本文的主要焦点是场景解析,但从结构上看,我们可以注意到条形池和坐标注意力架构设计之间的相似之处。条带池本质上采用输入张量\(X \in \mathbb{R}^{C \ast H \ast W}\)并且对于每个空间特征地图,它分别将其缩减为两个空间向量\(H \ast 1\)和\(W \ast 1\)。然后,这两个向量通过两个 1D 卷积核,再通过双线性插值过程,得到原始的\(H \ast W\)形状,最后再按元素相加。然后,该图经过逐点卷积,并通过在相乘之前对其应用 sigmoid 激活,与原始特征图进行元素相乘。
三重注意(WACV 2021)
三重注意在领域方面与协调注意更相关,它提供了一种结构,该结构通过使用置换操作隔离每个空间维度来基本上计算与通道信息相对应的空间注意。该论文将这一概念称为“跨维度相互作用(CDI)”。关于三重注意力的深入分析,请点击查看我的博文。
回到协调注意力,让我们剖析一下模块中发生了什么。如本节开头的图所示,坐标注意(Coord Att。)获取输入张量$ x \在\mathbb{R}^{C \ast H \ast W}\(并在两个空间维度\)H\(和\)W\(上应用平均池,并获得两个张量\) x ' \在\mathbb{R}^{C \ast H \ast 1}\(和\) x ' ' \在\mathbb{R}^{C \ast 1 \ast W}\(中。然后,这两个张量被连接以形成\) x ' ' ' ' \ in \mathbb{r}^{c \ ast 1 \ ast(h+w)} \(并随后通过 2D 卷积内核,该内核基于指定的缩减比率\)r\(将通道从\)C\(缩减为\)\frac{C}{r}\(。接下来是一个规范化层(在这种情况下是批处理规范),然后是一个激活函数(在这种情况下是硬 Swish)。最后,张量在\mathbb{R}^{\frac{C}{r}被拆分为\) \ hat { x } \在\mathbb{R}^{\frac{C}{r}被拆分为$ \ ast 1 \ ast w }。这两个张量分别通过两个 2D 卷积核,每个卷积核将通道从\(\frac{C}{r}\)增加回\(C\),最后对作为注意力图的结果的两个张量应用 sigmoid 激活。然后,注意力图按元素顺序与原始输入张量\(X\)相乘。
PyTorch Code
下面的代码片段提供了坐标注意力模块的 PyTorch 代码,该模块可以插入到任何经典的主干中。
import torch
import torch.nn as nn
import math
import torch.nn.functional as F
class h_sigmoid(nn.Module):
def __init__(self, inplace=True):
super(h_sigmoid, self).__init__()
self.relu = nn.ReLU6(inplace=inplace)
def forward(self, x):
return self.relu(x + 3) / 6
class h_swish(nn.Module):
def __init__(self, inplace=True):
super(h_swish, self).__init__()
self.sigmoid = h_sigmoid(inplace=inplace)
def forward(self, x):
return x * self.sigmoid(x)
class CoordAtt(nn.Module):
def __init__(self, inp, oup, reduction=32):
super(CoordAtt, self).__init__()
self.pool_h = nn.AdaptiveAvgPool2d((None, 1))
self.pool_w = nn.AdaptiveAvgPool2d((1, None))
mip = max(8, inp // reduction)
self.conv1 = nn.Conv2d(inp, mip, kernel_size=1, stride=1, padding=0)
self.bn1 = nn.BatchNorm2d(mip)
self.act = h_swish()
self.conv_h = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0)
self.conv_w = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0)
def forward(self, x):
identity = x
n,c,h,w = x.size()
x_h = self.pool_h(x)
x_w = self.pool_w(x).permute(0, 1, 3, 2)
y = torch.cat([x_h, x_w], dim=2)
y = self.conv1(y)
y = self.bn1(y)
y = self.act(y)
x_h, x_w = torch.split(y, [h, w], dim=2)
x_w = x_w.permute(0, 1, 3, 2)
a_h = self.conv_h(x_h).sigmoid()
a_w = self.conv_w(x_w).sigmoid()
out = identity * a_w * a_h
return out
结果
对于作者进行的实验的完整的广泛的结果,我强烈建议看一看这篇论文。在这里,我们只展示了在 ImageNet 上使用 MobileNet 和 MobileNext 进行图像分类的突出结果,以及在 MS-COCO 上进行对象检测的突出结果,其中坐标注意力展示了在性能方面的全面改进。
结论
虽然结果本身令人印象深刻,但提出的方法与论文的主要动机之一:移动网络的低成本关注相违背,因为事实上,与对比的两种方法( CBAM 和 SE )相比,协调关注在参数和失败次数方面都更昂贵。第二个缺点是比较的次数有限。虽然 SE 和 CBAM 是突出的注意方法,但还有更多性能更好、成本更低的注意模块,作者未能与 Triplet 和 ECA 进行比较。此外,坐标注意力模块不是针对 CBAM 的苹果到苹果的比较,因为前者使用硬 swish 作为其激活,这提供了显著的性能提升,然而,它是与使用 ReLU(不如硬 swish)的后者相比较的。
然而,要由读者来决定这篇论文是否公正,是否提供了另一种有效的注意力模式来尝试。
参考
新冠肺炎:我们拥有的数据,以及我们如何使用它
原文:https://blog.paperspace.com/coronavirus-background-and-data/
介绍
我们中的许多人目前感觉被困在他们家的范围内,被困在四堵墙之间,不敢冒险到户外去避免新冠肺炎的传播。这是一种新的疾病,已经席卷了整个世界,并迅速影响到我们许多人。
为了抗击这种疫情,重要的是过滤错误信息,了解我们现有数据的局限性,并了解模拟不同流行病学情景的公认模型及其做出的预测。今天,世界各国领导人肩负着重大责任,他们做出的错误决定可能会破坏国家经济,杀死许多人,并摧毁其他人的生活。
这篇文章将看看当我们谈论冠状病毒时,我们到底在处理什么,我们拥有什么样的流行病学和生物信息数据,我们可以从这些数据中安全地得出什么样的推论,以及我们应该远离什么样的推论。我们不仅讨论了数据科学家或病毒学家一直在做什么,政策制定者和公共卫生专业人员在这种情况下可以做什么,还讨论了每个公民如何通过传播正确的知识、照顾他们的亲人和避免疫情病毒进一步传播的所有方式来提供帮助。
具体来说,我们将了解:
- 新型冠状病毒和与其相似的病毒
- 重要流行病学概念入门
- 了解疫情的指数增长
- 冠状病毒危机的数据来源
- 病毒序列数据的数据来源
- 分析在危机中什么有效,什么无效
- 检测试剂盒、疫苗和治疗方法
- 针对个人和组织的缓解策略
对于那些有兴趣跳过生物学细节直接获得数据的人,请随意直接进入关于冠状病毒数据的章节。
病毒简介
病毒是纳米尺度的实体,处于无生命的尖端,但却以某种方式活着。它们通常由蛋白质/脂质外壳中的遗传物质(DNA 或 RNA)组成。它们通过附着在植物或动物细胞上并将其遗传物质插入细胞来进行复制。遗传物质最初由宿主细胞机器翻译,宿主细胞机器产生蛋白质和病毒遗传物质的更多拷贝。然后,新的病毒物质被包装到新产生的蛋白质外壳中,当细胞被病毒不可持续的繁殖杀死时,蛋白质外壳就会释放出来。
病毒识别宿主细胞表面的特定蛋白质,以结合并迫使细胞摄取病毒物质。许多细胞表面蛋白在相关物种中是相似的;这使得病毒在变异时可以杂交成新的物种。我们人类面临的许多病毒都源自其他动物。幸运的是,病毒对宿主久而久之的伤害越来越小。这是因为如果宿主迅速死亡,病毒自我繁殖的时间就会减少。病毒通常是最危险的,因为它们跨越物种,因为它们与前一物种平衡。大多数人类病毒在其来源物种中相对无害。常见的来源是家畜、蝙蝠和猿猴。
类似新型冠状病毒的病毒
严重急性呼吸系统综合症(SARS-CoV-1)
SARS,或称严重急性呼吸系统综合症,于 2002 年 11 月以非典型肺炎的形式在中国首次出现。到 1 月份,它开始在中国其他地区出现。2003 年 2 月下旬,一名护理受感染者的人在香港一家酒店住宿,并在 24 小时内感染了另外 16 名客人。这一事件导致了疾病的全球传播。截至 2013 年 7 月,共有 8096 人感染,774 人死亡。有助于控制 SARS-1 爆发的因素,但不幸的是,对新型冠状病毒的爆发没有帮助,因为 SARS-1 只有在症状可见后才具有传染性。它也有一套更一致的症状,不像新冠肺炎病毒可以在症状出现之前很久就传播,而且症状本身也可能是不一致的。
中东呼吸综合征
MERS 是一种由冠状病毒引起的疾病,于 2012 年 9 月在沙特阿拉伯首次发现。MERS 的常见症状是发烧、咳嗽、呼吸急促,通常还有肺炎。MERS 的死亡率高达 35%左右,但不容易在人与人之间传播,除非他们有非常密切的接触。MERS-CoV 的最初来源是骆驼,但从动物到人类的确切传播途径尚不清楚。MERS 总共感染了 2494 人,其中 858 人在 27 个国家死亡。
甲型 H1N1 流感疫情
1918 年的 H1N1 流感疫情是人类面对的最后一次疫情,其规模相当于目前正在进行的新冠肺炎疫情。该病毒起源于美国(可能是堪萨斯州),并由于第一次世界大战期间军队的大规模迁移而传播。由于没有已知的疫苗或治疗方法,加上战时国家之间缺乏沟通,后果是可怕的。两年内,全球范围内死亡人数超过 4000 万,仅在印度就有 1000-1200 万人死亡。新冠肺炎目前的全球轨迹导致了比 H1N1 更好的结果,这完全是由于迅速的反应和最近的技术进步。
流行病学导论
根据流行病学词典 , 流行病学是对特定人群中与健康相关的状态或事件的分布和决定因素的研究。这项研究的应用是控制健康问题。这个词来自希腊语短语 epi ,意思是“之上”; demos ,意为人;和逻各斯,意思是研究降临到一个群体身上的事情。流行病学领域关注人群因健康相关灾难而可能面临的频率和模式。模式将包括与时间、年龄、区域、性别等相关的死亡率统计数据,而与频率相关的统计数据将与人口规模相关的死亡率统计数据相关。通过了解疾病的传播方式,有可能在社会学层面上缩小疾病原因的范围,并帮助决策者制定和实施有效的缓解策略。
地方病、流行病和大流行病
流行病学根据规模对影响特定人群的疾病进行分类,以及它们在地理区域的流行程度。
地方病:在人群中持续很长时间的特定疾病。这些疾病有稳定的发病率,并且在地理上受到控制。疟疾,这是一种威胁生命但可治愈的疾病,由通过受感染的雌性按蚊叮咬传播给人的寄生虫引起,在被认为是撒哈拉以南非洲的地方病,但在英国没有。东南亚的世卫组织地区、东地中海、西太平洋和美洲也处于危险之中。
流行病:当一种地方病,通常是突然地,在其正常区域之外传播或在人群中的频率增加比预期的快得多时,它被称为流行病。一个例子是纽约州的伤寒疫情(1906-1907 年)。玛丽·梅伦,通常被称为“伤寒玛丽”,在她作为一个庄园和医院单位的厨师期间,将病毒传播给了大约 122 名纽约人。那一年,10,771 人死于伤寒(尽管这些统计数据根据不同的来源而有所不同,比如这个声称 1907 年只有 3,000 人而不是 10,771 人死亡)。
大流行:疫情是一种跨国家和大洲传播的流行病,例如艾滋病毒/艾滋病疫情,根据发表在《柳叶刀》上的报告,其全球发病率在 1997 年达到高峰,每年 330 万。流行病通常是由艾滋病毒、麻疹或流感等病毒引起的,并通过载体(如动物或人类)传播,在这一点上它们成为大流行。
流行病的指数式传播
在流行病学中,需要量化疾病爆发的严重程度。 SEIR 模型和 RO 是最广泛接受的量化模型。通过计算基本再生数来实现这一点的标准程序是:R[0] 。R[0] 被定义为在疫情开始时的整个传染期内,由单个受感染个体引起的二次感染的平均数量。如果 R[0] 如果 R[0]
适合新冠肺炎的基本流行病学模型是 SEIR 模型。该模型基于尚未暴露于病毒的易感个体、暴露的但无症状的已感染但尚未传播病毒的个体、感染的有症状并传播病毒的个体、移除的已从感染中康复或死亡的个体。个人按照前面提到的顺序经历这些阶段。
从一个状态到另一个状态的速率方程可以表示为如下的一阶微分方程:
d(s)/dt =-βsi+λ-s
d(E)/dt =βSI(+k)E
d(I)/dt = kE(γ+)I
d(R)/dt =γIR
其中,β是有效接触率,λ是出生率、死亡率,k 是从接触到感染的进展率,γ是清除率。为了模拟新冠肺炎,λ和可以安全地设置为零。进展率 k 是个体平均孵化率的倒数。清除率是恢复时间和死亡时间的加权平均值的倒数。
在这样的模型中,R[0] 为:
有关新型冠状病毒特定车型的详细信息,请参见本文的附录这里。SEIR 模型的 python 笔记本实现可以在这里找到。
Gabriel Goh 的流行病计算器是一个很好的工具,它可以让你根据上面的 SEIR 模型使用不同的数字来影响传播动态,并了解对人口的影响(根据干预开始的时间)。托马斯·普约在一个系列中做了一些很棒的分析,其中的第二部分决定了如果在疫情蔓延的整个过程中什么都不做,美国会有什么样的结果。下图可以看到。
源代码可以在这里找到。
在《华盛顿邮报》的这篇文章中可以找到另一篇惊人的文章,它让我们看到了流行病是如何传播的。作者设计了几个场景的模拟,其中人们(显示为点)自由移动,被迫接受隔离,遵守社会距离措施等。可视化使得理解流行病的传播更加直观。隔离受感染的病人如何影响人群的一个形象化的例子可以在下面看到。
同一篇文章中的另一个有趣的模拟显示了社会距离对人口的影响:当只有八分之一的人口被允许流动时会发生什么。
新冠肺炎(新型冠状病毒肺炎)
新冠肺炎疾病是由新型冠状病毒病毒引起的,这是一种大小在 60-140 纳米之间的β冠状病毒。冠状病毒通常导致 20%的普通感冒,MERS 和 SARS 是已知的严重感染病例。感染人类的病毒似乎来自蝙蝠或穿山甲,与这两种动物体内自然传播的冠状病毒具有大致相同的相似性。最初爆发的主要源头是武汉海鲜市场,但病毒是否起源于此尚属推测,但未经证实。它最初被认为是一种新型非细菌性肺炎,因为在 12 月中旬肺炎相关死亡人数激增。直到12 月 30 日,病原体才被确认为冠状病毒。
与 SARS-CoV-1 相似,病毒利用病毒外壳上的刺突蛋白进入宿主细胞。这种蛋白质附着在人类 ACE2 受体上,促进病毒和人类细胞膜的融合,并将病毒基因组插入人类细胞的细胞质中。
主要的临床症状是发烧、咳嗽、肌肉酸痛和呼吸困难,尽管这些症状在人与人之间并不一致。最近的研究也显示,相当一部分被感染的人没有表现出任何症状。在严重的情况下,疾病发展为 ARDS(急性呼吸窘迫综合征)、心律失常(心律不齐)和器官衰竭(肺/心脏/肾脏)。
冠状病毒危机
截至 2020 年 4 月 20 日,这种新型冠状病毒已经传播到至少 180 个国家,全球感染人数超过 240 万,死亡人数超过 16.6 万。迄今为止,欧洲受到的冲击最大。意大利死于新冠肺炎的人数最多,其次是西班牙、法国和英国。受到这场危机严重影响的其他国家包括伊朗、美国、德国和土耳其。为了应对危机和几个国家未能应对病毒的威胁,印度宣布完全封锁,并于最近将其延长至 5 月 3 日。
为了让研究更容易获得,并过滤掉错误信息,我们将尝试收集尽可能多的不同数据来源以及为解决危机而采取的不同举措。
冠状病毒危机的数据
您可以使用多种资源来收集数据,包括死亡人数、感染人数以及根据不同国家、地区和时间统计的康复人数。
利用这些数据,可以跟踪病毒的传播,确定风险区域,并相应地实施预防措施。
以下是一些不断更新危机数据的来源。
约翰·霍普斯金
约翰·霍普金斯大学系统科学与工程中心(JHU·CSSE)维护着一个基于网络的仪表板,它跟踪上面提到的不同数字,并在交互式地图上显示它们。这些数字还专门针对不同国家和不同国家的不同省份。还可以找到一些描述指数增长的死亡和感染的有趣图表。为该项目收集的数据是公开的,任何人都可以访问,可以在这里找到。在更新仪表板之前,他们确认了来自几个不同政府组织的统计数据,包括世卫组织、中国疾病预防控制中心、加拿大政府等。关于这项努力的更多信息可以在的博客文章中找到。你可以点击查看实时仪表盘。
Woldometer
Worldometer 还收集并定期更新活跃病例、危重病例和死亡病例的数量。有几个关于病毒潜伏期的静态图和研究片段,以及新感染和新恢复病例的病例图,这可能有助于确定疫情的拐点。任何有兴趣了解新冠肺炎的数据与 2003 年非典危机有何相似之处的人都可以在这里找到世卫组织关于疫情非典的数据。
数据分析
已经发表了许多文章,这些文章描绘了死亡人数、康复情况等。在不同的国家。几个仪表板(如上所述)以数字和统计数据、图表、饼图、地图、热图等形式提供了关于疫情的世界现状的可视化效果。因此,我们不会在本文中关注这些分析。
托马斯·普约(发表于 3 月 10 日和 20 日)的两篇系列文章做了一些很好的数据分析。第一部分有 30 多个译本。该系列第一部分中的一个更重要的图表(如下所示)描述了中国湖北冠状病毒相关事件的时间表。
橙色条显示诊断出的病例,灰色条显示实际感染人数。真正的病例只能通过回顾过去,并确定那些被诊断的人的症状开始的时间来发现。Pueyo 在这篇文章中指出:
“这意味着橙色条柱向您展示当局所知的情况,灰色条柱则展示真实发生的情况…”
在封锁一段时间后,诊断病例的数量迅速增加,并在此后不久开始减少,而实际冠状病毒病例的数量在封锁后迅速减少。
"如果你把橙色条叠加到 1/22,你会得到 444 种情况。现在把所有的灰色条加起来。它们加起来大约有 12,000 个案例。所以当武汉自认为有 444 例时,实际上多了 27 倍。如果法国认为它有 1400 个病例,它很可能有数万个病例。”
确诊病例数量迅速增加,然后开始下降,只是因为一个地区的医疗机构增加了医疗设施,同时实施了足够严格的隔离,以立即发现新病例,并避免将病毒暴露给未感染人群。他在文章中还提到:
直到 1 月 23 日武汉奥运会闭幕,你可以看看灰色的图表:它正呈指数增长。真实案例呈爆炸式增长。武汉一关闭,病例就慢下来了。1 月 24 日,当另外 15 个城市关闭时,真实病例的数量(再次,灰色)停滞不前。
他还强调了韩国的情况,在那里,病毒在最初的 30 天里被控制住了,直到一个病人把它传染给了成千上万的人。虽然他们的遏制努力可以在他们的传播计数中清楚地看到,但他们仍然为这一失误付出代价。
病毒序列
共享所有流感数据的全球倡议组织(Global Initiative on Sharing All influence Data)正在领导一项大规模的工作,以共享来自几个不同国家的新冠肺炎的基因组序列,使独立研究人员和组织更容易更好地了解该病毒,并有望开发出疫苗。这些数据包括来自中国、匈牙利、澳大利亚和越南等地的序列。基于这些数据, Nexstrain 已经创建了一个交互式仪表板,让您可以根据时间和地理可视化新冠肺炎的传播,以及提交给 GISAID 或由其收集的基因组序列。请记住,在 ORF1b 和 S 基因中看到的高突变率也在 SARS-CoV-1 中看到,但它们没有可观察到的影响,很可能是中性突变的例子。
你可以悬停在不同的数据点上,找到核苷酸突变、分歧和其他有趣的数据分析。你也可以在这里阅读他们最新的形势报告,他们分析了 723 个公开共享的新冠肺炎基因组。你可以在他们的 GitHub 上看到用于分析的脚本,并了解如何从这篇叙述中阅读系统进化树。
与 GISAID 类似,中国国家生物信息中心一直在收集不同冠状病毒的基因组序列。这些序列可供任何感兴趣的人下载。下载部分也提到了序列的质量。数据已经被分析用于基因分型图可视化、贝叶斯系统树和变异频率热图等。更多序列数据可在 NCBI SARS-Cov-2 序列页面获得。
该病毒大小为 30 KB,这意味着它由 30,000 个 RNA 核苷酸组成(A/U/G/C)。由于它是由 RNA 构成的病毒,所以它表现出较高的突变率(一个字母变成另一个字母)。这是正常的,没有证据表明这些变化会对临床结果产生任何可观察到的影响。然而,它对于追踪感染是有用的,因为一旦突变发生,它将在病毒的所有子代中可见,从而导致进一步的感染。
检测试剂盒、疫苗和治疗方法
在查看标准测试策略时,存在两种重要的技术。PCR(聚合酶链式反应)是一种 DNA 扩增技术,用于将少量 DNA 转化为大量 DNA,以便进行分析。结合荧光染料,PCR 可以告诉科学家有多少 DNA,这有助于确定病原体是否感染了人。
SARS-Cov-2 基因组是由单链 RNA 组成的,它必须转化为 DNA 才能进行 PCR。这是在一种叫做逆转录酶的酶的帮助下完成的。结合这两种技术,我们得到了 RT-PCR,这是目前可用于检测新冠肺炎的方法。如果你有所有需要的试剂,测试本身只需要一天的时间。样本从患者喉咙或鼻子的后部收集,运送到测试实验室,并通过使用一种名为 RNA 提取的过程将 RNA 从所有其他物质——蛋白质、细胞和酶——中分离出来,为测试做准备。一旦完成,用逆转录酶将 RNA 转化成 DNA,用 PCR 扩增病毒的 DNA,所用的核苷酸和引物将与病毒基因组的特定片段结合。如果一切顺利,它只会放大包含病毒的 DNA 片段,而不会放大其他任何东西。所有这些都发生在 PCR 机器中,该机器只有经过生物安全 3 级(以及最近的 BSL2)许可培训的个人才能操作,并且在几所大学和研究实验室中都可以使用。世卫组织实验室测试指导文件可在这里找到。
RT-PCR 测试是测试病原体的通用方法,需要针对不同感染和疾病的方案;用什么 RNA 提取方法,用什么 PCR 仪,用什么引物。世卫组织为 T2 新冠肺炎 T3 推荐的方案 T1 包括使用四个引物进行测试:其中两个针对 SARS-Cov-2 基因组的区域,有助于创建一种封装和保护病毒的蛋白质,一个针对整个 SARS 样病毒,第四个针对人类基因组,作为阳性质量控制。
抗体检测试剂盒另一方面,检测身体针对疾病产生的抗体。这可以检测出已经从感染中康复的人,因为抗体在康复后会持续几周。这不能用于诊断,因为感染后身体需要 7-10 天才能产生可检测水平的抗体,但可用于估计疾病的无症状传播。这些测试的另一个优点是它们能在半小时内返回结果。
目前有两种疫苗正在进行临床试验,另外 52 种正在等待批准进行临床试验。在目前处于临床试验中的两种方法中,LNP 包裹的 mRNA 是一种新的疫苗接种方法,它通过注射编码病毒蛋白的 mRNA 来工作,然后被免疫系统拾取。
世卫组织正在一项名为“团结”的大规模研究中对治疗新冠肺炎的有希望的候选药物进行测试。目前正在测试的由其他疾病改造而来的四种治疗方法是:
- 抗 HIV 药物利托那韦/洛匹那韦
- Remdesivir ,这是一种抗埃博拉药物,在发现单克隆抗体后被放弃
- 氯喹和羟氯喹,为非专利抗疟药
- 利托那韦/洛匹那韦与干扰素-β
减轻
要控制疫情,有几件事情必须协同努力,包括加速和积极的检测和追踪、病例隔离和检疫、社会距离和对病毒传播方式的认识,以及每个家庭可以做些什么来最大限度地保护亲人的安全。
卫生和寻求医疗保健
《世卫组织公众指南》敦促人们集体遵守一定的卫生标准,并注意降低自己和周围其他人感染病毒的风险。
建议每个人都应该:
- 经常洗手以杀死可能在你手上的病毒和病原体。
- 避免接触眼睛、鼻子和嘴巴,因为一旦被污染,你的手可以通过呼吸道将病毒转移到肺部,增加感染的风险。
- 练习呼吸卫生;咳嗽或打喷嚏时捂住嘴或鼻子,以减少将疾病传播给他人的机会。
- 避免接触表面。根据表面的类型,病毒倾向于在表面停留几个小时到几天。如果有,立即洗手。
- 如果您发烧、咳嗽或呼吸困难,请尽早寻求医疗帮助,以避免危及自己和他人的生命。这也将有助于国家当局更清楚地了解你所在地区的情况,以便他们能够相应地设计缓解战略。
积极测试和接触追踪
对抗感染的最好方法是通过对所有已知和疑似病例及其接触者进行积极的重复检测,正如 T2 的应对措施所示。只有当你意识到这些病例时,你才能通过隔离和隔离患者来有效地防止其传播。有感染风险的个人应该经常检测,因为一个人在出现症状之前就可以传播疾病。这反映在世卫组织的通信中概括为“测试、测试、测试”。
戴口罩
戴口罩的功效最近一直是一个有争议的话题。一些支持者错误地将南韩、日本和新加坡对新冠肺炎的成功回应归因于面具的作用。有证据表明,当感染者戴口罩时,戴口罩可以降低其他人被感染的风险。如果你有任何新冠肺炎症状,强烈建议你戴上口罩。目前世卫组织的指导方针不建议没有症状的人戴口罩,但许多国家正在实施政策,要求外出时戴口罩。世卫组织的指导方针最初是为了防止医护人员和其他在高风险环境中的重要工作人员缺乏口罩。鼓励在有充足供应的情况下佩戴自制口罩或普通口罩。不过,N95 口罩只应由高风险环境中的人或免疫力低下的人佩戴,而不应由普通公众佩戴。
还有一个问题是,人们感觉受到了保护,并以更危险的行为作为补偿。即使戴着口罩,也应该始终优先保持物理距离,避免接触面部或口罩。取下面罩时必须小心,以免接触到外部区域,并在取下面罩后立即洗手。不应该长时间佩戴口罩。外科口罩摘下后应立即丢弃在密闭的垃圾箱中。可重复使用的口罩每次使用后都要用洗涤剂清洗。
隔离和社会距离
在研究缓解策略时,Pueyo 的文章提到了英国对 ICU 病床的需求,用于不同类型的社会距离措施。
从图表中可以清楚地看到社会距离的影响:随着措施越来越严格,峰值的开始被推迟,同时总体上峰值降低。这使我们的死亡人数减少,有更多的时间来加强我们的医疗保健系统,同时降低医疗保健系统超载和崩溃的可能性。拥有一个不负担过重的医疗系统的另一个隐藏的好处是传染病会传播到护士、公共卫生工作者等。也降低了,这相当于许多目前被感染的人。
在正经历封锁的国家,人们偏执地认为实施的社交距离措施可能会延长,不仅是几周,而是几个月。除了这样一个事实,即有人主张这样的封锁是冒着国家经济崩溃的风险,没有以积极的速度部署适量的测试设施,社会距离措施没有做太多,但推迟了疫情。连续几个月不实施宵禁也有可能缓解危机。
结论
为了总结危机和缓解策略,让我们看看托马斯·普约系列的另一个非常重要的图表。
图表列出了抑制病毒的所有不同方法。看看新加坡、韩国和台湾等成功应对这种病毒的国家,很明显,通过检测、隔离、公共去污、医疗资源和社会距离等方面的适当努力,可以共同减缓和减少这种致命病毒的影响。如果各国能够在早期遏制危机,它们可能不必关闭公立和私立学校、大学、宗教场所等。但如果不采取措施,各国可能会被迫以经济萎缩和一些基本服务长期关闭来支付。
进一步阅读
- https://www.ncbi.nlm.nih.gov/books/NBK21523/
- https://www . who . int/emergencies/diseases/novel-coronavirus-2019
- https://meltingasphalt.com/interactive/outbreak/
- https://www . science mag . org/news/2020/03/coronavirus-cases-has-dropped-through-Korean-whats-secret-its-success
- https://www.nytimes.com/article/coronavirus-timeline.html
CPU 实例入门(Linux)
CPU 实例允许您在各种用例中按需扩展计算能力。我们很高兴地宣布他们在我们的最新版本中的可用性。在这里,我们将带您通过这些步骤来开始旋转您自己的。
1。报名
如果你还没有注册,要知道你需要有一张信用卡才能开始注册。
2。转到机器创建
选择新机器或+符号将带您进入入职流程的相同部分。
3。选择您的地区
4。选择您的操作系统
CPU 实例目前仅适用于 Ubuntu 14.04 和 Ubuntu 16.04
5。选择您的 CPU 实例
完全灰显且不可访问的磁贴目前要么即将推出,要么限量发行。如果你有兴趣进入,请联系 hello@paperspace.com。
6。选择您的存储器、相同规格的机器数量,并设置您的网络
在机器详细信息下,您现在可以添加具有相同规格的额外机器,并根据您的意愿命名它们。
7。添加附加选项
默认情况下,自动快照是打开的,但如果您愿意,也可以将其关闭。
8。付款
在右手边,你可以看到一个正在运行的计算器。
创建您的 Paperspace 计算机后,您将返回到控制台,在那里您可以实时看到它们的配置状态。
OpenAI 健身房入门:创建定制健身房环境
原文:https://blog.paperspace.com/creating-custom-environments-openai-gym/
OpenAI Gym 提供了许多令人敬畏的环境,从经典的控制任务环境到让您训练代理玩 Atari 游戏(如 Breakout、Pacman 和 Seaquest)的环境。然而,您可能仍然有一项任务需要创建一个自定义环境,而这并不是健身房的一部分。幸运的是,Gym 足够灵活,允许你这样做,这正是这篇文章的主题。
在这篇文章中,我们将设计一个定制的环境,包括驾驶一架直升机,同时避开半空中的障碍物。注意,这是开放人工智能健身房系列的第二部分,第一部分介绍的概念知识被认为是这篇文章的先决条件。因此,如果你没有读过第 1 部分,这里是链接。
你也可以用一个渐变社区笔记本在一个免费的 GPU 上运行本教程中的所有代码。
依赖/导入
我们首先从安装一些重要的依赖项开始。
!pip install opencv-python
!pip install pillow
我们也从必要的进口开始。
import numpy as np
import cv2
import matplotlib.pyplot as plt
import PIL.Image as Image
import gym
import random
from gym import Env, spaces
import time
font = cv2.FONT_HERSHEY_COMPLEX_SMALL
环境描述
我们正在创建的环境基本上是一个受 Dino Run 游戏启发很大的游戏,如果你与互联网断开连接,你可以在 Google Chrome 上玩这个游戏。有一只恐龙,你必须跳过仙人掌,避免撞到鸟。你走过的距离代表了你最终得到的回报。
在我们的游戏中,我们的特工不是恐龙,而是直升机驾驶员。
- 直升机必须覆盖尽可能多的距离以获得最大的回报。直升机必须避开一些鸟。
- 如果发生鸟撞,这一集就结束了。如果直升机燃料耗尽,这一集也会终止。
- 就像鸟一样,有漂浮的油箱(是的,贴近现实没分,我知道!)中,斩波器可以收集这些燃料以给斩波器加满燃料(固定为 1000 升)。
请注意,这将只是一个概念的证明,而不是最美观的游戏。然而,如果你想改进它,这篇文章会给你足够的知识来做到这一点!
设计环境时首先要考虑的是决定我们将使用哪种观察空间和行动空间。
- 观察空间可以是连续的,也可以是离散的。离散动作空间的一个例子是网格世界,其中观察空间由单元定义,代理可以在这些单元中的一个内。连续动作空间的一个例子是代理的位置由实值坐标描述。
- 动作空间可以是连续的,也可以是离散的。离散空间的一个例子是每个动作对应于代理的特定行为,但是该行为不能被量化。这方面的一个例子是马里奥兄弟,其中每个动作都会导致向左、向右移动、跳跃等。你的行为不能量化正在产生的行为,也就是说,你可以跳,但不能跳得更高、更高或更低。但是,在愤怒的小鸟这样的游戏里,你决定弹弓拉伸多少(你量化)。
直升机景观级
我们开始实现环境类ChopperScape
的__init__
函数。在__init__
函数中,我们将定义观察和动作空间。除此之外,我们还将实现其他一些属性:
- 这代表了我们的观察图像。
- 这定义了我们屏幕的合法区域,在这里可以放置屏幕的各种元素,比如菜刀和鸟。其他区域用于显示剩余燃料、奖励和填充等信息。
elements
:存储任何给定时间存储在屏幕上的活动元素(如菜刀、鸟等)。)max_fuel
:斩波器能容纳的最大燃油量。
class ChopperScape(Env):
def __init__(self):
super(ChopperScape, self).__init__()
# Define a 2-D observation space
self.observation_shape = (600, 800, 3)
self.observation_space = spaces.Box(low = np.zeros(self.observation_shape),
high = np.ones(self.observation_shape),
dtype = np.float16)
# Define an action space ranging from 0 to 4
self.action_space = spaces.Discrete(6,)
# Create a canvas to render the environment images upon
self.canvas = np.ones(self.observation_shape) * 1
# Define elements present inside the environment
self.elements = []
# Maximum fuel chopper can take at once
self.max_fuel = 1000
# Permissible area of helicper to be
self.y_min = int (self.observation_shape[0] * 0.1)
self.x_min = 0
self.y_max = int (self.observation_shape[0] * 0.9)
self.x_max = self.observation_shape[1]
环境要素
一旦我们确定了行动空间和观察空间,我们需要最终确定我们环境的元素。在我们的游戏中,我们有三个不同的元素:直升机、飞鸟和浮动加油站。我们将把所有这些实现为独立的类,它们继承自一个叫做Point
的公共基类。
点基类
Point
类用于定义我们观察图像上的任意点。我们用以下属性定义这个类:
(x,y)
:点在图像上的位置。(x_min, x_max, y_min, y_max)
:该点的允许坐标。如果我们试图将点的位置设置在这些限制之外,位置值将被限制在这些限制之内。name
:点的名称。
我们为此类定义了以下成员函数。
get_position
:获取点的坐标。set_position
:将点的坐标设置为某个值。move
:将点移动一定的值。
class Point(object):
def __init__(self, name, x_max, x_min, y_max, y_min):
self.x = 0
self.y = 0
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
self.name = name
def set_position(self, x, y):
self.x = self.clamp(x, self.x_min, self.x_max - self.icon_w)
self.y = self.clamp(y, self.y_min, self.y_max - self.icon_h)
def get_position(self):
return (self.x, self.y)
def move(self, del_x, del_y):
self.x += del_x
self.y += del_y
self.x = self.clamp(self.x, self.x_min, self.x_max - self.icon_w)
self.y = self.clamp(self.y, self.y_min, self.y_max - self.icon_h)
def clamp(self, n, minn, maxn):
return max(min(maxn, n), minn)
现在我们定义类别Chopper
、Bird
和Fuel
。这些类从Point
类派生而来,并引入了一组新的属性:
icon
:渲染游戏时,观察图像上将显示的点的图标。(icon_w, icon_h)
:图标的尺寸。
如果您正在查看渐变笔记本,用于图标的图像将随笔记本一起托管。
class Chopper(Point):
def __init__(self, name, x_max, x_min, y_max, y_min):
super(Chopper, self).__init__(name, x_max, x_min, y_max, y_min)
self.icon = cv2.imread("chopper.png") / 255.0
self.icon_w = 64
self.icon_h = 64
self.icon = cv2.resize(self.icon, (self.icon_h, self.icon_w))
class Bird(Point):
def __init__(self, name, x_max, x_min, y_max, y_min):
super(Bird, self).__init__(name, x_max, x_min, y_max, y_min)
self.icon = cv2.imread("bird.png") / 255.0
self.icon_w = 32
self.icon_h = 32
self.icon = cv2.resize(self.icon, (self.icon_h, self.icon_w))
class Fuel(Point):
def __init__(self, name, x_max, x_min, y_max, y_min):
super(Fuel, self).__init__(name, x_max, x_min, y_max, y_min)
self.icon = cv2.imread("fuel.png") / 255.0
self.icon_w = 32
self.icon_h = 32
self.icon = cv2.resize(self.icon, (self.icon_h, self.icon_w))
回到直升机场景类
回想一下第一部分中的内容,任何健身房Env
都有两个重要的功能:
reset
:将环境复位到初始状态,返回初始观察。step
:通过应用一个动作来执行环境中的一个步骤。返回新的观察、奖励、完成状态和其他信息。
在这一节中,我们将实现环境中的reset
和step
函数以及许多其他辅助函数。我们从reset
功能开始。
重置功能
当我们重置环境时,我们需要重置环境中所有基于状态的变量。这些因素包括消耗的燃料、阶段性回报以及环境中存在的元素。
在我们的例子中,当我们重置环境时,除了初始状态的斩波器,我们什么都没有。我们在图像左上角的区域随机初始化我们的斩波器。该区域占图像宽度的 5-10%,占图像高度的 15-20%。
我们还定义了一个名为draw_elements_on_canvas
的辅助函数,它基本上将游戏中出现的每个元素的图标放在观察图像中它们各自的位置上。如果位置超出允许的范围,则图标被放置在范围边界上。我们还打印剩余燃料等重要信息。
我们最后返回放置元素的画布作为观察。
%%add_to ChopperScape
def draw_elements_on_canvas(self):
# Init the canvas
self.canvas = np.ones(self.observation_shape) * 1
# Draw the heliopter on canvas
for elem in self.elements:
elem_shape = elem.icon.shape
x,y = elem.x, elem.y
self.canvas[y : y + elem_shape[1], x:x + elem_shape[0]] = elem.icon
text = 'Fuel Left: {} | Rewards: {}'.format(self.fuel_left, self.ep_return)
# Put the info on canvas
self.canvas = cv2.putText(self.canvas, text, (10,20), font,
0.8, (0,0,0), 1, cv2.LINE_AA)
def reset(self):
# Reset the fuel consumed
self.fuel_left = self.max_fuel
# Reset the reward
self.ep_return = 0
# Number of birds
self.bird_count = 0
self.fuel_count = 0
# Determine a place to intialise the chopper in
x = random.randrange(int(self.observation_shape[0] * 0.05), int(self.observation_shape[0] * 0.10))
y = random.randrange(int(self.observation_shape[1] * 0.15), int(self.observation_shape[1] * 0.20))
# Intialise the chopper
self.chopper = Chopper("chopper", self.x_max, self.x_min, self.y_max, self.y_min)
self.chopper.set_position(x,y)
# Intialise the elements
self.elements = [self.chopper]
# Reset the Canvas
self.canvas = np.ones(self.observation_shape) * 1
# Draw elements on the canvas
self.draw_elements_on_canvas()
# return the observation
return self.canvas
在我们继续下一步之前,让我们看看我们最初的观察是什么样子的。
env = ChopperScape()
obs = env.reset()
plt.imshow(obs)
因为我们的观察和游戏的游戏画面是一样的,我们的渲染函数也应该返回我们的观察。我们为两种模式构建功能,一种是在弹出窗口中呈现游戏的human
,而rgb_array
以像素数组的形式返回游戏。
%%add_to ChopperScape
def render(self, mode = "human"):
assert mode in ["human", "rgb_array"], "Invalid mode, must be either \"human\" or \"rgb_array\""
if mode == "human":
cv2.imshow("Game", self.canvas)
cv2.waitKey(10)
elif mode == "rgb_array":
return self.canvas
def close(self):
cv2.destroyAllWindows()
env = ChopperScape()
obs = env.reset()
screen = env.render(mode = "rgb_array")
plt.imshow(screen)
阶跃函数
现在我们已经有了reset
函数,我们开始实现step
函数,它将包含给定动作时将我们的环境从一个状态转换到下一个状态的代码。在许多方面,这部分是众所周知的我们环境的一部分,这也是大部分计划进行的地方。
我们首先需要列出在环境的一个过渡步骤中需要发生的事情。这基本上可以分为两个部分:
- 对我们的代理采取行动。
- 环境中发生的所有其他事情,例如非 RL 参与者的行为(例如鸟类和浮动加油站)。
所以我们先来关注一下(1)。我们为游戏提供动作来控制我们的直升机做什么。我们基本上有 5 个动作,分别是向右、向左、向下、向上移动或什么都不做,分别用 0、1、2、3 和 4 表示。
我们定义了一个名为get_action_meanings()
的成员函数,它会告诉我们每个动作被映射到哪个整数,以供我们参考。
%%add_to ChopperScape
def get_action_meanings(self):
return {0: "Right", 1: "Left", 2: "Down", 3: "Up", 4: "Do Nothing"}
我们还通过检查被传递的动作是否出现在动作空间中来验证它是否是有效的动作。如果不是,我们提出一个断言。
# Assert that it is a valid action
assert self.action_space.contains(action), "Invalid Action"
完成后,我们使用之前定义的move
功能相应地改变斩波器的位置。每个动作导致在各自方向上移动 5 个坐标。
# apply the action to the chopper
if action == 0:
self.chopper.move(0,5)
elif action == 1:
self.chopper.move(0,-5)
elif action == 2:
self.chopper.move(5,0)
elif action == 3:
self.chopper.move(-5,0)
elif action == 4:
self.chopper.move(0,0)
既然我们已经将动作应用到了直升机上,那么我们将重点放在环境的其他元素上:
- 鸟类从屏幕的右边缘随机产卵,概率为 1%(即一只鸟很可能每一百帧出现在右边缘一次)。这只鸟每帧向左移动 5 个坐标点。如果他们击中直升机,游戏结束。否则,一旦到达左边缘,它们就会从游戏中消失。
- 燃料箱从屏幕的底部边缘随机产生,概率为 1 %(即燃料箱很可能每一百帧在底部边缘出现一次)。这只鸟每帧向上移动 5 个坐标。如果他们撞上了直升机,直升机就会加满油。否则,一旦到达顶部边缘,它们就会从游戏中消失。
为了实现上面概述的特性,我们需要实现一个助手函数来帮助我们确定两个Point
物体(比如一架直升机/鸟,直升机/油箱)是否发生了碰撞。我们如何定义碰撞?当两个点的中心坐标之间的距离小于它们的维度之和的一半时,我们说这两个点发生了碰撞。我们称这个函数为has_collided
。
%%add_to ChopperScape
def has_collided(self, elem1, elem2):
x_col = False
y_col = False
elem1_x, elem1_y = elem1.get_position()
elem2_x, elem2_y = elem2.get_position()
if 2 * abs(elem1_x - elem2_x) <= (elem1.icon_w + elem2.icon_w):
x_col = True
if 2 * abs(elem1_y - elem2_y) <= (elem1.icon_h + elem2.icon_h):
y_col = True
if x_col and y_col:
return True
return False
除此之外,我们还得做一些簿记工作。每一步的回报是 1,因此,每集的情节返回计数器都更新 1。如果有碰撞,奖励-10,剧集终止。燃油计数器每减少一步就减少 1。
最后,我们实现我们的step
函数。我写了大量的评论来指导你。
%%add_to ChopperScape
def step(self, action):
# Flag that marks the termination of an episode
done = False
# Assert that it is a valid action
assert self.action_space.contains(action), "Invalid Action"
# Decrease the fuel counter
self.fuel_left -= 1
# Reward for executing a step.
reward = 1
# apply the action to the chopper
if action == 0:
self.chopper.move(0,5)
elif action == 1:
self.chopper.move(0,-5)
elif action == 2:
self.chopper.move(5,0)
elif action == 3:
self.chopper.move(-5,0)
elif action == 4:
self.chopper.move(0,0)
# Spawn a bird at the right edge with prob 0.01
if random.random() < 0.01:
# Spawn a bird
spawned_bird = Bird("bird_{}".format(self.bird_count), self.x_max, self.x_min, self.y_max, self.y_min)
self.bird_count += 1
# Compute the x,y co-ordinates of the position from where the bird has to be spawned
# Horizontally, the position is on the right edge and vertically, the height is randomly
# sampled from the set of permissible values
bird_x = self.x_max
bird_y = random.randrange(self.y_min, self.y_max)
spawned_bird.set_position(self.x_max, bird_y)
# Append the spawned bird to the elements currently present in Env.
self.elements.append(spawned_bird)
# Spawn a fuel at the bottom edge with prob 0.01
if random.random() < 0.01:
# Spawn a fuel tank
spawned_fuel = Fuel("fuel_{}".format(self.bird_count), self.x_max, self.x_min, self.y_max, self.y_min)
self.fuel_count += 1
# Compute the x,y co-ordinates of the position from where the fuel tank has to be spawned
# Horizontally, the position is randomly chosen from the list of permissible values and
# vertically, the position is on the bottom edge
fuel_x = random.randrange(self.x_min, self.x_max)
fuel_y = self.y_max
spawned_fuel.set_position(fuel_x, fuel_y)
# Append the spawned fuel tank to the elemetns currently present in the Env.
self.elements.append(spawned_fuel)
# For elements in the Ev
for elem in self.elements:
if isinstance(elem, Bird):
# If the bird has reached the left edge, remove it from the Env
if elem.get_position()[0] <= self.x_min:
self.elements.remove(elem)
else:
# Move the bird left by 5 pts.
elem.move(-5,0)
# If the bird has collided.
if self.has_collided(self.chopper, elem):
# Conclude the episode and remove the chopper from the Env.
done = True
reward = -10
self.elements.remove(self.chopper)
if isinstance(elem, Fuel):
# If the fuel tank has reached the top, remove it from the Env
if elem.get_position()[1] <= self.y_min:
self.elements.remove(elem)
else:
# Move the Tank up by 5 pts.
elem.move(0, -5)
# If the fuel tank has collided with the chopper.
if self.has_collided(self.chopper, elem):
# Remove the fuel tank from the env.
self.elements.remove(elem)
# Fill the fuel tank of the chopper to full.
self.fuel_left = self.max_fuel
# Increment the episodic return
self.ep_return += 1
# Draw elements on the canvas
self.draw_elements_on_canvas()
# If out of fuel, end the episode.
if self.fuel_left == 0:
done = True
return self.canvas, reward, done, []
看到它的实际应用
我们环境的代码到此结束。现在,使用一个采取随机行动的代理在环境中执行一些步骤!
from IPython import display
env = ChopperScape()
obs = env.reset()
while True:
# Take a random action
action = env.action_space.sample()
obs, reward, done, info = env.step(action)
# Render the game
env.render()
if done == True:
break
env.close()
Rendering the Environment
结论
各位,这部分到此为止。我希望这篇教程能让你对设计一个定制的 OpenAI 环境的一些考虑和设计决策有所了解。你现在可以试着创造一个你自己选择的环境,或者如果你愿意,你可以对我们刚刚为练习设计的环境做一些改进。一些现成的建议是:
- 你可以为直升机设定多重生命,而不是在第一次撞鸟时就结束。
- 设计一个邪恶的变异鸟类外星种族,它们也能够向直升机发射导弹,直升机必须避开它们。
- 做点什么当油箱和鸟相撞的时候!
有了这些建议,就结束了。编码快乐!
用渐变和 ml5.js 创建你自己的风格转移镜像
原文:https://blog.paperspace.com/creating-your-own-style-transfer-mirror/
在这篇文章中,我们将学习如何用 Paperspace 的渐变训练一个风格转移网络,并使用 ml5.js 中的模型来创建一个交互式风格转移镜像。这篇文章是一系列博客文章的第二篇,这些文章致力于在 Paperspace 中训练机器学习模型,然后在 ml5.js 中使用它们。你可以在这里阅读关于如何训练 LSTM 网络生成文本的第一篇文章。
风格转移
风格转换是以其他图像的风格重新组合图像的技术。 1
它最早出现在 2015 年 9 月,当时 Gatys et。al 发表论文一种艺术风格的神经算法。在这篇论文中,研究人员展示了深度神经网络,特别是卷积神经网络,如何开发和提取图像风格的表示,并将这种表示存储在特征图中。想法是然后使用学习的样式表示并且把它应用到另一个图像。更具体地说:
该系统使用神经表示来分离和重组任意图像的内容和风格,为艺术图像的创建提供神经算法。[...]我们的工作提供了一条通往算法理解人类如何创造和感知艺术意象的道路 2
基本上,你训练一个深度神经网络来提取图像风格的表示。然后,您可以将这个样式应用到内容图像(C)中,并创建一个新的图像(C[S] ),它包含 C 的内容,但是样式是 S。al 出版,其他类似的方法和优化也出版了。实时风格转换和超分辨率的感知损失Johnson 等人。艾儿希多推出了优化工艺的新方法,速度快了三个数量级 3 ,并且拥有高分辨率的图像。(你可以了解更多关于网络在传递风格时所做的技术细节,这里,这里和这个以前的 Paperspace 帖子。)
巴勃罗·毕加索于 1937 年在玻璃上绘画,分别由他的蓝色、非洲和立体主义时期的作品重新设计。吉恩·科岗。
浏览器中的样式转换镜像
在本教程中,我们将训练一个模型来捕捉和学习你想要的任何图像的风格。然后,我们将在浏览器中使用这个模型,用 ml5.js 创建一个交互式镜像,它将使用网络摄像头,并在捕获的图像上应用实时风格传输。以下是智利艺术家博罗罗于 1993 年使用通昆 Chungungo Pate 工厂制作的最终效果演示(请允许并启用您的网络摄像头):
https://paperspace.github.io/training_styletransfer/ml5js_example/
多亏了 ml5.js ,我们可以在浏览器上完整地运行这个模型。如果你没有读过的上一篇文章, ml5.js 是一个新的 JavaScript 库,旨在让机器学习对艺术家、创意程序员和学生等广大受众变得触手可及。该库在浏览器中提供对机器学习算法和模型的访问,构建在 TensorFlow.js 之上,没有其他外部依赖。
因此,我们将使用 Gradient 的 GPU 加速在 Python 中训练一个模型,将模型导出到 JavaScript,并使用 ml5.styleTransfer() 方法在浏览器上运行所有内容。
安装
您可以在这个库的中找到这个项目的代码。这个代码基于 github.com/lengstrom/fast-style-transfer的,它结合了 Gatys 的艺术风格的神经算法、Johnson 的实时风格转换和超分辨率的感知损失以及 Ulyanov 的实例归一化。
训练该算法需要访问 COCO 数据集。COCO 是一个大规模的对象检测、分割和字幕数据集。我们将使用的数据集版本总共约为 15GB。幸运的是,Paperspace 有公共数据集,你可以从你的工作中访问,所以没有必要下载。公共数据集会自动装载到您的作业和笔记本的只读/datasets
目录下。
安装图纸空间节点 API
我们将使用纸空间节点 API 或 Python API 。如果您没有安装它,您可以使用 npm 轻松安装它:
npm install -g paperspace-node
或者使用 Python:
pip install paperspace
(如果你愿意,也可以从 GitHub 发布页面安装二进制文件)。
创建 Paperspace 帐户后,您将能够从命令行使用您的凭据登录:
paperspace login
出现提示时,添加您的 Paperspace 电子邮件和密码。
如果您还没有 Paperspace 帐户,您可以使用此链接免费获得 5 美元!链接:https://www.paperspace.com/&R=VZTQGMT
培训说明
1)克隆存储库
从克隆或下载项目存储库开始:
git clone https://github.com/Paperspace/training_styletransfer.git
cd training_styletransfer
这将是我们项目的开始。
2)选择一个样式图像
将您想要训练风格的图像放在/images
文件夹中。
3)在渐变上运行你的代码
在资源库root
中,您会发现一个名为run.sh
的文件。这是一个脚本,其中包含我们将使用 run 来训练我们的模型的指令。
打开run.sh
并修改--style
参数以指向您的图像:
python style.py --style images/YOURIMAGE.jpg \
--checkpoint-dir checkpoints/ \
--vgg-path /styletransfer/data/imagenet-vgg-verydeep-19.mat \
--train-path /datasets/coco/ \
--model-dir /artifacts \
--test images/violetaparra.jpg \
--test-dir tests/ \
--content-weight 1.5e1 \
--checkpoint-iterations 1000 \
--batch-size 20
--style
应该指向您想要使用的图像。--model-dir
将是保存 ml5.js 模型的文件夹。 --test
是一个图像,将用于测试每个时期的进程。您可以在此处的和此处的代码的原始存储库中了解更多关于如何使用所有参数进行培训的信息。
现在我们可以开始训练了!类型:
paperspace jobs create --container cvalenzuelab/styletransfer --machineType P5000 --command './run.sh' --project 'Style Transfer training'
这意味着我们希望create
一个新的paperspace job
使用一个 Docker 镜像作为基础container
,该镜像预装了我们需要的所有依赖项。我们还想使用一个machineType P5000
,我们想运行command
./run.sh
来开始训练过程。这个project
将被称为Style Transfer training
当培训过程开始时,您应该看到以下内容:
Uploading styletransfer.zip [========================================] 1555381/bps 100% 0.0s
New jobId: jstj01ojrollcf
Cluster: PS Jobs
Job Pending
Waiting for job to run...
Job Running
Storage Region: East Coast (NY2)
Awaiting logs...
ml5.js Style Transfer Training!
Note: This traning will take a couple of hours.
Training is starting!...
在 P5000 机器上训练该模型需要 2-3 个小时。如果你想让这个过程更快,你可以选择一个更好的 GPU。如果您愿意,也可以关闭终端,训练过程仍将继续,不会中断。如果您登录到Paperspace.com,在渐变选项卡下您可以检查模型的状态。您也可以通过键入以下命令来检查它:
paperspace jobs logs --tail --jobId YOUR_JOB_ID
4)下载模型
完成后,您应该会在日志中看到以下内容:
Converting model to ml5js
Writing manifest to artifacts/manifest.json
Done! Checkpoint saved. Visit https://ml5js.org/docs/StyleTransfer for more information
这意味着最终模型已准备好,放在您的作业的/artifacts
文件夹中!
如果你去Paperspace.com,在渐变标签下检查你的工作,点击‘工件’你会看到一个名为/model
的文件夹。这个文件夹包含了架构和所有从我们的模型中学习到的权重。它们被移植成一种 JSON 友好的格式,web 浏览器可以使用这种格式,ml5.js 也可以使用这种格式来加载它。单击右边的图标下载模型或键入:
paperspace jobs artifactsGet --jobId YOUR_JOB_ID
这将下载包含我们训练好的模型的文件夹。一定要下载/ml5js_example/models
里面的模型。
现在我们准备在 ml5.js 中试用我们的模型!
5)使用模型
在我们项目的/root
中,你会发现一个名为/ml5js_example
的文件夹。这个文件夹包含了一个非常简单的例子,说明如何在 ml5 中加载一个风格转换模型,并使用相机作为输入(它使用 p5.js 来使这个过程更容易)。可以看原例子,代码这里。现在,您应该更改的行是这样的,在/ml5js_example/sketch.js
的结尾:
const style = new ml5.styleTransfer('./models/YOUR_NEW_MODEL');
YOUR_NEW_MODEL
应该是你刚下载的型号名称。
我们几乎准备好测试模型了。剩下的唯一一件事就是启动一个服务器来查看我们的文件。如果您使用的是 Python 2:
python -m SimpleHTTPServer
如果您使用的是 Python 3:
python -m http.server
请访问 http://localhost:8000 ,如果一切顺利,您应该会看到演示:
https://paperspace.github.io/training_styletransfer/ml5js_example/
关于训练和图像的一个注记
尝试不同的图像和超参数,发现不同的结果。避免带有大量图案的几何图形或图像,因为这些图像没有足够的网络可以学习的可识别特征。举个例子,这是用智利动态艺术家马蒂尔德·佩雷斯的绢本印刷。
https://paperspace.github.io/training_styletransfer/ml5js_example/geometric.html
您会注意到结果不如上一个示例,因为输入图像主要由规则的几何图案重复组成,几乎没有独立的特征。
更多资源
DALL-E:人工智能程序内部,从文本描述中创建图像
萨瓦尔多·达利。超现实主义的古怪大师。被一些人描述为一个淘气的挑衅者。许多人认为他是一个傲慢的破坏者。一些人认为他疯了。尽管如此,每个人都认为他拥有不可否认的技术精湛。简而言之,没有几个人能像达利那样给生活带来想象。作为一名有创造力的艺术家,达利鼓舞人心。
西班牙抢劫犯罪剧 La casa de papel 的创作者和 Elon Musk 支持的人工智能研究实验室 OpenAI 的更多科学创作者对此进行了暗示。虽然来自两个相距甚远的领域,但两组创作者都从达利那里获得了灵感。受欢迎的网飞秀中受达利启发的面具暗指达利的张扬个性,而 OpenAI 的人工智能产品 DALL-E 则提到了他作品中的“给生活带来想象”行为。
当 DALL-E 在 2021 年 1 月首次发射时,OpenAI 将 报告为一个 GPT 3 的 120 亿参数版本,训练它使用文本-图像对的数据集从文本描述中生成图像。在那个时候,关于它的名字 DALL-E 的起源的问题并不少见。然而,在体验了该系统的强大功能后,人们一致认为该应用程序将萨瓦尔多·达利的奇妙疯狂与 Pixar 同名电影中憨态可掬的迪士尼机器人 WALL-E 的轻松生活属性结合在一起。因此,它不仅是名副其实的 DALL-E,而且它正在做一些令人难以置信的不同。
DALL-E 是什么?
当时(直到今天)大多数人工智能产品都试图将一种感觉转化为另一种感觉。他们的工作通常围绕视觉的可信度(即图像分析、笔迹识别等)。)、语言(即翻译、内容版主等。)、语音(即说话人验证、文本到语音的转录、实时翻译等。),知识和搜索。例如,Meta(当时是脸书)的 wav2letter++ 和 Mozilla 的 DeepSpeech 将语音转换为文本——利用两种感官工作。但后来,DALL-E 出现了,它能够连接到想象的感觉,并像其他产品一样重新创造它。与谷歌搜索不同,DALL-E 不检索已经存在的图像。
最简单的说,DALL-E 由两部分组成——一个自动编码器和一个变压器。前者学习在压缩的潜在空间中准确地表现图像,而后者学习语言和这种离散的图像表现之间存在的相关性。在我看来,transformer 是 DALL-E 的核心,因为它允许模型生成与给定文本提示相匹配的新图像。此外,它还负责如何将语言和图像结合在一起,因此当模型被提示生成如下所示的“一辆停在停车场的绿色校车”的图像时,它能够吐出一些校车的创意设计,这些设计可能以前只存在于我们的想象中,特别是因为几乎所有的校车都是黄色的。
2022 年 4 月,OpenAI 公布了 DALL-E 2。该公司称能够以四倍于前一版本的分辨率生成更加真实和准确的图像,从而证明了许多人所描述的令人惊叹的图像视觉质量。令人印象深刻的是,该系统的智能更加惊人。尽管 OpenAI 并没有向所有人开放 DALL-E 2,但一个独立的开源开发者社区已经在他们可以访问的其他预训练模型的基础上构建了一些文本到图像的生成器。
其中一个发电机是 DALL-E Mini。它最近在拥抱脸上推出,并在互联网上掀起了风暴。该软件的名字和许多代码都来自 DALL-E 2。我用它工作,这里是我能注意到的关于这个系统的一些要点。该系统不仅能够以非凡的保真度和适应性将多种多样的艺术风格应用于特定的主题,更重要的是,它能够捕捉他们的精神/情绪。这意味着用该系统生成的卡通图像是轻松的,自然主义将是日常场景提示的主题,印象派绘画通常会表现得平静,同时给人以强烈的感觉,当然,该系统创建的黑色照片会微妙地令人不安。此外,生成的许多图像显示了 DALL-E 2 创造惊人的超现实主义图像的非凡能力。
如果在这一点上,你发现自己在思考 DALL-E 2 算法的工作原理,我认为这是公平的。在接下来的段落中,我将像向我在技术生态系统(而不是机器学习)工作的表弟解释算法一样解释算法,然后,我将如何向我非常熟悉机器学习的哥哥解释。
向我表哥解释
作为一点背景,需要注意的是,2015 年开发了自动图像字幕。那时,机器学习模型已经可以标记图像中的对象,但现在他们学会了将这些标签放入自然语言描述中。这一发展不仅使人们知道图像到文本的转换是可能的,而且也意味着翻转场景即文本到图像的转换是可能的。
现在,对于一个能够响应如此多不同提示的图像生成器来说,它需要一个大规模和多样化的训练数据集。理想情况下,数以百万计的图片和文字描述是从互联网上搜集来的。这些文字说明来自于网站所有者为了方便访问和搜索引擎而上传的图片文本。这就是工程师如何获得这些庞大的数据集。但是,模型实际上对数据集做了什么呢?你可能会认为,当我们给它一个文本提示,如“黄桌上的绿球”,它会搜索训练数据以找到相关的图像,然后复制其中的一些像素。然而,这种假设是错误的!因为新生成的图像不是来自训练数据,所以不会发生这种情况。相反,它来自深度学习模型的已学【潜在空间】。
当输入一个提示时,深度学习算法最初会检查所有的训练数据,找到有助于提高其任务性能的变量,在这个过程中,它会建立一个远不止三维的数学空间。这些轴代表了人类甚至无法识别或命名的变量。每个轴都有有意义的集群。例如,我们可以有一个轴来捕捉香蕉的本质,另一个轴代表 20 世纪 60 年代照片的纹理和颜色。创建了这些轴之后,文本就被传送到数学空间中的相应点。最后,数学空间中的点被转换成实际图像。这涉及到一个叫做扩散的生成过程。它从噪音开始,然后经过一系列迭代,将像素排列成对人类有意义的成分。
向我的哥哥解释
在生产层面,DALL-E 2 只是将文本作为输入,将图像作为输出。然而,这并不是一蹴而就的。这一过程的大致图示如下。
A high-level overview of CLIP. Above the dotted line, the CLIP training process is seen. Below the dotted line, the text-to-image generation process is shown: a CLIP text embedding is first fed to an autoregressive or diffusion prior to produce an image embedding, and then this embedding is used to condition a diffusion decoder which produces a final image. Note that the CLIP model is frozen during training of the prior and decoder - [Source]
图片说明-> { 通过剪辑文字嵌入}->->-{通过剪辑图片嵌入} - >解码器- >图片
从上图中可以看出,要理解 DALL-E 2 的体系结构,首先需要理解 CLIP——DALL-E2 的一个重要组成部分。对比语言图像预训练的缩写,它是一个神经网络,充当文本和图像之间的链接。同样由 OpenAI 开发的 CLIP 做的与 DALL-E 2 相反——它返回给定图像的最佳字幕。这是一个对比模型,意味着它将图像与其对应的标题进行匹配,而不是对它们进行分类。为了在 DALL-E 2 中做到这一点,它训练了两个编码器——一个将图像训练成图像嵌入,另一个将文本或字幕转换成文本嵌入。这意味着它能够创建一个表示图像和语言特征的向量空间。这个共享的向量空间构成了 DALL-E 模型的架构基础,它提供了一种图像-文本字典,可以在两者之间进行转换。
The diffusion process - [Source]
此外,前面解释中提到的扩散模型是一种生成模型,其工作原理是通过高斯噪声的迭代添加来破坏训练数据,然后通过反转该过程来学习恢复数据。
给我表弟的提示:这个过程类似于拥有一个排列好的魔方,一边在脑海中记下每一步棋,一边摸索,然后做完全相反的事情,直到从摸索的魔方中获得一个排列好的魔方。
演示-从-到
2021 年 3 月, OpenAI 在 DALL E public 制作了用于离散变分自动编码器(dVAE)的 PyTorch 包。离散 VAE (dVAE)是 VQ-VAE(矢量量化变分自动编码器)的变体,是 DALL-E 用来学习离散图像表示的自动编码器技术。安装了所需的库,获得了要处理的样本图像,预处理了样本图像,然后 PyTorch 包的应用程序代码如下所示。
from dall_e import map_pixels, unmap_pixels, load_model
from IPython.display import display, display_markdown
import torch.nn.functional as F
z_logits = enc(x)
z = torch.argmax(z_logits, axis=1)
z = F.one_hot(z, num_classes=enc.vocab_size).permute(0, 3, 1, 2).float()
x_stats = dec(z).float()
x_rec = unmap_pixels(torch.sigmoid(x_stats[:, :3]))
x_rec = T.ToPILImage(mode='RGB')(x_rec[0])
display_markdown('Reconstructed image:')
display(x_rec)
您可以在渐变笔记本中运行此代码,方法是单击下面的链接,然后打开 usage.ipynb 笔记本:
演示-自 E 2
最后,DALL-E 2 对之前的版本进行了改进,它能够编辑现有的图像。例如,我们可以‘添加一个沙发’关于一个空的客厅。DALL-E 2 发布后不久,OpenAI 首席执行官山姆·奥特曼在推特上写道‘AGI 将变得狂野’。在某些情况下,根据维基百科的说法,AGI(人工通用智能)意味着智能代理理解或学习人类可以完成的任何智力任务的能力。基本上,通用 AI。现在,首先,我想指出的是,在图像生成方面,DALL-E 2 无疑令人印象极其深刻。然而,尽管我同意/认为 AGI 的潜力可以被认为是“狂野的”,但我质疑 DALL-E 2 在解决常识推理、可靠性、理解力和其他真正通用的人工智能所需的素质等深层问题方面是否构成了进步。
AGI 会很疯狂
— Sam Altman (@sama) April 6, 2022
DALL-E Mini:在微型模型中生成强大的图像
在计算机视觉研究和外行使用计算机视觉应用的世界中,DALL-E 已经成为公认的开创性项目之一。虽然艺术质量在某些方面受到影响,如构图,即能够以某种有意义的方式合并多个对象属性(如形状、位移或颜色)的特性,但 DALL-E 是用户在现实的短时间内生成自己的艺术作品的最容易获得的选项之一。
问题是 DALL-E 仍然需要一个或多个具有大量内存的高级 GPU,以便能够以有利于在某种应用程序中使用的方式这样做,并且使用这样强大的 GPU 的服务器成本是不可持续的。Boris Dayma 领导的 Craiyon 团队与 Hugging Face 合作,发现了这个问题,并迅速推出了解决方案。
[Source]
在短短的几周内,他们的产品 DALL-E Mini 或 Craiyon(为避免与最初的 DALL-E 和 DALL-E 2 混淆而改名)诞生了。借助谷歌强大的 JAX 库,Craiyon 能够在很短的时间内,用相对较少的 GPU 计算,几乎模仿其更大的堂兄弟的功效。通过这种方式,Craiyon 能够仅通过一个文本提示就快速生成高质量的艺术品。这是一个真正值得庆祝的进步,Craiyon 已经通过他们的网络应用席卷了全世界。
在今天的教程中,我们将讨论 Craiyon 如何工作,Craiyon 在 JAX 而不是更传统的 TensorFlow 或 Torch 中编写的一些原因,分解 Craiyon 架构的一些关键功能以生成艺术,然后将 Craiyon 与 DALL-E 进行比较和对比。然后我们将跳转到一个可以在渐变笔记本中访问的编码演示,这样您就可以开始使用 Craiyon 生成自己的作品或尽快制作自己的 DALL-E Mini 应用程序。
有关使用 DALL-E Mini 图块在渐变笔记本中运行 DALL-E Mini 的教程,请跳到“代码演示”一节
DALL-E Mini 是如何工作的?
开发 JAX 的基本工作流程是首先训练 PyTorch 模型 VQGAN。这是一个预训练、保存的检查点,它首先在 ImageNet 上进行微调,缩减因子 f=16,词汇量为 16384。这很重要,因为它解释了我们将在后面看到的许多人为因素。这是因为该模型对于处理各种各样的图像来说非常健壮,但是对于处理人或人脸编码来说优化较差。作者选择尝试通过更多的微调来解决这个问题,但是,很可能由于模式崩溃,这是一个失败。
剩下的代码在 JAX,所以我们先来谈谈为什么使用 JAX 来实现这个模型。
JAX
JAX 是由谷歌研究院开发的一个相对较新的库。与 TensorFlow 不同,它不是一个官方产品,因此随着它在稳定性方面的发展,它在研究社区中变得非常受欢迎。
JAX 利用即时编译的能力,允许用户捕获最大数量的 FLOPs 来生成优化的代码。这种实时(JIT)编译能够极大地提高计算速度。此外,除了通过自动签名在 CPU 上运行外,JAX 还支持在 GPU/TPU 上运行 NumPy 代码。这三种能力使得 JAX 可以在很大程度上加速张量运算。
[Source]
从上面的图中可以看出,在使用多层感知器进行的这一系列测试中,对于大多数用例来说,采用 jit 的 GPU 上的 JAX 表现出比竞争对手的 DL 库显著的速度提升。值得注意的是 Pytorch nn。总体而言,线性 MLP 速度最快,但我们可以假设这可能适用于特定的边缘情况,例如使用特别线性的数据。对于我们的计算机视觉来说,线性或任何其他混淆因素都不可能克服 JAX 的速度和效用。
克雷永是如何工作的?
既然我们理解了为什么使用 JAX 而不是 PyTorch 或 TensorFlow,让我们从 Craiyon 开始吧。让我们来看看架构。
架构概述
[Source]
克雷永架构在执行上过于简单,但简单中蕴含着力量。从上面关于训练的图表中我们可以看到,图像和描述通过一系列不同的算法来获得类似于输入提示的图像输出。
首先进行训练,通过 VQGAN 编码器对图像进行编码,以将图像转换为令牌。类似地,文本描述也将被带到 BART 编码器进行转录。接下来,BART 编码器的输出和编码图像通过 BART 解码器。这是一个自回归模型,能够获取信息并尝试准确预测序列中的下一个令牌。
[Source]
既然模型已经被训练来准确预测这些标记,我们就可以使用标题来生成图像。使用输入的字幕作为提示,基于解码器对下一个标记的预测分布,图像标记被顺序采样。BART 解码器然后基于来自 BART 编码器和 VQGAN 编码器的编码输出一系列样本候选图像编码。然后,这些序列由 VQGAN 解码器解码成近似于样本序列的图像。最后, CLIP 对输出的图像进行排序,并选择生成的最佳图像显示源。VQGAN 和 CLIP 再次显示出和再次显示出在一起使用时非常有效。
Craiyon 和 DALL-E 2 有什么不同?
[Source]
DALL-E 2 是目前公开可用的性能最高的图像生成器之一,仅次于 Imagen & Parti ( FID 为 7.3 )的未在极其强大的 MS-COCO 数据集上训练的模型。它通过使用 CLIP 来指导 OpenAI 的另一个计算机视觉模型 GLIDE 的修改版本的生成工作。查看这篇文章,了解 GLIDE 如何使用扩散模型先验生成图像。它们共同创造了一个强大的系统,其能力大于各个部分的总和。然而,累积起来,这使得模型变得庞大且运行起来昂贵。
这是创建克雷永的主要动力之一。克雷永比最初的 DALL-E 小 27 倍,这是因为人们努力制造出如此轻便的模型,使得克雷永如此普及。
[Source]
话虽如此,DALL-E 2 一点也不臃肿。模型大小的极端差异可以归因于导致类似输出的主要架构差异。DALL-E 2 图像生成任务的典型管道如上图所示,非常简单。正如我们所看到的,这个架构只是在接受和产生的内容上看起来相似。实现这一点的各种过程在两种模型之间有很大的不同。
我们看到的情况如下:
- 首先,剪辑文本编码器将图像描述映射到表示空间
- 那么扩散先验从剪辑文本编码映射到相应的剪辑图像编码
- 最后,修改的滑动生成模型经由反向扩散从表示空间映射到图像空间,生成传达输入字幕【源】内的语义信息的许多可能图像之一
这个框架无疑是强大的,但是比 DALL-E Mini 需要更多的计算开销。现在我们已经了解了什么是 DALL-E Mini,并看到了它与其同名产品相比是如何工作的,让我们来看看使这一切成为可能的代码。
代码演示
由于 DALL-E Mini/Craiyon 需要 JAX 和 GPU 支持才能运行,Gradient 是您访问所需计算的绝佳场所。您可以在此处查看此代码的公开版本,并通过转到笔记本创建页面,选择任何 GPU 机器,然后切换“高级选项”来创建自己的版本然后,使用这个 Github repo 作为“工作区 URL”,使用下面的标签作为“容器名”
paperspace/gradient-base:pt112-tf29-jax0314-py39
我们在这里使用的代码来自最初的研究团队提供的 DALL-E 迷你推理管道笔记本。这可以在 HuggingFace 这里找到。
必需的安装
第一步是安装我们需要的所有软件包。下面是 PyTorch 容器在 Gradient 上合适的 JAX 安装。注意,这些可能很挑剔,所以如果您在使用 CUDA 早期版本(< 11.0)或 cuDNN 不同版本的机器上运行这些代码,可能需要适当地重新配置。
我们还将安装 DALL-E 迷你库本身,以及子组件 vqgan-jax。
# Install required libraries
!pip install pyyaml==5.4.1 --ignore-installed
!pip install -q dalle-mini
!pip install -q git+https://github.com/patil-suraj/vqgan-jax.git
#setup for easy display
!pip install ipywidgets
!pip install --upgrade tqdm
设置
为了建立我们的推理和图像生成空间,通常我们首先需要从 Weights & Biases 或 HuggingFace Hub 获取将要使用的模型版本。这个版本来自 Weights & Biases,因此您需要创建一个帐户来访问 API 密钥。幸运的是,这可以免费完成。下面是指向预训练的 DALL-E Mini 和 VQGAN 模型的代码。
在 Gradient 笔记本中,我们可以通过装载已向用户提供的公共数据集,以平台的独特方式访问这些数据。建议使用这种方法,因为它速度更快。
https://blog.paperspace.com/content/media/2022/07/dalleminiupload.mp4
How to mount the dataset
要装载数据集,只需导航到屏幕左侧并选择第三个选项“Data Sources”选项卡。然后,单击“Public”切换到公共数据集,然后选择“dalle-mini-models”数据集并单击“Mount”这将把文件移动到../datasets/dalle-mini-models
以便于访问。
# Model references: pull from mounted public dataset
# imports
import jax
import jax.numpy as jnp
# dalle-mega
DALLE_MODEL = "../datasets/dalle-mini-models/dallebart" # can be wandb artifact or 🤗 Hub or local folder or google bucket
DALLE_COMMIT_ID = None
# if the notebook crashes too often you can use dalle-mini instead by uncommenting below line
# DALLE_MODEL = "dalle-mini/dalle-mini/mini-1:v0"
# VQGAN model
VQGAN_REPO = "../datasets/dalle-mini-models/vqgan-jax"
VQGAN_COMMIT_ID = "e93a26e7707683d349bf5d5c41c5b0ef69b677a9"
```py
如果您选择从 web 下载模型数据,您可以使用以下代码来完成:
Model references: pull entire model from web (~5 GB)
dalle-mega
DALLE_MODEL = "dalle-mini/dalle-mini/mega-1-fp16:latest" # can be wandb artifact or 🤗 Hub or local folder or google bucket
DALLE_COMMIT_ID = None
if the notebook crashes too often you can use dalle-mini instead by uncommenting below line
DALLE_MODEL = "dalle-mini/dalle-mini/mini-1:v0"
VQGAN model
VQGAN_REPO = "dalle-mini/vqgan_imagenet_f16_16384"
VQGAN_COMMIT_ID = "e93a26e7707683d349bf5d5c41c5b0ef69b677a9"
为了实际加载模型,我们可以使用 dalle_mini 和 vqgan_jax 包中提供的方法。正如我们在上一节中所描述的,DALL-E Mini 利用了 VQGAN 和 BART。运行下面单元格中的代码,将模型下载到您的笔记本中。
Load models & tokenizer
from ipywidgets import FloatProgress as IProgress
from dalle_mini import DalleBart, DalleBartProcessor
from vqgan_jax.modeling_flax_vqgan import VQModel
from transformers import CLIPProcessor, FlaxCLIPModel
utils.logging.disable_progress_bar()
Load dalle-mini
model, params = DalleBart.from_pretrained(
DALLE_MODEL, revision=DALLE_COMMIT_ID, dtype=jnp.float16, _do_init=False
)
Load VQGAN
vqgan, vqgan_params = VQModel.from_pretrained(
VQGAN_REPO, revision=VQGAN_COMMIT_ID, _do_init=False
)
然后,我们在每个设备上复制模型参数,以便更快地进行推断。这是为了利用您可能使用的任何多机实例。我们在一台 2 x A5000 机器上进行梯度测试。
from flax.jax_utils import replicate
params = replicate(params)
vqgan_params = replicate(vqgan_params)
我们可以对我们的模型函数做同样的事情。下面的代码片段将对它们进行编译和并行化,以利用我们的多种设备。
from functools import partial
model inference
@partial(jax.pmap, axis_name="batch", static_broadcasted_argnums=(3, 4, 5, 6))
def p_generate(
tokenized_prompt, key, params, top_k, top_p, temperature, condition_scale
):
return model.generate(
**tokenized_prompt,
prng_key=key,
params=params,
top_k=top_k,
top_p=top_p,
temperature=temperature,
condition_scale=condition_scale,
)
decode image
@partial(jax.pmap, axis_name="batch")
def p_decode(indices, params):
return vqgan.decode_code(indices, params=params)
最后,我们生成随机密钥传递给我们的模型,以确保生成的图像的唯一性。
import random
create a random key
seed = random.randint(0, 2**32 - 1)
key = jax.random.PRNGKey(seed)
### 准备文本输入
为了接收我们的文本片段提示,我们的模型需要处理函数来加载数据。我们将使用提供的 DalleBartProcessor。这可以使用我们安装的软件包直接创建,也可以从 Weights & Biases 下载整个模型包(不推荐)。
Create the processor piece by piece
from dalle_mini.model.configuration import DalleBartConfig
from dalle_mini.model.text import TextNormalizer
from dalle_mini.model.tokenizer import DalleBartTokenizer
from dalle_mini.model.utils import PretrainedFromWandbMixin
tokenizer = DalleBartTokenizer.from_pretrained('dalle-mini/dalle-mega')
config = DalleBartConfig.from_pretrained('dalle-mini/dalle-mega')
processor = DalleBartProcessor(tokenizer, config.normalize_text, config.max_text_length)
您也可以直接下载模型数据,但这会强制进行完整下载,并且可能会很耗时:
Download all model files (~5 GB)
from dalle_mini import DalleBartProcessor
processor = DalleBartProcessor.from_pretrained(DALLE_MODEL, revision=DALLE_COMMIT_ID)
然后我们可以实例化我们的提示。我们应该尽力提交具有模型熟悉的特征的图像,因此避免不太为人所知的专有名词、行话和技术术语。您可以使用它来更改生成的图像的内容并修改其样式。这里有一些我认为有趣的提示示例。
prompts = [
"fine art painting of a foolish samurai warrior wielding a magic sword stepping forth to oppose the evil that is Aku",
"Barack Obama holding up the World Cup trophy",
"Obi Wan Kenobi standing over lava with a lightsaber"
]
然后,我们通过将提示复制到每台设备上来完成设置。我们也可以多次使用同一个提示来加快推断速度。
tokenized_prompts = processor(prompts)
tokenized_prompt = replicate(tokenized_prompts)
### 推理
现在,我们已经设置好一切,开始生成我们的图像。让我们为我们的推理过程设置几个快速参数。特别是,改变`n_predictions`将影响它生成图像的次数,而`temperature`将影响从提示中处理出来的 ngrams 的数量/长度,作为编码的标记。
number of predictions per prompt
n_predictions = 8
We can customize generation parameters (see https://huggingface.co/blog/how-to-generate)
gen_top_k = None
gen_top_p = None
temperature = None
cond_scale = 10.0
最后,我们到达我们的训练循环。对于每个步骤,我们使用 p_generate 为每个使用 DALL-E BART 编码器的标记化提示生成图像编码。然后,编码会删除序列开始(BOS)标记,然后传递给 VQGAN 解码器。然后,解码器获取图像编码,每个图像编码对应于提示列表中的单个提示,并为每个图像编码生成图像数据。这些然后显示给我们看。
from flax.training.common_utils import shard_prng_key
import numpy as np
from PIL import Image
from tqdm.notebook import trange
print(f"Prompts: {prompts}\n")
generate images
images = []
for i in trange(max(n_predictions // jax.device_count(), 1)):
# get a new key
key, subkey = jax.random.split(key)
# generate images
encoded_images = p_generate(
tokenized_prompt,
shard_prng_key(subkey),
params,
gen_top_k,
gen_top_p,
temperature,
cond_scale,
)
# remove BOS
encoded_images = encoded_images.sequences[..., 1:]
# decode images
decoded_images = p_decode(encoded_images, vqgan_params)
decoded_images = decoded_images.clip(0.0, 1.0).reshape((-1, 256, 256, 3))
for decoded_img in decoded_images:
img = Image.fromarray(np.asarray(decoded_img * 255, dtype=np.uint8))
images.append(img)
display(img)
print()
输出:
![](https://github.com/OpenDocCN/geekdoc-dl-zh/raw/master/paperspace/img/0e735b9b94136b814f8cdc22d85c6cf5.png)
Sample images generated using DALL-E Mini on 2xA5000
成功!如果一切都做对了,你应该会得到三个看起来非常诡异的山谷人形表演我们描述的动作。正如你所看到的,许多非人形的特征看起来比人类特征更真实。图案、衣服和被拿着的物品都相当接近我们对它们在低分辨率下的预期。然而,面部和头部尤其是一个真正的斗争,我们可以从它重新创建巴拉克·奥巴马头部的令人钦佩的尝试中看出,它在面部上表现最好,由于他的普遍受欢迎程度,这些面部更有可能出现在他们的训练数据中。Craiyon 团队打算进一步训练他们的模型来尝试和补救这一点,但在本文发布时还不可用。
### 对产出进行排序
下一步,我们可以选择使用 CLIP 来评估和排列新生成图像的质量。这就是生产这种产品的想法发挥作用的地方。通过这种剪辑集成,我们可以消除对评估生成图像质量的人工检查的需要,并在实践中自动使用 Craiyon。
为此,您首先需要加载预训练的剪辑模型和处理器。然后我们创建一个简短的函数`p_clip`来给我们的图像打分。
CLIP model
CLIP_REPO = "openai/clip-vit-base-patch32"
CLIP_COMMIT_ID = None
Load CLIP
clip, clip_params = FlaxCLIPModel.from_pretrained(
CLIP_REPO, revision=CLIP_COMMIT_ID, dtype=jnp.float16, _do_init=False
)
clip_processor = CLIPProcessor.from_pretrained(CLIP_REPO, revision=CLIP_COMMIT_ID)
clip_params = replicate(clip_params)
score images
@partial(jax.pmap, axis_name="batch")
def p_clip(inputs, params):
logits = clip(params=params, **inputs).logits_per_image
return logits
接下来,我们使用`clip_processor`为剪辑模型准备我们的图像,然后将它们提交给`p_clip`来计算我们将用来对图像进行排序的分数。
from flax.training.common_utils import shard
get clip scores
clip_inputs = clip_processor(
text=prompts * jax.device_count(),
images=images,
return_tensors="np",
padding="max_length",
max_length=77,
truncation=True,
).data
logits = p_clip(shard(clip_inputs), clip_params)
organize scores per prompt
p = len(prompts)
logits = np.asarray([logits[:, i::p, i] for i in range(p)]).squeeze()
最后,我们可以在提示中使用这个 logits 值,根据图像与原始提示编码的排序接近度来显示图像的有序排序。
for i, prompt in enumerate(prompts):
print(f"Prompt: {prompt}\n")
for idx in logits[i].argsort()[::-1]:
display(images[idx * p + i])
print(f"Score: {jnp.asarray(logits[i][idx], dtype=jnp.float32):.2f}\n")
print()
以下是另一组提示的一些示例:
![](https://github.com/OpenDocCN/geekdoc-dl-zh/raw/master/paperspace/img/b080edb0b1c18b01b0372de70199fea3.png)
The more detailed images that better approximate the original prompt will receive higher scores
## 结论
在本文中,我们分析了 DALL-E Mini/Craiyon 的理由和灵感,探索了它的前辈以进行比较,并使用 Python 代码实现了光照图像生成器。这种用于图像生成的轻量级模型的实用性已经得到了证明。最初的团队已经将 DALL-E Mini 项目更名为 [Craiyon](https://www.craiyon.com/) ,并由于其可访问性而在互联网上取得了巨大成功。
如果你想了解更多关于 DALL-E Mini 及其与 DALL-E 2 的关系,请务必查看我们在 YouTube 频道上关于 DALL-E 的技术讲座。
[https://www.youtube.com/embed/92gVkdQigNY?feature=oembed](https://www.youtube.com/embed/92gVkdQigNY?feature=oembed)
您可以在这里的 [Github repo 中找到我们在本文中使用的代码。你也可以在 GPU 驱动的渐变笔记本](https://github.com/gradient-ai/dalle-mini) [这里](https://console.paperspace.com/ml-showcase/notebook/r9xxyd0pguepnvl?file=%2FDALL-E-Mini-inference-pipeline.ipynb)访问代码。
# 数据扩充:一种类不平衡缓解措施
> 原文:<https://blog.paperspace.com/data-augmentation-a-class-imbalance-mitigative-measure/>
在上一篇文章中,我们讨论了类不平衡对 convnet 性能的影响,以及特定模型目标的实现。我们还讨论了一些有助于处理类不平衡的方法,在这一点上提到了上采样。在本文中,我们将更详细地了解上采样,看看它如何应用于图像数据。
```py
# article dependencies
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
from tqdm import tqdm as tqdm_regular
import seaborn as sns
from torchvision.utils import make_grid
import random
# setting up device
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
上采样
不平衡数据集环境中的上采样指的是将少数类中的图像数量提高到与多数类中的图像数量相匹配的过程。正如我前面提到的,这可以通过为少数类收集更多的数据来实现,也可以通过从现有数据中创建新的数据实例来补充差异。从先前存在的数据创建新数据实例的过程被称为 数据扩充 。
图像数据增强
至于图像,我们如何从那些已经存在的图像中生成新的图像呢?我们不一定需要利用生成模型(尽管这是一个非常可行的选择)。一个简单得多的技术是创建原始图像的副本,并对它们进行足够微妙的转换,以使它们被视为新图像。
请记住,我们可以把图像想象成一堆像素——像素是代表强度的数字。如果我们找到转换或处理这些数字的方法,我们可以得到一组新的数字,它们保留了原始图像的大部分整体属性,同时又足够清晰,可以被视为一个不同的图像。如果这被存档,卷积神经网络将把增强的图像视为全新的图像实例,从而帮助补充数据集。
图像增强技术
在这一节中,我们将看看一些常见的图像增强技术。然而,应该注意的是,这绝不是一个详尽的清单。
随机种植
随机裁剪是一种增强技术,其中图像的随机片段被裁剪,从而使其聚焦。原始图像的这种裁剪版本将丢失一些像素,本质上使其成为自己独特的图像。除了作为一种增强技术之外,随机裁剪可以帮助在模型中添加一些冗余,因为用随机裁剪增强图像训练的模型可以具有识别图像的能力,即使当感兴趣的对象不在全景中时。
def random_crop(dataset: list, crop_size=(20, 20)):
"""
This function replicates the random crop process
"""
cropped = []
images = [x[0] for x in dataset]
for image in tqdm_regular(images):
# deriving image size
img_size = image.shape
# extracting channels
channel_0, channel_1, channel_2 = image[:,:,0], image[:,:,1], image[:,:,2]
# deriving random indicies
idx_row = random.randint(0, img_size[0] - crop_size[0])
idx_column = random.randint(0, img_size[0] - crop_size[0])
# cropping image per channel
channel_0 = channel_0[idx_row:idx_row + crop_size[0],
idx_column:idx_column + crop_size[1]]
channel_1 = channel_1[idx_row:idx_row + crop_size[0],
idx_column:idx_column + crop_size[1]]
channel_2 = channel_2[idx_row:idx_row + crop_size[0],
idx_column:idx_column + crop_size[1]]
# stacking images
image = np.dstack((channel_0, channel_1, channel_2))
# resizing image
image = cv2.resize(image, (32, 32))
# labelling and appending to list
cropped.append((image, 1))
return cropped
图像噪声
一种增强技术,有目的地“破坏”图像中的随机像素,以创建一个完全不同的图像的幻觉。这种损坏是通过随机将一些像素转换为白色或黑色来实现的。通过噪声增强的图像具有与其原始版本完全不同的亮度的某些像素,从而被感知为不同的。
def noise_image(dataset: list, noise_intensity=0.2):
"""
This function replicates the image noising process
"""
noised = []
noise_threshold = 1 - noise_intensity
images = [x[0] for x in dataset]
for image in tqdm_regular(images):
# extracting channels
channel_0, channel_1, channel_2 = image[:,:,0], image[:,:,1], image[:,:,2]
# flatenning channels
channel_0 = channel_0.reshape(1024)
channel_1 = channel_1.reshape(1024)
channel_2 = channel_2.reshape(1024)
# creating vector of zeros
noise_0 = np.zeros(1024, dtype='uint8')
noise_1 = np.zeros(1024, dtype='uint8')
noise_2 = np.zeros(1024, dtype='uint8')
# noise probability
for idx in range(1024):
regulator = round(random.random(), 1)
if regulator > noise_threshold:
noise_0[idx] = 255
noise_1[idx] = 255
noise_2[idx] = 255
elif regulator == noise_threshold:
noise_0[idx] = 0
noise_1[idx] = 0
noise_2[idx] = 0
else:
noise_0[idx] = channel_0[idx]
noise_1[idx] = channel_1[idx]
noise_2[idx] = channel_2[idx]
# reshaping noise vectors
noise_0 = noise_0.reshape((32, 32))
noise_1 = noise_1.reshape((32, 32))
noise_2 = noise_2.reshape((32, 32))
# stacking images
image = np.dstack((noise_0, noise_1, noise_2))
# labelling and appending to list
noised.append((image, 1))
return noised
图像翻转
图像翻转是图像处理中的主要技术,是一种增强技术,其中像素的行或列的排列被反转,产生镜像效果。当图像翻转时,其像素的排列会有效地改变,从而使它们被感知为与原始图像不同。
def flip_image(dataset: list):
"""
This function replicates the process of horizontal flipping
"""
flipped = []
images = [x[0] for x in dataset]
for image in tqdm_regular(images):
# extracting channels
channel_0, channel_1, channel_2 = image[:,:,0], image[:,:,1], image[:,:,2]
channel_0 = channel_0[:, ::-1]
channel_1 = channel_1[:, ::-1]
channel_2 = channel_2[:, ::-1]
# stacking images
image = np.dstack((channel_0, channel_1, channel_2))
# labelling and appending to list
flipped.append((image, 1))
return flipped
图像模糊
另一种图像处理规则,模糊作为一种增强技术,其中像素强度全面变化,以便在模糊版本中创建一种模糊效果。由于像素值发生了变化,模糊版本被视为像素级别的全新图像。
def blur_image(dataset, kernel_size=5, padding=True):
"""This function performs convolution over an image
with the aim of blurring"""
# defining internal function for padding
def pad_image(image, padding=2):
"""
This function performs zero padding using the number of
padding layers supplied as argument and return the padded
image.
"""
# extracting channels
channel_0, channel_1, channel_2 = image[:,:,0], image[:,:,1], image[:,:,2]
# creating an array of zeros
padded_0 = np.zeros((image.shape[0] + padding*2,
image.shape[1] + padding*2), dtype='uint8')
padded_1 = np.zeros((image.shape[0] + padding*2,
image.shape[1] + padding*2), dtype='uint8')
padded_2 = np.zeros((image.shape[0] + padding*2,
image.shape[1] + padding*2), dtype='uint8')
# inserting image into zero array
padded_0[int(padding):-int(padding),
int(padding):-int(padding)] = channel_0
padded_1[int(padding):-int(padding),
int(padding):-int(padding)] = channel_1
padded_2[int(padding):-int(padding),
int(padding):-int(padding)] = channel_2
# stacking images
padded = np.dstack((padded_0, padded_1, padded_2))
return padded
# defining list to hold blurred images
all_blurred = []
# defining gaussian 5x5 filter
gauss_5 = np.array([[1, 4, 7, 4, 1],
[4, 16, 26, 16, 4],
[7, 26, 41, 26, 7],
[4, 16, 26, 16, 4],
[1, 4, 7, 4, 1]])
filter = 1/273 * gauss_5
# extracting images
images = [x[0] for x in dataset]
for image in tqdm_regular(images):
if padding:
image = pad_image(image)
else:
image = image
# extracting channels
channel_0, channel_1, channel_2 = image[:,:,0], image[:,:,1], image[:,:,2]
# creating an array to store convolutions
blurred_0 = np.zeros(((image.shape[0] - kernel_size) + 1,
(image.shape[1] - kernel_size) + 1), dtype='uint8')
blurred_1 = np.zeros(((image.shape[0] - kernel_size) + 1,
(image.shape[1] - kernel_size) + 1), dtype='uint8')
blurred_2 = np.zeros(((image.shape[0] - kernel_size) + 1,
(image.shape[1] - kernel_size) + 1), dtype='uint8')
# performing convolution
for i in range(image.shape[0]):
for j in range(image.shape[1]):
try:
blurred_0[i,j] = (channel_0[i:(i+kernel_size), j:(j+kernel_size)] * filter).sum()
except Exception:
pass
for i in range(image.shape[0]):
for j in range(image.shape[1]):
try:
blurred_1[i,j] = (channel_1[i:(i+kernel_size), j:(j+kernel_size)] * filter).sum()
except Exception:
pass
for i in range(image.shape[0]):
for j in range(image.shape[1]):
try:
blurred_2[i,j] = (channel_2[i:(i+kernel_size), j:(j+kernel_size)] * filter).sum()
except Exception:
pass
# stacking images
blurred = np.dstack((blurred_0, blurred_1, blurred_2))
# labelling and appending to list
all_blurred.append((blurred, 1))
return all_blurred
把所有的放在一起
在本节中,我们将利用上面定义的增强技术对上一篇文章中的数据集进行上采样,其中我们有 4:1 的类别不平衡(80%的猫,20%的狗)。为此,我们将使用 CIFAR-10 数据集,该数据集可以使用下面的代码单元加载到 PyTorch 中。
# loading training data
training_set = Datasets.CIFAR10(root='./', download=True,
transform=transforms.ToTensor())
# loading validation data
validation_set = Datasets.CIFAR10(root='./', download=True, train=False,
transform=transforms.ToTensor())
我们现在将使用如下定义的函数从数据集中提取猫和狗的图像。
def extract_images(dataset):
"""
This function helps to extract cat and dog images
from the cifar-10 dataset
"""
cats = []
dogs = []
for idx in tqdm_regular(range(len(dataset))):
if dataset.targets[idx]==3:
cats.append((dataset.data[idx], 0))
elif dataset.targets[idx]==5:
dogs.append((dataset.data[idx], 1))
else:
pass
return cats, dogs
# extracting from the training set
train_cats, train_dogs = extract_images(training_set)
# extracting from the validation set
val_cats, val_dogs = extract_images(validation_set)
通过增强对训练图像进行上采样
在关于类别不平衡的文章中,我们通过使用前 4800 张猫图片和前 1200 张狗图片(即data = train_cats[:4800] + train_dogs[:1200]
)建立了有利于猫的 4:1 不平衡。为了发挥协同作用,我们将保持相同的主题,这意味着我们需要用 3600 张图片增加的狗图片。
为了保持简单,我们将利用上述三种增强方法,用每种方法产生原始图像的 1200 个增强版本。
# deriving images of interest
dog_images = train_dogs[:1200]
# creating random cropped copies
dog_cropped = random_crop(dog_images)
# creating flipped copies
dog_flipped = flip_image(dog_images)
# creating noised copies
dog_noised = noise_image(dog_images)
拼凑一个数据集
既然转换后的副本已经就绪,我们现在需要做的就是为训练集和验证集整合数据集。
# creating a dataset of 4,800 dog images
train_dogs = dog_images + dog_cropped + dog_flipped + dog_noised
# instantiating training data
training_images = train_cats[:4800] + train_dogs
random.shuffle(training_images)
# instantiating validation data
validation_images = val_cats + val_dogs
random.shuffle(validation_images)
接下来,我们需要定义一个类,以便能够从我们的自定义数据集创建 PyTorch 数据集。
# defining dataset class
class CustomCatsvsDogs(Dataset):
def __init__(self, data, transforms=None):
self.data = data
self.transforms = transforms
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
image = self.data[idx][0]
label = torch.tensor(self.data[idx][1])
if self.transforms!=None:
image = self.transforms(image)
return(image, label)
# creating pytorch datasets
training_data = CustomCatsvsDogs(training_images, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))
validation_data = CustomCatsvsDogs(validation_images, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))
转换类别
在训练 convnet 的过程中,我们需要定义一个类,使我们能够将训练、验证、度量计算和日志记录以及模型利用打包到一个对象中,如下所示。
class ConvolutionalNeuralNet_2():
def __init__(self, network):
self.network = network.to(device)
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'training_accuracy_per_epoch': [],
'training_recall_per_epoch': [],
'training_precision_per_epoch': [],
'validation_accuracy_per_epoch': [],
'validation_recall_per_epoch': [],
'validation_precision_per_epoch': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
elif isinstance(module, nn.Linear):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# defining accuracy function
def accuracy(network, dataloader):
network.eval()
all_predictions = []
all_labels = []
# computing accuracy
total_correct = 0
total_instances = 0
for images, labels in tqdm(dataloader):
images, labels = images.to(device), labels.to(device)
all_labels.extend(labels)
predictions = torch.argmax(network(images), dim=1)
all_predictions.extend(predictions)
correct_predictions = sum(predictions==labels).item()
total_correct+=correct_predictions
total_instances+=len(images)
accuracy = round(total_correct/total_instances, 3)
# computing recall and precision
true_positives = 0
false_negatives = 0
false_positives = 0
for idx in range(len(all_predictions)):
if all_predictions[idx].item()==1 and all_labels[idx].item()==1:
true_positives+=1
elif all_predictions[idx].item()==0 and all_labels[idx].item()==1:
false_negatives+=1
elif all_predictions[idx].item()==1 and all_labels[idx].item()==0:
false_positives+=1
try:
recall = round(true_positives/(true_positives + false_negatives), 3)
except ZeroDivisionError:
recall = 0.0
try:
precision = round(true_positives/(true_positives + false_positives), 3)
except ZeroDivisionError:
precision = 0.0
return accuracy, recall, precision
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
# setting convnet to training mode
self.network.train()
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
# training
print('training...')
for images, labels in tqdm(train_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# resetting gradients
self.optimizer.zero_grad()
# making predictions
predictions = self.network(images)
# computing loss
loss = loss_function(predictions, labels)
log_dict['training_loss_per_batch'].append(loss.item())
train_losses.append(loss.item())
# computing gradients
loss.backward()
# updating weights
self.optimizer.step()
with torch.no_grad():
print('deriving training accuracy...')
# computing training accuracy
train_accuracy, train_recall, train_precision = accuracy(self.network, train_loader)
log_dict['training_accuracy_per_epoch'].append(train_accuracy)
log_dict['training_recall_per_epoch'].append(train_recall)
log_dict['training_precision_per_epoch'].append(train_precision)
# validation
print('validating...')
val_losses = []
# setting convnet to evaluation mode
self.network.eval()
with torch.no_grad():
for images, labels in tqdm(val_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# making predictions
predictions = self.network(images)
# computing loss
val_loss = loss_function(predictions, labels)
log_dict['validation_loss_per_batch'].append(val_loss.item())
val_losses.append(val_loss.item())
# computing accuracy
print('deriving validation accuracy...')
val_accuracy, val_recall, val_precision = accuracy(self.network, val_loader)
log_dict['validation_accuracy_per_epoch'].append(val_accuracy)
log_dict['validation_recall_per_epoch'].append(val_recall)
log_dict['validation_precision_per_epoch'].append(val_precision)
train_losses = np.array(train_losses).mean()
val_losses = np.array(val_losses).mean()
print(f'training_loss: {round(train_losses, 4)} training_accuracy: '+
f'{train_accuracy} training_recall: {train_recall} training_precision: {train_precision} *~* validation_loss: {round(val_losses, 4)} '+
f'validation_accuracy: {val_accuracy} validation_recall: {val_recall} validation_precision: {val_precision}\n')
return log_dict
def predict(self, x):
return self.network(x)
接下来,我们需要为这个二进制分类任务定义一个卷积神经网络。出于本文的考虑,我们将使用下面代码块中定义的自定义构建的 convnet。
class ConvNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 8, 3, padding=1)
self.batchnorm1 = nn.BatchNorm2d(8)
self.conv2 = nn.Conv2d(8, 8, 3, padding=1)
self.batchnorm2 = nn.BatchNorm2d(8)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(8, 32, 3, padding=1)
self.batchnorm3 = nn.BatchNorm2d(32)
self.conv4 = nn.Conv2d(32, 32, 3, padding=1)
self.batchnorm4 = nn.BatchNorm2d(32)
self.pool4 = nn.MaxPool2d(2)
self.conv5 = nn.Conv2d(32, 128, 3, padding=1)
self.batchnorm5 = nn.BatchNorm2d(128)
self.conv6 = nn.Conv2d(128, 128, 3, padding=1)
self.batchnorm6 = nn.BatchNorm2d(128)
self.pool6 = nn.MaxPool2d(2)
self.conv7 = nn.Conv2d(128, 2, 1)
self.pool7 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 3, 32, 32)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
output_1 = self.batchnorm1(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
output_2 = self.batchnorm2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
output_3 = self.batchnorm3(output_3)
#-------------
# LAYER 4
#-------------
output_4 = self.conv4(output_3)
output_4 = F.relu(output_4)
output_4 = self.pool4(output_4)
output_4 = self.batchnorm4(output_4)
#-------------
# LAYER 5
#-------------
output_5 = self.conv5(output_4)
output_5 = F.relu(output_5)
output_5 = self.batchnorm5(output_5)
#-------------
# LAYER 6
#-------------
output_6 = self.conv6(output_5)
output_6 = F.relu(output_6)
output_6 = self.pool6(output_6)
output_6 = self.batchnorm6(output_6)
#--------------
# OUTPUT LAYER
#--------------
output_7 = self.conv7(output_6)
output_7 = self.pool7(output_7)
output_7 = output_7.view(-1, 2)
return F.softmax(output_7, dim=1)
训练卷积神经网络
通过利用我们在上一节中定义的 convnet 并将其实例化为卷积神经网络类的成员(也在上一节中定义),我们现在可以使用如下定义的参数来训练我们的 conv net 10 个时期。
# training model
model = ConvolutionalNeuralNet_2(ConvNet())
log_dict = model.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_data, validation_set=validation_data)
分析结果
回顾一下,在类不平衡的文章中,当我们在不平衡的数据集上训练一个模型时,我们得到了一个训练准确率为 80%,验证准确率为 50%,验证召回率为 0%的模型。这表明该模型是不加选择的,只是简单地将所有图像实例预测为猫。
然而,正如我们所做的那样,在增强数据上训练模型产生了如下图所示的结果。总的来说,在整个训练过程中,训练和验证的准确性都有所提高,尽管验证的准确性从第 5 个时期开始就趋于稳定。
然而,主要关注的是验证指标,从第 3 个时期开始验证准确度约为 73%,验证召回率不是 0%,事实上,到第 9 个时期,它攀升到高达 78%,这表明该模型现在实际上是有区别的,即使我们已经使用增强图像用于训练目的。可以通过尝试其他增强方法或调整类权重来进一步调整性能。
寻找最佳技术
您可能已经注意到,我没有选择模糊作为该数据集的增强方法。那是因为我实际上已经尝试过了,但没有产生理想的结果。事实上,某些数据集具有对它们最有效的增强技术方案,因此,必须为正在处理的任何数据集找到最佳技术。
结束语
在本文中,我们将数据扩充作为一种处理类不平衡的上采样技术。我们进一步详细讨论了一些图像增强技术,以及它们如何在 Python 中实现。此后,我们扩充了一个数据集,并使用该数据集训练了一个 convnet,结果显示它产生了合理的验证准确性和召回分数。
包围盒的数据扩充:缩放和平移
原文:https://blog.paperspace.com/data-augmentation-bounding-boxes-scaling-translation/
这是我们正在做的一系列文章中的第二部分,涵盖了实现将图像增强技术应用于对象检测任务。在这一部分中,我们将介绍如何实现缩放和转换增强技术,以及在增强后,如果您的边界框的一部分在图像之外,该怎么办。
在上一部分中,我们介绍了实现增强以及水平翻转增强的统一方法。
GitHub 回购
本文和整个增强库的所有内容都可以在下面的 Github Repo 中找到。
https://github . com/paper space/dataincreasationforobjectdetection
证明文件
这个项目的文档可以在你的浏览器中打开docs/build/html/index.html
或者点击链接找到。
本系列的第 1 部分是本文的先决条件,强烈建议您通读一遍。
这个系列有 4 个部分。
1。第 1 部分:基础设计和水平翻转
2。第二部分:缩放和平移
3。第三部分:旋转和剪切
4。第 4 部分:烘焙增强到输入管道
在这篇文章中,我们将实现几个叫做 Scale 和 Translate 的扩展,它们显然是按照它们的意思做的。
规模
缩放转换的结果将如下所示。
Left: Original Image Right: Scale Augmentation applied.
设计决策
- 我们首先需要考虑的是规模扩大的参数。显而易见的选择是询问原始图像的维度的因子,我们希望它缩放我们的图像。因此,必须是大于-1 的值。您不能缩放小于其自身的尺寸。
- 人们可以通过将缩放因子约束为高度和宽度都相同来选择保持纵横比。然而,我们可以允许比例因子不同,这不仅产生比例放大,而且改变图像的纵横比。我们引入了一个布尔变量
diff
,它可以打开/关闭这个功能。 - 在实现这种增强的随机版本时,我们需要从一个区间中随机抽取一个比例因子的样本。我们处理它的方式是,如果用户能够提供比例因子
scale
将被采样的范围。如果用户只提供一个浮点数scale
,它必须是正的,比例因子从(-scale
,scale
)采样。
现在让我们来定义__init__
方法。
class RandomScale(object):
"""Randomly scales an image
Bounding boxes which have an area of less than 25% in the remaining in the
transformed image is dropped. The resolution is maintained, and the remaining
area if any is filled by black color.
Parameters
----------
scale: float or tuple(float)
if **float**, the image is scaled by a factor drawn
randomly from a range (1 - `scale` , 1 + `scale`). If **tuple**,
the `scale` is drawn randomly from values specified by the
tuple
Returns
-------
numpy.ndaaray
Scaled image in the numpy format of shape `HxWxC`
numpy.ndarray
Tranformed bounding box co-ordinates of the format `n x 4` where n is
number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
"""
def __init__(self, scale = 0.2, diff = False):
self.scale = scale
if type(self.scale) == tuple:
assert len(self.scale) == 2, "Invalid range"
assert self.scale[0] > -1, "Scale factor can't be less than -1"
assert self.scale[1] > -1, "Scale factor can't be less than -1"
else:
assert self.scale > 0, "Please input a positive float"
self.scale = (max(-1, -self.scale), self.scale)
self.diff = diff
这里的一个教训是不应该对__init__
函数中的最终参数进行采样。如果您对__init__
函数中的参数进行采样,那么每次调用函数时都会使用相同的参数值。这就消除了增加的随机因素。
将其移动到__call__
功能将导致每次应用增强时参数具有不同的值(来自范围)。
增强逻辑
比例转换的逻辑相当简单。我们使用 OpenCV 函数cv2.resize
来缩放我们的图像,并通过缩放因子来缩放我们的边界框。
img_shape = img.shape
if self.diff:
scale_x = random.uniform(*self.scale)
scale_y = random.uniform(*self.scale)
else:
scale_x = random.uniform(*self.scale)
scale_y = scale_x
resize_scale_x = 1 + scale_x
resize_scale_y = 1 + scale_y
img= cv2.resize(img, None, fx = resize_scale_x, fy = resize_scale_y)
bboxes[:,:4] *= [resize_scale_x, resize_scale_y, resize_scale_x, resize_scale_y]
但是,我们将保持大小不变。如果我们要缩小规模,就会有剩余的面积。我们将把它涂成黑色。输出将类似于上面显示的增强图像。
首先,我们创建一个原始图像大小的黑色图像。
canvas = np.zeros(img_shape, dtype = np.uint8)
然后,我们确定缩放图像的大小。如果它超过了原始图像的尺寸(我们正在按比例放大),那么它需要在原始尺寸被切断。然后,我们将调整后的图像“粘贴”到画布上。
y_lim = int(min(resize_scale_y,1)*img_shape[0])
x_lim = int(min(resize_scale_x,1)*img_shape[1])
canvas[:y_lim,:x_lim,:] = img[:y_lim,:x_lim,:]
img = canvas
边界框剪辑
最后剩下的唯一一件事是,由于缩放,一个对象被从图像中排除。例如,考虑放大 0.1 倍,这意味着最终图像尺寸是原始图像的 1.1 倍。这将足球从我们的形象中驱逐出去。
The football is expelled as result of scaling up.
想一想,你会意识到这种情况不仅出现在扩大规模的时候,也出现在像翻译这样的其他扩大中。
因此,我们在助手文件bbox_utils.py
中定义了一个函数clip_box
,它基于图像边界内的边界框总面积的百分比来裁剪边界框。这个百分比是一个可控的参数。
在文件bbox_utils.py
中定义、
def clip_box(bbox, clip_box, alpha):
"""Clip the bounding boxes to the borders of an image
Parameters
----------
bbox: numpy.ndarray
Numpy array containing bounding boxes of shape `N X 4` where N is the
number of bounding boxes and the bounding boxes are represented in the
format `x1 y1 x2 y2`
clip_box: numpy.ndarray
An array of shape (4,) specifying the diagonal co-ordinates of the image
The coordinates are represented in the format `x1 y1 x2 y2`
alpha: float
If the fraction of a bounding box left in the image after being clipped is
less than `alpha` the bounding box is dropped.
Returns
-------
numpy.ndarray
Numpy array containing **clipped** bounding boxes of shape `N X 4` where N is the
number of bounding boxes left are being clipped and the bounding boxes are represented in the
format `x1 y1 x2 y2`
"""
ar_ = (bbox_area(bbox))
x_min = np.maximum(bbox[:,0], clip_box[0]).reshape(-1,1)
y_min = np.maximum(bbox[:,1], clip_box[1]).reshape(-1,1)
x_max = np.minimum(bbox[:,2], clip_box[2]).reshape(-1,1)
y_max = np.minimum(bbox[:,3], clip_box[3]).reshape(-1,1)
bbox = np.hstack((x_min, y_min, x_max, y_max, bbox[:,4:]))
delta_area = ((ar_ - bbox_area(bbox))/ar_)
mask = (delta_area < (1 - alpha)).astype(int)
bbox = bbox[mask == 1,:]
return bbox
上面的函数修改了bboxes
数组,并删除了由于增加而丢失太多区域的边界框。
然后,我们简单地在我们的RandomScale
的__call__
方法中使用这个函数,以确保所有的盒子都被剪辑。这里,我们去掉了所有那些在图像范围内面积小于 25%的边界框。
bboxes = clip_box(bboxes, [0,0,1 + img_shape[1], img_shape[0]], 0.25)
为了计算边界框的面积,我们还定义了一个函数bbox_area
。
最后,我们完成的__call__
方法看起来像:
def __call__(self, img, bboxes):
#Chose a random digit to scale by
img_shape = img.shape
if self.diff:
scale_x = random.uniform(*self.scale)
scale_y = random.uniform(*self.scale)
else:
scale_x = random.uniform(*self.scale)
scale_y = scale_x
resize_scale_x = 1 + scale_x
resize_scale_y = 1 + scale_y
img= cv2.resize(img, None, fx = resize_scale_x, fy = resize_scale_y)
bboxes[:,:4] *= [resize_scale_x, resize_scale_y, resize_scale_x, resize_scale_y]
canvas = np.zeros(img_shape, dtype = np.uint8)
y_lim = int(min(resize_scale_y,1)*img_shape[0])
x_lim = int(min(resize_scale_x,1)*img_shape[1])
print(y_lim, x_lim)
canvas[:y_lim,:x_lim,:] = img[:y_lim,:x_lim,:]
img = canvas
bboxes = clip_box(bboxes, [0,0,1 + img_shape[1], img_shape[0]], 0.25)
return img, bboxes
翻译
我们要讨论的下一个增强是翻译,它会产生这样的效果。
同样,像比例放大一样,放大的参数是图像的尺寸因子,图像应该通过该因子进行平移。同样的设计决策也适用。
除了确保平移因子不小于-1,你还应该确保它不大于 1,否则你只会得到一个黑色的图像,因为整个图像将被转移。
class RandomTranslate(object):
"""Randomly Translates the image
Bounding boxes which have an area of less than 25% in the remaining in the
transformed image is dropped. The resolution is maintained, and the remaining
area if any is filled by black color.
Parameters
----------
translate: float or tuple(float)
if **float**, the image is translated by a factor drawn
randomly from a range (1 - `translate` , 1 + `translate`). If **tuple**,
`translate` is drawn randomly from values specified by the
tuple
Returns
-------
numpy.ndaaray
Translated image in the numpy format of shape `HxWxC`
numpy.ndarray
Tranformed bounding box co-ordinates of the format `n x 4` where n is
number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
"""
def __init__(self, translate = 0.2, diff = False):
self.translate = translate
if type(self.translate) == tuple:
assert len(self.translate) == 2, "Invalid range"
assert self.translate[0] > 0 & self.translate[0] < 1
assert self.translate[1] > 0 & self.translate[1] < 1
else:
assert self.translate > 0 & self.translate < 1
self.translate = (-self.translate, self.translate)
self.diff = diff
逻辑的扩充
这种扩张的逻辑比规模要复杂得多。因此,它值得一些解释。
我们首先从设置变量开始。
def __call__(self, img, bboxes):
#Chose a random digit to scale by
img_shape = img.shape
#translate the image
#percentage of the dimension of the image to translate
translate_factor_x = random.uniform(*self.translate)
translate_factor_y = random.uniform(*self.translate)
if not self.diff:
translate_factor_y = translate_factor_x
现在,当我们翻译图像时,会留下一些拖尾空间。我们将把它涂成黑色。如果你看上面演示翻译的图像,你可以很容易地辨认出黑色的空间。
我们像以前一样进行。我们首先初始化一个黑色图像,大约是原始图像的大小。
canvas = np.zeros(img_shape)
然后,我们有两个任务。首先,如第一幅图所示,确定图像将粘贴到黑色画布的哪个部分。
第二,图像的哪一部分会被粘贴。
Left: The part of image which will be pasted. Right: The area of Canvas on which the image will be pasted,
因此,我们首先计算出要在画布图像上粘贴的图像区域。(图像上的紫色斑块,位于上图左侧)。
#get the top-left corner co-ordinates of the shifted image
corner_x = int(translate_factor_x*img.shape[1])
corner_y = int(translate_factor_y*img.shape[0])
mask = img[max(-corner_y, 0):min(img.shape[0], -corner_y + img_shape[0]), max(-corner_x, 0):min(img.shape[1], -corner_x + img_shape[1]),:]
现在让我们得到画布上我们要“粘贴”的部分mask
orig_box_cords = [max(0,corner_y), max(corner_x,0), min(img_shape[0], corner_y + img.shape[0]), min(img_shape[1],corner_x + img.shape[1])]
canvas[orig_box_cords[0]:orig_box_cords[2], orig_box_cords[1]:orig_box_cords[3],:] = mask
img = canvas
移动箱子相对简单。你只需要偏移边界框的角。作为增强的结果,我们还裁剪了图像内面积小于 25%的边界框。
bboxes[:,:4] += [corner_x, corner_y, corner_x, corner_y]
bboxes = clip_box(bboxes, [0,0,img_shape[1], img_shape[0]], 0.25)
总结一下,我们的调用函数是这样的。
def __call__(self, img, bboxes):
#Chose a random digit to scale by
img_shape = img.shape
#translate the image
#percentage of the dimension of the image to translate
translate_factor_x = random.uniform(*self.translate)
translate_factor_y = random.uniform(*self.translate)
if not self.diff:
translate_factor_y = translate_factor_x
canvas = np.zeros(img_shape).astype(np.uint8)
corner_x = int(translate_factor_x*img.shape[1])
corner_y = int(translate_factor_y*img.shape[0])
#change the origin to the top-left corner of the translated box
orig_box_cords = [max(0,corner_y), max(corner_x,0), min(img_shape[0], corner_y + img.shape[0]), min(img_shape[1],corner_x + img.shape[1])]
mask = img[max(-corner_y, 0):min(img.shape[0], -corner_y + img_shape[0]), max(-corner_x, 0):min(img.shape[1], -corner_x + img_shape[1]),:]
canvas[orig_box_cords[0]:orig_box_cords[2], orig_box_cords[1]:orig_box_cords[3],:] = mask
img = canvas
bboxes[:,:4] += [corner_x, corner_y, corner_x, corner_y]
bboxes = clip_box(bboxes, [0,0,img_shape[1], img_shape[0]], 0.25)
return img, bboxes
测试
正如上一个示例中所详述的,您可以在样本图像上测试这些增强,或者在您自己的图像上测试这些增强,前提是您已经以正确的格式存储了增强。
from data_aug.bbox_utils import *
import matplotlib.pyplot as plt
scale = RandomScale(0.2, diff = True)
translate = RandomTranslate(0.2, diff = True)
img, bboxes = translate(img, bboxes)
img,bboxes = scale(img, bboxes)
plt.imshow(draw_rect(img, bboxes))
最终结果可能是这样的。
你可以试着先做缩放,再做平移。您可能会发现相同参数值的结果是不同的。能解释一下吗?这是留给读者的一个练习。
另一个练习可以是尝试实现我们已经实现的增强的确定性版本。
这是这一部分。在下一部分中,我们将使用 OpenCV 的仿射变换特性,在旋转和剪切方面,增强将变得更高级、更混乱。敬请期待!
包围盒的数据扩充:重新思考对象检测的图像变换
原文:https://blog.paperspace.com/data-augmentation-for-bounding-boxes/
当谈到从深度学习任务中获得良好的表现时,数据越多越好。然而,我们可能只有有限的数据。数据扩充是解决数据短缺的一种方法,通过人为地扩充我们的数据集。事实上,这项技术已经被证明是如此成功,以至于它已经成为深度学习系统的一个主要部分。
数据增强为什么有效?
理解数据增强为什么有效的一个非常直接的方法是将它视为一种人工扩展数据集的方法。深度学习应用也是如此,数据越多越好。
理解数据增强为什么如此有效的另一种方式是将它视为我们数据集的附加噪声。在在线数据扩充的情况下尤其如此,或者每次我们将数据样本送入训练循环时随机扩充每个数据样本。
Left: Original Image, Right: Augmented Image.
每次神经网络看到相同的图像时,由于对其应用了随机数据增强,所以会有一点不同。这种差异可以被视为每次添加到我们的数据样本中的噪声,这种噪声迫使神经网络学习一般化的特征,而不是在数据集上过度拟合。
GitHub 回购
本文和整个增强库的所有内容都可以在下面的 Github Repo 中找到。
https://github . com/paper space/dataincreasionforobjectdetection
证明文件
这个项目的文档可以在你的浏览器中打开docs/build/html/index.html
或者点击链接找到。
这个系列有 4 个部分。
1。第 1 部分:基础设计和水平翻转
2。第二部分:缩放和平移
3。第三部分:旋转和剪切
4。第 4 部分:烘焙增强到输入管道
包围盒的对象检测
现在,许多深度学习库,如 torchvision、keras 和 Github 上的专业库,都为分类训练任务提供了数据增强。然而,仍然缺少对用于对象检测任务的数据扩充的支持。例如,为分类任务水平翻转图像的增强将看起来像上面的那个。
然而,为对象检测任务做同样的扩充也需要你更新边界框。比如这个。
Change of Bounding Boxes during Horizontal Flip
正是这种类型的数据扩充,或者具体地说,主要数据扩充技术的检测等价物要求我们更新边界框,我们将在这些文章中讨论。准确地说,这是我们将涉及的增强功能的确切列表。
- 水平翻转(如上图所示)
2.缩放和平移
3.旋转
4.剪羊毛
5.为神经网络的输入调整大小
技术细节
我们将把我们小小的数据扩充库建立在 Numpy 和 OpenCV 的基础上。
我们将把我们的扩充定义为类,可以调用类的实例来执行扩充。我们将定义一个统一的方法来定义这些类,这样你也可以编写你自己的数据扩充。
我们还将定义一个数据扩充,它本身什么也不做,但是将数据扩充组合起来,以便它们可以在一个序列中应用。
对于每个数据扩充,我们将定义它的两个变体,一个随机的和一个确定的的。在随机模式中,增强是随机发生的,而在确定模式中,增强的参数(如要旋转的角度)是固定的。
数据扩充示例:水平翻转
本文将概述编写增广的一般方法。我们还将回顾一些实用功能,这将有助于我们可视化检测,以及其他一些东西。那么,我们开始吧。
存储注释的格式
对于每个图像,我们将边界框注释存储在一个 numpy 数组中,该数组有 N 行和 5 列。这里, N 表示图像中物体的数量,而五列表示:
- 左上 x 坐标
- 左上 y 坐标
- 右下角 x 坐标
- 右下角的 y 坐标
- 对象的类别
Format for storing Bounding Box Annotations
我知道很多数据集,注释工具以其他格式存储注释,因此,我会让您将存储数据注释的任何存储格式转换为上述格式。
是的,出于演示的目的,我们将使用下面的图片,莱昂内尔·梅西在对尼日利亚的比赛中攻入了漂亮的一球。
文件组织
我们将代码保存在两个文件中,data_aug.py
和bbox_util.py
。第一个文件将包含用于扩充的代码,而第二个文件将包含用于辅助函数的代码。
这两个文件都位于一个名为data_aug
的文件夹中
让我们假设你必须在你的训练循环中使用这些数据扩充。我将让您了解如何提取图像,并确保注释的格式正确。
然而,为了简单起见,让我们一次只使用一个图像。您可以轻松地将这段代码移到循环内部,或者您的数据获取函数来扩展功能。
将 github repo 克隆到包含您的训练代码文件或您需要进行增强的文件的文件夹中。
git clone https://github.com/Paperspace/DataAugmentationForObjectDetection
随机水平翻转
首先,我们导入所有必要的东西,并确保添加了路径,即使我们从包含文件的文件夹之外调用函数。以下代码放在文件data_aug.py
中
import random
import numpy as np
import cv2
import matplotlib.pyplot as plt
import sys
import os
lib_path = os.path.join(os.path.realpath("."), "data_aug")
sys.path.append(lib_path)
将实施的数据增强是RandomHorizontalFlip
,其以概率 p. 水平翻转图像
我们首先从定义类开始,它是__init__
方法。init 方法包含增强的参数。对于这种增强,它是每个图像翻转的概率。对于另一个类似旋转的增强,它可以包含对象旋转的角度。
class RandomHorizontalFlip(object):
"""Randomly horizontally flips the Image with the probability *p*
Parameters
----------
p: float
The probability with which the image is flipped
Returns
-------
numpy.ndaaray
Flipped image in the numpy format of shape `HxWxC`
numpy.ndarray
Tranformed bounding box co-ordinates of the format `n x 4` where n is
number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
"""
def __init__(self, p=0.5):
self.p = p
函数的 docstring 已经以 Numpy docstring 格式编写。这将有助于使用 Sphinx 生成文档。
每个函数的__init__
方法用于定义增强的所有参数。然而,增强的实际逻辑是在__call__
函数中定义的。
当从一个类实例调用调用函数时,该函数带有两个参数,img
和bboxes
,其中img
是包含像素值的 OpenCV numpy 数组,bboxes
是包含边界框注释的 numpy 数组。
__call__
函数也返回相同的参数,这有助于我们将一系列要在序列中应用的增强链接在一起。
def __call__(self, img, bboxes):
img_center = np.array(img.shape[:2])[::-1]/2
img_center = np.hstack((img_center, img_center))
if random.random() < self.p:
img = img[:,::-1,:]
bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])
box_w = abs(bboxes[:,0] - bboxes[:,2])
bboxes[:,0] -= box_w
bboxes[:,2] += box_w
return img, bboxes
让我们一点一点地了解这里发生的一切。
在水平翻转中,我们围绕穿过图像中心的垂直线旋转图像。
然后,每个角的新坐标可以被描述为穿过图像中心的垂直线中的角的镜像。对于数学上的倾斜,通过中心的垂直线将是连接原始拐角和新的转换拐角的线的垂直平分线。****
为了更好地理解正在发生的事情,请看下图。变换图像的右半部分和原始图像的左半部分中的像素是彼此关于中心线的镜像。
以上是通过下面这段代码完成的。
img_center = np.array(img.shape[:2])[::-1]/2
img_center = np.hstack((img_center, img_center))
if random.random() < self.p:
img = img[:,::-1,:]
bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])
注意,img = img[:,::-1,:]
行基本上是获取包含图像的数组,并在第一维度(即存储像素值的 x 坐标的维度)反转它的元素。
然而,人们必须注意到左上角的镜像是结果框的右上角。事实上,结果坐标是边界框的右上角和左下角坐标。然而,我们需要它们在左上角和右下角的格式。
The side-effect of our code
下面这段代码负责转换。
box_w = abs(bboxes[:,0] - bboxes[:,2])
bboxes[:,0] -= box_w
bboxes[:,2] += box_w
最后,我们返回图像和包含边界框的数组。
水平翻转的确定性版本
上面的代码以概率 p 随机地应用转换。然而,如果我们想要构建一个确定性版本,我们可以简单地将参数 p 作为 1 传递。或者我们可以写另一个类,其中我们根本没有参数 p ,并像这样实现__call__
函数。
def __call__(self, img, bboxes):
img_center = np.array(img.shape[:2])[::-1]/2
img_center = np.hstack((img_center, img_center))
img = img[:,::-1,:]
bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])
box_w = abs(bboxes[:,0] - bboxes[:,2])
bboxes[:,0] -= box_w
bboxes[:,2] += box_w
return img, bboxes
看到它的实际应用
现在,让我们假设你必须用水平翻转增强你的图像。我们将在一个图像上使用它,但是你可以在任何你喜欢的数字上使用它。首先,我们创建一个文件test.py
。我们从进口所有好东西开始。
from data_aug.data_aug import *
import cv2
import pickle as pkl
import numpy as np
import matplotlib.pyplot as plt
然后,我们导入图像并加载注释。
img = cv2.imread("messi.jpg")[:,:,::-1] #OpenCV uses BGR channels
bboxes = pkl.load(open("messi_ann.pkl", "rb"))
#print(bboxes) #visual inspection
为了看看我们的增强是否真的起作用,我们定义了一个助手函数draw_rect
,它接受img
和bboxes
并返回一个 numpy 图像数组,在该图像上绘制边界框。
让我们创建一个文件bbox_utils.py
并导入必要的内容。
import cv2
import numpy as np
现在,我们定义函数draw_rect
def draw_rect(im, cords, color = None):
"""Draw the rectangle on the image
Parameters
----------
im : numpy.ndarray
numpy image
cords: numpy.ndarray
Numpy array containing bounding boxes of shape `N X 4` where N is the
number of bounding boxes and the bounding boxes are represented in the
format `x1 y1 x2 y2`
Returns
-------
numpy.ndarray
numpy image with bounding boxes drawn on it
"""
im = im.copy()
cords = cords.reshape(-1,4)
if not color:
color = [255,255,255]
for cord in cords:
pt1, pt2 = (cord[0], cord[1]) , (cord[2], cord[3])
pt1 = int(pt1[0]), int(pt1[1])
pt2 = int(pt2[0]), int(pt2[1])
im = cv2.rectangle(im.copy(), pt1, pt2, color, int(max(im.shape[:2])/200))
return im
一旦完成,让我们回到我们的test.py
文件,并绘制原始的边界框。
plt.imshow(draw_rect(img, bboxes))
这就产生了这样的东西。
让我们看看我们转型的效果。
hor_flip = RandomHorizontalFlip(1)
img, bboxes = hor_flip(img, bboxes)
plt.imshow(draw_rect(img, bboxes))
你应该得到这样的东西。
外卖课程
- 边界框注释应该存储在大小为 N×5 的 numpy 数组中,其中 N 是对象的数量,每个框由具有 5 个属性的行表示;左上角的坐标,右下角的坐标,对象的类。
- 每个数据扩充被定义为一个类,其中
__init__
方法用于定义扩充的参数,而__call__
方法描述了扩充的实际逻辑。它接受两个参数,图像img
和边界框注释bboxes
,并返回转换后的值。
这就是这篇文章的内容。在下一篇文章中,我们将处理Scale
和Translate
增强。考虑到有更多的参数(缩放和平移因子),它们不仅是更复杂的转换,而且还带来了一些我们在HorizontalFlip
转换中不必应对的挑战。一个例子是,如果一个框的一部分在增强后的图像之外,则决定是否保留该框。
自然语言处理中流行的数据扩充技术
数据扩充是在机器学习的各个垂直领域广泛使用的实践,有助于增加现有数据集中的数据样本。可能有多种原因导致您希望在训练数据中包含更多样本。这可能是因为你收集的数据太少,无法开始训练一个好的 ML 模型,或者也许你看到了一些过度拟合发生的证据。在任何一种情况下,数据增强都可以帮助使用各种技术在现有数据中引入多样性/多样性,并最终帮助我们的机器学习模型处理对未知样本的过度拟合和推广。这里也可以认为是正则化。增强方法在计算机视觉领域非常流行,在自然语言处理领域也是如此。
在这篇博客中,我们将主要关注一些流行的技术和 python 库,它们可以用来扩充文本数据。
方法 1
同义词替换(SR)——随机选择一个不停的单词,用它的同义词替换。
注意:停用词是那些在所有文档中出现的可能性相同的常用词。这些单词本身并没有给相关文本添加任何重要的分界信息。其中一些例子有“the”、“an”、“is”等。可以使用特定语言的单个通用停用词列表来将它们从给定文本中移除,尽管通常的做法是通过从特定领域语料库中挖掘常用词来扩展通用列表。自然语言工具包(NLTK)预先打包了一个停止用户列表,您可以将它作为一个起点。
随机插入(RI) -在文本中的随机位置插入随机选择的单词的同义词。
随机替换(RS) -随机交换文本中任意两个单词。
随机删除(RD) -从文本中随机删除一个单词。
上述每个功能都可以执行 1 次以上,但是应该考虑信息损失和样本总数之间的权衡来选择。这种转换背后的直觉是模仿任何人在书写一段文本时都会犯的一般书写行为和错误。我还建议你浏览一下论文或观看这个视频来获得关于所做实验的见解。你可以从这个令人敬畏的 python 库中学习例子,直接使用这些开箱即用的函数。该库还允许您在字符级别执行这些操作,而不仅仅是单词。
除了上面提到的,还有一件事可以尝试做表面级转换就是使用预定义的映射,使用正则表达式来扩展收缩。例如- 不要- >不要,我们要- >我们要等。
方法 2
回译也是在实践中进行文本增强时经常使用的一种流行技术。考虑到当今翻译系统的概率性行为,反向翻译可以非常有效地生成原文的释义版本。释义是保留原句原意的句子,同时也允许对内容进行重新措辞。例如,如果原来的句子是“每个人都经常去看电影”,那么它的一个释义可以是“每个人都经常去看电影”。
反向翻译方法包括 3 个基本步骤。其中每一个都在下面提到:
中间翻译 -将文本从源语言翻译成目标语言。例如,英语到法语。
反向翻译 -将步骤 1 中的文本翻译回其源语言。
修剪——如果与原文相同,删除反向翻译。
您可以使用任何可用的翻译模型来构建这个管道。实践中经常看到,做一级以上的翻译可以得到很好的解释。例如,按照管道-英语到法语,法语到西班牙语,西班牙语到英语。下图说明了流程。但当然,这取决于手头的计算带宽。你可以从这个棒极了的 python 库中学习例子,直接使用这个开箱即用的函数。
Back-Translation Flow
你也可以看看这个 github 的,它在 Quora 重复问题数据集上微调一个预先训练好的 T5 模型,以监督的方式生成转述问题。你总是可以通过在微软研究释义语料库、 PPDB:释义数据库和类似的数据集上进行微调来进一步扩展它,以构建一个更通用的释义系统。
方法三
随着变形金刚模型在自然语言处理方面的最新进展,这篇博客谈到了使用 GPT-2 模型进行文本数据增强(点击这里查看 GTP-2 的更全面概述)。在很高的层面上,该博客讨论了通过向输入的例句添加类别标签的前缀文本,对来自训练数据的句子的现有 GPT-2 模型进行微调。然后,它对模型进行训练,以在存在类别标签的情况下最大化生成序列的可能性。添加类标签有助于模型理解训练数据中可用的各种类聚类。在推理过程中,只要给 GPT-2 模型一个类名提示,它就会生成与类名意图一致的句子。你可以在text augmentation-gp T2 Github找到代码。此外,下面是上述模型(如官方 GitHub 中所述)在垃圾邮件/业余邮件分类任务上生成的一些示例。
SPAM: you have been awarded a £2000 cash prize. call 090663644177 or call 090530663647<|endoftext|>
SPAM: FREE Call Todays top players, the No1 players and their opponents and get their opinions on www.todaysplay.co.uk Todays Top Club players are in the draw for a chance to be awarded the £1000 prize. TodaysClub.com<|endoftext|>
HAM: I don't think so. You got anything else?<|endoftext|>
HAM: Ugh I don't want to go to school.. Cuz I can't go to exam..<|endoftext|>
Synthetic examples generated by the GPT-2 model
您可以将此方法用于渐变笔记本,方法是创建以 repo 作为工作区 URL 的笔记本,可在“高级选项切换”部分找到。
方法 4
上面提到的一些方法如果盲目地进行,可能会导致标签和文本意图不匹配。比如,如果句子是“我不喜欢这个食物”,标签是“负面”。假设我们正在进行随机单词删除,结果是单词“don't”被删除,所以现在新句子变成了“我喜欢这种食物”,标签为“负面”。这是不对的!因此,关注转换后的句子并相应地调整它们总是很重要的。
因此,作者在中使用预先训练的 Transformer 模型进行数据增强,指出了上述局限性,并提出了一种执行上下文增强的技术,以保留标签和句子意图。他们通过微调各种预训练的变压器模型来做到这一点,如伯特、 GPT 、Seq2Seq 巴特、 T5 等句子以及类标签信息。他们尝试了它的两种变体,这两种变体都在下面提到:
Expand -这涉及到为每个句子预先添加类标签,并添加类标签作为模型词汇表的一部分,然后根据各个 Transformer 模型的预定义目标对其进行微调。
Prepend——这包括为每个句子预先添加类标签,但不添加类标签作为模型词汇表的一部分,并根据相应 Transformer 模型的预定义目标对其进行微调。
他们发现,与作者尝试的其他方法相比,预训练的 Seq2Seq 模型表现良好。我建议你浏览一下论文来获得对实验的见解。同样,你也可以随意访问代码来获取相同的信息。
结论
如前所述,数据扩充是您在构建机器学习模型时应该纳入的重要实践之一。在这篇博客中,我们看到了自然语言处理领域中的各种数据扩充技术,从上下文无关到敏感转换,python 库可以帮助更快地构建这些函数的原型,以及在使用各种转换选项时需要记住的事情。此外,值得注意的是,数据扩充可能并不总是有助于提高模型性能(但同样,这也不能保证机器学习中的任何实践),但肯定的是,可以尝试一些东西,并将其放在手边是一种好的实践。根据经验,尝试各种增强方法,看看哪种效果更好,并使用验证集来调整要增强的样本数量,以获得最佳结果,这总是好的。
希望你觉得这篇文章有用。谢谢大家!
包围盒的数据扩充:为检测器构建输入管道
原文:https://blog.paperspace.com/data-augmentation-for-object-detection-building-input-pipelines/
你好。这是我们关于将图像增强方法应用于目标检测任务系列的第四部分,也是最后一部分。在前三篇文章中,我们已经讨论了各种图像增强技术,如翻转、旋转、剪切、缩放和平移。这一部分是关于如何将所有这些整合到一起,并将其烘焙到你的深层网络的输入管道中。那么,我们开始吧。
在开始之前,您应该已经阅读了本系列的前几篇文章。
这个系列有 4 个部分。
1。第 1 部分:基础设计和水平翻转
2。第二部分:缩放和平移
3。第三部分:旋转和剪切
4。第 4 部分:烘焙增强到输入管道
GitHub 回购
本文和整个增强库的所有内容都可以在下面的 Github Repo 中找到。
https://github . com/paper space/dataincreasationforobjectdetection
证明文件
这个项目的文档可以在你的浏览器中打开docs/build/html/index.html
或者点击链接找到。
结合多重转换
现在,如果您想要应用多个转换,您可以通过一个接一个地顺序应用它们来实现。例如,如果我要应用翻转,然后缩放和旋转,这是我将如何完成它。
img, bboxes = RandomHorizontalFlip(1)(img, bboxes)
img, bboxes = RandomScale(0.2, diff = True)(img, bboxes)
img, bboxes = RandomRotate(10)(img, bboxes)
我需要应用的转换越多,我的代码就越长。
在这一点上,我们将实现一个函数,该函数单独组合多个数据扩充。我们将以与其他数据扩充相同的方式实现它,只是它将其他数据扩充的类实例列表作为参数。我们来写这个函数。
class Sequence(object):
"""Initialise Sequence object
Apply a Sequence of transformations to the images/boxes.
Parameters
----------
augemnetations : list
List containing Transformation Objects in Sequence they are to be
applied
probs : int or list
If **int**, the probability with which each of the transformation will
be applied. If **list**, the length must be equal to *augmentations*.
Each element of this list is the probability with which each
corresponding transformation is applied
Returns
-------
Sequence
Sequence Object
"""
def __init__(self, augmentations, probs = 1):
self.augmentations = augmentations
self.probs = probs
属性存储了我们讨论过的扩充列表。还有另一个属性self.probs
,它保存了相应实例的增强将被应用的概率。
__call__
函数看起来像。
def __call__(self, images, bboxes):
for i, augmentation in enumerate(self.augmentations):
if type(self.probs) == list:
prob = self.probs[i]
else:
prob = self.probs
if random.random() < prob:
images, bboxes = augmentation(images, bboxes)
return images, bboxes
现在,如果我们要应用与上面相同的一组转换,我们将编写。
transforms = Sequence([RandomHorizontalFlip(1), RandomScale(0.2, diff = True), RandomRotate(10)])
img, bboxes = transforms(img, bboxes)
这是结果。
调整到输入尺寸
虽然现在很多架构都是完全卷积的,因此大小是不变的,但为了统一起见,我们通常会选择一个恒定的输入大小,以便将我们的图像分批处理,这有助于提高速度。
因此,我们希望有一个图像转换,将我们的图像以及我们的边界框调整到一个恒定的大小。我们也想保持我们的长宽比。
Image is resized to 608 x 608
在上面的例子中,原始图像的大小是800 x 505
。当我们必须在保持纵横比不变的情况下将矩形图像的大小调整为正方形时,我们各向同性地调整图像的大小(保持纵横比不变),以便长边等于输入维度。
我们实现调整大小的方式与我们调整扩充的方式相同。
class Resize(object):
"""Resize the image in accordance to `image_letter_box` function in darknet
The aspect ratio is maintained. The longer side is resized to the input
size of the network, while the remaining space on the shorter side is filled
with black color. **This should be the last transform**
Parameters
----------
inp_dim : tuple(int)
tuple containing the size to which the image will be resized.
Returns
-------
numpy.ndaaray
Sheared image in the numpy format of shape `HxWxC`
numpy.ndarray
Resized bounding box co-ordinates of the format `n x 4` where n is
number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
"""
def __init__(self, inp_dim):
self.inp_dim = inp_dim
在构建增强逻辑之前,我们将实现一个名为letter_box
image 的函数,该函数调整图像的大小,使长边等于输入尺寸,图像沿短边居中。
def letterbox_image(img, inp_dim):
'''resize image with unchanged aspect ratio using padding'''
img_w, img_h = img.shape[1], img.shape[0]
w, h = inp_dim
new_w = int(img_w * min(w/img_w, h/img_h))
new_h = int(img_h * min(w/img_w, h/img_h))
resized_image = cv2.resize(img, (new_w,new_h), interpolation = cv2.INTER_CUBIC)
#create a black canvas
canvas = np.full((inp_dim[1], inp_dim[0], 3), 128)
#paste the image on the canvas
canvas[(h-new_h)//2:(h-new_h)//2 + new_h,(w-new_w)//2:(w-new_w)//2 + new_w, :] = resized_image
return canvas
我们最终实现了__call__
函数。
def __call__(self, img, bboxes):
w,h = img.shape[1], img.shape[0]
img = letterbox_image(img, self.inp_dim)
scale = min(self.inp_dim/h, self.inp_dim/w)
bboxes[:,:4] *= (scale)
new_w = scale*w
new_h = scale*h
inp_dim = self.inp_dim
del_h = (inp_dim - new_h)/2
del_w = (inp_dim - new_w)/2
add_matrix = np.array([[del_w, del_h, del_w, del_h]]).astype(int)
bboxes[:,:4] += add_matrix
img = img.astype(np.uint8)
return img, bboxes
为 COCO 数据集构建输入管道
现在,我们已经完成了增强,也有了组合这些增强的方法,我们实际上可以考虑设计一个输入管道,为我们提供来自 COCO 数据集的图像和注释,并动态应用增强。
离线增强与在线增强
在深层网络中,可以使用两种方式来完成增强。离线增强和在线增强。
在离线扩充中,我们扩充我们的数据集,创建新的扩充数据并将其存储在磁盘上。这可以帮助我们将我们的 train 示例增加任意多倍。因为我们有各种各样的增强,随机地应用它们可以帮助我们在开始重复之前将训练数据增加许多倍。
然而,这种方法有一个缺点,当我们的数据的大小太大时,它就不适合了。考虑占用 50 GB 内存的训练数据集。仅增加一次就会使大小增加到 100 GB。如果您有样本磁盘空间,最好是 SSD 或高 RPM 硬盘,这可能不是问题。
在在线增强中,就在图像被馈送到神经网络之前应用增强。这比我们以前的方法有几个好处。
- 没有空间需求,因为增强是动态完成的,我们不需要保存增强的训练示例。
- 每次图像被输入神经网络,我们都会得到相同图像的噪声版本。众所周知,微小的噪音可以帮助神经网络更好地泛化。每次神经网络看到相同的图像时,由于对它应用了增强,它会有一点不同。这种差异可以被认为是噪音,这有助于我们的网络更好地一般化。
- 我们在每个时期获得不同的扩充数据集,而无需存储任何额外的图像。
CPU 还是 GPU?
另一方面,从计算的角度来看,人们可能会想,在训练循环期间,是 CPU 还是 GPU 应该进行在线增强。
答案很可能是 CPU。CUDA 调用本质上是异步的。简而言之,这意味着在调用 GPU 命令(CUDA)之后,执行的控制权就回到了 CPU。
让我们来分析一下它是如何发生的。
CPU 不断读取代码,最终到达必须调用 GPU 的点。例如,在 PyTorch 中,命令net = net.cuda()
通知 GPU 需要将变量net
放到 GPU 上。现在任何使用net
进行的计算都是由 GPU 完成的。
CPU 进行 CUDA 调用。这个调用是异步。这意味着 CPU 不会等待 GPU 完成调用指定的任务。执行的控制权立即返回给 CPU ,CPU 可以开始执行后面的代码行,而 GPU 可以在后台做它的事情。
这意味着 GPU 现在可以在后台/并行执行计算。现代深度学习库确保调用被正确调度,以确保我们的代码正常工作。然而,这个功能经常被深度学习库用来加速训练。当 GPU 忙于执行当前时期的向前和向后传递时,下一个时期的数据可以同时由 CPU 从磁盘中读取并加载到 RAM 中。
所以回到我们的问题,在线增强应该由 CPU 还是 GPU 来完成?它们应该由 CPU 完成的原因是,当 GPU 忙于执行当前时段的向前和向后传递时,下一个时段的增强可以在 CPU 上并行发生。
如果我们将增强功能放在 GPU 上,那么 GPU 将不得不等待 CPU 从磁盘中读取图像并将它们发送过来。这种等待状态会降低训练速度。另一件要注意的事情是,当 GPU 正在进行计算(增强+向前/向后传递)时,CPU 可能处于空闲状态(假设它已经从磁盘中读取了数据)。
设置 COCO 数据集
为了向您展示您应该如何使用我们刚刚实现的增强,我们以 COCO 数据集为例。出于演示目的,我们将使用pytorch
和torchvision
包。我会尽量保持它的通用性,这样你也可以让它与其他库或你自己的定制代码一起工作。
现在,在 PyTorch 中,数据管道是使用torch.utils.dataset
类构建的。这个类基本上包含两个重要的函数。
__init__
函数描述了数据集的细节。这包括存储图像和注释的目录等。__len__
返回训练样本的数量__getitem__
返回一个单独的训练示例(也许是标签)。
在这三个函数中,__getitem__
函数是我们感兴趣的。我们将在这里做图像放大。
通常,您应该查看代码库中的位置,从磁盘中读取图像和注释。这应该是你应该插入增强代码的地方。这确保了每个图像的增强是不同的,即使是一批图像。
下面这段代码通常用于 COCO 训练数据集中的示例。让我们假设train2017
是包含图像的文件夹,annots.json
是包含注释 json 文件的文件。
我没有深入讨论如何下载 COCO 数据集的原因是,这只是演示如何修改现有的输入管道来合并增强功能,而不是设置 COCO 输入管道的详尽指南
事实上,数据集的大小约为 19.3 GB,所以您可能不想下载它。
from torchvision.datasets import CocoDetection
coco_dataset = CocoDetection(root = "train2017", annFile = "annots.json")
for image, annotation in coco_dataset:
# forward / backward pass
现在,为了添加图像增强,我们需要找到负责从磁盘读取图像和注释的代码。在我们的例子中,这项工作是由CocoDetection
类的__getitem__
函数完成的。
我们可以进入torchvision
的源代码并修改CocoDetection
,但是修补内置的功能不是一个好主意。因此,我们定义了一个从 CocoDetection 类派生的新类。
class CocoAugment(CocoDetection):
def __init__(self, root, annFile, transforms, target_transforms, det_transforms):
super(CocoAugment, self).__init__(root, annFile, transforms, target_transforms)
self.det_transforms = det_transforms
def __getitem__(self, idx):
img, bboxes = super(CocoAugment, self).__getitem__(idx)
bboxes = transform_annotation(bboxes)
img, bboxes = self.det_transforms(img, bboxes)
return bboxes
让我们看看这里到底发生了什么。
在我们的新类中,我们引入了属性det_transforms
,它将用于保存应用于图像和边界框的增强。注意,我们还有属性transforms
和target_transforms
,它们用于应用torchvision
的内置数据扩充。然而,这些扩充只是为分类任务而构建的,并不支持扩充边界框。
然后,在__getitem__
方法中,我们首先获取图像,以及父类的__getitem__
返回的注释bboxes
。正如我们在本系列的第 1 部分中提到的,注释必须采用特定的格式,以便增强功能能够工作。我们定义函数transform_annotation
来做这件事。
由CocoDetection
的__getitem__
方法返回的图像中对象的边界框注释是一个列表形式,其中包含每个边界框的字典。边界框属性由字典的元素定义。每个边界框由其左上角、高度和宽度定义。我们必须将其更改为我们的格式,其中每个边界框由左上角和右下角定义。
def transform_annotation(x):
#convert the PIL image to a numpy array
image = np.array(x[0])
#get the bounding boxes and convert them into 2 corners format
boxes = [a["bbox"] for a in x[1]]
boxes = np.array(boxes)
boxes = boxes.reshape(-1,4)
boxes[:,2] += boxes[:,0]
boxes[:,3] += boxes[:,1]
#grab the classes
category_ids = np.array([a["category_id"] for a in x[1]]).reshape(-1,1)
ground_truth = np.concatenate([boxes, category_ids], 1).reshape(-1,5)
return image, ground_truth
然后,简单地应用增强,现在我们得到了增强的图像。总结整个代码,
from torchvision.datasets import CocoDetection
import numpy as np
import matplotlib.pyplot as plt
def transform_annotation(x):
#convert the PIL image to a numpy array
image = np.array(x[0])
#get the bounding boxes and convert them into 2 corners format
boxes = [a["bbox"] for a in x[1]]
boxes = np.array(boxes)
boxes = boxes.reshape(-1,4)
boxes[:,2] += boxes[:,0]
boxes[:,3] += boxes[:,1]
#grab the classes
category_ids = np.array([a["category_id"] for a in x[1]]).reshape(-1,1)
ground_truth = np.concatenate([boxes, category_ids], 1).reshape(-1,5)
return image, ground_truth
class CocoAugment(CocoDetection):
def __init__(self, root, annFile, transforms, target_transforms, det_transforms):
super(CocoAugment, self).__init__(root, annFile, transforms, target_transforms)
self.det_transforms = det_transforms
def __getitem__(self, idx):
img, bboxes = super(CocoAugment, self).__getitem__(idx)
bboxes = transform_annotation(bboxes)
img, bboxes = self.det_transforms(img, bboxes)
return bboxes
det_tran = Sequence([RandomHorizontalFlip(1), RandomScale(0.4, diff = True), RandomRotate(10)])
coco_dataset = CocoDetection(root = "train2017", annFile = "annots.json", det_transforms = det_tran)
for image, annotation in coco_dataset:
# forward / backward pass
结论
这就结束了我们的一系列图像增强的目标检测任务。图像增强是对抗深度神经网络中过拟合的最强大但概念上简单的技术之一。随着我们的网络变得越来越复杂,我们需要更多的数据来获得良好的收敛速度,如果数据可用性是一个瓶颈,增强无疑是一种前进的方式。
我想象在接下来的几年里,我们会看到更复杂的数据增强形式,例如由生成网络进行的增强,以及智能增强,其中增强是为了产生更多网络努力的那种例子。
同时,我们的小库也定义了更多的扩充。通过打开docs
文件夹中的 index.html 文件,可以找到它们的详细摘要。我们还没有涵盖我们系列中的所有增强。例如,关于 HSV(色调、饱和度和亮度)的增强不包括在内,因为它们不需要增强边界框。
您现在可以继续,甚至定义一些您自己的增强。例如,我们没有实现垂直翻转,因为在倒置的图像上训练分类器没有意义。然而,如果我们的数据集由卫星图像组成,垂直翻转只是交换对象的方向,可能是有意义的。黑客快乐!
P.S. 本项目的文档已使用 Sphynx 生成。如果您实现了新的扩充,并且想要生成文档字符串,请使用 Numpy 约定来生成文档字符串。该项目的 Sphynx 文件位于docs/source
文件夹中。
进一步阅读
包围盒的数据扩充:旋转和剪切
原文:https://blog.paperspace.com/data-augmentation-for-object-detection-rotation-and-shearing/
这是该系列的第 3 部分,我们正在寻找使图像增强技术适应目标检测任务的方法。在这一部分,我们将介绍如何使用 OpenCV 的仿射变换特性来实现旋转和剪切图像以及包围盒。
在开始之前,强烈建议您阅读完本系列的最后两部分,因为它们构成了我们在这里要做的事情的基础。
GitHub 回购
本文和整个增强库的所有内容都可以在下面的 Github Repo 中找到。
https://github . com/paper space/dataincreasionforobjectdetection
证明文件
这个项目的文档可以在你的浏览器中打开docs/build/html/index.html
或者点击链接找到。
这个系列有 4 个部分。
1。第 1 部分:基础设计和水平翻转
2。第二部分:缩放和平移
3。第三部分:旋转和剪切
4。第 4 部分:烘焙增强到输入管道
这一部分假设您已经阅读了上面的文章,因为我们将使用在以前的文章中已经介绍过的功能。
我们走吧。
旋转
旋转变换的结果通常如下所示
旋转是最难处理的数据扩充之一。很快你就会知道为什么了。
在我们接触代码之前,我想在这里定义一些术语。
- 仿射变换。图像的一种变换,使得图像中的平行线在变换后保持平行。缩放、平移、旋转都是仿射变换的例子
在计算机图形学中,我们也使用一种叫做变换矩阵的东西,这是一种非常方便的工具来执行仿射变换。
详细讨论转换矩阵是不可能的,因为这会使我们偏离我们的任务。所以,我在文章的最后提供了一个链接,你可以在那里读到更多。同时,把变换矩阵想象成一个矩阵,用它乘以一个点的坐标来产生变换后的点。
变换矩阵是一个2 x 3
矩阵,乘以[x y 1]
,其中(x,y)是该点的坐标。有一个 1 的想法是为了方便剪切,你可以在下面的链接中了解更多。将一个2 x 3
矩阵与一个3 x 1
矩阵相乘,我们得到一个包含新点坐标的2 x 1
矩阵。
变换矩阵也可以用来得到一个点绕图像中心旋转后的坐标。将点旋转\(\theta\)的变换矩阵如下所示。
Image source: https://cristianpb.github.io/blog/image-rotation-opencv. Scale is 1.
谢天谢地,我们不需要编码。OpenCV 已经提供了内置的功能来使用它的cv2.warpAffine
函数来完成这项工作。所以,有了必备的理论知识,让我们开始吧。
我们从定义我们的__init__
函数开始。
def __init__(self, angle = 10):
self.angle = angle
if type(self.angle) == tuple:
assert len(self.angle) == 2, "Invalid range"
else:
self.angle = (-self.angle, self.angle)
旋转图像
现在,我们要做的第一件事是围绕中心旋转图像一个角度。为此,我们需要我们的转换矩阵。为此,我们使用 OpenCV 函数getRotationMatrix2D
。
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)
现在,我们可以通过使用warpAffine
函数简单地得到旋转图像。
image = cv2.warpAffine(image, M, (w, h))
该函数的第三个参数是(w,h)
,这是因为我们希望保持原来的分辨率。但是如果你稍微想象一下,一个旋转后的图像会有不同的维度,如果它们超过了原来的维度,OpenCV 会简单的把它们切掉。这里有一个例子。
OpenCV rotation side-effect.
我们在这里丢失了一些信息。那么,我们该如何克服这一点呢?谢天谢地,OpenCV 为我们提供了一个函数参数,帮助我们确定最终图像的尺寸。如果我们能把它从(w,h)
改变到一个尺寸,使正好容纳我们旋转的图像,我们就完成了。
这个灵感来自 Adrian Rosebrock 在他的博客 PyImageSearch 上的一篇文章。
现在的问题是我们如何找到这个新的维度。一点点三角学就能帮我们完成这项工作。确切地说,如果你看下面的图表。
Image source: https://cristianpb.github.io/blog/image-rotation-opencv
在哪里
$ $ N _ w = h * sin(\ theta)+w * cos(\ theta)\ N _ h = h * cos(\ theta)+w * sin(\ theta)$ $
现在,我们计算新的宽度和高度。注意,我们可以从转换矩阵中获得\(\sin(\theta)\)和\(\cos(\theta)\)的值。
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
还是少了点什么。有一点是肯定的,图像的中心不会移动,因为它本身就是旋转轴。然而,由于图像的宽度和高度现在是nW, nH
,中心必须位于nW/2, nH/2
。为了确保这一点,我们必须通过nW/2 - cX, nH/2 - cH
来转换图像,其中cX, cH
是先前的中心。
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
综上所述,我们将负责旋转图像的代码放在函数rotate_im
中,并将其放在bbox_util.py
中
def rotate_im(image, angle):
"""Rotate the image.
Rotate the image such that the rotated image is enclosed inside the tightest
rectangle. The area not occupied by the pixels of the original image is colored
black.
Parameters
----------
image : numpy.ndarray
numpy image
angle : float
angle by which the image is to be rotated
Returns
-------
numpy.ndarray
Rotated Image
"""
# grab the dimensions of the image and then determine the
# centre
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# perform the actual rotation and return the image
image = cv2.warpAffine(image, M, (nW, nH))
# image = cv2.resize(image, (w,h))
return image
旋转边界框
这是此次增强中最具挑战性的部分。在这里,我们首先需要旋转边界框,这给了我们一个倾斜的矩形框。然后,我们必须找到与包含倾斜矩形框的图像的边平行的最紧密的矩形。
我的意思是。
Final Bounding Box, shown only for one image.
现在,为了得到旋转后的边界框,如中间的图片所示,我们需要一个框的四个角的所有坐标。
我们实际上可以只使用两个角来得到最终的边界框,但这需要更多的三角运算来计算出只使用两个角的最终边界框的尺寸(在上图的右边,黑色)。有了中间框的四个角在中间,计算起来就容易多了。这只是让代码更复杂的问题。
因此,首先,我们在文件bbox_utils.py
中编写函数get_corners
来获得所有的 4 个角。
def get_corners(bboxes):
"""Get corners of bounding boxes
Parameters
----------
bboxes: numpy.ndarray
Numpy array containing bounding boxes of shape `N X 4` where N is the
number of bounding boxes and the bounding boxes are represented in the
format `x1 y1 x2 y2`
returns
-------
numpy.ndarray
Numpy array of shape `N x 8` containing N bounding boxes each described by their
corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`
"""
width = (bboxes[:,2] - bboxes[:,0]).reshape(-1,1)
height = (bboxes[:,3] - bboxes[:,1]).reshape(-1,1)
x1 = bboxes[:,0].reshape(-1,1)
y1 = bboxes[:,1].reshape(-1,1)
x2 = x1 + width
y2 = y1
x3 = x1
y3 = y1 + height
x4 = bboxes[:,2].reshape(-1,1)
y4 = bboxes[:,3].reshape(-1,1)
corners = np.hstack((x1,y1,x2,y2,x3,y3,x4,y4))
return corners
在这之后,现在我们有了由 8 个坐标x1,y1,x2,y2,x3,y3,x4,y4
描述的每个边界框。我们现在在文件bbox_util.py
中定义函数rotate_box
,它通过给我们变换后的点来为我们旋转边界框。我们使用转换矩阵。
def rotate_box(corners,angle, cx, cy, h, w):
"""Rotate the bounding box.
Parameters
----------
corners : numpy.ndarray
Numpy array of shape `N x 8` containing N bounding boxes each described by their
corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`
angle : float
angle by which the image is to be rotated
cx : int
x coordinate of the center of image (about which the box will be rotated)
cy : int
y coordinate of the center of image (about which the box will be rotated)
h : int
height of the image
w : int
width of the image
Returns
-------
numpy.ndarray
Numpy array of shape `N x 8` containing N rotated bounding boxes each described by their
corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`
"""
corners = corners.reshape(-1,2)
corners = np.hstack((corners, np.ones((corners.shape[0],1), dtype = type(corners[0][0]))))
M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cx
M[1, 2] += (nH / 2) - cy
# Prepare the vector to be transformed
calculated = np.dot(M,corners.T).T
calculated = calculated.reshape(-1,8)
return calculated
现在,最后一件事是定义一个函数get_enclosing_box
,它让我们得到了我们所说的最紧密的盒子。
def get_enclosing_box(corners):
"""Get an enclosing box for ratated corners of a bounding box
Parameters
----------
corners : numpy.ndarray
Numpy array of shape `N x 8` containing N bounding boxes each described by their
corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`
Returns
-------
numpy.ndarray
Numpy array containing enclosing bounding boxes of shape `N X 4` where N is the
number of bounding boxes and the bounding boxes are represented in the
format `x1 y1 x2 y2`
"""
x_ = corners[:,[0,2,4,6]]
y_ = corners[:,[1,3,5,7]]
xmin = np.min(x_,1).reshape(-1,1)
ymin = np.min(y_,1).reshape(-1,1)
xmax = np.max(x_,1).reshape(-1,1)
ymax = np.max(y_,1).reshape(-1,1)
final = np.hstack((xmin, ymin, xmax, ymax,corners[:,8:]))
return final
这再次给了我们一个符号,其中每个边界框由 4 个坐标或两个角来确定。使用所有这些辅助函数,我们最终组装了我们的__call__
函数。
def __call__(self, img, bboxes):
angle = random.uniform(*self.angle)
w,h = img.shape[1], img.shape[0]
cx, cy = w//2, h//2
img = rotate_im(img, angle)
corners = get_corners(bboxes)
corners = np.hstack((corners, bboxes[:,4:]))
corners[:,:8] = rotate_box(corners[:,:8], angle, cx, cy, h, w)
new_bbox = get_enclosing_box(corners)
scale_factor_x = img.shape[1] / w
scale_factor_y = img.shape[0] / h
img = cv2.resize(img, (w,h))
new_bbox[:,:4] /= [scale_factor_x, scale_factor_y, scale_factor_x, scale_factor_y]
bboxes = new_bbox
bboxes = clip_box(bboxes, [0,0,w, h], 0.25)
return img, bboxes
注意,在函数的末尾,我们重新调整了图像和边界框的尺寸,这样我们的最终尺寸是w,h
而不是nW, nH
。这只是为了保持图像的尺寸。我们还裁剪了一些方框,以防任何方框在变换后会从图像中消失。
剪羊毛
剪切是另一种边界框变换,它可以在变换矩阵的帮助下完成。剪切产生的效果看起来像。
在剪切中,我们将矩形图像变成...嗯...类似平行四边形的图像?剪切中使用的变换矩阵是。
上面是一个水平剪切的例子。在此,坐标为x
、y
的像素被移动到x + alpha*y
、y
。alpha
是剪切因子。因此,我们将__init__
函数定义为。
class RandomShear(object):
"""Randomly shears an image in horizontal direction
Bounding boxes which have an area of less than 25% in the remaining in the
transformed image is dropped. The resolution is maintained, and the remaining
area if any is filled by black color.
Parameters
----------
shear_factor: float or tuple(float)
if **float**, the image is sheared horizontally by a factor drawn
randomly from a range (-`shear_factor`, `shear_factor`). If **tuple**,
the `shear_factor` is drawn randomly from values specified by the
tuple
Returns
-------
numpy.ndaaray
Sheared image in the numpy format of shape `HxWxC`
numpy.ndarray
Tranformed bounding box co-ordinates of the format `n x 4` where n is
number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
"""
def __init__(self, shear_factor = 0.2):
self.shear_factor = shear_factor
if type(self.shear_factor) == tuple:
assert len(self.shear_factor) == 2, "Invalid range for scaling factor"
else:
self.shear_factor = (-self.shear_factor, self.shear_factor)
shear_factor = random.uniform(*self.shear_factor)
增强逻辑
因为我们只涉及水平剪切,我们只需要根据等式x = x + alpha*y
改变盒子角的 x 坐标。我们的调用函数看起来像。
def __call__(self, img, bboxes):
shear_factor = random.uniform(*self.shear_factor)
w,h = img.shape[1], img.shape[0]
if shear_factor < 0:
img, bboxes = HorizontalFlip()(img, bboxes)
M = np.array([[1, abs(shear_factor), 0],[0,1,0]])
nW = img.shape[1] + abs(shear_factor*img.shape[0])
bboxes[:,[0,2]] += ((bboxes[:,[1,3]]) * abs(shear_factor) ).astype(int)
img = cv2.warpAffine(img, M, (int(nW), img.shape[0]))
if shear_factor < 0:
img, bboxes = HorizontalFlip()(img, bboxes)
img = cv2.resize(img, (w,h))
scale_factor_x = nW / w
bboxes[:,:4] /= [scale_factor_x, 1, scale_factor_x, 1]
return img, bboxes
一个有趣的例子是负切变。负剪切需要更多一点的黑客工作。如果我们只是用正剪切的情况来剪切,我们得到的盒子一定会更小。这是因为为了让方程工作,盒子的坐标必须是格式x1, y1, x2, y2
,其中x2
是我们剪切方向的更远处的角。
这适用于正剪切的情况,因为在我们的默认设置中,x2 是右下角的 x 坐标,而x1
是左上角的坐标。剪切的方向是正的,或从左向右。
当我们使用负剪切时,剪切的方向是从右向左,而x2
在负方向上不比x1
更远。解决这个问题的一个方法是得到另一组角(这将满足约束,你能证明吗?).应用剪切变换,然后根据我们遵循的符号改变到另一组角。
我们可以这么做,但有更好的方法。以下是如何用剪切因子-alpha
执行负剪切。
- 水平翻转图像和方框。
- 使用剪切系数
alpha
应用正剪切变换 - 再次水平翻转图像和方框。
我更希望你拿一张纸和一支笔来验证为什么上面的方法有效!由于这个原因,你会在上面的函数中看到两个处理负剪力的代码行。
if shear_factor < 0:
img, bboxes = HorizontalFlip()(img, bboxes)
测试它
现在,我们已经完成了旋转和剪切增强,是时候测试它们了。
from data_aug.bbox_utils import *
import matplotlib.pyplot as plt
rotate = RandomRotate(20)
shear = RandomShear(0.7)
img, bboxes = rotate(img, bboxes)
img,bboxes = shear(img, bboxes)
plt.imshow(draw_rect(img, bboxes))
这是这一部分,我们几乎完成了我们的扩增。只剩下一点调整大小的增加,这与其说是增加,不如说是输入预处理步骤。
在下一个也是最后一个部分,我们将向您展示如何快速将这些增强纳入您的深度学习输入管道,如何无缝地将它们与多个增强相结合,以及如何生成文档。
练习
这里有一些你可以自己尝试的事情。
- 实现上述扩充的确定性版本。
- 缩放和平移也可以使用具有小得多的代码的变换矩阵来实现。试着用这种方式实现它们。
- 实施垂直剪切
进一步阅读
司法资源数据
现在,那些致力于种族公正和公平的人比以往任何时候都更需要有意的支持。虽然 Paperspace 是一家技术公司,但我们仍然可以为数据科学家和机器学习工程师为实现社会变革所做的宝贵工作做出贡献。
作为一个团队,我们发起了一项种族平等倡议,迄今为止,该倡议提供了在线编码研讨会、学费支持以及与有色人种和黑人企业的合作。
作为我们计划的一部分,我们开始寻找最引人注目、最有影响力和最有趣的项目,利用数据的力量来实现公正。下面,我们分享了一组迄今为止我们发现的最具代表性的公司、项目和资源:
黑人生活的数据——由有色女性创立和运营,D4BL 将“数据视为抗议。数据作为责任。数据作为集体行动。”通过活动家、组织者和数学家之间的直接合作,他们是一场“在黑人生活中创造具体和可衡量的变化的运动。”
零号战役 —这个平台让任何人都可以很容易地了解警察暴力事件,了解更多信息,并采取行动(例如,向当地代表发送电子邮件)。
加州警察记分卡 —交互式、精心设计的数据仪表板,显示加州警察局基于其警务策略的等级和分数(目前仅适用),特别关注暴力问题&问责制,并计划扩展到所有 50 个州。
警务公正中心——“你如何衡量公正?”中心问。该组织聚集了科学家、种族平等专家、“数据专家”和社区领袖,致力于体现“科学可以打造的通往公共安全、社区信任和种族平等的道路”请务必查看该中心的 Kaggle 数据集和经常更新的竞赛页面。
data kind—合作的晚间、周末或数月项目研讨会为社会变革组织提供无偿数据科学团队,以解决他们的特殊问题&需求。这使拥有数据科学人才和资源的组织能够受益于大型盈利性公司,同时也向数据科学家展示了他们如何利用自己的技能造福人类。
HRDAG——(人权数据分析组)——第一站:人权。该组织孜孜不倦地致力于“构建科学上站得住脚的、基于证据的论点,从而产生问责的结果。”每一个项目和倡议都是由“基于证据的论证推动的,这些论证将导致问责的结果。”
Bayes 的影响 — Bayes 正在开发开源的 web 应用程序,这些应用程序为执法机构提供了一个低摩擦的界面来收集既标准化又可靠的重要数据。“我们的第一个产品是一个名为 URSUS 的网络工具,由 CA 司法部长和司法部共同开发,帮助加州所有 800 个执法机构收集和报告使用武力的数据。不计成本。”
【生活城市】——LC 与 Code for America 和 National Neighborhood Indicators Partnership 等知名组织合作,提供赠款、辅导和知识共享,以支持解决低收入人群需求的城市级数据解决方案。
无论你是一名数据科学家、ML 工程师、活动家,或者只是一个感到有贡献的人,我们希望你能在上面的资源中找到灵感,甚至是你的下一步行动。此外,如果你对这个列表有什么建议,如果你能写信给与我们分享,我们将不胜感激。
PyTorch 中数据加载器类和抽象的综合指南
原文:https://blog.paperspace.com/dataloaders-abstractions-pytorch/
在这篇文章中,我们将处理机器学习和深度学习领域最具挑战性的问题之一:加载和处理不同类型的数据。
假设您已经熟悉了 PyTorch 中的神经网络编码,现在您正在使用带有多层感知器的 MNIST 数据集预测一个数字。在这种情况下,您可能使用 torch DataLoader
类直接加载图像并将其转换为张量。但是现在,在这篇文章中,我们将学习如何超越DataLoader
类,并遵循在处理各种形式的数据(如 CSV 文件、图像、文本等)时可以使用的最佳实践。以下是我们将要涉及的主题。
- 处理数据集
- PyTorch 中的数据加载
- 深入查看 MNIST 数据集
- 转换和重新调整数据
- 在 PyTorch 中创建自定义数据集
- 摘要
您可以跟随代码并在 ML Showcase 的渐变社区笔记本上免费运行它。
处理数据集
如果你正在从事一个涉及深度学习的实时项目,通常你的大部分时间都用于处理数据,而不是你要建立的神经网络。这是因为数据就像是你网络的燃料:越合适,结果就越快越准确!你的神经网络表现不佳的一个主要原因可能是由于坏的,或理解不充分的数据。因此,以更直观的方式理解、预处理数据并将其加载到网络中是非常重要的。
在许多情况下,我们在默认或众所周知的数据集(如 MNIST 或 CIFAR)上训练神经网络。在进行这些工作时,我们可以轻松地实现预测和分类类型问题的 90%以上的准确率。原因是,这些数据集组织整齐,易于预处理。但是当你在自己的数据集上工作时,要达到高精度是相当棘手和具有挑战性的。在接下来的章节中,我们将学习如何使用自定义数据集。在此之前,我们将快速浏览一下 PyTorch 库中包含的数据集。
PyTorch 附带了几个内置数据集,所有这些都预加载在类torch.datasets
中。想起什么了吗?在前面的例子中,当我们对 MNIST 图像进行分类时,我们使用同一个类来下载我们的图像。包里有什么torch
和torchvision
?包torch
由实现神经网络所需的所有核心类和方法组成,而torchvision
是一个支持包,由流行的数据集、模型架构和计算机视觉的常见图像转换组成。还有一个名为torchtext
的包,它拥有 PyTorch 自然语言处理的所有基本工具。该包由与文本相关的数据集组成。
这里有一个包含在类torchvision
和torchtext
中的数据集的快速概述。
火炬视觉中的数据集
MNIST: MNIST 是一个数据集,由归一化和中心裁剪的手写图像组成。它有超过 60,000 张训练图像和 10,000 张测试图像。这是学习和实验中最常用的数据集之一。要加载和使用数据集,您可以在安装完torchvision
包后使用下面的语法导入。
- T2
torchvision.datasets.MNIST()
时尚 MNIST: 该数据集类似于 MNIST,但该数据集包括 t 恤、裤子、包包等服装项目,而不是手写数字。训练和测试样本数分别为 60000 和 10000。下面是 FMNIST 类的位置。
- T2
torchvision.datasets.FashionMNIST()
CIFS ar:****CIFS ar 数据集有两个版本,CIFAR10 和 CIFAR100。CIFAR10 由 10 个不同标签的图像组成,而 CIFAR100 有 100 个不同的类别。这些包括常见的图像,如卡车、青蛙、船、汽车、鹿等。建议将此数据集用于构建 CNN。
- T2
torchvision.datasets.CIFAR10()
- T2
torchvision.datasets.CIFAR100()
这个数据集由超过 100,000 个日常物品组成,比如人、瓶子、文具、书籍等。这个图像数据集广泛用于对象检测和图像字幕应用。下面是可以加载 COCO 的位置:
- T2
torchvision.datasets.CocoCaptions()
EMNIST: 该数据集是 MNIST 数据集的高级版本。它由图像组成,包括数字和字母。如果您正在处理一个基于从图像中识别文本的问题,这是进行训练的合适数据集。下面是课堂:
torchvision.datasets.EMNIST()
IMAGE-NET: ImageNet 是用于训练高端神经网络的旗舰数据集之一。它包含超过 120 万张图片,分布在 10,000 个类别中。通常,该数据集被加载到高端硬件系统上,因为单靠 CPU 无法处理如此大规模的数据集。下面是加载 ImageNet 数据集的类:
- T2
torchvision.datasets.ImageNet()
这些是在 PyTorch 中构建神经网络时最常用的几个数据集。其他一些包括 KMNIST,QMNIST,LSUN,STL10,SVHN,PhotoTour,SBU,Cityscapes,SBD,USPS,Kinetics-400。你可以从 PyTorch 的官方文档中了解更多。
Torchtext 中的数据集
如前所述,torchtext
是一个支持包,由所有自然语言处理的基本工具组成。如果你是 NLP 的新手,它是人工智能的一个子领域,处理和分析大量的自然语言数据(大多与文本相关)。
现在让我们来看看几个流行的文本数据集进行实验和工作。
IMDB: 这是一个用于情感分类的数据集,包含 25,000 条用于训练的高度极性电影评论,以及另外 25,000 条用于测试的评论。我们可以通过使用torchtext
中的以下类来加载这些数据:
- T2
torchtext.datasets.IMDB()
WikiText2: 这个语言建模数据集是超过 1 亿个标记的集合。它摘自维基百科,保留了标点符号和实际的字母大小写。它广泛用于涉及长期依赖的应用程序中。该数据可从torchtext
加载,如下所示:
- T2
torchtext.datasets.WikiText2()
除了上面两个流行的数据集,在torchtext
库中还有更多可用的,如 SST、TREC、SNLI、MultiNLI、WikiText-2、WikiText103、PennTreebank、Multi30k 等。
到目前为止,我们已经看到了基于一组预定义的图像和文本的数据集。但是如果你有自己的呢?你是怎么装的?现在让我们学习一下ImageFolder
类,您可以用它来加载您自己的图像数据集。
ImageFolder 类
ImageFolder
是torchvision
中的一个通用数据加载器类,可以帮助你加载你自己的图像数据集。让我们想象一下,你正在处理一个分类问题,并建立一个神经网络来识别给定图像是苹果还是橙子。要在 PyTorch 中做到这一点,第一步是在默认的文件夹结构中排列图像,如下所示:
`root
├── orange
│ ├── orange_image1.png
│ └── orange_image1.png
├── apple
│ └── apple_image1.png
│ └── apple_image2.png
│ └── apple_image3.png`
**如图所示排列数据集后,可以使用ImageLoader
类加载所有这些图像。下面是您可以使用的代码片段:
torchvision.datasets.ImageFolder(root, transform)
在下一节中,让我们看看如何将数据加载到我们的程序中。
PyTorch 中的数据加载
数据加载是建立深度学习管道或训练模型的第一步。当数据的复杂性增加时,这项任务变得更具挑战性。在本节中,我们将了解 PyTorch 中的DataLoader
类,它帮助我们加载和迭代数据集中的元素。该类在torch.utils.data
模块中作为DataLoader
提供。DataLoader
可以导入如下:
from torch.utils.data import DataLoader
现在让我们详细讨论一下DataLoader
类接受的参数,如下所示。
from torch.utils.data import DataLoader
DataLoader(
dataset,
batch_size=1,
shuffle=False,
num_workers=0,
collate_fn=None,
pin_memory=False,
)
1。数据集:DataLoader
类中的第一个参数是dataset
。这是我们加载数据的地方。
2。数据分批: batch_size
指一次迭代中使用的训练样本数。通常,我们将数据分为训练集和测试集,并且每个数据集可能有不同的批量大小。
3。搅乱数据: shuffle
是传递给DataLoader
类的另一个参数。该参数接受一个布尔值(真/假)。如果随机播放设置为True
,那么所有的样本都被随机播放并批量加载。否则,它们将被一个接一个地发送,没有任何洗牌。
4。允许多处理:由于深度学习涉及到用大量数据训练模型,只运行单个进程最终会花费大量时间。在 PyTorch 中,您可以通过使用参数num_workers
允许多重处理来增加同时运行的进程数量。这也取决于批处理的大小,但是我不会将num_workers
设置为相同的数字,因为每个工人只装载一个批处理,并且只在它准备好的时候才返回它。
num_workers=0
表示它是在需要时加载数据的主进程。num_workers=1
表示你只有一个工人,所以可能会慢。
5。合并数据集:如果我们想要合并数据集,就使用collate_fn
参数。此参数是可选的,主要用于从地图样式的数据集中加载批处理。
6。在 CUDA 张量上加载数据:您可以使用pin_memory
参数直接将数据集加载为 CUDA 张量。它是一个可选参数,接受一个布尔值;如果设置为True
,DataLoader
类会在返回张量之前将它们复制到 CUDA 固定的内存中。
让我们看一个例子来更好地理解通常的数据加载管道。
深入查看 MNIST 数据集
PyTorch 的torchvision
存储库托管了一些标准数据集,MNIST 是最受欢迎的之一。现在,我们将了解 PyTorch 如何从 pytorch/vision 存储库中加载 MNIST 数据集。让我们首先下载数据集,并将其加载到一个名为data_train
的变量中。然后我们将打印一个样本图像。
# Import MNIST
from torchvision.datasets import MNIST
# Download and Save MNIST
data_train = MNIST('~/mnist_data', train=True, download=True)
# Print Data
print(data_train)
print(data_train[12])
输出:
Dataset MNIST Number of datapoints: 60000 Root location: /Users/viharkurama/mnist_data Split: Train (<PIL.Image.Image image mode=L size=28x28 at 0x11164A100>, 3)
现在让我们尝试提取元组,其中第一个值对应于图像,第二个值对应于其各自的标签。下面是代码片段:
import matplotlib.pyplot as plt
random_image = data_train[0][0]
random_image_label = data_train[0][1]
# Print the Image using Matplotlib
plt.imshow(random_image)
print("The label of the image is:", random_image_label)
大多数时候,您不会访问带有索引的图像,而是将包含图像的矩阵发送到您的模型。当您需要准备数据批次时(也许,在每次运行之前对它们进行洗牌),这很方便。现在让我们看看这是如何实时工作的。让我们使用DataLoader
类来加载数据集,如下所示。
import torch
from torchvision import transforms
data_train = torch.utils.data.DataLoader(
MNIST(
'~/mnist_data', train=True, download=True,
transform = transforms.Compose([
transforms.ToTensor()
])),
batch_size=64,
shuffle=True
)
for batch_idx, samples in enumerate(data_train):
print(batch_idx, samples)
这就是我们如何使用DataLoader
加载一个简单的数据集。然而,我们不能总是依赖于每个数据集的DataLoader
。我们经常处理包含不对称分辨率图像的大型或不规则数据集,这就是 GPU 发挥重要作用的地方。
在 GPU 上加载数据
我们可以启用 GPU 来更快地训练我们的模型。现在让我们看看在加载数据时可以使用的CUDA
(GPU 对 PyTorch 的支持)的配置。下面是一个示例代码片段:
device = "cuda" if torch.cuda.is_available() else "cpu"
kwargs = {'num_workers': 1, 'pin_memory': True} if device=='cuda' else {}
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('/files/', train=True, download=True),
batch_size=batch_size_train, **kwargs)
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('files/', train=False, download=True),
batch_size=batch_size, **kwargs)
在上面,我们声明了一个名为device
的新变量。接下来,我们编写一个简单的if
条件来检查当前的硬件配置。如果它支持GPU
,它会将device
设置为cuda
,否则它会将其设置为cpu
。变量num_workers
表示并行生成批处理的进程数量。对于数据加载,将pin_memory=True
传递给DataLoader
类会自动将提取的数据张量放入固定内存,从而使数据更快地传输到支持 CUDA 的 GPU。
在下一节中,我们将学习转换,它定义了加载数据的预处理步骤。
转换和重新调整数据
PyTorch 变换定义了简单的图像变换技术,可将整个数据集转换为独特的格式。例如,考虑包含不同分辨率的不同汽车图片的数据集。训练时,训练数据集中的所有图像应该具有相同的分辨率大小。如果我们手动将所有图像转换为所需的输入大小,这将非常耗时,因此我们可以使用 transforms 来代替;通过几行 PyTorch 代码,我们数据集中的所有图像都可以转换成所需的输入大小和分辨率。您也可以使用transforms
模块调整它们的大小。最常用的几个操作是transforms.Resize()
调整图像大小、transforms.CenterCrop()
从中心裁剪图像,以及transforms.RandomResizedCrop()
随机调整数据集中所有图像的大小。
现在让我们从torchvision.datasets
加载 CIFAR10 并应用以下转换:
- 将所有图像的大小调整为 32×32
- 对图像应用中心裁剪变换
- 将裁剪的图像转换为张量
- 标准化图像
首先,我们导入必要的模块,以及来自torchvision
模块的transforms
。NumPy 和 Matplotlib 库用于可视化数据集。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
接下来,我们将定义一个名为transforms
的变量,在这个变量中,我们将按顺序编写所有预处理步骤。我们使用了Compose
类将所有的转换操作链接在一起。
transform = transforms.Compose([
# resize
transforms.Resize(32),
# center-crop
transforms.CenterCrop(32),
# to-tensor
transforms.ToTensor(),
# normalize
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
resize
:此Resize
转换将所有图像转换为定义的大小。在这种情况下,我们希望将所有图像的大小调整为 32×32。因此,我们把32
作为一个参数。center-crop
:接下来我们使用CenterCrop
变换来裁剪图像。我们发送的参数也是分辨率/大小,但是因为我们已经将图像的大小调整为32x32
,所以图像将与这个裁剪居中对齐。这意味着图像将从中心(垂直和水平)裁剪 32 个单位。to-tensor
:我们使用方法ToTensor()
将图像转换成Tensor
数据类型。normalize
:这将张量中的所有值归一化,使它们位于 0.5 和 1 之间。
在下一步中,在执行了我们刚刚定义的转换之后,我们将使用trainloader
将CIFAR
数据集加载到trainset
中。
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
shuffle=False)
我们从torchvision.datasets
获取 CIFAR 数据集,将train
和download
参数设置为True
。接下来,我们将转换参数设置为已定义的transform
变量。DataLoader
iterable 被初始化,我们将trainset
作为参数传递给它。batch_size
被设置为4
,随机播放为False
。接下来,我们可以使用下面的代码片段来可视化图像。在 ML Showcase 上查看相应的渐变社区笔记本来运行代码并查看结果。
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
dataiter = iter(trainloader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images))
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
除了Resize()
、CenterCrop()
和RandomResizedCrop()
之外,还有各种其他的Transform
等级。让我们看看最常用的。
转换类别
- PyTorch 中的这个类在任意位置裁剪给定的 PIL 图像。以下是
RandomCrop
接受的论点:
torchvision.transforms.RandomCrop(size, padding=None, pad_if_needed=False, fill=0)
size
:该参数取一个整数,表示期望的随机裁剪输出大小。例如,如果大小设置为 32,输出将是大小为 32×32 的随机裁剪图像。padding
:这是一个整数自变量,初始设置为None
。如果设置为整数,它会为图像添加额外的边框。例如,如果填充设置为4
,则它会以 4 个单位填充左、上、右和下边框。pad_if_needed
:可选参数,取布尔值。如果它被设置为True
,那么它会在图像周围填充一个较小的区域,以避免最小的分辨率误差。默认情况下,该参数设置为False
。fill
:该常量值初始化所有填充像素的值。默认填充值为0
。
2.有时,为了让模型在训练时更加健壮,我们会随机翻转图像。类RandomHorizontalFlip
用于实现这样的结果。它有一个默认参数,p
,表示图像翻转的概率(在 0 和 1 之间)。默认值为0.5
。
torchvision.transforms.RandomHorizontalFlip(p=0.5)
3.Normalize
: 将图像标准化,以平均值和标准偏差作为参数。该类有四个参数,如下所示:
torchvision.transforms.functional.normalize(tensor, mean, std, inplace=False)
tensor
参数接受一个具有三个值的张量:C、H 和 w。它们分别代表通道的数量、高度和宽度。基于给定的参数,输入图像的所有像素值都被归一化。mean
和std
参数接受一系列关于每个通道的平均值和标准偏差。inplace
参数是一个布尔值。如果设置为True
,所有操作都应就地计算。
4.ToTensor
: 这个类将 PIL 图像或者一个 NumPy n 维数组转换成一个张量。
torchvision.transforms.functional.to_tensor(img)
现在,让我们了解加载自定义数据集背后的机制,而不是使用内置数据集。
在 PyTorch 中创建自定义数据集
到目前为止,我们已经学习了加载数据集以及预处理数据的各种方法。在本节中,我们将创建一个由数字和文本组成的简单自定义数据集。我们将讨论 PyTorch 中的Dataset
对象,它有助于处理数字和文本文件,以及如何针对特定任务优化管道。这里的技巧是抽象数据集类中的__getitem__()
和__len__()
方法。
__getitem__()
方法通过索引返回数据集中选定的样本。__len__()
方法返回数据集的总大小。例如,如果您的数据集包含 1,00,000 个样本,那么len
方法应该返回 1,00,000。
注意,此时,数据尚未加载到内存中。
下面是解释__getitem__()
和__len__()
方法实现的抽象视图:
class Dataset(object):
def __getitem__(self, index):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
创建自定义数据集并不复杂,但是作为加载数据的典型过程的附加步骤,有必要构建一个接口来获得一个良好的抽象(至少可以说是一个良好的语法糖)。现在,我们将创建一个包含数字及其平方值的新数据集。让我们称我们的数据集为 SquareDataset。它的目的是返回范围[a,b]
内值的平方。下面是相关代码:
import torch
import torchvision
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets, transforms
class SquareDataset(Dataset):
def __init__(self, a=0, b=1):
super(Dataset, self).__init__()
assert a <= b
self.a = a
self.b = b
def __len__(self):
return self.b - self.a + 1
def __getitem__(self, index):
assert self.a <= index <= self.b
return index, index**2
data_train = SquareDataset(a=1,b=64)
data_train_loader = DataLoader(data_train, batch_size=64, shuffle=True)
print(len(data_train))
在上面的代码块中,我们创建了一个名为 SquareDataset 的 Python 类,它继承了 PyTorch 的 Dataset 类。接下来,我们调用了一个__init__()
构造函数,其中a
和b
分别被初始化为0
和1
。super
类用于从继承的Dataset
类中访问len
和get_item
方法。接下来,我们使用assert
语句来检查a
是否小于或等于b
,因为我们想要创建一个数据集,其中的值位于a
和b
之间。
然后,我们使用SquareDataset
类创建了一个数据集,其中的数据值在 1 到 64 的范围内。我们将它加载到一个名为data_train
的变量中。最后,Dataloader
类为存储在data_train_loader
中的数据创建了一个迭代器,其中batch_size
被初始化为 64,shuffle
被设置为True
。
数据加载器通过采用面向对象的编程概念来利用 Python 的优点。一个很好的练习是使用一些流行的数据集,包括 CelebA、PIMA、COCO、ImageNet、CIFAR-10/100 等,检查各种数据加载器。
摘要
在这篇文章中,我们学习了数据加载和抽象。我们从包torchvision
和torchtext
中可用的数据集开始,回顾了几个流行的数据集。然后我们学习了DataLoader
类,以及它在按照给定的参数组织数据时的重要性。后来,我们通过查看各种可能的技术将 MNIST 数据集调用到我们的工作空间中,对其进行了深入分析。还引入了数据加载器和转换,在 MNIST 的例子中提到了它们的重要性。通过对RandomCrop
、RandomHorizontalFlip
、Normalize
、ToTensor
和RandomRotate
类的解释,对转换及其类有了更深入的了解。此后,通过 PyTorch CUDA 的例子解释了 GPU 优于 CPU 的原因。创建一个自定义数据集并不是一项复杂的任务,这个语句已经通过一小段代码得到了证明。您在本教程中学到的概念和基础知识都是使用 PyTorch 的基础。**
理解 GauGAN 第 4 部分:调试培训&决定 GauGAN 是否适合你
原文:https://blog.paperspace.com/debugging-gaugan-training-and-business-considerations/
嘿,伙计们,欢迎来到我们高根系列的第四部分。在前三篇文章中,我们讨论了以下主题:
在这一部分,我们将讨论如果你的结果不好,你能做什么。我们还将讨论您是否可以将 GauGAN 用于您在生产中需要的东西。
在这篇文章中,我将从我称之为甘急救包开始。这将涵盖万一你的结果不好时该怎么做的基本提示和技巧。然而,如果这些问题仍然没有解决,那么也许你需要后退一步,重新考虑 GauGAN 是否是你的正确选择。即使理论上有可能用 GauGAN 来解决你正在尝试解决的问题,但这可能需要一个巨大的数据集或非常高的计算预算,这将把成本增加到可能不值得投资的水平。
因此,在本文的后半部分,我将讨论 GauGAN 从商业角度来看是否有意义。尽管 GANs 令人兴奋,但它们仍处于非常初级的发展阶段。GauGAN 自称是语义图像生成的最先进技术,但它仍然无法达到可以商业化或运输的产品所需的行为类型。
所以,让我们开始吧!
甘急救包
所以,你在数据集上训练,结果看起来非常糟糕。你是做什么的?你是合身还是合身?
GauGAN 的损耗曲线
一般来说,GAN 训练曲线比它们的判别表兄弟(如对象检测或图像分类)更难解释。这是因为即使是训练有素的模型,损失率也会收敛到一个恒定值的范围内,并在这些值之间保持振荡。例如,这是一个训练有素的 DCGAN 模型的鉴频器和发生器的损耗图,这是一个非常有效的 GAN。
Discriminator Loss
如果您在训练对象检测模型时看到这样的结果,您肯定可以说训练已经饱和,模型已经停止学习。然而,在甘被训练的例子中,学习一直在进行。
Training curves from Tensorboard for GauGAN trained on Camvid
为什么甘人会这样?这是因为与只有一个神经网络在学习(并试图减少损失)的正常深度学习算法不同,GANs 有两个神经网络相互竞争。对于数学倾向,两个模型被同时训练以找到两人非合作博弈的纳什均衡。然而,每个模型独立更新其成本,而不考虑游戏中的其他玩家。同时更新两个模型的梯度不能保证收敛。
如果上面的线对你没有任何意义,请理解这一点:如果鉴频器的损耗下降,那么发电机的损耗必然上升,反之亦然。两者的饱和损耗意味着两者实际上是相互竞争的。如果做不到这一点,我们的培训就无法进行。
如果鉴别器完全抽取了发生器,你的发生器什么也学不到。然而,如果发生相反的情况,生成器可以产生任何乱码,鉴别器将无法将其识别为假的。鉴别器通常被表示为发生器的对手,但实际上,发生器只从鉴别器反向传播的梯度中学习。换句话说,鉴别器为生成器提供监督,并告诉它“嘿,生成器,这还不够真实。利用这些梯度,更好地工作”。
因此,发生器和鉴别器之间的不平衡是我们需要经常使用 gan 解决的问题。
鉴别器和发生器之间的不平衡
当我们谈到鉴频器和发电机之间的不平衡时,最有可能的是我们要处理鉴频器超过发电机的问题。如果鉴别器失败,那么生成器可以自由地产生任意随机图像,而不会受到惩罚,因此生成器也将失败。
那么,你如何确定你的鉴别器压制了发电机?首先,鉴频器损耗几乎为零,而发电机损耗将会很高。直观检查您的训练结果也可能会显示您的发生器产生的主要是噪声,而不是真实的图像。还应该检查鉴别器的分类精度;如果鉴别器精度在相当长的一段时间内超过 80-85%,则可能出现这种不平衡。
如何恢复平衡
有几件事你可以试着克服这个问题。考虑按顺序执行以下操作:
- 第一步也是非常直观的一步是通过增加发生器中的过滤器数量(
--ngf
)和层数(--n_downsample_layers
)的值来使发生器更强大。 - 降低发电机的学习率。这可能有助于它更好地学习,因为它可以更彻底地探索损失面,而不是四处射击。你也可以将一个较慢的学习速率和比鉴别器更多的更新步骤结合起来。例如,考虑在每次更新鉴别器时更新生成器两次。
- 对真实图像使用软标签。虽然在训练的开始,当计算鉴别器损耗时,真实图像的标签被设置为 1,但是尝试用 0.9(或者可能是 0.7 和 1.2 之间的随机数)来表示它们。这到底为什么有效?据观察,对于硬标签(1),鉴别器可能变得过于自信,并最终仅依赖于特征的子集来对示例进行分类。这可能导致生成器只关注那些特征来欺骗鉴别器,从而导致训练崩溃。
- 在鉴别器中使用光谱归一化。GauGAN 的代码 repo 默认情况下对生成器和鉴别器都使用谱范数。
- 在将真实数据和生成的数据发送到鉴别器之前,向它们添加噪声。有什么帮助?这很有帮助,因为人们经常观察到,尽管数据分布是高维的,但却存在于低维流形上,这使得鉴别器很容易找到一个超平面,将真实数据与虚假数据完美分离。如果这一行对你来说没有意义,不要担心。这只是意味着噪声阻止了鉴频器完全压制发电机。
- 最后,如果您发现您的生成器已经停止改进,尝试使用
--ndf
标志使鉴别器更强大。有时发电机停止学习是因为监督不够好。
在 GauGAN 的其他损失
除了主要的对抗性损失,GauGAN 还使用了特征匹配损失和感知损失。(我没有带 KL 发散损失训练)。
这是稳定训练中每次损失的样子。
From top to bottom: curves for feature matching loss and perceptual loss.
我希望你对逐渐减少的感知损失(VGG 损失)保持一点警惕。VGG 损失在训练集上正在减少,但您可能也希望在测试集/验证集上看到它。由于对抗性损失在训练期间稳定,梯度下降倾向于通过减少感知损失来减少净损失。你必须小心,以防这导致过度拟合,并相应地通过重新调整它来减少它对损失项的贡献。
不幸的是,GauGAN 的代码没有提供设置砝码刻度的选项,必须通过修改代码来实现。这可以通过在文件pix2pix_trainer.py
中摆弄函数run_generator_one_step
和run_discriminator_one_step
中定义的代码来完成。
def run_generator_one_step(self, data):
self.optimizer_G.zero_grad()
g_losses, generated = self.pix2pix_model(data, mode='generator')
g_loss = sum(g_losses.values()).mean()
g_loss.backward()
self.optimizer_G.step()
self.g_losses = g_losses
self.generated = generated
Code to modify scales for Generator Losses
第 4 行返回的对象g_loss
是由关键字GAN
(甘损失)GAN_Feat
(特征匹配损失)VGG
(VGG 损失)组成的字典。将这些值乘以数字来缩放它们。例如,假设我想将 VGG 损失缩放 2。所以,我的函数应该是这样的:
def run_generator_one_step(self, data):
self.optimizer_G.zero_grad()
g_losses, generated = self.pix2pix_model(data, mode='generator')
# scale modification
g_losses['VGG'] *= 2
g_loss = sum(g_losses.values()).mean()
g_loss.backward()
self.optimizer_G.step()
self.g_losses = g_losses
self.generated = generated
Modified code to scale VGG Loss
def run_discriminator_one_step(self, data):
self.optimizer_D.zero_grad()
d_losses = self.pix2pix_model(data, mode='discriminator')
d_loss = sum(d_losses.values()).mean()
d_loss.backward()
self.optimizer_D.step()
self.d_losses = d_losses
Code to modify scales for Discriminator Losses
类似地,第 3 行返回的对象d_losses
包含关键字D_real
和D_fake
,分别用于真实和虚假图像的鉴别器损失。
批量
批量越大,结果应该越好,因为小批量通常会对数据分布的统计数据提供非常嘈杂的估计。作者使用 128 的批量大小,这需要一个八 GPU 机器,每个 GPU 具有 16 GB VRAM。因此,训练 GauGAN 可能相当昂贵。
如果您正在为您的业务使用 GauGAN,或者如果您缺少资源,也许现在不是使用 GauGAN 的最佳时机。在下一节中,我将介绍如何决定是否应该重新考虑使用 GauGAN 的决定——尤其是从商业角度。
GauGAN 适合你吗?
这是这篇文章的要点。GauGAN 适合你吗?在 MathWorks 工作时,我学到了很多关于实现 DL 算法的商业方面的东西。就这些而言,GauGAN 的要求相当高,原因如下。
你的问题应该是适当的困难
我这么说是什么意思?你首先要考虑的是,GauGAN 有它的局限性。这项技术在很大程度上仍处于萌芽阶段。高根擅长综合各种事物的纹理细节。只要边界不在同一个类的对象之间,它们通常都处理得很好。虽然实例分割贴图在这种情况下有所帮助,但两个相似类别的重叠对象通常会变形为一个扭曲的对象。
Nvidia 在他们的 GauGAN 演示中使用了 Flickr landscapes 数据集。风景主要由纹理组成,例如山、天空、大海等的纹理。
然而,GauGAN 可能难以应对密集的交通场景,这需要它在一个小区域内创建具有大量空间细节的对象。以下图中的汽车为例。
GauGAN messes up details of a car
与纹理填充不同,纹理填充具有相当大的随机性,对象的结构组件需要以更受约束的方式进行渲染。虽然 GauGAN 在这些空间细节稀疏并且分布在图像的大面积上时可以表现得很好(就像只有一个人体图形要在相机前渲染),但当这些对象应该有许多细节限制在图像的一小部分时(就像一群各种姿势的行人),它可能会产生模糊的对象。
因此,如果你的问题有稀疏的结构细节(如单个人像或特写汽车照片)或纹理细节(如风景或微观细菌污渍),请使用 GauGAN。否则,您可能希望等待技术改进。
你需要计算资源
高根需要大量资源。我的意思是很多。批量大小可以决定你训练的成败,更大的批量需要你有大内存的 GPU。当然,这也取决于你的图像大小和问题的复杂程度。
如果您正在处理大小为 128 x 128 的细菌污渍图像,您可以轻松地在 24 GB GPU 系统中安装足够大的一批图像。但是,如果您正在处理 1024 x 768 比例的图像,请准备好支付大量资源。使用它的默认参数,您将需要大约 16 GB 的内存来容纳一个示例。
您可以尝试使用双线性上采样后期生成来增加图像的大小。或者,如果您觉得您的任务并不太难,尝试减少生成器和鉴别器中的滤波器数量,看看您是否可以在性能上稍有损失。
此外,由于批量较小,获得正确的模型可能需要较长的训练时间。众所周知,GANs 很难工作,可能需要大量的实验才能正常工作。如果您的任务与已经提供的预训练模型中的一个任务相匹配,那么您应该感到幸运,因为相同的超参数集可能会起作用。
因此,时间是你需要注意的另一个资源,特别是如果你的问题不同于预先训练的模型所提供的任务。
你需要正确的数据
如果您已经阅读了这个系列,您将会意识到我已经使用 CamVid 数据集进行了演示。我选择它有几个原因。这是一个足够小的数据集,只有 700 张图片可以训练,所以你可以很快得到结果。第二,你可能已经注意到了,结果可能相当糟糕。CamVid 数据集几乎所有的事情都会出错,这几乎让我想起了墨菲定律。
这是 CamVid 的最佳结果之一。
这是最糟糕的一个。是的,很糟糕。
因此,当您考虑为您的问题获取数据时,请确保它不像 CamVid 一样,原因如下。
数据集太小了。 GauGAN 和任何深度学习网络一样,需要大量的数据。对于像交通场景这样的复杂场景,训练集的一个好数字应该是大约 5000-10000 个示例。相比之下,我训练的 CamVid 有 630。咩。我处理过城市风景(3000 张图片)和英特尔印度驾驶数据集(7000 张图片)。训练性能随着数据量的增加而提高。
达到一个好的解决方案所需的数据量也会随着手头任务的复杂性而变化。对于细菌污渍,你只需要 2000 个例子。甚至像 Cityscapes 这样的数据集也比印度驾驶数据集简单得多,印度驾驶数据集包含更多类型的车辆、位置(如未铺设道路的小巷)和更多类型的表面(如道路旁边的泥泞表面)。
预训练模型是判断您需要多少数据的最佳选择。通过查看提供了预训练模型的数据集来评估任务的复杂性,并相应地推断任务所需的数据量。
不够多样化。cam vid 的问题是,它没有像城市风景那样的多样化图像,而是一个数据集,只包含剑桥四次行驶的后续帧。这意味着后续帧之间的差异非常小,或者它们高度相关。例如,考虑这三个连续的帧:
在最坏的情况下,车辆是静止的,你有几乎完全相同的框架。
这给了我们一个欺骗性的想法,我们有大约 640 幅图像,而事实是这些帧中有许多重复的信息,这是令人窒息的 GauGAN。
有些模式比其他模式更常见。当我说你的数据应该多样化时,我的意思是它应该涵盖你的对象可能出现的所有可能的方式。不平衡可能会导致高根在更稀疏的方向上表现不佳。例如,在数据集中,汽车大多是从后面和前面看到的,而从侧面看到汽车的情况非常少见。这导致算法试图将侧面方向的汽车绘制成前/后方向的汽车。例如:
GauGAN struggling to paint side views of a car
训练集和测试集过于相似。来自连续视频场景的随机采样帧可以让您的测试集与训练集非常相似,并给你一个看似良好的性能。例如,考虑从 CamVid 数据中随机抽样来创建您的测试集。
考虑你在上面看到的三个框架。假设第二帧被采样到我们的测试集中。由于第二帧与第一帧和第三帧非常相似,因此该算法将对其执行良好。我们的学习算法是过度拟合的,尽管在训练结果中并不明显。
在 CamVid 中,更好的衡量标准是在三种驾驶场景下进行训练,并在第四种场景下进行测试。不幸的是,这也不能很好地工作,因为我们的数据集太小了。
结论
我们的 GauGAN 系列到此结束。这个系列的目的是让你了解最新的 GAN 技术,告诉你它的问题以及如何解决它们。被与 GauGAN 相关的另一个问题所困扰?用它产生了一些很酷的结果?还是解决了我们上面提到的一个公开的问题?请随意点击评论区。在那之前,这里有一些资源可以让你进一步了解 GANS。
了解 GauGAN 系列
- 第一部分:解开 Nvidia 的风景画 GANs
- 第二部分:定制数据集培训
- 第 3 部分:模型评估技术
- 第四部分:调试培训&决定 GauGAN 是否适合你
拥抱脸的决策变形金刚
原文:https://blog.paperspace.com/decision-transformers-with-hugging-face/
决策转换器是一种新型的机器学习模型,它使转换器与强化学习相结合,开辟了研究和应用的新途径。
最近,拥抱脸在他们的博客上发布了这些,在这篇文章中,我们将展示如何开始与他们一起工作在 Paperspace Gradient 上。
你可以在高级选项中使用这个链接作为你的工作区 URL,运行一个渐变笔记本,亲自尝试一下。
什么是决策变形金刚?
决策变压器结合了变压器的能力来处理顺序数据(自然语言处理等)。),以及强化学习 (RL)递归提高环境中智能代理的能力。
RL 要解决的问题被转换为一系列步骤,然后用于训练变压器。这意味着代理不必在环境中进行学习,并且可以节省 RL 否则需要的大量计算能力。
因为代理在被训练时不在环境中,这被称为离线强化学习。
在常规 RL 中,代理学习在其环境中采取的行动,以最大化其未来回报。在离线 RL 中,转换器查看信息广播序列,并使用它来生成它应该做的未来序列。
换句话说,我们正在利用变形金刚擅长的东西——生成未来序列——来给 RL 提供我们想要的东西:一个从环境中获得回报的智能体。
因此,决策变压器代表了变压器和 RL 之间的融合,这是一个令人兴奋的前景,可以结合两者的力量。
关于决策变形金刚的更多信息,请参见拥抱脸博客条目,或者原始论文。
你如何在 Paperspace 上运行它们?
我们最近发布了我们笔记本的重大更新,现在使用 GPU 运行机器学习内容更加容易,无论是小规模的演示,还是大规模的解决实际问题。
虽然在 us 上运行的步骤数量比在 HuggingFace 演示示例中使用的要多,后者基本上只运行一个步骤,但这里的过程与您在自己的真实项目中工作的过程是一样的。
首先,进入console.paperspace.com登录 Paperspace 渐变控制台。如果您还没有帐户,您可以注册免费访问我们的免费远程 GPU,以及选择升级到更高质量的 GPU 机器可用的付费帐户。登录到 Paperspace 后,继续执行以下步骤。
1.创建项目
通过从左上方的下拉列表中进行选择,确保您处于图纸空间渐变而非核心。然后,您将看到如下屏幕:
Gradient's create a Project page
或者一个现有的项目列表,如果你在一个私人的工作空间或者团队中。
无论哪种方式,点击创建一个项目并给它命名,例如决策变形金刚。
2.创建笔记本
在您的新项目中,您将看到“笔记本”选项卡,它将引导您创建一个新的笔记本实例。然后,笔记本实例允许您管理和运行单个的.ipynb
Jupyter 笔记本。
Gradient's create a Notebook page
笔记本创建通过一系列步骤来选择运行时(PyTorch、TensorFlow 等。)和机器类型(CPU,哪个 GPU 等。)你会喜欢的,再加上其他一些细节。
对于运行时,选择 PyTorch:
Select PyTorch runtime
3.选择 GPU
从可用类型列表中选择您的 GPU。有多种类型可供选择,从 Nvidia Maxwell M4000 到 Ampere A100,以及单 GPU 或多 GPU 设置。如果您想了解更多细节,我们最近对这些进行了基准测试。
Choose GPU
对于此演示,M4000 或 P4000 实际上已经足够了。
4.指向决策转换器 GitHub 库
默认情况下,我们的 PyTorch 运行时指向我们的快速启动 PyTorch repo,所以这里我们想改为指向决策转换器。
为此,打开高级选项,在工作区下,将工作区 URL 从 https://github.com/gradient-ai/PyTorch 的更改为 https://github.com/gradient-ai/Decision-Transformers的。其他选项可以保持原样。
通过同样的方法,在创建一个笔记本作为工作目录内容时,您可以指向任何可访问的 repo。
Use decision transformer GitHub repo
5.启动笔记本
您的笔记本现在可以使用了!继续并单击开始笔记本。
这将需要几秒钟的时间来启动,然后您将看到笔记本屏幕,左侧导航栏中有一个文件列表。当任务栏左上角显示“正在运行”时,您就知道笔记本准备好了。
6.运行决策转换器
双击decision_transformers.ipynb
打开笔记本文件,然后点击右上角的运行所有执行单元格。
Part of the decision transformers notebook on Paperspace
注意——如果第一次通过视频生成失败,点击重启内核然后再次运行所有重做。
这个笔记本是 Colab 上运行的拥抱脸决策变形金刚回购的原始版本的稍微修改版本。在库设置中有一些小的变化,一些行确保产生的单元集在 Run All 下仍然工作。
他们长什么样?
如果笔记本运行正常,您应该会看到一段经过培训的代理在其环境中穿行的视频,如下所示:
https://blog.paperspace.com/content/media/2022/04/decision_transformer.mp4
Decision transformer output
你可以看到模型表现得相当好,行驶了一段距离没有摔倒。
要查看更详细的代码,请运行笔记本,或者查看拥抱脸博客条目以获得一些带解释的片段。
后续步骤
既然您已经在 Paperspace 上建立了一个有效的决策转换器,这就打开了一系列的可能性。
- 关于决策变形金刚的更多细节,参见拥抱脸博客条目。这包括更多关于决策转换器的理论,到一些代表不同运动形式的预训练模型检查点的链接,模型学习的自回归预测函数的细节,以及一些模型评估。
- 为了运行你自己的拥抱脸模型,你可以像上面一样启动一个笔记本,并指向他们的回复,例如 https://github.com/huggingface/transformers。
- 为了大规模运行,例如进行更长时间的预训练或微调运行,请从我们选择的 Pascal、Volta 和 Ampere GPUs 或多 GPU 中进行选择,作为您笔记本电脑的计算能力。
- 要在生产环境中运行,请查看 Paperspace 的工作流和部署。
决策转换器是我们现在在深度学习中看到的一些令人兴奋的融合的一部分,在这些融合中,以前独立领域的模型,如文本和计算机视觉,正在变得更加通用,并且能够跨多个领域。
甚至可能是跨文本、图像等工作的通用架构。例如感知者 IO ,将能够用于解决大多数问题。
无论发生什么,诸如拥抱 Face 的机器学习模型和 Paperspace 的端到端数据科学+ MLOps + GPU 基础设施等功能的组合将继续向更多用户开放。
决策树介绍
决策树是许多经典机器学习算法的基础,如随机森林、装袋和助推决策树。它们是由加州大学伯克利分校的统计学家利奥·布雷曼首先提出的。他的想法是将数据表示为一棵树,其中每个内部节点表示对一个属性的测试(基本上是一个条件),每个分支表示测试的结果,每个叶节点(终端节点)持有一个类标签。
决策树现在广泛应用于预测建模的许多应用中,包括分类和回归。有时决策树也被称为车,是 C 分类 a 和 R 回归 T 树的简称。让我们深入讨论决策树是如何工作的,它们是如何从零开始构建的,以及我们如何用 Python 实现它们。
在本文中,我们将介绍以下模块:
- 为什么选择决策树?
- 决策树的类型
- 关键术语
- 如何创建决策树
- 基尼杂质
- 卡方检验
- 信息增益
- 决策树的应用
- 解码超参数
- 算法编码
- 优点和缺点
- 总结和结论
为什么选择决策树?
基于树的算法是用于分类和回归的相关非参数和监督方法的流行家族。如果你想知道监督学习是什么,它是一种机器学习算法,涉及用既有输入又有输出标签的数据训练模型(换句话说,我们有已知真实类或值的数据,如果它预测不正确,可以告诉算法这些是什么)。
决策树看起来像一个模糊的颠倒的树,在根上有一个决策规则,后续的决策规则从它下面展开。例如,决策规则可以是一个人是否锻炼。也可以有没有任何决策规则的节点;这些被称为叶节点。在我们继续之前,让我们快速了解一下不同类型的决策树。
决策树的类型
根据目标变量,决策树分为两种类型。
-
分类变量决策树 s : 这是算法有分类目标变量的地方。例如,假设要求您预测一台计算机的相对价格,分为三类:低、中或高。功能可能包括显示器类型、扬声器质量、 RAM 和 SSD 。决策树将从这些特征中学习,在将每个数据点通过每个节点后,它将在三个分类目标低、中或高之一的叶节点处结束。
-
连续变量决策树 s : 在这种情况下,输入到决策树的特征(例如,房子的质量)将用于预测连续输出(例如,房子的价格)。
关键术语
让我们看看决策树是什么样子的,以及当给定一个新的预测输入时,它们是如何工作的。
下图解释了决策树的基本结构。每棵树都有一个根节点,输入在这里传递。这个根节点被进一步划分成决策节点集,其中结果和观察是有条件的。将单个节点划分为多个节点的过程称为分裂。如果一个节点没有分裂成更多的节点,那么它被称为叶节点,或者终端节点。决策树的一个子部分称为分支或子树(例如在下图的方框中)。
Example of a Decision Tree
还有一个概念与分裂完全相反。如果有可以消除的决策规则,我们就把它们从树上砍下来。这个过程被称为修剪,有助于最小化算法的复杂性。
现在我们对基本决策树的样子有了一个清晰的概念,让我们深入了解如何进行分裂,以及我们如何自己构建一个决策树。
如何创建决策树
在本节中,我们将讨论描述如何创建决策树的核心算法。这些算法完全依赖于目标变量,但是,这些算法与用于分类和回归树的算法不同。
有几种技术可以用来决定如何分割给定的数据。决策树的主要目标是在节点之间进行最佳分割,从而以最佳方式将数据划分到正确的类别中。为此,我们需要使用正确的决策规则。规则直接影响算法的性能。
在我们开始之前,需要考虑一些假设:
- 开始时,整个数据被认为是根,此后,我们使用算法进行分裂或将根分成子树。
- 特征值被认为是分类的。如果这些值是连续的,则在构建模型之前会将它们分开。
- 记录是基于属性值递归分布的。
- 属性作为树的根或内部节点的排序是使用统计方法来完成的。
让我们从常用的拆分技术开始,从而构建决策树。
基尼杂质
如果所有的元素都被正确地划分到不同的类中(一个理想的场景),那么这个划分就被认为是纯。基尼不纯度(发音像“精灵”)用于衡量随机选择的样本被某个节点错误分类的可能性。它被称为“杂质”度量,因为它让我们了解模型与纯除法有什么不同。
基尼杂质分数的程度总是在 0 到 1 之间,其中 0 表示所有元素都属于某一类(或者划分是纯的),1 表示元素随机分布在各个类中。基尼系数为 0.5 表示元素被平均分配到某些类别中。基尼系数的数学符号由以下公式表示:
其中p[I]是特定元素属于特定类的概率。
现在,让我们看看使用基尼系数作为指导来计算和构建决策树的伪代码。
Gini Index:
for each branch in a split:
Calculate percent branch represents # Used for weighting
for each class in-branch:
Calculate the probability of that class in the given branch
Square the class probability
Sum the squared class probabilities
Subtract the sum from 1 # This is the Gini Index for that branch
Weight each branch based on the baseline probability
Sum the weighted Gini index for each split
我们现在来看一个解释上述算法的简单例子。考虑下面的数据表,其中每个元素(行)有两个描述它的变量和一个相关的类标签。
| 班级 | Var 1 | Var 2 |
| A | Zero | Thirty-three |
| A | Zero | Fifty-four |
| A | Zero | fifty-six |
| A | Zero | forty-two |
| A | one | Fifty |
| B | one | Fifty-five |
| B | one | Thirty-one |
| B | Zero | -4 |
| B | one | Seventy-seven |
| B | Zero | forty-nine |
基尼指数示例:
- Var1 : Var1 的拆分基线有 4 个实例(4/10)等于 1,6 个实例(6/10)等于 0。
- 对于 Var1 == 1 & 类 == A : 1 / 4 的实例有类等于 A 。
- 对于 Var1 == 1 & 类 == B : 3 / 4 的实例有类等于 B 。
- 这里的基尼指数是 1-((1/4)^2 + (3/4)^2) = 0.375
- 对于 Var1 == 0 & 类 == A : 4 / 6 的实例有类等于 A 。
- 对于 Var1 == 0 & 类 == B : 2 / 6 实例有类等于 B 。
- 基尼指数这里是 1-((4/6)^2 + (2/6)^2) = 0.4444
- 然后,我们根据每个拆分所占数据的基线/比例,对每个拆分进行加权和求和。
- 4/10 * 0.375 + 6/10 * 0.444 = 0.41667
信息增益
信息增益描述了通过属性获得的信息量。它告诉我们属性有多重要。由于决策树的构建完全是为了找到确保高准确性的正确分裂节点,所以信息增益完全是为了找到返回最高信息增益的最佳节点。这是使用被称为熵的因子计算的。熵定义了系统的无序程度。无序越多,熵就越大。当样本是完全同质的,那么熵是零,如果样本是部分有序的,比如说 50%的样本是有序的,那么熵是一。
这作为确定信息增益的基本因素。熵和信息增益一起用来构造决策树,算法称为 ID3 。
让我们理解用于计算信息增益的一步一步的过程,从而构建决策树,
- 使用以下公式计算输出属性(分割前)的熵:
这里,p 是成功的概率,q 是节点失败的概率。比如说,10 个数据值中,5 个属于真,5 个属于假,那么 c 计算为 2, p_1 和 p_2 计算为。
- 使用公式计算所有输入属性的熵,
t 是输出属性,
x 是输入属性,
P(c)是 X 处可能出现的数据点的概率,以及
E(c)是与可能的数据点相关的熵 w . r . t '' True'。
假设一个输入属性(优先级),其中提到了两个可能的值,低和高关于低,有 5 个数据点相关,其中 2 个属于真,3 个属于假。关于高,其余 5 个数据点相关联,其中 4 个属于真,1 个属于假。那么 E(T,X)将是,
在 E(2,3)中,p 是 2,q 是 3。
在 E(4,1)中,p 是 4,q 是 1。
对给定数据集中的所有输入属性重复进行相同的计算。
- 使用上述两个值,通过从分割前的总熵中减去每个属性的熵来计算信息增益或熵的减少,
- 选择具有最高信息增益的属性作为分割节点。
- 通过根据分割来分割数据集,重复步骤 1-4。该算法一直运行,直到所有数据都被分类。
要点记住:
- 叶节点是没有熵的节点,或者熵为零的节点。在叶节点上不再进行进一步的分割。
- 只有需要进一步分裂的分支,即熵> 0 时(有杂质时)才需要经历这个分裂过程。
c. 卡方
如果目标变量是分类的,如成功-失败/高-低,卡方方法很有效。该算法的核心思想是找出子节点和父节点之间存在的差异的统计显著性。用于计算卡方的数学方程是,
它代表目标变量的观察频率和预期频率之间的标准化差异的平方和。
使用卡方的另一个主要优势是,它可以在单个节点上执行多次分割,从而提高准确度和精度。
决策树的应用
决策树是机器学习领域中最基本也是最广泛使用的算法之一。它在分类和回归建模的不同领域得到了应用。由于其描绘可视化输出的能力,人们可以很容易地从建模过程流中获得洞察力。这里有几个可以使用决策树的例子,
- 企业管理
- 客户关系管理
- 欺诈性声明检测
- 能耗
- 医疗保健管理
- 故障诊断
解码超参数
Scikit-learn 提供了一些与决策树分类器一起使用的功能或参数,以根据给定的数据提高模型的准确性。
-
判据: 该参数用来衡量分割的质量。该参数的默认值设置为“Gini”。如果你想用熵增益来计算测度,可以把这个参数改成“熵”。
-
splitter :该参数用于选择在每个节点的拆分。如果希望子树具有最佳分割,可以将该参数设置为“最佳”。我们还可以有一个随机分割,其值设置为“随机”。
-
max-depth:这是一个整数参数,通过它我们可以限制树的深度。该参数的默认值设置为 None。
-
min _ samples _ split:该参数用于定义拆分一个内部节点所需的最小样本数。
-
max _ leaf _ nodes:max _ leaf _ nodes 的默认值设置为无。该参数用于以最佳优先的方式生长具有 max_leaf_nodes 的树。
算法编码
步骤 1:导入模块
构建决策树模型的第一步也是最重要的一步是导入必要的包和模块。我们从 sklearn 包中导入决策树分类器类。这是一个内置的类,其中编码了整个决策树算法。在这个程序中,我们将使用可以从 sklearn.datasets 导入的虹膜数据集。pydotplus 包用于可视化决策树。下面是代码片段,
import pydotplus
from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets
步骤 2:探索数据
接下来,我们通过使用 load_iris() 方法从 datasets 包中加载数据来准备好数据。我们将数据分配给虹膜变量。这个 iris 变量有两个键,一个是数据键,其中显示所有输入,即萼片长度、萼片宽度、花瓣长度和花瓣宽度。在 target 键中,我们有花朵类型,其值为鸢尾、杂色鸢尾和海滨鸢尾。我们将这些分别加载到特征和目标变量中。
iris = datasets.load_iris()
features = iris.data
target = iris.target
print(features)
print(target)
Output:
[[5.1 3.5 1.4 0.2]
[4.9 3\. 1.4 0.2]
[4.7 3.2 1.3 0.2]
[4.6 3.1 1.5 0.2]
[5.8 4\. 1.2 0.2]
[5.7 4.4 1.5 0.4]
. . . .
. . . .
]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
这是我们的数据集的样子。
步骤 3:创建决策树分类器对象
这里,我们将决策树分类器加载到一个名为模型的变量中,这个变量是之前从 sklearn 包中导入的。
decisiontree = DecisionTreeClassifier(random_state=0)
步骤 5:拟合模型
这是训练过程的核心部分,通过对给定数据进行分割来构建决策树。我们用作为参数发送给 fit() 方法的特征和目标值来训练算法。这种方法是通过在特征和目标上训练模型来拟合数据。
model = decisiontree.fit(features, target)
第六步:做预测
在这一步中,我们进行样本观察并做出预测。我们创建了一个新的列表,包括花的萼片和花瓣的尺寸。此外,我们在经过训练的模型上使用 predict() 方法来检查它所属的类别。我们还可以通过使用 predict_proba 方法来检查预测的概率(类概率)。
observation = [[ 5, 4, 3, 2]] # Predict observation's class
model.predict(observation)
model.predict_proba(observation)
Output:
array([1])
array([[0., 1., 0.]])
步骤 7:用于预测的点数据
在这一步中,我们以点格式(一种图形描述语言)导出训练好的模型。为了实现这一点,我们使用了可以从 sklearn 包中导入的树类。在此基础上,我们使用 export_graphviz 方法,将决策树、特征和目标变量作为参数。
from sklearn import tree
dot_data = tree.export_graphviz(decisiontree, out_file=None,
feature_names=iris.feature_names,
class_names=iris.target_names
)
第八步:绘制图形
在最后一步,我们使用从 IPython.display 包导入的 Image 类来可视化决策树。
from IPython.display import Image
graph = pydotplus.graph_from_dot_data(dot_data) # Show graph
Image(graph.create_png())
Resultant Decision Tree
优点和缺点
决策树有一些优点和缺点。先说优点。与其他算法相比,决策树在处理数据时花费的时间非常少。可以跳过一些预处理步骤,如数据的标准化、转换和缩放。尽管数据集中存在缺失值,但模型的性能不会受到影响。决策树模型直观,易于向技术团队和利益相关者解释,并且可以跨多个组织实现。
缺点来了。在决策树中,数据的微小变化会导致决策树结构的巨大变化,从而导致不稳定。训练时间急剧增加,与数据集的大小成比例。在某些情况下,与其他传统算法相比,计算可能会变得复杂。
总结和结论
在本文中,我们已经深入讨论了决策树算法。这是一种监督学习算法,可用于分类和回归。决策树的主要目标是根据一组规则和条件将数据集分割成一棵树。我们讨论了决策树的关键组成部分,如根节点、叶节点、子树、分裂和修剪。此外,我们已经看到了决策树是如何工作的,以及如何使用流行的算法如 GINI、信息增益和卡方来执行战略分割。此外,我们使用 scikit-learn 在 IRIS 数据集上从头开始编码决策树。最后,我们讨论了使用决策树的优点和缺点。还有很多东西需要学习,本文将为您提供探索其他高级分类算法的快速入门。
参考
评估深度学习模型:混淆矩阵、准确度、精确度和回忆
原文:https://blog.paperspace.com/deep-learning-metrics-precision-recall-accuracy/
在计算机视觉中,目标检测是在图像中定位一个或多个目标的问题。除了传统的对象检测技术,像 R-CNN 和 YOLO 这样的高级深度学习模型可以对不同类型的对象进行令人印象深刻的检测。这些模型接受图像作为输入,并返回每个检测到的对象周围的边界框的坐标。
本教程讨论混淆矩阵,以及如何计算精度,召回和准确性。在另一个教程中,将讨论地图。
具体来说,我们将涵盖:
- 二元分类的混淆矩阵
- 多类分类的混淆矩阵
- 用 Scikit-learn 计算混淆矩阵
- 准确度、精确度和召回率
- 精准还是召回?
- 结论
二元分类的混淆矩阵
在二元分类中,每个输入样本被分配到两个类别之一。通常这两个类被赋予类似于 1 和 0 或正 和负的标签。更具体地说,这两个类标签可能是类似于恶性 或良性(例如,如果问题是关于癌症分类),或者成功或失败(例如,如果是关于对学生考试成绩进行分类)。
假设有一个二元分类问题,类别为正和为负。以下是用于训练模型的七个样本的标签示例。这些被称为样本的地面真相标签。
positive, negative, negative, positive, positive, positive, negative
请注意,类别标签用于帮助我们人类区分不同的类别。对模型非常重要的是一个数字分数。当向模型输入单个样本时,模型不一定返回类别标签,而是返回分数。例如,当这七个样本被输入到模型中时,它们的类得分可能是:
0.6, 0.2, 0.55, 0.9, 0.4, 0.8, 0.5
根据分数,每个样本被赋予一个类别标签。我们如何将这些分数转换成标签?我们通过使用阈值来做到这一点。该阈值是模型的超参数,可以由用户定义。例如,阈值可以是 0.5,那么任何大于或等于 0.5 的样本都被赋予正标签。否则,就是负。以下是样品的预测标签:
positive (0.6), negative (0.2), positive (0.55), positive (0.9), negative (0.4), positive (0.8), positive (0.5)
为了比较,这里既有基本事实也有预测标签。乍一看,我们可以看到 4 个正确的预测和 3 个错误的预测。请注意,更改阈值可能会产生不同的结果。例如,将阈值设置为 0.6 只会留下两个不正确的预测。
Ground-Truth: positive, negative, negative, positive, positive, positive, negative
Predicted : positive, negative, positive, positive, negative, positive, positive
为了提取关于模型性能的更多信息,使用混淆矩阵。混淆矩阵有助于我们直观地看到模型在区分两个类别时是否“混淆”。如下图所示,这是一个 2×2 矩阵。两行和列的标签为正和负以反映两个类别标签。在此示例中,行标签表示实际标签,而列标签表示预测标签。这是可以改变的。
矩阵的 4 个元素(红色和绿色的项目)代表 4 个度量标准,用于计算模型做出的正确和错误预测的数量。每个元素都有一个由两个单词组成的标签:
- 真或假
- 正或负
当预测是正确的(即,在预测和基本事实标签之间存在匹配)时为真,当预测和基本事实标签之间存在不匹配时为假。正或负指预测标签。
综上,每当预测错了,第一个字就是假。否则,就是真。目标是最大化带有单词真 ( 真正和真负)的度量,最小化其他两个度量(假正和假负)。因此,混淆矩阵中的四个度量是:
- 左上(真阳性):模型正确将一个阳性样本归类为阳性的次数?
- 右上(假阴性):模型有多少次错误地将一个阳性样本归类为阴性?
- 左下(假阳性):模型有多少次错误地将一个阴性样本归类为阳性?
- 右下(真阴性):模型正确将一个阴性样本归类为阴性的次数是多少?
我们可以为之前看到的七个预测计算这四个指标。下图给出了由此产生的混淆矩阵。
这就是如何计算二进制分类问题的混淆矩阵。现在让我们看看如何计算一个多类问题。
多类分类的混淆矩阵
如果我们有两节以上的课呢?对于多类分类问题,我们如何计算混淆矩阵中的这四个度量?简单!
假设有 9 个样本,其中每个样本属于三个类别之一:白色、黑色或红色。这是 9 个样本的基本数据。
Red, Black, Red, White, White, Red, Black, Red, White
当样本被输入模型时,这里是预测的标签。
Red, White, Black, White, Red, Red, Black, White, Red
为了便于比较,这里它们是并排的。
Ground-Truth: Red, Black, Red, White, White, Red, Black, Red, White
Predicted: Red, White, Black, White, Red, Red, Black, White, Red
在计算混淆矩阵之前,必须指定一个目标类。我们把红类设为目标。该班标记为正,其他班标记为负。
Positive, Negative, Positive, Negative, Negative, Positive, Negative, Positive, Negative
Positive, Negative, Negative, Negative, Positive, Positive, Negative, Negative, Positive
现在又只有两类(正和负)。因此,混淆矩阵可以像上一节那样计算。注意,这个矩阵只是针对红类。
对于白色类,将其每次出现替换为正,所有其他类标签替换为负。替换后,这里是地面真相和预测标签。下图显示了白类的混淆矩阵。
Negative, Negative, Negative, Positive, Positive, Negative, Negative, Negative, Positive
Negative, Positive, Negative, Positive, Negative, Negative, Negative, Positive, Negative
同样,这里是黑类的混淆矩阵。
用 Scikit-Learn 计算混淆矩阵
Python 中流行的 Scikit-learn 库有一个名为metrics
的模块,可以用来计算混淆矩阵中的度量。
对于二元类问题,使用confusion_matrix()
函数。在其可接受的参数中,我们使用以下两个:
- 真相标签。
y_pred
:预测标签。
下面的代码为我们之前讨论的二进制分类示例计算混淆矩阵。
import sklearn.metrics
y_true = ["positive", "negative", "negative", "positive", "positive", "positive", "negative"]
y_pred = ["positive", "negative", "positive", "positive", "negative", "positive", "positive"]
r = sklearn.metrics.confusion_matrix(y_true, y_pred)
print(r)
array([[1, 2],
[1, 3]], dtype=int64)
请注意,度量的顺序与之前讨论的不同。例如,真正指标在右下角,而真负在左上角。为了解决这个问题,我们可以翻转矩阵。
import numpy
r = numpy.flip(r)
print(r)
array([[3, 1],
[2, 1]], dtype=int64)
为了计算多类分类问题的混淆矩阵,使用了multilabel_confusion_matrix()
函数,如下所示。除了y_true
和y_pred
参数,第三个名为labels
的参数接受一个类标签列表。
import sklearn.metrics
import numpy
y_true = ["Red", "Black", "Red", "White", "White", "Red", "Black", "Red", "White"]
y_pred = ["Red", "White", "Black", "White", "Red", "Red", "Black", "White", "Red"]
r = sklearn.metrics.multilabel_confusion_matrix(y_true, y_pred, labels=["White", "Black", "Red"])
print(r)
array([
[[4 2]
[2 1]]
[[6 1]
[1 1]]
[[3 2]
[2 2]]], dtype=int64)
该函数计算每个类的混淆矩阵,并返回所有矩阵。矩阵的顺序与labels
参数中标签的顺序相匹配。为了调整矩阵中指标的顺序,我们将像以前一样使用numpy.flip()
函数。
print(numpy.flip(r[0])) # White class confusion matrix
print(numpy.flip(r[1])) # Black class confusion matrix
print(numpy.flip(r[2])) # Red class confusion matrix
# White class confusion matrix
[[1 2]
[2 4]]
# Black class confusion matrix
[[1 1]
[1 6]]
# Red class confusion matrix
[[2 2]
[2 3]]
在本教程的剩余部分,我们将只关注两个类。下一节讨论基于混淆矩阵计算的三个关键指标。
准确度、精确度和召回率
正如我们已经看到的,混淆矩阵提供了四个不同的单独指标。基于这四个指标,可以计算其他指标,这些指标提供了有关模型行为的更多信息:
- 准确(性)
- 精确
- 回忆
接下来的小节将分别讨论这三个指标。
精度
准确性是一个度量标准,通常描述模型在所有类别中的表现。当所有类都同等重要时,这是很有用的。它的计算方法是正确预测数与预测总数之比。
以下是如何根据之前计算的混淆矩阵,使用 Scikit-learn 计算准确度。变量acc
保存将真阳性和真阴性之和除以矩阵中所有值之和的结果。结果是0.5714
,这意味着该模型在做出正确预测时是57.14%
准确的。
import numpy
import sklearn.metrics
y_true = ["positive", "negative", "negative", "positive", "positive", "positive", "negative"]
y_pred = ["positive", "negative", "positive", "positive", "negative", "positive", "positive"]
r = sklearn.metrics.confusion_matrix(y_true, y_pred)
r = numpy.flip(r)
acc = (r[0][0] + r[-1][-1]) / numpy.sum(r)
print(acc)
0.571
sklearn.metrics
模块有一个叫accuracy_score()
的函数,也可以计算精度。它接受基本事实和预测标签作为参数。
acc = sklearn.metrics.accuracy_score(y_true, y_pred)
请注意,准确性可能是欺骗性的。一种情况是当数据不平衡时。假设总共有 600 个样本,其中 550 个属于阳性类,只有 50 个属于阴性类。由于大多数样本属于一个类别,该类别的准确度将高于另一个类别。
如果该模型对阳性类做出了总共 530/550 的正确预测,相比之下,对阴性类的正确预测仅为 5/50,那么总准确度为(530 + 5) / 600 = 0.8917
。这意味着该模型的准确率为 89.17%。考虑到这一点,您可能会认为,对于任何样本(无论其类别如何),该模型都有可能在 89.17%的情况下做出正确的预测。这是无效的,尤其是当你考虑到模型表现不佳的负类时。
精度
精度计算为正确分类的阳性样本数与分类为阳性的样本总数之比(正确或不正确)。精度衡量模型将样本分类为阳性的准确性。
当模型做出许多不正确的正分类,或很少正确的正分类时,这增加了分母并使精度变小。另一方面,在以下情况下精度较高:
- 模型做出许多正确的正分类(最大化真正)。
- 该模型做出更少的错误阳性分类(最小化假阳性)。
想象一个被别人信任的男人;当他预言某事时,其他人相信他。精密就像这个人。当精度较高时,当模型预测样本为正时,可以信任模型。因此,精度有助于了解模型在表示样本为阳性时的准确性。
基于前面的讨论,下面是精度的定义:
精度反映了模型将样本分类为阳性的可靠程度。
在下图中,绿色标记表示样本被归类为阳性,红色标记表示样本为阴性。该模型将两个阳性样本正确分类,但将一个阴性样本错误分类为阳性。因此,真阳性率为 2,假阳性率为 1,精度为2/(2+1)=0.667
。换句话说,当模型说样本为阳性时,其可信度为 66.7%。
精度的目标是将所有阳性样本归类为阳性,而不会将一个阴性样本误归类为阳性。根据下图,如果所有三个阳性样本分类正确,但一个阴性样本分类错误,则精度为3/(3+1)=0.75
。因此,当模型说一个样本是阳性时,它是 75%准确的。
获得 100%精度的唯一方法是将所有的阳性样本归类为阳性,此外不要将一个阴性样本误归类为阳性。
在 Scikit-learn 中,sklearn.metrics
模块有一个名为precision_score()
的函数,它接受实际和预测标签并返回精度。pos_label
参数接受正类的标签。默认为1
。
import sklearn.metrics
y_true = ["positive", "positive", "positive", "negative", "negative", "negative"]
y_pred = ["positive", "positive", "negative", "positive", "negative", "negative"]
precision = sklearn.metrics.precision_score(y_true, y_pred, pos_label="positive")
print(precision)
0.6666666666666666
回忆
召回的计算方法是被正确归类为阳性的阳性样本数与阳性样本总数之比。召回测量模型检测阳性样本的能力。召回率越高,检测到的阳性样本越多。
召回只关心阳性样本如何分类。这与阴性样本的分类方式无关,例如精度。当模型将所有阳性样本分类为阳性时,那么即使所有阴性样本被错误地分类为阳性,召回率也将是 100%。让我们看一些例子。
在下图中,有 4 种不同的情况(A 到 D ),并且都有相同的回忆,即0.667
。每种情况的不同之处仅在于如何对阴性样本进行分类。例如,案例 A 将所有阴性样本正确分类为阴性,但案例 D 将所有阴性样本错误分类为阳性。不管阴性样本如何分类,召回只关心阳性样本。
在上面显示的 4 个病例中,只有 2 个阳性样本被正确分类为阳性。因此,真正的率是 2。假阴性率为 1,因为只有一个阳性样本被归类为阴性。结果召回的是2/(2+1)=2/3=0.667
。
因为阴性样本被归类为阳性还是阴性并不重要,所以最好完全忽略阴性样本,如下图所示。计算召回率时只需要考虑正样本。
召回高或低意味着什么?当召回率较高时,意味着模型可以将所有阳性样本正确分类为阳性。因此,该模型检测阳性样本的能力是可信的。
在下图中,召回率为 1.0,因为所有阳性样本都被正确归类为阳性。真阳性率为 3,假阴性率为 0。因此,召回等于3/(3+0)=1
。这意味着模型检测到了所有阳性样本。因为召回忽略了如何对阴性样本进行分类,仍可能有许多阴性样本被分类为阳性(即高假阳性率)。召回没有考虑到这一点。
另一方面,当未能检测到任何阳性样本时,召回率为 0.0。在下图中,所有阳性样本都被错误地归类为阴性。这意味着模型检测到 0%的阳性样本。真阳性率为 0,假阴性率为 3。因此,召回等于0/(0+3)=0
。
当召回值介于 0.0 和 1.0 之间时,该值反映了模型正确分类为阳性的阳性样本的百分比。例如,如果有 10 个阳性样本,召回率为 0.6,这意味着模型正确分类了 60%的阳性样本(即0.6*10=6
阳性样本被正确分类)。
类似于precision_score()
函数,sklearn.metrics
模块中的recall_score()
函数计算召回。下一个代码块显示了一个示例。
import sklearn.metrics
y_true = ["positive", "positive", "positive", "negative", "negative", "negative"]
y_pred = ["positive", "positive", "negative", "positive", "negative", "negative"]
recall = sklearn.metrics.recall_score(y_true, y_pred, pos_label="positive")
print(recall)
0.6666666666666666
定义了精确度和召回率之后,让我们快速回顾一下:
- 精度衡量模型在对正样本进行分类时的可信度,召回衡量模型正确分类了多少正样本。
- 精度考虑了阳性和阴性样本的分类方式,但召回在其计算中仅考虑阳性样本。换句话说,精确度依赖于负样本和正样本,但是召回率仅依赖于正样本(并且独立于负样本)。
- 精度考虑样本何时被分类为阳性,但不关心正确分类所有阳性样本。召回关心的是正确分类所有阳性样本,但不关心一个阴性样本是否被分类为阳性。
- 当一个模型具有高召回率但低精确度时,那么该模型正确地分类了大多数阳性样本,但是它具有许多假阳性(即,将许多阴性样本分类为阳性)。当一个模型具有高精度但低召回率时,那么该模型在将一个样本分类为阳性时是准确的,但它只能对少数阳性样本进行分类。
以下是一些测试你理解能力的问题:
- 如果召回率为 1.0,数据集有 5 个阳性样本,那么模型正确分类了多少个阳性样本?(
5
) - 假设当数据集有 30 个阳性样本时,召回率为 0.3,那么模型正确分类了多少个阳性样本?(
0.3*30=9
样本) - 如果召回率为 0.0,数据集有 14 个阳性样本,那么模型正确分类了多少个阳性样本?(
0
)
精确还是回忆?
使用精确还是召回取决于所解决问题的类型。如果目标是检测所有阳性样本(不关心阴性样本是否会被误分类为阳性),那么使用 recall。如果问题对将样本归类为一般的阳性敏感,即包括被错误归类为阳性的阴性样本,则使用 precision。
想象一下,给你一张图像,要求你检测图像中的所有汽车。您使用哪种度量标准?因为目标是检测所有汽车,所以使用 recall。这可能会将一些对象误分类为汽车,但它最终会检测到所有的目标对象。
现在假设给你一张乳房 x 光照片,要求你检测是否有癌症。您使用哪种度量标准?因为它对错误地将图像识别为癌症非常敏感,所以我们必须确定何时将图像分类为阳性(即患有癌症)。因此,精度是首选指标。
结论
本教程讨论了混淆矩阵以及如何在二进制和多类分类问题中计算其 4 个度量(真/假阳性/阴性)。使用 Scikit-learn 中的metrics
模块,我们看到了如何在 Python 中计算混淆矩阵。
基于这 4 个指标,我们开始讨论准确度、精确度和召回率。每个指标都是基于几个例子定义的。sklearn.metrics
模块用于计算它们中的每一个。
基于这里给出的概念,在下一个教程中,我们将看到如何使用精确度-召回曲线、平均精确度和平均精确度(mAP)。
降噪自动编码器
在之前的文章中,我提到过自动编码器可能不是生成任务的首选。话虽如此,但他们都有自己独特的优势。在这篇文章中,我们将看看这些优势之一,图像去噪。
设置和导入
# article dependencies
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
from tqdm import tqdm as tqdm_regular
import seaborn as sns
from torchvision.utils import make_grid
import random
# configuring device
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
自动编码器和表示学习
到目前为止,我们知道自动编码器学习从输入到输出的映射,唯一的目的是重建输入数据。然而,从表面上看,这真的没有多大用处。
例如,在卷积自动编码器的情况下,这些自动编码器学习表示以重建图像,您会同意简单地将图像通过卷积自动编码器只是为了在另一端获得相同图像的重建并不太有益。
超越图像重建
与我们关注图像数据的主题保持一致,考虑这样一种情况,我们有一堆损坏的图像,这些图像在某种意义上是损坏的,即一些/所有像素已经以某种不期望的方式被修改。如果可以再现这种特定形式的图像损坏,使得从一组未损坏的图像中生成损坏图像的数据集,则可以训练卷积自动编码器来学习从损坏图像到未损坏图像的映射,从而有效地学习消除这种特定形式的损坏的图像。
上述上下文中的图像恶化被称为 噪声、,并且从图像中去除所述恶化的过程被称为 图像去噪 ,而用于此效果的自动编码器被称为 去噪自动编码器 。
实现去噪自动编码器
在本节中,我们将准备一个数据集,通过向图像添加一些噪声来训练去噪自动编码器,并训练卷积自动编码器来消除特定类型的图像噪声。
资料组
CIFAR-10 数据集将用于训练和验证目的。这是一个数据集,包含 10 类图像,从青蛙到汽车、鸟类等等。可以在 PyTorch 中加载它,就像下面的代码单元格中所做的那样。
# loading training data
training_set = Datasets.CIFAR10(root='./', download=True,
transform=transforms.ToTensor())
# loading validation data
validation_set = Datasets.CIFAR10(root='./', download=True, train=False,
transform=transforms.ToTensor())
CIFAR-10 images.
由于我们将学习图像到图像的映射,在这种情况下,我们不需要类标签,所有需要做的就是从它们各自的对象中提取训练和验证图像。此外,为了直观起见,我们将从验证集中的每个类别中提取一幅图像,以便我们可以看到在训练时,自动编码器在每个时期后对该类别的图像进行去噪的效果如何,我们将此称为测试集。
def extract_each_class(dataset):
"""
This function searches for and returns
one image per class
"""
images = []
ITERATE = True
i = 0
j = 0
while ITERATE:
for label in tqdm_regular(dataset.targets):
if label==j:
images.append(dataset.data[i])
print(f'class {j} found')
i+=1
j+=1
if j==10:
ITERATE = False
else:
i+=1
return images
# extracting training images
training_images = [x for x in training_set.data]
# extracting validation images
validation_images = [x for x in validation_set.data]
# extracting one image from each class in the validation set
test_images = extract_each_class(validation_set)
图像转换为灰度
虽然有不同类型的图像噪声,但在本文中,我们将重点讨论“椒盐噪声”,这是一种普遍存在于灰度图像中的噪声。正如我们所知,CIFAR-10 图像是彩色的,为了方便地将它们转换为灰度,我们可以简单地取各个通道的单个像素的平均值,这样我们就可以从 3 通道图像(彩色)转换为单通道图像(灰度)。
# converting images to grayscale by taking mean across axis-2 (depth)
training_gray = [x.mean(axis=2) for x in training_images]
validation_gray = [x.mean(axis=2) for x in validation_images]
test_gray = [x.mean(axis=2) for x in test_images]
为了稍微清理一下像素值,让我们通过将像素限制在 0 和 1 之间的值来归一化像素,这对于大多数灰度图像来说是典型的。
def min_max_normalize_gray(dataset: list):
"""
This function normalizes data by constraining
data points between the range of 0 & 1
"""
# create a list to hold normalized data
normalized = []
for image in tqdm_regular(dataset):
# creating temporary store
temp = []
# flatenning
pixels = image.flatten()
# derive minimum and maximum values
minimum = pixels.min()
maximum = pixels.max()
# convert to list for iteration
pixels = list(pixels)
for pixel in pixels:
# normalizing pixels
normalize = (pixel-minimum)/(maximum-minimum)
# appending each pixel to temporary store
temp.append(round(normalize, 2))
temp = np.array(temp)
temp = temp.reshape((32, 32))
# appending normalized image to list
normalized.append(temp)
return normalized
# normalizing pixels
training_gray = min_max_normalize_gray(training_gray)
validation_gray = min_max_normalize_gray(validation_gray)
test_gray = min_max_normalize_gray(test_gray)
创建嘈杂的副本
椒盐噪声可以被认为是散布在图像表面的白色(盐)和黑色(胡椒)像素点。从概念上来说,这仅仅意味着一些像素被随机转换成 0(黑色)和 1(白色)。有了这些知识,我们可以使用下面的代码单元再现椒盐噪声。
def random_noise(dataset: list, noise_intensity=0.2):
"""
This function replicates the salt and pepper noise process
"""
noised = []
noise_threshold = 1 - noise_intensity
for image in tqdm_regular(dataset):
# flatenning image
image = image.reshape(1024)
# creating vector of zeros
noise_vector = np.zeros(1024)
# noise probability
for idx in range(1024):
regulator = round(random.random(), 1)
if regulator > noise_threshold:
noise_vector[idx] = 1
elif regulator == noise_threshold:
noise_vector[idx] = 0
else:
noise_vector[idx] = image[idx]
# reshaping noise vectors
noise_vector = noise_vector.reshape((32, 32))
noised.append(noise_vector)
return noised
# adding noise to images
training_noised = random_noise(training_gray)
validation_noised = random_noise(validation_gray)
test_noised = random_noise(test_gray)
可视化上面定义的过程中的噪声图像显示了模拟盐和胡椒噪声的白色和黑色斑点的存在。由于 CIFAR-10 数据集中的图像大小为 32 x 32 像素,请原谅严重的像素化。
Uncorrupted vs corrupted image.
现在可以通过压缩损坏的和未损坏的图像来将训练集、验证集和测试集放在一起,以形成图像-目标对,如下面的代码单元格中所做的那样。
# creating image-target pair
training_set = list(zip(training_noised, training_gray))
validation_set = list(zip(validation_noised, validation_gray))
test_set = list(zip(test_noised, test_gray))
PyTorch 数据集
为了在 PyTorch 中使用我们的数据集,我们需要将它实例化为 PyTorch 数据集类的成员,如下所示。注意,图像中的像素再次在平均值 0.5 和标准偏差 0.5 附近被归一化,以试图将所有像素置于可管理的近似分布内。
# defining dataset class
class CustomCIFAR10(Dataset):
def __init__(self, data, transforms=None):
self.data = data
self.transforms = transforms
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
image = self.data[idx][0]
target = self.data[idx][1]
if self.transforms!=None:
image = self.transforms(image)
target = self.transforms(target)
return (image, target)
# creating pytorch datasets
training_data = CustomCIFAR10(training_set, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize(0.5, 0.5)]))
validation_data = CustomCIFAR10(validation_set, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize(0.5, 0.5)]))
test_data = CustomCIFAR10(test_set, transforms=transforms.Compose([transforms.ToTensor(),
transforms.Normalize(0.5, 0.5)]))
拼接卷积自动编码器
现在需要定义卷积自动编码器。对于本文,我们将实现下图所示的定制自动编码器架构。
Convolutional autoencoder architecture to be used for the sake of image denoising.
该自动编码器由一个编码器和一个解码器组成,每个解码器有 6 个卷积层。还指定了大小为 1000 的瓶颈/潜在空间。该架构是在 PyTorch 中实现的,如下面的代码单元所示。
# defining encoder
class Encoder(nn.Module):
def __init__(self, in_channels=3, out_channels=16, latent_dim=1000, act_fn=nn.ReLU()):
super().__init__()
self.in_channels = in_channels
self.net = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1), # (32, 32)
act_fn,
nn.Conv2d(out_channels, out_channels, 3, padding=1),
act_fn,
nn.Conv2d(out_channels, 2*out_channels, 3, padding=1, stride=2), # (16, 16)
act_fn,
nn.Conv2d(2*out_channels, 2*out_channels, 3, padding=1),
act_fn,
nn.Conv2d(2*out_channels, 4*out_channels, 3, padding=1, stride=2), # (8, 8)
act_fn,
nn.Conv2d(4*out_channels, 4*out_channels, 3, padding=1),
act_fn,
nn.Flatten(),
nn.Linear(4*out_channels*8*8, latent_dim),
act_fn
)
def forward(self, x):
x = x.view(-1, self.in_channels, 32, 32)
output = self.net(x)
return output
# defining decoder
class Decoder(nn.Module):
def __init__(self, in_channels=3, out_channels=16, latent_dim=1000, act_fn=nn.ReLU()):
super().__init__()
self.out_channels = out_channels
self.linear = nn.Sequential(
nn.Linear(latent_dim, 4*out_channels*8*8),
act_fn
)
self.conv = nn.Sequential(
nn.ConvTranspose2d(4*out_channels, 4*out_channels, 3, padding=1), # (8, 8)
act_fn,
nn.ConvTranspose2d(4*out_channels, 2*out_channels, 3, padding=1,
stride=2, output_padding=1), # (16, 16)
act_fn,
nn.ConvTranspose2d(2*out_channels, 2*out_channels, 3, padding=1),
act_fn,
nn.ConvTranspose2d(2*out_channels, out_channels, 3, padding=1,
stride=2, output_padding=1), # (32, 32)
act_fn,
nn.ConvTranspose2d(out_channels, out_channels, 3, padding=1),
act_fn,
nn.ConvTranspose2d(out_channels, in_channels, 3, padding=1)
)
def forward(self, x):
output = self.linear(x)
output = output.view(-1, 4*self.out_channels, 8, 8)
output = self.conv(output)
return output
# defining autoencoder
class Autoencoder(nn.Module):
def __init__(self, encoder, decoder):
super().__init__()
self.encoder = encoder
self.encoder.to(device)
self.decoder = decoder
self.decoder.to(device)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
卷积自动编码器类
一个典型的自动编码器执行 3 个主要功能,它通过它的编码器学习一个矢量表示,在图像被它的解码器重建之前,这个表示在它的瓶颈中被压缩。为了能够在需要时单独使用自动编码器的这些独立组件,我们将定义一个类,通过将其中两个函数定义为方法来帮助实现这一点。为了可移植性,一个训练方法也将被构建到这个类中。
# defining class
class ConvolutionalAutoencoder():
def __init__(self, autoencoder):
self.network = autoencoder
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set, test_set,
image_channels=3):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'visualizations': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
elif isinstance(module, nn.Linear):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
test_loader = DataLoader(test_set, 10)
# setting convnet to training mode
self.network.train()
self.network.to(device)
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
#------------
# TRAINING
#------------
print('training...')
for images, targets in tqdm(train_loader):
# zeroing gradients
self.optimizer.zero_grad()
# sending images and targets to device
images = images.to(device).type(torch.cuda.FloatTensor)
targets = targets.to(device).type(torch.cuda.FloatTensor)
# reconstructing images
output = self.network(images)
# computing loss
loss = loss_function(output, targets)
loss = loss#.type(torch.cuda.FloatTensor)
# calculating gradients
loss.backward()
# optimizing weights
self.optimizer.step()
#--------------
# LOGGING
#--------------
log_dict['training_loss_per_batch'].append(loss.item())
#--------------
# VALIDATION
#--------------
print('validating...')
for val_images, val_targets in tqdm(val_loader):
with torch.no_grad():
# sending validation images and targets to device
val_images = val_images.to(device).type(torch.cuda.FloatTensor)
val_targets = val_targets.to(device).type(torch.cuda.FloatTensor)
# reconstructing images
output = self.network(val_images)
# computing validation loss
val_loss = loss_function(output, val_targets)
#--------------
# LOGGING
#--------------
log_dict['validation_loss_per_batch'].append(val_loss.item())
#--------------
# VISUALISATION
#--------------
print(f'training_loss: {round(loss.item(), 4)} validation_loss: {round(val_loss.item(), 4)}')
for test_images, test_targets in test_loader:
# sending test images to device
test_images = test_images.to(device).type(torch.cuda.FloatTensor)
with torch.no_grad():
# reconstructing test images
reconstructed_imgs = self.network(test_images)
# sending reconstructed and images to cpu to allow for visualization
reconstructed_imgs = reconstructed_imgs.cpu()
test_images = test_images.cpu()
# visualisation
imgs = torch.stack([test_images.view(-1, image_channels, 32, 32), reconstructed_imgs],
dim=1).flatten(0,1)
grid = make_grid(imgs, nrow=10, normalize=True, padding=1)
grid = grid.permute(1, 2, 0)
plt.figure(dpi=170)
plt.title('Original/Reconstructed')
plt.imshow(grid)
log_dict['visualizations'].append(grid)
plt.axis('off')
plt.show()
return log_dict
def autoencode(self, x):
return self.network(x)
def encode(self, x):
encoder = self.network.encoder
return encoder(x)
def decode(self, x):
decoder = self.network.decoder
return decoder(x)
训练去噪自动编码器
现在一个去噪自动编码器准备好被训练。训练是通过将 autoencoder 类实例化为卷积 autoencoder 类的成员并调用 train 方法来完成的。均方误差被用作选择的损失函数,因为模型使用 64 的批量大小被训练 15 个时期。
# training model
model = ConvolutionalAutoencoder(Autoencoder(Encoder(in_channels=1),
Decoder(in_channels=1)))
log_dict = model.train(nn.MSELoss(), epochs=15, batch_size=64,
training_set=training_data, validation_set=validation_data,
test_set=test_data, image_channels=1)
在第一个时期之后,很明显,自动编码器已经在从图像中去除噪声/破坏方面做得不错,如在每个时期之后返回的可视化中所看到的。它的重建是非常低的细节(模糊)。
Epoch 1.
针对更多时段的训练确保了产生更精细的重建,并且到第 15 时段,与时段 1 相比,在去噪图像的质量上可以看到明显的提升。必须记住的是,可视化中去噪的图像是测试图像,自动编码器并没有在这些图像上进行训练,这证明了它的通用性。
Epoch 15.
看一下训练和验证损失图,很明显两个损失都是下降趋势,因此意味着自动编码器仍然可以从一些额外的训练时期中受益。
Training and validation losses.
结束语
在本文中,我们了解了自动编码器的一个用途,即图像去噪。我们可以看到 autoencoder 的表示学习如何允许它学习映射,从而足够有效地修复不正确的像素/数据点。
这可以扩展到表格数据应用程序,在这些应用程序中,自动编码器有助于填充数据实例中的缺失值。然而,应该注意的是,去噪自动编码器仅在它们已经被训练的特定种类的噪声上工作。
开放领域问答中的密集段落检索
在这篇博客中,我们将深入探讨论文中关于开放领域问答的密集段落检索。我们将尝试并理解它的理论方面,然后是一个快速的伪代码实现。
Photo by Emily Morter / Unsplash
开放领域问答系统严重依赖于高效的段落检索方法。这一步有助于选择回答任何问题的相关候选上下文。开放领域问答系统通常遵循两步流水线:(第一步)上下文检索器(第二步)机器阅读器。上下文检索器负责获取与问题相关并可能包含答案的一小组段落。机器阅读器负责从这些段落中识别正确答案。在这篇博客中,我们主要讨论管道的上下文检索部分的改进。
Open-domain Extractive QA pipeline
传统系统将 TF-IDF 和 BM25 的一些逻辑建模到它们的检索器中,这通常工作得相当好,但问题是:“我们能做得更好吗”?
我们将不会进入如何工作的细节 TF-IDF 和 BM25 ,随时检查这和这同样。简而言之,它们在两个文本片段的加权单词袋表示法之间执行某种稀疏向量相似性。这种系统的一个明显的局限性是不能检索单词不匹配的上下文。最近,随着对自然语言处理中与词袋表示相反的语义建模的大肆宣传和影响,来自脸书人工智能/Meta 实验室、华盛顿大学和普林斯顿大学的这项工作表明,基于密集向量表示可以有效地实现检索,并且还被视为以相当大的幅度超过传统技术。
作者提出了一种方法,通过简单的双编码器框架,利用少量的问题和黄金段落对来学习这些密集表示,也称为嵌入。
让我们直接看一个论文中提到的例子来理解我们到底在试图解决什么。考虑问题“魔戒中的坏人是谁?”,可以从上下文中回答“萨拉·贝克最出名的是在《指环王》三部曲中饰演反派索隆。”基于术语的系统将难以检索这样的上下文,而密集的检索系统将能够更好地将短语“坏人”与单词“恶棍”匹配,从而获取正确的上下文。随着我们在博客中的进展,我们将会谈到这个方法的细节,但是让我们先看看一些结果。
下图显示了 top-k 准确性的趋势,我们对在不同大小的真实数据集上训练的模型的 k 值进行了缩放。因此,这里的 top-k 准确性意味着,对于给定的查询(q ),有多少得分最高的段落是正确的,并且可以提供正确的答案。
DPR Top-k accuracy performance when compared to BM25
可以清楚地看到,与 BM25 技术相比,仅用 10k 个问题和段落黄金对的训练样本,所提出的方法就可以在 top-k 准确度数字上获得几乎 7-9%的提高。这里, k 的值可以从非常小的 5 到非常大的 100。令人惊讶的是,如果你对一个给定的问题只有 10-20 段感兴趣,那么即使只有 1k 黄金样本的训练也显示出 top-k 准确率比 BM25 技术提高了近 2-4%。
密集通道检索器(DPR)
密集段落检索器(DPR)负责基于段落和问题的高质量低维连续表示之间的相似性来获取与所问问题相关的段落。此外,由于整个系统必须合理地快速服务于用户的请求,所以包含这些表示的索引被预先计算和维护。现在,在推理期间,对于出现的任何新查询/问题,我们可以有效地检索一些顶部的 k 段落,然后只在这个较小的子集上运行我们的阅读器组件。论文中的作者使用了脸书人工智能相似性搜索(FAISS) ,这是一个允许我们快速(近似最近邻居)搜索彼此相似的多媒体文档的库。这里, k 的大小取决于几个因素,如流水线中的预期延迟、可用计算、召回等。但一般来说,10-50 之间的任何值都是合适的。
问题和段落嵌入之间的相似性通过计算它们之间的点积来表示。作者也试验了其他类似的方法,但最终选择了点积,因为它简单。相似性得分(或距离越小)越高,该段落与问题越相关。数学表示如下 -
Similarity Calculation
这里, q 、 p 、 Eq 、 Ep 分别对应问题文本、段落文本、输出问题表征的 BERT 模型、输出段落表征的 BERT 模型。作者使用768 维度表示的 CLS 令牌作为输入文本块的最终表示。
为了训练模型,数据集被表示为$ D={ < q1,p1,p2,p3..pn >,< q2,p2,p1,p3..pn >...}$,这里的气是第 i 个问题,每个问题都配有一个正例和一些反例。为了简单起见,上面每个例子的第一个索引是对问题 qi 的正面段落,其余都是负面的。并且他们将损失函数优化为正通过的负对数似然。下图显示了同一 - 的数学表示
Loss Function
这里, qi,pi(+),, pi(-) 分别是第 i 题, i 第相关的段落(正例),和 i 第不相关的段落(负例)。优化的目标是最大化 qi 和 pi(+) 之间的相似性,减少不相关的 qiqi和 pi(-) 之间的相似性。
通常在检索场景中,获得正面例子是简单的(如给出问题和相关段落的训练数据所示),而人们可能必须考虑有效地选择负面例子。作者在论文中实验了三种负生成方法:
- (1) Random :从语料库中随机抽取的任意段落
- (2)BM25:BM25 返回的顶部段落,这些段落可能不一定包含答案,但与大多数问题标记匹配
- (3) 黄金:与训练集中出现的其他问题配对的正面段落(特别是出现在同一小批中的那些)。发现第 3 点和第 2 点的组合效果最好。
至此,我们总结了对所提出模型的理论理解。接下来,我们开始编写可用于在数据集上训练 DPR 模型的模板。
代码模板
我们将使用 [Simple Transformers python 库](Simple Transformers is a Natural Language Processing (NLP) library designed to simplify the usage of Transformer models without having to compromise on utility. It is built on the amazing work of Hugging Face and their Transformers library.)来设置实现 DPR 模型的模板。简单变压器旨在简化变压器模型的使用,而不必牺牲效用。它建立在拥抱脸和它的变形金刚库的惊人工作之上。它支持 NLP 中的各种任务,请随意查看完整列表这里。
首先,我们可以使用下面的命令 - 来安装它
pip install simpletransformers
接下来,我们需要定义我们将用于训练我们的模型的所有配置,并导入我们将用于这个实验的两个包。simpletransformers
库为您提供了使用 dataclass 或 python 字典定义配置的选项。出于这个博客的目的,我们将使用 dataclass: RetrievalArgs
。也可以从这个来源随意检查字典选项。
from simpletransformers.retrieval import RetrievalModel, RetrievalArgs
import torch
### loading pre-trained weights of passage and question encoder ###
Eq = "bert-base-uncased"
Ep = "bert-base-uncased"
model_args = RetrievalArgs()
#model_args.retrieve_n_docs
#model_args.hard_negatives
#model_args.max_seq_length
#model_args.num_train_epochs
#model_args.train_batch_size
#model_args.learning_rate
Defining configuration for DPR training
这里 Eq 和 Ep 持有将用于编码问题和段落的模型。我们还可以从一些公开可用的预先训练的 DPR 编码器开始,用于上下文和段落 ( 一个例子是上下文:“Facebook/DPR-question _ encoder-single-NQ-base”,段落:“Facebook/DPR-CTX _ encoder-single-NQ-base”)。设置为True
时的hard_negatives
有助于模型在批内否定的基础上,从使用 BM25 等技术生成的否定示例中学习。如上所述,该论文还提出了基于 BM25 或类似方法的批内底片和获取底片样本的概念。
要了解更多信息,库 authrors 提供了生成这些硬底片的代码片段。请在他们的文档中随意查看。
接下来,我们加载数据集,为了简单起见,我们将手动定义一些数据点(源)。我们将导入 Pandas 库来促进这一点,并将给出我们如何转换我们的实际数据以适应管道的想法。
import pandas as pd
train_data = [
{
"query_text": "Who is the protaganist of Dune?",
"gold_passage": 'Dune is set in the distant future amidst a feudal interstellar society in which various noble houses control planetary fiefs. It tells the story of young Paul Atreides, whose family accepts the stewardship of the planet Arrakis. While the planet is an inhospitable and sparsely populated desert wasteland, it is the only source of melange, or "spice", a drug that extends life and enhances mental abilities. Melange is also necessary for space navigation, which requires a kind of multidimensional awareness and foresight that only the drug provides. As melange can only be produced on Arrakis, control of the planet is a coveted and dangerous undertaking.',
},
{
"query_text": "Who is the author of Dune?",
"gold_passage": "Dune is a 1965 science fiction novel by American author Frank Herbert, originally published as two separate serials in Analog magazine.",
}
...
]
train = pd.DataFrame(
train_data
)
有了这个,我们就可以继续训练 DPR 模型了。在我们已经将hard_negatives
设置为True
的情况下,我们将不得不以上述格式为每个数据点设置另一个键(hard_negative
)。
cuda_available = torch.cuda.is_available()
model = RetrievalModel(
model_type = "dpr",
context_encoder_name = Ep,
query_encoder_name = Eq,
args = model_args,
use_cuda = cuda_available
)
model.train_model(train, eval_data = eval, \
output_dir = 'model/', \
show_running_loss = True)
model.eval_model(test, verbose=True)
Training and Evaluating DPR
接下来,我们将所有必要的参数传递给我们的模型,并通过指定保存模型的输出目录来进行训练。eval
和test
数据帧的格式也与train
完全相同。这里,train
、eval
和test
是熊猫数据帧,包含 2-3 列- query_text
、gold_passage
、hard_negative
(可选)
query_text
:查询/问题文本序列gold_passage
:黄金段落文本序列hard_negative
:BM25 中的硬否定段落文本序列(可选)
用训练好的 DPR 模型进行推理
一旦训练完成,你已经保存了你的模型。现在,您可以将问题传递给模型,指定要返回的文档数量,这样就可以了。
questions = [
'What year did Dune release?'
]
predicted_passages, _, _, _ = model.predict(questions, retrieve_n_docs=2)
Single example inference with DPR
总结想法
看到新兴技术的采用以及它在这种实际用例中的应用是非常有趣的。在本文中,我们讨论了什么是 DPR,它是如何工作的,以及它的用途。我们也使用简单的 Transformers python 库实现了它。请随意查看其他规范,您可以使用它们来训练一个高效的模型。
我希望你喜欢这篇文章。此外,如果你更喜欢看视频而不是阅读文本,你可以在这里查看视频论文的解释。虽然你不会在这个博客中找到这个独家实现;)
部署深度学习模型第 1 部分:准备模型
原文:https://blog.paperspace.com/deploying-deep-learning-models-flask-web-python/
无论你是在本地工作还是在云上工作,许多机器学习工程师都没有实际部署他们的模型以便在全球范围内使用的经验。在本教程中,我们将看到如何通过在 web 上部署您的项目,将您的作品展示给观众。我们将从创建一个识别手写数字的简单模型开始。然后,我们将一步一步地看到如何使用 Flask(一个用 Python 编写的微型 web 框架)创建一个用于在 web 上部署它的接口。
快速建立模型:CNN 与 MNIST
在我们开始将模型部署到生产环境之前,让我们先创建一个可以保存和部署的简单模型。如果你已经建立了自己的模型,可以随意跳到下面的用 h5py 保存训练好的模型,或者创建一个 Flask 应用程序为模型服务。出于我们的目的,我们将从一个简单的用例开始,使用 MNIST 数据集来识别手写数字,从而创建一个深度学习模型。这将让我们了解如何从头开始定义网络架构,然后训练、评估和保存它们以便部署。
卷积神经网络(CNN)用于手写识别任务,以及大多数图像识别任务。图像首先通过不同的卷积层发送,神经元在卷积层提取和识别特征。每当网络在测试集中遇到与它在训练中学习到的特征相似的模式时,它会将该图像分类到相应的输出标签。
现在让我们使用 Keras 深度学习框架通过 8 个简单的步骤来实现该算法。
步骤 1:导入必要的模块和层
我们总是从导入我们将使用的所有模块和函数开始。这个神经网络是在 Keras 中实现的(它预装在 Paperspace 中,但是如果您在本地运行它,您总是可以使用pip install Keras
从命令行安装 Keras)。接下来,我们导入将用于构建神经网络架构的模型和层,在本例中是 CNN。
# imports
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
步骤 2:定义超参数
为您的网络选择超参数可能是一项具有挑战性的任务。在不涉及太多理论或测试许多不同值的情况下,这里我们使用批量大小(定义在更新模型权重之前要处理的训练样本的数量)和时期数量(训练集中数据的完整呈现以供学习)的标准值。因为我们考虑数字 1-10,所以有 10 个类。
# Hyperparameters
num_classes = 10
batch_size = 128
epochs = 12
步骤 3:加载图像
下一步是加载我们的数据集,并为我们的训练过程设置恒定的图像大小。图像大小固定为(28 x 28),因为网络输入参数总是不变的(不能用不同的维度训练网络)。我们简单地用步骤 1 中导入的 MNIST 类的加载方法加载我们的 MNIST 数据集。
# Image Resolution
img_rows, img_cols = 28, 28
# Loading the data.
(x_train, y_train), (x_test, y_test) = mnist.load_data()
第四步:数据预处理
在这一步中,我们需要确保训练数据经过预处理并调整到相同的方向;如果输入的大小不同,网络的性能将会不准确。我们对每个图像使用简单的整形方法,并在整个数据集上迭代。接下来,我们将受关注的标签分配给训练过程的每幅图像,在这种情况下,我们使用to_categorical
方法将标签分配给每幅图像。
# Preparing the data
if K.image_data_format() == 'channels_first':
x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
步骤 5:定义架构
使用 Keras 框架,我们可以通过顺序添加层来轻松声明模型。我们为此使用了add()
方法。
# Creating the Model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),activation='relu',input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adadelta(),
metrics=['accuracy'])
第六步:训练循环
接下来,我们用声明的超参数拟合模型,并启动训练过程。这可以简单地通过使用model.fit()
方法并传递参数来完成。
# Training the Model
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))
步骤 7:评估模型
# Evaluating the Predictions on the Model
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
步骤 8:保存模型
# Saving the model for Future Inferences
model_json = model.to_json()
with open("model.json", "w") as json_file:
json_file.write(model_json)
# serialize weights to HDF5
model.save_weights("model.h5")
运行该程序并成功完成培训后,您将在同一目录中找到两个文件:
- 模型. json
- 型号. h5
model.h5 文件是一个保存权重的二进制文件。文件 model.json 是您刚刚构建的模型的架构。
使用 h5py 保存训练模型
HDF5 库允许用户存储大量数字数据,并使用 NumPy 轻松操作这些数据。例如,您可以将存储在磁盘上的多 TB 数据集切片,就像它们是真正的 NumPy 数组一样。数以千计的数据集可以存储在一个文件中,按照你想要的方式进行分类和标记。
上面添加了save_weights
方法,以便保存网络使用 h5py 学习的权重。h5py 包是 HDF5 二进制数据格式的 Pythonic 接口。
现在我们已经将模型保存为 HDF5 格式,我们可以随时加载权重,并将其应用于未来的任务。为了加载权重,我们还需要定义相应的模型架构。让我们从之前使用的 JSON 文件开始。一旦模型准备好了训练好的权重,我们就可以用它来进行推理了。
# imports
from keras import model_from_json
# opening and store file in a variable
json_file = open('model.json','r')
loaded_model_json = json_file.read()
json_file.close()
# use Keras model_from_json to make a loaded model
loaded_model = model_from_json(loaded_model_json)
# load weights into new model
loaded_model.load_weights("model.h5")
print("Loaded Model from disk")
# compile and evaluate loaded model
loaded_model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
现在我们已经保存了模型以及从训练中学习到的权重,我们可以使用它们对新数据进行推断。这就是我们如何使训练好的模型可重用。
创建一个为模型服务的 Flask 应用程序
为了服务保存的模型,我们将使用 Flask,这是一个用 Python 编写的微型 web 框架(它被称为“微型”框架,因为它不需要特定的工具或库)。
为了创建能够识别不同手写数字的 web 应用程序,我们需要 flask 应用程序中的两条路线:
- 绘制图像的用户的索引页路径
- 从我们保存的模型进行推理的预测路线
这些定义如下。
from flask import Flask, render_template, request
@app.route('/')
def index_view():
return render_template('index.html')
@app.route('/predict/',methods=['GET','POST'])
def predict():
response = "For ML Prediction"
return response
if __name__ == '__main__':
app.run(debug=True, port=8000)
现在,让我们继续实现我们完整的 app . py。predict 函数应该获取用户绘制的图像并将其发送给模型。在我们的例子中,图像是一个包含像素亮度的 NumPy 数组。
from flask import Flask, render_template, request
from scipy.misc import imsave, imread, imresize
import numpy as np
import keras.models
import re
import sys
import os
import base64
sys.path.append(os.path.abspath("./model"))
from load import *
global graph, model
model, graph = init()
app = Flask(__name__)
@app.route('/')
def index_view():
return render_template('index.html')
def convertImage(imgData1):
imgstr = re.search(b'base64,(.*)',imgData1).group(1)
with open('output.png','wb') as output:
output.write(base64.b64decode(imgstr))
@app.route('/predict/',methods=['GET','POST'])
def predict():
imgData = request.get_data()
convertImage(imgData)
x = imread('output.png',mode='L')
x = np.invert(x)
x = imresize(x,(28,28))
x = x.reshape(1,28,28,1)
with graph.as_default():
out = model.predict(x)
print(out)
print(np.argmax(out,axis=1))
response = np.array_str(np.argmax(out,axis=1))
return response
if __name__ == '__main__':
app.run(debug=True, port=8000)
这里我们有加载器函数 load.py:
import numpy as np
import keras.models
from keras.models import model_from_json
from scipy.misc import imread, imresize,imshow
import tensorflow as tf
def init():
json_file = open('model.json','r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
#load weights into new model
loaded_model.load_weights("model.h5")
print("Loaded Model from disk")
#compile and evaluate loaded model
loaded_model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
#loss,accuracy = model.evaluate(X_test,y_test)
#print('loss:', loss)
#print('accuracy:', accuracy)
graph = tf.get_default_graph()
return loaded_model,graph
在我们深入到部署到云中的最后一步之前,让我们创建一个界面,让用户能够从浏览器中绘制图像。我们将使用 JavaScript 并在 HTML 页面上呈现画布。下面是渲染画布以供绘制的 JavaScript 代码片段。
(function()
{
var canvas = document.querySelector( "#canvas" );
var context = canvas.getContext( "2d" );
canvas.width = 280;
canvas.height = 280;
var Mouse = { x: 0, y: 0 };
var lastMouse = { x: 0, y: 0 };
context.fillStyle="white";
context.fillRect(0,0,canvas.width,canvas.height);
context.color = "black";
context.lineWidth = 6;
context.lineJoin = context.lineCap = 'round';
debug();
canvas.addEventListener( "mousemove", function( e )
{
lastMouse.x = Mouse.x;
lastMouse.y = Mouse.y;
Mouse.x = e.pageX - this.offsetLeft;
Mouse.y = e.pageY - this.offsetTop;
}, false );
canvas.addEventListener( "mousedown", function( e )
{
canvas.addEventListener( "mousemove", onPaint, false );
}, false );
canvas.addEventListener( "mouseup", function()
{
canvas.removeEventListener( "mousemove", onPaint, false );
}, false );
var onPaint = function()
{
context.lineWidth = context.lineWidth;
context.lineJoin = "round";
context.lineCap = "round";
context.strokeStyle = context.color;
context.beginPath();
context.moveTo( lastMouse.x, lastMouse.y );
context.lineTo( Mouse.x, Mouse.y );
context.closePath();
context.stroke();
};
function debug()
{
/* CLEAR BUTTON */
var clearButton = $( "#clearButton" );
clearButton.on( "click", function()
{
context.clearRect( 0, 0, 280, 280 );
context.fillStyle="white";
context.fillRect(0,0,canvas.width,canvas.height);
});
$( "#colors" ).change(function()
{
var color = $( "#colors" ).val();
context.color = color;
});
$( "#lineWidth" ).change(function()
{
context.lineWidth = $( this ).val();
});
}
}());
一旦你在 HTML 中使用了这个片段,在本教程结束时,你的目录结构应该是这样的:
ml-in-prod/
app . py
procfile
requirements . txt
runtime . txt【T4/
【model . JSON】
model . h
这就对了。您的应用程序已经启动并运行。在下一个教程中,我们将了解如何将其部署在 Paperspace 云 GPU 上,以使应用程序更加强大、可靠和易于访问。
部署深度学习模型第 2 部分:在 Paperspace 上托管
原文:https://blog.paperspace.com/deploying-deep-learning-models-part-ii-hosting-on-paperspace/
[2021 年 12 月 2 日更新:本文包含关于梯度实验的信息。实验现已被弃用,渐变工作流已经取代了它的功能。请参见工作流程文档了解更多信息。]
Gradient 是一个 Paperspace 产品,它简化了深度学习模型的开发、培训和部署。该平台为机器学习开发者提供了基础设施自动化和软件开发工具包。
要在渐变上创建一个基于 flask 的应用程序,你需要做的就是:
- 在“requirements.txt”中指定我们的应用程序的依赖项
- 初始化您的本地 Git 存储库,并将其发布在 Github 上
- 创建应用程序并将其部署到渐变
本教程假设您已经完成了第 1 部分中概述的步骤。如果你对我们为什么有某些文件或目录感到困惑,请参考部署深度学习模型第 1 部分:准备模型以供参考。
与 Git 集成
首先,创建如下所示的“requirements.txt”文件,指定设置环境所需的库。
Keras==2.2.0
tensorflow==1.8.0
flask
numpy
pandas
Pillow==5.2.0
gunicorn==18.0.0
您需要使用 git 将您的代码推入 Github 存储库。如果你还没有,在 Github 和 install git 上注册。
要将代码推送到 Github,创建一个名为“ml-in-prod”的新存储库,并从您的终端执行以下命令。请注意,您应该将第一行中的“用户名”和“回购名称”更改为您自己的名称。
git remote add origin https://github.com/user_name/repo_name.git
git add .
git commit -m "first-commit"
git push origin master
检查你的 Github repo 它应该有我们创建的所有文件。
从“ml-in-prod”存储库中,执行以下命令:
去吧,init
这将初始化 git。
部署到渐变
现在,要在 Gradient 上部署您的模型,请执行以下操作。
在 app.py 文件的app.run()
函数中,主机参数将被设置为 0.0.0.0。换句话说,app.run()
应该看起来像app.run(debug=True, port=8000, host='0.0.0.0')
。这表明 flask 服务器“监听”所有接口。
如果您还没有付费帐户,请随意使用免费的渐变社区笔记本,开始使用免费的云 GPU。登录后,导航至左侧导航栏“渐变”下的“项目”。
点击“创建项目”
我们将在这里创建一个“独立项目”。有关不同类型项目(独立项目或梯度项目)的更多信息,请参考梯度项目文档。
输入项目名称“ml-in-prod”,然后点击“创建项目”
现在点击你新创建的项目。下一页是我们创建实验和作业的地方。
目前,它有 0 个实验。我们现在创建一个。
向下滚动,您将看到以下要填写的字段。
选择机器类型为 P5000。
- 再往前,容器是设置整个环境的 docker 图像。除了我们将在后面看到的“requirements.txt ”,这个容器还有助于安装我们额外需要的大多数依赖项。在这个用例中,“测试容器”满足了我们的需求。
- 工作空间是需要部署的代码。之前,我们将应用程序推送到 Github 上,在这里使用它(在 Github repo 中的克隆/下载按钮中,获取这个 URL)。
- 接下来是命令,其中我们指定了在执行这个实验时需要运行的命令。
“Requirements.txt”会安装我们考虑的容器映像中可能不存在的所有其他依赖项。pip install-r“requirements . txt”是使用的命令。python app.py 执行我们一直编码的应用。
因为我们以前在应用程序中使用过端口 8000,所以在这里使用相同的端口。
自定义指标可以暂时留空,然后单击“提交实验”。
- 显示了运行实验的状态以及我们之前使用的命令和容器。
- 单击它会将您导航到作业控制台。
状态栏中的“正在运行”表示代码正在成功执行,没有任何中断。
- 按下它会带您进入工作详细信息。
- 在“环境”部分,提到了作业容器的详细信息。
- 向下滚动,其中的 URL 是部署应用程序的位置。
- 复制 URL,将端口号“8000”附加到它后面。
app 来了!在所示的画布上画一个数字,它会为您预测输出。
部署用于梯度深度学习的 Flask 应用程序
在今天的教程中,我们将从上周的教程中吸取教训,并将其应用于构建一个基本的 Flask 应用程序,然后使用渐变部署来访问浏览器中的 API。这将让我们访问一个简单的界面,在这里我们可以快速上传照片,并通过 GFP-GAN 在两步过程中进行处理和恢复。
在本教程结束时,您将知道如何编写 Flask 应用程序来部署 PyTorch 模型 GFP-GAN ,如何编写和创建必要的 Docker 文件并将其上传到 Dockerhub 以访问 Gradient,以及如何使用部署将 Flask 应用程序转变为云 GPU 增压的原型网页。
请务必阅读陶昕等人之前的博客和论文。
完成此操作的工作流程如下:
- 设置本地环境
- 在 Docker 容器中创建 flask 应用程序
- 在本地测试并推送到 Dockerhub
- 使用图像在梯度上部署
- 还原照片!
Sample of GFP-GANs capabilities on a sample from Reddit
建立
首先,我们需要进行一些安装。这些是运行 GFP-GAN 所需的所有库:
torch>=1.7
numpy<1.21 # numba requires numpy<1.21,>=1.17
opencv-python
torchvision
scipy
tqdm
lmdb
pyyaml
tb-nightly
yapf
Flask
#These four have to be installed directly for reasons I haven't been able to figure out (see Dockerfile section)
opencv-python-headless
realesrgan==0.2.2.4
pip install basicsr
facexlib
其中大部分将使用一个requirements.txt
脚本来安装,它们是在 Flask 应用程序上运行模型以及在本地机器上测试模型所必需的。安装完成后,打开您喜欢的代码编辑器。
此时,继续将 GFP-GAN repo 克隆到您的本地机器上。在您的编辑器中打开 GFP-GAN repo 的工作区,并导航到文件app.py
。这就是 Flask 应用程序的结构所在。
烧瓶应用程序
import sys
import io
import json
import argparse
import cv2
import glob
import numpy as np
import os
from basicsr.utils import imwrite
from os import listdir
from os.path import isfile, join
from werkzeug.utils import secure_filename
import torch
import torchvision.transforms as transforms
from PIL import Image
from flask import Flask, request, render_template, redirect, url_for
from gfpgan import GFPGANer
这些都是运行这个示例应用程序所需的导入。GFP-GAN 是用 PyTorch 编写的,所以容器运行也需要 Torch 和 torchvision。其余的用来确保 Flask 应用程序正常运行。
UPLOAD_FOLDER = 'inputs/whole_imgs'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__,static_folder='results')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
下一个代码块用于实例化我们的 Flask 应用程序。在这个过程中,我们将whole_imgs
目录指定为上传文件的目的地,并将结果目录指定为保存 HTML 显示的静态图像。这些图像仅限于更常见的类型,以确保在训练中没有任何问题。
@app.route("/", methods = ['GET', 'POST'])
def index():
return render_template("index.html")
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/upload", methods = ['GET', 'POST'])
def upload_file():
# Clear the input and source folders, and dump previous images to saved. You can build another app route to take advantage of these saved images as needed.
source = 'inputs/whole_imgs/'
destination = 'inputs/saved/'
out = 'results/restored_imgs'
for f in os.listdir(source):
os.remove(os.path.join(source, f))
for f in os.listdir(destination):
os.remove(os.path.join(destination, f))
for f in os.listdir(out):
os.remove(os.path.join(out, f))
# Get the file upload
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
print('No file part')
return redirect(request.url)
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
if file.filename == '':
print('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('main', name=filename))
#HTML form for uploading the file to the app
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
</form>
'''
新网站的/upload
API 端点是用户能够直接上传他们选择由 GFP-GAN 增强和恢复的文件的地方。这些脚本获取文件,确保其与模型的兼容性,并将其上传到应用程序从中提取恢复内容的目录中。这一切都采取了传统的“选择文件”按钮的形式,然后是“上传”按钮。
@app.route('/main', methods=['POST','GET'])
def main():
"""Inference demo for GFPGAN.
All credit to Xintao et al. at https://github.com/TencentARC/GFPGAN for writing this script i've adapted here for Flask.
"""
# Normally, this repo was designed to be executed from the command line with a series of optional and required args.
# Instead, you will need to change the arguments you need to change using the sys.argv command (see line 29)
parser = argparse.ArgumentParser()
parser.add_argument('--upscale', type=int, default=2, help='The final upsampling scale of the image')
parser.add_argument('--arch', type=str, default='clean', help='The GFPGAN architecture. Option: clean | original')
parser.add_argument('--channel', type=int, default=2, help='Channel multiplier for large networks of StyleGAN2')
parser.add_argument('--model_path', type=str, default='GFPGANCleanv1-NoCE-C2.pth')
parser.add_argument('--bg_upsampler', type=str, default='realesrgan', help='background upsampler')
parser.add_argument(
'--bg_tile', type=int, default=400, help='Tile size for background sampler, 0 for no tile during testing')
parser.add_argument('--test_path', type=str, default='upload/', help='Input folder')
parser.add_argument('--suffix', type=str, default=None, help='Suffix of the restored faces')
parser.add_argument('--only_center_face', action='store_true', help='Only restore the center face')
parser.add_argument('--aligned', action='store_true', help='Input are aligned faces')
parser.add_argument('--paste_back', action='store_false', help='Paste the restored faces back to images')
parser.add_argument('--save_root', type=str, default='results', help='Path to save root')
parser.add_argument(
'--ext',
type=str,
default='auto',
help='Image extension. Options: auto | jpg | png, auto means using the same extension as inputs')
# directly input the args you want to run inference_gfpgan using sys.argv. you can change these as needed, but i have used the defaults for simplicity.
sys.argv = ['--model_path GFPGANCleanv1-NoCE-C2.pth --upscale 2 --test_path inputs/whole_imgs --save_root results --bg_upsampler realesrgan']
args = parser.parse_args()
if args.test_path.endswith('/'):
args.test_path = args.test_path[:-1]
os.makedirs(args.save_root, exist_ok=True)
# background upsampler
if args.bg_upsampler == 'realesrgan':
if not torch.cuda.is_available(): # CPU
import warnings
warnings.warn('The unoptimized RealESRGAN is very slow on CPU. We do not use it. '
'If you really want to use it, please modify the corresponding codes.')
bg_upsampler = None
else:
from realesrgan import RealESRGANer
bg_upsampler = RealESRGANer(
scale=2,
model_path='https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth',
tile=args.bg_tile,
tile_pad=10,
pre_pad=0,
half=True) # need to set False in CPU mode
else:
bg_upsampler = None
args.test_path = 'inputs/whole_imgs'
# set up GFPGAN restorer
restorer = GFPGANer(
model_path=args.model_path,
upscale=args.upscale,
arch=args.arch,
channel_multiplier=args.channel,
bg_upsampler=bg_upsampler)
img_list = sorted(glob.glob(os.path.join(args.test_path, '*')))
print(img_list, '**')
count = 4
for img_path in img_list:
count -= 1
if count == 0:
break
# read image
print('yes')
img_name = os.path.basename(img_path)
print(f'Processing {img_name} ...')
basename, ext = os.path.splitext(img_name)
input_img = cv2.imread(img_path, cv2.IMREAD_COLOR)
# restore faces and background if necessary
cropped_faces, restored_faces, restored_img = restorer.enhance(
input_img, has_aligned=args.aligned, only_center_face=args.only_center_face, paste_back=args.paste_back)
# save faces
for idx, (cropped_face, restored_face) in enumerate(zip(cropped_faces, restored_faces)):
print('1')
# save cropped face
save_crop_path = os.path.join(args.save_root, 'cropped_faces', f'{basename}_{idx:02d}.png')
imwrite(cropped_face, save_crop_path)
# save restored face
if args.suffix is not None:
save_face_name = f'{basename}_{idx:02d}_{args.suffix}.png'
else:
save_face_name = f'{basename}_{idx:02d}.png'
save_restore_path = os.path.join(args.save_root, 'restored_faces', save_face_name)
imwrite(restored_face, save_restore_path)
# save comparison image
cmp_img = np.concatenate((cropped_face, restored_face), axis=1)
imwrite(cmp_img, os.path.join(args.save_root, 'cmp', f'{basename}_{idx:02d}.png'))
# save restored img
if restored_img is not None:
if args.ext == 'auto':
extension = ext[1:]
else:
extension = args.ext
if args.suffix is not None:
save_restore_path = os.path.join(args.save_root, 'restored_imgs',
f'{basename}_{args.suffix}.{extension}')
else:
save_restore_path = os.path.join(args.save_root, 'restored_imgs', f'{basename}.{extension}')
imwrite(restored_img, save_restore_path)
onlyfiles = [f for f in listdir('results/restored_imgs') if isfile(join('results/restored_imgs', f))]
onlyfiles.remove('.DS_Store')
return render_template("index2.html", variable = onlyfiles[0])
if __name__ == '__main__':
app.run(#host="0.0.0.0")
上面是回购协议中的inference_gfpgan.py
脚本的代码,已经被改编到我们的应用程序中。这个脚本解析我们使用sys.argv
调用编写的输入参数。这些被设置为函数中概述的默认值,但是这造成了一个小问题。Docker 映像的庞大规模使得在将映像推送到 Dockerhub 部署之前考虑这些参数非常重要。在未来,为了生产梯度上的应用程序,我们需要考虑更多的动态方法来改变恢复过程的参数。
该应用程序中的 GFP-GAN 函数会根据 args 进行一些调整,但它会获取每个输入的图像,增强它们的面部图像和背景,然后将新版本保存到预定或默认的结果目录中。
码头工人
GFP-GAN repo 中有一个示例Dockerfile
用于创建 docker 映像,该映像稍后可以部署到渐变 GPU。让我们看看这个例子,看看如何用 Flask 为 PyTorch 部署编写一个非常基本的 Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.8-slim-buster
RUN pip install flask
RUN apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y git
RUN git clone https://github.com/gradient-ai/GFPGAN.git
WORKDIR /GFPGAN
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
RUN pip3 install opencv-python-headless
RUN pip install realesrgan==0.2.2.4
RUN pip install basicsr
RUN pip install facexlib
RUN python setup.py develop
RUN wget https://github.com/TencentARC/GFPGAN/releases/download/v0.2.0/GFPGANCleanv1-NoCE-C2.pth
EXPOSE 5000
COPY . .
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
这个Dockerfile
在运行时,将下载所有必需的包并构建一个可以作为容器运行的映像。这个页面主要由安装组成,但也包括另外两个重要的工作:RUN wget
获取模型权重,执行setup.py
完成容器环境的配置。
一旦您的文件准备就绪,我们就可以通过运行以下命令来构建 Docker 映像:
docker build gfpgan .
一旦我们在本地构建了您的映像,使用以下命令运行容器:
docker run -d -p 5000:5000 gfpgan-flask-demo-gradient:latest
然后,我们可以 localhost:5000,并遵循通过应用程序运行 GFP-GAN 所需的两个步骤。
我们的应用程序现在运行在 localhost:5000 端口上,您可以通过浏览器访问它。按照提示上传您希望看到恢复的照片。在上传和恢复过程中检查终端的进度,以获取 Flask 应用程序的日志。
运行upload_photo()
时,将执行main()
脚本,并将结果保存到结果目录。一旦修饰过的照片生成并保存,应用程序将使用 HTML 在网页中显示它。从这里你可以直接下载。
在 Docker 应用程序或 Docker CLI 中,您现在可以将新映像推送到 Dockerhub,以便根据需要进行共享、版本控制和改进。如果你不想建立自己的 docker 文件,你可以通过这个 url 访问我的。
部署
image: jameshskelton/paperspace:gfpgan-flask-demo-gradient
port: 5000
resources:
replicas: 1
instanceType: P4000
从Dockerfile
开始在斜坡上部署的简易性不可低估。首先,创建一个新项目,并在渐变 GUI 中导航到它的 Deployments 选项卡。在设置部署的 GUI 中,我们可以看到设置的逐步说明。在页面顶部,选择“上传部署规范”链接。这将允许您直接上传您的规格。上面的示例部署规范可用于将新的 Dockerized Flask 应用程序部署到渐变部署。只需替换单击链接时弹出的文本编辑器中的变量,然后粘贴您的规范。您也可以使用 Gradient CLI 命令创建部署
gradient deployments create --name [deployment name] --projectId [your project id] --spec [path to your deployment spec file]
A sample deployment for this Flask app
一旦您上传了您的规范,导航到您的项目中的 Deployments 选项卡,如果它没有重定向您,然后单击您的新部署。在细节部分,您可以获取我们新部署的 Flask API 的 url。如果你点击那个链接,它会把你带到应用程序在/upload
页面上的 HTML 界面
The HTML forms a simple GUI for you to submit your image to the model for restoration
按照 GUI 中的提示,上传要恢复的映像。当您选择“上传”按钮时,它会自动将您的图像加载到输入目录中,并初始化 GAN 以通过应用程序的main()
功能运行。完成后,网页将自动重定向以在 HTML 中显示新图像。从这里,您可以通过返回到原始 url 并上传新照片来再次运行模型。
In the first set of pixellated photos of Adele, we can see how the algorithm attempted to infer the curvature of her cheeks and chin but ultimately failed. We can thus conclude that the model is ill suited for artificially damaged or obfuscated images. In the second set, a group of old friends are photographed in a darkened area. The GFP-GAN performs much more appropriately because it is able to infer the subjects faces through low-light quite well. Photos taken from GFP-GAN github and reddit/r/restoration.
值得一提的是,该程序最适用于以某种方式受损的高质量图像。上面是两组样本图像,右边的照片更适合模特,另一张是误用的例子。如果你的照片特别模糊或像素化,你可能会对照片中的主体产生一些奇怪的效果,从恐怖谷到彻头彻尾的身体恐怖。
结论
在本教程中,我们看到了如何构建一个 Flask 应用程序来服务 PyTorch GFP-GAN 实现,如何通过简单的 HTML GUI 将其部署为 web 可访问的 API 端点,以及如何使用这个新部署的应用程序来恢复损坏的照片。这是一个非常简单的 Flask 应用程序,因此可以随意修改它以更好地满足您的需求。一种可能性是将该应用程序连接到 Reddit PRAW 应用程序,并使用它来自动解析和回复一些更受欢迎的图像恢复子编辑中的帖子,如 R/estoration。
如果你想展示你用 GFP-GAN 构建渐变的作品,一定要发微博给我们!请务必查看今天项目的 GitHub 回购协议!
用 PyTorch 从胸部 X 线扫描中检测和定位肺炎
原文:https://blog.paperspace.com/detecting-and-localizing-pneumonia-from-chest-x-ray-scans-with-pytorch/
这些年来,我们已经看到了非常强大的模型被建立来区分物体。这些模型在性能和延迟方面一天天变得越来越好,但我们是否想过这些模型从用于训练它们做出几乎完美预测的图像中提取了什么?毫无疑问,我们提供给这些模型的图像中有一些特征,它们可以用来进行预测,这也是我们在本文中试图探索的。不久前,斯坦福大学的研究人员发布了一篇论文https://arxiv.org/abs/1901.07031,讲述了他们如何利用深度学习推动肺炎诊断的边缘。他们的工作真的让我着迷,所以我在 Pytorch 中进行了尝试,我将向您展示我如何使用 Kaggle 上的不同数据集来实现这项工作。
类激活图论文链接:http://cnnlocalization . csail . MIT . edu/Zhou _ Learning _ Deep _ Features _ 2016 _ paper . pdf
在这篇文章中,我们将建立一个机器学习管道,从胸部 x 射线图像中对患者是否患有肺炎进行分类,然后在模型用于做出这些决定的区域上绘制热图。下面是整个项目的简要概述。
加载并预处理我们的数据
将数据输入模型并进行训练
执行向前传递和向后传递来训练我们的模型
测试和评估我们的模型
模型评估
最后,我们在测试样本上绘制类激活图,以生成我们最终想要的输出
肺炎样本
逻辑
我们在这个项目中的目标是将胸部 x 射线图像分类为包含或不包含肺炎,并在用于识别肺炎的区分区域上绘制类激活图。我们将利用全球平均池在现有规范模型中提供的连接的简单性质,来设计一个管道来执行手头的任务。由于 ResNet 已经有了一个全局平均池,我们发现它非常适合用于这个任务。为了全面掌握手头的任务,我们需要访问模型中的一些区域。一个重要的领域是全球平均池层。
全球平均池层
通常在卷积神经网络架构中,所有卷积层都由一个或多个全连接层处理,但是这些全连接层通常具有许多参数,使得模型易于过拟合。作为卷积层末端的全连接层的替代,全局平均池层将全连接层的参数数量减少到零,因为它们只是将空间维度减少到由最后一个卷积层产生的特征图。它们的工作方式与平均和最大池图层完全相同,但通过采用大小为 h x w x d 的张量并生成大小为 1 x 1 x d 的张量来执行更极端的降维。它们所做的只是将每个 h x w 要素图的平均值转换为一个值。
资料组
对于这个项目,我们将使用 kaggle 上可用的数据集,包括 5433 个训练数据点、624 个验证数据点和 16 个测试数据点。
链接数据集:https://www . ka ggle . com/paultimothymooney/chest-Xray-pneumonia
模型架构
我们这个项目的基准型号是 ResNet 152。像其他卷积网络架构一样,ResNet 模型由一系列卷积层组成,但其设计方式有利于非常深的网络。卷积层被排列成一系列残余块。这些剩余块的意义在于防止消失梯度的问题,该问题在非常深的卷积网络中非常普遍。残余块具有允许在非常深的网络中梯度流动的跳跃连接。
ResNet block with a skip connection that allows the input x to skip the non-linear layers(Convolutional layers)
建立并训练我们的分类模型。
让我们提醒自己,我们的主要目标是在用于识别胸部 x 射线图像中的肺炎的区分区域上绘制堆图,但是为了实现该目标,我们必须训练我们的模型来执行正常分类。我们将以面向对象的编程风格构建我们的模型,这是 Pytorch 中构建模型的一种传统方式。构建我们的模型要做的第一件事是导入所有需要的包。该项目所需的软件包如下:
- Torch (torch.nn,torch.optim,torchvision,torchvision.transforms)
- Numpy
- Matplotlib
- 我的天啊
- PIL
Pytorch 为我们提供了非常强大的库来加载和预处理我们的数据,而无需编写任何样板代码。我们将使用 Dataset 模块和 ImageFolder 模块从包含图像的目录中加载数据,并应用一些数据扩充来生成图像的不同变体。
#Using the transforms module in the torchvision module, we define a set of functions that perform data augmentation on our dataset to obtain more data.#
transformers = {'train_transforms' : transforms.Compose([
transforms.Resize((224,224)),
#transforms.CenterCrop(224),
transforms.RandomRotation(20),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]),
'test_transforms' : transforms.Compose([
transforms.Resize((224,224)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]),
'valid_transforms' : transforms.Compose([
transforms.Resize((224,224)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])}
trans = ['train_transforms','valid_transforms','test_transforms']
path = "/content/gdrive/My Drive/chest_xray/"
categories = ['train','val','test']
使用 torch vision . datasets . image folder 模块,我们从数据集目录中加载图像。
dset = {x : torchvision.datasets.ImageFolder(path+x, transform=transformers[y]) for x,y in zip(categories, trans)}
dataset_sizes = {x : len(dset[x]) for x in ["train","test"]}
num_threads = 4
#By passing a dataset instance into a DataLoader module, we create dataloader which generates images in batches.
dataloaders = {x : torch.utils.data.DataLoader(dset[x], batch_size=256, shuffle=True, num_workers=num_threads)
for x in categories}
现在我们已经完成了数据集的加载,我们可以继续使用下面的代码片段来查看一些示例。
def imshow(inp, title=None):
inp = inp.numpy().transpose((1,2,0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std*inp + mean
inp = np.clip(inp,0,1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001)
inputs,classes = next(iter(dataloaders["train"]))
out = torchvision.utils.make_grid(inputs)
class_names = dataset["train"].classes
imshow(out, title = [class_names[x] for x in classes])
生成样本。请注意,您应该减少批量,以生产更大的样品供查看。
我们刚刚验证了我们的数据被正确加载,因此我们可以继续构建我们的模型。正如我前面提到的,我们将以面向对象的编程风格来构建我们的模型。我们的模型类将从 nn 继承。PyTorch 提供的模块。nn。就像 TensorFlow 等其他机器学习框架一样,模块为我们提供了构建神经网络所需的所有功能。你可以访问https://py torch . org/docs/stable/_ modules/torch/nn/modules/module . html了解更多信息。
在定义了我们的模型类并从 nn 继承之后。模块中,我们通过一种叫做迁移学习的技术,利用 ResNet-152 的特征提取器,在 init 构造函数中定义模型的图形。torchvision 模块为我们提供了在非常庞大的数据集(ImageNet)上训练过的最先进的模型,因此具有非常强大的特征提取器。Pytorch 为我们提供了获取和冻结这些强大的特征提取器的能力,根据我们的问题域附加我们自己的分类器,并训练结果模型以适应我们的问题。
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
#obtain the ResNet model from torchvision.model library
self.model = torchvision.models.resnet152(pretrained=True)
#build our classifier and since we are classifying the images into NORMAL and PNEMONIA, we output a two-dimensional tensor.
self.classifier = nn.Sequential(
nn.Linear(self.model.fc.in_features,2),
nn.LogSoftmax(dim=1))
#Requires_grad = False denies the ResNet model the ability to update its parameters hence make it unable to train.
for params in self.model.parameters():
params.requires_grad = False
#We replace the fully connected layers of the base model(ResNet model) which served as the classifier with our custom trainable classifier.
self.model.fc = self.classifier
从神经网络构建的每个模型。模块要求我们覆盖 forward 函数,我们定义了每次调用时执行的前向传递计算。
def forward(self, x):
# x is our input data
return self.model(x)
在您继续下面的部分之前,我建议您在 Pytorch 官方页面上学习这个 60 分钟的 blitz 教程:https://py torch . org/tutorials/beginner/deep _ learning _ 60min _ blitz . html但是如果您仍然选择继续,不要担心,我会尽最大努力在代码的评论中解释代码的每一点。我们训练过程的下一部分是定义一个拟合函数(不是必须在模型类中定义的),我们基本上是在数据集上训练我们的模型。让我们看看这样做的代码。
def fit(self, dataloaders, num_epochs):
#we check whether a gpu is enabled for our environment.
train_on_gpu = torch.cuda.is_available()
#we define our optimizer and pass in the model parameters(weights and biases) into the constructor of the optimizer we want. More info: https://pytorch.org/docs/stable/optim.html
optimizer = optim.Adam(self.model.fc.parameters())
#Essentially what scheduler does is to reduce our learning by a certain factor when less progress is being made in our training.
scheduler = optim.lr_scheduler.StepLR(optimizer, 4)
#criterion is the loss function of our model. we use Negative Log-Likelihood loss because we used log-softmax as the last layer of our model. We can remove the log-softmax layer and replace the nn.NLLLoss() with nn.CrossEntropyLoss()
criterion = nn.NLLLoss()
since = time.time()
#model.state_dict() is a dictionary of our model's parameters. What we did here is to deepcopy it and assign it to a variable
best_model_wts = copy.deepcopy(self.model.state_dict())
best_acc = 0.0
#we check if a gpu is enabled for our environment and move our model to the gpu
if train_on_gpu:
self.model = self.model.cuda()
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# Each epoch has a training and validation phase. We iterate through the training set and validation set in every epoch.
for phase in ['train', 'test']:
#we apply the scheduler to the learning rate in the training phase since we don't train our model in the validation phase
if phase == 'train':
scheduler.step()
self.model.train() # Set model to training mode
else:
self.model.eval() # Set model to evaluate mode to turn off features like dropout.
running_loss = 0.0
running_corrects = 0
# Iterate over batches of train and validation data.
for inputs, labels in dataloaders[phase]:
if train_on_gpu:
inputs = inputs.cuda()
labels = labels.cuda()
# clear all gradients since gradients get accumulated after every iteration.
optimizer.zero_grad()
# track history if only in training phase
with torch.set_grad_enabled(phase == 'train'):
outputs = self.model(inputs)
_, preds = torch.max(outputs, 1)
#calculates the loss between the output of our model and ground-truth labels
loss = criterion(outputs, labels)
# perform backpropagation and optimization only if in training phase
if phase == 'train':
#backpropagate gradients from the loss node through all the parameters
loss.backward()
#Update parameters(Weighs and biases) of our model using the gradients.
optimizer.step()
# statistics
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects.double() / dataset_sizes[phase]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(
phase, epoch_loss, epoch_acc))
# deep copy the model if we obtain a better validation accuracy than the previous one.
if phase == 'test' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(self.model.state_dict())
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(
time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
# load best model parameters and return it as the final trained model.
self.model.load_state_dict(best_model_wts)
return self.model
#we instantiate our model class
model = Model()
#run 10 training epochs on our model
model_ft = model.fit(dataloaders, 10)
在对模型进行一些时期的训练后,我们应该会达到验证准确性的较高值。现在我们有了一个训练好的模型,这意味着我们正在朝着我们的目标前进——在我们的模型用来识别肺炎痕迹的区分区域上绘制类激活图。
主要目标-绘制类激活图
我们之前了解到,全局平均池层将张量的高-宽维度从 h x w x d 减少到 1 x 1 x d。然后,对这个 1 x 1 x d 维度的向量/张量应用加权和,然后输入到 softmax 层,以生成类的概率-最高概率是模型预测的类。如果然后我们可以对一维向量(全局平均池层的输出)执行加权求和,以产生正确表示输入图像的另一个向量(输出概率),那么也许我们可以对 h×w×d 张量执行加权求和,最有可能是最后一个卷积层或最大池层的输出,以产生也正确表示输入图像的 h1×w1×D1 张量。输出张量 h1 x w1 x d1 包含比全局平均池层的输出的加权和更多的输入图像的空间信息。用于对全局平均池层的输出进行加权求和的权重是对应于预测类的权重。
Weighted sum of output of the Global Average Pooling layer produces our model’s predictions.
从上图我们可以看到,W1,W2 … Wn 是预测类(澳洲梗)对应的体重。为了生成预测得分的类激活图,我们可以通过权重 W1、W2 … Wn 将预测得分映射回最后一个卷积层。我们使用 W1、W2 … Wn 对最后一个卷积层的激活执行加权求和,以生成类激活图。
weighted sum on activations of last convolutional layer Generates final class activation map.
既然我们对如何生成类激活图有了一些直觉,让我们直接进入代码。
前面我们说过,为了生成类激活图,我们需要对最后一个卷积层的激活执行加权求和,但是在每次前向传递中,我们只得到最后一个全连接层的激活。为了激活最后一个卷积层,我们使用 PyTorch register_forward_hook 模块。下面的代码片段说明了如何做到这一点。
class LayerActivations():
features=[]
def __init__(self,model):
self.hooks = []
#model.layer4 is the last layer of our network before the Global Average Pooling layer(last convolutional layer).
self.hooks.append(model.layer4.register_forward_hook(self.hook_fn))
def hook_fn(self,module,input,output):
self.features.append(output)
def remove(self):
for hook in self.hooks:
hook.remove()
每当我们在实例化 LayerActivations 类后调用向前传递时,model.layer4 的输出都会被附加到 features 列表中。然后,我们可以通过调用 layer activations(model _ ft)features 来获得输出激活。
model_ft = model.model
acts = LayerActivations(model_ft)
接下来,我们加载一个测试图像,并通过我们的模型向前传递。
loader = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor()])
def image_loader(image_name):
image = PIL.Image.open(image_name).convert("RGB")
image = loader(image).float()
image = image.unsqueeze(0)
return image
image_path = '/content/gdrive/My Drive/chest_xray/test/PNEUMONIA/person100_bacteria_475.jpeg'
#load image and perform a forward pass through our model.
img = image_loader(image_path)
logps = model_ft(img.cuda() if torch.cuda.is_available() else img)
out_features = acts.features[0].squeeze(0) #since we have performed a forward pass through our model, we can obtain activations from layer(model.layer4) defined in the LayerActivation class from the features list and take out the batch dimension.
out_features = np.transpose(out_features.cpu(),(1,2,0)) # Changes shape from 2048 x 7 x7 to 7 x 7 x 2048\. Just performs a matrix transpose on the output features tensor.
打印 model.layer4(全局平均池层之前的最后一层)的输出激活大小,我们得到 7 x 7 x 2048 作为输出。我们之前说过,为了获得特定类的类激活图,我们需要获得与该类相关联的权重,并使用该权重对最后一个卷积层的激活执行加权求和。让我们用几行代码来实现。
ps = torch.exp(logps) #Our final model layer is a log-softmax activation. We perform torch.exp to take out the log and obtain the softmax values.
pred = np.argmax(ps.cpu().detach()) #Obtain the axis of the predicted class.
W = model_ft.fc[0].weight #We obtain all the weights connecting the Global Average Pooling layer to the final fully connected layer.
w = W[pred,:] # We obtain the weights associated with the predicted class which is a 2048 dimensional vector.
现在我们已经有了最后一个卷积层的激活和与预测类相关的权重,我们可以使用 np.dot 方法执行加权求和。点函数只是在两个数组或张量上执行点积。
cam = np.dot(out_features.detach(),w.detach().cpu())
#dot product between a 7x7x2048 tensor and a 2048 tensor yields a 7x7 tensor.
#cam will therefore have a shape of 7x7.
从上面提出的理论来看,cam 似乎是我们的类激活图,是的。但是 cam 是一个 7x7 张量,我们需要放大它以适应我们的图像。这就是 Scipy 软件包的用武之地。Scipy 的 ndimg 包为我们提供了一个缩放功能,我们可以使用它将我们的 cam 张量从 7x7 上采样到 224x224,这是我们输入图像的大小。让我们看看如何。
class_activation = ndimg.zoom(cam, zoom=(32,32),order=1)
#zoom is the number of times we scale up our cam tensor. (7x32, 7x32) = (224,224)
让我们绘制输入图像和 class_activation 来查看我们的输出。
img = np.squeeze(img, axis=0) #removes the batch dimension from the input image (1x3x224x224) to (3x224x224)
img = np.transpose(img,(1,2,0)) #matplotlib supports channel-last dimensions so we perform a transpose operation on our image which changes its shape to (224x224,3)
#we plot both input image and class_activation below to get our desired output.
plt.imshow(img, cmap='jet',alpha=1) #jet indicates that color-scheme we are using and alpha indicates the intensity of the color-scheme
plt.imshow(class_activation,cmap='jet',alpha=0.5)
瞧啊。
让我们来看看模型归类为正常的一些图像。
从这几幅图像中,我们可以观察到模型正在观察一个特定区域来识别肺炎图像和完全不同的区域来识别正常图像。现在可以有把握地说,我们的模型已经学会区分有肺炎痕迹的胸部 x 光扫描和没有肺炎痕迹的胸部 x 光扫描。
后续步骤
我们刚刚将深度学习应用于目前正在研究的一个非常重要的领域,医学图像分析。我们能够用不足的数据集建立一个强大的模型。斯坦福大学的研究人员最近开放了他们庞大的胸部 x 光数据集,足以建立一个比我们已经建立的更强大的模型。你可能也想试试这个链接:https://stanfordmlgroup.github.io/competitions/chexpert/我们已经太专注于胸部 x 光分析了。你可能想把你的注意力放在斯坦福大学的数据集上,那里的骨骼 x 光扫描分析了异常情况。
链接:https://stanfordmlgroup.github.io/competitions/mura/
由于
- 非常感谢 Alexis Cook 提供的关于全局平均池和类激活图的精彩教程。https://alexisbcook . github . io/2017/global-average-pooling-layers-for-object-localization/
- 此外,我要感谢斯坦福大学的 ML 研究小组,感谢他们如此伟大和激励人心的项目,也感谢他们开源他们的数据集
关于我
我是一名本科生,目前在读电气电子工程。我也是一个深度学习爱好者和作家。我的工作主要集中在计算机视觉在医学图像分析中的应用。我希望有一天能打入自动驾驶汽车领域。你可以在推特(@henryansah083)上关注:https://twitter.com/henryansah083?s=09LinkedIn:https://www.linkedin.com/in/henry-ansah-6a8b84167/
降维-自动编码器
原文:https://blog.paperspace.com/dimension-reduction-with-autoencoders/
本教程是关于降维的 7 部分系列的一部分:
- 理解主成分分析(PCA)的降维
- 利用独立成分分析(ICA)深入降低维度
- 多维标度(MDS)
- 【T0 向】T1
- t-SNE
- IsoMap
- Autoencoders
(这篇文章假设你有神经网络的工作知识。有代码的笔记本可在 github repo 获得
自动编码器可以定义为神经网络,其主要目的是学习数据集中的基础流形或特征空间。自动编码器试图在输出端重建输入。与其他非线性降维方法不同,自动编码器不努力保持单一属性,如距离(MDS)、拓扑(LLE)。自动编码器通常由两部分组成:将输入转换为隐藏代码的编码器和从隐藏代码重构输入的解码器。自动编码器的一个简单例子就是下图所示的神经网络。
有人可能会问“如果输出和输入一样,自动编码器有什么用?如果最终结果与输入相同,特征学习或降维是如何发生的?”。
自动编码器背后的假设是,转换input --> hidden --> input
将帮助我们了解数据集的重要属性。我们要学习的性质反过来又取决于对网络的限制。
自动编码器的类型
让我们来讨论几种流行的自动编码器。
- 正则化自动编码器:这些类型的自动编码器在其损失函数中使用各种正则化项来实现所需的属性。
隐藏代码的大小可以大于输入大小。1.1 稀疏自动编码器-稀疏自动编码器增加了隐藏层稀疏性的惩罚。正则化迫使隐藏层仅激活每个数据样本的一些隐藏单元。通过激活,我们意味着如果第 j 个隐藏单元的值接近 1,则它被激活,否则被去激活。从去激活的节点到下一层的输出为零。这种限制迫使网络仅压缩和存储数据的重要特征。稀疏自动编码器的损失函数可以表示为
L(W,b) = J(W,b) +正则化项
中间层表示隐藏层。绿色和红色节点分别表示停用和激活的节点。
1.2 去噪自动编码器:在去噪自动编码器中,随机噪声被故意添加到输入中,并且网络被迫重建未掺杂的输入。解码器功能学会抵抗输入中的微小变化。这种预训练产生了在一定程度上不受输入中噪声影响的健壮的神经网络。
标准正态函数被用作噪声函数来产生被破坏的输入。
1.3 收缩自动编码器:收缩自动编码器不是向输入添加噪声,而是在特征提取函数的导数的大值上添加惩罚。当输入的变化不显著时,小的特征提取函数(f(x))导数值导致可忽略的特征变化。在收缩编码器中,特征提取功能是鲁棒的,而在去噪编码器中,解码器功能是鲁棒的。
2。变分自动编码器:变分自动编码器基于非线性潜变量模型。在潜在变量模型中,我们假设可观察的 x 是从隐藏变量 y 中产生的。这些隐藏变量 y 包含了关于数据的重要属性。这些自动编码器由两个神经网络组成,第一个用于学习潜在变量分布,第二个用于从潜在变量分布获得的随机样本中产生可观测值。除了最小化重建损失之外,这些自动编码器还最小化潜在变量的假设分布和由编码器产生的分布之间的差异。它们在生成图像方面非常受欢迎。
潜在变量分布的一个好选择是高斯分布。如上图所示,编码器输出假设高斯的参数。接下来,从高斯分布中提取随机样本,解码器从随机样本中重构输入。
3。欠完整自动编码器:在欠完整自动编码器中,隐藏层的大小小于输入层。通过减小隐藏层的大小,我们迫使网络学习数据集的重要特征。一旦训练阶段结束,解码器部分被丢弃,编码器被用于将数据样本变换到特征子空间。如果解码器变换是线性的并且损失函数是 MSE(均方误差),则特征子空间与 PCA 的特征子空间相同。对于一个学习有用东西的网络来说,隐藏代码的大小不应该接近或大于网络的输入大小。还有,一个高容量(深度和高度非线性)的网络,可能学不到什么有用的东西。降维方法基于这样的假设,即数据的维度被人为地膨胀,而其固有维度要低得多。随着我们在自动编码器中增加层数,隐藏层的尺寸将不得不减小。如果隐藏层的大小变得小于数据的固有维度,将会导致信息丢失。解码器可以学习将隐藏层映射到特定的输入,因为层数很大并且是高度非线性的。
多层编码器和解码器的图像。下面显示了一个简单的自动编码器。
欠完整自动编码器的损失函数由下式给出:
L(x,g(f(x)))=(x-g(f(x)))²
因为这篇文章是关于使用自动编码器降维的,我们将在 pyspark 上实现欠完整自动编码器。【spark 的开源深度学习库很少。例如英特尔的 bigdl ,雅虎的 tensorflowonspark ,databricks 的 spark 深度学习。
我们将使用英特尔的 bigdl。
步骤 1 安装 bigdl
如果你已经安装了 spark run pip install --user bigdl --no-deps
否则运行pip install --user bigdl
。在后一种情况下,pip 将安装 pyspark 和 bigdl。
第二步。必要的进口
%matplotlib inline
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
# some imports from bigdl
from bigdl.nn.layer import *
from bigdl.nn.criterion import *
from bigdl.optim.optimizer import *
from bigdl.util.common import *
from bigdl.dataset.transformer import *
from pyspark import SparkContext
sc=(SparkContext.getOrCreate(
conf=create_spark_conf().
setMaster("local[4]")>
set("spark.driver.memory","2g")))
# function to initialize the bigdl library
init_engine()
第三步。加载并准备数据
# bigdl provides a nice function for
# downloading and reading mnist dataset
from bigdl.dataset import mnist
mnist_path = "mnist"
images_train, labels_train = mnist.read_data_sets(mnist_path, "train")
# mean and stddev of the pixel values
mean = np.mean(images_train)
std = np.std(images_train)
# parallelize, center and scale the images_train
rdd_images = (sc.parallelize(images_train).
map(lambda features: (features - mean)/std))
print("total number of images ",rdd_images.count())
步骤 3 为模型创建函数
# Parameters for training
BATCH_SIZE = 100
NUM_EPOCHS = 2
# Network Parameters
SIZE_HIDDEN = 32
# shape of the input data
SIZE_INPUT = 784
# function for creating an autoencoder
def get_autoencoder(hidden_size, input_size):
# Initialize a sequential type container
module = Sequential()
# create encoder layers
module.add(Linear(input_size, hidden_size))
module.add(ReLU())
# create decoder layers
module.add(Linear(hidden_size, input_size))
module.add(Sigmoid())
return(module)
步骤 4 建立深度学习图表
undercomplete_ae = get_autoencoder( SIZE_HIDDEN, SIZE_INPUT)
# transform dataset to rdd(Sample) from rdd(ndarray).
# Sample represents a record in the dataset. A sample
# consists of two tensors a features tensor and a label tensor.
# In our autoencoder features and label will be same
train_data = (rdd_images.map(lambda x:
Sample.from_ndarray(x.reshape(28*28),
x.reshape(28*28))))
# Create an Optimizer
optimizer = Optimizer(
model = undercomplete_ae,
training_rdd = train_data,
criterion = MSECriterion(),
optim_method = Adam(),
end_trigger = MaxEpoch(NUM_EPOCHS),
batch_size = BATCH_SIZE)
# write summary
app_name='undercomplete_autoencoder-'+dt.datetime.now().strftime("%Y%m%d-%H%M%S")
train_summary = TrainSummary(log_dir='/tmp/bigdl_summary',
app_name=app_name)
optimizer.set_train_summary(train_summary)
print("logs to saved to ",app_name)
第五步训练模型
# run training process
trained_UAE = optimizer.optimize()
第六步根据测试数据模拟性能
# let's check our model performance on the test data
(images, labels) = mnist.read_data_sets(mnist_path, "test")
rdd_test = (sc.parallelize(images).
map(lambda features: ((features -
mean)/std).reshape(28*28)).map(
lambda features: Sample.
from_ndarray(features, features)))
examples = trained_UAE.predict(rdd_test).take(10)
f, a = plt.subplots(2, 10, figsize=(10, 2))
for i in range(10):
a[0][i].imshow(np.reshape(images[i], (28, 28)))
a[1][i].imshow(np.reshape(examples[i], (28, 28)))
正如我们从图像中看到的,重建非常接近原始输入。
结论:通过这篇文章,我们讨论了自动编码器如何用于降维。一开始,我们讨论了不同类型的自动编码器及其用途。后来,我们使用 intel 的 bigdl 和 pyspark 实现了一个欠完整的自动编码器。更多关于 bigdl 的教程,请访问 bigdl 教程
这篇文章结束了我们关于降维的系列文章。
利用独立分量分析(ICA)更深入地降低维数
原文:https://blog.paperspace.com/dimension-reduction-with-independent-components-analysis/
本教程是关于降维的 7 部分系列的一部分:
-
LLE (即将推出!)
-
t-SNE (即将推出!)
-
IsoMap (即将推出!)
-
自动编码器(即将推出!)
(在 github 上有一个带数学和代码的 IPython 笔记本。)
今天,我们将学习另一种叫做 ICA 的降维方法。ICA 是一种线性降维方法,它将数据集转换成独立分量的列。盲源分离和“鸡尾酒会问题”是它的其他名称。ICA 是神经成像、fMRI 和 EEG 分析中的重要工具,有助于将正常信号与异常信号分开。那么,ICA 到底是什么?
ICA 代表独立成分分析。它假设每个数据样本都是独立成分的混合物,并且它的目标是找到这些独立成分。ICA 的核心是“独立”。我们应该先试着理解这一点。
独立在 ICA 的上下文中意味着什么?什么时候我们可以有把握地说两个变量是独立的?与‘关联’有什么不同?最后,你如何衡量独立的程度?
假设 x 、 y 是两个随机变量,它们的分布函数分别由P[x]T7、P[y]给出。如果我们收到一些关于 x 的信息,但这并没有改变我们对 y 的任何知识,那么我们可以有把握地说 x,y 是独立变量。现在你会说“打住,这就是你说的关联缺失”。是的,你是对的,但只是部分对。相关性不是衡量两个变量之间依赖关系的唯一手段。事实上,相关性捕捉的是线性相关性。如果两个变量是独立的,那么线性和非线性相关性都为零。没有线性相关性并不意味着独立,因为可能存在非线性关系。我们举个小例子来理解这一点。****
假设 x (-5,-4,-3,-2,-1,0,1,2,3,4,5) &
y = x² 这就给了我们 y (25,16,9,4,1,0,1,4,9,16,25)。现在,计算这两个变量之间的相关性。
import numpy as np
x = np.array([-5,-4,-2,-1,0,1,2,3,4,5])
y = np.array([25,16,9,4,1,0,1,4,9,16,25])
np.correlate(x,y)
0.0
正如你所看到的,在上面的例子中,相关性是 0,尽管他们有一个非线性的关系。因此,两个变量之间的独立性意味着零相关,但反之则不然。
让我们回到今天的话题。如前所述,ICA 试图找出构成数据的独立来源。我们将从一个经典的例子开始解释 ICA 及其工作原理。
上图所示的爱丽丝和鲍勃,两人同时在说话。两个话筒分别从爱丽丝和鲍勃接收输入 S1 & S2。ICA 假设混合过程是线性的,即它可以表示为矩阵乘法。每个话筒根据矩阵 a 给出的位置和设置混合 S1 & S2。矩阵运算产生矢量 M 作为输出。现在,你想把 S1 的 S2 和 M1 的 M2 分开。这被称为酒会问题或盲源分离。
如果矩阵 A 已知,这个问题的解是简单的。一个简单的矩阵求逆,然后乘以 M,就会给出答案。但是在现实世界中,矩阵 A 通常是未知的。我们仅有的信息是混合过程的输出。
ICA 解决这个问题的方法基于三个假设。这些是:
- 混合过程是线性的。
- 所有源信号都是相互独立的。
- 所有源信号都具有非高斯分布。
我们已经讨论了前两个假设。让我们来谈谈 ICA 的第三个假设:源信号的非高斯性。
这个假设的基础来自于中心极限定理。根据中心极限定理,独立随机变量之和比独立变量更高斯。所以要推断源变量,我们必须远离高斯。在高斯分布的情况下,不相关的高斯变量也是独立的,这是与高斯分布相关的独特性质。
让我们举一个简单的例子来理解这个概念。首先,创建四个数据集——两个来自高斯分布,两个来自均匀分布。
np.random.seed(100)
U1 = np.random.uniform(-1, 1, 1000)
U2 = np.random.uniform(-1, 1, 1000)
G1 = np.random.randn(1000)
G2 = np.random.randn(1000)
%matplotlib inline
# let's plot our signals
from matplotlib import pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(121, aspect = "equal")
ax1.scatter(U1, U2, marker = ".")
ax1.set_title("Uniform")
ax2 = fig.add_subplot(122, aspect = "equal")
ax2.scatter(G1, G2, marker = ".")
ax2.set_title("Gaussian")
plt.show()
现在,混合 U1 & U2 和 G1 & G2 以创建输出 U_mix 和 G_mix。
# now comes the mixing part. we can choose a random matrix for the mixing
A = np.array([[1, 0], [1, 2]])
U_source = np.array([U1,U2])
U_mix = U_source.T.dot(A)
G_source = np.array([G1, G2])
G_mix = G_source.T.dot(A)
# plot of our dataset
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.set_title("Mixed Uniform ")
ax1.scatter(U_mix[:, 0], U_mix[:,1], marker = ".")
ax2 = fig.add_subplot(122)
ax2.set_title("Mixed Gaussian ")
ax2.scatter(G_mix[:, 0], G_mix[:, 1], marker = ".")
plt.show()
U_mix 和 G_mix 是我们在现实世界场景中拥有的。从两种混合物中去除线性相关性。
# PCA and whitening the dataset
from sklearn.decomposition import PCA
U_pca = PCA(whiten=True).fit_transform(U_mix)
G_pca = PCA(whiten=True).fit_transform(G_mix)
# let's plot the uncorrelated columns from the datasets
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.set_title("PCA Uniform ")
ax1.scatter(U_pca[:, 0], U_pca[:,1], marker = ".")
ax2 = fig.add_subplot(122)
ax2.set_title("PCA Gaussian ")
ax2.scatter(G_pca[:, 0], G_pca[:, 1], marker = ".")
注意不相关(PCA 均匀,PCA 高斯 2)和源图(均匀,高斯)之间的差异。在高斯的情况下,它们看起来很相似,而不相关的均匀需要旋转才能到达那里。通过去除高斯情况下的相关性,我们实现了变量之间的独立性。如果源变量是高斯型的,则不需要 ICA,PCA 就足够了。
我们如何衡量和消除变量之间的非线性相关性?
变量之间的非线性相关性可以通过变量之间的互信息来度量。互信息越高,依赖性就越高。Mutual information = sum of entropies of marginal distribution - entropy of the joint distribution
熵是分布中不确定性的度量。变量 x 的熵由H(x) = sum(log(P(x))*P(x)) for every possible of value of x
给出。
高斯分布的熵最高。与熵密切相关的一个术语是负熵,表述为
negentropy(x) = H(x_gaussian) - H(x)
。这里 x_gaussian 是与 x 具有相同协方差的高斯随机向量,因此,如果 x 是高斯随机变量,则负熵总是非零且等于零。
还有,mutual information(y1,y2) = constant - sum(negentropy(yi))
负熵和互信息的计算需要熵的知识。熵计算需要未知的概率分布函数。我们可以用一些合适的函数来近似负熵。一些常见的例子有 tanh(ay)、-exp(-y² )和-y*exp(-y² )。
伪码 ICA
G&G分别为逼近函数及其导数。x 是数据集。
- 初始化
- X =五氯苯甲醚(X)
- 而 W 变化:
W = average(X * G(WX))-average(G(W^TX))W
W =正交化(W) - 返回 S = WX
正交化是使矩阵的列正交的过程。
应该选择多少个独立元件?应该选择哪些独立组件?
ICA 输出一个列为独立源的源矩阵。它从来没有告诉我们一个组件是重要的还是不相关的。如果列数较少,建议检查每个组件。对于大量组件,应在 PCA 阶段进行选择(2^和步骤)。如果您不熟悉 PCA,请查看本系列的后 1 部分。
让我们在 PySpark 中实现这个算法。我们将创建几个信号,然后将它们混合起来,以获得适合 ICA 分析的数据集。
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
np.random.seed(0)
num_rows = 3000
t = np.linaspace(0,10, n_samples)
# create signals sources
s1 = np.sin(3*t) # a sine wave
s2 = np.sign(np.cos(6*time)) # a square wave
s3 = signal.sawtooth(2 *t) # a sawtooth wave
# combine single sources to create a numpy matrix
S = np.c_[s1,s2,s3]
# add a bit of random noise to each value
S += 0.2 no.random.normal(size = S.shape)
# create a mixing matrix A
A = np.array([[1, 1.5, 0.5], [2.5, 1.0, 2.0], [1.0, 0.5, 4.0]])
X = S.dot(A.T)
#plot the single sources and mixed signals
plt.figure(figsize =(26,12) )
colors = ['red', 'blue', 'orange']
plt.subplot(2,1,1)
plt.title('True Sources')
for color, series in zip(colors, S.T):
plt.plot(series, color)
plt.subplot(2,1,2)
plt.title('Observations(mixed signal)')
for color, series in zip(colors, X.T):
plt.plot(series, color)
对数据集进行 PCA 和白化编码。
from pyspark.mllib.linalg.distributed import IndexedRowMatrix, IndexedRow, BlockMatrix
from pyspark.mllib.feature import StandardScaler
from pyspark.mllib.linalg import Vectors, DenseMatrix, Matrix
from sklearn import datasets
# create the standardizer model for standardizing the dataset
X_rdd = sc.parallelize(X).map(lambda x:Vectors.dense(x) )
scaler = StandardScaler(withMean = True, withStd = False).fit(iris_rdd)
X_sc = scaler.transform(X_rdd)
#create the IndexedRowMatrix from rdd
X_rm = IndexedRowMatrix(X_sc.zipWithIndex().map(lambda x: (x[1], x[0])))
# compute the svd factorization of the matrix. First the number of columns and second a boolean stating whether
# to compute U or not.
svd_o = X_rm.computeSVD(X_rm.numCols(), True)
# svd_o.V is of shape n * k not k * n(as in sklearn)
P_comps = svd_o.V.toArray().copy()
num_rows = X_rm.numRows()
# U is whitened and projected onto principal components subspace.
S = svd_o.s.toArray()
eig_vals = S**2
# change the ncomp to 3 for this tutorial
#n_comp = np.argmax(np.cumsum(eig_vals)/eig_vals.sum() > 0.95)+1
n_comp = 3
U = svd_o.U.rows.map(lambda x:(x.index, (np.sqrt(num_rows-1)*x.vector).tolist()[0:n_comp]))
# K is our transformation matrix to obtain projection on PC's subspace
K = (U/S).T[:n_comp]
现在,计算独立分量的代码。
import pyspark.sql.functions as f
import pyspark.sql.types as t
df = spark.createDataFrame(U).toDF("id", "features")
# Approximating function g(y) = x*exp(-x**2/2) and its derivative
def g(X):
x = np.array(X)
return(x * np.exp(-x**2/2.0))
def gprime(Y):
y = np.array(Y)
return((1-y**2)*np.exp(-y**2/2.0))
# function for calculating step 2 of the ICA algorithm
def calc(df):
function to calculate the appoximating function and its derivative
def foo(x,y):
y_arr = np.array(y)
gy = g(y_arr)
gp = gprime(y_arr)
x_arr = np.array(x)
res = np.outer(gy,x_arr)
return([res.flatten().tolist(), gp.tolist()])
udf_foo = f.udf(foo, t.ArrayType(t.ArrayType(t.DoubleType())))
df2 = df.withColumn("vals", udf_foo("features","Y"))
df2 = df2.select("id", f.col("vals").getItem(0).alias("gy"), f.col("vals").getItem(1).alias("gy_"))
GY_ = np.array(df2.agg(f.array([f.sum(f.col("gy")[i])
for i in range(n_comp**2)])).collect()[0][0]).reshape(n_comp,n_comp)/num_rows
GY_AVG_V = np.array(df2.agg(f.array([f.avg(f.col("gy_")[i])
for i in range(n_comp)])).collect()[0][0]).reshape(n_comp,1)*V
return(GY_, GY_AVG_V)
np.random.seed(101)
# Initialization
V = np.random.rand(n_comp, n_comp)
# symmetric decorelation function
def sym_decorrelation(V):
U,D,VT = np.linalg.svd(V)
Y = np.dot(np.dot(U,np.diag(1.0/D)),U.T)
return np.dot(Y,V)
numIters = 10
V = sym_decorrelation(v_init)
tol =1e-3
V_bc = sc.broadcast(V)
for i in range(numIters):
# Y = V*X
udf_mult = f.udf(lambda x: V_bc.value.dot(np.array(x)).tolist(), t.ArrayType(t.DoubleType()))
df = df.withColumn("Y", udf_mult("features"))
gy_x_mean, g_y_mean_V = calc(df)
V_new = gy_x_mean - g_y_mean_V
V_new = sym_decorrelation( V_new )
#condition for convergence
lim = max(abs(abs(np.diag(V_new.dot(V.T)))-1))
V = V_new
# V needs to be broadcasted after every change
V_bc = sc.broadcast(V)
print("i= ",i," lim = ",lim)
if lim < tol:
break
elif i== numIters:
print("Lower the tolerance or increase the number of iterations")
#calculate the unmixing matrix for dataset
W = V.dot(K)
#now multiply U with V to get source signals
S_ = df.withColumn("Y", udf_mult("features"))
绘制结果 S_
plt.title('Recovered source Signals')
for color, series in zip(colors, S_.T):
plt.plot(series, color)
ICA 的缺点 : ICA 不能揭示数据集的非线性关系。ICA 没有告诉我们任何关于独立组件的顺序或者它们中有多少是相关的。
结论:在这篇文章中,我们学习了独立成分分析的实用方面。我们触及了一些与理解 ICA 相关的重要话题,如高斯性和独立性。之后,在 pyspark 上实现了 ICA 算法,并在玩具数据集上进行了实验。
如果你想了解更多关于 ICA 及其应用的知识,试试关于 fMRI 和 EEG 数据的 ICA 论文。
本系列的下一篇文章将讨论多维缩放
降维- IsoMap
原文:https://blog.paperspace.com/dimension-reduction-with-isomap/
本教程是关于降维的 7 部分系列的一部分:
- 理解主成分分析(PCA)的降维
- 利用独立成分分析(ICA)深入降低维度
- 多维标度(MDS)
- 【T0 向】T1
- t-SNE
- IsoMap
- Autoencoders
(带有数学和代码的 jupyter 笔记本(spark)可在 github repo 上获得)
Isomap 代表等距映射。Isomap 是一种基于谱理论的非线性降维方法,它试图在低维中保持测地线距离。Isomap 从创建一个邻居网络开始。之后,它使用图距离来近似所有点对之间的测地线距离。然后,通过测地距离矩阵的特征值分解,找到数据集的低维嵌入。在非线性流形中,当且仅当邻域结构可以近似为线性时,欧几里德距离度量才成立。如果邻域包含空洞,那么欧几里德距离可能会产生很大的误导。与此相反,如果我们通过遵循流形来测量两点之间的距离,我们将更好地近似两点之间的距离。让我们用一个极其简单的二维例子来理解这一点。假设我们的数据位于一个二维结构的圆形流形上,如下图所示。
为什么非线性流形中的测地线距离优于欧氏距离?
我们将使用欧几里得距离和近似测地线距离将数据简化为一维。现在,如果我们看看基于欧几里德度量的 1-D 映射,我们看到对于相距很远的点(a & b)映射得很差。只有可以近似位于线性流形(c & d)上的点才能给出满意的结果。另一方面,看看测地线距离的映射,它很好地将近点近似为邻居,将远点近似为远点。
图像中两点间的测地线距离用两点间的图形距离来近似。因此,欧几里得距离不应用于近似非线性流形中两点之间的距离,而测地线距离可以使用。
Isomap 使用上述原理来创建特征值分解的相似矩阵。不像其他非线性降维方法,如 LLE 和 LPP 只使用局部信息,isomap 使用局部信息来创建全局相似性矩阵。isomap 算法使用欧几里得度量来准备邻域图。然后,它通过使用图距离测量两点之间的最短路径来近似两点之间的测地线距离。因此,在低维嵌入中,它近似数据集的全局和局部结构。
让我们对实现 Isomap 算法所需的一些概念有一个基本的了解。
Pregel API——Pregel 是 Google 开发的用于处理大规模图形的分布式编程模型。它是 Apache giraph 项目和 spark 的 GraphX 库背后的灵感。Pregel 基本上是一个消息传递接口,它基于一个顶点的状态应该依赖于它的邻居的想法。预凝胶计算将图形和一组顶点状态作为输入。在称为超级步骤的每次迭代中,它处理在顶点接收的消息并更新顶点状态。之后,它决定它的哪个邻居应该在下一个超步骤接收该消息,以及该消息应该是什么。因此,消息沿着边传递,计算只发生在顶点。该图不是仅通过网络传递的消息。计算在最大迭代次数或没有消息要传递时停止。我们用一个简单的例子来理解一下。假设,我们需要找到下图中每个顶点的度数。下图显示了预凝胶模型的单次迭代。
在初始化时,每个顶点的度数都是 0。我们可以发送一个空消息作为初始消息来开始计算。在超级步骤 1 结束时,每个顶点通过其每条边发送消息 1。在下一个超步骤中,每个顶点对收到的消息求和并更新其度数。
经典 MDS - Isomap 与 Torgerson 和 Gower 提出的原始多维标度算法密切相关。实际上,它是经典多维标度的扩展。经典的多维算法给出了降维问题的闭式解。经典 MDS 使用欧几里得距离作为相似性度量,而 isomap 使用测地线距离。古典 MDS 的舞步是
- 从给定的 X 创建相异度的平方δ²(X)的矩阵。
- 通过对相异矩阵 B = 0.5 *(J δ² J)进行双重定心来获得矩阵 B
- 计算矩阵 B 的特征值分解,B[δ]= QλQ’
- 选择具有 K 个最高特征值的 K 个特征向量。
IsoMap 的步骤 :
Isomap 仅在最初的几个步骤上不同于传统的 MDS。它使用图形距离,而不是使用欧几里得度量来表示不相似性。Isomap 算法的步骤是:
-
邻域图:从数据集创建邻域图和邻接矩阵。
-
相异度矩阵:邻域搜索之后,我们将使用 spark 的 graphX 库来计算点之间的测地线距离。在创建我们的邻居网络时,我们必须确保生成的图是单个连通的部分。如果没有,那么我们的相似性矩阵将保持不完整,结果将是不一致的。我们需要迭代不同的邻域选择参数值来获得完全连通图。到目前为止,spark 还没有加权图的最短路径函数。我们必须执行它。下面的代码展示了一个使用 pregel 的最短路径算法,比如 graphX 的 api。代码来自 graphX lib 的未加权图的最短路径函数。函数 addMaps 和 sendMessage 已经修改为支持加权图。
def ShortestPath(Verts: RDD[(VertexId, imMap[Long, Double])], Edges: RDD[Edge[Double]], landmarks: Seq[Long] = Seq()): Graph[imMap[Long,Double],Double] = { val g = Graph(Verts, Edges) type SPMap = Map[VertexId, Double] def makeMap(x: (VertexId, Double)*) = Map(x: _*) def incrementMap(spmap1: SPMap, spmap2: SPMap, d: Double): SPMap = { spmap1.map { case (k, v) => if (v + d < spmap2.getOrElse(k, Double.MaxValue)) k -> (v + d) else -1L -> 0.0 } } def addMaps(spmap1: SPMap, spmap2: SPMap): SPMap = { (spmap1.keySet ++ spmap2.keySet).map { k => k -> math.min(spmap1. getOrElse(k, Double.MaxValue), spmap2.getOrElse(k, Double.MaxValue)) }(collection.breakOut) } var spGraph: Graph[imMap[Long,Double],Double] = null if (landmarks.isEmpty){ spGraph = g.mapVertices { (vid, attr) => makeMap(vid -> 0)} } else{ spGraph = g.mapVertices { (vid, attr) => if (landmarks.contains(vid)) makeMap(vid -> 0) else makeMap()} } val initialMessage = makeMap() def vertexProgram(id: VertexId, attr: SPMap, msg: SPMap): SPMap = { addMaps(attr, msg) } def sendMessage(edge: EdgeTriplet[SPMap, Double]): Iterator[(VertexId, SPMap)] = { val newAttr = incrementMap(edge.srcAttr, edge.dstAttr, edge.attr) - (-1) if (!newAttr.isEmpty) Iterator((edge.dstId, newAttr)) else Iterator.empty } val h = Pregel(spGraph, initialMessage)(vertexProgram, sendMessage, addMaps) return(h) }
特征值分解:在特征值分解之前,我们要对距离进行平方,并对平方后的相似度矩阵进行双居中。在特征值分解之后,选择具有 K 个最高特征值的前 K 个特征向量。
isomap 降维后 MNIST 数据集子集的图。
我们实现的是普通版本的 isomap。这需要大量的时间和计算能力。它有两个瓶颈:第一,相异矩阵的计算需要 O(N² 次运算,其中 N 是样本的数量;第二,成对图形距离的计算。如果 N 很大,这在大数据集的情况下通常是正确的,这就变得不切实际了。这个问题的解决方案是地标 Isomap。地标 isomap 基于地标 MDS。地标 MDS 选择一组称为地标的点,并在其上实现经典 MDS。基于从经典 MDS 获得的映射,使用基于距离的三角测量在低维嵌入中映射剩余的点。
地标经典缩放的步骤
- 选择地标点 X[地标点]
- 对标志点应用经典 MDS,得到低维插值 L[k]
- 计算δ[u] 其中δ[ui] 是标志点相异矩阵的第 i[行]的平均值。
- 给定向量 x[a] 计算δ[a] 其中δ[ai] 是点 x[a] 和标志点 i 之间的平方距离
- 对 x[a] 的低维嵌入由 y[a]= 0.5 * L^(-1)-6】k(δ[a]-δ[u]给出其中 L ^(-1)[k] 是 L[k]
的 penrose moore 逆选择标志点可以是随机的或者通过特定的方法。为了获得 K 维嵌入,至少需要 K+1 个标志点。出于与算法稳定性相关的原因,所选择的标志点的数量应该多于严格的最小值。地标 isomap 中等距映射的准确性不会由于算法中的近似而受到太大影响。
Isomap 的缺点:当流形没有被很好地采样并且包含孔洞时,Isomap 的性能很差。如前所述,邻域图的创建很复杂,稍有错误的参数就会产生不好的结果。
结论:在本文中,我们讨论了另一种流形学习算法 IsoMap(等距映射)。
在帖子的开头,我们谈到了什么是等距映射,以及它与其他降维算法有何不同。然后,我们简单讨论了一下 pregel API。后来,我们使用 spark 的 GraphX 库在 scala 中实现了一个 isomap 算法。对于希望深入研究分布式图形处理的人来说,GraphX 是一个很好的起点。
本系列的下一篇文章将是关于自动编码器
降维- LLE
原文:https://blog.paperspace.com/dimension-reduction-with-lle/
本教程是关于降维的 7 部分系列的一部分:
(带有数学和代码(python 和 pyspark)的 jupyter 笔记本可在 github repo 上获得)
LLE 是一种拓扑保持的流形学习方法。所有流形学习算法都假设数据集位于低维的光滑非线性流形上,并且映射f:RD->RD(D>>D)可以通过保留高维空间的一个或多个属性来找到。拓扑保留意味着邻域结构是完整的。不像,像 MDS 拓扑保持这样的距离保持方法更直观。高维空间中相邻的点在低维空间中应该是靠近的。《LLE》背后的灵感来源于原创作者所说的“放眼全球,立足本地”。像 SOM(自组织映射)这样的方法也是拓扑保持的,但是它们为下流形假设了预定义的格。LLE 根据数据集中包含的信息创建格网。上图显示了一个模拟数据集及其 LLE 嵌入。模拟 S 曲线的几个点被圈起来。他们是同一点的邻居。我们可以看到,在 LLE 嵌入中,它们被映射得彼此靠近(圆中的点)。这显示了 LLE 在保存邻里结构方面的能力。LLE 几乎没有做出什么重要的假设。这些是:
- 数据采样良好,即数据集密度高。
- 数据集位于光滑流形上。
数据的良好采样意味着对于每个点,在其邻域内至少有 2d 个点。如果流形是光滑的,我们可以假设该点及其邻域位于局部线性流形上。如果有急弯或洞,这个属性就不成立。
LLE 的实施
必要的进口。
#necessary imports
from sklearn import datasets
from pyspark.sql import SQLContext as SQC
from pyspark.mllib.linalg import Vectors as mllibVs, VectorUDT as mllibVUDT
from pyspark.ml.linalg import Vectors as mlVs, VectorUDT as mlVUDT
from pyspark.sql.types import *
from pyspark.mllib.linalg import *
from pyspark.sql.functions import *
from pyspark.ml.feature import StandardScaler
from pyspark.mllib.linalg.distributed import IndexedRowMatrix
import math as m
import numpy as np
LLE 的脚步:
- 创建邻域图
- 对于每个点,计算局部权重矩阵 W
- 对于每个点,使用 W 从它的邻居创建点。
LLE 的第一步:邻居图:
LLE 的第一步是创建邻居图。需要距离度量来测量两点之间的距离并将它们归类为邻居。例如欧几里德、马哈拉诺比斯、汉明和余弦。
邻域矩阵:可以使用e-邻域、K-最近邻居、位置敏感哈希创建邻域矩阵。
e-邻域:如果节点[i] &节点[j] 之间的距离小于一个固定参数 e ,则在它们之间创建一条边。 e 既不能小,也不能大。如果 e 大,则每个节点都有一条边与其他节点相连,而它的小很多节点都没有邻居。
K-最近邻:使用这种方法,对于每个节点,我们选择 K 个最近的数据点作为它的邻居。这种方法确保每个节点都有 K 个邻居。如果数据点的密度变化很大,这种方法将产生不对称的邻域图。例如,节点[i] 可能被远处的点选择作为其邻居,因为该点的密度较低。
局部敏感散列:与上述两种方法不同,局部敏感散列是一种选择邻居的近似方法。通俗地说,位置敏感散列使用一组散列函数来散列每个点,并且被散列到相同桶的所有点被分类为邻居。如果你想更多地了解 LSH,网上有很多好的教程。
# for every point create an id sorted vector of distance from every other point
num_samples = 3000
X, color = datasets.make_s_curve(num_samples)
df = (spark.createDataFrame(sc.parallelize(X.tolist()).zipWithIndex().
map(lambda x: (x[1], mlVs.dense(x[0]))), ["id", "features"]))
udf_dist = udf(lambda x, y: float(x.squared_distance(y)), DoubleType())
df_2 = df
df = df.crossJoin(df ).toDF('x_id', 'x_feature', 'y_id', 'y_feature')
df = df.withColumn("sim", udf_dist(df.x_feature, df.y_feature))
df = df.drop("x_feature")
st = struct([name for name in ["y_id", "sim","y_feature"]]).alias("map")
df = df.select("x_id", st)
df = df.groupby("x_id").agg(collect_list("map").alias("map"))
df = df.join(df_2, df_2.id == df.x_id, "inner").drop("x_id")
LLE 第二步 : 利用其邻居对一个点进行线性加权重建 :
数据集的每个点都被重建为其邻居的线性加权和。因为只有邻居参与重建,所以重建是地方性的。重构是通过权重的线性系数实现的,因此是线性的。这就是为什么这种方法被称为局部线性嵌入。
点 P[i] 和 P[j] 的权重相互独立。
一个点及其邻居的旋转、重缩放和平移都不会影响该点的局部 W 矩阵。
重要的是要注意,在求解 W 时,如果邻居的数量大于原始维度 D,一些权重系数可能为零,从而导致多个解。这个问题可以通过向惩罚大权重的最小二乘问题添加正则化来处理。
对于每个点 Y[i] :
创建一个矩阵 Z 与 Y[i]
的所有邻居从 Z
中减去 Y[i] 创建局部协方差矩阵 C = ZZ^T
添加一个正则项以避免 C 奇异, C = C+reg * I
solveCW= 1 forW
setW[ij]= 0 如果 j 不是 I
setW=W/sum(W【T47)$
# calculate local neighborhood matrix
def get_weights(map_list, k, features, reg):
sorted_map = sorted(map_list, key = lambda x: x[1])[1:(k+1)]
neighbors = np.array([s[2] for s in sorted_map])
ind = [s[0] for s in sorted_map]
nbors, n_features = neighbors.shape
neighbors_mat = neighbors - features.toArray().reshape(1,n_features)
cov_neighbors = np.dot(neighbors_mat, neighbors_mat.T)
# add regularization term
trace = np.trace(cov_neighbors)
if trace > 0:
R = reg * trace
else:
R = reg
cov_neighbors.flat[::nbors + 1] += R
weights = linalg.solve(cov_neighbors, np.ones(k).T, sym_pos=True)
weights = weights/weights.sum()
full_weights = np.zeros(len(map_list))
full_weights[ind] = weights
return(Vectors.dense(full_weights))
udf_sort = udf(get_weights, mllibVUDT())
df = df.withColumn("weights", udf_sort("map", lit(10), "features", lit(0.001)))
LLE 第三步:重建低维点 :
在这一步,我们不需要数据集。现在,我们必须使用它的邻居和局部 W 矩阵在低维中创建每个点。邻域图和局部权重矩阵捕捉流形的拓扑。
低维再造误差被约束,使其适定。等式 1(在 LLE 的图像步骤中)约束输出以原点为中心,使其平移不变。需要等式 2 来使输出旋转和缩放不变。
# udf for creating a row of identity matrix
def I(ind):
i = [0]*150
i[ind]=1.0
return(mllibVs.dense(i))
# convert dataframe to indexedrowmatrix
weights_irm = IndexedRowMatrix(df.select(["id","weights"]).rdd.map(lambda x:(x[0], I(x[0])-x[1])))
M = weights_irm.toBlockMatrix().transpose().multiply( weights_irm.toBlockMatrix() )
SVD = M.toIndexedRowMatrix().computeSVD(150, True)
# select the vectors for low dimensional embedding
lle_embbeding = np.fliplr(SVD.V.toArray()[:,-(d+1):-1])
绘制结果嵌入
LLE 之后 MNIST 数据集子集的可视化。
LLE 的缺点 :
LLE 对离群值和噪声很敏感。数据集具有变化的密度,并且不可能总是具有平滑的流形。在这些情况下,LLE 给出了一个糟糕的结果。
结论:在本文中,我们讨论了与理解 LLE 及其实施相关的基本和重要概念。后来,我们在 pyspark 上实现了一个标准的 LLE 算法。黑森 LLE,修改 LLE,和 LTSA 地址 LLE 的一些缺点。
我们将在下一篇文章中讨论 t-SNE。
多维标度(MDS)
原文:https://blog.paperspace.com/dimension-reduction-with-multi-dimension-scaling/
本教程来自关于降维的 7 部分系列:
- 理解主成分分析(PCA)的降维
- 利用独立成分分析(ICA)深入降低维度
- 【MDS】
- LLE (即将推出!)
- t-SNE (即将推出!)
- IsoMap (即将推出!)
- 自动编码器(即将推出!)
(在 github 上有一个 Jupyter 笔记本,里面有数学和代码(python 和 pyspark)。)
多维标度是一种距离保持的流形学习方法。所有流形学习算法都假设数据集位于低维的平滑、非线性流形上,并且可以通过保留高维空间的一个或多个属性来找到映射f:RD->RD(D>>D)。距离保持方法假设流形可以由它的点的成对距离来定义。在距离保持方法中,低维嵌入是以这样的方式从较高维度获得的,即点之间的成对距离保持相同。一些距离保持方法保持空间距离(MDS ),而一些保持图形距离。
MDS 不是单一的方法,而是一系列方法。MDS 采用相异度矩阵 D ,其中 D[ij] 表示点 i 和 j 之间的相异度,并在较低维度上产生映射,尽可能接近地保留相异度。相异矩阵可以从给定的数据集中观察或计算。MDS 在社会学、人类学等人文科学领域,尤其是心理测量学领域广受欢迎和发展。
让我们用 MDS 的书中的一个例子来更好地理解它。下表显示了 1970 年美国不同类型犯罪率之间的相互关系,图片显示的是 MDS 嵌入。随着变量数量的增加,人类的大脑越来越难识别变量之间的关系。
T3
图中各点的相对位置取决于它们在相关表中的不同之处,即具有高相关性的犯罪率彼此靠近,而不具有高相关性的犯罪率彼此远离。从图中我们可以看出,横向犯罪分布可以解释为“暴力犯罪对财产犯罪”,而纵向犯罪分布可以解释为“街头犯罪对隐蔽犯罪”。
MDS 可以分为两类:
-
度量 MDS -度量 MDS 用于定量数据,并试图保留原始的相异度量。给定一个相异矩阵 D ,一个单调函数 f ,p(子空间维数)度量 MDS 试图找到一个最优配置 x⊂r^ps . t . f(d[ij])≈d[ij]=(x[I]—x[j])²。公制 MDS 的另一个版本是经典的 MDS(原 MDS)公式,它提供了封闭形式的解决方案。它不试图在低维中逼近相异度,而是在解中使用特征值分解。
-
非公制 MDS -非公制 MDS 用于序数数据。它试图保持不相似性度量的顺序不变。例如,如果 P[ij] 在 i[th] & j[th] 和 P[32] > P[89] 之间不同,那么非公制 mds 创建一个映射 s . t . d[32]>d[89]。
我们将使用 SMACOF(通过复杂函数的优化来缩放)算法来实现度量 MDS。在深入研究度量 MDS 的实现之前,我们需要了解一些关于优化的 MM(优化-少数化)算法。
求函数最优的 MM
MM 算法是一种求复杂函数最优的迭代算法。假设我们有一个函数 f(x) ,我们需要找到它的最小值。MM 不是直接优化 f(x) 而是使用一个近似函数 g(x,x[m] ) 来寻找一个最优值。如果问题是寻找 f(x) 的最小值,那么 g(x,x[m] ) 称为优化函数,否则称为优化函数,而 x[m] 称为支撑点。
如果 g(x,x[m] ) 是 f(x) 的优化函数,则必须满足以下条件:
- 优化 g(x,x[m] ) 应该比 f(x) 容易。
- 对于任意 x , f(x) ≤ g(x,x[m] )
- f(x[m] )=g(x[m] ,x[m]
MM 算法的步骤:
- 选择一个随机支撑点 x[m]
- 求 x[min]= arg min[x]g(x,x[m]
- 如果 f(x[min])—f(x[m])≈ebreak(其中 e 是一个很小的数)否则转到步骤 4
- 设 x[m] =x[min] 进入第二步
我们用一个例子来理解 MM 算法。
绿色的图是我们需要求最小值的函数。每幅图像代表算法的一次迭代,第一幅图像作为初始设置。我们的初始支点是 x[m] =9.00,g(x,9)的最小值在 x[min] =6.49。现在,如果我们移动到下一幅图像,我们会看到 x[m] 现在是 6.49,并且我们基于 g(x,6.49)得到了新的 x[min] =6.20。如果我们移动到下一次迭代,x[m] 变为 6.20,x[min] 值变为 5.84。继续后续迭代,我们看到最小值向绿色图的最小值移动(如下图所示)。MM 算法最重要的部分是找到一个好的逼近函数。
现在,让我们转到度量 MDS 的 SMACOF 算法。如前所述,度量 MDS 试图逼近相异矩阵并最小化由
σ(X)=σ[ij]wij²给出的应力函数, 其中
w[ij] 是分配给 i 和jδ[ij]是给定相异度矩阵
d[ij] 的元素(X)是我们需要找到的 X 的相异度
我们不去深究应力函数的支配函数的推导。 如果你想了解,请查阅这本优秀的书(主题-优化压力,第 187 页)。
MDS 的脚步
- 创建一个不相似矩阵。
- 选择任意一点 X[m] 作为支点。
- 求应力优化函数的最小值。
- 如果σ(X[m])—σ(X[min])<ebreak else 设置 X[m] =X[min] 并转到步骤 2
第一步——相异度矩阵 :
我们需要一个距离度量来计算数据集中两点之间的距离。让我们快速浏览几个流行的距离度量。
欧几里德度量:这是距离测量中最常用的度量。它等于两点之间的直线距离。
曼哈顿公制:两点之间的距离是沿轴线的直角方向测量的。它也被称为直线距离、出租车公制或城市街区距离。
余弦度量:余弦距离测量两个向量之间角度的余弦。余弦值越小,点越近。
Mahalanobis 度量 : Mahalanobis 度量用于测量点 p 到分布 d 的距离,在检测异常值时特别有用。
汉明度量:汉明度量统计两个向量中条目不同的地方的数量。它是信息论中的一个重要度量。
我们将使用欧几里德度量作为相异度度量。
from sklearn import datasets
import math as ma
import numpy as np
from pyspark.sql import types as t
from pyspark.sql import functions as f
digits = datasets.load_digits(n_class=6)
data = digits.data
# repartitioning the dataframe by id column will speed up the join operation
df = spark.createDataFrame(sc.parallelize(data.tolist()).zipWithIndex()).toDF("features",
"id").repartition("id")
df.cache()
euclidean = lambda x,y:ma.sqrt(np.sum((np.array(x)-np.array(y))**2))
data_bc = sc.broadcast(df.sort("id").select("features").rdd.collect())
# create the distance metric
def pairwise_metric1(y):
dist = []
for x in data_bc.value:
dist += [ma.sqrt(np.sum((np.array(x)-np.array(y))**2))]
return(dist)
udf_dist1 = f.udf(pairwise_metric1, t.ArrayType(t.DoubleType()))
df = df.withColumn("D", udf_dist1("features"))
第二步:SCAMOF 算法:
n,p = data.shape
dim = 2
X = np.random.rand(n,dim)
# randomly initialize a solution for the pivot point.
dfrand = spark.createDataFrame(sc.parallelize(X.tolist()).zipWithIndex()).toDF("X",
"id2").repartition("id2")
df = df.join(dfrand, df.id==dfrand.id2, "inner").drop("id1")
def pairwise_metric2(y):
dist = []
for x in X_bc.value:
dist += [ma.sqrt(np.sum((np.array(x)-np.array(y))**2))]
return(dist)
# create the matrix B
def B(id,x,y):
y,x = np.array(y), np.array(x)
y[y==0.0] = np.inf
z = -x/y
z[id] = -(np.sum(z)-z[id])
return(z.tolist())
# function for matrix multiplication using outer multiplication
def df_mult(df, col1, col2, n1, n2, matrix=True):
udf_mult = f.udf(lambda x,y:np.outer(np.array(x),
np.array(y)).flatten().tolist(),
t.ArrayType(t.DoubleType()))
df = df.withColumn("mult", udf_mult(col1, col2))
df = df.agg(f.array([f.sum(f.col("mult")[i])
for i in range(n1*n2)])).toDF("mult")
if not matrix:
return(df)
st = t.ArrayType(t.StructType(
[t.StructField("id",t.LongType()),
t.StructField("row",t.ArrayType(
t.DoubleType()))]))
udf_arange = (f.udf(lambda x:[(i,j.tolist())
for i,j in enumerate(np.array(x).
reshape(n1,n2)/n1)], st))
df = (df.withColumn("mult",
udf_arange("mult")).select(
f.explode("mult").alias("mult")))
df = (df.select(f.col("mult.id").alias("id2"),
f.col("mult.row").
alias("X_min")).
repartition("id2"))
return(df)
udf_B = f.udf(B, t.ArrayType(t.DoubleType()))
udf_sigma = (f.udf(lambda x,y: float(np.sum((
np.array(x)-np.array(y))**2)),
t.DoubleType()))
sigma_old = np.inf
tol = 1e-4
max_iter = 1000
for i in range(max_iter):
X_bc = sc.broadcast(df.sort("id").select("X").rdd.collect())
def pairwise_metric2(y):
dist = []
for x in X_bc.value:
dist += [ma.sqrt(np.sum((np.array(x)-np.array(y))**2))]
return(dist)
udf_dist2 = f.udf(pairwise_metric2, t.ArrayType(t.DoubleType()))
df = df.withColumn("di", udf_dist2("X"))
df = df.withColumn("sigma", udf_sigma("D","di"))
sigma_new = df.agg({"sigma":"sum"}).collect()[0][0]
print(sigma_old, sigma_new)
sigma_old = sigma_new
df = df.withColumn("B", udf_B("id","D","di")).drop("di")
X_min = df_mult(df, "B", "X", n, dim)
df = df.join(X_min, df.id==X_min.id2).select("id", "D", f.col("X_min").alias("X"))
# cache action will prevent recreation of dataframe from base
df.cache()
MDS 嵌入的情节。
虽然这些簇不很明显,但还是很容易辨认出来。
MDS 的缺点 :
MDS 每次迭代计算相异度矩阵都需要很大的计算能力。很难将新数据嵌入 MDS。
结论:和 PCA 一样,MDS 是一种古老的方法。它已经被很好地研究过了。它几乎没有像 sammon mapping 那样的扩展。通过这篇文章,我们试图加深对 MDS 及其运作的理解。我们复习了一些与 MDS 相关的领域,并在 pyspark 中实现了一个基本的 MDS。
本系列的下一篇文章将在先睹为快!
用主成分分析(PCA)理解降维
原文:https://blog.paperspace.com/dimension-reduction-with-principal-component-analysis/
本教程是关于降维的 7 部分系列的一部分:
- 用主成分分析理解降维
- 利用独立成分分析(ICA)深入降低维度
- 多维标度(MDS)
- LLE (即将推出!)
- t-SNE (即将推出!)
- IsoMap (即将推出!)
- 自动编码器(即将推出!)
维度的诅咒
大数据分析是如今的流行语。每个人都在谈论它。大数据分析已经在许多领域得到应用,如医学、政治、约会。虽然大数据分析被用于改善人类生活的许多方面,但它也有自己的问题。其中之一就是“维度诅咒”。维度诅咒是指大量维度导致的数据规模的指数级增长。随着数据维数的增加,处理数据变得越来越困难。降维是解决维数灾难的一种方法。通俗地说,降维方法是通过提取相关信息,将其余数据作为噪声处理,从而降低数据的大小。
通过一系列的帖子,我们将学习并实现使用大数据框架 pyspark 的降维算法。
本系列的第一篇文章将在 PCA 上发表。
主成分分析
(有更数学化的笔记本有 python 和 pyspark 代码可用 github repo )
主成分分析(PCA)是最流行的线性降维之一。有时,它单独使用,有时作为其他降维方法的起始解决方案。PCA 是一种基于投影的方法,它通过将数据投影到一组正交轴上来变换数据。
让我们对 PCA 有一个直观的了解。假设,您希望根据不同食物的营养成分来区分它们。哪一个变量将是区分食物项目的好选择?如果你选择一个变量,这个变量从一种食物到另一种食物变化很大,你将能够正确地分离它们。如果在食品中选择的变量几乎相同,你的工作将会困难得多。如果数据没有一个适当分离食物项目的变量会怎样?我们可以通过
artVar1 = 2 X orgVar1 - 3 X orgVar2 + 5 X orgVar3
这样的原始变量的线性组合来创建一个人工变量。
这就是主成分分析的本质,它寻找原始变量的最佳线性组合,从而使新变量的方差或分布最大化。
现在,让我们通过一个动画来了解 PCA 是如何达到上述目的的。
幸运的是,多亏了线性代数,我们不必为 PCA 费太多力气。线性代数中的特征值分解和奇异值分解是 PCA 中的两个主要步骤。
特征值分解,特征向量,特征值
特征值分解是一种适用于半正定矩阵的矩阵分解算法。在 PCA 的上下文中,特征向量表示方向或轴,相应的特征值表示沿该特征向量的方差。特征值越高,沿特征向量的方差就越大。
上图为一个正定矩阵的特征值分解一个。 Q 是列为特征向量的正交矩阵λ是以特征值为对角元素的对角矩阵。
奇异值分解
SVD 是一种矩阵分解方法,它将矩阵表示为秩为 1 的矩阵的线性组合。奇异值分解比主成分分析更稳定,并且不需要正定矩阵。
如图 SVD 产生三个矩阵 U,S & V. U 和 V 正交矩阵,它们的列分别代表 AA^T 和 A^T A 的特征向量。矩阵 S 是对角矩阵,对角值称为奇异值。每个奇异值是相应特征值的平方根。
降维如何适应这一切?一旦你计算出特征值和特征向量,选择重要的特征向量来组成一组主轴。
特征向量的选择
我们如何选择要保留的主轴数?应该选择哪些主轴?
一个特征向量的重要性由相应特征值所解释的总方差的百分比来衡量。假设V1
& V2
为两个特征向量,40%
& 10%
分别为沿其方向的总方差。如果让我们从这两个特征向量中选择一个,我们会选择V1
,因为它给了我们更多关于数据的信息。
所有特征向量按照特征值降序排列。现在,我们必须决定保留多少特征向量。为此,我们将讨论两种方法总方差解释和碎石图。
总差异解释
假设,我们有一个向量的n
个特征值(e[0] ,...,e[n] )按降序排序。取每个指数的特征值的累积和,直到总和大于总方差的95%
。拒绝该索引之后的所有特征值和特征向量。
碎石图
同样,我们必须按降序排列特征值。绘制特征值与指数的关系图。您将得到如下图所示的图表。
理想的平地是一条陡峭的曲线,其后是一个急转弯和一条直线。舍弃急转弯后的所有特征值及其对应的特征向量。例如,在上面显示的图像中,急弯位于 4。所以,主轴数应该是 4。
py spark 中的 PCA
让我们在 pyspark 中实现 PCA 算法。
熟悉数据。
#read the dataset and plot a scatter graph between 1st and 2nd variable
import matplotlib.pyplot as plt
iris = datasets.load_iris()
data = iris.data
target = iris.target
setosa = data[target==0]
versicolor = data[target==1]
verginica = data[target==2]
plt.scatter(setosa[:,0], setosa[:,1], c="b",label="setosa")
plt.scatter(versicolor[:,0], versicolor[:,1], c="g",label="versicolor")
plt.scatter(verginica[:,0], verginica[:,1], c="r",label="verginica")
将 numpy 数组转换成 spark 数据帧。
# necesary imports
from pyspark.mllib.linalg.distributed import
IndexedRowMatrix, IndexedRow
from pyspark.ml.feature import StandardScaler
from pyspark.ml.linalg import Vectors, VectorUDT
from pyspark.sql import functions as f
# numpy array -> rdd -> dataframe
rdd = sc.parallelize(iris_data.tolist()).zipWithIndex()
iris_df =
spark.createDataFrame(rdd).toDF("features","id")
n = rdd.count()
p = len(rdd.take(1)[0][0])
# change the data type of features to vectorUDT from array[double]
udf_change = f.udf(lambda x: Vectors.dense(x), VectorUDT())
iris_df = iris_df.withColumn("features", udf_change("features"))
对数据进行预处理——标准化把所有变量都带到了同一水平。
# create the standard scaler model
stdScaler = StandardScaler(withMean = True, withStd = True, inputCol="features", outputCol="scaled_features")
#fit the model on the dataset
model = stdScaler.fit(iris_df)
# transform the dataset
iris_std_df = model.transform(iris_df).drop("features").withColumnRenamed("scaled_features","features")
IndexedRowMatrix 是一个按行索引的分布式矩阵。将数据帧转换为 IndexedRowMatrix,并计算主成分。我们将使用奇异值分解进行矩阵分解。
# now create the indexed row matrix
iris_irm = IndexedRowMatrix(iris_std_df.rdd.map(lambda x: IndexedRow(x[0], x[1].tolist())))
computesvd 函数接受两个参数,一个整数和一个布尔值。整数参数给出了要保留的奇异值的数量。因为我们不知道它的值,所以它等于维数。布尔参数声明是否计算 u。
SVD = iris_irm.computeSVD(p, True)
U = SVD.U
S = SVD.s.toArray()
# compute the eigenvalues and number of components to retain
eigvals = S**2/(n-1)
eigvals = np.flipud(np.sort(eigvals))
cumsum = eigvals.cumsum()
total_variance_explained = cumsum/eigvals.sum()
K = np.argmax(total_variance_explained>0.95)+1
# compute the principal components
V = SVD.V
U = U.rows.map(lambda x: (x.index, x.vector[0:K]*S[0:K]))
princ_comps = np.array(list(map(lambda x:x[1], sorted(U.collect(), key = lambda x:x[0]))))
画出合成的主成分
setosa = princ_comps[iris_target==0]
versicolor = princ_comps[iris_target==1]
verginica = princ_comps[iris_target==2]
plt.scatter(setosa[:,0], setosa[:,1], c="b",label="setosa")
plt.scatter(versicolor[:,0], versicolor[:,1], c="g",label="versicolor")
plt.scatter(verginica[:,0], verginica[:,1], c="r",label="verginica")
PCA 清楚地呈现了数据集的更好的图像。使用主成分分析对 mnist 数据集的子集进行可视化。
PCA 能够更准确地区分数字。零、一和四被清楚地分组,而 PCA 发现很难区分二、三和五。
PCA 的缺点 -如果变量的数量很大,解释主成分变得很困难。当变量之间具有线性关系时,PCA 是最合适的。此外,PCA 容易受到大的异常值的影响。
结论 : PCA 是一种古老的方法,已经得到了很好的研究。基本主成分分析有许多扩展,解决了它的缺点,如鲁棒主成分分析,核主成分分析,增量主成分分析。
通过这篇文章,我们对 PCA 有了一个基本而直观的了解。我们讨论了与实现 PCA 相关的几个重要概念。后来,我们使用 pyspark 在一些真实数据集上实现了 PCA。如果你想更深入,尝试在更大的数据集上实现 PCA 的一些扩展。
我们将在本系列的下一篇文章中讨论另一种线性降维方法 ICA 。
降维- t-SNE
原文:https://blog.paperspace.com/dimension-reduction-with-t-sne/
本教程是关于降维的 7 部分系列的一部分:
(一个更数学化的带代码的笔记本是可用的 github repo )
t-SNE is a new award-winning technique for dimension reduction and data visualization. t-SNE not only captures the local structure of the higher dimension but also preserves the global structures of the data like clusters. It has stunning ability to produce well-defined segregated clusters. t-SNE is based on stochastic neighbor embedding(SNE). t-SNE was developed to address some of the problems in SNE. So let's have a basic understanding of SNE.
SNE: stochastic neighbor embedding uses a probabilistic approach to embed a high dimension dataset into lower dimension by preserving the neighborhood structure of the dataset. A Gaussian probability distribution centered on each point is defined over all the potential neighbors of this point. SNE aims to minimize the difference in probability distribution in the higher dimension and lower dimension.
For each object, i and it's neighbor j , we compute a P[i|j] which reflects the probability that j is neighbor of i
P[i|j] = exp(−d²[ij])Σ[k≠i]exp(−d²[ij])) where d²[ij] is the dissimilarity between element i and j given as input or calculated from the dataset provided.
The dissimilarity between x[i] and x[j] can be calculated using the following formula
d²[ij] = ||x[i]−x[j]||² / (2σ²[i]), where σ[i] generally calculated through a binary search by equating the entropy of the distribution centered at x[i] to perplexity which is chosen by hand. This method generates a probability matrix which is asymmetric.
Now, a random solution is chosen as the starting point for the low dimensional embedding. A probability distribution is defined on it in the same way as done above but with a constant σ=0.5 for all points.
SNE tries to minimize the difference between these two distributions. We can calculate the difference between two distributions using Kullback-Liebler divergence. For two discrete distirbution P and Q KL divergence is given by DKL(P||Q)=Σ[i]Pi.
SNE defines a cost function based of the difference between p[ij] and q[ij] which is given by
C=Σ[i]Σj
While embedding the dataset in lower dimension, two kinds of error can occur, first neighbors are mapped as faraway points( p[ij] is large and q[ij] is small) and points which are far away mapped as neighbors( p[ij] is small while q[ij] is large).Look closely at the cost function, the cost of the first kind of error i.e. mapping large P[ij] with small q[ij] is smaller than the cost while mapping small p[ij] as large q[ij] . SNE heavily penalizes if the neighbors are mapped faraway from each other.
Some of the shortcomings of SNE approach are asymmetric probability matrix P, crowding problem. As pointed out earlier the probability matrix P is asymmetric. Suppose a point X[i] is far away from other points, it's P[ij] will be very small for all j. So, It will have little effect on the cost function and embedding it correctly in the lower dimension will be hard.
Any n-dimensional Euclidean space can have an object with n+1 or less equidistant vertices not more than that. Now, when the intrinsic dimension of a dataset is high say 20, and we are reducing its dimensions from 100 to 2 or 3 our solution will be affected by crowding problem. The amount of space available to map close points in 10 or 15 dimensions will always be greater than the space available in 2 or 3 dimensions. In order to map close points properly, moderately distant points will be pushed too far. This will eat the gaps in original clusters and it will look like a single giant cluster.
We need to brush up few more topics before we move to t-SNE.
Student-t distribution -- Student-t distribution is a continuous symmetric probability distribution function with heavy tails. It has only one parameter degree of freedom. As the degree of freedom increases, it approaches the normal distribution function. When degree of freedom =1, it takes the form of Cauchy distribution function and its probability density function is given by
f(t)=1/π(1+t2)
Entropy: Entrophy is measure of the average information contained in a data. For a variable x with pdf p(x), it is given by
H(x) = −Σ[i](p(x[i]) × log2)
Perpexility: In information theory, perplexity measures how good a probability distribution predicts a sample. A low perplexity indicates that distribution function is good at predicting sample. It is given by
Perpx(x)=2^H(x), where H(x) is the entropy of the distribution.
t-SNE
t-SNE 在两个方面不同于 SNE,首先,它使用学生 t 分布来衡量低维中 Y[i] 和 Y[j] 之间的相似性,其次,对于高维,它使用对称概率分布,使得 P[ji] =P[ij] 。
t-SNE 算法步骤:
- 为每个 i 和 j 计算成对相似度 P[ij] 。
- 使 P[ij] 对称。
- 选择一个随机解 Y[0]
- 未完成时:
计算 Y 的两两相似度[I]计算梯度
如果 i > max_iter break
否则
i = i+1
计算概率分布 :
为了计算成对相似性,我们需要知道以 x[i] 为中心的高斯分布的方差σ[i] 。有人可能会想,为什么不为每个 x[i] 设置一个σ[i] 的单一值。数据的密度可能会有所不同,对于密度较高的地方,我们需要较小的σ[i] ,而对于点距离较远的地方,我们需要较大的σ[i] 。以 x[i] 为中心的高斯分布的熵随着σ[i] 的增加而增加。为了获得σ[i] ,我们需要执行二分搜索法,使得以 x[i] 为中心的高斯分布的困惑度等于用户指定的困惑度。现在,如果你在思考困惑是如何融入这一切的。你可以把困惑看作是邻居数量的一种度量。
参数对嵌入的影响 :
为了使 t-SNE 有意义,我们必须选择正确的困惑值。困惑平衡了数据集的局部和全局方面。一个非常高的值将导致集群合并成一个大集群,而低的值将产生许多没有意义的小集群。下图显示了虹膜数据集上 t-SNE 的困惑效果。
当 K(邻居数)= 5 t-SNE 产生许多小簇。当班级人数很多时,这会产生问题。随着邻居数量的增加,来自相同类的聚类合并。在 K=25 和 K=50 时,我们有几类定义明确的聚类。此外,随着 K 的增加,团簇变得更加密集。
t-SNE 嵌入后 MNIST 数据集子集的图。
t-SNE 为每一个数字产生一个定义明确且独立的聚类。
t-SNE 的缺点
t-SNE 的问题出现在固有维数较高时,即大于 2-3 维。像其他基于梯度下降的算法一样,t-SNE 有陷入局部最优的趋势。由于最近邻搜索查询,基本的 t-SNE 算法很慢。
结论:我们通过这篇帖子谈到了另一种降维可视化方法 t-SNE 。一开始,我们讨论了与 SNE 霸王龙相关的重要话题。之后,使用 pyspark 实现了一个基本的 t-SNE。基本 t-SNE 的扩展很少,这提高了算法的时间复杂度。希望深入研究这个话题的人巴恩斯-胡特 t-SNE 将是一个很好的起点。
在下一篇文章中,我们将了解 Isomap
建立一个人工智能来玩恐龙快跑
构建强化学习模型的教程
DeepMind 在 2013 年发表了一篇题为“用深度强化学习玩雅达利”的文章,介绍了一种用于强化学习的新的深度学习模型,并展示了它在仅使用原始像素作为输入的情况下,掌握雅达利 2600 电脑游戏的困难控制策略的能力。在本教程中,我将使用 Keras 实现这篇论文。我们将从强化学习的基础开始,然后深入代码进行实际理解。
AI 玩游戏 |
2018 年 3 月初开始做这个项目,得到了一些不错的结果。然而,只有 CPU 的系统是学习更多特性的瓶颈。强大的 GPU 极大地提高了性能。
在我们有一个运行模型之前,有许多步骤和概念需要我们理解。
步骤:
- 在浏览器(JavaScript)和模型(Python)之间建立双向接口
- 捕获和预处理图像
- 火车模型
- 评价
源代码:【https://github.com/Paperspace/DinoRunTutorial.git】T2
入门指南
为了训练和玩游戏,在使用
git clone https://github.com/Paperspace/DinoRunTutorial.git
设置好环境后克隆 GitHub 库,并在 jupyter 笔记本
Reinforcement Learning Dino Run.ipynb
上工作,确保第一次运行init_cache()
来初始化文件系统结构。
强化学习
学走路的孩子
这对许多人来说可能是一个新词,但我们每个人都已经使用强化学习(RL)的概念学会了走路,这是我们大脑仍然如何工作的。奖励系统是任何 RL 算法的基础。如果我们回到儿童走路的类比,积极的奖励将是父母的拍手或够到糖果的能力,而消极的奖励将是没有糖果。然后,孩子在开始走路之前首先学会站立。就人工智能而言,智能体(在我们的例子中是 Dino)的主要目标是通过在环境中执行特定的动作序列来最大化某个数字奖励。RL 中最大的挑战是缺乏监督(标记数据)来指导代理。它必须自己探索和学习。代理从随机执行动作开始,观察每个动作带来的回报,并学会在面对类似的环境状态时预测最佳的可能动作
普通强化学习框架 |
q 学习
我们使用 Q-learning,一种 RL 技术,在这里我们试图逼近一个特殊的函数,这个函数驱动任何环境状态序列的动作选择策略。Q-learning 是强化学习的无模型实现,其中针对每个状态、采取的行动和产生的奖励维护一个 Q 值表。一个样本 Q 表应该能让我们了解数据是如何构成的。在我们的例子中,状态是游戏截图和动作,什么都不做,跳转[0,1]
样本 Q 表 |
我们利用深度神经网络通过回归来解决这个问题,并选择具有最高预测 Q 值的动作。要详细了解 Q-learning,请参考 Tambet Matiisen 的这篇惊人的博文。你也可以参考我的以前的帖子来了解 Q-learning 的所有超级参数
设置
让我们设置环境,开始培训过程。
1。 选择虚拟机:我们需要一个完整的桌面环境,在这里我们可以捕获并利用屏幕截图进行培训。我选择了 Paperspace 的 ML-in-a-box (MLIAB) Ubuntu 图像。MLIAB 的优势在于它预装了 Anaconda 和许多其他 ML-library。
盒子里的机器学习 |
2。 配置并安装 keras 使用 GPU:
我们需要安装 Keras 和 tensorflow 的 GPU 版本
Paperspace 的虚拟机已经预装了这些,但如果没有安装它们
pip install keras
pip install tensorflow
此外,确保 GPU 被设置识别。执行下面的 python 代码,你应该会看到可用的 GPU 设备
from keras import backend as K
K.tensorflow_backend._get_available_gpus()
3。 安装依赖关系
- 硒
pip install selenium
- OpenCV
pip install opencv-python
- 从 http://chromedriver.chromium.org 下载 Chromedrive
游戏框架
你可以通过将浏览器指向 chrome://dino 或者直接拔掉网络插头来启动游戏。如果我们打算修改游戏代码,另一种方法是从 chromium 的开源库提取游戏。
我们的模型是用 python 写的,游戏是用 JavaScript 构建的,我们需要一些接口工具让它们相互交流。
Selenium 是一个流行的浏览器自动化工具,用于向浏览器发送动作,并获得不同的游戏参数,如当前得分。
现在我们有了一个向游戏发送动作的接口,我们需要一个捕捉游戏屏幕的机制
Selenium 和 OpenCV 分别为屏幕捕捉和图像预处理提供了最佳性能,实现了 6-7 fps 的下降帧速率。
我们只需要每个时间帧 4 帧,足够作为一个功能学习速度
游戏模块
我们用这个模块实现了 Python 和 JavaScript 之间的接口。下面的代码片段应该能让您大致了解模块中发生的事情。
class Game:
def __init__(self):
self._driver = webdriver.Chrome(executable_path = chrome_driver_path)
self._driver.set_window_position(x=-10,y=0)
self._driver.get(game_url)
def restart(self):
self._driver.execute_script("Runner.instance_.restart()")
def press_up(self):
self._driver.find_element_by_tag_name("body").send_keys(Keys.ARROW_UP)
def get_score(self):
score_array = self._driver.execute_script("return Runner.instance_.distanceMeter.digits")
score = ''.join(score_array).
return int(score)
代理模块
我们用代理模块包装所有的接口。我们使用这个模块控制 Dino,并获取代理在环境中的状态。
class DinoAgent:
def __init__(self,game): #takes game as input for taking actions
self._game = game;
self.jump(); #to start the game, we need to jump once
def is_crashed(self):
return self._game.get_crashed()
def jump(self):
self._game.press_up()
游戏状态模块
为了向模块发送动作,并获得环境作为该动作的结果而转变到的结果状态,我们使用游戏状态模块。它通过接收&执行动作、决定奖励和返回经验元组来简化过程。
class Game_sate:
def __init__(self,agent,game):
self._agent = agent
self._game = game
def get_state(self,actions):
score = self._game.get_score()
reward = 0.1 #survival reward
is_over = False #game over
if actions[1] == 1: #else do nothing
self._agent.jump()
image = grab_screen(self._game._driver)
if self._agent.is_crashed():
self._game.restart()
reward = -1
is_over = True
return image, reward, is_over #return the Experience tuple
图像管道
图像捕捉
我们有多种方法可以捕捉游戏屏幕,比如使用 PIL 和 MSS python 库来对整个屏幕进行截图,并裁剪感兴趣的区域。然而,最大的缺点是对屏幕分辨率和窗口位置的敏感性。幸运的是,这个游戏使用了 HTML 画布。我们可以使用 JavaScript 轻松获得 base64 格式的图像。我们使用 selenium 运行这个脚本。
#javascript code to get the image data from canvas
var canvas = document.getElementsByClassName('runner-canvas')[0];
var img_data = canvas.toDataURL()
return img_data
从画布中提取的图像 |
def grab_screen(_driver = None):
image_b64 = _driver.execute_script(getbase64Script)
screen = np.array(Image.open(BytesIO(base64.b64decode(image_b64))))
image = process_img(screen)#processing image as required
return image
图像处理
拍摄的原始图像分辨率约为 600x150,具有 3 个(RGB)通道。我们打算使用 4 个连续的屏幕截图作为模型的单一输入。这使得我们的单个输入的尺寸为 600×150×3×4。这在计算上是昂贵的,并且不是所有的特征对玩游戏都有用。因此,我们使用 OpenCV 库来调整大小,裁剪和处理图像。最终处理的输入仅为 80x80 像素,单通道(灰度)。
def process_img(image):
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image = image[:300, :500]
return image
图像处理 |
模型架构
我们得到了输入和利用模型输出来玩游戏的方法,让我们看看模型架构。
我们使用一系列的三个卷积层,然后将它们展平为密集层和输出层。纯 CPU 模型不包括池化层,因为我已经删除了许多功能,添加池化层会导致已经很少的功能大量丢失。但是借助 GPU 的能力,我们可以在不降低帧速率的情况下容纳更多功能。
最大池化图层显著改善了密集要素集的处理。
模型架构 |
我们的输出层由两个神经元组成,每个神经元代表每个动作的最大预测回报。然后,我们选择具有最大回报(Q 值)的行动 |
def buildmodel():
print("Now we build the model")
model = Sequential()
model.add(Conv2D(32, (8, 8), padding='same',strides=(4, 4),input_shape=(img_cols,img_rows,img_channels))) #80*80*4
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Activation('relu'))
model.add(Conv2D(64, (4, 4),strides=(2, 2), padding='same'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3),strides=(1, 1), padding='same'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Activation('relu'))
model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dense(ACTIONS))
adam = Adam(lr=LEARNING_RATE)
model.compile(loss='mse',optimizer=adam)
print("We finish building the model")
return model
培养
这些都是在训练阶段发生的事情
- 从无动作开始并获得初始状态(s_t)
- 观察游戏,观察步数
- 预测并执行一个动作
- 在重放记忆中存储经验
- 从重放存储器中随机选择一批,并在其上训练模型
- 游戏结束后重启
这段代码有点长,但是很容易理解
def trainNetwork(model,game_state):
# store the previous observations in replay memory
D = deque() #experience replay memory
# get the first state by doing nothing
do_nothing = np.zeros(ACTIONS)
do_nothing[0] =1 #0 => do nothing,
#1=> jump
x_t, r_0, terminal = game_state.get_state(do_nothing) # get next step after performing the action
s_t = np.stack((x_t, x_t, x_t, x_t), axis=2).reshape(1,20,40,4) # stack 4 images to create placeholder input reshaped 1*20*40*4
OBSERVE = OBSERVATION
epsilon = INITIAL_EPSILON
t = 0
while (True): #endless running
loss = 0
Q_sa = 0
action_index = 0
r_t = 0 #reward at t
a_t = np.zeros([ACTIONS]) # action at t
q = model.predict(s_t) #input a stack of 4 images, get the prediction
max_Q = np.argmax(q) # chosing index with maximum q value
action_index = max_Q
a_t[action_index] = 1 # o=> do nothing, 1=> jump
#run the selected action and observed next state and reward
x_t1, r_t, terminal = game_state.get_state(a_t)
x_t1 = x_t1.reshape(1, x_t1.shape[0], x_t1.shape[1], 1) #1x20x40x1
s_t1 = np.append(x_t1, s_t[:, :, :, :3], axis=3) # append the new image to input stack and remove the first one
D.append((s_t, action_index, r_t, s_t1, terminal))# store the transition
#only train if done observing; sample a minibatch to train on
trainBatch(random.sample(D, BATCH)) if t > OBSERVE else 0
s_t = s_t1
t += 1
请注意,我们从重放记忆中随机抽取了 32 次经验重放,并使用了分批训练方法。这样做的原因是游戏结构中不平衡的动作分配以及为了避免过度拟合。
def trainBatch(minibatch):
for i in range(0, len(minibatch)):
loss = 0
inputs = np.zeros((BATCH, s_t.shape[1], s_t.shape[2], s_t.shape[3])) #32, 20, 40, 4
targets = np.zeros((inputs.shape[0], ACTIONS)) #32, 2
state_t = minibatch[i][0] # 4D stack of images
action_t = minibatch[i][1] #This is action index
reward_t = minibatch[i][2] #reward at state_t due to action_t
state_t1 = minibatch[i][3] #next state
terminal = minibatch[i][4] #wheather the agent died or survided due the action
inputs[i:i + 1] = state_t
targets[i] = model.predict(state_t) # predicted q values
Q_sa = model.predict(state_t1) #predict q values for next step
if terminal:
targets[i, action_t] = reward_t # if terminated, only equals reward
else:
targets[i, action_t] = reward_t + GAMMA * np.max(Q_sa)
loss += model.train_on_batch(inputs, targets)
结果
通过使用这种架构,我们应该能够得到好的结果。GPU 显著提高了结果,这可以通过平均分数的提高来验证。下图显示了训练开始时的平均分数。训练结束时,每 10 场比赛的平均得分保持在 1000 分以上。
每 10 场比赛的平均得分 |
记录的最高分是 4000+,远远超过了之前的 250 分(也远远超过了大多数人的能力!).该图显示了训练期游戏的最高分数的进程(等级= 10)。
每 10 场比赛的最高分数 |
恐龙的速度与分数成正比,使得在更高的速度下更难发现和决定一个动作。因此,整场比赛都是匀速进行的。本博客中的代码片段仅供参考。关于带有附加设置的功能代码,请参考 GitHub repo 。您可以随意地修改代码并做出贡献。
关于我 : 有了一些软件行业的经验,我正在探索 ML 和 AI 领域及其应用。目前我正在波士顿东北大学攻读硕士学位。我喜欢联系,讨论和贡献类似的项目。请随时联系 LinkedIn。
使用 Deadline 在云中进行分布式渲染
原文:https://blog.paperspace.com/distributed-rendering-in-the-cloud-with-deadline/
Deadline 是最受欢迎的分布式渲染应用之一,是从事动画、视觉特效和游戏开发等行业的人的必备工具。在本教程中,我们将了解如何在 Paperspace 上设置截止时间,特别是将重点放在跨多个渲染从属对象分发渲染上...
Deadline 是最流行的分布式渲染应用程序之一,是从事动画、视觉特效和游戏开发等行业的人的必备工具。在本教程中,我们将了解如何在 Paperspace 上设置 Deadline,特别是将重点放在跨多个渲染从属对象分发渲染上。由于 Deadline 中的大量程序支持,您可以将本教程用于许多软件,包括 Maya、3ds max、Cinema 4D、After Effects 和 Nuke 等等。阅读本教程后,您将能够快速设置您的分布式云渲染场,并立即进行渲染。
注意 : 本教程将涵盖基本设置。我们将在后面的教程中回顾截止时间 VMX 集成。
先决条件
为了完成本教程,您必须具备:
-
一台以上的纸张空间机器。
-
共享的图纸空间驱动器。
-
每台机器上许可和安装的最后期限。安装说明。(确保将存储库设置到您的共享驱动器。)
-
由 Deadline 支持的应用程序。支持的软件列表。出于本教程的目的,我们将使用 4D 电影院。
-
安装在您各自软件中的截止日期客户端插件。此处为申请信息。
设置截止日期
步骤 1–将共享驱动器安装到特定的驱动器盘符上
这对一些人来说似乎是显而易见的,但是确保你的共享驱动器在所有机器上安装到一个一致的盘符上对于 Deadline 能够运行是极其重要的。对于本例,我们将在第一台机器上将共享驱动器挂载到字母“Z:”上。
首先访问您的 Paperspace 机器上的文件浏览器,右键单击“这台电脑”并选择“映射网络驱动器”
输入由 Paperspace 提供的驱动器共享路径,并确保选中“登录时重新连接”和“使用不同的凭据连接”。记下将要使用的驱动器号,在本例中为“Z:”。
在弹出的登录屏幕中,输入 Paperspace 提供的登录凭据,以连接到共享驱动器并装载它。确保选中“记住我的凭据”
步骤 2–在共享驱动器上设置截止日期存储库
首先,我们需要确保我们的期限库被设置到我们的共享驱动器上。转到 deadline launcher 应用程序,在右下角右键单击 Deadline launcher 应用程序,然后选择“更改存储库”。
应该会弹出存储库路径窗口。确保路径指向共享驱动器存储库。
注意 : 如果您的共享驱动器上没有存储库,您可以简单地将其从存在的任何地方复制到您的共享驱动器,然后将该窗口的路径设置为复制的存储库。
第 3 步–启动截止时间从模块
要启动最后期限从属,右键单击最后期限启动器图标并选择“启动从属”。
一旦从机启动,它应该说“空闲”
步骤 4–设置所有节点。
对所有渲染节点重复步骤 1-3,或使用 Paperspace 创建您设置的计算机的模板,并克隆它!
步骤 5–检查节点的功能
从截止日期启动器中打开截止日期监视器。
检查从属列表中的节点,确保它们是活动的。
注意 : 如果你看不到你的节点或者它们是不活动的,确保你遵循了上面的所有步骤。如果您仍然看不到它们,请尝试重启机器并重新连接到存储库。如果他们仍然没有出现,尝试检查文档安装中的任何错误。
带截止日期的渲染
步骤 1–使用截止日期提交插件启动您的应用程序。
出于本教程的目的,我们将使用 4D 电影院。我们将在未来介绍其他计划。
启动要渲染的项目文件。确保:
- 你的插件和程序在所有机器上都是一样的。
- 您的项目文件需要与渲染所需的资源一起保存到共享驱动器中。在 4D 影院中,最好的方法是前往“文件”>“存储项目和资源”,然后导航到共享驱动器。
第二步——用截止日期提交插件提交你的渲染任务。
进入“插件”菜单,点击“提交到截止日期”。
在提交窗口中,确保设置符合您的喜好。要考虑的一些设置有:
- 池和组是隔离特定计算机集合进行渲染的一种方式。
- 计算机列表对于从渲染任务中隔离或移除某些计算机非常有用。
优先级让你决定渲染应该以什么顺序完成。 - 并发任务是你可以告诉 Deadline 一个程序的 Deadline 应该同时运行多少个实例。小心这一点,因为它可能会导致机器挂起和冻结。
- 框架列表是你告诉 Deadline 渲染什么框架的地方。
- “每任务帧数”是指在渲染任务期间,在 Deadline 关闭应用程序之前应渲染的帧数。
一旦你点击提交,你应该得到一个“成功提交渲染”窗口。
第三步——检查你的渲染状态的最后期限监视器。
期限监视器是你可以跟踪你的渲染并确保它正在进行的地方。
- 注意 : 如果您遇到任何错误,您可以通过右键单击出错的框架并选择“连接到从属日志”或“查看任务报告”来检查从属日志
结论
希望这篇教程能让你明白在云端设置 Deadline 并输出你的项目文件是多么容易。我们迫不及待地想看看你用这种软件和硬件的强大组合创造出什么样的东西!
要开始设置你自己的渲染管道,在这里注册。
稳定扩散教程第 1 部分:在渐变笔记本中运行 Dreambooth
原文:https://blog.paperspace.com/dreambooth-stable-diffusion-tutorial-1/
2022 年 11 月 3 日更新:关于文本倒置的第 2 部分现已上线,并更新了演示笔记本!
Dreambooth 是对潜在扩散模型(T1)背后的技术的一个令人难以置信的新转折,通过扩展,是来自 Runway ML 和 CompVis 的非常受欢迎的预训练模型(T2)稳定扩散模型(T3)。
这种新方法允许用户输入一个主题(如特定的狗、人或建筑)的一些图像(最少 3-5 个)和相应的类名(如“狗”、“人”、“建筑”),以便微调和个性化任何对涉及该主题的唯一标识符进行编码的文本到图像模型。当与稳定扩散令人难以置信的多功能性相结合时,我们可以创建令人难以置信的模型,其特征是对我们选择的任何物体或风格的稳健描绘。
在本教程中,我们将逐步完成梯度笔记本中 Dreambooth 稳定扩散模型的设置、训练和推断。我们提供了针对笔记本电脑环境优化的演示代码,以便用户可以利用平台上各种强大的 GPU。
要加载本教程,只需点击此链接到在渐变笔记本上运行。
进入网页后,选择您喜欢的项目。此链接当前设置为与 P5000 一起运行,但可以更改为更快的培训。在 P5000 上进行 500 个周期的训练大约需要 25 分钟。
注意:您将需要至少 16 GB 的 GPU RAM 来运行此模型培训。P5000、P6000、V100、V100-32G、RTX5000、A4000、A5000、A100 和 A100-80G 动力机器都能够运行此培训。
笔记本的 URL
将下面显示<YOUR-GPU-CHOICE>
的 url 更改为 Paperspace 提供的任何 GPU。
https://console.paperspace.com/github/gradient-ai/dreambooth-stable-diffusion/blob/main/sd_dreambooth_gradient.ipynb?machine=<YOUR-GPU-CHOICE>
现在我们已经打开了笔记本,启动它开始演示。
注意:这个演示是基于 HuggingFace 笔记本找到的这里
第一步:设置
The Dreambooth Notebook in Gradient
一旦我们启动了笔记本,让我们确保我们正在使用sd_dreambooth_gradient.ipynb
,然后按照页面上的说明设置笔记本环境。
首先运行顶部的 install 单元来获得必要的包。
#@title Install the required libs
!pip install -qq diffusers==0.4.1 accelerate tensorboard ftfy
!pip install -qq -U transformers
!pip install -qq "ipywidgets>=7,<8"
!pip install -qq bitsandbytes
然后,我们将使用下一个单元格登录 Huggingface,这样我们就可以访问模型文件。请确保使用您的 HuggingFace 帐户在这个 URL 获得访问权限,然后将在这里找到的令牌粘贴到下面的代码单元格中,在那里显示<your_huggingface_token>
。运行单元进行登录。
!wget https://raw.githubusercontent.com/gradient-ai/stable-diffusion/main/login.py
!python login.py --token <your_huggingface_token>
最后,我们将通过导入相关的库来完成设置,并创建一个image_grid
函数来帮助在一个网格中显示我们的图像输出,该网格由样本数和行数决定。
#@title Import required libraries
import argparse
import itertools
import math
import os
from contextlib import nullcontext
import random
import numpy as np
import torch
import torch.nn.functional as F
import torch.utils.checkpoint
from torch.utils.data import Dataset
import PIL
from accelerate import Accelerator
from accelerate.logging import get_logger
from accelerate.utils import set_seed
from diffusers import AutoencoderKL, DDPMScheduler, PNDMScheduler, StableDiffusionPipeline, UNet2DConditionModel
from diffusers.hub_utils import init_git_repo, push_to_hub
from diffusers.optimization import get_scheduler
from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker
from PIL import Image
from torchvision import transforms
from tqdm.auto import tqdm
from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer
import bitsandbytes as bnb
# Helper function
def image_grid(imgs, rows, cols):
assert len(imgs) == rows*cols
w, h = imgs[0].size
grid = Image.new('RGB', size=(cols*w, rows*h))
grid_w, grid_h = grid.size
for i, img in enumerate(imgs):
grid.paste(img, box=(i%cols*w, i//cols*h))
return grid
现在设置已经完成,我们将开始设置数据和概念变量,我们将在以后的微调中使用它们。
建立我们的概念
https://blog.paperspace.com/content/media/2022/10/Screen-Recording-2022-10-28-at-7.30.40-PM.mp4
How to upload sample images to inputs from local machine
现在我们有两条路可走:严格按照演示来,或者使用我们自己的图像。如果我们想使用我们自己的图像,创建inputs
目录并将这些图像上传到其中。否则,运行下面的单元格。它将创建inputs
目录,并设置要下载到该目录的图像 URL 列表。
!mkdir inputs
#@markdown Add here the URLs to the images of the concept you are adding. 3-5 should be fine
urls = [
"https://huggingface.co/datasets/valhalimg/resolve/main/2.jpeg",
"https://huggingface.co/datasets/valhalimg/resolve/main/3.jpeg",
"https://huggingface.co/datasets/valhalimg/resolve/main/5.jpeg",
"https://huggingface.co/datasets/valhalimg/resolve/main/6.jpeg",
## You can add additional images here
]
现在,我们需要通过将我们概念的图片上传或下载到 repo inputs
来开始为我们的 Dreambooth 培训设置主题。
如果数据是在线公开存储的,您还可以使用上面单元格中的代码来更改为该演示下载的示例图像 URL。
# @title Setup and check the images you have just added
import requests
import glob
from io import BytesIO
def download_image(url):
try:
response = requests.get(url)
except:
return None
return Image.open(BytesIO(response.content)).convert("RGB")
images = list(filter(None,[download_image(url) for url in urls]))
save_path = "./inputs"
if not os.path.exists(save_path):
os.mkdir(save_path)
[image.save(f"{save_path}/{i}.jpeg") for i, image in enumerate(images)]
image_grid(images, 1, len(images))
该单元将把数据下载到输入文件夹,用于演示。
为了确保质量概念的调整,这些图像应该在一个主题中保持一致,但在一些细微的方面有所不同,如照明、视角、距离和噪声。下面是一个网格,显示了我们用image_grid
下载的图像。
a clay cat toy
正如我们所看到的,同一个图像是从不同的角度和视角展示的,并且具有不同的背景以防止过度拟合。
设置训练变量
#@markdown `pretrained_model_name_or_path` which Stable Diffusion checkpoint you want to use
pretrained_model_name_or_path = "runwayml/stable-diffusion-v1-5" #@param {type:"string"}
接下来,我们将实例化用于模型训练的训练变量和那些将对应于图像数据的变量。本次演示我们将使用 Runway ML 的稳定扩散-v1-5 检查点。如果您尚未在主页上访问模型,请务必点击确认按钮。
#@title Settings for your newly created concept
instance_prompt = "a photo of sks toy" #@param {type:"string"}
prior_preservation = False #@param {type:"boolean"}
prior_preservation_class_prompt = "a photo of a cat toy" #@param {type:"string"}
class_prompt=prior_preservation_class_prompt
# Set your data folder path
prior_preservation_class_folder = './inputs'
class_data_root=prior_preservation_class_folder
num_class_images = len(os.listdir(prior_preservation_class_folder))
sample_batch_size = 4
prior_loss_weight = 0.5
接下来,我们设置模型训练的设置。
instance_prompt
是一个提示,应该包含对你的对象或风格的良好描述。它与初始化词sks
成对出现。
如果我们希望概念的类别(例如:玩具、狗、画)被保证保留,我们可以将prior_preservation
设置为 True。这提高了质量,有助于以训练时间为代价的概括。
然后我们设置prior_preservation_class_folder
和class_data_root
来设置输入文件夹路径。
最后,我们使用该文件夹大小来确定所有类图像的数量,将批处理大小设置为 4,并将prior_loss_weight
设置为 0 . 5,该值决定了先前保存的类应该有多强。
向模型传授新概念(使用 Dreambooth 进行微调)
创建数据集类以促进培训
为了开始教授期望的概念,我们需要实例化DreamBoothDataset
和PromptDataset
类来处理训练数据输入的组织。构建这些数据集对象是为了确保输入数据经过优化,以便对模型进行微调。这方面的代码如下所示:
#@title Setup the Classes
from pathlib import Path
from torchvision import transforms
class DreamBoothDataset(Dataset):
def __init__(
self,
instance_data_root,
instance_prompt,
tokenizer,
class_data_root=None,
class_prompt=None,
size=512,
center_crop=False,
):
self.size = size
self.center_crop = center_crop
self.tokenizer = tokenizer
self.instance_data_root = Path(instance_data_root)
if not self.instance_data_root.exists():
raise ValueError("Instance images root doesn't exists.")
self.instance_images_path = list(Path(instance_data_root).iterdir())
self.num_instance_images = len(self.instance_images_path)
self.instance_prompt = instance_prompt
self._length = self.num_instance_images
if class_data_root is not None:
self.class_data_root = Path(class_data_root)
self.class_data_root.mkdir(parents=True, exist_ok=True)
self.class_images_path = list(Path(class_data_root).iterdir())
self.num_class_images = len(self.class_images_path)
self._length = max(self.num_class_images, self.num_instance_images)
self.class_prompt = class_prompt
else:
self.class_data_root = None
self.image_transforms = transforms.Compose(
[
transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR),
transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]),
]
)
def __len__(self):
return self._length
def __getitem__(self, index):
example = {}
instance_image = Image.open(self.instance_images_path[index % self.num_instance_images])
if not instance_image.mode == "RGB":
instance_image = instance_image.convert("RGB")
example["instance_images"] = self.image_transforms(instance_image)
example["instance_prompt_ids"] = self.tokenizer(
self.instance_prompt,
padding="do_not_pad",
truncation=True,
max_length=self.tokenizer.model_max_length,
).input_ids
if self.class_data_root:
class_image = Image.open(self.class_images_path[index % self.num_class_images])
if not class_image.mode == "RGB":
class_image = class_image.convert("RGB")
example["class_images"] = self.image_transforms(class_image)
example["class_prompt_ids"] = self.tokenizer(
self.class_prompt,
padding="do_not_pad",
truncation=True,
max_length=self.tokenizer.model_max_length,
).input_ids
return example
class PromptDataset(Dataset):
def __init__(self, prompt, num_samples):
self.prompt = prompt
self.num_samples = num_samples
def __len__(self):
return self.num_samples
def __getitem__(self, index):
example = {}
example["prompt"] = self.prompt
example["index"] = index
return example
加载模型检查点
#@title Load the Stable Diffusion model
#@markdown Please read and if you agree accept the LICENSE [here](https://huggingface.co/runwayml/stable-diffusion-v1-5) if you see an error
# Load models and create wrapper for stable diffusion
text_encoder = CLIPTextModel.from_pretrained(
pretrained_model_name_or_path, subfolder="text_encoder"
)
vae = AutoencoderKL.from_pretrained(
pretrained_model_name_or_path, subfolder="vae"
)
unet = UNet2DConditionModel.from_pretrained(
pretrained_model_name_or_path, subfolder="unet"
)
tokenizer = CLIPTokenizer.from_pretrained(
pretrained_model_name_or_path,
subfolder="tokenizer",
)
接下来,我们将加载运行这个调优过程所需的三个独立组件。这些共同构成了模型的完整管道。我们的概念的最终输出将是类似的格式。在本教程的后面,我们将展示如何将这种存储库概念格式转换成经典的稳定扩散检查点。
记住,我们需要访问 v1-5 检查点页面,接受用户协议下载这些文件。
为培训设置参数
既然已经建立了模型,我们需要实例化训练参数。通过运行下面的单元格,我们使用Namespace
实例化了 Dreambooth 培训过程的调整参数。
#@title Setting up all training args
!mkdir outputs
save_path = 'outputs'
from argparse import Namespace
args = Namespace(
pretrained_model_name_or_path=pretrained_model_name_or_path,
resolution=512,
center_crop=True,
instance_data_dir=save_path,
instance_prompt=instance_prompt,
learning_rate=5e-06,
max_train_steps=500,
train_batch_size=1,
gradient_accumulation_steps=2,
max_grad_norm=1.0,
mixed_precision="no", # set to "fp16" for mixed-precision training.
gradient_checkpointing=True, # set this to True to lower the memory usage.
use_8bit_adam=True, # use 8bit optimizer from bitsandbytes
seed=34354,
with_prior_preservation=prior_preservation,
prior_loss_weight=prior_loss_weight,
sample_batch_size=2,
class_data_dir=prior_preservation_class_folder,
class_prompt=None,
num_class_images=num_class_images,
output_dir="dreambooth-concept",
)
特别是,我们可能希望编辑:
- 可以说是最重要的论点——改变这一点,提高或降低训练周期,这会极大地影响结果
seed
:为计算训练过程中的损失而生成的样本图像的种子——对样本图像的最终结果有重大影响,以及训练后哪些特征被认为对模型是显著的output_dir
:经过培训的 Dreambooth 概念的最终输出目录resolution
:输入的训练图像的大小mixed_precision
:告诉模型使用全精度和半精度来进一步加速训练
用 accelerate 定义训练功能
这里我们为下面的训练运行实例化训练函数。这使用加速器包来增加在多 GPU 配置上训练该功能的能力。在运行下面单元格中的代码之前,请按照中的注释查看训练函数的每个步骤。
#@title Training function
from accelerate.utils import set_seed
def training_function(text_encoder, vae, unet):
logger = get_logger(__name__)
accelerator = Accelerator(
gradient_accumulation_steps=args.gradient_accumulation_steps,
mixed_precision=args.mixed_precision,
)
set_seed(args.seed)
if args.gradient_checkpointing:
unet.enable_gradient_checkpointing()
# Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs
if args.use_8bit_adam:
optimizer_class = bnb.optim.AdamW8bit
else:
optimizer_class = torch.optim.AdamW
optimizer = optimizer_class(
unet.parameters(), # only optimize unet
lr=args.learning_rate,
)
noise_scheduler = DDPMScheduler(
beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000
)
train_dataset = DreamBoothDataset(
instance_data_root='inputs',
instance_prompt=args.instance_prompt,
class_data_root=args.class_data_dir if args.with_prior_preservation else None,
class_prompt=args.class_prompt,
tokenizer=tokenizer,
size=args.resolution,
center_crop=args.center_crop,
)
def collate_fn(examples):
input_ids = [example["instance_prompt_ids"] for example in examples]
pixel_values = [example["instance_images"] for example in examples]
# concat class and instance examples for prior preservation
if args.with_prior_preservation:
input_ids += [example["class_prompt_ids"] for example in examples]
pixel_values += [example["class_images"] for example in examples]
pixel_values = torch.stack(pixel_values)
pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
input_ids = tokenizer.pad({"input_ids": input_ids}, padding=True, return_tensors="pt").input_ids
batch = {
"input_ids": input_ids,
"pixel_values": pixel_values,
}
return batch
train_dataloader = torch.utils.data.DataLoader(
train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn
)
unet, optimizer, train_dataloader = accelerator.prepare(unet, optimizer, train_dataloader)
# Move text_encode and vae to gpu
text_encoder.to(accelerator.device)
vae.to(accelerator.device)
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
# Train!
total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps
logger.info("***** Running training *****")
logger.info(f" Num examples = {len(train_dataset)}")
logger.info(f" Instantaneous batch size per device = {args.train_batch_size}")
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}")
logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
logger.info(f" Total optimization steps = {args.max_train_steps}")
# Only show the progress bar once on each machine.
progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process)
progress_bar.set_description("Steps")
global_step = 0
for epoch in range(num_train_epochs):
unet.train()
for step, batch in enumerate(train_dataloader):
with accelerator.accumulate(unet):
# Convert images to latent space
with torch.no_grad():
latents = vae.encode(batch["pixel_values"]).latent_dist.sample()
latents = latents * 0.18215
# Sample noise that we'll add to the latents
noise = torch.randn(latents.shape).to(latents.device)
bsz = latents.shape[0]
# Sample a random timestep for each image
timesteps = torch.randint(
0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device
).long()
# Add noise to the latents according to the noise magnitude at each timestep
# (this is the forward diffusion process)
noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)
# Get the text embedding for conditioning
with torch.no_grad():
encoder_hidden_states = text_encoder(batch["input_ids"])[0]
# Predict the noise residual
noise_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample
if args.with_prior_preservation:
# Chunk the noise and noise_pred into two parts and compute the loss on each part separately.
noise_pred, noise_pred_prior = torch.chunk(noise_pred, 2, dim=0)
noise, noise_prior = torch.chunk(noise, 2, dim=0)
# Compute instance loss
loss = F.mse_loss(noise_pred, noise, reduction="none").mean([1, 2, 3]).mean()
# Compute prior loss
prior_loss = F.mse_loss(noise_pred_prior, noise_prior, reduction="none").mean([1, 2, 3]).mean()
# Add the prior loss to the instance loss.
loss = loss + args.prior_loss_weight * prior_loss
else:
loss = F.mse_loss(noise_pred, noise, reduction="none").mean([1, 2, 3]).mean()
accelerator.backward(loss)
if accelerator.sync_gradients:
accelerator.clip_grad_norm_(unet.parameters(), args.max_grad_norm)
optimizer.step()
optimizer.zero_grad()
# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
progress_bar.update(1)
global_step += 1
logs = {"loss": loss.detach().item()}
progress_bar.set_postfix(**logs)
if global_step >= args.max_train_steps:
break
accelerator.wait_for_everyone()
# Create the pipeline using using the trained modules and save it.
if accelerator.is_main_process:
pipeline = StableDiffusionPipeline(
text_encoder=text_encoder,
vae=vae,
unet=accelerator.unwrap_model(unet),
tokenizer=tokenizer,
scheduler=PNDMScheduler(
beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", skip_prk_steps=True
),
safety_checker=StableDiffusionSafetyChecker.from_pretrained("CompVis/stable-diffusion-safety-checker"),
feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"),
)
pipeline.save_pretrained(args.output_dir)
跑步训练
#@title Run training
import accelerate
accelerate.notebook_launcher(training_function, args=(text_encoder, vae, unet), num_processes =1)
with torch.no_grad():
torch.cuda.empty_cache()
最后,我们准备开始微调我们的 Dreambooth 概念。如果我们使用不止一个 GPU,我们现在可以利用 accelerate 并相应地更改num_processes
参数。
设置推理管道
一旦训练完成,我们就可以使用推理部分中的代码通过任何提示对模型进行采样。
#@title Set up the pipeline
try:
pipe
except NameError:
pipe = StableDiffusionPipeline.from_pretrained(
args.output_dir,
torch_dtype=torch.float16,
).to("cuda")
为了降低采样成本,我们首先以半精度格式实例化管道。StableDiffusionPipeline.from_pretrained()
函数接受我们到概念目录的路径,使用里面的二进制文件加载到微调模型中。然后,我们可以将 prompt 变量加载到这个管道中,以生成与我们想要的输出相对应的图像。
#@title Run the Stable Diffusion pipeline
prompt = "a photo of a sks cat toy riding a bicycle" #@param {type:"string"}
num_samples = 3 #@param {type:"number"}
num_rows = 3 #@param {type:"number"}
all_images = []
for _ in range(num_rows):
images = pipe([prompt] * num_samples, num_inference_steps=75, guidance_scale=7.5, seed = 'random').images
all_images.extend(images)
grid = image_grid(all_images, num_samples, num_rows)
grid
在演示之后,我们的提示“sks 猫玩具骑自行车的照片”被调用。然后我们可以将num_samples
和num_rows
与image_grid
函数一起使用,将它们放在一个照片网格中。让我们来看一个例子,这些图像是由一个模型生成的,提示为“一张骑自行车的 sks 猫玩具的照片”,该模型在相对较短的 250 个时期内对样本图像进行了训练:
Samples from concept trained for 250 epochs with seed 34354
正如我们所见,它保留了许多原始猫玩具形象的特质。其中一个图像,底部中心,几乎与原始对象相同。当模型试图将它放在自行车上时,其余的图像显示了与原始样本不同程度的质量。发生这种情况的原因很简单:骑自行车的猫可能不存在于原始数据中,因此使用扩散重建比仅重建分散的要素更困难是有道理的。相反,最终的图像展示了随机收集的 Dreambooth 概念的各个方面,这些方面随机地与提示相关的不同功能相统一,但是我们可以看到这些图像如何通过更多的训练来代表我们的提示。
让我们来看看从一个类似的概念延伸到 500 个训练时期的结果:
Samples from concept trained for 500 epochs with seed 34354
如您所见,这些样本中有几个比 250 个纪元的概念更接近人类对提示的定性解释。特别是,中间一排的三个样品显示了骑着非常准确的自行车的粘土猫玩具的所有特征。其他行具有更真实的猫对象,但仍显示比之前测试更完整的自行车图像。这种差异可能是由于在从概念对象的特征进行推断的过程中,在进一步训练的模型中缺乏混杂效应。通过更多地暴露于初始数据,概念中的中心对象的重新创建不太可能干扰从提示推断的附加特征的生成,在这种情况下是自行车。
此外,提请注意随机种子引起的每行的一致变化也很重要。每一行都显示了相似的显著特征,这些特征在整个网格中并不保守。这清楚地表明,种子有效地控制了概念推理的随机性。种子也适用于训练期间,改变它可以对概念训练的结果有重大影响。确保在这两个任务中测试各种种子,以获得最佳的最终输出。
要从这个阶段继续下去,建议尝试将你的训练扩展到 1000 个历元,看看我们的输出如何提高或降低。随着纪元数量的增长,我们需要警惕过度拟合。如果我们输入的图像在背景特征和物体的视角方面不够可变,并且训练足够广泛,那么由于这种过度训练,在生成期间将很难操纵物体离开它们的原始位置。确保输入图像包含不同的内容,以确保推理的最终输出更加可靠。
将模型从扩散器格式转换为原始的稳定扩散检查点文件
最后,现在我们已经完成了 Dreambooth 概念的创建,我们在目录dreambooth-concept
中留下了一个扩散器兼容文件夹。如果我们想将它用于经典的稳定扩散脚本或其他项目,如稳定扩散 Web UI(现在可以轻松地从稳定扩散渐变笔记本运行时运行),那么我们可以使用以下脚本来创建一个可共享、可下载的.ckpt
文件。
# Params
# --model_path = Path to the model to convert.
# --checkpoint_path = Path to the output model.
# --half = store_true, Save weights in half precision/
!git clone https://github.com/huggingface/diffusers
%cd diffusers/scripts
!python convert_diffusers_to_original_stable_diffusion.py --model_path <path to dreambooth concept> --checkpoint_path <name/path of your new model>
%cd ~/../notebooks
这个脚本将把huggingface/diffusers
库克隆到我们的笔记本上。然后,我们将目录改为scripts
,以利用convert_diffusers_to_original_stable_diffusion.py
脚本。只需输入model_path
作为保存 Dreambooth 概念的存储库的路径,输入期望的输出路径和新检查点的名称到checkpoint_path
,如果您想以半精度格式保存它以减少计算开销,请使用half
标志。
结束语
本教程基于 HuggingFace 笔记本回购的扩散器部分的笔记本。在本专题讲座中,我们介绍了在渐变笔记本中从头开始创建 Dreambooth 概念的每个步骤,并演示了如何将该笔记本重新应用于任何类型的图像数据以生成新概念。之后,我们演示了如何在笔记本中对新概念进行采样,以从包含与原始概念相对应的特征的文本提示中生成新图像,并讨论了在训练和推理过程中延长训练和改变种子的效果。最后,我们展示了如何将这个概念提取为一个.ckpt
文件,以便可以在其他项目中重用。
请务必尽快查看本教程系列的第 2 部分,在那里我们将查看文本反转图像嵌入,并展示如何将该技术与 Dreambooth 集成,以创建真正准确的原始概念生成图像。
稳定扩散教程第 2 部分:使用文本反转嵌入获得对生成图像的实质性控制
原文:https://blog.paperspace.com/dreambooth-stable-diffusion-tutorial-part-2-textual-inversion/
在我们的上一个教程中,我们展示了如何使用 Dreambooth 稳定扩散来创建可复制的基线概念模型,以更好地合成与输入图像的主题相对应的对象或风格,有效地微调模型。其他微调稳定扩散的尝试包括移植模型以使用其他技术,如使用 glid-3-XL-stable 的导向扩散。虽然有效,但对于大多数没有强大数据中心 GPU 的人来说,这种策略的计算成本非常高。Dreambooth 的稳健策略只需要 16 GB 的 GPU RAM 来运行,与其他技术相比有了显著的下降。这为更广泛的用户提供了一个更实惠、更容易获得的切入点,让他们加入到快速扩张的稳定扩散用户社区中来。
我们可以尝试的另一个获得类似结果的流行方法是文本反转。就计算而言,它也同样昂贵,因此它是调整模型的一个很好的额外选择。然而,这有点用词不当,因为扩散模型本身并没有被调整。相反,文本倒置“通过在预先训练的文本到图像模型的嵌入空间中使用新的“单词”来描述它们,来学习生成特定的概念,如个人物品或艺术风格。这些可以用在新的句子中,就像其他任何单词一样。”[ 来源实际上,这给了我们控制稳定扩散生成过程的另一端:对文本输入的更大控制。当结合我们用 Dreambooth 训练的概念时,我们可以开始真正影响我们的推理过程。
在本教程中,我们将展示如何在一组预先制作的图像上训练文本反转,这些图像来自我们用于 Dreambooth 的同一数据源。一旦我们遍历了代码,我们将演示如何在从渐变笔记本启动的稳定扩散 Web UI 中将我们的新嵌入与我们的 Dreambooth 概念相结合。
设置和安装
#@title Install the required libraries
!pip install -qq accelerate tensorboard ftfy
!pip install -qq -U transformers
!pip install -qq -U diffusers
!pip install -qq bitsandbytes
!pip install gradio
#create the directories we will use for the task
!mkdir inputs_textual_inversion
!git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui
进入笔记本后,我们可以滚动到第一个代码单元,开始对笔记本进行必要的安装。我们还将创建一个目录来保存用于培训的输入文件。在下一个单元格中,我们导入这些包,并定义一个有用的帮助函数,用于稍后显示我们的图像。
#@title Import required libraries
import argparse
import itertools
import math
import os
import random
import numpy as np
import torch
import torch.nn.functional as F
import torch.utils.checkpoint
from torch.utils.data import Dataset
import PIL
from accelerate import Accelerator
from accelerate.logging import get_logger
from accelerate.utils import set_seed
from diffusers import AutoencoderKL, DDPMScheduler, PNDMScheduler, StableDiffusionPipeline, UNet2DConditionModel
from diffusers.hub_utils import init_git_repo, push_to_hub
from diffusers.optimization import get_scheduler
from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker
from PIL import Image
from torchvision import transforms
from tqdm.auto import tqdm
from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer
## Instantiate helper function
def image_grid(imgs, rows, cols):
assert len(imgs) == rows*cols
w, h = imgs[0].size
grid = Image.new('RGB', size=(cols*w, rows*h))
grid_w, grid_h = grid.size
for i, img in enumerate(imgs):
grid.paste(img, box=(i%cols*w, i//cols*h))
return grid
现在我们已经设置了工作空间,我们需要加载我们的模型。
稳定扩散 v1-5 模型中的加载
https://blog.paperspace.com/content/media/2022/11/stab-upload.mp4
为了使访问稳定扩散模型变得容易并且不占用任何存储空间,我们添加了稳定扩散模型 v1-5 作为可挂载的公共数据集。
要以这种方式使用模型,只需使用笔记本 GUI 最左侧的导航器导航到“Data Sources”选项卡。然后,单击“公共”切换到梯度公共数据集,向下滚动,直到您找到列表底部附近的“稳定扩散-扩散器”。点击“挂载”使这些文件可从datasets
目录中访问。这个目录在根文件夹中,所以可以从工作目录notebooks/
通过路径../datasets/stable-diffusion-diffusers/stable-diffusion-v1-5
访问它。
教授新概念的设置
如前所述,文本反演涉及训练稳定扩散模型,以在从同一模型生成时,通过在功能上为模型创建全新的单词标记来更好地重新创建一组图像的不同特征,以将这些特征归因于该模型。首先,我们需要获取代表我们想要体现的概念的数据。
在这次演示中,我们将使用银河护卫队电影中的塑料玩具格鲁特的图像。我们提供了样本代码,使访问这些图像变得容易。
#@markdown Add here the URLs to the images of the concept you are adding. 3-5 should be fine
urls = [
"https://datasets-server.huggingface.co/assets/valhalimg/--/valhalla--images/train/7/image/image.jpg",
"https://datasets-server.huggingface.co/assets/valhalimg/--/valhalla--images/train/5/image/image.jpg",
"https://datasets-server.huggingface.co/assets/valhalimg/--/valhalla--images/train/7/image/image.jpg"]
现在我们有了 URL,使用下面的代码块将它们下载到您想要的save_path
。我们将使用之前创建的inputs_textual_inversion
。
# @title Setup and check the images you have just added
import requests
import glob
from io import BytesIO
def download_image(url):
try:
response = requests.get(url)
except:
return None
return Image.open(BytesIO(response.content)).convert("RGB")
images = list(filter(None,[download_image(url) for url in urls]))
save_path = "./inputs_textual_inversion"
if not os.path.exists(save_path):
os.mkdir(save_path)
[image.save(f"{save_path}/{i}.jpeg") for i, image in enumerate(images)]
image_grid(images, 1, len(images))
这会将文件保存到您的目录中,并显示一个网格示例,如下所示:
Sample Groot photos
我们在教什么
#@title Settings for your newly created concept
concept_name = "grooty"
#@markdown `initializer_token` is a word that can summarise what your
new concept is, to be used as a starting point
initializer_token = "groot" #@param {type:"string"}
#@markdown `what_to_teach`: what is it that you are teaching? `object` enables you to teach the model a new object to be used, `style` allows you to teach the model a new style one can use.
what_to_teach = "object" #@param ["object", "style"]
#@markdown `placeholder_token` is the token you are going to use to represent your new concept (so when you prompt the model, you will say "A `<my-placeholder-token>` in an amusement park"). We use angle brackets to differentiate a token from other words/tokens, to avoid collision.
placeholder_token = f'<{concept_name}>'
我们现在需要定义我们的概念,以便模型能够理解。我们首先建立我们的concept_name
和initializer_token
变量。初始化标记是一个尽可能概括概念的对象或风格的词。然后,我们定义这个概念是否是一个对象,可能是图像选择中的一个物理对象,或者是一种风格,一个贯穿每幅图像的一致的模式或风格。
概念名也作为我们的placeholder_token
。这用于在一组标准化句子中代替其他单词,帮助模型物理地放置具有提示的特征。我们将把我们的概念命名为“grooty”,并使用初始化标记“groot”。
如果您想使用您自己的自定义数据来代替演示值,您可以将它们上传到目录”。/inputs_textual_inversion "并根据需要更改上面的变量。
创建数据集
既然我们已经设置了输入,我们需要建立我们的placeholder_token
将被使用的句子。这些例句对文本反转过程的整体能力有很大的影响,所以考虑根据需要修改它们以使用不同类型的图像。例如,下面的代码将非常适合这个演示,但是对于试图创建一个人的文本反转嵌入来说是没有帮助的。
#@title Setup the prompt templates for training
imagenet_templates_small = [
"a photo of a {}",
"a rendering of a {}",
"a cropped photo of the {}",
"the photo of a {}",
"a photo of a clean {}",
"a photo of a dirty {}",
"a dark photo of the {}",
"a photo of my {}",
"a photo of the cool {}",
"a close-up photo of a {}",
"a bright photo of the {}",
"a cropped photo of a {}",
"a photo of the {}",
"a good photo of the {}",
"a photo of one {}",
"a close-up photo of the {}",
"a rendition of the {}",
"a photo of the clean {}",
"a rendition of a {}",
"a photo of a nice {}",
"a good photo of a {}",
"a photo of the nice {}",
"a photo of the small {}",
"a photo of the weird {}",
"a photo of the large {}",
"a photo of a cool {}",
"a photo of a small {}",
]
imagenet_style_templates_small = [
"a painting in the style of {}",
"a rendering in the style of {}",
"a cropped painting in the style of {}",
"the painting in the style of {}",
"a clean painting in the style of {}",
"a dirty painting in the style of {}",
"a dark painting in the style of {}",
"a picture in the style of {}",
"a cool painting in the style of {}",
"a close-up painting in the style of {}",
"a bright painting in the style of {}",
"a cropped painting in the style of {}",
"a good painting in the style of {}",
"a close-up painting in the style of {}",
"a rendition in the style of {}",
"a nice painting in the style of {}",
"a small painting in the style of {}",
"a weird painting in the style of {}",
"a large painting in the style of {}",
]
这些提示模板被分为对象和样式列表。现在,我们可以将它们与自定义数据集类一起使用,以便于将它们传递给模型。
#@title Setup the dataset
class TextualInversionDataset(Dataset):
def __init__(
self,
data_root,
tokenizer,
learnable_property="object", # [object, style]
size=512,
repeats=100,
interpolation="bicubic",
flip_p=0.5,
set="train",
placeholder_token="*",
center_crop=False,
):
self.data_root = data_root
self.tokenizer = tokenizer
self.learnable_property = learnable_property
self.size = size
self.placeholder_token = placeholder_token
self.center_crop = center_crop
self.flip_p = flip_p
self.image_paths = [os.path.join(self.data_root, file_path) for file_path in os.listdir(self.data_root)]
self.num_images = len(self.image_paths)
self._length = self.num_images
if set == "train":
self._length = self.num_images * repeats
self.interpolation = {
"linear": PIL.Image.LINEAR,
"bilinear": PIL.Image.BILINEAR,
"bicubic": PIL.Image.BICUBIC,
"lanczos": PIL.Image.LANCZOS,
}[interpolation]
self.templates = imagenet_style_templates_small if learnable_property == "style" else imagenet_templates_small
self.flip_transform = transforms.RandomHorizontalFlip(p=self.flip_p)
def __len__(self):
return self._length
def __getitem__(self, i):
example = {}
image = Image.open(self.image_paths[i % self.num_images])
if not image.mode == "RGB":
image = image.convert("RGB")
placeholder_string = self.placeholder_token
text = random.choice(self.templates).format(placeholder_string)
example["input_ids"] = self.tokenizer(
text,
padding="max_length",
truncation=True,
max_length=self.tokenizer.model_max_length,
return_tensors="pt",
).input_ids[0]
# default to score-sde preprocessing
img = np.array(image).astype(np.uint8)
if self.center_crop:
crop = min(img.shape[0], img.shape[1])
h, w, = (
img.shape[0],
img.shape[1],
)
img = img[(h - crop) // 2 : (h + crop) // 2, (w - crop) // 2 : (w + crop) // 2]
image = Image.fromarray(img)
image = image.resize((self.size, self.size), resample=self.interpolation)
image = self.flip_transform(image)
image = np.array(image).astype(np.uint8)
image = (image / 127.5 - 1.0).astype(np.float32)
example["pixel_values"] = torch.from_numpy(image).permute(2, 0, 1)
return example
该数据集对象通过根据需要对图像输入进行转换和整形以提高训练期间的整体模型锐度,从而确保图像输入以文本反转的方式最佳运行。
下载模型文件
#@title Load the Stable Diffusion model
#@markdown set `pretrained_model_name_or_path` to which Stable Diffusion checkpoint you want to use
## Use local files
pretrained_model_name_or_path = "../datasets/stable-diffusion-diffusers/stable-diffusion-v1-5" #@param {type:"string"}
## Download online files
#@markdown Please read and, if you agree, accept the LICENSE [here](https://huggingface.co/runwayml/stable-diffusion-v1-5) if you see an error
# pretrained_model_name_or_path = "runwayml/stable-diffusion-v1-5" #@param {type:"string"}
现在,如果我们在渐变笔记本上运行,那么我们有两个选择。之前,我们将模型安装在公共数据集目录中,可以在工作目录中的../datasets/stable-diffusion-diffusers/stable-diffusion-v1-5
处访问它们。
如果我们想使用 Runway ML 回购的在线版本,那么我们可以讨论出较低的行。这将把模型下载到缓存中,并计入存储。您需要将您的 Huggingface 令牌粘贴到笔记本顶部的单元格中,标题为“备用访问:登录 HuggingFace 以在线访问模型”
设置我们的新令牌
#@title Load the tokenizer and add the placeholder token as a additional special token.
#@markdown Please read and, if you agree, accept the LICENSE [here](https://huggingface.co/runwayml/stable-diffusion-v1-5) if you see an error
tokenizer = CLIPTokenizer.from_pretrained(
pretrained_model_name_or_path,
subfolder="tokenizer")
# Add the placeholder token in tokenizer
num_added_tokens = tokenizer.add_tokens(placeholder_token)
if num_added_tokens == 0:
raise ValueError(
f"The tokenizer already contains the token {placeholder_token}. Please pass a different"
" `placeholder_token` that is not already in the tokenizer."
)
接下来,我们将从模型的目录中加载 CLIPTokenizer。然后,我们可以添加新令牌作为新令牌。这样,当我们训练时,新的令牌将能够与图像中的特征相关联。
#@title Get token ids for our placeholder and initializer token. This code block will complain if initializer string is not a single token
# Convert the initializer_token, placeholder_token to ids
token_ids = tokenizer.encode(initializer_token, add_special_tokens=False)
# Check if initializer_token is a single token or a sequence of tokens
if len(token_ids) > 1:
raise ValueError("The initializer token must be a single token.")
initializer_token_id = token_ids[0]
placeholder_token_id = tokenizer.convert_tokens_to_ids(placeholder_token)
然后,我们将对initializer_token
和placeholder_token
进行编码,以获得它们的令牌 id。如果生成了多个令牌,它将提示我们输入单个令牌。这很可能是由于输入一个短语或句子作为占位符标记而导致的。
#@title Load the Stable Diffusion model
# Load models and create wrapper for stable diffusion
text_encoder = CLIPTextModel.from_pretrained(
pretrained_model_name_or_path, subfolder="text_encoder",
)
vae = AutoencoderKL.from_pretrained(
pretrained_model_name_or_path, subfolder="vae",
)
unet = UNet2DConditionModel.from_pretrained(
pretrained_model_name_or_path, subfolder="unet",
)
最后,我们加载稳定扩散 V1-5 模型的text_encoder
、vae
和unet
子组件。
text_encoder.resize_token_embeddings(len(tokenizer))
token_embeds = text_encoder.get_input_embeddings().weight.data
token_embeds[placeholder_token_id] = token_embeds[initializer_token_id]
由于我们已经在tokenizer
中添加了placeholder_token
,我们需要在这里重新调整令牌嵌入的大小,并在令牌嵌入中为我们的placeholder_token
创建一个新的嵌入向量。然后,我们可以用初始化器标记的嵌入来初始化新添加的占位符标记。
def freeze_params(params):
for param in params:
param.requires_grad = False
# Freeze vae and unet
freeze_params(vae.parameters())
freeze_params(unet.parameters())
# Freeze all parameters except for the token embeddings in text encoder
params_to_freeze = itertools.chain(
text_encoder.text_model.encoder.parameters(),
text_encoder.text_model.final_layer_norm.parameters(),
text_encoder.text_model.embeddings.position_embedding.parameters(),
)
freeze_params(params_to_freeze)
因为我们只训练新添加的嵌入向量,所以我们可以在这里冻结其余的模型参数。至此,我们已经完成了训练数据集的设置,并可以加载它了。我们将使用它来创建用于培训的数据加载器。
train_dataset = TextualInversionDataset(
data_root=save_path,
tokenizer=tokenizer,
size=512,
placeholder_token=placeholder_token,
repeats=100,
learnable_property=what_to_teach, #Option selected above between object and style
center_crop=False,
set="train",
)
def create_dataloader(train_batch_size=16):
return torch.utils.data.DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
培训设置
在开始运行训练之前,我们需要定义噪声调度器和训练超参数,并创建训练函数本身。
noise_scheduler = DDPMScheduler(
beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000, tensor_format="pt"
)
在这个例子中,我们将使用 DDPMScheduler,但是像 PLMS 这样的其他调度器可能会产生更好的结果。考虑选择不同的调度程序,看看训练结果有何不同。
hyperparameters = {
"learning_rate": 5e-04,
"scale_lr": True,
"max_train_steps": 1000,
"train_batch_size": 1,
"gradient_accumulation_steps": 1,
"seed": 42,
"output_dir": f'/notebooks/concepts/{concept_name}-concept',
}
接下来,我们设置超参数进行训练。特别是,考虑改变max_train_steps
和seed
来更好地控制嵌入的结果。更高的训练步长值将导致更精确的概念表示,并且改变种子将改变扩散模型用来构建样本图像以计算损失的“随机性”。此外,如果我们在一个拥有超过 16GB VRAM 的 GPU 上,我们可以更改train_batch_size
,并将output_dir
更改为我们选择的任何位置。
培训功能
def training_function(text_encoder, vae, unet):
logger = get_logger(__name__)
train_batch_size = hyperparameters["train_batch_size"]
gradient_accumulation_steps = hyperparameters["gradient_accumulation_steps"]
learning_rate = hyperparameters["learning_rate"]
max_train_steps = hyperparameters["max_train_steps"]
output_dir = hyperparameters["output_dir"]
accelerator = Accelerator(
gradient_accumulation_steps=gradient_accumulation_steps,
# distributed_type='MULTI_GPU'
# distribute_type: Accelerate.DistributedType.MULTI_GPU
# fp16=True,
# cpu=True,
)
train_dataloader = create_dataloader(train_batch_size)
if hyperparameters["scale_lr"]:
learning_rate = (
learning_rate * gradient_accumulation_steps * train_batch_size * accelerator.num_processes
)
# Initialize the optimizer
optimizer = torch.optim.AdamW(
text_encoder.get_input_embeddings().parameters(), # only optimize the embeddings
lr=learning_rate,
)
text_encoder, optimizer, train_dataloader = accelerator.prepare(
text_encoder, optimizer, train_dataloader
)
# Move vae and unet to device
vae.to(accelerator.device)
unet.to(accelerator.device)
# Keep vae and unet in eval model as we don't train these
vae.eval()
unet.eval()
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / gradient_accumulation_steps)
num_train_epochs = math.ceil(max_train_steps / num_update_steps_per_epoch)
# Train!
total_batch_size = train_batch_size * accelerator.num_processes * gradient_accumulation_steps
logger.info("***** Running training *****")
logger.info(f" Num examples = {len(train_dataset)}")
logger.info(f" Instantaneous batch size per device = {train_batch_size}")
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}")
logger.info(f" Gradient Accumulation steps = {gradient_accumulation_steps}")
logger.info(f" Total optimization steps = {max_train_steps}")
# Only show the progress bar once on each machine.
progress_bar = tqdm(range(max_train_steps), disable=not accelerator.is_local_main_process)
progress_bar.set_description("Steps")
global_step = 0
for epoch in range(num_train_epochs):
text_encoder.train()
for step, batch in enumerate(train_dataloader):
with accelerator.accumulate(text_encoder):
# Convert images to latent space
latents = vae.encode(batch["pixel_values"]).latent_dist.sample().detach()
latents = latents * 0.18215
# Sample noise that we'll add to the latents
noise = torch.randn(latents.shape).to(latents.device)
bsz = latents.shape[0]
# Sample a random timestep for each image
timesteps = torch.randint(0, noise_scheduler.num_train_timesteps, (bsz,), device=latents.device).long()
# Add noise to the latents according to the noise magnitude at each timestep
# (this is the forward diffusion process)
noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)
# Get the text embedding for conditioning
encoder_hidden_states = text_encoder(batch["input_ids"])[0]
# Predict the noise residual
noise_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample
loss = F.mse_loss(noise_pred, noise, reduction="none").mean([1, 2, 3]).mean()
accelerator.backward(loss)
# Zero out the gradients for all token embeddings except the newly added
# embeddings for the concept, as we only want to optimize the concept embeddings
if accelerator.num_processes > 1:
grads = text_encoder.module.get_input_embeddings().weight.grad
else:
grads = text_encoder.get_input_embeddings().weight.grad
# Get the index for tokens that we want to zero the grads for
index_grads_to_zero = torch.arange(len(tokenizer)) != placeholder_token_id
grads.data[index_grads_to_zero, :] = grads.data[index_grads_to_zero, :].fill_(0)
optimizer.step()
optimizer.zero_grad()
# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
progress_bar.update(1)
global_step += 1
logs = {"loss": loss.detach().item()}
progress_bar.set_postfix(**logs)
if global_step >= max_train_steps:
break
accelerator.wait_for_everyone()
# Create the pipeline using using the trained modules and save it.
if accelerator.is_main_process:
pipeline = StableDiffusionPipeline(
text_encoder=accelerator.unwrap_model(text_encoder),
vae=vae,
unet=unet,
tokenizer=tokenizer,
scheduler=PNDMScheduler(
beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", skip_prk_steps=True
),
safety_checker=StableDiffusionSafetyChecker.from_pretrained("CompVis/stable-diffusion-safety-checker"),
feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"),
)
pipeline.save_pretrained(output_dir)
# Also save the newly trained embeddings
learned_embeds = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[placeholder_token_id]
learned_embeds_dict = {placeholder_token: learned_embeds.detach().cpu()}
torch.save(learned_embeds_dict, os.path.join(output_dir, "learned_embeds.bin"))
下面是上述街区所发生情况的大致分类:
- 首先,我们初始化优化器、数据集和 text_encoder,并将它们加载到 accelerate 中
- 接下来,我们将 VAE 和 UNET 移到 GPU,并将它们设置为
.eval()
,因为它们不需要被训练 - 然后,我们重新计算我们的总训练步骤,因为训练数据加载器的大小可能已经改变,并且从
train_batch_size
乘以正在进行的过程(进行训练的机器)的数量和gradient_accumulation_steps
来计算我们的total_batch_size
- 然后,我们根据总训练步数计算出的
num_train_epochs
进行训练 - 对于每个时期,我们在
text_encoder
上调用 train,并逐批逐步处理每个输入 - 对于每一批,图像首先被转换成潜在空间。然后,我们使用这些潜在值为扩散过程生成样本噪声
- 然后,我们为每幅图像采样一个随机时间戳,并将由每个时间戳的噪声确定的大小的样本噪声添加到 latents。这是功能性向前扩散
- 然后,我们得到文本嵌入,并使用它来预测噪声残差,以计算该步骤的损失
- 最后,我们将所有标记嵌入的梯度清零,除了新添加的概念嵌入。因为我们只想优化概念嵌入,这将防止任何混淆的影响
- 然后,我们获取我们希望将梯度清零的标记的索引,并清零优化器,然后继续下一个循环
- 训练循环完成后,我们使用 StableDiffusionPipeline 将模型文件和
learned_embeds.bin
文件保存到我们定义为output_dir
的目录中
跑步训练
现在,我们已经运行并尝试理解了代码生成嵌入所采取的每个步骤,我们可以使用 accelerate 运行我们的训练函数,使用下面的代码单元格来获得图像嵌入:
import accelerate
accelerate.notebook_launcher(training_function, args=(text_encoder, vae, unet), num_processes=1)
该演示的嵌入保存到concepts/grooty-demo/learned_embeds.bin
。
使用新训练的嵌入运行推理代码
我们可以使用StableDiffusionPipeline
函数,通过新训练的图像嵌入对稳定的扩散模型进行采样。首先,我们需要实例化管道。
#@title Set up the pipeline
pipe = StableDiffusionPipeline.from_pretrained(
hyperparameters["output_dir"],
# "downloaded_embedding",
torch_dtype=torch.float16,
device_map="auto",
).to("cuda")
然后,我们可以使用之前的placeholder_token
值对模型进行采样,将图像嵌入的质量和特征传递给模型的输出。对于该示例,我们将使用提示“一个半透明的中国玉雕像placeholder_token
、HDR、productshot render、Cinema4D、焦散”
#@title Run the Stable Diffusion pipeline
#@markdown Don't forget to use the placeholder token in your prompt
prompt = f"a translucent jade chinese figurine of {placeholder_token}, HDR, productshot render, Cinema4D, caustics" #@param {type:"string"}
num_samples = 5 #@param {type:"number"}
num_rows = 4 #@param {type:"number"}
# prevent safety checking
def dummy(images, **kwargs):
return images, False
pipe.safety_checker = dummy
all_images = []
for _ in range(num_rows):
images = pipe([prompt] * num_samples, num_inference_steps=50, guidance_scale=7.5).images
all_images.extend(images)
grid = image_grid(all_images, num_samples, num_rows)
grid
如果一切运行正常,您应该会得到一个 5x4 的图像网格,如下所示:
a 4x5 grid of samples from this demo
正如我们所看到的,模型显然已经能够理解格鲁特玩具的特征。值得注意的是,几乎每张照片中都出现了没有瞳孔的大眼睛和尖尖的头部结构。
尝试增加或减少max_train_steps
变量,查看增加的训练如何影响嵌入的拟合度。也要警惕过度拟合,因为如果图像中的特征过于一致,模型就有可能无法概括背景特征之类的东西。例如,由每张照片中站在特定建筑物外的人组成的训练数据集,除了对象之外,还可能产生建筑物作为嵌入的特征。
使用我们的新嵌入稳定扩散网络用户界面
现在,我们有了新的嵌入,我们也可以使用它与我们的 Dreambooth 模型一起使用,该模型在上一节课中使用稳定扩散 Web UI 进行了培训。我们所需要做的就是将它移动到 Web UI 的 embeddings 文件夹中,我们可以将这种嵌入用于我们拥有的任何 Web UI 模型,包括 Dreambooth 检查点。
- 首先,让我们在概念文件夹中找到我们的
learned_embed.bin
文件,concepts/grooty-concept
(如果您跟随演示的话) - 第二,你会想给它重新命名,以反映你的概念。我选择了
grooty.bin
- 第三,将文件移动到 Web UI 的嵌入文件夹中
然后在任何提示中使用您的占位符令牌来突出您的嵌入!使用下面的单元格将演示文本倒置嵌入移动到 Stable Diffusion Web UI 报告:
!mv concepts/grooty-concept/learned_embeds.bin stable-diffusion-webui/embeddings/grooty.bin
使用预先训练好的嵌入和公共数据集模型文件启动 Web UI
%cd stable-diffusion-webui
# launch the webui
!python launch.py --share --ckpt ../../datasets/stable-diffusion-classic/v1-5-pruned-emaonly.ckpt
最后,我们可以启动 Web UI。在这里,如果您也保存了 Dreambooth 概念,我们现在可以将两种不同培训方法的效果结合起来。下面是一个例子,使用我们的玩具猫 Dreambooth 模型,使用提示“一个半透明的中国玉雕像,HDR sks,productshot render,Cinema4D,caustics”。记住grooty
代表我们的新图像嵌入,sks
提示猫玩具对象。
Textual Inversion on Groot toy with Dreambooth run on Cat toy
正如我们所看到的,我们得到了一个猫玩具对象的初步展示,它有着不同的特征,比如颜色,眼睛和手开始看起来更像格鲁特玩具。如果我们要增加任何一个提示的权重(使用括号“()”来完成,就像这个感叹词),我们可以增加任何一个特性的呈现。
结束语
在前两篇文章中,我们研究了调整稳定扩散的两种不同方法。首先, Dreambooth 进行了适当的微调以生成一个新的检查点。今天,我们研究了纹理反转,它通过在预训练的文本到图像模型的嵌入空间中使用新的“单词”来描述个人物品或艺术风格,从而学习生成特定的概念。
本教程的读者现在应该准备好在他们自己输入的图像上训练文本反转,然后使用这些嵌入来使用StableDiffusionPipeline
和稳定扩散 Web UI 生成图像。我们鼓励您继续尝试这些笔记本电脑,并使用 Textual Inversion 和 Dreambooth 对您的稳定扩散输出进行前所未有的控制。
端到端自动语音识别技术现状
原文:https://blog.paperspace.com/end-to-end-automatic-speech-recognition-state-of-the-art/
介绍
在经历了语音处理并掌握了理解音乐和语音所需的一般概念之后,我们处于一个很好的位置来探索涉及深度学习的 ASR 或自动语音识别的最新方法。我们将看到一些在机器学习的声音和 ASR 领域的研究社区中取得重大成就的最新论文。
以下是该领域的一些重要论文,从最老的开始。
💡Gradient Notebooks for three packages (DeepSpeech2, Wav2Vec2, Conformer) can be found in their respective sections
深度演讲
DeepSpeech 是首批通过摆脱背景噪声、混响、说话人变化等手工设计的系统,使基于深度学习的 ASR 受到欢迎的论文之一。它也不需要音素字典。这是通过使用一个基于 RNN 的网络来学习这些效果而不是硬编码来实现的。
单层 RNN 被夹在三个 1D 卷积和一个全连接层之间,并在语音频谱图上被训练。完全连接的层使用限幅 ReLu 激活函数。第四层或第一 RNN 层是双向的,最后一层将前向和后向表示作为输入,然后使用 softmax 激活。该模型被期望将时间序列中的频率仓信息作为输入,并在给定先前输入和先前字符的情况下输出不同字符的概率。
他们还使用了一些基本的数据增强方法,如及时翻译频谱图(5 毫秒)。对于正则化,在非递归层上使用了 5-10%的下降。
语言模型
为了用语音 RNN 进行解码,他们使用一种语言模型来帮助纠正拼写错误,这种错误在语音上是正确的,但不符合语言拼写和语法。所使用的语言模型是基于 n-gram 的模型,该模型是使用 KemLM 工具包训练的。
该模型的综合目标是找到一个将最大化以下内容的序列
$ Q(c)= log(P(c | x))+\ alpha log(P _ { lm }(c))+\ beta * word \ _ count(c)$
其中\(\alpha\)和\(\beta\)是可调参数,用于控制 RNN、语言模型和句子长度之间的权衡。
使用典型波束尺寸在 1000-8000 美元范围内的波束解码器。
数据和模型并行性
为了提高模型在训练和推理过程中的效率,使用了不同的并行机制。数据被分割到多个 GPU 计算梯度上,用于小批量,这些小批量是通过将相似长度的样本组合在一起并用静音填充而创建的。梯度更新以通常的方式计算,即通过将并行任务的梯度相加。
数据并行在训练中产生加速,但是面临折衷,因为较大的小批量不能提高训练收敛速度。另一种向上扩展的方法是并行化模型。这是通过为非递归层在时间上分割模型来实现的。序列的一半分配给一个 GPU,另一半分配给另一个 GPU。
对于循环层,第一个 GPU 计算前半部分,即前向激活,而第二个 GPU 计算后向激活。然后,两者交换角色,交换中间激活,并完成向前和向后的计算。
RNN 大步走来
最后,为了扩大训练和推断而进行的另一个优化是使用步长为 2 的 RNN,使得展开的 RNN 的大小是原始未拉伸的 RNN 的一半。
深度演讲 2
Deep Speech 2 用英语和普通话这两种截然不同的语言演示了端到端 ASR 模型的性能。除了对模型架构进行实验,本文中很大一部分工作都是针对使用 HPC(高性能计算)技术来提高深度学习模型的性能,这使得在几天内而不是几周内运行训练实验成为可能。他们还表明,通过在数据中心的 GPU 上使用一种称为批处理调度的技术,可以以一种廉价的方式为低延迟在线 ASR 部署模型。
用于此的模型比 DeepSpeech 模型更深。有几个双向递归图层与批处理规范化一起使用。CTC 损失被用作损失函数,以将语音发射与文本预测对齐。在一些实验中,循环层也被 GRUs 代替。对于所有网络深度,GRU 架构都实现了更好的 WER(字差错率)。
批量标准化
跨层的批量标准化能够在加速训练的同时改善最终的泛化误差。递归网络的批量归一化有点棘手,在论文中,他们提到序列式归一化能够纠正在使用步进式归一化和在连续时间步长上累积平均值时出现的问题,这两种方法都没有改善优化,但增加了反向传播的复杂性。
BatchNorm 在训练中工作得很好,但是在部署的在线 ASR 系统中变得难以实现,因为需要评估每个话语而不是一批。它们存储训练期间收集的每个神经元的均值和方差的运行平均值,并在推理部署中使用这些值。
索塔格拉德
较长的序列通常对成本函数(在这种情况下是 CTC)有更大的贡献,并且被用作话语难度的启发式。然后,按照话语长度对数据进行分类,并相应地为第一个时期创建小批量。对于接下来的时期,使用小批量的随机抽样。作者称这种方法为“分类分级”。随着模型越来越深入,这种课程策略提高了模型的性能。与 BatchNorm 一起使用时,效果会更明显。
这种方法的有效性可以归因于这样一个事实,即我们经常使用相同的学习率,即使序列大小的差异相当于梯度值的实质性差异。
频率卷积
时间卷积在 DeepSpeech 和 DeepSpeech 2 中使用。在 DeepSpeech 2 中,使用的特征是时域和频域的。DeepSpeech 1 使用频谱图作为输入,时间卷积作为第一层。DeepSpeech 2 实验了一个和三个这样的卷积层。它们以“相同”模式应用卷积,允许它们在频率和时间上保持输入特征。他们发现 1D 不变卷积对干净的或有噪声的数据没有太大的好处。2D 不变卷积大大改善了有噪声数据的结果,并略微改善了干净数据的结果。
这些回旋的跨越不是以一种简单的方式完成的,因为那不会产生很大的结果。取而代之的是,他们使用非重叠的双元词作为他们的标签,而不是数量更大并且占据几个更大时间步长的词。例如,具有非重叠二元模型的句子“cat sat”被分段为\([th,e,space,ca,t,space,sa,t]\)。请注意,奇数单词的最后一个单词变成了单字。一个空格也被认为是一个单字。结果如下所示。
行卷积
他们设计了一个特殊的层,以消除 DeepSpeech 1 中使用的 RNNs 中的双向特性。它们使用特定时间步长的未来上下文,并基于特征矩阵\(h_{t:t + \tau} = [h_t,h_{t+1},...,h_{t + \tau}]\)大小为$ d x (\tau + 1)\(和一个相同大小的参数矩阵 W。\)\tau$此处表示未来上下文中的时间步长。
由于上面显示的类似卷积的操作,他们称之为行卷积。
wav2 字母
本文作者使用三种不同的输入特征: MFCCs、功率谱和原始波形。这个模型的输出是每个字母一个分数,给定一个字典\(L\)。他们使用标准的 1D 卷积网络进行声学建模。除了前两层使用步长卷积以使训练更有效。其思想是,输入序列可以很长,并且卷积的第一层如果是步进的,则可以减少其后所有层所需的计算。一个 4-gram 语言模型和一个 beam 解码器用于最终的转录。
自动分段标准(CTC 替代方案)
CTC graph
ASG graph
本文的主要贡献是他们使用的替代损失,称为自动分段标准。它与流行的 CTC 损耗有以下几个方面的不同:
- 没有空白标签。他们发现空白标签在模拟“垃圾”帧时不起作用。至于模拟字母中的重复,他们使用不同的方式用额外的标签来编码。例如,\(卡特彼勒\)变成了\(卡特彼勒 2ar\)。
- CTC 标准图中所有节点的未标准化分数。这使得插入不同的语言分数更加简单。
- 全局规格化代替每帧规格化。
贾斯珀
Jasper 在模型中只使用 1D 卷积(类似于 Wav2Letter)、批量归一化、ReLu、dropout 和残差连接。他们还提出了一个名为 NovoGrad 的分层优化器。
本文对该体系结构的解释如下:
Jasper 使用从具有 10ms 重叠的 20ms 窗口计算的 MFCCs,并输出每帧字符的概率分布。Jasper 有一个块架构:一个 Jasper BxR 模型有 B 个块,每个块有 R 个子块。每个子块应用以下操作:1D 卷积、批量范数、ReLU 和丢失。一个块中的所有子块具有相同数量的输出通道。”
他们还构建了一个 Jasper 变体,它不是在一个块内有连接,而是跨块有连接。每个卷积块的输出被加到所有后续卷积块的输入上。他们称之为贾斯珀博士或贾斯珀稠密剩余模型。
诺沃格勒
NovoGrad 类似于 Adam Optimizer ,它通过查看从梯度和常数参数计算的一阶和二阶矩来计算每个优化步骤的自适应学习率。它的一部分类似于动量,但 Adam 在基于动量的优化速度较高的情况下表现更好,因为它根据二阶矩为梯度步长提供了相反的力。
在 NovoGrad 中,它的二阶矩是按层而不是按重量计算的。这减少了内存消耗,并且作者发现这在数值上更加稳定。在每一步,NovoGrad 正常计算随机梯度。各层的二阶矩计算如下:
$ v ^ {l}{t} = \beta。v ^ {l}{t-1} + {1 - \beta{2}}。|| g ^{l}_{t} || ^ {2}$
在计算一阶矩之前,二阶矩用于重新缩放梯度\(g_{t}^{l}\)。使用 L2 正则化,并且权重衰减被添加到重新缩放的梯度。
$ m^{l}{t} = \beta。m ^ { l } _ { t-1 }+\ frac { g ^ { l } _ { t } } { \ sqrt { v ^ { l } _ { t }+\ epsilon } }+d . w _ { t } $
Wav2Vec
Wav2Vec 探索语音识别的无监督预训练。他们使用原始音频作为输入,并使用对比损失对大量数据进行训练。他们使用这些表示来微调受监督的任务模型,并发现它们的性能与最先进的水平相当。使用的模型是为下一个时间步长预测任务训练的简单多层卷积层。
编码器网络+上下文网络
他们将两个网络应用于原始音频信号输入。编码器网络将音频信号嵌入潜在空间中,并且上下文网络组合编码器的多个时间步长以获得上下文化的表示。编码器网络是五层卷积网络。编码器的输出是低频表示。另一方面,上下文网络具有 9 层,并且将多个潜在表示混合到单个上下文张量中。它将编码器网络的输出作为输入。
the encoder network maps the input X to Z which is then mapped to C through the context network.
编码器和上下文网络中的每一层都包括具有 512 个通道的因果卷积、归一化层和 ReLu 激活。归一化是在时间和特征维度上完成的,以避免由于缩放和输入偏移而引起的变化。对于更大的模型,作者发现在编码器和上下文网络中引入剩余连接是很重要的。在上下文网络的末端应用完全连接的层,以产生上下文嵌入。
对比损失
对比损失用于比较干扰项样本(通过应用于输入直到所需时间步长的先验分布创建)和上下文嵌入的 sigmoid 之间的相似性。通过将从时间\(t\)直到时间\(t + k\)的干扰物样本与上下文嵌入的 sigmoid 进行比较,找到损失。
他们通过替换 MFCC 表示法在几个数据集上测试这些嵌入。解码是使用一个 4-gram KenLM 语言模型和一个 beam 解码器来完成的。使用的优化器是 Adam 和余弦学习率时间表。
Wav2Vec 2.0
这项工作建立在 Wav2Vec 模型的基础上,通过在堆叠 CNN 的顶部添加一个掩蔽的变换器,CNN 表示的量化形式和对比损失考虑了时间步长$ T \(的变换器输出和从\) t = 1 开始的 CNN 的量化表示....新台币。
该模型通过在不同的时间步骤屏蔽输入,在大量未标记的数据上进行训练,类似于 Wav2Vec,然后针对监督任务进行训练,以与其他方法和框架进行比较。
特征编码器
编码器由几个时间卷积+层归一化+ GELU 激活块组成。从卷积编码器生成的表示被传递到遵循变换器架构的上下文网络。来自变换器架构的固定位置嵌入被另一个卷积层取代,该卷积层的作用类似于相对位置嵌入。
量化模块
乘积量化用于量化来自卷积编码器的表示。乘积量化包括将每个矢量分割成子空间,并使用 k-均值聚类找到每个子空间的质心,这些质心代表一个码本。一旦聚类模型被训练,对于每个子空间,每个向量被映射到它离码本最近的质心。每个子空间的每个质心被转换成消耗更少存储器的符号码(因此命名为码本)。这些最终向量的级联版本是我们的乘积量化表示。Gumbel Softmax 用于将特征编码器的输出映射到 logits。
对比损失
总的优化目标如下:
其中$ L_m \(是对比损失,其在后向传递中将训练模型从\) K $个干扰物样本中识别真实的量化潜在语音表示。
其中$ c_t \(是上下文向量,\) q_t \(是量化表示,\) q \(是干扰项集,\) q_t $是。
多样性损失
分集损失被设计成增加量化码本表示的使用。这是通过在一批话语中的每个码本的码本条目上最大化平均 softmax 分布的熵来实现的。
其中$ G \(是码本的数量,\) V \(是每个码本中的条目,\) p_g \(表示每个码本,\) p_{g,v} \(是\) p_g \(中的\) v^{th} $条目。
ContextNet
ContextNet 超越了 CNN-RNN 换能器架构,并对具有挤压和激励模块的全卷积层进行了实验。他们还介绍了一种简单的网络扩展方法,这种方法在精度与计算的权衡上提供了良好的结果。作者认为,与基于变压器或基于 RNN 的模型相比,基于 CNN 的架构由于有限的内核大小而缺乏全局上下文是其性能较低的主要原因,因为后者在技术上可以访问整个建模序列。
压缩和激励网络
为了解决 CNN 无法在给定序列的较长上下文中进行概括的问题,他们在卷积架构中引入了挤压和激励模块。挤压和激励模块通过将多通道、多特征输入编码到每个通道的单个神经元中来工作。该表示由完全连接的层(压缩)编码成缩小了因子\(r\)的更小的表示,因此将神经元的数量减少到$ C / r $。然后,这些神经元通过另一个完全连接的层(激励),将压缩的表示传播回原始全局上下文向量的大小。
作者使用 RNN-T 解码代替 CTC 解码器。他们还使用了 swish 激活功能,该功能可以轻微但持续地减少 WER。
本文的另一个贡献是缩放方案,该方案通过全局改变各种卷积层中的通道数量来逐步对输入进行下采样。这大大减小了模型的大小,同时保持了良好的准确性。
Emformer
这篇论文的名字来源于“高效内存转换器”,因为他们优化了转换器架构,以便为低延迟流语音识别应用提供服务。本文建立在 AM-TRF 方法的基础上。
美国 TRF 时间
AM-TRF 或“增强内存转换器”限制输入的左上下文长度,将输入序列分成块以供转换器并行化,并使用内存库来捕获每个块中的自我关注无法捕获的长期相关性。
输入被分成\(I\)个不重叠的片段\(C_{1} ^ {n}...C_{I - 1} ^ {n}\)。假设\(n\)是图层索引。为了避免边界效应,这些输入块中的每一个都与左右上下文块$L_{i}^{n} \(和\) R_{i} ^ {n}\(连接,以形成上下文片段\)X_{i} ^ {n}\(。AM-TRF 层占用一个内存库\) M_{i} ^ {n} = [m_{1} ^ {n}...m_{i - 1} ^ {n}]\(和\) X_{i} ^ {n}\(作为输入和输出\) X_{i + 1} ^ {n}\(和内存向量\)m_{i} ^ {n}$组成。
缓存键和值
作者认识到 AM-TRF 在流式语音识别任务上表现良好,但是它的体系结构有一些缺点。例如,它没有有效地使用左侧上下文。如上图所示,每次都必须重新计算左边的上下文向量,即使它们可能与中间的上下文重叠。
他们提出了一种左上下文的键值缓存机制,可以从前面的片段中继承,大大减少了计算量。
内存结转
AM-TRF 中的块处理机制要求每个时间步都将前一时间步的存储体作为同一层的输入。这限制了 GPU 的并行化和使用。为了允许更快的计算,Emformer 将来自注意机制的内存向量输出传递到下一层的下一个段,而不是同一层的下一个块。
右上下文泄漏
此外,为了避免前瞻上下文泄漏,Emformer 模型在训练期间使用掩蔽机制,这与使用顺序块处理的 AM-TRF 模型形成对比。
"Emformer 制作每个段的前瞻上下文的硬拷贝,并将前瞻上下文拷贝放在输入序列的开头。例如,第一块中的帧 2 处的输出仅使用来自当前块的信息以及右上下文帧 4,而没有右上下文泄漏。”
符合
Conformer 或“卷积增强变换器”以参数有效的方式融合了两个世界的优点——卷积的局部特征学习和递归网络和变换器的全局上下文学习。
Conformer 编码器
conformer 模块有四个组成部分,即前馈模块、自关注模块、卷积模块和最后的第二前馈模块。
对于自我关注模块,他们从 Transformer-XL 论文及其相对正弦位置编码方案中获得灵感。这有助于模型更好地概括不同的序列长度。
卷积模块使用带有 GLU 激活的逐点卷积、1D 深度卷积、带有 swish 激活的批量归一化,并以另一个逐点卷积和丢弃结束。
前馈模块由两个线性变换和其间的一个非线性变换(swish 激活)组成。
所提出的 conformer 模块将多头自关注模块和卷积模块夹在两个前馈模块之间。
单层 LSTM 解码器被用作他们所有实验的解码器。他们扫描了三种不同模型尺寸的几个建筑超参数- 10M、30M 和 118M 参数。
结论
在前几篇文章中,我们一直在研究音频数据。我们研究了音频处理和音乐分析,并且在上一篇文章中探索了自动语音识别问题的基础。在本文中,我们探索了人们利用深度学习来解决端到端 ASR 问题的方法,使用时间卷积、递归网络、变压器架构、其中每一种的变体、其中一种以上的混合等等。我们还研究了从计算的角度看待问题的一些方法,这些方法包括使用量化压缩网络、修改模型架构,以及将它们并行化以提高效率。
在接下来的文章中,我们将研究开源的端到端框架,并使用 Conformer 架构实现一个训练和预测管道。
端到端自动语音识别:简介
原文:https://blog.paperspace.com/end-to-end-automatic-speech-recognition/
介绍
自动语音识别或 ASR 在深度学习社区中更普遍地被称为是消费语音音频信号并输出所述语音输入的准确文本表示的能力。这个研究领域和许多其他领域一样,其发展停滞不前,直到深度学习方法使新技术能够提高 ASR 模型的性能和效率。
在本系列的前两部分中,我们探索了一些工具,这些工具使我们能够更深入地研究声学建模,以完成更艰难、更具挑战性的下游任务。在您开始阅读本文之前,我建议您先阅读上面提到的第一批文章,以掌握音频信号处理的基础知识。
在本文中,我们将尝试剖析常见的 ASR 管道,这些管道允许端到端的语音识别,从声音记录开始,并推出转录声音的文本。
在线与离线 ASR
在我们进一步讨论之前,让我们先了解一下在线和离线 ASR 的区别。在线 ASR 发生在说话者说话时,转录过程同时发生,有轻微的滞后。我们的 ASR 模型所需的计算能力和低延迟对于在线 ASR 任务以及转录的准确性是至关重要的,这三个目标是相互冲突的。
另一方面,当我们事先可以获得完整的语音记录,并且我们的模型可以专注于尽可能准确地将语音转录成文本时,就会发生离线 ASR。然后,可以根据您的应用程序的要求提供结果。离线 ASR 可以让我们负担得起更大的模型,这可能需要更多的资源和时间,但不会牺牲准确性。
这两种模型都非常相关,因为它们服务于不同的下游应用,包括但不限于对话代理、专家代理、RPA 机器人、语音分析和摘要等。
虽然 ASR 的两种模式是不同的,但是底层技术在很大程度上是交叉的;因此,我们只有在必要时才会提到这种分歧。
ASR 管道
语音识别的自动化,正如你可以直观地想象到的,将要求我们把问题分解成更简单的子问题,比如语音和语言。
大体上,管道由两个主要部分组成:
- 声学编码器
- 文本解码器
声学编码器输出语音输入的编码,以便解码器可以使用该编码来输出编码的有意义的文本表示。
为了更深入地理解这一点,我们面临以下问题
- 声学建模 -将时域音频信号转换为频域信息,并利用该信息创建能够理解不同种类语音之间差异的模型——不同的语言、不同的说话者、不同的话语、不同种类的噪声以及由此产生的沉默等。
- 语言建模 -获取声学模型提供的特征,并将其转录成有意义且准确的文本,就像人类理解所说的内容时所经历的那样。除了理解音频输入,文本模型还需要理解所发出的话语中的语言的语法和句法、不同单词之间的语义关系等。
- 解码算法 -可以利用不同的策略来获取语言嵌入,从而得出文本输出。这些将取决于我们所使用的语言模型的种类,它们是基于字符的还是基于词典的,如果存在与解码器管道相关联的基于深度学习的语言模型,等等。
- 语音到文本对齐 -语音如果被分成时间段,则不会直接与文本输出对齐,因此我们处理可变向量空间,并且能够将多个语音帧对齐到一个字符/单词对于我们的应用变得非常重要。
source: https://www.mdpi.com/2073-8994/11/8/1018
我们将逐一研究管道的所有这些部分。
声学建模
我们从录音设备中得到的声音通常是一种振幅随时间变化的波形。这些信息起初似乎是随机的,很难理解。自回归模型有时可以帮助揭示这种波形中的模式,但它们不足以用于语音识别。为了更深入地了解波形,我们将它们转换到频域。这是使用傅立叶变换完成的。短时傅立叶变换为我们提供了不同的频率仓,以及每帧声音在每个频率仓中的声音幅度。
MFCCs
人类感知声音的尺度不是线性的。相反,他们在对数尺度上感知它。因此,使用以下公式将线性频率标度转换为 mel 标度
MFCCs 的计算方法是:对音频信号应用预加重滤波器,获取该信号的 STFT,应用基于 mel 尺度的滤波器组,进行 DCT(离散余弦变换),并归一化输出。预加重滤波器是一种使用信号的加权单阶时间差来稳定音频信号的方法。滤波器组是一组三角波形。
音素和字形
音位是包含语义内容的最小声音单位。它不同于字母表,有一个声音元素。声学内容的变化导致音位的变化,可以对音位进行分析,将其转化为可识别的语言。并非所有的声学变化都会引起音位的变化,例如,唱歌不会改变歌词的内容。
将音素分成桶的一个主要方法是理解发声。如果声音来自声带,音素就是有声的。这与无声音素形成对比。例如,单词“vat”或单词“V”是使用声带发出的,而单词“fat”中的单词“F”是在没有任何声带干预的情况下发出的。
元音总是有声的,不同的共振峰(嘴唇,舌头等。)都是怎么用的,怎么用的。辅音有各种各样的类别,有清音和浊音。你可以通过上面的链接了解更多关于音素的知识。
字素被描述为“可能引起意义变化的最小对比语言单位”英语中大约有 40 个有区别的音素,但有 70 个字母或字母组合象征着音素。
例如,单词“ghost”包含五个字母和四个字素(“gh”、“o”、“s”和“t”),代表四个音素。
HMM-GMM
隐马尔可夫模型是顺序建模工具,它做出强有力的假设,即系统的状态仅依赖于其先前的状态,而不依赖于在此之前的状态。
尽管这是一个强有力的并且经常是错误的假设,但是基于 HMM 的模型在语音识别领域取得了很大的成功,其中声音内容是特定于说话者的。
source: https://jonathan-hui.medium.com/speech-recognition-gmm-hmm-8bb5eff8b196
上图以文本格式显示了电话,这与我们在英语中的拼写方式相似。音素和字形这两个词可以互换使用。
使用先前状态的 MFCC 特征来预测电话。每个音素的 HMM 概率被学习为高斯混合模型,其充当 HMM 的发射概率。
高斯混合模型是一种概率模型,它假设所有数据点都是由具有未知参数的高斯分布混合生成的。在我们的例子中,每个音素被表示为它前面的音素的高斯模型的混合,如马尔可夫假设所规定的。高斯混合模型也可以为多于 2 种模态建模。期望最大化算法用于学习高斯混合模型参数。
该算法可使用动态规划在多项式时间内求解,此处详细解释了该方法。
请注意,这里仍然没有考虑词汇建模,语音被转换为其语音转录而不是文本表示。
深层声学模型
时域信息可以使用深度神经网络(如 RNNs 及其变体)来表示,而不是由 hmm 来表示。CNN 也可以代替 rnn 用于光谱信息,以使用 MFCCs 作为输入来模拟时间状态。
当前最先进的模型应用了比普通 CNN 或 RNNs 或 LSTMs 更新颖的方法。这些包括基于注意力的模型,变压器模型,变分自动编码器等。我们将在稍后回顾该领域中一些有影响力的论文时了解这些内容。深度声学模型已经能够帮助声学建模变得更加一般化,跨越说话者、语言、口音、领域等。
语言建模
语言极其复杂,即使没有语音成分,也很难为语言编写规则。同样的词在不同文化的不同语境中有不同的用法。一些拼写相同的单词有不同的意思。有些单词发音相同,但拼写不同。每种语言的语法都在变化,不同语言中的语法规则的通用性很低。
语言建模承担了在给定的上下文中预测下一个单词应该是什么的困难任务。这可以使用统计方法或深度神经网络来完成,这也反映在语言建模技术随时间的演变中。语言模型承担给字母或单词分配概率的任务,分别给出前面的单词或字母。
词性和依存句法分析
词性标注或词性标注正是它听起来的样子。将每个标记或单词标记为特定词类的能力。一些通用词类包括名词、形容词、代词、副词等。你可能会在高中的语言课上认识到这些。
依存句法分析使我们能够通过创建一个树形结构来建立不同词类之间的句法关系。有许多不同的解析器获取带词性标签的语音语料库,并将其转换为依存关系树,这是所有常见的 NLP 工具都提供的功能,如 NLTK 、 SpaCy 等。
你可以在这里了解更多关于依赖上下文的语法和依赖解析。
词典
词典指的是包含关于单个单词或单词串的信息(语义、语法)的 NLP 系统的组件。对于大多数应用程序,词典指的是单词和标点符号的列表以及与每个单词或标点符号相关联的字符或字符串。
词典根据词类和依存句法分析所描述的词类之间的关系来描述自然分类中的单词。
上面的概念,虽然不是独立于语言的,但允许我们通过计算来理解特定句子的内容,并且早期的许多语言建模工作都是由使用这些概念的语言理解所主导的。
统计语言模型
统计语言建模主要是关于估计$ Pr(S) \(的,其中\) S \(是句子的语料库,而计算语言学主要是关于估计\) Pr(H|S) \(的,其中\) H $是语言的隐藏状态(如上面讨论的句法和依存关系树)。
正如这篇文章正确指出的,在试图对语言建模时有两个重要的问题——稀疏性和上下文。
稀疏性指的是每种语言的广泛词汇以及我们有效编码这些词汇的能力。最直观的方法是创建 N 维独热向量,其中 N 是词汇表的大小。但是随着句子长度的增加,这就产生了数据越来越稀疏的矩阵。当我们采用这种方法时,我们会遇到众所周知的“T2 维度诅咒”。
单词的 上下文是为了语言建模的目的,即单词周围的单词。单词前后的单词提供了关于该单词用法的大量信息,因此影响其出现的概率。统计语言建模的目标是预测给定上下文的单词。
深层语言模型
深度语言模型通过将每个单词表示为 N 维嵌入来解决稀疏性和上下文理解。
嵌入是由各种深度学习架构生成的 N 维空间中的可学习表示。考虑到语言的时间特性,rnn 首先被用于此。RNNs 成为许多问题的牺牲品,包括爆炸和消失梯度。接下来出现了一个改进版本,长短期记忆(LSTM)网络,但仍然缺乏捕捉跨一段或多段语言的长期依赖性的能力。
最近,基于注意力的模型和转换器在 NLP 和建模领域取得了越来越多的成功。
解码算法
声学模型可以为我们定义的词汇提供随时间变化的排放概率。模型可以基于字符或字符串。有各种解码策略来利用这些概率来预测特定语音模式用字母/字素/单词来表示什么。
贪婪解码
贪婪解码就是简单地获取具有最高条件概率的令牌。
$ $ y _ { t } = arg max _ { y \ in V } P(y | y _ { 1 }...y_{t-1}) $$
在基于字符的模型中,这通常是一种幼稚的方法,因为基于上下文的概率通常隐藏在概率分布中。贪婪解码器假设具有最高概率的记号是正确的记号,但是在时间\(t\)预测的记号之前的单词的可能性通常也与做出正确的预测非常相关。
波束解码
波束解码扩展了贪婪解码算法,以考虑多个可能的序列,这些序列可以聚集在一起以产生高概率结果,而不是在每个时间帧仅取最高概率的令牌。
光束解码器在这里的 中可以得到最好的直观解释。
波束搜索对贪婪解码算法做了两个重要的改进。
- 波束搜索不是只考虑顶部的令牌,而是考虑顶部的\(N\)个令牌。
- 波束搜索不是孤立地考虑每个时间步,而是考虑前面单词的联合概率,并挑选出\(N\)个最佳序列。
关于如何实现波束搜索算法的简单教程可以在这里找到。
语音到文本对齐
当转换成文本时,语音中的时间帧并不对应一对一的映射。这就要求我们将多个语音帧映射到一个单一的文本令牌。这种不一致使得大多数基于损失的深度学习优化的正常方法不适合语音识别应用。
CTC 损失
CTC 丢失或者说连接主义者的时间分类丢失是无对齐的。它引入了一个新的标记,通常被称为空白标记,用于注释不同标记比对中的中断。如果多个时间步长由相同的字符表示,则合并空白记号之间的记号,如果它们不重复,则保持原样。空白标记确保具有重复标记的单词不会折叠成错误的拼写。
source: https://distill.pub/2017/ctc/
这篇文章很好地解释了 CTC 如何工作,该算法如何使用巧妙的动态编程方法来降低计算成本,以及它如何以可微分的方式解决未对准的问题,以便我们的神经网络可以使用梯度下降从损失函数中学习。
RNN-T 损失
RNN -T 损耗或 RNN-换能器损耗通过使用预测网络和加入网络以及我们从声学模型中获得的嵌入网络来解决对准问题。预测器网络是使用 GRU 实现的自回归网络。接合器是简单的前馈网络,其组合编码器向量\(f_{t}\)和预测器向量\(g_{u}\)并输出所有标签上的 softmax \(h(t,u)\)以及“空”输出\(\phi\)。
source: https://lorenlugosch.github.io/posts/2020/11/transducer/
作为损失函数,RNN-T 损失将对数域中所有可能比对的所有概率相加,并将其用作损失函数。
关于 RNN 传感器网络的一些值得注意的事情是
- 预测器网络只能访问\(y\)并因此只能对文本数据进行训练。
- 该模型可用于流式或在线 ASR。
RNN 传感器损耗的一个很好的解释可以在这里找到。相同的代码教程可以通过点击下面的链接找到。
结论
在本文中,我们研究了端到端 ASR 管道的基本要素、这些管道遇到的主要挑战以及一些潜在的解决方案。我们研究了统计声学建模、统计语言建模、解码/搜索算法以及针对语音输入和文本输出之间的错位问题的解决方案。最后,我们讨论了语音到文本的对齐,其中我们讨论了 CTC 损失和 RNN-T 损失。
在本文中,我们几乎没有触及用于声学建模或语言建模的深度学习算法的表面。一些最先进的算法能够将声学建模和语言建模这两种功能结合到一个网络中。我们将在下一篇文章中看到许多这样有趣的方法,它们处于基于深度学习的 ASR 研究的前沿。
敬请期待!
梯度上的端到端数据科学:Nvidia Merlin
原文:https://blog.paperspace.com/end-to-end-data-science-on-gradient-nvidia-merlin/
Gradient 旨在成为一个端到端的数据科学平台,因此涵盖了数据科学的所有阶段,从原始数据的初始查看,到在生产环境中部署模型并附带应用程序。
因此,展示梯度上端到端数据科学的工作示例是有益的。
Nvidia 的 GPU 加速推荐系统 Merlin 就是这样一个例子。作为 Nvidia 的用例框架之一,它拥有一些包含端到端工作的优秀笔记本电脑,并展示了一系列协同工作的工具,以大规模地与数据进行交互,并提供可操作的输出。
这篇博客中展示的端到端数据科学的主要部分是:
- 大规模 ETL/ELT
- 特征工程
- 专题报道
- 综合数据
- 深度学习推荐器的模型训练
- 保存模型
- 部署
- 模型集合:求解部署预处理
此外,其他详细方法,例如近似最近邻搜索、分类特征的单热点和多热点编码以及频率阈值(少于给定数量的类出现被映射到相同的索引)。
数据准备、模型训练和部署等各个阶段都在 GPU 上完成,因此速度大大加快。
工作笔记本示例中包含的工具有:
- Nvidia Merlin NVTabular 大规模 ETL 工作流,包括大于内存的数据集、功能工程和数据准备
- Nvidia RAPIDS cuDF 用于熟悉的 Pandas API 中的大型数据帧
- 用于并行计算的开源 Python 库
- Apache Parquet 面向列的数据存储格式
generate_data()
Merlin 中的合成数据- 盛宴特色店
- 梅林模型包括深度学习的推荐器模型
- Merlin Systems 的操作员和库有助于将模型与端到端工作流程的其他部分集成
- Nvidia Triton 推理服务器投入生产部署
另外还有 Faiss 快速相似性搜索、Protobuf 文本文件和 GraphViz。
让我们简单看看 Nvidia Merlin,然后是如何在 Gradient 上运行它,以及提供的三个端到端示例。
英伟达梅林
Merlin 是 Nvidia 的端到端推荐系统,专为生产规模的 GPU 加速工作而设计。
因此,它是专门为推荐者设计的,而不是像计算机视觉或 NLP 文本模型那样。然而,推荐器是数据科学中的一个常见用例,需要自己的框架来实现最佳结果,就像深度学习的其他子领域一样。
它解决了尝试大规模端到端数据科学时出现的许多问题,例如:
- 数据准备是在 GPU 上完成的,因此速度大大加快(NVTabular)
- 大型数据可以在熟悉的 API 中处理,而不必在代码中添加并行性(cuDF)
- 使用一种高效的面向列的文件存储格式,比纯文本文件(Parquet)快得多
- 包括一个特征库,使特征工程有组织、简化和可重用(Feast)
- 当需要时,真实的训练数据可以通过合成数据来增加,这是人工智能工作中越来越受欢迎的组成部分
- 分布式模型训练可以更快地训练出更好的模型
- 模型针对推理进行了优化,可以以健壮的方式部署到生产中(Triton 推理服务器)
- 通过将典型推荐器的多个组件部署为一个集成(Triton 集成),解决了在部署中对输入原始数据的预处理
运行它最简单的方法是使用 Nvidia 提供的支持 GPU 的 Docker 容器。通过在 Gradient 上运行,设置和使用它们的许多障碍都被消除了,例如获取 GPU 硬件和设置 Docker。
在梯度上运行
因为 Merlin 是以 Docker 容器和 GitHub 存储库的形式提供的,所以可以使用我们的运行时立即在 Gradient 上运行,这两个运行时结合了这两个组件。
创建笔记本
登录后,创建一个项目,然后创建一个笔记本。
Notebook creation in Gradient. You can see the use of the Merlin GitHub repository, Docker container, and Nvidia NGC Container catalog Docker registry credentials (see below) under Advanced Options.
选择一台运行它的 GPU 机器。我们使用了 48GB 内存的安培 A6000,但其他的,如 A100,可能也能工作。这是一个运行起来很昂贵的包,所以在选择机器类型时要考虑到高昂的 GPU 成本,如果使用功能不太强大的机器,还有可能出现 OOM 错误。
选择 TensorFlow 推荐运行时(编写时为 TensorFlow 2.9.1)。在高级选项下,输入https://github.com/NVIDIA-Merlin/Merlin
作为工作空间,输入nvcr.io/nvidia/merlin/merlin-tensorflow:22.10
作为图像。
您也可以点击以下链接启动笔记本(如果您是 Growth 帐户用户,请参见链接了解详情):
需要额外的步骤:Nvidia API 密钥
对于许多容器和存储库来说,这就是全部!你已经准备好了。
然而,对于这一个,有一个额外的步骤 : Nvidia NGC 目录 Docker 图像要求你首先登录 Nvidia 来下载它们。幸运的是,Paperspace 使这变得很容易,因为它能够登录到经过认证的 Docker 注册中心。
因此,在高级选项下,为注册表用户名输入$oauthtoken
(这个实际的字符串,没有替代),并在注册表密码下,粘贴您的英伟达 NGC 目录 API 密钥。
💡If you don't have an Nvidia NGC Catalog API key, you can create one by going to the NGC Catalog page, signing up, then in your profile dropdown on the top right under Setup there is an option to generate a key.
也可以用通常的方式(包括 API 键)编程创建一个渐变笔记本,使用渐变命令行接口。
有关渐变笔记本及其创建的更多信息,请参见文档。
这些例子
Nvidia Merlin 存储库提供了三个主要的示例:
- 多级推荐器
- MovieLens 数据集
- 使用 Criteo 缩放数据
这里我们不会深入讨论推荐器是如何工作的,但是:
(a)谈论如何解决大规模端到端工作中遇到的常见问题
(b)提供一些注释和指示,以便在坡度上最佳运行示例。事实上,他们很容易就能实现他们的目标,但是这里或那里的一些奇怪的细节值得一提。
💡Note: the examples provided in the Merlin containers have parallel tracks using either TensorFlow 2 or HugeCTR (plus some PyTorch), but they run on different containers. To simplify the presentation, we focus on the TensorFlow 2 route, which provides end-to-end in all cases.
一般设置
一般渐变笔记本设置的一些注意事项:
- 我们在 6000 单 GPU 上运行笔记本电脑。其他 GPU 应该可以工作,尽管一些内存较小的 GPU 可能不会做得那么好。
- 如果遇到内存不足的错误,重置有问题的 Jupyter 笔记本或其他 run 笔记本的内核应该会清除已使用的 GPU 内存,并允许事情运行。您可以通过 GUI 左侧导航栏 metrics 选项卡或终端上的
nvidia-smi
来检查 GPU 内存使用情况。 - 我们建议在单独的渐变笔记本中运行多阶段、移动镜头和缩放标准,这样就可以清楚地看到哪个输出来自哪个示例。所有 3 个都可以使用相同的 Github 存储库和 Docker 容器,如上面的 Create Notebook 中所述。
- 运行笔记本时,您可能会偶尔看到警告,如 NUMA 代码或不赞成使用的项目。这些可能来自梯度,或从原来的例子,并不重要,看到笔记本运行。
示例 1:多阶段
多级推荐器的例子可以在这里找到,由两个 Jupyter 笔记本组成:01-Building-Recommender-Systems-with-Merlin.ipynb
和02-Deploying-multi-stage-RecSys-with-Merlin-Systems.ipynb
。
它演示了如何使用四个阶段来构建推荐系统管道:检索、过滤、评分和排序。第一个笔记本中的图显示了其中的每一项:
Multi-stage recommendation system (from the first notebook in this example)
它训练检索(双塔)模型和评分/排名模型(深度学习推荐模型,又名。DLRM),包括数据准备和一个特性库,更重要的是,展示了如何使用 Triton 推理服务器将多模型设置部署到生产环境中。
在01-Building-Recommender-Systems-with-Merlin.ipynb
中,准备数据集,进行特征工程,训练模型。默认情况下,生成的数据集是合成的,使用梅林的generate_data()
函数,但也可以选择使用来自阿里 CCP 的真实数据(阿里巴巴点击和转化预测)。特性工程使用 NVTabular 完成,特性库使用 Feast。模型训练后,输出到/workspace/data/
。
在02-Deploying-multi-stage-RecSys-with-Merlin-Systems.ipynb
中,部署被设置,包括特征存储、模型图形作为一个整体导出以及 Triton 服务器。然后部署模型,并检索建议。
不幸的是,笔记本并没有提供太多关于建议正确性的见解,或者他们将如何被使用,但是他们看起来似乎是合理的。
运行笔记本
当运行 notebook 1 时,您可以执行 Run All,除了您需要取消注释第一个代码单元格中的%pip install "feast<0.20" faiss-gpu
行。
对于 notebook 2,假设您运行了 notebook 1,那么您可以运行除最后一个单元格之外的所有内容,最后一个单元格将会失败。为了运行该单元,首先在终端中使用tritonserver --model-repository=/Merlin/examples/Building-and-deploying-multi-stage-RecSys/poc_ensemble/ --backend-config=tensorflow,version=2
启动 Triton 推理服务器。如果它说内存不足,重启 notebook 1 内核清除一些。然后就可以运行笔记本 2 的最后一个单元格了。
💡Note: At the present time the deployment in this example will in fact hang on Gradient before becoming ready, and so the last cell in notebook 2 will still fail. This is because the Docker container invocation (docker run
) used in the original Nvidia GitHub repository uses the argument memlock=-1
, and in Gradient the container is invoked behind the scenes without this. A fix is planned in the first quarter of 2023. In the meantime, to see deployments that do run, go to the MovieLens and Scaling Data examples below, which do not have this issue.
示例 2:电影镜头
MovieLens 推荐器的例子可以在这里找到,由 4 个步骤的笔记本组成。我们将重点关注 TensorFlow (TF)路径,因为它涵盖了培训和部署,并允许我们继续使用与上面相同的 Docker 映像。因此,我们运行这 4 台笔记本电脑:
01-Download-Convert.ipynb
02-ETL-with-NVTabular.ipynb
03-Training-with-TF.ipynb
04-Triton-Inference-with-TF.ipynb
在01-Download-Convert.ipynb
中,MovieLens-25M 数据集被下载并从其提供的 CSV 格式转换成更高效的拼花。
在02-ETL-with-NVTabular.ipynb
中,NVTabular 用于进行数据准备。它有一些有用的功能,如多热点功能,这意味着像电影类型这样的信息,其类别数量因每个数据行而异,可以得到充分利用。这是对用户、项目和评级信息的常规使用的补充。NVTabular 还能够处理大于 GPU 内存大小的数据。
然后训练模型,这里使用具有嵌入、ReLU 等的深度学习层。NVTabular 的数据加载器通过将数据直接读入 GPU 内存来加速训练,以及其他优化,消除了使用 GPU 时可能成为瓶颈的问题。
最后,在04-Triton-Inference-with-TF.ipynb
中,使用 Triton 推理服务器部署模型。与上面第一个多阶段的例子一样,我们没有深入了解这些建议的效用,但是很明显模型输出正在产生。
这里的示例再次展示了 NVTabular 在进行实际数据准备方面的能力,并使用 GPU 加速了它:
NVTabular functionality (from the second notebook in this example)
我们还看到使用 Triton 推理服务器来解决在生产部署中预处理输入的不可见数据的问题:
Solving production deployment preprocessing by using a model ensemble (from the fourth notebook in this example)
运行笔记本
前 3 台笔记本可以使用“全部运行”运行。
在运行 notebook 4 之前,启动 Triton 推理服务器,使用在 Gradient 上创建的目录:tritonserver --model-repository=/root/nvt-examples/models/ --backend-config=tensorflow,version=2 --model-control-mode=explicit
。
那么 notebook 4 也可以运行 All。
示例 3:缩放标准数据
💡Note: This example takes longer to run than the others, 20 minutes+, because notebook 1 has steps to download, extract, and then write the now large-scale data.
在第三个也是最后一个例子中,我们看到了如何处理大规模数据。完整的公共 Criteo 数据集为 1tb,由 24 天内的 40 亿行点击日志组成。
我们关注的是 2 天的子集,尽管我们确实发现使用全部 24 天都可以下载和准备。培训和部署工作了 7 天,但 24 天需要一个更大的 GPU 设置。
对于整整 24 天,信息在线引用“1.3 兆兆字节”,但实际上这是压缩和未压缩文件的组合大小,它们不必都保存在磁盘上。1tb 是转换为 Parquet 之前平面文件的未压缩大小,大约为 250GB。
与前两个示例一样,我们关注 TensorFlow 2 路径,并运行:
01-Download-Convert.ipynb
02-ETL-with-NVTabular.ipynb
03-Training-with-Merlin-Models-TensorFlow.ipynb
04-Triton-Inference-with-Merlin-Models-TensorFlow.ipynb
在01-Download-Convert.ipynb
中,数据集被下载并转换成拼花格式。默认情况下,它不会下载所有 1tb 的数据,而是下载 24 天中的 2 天,总共压缩了大约 30GB。可以选择下载 2 到 24 天之间的任意天数,因此您可以根据需要进行扩展。这款笔记本还设置并使用了 Dask 集群。
在02-ETL-with-NVTabular.ipynb
中,使用 NVTabular 准备数据,设置包括扩展到多 GPU 和多节点的能力。这使用 Dask 集群和 Nvidia RAPIDS dask_cudf
库来处理 Dask 数据帧。数据准备包括设定频率阈值,以便将不经常出现的类别归为一类。生成的 NVTabular 工作流在单个 GPU 上运行大约需要一分钟,包含 2 天的数据,输出到/raid/data/criteo/test_dask/output/
。
Dask dataframes simplify working with large data in a distributed fashion (from the second notebook in this example)
模特在03-Training-with-Merlin-Models-TensorFlow.ipynb
接受训练。这一次,又是 DLRM(深度学习排名模型),训练+评估用了 2 天的数据值几分钟。模型被保存到/raid/data/criteo/test_dask/output/dlrm/
。
在04-Triton-Inference-with-Merlin-Models-TensorFlow.ipynb
中部署模型时,Triton 设置包括在笔记本 2 中设置的特征工程 NVTabular 工作流。
与其他两个示例一样,模型输出建议的正确性和实用性并没有真正得到解决,但我们再次看到它们正在产生,因此我们已经运行了我们的“端到端”数据科学。
运行笔记本
所有 4 个都可以使用“全部运行”按原样运行。
💡Note: For this example in particular, you should restart the notebook kernels after running each one both to avoid running out of GPU memory that has been used for the large data, and to avoid attempting to start a Dask cluster twice on the same port.
对于 notebook 1,在它已经运行并且原始数据被转换为 Parquet 之后,您可以通过执行rm -rf /raid/data/criteo/crit_orig/
来删除原始数据以节省磁盘空间。转换后的数据在/raid/data/criteo/converted/criteo/
中。注意,如果你关闭并重启渐变笔记本,数据将被删除,因为/raid
不是一个持久目录。要保留它,将其移动到持久化的地方,比如/storage
。
对于使用 Dask 的 Notebook 2,Gradient 支持多 GPU,尽管您需要将机器类型更改为多 GPU 实例,如 A6000x2。但是,这不是必需的,它将在单个 A6000 上运行良好。不幸的是,它提到的将在127.0.0.1:8787
中显示的仪表板不能与当前的渐变笔记本架构一起工作,但它不是其他功能所必需的。您仍然可以在单元输出中看到 CUDA 集群设置。
对于笔记本 3 来说,它是按原样运行的。
对于 notebook 4,在使用tritonserver --model-repository=/raid/data/criteo/test_dask/output/ensemble --backend-config=tensorflow,version=2
启动 Triton 推理服务器之前,一定要先运行 All。倒数第二个单元将失败,但在服务器启动后,您可以运行最后两个单元。这些型号的服务器需要一两分钟才能启动。
结论和后续步骤
我们已经看到真正的端到端数据科学是如何在 Paperspace Gradient 上以适合生产的规模和稳健性完成的。Nvidia Merlin 推荐器很好地展示了这一点。
我们看到了三个例子:
- 多级推荐器
- MovieLens 数据集
- 使用 Criteo 缩放数据
以及端到端数据科学中常用的大量不同工具协同工作。
对于一些后续步骤,您可以:
- 亲自试用这些笔记本
- 了解更多关于 Nvidia Merlin ( 网站、 GitHub 库、文档、 Docker 图片
- 在其文档中了解更多关于 Paperspace Gradient 的信息
- 了解更多有关其他 Nvidia 端到端数据科学功能的信息,例如 RAPIDS ,以及用于企业工作的 LaunchPad
祝你好运!
采用渐变工作流的端到端数据科学:StyleGAN2
原文:https://blog.paperspace.com/end-to-end-data-science-with-gradient-workflows-stylegan2/
生产中的数据科学需要许多部分协同工作,以实现真正的端到端功能。这包括数据准备、模型训练和部署,既要有一个严谨的框架来构建和版本化一个项目及其组件,又要有在大规模数据集上完成这些工作的能力。
在这个项目中,我们展示了:
- 使用 42gb 图像文件和梯度管理数据集进行数据准备
- 使用梯度公共数据集作为数据源
- 在图像样本上使用 StyleGAN2 深度学习的模型训练
- 使用适当的度量标准进行模型评估
- 生成新图像的模型推理
- 版本化渐变对象,包括数据集和模型
- 资源控制,为不同的作业指定不同大小的机器
- 通过更改链接的 GitHub 存储库触发工作流重新运行
全部在渐变工作流程提供的结构内。
附带的教程和 GitHub 库允许你全面运行端到端项目,或者只运行第二部分来查看模型训练。
StyleGAN2
我们的 StyleGAN2 演示基于流行的 Nvidia StyleGAN2 库。我们使用它的图像生成功能,利用来自 LSUN 在线数据库的训练数据生成猫的图片。
StyleGAN2 是使用生成对抗网络 (GANs)生成图像的 StyleGAN 方法的实现。这些方法通过让两个网络竞争来实现,一个用来区分不同类别的图像,另一个试图欺骗第一个错误分类。这样,两个网络的性能被迭代地提高。
这样做的结果是,经过训练的网络能够生成以前没有见过的新图像,但是与训练集相似。因此,网上出现了大量不存在的人的照片、假视频等等。在我们的例子中,这意味着我们训练过的网络可以生成猫的新图片。
数据准备
典型的真实机器学习项目,尤其是深度学习,可能涉及到在训练或生产过程中使用大量图像,从而使用大量数据。因此,我们证明了梯度工作流能够以稳健的方式处理如此大的数据集。
猫图像存储在 42GB 大小的 LMDB 格式数据库中。这是通过 curl 从原始在线位置下载的:
curl -C - -o /outputs/catImageDatabase/cat.zip 'http://dl.yf.io/lsun/objects/cat.zip'
在我们的例子中,连接是可靠的,所以这里显示的恢复失败下载的选项不需要放在脚本的循环中,但是由于任意命令可以在工作流 YAML 或它调用的脚本中执行,所以可以添加这个选项。
然后,我们使用 Nvidia 存储库的dataset_tool.py
命令提取图像,该命令在下载完成后由工作流 YAML 在工作流内的单独作业中调用。
必须将图像从提供的 LMDB 数据库格式提取为多种分辨率的 TensorFlow TFRecords 格式,以便将它们传递给模型。
当数据准备工作流显示在渐变 GUI 中时,它看起来像这样
StyleGAN2 Gradient Workflow for data preparation. The cat image database is 42GB in size.
提取之后,图像就可以在模型中使用了。
模型训练、推理和评估
我们在项目中的第二个工作流显示了 Nvidia 为此数据集提供的训练模型,以及我们自己对同一模型的训练运行。
由于两个工作流运行是独立的,这意味着第二个工作流既可以在第一个工作流刚刚生成的即时输出上运行,也可以在先前制作的同一准备数据的其他副本上运行。我们将这样一个先前制作的副本存储在我们的梯度公共数据集管理的存储中,这样,第二个工作流可以在没有第一个工作流的情况下运行,第一个工作流涉及 42GB 数据集下载,每次都要再次运行。
使用适当的度量来评估所提供的模型,并用于对新图像进行推断。换句话说,我们用它来生成新的猫图像。然后,我们对我们的模型做同样的事情,并首先对它进行训练。
GAN 操作机制的一部分是使用真实图像和由网络生成的假图像,该网络试图欺骗另一个网络进行错误分类。
因此,脚本的输出包括真实图像的样本,换句话说,就是猫的大型蒙太奇:
Sample of real images used in model training
由于我们在一个工作流中进行训练、评估和推理,这导致了一个更复杂的工作流图。使用 GraphViz,我们可以根据功能给组件着色,如下所示
StyleGAN2 Gradient Workflow for model training, evaluation, and inference, in GraphViz
当在工作流 GUI 中显示时,它看起来像这样
Same, in the Gradient Workflows GUI
在第一张图中,我们可以看到版本化的梯度管理数据集(灰色框)从 StyleGAN2 repo(绿色框)馈送到预训练模型的实例,以及我们训练的模型的实例(黄色框)。回购本身是蓝色的。
在第二张图中,我们看到了相同的结构,工作流中的作业名称,使用的渐变动作(git-checkout@v1,等等。),以及大概的运行时间。GUI 工作流图保证是正确的,因为它是从 YAML 自动生成的。
结果
当来自 StyleGAN2 repo 的预训练模型运行时,它产生了猫的新图像,如预期的那样。这些看起来像这样:
Generated images of cats from pre-trained model
它们看起来非常逼真!然而,它并不完美,从第三张图片中猫的三条前腿可以看出。
当我们从预先训练的模型切换到我们自己训练的网络时,我们看到运行的结果并不太好,因为我们将原始的大量图像从 160 万图像二次采样到 1000 个图像,并且只在短时间内训练网络。这是为了让运行时便于用户运行演示,同时也能看到渐变平台的强大功能。所以这些图像本质上是低分辨率噪声:
Generated image from our model (the sample is very small, and the runtime short)
用户可以尝试更大的训练集并运行更长时间的训练(见下文),因为这样做的所有数据和信息都存在。
触发工作流重新运行
在大型项目或生产环境中,项目组件(代码、数据集、模型、部署等)之间的一致性。)重要。因此,当一个项目链接到一个 GitHub 存储库时,我们可能希望那里有一个变更,比如模型设置的变更,来触发模型重新运行。这确保了模型与设置一致。另一个例子是确保部署的模型与训练的模型相同。
在具有模型实验、完成的模型、阶段部署、实际部署、各种监视和更新方法、更改数据等等的真实项目中,维护项目一致性可能会变得非常复杂。因此,通过在其 YAML 代码中指定工作流,以及对所有内容进行版本控制,工作流允许您确切地了解正在发生的事情。
在这里的项目中,表示工作流将被触发以再次运行的代码是
on:
github:
branches:
only: main
这意味着对存储库主分支的任何更改,都要再次运行工作流。YAML 和我们的梯度行动将允许这是更细粒度的,如果你只是想特定的变化触发重新运行。
被触发重新运行的 YAML 文件被放在存储库中的.gradient/workflows
目录中,类似于 GitHub 自己的 Actions 功能。
对于我们的 StyleGAN2 工作流,我们可以很容易地看到一个重新运行触发器,例如,通过改变用于生成图像的随机种子
python run_generator.py generate-images \
--network=/inputs/pretrainedNetwork/stylegan2-cat-config-f.pkl \
--seeds=6600-6605 \
--truncation-psi=0.5 \
--result-dir=/outputs/generatedCatsPretrained
当更新的 YAML 被提交到储存库时,这将重新运行模型训练并将输出新图像到输出梯度管理数据集的新版本。重新运行显示在工作流 GUI 中,包括触发它的事件。
一个更大的改变可以通过,而不仅仅是改变种子,在我们的第一个工作流程中,从上面提到的 1000 个小样本中改变提取图像的数量,到更大的数量。然后重新运行第二个工作流,以查看生成的训练模型的性能是否有所提高。
工作流触发器、链接到项目的存储库、工作流 YAML 中的任意代码、版本化数据集和模型,以及未来的集成模型部署的组合,形成了一个强大的系统,可将您的数据科学从实验带入生产。
结论
我们使用 StyleGAN2 深度学习来展示:
- 使用 42gb 图像文件和梯度管理数据集进行数据准备
- 使用梯度公共数据集作为数据源
- 对图像样本进行深度学习模型训练
- 使用适当的度量标准进行模型评估
- 生成新图像的模型推理
- 版本化渐变对象,包括数据集和模型
- 资源控制,为不同的作业指定不同大小的机器
- 通过更改链接的 GitHub 存储库触发工作流重新运行
后续步骤
这个博客条目对应的教程和 GitHub 库都是可用的。本教程包含有关如何自己运行工作流的更多详细信息。
这个项目的教程是更高级的教程之一,需要更长的时间来运行,所以如果你想要更简单或更快的东西,请查看我们更新的 GUI onboarding,文档中的工作流入门,或者另一个教程,如 NLP 文本生成(与博客 + repo )。
另一个更长的端到端例子,也包括渐变笔记本,是我们的推荐者项目,它也有博客和回购。
如果你已经完成了教程,并想用渐变写一些新的东西,一种可能性是使用 StyleGAN2 的其他数据集之一来做这个相同的项目,来自同一个 LSUN 位置,或者,比方说, Flickr Faces 数据集 (FFHQ)。
还有一个更新版本的 GAN 图像生成,StyleGAN-ADA ( TensorFlow 或 PyTorch 版本),它可能以类似的方式运行,以及即将推出的无别名 GAN 。
具有梯度的端到端推荐系统-第 1 部分:提出一个商业问题
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-1-business-problem/
系列介绍
对于许多组织来说,数据科学家和工程师面临的一个主要问题是如何最好地从实验阶段进入生产阶段。渐变笔记本和渐变工作流在这两方面都提供了功能。
这个博客系列演示了渐变笔记本和工作流作为端到端项目的一部分的使用。在这个过程中,我们将详细解释每个步骤,并且我们将使数据科学家、ML 工程师和其他技术读者能够访问这些材料。
在这六个部分的第一部分中,我们概述了任何企业数据科学分析的最重要部分——提出业务问题。让我们开始吧。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建张量流模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
第 1 部分简介
推荐系统的输出现在是网络世界的一个普遍特征。我们很多人都很熟悉亚马逊这样的网站如何鼓励你购买额外的商品来补充你购物车中的商品,或者 YouTube 如何建议你在去其他地方之前再看一个视频。
我们也很熟悉它们不太好用的时候。例如,如果你从亚马逊购买了一套艾伦扳手,亚马逊可能会在一段时间内继续向你展示艾伦扳手,尽管你刚刚购买了一套。或者,如果你在公路旅行中预订了一家酒店,你可能仍然会收到几周后你离开的那个城镇的报价。像这样的例子可能有一些统计学上的有效性,因为你比一个随机的用户对这些项目更感兴趣,但是它们并没有捕捉到人类会有的经验常识。
因此,尽管推荐系统已经变得越来越好,但仍然有改进的空间,以捕捉用户想要的全部复杂性。在少数顶级科技公司之外尤其如此,这些公司的团队往往缺少数据科学家和工程师。
对于许多企业来说,向技术堆栈中添加一个性能良好的推荐模型有可能大幅提高收入。
在这个由 6 部分组成的博客系列中,我们旨在展示深度学习如何改善推荐器的结果,以及 Paperspace Gradient 如何使构建这样的系统变得更容易。
特别是,我们将关注构建这样一个推荐器的整个生命周期——从编写笔记本和训练模型的实验阶段,到部署模型和实现商业价值的生产阶段。
系列集锦
- 演示一个现实世界风格的机器学习的例子
- 将端到端数据流与梯度笔记本电脑和工作流程相结合
- 使用基于 Gradient 与 Git 集成的现代数据科学方法
- 使用 TensorFlow 2 和 TensorFlow 推荐器(TFRS)来训练包括深度学习的推荐器模型
- 使用反映真实项目变量和现实的训练数据
- 使用完整的 TensorFlow 子类化 API 构建自定义模型
- 显示改善结果的工作超参数调整
- 使用梯度部署及其 TensorFlow 服务集成的部署模型
- 包括一个独立的工作 Jupyter 笔记本和 Git 存储库
- 让它与广泛的受众相关,包括 ML 工程师、数据科学家以及介于两者之间的人
理解业务问题
正如人们常说但并不总是实践的那样,数据科学应该从要解决的业务问题开始。
在流程的每一步都有太多可能的方法(数据、特征工程、算法、性能指标等)。)在头脑中没有特定标准的情况下探索。
“构建一个更好的分类器”(或者本例中的推荐器)并不是一个商业问题,这也是事实。这是解决方案的一部分。业务问题需要以描述业务直接价值的术语给出。
下面是根据《用数据思考》一书所做的演讲中的一个例子:
不是业务问题范围:我们将使用 R 来创建逻辑回归,以预测谁将停止使用该产品。
业务问题范围:上下文=公司有订阅模式。首席执行官想知道我们能否锁定那些可能放弃交易的用户。需求=想要了解谁提前下班,以便我们可以干预。Vision =预测模型,使用行为数据来预测谁将退出,足够快速地识别他们以发挥作用。Outcome =软件团队将在每天运行的批处理过程中实现该模型,自动发出要约。计算精度和召回使用举行的用户,发送每周电子邮件。
范围比这更长并不罕见——关键是要用可测量的术语来理解结果。
对于这个项目,我们的业务问题如下:
证明 Paperspace 渐变笔记本和工作流可用于解决真实的机器学习问题。通过展示从原始数据到生产部署的端到端解决方案来做到这一点。进一步表明所演示的内容可以构建成一个完整的企业级系统。在这种情况下,该模型是一个推荐系统,其结果通过利用经过调整的深度学习组件来改进。
现在让我们继续谈论推荐系统!我们将特别讨论经典方法所体现的思想,以及当加入深度学习时它们是如何变化的。然后,我们将看到梯度如何有助于将这些方法带入生活。
经典推荐系统
给任何不是深度学习的机器学习(ML)方法贴上“经典”的标签,都可能会产生误导。
事实上,梯度增强决策树等“经典”方法可以像深度学习一样解决任意复杂的非线性问题——对于许多商业问题来说,这些方法更合适,因为它们保留了人类可读的数据特征,减少了计算密集型的训练时间。
因此,这里的“经典”指的是利用(或许同样被低估的)经典统计方法的方法,而不是经过训练的 ML 模型。这些形成了 ML 模型可以改进的基线。
以下是推荐器模型试图实现的一个常见任务:
向用户呈现相似用户已经喜欢的建议的新项目
一个常见的设置是使用一个检索模型和一个排名模型。在这种设置中,检索模型从完整的库存中选择一组候选项目进行推荐,然后排名模型细化选择并在较小的数据集上对推荐进行排序。
建议中的经典统计部分是使用协同过滤,其中我们有:
- 用户的偏好,或者是明确的,如评级,或者是隐含的,如观看或购买
- 衡量每个用户之间的距离,以找到与他们最相似的用户
- 向用户推荐与他们相似的用户喜欢的项目
这些方法有效,但准确性有限,因为它们没有使用数据集中可用的所有信息,例如,距离只能用矩阵或低维张量表示,如用户-用户(相似评级)或物品-物品(购买的相似物品)。
即使将人口统计等用户特征添加到计算中(上下文感知),那些不直接转化为距离上的明显差异的特征(如特征交互)也不一定会被捕获。
深度学习的添加
在复杂的非线性映射代表问题的最佳解决方案的领域中,ML 能够改进经典统计,同样地,ML 也能够改进推荐器。
向推荐者添加 ML 可以:
- 更多要捕捉的特征
- 使用更多类型的特征,例如多列用户或项目信息,而不仅仅是距离
- 这些特征的非线性组合被模型发现
- 为每个用户提供更精细、更个性化的推荐
- 缓解如何处理新用户或项目的“冷启动”问题
在这个系列中,我们重点关注向推荐器排名模型添加一些深度学习层,该模型改进了对用户喜欢推荐项目程度的预测。
当前的艺术状态由像脸书的深度学习推荐模型( DLRM )这样的设置来代表。这是一种协同过滤和深度学习的混合方法,它学习较低阶的功能交互,但将较高阶的功能交互限制在那些增加性能而不会增加太多计算资源成本的功能交互。
虽然我们没有构建一个如此复杂的系统,但我们的目标是展示 Gradient 为构建这样的生产系统所提供的功能基础。
梯度实施
在本系列的下一部分中,我们将开始使用 TensorFlow 2 和 TensorFlow 推荐器库在 Paperspace Gradient 中实现该模型。然后,我们将使用 TensorFlow 服务的 Gradient 集成来部署模型。
我们将展示渐变笔记本的使用,包括如何进行数据准备和模型训练,然后演示如何创建一个生产设置,在渐变工作流中执行模型训练和部署。
Screenshot showing part of the Gradient Notebook for this blog series
然后
在系列的第 2 部分-准备数据 中,我们将展示如何使用 TensorFlow 2 和 Gradient 笔记本从原始数据集到合适的模型训练集。
具有梯度的端到端推荐系统-第 2 部分:准备数据
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-2-data-preparation/
第 2 部分简介
在六部分系列的第二部分中,我们将展示如何使用 TensorFlow 2 和梯度笔记本从原始数据集到合适的模型训练集。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建张量流模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库。
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
数据准备的作用
众所周知,如果数据不足,任何机器学习模型都无法解决问题。这意味着必须正确地准备数据,以适合传递给 ML 模型。
太多的在线内容忽略了数据准备的现实。企业 ML 的一个推荐范例是“做一个试点,而不是一个概念验证”,这意味着建立一些简单的端到端的东西,然后回去完善细节。
因为梯度使得部署模型变得容易,它鼓励我们从一开始就记住模型部署和生产。在数据准备过程中,这意味着发现问题,而这些问题可能要等到以后才会被发现——事实上,我们将在下面看到一个这样的例子。
渐变鼓励实体版本化的工作方式。使用诸如 Git repos、YAML 文件等工具,我们可以抵消数据科学家投身于难以重现的特定操作顺序的自然倾向。
版本化的问题不仅仅是模型和代码,还有数据本身,也很重要。除了小数据,这种版本控制在 Git 中不起作用。在这一系列中,数据仍然相对较少,因此充分考虑这一限制被认为是未来的工作。
MovieLens 数据集
因此,虽然我们不会以一个 100%典型的业务场景开始这个系列,例如包含数百万个多种格式的非结构化原始文件的 petascale 数据湖,这些文件缺乏模式(甚至没有联系人来解释它们),但我们确实使用了在 ML 研究中广泛使用的数据。
数据集特别有用,因为它包含了现实世界企业数据集的许多典型特征。
MovieLens dataset used in this blog series
MovieLens 数据集包含有关电影、用户以及用户对电影的评级的信息。对于新用户,我们希望能够推荐他们可能观看和喜欢的新电影。
我们将首先使用检索模型选择候选项,然后使用排名模型预测用户的评分。
预测收视率最高的电影将成为我们的推荐。
初始数据的前几行如下:
{'bucketized_user_age': 45.0,
'movie_genres': array([7]),
'movie_id': b'357',
'movie_title': b"One Flew Over the Cuckoo's Nest (1975)",
'raw_user_age': 46.0,
'timestamp': 879024327,
'user_gender': True,
'user_id': b'138',
'user_occupation_label': 4,
'user_occupation_text': b'doctor',
'user_rating': 4.0,
'user_zip_code': b'53211'}
{'bucketized_user_age': 25.0,
'movie_genres': array([ 4, 14]),
'movie_id': b'709',
'movie_title': b'Strictly Ballroom (1992)',
'raw_user_age': 32.0,
'timestamp': 875654590,
'user_gender': True,
'user_id': b'92',
'user_occupation_label': 5,
'user_occupation_text': b'entertainment',
'user_rating': 2.0,
'user_zip_code': b'80525'}
我们看到大多数列都是不言自明的,尽管有些不明显。数据被分成电影信息、用户信息和观看电影的结果,例如何时观看、用户评级等。
我们还发现了在准备过程中需要解决的各种数据问题:
- 每一行都是一个字典,所以我们需要提取条目或者正确地访问它们,以便能够做准备。
- 目标列与功能列没有区别。因此,我们必须使用上下文和我们的领域知识来查看目标列是
user_rating
,并且其他列都不是欺骗变量。这里的欺骗变量将是直到用户观看电影之后才可用的信息,因此在推荐用户还没有观看的电影时不能使用该信息。(1) - 一些数据列被格式化为字节码(例如
b'357'
),而不是常规的 UTF-8 unicode。如果模型被部署为 REST API,并且数据被转换为 JSON,那么这将会失败。JSON 要求数据是 UTF,最好是 UTF-8。所以我们需要对此做出解释。 - 给定的时间戳不适合 ML。一列唯一或几乎唯一的连续值不会添加模型可以使用的信息。然而,如果我们能够将这些价值具体化为一些重复出现的东西,如一天中的时间、一周中的日子、一年中的季节或假日,它们可能会变得更有价值。
- 由于存在时间戳,并且用户查看习惯会随着时间的推移而改变,因此需要考虑处理时间序列的问题,并且最好将数据拆分为时间上不重叠的定型集、验证集和测试集。不这样做违反了 ML 的假设,即训练集中的行是不相关的,并可能导致虚假的高模型性能或过度拟合。
显然,还有许多其他问题可以问和回答:
- 是否有缺失或其他错误/超出范围的值?
- 流派是什么意思?
- id 是唯一的吗?它们是否与电影名称一一对应?
- 用户性别
True
是什么? - 用户 id 是唯一的吗?
- 有哪些职业标签?
- 为什么评级浮动不是整数?
- 所有行都有相同的列数吗?
在一个真实的业务场景中,我们最好与数据发起者讨论这些列的含义,并检查每个人都在同一个页面上。我们还可能使用各种图表、探索性数据分析(EDA)工具,并应用我们所掌握的任何其他领域知识。
在这里的例子中,MovieLens 数据已经是众所周知的了。虽然人们不应该假设这使得它对任务来说是明智的,甚至是一致的,但我们不需要花时间做全面项目中会做的大量数据 EDA。
(1) 关于“信息可用”,这里实际上有一个微妙之处,因为当然,除了 user_rating
之外,用户观看电影时的时间戳列事先是不可用的。然而,与评级不同,当进行推荐时“现在”的时间是可用的,因此知道用户更可能在周末晚上观看某些类型的电影可以被使用而不会被欺骗。
准备数据
TensorFlow 有各种帮助数据准备的模块。对于 MovieLens 来说,数据还不够大,不需要 Spark 等其他工具,但当然情况并非总是如此。
在全面的生产推荐系统中,通常需要更多的工具来处理大于内存的数据。随着时间的推移,大型用户群可能会有数十亿行可用。
数据从官方 TensorFlow 数据集存储库的movielens/
目录加载到 TensorFlow 2 中。Gradient 可以连接到其他数据源,比如亚马逊 S3。
import tensorflow_datasets as tfds
...
ratings_raw = tfds.load('movielens/100k-ratings', split='train')
它们作为 TensorFlow Prefetch
数据集类型返回。然后,我们可以使用 Python lambda 函数和 TensorFlow 的.map
来选择将用于构建模型的列。
对于这个系列来说,只有movie_title
、timestamp
、user_id
和user_rating
。
ratings = ratings_raw.map(lambda x: {
'movie_title': x['movie_title'],
'timestamp': x['timestamp'],
'user_id': x['user_id'],
'user_rating': x['user_rating']
})
TensorFlow in-part 与 NumPy 重叠,因此我们可以使用.concatenate
、.min
、.max
、.batch
和.as_numpy_iterator
例程来提取时间戳,并创建时间上不重叠的训练、验证和测试集。简单又好看!😀
timestamps = np.concatenate(list(ratings.map(lambda x: x['timestamp']).batch(100)))
max_time = timestamps.max()
...
sixtieth_percentile = min_time + 0.6*(max_time - min_time)
...
train = ratings.filter(lambda x: x['timestamp'] <= sixtieth_percentile)
validation = ratings.filter(lambda x: x['timestamp'] > sixtieth_percentile and x['timestamp'] <= eightieth_percentile)
test = ratings.filter(lambda x: x['timestamp'] > eightieth_percentile)
然后,我们在每个集合中混洗数据,因为 ML 模型假设数据是 IID 的(行是独立的且同分布的)。我们需要打乱数据,因为我们的数据似乎最初是按时间排序的。
train = train.shuffle(ntimes_tr)
...
最后,我们获得推荐器模型所需的唯一电影标题和用户 id 的列表。
movie_titles = ratings.batch(1_000_000).map(lambda x: x['movie_title'])
...
unique_movie_titles = np.unique(np.concatenate(list(movie_titles)))
...
笔记本中提到了一些进一步的调整,以使一切都协调工作。这些包括让.len()
处理应用 lambda 得到的FilterDataset
类型。这些在笔记本中应该是不言自明的,因为它们基本上相当于弄清楚如何使用 TensorFlow 来完成所需的工作。
还要注意,我们假设使用时间百分比得到的行数大致对应于数据百分比的行数。这是发生在数据准备中的典型假设,不一定通过简单地阅读代码就能得出。(在这种情况下,数字是一致的,如笔记本所示。)
其余的输入数据处理在模型中完成,如第 3 部分所示。
然后
在系列的第三部分——构建 TensorFlow 模型 中,我们将使用 TensorFlow 推荐器库构建一个基本的推荐器模型,并在上述准备好的数据上对其进行训练。
具有梯度的端到端推荐系统——第 3 部分:建立张量流模型
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-3-building-tensorflow-model/
第 3 部分介绍
在这个六部分系列的第三部分中,我们将使用 TensorFlow 推荐器库来构建一个基本的推荐器模型,并根据我们在第二部分中准备的数据对其进行训练。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建 TensorFlow 模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库。
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
推荐模型
现在我们有了合适的数据(来自第 2 部分)要传递给模型,我们可以继续使用 TensorFlow 构建我们的基本推荐器。
对于用户来说,TensorFlow 模型的最简单形式——尤其是现在的 TensorFlow 2 比版本 1 更易于使用——是使用 Keras 的顺序层模型公式。这允许指定模型层(通常是神经网络)及其超参数。
一些更复杂的模型,比如那些层的非顺序排列的模型,可以用函数式 API 来指定。
推荐系统不遵循标准的监督设置,因为它们输出可能是几个推荐的有序列表,而不仅仅是单个预测或无序的分类概率。因此,它们需要使用子类化 API 的最通用的 TensorFlow 设置。模型被写成类,用户对它们的设置有最大的控制权。
真实的业务问题通常需要 ML 模型中的一些定制组件,这些组件不能用更简单的接口来表示。这意味着必须使用完整的类公式来获得必要的通用性和对过程的控制。
TensorFlow Recommenders library used in this project
然而,我们并不完全从底层组件定义我们自己的模型,而是利用 TensorFlow 库 TensorFlow 推荐器( TFRS )。这为推荐系统的公共组件实现了类,比如嵌入,然后可以使用子类化 API 组装这些嵌入。
如果您熟悉 TensorFlow 推荐器教程,您可能认识本系列中的一些示例,但是我们强调的是构建一个具有数据准备和部署功能的端到端示例,而不是向推荐器本身添加功能。
模型组件
我们将建立模型的排名组件,其中我们将预测用户对新电影的评级,然后输出建议。
为了使模型能够进行预测,我们需要输入movie_title
和user_id
列,并在user_rating
列上训练模型,以便它可以预测新的潜在用户+电影组合的评级。
为了从标题和用户的大列表到模型可以使用的低维表示,我们将这些特征转换成嵌入。
嵌入的计算方法是将原始类别映射到整数(称为词汇表),然后将它们转换到一个维数低得多的连续向量空间,比如 32。
这些然后被传递到深度学习层,深度学习层预测用户评级并输出推荐。
如果在模型中计算嵌入,我们可以更简单地部署模型。这是因为在新的原始数据传入和模型对其运行预测之间,在模型外部进行的数据操作较少。
这减少了错误的范围,是 TensorFlow、TFRS 和其他现代 ML 库的优势。
模型实现
基本排名模型通过创建计算嵌入的 TFRS 模型类来实例化。然后,它包含预测用户评级的深度学习层,计算每次训练迭代损失的代码,以及允许模型在训练和保存后进行部署的调用方法。
该课程值得在此全面展示:
class MovielensModelBasicRanking(tfrs.models.Model):
def __init__(self) -> None:
super().__init__()
embedding_dimension = 32
# The embeddings use Keras's preprocessing layers
# Embeddings for movies
self.movie_model: tf.keras.layers.Layer = tf.keras.Sequential([
tf.keras.layers.experimental.preprocessing.StringLookup(
vocabulary=unique_movie_titles, mask_token=None),
tf.keras.layers.Embedding(len(unique_movie_titles) + 1, embedding_dimension)
])
# Embeddings for users
self.user_model: tf.keras.layers.Layer = tf.keras.Sequential([
tf.keras.layers.experimental.preprocessing.StringLookup(
vocabulary=unique_user_ids, mask_token=None),
tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
])
# Predicted ratings
# This is where deep learning is being used in the recommender system
# The predictions are output by the final layer, hence its size of 1
self.rating_model = tf.keras.Sequential([
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1)
])
# Ranking is written as a TFRS task
self.task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
loss = tf.keras.losses.MeanSquaredError(),
metrics=[tf.keras.metrics.RootMeanSquaredError()]
)
# The call method allows the model to be run, and saved
# The embeddings are passed into the model
# The embeddings and predicted rating are returned
def call(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
user_embeddings = self.user_model(features['user_id'])
movie_embeddings = self.movie_model(features['movie_title'])
return (
user_embeddings,
movie_embeddings,
self.rating_model(
tf.concat([user_embeddings, movie_embeddings], axis=1)
),
)
# This is the TFRS built-in method that computes the model loss function during training
def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
ratings = features.pop('user_rating')
user_embeddings, movie_embeddings, rating_predictions = self(features)
rating_loss = self.task(
labels=ratings,
predictions=rating_predictions,
)
return rating_loss
为了在 TFRS 教程之后向它传递数据,还有一些 TensorFlow 咒语。
这些方法是将训练、验证和测试数据集放入缓存形式以提高性能,并分批发送数据,而不是一次发送所有数据或一次发送一行数据。:
cached_train = train.shuffle(ntimes_tr).batch(8192).cache()
...
对于 2 个批次,传递给模型的数据的形式(在编写分析时,应该总是进行完整性检查)是:
{'movie_title': array([b'Spanking the Monkey (1994)', b'Bob Roberts (1992)',
b'Like Water For Chocolate (Como agua para chocolate) (1992)', ...,
b"Stephen King's The Langoliers (1995)",
b'Alice in Wonderland (1951)', b'Phantom, The (1996)'],
dtype=object),
'timestamp': array([879618019, 883602865, 884209535, ..., 885548475, 879518233,
877751143]),
'user_id': array([b'535', b'6', b'198', ..., b'405', b'295', b'207'], dtype=object),
'user_rating': array([3., 3., 3., ..., 1., 4., 2.], dtype=float32)}
{'movie_title': array([b'Top Gun (1986)', b'Grace of My Heart (1996)',
b'Ghost and the Darkness, The (1996)', ..., b'Crimson Tide (1995)',
b'To Kill a Mockingbird (1962)', b'Harold and Maude (1971)'],
dtype=object),
'timestamp': array([882397829, 881959103, 884748708, ..., 880610814, 877554174,
880844350]),
'user_id': array([b'457', b'498', b'320', ..., b'233', b'453', b'916'], dtype=object),
'user_rating': array([4., 3., 4., ..., 3., 3., 4.], dtype=float32)}
省略号代表每个批次中未显示的其余字段。
通过与原始原始输入数据中的一行进行比较,我们可以看到已经选择了正确的列movie_title
和user_id
:
{'bucketized_user_age': 45.0,
'movie_genres': array([7]),
'movie_id': b'357',
'movie_title': b"One Flew Over the Cuckoo's Nest (1975)",
'raw_user_age': 46.0,
'timestamp': 879024327,
'user_gender': True,
'user_id': b'138',
'user_occupation_label': 4,
'user_occupation_text': b'doctor',
'user_rating': 4.0,
'user_zip_code': b'53211'}
id 是类别而不是数字。目标评级是的数值,每一批的数据都在一个数组中。
培训和结果
模型训练遵循编译模型和运行拟合的标准张量流方法:
model_br = MovielensModelBasicRanking()
model_br.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))
history_br = model_br.fit(cached_train, epochs=3, validation_data=cached_validation)
这里我们使用了 Adagrad 优化器。我们已经指定了 0.1 的学习率和少量的历元。
在第 4 部分中,我们将调整这些参数以提高性能,并展示除了使用笔记本之外,如何使用渐变工作流来训练模型。
用于衡量模型性能的指标是预测用户评级与其真实评级之间的均方根误差(RMSE)。
该模型的性能可以从历史中看出:
rmse_br = history_br.history['root_mean_squared_error'][-1]
print(f'Root mean squared error in user rating from training: {rmse_br:.2f}')
这给出了:
Root mean squared error in user rating from training: 1.11
换句话说,在 0-5 的范围内预测某人的电影评级的 RMSE 是 1.11。我们可以类似地从验证集中输出 RMSE,如果模型没有过度拟合数据,这应该是类似的。
我们可以按如下方式查看该模型的摘要,其中我们看到了深度学习层和排名层:
model_br.summary()
Model: "movielens_model_basic_ranking"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
sequential (Sequential) (None, 32) 53280
_________________________________________________________________
sequential_1 (Sequential) (None, 32) 30208
_________________________________________________________________
sequential_2 (Sequential) (None, 1) 33153
_________________________________________________________________
ranking (Ranking) multiple 2
=================================================================
Total params: 116,643
Trainable params: 116,641
Non-trainable params: 2
_________________________________________________________________
最后,我们可以在看不见的测试数据上评估模型,因为我们总是应该引用生产中现实的预期模型性能:
eval_br = model_br.evaluate(cached_test, return_dict=True)
...
Root mean squared error in user rating from evaluation: 1.12
性能类似于训练,所以我们的模型看起来工作正常。
然后
在系列的第 4 部分- 调整模型以获得最佳性能 中,我们将通过调整一些模型参数来改善上述结果,并展示如何在梯度工作流中完成培训,以充分利用自动化和可重复性。
具有梯度的端到端推荐系统——第 4 部分:调整模型以获得最佳性能
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-4-tuning-model-best-performance/
第 4 部分介绍
在这个六部分系列的第四部分中,我们将通过调整一些超参数来改进第三部分中模型的结果,并演示如何在梯度工作流中完成训练过程。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建 TensorFlow 模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库。
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
渐变调谐
在第 3 部分中,我们展示了如何构建和训练一个基本的推荐模型。现在我们将调整一些超参数来提高它的性能。
虽然调谐可以完全在笔记本上完成,但 Gradient 也允许将超参数调谐作为 Gradient 工作流程的一部分。
梯度工作流的优势在于,我们可以通过 YAML 集成 Git、Docker 容器和运行参数的完整定义,以实现版本化和可重复的实验。
一旦我们有了这种性质的实验,我们就可以使用 Gradient 的现代 MLOps 堆栈(包括 CI/CD、微服务、Kubernetes 等等)将模型部署到生产中。
当我们谈论 MLOps 时,我们指的是什么
CI/CD 指的是持续集成——其中代码和其他对象的版本被正确地合并——以及持续部署——其中软件被快速部署并以自动化的方式定期更新。
虽然 CI/CD 在软件开发中已经变得很普遍,但是这个概念需要适应 ML 用例,因为 ML 模型处理变化的数据,因此不能按照相同的参数固定。
Kubernetes 已经成为机器学习团队用来分配计算资源的事实上的容器编排系统,但通常被认为是非工程数据科学家难以配置。
梯度的一个特别优势是,适用于单节点情况的方法可以直接扩展到多节点情况。用户只需在单节点情况下添加适当的额外设置,以指定多节点要求,梯度将处理分布。我们将在第 6 部分的结尾更详细地讨论这些可能性。
调谐的实现
在本教程中,我们将使用 TensorFlow 进行一些基本的调优来演示功能。我们将在对数标度上改变学习率,然后放大到对数标度的最佳值附近的线性标度。我们还将为更多的纪元训练模型,并添加 L2 正则化。
当对完整的模型类使用 TensorFlow 子类化 API 时,需要指定的部分内容是如何以及将哪些超参数传递给模型。因为我们的调优相对简单,所以我们将正则项添加到模型代码中,并将时期数和学习速率传递给。编译和。安装台阶。
模型调整循环如下所示:
learning_rates_logarithmic = [0.001, 0.01, 0.1, 1]
epochs = 30
histories_tr = {}
...
for val in learning_rates_logarithmic:
print('Learning rate = {}'.format(val))
model_tr = MovielensModelTunedRanking()
model_tr.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=val))
lr = str(val)
histories_tr[lr] = model_tr.fit(cached_train, epochs=epochs, validation_data=cached_validation)
为了进行更广泛的调优,可以编写更复杂的循环。也可以使用额外的库,比如 HParams,或者使用 smart-search/AutoML 来搜索比网格搜索更大的空间。
出于本系列的目的,我们的调优很简单。我们可以使用 Matplotlib 绘制其值的单个内联图:
Model tuning for learning rates 0.001 to 1 for the training set (solid lines) and validation set (dotted lines)
对于更复杂的超参数集或其他可视化,Gradient 允许通过 TensorFlow 回调保存较长调优运行的中间阶段。
我们发现 0.1 的学习率是最好的,放大来看,当使用线性网格时,它仍然是最好的。除了学习率,我们还运行了更多时期的模型,结果是预测的均方误差从 1.11 提高到 1.06。
这不是一个巨大的进步,但意义重大。它实现了我们的目的,即展示模型调整,从而改善结果。还有许多其他可以优化的超参数(作者尝试了一些),但是像改变优化器或层大小和数量这样的简单变化不会产生很大的影响。
从这里的结果改进的最好方法是在一个完整的推荐系统中加入更复杂的架构组件,如 TFRS 系列教程中所示。
获胜的策略是整合交叉特征等概念,并添加我们提到但没有实现的时间序列季节性。
所有这些可能性都只是模型的更长版本,使用我们在此展示的完全通用的 TensorFlow 子类 API 接口——很明显,如果用户需要,它们可以在渐变生态系统中实现和部署。
既然我们已经有了一个经过训练和调整的模型,可以很好地满足我们的目的,下一步就是将它部署到生产中。
在工作流中训练最终模型
在上面的例子中,我们从笔记本中进行了调优,结果和版本控制有些特别。
梯度工作流提供了一种更严格的方法,其中数据、模型以及输入和输出的版本对于给定的运行都是固定的。
我们没有在这里重复整个调优过程,我们只是展示了训练最终模型的工作流程。笔记本代码的相关部分在 Python .py
脚本中重复,工作流在 YAML 文件中指定。
这里的工作流是从笔记本中调用的,使用的是 Gradient SDK,但也可以从命令行调用,而不需要使用笔记本。
许多用户对 YAML 不太熟悉,但 YAML 语法(或类似的东西)是必要的,它将工作流指定到所需的精度水平,使其成为产品级的。
我们在渐变文档中提供了如何使用它(和工作流)的各种示例。特别是,有一个页面使用了 YAML 的数据科学,它解决了几个可能会出现在从事数据科学的非 YAML 专家面前的问题。
为了训练我们的最终模型,.py
脚本重复笔记本的数据准备、模型类定义和模型训练部分。用于训练模型的 YAML 文件如下所示:
...
defaults:
resources:
instance-type: P4000
env:
PAPERSPACE_API_KEY: secret:api_key_recommender
jobs:
...
CloneRecRepo:
outputs:
repoRec:
type: volume
uses: git-checkout@v1
with:
url: https://github.com/gradient-ai/Deep-Learning-Recommender-TF
...
RecommenderTrain:
needs:
- CloneRecRepo
inputs:
repoRec: CloneRecRepo.outputs.repoRec
env:
HP_FINAL_EPOCHS: '50'
HP_FINAL_LR: '0.1'
outputs:
trainedRecommender:
type: dataset
with:
ref: recommender
uses: script@v1
with:
script: |-
cp -R /inputs/repoRec /Deep-Learning-Recommender-TF
cd /Deep-Learning-Recommender-TF
python workflow_train_model.py
image: tensorflow/tensorflow:2.4.1-jupyter
我们可以看到它运行两个任务,CloneRecRepo
和RecommenderTrain
。
CloneRecRepo
获取该项目的 GitHub 资源库的指定版本,这里是当前主分支的默认版本,并将其作为挂载卷输出。
在这种情况下,回购是公共的,因此不需要提供凭证(作为机密存储在 Gradient 中)来克隆它。uses:
行指定了一个渐变动作,这是一个特殊的过程,比如克隆一个 repo 或者在容器中运行一个命令。渐变动作可以并行运行。
RecommenderTrain
然后使用挂载卷中的 repo 来训练模型,并将其输出到trainedRecommender
下的一组版本化文件中。
我们指定需要使用needs
成功完成CloneRecRepo
作业。
渐变动作script@v1
允许运行一组命令,其中主要的是我们的训练脚本workflow_train_model.py
。该脚本在 Gradient 的 Kubernetes 管理的基础设施上的指定 Docker 容器tensorflow/tensorflow:2.4.1-jupyter
上运行。
模型超参数(或通常的任何参数)通过env:
字段作为环境变量传递给脚本。
(对于RecommenderTrain
作业,输出的语法有点混乱,因为包含模型的文件集在 Gradient 中被称为“数据集”这个名字将来可能会改变。还需要将回购从/inputs
目录复制到工作目录,因为/inputs
是只读的。这也是可以改进的,请注意,可能包含大得多的文件的数据集不必复制。)
运行培训时,我们可以在 Gradient 的 GUI 中看到工作流的详细信息:
Recommender model training Workflow in Gradient
工作流作业显示为有向无环图(DAG ),可以查看 YAML、输出和版本。我们的输出是来自RecommenderTrain
工作的训练模型。
输出模型与我们上面使用笔记本训练的模型相同。
有关 YAML 和工作流程的详细信息,请参见梯度文档。在 GitHub 项目库中,.py
和 YAML 文件的名称是workflow_train_model.py
和workflow-train-model.yaml
。
笔记本与工作流
从笔记本上运行数据科学项目与使用更基于 IDE 的方法和 Python 脚本之间的理想相互作用是数据科学和 MLOps 社区中尚未解决的一个普遍问题。
在不同的阶段使用这两个接口有很好的理由,但是从一个到另一个的转换,通常是从项目的探索阶段到生产阶段,可能涉及到代码的复制或重写。
由于 Paperspace 的产品包含必要的计算硬件,以及笔记本和 IDE 方法,Gradient 现在和将来都有能力支持这两种方法。
然后
在系列的第 5 部分- 将模型部署到生产 中,我们将展示如何使用梯度工作流部署模型,以及它与 TensorFlow 服务的集成。
具有梯度的端到端推荐系统——第 5 部分:将模型部署到产品中
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-5-deploying-model-production/
第 5 部分介绍
在这个六部分系列的第五部分中,我们将展示如何使用梯度工作流部署模型及其与 TensorFlow 服务的集成。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建张量流模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库。
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
部署模型
既然我们已经有了训练好的模型,我们就可以将它部署到生产中了。
生产不同于实验和培训,因为重点从解决问题(例如,我们可以在笔记本上完成)转移到为可靠部署的模型设计解决方案。
当到了部署模型的时候,我们不再仅仅关心分析的正确性,而是关心服务于解决方案的架构的健壮性。
生产的一个中间步骤可能在实验和训练阶段进行,就是将看不见的测试数据发送给模型。通常我们会这样做,如果我们仍然有一些基础事实来衡量模型性能。
然而,这或多或少类似于实际生产,这取决于在培训阶段有多少数据是以与培训和验证数据相同的方式准备的。在 TensorFlow 中,这可能在带有测试集的模型上运行.predict
。
按照我们的标准,全面生产意味着向模型设置发送原始的、不可见的数据,并让模型返回正确的、可操作的输出,如预测。
该设置需要对输入中的意外变化具有鲁棒性。它需要能够检测应用程序问题(如延迟)和数据科学问题(如模型漂移)。并且它需要能够在需要时处理模型的再训练。
可能没有与模型输出相比较的基本事实标签,所以必须利用其他方法来确保模型的输出是有意义的。
机器学习模型是高度非线性的,因此输入的小变化会导致输出的大变化。有可能在几分之一秒内将输出从有价值的商业信息变成无意义的信息。负责部署的团队很可能不负责发送给他们的数据,因此在部署过程中注意不要破坏底层模型至关重要。
Gradient 的主要优势在于,它通过提供硬件、配置的 Kubernetes 集群、容器,以及对于 TensorFlow,与 TF 服务的集成,使生产(或类似生产)部署步骤比其他方式更容易。
可以通过 GUI、SDK 或命令行创建和管理部署。如果用户不希望使用云资源,并且需要内部设置,那么公司也可以提供帮助。
在一些企业中,尤其是较大的企业,最终的生产部署可能要经过漫长的审批、流程、检查、安全等。训练和实验阶段不需要的,并且可能是使用不同软件栈的情况。
在这种情况下,进行实验和培训的团队可能不同于进行部署的团队。
因为 Gradient 包含笔记本电脑和工作流,而且还在一个工具中包含 MLOps 和部署功能,所以可以按照良好的软件工程实践来训练和部署模型。
特别是,梯度促进迭代,这是数据科学的一个关键组成部分,不仅在数据准备、特征工程和模型训练之间,而且在模型训练和模型部署之间。
梯度部署
Gradient model deployments, showing versioning
Gradient 支持在其注册表中部署模型,这意味着它们要么是由用户上传的,要么是由 Gradient 工作流创建的,正如我们在第 4 部分中所做的那样。
部署模型时,用户选择以下选项:
- 模型
- 容器–渐变提供的容器或自定义容器
- 机器–公共集群或私有集群
- 加速器——CPU 或 GPU 机器
- 实例——单台机器或分布式机器
- 在容器中运行的命令
- 如果需要,各种高级选项
这还是有相当多的选项可以选择的,但应该不会让人应接不暇。Gradient 试图为用户不希望更改或不知道如何更改的设置提供智能默认值,这样做可以使用户快速启动并运行。
需要时,用户可以对所需的设置进行精细控制。
在本系列中,推荐器部署相对简单。我们使用定制模型和子类化 API,但是 SavedModel 与更简单的 API 格式相同,所以部署是相同的。
我们没有使用大型数据集,所以我们不需要分布式计算,一台 CPU 机器就足以满足我们展示一个返回正确预测的模型的既定目的。
部署可以通过 GUI、笔记本中的 SDK 或.py
、命令行来完成,或者,正如我们将在这里看到的,通过工作流来完成。
部署实施
可以通过命令行(或者将来的 GUI)调用工作流,也可以使用笔记本中的 SDK。这里我们使用后一种方法。
至于工作流中的模型训练,我们需要导入 Gradient SDK 并将我们的 API 密钥传递给它,以便安全地访问系统:
from gradient import sdk_client
workflows_client = sdk_client.WorkflowsClient(api_key)
然后,我们指向要运行的特定工作流并调用它:
spec_path = './workflow-train-model.yaml'
yaml_spec = open(spec_path, 'r')
spec = yaml.safe_load(yaml_spec)
workflow_param = {
'workflow_id' : workflow_id_train,
'spec': spec,
'inputs': None
}
workflow_run = workflows_client.run_workflow(**workflow_param) # "**" is used because parameters is a dictionary
随着工作流产品的成熟,将来这个过程会更加友好。
工作流 YAML 看起来类似于第 4 部分中的模型训练。这里我们调用一个作业来获取模型,一个作业来创建部署,一个作业来启动部署。
Recommender model deployment Workflow in Gradient
一旦部署开始,模型就在端点上,就好像它是使用渐变部署中的任何其他方法开始的一样。
因此,我们可以向它发送一些数据,并查看模型的响应。如果我们给它发送一些用户和电影,我们应该看到它预测用户会给他们什么评级!
向模型发送数据
在训练阶段,数据作为 TensorFlow 缓存数据集传递。对于模型部署,它必须作为 JavaScript 对象符号或 JSON 传递,这是 YAML 的一个子集。
在全面的生产环境中,每秒可能会有数百万个点流向模型。
我们不打算在这里重复,而是发送几行数据来查看模型响应。更大规模的行可以用同样的方式发送,也许发送到分布式部署(由 Gradient 支持)而不是单个端点。
给定每行字典形式的测试数据:
test_rows_1 = {'movie_title': 'Devils Advocate, The (1997)',
'timestamp': 892870992,
'user_id': '587'}
test_rows_2 = {'movie_title': 'Donnie Brasco (1997)',
'timestamp': 891499278,
'user_id': '782'}
test_rows_3 = {'movie_title': 'Craft, The (1996)',
'timestamp': 891700850,
'user_id': '280'}
...
让我们先转换成 JSON 如下:
data = json.dumps({'signature_name': 'serving_default', 'instances': [test_rows_1, test_rows_2, test_rows_3, ... ]})
然后发送到模型的 API 端点:
endpoint = deployment[0].endpoint
headers = {'content-type': 'application/json'}
json_response = requests.post(endpoint, data=data, headers=headers)
返回的预测通过以下方式可见:
predictions = json.loads(json_response.text)['predictions']
print(predictions[0]["output_3"])
...
如果这个给定的用户要观看这个给定的电影,则预测评级在 0-5 的范围内给出:
[3.42430091]
[3.71842861]
[2.88263416]
...
因此用户 587 更可能喜欢《魔鬼代言人》,而用户 280 更可能喜欢《手艺》,等等。
我们现在已经部署了一个模型,该模型将为用户和电影的新组合返回评级预测!
这些预测可以用来推荐用户接下来应该看什么电影。虽然我们可能会添加组件来以更大的规模传输数据流(我们并没有试图展示这一点),但这种架构可以放心地用于生产。
进一步的步骤
与实验阶段一样,我们可以从这个设置中给我们的合理的缺省值中受益。我们还可以利用这样一个事实,即通过 GitHub 很容易建立集群、容器等等,并进行适当的版本控制。
Gradient 还可以轻松地将上述设置扩展到分布式培训和部署,包括 Paperspace 平台上已有的 GPU。
在这之后的另一个明显的步骤是做模型监控。由于模型作为容器化的微服务位于 API 端点上,因此可以使用任何可以看到它的工具。第 6 部分将讨论这一扩展和许多其他扩展。
然后
在本系列的最后部分-讨论、结论和后续步骤 中,我们将讨论本系列中所展示的要点,得出一些关于我们的推荐模型的结论,特别是,详细说明本项目未来可以做出的一些改进。
具有梯度的端到端推荐系统——第六部分:结论和下一步
原文:https://blog.paperspace.com/end-to-end-recommender-system-part-6-conclusion-next-steps/
第 6 部分介绍
在这个六部分系列的最后一部分,我们回顾了系列的要点,并指出下一步的工作,既针对这项工作的梯度可以做的其他事情,也针对希望了解更多的读者。
系列零件
第 1 部分:提出业务问题
第 2 部分:准备数据
第 3 部分:构建张量流模型
第 4 部分:调整模型以获得最佳性能
第 5 部分:将模型部署到生产中
第 6 部分:总结、结论和后续步骤
伴随材料
- 本博客系列附带资料的主要位置是位于https://github.com/gradient-ai/Deep-Learning-Recommender-TF的 GitHub 资源库。
- 其中包含项目的笔记本
deep_learning_recommender_tf.ipynb
,可以在渐变笔记本或 JupyterLab 界面中运行,以及渐变工作流的 3 个文件:workflow_train_model.py
、workflow-train-model.yaml
和workflow-deploy-model.yaml
。 - 回购的目的是能够被使用和遵循,而不必参考博客系列,反之亦然,但它们相互补充。
注意
公共集群上的 Gradient 产品和工作流中的模型部署支持目前正处于待定状态,预计将于 2021 年第四季度推出。因此,笔记本deep_learning_recommender_tf.ipynb
中关于模型部署的第 5 部分已显示,但尚未运行。
讨论
正如我们在第 1 部分中看到的,我们打算展示这个系列的各种亮点。概括一下这些:
- 演示一个现实世界风格的机器学习的例子
- 将端到端数据流与梯度笔记本电脑和工作流程相结合
- 使用基于 Gradient 与 Git 集成的现代数据科学方法
- 使用 TensorFlow 2 和 TensorFlow 推荐器(TFRS)来训练包括深度学习的推荐器模型
- 使用反映真实项目变量和现实的训练数据
- 使用完整的 TensorFlow 子类化 API 构建自定义模型
- 显示改善结果的工作超参数调整
- 使用梯度部署及其 TensorFlow 服务集成的部署模型
- 包括一个独立的工作 Jupyter 笔记本和 Git 存储库
- 让它与广泛的受众相关,包括 ML 工程师、数据科学家以及介于两者之间的人
我们现在可以添加一些讨论和进一步的评论。
很多网上资料显示 ML 却忽略工程,或者显示工程却忽略 ML。
换句话说,一个复杂的模型可能被训练,但我们不知道如何部署它,或者一个新的工程设置可能被显示,但只是在玩具模型或数据上。
在这两种情况下,像数据准备和模型调整这样的基本要素也经常被忽略。这使得想要做真实事情的用户缺少工作示例。
因为 Gradient 横跨数据科学实验和生产(笔记本和工作流),并且旨在用于真实端到端问题的数据科学,所以我们希望展示这两个领域,而不是跳过它们,同时避免系列变得太长。
企业中的数据科学仍然是一个不断发展的领域。
最佳实践在不断变化。数据科学家有时比软件工程师具有更差的软件工程技能,并且软件工程师可能正在生产包含他们不完全理解的分析的模型。在笔记本电脑上工作的数据科学家和试图将其转化为生产部署的工程师之间的差异就是例证。
事实是,数据科学从根本上来说是迭代的,Gradient 等工具可以帮助数据科学家和 ML 工程师在共享环境中迭代,这是实现共享目标的重要条件。
TensorFlow 2 (TF2)比 TensorFlow 1 好用得多。许多在线材料,如教程、博客、堆栈溢出,仍然是版本 1,或者没有区分。梯度材料的大部分也是如此,因为它比 TF2 存在的时间更长。所以我们认为在这里开始展示 TF2 的实质性例子以及它如何应用于梯度是很重要的。
TensorFlow 推荐器(TFRS)是对 TensorFlow 的扩展,它添加了一些类,使构建一流的推荐器变得更加容易。在写这篇文章的时候,它还是 0.4 版本。但它看起来令人印象深刻,非常适合我们在这里使用它的任务。
子类化 API 很好地展示了出来,因为现实世界的业务问题通常有一些必需的定制组件,这些组件是更简单的高级顺序和功能 ML 接口(如 AutoML 或无代码工具)无法表达的。因此,通过在这里显示子类化 API,它演示了梯度设置如何可以用于解决实际问题的完全通用的分析。
部署模型并不容易。
这方面的一个例子是,在撰写本文时,8 个 TFRS 教程并没有显示一个工作的生产部署。
这是有原因的。他们模型的教学设置意味着他们被安排来清楚地展示他们是如何工作的,而不是被部署。它们确实以某种形式显示了返回的预测,但清楚地显示生产部署将是一个理想的补充。我们在本系列中展示了这样的部署,这对于我们来说更容易,因为 Gradient 已经有了必要的基础设施、容器和计算硬件来完成它。所以我们所要做的就是适当地重写模型类。
附带的笔记本大致跟随博客,但并不完全对应其所有部分。该笔记本被设计为独立的,所以你不需要在它和这个博客之间来回翻转来使用它。希望这种设置对那些也看笔记本的读者来说效果不错。
结论
我们的业务问题被表述为:
证明 Paperspace 渐变笔记本和工作流可用于解决真实的机器学习问题。通过展示从原始数据到生产部署的端到端解决方案来做到这一点。进一步表明所演示的内容可以构建成一个完整的企业级系统。在这种情况下,该模型是一个推荐系统,其结果通过利用经过调整的深度学习组件来改进。
我们已经表明,添加了调整的深度学习的推荐器将预测和真实用户评级之间的均方根误差从 1.11 提高到 1.06。进一步改进的主要方法是添加推荐系统架构的其他元素,例如交叉特征。
Gradient 将数据科学家的用户友好型和分析友好型工作方式与工程师的坚实 MLOps 基础相结合。我们展示了笔记本电脑和工作流的使用如何有助于有效的端到端数据科学项目。
感谢阅读!
后续步骤
在这里,我们讨论了如果工作要扩展,项目的一些后续步骤。它们大致按照在数据流中遇到的顺序排列。
作为读者,接下来的步骤是在项目 GitHub 资源库中试用笔记本,或者点击下面的链接。
数据科学和推荐者
这些后续步骤是由一些一般的数据科学考虑推动的,并且更具体地针对推荐者和这个项目。
商业故事
我们从谈论业务问题开始,但最终没有扩展到成功的用户评级预测之外。当然,这里我们的实际目的是“证明纸空间梯度可以用于解决真正的机器学习问题”,但它可以显示,例如,数据科学指标如何转换为业务指标,并进而显示这些指标是否满足量化目标并增加价值。
探索性数据分析
第 2 部分解决了一些与数据相关的问题,比如特性和目标没有被区分,但是也提到了在一个完整的项目中要做的各种进一步的探索。下一步将是绘制诸如训练集中用户评级的分布图。我们还可以发现其他潜在的错误值、意外的分布等等。然后,可以将它们与测试集的输出进行比较,并形成监控模型漂移的基础之一。
数据清理
数据中的错误值或缺失值可以采取多种形式,如Inf
、NaN
、0
、9999.9
、超出范围等。,并且通常只能通过使用领域知识和与提供数据的人的通信来完全定位。在电影数据中,电影和 id 不匹配、重复、错别字、分级错误等。,也可能存在。对于这种数据,一些算法比其他算法更健壮,但是通常对于 ML,更好的数据准备将给出更好的结果。
大数据
这个术语现在已经有点过时了,但是作者最喜欢的定义是“数据的大小打破了你最喜欢的分析工具。”推荐系统可能会访问包含数百万用户和内容项的数据,从而产生比电影镜头大得多的数据集。展示如何在梯度生态系统中处理此类数据,用于培训和部署(流),将是有价值的。
特征工程
利用推荐器的深度学习组件,可以使用比例如矩阵分解中更丰富的特征。这里要添加的一个明显的例子是将时间戳特征化为一天中的时间、一周中的日期等。,但也有更复杂的方法,如 TFRS 教程中显示的特征交叉。
超参数调谐
笔记本显示了在几个学习率上的基本超参数网格搜索。这足以说明如何通过梯度笔记本和工作流程来处理它们。其他常见参数,如层大小、层数、优化器、激活函数、正则化和网络架构,可以得到更充分的探讨。
将所有这些写成循环会变得很笨拙,所以通过环境变量传递参数,或者使用 Keras tuner 或 TensorBoard HParams 的某种组合会给出更好的搜索。
类似地,某种形式的智能参数搜索或 AutoML 可以给出比定义自己的超参数网格更好的结果。
添加检索模型
我们的项目显示了排名模型,因此可以添加相应的检索模型。包含两者的组合模型也是可能的。
更复杂的推荐器
TFRS 教程显示了各种比检索和排序更复杂的模型,包括组合的检索和排序模型,以及实现上述特征交叉的深度交叉网络(DCN)。DCNs 倾向于使用更少的参数给出同样好的建议。由于我们已经在使用和部署模型的通用子类化 API 形式,所有这些都可以添加到我们的项目中。
冷启动问题
对于推荐者来说,冷启动问题在这里表现为向还没有看过任何电影并且因此没有给出评级的新用户推荐什么。通过引入新用户,这可以在部署期间得到明确解决。
多样化
仅推荐与用户已经观看过的非常相似的电影可能是过于限制性的,因为他们可能不想观看更多相同的电影。推荐太不一样的电影也没有用,因为他们不会想看那些。推荐的内容和用户已经观看的内容之间的差异或变化量是可以调整的。与许多产品调整一样,指导这一点的一个好方法是具体说明什么业务指标是最大化的,并通过 A/B 测试确定什么工作得好。
大规模部署,高性能
这里,我们展示了几行示例数据,这些数据是从一个已经存在的字典格式传递到部署的模型的。更好的方法是采用一种可能在生产系统中传递的格式,例如由公司的上游堆栈处理的表单,该堆栈处理传入的用户活动,但从 ML 的角度来看仍然是原始的。
因为推荐很可能需要实时提供,所以数据流比批处理要好。TFRS 展示了一个性能增强的例子,其中 ScaNN 近似最近邻库用于加速邻居计算,从而加速系统的检索模型部分。
转换字节编码的数据
我们避开了将字节编码的电影数据转换成 JSON 的问题,只使用样本行发送到已经作为字典给出的模型。如果进入生产部署的新电影数据是字节编码的,并且需要转换,这当然必须解决。
词汇外(OOV)课程
模型部署需要对训练集中不存在的数据具有鲁棒性,例如错误的格式、数值数据的越界值或分类数据的新的未知类。特别是对于像电影这样的东西,新的看不见的类,比如新的电影,可能最终会被提供给已部署的模型。除了拒绝不符合所有标准的行之外,TensorFlow 还能够通过将这些类分配给一个或多个 OOV 类占位符来处理这些类,这可以显示出来。
输出
这里我们看到输出看起来没问题。正如所料,它们是 0 到 5 之间的数字。然而,可能的情况是,通过简单地将大多数预测指定为大约 3.5 的模型已经实现了最佳均方误差。因此,探索预测并将其与训练集进行比较将是非常重要的。
再现性
由于 Gradient 的设置包括 Git 存储库、模型版本、容器和 YAML 工作流规范,推荐器模型训练和给定数据的结果应该是可再现的。
在实践中,TensorFlow 和神经网络有大量的数据选择、参数和随机种子,它们都各不相同,此外,在分布式系统中,不能保证文件行的顺序。因此,精确的再现性可能是不可能的,但仍然可以表明结果在统计上是可再现的——例如,如果误差线是使用一些合理的方法从评级中得出的,那么同一版本数据流的未来实例在这些误差范围内应该是一致的。
解释和说明
众所周知,各种行业都有法规要求来说明“为什么”一个模型做出某种预测,并且一些算法比其他算法更容易解释。深度学习是更困难的方法之一,但模型不可知的方法,如 SHAP,可以显示出来。此外,它们必须适合推荐者,而不是常规的监督学习。
公平/偏见
随着模型影响到我们日常生活的更多方面,它们变得越来越重要。虽然电影推荐可能不是这方面最重要的例子,但推荐者仍有可能被改变以促进某些内容或抑制其他内容。例如,显示推荐的电影范围公平地反映了存在的内容,并且在推荐中不存在依赖于例如用户人口统计的歧视性特征的偏见,这将是有用的。
迁移学习
机器学习可能是计算密集型的,特别是深度学习可能需要大量的计算时间和训练数据才能给出最佳结果。虽然 Gradient 旨在使您可以轻松地将 GPU 和分布式计算添加到您的设置中,但尽可能减少计算负担是有意义的。
现在越来越普遍的一种方法是迁移学习,在这种方法中,基于基本信息(如图像的常见成分)训练的网络可以用作训练更具体内容的起点,而不是从头开始训练网络。迁移学习常用于计算机视觉和自然语言处理。
模型监控
监控分为应用程序状态——例如模型是否正在运行、正常运行时间、吞吐量延迟等。、以及数据科学状态,例如输出的敏感性、概念漂移、数据漂移或模型漂移。
由于用于比较模型输出的基本事实标签通常不可用,或者可能稍后才可用,因此需要基于输入和输出数据来监控性能。因此,应用程序监控可以通过查询 Gradient 对 Prometheus 数据库的后端使用情况来显示。
各种数据科学指标,如训练集中的评级分布和输出数据中的评级分布之间的“距离”,可以通过与现有监控工具的集成来导出。监控的关键点是模型在一个 API 端点上,所以它正在做的事情可以被任何可以看到它的工具访问。
模型应用程序
虽然本博客系列的读者和 Gradient 的用户很可能是技术人员,但该模型的结果及其商业价值可以通过一个可以看到其端点的应用程序(如 Streamlit)向非技术人员开放。
链接和延伸阅读
图纸空间
主站点
AI wiki
博客
商业资源
社区
联系销售
核心,我们的云基础设施
脸书
GitHub
帮助中心
学习
梯度
主站点
API 引用
高级技术组
文档
机器学习展示
公共数据集
推荐者和 TensorFlow
Paperspace 是 TensorFlow 的服务合作伙伴,也与 fast.ai、Nvidia 和其他公司合作。本系列中的代码部分基于 TensorFlow 推荐器教程。
tensor flow
tensor flow Python API
tensor flow 推荐器
Paperspace 公开发布&面向团队的 Paperspace!
我们很高兴最终宣布 Paperspace 正式上市。从今天开始,任何人都可以去 www.paperspace.com 注册一台云电脑,并创建一个账户。
当我们第一次宣布 Paperspace 时,我们并不知道会触动多大的神经。第一天我们就有超过 12,000 人注册。今天,个人使用 Paperspace 做几乎所有你能想到的事情,从基因组学到游戏,到用 CAD 设计 3D 世界。
我们的愿景是重新发明台式电脑,我们已经不间断地工作了一年多,以完善我们的流算法,设计我们的网络界面,最重要的是,增加服务器容量,以支持越来越多的观众。
为团队介绍 Paperspace】
随着我们的不断发展,纸面空间也必须不断发展,以适应越来越大的部署。今天,我们很高兴地宣布,我们正式推出 Paperspace for Teams,这是 Paperspace 的下一个主要版本!
早期我们意识到团队和个人使用 Paperspace 的方式不同。他们是重度协作者,他们需要将办公室相互连接起来,并且需要工具来管理大型部署。很简单,人越多=越复杂。
因此,我们坐下来,花了几个月的时间收集来自用户的反馈,并专门为团队开发了一个产品。有了这一新产品,Paperspace 可以进行从 5 人办公室到 50,000 人企业的任何规模的部署。
这仅仅是开始,我们在接下来的几个月里还有很多(从游戏到 ML 等等)。订阅我们的博客,获取所有最新公告。
- PS 团队
用 GANs 生成人脸
Photo by Timon Klauser / Unsplash
在高计算能力和先进技术进步的现代时代,我们能够完成许多曾经被认为不可能完成的复杂任务。生成网络是过去十年中最强有力的话题之一,因为它们已经成功地产生了惊人的结果。我们将在本文中讨论的任务之一,也曾被认为是非常复杂的,是借助于生成性对抗网络来生成真实的人脸。已经有多个成功部署的模型和不同的神经架构构建成功地完成了这项任务。
本文将着重于借助生成对抗网络来理解面部生成的一些基本概念。我们将尝试借助于我们在以前的工作中构建的深度卷积生成对抗网络(DCGANs)来完成逼真图像的生成。在继续阅读本文之前,我建议查看我之前的两部作品,以了解本文的最新内容。要开始使用 GANs,请查看以下链接-“”生成性对抗网络(GANs)完全指南;要进一步了解 DCGANs,请查看以下链接-“开始使用 DCGANs ”
要理解我们将在本文中涉及的众多核心概念,请查看目录。我们将首先介绍使用 GANs 生成面部结构,然后进一步了解一些已成功部署的现有技术,以实现这些任务的最佳结果。我们将快速概述 DCGANs 的架构和方法,以完成以下任务。然后进行代码演练,讨论实现理想结果的各个步骤。最后,我们将总结未来工作的一些急需的部分,以及我们可以对构建的架构模型进行的改进和提高的类型。
简介:
生成性对抗网络(GANs)增长迅速。这些双神经网络架构的持续发展,包括一个发生器模型和鉴别器,有助于稳定输出和生成真实图像,人眼几乎不可能区分这些图像。上面 1024x1024 大小的图像显示了一个开朗的女人的图片。看下面的图片,对于刚接触深度学习的人来说,这个人实际上并不存在,这可能会让他们感到惊讶。是从名为这个 X 不存在的网站生成的,你也可以通过刷新下面的链接自行生成随机面孔。每次运行生成器时,都会生成一个全新的真实面孔。
当我问我的几个朋友,他们对这张特定的图片有什么想法时,几乎没有人能够弄清楚下面的图片是在人工智能(AI)的帮助下生成的。准确地说,用于生成以下人脸的 StyleGAN2 体系结构表现出色,产生的结果与真实照片没有区别。只有通过进一步的自省,你才能注意到上面描述的下图中的轻微异常。一个值得注意的问题可能是奇怪的阴影背景。另一个问题可能是她戴的两个耳环的不同,但这也可以被认为是一种有趣的时尚感。像这样的小错误仍然很难解决
推测生成的图像可能不是真实的是很有趣的,但我们正在进行以下讨论的简单事实足以证明,生成网络在面部结构生成或任何其他类似项目等任务中的潜力是巨大的,在即将到来的未来值得关注。我们将探索这些生成网络所具有的巨大潜力,从零开始创建逼真的人脸或任何其他类型的图像。两个神经网络相互竞争以产生最有效的结果的概念是深度学习研究中一个有趣的方面。
让我们从理解当前在人脸生成中使用的一些方法和预先存在的技术开始。一旦我们探索了这些预先存在的技术,我们将更详细地讨论如何在深度卷积生成对抗网络的帮助下产生这些类似的结果。因此,事不宜迟,让我们开始相应地理解和探讨这些主题。
了解一些预先存在的技术:
*
在不到十年的短时间内,GANs 技术的进步日新月异,并取得了极大的成功。2014 年,一个简单的生成式对抗性架构开始了冒险,通过多年的进步和改进,它很快受到了持续的欢迎。在生成神经网络领域,开发人员和研究人员不断发现大量的新技术和方法。上图显示了发展的快速步伐,这是通过在生成性对抗网络中使用的整体技术和方法的改进而实现的。让我们简单地讨论一下目前在机器学习领域中使用的一些奇妙的技术,这些技术的目的是生成真实的人脸。
NVIDIA 开发的 StyleGAN 架构是一个主要架构,它出色地完成了从零开始生成照片级真实感人脸的任务。虽然这些初始模型一开始就产生了惊人的结果,但是在 StyleGAN 架构的第二次迭代中,这些模型获得了巨大的流行。经过彻底的研究、分析和实验,他们修复了一些典型的人工制品特征。2019 年,新版本的风格 GANs 2 发布了先进的模型架构和几种改进培训程序的方法,以达到最佳效果。要了解和学习更多关于这种特殊架构的知识,我建议看看下面的研究论文。最近,发布了 StyleGAN 架构的第三次迭代,在它通常执行的任务类型上有了改进的结果和进一步的进步。
另一个有趣的概念是人脸应用程序,它通过改变人脸来改变一个人的年龄类型,这也是在 GAN 的帮助下建立的。典型地,为了执行这些类型的任务,使用循环 GAN 架构,该架构最近获得了极大的普及。这些甘执行学习不同风格的各种图像的转换的动作。比如 Face App,将生成或上传的人脸转化为不同年龄段的人脸。我们将在未来的工作中探索循环 GAN 和样式,但出于本文的目的,我们将坚持我们之前讨论的 DCGANs 架构来实现生成人脸的任务。
使用 DCGANs 生成面:
在本文的这一部分,我们将进一步探讨 DCGANs 的概念,以及如何在这种架构的帮助下生成大量的人脸。在我之前的一篇关于 DCGANs 的文章中,我们已经涵盖了在这些生成网络的帮助下理解和解决一个典型任务所需的大部分基本概念。因此,我们将有一个模型类型的简要概述,我们将利用它来解决面部生成的任务,并实现最佳结果。让我们开始探索这些概念。
DCGANs 概述:
虽然在生成网络的早期,基本或普通的生成对抗网络产生了一些最好的作品,但这些网络对于更复杂的任务来说不是高度可持续的,并且对于执行某些操作来说在计算上不是那么有效。为此,我们使用各种生成网络架构,这些架构是为特定目的而构建的,并且比其他架构更有效地执行特定动作。在本文中,我们将利用具有深度卷积生成器架构的 DCGANs 模型来生成图像。具有随机潜在噪声向量的发生器将产生面部结构,并且作为图像分类器的鉴别器将区分输出图像是真是假。
虽然深度卷积生成对抗网络的工作过程类似于典型的 GAN 类型网络的工作,但是存在一些关键的架构改进,这些改进被开发来在大多数数据集上产生更有效的结果。在鉴别器模型中,大多数池层被替换为步长卷积,而在生成器模型中被替换为分数步长卷积。为了获得整体架构的稳定性,在生成器和鉴别器模型中广泛使用了批量规范化层。对于完全卷积型架构,大多数后来的密集层,即完全连接的层被移除。
鉴别器模型中的所有卷积层之后是泄漏 ReLU 激活函数,而不是用于分类目的的具有 Sigmoid 的最后一层。生成器模型在除最终层之外的所有层中使用 ReLU 激活函数,最终层出于生成目的使用 tanh 函数。然而,值得注意的是,我们在发电机架构上做了一些小的改动,因为这些是现代的一些微小变化,在特定的用例中显示了一些更好的结果。查看者和开发人员可以根据他们的需求,随意尝试他们自己的变体或坚持他们各自项目的默认原型。
要遵循的程序:
为了有效地构建这个项目,我们将遵循的程序是确保我们为所需的任务收集了最好的数据集。我们的选择是利用互联网上可用的高质量数据集,或者坚持使用其他一些替代方法来执行这些任务。我们将利用名人面孔属性(CelebA)数据集来开发我们的面部识别生成网络。以下数据集可在 Kaggle 上获得,建议您下载它以继续文章的剩余内容。如果你有一个先进的系统,拥有计算高端问题的资源,那么你可以尝试名人 HQ 数据集。此外,请记住,Paperspace 上的渐变平台是这些大量 GPU 相关任务的最佳选择之一。
一旦我们下载了数据集,确保您在“图像”目录中提取了 img align celeba zip 文件,以继续下一步。我们将在这个项目中利用 TensorFlow 和 Keras 深度学习框架。如果你没有这两个库的完整知识,或者想快速更新你的基础知识,我建议你查看 TensorFlow 的以下链接和 Keras 的这个特定的链接。一旦我们访问了数据集,我们将构建生成器和鉴别器模型来执行人脸生成任务。整个建筑的构建将集中于创造一个对抗性的框架,以生成图像并对最现实的解释进行分类。在整个建筑构建完成后,我们将开始训练过程并相应地保存图像。最后,我们将保存生成器和鉴别器模型,以便将来可以重用它们。
代码演练:
在本文的这一部分,我们将探索如何从头开始编写我们的人脸生成器模型,以便我们可以利用这些生成器模型来获得高质量的结果,类似于一些最先进的方法。值得注意的是,在这个项目中,我们将使用较小的尺寸(64x64 ),这样所有层次的人都可以构建这个作品,而不会受到图形限制的阻碍。Paperspace 提供的梯度平台是这些复杂计算项目的最佳实用选项之一。如果你有更好的设备或者想要得到更好的结果,那么我会建议你尝试更多互联网上的高清数据集。请随意探索这些选项,并相应地选择最佳选项。出于本文的目的,我们将坚持使用一个简单的较小维度的数据集,它不会占用太多空间。让我们开始这项工程的建设吧。
*### 导入基本库:
大多数项目的第一步是导入我们将用于所需项目开发的所有基本库。为了构建这个人脸生成模型,我们将利用 TensorFlow 和 Keras 深度学习框架来实现我们的目标。如果您喜欢 PyTorch,这也是按照您的意愿开发项目的另一个有效选择。如果您不熟悉 TensorFlow 或 Keras,请查看以下两篇文章,它们将详细指导您如何开始使用这些库-“tensor flow绝对指南”和“Keras绝对指南。”除了这些精彩的框架,我们还将利用其他基本库,如 matplotlib、numpy 和 OpenCV,来相应地执行可视化、计算和图形相关的操作。
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt
import os
import cv2
from tqdm import tqdm
您可以随意使用您认为必要或适合执行以下任务的其他库。您还可以添加许多图形工具或可视化图形选项来跟踪您的进度,并最终帮助您改善结果。一旦我们成功地导入了我们需要的所有库,我们就可以继续研究数据并构建我们的模型来解决下面的任务。
探索数据:
在这一步中,我们将把数据加载到数据集格式中,并相应地映射细节。Keras 中的预处理类允许我们访问来自特定目录的数据流,这样我们就可以访问存储在特定文件夹中的所有图像。确保一旦提取了包含超过 200000 张图像的名人数据集,就将提取的目录放入另一个名为“ Images 的目录或您选择的任何其他文件夹名称中。数据集将来自以下目录,我们将指定图像大小为 64 x 64 ,用于计算以下数据。
dataset = keras.preprocessing.image_dataset_from_directory(
"Images", label_mode=None, image_size=(64, 64), batch_size=32
)
dataset = dataset.map(lambda x: x / 255.0)
上述代码单元块的成功执行应该显示类似于以下语句的结果-“找到属于 1 个类的 202599 个文件。"您可以选择随机显示一幅图像,以检查您是否已按计划精确加载数据集。下面的代码块是可视化数据的方法之一。
for i in dataset:
plt.axis("off")
plt.imshow((i.numpy() * 255).astype("int32")[0])
break
通过对数据的标准探索并将它们有效地组合到您的代码中,您可以继续为面生成的计算构建鉴别器和生成器块。让我们在文章接下来的部分中讨论这些内容。
构建鉴别器模型:
首先,我们将建立生成网络的鉴别器模型来对图像进行真伪分类。鉴别器的模型架构与上一节讨论的完全一样。我们将使用(64,64,3)的图像形状,它与原始图像的大小相同。泄漏 ReLU 将跟随在不同滤波器大小的每个卷积层之后。最后,在以下组合的三个块之后,我们将添加一个平坦层,一个下降层,最后添加具有 sigmoid 激活函数的密集层,以做出 0 或 1 的预测,用于区分真实或虚假的图像。
discriminator = keras.Sequential(
[
keras.Input(shape=(64, 64, 3)),
layers.Conv2D(64, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Conv2D(128, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Flatten(),
layers.Dropout(0.2),
layers.Dense(1, activation="sigmoid"),
],
name="discriminator",
)
请注意,如果您针对不同的图像大小(尤其是分辨率较高的图像)执行以下操作,您将需要修改一些参数,如过滤器大小,以获得更精确的结果。但是,请注意,GPU 可能有一些限制,根据您在本地系统上使用的机器类型,您可能会遇到资源耗尽错误。对于更高分辨率的任务,Paperspace 上的渐变平台是一个值得考虑的好选择。
构建发电机模型:
将构建 DCGANs 的生成器模型,与本文概述部分中讨论的类似。我们将做的一些改变是在发电机模型中使用的一般激活函数的修改。请注意,本文中建议的 ReLU 函数由泄漏 ReLU 代替,类似于我们在鉴别器中使用的 ReLU 函数,并且最终的 tanh 函数由 sigmoid 激活代替。如果您希望使用更高质量的图像来构建项目(如 512x512 或 1024x1024),请确保对去卷积层的跨度进行必要的更改,以适应特定的架构。
latent_dim = 128
generator = keras.Sequential(
[
keras.Input(shape=(latent_dim,)),
layers.Dense(8 * 8 * 128),
layers.Reshape((8, 8, 128)),
layers.Conv2DTranspose(128, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Conv2DTranspose(256, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Conv2DTranspose(512, kernel_size=4, strides=2, padding="same"),
layers.LeakyReLU(alpha=0.2),
layers.Conv2D(3, kernel_size=5, padding="same", activation="sigmoid"),
],
name="generator",
)
一旦您创建了发生器和鉴别器模型,我们就可以开始 GANs 的训练过程。我们还将每隔一段时间生成图像,以确保我们收到关于我们的架构构建的性能的简要想法。
训练和生成图像:
构建我们项目的最后一步是为大量时期训练模型。请注意,我只为后面的火车使用了五个历元,并以 100 的间隔保存了一个图像。梯度带功能将相应地自动编译两个模型,并且可以开始下一个项目的构建阶段。继续训练模型,直到您对生成器产生的结果感到满意为止。
for epoch in range(5):
for index, real in enumerate(tqdm(dataset)):
batch_size = real.shape[0]
vectors = tf.random.normal(shape = (batch_size, latent_dim))
fake = generator(vectors)
if index % 100 == 0:
Image = keras.preprocessing.image.img_to_array(fake[0])
# path = (f"My_Images/images{epoch}_{index}.png")
# cv2.imwrite(path, image)
keras.preprocessing.image.save_img(f"My_Images/images{epoch}_{index}.png", Image)
with tf.GradientTape() as disc_tape:
real_loss = loss_function(tf.ones((batch_size, 1)), discriminator(real))
fake_loss = loss_function(tf.zeros(batch_size, 1), discriminator(fake))
total_disc_loss = (real_loss + fake_loss)/2
grads = disc_tape.gradient(total_disc_loss, discriminator.trainable_weights)
discriminator_optimizer.apply_gradients(zip(grads, discriminator.trainable_weights))
with tf.GradientTape() as gen_tape:
fake = generator(vectors)
generator_output = discriminator(fake)
generator_loss = loss_function(tf.ones(batch_size, 1), generator_output)
grads = gen_tape.gradient(generator_loss, generator.trainable_weights)
generator_optimizer.apply_gradients(zip(grads, generator.trainable_weights))
一旦您的训练过程完成,您就可以将生成器和鉴别器模型保存到它们各自的变量中,保存在您创建的同一个目录或另一个目录中。请注意,您也可以在每个时期的训练过程中保存这些模型。如果您有任何系统问题或其他可能影响训练过程的完整计算的类似问题,最好的想法可能是在每次迭代结束时保存模型。
generator.save("gen/")
discriminator.save("disc/")
请随意在这个项目上进行其他实验。我推荐尝试的一些变化是为 GANs 构建一个自定义类,并相应地构建一个鉴别器和发电机损耗的跟踪器。您还可以创建检查点系统来监控最佳结果,并保存生成器和鉴别器模型。一旦您对给定的图像尺寸感到满意,就值得尝试更高的分辨率以获得更好的解释结果。现在让我们看看一些可能的未来工作,我们可以专注于神经网络和深度学习这一领域的进一步发展。
未来作品:
人脸生成是一个不断发展的概念。即使在生成性对抗网络领域有了所有的发现和新的现代技术,仍有巨大的潜力远远超出简单的面孔世代的界限。在文章的这一部分,我们将重点关注深度学习领域目前正在发生的未来作品的类型,以及你在使用生成网络和深度学习方法论的人脸生成艺术方面的下一步进展必须是什么。让我们分析几个有助于创新和将我们的生成模型提升到更高水平的想法。
我们模型的进一步发展和改进:
我们知道,我们目前建立的模型能够在多次训练后生成非常逼真的训练图像,这可能需要几个小时到几天的训练,具体取决于您使用的系统类型。然而,我们可以注意到,尽管该模型在生成大量人脸方面表现得相当好,但是由于生成器模型随机生成人脸结构,因此我们在选择生成时所拥有的控制量相当少。我们可以对模型进行的一些主要改进是,能够以某种方式构建模型,使许多参数处于我们的控制之下,并且我们可以选择稳定接收的输出。
您可以调整的一些参数和属性是人类特征,如性别、年龄、情感类型、各种头部姿势、种族、皮肤色调、头发颜色、佩戴的配件类型(如帽子或太阳镜)等等。通过让所有这些角色随着你构建的生成性对抗网络而变化,许多变化都是可能的。在 Face Generator 网站上可以看到一个执行这种操作的例子,在那里你可以生成几乎 11,232,000 个以上的同一张脸的不同组合。我们也可以在我们的训练模型中进行这样的改进,以获得更好的结果,同时对我们选择生成的面部结构类型进行体面的控制。
写实 Deepfakes 作品:
在前面的一个章节中,我们讨论了 Face App 如何在生成性对抗网络的帮助下,从特定范围内改变一个人的年龄,即使用 Cycle GANs。为了让这些实验更进一步,可以在我们的项目中使用 Deepfakes。通过有效利用人工智能和机器学习,即使用自动编码器和生成对抗网络等生成网络,可以生成合成媒体(AI 生成的媒体)。这种强大的技术可以操纵或生成人脸、视频内容或音频内容,这些内容极其逼真,几乎无法区分真假。
通过现代的 Deepfakes 计算,你可以生成一张脸,并让生成的同一张脸执行许多动作,如有趣的对话或实际上从未存在的面部运动。将我们的生成性神经网络架构与其他类似技术相结合,创造出绝对令人难以置信和意想不到的东西,有着巨大的空间。未来值得尝试的实验之一是构建一个生成模型,可以生成高精度、高真实感的高质量人脸。使用这种生成的人脸并将其与 Deepfakes 技术相结合,许多成就和有趣的项目都是可能的。请随意探索这些主题为开发人员提供的各种组合和排列。
结论:
异常精确的人脸生成和逼真的显示是生成式对抗网络最伟大的成就之一。从几十年前认为面部生成的以下任务几乎是不可能的,到 2014 年生成平均面部生成,到这些看起来如此逼真和难以置信的面部结构的持续发展,是一个巨大的成就。这些生成神经网络接管大部分深度学习并创造新的研究领域的可能性是巨大的。因此,对于现代深度学习研究人员来说,开始实验和学习这些现象模型架构具有极其重要的意义。
在这篇文章中,我们理解了生成对立网络对于完成复杂任务的重要性,例如生成从未实际存在过的人类的真实面孔。我们讨论了不同的预先存在的技术的主题,这些技术在现代开发阶段被不断地部署,以成功地完成下面的任务。然后,我们研究了我们以前的 DCGANs 架构的简要概述,并了解了如何使用这些网络处理人脸生成。在对以下内容进行了广泛的代码审查后,我们理解了使用 DCGANs 生成人脸的过程。最后,我们探索了这些网络的巨大潜力,获得了更多关于这些模型未来发展和改进的知识。
在接下来的文章中,我们将进一步探讨生成对抗网络的主题,并尝试了解多年来发展起来的不同类型的生成对抗网络。我们将探索的下一个工作将涉及超分辨率生成对抗网络(SRGANs)的效用。在那之前,享受研究、编程和开发吧!*
使用 Keras 和 Streamlit 进行人脸验证
原文:https://blog.paperspace.com/face-verification-with-keras/
Streamlit 使数据科学家和机器学习实践者能够快速构建数据和机器学习应用程序。在本文中,我们将了解如何使用 Streamlit 构建人脸验证应用程序。然而,在我们开始验证人脸之前,我们必须检测它们。在计算机视觉中,人脸检测是在图像中定位和定位人脸的任务。人脸验证是比较两幅或多幅图像相似性的过程。
下图显示了我们将在本文中使用的应用程序。现场演示是位于这里。
用 MTCNN 检测人脸
MTCNN 代表多任务级联卷积神经网络,在本文的中首次提出。该框架由三个阶段组成:
- 在第一步中,候选窗口和它们的包围盒回归向量通过称为建议网络(P-Net)的浅层 CNN 产生
- 下一步使用复杂的 CNN 来拒绝非人脸窗口。该论文将这种 CNN 称为精炼网络(R-Net)。
- 然后,它使用更复杂的 CNN 来提炼结果,并输出五个面部标志的位置。
上述三级级联框架的输入是已经被调整大小到不同比例的图像,以便构建图像金字塔。
在本文中,我们将使用 Iván de Paz Centeno 的实现该文件。它以 pip 安装包的形式提供。
pip install mtcnn
人脸识别和验证模型
VGGFace 指的是从牛津大学的视觉几何小组(VGG)的计算机视觉数据集开发的人脸识别模型。这个系列的主要机型有 VGGFace 和 VGGFace2。深度人脸识别论文提出了一种用于人脸识别和验证的大规模数据集。本文描述了一种通过比较人脸在欧几里德空间中的嵌入来验证人脸的方法。这是通过三重损耗来实现的。在这个实现中,相似人脸之间的欧几里德距离很小,而不同人脸之间的欧几里德距离较大。
VGGFace2 在本文的中有描述。它提出了一个新的大规模人脸数据集,包含 9131 个对象的 331 万张图像。每个对象平均有 362.6 张图片。在该数据集上训练的模型被称为 VGGFace2。这些模型可以在这个 GitHub repo 上找到。我们将使用这个模型的 Keras 实现来验证人脸。我们需要做的第一件事是检测人脸,然后获得它们的嵌入。这篇论文将这些嵌入称为面部描述符。然后使用余弦相似度计算这些描述符之间的距离。
在 Keras 中,我们可以通过使用 keras-vggface 包来使用 VGGDFace 模型。你必须从 GitHub 安装它。
pip install git+https://github.com/rcmalli/keras-vggface.git
设置细流应用程序
在我们开始开发应用程序之前,您需要安装所有的需求。需求在这里可用。下载文件后,通过 pip 安装所有要求。
pip install -r requirements.txt
导入所有需求
现在打开一个新的 Python 文件(最好命名为app.py
)并在文件顶部导入所有需求:
MTCNN
将用于人脸检测Streamlit
用于设置网络应用程序Matplotlib
用于显示人脸Circle
用于在脸上标记地标Rectangle
用于在检测到的人脸周围创建一个方框Image
用于打开图像文件cosine
计算人脸之间的余弦相似度VGGface
用于获取人脸嵌入preprocess_input
用于将图像转换成模型要求的格式
from mtcnn.mtcnn import MTCNN
import streamlit as st
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.patches import Circle
from PIL import Image
from numpy import asarray
from scipy.spatial.distance import cosine
from keras_vggface.vggface import VGGFace
from keras_vggface.utils import preprocess_input
设置下拉菜单
下一步是设置下拉选项。幸运的是,Streamlit 提供了各种用于构建 HTML 页面的小部件。
在这种情况下,我们需要创建一个select_box
。选择框函数需要一个标签和选择框所需的选项。将函数赋给变量使我们能够捕捉用户选择的选项。
choice = st.selectbox("Select Option",[
"Face Detection",
"Face Detection 2",
"Face Verification"
])
定义主要功能
我们需要做的下一件事是创建一个主函数,它将负责各种用户选择。在这个函数中,我们将构建检测人脸以及验证人脸的代码。
让我们从实现面部检测功能开始。第一步是检查用户是否选择了面部检测选项。在那之后,接下来发生的是:
- 使用 Streamlit 中的
file_uploader
上传文件 - 使用 Pillow 打开图像,并将其转换为 NumPy 数组
- 关闭 Matplotlib 轴后可视化图像
- 创建一个用于检测人脸的
MTCNN
检测器 - 获得面部和面部标志的坐标
- 使用
Circle
和Rectangle
显示带有边界框和面部标志的面部
def main():
fig = plt.figure()
if choice == "Face Detection":
uploaded_file = st.file_uploader("Choose File", type=["jpg","png"])
if uploaded_file is not None:
data = asarray(Image.open(uploaded_file))
plt.axis("off")
plt.imshow(data)
ax = plt.gca()
detector = MTCNN()
faces = detector.detect_faces(data)
for face in faces:
x, y, width, height = face['box']
rect = Rectangle((x, y), width, height, fill=False, color='maroon')
ax.add_patch(rect)
for _, value in face['keypoints'].items():
dot = Circle(value, radius=2, color='maroon')
ax.add_patch(dot)
st.pyplot(fig)
除了显示图像中的点和边界框,我们还可以返回检测到的人脸。过程类似于我们上面所做的。唯一不同的是,我们使用检测到的人脸的坐标来提取人脸。之后,我们使用 Pillow 将数组转换成图像。
results = detector.detect_faces(pixels)
x1, y1, width, height = results[0]["box"]
x2, y2 = x1 + width, y1 + height
face = pixels[y1:y2, x1:x2]
我们使用 Streamlit beta_columns
在左边显示图像,在右边显示检测到的人脸。这是该部分的完整代码。
elif choice == "Face Detection 2":
uploaded_file = st.file_uploader("Choose File", type=["jpg","png"])
if uploaded_file is not None:
column1, column2 = st.beta_columns(2)
image = Image.open(uploaded_file)
with column1:
size = 450, 450
resized_image = image.thumbnail(size)
image.save("thumb.png")
st.image("thumb.png")
pixels = asarray(image)
plt.axis("off")
plt.imshow(pixels)
detector = MTCNN()
results = detector.detect_faces(pixels)
x1, y1, width, height = results[0]["box"]
x2, y2 = x1 + width, y1 + height
face = pixels[y1:y2, x1:x2]
image = Image.fromarray(face)
image = image.resize((224, 224))
face_array = asarray(image)
with column2:
plt.imshow(face_array)
st.pyplot(fig)
人脸验证
为了验证人脸,我们首先需要使用 VGGFace 模型嵌入人脸。在模型中传递include_top=False
意味着我们对模型的最终分类层不感兴趣。这很重要,因为我们只对学习人脸嵌入感兴趣。之后,我们将计算两个人脸嵌入的余弦相似度。我们还设置了 0.50 的阈值来确定面部相似性。我们可以使用 Streamlit 列来并排展示这两个面。
elif choice == "Face Verification":
column1, column2 = st.beta_columns(2)
with column1:
image1 = st.file_uploader("Choose File", type=["jpg","png"])
with column2:
image2 = st.file_uploader("Select File", type=["jpg","png"])
if (image1 is not None) & (image2 is not None):
col1, col2 = st.beta_columns(2)
image1 = Image.open(image1)
image2 = Image.open(image2)
with col1:
st.image(image1)
with col2:
st.image(image2)
filenames = [image1,image2]
faces = [extract_face(f) for f in filenames]
samples = asarray(faces, "float32")
samples = preprocess_input(samples, version=2)
model = VGGFace(model= "resnet50" , include_top=False, input_shape=(224, 224, 3),
pooling= "avg" )
# perform prediction
embeddings = model.predict(samples)
thresh = 0.5
score = cosine(embeddings[0], embeddings[1])
if score <= thresh:
st.success( " >face is a match (%.3f <= %.3f) " % (score, thresh))
else:
st.error(" >face is NOT a match (%.3f > %.3f)" % (score, thresh))
我们还创建了一个函数,用于从上传的图像中检测和返回人脸。功能:
- 使用 MTCNN 检测人脸
- 从图像中提取人脸
- 使用 Pillow 将图像数组转换为图像
- 将图像调整到所需的大小
- 以 NumPy 数组的形式返回图像
def extract_face(file):
pixels = asarray(file)
plt.axis("off")
plt.imshow(pixels)
detector = MTCNN()
results = detector.detect_faces(pixels)
x1, y1, width, height = results[0]["box"]
x2, y2 = x1 + width, y1 + height
face = pixels[y1:y2, x1:x2]
image = Image.fromarray(face)
image = image.resize((224, 224))
face_array = asarray(image)
return face_array
我们也可以用两张不相似的脸来证明这一点。
这个实现中的最后一件事是运行 main 函数。
if __name__ == "__main__":
main()
该项目的代码和要求可以在这里找到。
使用 Streamlit 共享部署应用程序
Streamlit 允许我们只需点击两次按钮即可部署机器学习应用程序。前往share.streamlit.com开设一个免费账户。单击“新建应用程序”按钮开始部署过程。对于这个免费版本,应用程序需要在 GitHub 上免费提供。
之后,您将选择想要部署的存储库,并单击 deploy 按钮。repo 需要有一个包含应用程序所需的所有库的requirements.txt
文件。
最后的想法
在本文中,您看到了如何使用 Streamlit 和 Keras 构建和部署图像验证应用程序。 Streamlit 可用于构建和部署各种数据科学和机器学习应用。它的语法也很容易掌握。在 VGG 模型中,我们使用了 ResNet 架构,您可以尝试不同的模型,如 VGG16 和 SENet 50,并比较结果。如果你觉得雄心勃勃,你还可以进一步微调模型。
就这样了,伙计们!
与法肯斯坦博士和 CtrlShiftFace 一起使用
原文:https://blog.paperspace.com/faceswap-face-off-viral-video-creators-dr-fakenstein-ctrl_shift_face/
更新!(2019 年 12 月) CtrlShiftFace 有新视频首页史泰龙用 Paperspace 制作:
https://www.youtube.com/embed/2svOtXaD3gg?feature=oembed
Deepfake 技术开启了媒体制作的新时代。与所有技术一样,积极和有害的用例都存在。作为伦理技术专家,我们渴望推动可能的极限,同时关注我们创造的工具的影响。我们很高兴看到我们的用户推动了娱乐、媒体等领域的发展。
https://www.youtube.com/embed/sTksmoTdT4Q?feature=oembed
Paperspace 的 GPU 云最初旨在通过我们的核心产品增加创意生产。现在,随着我们的用于 ML &数据科学的渐变工具套件以及 deepfake 技术的出现,我们发现自己正处于这两者的结合点。我们渴望看到 CORE & Gradient 如何以创新的方式结合起来,并与两家利用这两者的 deepfake 生产商合作,以便更好地了解 Paperspace 如何帮助他们的工作流程,并评估该技术的更广泛影响。我们认为他们的病毒式面部交换视频(在同样著名的源材料中使用高知名度的名人)是一种良性的方式来突出这项技术的复杂性,同时鼓励公众质疑提供给他们的内容的真实性。
https://www.youtube.com/embed/aUphMqs1vFw?feature=oembed
法肯斯坦博士 和 Ctrl_Shift_Face 凭借其经常爆笑的和频繁的 挑衅的混搭,将视频内容的混合文化带到了一个全新的水平。“法肯斯坦博士”是杰夫·怀特的网名,她现在是一名专业的深度假制作人,住在新西兰。“CtrlShiftFace”是一位欧洲 VFX 专家的在线角色,他选择保持匿名。在通过 Paperspace 与这两家公司合作以提高他们的创意产出后,我们了解了他们的故事,并了解了他们对艺术现状的想法以及大众对赝品的接受程度。
你是谁,和深度假货有什么关系?
CSF: 不适用
DF: 我在 Instagram 上以@ drfakenstein的身份为社交媒体制作喜剧深度假视频。最初是作为一个爱好开始的,现在我和我在新西兰的公司一起为电视、电影、音乐行业和社交媒体专业制作 deepfake 内容。
你是如何发现这项技术的?
CSF: 我看了 SIGGRAPH paper tech 演示。那是我第一次看到 deepfake 的地方,几年前。我热爱各种形式的科技...人工智能是科技的未来。它将无处不在。我被它迷住了。我认为 deepfakes 是创造这些奇异的替代时间线的工具。
DF :我总是在学习新技术。当比特币价格在 2018 年暴跌时,我最初自学了如何挖掘加密货币,并拥有多余的计算机 GPU。我读了一篇关于 deepfake 技术的文章,认为用它来学习和制作视频可能很有趣。所以我用 crypto miner 的零件为 deepfakes 造了一台电脑,并尝试用 deepfake 软件交换面部,我最初做这些是为了让自己笑,我想其他人可能会喜欢他们的视频,所以把它们发布到 Instagram 上。
这项技术的现状如何?当前的挑战或限制是什么?
通过软件开发和学到的技术来改进 deepfake 流程,它仍处于起步阶段,并在快速改进。与 6 个月前相比,我现在能做的是一个巨大的质量飞跃,随着时间的推移只会越来越好。Deepfakes 和 Paperspace 很配。它允许我以比典型的 deepfake 设置更高的速度和质量来渲染人脸。
几年前一些关于 deepfakes 的 SIGGRAPH 论文比我用的要先进得多。我正在使用的软件是在 GitHub 上开源的。《太空漫游》中的埃隆·马斯克使用了极具挑战性的灯光。此外,特写镜头很难,因为目前 deepfakes 的分辨率不是很高。它(通常)小于 200x200 像素。主要的限制是计算机能力。您需要配备大量 VRAM 的高端专业级 GPU,而这正是 Paperspace 所能提供的。
https://www.youtube.com/embed/VWrhRBb-1Ig?feature=oembed
假内容与真内容难以区分还需要多久?炒作是否言过其实或有根据?
我认为我们距离难以区分的 deepfakes 还有几年的时间。即使“6 个月”在媒体上被抛来抛去,我相信它会更长。我认为有些夸大其词了。最近有很多媒体关注 deepfake 技术。虽然总是存在邪恶的人制作内容来陷害他人或传播虚假新闻的风险,但有人正在研究检测这些深度伪造的技术。我还认为,以娱乐为目的的 deepfakes 正在提高人们对这项技术及其能力的认识。
当然你可以用它来做邪恶的事情,但那不是我做的。所有的面孔都是平等的。你甚至可以在真人身上伪造动画或视频游戏角色,反之亦然。英伟达已经有了可以生成高分辨率肖像的技术。deepfakes 将如此先进,以至于无法与真实的东西区分开来,这只是时间问题,但我认为公众不会很容易获得这种软件,但大型预算电影工作室将更经常使用这种软件。
https://www.youtube.com/embed/FiGeTuw-QJU?feature=oembed
在一个理想的世界里,谁能接触到这项技术?
DF :我不相信应该被限制,我用的 deepfake 软件是开源的。还有其他 deepfake 技术没有被大学发布。一家公司将 deepfake 技术商业化只是时间问题。Deepfakes 只是制作喜剧视频的另一个工具,这项技术使我能够与我从未想象过的人一起工作,并制作令人发笑并照亮他们一天的内容。
你认为目前围绕深度假货的讨论缺少了什么?
娱乐产业的潜力被危言耸听所掩盖。deepfake 技术有许多不同的可能性,但大多数都被忽略了。讽刺的 Deepfakes,电影“如果”的,带回流行的演员已经传递回电影或电视等。影视特技工作的 Deepfake。面向音乐行业的 Deepfake 面向一直在路上或者对音乐视频或内容制作不靠谱的艺人的标签。深度伪装有一个负面的名声,因为一些人最初使用这项技术制作色情作品,把名人的脸放在里面。有一个由深度制作者组成的“ S afe F 或 W ork”社区,他们看到了这项技术在娱乐方面的潜力,并为迅速增长的观众提供了很好的内容。
媒体喜欢围绕 deepfakes 传播的恐惧比 deepfakes 本身更危险。同样,值得一提的是,检测 deepfakes 的人工智能程序也在开发中,同时 deepfakes 本身也在开发中。
涉及 deepfakes 的“最坏情况”有哪些?
人们相信深度造假是真的,它会毁掉某人的名誉或牵连他们。
或者相反,一个有权势的政治人物实际上被发现在做某事,并且可以坚持说他们犯罪的证据是假的,即使是真的。
https://www.youtube.com/embed/Ho9h0ouemWQ?feature=oembed
使用深度学习的面部识别
原文:https://blog.paperspace.com/facial-recognition-using-deep-learning/
卷积神经网络(CNN)和特征提取
卷积神经网络允许我们从图像中提取广泛的特征。事实证明,我们也可以将这种特征提取的想法用于人脸识别!这就是我们在本教程中要探索的,使用深度 conv 网进行人脸识别。注意:这是人脸识别(即实际说出是谁的脸),而不仅仅是检测(即识别图片中的人脸)。
如果你不知道什么是深度学习(或者什么是神经网络),请阅读我的帖子初学者的深度学习。如果你想尝试一个使用卷积神经网络进行图像分类的基础教程,你可以尝试这个教程。请记住,本教程假设您有基本的编程经验(最好是 Python),并且您了解深度学习和神经网络的基本思想。
我们将要使用的人脸识别方法相当简单。这里的关键是让一个深度神经网络产生一串描述一张脸的数字(称为人脸编码)。当您传入同一个人的两个不同图像时,网络应该为这两个图像返回相似的输出(即更接近的数字),而当您传入两个不同人的图像时,网络应该为这两个图像返回非常不同的输出。这意味着需要训练神经网络来自动识别面部的不同特征,并基于此计算数字。神经网络的输出可以被认为是特定人脸的标识符——如果你传入同一个人的不同图像,神经网络的输出将非常相似/接近,而如果你传入不同人的图像,输出将非常不同。
谢天谢地,我们不必经历训练或建立自己的神经网络的麻烦。我们可以通过我们可以使用的 dlib 访问一个经过训练的模型。它确实做了我们需要它做的事情——当我们传入某人的面部图像时,它输出一串数字(面部编码);比较来自不同图像的人脸编码将告诉我们某人的脸是否与我们有图像的任何人相匹配。以下是我们将采取的步骤:
-
检测/识别图像中的人脸(使用人脸检测模型)——为了简单起见,本教程将只使用包含一个人脸/人的图像,而不是更多/更少
-
预测面部姿态/ 界标(针对步骤 1 中识别的面部)
-
使用来自步骤 2 的数据和实际图像,计算面部编码(描述面部的数字)
-
比较已知人脸的人脸编码和测试图像中的人脸编码,判断照片中的人是谁
希望你对这将如何工作有一个基本的概念(当然上面的描述是非常简化的)。现在是时候开始建设了!
准备图像
首先,创建一个项目文件夹(只是一个保存代码和图片的文件夹)。对我来说,这叫做人脸识别,但你可以随便叫它什么。在该文件夹中,创建另一个名为 images 的文件夹。这是一个文件夹,将保存您要对其运行人脸识别的不同人的图像。从脸书下载一些朋友的图片(每人一张),将图片重命名为朋友的名字(如 taus.jpg 或 john.jpg),然后将它们全部存储在您刚刚创建的 images 文件夹中。需要记住的一件重要事情是:请确保所有这些图片中只有一张脸(即它们不能是组图),并且它们都是 JPEG 格式,文件名以. jpg 结尾。
接下来,在您的项目文件夹(对我来说是 face_recognition 文件夹)中创建另一个文件夹,并将其命名为 test。该文件夹将包含同一个人的幅不同的幅图像,这些人的照片存储在 images 文件夹中。再次,确保每张照片只有一个人在里面。在测试文件夹中,您可以随意命名图像文件,并且您可以拥有每个人的多张照片(因为我们将对测试文件夹中的所有照片运行人脸识别)。
安装依赖项
这个项目最重要的依赖项是 Python 2.7 和 pip。您可以使用 Anaconda 2(这只是一个预打包了 pip 的 Python 发行版)通过点击这个链接来安装这两个版本(如果您还没有安装的话)。注意:请确保将 Anaconda 2 添加到您的路径中,并注册为您的系统 Python 2.7(在安装过程中会有提示;只需按“是”或勾选复选框)。
如果您已经完成了 Anaconda 2 的设置,或者您已经预先在机器上安装了 Python 2.7 和 pip,那么您可以继续安装 dlib (我们将使用的机器学习库)和其他依赖项。为此,请在终端(Mac OS 或 Linux)或命令提示符(Windows)中键入以下命令:
pip install --user numpy scipy dlib
如果您是 Mac 或 Linux 用户,并且在使用上述命令时遇到问题,请尝试以下命令:
sudo pip install --user numpy scipy dlib
如果上述过程对您不起作用,您可能需要手动下载、编译和安装 dlib 及其 Python API。为此,你必须读一些关于 http://dlib.net/的书。不幸的是,这超出了这篇博文的范围,因此我不会在这里讨论。
你需要做的最后一件事是下载预先训练好的人脸识别模型。你需要两种型号。一个模型预测一张脸的形状/姿势(基本上给你形状在图像中如何定位的数字)。另一个模型,获取人脸并给你人脸编码(基本上是描述那个特定人的脸的数字)。以下是关于如何下载、提取和准备它们的说明:
-
从这个链接下载 dlib _ face _ recognition _ resnet _ model _ v1 . dat . bz2,从这个链接下载 shape _ predictor _ 68 _ face _ landmarks . dat . bz2
-
一旦下载了这两个文件,就需要解压它们(它们以 bz2 格式压缩)。在 Windows 上,你可以使用 Easy 7-zip 来实现。在 Mac 或 Linux 上,您应该能够双击文件并提取它们。如果这不起作用,只需在您的终端中为这两个文件键入以下内容:bzip2 {PATH_TO_FILE} -解压缩(将{PATH_TO_FILE}替换为您试图提取的文件的实际路径;对我来说,命令应该是 bzip2 ~/Downloads/dlib _ face _ recognition _ resnet _ model _ v1 . dat . bz2-decompress 和 bzip2 ~/Downloads/shape _ predictor _ 68 _ face _ landmarks . dat . bz2-decompress。
-
提取之后,您应该有两个名为 dlib _ face _ recognition _ resnet _ model _ v1 . dat 和 shape _ predictor _ 68 _ face _ landmarks . dat 的文件。将这两个文件复制到您的项目文件夹中(对我来说,这是我为此项目创建的 face_recognition 文件夹)。
代码!
现在,你已经设置好了一切,在文本编辑器(最好是 Atom 或 Sublime Text )中打开你的项目文件夹(对我来说叫做 _ recognitionfor)。在名为 recognize.py 的文件夹中创建一个新文件。我们将在这里添加代码以匹配您朋友的面孔。注意,这个过程有两个主要部分:首先,在 images 文件夹中加载已知人脸的人脸编码;完成后,从存储在测试文件夹中的人脸/图像中获取人脸编码,并将它们与我们所有已知的人脸一一匹配。我们将逐步完成这一部分。如果您想看到它运行,您可以将该部分中的所有代码一个接一个地复制粘贴到您的文件中(即,按照下面列出的相同顺序合并所有单独的代码部分)。仔细阅读每个代码块中的注释,了解它的作用。
第 1 部分:初始化和设置在这里,我们导入所需的库,并设置人脸识别所需的对象/参数。
import dlib
import scipy.misc
import numpy as np
import os
# Get Face Detector from dlib
# This allows us to detect faces in images
face_detector = dlib.get_frontal_face_detector()
# Get Pose Predictor from dlib
# This allows us to detect landmark points in faces and understand the pose/angle of the face
shape_predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
# Get the face recognition model
# This is what gives us the face encodings (numbers that identify the face of a particular person)
face_recognition_model = dlib.face_recognition_model_v1('dlib_face_recognition_resnet_model_v1.dat')
# This is the tolerance for face comparisons
# The lower the number - the stricter the comparison
# To avoid false matches, use lower value
# To avoid false negatives (i.e. faces of the same person doesn't match), use higher value
# 0.5-0.6 works well
TOLERANCE = 0.6
第 2 部分:从图像中获取人脸编码这里我们编写一个函数,它将获取一个图像文件名,并为我们提供该图像的人脸编码。
# This function will take an image and return its face encodings using the neural network
def get_face_encodings(path_to_image):
# Load image using scipy
image = scipy.misc.imread(path_to_image)
# Detect faces using the face detector
detected_faces = face_detector(image, 1)
# Get pose/landmarks of those faces
# Will be used as an input to the function that computes face encodings
# This allows the neural network to be able to produce similar numbers for faces of the same people, regardless of camera angle and/or face positioning in the image
shapes_faces = [shape_predictor(image, face) for face in detected_faces]
# For every face detected, compute the face encodings
return [np.array(face_recognition_model.compute_face_descriptor(image, face_pose, 1)) for face_pose in shapes_faces]
第 3a 部分:比较人脸我们在这里编写一个函数,它将给定的人脸编码与一系列已知的人脸编码进行比较。它将返回一个布尔值(真/假)数组,指示是否存在匹配。
# This function takes a list of known faces
def compare_face_encodings(known_faces, face):
# Finds the difference between each known face and the given face (that we are comparing)
# Calculate norm for the differences with each known face
# Return an array with True/Face values based on whether or not a known face matched with the given face
# A match occurs when the (norm) difference between a known face and the given face is less than or equal to the TOLERANCE value
return (np.linalg.norm(known_faces - face, axis=1) <= TOLERANCE)
第 3b 部分:查找匹配我们在这里编写一个函数,它将接受一个已知人脸编码列表、一个人名列表(对应于已知人脸编码列表)和一个要查找匹配的人脸。它将调用 3a 中的函数,并返回与给定人脸匹配的人的姓名。
# This function returns the name of the person whose image matches with the given face (or 'Not Found')
# known_faces is a list of face encodings
# names is a list of the names of people (in the same order as the face encodings - to match the name with an encoding)
# face is the face we are looking for
def find_match(known_faces, names, face):
# Call compare_face_encodings to get a list of True/False values indicating whether or not there's a match
matches = compare_face_encodings(known_faces, face)
# Return the name of the first match
count = 0
for match in matches:
if match:
return names[count]
count += 1
# Return not found if no match found
return 'Not Found'
至此,我们拥有了运行程序所需的函数。是时候编写应用程序的最后一部分了(我将把它分成两个独立的部分)。
部分 4a:获取图像文件夹中所有人脸的人脸编码
# Get path to all the known images
# Filtering on .jpg extension - so this will only work with JPEG images ending with .jpg
image_filenames = filter(lambda x: x.endswith('.jpg'), os.listdir('images/'))
# Sort in alphabetical order
image_filenames = sorted(image_filenames)
# Get full paths to images
paths_to_images = ['images/' + x for x in image_filenames]
# List of face encodings we have
face_encodings = []
# Loop over images to get the encoding one by one
for path_to_image in paths_to_images:
# Get face encodings from the image
face_encodings_in_image = get_face_encodings(path_to_image)
# Make sure there's exactly one face in the image
if len(face_encodings_in_image) != 1:
print("Please change image: " + path_to_image + " - it has " + str(len(face_encodings_in_image)) + " faces; it can only have one")
exit()
# Append the face encoding found in that image to the list of face encodings we have
face_encodings.append(get_face_encodings(path_to_image)[0])
部分 4b:将测试文件夹中的每张图像与已知人脸进行匹配(逐个)
# Get path to all the test images
# Filtering on .jpg extension - so this will only work with JPEG images ending with .jpg
test_filenames = filter(lambda x: x.endswith('.jpg'), os.listdir('test/'))
# Get full paths to test images
paths_to_test_images = ['test/' + x for x in test_filenames]
# Get list of names of people by eliminating the .JPG extension from image filenames
names = [x[:-4] for x in image_filenames]
# Iterate over test images to find match one by one
for path_to_image in paths_to_test_images:
# Get face encodings from the test image
face_encodings_in_image = get_face_encodings(path_to_image)
# Make sure there's exactly one face in the image
if len(face_encodings_in_image) != 1:
print("Please change image: " + path_to_image + " - it has " + str(len(face_encodings_in_image)) + " faces; it can only have one")
exit()
# Find match for the face encoding found in this test image
match = find_match(face_encodings, names, face_encodings_in_image[0])
# Print the path of test image and the corresponding match
print(path_to_image, match)
就是这样!一旦您将第 1 部分到第 4b 部分的所有代码复制粘贴到 recognize.py 文件中(一个接一个——按照我编写它们的顺序),您应该能够使用您的终端(Mac OS 或 Linux)或命令提示符(Windows)通过键入这些命令来运行它(用您的项目文件夹的完整路径替换 {PROJECT_FOLDER_PATH} ;对我来说是/用户/陶斯/人脸识别):
cd **{PROJECT_FOLDER_PATH}
**python recognize.py
这将为您提供类似如下的输出:
('test/1.jpg', 'Motasim')
('test/2.jpg', 'Not Found')
('test/3.jpg', 'Taus')
('test/4.jpg', 'Sania')
('test/5.jpg', 'Mubin')
文件名旁边的名称显示与给定人脸匹配的人的姓名。请注意,这可能并不适用于所有图像。为了获得此代码的最佳性能,请尝试使用人脸清晰可见的图像。当然,还有其他方法可以使它准确(比如通过实际修改我们的代码来检查多个图像或者使用抖动,等等)。)但这只是为了让你对人脸识别的工作原理有一个基本的了解。
这篇文章的灵感来自于亚当·盖特基(Adam Geitgey)的博客文章(T3)和(T4)关于人脸识别的 Github 报告(T5)。此外,我们正在使用 dlib 网站上提供的一些预先训练好的模型——所以向他们致敬,让他们可以公开访问。我的主要目标是介绍和解释人脸识别的基本深度学习解决方案。当然,有更简单的方法来做同样的事情,但我认为我应该使用 dlib 来一部分一部分地(详细地)做这件事,这样你就能真正理解不同的运动部件。还有其他运行人脸识别的方法(非深度学习),请随意研究它们。这种方法很酷的一点是,你可以用每个人/对象的一两张图像来运行它(假设该模型在实际区分两张脸方面做得很好)。
不管怎样,我希望你喜欢这篇文章。请随意发表评论。
用深度学习理解面部识别
原文:https://blog.paperspace.com/facial-recognition-with-deep-learning/
在这篇文章中,我们将以一种简单的方式来分解深度学习的面部识别,以便您理解。阅读本文后,您将能够:
- 将面部识别和深度学习联系起来
- 定义并解释面部识别的工作原理
- 用 Python 写一个函数,实际看看人脸识别是如何工作的。
注意:本文假设您已经掌握了 python 编码的基本知识
介绍
面部识别已经发展成为人类识别技术中最合适和最合理的技术。在所有像签名、手形图、声音识别和语音这样的技术中,面部识别由于其无接触的特性而被优先使用。使我们能够使用面部识别的新应用程序基于深度学习技术。
那样的话,我们就要明白什么是深度学习了。你听说过人工智能吗?这不是我们今天的话题,但是当你想到“人工”这个词的时候,人工智能是通过计算机应用模拟人类智能。
深度学习在人工智能下运行。因此,它T5 是人工智能的一个元素,它模拟人脑的数据分析过程和模式创建,以便做出最终决定。
面部识别的定义。
面部识别是一种通过从图片、视频镜头或实时分析人脸来识别人类的技术。
直到最近,面部识别一直是计算机视觉的一个问题。深度学习技术的引入能够掌握大数据人脸,并分析丰富而复杂的人脸图像,这使得这项新技术变得更容易,使这项新技术变得高效,后来在面部识别方面甚至比人类视觉更好。
面部识别是如何工作的
我们将用于面部识别的程序很容易执行。我们的目标是开发一个神经网络,它给出一组代表人脸的数字,这些数字被称为人脸编码。
让我们说,你有同一个人的几个图像,神经网络应该为这两个图像产生相关的输出,以显示它们是同一个人。
或者,当你有两个人的图像时,神经网络应该产生非常独特的结果来显示他们是两个不同的人。
因此,神经网络应该被训练成自动识别人脸,并根据差异和相似性计算数字。
这个教程应该给我们一个面部识别是如何工作的基本概念。我们将使用以下步骤:
- 使用来自 dlib 的训练模型来识别图像中的人脸。
- 为识别的面部测量面部布局。
- 使用步骤 1 和 2 的输出计算人脸编码。
- 比较已识别人脸和未识别人脸的编码。
现在,让我们开始建设。
在开始之前,您需要在同一个项目文件夹中包含以下内容。我为这个项目创建了一个文件夹,命名为人脸识别
- 获取 JPEG 格式的图片。确保所有图像只有一张脸,并且不是集体照。在人脸识别下创建另一个文件夹,并重命名文件夹图片。
- 获取同一个人的多张照片,将它们放在人脸识别下的一个新文件夹中,并将文件夹重命名为 test。图像中相同人物的不同图像将包含在该文件夹中
- 确保你已经安装了 Python 2.7 和 pip 。 Python 2.7 和 pip 可以使用 Anaconda 2 这里 ( 一个 Python 发行版)安装。
- 安装完 Python 2.7 和 pip 之后,使用下面的命令在终端中安装 dlib 。
窗户
pip install --user numpy imageio dlib
Mac、Linux
sudo pip install --user numpy imageio dlib
****
纸空间梯度
运行提供的笔记本中的第一个单元以完成安装。在笔记本创建过程中,您可以通过在高级选项>工作区 URL 字段中放置这个 Github URL 来创建自己的笔记本,或者通过将提供的笔记本分支到您自己的渐变团队空间。
注意:无论您的平台是什么,您都可以将 repo 克隆到您的计算机上以快速完成设置。如果您这样设置,请确保仍然运行笔记本中的第一个单元。
- 现在是时候下载面部识别的现有训练模型了。你需要两个模型。第一个模型预测人脸的布局和位置,而第二个模型分析人脸并产生人脸编码。使用以下步骤下载您需要的文件。
- 下载 dlib _ face _ recognition _ resnet _ model _ v1 . bz2这里和 shape _ predictor _ 68 _ face _ landmarks . dat . bz2这里。(使用 wget 进行渐变)
- 下载成功后,您将从 zip 文件夹中提取文件。
- 继续将下载的名为 dlib _ face _ recognition _ resnet _ model _ v1 . dat 和 shape _ predictor _ 68 _ face _ landmarks . dat 的文件复制到主项目文件夹中(在我们的例子中,它应该被命名为 face-recognition)。
现在我们已经设置好了一切,是时候开始编码了。这总是有趣的部分,对不对?
**# 编码!
使用您喜欢的文本编辑器打开您的文件夹(在本教程中,我将使用 VS 代码)。
在文件夹下添加一个新文件,并将其命名为 face.py。在这个 face.py 文件中,我们将编写与两个不同的文件夹 images 和 test 中的人脸相匹配的所有代码。您可以从终端执行这个脚本,或者如果您选择了克隆 repo,也可以在笔记本中执行。克隆 repo 将使您能够访问已经制作好的 face.py 文件,以及样本图像。
第一步:配置和开始:在这一部分,我们集成适当的库,并为我们的面部识别建立对象/参数。
import dlib
import imageio
import numpy as np
import os
# Get Face Detector from dlib
# This allows us to detect faces in image
face_detector = dlib.get_frontal_face_detector()
# Get Pose Predictor from dlib
# This allows us to detect landmark points in faces and understand the pose/angle of the face
shape_predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
# Get the face recognition model
# This is what gives us the face encodings (numbers that identify the face of a particular person)
face_recognition_model = dlib.face_recognition_model_v1('dlib_face_recognition_resnet_model_v1.dat')
# This is the tolerance for face comparisons
# The lower the number - the stricter the comparison
# Use a smaller value to avoid hits
# To avoid false negatives (i.e. faces of the same person doesn't match), use higher value
# 0.5-0.6 works well
TOLERANCE = 0.6
步骤 2:从 JPEG 中获取面部编码:我们正在构建代码,该代码将接受图像标题并返回图片的面部编码。
# Using the neural network, this code takes an image and returns its ace encodings
def get_face_encodings(path_to_image):
# Load image using imageio
image = imageio.imread(path_to_image)
# Face detection is done with the use of a face detector
detected_faces = face_detector(image, 1)
# Get the faces' poses/landmarks
# The code that calculates face encodings will take this as an argument # This enables the neural network to provide similar statistics for almost the same people's faces independent of camera angle or face placement in the picture
shapes_faces = [shape_predictor(image, face) for face in detected_faces]
# Compile the face encodings for each face found and return
return [np.array(face_recognition_model.compute_face_descriptor(image, face_pose, 1)) for face_pose in shapes_faces]
第三步:面对面的比较:我们将编写一段代码,将一个特定的人脸编码与一组相似的人脸编码进行比较。它将给出一个布尔值(真/假)数组,指示是否匹配。
# This function takes a list of known faces
def compare_face_encodings(known_faces, face):
# Finds the difference between each known face and the given face (that we are comparing)
# Calculate norm for the differences with each known face
# Return an array with True/Face values based on whether or not a known face matched with the given face
# A match occurs when the (norm) difference between a known face and the given face is less than or equal to the TOLERANCE value
return (np.linalg.norm(known_faces - face, axis=1) <= TOLERANCE)
第四步:寻找匹配:我们将编写一个函数,它采用一些已知的人脸编码、一个人名列表(对应于获得的人脸编码集合)和一个要匹配的人脸。它将使用 3 中的代码来检索人脸与当前人脸相匹配的人的姓名。
# This function returns the name of the person whose image matches with the given face (or 'Not Found')
# known_faces is a list of face encodings
# names is a list of the names of people (in the same order as the face encodings - to match the name with an encoding)
# face is the face we are looking for
def find_match(known_faces, names, face):
# Call compare_face_encodings to get a list of True/False values indicating whether or not there's a match
matches = compare_face_encodings(known_faces, face)
# Return the name of the first match
count = 0
for match in matches:
if match:
return names[count]
count += 1
# Return not found if no match found
return 'Not Found'
步骤 5:获取子文件夹- images 中所有照片的人脸编码
# Get path to all the known images
# Filtering on .jpg extension - so this will only work with JPEG images ending with .jpg
image_filenames = filter(lambda x: x.endswith('.jpg'), os.listdir('images/'))
# Sort in alphabetical order
image_filenames = sorted(image_filenames)
# Get full paths to images
paths_to_images = ['images/' + x for x in image_filenames]
# List of face encodings we have
face_encodings = []
# Loop over images to get the encoding one by one
for path_to_image in paths_to_images:
# Get face encodings from the image
face_encodings_in_image = get_face_encodings(path_to_image)
# Make sure there's exactly one face in the image
if len(face_encodings_in_image) != 1:
print("Please change image: " + path_to_image + " - it has " + str(len(face_encodings_in_image)) + " faces; it can only have one")
exit()
# Append the face encoding found in that image to the list of face encodings we have
face_encodings.append(get_face_encodings(path_to_image)[0])
步骤 6:识别测试子文件夹中每幅图像中的已识别人脸(一个接一个)
# Get path to all the test images
# Filtering on .jpg extension - so this will only work with JPEG images ending with .jpg
test_filenames = filter(lambda x: x.endswith('.jpg'), os.listdir('test/'))
# Get full paths to test images
paths_to_test_images = ['test/' + x for x in test_filenames]
# Get list of names of people by eliminating the .JPG extension from image filenames
names = [x[:-4] for x in image_filenames]
# Iterate over test images to find match one by one
for path_to_image in paths_to_test_images:
# Get face encodings from the test image
face_encodings_in_image = get_face_encodings(path_to_image)
# Make sure there's exactly one face in the image
if len(face_encodings_in_image) != 1:
print("Please change image: " + path_to_image + " - it has " + str(len(face_encodings_in_image)) + " faces; it can only have one")
exit()
# Find match for the face encoding found in this test image
match = find_match(face_encodings, names, face_encodings_in_image[0])
# Print the path of test image and the corresponding match
print(path_to_image, match)
一旦您完成了步骤 1 到 6,您将能够使用您的终端使用下面的命令运行代码。
cd {PROJECT_FOLDER_PATH}
python face.py
您将获得以下输出
('test/1.jpg', 'Sham')
('test/2.jpg', 'Not Found')
('test/3.jpg', 'Traversy')
('test/4.jpg', 'Maura')
('test/5.jpg', 'Mercc')
文件名旁边的名字是与指定人脸匹配的人的名字。请记住,这可能并不适用于所有照片。
现在你已经看到了它是如何完成的,下一步你应该试着利用你自己的照片。使用此代码时,照片中人的脸部完全可见,以获得最佳效果。**
假香蕉:在 Paperspace 上的 HackMIT 检测假新闻
假香蕉——在你滑倒之前检查你的事实。
点击这里查看我们的 Github 回购!
今年在 HackMIT 2017 上,我们的团队 Fake Bananas 利用 Paperspace 的服务器基础设施建立了一个机器学习模型,通过将给定的文章或用户短语与已知的知名和不可替代的新闻来源进行比较,准确区分虚假和合法的新闻。我们的项目进入了前 10 名(更具体的排名没有披露),并获得了最佳数据使用奖和最佳机器学习公益使用奖。我们的项目目前没有公开托管,因为我们的后端服务收费很高,但我们希望通过与这些公司合作制定学生价格来改变这种情况。
文章大纲
动机
我们的目标是试图解决日益严重的假新闻问题,这一问题因社交媒体的广泛使用而加剧。例如,许多人认为社交媒体上的假新闻是导致有争议的 2016 年美国大选结果的一个重要因素。我们想创建一个易于使用的系统来检测用户声明或文章的可信度。
概述
有许多方法可以尝试检测互联网上的虚假或有偏见的新闻。然而,我们觉得我们基于姿态检测的实现提供了最大的灵活性和可靠性,而不必陷入将个人声明标记为真或假的困境。相反,我们的目标是一种更通用的方法,将来自未知来源的文章分类为一般同意或一般不同意已知(高和低)可信度的来源。
此外,我们的实现特别引人注目,因为我们可以接受用户输入作为一篇文章的链接,或者作为任何需要进行事实检查的任意声明(如奥巴马不是美国公民)。这样,我们的程序就像一个寻找事实的搜索引擎,返回相关文章的链接以及文章对该主题的立场(同意/不同意/中立)!我们的程序为用户提供了巨大的研究和发现潜力,以及简单地检查索赔。
我们希望基于 姿态检测 的概念,创建一个易于使用的系统来检测用户声明或文章的可信度。假新闻很难识别。许多“事实”非常复杂,难以核实,或者存在于“真理的连续统一体”上,或者是事实和虚构重叠的复合句。解决这个问题的最好方法不是通过事实核查,而是通过比较有信誉的消息来源对某项声明的看法。
程序管道
- 用户输入类似于“奥巴马不是美国公民”的声明
- 我们的程序将搜索 Event Registry 的数据库,寻找与关键词相关的数千篇文章。
- 我们通过我们自己开发的立场检测机器学习模型来运行这些文章,该模型将确定每篇文章与索赔的相关性及其对索赔的立场。我们确定一篇文章是同意/不同意/中立还是与输入声明无关。
- 然后,我们访问我们不断发展的来源信誉数据库。如果许多有信誉的来源都同意你的说法,那么它可能是真的!
- 然后我们引用我们的来源,这样我们的用户就可以点击阅读更多关于这个话题的内容!
解析输入并获取文章
给定一个用户 URL 或声明,我们使用微软的 Azure Cognitive 和 IBM 的自然语言处理来解析文章或声明,并执行关键字提取。然后,我们使用关键字的组合从 Event Registry 的数据库中收集了多达几千篇文章,以传递给机器学习模型。在这里,我们倾向于收集更多而不是更少的文章,因为机器学习将准确地确定管道中进一步的相关性。
在梳理了大量报纸和自然语言处理 API 之后,我们发现查找相关文章的最佳方式是通过搜索关键字。面临的挑战是实现一种自然语言处理算法,提取最相关的可搜索关键词,并提取正确数量的关键词。许多算法只是简单的总结,会返回超过 50 个关键词,这对于搜索来说太多了。最重要的是,许多算法是资源耗尽的,有时需要一分钟来解析一个给定的文本。
最后,我们实现了微软的 Azure 和 IBM 的 Watson 来处理、解析和提取新闻文章或声明的关键字。我们将提取的关键词传递到 Event Registry 的近 2 亿篇文章的令人难以置信的数据库中,以找到尽可能多的相关文章。
随着时间的推移,我们很乐意实现 Event Registry 的数据可视化功能,包括生成标签云和图表,显示给定主题的顶级新闻发布者。
-由亨利·韩带头
姿态检测的机器学习
观看我们制作的视频(约 4 分钟)https://www.youtube.com/watch?v=j0n-0-3XhWc中我们机器学习管道的详细纲要。
我们在 Tensorflow 中创建和实现了一个机器学习模型,该模型基于姿势检测领域的几篇研究论文[1][2][3]。我们的模型使用词袋、Google 的 word-2-vec、TF、TF-IDF(术语频率、逆文档频率)和 Scikit-learn 中的“停用词”的组合来对我们的输入进行矢量化。其通过具有 ReLU 激活的单个隐藏层、完全连接的层和 softmax 激活功能运行,以产生 4 个输出之一。我们正在比较一个任意的正文和一个任意的声明。所以我们的 ML 输出,不管我们的正文是否与声明“相关”。如果它是相关的,那么它输出身体是否“同意”、“不同意”或“对我们的主张保持中立”。我们的模型在我们的测试数据上实现了 82%的准确度(对于纯姿态检测..不一定‘假新闻’检测)[4]。
我们面临的一个挑战是 Tensorflow 会话的缓慢加载时间(大约 30 秒)。
-由卡斯坦·戴带头
其他(更糟糕的)假新闻检测方法
1.假新闻风格检测
一些团队试图在“假”文章集和“真”文章集上训练机器学习模型。这种方法很可怕,因为假新闻可以出现在写得好的文章里,反之亦然!风格不等于内容,我们关心的是找到真正的内容。
2.事实核查
一些团队试图细致地检查文章中每个事实的真实性。这很有趣,最终可能成为未来一些假新闻检测系统的一部分,但今天这种方法不可行。事实的真相存在于一个连续体上,并严重依赖于单个单词及其内涵的细微差别。人类语言的细微差别很难解析为真/假二分法。
人类的语言是微妙的。判断一个陈述是对还是错
没有什么是对还是错的数据库
一篇文章中的许多事实存在于真理光谱的各个方面——那篇文章是对还是错?
Paperspace 的 GPU
我们选择使用 paperspace 来训练和运行我们的机器学习模型,主要是因为启动和运行 paperspace 机器进行机器学习是多么快速和容易。这个项目是在 24 小时的期限内为 HackMIT 完成的。因此,速度是至关重要的,在尝试启动和运行 TensorFlow 模型时,“盒子里的 MLL”预设为我们节省了大量时间。
我喜欢 Paperspace,因为它:
- 快速设置
- 机器中的 ML 消除了设置 CUDA 的噩梦。
- 首次设置计算机时,图形模式非常有用
- 使用方便
- 使用 Windows 应用程序,访问终端非常简单
- 大量廉价的虚拟内存
- 我们的模型需要大约 12 GB 的 V-RAM,这使得 V-RAM 的数量成为我们选择 GPU 时最大的限制因素。
- 在发表这篇文章的时候,他们比竞争对手(AWS,Azure)便宜很多,因为他们有更多的 V-ram 和更快的卡。
来源信誉数据库
为了让我们的应用程序能够工作,我们需要能够将新的立场与我们不断改进的来源信誉数据库进行比较。我们编写了一个 python 脚本来跟踪所有遇到的源以及计算权重的信誉分数。一开始,我们根据全国性的研究对声誉进行硬编码,然后每次运行我们的算法时,我们都会将任何新遇到的来源添加到我们的数据库中。为了做到这一点,我们通过将每篇新文章对输入声明的立场与具有已知声誉的来源的立场进行比较,并对结果进行平均,来计算每篇新文章的声誉得分。在未来,我们希望结合更精确的数据科学技术来改进我们的数据库。作为一个较小的项目,我们还希望找到一种比用 csv 跟踪数据库更简化的方法,即在应用程序的单次运行之外存在一个数据库副本。由乔希·弗里尔带头
网络应用架构
我们使用 ReactJS 作为前端,Flask dev 服务器作为后端。当 Flask 服务器在特定端点接收到 POST 请求时,它使用用户的 URL 或声明的参数启动我们的抓取和机器学习预测脚本。我们的依赖项包括两个开源库:Semantic-Ui-React 和 SweetAlerts。我们使用脸书的“创建-反应-应用”工具来创建我们的 web 应用程序的结构。
-由杰森·金打头阵
团队成员
我们现在都是斯沃斯莫尔学院的索菲莫尔。
参考文献
[1]伦敦大学学院关于这个题目的简短论文:
@article{riedel2017fnc,
author = {Benjamin Riedel and Isabelle Augenstein and George Spithourakis and Sebastian Riedel},
title = {A simple but tough-to-beat baseline for the {F}ake {N}ews {C}hallenge stance detection task},
journal = {CoRR},
volume = {abs/1707.03264},
year = {2017},
url = {http://arxiv.org/abs/1707.03264}
}
[2]思科-塔罗斯在假新闻挑战上的工作
https://github.com/Cisco-Talos/fnc-1
[3]雅典假新闻挑战小组。
他们的 Github 回购
他们的博客文章
他们的技术论文
[4]我们的训练数据来自 FakeNewsChallenge.org,在 2016 年,各团队竞相开发姿态检测模型,并且仅开发姿态检测模型,我们以此为基础来进行假新闻检测。
2020 年全新 Fast.ai 课程
大约三年前,我们首次与杰瑞米·霍华德和 T2 的 Fast.ai 团队合作,推出了最初的课程。
如果你还不熟悉的话,Fast.ai 是一个非营利组织,它可能是 MOOC 推出的最受欢迎、最成功、最有效的机器学习项目——面向程序员的实用深度学习。
我们很高兴地宣布,Fast.ai 团队已经推出了 2020 年的更新课程,Gradient 再次成为这个新的改进课程的推荐平台之一。
Fast.ai 建议使用 Paperspace Gradient 来实现强大的可配置 Jupyter 笔记本环境。正如课程描述中提到的,“ Gradient 是一个“真正的”Jupyter 笔记本,因此课程中的所有内容都有效。它还提供空间来保存您的笔记本电脑和模型。
除了提供全功能的 Jupyter 笔记本环境,Gradient 还解决了其他流行的云笔记本在不同来源的数据管理和接收以及跨会话持久化笔记本方面存在的一些问题。
多年来,我们已经支持了数以万计的 Fast.ai 学生掌握深度学习——我们自豪地支持每年许多学生的成功,他们发现梯度在他们的 ML 旅程中有所帮助。
我们也很高兴继续为 Fast.ai 学生(以及任何对机器学习和深度学习感兴趣的人)提供免费的 GPU。
额外奖励:如果我们的免费机器容量不足,您可以在年底前使用促销代码 FASTAIGR20 获得 15 美元的免费梯度积分。️
我们希望您能抽出时间来看看新课程。我们会在这里支持你的 Fast.ai 之旅!
一如既往地感谢 Paperspace 团队。♥️
如何在 Paperspace 上启动并运行 fast.ai:
1. If you haven't already, select Gradient from the Paperspace console home screen
2. Select Notebooks from the Gradient sidebar
3. Select Create Notebook
4. Select the fast.ai container. This is pre-loaded with the latest fast.ai software.
5. Select a machine type on which to run the notebook
6. Give your new notebook a name
7. Select Create Notebook to begin notebook provisioning
8. Notebook will take a few minutes to provision and load
9. Once notebook is ready to run, you will be able to press Open
10. Run your notebook in the Paperspace Jupyter notebook environment!
Fast.ai 实用深度学习 for Coders v5 发布!
原文:https://blog.paperspace.com/fast-ai-practical-deep-learning-for-coders-new-v5-2022-release/
Fast.ai 的团队刚刚发布了非常受欢迎的程序员实用深度学习课程的最新版本,我们很高兴 Gradient Notebooks 再次成为该课程的官方推荐云笔记本!
感谢您为我们的学生提供了一次绝佳的体验!@ hello perspace是我们在课程网站上推荐用于完成课程的仅有的两种笔记本服务之一。
— Jeremy Howard (@jeremyphoward) July 22, 2022
快速跟踪部署
遵循本指南,了解如何使用梯度部署通过 FastAPI 部署我们的模型。读者应该期望学习如何将他们训练好的模型作为一个渐变模型工件上传,创建一个 Docker 图像来服务他们的模型,并使用一个部署在渐变上部署他们的图像。
重要提示:在本教程的最后,我们将完成一个渐变部署。请确保通过更改
replicas: 0
或向规范添加enabled: false
标志或完全删除部署来关闭它。否则,只要部署正在运行,所有者的帐户将被收取计算费用。通过检查 Paperspace 控制台中部署页面顶部的状态,我们可以确认我们的部署处于离线状态。
前言
本指南的目标是向用户展示如何获取他们自己的训练模型,并通过提供一个端点将它们部署到梯度部署上,用户可以发送请求以返回从他们的模型生成的输出。为此,我们将使用 FastAPI 。FastAPI 是一个现代化的高性能 web 框架,用于构建易于使用的 API,非常适合创建应用程序来为模型提供服务。
在下面的教程中,我们将创建一个 FastAPI 应用程序,通过提供一个端点来服务我们的模型,我们的模型可以从这个端点被调用。这个端点将接受一个 API 请求,生成一个模型输出,并将该输出返回给发送方。我们将创建一个包含 FastAPI 应用程序的 Docker 映像,然后将该映像与模型一起部署在 Gradient 上,以允许用户访问我们的模型。
先决条件
作为本教程的起点,我们需要:
- 受过训练的模特
- FastAPI 应用程序文件
本教程并不打算深入研究 FastAPI 开发,而是指导如何在 Gradient 上部署 FastAPI 应用程序来服务我们的模型。但是,我们将在下面突出显示一些我们在这个演示应用程序中使用的文件和代码片段。我们还可以查看我们在这个项目中使用的 GitHub 库,其中包括我们在这个 FastAPI 应用程序中使用的所有文件、other 文件以及任何其他附带文件。
在高层次上,FastAPI 应用程序(在 main.py 中)设置了以下进程:
- 为模型预测函数建立端点
- 检查要接收的 API 请求
- 输入附加的图像文件
- 通过将图像转换为适合模型输入的张量来处理图像
- 将图像传递到模型中,并捕获分类预测
- 将预测返回给发送方
我们可以在下面看到 FastAPI 应用程序的一个片段。
# Library import and model setup
# ...
app = FastAPI()
@app.get("/predict")
async def predict(image: bytes = File()):
# Transform input image into tensor
tensor = imgToTensor(image)
# Generate model output
model.eval()
with torch.inference_mode():
output = model(tensor)
# Convert model output to prediction
_, predicted = torch.max(output.data, 1)
prediction = prediction_classes[predicted]
# Return prediction
return {"prediction": prediction}
这个特定项目的目标是部署一个图像分类模型( ResNet ),该模型可以接收包含图像的 API 请求,预测图像属于哪个类,并返回该预测。下面简单描述一下 GitHub 仓库中的主要文件。
- main . py——构建并定义 FastAPI 应用程序
- preprocess . py——包含将图像从 API 请求转换成模型的适当格式的过程
- resnet.py -包括创建 resnet 模型对象的函数
- 配置——包含模型和应用程序的配置
- requirements.txt -应用程序的 Python 库要求
注意:requirements . txt 文件包含运行模型推理所需的库。当我们在下面的步骤中构建 Docker 映像时,我们将从已经包含 Python 3.9、适当的 CUDA 驱动程序和运行 FastAPI 的包的基础映像中导入。然而,我们的 requirements.txt 文件将需要包含任何 ML 框架或其他所需的 Python 库(例如 PyTorch、TensorFlow、NumPy)。
一旦我们满足了上面的需求,我们就可以按照下面的步骤让我们的应用程序在 Gradient 上运行了。
辅导的
入门指南
让我们的模型在 Gradient 上的 FastAPI 应用程序上运行有三个主要步骤。步骤如下:
- 将我们的模型作为渐变模型工件上传
- 为我们的应用程序创建一个 Docker 图像
- 创建并运行我们的渐变部署
假设我们已经满足了上面的先决条件,我们需要做的第一件事就是上传我们的模型作为一个渐变模型工件。让我们从下面使用两种不同的方法来看这是如何工作的开始。
上传我们的模型
有两种方法可以将我们的模型文件作为渐变模型工件上传。上传我们模型的第一种方式是通过 Paperspace 控制台,在那里 23 可以从我们的机器上选择一个本地文件。第二种方式是通过渐变 CLI。我们将在下面强调上传模型的两种方式。
通过控制台上传模型
要通过 Paperspace 控制台上传我们的模型,请确保我们的模型文件位于我们的本地机器上。接下来,导航到我们的渐变项目,进入模型选项卡,并点击上传模型。将出现一个弹出窗口,允许我们命名模型并选择要上传的文件。其他配置如下图所示。
注意:对 PyTorch/PopTorch 型号使用“自定义”型号类型。
Uploading the model through the Paperspace Console
通过 Gradient CLI 上传模型
要使用 Gradient CLI 上传我们的模型,我们首先需要确保我们的模型文件位于安装了Gradient CLI的环境中。在这种情况下,我们将假设模型文件已经过训练,并且位于安装了 CLI 的渐变笔记本中。
注意:关于 CLI 命令的更多信息可以在我们的文档中找到这里。
我们需要采取的第一步是找到我们的渐变 API 键。我们将使用它通过 CLI 登录我们的帐户。为了找到我们的 API 密钥,我们可以参考这个文档。
一旦我们有了 API 密钥,我们就可以通过使用我们环境的终端中提供的 CLI 命令来执行下面的步骤。
- 使用我们的 API 密钥登录我们的梯度帐户
gradient apiKey "your-api-key"
- 创建一个数据集,作为梯度模型工件的参考。
gradient datasets create \
--name "dataset-name" \
--storageProviderId "storage-provider"
注意:要获取渐变存储提供商 ID,请导航至控制台右上角的我们的个人资料,进入团队设置,进入存储,并在页面底部找到存储提供商 ID。该值应该类似于:
splvqdtq9ofuxui
。下面是一个例子。
Retrieving the Storage Provider ID
- 将我们的模型文件上传到我们创建的数据集。运行上述命令后,将输出数据集 ID。
gradient datasets versions create \
--id "your-dataset-id" \
--source-path "./local/path/to/model-file"
- 使用我们创建的数据集作为参考,创建一个渐变模型工件。
gradient models create \
--name "model-name" \
--modelType "Custom" \
--datasetRef "your-dataset-id"
太好了!我们现在已经创建了渐变模型对象。让我们继续为我们的部署创建 Docker 映像。
创建 Docker 图像
接下来,我们需要为我们的部署构建 Docker 映像。这个映像将包括我们的 FastAPI 模型服务运行的任何应用程序文件以及运行的环境。当我们浏览下面的细节时,请随意参考包含所有讨论文件的 GitHub 库。
设置
下面是我们用来创建演示应用程序映像的模板 Dockerfile 文件。此 docker 文件使用 Paperspace 托管的 FastAPI 部署映像作为其基础层,复制应用程序文件,并运行适当的设置命令。每个命令的详细信息如下。
FROM paperspace/fastapi-deployment:latest
WORKDIR /app
COPY main.py preprocess.py resnet.py requirements.txt ./
COPY config ./config
RUN pip3 install -U pip && pip3 install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
每个 Dockerfile 文件步骤的简要描述:
FROM: 设置基础图像以在其上构建我们的图像
工作目录:设置其他 Dockerfile 指令的工作目录
复制:将运行 FastAPI 应用程序所需的本地存储文件复制到镜像中的工作目录(即/app
)
运行:运行 pip 更新并在 requirements.txt 上安装,以安装应用程序需求
CMD: 命令,启动运行 FastAPI 应用程序的 uvicorn 服务器
正如本教程开始时提到的,基本 Docker 映像paperspace/fastapi-deployment
包含 Python 3.9、所需的 CUDA 驱动程序和运行 FastAPI 应用程序所需的库。所有其他要求都需要包含在我们的 requirements.txt 文件中。
我们可以使用上面的模板,并根据我们的目的对其进行修改。可能需要进行一些更改:
- Requirements.txt——确保我们的 requirements . txt 具有适合我们的应用程序的 python 库需求。我们的文件将需要包含任何 ML 框架或所需的其他 Python 库(例如 PyTorch、NumPy)。
- Dockerfile COPY 命令——我们将在这里列出运行 FastAPI 应用程序所需的特定于应用程序的文件。
一旦我们完成了上面的更改,但在构建 Docker 映像之前,我们需要创建一个存储库来托管我们的 Docker 映像。我们建议使用 Docker Hub 作为容器注册中心。任何人都可以注册一个免费的 Docker Hub 账户来利用这项有用的服务。
创建 Docker Hub 帐户后,在 Docker Hub 中创建一个公共存储库。例如,我们将我们的存储库命名为resnet-fastapi
,它将在paperspace/resnet-fastapi
托管在 Docker Hub 上。
我们现在准备建立我们的 Docker 形象。为此,我们需要在安装了 Docker 的环境中安装代码文件,包括 Docker 文件。我们建议使用带有 ML-in-a-Box 模板的 Paperspace 核心机。该机器将拥有构建 Docker 映像所需的所有软件包,并允许用户利用 Paperspace 机器的极快下载和上传速度。关于如何设置的更多说明在这里。
我们还建议创建一个 GitHub 存储库来存储项目,包括应用程序文件、Dockerfile 和任何其他附带文件。一旦我们有了一个存储项目文件的 GitHub 存储库,那么我们就可以将我们的存储库克隆到我们的本地或核心机器上,这样所有需要的文件都在同一个目录中。
Docker 映像构建
一旦我们的 Docker 文件被创建,并且我们有了 Docker Hub 存储库,我们就可以构建我们的 Docker 映像了。为此,我们克隆我们的项目 GitHub 库,或者将必要的文件移动到我们选择的机器上。
要从我们的 Docker 文件构建 Docker 映像,请使用我们的 Docker Hub 帐户和映像名称运行以下命令。请注意,我们使用了 Paperspace 名称空间,并将我们的图像命名为resnet-fastapi
(演示将有所不同),并添加了一个可选的标签latest
。
nvidia-docker build -t paperspace/resnet-fastapi:latest
注意:我们可以在这两个命令中用docker
替换nvidia-docker
,但是建议使用 nvidia-docker,因为它是 docker 命令的包装器,提供了一个容器,其中包含在 GPU 上执行代码所需的组件。
一旦我们的 Docker 映像构建完成,我们可以使用下面的命令将它推送到我们的 Docker Hub 存储库。
nvidia-docker push paperspace/resnet-fastapi:latest
恭喜你。我们的图像现在在 Docker Hub 上。我们现在准备在 Gradient 上部署应用程序。
部署 FastAPI 应用程序
在 Gradient 上部署我们的 FastAPI 应用程序有两种方式。第一种是使用图纸空间控制台中的模型构建器界面。另一种方法是使用渐变 CLI。我们将在下面展示这两种方法。
通过模型构建器部署应用
首先,我们导航到渐变项目中的 Deployments 选项卡,并单击“Create”。这将打开一个模型构建器表单,我们可以使用它来指定部署的配置。下面是我们选择的配置的屏幕截图。下面是每个配置的更多细节。
Deployment builder configurations
配置选项
部署名称:我们的部署所需的名称
选择一台机器:我们希望在其上运行部署的机器类型
副本:我们的图像要旋转起来的容器的数量。如果我们将副本设置为多于 1 个,那么会有一个内置的负载平衡器,它会根据每个实例的当前利用率将传入的请求定向到我们的容器。将副本设置为 0 将禁用部署
Image: 我们想要旋转的图像的名称空间和标签
Port: 运行我们的应用程序的本地容器端口。如果我们使用上面的 Dockerfile 模板,那么我们的 FastAPI 应用程序应该在端口 80 上运行
环境变量:(可选)我们在应用程序文件中使用这些变量来加载模型,这样应用程序本身就不知道哪个模型文件被加载了,或者它被挂载到了哪个路径。更多细节请见下文
添加模型:下拉菜单选择我们在本教程第一步上传的模型
为了更好地解释我们如何使用上面设置的环境变量,看一下我们在应用程序中用来将 PyTorch 模型加载到 GPU 上的模型设置函数。
def model_setup():
MODEL_DIR = os.getenv('MODEL_DIR')
MODEL_FILE = os.getenv('MODEL_FILE')
MODEL_NAME = os.getenv('MODEL_NAME')
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILE)
model_dict = model_dict = {'resnet18': resnet18(3,10)
,'resnet34': resnet34(3,10)
,'resnet50': resnet50(3,10)
,'resnet101': resnet101(3,10)
,'resnet152': resnet152(3,10)
}
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model_dict[MODEL_NAME]
model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
return model
请注意,通过使用环境变量和模型字典,我们可以在该应用程序中使用相同的 Python 代码,然后使用模型构建器配置来加载不同的 ResNet 模型,并保持图像不变。这为我提供了一些额外的灵活性,但不是必需的。
设置好上述配置后,单击屏幕底部的“部署”按钮。我们最初会看到我们的部署状态设置为 offline,但是随着部署的构建和开始扩展,然后准备好为请求提供服务,我们会看到状态发生变化。根据图像和模型的大小,这可能需要一些时间,但我们应该在一两分钟内看到变化。
Gradient Deployment object status
通过 CLI 进行梯度部署
部署 FastAPI 应用的另一种方式是通过 Gradient CLI 。创建部署规范的过程是相同的,但是在这种情况下,我们将创建一个保存我们的部署规范的.yaml
文件,然后通过 CLI 发送它来创建我们的部署。在项目资源库中可以找到部署规范 yaml 文件。
enabled: true
image: paperspace/resnet-fastapi:latest
port: 80
models:
- id: mo6sz1arw1oyp3
path: /opt/mo6sz1arw1oyp3
env:
- name: MODEL_NAME
value: resnet50
- name: MODEL_FILE
value: resnet50.pt
- name: MODEL_DIR
value: /opt/mo6sz1arw1oyp3
resources:
replicas: 1
instanceType: A4000
正如我们将注意到的,这些设置与上面在部署构建器 UI 中显示的设置相同,但是在一个 yaml 文件中。
为了创建我们的部署,在我们的 deployment-spec.yaml 文件的位置打开一个终端,确保我们使用 Gradient API 密钥登录,并运行下面的命令。
gradient deployments create \
--name "resnet-fastapi" \
--projectId "your-project-id" \
--spec "deployment-spec.yaml"
注意:我们可以通过在 Paperspace 控制台中导航到我们的项目并复制屏幕左上角的值来找到我们的项目 ID,如下所示。
Get our Project ID
满足请求
一旦我们的部署启动,并且状态显示为 Ready,就可以为请求提供服务了。下面我们使用 Postman 向我们的应用程序端点和正确的路径/predict
发送一个 GET 请求。我们可以在 Paperspace 控制台中的值Endpoint
下找到我们的部署端点。在本例中,我们附加了一个猫的文件,作为发送到部署模型的请求的一部分。在我们发送请求之后,我们依次接收该图像的预测分类。
关闭我们的部署
当我们使用完我们的部署时,请确保通过更改replicas: 0
或向规范添加enabled: false
标志或完全删除部署来关闭它。否则,只要我们的部署还在运行,我们就需要支付计算费用。通过检查 Paperspace 控制台中部署页面顶部的状态,我们可以确认我们的部署处于离线状态。
结论
太好了!现在我们应该能够使用我们的深度学习模型,并使用 FastAPI 框架在 Gradient 上部署它。请记住,FastAPI 是一个轻量级的、但是高性能的、用于构建 API 的 web 框架,并且是一个很好的方式来轻松地部署我们在 Gradient 上开发的模型。
同样,上面 FastAPI 部署教程中讨论的相关项目文件可以在这个 GitHub 资源库中找到。
采用 Cython 的 NumPy 阵列处理:速度提高 1250 倍
原文:https://blog.paperspace.com/faster-numpy-array-processing-ndarray-cython/
本教程将向您展示如何使用 Cython 加速 NumPy 数组的处理。通过在 Python 中显式指定变量的数据类型,Cython 可以在运行时大幅提高速度。
本教程涵盖的部分如下:
- 遍历 NumPy 数组
- NumPy 数组的 Cython 类型
- NumPy 数组元素的数据类型
- NumPy 数组作为函数参数
- 对 NumPy 数组进行索引,而不是迭代
- 禁用边界检查和负索引
- 摘要
关于 Cython 及其使用方法的介绍,请查看我在上发表的使用 Cython 提升 Python 脚本的文章。要不,我们开始吧!
遍历 NumPy 数组
我们将从与上一教程相同的代码开始,除了这里我们将遍历一个 NumPy 数组而不是一个列表。NumPy 数组是使用 arrange()函数在 arr 变量中创建的,该函数从 0 开始以 1 为步长返回 10 亿个数字。
import time
import numpy
total = 0
arr = numpy.arange(1000000000)
t1 = time.time()
for k in arr:
total = total + k
print("Total = ", total)
t2 = time.time()
t = t2 - t1
print("%.20f" % t)
我在一台配有 Core i7-6500U CPU @ 2.5 GHz 和 16 GB DDR3 RAM 的机器上运行这个程序。Python 代码在 458 秒(7.63 分钟)内完成。太长了。
让我们看看编辑在之前的教程中创建的 Cython 脚本后需要多长时间才能完成,如下所示。唯一的变化是在循环的中包含了 NumPy 数组。请注意,在使用 Cython 脚本之前,您必须使用下面的命令来重新构建它。
python setup.py build_ext --inplace
当前形式的 Cython 脚本在 128 秒(2.13 分钟)内完成。仍然很长,但这是一个开始。让我们看看如何让它更快。
NumPy 数组的 Cython 类型
之前我们看到,在为所使用的变量显式定义 C 类型后,Cython 代码运行得非常快。NumPy 数组也是这种情况。如果我们保留 NumPy 数组的当前形式,Cython 的工作方式与常规 Python 完全一样,为数组中的每个数字创建一个对象。为了让事情运行得更快,我们还需要为 NumPy 数组定义一个 C 数据类型,就像为任何其他变量一样。
NumPy 数组的数据类型为 ndarray ,代表 n 维数组。如果您使用关键字 int 来创建整数类型的变量,那么您可以使用 ndarray 来创建 NumPy 数组的变量。注意,必须使用 NumPy 调用n array,因为n array在 NumPy 内部。因此,创建 NumPy 数组变量的语法是 numpy.ndarray 。下面列出的代码创建了一个名为 arr 的变量,数据类型为 NumPy ndarray 。
首先要注意的是,NumPy 是使用第二行中的常规关键字 import 导入的。在第三行,您可能会注意到 NumPy 也是使用关键字 cimport 导入的。
现在我们来看看 Cython 文件可以分为两类:
- 定义文件(。pxd)
- 实现文件(。pyx)
定义文件的扩展名为。pxd,用于保存 C 声明,例如要导入并在其他 Cython 文件中使用的数据类型。另一个文件是带有扩展名的实现文件。pyx,我们目前用它来编写 Cython 代码。在这个文件中,我们可以导入一个定义文件来使用其中声明的内容。
下面的代码将被写入扩展名为. pyx 的实现文件中。 cimport numpy 语句在 Cython 中导入一个名为“numpy”的定义文件。这样做是因为 cy thon“numpy”文件具有处理 NumPy 数组的数据类型。
下面的代码定义了前面讨论的变量,分别是 maxval 、 total 、 k 、 t1 、 t2 和 t 。有一个名为 arr 的新变量保存数组,数据类型numpy.ndarray
。之前使用了两个导入语句,即import numpy
和cimport numpy
。这里哪一个是相关的?这里我们将使用需要的cimport numpy
,而不是常规的import
。这让我们可以访问在 Cython numpy 定义文件中声明的 numpy.ndarray 类型,因此我们可以将 arr 变量的类型定义为 numpy.ndarray
maxval 变量被设置为等于 NumPy 数组的长度。我们可以从创建一个长度为 10,000 的数组开始,并在以后增加这个数字,以比较 Cython 相对于 Python 的改进。
import time
import numpy
cimport numpy
cdef unsigned long long int maxval
cdef unsigned long long int total
cdef int k
cdef double t1, t2, t
cdef numpy.ndarray arr
maxval = 10000
arr = numpy.arange(maxval)
t1 = time.time()
for k in arr:
total = total + k
print "Total =", total
t2 = time.time()
t = t2 - t1
print("%.20f" % t)
在创建了一个类型为numpy.ndarray
的变量并定义了它的长度之后,接下来是使用numpy.arange()
函数创建数组。注意,这里我们使用的是 Python NumPy,它是使用import numpy
语句导入的。
通过运行上面的代码,Cython 只用了 0.001 秒就完成了。对于 Python,代码耗时 0.003 秒。在这种情况下,Cython 比 Python 快近 3 倍。
当maxsize
变量设置为 100 万时,Cython 代码运行 0.096 秒,而 Python 需要 0.293 秒(Cython 也快了 3 倍)。当处理 1 亿个时,Cython 需要 10.220 秒,而 Python 需要 37.173 秒。对于 10 亿,Cython 需要 120 秒,而 Python 需要 458 秒。尽管如此,Cython 可以做得更好。让我们看看怎么做。
NumPy 数组元素的数据类型
第一个改进与数组的数据类型有关。NumPy 数组arr
的数据类型根据下一行定义。注意,我们所做的只是定义数组的类型,但是我们可以给 Cython 更多的信息来简化事情。
请注意,没有任何东西可以警告您有一部分代码需要优化。一切都会起作用;你必须调查你的代码,找出可以优化的部分,以便运行得更快。
cdef numpy.ndarray arr
除了定义数组的数据类型,我们还可以定义两条信息:
- 数组元素的数据类型
- 维度数量
数组元素的数据类型是int
,根据下面的代码行定义。使用 cimport 导入的 numpy 有一个对应于 NumPy 中每种类型的类型,但在末尾有 _t 。比如常规 NumPy 中的 int 对应 Cython 中的 int_t 。
参数是ndim
,它指定了数组中的维数。这里设置为 1。请注意,它的默认值也是 1,因此可以从我们的示例中省略。如果使用了更多的维度,我们必须指定它。
cdef numpy.ndarray[numpy.int_t, ndim=1] arr
不幸的是,只有当 NumPy 数组是函数中的一个参数或者函数中的一个局部变量时——而不是在脚本体中——才允许这样定义 NumPy 数组的类型。我希望 Cython 尽快解决这个问题。我们现在需要编辑前面的代码,将其添加到下一节将要创建的函数中。现在,让我们在定义数组之后创建它。
注意,我们将变量arr
的类型定义为numpy.ndarray
,但是不要忘记这是容器的类型。该容器包含元素,如果没有指定其他内容,这些元素将被转换为对象。为了强制这些元素为整数,根据下一行将dtype
参数设置为numpy.int
。
arr = numpy.arange(maxval, dtype=numpy.int)
这里使用的 numpy 是使用cimport
关键字导入的。一般来说,无论何时发现用于定义变量的关键字 numpy,都要确保它是使用cimport
关键字从 Cython 导入的。
NumPy 数组作为函数参数
准备好数组后,下一步是创建一个函数,该函数接受类型为numpy.ndarray
的变量,如下所示。这个函数被命名为do_calc()
。
import time
import numpy
cimport numpy
ctypedef numpy.int_t DTYPE_t
def do_calc(numpy.ndarray[DTYPE_t, ndim=1] arr):
cdef int maxval
cdef unsigned long long int total
cdef int k
cdef double t1, t2, t
t1 = time.time()
for k in arr:
total = total + k
print "Total = ", total
t2 = time.time()
t = t2 - t1
print("%.20f" % t)
import test_cython
import numpy
arr = numpy.arange(1000000000, dtype=numpy.int)
test_cython.do_calc(arr)
构建完 Cython 脚本后,接下来我们根据下面的代码调用函数do_calc()
。这种情况下的计算时间从 120 秒减少到 98 秒。这使得 Cython 在对 10 亿个数字求和时比 Python 快 5 倍。正如你现在所期望的,对我来说这仍然不够快。我们将在下一节看到另一个加速计算的技巧。
NumPy 数组上的索引与迭代
Cython 只是将计算时间减少了 5 倍,这并不鼓励我使用 Cython。但这不是 Cython 的问题,而是使用的问题。问题在于循环是如何产生的。让我们仔细看看下面给出的循环。
在之前的教程中,提到了非常重要的一点,那就是 Python 只是一个接口。界面只是让用户觉得事情更简单。请注意,简单的方法并不总是做某事的有效方法。
python[接口]有一种迭代数组的方法,这些数组在下面的循环中实现。循环变量 k 在 arr NumPy 数组中循环,从数组中一个接一个地取出元素,然后将该元素赋给变量 k 。以这种方式循环遍历数组是 Python 中引入的一种风格,但它不是 C 用于循环遍历数组的方式。
for k in arr:
total = total + k
对于编程语言来说,循环遍历数组的通常方式是从 0[有时从 1]开始创建索引,直到到达数组中的最后一个索引。每个索引用于索引数组以返回相应的元素。这是循环遍历数组的正常方式。因为 C 不知道如何以 Python 风格遍历数组,所以上面的循环是以 Python 风格执行的,因此执行起来要花很多时间。
为了克服这个问题,我们需要创建一个普通样式的循环,使用索引for
访问数组元素。新的循环实现如下。
首先,有一个名为arr _ shape的新变量用于存储数组中元素的数量。在我们的示例中,只有一个维度,它的长度通过使用索引 0 索引数组形状的结果来返回。**
然后 arr_shape 变量被提供给range()
函数,该函数返回访问数组元素的索引。在这种情况下,变量 k 代表一个索引,而不是一个数组值。
在循环内部,通过索引 k 索引变量 arr 来返回元素。
cdef int arr_shape = arr.shape[0]
for k in range(arr_shape):
total = total + arr[k]
让我们编辑 Cython 脚本来包含上面的循环。下面列出了新脚本。旧循环被注释掉了。
import time
import numpy
cimport numpy
ctypedef numpy.int_t DTYPE_t
def do_calc(numpy.ndarray[DTYPE_t, ndim=1] arr):
cdef int maxval
cdef unsigned long long int total
cdef int k
cdef double t1, t2, t
cdef int arr_shape = arr.shape[0]
t1=time.time()
# for k in arr:
# total = total + k
for k in range(arr_shape):
total = total + arr[k]
print "Total =", total
t2=time.time()
t = t2-t1
print("%.20f" % t)
通过构建 Cython 脚本,在将循环改为使用索引后,对 10 亿个数字求和的计算时间现在大约只有一秒钟。所以,时间从 120 秒减少到仅仅 1 秒。这是我们对 Cython 的期望。
请注意,当我们使用 Python 风格遍历数组时,不会发生任何错误。没有迹象可以帮助我们找出代码没有优化的原因。因此,我们必须仔细寻找代码的每一部分,寻找优化的可能性。
注意,普通 Python 执行上述代码需要 500 多秒,而 Cython 只需要 1 秒左右。因此,对于 10 亿个数的求和,Cython 比 Python 快 500 倍。超级棒。记住,为了减少计算时间,我们牺牲了 Python 的简单性。在我看来,将时间减少 500 倍,值得使用 Cython 优化代码。
代码速度提高 500 倍固然很好,但还有一个改进,这将在下一节讨论。
禁用边界检查和负索引
如 Cython 文档中所述,导致代码变慢的因素有很多,包括:
- 边界检查,以确保索引在数组的范围内。
- 使用负索引访问数组元素。
当 Cython 执行代码时,这两个特性是活动的。可以使用负索引(如-1)来访问数组中的最后一个元素。Cython 还确保没有索引超出范围,如果出现这种情况,代码不会崩溃。如果您不需要这些功能,您可以禁用它以节省更多时间。这是通过添加以下行来实现的。
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
The new code after disabling such features is as follows.
import time
import numpy
cimport numpy
cimport cython
ctypedef numpy.int_t DTYPE_t
@cython.boundscheck(False) # turn off bounds-checking for entire function
@cython.wraparound(False) # turn off negative index wrapping for entire function
def do_calc(numpy.ndarray[DTYPE_t, ndim=1] arr):
cdef int maxval
cdef unsigned long long int total
cdef int k
cdef double t1, t2, t
cdef int arr_shape = arr.shape[0]
t1=time.time()
# for k in arr:
# total = total + k
for k in range(arr_shape):
total = total + arr[k]
print "Total =", total
t2=time.time()
t = t2-t1
print("%.20f" % t)
构建并运行 Cython 脚本后,时间不在 0.4 秒左右。与 Python 脚本的计算时间(大约 500 秒)相比,Cython 现在比 Python 快了大约 1250 倍。
摘要
本教程使用 Cython 来提高 NumPy 数组处理的性能。我们通过四种不同的方式实现了这一目标:
1.定义 NumPy 数组数据类型
我们从使用numpy.ndarray
指定 NumPy 数组的数据类型开始。我们看到这个类型在使用cimport
关键字导入的定义文件中是可用的。
2.指定数组元素的数据类型+维数
仅仅将numpy.ndarray
类型赋给一个变量是一个开始——但这还不够。仍然需要提供两条信息:数组元素的数据类型和数组的维数。两者都对处理时间有很大影响。
只有当 NumPy 数组被定义为函数参数或函数内部的局部变量时,这些细节才会被接受。因此,我们在这些点上添加了 Cython 代码。您还可以指定函数的返回数据类型。
3.使用索引遍历 NumPy 数组
减少处理时间的第三种方法是避免 Pythonic 循环,在这种循环中,一个变量由数组中的值逐个赋值。相反,只需使用索引遍历数组。这导致时间的大量减少。
4.禁用不必要的功能
最后,您可以通过禁用 Cython 中为每个函数默认完成的一些检查来减少一些额外的毫秒数。这些包括“边界检查”和“回绕”禁用这些功能取决于您的具体需求。例如,如果使用负索引,则需要启用回绕功能。
结论
本教程讨论了使用 Cython 操作 NumPy 数组,其速度是 Python 单独处理的 1000 倍以上。减少计算时间的关键是指定变量的数据类型,并对数组进行索引而不是迭代。
在下一篇教程中,我们将通过使用 Cython 来减少遗传算法的 Python 实现的计算时间,从而总结和推进我们迄今为止的知识。
如何使用 NumPy 整形和转置来加速对象检测
原文:https://blog.paperspace.com/faster-object-detection-numpy-reshape-transpose/
这是我们正在进行的 NumPy 优化系列的第 4 部分。在第 1 部分和第 2 部分中,我们介绍了向量化和广播的概念,以及如何应用它们来优化 K-Means 聚类算法的实现。接下来,第 3 部分介绍了一些重要的概念,比如 NumPy 中的步长、整形和转置。在这篇文章的第 4 部分中,我们将讨论如何应用这些概念来加速基于深度学习的物体检测器:YOLO。
下面是前面部分的链接,供您参考。
NumPy Optimization: Vectorization and Broadcasting | Paperspace BlogIn Part 1 of our series on writing efficient code with NumPy we cover why loops are slow in Python, and how to replace them with vectorized code. We also dig deep into how broadcasting works, along with a few practical examples.Paperspace BlogAyoosh KathuriaUsing NumPy to Speed Up K-Means Clustering by 70x | Paperspace BlogIn this part we’ll see how to speed up an implementation of the k-means clustering algorithm by 70x using NumPy. We cover how to use cProfile to find bottlenecks in the code, and how to address them using vectorization.Paperspace BlogAyoosh KathuriaNumPy Internals, Strides, Reshape and Transpose | Paperspace BlogWe cover basic mistakes that can lead to unnecessary copying of data and memory allocation in NumPy. We further cover NumPy internals, strides, reshaping, and transpose in detail.Paperspace BlogAyoosh Kathuria
第 3 部分概述了如何使用类似于reshape
和transpose
的各种操作来避免不必要的内存分配和数据复制,从而加速我们的代码。在这一部分,我们将看到它的实际应用。
我们将特别关注对象检测器输出管道中的一个特定元素,它涉及到重新安排内存中的信息。然后我们将实现一个天真的版本,通过使用一个循环将信息复制到一个新的地方来执行信息的重新排列。接下来我们将使用reshape
和transpose
来优化操作,这样我们就可以不使用循环来完成它。这将导致探测器的 FPS 速度大大加快。
所以让我们开始吧!
理解问题陈述
我们在这里处理的问题是由嵌套循环引起的(这并不奇怪!).几年前,当我在一个名为 YOLO(你只能看一次)的基于深度学习的对象检测器的输出管道上工作时,我遇到了它。
现在,我不想离题到 YOLO 的细节,所以我将保持问题陈述非常有限和简洁。我将描述足够多的内容,这样即使你从未听说过对象检测,你也能理解。
然而,如果您有兴趣跟进,我已经写了一个关于如何从零开始实现 YOLO 的 5 部分系列。
YOLO 使用卷积神经网络来预测图像中的对象。检测器的输出是卷积特征图。
以防上面的几行听起来像胡言乱语,这里有一个简化的版本。YOLO 是一个神经网络,它将输出一个卷积特征图,只是一个计算机视觉中常用的数据结构的花哨名称(就像链表、字典等)。).
一个卷积特征图本质上是一个多通道的空间网格。每个通道都包含有关空间格网中所有位置的特定要素的信息。在某种程度上,图像也可以定义为卷积特征图,用 3 个通道来描述红色、绿色和蓝色的强度。三维图像,其中每个通道包含一个特定的特征。
假设我们正试图检测包含轨道引擎的图像中的对象。我们将图像提供给网络,输出如下图所示。
图像被分成一个网格。特征图的每个单元在对应于每个网格单元的图像的特定部分中寻找对象。每个单元包含关于在网格中居中的 3 个边界框的数据。如果我们有一个网格,比如说3 x 3
,那么我们将有3 x 3 x 3 = 27
个这样的盒子的数据。输出管道将过滤这些盒子,留下几个包含对象的盒子,而其他大多数将被拒绝。涉及的典型步骤有:
- 基于某个分数的阈值盒。
- 移除指向同一对象的所有重叠框,只留下一个。
- 将数据转换成可以在图像上绘制的实际方框。
然而,鉴于信息在卷积特征图中的存储方式,执行上面强调的操作可能会导致混乱的代码(有关更多详细信息,请参考 YOLO 系列的第 4 部分)。为了使代码更容易阅读和管理,我们希望将存储在卷积特征图中的信息重新整理为表格形式,如下所示。
这就是问题所在!我们只需要重新整理信息。就是这样。没必要深入 YOLO!所以让我们开始吧。
设置实验
为了这个实验的目的,我提供了 pickled 卷积特征图,可以从这里下载。
我们首先使用pickle
将特征图加载到系统中。
import pickle
conv_map = pickle.load(open("conv_map.pkl", "rb"))
PyTorch 中的卷积神经网络是最广泛使用的深度学习库之一,以[B x C x H x W]
格式存储,其中B
是批量大小,C
是通道,H
和W
是维度。在上面使用的可视化中,卷积图是用格式[H W C]
演示的,但是使用[C H W]
格式有助于优化底层的计算。
pickled 文件包含一个 NumPy 数组,表示一个[B C H W]
格式的卷积特征图。我们首先打印加载的数组的形状。
print(conv_map.shape)
#output -> (1, 255, 13, 13)
这里批量为 1,通道数为 255,空间维度为13 x 13
。255 个通道对应于 3 个盒子,每个盒子的信息由 85 个浮点表示。我们想要创建一个数据结构,其中每行代表一个盒子,我们有 85 列代表这个信息。
天真的解决方案
让我们先尝试一下简单的解决方案。我们将首先为新的数据结构预分配空间,然后通过循环卷积特征图来填充空间。
# Get the shape related info
b, c, h, w = (conv_map.shape)
box_info_length = 85
# Pre-allocate the memory
counter = 0
output = np.zeros((b, c * h * w // box_info_length, box_info_length))
# Loop over dimensions
for x_b in range(b):
for x_h in range(h):
for x_w in range(w):
for x_c in range(0, c, box_info_length):
# Set the values
output[x_b, counter] = conv_map[x_b, x_c: x_c + box_info_length, x_h, x_w]
counter += 1
我们首先遍历图像中的每个单元格,然后遍历该单元格描述的每个方框。
你可能已经发现,四重嵌套循环不是一个好主意。如果我们使用更大的图像尺寸(更大的h
和w
)、每个单元更大数量的盒子(c
)或者更大的批量(b
),解决方案将不能很好地扩展。
我们能把这个矢量化吗?这里的操作涉及重新排列信息,而不是执行可以矢量化的操作。即使我们认为重新排列信息是一种复制操作,为了得到想要的结果,我们需要以这样一种方式重塑原始数据,使矢量化结果具有我们需要的表格形状。因此,即使对于矢量化,数据整形/重排也是执行矢量化复制的先决条件。
因此,如果我们想加速上面的数据整形练习,我们必须想出一种不使用缓慢的 pythonic 循环的方法。我们将使用我们在第 3 部分(整形和转置)中学到的特性来实现。
优化解决方案
让我们从初始化各种辅助变量开始。
output = conv_map[:]
b, c, h, w = (conv_map.shape)
box_info_length = 85
b
是批量大小,1。box_info_length
是用来描述单个盒子的元素数量,85。c
是通道的数量,或者关于每个网格单元 3 个盒子的信息,box_info_length
* 3 = 255。h
和w
是空间维度,都是 13。
因此,我们的目标数据结构中的列数将等于box_info_length
(85),因为我们需要每行描述一个框。重新插入显示需要执行的转换的图像,供您在此处参考。
从这里开始,我将卷积映射称为源数组,而我们想要创建的表格形式是目标数组。
在我们的目标数组中,第一维是批处理。第二个维度是盒子本身。在我们的例子中,每个单元格预测 3 个盒子,所以我们的目标数组将有H x W x 3
或13 x 13 x 3 = 507
个这样的盒子。在我们的源数组中,在任意位置[h,w]
的 3 个盒子分别由[h, w, 0:85]
、[h, w, 85:170]
和[h, w, 170:255]
给出。我们的目标数组中的三行描述了相同的信息。
在内存中,源数组的数据是这样存储的。
现在,我们想直观地了解数据是如何存储在目标阵列中的。但在此之前,我们想介绍一些术语,以便在源阵列和目标阵列之间映射信息。
在目标数组中,每个方框代表一行。索引为D (h, w, n)
的行表示由源数组中[h,w]
处的网格单元预测的框n
。目标数组中的列表示为代表盒子特征的[f1, f2......fn]
。
我们的源数组有 4 维,而我们的目标数组有 3 维。用于表示源数组中盒子(h
和w
)的空间细节的两个维度在目标数组中基本上合并为一个。
因此,我们首先将源数组中的空间维度展平为一个。
output = output.reshape(b, c, h * w)
一旦这样做了,我们是否可以简单地将它整形为数组 to [b, num_boxes, box_width_length]
并完成它?答案是否定的,为什么?因为我们知道整形不会改变数组在内存中的存储方式。
在我们的数组(我们刚刚重塑的)中,我们以下面的方式存储信息。
[b0][c0][h0,w0] [b0][c0][h0,w1] [b0][c0][h0,w2] [b0][c0][h0,w3]
然而,在我们的目标阵列中,两个相邻的单元描述了单个单元的两个通道。
[b0][D(h0, w0, B0)][f1] [b0][D(h0, w0, b0)][f2] [b0][D(h0, w0, b0)][f3]
对于每个批次,并且对于该批次中的每个空间位置,存储跨通道的信息,然后移动到另一个空间位置,然后移动到另一个批次元素。
注f1....fn
基本上是沿通道描述的信息,通道c
在源阵列中。(参考上面的转换图)。
这意味着在内存中,我们的源数组以不同的方式存储在我们的目标数组中。光靠整形是不行的。因此,我们必须修改数组的存储方式。
我们拥有的数组和我们想要得到的数组之间的根本区别在于编码通道和空间信息的维度顺序。如果我们使用现有的数组,并交换通道和空间特征的维度,我们将得到一个这样存储在内存中的数组。
[b0][h0,w0][c0] [b0][h0,w0][c1] [b0][h0,w0][c2] [b0][h0,w0][c3]
这正是信息在目标阵列中的存储方式。尺寸的重新排序可以通过transpose
操作来完成。
output = output.transpose(0, 2, 1)
然而,我们的工作还没有完成。在新数组的第三维中,我们仍然拥有完整的通道信息。如上所述,频道信息包含三个首尾相连的盒子的信息。
我们现在拥有的数组的形状是[b][h*w][c]
。arr[b][h,w]
描述了三个盒子arr[b][h,w][c:c//3]
、arr[b][h,w][c//3: 2*c//3]
和arr[b][h,w][2*c//3:c]
。我们想要做的是创建一个形状为[b][h*w*3][c/3]
的数组,这样每个通道的 3 个盒子在第二维空间中被容纳为 3 个条目。
请注意,我们当前数组在内存中的存储方式与我们的目标数组是一样的。
# Current Array
[b0][h0,w0][c0] [b0][h0,w0][c1] [b0][h0,w0][c2] [b0][h0,w0][c3]
# Destination Array
[b0][D(h0, w0, b0)][f1] [b0][D(h0, w0, b0)][f2] [b0][D(h0, w0, b0)][f3]
因此,我们可以使用一个reshape
操作轻松地将当前数组转换成目标数组。
output = output.reshape(b, c * h * w // box_info_length, box_info_length)
这最终给了我们想要的没有循环的输出!
为新代码计时
现在我们有了完成任务的新方法。让我们比较一下两个版本的代码所用的时间。先给循环版计时。
%%timeit -n 1000
counter = 0
b, c, h, w = (conv_map.shape)
box_info_length = 85
counter = 0
output = np.zeros((b, c * h * w // box_info_length, box_info_length))
for x_b in range(b):
for x_h in range(h):
for x_w in range(w):
for x_c in range(0, c, box_info_length):
output[x_b, counter] = conv_map[x_b, x_c: x_c + box_info_length, x_h, x_w]
counter += 1
现在让我们对优化后的代码计时。
%%timeit -n 1000
output = conv_map[:]
b, c, h, w = (conv_map.shape)
box_info_length = 85
output = output.reshape(b, c, h * w)
output = output.transpose(0, 2, 1)
output = output.reshape(b, c * h * w // box_info_length, box_info_length)
我们看到优化后的代码运行时间约为 18.2 纳秒,比循环版本快 20 倍!
物体探测器对 FPS 的影响
虽然优化后的代码运行速度比循环版本快 20 倍左右,但当我在我的 RTX 2060 GPU 上使用 0.75 的 置信度阈值 时,实际的 FPS 提升约为 8 倍。我们之所以获得较小的加速是因为下面提到的几个原因。
首先,如果检测到的对象数量太多,那么代码的瓶颈就变成了在图像上绘制矩形框,而不是输出管道。因此,来自输出流水线的增益并不完全对应于 FPS 中的增益。
第二,每帧有不同数量的盒子。甚至帧速率的移动平均值在整个视频中也是变化的。当然,甚至置信度阈值也会影响帧速率,因为置信度阈值越低,意味着检测到的盒子越多。
第三,你得到的加速可能会因你使用的 GPU 类型而异。
这就是为什么我演示了代码片段之间的时间差,而不是检测器本身。检测器的 FPS 加速会根据我上面提到的参数而有所不同。然而,每次我使用这个技巧,我的速度总是相当可观。即使 FPS 的速度提高了 5 倍,也意味着可以实时执行对象检测。
设置一个物体检测器可能是一个相当复杂的练习,超出了本文的范围,因为不是所有的读者都喜欢基于深度学习的计算机视觉。对那些感兴趣的人,你可以看看我的 YOLO 系列。该特定实现使用整形和转置来加速输出流水线。
结论
各位,这部分到此为止。在这一部分中,我们看到了像reshape
和transpose
这样的方法是如何帮助我们加速信息重组任务的。虽然transpose
不需要复制,但是循环仍然被移到 C 例程中,因此比使用 pythonic 循环实现的转置要快。
虽然我们优化了对象检测器的输出,但类似的想法也可以应用于类似语义分割、姿态检测等相关应用。其中卷积特征图是我们的最终输出。现在,我将停止这个系列。我希望这些帖子能帮助你更好地理解 NumPy,并写出更优化的代码。在那之前,结束!
更快的 R-CNN 解释为对象检测任务
原文:https://blog.paperspace.com/faster-r-cnn-explained-object-detection/
这篇文章对微软的一组研究人员开发的更快的 R-CNN 模型进行了评论。更快的 R-CNN 是用于对象检测的深度卷积网络,在用户看来是单个端到端的统一网络。该网络可以准确快速地预测不同物体的位置。为了真正理解更快的 R-CNN,我们还必须快速概述一下它的发展网络,即 R-CNN 和快速 R-CNN。
本文从快速回顾基于区域的 CNN ( R-CNN )开始,这是使用预训练的 CNN 建立提取特征的对象检测模型的第一次尝试。接下来,快速 R-CNN 快速回顾,比 R-CNN 更快但不幸的是忽略了地区提案是如何产生的。这一问题随后由更快的 R-CNN 解决,它构建了一个区域提议网络,可以生成区域提议,这些提议被馈送到检测模型(快速 R-CNN)以检查对象。
这篇文章的大纲如下:
- 对象检测管道概述
- R-CNN 评论
- 快速 R-CNN 概述
- 更快的 R-CNN
- 主要贡献
- 区域提案网络
- 锚
- 客观性分数
- RPN 和快速 R-CNN 之间的特征共享
- 训练更快的 R-CNN
- 交替训练
- 近似联合训练
- 非近似联合训练
- 缺点
- 屏蔽 R-CNN
- 结论
- 参考
文中提到的论文可以免费下载。在本文末尾的参考文献部分可以找到这些论文的引文和下载链接。
让我们开始吧。
物体检测流水线概述
传统的物体检测技术遵循下图中给出的 3 个主要步骤。第一步是提出几个区域提案。这些区域建议是其中可能有对象的候选者。这些区域的数量通常为数千个,例如 2000 个或更多。生成区域建议的一些算法的例子是选择性搜索和边缘框。
从每个区域提议中,使用各种图像描述符(例如,像梯度方向直方图(HOG ))来提取固定长度的特征向量。这个特征向量对于物体检测器的成功是至关重要的。向量应该充分地描述一个对象,即使它由于某种变换而变化,如缩放或平移。
然后,使用特征向量将每个区域提议分配给背景类或对象类之一。随着类数量的增加,构建一个能够区分所有这些对象的模型的复杂性也在增加。用于对区域提议进行分类的一个流行模型是支持向量机(SVM)。
这个快速概述足以理解基于区域的卷积神经网络(R-CNN)的基础。
R-CNN 快速概览
2014 年,加州大学伯克利分校的一组研究人员开发了一种深度卷积网络,称为 R-CNN(基于区域的卷积神经网络的简称)\([1]\)可以检测图像中 80 种不同类型的对象。
与上图所示的对象检测技术的通用管道相比,R-CNN \([1]\)的主要贡献只是基于卷积神经网络(CNN)提取特征。除此之外,一切都类似于通用对象检测管道。下图显示了 R-CNN 模型的工作原理。
R-CNN 由 3 个主要模块组成:
- 第一个模块使用选择性搜索算法生成 2000 个区域建议。
- 在被调整大小到固定的预定义大小之后,第二模块从每个区域提议中提取长度为 4096 的特征向量。
- 第三模块使用预训练的 SVM 算法将区域提议分类到背景或对象类别之一。
R-CNN 模式有一些缺点:
- 它是一个多阶段模型,每个阶段都是一个独立的组件。因此,它不能进行端到端的培训。
- 它将从预先训练的 CNN 提取的特征缓存在磁盘上,以便稍后训练支持向量机。这需要数百千兆字节的存储空间。
- R-CNN 依赖于选择性搜索算法来生成区域提议,这需要大量时间。此外,该算法不能针对检测问题进行定制。
- 每个区域提议被独立地馈送给 CNN 用于特征提取。这使得实时运行 R-CNN 变得不可能。
作为 R-CNN 模型的扩展,快速 R-CNN 模型被提出\([2]\)以克服一些限制。下一节将简要介绍快速 R-CNN。
快速 R-CNN 快速概述
Fast R-CNN \([2]\)是一个物体探测器,由脸书人工智能研究员、前微软研究员 Ross Girshick 单独开发。快速 R-CNN 克服了 R-CNN 的几个问题。顾名思义,快速 R-CNN 相对于 R-CNN 的一个优势就是速度快。
以下是$[2]中主要贡献的总结:
- 提出了一个名为 ROI Pooling 的新层,它从同一图像的所有提议(即 ROI)中提取等长特征向量。
- 与具有多个阶段(区域提议生成、特征提取和使用 SVM 的分类)的 R-CNN 相比,更快的 R-CNN 建立了只有单个阶段的网络。
- 更快的 R-CNN 跨所有提议(即 ROI)共享计算(即卷积层计算),而不是为每个提议单独进行计算。这是通过使用新的 ROI Pooling 层来实现的,这使得快速 R-CNN 比 R-CNN 更快。
- 快速 R-CNN 不缓存提取的特征,因此与需要数百千兆字节的 R-CNN 相比,不需要这么多的磁盘存储。
- 快速 R-CNN 比 R-CNN 更准确。
快速 R-CNN 的一般架构如下所示。与 R-CNN 中的 3 个阶段相比,该模型由单个阶段组成。它只接受一个图像作为输入,并返回检测到的对象的类别概率和边界框。
来自最后一个卷积层的特征图被馈送到 ROI 汇集层。原因是从每个区域提议中提取固定长度的特征向量。下面的 GIF 显示了 ROI Pooling 层是如何工作的。
简而言之,ROI Pooling 层的工作原理是将每个区域提案分成一个单元格网格。max pooling 运算应用于网格中的每个单元格,以返回单个值。来自所有单元的所有值代表特征向量。如果网格大小为 2 × 2,则特征向量长度为 4。
有关 ROI Pooling 层的更多信息,请查看本文。
使用 ROI 池提取的特征向量然后被传递到一些 FC 层。最后一个 FC 层的输出被分成两个分支:
- Softmax 层预测班级分数
- FC 层预测被检测物体的包围盒
在 R-CNN 中,每个区域提议独立于其他区域提议被馈送到模型。这意味着如果单个区域需要 S 秒来处理,那么 N 个区域需要S*N
秒。快速 R-CNN 比 R-CNN 更快,因为它跨多个提议共享计算。
R-CNN \([1]\)从每幅图像中采样单个 ROI,而快速 R-CNN \([2]\)从同一幅图像中采样多个 ROI。例如,R-CNN 从 128 个不同的图像中选择一批 128 个区域。因此,总处理时间为128*S
秒。
对于更快的 R-CNN,这一批 128 个区域可以仅从 2 个图像中选择(每个图像 64 个区域)。当从同一图像中对区域进行采样时,它们的卷积层计算是共享的,这减少了时间。所以,处理时间下降到2*S
。然而,从同一图像中采样多个区域会降低性能,因为所有区域都是相关的。
尽管快速 R-CNN 模型具有优点,但是存在严重的缺点,因为它依赖于耗时的选择性搜索算法来生成区域提议。选择性搜索方法不能在特定对象检测任务上定制。因此,检测数据集中的所有目标对象可能不够准确。
在下一节中,将介绍更快的 R-CNN \([3]\)。更快的 R-CNN 建立了一个生成地区建议的网络。
更快的 R-CNN
更快的 R-CNN \([3]\)是快速 R-CNN \([2]\)的扩展。顾名思义,更快的 R-CNN 比快速 R-CNN 更快,这要归功于区域提议网络(RPN)。
主要贡献
本文的主要贡献是\([3]\):
- 提议区域提议网络(RPN) 这是一个完全卷积的网络,可生成各种规模和纵横比的提议。RPN 使用神经网络的术语来告诉目标检测(快速 R-CNN)在哪里寻找。
- 本文引入了锚框的概念,而不是使用图像金字塔(即图像的多个实例,但比例不同)或过滤器金字塔(即大小不同的多个过滤器)。锚定框是具有特定比例和纵横比的参考框。对于多个参考锚定框,单个区域存在多个比例和纵横比。这可以被认为是参考锚盒的金字塔。然后将每个区域映射到每个参考锚定框,从而检测不同比例和纵横比的对象。
- 卷积计算由 RPN 和快速 R-CNN 共享。这减少了计算时间。
更快的 R-CNN 的架构如下图所示。它由两个模块组成:
- RPN :用于生成区域建议。
- 快速 R-CNN :用于检测建议区域内的物体。
RPN 模块负责生成区域提案。它应用了神经网络中注意力的概念,因此它引导快速 R-CNN 检测模块在图像中的何处寻找物体。
请注意卷积层(例如计算)是如何在 RPN 和快速 R-CNN 模块之间共享的。
更快的 R-CNN 工作方式如下:
- RPN 生成区域建议。
- 对于图像中的所有区域提议,使用 ROI 池层\([2]\),从每个区域提取固定长度的特征向量。
- 然后使用快速 R-CNN 对提取的特征向量进行分类。
- 除了它们的包围盒之外,还返回检测到的对象的类别分数。
地区提案网络
R-CNN 和快速 R-CNN 模型依赖于选择性搜索算法来生成区域提议。每一个提议都被送到预先训练好的 CNN 进行分类。本文提出了一种可以产生区域建议的网络,称为区域建议网络(RPN)。这有一些优点:
- 现在使用可以根据检测任务来训练和定制的网络来生成区域提议。
- 因为建议是使用网络生成的,所以可以对其进行端到端的训练,以根据检测任务进行定制。因此,与一般的方法如选择性搜索和边缘框相比,它产生了更好的区域建议。
- RPN 使用快速 R-CNN 检测网络中使用的相同卷积层来处理图像。因此,与选择性搜索等算法相比,RPN 不需要额外的时间来产生建议。
- 由于共享相同的卷积层,RPN 和快速 R-CNN 可以合并/统一到单个网络中。因此,训练只进行一次。
RPN 处理从与快速 R-CNN 共享的最后一个卷积层返回的输出特征图。下图显示了这一点。基于大小为nxn
的矩形窗口,滑动窗口穿过特征图。对于每个窗口,生成几个候选区域提议。这些提案不是最终提案,因为它们将根据“客观性得分”进行筛选(如下所述)。
锚
根据下图,最后一个共享卷积层的特征图通过一个大小为nxn
的矩形滑动窗口,其中n=3
为 VGG-16 网。对于每个窗口,K
区域的建议被生成。每个方案都根据一个称为锚盒的参考盒进行参数化。锚箱的两个参数是:
- 规模
- 长宽比
通常,有 3 个比例和 3 个纵横比,因此总共有K=9
个锚盒。但是K
可能不同于 9。换句话说,K
区域是从每个区域提议中产生的,其中每个K
区域在比例或纵横比上有所不同。下图显示了一些锚点变体。
使用参考锚(即锚盒),使用单一比例的单一图像,同时能够提供比例不变的对象检测器,因为锚存在于不同的比例。这避免了使用多个图像或过滤器。多尺度锚是在 RPN 和快速 R-CNN 检测网络中共享特征的关键。
对于每个nxn
区域提议,提取特征向量(对于 ZF 网长度为 256,对于 VGG-16 网长度为 512)。该向量然后被馈送到两个兄弟全连接层:
- 第一个 FC 层被命名为
cls
,并且代表一个二元分类器,该二元分类器为每个区域提议(即,该区域是否包含对象,或者是背景的一部分)生成对象性得分。 - 第二个 FC 层被命名为
reg
,它返回一个定义区域边界框的四维向量。
第一 FC 层(即二元分类器)具有 2 个输出。第一个用于将该区域分类为背景,第二个用于将该区域分类为对象。下一节讨论如何将客观性分数分配给每个锚点,以及如何使用它来产生分类标签。
客观分数
cls
层为每个区域提议输出 2 个元素的向量。如果第一个元素为 1,第二个元素为 0,则区域建议被分类为背景。如果第二个元素为 1,第一个元素为 0,则该区域表示一个对象。
为了训练 RPN,基于交集-并集(IoU) 给每个锚点一个正的或负的客观分数。
IoU 是锚盒和地面实况盒之间的相交区域与两个盒的联合区域的之比。IoU 范围为 0.0 至 1.0。无交集时,借据为 0.0。随着两个盒子越来越靠近,IoU 会增加,直到达到 1.0(当两个盒子 100%相同时)。
接下来的 4 个条件使用 IoU 来确定是否将正的或负的客观性分数分配给锚点:
- 与任何地面实况框具有高于 0.7 的 IoU 重叠的锚被给予正对象标签。
- 如果没有 IoU 重叠高于 0.7 的锚点,则使用地面实况框为 IoU 重叠最高的锚点分配一个正标签。
- 当所有地面实况框的 IoU 重叠小于 0.3 时,负的客观性分数被分配给非正的锚。负的客观性分数意味着锚被分类为背景。
- 既不积极也不消极的锚对训练目标没有贡献。
第一次看论文的时候被第二个和第三个条件搞糊涂了。所以,还是多澄清一下吧。
假设有 3 个区域提案与 3 个锚相关联,下面列出了它们的 IoU 分数和 3 个实际情况框。因为有一个 IoU 分数为 0.9(高于 0.7)的锚点,所以它被赋予该基本事实框的正客观性分数,而所有其他框为负。
0.9, 0.55, 0.1
以下是主播分类的结果:
positive, negative, negative
第二个条件意味着,当没有锚点的 IoU 重叠分数高于 0.7 时,则搜索具有最高 IoU 的锚点,并为其分配正的客观分数。预计最大借据得分小于或等于 0.7,但令人困惑的是,论文没有提到借据得分的最小值。
预计最小值应该是 0.5 。因此,如果锚盒的 IoU 分数大于 0.5 但小于或等于 0.7 ,则为其分配一个正的客观性分数。
假设下面列出了某主播的 IoU 评分。因为最高的 IoU 分数是第二个,值为 0.55,所以落在第二个条件下。因此,它被赋予一个正的客观性分数。
0.2, 0.55, 0.1
以下是主播分类的结果:
negative, positive, negative
第三个条件指定当具有所有基本事实框的锚的 IoU 分数小于 0.3 时,则该锚被分配负的客观性分数。对于接下来的 IoU 分数,由于所有的 IoU 分数都小于 0.3,锚被给予 3 种情况的负分数。
0.2, 0.25, 0.1
以下是主播分类的结果:
negative, negative, negative
根据第四个条件,当一个锚的 IoU 得分大于或等于 0.3 但小于或等于 0.5 时,它既不被分类为正面也不被分类为负面。这个锚不用于训练分类器。
对于以下 IoU 分数,锚不会被分配任何标签,因为所有分数都在 0.3 和 0.5 之间(包括 0.3 和 0.5)。
0.4, 0.43, 0.45
下一个等式总结了 4 个条件。
注意,第一个条件(0.7 < IoU
)通常足以将锚标记为阳性(即,包含对象),但是作者更喜欢提及第二个条件(0.5 < IoU <= 0.7
),用于不存在 IoU 为 0.7 的区域的罕见情况。
RPN 和 Fast R-CNN 之间的功能共享
快速 R-CNN 架构中的两个模块,即 RPN 和快速 R-CNN,是独立的网络。他们每个人都可以单独训练。相反,对于更快的 R-CNN,可以建立一个统一的网络,其中 RPN 和快速 R-CNN 同时被训练。
核心思想是 RPN 和快速 R-CNN 共享相同的卷积层。这些层只存在一次,但是在两个网络中使用。可以称之为图层共享或者特征共享。请记住,锚$[ 3]$使得在更快的 R-CNN 中的两个模块之间共享特性/层成为可能。
训练更快的 R-CNN
快速 R-CNN 论文\([3]\)提到了在共享卷积层的同时训练 RPN 和快速 R-CNN 的 3 种不同方法:
- 交替训练
- 近似联合训练
- 非近似联合训练
交替训练
第一种方法被称为交替训练,其中 RPN 首先被训练以生成区域提议。共享卷积层的权重基于 ImageNet 上预先训练的模型进行初始化。RPN 的其他权重是随机初始化的。
在 RPN 产生区域提议的盒子之后,RPN 和共享卷积层的权重都被调整。
RPN 生成的建议用于训练快速 R-CNN 模块。在这种情况下,共享卷积层的权重由 RPN 用调整后的权重初始化。其他快速 R-CNN 权重是随机初始化的。当快速 R-CNN 被训练时,快速 R-CNN 和共享层的权重都被调整。共享层中调整的权重再次用于训练 RPN,并且重复该过程。
根据\([3]\), 交替训练是训练两个模块的首选方式,并应用于所有实验。
近似联合训练
第二种方法称为近似联合训练,其中 RPN 和 Fast R-CNN 都被视为单个网络,而不是 2 个独立的模块。在这种情况下,区域提案由区域方案网络产生。
在不更新 RPN 和共享层的权重的情况下,建议被直接馈送到快速 R-CNN,该 CNN 检测物体的位置。只有在快速 R-CNN 产生其输出之后,快速 R-CNN 中的权重才被调整。
因为在生成区域提议之后,RPN 和共享层的权重没有被更新,所以相对于区域提议的权重梯度被忽略。与第一种方法相比,这降低了该方法的准确性(即使结果很接近)。另一方面,训练时间减少约 25-50%。
非近似联合训练
在近似联合训练方法中,使用 RoI 扭曲层,以允许计算相对于提议的边界框的权重梯度。
弊端
更快的 R-CNN 的一个缺点是,在训练 RPN 时,小批量(大小为 256)中的所有锚都是从单个图像中提取的。因为来自单个图像的所有样本可能是相关的(即,它们的特征是相似的),所以网络可能需要很多时间才能达到收敛。
屏蔽 R-CNN
作为对更快的 R-CNN \([3]\)的扩展,Mask R-CNN 模型包括另一个分支,它为每个检测到的对象返回一个遮罩。
结论
本文回顾了一种用于对象检测的深度卷积神经网络,称为快速 R-CNN,它可以准确地检测和分类图像中的对象。
本文首先回顾了任何对象检测模型的一般步骤。然后,它快速回顾了 R-CNN 和快速 R-CNN 模型如何工作,以便了解快速 R-CNN 正在克服的挑战。
更快的 R-CNN 是端到端训练的单级模型。它使用了一种新颖的区域建议网络(RPN)来生成区域建议,与传统的算法(如选择性搜索)相比,它节省了时间。它使用 ROI 池层从每个区域提议中提取固定长度的特征向量。
我们看到的快速 R-CNN 的一个缺点是,对于 RPN,小批量中的所有锚都是从单个图像中提取的。因为来自单个图像的所有样本可能是相关的(即,它们的特征是相似的),所以网络可能需要很多时间才能达到收敛。
也就是说,更快的 R-CNN 是最先进的对象检测模型。屏蔽 R-CNN 从那时起就在更快的 R-CNN 的基础上构建,以返回每个检测到的对象的对象屏蔽。
参考文献
- Girshick,Ross 等人,“用于精确对象检测和语义分割的丰富特征层次”IEEE 计算机视觉和模式识别会议论文集。2014.
- 罗斯·吉斯克。"快速 r-cnn "IEEE 计算机视觉国际会议论文集。2015.
- 任,,等.〈快速 r-cnn:面向区域提议网络的实时目标检测.〉神经信息处理系统的进展。2015.
- 何,,等《美国有线电视新闻网》IEEE 计算机视觉国际会议论文集。2017.
使用 Keras 的联合学习
原文:https://blog.paperspace.com/federated-learning-with-keras/
每秒钟都会产生大量的数据。例如,用户在浏览网页或使用应用程序时,经常通过触摸屏幕来生成数据。使用机器学习来增强用户体验的应用程序可能会受益于此类数据,因为它包含有助于增强未来预测的信息。
也就是说,出于隐私考虑,这些数据可能不会与其他人共享。为了保持数据的私密性,但仍然用它来训练机器学习模型,保护隐私的机器学习一直在兴起。
本教程讨论了如何使用联邦学习来训练 Keras 模型,同时保持用户数据的私密性。本教程的代码可以在这个 GitHub 项目的 KerasFederated 目录下获得,它附带了一个使用 Kivy 创建的 GUI。
以下是讨论的主题:
- 联邦学习快速回顾
- 入门指南
- 准备培训数据
- 构建 Keras 模型
- 构建解决方案群体
- 监听服务器上的连接
- 服务器回复客户端请求
- 客户行为
- 结论
联合学习快速回顾
在传统的机器学习中,用户数据被收集到中央服务器中,在那里训练模型。这也被称为集中式机器学习,因为用户数据(即使是私有的)被上传到中央服务器。
正如 Lingjuan 等人(2020) 在他们的论文中提到的,社会已经越来越意识到共享数据侵犯用户隐私的危险。例如,训练一个可以识别人脸的模型来登录应用程序,或者共享透露患者私人信息的医疗记录。
联邦学习(简称 FL)来解决集中式机器学习的隐私相关问题。FL 使用客户端-服务器架构来训练模型。数据在客户端可用,模型在服务器端可用。我们如何使用客户端的数据训练服务器的模型?下一张图来自于联合学习的威胁论文,展示了 FL 是如何工作的。
服务器上有一个全局模型。模型的最新版本与客户端共享,其中每个客户端根据其私有数据更新模型。客户端只与更新全局模型的服务器共享训练模型的梯度。
联邦学习不是从天上掉下来的,仍然受到一些隐私问题的困扰。这篇综述论文将 FL 攻击总结为:
- 投毒攻击:模型做出符合攻击者目的的预测。
- 推理攻击:攻击者恢复用户的私有数据。
有关 FL 的更多信息,请查看以下资源:
本教程的剩余部分将重点描述如何为联邦学习训练我们的 Keras 模型。
入门
在使用 FL 构建和训练 Keras 模型之前,让我们先熟悉一下将要使用的项目。基础项目在链接可用。该项目中发布的示例展示了如何使用神经网络来训练 XOR 问题,该神经网络使用遗传算法和 PyGAD 库来训练。该项目有一个 GUI 来简化交互。
下载项目后,根目录下有三个 Python 文件,它们是:
server.py
:FL 服务器建立模型并发送给客户端。client1.py
:一个客户端有 2 个训练样本。client2.py
:另一个客户有另外 2 个训练样本。
您可以根据需要添加更多客户端。这个项目使用 Python 套接字编程从头开始构建客户机和服务器。
通过运行服务器,出现以下 Kivy 窗口。以下是对 GUI 中按钮的描述:
- 创建套接字:创建一个套接字。
- 绑定套接字:将套接字绑定到在两个文本字段中输入的 IPv4 地址和端口号。
- 听连接:打开插座进行连接。
- 关闭插座:关闭插座。
- 插座状态:显示一些信息性消息。
ِAfter 服务器运行,接下来是运行客户端。当客户端运行时,下图中的 GUI 会显示以下按钮:
- 创建套接字:创建一个套接字。
- 连接到服务器:将客户端的套接字连接到服务器的套接字。确保服务器的套接字接受连接。
- 接收&训练模型:这是实际工作发生的地方,因为客户机从服务器接收模型,训练它,并将其发送回服务器。
- 关闭插座:关闭插座。
服务器有一个超时计时器,默认为 5 秒。如果在此超时时间内没有收到数据,则连接关闭。这意味着服务器必须在 5 秒内响应客户端。如果客户端训练模型和响应服务器的时间超过 5 秒,请增加该时间。
对于客户端,定时器时间是在创建RecvThread
类的实例时指定的。对于服务器来说,时间是SocketThread
类的一个属性。
一旦误差为 0.0 美元,服务器就停止训练模型。只要不是\(0.0,\)服务器就会一直把模型发送给客户端。下图总结了客户端和服务器之间的通信。
当客户机第一次连接到服务器时,它发送一个 echo 消息。服务器用其全局模型的副本来响应回显消息。
客户端基于其本地数据训练模型,并将训练好的模型发送到服务器。服务器根据其测试数据评估模型。如果服务器决定需要更多的训练,服务器用模型的最新副本来响应客户端。这个过程一直持续到服务器将模型标记为已训练。此时,客户端和服务器之间的连接关闭。
在模型被训练之后,服务器不再将模型发送给客户端,并且连接变为空闲。超时计时器到期后,服务器和客户端之间的连接会自动关闭。
确保项目按预期运行后,让我们继续下一部分,准备用于训练模型的数据。稍后,创建 Keras 模型,并准备使用 PyGAD 进行训练。
准备培训数据
为了使用 FL 训练模型,训练数据分布在客户端上。服务器本身没有任何训练数据,只有测试数据来评估从客户端接收的模型。
本教程中讨论的示例只是考虑基于 XOR 问题的训练样本来训练 Keras 模型。XOR 问题只有 4 个样本,如下面的代码所示。
每个样本的输出是两个元素的向量。如果第一个元素是\(1\),那么这个样本的 XOR 的输出是\(1\)。如果第二个元素是\(0\),那么这个样本的 XOR 输出是\(1\)。例如,分配给样本[1, 1]
的输出向量是[1, 0]
,这意味着输出是\(0\)。
data_inputs = numpy.array([[1, 1],
[1, 0],
[0, 1],
[0, 0]])
data_outputs = numpy.array([[1, 0],
[0, 1],
[0, 1],
[1, 0]])
在本教程中,将只有 2 个客户端,其中每个客户端只有 2 个训练样本。第一个客户端有以下两个样本:
data_inputs = numpy.array([[1, 1],
[1, 0]])
data_outputs = numpy.array([[1, 0],
[0, 1]])
第二个客户端有另外两个样本:
data_inputs = numpy.array([[0, 1],
[0, 0]])
data_outputs = numpy.array([[0, 1],
[1, 0]])
因为 XOR 问题没有额外的样本用作测试数据,所以使用相同的训练数据作为测试数据。如果你正在解决另一个有大量数据的问题,那么使用一些不同于训练样本的测试样本。
给定训练数据,下一节构建处理 XOR 问题的 Keras 模型。
建立 Keras 模型
根据您的喜好,使用顺序 API 或函数 API 构建 Keras 模型。下面是一个为 XOR 问题构建简单 Keras 模型的示例。该模型有以下 3 层:
- 两个神经元的输入。
- 隐藏着 4 个神经元。
- 具有 2 个神经元和 Softmax 功能的输出。
输入的数量是 2,因为 XOR 的每个样本只有 2 个输入。
import tensorflow.keras
num_inputs = 2
num_classes = 2
input_layer = tensorflow.keras.layers.Input(num_inputs)
dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer)
output_layer = tensorflow.keras.layers.Dense(num_classes, activation="softmax")(dense_layer)
model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer)
下一节使用创建的模型为遗传算法创建一组解决方案。
构建解决方案群体
本教程不使用梯度下降,而是使用遗传算法来训练 Keras 模型。要了解训练是如何工作的,请查看本教程:如何使用 PyGAD 的遗传算法训练 Keras 模型。
下一个代码块使用pygad.kerasga.KerasGA
类构建一组解决方案,其中每个解决方案包含 Keras 模型参数的一些值。在本例中,使用了 10 种溶液。
import pygad.kerasga
num_solutions = 10
keras_ga = pygad.kerasga.KerasGA(model=model,
num_solutions=num_solutions)
解决方案的群体被保存到KerasGA
类的population_weights
属性中。因此,可以按如下方式简单地访问它:
keras_ga.population_weights
下一节展示了服务器如何监听来自客户端的连接。
在服务器上监听连接
服务器有一个名为ListenThread
的类,是一个从服务器 socket 打开那一刻开始的线程。对于来自客户端的每个连接,从SocketThread
类创建一个新的实例来服务客户端。
在SocketThread
类的构造函数中,有两个重要的参数:
buffer_size
:缓冲区大小(以字节为单位)。它默认为 1,024 ,这意味着该模型被分割成一个个 1,024 字节的块。如果模型尺寸较大,请使用较大的值。否则,服务器可能要花很多时间从客户端接收模型。recv_timeout
:如果在此秒数后没有从客户端收到任何信息,将关闭与客户端的连接。如果在客户端训练模型需要很长时间,请允许服务器等待更长时间(更多秒)。如果未正确设置此参数,则当客户端使用其本地数据训练模型时,连接可能会关闭。
socket_thread = SocketThread(...,
buffer_size=1024,
recv_timeout=10)
下一节讨论服务器如何响应客户机的请求。
服务器回复客户端请求
server.py
脚本有一个名为SocketThread
的类,它有 4 个方法:
run()
:这个方法运行一个无限循环来接收客户端的消息。recv()
:接收客户端的消息。reply()
:回复客户端。model_averaging()
:计算模型中的当前参数与从客户端接收的模型之间的平均值。
本节重点介绍reply()
和model_averaging()
方法。
在reply()
方法中,服务器根据从客户端接收到的信息决定其响应。客户端通常发送以下形式的嵌套字典:
data = {"subject": ...,
"data": {"best_model_weights_vector": ...}}
它有两把钥匙:
subject
:其值可以是echo
或model
。data
:它的值可以是None
,也可以是到目前为止只有一个名为best_model_weights_vector
的键的字典。
如果subject
是echo
,这意味着客户端刚刚打开了与服务器的连接。subject="echo"
时客户端可能会也可能不会发送模型。因此,如果分配给键best_model_weights_vector
的值是None
,客户端没有发送模型。在这种情况下,服务器会回复其最新型号。
如果在subject="echo"
时分配给best_model_weights_vector
键的值不是None
,那么服务器仅在模型的当前精度不是1.0
时才回复模型,这意味着模型是 100%准确的。服务器回复的消息是以下形式的嵌套字典:
data = {"subject": ...,
"data": {
"population_weights": ...,
"model_json": ...,
"num_solutions": ...
}
}
下面是对这些键的含义的描述:
subject
:消息主题可以是"model"
也可以是"done"
。如果设置为"model"
,那么客户端知道服务器正在发送模型。如果它被设置为"done"
,那么客户端知道模型被训练。population_weights
:保存所有解的参数的群体。model_json
:JSON 格式的 Keras 模型架构。num_solutions
:群体中解的数量。
下面的代码块实现了到目前为止所讨论的内容。首先,它检查主题是否是"echo"
。如果是这样,那么它准备一个字典来保存群体、模型架构和许多解决方案,并将它发送给客户机。
如果subject
是"model"
,那么客户端将模型附加到它的消息中。因此,分配给键best_model_weights_vector
的值包含了模型的参数。如果subject
为"done"
,则模型被训练,data
键将被设置为None
。
如果model
不是None
,这意味着客户端在其消息中附加了一个模型。服务器首先检查自己的模型精度是否为 1.0。如果是,那么服务器将不会向客户端发送模型(data
键设置为None
),因为服务器不再需要训练模型。如果准确度不是 1.0,则服务器回复其要在客户端训练的模型。
def reply(self, received_data):
...
if subject == "echo":
if model is None:
data_dict = {"population_weights": keras_ga.population_weights,
"model_json": model.to_json(),
"num_solutions": keras_ga.num_solutions}
data = {"subject": "model", "data": data_dict}
else:
predictions = model.predict(data_inputs)
ba = tensorflow.keras.metrics.BinaryAccuracy()
ba.update_state(data_outputs, predictions)
accuracy = ba.result().numpy()
if accuracy == 1.0:
data = {"subject": "done", "data": None}
else:
data_dict = {"population_weights": keras_ga.population_weights,
"model_json": model.to_json(),
"num_solutions": keras_ga.num_solutions}
data = {"subject": "model", "data": data_dict}
elif subject == "model":
best_model_weights_vector = received_data["data"]["best_model_weights_vector"]
best_model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model, weights_vector=best_model_weights_vector)
new_model = tensorflow.keras.models.clone_model(model)
new_model.set_weights(weights=best_model_weights_matrix)
predictions = model.predict(data_inputs)
ba = tensorflow.keras.metrics.BinaryAccuracy()
ba.update_state(data_outputs, predictions)
accuracy = ba.result().numpy()
if accuracy == 1.0:
data = {"subject": "done", "data": None}
response = pickle.dumps(data)
return
self.model_averaging(model, best_model_weights_matrix)
predictions = model.predict(data_inputs)
ba = tensorflow.keras.metrics.BinaryAccuracy()
ba.update_state(data_outputs, predictions)
accuracy = ba.result().numpy()
if accuracy != 1.0:
data_dict = {"population_weights": keras_ga.population_weights,
"model_json": model.to_json(),
"num_solutions": keras_ga.num_solutions}
data = {"subject": "model", "data": data_dict}
response = pickle.dumps(data)
else:
data = {"subject": "done", "data": None}
response = pickle.dumps(data)
...
如果subject
键是"model"
,那么客户端发送一个模型。在这种情况下,服务器获取 Keras 模型参数,并在best_model_weights_matrix
变量中以矩阵形式准备它们。基于这些参数,计算模型精度。
如果精度为 1.0,则模型训练成功,服务器不会将模型发送给客户端。否则,使用model_averaging()
方法将从客户端接收的参数与服务器端的参数进行平均。
调用model_averaging()
方法后,再次计算模型精度。如果精度不是 1.0,则服务器将新的模型参数发送给客户端。否则,不发送模型。
下面给出了model_averaging()
方法的实现。它接收服务器的模型和从客户端接收的参数,并对它们进行平均。最后,将新参数设置到模型中。
def model_averaging(self, model, best_model_weights_matrix):
model_weights_vector = pygad.kerasga.model_weights_as_vector(model=model)
model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model, weights_vector=model_weights_vector)
new_weights = model_weights_matrix
for idx, arr in enumerate(new_weights):
new_weights[idx] = new_weights[idx] + best_model_weights_matrix[idx]
new_weights[idx] = new_weights[idx] / 2
model.set_weights(weights=new_weights)
下一节讨论客户端的行为。
客户端行为
客户端有一个名为RecvThread
的类,它创建一个线程,从客户端和服务器之间的连接打开时就开始工作,一直运行到连接关闭。
这个类有两个方法:
run()
:此方法使用无限循环在客户端之间发送和接收数据。recv()
:接收服务器的消息。
下面是run()
方法的重要代码部分。当客户端发出第一个请求时,它的主题是"echo"
,而best_model_weights_vector
键被设置为None
。
然后,客户机接收服务器的响应,并检查响应的subject
。如果是"model"
,那么服务器发来一个模型。如果是"done"
,那么客户端打破无限循环关闭连接。
def run(self):
...
subject = "echo"
server_data = None
best_model_weights_vector = None
best_sol_idx = -1
while True:
data_dict = {"best_model_weights_vector": best_model_weights_vector}
data = {"subject": subject, "data": data_dict}
data_byte = pickle.dumps(data)
try:
self.kivy_app.soc.sendall(data_byte)
except BaseException as e:
break
received_data, status = self.recv()
subject = received_data["subject"]
if subject == "model":
server_data = received_data["data"]
elif subject == "done":
break
else:
break
ga_instance = prepare_GA(server_data)
ga_instance.run()
subject = "model"
best_sol_idx = ga_instance.best_solution()[2]
best_model_weights_vector = ga_instance.population[best_sol_idx, :]
...
基于服务器发送的模型,调用名为prepare_GA()
的函数,该函数使用 PyGAD 使用遗传算法训练模型。
一旦训练完成,服务器回复客户端,同时将subject
键的值设置为"model"
,将best_model_weights_vector
键的值设置为最佳模型的参数。
请注意,所有客户端的工作方式都相同,只是使用的训练数据不同。该项目有两个客户。在的目录下,客户端的脚本被命名为 client1.py
和 client2.py
。
结论
本教程讨论了如何使用联邦学习来训练 Keras 模型。联合学习是一种客户端-服务器范式,其中一些客户端使用其私有数据训练全局模型,而不将其共享给中央服务器。
所讨论的例子只有两个客户端,它们一起工作来训练一个构建 XOR 门的模型。该模型在不共享客户私人数据的情况下被训练。
您可以轻松地将该项目与其他数据一起使用。您需要做的就是在服务器和客户端设置data_inputs
和data_outputs
数组。
前馈与反馈神经网络
原文:https://blog.paperspace.com/feed-forward-vs-feedback-neural-networks/
神经网络的结构在许多应用的人工智能建模研究中变得越来越重要。已经开发了两种相反的结构范例:反馈(递归)神经网络和前馈神经网络。在本文中,我们在全面分析了这两种体系结构之后,对它们进行了深入的比较。然后,我们通过一些用例比较每个神经网络结构的性能。
首先,让我们从基础开始。
什么是神经网络?
作为深度学习的基本构建模块,神经网络以模拟人脑的行为而闻名,同时处理具有挑战性的数据驱动问题。
为了产生所需的输出,输入数据通过几层人工神经元进行处理,这些人工神经元一层叠一层。应用范围从简单的图像分类到更关键和复杂的问题,如自然语言处理、文本生成和其他与世界相关的问题。
神经网络的要素
构成神经网络结构的神经元复制了大脑的有机行为。
Elementary structure of a single neuron in a Neural Network
现在,我们将定义与神经网络相关的各种组件,并展示我们如何从神经元的基本表示出发,构建一些最复杂的架构。
投入
它是输入学习模型的数据(即特征)的集合。例如,当前大气测量的阵列可以用作气象预测模型的输入。
重量
重视最有助于学习过程的特征是使用权重的主要目的。通过在输入值和权重矩阵之间添加标量乘法,我们可以提高某些要素的效果,同时降低其他要素的效果。例如,高音调音符的存在会比在流派之间常见的其他平均音调音符更多地影响音乐流派分类模型的选择。
激活功能
为了考虑随着输入改变的线性,激活函数将非线性引入神经元的操作。没有它,输出将只是输入值的线性组合,网络将无法适应非线性。
最常用的激活函数有:单位阶跃、sigmoid、分段线性和高斯函数。
Illustrations of the common activation functions
偏见
偏差的目的是改变激活函数产生的值。它的功能相当于线性函数中的常数。所以,这基本上是激活函数输出的一个转移。
层
人工神经网络是由多个神经层堆叠而成的。每一层都是由几个神经元堆叠成一排。我们区分三种类型的层:输入层、隐藏层和输出层。
输入层
模型的输入层接收我们从图像或数字向量等外部来源引入的数据。这是整个神经网络设计中唯一可以看到的一层,它传输来自外部世界的所有信息,而不进行任何处理。
隐藏层
隐藏层使深度学习成为今天的样子。它们是中间层,完成所有计算并提取数据特征。对数据中隐藏特征的搜索可以包括许多相互关联的隐藏层。例如,在图像处理中,第一个隐藏层通常负责更高级别的功能,例如检测边界、形状和边界。另一方面,后面的隐藏层执行更复杂的任务,如分类或分割整个对象。
输出层
输出层使用前面隐藏层的数据进行最终预测。这是我们获得最终结果的一层,因此是最重要的一层。
在输出层中,分类和回归模型通常只有一个节点。然而,这完全取决于手头问题的性质以及模型是如何开发的。一些最新的模型具有二维输出层。例如,Meta 的新场景制作模型可以简单地从输入的文本中生成图像。
这些层是如何协同工作的?
输入节点以可以用数字表示的形式接收数据。每个节点被分配一个号码;数字越高,激活程度越大。信息显示为激活值。网络然后向外传播这些信息。激活值根据连接强度(权重)从一个节点发送到另一个节点,以表示抑制或激发。每个节点在根据其激活函数改变激活值之前,将它接收到的激活值相加。在到达输出节点之前,激活通过网络的隐藏层传播。然后,输出节点将输入有意义地反映给外部世界。通过将每个节点的权重分配到每个节点负责的误差的比例,误差(即预测值和实际值之间的差)被向后传播。
Example of a basic neural network
上述示例中的神经网络包括由三个输入节点组成的输入层、各基于四个节点的两个隐藏层以及由两个节点组成的输出层。
前馈神经网络的结构
在前馈网络中,信号只能向一个方向传输。这些网络被认为是具有输入、输出和隐藏层的非递归网络。一层处理单元接收输入数据并在那里执行计算。基于其输入的加权总和,每个处理元件执行其计算。新导出的值随后被用作后续层的新输入值。这一过程一直持续到通过所有层后确定输出为止。
感知器(线性和非线性)和径向基函数网络是前馈网络的例子。事实上,单层感知器网络是最基本的神经网络类型。它有一个单层的输出节点,输入通过一组权重直接输入输出。每个节点计算权重和输入的乘积的总和。这种神经网络结构是第一个也是最基本的架构之一。
Illustration of the Perceptron basic Model
使用反向传播技术在多层前馈神经网络上进行学习。为每个训练样本生成的属性由输入来激励。隐藏层同时被输入输入层的加权输出。隐藏层的加权输出可以用作附加隐藏层的输入,等等。许多隐藏层的使用是任意的;通常,只有一个用于基本网络。
构成输出层的单元使用最终隐藏层的加权输出作为输入,以扩展给定样本的网络预测。由于它们的符号生物成分,隐藏层和输出层中的单元被描述为神经元或输出单元。
卷积神经网络(CNN)是前馈架构最知名的迭代之一。它们通过使用线性代数的概念,特别是矩阵乘法来识别图像中的模式,为图像分类和对象识别任务提供了一种更具可扩展性的技术。
下面是一个对手写数字进行分类的 CNN 架构的例子
An Example CNN architecture for a handwritten digit recognition task (source)
通过使用相关的滤波器,CNN 可以有效地捕捉图像中的空间和时间相关性。因为需要考虑的因素较少,并且权重可以重复使用,所以该架构提供了对图像数据集的更好拟合。换句话说,网络可以被训练以更好地理解图像的复杂程度。
前馈神经网络是如何训练的?
这种网络的典型算法是反向传播。它是一种基于前一个时期(即迭代)中记录的错误率来调整神经网络权重的技术。通过适当调整权重,您可以降低错误率,并通过扩大模型的适用性来提高模型的可靠性。
单个权重的损失函数的梯度通过使用链规则的神经网络反向传播算法来计算。与本机直接计算相比,它一次有效地计算一个层。虽然它计算渐变,但它并不指定如何应用渐变。它扩大了德尔塔规则的计算范围。
Illustration of back-propagation algorithm
反馈神经网络的结构
反馈网络,如递归神经网络 ( RNN ),具有反馈路径,允许信号使用环路在两个方向上传播。神经元的连接可以用任何方式进行。由于这种网络包含环路,它转化为一个非线性动态系统,在训练过程中不断进化,直到达到平衡状态。
在研究中,RNN 是最突出的反馈网络类型。它们是一种人工神经网络,沿着时间序列将节点之间的连接形成有向或无向图。因此,它可以显示时间动态行为。rnn 可以通过使用它们的内部状态来处理不同长度的输入序列,内部状态可以表示一种形式的存储器。因此,它们可用于语音识别或手写识别等应用。
Example of feed-back neural network
如何训练一个反馈神经网络?
穿越时间的反向传播或 BPTT 是这类网络的常用算法。这是一种基于梯度的方法,用于训练特定的递归神经网络类型。并且,它被认为是前馈网络的反向传播的扩展,具有对反馈网络中存在的递归的适应。
CNN vs RNN
正如已经提到的,CNN 不是像 RNN 那样建造的。rnn 将结果发送回网络,而 CNN 是采用过滤器和池层的前馈神经网络。
在应用方面,细胞神经网络经常被用来模拟涉及空间数据的问题,如图像。当处理时间序列数据时,如文本或图像序列,rnn 表现更好。
这些差异可以归纳在下表中:
卷积神经网络 | 递归神经网络 | |
---|---|---|
体系结构 | 前馈神经网络 | 反馈神经网络 |
布局 | 多层节点,包括 | |
卷积层 | 信息向不同方向流动,模拟记忆效应 | |
数据类型 | 图像数据 | 序列数据 |
输入/输出 | 输入和输出 | |
的大小是固定的(即输入图像用 | ||
固定大小,输出图像用 | ||
分类) | 输入和输出的大小可能不同(例如,接收不同的文本和生成不同的翻译) | |
用例 | ||
图像分类,识别,医学影像,图像分析, | ||
人脸检测 | 文本翻译,自然语言处理, | |
语言翻译,情感分析 | ||
缺点 | 大量训练数据 | 缓慢而复杂的训练程序 |
描述 | CNN 采用神经元连接模式。而且,它们的灵感来自动物视觉皮层中单个神经元的排列,这使它们能够对视野的重叠区域做出反应。 | 递归神经网络使用时间序列信息。例如,用户之前说的话可能会影响模型对他接下来会说什么的预测。 |
架构示例
AlexNet
被称为 AlexNet 的卷积神经网络(CNN)架构由 Alex Krizhevsky 创建。八层组成了 AlexNet 前五层是卷积层,其中一些是最大池层,最后三层是全连接层。它利用了不饱和 ReLU 激活函数,在训练效率上胜过 tanh 和 sigmoid 。被认为是计算机视觉领域最有影响力的研究之一, AlexNet 引发了大量使用 CNN 和 GPU 加速深度学习的进一步研究的发表。事实上,根据 F 的说法,截至 2022 年,AlexNet 出版物已收到超过 69,000 次引用。
AlexNet Architecture with Pyramid Pooling and Supervision (source)
LeNet
Yann LeCun 提出了称为 LeNet 的卷积神经网络拓扑。首批卷积神经网络之一 LeNet-5 有助于深度学习的进步。LeNet 是第一个卷积神经网络的原型,拥有卷积神经网络的基本组件,包括卷积层、池层和完全连接层,为其未来的发展奠定了基础。LeNet-5 由七层组成,如图所示。
Structure of LeNet-5 (source)
长短期记忆(LSTM)
LSTM 网络是 RNNs 的突出例子之一。除了单个数据点之外,这些架构还可以分析完整的数据序列。例如,LSTM 可以用来执行任务,如未分段的笔迹识别,语音识别,语言翻译和机器人控制。
Long Short Term Memory (LSTM) cell (source)
LSTM 网络是由细胞构成的(见上图),LSTM 细胞的基本组成部分一般是:遗忘门,输入门,输出门和细胞状态。
门控循环单元(GRU)
这个 RNN 导数与 LSTMs 相当,因为它试图解决表征 RNN 模型的短期记忆问题。GRU 的参数比 LSTM 少,因为它没有输出门,但它类似于带遗忘门的 LSTM。人们发现,GRU 和 LSTM 在一些音乐建模、语音信号建模和自然语言处理任务上表现相似。GRUs 已经在几个较小的、不太频繁的数据集上展示了优越的性能。
Diagram of the gated recurrent unit cell (Source)
用例
根据应用的不同,前馈结构可能更适合某些型号,而反馈设计可能更适合其他型号。这里有几个选择一种架构而不是另一种架构的例子。
预测货币汇率
在对日元汇率建模的研究中,尽管应用起来非常简单明了,但样本外数据的结果表明,前馈模型在预测价格水平和价格方向方面相当准确。事实上,前馈模型优于递归网络预测性能。这可能是因为反馈模型必须从后向前和从向前向后传输数据,而反馈模型经常会出现混乱或不稳定。
部分遮挡物体的识别
人们普遍认为前馈处理用于物体识别。针对被遮挡刺激的周期性自上而下连接可能能够重建输入图像中丢失的信息。法兰克福高等研究院的研究人员研究了这个话题。他们已经证明,对于遮挡对象检测,递归神经网络架构表现出显著的性能改进。《认知神经科学杂志》的另一篇文章报道了同样的发现。由作者进行的实验和模型模拟强调了前馈视觉的局限性,并认为物体识别实际上是一个高度互动的动态过程,依赖于几个大脑区域的合作。
图像分类
在某些情况下,当结合适当的训练方法时,简单的前馈结构优于递归网络。例如,ResMLP,一种仅基于多层感知器的图像分类架构。一个研究项目展示了这种结构在与数据高效训练一起使用时的性能。已经证明,具有残差块的简单残差架构由具有单个隐藏层和线性补丁交互层的前馈网络组成,如果与现代训练方法(如为基于变压器的架构引入的方法)一起使用,则该架构可以在 ImageNet 分类基准上表现得令人惊讶地好。
文本分类
如前所述,rnn 是解决文本分类问题最成功的模型。在研究中提出了三种不同的信息共享策略,用共享层和特定任务层来表示文本。所有这些任务都是在整个网络上联合训练的。在四个基准文本分类任务上的实验表明,所提出的 RNN 模型具有较高的文本分类性能。
另一篇论文中提出了一种基于 LSTM 的文本数据情感分类方法。这种 LSTM 技术展示了情感分类的性能,准确率为 85%,这被认为是情感分析模型的高准确度。
教程
在 Paperspace 中,为 CNN 和 rnn 发布了许多教程,我们在此列表中提供了一个简短的选择来帮助您入门:
在本教程中,我们使用 CNN 结构的 PyTorch 实现来定位输入端图像中给定对象的位置。
在这篇文章中,我们提出了一个 R-CNN 的实现,使用库 Keras 来制作一个对象检测模型。
而在本文中,我们使用 Keras 实现了一个名为 Seq2Seq 的模型,这是一个用于文本摘要的 RNN 模型。
然后,在这个双向 RNN 的实现中,我们使用库 Keras 建立了一个情感分析模型。
结论
简单来说,解决各种挑战需要不同的工具。当你第一次开始使用机器学习时,理解并描述你试图解决的问题是至关重要的。要有足够的能力自己建造东西需要大量的练习,因此增加这方面的知识将有助于实施程序。
在这篇文章中,我们研究了前馈和反馈神经网络拓扑结构之间的差异。然后,我们探讨了这些架构的两个例子,它们推动了人工智能领域的发展:卷积神经网络(CNN)和递归神经网络(RNNs)。然后,我们给出了每个结构的例子以及真实世界的用例。
资源
https://link.springer.com/article/10.1007/BF00868008
https://arxiv.org/pdf/2104.10615.pdf
https://dl . ACM . org/doi/10.1162/jocn _ a _ 00282
https://arxiv.org/pdf/2105.03404.pdf
https://www.ijcai.org/Proceedings/16/Papers/408.pdf
https://www . ijert . org/research/text-based-opinion-analysis-using-lstm-ijertv 9 is 050290 . pdf
你需要知道的关于少量学习的一切
深度学习模型在图像分类、语义分割、对象检测等计算机视觉任务中的成功。归功于利用了用于训练网络的大量标记数据——一种称为监督学习的方法。尽管在这个信息技术时代,大量的非结构化数据是可用的,但是带注释的数据很难获得。
由于这个原因,数据标记占用了计算机视觉机器学习项目的大部分时间,并且也是一项昂贵的工作。此外,在医疗保健等领域,只有专家医生才能对数据进行分类——例如,看看下面两幅宫颈细胞学图像——你能肯定地说哪一幅是癌性的吗?
Source: SIPaKMeD Dataset
大多数未经训练的医疗专业人员不会知道答案——(a)是癌性的,而(b)是良性的。因此,在这种情况下,数据标记更加困难。在最好的情况下,我们将只有少数带注释的样本,这远远不足以训练监督学习模型。
此外,随着时间的推移,更新的数据可能会逐渐变得可用——比如,当新发现的鸟类物种的数据变得可用时。在大数据集上训练深度神经网络会消耗大量计算能力(例如,ResNet-200 在 8 个 GPU 上训练大约需要三周时间)。因此,在大多数情况下,必须重新训练模型以适应新的可用数据是不可行的。
这就是相对较新的少量学习概念的由来。
什么是少投学习?
少数镜头学习(FSL)是一种机器学习框架,它使预训练模型能够在每个类中仅使用几个标记样本的情况下,对新类别的数据(预训练模型在训练期间没有见过的)进行归纳。它属于元学习的范式(元学习就是学会学习)。
General overview of a Few-Shot Learning framework. Image by the author
我们,人类,能够利用我们以前学到的知识,仅仅通过几个例子,就很容易地识别出新的数据类别。FSL 也打算效仿。这叫元学习,举个例子可以更好理解。
假设你第一次去一个充满异国情调的动物园,你看到了一只你从未见过的特殊的鸟。现在,给你一套三张卡片,每张包含两张不同鸟类的图片。通过看到卡片上每个物种和动物园里的鸟的图像,你将能够利用羽毛的颜色、尾巴的长度等信息很容易地推断出鸟的种类。在这里,您通过使用一些辅助信息自己了解了这种鸟的种类。这就是元学习试图模仿的。
与少投学习相关的重要术语
让我们讨论几个与 FSL 文学相关的常见术语,这将有助于对该主题的进一步讨论。
支持集:支持集由每个新数据类别的少量标记样本组成,预训练模型将使用这些样本对这些新类别进行归纳。
查询集:查询集由新旧数据类别的样本组成,模型需要使用以前的知识和从支持集获得的信息对这些样本进行归纳。
N 向 K-shot 学习方案:这是 FSL 文献中使用的一个常用短语,它本质上描述了一个模型将要处理的少数镜头问题陈述。“ N -way”表示有“ N 个预训练模型需要概括的小说类别。更高的“ N ”值意味着更困难的任务。" K "-shot 为每个" N "小说类定义支持集中可用的标记样本数。随着“ K 值的降低,少量拍摄任务变得更加困难(即,更低的精度),因为可用于做出推断的支持信息更少。
" K 值通常在 1 到 5 的范围内。 K=1 任务被命名为“一次性学习”,因为它们特别难以解决。我们将在本文后面讨论它们。 K=0 也是可以的,这就是所谓的“零拍学习”零镜头学习与所有其他少镜头学习方法有很大不同(因为它属于无监督学习范式)。因此,我们不会在本文中讨论它们。
为什么少拍学习?
传统的监督学习方法使用大量的标记数据进行训练。此外,测试集包括的数据样本不仅属于与训练集相同的类别,而且必须来自相似的统计分布。例如,由手机拍摄的图像创建的数据集在统计上不同于由高级 DSLR 相机拍摄的图像创建的数据集。这就是通常所说的域转移。
少量学习通过以下方式缓解了上述问题:
- 训练模型不需要大量昂贵的标记数据,因为顾名思义,目的是仅使用少量标记样本进行概化。
- 由于预训练模型(已经在大量数据集上训练的模型,例如,在 ImageNet 上)被扩展到新的数据类别,因此不需要从头开始重新训练模型,这节省了大量计算能力。
- 使用 FSL,模型还可以了解只有有限先验信息的稀有数据类别。例如,来自濒危或新识别的动物/植物物种的数据是稀缺的,这将足以训练 FSL 模型。
- 即使模型已经使用统计上不同的数据分布进行了预训练,它也可以用于扩展到其他数据域,只要支持集和查询集中的数据是一致的。
少镜头学习是如何工作的?
传统的少镜头框架的主要目标是学习相似性函数,该函数可以映射支持集和查询集中的类之间的相似性。相似性函数通常输出相似性的概率值。
An ideal scenario for a similarity measure in Few-Shot Learning. Image by the author
例如,在下图中,当比较两张猫的图像(I1 和 I2)时,完美的相似性函数应该输出值 1.0。对于另外两种情况,猫的图像与豹猫的图像进行比较,相似性输出应该是 0.0。然而,这是一个理想的场景。实际上,I1 和 I2 的值可能是 0.95,其他两种情况下可能是一个大于 0 的小值(如 0.02 和 0.03)。
Overview of how a Few-Shot model makes a prediction. Image by the author
现在,我们使用大规模标记数据集来训练这样一个相似度函数的参数。用于以受监督的方式预训练深度模型的训练集可以用于此目的。一旦训练了相似性函数的参数,就可以在少量学习阶段中使用它,以通过使用支持集信息来确定查询集上的相似性概率。然后,对于每个查询集样本,具有来自支持集的最高相似性的类将通过少镜头模型被推断为类标签预测。上面举例说明了一个这样的例子。
暹罗网络
在少数镜头学习文献中,相似性函数根本不需要是“函数”。它们也可以,而且通常会是神经网络:其中一个最流行的例子是暹罗网络。这个名字来源于“连体双胞胎”身体相连的事实。不像传统的神经网络有一个输入分支和一个输出分支,暹罗网络有两个或三个输入分支(基于训练方法)和一个输出分支。
有两种方法来训练暹罗网络,我们将在下面讨论:
方法-1:成对相似度
在这种方法中,给定一个连体网络的两个输入及其相应的标签(采用用于预训练特征提取器的训练集)。这里,首先,我们从数据集中随机选择一个样本(比如说,我们选择一只狗的图像)。然后,我们再次从数据集中随机选择一个样本。如果第二个样本与第一个样本属于同一类,也就是说,如果第二个图像也是一只狗,那么我们将标签“1.0”指定为暹罗网络的基础事实。对于所有其他类别,标签“0.0”被指定为基本事实。
Overview of Learning Pairwise Similarity in Siamese Networks. Image by the author
因此,这个网络本质上通过标记的例子学习相似性匹配标准。上面已经用一个例子说明了这一点。图像首先分别通过相同的预先训练的特征提取器(典型地,卷积神经网络)以获得它们相应的表示。然后,两个获得的表示被连接并通过密集层和 sigmoid 激活函数以获得相似性得分。我们已经知道样本是否属于同一个类别,因此该信息被用作计算损失和计算反向传播的基本事实相似性得分。
在 Python3 中,两个样本之间的余弦相似性可以使用以下公式计算:
import torch
import torch.nn as nn
input1 = torch.randn(100, 128)
input2 = torch.randn(100, 128)
cos = nn.CosineSimilarity(dim=1, eps=1e-6)
output = cos(input1, input2)
为了获得图像和关于它们是否属于同一个类的相应信息,需要在 Python3 中实现以下自定义数据集:
import random
from PIL import Image
import torchvision
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
import torchvision.utils
import torch
import torch.nn as nn
class SiameseDataset(Dataset):
def __init__(self,folder,transform=None):
self.folder = folder #type: torchvision.datasets.ImageFolder
self.transform = transform #type: torchvision.transforms
def __getitem__(self,index):
#Random image set as anchor
image0_tuple = random.choice(self.folder.imgs)
random_val = random.randint(0,1)
if random_val: #If random_val = 1, output a positive class sample
while True:
#Find "positive" Image
image1_tuple = random.choice(self.folder.imgs)
if image0_tuple[1] == image1_tuple[1]:
break
else: #If random_val = 0, output a negative class sample
while True:
#Find "negative" Image
image1_tuple = random.choice(self.folder.imgs)
if image0_tuple[1] != image1_tuple[1]:
break
image0 = Image.open(image0_tuple[0])
image1 = Image.open(image1_tuple[0])
image0 = image0.convert("L")
image1 = image1.convert("L")
if self.transform is not None:
image0 = self.transform(image0)
image1 = self.transform(image1)
#Return the two images along with the information of whether they belong to the same class
return image0, image1, int(random_val)
def __len__(self):
return len(self.folder.imgs)
方法-2:三重态丢失
这种方法基于“三重损失”标准,可视为方法 1 的扩展,尽管这里使用的训练策略不同。首先,我们从数据集(训练集)中随机选择一个数据样本,我们称之为“锚”样本。接下来,我们选择另外两个数据样本——一个来自与锚样本相同的类——称为“正”样本,另一个来自与锚不同的类——称为“负”样本。
一旦这三个样本被选择,它们通过相同的神经网络以获得它们在嵌入空间中的相应表示。然后,我们计算锚和正样本表示之间的 L2 归一化距离(姑且称之为“d+”)以及锚和负样本嵌入之间的 L2 归一化距离(姑且称之为“d-”)。这些参数允许我们定义需要最小化的损失函数,如下图所示。
Overview of the Triplet Loss method of Siamese Network Training. Image by the author
这里,“> 0”是防止 max 函数的两项不相等的余量。这里的目的是在嵌入空间中尽可能远地推送锚样本和负样本的表示,同时尽可能近地拉取锚样本和负样本的表示,如下所示。
Example of how representations of data samples in the embedding space are aligned. Image by the author
使用 PyTorch,可以非常容易地实现三重态损失,如下所示(使用随机锚、正样本和负样本的示例):
import torch
import torch.nn as nn
triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
output.backward()
要使用 PyTorch 从影像数据集中生成锚定样本、阳性样本和阴性样本,需要编写以下自定义数据集类:
import random
from PIL import Image
import torchvision
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
import torchvision.utils
import torch
import torch.nn as nn
class SiameseDataset(Dataset):
def __init__(self,folder,transform=None):
self.folder = folder #type: torchvision.datasets.ImageFolder
self.transform = transform #type: torchvision.transforms
def __getitem__(self,index):
#Random image set as anchor
anchor_tuple = random.choice(self.folder.imgs)
while True:
#Find "positive" Image
positive_tuple = random.choice(self.folder.imgs)
if anchor_tuple[1] == positive_tuple[1]:
break
while True:
#Find "negative" Image
negative_tuple = random.choice(self.folder.imgs)
if anchor_tuple[1] != negative_tuple[1]:
break
anchor = Image.open(anchor_tuple[0])
positive = Image.open(positive_tuple[0])
negative = Image.open(negative_tuple[0])
anchor = anchor.convert("L")
positive = positive.convert("L")
negative = negative.convert("L")
if self.transform is not None:
anchor = self.transform(anchor)
positive = self.transform(positive)
negative = self.transform(negative)
return anchor, positive, negative
def __len__(self):
return len(self.folder.imgs)
上面的代码可以很容易地在渐变中实现。只需打开一个笔记本,在“高级选项”的“工作区 URL”字段中填入以下 URL:
少镜头学习方法
一次性学习方法可以大致分为四类,我们接下来将讨论这四类:
数据级
数据级 FSL 方法有一个简单的概念。如果 FSL 模型训练由于缺乏训练数据而受阻(并且为了防止过拟合或欠拟合),可以添加更多的数据——这些数据可能是结构化的,也可能不是结构化的。也就是说,假设我们在支持集中每个类有两个标记样本,这可能不够,所以我们可以尝试使用各种技术来增加样本。
尽管数据扩充本身并不能提供全新的信息,但它仍然有助于 FSL 训练。另一种方法可以是将未标记的数据添加到支持集,使得 FSL 问题是半监督的。FSL 模型甚至可以使用非结构化数据来收集更多信息,这已被证明可以提高少量拍摄的性能。
其他方法也旨在使用生成网络(如 GAN 模型)从现有的数据分布中合成全新的数据。然而,对于基于 GAN 的方法,需要大量的标记训练数据来训练模型的参数,然后才能使用少量的支持集样本来生成新的样本。
参数级
在 FSL,样品是有限的;因此,过拟合是常见的,因为样本具有广泛的高维空间。参数级 FSL 方法涉及元学习的使用,元学习控制模型参数的利用,以智能地推断哪些特征对于手头的任务是重要的。
约束参数空间并使用正则化技术的 FSL 方法属于参数级方法的范畴。模型被训练以在参数空间中找到最佳路线,从而提供目标预测。
公制级别
度量级 FSL 方法旨在学习数据点之间的距离函数。从图像中提取特征,并且在嵌入空间中计算图像之间的距离。该距离函数可以是欧几里德距离、推土机距离、基于余弦相似性的距离等。这是我们在讨论暹罗网络时讨论过的内容。
这些方法使得距离函数能够使用已经用于训练特征提取器模型的训练集数据来调整其参数。然后,距离函数将基于支持集和查询集之间的相似性得分(样本在嵌入空间中有多接近)进行推断。
基于梯度的元学习
基于梯度的元学习方法使用两个学习者——教师模型(基础学习者)和使用知识提炼的学生模型(元学习者)。教师模型通过高维参数空间引导学生模型。
使用来自支持集的信息,教师模型首先被训练来对查询集样本进行预测。从教师模型导出的分类损失然后被用于训练学生模型,使其精通分类任务。
一次性学习
正如到目前为止的讨论所建议的,一次学习是这样一个任务,其中支持集只由每个类的一个数据样本组成。你可以想象在支持信息较少的情况下,任务会更加复杂。现代智能手机中使用的人脸识别技术使用一次性学习。
一个这样的例子是 Shaban 等人在这篇论文中探索的一次性语义分割方法。作者提出了一种新的两个分支的方法,其中第一个分支将标记的图像作为输入,并产生一个参数向量作为输出。第二个分支将这些参数以及新图像作为输入,并为新类别产生图像的分割掩模作为输出。他们的架构图如下所示。
Source: Paper
与单次学习的微调方法不同,单次学习可能需要多次迭代来学习分割网络的参数,该网络的第一个分支在单次正向传递中计算参数。这具有几个优点:1)单次正向传递使得所提出的方法快速;2)一次性学习的方法是完全可微分的,允许分支与网络的分段分支联合训练;3)最后,参数的个数与图像的大小无关,所以一次法在缩放方面没有问题。
少镜头学习的应用
在深度学习文献中,少镜头学习已经广泛用于多个领域,从图像分类和对象检测等计算机视觉任务到遥感、自然语言处理等。让我们在本节中简要讨论一下它们。
图像分类
少数镜头学习已广泛用于图像分类,其中一些例子,我们已经探讨。
张等人在他们的论文中提出了一种有趣的少镜头图像分类方法。比较两个复杂结构化表示的自然方法是比较它们的构建块。困难在于我们没有他们相应的培训监督,而且不是所有的建筑构件都能在其他结构中找到它们的对应部分。为了解决上述问题,本文将少镜头分类形式化为最佳匹配的一个实例。作者建议使用两个结构之间的最佳匹配成本来表示它们的相似性。
Source: Paper
给定两幅图像产生的特征表示,作者采用推土机距离(EMD)来计算它们的结构相似性。EMD 是计算结构表示之间距离的度量,最初是为图像检索提出的。给定所有元素对之间的距离,EMD 可以获得具有最小成本的两个结构之间的最佳匹配流。它也可以解释为用另一个重建结构表示的最小成本。
物体检测
目标检测是在图像或视频序列中识别和定位目标的计算机视觉问题。单个图像可以包含多个对象。因此,它不同于简单的图像分类任务,其中整个图像被赋予一个类别标签。
这篇论文提出了开放式中心网(ONCE)模型来解决增量少镜头检测目标检测的问题。作者采取了基于特征的知识转移策略,将之前名为 CentreNet 的模型分解为类通用和类专用组件,以实现增量式少量学习。更具体地说,ONCE 首先使用丰富的基类训练数据来训练类通用特征提取器。接下来是元学习,这是一个特定于类的代码生成器,具有模拟的少量学习任务。一旦被训练,给定新对象类的少量图像,元训练的类代码生成器优雅地使一次检测器能够在元测试阶段(新类注册)以有效的前馈方式递增地学习新类。
Source: Paper
语义分割
语义分割是一项任务,其中图像中的每个像素被分配一个类别——一个或多个对象,或者背景。在文献中,少镜头学习已经被用于执行二元和多标签语义分割。
刘等人在这篇论文中提出了一个新颖的基于原型的半监督少镜头语义切分框架,其主要思想是在两个方向上丰富语义类的原型表示。首先,他们将常用的整体原型表示分解为一小组部分感知的原型,这些原型能够捕捉不同的细粒度对象特征,并在语义对象区域中产生更好的空间覆盖。此外,作者将一组未标记的图像合并到他们的支持集,以便可以从标记和未标记的数据源中学习零件感知原型。这使得它们能够超越受限的小支持集,并更好地模拟对象特征的类内变化。他们的模型概述如下所示。
Source: Paper
机器人技术
机器人领域还使用了少量学习方法,使机器人能够模仿人类的能力,仅使用少量演示来概括任务。
为了减少学习中涉及的试验次数,吴等人在这篇论文中提出了一种算法来解决模仿中的“如何做”问题。作者介绍了一种通过模仿学习路径规划的新计算模型,该模型使用了计划适应中的基本思想——在演示和给定情境中存在不变的特征点——来为新场景生成运动路径。
自然语言处理
少数镜头学习最近在自然语言处理任务中也变得流行,其中语言处理的标签本来就很难获得。
例如,Yu 等人在他们的论文中,使用少数镜头学习,特别是度量学习方法来解决文本分类问题。他们的元学习者使用元训练任务上的任务聚类来选择和组合用于学习目标任务的多个度量。在元训练过程中,作者提出将元训练任务划分成簇,使得每个簇中的任务可能是相关的。
然后在每个聚类中,作者训练一个深度嵌入函数作为度量。这确保了公共指标只在同一个集群中的任务之间共享。此外,在元测试期间,每个目标 FSL 任务被分配给特定于任务的度量,该度量是由不同集群定义的度量的线性组合。通过这种方式,多样化的少数镜头任务可以从以前的学习经验中获得不同的度量。
结论
深度学习已经成为解决复杂的计算机视觉和模式识别任务的事实上的选择。然而,对大量标记训练数据的需求和训练深度架构所产生的计算成本阻碍了此类任务的进展。
少数镜头学习是这个问题的一个变通方法,允许预训练的深度模型扩展到新的数据,只有几个标记的例子,没有重新训练。由于其可靠的性能,像图像分类和分割、对象识别、自然语言处理等任务。看到了 FSL 建筑的使用的兴起。
对更好的 FSL 模型的研究仍在积极进行,以使它们与完全监督的学习方法一样准确,甚至更好。像一次性或零次学习这样明显更复杂的问题正在被广泛研究,以弥合人工智能和人类学习者之间的差距。
用人工智能对抗冠状病毒,第 1 部分:用深度学习和计算机视觉改进测试
原文:https://blog.paperspace.com/fighting-corona-virus-with-ai-medical-imaging-testing/
上面的引文来自《搏击俱乐部》一书,该书还被拍成电影,由布拉德·皮特、爱德华·诺顿和海伦娜·伯翰·卡特主演。1996 年这本书出版时,我只有一岁。主角泰勒·德登讲述了当代人是如何陷入危机的,因为他们没有见证一个将他们的生活一分为二的标志性事件,即前的和后的。在某种程度上,像我这样的千禧一代也是如此,直到新型冠状病毒疫情袭击了我们。
现在让我说清楚。在过去的几十年里,经济并不是没有衰退过(例如津巴布韦、委内瑞拉)。我也不否认世界上的一些地方面临着已经夺去了成千上万人生命的流行病(SARS,埃博拉病毒,等等)。).但是几乎所有这些都被限制在世界上的某些地区,所以其他地区可以来救援。冠状病毒给整个世界带来的灾难——无论是不堪重负的医疗系统、数百万人被封锁、儿童失学还是经济遭受重创——在很大程度上是前所未有的。
这是约翰·霍普金斯大学的一个实时仪表板,它记录了世界各地的病例。在这篇文章发表的时候,世界上已经有将近 90 万个病例。
虽然泰勒·德登倡导建立一个业余搏击俱乐部,让人们互相殴打,以此来应对他们存在的愤怒,但人工智能和数据科学的进步让我们许多人有机会帮助人类击败冠状病毒。当然,后一种选择也更好,因为前者不太符合社交距离的概念。
这个系列讲的是什么?
与我之前完成的系列不同(涵盖 GauGAN 、从头实现 YOLO、边界框数据增强等等),这个系列将如何发展更加开放。就在我们说话的时候,关于如何利用人工智能对付冠状病毒的研究正在进行中。在这一点上,公共领域中关于新冠肺炎的医学数据(比如说病人的 CT 扫描)充其量也是不足的。当然,出于隐私考虑,医疗数据可能需要一段时间才能进入公众视野。
也就是说,我们看到在几个领域出现了利用人工智能应对冠状病毒的研究。其中包括:
- 使用基于计算机视觉的深度学习作为工具,在给定患者肺部 CT 扫描的情况下,帮助诊断新冠肺炎。
- 试图提出使用基于深度学习的蛋白质折叠解决方案的治疗方法。
- 使用自然语言处理从大量关于新冠肺炎的文献中提取有意义的见解。
- 使用计算机视觉辅助监视来监控人群,例如,加强社交距离。
Medical imaging has been a hot topic in computer vision research
本系列的想法是研究这样的研究途径,如果可能的话,为您提供玩具示例,以便您可以在数据可用时开始解决问题。
我们将在本文中讨论的内容
在这篇文章中,我将谈论:
- 测试在解决疫情问题中的重要性。
- 当前测试套件面临的挑战。
- 几篇新论文概述了深度学习如何用于从 CT 扫描中诊断新冠肺炎
- 如何解释深度学习诊断系统的结果,以及一些注意事项。
首先,免责声明...
在我继续说下去之前,让我说我不是医学或放射学专业人士。我的经验是机器学习、深度学习和计算机科学。无论我写什么,都来自于我对医学成像有限的研究,如果我写了一些不正确的东西,而你恰好是专业人士,请点击下面的评论部分,让我知道,以便我可以修复它。
测试。测试。测试。
这些是来自世界卫生组织(世卫组织)的人。在他们的一次新闻发布会上,中间的那个家伙说,他对各国的首要建议是尽可能多的人进行测试,测试,测试,测试,如果可能的话,对每个有症状的人进行冠状病毒测试,无论旅行或接触史如何。
当谈到冠状病毒时,测试变得非常重要,因为你可能在 5-14 天内(这被称为潜伏期)不会出现症状。在这个时候,如果你不隔离自己,你会把病毒传播给你接触的人。
RT-聚合酶链式反应测试
目前用于检测新冠肺炎患者的金标准是一种叫做逆转录聚合酶链式反应测试、T3 或 RT-PCR 的方法。它包括从一个人的鼻子或喉咙取一个拭子,然后把它送到一台机器上检查病毒的存在。然而,RT-PCR 方法并非没有缺点。
- 这些拭子样本需要被运送到 RT-PCR 机器所在的检测中心。如果时间太长,病毒可能会死亡,受感染病人的拭子可能会变成阴性。
- 它们很贵。
- 已经观察到,RT-聚合酶链式反应测试具有低灵敏度,即给出假阴性的可能性高。这就是为什么政府经常做多重测试来确认。
- 他们也给出了很多假阳性。这是有问题的,因为你可能会让那些实际上没有感染病毒的人给你的卫生系统带来不必要的负担。
如果你仍然不相信,样本收集可能需要把一个棉签塞到你的鼻子里很远,以至于感觉它在接触你的大脑。大家都是这么形容的。不开玩笑。
If this doesn't motivate you to come up with a better way to test for corona, nothing will.
CT 扫描
对从武汉医院收集的数据的研究表明,在感染者中,当涉及到新冠肺炎的诊断时,CT 扫描具有比 RT-PCR 高得多的灵敏度。根据发表在放射学上的对 1014 名新冠肺炎患者进行的研究,RT-PCR 只能将 601/1014 (59 %)名患者标记为阳性,而 CT 扫描将 888/1014 (88%)名患者标记为阳性。
“结果显示,601 名患者(59%)的 RT-PCR 结果呈阳性,888 名患者(88%)的胸部 CT 扫描呈阳性。基于 RT-PCR 阳性结果,胸部 CT 提示新冠肺炎的敏感性为 97%。在 RT-PCR 结果为阴性的患者中,75%的患者(413 例患者中的 308 例)胸部 CT 检查结果为阳性。其中,48%被认为是极有可能的病例,33%被认为是可能的病例。通过对一系列 RT-PCR 检测和 CT 扫描的分析,从最初的阴性到阳性 RT-PCR 结果之间的间隔为 4 到 8 天。”
这项研究报告称,CT 扫描的灵敏度约为 97 %,而 RT-PCR 的灵敏度约为 71%。
基于这些发现,我们有理由相信使用 CAT 扫描进行诊断是有益的。然而,如果进行 CAT 扫描,你仍然需要一名合格的放射科医生来确认新冠肺炎的存在。不幸的是,放射科医生数量有限,时间也很紧迫,尤其是在病例像意大利那样激增的情况下。
新冠肺炎的特色
这是一个健康病人的 CT 扫描图。
A few Lung Nodules are normal
现在让我们来看一下一位患者的 CT 扫描,他患有由新冠肺炎病毒引起的肺炎。
Notice the "ground glass"-like opacity marked by the arrow on the left. We see there are a couple of such opacities on the periphery of the left lung.
一般来说,肺炎会导致肺部积液,表现为肺部阴影。关于应用深度学习使用 ct 扫描来诊断新冠肺炎的文献确实指出了在新冠肺炎的肺部 CT 扫描中发现的与由不同原因导致的其他类型的肺炎相比的一些独特的特征。
一篇名为 深度学习系统筛查冠状病毒疾病 2019 肺炎 的论文,列举了三个这样的特征:
"...毛玻璃样外观,沿胸膜呈明显的周边分布,通常一个病例有多个独立的感染病灶
- 第一种意味着肺部阴影看起来像毛玻璃。
- 第二意味着这些阴影的大部分发生在肺的边缘。
- 第三意味着我们可以有不止一个这样的不透明集群。
所有这三个特征都可以在上面肺部的 CT 扫描图像中观察到。
如果你想了解更多关于新冠肺炎患者 CT 扫描的特征,这里有一篇精彩的文章涉及这个话题。
使用深度学习检测冠状病毒
在过去的几年中,计算机视觉在医学成像领域的应用激增,以诊断各种疾病。例子包括斯坦福的 CheXNet 用于从肺部 x 光诊断肺炎,从视网膜图像预测心血管风险因素,以及皮肤癌分类。
同样,最近发表的一些研究论文基本上都是通过 CT 扫描来预测一个人是否患有新冠肺炎。为了让自己保持更新,你可以随时查看谷歌学者。
CT 扫描数据
CT 扫描(或计算机断层扫描)是使用一种机器完成的,在这种机器中,扫描仪绕着你的身体移动,以创建你的器官的三维模型。这是 CT 扫描仪如何工作的演示。
https://www.youtube.com/embed/l9swbAtRRbg?feature=oembed
所以,在这种情况下,你的数据要么是...
- 一个三维体积,你必须使用一个三维卷积网。
- 多个横截面切片,对此我们可以使用二维神经网络。
提取感兴趣区域(ROI)
作为第一步,所有这些方法都涉及一定量的数据预处理,以从 CT 扫描中切出感兴趣的区域。一旦这些 ROI 被提取出来,它们就会被发送到一个深度神经网络,以将病例分类为新冠肺炎或另一个类别,如细菌性肺炎、病毒性肺炎、无感染等。(论文中的“其他”类别各不相同)。
这里有一个来自论文 深度学习系统的例子,用于筛选冠状病毒疾病 2019 肺炎 ,它对 CT 扫描的二维切片起作用。
The small bounding box around a diffuse opacity in the third figure is an example of RoI.
本文首先利用图像预处理方法对肺部进行分割。然后使用基于 VNET20 的分割模型 VNET-IR-RPN17 提取感兴趣区域。该模型本身被训练用于从肺结核中提取 ROI,但是发现它对于新冠肺炎用例也足够好地工作。
然后每个 RoI 通过一个分类模型运行,在那里我们得到类概率。可以有一个或多个 ROI,给我们多个概率。然后使用一种叫做噪声或贝叶斯函数的东西将这些概率结合起来。关于这一点的更多细节可以在上面链接的论文中找到。
$ $ score = 1-\sum_{i=1}^{no. :of :rois } p _ { I } $ $
该论文还利用了这样一个事实,即对于冠状病毒来说,通常在肺部边缘周围发现不透明,这在后期阶段被用作网络的输入。关于如何精确计算这个距离的细节可以在论文中找到。
Architecture of the classification network.
网络架构受到 ResNet 的启发,关注最终分类层的局部性。
使用三维信息
在另一篇题为 冠状病毒(新冠肺炎)疫情的快速人工智能开发周期:使用深度学习 CT 图像分析进行自动检测的初步结果&患者监测 s 的论文中,作者使用 3-D U-Net 架构从扫描中提取肺部(与 VNET20 模型功能相同)。
然后这些肺部作物被神经网络分类。整个肺被传递到分类网络,不像以前的工作那样在分类之前从肺中提取 ROI。然后,作者使用 Grad-CAM 技术创建肺部的“热图”。
The red spot shows the areas of the lung that correspond most strongly to CoViD-19, whereas the blue colors show the unimportant regions.
为了对每个患者进行分类,使用了 CT 扫描的多个切片。这些扫描中的每一个都用于计算类别概率。如果大多数切片具有最高分类概率的新冠肺炎,则患者被分类为新冠肺炎阳性。(换句话说,每一片都算一票)。
除了对二维肺部作物进行分类,作者还使用现成的软件 RADLogics ,该软件可以检测三维肺部体积内的结节和小阴影。然后,来自软件的补丁和来自早期阶段的热图被组合以创建三维可视化。
The patches in red correspond to the spots picked up by the classification system, whereas the spots in green are the ones which are picked by the software.
然后,使用补片的体积来创建“电晕得分”。
女修道院
在北美放射学会最近发表的一篇论文中, 人工智能在胸部 CT 上将新冠肺炎与社区获得性肺炎区分开来,一个名为 CovNet 的架构被提议对同一 CT 扫描的多个二维切片进行处理。
从每个切片中导出特征向量。然后这些多个特征向量被最大化汇集以获得单个特征向量。这个特征向量然后被馈送到完全连接的层,这些层将病例分类为新冠肺炎、社区获得性肺炎或非肺炎。
评估结果
而评价任何冠状病毒诊断方法的结果,准确性都不够。这是因为我们测试的所有人中,只有少数人会感染病毒。世界上几乎没有哪个国家的阳性命中率超过 20%(所有测试中的阳性案例)。在这种情况下,假设我们开发了一个解决方案,将所有东西都称为负面的。从准确性的角度来看,尽管是一个完全无用的分类器,该解决方案仍有 80%的准确性。
因此,我们需要关注其他指标,例如:
灵敏度,或真阳性率。这是阳性样本总数中真正阳性的比例,或者简单地说,我们正确分类为阳性的冠状病毒感染患者的数量。敏感度太低意味着有很多人感染了我们的算法归类为阴性的病毒。这是一个特别令人担忧的缺点,因为它会让许多感染者回家,并可能传播病毒。
特异性,或真阴性率。这是真正的阴性样本占阴性样本总数的比例,或者简单地说,我们正确归类为阴性的未感染人数。特异性太低意味着我们将告诉许多没有感染病毒的人他们感染了。虽然不像低灵敏度那样令人担忧,但如果我们在系统中获得太多的假阳性,它可能会给卫生系统带来过度的压力。
人们也使用类似于 Precision 的指标(在我们诊断为阳性的所有患者中,有多少人实际上患有这种疾病;这对于衡量我们的测试资源丰富程度)和 F1 分数(结合了精确度和灵敏度)非常有用。
ROC 曲线下的面积是我们的分类器如何区分两个类别的量度。通常,分类器会给所有情况一个概率,然后我们使用一个阈值来确定结果。理想情况下,我们希望所有阳性病例的得分远高于阈值,而阴性病例的得分远低于阈值。为什么?因为如果一个例子更接近阈值,就很难对预测有信心。
例如,假设我们使用 0.5 的阈值。假设一个分类器给一个案例分配 0.9 的概率。我们可以自信地说,这是一个积极的案例。然而,考虑另一种情况为 0.52。我们不能用同样的自信说这个例子是正的。有人可能会想,如果输入有一点变化,我们可能会得到 0.48 分,我们会把同样的例子称为否定的。
AUROC 是灵敏度和假阳性率(1 -特异性)之间的图形。它只能针对二元分类情况进行计算,因此当我们有 n 个类别时,我们必须绘制 n “一个类别”对“所有其他类别”的 AUC 曲线。
1-vs-all AOC curves for results in the paper Artificial Intelligence Distinguishes COVID-19 from Community Acquired Pneumonia on Chest CT
ROC 曲线下的面积范围可以从 0 到 1,其中 1 代表完美的分类器,0.5(意味着曲线遵循线 y=x) 代表像掷硬币一样好的分类器(随机机会)。小于 0.5 的面积意味着你的分类器更差,并且经常做出不正确的预测。
如果你想深入了解 AUROC 的工作原理,这里有一个很好的资源:
不过有一个警告...
在结束这篇文章之前,我想指出我们在将深度学习工具应用于医疗用例时面临的一些挑战。
就新冠肺炎而言,在这篇文章中,我们基本上介绍了如何通过 ct 扫描检测疾病。但是说实话,你真的不能通过 ct 扫描检测出肺炎。这是因为肺炎是我们所说的“临床诊断”。放射科医生不会仅仅看 CT 扫描就宣布患者患有新冠肺炎。他们可能还会检查血检、临床病史、其他症状等。得出结论。这是因为由不同原因引起的肺炎可能会也可能不会在 ct 扫描中产生明显不同的特征。当一篇论文报告说 CT 扫描比 RT-PCR 具有更好的灵敏度时,它实际上意味着医生将 CT 扫描与其他症状和特征相结合来诊断患者的方法。
虽然这绝不是对深度学习用于诊断新冠肺炎的批评,但人们必须警惕 CheXNet 问世时的这种浮华标题。
放射科医生应该担心自己的工作吗?突发新闻:我们现在可以比放射科医生更好地通过胸部 x 光诊断肺炎。https://t.co/CjqbzSqwTx
— Andrew Ng (@AndrewYNg) November 15, 2017
用人工智能对抗冠状病毒,第 2 部分:使用 PyTorch 构建 CT 扫描新冠肺炎分类器
原文:https://blog.paperspace.com/fighting-coronavirus-with-ai-building-covid-19-classifier/
您可以跟随本教程的代码,并在 ML Showcase 的免费 GPU 上运行它。
新冠肺炎继续对世界各地的医疗系统和经济造成严重破坏。超过 50 万人死亡,1140 万人患病,超过 10 亿人失业,新冠肺炎疫情可以说是 21 世纪最大的危机。我们还见证了世界以前所未有的规模联合起来抗击疫情——无论是加快疫苗试验、大规模生产口罩和呼吸机,还是在封锁期维持国家运转的巨大经济刺激。
话虽如此,我确信机器学习社区可以发挥作用。其实这就是这个系列的全部。在最后一部分的中,我概述了深度学习是如何被用来开发更好的新冠肺炎测试方法的。我所涉及的所有文献都使用了从医院获得的医学数据,而这些数据在公共领域是不可获得的,这使得很难进行任何形式的指导。然而,这种情况已经改变。
Improving Coronavirus Testing with Deep Learning | Paperspace BlogThis post will cover how testing is done for the coronavirus, why it’s important in battling the pandemic, and how deep learning tools for medical imaging can help us improve the quality of COVID-19 testing.Paperspace BlogAyoosh KathuriaA Review of Active COVID-19 Research and Datasets | Paperspace BlogThe economy has come to a halt; people are quarantined; work is stagnating; andgovernments fear the public health crisis this could turn into. The deadlycoronavirus (SARS-CoV-2, or COVID-19 for “coronavirus disease 2019”) isspreading fast. It’s 2020 but even with the most advanced technology avai…Paperspace BlogVihar Kurama
最近,加州大学圣地亚哥分校开源了一个数据集,其中包含新冠肺炎患者的肺部 CT 扫描图像,这是公共领域的第一次。在这篇文章中,我们将使用 PyTorch 构建一个分类器,该分类器获取患者的肺部 CT 扫描,并将其分类为新冠肺炎阳性或阴性。
所以,让我们开始吧!
首先要做的是...
我们首先导入代码所需的模块,设置 GPU,并设置 TensorBoard 目录来记录我们的培训指标。
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms as transforms
from skimage.util import montage
import os
import cv2
import random
import matplotlib.pyplot as plt
import torch.optim as optim
from PIL import Image
from sklearn.metrics import classification_report, roc_auc_score, roc_curve, confusion_matrix
from torch.utils.tensorboard import SummaryWriter
import glob
import shutil
import numpy as np
from torchvision.models import vgg19_bn
import numpy as np
import seaborn as sns
random.seed(0)
log_dir = "~/logs"
writer = SummaryWriter(log_dir)
device = "cuda:0" if torch.cuda.is_available() else "cpu"
创建数据集
我们将在 GitHub 上使用由加州大学圣地亚哥分校提供的新冠肺炎 CT 扫描。该数据集包含取自各种放射学/医学杂志的图像,如 medRxiv、bioRxiv、NEJM、JAMA、Lancet。
我们首先通过克隆 GitHub repo 来获取数据。从命令行运行:
git clone https://github.com/UCSD-AI4H/COVID-CT
下载完数据后,将光盘放入COVID-CT
文件夹,提取包含图像的 zip 文件。
cd COVID-CT/Images-processed/
unzip CT_COVID.zip
unzip CT_NonCOVID.zip
#cd back to the main folder
cd ..
关于数据集
在我们开始构建分类器之前,让我记下数据的结构。我们有新冠肺炎阳性患者扫描的阳性类别,而阴性类别包含健康患者和患有可能导致肺部混浊的其他(非新冠肺炎)疾病的患者的混合物。
为了训练一个稳健的分类器,我们还必须有关于非新冠肺炎患者的信息。这很重要,因为医生从来不会直接派人去做 CT 扫描。事实上,由于肺炎是一种临床诊断,接受 ct 扫描的人很可能患有呼吸道疾病,如病毒性/细菌性肺炎/链球菌等。,已经。我们很少看到健康的病人被送去做 CT 扫描。
因此,一个实用的分类器必须区分,比如说,新冠肺炎诱发的肺炎和其他类型的肺炎。然而,该数据集中的负类是混合的,并且包含健康的肺,以及患有其他疾病(如癌症)的患者的肺。那么说这些有什么意义呢?重点是你应该把这个分类器作为一个教育目的。然而,任何你想在野外使用的分类器都需要更多的有区别的数据。
考虑到这一点,让我们来看看数据集中的一些例子。
数据集的样本图像
我们首先从新冠肺炎阳性病例开始。
covid_files_path = 'Images-processed/CT_COVID/'
covid_files = [os.path.join(covid_files_path, x) for x in os.listdir(covid_files_path)]
covid_images = [cv2.imread(x) for x in random.sample(covid_files, 5)]
plt.figure(figsize=(20,10))
columns = 5
for i, image in enumerate(covid_images):
plt.subplot(len(covid_images) / columns + 1, columns, i + 1)
plt.imshow(image)
Corona positive cases of CT scans from the dataset.
通过将变量covid_files_path
的值改为Images-processed/CT_NonCOVID
,我们同样可以看到非电晕情况的随机样本。
Non-Corona positive cases of CT scans from the dataset.
加载数据
数据集分为三部分:训练集(425 个示例)、验证集(118 个示例)和测试集(203 个示例)。文件夹Data-split
中提供了此拆分的信息。该文件夹包含文本文件,这些文件解释了哪些文件属于每个分割。
我们编写一个函数来读取这些文件,并将它们放入字符串列表中。
def read_txt(txt_path):
with open(txt_path) as f:
lines = f.readlines()
txt_data = [line.strip() for line in lines]
return txt_data
然后我们创建了COVIDCTDataset
类,它基本上是torch.utils.data.Dataset
类的子类。
class CovidCTDataset(Dataset):
def __init__(self, root_dir, classes, covid_files, non_covid_files, transform=None):
self.root_dir = root_dir
self.classes = classes
self.files_path = [non_covid_files, covid_files]
self.image_list = []
# read the files from data split text files
covid_files = read_txt(covid_files)
non_covid_files = read_txt(non_covid_files)
# combine the positive and negative files into a cummulative files list
for cls_index in range(len(self.classes)):
class_files = [[os.path.join(self.root_dir, self.classes[cls_index], x), cls_index] \
for x in read_txt(self.files_path[cls_index])]
self.image_list += class_files
self.transform = transform
def __len__(self):
return len(self.image_list)
def __getitem__(self, idx):
path = self.image_list[idx][0]
# Read the image
image = Image.open(path).convert('RGB')
# Apply transforms
if self.transform:
image = self.transform(image)
label = int(self.image_list[idx][1])
data = {'img': image,
'label': label,
'paths' : path}
return data
数据集返回包含图像张量、标签张量和批处理中包含的图像路径列表的字典。
输入预处理和数据扩充
对于训练数据:
- 在保持纵横比的同时,将图像的短边尺寸调整为 256
- 随机裁剪图像尺寸的 50%到 100%,长宽比随机从原始长宽比的 75%到 133%。最后,裁剪的大小调整为 224 × 224
- 以 0.5 的概率水平翻转图像
- 将图像归一化为平均值为 0,标准偏差为 1
用于测试:
- 将图像大小调整为 224 × 224。
- 归一化图像,使平均值为 0,标准偏差为 1
normalize = transforms.Normalize(mean=[0,0,0], std=[1,1,1])
train_transformer = transforms.Compose([
transforms.Resize(256),
transforms.RandomResizedCrop((224),scale=(0.5,1.0)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize
])
val_transformer = transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
normalize
])
定义了Dataset
和DataLoader
类后,现在让我们实例化它们。对于非 COVID 病例,我们使用标签0
,而对于 COVID 阳性病例,我们使用1
。
batchsize = 8
trainset = CovidCTDataset(root_dir='Images-processed/',
classes = ['CT_NonCOVID', 'CT_COVID'],
covid_files='Data-split/COVID/trainCT_COVID.txt',
non_covid_files='Data-split/NonCOVID/trainCT_NonCOVID.txt',
transform= train_transformer)
valset = CovidCTDataset(root_dir='Images-processed/',
classes = ['CT_NonCOVID', 'CT_COVID'],
covid_files='Data-split/COVID/valCT_COVID.txt',
non_covid_files = 'Data-split/NonCOVID/valCT_NonCOVID.txt',
transform= val_transformer)
testset = CovidCTDataset(root_dir='Images-processed/',
classes = ['CT_NonCOVID', 'CT_COVID'],
covid_files='Data-split/COVID/testCT_COVID.txt',
non_covid_files='Data-split/NonCOVID/testCT_NonCOVID.txt',
transform= val_transformer)
train_loader = DataLoader(trainset, batch_size=batchsize, drop_last=False, shuffle=True)
val_loader = DataLoader(valset, batch_size=batchsize, drop_last=False, shuffle=False)
test_loader = DataLoader(testset, batch_size=batchsize, drop_last=False, shuffle=False)
我们使用 8 的小批量。
性能指标
正如我们在第 1 部分中提到的,精确度可能不足以确定分类器的功效。因此,我们需要计算灵敏度、特异性、ROC 下面积等指标。我们编写函数compute_metrics
来计算这些度量和其他一些对以后的分析有用的量。
def compute_metrics(model, test_loader, plot_roc_curve = False):
model.eval()
val_loss = 0
val_correct = 0
criterion = nn.CrossEntropyLoss()
score_list = torch.Tensor([]).to(device)
pred_list = torch.Tensor([]).to(device).long()
target_list = torch.Tensor([]).to(device).long()
path_list = []
for iter_num, data in enumerate(test_loader):
# Convert image data into single channel data
image, target = data['img'].to(device), data['label'].to(device)
paths = data['paths']
path_list.extend(paths)
# Compute the loss
with torch.no_grad():
output = model(image)
# Log loss
val_loss += criterion(output, target.long()).item()
# Calculate the number of correctly classified examples
pred = output.argmax(dim=1, keepdim=True)
val_correct += pred.eq(target.long().view_as(pred)).sum().item()
# Bookkeeping
score_list = torch.cat([score_list, nn.Softmax(dim = 1)(output)[:,1].squeeze()])
pred_list = torch.cat([pred_list, pred.squeeze()])
target_list = torch.cat([target_list, target.squeeze()])
classification_metrics = classification_report(target_list.tolist(), pred_list.tolist(),
target_names = ['CT_NonCOVID', 'CT_COVID'],
output_dict= True)
# sensitivity is the recall of the positive class
sensitivity = classification_metrics['CT_COVID']['recall']
# specificity is the recall of the negative class
specificity = classification_metrics['CT_NonCOVID']['recall']
# accuracy
accuracy = classification_metrics['accuracy']
# confusion matrix
conf_matrix = confusion_matrix(target_list.tolist(), pred_list.tolist())
# roc score
roc_score = roc_auc_score(target_list.tolist(), score_list.tolist())
# plot the roc curve
if plot_roc_curve:
fpr, tpr, _ = roc_curve(target_list.tolist(), score_list.tolist())
plt.plot(fpr, tpr, label = "Area under ROC = {:.4f}".format(roc_score))
plt.legend(loc = 'best')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.show()
# put together values
metrics_dict = {"Accuracy": accuracy,
"Sensitivity": sensitivity,
"Specificity": specificity,
"Roc_score" : roc_score,
"Confusion Matrix": conf_matrix,
"Validation Loss": val_loss / len(test_loader),
"score_list": score_list.tolist(),
"pred_list": pred_list.tolist(),
"target_list": target_list.tolist(),
"paths": path_list}
return metrics_dict
定义模型
我们现在定义我们的模型。我们使用预训练的 VGG-19 与批量标准化作为我们的模型。然后,我们将其最终的线性层替换为输出端有 2 个神经元的层,并在我们的数据集上执行迁移学习。
我们使用交叉熵损失作为我们的目标函数。
model = vgg19_bn(pretrained=True)
model.classifier[6] = nn.Linear(4096, 2)
model.to(device)
现在,您还可以尝试其他型号,如 ResNet、DenseNet 等。,尤其是如果你正在寻找更轻的模型,因为 VGG-19 有更多的参数比 ResNet 或 DenseNet。我选择 VGG 是因为它通常会带来更直观的激活地图。
如果您想要使用另一个模型,请确保您替换了最后一层,以获得两个输出。
训练超参数
我们现在设置训练超参数。我们使用 0.01 的初始学习率。我们使用动量值为 0.9 的随机梯度下降。
learning_rate = 0.01
optimizer = optim.SGD(model.parameters(), lr = learning_rate, momentum=0.9)
提前停止
我们实现了一个名为EarlyStopping
的类,它保存了损失和准确性的运行平均值。这将有助于我们实施,嗯,你猜对了——提前停止。
这个类保持了损失和精度的移动平均值。如果度量没有提高超过由patience
定义的设定的历元数,那么方法stop
返回:
0 ,如果没有因为准确性或损失而耗尽耐心
1 ,如果对准确性和损失都已经没有耐心了
2 ,如果只是为了准确性而耗尽了耐心
3 ,如果已经因为失去而耗尽了耐心
请注意,术语耐心已经耗尽对于一个指标来说意味着该指标在一定数量的时期内没有改善。
from collections import deque
class EarlyStopping(object):
def __init__(self, patience = 8):
super(EarlyStopping, self).__init__()
self.patience = patience
self.previous_loss = int(1e8)
self.previous_accuracy = 0
self.init = False
self.accuracy_decrease_iters = 0
self.loss_increase_iters = 0
self.best_running_accuracy = 0
self.best_running_loss = int(1e7)
def add_data(self, model, loss, accuracy):
# compute moving average
if not self.init:
running_loss = loss
running_accuracy = accuracy
self.init = True
else:
running_loss = 0.2 * loss + 0.8 * self.previous_loss
running_accuracy = 0.2 * accuracy + 0.8 * self.previous_accuracy
# check if running accuracy has improved beyond the best running accuracy recorded so far
if running_accuracy < self.best_running_accuracy:
self.accuracy_decrease_iters += 1
else:
self.best_running_accuracy = running_accuracy
self.accuracy_decrease_iters = 0
# check if the running loss has decreased from the best running loss recorded so far
if running_loss > self.best_running_loss:
self.loss_increase_iters += 1
else:
self.best_running_loss = running_loss
self.loss_increase_iters = 0
# log the current accuracy and loss
self.previous_accuracy = running_accuracy
self.previous_loss = running_loss
def stop(self):
# compute thresholds
accuracy_threshold = self.accuracy_decrease_iters > self.patience
loss_threshold = self.loss_increase_iters > self.patience
# return codes corresponding to exhuaustion of patience for either accuracy or loss
# or both of them
if accuracy_threshold and loss_threshold:
return 1
if accuracy_threshold:
return 2
if loss_threshold:
return 3
return 0
def reset(self):
# reset
self.accuracy_decrease_iters = 0
self.loss_increase_iters = 0
early_stopper = EarlyStopping(patience = 5)
训练循环
如果对运行验证损失的耐心耗尽,但不是对运行准确性的耐心耗尽,我们将我们的学习率乘以 0.1。如果对运行确认损失和运行准确性的耐心耗尽,我们停止训练。
这种策略的原因在于交叉熵损失的性质,其中较高的验证损失不一定对应于较低的准确性。为什么?因为交叉熵损失的一个微妙之处是它更喜欢高置信度的预测。因此,对其预测不太有信心的更精确的模型可能比具有较低精确度但非常有信心预测的模型具有更高的损失。因此,我们只在精度也停止增加时才决定停止。
我们最多训练 60 个纪元。
关于批量的一个注记
如你所见,我使用了 8 的批量大小。然而,为了获得好的结果,你必须使用更大的批量,比如 64 或 128。我的 RTX 2060 只能装 8 件。为了实质上实现大小为 64 的批量更新,我们可以在 8 次迭代上累积梯度(8(批量)* 8(迭代)= 64),并且仅在那时执行梯度更新。这样做的基本模板非常简单。
loss += one_iter_loss / 8
if i %% 8 == 0:
loss.backward()
我们将损失除以 8,因为我们添加了 8 次迭代的更新,并且我们需要重新调整损失。
以下是训练循环的代码。这是一大段代码,所以我加入了注释,这样你可以很容易地理解。
best_model = model
best_val_score = 0
criterion = nn.CrossEntropyLoss()
for epoch in range(60):
model.train()
train_loss = 0
train_correct = 0
for iter_num, data in enumerate(train_loader):
image, target = data['img'].to(device), data['label'].to(device)
# Compute the loss
output = model(image)
loss = criterion(output, target.long()) / 8
# Log loss
train_loss += loss.item()
loss.backward()
# Perform gradient udpate
if iter_num % 8 == 0:
optimizer.step()
optimizer.zero_grad()
# Calculate the number of correctly classified examples
pred = output.argmax(dim=1, keepdim=True)
train_correct += pred.eq(target.long().view_as(pred)).sum().item()
# Compute and print the performance metrics
metrics_dict = compute_metrics(model, val_loader)
print('------------------ Epoch {} Iteration {}--------------------------------------'.format(epoch,
iter_num))
print("Accuracy \t {:.3f}".format(metrics_dict['Accuracy']))
print("Sensitivity \t {:.3f}".format(metrics_dict['Sensitivity']))
print("Specificity \t {:.3f}".format(metrics_dict['Specificity']))
print("Area Under ROC \t {:.3f}".format(metrics_dict['Roc_score']))
print("Val Loss \t {}".format(metrics_dict["Validation Loss"]))
print("------------------------------------------------------------------------------")
# Save the model with best validation accuracy
if metrics_dict['Accuracy'] > best_val_score:
torch.save(model, "best_model.pkl")
best_val_score = metrics_dict['Accuracy']
# print the metrics for training data for the epoch
print('\nTraining Performance Epoch {}: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
epoch, train_loss/len(train_loader.dataset), train_correct, len(train_loader.dataset),
100.0 * train_correct / len(train_loader.dataset)))
# log the accuracy and losses in tensorboard
writer.add_scalars( "Losses", {'Train loss': train_loss / len(train_loader), 'Validation_loss': metrics_dict["Validation Loss"]},
epoch)
writer.add_scalars( "Accuracies", {"Train Accuracy": 100.0 * train_correct / len(train_loader.dataset),
"Valid Accuracy": 100.0 * metrics_dict["Accuracy"]}, epoch)
# Add data to the EarlyStopper object
early_stopper.add_data(model, metrics_dict['Validation Loss'], metrics_dict['Accuracy'])
# If both accuracy and loss are not improving, stop the training
if early_stopper.stop() == 1:
break
# if only loss is not improving, lower the learning rate
if early_stopper.stop() == 3:
for param_group in optimizer.param_groups:
learning_rate *= 0.1
param_group['lr'] = learning_rate
print('Updating the learning rate to {}'.format(learning_rate))
early_stopper.reset()
当这个网络训练时,你可以通过进入目录logs
并运行 TensorBoard 来查看 TensorBoard 中绘制的训练/验证准确度和损失。
cd logs
tensorboard --logdir .
测试性能
对于测试,您可以使用以下任一选项:
- 最新型号
- 加载具有最佳验证精度的模型,并存储为
best_model.pkl
。要加载它,使用model = torch.load('best_model.pkl
- 此处提供预先训练的模型。下载模型并使用
model = torch.load('pretrained_covid_model.pkl')
。从这里下载预训练模型。
一旦您加载了模型,您就可以使用下面的代码来计算性能指标。
model = torch.load("pretrained_covid_model.pkl" )
metrics_dict = compute_metrics(model, test_loader, plot_roc_curve = True)
print('------------------- Test Performance --------------------------------------')
print("Accuracy \t {:.3f}".format(metrics_dict['Accuracy']))
print("Sensitivity \t {:.3f}".format(metrics_dict['Sensitivity']))
print("Specificity \t {:.3f}".format(metrics_dict['Specificity']))
print("Area Under ROC \t {:.3f}".format(metrics_dict['Roc_score']))
print("------------------------------------------------------------------------------")
运行这段代码会产生:
ROC Curve and Test performance of the model
你也可以打印模型的混淆矩阵。
conf_matrix = metrics_dict["Confusion Matrix"]
ax= plt.subplot()
sns.heatmap(conf_matrix, annot=True, ax = ax, cmap = 'Blues'); #annot=True to annotate cells
# labels, title and ticks
ax.set_xlabel('Predicted labels');ax.set_ylabel('True labels');
ax.set_title('Confusion Matrix');
ax.xaxis.set_ticklabels(['CoViD', 'NonCoViD']); ax.yaxis.set_ticklabels(['CoViD', 'NonCoViD']);
识别错误
我们现在来看看我们的模型所犯的错误。我们首先得到错误分类的例子的指数。然后,我们查看分配给错误分类示例的分数,并绘制直方图。
targets = np.array(metrics_dict['target_list'])
preds = np.array(metrics_dict['pred_list'])
scores = np.array(metrics_dict['score_list'])
misclassified_indexes = np.nonzero(targets != preds)
misclassified_scores = scores[misclassified_indexes[0]]
# plot the historgram of misclassified scores
plt.hist(misclassified_scores)
plt.xlabel("scores")
plt.ylabel("No. of examples")
plt.show()
我们绘制分数来观察我们的模型所犯错误的性质。分数接近 0.5(我们的阈值)的例子的错误意味着我们的模型对这些例子是不明确的。我们也看到两端的尖峰,0.0 和 1.0。这意味着模型对这些例子的错误分类非常有信心。
使用 Grad-CAM 可视化激活
梯度加权类激活映射,或更简单的 Grad-CAM,帮助我们获得网络正在看到的,并帮助我们看到在给定图像作为输入的特定层中哪些神经元正在放电。
我们首先从克隆实现 Grad-CAM 的必要回购开始。
!git clone https://github.com/jacobgil/pytorch-grad-cam
!mv pytorch-grad-cam gradcam
现在我们定义一个名为do_grad_cam
的函数,它获取图像的路径并输出一个带有 Grad-CAM 遮罩的图像。
from gradcam.gradcam import *
def do_grad_cam(path):
# Initialise the grad cam object.
# we use model.features as the feature extractor and use the layer no. 35 for gradients.
grad_cam = GradCam(model=model, feature_module=model.features, \
target_layer_names=["35"], use_cuda=True)
# read in the image, and prepare it for the network
orig_im = cv2.imread(path)
img = Image.fromarray(orig_im)
inp = val_transformer(img).unsqueeze(0)
# main inference
mask = grad_cam(inp, None)
# create the heatmap
heatmap = cv2.applyColorMap(np.uint8(255 * mask), cv2.COLORMAP_JET)
heatmap = np.float32(heatmap) / 255
#add the heatmap to the original image
cam = heatmap + np.float32(cv2.resize(orig_im, (224,224))/255.)
cam = cam / np.max(cam)
# BGR -> RGB since OpenCV operates with BGR values.
cam = cam[:,:,::-1]
return cam
现在,让我们使用 grad cam 功能来可视化一些真阳性、真阴性、假阳性和假阴性的示例。
真阳性
true_positives = np.logical_and(preds == 1, targets == 1)
true_positives = np.logical_and(true_positives, scores > 0.9)
true_positives_indices = np.nonzero(true_positives)
true_positives_paths = [metrics_dict['paths'][i] for i in true_positives_indices[0]]
true_positive_images = [do_grad_cam(x) for x in random.sample(true_positives_paths, 10)]
plt.figure(figsize=(30,15))
columns = 5
for i, image in enumerate(true_positive_images):
plt.subplot(len(true_positive_images) / columns + 1, columns, i + 1)
plt.imshow(image)
我们看到该模型能够关注毛玻璃畸形,这是 COVID 感染患者的 CT 扫描的特征。然而,我们也看到,有时网络聚焦于肺的边界(灰色)来做出决定。我不是放射科医生,我不能说我们是否应该查看边界来做出决定,但如果不是,那么这是值得进一步检查的事情。
假阳性
在这些例子中,我们看到网络再次聚焦于不透明度,但它并没有足够的效率来从 COVID 肺炎中识别对应于非 COVID 肺炎的不透明度。
真正的否定
在真正的阴性情况下,网络似乎更关注肺部边界,而不是肺部阴影。它看到的不透明物不是很致密,不像毛玻璃。同样,由于我不是放射科医师,我可能完全偏离了轨道,但似乎该模型依赖于不透明的稀缺程度来做出负面预测。
假阴性
最后,在假阴性中,网络能够检测到不透明性,但将其归类为阴性。似乎这个网络对不透明性有一些限制。在这种情况下,似乎网络认为他们是非 COVID 肺炎。特别是,对于第 2 行第 2 列和第 1 行第 3 列中的图像,网络几乎没有任何东西可以处理。
我们看到的另一个假象是网络聚焦在图像的边缘,这可能是由于过度拟合造成的。
改进的余地
虽然该模型确实给出了不错的指标,但我们可以进一步改进它。这里有一些我们可以研究的途径。
- 获取更多数据:数据集只有 746 个示例,对于在其上训练的模型来说太小,无法在现实世界中部署。从精度和损失的 TensorBoard 记录可以明显看出,模型过度拟合数据集。
列车精度几乎达到 100 %,而列车损耗大约为 0.01。然而,验证精度徘徊在 80 左右,验证损失在 0.6 左右。添加更多数据会有所帮助。我们既可以获得数据,也可以使用 GANs 来创建更多的医疗数据,这种方法已在下面的文章中进行了概述。
2.获取差异化数据:正如我之前观察到的,阴性类别的数据是健康患者和非 COVID 疾病患者的混合。如果我们有标签将健康患者与不健康的非 COVID 患者分开,性能会好得多。这可能是我们的模型将非 COVID 不透明度误认为 COVID 不透明度的原因。
3.使用更好的表示:虽然迁移学习是一种非常成功的技术,但在日常用品数据集 ImageNet 上预先训练的模型上执行医学图像应用可能不是最佳的。因此,您可能想要学习一些医疗任务本身的表示。以下论文的作者采用了这种方法,通过使用称为 MoCo 的自我对比表示学习技术来学习 LUNA16 数据集上的表示,该方法实现了 86%的更高准确率。
4.使用更好的网络:虽然我们使用了普通的 VGG-19,但人们可以使用高级架构来学习分类器。例如,一篇论文使用了 SqueezeNet 中的 Fire 模块的修改和贝叶斯优化来实现 83%的准确率。
虽然所有这些都是令人兴奋的事情,我们将这些留给另一篇文章。
结论
在这篇文章中,我们创建了一个简单的基于 CNN 的分类器,将肺部 CT 分为 COVID 相关和非 COVID 相关。我们达到了 82.8%的准确率。更重要的是,这篇文章将为您提供开始试验 COVID-CT 数据集的基本代码设置,以便您可以开始修改代码以获得更好的性能。
包含所有代码的 Jupyter 笔记本可以免费运行,也可以从 ML Showcase 下载。
有关冠状病毒的更多帖子,请查看:
- 我的第一篇关于使用深度学习改进新冠肺炎测试的文章
- 对活跃的新冠肺炎研究小组和数据集的回顾
- 新冠肺炎:我们拥有的数据,以及我们如何使用这些数据