TensorFlow2 神经网络应用指南(三)
十、推荐系统
推荐系统(RSs)是强大的信息过滤系统,它根据用户的偏好和项目的特征对项目进行排序并推荐给用户。这些推荐可以从看哪部电影到购买什么产品,从听哪首歌到接受哪种服务。推荐系统的目标是向用户推荐合适的项目,以建立信任关系,实现长期的商业目标。大多数大型科技公司,如亚马逊、网飞、Spotify、YouTube 和谷歌,都在很大程度上受益于推荐系统;亚马逊示例见图 10-1 。
图 10-1
Amazon.com 礼品创意推荐系统
让我们在下一节看看推荐系统的流行方法。
流行的方法
有多种方法可以创建一个强大的推荐系统,但是最常用的两种方法是(I)协同过滤和(ii)基于内容的过滤。在本节中,我们将简要介绍这些过滤方法。
协同过滤
协同过滤是一种推荐方法,它基于具有相似特征的用户的反应来过滤出用户可能偏好的项目。它基于将用户分组为具有相似偏好的更小的组,并向他们推荐组中其他成员满意的项目。
协作过滤的主要假设是,过去同意的用户倾向于将来同意。因此,纯粹的协同过滤系统只需要用户对一组给定项目的历史偏好数据。图 10-2 显示了协同过滤方法的直观解释。
图 10-2
协作推荐系统的描述
协同过滤子方法
在协同过滤方法中也有子方法。协同过滤可以是(I)基于记忆的或者(ii)基于模型的。基于记忆的方法是基于使用选定的度量(例如,余弦相似性或皮尔逊相关)来寻找相似的用户,并对评级进行加权平均。虽然它很容易构建并且更具可解释性,但是当数据有限时,它的性能并不好。另一方面,基于模型的方法利用机器学习来预测未评级项目的预期用户评级。虽然这种方法妨碍了模型的可解释性,但在可用数据有限的情况下,这种方法更有效。
数据收集
由于协同过滤是基于用户的历史数据,所以采用协同方法开发推荐系统的一个重要步骤是收集用户的反馈和偏好数据。该数据可以是用户的显式反馈或隐式行为。
显式数据收集
显式数据收集包括用户直接提供给系统的所有数据。这包括
-
用户对滑动标尺上的项目的评分
-
用户的项目在收藏中从最喜欢到最不喜欢的排序
-
用户在两个或多个项目之间的选择
-
用户最喜欢的项目列表
隐式数据收集
隐式数据收集基于用户的可观察行为。这些观察可以在系统内部进行,也可以在系统外部使用 cookies 和第三方解决方案等工具进行。隐式数据包括
-
用户的已查看项目列表
-
用户在线购买的商品的记录
-
用户访问的网站
-
用户的社交网络参与度
关于协同过滤的问题
尽管协同过滤工作得非常好,但是推荐系统可能会遇到三个常见问题:
-
冷启动
-
可量测性
-
稀少
冷启动
当推荐系统首次被部署时,关于用户和项目的可用数据的大小是最小的,这是非常普遍的。此外,即使当推荐系统成熟时,关于新增加的用户或产品的足够信息也可能比期望的数量少得多,这对于做出有价值的推荐是至关重要的。因此,当没有足够的信息时,推荐系统往往不能提供有用的推荐,这被称为“冷启动”问题。
可量测性
关于基于协同过滤的推荐系统的另一个问题是可扩展性。特别是当基于存储器的方法被用于具有数百万用户的系统时,计算相似性度量可能变成非常耗时和资源密集型的任务。
稀少
最后,收集关于项目的足够信息可能是建立成功的推荐系统的另一个问题。这是因为基于观看、销售或收听的项目的反馈百分比较低。因此,低水平的反馈比率可能会降低结果的重要性,并提供不正确的项目排名。
基于内容的过滤(基于个性的方法)
基于内容的过滤是推荐系统的另一种流行方法。内容指的是用户参与的项目的内容或属性。在基于内容的过滤方法中,项目被分类,并且基于用户的有限反馈,系统推荐属于用户喜欢的类别的新项目。例如,当你给动作片正面评价,给儿童片负面评价时,基于内容过滤的推荐系统会给你推荐另一部动作片。
对于基于内容的过滤,项目和用户都用关键字标记,以便对它们进行分类。项目基于它们的属性被标记,而为了标记用户,专用模型被设计为基于他们与推荐系统的交互来创建用户简档。向量空间表示算法(例如,tf-idf)用于提取项目的特征。然后,系统根据算法做出推荐。
近年来,单纯基于内容过滤的推荐系统已经不再流行。通常,基于内容的过滤与其他过滤方法一起使用来创建混合模型。
其他推荐系统方法
除了协作和基于内容的推荐系统之外,还有其他种类的推荐系统正在被越来越多地使用。
-
多标准推荐系统:这些推荐系统使用不止一个标准来进行推荐。一般来说,推荐系统收集对一个项目的单个偏好评级。但是更复杂的用户评级系统可以帮助创建更准确和更先进的推荐系统。例如,在电影推荐系统中,收集特定电影方面(例如,表演、视觉效果、演员)的评级可以提高推荐性能,而不是收集给定电影的总体评级。
-
风险感知推荐系统:集成了风险度量的推荐系统称为风险感知推荐系统。例如,推荐的频率(例如,30 次/天)和定时(例如,在营业时间)可能影响用户的体验,并且这些推荐系统将这些特征考虑在内以增强用户体验。
-
移动推荐系统:移动推荐系统利用移动设备收集的数据。这些推荐系统通常实时操作,基于用户的变化状态(例如,用户的位置)进行瞬时更新。
-
混合推荐系统:混合推荐系统结合了多种方法,如基于内容的过滤、协同过滤和风险评估。他们从每种方法的输出中进行选择、混合或加权,并提供最终的推荐输出。
请注意,前面的列表是一个不完整的列表,它随着计算机和数据科学的进步而增长。
案例研究|使用 MovieLens 数据集的深度协同过滤
我们将使用模型子类化构建一个定制的神经网络来实现协同过滤。记住,协同过滤的主要假设是,过去同意的用户将来也会同意。因此,纯粹的协同过滤系统只需要用户对一组项目的历史偏好。
MovieLens 数据集
在这个案例研究中,我们使用了一个流行的电影评级数据集 MovieLens,它是由 GroupLens 设计和维护的。GroupLens 是明尼苏达大学计算机科学与工程系的一个研究实验室,他们在位于 https://grouplens.org/datasets/movielens/ 的网页上维护了大量数据集。在此页面上,您可以访问许多具有不同数量观察值的数据集。
我们更喜欢一个小数据集:MovieLens 最新的小数据集包括 100,000 个评级和 3,600 个标签应用程序,由 600 个用户应用于 9,000 部电影。数据集的大小大约只有 1 MB,这使得我们的网络训练过程非常快。数据集在 http://files.grouplens.org/datasets/movielens/ml-latest-small.zip 可用。
在案例研究期间,我们将深入研究数据集的列(即,userId、movieId、rating、timestamp)的含义。让我们从最初的进口开始
初始进口
本案例研究需要六个初始导入,它们是为了以下功能而导入的:
-
TensorFlow :建立和训练我们的模型,并进行预测
-
ZipFile :解压保存为 zip 文件的 MovieLens 数据集
-
熊猫:创建数据帧并执行基本的数据处理任务
-
NumPy :生成 NumPy 数组,进行数据处理任务
-
来自 scikit 的
train_test_split-学习:进行训练和测试分割操作 -
从 TensorFlow 嵌入:从 TensorFlow 导入嵌入图层
-
get_file来自 TensorFlow :从外部 URL 下载数据集
下面几行导入所有相关的库和函数:
import tensorflow as tf
from zipfile import ZipFile
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from tensorflow.keras.layers import Embedding
from tensorflow.keras.utils import get_file
加载数据
既然我们已经完成了最初的导入,我们就可以把精力集中在数据处理和模型构建上了。我们将从官方发布者的网站 GroupLens.org 下载和加载我们的数据。然后,我们将使用 TensorFlow 的get_file()函数下载数据集,代码如下:
URL = "http://files.grouplens.org/datasets/movielens/ml-latest-small.zip"
movielens_path = get_file("movielens.zip", URL, extract=True)
Colab 将临时下载并保存包含多个 CSV 文件的 zip 文件。为了能够打开这些 CSV 文件中的一个,我们需要ZipFile()函数,其工作方式如下:
with ZipFile(movielens_path) as z:
with z.open("ml-latest-small/ratings.csv") as f:
df = pd.read_csv(f)
使用前面的代码,我们将评级表保存为 Pandas DataFrame,如图 10-3 所示。
图 10-3
评级数据集的前五行
评级数据框架有四列:
-
userId :每个用户的 Id 号
-
movieId :每部电影的 Id 号
-
评分:特定用户对电影的评分
-
时间戳:显示用户对电影的评分时间
现在我们知道了我们的数据集,是时候处理我们的列并为深度学习模型做准备了。
处理数据
在我们的 MovieLens 数据集中,用户 id 从 1 开始,电影 id 不是连续的。这对于训练期间的计算效率来说不是很健康。因此,我们会给他们新的 ID 号,以后可以映射回原来的 ID 号。
正在处理用户 id
我们首先需要枚举唯一的用户 id,并根据枚举的用户 id 创建一个字典。然后,我们还用这些枚举 id 创建一个反向字典(键和值是反向的)。然后,我们为新用户 id 创建一个新列,名为user。最后,我们用下面的代码将唯一用户计数保存为num_users:
user_ids = df["userId"].unique().tolist()
user2user_encoded = {x: i for i, x in enumerate(user_ids)}
user_encoded2user = {i: x for i, x in enumerate(user_ids)}
df["user"] = df["userId"].map(user2user_encoded)
num_users = len(user_encoded2user)
正在处理电影 id
对于电影 id,我们也遵循与用户 id 相似的路径。这一步对于电影 id 更为重要,因为这些 id 在我们的数据集中不是连续给出的。下面的代码处理电影 id,用新 id 创建一个新列,并将唯一的电影计数保存为num_movies:
movie_ids = df["movieId"].unique().tolist()
movie2movie_encoded = {x: i for i, x in enumerate(movie_ids)}
movie_encoded2movie = {i: x for i, x in enumerate(movie_ids)}
df["movie"] = df["movieId"].map(movie2movie_encoded)
num_movies = len(movie_encoded2movie)
现在,我们可以使用以下代码查看数据集中有多少用户和电影:
print("Number of users: ", num_users,
"\nNumber of Movies: ", num_movies)
Output:
Number of users: 610
Number of Movies: 9724
处理评级
对于评级,我们所要做的就是为了计算效率和模型的可靠性将它们标准化。我们需要检测最小和最大额定值,然后应用 lambda 函数进行最小最大标准化。下面的代码成功地做到了这一点:
min, max = df["rating"].min(), df["rating"].max()
df["rating"] = df["rating"].apply(lambda x:(x-min)/(max-min))
让我们用图 10-4 最后看一下我们处理过的测向数据帧:
图 10-4
已处理评级数据集的前五行
分割数据集
由于这是一项监督学习任务,我们需要将数据分为(I)特征(x)和标签(Y)以及(ii)训练和验证集。
对于要素和标注分割,我们只需选择列并将其另存为新变量,如下所示:
X = df[["user", "movie"]].values
y = df["rating"].values
新的user和movie列是我们的功能,我们将使用它们来预测一部未上映电影的用户评级。
对于训练和验证分割,我们可以使用 scikit-learn 中的train_test_split()函数,它可以分割和打乱我们的数据集。以下代码足以拆分我们的数据集:
(x_train, x_val, y_train, y_val) = train_test_split(
X, y,
test_size=0.1,
random_state=42)
让我们来看看四个新数据集的形状:
print("Shape of the x_train: ", x_train.shape)
print("Shape of the y_train: ", y_train.shape)
print("Shape of the x_val: ", x_val.shape)
print("Shape of the x_val: ", y_val.shape)
Output:
Shape of the x_train: (90752, 2)
Shape of the y_train: (90752,)
Shape of the x_val: (10084, 2)
Shape of the x_val: (10084,)
构建模型
在 TensorFlow 中,除了顺序 API 和函数 API 之外,还有第三种构建模型的选择:模型子类化。在模型子类化中,我们可以自由地从头开始实现任何东西。模型子类化是完全可定制的,使我们能够实现我们自己的定制模型。这是一个非常强大的方法,因为我们可以建立任何类型的模型。但是,它需要基本的面向对象编程知识。我们的自定义类将子类化tf.keras.Model对象。它还需要声明几个变量和函数。不过,这没什么可怕的。要构建模型,我们只需完成以下任务:
-
创建一个类扩展
keras.Model对象。 -
创建一个
__init__函数来声明我们在模型中使用的七个变量:-
embedding_size -
num_users -
user_embedding -
user_bias -
num_movies -
movie_embedding -
movie_bias
-
-
创建一个调用函数,告诉模型如何用函数
.初始化变量来处理输入 -
在一个 Sigmoid 激活层之后返回输出。
下面的代码做到了所有这些(请注意,大部分代码都是注释):
class RecommenderNet(tf.keras.Model):
# __init function is to initialize the values of
# instance members for the new object
def __init__(self, num_users, num_movies, embedding_size, **kwargs):
super(RecommenderNet, self).__init__(**kwargs)
# Variable for embedding size
self.embedding_size = embedding_size
# Variables for user count, and related weights and biases
self.num_users = num_users
self.user_embedding = Embedding(
num_users,
embedding_size,
embeddings_initializer="he_normal",
embeddings_regularizer=tf.keras.regularizers.l2(1e-6),
)
self.user_bias = Embedding(num_users, 1)
# Variables for movie count, and related weights and biases
self.num_movies = num_movies
self.movie_embedding = Embedding(
num_movies,
embedding_size,
embeddings_initializer="he_normal",
embeddings_regularizer=tf.keras.regularizers.l2(1e-6),
)
self.movie_bias = Embedding(num_movies, 1)
def call(self, inputs):
# call function is for the dot products
# of user and movie vectors
# It also accepts the inputs, feeds them into the layers,
# and feed into the final sigmoid layer
# User vector and bias values with input values
user_vector = self.user_embedding(inputs[:, 0])
user_bias = self.user_bias(inputs[:, 0])
# Movie vector and bias values with input values
movie_vector = self.movie_embedding(inputs[:, 1])
movie_bias = self.movie_bias(inputs[:, 1])
# tf.tensordot calculcates the dot product
dot_user_movie = tf.tensordot(user_vector, movie_vector, 2)
# Add all the components (including bias)
x = dot_user_movie + user_bias + movie_bias
# The sigmoid activation forces the rating to between 0 and 1
return tf.nn.sigmoid(x)
在声明了RecommenderNet类之后,我们可以创建这个定制类的一个实例来构建我们的定制RecommenderNet模型:
model = RecommenderNet(num_users, num_movies, embedding_size=50)
编译和训练模型
创建模型后,我们可以配置它。由于我们致力于预测一部未上映电影的收视率,这更像是一项回归任务。因此,使用均方误差(MSE)度量–而不是交叉熵度量–将是更好的选择。此外,我们还选择 Adam optimizer 作为我们的优化器。以下代码完成了所有这些工作:
model.compile(
loss='mse',
optimizer=tf.keras.optimizers.Adam(lr=0.001)
)
我们将使用以下代码为 5 个时期训练我们的定制模型:
history = model.fit(
x=x_train,
y=y_train,
batch_size=64,
epochs=5,
verbose=1,
validation_data=(x_val, y_val),
)
图 10-5 显示了每个时期的 MSE 损失值。
图 10-5
在我们的定制模型训练期间的纪元统计
提出建议
现在我们的模型已经训练好了,可以用协同过滤来做推荐了。我们可以用下面的代码随机选择一个用户 ID:
user_id = df.userId.sample(1).iloc[0]
print("The selected user ID is: ", user_id)
Output: The selected user ID is: 414
下一步是过滤掉用户以前看过的电影。下面的代码列出了用户以前没有看过的电影:
movies_watched = df[df.userId == user_id]
not_watched = df[~df['movieId'].isin(movies_watched.movieId.values)]['movieId'].unique()
not_watched = [[movie2movie_encoded.get(x)] for x in not_watched]
print('The number of movies the user has not seen before: ', len(not_watched))
Output: The number of movies the user has not seen before is 7026
通过下面的代码,我们获得了在初始数据处理步骤中提供给用户的新 ID 号,然后使用np.hstack()函数创建一个 NumPy 数组,并使用model.predict()函数生成预测的电影评级:
user_encoder = user2user_encoded.get(user_id)
user_movie_array = np.hstack(
([[user_encoder]] * len(not_watched), not_watched )
)
ratings = model.predict(user_movie_array).flatten()
前面的代码给了我们一个 NumPy 数组,其中包含所有电影的标准化评级值。但是我们不需要全部。我们只需要收视率最高的前 10 部电影。此外,我们需要他们的身份证号码,以便我们可以映射他们,了解他们有哪些头衔。
NumPy argsort()函数对所有项目进行排序,并返回它们的索引(id)。最后,我们需要颠倒它们,因为它是按升序工作的。以下代码完成了所有这些任务:
top10_indices = ratings.argsort()[-10:][::-1]
以下代码将我们分配的电影 id 转换为数据集中给定的原始电影 id:
recommended_movie_ids = [
movie_encoded2movie.get(not_watched[x][0]) for x in top10_indices
]
现在我们有了前 10 部电影的原始 id。但是我们不能只向用户显示电影 id。相反,我们希望向他们显示电影标题及其类型信息。因此,我们需要使用 zip 文件中的另一个 CSV 文件:movies.csv。以下代码将加载数据集并创建一个名为movie_df的熊猫数据帧(输出见图 10-6 ):
# Create a DataFrame from Movies.csv file
with ZipFile(movielens_path) as z:
with z.open("ml-latest-small/movies.csv") as f:
movie_df = pd.read_csv(f)
movie_df.head(2)
图 10-6
电影数据集的前两行
让我们通过过滤用户观看的前 10 部电影来检查用户已经观看并给出高评级的电影:
top_movies_user = (
movies_watched.sort_values(by="rating", ascending=False)
.head(10)
.movieId.values
)
movie_df_rows = movie_df[movie_df["movieId"].isin(top_movies_user)]
我们可以通过运行下面的代码来查看它们,如图 10-7 所示:
print("Movies with high ratings from user")
movie_df_rows[['title','genres']]
图 10-7
用户评价高的电影列表
现在我们还可以查看我们的协同过滤模型推荐给用户的前 10 部电影,代码如下,如图 10-8 。
recommended_movies = movie_df[movie_df["movieId"].isin(recommended_movie_ids)]
print("Top 10 movie recommendations")
recommended_movies[['title','genres']]
图 10-8
为用户推荐的前 10 部电影
正如你所看到的,用户观看的大多数电影都是经典电影,我们的推荐系统还向用户推荐了从 20 世纪 40 年代到 20 世纪 70 年代的电影。此外,观看的电影和推荐的电影在类型上也很相似。
在这个案例研究中,我们成功地构建了一个基于纯协同过滤方法的工作推荐系统。您可以轻松地更改userId并为其他用户提供建议,以测试模型的成功。此外,您可以使用不同的、可能更大的 MovieLens 数据集来提高模型的准确性。试着改变变量,测试你的模型。
结论
在这一章中,我们介绍了推荐系统,它可以使用神经网络来构建。我们介绍了推荐系统的不同方法,并使用基于深度协同过滤的 MovieLens 数据集构建了一个推荐系统。这个推荐系统能够推荐用户最喜欢的未看过的电影。
在下一章,我们将介绍自编码器网络,它主要用于无监督学习任务。
十一、自编码器
在前几章中,我们讨论了前馈神经网络、CNN 和 rnn。这些网络主要用于监督学习任务。在这一章中,我们重点关注自编码器(见图 11-1 ),一种主要用于无监督学习任务的神经网络架构。
自编码器的主要承诺是学习给定数据集的编码结构和解码结构。自编码器主要用于降维、降噪和一些生成任务。有几种为特定任务设计的自编码器,但首先,让我们深入了解自编码器的架构。
图 11-1
自编码器架构的粗略可视化
自编码器的优点和缺点
自编码器是非常有前途的神经网络架构,被认为是其他无监督机器学习模型(如主成分分析)的强大替代方案。
实际上,自编码器可以做 PCA 模型所做的一切,甚至更多。纯线性自编码器将给出与 PCA 相同的结果。但是,在非线性特征提取问题中,自编码器可以比 PCA 模型做得更多。在大多数情况下,这些问题具有非线性性质,因此,它们肯定优于 PCA 模型。
但并非一切都是黑白分明的。就像其他神经网络架构一样,与 PCA 模型相比,自编码器需要大量数据和计算能力。此外,自编码器训练中使用的结构不良的训练数据集甚至会进一步模糊我们试图提取的特征,因为自编码器专注于提取所有信息,而不是提取相关信息。因此,结构不良的数据集可能对解决机器学习任务有害。
在半监督学习任务中,自编码器可以与不同的神经网络架构耦合,例如前馈 NNs、CNN 和 RNNs。这些组合可以在几个机器学习任务中提供成功的结果。但这也可能进一步损害模型的可解释性。尽管存在缺点,但自编码器提供了许多好处,并且可以(I)与其他神经网络结合使用,以及(ii)独立地用于无监督和半监督的学习任务。
自解压体系结构
自编码器是由 Hinton 和 PDP 小组在 20 世纪 80 年代首次推出的。该建议的主要目的是解决无监督的反向传播问题(也称为“无教师反向传播”问题)。
自编码器网络最重要的结构特征是能够对输入进行编码,但只能解码为原始形式。因此,自编码器的输入端和输出端几乎只接收相同的数据。这将消除监管标签数据的必要性。因此,在每个自编码器网络中有一个编码器网络和一个解码器网络。这些编码器和解码器网络通过一个狭窄的潜在空间连接,如图 11-2 所示。
图 11-2
自编码器网络的例子
由于自编码器的主要任务是确保输入和输出值的等效性,因此自编码器网络被迫保留网络中最相关的信息,以便最终重建输入值。这种性质使得自编码器非常适合于降维、特征学习和降噪(即去噪)。
自编码器的最基本形式由三个主要部分组成:(I)输入层,(ii)潜在空间层,和(iii)输出层。输入层与潜在空间一起构成编码器网络,而输出层与潜在空间一起构成解码器网络。简单的多层感知器将编码器和解码器结合在一起,是基本自编码器的一个例子,如图 11-3 所示。
图 11-3
基本自编码器网络的示例
目标是以优化的方式调整权重,以最小化输入图层值和输出图层值之间的差异。这是通过误差项的反向传播实现的,类似于前馈神经网络。
自编码器中使用的层
自编码器中可以使用的层可能因问题而异。每个自编码器必须有一个编码器和一个解码器网络,它们通过一个层,即潜在空间连接。这是 autoencoder 的承诺,您可以在这些网络中添加任何类型的层,包括但不限于密集层、卷积层、池化层、LSTM 层和 GRU 层。事实上,根据手头任务的性质,编码器和解码器网络可以设计为独立的前馈、CNN 或 RNN 网络。另一方面,在大多数自编码器应用中,解码器网络被设计为编码器网络的反向版本,以确保模型的收敛性。例如,当您使用卷积层构建基于 CNN 的编码器时,解码器网络必须由转置卷积层组成。
深度的优势
在图 11-3 中,你可以看到自编码器的基本版本。但是,在大多数实际应用中,编码器和解码器网络中插入多层有三个原因:
-
与浅层自编码器相比,压缩效果更好:与浅层自编码器相比,多层自编码器在将重要信息压缩到潜在空间方面做得更好。
-
更低的代价(误差)测度:一般来说,多层网络更擅长收敛于复杂函数,这就降低了 MSE 等代价测度。
-
所需的训练数据量更少:当可用数据量有限时,多层自编码器比浅层自编码器收敛得更好。
在下一节中,让我们看看自编码器的变化。
自编码器的变体
所有的自编码器类型都有一个编码器-解码器架构,但是有几种不同的自编码器来处理特定的机器学习任务。有三大类:(I)欠完备自编码器,(ii)正则化自编码器,和(iii)变分自编码器。
欠完整自编码器
欠完整自编码器是基本的自编码器,它将潜在空间中的神经元数量限制为比输入层更小的尺寸。与输入层中的神经元计数相比,其潜在空间中的神经元计数较小的自编码器称为欠完整自编码器。
欠完成自编码器将输入复制到输出,这似乎毫无意义。但是,自编码器的有用部分是它的潜在空间,解码器的输出很少被使用。自编码器的目标是通过在位于编码器和解码器网络交叉点的潜在空间中总结特征来提取特征。
然而,在某些情况下,autoencoder 只是将任务从编码器复制到解码器(即,从输入到输出),而没有学到任何重要的东西。为了能够消除这种可能性,自编码器的容量受到正则化方法的限制。这些容量有限的自编码器组成了正则化自编码器家族。
正则化自编码器
在自编码器中遇到的主要问题之一是倾向于为解码器制作编码器结构的对称副本。这个问题损害了自编码器从模型中获得有意义的特征的能力。有几种方法可以防止自编码器为解码器复制其编码器网络,这对捕获信息至关重要。规则化自编码器的变体配置有专门的成本函数,该成本函数鼓励这些自编码器发现有意义的特征,并防止它们将输入无用地复制到输出。正则化自编码器有三种流行的变体:
-
稀疏自编码器
-
降噪自编码器(DAE)
-
收缩式自编码器
稀疏自编码器
稀疏自编码器(SAE)是依赖于潜在空间内活动神经元的稀疏性的自编码器。一般来说,潜在空间中的神经元数量少于输入层和输出层中的神经元数量,这使得它们欠完备。另一方面,有一些自编码器,其潜在空间中的神经元比输入层中的多,这被称为过完备。
欠完整和过完整自编码器在特定情况下都可能无法学习到有意义的特征,而稀疏自编码器通过向潜在空间引入稀疏性来解决这个问题。在训练过程中,一些神经元被故意停用,这迫使模型从数据中学习有意义的特征。因此,自编码器必须响应数据集的独特统计特征,而不仅仅是作为满足等式的唯一目的的身份函数。稀疏自编码器通常用于提取用于另一项任务(如分类)的特征。
降噪自编码器(DAE)
去噪自编码器(DAE)是特殊的自编码器,其被设计成通过进行精确的近似来最小化原始输入和输入的损坏副本之间的误差。因此,去噪自编码器必须找到方法来测量损坏的副本和原始副本之间的差异。在他们了解损坏的副本和原始副本之间的差异以及如何消除这种差异后,他们可以用来清理有噪声的数据。例如,我们可以使用图像数据集及其添加了噪声的副本来训练去噪自编码器网络。然后,这个经过训练的模型可以在现实世界中用于清理有噪声的图像文件。
收缩式自编码器
收缩型自编码器(CAE)主要与其他类型的自编码器并行使用。由于它们对训练数据集中的小变化不太敏感,因此它们在维数减少和生成任务中非常方便,特别是当其他自编码器类型无法学习有意义的特征时。这种学习是通过向优化器算法试图最小化的成本函数添加特定的正则项来实现的。该特定正则化符对应于关于输入数据的编码器激活的雅可比矩阵(函数的所有一阶偏导数的矩阵)的 Frobenius 范数。
图 11-4
变分自编码器的可视化
虽然收缩自编码器的正则化策略类似于稀疏自编码器,但是其对小变化的阻力–尽管采用不同的手段–也显示出与去噪自编码器的阻力相似。如前所述,当其他自编码器无法学习有意义的特性时,它们通常与其他自编码器一起使用,作为最后的手段。
可变自编码器(VAE)
与其他自编码器(如稀疏和去噪自编码器)不同,变分自编码器(vae)主要用于生成任务。它们的功能更类似于生成对抗网络,并且由于它们的网络架构(由编码器网络和解码器网络组成),它们被视为自编码器的变体。
对于生成性任务,我们需要连续函数的随机变化。然而,普通的自编码器不提供连续的空间。因此,与其他自编码器相比,VAEs 的不同之处在于它位于潜在空间中的连续空间。
连续空间由两个神经元创建,一个均值神经元和一个方差神经元。这两个神经元用来得到一个采样编码,这个采样编码传递给解码器,如图 11-4 所示。由于编码是从具有与输入相同的均值和方差的分布中生成的,所以解码器从涉及相同潜在空间的所有邻近点中学习,这使得模型能够使用输入数据生成相似但不相同的输出。
自编码器的使用案例
尽管自编码器的传统用例是降维,但是随着围绕自编码器的研究的成熟,已经观察到了自编码器的新用例。自编码器使用案例的非详尽列表如下:
-
降维:通过将输入层的高特征空间映射到潜在空间的低特征空间,自编码器可以降低维数。具有线性激活函数的非常基本的自编码器将使用主分量分析(PCA)方法呈现相同的结果。
-
降噪:尤其是降噪自编码器可以成功去除图像、视频、声音和其他类型数据中的噪声。
-
图像处理:自编码器可用于图像压缩和图像去噪。
-
药物发现:变分编码器由于其生成性,可用于药物发现。
-
机器翻译:通过将源语言的文本作为输入,将目标语言的文本作为输出,自编码器可以学习神经机器翻译所需的重要特征。
-
此外,自编码器还用于许多其他任务,如信息检索、异常检测、群体合成和流行度预测。
案例研究|时尚 MNIST 图像去噪
既然我们已经讨论了自编码器的概念部分,我们可以继续进行案例研究了。在这个案例研究中,我们采用了 TensorFlow 的一个官方教程“自编码器简介”。 1
案例研究的目标是去噪(清除噪声)图像。对于这项任务,我们应用随机噪声的整个数据集。然后,我们将这个包含噪声图像的数据集提供给自编码器的一端,而将干净的版本提供给另一端。在训练步骤之后,我们的自编码器学习如何清除图像噪声。
时尚 MNIST 数据集
在这个案例研究中,我们使用人工智能社区的另一个流行数据集:时尚 MNIST。时尚 MNIST 由位于德国柏林的欧洲电子商务公司 Zalando 设计和维护。时尚 MNIST 由 60,000 幅图像的训练集和 10,000 幅图像的测试集组成。每个示例都是 28 x 28 灰度图像,与来自 10 个类别的标签相关联。时装 MNIST,包含服装项目的图像(如图 11-5 所示),被设计为包含手写数字的 MNIST 数据集的替代数据集。
初始进口
本案例研究需要七个初始导入,它们是为了以下功能而导入的:
-
TensorFlow :建立和训练我们的模型,并进行预测
-
Matplotlib :发现我们的数据集并可视化我们的结果
-
NumPy :生成 NumPy 数组,进行数据处理任务
-
熊猫:创建数据帧并执行基本的数据处理任务
-
fashion_mnist来自 TensorFlow :将时尚 MNIST 数据集直接加载到 Colab 笔记本 -
来自 scikit 的
train_test_split-学习:进行训练和测试分割操作 -
TensorFlow 中的
Conv2DTranspose、Conv2D和Input图层:使用这些图层构建自编码器模型
下面几行导入所有相关的库和方法:
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from tensorflow.keras.datasets import fashion_mnist
from sklearn.model_selection import train_test_split
from tensorflow.keras.layers import Conv2DTranspose, Conv2D, Input
加载和处理数据
初始导入后,我们可以使用以下代码轻松下载和加载时尚 MNIST 数据集:
# We don't need y_train and y_test
(x_train, _), (x_test, _) = fashion_mnist.load_data()
print('Max value in the x_train is', x_train[0].max())
print('Min value in the x_train is', x_train[0].min())
Output:
Max value in the x_train is 255
Min value in the x_train is 0
现在我们有两个数据集,包含代表图像像素值的数组。注意,我们不会使用标签,所以我们甚至没有保存 y 值。
让我们取一个数据集样本,并使用以下 Matplotlib 代码绘制图像:
fig, axs = plt.subplots(5, 10)
plt.figure(figsize=(5, 10))
fig.tight_layout(pad=-1)
a = 0
for i in range(5):
for j in range(10):
axs[i, j].imshow(tf.squeeze(x_test[a]))
axs[i, j].xaxis.set_visible(False)
axs[i, j].yaxis.set_visible(False)
a = a + 1
plt.gray()
图 11-5 显示了输出,即所选服装项目的网格:
Output:
图 11-5
来自时尚 MNIST 数据集的示例
为了计算效率和模型可靠性,我们必须对我们的图像数据应用最小最大归一化,将值范围限制在 0 和 1 之间。因为我们的数据是 RGB 格式的,所以我们的最小值是 0,最大值是 255,我们可以用下面几行进行最小最大规范化操作:
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
我们还必须调整 NumPy 数组的形状,因为数据集的当前形状是(60000,28,28)和(10000,28,28)。我们只需要添加一个具有单一值的第四维(),例如从(60000,28,28)到(60000,28,28,1) 。第四维空间很大程度上证明了我们的数据是灰度格式的,用一个值表示从白色到黑色的颜色信息。如果我们有彩色图像,那么我们就需要第四维中的三个值。但是我们所需要的是包含单一值的第四维,因为我们使用灰度图像。以下代码行可以做到这一点:
x_train = x_train[..., tf.newaxis]
x_test = x_test[..., tf.newaxis]
让我们用下面几行来看看 NumPy 数组的形状:
print(x_train.shape)
print(x_test.shape)
Output:
(60000, 28, 28, 1)
(10000, 28, 28, 1)
向图像添加噪声
记住,我们的目标是创建一个去噪的自编码器。对于这项任务,我们需要图像文件的干净和嘈杂的副本。自编码器的任务是调整其权重,以复制噪声处理过程,并能够消除噪声图像。换句话说,我们故意在图像中添加随机噪声来扭曲它们,这样我们的自编码器就可以知道它们是如何变成噪声的,以及如何去噪。因此,我们需要在现有图像中添加噪声。
我们通过使用tf.random.normal方法给每个数组项添加一个随机生成的值。然后,我们将随机值乘以一个noise_factor,您可以随意使用它。以下代码向图像添加了噪声:
noise_factor = 0.6
x_train_noisy = x_train + noise_factor * tf.random.normal(shape=x_train.shape)
x_test_noisy = x_test + noise_factor * tf.random.normal(shape=x_test.shape)
我们还需要确保数组项的值在 0 到 1 的范围内。为此,我们可以使用tf.clip_by_value方法。clip_by_value是一种 TensorFlow 方法,它截取最小值-最大值范围之外的值,并用指定的最小值或最大值替换它们。以下代码将值截取到范围之外:
x_train_noisy = tf.clip_by_value(x_train_noisy, clip_value_min=0., clip_value_max=1.)
x_test_noisy = tf.clip_by_value(x_test_noisy, clip_value_min=0., clip_value_max=1.)
现在我们有了噪声和干净的图像,让我们用下面的代码来看看随机噪声的效果:
n = 5
plt.figure(figsize=(20, 8))
for i in range(n):
ax = plt.subplot(2, n, i + 1)
plt.title("original", size=20)
plt.imshow(tf.squeeze(x_test[i]))
plt.gray()
bx = plt.subplot(2, n, n+ i + 1)
plt.title("original + noise", size=20)
plt.imshow(tf.squeeze(x_test_noisy[i]))
plt.gray()
plt.show()
图 11-6 显示了原始图像及其噪声版本:
Output:
图 11-6
时尚 MNIST 干净与嘈杂的图像示例
正如你所看到的,我们对我们的图像应用了强噪声,没有人能看出底部的图像中有服装项目。但是,通过我们的自编码器,我们将能够对这些极其嘈杂的图像进行降噪处理。
构建模型
正如我们在第十章中所做的,我们再次利用了模型子类化。在模型子类化中,我们可以自由地从头开始实现任何东西。这是一个非常强大的方法,因为我们可以建立任何类型的模型。我们的自定义类将扩展tf.keras.Model对象。它还需要声明几个变量和函数。不过,这没什么可怕的。要构建模型,我们只需完成以下任务:
-
创建一个扩展
keras.Model对象的类。 -
创建一个
__init__函数来声明用顺序 API 构建的两个独立的模型。- 在它们内部,我们需要声明可以相互反转的层。编码器模型的 Conv2D 层,而解码器模型的 conv 2d 转置层。
-
创建一个调用函数,告诉模型如何使用初始化变量和
__init__方法处理输入:-
我们需要调用初始化的编码器模型,该模型将图像作为输入。
-
我们还需要调用初始化的解码器模型,它将编码器模型的输出(已编码)作为输入。
-
-
返回解码器的输出。
以下代码完成了所有这些工作:
class Denoise(tf.keras.Model):
def __init__(self):
super(Denoise, self).__init__()
self.encoder = tf.keras.Sequential([
Input(shape=(28, 28, 1)),
Conv2D(16, (3,3), activation="relu", padding="same", strides=2),
Conv2D(8, (3,3), activation="relu", padding="same", strides=2)])
self.decoder = tf.keras.Sequential([
Conv2DTranspose(8, kernel_size=3, strides=2, activation="relu", padding="same"),
Conv2DTranspose(16, kernel_size=3, strides=2, activation="relu", padding="same"),
Conv2D(1, kernel_size=(3,3), activation="sigmoid", padding="same")])
def call(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
让我们用下面的代码创建一个模型对象:
autoencoder = Denoise()
我们使用 Adam optimizer 作为我们的优化算法,使用均方误差(MSE)作为我们的损失函数。以下代码设置了这些配置:
autoencoder.compile(optimizer='adam', loss="mse")
最后,我们可以通过输入有噪声和干净的图像来运行我们的模型 10 个时期,这将需要大约 1 分钟来训练。我们还使用测试数据集进行验证。以下代码用于训练模型:
autoencoder.fit(x_train_noisy, x_train,
图 11-7 显示了每个历元的训练过程输出:
epochs=10,
shuffle=True,
validation_data=(x_test_noisy, x_test))
图 11-7
在我们的定制模型训练期间的纪元统计
噪声图像去噪
既然我们已经训练了我们的模型,我们可以轻松地完成去噪任务。为了简化预测过程,我们使用测试数据集。但是,您可以随意处理和尝试其他图像,例如 MNIST 数据集中的数字。
现在,我们运行以下行来对有噪声的测试图像进行降噪:
encoded_imgs=autoencoder.encoder(x_test).numpy()
decoded_imgs=autoencoder.decoder(encoded_imgs.numpy()
正如您在这里看到的,我们可以分别使用编码器和解码器网络及其相应的属性。因此,我们首先使用编码器网络对我们的图像进行编码(x_test)。然后,我们在解码器网络中使用这些编码图像(encoded_imgs)来生成我们在开始时使用的图像的干净版本(decoded_imgs)。
我们可以使用以下代码比较测试数据集的前十幅图像的噪声、重建(去噪)和原始版本:
n = 10
plt.figure(figsize=(20, 6))
for i in range(n):
# display original + noise
bx = plt.subplot(3, n, i + 1)
plt.title("original + noise")
plt.imshow(tf.squeeze(x_test_noisy[i]))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
# display reconstruction
cx = plt.subplot(3, n, i + n + 1)
plt.title("reconstructed")
plt.imshow(tf.squeeze(decoded_imgs[i]))
plt.gray()
bx.get_xaxis().set_visible(False)
bx.get_yaxis().set_visible(False)
# display original
ax = plt.subplot(3, n, i + 2*n + 1)
plt.title("original")
plt.imshow(tf.squeeze(x_test[i]))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
图 11-8 显示了所选图像的噪声、重建和原始版本:
输出:
图 11-8
时尚 MNIST 测试数据集样本图像与噪声,重建(去噪)和原始版本
正如你之前看到的,我们的模型可以成功地对非常嘈杂的照片进行降噪,这是以前从未见过的(我们使用了测试数据集)。显然有一些未恢复的失真,例如右边第二个图像中缺失的拖鞋底部。然而,如果你考虑噪声图像的变形程度,我们可以说我们的模型在恢复失真图像方面相当成功。
在我的脑海中,你可以–例如–考虑扩展这个自编码器,并将其嵌入到照片增强应用程序中,这可以增加照片的清晰度和清晰度。
结论
在这一章中,我们介绍了一个神经网络架构,自编码器,它主要用于无监督的学习任务。我们还进行了一个案例研究,其中我们训练了一个能够对失真图像进行去噪的自编码器模型。
在下一章中,我们将深入研究生成对抗网络,它彻底改变了深度学习的生成方面。
Footnotes 1自编码器简介,TensorFlow, www.tensorflow.org/tutorials/generative/autoencoder 上提供
十二、生成对抗网络
生成对抗网络(GANs)是 Ian Goodfellow 和他的同事在 2014 年设计的一种深度学习模型。
GANs 的发明出乎意料。著名的研究员,当时是蒙特利尔大学的博士研究员,伊恩·古德费勒,在一个朋友的告别派对上和他的朋友讨论其他生成算法的缺陷时偶然想到了这个想法。聚会结束后,他满怀希望地回到家中,实施了他心中的构想。令人惊讶的是,在第一次试验中,一切都如他所愿,他成功地创建了生成性对抗网络(简称 GANs)。
据脸书大学人工智能研究主任、纽约大学教授 Yann LeCun 称,GANs 是“过去 10 年机器学习中最有趣的想法”
方法
在 GAN 架构中,有两个神经网络(一个生成器和一个鉴别器)在博弈中相互竞争。在暴露于训练集之后,生成器学习生成具有相似特征的新样本。另一方面,鉴别器试图判断生成的数据是真实的还是伪造的。通过训练,生成器被迫生成接近真实的样本,使得鉴别器无法将它们与训练数据区分开。训练结束后,我们可以使用生成器生成非常真实的样本,如图像、声音和文本。
GANs 最初被设计用来处理无监督的学习任务。然而,最近的研究表明,GANs 在监督、半监督和强化学习任务中表现出良好的结果。
体系结构
如前所述,有两个网络形成了生成性对抗网络:生成者网络和鉴别者网络。这两个网络通过一个潜在的空间相互连接,所有的魔法都在这里发生。换句话说,我们使用发生器网络的输出作为鉴别器网络的输入。让我们深入研究一下生成网络和判别网络,以真正理解 gan 是如何工作的;见图 12-1 :
图 12-1
生成性对抗网络的可视化
GAN 组件
生成网络
生成器网络采用固定长度的随机向量(从随机噪声开始)并生成新样本。它使用高斯分布来生成新的样本,通常从一维层开始,最终被整形为训练数据样本的形状。例如,如果我们使用 MNIST 数据集来生成图像,则生成器网络的输出图层必须对应于图像尺寸(例如,28 x 28 x 1)。这最后一层也被称为潜在空间或向量空间。
鉴别器网络
鉴别器网络以相对相反的顺序工作。生成网络的输出被用作鉴别器网络中的输入数据(例如,28×28×1)。鉴别器网络的主要任务是决定产生的样本是否真实。因此,鉴别器网络的输出由单个神经元密集层提供,该神经元密集层输出所生成样本的真实性的概率(例如,0.6475)。
潜在空间
潜在空间(即向量空间)作为发生器网络的输出和鉴别器网络的输入。生成对抗模型中的潜在空间通常具有原始训练数据集样本的形状。潜在空间试图捕捉训练数据集的特征,以便生成器可以成功地生成接近真实的样本。
一个已知问题:模式崩溃
在生成对抗网络的训练过程中,我们经常会遇到“模式崩溃”的问题。模式崩溃基本上是指未能正确归纳,或者换句话说,未能了解成功生成样本的有意义特征。模式崩溃的形式可能是完全学习失败或学习部分特征失败。例如,当我们处理 MNIST 数据集(从 0 到 9 的手写数字)时,由于模式崩溃问题,我们的 GAN 可能永远不会学习生成一些数字。模式崩溃有两种可能的解释:
-
弱鉴别网络
-
目标函数选择错误
因此,研究一下我们网络的规模和深度,以及目标函数,可能会解决这个问题。
关于建筑的最后笔记
保持发生器和鉴别器网络之间的良性竞争对于构建有用的 GAN 模型是至关重要的。只要这两个网络互相对抗来完善它们的性能,你就可以根据问题自由设计这些网络的内部结构。例如,在处理序列数据时,只要其中一个网络作为生成器网络,而另一个网络作为鉴别器网络,就可以用 LSTM 和 GRU 图层构建两个网络。另一个例子是我们的案例研究。当用 GANS 生成图像时,我们给我们的网络增加了许多卷积或转置卷积层,因为它们降低了图像数据的计算复杂度。
氮化镓的应用
目前有许多领域正在使用 GANs,可列举如下:
-
时尚、艺术和广告
-
制造业和 R&D
-
视频游戏
-
恶意应用程序和深度伪造
-
其他应用
艺术和时尚
生成对抗网络能够“生成”样本。所以,他们天生就有创造力。这就是为什么艺术和时尚是生成性对抗网络最有前途的领域之一。有了训练有素的 GANs,你可以创作绘画、歌曲、服装,甚至诗歌。事实上,Nvidia 的 StyleGAN 网络生成的一幅画“Edmond de Belamy,from La Famille de Belamy”在纽约以 43.25 万美元的价格售出。因此,你可以清楚地看到甘如何有潜力被用于艺术世界。
制造、研究和 R&D
GANs 可用于预测科学研究项目和工业应用中的计算瓶颈。
GAN 网络也可用于提高基于统计分布的图像的清晰度。换句话说,GANs 可以使用统计分布来预测缺失的部分,并产生合适的像素值,这将提高望远镜或显微镜拍摄的图像质量。
视频游戏
GANs 可用于使用小清晰度图像获得更精确和更清晰的图像。这种能力可能被用来使老游戏对新的一代更有吸引力。
恶意应用程序和深度伪造
GANs 可能被用来生成接近真实的虚假社会档案或虚假的名人视频。例如,GAN 算法可用于伪造证据来陷害某人。因此,有许多恶意的 GAN 应用程序,也有许多检测恶意 GAN 生成的样本并将其标记为假的 GAN。
杂项应用
除了前面的用例,gan 还用于以下目的:
-
用于医疗行业的早期诊断
-
在建筑和内部设计行业中生成逼真的图像
-
从图像中重建物体的三维模型
-
对于诸如老化的图像处理
-
以产生可用于癌症研究的蛋白质序列
-
通过声音重建一个人的脸。
生成性对抗性网络应用是巨大的和无限的,它是人工智能界的一个非常热门的话题。既然我们已经讨论了生成性敌对网络的基础,我们可以开始我们的案例研究了。请注意,我们将从 TensorFlow 团队发布的深度卷积 GAN 教程中获取自己的内容。 1
案例研究|使用 MNIST 生成数字
在本案例研究中,我们一步一步地构建了一个生成性对抗网络(GAN),它能够生成手写数字(0 到 9)。为了能够完成这项任务,我们需要建立一个生成器网络和一个鉴别器网络,以便我们的生成模型可以学习欺骗鉴别器模型,后者检查生成器网络生产什么。让我们从最初的进口开始。
初始进口
正如我们在案例研究中经常做的那样,我们进行了一些初始导入,这些导入在我们的 Colab 笔记本的不同单元格中使用。以下行导入 TensorFlow、相关 TensorFlow 图层对象和 Matplotlib:
import tensorflow as tf
from tensorflow.keras.layers import(Dense,
BatchNormalization,
LeakyReLU,
Reshape,
Conv2DTranspose,
Conv2D,
Dropout,
Flatten)
import matplotlib.pyplot as plt
在接下来的部分中,我们还使用其他库,如 os 、 time 、 IPython.display 、 PIL 、 glob 、和 imageio ,但为了保持它们与上下文相关,我们只在需要使用它们时才导入它们。
加载并处理 MNIST 数据集
我们已经讨论过几次 MNIST 数据集的细节。这是一个手写数字数据集,有 60,000 个训练样本和 10,000 个测试样本。如果你想了解更多关于 MNIST 数据集的信息,请参考第七章。
由于这是一个无监督的学习任务,我们只需要特征,因此我们不保存标签数组。让我们用下面几行导入数据集:
# underscore to omit the label arrays
(train_images, train_labels), (_, _) = tf.keras.datasets.mnist.load_data()
然后,我们重塑我们的train_images,使其具有第四维度,并使用以下代码对其进行规范化(在-1 到 1 的范围内):
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5 # Normalize the images to [-1, 1]
然后,我们设置一个BUFFER_SIZE用于混洗,一个BATCH_SIZE用于批量处理数据。然后,我们调用以下函数将 NumPy 数组转换为 TensorFlow Dataset 对象:
# Batch and shuffle the data
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
现在我们的数据被处理和清理。我们可以进入模型构建部分。
构建 GAN 模型
与其他案例研究相比,本案例研究的模型构建部分稍微高级一些。我们需要定义自定义损失、训练步骤和训练循环函数。理解正在发生的事情可能更具挑战性。但是我会尽可能地添加更多的评论,让你更容易理解。此外,将这个案例研究视为成为高级机器学习专家的一条途径。另外,如果你真的关注了评论,那就比看起来容易多了。
发电机网络
作为我们 GAN 网络的一部分,我们首先构建一个带有顺序 API 的生成器。生成器将接受具有 100 个数据点的一维输入,并将其慢慢转换成 28×28 像素的图像数据。由于我们使用该模型从一维输入生成图像,因此使用转置卷积层是最佳选择。转置卷积层的工作方式与卷积层正好相反。它们增加了图像数据的清晰度。在使用转置卷积层之后,我们还利用了批量归一化和泄漏 ReLU 层。下面的代码为我们定义了这个网络:
def make_generator_model():
model = tf.keras.Sequential()
model.add(Dense(7*7*256, use_bias=False, input_shape=(100,)))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Reshape((7, 7, 256)))
assert model.output_shape == (None, 7, 7, 256) # Note: None is the batch size
model.add(Conv2DTranspose(128, (5, 5), strides=(1, 1), padding="same", use_bias=False))
assert model.output_shape == (None, 7, 7, 128)
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Conv2DTranspose(64, (5, 5), strides=(2, 2), padding="same", use_bias=False))
assert model.output_shape == (None, 14, 14, 64)
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Conv2DTranspose(1, (5, 5), strides=(2, 2), padding="same", use_bias=False, activation="tanh"))
assert model.output_shape == (None, 28, 28, 1)
return model
我们可以用下面的代码来声明我们的网络:
generator = make_generator_model()
让我们来看看图 12-2 中的发电机网络总结:
generator.summary()
Output:
图 12-2
我们的发电机网络概述
并使用我们未经训练的生成器网络生成和绘制一个样本,代码如下:
# Create a random noise and generate a sample
noise = tf.random.normal([1, 100])
generated_image = generator(noise, training=False)
# Visualize the generated sample
plt.imshow(generated_image[0, :, :, 0], cmap="gray")
Output is shown in Figure``12-3
图 12-3
未经训练的随机生成样本的示例
鉴别器网络
在生成器网络之后,我们应该构建一个鉴别器网络来检查生成器生成的样本。我们的鉴别器网络必须决定生成图像的伪造概率。因此,它获取生成的图像数据(28 x 28)并输出一个值。对于这个任务,我们使用由泄漏 ReLU 和漏失层支持的卷积层。展平图层将二维数据转换为一维数据,密集图层用于将输出转换为单个值。下面几行定义了鉴别器网络的功能:
def make_discriminator_model():
model = tf.keras.Sequential()
model.add(Conv2D(64, (5, 5), strides=(2, 2), padding="same", input_shape=[28, 28, 1]))
model.add(LeakyReLU())
model.add(Dropout(0.3))
model.add(Conv2D(128, (5, 5), strides=(2, 2), padding="same"))
model.add(LeakyReLU())
model.add(Dropout(0.3))
model.add(Flatten())
model.add(Dense(1))
return model
我们可以通过调用以下函数来创建鉴别器网络:
discriminator = make_discriminator_model()
我们可以看到我们的鉴频器网络总结,代码如下(输出见图 12-4 ):
discriminator.summary()
Output:
图 12-4
我们的鉴别器网络综述
如果我们使用鉴别器网络,我们实际上可以决定我们随机生成的图像是否足够真实:
decision = discriminator(generated_image)
print (decision)
Output:
tf.Tensor([[-0.00108097]], shape=(1, 1), dtype=float32)
正如你所看到的,我们的输出小于零,我们可以得出结论,这个未经训练的生成器网络生成的特定样本是假的。
配置 GAN 网络
作为模型配置的一部分,我们需要为生成器和鉴别器设置损失函数。此外,我们还需要为它们设置单独的优化器。
损失函数
我们从从tf.keras.losses模块创建一个二进制交叉熵对象开始。我们还将参数from_logits设置为 true。创建对象后,我们用定制的鉴别器和生成器损耗函数填充它们。
我们的鉴别器损耗计算为(I)鉴别器对真实图像的预测为一组 1,以及(ii)鉴别器对生成图像的预测为一组 0 的组合。
我们的发电机损耗是通过测量它欺骗鉴别器的能力来计算的。因此,我们需要将鉴别者对生成的图像的决定与一系列的决定进行比较。
以下代码行完成了所有这些工作:
# This method returns a helper function to compute cross entropy loss
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)
【计算机】优化程序
我们还为生成器和鉴别器网络分别设置了两个优化器。我们可以使用来自tf.keras.optimizers模块的 Adam 对象。下面几行设置了优化器:
generator_optimizer=tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer=tf.keras.optimizers.Adam(1e-4)
设置检查点
由于网络的复杂性,训练 GAN 网络比训练其它网络需要更长的时间。我们必须运行至少 50 – 60 个时期的训练,以生成有意义的图像。因此,设置检查点对于以后使用我们的模型非常有用。
通过使用操作系统库,我们设置了一个保存所有培训步骤的路径,代码如下:
import os
checkpoint_dir = './training_checkpoints'
checkpoint_prefix=os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(
generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator)
训练 GAN 模型
让我们用下面几行创建一些变量:
EPOCHS = 60
# We will reuse this seed overtime (so it's easier)
# to visualize progress in the animated GIF)
noise_dim = 100
num_examples_to_generate = 16
seed = tf.random.normal([num_examples_to_generate, noise_dim])
我们的种子是我们用来生成图像的噪音。下面的代码生成一个形状为(16,100)的正态分布随机数组。
训练步骤
这是我们模型中最不寻常的部分:我们正在设置一个定制的训练步骤。在通过注释tf.function模块定义自定义train_step()函数后,我们的模型将基于我们定义的自定义train_step()函数进行训练。
以下带有过多注释的代码是针对培训步骤的。请仔细阅读评论。
# tf.function annotation causes the function
# to be "compiled" as part of the training
@tf.function
def train_step(images):
# 1 - Create a random noise to feed it into the model
# for the image generation
noise = tf.random.normal([BATCH_SIZE, noise_dim])
# 2 - Generate images and calculate loss values
# GradientTape method records operations for automatic differentiation.
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
# 3 - Calculate gradients using loss values and model variables
# "gradient" method computes the gradient using
# operations recorded in context of this tape (gen_tape and disc_tape).
# It accepts a target (e.g., gen_loss) variable and
# a source variable (e.g.,generator.trainable_variables)
# target --> a list or nested structure of Tensors or Variables to be differentiated.
# source --> a list or nested structure of Tensors or Variables.
# target will be differentiated against elements in sources.
# "gradient" method returns a list or nested structure of Tensors
# (or IndexedSlices, or None), one for each element in sources.
# Returned structure is the same as the structure of sources.
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient( disc_loss, discriminator.trainable_variables)
# 4 - Process Gradients and Run the Optimizer
# "apply_gradients" method processes aggregated gradients.
# ex: optimizer.apply_gradients(zip(grads, vars))
"""
Example use of apply_gradients:
grads = tape.gradient(loss, vars)
grads = tf.distribute.get_replica_context().all_reduce('sum', grads)
# Processing aggregated gradients.
optimizer.apply_gradients(zip(grads, vars), experimental_aggregate_gradients=False)
"""
generator_optimizer.apply_gradients(zip( gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip( gradients_of_discriminator, discriminator.trainable_variables))
现在我们已经用tf.function注释定义了我们的定制训练步骤,我们可以为训练循环定义我们的训练函数了。
训练循环
我们为训练循环定义了一个名为train的函数。我们不仅运行 for 循环在 MNIST 上迭代我们的自定义训练步骤,还使用单个函数执行以下操作:
-
在训练期间
-
开始记录每个时期开始时花费的时间
-
制作 GIF 图像并显示它们
-
每隔 5 个时期将模型保存为一个检查点
-
打印出完整的纪元时间
-
-
训练完成后,最终生成最终图像
以下带有详细注释的行完成所有这些任务:
import time
from IPython import display # A command shell for interactive computing in Python.
def train(dataset, epochs):
# A. For each epoch, do the following:
for epoch in range(epochs):
start = time.time()
# 1 - For each batch of the epoch,
for image_batch in dataset:
# 1.a - run the custom "train_step" function
# we just declared above
train_step(image_batch)
# 2 - Produce images for the GIF as we go
display.clear_output(wait=True)
generate_and_save_images(generator,
epoch + 1,
seed)
# 3 - Save the model every 5 epochs as
# a checkpoint, which we will use later
if (epoch + 1) % 5 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
# 4 - Print out the completed epoch no. and the time spent
print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))
# B. Generate a final image after the training is completed
display.clear_output(wait=True)
generate_and_save_images(generator,
epochs,
seed)
图像生成功能
在训练函数中,有一个我们还没有定义的自定义图像生成函数。我们的图像生成功能执行以下任务:
-
使用模型生成图像。
-
使用 Matplotlib 在 4 x 4 网格布局中显示生成的图像。
-
最后保存最后的数字。
以下部门负责这些任务:
def generate_and_save_images(model, epoch, test_input):
# Notice `training` is set to False.
# This is so all layers run in inference mode (batchnorm).
# 1 - Generate images
predictions = model(test_input, training=False)
# 2 - Plot the generated images
fig = plt.figure(figsize=(4,4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i+1)
plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap="gray")
plt.axis('off')
# 3 - Save the generated images
plt.savefig('image_at_epoch_{:04d}.png'.format( epoch))
plt.show()
既然我们已经定义了自定义图像生成函数,我们可以在下一部分中安全地调用 train 函数。
开始训练
开始训练循环非常容易。下面的单行代码将从 train 函数开始训练,该函数循环遍历train_step()函数并使用generate_and_save_images()函数生成图像。在此过程中,我们还会收到统计数据和信息,以及在 4 x 4 网格布局上生成的图像。
train(train_dataset, EPOCHS)
Output:
图 12-5
在 4 x 4 网格布局中 60 个时期后生成的图像
如图 12-5 所示,经过 60 个历元,生成的图像非常接近正确的手写数字。我唯一看不到的数字是数字二(2),这可能只是一个巧合。
既然我们已经训练了我们的模型并保存了我们的检查点,我们可以用下面的代码行恢复训练好的模型:
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
在训练过程中动画生成的数字
在训练过程中,我们的generate_and_save_images()函数成功保存了每个时期生成的 4 x 4 图像网格布局。让我们通过一个简单的练习来看看我们的模型的生成能力是如何随着时间的推移而发展的。
为了能够打开图像,我们可以使用 PIL (Python Image Library),它支持许多不同的图像格式,包括 PNG。我们可以定义一个自定义函数,用下面几行打开图像:
# PIL is a library which may open different image file formats
import PIL
# Display a single image using the epoch number
def display_image(epoch_no):
return PIL.Image.open( 'image_at_epoch_{:04d}.png'.format( epoch_no ))
现在用下面一行测试这个函数,它将显示我们的模型生成的最新 PNG 文件:
display_image(EPOCHS)
输出 is shown in Figure 12-6 :
图 12-6
GAN 模型生成的最新 PNG 文件的显示。注意,它们与图 12-5 所示的样品相同。因为我们从上一个检查点恢复了模型
使用display_images()功能,我们可以显示任何我们想要的图像。除此之外,生成一个动画 GIF 图像来显示我们的模型是如何随着时间的推移而演变的,这不是很酷吗?我们可以使用 glob 和 imageio 库来实现这一点,它们将所有的 PNG 文件堆积起来,创建一个动画 GIF 文件。以下代码行执行此任务:
import glob # The glob module is used for Unix style pathname pattern expansion.
import imageio # The library that provides an easy interface to read and write a wide range of image data
anim_file = 'dcgan.gif'
with imageio.get_writer(anim_file, mode="I") as writer:
filenames = glob.glob('image*.png')
filenames = sorted(filenames)
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
点击 Google Colab 笔记本左侧的文件图标,查看所有文件,包括“ dcgan.gif ”。您可以简单地下载它来查看我们的模型在每个时期生成的图像的动画版本。为了能够在您的 Google Colab 笔记本中查看 GIF 图像,您可以使用以下代码行:
display.Image(open('dcgan.gif','rb').read())
图 12-7 显示了我们创建的 GIF 图像中的几帧:
图 12-7
从不同时代产生的数字例子。了解 GAN 模型如何随着时间的推移学习生成数字
结论
在这一章中,我们讨论了最后一个神经网络架构,生成对抗网络,它主要用于艺术、制造、研究和游戏等领域的生成任务。我们还进行了一个案例研究,其中我们训练了一个能够生成手写数字的 GAN 模型。
Footnotes 1深度卷积生成对抗网络,TensorFlow,可用在 www.tensorflow.org/tutorials/generative/dcgan