LSTM – przewidywanie tekstu (sieci neuronowe, część 4)

Czytanie zajmie Ci około 9 minut

Czy da się napisać tekst za pomocą sieci neuronowych?

Skorzystamy (podobnie jak w przypadku sieci CNN w poprzednich częściach) z TensorFlow opakowanego w Keras. Załadujmy wiec potrzebne pakiety i ustalmy kilka stałych dla kolejnych elementów kodu:

Za dane treningowe posłuży nam tekst jednej z książek Remigiusza Mroza o Chyłce i Zordonie (o poprzedniego wpisu gdzie analizowałem te książki zdążyłem przeczytać wszystkie części, a już za chwile kolejna część :) – Kasacja. Aby przyspieszyć cały proces wybierzemy tylko cześć tekstu (max_lines powyżej), co jednocześnie pozwoli na ograniczenie potrzebnej ilości pamięci.

Przygotowanie danych

Mamy tekst o długości 294902 znaków. Sensowne wyniki zaczynają się przy rozmiarze co najmniej 100 tysięcy znaków – im więcej tym lepiej :)

Unikalnych znaków mamy zaś 61. W kolejnym kroku podzielimy tekst na okna i budujemy listę ciąg – następny znak.

Jakie okna? Dla przykładowego ciągu ala ma kota i okna o szerokości pięciu znaków mamy kolejno:

  • w oknie pierwszym: ala m i następny znak a
  • w oknie drugim: la ma i następny znak to spacja
  • w oknie trzecim: a ma (spacja na końcu) i k jako następny znak
  • i tak dalej

Przykładowe wyniki całej tej transformacji wyglądają następująco:

Wektoryzacja

O co w tym chodzi?

Każdy znak zapisujemy jako one-hot-encoder – czyli za każdym razem, dla każdego znaku trzymamy wektor długości liczby unikalnych znaków z zapalonym odpowiednim znakiem. Obrazek wyjaśnia to chyba najlepiej:

Potrzebujemy w ten sposób zakodować wszystkie dane: zarówno cechy (X) jak i odpowiedzi (Y).

Na obrazku poniżej mamy okno 5-znakowe i na tym się skupmy na chwilę. Dla każdego ciągu znaków wybranego przez okno przygotowujemy macierz pięciu wierszy i 27 kolumn (litery a-z plus spacja) będącą cechą. W macierzy tej w sposób opisany wyżej kodujemy kolejne literki z okna. Odpowiedzią jest jedna litera – również zakodowana (w wektorze 27-elementowym).

Odpowiedni kod przygotowujący macierze X i Y poniżej:

Można napisać to na wiele sposobów. Sieci neuronowe jednak najlepiej działają ze zmiennymi typu one-hot-encoder (a więc wektorami z zapalonymi poszczególnymi wartościami) stąd taki układ. Mniej pamięciożerne byłoby zapisanie chociażby w odpowiedzi numeru literki, ale tak sieć nie zadziała :) Zadziała jakieś drzewo czy las losowy.

Dobra, budujmy ten model!

Model 1

Na początek krótkie wprowadzenie – co robi siec rekurencyjna (RNN) typu LSTM (Long Short Term Memory)? Świetne wytłumaczenia znajdziecie w dwóch postach (angielskich):

W dużym uproszczeniu siec bierze do obliczenia wag nie tylko stan (cechy i odpowiedz na nie) w chwili Xt0 oraz Yt0 (aktualnej) ale tez w chwilach poprzednich Xt-1 oraz Tt-1. Ile tych poprzednich chwil jest brane pod uwagę?

To ustalamy w parametrach warstwy:

layer_lstm(hidden_nodes, input_shape = (timesteps, input_dim)))

gdzie:

  • hidden_nodes określa liczbę neuronów w warstwie LSTM. Im większa liczba neuronów tym silniejsza siec. Oczywiście rośnie też czas trenowania sieci
  • timesteps – liczba kroków czasowych, które chcemy uwzględnić. Na przykład jeśli chcemy sklasyfikować zdanie, będzie to liczba słów w zdaniu. W naszym wypadku jest to liczba znaków (rozmiar okna)
  • input_dim określa liczbę cech – na przykład długość wektora w którym zapisujemy słowo (w naszym przypadku tyle ile mamy unikalnych znaków w danych treningowych – 61)

Upakujmy więc to w model:

Trenujemy model i patrzymy na jego osiągi:

Wynik treningu sieci:

  • loss na koniec: 1.2982
  • val_loss na koniec: 1.8858

Widzimy przede wszystkim, że val_loss jest stabilne (lub rośnie) kiedy loss spada – to oznacza przeuczenie sieci. Za chwilę spróbujemy zbudować inna architekturę i poradzić sobie z tym problemem. Wcześniej jednak

Przewidywanie tekstu

Na początek zbudujemy funkcję, która na podstawie podanego ciągu znaków przewidzi kolejne n-znaków zgodnie z modelem.

Przewidujemy przykładowy tekst – 100 nowych znaków:

Spróbujmy z innym tekstem początkowym:

Co ciekawe – ciąg wejściowy może być całkowicie bzdurny, po kilku literach tekst prostuje się sam:

Znaczenie ma całkowity układ liter, nie tylko na przykład ostatni znak – porównajcie:

Widzimy tutaj ciekawa rzecz: po kilku nowych literach tekst zaczyna się powtarzać. Oznacza to, że siec sobie trochę nie poradziła i zapętliła się. Można przygotować model jeszcze raz, tym razem zamiast neuronów dać ich na przykład dwa razy więcej.

Model 2

Dodajmy jeszcze jedna warstwę Dense oraz zmieńmy funkcję optymalizującą (nie powinno się tego robić jednocześnie – nie wiadomo która zmiana wpłynie na wyniki).

Trenujemy model:

Wynik treningu sieci:

  • loss na koniec: 1.5042
  • val_loss na koniec: 1.8949

Jak widać linie loss i val_loss są teraz nieco bardziej zbieżne, ale w dalszym ciągu nie pozbyliśmy się overfittingu. Można próbować z innymi wartościami dla dropout (dobrze wypadło 0.5) lub innym rozmiarem warstw dense. Zostańmy przy tym co mamy – w końcu chodzi o ideę.

Przewidujemy przykładowy tekst – weźmy te same ciągi co poprzednio, tym razem może trochę więcej znaków?

Spróbujmy z innym tekstem początkowym i większa długością przewidywanego tekstu:

Znowu widzimy zapętlenie po kilkudziesięciu znakach.

Możemy zamiast przewidywać kolejne litery przewidywać całe słowa. Wówczas całe przygotowanie danych trzeba przeprowadzić inaczej – rozbić tekst na unikalne słowa (i to będzie baza). Słów mamy więcej niż znaków – długość wektora (matematycznie mówiąc: wymiar przestrzeni w której opisujemy wektor) będzie odpowiednio dłuższa. W wyniku przewidywania kolejnych slow nie dostaniemy czegoś co wygląda podobnie do slow (jak w powyższych przykładach) a konkretne słowa. Czy sensownie powiązane ze sobą? Zapewne nie do końca, szczególnie dla bardziej złożonych zdań. Warto tez taka siec trenować ogromna ilością danych (tak, aby było jak najwięcej kombinacji rożnych slow ze sobą).

Przewidywanie znaków daje w wyniku pozorny tekst w naszym języku – siec odpowiada kolejnymi znakami, ale nie wie czy utworzone ciągi coś znaczą. To tylko zbitki liter (i dodatkowo znaków specjalnych typu spacje czy znaki przestankowe). W przypadku przewidywania słów mielibyśmy prawdziwe słowa, ale zapewne bezsensowny ich układ, nie dający sensownych zdań. Może ktoś z Was trenował tego typu sieci o większej architekturze i zechce podzielić się wynikami?

Inne podejście do tego samego problemu to łańcuchy Markova. Idea polega mniej więcej na tym samym:

  • dla każdego ciągu znaków (lub słów) w oknie sprawdzamy jaki znak (lub jakie słowo) występuje jako kolejny
  • dla każdego identycznego ciągu w oknie mamy rożne prawdopodobieństwa następników
  • przy generowaniu następnika bierzemy odpowiedni, według prawdopodobieństwa
  • nowym oknem jest ciąg uzupełniony o dodany znak (lub słowo)

Problemem jest sytuacja w której nie mamy odpowiednika w danych treningowych – w łańcuchu Markova nie wiemy co zrobić (nie było takiego przypadku w danych treningowych), siec LSTM jakoś sobie z tym radzi (automagicznie).

Do czego można jeszcze wykorzystać sieć LSTM? Do przewidywania w szeregach czasowych (na przykład notowań na giełdzie). Działamy podobnie jak w przypadku odgadywania kolejnych liter: bierzemy wektor wartości (kilku, kilkunastu) do chwili t-1 jako cechy (X) i kolejna wartość z chwili t0 jako odpowiedź. Przesuwamy takie okno przez cały szereg i z tak zbudowanych danych trenujemy sieć. Kilka zastosowań znajdziecie pod linkami:

Dodaj komentarz