Визуализируем свою музыкальную коллекцию

В этой статье мы рассмотрим изящный способ визуализации музыкальной MP3-коллекции. Мы создадим гексагональную диаграмму, на которой похожие по звучанию композиции располагаются рядом друг с другом, а цвета различных регионов диаграммы соответствуют различным музыкальным жанрам, таким как классика, хип-хоп, хард-рок и т.д. В качестве примера ниже визуализированы три музыкальных альбома из моей коллекции: «Каприсы для скрипки» (Паганини), «The Eminem Show» (Eminem) и «X&Y» (Coldplay).

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

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

И, наконец, несмотря на то, что цифровая музыка распространяется в различных форматах (MP3, WMA, M4A, OGG и др.), чтобы не усложнять проект, я решил сфокусироваться только на MP3-файлах. Однако разработанный алгоритм способен работать с любым форматом, который может быть преобразован в WAV.

Итак, перед нами интересное упражнение, включающее обработку звука, машинное обучение и визуализацию. Базовый алгоритм является следующим:

  1. Конвертируем MP3-файлы в WAV-файлы с малым битрейтом.
  2. Извлекаем статистические признаки из WAV-файлов.
  3. Находим оптимальное подмножество признаков, стремясь к тому, чтобы композиции, расположенные «близко» друг к другу в данном пространстве признаков, также имели похожее звучание и для человеческого слуха.
  4. Выполняем уменьшение размерности и получаем 2-мерные векторы признаков, каждый из которых определяет точку на плоскости.
  5. Чтобы получить более эффектный внешний вид визуализации, создаем гексагональную сетку точек, и с помощью метода ближайшего соседа каждой точке сетки ставим в соответствие ближайший 2-мерный вектор.
  6. Возвращаемся в исходное пространство признаков большой размерности и кластеризуем треки, формируя заданное количество кластеров (оптимально 10).
  7. На гексагональной сетке окрашиваем в различные цвета 10 треков, каждый из которых является ближайшим к центру своего кластера.
  8. Интерполируем цвета остальных треков на основе их близости к центрам кластеров.

Далее мы рассмотрим алгоритм подробнее.

Конвертируем MP3-файлы в WAV-формат

Первым делом мы конвертируем MP3-файлы в формат WAV. Это преобразование позволит нам, используя модуль wave из стандартной библиотеки Python, с легкостью читать аудиоданные для дальнейшей обработки с помощью NumPy. В процессе преобразования мы понизим частоту дискретизации до 10 кГц и установим режим моно, чтобы для извлечения признаков потребовалось меньше вычислительных ресурсов. Я выполнил конвертирование с помощь популярного свободного консольного аудиоплеера mpg123, который можно легко вызвать из Python. Представленный ниже фрагмент кода находит MP3-файл в каталоге Music и преобразует его во временный WAV-файл (моно, 10 кГц) с помощью mpg123. Затем выполняется код, вычисляющий признаки для данного WAV-файла (этот фрагмент кода мы рассмотрим в следующем разделе). Процесс повторяется для всех файлов в каталоге Music.

import subprocess

import wave

import struct

import numpy

import csv

import sys



def read_wav(wav_file):

    """Returns two chunks of sound data from wave file."""

    w = wave.open(wav_file)

    n = 60 * 10000

    if w.getnframes() < n * 2:

        raise ValueError('Wave file too short')

    frames = w.readframes(n)

    wav_data1 = struct.unpack('%dh' % n, frames)

    frames = w.readframes(n)

    wav_data2 = struct.unpack('%dh' % n, frames)

    return wav_data1, wav_data2



def compute_chunk_features(mp3_file):

    """Return feature vectors for two chunks of an MP3 file."""

    # Extract MP3 file to a mono, 10kHz WAV file

    mpg123_command = '..\\mpg123-1.12.3-x86-64\\mpg123.exe -w "%s" -r 10000 -m "%s"'

    out_file = 'temp.wav'

    cmd = mpg123_command % (out_file, mp3_file)

    temp = subprocess.call(cmd)

    # Read in chunks of data from WAV file

    wav_data1, wav_data2 = read_wav(out_file)

    # We'll cover how the features are computed in the next section!

    return features(wav_data1), features(wav_data2)



# Main script starts here

# =======================



for path, dirs, files in os.walk('C:/Users/Christian/Music/'):

    for f in files:

        if not f.endswith('.mp3'):

            # Skip any non-MP3 files

            continue

        mp3_file = os.path.join(path, f)

        # Extract the track name (i.e. the file name) plus the names

        # of the two preceding directories. This will be useful

        # later for plotting.

        tail, track = os.path.split(mp3_file)

        tail, dir1 = os.path.split(tail)

        tail, dir2 = os.path.split(tail)

        # Compute features. feature_vec1 and feature_vec2 are lists of floating

        # point numbers representing the statistical features we have extracted

        # from the raw sound data.

        try:

            feature_vec1, feature_vec2 = compute_chunk_features(mp3_file)

        except:

            continue

Извлечение признаков

Загруженный в переменную WAV-файл (моно, 10 кГц) представляет собой массив целых чисел в диапазоне от -254 до 255, при этом на каждую секунду трека приходится 10000 таких чисел. Каждое число определяет относительную амплитуду звуковой волны в данный момент времени. Мы возьмем из каждой песни по два фрагмента длительностью 60 секунд каждый. Таким образом, каждый фрагмент будет представлен массивом, содержащим 600000 целых чисел. В приведенном выше коде функция read_wav возвращает эти массивы. Ниже показаны диаграммы 10-секундных фрагментов четырех песен из альбома «The Eminem Show»:

А теперь для сравнения посмотрим на фрагменты четырех каприсов Паганини:

Мы видим, что четыре трека из альбома «The Eminem Show» имеют существенное сходство между собой. То же самое можно сказать и в отношении каприсов. Далее мы извлечем некоторые статистические признаки треков, которые позволят нам определять степень сходства различных музыкальных композиций, а затем применим машинное обучение, чтобы сгруппировать композиции, основываясь на том, насколько похоже их звучание для человеческого слуха.

Первой группой признаков будут статистические моменты сигнала: среднее значение (mean), среднеквадратическое отклонение (standard deviation), коэффициент асимметрии (skewness) и коэффициент эксцесса (kurtosis). Мы вычислим эти признаки как на основе исходных амплитуд, так и на основе сглаженных амплитуд, чтобы извлечь свойства музыки в различных временных масштабах. Я использовал четыре варианта сглаживающего окна с шириной 1, 10, 100 и 1000 отсчетов. Другие параметры окна, вероятно, также могут обеспечить хороший результат.

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

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

В итоге мы получили 42 различных признака для каждого трека. Ниже представлен код на Python для вычисления этих признаков на основе массива амплитуд:

def moments(x):

    mean = x.mean()

    std = x.var()**0.5

    skewness = ((x - mean)**3).mean() / std**3

    kurtosis = ((x - mean)**4).mean() / std**4

    return [mean, std, skewness, kurtosis]



def fftfeatures(wavdata):

    f = numpy.fft.fft(wavdata)

    f = f[2:(f.size / 2 + 1)]

    f = abs(f)

    total_power = f.sum()

    f = numpy.array_split(f, 10)

    return [e.sum() / total_power for e in f]



def features(x):

    x = numpy.array(x)

    f = []



    xs = x

    diff = xs[1:] - xs[:-1]

    f.extend(moments(xs))

    f.extend(moments(diff))



    xs = x.reshape(-1, 10).mean(1)

    diff = xs[1:] - xs[:-1]

    f.extend(moments(xs))

    f.extend(moments(diff))



    xs = x.reshape(-1, 100).mean(1)

    diff = xs[1:] - xs[:-1]

    f.extend(moments(xs))

    f.extend(moments(diff))



    xs = x.reshape(-1, 1000).mean(1)

    diff = xs[1:] - xs[:-1]

    f.extend(moments(xs))

    f.extend(moments(diff))



    f.extend(fftfeatures(x))

    return f



# f will be a list of 42 floating point features with the following

# names:



# amp1mean

# amp1std

# amp1skew

# amp1kurt

# amp1dmean

# amp1dstd

# amp1dskew

# amp1dkurt

# amp10mean

# amp10std

# amp10skew

# amp10kurt

# amp10dmean

# amp10dstd

# amp10dskew

# amp10dkurt

# amp100mean

# amp100std

# amp100skew

# amp100kurt

# amp100dmean

# amp100dstd

# amp100dskew

# amp100dkurt

# amp1000mean

# amp1000std

# amp1000skew

# amp1000kurt

# amp1000dmean

# amp1000dstd

# amp1000dskew

# amp1000dkurt

# power1

# power2

# power3

# power4

# power5

# power6

# power7

# power8

# power9

# power10

Выбираем оптимальное подмножество признаков

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

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

Чтобы протестировать различные комбинации 42 признаков и найти их подмножество, которое позволяет наиболее точно определить соответствие фрагментов в среднем по всем трекам, я применил генетический алгоритм (R-пакет genalg). На рисунке ниже мы наблюдаем уменьшение ошибки классификатора на основе метода ближайшего соседа при сопоставлении фрагментов треков на протяжении 100 поколений генетического алгоритма.

Если бы мы использовали все 42 признака, величина ошибки составила бы 275. Благодаря отбору признаков с помощью генетического алгоритма, мы уменьшили значение ошибки до 90, что является существенным улучшением. Оптимальное подмножество признаков является следующим:

  • amp10mean
  • amp10std
  • amp10skew
  • amp10dstd
  • amp10dskew
  • amp10dkurt
  • amp100mean
  • amp100std
  • amp100dstd
  • amp1000mean
  • power2
  • power3
  • power4
  • power5
  • power6
  • power7
  • power8
  • power9

Визуализируем данные в 2-мерном пространстве

Оптимальное подмножество содержит 18 признаков, но в конечном итоге мы хотим визуализировать нашу музыкальную коллекцию на плоскости. Следовательно, необходимо преобразовать данное 18-мерное пространство в 2-мерное. Я выполнил преобразование с помощью метода главных компонент (principal component analysis). В результате уменьшения размерности появляются определенные неточности визуализации, например, некоторые композиции, расположенные «близко» друг к другу в 18-мерном пространстве, будут не так близки в 2-мерном пространстве. Подобные погрешности неизбежны, но, к счастью, они не вносят существенные искажения во взаимное расположение треков на плоскости.

Визуализируем треки в виде гексагональной сетки

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

  1. Исходными точками нам послужат 2-мерные проекции 18-мерных векторов признаков. Выводим исходные точки на плоскость в пределах значительно большей гексагональной сетки точек.
  2. Начиная с наиболее удаленных точек сетки, перемещаем в каждую точку сетки ближайшую исходную точку.
  3. Таким образом, мы равномерно распределяем исходные точки по плоскости в виде гексагональной сетки и получаем стильную визуализацию.

Добавляем цвет

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

Итоговая диаграмма

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =