diff --git a/chapter_computer-vision/image-augmentation.md b/chapter_computer-vision/image-augmentation.md index 5dabbd63caa2ca7b685de35561435423188bcb1d..3f8bf3dcafe55854f5fdef031fd3849dd162a756 100644 --- a/chapter_computer-vision/image-augmentation.md +++ b/chapter_computer-vision/image-augmentation.md @@ -48,7 +48,7 @@ apply(img, transforms.RandomFlipTopBottom()) ```{.python .input n=5} shape_aug = transforms.RandomResizedCrop( - (200, 200), scale=(.1, 1), ratio=(.5, 2)) + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) apply(img, shape_aug) ``` @@ -57,20 +57,20 @@ apply(img, shape_aug) 形状变化外的一个另一大类是变化颜色。颜色一般有四个可以调的参数:亮度、对比、饱和度和色相。下面例子里我们随机将亮度在当前值上增加或减小一个在0到50%之前的量。 ```{.python .input n=6} -apply(img, transforms.RandomLighting(.5)) +apply(img, transforms.RandomBrightness(0.5)) ``` 同样的修改色相。 ```{.python .input n=7} -apply(img, transforms.RandomHue(.5)) +apply(img, transforms.RandomHue(0.5)) ``` 或者用使用`RandomColorJitter`来一起使用。 ```{.python .input n=8} color_aug = transforms.RandomColorJitter( - brightness=.5, contrast=.5, saturation=.5, hue=.5) + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) apply(img, color_aug) ``` @@ -89,7 +89,8 @@ apply(img, augs) 接下来我们来看一个将图片增广应用在实际训练的例子,并比较其与不使用时的区别。这里我们使用CIFAR-10数据集,而不是之前我们一直使用的FashionMNIST。原因在于FashionMNIST中物体位置和尺寸都已经统一化了,而CIFAR-10中物体颜色和大小区别更加显著。下面我们展示CIFAR-10中的前32张训练图片。 ```{.python .input n=10} -gb.show_images(gluon.data.vision.CIFAR10(train=True)[0:32][0], 4, 8, scale=0.8) +gb.show_images(gluon.data.vision.CIFAR10(train=True)[0:32][0], 4, 8, + scale=0.8); ``` 在训练时,我们通常将图片增广作用在训练图片上,使得模型能识别出各种变化过后的版本。这里我们仅仅使用最简单的随机水平翻转。此外我们使用`ToTensor`变换来图片转成MXNet需要的格式,即格式为(批量,通道,高,宽)以及类型为32位浮点数。 @@ -119,7 +120,7 @@ def load_cifar10(is_train, augs, batch_size): 我们使用ResNet 18来训练CIFAR-10。训练的的代码跟[“残差网络:ResNet”](..//chapter_convolutional-neural-networks/resnet.md)一致,除了使用所有可用的GPU和不同的学习率外。 ```{.python .input n=13} -def train(train_augs, test_augs, lr=.1): +def train(train_augs, test_augs, lr=0.1): batch_size = 256 ctx = gb.try_all_gpus() net = gb.resnet18(10) diff --git a/chapter_computer-vision/kaggle-gluon-cifar10.md b/chapter_computer-vision/kaggle-gluon-cifar10.md index f89962e139cc445e05cf8c9e395734b56c205fe1..fb3f85460deb4f5228ccf73514ba500cf3fccb12 100644 --- a/chapter_computer-vision/kaggle-gluon-cifar10.md +++ b/chapter_computer-vision/kaggle-gluon-cifar10.md @@ -29,7 +29,7 @@ import shutil ## 获取数据集 -比赛数据分为训练数据集和测试数据集。训练集包含5万张图片。测试集包含30万张图片:其中有1万张图片用来计分,其他29万张不计分的图片是为了防止人工标注测试集。两个数据集中的图片格式都是png,高和宽均为32,并含有RGB三个通道(彩色)。图片的类别数为10,类别分别为飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,如图9.X所示。 +比赛数据分为训练数据集和测试数据集。训练集包含5万张图片。测试集包含30万张图片:其中有1万张图片用来计分,其他29万张不计分的图片是为了防止人工标注测试集。两个数据集中的图片格式都是png,高和宽均为32像素,并含有RGB三个通道(彩色)。图片的类别数为10,类别分别为飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,如图9.X所示。 ![CIFAR-10图像的类别分别为飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。](../img/cifar10.png) @@ -47,7 +47,7 @@ import shutil * ../data/kaggle_cifar10/test/[1-300000].png * ../data/kaggle_cifar10/trainLabels.csv -为方便快速上手,我们提供了上述数据集的小规模采样,例如仅含100个训练样本的“train_tiny.zip”和1个测试样本的“test_tiny.zip”。它们解压后的文件夹名称分别为“train_tiny”和“test_tiny”。此外,训练数据集标签的压缩文件解压后得到“trainLabels.csv”。如果你将使用上述的Kaggle比赛完整数据集,还需要把下面`demo`变量改为`False`。 +为方便快速上手,我们提供了上述数据集的小规模采样,例如仅含100个训练样本的“train_tiny.zip”和1个测试样本的“test_tiny.zip”。它们解压后的文件夹名称分别为“train_tiny”和“test_tiny”。此外,训练数据集标签的压缩文件解压后得到“trainLabels.csv”。如果你将使用上述Kaggle比赛的完整数据集,还需要把下面`demo`变量改为`False`。 ```{.python .input} # 如果使用下载的 Kaggle 比赛的完整数据集,把下面改为 False。 @@ -61,7 +61,7 @@ if demo: ### 整理数据集 -我们定义下面的`reorg_cifar10_data`函数来整理数据集。整理后,同一类图片将被放在同一个文件夹下,便于我们稍后读取。该函数中的参数`valid_ratio`是验证集占原始训练集的比重。以`valid_ratio=0.1`为例,由于原始训练数据有50,000张图片,调参时将有45,000张图片用于训练并存放在路径“`input_dir`/train”,而另外5,000张图片为验证集并存放在路径“`input_dir`/valid”。 +我们定义下面的`reorg_cifar10_data`函数来整理数据集。整理后,同一类图片将被放在同一个文件夹下,便于我们稍后读取。该函数中的参数`valid_ratio`是验证集样本数与原始训练集样本数之比。以`valid_ratio=0.1`为例,由于原始训练数据集有50,000张图片,调参时将有45,000张图片用于训练并存放在路径“`input_dir`/train”,而另外5,000张图片为验证集并存放在路径“`input_dir`/valid”。 ```{.python .input n=2} def reorg_cifar10_data(data_dir, label_file, train_dir, test_dir, input_dir, @@ -112,11 +112,11 @@ def reorg_cifar10_data(data_dir, label_file, train_dir, test_dir, input_dir, ```{.python .input n=3} if demo: - # 注意:此处使用小训练集。Kaggle 比赛的完整数据集应包括 5 万训练样本。 + # 注意:此处使用小训练集。 train_dir = 'train_tiny' - # 注意:此处使用小测试集。Kaggle 比赛的完整数据集应包括 30 万测试样本。 + # 注意:此处使用小测试集。 test_dir = 'test_tiny' - # 注意:此处相应使用小批量。使用 Kaggle 比赛的完整数据集时可设较大的整数。 + # 注意:此处将批量大小相应设小。使用 Kaggle 比赛的完整数据集时可设较大整数。 batch_size = 1 else: train_dir = 'train' @@ -131,18 +131,20 @@ reorg_cifar10_data(data_dir, label_file, train_dir, test_dir, input_dir, valid_ratio) ``` -## 增广数据 +## 图片增广 -为应对过拟合,我们在这里使用`transforms`来增广数据。例如,加入`transforms.RandomFlipLeftRight()`即可随机对图片做镜面反转。我们也通过`transforms.Normalize()`对彩色图像RGB三个通道分别做标准化。以下列举了部分操作。这些操作可以根据需求来决定是否使用,它们的超参数也都是可调的。 +为应对过拟合,我们在这里使用`transforms`来增广数据。例如,加入`transforms.RandomFlipLeftRight()`即可随机对图片做镜面反转。我们也通过`transforms.Normalize()`对彩色图像RGB三个通道分别做标准化。以下列举了部分操作。这些操作可以根据需求来决定是否使用或修改。 ```{.python .input n=4} transform_train = transforms.Compose([ - # 随机按照 scale 和 ratio 裁剪,并放缩为 32 x 32 的正方形。 - transforms.RandomResizedCrop(32, scale=(0.08, 1.0), - ratio=(3.0/4.0, 4.0/3.0)), + # 将图片放大成高和宽各为 40 像素的正方形。 + transforms.Resize(40), + # 随机对高和宽各为 40 像素的正方形图片裁剪出面积为原图片面积 0.64 到 1 倍之间的小正方 + # 形,再放缩为高和宽各为 32 像素的正方形。 + transforms.RandomResizedCrop(32, scale=(0.64, 1.0), ratio=(1.0, 1.0)), # 随机左右翻转图片。 transforms.RandomFlipLeftRight(), - # 将图片像素值缩小到(0, 1)内,并将数据格式从“高*宽*通道”改为“通道*高*宽”。 + # 将图片像素值按比例缩小到 0 和 1 之间,并将数据格式从“高*宽*通道”改为“通道*高*宽”。 transforms.ToTensor(), # 对图片的每个通道做标准化。 transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]) @@ -344,10 +346,9 @@ df.to_csv('submission.csv', index=False) ## 练习 -* 使用Kaggle比赛的完整CIFAR-10数据集。把`batch_size`和`num_epochs`分别改为128和100。看看你可以在这个比赛中拿到什么样的准确率和名次? +* 使用Kaggle比赛的完整CIFAR-10数据集。把批量大小`batch_size`和迭代周期数`num_epochs`分别改为128和100。看看你可以在这个比赛中拿到什么样的准确率和名次? * 如果不使用增强数据的方法能拿到什么样的准确率? -* 在`transforms.RandomResizedCrop`前依次添加以下数据增广操作(使用逗号隔开):transforms.CenterCrop(32)、transforms.RandomFlipTopBottom()、transforms.RandomColorJitter(brightness=0.0, contrast=0.0, saturation=0.0, hue=0.0)、transforms.RandomLighting(0.0)、transforms.Cast('float32')和transforms.Resize(32)。调一调它们的超参数。对结果有什么影响? -* 扫码直达讨论区,在社区交流方法和结果。相信你一定会有收获。 +* 扫码直达讨论区,在社区交流方法和结果。你能发掘出其他更好的技巧吗? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1545/) diff --git a/chapter_computer-vision/kaggle-gluon-dog.md b/chapter_computer-vision/kaggle-gluon-dog.md index 4fea3c9603cb238a56cf000ed33c08bcb42a0f6d..d0233d67976d0aeddf381d60d1a48f9e239b21c9 100644 --- a/chapter_computer-vision/kaggle-gluon-dog.md +++ b/chapter_computer-vision/kaggle-gluon-dog.md @@ -1,13 +1,11 @@ # 实战Kaggle比赛:狗的品种识别 (ImageNet Dogs) -我们在本章中选择了Kaggle中的[狗的品种识别问题](https://www.kaggle.com/c/dog-breed-identification)。这是著名的ImageNet的子集数据集。与之前的[CIFAR-10原始图像分类问题](kaggle-gluon-cifar10.md)不同,本问题中的图片文件大小更接近真实照片大小,且大小不一。本问题的输出也变的更加通用:我们将输出每张图片对应120种狗的分别概率。 - 我们将在本节动手实战Kaggle比赛中的狗的品种识别问题。该比赛的网页地址是 > https://www.kaggle.com/c/dog-breed-identification -在这个比赛中,我们将识别120类不同品种的狗。这个比赛的数据集实际上是著名的ImageNet的子集数据集。和上一节CIFAR-10数据集中的图像不同,ImageNet数据集中的图像更加接近真实图片大小,且大小不一。 +在这个比赛中,我们将识别120类不同品种的狗。这个比赛的数据集实际上是著名的ImageNet的子集数据集。和上一节CIFAR-10数据集中的图像不同,ImageNet数据集中的图像的高和宽更大,且大小不一。 图9.X展示了该比赛的网页信息。为了便于提交结果,请先在Kaggle网站上注册账号。 @@ -33,38 +31,25 @@ import shutil import zipfile ``` -## 整理原始数据集 - -比赛数据分为训练数据集和测试数据集。训练集包含10,222张图片。测试集包含10,357张图片。 +## 获取数据集 -两个数据集都是jpg彩色图片,大小接近真实照片大小,且大小不一。训练集一共有120类狗的图片。 +比赛数据分为训练数据集和测试数据集。训练集包含10,222张图片。测试集包含10,357张图片。两个数据集中的图片格式都是jpg。这些图片都含有RGB三个通道(彩色),高和宽的大小不一。训练集中狗的类别共有120种,例如拉布拉多、贵宾、腊肠、萨摩耶、哈士奇、吉娃娃和约克夏。 ### 下载数据集 - -登录Kaggle后,数据可以从[120种狗类识别问题](https://www.kaggle.com/c/dog-breed-identification/data)中下载。 - -* [训练数据集train.zip下载地址](https://www.kaggle.com/c/dog-breed-identification/download/train.zip) - -* [测试数据集test.zip下载地址](https://www.kaggle.com/c/dog-breed-identification/download/test.zip) - -* [训练数据标签label.csv.zip下载地址](https://www.kaggle.com/c/dog-breed-identification/download/labels.csv.zip) - - -### 解压数据集 - -训练数据集train.zip和测试数据集test.zip都是压缩格式,下载后它们的路径可以如下: +登录Kaggle后,我们可以点击图9.X所示的狗的品种识别比赛网页上的“Data”标签,并分别下载训练数据集“train.zip”、测试数据集“test.zip”和训练数据集标签“label.csv.zip”。下载完成后,将它们分别存放在以下路径: * ../data/kaggle_dog/train.zip * ../data/kaggle_dog/test.zip * ../data/kaggle_dog/labels.csv.zip -为了使网页编译快一点,我们在git repo里仅仅存放小数据样本('train_valid_test_tiny.zip')。执行以下代码会从git repo里解压生成小数据样本。 + +为方便快速上手,我们提供了上述数据集的小规模采样“train_valid_test_tiny.zip”。如果你将使用上述Kaggle比赛的完整数据集,还需要把下面`demo`变量改为`False`。 ```{.python .input n=1} -# 如果训练下载的Kaggle的完整数据集,把demo改为False。 +# 如果使用下载的 Kaggle 比赛的完整数据集,把下面改为 False。 demo = True data_dir = '../data/kaggle_dog' @@ -80,9 +65,8 @@ for f in zipfiles: ### 整理数据集 -对于Kaggle的完整数据集,我们需要定义下面的`reorg_dog_data`函数来整理一下。整理后,同一类狗的图片将出现在在同一个文件夹下,便于`Gluon`稍后读取。 - -函数中的参数如data_dir、train_dir和test_dir对应上述数据存放路径及原始训练和测试的图片集文件夹名称。参数label_file为训练数据标签的文件名称。参数input_dir是整理后数据集文件夹名称。参数valid_ratio是验证集中每类狗的数量占原始训练集中数量最少一类的狗的数量(66)的比重。 +我们定义下面的`reorg_dog_data`函数来整理Kaggle比赛的完整数据集。整理后,同一类狗的图片将被放在同一个文件夹下,便于我们稍后读取。 +该函数中的参数`valid_ratio`是验证集中每类狗的样本数与原始训练集中数量最少一类的狗的样本数(66)之比。 ```{.python .input n=2} def reorg_dog_data(data_dir, label_file, train_dir, test_dir, input_dir, @@ -96,10 +80,10 @@ def reorg_dog_data(data_dir, label_file, train_dir, test_dir, input_dir, labels = set(idx_label.values()) n_train = len(os.listdir(os.path.join(data_dir, train_dir))) - # 训练集中数量最少一类的狗的数量。 + # 训练集中数量最少一类的狗的样本数。 min_n_train_per_label = ( collections.Counter(idx_label.values()).most_common()[:-2:-1][0][1]) - # 验证集中每类狗的数量。 + # 验证集中每类狗的样本数。 n_valid_per_label = math.floor(min_n_train_per_label * valid_ratio) label_count = {} @@ -131,14 +115,14 @@ def reorg_dog_data(data_dir, label_file, train_dir, test_dir, input_dir, os.path.join(data_dir, input_dir, 'test', 'unknown')) ``` -再次强调,为了使网页编译快一点,我们在这里仅仅使用小数据样本。相应地,我们仅将批量大小设为2。实际训练和测试时应使用Kaggle的完整数据集并调用`reorg_dog_data`函数整理便于`Gluon`读取的格式。由于数据集较大,批量大小batch_size大小可设为一个较大的整数,例如128。 +我们在这里仅仅使用小数据集。相应地,我们仅将批量大小设为1。实际训练和测试时应使用Kaggle比赛的完整数据集并调用`reorg_dog_data`函数整理数据集。同时,我们也需要将批量大小`batch_size`设为一个较大的整数,例如128。 ```{.python .input n=3} if demo: - # 注意:此处使用小数据集为便于网页编译。 + # 注意:此处使用小数据集。 input_dir = 'train_valid_test_tiny' - # 注意:此处相应使用小批量。对Kaggle的完整数据集可设较大的整数,例如128。 - batch_size = 2 + # 注意:此处将批量大小相应设小。使用 Kaggle 比赛的完整数据集时可设较大整数。 + batch_size = 1 else: label_file = 'labels.csv' train_dir = 'train' @@ -150,43 +134,41 @@ else: valid_ratio) ``` -## 使用Gluon读取整理后的数据集 +## 图片增广 -为避免过拟合,我们在这里使用`transforms`来增广数据集。例如我们加入`transforms.RandomFlipLeftRight()`即可随机对每张图片做镜面反转。以下我们列举了所有可能用到的操作,这些操作可以根据需求来决定是否调用,它们的参数也都是可调的。 +为应对过拟合,我们在这里使用`transforms`来增广数据集。例如,加入`transforms.RandomFlipLeftRight()`即可随机对图片做镜面反转。我们也通过`transforms.Normalize()`对彩色图像RGB三个通道分别做标准化。以下列举了部分操作。这些操作可以根据需求来决定是否使用或修改。 ```{.python .input n=4} transform_train = transforms.Compose([ - # transforms.CenterCrop(32), - # transforms.RandomFlipTopBottom(), - # transforms.RandomColorJitter(brightness=0.0, contrast=0.0, - # saturation=0.0, hue=0.0), - # transforms.RandomLighting(0.0), - # transforms.Cast('float32'), - - # 将图片按比例放缩至短边为256像素 - transforms.Resize(256), - # 随机按照scale和ratio裁剪,并放缩为224x224的正方形 + # 随机对图片裁剪出面积为原图片面积 0.08 到 1 倍之间、且高和宽之比在 3/4 和 4/3 之间 + # 的图片,再放缩为高和宽均为 224 像素的新图片。 transforms.RandomResizedCrop(224, scale=(0.08, 1.0), ratio=(3.0/4.0, 4.0/3.0)), - # 随机左右翻转图片 + # 随机左右翻转图片。 transforms.RandomFlipLeftRight(), - # 将图片像素值缩小到(0,1)内,并将数据格式从"高*宽*通道"改为"通道*高*宽" + # 随机抖动亮度、对比度和饱和度。 + transforms.RandomColorJitter(brightness=0.4, contrast=0.4, + saturation=0.4), + # 随机加噪音。 + transforms.RandomLighting(0.1), + + # 将图片像素值按比例缩小到 0 和 1 之间,并将数据格式从“高*宽*通道”改为“通道*高*宽”。 transforms.ToTensor(), - # 对图片的每个通道做标准化 + # 对图片的每个通道做标准化。 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) -# 去掉随机裁剪/翻转,保留确定性的图像预处理结果 +# 测试时,只使用确定性的图像预处理操作。 transform_test = transforms.Compose([ transforms.Resize(256), - # 将图片中央的224x224正方形区域裁剪出来 + # 将图片中央的高和宽均为 224 的正方形区域裁剪出来。 transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) ``` -接下来,我们可以使用`Gluon`中的`ImageFolderDataset`类来读取整理后的数据集。注意,我们要在`loader`中调用刚刚定义好的图片增广函数。通过`vision.ImageFolderDataset`读入的数据是一个`(image, label)`组合,`transform_first()`的作用便是对这个组合中的第一个成员(即读入的图像)做图片增广操作。 +接下来,我们可以使用`ImageFolderDataset`类来读取整理后的数据集,其中每个数据样本包括图像和标签。需要注意的是,我们要在`DataLoader`中调用刚刚定义好的图片增广函数。其中`transform_first`函数指明对每个数据样本中的图像做数据增广。 ```{.python .input n=5} # 读取原始图像文件。flag=1 说明输入图像有三个通道(彩色)。 @@ -209,47 +191,31 @@ test_data = gdata.DataLoader(test_ds.transform_first(transform_test), batch_size, shuffle=False, last_batch='keep') ``` -## 设计模型 - -这个比赛的数据属于ImageNet数据集的子集,因此我们可以借助[迁移学习](fine-tuning.md)的思想,选用在ImageNet全集上预训练过的模型,并通过微调在新数据集上进行训练。`Gluon`提供了不少预训练模型,综合考虑模型大小与准确率,我们选择使用[ResNet-34](resnet-gluon.md)。 - -这里,我们使用与前述教程略微不同的迁移学习方法。在新的训练数据与预训练数据相似的情况下,我们认为原有特征是可重用的。基于这个原因,在一个预训练好的新模型上,我们可以不去改变原已训练好的权重,而是在原网络结构上新加一个小的输出网络。 - -在训练过程中,我们让训练图片通过正向传播经过原有特征层与新定义的全连接网络,然后只在这个小网络上通过反向传播更新权重。这样的做法既能够节省在整个模型进行后向传播的时间,也能节省在特征层上储存梯度所需要的内存空间。 +## 定义模型并使用微调 -注意,我们在之前定义的数据预处理函数里用了ImageNet数据集上的均值和标准差做标准化,这样才能保证预训练模型能够捕捉正确的数据特征。 +这个比赛的数据属于ImageNet数据集的子集,因此我们可以应用[“微调”](fine-tuning.md)一节中介绍的思路,选用在ImageNet完整数据集上预训练过的模型,并通过微调在比赛数据集上进行训练。Gluon提供了丰富的预训练模型,我们在这里以预训练过的ResNet-34模型为例。由于比赛数据集属于预训练数据集的子集,我们可以重用预训练模型在输出层的输入(即特征),并将原输出层替换成新的可以训练的小规模输出网络,例如两个串联的全连接层。由于预训练模型的参数在训练中是固定的,我们既节约了它们的训练时间,又节省了存储它们梯度所需的空间。 -![](../img/fix_feature_fine_tune.png) - -首先我们定义一个网络,并拿到预训练好的`ResNet-34`模型权重。接下来我们新定义一个两层的全连接网络作为输出层,并初始化其权重,为接下来的训练做准备。 +需要注意的是,我们在图片增广中使用了ImageNet数据集上RGB三个通道的均值和标准差做标准化,这和预训练模型所做的标准化是一致的。 ```{.python .input n=6} def get_net(ctx): - # 设置 pretrained=True 就能拿到预训练模型的权重,第一次使用需要联网下载。 + # 设 pretrained=True 就能获取预训练模型的参数。第一次使用时需要联网下载。 finetune_net = model_zoo.vision.resnet34_v2(pretrained=True) # 定义新的输出网络。 finetune_net.output_new = nn.HybridSequential(prefix='') - # 定义256个神经元的全连接层。 finetune_net.output_new.add(nn.Dense(256, activation='relu')) - # 定义120个神经元的全连接层,输出分类预测。 + # 120是输出的类别数。 finetune_net.output_new.add(nn.Dense(120)) - # 初始化这个输出网络。 + # 初始化输出网络。 finetune_net.output_new.initialize(init.Xavier(), ctx=ctx) - # 把网络参数分配到即将用于计算的CPU/GPU上。 + # 把模型参数分配到即将用于计算的 CPU或GPU 上。 finetune_net.collect_params().reset_ctx(ctx) return finetune_net ``` -## 训练模型并调参 - -在[过拟合](../chapter_supervised-learning/underfit-overfit.md)中我们讲过,过度依赖训练数据集的误差来推断测试数据集的误差容易导致过拟合。由于图像分类训练时间可能较长,为了方便,我们这里不再使用K折交叉验证,而是依赖验证集的结果来调参。 - -我们定义损失函数以便于计算验证集上的损失函数值。我们也定义了模型训练函数,其中的优化算法和参数都是可以调的。 - -注意,我们为了只更新新的输出层参数,做了两处修改: +## 定义训练函数 -1. 在`gluon.Trainer`里只对`net.output_new.collect_params()`定义了优化方法和参数。 -2. 在训练时只在新输出层上记录自动求导的结果。 +我们将依赖模型在验证集上的表现来选择模型并调节超参数。模型的训练函数`train`只会训练我们定义的输出网络。我们记录了每个迭代周期的训练时间。这有助于比较不同模型的时间开销。 ```{.python .input n=7} loss = gloss.SoftmaxCrossEntropyLoss() @@ -258,16 +224,16 @@ def get_loss(data, net, ctx): l = 0.0 for X, y in data: y = y.as_in_context(ctx) - # 计算特征层的结果。 + # 计算预训练模型输出层的输入,即特征。 output_features = net.features(X.as_in_context(ctx)) - # 将特征层的结果作为输入,计算全连接网络的结果。 + # 将特征作为我们定义的输出网络的输入,计算输出。 outputs = net.output_new(output_features) l += loss(outputs, y).mean().asscalar() return l / len(data) def train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period, lr_decay): - # 只在新的全连接网络的参数上进行训练。 + # 只训练我们定义的输出网络。 trainer = gluon.Trainer(net.output_new.collect_params(), 'sgd', {'learning_rate': lr, 'momentum': 0.9, 'wd': wd}) prev_time = datetime.datetime.now() @@ -277,13 +243,13 @@ def train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period, trainer.set_learning_rate(trainer.learning_rate * lr_decay) for X, y in train_data: y = y.astype('float32').as_in_context(ctx) - # 正向传播计算特征层的结果。 + # 计算预训练模型输出层的输入,即特征。 output_features = net.features(X.as_in_context(ctx)) with autograd.record(): - # 将特征层的结果作为输入,计算全连接网络的结果。 + # 将特征作为我们定义的输出网络的输入,计算输出。 outputs = net.output_new(output_features) l = loss(outputs, y) - # 反向传播与权重更新只发生在全连接网络上。 + # 反向传播只发生在我们定义的输出网络上。 l.backward() trainer.step(batch_size) train_l += l.mean().asscalar() @@ -302,15 +268,18 @@ def train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period, print(epoch_s + time_s + ', lr ' + str(trainer.learning_rate)) ``` -以下定义训练参数并训练模型。这些参数均可调。为了使网页编译快一点,我们这里将epoch数量有意设为1。事实上,epoch一般可以调大些。我们将依据验证集的结果不断优化模型设计和调整参数。 +## 训练并验证模型 -另外,微调一个预训练模型往往不需要特别久的额外训练。依据下面的参数设置,优化算法的学习率设为0.01,并将在每10个epoch自乘0.1。 +现在,我们可以训练并验证模型了。以下的超参数都是可以调节的,例如增加迭代周期。 ```{.python .input n=9} ctx = gb.try_gpu() num_epochs = 1 +# 学习率。 lr = 0.01 +# 权重衰减参数。 wd = 1e-4 +# 优化算法的学习率将在每10个迭代周期时自乘0.1。 lr_period = 10 lr_decay = 0.1 @@ -320,9 +289,9 @@ train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period, lr_decay) ``` -## 对测试集分类 +## 对测试集分类并在Kaggle提交结果 -当得到一组满意的模型设计和参数后,我们使用全部训练数据集(含验证集)重新训练模型,并对测试集分类。注意,我们要用刚训练好的新输出层做预测。 +当得到一组满意的模型设计和超参数后,我们使用全部训练数据集(含验证集)重新训练模型,并对测试集分类。注意,我们要用刚训练好的输出网络做预测。 ```{.python .input n=8} net = get_net(ctx) @@ -332,9 +301,9 @@ train(net, train_valid_data, None, num_epochs, lr, wd, ctx, lr_period, preds = [] for data, label in test_data: - # 计算特征层的结果。 + # 计算预训练模型输出层的输入,即特征。 output_features = net.features(data.as_in_context(ctx)) - # 将特征层的结果作为输入,计算全连接网络的结果。 + # 将特征作为我们定义的输出网络的输入,计算输出。 output = nd.softmax(net.output_new(output_features)) preds.extend(output.asnumpy()) ids = sorted(os.listdir(os.path.join(data_dir, input_dir, 'test/unknown'))) @@ -345,18 +314,19 @@ with open('submission.csv', 'w') as f: [str(num) for num in output]) + '\n') ``` -执行完上述代码后,会生成一个“submission.csv”文件。这个文件符合Kaggle比赛要求的提交格式。这时我们可以在Kaggle上把对测试集分类的结果提交并查看分类准确率。你需要登录Kaggle网站,访问ImageNet Dogs比赛网页,并点击右侧“Submit Predictions”或“Late Submission”按钮 [1]。然后,点击页面下方“Upload Submission File”选择需要提交的分类结果文件。最后,点击页面最下方的“Make Submission”按钮就可以查看结果了。 +执行完上述代码后,会生成一个“submission.csv”文件。这个文件符合Kaggle比赛要求的提交格式。这时我们可以在Kaggle上把对测试集分类的结果提交并查看分类准确率。你需要登录Kaggle网站,访问ImageNet Dogs比赛网页,并点击右侧“Submit Predictions”或“Late Submission”按钮。然后,点击页面下方“Upload Submission File”选择需要提交的分类结果文件。最后,点击页面最下方的“Make Submission”按钮就可以查看结果了。 ## 小结 -* 我们可以利用在ImageNet数据集上预训练的模型对它的子集数据集做分类。 +* 我们可以使用在ImageNet数据集上预训练的模型并微调,从而以较小的计算开销对ImageNet的子集数据集做分类。 ## 练习 -* 使用Kaggle完整数据集,把batch_size和num_epochs分别调大些,可以在Kaggle上拿到什么样的准确率和名次? -* 扫码直达讨论区,在社区交流方法和结果。相信你一定会有收获。 +* 使用Kaggle完整数据集,把批量大小`batch_size`和迭代周期数`num_epochs`分别调大些,可以在Kaggle上拿到什么样的结果? +* 使用更深的预训练模型并微调,你能获得更好的结果吗? +* 扫码直达讨论区,在社区交流方法和结果。你能发掘出其他更好的技巧吗? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/2399) diff --git a/chapter_supervised-learning/kaggle-gluon-kfold.md b/chapter_supervised-learning/kaggle-gluon-kfold.md index 7ce3372c9abf22edd49d765179f6179e6bbaeb0e..c115dd746613b420677d7934030e413457f010e6 100644 --- a/chapter_supervised-learning/kaggle-gluon-kfold.md +++ b/chapter_supervised-learning/kaggle-gluon-kfold.md @@ -259,7 +259,7 @@ train_and_pred(num_epochs, verbose_epoch, train_features, test_features, * 在Kaggle提交本教程的预测结果。观察一下,这个结果能在Kaggle上拿到什么样的分数? * 对照$K$折交叉验证结果,不断修改模型(例如添加隐藏层)和调参,你能提高Kaggle上的分数吗? * 如果不使用本节中对连续数值特征的标准化处理,结果会有什么变化? -* 扫码直达讨论区,在社区交流方法和结果。相信你一定会有收获。 +* 扫码直达讨论区,在社区交流方法和结果。你能发掘出其他更好的技巧吗? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1039)