TensorFlow 实战(二)(3)https://developer.aliyun.com/article/1522701
6.1.1 文件夹/文件结构
数据现在应该在 Ch06/data 文件夹中可用了。现在是时候探索数据集了。我们将首先手动浏览提供给我们的文件夹中的数据。你会注意到有三个文件夹和两个文件(图 6.2)。四处看看并探索一下。
图 6.2 tiny-imagenet-200 数据集中找到的文件夹和文件
文件 wnids.txt 包含一组 200 个 ID(称为wnids或 WordNet IDs,基于词汇数据库 WordNet [wordnet.princeton.edu/]; 图 6.3)。每个 ID 代表一个图像类别(例如,金鱼类)。
图 6.3 来自 wnids.txt 的示例内容。每行包含一个 wnid(WordNet ID)。
文件 words.txt 以制表符分隔值(TSV)格式提供了对这些 ID 的人性化描述(表 6.1)。请注意,这个文件包含超过 82,000 行(远超过我们的 200 个类别)并来自一个更大的数据集。
表 6.1 来自 words.txt 的示例内容。其中包含数据集中的 wnids 以及它们的描述。
| n00001740 | entity |
| n00001930 | physical entity |
| n00002137 | 抽象,抽象实体 |
| n00002452 | 东西 |
| n00002684 | 物体,实物 |
| n00003553 | 整体,单位 |
| n00003993 | 同种异体 |
| n00004258 | 生物,有机物 |
| n00004475 | 有机体,存在 |
| n00005787 | 底栖生物 |
| n00005930 | 矮人 |
| n00006024 | 异养生物 |
| n00006150 | 父母 |
| n00006269 | 生命 |
| n00006400 | 生物体 |
训练文件夹包含训练数据。它包含一个名为 images 的子文件夹,在其中,您可以找到 200 个文件夹,每个都有一个标签(即 wnid)。在每个这些子文件夹中,您将找到代表该类别的一系列图像。每个以其名称作为 wnid 的子文件夹包含每类 500 张图像,总共有 100,000 张(在所有子文件夹中)。图 6.4 描述了这种结构,以及训练文件夹中找到的一些数据。
图 6.4 tiny-imagenet-200 数据集的总体结构。它有三个文本文件(wnids.txt、words.txt 和 val/val_annotations.txt)和三个文件夹(train、val 和 test)。我们只使用 train 和 val 文件夹。
val 文件夹包含一个名为 images 的子文件夹和一组图像(这些图像不像在 train 文件夹中那样被进一步分成子文件夹)。这些图像的标签(或 wnids)可以在 val 文件夹中的 val_annotations.txt 文件中找到。
最后一个文件夹称为测试文件夹,在本章中我们将忽略它。该数据集是竞赛的一部分,数据用于评分提交的模型。我们没有这个测试集的标签。
6.1.2 理解数据集中的类别
我们已经了解了我们拥有的数据的类型以及其可用性。接下来,让我们识别一些数据中的类别。为此,我们将定义一个名为 get_tiny_imagenet_classes() 的函数,该函数读取 wnids.txt 和 words.txt 文件,并创建一个包含两列的 pd.DataFrame(即 pandas DataFrame):wnid 及其相应的类别描述(见下一个列表)。
列表 6.1 获取数据集中类别的类别描述
import pandas as pd ❶ import os ❶ data_dir = os.path.join('data', 'tiny-imagenet-200') ❷ wnids_path = os.path.join(data_dir, 'wnids.txt') ❷ words_path = os.path.join(data_dir, 'words.txt') ❷ def get_tiny_imagenet_classes(wnids_path, words_path): ❸ wnids = pd.read_csv(wnids_path, header=None, squeeze=True) ❹ words = pd.read_csv(words_path, sep='\t', index_col=0, header=None) ❹ words_200 = words.loc[wnids].rename({1:'class'}, axis=1) ❺ words_200.index.name = 'wnid' ❻ return words_200.reset_index() ❼ labels = get_tiny_imagenet_classes(wnids_path, words_path) ❽ labels.head(n=25) ❾
❶ 导入 pandas 和 os 包
❷ 定义数据目录、wnids.txt 和 words.txt 文件的路径
❸ 定义一个函数来读取 tiny_imagenet 类别的类别描述
❹ 使用 pandas 读取 wnids.txt 和 words.txt 作为 CSV 文件
❺ 仅获取 tiny-imagenet-200 数据集中存在的类别
❻ 将数据框的索引名称设置为“wnid”
❼ 重置索引,使其成为数据框中的一列(该列的列名为“wnid”)
❽ 执行函数以获取类别描述
❾ 检查数据框的头部(前 25 个条目)
此函数首先读取包含 wnids 列表的 wnids.txt 文件,该列表对应于数据集中可用的类别,作为 pd.Series(即 pandas series)对象。 接下来,它将 words.txt 文件读取为 pd.DataFrame(即 pandas DataFrame),其中包含 wnid 到类别描述的映射,并将其分配给 words。 然后,它选择在 wnids pandas 系列中存在 wnid 的项目。 这将返回一个包含 200 行的 pd.DataFrame(表 6.2)。 请记住,words.txt 中的项数远远大于实际数据集,因此我们只需要选择与我们相关的项。
表 6.2 使用 get_tiny_imagenet_classes()函数生成的标签 ID 及其描述的示例
| 风 | 课程 | |
| 0 | n02124075 | 埃及猫 |
| 1 | n04067472 | 卷轴 |
| 2 | n04540053 | 排球 |
| 3 | n04099969 | 摇椅,摇椅 |
| 4 | n07749582 | 柠檬 |
| 5 | n01641577 | 牛蛙,美洲牛蛙 |
| 6 | n02802426 | 篮球 |
| 7 | n09246464 | 悬崖,跌落,坠落 |
| 8 | n07920052 | 浓缩咖啡 |
| 9 | n03970156 | 吸盘,管道工的助手 |
| 10 | n03891332 | 停车计时器 |
| 11 | n02106662 | 德国牧羊犬,德国牧羊犬,德国牧羊犬… |
| 12 | n03201208 | 餐桌,板 |
| 13 | n02279972 | 帝王蝴蝶,帝王蝴蝶,小米蝴蝶 |
| 14 | n02132136 | 棕熊,棕熊,北极熊 |
| 15 | n041146614 | 校车 |
然后我们将计算每个类别的数据点(即图像)的数量:
def get_image_count(data_dir): # Get the count of JPEG files in a given folder (data_dir) return len( [f for f in os.listdir(data_dir) if f.lower().endswith('jpeg')] ) # Apply the function above to all the subdirectories in the train folder labels["n_train"] = labels["wnid"].apply( lambda x: get_image_count(os.path.join(data_dir, 'train', x, 'images')) ) # Get the top 10 entries in the labels dataframe labels.head(n=10)
此代码创建一个名为 n_train 的新列,显示每个 wnid 找到了多少个数据点(即图像)。 这可以通过 pandas pd.Series .apply()函数来实现,该函数将 get_image_count()应用于系列 labels[“wnid”]中的每个项目。 具体来说,get_image_count()接受一个路径并返回该文件夹中找到的 JPEG 文件的数量。 当您将此 get_image_count()函数与 pd.Series.apply()结合使用时,它会进入 train 文件夹中的每个文件夹,并计算图像的数量。 一旦运行了标签.head(n=10)行,您应该会得到表 6.3 中显示的结果。
表 6.3 计算了 n_train(训练样本数)的数据示例
| 风 | 课程 | n_train | |
| 0 | n02124075 | 埃及猫 | 500 |
| 1 | n04067472 | 卷轴 | 500 |
| 2 | n04540053 | 排球 | 500 |
| 3 | n04099969 | 摇椅,摇椅 | 500 |
| 4 | n07749582 | 柠檬 | 500 |
| 5 | n01641577 | 牛蛙,美洲牛蛙 | 500 |
| 6 | n02802426 | 篮球 | 500 |
| 7 | n09246464 | 悬崖,跌落,坠落 | 500 |
| 8 | n07920052 | 浓缩咖啡 | 500 |
| 9 | n03970156 | 吸盘,管道工的助手 | 500 |
让我们快速验证结果是否正确。 进入 train 文件夹中的 n02802426 子目录,其中应该包含篮球的图像。 图 6.5 显示了几个示例图像。
图 6.5 wnid 类别 n02802426(即篮球)的样本图像
你可能会发现这些图像与你预期的截然不同。你可能期望看到清晰放大的篮球图像。但在现实世界中,永远不会出现这种情况。真实数据集是有噪声的。你可以看到以下图像:
- 篮球几乎看不见(左上角)。
- 篮球是绿色的(左下角)。
- 篮球在婴儿旁边(即上下文无关)(中间上方)。
这会让你更加欣赏深度网络,因为这对一堆堆叠的矩阵乘法(即深度网络)是一个困难的问题。需要精确的场景理解才能成功解决此任务。尽管困难,但奖励很大。我们开发的模型最终将用于识别各种背景和上下文中的物体,例如客厅、厨房和室外。这正是这个数据集为模型训练的目的:在各种情境中理解/检测物体。你可能可以想象为什么现代 CAPTCHA 越来越聪明,并且可以跟上能够更准确地分类对象的算法。对于受过适当训练的 CNN 来说,识别具有混乱背景或小遮挡的 CAPTCHA 并不困难。
你还可以快速检查我们生成的 n_train 列的摘要统计数据(例如,平均值、标准差等)。这提供了比查看所有 200 行更容易消化的列的摘要。这是使用 pandas 描述() 函数完成的:
labels["n_train"].describe()
执行此操作将返回以下系列:
count 200.0 mean 500.0 std 0.0 min 500.0 25% 500.0 50% 500.0 75% 500.0 max 500.0 Name: n_train, dtype: float64
你可以看到它返回了列的重要统计信息,如平均值、标准差、最小值和最大值。每个类别都有 500 张图像,这意味着数据集完美地平衡了类别。这是验证我们有一个类平衡数据集的有用方法。
6.1.3 计算数据集上的简单统计量
分析数据的各种属性也是一个重要步骤。根据你处理的数据类型,分析类型会发生变化。在这里,我们将找出图像的平均大小(甚至是 25/50/75 百分位数)。
在实际模型中准备好这些信息可以节省很多时间,因为你必须了解图像大小(高度和宽度)的基本统计信息,以裁剪或填充图像到固定大小,因为图像分类 CNN 只能处理固定大小的图像(见下一个列表)。
列表 6.2 计算图像宽度和高度统计数据
import os ❶ from PIL import Image ❶ import pandas as pd ❶ image_sizes = [] ❷ for wnid in labels["wnid"].iloc[:25]: ❸ img_dir = os.path.join( 'data', 'tiny-imagenet-200', 'train', wnid, 'images' ) ❹ for f in os.listdir(img_dir): ❺ if f.endswith('JPEG'): ❺ image_sizes.append(Image.open(os.path.join(img_dir, f)).size) ❻ img_df = pd.DataFrame.from_records(image_sizes) ❼ img_df.columns = ["width", "height"] ❽ img_df.describe() ❾
❶ 导入 os、PIL 和 pandas 包
❷ 定义一个列表来保存图像大小
❸ 在数据集中循环前 25 类
❹ 在循环中为特定类别定义图像目录
❺ 在该目录中循环所有具有扩展名 JPEG 的图像
❻ 将每个图像的大小(即 (宽度、高度) 元组)添加到 image_sizes 中
❼ 从 image_sizes 中的元组创建数据框架
❽ 适当设置列名
❾ 获取我们获取的图像的宽度和高度的摘要统计信息
在这里,我们从之前创建的标签 DataFrame 中获取前 25 个 wnid(处理所有 wnid 会花费太多时间)。然后,对于每个 wnid,我们进入包含属于它的数据的子文件夹,并使用以下方法获取每个图像的宽度和高度信息
Image.open(os.path.join(img_dir, f)).size
使用Image.open().size函数返回给定图像的元组(宽度,高度)。我们将遇到的所有图像的宽度和高度记录在image_sizes列表中。最后,image_sizes列表如下所示:
image_sizes = [(image_1.width, image_1.height), (image_2.width, image_2.height), ..., (image_n.width, image_n.height)]
对于这种格式的数据,我们可以使用pd.DataFrame.from_records()函数将此列表创建为pd.DataFrame。image_sizes中的单个元素是一条记录。例如,(image_1.width, image_1.height)是一条记录。因此,image_sizes是一组记录的列表。当您从记录列表创建pd.DataFrame时,每条记录都变为pandas DataFrame中的一行,其中每条记录中的每个元素都变为列。例如,由于每条记录中都有图像宽度和图像高度作为元素,因此宽度和高度成为pandas DataFrame中的列。最后,我们执行img_df.describe()以获取我们读取的图像的宽度和高度的基本统计信息(表 6.4)。
表 6.4 图像的宽度和高度统计信息
| 宽度 | 高度 | |
| count | 12500.0 | 12500.0 |
| mean | 64.0 | 64.0 |
| std | 0.0 | 0.0 |
| min | 64.0 | 64.0 |
| 25% | 64.0 | 64.0 |
| 50% | 64.0 | 64.0 |
| 75% | 64.0 | 64.0 |
| max | 64.0 | 64.0 |
接下来,我们将讨论如何创建数据管道来摄取我们刚刚讨论的图像数据。
练习 1
假设在浏览数据集时,您遇到了一些损坏的图像(即,它们具有负值像素)。假设您已经有了一个名为df的pd.DataFrame(),其中包含一个带有图像文件路径的单列(称为filepath),请使用pandas apply()函数读取每个图像的最小值,并将其分配给名为minimum的列。要读取图像,您可以假设已完成from PIL import Image和import numpy as np,您还可以使用np.array(将)
PIL.Image转换为数组。
6.2 使用 Keras ImageDataGenerator 创建数据管道
您已经很好地探索了数据集,并了解了诸如有多少类别、存在什么样的对象以及图像的大小等信息。现在,您将为三个不同的数据集创建三个数据生成器:训练、验证和测试。这些数据生成器以批量从磁盘中检索数据,并执行任何所需的预处理。这样,数据就可以被模型轻松消耗。为此,我们将使用方便的tensorflow.keras.preprocessing.image.ImageDataGenerator。
我们将从定义一个 Keras ImageDataGenerator()开始,以在构建模型时提供数据:
from tensorflow.keras.preprocessing.image import ImageDataGenerator import os random_seed = 4321 batch_size = 128 image_gen = ImageDataGenerator(samplewise_center=True, validation_split=0.1)
设置 samplewise_center=True,生成的图像将具有归一化的值。每个图像将通过减去该图像的平均像素值来居中。validation_split 参数在训练数据中扮演着重要的角色。这让我们将训练数据分成两个子集,训练集和验证集,通过从训练数据中分离出一部分(在本例中为 10%)。在机器学习问题中,通常应该有三个数据集:
- 训练数据—通常是最大的数据集。我们用它来训练模型。
- 验证数据—保留数据集。它不用于训练模型,而是用于在训练过程中监视模型的性能。请注意,此验证集在训练过程中必须保持固定(不应更改)。
- 测试数据—保留数据集。与验证数据集不同,这仅在模型训练完成后使用。这表示模型在未见的真实世界数据上的表现。这是因为模型在测试时间之前没有以任何方式与测试数据集交互(与训练和验证数据集不同)。
我们还将为稍后的数据生成定义一个随机种子和批量大小。
创建一个 ImageDataGenerator 后,您可以使用其中的一个 flow 函数来读取来自异构源的数据。例如,Keras 目前提供了以下方法:
- flow()—从 NumPy 数组或 pandas DataFrame 中读取数据
- flow_from_dataframe()—从包含文件名和它们关联标签的文件中读取数据
- flow_from_directory()—从文件夹中读取数据,该文件夹中的图像根据它们所属的类别组织到子文件夹中。
首先,我们将查看 flow_from_directory(),因为我们的训练目录以 flow_from_directory()函数期望数据的确切格式存储。具体来说,flow_from_directory()期望数据的格式如图 6.6 所示。
图 6.6 流从目录方法所预期的文件夹结构
流方法返回数据生成器,这些生成器是 Python 生成器。生成器本质上是一个返回迭代器(称为generator-iterator)的函数。但为了保持我们的讨论简单,我们将生成器和迭代器都称为生成器。您可以像处理列表一样迭代生成器,并以顺序方式返回项目。这里是一个生成器的例子:
def simple_generator(): for i in range(0, 100): yield (i, i*2)
请注意使用关键字 yield,您可以将其视为 return 关键字。但是,与 return 不同,yield 不会在执行该行后立即退出函数。现在您可以将迭代器定义为
iterator = simple_generator()
您可以将迭代器视为包含[(0, 0), (1, 2), (2, 4), …,(98, 196), (99, 198)]的列表。然而,在幕后,生成器比列表对象更节省内存。在我们的情况下,数据生成器将在单次迭代中返回一批图像和目标(即,图像和标签的元组)。您可以直接将这些生成器提供给像tf.keras.models.Model.fit()这样的方法,以训练模型。flow_from_directory()方法用于检索数据:
target_size = (56,56) train_gen = image_gen.flow_from_directory( directory=os.path.join('data','tiny-imagenet-200', 'train'), target_size=target_size, classes=None, class_mode='categorical', batch_size=batch_size, shuffle=True, seed=random_seed, subset='training' ) valid_gen = image_gen.flow_from_directory ( directory=os.path.join('data','tiny-imagenet-200', 'train'), target_size=target_size, classes=None, class_mode='categorical', batch_size=batch_size, shuffle=True, seed=random_seed, subset='validation' )
您可以看到已为这些函数设置了许多参数。需要注意的最重要的参数是subset参数,对于train_gen设置为“training”,对于valid_gen设置为“validation”。其他参数如下:
- 目录(string)—父目录的位置,在这里数据进一步分成表示类别的子文件夹。
- 目标大小(int 元组)—图像的目标大小,表示为(高度,宽度)的元组。图像将被调整为指定的高度和宽度。
- 类别模式(string)—我们将要提供给模型的目标类型。因为我们希望目标是表示每个类别的独热编码向量,所以我们将其设置为’categorical’。可用类型包括“categorical”(默认值)、“binary”(对于只有两类(0 或 1)的数据集)、“sparse”(数值标签而不是独热编码向量)、“input”或 None(没有标签)、以及“raw”或“multi_output”(仅在特殊情况下可用)。
- 批量大小(int)—单个数据批次的大小。
- 是否在获取时对数据进行洗牌(bool)—是否在获取时对数据进行洗牌。
- 随机种子(int)—数据洗牌的随机种子,因此我们每次运行时都能获得一致的结果。
- 子集(string)—如果
validation_split > 0,则需要哪个子集。这需要设置为“training”或“validation”之一。
请注意,即使我们有 64 × 64 的图像,我们也将它们调整为 56 × 56。这是因为我们将使用的模型设计用于 224 × 224 的图像。具有 224 × 224 尺寸的图像使得将模型适应我们的数据变得更加容易。
我们可以让我们的解决方案变得更加闪亮!您可以看到,在train_gen和valid_gen之间,使用的参数有很多重复。实际上,除了subset之外,所有参数都相同。这种重复会使代码变得凌乱,并为错误留下余地(如果需要更改参数,则可能会设置一个而忘记另一个)。您可以在 Python 中使用偏函数来创建具有重复参数的偏函数,然后使用它来创建train_gen和valid_gen:
from functools import partial target_size = (56,56) partial_flow_func = partial( image_gen.flow_from_directory, directory=os.path.join('data','tiny-imagenet-200', 'train'), target_size=target_size, classes=None, class_mode='categorical', batch_size=batch_size, shuffle=True, seed=random_seed) train_gen = partial_flow_func(subset='training') valid_gen = partial_flow_func(subset='validation')
这里,我们首先创建一个partial_flow_function(一个 Python 函数),它实质上是flow_from_directory函数,有一些参数已经填充。然后,为了创建train_gen和valid_gen,我们只传递了subset参数。这样可以使代码更加清晰。
验证数据检查:不要期望框架为您处理事务
现在我们有了一个训练数据生成器和一个验证数据生成器,我们不应该盲目地承诺使用它们。我们必须确保我们从训练数据随机采样的验证数据在每次遍历训练数据集时保持一致。这似乎是一个应该由框架本身处理的微不足道的事情,但最好不要认为这是理所当然的。如果你这样做
如果不执行此检查,最终你会付出代价,因此最好确保我们在不同试验中获得一致的结果。
为此,你可以对验证数据生成器的输出进行多次迭代,进行固定次数的迭代,并确保每次试验中都获得相同的标签序列。此代码在笔记本中可用(在“验证验证数据的一致性”部分下)。
我们还没有完成。我们需要对 flow_from_directory() 函数返回的生成器进行轻微修改。如果你查看数据生成器中的项,你会看到它是一个元组(x,y),其中 x 是一批图像,y 是一批 one-hot 编码的目标。我们在这里使用的模型有一个最终预测层和两个额外的辅助预测层。总共,该模型有三个输出层,因此我们需要返回(x,(y,y,y))而不是一个元组(x,y),通过三次复制 y。我们可以通过定义一个新的生成器 data_gen_aux() 来修复这个问题,该生成器接受现有的生成器并修改其输出,如所示。这需要对训练数据生成器和验证数据生成器都进行修复:
def data_gen_aux(gen): for x,y in gen: yield x,(y,y,y) train_gen_aux = data_gen_aux(train_gen) valid_gen_aux = data_gen_aux(valid_gen)
是时候为测试数据创建一个数据生成器了。回想一下,我们说过我们正在使用的测试数据(即 val 目录)的结构与训练和 tran_val 数据文件夹不同。因此,它需要特殊处理。类标签存储在一个名为 val_annotations.txt 的文件中,并且图像放置在一个具有扁平结构的单个文件夹中。不用担心;Keras 也为这种情况提供了一个函数。在这种情况下,我们将首先使用 get_test_labels_df() 函数将 val_annotations.txt 读取为一个 pd.DataFrame。该函数简单地读取 val_annotations.txt 文件,并创建一个具有两列的 pd.DataFrame,即图像的文件名和类标签:
def get_test_labels_df(test_labels_path): test_df = pd.read_csv(test_labels_path, sep='\t', index_col=None, header=None) test_df = test_df.iloc[:,[0,1]].rename({0:"filename", 1:"class"}, axis=1) return test_df test_df = get_test_labels_df(os.path.join('data','tiny-imagenet-200', 'val', 'val_annotations.txt'))
接下来,我们将使用 flow_from_dataframe() 函数创建我们的测试数据生成器。你只需要传递我们之前创建的 test_df(作为 dataframe 参数)和指向图像所在目录的目录参数。请注意,我们为测试数据设置了 shuffle=False,因为我们希望以相同的顺序输入测试数据,以便我们监视的性能指标将保持不变,除非我们更改模型:
test_gen = image_gen.flow_from_dataframe( dataframe=test_df, directory=os.path.join('data','tiny-imagenet- ➥ 200', 'val', 'images'), target_size=target_size, ➥ class_mode='categorical', batch_size=batch_size, shuffle=False )
接下来,我们将使用 Keras 定义一个复杂的计算机视觉模型,并最终在我们准备好的数据上对其进行训练。
练习 2
作为测试过程的一部分,假设你想要查看模型对训练数据中损坏标签的鲁棒性如何。为此,你计划创建一个生成器,以 50% 的概率将标签设置为 0。你将如何修改以下生成器以实现此目的?你可以使用 np.random.normal() 从具有零均值和单位方差的正态分布中随机抽取一个值:
def data_gen_corrupt(gen): for x,y in gen: yield x,(y,y,y)
6.3 Inception net:实现最先进的图像分类器
你已经分析了数据集,并对数据的外观有了全面的了解。对于图像,你无疑会转向卷积神经网络(CNNs),因为它们是业内最好的。现在是构建一个模型来学习客户个人喜好的时候了。在这里,我们将使用 Keras functional API 复制一个最先进的 CNN 模型(称为 Inception net)。
Inception 网络是一个复杂的 CNN,以其提供的最先进性能而著称。Inception 网络的名字来源于流行的互联网梗“我们需要更深入”,该梗以电影 Inception 中的莱昂纳多·迪卡普里奥为特色。
Inception 模型在短时间内推出了六个不同版本(大约在 2015-2016 年之间)。这证明了该模型在计算机视觉研究人员中有多受欢迎。为了纪念过去,我们将实现首个推出的 Inception 模型(即 Inception 网络 v1),并随后将其与其他模型进行比较。由于这是一个高级 CNN,对其架构和一些设计决策的深入了解至关重要。让我们来看看 Inception 模型,它与典型 CNN 有何不同,最重要的是,它为什么不同。
Inception 模型(或 Inception 网络)不是典型的 CNN。它的主要特点是复杂性,因为模型越复杂(即参数越多),准确率就越高。例如,Inception 网络 v1 几乎有 20 层。但是当涉及到复杂模型时,会出现两个主要问题:
- 如果你没有足够大的数据集用于一个复杂模型,那么很可能模型会对训练数据过拟合,导致在真实世界数据上的整体性能不佳。
- 复杂的模型导致更多的训练时间和更多的工程努力来将这些模型适配到相对较小的 GPU 内存中。
这要求以更加务实的方式来解决这个问题,比如回答“我们如何在深度模型中引入稀疏性,以减少过拟合风险以及对内存的需求?”这是 Inception 网络模型中回答的主要问题。
什么是过拟合?
过拟合是机器学习中的一个重要概念,而且常常难以避免。过拟合是指模型学习很好地表示训练数据(即高训练精度),但在未见过的数据上表现不佳(即低测试精度)的现象。当模型试图记住训练样本而不是从数据中学习可泛化的特征时,就会发生这种情况。这在深度网络中很普遍,因为它们通常比数据量更多的参数。过拟合将在下一章中更详细地讨论。
让我们再次回顾 CNN 的基础知识。
6.3.1 CNN 回顾
CNN 主要用于处理图像和解决计算机视觉问题(例如图像分类、目标检测等)。如图 6.7 所示,CNN 有三个组成部分:
- 卷积层
- 池化层全连接层
图 6.7 一个简单的卷积神经网络。首先,我们有一个具有高度、宽度和通道维度的图像,然后是一个卷积和一个池化层。最后,最后一个卷积/池化层的输出被展平并馈送到一组全连接层。
卷积操作将一个固定大小的小核(也称为过滤器)沿输入的宽度和高度维度移动。在这样做时,它在每个位置产生一个单一值。卷积操作使用具有一定宽度、高度和若干通道的输入,并产生具有一定宽度、高度和单一通道的输出。为了产生多通道输出,卷积层堆叠许多这些过滤器,导致与过滤器数量相同数量的输出。卷积层具有以下重要参数:
- 过滤器数量 — 决定卷积层产生的输出的通道深度(或特征图的数量)
- 核大小 — 也称为感受野,它决定了过滤器的大小(即高度和宽度)。核大小越大,模型在一次观察中看到的图像部分就越多。但更大的过滤器会导致更长的训练时间和更大的内存需求。
- 步长 — 决定在卷积图像时跳过多少像素。更高的步长导致较小的输出大小(步长通常仅用于高度和宽度维度)。
- 填充 — 通过添加零值的虚拟边界来防止卷积操作期间自动降低维度,从而使输出具有与输入相同的高度和宽度。
图 6.8 展示了卷积操作的工作原理。
图 6.8 在卷积操作中移动窗口时发生的计算
当处理输入时,池化操作表现出与卷积操作相同的行为。但是,所涉及的确切计算是不同的。池化有两种不同的类型:最大池化和平均池化。最大池化在图 6.9 中显示的深灰色框中找到的最大值作为窗口移过输入时的输出。平均池化在窗口移过输入时取深灰色框的平均值作为输出。
注意 CNNs 在输出处使用平均池化,并在其他地方使用最大池化层。已发现该配置提供了更好的性能。
图 6.9 池化操作如何计算输出。它查看一个小窗口,并将该窗口中的输入最大值作为相应单元的输出。
池化操作的好处在于它使得 CNN 具有平移不变性。平移不变性意味着模型可以识别物体,而不管它出现在何处。由于最大池化的计算方式,生成的特征图是相似的,即使对象/特征与模型训练的位置相差几个像素。这意味着,如果你正在训练一个分类狗的模型,网络将对狗出现的确切位置具有弹性(只有在一定程度上)。
最后,你有一个全连接层。由于我们目前主要关注分类模型,我们需要为任何给定的图像输出一个类别的概率分布。我们通过将少量的全连接层连接到 CNNs 的末尾来实现这一点。全连接层将最后的卷积/池化输出作为输入,并在分类问题中生成类别的概率分布。
正如你所见,CNNs 有许多超参数(例如,层数、卷积窗口大小、步幅、全连接隐藏层大小等)。为了获得最佳结果,需要使用超参数优化技术(例如,网格搜索、随机搜索)来选择它们。
6.3.2 Inception 网络 v1
Inception 网络 v1(也称为 GoogLeNet)(mng.bz/R4GD) 将 CNNs 带入了另一个层次。它不是一个典型的 CNN,与标准 CNN 相比,需要更多的实现工作。乍一看,Inception 网络可能看起来有点可怕(见图 6.10)。但是你只需要理解几个新概念,就可以理解这个模型。主要是这些概念的重复应用使模型变得复杂。
图 6.10 Inception 网络 v1 的抽象架构。Inception 网络从一个称为干扰的起始开始,这是一个在典型 CNN 中找到的普通卷积/池化层序列。然后,Inception 网络引入了一个称为 Inception 块的新组件。最后,Inception 网络还使用了辅助输出层。
让我们首先在宏观层面理解 Inception 模型中的内容,如图 6.10 所示,暂时忽略诸如层和它们的参数之类的细节。我们将在开发出强大的宏观水平理解后详细阐述这些细节。
Inception 网络以称为stem的东西开始。stem 包含与典型 CNN 的卷积和池化层相同的卷积和池化层。换句话说,stem 是按特定顺序组织的卷积和池化层的序列。
接下来,你有几个Inception blocks,这些块被 max pooling 层交错。一个 Inception block 包含一组并行的具有不同核大小的子卷积层。这使得模型能够在给定深度上以不同大小的感受野查看输入。我们将详细研究这背后的细节和动机。
最后,你有一个全连接层,它类似于典型 CNN 中的最终预测层。你还可以看到还有两个更多的临时全连接层。这些被称为辅助输出层。与最终预测层一样,它们由全连接层和 softmax 激活组成,输出数据集中类别的概率分布。尽管它们与最终预测层具有相同的外观,但它们不会对模型的最终输出做出贡献,但在训练过程中起着重要作用,稳定训练变得越来越艰难,因为模型变得越来越深(主要是由于计算机中数值的有限精度)。
让我们从头开始实现原始的 Inception 网络的一个版本。在此过程中,我们将讨论我们遇到的任何新概念。
注意!我们将构建一个略有不同的 Inception 网络 v1。
我们正在实现与原始 Inception 网络 v1 模型略有不同的东西,以应对某种实际限制。原始 Inception 网络设计用于处理尺寸为 224 × 224 × 3 的输入,属于 1,000 个类别,而我们有尺寸为 64 × 64 × 3 的输入,属于 200 个类别,我们将其调整为 56 × 56 × 3,以便其是 224 的因数(即,56 × 4 = 224)。因此,我们将对原始 Inception 网络进行一些修改。如果你愿意,你可以暂时忽略以下细节。但是如果你感兴趣,我们具体进行以下更改:
- 使前三个具有步长 2 的层(在 stem 中)的步长为 1,以便我们在拥有较小输入图像时享受模型的全部深度。
- 将最后一个全连接分类层的大小从 1,000 更改为 200,因为我们只有 200 个类别。
- 移除一些正则化(即,dropout、loss weighting;这些将在下一章重新引入)。
如果你对这里讨论的模型感到舒适,理解原始的 Inception v1 模型将不会有问题。
首先,我们定义一个创建 Inception net v1 干部结构的函数。干部结构是 Inception 网络的前几层,看起来不过是典型卷积/池化层,但有一个新的层(称为 lambda 层),执行一些称为 局部响应归一化(LRN)的功能。我们将在稍后更详细地讨论该层的目的(请参见下一个清单)。
代码清单 6.3 Inception 网络中的干部结构的定义
def stem(inp): conv1 = Conv2D( 64, (7,7), strides=(1,1), activation='relu', padding='same' )(inp) ❶ maxpool2 = MaxPool2D((3,3), strides=(2,2), padding='same')(conv1) ❷ lrn3 = Lambda( lambda x: tf.nn.local_response_normalization(x) )(maxpool2) ❸ conv4 = Conv2D( 64, (1,1), strides=(1,1), padding='same' )(lrn3) ❹ conv5 = Conv2D( 192, (3,3), strides=(1,1), activation='relu', padding='same' )(conv4) ❹ lrn6 = Lambda(lambda x: tf.nn.local_response_normalization(x))(conv5) ❺ maxpool7 = MaxPool2D((3,3), strides=(1,1), padding='same')(lrn6) ❻ return maxpool7 ❼
❶ 第一个卷积层的输出
❷ 第一个最大池化层的输出
❸ 第一个局部响应归一化层。我们定义一个封装了 LRN 功能的 lambda 函数。
❹ 后续的卷积层
❺ 第二个 LRN 层
❻ 最大池化层
❼ 返回最终输出(即最大池化层的输出)
TensorFlow 实战(二)(5)https://developer.aliyun.com/article/1522704