tf20-cv-cb-merge-4

46 阅读37分钟

TensorFlow 2.0 计算机视觉秘籍(五)

原文:annas-archive.org/md5/cf3ce16c27a13f4ce55f8e29a1bf85e1

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:第十章:将深度学习的力量应用到视频中

计算机视觉关注的是视觉数据的理解。当然,这也包括视频,视频本质上是图像的序列,这意味着我们可以利用我们关于图像处理的深度学习知识,应用到视频中并获得很好的结果。

在本章中,我们将开始训练卷积神经网络,检测人脸中的情感,然后学习如何在实时上下文中使用我们的摄像头应用它。

然后,在接下来的食谱中,我们将使用TensorFlow HubTFHub)托管的非常先进的架构,专门用于解决与视频相关的有趣问题,如动作识别、帧生成和文本到视频的检索。

这里是我们将要覆盖的食谱内容:

  • 实时检测情感

  • 使用 TensorFlow Hub 识别动作

  • 使用 TensorFlow Hub 生成视频的中间帧

  • 使用 TensorFlow Hub 进行文本到视频的检索

技术要求

和往常一样,拥有 GPU 是一个很大的优势,特别是在第一个食谱中,我们将从零开始实现一个网络。因为本章剩余部分利用了 TFHub 中的模型,所以即使是 CPU 也应该足够,尽管 GPU 能显著提高速度!在准备就绪部分,你可以找到每个食谱的准备步骤。你可以在这里找到本章的代码:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch10

查看以下链接,观看代码实际演示视频:

bit.ly/3qkTJ2l

实时检测情感

从最基本的形式来看,视频仅仅是图像序列。通过利用这一看似简单或微不足道的事实,我们可以将图像分类的知识应用到视频处理上,从而创建出由深度学习驱动的非常有趣的视频处理管道。

在本食谱中,我们将构建一个算法,实时检测情感(来自摄像头流或视频文件)。非常有趣,对吧?

让我们开始吧。

准备就绪

首先,我们需要安装一些外部库,如OpenCVimutils。执行以下命令安装它们:

$> pip install opencv-contrib-python imutils

为了训练情感分类器网络,我们将使用来自 Kaggle 比赛的数据集(~/.keras/datasets文件夹),将其提取为emotion_recognition,然后解压fer2013.tar.gz文件。

这里是一些示例图像:

图 10.1 – 示例图像。情感从左到右:悲伤、生气、害怕、惊讶、开心和中立

图 10.1 – 示例图像。情感从左到右:悲伤、生气、害怕、惊讶、开心和中立

让我们开始吧!

如何实现……

在本食谱结束时,你将拥有自己的情感检测器!

  1. 导入所有依赖项:

    import csv
    import glob
    import pathlib
    import cv2
    import imutils
    import numpy as np
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    from tensorflow.keras.optimizers import Adam
    from tensorflow.keras.preprocessing.image import *
    from tensorflow.keras.utils import to_categorical
    
  2. 定义数据集中所有可能情感的列表,并为每个情感指定一个颜色:

    EMOTIONS = ['angry', 'scared', 'happy', 'sad', 
              'surprised','neutral']
    COLORS = {'angry': (0, 0, 255),
        'scared': (0, 128, 255),
        'happy': (0, 255, 255),
        'sad': (255, 0, 0),
        'surprised': (178, 255, 102),
        'neutral': (160, 160, 160)
    }
    
  3. 定义一个方法来构建情感分类器的架构。它接收输入形状和数据集中的类别数量:

    def build_network(input_shape, classes):
        input = Input(shape=input_shape)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same',
                   kernel_initializer='he_normal')(input)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x) 
    
  4. 网络中的每个块由两个 ELU 激活、批量归一化的卷积层组成,接着是一个最大池化层,最后是一个丢弃层。前面定义的块每个卷积层有 32 个滤波器,而后面的块每个卷积层有 64 个滤波器:

        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  5. 第三个块每个卷积层有 128 个滤波器:

        x = Conv2D(filters=128,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=128,
                   kernel_size=(3, 3),
                   kernel_initializer='he_normal',
                   padding='same')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    
  6. 接下来,我们有两个密集层,ELU 激活、批量归一化,后面也跟着一个丢弃层,每个层有 64 个单元:

        x = Flatten()(x)
        x = Dense(units=64,
                  kernel_initializer='he_normal')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
        x = Dense(units=64,
                  kernel_initializer='he_normal')(x)
        x = ELU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.5)(x)
    
  7. 最后,我们遇到输出层,神经元数量与数据集中的类别数量相同,当然,采用 softmax 激活函数:

        x = Dense(units=classes,
                  kernel_initializer='he_normal')(x)
        output = Softmax()(x)
        return Model(input, output)
    
  8. load_dataset()加载训练集、验证集和测试集的图像和标签:

    def load_dataset(dataset_path, classes):
        train_images = []
        train_labels = []
        val_images = []
        val_labels = []
        test_images = []
        test_labels = []
    
  9. 这个数据集中的数据存储在一个 CSV 文件中,分为emotionpixelsUsage三列。我们首先解析emotion列。尽管数据集包含七类面部表情,我们将厌恶愤怒(分别编码为01)合并,因为它们共享大多数面部特征,合并后会得到更好的结果:

        with open(dataset_path, 'r') as f:
            reader = csv.DictReader(f)
            for line in reader:
                label = int(line['emotion'])
                if label <= 1:
                  label = 0  # This merges classes 1 and 0.
                if label > 0:
                  label -= 1  # All classes start from 0.
    
  10. 接下来,我们解析pixels列,它包含 2,034 个空格分隔的整数,代表图像的灰度像素(48x48=2034):

                image = np.array(line['pixels'].split
                                        (' '),
                                 dtype='uint8')
                image = image.reshape((48, 48))
                image = img_to_array(image)
    
  11. 现在,为了弄清楚这张图像和标签属于哪个子集,我们需要查看Usage列:

                if line['Usage'] == 'Training':
                    train_images.append(image)
                    train_labels.append(label)
                elif line['Usage'] == 'PrivateTest':
                    val_images.append(image)
                    val_labels.append(label)
                else:
                    test_images.append(image)
                    test_labels.append(label)
    
  12. 将所有的图像转换为 NumPy 数组:

        train_images = np.array(train_images)
        val_images = np.array(val_images)
        test_images = np.array(test_images)
    
  13. 然后,对所有标签进行独热编码:

        train_labels = 
        to_categorical(np.array(train_labels),
                                      classes)
        val_labels = to_categorical(np.array(val_labels), 
                                     classes)
        test_labels = to_categorical(np.array(test_labels),
                                     classes)
    
  14. 返回所有的图像和标签:

        return (train_images, train_labels), \
               (val_images, val_labels), \
               (test_images, test_labels)
    
  15. 定义一个计算矩形区域面积的函数。稍后我们将用它来获取最大的面部检测结果:

    def rectangle_area(r):
        return (r[2] - r[0]) * (r[3] - r[1])
    
  16. 现在,我们将创建一个条形图来显示每一帧中检测到的情感的概率分布。以下函数用于绘制每个条形图,代表某一特定情感:

    def plot_emotion(emotions_plot, emotion, probability, 
                     index):
        w = int(probability * emotions_plot.shape[1])
        cv2.rectangle(emotions_plot,
                      (5, (index * 35) + 5),
                      (w, (index * 35) + 35),
                      color=COLORS[emotion],
                      thickness=-1)
        white = (255, 255, 255)
        text = f'{emotion}: {probability * 100:.2f}%'
        cv2.putText(emotions_plot,
                    text,
                    (10, (index * 35) + 23),
                    fontFace=cv2.FONT_HERSHEY_COMPLEX,
                    fontScale=0.45,
                    color=white,
                    thickness=2)
        return emotions_plot
    
  17. 我们还会在检测到的面部周围画一个边界框,并标注上识别出的情感:

    def plot_face(image, emotion, detection):
        frame_x, frame_y, frame_width, frame_height = detection
        cv2.rectangle(image,
                      (frame_x, frame_y),
                      (frame_x + frame_width,
                       frame_y + frame_height),
                      color=COLORS[emotion],
                      thickness=2)
        cv2.putText(image,
                    emotion,
                    (frame_x, frame_y - 10),
                    fontFace=cv2.FONT_HERSHEY_COMPLEX,
                    fontScale=0.45,
                    color=COLORS[emotion],
                    thickness=2)
        return image
    
  18. 定义predict_emotion()函数,该函数接收情感分类器和输入图像,并返回模型输出的预测结果:

    def predict_emotion(model, roi):
        roi = cv2.resize(roi, (48, 48))
        roi = roi.astype('float') / 255.0
        roi = img_to_array(roi)
        roi = np.expand_dims(roi, axis=0)
        predictions = model.predict(roi)[0]
        return predictions
    
  19. 如果有保存的模型,则加载它:

    checkpoints = sorted(list(glob.glob('./*.h5')), reverse=True)
    if len(checkpoints) > 0:
        model = load_model(checkpoints[0])
    
  20. 否则,从头开始训练模型。首先,构建 CSV 文件的路径,然后计算数据集中的类别数量:

    else:
        base_path = (pathlib.Path.home() / '.keras' / 
                     'datasets' /
                     'emotion_recognition' / 'fer2013')
        input_path = str(base_path / 'fer2013.csv')
        classes = len(EMOTIONS)
    
  21. 然后,加载每个数据子集:

        (train_images, train_labels), \
        (val_images, val_labels), \
        (test_images, test_labels) = load_dataset(input_path,
                                                  classes)
    
  22. 构建网络并编译它。同时,定义一个ModelCheckpoint回调函数来保存最佳表现的模型(基于验证损失):

        model = build_network((48, 48, 1), classes)
        model.compile(loss='categorical_crossentropy',
                      optimizer=Adam(lr=0.003),
                      metrics=['accuracy'])
        checkpoint_pattern = ('model-ep{epoch:03d}-
                              loss{loss:.3f}'
                              '-val_loss{val_loss:.3f}.h5')
        checkpoint = ModelCheckpoint(checkpoint_pattern,
                                     monitor='val_loss',
                                     verbose=1,
                                     save_best_only=True,
                                     mode='min')
    
  23. 定义训练集和验证集的增强器和生成器。注意,我们仅增强训练集,而验证集中的图像只是进行重缩放:

        BATCH_SIZE = 128
        train_augmenter = ImageDataGenerator(rotation_
                                range=10,zoom_range=0.1,
                                  horizontal_flip=True,
                                        rescale=1\. / 255.,
                                    fill_mode='nearest')
        train_gen = train_augmenter.flow(train_images,
                                         train_labels,
                                     batch_size=BATCH_SIZE)
        train_steps = len(train_images) // BATCH_SIZE
        val_augmenter = ImageDataGenerator(rescale=1\. / 255.)
        val_gen = val_augmenter.flow(val_images,val_labels,
                             batch_size=BATCH_SIZE)
    
  24. 训练模型 300 个周期,然后在测试集上评估模型(我们只对该子集中的图像进行重缩放):

        EPOCHS = 300
        model.fit(train_gen,
                  steps_per_epoch=train_steps,
                  validation_data=val_gen,
                  epochs=EPOCHS,
                  verbose=1,
                  callbacks=[checkpoint])
       test_augmenter = ImageDataGenerator(rescale=1\. / 255.)
        test_gen = test_augmenter.flow(test_images,
                                       test_labels,
                                       batch_size=BATCH_SIZE)
        test_steps = len(test_images) // BATCH_SIZE
        _, accuracy = model.evaluate(test_gen, 
                                     steps=test_steps)
        print(f'Accuracy: {accuracy * 100}%')
    
  25. 实例化一个cv2.VideoCapture()对象来获取测试视频中的帧。如果你想使用你的网络摄像头,将video_path替换为0

    video_path = 'emotions.mp4'
    camera = cv2.VideoCapture(video_path)  # Pass 0 to use webcam
    
  26. 创建一个Haar 级联人脸检测器(这是本书范围之外的内容。如果你想了解更多关于 Haar 级联的内容,请参考本配方中的另见部分):

    cascade_file = 'resources/haarcascade_frontalface_default.xml'
    det = cv2.CascadeClassifier(cascade_file)
    
  27. 遍历视频中的每一帧(或网络摄像头流),只有在没有更多帧可以读取,或用户按下 Q 键时才退出:

    while True:
        frame_exists, frame = camera.read()
        if not frame_exists:
            break
    
  28. 将帧调整为宽度为 380 像素(高度会自动计算以保持宽高比)。同时,创建一个画布,用于绘制情感条形图,并创建一个输入帧的副本,用于绘制检测到的人脸:

        frame = imutils.resize(frame, width=380)
        emotions_plot = np.zeros_like(frame, 
                                      dtype='uint8')
        copy = frame.copy()
    
  29. 由于 Haar 级联方法是在灰度图像上工作的,我们必须将输入帧转换为黑白图像。然后,我们在其上运行人脸检测器:

        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        detections = \
            det.detectMultiScale(gray,scaleFactor=1.1,
                                 minNeighbors=5,
                                 minSize=(35, 35),
    
                            flags=cv2.CASCADE_SCALE_IMAGE)
    
  30. 验证是否有任何检测,并获取面积最大的那个:

        if len(detections) > 0:
            detections = sorted(detections,
                                key=rectangle_area)
            best_detection = detections[-1]
    
  31. 提取与检测到的面部表情对应的感兴趣区域(roi),并从中提取情感:

            (frame_x, frame_y,
             frame_width, frame_height) = best_detection
            roi = gray[frame_y:frame_y + frame_height,
                       frame_x:frame_x + frame_width]
            predictions = predict_emotion(model, roi)
            label = EMOTIONS[predictions.argmax()]
    
  32. 创建情感分布图:

            for i, (emotion, probability) in \
                    enumerate(zip(EMOTIONS, predictions)):
                emotions_plot = plot_emotion(emotions_plot,
                                             emotion,
                                             probability,
                                             i)
    
  33. 绘制检测到的面部表情及其所展示的情感:

            clone = plot_face(copy, label, best_detection)
    
  34. 显示结果:

        cv2.imshow('Face & emotions',
                   np.hstack([copy, emotions_plot]))
    
  35. 检查用户是否按下了 Q 键,如果按下了,则退出循环:

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
  36. 最后,释放资源:

    camera.release()
    cv2.destroyAllWindows()
    

    在 300 个周期后,我获得了 65.74%的测试准确率。在这里,你可以看到一些测试视频中检测到的情感快照:

图 10.2 – 在两个不同快照中检测到的情感

图 10.2 – 在两个不同快照中检测到的情感

我们可以看到网络正确地识别出了顶部帧中的悲伤表情,底部帧中识别出了幸福表情。让我们来看一个另一个例子:

图 10.3 – 在三个不同快照中检测到的情感

图 10.3 – 在三个不同快照中检测到的情感

在第一帧中,女孩显然呈现出中性表情,网络正确地识别出来了。第二帧中,她的面部表情显示出愤怒,分类器也检测到这一点。第三帧更有趣,因为她的表情显示出惊讶,但也可以被解读为恐惧。我们的检测器似乎在这两种情感之间有所犹豫。

让我们前往下一部分,好吗?

它是如何工作的……

在本配方中,我们实现了一个相当强大的情感检测器,用于视频流,无论是来自内建的网络摄像头,还是存储的视频文件。我们首先解析了FER 2013数据集,它与大多数其他图像数据集不同,是 CSV 格式的。然后,我们在其图像上训练了一个情感分类器,在测试集上达到了 65.74%的准确率。

我们必须考虑到面部表情的解读非常复杂,甚至对于人类来说也是如此。在某一时刻,我们可能会展示混合情感。此外,还有许多表情具有相似特征,比如愤怒厌恶,以及恐惧惊讶,等等。

本食谱中的最后一步是将输入视频流中的每一帧传递给 Haar Cascade 人脸检测器,然后使用训练好的分类器从检测到的人脸区域获取情感。

尽管这种方法对这个特定问题有效,但我们必须考虑到我们忽略了一个关键假设:每一帧都是独立的。简单来说,我们将视频中的每一帧当作一个独立的图像处理,但实际上,处理视频时并非如此,因为存在时间维度,如果考虑到这一点,将会得到更稳定、更好的结果。

另请参见

这是一个很好的资源,用于理解 Haar Cascade 分类器:docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html

使用 TensorFlow Hub 识别动作

深度学习在视频处理中的一个非常有趣的应用是动作识别。这是一个具有挑战性的问题,因为它不仅涉及到图像分类中通常遇到的困难,还包括了时间维度。视频中的一个动作可能会根据帧呈现的顺序而有所不同。

好消息是,存在一个非常适合这种问题的架构,称为膨胀 3D 卷积网络I3D),在本食谱中,我们将使用 TFHub 上托管的训练版本来识别一组多样化视频中的动作!

开始吧。

准备工作

我们需要安装几个补充库,如OpenCVTFHubimageio。执行以下命令:

$> pip install opencv-contrib-python tensorflow-hub imageio

就是这样!让我们开始实现吧。

如何做…

执行以下步骤以完成本食谱:

  1. 导入所有所需的依赖项:

    import os
    import random
    import re
    import ssl
    import tempfile
    from urllib import request
    import cv2
    import imageio
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow_docs.vis import embed
    
  2. 定义UCF101 – 动作识别数据集的路径,从中获取我们稍后将传递给模型的测试视频:

    UCF_ROOT = 'https://www.crcv.ucf.edu/THUMOS14/UCF101/UCF101/'
    
  3. 定义Kinetics数据集的标签文件路径,后者用于训练我们将很快使用的 3D 卷积网络:

    KINETICS_URL = ('https://raw.githubusercontent.com/deepmind/'
                    'kinetics-i3d/master/data/label_map.txt')
    
  4. 创建一个临时目录,用于缓存下载的资源:

    CACHE_DIR = tempfile.mkdtemp()
    
  5. 创建一个未经验证的 SSL 上下文。我们需要这个以便能够从 UCF 的网站下载数据(在编写本书时,似乎他们的证书已过期):

    UNVERIFIED_CONTEXT = ssl._create_unverified_context()
    
  6. 定义fetch_ucf_videos()函数,该函数下载我们将从中选择的测试视频列表,以测试我们的动作识别器:

    def fetch_ucf_videos():
        index = \
            (request
             .urlopen(UCF_ROOT, 
                      context=UNVERIFIED_CONTEXT)
             .read()
             .decode('utf-8'))
        videos = re.findall('(v_[\w]+\.avi)', index)
        return sorted(set(videos))
    
  7. 定义fetch_kinetics_labels()函数,用于下载并解析Kinetics数据集的标签:

    def fetch_kinetics_labels():
        with request.urlopen(KINETICS_URL) as f:
            labels = [line.decode('utf-8').strip()
                      for line in f.readlines()]
        return labels
    
  8. 定义fetch_random_video()函数,该函数从我们的UCF101视频列表中选择一个随机视频,并将其下载到第 4 步中创建的临时目录中:

    def fetch_random_video(videos_list):
        video_name = random.choice(videos_list)
        cache_path = os.path.join(CACHE_DIR, video_name)
        if not os.path.exists(cache_path):
            url = request.urljoin(UCF_ROOT, video_name)
            response = (request
                        .urlopen(url,
    
                         context=UNVERIFIED_CONTEXT)
                        .read())
            with open(cache_path, 'wb') as f:
                f.write(response)
        return cache_path
    
  9. 定义crop_center()函数,该函数接受一张图片并裁剪出对应于接收帧中心的正方形区域:

    def crop_center(frame):
        height, width = frame.shape[:2]
        smallest_dimension = min(width, height)
        x_start = (width // 2) - (smallest_dimension // 2)
        x_end = x_start + smallest_dimension
        y_start = (height // 2) - (smallest_dimension // 2)
        y_end = y_start + smallest_dimension
        roi = frame[y_start:y_end, x_start:x_end]
        return roi
    
  10. 定义 read_video() 函数,它从我们的缓存中读取最多 max_frames 帧,并返回所有读取的帧列表。它还会裁剪每帧的中心,将其调整为 224x224x3 的大小(网络期望的输入形状),并进行归一化处理:

    def read_video(path, max_frames=32, resize=(224, 224)):
        capture = cv2.VideoCapture(path)
        frames = []
        while len(frames) <= max_frames:
            frame_read, frame = capture.read()
            if not frame_read:
                break
            frame = crop_center(frame)
            frame = cv2.resize(frame, resize)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frames.append(frame)
        capture.release()
        frames = np.array(frames)
        return frames / 255.
    
  11. 定义 predict() 函数,用于获取模型在输入视频中识别的前五个最可能的动作:

    def predict(model, labels, sample_video):
        model_input = tf.constant(sample_video,
                                  dtype=tf.float32)
        model_input = model_input[tf.newaxis, ...]
        logits = model(model_input)['default'][0]
        probabilities = tf.nn.softmax(logits)
        print('Top 5 actions:')
        for i in np.argsort(probabilities)[::-1][:5]:
            print(f'{labels[i]}:  {probabilities[i] * 100:5.2f}%')
    
  12. 定义 save_as_gif() 函数,它接收一个包含视频帧的列表,并用它们创建 GIF 格式的表示:

    def save_as_gif(images, video_name):
        converted_images = np.clip(images * 255, 0, 255)
        converted_images = converted_images.astype(np.uint8)
        imageio.mimsave(f'./{video_name}.gif',
                        converted_images,
                        fps=25)
    
  13. 获取视频和标签:

    VIDEO_LIST = fetch_ucf_videos()
    LABELS = fetch_kinetics_labels()
    
  14. 获取一个随机视频并读取其帧:

    video_path = fetch_random_video(VIDEO_LIST)
    sample_video = read_video(video_path)
    
  15. 从 TFHub 加载 I3D:

    model_path = 'https://tfhub.dev/deepmind/i3d-kinetics-400/1'
    model = tfhub.load(model_path)
    model = model.signatures['default']
    
  16. 最后,将视频传递给网络以获得预测结果,然后将视频保存为 GIF 格式:

    predict(model, LABELS, sample_video)
    video_name = video_path.rsplit('/', maxsplit=1)[1][:-4]
    save_as_gif(sample_video, video_name)
    

    这是我获得的随机视频的第一帧:

图 10.4 – 随机 UCF101 视频的帧

图 10.4 – 随机 UCF101 视频的帧

这是模型生成的前五个预测:

Top 5 actions:
mopping floor:  75.29%
cleaning floor:  21.11%
sanding floor:   0.85%
spraying:   0.69%
sweeping floor:   0.64%

看起来网络理解视频中呈现的动作与地板有关,因为五个预测中有四个与此相关。然而,mopping floor才是正确的预测。

现在让我们进入 它是如何工作的…… 部分。

它是如何工作的……

在这个方案中,我们利用了 3D 卷积网络的强大功能来识别视频中的动作。顾名思义,3D 卷积是二维卷积的自然扩展,它可以在两个方向上进行操作。3D 卷积不仅考虑了宽度和高度,还考虑了深度,因此它非常适合某些特殊类型的图像,如磁共振成像(MRI),或者在本例中是视频,视频实际上就是一系列叠加在一起的图像。

我们首先从 UCF101 数据集中获取了一系列视频,并从 Kinetics 数据集中获取了一组动作标签。需要记住的是,我们从 TFHub 下载的 I3D 是在 Kinetics 数据集上训练的。因此,我们传递给它的视频是未见过的。

接下来,我们实现了一系列辅助函数,用于获取、预处理并调整每个输入视频的格式,以符合 I3D 的预期。然后,我们从 TFHub 加载了上述网络,并用它来显示视频中识别到的前五个动作。

你可以对这个解决方案进行一个有趣的扩展,即从文件系统中读取自定义视频,或者更好的是,将来自摄像头的图像流传递给网络,看看它的表现如何!

另请参见

I3D 是一种用于视频处理的突破性架构,因此我强烈建议你阅读原始论文:arxiv.org/abs/1705.07750。这里有一篇相当有趣的文章,解释了 1D、2D 和 3D 卷积的区别:towardsdatascience.com/understanding-1d-and-3d-convolution-neural-network-keras-9d8f76e29610。你可以在这里了解更多关于UCF101数据集的信息:www.crcv.ucf.edu/data/UCF101… I3D 实现的更多细节:tfhub.dev/deepmind/i3d-kinetics-400/1

使用 TensorFlow Hub 生成视频的中间帧

深度学习在视频中的另一个有趣应用涉及帧生成。这个技术的一个有趣且实用的例子是慢动作,其中一个网络根据上下文决定如何创建插入帧,从而扩展视频长度,并制造出用高速摄像机拍摄的假象(如果你想了解更多内容,可以参考*另见…*部分)。

在这个食谱中,我们将使用 3D 卷积网络来生成视频的中间帧,给定视频的第一帧和最后一帧。

为此,我们将依赖 TFHub。

让我们开始这个食谱。

准备就绪

我们必须安装 TFHub 和 TensorFlow Datasets

$> pip install tensorflow-hub tensorflow-datasets

我们将使用的模型是在 BAIR Robot Pushing Videos 数据集上训练的,该数据集可在 TensorFlow Datasets 中获得。然而,如果我们通过库访问它,我们将下载远超过我们这个食谱所需的数据。因此,我们将使用测试集的一个较小子集。执行以下命令来下载它并将其放入 ~/.keras/datasets/bair_robot_pushing 文件夹中:

$> wget -nv https://storage.googleapis.com/download.tensorflow.org/data/bair_test_traj_0_to_255.tfrecords -O ~/.keras/datasets/bair_robot_pushing/traj_0_to_255.tfrecords

现在一切准备就绪!让我们开始实施。

如何实现…

执行以下步骤,学习如何通过托管在 TFHub 中的模型生成中间帧,使用 直接 3D 卷积

  1. 导入依赖库:

    import pathlib
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow_datasets.core import SplitGenerator
    from tensorflow_datasets.video.bair_robot_pushing import \
        BairRobotPushingSmall
    
  2. 定义 plot_first_and_last_for_sample() 函数,该函数绘制四个视频样本的第一帧和最后一帧的图像:

    def plot_first_and_last_for_sample(frames, batch_size):
        for i in range(4):
            plt.subplot(batch_size, 2, 1 + 2 * i)
            plt.imshow(frames[i, 0] / 255.)
            plt.title(f'Video {i}: first frame')
            plt.axis('off')
            plt.subplot(batch_size, 2, 2 + 2 * i)
            plt.imshow(frames[i, 1] / 255.)
            plt.title(f'Video {i}: last frame')
            plt.axis('off')
    
  3. 定义 plot_generated_frames_for_sample() 函数,该函数绘制为四个视频样本生成的中间帧:

    def plot_generated_frames_for_sample(gen_videos):
        for video_id in range(4):
            fig = plt.figure(figsize=(10 * 2, 2))
            for frame_id in range(1, 16):
                ax = fig.add_axes(
                    [frame_id / 16., 0, (frame_id + 1) / 
                           16., 1],
                    xmargin=0, ymargin=0)
                ax.imshow(gen_videos[video_id, frame_id])
                ax.axis('off')
    
  4. 我们需要修补 BarRobotPushingSmall()(参见步骤 6)数据集构建器,只期望测试集可用,而不是同时包含训练集和测试集。因此,我们必须创建一个自定义的 SplitGenerator()

    def split_gen_func(data_path):
        return [SplitGenerator(name='test',
                               gen_kwargs={'filedir': 
                                           data_path})]
    
  5. 定义数据路径:

    DATA_PATH = str(pathlib.Path.home() / '.keras' / 
                       'datasets' /
                    'bair_robot_pushing')
    
  6. 创建一个 BarRobotPushingSmall() 构建器,将其传递给步骤 4中创建的自定义拆分生成器,然后准备数据集:

    builder = BairRobotPushingSmall()
    builder._split_generators = lambda _:split_gen_func(DATA_PATH)
    builder.download_and_prepare()
    
  7. 获取第一批视频:

    BATCH_SIZE = 16
    dataset = builder.as_dataset(split='test')
    test_videos = dataset.batch(BATCH_SIZE)
    for video in test_videos:
        first_batch = video
        break
    
  8. 保留每个视频批次中的第一帧和最后一帧:

    input_frames = first_batch['image_aux1'][:, ::15]
    input_frames = tf.cast(input_frames, tf.float32)
    
  9. 从 TFHub 加载生成器模型:

    model_path = 'https://tfhub.dev/google/tweening_conv3d_bair/1'
    model = tfhub.load(model_path)
    model = model.signatures['default']
    
  10. 将视频批次传递到模型中,生成中间帧:

    middle_frames = model(input_frames)['default']
    middle_frames = middle_frames / 255.0
    
  11. 将每个视频批次的首尾帧与网络在步骤 10中生成的相应中间帧进行连接:

    generated_videos = np.concatenate(
        [input_frames[:, :1] / 255.0,  # All first frames
         middle_frames,  # All inbetween frames
         input_frames[:, 1:] / 255.0],  # All last frames
        axis=1)
    
  12. 最后,绘制首尾帧,以及中间帧:

    plt.figure(figsize=(4, 2 * BATCH_SIZE))
    plot_first_and_last_for_sample(input_frames, 
                                    BATCH_SIZE)
    plot_generated_frames_for_sample(generated_videos)
    plt.show()
    

    图 10.5中,我们可以观察到我们四个示例视频中每个视频的首尾帧:

图 10.5 – 每个视频的首尾帧

图 10.5 – 每个视频的首尾帧

图 10.6中,我们观察到模型为每个视频生成的 14 帧中间帧。仔细检查可以发现,它们与传递给网络的首尾真实帧是一致的:

图 10.6 – 模型为每个示例视频生成的中间帧

图 10.6 – 模型为每个示例视频生成的中间帧

让我们进入*它是如何工作的…*部分,回顾我们所做的工作。

它是如何工作的…

在本节中,我们学习了深度学习在视频中的另一个有趣且有用的应用,特别是在生成模型的背景下,3D 卷积网络的应用。

我们使用了一个在 BAIR Robot Pushing Videos 数据集上训练的最先进架构,该数据集托管在 TFHub 上,并用它生成了一个全新的视频序列,仅以视频的首尾帧作为种子。

由于下载整个 30 GB 的 BAIR 数据集会显得过于冗余,考虑到我们只需要一个小得多的子集来测试我们的解决方案,我们无法直接依赖 TensorFlow 数据集的 load() 方法。因此,我们下载了测试视频的一个子集,并对 BairRobotPushingSmall() 构建器进行了必要的调整,以加载和准备示例视频。

必须提到的是,这个模型是在一个非常特定的数据集上训练的,但它确实展示了这个架构强大的生成能力。我鼓励你查看另见部分,其中列出了如果你想在自己的数据上实现视频生成网络时可能有帮助的有用资源。

另见

你可以在此了解更多关于 BAIR Robot Pushing Videos 数据集的信息:arxiv.org/abs/1710.05268。我鼓励你阅读题为 视频中间插帧使用直接 3D 卷积 的论文,这篇论文提出了我们在本节中使用的网络:arxiv.org/abs/1905.10…. 你可以在以下链接找到我们依赖的 TFHub 模型:tfhub.dev/google/twee…. 最后,以下是关于将普通视频转化为慢动作的 AI 的一篇有趣文章:petapixel.com/2020/09/08/this-ai-can-transform-regular-footage-into-slow-motion-with-no-artifacts/.

使用 TensorFlow Hub 进行文本到视频检索

深度学习在视频中的应用不仅限于分类、分类或生成。神经网络的最大资源之一是它们对数据特征的内部表示。一个网络在某一任务上越优秀,它们的内部数学模型就越好。我们可以利用最先进模型的内部工作原理,构建有趣的应用。

在这个步骤中,我们将基于由S3D模型生成的嵌入创建一个小型搜索引擎,该模型已经在 TFHub 上训练并准备好使用。

你准备好了吗?让我们开始吧!

准备就绪

首先,我们必须安装OpenCV和 TFHub,方法如下:

$> pip install opencv-contrib-python tensorflow-hub

这就是我们需要的,开始这个步骤吧!

如何做到这一点……

执行以下步骤,学习如何使用 TFHub 进行文本到视频的检索:

  1. 第一步是导入我们将使用的所有依赖项:

    import math
    import os
    import uuid
    import cv2
    import numpy as np
    import tensorflow as tf
    import tensorflow_hub as tfhub
    from tensorflow.keras.utils import get_file
    
  2. 定义一个函数,使用 S3D 实例生成文本和视频嵌入:

    def produce_embeddings(model, input_frames, input_words):
        frames = tf.cast(input_frames, dtype=tf.float32)
        frames = tf.constant(frames)
        video_model = model.signatures['video']
        video_embedding = video_model(frames)
        video_embedding = video_embedding['video_embedding']
        words = tf.constant(input_words)
        text_model = model.signatures['text']
        text_embedding = text_model(words)
        text_embedding = text_embedding['text_embedding']
        return video_embedding, text_embedding
    
  3. 定义crop_center()函数,该函数接收一张图像并裁剪出与接收到的帧中心相对应的正方形区域:

    def crop_center(frame):
        height, width = frame.shape[:2]
        smallest_dimension = min(width, height)
        x_start = (width // 2) - (smallest_dimension // 2)
        x_end = x_start + smallest_dimension
        y_start = (height // 2) - (smallest_dimension // 
                                        2)
        y_end = y_start + smallest_dimension
        roi = frame[y_start:y_end, x_start:x_end]
        return roi
    
  4. 定义fetch_and_read_video()函数,顾名思义,该函数下载视频并读取它。在最后一步,我们使用 OpenCV。首先,从给定的 URL 获取视频:

    def fetch_and_read_video(video_url,
                             max_frames=32,
                             resize=(224, 224)):
        extension = video_url.rsplit(os.path.sep,
                                     maxsplit=1)[-1]
        path = get_file(f'{str(uuid.uuid4())}.{extension}',
                        video_url,
                        cache_dir='.',
                        cache_subdir='.')
    

    我们从 URL 中提取视频格式。然后,我们将视频保存在当前文件夹中,文件名为一个随机生成的 UUID。

  5. 接下来,我们将加载这个获取的视频的max_frames

        capture = cv2.VideoCapture(path)
        frames = []
        while len(frames) <= max_frames:
            frame_read, frame = capture.read()
            if not frame_read:
                break
            frame = crop_center(frame)
            frame = cv2.resize(frame, resize)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frames.append(frame)
        capture.release()
        frames = np.array(frames)
    
  6. 如果视频的帧数不足,我们将重复此过程,直到达到所需的容量:

        if len(frames) < max_frames:
            repetitions = math.ceil(float(max_frames) /        
                                    len(frames))
            repetitions = int(repetitions)
            frames = frames.repeat(repetitions, axis=0)
    
  7. 返回归一化后的帧:

        frames = frames[:max_frames]
        return frames / 255.0
    
  8. 定义视频的 URL:

    URLS = [
        ('https://media.giphy.com/media/'
         'WWYSFIZo4fsLC/source.gif'),
        ('https://media.giphy.com/media/'
         'fwhIy2QQtu5vObfjrs/source.gif'),
        ('https://media.giphy.com/media/'
         'W307DdkjIsRHVWvoFE/source.gif'),
        ('https://media.giphy.com/media/'
         'FOcbaDiNEaqqY/source.gif'),
        ('https://media.giphy.com/media/'
         'VJwck53yG6y8s2H3Og/source.gif')]
    
  9. 获取并读取每个视频:

    VIDEOS = [fetch_and_read_video(url) for url in URLS]
    
  10. 定义与每个视频相关联的查询(标题)。请注意,它们必须按正确的顺序排列:

    QUERIES = ['beach', 'playing drums', 'airplane taking 
                  off',
               'biking', 'dog catching frisbee']
    
  11. 从 TFHub 加载 S3D:

    model = tfhub.load
    ('https://tfhub.dev/deepmind/mil-nce/s3d/1')
    
  12. 获取文本和视频嵌入:

    video_emb, text_emb = produce_embeddings(model,
                                  np.stack(VIDEOS, axis=0),
                                             np.array(QUERIES))
    
  13. 计算文本和视频嵌入之间的相似度得分:

    scores = np.dot(text_emb, tf.transpose(video_emb))
    
  14. 获取每个视频的第一帧,将其重新缩放回[0, 255],然后转换为 BGR 空间,以便我们可以使用 OpenCV 显示它。我们这样做是为了展示实验结果:

    first_frames = [v[0] for v in VIDEOS]
    first_frames = [cv2.cvtColor((f * 255.0).astype('uint8'),
                                 cv2.COLOR_RGB2BGR) for f 
                                    in  first_frames]
    
  15. 遍历每个(查询,视频,得分)三元组,并显示每个查询的最相似视频:

    for query, video, query_scores in zip(QUERIES,VIDEOS,scores):
        sorted_results = sorted(list(zip(QUERIES,
                                         first_frames,
                                         query_scores)),
                                key=lambda p: p[-1],
                                reverse=True)
        annotated_frames = []
        for i, (q, f, s) in enumerate(sorted_results, 
                                     start=1):
            frame = f.copy()
            cv2.putText(frame,
                        f'#{i} - Score: {s:.2f}',
                        (8, 15),
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=0.6,
                        color=(0, 0, 255),
                        thickness=2)
            annotated_frames.append(frame)
        cv2.imshow(f'Results for query “{query}”',
                   np.hstack(annotated_frames))
        cv2.waitKey(0)
    

    首先,我们来看一下海滩查询的结果:

图 10.7 – 针对“海滩”查询的排名结果

图 10.7 – 针对“海滩”查询的排名结果

正如预期的那样,第一个结果(得分最高)是一张海滩的图片。现在,让我们试试打鼓

图 10.8 – 针对“打鼓”查询的排名结果

图 10.8 – 针对“打鼓”查询的排名结果

太棒了!看来这个实例中查询文本和图像之间的相似度更强。接下来是一个更具挑战性的查询:

图 10.9 – 针对“飞机起飞”查询的排名结果

图 10.9 – 针对“飞机起飞”查询的排名结果

尽管飞机起飞是一个稍微复杂一点的查询,但我们的解决方案毫无问题地产生了正确的结果。现在让我们试试biking

图 10.10 – BIKING 查询的排名结果

图 10.10 – BIKING 查询的排名结果

又一个匹配!那狗抓飞盘呢?

图 10.11 – DOG CATCHING FRISBEE 查询的排名结果

图 10.11 – DOG CATCHING FRISBEE 查询的排名结果

一点问题都没有!我们所见到的令人满意的结果,归功于 S3D 在将图像与最能描述它们的文字进行匹配方面所做的出色工作。如果你已经阅读了介绍 S3D 的论文,你不会对这一事实感到惊讶,因为它是在大量数据上进行训练的。

现在让我们继续下一部分。

它是如何工作的……

在这个方法中,我们利用了 S3D 模型生成嵌入的能力,既针对文本也针对视频,创建了一个小型数据库,并将其用作一个玩具搜索引擎的基础。通过这种方式,我们展示了拥有一个能够在图像和文本之间生成丰富的信息向量双向映射的网络的实用性。

参见

我强烈推荐你阅读我们在这个方法中使用的模型所发表的论文,内容非常有趣!这是链接:arxiv.org/pdf/1912.06…

第十一章:第十一章:使用 AutoML 简化网络实现

计算机视觉,特别是与深度学习结合时,是一个不适合胆小者的领域!在传统的计算机编程中,我们有有限的调试和实验选项,而在机器学习中,情况则不同。

当然,机器学习本身的随机性在使得创建足够好的解决方案变得困难的过程中起着一定作用,但我们需要调整的无数参数、变量、控制和设置同样是挑战所在,只有将它们调整正确,才能释放神经网络在特定问题上的真正力量。

选择合适的架构只是开始,因为我们还需要考虑预处理技术、学习率、优化器、损失函数、数据拆分等众多因素。

我的观点是,深度学习很难!从哪里开始呢?如果我们能有一种方法来减轻在如此多的组合中寻找的负担,那该多好!

嗯,它确实存在!它被称为自动机器学习AutoML),在本章中,我们将学习如何利用这一领域最有前景的工具之一,它是建立在 TensorFlow 之上的,叫做AutoKeras

在本章中,我们将涵盖以下配方:

  • 使用 AutoKeras 创建一个简单的图像分类器

  • 使用 AutoKeras 创建一个简单的图像回归器

  • 在 AutoKeras 中导出和导入模型

  • 使用 AutoKeras 的 AutoModel 控制架构生成

  • 使用 AutoKeras 预测年龄和性别

让我们开始吧!

技术要求

你会首先注意到的是,AutoML非常消耗资源,因此如果你想复制并扩展我们将在本章中讨论的配方,访问GPU是必须的。此外,由于我们将在所有提供的示例中使用AutoKeras,请按以下方式安装它:

$> pip install git+https://github.com/keras-team/keras-tuner.git@1.0.2rc2 autokeras pydot graphviz

本章我们将使用的AutoKeras版本仅支持 TensorFlow 2.3,因此请确保已安装该版本(如果愿意,你也可以创建一个全新的环境)。在每个配方的准备工作部分,你会找到任何需要的准备信息。和往常一样,本章中的代码可以在github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch11获取。

查看以下链接,观看 Code in Action 视频:

bit.ly/2Na6XRz

使用 AutoKeras 创建一个简单的图像分类器

图像分类无疑是神经网络在计算机视觉中的事实性应用。然而,正如我们所知道的,根据数据集的复杂性、信息的可用性以及无数其他因素,创建一个合适的图像分类器的过程有时可能相当繁琐。

在这个教程中,我们将借助AutoML的魔力轻松实现一个图像分类器。不信吗?那就开始吧,一起看看!

如何实现…

在本教程结束时,你将能用不超过十几行代码实现一个图像分类器!让我们开始吧:

  1. 导入所有需要的模块:

    from autokeras import ImageClassifier
    from tensorflow.keras.datasets import fashion_mnist as fm
    

    为了简化,我们将使用著名的Fashion-MNIST数据集,它是著名的MNIST的一个更具挑战性的版本。

  2. 加载训练和测试数据:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 将图像归一化到[0, 1]的范围:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  4. 定义我们允许每个可能的网络(称为一次试验)训练的轮次数:

    EPOCHS = 10
    
  5. 这就是魔法发生的地方。定义一个ImageClassifier()实例:

    classifier = ImageClassifier(seed=9, max_trials=10)
    

    注意,我们将分类器的种子设为 9,并允许它找到一个合适的网络 10 次。我们这样做是为了让神经架构搜索NAS)过程在合理的时间内终止(要了解更多关于NAS的信息,请参考参见部分)。

  6. 在测试数据上对分类器进行 10 个轮次的训练(每次试验):

    classifier.fit(X_train, y_train, epochs=EPOCHS)
    
  7. 最后,在测试集上评估最佳分类器并打印准确率:

    print(classifier.evaluate(X_test, y_test))
    

    过一段时间后(别忘了库正在训练 10 个具有不同复杂度的模型),我们应该能得到大约 93%的准确率。考虑到我们甚至没有写 10 行代码,这个结果还不错!

我们将在*工作原理…*部分进一步讨论我们所做的工作。

工作原理…

在这个教程中,我们创建了最轻松的图像分类器!我们将所有主要决策都交给了AutoML工具——AutoKeras。从选择架构到选择使用哪种优化器,所有这些决策都由框架做出。

你可能已经注意到,我们通过指定最多 10 次试验和每次试验最多 10 个轮次来限制搜索空间。我们这样做是为了让程序在合理的时间内终止,但正如你可能猜到的,这些参数也可以交给AutoKeras来处理。

尽管AutoML具有很高的自主性,我们仍然可以根据需要指导框架。正如其名所示,AutoML提供了一种自动化寻找针对特定问题足够好组合的方法。然而,这并不意味着不需要人类的专业知识和先前的经验。事实上,通常情况下,一个经过精心设计的网络(通常是通过深入研究数据所得)往往比AutoML在没有任何先前信息的情况下找到的网络效果更好。

最终,AutoML是一个工具,应该用来增强我们对深度学习的掌握,而不是取而代之——因为它做不到这一点。

参见

你可以在这里了解更多关于NAS的信息:en.wikipedia.org/wiki/Neural_architecture_search

使用 AutoKeras 创建一个简单的图像回归器

AutoKeras的强大功能不仅限于图像分类。尽管不如图像分类流行,图像回归是一个类似的问题,我们希望根据图像中的空间信息预测一个连续的量。

在本配方中,我们将训练一个图像回归器,预测人们的年龄,同时使用AutoML

让我们开始吧。

准备工作

在本配方中,我们将使用APPA-REAL数据集,该数据集包含 7,591 张图像,标注了广泛对象的真实年龄和表观年龄。您可以在chalearnlap.cvc.uab.es/dataset/26/description/#查看更多有关该数据集的信息并下载它。将数据解压到您选择的目录中。为了配方的目的,我们假设数据集位于~/.keras/datasets/appa-real-release文件夹中。

以下是一些示例图像:

图 11.1 – 来自 APPA-REAL 数据集的示例图像

](tos-cn-i-73owjymdk6/0845c796fd414b799289bfc967cc27f2)

图 11.1 – 来自 APPA-REAL 数据集的示例图像

让我们实现这个配方!

如何操作……

按照以下步骤完成此配方:

  1. 导入我们将要使用的模块:

    import csv
    import pathlib
    import numpy as np
    from autokeras import ImageRegressor
    from tensorflow.keras.preprocessing.image import *
    
  2. 数据集的每个子集(训练集、测试集和验证集)都在一个 CSV 文件中定义。在这个文件中,除了许多其他列外,我们还有图像路径和照片中人物的实际年龄。在此步骤中,我们将定义load_mapping()函数,该函数将从图像路径创建一个映射,用于加载实际数据到内存中:

    def load_mapping(csv_path, faces_path):
        mapping = {}
        with open(csv_path, 'r') as f:
            reader = csv.DictReader(f)
            for line in reader:
                file_name = line["file_name"].rsplit(".")[0]
               key = f'{faces_path}/{file_name}.jpg_face.jpg'
                mapping[key] = int(line['real_age'])
        return mapping
    
  3. 定义get_image_and_labels()函数,该函数接收load_mapping()函数生成的映射,并返回一个图像数组(归一化到[-1, 1]范围内)和一个相应年龄的数组:

    def get_images_and_labels(mapping):
        images = []
        labels = []
        for image_path, label in mapping.items():
            try:
                image = load_img(image_path, target_size=(64, 
                                                        64))
                image = img_to_array(image)
                images.append(image)
                labels.append(label)
            except FileNotFoundError:
                continue
        return (np.array(images) - 127.5) / 127.5, \
               np.array(labels).astype('float32')
    

    请注意,每张图像都已调整大小,确保其尺寸为 64x64x3。这是必要的,因为数据集中的图像尺寸不统一。

  4. 定义 CSV 文件的路径,以创建每个子集的数据映射:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' 
                 /'appa-real-release')
    train_csv_path = str(base_path / 'gt_train.csv')
    test_csv_path = str(base_path / 'gt_test.csv')
    val_csv_path = str(base_path / 'gt_valid.csv')
    
  5. 定义每个子集的图像所在目录的路径:

    train_faces_path = str(base_path / 'train')
    test_faces_path = str(base_path / 'test')
    val_faces_path = str(base_path / 'valid')
    
  6. 为每个子集创建映射:

    train_mapping = load_mapping(train_csv_path, 
                                train_faces_path)
    test_mapping = load_mapping(test_csv_path, 
                               test_faces_path)
    val_mapping = load_mapping(val_csv_path, 
                               val_faces_path)
    
  7. 获取每个子集的图像和标签:

    X_train, y_train = get_images_and_labels(train_mapping)
    X_test, y_test = get_images_and_labels(test_mapping)
    X_val, y_val = get_images_and_labels(val_mapping)
    
  8. 我们将在每次试验中训练每个网络,最多训练 15 个 epoch:

    EPOCHS = 15
    
  9. 我们实例化一个ImageRegressor()对象,它封装了adam优化器:

    regressor = ImageRegressor(seed=9,
                               max_trials=10,
                               optimizer='adam')
    
  10. 拟合回归器。请注意,我们传递了自己的验证集。如果我们不这样做,AutoKeras默认会取 20%的训练数据来验证它的实验:

    regressor.fit(X_train, y_train,
                  epochs=EPOCHS,
                  validation_data=(X_val, y_val))
    
  11. 最后,我们必须在测试数据上评估最佳回归器并打印其性能指标:

    print(regressor.evaluate(X_test, y_test))
    

    一段时间后,我们应该获得 241.248 的测试损失,考虑到我们的工作主要是加载数据集,这个结果还不错。

让我们进入*它是如何工作的……*部分。

它是如何工作的……

在这个食谱中,我们将模型的创建委托给了AutoML框架,类似于我们在使用 AutoKeras 创建简单图像分类器食谱中的做法。不过,这次,我们的目标是解决回归问题,即根据人脸照片预测一个人的年龄,而不是分类问题。

这一次,因为我们使用了一个真实世界的数据集,我们不得不实现几个辅助函数来加载数据,并将其转化为AutoKeras可以使用的正确形状。不过,在做完这些之后,我们让框架接管,利用它内建的NAS算法,在 15 次迭代中找到最佳模型。

我们在测试集上获得了一个相当不错的 241.248 的损失值。预测一个人的年龄并不是一件简单的任务,尽管乍一看它似乎很简单。我邀请你仔细查看APPA-REAL的 CSV 文件,这样你就能看到人类对年龄估算的偏差!

另见

你可以在这里了解更多关于NAS的信息:en.wikipedia.org/wiki/Neural_architecture_search

在 AutoKeras 中导出和导入模型

在使用AutoML时,我们可能会担心其黑盒性质。我们能控制生成的模型吗?我们可以扩展它们吗?理解它们吗?重新使用它们吗?

当然可以!AutoKeras的一个优点是它构建在 TensorFlow 之上,因此尽管它非常复杂,但在底层,训练的模型只是 TensorFlow 图,我们可以在以后导出、调整和优化这些模型(如果需要的话)。

在这个食谱中,我们将学习如何导出一个在AutoKeras上训练的模型,然后将其作为一个普通的 TensorFlow 网络导入。

你准备好了吗?让我们开始吧。

如何做到…

按照以下步骤完成本食谱:

  1. 导入必要的依赖项:

    from autokeras import *
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.models import load_model
    from tensorflow.keras.utils import plot_model
    
  2. 加载Fashion-MNIST数据集的训练和测试集:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 将数据归一化到[0, 1]区间:

    X_train = X_train.astype('float32') / 255.0
    X_test = X_test.astype('float32') / 255.0
    
  4. 定义我们将为每个网络训练的周期数:

    EPOCHS = 10
    
  5. 创建一个ImageClassifier(),它将尝试在 20 次试验中找到最佳分类器,每次训练 10 个周期。我们将指定adam作为优化器,并为可重复性设置ImageClassifier()的种子:

    classifier = ImageClassifier(seed=9,
                                 max_trials=20,
                                 optimizer='adam')
    
  6. 训练分类器。我们将允许AutoKeras自动选择 20%的训练数据作为验证集:

    classifier.fit(X_train, y_train, epochs=EPOCHS)
    
  7. 导出最佳模型并将其保存到磁盘:

    model = classifier.export_model()
    model.save('model.h5')
    
  8. 将模型重新加载到内存中:

    model = load_model('model.h5',
                       custom_objects=CUSTOM_OBJECTS)
    
  9. 在测试集上评估训练模型:

    print(classifier.evaluate(X_test, y_test))
    
  10. 打印最佳模型的文本摘要:

    print(model.summary())
    
  11. 最后,生成AutoKeras找到的最佳模型的架构图:

    plot_model(model,
               show_shapes=True,
               show_layer_names=True,
               to_file='model.png')
    

    在 20 次试验之后,AutoKeras创建的最佳模型在测试集上的准确率达到了 91.5%。以下截图展示了模型的摘要:

图 11.2 – AutoKeras最佳模型摘要

图 11.2 – AutoKeras最佳模型摘要

以下图表展示了模型的架构:

图 11.3 – AutoKeras 的最佳模型架构

图 11.3 – AutoKeras 的最佳模型架构

图 11.2中,我们可以看到AutoKeras被认为是最适合Fashion-MNIST的网络,至少在我们设定的范围内是这样。你可以在配套的 GitHub 仓库中更详细地查看完整架构。

让我们继续到下一节。

它是如何工作的……

在这个配方中,我们展示了AutoML如何作为我们解决新计算机视觉问题时的一个很好的起点。如何做?我们可以利用它快速生成表现良好的模型,然后基于我们对当前数据集的领域知识进行扩展。

实现这一点的公式很简单:让AutoML做一段时间的繁重工作,然后导出最佳网络并将其导入 TensorFlow 的框架中,这样你就可以在其上构建你的解决方案。

这不仅展示了像AutoKeras这样的工具的可用性,还让我们得以窥见幕后,理解由NAS生成的模型的构建块。

另见

AutoKeras的基础是NAS。你可以在这里阅读更多相关内容(非常有趣!):en.wikipedia.org/wiki/Neural_architecture_search

使用 AutoKeras 的 AutoModel 控制架构生成

AutoKeras自动决定哪个架构最适合是很好的,但这可能会非常耗时——有时是不可接受的。

我们能否施加更多控制?我们能否提示哪些选项最适合我们的特定问题?我们能否通过提供一组必须遵循的指导方针,使AutoML在我们事先的知识或偏好基础上进行实验,同时又给它足够的自由度进行试探?

是的,我们可以,在这个配方中,你将通过利用AutoKeras中的一个特别功能——AutoModel 来学习如何操作!

如何操作……

按照这些步骤学习如何自定义AutoModel的搜索空间:

  1. 我们需要做的第一件事是导入所有必需的依赖项:

    from autokeras import *
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.models import load_model
    from tensorflow.keras.utils import *
    
  2. 因为我们将在Fashion-MNIST上训练我们的自定义模型,所以我们必须分别加载训练和测试拆分数据:

    (X_train, y_train), (X_test, y_test) = fm.load_data()
    
  3. 为了避免数值不稳定问题,让我们将两个拆分的图像归一化到[0, 1]范围内:

    X_train = X_train.astype('float32')
    X_test = X_test.astype('float32')
    
  4. 定义create_automodel()函数,该函数定义了底层Block的自定义搜索空间,负责定义的任务,如图像增强、归一化、图像处理或分类。首先,我们必须定义输入块,它将通过Normalization()ImageAugmentation()块分别进行归一化和增强:

    def create_automodel(max_trials=10):
        input = ImageInput()
        x = Normalization()(input)
        x = ImageAugmentation(horizontal_flip=False,
                              vertical_flip=False)(x)
    

    注意,我们在ImageAugmentation()块中禁用了水平和垂直翻转。这是因为这些操作会改变Fashion-MNIST中图像的类别。

  5. 现在,我们将图表分叉。左侧分支使用ConvBlock()搜索普通卷积层。右侧分支,我们将探索更复杂的类似 Xception 的架构(有关Xception架构的更多信息,请参阅另见部分):

        left = ConvBlock()(x)
        right = XceptionBlock(pretrained=True)(x)
    

    在前面的代码片段中,我们指示AutoKeras只探索在 ImageNet 上预训练的Xception架构。

  6. 我们将合并左右两个分支,展开它们,然后通过DenseBlock()传递结果,正如它的名字所示,它会搜索完全连接的层组合:

        x = Merge()([left, right])
        x = SpatialReduction(reduction_type='flatten')(x)
        x = DenseBlock()(x)
    
  7. 这个图表的输出将是一个ClassificationHead()。这是因为我们处理的是一个分类问题。请注意,我们没有指定类别的数量。这是因为AutoKeras会从数据中推断出这些信息:

        output = ClassificationHead()(x)
    
  8. 我们可以通过构建并返回一个AutoModel()实例来结束create_automodel()。我们必须指定输入和输出,以及要执行的最大尝试次数:

        return AutoModel(inputs=input,
                         outputs=output,
                         overwrite=True,
                         max_trials=max_trials)
    
  9. 让我们训练每个试验模型 10 个周期:

    EPOCHS = 10
    
  10. 创建AutoModel并进行拟合:

    model = create_automodel()
    model.fit(X_train, y_train, epochs=EPOCHS)
    
  11. 让我们导出最佳模型:

    model = model.export_model()
    
  12. 在测试集上评估模型:

    print(model.evaluate(X_test, to_categorical(y_test)))
    
  13. 绘制最佳模型的架构:

    plot_model(model,
               show_shapes=True,
               show_layer_names=True,
               to_file='automodel.png')
    

    我最终得到的架构在测试集上的准确率达到了 90%,尽管你的结果可能有所不同。更有趣的是生成的模型的结构:

图 11.4 – AutoKeras 的最佳模型架构

图 11.4 – AutoKeras 的最佳模型架构

图 11.4 – AutoKeras 的最佳模型架构

上面的图表揭示了AutoModel根据我们在create_automodel()中设计的蓝图生成了一个网络。

现在,让我们进入*它是如何工作的……*部分。

它是如何工作的……

在这个示例中,我们利用了AutoModel模块来缩小搜索空间。当我们大致知道最终模型应该是什么样时,这个特性非常有用。因为我们不会让AutoKeras浪费时间尝试无效、无用的组合,所以可以节省大量时间。一个这样的无效组合的例子可以在第 4 步中看到,我们告诉AutoKeras不要将图像翻转作为图像增强的一部分。因为根据我们问题的特点,这个操作会改变Fashion-MNIST中数字的类别。

证明我们引导了create_automodel()函数。

很令人印象深刻,对吧?

另见

这里我们没有做的一件事是实现我们自己的Block,这在AutoKeras中是可能的。为什么不尝试一下呢?你可以从阅读这里的文档开始:autokeras.com/tutorial/customized/。要查看所有可用的模块,可以访问autokeras.com/block/。在这个示例中,我们使用了类似 Xception 的层。要了解更多关于 Xception 的信息,可以阅读原始论文:arxiv.org/abs/1610.02357

使用 AutoKeras 预测年龄和性别

在这个方案中,我们将学习 AutoML 的一个实际应用,可以作为模板来创建原型、MVP,或者仅仅借助 AutoML 来解决现实世界中的问题。

更具体地说,我们将创建一个年龄和性别分类程序,其中有一个特别的地方:性别和年龄分类器的架构将由AutoKeras负责。我们将负责获取和整理数据,并创建框架来在我们自己的图像上测试解决方案。

希望你准备好了,因为我们马上开始!

准备好了吗

我们需要几个外部库,比如 OpenCV、scikit-learnimutils。所有这些依赖项可以一次性安装,方法如下:

$> pip install opencv-contrib-python scikit-learn imutils

在数据方面,我们将使用Adience数据集,其中包含 26,580 张 2,284 个主体的图像,并附有其性别和年龄。要下载数据,请访问 talhassner.github.io/home/projects/Adience/Adience-data.html

接下来,你需要进入下载部分并输入你的姓名和电子邮件,如下图所示:

图 11.5 – 输入你的信息以接收存储数据的 FTP 服务器凭证

图 11.5 – 输入你的信息以接收存储数据的 FTP 服务器凭证

一旦你点击提交按钮,你将获得访问 FTP 服务器所需的凭证,该服务器存储着数据。你可以在这里访问:www.cslab.openu.ac.il/download/

确保点击第一个链接,标签为Adience OUI 未滤镜的人脸用于性别和年龄分类

图 11.6 – 进入高亮链接

图 11.6 – 进入高亮链接

输入你之前收到的凭证并访问第二个链接,名称为AdienceBenchmarkOfUnfilteredFacesForGenderAndAgeClassification

图 11.7 – 点击高亮链接

图 11.7 – 点击高亮链接

最后,下载 aligned.tar.gzfold_frontal_0_data.txtfold_frontal_1_data.txtfold_frontal_2_data.txtfold_frontal_3_data.txtfold_frontal_4_data.txt

图 11.8 – 下载 aligned.tar.gz 和所有 fold_frontal_*_data.txt 文件

图 11.8 – 下载 aligned.tar.gz 和所有 fold_frontal_*_data.txt 文件

解压 aligned.tar.gz 到你选择的目录中,命名为 adience。在该目录内,创建一个名为 folds 的子目录,并将所有 fold_frontal_*_data.txt 文件移动到其中。为了本方案的方便,我们假设数据集位于 ~/.keras/datasets/adience

下面是一些示例图像:

图 11.9 – 来自 Adience 数据集的示例图像

图 11.9 – 来自 Adience 数据集的示例图像

让我们实现这个方案吧!

如何操作…

完成以下步骤以实现一个年龄和性别分类器,使用AutoML

  1. 我们需要做的第一件事是导入所有必要的依赖:

    import csv
    import os
    import pathlib
    from glob import glob
    import cv2
    import imutils
    import numpy as np
    from autokeras import *
    from sklearn.preprocessing import LabelEncoder
    from tensorflow.keras.models import load_model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义Adience数据集的基本路径,以及包含图像与其受试者年龄和性别关系的折叠(CSV 格式):

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' 
                     /'adience')
    folds_path = str(base_path / 'folds')
    
  3. Adience中的年龄以区间、组别或括号的形式表示。在这里,我们将定义一个数组,用于将折叠中的报告年龄映射到正确的区间:

    AGE_BINS = [(0, 2), (4, 6), (8, 13), (15, 20), (25, 32), 
                (38, 43), (48, 53), (60, 99)]
    
  4. 定义age_to_bin()函数,该函数接收一个输入(如折叠 CSV 行中的值),并将其映射到相应的区间。例如,如果输入是(27, 29),输出将是25_32

    def age_to_bin(age):
        age = age.replace('(', '').replace(')', '').
                                        split(',')
        lower, upper = [int(x.strip()) for x in age]
        for bin_low, bin_up in AGE_BINS:
            if lower >= bin_low and upper <= bin_up:
                label = f'{bin_low}_{bin_up}'
                return label
    
  5. 定义一个函数来计算矩形的面积。我们稍后将用它来获取最大的面部检测区域:

    def rectangle_area(r):
        return (r[2] - r[0]) * (r[3] - r[1])
    
  6. 我们还将绘制一个边框框住检测到的人脸,并附上识别出的年龄和性别:

    def plot_face(image, age_gender, detection):
        frame_x, frame_y, frame_width, frame_height = detection
        cv2.rectangle(image,
                      (frame_x, frame_y),
                      (frame_x + frame_width,
                       frame_y + frame_height),
                      color=(0, 255, 0),
                      thickness=2)
        cv2.putText(image,
                    age_gender,
                    (frame_x, frame_y - 10),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=0.45,
                    color=(0, 255, 0),
                    thickness=2)
        return image
    
  7. 定义predict()函数,我们将用它来预测传入roi参数的人的年龄和性别(取决于model):

    def predict(model, roi):
        roi = cv2.resize(roi, (64, 64))
        roi = roi.astype('float32') / 255.0
        roi = img_to_array(roi)
        roi = np.expand_dims(roi, axis=0)
        predictions = model.predict(roi)[0]
        return predictions
    
  8. 定义存储数据集中所有图像、年龄和性别的列表:

    images = []
    ages = []
    genders = []
    
  9. 遍历每个折叠文件。这些文件将是 CSV 格式:

    folds_pattern = os.path.sep.join([folds_path, '*.txt'])
    for fold_path in glob(folds_pattern):
        with open(fold_path, 'r') as f:
            reader = csv.DictReader(f, delimiter='\t')
    
  10. 如果年龄或性别字段不明确,跳过当前行:

            for line in reader:
                if ((line['age'][0] != '(') or
                        (line['gender'] not in {'m', 'f'})):
                    Continue
    
  11. 将年龄映射到一个有效的区间。如果从age_to_bin()返回None,这意味着年龄不对应我们定义的任何类别,因此必须跳过此记录:

                age_label = age_to_bin(line['age'])
                if age_label is None:
                    continue
    
  12. 加载图像:

                aligned_face_file = 
                               (f'landmark_aligned_face.'
                                     f'{line["face_id"]}.'
                              f'{line["original_image"]}')
                image_path = os.path.sep.join(
                                 [str(base_path),
                                 line["user_id"],
                               aligned_face_file])
                image = load_img(image_path, 
                                 target_size=(64, 64))
                image = img_to_array(image)
    
  13. 将图像、年龄和性别添加到相应的集合中:

                images.append(image)
                ages.append(age_label)
                genders.append(line['gender'])
    
  14. 为每个问题(年龄分类和性别预测)创建两份图像副本:

    age_images = np.array(images).astype('float32') / 255.0
    gender_images = np.copy(images)
    
  15. 编码年龄和性别:

    gender_enc = LabelEncoder()
    age_enc = LabelEncoder()
    gender_labels = gender_enc.fit_transform(genders)
    age_labels = age_enc.fit_transform(ages)
    
  16. 定义每次试验的次数和每次试验的周期。这些参数会影响两个模型:

    EPOCHS = 100
    MAX_TRIALS = 10
    
  17. 如果有训练好的年龄分类器,加载它;否则,从头开始训练一个ImageClassifier()并保存到磁盘:

    if os.path.exists('age_model.h5'):
        age_model = load_model('age_model.h5')
    else:
        age_clf = ImageClassifier(seed=9,
                                  max_trials=MAX_TRIALS,
                                  project_name='age_clf',
                                  overwrite=True)
        age_clf.fit(age_images, age_labels, epochs=EPOCHS)
        age_model = age_clf.export_model()
        age_model.save('age_model.h5')
    
  18. 如果有训练好的性别分类器,加载它;否则,从头开始训练一个ImageClassifier()并保存到磁盘:

    if os.path.exists('gender_model.h5'):
        gender_model = load_model('gender_model.h5')
    else:
        gender_clf = ImageClassifier(seed=9,
    
                                   max_trials=MAX_TRIALS,
                                project_name='gender_clf',
                                     overwrite=True)
        gender_clf.fit(gender_images, gender_labels,
                       epochs=EPOCHS)
        gender_model = gender_clf.export_model()
        gender_model.save('gender_model.h5')
    
  19. 从磁盘读取测试图像:

    image = cv2.imread('woman.jpg')
    
  20. 创建一个Haar Cascades人脸检测器。(这是本书范围之外的主题。如果你想了解更多关于 Haar Cascades 的内容,请参阅本配方的另见部分。)使用以下代码来完成:

    cascade_file = 'resources/haarcascade_frontalface_default.xml'
    det = cv2.CascadeClassifier(cascade_file)
    
  21. 调整图像大小,使其宽度为 380 像素。得益于imutils.resize()函数,我们可以放心结果会保持纵横比。因为该函数会自动计算高度以确保这一条件:

    image = imutils.resize(image, width=380)
    
  22. 创建原始图像的副本,以便我们可以在其上绘制检测结果:

    copy = image.copy()
    
  23. 将图像转换为灰度,并通过人脸检测器:

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    detections = \
        det.detectMultiScale(gray,
                             scaleFactor=1.1,
                             minNeighbors=5,
                             minSize=(35, 35),
                          flags=cv2.CASCADE_SCALE_IMAGE)
    
  24. 验证是否有检测到的对象,并获取具有最大面积的一个:

    if len(detections) > 0:
        detections = sorted(detections, key=rectangle_area)
        best_detection = detections[-1]
    
  25. 提取与检测到的人脸相对应的兴趣区域(roi),并提取其年龄和性别:

        (frame_x, frame_y,
         frame_width, frame_height) = best_detection
        roi = image[frame_y:frame_y + frame_height,
                    frame_x:frame_x + frame_width]
        age_pred = predict(age_model, roi).argmax()
        age = age_enc.inverse_transform([age_pred])[0]
        gender_pred = predict(gender_model, roi).argmax()
        gender = gender_enc.inverse_transform([gender_pred])[0]
    

    注意,我们使用每个编码器来还原为人类可读的标签,用于预测的年龄和性别。

  26. 将预测的年龄和性别标注在原始图像上,并显示结果:

        clone = plot_face(copy,
                          f'Gender: {gender} - Age: 
                           {age}',
                          best_detection)
        cv2.imshow('Result', copy)
        cv2.waitKey(0)
    

    重要提示

    第一次执行此脚本时,您需要等待很长时间——可能超过 24 小时(取决于您的硬件)。这是因为每个模型都经过大量的试验和轮次训练。然而,之后的运行会更快,因为程序将加载已经训练好的分类器。

    我们可以在以下截图中看到一个成功预测年龄和性别的例子:

图 11.10 – 我们的模型判定照片中的人是女性,年龄在 25 至 32 岁之间。看起来差不多吧?

图 11.10 – 我们的模型判定照片中的人是女性,年龄在 25 至 32 岁之间。看起来差不多吧?

难道不是真正令人惊叹吗?所有的繁重工作都是由AutoKeras完成的!我们正在生活在未来!

它是如何工作的……

在这个示例中,我们实现了一个实际的解决方案,来应对一个令人意外的挑战性问题:年龄和性别预测。

为什么这很具挑战性?一个人的表面年龄可能会有所不同,取决于多种因素,如种族、性别、健康状况和其他生活条件。我们人类在仅凭外貌特征来估算一个人(男性或女性)的年龄时,并没有我们想象中的那么准确。

例如,一个大致健康的 25 岁的人与另一个 25 岁的重度饮酒和吸烟者看起来会有很大的不同。

无论如何,我们相信AutoML的力量,找到了两个模型:一个用于性别分类,另一个用于年龄预测。我们必须强调,在这种情况下,我们将年龄预测视为分类问题,而不是回归问题。这是因为这样更容易选择一个年龄范围,而不是给出一个精确的数值。

经过长时间的等待(我们对每个模型进行了超过 100 轮的训练),我们得到了两个合格的网络,并将它们整合到一个框架中,该框架可以自动识别照片中的人脸,并使用这些模型标注出预测的年龄和性别。

如您所见,我们依赖于ImageClassifier(),这意味着我们将网络创建过程的 100%控制权交给了AutoModel,以缩小搜索空间,从而在更短的时间内获得潜在的更好解决方案。为什么不试试看呢?

另见

阅读以下论文,了解Adience数据集的作者如何解决这个问题:talhassner.github.io/home/projects/cnn_agegender/CVPR2015_CNN_AgeGenderEstimation.pdf。要了解更多关于我们之前使用的 Haar Cascade 分类器的信息,请阅读这个教程:docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html

第十二章:第十二章:提升性能

更多时候,从好到优秀的跃升并不涉及剧烈的变化,而是细微的调整和微调。

人们常说,20%的努力可以带来 80%的成果(这就是帕累托原则)。但是 80%和 100%之间的差距呢?我们需要做什么才能超越预期,改进我们的解决方案,最大限度地提升计算机视觉算法的性能?

嗯,和所有深度学习相关的事情一样,答案是艺术与科学的结合。好消息是,本章将专注于一些简单的工具,你可以用它们来提升神经网络的性能!

本章将涵盖以下方法:

  • 使用卷积神经网络集成来提高准确性

  • 使用测试时数据增强来提高准确性

  • 使用排名-N 准确率来评估性能

  • 使用标签平滑提高性能

  • 检查点模型

  • 使用tf.GradientTape自定义训练过程

  • 可视化类激活图以更好地理解你的网络

让我们开始吧!

技术要求

如往常一样,如果你能使用 GPU,你将能从这些方法中获得最大收益,因为本章中的某些示例对资源的需求相当高。此外,如果有任何你需要执行的准备工作以完成某个方法,你会在准备工作部分找到相关内容。最后,关于本章的代码可以在 GitHub 的附带仓库中找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch12

查看以下链接,观看代码实战视频:

bit.ly/2Ko3H3K

使用卷积神经网络集成来提高准确性

在机器学习中,最强大的分类器之一,实际上是一个元分类器,叫做集成。集成由所谓的弱分类器组成,弱分类器是指比随机猜测稍微好一点的预测模型。然而,当它们结合在一起时,结果是一个相当强大的算法,特别是在面对高方差(过拟合)时。我们可能遇到的一些最著名的集成方法包括随机森林和梯度提升机。

好消息是,当涉及到神经网络时,我们可以利用相同的原理,从而创造出一个整体效果,超越单独部分的总和。你想知道怎么做吗?继续阅读吧!

准备工作

本方法依赖于Pillowtensorflow_docs,可以通过以下方式轻松安装:

$> pip install Pillow git+https://github.com/tensorflow/docs

我们还将使用著名的Caltech 101数据集,可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压101_ObjectCategories.tar.gz到你选择的位置。为了这个配方,我们将它放在~/.keras/datasets/101_ObjectCategories中。

以下是一些示例图像:

图 12.1 – Caltech 101 样本图像

](tos-cn-i-73owjymdk6/bd1ee7ee4a0a45aa95ca19b9286cfce3)

图 12.1 – Caltech 101 样本图像

让我们开始这个配方吧?

如何操作…

按照以下步骤创建卷积神经网络CNN)的集成:

  1. 导入所有必需的模块:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义load_images_and_labels()函数,读取Caltech 101数据集中的图像和类别,并将它们作为 NumPy 数组返回:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        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)
    
  3. 定义build_model()函数,负责构建一个类似 VGG 的卷积神经网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 定义plot_model_history()函数,我们将用它来绘制集成中各个网络的训练和验证曲线:

    def plot_model_history(model_history, metric, 
                           plot_name):
        plt.style.use('seaborn-darkgrid')
        plotter = tfdocs.plots.HistoryPlotter()
        plotter.plot({'Model': model_history}, 
                      metric=metric)
        plt.title(f'{metric.upper()}')
        plt.ylim([0, 1])
        plt.savefig(f'{plot_name}.png')
        plt.close()
    
  5. 为了提高可复现性,设置一个随机种子:

    SEED = 999
    np.random.seed(SEED)
    
  6. 编译Caltech 101图像的路径以及类别:

    base_path = (pathlib.Path.home() / '.keras' / 
                  'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  7. 加载图像和标签,同时对图像进行归一化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 保留 20%的数据用于测试,其余用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小、训练轮次以及每个轮次的批次数:

    BATCH_SIZE = 64
    STEPS_PER_EPOCH = len(X_train) // BATCH_SIZE
    EPOCHS = 40
    
  10. 我们将在这里使用数据增强,执行一系列随机变换,如水平翻转、旋转和缩放:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  11. 我们的集成将包含5个模型。我们会将每个网络在集成中的预测保存到ensemble_preds列表中:

    NUM_MODELS = 5
    ensemble_preds = []
    
  12. 我们将以类似的方式训练每个模型。首先创建并编译网络本身:

    for n in range(NUM_MODELS):
        print(f'Training model {n + 1}/{NUM_MODELS}')
        model = build_network(64, 64, 3, len(CLASSES))
        model.compile(loss='categorical_crossentropy',
                      optimizer='rmsprop',
                      metrics=['accuracy'])
    
  13. 然后,我们将使用数据增强来拟合模型:

        train_generator = augmenter.flow(X_train, y_train,
                                         BATCH_SIZE)
        hist = model.fit(train_generator,
                         steps_per_epoch=STEPS_PER_EPOCH,
                         validation_data=(X_test, y_test),
                         epochs=EPOCHS,
                         verbose=2)
    
  14. 计算模型在测试集上的准确率,绘制训练和验证准确率曲线,并将其预测结果存储在ensemble_preds中:

        predictions = model.predict(X_test, 
                                   batch_size=BATCH_SIZE)
        accuracy = accuracy_score(y_test.argmax(axis=1),
                                  predictions.argmax(axis=1))
        print(f'Test accuracy (Model #{n + 1}): {accuracy}')
        plot_model_history(hist, 'accuracy', f'model_{n +1}')
        ensemble_preds.append(predictions)
    
  15. 最后一步是对每个集成成员的预测进行平均,从而有效地为整个元分类器产生联合预测,然后计算测试集上的准确率:

    ensemble_preds = np.average(ensemble_preds, axis=0)
    ensemble_acc = accuracy_score(y_test.argmax(axis=1),
                             ensemble_preds.argmax(axis=1))
    print(f'Test accuracy (ensemble): {ensemble_acc}')
    

    因为我们训练的是五个网络,所以这个程序可能需要一段时间才能完成。完成后,你应该能看到每个集成网络成员的准确率类似于以下内容:

    Test accuracy (Model #1): 0.6658986175115207
    Test accuracy (Model #2): 0.6751152073732719
    Test accuracy (Model #3): 0.673963133640553
    Test accuracy (Model #4): 0.6491935483870968
    Test accuracy (Model #5): 0.6756912442396313
    

    在这里,我们可以看到准确率在 65%到 67.5%之间。下图展示了模型 1 到 5 的训练和验证曲线(从左到右,上排为模型 1、2、3,下排为模型 4、5):

图 12.2 – 五个模型在集成中的训练和验证准确率曲线

](tos-cn-i-73owjymdk6/74830aa4054f44e590e670f83145ddce)

图 12.2 – 五个模型在集成中的训练和验证准确率曲线

然而,最有趣的结果是集成模型的准确性,这是通过平均每个模型的预测结果得出的:

Test accuracy (ensemble): 0.7223502304147466

真是令人印象深刻!仅仅通过结合五个网络的预测,我们就把准确率提升到了 72.2%,并且是在一个非常具有挑战性的数据集——Caltech 101上!我们将在下一部分进一步讨论这一点。

它是如何工作的…

在本教程中,我们通过在具有挑战性的Caltech 101数据集上训练五个神经网络,利用了集成的力量。必须指出的是,我们的过程相当简单而不起眼。我们首先加载并整理数据,使其适合训练,然后使用相同的模板训练多个 VGG 风格的架构副本。

为了创建更强大的分类器,我们使用了数据增强,并对每个网络进行了 40 个周期的训练。除了这些细节之外,我们没有改变网络的架构,也没有调整每个特定成员。结果是,每个模型在测试集上的准确率介于 65%和 67%之间。然而,当它们结合起来时,达到了一个不错的 72%!

那么,为什么会发生这种情况呢?集成学习的原理是每个模型在训练过程中都会形成自己的偏差,这是深度学习随机性质的结果。然而,通过投票过程(基本上就是平均它们的预测结果)结合它们的决策时,这些差异会被平滑掉,从而给出更强健的结果。

当然,训练多个模型是一项资源密集型任务,根据问题的规模和复杂性,这可能完全不可能实现。然而,这仍然是一个非常有用的工具,通过创建并结合多个相同网络的副本,可以提高预测能力。

不错吧?

另见

如果你想了解集成背后的数学原理,可以阅读这篇关于詹森不等式的文章:en.wikipedia.org/wiki/Jensen%27s_inequality

使用测试时增强提高准确性

大多数情况下,当我们测试一个网络的预测能力时,我们会使用一个测试集。这个测试集包含了模型从未见过的图像。然后,我们将它们呈现给模型,并询问模型每个图像属于哪个类别。问题是……我们只做了一次

如果我们更宽容一点,给模型多次机会去做这个任务呢?它的准确性会提高吗?嗯,往往会提高!

这种技术被称为测试时增强TTA),它是本教程的重点。

准备工作

为了加载数据集中的图像,我们需要Pillow。可以使用以下命令安装:

$> pip install Pillow

然后,下载Caltech 101数据集,地址如下:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压101_ObjectCategories.tar.gz到你选择的位置。在本食谱的其余部分中,我们将假设数据集位于~/.keras/datasets/101_ObjectCategories

这是Caltech 101中可以找到的一个示例:

![图 12.3 – Caltech 101 样本图像]

](github.com/OpenDocCN/f…)

图 12.3 – Caltech 101 样本图像

我们准备开始了!

如何操作……

按照这些步骤学习 TTA 的好处:

  1. 导入我们需要的依赖项:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义load_images_and_labels()函数,以便从Caltech 101(NumPy 格式)中读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        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)
    
  3. 定义build_model()函数,该函数基于著名的VGG架构返回一个网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. flip_augment()函数是我们TTA方案的基础。它接收一张图像,并生成其副本,这些副本可以随机水平翻转(50%的概率):

    def flip_augment(image, num_test=10):
        augmented = []
        for i in range(num_test):
            should_flip = np.random.randint(0, 2)
            if should_flip:
                flipped = np.fliplr(image.copy())
                augmented.append(flipped)
            else:
                augmented.append(image.copy())
        return np.array(augmented)
    
  5. 为了确保可重复性,请设置随机种子:

    SEED = 84
    np.random.seed(SEED)
    
  6. 编译Caltech 101图像的路径及其类别:

    base_path = (pathlib.Path.home() / '.keras' / 
                 'datasets' /'101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
                   p.split(os.path.sep)[-2] 
                   !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in 
               image_paths}
    
  7. 加载图像和标签,同时对图像进行标准化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 使用 20%的数据进行测试,剩余部分用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y, test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小和周期数:

    BATCH_SIZE = 64
    EPOCHS = 40
    
  10. 我们将随机地水平翻转训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True)
    
  11. 构建并编译网络:

    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    
  12. 拟合模型:

    train_generator = augmenter.flow(X_train, y_train,
                                     BATCH_SIZE)
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS,
              verbose=2)
    
  13. 对测试集进行预测,并利用预测结果计算模型的准确性:

    predictions = model.predict(X_test,
                                batch_size=BATCH_SIZE)
    accuracy = accuracy_score(y_test.argmax(axis=1), 
                              predictions.argmax(axis=1))
    print(f'Accuracy, without TTA: {accuracy}')
    
  14. 现在,我们将在测试集上使用TTA。我们将把每个图像副本的预测结果存储在预测列表中。我们将创建每个图像的 10 个副本:

    predictions = []
    NUM_TEST = 10
    
  15. 接下来,我们将对测试集中的每个图像进行迭代,创建其副本的批次,并将其传递通过模型:

    for index in range(len(X_test)):
        batch = flip_augment(X_test[index], NUM_TEST)
        sample_predictions = model.predict(batch)
    
  16. 每张图像的最终预测将是该批次中预测最多的类别:

        sample_predictions = np.argmax(
            np.sum(sample_predictions, axis=0))
        predictions.append(sample_predictions)
    
  17. 最后,我们将使用 TTA 计算模型预测的准确性:

    accuracy = accuracy_score(y_test.argmax(axis=1), 
                              predictions)
    print(f'Accuracy with TTA: {accuracy}')
    

    稍等片刻,我们将看到类似于这些的结果:

    Accuracy, without TTA: 0.6440092165898618
    Accuracy with TTA: 0.6532258064516129
    

网络在没有 TTA 的情况下达到 64.4%的准确率,而如果我们给模型更多机会生成正确预测,准确率将提高到 65.3%。很酷,对吧?

让我们进入*如何工作……*部分。

如何工作……

在这个示例中,我们学习到测试时增强是一种简单的技术,一旦网络训练完成,只需要做少量修改。其背后的原因是,如果我们在测试集中给网络呈现一些与训练时图像相似的变化图像,网络的表现会更好。

然而,关键在于,这些变换应当在评估阶段进行,且应该与训练期间的变换相匹配;否则,我们将向模型输入不一致的数据!

但是有一个警告:TTA 实际上非常非常慢!毕竟,我们是在将测试集的大小乘以增强因子,在我们的例子中是 10。这意味着网络不再一次处理一张图像,而是必须处理 10 张图像。

当然,TTA 不适合实时或速度受限的应用,但当时间或速度不是问题时,它仍然可以非常有用。

使用 rank-N 准确度来评估性能

大多数时候,当我们训练基于深度学习的图像分类器时,我们关心的是准确度,它是模型性能的一个二元度量,基于模型的预测与真实标签之间的一对一比较。当模型说照片中有一只 时,照片中真的有 吗?换句话说,我们衡量的是模型的 精确度

然而,对于更复杂的数据集,这种评估网络学习的方法可能适得其反,甚至不公平,因为它过于限制。假如模型没有将图片中的猫科动物识别为 ,而是误识别为 老虎 呢?更重要的是,如果第二可能性最大的类别确实是 呢?这意味着模型还需要进一步学习,但它正在逐步接近目标!这很有价值!

这就是 rank-N 准确度 的原理,它是一种更宽容、更公平的评估预测模型性能的方法,当真实标签出现在模型输出的前 N 个最可能类别中时,该预测会被视为正确。在本教程中,我们将学习如何实现并使用这种方法。

让我们开始吧。

准备工作

安装 Pillow

$> pip install Pillow

接下来,下载并解压 Caltech 101 数据集,数据集可以在这里找到:www.vision.caltech.edu/Image_Datasets/Caltech101/。确保点击下载 101_ObjectCategories.tar.gz 文件。下载后,将其放在你选择的位置。在本教程的后续部分,我们将假设数据集位于 ~/.keras/datasets/101_ObjectCategories 目录下。

这是 Caltech 101 的一个样本:

图 12.4 – Caltech 101 样本图像

图 12.4 – Caltech 101 样本图像

让我们开始实现这个教程吧!

如何实现…

按照以下步骤实现并使用 rank-N 准确度

  1. 导入必要的模块:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义 load_images_and_labels() 函数,用于从 Caltech 101 读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        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)
    
  3. 定义 build_model() 函数,创建一个 VGG 风格的网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 定义 rank_n() 函数,它根据预测结果和真实标签计算 rank-N 准确度。请注意,它会输出一个介于 0 和 1 之间的值,当真实标签出现在模型输出的前 N 个最可能的类别中时,才会算作“命中”或正确预测:

    def rank_n(predictions, labels, n):
        score = 0.0
        for prediction, actual in zip(predictions, labels):
            prediction = np.argsort(prediction)[::-1]
            if actual in prediction[:n]:
                score += 1
        return score / float(len(predictions))
    
  5. 为了可复现性,设置随机种子:

    SEED = 42
    np.random.seed(SEED)
    
  6. 编译 Caltech 101 的图像路径,以及它的类别:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets' /
                 '101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
            p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  7. 加载图像和标签,同时对图像进行归一化处理并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  8. 将 20%的数据用于测试,剩下的用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                      random_state=SEED)
    
  9. 定义批次大小和训练轮数:

    BATCH_SIZE = 64
    EPOCHS = 40
    
  10. 定义一个ImageDataGenerator(),以随机翻转、旋转和其他转换来增强训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  11. 构建并编译网络:

    model = build_network(64, 64, 3, len(CLASSES))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    
  12. 拟合模型:

    train_generator = augmenter.flow(X_train, y_train,
                                     BATCH_SIZE)
    model.fit(train_generator,
              steps_per_epoch=len(X_train) // BATCH_SIZE,
              validation_data=(X_test, y_test),
              epochs=EPOCHS,
              verbose=2)
    
  13. 在测试集上进行预测:

    predictions = model.predict(X_test, 
                                batch_size=BATCH_SIZE)
    
  14. 计算排名-1(常规准确率)、排名-3、排名-5 和排名-10 的准确率:

    y_test = y_test.argmax(axis=1)
    for n in [1, 3, 5, 10]:
        rank_n_accuracy = rank_n(predictions, y_test, n=n) * 100
        print(f'Rank-{n}: {rank_n_accuracy:.2f}%')
    

    以下是结果:

    Rank-1: 64.29%
    Rank-3: 78.05%
    Rank-5: 83.01%
    Rank-10: 89.69%
    

在这里,我们可以观察到,64.29%的时间,网络产生了完全匹配的结果。然而,78.05%的时间,正确的预测出现在前 3 名,83.01%的时间出现在前 5 名,几乎 90%的时间出现在前 10 名。这些结果相当有趣且令人鼓舞,考虑到我们的数据集包含了 101 个彼此差异很大的类别。

我们将在*如何工作...*部分深入探讨。

如何工作…

在本教程中,我们了解了排名-N 准确率的存在和实用性。我们还通过一个简单的函数rank_n()实现了它,并在一个已经训练过具有挑战性的Caltech-101数据集的网络上进行了测试。

排名-N,尤其是排名-1 和排名-5 的准确率,在训练过大型且具有挑战性数据集的网络文献中很常见,例如 COCO 或 ImageNet,这些数据集即使人类也很难区分不同类别。它在我们有细粒度类别且这些类别共享一个共同的父类或祖先时尤为有用,例如巴哥犬金毛猎犬,它们都是的品种。

排名-N 之所以有意义,是因为一个训练良好的模型,能够真正学会泛化,将在其前 N 个预测中产生语境上相似的类别(通常是前 5 名)。

当然,我们也可以把排名-N 准确率使用得过头,直到它失去意义和实用性。例如,在MNIST数据集上进行排名-5 准确率评估,该数据集仅包含 10 个类别,这几乎是没有意义的。

另见

想看到排名-N 在实际应用中的效果吗?请查看这篇论文的结果部分:arxiv.org/pdf/1610.02357.pdf

使用标签平滑提高性能

机器学习中我们必须不断应对的一个常见问题是过拟合。我们可以使用多种技术来防止模型丧失泛化能力,例如 dropout、L1 和 L2 正则化,甚至数据增强。最近加入这个组的一个新技术是标签平滑,它是独热编码的一个更宽容的替代方案。

而在独热编码中,我们通过二进制向量表示每个类别,其中唯一非零元素对应被编码的类别,使用标签平滑时,我们将每个标签表示为一个概率分布,其中所有元素都有非零的概率。最高概率对应的类别,当然是与编码类别相符的。

例如,平滑后的* [0, 1, 0] 向量会变成 [0.01, 0.98, 0.01] *。

在这个教程中,我们将学习如何使用标签平滑。继续阅读!

准备就绪

安装Pillow,我们需要它来处理数据集中的图像:

$> pip install Pillow

访问Caltech 101官网:www.vision.caltech.edu/Image_Datasets/Caltech101/。下载并解压名为101_ObjectCategories.tar.gz的文件到你喜欢的目录。从现在开始,我们假设数据位于~/.keras/datasets/101_ObjectCategories

这里是Caltech 101的一个样本:

图 12.5 – Caltech 101 样本图像

图 12.5 – Caltech 101 样本图像

让我们开始吧!

如何操作…

按照以下步骤完成这个教程:

  1. 导入必要的依赖项:

    import os
    import pathlib
    from glob import glob
    import numpy as np
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras import Model
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import CategoricalCrossentropy
    from tensorflow.keras.preprocessing.image import *
    
  2. 创建load_images_and_labels()函数,用来从Caltech 101读取数据:

    def load_images_and_labels(image_paths, 
                               target_size=(64, 64)):
        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)
    
  3. 实现build_model()函数,创建一个基于VGG的网络:

    def build_network(width, height, depth, classes):
        input_layer = Input(shape=(width, height, depth))
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(input_layer)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=32,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Conv2D(filters=64,
                   kernel_size=(3, 3),
                   padding='same')(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = MaxPooling2D(pool_size=(2, 2))(x)
        x = Dropout(rate=0.25)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=512)(x)
        x = ReLU()(x)
        x = BatchNormalization(axis=-1)(x)
        x = Dropout(rate=0.25)(x)
        x = Dense(units=classes)(x)
        output = Softmax()(x)
        return Model(input_layer, output)
    
  4. 设置一个随机种子以增强可复现性:

    SEED = 9
    np.random.seed(SEED)
    
  5. 编译Caltech 101的图像路径以及其类别:

    base_path = (pathlib.Path.home() / '.keras' / 'datasets'         
                  /'101_ObjectCategories')
    images_pattern = str(base_path / '*' / '*.jpg')
    image_paths = [*glob(images_pattern)]
    image_paths = [p for p in image_paths if
            p.split(os.path.sep)[-2] !='BACKGROUND_Google']
    CLASSES = {p.split(os.path.sep)[-2] for p in image_paths}
    
  6. 加载图像和标签,同时对图像进行归一化,并对标签进行独热编码:

    X, y = load_images_and_labels(image_paths)
    X = X.astype('float') / 255.0
    y = LabelBinarizer().fit_transform(y)
    
  7. 使用 20%的数据作为测试数据,其余数据用于训练模型:

    (X_train, X_test,
     y_train, y_test) = train_test_split(X, y,
                                         test_size=0.2,
                                       random_state=SEED)
    
  8. 定义批次大小和训练轮次:

    BATCH_SIZE = 128
    EPOCHS = 40
    
  9. 定义一个ImageDataGenerator()来通过随机翻转、旋转和其他变换增强训练集中的图像:

    augmenter = ImageDataGenerator(horizontal_flip=True,
                                   rotation_range=30,
                                   width_shift_range=0.1,
                                   height_shift_range=0.1,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   fill_mode='nearest')
    
  10. 我们将训练两个模型:一个使用标签平滑,另一个不使用。这将让我们比较它们的表现,评估标签平滑是否对性能有影响。两个案例的逻辑几乎相同,从模型创建过程开始:

    for with_label_smoothing in [False, True]:
        model = build_network(64, 64, 3, len(CLASSES))
    
  11. 如果with_label_smoothingTrue,则我们将平滑因子设置为 0.1。否则,平滑因子为 0,这意味着我们将使用常规的独热编码:

        if with_label_smoothing:
            factor = 0.1
        else:
            factor = 0
    
  12. 我们应用损失函数——在这种情况下是CategoricalCrossentropy()

        loss = CategoricalCrossentropy(label_smoothing=factor)
    
  13. 编译并训练模型:

        model.compile(loss=loss,
                      optimizer='rmsprop',
                      metrics=['accuracy'])
        train_generator = augmenter.flow(X_train, y_train,
                                         BATCH_SIZE)
        model.fit(train_generator,
                  steps_per_epoch=len(X_train) // 
                  BATCH_SIZE,
                  validation_data=(X_test, y_test),
                  epochs=EPOCHS,
                  verbose=2)
    
  14. 在测试集上进行预测并计算准确率:

        predictions = model.predict(X_test, 
                                   batch_size=BATCH_SIZE)
        accuracy = accuracy_score(y_test.argmax(axis=1),
                                  predictions.argmax(axis=1))
        print(f'Test accuracy '
              f'{"with" if with_label_smoothing else 
               "without"} '
              f'label smoothing: {accuracy * 100:.2f}%')
    

    脚本将训练两个模型:一个不使用损失函数。以下是结果:

    Test accuracy without label smoothing: 65.09%
    Test accuracy with label smoothing: 65.78%
    

仅仅通过使用标签平滑,我们提高了测试分数接近 0.7%,这是一个不可忽视的提升,考虑到我们的数据集大小和复杂性。在下一部分我们将深入探讨。

它是如何工作的……

在这个教程中,我们学习了如何应用CategoricalCrossentropy()损失函数,用于衡量网络的学习效果。

那么,为什么标签平滑有效呢?尽管在许多深度学习领域中被广泛应用,包括自然语言处理NLP)和当然的计算机视觉标签平滑仍然没有被完全理解。然而,许多人(包括我们在这个示例中的研究者)观察到,通过软化目标,网络的泛化能力和学习速度通常显著提高,防止其过于自信,从而保护我们免受过拟合的负面影响。

要了解有关标签平滑的有趣见解,请阅读另见部分提到的论文。

另见

这篇论文探讨了标签平滑有效的原因,以及它何时无效。值得一读!你可以在这里下载:arxiv.org/abs/1906.02629

检查点模型

训练一个深度神经网络是一个耗时且消耗存储和资源的过程。每次我们想要使用网络时重新训练它是不合理且不切实际的。好消息是,我们可以使用一种机制,在训练过程中自动保存网络的最佳版本。

在这个教程中,我们将讨论一种机制,称为检查点。

如何做……

按照以下步骤了解您在 TensorFlow 中可以使用的不同检查点方式:

  1. 导入我们将使用的模块:

    import numpy as np
    import tensorflow as tf
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import LabelBinarizer
    from tensorflow.keras.callbacks import ModelCheckpoint
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.layers import *
    from tensorflow.keras.models import *
    
  2. 定义一个函数,将Fashion-MNIST加载到tf.data.Datasets中:

    def load_dataset():
        (X_train, y_train), (X_test, y_test) = fm.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=3)
        X_test = np.expand_dims(X_test, axis=3)
        label_binarizer = LabelBinarizer()
        y_train = label_binarizer.fit_transform(y_train)
        y_test = label_binarizer.fit_transform(y_test)
    
  3. 使用 20% 的训练数据来验证数据集:

        (X_train, X_val,
         y_train, y_val) = train_test_split(X_train, y_train,
                                            train_size=0.8)
    
  4. 将训练集、测试集和验证集转换为tf.data.Datasets

        train_ds = (tf.data.Dataset
                    .from_tensor_slices((X_train, 
                                         y_train)))
        val_ds = (tf.data.Dataset
                  .from_tensor_slices((X_val, y_val)))
        test_ds = (tf.data.Dataset
                   .from_tensor_slices((X_test, y_test)))
        train_ds = (train_ds.shuffle(buffer_size=BUFFER_SIZE)
                    .batch(BATCH_SIZE)
                    .prefetch(buffer_size=BUFFER_SIZE))
        val_ds = (val_ds
                  .batch(BATCH_SIZE)
                  .prefetch(buffer_size=BUFFER_SIZE))
        test_ds = test_ds.batch(BATCH_SIZE)
        return train_ds, val_ds, test_ds
    
  5. 定义build_network()方法,顾名思义,它创建我们将在Fashion-MNIST上训练的模型:

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        x = Conv2D(filters=20,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Conv2D(filters=50,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.5)(x)
        x = Dense(10)(x)
        output = Softmax()(x)
        return Model(inputs=input_layer, outputs=output)
    
  6. 定义train_and_checkpoint()函数,它加载数据集,然后根据checkpointer参数中设定的逻辑构建、编译并训练网络,同时保存检查点。

    def train_and_checkpoint(checkpointer):
        train_dataset, val_dataset, test_dataset = load_dataset()
    
        model = build_network()
        model.compile(loss='categorical_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
        model.fit(train_dataset,
                  validation_data=val_dataset,
                  epochs=EPOCHS,
                  callbacks=[checkpointer])
    
  7. 定义批量大小、训练模型的轮次数以及每个数据子集的缓冲区大小:

    BATCH_SIZE = 256
    BUFFER_SIZE = 1024
    EPOCHS = 100
    
  8. 生成检查点的第一种方式是每次迭代后保存一个不同的模型。为此,我们必须将save_best_only=False传递给ModelCheckpoint()

    checkpoint_pattern = (
        'save_all/model-ep{epoch:03d}-loss{loss:.3f}'
        '-val_loss{val_loss:.3f}.h5')
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=False,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    注意,我们将所有的检查点保存在save_all文件夹中,检查点模型名称中包含了轮次、损失和验证损失。

  9. 一种更高效的检查点方式是仅保存迄今为止最好的模型。我们可以通过在ModelCheckpoint()中将save_best_only设置为True来实现:

    checkpoint_pattern = (
        'best_only/model-ep{epoch:03d}-loss{loss:.3f}'
        '-val_loss{val_loss:.3f}.h5')
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=True,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    我们将把结果保存在best_only目录中。

  10. 一种更简洁的生成检查点的方式是只保存一个与当前最佳模型相对应的检查点,而不是存储每个逐步改进的模型。为了实现这一点,我们可以从检查点名称中删除任何参数:

    checkpoint_pattern = 'overwrite/model.h5'
    checkpoint = ModelCheckpoint(checkpoint_pattern,
                                 monitor='val_loss',
                                 verbose=1,
                                 save_best_only=True,
                                 mode='min')
    train_and_checkpoint(checkpoint)
    

    运行这三个实验后,我们可以检查每个输出文件夹,看看生成了多少个检查点。在第一个实验中,我们在每个 epoch 后保存了一个模型,如下图所示:

图 12.6 – 实验 1 结果

图 12.6 – 实验 1 结果

这种方法的缺点是,我们会得到很多无用的快照。优点是,如果需要的话,我们可以通过加载相应的 epoch 恢复训练。更好的方法是只保存到目前为止最好的模型,正如以下截图所示,这样生成的模型会更少。通过检查检查点名称,我们可以看到每个检查点的验证损失都低于前一个:

图 12.7 – 实验 2 结果

图 12.7 – 实验 2 结果

最后,我们可以只保存最好的模型,如下图所示:

图 12.8 – 实验 3 结果

图 12.8 – 实验 3 结果

让我们继续进入下一部分。

它是如何工作的……

在这个教程中,我们学习了如何进行模型检查点保存,这为我们节省了大量时间,因为我们不需要从头开始重新训练模型。检查点保存非常棒,因为我们可以根据自己的标准保存最好的模型,例如验证损失、训练准确度或任何其他度量标准。

通过利用 ModelCheckpoint() 回调,我们可以在每个完成的 epoch 后保存网络的快照,从而仅保留最好的模型或训练过程中产生的最佳模型历史。

每种策略都有其优缺点。例如,在每个 epoch 后生成模型的好处是我们可以从任何 epoch 恢复训练,但这会占用大量磁盘空间,而仅保存最好的模型则能节省空间,但会降低我们的实验灵活性。

你将在下一个项目中使用什么策略?

使用 tf.GradientTape 自定义训练过程

TensorFlow 的最大竞争对手之一是另一个著名框架:PyTorch。直到 TensorFlow 2.x 发布之前,PyTorch 的吸引力在于它给用户提供的控制程度,尤其是在训练神经网络时。

如果我们正在处理一些传统的神经网络以解决常见问题,如图像分类,我们不需要对如何训练模型进行过多控制,因此可以依赖 TensorFlow(或 Keras API)的内置功能、损失函数和优化器,而没有问题。

但是,如果我们是那些探索新方法、新架构以及解决挑战性问题的新策略的研究人员呢?过去,正因为 PyTorch 在自定义训练模型方面比 TensorFlow 1.x 容易得多,我们才不得不使用它,但现在情况已经不同了!TensorFlow 2.x 的tf.GradientTape使得我们能够更加轻松地为 Keras 和低级 TensorFlow 实现的模型创建自定义训练循环,在本章节中,我们将学习如何使用它。

如何做到…

按照以下步骤完成本章节:

  1. 导入我们将使用的模块:

    import time
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.datasets import fashion_mnist as fm
    from tensorflow.keras.layers import *
    from tensorflow.keras.losses import categorical_crossentropy
    from tensorflow.keras.models import Model
    from tensorflow.keras.optimizers import RMSprop
    from tensorflow.keras.utils import to_categorical
    
  2. 定义一个函数来加载和准备Fashion-MNIST

    def load_dataset():
        (X_train, y_train), (X_test, y_test) = fm.load_data()
        X_train = X_train.astype('float32') / 255.0
        X_test = X_test.astype('float32') / 255.0
        # Reshape grayscale to include channel dimension.
        X_train = np.expand_dims(X_train, axis=-1)
        X_test = np.expand_dims(X_test, axis=-1)
        y_train = to_categorical(y_train)
        y_test = to_categorical(y_test)
        return (X_train, y_train), (X_test, y_test)
    
  3. 定义build_network()方法,顾名思义,它创建我们将在Fashion-MNIST上训练的模型:

    def build_network():
        input_layer = Input(shape=(28, 28, 1))
        x = Conv2D(filters=20,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(input_layer)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
        x = Conv2D(filters=50,
                   kernel_size=(5, 5),
                   padding='same',
                   strides=(1, 1))(x)
        x = ELU()(x)
        x = BatchNormalization()(x)
        x = MaxPooling2D(pool_size=(2, 2),
                         strides=(2, 2))(x)
        x = Dropout(0.5)(x)
    

    现在,构建网络的全连接部分:

        x = Flatten()(x)
        x = Dense(units=500)(x)
        x = ELU()(x)
        x = Dropout(0.5)(x)
        x = Dense(10)(x)
        output = Softmax()(x)
        return Model(inputs=input_layer, outputs=output)
    
  4. 为了演示如何使用tf.GradientTape,我们将实现training_step()函数,该函数获取一批数据的梯度,然后通过优化器进行反向传播:

    def training_step(X, y, model, optimizer):
        with tf.GradientTape() as tape:
            predictions = model(X)
            loss = categorical_crossentropy(y, predictions)
        gradients = tape.gradient(loss, 
                              model.trainable_variables)
        optimizer.apply_gradients(zip(gradients,
                              model.trainable_variables))
    
  5. 定义批次大小和训练模型的轮次:

    BATCH_SIZE = 256
    EPOCHS = 100
    
  6. 加载数据集:

    (X_train, y_train), (X_test, y_test) = load_dataset()
    
  7. 创建优化器和网络:

    optimizer = RMSprop()
    model = build_network()
    
  8. 现在,我们将创建自定义训练循环。首先,我们将遍历每个轮次,衡量完成的时间:

    for epoch in range(EPOCHS):
        print(f'Epoch {epoch + 1}/{EPOCHS}')
        start = time.time()
    
  9. 现在,我们将遍历每个数据批次,并将它们与网络和优化器一起传递给training_step()函数:

        for i in range(int(len(X_train) / BATCH_SIZE)):
            X_batch = X_train[i * BATCH_SIZE:
                              i * BATCH_SIZE + BATCH_SIZE]
            y_batch = y_train[i * BATCH_SIZE:
                              i * BATCH_SIZE + BATCH_SIZE]
            training_step(X_batch, y_batch, model, 
                          optimizer)
    
  10. 然后,我们将打印当前轮次的时间:

        elapsed = time.time() - start
        print(f'\tElapsed time: {elapsed:.2f} seconds.')
    
  11. 最后,在测试集上评估网络,确保它没有出现任何问题:

    model.compile(loss=categorical_crossentropy,
                  optimizer=optimizer,
                  metrics=['accuracy'])
    results = model.evaluate(X_test, y_test)
    print(f'Loss: {results[0]}, Accuracy: {results[1]}')
    

    以下是结果:

    Loss: 1.7750033140182495, Accuracy: 0.9083999991416931
    

让我们继续下一个部分。

它是如何工作的…

在本章节中,我们学习了如何创建自己的自定义训练循环。虽然我们在这个实例中没有做任何特别有趣的事情,但我们重点介绍了如何使用tf.GradientTape来“烹饪”一个自定义深度学习训练循环的组件(或者说是食材):

  • 网络架构本身

  • 用于计算模型损失的损失函数

  • 用于根据梯度更新模型权重的优化器

  • 步骤函数,它实现了前向传播(计算梯度)和反向传播(通过优化器应用梯度)

如果你想研究tf.GradientTape的更真实和吸引人的应用,可以参考第六章生成模型与对抗攻击第七章用 CNN 和 RNN 进行图像描述;以及第八章通过分割实现细粒度图像理解。不过,你也可以直接阅读下一篇章节,在那里我们将学习如何可视化类别激活图来调试深度神经网络!

可视化类别激活图,以更好地理解你的网络

尽管深度神经网络具有无可争议的强大能力和实用性,但关于它们的最大抱怨之一就是它们的神秘性。大多数时候,我们将它们视为黑箱,知道它们能工作,但不知道为什么。

特别是,真正具有挑战性的是解释为什么一个网络会得到特定的结果,哪些神经元被激活了,为什么会被激活,或者网络在看哪里,以确定图像中物体的类别或性质。

换句话说,我们如何信任我们不理解的东西?如果它坏了,我们又该如何改进或修复它?

幸运的是,在这个配方中,我们将研究一种新方法,来揭示这些话题,称为梯度加权类激活映射,或简称Grad-CAM

准备好了吗?我们开始吧!

准备工作

对于这个配方,我们需要OpenCVPillowimutils。你可以像这样一次性安装它们:

$> pip install Pillow opencv-python imutils

现在,我们准备好实现这个配方了。

如何实现…

按照这些步骤完成这个配方:

  1. 导入我们将要使用的模块:

    import cv2
    import imutils 
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.applications import *
    from tensorflow.keras.models import Model
    from tensorflow.keras.preprocessing.image import *
    
  2. 定义GradCAM类,它将封装Grad-CAM算法,使我们能够生成给定层的激活图热力图。让我们从定义构造函数开始:

    class GradGAM(object):
        def __init__(self, model, class_index, 
                     layer_name=None):
            self.class_index = class_index
            if layer_name is None:
                for layer in reversed(model.layers):
                    if len(layer.output_shape) == 4:
                        layer_name = layer.name
                        break
            self.grad_model = 
                      self._create_grad_model(model,
    
                                           layer_name)
    
  3. 在这里,我们接收的是我们想要检查的类的class_index,以及我们希望可视化其激活的层的layer_name。如果我们没有接收到layer_name,我们将默认使用模型的最外层输出层。最后,我们通过调用这里定义的_create_grad_model()方法创建grad_model

        def _create_grad_model(self, model, layer_name):
            return Model(inputs=[model.inputs],
                         outputs=[
                           model.get_layer(layer_name).
                              output,model.output])
    

    这个模型与model的输入相同,但输出既包含兴趣层的激活,也包含model本身的预测。

  4. 接下来,我们必须定义compute_heatmap()方法。首先,我们需要将输入图像传递给grad_model,以获取兴趣层的激活图和预测:

        def compute_heatmap(self, image, epsilon=1e-8):
            with tf.GradientTape() as tape:
                inputs = tf.cast(image, tf.float32)
                conv_outputs, preds = self.grad_model(inputs)
                loss = preds[:, self.class_index]
    
  5. 我们可以根据与class_index对应的损失来计算梯度:

    grads = tape.gradient(loss, conv_outputs)
    
  6. 我们可以通过基本上在float_conv_outputsfloat_grads中找到正值并将其与梯度相乘来计算引导梯度,这样我们就可以可视化哪些神经元正在激活:

            guided_grads = (tf.cast(conv_outputs > 0, 
                          'float32') *
                            tf.cast(grads > 0, 'float32') *
                            grads)
    
  7. 现在,我们可以通过平均引导梯度来计算梯度权重,然后使用这些权重将加权映射添加到我们的Grad-CAM可视化中:

            conv_outputs = conv_outputs[0]
            guided_grads = guided_grads[0]
            weights = tf.reduce_mean(guided_grads, 
                                     axis=(0, 1))
            cam = tf.reduce_sum(
                tf.multiply(weights, conv_outputs),
                axis=-1)
    
  8. 然后,我们将Grad-CAM可视化结果调整为输入图像的尺寸,进行最小-最大归一化后再返回:

            height, width = image.shape[1:3]
            heatmap = cv2.resize(cam.numpy(), (width, 
                                  height))
            min = heatmap.min()
            max = heatmap.max()
            heatmap = (heatmap - min) / ((max - min) + 
                                                   epsilon)
            heatmap = (heatmap * 255.0).astype('uint8')
            return heatmap
    
  9. GradCAM类的最后一个方法将热力图叠加到原始图像上。这使我们能够更好地了解网络在做出预测时注视的视觉线索:

        def overlay_heatmap(self,
                            heatmap,
                            image, alpha=0.5,
                            colormap=cv2.COLORMAP_VIRIDIS):
            heatmap = cv2.applyColorMap(heatmap, colormap)
            output = cv2.addWeighted(image,
                                     alpha,
                                     heatmap,
                                     1 - alpha,
                                     0)
            return heatmap, output
    
  10. 让我们实例化一个在 ImageNet 上训练过的ResNet50模型:

    model = ResNet50(weights='imagenet')
    
  11. 加载输入图像,将其调整为 ResNet50 所期望的尺寸,将其转换为 NumPy 数组,并进行预处理:

    image = load_img('dog.jpg', target_size=(224, 224))
    image = img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image = imagenet_utils.preprocess_input(image)
    
  12. 将图像通过模型并提取最可能类别的索引:

    predictions = model.predict(image)
    i = np.argmax(predictions[0])
    
  13. 实例化一个GradCAM对象并计算热力图:

    cam = GradGAM(model, i)
    heatmap = cam.compute_heatmap(image)
    
  14. 将热力图叠加在原始图像上:

    original_image = cv2.imread('dog.jpg')
    heatmap = cv2.resize(heatmap, (original_image.shape[1],
                                   original_image.shape[0]))
    heatmap, output = cam.overlay_heatmap(heatmap, 
                                          original_image,
                                          alpha=0.5)
    
  15. 解码预测结果,使其可供人类读取:

    decoded = imagenet_utils.decode_predictions(predictions)
    _, label, probability = decoded[0][0]
    
  16. 用类别及其关联的概率标注覆盖的热力图:

    cv2.rectangle(output, (0, 0), (340, 40), (0, 0, 0), -1)
    cv2.putText(output, f'{label}: {probability * 100:.2f}%',
                (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.8,
                (255, 255, 255), 2)
    
  17. 最后,将原始图像、热力图和标注覆盖层合并为一张图像并保存到磁盘:

    output = np.hstack([original_image, heatmap, output])
    output = imutils.resize(output, height=700)
    cv2.imwrite('output.jpg', output)
    

这是结果:

图 12.9 – Grad-CAM 可视化

图 12.9 – Grad-CAM 可视化

如我们所见,网络将我的狗分类为巴哥犬,这是正确的,置信度为 85.03%。此外,热力图显示网络在我的狗的鼻子和眼睛周围激活,这意味着这些是重要特征,模型的表现符合预期。

它是如何工作的……

在这个案例中,我们学习并实现了Grad-CAM,这是一种非常有用的算法,用于可视化检查神经网络的激活情况。这是调试网络行为的有效方法,它确保网络关注图像的正确部分。

这是一个非常重要的工具,因为我们模型的高准确率或性能可能与实际学习的关系不大,而与一些未考虑到的因素有更多关系。例如,如果我们正在开发一个宠物分类器来区分狗和猫,我们应该使用Grad-CAM来验证网络是否关注这些动物固有的特征,以便正确分类,而不是关注周围环境、背景噪音或图像中的次要元素。

另见

你可以通过阅读以下论文来扩展你对Grad-CAM的知识:arxiv.org/abs/1610.02391