{"cells":[{"metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","trusted":true},"cell_type":"code","source":"import os\nimport sys\nimport time\nimport math\nimport random\nimport librosa\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.init as init\nfrom torchvision import transforms, utils\nfrom torch.utils.data import Dataset, DataLoader\n\n\n# Ignore warnings\nimport warnings\nwarnings.filterwarnings(\"ignore\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# seeding function for reproducibility\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","trusted":true},"cell_type":"code","source":"ROOT = \"/kaggle/input/birdsong-recognition/\"\nos.listdir(ROOT)","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"df = pd.read_csv(os.path.join(ROOT, 'train.csv'))[['ebird_code', 'filename', 'duration']]\ndf['path'] = ROOT+'train_audio/' + df['ebird_code'] + \"/\" + df['filename']\ndf.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"SEED = 42\nFRAC = 0.2     # Validation fraction\nSR = 32000     # sampling rate\nMAXLEN = 60    # seconds\nN_MELS = 128\nbatch_size = 8\n\nseed_everything(SEED)\ndevice = torch.device('cpu')\n\n#Random sample of 10 birds to test code.\nclasses = set(random.sample(df['ebird_code'].unique().tolist(), 10)) \nprint(classes)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df = df[df.ebird_code.apply(lambda x: x in classes)].reset_index(drop=True)\nkeys = set(df.ebird_code)\nvalues = np.arange(0, len(keys))\ncode_dict = dict(zip(sorted(keys), values))\ndf['label'] = df['ebird_code'].apply(lambda x: code_dict[x])\ndf.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class BirdSoundDataset(Dataset):\n    \"\"\"Bird Sound dataset.\"\"\"\n\n    def __init__(self, df):\n        \"\"\"\n        Args:\n            df (pd.DataFrame): must have ['path', 'label'] columns\n        \"\"\"\n        self.df = df\n\n    def __len__(self):\n        return len(self.df)\n    \n    \n    def loadMP3(self, path, duration):\n        \"\"\"\n        \"\"\"\n        try:\n            audio, sample_rate = librosa.load(path, \n                                              sr=SR, \n                                              mono=True, \n                                              offset=0.0,\n                                              duration=duration, \n                                              res_type='kaiser_fast')\n            mels = librosa.feature.melspectrogram(y=audio, sr=SR,n_mels=N_MELS)\n            return mels\n            # mels will be of shape (N_MELS, ceil(duration*SR/512)) \n            # 512 here is default hop length\n\n        except Exception as e:\n            print(\"Error encountered while parsing file: \", path, e)\n            mels = np.zeros((N_MELS, MAXLEN*SR//512), dtype=np.float32)\n            return mels\n            \n\n    def __getitem__(self, idx):\n        if torch.is_tensor(idx):\n            idx = idx.tolist()\n        \n        path = self.df['path'].iloc[idx]\n        duration = self.df['duration'].iloc[idx]\n        if duration < MAXLEN:\n            duration = None # read entire file\n        else:\n            duration = MAXLEN\n        if os.path.exists(\"./\"+path.split('/')[-1]+\".npy\"):\n            mels = np.load(\"./\"+path.split('/')[-1]+\".npy\")\n        else:\n            mels = self.loadMP3(path, duration)\n            np.save(\"./\"+path.split('/')[-1]+\".npy\", mels)\n        label  = self.df['label'].iloc[idx]\n        sample = {'label':label, 'features': mels, 'duration': duration}\n        return sample","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"ds = BirdSoundDataset(df)\nds[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Train Val split\ndf = df.sample(frac=1).reset_index(drop=True)\ntrain_len = int(len(df) * (1-FRAC))\ntrain_df = df.iloc[:train_len]\nvalid_df = df.iloc[train_len:]\ntrain_df.shape, valid_df.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### custom collect function to handle features of different lengths\n#### Wrap features along the time axis to get all elements on batch in same shape","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# Custom collect function to wrap sound features\n\nimport random\n\ndef collate_fn_wrap(batch):\n    '''\n    wraps batch of variable length\n    '''\n    \n    ## get sequence lengths\n    lengths = [t['features'].shape[1] for t in batch]\n    maxlen = max(312, random.choice(lengths))#max(lengths)\n    \n    for i in range(len(batch)):\n        batch[i]['features'] = torch.from_numpy(batch[i]['features'])\n        k = math.ceil(maxlen/lengths[i])\n        batch[i]['features'] = batch[i]['features'].repeat(1, k)[:, :maxlen]\n        # assert batch[i]['features'].shape[1] == maxlen\n        \n    labels = torch.tensor([i['label'] for i in batch])\n    features = torch.stack([i['features'] for i in batch])\n    return {'features':features, 'labels':labels}","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# prepare data loaders\ntrain_loader = torch.utils.data.DataLoader(BirdSoundDataset(train_df),\n                                           batch_size=batch_size, \n                                           num_workers=4, \n                                           shuffle=True, \n                                           collate_fn=collate_fn_wrap,\n                                           drop_last = True)\n\nvalid_loader = torch.utils.data.DataLoader(BirdSoundDataset(valid_df), \n                                           batch_size=batch_size, \n                                           num_workers=4, \n                                           shuffle=False, \n                                           collate_fn=collate_fn_wrap,\n                                           drop_last = True)\n\nlen(train_loader), len(valid_loader)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Model \n### Conv2D's -> LSTM -> Classifier","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch.nn as nn\nimport torch.nn.functional as F\nimport random\n\n\nclass BirdModel(nn.Module):\n    \n    def __init__(self, outdim=len(classes)):\n        super(BirdModel, self).__init__()\n        \n        self.conv1 = torch.nn.Conv2d(1, 32, (16, 8), \n                                    stride=(8, 4),\n                                    padding=0, \n                                    dilation=1,\n                                    groups=1, \n                                    bias=True, \n                                    padding_mode='zeros')\n    \n        self.conv2 = torch.nn.Conv2d(32, 256, (8, 16), \n                                    stride=(1, 8),\n                                    padding=0, \n                                    dilation=1,\n                                    groups=1, \n                                    bias=True, \n                                    padding_mode='zeros')\n        \n        self.lstm = torch.nn.LSTM(input_size=256,\n                                  hidden_size=256,\n                                  num_layers=2, \n                                  dropout=0.2,\n                                  bidirectional=True)\n        \n        self.pool = torch.nn.MaxPool2d(kernel_size=(2,2),\n                                       stride=None,\n                                       padding=0,\n                                       dilation=1,\n                                       return_indices=False,\n                                       ceil_mode=True)\n        \n        self.fc = nn.Linear(2*256+128, outdim)\n        self.MaxPool1d = nn.AdaptiveMaxPool1d(1)\n        self.drop = nn.Dropout(p=0.2)\n        \n    def forward(self, input):\n        \n        avg_features = torch.mean(input, dim=2)\n        x = self.pool(self.conv1(input.unsqueeze(1)))\n        x = self.pool(self.conv2(x))\n        x = x.squeeze(2).permute(2, 0, 1)\n        \n        x, _ = self.lstm(x)\n        x = self.MaxPool1d(x.permute(1, 2, 0)).squeeze(2)\n        x = torch.cat((x, avg_features), dim=1)\n        return self.fc(self.drop(x))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model = BirdModel()\nmodel.to(device)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"criterion = torch.nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr = 0.001)\nscheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Mixup Augmentation","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def mixup_data(x, y, alpha=1.0, use_cuda=True):\n\n    '''Compute the mixup data. Return mixed inputs, pairs of targets, and lambda'''\n    if alpha > 0.:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.\n    batch_size = x.size()[0]\n    if use_cuda:\n        index = torch.randperm(batch_size).cuda()\n    else:\n        index = torch.randperm(batch_size)\n\n    mixed_x = lam * x + (1 - lam) * x[index,:]\n    y_a, y_b = y, y[index]\n    return mixed_x, y_a, y_b, lam\n\ndef mixup_criterion(y_a, y_b, lam):\n    return lambda criterion, pred: lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# number of epochs to train the model\nn_epochs = 20\n\nvalid_loss_min = np.Inf # track change in validation loss\n\nfor epoch in range(1, n_epochs+1):\n\n    # keep track of training and validation loss\n    train_loss = 0.0\n    valid_loss = 0.0\n    \n    ###################\n    # train the model #\n    ###################\n    model.train()\n    \n    bar = tqdm(train_loader, total=len(train_loader), leave=False)\n    for data in bar:\n        \n        features = data['features'].to(device)\n        target = data['labels'].to(device)\n        \n        optimizer.zero_grad()\n        \n        inputs, targets_a, targets_b, lam = mixup_data(features, target, 0.2, use_cuda=torch.cuda.is_available())\n        outputs = model(inputs)\n        loss_func = mixup_criterion(targets_a, targets_b, lam)\n        loss = loss_func(criterion, outputs)\n        \n        loss.backward()\n        \n        optimizer.step()\n        \n        bar.set_postfix({'loss': loss.item()})\n        train_loss += loss.item()*features.size(0)\n        \n    ######################    \n    # validate the model #\n    ######################\n    with torch.no_grad():\n        targets = []\n        preds = []\n        model.eval()\n        bar = tqdm(valid_loader, total=len(valid_loader), leave=False)\n        for data in bar:\n            features = data['features'].to(device)\n            target = data['labels'].to(device)\n            \n            output = model(features)\n            loss = criterion(output, target)\n            \n            pred = torch.argmax(output, dim=1)\n            \n            targets.extend(target.cpu().detach().numpy().tolist())\n            preds.extend(pred.cpu().detach().numpy().tolist())\n            \n            # update average validation loss\n            valid_loss += loss.item()*features.size(0)\n    \n    acc = np.sum(np.array(preds) == np.array(targets)) / len(preds)\n    \n    \n    scheduler.step()\n    \n    # calculate average losses\n    train_loss = train_loss/len(train_loader.dataset)\n    valid_loss = valid_loss/len(valid_loader.dataset)\n        \n    # print training/validation statistics \n    print('Epoch: {} \\tValidation Acc: {:.6f}'.format(epoch, acc))\n    print('Training Loss: {:.6f} \\tValidation Loss: {:.6f}'.format(train_loss, valid_loss))\n    \n    # save model if validation loss has decreased\n    if valid_loss <= valid_loss_min:\n        print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(\n        valid_loss_min,\n        valid_loss))\n        torch.save(model.state_dict(), 'model_cifar.pt')\n        valid_loss_min = valid_loss","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"print(preds[:5], targets[:5])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(classification_report(targets, preds))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\n\nconfusion_matrix(targets, preds)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!rm *.npy","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}