praca-magisterska/docs/document.tex

1123 lines
61 KiB
TeX
Raw Normal View History

\documentclass[12pt, a4paper, reqno, twoside]{mwbk}
\usepackage[MeX]{polski}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{makeidx}
\usepackage{graphicx}
\usepackage{fancyhdr}
\usepackage{pythonhighlight}
\usepackage{chapter-style}
2020-04-06 19:23:25 +02:00
\usepackage{tikz}
\usepackage{listings}
\usepackage{subfig}
2020-05-28 00:07:11 +02:00
% \usepackage[T1]{fontenc}
\newcommand{\textoperatorname}[1]{
\operatorname{\textnormal{#1}}
}
\newcommand{\tens}[1]{
\mathbin{\mathop{\otimes}\limits_{#1}}
}
\textheight 21.1 cm
\voffset = 1.2 cm
\textwidth 14 cm
\hoffset = -0.5 cm
\oddsidemargin = 1.4 cm
\newtheorem{de}{Definicja}
\newtheorem{tw}{Twierdzenie}
\captionsetup[figure]{labelfont={bf}, name={Rysunek}, labelsep=period}
\pagestyle{fancy}
\renewcommand{\chaptermark}[1]{\markboth{#1}{}}
%\renewcommand{\sectionmark}[1]{\markright{\thesection\ #1}}
\fancyhf{} \fancyhead[LE,RO]{\small\thepage}
%\fancyhead[LO]{\small\rightmark}
\fancyhead[RE]{\small\leftmark}
\fancyheadoffset[LO]{0 cm}
\fancyheadoffset[RE]{0 cm}
\renewcommand{\headrulewidth}{0.5pt}
\renewcommand{\footrulewidth}{0pt}
\addtolength{\headheight}{0.5pt}
\fancypagestyle{plain}{
\fancyhead{}
\renewcommand{\headrulewidth}{0pt}
}
\makeindex
\begin{document}
% Strona tytułowa
\thispagestyle{empty}
\begin{center}
\textsc{UNIWERSYTET IM. ADAMA MICKIEWICZA W POZNANIU}
\vglue 0.1 cm
\textsc{WYDZIAŁ MATEMATYKI I INFORMATYKI}
\vglue 2.1 cm
{\LARGE \bf Cezary Adam Pukownik}
\vglue 1cm
{\large Kierunek: Analiza i przetwarzanie danych}
2020-04-06 19:23:25 +02:00
% \medskip
2020-04-06 19:23:25 +02:00
% {\large Specjalność: Uczenie maszynowe}
\medskip
{\large Numer albumu: 444337}
\vspace{1.5cm}
{\Huge \bf Generowanie muzyki \\[4pt] przy pomocy głębokiego uczenia\\}
\vspace{0.8cm}
{\large \bf Music generation with deep learning\\}
\end{center}
\vspace{3cm}
\hspace{7.5cm}{Praca magisterska}\\[-12pt]
\hspace{7.5cm}{napisana pod kierunkiem}\\[-12pt]
\hspace{7.5cm}{dr hab. Tomasza Góreckiego}
\vspace{2.2cm}
\begin{center}
\textsc{POZNAŃ 2020}
\end{center}
% Koniec strony tytułowej
\clearpage \thispagestyle{empty} \cleardoublepage
% Oświadczenie
2020-05-28 00:07:11 +02:00
\thispagestyle{empty}
\begin{flushright}
Poznań, dnia .....................
\end{flushright}
\vglue 2.4 cm
\begin{center}
\large \bf OŚWIADCZENIE
\end{center}
\vglue 1.2 cm
Ja, niżej podpisany Cezary Pukownik, student Wydziału Matematyki i Informatyki Uniwersytetu im. Adama Mickiewicza w Poznaniu oświadczam, że przedkładaną pracę dyplomową pt: "Generowanie muzyki przy pomocy głębokiego uczenia", napisałem samodzielnie. Oznacza to, że przy pisaniu pracy, poza niezbędnymi konsultacjami, nie korzystałem z pomocy innych osób, a w szczególności nie zlecałem opracowania rozprawy lub jej części innym osobom, ani nie odpisywałem tej rozprawy lub jej części od innych osób.
Oświadczam również, że egzemplarz pracy dyplomowej w wersji drukowanej jest całkowicie zgodny z egzemplarzem pracy dyplomowej w wersji elektronicznej.
Jednocześnie przyjmuję do wiadomości, że przypisanie sobie, w pracy dyplomowej, autorstwa istotnego fragmentu lub innych elementów cudzego utworu lub ustalenia naukowego stanowi podstawę stwierdzenia nieważności postępowania w sprawie nadania tytułu zawodowego.
\bigskip
\noindent $[TAK]^{\star}$ - wyrażam zgodę na udostępnianie mojej pracy w czytelni Archiwum UAM
\medskip
\noindent $[TAK]^{\star}$ - wyrażam zgodę na udostępnianie mojej pracy w zakresie koniecznym do ochrony mojego prawa do autorstwa lub praw osób trzecich
\vglue 1.2 cm
\noindent{\small $^{\star}$Należy wpisać TAK w przypadku wyrażenia zgody na udostępnianie pracy w czytelni Archiwum UAM, NIE w przypadku braku zgody. Niewypełnienie pola oznacza brak zgody na udostępnianie pracy.}
\vglue 2 cm
\hglue 6cm ............................................................
% Koniec oświadczenia
2020-04-06 19:23:25 +02:00
% #######################################################
% CZĘŚĆ GŁÓWNA PRACY
% #######################################################
\clearpage \thispagestyle{empty} \cleardoublepage
\tableofcontents
2020-05-28 00:07:11 +02:00
\chapter*{Streszczenie}
\chapter*{Abstract}
2020-05-28 00:07:11 +02:00
2020-04-06 19:23:25 +02:00
\chapter*{Wstęp}
Uczenie maszynowe w ostatnich latach mocno zyskało na popularności. Zastosowania i możliwości różnych algorytmów uczenia maszynowego czasami przekraczają nasze wyobrażenie o tym co komputer może zrobić. Niektóre aplikacje potrafią wręcz zaskoczyć użytkowników tym co potrafią zrobić. Wśród takich aplikacji znajdują się takie, które potrafią przewidywać następne wartości akcji giełdowych, rozpoznawać na filmie obiekty w czasie rzeczywistym, czy nawet prowadzić samochód. Algorytmy wyuczone proponują nam spersonalizowane reklamy, czy produkty na podstawie naszych upodobań. Najczęstsze zastosowania dotyczą przetwarzania obrazów lub tekstu, natomiast zastosowania w przetwarzaniu muzyki są niszowe i rzadziej spotykane.
2020-04-06 19:23:25 +02:00
Celem tej pracy jest stworzenie modelu sieci neurowej, którego zadaniem bedzie generowanie krótkich multiinstrumentalnych klipów muzycznych.
2020-04-06 19:23:25 +02:00
W pierwszej częsci swojej pracy przedstawię podstawowe koncepcje związane z muzyką oraz sposobami jej reprezentacji. Następnie opiszę w jaki sposób działają sieci neuronowe, jak się uczą oraz podstawowe architektury sieci, które pomogą zrozumieć model który wykorzystałem.
Następnie przedstawię koncepcję działania modelu, jakie idee stały za wyborami, które podjąłem w projektowaniu sieci. W szczegółowy sposób opiszę sposób ekstrakcji danych tak aby mogły być one wykorzystane przez model. Opiszę architekturę którą wybrałem oraz przedstawię i opisze fragmenty kodu w języku python.
2020-04-06 19:23:25 +02:00
W kolejnym rozdziale skupimy się na rezultatach pracy, przedstawie zalety i wady modelu. Przeprowadze analizę jakie muzyczne koncepcje model się nauczył na podstawie danych oraz doprowadzę do ostatecznej konkluzji czy wygenerowana muzyka może być przyjemna dla odbiorcy.
\newpage
\chapter{Wprowadzenie do sieci neuronowych}
Aby lepiej zrozumieć w jaki sposób odpowiednio skonstruowane sieci neuronowe potrafią sprostać takiemu zadaniu jak generowanie muzyki, w tym rozdziale przedstawię od podstaw zasady działania sieci neuronowych. Opiszę w jaki sposób można od regresji liniowej przejść do prostych sieci oraz w jaki sposób uczy się sieci neuronowe. Ostatecznie przedstawię architektury, które wykorzystałem w projekcie.
\section{Regresja liniowa}
Podstawą wszystkich sieci neuronowych jest regresja liniowa. W statystyce wykorzystywana, aby wyjaśnić liniowe zalezności między zmiennymi.
Prosty model regresji liniowej dla jednej zmiennej można opisać wzorem
% \footnote{Statystyka, Mieczysław Sobczyk s.179}
\[y = ax+b+\epsilon,\]
gdzie
\begin{itemize}
\item $y$ jest zmienną objaśnianą,
\item $x$ jest to zmienna objaśniająca,
\item $a$ jest parametrem modelu,
\item $b$ jest wyrazem wolnym modelu,
\item $\epsilon$ jest składnikiem losowym \cite{statystyka}.
\end{itemize}
\medskip
Zadaniem jest znalezienie takiego parametru $a\in \mathbb{R}$ oraz wyrazu wolnego $b \in \mathbb{R}$, aby dla znanych wartości $x \in \mathbb{R}$ oszacowanie zmiennej objasnianej $\hat{y} \in \mathbb{R}$ najlepiej opisywała zmienną objasnaną $y \in \mathbb{R}$. Tak zdefiniowany model opisuje zmienną $y$ z dokładnością do składnika losowego. W praktyce oznacza to, że szacowane modele będą przybliżeniem opisywanych zależności.
\medskip
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/linear_reg.png}
\caption{Regresja liniowa jednej zmiennej}
\label{fig:linreg}
\end{figure}
Wartość zmiennej objaśnianej $y$ można również opisać za pomocą wielu zmiennych objaśniająych. Wtedy dla zmiennych objaśniającej $x_1, x_2, ... , x_p \in \mathbb{R}$ szukamy parametrów $\theta_1, \theta_2, ... ,\theta_p \in \mathbb{R}$, gdzi $p \in \mathbb{N}$ jest liczbą cech. Otrzymany w ten sposób model nazywany jest również hipotezą i oznaczamy go $h(x)$.
\[h(x) = b + \theta_1x_2 + \theta_2x_2 + ... + \theta_px_p + \epsilon = b + \sum_{i=1}^{p} \theta_ix_i + \epsilon.\]
Rysunek~\ref{fig:linreg} przedstawia przykładowy model regresji liniowej jednej zmiennej, dopasowany do zbioru.
\section{Uczenie modelu}
Celem uczenia modelu jest znalezienie ogólnych parametrów, aby model dla wartości wejściowych $x$ zwracał wartości predykcji $\hat{y}$ najlepiej opisującej całe zjawisko według pewnego kryterium. Formalnie, aby suma wszystkich różnic między predykcją, a rzeczywistością była najmniejsza.
\[
\textoperatorname{błąd} = \sum_{i=1}^m | \textoperatorname{predykcja} - \textoperatorname{rzeczywistość} |,
\]
gdzie $n \in \mathbb{N}$ jest wielkością zbioru danych jakim dysponujemy. Minimalizując bład dla modelu jesteśmy wstanie znaleźć przybliżenie funkcji $h(x)$.
\subsection{Funkcja kosztu}
W tym celu używa się funkcji $J_\theta(h)$, która zwraca wartość błędu między wartościami $h(x)$ oraz $y$ dla wszystkich obserwacji. Taka funkcja nazywana jest funkcją kosztu (\textit{ang. cost function}).
Dla przykładu regresji liniowej funkcją kosztu może być błąd średniokwadratowy (\textit{ang. mean squared error}). Wtedy funkcja kosztu przyjmuje postać:
\[ J_\theta(h) = \frac{1}{m}\sum_{i=1}^{m}(y_i-h(x_i))^2.\]
Przy zdefiniowanej funkcji kosztu proces uczenia sprowadza się do znalezienia takich parametrów funkcji $h(x)$, aby funkcja kosztu była najmniejsza. Jest to problem optymalizacyjny sprowadzający się do znalezienia globalnego minimum funkcji.
\subsection{Znajdowanie minimum funkcji}
Aby znaleźć minimum funkcji $f$ możemy skorzystać z analizy matematycznej. Wiemy, że jeśli funkcja $f$ jest różniczkowalna, to funkcja może przyjmować minimum lokalne, gdy $f'(x_0) = 0$ dla pewnego $x_0$ z dzieniny funkcji $f$. Dodatkowo jeśli istanieje otoczenie punktu $x_0$, że dla wszystkich punktów z tego otoczenia spełniona jest nierówność:
\[ f(x)>f(x_0), \]
to znaleziony punkt $x_0$ jest minimum lokalnym.
W teorii należałoby zatem wybrać taką funkcję kosztu, aby była różniczkowalna. Rozwiązać równanie $J_\theta'(h)=0$, następnie dla otrzymanych wyników sprawdzić powyższą nierówność oraz wybrać najmniejszy wynik ze wszystkich \cite{analiza_mat}. W praktyce rozwiązanie takiego równania ze względu na jego złożoność może się okazać niewykonalne. Aby rozwiązać ten problem powstały inne metody, które pozwalają szukać ekstremów funkcji, jednak nigdy nie będziemy mieli pewności, że otrzymany wynik jest minimum globalnym funkcji kosztu.
% \footnote{źródlo: Analiza matematyczna, Krysicki Włodarski, s.187 }.
\subsection{Metody gradientowe}
Metody gradientowe (\textit{ang. gradient descent}) są to iteracyjne algorytmy służące do znajdowania minimum funkcji. Aby móc skorzystać z metod gradientowych analizowana funkcja musi być ciągła oraz różniczkowalna. Sposób ich działania można intuicyjnie opisać w nastepujących krokach.
1. Wybierz punkt początkowy.
2. Oblicz kierunek, w którym funkcja maleje.
3. Przejdź do kolejnego punktu zgodnie obliczonym kierunkiem o pewną małą wartość.
4. Powtarzamy, aż osiągniemy minimum funkcji.
Wizualizację algorytmu została przedstawiona na rysunku~\ref{fig:gradient}.
\begin{figure}[!htb]
\centering
\subfloat[Wyznaczenie gradientu]{{\includegraphics[width=5cm]{images/gradient_descent_1_long.png} }}%
\qquad
\subfloat[Iteracja kolejnych punktów]{{\includegraphics[width=5cm]{images/gradient_descent_2_long.png} }}%
\caption{Wizualizacja algorytmu gradientu prostego}%
\label{fig:gradient}
\end{figure}
\medskip
Dla funkcji $h(x)$ należy ustalić wartość początkową $\Theta_0$ dla wszystkich parametrów $\theta_1$ ... $\theta_p$.
\[ \Theta_0 = \mathbf{\boldsymbol{\left[ \theta_1, \theta_2, ... ,\theta_n \right]}}. \]
Następnie policzyć wszystkie pochodne cześciowe $\frac{\partial J_\theta(h)}{\partial \theta_i}$. Otrzymamy w ten sposób gradient $\nabla J_\theta(h)$, gdzie
\[
\nabla J_\theta(h) = \mathbf{\boldsymbol{\left[ \frac{\partial J_\theta(h)}{\partial \theta_1}, \frac{\partial J_\theta(h)}{\partial \theta_2}, ... , \frac{\partial J_\theta(h)}{\partial \theta_n} \right]}}.
\]
Następnie obliczyć element $\Theta_{k+1}$ ze wzoru
\[
\Theta_{k+1} = \Theta_{k} - \alpha\nabla J_\theta(h),
\]
gdzie $\alpha \in \mathbb{R}$ jest współczynnikiem uczenia (\textit{ang. learning rate}), a $k \in \mathbb{N}$ jest kolejną iteracją algorytmu. Proces ten należy powtarzać do pewnego momentu. Najczęściej z góry określoną liczbę razy lub do momentu, gdy uzysk funkcji kosztu spowodowany nastepną iteracją jest mniejszy niż ustalona wartość. Otrzymany w ten sposób wektor parametrów $\Theta_k$ jest wynikiem algorytmu \cite{survay}.
% \footnote{Deep Learning techniques for music geneation - A survay s.44}
Wykorzystując metody gradientowe otrzymujemy wyuczony model. Parametry $\theta_i$ modelu $h(x)$ zostały ustalone w taki sposób, aby błąd między predykcją, a rzeczywistością był najmniejszy.
\section{Regresja liniowa jako model sieci neuronowej}
\label{section:linreg}
Omawiany model regresji możemy zapisać w sposób graficzny tak jak przedstawiono na rysunku~\ref{fig:neural_model_one}.
\begin{figure}[!htb]
\centering
\includegraphics[width=8cm]{images/naural_model_one_ver2.png}
\caption{Regresja liniowa jako model sieci neuronowej}
\label{fig:neural_model_one}
\end{figure}
Każdy węzeł z lewej strony reprezentuje zmienną objaśniającą $x_i$. Połączenia nazywane są wagami (\textit{ang. weights}) i reprezentują one parametry $\theta_i$. Węzeł z prawej strony oznaczony jako $\hat{y}$ jest sumą iloczynów wag oraz wartości węzłów z prawej strony. Wtedy
\[
\hat{y} =
\mathbb{\boldsymbol{\begin{bmatrix}
1 \\ x_1 \\ x_2 \\ \vdots \\ x_n
\end{bmatrix}
\begin{bmatrix}
b & \theta_1 & \theta_2 & \dots & \theta_p
\end{bmatrix} }}
=
b + x_1\theta_1 + x_2\theta_2 + \dots + x_n\theta_n =
b + \sum_{i=1}^n x_i\theta_i
\]
co jest równoważne omawianemu modelowi regresji liniowej. Węzły sieci nazywane są neuronami, a wyraz wolny modelu $b$ nazywany jest biasem (\textit{ang. bias}).
W łatwy sposób możemy rozbudować ten model do regresji liniowej wielu zmiennych. Predykcją modelu nie będzie jak do tej pory jedna wartość $\hat{y}$, tylko wektor wartości $\hat{y_1}, \hat{y_2}, \dots , \hat{y_q}$, który oznaczać bedziemy jako $\hat{Y}$. Model ten został przedstawiony na rysunku~\ref{fig:neural_model_multi}.
\begin{figure}[!htb]
\centering
\includegraphics[width=8cm]{images/naural_model_multi_ver2.png}
\caption{Regresja liniowa wielu zmiennych jako model sieci nauronowej}
\label{fig:neural_model_multi}
\end{figure}
Dla uogólnienia pojedyncze wagi modelu zapisywać będę jako $w_{pq}$, natomiast macierz wag jako $W$. Algebricznie zapisalibyśmy ten model jako
\[
\mathbb{\boldsymbol{\begin{bmatrix}
1 & 1 & \dots & 1\\
x_{11} & x_{12} & \dots & x_{1q} \\
x_{21} & x_{22} & \dots & x_{2q} \\
\vdots & \vdots & \ddots & \vdots \\
x_{p1} & x_{p2} & \dots & x_{pq} \\
\end{bmatrix}
\begin{bmatrix}
b_1 & w_{11} & w_{12} & \dots & w_{1p} \\
b_2 & w_{21} & w_{22} & \dots & w_{2p} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
b_q & w_{q1} & w_{q2} & \dots & w_{qp} \\
\end{bmatrix} }}
=
\mathbb{\boldsymbol{
\begin{bmatrix}
h_1(x) \\ h_2(x) \\ \vdots \\ h_q(x)
\end{bmatrix} }}
=
\mathbb{\boldsymbol{
\begin{bmatrix}
\hat{y_1} \\ \hat{y_2} \\ \vdots \\ \hat{y_q}
\end{bmatrix} }}
\]
\[
b+XW = \hat{Y},
\]
gdzie $p$ jest liczbą zmiennych niezależnych, $q$ jest liczbą zmiennych zaleznych, $X$ jest rozszerzonym do macierzy o rozmiarach $q$ x $p$ wektorem zmiennych objaśniających, w taki sposób że $x_{i1} = x_{i2} = \dots = x_{ip}$ dla $i = 1, 2, ..., q$, $W$ jest macierzą wag o rozmiarach $p$ x $q$, natomiast $b$ jest sumą wyrazów wolnych $b_1, ... ,b_q$. Możemy zauważyć, że model dla wielu zmiennych jest wieloma modelami dla jednej zmiennej, gdzie każdy model operuje na tych samych danych wejściowych. Taki model może być uznany za sieć neuronową i nazywany jest perceptronem.
\section{Funkcje aktywacji}
Omawiany model służy rozwiązywaniu problemu regresji, ponieważ wartości predykcji nie są uregulowane i mogą przyjmować wartości z $\mathbb{R}$. W celu przekształcenia tego modelu, aby móc go wykorzystać do rozwiązania problemu klasyfikacji, należy dodatkowo na otrzymanym wektorze $\hat{Y}$ wykonać pewną funkcję, która przekształci wynik. W tym celu używamy funkcji aktywacji (\textit{ang. activation function}). Istnieje wiele różnych funkcji aktywacji, a każda posiada inną charakterystykę i wpływ na model. Najpopularnijeszą grupą fukcji są funkcje sigmoidalne (\textit{ang. sigmoid functions}). Jedną z nich jest funkcja logistyczna (\textit{ang. logistic curve})
\[
\sigma(x) = \frac{1}{1+e^{-x}}
\]
oraz wykresie przedstawionym na rysunku~\ref{fig:sigmoid}
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/sigmoid.png}
\caption{Funkcja logistyczna}
\label{fig:sigmoid}
\end{figure}
Funkcja logistyczna ma pewne użyteczne właściwości, które pozwolą kontrolować wartości węzłów oraz rzutować wartości z całego $\mathbb{R}$ do wartości z przedziału $(0,1)$. Dzięki tej właściwości funkcja logistyczna jest często używana, aby otrzymać prawdopodobieństwo wystąpienia pewnego zdarzenia. Dodatkowo funkcja logistyczna szybko przyjmuje wartości skrajne, co oznacza że dla bardzo dużych wartości ujemnych i bardzo dużych wartości dodatnich funkcja staje się mało wrażliwa na zmiany wartości wraz ze zmianą wartości argumentu
\cite{deep_learning}.
% \footnote{Deep Learning Book, s.66}
W ten sposób możemy w łatwy sposób zmienić model regresji liniowej na model regresji logistycznej.
\[
\sigma(b+XW) = \hat{Y}
\]
W dalszych cześciach pracy, kiedy bedę używał funkcji aktywacji nie wskazując na konkretną funkcję, będe wykorzystywał oznaczenie $AF(x)$.
\section{Wielowarstwowe sieci neuronowe}
Model omawiany wcześniej może posłużyć jako podstawowy element do budowania bardziej skomplikowanych modeli. Aby to zrobić, należy potraktować otrzymany wektor $\hat{Y}$ jako wektor wejściowy do następngo podstawowego modelu. Składając ze sobą wiele perceptronów w jeden model, tworzymy warstwy (\textit{ang. layers}) sieci neuronowej.
Wyróżniamy trzy rodzaje warstw:
\begin{itemize}
\item warstwę wejściową (\textit{ang. input layer}), która jest pierwszą warstwą modelu,
\item warstwę wyjściową (\textit{ang. output layer}), która jest ostatnią wartstwą modelu,
\item wartwy ukryte (\textit{ang. hidden layer}), które są warstwami pomiędzy warstwą wejsciową oraz wyjściową.
\end{itemize}
Na rysunku~\ref{fig:neural_net_1} przedstawiono sieć posiadającą warstwę wejściową, dwie wartswy ukryte oraz wartswę wyjsciową.
\begin{figure}[!htb]
\centering
\includegraphics[width=8cm]{images/neural_net_1_ver2.png}
\caption{Przykład modelu wielowarstwowej sieci neuronowej}
\label{fig:neural_net_1}
\end{figure}
Tego typu modele są głębokimi sieciami neuronowymi (\textit{ang. deep neural networks}). Istnieje wiele różnich architektur głębokich sieci neuronowych, które wykorzystują te podstawowe koncepcje i rozszerzają je o dodatkowe warstwy, połącznia, funkcje aktywacji czy neurony o specjalnych właściwościach.
\subsection{Jednokierunkowe sieci neuronowe}
\label{section:feedforeward}
Jednokierunkowe sieci neuronowe (\textit{ang. feedforward neural networks}) są to najprostrze sieci neuronowe, które wprost czerpią z omówionych wcześniej podstawowych wartsw. Możemy się również spotkać z nazwą wielowarstwowy perceptron (\textit{ang. multi layer perceptron - MLP}) ze względu na fakt, że jest zbudowany z wielu perceptronów zaprezentoanych w rozdziale~\ref{section:linreg}. Działają one w taki sposób, że zasila się je danymi do warstwy wejściowej, następnie sukcesywnie wykonuje się obliczenia do momentu dotarcia do końca sieci.
Każdy krok z warstwy $k-1$ do warstwy $k$ obliczany jest zgodnie ze wzorem \cite{survay} %\footnote{Deep Learning techniques for music geneation - A survay s.63}
\[
X_k = AF(b_k + W_kX_{k-1}).
\]
\subsubsection{Propagacja wsteczna błędu}
\label{section:backpropagation}
Kiedy używamy jednokierunkowych sieci neuronowych, zasilamy je danymi wejściowymi $x$ ostatecznie otrzymując predykcję $\hat{y}$. Taki sposób działania nazywa się propagcją wprzód (\textit{ang. foreward propagation}). Podczas uczenia sieci kontynuuje się ten proces obliczając koszt $J(h)$. Propagacja wsteczna (\textit{ang. back-propagation}) pozwala na przepływ informacji od funkcji kosztu wstecz sieci neuronowej, aby ostatecznie obliczyć gradient. Zasada działania algorytmu propagacji wsteczniej błędu polega na sukcesywnym aktualizowaniu wag i biasów oraz przesyłaniu wstecz po warstwach sieci. Dzięki temu jesteśmy wstanie wyuczyć sieć oraz obliczyć optymalne wagi i biasy dla całej sieci neuronowej.
\subsection{Autoencodery}
\label{section:autoencoder}
Autoencoder jest szczególnym przypadkiem sieci neuronowej. Posiada jedną warstwę ukrytą, a rozmiar wartwy wejściowej musi być równy rozmiarowi wartwy wyjściowej, tworząc w ten sposób symetryczną sieć, której kształt przypomina klepsydrę. Przykład autoencodera przedstawiono na rysunku~\ref{fig:autoencoder}.
\begin{figure}[!htb]
\centering
\includegraphics[width=8cm]{images/autoencoder.png}
\caption{Przykład modelu autoencodera}
\label{fig:autoencoder}
\end{figure}
Podczas uczenia autoencodera przedstawia się dane wejściowe jako cel. W ten sposób ta architektura stara się odtworzyć funkcje identycznościowe. Zadanie nie jest trywialne jak mogło by się zdawać, ponieważ zazwyczaj ukryta warstwa jest mniejszego rozmiaru niż dane wejściowe. Z tego względu autoencoder jest zmuszony do wydobycia istotnych cech danych wejściowych, skompresowania, a następnie jak najwierniejszego ich odtworzenia. Część kompresująca dane nazywana jest encoderem, natomiast część dekompresująca decoderem.
Cechy, które zostały odkryte przez autoencoder nazywane są zmiennymi utajnionymi (\textit{ang. latent variables}). Zarówno encoder jak i dekoder można wyodrębnić z autoencodera i wykorzystywać go jako osobną sieć neuronową.
Ciekawą cechą decodera jest jego generatywny charakter, ponieważ dostarczając zupełnie nowe informacje jako zmienne wejściowe, decoder odtworzy je na podobieństwo danych, na których został nauczony.
\subsection{Rekurencyjne sieci neuronowe}
\label{section:rnn}
Rekurencyjne sieci neuronowe (\textit{ang. recurrent neural networks; RNN}) w uproszczeniu są to MLP posiadające pamięć. Wykorzystywane są do analizawania i przewidywania sekwencji wartości uporządkowanych w czasie. Rekurencyjne sieci neuronowe znalazły zastosowanie w przetwarzaniu języka natutralnego, np. tłumaczenia na różne języki świata. Potrafią poradzić sobie z różnej długości sekwencjami od krótkich zawierających kilka elementów do bardzo długich jak próbki audio, czy tekst zawierający dziesiątki tysięcy kroków czasu.
Rekurencyjne sieci neuronowe działają podobnie do omawianych w sekcji~\ref{section:feedforeward} sieci jednokierunkowych z tym wyjątkiem, że kierunek przepływu informacji płynie również wstecz sieci. Jeden neuron sieci RNN otrzymuje dane wejściowe $x_{(t)}$, wytwarza dane wyjściowe $y_{(t)}$, a następnie wysyła te dane wyjściowe z powrotem do samego siebie. W ten sposób neuron RNN posiada dwa wejścia $x_{(t)}$ oraz $y_{(t-1)}$. Możemy również zaprezentować sieć RNN w postaci odwiniętej w czasie (\textit{ang. unrolled through time}).
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/rnn.png}
\caption{Rekurencyjny neuron (po lewej) odwinięty w czasie (po prawej)}
\label{fig:autoencoder}
\end{figure}
Gdyby rozważyć całą warstwę neuronów tego typu, wtedy wartswa przyjmowała by dwie macierze wag $W_x$ oraz $W_y$. Dane wyjściowe całej warstwy zostaną obliczone wtedy zgodnie ze wzorem
\[
y_{(t)} = AF(W_x^\top x_{(t)} + W_y^\top y_{(t-1)} + b)
\]
Aby wytrenować sieć neuronową stosuje się propagację wsteczną w czasie (\textit{ang. backpropagation through time; BPTT}). Polega ona na odwinięciu sieci RNN, a następnie zastosowania zwykłej metody wstecznej propagacji\cite{handson}. %\footnote{Hands-on machine learning with scikit-learn, keras and TensorFlow s.497}
\subsection{LSTM}
Komórki LSTM (\textit{ang. long-short term memory}) są rozszerzeniem neuronów sieci rekurencyjnych. Pozwalają wykrywać zależności w danych w długim okresie. Posiadają dwa wektory opisujące stan neuronu. Wektor $h_{(t)}$ określa stan krótkookresowy i wektor $c_{(t)}$ określa stan długookresowy.
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/lstm_cell2.png}
\caption{Komórka LSTM}
\label{fig:lstm}
\end{figure}
Główny pomysł na funcjonowanie komórek LSTM był taki, aby sieć sama mogła się nauczyć jakie informacje są istotne i je przechować, a które informacje można pominąć, zapomnieć. Schemat komórki LSTM przedstawiono na rysunku~\ref{fig:lstm}. Aby to osiągnąć powstała idea bramek (\textit{ang. gates}), oraz kontrolerów bramek (\textit{ang. gate controllers}). W komórce LSTM wyróżniamy trzy bramki. Bramkę zapomnienia (\textit{ang. forget gate}) sterowaną przez $f_{(t)}$, bramkę wejściową(\textit{ang. input gate}) sterowaną przez $i_{(t)}$, oraz bramkę wyjściową (\textit{ang. output gate}), sterowaną przez $o_{(t)}$. Przepływ danych w komórce LSTM zaczyna w miejscu gdzie wektor wejściowy $x_{(t)}$ i poprzedni którkoterminowy stan $h_{(t-1)}$ trafiają do czterech warstw. Główną warstwą jest ta zwracająca $g_{(t)}$. W podstawowej komórce RNN jest tylko ta warstwa. Pozostałe trzy wartswy po przejściu przez funkcje logistyczne trafiają do bramek. Bramka zapomnienia kontroluje, które informacje z długookresowego stanu $c_{(t-1)}$ powinny zostać wykasowane. Bramka wejściowa kontroluje jakie informacje z $g_{(t)}$ powinny zostać przekazane dalej i dodane do nastepnego stanu długookresowego $c_{(t)}$. Bramka wyjściowa odpowiada za wybranie odpowiednich elementów z stanu długookresowego i przekazanie ich następnych kroku. Wynik komórki zostaje przekazany do wyjścia komórki $y_{(t)}$ oraz jako następny stan krótkoterminowy $h_{(t)}$.
Kolejne etapy komórki LSTM obliczane są zgodnie z poniższymy wzorami:
\[
i_{(t)} = \sigma(W_{xi}^{\top}x_{(t)} + W_{hi}^{\top}h_{(t-1)} + b_i)
\]
\[
f_{(t)} = \sigma(W_{xf}^{\top}x_{(t)} + W_{hf}^{\top}h_{(t-1)} + b_f)
\]
\[
o_{(t)} = \sigma(W_{xo}^{\top}x_{(t)} + W_{ho}^{\top}h_{(t-1)} + b_o)
\]
\[
g_{(t)} = tanh(W_{xg}^{\top}x_{(t)} + W_{hg}^{\top}h_{(t-1)} + b_g)
\]
\[
c_{(t)} = f_{(t)}\tens{} c_{(t-1)} + i_{(t)}\tens{} g_{(t)}
\]
\[
y_{(t)} = h_{(t)} = o_{(t)} \tens{} tanh \left( c_{(t)} \right),
\]
gdzie $W_{xi}$, $W_{xf}$, $W_{xo}$, $W_{xg}$ są to macierze wag dla każdej w czterech warstw połączonych z wektorem wejściowym $x_{(t)}$, $W_{hi}$, $W_{hf}$, $W_{ho}$, $W_{hg}$ są to macierze wag dla każdej w czterech warstw połączonych z poprzednim krótkookresowym stanem $h_{(t-1)}$, a $b_i$, $b_f$, $b_o$, $b_g$ to biasy dla każdej z tych warstw \cite{handson}. %\footnote{Hands-on Machine Learning with Scikit-Learn, Keras, and TensorFlow s.517}
Funckja $tanh$ to tangens hiperboliczny, jedna z funkcji sigmoidalnych. Wykres funkcji $tanh$ został przedstawiony na rysunku~\ref{fig:tanh}
\[
tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
\]
To co różni funkcję $tanh$ od $\sigma$ to zakres przyjmowanych wartości. Tangens hiperboliczny przyjmuje wartości z przedziału $(-1, 1)$.
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/tanh.png}
\caption{Tangens hiperboliczny}
\label{fig:tanh}
\end{figure}
\subsection{Sequence-to-sequence}
Model w architekturze Sequence-to-sequance (\textit{ang. seq2seq}) został wynaleziony z myślą o tłumaczeniu maszynowym języków, ale zastosowanie dla niego znaleziono również w rozpoznawaniu mowy, opisywaniu wideo, czy tworzeniu chatbotów. Jego główną zaletą jest przetwarzanie sekwencji elementów o różnych długościach. Jest to naturalne, ponieważ tłumacząc z języka na język często tą samą sentencję można wyrazić różną liczbę słów. Dla przykładu zdanie po Polsku "Co dzisiaj robisz?" zawiera trzy słowa, natomiast przetłumaczone na Angielski "What are you doing today?" zawiera pięć słów. Nie można tego osiągnąć zwykłą siecią LSTM, dlatego model seq2seq został zaprojektowany, aby móc go zastosować do tego typu problemów \cite{seq2seq}. %\footnote{https://towardsdatascience.com/understanding-encoder-decoder-sequence-to-sequence
%-model-679e04af4346 26.05.2020 14:58}
Model sequence-to-sequance ma dwie części, encoder i decoder. Obie czeście są w zasadzie dwiema zupełnie osobnymi modelami, połączonymi ze sobą w jedną sieć.
Zadaniem encodera, podobnie jak zostało to opisane w rozdziale~\ref{section:autoencoder} o autoencoderze, jest wydobycie z wektora wejściowego najistotniejszych informacji i skompresowanie ich. Następnie wektor stanu encodera jest przekazywany do decodera, który
na jego podstawie rekonstruuje sekwencję.
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/seq2seq2.png}
\caption{Architektura modelu sequance-to-sequance}
\label{fig:seq2seq}
\end{figure}
Więcej szczegółów technicznych dotycząych modelu sequence-to-sequence przedstawię w dalszych rozdziałach pracy.
\chapter{Wprowadzenie do teorii muzyki}
2020-05-28 00:07:11 +02:00
W tym rozdziale przedstawię podstawowe koncepcje muzyczne, sposoby reprezentacji muzyki.
2020-04-06 19:23:25 +02:00
\section{Podstawowe koncepcje}
\subsection{Dźwięk muzyczny}
Drgania powietrza z otoczenia człowieka są przetwarzane w mózgu i rozumiane jako dźwięki. Takie drgania nazywamy falą dźwiękową. Dzwięk muzyczny jest to fala dźwiękowa, którą wytwarza intrument muzyczny. Dzwięk muzyczny charakteryzuje się trzema podstawowymi parametrami:
\begin{itemize}
\item wysokością (\textit{ang. pitch}) - jest to częstotliwość drgań wyrażona w hercach. Im większa częstotliwość tym dzźięk jest rozumiany jako wyższy,
2020-04-06 19:23:25 +02:00
Zakres słyszalny dla człowieka wynosi od 20Hz do 20kHz.
\item głośność (\textit{ang. velocity}) - jest to amplituda drgań fali dźwiękowej. Im większa aplituda, tym dźwięk jest odczuwany jako głosniejszy,
\item długość (\textit{ang. duration}) - jest to czas z jakim dźwięk wybrzmiewa, np. 2 sekundy.
2020-04-06 19:23:25 +02:00
\end{itemize}
% Przykład dźwięku myczynego przedstawiono na Rusynku~\ref{fig:sine}
% \begin{figure}[!htb]
% \centering
% \includegraphics[width=\linewidth]{images/sine_wave.png}
% \caption{Przykład fali dźwiękowej}
% \label{fig:sine}
% \end{figure}
\subsection{Sygnał dźwiękowy}
W rzeczywistości utwór muzyczny jest zazwyczaj kombinacją wielu fal dźwiękowych, o różnych charaketystykach i nazwywany jest sygnałem dźwiękowym. Wizualizację sygnalu dźwiękowego przedstawiono na Rysunku ~\ref{fig:waveform}
2020-04-06 19:23:25 +02:00
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/waveform.png}
\caption{Przykład przebiegu fali dźwiękowej}
\label{fig:waveform}
\end{figure}
\subsection{Zapis nutowy}
2020-05-28 00:07:11 +02:00
Reprezentacja muzyki jako sygnału dźwiękowego przechowuje informacje o dokładnym brzmieniu danego utworu tzn. jakie drgania należy wytworzyć, aby móc odtwozyć muzykę. Taki zapis nie informuje nas bezpośrednio jakie instrumenty zostały użyte, jakie wysokości i długości dźwięków zostały wykorzystane. Dlatego ludzkość na przestrzeni wieków opracowała abstrakcyjne objekty, które reprezentują utwór w czytelny dla człowieka sposób.
2020-04-06 19:23:25 +02:00
\subsubsection{Tempo}
W muzyce symbolicznej tempo informuje nas o prędkości utworu. W muzyce klasycznej stosowało się opisowy sposób dostosowywania tempa np. Allegro - Szybko lub Adagio - wolno. Jak można szybko stwierdzić są to zwroty subiektywne i nie wyznaczają tempa jednoznacznie. Obecnie wyraża się tempo w liczbie uderzeń na minutę (\textit{ang. beats per minute; BPM}). I tak Allegro jest to od 120 do 168 BPM a Adagio od 66 do 76 BPM \cite{tempos}. % \footnote{źródlo: http://www.classicalmusiccity.com/search/article.php?vars=446/
% Basic-Tempo-Markings.html 5 kwietnia 19:37
% }
2020-04-06 19:23:25 +02:00
\subsubsection{Nuta}
Nuta jest to graficzna reprezentacja dźwięku muzycznego. Informuje nas ona o dwóch parametrach dźwięku, wysokości oraz długości dźwięku. Długość dźwięku nazywa się jej wartością. Podstawową wartością nuty jest ćwierćnuta, odpowiada ona jednemu uderzeniu (\textit{ang. beat}). Ta wartość pozwala nam zrozumieć jak długo nalezy wygrywać nutę relatywnie do pozostałych nut w utworze. Jeśli obok siebie ustawimy dwie nuty o wartościach ćwirćnuty i ósemki, wiemy że tę drugą nutę powinniśmy zagrać dwa razy krócej niż pierwszą. Aby wiedzieć dokładnie jak dlugo powinna wybrzmiewać nuta musimy odwołać się do tempa utworu. Dla przykładu w tempie 60 BPM w ciągu minuty zagramy dokładnie 60 ćwierć nut. Kolejne wartości tworzone poprzez sumowanie lub podział długości ćwierćnuty. Półnuta trwa tyle co dwie ćwierćnuty, cała nuta tyle co dwie półnuty, ósemka trwa połowę czasu ćwiercnuty, a szesnastka połowę ósemki itd.
2020-04-06 19:23:25 +02:00
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/wartosc_nut.jpg}
\caption[]{Podział wartości nut\footnotemark}
2020-04-06 19:23:25 +02:00
\label{fig:waveform}
\end{figure}
\footnotetext{źródlo: https://www.infomusic.pl/poradnik/46934,poradnik-teoria-muzyki-rytm
\newline
5 kwietnia 2020 12:46}
2020-04-06 19:23:25 +02:00
Tak jak pisałem wcześniej, wysokość dźwięku jest to częstotliwość drgań fali dźwiękowej wyrażona w hercach. W muzyce symbolicznej dla uproszczenia wybrane częstotliwości zostały nazwane literami alfabetu C, D, E, F, G, A, H.
Każdej literze przypisana jest częstotliwość zgodnie z Tabelą~\ref{table:dzwieki}
\begin{table}[!htb]
\centering
\begin{tabular}{||c | c||}
\hline
Dzwięk & Ćzęstotliwość \\ [0.5ex]
\hline\hline
$C_4$ & 261,6\\
\hline
$D_4$ & 293,7\\
\hline
$E_4$ & 329,6\\
\hline
$F_4$ & 349,2\\
\hline
$G_4$ & 391,9\\
\hline
$A_4$ & 440,0\\
\hline
$H_4$ & 493,9\\
\hline
\end{tabular}
\caption{Dźwięki symboliczne oraz ich częstosliwości}
\label{table:dzwieki}
\end{table}
W zapisie nutowym aby nucie nadać wysokość, umieszcza się ją w odpowiednim miejscu na pięciolonii. Przedstawione powyżej dźwięki zapisalibyśmy w taki sposób jak przedstawiono na Rysunku~\ref{fig:pieciolinia}
\begin{figure}[!htb]
\centering
\includegraphics[width=\linewidth]{images/nuty_linia.png}
\caption{źródlo: https://amplitudaschool.weebly.com/lekcja-11.html 5 kwietnia 2020 13:24}
\label{fig:pieciolinia}
\end{figure}
2020-06-01 11:38:31 +02:00
\subsubsection{Interwały}
2020-04-06 19:23:25 +02:00
\subsubsection{Oktawy}
Oktawą nazywamy zestaw ośmiu nut od C do H. Podane w Tabeli~\ref{table:dzwieki} częstotliwości nut odpowiadają dźwiękom w oktawie czwartej. Dlatego w indeksie dolnym nuty widnieje liczba 4. Aby utworzyć dźwięk, np. $A_5$ należy pomnożyć częstotliwość dźwięku $A_4$ razy dwa, natomiast aby utworzyć dźwięk $A_3$, należy tę częstotliwość podzielić przez dwa.
\[ A_5 = 440Hz * 2 = 880Hz \]
\[ A_3 = 440Hz / 2 = 220Hz \]
W ten sposób możemy utworzyć nieskończenie wiele oktaw, jednak w rzeczywistości używa się nut od C0 do C8.
\subsubsection{Akord}
Gdy w jednym momencie zabrzmią dwie lub więcej różnych nut, wtedy mówimy o akordzie. Akord potrafi dodać emocje do brzmienia całego utworu.
\subsubsection{Skala}
\label{section:skala}
Do opisu, czym jest skala. -> Skala jest to zestaw nut, które dobrze ze sobą brzmią. Skalę opisujemy dwoma parametrami. Tonację, oraz modem. Tonacja jest to nuta startowa, dla skali. Mod nastomiast jest to zestaw interwałów liczony od pierwszej nuty. np. C-Dur, gdzie C jest wartością początkową, a Dur opisuje interwału, Możemu utorzyć inne skale, np G-Dur, używając tych samych interwałów, ale zaczynając od innej nuty.
2020-04-06 19:23:25 +02:00
\section{Cyfrowa reprezentacja muzyki symbolicznej}
\subsection{Standard MIDI}
Standrd MIDI (ang. Musical Instrument Digital Interface) został stworzony w 1983 aby umożliwić synchronizację i wymianę informacji między elektronicznymi urządzeniami muzycznymi takimi jak syntezatory, keyboardy czy sekwencery.
W późniejszych latach został on zaadaptowany do środowiska komputerowego jako cyfrowa reprezentacja muzyki symbolicznej.
\begin{figure}[!htb]
\centering
\begin{python}
2020-04-06 19:23:25 +02:00
note_on channel=0 note=48 velocity=100 time=0
note_on channel=0 note=53 velocity=100 time=0
note_on channel=0 note=60 velocity=100 time=0
note_on channel=0 note=48 velocity=0 time=220
note_on channel=0 note=48 velocity=100 time=0
note_on channel=0 note=53 velocity=0 time=0
note_on channel=0 note=55 velocity=100 time=0
note_on channel=0 note=60 velocity=0 time=0
\end{python}
2020-04-06 19:23:25 +02:00
\caption{Fragment protokołu MIDI}
\end{figure}
\subsubsection{Wiadomości}
Plik MIDI zawiera zestaw wiadomości przesyłanych w czasie rzeczywistym o każdej nucie w utworze. Dwie wiadomości, które są dla nas szczególnie istotne to:
\begin{itemize}
\item note\textunderscore on, który sygnalizuje aby rozpocząć grać nutę,
\item note\textunderscore off, który sygnalizuje aby zakończyć grać nutę.
\end{itemize}
\medskip
Dla przykładu wiadomość:
\begin{center}
note\textunderscore on channel 0 note 48 velocity 100 time 0 \\
2020-04-06 19:23:25 +02:00
\end{center}
oznacza aby na kanele 0 zagrać dźwięk nr 48 z głośnością 100 w momencie 0 utworu. Nie informuje nas on jednak o długości trwania dźwęku. Aby zakończyć dźwęk, należy wysłac wiadomość:
\begin{center}
note\textunderscore off, channel 0, note 48, velocity 100, time 24. \\
\end{center}
Zwróćmy uwagę że aby ustalić wartość nuty, potrzebujemy odebrać dwie wiadomości. Różnica między parametrami time, informuje nas o długości nuty. W tym przypadku jest to 24.
Co oznacza ćwierćnutę.
\subsubsection{Rozdzielczość}
Czas w MIDI jest reprezentowany jako liczba naturalna i jest on zależny od ustalonego tempa utworu. Standardowa rozdzielczość pliku MIDI to 24. Oznacza to, że jedna jednostka czasu odpowiada jednej dwudziestejczwartej jednego udeżenia.
\subsubsection{Kanały}
Plik MIDI posiada 16 kanałów numerowanych od 0 do 15. Każdy kanał odpowiada intrumentowi lub ścieżce. Kanał 9 jest kanałem zarezerwowanym na intrumenty perkusyjne.
\subsubsection{Nuty}
Nuty w formacie MIDI opisane są kolejnymi cyframi naturalnymi w przedziale od 0 do 127. Odpowiada to dźwękom od $C_0$ do $C_8$. Dla przykładu nuta 69 odpowiada $A_4$, a nuta 47 odpowiada $B_2$.
2020-05-28 00:07:11 +02:00
Wyjątkiem są nuty z kanału dziewiątego, gdzie istnieją tylko nuty z zakresu od 35 do 81 i każda nuta odpowiada innemu elementowi perkusyjnemu np. 35 to stopa (\textit{kick}), a 37 to werbel {\textit{snare}}.
2020-04-06 19:23:25 +02:00
\subsubsection{Głośność}
2020-05-28 00:07:11 +02:00
Za głośność dźwęku odpowiada parametr \textit{velocity}, który jest liczbą z przedziału od 0 do 127. Im większa jest wartość tym głośniej wybrzmi dźwęk.
2020-04-06 19:23:25 +02:00
\subsubsection{Program}
2020-05-28 00:07:11 +02:00
Program w kontekscie standardu MIDI oznacza instrument który ma zagrać nuty. W standardzie GM (ang. General MIDI), jest 16 grup intrumenów a w każdej z nich znajduje się po 8 intrumentów. Są to piania, chromatyczne perkusje, organy, gitary, basy, intrumenty smyczkowe, zestawy intrumentów, intrumenty dmuchane blaszane, intrumenty dmuchane drewniane, flety, syntezatory prowadzące, syntezatory uzupełniające, efekty syntetyczne, instrumenty etniczne, perkusjonalia i efekty dzwiekowe.
2020-04-06 19:23:25 +02:00
\subsubsection{Ścieżka}
2020-05-28 00:07:11 +02:00
Ścieżka (ang. Track) grupuje nuty aby podzielić utwór muzyczny na różne intrumenty lub partie. Protokół MIDI pozwala aby grać wiele ścieżek dzwiękowych jednocześnie, wtedy mówimy o muzyce polifonicznej lub multiintrumentalnej.
2020-04-06 19:23:25 +02:00
\chapter{Projekt}
2020-05-28 00:07:11 +02:00
W tym rozdzialę opiszę w jaki sposób zbudowałem swój własny geneator muzyki, jak przechodził proces uczenia, jakie próbki udało mi się wygenrować. Opis kodu który napisałem.
\section{Koncepcja}
Celem tej pracy, było wykonanie modelu, który przy użyciu głębokiego uczenia będzie w stanie generować krótkie klipy multiinstrumentalne. Zainspirował mnie sposób w jaki tworzy się muzykę w zespole. W przeciwieństwie do muzyki tworzonej przez jednego kompozytora, w zespole każda partia tworzona jest przez muzyka grającego na danym instrumencie. Przykładowy sposób tworzenia utworu w zepole, np. rockowym wygląda jak następuje. Jedna osoba tworzy (generuje) pierwszą patrię muzyczną, np partię na gitary. Ta partia została stworzona bez odniesienia do innych członków zespołu. Następnie taka partia zostaje przedstawiona zespołowi. Każdy z członków zespołu musi teraz stworzyć swoje partie w taki sposób, aby pasowały one muzycznie do pierwszej partii. W ten sposób powstają nam zalezności między partiami, tworzącymi cały utwór.
Na podstawie tej ideii postanowiłem opracować model składający się z wielu sieci nauronowych, każda z nich odpowiadać będzie jednej partii w utworze, muzykowi w zespole. Jedna z tych sieci będzie generatorem. Ta sieć powinna być skonstuowana w taki sposób aby zainicjować partię muzyczną. Pozostałe będą dopasowywać swoje partie w taki sposób aby pasowaly pod partię wygenerowaną. Te sieci nawywać bedę modelami akompaniującymi. Dzieki temu jesteśmy wstanie stworzyć model wielu sieci, w którym następna sieć, będzie produkować swoje partie na podstawie tego co wygenerowała poprzednia.
Kluczone było zauważenie podobieństwa między językiem naturalnym oraz muzyką. Zarówno zdanie, jak i partia muzyczna składa się z sekwencji elementów rozmieszczonych w czasie. Elementy te są zależne od długoterminowego kontekstu, oraz od tego jaki element był ustawiony wcześniej. Dla języka naturalnego są to słowa, na muzyki są to nuty i akordy. Dodatkowo pomyslałem, że różne instrumenty można porównać do róznych języków świata. Wtedy, aby stworzyć melodię np. basu tak aby pasowała pod partę gitary, nalezy "przetłumaczyć" język gitary na język basu. Do tłumaczeń języka naturalnego wykorzystuje się modele sequence-to-sequence, dlatego postanowiłem w modelu generowania muzyki wykorzystać właśnie tą architekturę. Dodatkowo modele sequance-to-sequance mają tę cechę, że liczba elementów sekwencji wejściowej, może być inna niż liczba elementów sekwencji wyjściowej. Idealnie sprawdzi się w przypadku muzyki, ponieważ o długości trwania ścieżki muzycznej nie świadczy liczba nut, tylko suma ich wartości.
2020-06-01 11:38:31 +02:00
\section{Wstępne przygotowanie danych do treningu}
2020-05-28 00:07:11 +02:00
2020-06-01 11:38:31 +02:00
Aby móc przedstawić sieci neuronowej muzykę, aby mogła być ona przez niego zrozumiana nalezy zamienić ją na liczby, wektory i macierze. Istenieje wiele technik umożliwiających osiągnięcie tego celu. W mojej pracy wykorzystałem technikę o nazwie one-hot encoding. Enkodowanie One-Hot jest wykorzystywane w uczeniu maszynowym aby nadać liczbową wartość danych kategorycznych. Polega ona stworzeniu słownika, w ktorym każde słowo otrzyma swój unikatowy identyfikator, następnie zostanie utworzony wektor o wymiarze słów w słowniku, gdzie na pozycji odpowiadającej indeksowi słowa bedzie wartość 1 a na pozostałych bedzie watość zero \cite{onehot}. %\footnote{https://deepai.org/machine-learning-glossary-and-terms/one-hot-encoding 28 maja 2020 12:24}
2020-05-28 00:07:11 +02:00
\subsection{Przykład działania One-Hot Encoding}
2020-05-28 00:07:11 +02:00
2020-06-01 11:38:31 +02:00
Weźmy sekwencję liter w słowie MATEMATYKA. Znajdźmy unikatowe elementy tej sekwencji, oraz nadajmy im unikatowy indentyfikator. Kolejność nie ma znaczenia.
2020-05-28 00:07:11 +02:00
\begin{center}
M - 0,
A - 1,
T - 2,
E - 3,
Y - 4,
K - 5.
\end{center}
2020-05-28 00:07:11 +02:00
2020-06-01 11:38:31 +02:00
Kodując słowo MATEMATYKA, otrzymalibysmy macierz
2020-05-28 00:07:11 +02:00
\[
2020-06-01 11:38:31 +02:00
\mathbb{\boldsymbol{
2020-05-28 00:07:11 +02:00
\begin{bmatrix}
1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 1 \\
0 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\
2020-06-01 11:38:31 +02:00
\end{bmatrix} }}
2020-05-28 00:07:11 +02:00
\]
\subsection{Muzyczne "słowo"}
2020-06-01 11:38:31 +02:00
Na potrzeby dostosowania danych muzycznych do koncepcji słów w zdaniu, zakodowałem pojedyncze słowo muzyczne jako
\[
((zbiór wysokości), długość)
\]
W ten sposób byłem w stanie zakodować podedyjcze nuty oraz akordy.
2020-06-01 11:38:31 +02:00
Akord C-dur składojący się z dzwięków C, E i G o długości ósemki, zapisalibyśmy w następujący sposób.
\[
2020-06-01 11:38:31 +02:00
((60, 64, 67), 0.5)
\]
W ten sposób jesteśmy wstanie kodować melodię w sekwencji słów muzycznych. Tak skonstruowane dane, mają niestety swoje negatywne aspekty. Nie da się w ten sposób zapisać partii, w której zostaje grana nowa nuta gdy poprzednia jeszcze powinna brzmieć. Nasz zapis zakłąda, że melodia, jest grana element po elemencie, i nowy element wymusza zakończenie poprzedniego. Nie przechowujemy również informacji o dynamice melodii (głośności). Rozszerzenie tego zapisu o informacje o głośności nie jest trudne i nie będzie wymagać przebudowania modelu, natomiast zwiększy liczbę możliwych "słów muzycznych" w słowniku i zwiększy złożoność obliczeniową. Zdecydowałem się na niewykorzystanie tych danych w generowaniu muzyki.
\subsection{Konwersja MIDI na sekwencje słów muzycznych}
2020-06-01 11:38:31 +02:00
Powrzechny sposób przechowywania muzyki symbolicznej w formie cyfrowej to pliki *.mid lub *.midi które przechowuja informację o potoku wiadomości protokołu MIDI. Aby odczytać wiadomości plików MIDI, wykorzystałem bibliotekę \pyth{pretty_midi}, która zawiera wiele funcji pozwalających na edycję plików MIDI.
Aby otworzyć pliki midi za pomocą bilbioteli \pyth{pretty_midi} należy skorzystać z poniższej skłądni.
\begin{python}
>>> import pretty_midi as pm
>>> midi_path = 'example.mid'
>>> midi = pm.PrettyMIDI(midi_path)
>>> melody = midi.instruments[0]
>>> melody.notes
[Note(start=18.873909, end=19.186408, pitch=71, velocity=110),
Note(start=19.132529, end=19.471968, pitch=76, velocity=114),
Note(start=19.396538, end=19.768304, pitch=80, velocity=111),
Note(start=19.655158, end=19.951494, pitch=81, velocity=105),
Note(start=19.913779, end=20.226278, pitch=80, velocity=99),
Note(start=20.172399, end=20.452571, pitch=76, velocity=119),
Note(start=20.431020, end=20.624985, pitch=71, velocity=115),
Note(start=20.689640, end=20.975200, pitch=69, velocity=114),
...]
\end{python}
2020-06-01 11:38:31 +02:00
Dzięki bibliotece \pyth{pretty_midi}, plik midi został odczytany i przechwany w objecie \pyth{PrettyMIDI}. Ten objekt posiada atrybut \pyth{instruments}, który jest listą ścieżek pliku MIDI. Objekt ścieżki posiada atrubut \pyth{notes}, który jest listą nut tej ścieżki. Możemy zobaczyć, że biblioteka \pyth{pretty_midi} zamieniła potok sygnałów protokołu MIDI na konkretne nuty posiadajace parametry $start$, $end$, $pitch$ orac $velocity$. Aby otrzymać sekwencję danych w takim formacie w jakim potrzebujemy możemy zastosować na obiekcie \pyth{Instrument} poniższą funkcję.
\begin{python}
def parse_pretty_midi_instrument(instrument, resolution,
time_to_tick, key_offset):
''' arguments: a prettyMidi instrument object
return: a custom SingleTrack object
'''
first_tick = None
prev_tick = 0
prev_note_lenth = 0
max_rest_len = 4.0
notes = defaultdict(lambda:[set(), set()])
for note in instrument.notes:
if first_tick == None:
first_tick = 0
tick = round_to_sixteenth_note(
time_to_tick(note.start)/resolution)
if prev_tick != None:
act_tick = prev_tick + prev_note_lenth
if act_tick < tick:
rest_lenth = tick - act_tick
while rest_lenth > max_rest_len:
notes[act_tick] = [{-1},{max_rest_len}]
act_tick += max_rest_len
rest_lenth -= max_rest_len
notes[act_tick] = [{-1},{rest_lenth}]
note_lenth = round_to_sixteenth_note(
time_to_tick(note.end-note.start)/resolution)
if -1 in notes[tick][0]:
notes[tick] = [set(), set()]
if instrument.is_drum:
notes[tick][0].add(note.pitch)
else:
notes[tick][0].add(note.pitch+key_offset)
notes[tick][1].add(note_lenth)
prev_tick = tick
prev_note_lenth = note_lenth
notes = [(tuple(e[0]), max(e[1])) for e in notes.values()]
if instrument.is_drum:
name = 'Drums'
else :
pm.program_to_instrument_class(instrument.program)
return SingleTrack(name,
instrument.program,
instrument.is_drum,
Stream(first_tick, notes))
\end{python}
Powyższa funkcja w zamienia wartości absolutne czasu, na wartości względne o ustalonej rozdzielczości przez plik MIDI. Dodatkowo zmniejsza szczegółowość, i zaokrągla czas zagrania nuty po szesnastki. Gdy w tym samym momencie, czyli jeśli kilka nut posiada tą samą wartość start, zostają dodane do jednego słowa muzycznego aby utworzyć akord. Pauzy są kodowane jako $-1$. Dodatkowo jeśli pauza trwa dłużej niż takt wtedy zostaje podzielona na mniejsze części o długości \pyth{max_rest_len}. Funkcja zwraca obiekt \pyth{SingleTrack}, który jest obiektem stworzonym aby poza nutami, przechowywać inne istotne informacje na temat ściezki, którą będą istotne w następnych częściach przetwarzania danych. Ostatecznie sekwencje słów muzycznych przechowane są w \pyth{notes}.
\begin{python}
>>> resolution = midi.resolution
>>> time_to_tick = midi.time_to_tick
>>> intrument = melody
>>> single_track = parse_pretty_midi_instrument(intrument,
resolution, time_to_tick, key_offset=0)
>>> single_track.stream.notes
[((-1,), 4.0),
((-1,), 0.5),
((71,), 0.5),
((76,), 0.75),
((80,), 0.75),
((81,), 0.5),
((80,), 0.5),
((76,), 0.5),
((71,), 0.5),
((69,), 0.5),
((68,), 0.5),
((69,), 0.25),
...]
\end{python}
\subsection{Inne aspekty przygotowania danych}
Po odczytaniu danych i konwersji je do pożądanego formatu dane należy oczyścić. W mojej pracy zastowowałem kilka operacji, w celu zwiększenia muzycznego sensu danych.
\subsubsection{Unormowanie skali}
2020-06-01 11:38:31 +02:00
W muzyce istenieje pojęcie skali. Skala jest to zestaw nut, które dobrze ze sobą wspołgrają. Zostało to szerzej opiswane w podrozdziale~\ref{section:skala}. W uczeniu maszynowym powodouje to realny problem, ponieważ piosenki wykorzystują różne skale, i sieć neuronowa będzie preferować wybranie skali częściej używanej. Dodatkowo zmiana skali, nie zmienia drastycznie kontenku muzycznego utworu. Zmiana wysokości przyskich nut, bez zmiany ich względnych interwałów nazywana jest transpozycją. Aby rozwiązać ten problem zaleca się agmentację danych, do wszystkich mozliwych skal. W mojej pracy wykorzystalem jednak inne rozwiązanie. Zamiast rozszerzać zbiór danych, sprowadziłem wszystkie ścieżki muzyczne do jednej skali C. Dzięki temu model przyłoży większą uwagę na rozumienie wzajemnych relacji, zamiast uczyć się pojęcia tonacji skali \cite{survay}.
\subsubsection{Podział na takty}
2020-06-01 11:38:31 +02:00
Długie listy muzycznych słów zostały podzielone na takty (\textit{bars}), o odpowiedniej długości, domyslnie o długości 4, co odpowiada czterem ćwierćnutom. Dzięki temu utwór muzyczny zostanie podzielony na mniejsze sekwencje. Sekwencje te będą posidały różną liczbę elementów, ale będą tak samo długie, w kontekcie muzycznym. Głównym celem takie zabiegu, jest zapewnienie muzycznego sensu sekwenjom. Takt jest naturalnym dla muzyki podziałem dłuższegej pratii na mniejsze.
\subsection{Podział danych na dane wejściowe i wyjściowe}
2020-06-01 11:38:31 +02:00
Na podstawie przetworzonych danych, należy przygotować dane wejściowe $X$ i wyjściowe $Y$ dla sieci neurowonych, aby przeprowadzić proces uczenia. W tym celu będziemy rozważać pary sekwencji $(x, y)$, gdzie $x \in X$ i $y \in Y$. Każda senwencja zawierać bedzie omówione wcześniej słowa muzyczne. W przygotowanym przeze mnie modelu, występują dwa rodzaje sieci neuronowych, sieć generująca oraz sieć akompaniująca.
\subsubsection{Przygotowanie danych dla generatora}
2020-06-01 11:38:31 +02:00
Model generatywny będzie tworzył partie muzyczne, na podstawie poprzeprzednich sekwencji tego samego intrumentu. Weźmy parię muzyczną $G$, która jest uporządkowaną listą elementów $g$ w czasie. Każdy element $g$ jest taktem składających sie ze słów muzycznych.
\[
G = \left[g_1, g_2, g_n \right],
\]
gdzie $n$ jest liczbą taktów w partii muzycznej. Pary $(x_t, y_t)$ tworzymy według poniżej reguły
\[
\begin{array}{c}
x_t = g_t \\
y_t = g_{t+1},
\end{array}
\]
dla $t \in (1, n-1)$.
Dzięki takiemu zdefiniowaniu danych uczacych generator będzie uczył sie jak powinien wyglądać następny takt, na podstawie poprzedniego. W ten sposób będziemy wstanie wykorzystać model, do generowania muzyki bez danych wejściowych. Wątek zostanie rozwinięty w dalejszej części pracy.
2020-06-01 11:38:31 +02:00
Implementacja w pythonie:
\begin{python}
def get_data_seq2seq_melody(self, instrument_class,
x_seq_len=4):
'''return a list of bars with content for every track
with given instrument class for melody generaiton
x_seq_len and y_seq_len
x previous sentence, y next sentence of the same melody line
'''
instrument_tracks =
self.tracks_by_instrument[instrument_class]
for track_index in instrument_tracks:
bars = self.tracks[track_index].stream_to_bars()
bars_indexes_with_content =
get_bar_indexes_with_content(bars)
bars_with_content =
[bars[i] for i in get_bar_indexes_with_content(bars)]
x_seq = []
y_seq = []
for i in range(len(bars_with_content)-x_seq_len-1):
_x_seq =
[note for bar in bars_with_content[i:i+x_seq_len]
for note in bar]
_y_bar = bars_with_content[i+x_seq_len]
x_seq.append(_x_seq)
y_seq.append(_y_bar)
return x_seq, y_seq
\end{python}
\subsubsection{Przygotowanie danych dla akomaniamentu}
2020-06-01 11:38:31 +02:00
Model akompaniujący natomiast, będzie na podstawie partii jednego tworzyć partię na nowy instrument, dla tego samego kroku czasu.
Niech $G, B$ będą sekcjami muzycznymi różnych instrumentów tej samej długości oraz niech
\[
\begin{array}{c}
G = \left[g_1, g_2, ..., g_k \right] \\
B = \left[b_1, b_2, ..., b_k \right],
\end{array}
\]
wówczas pary dla zbioru uczącego tworzymy w nastęujący sposób
\[
\begin{array}{c}
x_t = g_t \\
y_t = b_t,
\end{array}
\]
dla $t \in (1, k)$.
Istotne jest aby każdy element ze zbioru taktów partii $B$ był rzeczywistą aranżacją tego instrumentu dla taktów partii $G$ oraz aby między elementami $g_t$ oraz $b_t$ była muzyczna relacja.
Implementacja przedstawionej techniki w pythonie.
\begin{python}
def get_data_seq2seq_arrangment(self, x_instrument,
y_instrument, bars_in_seq=4):
'''this method is returning a sequances of given lenth
by rolling this lists of x and y for arrangemt generation
x and y has the same bar lenth, and represent the
same musical phrase played mb difrent instruments (tracks)
'''
x_seq = []
y_seq = []
x_bars, y_bars =
self.get_common_bars_for_every_possible_pair(
x_instrument, y_instrument)
for i in range(len(x_bars) - bars_in_seq + 1):
x_seq_to_add =
[note for bar in
x_bars[i:i+bars_in_seq] for note in bar ]
y_seq_to_add =
[note for bar in
y_bars[i:i+bars_in_seq] for note in bar ]
x_seq.append(x_seq_to_add)
y_seq.append(y_seq_to_add)
return x_seq, y_seq
\end{python}
\subsection{Inne apekty przygotowania zbioru uczącego}
\subsubsection{Oczyszczenie danych}
W przygotowaniu danych dla modelu, ważne jest aby dostarczone dane były jak najlepszej jakości. W tym celu zastosowałem jeszcze deduplikację par $(x, y)$ oraz usunąłem takty które nie zawierały muzycznego kontentu.
\subsubsection{Wybór programu dla intrumentu}
Podczas etapu ekstracji danych z plików MIDI poza informacjami o muzyce, zapamiętuję również informacje o programie partii muzycznej. Każda ścieżka MIDI przechowuje informacje o intrumencie (brzmieniu) danej partii. Istenieje 128 rówznych programów, dla zmniejszenia szczegółwości na potrzeby modelu wyróżniam 16 intrumentów zgodnie z grupą do jakiej należą w podziale General MIDI. Dla każdej z grup, sprawdzam jaki program został najczęściej wykorzystywany i zapisuję go na przyszłość, aby móc wygenrowanej muzyce przy kompilacji do MIDI zdefiniować brzmienie intrumentu zgodnie z najczęściej wykorzystywanym w zbiorze MIDI, który został wykorzystany do stworzenia zbioru uczacego dla modelu.
\subsubsection{Melodia}
Dodatkowym elementem procesu ektracji danych jest znaleniezie ścieżek melodii przewodnich w plikach MIDI. Ścieżki tego typu zamiast być oznaczone nazwą grupy intrumentów do której należą oznaczone są nazwą \pyth{Melody}. Melodia jest kategorią ścieżek z podziału ze względu na rolę partii w utworze, zamiast na intrument. Istnieją też inne role intrumentów, jednak często rola jest w pewnym sensie definiowana przez instrument. Nie jest to zasada, bardziej prawidłowość w muzyce. Uznazłem że wydobycie ten informacji na temat ścieżki nada więcej muzycznego sensu danym.
Aby sprawdzić czy dana ścieżka jest melodią, zastosowałem poniższą funkcję:
\begin{python}
def check_if_melody(self):
'''checks if Track object could be a melody
it checks if percentage of single notes in
Track.stream.notes is higher than treshold
of 90 and there is at least 2 notes in bar per average
'''
events = None
single_notes = None
content_lenth = None
for note in self.stream.notes:
if self.name not in ['Bass','Drums']:
events = 0
content_lenth = 0
single_notes = 0
if note[0][0] != -1: # if note is not a rest
events += 1
content_lenth += note[1]
if len(note[0]) == 1: # if note is a single note
single_notes += 1
if events != None:
if events == 0 or content_lenth == 0:
return False
else:
single_notes_rate = single_notes/events
density_rate = events/content_lenth
if single_notes_rate >= 0.9 and density_rate < 2:
self.name = 'Melody'
return True
else:
return False
else:
return False
\end{python}
Funkcja sprawdza liczbę pojedynczych nut i akordów w ścieżce, oraz zagęszczenie nut w takcie. Jeśli jest więcej niż 90 pojedynczych nut w ścieżce oraz jest średnio więcej nut w takcie niż dwie, wtedy uznaję że partia intrumentalna utworu jest melodią.
\section{Tranformacja danych dla modelu}
2020-05-28 00:07:11 +02:00
\section{Definicja modelu}
2020-06-01 11:38:31 +02:00
\section{Uczenie modelu}
\section{Generowanie muzyki przy pomocy wytrenowanego modelu}
2020-05-28 00:07:11 +02:00
\section{Wyniki}
2020-05-28 00:07:11 +02:00
\section{Wnioski}
2020-05-28 00:07:11 +02:00
2020-04-06 19:23:25 +02:00
\chapter{Podsumowanie}
Ostateczne wnioski, czy muzyka generowana komputerowa da się lubić? Czy to pozytywnie wpłynie na przemysł muzyczny? Tak i nie. Może złużyć jako inspiracja dla muzyków, proces wspierający. Z drugiej strony może obnizy koszty produkowania muzyki pop, która i tak jest już bardzo powtarzalna. Czy sieci neuronowe nauczą się produkować Hity?
\begin{thebibliography}{99}
\bibitem{survay} Briot, J.P., Hadjeres, G., Pachet, F.D. (2019): {\em Deep Learning Techniques for Music Generation - A Survey. arXiv:1709.01620v3}
\bibitem{onehot} DeepAI (2019): {\em One Hot Encoding}
\bibitem{handson} Géron, A. (2019): {\em Hands-on machine learning with scikit-learn, keras and TensorFlow. O'Reilly.}
\bibitem{deep_learning} Goodfellow, I., Bengio, Y., Courville, A. (2016): {\em Deep Learning. MIT Press.}
\bibitem{seq2seq} Kostadinov, S. (2019): {\em Understanding Encoder-Decoder Sequence to Sequence Model}
\bibitem{analiza_mat} Krysicki, W., Włodarski, L. (1999): {\em Analiza matematyczna w zadaniach, PWN.}
\bibitem{statystyka} Sobczyk, M. (2006): {\em Statystyka. UMCS.}
\bibitem{tempos} Swinney, A. (2020): {\em What is a tempo marking?}
\bibitem{deep_learning_2} Zocca, V., Spacagna, G., Slater, D., Roelants, P. (2018): {\em Deep Learning. Uczenie głębokie z językiem Python. Helion.}
% \footnote{Statystyka, Mieczysław Sobczyk s.179}
% \footnote{źródlo: Analiza matematyczna, Krysicki Włodarski, s.187 }
% \footnote{Deep Learning techniques for music geneation - A survay s.44}
% \footnote{Deep Learning Book, s.66}
% \footnote{Deep Learning techniques for music geneation - A survay s.63}
% \footnote{Hands-on machine learning with scikit-learn, keras and TensorFlow s.497}
% \footnote{https://towardsdatascience.com/understanding-encoder-decoder-sequence-to-sequence
% -model-679e04af4346 26.05.2020 14:58}
% \footnote{źródlo: http://www.classicalmusiccity.com/search/article.php?vars=446/
% Basic-Tempo-Markings.html 5 kwietnia 19:37
% }
% \footnote{https://deepai.org/machine-learning-glossary-and-terms/one-hot-encoding 28 maja 2020 12:24}
\end{thebibliography}
\printindex
\end{document}