Python 人工智能秘籍(五)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:高级图像应用

计算机视觉中的人工智能应用包括机器人技术、自动驾驶汽车、人脸识别、生物医学图像中的疾病识别以及制造业的质量控制等。

在本章中,我们将从图像识别(或图像分类)开始,我们将探讨基本模型和更高级的模型。然后,我们将使用生成对抗网络GANs)创建图像。

在本章中,我们将涵盖以下内容:

  • 识别服装项目

  • 生成图像

  • 编码图像和样式

技术要求

我们将使用许多标准库,如 NumPy、Keras 和 PyTorch,但我们也会看到一些在每个配方开始时会提到的更多库,因为它们变得相关。

你可以在 GitHub 上找到本章配方的笔记本:github.com/PacktPublishing/Artificial-Intelligence-with-Python-Cookbook/tree/master/chapter07

识别服装项目

图像分类的一个流行例子是 MNIST 数据集,其中包含不同风格的数字从 0 到 9。在这里,我们将使用一种称为 Fashion-MNIST 的可替换数据集,其中包含不同的服装。

Fashion-MNIST 是 Zalando 的服装图片数据集,包括一个由 60,000 个示例组成的训练集和一个由 10,000 个示例组成的测试集:github.com/zalandoresearch/fashion-mnist

这里是数据集中的一些示例:

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

在这个配方中,我们将使用不同的模型识别服装项目——我们将从通用图像特征(高斯差分DoG)和支持向量机开始;然后我们将转向前馈多层感知器MLP);接着我们将使用卷积神经网络ConvNet);最后,我们将使用 MobileNet 进行迁移学习。

准备工作

在我们开始之前,我们必须安装一个库。在这个配方中,我们将使用scikit-image,这是一个用于图像变换的库,因此我们将快速设置它:

pip install scikit-image

现在我们已经准备好进入配方了!

如何实现…

我们将首先加载和准备数据集,然后我们将使用不同的方法学习 Fashion-MNIST 数据集中服装项目的分类模型。让我们从加载 Fashion-MNIST 时尚数据集开始。

我们可以通过keras工具函数直接获取数据集:

from tensorflow import keras
from matplotlib import pyplot as plt

(train_images, train_labels), (test_images, test_labels) = keras.datasets.fashion_mnist.load_data()
train_images = train_images / 255.0
test_images = test_images / 255.0
plt.imshow(train_images[0])
plt.colorbar()
plt.grid(False)

我们还将图像标准化为 0 到 1 的范围,通过除以最大像素强度(255.0),并且我们可视化第一张图像。

我们应该看到一张运动鞋的图片,这是训练集中的第一张图片:

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

正如我们在本配方介绍中提到的,我们将在接下来的几节中应用不同的方法:

  • DoG 特征

  • MLP

  • LeNet

  • 使用 MobileNet 进行迁移学习

让我们从 DoG 开始。

高斯差分

在深度学习在图像识别中取得突破之前,图像是使用来自拉普拉斯差分或高斯的滤波器进行特征化的。这些功能在 scikit-image 中实现,因此我们可以采用现成的实现。

让我们编写一个函数,使用高斯金字塔提取图像特征:

import skimage.transform
import numpy as np

def get_pyramid_features(img):
    return np.hstack([
        layer.reshape(-1)
        for layer in skimage.transform.pyramids.pyramid_gaussian(img)
    ])

get_pyramid_features() 函数应用高斯金字塔并将这些特征展平为一个向量返回。我们将在 它是如何工作… 部分解释什么是高斯金字塔。

我们几乎准备好开始学习了。我们只需迭代所有图像并提取我们的高斯金字塔特征。让我们创建另一个执行此操作的函数:

from sklearn.svm import LinearSVC

def featurize(x_train, y_train):
    data = []
    labels = []
    for img, label in zip(x_train, y_train):
        data.append(get_pyramid_features(img))
        labels.append(label)

    data = np.array(data)
    labels = np.array(labels)
    return data, labels

为了训练模型,我们在我们的训练数据集上应用 featurize() 函数。我们将使用线性支持向量机作为我们的模型。然后,我们将此模型应用于从我们的测试数据集中提取的特征 - 请注意,这可能需要一些时间才能运行:

x_train, y_train = featurize(train_images, train_labels)
clf = LinearSVC(C=1, loss='hinge').fit(x_train, y_train)

x_val, y_val = featurize(test_images, test_labels)
print('accuracy: {:.3f}'.format(clf.score(x_val, y_val)))

使用这些特征的线性支持向量机在验证数据集上获得了 84% 的准确率。通过调整滤波器,我们可以达到更高的性能,但这超出了本文的范围。在 2012 年 AlexNet 发布之前,这种方法是图像分类的最先进方法之一。

训练模型的另一种方法是将图像展平,并直接将归一化的像素值输入到分类器中,例如 MLP。这是我们现在要尝试的。

多层感知器

用 MLP 对图像进行分类的一个相对简单的方法是。在这种情况下,使用了一个具有 10 个神经元的两层 MLP,你可以将隐藏层看作是 10 个特征检测器的特征提取层。

在本书中我们已经多次看到 MLP 的示例,因此我们将跳过此处的细节;可能感兴趣的是,我们将图像从 28x28 展平为 784 的向量。至于其余部分,可以说我们训练分类交叉熵,并监视准确性。

你将在以下代码块中看到这一点:

import tensorflow as tf
from tensorflow.keras.losses import SparseCategoricalCrossentropy

def compile_model(model):
  model.summary()
  model.compile(
    optimizer='adam',
    loss=SparseCategoricalCrossentropy(
      from_logits=True
    ),
    metrics=['accuracy']
  ) 

def create_mlp():
  model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(10)
  ])
  compile_model(model)
  return model

这个模型在两层及其连接之间有 101,770 个可训练参数。

我们将使用以下函数封装我们的训练集。这应该是相当容易理解的:

def train_model(model, train_images, test_images):
    model.fit(
        train_images,
        train_labels,
        epochs=50,
        verbose=1,
        validation_data=(test_images, test_labels)
    )
    loss, accuracy = model.evaluate(test_images, test_labels, verbose=0)
    print('loss:', loss)
    print('accuracy:', accuracy)

经过 50 个周期,我们在验证集上的准确率为 0.886。

下一个模型是经典的 ConvNet,为 MNIST 提出,使用卷积、池化和全连接层。

LeNet5

LeNet5 是一个前馈神经网络,具有卷积层和最大池化,以及用于导致输出的特征的全连接前馈层。让我们看看它的表现:

from tensorflow.keras.layers import (
  Conv2D, MaxPooling2D, Flatten, Dense
)

def create_lenet():
    model = tf.keras.Sequential([
        Conv2D(
            filters=6,
            kernel_size=(5, 5),
            padding='valid',
            input_shape=(28, 28, 1),
            activation='tanh'
        ),
        MaxPooling2D(pool_size=(2, 2)),
        Conv2D(
            filters=16,
            kernel_size=(5, 5),
            padding='valid',
            activation='tanh'
        ),
        MaxPooling2D(pool_size=(2, 2)),
        Flatten(),
        Dense(120, activation='tanh'),
        Dense(84, activation='tanh'),
        Dense(10, activation='softmax')
    ])
    compile_model(model)
    return model

create_lenet() 函数构建我们的模型。我们只需调用它,并使用它运行我们的 train_model() 函数,以适应训练数据集并查看我们的测试表现:

train_model(
    create_lenet(),
    train_images.reshape(train_images.shape + (1,)),
    test_images.reshape(test_images.shape + (1,)),
)

经过 50 集数,我们的验证准确率为 0.891。

我们还可以查看混淆矩阵,以查看我们如何区分特定的服装类别:

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

让我们继续进行我们最后一次尝试对服装项进行分类。

MobileNet 迁移学习

MobileNetV2 模型是在 ImageNet 上训练的,这是一个包含 1400 万张图像的数据库,已手动注释为 WordNet 分类系统的类别。

MobileNet 可以下载用于迁移学习的权重。这意味着我们保持大部分或所有 MobileNet 的权重固定。在大多数情况下,我们只需添加一个新的输出投影来区分 MobileNet 表示之上的新类别集:

base_model = tf.keras.applications.MobileNetV2(
    input_shape=(224, 224, 3),
    include_top=False,
    weights='imagenet'
)

MobileNet 包含 2,257,984 个参数。下载 MobileNet 时,我们有方便的选项可以省略输出层(include_top=False),这样可以节省工作量。

对于我们的迁移模型,我们必须附加一个池化层,然后像前两个神经网络一样附加一个输出层:

def create_transfer_model():
    base_model = tf.keras.applications.MobileNetV2(
        input_shape=(224, 224, 3),
        include_top=False,
        weights='imagenet'
    )
    base_model.trainable = False
    model = tf.keras.Sequential([
      base_model,
      tf.keras.layers.GlobalAveragePooling2D(),
      tf.keras.layers.Dense(10)
    ])
    compile_model(model)
    return model

请注意,在 MobileNet 模型中,我们会冻结或固定权重,只学习我们添加在顶部的两个层。

当我们下载 MobileNet 时,您可能已经注意到一个细节:我们指定了 224x224x3 的尺寸。MobileNet 有不同的输入形状,224x224x3 是最小的之一。这意味着我们必须重新缩放我们的图像,并将它们转换为 RGB(通过串联灰度)。您可以在 GitHub 上的在线笔记本中找到详细信息。

MobileNet 迁移学习的验证准确率与 LeNet 和我们的 MLP 非常相似:0.893。

工作原理…

图像分类包括为图像分配标签,这是深度学习革命开始的地方。

从前述 URL 获取的以下图表说明了 ImageNet 图像分类基准随时间的性能提高:

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

图上的 TOP 1 准确率(也更简单地称为准确率)在y轴上是一个度量,用于衡量所有预测中正确预测的比例,或者换句话说,正确识别对象的频率的比率。随着时间的推移(x轴),图上的最先进线一直在持续改进,直到现在,使用 NoisyStudent 方法达到了 87.4%的准确率(详细信息请见此处:paperswithcode.com/paper/self-training-with-noisy-student-improves)。

在下面的图表中,您可以看到图像识别中深度学习的时间线,可以看到复杂性(层数)的增加以及 ImageNet 大规模视觉识别挑战(ILSVRC)中错误率的降低:

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

您可以在www.image-net.org/challenges/LSVRC/找到有关挑战的更多详细信息。

高斯差分

高斯金字塔是原始图像的一系列经过递归缩小的版本,其中缩小是使用高斯滤波器完成的。您可以在 Scholarpedia 上详细了解 www.scholarpedia.org/article/Scale_Invariant_Feature_Transform

我们使用 skimage 的实用函数来提取特征,然后在顶部应用线性支持向量机作为分类器。为了提高性能,我们本可以尝试其他分类器,如随机森林或梯度提升。

LeNet5

CNN 或 ConvNet 是至少包含一个卷积层的神经网络。LeNet 是 ConvNet 的经典示例,最初由 Yann LeCun 等人提出,1989 年的原始形式是 (应用于手写邮政编码识别的反向传播),1998 年的修订形式(称为 LeNet5)是 (应用于文档识别的基于梯度的学习)。

您可以在以下图表中看到 LeNet 的架构(使用 NN-SVG 工具在 alexlenail.me/NN-SVG 创建):

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

卷积在图像识别中是非常重要的转换,也是非常深的神经网络中图像识别的重要组成部分。卷积包括前馈连接,称为过滤器或卷积核,应用于图像的矩形补丁(上一层)。然后,每个生成的映射是核在整个图像上滑动的结果。这些卷积映射通常后面跟随由池化层进行的子采样(在 LeNet 的情况下,是从每个核中提取的最大值)。

MobileNet 迁移学习

MobileNets(Howard 等人,MobileNets: 高效卷积神经网络用于移动视觉应用;2017;arxiv.org/abs/1704.04861)是由 Google 开发的一类模型,专为移动和嵌入式应用而设计,明确权衡延迟与准确性。MobileNet 主要由不同形状的堆叠卷积层组成。所有卷积层后跟批归一化和 ReLU 激活。最后一个卷积层后是一个平均池化层,去除空间维度,以及一个带有 softmax 函数的最终密集输出层。

在 Keras 中,加载模型只需一条命令。tf.keras.applications 包提供了许多模型的架构和权重,例如 DenseNet、EfficientNet、Inception-ResNet V2、Inception V3、MobileNetV1、MobileNetV2、NASNet-A、ResNet、ResNet v2、VGG16、VGG19 和 Xception V1。在我们的案例中,我们有一个预训练模型,这意味着它具有使用 tf.keras.applications.MobileNetV2() 函数的架构和权重。

我们可以重新训练(微调)模型以提高应用性能,或者我们可以使用原始模型,并在其上添加额外的层以对新类进行分类。

我们加载模型的操作是这样的:

    base_model = tf.keras.applications.MobileNetV2(
        input_shape=(224, 224, 3),
        include_top=False,
        weights='imagenet'
    )

正如之前提到的,我们可以从多个选择中指定不同的输入形状。include_top指定是否包含分类层。如果我们想要使用在 ImageNet 数据集上训练过的原始模型输出,我们会将其设置为True。由于我们的数据集中有不同的类别,我们想将其设置为False

如果我们想要微调模型(带或不带顶部),我们将保持基础模型(MobileNetV2)可训练。显然,这种训练方式可能需要更长的时间,因为需要训练更多的层。这就是为什么在训练过程中我们冻结了所有 MobileNetV2 的层,并将其trainable属性设置为False

另请参阅

你可以在*Khan 等人(2020)在 arXiv 上发表的《深度卷积神经网络最近架构的综述》中找到对 ConvNet 的回顾,从 LeNet 到 AlexNet 再到更近期的架构,可在此链接获取:arxiv.org/pdf/1901.06032.pdf

一个更近期的架构是 EfficientNet(Mingxing Tan 和 Quoc V. Le,2019),在 ImageNet 上实现了最先进的性能,同时比最佳 ConvNet 小约一个数量级,并且比最佳 ConvNet 快大约五倍:arxiv.org/abs/1905.11946

生成图像

2014 年由 Ian Goodfellow 等人引入的 GAN 对抗学习,是一种通过两个网络相互对抗的框架来拟合数据集的分布,其中一个模型生成示例,另一个模型区分这些示例是真实的还是虚假的。这可以帮助我们使用新的训练示例扩展我们的数据集。使用 GAN 的半监督训练可以在使用少量标记训练示例的同时,实现更高的监督任务性能。

本篇重点是在 MNIST 数据集上实现深度卷积生成对抗网络DCGAN)和鉴别器,这是最知名的数据集之一,包含 60,000 个 0 到 9 之间的数字。我们将在*工作原理…*部分解释术语和背景。

准备工作

对于这个步骤,我们不需要任何特殊的库。我们将使用 TensorFlow 与 Keras,NumPy 和 Matplotlib,这些都是我们之前见过的。为了保存图像,我们将使用 Pillow 库,你可以按以下方式安装或升级:

pip install --upgrade Pillow

让我们马上开始吧。

如何操作…

对于我们的 GAN 方法,我们需要一个生成器——一个接受某些输入(可能是噪声)的网络——以及一个鉴别器,一个图像分类器,例如本章中识别服装项目食谱中看到的那样。

生成器和鉴别器都是深度神经网络,并将一起进行训练。训练后,我们将看到训练损失、各个时期的示例图像以及最后时期的复合图像。

首先,我们将设计鉴别器。

这是一个经典的 ConvNet 示例。它是一系列卷积和池化操作(通常是平均或最大池化),接着是平坦化和输出层。更多详情,请参见本章的识别服装项目示例:

def discriminator_model():
    model = Sequential([
        Conv2D(
            64, (5, 5),
            padding='same',
            input_shape=(28, 28, 1),
            activation='tanh'
        ),
        MaxPooling2D(pool_size=(2, 2)),
        Conv2D(128, (5, 5), activation='tanh'),
        MaxPooling2D(pool_size=(2, 2)),
        Flatten(),
        Dense(1024, activation='tanh'),
        Dense(1, activation='sigmoid')
    ])
  return model

这与我们在本章的识别服装项目示例中介绍的 LeNet 卷积块非常相似。

接下来,我们设计生成器。

虽然鉴别器通过卷积和池化操作对其输入进行下采样,生成器则进行上采样。我们的生成器接受一个 100 维的输入向量,并通过执行与 ConvNet 相反方向的操作生成图像。因此,这种类型的网络有时被称为 DeconvNet。

生成器的输出通过 Tanh 激活函数重新标准化到-1 到 1 的范围内。DCGAN 论文(Alec Radford 等人,2015 年,《无监督学习中的深度卷积生成对抗网络》)的一个主要贡献之一是在反卷积操作后引入批标准化。在 Keras 中,有两种实现反卷积的方法:一种是使用UpSampling2D(参见www.tensorflow.org/api_docs/python/tf/keras/layers/UpSampling2D),另一种是使用Conv2DTranspose。这里,我们选择了UpSampling2D

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (
    Dense, Reshape, Activation,
    Flatten, BatchNormalization,
    UpSampling2D, Conv2D, MaxPooling2D
)

def create_generator_model():
    model = Sequential([
        Dense(input_dim=100, units=1024, activation='tanh'), Dense(128*7*7),
        BatchNormalization(),
        Activation('tanh'),
        Reshape((7, 7, 128), input_shape=(128*7*7,)),
        UpSampling2D(size=(2, 2)),
        Conv2D(64, (5, 5), padding='same'),
        Activation('tanh'),
        UpSampling2D(size=(2, 2)),
        Conv2D(1, (5, 5), padding='same'),
        Activation('tanh'),
    ])
    model.summary()
    return model

调用此函数,我们将使用summary()获取我们网络架构的输出。我们将看到有 6,751,233 个可训练参数。我们建议在强大的系统上运行此示例,例如 Google Colab。

为了训练网络,我们加载并标准化了 MNIST 数据集:

from tensorflow.keras.datasets import mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = X_train[:, :, :, None]
X_test = X_test[:, :, :, None]

图像以灰度形式呈现,像素值范围在 0–255 之间。我们将其标准化到-1 到+1 的范围内,然后重新调整为在末尾具有单例维度。

为了将误差传递给生成器,我们将生成器与鉴别器链在一起,如下所示:

def chain_generator_discriminator(g, d):
    model = Sequential()
    model.add(g)
    model.add(d)
    return model

作为我们的优化器,我们将使用 Keras 的随机梯度下降:

from tensorflow.keras.optimizers import SGD

def optim():
    return SGD(
        lr=0.0005,
        momentum=0.9,
        nesterov=True
    )

现在,让我们创建并初始化我们的模型:

d = discriminator_model()
g = generator_model()
d_on_g = chain_generator_discriminator(g, d)
d_on_g.compile(loss='binary_crossentropy', optimizer=optim())
d.compile(loss='binary_crossentropy', optimizer=optim())

单个训练步骤包括三个步骤:

  • 生成器从噪声中生成图像。

  • 鉴别器学习区分生成的图像和真实的图像。

  • 生成器通过鉴别器的反馈学习创建更好的图像。

让我们依次进行这些步骤。首先是从噪声生成图像:

import numpy as np

def generate_images(g, batch_size):
    noise = np.random.uniform(-1, 1, size=(batch_size, 100))
    image_batch = X_train[index*batch_size:(index+1)*batch_size]
    return g.predict(noise, verbose=0)

然后,鉴别器在给定假和真实图像时进行学习:

def learn_discriminate(d, image_batch, generated_images, batch_size):
    X = np.concatenate(
        (image_batch, generated_images)
    )
    y = np.array(
        [1] * batch_size + [0] * batch_size
    )
    loss = d.train_on_batch(X, y)
    return loss

我们将真实的1和假的0图像串联起来,作为鉴别器的输入。

最后,生成器从鉴别器的反馈中学习:

def learn_generate(d_on_g, d, batch_size):
    noise = np.random.uniform(-1, 1, (batch_size, 100))
    d.trainable = False
    targets = np.array([1] * batch_size)
    loss = d_on_g.train_on_batch(noise, targets)
    d.trainable = True
    return loss

请注意,在这个函数中,鉴别器目标的反转。与以前的假 0 不同,我们现在输入 1。同样重要的是,在生成器学习期间,鉴别器的参数是固定的(否则我们将再次忘记)。

我们可以在训练中加入额外的命令,以保存图像,以便可以通过视觉方式欣赏我们生成器的进展:

from PIL import Image

def save_images(generated_images, epoch, index):
    image = combine_images(generated_images)
    image = image*127.5+127.5
    Image.fromarray(
        image.astype(np.uint8)
    ).save('{}_{}.png'.format(epoch, index))

我们的训练通过交替进行以下步骤来进行:

from tqdm.notebook import trange

batch_size = 1024
generator_losses = []
discriminator_losses = []
for epoch in trange(100):
    for index in trange(nbatches):
        image_batch = X_train[index*batch_size:(index+1)*batch_size]
        generated_images = generate_images(g, batch_size)
        d_loss = learn_discriminate(
            d, image_batch, generated_images, batch_size
        )
        g_loss = learn_generate(d_on_g, d, batch_size)
        discriminator_losses.append(d_loss)
        generator_losses.append(g_loss)
        if (index % 20) == 0:
            save_images(generated_images, epoch, index)

我们让它运行。tqdm 进度条将显示剩余时间。在 Google Colab 上可能需要大约一个小时。

在 100 个 epochs 中,我们的训练错误看起来如下:

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

我们已经保存了图像,因此我们也可以查看生成器在 epochs 中的输出。以下是每个 100 个 epochs 中单个随机生成的数字的画廊:

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

我们可以看到图像通常变得越来越清晰。有趣的是,生成器的训练误差似乎在前几个 epochs 之后保持在相同的基线水平。这是一个显示最后一个 epoch 期间生成的 100 个图像的图像:

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

图像并不完美,但大部分可以识别为数字。

如何运作…

生成模型可以生成具有与训练集相同统计特性的新数据,这对半监督和无监督学习很有用。GAN 由 Ian Goodfellow 等人在 2014 年(Generative Adversarial Nets,NIPS;papers.nips.cc/paper/5423-generative-adversarial-nets)引入,而 DCGAN 由 Alec Radford 等人在 2015 年(Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networksarxiv.org/abs/1511.06434)引入。自原始论文以来,已提出许多增量改进。

在 GAN 技术中,生成网络学习将一个种子(例如,随机输入)映射到目标数据分布,而鉴别网络评估并区分生成器产生的数据和真实数据分布。

生成网络生成数据,鉴别网络评估数据。这两个神经网络相互竞争,生成网络的训练增加了鉴别网络的错误率,而鉴别器的训练增加了生成器的错误率,从而进行武器竞赛,迫使彼此变得更好。

在训练中,我们将随机噪声输入生成器,然后让鉴别器学习如何对生成器输出与真实图像进行分类。然后,给定鉴别器的输出,或者说其反向的生成器进行训练。鉴别器判断图像为假的可能性越小,对生成器越有利,反之亦然。

另请参见

原始的 GAN 论文,Generative Adversarial Networks(Ian Goodfellow 等人,2014 年),可以在 arXiv 上找到:arxiv.org/abs/1406.2661

DCGAN 论文,Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks(Alec Radford 等人,2015 年),也可以在 arXiv 上找到:arxiv.org/abs/1511.06434

你可以在 PyTorch 网站上找到有关 DCGAN 的教程:pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html

还有许多值得探索的 GAN 架构。Erik Linder-Norén 在 PyTorch 和 Keras 中实现了数十种最先进的架构。你可以在他的 GitHub 仓库中找到它们:github.com/eriklindernoren/PyTorch-GANgithub.com/eriklindernoren/Keras-GAN

图像和风格编码

自编码器在有效地表示输入方面非常有用。在 2016 年的一篇论文中,Makhazani 等人展示了对抗自编码器可以比变分自编码器创建更清晰的表示,并且——与我们在前一示例中看到的 DCGAN 类似——我们获得了学习创建新示例的额外好处,这对半监督或监督学习场景有帮助,并且允许使用更少的标记数据进行训练。以压缩方式表示还有助于基于内容的检索。

在这个示例中,我们将在 PyTorch 中实现对抗自编码器。我们将实现监督和无监督两种方法,并展示结果。在无监督方法中,类别之间有很好的聚类效果;在监督方法中,我们的编码器-解码器架构能够识别风格,从而使我们能够进行风格转移。在这个示例中,我们将使用计算机视觉的hello world数据集 MNIST。

准备工作

对于这个示例,我们将需要使用torchvision。这将帮助我们下载我们的数据集。我们将快速安装它:

!pip install torchvision

对于 PyTorch,我们需要先进行一些准备工作,比如启用CUDA并设置tensor类型和device

use_cuda = True
use_cuda = use_cuda and torch.cuda.is_available()
print(use_cuda)
if use_cuda:
    dtype = torch.cuda.FloatTensor
    device = torch.device('cuda:0')
else:
    dtype = torch.FloatTensor
    device = torch.device('cpu')

与其他示例风格不同,我们还会先导入必要的库:

import numpy as np
import torch
from torch import autograd
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, dataset
from torchvision.datasets import MNIST
import torchvision.transforms as T
from tqdm.notebook import trange

现在,让我们开始吧。

怎么做…

在这个示例中,我们将实现一个对抗自编码器,并将其应用于 MNIST 手写数字数据集。这段代码基于 Maurits Diephuis 和 Shideh Rezaeifar 的实现:github.com/mdiephuis/adversarial-autoencoders

首先我们将导入必要的库。然后,我们将加载我们的数据集,定义模型组件,包括编码器、解码器和判别器,然后进行训练,最后我们将可视化生成的表示。

首先是加载数据集。

我们需要设置一些全局变量,这些变量将定义训练和数据集。然后,我们加载我们的数据集:

EPS = torch.finfo(torch.float32).eps
batch_size = 1024
n_classes = 10
batch_size = 1024
n_classes = 10

train_loader = torch.utils.data.DataLoader(
    MNIST(
        'Data/',
        train=True,
        download=True,
        transform=T.Compose([
                T.transforms.ToTensor(),
                T.Normalize((0.1307,), (0.3081,))
        ])
    ),
    batch_size=batch_size,
    shuffle=True
)

val_loader = torch.utils.data.DataLoader(
    MNIST(
        'Val/',
        train=False,
        download=True,
        transform=T.Compose([
                T.transforms.ToTensor(),
                T.Normalize((0.1307,), (0.3081,))
        ])
    ),
    batch_size=batch_size,
    shuffle=False
)

规范化中的转换对应于 MNIST 数据集的均值和标准差。

接下来是定义自编码器模型。

自编码器由编码器、解码器和判别器组成。如果您熟悉自编码器,这对您来说不是什么新东西。在下一节*工作原理……*中,我们将对其进行详细解释和分解。

首先,我们将定义我们的编码器和解码器:

dims = 10
class Encoder(nn.Module):
    def __init__(self, dim_input, dim_z):
        super(Encoder, self).__init__()
        self.dim_input = dim_input # image size
        self.dim_z = dim_z
        self.network = []
        self.network.extend([
            nn.Linear(self.dim_input, self.dim_input // 2),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.Linear(self.dim_input // 2, self.dim_input // 2),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.Linear(self.dim_input // 2, self.dim_z),
        ])
        self.network = nn.Sequential(*self.network)
    def forward(self, x):
        z = self.network(x)
        return z

请注意dim参数,它表示表示层的大小。我们选择10作为我们编码层的大小。

然后,我们将定义我们的解码器:

class Decoder(nn.Module):
    def __init__(self, dim_input , dim_z, supervised=False):
        super(Decoder, self).__init__()
        self.dim_input = dim_input
        self.dim_z = dim_z
        self.supervised = supervised
        self.network = []
        self.network.extend([
            nn.Linear(self.dim_z, self.dim_input // 2) if not self.supervised
            else nn.Linear(self.dim_z + n_classes, self.dim_input // 2),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.Linear(self.dim_input // 2, self.dim_input // 2),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.Linear(self.dim_input // 2, self.dim_input),
            nn.Sigmoid(),
        ])
        self.network = nn.Sequential(*self.network)
    def forward(self, z):
        x_recon = self.network(z)
        return x_recon

顺便说一下,我们还可以定义我们的判别器来与我们的编码器竞争:

class Discriminator(nn.Module):
    def __init__(self, dims, dim_h):
        super(Discriminator,self).__init__()
        self.dim_z = dims
        self.dim_h = dim_h
        self.network = []
        self.network.extend([
            nn.Linear(self.dim_z, self.dim_h),
            nn.Dropout(p=0.2), 
            nn.ReLU(),
            nn.Dropout(p=0.2), 
            nn.Linear(self.dim_h, self.dim_h),
            nn.ReLU(),
            nn.Linear(self.dim_h, 1),
            nn.Sigmoid(),
        ])
        self.network = nn.Sequential(*self.network)

    def forward(self, z):
        disc = self.network(z)
        return disc

请注意,为了保持在 0 和 1 的范围内,我们压缩了我们的输出。这对我们的损失函数非常重要。

然后是训练模型。

对抗自编码器可以在有监督的方式下使用,其中标签被输入到解码器中,除了编码输出之外,我们还需要一个实用函数,将变量进行独热编码:

def one_hot_encoding(labels, n_classes=10):
    cat = np.array(labels.data.tolist())
    cat = np.eye(n_classes)[cat].astype('float32')
    cat = torch.from_numpy(cat)
    return autograd.Variable(cat)

我们将展示如何在有标签和无标签情况下使用对抗自编码器:

def train_validate(
        encoder,
        decoder,
        Disc,
        dataloader,
        optim_encoder,
        optim_decoder,
        optim_D,
        train):
    total_rec_loss = 0
    total_disc_loss = 0
    total_gen_loss = 0
    if train:
        encoder.train()
        decoder.train()
        Disc.train()
    else:
        encoder.eval()
        decoder.eval()
        Disc.eval()

    iteration = 0
    for (data, labels) in dataloader:
        # [ training loop here, see next code segment ]

    M = len(dataloader.dataset)
    return total_rec_loss / M, total_disc_loss / M, total_gen_loss / M

如您在评论中所见,我们已经拆分出训练循环。训练循环如下所示:

    for (data, labels) in dataloader:
        # Reconstruction loss:
        for p in Disc.parameters():
            p.requires_grad = False

        real_data_v = autograd.Variable(data).to(device).view(-1, 784)
        encoding = encoder(real_data_v)

        if decoder.supervised:
            categories = one_hot_encoding(labels, n_classes=10).to(device)
            decoded = decoder(torch.cat((categories, encoding), 1))
        else:
            decoded = decoder(encoding)

        reconstruction_loss = F.binary_cross_entropy(decoded, real_data_v)
        total_rec_loss += reconstruction_loss.item()
        if train:
            optim_encoder.zero_grad()
            optim_decoder.zero_grad()
            reconstruction_loss.backward()
            optim_encoder.step()
            optim_decoder.step()

        encoder.eval()
        z_real_gauss = autograd.Variable(
            torch.randn(data.size()[0], dims) * 5.0
        ).to(device)
        z_fake_gauss = encoder(real_data_v)
        D_real_gauss = Disc(z_real_gauss)
        D_fake_gauss = Disc(z_fake_gauss)

        D_loss = -torch.mean(
            torch.log(D_real_gauss + EPS) +
            torch.log(1 - D_fake_gauss + EPS)
        )
        total_disc_loss += D_loss.item()

        if train:
            optim_D.zero_grad()
            D_loss.backward()
            optim_D.step()

        if train:
            encoder.train()
        else:
            encoder.eval()
        z_fake_gauss = encoder(real_data_v)
        D_fake_gauss = Disc(z_fake_gauss)

        G_loss = -torch.mean(torch.log(D_fake_gauss + EPS))
        total_gen_loss += G_loss.item()

        if train:
            optim_encoder_reg.zero_grad()
            G_loss.backward()
            optim_encoder_reg.step()

        if (iteration % 100) == 0:
            print(
                'reconstruction loss: %.4f, discriminator loss: %.4f , generator loss: %.4f' %
                (reconstruction_loss.item(), D_loss.item(), G_loss.item()))

        iteration += 1

对于这段代码,我们将在*工作原理……*部分讨论如何计算和反向传播三种不同的损失。还请注意监督参数,它定义了我们是要使用监督还是无监督训练。

现在,让我们初始化我们的模型和优化器:

encoder = Encoder(784, dims).to(device)
decoder = Decoder(784, dims, supervised=True).to(device)
Disc = Discriminator(dims, 1500).to(device)

lr = 0.001
optim_encoder = torch.optim.Adam(encoder.parameters(), lr=lr)
optim_decoder = torch.optim.Adam(decoder.parameters(), lr=lr)
optim_D = torch.optim.Adam(Disc.parameters(), lr=lr)
optim_encoder_reg = torch.optim.Adam(encoder.parameters(), lr=lr * 0.1)

现在我们可以开始训练:

train_loss = []
val_loss = []
for epoch in trange(n_epochs):
    l1, l2, l3 = train_validate(
        encoder, decoder, Disc,
        train_loader, optim_encoder, optim_decoder,
        optim_D, train=True
    )
    print('epoch: {} ---- training loss: {:.8f}'.format(epoch, l1))
    train_loss.append(l1)

    if (epoch % 5) == 0:
        l1, l2, l3 = train_validate(
            encoder, decoder, Disc,
            val_loader, optim_encoder,
            optim_decoder, optim_D, False
        )
        print('epoch: {} ---- validation loss: {:.8f}'.format(epoch, l1))
        val_loss.append(l1)

这没什么大不了的,除了之前定义的train_validate()函数的调用,一次是train=True选项,一次是train=False选项。从这两个调用中,我们分别收集用于训练和验证的错误。

训练和验证错误持续下降,正如我们在下面的图表中所见:

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

如果您运行此代码,比较生成器和判别器的损失——看到生成器和判别器的损失如何相互影响是很有趣的。

下一步是可视化表示:

在有监督条件下,编码器空间的投影与类别关系不大,正如您在下面的tsne图中所见:

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

这是编码器数字表示空间的二维可视化。颜色(或者如果您在黑白显示器上看的话,阴影)代表不同的数字,它们都被集中在一起,而不是分成群集。编码器根本不区分不同的数字。

编码的东西完全是别的,那就是风格。事实上,我们可以分别在两个维度上变化输入到解码器中,以展示这一点:

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

前五行对应第一个维度的线性范围,第二个维度保持恒定,然后在接下来的五行中,第一个维度固定,第二个维度变化。我们可以看到第一个维度对应厚度,第二个维度对应倾斜度。这被称为风格转移。

我们还可以尝试无监督训练,通过设置supervised=False。我们应该看到这样的投影,其中类别在tsne投影的 2D 空间中聚类:

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

这是数字编码器表示空间的二维可视化。每种颜色(或阴影)代表不同的数字。我们可以看到不同的聚类将同一数字的实例组合在一起。编码器区分不同的数字。

在下一节中,我们将讨论其工作原理。

工作原理…

自编码器是一个由两部分组成的网络 - 编码器和解码器 - 其中编码器将输入映射到潜在空间,解码器重建输入。自编码器可以通过重建损失进行训练,该损失通常是原始输入与恢复输入之间的平方误差。

对抗自编码器于 2016 年由 Makhazani 等人引入(对抗自编码器; arxiv.org/pdf/1511.05644.pdf)。该出版物展示了它们如何在聚类和半监督训练中使用,或者它们如何解相关类别表示。对抗自编码器是一种使用 GAN 执行变分推断的概率自编码器。与直接匹配输入和输出不同,自编码器的隐藏编码向量的聚合后验被匹配到任意先验分布。

由于对抗自编码器是 GAN,因此依赖于生成器和鉴别器之间的竞争,训练比普通自编码器更复杂一些。我们计算三种不同类型的错误:

  • 标准重建误差

  • 鉴别器的一个错误,量化了无法区分真实随机数和编码器输出的失败

  • 对编码器错误的惩罚,因为它未能欺骗鉴别器

在我们的情况下,我们强制先验分布和解码器输出在 0 和 1 之间的范围内,并且因此可以使用交叉熵作为重建误差。

可能有助于突出显示负责计算不同类型误差的代码段。

重建误差如下所示:

if decoder.supervised:
    categories = one_hot_encoding(labels, n_classes=10).to(device)
    decoded = decoder(torch.cat((categories, encoding), 1))
else:
    decoded = decoder(encoding)
    reconstruction_loss = F.binary_cross_entropy(decoded, real_data_v)

有一个额外的标志用于将标签作为监督训练输入到解码器中。我们发现在监督设置中,编码器并不表示数字,而是表示风格。我们认为这是因为在监督设置中,重建误差不再依赖于标签。

鉴别器损失计算如下:

        # i) latent representation:
        encoder.eval()
        z_real_gauss = autograd.Variable(
            torch.randn(data.size()[0], dims) * 5.0
        ).to(device)
        z_fake_gauss = encoder(real_data_v)
        # ii) feed into discriminator
        D_real_gauss = Disc(z_real_gauss)
        D_fake_gauss = Disc(z_fake_gauss)

        D_loss = -torch.mean(
            torch.log(D_real_gauss + EPS) +
            torch.log(1 - D_fake_gauss + EPS)
        )

请注意,这是为了训练鉴别器,而不是编码器,因此有encoder.eval()语句。

生成器损失计算如下:

        if train:
            encoder.train()
        else:
            encoder.eval()
        z_fake_gauss = encoder(real_data_v)
        D_fake_gauss = Disc(z_fake_gauss)

        G_loss = -torch.mean(torch.log(D_fake_gauss + EPS))
        total_gen_loss += G_loss.item()

在下一节中,我们将查看更多的材料和背景。

另请参见

对于更更新、更全面的对抗自编码器实现,请参考 Maurits Diephuis 维护的存储库:github.com/mdiephuis/generative-models

关于自编码器和对抗自编码器的更多历史和数学背景,请参阅 Lilian Weng 在她的博客上的优秀概述文章From Autoencoder to Beta-VAElilianweng.github.io/lil-log/2018/08/12/from-autoencoder-to-beta-vae.html

最后,请查看 Maurits Diephuis 等人的Variational Saccading: Efficient Inference for Large Resolution Images(Ramapuram 等人,2019)。他们引入了一个概率算法,用于专注于更大图像中感兴趣的区域,灵感来源于眼睛的扫视运动。你可以在 GitHub 上找到他们的代码:github.com/jramapuram/variational_saccading

第八章:处理动态图像

本章涉及视频应用。虽然应用于图像的方法可以应用于视频的单帧,但通常会导致时间上的一致性损失。我们将尝试在消费者硬件上找到可能性和趣味性之间的平衡,并展示和实施。

谈论视频时应该考虑到很多应用程序,例如目标跟踪、事件检测(监视)、深度伪造、3D 场景重建和导航(自动驾驶汽车)。

很多伪造视频需要耗费数小时或数天的计算时间。我们将尝试在可能性和趣味性之间找到一个合理的平衡。这种平衡可能比其他章节更为明显,因为视频的计算不像单独的图像计算那样耗费资源。作为这种平衡的一部分,我们将逐帧处理视频,而不是跨时间域处理。尽管如此,我们仍将通过提供实际的真实应用示例或至少类似的示例来解决问题。

在本章中,我们将从图像检测开始,其中算法将图像识别模型应用于图像的不同部分以定位对象。然后我们将展示如何将其应用到视频流中。然后我们将使用深度伪造模型创建视频,并参考更多相关的模型,用于创建和检测深度伪造。

在本章中,我们将看到以下示例:

  • 定位对象

  • 视频伪造

技术要求

我们将使用许多标准库,包括kerasopencv,但在每个示例开始之前我们会提到更多的库。

您可以在 GitHub 上找到本章示例的笔记本,链接为github.com/PacktPublishing/Artificial-Intelligence-with-Python-Cookbook/tree/master/chapter08

定位对象

对象检测是指在图像和视频中识别特定类别的对象。例如,在自动驾驶汽车中,必须识别行人和树木以避让。

在这个示例中,我们将在 Keras 中实现一个对象检测算法。我们将首先将其应用于单个图像,然后应用于我们的笔记本摄像头。在*工作原理…*部分,我们将讨论理论和更多关于对象检测的算法。

准备工作

对于这个示例,我们将需要开放计算机视觉库OpenCV)和scikit-image的 Python 绑定:

!pip install -U opencv-python scikit-image

作为我们的示例图像,我们将从一个对象检测工具箱中下载一张图像:

def download_file(url: str, filename='demo.jpg'):
    import requests
    response = requests.get(url)
    with open(filename, 'wb') as f:
        f.write(response.content)

download_file('https://raw.githubusercontent.com/open-mmlab/mmdetection/master/demo/demo.jpg')

请注意,任何其他图像都可以。

我们将使用基于keras-yolo3库的代码,只需进行少量更改即可快速设置。我们也可以快速下载这个:

download_file('https://gist.githubusercontent.com/benman1/51b2e4b10365333f0af34f4839f86f27/raw/991b41e5d5d83174d3d75b55915033550e16adf8/keras-yolo3.py', 'keras_yolo3.py')

最后,我们还需要YOLOv3网络的权重,可以从 darknet 开源实现中下载:

download_file('https://pjreddie.com/media/files/yolov3.weights', 'yolov3.weights')

现在,你应该在本地目录中拥有示例图像、yolo3-keras Python 脚本以及 YOLOv3 网络权重,从中运行你的笔记本。

如何实现它……

在本节中,我们将使用 Keras 实现一个物体检测算法。

我们将导入 keras-yolo3 库,加载预训练的权重,然后对给定的图像或摄像头视频流进行物体检测:

  1. 由于我们在 keras-yolo3 脚本中已实现了大部分物体检测功能,所以我们只需要导入它:
from keras_yolo3 import load_model, detect
  1. 我们可以按以下方式加载带有预训练权重的网络。请注意,权重文件相当大 - 它们将占用大约 237 MB 的磁盘空间:
yolov3 = load_model('yolov3.weights')

我们的模型现在可作为 Keras 模型使用。

  1. 然后,我们可以对我们的示例图像执行物体检测:
from matplotlib import pyplot as plt

plt.imshow(detect(yolov3, 'demo.jpg'))

我们应该看到我们的示例图像标注了每个边界框的标签,如下截图所示:

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

我们可以使用 OpenCV 库扩展此功能以处理视频。我们可以逐帧捕获连接到计算机的摄像头的图像,运行物体检测,并显示带标注的图像。

请注意,此实现未经优化,可能运行比较慢。要获取更快的实现,请参考参考资料部分中链接的 darknet 实现。

当你运行以下代码时,请注意你可以按 q 键停止摄像头:

import cv2
from skimage import color

cap = cv2.VideoCapture(0)

while(True):
    ret, frame = cap.read()

    img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)    
    img = detect(yolov3, img)
    cv2.imshow('frame', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

我们以灰度方式捕获图像,但随后必须使用 scikit-image 将其转换回 RGB,通过堆叠图像来检测对象并显示带标注的帧。

这是我们获得的图像:

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

在下一节中,我们将讨论这个配方及其背景解释。

它是如何工作的……

我们已经使用 Keras 实现了一个物体检测算法。这是一个标准库的开箱即用功能,但我们将其连接到摄像头,并应用到了一个示例图像上。

图像检测的主要算法如下:

物体检测的主要需求之一是速度 – 你不希望在识别前等待撞上树。

图像检测是基于图像识别的基础上,增加了在图像中搜索候选位置的复杂性。

Fast R-CNN 是 R-CNN 的改进(2014 年同一作者)。每个感兴趣区域,即由边界框定义的矩形图像块,通过图像金字塔进行尺度归一化。卷积网络然后可以通过一次前向传递处理这些对象提议(从几千到成千上万)。作为实现细节,Fast R-CNN 使用奇异值分解压缩完全连接层以提高速度。

YOLO 是一个单一网络,直接从图像中提出边界框和类别。在其实验中,作者以每秒 45 帧和 155 帧的速度运行了不同版本的 YOLO。

SSD 是一种单阶段模型,摒弃了独立对象提议生成的需要,而选择通过网络传递一组离散的边界框。然后在不同分辨率和边界框位置上组合预测结果。

顺便提一下,Joseph Redmon 发表并维护了他的 YOLO 架构的多个增量改进,但他已经离开了学术界。 YOLO 系列的最新实现由 Bochkovskiy 等人在相同的精神中进行,也在 Redmon 的 GitHub 仓库上得到了认可:github.com/AlexeyAB/darknet

YOLOv4 在其 CNN 中引入了几种新的网络特性,展示了快速的处理速度,同时保持了显著优于 YOLOv3 的精度水平(43.5% 平均精度 (AP),在 Tesla V100 GPU 上实时速度约为每秒 65 帧,针对 MS COCO 数据集)。

还有更多…

与网络摄像头交互的方式有多种,并且甚至有一些移动应用程序允许您流式传输摄像头内容,这意味着您可以将其插入在云上运行的应用程序中(例如 Colab 笔记本)或服务器上。

最常见的库之一是matplotlib,也可以从网络摄像头实时更新 matplotlib 图形,如下所示的代码块:

%matplotlib notebook
import cv2
import matplotlib.pyplot as plt

def grab_frame(cap):
    ret, frame = cap.read()
    if not ret:
        print('No image captured!')
        exit()
    return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

cap = cv2.VideoCapture(0)
fig, ax = plt.subplots(1, 1)
im = ax.imshow(grab_frame(cap))

plt.tick_params(
    top=False, bottom=False, left=False, right=False,
    labelleft=False, labelbottom=False
)
plt.show()

while True:
    try:
        im.set_data(grab_frame(cap))
        fig.canvas.draw()
    except KeyboardInterrupt:
        cap.release()
        break

这是初始化视频源并在 matplotlib 子图中显示的基本模板。我们可以通过中断内核来停止。

我们将在下一节中提到更多的库以供玩耍。

另请参见

我们建议您查看 YOLOv4 论文,可在 arxiv 上找到:arxiv.org/abs/2004.10934

有关对象检测,有几个库可用:

让我们继续下一个步骤吧!

伪造视频

深度伪造是通过深度学习应用制作的操纵视频。潜在的不道德用途在媒体上已经存在一段时间。您可以想象这可能如何落入试图破坏政府的宣传机制之手中。请注意,我们建议不要出于不正当目的制作深度伪造视频。

深度伪造技术存在一些道德应用,其中一些非常有趣。您是否想过史泰龙可能会在《终结者》中看起来如何?今天您可以实现!

在本教程中,我们将学习如何使用 Python 创建深度伪造。我们将下载两部电影的公共领域视频,并通过替换其中一个人物的脸部来制作深度伪造。《Charade》是一部 1963 年由斯坦利·多南导演的电影,风格类似于希区柯克的影片。影片中 50 多岁的凯瑞·格兰特与 30 多岁的奥黛丽·赫本搭档。我们认为让这对配对年龄更合适。在一番搜索后,我们找到了 1963 年约翰·韦恩主演的《McLintock!》中的莫琳·奥哈拉,以取代奥黛丽·赫本。

准备工作

Faceit是围绕faceswap库的一个包装器,它简化了我们进行深度伪造所需的许多任务。我们已经在github.com/benman1/faceit上分支了faceit仓库。

我们需要做的是下载faceit仓库并安装必需的库。

你可以用git克隆(clone)这个仓库(如果在ipython笔记本中输入此命令,请加上感叹号):

git clone https://github.com/benman1/faceit

我们发现 Docker 容器非常适合安装依赖项(这需要安装 Docker)。我们可以像这样创建一个 Docker 容器:

./dockerbuild.sh

这个过程可能需要一些时间来构建。请注意,Docker 镜像基于 Nvidia 的容器,因此您可以在容器内使用 GPU。

请注意,尽管有一个轻量级模型可供使用,我们强烈建议您在配备 GPU 的机器上运行深度伪造技术。

最后,我们可以这样进入我们的容器:

./dockerrun.sh

在容器内部,我们可以运行 Python 3.6. 以下所有命令都假定我们在容器内并且在 /project 目录下。

如何做…

我们需要将视频和面部定义为深度伪造过程的输入。

  1. 我们可以像这样在 Python(Python 3.6)中定义我们的模型:
from faceit import *

faceit = FaceIt('hepburn_to_ohara', 'hepburn', 'ohara')

这清楚地表明我们想要用ohara替换hepburn(这是我们在进程内部对它们命名的方式)。我们必须将图像放在data/persons目录下分别命名为hepburn.jpgohara.jpg。我们已经在仓库的便利性部分提供了这些图像。

如果我们没有提供图像,faceit将提取所有的面部图像,不论它们显示的是谁。然后我们可以将其中两张图像放在persons目录下,并删除data/processed/目录下面部的目录。

  1. 接下来,我们需要定义我们要使用的视频。我们可以选择使用完整的电影或短片。我们没有找到《McLintock!》电影的好短片,因此我们正在使用整部电影。至于《Charade》,我们专注于单场景的片段。我们将这些片段保存在磁盘上,名称为mclintock.mp4who_trust.mp4

请注意,只能从允许下载或不禁止下载的网站下载视频,即使是公共领域的视频:

faceit.add('ohara', 'mclintock.mp4')
faceit.add('hepburn', 'who_trust.mp4')
FaceIt.add_model(faceit)

这定义了我们的模型使用的数据为一对视频。Faceit 允许一个可选的第三参数,它可以是一个链接到视频的链接,从中可以自动下载。但是,在您从 YouTube 或其他网站下载视频之前,请确保这在其服务条款中允许,并且在您的司法管辖区内合法。

  1. 然后,通过几行代码(和大量的调整和等待)启动深度伪造的创建:
faceit.preprocess()
faceit.train()
faceit.convert('who_trust.mp4', face_filter=True, photos=False)

预处理步骤包括下载视频,提取所有帧作为图像,最后提取面部。我们已经提供了这些面部,因此您不必执行预处理步骤。

下面的图片显示奥黛丽·赫本在左边,莫琳·奥哈拉在右边扮演奥黛丽·赫本:

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

这些变化可能看起来很微妙。如果你想要更清晰的东西,我们可以使用同样的模型将凯瑞·格兰特替换为莫琳·奥哈拉:

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

实际上,我们可以通过在转换中禁用面部过滤器来制作一部名为*《成为莫琳·奥哈拉》*的电影。

我们本可以使用更先进的模型,更多的训练来改进深度伪造,或者我们可以选择一个更简单的场景。然而,有时候结果看起来并不差。我们已将我们的伪造视频上传到 YouTube,您可以在此观看:youtu.be/vDLxg5qXz4k

它的工作原理…

典型的深度伪造流程包括我们在配方中方便地忽略的一些步骤,因为 Faceit 提供了的抽象。这些步骤是以下,给定人物 A 和人物 B,其中 A 将被 B 替换:

  • 选择 A 和 B 的视频,并将视频分割成帧。

  • 从这些帧中使用人脸识别为 A 和 B 提取面部。这些面部,或面部表情和面部姿势,理想情况下应该代表你将制作的视频。确保它们不模糊,不被遮挡,不显示除 A 和 B 之外的任何人,并且它们不要太重复。

  • 你可以训练一个模型处理这些面孔,该模型可以用 B 替换面孔 A。你应该让训练运行一段时间。这可能需要几天,甚至几周或几个月,具体取决于面孔数量和模型复杂性。

  • 现在我们可以通过运行面部识别和模型在其上进行转换视频。

在我们的情况下,面部识别库(face-recognition)在检测和识别方面表现非常出色。然而,它仍然存在高假阳性和假阴性。这可能导致体验不佳,特别是在有几个面部的帧中。

在当前版本的 faceswap 库中,我们将从目标视频中提取帧以获取所有脸部对齐的地标。然后,我们可以使用 GUI 手动检查和清理这些对齐,以确保它们包含正确的脸部。这些对齐将用于转换:forum.faceswap.dev/viewtopic.php?t=27#align.

每个步骤都需要非常注意。整个操作的核心是模型。可以有不同的模型,包括生成对抗自编码器和其他模型。faceswap 中的原始模型是带有变化的自编码器。我们之前在第七章中使用过自编码器,高级图像应用。这个相对传统,我们可以从那里的自编码器实现中获取我们的自编码器实现。但是为了完整起见,我们将展示其基于 keras/tensorflow 的实现(简化):

from keras.models import Model
from keras.layers import Input, Dense, Flatten, Reshape
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import Conv2D
from keras.optimizers import Adam
from lib.PixelShuffler import PixelShuffler

IMAGE_SHAPE = (64, 64, 3)
ENCODER_DIM = 1024

def conv(self, filters):
    def block(x):
    x = Conv2D(filters, kernel_size=5, strides=2, padding='same')(x)
    x = LeakyReLU(0.1)(x)
    return x
 return block

def upscale(self, filters):
    def block(x):
    x = Conv2D(filters * 4, kernel_size=3, padding='same')(x)
    x = LeakyReLU(0.1)(x)
    x = PixelShuffler()(x)
    return x
 return block

def Encoder():
    input_ = Input(shape=IMAGE_SHAPE)
    x = input_
    x = conv(128)(x)
    x = conv(256)(x)
    x = conv(512)(x)
    x = conv(1024)(x)
    x = Dense(ENCODER_DIM)(Flatten()(x))
    x = Dense(4 * 4 * 1024)(x)
    x = Reshape((4, 4, 1024))(x)
    x = upscale(512)(x)
    return Model(input_, x)

def Decoder():
    input_ = Input(shape=(8, 8, 512))
    x = input_
    x = upscale(256)(x)
    x = upscale(128)(x)
    x = upscale(64)(x)
    x = Conv2D(3, kernel_size=5, padding='same', activation='sigmoid')(x)
    return Model(input_, x)

这段代码本身并不是非常有趣。我们有两个函数,Decoder()Encoder(),分别返回解码器和编码器模型。这是一个卷积编码器-解码器架构。在解码器的放大操作中,PixelShuffle 层通过排列将数据从深度重新排列为空间数据块。

现在,自编码器更有趣的部分在于训练是如何进行的,作为两个模型:

optimizer = Adam(lr=5e-5, beta_1=0.5, beta_2=0.999)
x = Input(shape=IMAGE_SHAPE)

encoder = Encoder()
decoder_A, decoder_B = Decoder(), Decoder()
autoencoder_A = Model(x, decoder_A(encoder(x)))
autoencoder_B = Model(x, decoder_B(encoder(x)))

autoencoder_A.compile(optimizer=optimizer, loss='mean_absolute_error')
autoencoder_B.compile(optimizer=optimizer, loss='mean_absolute_error')

我们有两个自编码器,一个用于训练 A 脸部,另一个用于训练 B 脸部。两个自编码器都在最小化输出与输入之间的重建误差(以平均绝对误差度量)。如前所述,我们有一个单一编码器,它是两个模型的一部分,并且因此将在 A 脸部和 B 脸部上都进行训练。解码器模型在两个脸部之间是分开的。这种架构确保我们在 A 脸部和 B 脸部之间有一个共同的潜在表示。在转换中,我们可以从 A 中取出一个脸部,表示它,然后应用 B 的解码器以获得对应于潜在表示的 B 脸部。

另请参阅

我们整理了一些关于玩弄视频和深度伪造以及检测深度伪造的进一步参考资料。

深度伪造

我们整理了一些关于深度伪造以及与创建深度伪造过程相关的链接。

在这个示例中,面部识别库已经被用来选择图像区域进行训练和应用变换。该库在 GitHub 上可获得:github.com/ageitgey/face_recognition

这里有一些简单视频伪造应用的不错示例:

对于更复杂的视频操作,如深度伪造,有很多可用的工具,我们将重点介绍两个:

提出并实现了许多不同的模型,包括以下内容:

faceit live 仓库(github.com/alew3/faceit_live)是 faceit 的一个分支,可以在实时视频源上运行,并配有一个反馈视频给恶作剧参与者的 hack。

深度伪造检测

以下链接与检测深度伪造相关:

论文《DeepFakes and Beyond: A Survey of *Face Manipulation and Fake Detection》(Ruben Tolosana 等人,2020)提供了更多的链接和数据集资源及方法。

第九章:音频和语音中的深度学习

在本章中,我们将处理声音和语音。声音数据以波形的形式出现,因此需要与其他类型的数据进行不同的预处理。

在音频信号的机器学习中,商业应用包括语音增强(例如助听器)、语音到文本和文本到语音、噪声取消(例如耳机)、根据用户喜好推荐音乐(如 Spotify)以及生成音频。在音频中可以遇到许多有趣的问题,包括音乐流派的分类、音乐的转录、生成音乐等等。

我们将在本章中实现几个与声音和语音相关的应用。首先,我们将做一个简单的分类任务的例子,尝试区分不同的词汇。这将是智能家居设备中区分不同命令的典型应用。然后,我们将研究一个文本到语音的架构。您可以应用这个架构从文本创建自己的音频书籍,或者为您自己的智能家居设备的语音输出。最后,我们将结束于生成音乐的配方。从商业角度来看,这可能更多是一个利基应用,但您可以为了乐趣或娱乐您的视频游戏用户而构建自己的音乐。

在本章中,我们将看看以下的配方:

  • 识别语音命令

  • 从文本合成语音

  • 生成旋律

技术要求

您可以在 GitHub 上找到与本章配方相关的笔记本的源代码:github.com/PacktPublishing/Artificial-Intelligence-with-Python-Cookbook/tree/master/chapter09

我们将在本章中使用音频处理库 librosalibrosa.org/doc/latest/index.html)。您可以按如下方式安装它:

!pip install librosa

Librosa 在 Colab 中默认安装。

对于本章中的配方,请确保您有一个可用的 GPU。在 Google Colab 上,请确保您激活了 GPU 运行时。

识别语音命令

在这个配方中,我们将在谷歌的语音命令数据集上解决一个简单的声音识别问题。我们将把声音命令分类到不同的类别中。然后,我们将建立一个深度学习模型并进行训练。

准备工作

对于本配方,我们需要在章节开头提到的 librosa 库。我们还需要下载语音命令数据集,为此我们首先需要安装 wget 库:

!pip install wget

或者,我们可以在 Linux 和 macOS 中使用 !wget 系统命令。我们将创建一个新目录,下载带有数据集的存档文件,并提取 tarfile

import os
import wget
import tarfile

DATA_DIR = 'sound_commands'
DATASET_URL = 'http://download.tensorflow.org/data/speech_commands_v0.01.tar.gz'
ARCHIVE = os.path.basename(DATASET_URL)
os.mkdir(DATA_DIR)
os.chdir(DATA_DIR)
wget.download(DATASET_URL)
with tarfile.open(ARCHIVE, 'r:gz') as tar:
  tar.extractall(path='data/train')
os.remove(ARCHIVE)

这为我们在 data/train 目录下获得了许多文件和目录:

_background_noise_  five     marvin        right             tree
bed                 four     nine       seven             two
bird                go       no         sheila            up
cat                 happy    off        six               validation_list.txt
dog                 house    on         stop              wow
down                left     one        testing_list.txt  yes
eight               LICENSE  README.md  three             zero

大多数指的是语音命令;例如,bed 目录包含了 bed 命令的示例。

有了这些,我们现在准备好开始了。

如何做…

在这个教程中,我们将训练一个神经网络来识别语音命令。这个教程的灵感来自于 TensorFlow 在语音命令上的教程,网址是 www.tensorflow.org/tutorials/audio/simple_audio

首先进行数据探索,然后导入和预处理数据集进行训练,接着创建模型,训练并在验证中检查其性能:

  1. 让我们从一些数据探索开始:我们将听一个命令,查看其波形,然后查看其频谱。librosa 库提供了将声音文件加载到向量中的功能:
import librosa
x, sr = librosa.load('data/train/bed/58df33b5_nohash_0.wav')

我们还可以获得一个 Jupyter 小部件来听声音文件或加载的向量:

import IPython.display as ipd
ipd.Audio(x, rate=sr)

小部件看起来像这样:

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

按下播放,我们听到声音。请注意,即使在远程连接上(例如使用 Google Colab),这也可以工作。

现在让我们看一下声音波形:

%matplotlib inline
import matplotlib.pyplot as plt
import librosa.display

plt.figure(figsize=(14, 5))
librosa.display.waveplot(x, sr=sr, alpha=0.8)

波形看起来像这样:

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

这也称为压力-时间图,显示随时间变化的(有符号的)振幅。

我们可以如下绘制频谱:

X = librosa.stft(x)
Xdb = librosa.amplitude_to_db(abs(X))
plt.figure(figsize=(14, 5))
librosa.display.specshow(Xdb, sr=sr, x_axis='time', y_axis='log')
plt.colorbar()

频谱看起来像这样:

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

请注意,我们在y轴上使用了对数尺度。

  1. 现在,让我们进行数据导入和预处理。我们需要迭代文件,并将它们存储为向量:
from tqdm.notebook import tqdm

def vectorize_directory(dirpath, label=0):
 features = []
  labels = [0]
  files = os.listdir(dirpath)
  for filename in tqdm(files):
    x, _ = librosa.load(
        os.path.join(dirpath, filename)
    )
    if len(x) == 22050:
      features.append(x)
  return features, [label] * len(features)

features, labels = vectorize_directory('data/train/bed/')
f, l = vectorize_directory('data/train/bird/', 1)
features.extend(f)
labels.extend(l)
f, l = vectorize_directory('data/train/tree/', 2)
features.extend(f)
labels.extend(l)

为简单起见,我们这里只使用了三个命令:bedbirdtree。这已足以说明深度神经网络在声音分类中的问题和应用,也足够简单,不会花费太多时间。然而,这个过程仍然可能需要一些时间。在 Google Colab 上大约需要一个小时。

最后,我们需要将 Python 特征列表转换为 NumPy 数组,并且需要分割训练和验证数据:

import numpy as np
from sklearn.model_selection import train_test_split

features = np.concatenate([f.reshape(1, -1) for f in features], axis=0)
labels = np.array(labels)
X_train, X_test, y_train, y_test = train_test_split(
 features, labels, test_size=0.33, random_state=42
)

现在我们需要对我们的训练数据做一些处理。我们需要一个可以训练的模型。

  1. 让我们创建一个深度学习模型,然后进行训练和测试。首先我们需要创建我们的模型和标准化。让我们先进行标准化操作:
import tensorflow.keras as keras
from tensorflow.keras.layers import *
from tensorflow.keras.regularizers import l2
from tensorflow.keras.models import Model
import tensorflow.keras.backend as K

def preprocess(x):
    x = (x + 0.8) / 7.0
    x = K.clip(x, -5, 5)
    return x

Preprocess = Lambda(preprocess)

接下来是以下内容:

def relu6(x):
    return K.relu(x, max_value=6)

def conv_layer(x, num_filters=100, k=3, strides=2):
    x = Conv1D(
          num_filters,
          (k),
          padding='valid',
          use_bias=False,
          kernel_regularizer=l2(1e-6)
        )(x)
    x = BatchNormalization()(x)
    x = Activation(relu6)(x)
    x = MaxPool1D(pool_size=num_filters, strides=None, padding='valid')(x)
    return x

def create_model(classes, nlayers=1, filters=100, k=100):
    input_layer = Input(shape=[features.shape[1]])
    x = Preprocess(input_layer)
    x = Reshape([features.shape[1], 1])(x)
    for _ in range(nlayers):
        x = conv_layer(x, num_filters=filters, k=k)
        x = Reshape([219 * filters])(x)
        x = Dense(
            units=len(classes), activation='softmax',
            kernel_regularizer=l2(1e-2)
        )(x)
    model = Model(input_layer, x, name='conv1d_sound')
    model.compile(
        optimizer=keras.optimizers.Adam(lr=3e-4),
        loss=keras.losses.SparseCategoricalCrossentropy(),
        metrics=[keras.metrics.sparse_categorical_accuracy])
    model.summary()
    return model

model = create_model(classes

请注意 conv_layer() 函数,它提供了网络的核心部分。在视觉中可以使用非常类似的一维卷积模块,这里只是我们在这里使用了一维卷积。

这给了我们一个相对较小的模型,仅约有 75,000 个参数:

Layer (type)                 Output Shape              Param #   
=================================================================
input_46 (InputLayer)        [(None, 22050)]           0         
_________________________________________________________________
lambda_44 (Lambda)           (None, 22050)             0         
_________________________________________________________________
reshape_86 (Reshape)         (None, 22050, 1)          0         
_________________________________________________________________
conv1d_56 (Conv1D)           (None, 21951, 100)        10000     
_________________________________________________________________
batch_normalization_43 (Batc (None, 21951, 100)        400       
_________________________________________________________________
activation_43 (Activation)   (None, 21951, 100)        0         
_________________________________________________________________
max_pooling1d_29 (MaxPooling (None, 219, 100)          0         
_________________________________________________________________
reshape_87 (Reshape)         (None, 21900)             0         
_________________________________________________________________
dense_33 (Dense)             (None, 3)                 65703     
=================================================================
Total params: 76,103
Trainable params: 75,903
Non-trainable params: 200
_________________________________________________________________

请注意,最大的层(按参数计算)是最后的密集层。我们可以通过修改密集层之前的卷积或最大池化操作进一步减少参数数量。

现在我们可以进行训练和验证:

import sklearn

model.fit(X_train, y_train, epochs=30)
predicted = model.predict(X_test)
print('accuracy: {:.3f}'.format(
    sklearn.metrics.accuracy_score(y_test, predicted.argmax(axis=1))
))

在验证集中,模型的准确率输出应该大约为 0.805。

工作原理…

声音与其他领域并无太大不同,除了预处理。了解如何在文件中存储声音至关重要。在基本水平上,声音以振幅随时间和频率存储。声音以离散间隔采样(这是采样率)。48 kHz 是 DVD 的典型录音质量,指的是每秒 48,000 次的采样频率。比特深度(也称为动态范围)是信号振幅的分辨率(例如,16 比特意味着振幅范围为 0-65,535)。

对于机器学习,我们可以从波形中进行特征提取,并在原始波形上使用 1D 卷积,或在声谱图表示(例如,Mel 声谱图 – Davis 和 Mermelstein,连续语音中基于音节的识别实验,1980 年)上使用 2D 卷积。我们之前在第七章中处理过卷积,高级图像应用。简而言之,卷积是对层输入上的矩形补丁应用的前向滤波器。生成的映射通常在池化层之后进行子采样。

卷积层可以非常深度堆叠(例如,Dai 等人,2016:arxiv.org/abs/1610.00087)。我们已经为读者实验堆叠层提供了便利。层数nlayerscreate_model()的参数之一。

有趣的是,许多语音识别模型使用递归神经网络。然而,一些模型,如 Facebook 的 wav2letter (github.com/facebookresearch/wav2letter),例如,使用完全卷积模型,这与本方案中采用的方法并无太大不同。

参见

除了librosa,在 Python 中用于音频处理的有用库还包括pydub (github.com/jiaaro/pydub) 和 scipypyAudioProcessing 库提供了音频的特征提取和分类功能:github.com/jsingh811/pyAudioProcessing

还有一些有趣的库和存储库值得探索:

文本转语音合成

一个文本到语音程序,对人类来说很容易理解,可以让有视觉或阅读障碍的人听到家用电脑上的书写文字,或者在驾车时让您享受阅读一本书。在这个示例中,我们将加载一个文本到语音模型,并让它朗读给我们听。在 它是如何工作的…… 部分,我们将介绍模型实现和模型架构。

准备工作完成

对于这个示例,请确保您有一个可用的 GPU。在 Google Colab 上,请确保您激活了 GPU 运行时。我们还需要安装 wget 库,可以在笔记本中如下安装:

!pip install wget

我们还需要从 GitHub 克隆pytorch-dc-tts存储库并安装其要求。请从笔记本运行此命令(或在终端中运行,不带前导感叹号):

from os.path import exists

if not exists('pytorch-dc-tts'):
 !git clone --quiet https://github.com/tugstugi/pytorch-dc-tts

!pip install --ignore-installed librosa

请注意,您需要安装 Git 才能使其正常工作。如果您没有安装 Git,您可以直接从您的 Web 浏览器中下载存储库。

我们已经准备好处理主要示例了。

如何操作……

我们将下载 Torch 模型文件,加载它们到 Torch 中,然后从句子中合成语音:

  1. 下载模型文件:我们将从dropbox下载数据集:
import wget

if not exists('ljspeech-text2mel.pth'):
    wget.download(      'https://www.dropbox.com/s/4t13ugxzzgnocbj/step-300K.pth',
        'ljspeech-text2mel.pth'
    )

if not exists('ljspeech-ssrn.pth'):
    wget.download(
   'https://www.dropbox.com/s/gw4aqrgcvccmg0g/step-100K.pth',
        'ljspeech-ssrn.pth'
    )

现在我们可以在 torch 中加载模型。

  1. 加载模型:让我们先处理依赖关系:
import sys
sys.path.append('pytorch-dc-tts')
import numpy as np
import torch
import IPython
from IPython.display import Audio
from hparams import HParams as hp
from audio import save_to_wav
from models import Text2Mel, SSRN
from datasets.lj_speech import vocab, idx2char, get_test_data

现在我们可以加载模型:

torch.set_grad_enabled(False)
text2mel = Text2Mel(vocab)
text2mel.load_state_dict(torch.load('ljspeech-text2mel.pth').state_dict())
text2mel = text2mel.eval()
ssrn = SSRN()
ssrn.load_state_dict(torch.load('ljspeech-ssrn.pth').state_dict())
ssrn = ssrn.eval()

最后,我们可以大声朗读这些句子。

  1. 语音合成:我们选择了一些花园路径句子。这些句子在语法上是正确的,但会误导读者关于它们最初的理解。

以下句子是花园路径句子的例子——这些句子会误导听众关于单词如何相互关联的理解。我们选择它们是因为它们既简短又有趣。您可以在学术文献中找到这些及更多花园路径句子,比如在《Up the Garden Path》(Tomáš Gráf;2013 年发表于 Acta Universitatis Carolinae Philologica)中:

SENTENCES = [
 'The horse raced past the barn fell.',
 'The old man the boat.',
 'The florist sent the flowers was pleased.',
 'The cotton clothing is made of grows in Mississippi.',
 'The sour drink from the ocean.',
 'Have the students who failed the exam take the supplementary.',
 'We painted the wall with cracks.',
 'The girl told the story cried.',
 'The raft floated down the river sank.',
 'Fat people eat accumulates.'
]

我们可以按照以下步骤从这些句子生成语音:

for i in range(len(SENTENCES)):    
    sentences = [SENTENCES[i]]
    max_N = len(sentences[0])
    L = torch.from_numpy(get_test_data(sentences, max_N))
    zeros = torch.from_numpy(np.zeros((1, hp.n_mels, 1), np.float32))
    Y = zeros
    A = None

    for t in range(hp.max_T):
      _, Y_t, A = text2mel(L, Y, monotonic_attention=True)
      Y = torch.cat((zeros, Y_t), -1)
      _, attention = torch.max(A[0, :, -1], 0)
      attention = attention.item()
      if L[0, attention] == vocab.index('E'): # EOS
          break

    _, Z = ssrn(Y)
    Z = Z.cpu().detach().numpy()
    save_to_wav(Z[0, :, :].T, '%d.wav' % (i + 1))
    IPython.display.display(Audio('%d.wav' % (i + 1), rate=hp.sr))

还有更多… 部分,我们将看一下如何为不同数据集训练模型。

它是如何工作的……

语音合成是通过程序产生人类语音的过程,称为语音合成器。从自然语言到语音的合成称为文本到语音TTS)。合成的语音可以通过连接来自录制片段的音频生成,这些片段以单位如独特的声音、音素和音素对(双音素)出现。

让我们稍微深入了解两种方法的细节。

基于引导注意力的深度卷积网络

在这个示例中,我们加载了 Hideyuki Tachibana 和其他人发表的模型,《基于深度卷积网络和引导注意力的高效可训练文本到语音系统》(2017 年;arxiv.org/abs/1710.08969)。我们使用了在 github.com/tugstugi/pytorch-dc-tts 上的实现。

发表于 2017 年,这种方法的新颖之处在于在网络中不使用循环,而是依赖于卷积,这一决定导致训练和推断比其他模型快得多。事实上,他们声称在一台配备两个现成的 GPU 的游戏 PC 上训练他们的深度卷积 TTS 网络只需大约 15 小时。在librivox公共领域有声书项目的数据集上进行了 15 小时的训练后,众包的平均意见分数似乎没有增加。作者提供了一个演示页面,展示了训练不同阶段的音频样本,您可以听到说出的句子,例如Wasserstein GAN 的两人零和博弈是通过考虑 Kantorovich-Rubinstein 对偶导出的tachi-hi.github.io/tts_samples/

该架构由两个子网络组成,可以分别训练,一个用于从文本合成频谱图,另一个用于从频谱图创建波形。文本到频谱图部分包括以下模块:

  • 文本编码器

  • 音频编码器

  • 注意力

  • 音频解码器

这个方法的有趣之处在于标题中提到的引导注意力,它负责将字符与时间对齐。他们约束这个注意力矩阵几乎是线性随时间的,而不是随机顺序阅读字符,给定一个引导注意力损失

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

这偏爱矩阵对角线上的值而不是矩阵外的值。他们认为这种约束有助于显著加快训练时间。

WaveGAN

还有更多…部分,我们将加载一个不同的模型,WaveGAN,由 Chris Donahue 等人发布,标题为WaveGAN: 使用生成对抗网络学习合成原始音频(2018 年;arxiv.org/abs/1802.04208)。

Donahue 等人在无监督环境中训练了一个 GAN 以合成原始音频波形。他们尝试了两种不同的策略:

  • 一种名为Spectrogram-StrategySpecGAN)的方法,他们使用了 DCGAN(请参阅第七章中的生成图像章节,高级图像应用),并将其应用于频谱图(频率随时间变化)

  • 一种名为Waveform-StrategyWaveGAN)的方法,他们将架构展平(1D 卷积)

对于第一种策略,他们必须开发一个能够将频谱图转换回文本的方法。

对于 WaveGAN,他们将 2D 卷积展平为 1D,同时保持大小(例如,5x5 的核变为 25 的 1D 核)。步幅为 2x2 变为 4。他们移除了批标准化层。他们使用了 Wasserstein GAN-GP 策略进行训练(Ishaan Gulrajani 等人,2017 年;Wasserstein GANs 的改进训练arxiv.org/abs/1704.00028)。

他们的 WaveGAN 在人类评判(平均意见分数)方面表现明显不如他们的 SpecGAN。您可以在 chrisdonahue.com/wavegan_examples/ 找到一些生成的声音示例。

还有更多…

我们还可以使用 WaveGAN 模型从文本合成语音。

我们将下载在前一教程中遇到的语音命令上训练的模型检查点。然后我们将运行模型生成语音:

  1. 下载 TensorFlow 模型检查点:我们将按以下方式下载模型数据:
import wget

wget.download(
  'https://s3.amazonaws.com/wavegan-v1/models/timit.ckpt.index',
  'model.ckpt.index'
)
wget.download(
  'https://s3.amazonaws.com/wavegan-v1/models/timit.ckpt.data-00000-of-00001',
  'model.ckpt.data-00000-of-00001')
wget.download(
  'https://s3.amazonaws.com/wavegan-v1/models/timit_infer.meta',
  'infer.meta'
);

现在我们可以将计算图加载到内存中:

import tensorflow as tf

tf.reset_default_graph()
saver = tf.train.import_meta_graph('infer.meta')
graph = tf.get_default_graph()
sess = tf.InteractiveSession()
saver.restore(sess, 'model.ckpt');

现在我们可以生成语音。

  1. 生成语音:模型架构涉及字母的潜在表示。我们可以根据潜在表示的随机初始化来听模型构建的内容:
import numpy as np
import PIL.Image
from IPython.display import display, Audio
import time as time

_z = (np.random.rand(2, 100) * 2.) - 1.
z = graph.get_tensor_by_name('z:0')
G_z = graph.get_tensor_by_name('G_z:0')[:, :, 0]
G_z_spec = graph.get_tensor_by_name('G_z_spec:0')

start = time.time()
_G_z, _G_z_spec = sess.run([G_z, G_z_spec], {z: _z})
print('Finished! (Took {} seconds)'.format(time.time() - start))

for i in range(2):
    display(Audio(_G_z[i], rate=16000))

这应该展示了两个使用 Jupyter 小部件的生成声音示例:

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

如果这些听起来并不特别自然,不要担心。毕竟,我们使用了潜在空间的随机初始化。

另请参阅

基于深度卷积网络和引导注意力的高效可训练文本到语音系统 (arxiv.org/abs/1710.08969)。在 Erdene-Ochir Tuguldur 的 GitHub 仓库中,您可以找到该论文的 PyTorch 实现。蒙古文文本到语音是在《蒙古圣经》的 5 小时音频上训练的:github.com/tugstugi/pytorch-dc-tts

在 Chris Donahue 的 WaveGAN GitHub 仓库中,您可以看到 WaveGAN 的实现以及从 MP3、WAV、OGG 等格式的音频文件中进行训练的示例,而无需预处理 (github.com/chrisdonahue/wavegan)。

Mozilla 开源了他们的 TensorFlow 实现 Baidu 的 Deep Speech 架构(2014 年),您可以在这里找到:github.com/mozilla/DeepSpeech

生成旋律

人工智能 (AI) 在音乐中是一个迷人的话题。如果您最喜欢的 70 年代乐队正在推出新歌,但可能更现代化会很酷吧?索尼与披头士合作做到了这一点,您可以在 YouTube 上听到一首歌,完整地包含了自动生成的歌词,名为 Daddy’s carwww.youtube.com/watch?v=LSHZ_b05W7o

在这个教程中,我们将生成一个旋律。更具体地说,我们将使用 Magenta Python 库中的功能继续一首歌曲。

准备工作

我们需要安装 Magenta 库以及一些系统库作为依赖。请注意,为了安装系统依赖项,您需要管理员权限。如果您不是在 Linux(或 *nix)上,您将需要找到与您的系统对应的依赖项。

在 macOS 上,这应该相对简单。否则,在 Colab 环境中运行可能更容易:

!apt-get update -qq && apt-get install -qq libfluidsynth1 fluid-soundfont-gm build-essential libasound2-dev libjack-dev
!pip install -qU pyfluidsynth pretty_midi
!pip install -qU magenta

如果您在 Colab 上,您需要另一种调整以允许 Python 找到您的系统库:

import ctypes.util
orig_ctypes_util_find_library = ctypes.util.find_library
def proxy_find_library(lib):
  if lib == 'fluidsynth':
    return 'libfluidsynth.so.1'
  else:
    return orig_ctypes_util_find_library(lib)
ctypes.util.find_library = proxy_find_library

这是 Python 外部库导入系统的一个聪明的解决方法,取自原始的 Magenta 教程,位于colab.research.google.com/notebooks/magenta/hello_magenta/hello_magenta.ipynb

是时候发挥创造力了!

如何做…

我们首先组合一段旋律的开头,然后从 Magenta 加载MelodyRNN模型让它继续旋律:

  1. 让我们一起编曲。我们将使用小星星。Magenta 项目使用一种名为NoteSequence的音符序列表示,附带许多实用工具,包括与 MIDI 之间的转换。我们可以像这样向序列添加音符:
from note_seq.protobuf import music_pb2

twinkle_twinkle = music_pb2.NoteSequence()
twinkle_twinkle.notes.add(pitch=60, start_time=0.0, end_time=0.5, velocity=80)
twinkle_twinkle.notes.add(pitch=60, start_time=0.5, end_time=1.0, velocity=80)
twinkle_twinkle.notes.add(pitch=67, start_time=1.0, end_time=1.5, velocity=80)
twinkle_twinkle.notes.add(pitch=67, start_time=1.5, end_time=2.0, velocity=80)
twinkle_twinkle.notes.add(pitch=69, start_time=2.0, end_time=2.5, velocity=80)
twinkle_twinkle.notes.add(pitch=69, start_time=2.5, end_time=3.0, velocity=80)
twinkle_twinkle.notes.add(pitch=67, start_time=3.0, end_time=4.0, velocity=80)
twinkle_twinkle.notes.add(pitch=65, start_time=4.0, end_time=4.5, velocity=80)
twinkle_twinkle.notes.add(pitch=65, start_time=4.5, end_time=5.0, velocity=80)
twinkle_twinkle.notes.add(pitch=64, start_time=5.0, end_time=5.5, velocity=80)
twinkle_twinkle.notes.add(pitch=64, start_time=5.5, end_time=6.0, velocity=80)
twinkle_twinkle.notes.add(pitch=62, start_time=6.0, end_time=6.5, velocity=80)
twinkle_twinkle.notes.add(pitch=62, start_time=6.5, end_time=7.0, velocity=80)
twinkle_twinkle.notes.add(pitch=60, start_time=7.0, end_time=8.0, velocity=80) 
twinkle_twinkle.total_time = 8
twinkle_twinkle.tempos.add(qpm=60);

我们可以使用 Bokeh 可视化序列,然后播放音符序列:

import note_seq

note_seq.plot_sequence(twinkle_twinkle)
note_seq.play_sequence(twinkle_twinkle,synth=note_seq.fluidsynth)

看起来如下:

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

我们可以听歌的前 9 秒。

  1. 让我们从magenta加载MelodyRNN模型:
from magenta.models.melody_rnn import melody_rnn_sequence_generator
from magenta.models.shared import sequence_generator_bundle
from note_seq.protobuf import generator_pb2
from note_seq.protobuf import music_pb2

note_seq.notebook_utils.download_bundle('attention_rnn.mag', '/content/')
bundle = sequence_generator_bundle.read_bundle_file('/content/basic_rnn.mag')
generator_map = melody_rnn_sequence_generator.get_generator_map()
melody_rnn = generator_map'basic_rnn'
melody_rnn.initialize()

这应该只需几秒钟。与本书中遇到的其他模型相比,Magenta 模型非常小。

现在,我们可以将之前的旋律和一些参数一起输入,以继续这首歌曲:

def get_options(input_sequence, num_steps=128, temperature=1.0):
    last_end_time = (max(n.end_time for n in input_sequence.notes)
                      if input_sequence.notes else 0)
    qpm = input_sequence.tempos[0].qpm 
    seconds_per_step = 60.0 / qpm / melody_rnn.steps_per_quarter
    total_seconds = num_steps * seconds_per_step

    generator_options = generator_pb2.GeneratorOptions()
    generator_options.args['temperature'].float_value = temperature
    generate_section = generator_options.generate_sections.add(
      start_time=last_end_time + seconds_per_step,
      end_time=total_seconds)
    return generator_options

sequence = melody_rnn.generate(input_sequence, get_options(twinkle_twinkle))

现在我们可以绘制和播放新音乐:

note_seq.plot_sequence(sequence)
note_seq.play_sequence(sequence, synth=note_seq.fluidsynth)

再次,我们得到了 Bokeh 库的绘图和播放小部件:

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

我们可以像这样从我们的音符序列创建一个 MIDI 文件:

note_seq.sequence_proto_to_midi_file(sequence, 'twinkle_continued.mid')

这将在磁盘上创建一个新的 MIDI 文件。

在 Google Colab 上,我们可以这样下载 MIDI 文件:

from google.colab import files
files.download('twinkle_continued.mid')

我们可以通过 MIDI 文件将不同的旋律输入模型,或者我们可以尝试其他参数;我们可以增加或减少随机性(temperature参数),或让序列继续较长时间(num_steps参数)。

工作原理…

MelodyRNN 是一种基于 LSTM 的音符语言模型。为了理解 MelodyRNN,我们首先需要了解长短期记忆LSTM)的工作原理。1997 年由 Sepp Hochreiter 和 Jürgen Schmidhuber 发布(长短期记忆doi.org/10.1162%2Fneco.1997.9.8.1735),LSTM 是循环神经网络RNN)的最著名例子,代表了处理图像识别、语音识别、自然语言处理和时间序列等任务的最先进模型。LSTM 曾或正在背后支持 Google、Amazon、Microsoft 和 Facebook 的流行工具,用于语音识别和语言翻译。

LSTM 层的基本单元是 LSTM 单元,由几个调节器组成,我们可以在下面的示意图中看到:

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

本图表基于 Alex Graves 及其它人的深度递归神经网络语音识别(2013),摘自维基百科关于 LSTMs 的英语条目,网址为en.wikipedia.org/wiki/Long_short-term_memory

监管机构包括以下内容:

  • 一个输入门

  • 一个输出门

  • 一个遗忘门

我们可以解释这些门背后的直觉,而不陷入方程的细节。一个输入门调节输入对单元的影响力,一个输出门减少传出单元的激活,而遗忘门则衰减单元的活动。

LSTMs 能够处理不同长度的序列,这是它们的优点。然而,随着序列长度的增加,它们的性能会下降。为了学习更长的序列,Magenta 库提供了包含注意力机制的模型(Dzmitry Bahdanau 及其它人,2014 年,通过联合学习对齐和翻译来进行神经机器翻译arxiv.org/abs/1409.0473)。Bahdanau 及其它人表明,他们的注意力机制显著改善了处理长序列的性能。

在 MelodyRNN 中,注意力掩码a的应用如下:

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

您可以在 Magenta 文档中找到更多细节:magenta.tensorflow.org/2016/07/15/lookback-rnn-attention-rnn/

另见

请注意,Magenta 有不同版本的 MelodyRNN 模型可用(github.com/magenta/magenta/tree/master/magenta/models/melody_rnn)。除了 MelodyRNN 外,Magenta 还提供了其他模型,包括用于生成音乐的变分自编码器,以及许多基于浏览器的工具用于探索和生成音乐:github.com/magenta/magenta

DeepBeat 是一个用于嘻哈节拍生成的项目:github.com/nicholaschiang/deepbeat

Jukebox 是基于 Dhariwal 及其它人的论文Jukebox: A Generative Model for Music(2020 年;arxiv.org/abs/2005.00341)的开源项目。您可以在openai.com/blog/jukebox/找到许多音频样本。

您可以在github.com/pkmital/time-domain-neural-audio-style-transfer找到 Parag K. Mital 的 NIPS 论文时域神经音频风格转换(2017 年)的原始实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值