{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"pip install -U d2l","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.066966Z","iopub.execute_input":"2022-06-22T19:18:38.067334Z","iopub.status.idle":"2022-06-22T19:18:38.072216Z","shell.execute_reply.started":"2022-06-22T19:18:38.067302Z","shell.execute_reply":"2022-06-22T19:18:38.07088Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os\nimport torch\nimport torchvision\nfrom torch import nn\nfrom d2l import torch as d2l\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.transforms as transforms\nimport torchvision.models as models\nimport os\nimport shutil\nimport time\nimport pandas as pd\nimport random\nimport csv","metadata":{"execution":{"iopub.status.busy":"2022-06-22T20:34:02.669305Z","iopub.execute_input":"2022-06-22T20:34:02.669695Z","iopub.status.idle":"2022-06-22T20:34:02.678009Z","shell.execute_reply.started":"2022-06-22T20:34:02.669663Z","shell.execute_reply":"2022-06-22T20:34:02.676927Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"random.seed(0)\ntorch.manual_seed(0)\ntorch.cuda.manual_seed(0)","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.103256Z","iopub.execute_input":"2022-06-22T19:18:38.105702Z","iopub.status.idle":"2022-06-22T19:18:38.110752Z","shell.execute_reply.started":"2022-06-22T19:18:38.105665Z","shell.execute_reply":"2022-06-22T19:18:38.109841Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"train和test⽬录下分别是训练集和测试集的图像，训练集包含10,222张图像，测试集包含10,357张图像，图像格式都是JPEG，每张图像\n的⽂件名是⼀个唯⼀的id。labels.csv包含训练集图像的标签，⽂件包含10,222⾏，每⾏包含两列，第⼀列是图像id，第⼆列是狗的类别。\n狗的类别⼀共有120种。\n我们希望对数据进⾏整理，⽅便后续的读取，我们的主要⽬标是：\n从训练集中划分出验证数据集，⽤于调整超参数。划分之后，数据集应该包含4个部分：划分后的训练集、划分后的验证集、完整训练集、\n完整测试集\n对于4个部分，建⽴4个⽂件夹：train, valid, train_valid, test。在上述⽂件夹中，对每个类别都建⽴⼀个⽂件夹，在其中存放属于该类别\n的图像。前三个部分的标签已知，所以各有120个⼦⽂件夹，⽽测试集的标签未知，所以仅建⽴⼀个名为unknown的⼦⽂件夹，存放所有\n测试数据。\n","metadata":{}},{"cell_type":"code","source":"label_file, train_dir, test_dir = 'labels.csv', 'train', 'test' # data_dir中的⽂件夹、⽂件\nnew_data_dir = './train_valid_test' # 整理之后的数据存放的⽬录\nvalid_ratio = 0.1 # 验证集所占⽐例\ndata_dir = os.path.join('../input/dog-breed-identification')","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.117252Z","iopub.execute_input":"2022-06-22T19:18:38.119706Z","iopub.status.idle":"2022-06-22T19:18:38.125638Z","shell.execute_reply.started":"2022-06-22T19:18:38.119669Z","shell.execute_reply":"2022-06-22T19:18:38.124626Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 整理数据","metadata":{}},{"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))\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    train_files = os.listdir(os.path.join(data_dir, train_dir))\n    random.shuffle(train_files)\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    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'))\n","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.132407Z","iopub.execute_input":"2022-06-22T19:18:38.133361Z","iopub.status.idle":"2022-06-22T19:18:38.147744Z","shell.execute_reply.started":"2022-06-22T19:18:38.133323Z","shell.execute_reply":"2022-06-22T19:18:38.146538Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"reorg_dog_data(data_dir, label_file, train_dir, test_dir, new_data_dir, valid_ratio)","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.149626Z","iopub.execute_input":"2022-06-22T19:18:38.150325Z","iopub.status.idle":"2022-06-22T19:18:38.163151Z","shell.execute_reply.started":"2022-06-22T19:18:38.15029Z","shell.execute_reply":"2022-06-22T19:18:38.161877Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 图像增广","metadata":{}},{"cell_type":"code","source":"transform_train = torchvision.transforms.Compose([\n    # 随机裁剪图像，所得图像为原始面积的0.08到1之间，高宽比在3/4和4/3之间。\n    # 然后，缩放图像以创建224x224的新图像\n    torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),\n                                             ratio=(3.0/4.0, 4.0/3.0)),\n    torchvision.transforms.RandomHorizontalFlip(),\n    # 随机更改亮度，对比度和饱和度\n    torchvision.transforms.ColorJitter(brightness=0.4,\n                                       contrast=0.4,\n                                       saturation=0.4),\n    # 添加随机噪声\n    torchvision.transforms.ToTensor(),\n    # 标准化图像的每个通道\n    torchvision.transforms.Normalize([0.485, 0.456, 0.406],\n                                     [0.229, 0.224, 0.225])])","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.169233Z","iopub.execute_input":"2022-06-22T19:18:38.169836Z","iopub.status.idle":"2022-06-22T19:18:38.179256Z","shell.execute_reply.started":"2022-06-22T19:18:38.169803Z","shell.execute_reply":"2022-06-22T19:18:38.178059Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"transform_test = torchvision.transforms.Compose([\n    torchvision.transforms.Resize(256),\n    # 从图像中心裁切224x224大小的图片\n    torchvision.transforms.CenterCrop(224),\n    torchvision.transforms.ToTensor(),\n    torchvision.transforms.Normalize([0.485, 0.456, 0.406],\n                                     [0.229, 0.224, 0.225])])","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.180936Z","iopub.execute_input":"2022-06-22T19:18:38.181934Z","iopub.status.idle":"2022-06-22T19:18:38.195501Z","shell.execute_reply.started":"2022-06-22T19:18:38.181897Z","shell.execute_reply":"2022-06-22T19:18:38.194225Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 读取数据集","metadata":{}},{"cell_type":"code","source":"train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(\n    os.path.join('./', 'train_valid_test', folder),\n    transform=transform_train) for folder in ['train', 'train_valid']]\n\nvalid_ds, test_ds = [torchvision.datasets.ImageFolder(\n    os.path.join('./', 'train_valid_test', folder),\n    transform=transform_test) for folder in ['valid', 'test']]","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.197591Z","iopub.execute_input":"2022-06-22T19:18:38.197937Z","iopub.status.idle":"2022-06-22T19:18:38.359621Z","shell.execute_reply.started":"2022-06-22T19:18:38.197902Z","shell.execute_reply":"2022-06-22T19:18:38.358627Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_iter, train_valid_iter = [torch.utils.data.DataLoader(\n    dataset, batch_size, shuffle=True, drop_last=True)\n    for dataset in (train_ds, train_valid_ds)]\n\nvalid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,\n                                         drop_last=True)\n\ntest_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,\n                                        drop_last=False)","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.361606Z","iopub.execute_input":"2022-06-22T19:18:38.361961Z","iopub.status.idle":"2022-06-22T19:18:38.370605Z","shell.execute_reply.started":"2022-06-22T19:18:38.361926Z","shell.execute_reply":"2022-06-22T19:18:38.3696Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义模型\n这个⽐赛的数据属于ImageNet数据集的⼦集，我们使⽤微调的⽅法，选⽤在ImageNet完整数据集上预训练的模型来抽取图像特征，以作为\n⾃定义⼩规模输出⽹络的输⼊。\n此处我们使⽤与训练的ResNet-34模型，直接复⽤预训练模型在输出层的输⼊，即抽取的特征，然后我们重新定义输出层，本次我们仅对\n重定义的输出层的参数进⾏训练，⽽对于⽤于抽取特征的部分，我们保留预训练模型的参数。","metadata":{}},{"cell_type":"code","source":"def get_net(devices):\n    finetune_net = nn.Sequential()\n    finetune_net.features = torchvision.models.resnet34(pretrained=True)\n    # 定义一个新的输出网络，共有120个输出类别\n    finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),\n                                            nn.ReLU(),\n                                            nn.Linear(256, 120))\n    # 将模型参数分配给用于计算的CPU或GPU\n    finetune_net = finetune_net.to(devices[0])\n    # 冻结参数\n    for param in finetune_net.features.parameters():\n        param.requires_grad = False\n    return finetune_net","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.372288Z","iopub.execute_input":"2022-06-22T19:18:38.372682Z","iopub.status.idle":"2022-06-22T19:18:38.38457Z","shell.execute_reply.started":"2022-06-22T19:18:38.372648Z","shell.execute_reply":"2022-06-22T19:18:38.382593Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"loss = nn.CrossEntropyLoss(reduction='none')\n\ndef evaluate_loss(data_iter, net, devices):\n    l_sum, n = 0.0, 0\n    for features, labels in data_iter:\n        features, labels = features.to(devices[0]), labels.to(devices[0])\n        outputs = net(features)\n        l = loss(outputs, labels)\n        l_sum += l.sum()\n        n += labels.numel()\n    return (l_sum / n).to('cpu')","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.388616Z","iopub.execute_input":"2022-06-22T19:18:38.388955Z","iopub.status.idle":"2022-06-22T19:18:38.397856Z","shell.execute_reply.started":"2022-06-22T19:18:38.38892Z","shell.execute_reply":"2022-06-22T19:18:38.396922Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 定义训练函数","metadata":{}},{"cell_type":"code","source":"def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,\n          lr_decay):\n    # 只训练小型自定义输出网络\n    net = nn.DataParallel(net, device_ids=devices).to(devices[0])\n    trainer = torch.optim.SGD((param for param in net.parameters()\n                               if param.requires_grad), lr=lr,\n                              momentum=0.9, weight_decay=wd)\n    scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)\n    num_batches, timer = len(train_iter), d2l.Timer()\n    legend = ['train loss']\n    if valid_iter is not None:\n        legend.append('valid loss')\n    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],\n                            legend=legend)\n    for epoch in range(num_epochs):\n        metric = d2l.Accumulator(2)\n        for i, (features, labels) in enumerate(train_iter):\n            timer.start()\n            features, labels = features.to(devices[0]), labels.to(devices[0])\n            trainer.zero_grad()\n            output = net(features)\n            l = loss(output, labels).sum()\n            l.backward()\n            trainer.step()\n            metric.add(l, labels.shape[0])\n            timer.stop()\n            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:\n                animator.add(epoch + (i + 1) / num_batches,\n                             (metric[0] / metric[1], None))\n        measures = f'train loss {metric[0] / metric[1]:.3f}'\n        if valid_iter is not None:\n            valid_loss = evaluate_loss(valid_iter, net, devices)\n            animator.add(epoch + 1, (None, valid_loss.detach().cpu()))\n        scheduler.step()\n    if valid_iter is not None:\n        measures += f', valid loss {valid_loss:.3f}'\n    print(measures + f'\\n{metric[1] * num_epochs / timer.sum():.1f}'\n          f' examples/sec on {str(devices)}')","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.399106Z","iopub.execute_input":"2022-06-22T19:18:38.399828Z","iopub.status.idle":"2022-06-22T19:18:38.419162Z","shell.execute_reply.started":"2022-06-22T19:18:38.399792Z","shell.execute_reply":"2022-06-22T19:18:38.418072Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 调参数并训练","metadata":{}},{"cell_type":"code","source":"devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 1e-4, 1e-4\nlr_period, lr_decay, net = 2, 0.9, get_net(devices)\ntrain(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,\n      lr_decay)","metadata":{"execution":{"iopub.status.busy":"2022-06-22T19:18:38.420414Z","iopub.execute_input":"2022-06-22T19:18:38.42124Z","iopub.status.idle":"2022-06-22T19:31:58.454271Z","shell.execute_reply.started":"2022-06-22T19:18:38.421203Z","shell.execute_reply":"2022-06-22T19:31:58.453254Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 预测\n⽤训练好的模型对测试数据进⾏预测。⽐赛要求对测试集中的每张图⽚，都要预测其属于各个类别的概率。","metadata":{}},{"cell_type":"code","source":"net = get_net(devices)\ntrain(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,\n      lr_decay)\n\npreds = []\nfor data, label in test_iter:\n    output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0)\n    preds.extend(output.cpu().detach().numpy())\nids = sorted(os.listdir(\n    os.path.join('./', 'train_valid_test', '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')","metadata":{"execution":{"iopub.status.busy":"2022-06-22T20:34:25.261169Z","iopub.execute_input":"2022-06-22T20:34:25.261534Z","iopub.status.idle":"2022-06-22T20:49:04.719759Z","shell.execute_reply.started":"2022-06-22T20:34:25.261496Z","shell.execute_reply":"2022-06-22T20:49:04.718766Z"},"trusted":true},"execution_count":null,"outputs":[]}]}