Ciekawy zbiór danych przedstawiony na wykresie bubblechart

Autorem wpisu jest Wojciech Łabuński - doktorant na Wydziale Budowy Maszyn i Lotnictwa Politechniki Rzeszowskiej. W tym wpisie przedstawi krótki zarys prac nad wykresem bąbelkowym zgłoszonym do konkursu „Liczbowy zawrót głowy”. Zadanie konkursowe polegało na znalezieniu ciekawego zbioru danych, który można przedstawić na wykresie bubblechart lub geographic buble chart.

Wstęp

Nazywam się Wojciech Łabuński i jestem doktorantem na Wydziale Budowy Maszyn i Lotnictwa Politechniki Rzeszowskiej. Na co dzień zajmuję się robotyką przemysłową oraz przetwarzaniem obrazów z wykorzystaniem głębokich sieci neuronowych. Interesuję się również wykorzystaniem głębokiego uczenia w przetwarzaniu tekstu, które jest głównym tematem niniejszego opracowania.
W tym wpisie chciałbym przedstawić krótki zarys moich prac nad wykresem bąbelkowym zgłoszonym do konkursu „Liczbowy zawrót głowy”. Zadanie konkursowe polegało na znalezieniu ciekawego zbioru danych, który można przedstawić na wykresie bubblechart lub geographic buble chart.

Po sprawdzeniu dokumentacji [1] okazało się, że potrzebujemy trzech zmiennych.

bubblechart(x,y,sz)

Dwie pierwsze (x, y) określają położenie bańki na płaszczyźnie,
a trzecia (sz) jak duża ona ma być.

W pierwszym odruchu zacząłem sprawdzać bazy danych Głównego Urzędu Statystycznego, jednak brak API dla MATLABa zniechęcił mnie do dalszego poszukania tam danych (tutaj warto dodać, że moim założeniem było korzystanie tylko z narzędzi MATLABa, zarówno przy przygotowaniu zbioru danych, jak i potem przy generowaniu wykresu). Ostatecznie zdecydowałem,
że wykorzystam wiedzę na temat NLP i jednocześnie sprawdzę, jak działa Text Analitics Toolbox w MATLABie. Moim kolejnym krokiem było znalezienie ciekawego tekstu, który byłby dostępny jako multimedialny zasób. Skorzystałem więc ze strony Wolne Lektury [2], skąd mogłem pobrać teksty polskich książek. Kryterium wyboru było proste – im dłuższy tekst, tym lepiej (liczyła się dla mnie również jego spójność). Wybór padł na klasykę gatunku - „Trylogię” Henryka Sienkiewicza.

Mamy dane i co teraz? Na początek należy zastanowić się co z nimi zrobić, aby otrzymać trzy zmienne.
Zanim jednak przejdę do opisu rozwiązania i kolejnych czynności wykonywanych na potrzeby projektu, wyjaśnię pokrótce czym jest NLP.

Co to jest NLP?

Pozwólcie, że wyprzedzę Waszą ciekawość i sprawdzę co znajdziemy na temat tego hasła w Wikipedii. Internetowa encyklopedia wskazuje, że „przetwarzanie języka naturalnego (ang. natural language processing, NLP) to interdyscyplinarna dziedzina, łącząca zagadnienia sztucznej inteligencji i językoznawstwa, zajmująca się automatyzacją analizy, rozumienia, tłumaczenia i generowania języka naturalnego przez komputer.” [3].
Przetwarzanie języka naturalnego, czyli takiego, którym posługują się ludzie a nie maszyny, odpowiada nam na pytanie: jak analizować słowa, które są zbiorem liter, a nie liczb? Mówiąc jeszcze prościej – jak zamienić słowa na liczby, bo właśnie z liczbami komputer już sobie poradzi. Z pomocą przyszedł Tomas Mikolov, który wraz ze swoim zespołem (pracując dla Google’a) w 2013 roku opublikował dwie prace [4,5]. Zespół zrewolucjonizował naukę o języku, proponując algorytm, który przetwarza słowa w tekście i określa je jako wektory o zadanej długości.

Poniżej przedstawiam kilka reprezentacji wektorowych słów, które znalazły się w analizowanym przeze mnie korpusie (tak nazywa się zbiór analizowanych słów w NLP).

Jak widzimy słowo możemy przedstawić za pomocą dowolnej ilości liczb. Im dłuższy taki wektor, tym więcej zależności między słowami możemy przedstawić.

Dla porównania dodaję jeszcze dwa różne wektory dla słowa ‘rycerz’. Jeden ma 400 elementów, a drugi 10. Patrząc na oba musimy pamiętać, że poszczególne liczby w obu wektorach nie są swoimi odpowiednikami. Rozpatrując liczby -0.1057 i -0.0059 - obie są na tym samym miejscu, ale określają najprawdopodobniej zupełnie inną cechę słowa ‘rycerz’. Jaką? Tego nie wiemy, bo nie mamy dostępu do interpretacji poszczególnych liczb.

Krok po kroku

Całość prac wykonanych na potrzeby tego projektu w MATLABie podzieliłem na cztery części:

  • wczytanie tekstu,
  • trenowanie modelu,
  • przygotowanie tablicy frekwencyjnej,
  • wygenerowanie wykresu.

Krok 1: Wczytanie tekstu

Przed rozpoczęciem jakichkolwiek prac wczytujemy tekst do przestrzeni roboczej. Na szczęście Text Analitics Toolbox ułatwia nam pracę przy pomocy jednej funkcji- extractFileText. Otwiera ona nie tylko tradycyjne pliki tekstowe w formacie TXT, ale również dokumenty Microsoft Word oraz pliki HTML i PDF.

filename1 = "D:\#DANE\ksiazki\sienkiewicz\potop-tom-pierwszy.txt";
str1 = extractFileText(filename1);
textData1 = split(str1,newline);

W ten sposób otrzymujemy jedną zmienną, zawierającą cały tekst. Warto ją sobie rozdzielić na większą liczbę zmiennych, dla łatwiejszej analizy. Dzięki funkcji split, otrzymamy tablicę, w której komórkach znajdziemy poszczególne linie naszego oryginalnego dokumentu. (Tworzyliśmy nową zmienną, przy każdym wystąpieniu znaku nowej linii – ‘\n’.)

Rys. 1. Fragment tablicy zawierającej tekst podzielony na linie

Analogicznie przetwarzamy pliki zawierające wszystkie części „Trylogii” Sienkiewicza i zapisujemy je do jednej zmiennej. Następnie możemy już przejść do wyczekiwanej pracy z danymi.

Krok 2: Preprocessing

Pierwszym i podstawowym krokiem w pracy z danymi powinno być uporządkowanie i jak najbardziej dokładne wyeliminowanie szumu. W tym celu użyjemy funkcji preprocessText, którą możecie łatwo znaleźć w dokumentacji MATLABa i dostosować do swoich potrzeb.

function documents = preprocessText(textData, stop_words)
    % funkcja z dokumentacji Text Analytics Toolbox
    documents = tokenizedDocument(textData);
    documents = lower(documents);
    documents = erasePunctuation(documents);
    documents = removeWords(documents,stop_words);
    documents = removeShortWords(documents,2);
end

Jak widać składa się ona z kilku funkcji wewnętrznych, po kolei przetwarzających nasz tekst. Na początek – tokenizacja, czyli rozbijanie każdego elementu naszej tablicy z tekstem na pojedyncze słowa (tokeny). Wystarczy, że użyjemy funkcji tokenizeDocument, która zrobi to za nas. Następnie otrzymanym w ten sposób tokenom zmieniamy wielkość liter – we wszystkich słowach zamieniamy duże litery na małe, dodatkowo usuwamy interpunkcję, ponieważ w tym przypadku nie będziemy jej potrzebować. W kolejnym kroku eliminujemy słowa ze „stop listy”, tzw. stopwords. Stopwords to słowa, które nie wnoszą do naszej analizy żadnej wartości, jedynie mogłyby powodować zaszumienie. Jest ich w tekście najwięcej, niezależnie od języka, na którym pracujemy. Są to np. wszystkie spójniki („ale”, „i”, „natomiast”) czy zaimki („on”, „ja”, „jego”, „twoje”). W tym celu wykorzystujemy listę przygotowaną i udostępnioną przez Marcina Bielaka na GitHubie. [6] Dodatkowym wyzwaniem jest konieczność poszerzenia listy o słowa przeszkadzające nam w analizie, a pojawiające się jako walor archaiczny, przykładowo: „waćpan”, „takoż”, „przecie”. Przy dodaniu ich ręcznie do listy wybieram z puli 20 najczęściej powtarzających się te, które moim zdaniem nic nie wnosiły i tylko powodowały bałagan.

documents = preprocessText(textData,stop_words);
bag = bagOfWords(documents);
bag = removeEmptyDocuments(bag);

Na koniec z utworzonych i oczyszczonych tokenów tworzymy worek słów (bag of words). Taki model zawiera słowa, bez względu na znaczenie i kolejność występowania w zdaniu, jednak zachowuje on informacje o częstości występowania danego słowa w całym dokumencie, co później przyda nam się przy tworzeniu „bąbelków”.

Krok 3: Trenowanie modelu

Model reprezentacji wektorowej w MATLABie generujemy przy pomocy instrukcji trainWordEmbedding. Przeanalizujmy teraz po kolei opcje, z których korzystamy.

embed = trainWordEmbedding(documents, "Dimension", 400, "LossFunction","ns", "NGramRange",[1 3], "NumEpochs",40, "Window", 6, "Verbose",1)
  • Oprócz podania listy tokenów (u nas to tablica ‘documents’), musimy podać, ile wymiarów ma mieć dowolne słowo, czyli iloma liczbami będziemy opisywali każde słowo. Żeby to miało sens, liczba musi być taka sama dla każdego słowa - w tym wypadku zdecydowałem się na 400, aby móc uchwycić więcej niuansów w znaczeniu. Ogólnie rzecz biorąc, im więcej wymiarów, tym dokładniejszy model. Należy również pamiętać, że zbytnie rozbudowanie wymiarowości może być niebezpieczne, gdzie w konsekwencji wydłużeniu ulegnie czas obliczeń, albo możliwości sprzętu, na którym pracujemy ulegną przekroczeniu.
  • Kolejny element to funkcja minimalizacji błędu - w tym wypadku wybieramy negative sampling. Ten algorytm (w dużym uproszczeniu) pomaga w uczeniu modelu zmieniając wagi przypisywane danemu słowu, nie tylko dla poprawnych przykładów, ale również dla kilku losowo wybranych błędnych.
  • Ngram to połączenie n wyrazów, np. „mały rycerz” – bigram (dwa słowa). W tym przypadku, model bierze pod uwagę pojedyncze słowa, bigramy oraz trigramy (trzy słowa).
  • Ilość epok, czyli pełnych pętli uczenia definiujemy parametrem NumEpochs, na potrzeby projektu wybrałem 40.
  • Okno – Window, opisuje jak wiele sąsiadujących słów, które w danym momencie analizujemy, bierzemy pod uwagę.
  • Parametr Verbose pozwala nam zdecydować, w jaki sposób będą wyświetlane postępy w trakcie treningu.   

Krok 4: Przygotowanie tablicy

Musimy jeszcze przygotować funkcję zwracającą zadaną liczbę najczęściej powtarzających się wyrazów w podanym „worku słów”. Na wyjściu dostajemy tablicę: [słowo, ilość wystąpień].

function [tbl_array, tbl_array_counts] = word_count_2_array(num_counts, bag)
    % funkcja przygotowująca tablicę z num_counts najczęściej
    % powtarzających się słów w zbiorze słów
    
    tbl= topkwords(bag,num_counts);
    tbl_array = table2array(tbl(1:num_counts,1));
    tbl_array_counts = table2array(tbl(1:num_counts,2));

Dla niezaburzania odbioru przedstawiamy na wykresie 80 najczęściej powtarzających się słów.

[tbl_array, tbl_array_counts] = word_count_2_array(80, bag);

Krok 5: Utworzenie wykresu bąbelkowego

Funkcja bubblechart generuje wykres bąbelkowy – dla tej funkcji należy podać trzy wymiary, z czego dwa odpowiadające naszej reprezentacji wektorowej. Przypomnę tylko, że nasze frazy przedstawiamy jako wektory o 400 wymiarach. Jak więc możemy sprowadzić to do wymaganych trzech? Z pomocą przychodzi nam algorytm t-SNE (t-distributed stochastic neighbour embedding) - stochastyczna metoda porządkowania sąsiadów w oparciu o rozkład t. Nie będziemy w tym miejscu wchodzić w teorię i zastanawiać się jak działa. Na daną chwilę powinna nam wystarczyć informacja, że korzystając z niej możemy zredukować wymiarowość naszej reprezentacji i sprowadzić ją na płaszczyznę (do dwóch wymiarów). Trzecim wymiarem potrzebnym do utworzenia bąbelków jest liczba wystąpień danego słowa. Warto zwrócić uwagę na to, że metoda
t-SNE jest metodą probabilistyczną i za każdym wywołaniem funkcji położenie bąbelków względem osi układu współrzędnych będzie trochę inne. Jednak nie powinno nam to przeszkadzać, gdyż potrzebujemy zachować zawsze tylko tę samą odległości między słowami. Realizujemy to ustawiając parametr Distance na cosine w momencie deklarowania funkcji tsne(). Tym samym ustawiamy normę cosinusów, dzięki której możemy przedstawić podobieństwo między słowami, a nie różnicę w wielkości między ich wektorami.

Function bubble_generate(embed, tbl_array, tbl_array_counts)
    % funkcja generujaca wykres bąbelkowy na podstawie reprezentacji
    % wektorowej słów z wykorzysteniem algorytmu t-SNE  mniejszającego
    % wymiarowość reprezentacji do dwóch
    
    V = word2vec(embed,tbl_array);
    XY = tsne(V, “Distance”,”cosine”, “Algorithm”,”exact”, “Standard-ize”,true);

    bubblechart(XY(:,1),XY(:,2),tbl_array_counts, “SelectionHighlight”,”on”)
    bubblesize([25 85]);
    hold on
    text(double(XY(:,1)),double(XY(:,2)),tbl_array)
    title(‘Podobieństwo między 80 najczęściej występującymi słowami i częstość ich występowania w Trylogii H. Sienkiewicza’)
    xlabel(‘t-SNE: wymiar 1’);
    ylabel(‘t-SNE: wymiar 2’);
    bubblelegend(‘Częstość występowania słowa’,’Location’,’eastoutside’);
    hold off
end

Dodatkowo warto pamiętać, że korzystając z metody t-SNE przy redukcji wymiarowości, nie jesteśmy w stanie jasno zinterpretować do czego odnoszą się osie układu współrzędnych, gdyż na nich zawierają się zależności 400 wymiarów, które uprościliśmy.

Rys. 2. Gotowy wykres bąbelkowy przedstawiający 80 najczęściej występujących słów w tekście

Wynikiem naszej funkcji jest wykres bąbelkowy. Na płaszczyźnie mamy zawarte odwzorowanie zależności pomiędzy słowami, a wielkość bąbelka prezentuje ilość wystąpień słowa. Im bliżej siebie znajdują się bąbelki ze słowami, tym bardziej ich znaczenie jest zbliżone, a powiązania większe.

Analiza otrzymanego wykresu

Tyle o wykonaniu samego zadania i wygenerowaniu wykresu. Skoro już go mamy, to może na koniec zatrzymamy się przy nim i zastanowimy się, co właściwie otrzymaliśmy.

Już na pierwszy rzut oka możemy zauważyć kilka zbiorów słów, które w są w jakiś sposób ze sobą związane. Np. noc-dzień, Andrzej-Kmicic, Zagłoba-Wołodyjowski-Skrzetuski, mały-rycerz, twarz-oczy-serce-ręce-krwi; trochę dalej od tego klastra znajduje się głowa i głos.

Rys. 3. Przykładowe pary powiązanych ze sobą słów

Zwróćmy uwagę, że jedynym imieniem żeńskim jakie się pojawia jest „Basia” i jest ono położone w okolicy słów określających części ciała. Algorytm Word2Vec bierze pod uwagę otoczenie danego słowa, stąd możemy wywnioskować, że to imię pojawia się najczęściej w okolicy opisów części ciała.

Rys. 4. Większy klaster bąbelków, dotyczący części ciała

Algorytm odkrył również powiązanie pomiędzy imionami i nazwiskami: Michał Wołodyjowski, Andrzej Kmicic. Zauważył nawet pewne bliskie połączenie pomiędzy Zagłobą, Skrzetuskim i Wołodyjowskim. Ma to sens – wzięliśmy pod uwagę całą „Trylogię”, a wspomniane przeze mnie postaci przewijają się przez wszystkie jej części, najczęściej obok siebie. Co ciekawe, „Zagłoba” to słowo, które ma największą liczbę wystąpień w całym tekście (oczywiście po odsianiu stopwords).

Rys. 5. Klastry zawierające imiona i nazwiska głównych bohaterów występujących w „Trylogii”

Możemy łatwo zauważyć, że w całym wykresie pojawia się dość poważny problem. Mamy szum w postaci tych samych słów odmienionych przez przypadki, tryby lub osoby, np. pytał-spytał, twarz-twarzy. Jest to związane z tym, że Text Analitics Toolbox nie obsługuje języka polskiego. Jest dostosowany jedynie do angielskiego, niemieckiego, japońskiego i koreańskiego, więc niektóre z funkcji, które polegają na wykorzystaniu konkretnych cech danego języka, nie są dla nas dostępne.

Rys. 6. Obszar czasowników o podobnym znaczeniu

Nie możemy tych słów zwyczajnie przefiltrować, jak zrobiliśmy to w przypadku stopwords. W tym miejscu przydałaby nam się funkcja dokonująca lematyzacji słów. Co oznacza ten termin? Lematyzacja to sprowadzenie słowa do wersji podstawowej, pozbywając się wszystkich końcówek charakterystycznych dla czasów, osób itp. Dzięki temu nie mielibyśmy na wykresie klastrów takich jak głowa-głowy-głowę, czy kmicic-kmicica, tylko ostatecznie wyrazy głowa i kmicic. Pozwoliłoby nam to wziąć pod uwagę więcej niepowtarzających się słów. Niestety dla języka polskiego nie mamy w MATLABie funkcji lematyzującej. Moglibyśmy wykorzystać lematyzatory opublikowane przez polskich badaczy języków lub wykorzystać modele dostępne w języku Python np. te z biblioteki spacy [7], jednak założyłem, że w tym wypadku nie korzystamy z żadnych dodatkowych, zewnętrznych narzędzi.

I to by było na tyle. Dziękuję za uwagę i możliwość podzielenia się swoim pomysłem. Mam nadzieję, że otrzymany wykres i wpis na temat jego przygotowania okazał się dla Was interesujący.

W razie pytań możecie śmiało kontaktować się ze mną poprzez mail: wojciech.labunski[at]gmail.com oraz na >>LinkedIn

Linki

  1. https://www.mathworks.com/help/matlab/ref/bubblechart.html
  2. https://wolnelektury.pl/
  3. https://pl.wikipedia.org/wiki/Przetwarzanie_j%C4%99zyka_naturalnego
  4. https://arxiv.org/abs/1301.3781
  5. https://arxiv.org/abs/1310.4546
  6. https://github.com/bieli/stopwords
  7. https://spacy.io/api/lemmatizer
(Visited 181 times, 1 visits today)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *