[MoodOfTheSong] 5. Pierwsze cechy

Dobra. Było trochę o teorii, trochę o narzędziach, trochę o danych, ale kiedyś trzeba się wziąć na serio do roboty. Zaczynam więc eksperymentowanie z parametryzacją. Dzisiaj opiszę jak przygotowałem system na wczytanie danych, przetworzenie ich ustaloną metodą oraz zaprezentowanie wyników w jakiejś sensownej formie.

Pliki i foldery – refaktoring

Zacząłem od refaktoringu istniejącego kodu i stworzyłem klasę Signal, która reprezentuje jedno nagranie dźwiękowe zapisane jako plik:

class Signal:
    def __init__(self, path):
        assert os.path.isfile(path)
        self.path = path

    def value(self):
        file = wave.open(self.path, 'r')
        content = np.fromstring(file.readframes(-1), 'Int16')
        file.close()
        return content

Metoda value zwraca sygnał – czyli tablicę próbek.

Klasę reprezentującą katalog na dysku nazwałem Directory, a jej metoda signals zwraca zbiór sygnałów (nagrań) z danego folderu:

class Directory:
    def __init__(self, directory):
        self.directory = directory

    def signals(self):
        return map(
            lambda file: Signal(self.directory + file).value(),
            os.listdir(self.directory)
        )

Statystyki

Najważniejszą klasą jest Stats – reprezentuje ona statystyki dotyczące rozkładu cechy pośród nagrań w danym katalogu. O co chodzi? W folderze z wesołymi piosenkami, mamy dużo plików; obliczamy jakąś cechę (czyli jedną liczbę na utwór), a następnie wyciągamy średnią oraz odchylenie standardowe dla całego katalogu. Jest za to odpowiedzialna metoda value:

class Stats:
    def __init__(self, directory, transform):
        self.directory = directory
        self.transform = transform

    def value(self):
        rates = list(
            map(
                lambda data: self.transform(data).mean(),
                self.directory.signals()
            )
        )
        return {
            "mean": scipy.mean(rates),
            "std": scipy.std(rates)
        }

A czym jest transform? Jest to transformacja, czyli funkcja, która na podstawie sygnału, oblicza jego cechy.

Konkrety

Przetestujmy powyższe klasy w boju:

PATH = '../dataset/emotion-recognition-236f22a6fde0/4. dataset (audio)/'
MOODS = ['Angry_all/',
         'Happy_all/',
         'Relax_all/',
         'Sad_all/']
stats = map(
    lambda m: Stats(
        Directory(PATH + m),
        librosa.feature.zero_crossing_rate).value(),
    MOODS)

Powyższy kod tworzy wektor stats, w którym będą wartości średnie oraz odchylenia standardowe cechy zwanej gęstością przejść przez zero dla poszczególnych nastrojów.

Gęstość przejść przez zero

Czym jest gęstość przejść przez zero? Kiedy myślimy o sygnale dźwiękowym, najczęściej widzimy oczami wyobraźni twór podobny do tego z rysunku obok. Jak widać, jego wartości raz rosną, raz maleją. Jeśli w połowie wysokości narysujemy poziomą linię, to każde przecięcie się wykresu z nią, będzie przejściem przez zero. Inaczej mówiąc, będzie to każdy punkt, w którym wartości naszego sygnału zmieniają znak.

 

O co więc chodzi z tą gęstością? Funkcja, której użyłem (librosa.feature.zero_crossing_rate) dzieli sygnał wejściowy na równe fragmenty (domyślnie po 2048 próbek) i oblicza liczbę przejść przez zero dla każdego fragmentu. Aby trochę uprościć wszystko, zakładam, że wartość tej cechy jest raczej stała przez cały czas trwania utworu i interesuje nas tylko wartość średnia. Stąd użycie funkcji mean w klasie Stats, w linijce:

lambda data: self.transform(data).mean()

Wyniki

Żeby to wszystko zebrać do kupy, fajnie jest narysować wykres. Oto kod, który się tym zajmie:

for stat in stats:
    means.append(stat["mean"])
    deviations.append(stat["std"])

plt.bar([0, 1, 2, 3], means, .2, yerr=deviations, ecolor='k')
plt.show()

Większej filozofii tu nie ma: tworzymy dwa wektory means i deviations oraz rysujemy wyświetlamy wykres jak poniżej:

Każdy kolejny słupek to inny nastrój. Nie podpisałem ich… bo właściwie to nie ma większego znaczenia w tym momencie – jak widać różnice pomiędzy nastrojami są bardzo małe, niemal pomijalne. Na pewno więc gęstość przejść przez zero nie jest dobrym dyskryminatorem nastroju.

…a przynajmniej nie jest wystarczająca do jego określenia. I dobrze – jest jeszcze wiele ciekawych cech i ich kombinacji do sprawdzenia. Co ważne, mam już w miarę wygodny system do ich testowania.

No dobra, nie do końca wygodny, ale o tym w kolejnym wpisie!