О чем поют птицы? Распознавание птичьих звуков.

Слышали ли вы, о чем говорят птицы? Серьезно

Posted by yara_tchk on July 23, 2017
Оригинал статьи на английском
Список статей

О чем поют птицы? Распознавание птичьих звуков. Часть 1 - Начало

О чем поют птицы? Распознавание птичьих звуков. Часть 2 - Таксономия

О чем поют птицы? Распознавание птичьих звуков. Часть 3 - Послушайте

О чем поют птицы? Распознавание птичьих звуков. Часть 4 - Выбор датасета, загрузка и предобработка данных, визуализация и анализ

О чем поют птицы? Распознавание птичьих звуков. Часть 5 - Подготовка данных

Как обычно, привлеку ваше внимание с помощью красивой картинки, не объяснив, что это такое. Используйте свое воображение, чтобы угадать!


0. О чем вообще эта статья? Птицы? Звуки? Нейронные сети? Наука о данных?

Обо всем, просто нужно немного терпения. Расскажу, с чего началось

Три-четыре года назад я нашел это. Это гигантский борд с птичьими звуками, которые визуализируются как спектрограммы «с использованием машинного обучения». Это идеальный пример проекта Google - красивый, прилично выглядящий, дорогой и абсолютно бесполезный и не имеющий никакой связи с реальностью. В тот момент меня впечатлил этот проект, потому что я ничего не знал о Data Science и машинном обучении и действительно думал, что для создания таких визуализаций вам действительно нужна какая-то «черная магия».

Спустя несколько лет, запустив этот сайт и наш телеграм канал и наш проект  нейрокурятник, я смотрел на эту страницу уже более скептически.Примерно в то же время я наткнулся на это видео на Youtube (есть буквально десятки видеороликов, связанных с различными таксономическими единицами).


Я с упоением посмотрел несколько десятков этих видеороликов. Затем я вспомнил эту сцену из знаменитой телесериала «Силиконовая долина». 


А потом я еще вспомнил, как читал следующие статьи / новости / сообщения в блогах:

  • Как  при помощи react native, tensorflow и squeeze nets было сделано "not hotdog" приложение от сериала Silicon Valley
  • Squeeze net в keras, статья;
  • Еще один (sic! связанный с птицами) проект  с использованием raspberry pi и squeeze nets;
  • Моя короткая заметка про Tensorflow для Android от Google (Apple тоже запустили нечто подобное);


Понимаете связи =) ?

Не нужно быть гением, чтобы увидеть следующее:

  1. Расчеты подчиняются циклам. Когда какой-то расчет можно перенести на устройство конечного пользователя (ПК, ноутбуки, смартфоны) с некоторой выгодой, в конечном счете это обязательно произойдет;
  2. Squeeze net позволяет запускать предиктивную часть мощной нейросетки на мобильном устройстве без существенного ограничения, а веса занимают всего 5-10 Мб;
  3. По состоянию на ~ июль 2017 года никто не создал приложение / алгоритм распознавания видов птиц по звуку;


1. План действий

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

Грубо проект можно поделить на следующие составляющие

  1. Найти данные (песни), загрузить, выполнить базовый статистический анализ;
  2. Проанализировать их, создать подвыборку для проверки концепции;
  3. Изучить способы вытягивания переменных из звука;
  4. Проэкспериментировать с обычными нейронными сетями, чтобы убедиться, что этот проект жизнеспособен;
  5. Если проект жизнеспособен, перенастроить нейросеть, чтобы сжать сетевую архитектуру и уменьшить размер матрицы весов;
  6. Создать приложение на react native, которое сможет сообщить вам, что это за птицу, по звуку


Звучит достаточно простою Но подводных камней много, ообенно со сбором данных, выборками и архитектурой нейросетей.

2. Без лишних слов к действиям

Отдав некоторое время исследованиям и поискам, натолкнулся на этот потрясающий вебсайт, содержащй около 350 тыс. записей птичьих песен примерно 10 тыс. видов. Для сравнения в Царстве Животных (таксономический термин) примерно 30 тыс. видов животных (птицы тоже животные).



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


Этот вебсайт предоставляет еще и функциональное ;API. Начнем

Импортируем библиотеки, которые понадобятся вероятнее всего (я ленив).

from __future__ import print_function
import os.path
from collections import defaultdict
import string
import requests
from bs4 import BeautifulSoup
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
from sklearn.feature_extraction.text import CountVectorizer
import wordcloud
%matplotlib inline
import time


Как обычно, используем эту утилиту для отображения прогресса:

# https://github.com/alexanderkuk/log-progress
# Progress indicator utilitty
def log_progress(sequence, every=None, size=None, name='Items'):
    from ipywidgets import IntProgress, HTML, VBox
    from IPython.display import display
    is_iterator = False
    if size is None:
        try:
            size = len(sequence)
        except TypeError:
            is_iterator = True
    if size is not None:
        if every is None:
            if size <= 200:
                every = 1
            else:
                every = int(size / 200)     # every 0.5%
    else:
        assert every is not None, 'sequence is iterator, set every'
    if is_iterator:
        progress = IntProgress(min=0, max=1, value=1)
        progress.bar_style = 'info'
    else:
        progress = IntProgress(min=0, max=size, value=0)
    label = HTML()
    box = VBox(children=[label, progress])
    display(box)
    index = 0
    try:
        for index, record in enumerate(sequence, 1):
            if index == 1 or index % every == 0:
                if is_iterator:
                    label.value = '{name}: {index} / ?'.format(
                        name=name,
                        index=index
                    )
                else:
                    progress.value = index
                    label.value = u'{name}: {index} / {size}'.format(
                        name=name,
                        index=index,
                        size=size
                    )
            yield record
    except:
        progress.bar_style = 'danger'
        raise
    else:
        progress.bar_style = 'success'
        progress.value = index
        label.value = "{name}: {index}".format(
            name=name,
            index=str(index or '?')
        )


Сохраним некоторые важные переменные и напишем некоторые простые вспомогательные функции


api_endpoint = 'http://www.xeno-canto.org/api/2/recordings'
area_list = ['africa', 'america', 'asia', 'australia', 'europe']


def api_query(query=None,area=None,country=None,page=None):
    if((page is None) or (page == 0) or (page == '')):
        page = 1
       
    if ((query is None) or (query == '')):
        if ((area is None) or (area == '')):
            if ((country is None) or (country == '')):
                return None
            else: 
                return api_endpoint+'?query=cnt:'+country+'&page='+str(page)
        else:
            return api_endpoint+'?query=area:'+area+'&page='+str(page)
    else:
        return api_endpoint+'?query='+query+'&page='+str(page)  


Как обычно, опустим скучные преобразования, можно посмотреть весь код в формате ipynb в конце статьи. Сфокусируемся на более важных вещах.

Этот код дает нам представление о том, сколько страниц нам нужно собрать

area_df = pd.DataFrame(columns = ['area','numRecordings','numSpecies','numPages'])
area_df
       
for area in log_progress(area_list):
    try:
        result = requests.get(api_query(area=area))
        temp_dict = {'area':area,
                     'numRecordings': result.json()['numRecordings'],
                     'numSpecies': result.json()['numSpecies'],
                     'numPages': result.json()['numPages']}
        area_df = area_df.append(temp_dict, ignore_index=True)
    except Exception as ex:
        logger.error('Failed to upload to ftp: '+ str(ex))
area_df


Вот что получается. Неплохо, да?


Кусок кода, необходимый для сбора всех данных, совсем маленький! Обратите внимание, что мы используем iter_df на случай, если некоторые из наших запросов не обработаются

area_list = ['africa', 'america', 'asia', 'australia', 'europe']
response_cols = ['cnt',
                   'date',
                   'en',
                   'file',
                   'gen',
                   'id',
                   'lat',
                   'lic',
                   'lng',
                   'loc',
                   'q',
                   'rec',
                   'sp',
                   'ssp',
                   'time',
                   'type',
                   'url']
iter_df = pd.DataFrame(columns=['area','page','processed'])
for index, row in area_df.iterrows():
    iter_df_append = pd.DataFrame(columns=['area','page','processed'])
    iter_df_append.page = np.arange(1,row['numPages']+1,1)
    iter_df_append.processed = 0
    iter_df_append.area = row['area']
    iter_df = iter_df.append(iter_df_append,ignore_index =True)
result_df = pd.DataFrame(columns=[response_cols])
iter_df


from fake_useragent import UserAgentua = UserAgent()headers = ua.chromeheaders = {'User-Agent': headers}response_cols = ['cnt',                   'date',                   'en',                   'file',                   'gen',                   'id',                   'lat',                   'lic',                   'lng',                   'loc',                   'q',                   'rec',                   'sp',                   'ssp',                   'time',                   'type',                   'url',                    'area',                    'page']result_df = pd.DataFrame(columns=[response_cols])idx = np.arange(0,738)for num in log_progress(idx):    try:        query_url = api_query(area=iter_df.iloc[num].area, page=int(iter_df.iloc[num].page))        result = requests.get(query_url, headers=headers)            temp_df = pd.DataFrame(result.json()['recordings'])        temp_df['area'] = iter_df.iloc[num].area        temp_df['page'] = int(iter_df.iloc[num].page)               result_df = result_df.append(temp_df, ignore_index=True)               iter_df.set_value(num, 'processed', 1)        time.sleep(1)                 # Testing break         # if (num==1):        #    break    except Exception as ex:        print('Script failed for num: {}\nError type: {}\n'.format(str(num), str(ex)))

              

result_df.shape выдает нам (367577, 19).

3. Немного базового анализа наших данных

Не буду утомлять чрезмерными деталями (вы найдете их в ipynb), но укажу несколько вещей, которые мы должны понять, прежде чем приступать к нейронным сетям и прочему.

Нужно:

  • Понять данные (кто их собирает, почему, когда, какая форма используется и т.д.) - это вне данного скоупа, но само собой разумеется, я провел исследование;
  • Как в основном распределены переменные
  • Наиболее часто встречающиеся страны, виды, типы песен/криков
  • Можем ли мы обеспечить сбалансированность наших классов (т.е. обойти ситуации, когда у нас 9000 песен для одного вида птиц и 10 для другого в конечном наборе данных)?
  • Послушать птичьи песни (что мы обязательно сделаем).


На самом деле в нетехнических наборах данных лучше начинать с обычных сводных таблиц. Да. Именно так. Добрые старые сводные таблицы (вы можете сделать это в Excel или в python).

Этот кусок кода, к примеру 

table = pd.pivot_table(df,
    index=["cnt"],
    columns=["area"],
    values=["id",],
    aggfunc={"id":pd.Series.nunique},
    margins = True,
    fill_value=0)
pd.set_option('display.max_rows', len(table))
table = table.sort_values(by=('id','All'),ascending=False)
table


выдает вот что


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

Чтобы закончить статью, я просто представлю пару интересных инфо-график:


Сколько криков приходится на птичий вид:


Сколько криков приходится на птичий вид. В размерности Log10:


Легко видеть, что в среднем около 10^1.5, то есть 30-40.

Наиболее часто встречающиеся слова в описании криков (код в ipynb):



И, наконец, птичьи rhbrb по странам (в датасете есть еще широта и долгота, но я поленился рисовать карту - я объясню позже, почему).

Это было сделано при помощи этой штуки.


4. Ну и что?

Разве мы не можем просто скачать все песни, загрузить их в VGG-16, и все на этом?

Не так быстро. Выше было видно, что в среднем у нас есть ~ 30-40 песен птиц на каждого вида, а этого мало. И мы хотим создать классификатор, который фактически можно было бы использовать в реальных условиях, а не в каком-то огороженном саду. Поэтому нам нужны как минимум сотни песен в классе (в сбалансированном наборе данных!), чтобы нейронные сети работали правильно.

Очевидная идея состоит в том, чтобы использовать таксономические данные дерева для прогнозирования не видов птиц, а, например, рода птиц , что может быть легче (это может даже оказаться тривиальным, но мы говорим о стадии концепции сейчас).

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

5. Ресурсы


Как обычно - для корректного просмотра ячеек, я настоятельно рекомендую использовать подключаемый модуль для раскрывающихся ячеек в jupiter notebook.