{"cells":[{"metadata":{},"cell_type":"markdown","source":"\nNote: Instability between CV and LB\n\nSome work for this notebook:\n- Multi-label + LWLRAP metric\n- Multi-fold\n- Modify loss\n- Augs\n\nThank @koukyo1994 and credit: https://github.com/koukyo1994/kaggle-birdcall-6th-place"},{"metadata":{},"cell_type":"markdown","source":"# Library"},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install pytorch_lightning efficientnet_pytorch colorednoise torchlibrosa","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import sys\nsys.path.append('../input/kagglebirdcall/src')\n\nimport warnings\nwarnings.simplefilter(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\nimport os, math\nimport librosa\nimport librosa.display as display\nfrom collections import defaultdict\nfrom tqdm import tqdm\nfrom sklearn.model_selection import StratifiedKFold, train_test_split\nfrom sklearn.metrics import label_ranking_average_precision_score\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom pytorch_lightning.core import LightningModule\nfrom pytorch_lightning import Trainer\nfrom pytorch_lightning.callbacks import ModelCheckpoint\nfrom pytorch_lightning.loggers import TensorBoardLogger\n\nfrom models import *\nimport transforms as kbt","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Setting"},{"metadata":{"trusted":true},"cell_type":"code","source":"PERIOD = 10\nEPOCH = 80\nDEVICE= \"cuda:0\"\n\nbatch_size = 24\nsample_rate = 48000\nwindow_size = 2048\nhop_size = 512\nmel_bins = 128\nfmin = sample_rate/2.0\nfmax = 0\nclasses_num = 24\nis_inference = False  # if True, pls update weight for predict blow\n\ndata_dir = '../input/rfcx-species-audio-detection'\ntrain_tp_df = pd.read_csv(os.path.join(data_dir, 'train_tp.csv'))\ntrain_fp_df = pd.read_csv(os.path.join(data_dir, 'train_fp.csv'))\ndata_test = pd.read_csv(os.path.join(data_dir, 'sample_submission.csv'))\n\nOUTPUT_DIR = './'\n\nif not is_inference:\n    !pip install audiomentations\n\n    import audiomentations as aaug","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Dataloader"},{"metadata":{"trusted":true},"cell_type":"code","source":"class MelSpecDataset(torch.utils.data.Dataset):\n    def __init__(\n        self,\n        df=None,\n        num_classes=24,\n        effective_length=sample_rate * PERIOD,\n        waveform_transforms=None,\n        is_train=False,\n        data_dir=None\n    ):\n        self.data_dir = data_dir\n        self.is_train = is_train\n        self.num_classes = num_classes\n        self.df = df\n        self.effective_length = effective_length\n        self.waveform_transforms = waveform_transforms\n        \n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx: int):\n        recording_id = self.df.recording_id.values[idx]\n\n        prefix = 'train' if self.is_train else 'test'\n\n        y, sr = librosa.load(\n            os.path.join(self.data_dir, prefix , recording_id + '.flac'),\n            sr=None, mono=True, res_type=\"kaiser_fast\"\n        )\n\n        len_y = len(y)\n\n        # y = butter_bandpass_filter(y, f_min, f_max, order=9)          \n        t_min = float(self.df.t_min.values[idx]) * sr\n        t_max = float(self.df.t_max.values[idx]) * sr\n        \n        # Positioning sound slice\n        center = np.round((t_min + t_max) / 2)\n        beginning = center - self.effective_length / 2\n        if beginning < 0:\n            beginning = 0\n        \n        ending = beginning + self.effective_length\n        if ending > len_y:\n            ending = len_y\n            beginning = ending - self.effective_length\n\n        y = y[int(beginning):int(ending)]\n\n        # Do the augmentations\n        if self.waveform_transforms:\n            y = self.waveform_transforms(y, sr)\n\n        if self.is_train:\n            target = np.zeros(self.num_classes, dtype=np.single)\n            target[self.df.species_id.values[idx]] = 1\n            return  y, target\n        else:\n            return  y","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_full_df = train_tp_df\n\nfor index, row in train_full_df.iterrows():\n    if fmin > float(row['f_min']):\n        fmin = float(row['f_min'])\n    if fmax < float(row['f_max']):\n        fmax = float(row['f_max'])\n\n# Get some safety margin\nfmin = int(fmin * 0.9)\nfmax = int(fmax * 1.1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Model"},{"metadata":{"trusted":true},"cell_type":"code","source":"args = {\n    'sample_rate': sample_rate,\n    'window_size': window_size,\n    'hop_size': hop_size,\n    'mel_bins': mel_bins,\n    'fmin': fmin, \n    'fmax': fmax,\n    'classes_num': classes_num\n}\nbase_model = PANNsCNN14Att(**args)\nprint(base_model)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Training Utils"},{"metadata":{"trusted":true},"cell_type":"code","source":"def _one_sample_positive_class_precisions(scores, truth):\n    num_classes = scores.shape[0]\n    pos_class_indices = np.flatnonzero(truth > 0)\n\n    if not len(pos_class_indices):\n        return pos_class_indices, np.zeros(0)\n\n    retrieved_classes = np.argsort(scores)[::-1]\n\n    class_rankings = np.zeros(num_classes, dtype=np.int)\n    class_rankings[retrieved_classes] = range(num_classes)\n\n    retrieved_class_true = np.zeros(num_classes, dtype=np.bool)\n    retrieved_class_true[class_rankings[pos_class_indices]] = True\n\n    retrieved_cumulative_hits = np.cumsum(retrieved_class_true)\n\n    precision_at_hits = retrieved_cumulative_hits[class_rankings[pos_class_indices]] / (\n        1 + class_rankings[pos_class_indices].astype(np.float)\n    )\n    return pos_class_indices, precision_at_hits\n\n\ndef lwlrap(truth, scores):\n    assert truth.shape == scores.shape\n    num_samples, num_classes = scores.shape\n    precisions_for_samples_by_classes = np.zeros((num_samples, num_classes))\n    for sample_num in range(num_samples):\n        pos_class_indices, precision_at_hits = _one_sample_positive_class_precisions(\n            scores[sample_num, :], truth[sample_num, :]\n        )\n        precisions_for_samples_by_classes[\n            sample_num, pos_class_indices\n        ] = precision_at_hits\n\n    labels_per_class = np.sum(truth > 0, axis=0)\n    weight_per_class = labels_per_class / float(np.sum(labels_per_class))\n\n    per_class_lwlrap = np.sum(precisions_for_samples_by_classes, axis=0) / np.maximum(\n        1, labels_per_class\n    )\n    return per_class_lwlrap, weight_per_class\n\nclass ImprovedPANNsLoss(nn.Module):\n    def __init__(self, output_key=\"logit\", weights=[1, 0.5]):\n        super().__init__()\n\n        self.output_key = output_key\n        if output_key == \"logit\":\n            self.normal_loss = nn.BCEWithLogitsLoss()\n        else:\n            self.normal_loss = nn.BCELoss()\n\n        self.bce = nn.BCELoss()\n        self.weights = weights\n\n    def forward(self, input, target):\n        input_ = input[self.output_key]\n        target = target.float()\n\n        framewise_output = input[\"framewise_output\"]\n        clipwise_output_with_max, _ = framewise_output.max(dim=1)\n\n        normal_loss = self.normal_loss(input_, target)\n        auxiliary_loss = self.bce(clipwise_output_with_max, target)\n\n        return self.weights[0] * normal_loss + self.weights[1] * auxiliary_loss","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class LightModel(LightningModule):\n    def __init__(self, model, df, num_fold=0, data_dir=data_dir,\n                 batch_size=6, num_workers=4, DEVICE=\"cuda:0\"):\n        super().__init__()\n        self.model = model\n        self.use_fold = num_fold\n        self.data_dir = data_dir\n        self.batch_size = batch_size\n        self.num_workers = num_workers\n        self.DEVICE = DEVICE\n        self.loss_fn = ImprovedPANNsLoss()\n        self.train_df, self.val_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['species_id'])\n\n    def forward(self, batch):\n        return self.model(batch)\n    \n    def train_dataloader(self):\n        train_aug = aaug.Compose([\n            # aaug.AddGaussianNoise(min_amplitude=0.001, max_amplitude=0.015, p=0.3),\n            # aaug.AddGaussianSNR(p=0.3),\n            # aaug.PitchShift(min_semitones=-4, max_semitones=4, p=0.3),\n       ])\n\n        return DataLoader(\n            MelSpecDataset(\n                data_dir=self.data_dir,\n                df=self.train_df, is_train=True,\n                waveform_transforms=train_aug\n            ),\n            batch_size=self.batch_size,\n            num_workers=self.num_workers,\n            shuffle=True,\n            pin_memory=True,\n            drop_last=True\n        )\n\n    def val_dataloader(self):\n        return DataLoader(\n            MelSpecDataset(\n                data_dir=self.data_dir,\n                df=self.val_df, is_train=True\n            ), \n            batch_size=self.batch_size,\n            num_workers=self.num_workers,\n            shuffle=False,\n            pin_memory=True,\n            drop_last=False\n        )\n\n    def training_step(self, batch, batch_idx):\n        y, target = [x.to(self.DEVICE) for x in batch]\n        output = model(y)\n        bceLoss = self.loss_fn(output, target)\n        loss = bceLoss\n        return loss\n\n    def validation_step(self, batch, batch_idx):\n        y, target = [x.to(self.DEVICE) for x in batch]\n        output = model(y)\n        bceLoss = self.loss_fn(output, target)\n        loss = bceLoss\n        self.log('val_loss', loss, on_epoch=True, prog_bar=True)\n        p = torch.sigmoid(output['clipwise_output'])\n        score_class, weight = lwlrap(target.cpu().numpy(), p.cpu().numpy())\n        score = (score_class * weight).sum()\n        self.log(\"Lwlrap_epoch\", score, on_epoch=True, prog_bar=True)\n\n    def configure_optimizers(self):\n        optimizer = torch.optim.Adam(model.parameters(), lr=0.01)\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)\n        lr_scheduler = {\"scheduler\": scheduler }\n        return [optimizer], [lr_scheduler]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Training"},{"metadata":{"trusted":true},"cell_type":"code","source":"if not is_inference:\n    model = LightModel(base_model, train_full_df)\n\n    checkpoint_callback = ModelCheckpoint(\n        filepath=OUTPUT_DIR + '{epoch:02d}-{val_loss:.2f}-{Lwlrap_epoch:.3f}',\n        save_top_k=3,\n        verbose=1,\n        monitor='Lwlrap_epoch',\n        mode='max',\n        prefix='Cnn14_DecisionLevelAtt_',\n    )\n\n    logger = TensorBoardLogger(\n        save_dir=OUTPUT_DIR,\n        name='lightning_logs'\n    )\n    \n    trainer = Trainer(\n        max_epochs=EPOCH,\n        gradient_clip_val=1,\n        logger=logger,\n        checkpoint_callback=checkpoint_callback,\n        limit_val_batches=0.2,\n        gpus=int(torch.cuda.is_available())\n    )\n\n    trainer.fit(model)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Inference"},{"metadata":{"trusted":true},"cell_type":"code","source":"def read_audio_test(recording_id):\n    y, sr = librosa.load(\n        os.path.join(data_dir, 'test' , recording_id + '.flac'),\n        sr=None, mono=True, res_type=\"kaiser_fast\"\n    )\n\n    effective_length = sample_rate*PERIOD\n    # Split for enough segments to not miss anything\n    segments = len(y) / effective_length\n    segments = int(np.ceil(segments))\n    \n    features = []\n\n    for i in range(0, segments):\n        # Last segment going from the end\n        if (i + 1) * effective_length > len(y):\n            y = y[len(y) - effective_length:len(y)]\n        else:\n            y = y[i * effective_length:(i + 1) * effective_length]\n\n        features.append(y)\n\n    return torch.tensor(features)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"if is_inference:\n    # load model\n    paths = [\n        os.path.join('../input/dddddd/Cnn14_DecisionLevelAtt_-epoch65-val_loss0.14-LwAP_epoch0.797.ckpt')\n    ]\n    weights = np.array([1.0])\n    weights /= weights.sum()\n    nets = []\n\n    for path in paths:\n        net = LightModel(base_model, train_full_df).to(DEVICE)\n        net.load_state_dict(torch.load(path, map_location=DEVICE)[\"state_dict\"])\n        net.eval()\n        nets.append(net)\n    assert len(weights) == len(paths)\n\n    # load test data\n    test_file_list = data_test[\"recording_id\"].to_list()\n\n    # predict\n    preds = []\n    nets[0].eval()\n    with torch.no_grad():\n        for recording_id in tqdm(test_file_list):\n            inputs = read_audio_test(recording_id) \n            inputs = inputs.to(DEVICE)\n            o = []\n            for net in nets:\n                o.append(torch.max(net(inputs)[\"logit\"], dim=0).values.detach().cpu().numpy())\n            o = np.stack(o)\n            o = np.sum(o*weights[:, None, None], axis=0)\n            preds.append(o)\n    preds = np.vstack(preds)\n\n    # to output csv\n    sub = pd.DataFrame(preds, columns=[f\"s{i}\" for i in range(24)])\n    sub[\"recording_id\"] = data_test[\"recording_id\"].values[:len(sub)]\n    sub = sub[[\"recording_id\"] + [f\"s{i}\" for i in range(24)]]\n\n    sub.to_csv(OUTPUT_DIR + \"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}