d2l第13章的两个kaggle,以及该章的aug,finetune,cls的dataset处理

本文从dog种类kaggle开始着手,其中包含各种详细说明,并在文章最后附上补充条目。

目录

1.dog_kaggle

1.1图像分类数据集准备

 目的:

1.2图像增广

1.3加载数据集,dataset与dataloader

1.4模型net搭配--迁移学习finetune

1.5训练--学习率下降操作:

1.6预测与输出结果

2.探索--finetune

2.1修改fc

2.2自定义修改与添加xavier初始化参数:

 2.3封锁层的优化参数

2.4直观对比


1.dog_kaggle

1.1图像分类数据集准备

在kaggle中,下载得到的文件是这个样的:(当然这个train_valid_test是后面建的)其中这个csv文件里面有两列信息,第一列是图片名称,第二列是该图片对应的label。第一列中的图片随机的分布在test和train这两个文件夹中。

 目的:

在图像分类dataset中,最简单的加载方法就是建立一个文件夹,其中有若干个以label命名的文件夹,每个label文件夹中包含着都是该label的图片。那么目的有了,让我们来实现:

首先,读取上述的csv文件,并生成一个dict(pic_name,pic_label),这里注意,dict生成字典里面的key必须是不可更改的数据结构,像list就不行,这里两个都是str,故可以。

data_dir = '/CV/xhr/kaggle_cifar10_tiny/kaggle_cifar10_tiny'

def read_csv_labels(fname):
    """读取fname来给标签字典返回⼀个⽂件名"""
    with open(fname, 'r') as f:
        # 跳过⽂件头⾏(列名)
        lines = f.readlines()[1:]
    tokens = [l.rstrip().split(',') for l in lines]
    return dict((name, label) for name, label in tokens)

labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# 训练样本 :', len(labels))
print('# 类别 :', len(set(labels.values())))

'''
训练样本:  1000
类别数量:  120
'''

再分别创建三个文件夹,进行对原图片复制的操作。

首先先定义一个复制文件的函数:

def copyfile(filename, target_dir):
    os.makedirs(target_dir, exist_ok=True)
    shutil.copy(filename, target_dir)

函数解析:exist_ok=True表示若要创建的目录已经存在,则不会报错直接返回,若设置为False,则会返回FileExistsError。

shutil是一个包,里面的.copy函数是将文件从原路径filename复制到目标路径target_dir。注意,在此移动函数中,原文件是到具体的某一张图片,目标路径targer_dir可以指向某一个文件夹,这样前面不同的filename都会挪到同一个文件夹中。

再定义重头戏:创建并挪图片!

def reorg_train_valid(data_dir, labels, valid_ratio):
    n = collections.Counter(labels.values()).most_common()[-1][1]
    n_valid_per_label = max(1, math.floor(n * valid_ratio))
    label_count = {}
    for train_file in os.listdir(os.path.join(data_dir, 'train')):
        label = labels[train_file.split('.')[0]] # 得到每个图片名(除后缀),
                                                 # 然后通过labels字典里面key索引
                                                 # 这个label是图片的种类标签
        fname = os.path.join(data_dir, 'train', train_file)
        copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                     'train_valid', label))
        if label not in label_count or label_count[label] < n_valid_per_label:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                          'valid', label))
            label_count[label] = label_count.get(label, 0) + 1
            
        else:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                         'train', label))
    return n_valid_per_label

1.其中collections.Counter用于计算元素在一个可迭代对象中出现的次数,并返回一个Counter对象,键是元素,值是元素出现的次数。

2.most_common()方法返回一个列表,其中列表里面包含(value,num)的元组,分别记录出现的value是什么,出现了多少次,由最多到最低排列。

3.[-1][1]则返回的是出现次数最少的value对应的最少次数。

在此做了一个demo:

import collections

labels = {'a': 1, 'b': 2, 'c': 1, 'd': 3, 'e': 1, 'f': 2}  # 示例字典
print(labels['a'])
# 统计每个元素出现的次数,并返回出现次数最少的元素的出现次数
n = collections.Counter(labels.values())
print(n)
n1 = collections.Counter(labels.values()).most_common()
print(n1)
n2 = collections.Counter(labels.values()).most_common()[-1][1]
print(n2)

'''
1
Counter({1: 3, 2: 2, 3: 1})
[(1, 3), (2, 2), (3, 1)]
1
'''

4.这里返回最少出现的种类的出现频次n,是在设定验证集占训练集比值r后,验证集为每个类别拆分出max([nr],1)张图像.

5.其中,[nr]是向下取整,如果是1.6则取1

6.这个循环中,os.listdir(path)是返回该路径下所有的文件名字,在这里train_file是train文件下所有图片的名字

7.label返回的是每个train_file(train文件中的一张图片)的类别名字(labels字典里面对应key的value),这里labels传入的是前面生成的dict(pic_name,pic_label),这里通过索引字典的key(pic_name),返回该键的值(pic_label),这里两个都是str,所以返回的种类也是str

8.fname是将图片名与前面的data_dir名+train_valid拼起来,称为图片复制到的目标路径,然后再用之前定义的copyfile函数复制到train_valid里面的label文件中
## 对上面举个例子,比如train里面第一张图片1.png,在csv文件里面的label是frog,则labels字典里面为{'1','frog},此处train_file:'1.png',label为'frog',fname为该图片的绝对路径。具体见下图:

 9.里面的if判断函数,是抽取train_valid用的,当该label没有在label_count中,或者在但是对应该label的value小于n_vpl时,则抽取该图片到train_valid中的label文件夹中,当作验证集。

10.dict.get(label,0)表示索取dice中,ket=label的value,如果不存在该key,则创建一个且让他的value=0

 11.这样经过判断条件后,得到的valid是每个类别中取n_valid_per_label,即验证集占比×最少类别数向下取整的数目,其真实占比并不是所有训练集的比例。在本例中,最少类别样本数n×ratio向下取整=8,所以总valid数目为cls×8=80张图片,所以其占比一般比ratio小。

12.为什么会有train_valid_test?这里其实是做了1折验证,从train训练在valid检验调参,调好参数后,使用全部的训练数据进行训练,就是train_valid_test,它就是原始的train按类别分别建文件夹并挪图片的

---------------------------------------------------------分割线--------------------------------------------------------------

接下来再做一个test文件,方便读取test图片:

def reorg_test(data_dir):
    """在预测期间整理测试集,以⽅便读取"""
    for test_file in os.listdir(os.path.join(data_dir, 'test')):
        copyfile(os.path.join(data_dir, 'test', test_file),
                os.path.join(data_dir, 'train_valid_test', 'test',
                        'unknown'))

定义一个函数,来调用前面的read_csv_lables,reorg_train_valid和reorg_test:

def reorg_cifar10_data(data_dir, valid_ratio):
    labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
    reorg_train_valid(data_dir, labels, valid_ratio)
    reorg_test(data_dir)

定义超参数:

batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)

到此,即生成了train_test_valid文件夹,其内部是这样的:

1.2图像增广

transform_train = torchvision.transforms.Compose([
    # 在⾼度和宽度上将图像放⼤到40像素的正⽅形
    torchvision.transforms.Resize(40),
    # 随机裁剪出⼀个⾼度和宽度均为40像素的正⽅形图像,
    # ⽣成⼀个⾯积为原始图像⾯积0.64到1倍的⼩正⽅形,
    # 然后将其缩放为⾼度和宽度均为32像素的正⽅形
    torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
                                                ratio=(1.0, 1.0)),
    # 随机水平翻转,默认0.5概率
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    # 标准化图像的每个通道
    torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
                                    [0.2023, 0.1994, 0.2010])])

RandomResizedCrop里面可以设置剪裁大小,这里设置为64%-100%,ration是剪裁高宽比,这里保持与原图不变,因为原图也是正方形。

RandomResizedCrop表示在原图中随机裁剪一部分,不设置scale的话通常在(0.08到1.0)之间,位置随机裁剪尺寸也随机,然后再缩放到指定的大小。

Resize会将短边缩到括号尺寸,长边保持比例缩放,图像高宽比不变。

在对test的增广,一般不会进行别的操作,只是按惯例或者与原net一致的操作。

transform_test = torchvision.transforms.Compose([
    torchvision.transforms.Resize(256),
    # 从图像中⼼裁切224x224⼤⼩的图⽚
    torchvision.transforms.CenterCrop(224),
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize([0.485, 0.456, 0.406],
    [0.229, 0.224, 0.225])])

1.3加载数据集,dataset与dataloader

已经按上述要求创建好的文件夹,直接送入dataset即可。注意一下这里folder用for loop写法。

这里其实就是ImageFloder(文件夹路径),transform=...

train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
    os.path.join(data_dir, 'train_valid_test', folder),
    transform=transform_train) for folder in ['train', 'train_valid']]

valid_ds, test_ds = [torchvision.datasets.ImageFolder(
    os.path.join(data_dir, 'train_valid_test', folder),
    transform=transform_test) for folder in ['valid', 'test']]

train_iter, train_valid_iter = [torch.utils.data.DataLoader(
    dataset, batch_size, shuffle=True, drop_last=True)
    for dataset in (train_ds, train_valid_ds)]

valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
    drop_last=True)

drop_last是如果扫到最后不足bs,则后面这一点就丢掉了,但是test不能丢,因为要输出每一个test结果的


1.4模型net搭配--迁移学习finetune

这里的方式是将原net赋到一个新的子块features中,然后再新创一个mlp达到最终输出cls的目的。在第2块探索中会提到其他变式。

因为这个dog的分类是imagenet的子集,所以固定住前面的参数不变,只学习后面新创建的output_new。

def get_net(devices):
    finetune_net = nn.Sequential()
    # 定义迁移的原resnet34块叫features
    finetune_net.features = torchvision.models.resnet34(pretrained=True)
    # 定义⼀个新的输出⽹络,叫output_new,共有120个输出类别
    finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
                                            nn.ReLU(),
                                            nn.Linear(256, 120))
    # 将模型参数分配给⽤于计算的CPU或GPU
    finetune_net = finetune_net.to(devices[0])
    # 冻结参数
    for param in finetune_net.features.parameters():
        param.requires_grad = False
    return finetune_net

这个操作是在原始输出的fc之后,新加了out_new层:

这里的features和out_new都是自己定义的名字,可以理解为块的名字,resnet34网络作为一块,名字叫features,后面两个全连接层作为一块,名字叫output_new:

 定义损失计算:

loss = nn.CrossEntropyLoss(reduction='none')

def evaluate_loss(data_iter, net, devices):
    l_sum, n = 0.0, 0
    for features, labels in data_iter:
        features, labels = features.to(devices[0]), labels.to(devices[0])
        outputs = net(features)
        l = loss(outputs, labels)
        l_sum += l.sum()
        n += labels.numel()
    return (l_sum / n).to('cpu')

1.5训练--学习率下降操作:

其他的代码跟之前的都差不多,注意这里面新加了scheduler,设置的是每经过lr_period个epoch之后,学习率下降lr_decay。另需注意除了trainer.step()后,还要在每个epoch后面添加scheduler.step()命令。

def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
            lr_decay):
    trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9,
                                weight_decay=wd)
    scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)
    num_batches, timer = len(train_iter), d2l.Timer()
    legend = ['train loss', 'train acc']
    if valid_iter is not None:
        legend.append('valid acc')
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=legend)
    net = nn.DataParallel(net, device_ids=devices).to(devices[0])
    for epoch in range(num_epochs):
        net.train()
        metric = d2l.Accumulator(3)
        for i, (features, labels) in enumerate(train_iter):
            timer.start()
            l, acc = d2l.train_batch_ch13(net, features, labels,
                                        loss, trainer, devices)
            metric.add(l, acc, labels.shape[0])
            timer.stop()
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                    (metric[0] / metric[2], metric[1] / metric[2],
                    None))
        if valid_iter is not None:
            valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter)
            animator.add(epoch + 1, (None, None, valid_acc))
        scheduler.step()
    measures = (f'train loss {metric[0] / metric[2]:.3f}, '
                f'train acc {metric[1] / metric[2]:.3f}')
    if valid_iter is not None:
        measures += f', valid acc {valid_acc:.3f}'
    print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}'
        f' examples/sec on {str(devices)}')

训练命令行:

devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 2e-4, 5e-4
lr_period, lr_decay, net = 4, 0.9, get_net()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
    lr_decay)

1.6预测与输出结果

在这里注意,之前是1折验证,现在是超参数调整好了之后,应用全部的train数据进行训练,所以需要重新设置一下训练的数据。

net, preds = get_net(), []
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
        lr_decay)

for X, _ in test_iter:
    y_hat = net(X.to(devices[0]))
    preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort()
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x])
df.to_csv('submission1.csv', index=False)

preds里面,后面这一串是copy到cpu和numpy中,生成干净的list:

 这里根据类别号进行索引,是通过dataset里面的classes[i]实现的。

其中还要注意,这个extend是将两个列表扩充时,将list1.extend(list2)中的list2解包,分别扩充到list1中,而append则是将括号中的list2整体添加到list1中。

而且进行sort()排序操作后,索引的话要索引原表,如果print(sorted_ids.sort())会返回None,而print(sorted_ids)则会返回经过sort操作的,因为该操作是在原位置上操作的,不会新返回值。

2.探索--finetune

在上个案例中,采用的是在原迁移网络的基础上重新添加一些mlp,使最终输出值与num_cls相匹配。那么这里的探索主要围绕:1.修改最后的fc;2.权重初始化;3.封锁部分原参数。

2.1修改fc

可以直接暴力的只修改最后的Linear层,也可以将原fc全部重新写一遍,这里先说暴力只修改最后Linear层的:

直接.fc将最后的这一层修改即可:

finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(finetune_net.fc.weight)

2.2自定义修改与添加xavier初始化参数:

finetune_net.fc = nn.Sequential(
    nn.Linear(512, 124),
    nn.ReLU(inplace=True),
    nn.Linear(124, 2)
)

def init_weights(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
        nn.init.xavier_uniform_(m.weight)
finetune_net.fc.apply(init_weights)

注意,这里xavier只是对修改后的fc层进行,如果是:

finetune_net.apply(init_weights);

这就是对所有层进行初始化了,跟不迁移一样了。

###

注意:如果想修改模型的其他层,或者初始化中间层的参数,先对迁移后的finetune_net.eval(),看对应层的名字即可:

 2.3封锁层的优化参数

如果想封住某些层的参数,可以按照如下操作:

def get_net(devices):
    finetune_net = nn.Sequential()
    finetune_net.features = torchvision.models.resnet34(pretrained=True)
    # 定义⼀个新的输出⽹络,共有120个输出类别
    finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
                                            nn.ReLU(),
                                            nn.Linear(256, 120))
    # 将模型参数分配给⽤于计算的CPU或GPU
    finetune_net = finetune_net.to(devices[0])
    # 冻结参数
    for param in finetune_net.features.parameters():
        param.requires_grad = False
    return finetune_net

 这里是将features层封住,其中,这是在1节dog中的操作,这里把原resnet34中的所有层都赋予到features这个层上了:

 但如果要使用封住参数的话,在后面的优化参数命令也要改一下:

trainer = torch.optim.SGD((param for param in net.parameters()
                                if param.requires_grad), lr=lr,
                                momentum=0.9, weight_decay=wd)

 这样的话requires_grad=False的参数就不用参与训练优化更新了。

  注意操作细节,如果是在原来层上添加一个output_new,则要先定义一个Sequential()。这样在这个定义了Seq的finetune_net后面加.自定义模块,就等同于finetune_net[0]\[1]\[i]...

2.4直观对比

这里对比了一下在apply参数随机初始化xavier命令中,分别设置只对fc层初始化和对所有层初始化(相当于没迁移)的区别:

只对fc:

 所有层:

 可以看到区别还是很明显的,只对fc层模型精确度一开始就比较高,说明前面的参数比随机初始有效得多。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值