{"cells":[{"metadata":{"graffitiCellId":"id_9pjlvx1"},"cell_type":"markdown","source":"#  Kaggle上的狗品种识别（ImageNet Dogs）"},{"metadata":{"graffitiCellId":"id_nnzrhp8"},"cell_type":"markdown","source":"在本节中，我们将解决Kaggle竞赛中的犬种识别挑战，比赛的网址是https://www.kaggle.com/c/dog-breed-identification 在这项比赛中，我们尝试确定120种不同的狗。该比赛中使用的数据集实际上是著名的ImageNet数据集的子集。"},{"metadata":{"graffitiCellId":"id_fypnm12","trusted":true},"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision\nimport torchvision.transforms as transforms\nimport torchvision.models as models\nimport os\nimport shutil\nimport time\nimport pandas as pd\nimport random","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# 设置随机数种子\nrandom.seed(0)\ntorch.manual_seed(0)\ntorch.cuda.manual_seed(0)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 整理数据集"},{"metadata":{},"cell_type":"markdown","source":"我们可以从比赛网址上下载数据集，其目录结构为：\n\n```\n| Dog Breed Identification\n    | train\n    |   | 000bec180eb18c7604dcecc8fe0dba07.jpg\n    |   | 00a338a92e4e7bf543340dc849230e75.jpg\n    |   | ...\n    | test\n    |   | 00a3edd22dc7859c487a64777fc8d093.jpg\n    |   | 00a6892e5c7f92c1f465e213fd904582.jpg\n    |   | ...\n    | labels.csv\n    | sample_submission.csv\n```\n\ntrain和test目录下分别是训练集和测试集的图像，训练集包含10,222张图像，测试集包含10,357张图像，图像格式都是JPEG，每张图像的文件名是一个唯一的id。labels.csv包含训练集图像的标签，文件包含10,222行，每行包含两列，第一列是图像id，第二列是狗的类别。狗的类别一共有120种。\n\n我们希望对数据进行整理，方便后续的读取，我们的主要目标是：\n\n* 从训练集中划分出验证数据集，用于调整超参数。划分之后，数据集应该包含4个部分：划分后的训练集、划分后的验证集、完整训练集、完整测试集\n* 对于4个部分，建立4个文件夹：train, valid, train_valid, test。在上述文件夹中，对每个类别都建立一个文件夹，在其中存放属于该类别的图像。前三个部分的标签已知，所以各有120个子文件夹，而测试集的标签未知，所以仅建立一个名为unknown的子文件夹，存放所有测试数据。\n\n我们希望整理后的数据集目录结构为：\n```\n| train_valid_test\n    | train\n    |   | affenpinscher\n    |   |   | 00ca18751837cd6a22813f8e221f7819.jpg\n    |   |   | ...\n    |   | afghan_hound\n    |   |   | 0a4f1e17d720cdff35814651402b7cf4.jpg\n    |   |   | ...\n    |   | ...\n    | valid\n    |   | affenpinscher\n    |   |   | 56af8255b46eb1fa5722f37729525405.jpg\n    |   |   | ...\n    |   | afghan_hound\n    |   |   | 0df400016a7e7ab4abff824bf2743f02.jpg\n    |   |   | ...\n    |   | ...\n    | train_valid\n    |   | affenpinscher\n    |   |   | 00ca18751837cd6a22813f8e221f7819.jpg\n    |   |   | ...\n    |   | afghan_hound\n    |   |   | 0a4f1e17d720cdff35814651402b7cf4.jpg\n    |   |   | ...\n    |   | ...\n    | test\n    |   | unknown\n    |   |   | 00a3edd22dc7859c487a64777fc8d093.jpg\n    |   |   | ...\n```"},{"metadata":{"trusted":true},"cell_type":"code","source":"data_dir = '../input/dog-breed-identification'  # 数据集目录\nlabel_file, train_dir, test_dir = 'labels.csv', 'train', 'test'  # data_dir中的文件夹、文件\nnew_data_dir = './train_valid_test'  # 整理之后的数据存放的目录\nvalid_ratio = 0.1  # 验证集所占比例","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def mkdir_if_not_exist(path):\n    # 若目录path不存在，则创建目录\n    if not os.path.exists(os.path.join(*path)):\n        os.makedirs(os.path.join(*path))\n        \ndef reorg_dog_data(data_dir, label_file, train_dir, test_dir, new_data_dir, valid_ratio):\n    # 读取训练数据标签\n    labels = pd.read_csv(os.path.join(data_dir, label_file))\n    id2label = {Id: label for Id, label in labels.values}  # (key: value): (id: label)\n\n    # 随机打乱训练数据\n    train_files = os.listdir(os.path.join(data_dir, train_dir))\n    random.shuffle(train_files)    \n\n    # 原训练集\n    valid_ds_size = int(len(train_files) * valid_ratio)  # 验证集大小\n    for i, file in enumerate(train_files):\n        img_id = file.split('.')[0]  # file是形式为id.jpg的字符串\n        img_label = id2label[img_id]\n        if i < valid_ds_size:\n            mkdir_if_not_exist([new_data_dir, 'valid', img_label])\n            shutil.copy(os.path.join(data_dir, train_dir, file),\n                        os.path.join(new_data_dir, 'valid', img_label))\n        else:\n            mkdir_if_not_exist([new_data_dir, 'train', img_label])\n            shutil.copy(os.path.join(data_dir, train_dir, file),\n                        os.path.join(new_data_dir, 'train', img_label))\n        mkdir_if_not_exist([new_data_dir, 'train_valid', img_label])\n        shutil.copy(os.path.join(data_dir, train_dir, file),\n                    os.path.join(new_data_dir, 'train_valid', img_label))\n\n    # 测试集\n    mkdir_if_not_exist([new_data_dir, 'test', 'unknown'])\n    for test_file in os.listdir(os.path.join(data_dir, test_dir)):\n        shutil.copy(os.path.join(data_dir, test_dir, test_file),\n                    os.path.join(new_data_dir, 'test', 'unknown'))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"reorg_dog_data(data_dir, label_file, train_dir, test_dir, new_data_dir, valid_ratio)","execution_count":null,"outputs":[]},{"metadata":{"graffitiCellId":"id_nib4akb"},"cell_type":"markdown","source":"## 图像增强"},{"metadata":{"graffitiCellId":"id_pdfj7om","trusted":true},"cell_type":"code","source":"transform_train = transforms.Compose([\n    # 随机对图像裁剪出面积为原图像面积0.08~1倍、且高和宽之比在3/4~4/3的图像，再放缩为高和宽均为224像素的新图像\n    transforms.RandomResizedCrop(224, scale=(0.08, 1.0),  \n                                 ratio=(3.0/4.0, 4.0/3.0)),\n    # 以0.5的概率随机水平翻转\n    transforms.RandomHorizontalFlip(),\n    # 随机更改亮度、对比度和饱和度\n    transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4),\n    transforms.ToTensor(),\n    # 对各个通道做标准化，(0.485, 0.456, 0.406)和(0.229, 0.224, 0.225)是在ImageNet上计算得的各通道均值与方差\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # ImageNet上的均值和方差\n])\n\n# 在测试集上的图像增强只做确定性的操作\ntransform_test = transforms.Compose([\n    transforms.Resize(256),\n    # 将图像中央的高和宽均为224的正方形区域裁剪出来\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 读取数据"},{"metadata":{"graffitiCellId":"id_xtlsjze","trusted":true},"cell_type":"code","source":"# new_data_dir目录下有train, valid, train_valid, test四个目录\n# 这四个目录中，每个子目录表示一种类别，目录中是属于该类别的所有图像\ntrain_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'train'),\n                                            transform=transform_train)\nvalid_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'valid'),\n                                            transform=transform_test)\ntrain_valid_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'train_valid'),\n                                            transform=transform_train)\ntest_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'test'),\n                                            transform=transform_test)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 128\ntrain_iter = torch.utils.data.DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2)\nvalid_iter = torch.utils.data.DataLoader(valid_ds, batch_size=batch_size, shuffle=True, num_workers=2)\ntrain_valid_iter = torch.utils.data.DataLoader(train_valid_ds, batch_size=batch_size, shuffle=True, num_workers=2)\ntest_iter = torch.utils.data.DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=2)  # shuffle=False","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 定义模型\n\n这个比赛的数据属于ImageNet数据集的子集，我们使用微调的方法，选用在ImageNet完整数据集上预训练的模型来抽取图像特征，以作为自定义小规模输出网络的输入。\n\n此处我们使用与训练的ResNet-34模型，直接复用预训练模型在输出层的输入，即抽取的特征，然后我们重新定义输出层，本次我们仅对重定义的输出层的参数进行训练，而对于用于抽取特征的部分，我们保留预训练模型的参数。"},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_net(device):\n    finetune_net = models.resnet34(pretrained=True)  # 预训练的resnet34网络\n    for param in finetune_net.parameters():  # 冻结参数\n        param.requires_grad = False\n    # 原finetune_net.fc是一个输入单元数为512，输出单元数为1000的全连接层\n    # 替换掉原finetune_net.fc，新finetuen_net.fc中的模型参数会记录梯度\n    finetune_net.fc = nn.Sequential(\n        nn.Linear(in_features=512, out_features=256),\n        nn.ReLU(),\n        nn.Linear(in_features=256, out_features=120)  # 120是输出类别数\n    )\n    return finetune_net","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 定义训练函数"},{"metadata":{"trusted":true},"cell_type":"code","source":"def evaluate_loss_acc(data_iter, net, device):\n    # 计算data_iter上的平均损失与准确率\n    loss = nn.CrossEntropyLoss()\n    is_training = net.training  # Bool net是否处于train模式\n    net.eval()\n    l_sum, acc_sum, n = 0, 0, 0\n    with torch.no_grad():\n        for X, y in data_iter:\n            X, y = X.to(device), y.to(device)\n            y_hat = net(X)\n            l = loss(y_hat, y)\n            l_sum += l.item() * y.shape[0]\n            acc_sum += (y_hat.argmax(dim=1) == y).sum().item()\n            n += y.shape[0]\n    net.train(is_training)  # 恢复net的train/eval状态\n    return l_sum / n, acc_sum / n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def train(net, train_iter, valid_iter, num_epochs, lr, wd, device, lr_period,\n          lr_decay):\n    loss = nn.CrossEntropyLoss()\n    optimizer = optim.SGD(net.fc.parameters(), lr=lr, momentum=0.9, weight_decay=wd)\n    net = net.to(device)\n    for epoch in range(num_epochs):\n        train_l_sum, n, start = 0.0, 0, time.time()\n        if epoch > 0 and epoch % lr_period == 0:  # 每lr_period个epoch，学习率衰减一次\n            lr = lr * lr_decay\n            for param_group in optimizer.param_groups:\n                param_group['lr'] = lr\n        for X, y in train_iter:\n            X, y = X.to(device), y.to(device)\n            optimizer.zero_grad()\n            y_hat = net(X)\n            l = loss(y_hat, y)\n            l.backward()\n            optimizer.step()\n            train_l_sum += l.item() * y.shape[0]\n            n += y.shape[0]\n        time_s = \"time %.2f sec\" % (time.time() - start)\n        if valid_iter is not None:\n            valid_loss, valid_acc = evaluate_loss_acc(valid_iter, net, device)\n            epoch_s = (\"epoch %d, train loss %f, valid loss %f, valid acc %f, \"\n                       % (epoch + 1, train_l_sum / n, valid_loss, valid_acc))\n        else:\n            epoch_s = (\"epoch %d, train loss %f, \"\n                       % (epoch + 1, train_l_sum / n))\n        print(epoch_s + time_s + ', lr ' + str(lr))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 调参"},{"metadata":{"trusted":true},"cell_type":"code","source":"num_epochs, lr_period, lr_decay = 20, 10, 0.1\nlr, wd = 0.03, 1e-4\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# net = get_net(device)\n# train(net, train_iter, valid_iter, num_epochs, lr, wd, device, lr_period, lr_decay)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 在完整数据集上训练模型"},{"metadata":{"trusted":true},"cell_type":"code","source":"net = get_net(device)\ntrain(net, train_valid_iter, None, num_epochs, lr, wd, device, lr_period, lr_decay)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 对测试集分类并提交结果\n\n用训练好的模型对测试数据进行预测。比赛要求对测试集中的每张图片，都要预测其属于各个类别的概率。"},{"metadata":{"trusted":true},"cell_type":"code","source":"preds = []\nfor X, _ in test_iter:\n    X = X.to(device)\n    output = net(X)\n    output = torch.softmax(output, dim=1)\n    preds += output.tolist()\nids = sorted(os.listdir(os.path.join(new_data_dir, 'test/unknown')))\nwith open('submission.csv', 'w') as f:\n    f.write('id,' + ','.join(train_valid_ds.classes) + '\\n')\n    for i, output in zip(ids, preds):\n        f.write(i.split('.')[0] + ',' + ','.join(\n            [str(num) for num in output]) + '\\n')","execution_count":null,"outputs":[]}],"metadata":{"graffiti":{"firstAuthorId":"dev","id":"id_5vr6mb2","language":"EN"},"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.6.10"}},"nbformat":4,"nbformat_minor":1}