{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Introdução\n\n### Notebook baseado em: [pierretihon notebook](https://www.kaggle.com/pierretihon/a-homemade-tensorflow-model).\n\n#### Esse notebook tem o propósito de mostrar como construir uma rede convolucional usando Pytorch.\n#### Iremos criar a rede, as classes necessárias para carregar os dados, ver como utilizar uma GPU para o treinamento, e muito mais!\n\n\n#### O notebook está dividido em:\n\n1. Módulos/Classes\n    1. Config\n    2. Dataset\n    3. Modelo\n2. Carregamento dos dados\n3. Visualização dos dados\n4. Coletando um subset dos dados\n5. Treino do modelo\n6. Submissão\n\n"},{"metadata":{},"cell_type":"markdown","source":"# Importando classes necessárias"},{"metadata":{"trusted":true},"cell_type":"code","source":"\nimport os\nimport pickle\nfrom PIL import Image\n\n# classes relativas ao PyTorch\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import transforms\n\nfrom albumentations import (\n    Compose, Normalize, Resize, RandomResizedCrop, RandomCrop, HorizontalFlip\n)\nfrom albumentations.pytorch import ToTensorV2\n\nimport cv2\n\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.model_selection import train_test_split\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Para garantir que a execução use a mesma seed aleatória, configuramos para ser a mesma em qualquer execução do notebook."},{"metadata":{"trusted":true},"cell_type":"code","source":"seed = 42\nnp.random.seed(42)\ntorch.cuda.manual_seed_all(seed)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 1) Módulos/Classes\n\n## 1.1 Config class\nClasse que contém alguns parâmetros de configuração, como tamanho da imagem, caminho (*path*) até a imagem, classes que queremos predizer."},{"metadata":{"trusted":true},"cell_type":"code","source":"class Config:\n    img_train_path = '../input/ranzcr-clip-catheter-line-classification/train'\n    img_test_path = '../input/ranzcr-clip-catheter-line-classification/test'\n    \n    batch_size = 64\n    img_width= 256\n    img_height = 256\n    \n    target_cols = ['ETT - Abnormal', 'ETT - Borderline', 'ETT - Normal',\n                   'NGT - Abnormal', 'NGT - Borderline', 'NGT - Incompletely Imaged', 'NGT - Normal',\n                   'CVC - Abnormal', 'CVC - Borderline', 'CVC - Normal', 'Swan Ganz Catheter Present']\n    \n    n_classes = len(target_cols)\n    n_workers = 8","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 1.2 Dataset class\nPara carregarmos os dados para treinamento em Pytorch, é recomendado a construção de uma classe Dataset, que abstrai o carregamento dos dados, e permite que apliquemos transformações dado o contexto (treino, validação ou teste). Nela, é necessário que implementemos os seguintes métodos:\n- __init__\n- __getiitem__\n- __len__"},{"metadata":{"trusted":true},"cell_type":"code","source":"class ImageDataset(Dataset):\n    \n    def __init__(self, df, mode):\n        super().__init__()\n        self.filenames = df['StudyInstanceUID'].values\n        self.labels = df[Config.target_cols].values\n        self.len = len(df)\n        self.transform = self.train_transforms() if mode == 'train' else self.valid_transforms() if mode == 'valid' else None\n        \n    def __getitem__(self, idx):\n        filename = self.filenames[idx]\n        filepath = f'{Config.img_train_path}/{filename}.jpg'\n        image = cv2.imread(filepath)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        \n        label = torch.tensor(self.labels[idx]).float()\n        return image, label\n    \n    def __len__(self):\n        return self.len\n    \n    def train_transforms(self):\n        # transformações usadas no treino\n        return Compose([\n            Resize(Config.img_width, Config.img_height),\n            RandomResizedCrop(Config.img_width, Config.img_height, scale=(0.85, 1.0)),\n            HorizontalFlip(p=0.5),\n            Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225],\n            ),\n            ToTensorV2(),\n        ])\n\n    def valid_transforms(self):\n        # transformações usadas em validação\n        return Compose([\n            Resize(Config.img_width, Config.img_height),\n            Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225],\n            ),\n            ToTensorV2(),\n        ])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 1.3 Model class\nAgora temos de construir a classe do modelo. Em Pytorch, o módulo ```torch.nn``` contém as abstrações de uma rede neural necessárias para o aprendizado, como forward, ajuste dos pesos etc. Isso nos ajuda bastante, porque para definir uma rede, basta herdar o módulo em sua classe, e implementar 2 métodos:\n\n- __init__\n- __forward__\n\n### Vamos a construção da rede abaixo:\nEla é uma rede neural convolucional bem simples com basicamente 3 peças: as convoluções, o average pooling ([mais informações](https://pytorch.org/docs/stable/generated/torch.nn.AvgPool2d.html)) e a última parte, uma rede densa.\n\nO average pooling até não seria necessário, já que a parte convolucional e a densa já iriam compor uma rede neural convolucional.\n\n#### Na parte convolucional\nSão 4 camadas encadeadas de: Convolução, função de ativação (ReLU) e max pooling.\nVocê pode encontrar mais informações sobre essas operações [aqui](https://cs231n.github.io/convolutional-networks/).\n\n#### Na parte densa\nSão 5 camadas e uma de output, as 3 primeiras com [dropout](https://deeplearningbook.com.br/capitulo-23-como-funciona-o-dropout/), as duas últimas sem. Nossa saída é uma sigmóide, com 11 classes."},{"metadata":{"trusted":true},"cell_type":"code","source":"class SimpleModel(nn.Module):\n    def __init__(self, n_classes=Config.n_classes):\n        super(SimpleModel, self).__init__()\n        self.conv = nn.Sequential(\n            # first\n            nn.Conv2d(in_channels=3, out_channels=128, kernel_size=4, stride=4, padding=2),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2),\n            # second\n            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=2, stride=2, padding=2),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2),\n            # third\n            nn.Conv2d(in_channels=256, out_channels=128, kernel_size=2, stride=2, padding=2),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2),\n            # fourth\n            nn.Conv2d(in_channels=128, out_channels=64, kernel_size=2, stride=2, padding=2),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2)\n        )\n        self.avgpool = nn.AdaptiveAvgPool2d((4, 4))\n        self.dense = nn.Sequential(\n            nn.Flatten(),\n            #first\n            nn.Dropout(p=0.05),\n            nn.Linear(64*4*4, 512),\n            nn.ReLU(),\n            # second\n            nn.Dropout(p=0.1),\n            nn.Linear(512, 512),\n            nn.ReLU(),\n            # third\n            nn.Dropout(p=0.3),\n            nn.Linear(512, 128),\n            nn.ReLU(),\n            # fourth\n            nn.Linear(128, 64),\n            nn.ReLU(),\n            # fifth\n            nn.Linear(64, 32),\n            nn.ReLU(),\n            # output\n            nn.Linear(32, n_classes),\n            nn.Sigmoid()\n        )\n        \n    \n    def forward(self, x):\n        x = self.conv(x)\n        x = self.avgpool(x)\n        x = self.dense(x)\n        return x","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 2) Carregamento dos dados"},{"metadata":{},"cell_type":"markdown","source":"Iremos carregar os dados direto do Kaggle."},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"# useful paths\ncatherer_path = '/kaggle/input/ranzcr-clip-catheter-line-classification'\ntrain_path = os.path.join(catherer_path,'train')\ntest_path = os.path.join(catherer_path,'test')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Observando o shape do dataset e valores nulos:"},{"metadata":{"trusted":true},"cell_type":"code","source":"# train\ntrain_csv_path = os.path.join(catherer_path,'train.csv')\ntrain_df = pd.read_csv(train_csv_path)\n\nclasses = [col for col in train_df.columns if col not in ['StudyInstanceUID','PatientID']]\nprint(f'There are {len(classes)} to predict')\n\nprint(f\"Shape of train dataframe : {train_df.shape}\")\nprint(f\"check for null values: {train_df.isnull().sum().sum()}\")\n\n# test\ntest_csv_path = os.path.join(catherer_path,'sample_submission.csv')\ntest_df = pd.read_csv(test_csv_path)\ntest_filenames = test_df.StudyInstanceUID\n\nprint(f\"Shape of test dataframe : {test_df.shape}\")\nprint(f\"check for null values: {test_df.isnull().sum().sum()}\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Observando um trecho do nosso conjunto:"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 3) Visualização dos dados"},{"metadata":{},"cell_type":"markdown","source":"Observando uma imagem:"},{"metadata":{"trusted":true},"cell_type":"code","source":"img_path = Config.img_train_path + '/' + train_df['StudyInstanceUID'].iloc[0] + '.jpg'\nimg_example = Image.open(img_path)\nprint(f\"Image size = {img_example.size}\")\nplt.figure(figsize=(12,8))\nplt.imshow(img_example,cmap='Greys');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Podemos verificar pelo shape da imagem que ela é grande, e podemos diminuir as dimensões dela sem muita perda de informação."},{"metadata":{"trusted":true},"cell_type":"code","source":"img_example_red = img_example.resize((Config.img_width, Config.img_height))\nplt.figure(figsize=(12,8))\nplt.imshow(img_example_red,cmap='Greys');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 4) Coletando um subset dos dados"},{"metadata":{},"cell_type":"markdown","source":"Podemos limitar a quantidade de dados utilizadas para treino, para acelerar o processo de verificar o comportamento do modelo:"},{"metadata":{"trusted":true},"cell_type":"code","source":"frac = 0.8\nlim = True\nif lim:\n    red_train_df = train_df.sample(frac=frac)\nelse:\n    red_train_df = train_df.copy()\nprint(red_train_df.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"n_min = 1000\ncount_classes = red_train_df[classes].sum()\next_train_df = [red_train_df]\nfor pred_class in classes:\n    if count_classes[pred_class] < n_min:\n        new_df = red_train_df[red_train_df[pred_class]==1].sample(n_min,replace=True)\n        ext_train_df.append(new_df)\n        \next_train_df = pd.concat(ext_train_df)\nprint(ext_train_df.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Agora o número de classes em nosso DataFrame é:"},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(10,6))\ngraph = sns.barplot(x=classes,y=ext_train_df[classes].sum())\ngraph.set_xticklabels(graph.get_xticklabels(), rotation=90);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 5) Treino do modelo"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Dividindo em treino e validação\nX_train, X_valid = train_test_split(ext_train_df, test_size=0.2, shuffle=True)\nprint(f' X_train shape: {X_train.shape} and X_valid shape: {X_valid.shape}')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dataset = ImageDataset(df=X_train, mode='train')\nvalid_dataset = ImageDataset(df=X_valid, mode='valid')\ntest_dataset = ImageDataset(df=test_df, mode='valid')\n\n# A classe DataLoader é necessária para o carregamento dos batches\ntrain_dataloader = DataLoader(train_dataset, pin_memory=True, batch_size=Config.batch_size, num_workers=Config.n_workers, shuffle=True)\nvalid_dataloader = DataLoader(valid_dataset, pin_memory=True, batch_size=Config.batch_size, num_workers=Config.n_workers, shuffle=False)\ntest_dataloader = DataLoader(test_dataset, batch_size=Config.batch_size, num_workers=Config.n_workers, shuffle=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model = SimpleModel()\n# Nosso otimizador, necessário para o treinamento da rede, junto com a função de custo\noptimizer = torch.optim.AdamW(model.parameters())\nloss = nn.BCELoss()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Treinamento da rede\n\nAbaixo, temos a função com o loop de treinamento da nossa rede. \n\n1. Nele, fazemos um loop no dataloader, recuperando os dados de treino e o target. Aqui é necessário chamar o método .to(device) para passarmos os dados a GPU;\n2. Zeramos os gradientes previamente computados (necessário no Pytorch);\n3. Dizemos que o modelo está em modo de treinamento (```model.train()```);\n4. Passamos a imagem ao modelo (convertendo os valores para float) e temos o output;\n5. O output é usado para calcularmos a loss, utilizando também nossos valores de target;\n6. Realizamos o backward propagation;\n7. Damos um passo em direção a um mínimo local com o otimizador.\n\nUfa! Foram muitas etapas, mas são elas que possibilitam que nossa rede aprenda.\nTambém, ao fim do loop de treino, chamamos o função de validação para verificar como a função de custo se comporta."},{"metadata":{"trusted":true},"cell_type":"code","source":"train_history = []\nvalid_history = []\n\ndef train(model, loss, optimizer, train_dataloader, valid_dataloader, device='cuda', epochs=50):\n    for e in range(1, epochs+1):\n        for train_values, train_target in train_dataloader:\n            train_values, train_target = train_values.to(device), train_target.to(device)\n            # loop principal\n            optimizer.zero_grad()   \n            model.train()\n            output = model(train_values.float())\n            loss_train = loss(output, train_target.float())\n            loss_train.backward()\n            optimizer.step()\n        \n        print(f'Epoch {e}: \\ttrain loss {loss_train.item():.2f}')\n        valid(model, loss, optimizer, valid_dataloader, device, epochs=1)\n        train_history.append(loss_train)\n            \ndef valid(model, loss, optimizer, valid_dataloader, device, epochs=50):\n    for e in range(1, epochs+1):\n        for val_values, val_target in valid_dataloader:\n            val_values, val_target = val_values.to(device), val_target.to(device)\n            \n            model.eval()\n            val_output = model(val_values.float())\n            loss_val = loss(val_output, val_target.float())\n        \n        valid_history.append(loss_val)\n        print(f'\\t\\tvalidation loss {loss_val.item():.2f}')\n            \ndef predict_probs(filenames, model, device='cuda'):\n    model.eval()\n\n    transform = test_dataset.valid_transforms()\n    \n    predictions = []\n    for filename in tqdm(filenames):\n        filepath = f'{Config.img_test_path}/{filename}.jpg'\n        image = cv2.imread(filepath)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = transform(image=image)['image']\n        image = image.unsqueeze(0).to(device)\n        predictions.append(model(image).detach().cpu().numpy())\n    return predictions","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Um ponto crucial para treinarmos a rede convolucional é o uso da GPU\n\nPrecisamos verificar se a GPU está disponível, e posteriormente, passar o modelo para GPU (para que ele seja processado nessa unidade, e não na CPU)"},{"metadata":{"trusted":true},"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel.to(device)\nprint(f'Current device is {device}')\nprint(model)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Treinando o modelo:"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Treinamento do modelo\ntrain(model, loss, optimizer, train_dataloader, valid_dataloader, device, epochs=15)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 6) Submissão"},{"metadata":{},"cell_type":"markdown","source":"Para não precisarmos rodar o notebook todas as vezes para obter as saídas do modelo, podemos salvar os pesos da rede.\nEm torch, a função save permita que façamos isso:"},{"metadata":{"trusted":true},"cell_type":"code","source":"torch.save(model.state_dict(), 'simple_model.pt')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Verificando a curva de treino e de validação:"},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(range(len(train_history)), train_history)\nplt.plot(range(len(valid_history)), valid_history)\nplt.xlabel('Epochs')\nplt.ylabel('Loss')\nplt.show();","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Fazendo as predições no conjunto de teste e salvando em formato pickle"},{"metadata":{"trusted":true},"cell_type":"code","source":"predictions = predict_probs(test_filenames, model)\nprint(f'Predictions type = {type(predictions)}')\nprint(f'Predictions size = {len(predictions)}')\nwith open(\"preds.pkl\", \"wb\") as fp:\n    pickle.dump(predictions, fp)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Carrega o arquivo pkl\n# with open('../input/preds2/preds.pkl', 'rb') as pickle_file:\n#     preds = pickle.load(pickle_file)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Exemplo de predição\nprint(predictions[0])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"preds_one_len = []\nfor pred in predictions:\n    preds_one_len.append(pred.squeeze())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Criamos um DataFrame para submeter nossas predições, com os respectivos nomes das categorias"},{"metadata":{"trusted":true},"cell_type":"code","source":"pred_df = pd.DataFrame(columns=Config.target_cols, data=preds_one_len, index=test_df.index)\npred_df = pd.concat([test_df['StudyInstanceUID'],pred_df],axis=1)\npred_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Create the csv\npred_df.to_csv('submission.csv',index=False)","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}