За последние несколько столетий цивилизация быстро прогрессировала, но за счет окружающей среды. Незаконная вырубка лесов, а также охота достигли рекордно высокого уровня из-за нынешней ситуации и человеческой жадности. Можем ли мы приложить скромные усилия для спасения наших лесов с помощью глубокого обучения? Как насчет того, чтобы попытаться определить, соответствуют ли какие-либо звуки звуку записи или стрельбы?

Давайте посмотрим, как мы можем реализовать это с помощью PyTorch простым для понимания способом.

›Эта статья во многом вдохновлена Блогом 1 и Блогом 2 .

Цели

Прежде чем мы углубимся в какой-либо код, давайте попробуем более четко определить нашу проблему.

Мы пытаемся идентифицировать звуки вырубаемых деревьев, а также звуки стрельбы в лесу. Не будет рассматриваться аппаратная реализация, а будет рассмотрена только ее нейронная сеть.

Поток

Сначала нам нужно получить набор данных со звуками, которые нам нужны. Совершенно очевидно, что это потребуется для обучения нашей модели. Чтобы получить необходимые звуки, мы можем легко выполнить поиск в Интернете и найти около 50 каждого из наших классов. Я не буду предоставлять ссылку на набор данных, потому что им очень легко управлять, и в какой-то момент я могу загрузить его в Google. Как только у нас будет набор данных, мы попытаемся предварительно обработать его и подогнать под наш формат обучения. Затем мы создадим загрузчик данных, с помощью которого мы сможем передавать наши пакеты данных в модель. Нам также необходимо определить функцию потерь и оптимизатор, который подойдет для нашего случая. Наконец, нам нужно будет обучить нашу модель, а также иметь функцию, которая позволит нам проверить, насколько хорошо работает наша модель. В качестве дальнейшего объяснения темы в конце будут обсуждены некоторые советы, а также способы сделать лучшую модель.

(Примечание: - Весь код довольно длинный, поэтому в этой статье были рассмотрены только основные моменты. Все было объяснено, но размещение всего кода здесь было бы пустой тратой места, поэтому вы можете найти его в этом репозитории. )

Загрузчик данных

Буду считать, что у нас необходимые данные присутствуют. В этом уроке я буду следовать шаблону данных набора звуковых данных 8K. это означает, что данные делятся непосредственно на свертки и есть файлы с определенными метками. Перейдите по ссылке, чтобы отформатировать данные таким же образом, или используйте это руководство, чтобы изменить его в соответствии с вашим типом шаблона данных.

Давайте сначала импортируем все, что нам нужно.


import torch

import torch.nn as nn

import torch.nn.functional as F

import torch.optim as optim

from torchvision import datasets, transforms

from torch.optim.lr_scheduler import StepLR

from tqdm import tqdm

import librosa

import numpy as np

from torch.utils.data import TensorDataset, DataLoader, random_split

import pandas as pd

import os

Важно отметить, что мы намерены преобразовать наши данные с помощью так называемого Mel Frequency Cepstrum или MFCC. это метод обработки звука, в котором кратковременное представление мощности волны передается через преобразование, которое упрощает масштабирование и обработку. Вы можете думать об этом как о предоставлении возможности представления огромных вариаций данных, чтобы приблизиться к реакции слуховой системы человека. По сути, это приближает звук к тому, что мы слышим и понимаем. Мы используем библиотеку под названием librosa (выполните команду pip install librosa, чтобы получить ее), чтобы выполнить преобразование за нас.

def extract_mfcc(path):

    audio, sr = librosa.load(path)

    mfccs = librosa.feature.mfcc(audio, sr, n_mfcc=40)

    return np.mean(mfccs.T, axis=0)

Теперь, когда у нас есть функция, определяющая это преобразование, мы создаем набор данных. Мы перебираем данные по сгибам и для каждой сгиба выполняем MFCC. Сначала мы сохраняем все это в массивах, которые затем конвертируем в тензоры. Поскольку у нас может быть много данных, мы сохраним их как файлы .pt, что позволит нам напрямую загружать тензоры в следующий раз, когда мы захотим запустить скрипт, без необходимости обрабатывать его снова.

# Making a custom dataset

features = []

labels = []

folds = []

print("[INFO] Making dataset")

for i in range(len(df)):

     fold = df["fold"].iloc[i]

     filename = df["slice_file_name"].iloc[i]

     path = "/home/eragon/Documents/datasets/Def/audio/fold{0}/{1}".format(

         fold, filename)

     mfccs = extract_mfcc(path)

     features.append(mfccs)

     folds.append(fold)
     labels.append(df["classID"].iloc[i])
features = torch.tensor(features)
labels = torch.tensor(labels)
folds = torch.tensor(folds)

print("[INFO] Done making dataset")

# Save to disk

torch.save(

features, "/home/eragon/Documents/datasets/Def/features_mfccs.pt")

torch.save(labels, "/home/eragon/Documents/datasets/Def/labels.pt")

torch.save(folds, "/home/eragon/Documents/datasets/Def/folds.pt")

# For the next time 

#features = torch.load("/home/eragon/Documents/datasets/Def/features_mfccs.pt")

#labels = torch.load("/home/eragon/Documents/datasets/Def/labels.pt")

#folds = torch.load("/home/eragon/Documents/datasets/Def/folds.pt")

Теперь, когда у нас есть все это, мы создаем набор тензорных данных, используя упомянутые выше складки. Это очень характерно для формата набора данных 8K, поэтому это делается именно так. Если вы не следуете этому, могут быть более простые способы сделать это. Цель состоит в том, чтобы иметь способ перебора аудиоданных, хранящихся в массивах. После этого мы разделили наш набор данных на обучающий и проверочный.



def get_dataset(skip_fold):

    local_features = []

    local_labels = []

    for i in range(len(folds)):

        if folds[i] == skip_fold:

            continue

        local_features.append(features[i])

        local_labels.append(labels[i])

    local_features = torch.stack(local_features)

    local_labels = torch.stack(local_labels)

    return TensorDataset(local_features, local_labels)

# Loading dataset
dataset = get_dataset(skip_fold=10)

print("Length of dataset: ", len(dataset))

val_size = int(0.1*len(dataset))

train_size = len(dataset) - val_size

train_data, test_data = random_split(dataset, [train_size, val_size])

train_loader = torch.utils.data.DataLoader(train_data, **kwargs)

test_loader = torch.utils.data.DataLoader(test_data, **kwargs)

Модель

В качестве доказательства концепции мы используем простую модель нейронной сети. Сама по себе сеть может быть изменена для получения лучших результатов. Мы обсудим некоторые способы сделать это.

class Net(nn.Module):

    def __init__(self):

        super(Net, self).__init__()

        self.network = nn.Sequential(

            nn.Linear(40, 128),

            nn.ReLU(),

            nn.Linear(128,256),

            nn.ReLU(),

            nn.Linear(256, 512),

            nn.ReLU(),

            nn.Linear(512, 64),

            nn.ReLU(),
            nn.Linear(64, 10),
            nn.Tanh()

        )

    def forward(self, x):
        return self.network(x)

Наша первоначальная модель состоит как из линейных, так и из выпрямленных линейных единичных слоев / функций активации. Обратите внимание, что исходный входной канал необходимо изменить в соответствии с вашими данными. В случае с моим примером у меня было 40 в качестве входных данных, поэтому вы видите это в коде таким образом. Теперь следует отметить, что окончательная активация не является выпрямленным линейным блоком, а на самом деле является тангенсом. Это просто потому, что согласно экспериментам он работал лучше.

Как мы можем сделать эту модель лучше? Ну, во-первых, мы могли бы также рассмотреть возможность использования сверток. Мы также можем попытаться использовать функцию Swish Activation вместо ReLU. Мы можем попробовать поэкспериментировать с более глубокими сетями, а также с немного другими архитектурами. По сути, это очень простая модель, но, похоже, работает довольно хорошо.

Обучение

Собрав все необходимое, давайте попробуем написать код для обучения нашей модели с использованием данных. Нам нужно выбрать оптимизатор, и в нашем случае мы должны выбрать стохастический градиентный спуск вместо Адама. Каким-то образом это сработало лучше. Что касается нашей функции потерь, мы используем кросс-энтропийную потерю, поскольку у нас есть несколько классов данных. Мы следуем стандартному синтаксису. Сначала мы берем модель и настраиваем ее на обучаемость, что позволит нам сохранять градиенты и выполнять обратный проход в нейронной сети. Мы отправляем нашу модель в графический процессор и перебираем данные партиями.

Для каждого пакета мы отправляем данные, а также соответствующую метку в графический процессор. Мы также обнуляем градиенты. это позволяет нам не накапливать градиенты из предыдущего пакета, что потенциально может испортить наши результаты. После этого мы пропускаем нашу партию через модель и, используя выходные данные, можем рассчитать наши потери. Поскольку цель состоит в том, чтобы минимизировать эту потерю, мы выполняем обратный проход, а также обновляем наш оптимизатор. Поскольку обучение может занять много времени, мы хотели бы знать, в каком состоянии мы тренировались между ними. Поэтому мы также пишем крошечный фрагмент, чтобы распечатать показатели по мере того, как и когда продолжается обучение.

def train(args, model, device, train_loader, optimizer, epoch):

    model.train() # Setting model to train

    device = torch.device("cuda") # Sending to GPU

    for batch_idx, (data, target) in tqdm(enumerate(train_loader)):

        data, target = data.to(device), target.to(device)

        optimizer.zero_grad() #Reset grads

        output = model(data) # Passing batch through model

        loss = F.cross_entropy(output, target) # Will need to change everytime. Loss

        loss.backward() # Backprop

        optimizer.step() # Pass through optimizer

        if batch_idx % args.log_interval == 0:

            #print("Loss: ", loss.item())

            if args.dry_run:

                break

Был использован еще один небольшой трюк, чтобы добиться лучших результатов. Это тренировка за один цикл. Это было первоначально упомянуто в этой статье и в основном позволяет нам создать лучшую модель, сначала увеличивая скорость обучения, а затем уменьшая ее в течение определенного периода эпох. Поэтому мы обновляем и это.

optimizer = optim.SGD(model.parameters(), lr=args.lr,

                       weight_decay=args.weight_decay)

scheduler = optim.lr_scheduler.OneCycleLR(
    optimizer, max_lr=args.max_lr, steps_per_epoch=len(train_loader), epochs=10)

Тестирование

Одного обучения модели, конечно, недостаточно, и нам нужно определить, насколько хорошо работает наша модель. Мы следуем почти той же процедуре, что и на тренировке, с небольшими отличиями. Во-первых, мы установили модель в режим оценки вместо режима обучения. Мы также определяем переменную тестовых потерь, которая поможет нам накапливать убытки по партии.

def test(model, device, test_loader):

    model.eval()  # Setting model to test

    test_loss = 0
    with torch.no_grad():

        for data, target in tqdm(test_loader):

            data, target = data.to(device), target.to(device)

            output = model(data)

            test_loss = F.cross_entropy(output, target)

            acc = accuracy(output, target)

            print(f"Acc: {acc}")

            print(f"Val loss: {test_loss.detach()}, Val acc : {acc}")

Нам также необходимо определить функцию точности. В нашем случае мы сначала берем выходные данные и находим их максимум по первому измерению. Затем мы суммируем все значения, в которых предсказанные метки совпадают с требуемыми метками, и возвращаем тензор этого значения, деленный на общее количество предсказаний. Это в основном даст нам десятичное значение от 0 до 1, которое поможет нам определить, насколько хорошо работает наша модель.

def accuracy(outputs, labels):

    _, preds = torch.max(outputs, dim=1)

    return torch.tensor(torch.sum(preds == labels).item() / len(preds))

После этого мы можем сохранить нашу модель для будущих выводов. Таким образом мы можем идентифицировать звуки, которые нам нужны.

Реализация

Одна вещь, которую я не затронул в этой статье, - это физическая реализация этого. Ссылка, которую я дал в начале статьи, рассказывает о том, как организация использовала нечто подобное и фактически реализовала это в лесах. Не считая аппаратной части, есть две вещи, которые мы не рассмотрели подробно в отношении нашего программного обеспечения.

Во-первых, наша модель поезда может работать только с сигналами фиксированной длины, но в лесу есть непрерывный звук, так как же нам с этим работать? Проще говоря, мы берем определенную единицу времени, скажем несколько секунд, и каждые несколько секунд выполняем идентификацию. Затем мы усредняем результаты за период времени, вероятно, несколько минут, а затем мы можем использовать результаты этого, чтобы определить, действительно ли происходит требуемый нам звук.

Второй - развертывание. Поскольку для выполнения этой идентификации нам потребуется физическое устройство, мы можем использовать такое устройство, как Raspberry Pi с Wi-Fi, и развернуть нашу модель на устройстве. Конечно, мы не можем просто оставить ноутбук в лесу, так что это было бы более эффективным решением.

Конечно, есть разные способы сделать одно и то же, но это выходит за рамки данной статьи. Если вы хотите узнать больше, вы можете обратиться к этой статье. и этот."

Почти конец

То, что мы рассмотрели сегодня, представляет собой относительно простой способ сделать это. Существуют более сложные и, вероятно, более точные методы, хотя для большинства случаев использования этого метода достаточно, любые изменения в нем будут в основном частью изменения архитектуры, а не большей частью кода, представленного здесь. Таким образом, можно было бы масштабировать это или модифицировать только необходимые детали, чтобы сделать модель лучше.

Заключительные мысли

Перед тем, как закончить эту статью, я хотел бы добавить несколько вещей.

Во-первых, эффективность. Эти модели имеют много связей между нейронами. Многие из них могут и не понадобиться. Можно было бы сделать любую модель, особенно эту, немного более эффективной, удалив ненужные связи. Я подробно описал это в другой статье, на которую вы можете сослаться, если вам интересна эта тема.

Вторая и последняя мысль, которую я хотел добавить, - это наша ответственность. Это наш мир, и каждая мелочь, которую мы делаем, влияет на него. Как в наших интересах, так и в интересах всех остальных думать о наших действиях по отношению к окружающей среде. Можно попытаться исправить уже сломанную ситуацию, но не лучше ли вообще не иметь этой ситуации? В следующий раз, когда у вас будет возможность, подумайте, как наши повседневные поступки влияют на природу.

Маленькие вещи меняют большие. Все, что мы можем сделать - это крошечные рябь. Однажды они могут помочь нам жить в другом возрасте.

Спасибо :)
Любые комментарии приветствуются. Вы также можете связаться со мной на Github.

Получите доступ к экспертному обзору - Подпишитесь на DDI Intel