kaggle比赛:Classify Leaves图片树叶分类(使用resnet50预训练模型进行)

1、比赛介绍:

Classify Leaves | Kaggle

2、数据划分:

该任务是:给出树叶的图片,将给出的树叶分成176类。

#导入一些用到的包
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

接下来我们开始读取数据:读取数据集所在地址、并整理数据集。这里要注意,训练数据和测试数据都放在image文件夹下,我们需要通过train.csv和test.csv中的图片地址来将他们划分开。

整理数据集思路很简单:将训练数据集和测试数据集划分开,然后将训练数据集中每一个类建立一个文件夹,并把标签对应的图片复制一份到里面。(这里我们用到了验证数据集,和训练数据集操作是一样的)

 下面的几个函数就是对上面的描述进行操作,都在注释中。

def read_csv_labels(fname):
    """读取 `fname` 来给标签字典返回一个文件名。"""
    with open(fname, 'r') as f:
        lines = f.readlines()[1:]
    tokens = [l.rstrip().split(',') for l in lines]
    # rstrip()的作用是把后面的空格包括tab都去除
    return dict(((name, label) for name, label in tokens))
    # 返回字典,key是name,即图片名,value是lable,即标签

labels = read_csv_labels(os.path.join(data_dir, 'train.csv')) # 存放训练集标签的文件


def copyfile(filename, target_dir):
    """将文件复制到目标目录。"""
    os.makedirs(target_dir, exist_ok=True)
    shutil.copy(filename, target_dir)


def reorg_train_valid(data_dir, labels, valid_ratio):
    # 下面的collections.Counter就是统计label这个字典中有几个类别(返回字典);.most_common()则转换成元组;[-1][1]则返回最后一个元组的第二个值(因为这个类别数量最小)
    n = collections.Counter(labels.values()).most_common()[-1][1] # n就是数量最少类别对应的数量
    """也可以用下面这几段代码实现获取最少的类别数,可能更好理解
    leaves_labels = sorted(list(set(train_csv['label'])))
    n_classes = len(leaves_labels)
    class_to_num = dict(zip(leaves_labels, range(n_classes))) #leaves_labels和range(n_classes)换一下就是键和值的位置互换 就可以成num_to_class
    num_to_class = {v : k for k, v in class_to_num.items()}
    """
    n_valid_per_label = max(1, math.floor(n * valid_ratio)) # 根据最小类别数量,得出验证集的数量
    label_count = {}
    for train_file in labels: # 返回训练集中的图片名字列表(我们看到,训练标签转换成的字典,key就是训练集的图片名字)
        label = labels[train_file] # 每张图片 对应的标签
        fname = os.path.join(data_dir, 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: # 制作验证集。注:标签名作为key,value为每个标签的图片数量
            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 # 返回验证集的数量


# 在预测期间整理测试集,以方便读取
def reorg_test(data_dir):
    test = pd.read_csv(os.path.join(data_dir, 'test.csv'))
    for test_file in test['image']: # 获取测试集图片的名字,复制到指定文件夹下
        copyfile(
            os.path.join(data_dir, test_file),
            os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))


# 调用前面定义的函数,进行整理数据集
def reorg_leave_data(data_dir, valid_ratio):
    labels = read_csv_labels(os.path.join(data_dir, 'train.csv')) # 是个字典
    reorg_train_valid(data_dir, labels, valid_ratio) # 生成训练集和验证集
    reorg_test(data_dir) # 生成测试集

batch_size = 128
valid_ratio = 0.1 # 验证集的比例
if not os.path.exists(data_dir + "\\" + "train_valid_test"): # 判断是否已经制作好了数据集
    print("start create dataset")
    reorg_leave_data(data_dir, valid_ratio)
else:
    print("Already exists!")
print('dataset created finish')

3、数据增广:

接下来对图像进行变换,也就是数据增广: 这里需要说下,图像增广,这里并没有把每张图片变成多张,保存下来。而是每次读入的时候,随机的变换成一张,然后送入模型。从整个模型运行的角度看,实际上就是将数据集变大了,因为每次送入的图片大概率是不一样的(随机变换的)

transform_train = torchvision.transforms.Compose([
    torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), # 从图片的中心点剪裁出24*24的图片
                                             ratio=(3.0 / 4.0, 4.0 / 3.0)),
    torchvision.transforms.RandomHorizontalFlip(), # 左右翻转
    torchvision.transforms.ColorJitter(brightness=0.4, contrast=0.4,
                                       saturation=0.4),
    # 加入随机噪音
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize([0.485, 0.456, 0.406], # 对图片的每个通道做均值和方差
                                     [0.229, 0.224, 0.225])])


transform_test = torchvision.transforms.Compose([
    torchvision.transforms.Resize(256),
    torchvision.transforms.CenterCrop(224),
    # 加入随机噪音
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize([0.485, 0.456, 0.406], # 依然 对图片的每个通道做均值和方差
                                     [0.229, 0.224, 0.225])])

为什么一些深度学习的图像预处理使用mean=[0.485, 0.456, 0.406] and std=[0.229, 0.224, 0.225]来正则化?

这是因为使用了使用ImageNet的均值和标准差。使用Imagenet的均值和标准差是一种常见的做法。它们是根据数百万张图像计算得出的。如果要在自己的数据集上从头开始训练,则可以计算新的均值和标准差。否则,建议使用Imagenet预设模型及其平均值和标准差

4、读取数据: 

我们前面把数据集划分成训练集、验证集和测试集,并每一个类建立了一个文件夹。接下来我们使用 torchvision 的ImageFolder方法,将训练集、验证集和测试集读取进来。

# ImageFolder按照文件夹顺序,进行打标签

# 训练集和交叉验证集
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']]

# 把前面的数据放入torch的DataLoader,则每次迭代时,读取一个batch
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)

test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
                                        drop_last=False)

5、模型构造:

为了练习微调(迁移学习的一种)的做法,选用了resnet50预训练模型,作为这次比赛的模型。这么做也有点道理,因为resnet系列的预训练模型都是在ImageNet数据集上训练的,而ImageNet数据集,我们都知道100万的图片,分类为1000类,有树叶的分类,因此,可以使用迁移学习的方法做。可能不用微调,直接把resnet50重新训练一遍,应该效果会更好吧。但重在实践吧(doge

# 微调
def get_net(devices):
    finetune_net = nn.Sequential()
    finetune_net.features = torchvision.models.resnet50(pretrained=True) # 使用了resnet50的预训练模型
    finetune_net.output_new = nn.Sequential(nn.Linear(1000, 512), nn.ReLU(), # 在后面输出层中,又增加了几层,目的是维度降到类别数
                                            nn.Linear(512, 256), nn.ReLU(),
                                            nn.Linear(256, 176)) # 树叶分类有176类
    finetune_net = finetune_net.to(devices[0]) # 将模型送入gpu
    for param in finetune_net.features.parameters(): # 固定住预训练模型中的参数,只调节我们新加的几个层的参数
        param.requires_grad = False
    return finetune_net

6、计算损失:

# 下面这个是交叉熵损失,我们前面在模型的最后面没有用softmax(),是因为下面这个包含了
loss = nn.CrossEntropyLoss(reduction='none') # reduction='none'表示返回n个样本的loss

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() # numel()函数:返回数组中元素的个数
    return l_sum / n

7、模型训练:

def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay):
	"""
	wd:权衰量,用于防止过拟合
	lr_period:每隔几个epoch降低学习率
    lr_decay:降低学习率的比例
    """
    net = nn.DataParallel(net, device_ids=devices).to(devices[0]) # 使用多gpu
    trainer = torch.optim.SGD( # 随机梯度下降
        (param for param in net.parameters() if param.requires_grad), 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']
    if valid_iter is not None:
        legend.append('valid loss')
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=legend) # 画图
    for epoch in range(num_epochs):
        metric = d2l.Accumulator(2)
        for i, (features, labels) in enumerate(train_iter):
            timer.start()
            features, labels = features.to(devices[0]), labels.to(devices[0]) # 特征和标签都放到gpu
            trainer.zero_grad() # 梯度清零
            output = net(features) # 得出预测结果
            l = loss(output, labels).sum()
            l.backward() # 反向传播,求梯度
            trainer.step() # 更新参数
            metric.add(l, 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[1], None))
        measures = f'train loss {metric[0] / metric[1]:.3f}'
        if valid_iter is not None: # 计算验证集的loss
            valid_loss = evaluate_loss(valid_iter, net, devices)
            animator.add(epoch + 1, (None, valid_loss.cpu().detach()))
        scheduler.step() # 一个epoch完了,衰减学习率
    if valid_iter is not None:
        measures += f', valid loss {valid_loss:.3f}'
    print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}'
          f' examples/sec on {str(devices)}')

# 调用上面的函数:训练和验证模型
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 1e-4, 1e-4
lr_period, lr_decay, net = 2, 0.9, get_net(devices)
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay)

8、模型推理:

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

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

test = pd.read_csv(os.path.join(data_dir, 'test.csv')) # 存放 测试集图片地址 的文件
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 = test['image'] # 对应的id
df = pd.DataFrame({'image': sorted_ids, 'label': preds}) # 转换成pandas的DF格式

# .apply()函数:遍历DataFrame的元素(一行数据或者一列数据),默认列遍历
# ImageFolder返回对象的.classes属性:用一个 list 保存类别名称
# 这个的作用是:模型预测出来是概率最大的那个数的下标,在保存文件时,需要把数字类别转换为字符串类别,
# train_valid_ds.classes就是获取字符串类别名(返回的是一个列表),然后使用apply一行一行读取出来,把数字类别转换为字符串类别
df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x]) 
df.to_csv(r'..\data\classify-leaves\submission.csv', index=False)

---------------------------------------------------------------------------------------------------------------------------------

 初学感悟

之前总是很畏惧csv这种数据集的label,总是觉得很难处理,但通过这次的比赛认识到,这样的数据集只是多了一步数据处理的步骤,而且这些步骤只要学会一个后面的都差不多。还有就是比赛很多都是调的模型,就像这个用resnet50一样(finetune_net.features = torchvision.models.resnet50(pretrained=True)),别人搭建好的模型,需要写一个训练的脚本,用合适的方法处理数据能让结果更好。这是一些初学的感悟的吧,但不知实际情况是不是这样的。希望如此

你可能感兴趣的:(深度学习白皮书,分类,数据挖掘,人工智能)