TensorFlow 2.0 计算机视觉秘籍(二)
原文:
annas-archive.org/md5/cf3ce16c27a13f4ce55f8e29a1bf85e1译者:飞龙
第三章:第三章:利用预训练网络的迁移学习威力
尽管深度神经网络为计算机视觉带来了不可否认的强大力量,但它们在调整、训练和提高性能方面非常复杂。这种难度来自三个主要来源:
-
深度神经网络通常在数据充足时才会发挥作用,但往往并非如此。此外,数据既昂贵又有时难以扩展。
-
深度神经网络包含许多需要调节的参数,这些参数会影响模型的整体表现。
-
深度学习在时间、硬件和精力方面是非常资源密集型的。
别灰心!通过迁移学习,我们可以通过利用在庞大数据集(如 ImageNet)上预训练的经典架构中丰富的知识,节省大量时间和精力。而最棒的部分是?除了它是如此强大且有用的工具,迁移学习还很容易应用。在本章中,我们将学习如何做到这一点。
在本章中,我们将覆盖以下食谱:
-
使用预训练网络实现特征提取器
-
在提取的特征上训练一个简单的分类器
-
检查提取器和分类器的效果
-
使用增量学习训练分类器
-
使用 Keras API 微调网络
-
使用 TFHub 微调网络
我们开始吧!
技术要求
强烈建议你拥有 GPU 访问权限,因为迁移学习通常计算密集型。在每个食谱的准备工作部分,你将收到有关如何安装该食谱所需依赖项的具体说明。如果需要,你可以在这里找到本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3。
查看以下链接,观看 Code in Action 视频:
使用预训练网络实现特征提取器
利用迁移学习的最简单方法之一是将预训练模型用作特征提取器。这样,我们可以将深度学习和机器学习相结合,而这通常是我们做不到的,因为传统的机器学习算法无法处理原始图像。在这个示例中,我们将实现一个可重用的FeatureExtractor类,从一组输入图像中生成特征向量数据集,并将其保存在极速的 HDF5 格式中。
你准备好了吗?我们开始吧!
准备工作
你需要安装Pillow和tqdm(我们将用它来显示一个漂亮的进度条)。幸运的是,使用pip安装非常容易:
$> pip install Pillow tqdm
我们将使用Stanford Cars数据集,你可以在这里下载:imagenet.stanford.edu/internal/car196/car_ims.tgz。将数据解压到你选择的位置。在本配方中,我们假设数据位于~/.keras/datasets目录下,名为car_ims。
以下是数据集中的一些示例图像:
图 3.1 – 示例图像
我们将把提取的特征以 HDF5 格式存储,HDF5 是一种用于在磁盘上存储非常大的数值数据集的二进制分层协议,同时保持易于访问和按行级别计算。你可以在这里了解更多关于 HDF5 的内容:portal.hdfgroup.org/display/HDF5/HDF5。
如何做到这一点…
按照以下步骤完成此配方:
-
导入所有必要的包:
import glob import os import pathlib import h5py import numpy as np import sklearn.utils as skutils from sklearn.preprocessing import LabelEncoder from tensorflow.keras.applications import imagenet_utils from tensorflow.keras.applications.vgg16 import VGG16 from tensorflow.keras.preprocessing.image import * from tqdm import tqdm -
定义
FeatureExtractor类及其构造函数:class FeatureExtractor(object): def __init__(self, model, input_size, label_encoder, num_instances, feature_size, output_path, features_key='features', buffer_size=1000): -
我们需要确保输出路径是可写的:
if os.path.exists(output_path): error_msg = (f'{output_path} already exists. ' f'Please delete it and try again.') raise FileExistsError(error_msg) -
现在,让我们将输入参数存储为对象成员:
self.model = model self.input_size = input_size self.le = label_encoder self.feature_size = feature_size self.buffer_size = buffer_size self.buffer = {'features': [], 'labels': []} self.current_index = 0 -
self.buffer将包含实例和标签的缓冲区,而self.current_index将指向 HDF5 数据库内数据集中的下一个空闲位置。我们现在将创建它:self.db = h5py.File(output_path, 'w') self.features = self.db.create_dataset(features_ key, (num_instances, feature_size), dtype='float') self.labels = self.db.create_dataset('labels', (num_instances,), dtype='int') -
定义一个方法,从图像路径列表中提取特征和标签,并将它们存储到
HDF5数据库中:def extract_features(self, image_paths, labels, batch_size=64, shuffle=True): if shuffle: image_paths, labels = skutils.shuffle(image_paths, labels) encoded_labels = self.le.fit_transform(labels) self._store_class_labels(self.le.classes_) -
在对图像路径及其标签进行洗牌,并对标签进行编码和存储后,我们将遍历图像的批次,将它们传递通过预训练的网络。一旦完成,我们将把结果特征保存到 HDF5 数据库中(我们在这里使用的辅助方法稍后会定义):
for i in tqdm(range(0, len(image_paths), batch_size)): batch_paths = image_paths[i: i + batch_size] batch_labels = encoded_labels[i:i + batch_size] batch_images = [] for image_path in batch_paths: image = load_img(image_path, target_size=self.input_size) image = img_to_array(image) image = np.expand_dims(image, axis=0) image = imagenet_utils.preprocess_input(image) batch_images.append(image) batch_images = np.vstack(batch_images) feats = self.model.predict(batch_images, batch_size=batch_size) new_shape = (feats.shape[0], self.feature_size) feats = feats.reshape(new_shape) self._add(feats, batch_labels) self._close() -
定义一个私有方法,将特征和标签添加到相应的数据集:
def _add(self, rows, labels): self.buffer['features'].extend(rows) self.buffer['labels'].extend(labels) if len(self.buffer['features']) >= self.buffer_size: self._flush() -
定义一个私有方法,将缓冲区刷新到磁盘:
def _flush(self): next_index = (self.current_index + len(self.buffer['features'])) buffer_slice = slice(self.current_index, next_index) self.features[buffer_slice] = self.buffer['features'] self.labels[buffer_slice] = self.buffer['labels'] self.current_index = next_index self.buffer = {'features': [], 'labels': []} -
定义一个私有方法,将类别标签存储到 HDF5 数据库中:
def _store_class_labels(self, class_labels): data_type = h5py.special_dtype(vlen=str) shape = (len(class_labels),) label_ds = self.db.create_dataset('label_names', shape, dtype=data_type) label_ds[:] = class_labels -
定义一个私有方法,将关闭 HDF5 数据集:
def _close(self): if len(self.buffer['features']) > 0: self._flush() self.db.close() -
加载数据集中图像的路径:
files_pattern = (pathlib.Path.home() / '.keras' / 'datasets' /'car_ims' / '*.jpg') files_pattern = str(files_pattern) input_paths = [*glob.glob(files_pattern)] -
创建输出目录。我们将创建一个旋转车图像的数据集,以便潜在的分类器可以学习如何正确地将照片恢复到原始方向,通过正确预测旋转角度:
output_path = (pathlib.Path.home() / '.keras' / 'datasets' / 'car_ims_rotated') if not os.path.exists(str(output_path)): os.mkdir(str(output_path)) -
创建数据集的副本,对图像进行随机旋转:
labels = [] output_paths = [] for index in tqdm(range(len(input_paths))): image_path = input_paths[index] image = load_img(image_path) rotation_angle = np.random.choice([0, 90, 180, 270]) rotated_image = image.rotate(rotation_angle) rotated_image_path = str(output_path / f'{index}.jpg') rotated_image.save(rotated_image_path, 'JPEG') output_paths.append(rotated_image_path) labels.append(rotation_angle) image.close() rotated_image.close() -
实例化
FeatureExtractor,并使用预训练的VGG16网络从数据集中的图像提取特征:features_path = str(output_path / 'features.hdf5') model = VGG16(weights='imagenet', include_top=False) fe = FeatureExtractor(model=model, input_size=(224, 224, 3), label_encoder=LabelEncoder(), num_instances=len(input_paths), feature_size=512 * 7 * 7, output_path=features_path) -
提取特征和标签:
fe.extract_features(image_paths=output_paths, labels=labels)
几分钟后,应该会在~/.keras/datasets/car_ims_rotated中生成一个名为features.hdf5的文件。
它是如何工作的…
在本配方中,我们实现了一个可重用的组件,以便在 ImageNet 上使用预训练网络,如逻辑回归和支持向量机。
由于图像数据集通常过大,无法全部载入内存,我们选择了高性能且用户友好的 HDF5 格式,这种格式非常适合将大规模数值数据存储在磁盘上,同时保留了NumPy典型的易访问性。这意味着我们可以像操作常规NumPy数组一样与 HDF5 数据集进行交互,使其与整个SciPy生态系统兼容。
FeatureExtractor的结果是一个分层的 HDF5 文件(可以将其视为文件系统中的一个文件夹),包含三个数据集:features,包含特征向量;labels,存储编码后的标签;以及label_names,保存编码前的人类可读标签。
最后,我们使用FeatureExtractor创建了一个二进制表示的数据集,数据集包含了旋转了 0º、90º、180º或 270º的汽车图像。
提示
我们将在本章的后续教程中继续使用刚刚处理过的修改版Stanford Cars数据集。
另见
关于Stanford Cars数据集的更多信息,您可以访问官方页面:ai.stanford.edu/~jkrause/cars/car_dataset.html。要了解更多关于 HDF5 的信息,请访问 HDF Group 的官方网站:www.hdfgroup.org/。
在提取特征后训练一个简单的分类器
机器学习算法并不适合直接处理张量,因此它们无法直接从图像中学习。然而,通过使用预训练的网络作为特征提取器,我们弥补了这一差距,使我们能够利用广泛流行且经过实战验证的算法,如逻辑回归、决策树和支持向量机。
在本教程中,我们将使用在前一个教程中生成的特征(以 HDF5 格式)来训练一个图像方向检测器,以修正图像的旋转角度,将其恢复到原始状态。
准备工作
正如我们在本教程的介绍中提到的,我们将使用在前一个教程中生成的features.hdf5数据集,该数据集包含了来自Stanford Cars数据集的旋转图像的编码信息。我们假设该数据集位于以下位置:~/.keras/datasets/car_ims_rotated/features.hdf5。
以下是一些旋转的样本:
图 3.2 – 一辆旋转了 180º的汽车(左),以及另一辆旋转了 90º的汽车(右)
让我们开始吧!
如何操作…
按照以下步骤完成本教程:
-
导入所需的包:
import pathlib import h5py from sklearn.linear_model import LogisticRegressionCV from sklearn.metrics import classification_report -
加载 HDF5 格式的数据集:
dataset_path = str(pathlib.Path.home()/'.keras'/'datasets'/'car_ims_rotated'/'features.hdf5') db = h5py.File(dataset_path, 'r') -
由于数据集太大,我们只处理 50%的数据。以下代码将特征和标签分成两半:
SUBSET_INDEX = int(db['labels'].shape[0] * 0.5) features = db['features'][:SUBSET_INDEX] labels = db['labels'][:SUBSET_INDEX] -
取数据的前 80%来训练模型,其余 20%用于之后的评估:
TRAIN_PROPORTION = 0.8 SPLIT_INDEX = int(len(labels) * TRAIN_PROPORTION) X_train, y_train = (features[:SPLIT_INDEX], labels[:SPLIT_INDEX]) X_test, y_test = (features[SPLIT_INDEX:], labels[SPLIT_INDEX:]) -
训练一个交叉验证的
LogisticRegressionCV,通过交叉验证找到最佳的C参数:model = LogisticRegressionCV(n_jobs=-1) model.fit(X_train, y_train)请注意,
n_jobs=-1意味着我们将使用所有可用的核心来并行寻找最佳模型。您可以根据硬件的性能调整此值。 -
在测试集上评估模型。我们将计算分类报告,以获得模型性能的细节:
predictions = model.predict(X_test) report = classification_report(y_test, predictions, target_names=db['label_names']) print(report)这将打印以下报告:
precision recall f1-score support 0 1.00 1.00 1.00 404 90 0.98 0.99 0.99 373 180 0.99 1.00 1.00 409 270 1.00 0.98 0.99 433 accuracy 0.99 1619 macro avg 0.99 0.99 0.99 1619 weighted avg 0.99 0.99 0.99 1619该模型在区分四个类别方面表现良好,在测试集上达到了 99%的整体准确率!
-
最后,关闭 HDF5 文件以释放任何资源:
db.close()
我们将在下一节中了解这一切如何工作。
它是如何工作的…
我们刚刚训练了一个非常简单的逻辑回归模型,用于检测图像的旋转角度。为了实现这一点,我们利用了使用预训练VGG16网络在 ImageNet 上提取的丰富且富有表现力的特征(若需要更详细的解释,请参考本章的第一个食谱)。
由于数据量过大,而scikit-learn的机器学习算法一次性处理所有数据(更具体来说,大多数算法无法批处理数据),我们只使用了 50%的特征和标签,因内存限制。
几分钟后,我们在测试集上获得了惊人的 99%的表现。此外,通过分析分类报告,我们可以看到模型对其预测非常有信心,在所有四个类别中 F1 分数至少达到了 0.99。
另见
有关如何从预训练网络中提取特征的更多信息,请参阅本章的使用预训练网络实现特征提取器一节。
快速检查提取器和分类器
在处理一个新项目时,我们常常成为选择悖论的受害者:由于有太多选择,我们不知道从哪里或如何开始。哪个特征提取器最好?我们能训练出最具性能的模型吗?我们应该如何预处理数据?
在本食谱中,我们将实现一个框架,自动快速检查特征提取器和分类器。目标不是立即获得最好的模型,而是缩小选择范围,以便在后期专注于最有前景的选项。
准备工作
首先,我们需要安装Pillow和tqdm:
$> pip install Pillow tqdm
我们将使用一个名为17 Category Flower Dataset的数据集,下载地址:www.robots.ox.ac.uk/~vgg/data/flowers/17。不过,也可以下载一个整理好的版本,该版本按照类别组织成子文件夹,下载地址:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。请将其解压到您喜欢的位置。在本食谱中,我们假设数据位于~/.keras/datasets目录下,名称为flowers17。
最后,我们将重用在本章开头的使用预训练网络实现特征提取器食谱中定义的FeatureExtractor()类。如果你想了解更多,可以参考它。
以下是来自本食谱数据集17 类别花卉数据集的一些示例图像:
图 3.3 – 示例图像
准备工作完成后,让我们开始吧!
它是如何实现的……
以下步骤将帮助我们对几种特征提取器和机器学习算法的组合进行抽查。按照以下步骤完成本食谱:
-
导入必要的包:
import json import os import pathlib from glob import glob import h5py from sklearn.ensemble import * from sklearn.linear_model import * from sklearn.metrics import accuracy_score from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import LabelEncoder from sklearn.svm import LinearSVC from sklearn.tree import * from tensorflow.keras.applications import * from tqdm import tqdm from ch3.recipe1.feature_extractor import FeatureExtractor -
定义所有特征提取器的输入大小:
INPUT_SIZE = (224, 224, 3) -
定义一个函数,用于获取预训练网络的元组列表,以及它们输出的向量的维度:
def get_pretrained_networks(): return [ (VGG16(input_shape=INPUT_SIZE, weights='imagenet', include_top=False), 7 * 7 * 512), (VGG19(input_shape=INPUT_SIZE, weights='imagenet', include_top=False), 7 * 7 * 512), (Xception(input_shape=INPUT_SIZE, weights='imagenet', include_top=False), 7 * 7 * 2048), (ResNet152V2(input_shape=INPUT_SIZE, weights='imagenet', include_top=False), 7 * 7 * 2048), (InceptionResNetV2(input_shape=INPUT_SIZE, weights='imagenet', include_top=False), 5 * 5 * 1536) ] -
定义一个返回机器学习模型
dict以进行抽查的函数:def get_classifiers(): models = {} models['LogisticRegression'] = LogisticRegression() models['SGDClf'] = SGDClassifier() models['PAClf'] = PassiveAggressiveClassifier() models['DecisionTreeClf'] = DecisionTreeClassifier() models['ExtraTreeClf'] = ExtraTreeClassifier() n_trees = 100 models[f'AdaBoostClf-{n_trees}'] = \ AdaBoostClassifier(n_estimators=n_trees) models[f'BaggingClf-{n_trees}'] = \ BaggingClassifier(n_estimators=n_trees) models[f'RandomForestClf-{n_trees}'] = \ RandomForestClassifier(n_estimators=n_trees) models[f'ExtraTreesClf-{n_trees}'] = \ ExtraTreesClassifier(n_estimators=n_trees) models[f'GradientBoostingClf-{n_trees}'] = \ GradientBoostingClassifier(n_estimators=n_trees) number_of_neighbors = range(3, 25) for n in number_of_neighbors: models[f'KNeighborsClf-{n}'] = \ KNeighborsClassifier(n_neighbors=n) reg = [1e-3, 1e-2, 1, 10] for r in reg: models[f'LinearSVC-{r}'] = LinearSVC(C=r) models[f'RidgeClf-{r}'] = RidgeClassifier(alpha=r) print(f'Defined {len(models)} models.') return models -
定义数据集的路径,以及所有图像路径的列表:
dataset_path = (pathlib.Path.home() / '.keras' / 'datasets' 'flowers17') files_pattern = (dataset_path / 'images' / '*' / '*.jpg') images_path = [*glob(str(files_pattern))] -
将标签加载到内存中:
labels = [] for index in tqdm(range(len(images_path))): image_path = images_path[index] label = image_path.split(os.path.sep)[-2] labels.append(label) -
定义一些变量以便跟踪抽查过程。
final_report将包含每个分类器的准确率,分类器是在不同预训练网络提取的特征上训练的。best_model、best_accuracy和best_features将分别包含最佳模型的名称、准确率和生成特征的预训练网络的名称:final_report = {} best_model = None best_accuracy = -1 best_features = None -
遍历每个预训练网络,使用它从数据集中的图像提取特征:
for model, feature_size in get_pretrained_networks(): output_path = dataset_path / f'{model.name}_features.hdf5' output_path = str(output_path) fe = FeatureExtractor(model=model, input_size=INPUT_SIZE, label_encoder=LabelEncoder(), num_instances=len(images_path), feature_size=feature_size, output_path=output_path) fe.extract_features(image_paths=images_path, labels=labels) -
使用 80%的数据进行训练,20%的数据进行测试:
db = h5py.File(output_path, 'r') TRAIN_PROPORTION = 0.8 SPLIT_INDEX = int(len(labels) * TRAIN_PROPORTION) X_train, y_train = (db['features'][:SPLIT_INDEX], db['labels'][:SPLIT_INDEX]) X_test, y_test = (db['features'][SPLIT_INDEX:], db['labels'][SPLIT_INDEX:]) classifiers_report = { 'extractor': model.name } print(f'Spot-checking with features from {model.name}') -
使用当前迭代中提取的特征,遍历所有机器学习模型,使用训练集进行训练,并在测试集上进行评估:
for clf_name, clf in get_classifiers().items(): try: clf.fit(X_train, y_train) except Exception as e: print(f'\t{clf_name}: {e}') continue predictions = clf.predict(X_test) accuracy = accuracy_score(y_test, predictions) print(f'\t{clf_name}: {accuracy}') classifiers_report[clf_name] = accuracy -
检查是否有新的最佳模型。如果是,请更新相应的变量:
if accuracy > best_accuracy: best_accuracy = accuracy best_model = clf_name best_features = model.name -
将本次迭代的结果存储在
final_report中,并释放 HDF5 文件的资源:final_report[output_path] = classifiers_report db.close() -
更新
final_report,并写入最佳模型的信息。最后,将其写入磁盘:final_report['best_model'] = best_model final_report['best_accuracy'] = best_accuracy final_report['best_features'] = best_features with open('final_report.json', 'w') as f: json.dump(final_report, f)
检查final_report.json文件,我们可以看到最好的模型是PAClf(PassiveAggressiveClassifier),它在测试集上的准确率为 0.934(93.4%),并且是在我们从VGG19网络提取的特征上训练的。你可以在这里查看完整的输出:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/final_report.json。让我们进入下一部分,详细研究一下我们在本食谱中完成的项目。
它是如何工作的……
在本配方中,我们开发了一个框架,使我们能够自动抽查 40 种不同的机器学习算法,使用由五种不同的预训练网络生成的特征,最终进行了 200 次实验。通过这种方法的结果,我们发现,对于这个特定问题,最佳的模型组合是使用VGG19网络生成的向量训练PassiveAggressiveClassifier。
请注意,我们并没有专注于实现最大性能,而是基于充分的证据做出明智的决策,决定在优化此数据集的分类器时如何合理地分配时间和资源。现在,我们知道微调被动攻击性分类器最有可能带来回报。那么,我们多久才能得出这个结论呢?几个小时,甚至几天。
让计算机完成繁重工作的好处是,我们不必猜测,同时可以将时间自由地用于其他任务。这是不是很棒?
使用增量学习训练分类器
传统机器学习库的一个问题,如scikit-learn,是它们很少提供在大规模数据上训练模型的可能性,而这恰好是深度神经网络最适合处理的数据。拥有大量数据又能有什么用,如果我们不能使用它呢?
幸运的是,有一种方法可以绕过这个限制,叫做creme,它可以在数据集过大无法加载到内存时训练分类器。
准备工作
在本配方中,我们将利用creme,这是一个专门设计用于在无法加载到内存的大数据集上训练机器学习模型的实验性库。要安装creme,请执行以下命令:
$> pip install creme==0.5.1
我们将在本章的使用预训练网络实现特征提取器配方中使用我们生成的features.hdf5数据集,该数据集包含来自Stanford Cars数据集中旋转图像的编码信息。我们假设数据集位于以下位置:~/.keras/datasets/car_ims_rotated/features.hdf5。
以下是该数据集中的一些示例图像:
图 3.4 – 旋转 90º的汽车示例(左),和旋转 0º的另一辆汽车(右)
让我们开始吧!
如何做……
以下步骤将指导我们如何在大数据上逐步训练分类器:
-
导入所有必要的软件包:
import pathlib import h5py from creme import stream from creme.linear_model import LogisticRegression from creme.metrics import Accuracy from creme.multiclass import OneVsRestClassifier from creme.preprocessing import StandardScaler -
定义一个函数,将数据集保存为 CSV 文件:
def write_dataset(output_path, feats, labels, batch_size): feature_size = feats.shape[1] csv_columns = ['class'] + [f'feature_{i}' for i in range(feature_ size)] -
我们将为每个特征的类别设置一列,每个特征向量中的元素将设置多列。接下来,我们将批量写入 CSV 文件的内容,从头部开始:
dataset_size = labels.shape[0] with open(output_path, 'w') as f: f.write(f'{“,”.join(csv_columns)}\n') -
提取本次迭代中的批次:
for batch_number, index in \ enumerate(range(0, dataset_size, batch_size)): print(f'Processing batch {batch_number + 1} of ' f'{int(dataset_size / float(batch_size))}') batch_feats = feats[index: index + batch_size] batch_labels = labels[index: index + batch_size] -
现在,写入批次中的所有行:
for label, vector in \ zip(batch_labels, batch_feats): vector = ','.join([str(v) for v in vector]) f.write(f'{label},{vector}\n') -
加载 HDF5 格式的数据集:
dataset_path = str(pathlib.Path.home()/'.keras'/'datasets'/'car_ims_rotated'/'features.hdf5') db = h5py.File(dataset_path, 'r') -
定义分割索引,将数据分为训练集(80%)和测试集(20%):
TRAIN_PROPORTION = 0.8 SPLIT_INDEX = int(db['labels'].shape[0] * TRAIN_PROPORTION) -
将训练集和测试集子集写入磁盘,保存为 CSV 文件:
BATCH_SIZE = 256 write_dataset('train.csv', db['features'][:SPLIT_INDEX], db['labels'][:SPLIT_INDEX], BATCH_SIZE) write_dataset('test.csv', db['features'][SPLIT_INDEX:], db['labels'][SPLIT_INDEX:], BATCH_SIZE) -
creme要求我们将 CSV 文件中每一列的类型指定为dict实例。以下代码块指定了class应该编码为int类型,而其余列(对应特征)应该为float类型:FEATURE_SIZE = db['features'].shape[1] types = {f'feature_{i}': float for i in range(FEATURE_SIZE)} types['class'] = int -
在以下代码中,我们定义了一个
creme管道,每个输入在传递给分类器之前都会进行标准化。由于这是一个多类别问题,我们需要将LogisticRegression与OneVsRestClassifier包装在一起:model = StandardScaler() model |= OneVsRestClassifier(LogisticRegression()) -
将
Accuracy定义为目标指标,并创建一个针对train.csv数据集的迭代器:metric = Accuracy() dataset = stream.iter_csv('train.csv', target_name='class', converters=types) -
一次训练一个样本的分类器。每训练 100 个样本时,打印当前准确率:
print('Training started...') for i, (X, y) in enumerate(dataset): predictions = model.predict_one(X) model = model.fit_one(X, y) metric = metric.update(y, predictions) if i % 100 == 0: print(f'Update {i} - {metric}') print(f'Final - {metric}') -
创建一个针对
test.csv文件的迭代器:metric = Accuracy() test_dataset = stream.iter_csv('test.csv', target_name='class', converters=types) -
再次在测试集上评估模型,一次处理一个样本:
print('Testing model...') for i, (X, y) in enumerate(test_dataset): predictions = model.predict_one(X) metric = metric.update(y, predictions) if i % 1000 == 0: print(f'(TEST) Update {i} - {metric}') print(f'(TEST) Final - {metric}')
几分钟后,我们应该会得到一个在测试集上准确率约为 99% 的模型。我们将在下一部分详细查看这个过程。
它是如何工作的…
通常情况下,尽管我们有大量的数据可用,但由于硬件或软件限制,我们无法使用所有数据(在在提取特征上训练简单分类器这一食谱中,我们只使用了 50% 的数据,因为无法将其全部保存在内存中)。然而,通过增量学习(也称为在线学习),我们可以像训练神经网络一样,以批处理的方式训练传统的机器学习模型。
在这个食谱中,为了捕获我们Stanford Cars数据集的特征向量的全部信息,我们不得不将训练集和测试集写入 CSV 文件。接下来,我们训练了LogisticRegression并将其包装在OneVsRestClassifier中,后者学习了如何检测图像特征向量中的旋转角度。最后,我们在测试集上达到了非常满意的 99% 准确率。
使用 Keras API 微调网络
或许迁移学习的最大优势之一就是能够利用预训练网络中所编码的知识带来的顺风。在这些网络中,只需交换较浅的层,我们就能在新的、无关的数据集上获得出色的表现,即使我们的数据量很小。为什么?因为底层的信息几乎是普遍适用的:它编码了适用于几乎所有计算机视觉问题的基本形式和形状。
在这个食谱中,我们将对一个小型数据集微调预训练的VGG16网络,从而实现一个原本不太可能得到的高准确率。
准备工作
我们需要Pillow来实现此食谱。可以按如下方式安装:
$> pip install Pillow
我们将使用一个名为17 Category Flower Dataset的数据集,可以通过以下链接访问:www.robots.ox.ac.uk/~vgg/data/flowers/17。该数据集的一个版本已经按照每个类的子文件夹进行组织,可以在此链接找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。下载并解压到您选择的位置。从现在起,我们假设数据存储在~/.keras/datasets/flowers17目录中。
以下是来自该数据集的一些示例图像:
图 3.5 – 示例图像
让我们开始吧!
如何实现…
微调很简单!按照以下步骤完成这个食谱:
-
导入必要的依赖项:
import os import pathlib from glob import glob import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelBinarizer from tensorflow.keras import Model from tensorflow.keras.applications import VGG16 from tensorflow.keras.layers import * from tensorflow.keras.optimizers import * from tensorflow.keras.preprocessing.image import * -
设置随机种子:
SEED = 999 -
定义一个函数,从预训练模型构建一个新的网络,其中顶部的全连接层将是全新的,并且针对当前问题进行了调整:
def build_network(base_model, classes): x = Flatten()(base_model.output) x = Dense(units=256)(x) x = ReLU()(x) x = BatchNormalization(axis=-1)(x) x = Dropout(rate=0.5)(x) x = Dense(units=classes)(x) output = Softmax()(x) return output -
定义一个函数,将数据集中的图像和标签加载为
NumPy数组:def load_images_and_labels(image_paths, target_size=(256, 256)): images = [] labels = [] for image_path in image_paths: image = load_img(image_path, target_size=target_size) image = img_to_array(image) label = image_path.split(os.path.sep)[-2] images.append(image) labels.append(label) return np.array(images), np.array(labels) -
加载图像路径并从中提取类集合:
dataset_path = (pathlib.Path.home() / '.keras' / 'datasets' /'flowers17') files_pattern = (dataset_path / 'images' / '*' / '*.jpg') image_paths = [*glob(str(files_pattern))] CLASSES = {p.split(os.path.sep)[-2] for p in image_paths} -
加载图像并对其进行归一化,使用
LabelBinarizer()进行一热编码,并将数据拆分为训练集(80%)和测试集(20%):X, y = load_images_and_labels(image_paths) X = X.astype('float') / 255.0 y = LabelBinarizer().fit_transform(y) (X_train, X_test, y_train, y_test) = train_test_split(X, y, test_size=0.2, random_state=SEED) -
实例化一个预训练的
VGG16模型,去除顶部的全连接层。指定输入形状为 256x256x3:base_model = VGG16(weights='imagenet', include_top=False, input_tensor=Input(shape=(256, 256, 3)))冻结基础模型中的所有层。我们这样做是因为我们不希望重新训练它们,而是使用它们已有的知识:
for layer in base_model.layers: layer.trainable = False -
使用
build_network()(在步骤 3中定义)构建一个完整的网络,并在其上添加一组新层:model = build_network(base_model, len(CLASSES)) model = Model(base_model.input, model) -
定义批处理大小和一组要通过
ImageDataGenerator()应用的增强方法:BATCH_SIZE = 64 augmenter = ImageDataGenerator(rotation_range=30, horizontal_flip=True, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, fill_mode='nearest') train_generator = augmenter.flow(X_train, y_train, BATCH_SIZE) -
预热网络。这意味着我们将只训练新添加的层(其余部分被冻结),训练 20 个周期,使用RMSProp优化器,学习率为 0.001。最后,我们将在测试集上评估网络:
WARMING_EPOCHS = 20 model.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=1e-3), metrics=['accuracy']) model.fit(train_generator, steps_per_epoch=len(X_train) // BATCH_SIZE, validation_data=(X_test, y_test), epochs=WARMING_EPOCHS) result = model.evaluate(X_test, y_test) print(f'Test accuracy: {result[1]}') -
现在,网络已经预热完毕,我们将微调基础模型的最终层,特别是从第 16 层开始(记住,索引从零开始),以及全连接层,训练 50 个周期,使用SGD优化器,学习率为 0.001:
for layer in base_model.layers[15:]: layer.trainable = True EPOCHS = 50 model.compile(loss='categorical_crossentropy', optimizer=SGD(lr=1e-3), metrics=['accuracy']) model.fit(train_generator, steps_per_epoch=len(X_train) // BATCH_SIZE, validation_data=(X_test, y_test), epochs=EPOCHS) result = model.evaluate(X_test, y_test) print(f'Test accuracy: {result[1]}')
在预热后,网络在测试集上的准确率达到了 81.6%。然后,当我们进行了微调后,经过 50 个周期,测试集上的准确率提高到了 94.5%。我们将在下一节看到这一过程的具体细节。
它是如何工作的…
我们成功地利用了在庞大的 ImageNet 数据库上预训练的VGG16模型的知识。通过替换顶部的全连接层,这些层负责实际分类(其余部分充当特征提取器),我们使用自己的一组深度层来适应当前问题,从而在测试集上获得了超过 94.5%的不错准确率。
这个结果展示了迁移学习的强大能力,特别是考虑到数据集中每个类别只有 81 张图片(81x17=1,377 张图片),这对于从头开始训练一个表现良好的深度学习模型来说显然是不足够的。
提示
虽然并非总是必需的,但在微调网络时,最好先热身一下头部(顶部的全连接层),让它们有时间适应来自预训练网络的特征。
另见
你可以在这里阅读更多关于 Keras 预训练模型的内容:www.tensorflow.org/api_docs/py…
使用 TFHub 微调网络
微调网络的最简单方法之一是依赖于TensorFlow Hub(TFHub)中丰富的预训练模型。在这个任务中,我们将微调一个ResNetV1152特征提取器,以便从一个非常小的数据集中分类花卉。
准备就绪
我们需要tensorflow-hub和Pillow来完成这个任务。两者都可以很容易地安装,方法如下:
$> pip install tensorflow-hub Pillow
我们将使用一个名为17 类花卉数据集的数据集,您可以通过www.robots.ox.ac.uk/~vgg/data/flowers/17访问。建议你在这里获取数据的重组织版本:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch3/recipe3/flowers17.zip。下载并解压到您选择的位置。从现在开始,我们假设数据存储在~/.keras/datasets/flowers17。
以下是该数据集的一些示例图片:
图 3.6 – 示例图片
让我们开始吧!
如何做……
按照以下步骤成功完成这个任务:
-
导入所需的包:
import os import pathlib from glob import glob import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelBinarizer from tensorflow.keras import Sequential from tensorflow.keras.layers import * from tensorflow.keras.optimizers import RMSprop from tensorflow.keras.preprocessing.image import * from tensorflow_hub import KerasLayer -
设置随机种子:
SEED = 999 -
定义一个函数,从预训练模型构建一个新的网络,其中顶部的全连接层将是全新的,并且会根据我们数据中的类别数量进行调整:
def build_network(base_model, classes): return Sequential([ base_model, Dense(classes), Softmax() ]) -
定义一个函数,将数据集中的图片和标签加载为
NumPy数组:def load_images_and_labels(image_paths, target_size=(256, 256)): images = [] labels = [] for image_path in image_paths: image = load_img(image_path, target_size=target_size) image = img_to_array(image) label = image_path.split(os.path.sep)[-2] images.append(image) labels.append(label) return np.array(images), np.array(labels) -
加载图片路径并从中提取类别集合:
dataset_path = (pathlib.Path.home() / '.keras' / 'datasets' /'flowers17') files_pattern = (dataset_path / 'images' / '*' / '*.jpg') image_paths = [*glob(str(files_pattern))] CLASSES = {p.split(os.path.sep)[-2] for p in image_paths} -
加载图片并进行归一化,使用
LabelBinarizer()进行独热编码标签,然后将数据拆分为训练集(80%)和测试集(20%):X, y = load_images_and_labels(image_paths) X = X.astype('float') / 255.0 y = LabelBinarizer().fit_transform(y) (X_train, X_test, y_train, y_test) = train_test_split(X, y, test_size=0.2, random_state=SEED) -
实例化一个预训练的
KerasLayer()类,指定输入形状为 256x256x3:model_url = ('https://tfhub.dev/google/imagenet/' 'resnet_v1_152/feature_vector/4') base_model = KerasLayer(model_url, input_shape=(256, 256, 3))使基础模型不可训练:
base_model.trainable = False -
在使用基础模型作为起点的基础上,构建完整的网络:
model = build_network(base_model, len(CLASSES)) -
定义批量大小以及通过
ImageDataGenerator()应用的一组数据增强操作:BATCH_SIZE = 32 augmenter = ImageDataGenerator(rotation_range=30, horizontal_flip=True, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, fill_mode='nearest') train_generator = augmenter.flow(X_train, y_train, BATCH_SIZE) -
训练整个模型 20 个 epoch,并评估其在测试集上的性能:
EPOCHS = 20 model.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=1e-3), metrics=['accuracy']) model.fit(train_generator, steps_per_epoch=len(X_train) // BATCH_SIZE, validation_data=(X_test, y_test), epochs=EPOCHS) result = model.evaluate(X_test, y_test) print(f'Test accuracy: {result[1]}')
只需几分钟,我们就在测试集上获得了大约 95.22%的准确率。太棒了,你不觉得吗?现在,让我们深入了解一下。
它是如何工作的……
我们利用了预训练的17 类花卉数据集中编码的知识。
通过简单地更换顶层,我们在测试集上达到了令人印象深刻的 95.22%的准确率,考虑到所有的约束,这可是一个不小的成就。
与使用 Keras API 微调网络的做法不同,这次我们没有对模型的头部进行预热。再次强调,这不是硬性规定,而是我们工具箱中的另一种方法,我们应该根据具体项目尝试使用。
另见
你可以在这里阅读更多关于我们在本教程中使用的预训练模型的信息:tfhub.dev/google/imagenet/resnet_v1_152/feature_vector/4。
第四章:第四章:通过 DeepDream、神经风格迁移和图像超分辨率增强和美化图像
虽然深度神经网络在传统计算机视觉任务中表现出色,尤其是在纯粹的实际应用中,但它们也有有趣的一面!正如我们将在本章中发现的那样,我们可以借助一点聪明才智和数学的帮助,解锁深度学习的艺术面。
本章将从介绍DeepDream开始,这是一种使神经网络生成梦幻般图像的算法。接下来,我们将利用迁移学习的力量,将著名画作的风格应用到我们自己的图像上(这就是神经风格迁移)。最后,我们将结束于图像超分辨率,这是一种用于提升图像质量的深度学习方法。
本章将涵盖以下食谱:
-
实现 DeepDream
-
生成你自己的梦幻图像
-
实现神经风格迁移
-
将风格迁移应用到自定义图像
-
使用 TFHub 应用风格迁移
-
利用深度学习提高图像分辨率
让我们开始吧!
技术要求
在进行深度学习时,一般建议适用:如果可能,使用 GPU,因为它可以大大提高效率并减少计算时间。在每个食谱中,你会在准备工作部分找到具体的准备说明(如有必要)。你可以在这里找到本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4。
查看以下链接,观看代码实战视频:
实现 DeepDream
DeepDream是一个实验的产物,旨在可视化神经网络学习到的内部模式。为了实现这一目标,我们可以将一张图像传入网络,计算它关于特定层激活的梯度,然后修改图像,以增强这些激活的幅度,从而放大模式。结果?迷幻、超现实的照片!
尽管由于DeepDream的性质,本食谱稍显复杂,但我们将一步一步来,别担心。
让我们开始吧。
准备工作
本食谱无需额外安装任何内容。不过,我们不会深入讨论DeepDream的细节,但如果你对这个话题感兴趣,可以在这里阅读 Google 的原始博客文章:ai.googleblog.com/2015/06/inceptionism-going-deeper-into-neural.html。
如何操作……
按照以下步骤操作,你很快就能拥有自己的深度梦幻生成器:
-
导入所有必要的包:
import numpy as np import tensorflow as tf from tensorflow.keras import Model from tensorflow.keras.applications.inception_v3 import * -
定义
DeepDreamer类及其构造函数:class DeepDreamer(object): def __init__(self, octave_scale=1.30, octave_power_factors=None, layers=None): -
构造函数参数指定了我们将如何按比例增大图像的尺寸(
octave_scale),以及将应用于尺度的因子(octave_power_factors)。layers包含将用于生成梦境的目标层。接下来,我们将这些参数存储为对象成员:self.octave_scale = octave_scale if octave_power_factors is None: self.octave_power_factors = [*range(-2, 3)] else: self.octave_power_factors = octave_power_factors if layers is None: self.layers = ['mixed3', 'mixed5'] else: self.layers = layers -
如果某些输入是
None,我们使用默认值。如果不是,我们使用输入值。最后,通过从预训练的InceptionV3网络中提取layers来创建梦境生成模型:self.base_model = InceptionV3(weights='imagenet', include_top=False) outputs = [self.base_model.get_layer(name).output for name in self.layers] self.dreamer_model = Model(self.base_model.input, outputs) -
定义一个私有方法来计算损失:
def _calculate_loss(self, image): image_batch = tf.expand_dims(image, axis=0) activations = self.dreamer_model(image_batch) if len(activations) == 1: activations = [activations] losses = [] for activation in activations: loss = tf.math.reduce_mean(activation) losses.append(loss) total_loss = tf.reduce_sum(losses) return total_loss -
定义一个私有方法来执行梯度上升(记住,我们希望放大图像中的图案)。为了提高性能,我们可以将此函数封装在
tf.function中:@tf.function def _gradient_ascent(self, image, steps, step_size): loss = tf.constant(0.0) for _ in range(steps): with tf.GradientTape() as tape: tape.watch(image) loss = self._calculate_loss(image) gradients = tape.gradient(loss, image) gradients /= tf.math.reduce_std(gradients) + 1e-8 image = image + gradients * step_size image = tf.clip_by_value(image, -1, 1) return loss, image -
定义一个私有方法,将梦境生成器产生的图像张量转换回
NumPy数组:def _deprocess(self, image): image = 255 * (image + 1.0) / 2.0 image = tf.cast(image, tf.uint8) image = np.array(image) return image -
定义一个私有方法,通过执行
_gradient_ascent()一定步数来生成梦幻图像:def _dream(self, image, steps, step_size): image = preprocess_input(image) image = tf.convert_to_tensor(image) step_size = tf.convert_to_tensor(step_size) step_size = tf.constant(step_size) steps_remaining = steps current_step = 0 while steps_remaining > 0: if steps_remaining > 100: run_steps = tf.constant(100) else: run_steps = tf.constant(steps_remaining) steps_remaining -= run_steps current_step += run_steps loss, image = self._gradient_ascent(image, run_steps, step_size) result = self._deprocess(image) return result -
定义一个公共方法来生成梦幻图像。这与
_dream()(在第 6 步中定义,并在此内部使用)之间的主要区别是,我们将使用不同的图像尺寸(称为self.octave_scale,每个self.octave_power_factors中的幂次):def dream(self, image, steps=100, step_size=0.01): image = tf.constant(np.array(image)) base_shape = tf.shape(image)[:-1] base_shape = tf.cast(base_shape, tf.float32) for factor in self.octave_power_factors: new_shape = tf.cast( base_shape * (self.octave_scale ** factor), tf.int32) image = tf.image.resize(image, new_shape).numpy() image = self._dream(image, steps=steps, step_size=step_size) base_shape = tf.cast(base_shape, tf.int32) image = tf.image.resize(image, base_shape) image = tf.image.convert_image_dtype(image / 255.0, dtype=tf.uint8) image = np.array(image) return np.array(image)
DeepDreamer()类可以重用,用于生成我们提供的任何图像的梦幻版本。我们将在下一节中看到这个如何工作。
它是如何工作的……
我们刚刚实现了一个实用类,方便地应用DeepDream。该算法通过计算一组层的激活值的梯度,然后使用这些梯度来增强网络所见的图案。
在我们的DeepDreamer()类中,之前描述的过程已在_gradient_ascent()方法中实现(在第 4 步中定义),我们计算了梯度并将其添加到原始图像中,通过多个步骤得到结果。最终结果是一个激活图,其中在每个后续步骤中,目标层中某些神经元的兴奋度被放大。
生成梦境的过程包括多次应用梯度上升,我们在_dream()方法中基本上已经做了这个(第 6 步)。
应用梯度上升于相同尺度的一个问题是,结果看起来噪声较大,分辨率低。而且,图案似乎发生在相同的粒度层级,这会导致结果的均匀性,从而减少我们想要的梦幻效果。为了解决这些问题,主要方法dream()在不同的尺度上应用梯度上升(称为八度音阶),其中一个八度的梦幻输出作为下一次迭代的输入,并且在更高的尺度上进行处理。
另见
要查看将不同参数组合传递给DeepDreamer()后的梦幻效果,请参阅下一篇食谱,生成你自己的梦幻图像。
生成你自己的梦幻图像
深度学习有一个有趣的方面。DeepDream 是一个应用程序,旨在通过激活特定层的某些激活点来理解深度神经网络的内部工作原理。然而,除了实验的调查意图外,它还产生了迷幻、梦幻般有趣的图像。
在这个配方中,我们将尝试几种DeepDream的配置,看看它们如何影响结果。
准备开始
我们将使用本章第一个配方中的DeepDreamer()实现(实现 DeepDream)。虽然我鼓励你尝试用自己的图像进行测试,但如果你想尽量跟随这个配方,你可以在这里下载示例图像:github.com/PacktPublis…
让我们来看一下示例图像:
图 4.1 – 示例图像
让我们开始吧。
如何实现……
按照以下步骤制作你自己的梦幻照片:
-
让我们从导入所需的包开始。请注意,我们正在从之前的配方中导入
DeepDreamer(),实现 DeepDream:import matplotlib.pyplot as plt from tensorflow.keras.preprocessing.image import * from ch4.recipe1.deepdream import DeepDreamer -
定义
load_image()函数,从磁盘加载图像到内存中,作为NumPy数组:def load_image(image_path): image = load_img(image_path) image = img_to_array(image) return image -
定义一个函数,使用
matplotlib显示图像(以NumPy数组表示):def show_image(image): plt.imshow(image) plt.show() -
加载原始图像并显示:
original_image = load_image('road.jpg') show_image(original_image / 255.0)这里,我们可以看到显示的原始图像:
图 4.2 – 我们将很快修改的原始图像
如我们所见,这只是穿过森林的一条道路。
-
使用默认参数生成图像的梦幻版,并显示结果:
dreamy_image = DeepDreamer().dream(original_image) show_image(dreamy_image)这是结果:
图 4.3 – 使用默认参数的 DeepDream 结果
结果保留了原始照片的整体主题,但在其上添加了大量失真,形成了圆形、曲线和其他基本图案。酷——又有点怪异!
-
使用三层。靠近顶部的层(例如,
'mixed7')编码更高层次的模式:dreamy_image = (DeepDreamer(layers=['mixed2', 'mixed5', 'mixed7']) .dream(original_image)) show_image(dreamy_image)这是使用三层后的结果:
图 4.4 – 使用更多更高层次层的 DeepDream 结果
更多层的加入让生成的梦幻效果变得更柔和。我们可以看到,图案比以前更平滑,这很可能是因为
'mixed7'层编码了更多的抽象信息,因为它离网络架构的末端更远。我们记得,网络中的前几层学习基本的模式,如线条和形状,而靠近输出的层则将这些基本模式组合起来,学习更复杂、更抽象的信息。 -
最后,让我们使用更多的八度音阶。我们期望的结果是图像中噪声较少,且具有更多异质模式:
dreamy_image = (DeepDreamer(octave_power_factors=[-3, -1, 0, 3]) .dream(original_image)) show_image(dreamy_image)这是使用更多八度后得到的结果图像:
图 4.5 – 使用更多八度的 DeepDream 效果
生成的梦境包含了一种令人满意的高低层次模式的混合,并且比步骤 4中生成的梦境具有更好的色彩分布。
让我们进入下一部分,了解我们刚刚做了什么。
它是如何工作的……
在这个食谱中,我们利用了在实现 DeepDream食谱中所做的工作,生成了几种我们输入图像(森林中的一条道路)的梦幻版本。通过结合不同的参数,我们发现结果可以有很大的变化。使用更高层次的抽象信息,我们获得了噪音更少、模式更精细的图片。
如果我们选择使用更多八度,这意味着网络将处理更多的图像,且在不同的尺度上进行处理。这种方法生成的图像饱和度较低,同时保留了卷积神经网络前几层中典型的更原始、更基本的模式。
最后,通过仅使用一张图片和一点创造力,我们可以获得非常有趣的结果!
深度学习的另一个更具娱乐性的应用是神经风格迁移,我们将在下一个食谱中讲解。
实现神经风格迁移
创造力和艺术表现并不是我们通常将其与深度神经网络和人工智能相联系的特征。然而,你知道吗,通过正确的调整,我们可以将预训练网络转变为令人印象深刻的艺术家,能够将像莫奈、毕加索和梵高这样的著名画家的独特风格应用到我们的平凡照片中?
这正是神经风格迁移的工作原理。通过这个食谱的学习,最终我们将掌握任何画家的艺术造诣!
正在准备中
我们不需要安装任何库或引入额外的资源来实现神经风格迁移。然而,由于这是一个实践性的食谱,我们不会详细描述解决方案的内部工作原理。如果你对神经风格迁移的细节感兴趣,建议阅读原始论文:arxiv.org/abs/1508.06576。
我希望你已经准备好,因为我们马上就要开始了!
如何操作……
按照这些步骤实现你自己的可重用神经风格迁移器:
-
导入必要的包(注意,在我们的实现中,我们使用了一个预训练的VGG19网络):
import numpy as np import tensorflow as tf from tensorflow.keras import Model from tensorflow.keras.applications.vgg19 import * -
定义
StyleTransferrer()类及其构造函数:class StyleTransferrer(object): def __init__(self, content_layers=None, style_layers=None): -
唯一相关的参数是内容和风格生成的两个可选层列表。如果它们是
None,我们将在内部使用默认值(稍后我们会看到)。接下来,加载预训练的VGG19并将其冻结:self.model = VGG19(weights='imagenet', include_top=False) self.model.trainable = False -
设置风格和内容损失的权重(重要性)(稍后我们会使用这些参数)。另外,存储内容和风格层(如果需要的话,可以使用默认设置):
self.style_weight = 1e-2 self.content_weight = 1e4 if content_layers is None: self.content_layers = ['block5_conv2'] else: self.content_layers = content_layers if style_layers is None: self.style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] else: self.style_layers = style_layers -
定义并存储样式迁移模型,该模型以VGG19输入层为输入,输出所有内容层和样式层(请注意,我们可以使用任何模型,但通常使用 VGG19 或 InceptionV3 能获得最佳效果):
outputs = [self.model.get_layer(name).output for name in (self.style_layers + self.content_layers)] self.style_model = Model([self.model.input], outputs) -
定义一个私有方法,用于计算Gram 矩阵,它用于计算图像的样式。它由一个矩阵表示,其中包含输入张量中不同特征图之间的均值和相关性(例如,特定层中的权重),被称为Gram 矩阵。有关Gram 矩阵的更多信息,请参阅本配方中的另请参阅部分:
def _gram_matrix(self, input_tensor): result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = np.prod(input_shape[1:3]) num_locations = tf.cast(num_locations,tf.float32) result = result / num_locations return result -
接下来,定义一个私有方法,用于计算输出(内容和样式)。该私有方法的作用是将输入传递给模型,然后计算所有样式层的Gram 矩阵以及内容层的身份,返回映射每个层名称到处理后值的字典:
def _calc_outputs(self, inputs): inputs = inputs * 255 preprocessed_input = preprocess_input(inputs) outputs = self.style_model(preprocessed_input) style_outputs = outputs[:len(self.style_layers)] content_outputs = outputs[len(self.style_layers):] style_outputs = [self._gram_matrix(style_output) for style_output in style_outputs] content_dict = {content_name: value for (content_name, value) in zip(self.content_layers, content_outputs)} style_dict = {style_name: value for (style_name, value) in zip(self.style_layers, style_outputs)} return {'content': content_dict, 'style': style_dict} -
定义一个静态辅助私有方法,用于将值限制在
0和1之间:@staticmethod def _clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) -
定义一个静态辅助私有方法,用于计算一对输出和目标之间的损失:
@staticmethod def _compute_loss(outputs, targets): return tf.add_n([ tf.reduce_mean((outputs[key] - targets[key]) ** 2) for key in outputs.keys() ]) -
定义一个私有方法,用于计算总损失,该损失是通过分别计算样式损失和内容损失,乘以各自权重并分配到相应层,再加总得到的:
def _calc_total_loss(self, outputs, style_targets, content_targets): style_outputs = outputs['style'] content_outputs = outputs['content'] n_style_layers = len(self.style_layers) s_loss = self._compute_loss(style_outputs, style_targets) s_loss *= self.style_weight / n_style_layers n_content_layers = len(self.content_layers) c_loss = self._compute_loss(content_outputs, content_targets) c_loss *= self.content_weight / n_content_layers return s_loss + c_loss -
接下来,定义一个私有方法,用于训练模型。在一定数量的 epochs 和每个 epoch 的指定步数下,我们将计算输出(样式和内容),计算总损失,并获取并应用梯度到生成的图像,同时使用
Adam作为优化器:@tf.function() def _train(self, image, s_targets, c_targets, epochs, steps_per_epoch): optimizer = tf.optimizers.Adam(learning_rate=2e-2, beta_1=0.99, epsilon=0.1) for _ in range(epochs): for _ in range(steps_per_epoch): with tf.GradientTape() as tape: outputs = self._calc_outputs(image) loss = self._calc_total_loss(outputs, s_targets, c_targets) gradient = tape.gradient(loss, image) optimizer.apply_gradients([(gradient, image)]) image.assign(self._clip_0_1(image)) return image -
定义一个静态辅助私有方法,用于将张量转换为
NumPy图像:@staticmethod def _tensor_to_image(tensor): tensor = tensor * 255 tensor = np.array(tensor, dtype=np.uint8) if np.ndim(tensor) > 3: tensor = tensor[0] return tensor -
最后,定义一个公共的
transfer()方法,该方法接受一张样式图像和一张内容图像,生成一张新图像。它应该尽可能保留内容,同时应用样式图像的样式:def transfer(self, s_image, c_image, epochs=10, steps_per_epoch=100): s_targets = self._calc_outputs(s_image)['style'] c_targets = self._calc_outputs(c_image)['content'] image = tf.Variable(c_image) image = self._train(image, s_targets, c_targets, epochs, steps_per_epoch) return self._tensor_to_image(image)
这可真是费了一番功夫!我们将在下一部分深入探讨。
它是如何工作的…
在本配方中,我们学到,神经风格迁移是通过优化两个损失而不是一个来工作的。一方面,我们希望尽可能保留内容,另一方面,我们希望让这个内容看起来像是使用样式图像的风格生成的。
内容量化是通过使用内容层实现的,正如我们在图像分类中通常会做的那样。那么,如何量化样式呢?这时,Gram 矩阵发挥了至关重要的作用,因为它计算了样式层的特征图(更准确地说,是输出)之间的相关性。
我们如何告诉网络内容比风格更重要呢?通过在计算组合损失时使用权重。默认情况下,内容权重是10,000,而风格权重仅为0.01。这告诉网络它的大部分努力应该集中在重现内容上,但也要稍微优化一下风格。
最终,我们获得了一张图像,它保留了原始图像的连贯性,但却拥有了风格参考图像的视觉吸引力,这是通过优化输出,使其匹配两个输入图像的统计特征所得到的结果。
另见
如果你想了解更多关于StyleTransferrer()运作背后的数学原理,请参见下一个配方,将风格迁移应用于自定义图像。
将风格迁移应用于自定义图像
你是否曾经想过,如果你最喜欢的艺术家画了你的小狗 Fluffy 的照片会是什么样子?如果你车子的照片与最喜爱的画作的魔力结合,会变成什么样?好吧,你再也不需要想象了!通过神经风格迁移,我们可以轻松地将我们最喜欢的图像变成美丽的艺术作品!
在这个配方中,我们将使用我们在实现神经风格迁移配方中实现的StyleTransferrer()类来为我们自己的图像添加风格。
准备中
在这个配方中,我们将使用上一个配方中的StyleTransferrer()实现。为了最大化您从这个配方中获得的乐趣,您可以在这里找到示例图像以及许多不同的画作(您可以用作风格参考):
github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe4。
以下是我们将使用的示例图像:
图 4.6 – 示例内容图像
让我们开始吧!
如何操作…
以下步骤将教您如何将著名画作的风格转移到您自己的图像上:
-
导入必要的包:
import matplotlib.pyplot as plt import tensorflow as tf from chapter4.recipe3.styletransfer import StyleTransferrer请注意,我们正在导入在实现神经风格迁移配方中实现的
StyleTransferrer()。 -
告诉 TensorFlow 我们希望以急切模式运行,因为否则,它会尝试在图模式下运行
StyleTransferrer()中的tf.function装饰器函数,这将导致其无法正常工作:tf.config.experimental_run_functions_eagerly(True) -
定义一个函数,将图像加载为 TensorFlow 张量。请注意,我们正在将其重新缩放到一个合理的大小。我们这样做是因为神经风格迁移是一个资源密集型的过程,因此处理大图像可能需要很长时间:
def load_image(image_path): dimension = 512 image = tf.io.read_file(image_path) image = tf.image.decode_jpeg(image, channels=3) image = tf.image.convert_image_dtype(image, tf.float32) shape = tf.cast(tf.shape(image)[:-1], tf.float32) longest_dimension = max(shape) scale = dimension / longest_dimension new_shape = tf.cast(shape * scale, tf.int32) image = tf.image.resize(image, new_shape) return image[tf.newaxis, :] -
定义一个函数,用于通过
matplotlib显示图像:def show_image(image): if len(image.shape) > 3: image = tf.squeeze(image, axis=0) plt.imshow(image) plt.show() -
加载内容图像并显示它:
content = load_image('bmw.jpg') show_image(content)这是内容图像:
图 4.7 – 一辆车的内容图像
我们将把一幅画作的风格应用到这张图像上。
-
加载并显示风格图像:
style = load_image(art.jpg') show_image(style)这是风格图像:
图 4.8 – 风格图像
你能想象如果这幅画的艺术家为我们的车绘制图像,它会是什么样子吗?
-
使用
StyleTransferrer()将画作的风格应用到我们的 BMW 图像上。然后,展示结果:stylized_image = StyleTransferrer().transfer(style, content) show_image(stylized_image)这是结果:
图 4.9 – 将画作的风格应用到内容图像的结果
惊艳吧,是不是?
-
重复这个过程,这次进行 100 个训练周期:
stylized_image = StyleTransferrer().transfer(style, content, epochs=100) show_image(stylized_image)这是结果:
图 4.10 – 对内容图像应用画作风格的结果(100 个训练周期)
这次,结果更为锐利。然而,我们不得不等一段时间才能完成这个过程。时间和质量之间的权衡非常明显。
让我们继续进入下一部分。
它是如何工作的…
在这个食谱中,我们利用了在实现神经风格迁移食谱中所做的辛勤工作。我们取了一张汽车的图像,并将一幅酷炫迷人的艺术作品风格应用到其中。正如我们所看到的,结果非常吸引人。
然而,我们必须意识到这个过程的负担,因为在 CPU 上完成它需要很长时间——即使是在 GPU 上也是如此。因此,我们需要在用于精细化结果的训练周期或迭代次数与最终输出质量之间进行权衡。
参见也
我鼓励你尝试使用自己的图片和风格来应用这个食谱。作为起点,你可以使用以下仓库中的图像来快速入门:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe4。在那里,你将找到来自沃霍尔、马蒂斯、莫奈等人的著名艺术作品。
使用 TFHub 应用风格迁移
从零开始实现神经风格迁移是一项艰巨的任务。幸运的是,我们可以使用TensorFlow Hub(TFHub)中的现成解决方案。
在这个食谱中,我们只需几行代码,就能通过 TFHub 提供的工具和便捷性,快速为自己的图像添加风格。
准备工作
我们必须安装 tensorflow-hub。我们只需一个简单的 pip 命令即可完成:
$> pip install tensorflow-hub
如果你想访问不同的示例内容和风格图像,请访问这个链接:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe5。
让我们来看一下示例图像:
](tos-cn-i-73owjymdk6/63090714588148d6a74e827fbfd96539)
图 4.11 – 内容图像
让我们开始吧!
如何操作…
使用 TFHub 进行神经风格迁移非常简单!按照以下步骤完成此食谱:
-
导入必要的依赖项:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from tensorflow_hub import load -
定义一个将图像加载为 TensorFlow 张量的函数。由于神经风格迁移是一个计算密集型的过程,因此我们需要对图像进行重缩放,以节省时间和资源,因为处理大图像可能会花费很长时间:
def load_image(image_path): dimension = 512 image = tf.io.read_file(image_path) image = tf.image.decode_jpeg(image, channels=3) image = tf.image.convert_image_dtype(image, tf.float32) shape = tf.cast(tf.shape(image)[:-1], tf.float32) longest_dimension = max(shape) scale = dimension / longest_dimension new_shape = tf.cast(shape * scale, tf.int32) image = tf.image.resize(image, new_shape) return image[tf.newaxis, :] -
定义一个将张量转换为图像的函数:
def tensor_to_image(tensor): tensor = tensor * 255 tensor = np.array(tensor, dtype=np.uint8) if np.ndim(tensor) > 3: tensor = tensor[0] return tensor -
定义一个使用
matplotlib显示图像的函数:def show_image(image): if len(image.shape) > 3: image = tf.squeeze(image, axis=0) plt.imshow(image) plt.show() -
定义风格迁移实现的路径,并加载模型:
module_url = ('https://tfhub.dev/google/magenta/' 'arbitrary-image-stylization-v1-256/2') hub_module = load(module_url) -
加载内容图像,然后显示它:
image = load_image('bmw.jpg') show_image(image)就是这个:
图 4.12 – 一辆车的内容图像
我们将在下一步应用风格迁移到这张照片上。
-
加载并显示风格图像:
style_image = load_image('art4.jpg') show_image(style_image)在这里,你可以看到风格图像:
图 4.13 – 这是我们选择的风格图像
我们将这个和内容图像传递给我们最近创建的 TFHub 模块,并等待结果。
-
使用我们从 TFHub 下载的模型应用神经风格迁移,并显示结果:
results = hub_module(tf.constant(image), tf.constant(style_image)) stylized_image = tensor_to_image(results[0]) show_image(stylized_image)这是使用 TFHub 应用神经风格迁移的结果:
图 4.14 – 使用 TFHub 应用风格迁移的结果
瞧! 结果看起来相当不错,你不觉得吗?我们将在下一节深入探讨。
它是如何工作的……
在这个食谱中,我们学到,使用 TFHub 进行图像风格化比从头实现算法要容易得多。然而,它给了我们较少的控制,因为它像一个黑盒子。
无论哪种方式,结果都相当令人满意,因为它保持了原始场景的连贯性和意义,同时将风格图像的艺术特征叠加在上面。
最重要的部分是从 TFHub 下载正确的模块,然后使用 load() 函数加载它。
为了让预打包模块正常工作,我们必须将内容和风格图像都作为 tf.constant 常量传递。
最后,由于我们接收到的是一个张量,为了正确地在屏幕上显示结果,我们使用了自定义函数 tensor_to_image(),将其转化为可以通过 matplotlib 容易绘制的 NumPy 数组。
另见
你可以在此链接阅读更多关于我们使用的 TFHub 模块:tfhub.dev/google/mage…
另外,为什么不尝试一下你自己的图像和其他风格呢?你可以使用这里的资源作为起点:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch4/recipe5。
使用深度学习提高图像分辨率
卷积神经网络(CNN) 也可以用来提高低质量图像的分辨率。历史上,我们可以通过使用插值技术、基于示例的方法,或需要学习的低到高分辨率映射来实现这一点。
正如我们在这个步骤中将看到的,通过使用基于端到端深度学习的方法,我们可以更快地获得更好的结果。
听起来有趣吗?那我们就开始吧!
准备工作
在这个步骤中,我们需要使用Pillow,你可以通过以下命令安装:
$> pip install Pillow
在这个步骤中,我们使用的是Dog and Cat Detection数据集,该数据集托管在 Kaggle 上:www.kaggle.com/andrewmvd/dog-and-cat-detection。要下载它,你需要在网站上登录或注册。一旦登录,将其保存到你喜欢的地方,命名为dogscats.zip。然后,将其解压到一个名为dogscats的文件夹中。从现在开始,我们假设数据存储在~/.keras/datasets/dogscats目录下。
以下是数据集中两个类别的示例:
图 4.15 – 示例图像
让我们开始吧!
如何做……
按照以下步骤实现一个全卷积网络,以执行图像超分辨率:
-
导入所有必要的模块:
import pathlib from glob import glob import matplotlib.pyplot as plt import numpy as np from PIL import Image from tensorflow.keras import Model from tensorflow.keras.layers import * from tensorflow.keras.optimizers import Adam from tensorflow.keras.preprocessing.image import * -
定义一个函数,构建网络架构。请注意,这是一个全卷积网络,这意味着它仅由卷积层(除了激活层)组成,包括输出层:
def build_srcnn(height, width, depth): input = Input(shape=(height, width, depth)) x = Conv2D(filters=64, kernel_size=(9, 9), kernel_initializer='he_normal')(input) x = ReLU()(x) x = Conv2D(filters=32, kernel_size=(1, 1), kernel_initializer='he_normal')(x) x = ReLU()(x) output = Conv2D(filters=depth, kernel_size=(5, 5), kernel_initializer='he_normal')(x) return Model(input, output) -
定义一个函数,根据缩放因子调整图像的大小。需要考虑的是,它接收的是一个表示图像的
NumPy数组:def resize_image(image_array, factor): original_image = Image.fromarray(image_array) new_size = np.array(original_image.size) * factor new_size = new_size.astype(np.int32) new_size = tuple(new_size) resized = original_image.resize(new_size) resized = img_to_array(resized) resized = resized.astype(np.uint8) return resized -
定义一个函数,紧密裁剪图像。我们这样做是因为当我们稍后应用滑动窗口提取补丁时,希望图像能恰当地适应。
SCALE是我们希望网络学习如何放大图像的因子:def tight_crop_image(image): height, width = image.shape[:2] width -= int(width % SCALE) height -= int(height % SCALE) return image[:height, :width] -
定义一个函数,故意通过缩小图像然后再放大它来降低图像分辨率:
def downsize_upsize_image(image): scaled = resize_image(image, 1.0 / SCALE) scaled = resize_image(scaled, SCALE / 1.0) return scaled -
定义一个函数,用于从输入图像中裁剪补丁。
INPUT_DIM是我们输入到网络中的图像的高度和宽度:def crop_input(image, x, y): y_slice = slice(y, y + INPUT_DIM) x_slice = slice(x, x + INPUT_DIM) return image[y_slice, x_slice] -
定义一个函数,用于裁剪输出图像的区域。
LABEL_SIZE是网络输出图像的高度和宽度。另一方面,PAD是用于填充的像素数,确保我们正确裁剪感兴趣的区域:def crop_output(image, x, y): y_slice = slice(y + PAD, y + PAD + LABEL_SIZE) x_slice = slice(x + PAD, x + PAD + LABEL_SIZE) return image[y_slice, x_slice] -
设置随机种子:
SEED = 999 np.random.seed(SEED) -
加载数据集中所有图像的路径:
file_patten = (pathlib.Path.home() / '.keras' / 'datasets' / 'dogscats' / 'images' / '*.png') file_pattern = str(file_patten) dataset_paths = [*glob(file_pattern)] -
因为数据集非常庞大,而我们并不需要其中所有的图像来实现我们的目标,所以让我们随机挑选其中 1,500 张:
SUBSET_SIZE = 1500 dataset_paths = np.random.choice(dataset_paths, SUBSET_SIZE) -
定义将用于创建低分辨率补丁数据集(作为输入)和高分辨率补丁(作为标签)数据集的参数。除了
STRIDE参数外,所有这些参数都在前面的步骤中定义过。STRIDE是我们在水平和垂直轴上滑动提取补丁时使用的像素数:SCALE = 2.0 INPUT_DIM = 33 LABEL_SIZE = 21 PAD = int((INPUT_DIM - LABEL_SIZE) / 2.0) STRIDE = 14 -
构建数据集。输入将是从图像中提取的低分辨率补丁,这些补丁是通过缩小和放大处理过的。标签将是来自未改变图像的补丁:
data = [] labels = [] for image_path in dataset_paths: image = load_img(image_path) image = img_to_array(image) image = image.astype(np.uint8) image = tight_crop_image(image) scaled = downsize_upsize_image(image) height, width = image.shape[:2] for y in range(0, height - INPUT_DIM + 1, STRIDE): for x in range(0, width - INPUT_DIM + 1, STRIDE): crop = crop_input(scaled, x, y) target = crop_output(image, x, y) data.append(crop) labels.append(target) data = np.array(data) labels = np.array(labels) -
实例化网络,我们将在 12 个周期内进行训练,并使用
Adam()作为优化器,同时进行学习率衰减。损失函数是'mse'。为什么?因为我们的目标不是实现高准确率,而是学习一组过滤器,正确地将低分辨率图像块映射到高分辨率:EPOCHS = 12 optimizer = Adam(lr=1e-3, decay=1e-3 / EPOCHS) model = build_srcnn(INPUT_DIM, INPUT_DIM, 3) model.compile(loss='mse', optimizer=optimizer) -
训练网络:
BATCH_SIZE = 64 model.fit(data, labels, batch_size=BATCH_SIZE, epochs=EPOCHS) -
现在,为了评估我们的解决方案,我们将加载一张测试图像,将其转换为
NumPy数组,并降低其分辨率:image = load_img('dogs.jpg') image = img_to_array(image) image = image.astype(np.uint8) image = tight_crop_image(image) scaled = downsize_upsize_image(image) -
显示低分辨率图像:
plt.title('Low resolution image (Downsize + Upsize)') plt.imshow(scaled) plt.show()让我们看看结果:
图 4.16 – 低分辨率测试图像
现在,我们想要创建这张照片的更清晰版本。
-
创建一个与输入图像相同尺寸的画布。这是我们存储网络生成的高分辨率图像块的地方:
output = np.zeros(scaled.shape) height, width = output.shape[:2] -
提取低分辨率图像块,将它们传入网络以获得高分辨率的对应图像块,并将它们放置在输出画布的正确位置:
for y in range(0, height - INPUT_DIM + 1, LABEL_SIZE): for x in range(0, width - INPUT_DIM + 1, LABEL_SIZE): crop = crop_input(scaled, x, y) image_batch = np.expand_dims(crop, axis=0) prediction = model.predict(image_batch) new_shape = (LABEL_SIZE, LABEL_SIZE, 3) prediction = prediction.reshape(new_shape) output_y_slice = slice(y + PAD, y + PAD + LABEL_SIZE) output_x_slice = slice(x + PAD, x + PAD + LABEL_SIZE) output[output_y_slice, output_x_slice] = prediction -
最后,显示高分辨率结果:
plt.title('Super resolution result (SRCNN output)') plt.imshow(output / 255) plt.show()这是超分辨率输出:
图 4.17 – 高分辨率测试图像
与低分辨率图像相比,这张照片更好地展示了狗和整个场景的细节,你不觉得吗?
提示
我建议你在 PDF 或照片查看器中同时打开低分辨率和高分辨率的图像。这将帮助你仔细检查它们之间的差异,并让你确信网络完成了它的工作。在本书的打印版本中,可能很难判断这种区别。
它是如何工作的……
在这个教程中,我们创建了一个能够提高模糊或低分辨率图像分辨率的模型。这个实现的最大收获是它由完全卷积神经网络驱动,意味着它只包含卷积层及其激活。
这是一个回归问题,输出中的每个像素都是我们想要学习的特征。
然而,我们的目标不是优化准确性,而是训练模型,使特征图能够编码必要的信息,从低分辨率图像生成高分辨率图像块。
现在,我们必须问自己:为什么是图像块?我们不想学习图像中的内容。相反,我们希望网络弄清楚如何从低分辨率到高分辨率。图像块足够适合这个目的,因为它们包含了局部的模式,更容易理解。
你可能已经注意到我们并没有训练很多周期(只有 12 个)。这是经过设计的,因为研究表明,训练过长实际上会损害网络的性能。
最后需要注意的是,由于该网络是在狗和猫的图像上进行训练的,因此它的专长在于放大这些动物的照片。尽管如此,通过更换数据集,我们可以轻松地创建一个专门处理其他类型数据的超分辨率网络。
另请参见
我们的实现基于董等人的重要工作,有关该主题的论文可以在这里阅读:arxiv.org/abs/1501.00092
第五章:第五章:使用自编码器减少噪声
在深度神经网络家族中,最有趣的家族之一就是自编码器家族。正如其名称所示,它们的唯一目的就是处理输入数据,然后将其重建为原始形状。换句话说,自编码器学习将输入复制到输出。为什么?因为这一过程的副作用就是我们所追求的目标:不是生成标签或分类,而是学习输入到自编码器的图像的高效、高质量表示。这种表示的名称是编码。
它们是如何实现这一点的呢?通过同时训练两个网络:一个编码器,它接受图像并生成编码,另一个是解码器,它接受编码并尝试从中重建输入数据。
在本章中,我们将从基础开始,首先实现一个简单的全连接自编码器。之后,我们将创建一个更常见且多功能的卷积自编码器。我们还将学习如何在更实际的应用场景中使用自编码器,比如去噪图像、检测数据集中的异常值和创建逆向图像搜索索引。听起来有趣吗?
在本章中,我们将涵盖以下食谱:
-
创建一个简单的全连接自编码器
-
创建一个卷积自编码器
-
使用自编码器去噪图像
-
使用自编码器检测异常值
-
使用深度学习创建逆向图像搜索索引
-
实现一个变分自编码器
让我们开始吧!
技术要求
尽管使用 GPU 始终是一个好主意,但其中一些示例(特别是创建一个简单的全连接自编码器)在中端 CPU(如 Intel i5 或 i7)上运行良好。如果某个特定的示例依赖外部资源或需要预备步骤,您将在准备工作部分找到详细的准备说明。您可以随时访问本章的所有代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch5。
请查看以下链接,观看《代码实践》视频:
创建一个简单的全连接自编码器
自编码器在设计和功能上都很独特。这也是为什么掌握自编码器基本原理,尤其是实现可能是最简单版本的自编码器——全连接自编码器,是一个好主意。
在本示例中,我们将实现一个全连接自编码器,来重建Fashion-MNIST中的图像,这是一个标准数据集,几乎不需要预处理,允许我们专注于自编码器本身。
你准备好了吗?让我们开始吧!
准备工作
幸运的是,Fashion-MNIST已经与 TensorFlow 一起打包,因此我们无需自己下载。
我们将使用OpenCV,一个著名的计算机视觉库,来创建一个马赛克,以便我们能够将原始图像与自编码器重建的图像进行对比。你可以通过pip轻松安装OpenCV:
$> pip install opencv-contrib-python
现在,所有准备工作都已完成,来看看具体步骤吧!
如何操作…
按照以下步骤,来实现一个简单而有效的自编码器:
-
导入必要的包来实现全连接自编码器:
import cv2 import numpy as np from tensorflow.keras import Model from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.layers import * -
定义一个函数来构建自编码器的架构。默认情况下,编码或潜在向量的维度是128,但16、32和64也是不错的选择:
def build_autoencoder(input_shape=784, encoding_dim=128): input_layer = Input(shape=(input_shape,)) encoded = Dense(units=512)(input_layer) encoded = ReLU()(encoded) encoded = Dense(units=256)(encoded) encoded = ReLU()(encoded) encoded = Dense(encoding_dim)(encoded) encoding = ReLU()(encoded) decoded = Dense(units=256)(encoding) decoded = ReLU()(decoded) decoded = Dense(units=512)(decoded) decoded = ReLU()(decoded) decoded = Dense(units=input_shape)(decoded) decoded = Activation('sigmoid')(decoded) return Model(input_layer, decoded) -
定义一个函数,用于将一组常规图像与其原始对应图像进行对比绘制,以便直观评估自编码器的性能:
def plot_original_vs_generated(original, generated): num_images = 15 sample = np.random.randint(0, len(original), num_images) -
前一个代码块选择了 15 个随机索引,我们将用它们从
original和generated批次中挑选相同的样本图像。接下来,定义一个内部函数,这样我们就可以将 15 张图像按 3x5 网格排列:def stack(data): images = data[sample] return np.vstack([np.hstack(images[:5]), np.hstack(images[5:10]), np.hstack(images[10:15])]) -
现在,定义另一个内部函数,以便我们可以在图像上添加文字。这将有助于区分生成的图像和原始图像,稍后我们将看到如何操作:
def add_text(image, text, position): pt1 = position pt2 = (pt1[0] + 10 + (len(text) * 22), pt1[1] - 45) cv2.rectangle(image, pt1, pt2, (255, 255, 255), -1) cv2.putText(image, text, position, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1.3, color=(0, 0, 0), thickness=4) -
完成该函数,通过从原始和生成的图像组中选择相同的图像。然后,将这两组图像堆叠在一起形成马赛克,调整其大小为 860x860,使用
add_text()在马赛克中标注原始图像和生成图像,并显示结果:original = stack(original) generated = stack(generated) mosaic = np.vstack([original, generated]) mosaic = cv2.resize(mosaic, (860, 860), interpolation=cv2.INTER_AREA) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR) add_text(mosaic, 'Original', (50, 100)) add_text(mosaic, 'Generated', (50, 520)) cv2.imshow('Mosaic', mosaic) cv2.waitKey(0) -
下载(或加载缓存的)
Fashion-MNIST。由于这不是一个分类问题,我们只保留图像,而不保留标签:(X_train, _), (X_test, _) = fashion_mnist.load_data() -
对图像进行归一化:
X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 -
将图像重塑为向量:
X_train = X_train.reshape((X_train.shape[0], -1)) X_test = X_test.reshape((X_test.shape[0], -1)) -
构建自编码器并进行编译。我们将使用
'adam'作为优化器,均方误差('mse')作为损失函数。为什么?我们不关心分类是否正确,而是尽可能准确地重建输入,这意味着要最小化总体误差:autoencoder = build_autoencoder() autoencoder.compile(optimizer='adam', loss='mse') -
在 300 个 epoch 上拟合自编码器,这是一个足够大的数字,能够让网络学习到输入的良好表示。为了加快训练过程,我们每次传入
1024个向量的批次(可以根据硬件能力自由调整批次大小)。请注意,输入特征也是标签或目标:EPOCHS = 300 BATCH_SIZE = 1024 autoencoder.fit(X_train, X_train, epochs=EPOCHS, batch_size=BATCH_SIZE, shuffle=True, validation_data=(X_test, X_test)) -
对测试集进行预测(基本上就是生成测试向量的副本):
predictions = autoencoder.predict(X_test) -
将预测值和测试向量重新调整为 28x28x1 尺寸的灰度图像:
original_shape = (X_test.shape[0], 28, 28) predictions = predictions.reshape(original_shape) X_test = X_test.reshape(original_shape) -
生成原始图像与自编码器生成图像的对比图:
plot_original_vs_generated(X_test, predictions)这是结果:
图 5.1 – 原始图像(前三行)与生成的图像(底部三行)
根据结果来看,我们的自编码器做得相当不错。在所有情况下,服装的形状都得到了很好的保留。然而,它在重建内部细节时并不如预期准确,如第六行第四列中的 T 恤所示,原图中的横条纹在生成的副本中缺失了。
它是如何工作的……
在这个食谱中,我们了解到自编码器是通过将两个网络结合成一个来工作的:编码器和解码器。在 build_autoencoder() 函数中,我们实现了一个完全连接的自编码架构,其中编码器部分接受一个 784 元素的向量并输出一个包含 128 个数字的编码。然后,解码器接收这个编码并通过几个堆叠的全连接层进行扩展,最后一个层生成一个 784 元素的向量(与输入的维度相同)。
训练过程因此包括最小化编码器接收的输入与解码器产生的输出之间的距离或误差。实现这一目标的唯一方法是学习能在压缩输入时最小化信息损失的编码。
尽管损失函数(在此情况下为 MSE)是衡量自编码器学习进展的好方法,但对于这些特定的网络,视觉验证同样重要,甚至可能更为关键。这就是我们实现 plot_original_vs_generated() 函数的原因:检查副本是否看起来像它们的原始对应物。
你为什么不试试改变编码大小呢?它是如何影响副本质量的?
另见
如果你想知道为什么 Fashion-MNIST 会存在,可以查看这里的官方仓库:github.com/zalandoresearch/fashion-mnist。
创建卷积自编码器
与常规神经网络一样,处理图像时,使用卷积通常是最好的选择。在自编码器的情况下,这也是一样的。在这个食谱中,我们将实现一个卷积自编码器,用于重建 Fashion-MNIST 中的图像。
区别在于,在解码器中,我们将使用反向或转置卷积,它可以放大体积,而不是缩小它们。这是传统卷积层中发生的情况。
这是一个有趣的食谱。你准备好开始了吗?
准备工作
因为 TensorFlow 提供了方便的函数来下载 Fashion-MNIST,所以我们不需要在数据端做任何手动准备。然而,我们必须安装 OpenCV,以便我们可以可视化自编码器的输出。可以使用以下命令来完成:
$> pip install opencv-contrib-python
事不宜迟,让我们开始吧。
如何做……
按照以下步骤实现一个完全功能的卷积自编码器:
-
让我们导入必要的依赖:
import cv2 import numpy as np from tensorflow.keras import Model from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.layers import * -
定义
build_autoencoder()函数,该函数内部构建自编码器架构,并返回编码器、解码器以及自编码器本身。首先定义输入层和第一组 32 个卷积过滤器:def build_autoencoder(input_shape=(28, 28, 1), encoding_size=32, alpha=0.2): inputs = Input(shape=input_shape) encoder = Conv2D(filters=32, kernel_size=(3, 3), strides=2, padding='same')(inputs) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder)定义第二组卷积层(这次是 64 个卷积核):
encoder = Conv2D(filters=64, kernel_size=(3, 3), strides=2, padding='same')(encoder) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder)定义编码器的输出层:
encoder_output_shape = encoder.shape encoder = Flatten()(encoder) encoder_output = Dense(units=encoding_size)(encoder) encoder_model = Model(inputs, encoder_output) -
在步骤 2中,我们定义了编码器模型,这是一个常规的卷积神经网络。下一块代码定义了解码器模型,从输入和 64 个反卷积过滤器开始:
decoder_input = Input(shape=(encoding_size,)) target_shape = tuple(encoder_output_shape[1:]) decoder = Dense(np.prod(target_shape))(decoder_input) decoder = Reshape(target_shape)(decoder) decoder = Conv2DTranspose(filters=64, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder)定义第二组反卷积层(这次是 32 个卷积核):
decoder = Conv2DTranspose(filters=32, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder)定义解码器的输出层:
decoder = Conv2DTranspose(filters=1, kernel_size=(3, 3), padding='same')(decoder) outputs = Activation('sigmoid')(decoder) decoder_model = Model(decoder_input, outputs) -
解码器使用
Conv2DTranspose层,该层将输入扩展以生成更大的输出体积。注意,我们进入解码器的层数越多,Conv2DTranspose层使用的过滤器就越少。最后,定义自编码器:encoder_model_output = encoder_model(inputs) decoder_model_output = decoder_model(encoder_model_output) autoencoder_model = Model(inputs, decoder_model_output) return encoder_model, decoder_model, autoencoder_model自编码器是端到端的架构。它从输入层开始,进入编码器,最后通过解码器输出层,得出结果。
-
定义一个函数,将一般图像样本与其原始图像进行对比绘制。这将帮助我们直观评估自编码器的性能。(这是我们在前一个示例中定义的相同函数。有关更完整的解释,请参考本章的创建简单的全连接自编码器一节。)请看以下代码:
def plot_original_vs_generated(original, generated): num_images = 15 sample = np.random.randint(0, len(original), num_images) -
定义一个内部辅助函数,用于将图像样本堆叠成一个 3x5 的网格:
def stack(data): images = data[sample] return np.vstack([np.hstack(images[:5]), np.hstack(images[5:10]), np.hstack(images[10:15])]) -
接下来,定义一个函数,将文本放置到图像的指定位置:
def add_text(image, text, position): pt1 = position pt2 = (pt1[0] + 10 + (len(text) * 22), pt1[1] - 45) cv2.rectangle(image, pt1, pt2, (255, 255, 255), -1) cv2.putText(image, text, position, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1.3, color=(0, 0, 0), thickness=4) -
最后,创建一个包含原始图像和生成图像的马赛克:
original = stack(original) generated = stack(generated) mosaic = np.vstack([original, generated]) mosaic = cv2.resize(mosaic, (860, 860), interpolation=cv2.INTER_AREA) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR) add_text(mosaic, 'Original', (50, 100)) add_text(mosaic, 'Generated', (50, 520)) cv2.imshow('Mosaic', mosaic) cv2.waitKey(0) -
下载(或加载,如果已缓存)
Fashion-MNIST。我们只关心图像,因此可以丢弃标签:(X_train, _), (X_test, _) = fashion_mnist.load_data() -
对图像进行归一化并添加通道维度:
X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 X_train = np.expand_dims(X_train, axis=-1) X_test = np.expand_dims(X_test, axis=-1) -
这里,我们只关心自编码器,因此会忽略
build_autoencoder()函数的其他两个返回值。然而,在不同的情况下,我们可能需要保留它们。我们将使用'adam'优化器训练模型,并使用'mse'作为损失函数,因为我们希望减少误差,而不是优化分类准确性:_, _, autoencoder = build_autoencoder(encoding_size=256) autoencoder.compile(optimizer='adam', loss='mse') -
在 300 个训练周期中训练自编码器,每次批处理 512 张图像。注意,输入图像也是标签:
EPOCHS = 300 BATCH_SIZE = 512 autoencoder.fit(X_train, X_train, epochs=EPOCHS, batch_size=BATCH_SIZE, shuffle=True, validation_data=(X_test, X_test), verbose=1) -
复制测试集:
predictions = autoencoder.predict(X_test) -
将预测结果和测试图像的形状调整回 28x28(无通道维度):
original_shape = (X_test.shape[0], 28, 28) predictions = predictions.reshape(original_shape) X_test = X_test.reshape(original_shape) predictions = (predictions * 255.0).astype('uint8') X_test = (X_test * 255.0).astype('uint8') -
生成原始图像与自编码器输出的复制图像的对比马赛克:
plot_original_vs_generated(X_test, predictions)让我们看看结果:
图 5.2 – 原始图像的马赛克(前三行),与卷积自编码器生成的图像(后三行)进行对比
正如我们所见,自编码器已经学到了一个很好的编码,它使得它能够以最小的细节损失重建输入图像。让我们进入下一个部分,了解它是如何工作的!
它是如何工作的…
在这个教程中,我们了解到卷积自编码器是这一系列神经网络中最常见且最强大的成员之一。该架构的编码器部分是一个常规的卷积神经网络,依赖卷积和密集层来缩小输出并生成向量表示。解码器则是最有趣的部分,因为它必须处理相反的问题:根据合成的特征向量(即编码)重建输入。
它是如何做到的呢?通过使用转置卷积(Conv2DTranspose)。与传统的Conv2D层不同,转置卷积产生的是较浅的体积(较少的过滤器),但是它们更宽更高。结果是输出层只有一个过滤器,并且是 28x28 的维度,这与输入的形状相同。很有趣,不是吗?
训练过程包括最小化输出(生成的副本)和输入(原始图像)之间的误差。因此,均方误差(MSE)是一个合适的损失函数,因为它为我们提供了这个信息。
最后,我们通过目视检查一组测试图像及其合成的对照图像来评估自编码器的性能。
提示
在自编码器中,编码的大小至关重要,以确保解码器有足够的信息来重建输入。
另见
这里有一个关于转置卷积的很好的解释:towardsdatascience.com/transposed-convolution-demystified-84ca81b4baba。
使用自编码器去噪图像
使用图像重建输入是很棒的,但有没有更有用的方式来应用自编码器呢?当然有!其中之一就是图像去噪。如其名所示,这就是通过用合理的值替换损坏的像素和区域来恢复损坏的图像。
在这个教程中,我们将故意损坏Fashion-MNIST中的图像,然后训练一个自编码器去噪它们。
准备就绪
Fashion-MNIST可以通过 TensorFlow 提供的便利函数轻松访问,因此我们不需要手动下载数据集。另一方面,因为我们将使用OpenCV来创建一些可视化效果,所以我们必须安装它,方法如下:
$> pip install opencv-contrib-python
让我们开始吧!
如何做…
按照以下步骤实现一个能够恢复损坏图像的卷积自编码器:
-
导入所需的包:
import cv2 import numpy as np from tensorflow.keras import Model from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.layers import * -
定义
build_autoencoder()函数,它创建相应的神经网络架构。请注意,这是我们在前一个教程中实现的相同架构;因此,我们在这里不再详细讲解。有关详细解释,请参见创建卷积自编码器教程:def build_autoencoder(input_shape=(28, 28, 1), encoding_size=128, alpha=0.2): inputs = Input(shape=input_shape) encoder = Conv2D(filters=32, kernel_size=(3, 3), strides=2, padding='same')(inputs) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder = Conv2D(filters=64, kernel_size=(3, 3), strides=2, padding='same')(encoder) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder_output_shape = encoder.shape encoder = Flatten()(encoder) encoder_output = Dense(units=encoding_size)(encoder) encoder_model = Model(inputs, encoder_output) -
现在我们已经创建了编码器模型,接下来创建解码器:
decoder_input = Input(shape=(encoding_size,)) target_shape = tuple(encoder_output_shape[1:]) decoder = Dense(np.prod(target_shape))(decoder_input) decoder = Reshape(target_shape)(decoder) decoder = Conv2DTranspose(filters=64, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=32, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=1, kernel_size=(3, 3), padding='same')(decoder) outputs = Activation('sigmoid')(decoder) decoder_model = Model(decoder_input, outputs) -
最后,定义自编码器本身并返回三个模型:
encoder_model_output = encoder_model(inputs) decoder_model_output = decoder_model(encoder_model_output) autoencoder_model = Model(inputs, decoder_model_output) return encoder_model, decoder_model, autoencoder_model -
定义
plot_original_vs_generated()函数,该函数创建原始图像与生成图像的比较拼图。我们稍后将使用此函数来显示噪声图像及其恢复后的图像。与build_autoencoder()类似,该函数的工作方式与我们在创建一个简单的全连接自编码器食谱中定义的相同,因此如果您需要详细解释,请查阅该食谱:def plot_original_vs_generated(original, generated): num_images = 15 sample = np.random.randint(0, len(original), num_images) -
定义一个内部辅助函数,将一组图像按 3x5 网格堆叠:
def stack(data): images = data[sample] return np.vstack([np.hstack(images[:5]), np.hstack(images[5:10]), np.hstack(images[10:15])]) -
定义一个函数,将自定义文本放置在图像上的特定位置:
def add_text(image, text, position): pt1 = position pt2 = (pt1[0] + 10 + (len(text) * 22), pt1[1] - 45) cv2.rectangle(image, pt1, pt2, (255, 255, 255), -1) cv2.putText(image, text, position, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1.3, color=(0, 0, 0), thickness=4) -
创建包含原始图像和生成图像的拼图,标记每个子网格并显示结果:
original = stack(original) generated = stack(generated) mosaic = np.vstack([original, generated]) mosaic = cv2.resize(mosaic, (860, 860), interpolation=cv2.INTER_AREA) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_GRAY2BGR) add_text(mosaic, 'Original', (50, 100)) add_text(mosaic, 'Generated', (50, 520)) cv2.imshow('Mosaic', mosaic) cv2.waitKey(0) -
使用 TensorFlow 的便捷函数加载
Fashion-MNIST。我们将只保留图像,因为标签不需要:(X_train, _), (X_test, _) = fashion_mnist.load_data() -
对图像进行归一化,并使用
np.expand_dims()为其添加单一颜色通道:X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 X_train = np.expand_dims(X_train, axis=-1) X_test = np.expand_dims(X_test, axis=-1) -
生成两个与
X_train和X_test相同维度的张量。它们将对应于随机的0.5:train_noise = np.random.normal(loc=0.5, scale=0.5, size=X_train.shape) test_noise = np.random.normal(loc=0.5, scale=0.5, size=X_test.shape) -
通过分别向
X_train和X_test添加train_noise和test_noise来故意损坏这两个数据集。确保使用np.clip()将值保持在0和1之间:X_train_noisy = np.clip(X_train + train_noise, 0, 1) X_test_noisy = np.clip(X_test + test_noise, 0, 1) -
创建自编码器并编译它。我们将使用
'adam'作为优化器,'mse'作为损失函数,因为我们更关心减少误差,而不是提高准确率:_, _, autoencoder = build_autoencoder(encoding_size=128) autoencoder.compile(optimizer='adam', loss='mse') -
将模型训练
300个周期,每次批量处理1024张噪声图像。注意,特征是噪声图像,而标签或目标是原始图像,即未经损坏的图像:EPOCHS = 300 BATCH_SIZE = 1024 autoencoder.fit(X_train_noisy, X_train, epochs=EPOCHS, batch_size=BATCH_SIZE, shuffle=True, validation_data=(X_test_noisy,X_test)) -
使用训练好的模型进行预测。将噪声图像和生成图像都重新调整为 28x28,并将它们缩放到[0, 255]范围内:
predictions = autoencoder.predict(X_test) original_shape = (X_test_noisy.shape[0], 28, 28) predictions = predictions.reshape(original_shape) X_test_noisy = X_test_noisy.reshape(original_shape) predictions = (predictions * 255.0).astype('uint8') X_test_noisy = (X_test_noisy * 255.0).astype('uint8') -
最后,显示噪声图像与恢复图像的拼图:
plot_original_vs_generated(X_test_noisy, predictions)这是结果:
图 5.3 – 噪声图像(顶部)与网络恢复的图像(底部)拼图
看看顶部的图像有多么受损!好消息是,在大多数情况下,自编码器成功地恢复了它们。然而,它无法正确去除拼图边缘部分的噪声,这表明可以进行更多实验以提高性能(公平地说,这些坏例子即使对人类来说也很难辨别)。
它是如何工作的…
本食谱的新颖之处在于实际应用卷积自编码器。网络和其他构建模块在前两个食谱中已被详细讨论,因此我们将重点关注去噪问题本身。
为了重现实际中损坏图像的场景,我们在Fashion-MNIST数据集的训练集和测试集中加入了大量的高斯噪声。这种噪声被称为“盐和胡椒”,因为损坏后的图像看起来像是洒满了这些调料。
为了教会我们的自编码器图像原本的样子,我们将带噪声的图像作为特征,将原始图像作为目标或标签。这样,经过 300 个 epoch 后,网络学会了一个编码,可以在许多情况下将带盐和胡椒噪声的实例映射到令人满意的恢复版本。
然而,模型并不完美,正如我们在拼图中看到的那样,网络无法恢复网格边缘的图像。这证明了修复损坏图像的困难性。
使用自编码器检测异常值
自编码器的另一个重要应用是异常检测。这个应用场景的理念是,自编码器会对数据集中最常见的类别学习出一个误差非常小的编码,而对于稀有类别(异常值)的重建能力则会误差较大。
基于这个前提,在本教程中,我们将依赖卷积自编码器来检测Fashion-MNIST子集中的异常值。
让我们开始吧!
准备中
要安装OpenCV,请使用以下pip命令:
$> pip install opencv-contrib-python
我们将依赖 TensorFlow 内建的便捷函数来加载Fashion-MNIST数据集。
如何实现…
按照以下步骤完成这个教程:
-
导入所需的包:
import cv2 import numpy as np from sklearn.model_selection import train_test_split from tensorflow.keras import Model from tensorflow.keras.datasets import fashion_mnist as fmnist from tensorflow.keras.layers import * -
设置随机种子以保证可重复性:
SEED = 84 np.random.seed(SEED) -
定义一个函数来构建自编码器架构。这个函数遵循我们在创建卷积自编码器教程中学习的结构,如果你想了解更深入的解释,请回到那个教程。让我们从创建编码器模型开始:
def build_autoencoder(input_shape=(28, 28, 1), encoding_size=96, alpha=0.2): inputs = Input(shape=input_shape) encoder = Conv2D(filters=32, kernel_size=(3, 3), strides=2, padding='same')(inputs) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder = Conv2D(filters=64, kernel_size=(3, 3), strides=2, padding='same')(encoder) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder_output_shape = encoder.shape encoder = Flatten()(encoder) encoder_output = Dense(encoding_size)(encoder) encoder_model = Model(inputs, encoder_output) -
接下来,构建解码器:
decoder_input = Input(shape=(encoding_size,)) target_shape = tuple(encoder_output_shape[1:]) decoder = Dense(np.prod(target_shape))(decoder_input) decoder = Reshape(target_shape)(decoder) decoder = Conv2DTranspose(filters=64, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=32, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=1, kernel_size=(3, 3), padding='same')(decoder) outputs = Activation('sigmoid')(decoder) decoder_model = Model(decoder_input, outputs) -
最后,构建自编码器并返回三个模型:
encoder_model_output = encoder_model(inputs) decoder_model_output = decoder_model(encoder_model_output) autoencoder_model = Model(inputs, decoder_model_output) return encoder_model, decoder_model, autoencoder_model -
然后,定义一个函数来构建一个包含两个类别的数据集,其中一个类别表示异常或离群点。首先选择与这两个类别相关的实例,然后将它们打乱,以打破可能存在的顺序偏差:
def create_anomalous_dataset(features, labels, regular_label, anomaly_label, corruption_proportion=0.01): regular_data_idx = np.where(labels == regular_label)[0] anomalous_data_idx = np.where(labels == anomaly_label)[0] np.random.shuffle(regular_data_idx) np.random.shuffle(anomalous_data_idx) -
接下来,从异常类别中选择与
corruption_proportion成比例的实例。最后,通过将常规实例与离群点合并来创建最终的数据集:num_anomalies = int(len(regular_data_idx) * corruption_proportion) anomalous_data_idx = anomalous_data_idx[:num_anomalies] data = np.vstack([features[regular_data_idx], features[anomalous_data_idx]]) np.random.shuffle(data) return data -
加载
Fashion-MNIST。将训练集和测试集合并为一个数据集:(X_train, y_train), (X_test, y_test) = fmnist.load_data() X = np.vstack([X_train, X_test]) y = np.hstack([y_train, y_test]) -
定义常规标签和异常标签,然后创建异常数据集:
REGULAR_LABEL = 5 # Sandal ANOMALY_LABEL = 0 # T-shirt/top data = create_anomalous_dataset(X, y, REGULAR_LABEL, ANOMALY_LABEL) -
向数据集中添加一个通道维度,进行归一化,并将数据集分为 80%作为训练集,20%作为测试集:
data = np.expand_dims(data, axis=-1) data = data.astype('float32') / 255.0 X_train, X_test = train_test_split(data, train_size=0.8, random_state=SEED) -
构建自编码器并编译它。我们将使用
'adam'作为优化器,'mse'作为损失函数,因为这可以很好地衡量模型的误差:_, _, autoencoder = build_autoencoder(encoding_size=256) autoencoder.compile(optimizer='adam', loss='mse') -
将自编码器训练 300 个 epoch,每次处理
1024张图像:EPOCHS = 300 BATCH_SIZE = 1024 autoencoder.fit(X_train, X_train, epochs=EPOCHS, batch_size=BATCH_SIZE, validation_data=(X_test, X_test)) -
对数据进行预测以找出异常值。我们将计算原始图像与自动编码器生成图像之间的均方误差:
decoded = autoencoder.predict(data) mses = [] for original, generated in zip(data, decoded): mse = np.mean((original - generated) ** 2) mses.append(mse) -
选择误差大于 99.9%分位数的图像索引。这些将是我们的异常值:
threshold = np.quantile(mses, 0.999) outlier_idx = np.where(np.array(mses) >= threshold)[0] print(f'Number of outliers: {len(outlier_idx)}') -
为每个异常值保存原始图像与生成图像的比较图像:
decoded = (decoded * 255.0).astype('uint8') data = (data * 255.0).astype('uint8') for i in outlier_idx: image = np.hstack([data[i].reshape(28, 28), decoded[i].reshape(28, 28)]) cv2.imwrite(f'{i}.jpg', image)这是一个异常值的示例:
图 5.4 – 左:原始异常值。右:重建图像。
正如我们所看到的,我们可以利用自动编码器学习的编码知识轻松检测数据集中的异常或不常见图像。我们将在下一节中更详细地讨论这一点。
它是如何工作的…
本配方背后的思想非常简单:根据定义,异常值是数据集中事件或类别的稀有发生。因此,当我们在包含异常值的数据集上训练自动编码器时,它将没有足够的时间或示例来学习它们的适当表示。
通过利用网络在重建异常图像(在此示例中为 T 恤)时表现出的低置信度(换句话说,高误差),我们可以选择最差的副本来发现异常值。
然而,为了使此技术有效,自动编码器必须擅长重建常规类别(例如,凉鞋);否则,误报率将太高。
使用深度学习创建逆图像搜索索引
因为自动编码器的核心目的是学习图像集合的编码或低维表示,它们是非常优秀的特征提取器。此外,正如我们将在本配方中发现的那样,我们可以将它们作为图像搜索索引的完美构建模块。
准备就绪
让我们使用pip安装OpenCV。我们将用它来可视化自动编码器的输出,从而直观地评估图像搜索索引的有效性:
$> pip install opencv-python
我们将在下一节开始实现这个配方。
如何实现…
按照以下步骤创建您自己的图像搜索索引:
-
导入必要的库:
import cv2 import numpy as np from tensorflow.keras import Model from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.layers import * -
定义
build_autoencoder(),该函数实例化自动编码器。首先,让我们组装编码器部分:def build_autoencoder(input_shape=(28, 28, 1), encoding_size=32, alpha=0.2): inputs = Input(shape=input_shape) encoder = Conv2D(filters=32, kernel_size=(3, 3), strides=2, padding='same')(inputs) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder = Conv2D(filters=64, kernel_size=(3, 3), strides=2, padding='same')(encoder) encoder = LeakyReLU(alpha=alpha)(encoder) encoder = BatchNormalization()(encoder) encoder_output_shape = encoder.shape encoder = Flatten()(encoder) encoder_output = Dense(units=encoding_size, name='encoder_output')(encoder) -
下一步是定义解码器部分:
target_shape = tuple(encoder_output_shape[1:]) decoder = Dense(np.prod(target_shape))(encoder _output) decoder = Reshape(target_shape)(decoder) decoder = Conv2DTranspose(filters=64, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=32, kernel_size=(3, 3), strides=2, padding='same')(decoder) decoder = LeakyReLU(alpha=alpha)(decoder) decoder = BatchNormalization()(decoder) decoder = Conv2DTranspose(filters=1, kernel_size=(3, 3), padding='same')(decoder) outputs = Activation(activation='sigmoid', name='decoder_output')(decoder) -
最后,构建自动编码器并返回它:
autoencoder_model = Model(inputs, outputs) return autoencoder_model -
定义一个计算两个向量之间欧几里得距离的函数:
def euclidean_dist(x, y): return np.linalg.norm(x - y) -
定义
search()函数,该函数使用搜索索引(一个将特征向量与相应图像配对的字典)来检索与查询向量最相似的结果:def search(query_vector, search_index, max_results=16): vectors = search_index['features'] results = [] for i in range(len(vectors)): distance = euclidean_dist(query_vector, vectors[i]) results.append((distance, search_index['images'][i])) results = sorted(results, key=lambda p: p[0])[:max_results] return results -
加载
Fashion-MNIST数据集。仅保留以下图像:(X_train, _), (X_test, _) = fashion_mnist.load_data() -
对图像进行归一化并添加颜色通道维度:
X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 X_train = np.expand_dims(X_train, axis=-1) X_test = np.expand_dims(X_test, axis=-1) -
构建自动编码器并进行编译。我们将使用
'adam'作为优化器,'mse'作为损失函数,因为这样可以很好地衡量模型的误差:autoencoder = build_autoencoder() autoencoder.compile(optimizer='adam', loss='mse') -
训练自动编码器 10 个周期,每次批处理
512张图像:EPOCHS = 50 BATCH_SIZE = 512 autoencoder.fit(X_train, X_train, epochs=EPOCHS, batch_size=BATCH_SIZE, shuffle=True, validation_data=(X_test, X_test)) -
创建一个新模型,我们将用它作为特征提取器。它将接收与自编码器相同的输入,并输出自编码器学到的编码。实质上,我们是使用自编码器的编码器部分将图像转换为向量:
fe_input = autoencoder.input fe_output = autoencoder.get_layer('encoder_output').output feature_extractor = Model(inputs=fe_input, outputs=fe_output) -
创建搜索索引,由
X_train的特征向量和原始图像组成(原始图像必须重新调整为 28x28 并重新缩放到[0, 255]的范围):train_vectors = feature_extractor.predict(X_train) X_train = (X_train * 255.0).astype('uint8') X_train = X_train.reshape((X_train.shape[0], 28, 28)) search_index = { 'features': train_vectors, 'images': X_train } -
计算
X_test的特征向量,我们将把它用作查询图像的样本。并将X_test调整为 28x28 的形状,并将其值重新缩放到[0, 255]的范围:test_vectors = feature_extractor.predict(X_test) X_test = (X_test * 255.0).astype('uint8') X_test = X_test.reshape((X_test.shape[0], 28, 28)) -
选择 16 个随机测试图像(以及其对应的特征向量)作为查询:
sample_indices = np.random.randint(0, X_test.shape[0],16) sample_images = X_test[sample_indices] sample_queries = test_vectors[sample_indices] -
对测试样本中的每个图像进行搜索,并保存查询图像与从索引中提取的结果之间的并排视觉对比(记住,索引是由训练数据组成的):
for i, (vector, image) in \ enumerate(zip(sample_queries, sample_images)): results = search(vector, search_index) results = [r[1] for r in results] query_image = cv2.resize(image, (28 * 4, 28 * 4), interpolation=cv2.INTER_AREA) results_mosaic = np.vstack([np.hstack(results[0:4]), np.hstack(results[4:8]), np.hstack(results[8:12]), np.hstack(results[12:16])]) result_image = np.hstack([query_image, results_mosaic]) cv2.imwrite(f'{i}.jpg', result_image)下面是一个搜索结果的示例:
图 5.5 – 左:鞋子的查询图像。右:最佳的 16 个搜索结果,所有结果也都包含鞋子
正如前面的图像所示,我们的图像搜索索引是成功的!我们将在下一部分看到它是如何工作的。
它是如何工作的…
在本食谱中,我们学习了如何利用自编码器的独特特征——学习一个大大压缩输入图像信息的编码,从而实现最小的信息损失。然后,我们使用卷积自编码器的编码器部分提取时尚物品照片的特征,并构建了一个图像搜索索引。
通过这样做,使用这个索引作为搜索引擎就像计算查询向量(对应于查询图像)与索引中所有图像之间的欧几里得距离一样简单,只选择那些最接近查询的图像。
我们解决方案中最重要的方面是训练一个足够优秀的自编码器,以生成高质量的向量,因为它们决定了搜索引擎的成败。
另见
该实现基于 Dong 等人的出色工作,论文可在此阅读:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch5/recipe5。
实现变分自编码器
自编码器的一些最现代且复杂的应用场景是变分自编码器(VAE)。它们与其他自编码器的不同之处在于,变分自编码器并不是学习一个任意的函数,而是学习输入图像的概率分布。我们可以从这个分布中采样,以生成新的、未见过的数据点。
VAE实际上是一个生成模型,在这个食谱中,我们将实现一个。
准备就绪
我们不需要为这个食谱做任何特别的准备,所以让我们立即开始吧!
如何操作…
按照以下步骤学习如何实现和训练VAE:
-
导入必要的包:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from tensorflow.keras import Model from tensorflow.keras import backend as K from tensorflow.keras.datasets import fashion_mnist from tensorflow.keras.layers import * from tensorflow.keras.losses import mse from tensorflow.keras.optimizers import Adam -
因为我们很快会使用
tf.function注解,所以我们必须告诉 TensorFlow 以急切执行(eager execution)的方式运行函数:tf.config.experimental_run_functions_eagerly(True) -
定义一个类,封装我们实现
self.z_log_var和self.z_mean的功能,它们是我们将学习的潜在高斯分布的参数:self.z_log_var = None self.z_mean = None -
定义一些成员变量,用于存储
encoder、decoder和vae的输入和输出:self.inputs = None self.outputs = None self.encoder = None self.decoder = None self.vae = None -
定义
build_vae()方法,该方法构建变分自编码器架构(请注意,我们使用的是全连接层而不是卷积层):def build_vae(self): self.inputs = Input(shape=(self.original_dimension,)) x = Dense(self.encoding_dimension)(self.inputs) x = ReLU()(x) self.z_mean = Dense(self.latent_dimension)(x) self.z_log_var = Dense(self.latent_dimension)(x) z = Lambda(sampling)([self.z_mean, self.z_log_var]) self.encoder = Model(self.inputs, [self.z_mean, self.z_log_var, z])请注意,编码器只是一个完全连接的网络,它产生三个输出:
self.z_mean,这是我们训练建模的高斯分布的均值;self.z_log_var,这是该分布的对数方差;以及z,这是该概率空间中的一个样本点。为了简单地生成z,我们必须在Lambda层中包装一个自定义函数sampling()(在第 5 步中实现)。 -
接下来,定义解码器:
latent_inputs = Input(shape=(self.latent_dimension,)) x = Dense(self.encoding_dimension)(latent_inputs) x = ReLU()(x) self.outputs = Dense(self.original_dimension)(x) self.outputs = Activation('sigmoid')(self.outputs) self.decoder = Model(latent_inputs, self.outputs) -
解码器只是另一个完全连接的网络。解码器将从潜在维度中取样,以重构输入。最后,将编码器和解码器连接起来,创建VAE模型:
self.outputs = self.encoder(self.inputs)[2] self.outputs = self.decoder(self.outputs) self.vae = Model(self.inputs, self.outputs) -
定义
train()方法,该方法训练变分自编码器。因此,它接收训练和测试数据,以及迭代次数和批次大小:@tf.function def train(self, X_train, X_test, epochs=50, batch_size=64): -
将重建损失定义为输入和输出之间的均方误差(MSE):
reconstruction_loss = mse(self.inputs, self.outputs) reconstruction_loss *= self.original_dimensionkl_loss是reconstruction_loss:kl_loss = (1 + self.z_log_var - K.square(self.z_mean) - K.exp(self.z_log_var)) kl_loss = K.sum(kl_loss, axis=-1) kl_loss *= -0.5 vae_loss = K.mean(reconstruction_loss + kl_loss) -
配置
self.vae模型,使其使用vae_loss和Adam()作为优化器(学习率为 0.003)。然后,在指定的迭代次数内拟合网络。最后,返回三个模型:self.vae.add_loss(vae_loss) self.vae.compile(optimizer=Adam(lr=1e-3)) self.vae.fit(X_train, epochs=epochs, batch_size=batch_size, validation_data=(X_test, None)) return self.encoder, self.decoder, self.vae -
定义一个函数,该函数将在给定两个相关参数(通过
arguments数组传递)时生成潜在空间中的随机样本或点;即,z_mean和z_log_var:def sampling(arguments): z_mean, z_log_var = arguments batch = K.shape(z_mean)[0] dimension = K.int_shape(z_mean)[1] epsilon = K.random_normal(shape=(batch, dimension)) return z_mean + K.exp(0.5 * z_log_var) * epsilon请注意,
epsilon是一个随机的高斯向量。 -
定义一个函数,该函数将生成并绘制从潜在空间生成的图像。这将帮助我们了解靠近分布的形状,以及接近曲线尾部的形状:
def generate_and_plot(decoder, grid_size=5): cell_size = 28 figure_shape = (grid_size * cell_size, grid_size * cell_size) figure = np.zeros(figure_shape) -
创建一个值的范围,X 轴和 Y 轴的值从-4 到 4。我们将使用这些值在每个位置生成和可视化样本:
grid_x = np.linspace(-4, 4, grid_size) grid_y = np.linspace(-4, 4, grid_size)[::-1] -
使用解码器为每个
z_mean和z_log_var的组合生成新的样本:for i, z_log_var in enumerate(grid_y): for j, z_mean in enumerate(grid_x): z_sample = np.array([[z_mean, z_log_var]]) generated = decoder.predict(z_sample)[0] -
重塑样本,并将其放置在网格中的相应单元格中:
fashion_item = generated.reshape(cell_size, cell_size) y_slice = slice(i * cell_size, (i + 1) * cell_size) x_slice = slice(j * cell_size, (j + 1) * cell_size) figure[y_slice, x_slice] = fashion_item -
添加刻度和坐标轴标签,然后显示图形:
plt.figure(figsize=(10, 10)) start = cell_size // 2 end = (grid_size - 2) * cell_size + start + 1 pixel_range = np.arange(start, end, cell_size) sample_range_x = np.round(grid_x, 1) sample_range_y = np.round(grid_y, 1) plt.xticks(pixel_range, sample_range_x) plt.yticks(pixel_range, sample_range_y) plt.xlabel('z_mean') plt.ylabel('z_log_var') plt.imshow(figure) plt.show() -
加载
Fashion-MNIST数据集。对图像进行归一化,并添加颜色通道:(X_train, _), (X_test, _) = fashion_mnist.load_data() X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 X_train = X_train.reshape((X_train.shape[0], -1)) X_test = X_test.reshape((X_test.shape[0], -1)) -
实例化并构建变分自编码器:
vae = VAE(original_dimension=784, encoding_dimension=512, latent_dimension=2) vae.build_vae() -
训练模型 100 个周期:
_, decoder_model, vae_model = vae.train(X_train, X_test, epochs=100) -
使用解码器生成新图像并绘制结果:
generate_and_plot(decoder_model, grid_size=7)这是结果:
图 5.6 – VAE 学习的潜在空间可视化
在这里,我们可以看到构成潜在空间的点集,以及这些点对应的服装项目。 这是网络学习的概率分布的一个表示,其中分布中心的项目类似于 T 恤,而边缘的项目则更像裤子、毛衣和鞋子。
让我们继续进入下一节。
它是如何工作的……
在这个示例中,我们了解到,变分自编码器是一种更先进、更复杂的自编码器,它不像学习一个任意的、简单的函数来将输入映射到输出,而是学习输入的概率分布。这样,它就能够生成新的、未见过的图像,成为更现代生成模型的前驱,例如生成对抗网络(GANs)。
这个架构与我们在本章中学习的其他自编码器并没有太大不同。理解z的关键在于,我们通过sampling()函数在 Lambda 层中生成z。
这意味着,在每次迭代中,整个网络都在优化z_mean和z_log_var参数,使其与输入的概率分布尽可能接近。这样做是因为,只有这样,随机样本(z)的质量才足够高,解码器才能生成更好、更逼真的输出。
另见
我们可以用来调节VAE的一个关键组件是Kullback-Leibler散度,您可以在这里阅读更多内容:en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence。
请注意,VAE是生成模型的完美开端,我们将在下一章深入讨论它!