{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"from skimage.transform import resize\nfrom skimage.util import random_noise\nfrom skimage.filters import gaussian\nfrom skimage import exposure\nimport cv2\nimport numpy as np\nimport random\n\ndef addNoisy(img):\n    noise_img = random_noise(img)\n    return addChannels(noise_img)\n\ndef contrast_stretching(img):\n    p2, p98 = np.percentile(img, (2, 98))\n    contrast_img = exposure.rescale_intensity(img, in_range=(p2, p98))\n    return addChannels(contrast_img)\n\ndef log_correction(img):\n    log_img = exposure.adjust_log(img)\n    return addChannels(log_img)\n\ndef randomGaussian(img):\n    gaussian_img = gaussian(img, sigma=random.randint(0, 5))\n    return addChannels(gaussian_img)\n\ndef addChannels(img):\n    return np.stack((img, img, img))\n\ndef spec_to_image(spec):    \n    spec = resize(spec, (224, 400))\n    eps=1e-6\n    mean = spec.mean()\n    std = spec.std()\n    spec_norm = (spec - mean) / (std + eps)\n    spec_min, spec_max = spec_norm.min(), spec_norm.max()\n    spec_scaled = 255 * (spec_norm - spec_min) / (spec_max - spec_min)\n    spec_scaled = spec_scaled.astype(np.uint8)\n    spec_scaled = np.asarray(spec_scaled)\n    return spec_scaled","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import librosa\nfrom torch.utils.data import Dataset, DataLoader\n\nclass AudioData(Dataset):\n    def __init__(self, _data, data_type):\n        self.data = []\n        self.labels = []\n        for i in range(0, len(_data)):\n            # All sound files are 48000 bitrate, no need to slowly resample\n            wav, sr = librosa.load('/kaggle/input/rfcx-species-audio-detection/train/' + _data[i][0] + '.flac', sr=None)\n\n            t_min = float(_data[i][3]) * sr\n            t_max = float(_data[i][5]) * sr\n\n            # Positioning sound slice\n            center = np.round((t_min + t_max) / 2)\n            beginning = center - length / 2\n            if beginning < 0:\n                beginning = 0\n\n            ending = beginning + length\n            if ending > len(wav):\n                ending = len(wav)\n                beginning = ending - length\n\n            slice = wav[int(beginning):int(ending)]\n            \n            spec=librosa.feature.melspectrogram(slice, sr=sr,n_fft=fft,hop_length=hop,fmin=fmin,fmax=fmax)\n            spec_db=librosa.power_to_db(spec,top_db=80)\n            \n            img = spec_to_image(spec_db)\n            mel_spec = np.stack((img, img, img))\n            self.data.append(mel_spec)\n            label = int(_data[i][1])\n            self.labels.append(label)\n            \n            if data_type == \"train\":\n                augmentation_functions = [\n                    addNoisy, contrast_stretching,\n                    randomGaussian, log_correction\n                ]\n                for fun in augmentation_functions:\n                    mel_spec = fun(img)\n                    self.data.append(mel_spec)\n                    self.labels.append(label)\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        return self.data[idx], self.labels[idx]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Generating Mel spectrograms for training from true positive data"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import csv\nimport random\n\nfft = 2048\nhop = 512\n# Less rounding errors this way\nsr = 48000\nlength = 10 * sr\n\nwith open('/kaggle/input/rfcx-species-audio-detection/train_tp.csv') as f:\n    reader = csv.reader(f)\n    next(reader, None)\n    data = list(reader)\n\n# Check minimum/maximum frequencies for bird calls\n# Not neccesary, but there are usually plenty of noise in low frequencies, and removing it helps\nfmin = 24000\nfmax = 0\n\n# Skip header row (recording_id,species_id,songtype_id,t_min,f_min,t_max,f_max) and start from 1 instead of 0\nfor i in range(0, len(data)):\n    if fmin > float(data[i][4]):\n        fmin = float(data[i][4])\n    if fmax < float(data[i][6]):\n        fmax = float(data[i][6])\n# Get some safety margin\nfmin = int(fmin * 0.9)\nfmax = int(fmax * 1.1)\nprint('Minimum frequency: ' + str(fmin) + ', maximum frequency: ' + str(fmax))\n\npercentage_train = 90\nrandom.shuffle(data)\ntotal = len(data)\ntrain_data_amount = round(total / 100 * percentage_train)\ntrain_audio = data[:train_data_amount]\nval_audio = data[train_data_amount:]\ntrain_data = AudioData(train_audio, \"train\")\nvalid_data = AudioData(val_audio, \"valid\")\ntrain_loader = DataLoader(train_data, batch_size=16, shuffle=True)\nvalid_loader = DataLoader(valid_data, batch_size=16, shuffle=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"print(\"train\", len(train_data))\nprint(\"valid\", len(valid_data))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from torchvision.models import resnet50\nimport torch\nimport torch.nn as nn\nfrom torch.optim import lr_scheduler\nimport copy","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"num_birds = 24\n\nif torch.cuda.is_available():\n    device=torch.device('cuda:0')\nelse:\n    device=torch.device('cpu')\n\nresnet_model = resnet50(pretrained=True)\nnum_ftrs = resnet_model.fc.in_features\nresnet_model.fc = nn.Linear(num_ftrs, num_birds)\nresnet_model = resnet_model.to(device)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from tqdm import tqdm\n\nlearning_rate = 2e-4\noptimizer = torch.optim.Adam(resnet_model.parameters(), lr=learning_rate)\nepochs = 20\nloss_fn = nn.CrossEntropyLoss()\n\ndef setlr(optimizer, lr):\n    for param_group in optimizer.param_groups:\n        param_group['lr'] = lr\n    return optimizer\n\ndef lr_decay(optimizer, epoch):\n    if epoch%10==0:\n        new_lr = learning_rate / (10**(epoch//10))\n        optimizer = setlr(optimizer, new_lr)\n        print(f'Changed learning rate to {new_lr}')\n    return optimizer\n\ndef train(model, loss_fn, train_loader, valid_loader, epochs, optimizer, change_lr=None):\n    best_model_wts = copy.deepcopy(model.state_dict())\n    best_acc = 0.0\n    train_losses = []\n    valid_losses = []\n    \n    for epoch in tqdm(range(1,epochs+1)):\n        model.train()\n        batch_losses=[]\n        if change_lr:\n            optimizer = change_lr(optimizer, epoch)\n        for i, data in enumerate(train_loader):\n            x, y = data\n            optimizer.zero_grad()\n            x = x.to(device, dtype=torch.float32)\n            y = y.to(device, dtype=torch.long)\n            y_hat = model(x)\n            loss = loss_fn(y_hat, y)\n            loss.backward()\n            batch_losses.append(loss.item())\n            optimizer.step()\n            \n        train_losses.append(batch_losses)\n        print(f'Epoch - {epoch} Train-Loss : {np.mean(train_losses[-1])}')\n        model.eval()\n        batch_losses=[]\n        trace_y = []\n        trace_yhat = []\n        \n        for i, data in enumerate(valid_loader):\n            x, y = data\n            x = x.to(device, dtype=torch.float32)\n            y = y.to(device, dtype=torch.long)\n            y_hat = model(x)\n            loss = loss_fn(y_hat, y)\n            trace_y.append(y.cpu().detach().numpy())\n            trace_yhat.append(y_hat.cpu().detach().numpy())      \n            batch_losses.append(loss.item())\n        valid_losses.append(batch_losses)\n        trace_y = np.concatenate(trace_y)\n        trace_yhat = np.concatenate(trace_yhat)\n        accuracy = np.mean(trace_yhat.argmax(axis=1)==trace_y)\n        print(f'Epoch - {epoch} Valid-Loss : {np.mean(valid_losses[-1])} Valid-Accuracy : {accuracy}')\n        # deep copy the model\n        if accuracy > best_acc:\n            best_acc = accuracy\n            best_model_wts = copy.deepcopy(model.state_dict())\n\n    # load best model weights\n    model.load_state_dict(best_model_wts)\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"resnet_model = train(resnet_model, loss_fn, train_loader, valid_loader, epochs, optimizer, lr_decay)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Already defined above; for reference\n\n# fft = 2048\n# hop = 512\n# sr = 48000\n# length = 10 * sr\n\ndef load_test_file(f):\n    wav, sr = librosa.load('/kaggle/input/rfcx-species-audio-detection/test/' + f, sr=None)\n\n    # Split for enough segments to not miss anything\n    segments = len(wav) / length\n    segments = int(np.ceil(segments))\n    \n    mel_array = []\n    \n    for i in range(0, segments):\n        # Last segment going from the end\n        if (i + 1) * length > len(wav):\n            slice = wav[len(wav) - length:len(wav)]\n        else:\n            slice = wav[i * length:(i + 1) * length]\n        \n        # Same mel spectrogram as before\n        spec=librosa.feature.melspectrogram(slice, sr=sr,n_fft=fft,hop_length=hop,fmin=fmin,fmax=fmax)\n        spec_db=librosa.power_to_db(spec,top_db=80)\n\n        img = spec_to_image(spec_db)\n        mel_spec = np.stack((img, img, img))\n        mel_array.append(mel_spec)\n    \n    return mel_array","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Submitting predictions with best model"},{"metadata":{"trusted":true},"cell_type":"code","source":"import os\n    \n# Prediction loop\nprint('Starting prediction loop')\nwith open('submission.csv', 'w', newline='') as csvfile:\n    submission_writer = csv.writer(csvfile, delimiter=',')\n    submission_writer.writerow(['recording_id','s0','s1','s2','s3','s4','s5','s6','s7','s8','s9','s10','s11',\n                               's12','s13','s14','s15','s16','s17','s18','s19','s20','s21','s22','s23'])\n    \n    test_files = os.listdir('/kaggle/input/rfcx-species-audio-detection/test/')\n    print(len(test_files))\n    \n    # Every test file is split on several chunks and prediction is made for each chunk\n    for i in range(0, len(test_files)):\n        data = load_test_file(test_files[i])\n        data = torch.tensor(data)\n        data = data.float()\n        if torch.cuda.is_available():\n            data = data.cuda()\n\n        output = resnet_model(data)\n\n        # Taking max prediction from all slices per bird species\n        # Usually you want Sigmoid layer here to convert output to probabilities\n        # In this competition only relative ranking matters, and not the exact value of prediction, so we can use it directly\n        maxed_output = torch.max(output, dim=0)[0]\n        maxed_output = maxed_output.cpu().detach()\n        \n        file_id = str.split(test_files[i], '.')[0]\n        write_array = [file_id]\n        \n        for out in maxed_output:\n            write_array.append(out.item())\n\n        submission_writer.writerow(write_array)\n        if i % 100 == 0 and i > 0:\n            print('Predicted for ' + str(i) + ' of ' + str(len(test_files) + 1) + ' files')\n\nprint('Submission generated')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### References\nhttps://www.kaggle.com/fffrrt/all-in-one-rfcx-baseline-for-beginners\n\nhttps://www.kaggle.com/tomahim/image-manipulation-augmentation-with-skimage\n\nhttps://www.kaggle.com/safavieh/image-augmentation-using-skimage\n\nhttps://medium.com/@hasithsura/audio-classification-d37a82d6715"}],"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}