计算机视觉

1、卷积分类器

欢迎来到计算机视觉!

你有没有想过教电脑看东西?在本课程中,这正是你要做的!

在本课程中,您将:

使用现代深度学习网络构建具有Keras的图像分类器

使用可重用块设计您自己的定制convnet

学习视觉特征提取背后的基本思想

掌握迁移学习的艺术,提升你的模型

利用数据扩充扩展数据集

如果你参加了深度学习导论课程,你就会知道成功所需的一切。

现在让我们开始吧!

介绍

本课程将向您介绍计算机视觉的基本概念。我们的目标是学习神经网络如何能够很好地“理解”自然图像,从而解决人类视觉系统能够解决的相同类型的问题。

最擅长这项工作的神经网络称为卷积神经网络(有时我们称之为convnet或CNN)。卷积是一种数学运算,它赋予了convnet的各个层独特的结构。在以后的课程中,您将了解为什么此结构在解决计算机视觉问题时如此有效。

我们将把这些想法应用到图像分类问题中:给定一张图片,我们能训练计算机告诉我们它是什么图片吗?你可能见过一些应用程序,可以从照片中识别出一种植物。这是一个图像分类器!在本课程中,您将学习如何构建与专业应用程序中使用的图像分类器一样强大的图像分类器。

虽然我们的重点是图像分类,但您在本课程中学习的内容与各种计算机视觉问题相关。最后,您将准备好进入更高级的应用程序,如生成性对抗网络和图像分割。

卷积分类器

用于图像分类的convnet由两部分组成:卷积基和稠密头。

convnet的组成部分:映像、基、头、类;输入、提取、分类、输出。

基底用于从图像中提取特征。它主要由执行卷积运算的层组成,但通常也包括其他类型的层(您将在下一课中了解这些。)

头部用于确定图像的类别。它主要由致密层构成,但也可能包括其他层,如脱落层。

我们所说的视觉特征是什么意思?特征可以是线条、颜色、纹理、形状、图案,也可以是一些复杂的组合。

整个过程是这样的:

训练分类器

培训期间网络的目标是学习两件事:

要从图像(基础)中提取哪些特征,

哪一类与哪些功能(头部)相匹配。

如今,康维特很少从零开始训练。更常见的是,我们重用预训练模型的基础。在训练前的基地,我们再加上一个未经训练的脑袋。换句话说,我们重用网络中已经学会做的部分1.提取特征,并附加一些新的层来学习2.分类。

因为头部通常只有几个密集的层,所以可以从相对较少的数据中创建非常精确的分类器。

重用预先训练好的模型是一种被称为迁移学习的技术。它是如此有效,以至于现在几乎每个图像分类器都会使用它。

示例-训练Convnet分类器

在本课程中,我们将创建分类器,试图解决以下问题:这是一张汽车还是卡车的图片?我们的数据集是大约10000张各种汽车的图片,大约一半是汽车,一半是卡车。

步骤1-加载数据

下一个隐藏单元将导入一些库并设置我们的数据管道。我们有一个名为ds_train的训练分割和一个名为ds_valid的验证分割。

# 进口
import os, warnings
import matplotlib.pyplot as plt
from matplotlib import gridspec

import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing import image_dataset_from_directory

# 可玩性
def set_seed(seed=31415):
    np.random.seed(seed)
    tf.random.set_seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    os.environ['TF_DETERMINISTIC_OPS'] = '1'
set_seed(31415)

# 设置Matplotlib默认值
plt.rc('figure', autolayout=True)
plt.rc('axes', labelweight='bold', labelsize='large',
       titleweight='bold', titlesize=18, titlepad=10)
plt.rc('image', cmap='magma')
warnings.filterwarnings("ignore") # to clean up output cells


# 加载训练和验证集
ds_train_ = image_dataset_from_directory(
    '../input/car-or-truck/train',
    labels='inferred',
    label_mode='binary',
    image_size=[128, 128],
    interpolation='nearest',
    batch_size=64,
    shuffle=True,
)
ds_valid_ = image_dataset_from_directory(
    '../input/car-or-truck/valid',
    labels='inferred',
    label_mode='binary',
    image_size=[128, 128],
    interpolation='nearest',
    batch_size=64,
    shuffle=False,
)

# 数据管道
def convert_to_float(image, label):
    image = tf.image.convert_image_dtype(image, dtype=tf.float32)
    return image, label

AUTOTUNE = tf.data.experimental.AUTOTUNE
ds_train = (
    ds_train_
    .map(convert_to_float)
    .cache()
    .prefetch(buffer_size=AUTOTUNE)
)
ds_valid = (
    ds_valid_
    .map(convert_to_float)
    .cache()
    .prefetch(buffer_size=AUTOTUNE)
)

让我们看看训练集中的几个例子。

import matplotlib.pyplot as plt

第2步-定义预训练基础

最常用的预训练数据集是ImageNet,这是一个包含多种自然图像的大型数据集。Keras在其应用程序模块中包括在ImageNet上预训练的各种模型。我们将使用的预训练模型称为VGG16。

pretrained_base = tf.keras.models.load_model(
    '../input/cv-course-models/cv-course-models/vgg16-pretrained-base',
)
pretrained_base.trainable = False

步骤3-连接头

接下来,我们附加分类器头部。在本例中,我们将使用一层隐藏单元(第一个密集层),然后是一层,将输出转换为类1的概率分数,卡车。扁平层将底座的二维输出转换为头部所需的一维输入。

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    pretrained_base,
    layers.Flatten(),
    layers.Dense(6, activation='relu'),
    layers.Dense(1, activation='sigmoid'),
])

第4步-培训

最后,让我们训练模型。由于这是一个两类问题,我们将使用交叉熵和精度的二进制版本。adam优化器通常性能良好,因此我们也将选择它。

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['binary_accuracy'],
)

history = model.fit(
    ds_train,
    validation_data=ds_valid,
    epochs=30,
    verbose=0,
)

训练神经网络时,最好检查损失和度量图。历史对象在字典history.history中包含此信息。我们可以使用Pandas将此字典转换为数据帧,并使用内置方法绘制它。

import pandas as pd

history_frame = pd.DataFrame(history.history)
history_frame.loc[:, ['loss', 'val_loss']].plot()
history_frame.loc[:, ['binary_accuracy', 'val_binary_accuracy']].plot();

结论

在本课中,我们了解了convnet分类器的结构:在执行特征提取的基础上作为分类器的头部。

从本质上讲,head是一个普通的分类器,就像您在入门课程中学习的那样。对于特征,它使用由基础提取的特征。这是卷积分类器背后的基本思想:我们可以将执行特征工程的单元附加到分类器本身。

这是深度神经网络相对于传统机器学习模型的一大优势:给定正确的网络结构,深度神经网络可以学习如何设计解决问题所需的特征。

在接下来的几节课中,我们将了解卷积基是如何完成特征提取的。然后,您将学习如何应用这些想法并设计一些自己的分类器。

2、卷积和ReLU

import numpy as np
from itertools import product

def show_kernel(kernel, label=True, digits=None, text_size=28):
    # Format kernel
    kernel = np.array(kernel)
    if digits is not None:
        kernel = kernel.round(digits)

    # 绘图内核
    cmap = plt.get_cmap('Blues_r')
    plt.imshow(kernel, cmap=cmap)
    rows, cols = kernel.shape
    thresh = (kernel.max()+kernel.min())/2
    # (可选)添加值标签
    if label:
        for i, j in product(range(rows), range(cols)):
            val = kernel[i, j]
            color = cmap(0) if val > thresh else cmap(255)
            plt.text(j, i, val, 
                     color=color, size=text_size,
                     horizontalalignment='center', verticalalignment='center')
    plt.xticks([])
    plt.yticks([])

介绍

在上一课中,我们看到卷积分类器有两部分:卷积基和密集层的头部。我们了解到,基地的工作是从图像中提取视觉特征,然后头部将使用这些特征对图像进行分类。

在接下来的几节课中,我们将学习两种最重要的层类型,这两种层通常位于卷积图像分类器的基础上。这些是具有ReLU激活的卷积层和最大池层。在第5课中,您将学习如何通过将这些层组合成执行特征提取的块来设计自己的convnet。

本课程介绍卷积层及其ReLU激活函数。

特征提取

在我们深入了解卷积的细节之前,让我们先讨论一下网络中这些层的用途。我们将看到如何使用这三个操作(卷积、ReLU和最大池)来实现特征提取过程。

由base执行的特征提取包括三个基本操作:

针对特定特征过滤图像(卷积)

在过滤图像(ReLU)中检测该功能

压缩图像以增强功能(最大池)

下图说明了这个过程。您可以看到这三个操作如何隔离原始图像的某些特定特征(在本例中为水平线)。

通常,网络将在单个图像上并行执行多个提取。在现代ConvNet中,底部的最后一层生成1000多个独特的视觉特征并不少见。

卷积滤波器

卷积层执行滤波步骤。您可以在Keras模型中定义卷积层,如下所示:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Conv2D(filters=64, kernel_size=3), # activation is None
    # More layers follow
])
We can understand these parameters by looking at their relationship to the weights and activations of the layer. Let's do that now.

Weights
The weights a convnet learns during training are primarily contained in its convolutional layers. These weights we call kernels. We can represent them as small arrays:

内核通过扫描图像并产生像素值的加权和进行操作。通过这种方式,内核将像偏振光透镜一样,强调或不强调某些信息模式。

内核定义了卷积层如何连接到后面的层。上面的内核将输出中的每个神经元连接到输入中的九个神经元。通过使用kernel_size设置内核的维度,您可以告诉convnet如何形成这些连接。大多数情况下,内核都有奇数维,比如内核大小=(3,3)或(5,5),因此一个像素位于中心,但这不是必需的。

卷积层中的内核决定了它创建的特征类型。在培训期间,convnet尝试了解解决分类问题所需的功能。这意味着为内核找到最佳值。

激活

我们称之为特征映射的网络中的激活。它们是我们对图像应用过滤器时的结果;它们包含内核提取的视觉特性。下面是一些内核的图片和它们生成的特性图。

三个内核及其生成的特征映射。

内核和特性。

从内核中的数字模式可以看出它创建的特征映射的类型。通常,卷积在其输入中强调的内容将与内核中正数的形状相匹配。上面的左核和中核都将过滤水平形状。

使用filters参数,可以告诉卷积层希望它创建多少个特征贴图作为输出。

用ReLU检测

过滤后,特征映射通过激活功能。整流器功能有如下图表:

整流函数的图形看起来像一条负部分“整流”为0的线。

连接整流器的神经元称为整流线性单元。因此,我们也可以将整流器功能称为ReLU激活,甚至称为ReLU功能。

ReLU激活可以在其自己的激活层中定义,但通常您只将其作为Conv2D的激活函数。

model = keras.Sequential([
    layers.Conv2D(filters=64, kernel_size=3, activation='relu')
    # More layers follow
])

您可以将激活函数视为根据某种重要性度量对像素值进行评分。ReLU激活表示负值不重要,因此将其设置为0。(“一切不重要的都同样不重要。”)

这里是ReLU应用于上面的特征映射。注意它是如何成功地隔离特性的。

与其他激活函数一样,ReLU函数是非线性的。从本质上说,这意味着网络中所有层的总体效果与仅将这些效果相加得到的效果不同,这与仅使用单个层可以实现的效果相同。非线性确保了功能在深入网络时以有趣的方式组合(我们将在第5课中进一步探讨此“功能组合”。)

示例-应用卷积和ReLU

在本例中,我们将自己进行提取,以便更好地理解卷积网络在“幕后”做什么。

下面是我们将用于此示例的图像:

import tensorflow as tf
import matplotlib.pyplot as plt
plt.rc('figure', autolayout=True)
plt.rc('axes', labelweight='bold', labelsize='large',
       titleweight='bold', titlesize=18, titlepad=10)
plt.rc('image', cmap='magma')

image_path = '../input/computer-vision-resources/car_feature.jpg'
image = tf.io.read_file(image_path)
image = tf.io.decode_jpeg(image)

plt.figure(figsize=(6, 6))
plt.imshow(tf.squeeze(image), cmap='gray')
plt.axis('off')
plt.show();

对于过滤步骤,我们将定义一个内核,然后将其与卷积一起应用。本例中的内核是“边缘检测”内核。可以用tf.constant定义它,就像用np.array在Numpy中定义数组一样。这将创建一个TensorFlow使用的张量。

import tensorflow as tf

kernel = tf.constant([
    [-1, -1, -1],
    [-1,  8, -1],
    [-1, -1, -1],
])

plt.figure(figsize=(3, 3))
show_kernel(kernel)

TensorFlow在其tf.nn模块中包含许多由神经网络执行的常见操作。我们将使用的两个是conv2d和relu。这些只是Keras层的功能版本。

下一个隐藏的单元格进行了一些重新格式化,使其与TensorFlow兼容。对于这个例子,细节并不重要。

# 重新格式化以实现批处理兼容性。
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image = tf.expand_dims(image, axis=0)
kernel = tf.reshape(kernel, [*kernel.shape, 1, 1])
kernel = tf.cast(kernel, dtype=tf.float32)

现在让我们应用内核,看看会发生什么。

image_filter = tf.nn.conv2d(
    input=image,
    filters=kernel,
    # we'll talk about these two in lesson 4!
    strides=1,
    padding='SAME',
)

plt.figure(figsize=(6, 6))
plt.imshow(tf.squeeze(image_filter))
plt.axis('off')
plt.show();

接下来是使用ReLU功能的检测步骤。这个函数比卷积要简单得多,因为它没有任何参数可设置。

image_detect = tf.nn.relu(image_filter)

plt.figure(figsize=(6, 6))
plt.imshow(tf.squeeze(image_detect))
plt.axis('off')
plt.show();

现在我们已经创建了一个功能图!像这样的图像是头部用来解决分类问题的。我们可以想象,某些特征可能更具有汽车的特征,而另一些特征则更具有卡车的特征。convnet在培训期间的任务是创建能够找到这些特性的内核。

结论

在本课程中,我们看到了convnet用于执行特征提取的前两个步骤:使用Conv2D图层进行过滤和使用relu激活进行检测。

3、最大池化

介绍

在第2课中,我们开始讨论convnet中的基如何执行特征提取。我们了解了此过程中的前两个操作是如何在具有relu激活的Conv2D层中发生的。

在本课程中,我们将了解此序列中的第三个(也是最后一个)操作:使用最大池压缩,这在Keras中由MaxPool2D层完成。

用最大池压缩

将冷凝步骤添加到我们之前的模型中,将为我们提供:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Conv2D(filters=64, kernel_size=3), # activation is None
    layers.MaxPool2D(pool_size=2),
    # 更多的层次随之而来
])

MaxPool2D层与Conv2D层非常相似,只是它使用了一个简单的最大值函数而不是内核,pool_size参数类似于kernel_size。然而,MaxPool2D层不像其内核中的卷积层那样具有任何可训练权重。

让我们再看一看上一课中的提取图。请记住,MaxPool2D是压缩步骤。

请注意,在应用ReLU函数(检测)后,特征贴图最终会出现大量“死区”,即仅包含0的大区域(图像中的黑色区域)。必须在整个网络中进行这0次激活将增加模型的大小,而不会添加太多有用的信息。相反,我们希望压缩特征映射,只保留最有用的部分——特征本身。

这实际上就是最大池的作用。Max pooling在原始功能映射中获取一个激活补丁,并将其替换为该补丁中的最大激活。

当在ReLU激活后应用时,它具有“强化”功能的效果。池步骤将活动像素的比例增加到零像素。

示例-应用最大池

让我们将“压缩”步骤添加到第2课示例中所做的特征提取中。下一个隐藏单元将带我们回到我们结束的地方。

import tensorflow as tf
import matplotlib.pyplot as plt
import warnings

plt.rc('figure', autolayout=True)
plt.rc('axes', labelweight='bold', labelsize='large',
       titleweight='bold', titlesize=18, titlepad=10)
plt.rc('image', cmap='magma')
warnings.filterwarnings("ignore") # to clean up output cells

# Read image
image_path = '../input/computer-vision-resources/car_feature.jpg'
image = tf.io.read_file(image_path)
image = tf.io.decode_jpeg(image)

# Define kernel
kernel = tf.constant([
    [-1, -1, -1],
    [-1,  8, -1],
    [-1, -1, -1],
], dtype=tf.float32)

# Reformat for batch compatibility.
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image = tf.expand_dims(image, axis=0)
kernel = tf.reshape(kernel, [*kernel.shape, 1, 1])

# Filter step
image_filter = tf.nn.conv2d(
    input=image,
    filters=kernel,
    # we'll talk about these two in the next lesson!
    strides=1,
    padding='SAME'
)

# Detect step
image_detect = tf.nn.relu(image_filter)

# Show what we have so far
plt.figure(figsize=(12, 6))
plt.subplot(131)
plt.imshow(tf.squeeze(image), cmap='gray')
plt.axis('off')
plt.title('Input')
plt.subplot(132)
plt.imshow(tf.squeeze(image_filter))
plt.axis('off')
plt.title('Filter')
plt.subplot(133)
plt.imshow(tf.squeeze(image_detect))
plt.axis('off')
plt.title('Detect')
plt.show();

我们将使用tf.nn中的另一个函数来应用池步骤tf.nn.pool。这是一个Python函数,与构建模型时使用的MaxPool2D层的功能相同,但作为一个简单的函数,更易于直接使用。

import tensorflow as tf

image_condense = tf.nn.pool(
    input=image_detect, # image in the Detect step above
    window_shape=(2, 2),
    pooling_type='MAX',
    # we'll see what these do in the next lesson!
    strides=(2, 2),
    padding='SAME',
)

plt.figure(figsize=(6, 6))
plt.imshow(tf.squeeze(image_condense))
plt.axis('off')
plt.show();

很酷!希望您可以看到池步骤如何通过将图像压缩到最活跃的像素周围来强化该功能。

平移不变性

我们称零像素“不重要”。这是否意味着它们根本不携带任何信息?事实上,零像素携带位置信息。空白空间仍然定位图像中的特征。当MaxPool2D删除其中一些像素时,它将删除要素地图中的一些位置信息。这为convnet提供了一个称为平移不变性的属性。这意味着具有最大池的convnet将不会根据其在图像中的位置来区分特征。(““翻译”是一个数学词汇,用于在不旋转或改变形状或大小的情况下改变某物的位置。)

观察当我们重复将最大池应用于以下功能映射时会发生什么。

经过多次合并,原始图像中的两个点变得难以区分。换句话说,共享破坏了他们的一些位置信息。由于网络不再能够在特征图中区分它们,它也无法在原始图像中区分它们:它已经对位置上的差异保持不变。

事实上,合并只会在小距离的网络中创建平移不变性,就像图像中的两点一样。开始时相距较远的功能在合并后仍将保持不同;只有部分位置信息丢失,但不是全部。

这种对特征位置微小差异的不变性对于图像分类器来说是一个很好的特性。仅仅因为透视图或框架的不同,同一类型的特征可能会定位在原始图像的不同部分,但我们仍然希望分类器能够识别出它们是相同的。因为这种不变性被植入到网络中,我们可以用更少的数据进行训练:我们不再需要教它忽略这种差异。这使得卷积网络比只有密集层的网络具有更大的效率优势(在第6课中,您将看到另一种通过数据增强免费获得不变性的方法!)

结论

在本课中,我们了解了特征提取的最后一步:使用MaxPool2D压缩。在第4课中,我们将结束关于卷积和滑动窗口池的讨论。

4、滑动窗口

import numpy as np
from itertools import product
from skimage import draw, transform

def circle(size, val=None, r_shrink=0):
    circle = np.zeros([size[0]+1, size[1]+1])
    rr, cc = draw.circle_perimeter(
        size[0]//2, size[1]//2,
        radius=size[0]//2 - r_shrink,
        shape=[size[0]+1, size[1]+1],
    )
    if val is None:
        circle[rr, cc] = np.random.uniform(size=circle.shape)[rr, cc]
    else:
        circle[rr, cc] = val
    circle = transform.resize(circle, size, order=0)
    return circle

def show_kernel(kernel, label=True, digits=None, text_size=28):
    # Format kernel
    kernel = np.array(kernel)
    if digits is not None:
        kernel = kernel.round(digits)

    # Plot kernel
    cmap = plt.get_cmap('Blues_r')
    plt.imshow(kernel, cmap=cmap)
    rows, cols = kernel.shape
    thresh = (kernel.max()+kernel.min())/2
    # Optionally, add value labels
    if label:
        for i, j in product(range(rows), range(cols)):
            val = kernel[i, j]
            color = cmap(0) if val > thresh else cmap(255)
            plt.text(j, i, val, 
                     color=color, size=text_size,
                     horizontalalignment='center', verticalalignment='center')
    plt.xticks([])
    plt.yticks([])

def show_extraction(image,
                    kernel,
                    conv_stride=1,
                    conv_padding='valid',
                    activation='relu',
                    pool_size=2,
                    pool_stride=2,
                    pool_padding='same',
                    figsize=(10, 10),
                    subplot_shape=(2, 2),
                    ops=['Input', 'Filter', 'Detect', 'Condense'],
                    gamma=1.0):
    # Create Layers
    model = tf.keras.Sequential([
                    tf.keras.layers.Conv2D(
                        filters=1,
                        kernel_size=kernel.shape,
                        strides=conv_stride,
                        padding=conv_padding,
                        use_bias=False,
                        input_shape=image.shape,
                    ),
                    tf.keras.layers.Activation(activation),
                    tf.keras.layers.MaxPool2D(
                        pool_size=pool_size,
                        strides=pool_stride,
                        padding=pool_padding,
                    ),
                   ])

    layer_filter, layer_detect, layer_condense = model.layers
    kernel = tf.reshape(kernel, [*kernel.shape, 1, 1])
    layer_filter.set_weights([kernel])

    # Format for TF
    image = tf.expand_dims(image, axis=0)
    image = tf.image.convert_image_dtype(image, dtype=tf.float32) 
    
    # Extract Feature
    image_filter = layer_filter(image)
    image_detect = layer_detect(image_filter)
    image_condense = layer_condense(image_detect)
    
    images = {}
    if 'Input' in ops:
        images.update({'Input': (image, 1.0)})
    if 'Filter' in ops:
        images.update({'Filter': (image_filter, 1.0)})
    if 'Detect' in ops:
        images.update({'Detect': (image_detect, gamma)})
    if 'Condense' in ops:
        images.update({'Condense': (image_condense, gamma)})
    
    # Plot
    plt.figure(figsize=figsize)
    for i, title in enumerate(ops):
        image, gamma = images[title]
        plt.subplot(*subplot_shape, i+1)
        plt.imshow(tf.image.adjust_gamma(tf.squeeze(image), gamma))
        plt.axis('off')
        plt.title(title)

介绍

在前两节课中,我们了解了从图像中执行特征提取的三种操作:

带卷积层的滤波器

使用ReLU激活进行检测

使用最大池层进行压缩

卷积和池操作有一个共同的特点:它们都是通过滑动窗口执行的。通过卷积,这个“窗口”是由内核的维度,参数kernel_size给出的。对于池,它是池大小给定的池窗口。

还有两个额外的参数同时影响卷积层和池层——它们是窗口的步长以及是否在图像边缘使用填充。步长参数表示窗口在每一步应移动的距离,padding参数描述我们如何处理输入边缘的像素。

使用这两个参数,定义两个图层将变为:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Conv2D(filters=64,
                  kernel_size=3,
                  strides=1,
                  padding='same',
                  activation='relu'),
    layers.MaxPool2D(pool_size=2,
                     strides=1,
                     padding='same')
    # More layers follow
])

大步走

窗口在每一步移动的距离称为步幅。我们需要在图像的两个维度中指定步幅:一个用于从左到右移动,另一个用于从上到下移动。此动画显示步幅=(2,2),每一步移动2个像素。

步幅有什么影响?当任一方向上的跨步大于1时,滑动窗口将在每一步跳过输入中的一些像素。

因为我们希望使用高质量的特征进行分类,所以卷积层通常具有步长=(1,1)。增加步幅意味着我们在总结中遗漏了潜在的有价值的信息。但是,最大池层的步长值几乎总是大于1,如(2,2)或(3,3),但不大于窗口本身。

最后,请注意,当两个方向上的步幅值相同时,只需设置该数字;例如,您可以使用strips=2作为参数设置,而不是strips=(2,2)。

衬垫

在执行滑动窗口计算时,存在一个问题,即在输入边界处要做什么。完全停留在输入图像内意味着窗口将永远不会像对待输入中的每一个其他像素那样,笔直地位于这些边界像素之上。既然我们对所有像素的处理都不完全相同,那会有问题吗?

卷积对这些边界值的作用由其填充参数决定。在TensorFlow中,有两种选择:padding='same'或padding='valid'。每种方法都有权衡。

当我们设置padding='valid'时,卷积窗口将完全停留在输入内部。缺点是输出会收缩(丢失像素),而对于较大的内核,输出会收缩得更多。这将限制网络可以包含的层的数量,尤其是当输入尺寸较小时。

另一种方法是使用padding='same'。这里的技巧是在输入的边界周围填充0,使用刚好足够的0使输出的大小与输入的大小相同。然而,这可能具有稀释边界处像素的影响的效果。下面的动画显示了一个具有“相同”填充的滑动窗口。

我们一直在研究的VGG模型对其所有卷积层使用相同的填充。大多数现代convnet将使用这两者的某种组合(另一个要调整的参数!)

示例-探索滑动窗口

为了更好地理解滑动窗口参数的影响,它可以帮助观察低分辨率图像上的特征提取,以便我们可以看到各个像素。让我们看一个简单的圆圈。

下一个隐藏单元将为我们创建一个映像和内核。

import tensorflow as tf
import matplotlib.pyplot as plt

plt.rc('figure', autolayout=True)
plt.rc('axes', labelweight='bold', labelsize='large',
       titleweight='bold', titlesize=18, titlepad=10)
plt.rc('image', cmap='magma')

image = circle([64, 64], val=1.0, r_shrink=3)
image = tf.reshape(image, [*image.shape, 1])
# Bottom sobel
kernel = tf.constant(
    [[-1, -2, -1],
     [0, 0, 0],
     [1, 2, 1]],
)

show_kernel(kernel)

VGG体系结构相当简单。它使用步长为1的卷积和步长为2×2的最大池。我们在visiontools实用程序脚本中包含了一个函数,它将向我们展示所有步骤。

show_extraction(
    image, kernel,

    # 窗口参数
    conv_stride=1,
    pool_size=2,
    pool_stride=2,

    subplot_shape=(1, 4),
    figsize=(14, 6),
)

而且效果很好!内核设计用于检测水平线,我们可以看到,在生成的特征映射中,输入的更多水平部分以最大的激活结束。

如果我们把卷积的步长改为3,会发生什么?

show_extraction(
    image, kernel,

    # 窗口参数
    conv_stride=3,
    pool_size=2,
    pool_stride=2,

    subplot_shape=(1, 4),
    figsize=(14, 6),    
)

这似乎降低了提取的特征的质量。我们的输入圈相当“精细”,只有1像素宽。步长为3的卷积太粗糙,无法从中生成良好的特征贴图。

有时,模型会在其初始层中使用较大跨步的卷积。这通常也会与更大的内核相结合。例如,ResNet50模型在第一层中使用7×7的内核,步幅为2。这似乎可以加快大规模功能的生成,而不必牺牲输入中的太多信息。

结论

在本课中,我们研究了卷积和池的共同特征计算:滑动窗口和影响其在这些层中行为的参数。这种类型的加窗计算贡献了卷积网络的许多特性,是其功能的重要组成部分。

5、自定义Convnets

介绍

现在您已经看到了convnet用于提取功能的层,是时候将它们组合起来,构建自己的网络了!

简练

在最后三节课中,我们了解了卷积网络如何通过三个操作执行特征提取:过滤、检测和压缩。单轮特征提取只能从图像中提取相对简单的特征,例如简单的线条或对比度。这些太简单了,无法解决大多数分类问题。相反,convnets将一次又一次地重复此提取,以便特征在深入网络时变得更加复杂和精细。

卷积块

它通过将它们通过执行该提取的卷积块的长链来实现这一点。

这些卷积块是Conv2D和MaxPool2D层的堆栈,我们在上几节课中了解了它们在特征提取中的作用。

每个块代表一轮提取,通过组合这些块,convnet可以组合和重新组合生成的特征,对其进行生长和塑造,以更好地适应手头的问题。现代convnets的深层结构允许这种复杂的功能工程,并在很大程度上对其卓越的性能负责。

示例-设计一个Convnet

让我们看看如何定义一个能够设计复杂特征的深度卷积网络。在本例中,我们将创建一个Keras序列模型,然后在Cars数据集上对其进行训练。

步骤1-加载数据

此隐藏单元格加载数据。

步骤2-定义模型

下面是我们将使用的模型的示意图:

现在我们将定义模型。看看我们的模型是如何由三个Conv2D和MaxPool2D层块(底部)和一个致密层头组成的。我们可以通过填充适当的参数,或多或少地将此图直接转换为Keras序列模型。

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([

    # 第一卷积块
    layers.Conv2D(filters=32, kernel_size=5, activation="relu", padding='same',
                  # give the input dimensions in the first layer
                  # [height, width, color channels(RGB)]
                  input_shape=[128, 128, 3]),
    layers.MaxPool2D(),

    # 第二卷积块
    layers.Conv2D(filters=64, kernel_size=3, activation="relu", padding='same'),
    layers.MaxPool2D(),

    # 第三卷积块
    layers.Conv2D(filters=128, kernel_size=3, activation="relu", padding='same'),
    layers.MaxPool2D(),

    # 分级机机头
    layers.Flatten(),
    layers.Dense(units=6, activation="relu"),
    layers.Dense(units=1, activation="sigmoid"),
])
model.summary()

请注意,在这个定义中,过滤器的数量是如何逐块翻倍的:64128256。这是一种常见模式。由于MaxPool2D图层正在减小要素贴图的大小,因此我们可以增加创建的数量。

第3步-培训

我们可以像第1课中的模型一样训练这个模型:使用优化器以及适合于二进制分类的损失和度量来编译它。

model.compile(
    optimizer=tf.keras.optimizers.Adam(epsilon=0.01),
    loss='binary_crossentropy',
    metrics=['binary_accuracy']
)

history = model.fit(
    ds_train,
    validation_data=ds_valid,
    epochs=40,
    verbose=0,
)
import pandas as pd

history_frame = pd.DataFrame(history.history)
history_frame.loc[:, ['loss', 'val_loss']].plot()
history_frame.loc[:, ['binary_accuracy', 'val_binary_accuracy']].plot();

该模型比第1课中的VGG16模型小得多——与VGG16的16层相比,只有3个卷积层。尽管如此,它还是能够很好地拟合这个数据集。我们可能仍然能够通过添加更多的卷积层来改进这个简单模型,希望创建更适合数据集的特征。这就是我们在练习中要尝试的。

结论

在本教程中,您了解了如何构建由许多卷积块组成的自定义convnet,并能够进行复杂的特征工程。

6、数据增强

介绍

现在,您已经学习了卷积分类器的基本原理,可以继续学习更高级的主题了。

在本课中,您将学习一个可以增强图像分类器的技巧:它称为数据增强。

虚假数据的有用性

提高机器学习模型性能的最佳方法是在更多数据上对其进行训练。模型需要学习的例子越多,它就越能识别图像中哪些差异重要,哪些不重要。更多的数据有助于模型更好地概括。

获取更多数据的一个简单方法是使用已有的数据。如果我们能够以保留类的方式变换数据集中的图像,我们就可以教会分类器忽略这些类型的变换。例如,照片中的汽车是左向还是右向,并不能改变它是汽车而不是卡车的事实。所以,如果我们用翻转的图像来增加训练数据,我们的分类器就会知道“左或右”是一个应该忽略的差异。

这就是数据扩充背后的全部想法:添加一些看起来与真实数据相当相似的额外虚假数据,您的分类器将得到改进。

使用数据扩充

通常,在扩充数据集时会使用多种类型的转换。这些可能包括旋转图像、调整颜色或对比度、扭曲图像或许多其他事情,通常组合应用。下面是一个可以变换单个图像的不同方法的示例。

数据扩充通常是在线完成的,也就是说,当图像被送入网络进行训练时。回想一下,培训通常是在小批量数据上进行的。当使用数据增强时,这是一批16幅图像的样子。

应用转换。

每次在训练期间使用图像时,都会应用新的随机变换。这样,模型总是看到一些与以前看到的稍有不同的东西。训练数据中的这种额外差异有助于模型处理新数据。

重要的是要记住,并不是每个转换都对给定的问题有用。最重要的是,您使用的任何转换都不应该混淆这些类。例如,如果你在训练一个数字识别器,旋转图像会混淆“9”和“6”。最后,找到好的扩充的最佳方法与大多数ML问题相同:试试看!

示例-数据扩充培训

Keras允许您通过两种方式扩充数据。第一种方法是使用类似ImageDataGenerator的函数将其包含在数据管道中。第二种方法是通过使用Keras的预处理层将其包含在模型定义中。这是我们将采取的方法。我们的主要优势是,图像转换将在GPU而不是CPU上计算,这可能会加快训练速度。

在本练习中,我们将学习如何通过数据扩充改进第1课中的分类器。下一个隐藏单元设置数据管道。

步骤2-定义模型

为了说明增强的效果,我们将只向教程1中的模型添加几个简单的转换。

from tensorflow import keras
from tensorflow.keras import layers
# 这些是TF2.2中的一个新特性
from tensorflow.keras.layers.experimental import preprocessing


pretrained_base = tf.keras.models.load_model(
    '../input/cv-course-models/cv-course-models/vgg16-pretrained-base',
)
pretrained_base.trainable = False

model = keras.Sequential([
    # 预处理
    preprocessing.RandomFlip('horizontal'), # flip left-to-right
    preprocessing.RandomContrast(0.5), # contrast change by up to 50%
    # 基础
    pretrained_base,
    # 头
    layers.Flatten(),
    layers.Dense(6, activation='relu'),
    layers.Dense(1, activation='sigmoid'),
])

第3步-培训和评估

现在我们开始训练!

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['binary_accuracy'],
)

history = model.fit(
    ds_train,
    validation_data=ds_valid,
    epochs=30,
    verbose=0,
)
import pandas as pd

history_frame = pd.DataFrame(history.history)

history_frame.loc[:, ['loss', 'val_loss']].plot()
history_frame.loc[:, ['binary_accuracy', 'val_binary_accuracy']].plot();

教程1中的模型中的训练和验证曲线发散得相当快,这表明它可能受益于某种正则化。该模型的学习曲线能够保持更接近,我们在验证损失和准确性方面取得了一些适度的改进。这表明数据集确实从扩展中受益。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值