{"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":"# Seems to output 79433 images\n!mkdir -p /kaggle/temp/\n!cd /kaggle/temp/ && unzip -qo /kaggle/input/painter-by-numbers/replacements_for_corrupted_files.zip\n!cd /kaggle/temp/ && unzip -qn /kaggle/input/painter-by-numbers/train.zip\n\n# start time: 10:48,end: between 10:56, 11:03\n# start time: 13:37, end before: 13:46","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# For some reason some files are too large and it seems non-trivial to skip them...\n# Remove extremely large entries\n!find /kaggle/temp -type f -size +64M | sed 's/.*\\///' | awk '{print \"sed /,\"$1\"/d -i /kaggle/working/all_data_info.csv\" }'> /kaggle/temp/replace.sh\n# Move the .csv to somewhere writable\n!cp /kaggle/input/painter-by-numbers/all_data_info.csv /kaggle/working/all_data_info.csv\n!bash /kaggle/temp/replace.sh\n\n# Overwrite images that cause decompression bomb error \n!find /kaggle/temp -type f -size +64M | awk '{print \"cp -f \"$1\"s\",$1 }' | sed 's/...jpgs/.jpg/' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n!find /kaggle/temp -type f -size +64M | awk '{print \"cp -f \"$1\"s\",$1 }' | sed 's/..jpgs/.jpg/' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n\n# Kaggle times out after 9 hours, so move the images to persistent storage\n# and shrink them to fit, selecting 300 for min pixels because that's the size they get resized to initially\n!mkdir -p /kaggle/working/train\n\n!find /kaggle/temp -type f -size +1M | grep jpg$ | awk '{print \"mogrify -resize 300x300^ -path /kaggle/working/train \"$1 }' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n!ls /kaggle/working/train | awk '{print \"rm -f /kaggle/temp/train/\"$1}' > /kaggle/temp/remove-resized.sh\n!bash /kaggle/temp/remove-resized.sh","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!find /kaggle/temp -type f -size +512k | grep jpg$ | awk '{print \"mogrify -resize 300x300^ -path /kaggle/working/train \"$1 }' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n!ls /kaggle/working/train | awk '{print \"rm -f /kaggle/temp/train/\"$1}' > /kaggle/temp/remove-resized.sh\n!bash /kaggle/temp/remove-resized.sh\n\n!find /kaggle/temp -type f -size +128k | grep jpg$ | awk '{print \"mogrify -resize 300x300^ -path /kaggle/working/train \"$1 }' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n!ls /kaggle/working/train | awk '{print \"rm -f /kaggle/temp/train/\"$1}' > /kaggle/temp/remove-resized.sh\n!bash /kaggle/temp/remove-resized.sh\n\n!find /kaggle/temp -type f | grep jpg$ | awk '{print \"mv \"$1\" /kaggle/working/train\"}' > /kaggle/temp/replace.sh\n!bash /kaggle/temp/replace.sh\n\n# !find /kaggle/temp -type f | grep jpg$ | awk '{print \"mogrify -resize 300x300^ -path /kaggle/working/train \"$1 }' > /kaggle/temp/replace.sh\n# !bash -x /kaggle/temp/replace.sh\n!find /kaggle/working/train -type f | grep jpg | awk '{print \"identify -verbose -regard-warnings \"$1\">/dev/null\"}' > /kaggle/temp/validate.sh\n!bash /kaggle/temp/validate.sh\n\n!zip -r output.zip /kaggle/working/*\n# !ls /kaggle/working/train/ | wc -l","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from __future__ import print_function, division\n# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#    for filename in filenames:\n#        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n\nimport os\nimport torch\nimport pandas as pd\n# from skimage import io, transform\nfrom skimage import transform\nfrom PIL import Image\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, utils\n\n# Ignore warnings\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nplt.ion()   # interactive mode","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Dataset comes with a csv file with annotations which looks like this:","metadata":{}},{"cell_type":"code","source":"artist_frame = pd.read_csv('/kaggle/input/painter-by-numbers/all_data_info.csv')\n# Filter out just the training data\nartist_train = artist_frame.loc[artist_frame['in_train']].reset_index(drop=True)\n# Remove the train and test, vs train only field, and the is training field.\nartist_train = artist_train.drop(['artist_group', 'in_train'], axis=1)\n# Keep only one entry per file\nartist_train = artist_train.drop_duplicates(subset=['new_filename']).reset_index(drop=True)\ndel artist_frame\nartist_train.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"index_names = artist_train[artist_train ['pixelsx'] >= 18000].index\nartist_train.drop(index_names, inplace = True)\nartist_train = artist_train.reset_index(drop=True)\n\nimg_name = artist_train['new_filename'][0]\ngenre = artist_train['genre'][0]\nstyle = artist_train['style'][0]\n\nprint('Image name: {}'.format(img_name))\nprint('Genre: {}'.format(genre))\nprint('Style: {}'.format(style))\nartist_train.describe()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Pick a threshold that makes other no more common than the least common\ngenre_threshold = 1600\n\nvalue_counts = artist_train['genre'].value_counts() # Specific column \nto_remove = value_counts[value_counts <= genre_threshold].index\nartist_train['genre'].replace(to_remove, 'other', inplace=True)\nartist_train = artist_train.reset_index(drop='true')\nartist_train['genre'].value_counts().plot(kind='bar', title='genre')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"genre_dict = artist_train['genre'].drop_duplicates().reset_index(drop='true').to_dict()\n\ndef get_genre_key(val):\n    for key, value in genre_dict.items():\n         if val == value:\n            return key\n    return 1\nprint(genre_dict)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Pick a threshold that makes other no more common than the most common\nstyle_threshold = 300\n\nvalue_counts = artist_train['style'].value_counts() # Specific column \nto_remove = value_counts[value_counts <= style_threshold].index\nartist_train['style'].replace(to_remove, 'other', inplace=True)\ndel to_remove\ndel value_counts\nartist_train['style'].value_counts().plot(kind='bar', title='style')\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"del artist_train","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"https://pytorch.org/tutorials/beginner/data_loading_tutorial.html","metadata":{}},{"cell_type":"markdown","source":"## Dataset class\n\ntorch.utils.data.Dataset is an abstract class representing a dataset. Your custom dataset should inherit Dataset and override the following methods:\n\n    __len__ so that len(dataset) returns the size of the dataset.\n    __getitem__ to support the indexing such that dataset[i] can be used to get iiith sample.\n\nLet’s create a dataset class for our face landmarks dataset. We will read the csv in __init__ but leave the reading of images to __getitem__. This is memory efficient because all the images are not stored in the memory at once but read as required.\n\nSample of our dataset will be a dict {'image': image, 'artist': artist, 'genre': genre, 'style': style}. Our dataset will take an optional argument transform so that any required processing can be applied on the sample. We will see the usefulness of transform in the next section.","metadata":{}},{"cell_type":"code","source":"!ls /kaggle/working ","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class WikiArtDataset(Dataset):\n    \"\"\"Face Landmarks dataset.\"\"\"\n\n    def __init__(self, csv_file, root_dir, transform=None):\n        \"\"\"\n        Args:\n            csv_file (string): Path to the csv file with annotations.\n            root_dir (string): Directory with all the images.\n            transform (callable, optional): Optional transform to be applied\n                on a sample.\n        \"\"\"\n        self.artist_frame = pd.read_csv('/kaggle/input/painter-by-numbers/all_data_info.csv')\n        self.artist_frame = self.artist_frame.loc[self.artist_frame['in_train']].reset_index(drop=True)\n        # Remove the train and test, vs train only field, and the is training field.\n        self.artist_frame = self.artist_frame.drop(['artist_group', 'in_train'], axis=1)\n        \n        # Pick a threshold that makes 10 categories of genre\n        genre_threshold = 1600\n\n        value_counts = self.artist_frame['genre'].value_counts() # Specific column \n        to_remove = value_counts[value_counts <= genre_threshold].index\n        self.artist_frame['genre'].replace(to_remove, 'other', inplace=True)\n        self.artist_frame = self.artist_frame.reset_index(drop=True)\n        # Pick a threshold that makes 10 categories of style\n        style_threshold = 300\n\n        value_counts = self.artist_frame['style'].value_counts() # Specific column \n        to_remove = value_counts[value_counts <= style_threshold].index\n        self.artist_frame['style'].replace(to_remove, 'other', inplace=True)\n        self.artist_frame = self.artist_frame.reset_index(drop=True)\n        index_names = self.artist_frame[self.artist_frame['pixelsx'] >= 18000].index\n        self.artist_frame.drop(index_names, inplace = True)\n        self.artist_frame = self.artist_frame.reset_index(drop=True)\n        # Keep only one entry per file\n        self.artist_frame = self.artist_frame.drop_duplicates(subset=['new_filename']).reset_index(drop=True)\n        self.artist_frame = self.artist_frame.reset_index(drop=True)\n        self.genre_dict = self.artist_frame['genre'].drop_duplicates().reset_index(drop='true').to_dict()\n        self.root_dir = root_dir\n        del to_remove\n        del value_counts\n        del style_threshold\n        del genre_threshold\n        self.transform = transform\n\n    def __len__(self):\n        self.artist_frame = self.artist_frame.reset_index(drop=True)\n        return len(self.artist_frame)\n\n    def __getitem__(self, idx):\n        if torch.is_tensor(idx):\n            idx = idx.tolist()\n        img_name = os.path.join(self.root_dir,self.artist_frame['new_filename'][idx])\n        # ensure image stays or changes to PIL image or tensor\n        image = Image.open(img_name).convert('RGB')\n        genre = self.artist_frame['genre'][idx]\n        genre_key = int(get_genre_key(genre))\n        style = self.artist_frame['style'][idx]\n        sample = {'image': image, 'genre_key': genre_key}\n\n        if self.transform:\n\n            sample['image'] = self.transform(sample['image'])\n\n        return sample","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let’s instantiate this class and iterate through the data samples. We will print the sizes of first 4 samples and show their artist, genre, and style","metadata":{}},{"cell_type":"code","source":"wikiart_dataset = WikiArtDataset(csv_file='/kaggle/input/painter-by-numbers/all_data_info.csv',\n                                    root_dir='/kaggle/working/train')\n\nfig = plt.figure()\nprint(len(wikiart_dataset))\nfor i in range(len(wikiart_dataset)):\n    sample = wikiart_dataset[i]\n\n    print(i, sample['image'].size, sample['genre_key'])\n    plt.imshow(sample['image'])\n    ax = plt.subplot(1, 4, i + 1)\n    plt.tight_layout()\n    ax.set_title('Sample #{}'.format(sample['genre_key']))\n    ax.axis('off')\n    # show_landmarks(**sample)\n\n    if i == 3:\n        \n        plt.show()\n        break","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Compose transforms\n\nNow, we apply the transforms on a sample.\n\nLet’s say we want to rescale the shorter side of the image to 256 and then randomly crop a square of size 224 from it. i.e, we want to compose Rescale and RandomCrop transforms. torchvision.transforms.Compose is a simple callable class which allows us to do this.","metadata":{}},{"cell_type":"code","source":"scale = transforms.Resize(256)\ncrop = transforms.RandomCrop(128)\ncomposed = transforms.Compose([transforms.Resize(256),\n                               transforms.RandomCrop(224)])\n\n\n# Apply each of the above transforms on sample.\nfig = plt.figure()\nsample = wikiart_dataset[5]\n\nfor i, tsfrm in enumerate([scale, crop, composed]):\n    transformed_sample = tsfrm(sample['image'])\n    ax = plt.subplot(1, 3, i + 1)\n    plt.tight_layout()\n    ax.set_title(i)\n    plt.imshow(transformed_sample)\n    \n\nplt.show()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Iterating through the dataset\n\nLet’s put this all together to create a dataset with composed transforms. To summarize, every time this dataset is sampled:\n\n    An image is read from the file on the fly\n    Transforms are applied on the read image\n    Since one of the transforms is random, data is augmentated on sampling\n\nWe can iterate over the created dataset with a for i in range loop as before.","metadata":{}},{"cell_type":"code","source":"transformed_dataset = WikiArtDataset(csv_file='/kaggle/input/painter-by-numbers/all_data_info.csv',\n                                           root_dir='/kaggle/working/train',\n                                           transform=transforms.Compose([\n                                               # transforms.ToPILImage(),\n                                               transforms.Resize(300),\n                                               transforms.RandomCrop(300),\n                                               transforms.RandomRotation(15),\n                                               transforms.CenterCrop(250),\n                                               transforms.RandomCrop(224),\n                                               transforms.RandomHorizontalFlip(),\n                                               transforms.ToTensor(),\n                                               transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n                                               transforms.Lambda(lambda x : x + 0.1*torch.randn_like(x))\n                                           ]))\n\nfor i in range(len(transformed_dataset)):\n    sample = transformed_dataset[i]\n    \n    print(i, sample['image'].size(), sample['genre_key'])\n\n    if i == 3:\n        break","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Train, Validation split\nThe transfer learning tutorial calls for a train dataset and a validation dataset, so we'll do that here","metadata":{}},{"cell_type":"code","source":"import copy\nimport torch\nimport time\nfrom torchvision import transforms, datasets, models, utils\n# 70 % of data for training \ntrain_dataset_len = int(len(transformed_dataset)*0.7)\n# Use the rest for validation\nvalid_dataset_len = len(transformed_dataset) - train_dataset_len\n# Probably not the best approach transforming the validation dataset the same way as the training dataset\n# but it's good enough for Cindy work\ntrain_dataset, valid_dataset = torch.utils.data.random_split(transformed_dataset, (train_dataset_len, valid_dataset_len))\n# pick batch size that's a power of 2 and is approx 1% of the respective dataset\ntrain_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=384, shuffle=True, num_workers=4)\nvalid_dataloader = torch.utils.data.DataLoader(valid_dataset, batch_size=256, shuffle=True, num_workers=4)\nprint(len(train_dataloader))\nprint(len(valid_dataloader))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html\n\nThese two major transfer learning scenarios look as follows:\n\n- Finetuning the convnet: Instead of random initializaion, we initialize the network with a pretrained network, like the one that is trained on imagenet 1000 dataset. Rest of the training looks as usual.\n- ConvNet as fixed feature extractor: Here, we will freeze the weights for all of the network except that of the final fully connected layer. This last fully connected layer is replaced with a new one with random weights and only this layer is trained.\n","metadata":{}},{"cell_type":"markdown","source":"## Visualize a few images\n\nLet’s visualize a few training images so as to understand the data augmentations.","metadata":{}},{"cell_type":"code","source":"def imshow(inp, title=None):\n    \"\"\"Imshow for Tensor.\"\"\"\n    inp = inp.numpy().transpose((1, 2, 0))\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    inp = std * inp + mean\n    inp = np.clip(inp, 0, 1)\n    # Hard to see, so make it bigger\n    plt.figure(figsize = (60,60))\n    plt.imshow(inp)\n    if title is not None:\n        plt.title(title)\n    plt.pause(0.001)  # pause a bit so that plots are updated\n\n\n# Get a batch of training data\ndata1 = next(iter(train_dataloader))\nimage = data1['image']\ngenre = data1['genre_key']\nprint(data1['image'].shape)\n# Make a grid from batch\nout = utils.make_grid(image, nrow=16)\n\nimshow(out, title=genre)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Training the model\n\nNow, let’s write a general function to train a model. Here, we will illustrate:\n\n    Scheduling the learning rate\n    Saving the best model\n\nIn the following, parameter scheduler is an LR scheduler object from torch.optim.lr_scheduler.","metadata":{}},{"cell_type":"code","source":"def train_model(model, criterion, optimizer, scheduler, num_epochs=25):\n    since = time.time()\n\n    best_model_wts = copy.deepcopy(model.state_dict())\n    best_acc = 0.0\n\n    for epoch in range(num_epochs):\n        print('Epoch {}/{}'.format(epoch, num_epochs - 1))\n        print('-' * 10)\n\n        # Each epoch has a training and validation phase\n        for phase in ['train', 'val']:\n            if phase == 'train':\n                model.train()  # Set model to training mode\n                dataloader1 = train_dataloader\n            else:\n                model.eval()   # Set model to evaluate mode\n                dataloader1 = valid_dataloader\n\n            running_loss = 0.0\n            running_corrects = 0\n            batch_idx = 0\n            # Iterate over data.\n            dataset_size = len(dataloader1)\n            for data1 in iter(dataloader1):\n                # print(data1)\n                inputs = data1['image']\n                labels = data1['genre_key']\n                batch_idx = batch_idx+1\n                # zero the parameter gradients\n                optimizer.zero_grad()\n\n                # forward\n                # track history if only in train\n                with torch.set_grad_enabled(phase == 'train'):\n                    outputs = model(inputs)\n                    _, preds = torch.max(outputs, 1)\n                    loss = criterion(outputs, labels)\n\n                    # backward + optimize only if in training phase\n                    if phase == 'train':\n                        loss.backward()\n                        optimizer.step()\n                print(batch_idx, loss.item())\n                # statistics\n                running_loss += loss.item() * inputs.size(0)\n                running_corrects += torch.sum(preds == labels.data)\n            if phase == 'train':\n                scheduler.step()\n\n            epoch_loss = running_loss / dataset_sizes\n            epoch_acc = running_corrects.double() / dataset_sizes\n\n            print('{} Loss: {:.4f} Acc: {:.4f}'.format(\n                phase, epoch_loss, epoch_acc))\n\n            # deep copy the model\n            if phase == 'val' and epoch_acc > best_acc:\n                best_acc = epoch_acc\n                best_model_wts = copy.deepcopy(model.state_dict())\n\n        print()\n\n    time_elapsed = time.time() - since\n    print('Training complete in {:.0f}m {:.0f}s'.format(\n        time_elapsed // 60, time_elapsed % 60))\n    print('Best val Acc: {:4f}'.format(best_acc))\n\n    # load best model weights\n    model.load_state_dict(best_model_wts)\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Visualizing the model predictions\n\nGeneric function to display predictions for a few images","metadata":{}},{"cell_type":"code","source":"def visualize_model(model, num_images=6):\n    was_training = model.training\n    model.eval()\n    images_so_far = 0\n    fig = plt.figure()\n\n    with torch.no_grad():\n        for i, (inputs, labels) in enumerate(dataloaders['val']):\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n\n            outputs = model(inputs)\n            _, preds = torch.max(outputs, 1)\n\n            for j in range(inputs.size()[0]):\n                images_so_far += 1\n                ax = plt.subplot(num_images//2, 2, images_so_far)\n                ax.axis('off')\n                ax.set_title('predicted: {}'.format(class_names[preds[j]]))\n                imshow(inputs.cpu().data[j])\n\n                if images_so_far == num_images:\n                    model.train(mode=was_training)\n                    return\n        model.train(mode=was_training)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Finetuning the convnet\n\nLoad a pretrained model and reset final fully connected layer.","metadata":{}},{"cell_type":"code","source":"model_ft = models.resnet18(pretrained=True)\nnum_ftrs = model_ft.fc.in_features\n# Here the size of each output sample is set to 13 1 for each genre.\n# Alternatively, it can be generalized to nn.Linear(num_ftrs, len(class_names)).\nmodel_ft.fc = torch.nn.Linear(num_ftrs, len(genre_dict))\n\ncriterion = torch.nn.CrossEntropyLoss()\n\n# Observe that all parameters are being optimized\n# optimizer_ft = torch.optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)\noptimizer_ft = torch.optim.Adam(model_ft.parameters(), lr=0.001)\n# Decay LR by a factor of 0.1 every 7 epochs\nexp_lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=25)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}