Spark: czemu jedna akcja tworzy wiele jobów?

Spark: czemu jedna akcja tworzy wiele jobów?

Zgłębiając kwestie wydajnościowe zauważyłem, że dzieje się coś dziwnego: jedna akcja generuje wiele jobów. Postanowiłem to sprawdzić i opisać tutaj:-). Śmiało, częstuj się. A jeśli artykuł okaże się przydatny – podziel się nim na LinkedIn… czy gdziekolwiek chcesz.

Kawka w dłoń i ruszamy!

Podstawy – jak działa aplikacja sparkowa?

Bardzo często mówiąc o tym, że piszemy w sparku, mówimy “muszę napisać joba sparkowego”, “mój job szedł 30 godzin!” i inne.

Cóż… według nomenklatury sparkowej, nie jest to poprawne. Co gorsza – może być nieco mylące. Okazuje się bowiem, że to co nazywamy “jobem sparkowym” (czyli cały kod który ma coś zrobić i jest uruchamiany przy użyciu sparka) to tak naprawdę “aplikacja” (application). Aplikacja natomiast składa się z… jobów.

Nie mówię, że masz dokonywać rewolucji w swoim słowniku. Sam zresztą też chwalę się ile to nie trwał “mój job”;-). Pamiętaj jednak, że prawdziwe joby siedzą pod spodem aplikacji.

No dobrze – ale czym dokładnie są te joby? I jak jest zbudowana aplikacja sparkowa? Oczywiście to nie miejsce na szkolenie (na listę oczekujących kursu online “Fundament Apache Spark” możesz zapisać się tutaj). Spróbujmy jednak bardzo pokrótce zrozumieć jak zbudowana jest taka aplikacja.

Zróbmy to w kilku krokach:

  1. Podczas pisania kodu posługujemy się dwoma rodzajami: akcjami i transformacjami:
    • transformacje pozwalają nam przetworzyć RDD/Dataset na inny RDD/Dataset. Nie są jednak wykonywane od razu (lazy evaluation)
    • akcje z kolei wykonują wspomniane wcześniej transformacje i tym samym kończą pewną robotę do zrobienia – czyli joba;-).
  2. Job, czyli praca do wykonania. No właśnie – mamy kilka transformacji, które składają się w jeden ciąg operacji dokonywanych na danych. Na końcu są np. zapisane na HDFS albo wyświetlone na ekranie. I to jest właśnie 1 job. Tak więc powiedzmy to sobie wreszcie – 1 akcja = 1 job, yeeeaahh!
  3. Czyli w aplikacji może być kilka jobów. To teraz kolejne zagłębienie. Job składa się ze… stage’y. Czyli z etapów. Jak to się dzieje? Wróćmy do transformacji – bo na tym etapie mamy tylko z transformacjami do czynienia (w końcu akcja kończy joba).
    • Transformacje też możemy podzielić!
    • Narrow Transformations – gdy transformacje z jednej partycji wyprowadzają dokładnie jedną partycję. Narrow Transformations (np. filtry) są dokonywane w pamięci.
      • przykłady: filter, map, union, sample, intersection
    • Wide Transformations – gd transformacje wyprowadzają z jednej partycji wejściowej kilka partycji wyjściowych. Tutaj, ponieważ wide transformations powodują shuffling, dane muszą zostać zapisane na dysk.
      • przykłady: groupBy, join, repartition (te trzy, szczególnie dwa pierwsze, to klasyki – postrachy inżynierów sparkowych)
    • No i właśnie dlatego, że wide transformations powodują shuffling (przemieszanie danych między partycjami/executorami/nodami), musi zakończyć się jakiś etap joba. Czyli stage;-).

I można by kilka rzeczy dodać, ale tyle wystarczy, a nawet może zbyt wiele.

Nie mogłem jednak się powstrzymać. Uff… liczę, że jeszcze ze mną jesteś!

Ale dlaczego jedna akcja tworzy wiele jobów?!

Wspomniałem wyżej, że jednej akcji odpowiada jeden job w aplikacji. Jakie było moje zdziwienie, gdy zobaczyłem co następuje.

Oto mój kod. Przykładowy, ćwiczeniowy, prosty do zrozumienia (zmienna spark to instancja SparkSession):

val behaviorDF: Dataset[Row] = spark.read
  .option("header", "true")
  .csv(pathToBehaviorFile)

behaviorDF.show()
val brandCountDF: Dataset[Row] = behaviorDF.groupBy("brand")
  .count()
  .as("brandCount")

val userIdCount: Dataset[Row] = behaviorDF.groupBy("user_id")
  .count()
  .as("userCount")

val behaviorsWithCounts: Dataset[Row] = behaviorDF.join(brandCountDF, "brand")
  .join(userIdCount, "user_id")

behaviorsWithCounts.show(20)

 

Jak widać mamy dwie akcje:

  1. behaviorDF.show() – linijka 23 (w rzeczywistości)
  2. behaviorsWithCounts.show(20) – linijka 35 (w rzeczywistości).

Czyli z grubsza powinny być 2, może 3 joby (jeśli liczyć także wczytywanie danych).

Co zastałem w Spark UI?

WHAAAT…

Jak to się stało?

Czemu mam… 5 różnych jobów do akcji z linijki 35?

Otóż – mogą być za to odpowiedzialne dwie rzeczy:

  1. DataFrame to abstrakcja zbudowana na RDD. 1 akcja odpowiada 1 jobowi, ale… na RDD. Dataframe czasami “pod spodem” może wykonywać jeszcze inne akcje. JEDNAK TO NIE TO BYŁO MOJE ROZWIĄZANIE. Dotarłem do takiego wyjaśnienia więc się nim dzielę. U mnie natomiast DUŻO WAŻNIEJSZY okazał się pkt 2.
  2. Włączone Adaptive Query Execution – czyli mechanizm optymalizacyjny Apache Spark. Może być włączony albo wyłączony. Od Sparka 3.2.0 AQE włączone jest domyślnie!

Po ustawieniu prostej konfiguracji “spark.sql.adaptive.enabled” na “false”, jak poniżej…

val spark: SparkSession = SparkSession.builder()
  .appName("spark3-rdf-tests")
  .config("spark.sql.adaptive.enabled", false)
  //      .master("local[*]")
  .getOrCreate()

 

… i uruchomieniu aplikacji raz jeszcze, efekt w 100% pokrył się z moją wiedzą teoretyczną.

OMG CO ZA ULGA

UWAGA! Warto pamiętać, że AQE jest z zasady dobrym pomysłem. Nie wyłączaj tego, jeśli nie wiesz dokładnie po co to chcesz robić.

Ja na przykład wyłączam w celach edukacyjnych;-)


Szkolenie z Apache Spark – może tego właśnie potrzebujesz?

Jeśli reprezentujesz firmę i potrzebujecie solidnie przeczołgać się ze Sparka… jesteśmy tu dla Was!

Mamy solidnie sprawdzoną formułę.

I własny klaster, na którym poeksperymentujecie;-)

Więcej informacji na tej stronie.

Możesz też po prostu napisać na: kontakt@riotechdatafactory.com !


Co to jest Adaptive Query Execution?

No to teraz pokrótce: co to jest Adaptive Query Execution?

Przeczytasz o tym w dokumentacji Sparka, o tutaj.

Mówiąc jednak prosto i zwięźle: Adaptive Query Execution to mechanizm zawarty w Spark SQL, który pozwala zoptymalizować pracę aplikacji. AQE dokonuje optymalizacji bazując na “runtime statistics”. Temat samych statystyk będę poszerzał w przyszłości tutaj lub na kanale YouTube. Zapisz się na newsletter, żeby nie przegapić;-). I przy okazji zgarnij jedynego polskiego ebooka wprowadzającego w branżę Big Data (i to kompleksowo!).

AQE ma 3 podstawowe funkcjonalności:

  1. Łączenie partycji po shufflingu – dzięki temu mechanizmowi bardziej wydajnie dobierane są partycje. Widać to m.in. na powyższym przykładzie – gdy porównasz liczby partycji w obu przypadkach.
  2. Dzielenie partycji ze “skośnościami” po shufflingu (data skewness) – spark będzie optymalizował partycje, które podlegają “skośności” (są zbyt duże, co wychodzi dopiero po shufflingu).
  3. Zamiana “sort-merge join” na “broadcast join” – zamienia jeśli statystyki którakolwiek strona joina jest mniejsza niż poziom pozwalający na taką operację.

W praktyce AQE daje zauważalne rezultaty. Widać to dość symbolicznie na mojej małej aplikacji (ładuję tam jedynie 5 gb z hakiem), gdzie wynik z ~5.4 min zszedł do ~5 min.

Minusy? Przede wszystkim mniejsza czytelność podczas monitoringu joba. Co z jednej strony może wydać się śmieszne, ale w rzeczywistości, gdy musimy zoptymalizować jakąś bardzo złożoną aplikację – może zrobić się uciążliwe.

Podsumowanie

Podsumowując:

  1. Od Sparka 3.2.0 domyślnie włączony jest Adaptive Query Execution.
  2. To mechanizm, który pozwala na bardzo konkretną optymalizację. Powoduje niestety pewne “zaszumienie” monitoringu aplikacji.
  3. W efekcie zamiast zasady 1 akcja = 1 job, nasza aplikacja będzie bardziej porozbijana.
  4. Można to wyłączyć (aby nie zachęcać do pójścia “na łatwiznę” – jak to zrobić zostało zawarte w tekście;-)). Nie rób jednak tego bez solidnej argumentacji.
  5. Zapisz się na newsletter i przeczytaj ebooka, który pokaże Ci Big Data z kilku różnych storn. A jak Ci się spodoba, napisz o tym w sieci żeby i inni wiedzieli:-).

Jeśli natomiast szukasz czegoś, co pokaże Ci podstawy Sparka od A do Z… 

Może sprawdzisz kurs “Fundament Apache Spark”?

“Podobno” niektórzy od tego kursu… zaczęli całą przygodę z branżą;-).

Jak oczyścić dane w sparku? Castowanie, funkcje, nulle, regexpy itd. [wideo]

Jak oczyścić dane w sparku? Castowanie, funkcje, nulle, regexpy itd. [wideo]

Dziś kontynuujemy temat pierwszego kontaktu z danymi. W wideo opowiadam nieco o rzutowaniu (cast), funkcjach jak split, regexpach czy walce z nullami. Oczywiście wszystko z wykorzystaniem Apache Spark. Zapraszam!

A! Ważna informacja… tylko do końca stycznia można zakupić kurs “Fundament Apache Spark”. Nie zwlekaj z poznaniem jednej z najważniejszych technologii w branży.

Podstawowy problem, czyli… “how to clean data in spark?”

Przypominam jeszcze, jeśli nie jesteś członkiem newslettera, po zaciągnięciu się na nasz okręt dostajesz na wejściu prawie 140 stron ebooka o Big Data! Nie zwlekaj;-)

 

Loading
Jak załadować dane do Apache Spark? [Wideo]

Jak załadować dane do Apache Spark? [Wideo]

Po przerwie wracam z… poradnikami! Do końca stycznia zajmujemy się jeszcze Apache Spark, bo i do końca stycznia można zakupić kurs “Fundament Apache Spark”.

Dzisiaj zajmiemy się takimi tematami jak:

  • Zapoznawanie się z danymi
  • Ładowanie danych do Sparka z csv
  • Wstępna obróbka danych i łączenie różnych źródeł w jeden dataset

A to wszystko na danych z Twittera;-).

Podstawowy problem czyli… “How to load data in Spark”?

Przypominam jeszcze, jeśli nie jesteś członkiem newslettera, po zaciągnięciu się na nasz okręt dostajesz na wejściu prawie 140 stron ebooka o Big Data! Nie zwlekaj;-)

 

Loading
“Fundament Apache Spark” już dostępny! Jak wygląda pierwszy polski kurs o Sparku? [Wideo]

“Fundament Apache Spark” już dostępny! Jak wygląda pierwszy polski kurs o Sparku? [Wideo]

Z przyjemnością chcę ogłosić, że… pierwszy kurs autorstwa Riotech Data Factory… jest już dostępny! To “Fundament Apache Spark” i dzięki niemu poznasz podstawy tej technologii.

Chciałbym opisać coś więcej, natomiast najlepiej chyba będzie, jak zobaczysz  sam/a;-).

Przygotowałem specjalne wideo, na którym pokazuję od wejścia na stronę co zobaczysz po dokonaniu zakupu.

Całość tego jak wygląda kurs “od środka” zaczyna się ok 12 minuty;-).

Zobacz jak wygląda pierwszy kurs online o Apache Spark

Na stronę kursową przejdziesz klikając w ten link

Przypominam jeszcze, jeśli nie jesteś członkiem newslettera, po zaciągnięciu się na nasz okręt dostajesz na wejściu prawie 140 stron ebooka o Big Data! Nie zwlekaj;-)

 

Loading
Co to jest Spark? Dla kompletnie zielonych. [wideo] [jesień]

Co to jest Spark? Dla kompletnie zielonych. [wideo] [jesień]

Co to jest Apache Spark? Po co go używamy? Jak on działa? O tym wszystkim w nowym wideo z serii jesiennej!

Przy okazji: niedługo opublikuję kurs online ze Sparka.

Zapisz się tutaj na specjalny newsletter, nie przegap okienka w którym kurs się ukaże.

I otrzymaj oczywiście zniżkę;-)

Co to jest Apache Spark? Wideo dla początkujących

Przypominam jeszcze, jeśli nie jesteś członkiem newslettera, po zaciągnięciu się na nasz okręt dostajesz na wejściu prawie 140 stron ebooka o Big Data! Nie zwlekaj;-)

 

Loading
Jak uruchomić Spark na klastrze? [wideo]

Jak uruchomić Spark na klastrze? [wideo]

Kolejne wideo poradnikowe dotyczy Sparka. Pokazuję, w jaki sposób od A do Z uruchomić aplikację (job) sparkową na serwerze (na klastrze). Kod jest już gotowy i znajdziesz go w repozytorium;-). Jeśli zechcesz zagłębić się w kwestie techniczne, tutaj znajdziesz artykuł na temat spark submit.

How to index data in Solr with Apache Spark?

Poniżej wklejam wideo i zapraszam do subskrybowania kanału RDF na YouTube;-)

Dodatkowe materiały

Kod który widzisz na wideo jest dostępny publicznie. Znajdziesz go na otwartym repozytorium RDF – a dokładniej tutaj;-). Powodzenia!

Zapraszam na nasz profil LinkedIn oraz do newslettera;-).

UWAGA! Pierwszy polski ebook o Big Data już dostępny! Zapisz się na listę newslettera i podążaj “Szlakiem Big Data”. Więcej tutaj.

 

 

Loading
Jak zaindeksować dane w Solr z użyciem Spark? [wideo]

Jak zaindeksować dane w Solr z użyciem Spark? [wideo]

Po EKSTREMALNYM sukcesie poradnikowego wideo na temat tworzenia kolekcji w Solr (prawie 50 wyświetleń w pierwszych dniach. SZOK. Hollywood stuka puka do drzwi!), pociągnąłem temat. Ciągle poradnikowo, ciągle Solr. Tym razem jednak zaindeksujemy więcej dokumentów niż tradycyjnie robi się to w tutorialach. I zrobimy to znacznie przyjemniej, niż robi się to zwykle w tutorialach;-). Czas zaprzęgnąć Sparka do indeksacji danych w Solr!

How to index data in Solr with Apache Spark?

Poniżej wklejam wideo i zapraszam do subskrybowania kanału RDF na YouTube;-)

Dodatkowe materiały

Kod który widzisz na wideo jest dostępny publicznie. Znajdziesz go na otwartym repozytorium RDF – a dokładniej tutaj;-). Powodzenia!

Zapraszam na nasz profil LinkedIn oraz do newslettera;-).

UWAGA! Pierwszy polski ebook o Big Data już dostępny! Zapisz się na listę newslettera i podążaj “Szlakiem Big Data”. Więcej tutaj.

 

 

Loading
Zrozumieć Sparka: jak budować UDF? Instrukcja

Zrozumieć Sparka: jak budować UDF? Instrukcja

Dokonując transformacji w Sparku, bardzo często korzystamy z gotowych, wbudowanych rozwiązań. Łączenie tabel, explodowanie tablic na osobne wiersze czy wstawianie stałej wartości – te i wiele innych operacji zawarte są jako domyślne funkcje. Może się jednak okazać, że to nie wystarczy. Wtedy z pomocą w Sparku przychodzi mechanizm UDF (User Defined Function).

Dzisiaj o tym jak krok po kroku stworzyć UDFa, który może być wyorzystany w wygodny sposób wszędzie w projekcie. Do dzieła! Całą serię “zrozumieć sparka” poznasz tutaj.

Co to jest UDF w Sparku?

Wczuj się w sytuację. Tworzysz joba sparkowego, który obsługuje dane firmowe dotyczące pracowników. Chcesz przyznawać premie tym najlepszym, najwierniejszym i najbardziej pracowitym i zyskownym. Po zebraniu potrzebnych informacji w jednym DataFrame, będziemy chcieli utworzyć kolumnę “bonus” która zawiera prostą informację: kwotę premii na koniec roku.

Aby to wyliczyć, został utworzony wcześniej wzór. Wykorzystując informacje dotyczące stanowiska, zyskowności projektu, oceny współpracowników, przepracowanych godzin i kilku innych rzeczy. Oczywiście nie ma możliwości, żeby wyliczyć to przy pomocy zwykłych funkcji. Z drugiej jednak strony, jeśli mielibyśmy jednostkowo wszystkie potrzebne dane – nie ma problemu, aby taki wzór zakodować.

Temu właśnie służą sparkowe UDFs, czyli User Defined Functions. To funkcje, których działanie sami możemy napisać i które pozwolą nam na modyfikację Datasetów w sposób znacznie bardziej customowy. Można je utworzyć na kilka różnych sposobów, ale ja dzisiaj chciałbym przedstawić Ci swój ulubiony.

A ulubiony dlatego, ponieważ:

  • Jest elegancko zorganizowany
  • Daje możliwość wielokrotnego wykorzystywania UDFa w całym projekcie, przy jednokrotnej inicjalizacji go.

Jak zbudować UDF w Apache Spark? Instrukcja krok po kroku.

Instrukcja tworzenia UDFa jest dość prosta i można ją streścić do 3 kroków:

  1. Stwórz klasę UDFa (rozszerzającą UDFX – np. UDF1, jeśli mamy do podania jedną kolumnę).
  2. Zarejestruj UDFa.
  3. Wywołaj UDFa podczas dodawania kolumny.

Scenariusz

Zobrazujmy to pewnym przykładem. Mamy do dyspozycji dataframe z danymi o ludziach. Chcemy sprawdzić zagrożenie chorobami na podstawie informacji o nich. Dla zobrazowania – poniżej wygenerowany przeze mnie Dataframe. Taki sobie prosty zestaw:-).

Efekt który chcemy osiągnąć? te same dane, ale z kolumną oznaczającą zagrożenie: 1- niskie, 2-wysokie, 3-bardzo wysokie. Oczywiście bardzo tu banalizujemy, w rzeczywistości to nie będzie takie proste!

Załóżmy jednak, że mamy zakodować następujący mechanizm: zbieramy punkty zagrożenia.

  1. Bycie palaczem daje +20 do zagrożenia,
  2. Wiek ma przedziały: do 30 lat (+0); do 60 lat (+10); do 80 lat (+20); powyżej (+40)
  3. Aktywności fizyczne: jeśli są, to każda z nich daje -10 (czyli zabiera 10 pkt).

Tak, wiem – to nawet nie banalne, a prostackie. Rozumiem, zebrałem już baty od siebie samego na etapie wymyślania tego wiekopomnego dzieła. Idźmy więc dalej! Grunt, żeby był tutaj jakiś dość skomplikowany mechanizm (w każdym razie bardziej skomplikowany od takiego który łatwo możemy “ograć” funkcjami sparkowymi).

Krok 1 – Stwórz klasę UDFa

Disclaimer: zakładam, że piszemy w Scali (w Javie robi się to bardzo podobnie).

Oczywiście można też zrobić samą metodę. Ba! Można to “opękać” lambdą. Jednak, jak już napisałem, ten sposób rodzi największy porządek i jest moim ulubionym;-). Utwórz najpierw pakiet który nazwiesz “transformations”“udfs” czy jakkolwiek będzie dla Ciebie wygodnie. Grunt żeby trzymać wszystkie te klasy w jednym miejscu;-).

Wewnątrz pakietu utwórz klasę (scalową) o nazwie HealtFhormulaUDF. Ponieważ będziemy przyjmowali 3 wartości wejściowe (będące wartościami kolumn smoker, age activities), rozszerzymy interfejs UDF3<T1, T2, T3, R>. Oznacza to, że musimy podczas definicji klasy podać 3 typy wartości wejściowych oraz jeden typ tego co będzie zwracane.

Następnie tworzymy metodę call(T1 t1, T2 t2, T3 t3), która będzie wykonywać realną robotę. To w niej zaimplementujemy nasz mechanizm. Musi ona zwracać ten sam typ, który podaliśmy na końcu deklaracji klasy oraz przyjmować argumenty, które odpowiadają typami temu, co podaliśmy na początku deklaracji. Gdy już to mamy, wewnątrz należy zaimplementować mechanizm, który na podstawie wartości wejściowych wyliczy nam nasze ryzyko zachorowania. Wiem, brzmi to wszystko odrobinę skomplikowanie, ale już pokazuję o co chodzi. Spójrz na skończony przykład poniżej.

package udfs

import org.apache.spark.sql.api.java.UDF3

class HealthFormulaUDF extends UDF3[String, Int, String, Int]{
  override def call(smoker: String, age: Int, activities: String): Int = {
    val activitiesInArray: Array[String] = activities.split(",")
    val agePoints: Int = ageCalculator(age)
    val smokePoints: Int = if(smoker.toLowerCase.equals("t")) 20 else 0
    val activitiesPoints = activitiesInArray.size * 10
    agePoints + smokePoints - activitiesPoints
  }

  def ageCalculator(age: Int): Int ={
   age match {
    case x if(x < 30) => 0
    case x if(x >= 30 && x < 60) => 10
    case x if(x >= 60 && x < 80) => 20
    case _ => 40
   }
  }
}

Dodałem sobie jeszcze pomocniczą funkcję “ageCalculator()”, żeby nie upychać wszystkiego w metodzie call().

Zarejestruj UDF

Drugi krok to rejestracja UDF. Robimy to, aby potem w każdym miejscu projektu móc wykorzystać utworzony przez nas mechanizm. Właśnie z tego powodu polecam dokonać rejestracji zaraz za inicjalizacją Spark Session, a nie gdzieś w środku programu. Pozwoli to nabrać pewności, że ktokolwiek nie będzie w przyszłości wykorzystywał tego konkretnego UDFa, zrobi to po rejestracji, a nie przed. Poza tym utrzymamy porządek – będzie jedno miejsce na rejestrowanie UDFów, nie zaś przypadkowo tam gdzie komuś akurat się zachciało.

Aby zarejestrować musimy najpierw zainicjalizować obiekt UDFa. Robimy to w najprostszy możliwy sposób. Następnie dokonujemy rejestracji poprzez funkcję sparkSession.udf.register(). Musimy tam przekazać 3 argumenty:

  • Nazwę UDFa, do której będziemy się odnosić potem, przy wywoływaniu
  • Obiekt UDFa
  • Typ danych, jaki zwraca konkretny UDF (w naszym przypadku Integer). UWAGA! Typy te nie są prostymi typami Scalowymi. To typy sparkowe, które pochodzą z klasy DataTypes.

Poniżej zamieszczam całość, razem z inicjalizacją sparkSession aby było wiadomo w którym momencie t uczynić;-).

val sparkSession = SparkSession.builder()
  .appName("spark3-test")
  .master("local")
  .getOrCreate()

val healthFormulaUDF: HealthFormulaUDF = new HealthFormulaUDF()
sparkSession.udf.register("healthFormulaUDF", healthFormulaUDF, DataTypes.IntegerType)

W tym momencie UDF jest już zarejestrowany i można go wywoływać gdziekolwiek w całym projekcie.

Wywołaj UDF

Ostatni krok to wywołanie UDFa. To będzie bardzo proste, ale musimy zaimportować callUDF z pakietu org.apache.spark.sql.functions (można też zaimportować wszystkie funkcje;-)).

Ponieważ chcemy utworzyć nową kolumnę z liczbą punktów, skorzystamy z funkcji withColumn(). Całość poniżej.

val peopleWithDiseasePoints: Dataset[Row] = peopleDF.withColumn("diseasePoints",
        callUDF("healthFormulaUDF", col("smoker"), col("age"), col("activities")))

Efekt jest jak poniżej. Im mniej punktów w “diseasePoints” tym lepiej. Cóż, chyba nie mam się czym przejmować, mam -20 pkt!

Podsumowanie

W tym artykule dowiedzieliśmy się czym w Apache Spark jest UDF. Zasadniczo całość można sprowadzić do 3 prostych kroków:

  1. Stwórz klasę UDFa (rozszerzającą UDFX – np. UDF1, jeśli mamy do podania jedną kolumnę).
  2. Zarejestruj UDFa.
  3. Wywołaj UDFa podczas dodawania kolumny.

To był materiał z serii “Zrozumieć Sparka”. Nie pierwszy i definitywnie nie ostatni! Jeśli jesteś wyjątkowo głodny/a Sparka – daj znać szefowi. Przekonaj go, żeby zapisał Ciebie i Twoich kolegów/koleżanki na szkolenie ze Sparka. Omawiamy całą budowę od podstaw, pracujemy dużo i intensywnie na ciekawych danych, a wszystko robimy w miłej, sympatycznej atmosferze;-) – Zajrzyj tutaj!

A jeśli chcesz pozostać z nami w kontakcie – zapisz się na newsletter lub obserwuj RDF na LinkedIn. Koniecznie, zrób to i razem twórzmy polską społeczność Big Data!

 

Loading
Apache Spark: Jak napisać prosty mechanizm porównywania tekstów?

Apache Spark: Jak napisać prosty mechanizm porównywania tekstów?

Machine Learning w Sparku? Jak najbardziej! W poprzednim artykule pokazałem efekty prostego mechanizmu do porównywania tekstów, który zbudowałem. Całość jest zrobiona w Apache Spark, co niektórych może dziwić. Dzisiaj chcę się podzielić tym jak dokładnie zbudować taki mechanizm. Kubki w dłoń i lecimy zanurzyć się w kodzie!

Założenia

Jeśli chodzi o założenia, które dotyczą Ciebie – zakładam że umiesz tu Scalę oraz Sparka. Oba w stopniu podstawowym;-). W kontekście sparka polecam mój cykl “zrozumieć Sparka” czy generalnie wszystkie wpisy dotyczące tej technologii.

Jeśli chodzi o założenia naszego “projektu” – to są one dość proste:

  1. Bazujemy na zbiorze, który ma ~204 tysiące krótkich tekstów – konkretnie tweetów.
  2. Tweety dotyczą trzech dziedzin tematycznych:
    • COVID – znakomita większość (166543 – 81,7%)
    • Finanse – pewna część (28874 – 14,1%)
    • Grammy’s – margines (8490 – 4,2%)
  3. W ramach systemu przekazujemy tekst od użytkownika. W odpowiedzi dostajemy 5 najbardziej podobnych tweetów.

Pobieranie datasetów (wszystkie dostępne na portalu Kaggle): covid19_tweets, financial, GRAMMYs_tweets

Powiem jeszcze, że tutaj pokazuję jak zrobić to w prostej, batchowej wersji. Po prostu uruchomimy cały job sparkowy wraz z tekstem i dostaniemy odpowiedzi. W innym artykule jednak pokażę jak zrobić także joba streamingowego. Dzięki temu stworzymy mechanizm, który będzie nasłuchiwał i naprawdę szybko będzie zwracał wyniki w czasie rzeczywistym (mniej więcej, w zależności od zasobów – czas ocekiwania to kilka, kilkanaście sekund). Jeśli chcesz dowiedzieć się jak to zrobić – nie zapomnij zasubskrybować bloga RDF!

 

Loading

Spark MlLib

Zacznijmy od pewnej rzeczy, żeby nam się nie pomyliło. Spark posiada bibliotekę, która służy do pracy z machine learning. Nazywa się Spark MlLib. Problem polega na tym, że wewnątrz rozdziela się na dwie pod-biblioteki (w scali/javie są to po prostu dwa pakiety):

  1. Spark MlLib – metody, które pozwalają na prace operując bezpośrednio na RDD. Starsza część, jednak nadal wspierana.
  2. Spark Ml – metody, dzięki którym pracujemy na Datasetach/Dataframach. Jest to zdecydowanie nowocześniejszy kawałek biblioteki i to z niego właśnie korzystam.

Spark MlLib możemy pobrać z głównego repozytorium mavena tutaj.

Dodawanie dependencji jeśli korzystamy z Mavena (plik pom.xml):

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-mllib_2.12</artifactId>
    <version>3.0.0</version>
    <scope>provided</scope>
</dependency>

Oczywiście scope “provided” podajemy tylko w przypadku wysyłania później na klaster. Jeśli chcemy eksperymentować lokalnie, nie dodajemy go.

Dodawanie dependencji jeśli korystamy z SBT (plik build.sbt):

libraryDependencies += "org.apache.spark" %% "spark-mllib" % "3.0.0" % "provided"

Ta sama uwaga odnośnie “provided” co w przypadku mavena.

Spark NLP od John Snow Labs

Chociaż Spark posiada ten znakomity moduł SparkMlLib, to niestety brak w nim wielu algorytmów. Zawierają się w tych brakach nasze potrzeby. Na szczęście, luka została wypełniona przez niezależnych twórców. Jednym z takich ośrodków jest John Snow Labs (można znaleźć tutaj). Samą bibliotekę do przetwarzania tekstu, czyli Spark-NLP zaciągniemy bez problemu z głównego repozytorium Mavena

Dodawanie dependencji, jeśli korzystamy z Mavena (plik pom.xml):

<dependency>
    <groupId>com.johnsnowlabs.nlp</groupId>
    <artifactId>spark-nlp_2.12</artifactId>
    <version>3.3.4</version>
    <scope>test</scope>
</dependency>

Dodawanie dependencji jeśli korystamy z SBT (plik build.sbt):

libraryDependencies += "com.johnsnowlabs.nlp" %% "spark-nlp" % "3.3.4" % Test

Dane

Dane same w sobie pochodzą z 3 różnych źródeł. I jak to bywa w takich sytuacjach – są po prostu inne, pomimo że teoretycznie dotyczą tego samego (tweetów). W związku z tym musimy zrobić to, co zwykle robi się w ramach ETLów: sprowadzić do wspólnej postaci, a następnie połączyć.

Dane zapisane są w plikach CSV. Ponieważ do porównywania będziemy używać tylko teksty, z każdego zostawiamy tą samą kolumnę – text. Poza tą jedną kolumną dorzucimy jednak jeszcze jedną. To kolumna “category”, która będzie zawierać jedną z trzech klas (“covid”“finance”“grammys”). Nie będą to oczywiście klasy służące do uczenia, natomiast dzięki nim będziemy mogli sprawdzić potem na ile dobrze nasze wyszukiwania się “wstrzeliły” w oczekiwane grupy tematyczne. Na koniec, gdy już mamy identyczne struktury danych, możemy je połączyć zwykłą funkcją union”.

Całość upakowałem w metodę zwracającą Dataframe:

def prepareTwitterData(sparkSession: SparkSession): Dataset[Row] ={
  val covidDF: Dataset[Row] = sparkSession.read
    .option("header", "true")
    .csv("covid19_tweets.csv")
    .select("text")
    .withColumn("category", lit("covid"))
    .na.drop()
  val financialDF: Dataset[Row] = sparkSession.read
    .option("header", "true")
    .csv("financial.csv")
    .select("text")
    .withColumn("category", lit("finance"))
    .na.drop()

  val grammysDF: Dataset[Row] = sparkSession.read
    .option("header", "true")
    .csv("GRAMMYs_tweets.csv")
    .select("text")
    .withColumn("category", lit("grammys"))
    .na.drop()

  covidDF.union(financialDF)
    .union(grammysDF)
}

Przygotowanie tekstu do treningu

Gdy pracujemy z NLP, bazujemy oczywiście na tekście. Niestety, komputer nie rozumie tekstu. A co rozumie komputer? No jasne, liczby. Musimy więc sprowadzić tekst do poziomu liczb. Konkretnie wektorów, a jeszcze konkretniej – embeddingów. Embeddingi to nisko-wymiarowe reprezentacje czegoś wysoko-wymiarowego. W naszym przypadku będzie to tekst. Czym dokładnie są embeddingi, dobrze wyjaśnione jest na tej stronie. Na nasze, uproszczone potrzeby musimy jednak wiedzieć jedno: embeddingi pozwalają zachować kontekst. Oznacza to w dużym skrócie, że słowo “pizza” będzie bliżej słowa “spaghetti” niż słowa “sedan”.

Sprowadzanie do postaci liczbowej może się odbyć jednak dopiero wtedy, gdy odpowiednio przygotujemy tekst. Bardzo często w skład takiego przygotowania wchodzi oczyszczenie ze “śmieciowych znaków” (np. @, !, ” itd) oraz tzw. “stop words”, czyli wyrazów, które są spotykane na tyle często i wszędzie, że nie opłaca się ich rozpatrywać (np. I, and, be). Oczywiście może to rodzić różne problemy – np. jeśli okroimy frazy ze standardowych “stop words”, wyszukanie “To be or not to be” będzie… puste. To jednak już problem na inny czas;-).

Do przygotowania często wprowadza się także tokenizację, czyli podzielenie tekstu na tokeny. Bardzo często to po prostu wyciągnięcie wyrazów do osobnej listy, aby nie pracować na stringu, a na kolekcji wyrazów (stringów). Spotkamy tu także lemmatyzację, stemming (obie techniki dotyczą sprowadzenia różnych słów do odpowiedniej postaci, aby móc je porównywać).

W naszym przypadku jednak nie trzeba będzie robić tego wszystkiego. Jedyne co musimy, to załączyć DocumentAssembler. Jest to klasa, która przygotowuje dane do formatu zjadliwego przez Spark NLP.

Po zastosowaniu dostajemy kolumnę, która ma następującą strukturę:

root
 |-- document: array (nullable = true)
 |    |-- element: struct (containsNull = true)
 |    |    |-- annotatorType: string (nullable = true)
 |    |    |-- begin: integer (nullable = false)
 |    |    |-- end: integer (nullable = false)
 |    |    |-- result: string (nullable = true)
 |    |    |-- metadata: map (nullable = true)
 |    |    |    |-- key: string
 |    |    |    |-- value: string (valueContainsNull = true)
 |    |    |-- embeddings: array (nullable = true)
 |    |    |    |-- element: float (containsNull = false)

W naszym kodzie najpierw inicjalizujemy DocumentAssembler, wykorzystamy go nieco później. Przy inicjalizacji podajemy kolumnę wejściową oraz nazwę kolumny wyjściowej:

val docAssembler: DocumentAssembler = new DocumentAssembler().setInputCol("text")
      .setOutputCol("document")

Zastosowanie USE oraz budowa Pipeline

Jak już napisałem, my wykorzystamy Universal Sentence Encoder (USE). Dzięki tym embeddingom całe frazy (tweety) będą mogły nabrać konktekstu. Niestety, sam “surowy” Spark MlLib nie zapewnia tego algorytmu. Musimy tu zatem sięgnąć po wspomniany już wcześniej Spark NLP od John Snow Labs (podobnie jak przy DocumentAssembler). Zainicjalizujmy najpierw sam USE.

val use: UniversalSentenceEncoder = UniversalSentenceEncoder.pretrained()
      .setInputCols("document")
      .setOutputCol("sentenceEmbeddings")

Skoro mamy już obiekty dosAssembler oraz use, możemy utworzyć pipeline. Pipeline w Spark MlLib to zestaw powtarzających się kroków, które możemy razem “spiąć” w całość, a następnie wytrenować, używać. Wyjście jednego kroku jest wejściem kolejnego. Wytrenowany pipeline (funkcja fit) udostępnia nam model, który możemy zapisać, wczytać i korzystać z niego.

Nasz pipeline będzie bardzo prosty:

val pipeline: Pipeline = new Pipeline().setStages(Array(docAssembler, use))
val fitPipeline: PipelineModel = pipeline.fit(tweetsDF)

Gdy dysponujemy już wytrenowanym modelem, możemy przetworzyć nasze dane (funkcja transform). Po tym kroku otrzymamy gotowe do użycia wektory. Niestety, USE zagnieżdża je w swojej strukturze – musimy więc je sobie wyciągnąć. Oba kroki przedstawiam poniżej:

val vectorizedTweetsDF: Dataset[Row] = fitPipeline.transform(tweetsDF)
      .withColumn("sentenceEmbeddings", org.apache.spark.sql.functions.explode(col("sentenceEmbeddings.embeddings")))

Znakomicie! Mamy już tweety w formie wektorów. Teraz należy jeszcze zwektoryzować tekst użytkownika. Tekst będzie przechowywany w Dataframe z jednym wierszem (właśnie owym tekstem) w zmiennej sampleTextDF. Po wektoryzacji usunę zbędne kolumny i zmienię nazwy tak, aby było wiadomo, że te wektory dotyczą tekstu użytkownika, a nie tweetów (przyda się później, gdy będziemy łączyć ze sobą oba Dataframy).

val vectorizedUserTextDF: Dataset[Row] = fitPipeline.transform(sampleTextDF)
      .drop("document")
      .withColumn("userEmbeddings", org.apache.spark.sql.functions.explode(col("sentenceEmbeddings.embeddings")))
      .drop("sentenceEmbeddings")

Implementacja cosine similarity

Uff – sporo roboty za nami, gratuluję! Mamy już tweety oraz tekst użytkownika w formie wektorów. Czas zatem porównać, aby znaleźć te najbardziej podobne! Tylko pytanie, jak to najlepiej zrobić? Muszę przyznać że trochę czasu zajęło mi szukanie algorytmów, które mogą w tym pomóc. Finalnie wybór padł na cosine similarity. Co ważne – nie jest to żaden super-hiper-ekstra algorytm NLP. To zwykły wzór matematyczny, znany od dawna, który porównuje dwa wektory. Tak – dwa najzwyklejsze, matematyczne wektory. Jego wynik zawiera się między -1 a 1. -1 to skrajnie różne, 1 to identyczne. Nas zatem będą interesować wyniki możliwie blisko 1.

Problem? A no jest. Spark ani scala czy java nie mają zaimplementowanego CS. Tu pokornie powiem, że być może po prostu do tego nie dotarłem. Jeśli znasz gotową bibliotekę do zaimportowania – daj znać w komentarzu! Nie jest to jednak problem prawdziwy, bowiem możemy rozwiązać go raz dwa. Samodzielnie zaimplementujemy cosine similarity w sparku, dzięki UDFom (User Defined Function).

Najpierw zacznijmy od wzoru matematycznego:

{\displaystyle {\text{cosine similarity}}=S_{C}(A,B):=\cos(\theta )={\mathbf {A} \cdot \mathbf {B} \over \|\mathbf {A} \|\|\mathbf {B} \|}={\frac {\sum \limits _{i=1}^{n}{A_{i}B_{i}}}{{\sqrt {\sum \limits _{i=1}^{n}{A_{i}^{2}}}}{\sqrt {\sum \limits _{i=1}^{n}{B_{i}^{2}}}}}},}

Następnie utwórzmy klasę CosineSimilarityUDF, która przyjmuje dwa WrappedArrays (dwa wektory), natomiast zwraca zwykłą liczbę zmiennoprzecinkową Double. Wewnątrz konwertuję tablice na wektory, wykorzystuję własną metodę magnitude i zwracam odległość jednego wektora od drugiego.

Klasa CosineSimilarityUDF

import org.apache.spark.ml.linalg.{Vector, Vectors}
import org.apache.spark.sql.api.java.UDF2

import scala.collection.mutable

class CosinSimilarityUDF extends UDF2[mutable.WrappedArray[Float], mutable.WrappedArray[Float], Double]{
  override def call(arr1: mutable.WrappedArray[Float], arr2: mutable.WrappedArray[Float]): Double = {
    val vec1 = Vectors.dense(arr1.map(_.toDouble).toArray)
    val vec2 = Vectors.dense(arr2.map(_.toDouble).toArray)
    val mgnt1 = magnitude(vec1)
    val mgnt2 = magnitude(vec2)

    vec1.dot(vec2)/(mgnt1*mgnt2)
  }

  def magnitude(vector: Vector): Double={
    val values = vector.toArray
    Math.sqrt(values.map(i=>i*i).sum)
  }
}

Wykorzystanie Cosine Similarity – sprawdzamy podobieństwo tekstów!

Znakomicie – po utworzeniu tego UDFa, możemy śmiało wykorzystać go do obliczenia podobieństw między każdym z tweetów a tekstem użytkownika. Aby to uczynić, najpierw rejestrujemy naszego UDFa. Polecam to co zawsze polecam na szkoleniach ze Sparka – zrobić to zaraz po inicjalizacji SparkSession. Dzięki temu utrzymamy porządek i nie będziemy się martwić, jeśli w przyszłości w projekcie ktoś będzie również chciał użyć UDFa w nieznanym obecnie miejscu (inaczej może dojść do próby użycia UDFa zanim zostanie zarejestrowany).

val cosinSimilarityUDF: CosinSimilarityUDF = new CosinSimilarityUDF()
sparkSession.udf.register("cosinSimilarityUDF", cosinSimilarityUDF, DataTypes.DoubleType)

Wróćmy jednak na sam koniec, do punktu w którym mamy już zwektoryzowane wszystkie teksty. Najpierw sprawimy, że każdy tweet będzie miał dołączony do siebie tekst użytkownika. W tym celu zastosujemy crossjoin (artykuł o sposobach joinów w Sparku znajdziesz tutaj). Następnie użyjemy funkcji withColumn, dzięki której utworzymy nową kolumnę – właśnie z odległością. Wykorzystamy do jej obliczenia oczywiście zarejestrowany wcześniej UDF.

val dataWithUsersPhraseDF: Dataset[Row] = vectorizedTweetsDF.crossJoin(vectorizedUserTextDF)
val afterCosineSimilarityDF: Dataset[Row] = dataWithUsersPhraseDF.withColumn("cosineSimilarity", callUDF("cosinSimilarityUDF", col("sentenceEmbeddings"), col("userEmbeddings"))).cache()

Na sam koniec pokażemy 20 najbliższych tekstów, wraz z kategoriami. Aby uniknąć problemów z potencjalnymi “dziurami”, odfiltrowujemy rekordy, które w cosineSimilarity nie mają liczb. Następnie ustawiamy kolejność na desc, czyli malejącą. Dzięki temu dostaniemy wyniki od najbardziej podobnych do najmniej podobnych.

afterCosineSimilarityDF.filter(isnan(col("cosineSimilarity")) =!= true)
      .orderBy(col("cosineSimilarity").desc)
      .show(false)

I to koniec! Wynik dla hasła “The price of lumber is down 22% since hitting its YTD highs. The Macy’s $M turnaround is still happening” można zaobserwować poniżej. Więcej wyników – przypominam – można zaobserwować w poprzednim artykule;-).

Wyniki dla mechanizmu text similarity w Apache Spark.

Podsumowanie

Mam nadzieję, że się podobało! Daj znać koniecznie w komentarzu i prześlij ten artykuł dalej. Z pewnością to nie koniec przygody z Machine Learning w Sparku na tym blogu. Zostań koniecznie na dłużej i razem budujmy polskie środowisko Big Data;-). Jeśli chcesz pozostać z nami w kontakcie – zapisz się na newsletter lub obserwuj RDF na LinkedIn.

Pamiętaj także, że prowadzimy szkolenia z Apache Spark. Jakie są? Przede wszystkim bardzo mięsiste i tak bardzo zbliżone do rzeczywistości jak tylko się da. Pracujemy na prawdziwych danych, prawdziwym klastrze. Co więcej – wszystko to robimy w znakomitej atmosferze, a na koniec dostajesz garść materiałów! Kliknij tutaj i podrzuć pomysł swojemu szefowi;-).

 

Loading
Szukanie podobieństw w tekstach przy pomocy Spark ML – efekty

Szukanie podobieństw w tekstach przy pomocy Spark ML – efekty

Co ty na to, żeby zbudować system, dzięki któremu wyszukujemy podobne wypowiedzi polityków? Tak dla sprawdzenia – jeśli jeden coś powiedział, poszukamy czy jego oponenci nie mówili przypadkiem podobnie. Dzięki temu być może oczyścimy trochę debatę – świadomość, że nasi przedstawiciele nie różnią się aż tak bardzo, może być bardzo orzeźwiająca. Jednak taki mechanizm to dużo danych do przetworzenia i zinterpretowania. Dodatkowo tekstowych.

Dziś w artykule o podobnym problemie przy wykorzystaniu Apache Spark. Porozmawiamy więc o sztucznej inteligencji – a konkretniej machine learning, natural language processing (NLP) oraz text similarity. Wyjątkowo jednak nie w kontekście pythona, a właście Scali i Sparka.

Text Similarity (AI) w Apache Spark

Wróćmy do problemu podobieństw wypowiedzi polityków. Oczywiście musimy najpierw zebrać dane. Ile może ich być? Jeśli bazujemy na krótkich wypowiedziach – ogromne ilości. Wszystko zależy od tego jak bardzo chcemy się cofać w czasie i jak wiele osób wziąć pod lupę (Sejm? Senat? Rząd? Polityków lokalnych? A może zagranicznych?). Sumarycznie można się pokusić jednak o miliony, dziesiątki a nawet setki milionów krótkich wypowiedzi. A im bardziej w używaniu jest Twitter, tym więcej.

Pytanie, czy do tak dużych zbiorów można użyć bibliotek Pythonowych? Wiele z nich bazuje na jednej maszynie, bez możliwości naturalnego przetwarzania rozproszonego. Oczywiście nie wszystkie i z pewnością jest tam najmocniej rozwinięte środowisko do NLP. Na tym blogu skupimy się dziś jednak na mało popularnym pomyśle. Sprawdzimy na ile naprawdę poważnym rozwiązaniem może być Apache Spark w świecie machine learning. Dziś pokażę owoc eksperymentu nad przetwarzaniem tekstu w Apache Spark.

Po pierwsze: efekt

Zanim wskażę jakie techniki można zastosować, spójrzmy co udało się osiągnąć.

Zacznijmy od podstawowej rzeczy

  1. Bazujemy na zbiorze, który ma ~204 tysiące krótkich tekstów – konkretnie tweetów.
  2. Tweety dotyczą trzech dziedzin tematycznych:
    • COVID – znakomita większość (166543 – 81,7%)
    • Finanse – pewna część (28874 – 14,1%)
    • Grammy’s – margines (8490 – 4,2%)
  3. W ramach systemu przekazujemy tekst od użytkownika. W odpowiedzi dostajemy 5 najbardziej podobnych tweetów.

Efekty

Poniżej kilka efektów. Chcę zauważyć, że sporą rolę odgrywa tutaj kwestia nierówności zbiorów. Dane związane z ceremonią przyznania nagród Grammy’s są właściwie marginalne (nieco ponad 4%). Tweety COVIDowe zapełniają natomiast nasz zbiór w ponad 80%. Jest to istotne, gdyż przy sprawdzaniu efektywności najbardziej naturalnym odniesieniem jest zwykłe prawdopodobieństwo. W zbiorze 100 “najbardziej podobnych” tekstów (do jakiegokolwiek), ok 80 powinno być związanych z COVID-19, nieco ponad 10 to najpewniej finansowe, natomiast muzyczne będą w liczbie kilku sztuk.

Text Similarity w Apache Spark na przykładzie wywołania tweetów związanych z COVID-19

Fraza covidowa, najprostsza

Wyszukiwania zacznijmy od najprostszego podejścia: frazą wyszukiwaną niech będzie podobna do tej, o której wiemy, że istnieje w podanym zbiorze. Liczba w nawiasie to stopień podobieństwa – od -1 do 1 (gdzie 1 to identyczne).

Fraza: Praying for health and recovery of @ChouhanShivraj . #covid #covidPositive (zmiany są bardzo drobne).

Podobne wykryte frazy:

  1. Praying for good health and recovery of @ChouhanShivraj (0.9456217146059263)
  2. Prayers needed for our vascular surgeon fighting Covid @gpianomd #COVID19 #Prayers #frontlinedoctors (0.8043357071420172)
  3. Prayers for your good health and speedy recovery from #COVID19 @BSYBJP (0.801822609000082)
  4. Hon’ble @CMMadhyaPradesh Shri @ChouhanShivraj Ji tested #COVID19 positive. Praying for his speedy recovery. (0.7740378229093525)
  5. I pray for Former President Shri @CitiznMukherjee speedy recovery. Brain tumor wounds ji a lot, God may heal his p…  (0.7626450268959205)

Jak widać każda z tych fraz pochodzi z grupy COVIDowych. Dodatkowo dotyczy pragnień szybkiego powrotu do zdrowia oraz modlitwy za cierpiących.

Fraza finansowa, trudniejsza

Przejdźmy do czegoś odrobinę trudniejszego – sprawdźmy coś finansowego. Niech będzie to fraza, którą absolutnie wymyślę od początku do końca.

Fraza: Ford’s earnings grow another quarter

Podobne wykryte frazy:

  1. XLE Goes Positive Brent UP Big &amp; WTI UP Big Rally $XOM ExxonMobil Buy Now for the Rest of 2018 GASOLINE INVENTORIE… (0.7579525402567442)
  2. Morgan Stanley Begins Covering Murphy Oil $MUR Stock. “Shares to Hit $26.0” (0.7211353533183933)
  3. Seaport Global Securities Lowers Cabot Oil &amp; Gas Q2 2018 Earnings Estimates to $0.15 EPS (Previously $0.17).… (0.7211353533183933)
  4. William E. Ford Purchases 1000 Shares of BlackRock Inc. $BLK Stock (0.7195004202231048)
  5. Anadarko Petroleum Is On A Buyback Binge $APC (0.7187907206133348)

W tym przypadku podobieństwa są znacznie mniejsze. Warto zauważyć jednak dwie rzeczy: Po pierwsze – system wskazuje, że podobieństwa są mniejsze (0.76 to dużo mniej niż 0.95). Prawdopodobnie bardzo podobne po prostu więc nie istnieją. Druga rzecz – wszystkie podobne tweety pochodzą ze zbioru finansowych! Zbioru, który stanowi ok 14% całości danych. Pozwala to nabrać przekonania, że odpowiedzi nie są przypadkowe.

Fraza muzyczna, najtrudniejsza

Na koniec – najtrudniejsze zadanie ze wszystkich. Wybierzemy zdanie, które teoretycznie powinno pasować do zbioru będącego marginesem całości – do Grammy’s. Dodatkowo zdanie to wymyślę całkowicie. A niech tam – niech dotyczy najwspanialszej piosenkarki w dziejach! Oczywiście moim, zupełnie subiektywnym i amatorskim okiem;-).

Fraza: Amy Lee is the greatest singer of all time!

  1. Christina Aguilera &amp; Norah Jones were the only multiple recipients for ‘Best Female Pop Vocal Performance’ in the 2000s. (0.7306395709876714)
  2. @billboardcharts @justinbieber @billieeilish @oliviarodrigo @taylorswift13 @kanyewest Taylor the only real queen let’s be honest she deserves the Grammy for evermore but the #GRAMMYs wont give her. (0.7019156211438091)
  3. #GRAMMYs keep doing dirty to Lana Del Rey? Even though her talent is among the best this world has to offer (0.6868772967554752)
  4. Kylie Minogue deserved at least one nomination for Magic #GRAMMYs (0.6820704278110573)
  5. The answer will always be YES. #GRAMMYs #TwitterSpaces #SmallBusinesses #BlackOwned #adele #bts (0.6816903814884498)

I to właśnie te wyniki, przyznam, najmocniej wprowadziły mnie w euforię i ekscytację, gdy je zobaczyłem. I to nie tylko z powodu mojego niekłamanego uczucia do wokalistki Evanescence. Gdy spojrzymy na to “zdrowym, chłopskim okiem”, nie ma tutaj słowa o Grammy’s. Nie ma też szczególnego podobieństwa w słowach między pięcioma wymienionymi tweetami. Jest za to… kontekst. Jest podobieństwo tematyczne, jest znaczenie sensu.

A to wszystko naprawdę niedużym kosztem:-).

Text Similarity w Apache Spark na przykładzie wywołania tweetów muzycznych (z Grammy’s)

Apache Spark a text similarity – wykorzystane techniki

No dobrze, ale przejdźmy do konkretów – co należy zrobić, aby dostać takie wyniki? Tu zaproszę od razu do następnego artykułu, w którym pokażę dokładniej jak to zrobić. Dzisiejszy potraktujmy jako zajawkę. Żeby nie przeoczyć następnego – zapisz się na newsletter;-).

 

Loading

Po dość długich staraniach i wyeliminowaniu kilku ewidentnie beznadziejnych podejść, za sprawą kolegi Adama (za co ukłony w jego stronę) zacząłem czytać o embeddingach USE (Universal Sentence Encoder). Od razu powiem, że moją podstawową działką jest Big Data rozumiane jako składowanie i przetwarzanie danych. Sztuczna inteligencja to dopiero temat, który badam i definitywnie nie jestem w nim specem (choć parę kursów w tym kierunku ukończyłem i coś niecoś działałem). Liczę jednak, że obcowanie ze specami w tej działce (takimi jak właśnie Adam;-)) pomoże w eksploracji tego ciekawego gruntu.

Wróćmy jednak do USE. To była istne objawienie. Warto zaznaczyć, dla tych którzy nie do końca są zaznajomieni z tematyką machine learning, że komputer oczywiście tak naprawdę nie rozumie tekstu. Żeby mógł wyszukiwać podobieństwa, dzielić na grupy, uczyć się klas itd – potrzebne są liczby. Stąd wziął się pomysł sprowadzania tekstów do wektorów i różnego rodzaju wectorizerów – mechanizmów, które sprowadzają tekst do wektorów. Wektorów, czyli tablic jednowymiarowych o określonej długości (tu można się pomylić. Wielowymiarowe wektory dalej są jednowymiarowymi tablicami). Nieco bardziej rozbudowaną wersją są embeddingi, które mogą przechowywać w sobie wektory, natomiast które posiadają dodatkowe cechy pomocne. Jedną z nich (kluczową?) jest to, że słowa które chcemy zamienić na liczby, nabierają kontekstu. Pomaga to szczególnie mocno w niektórych przypadkach – na przykład naszych tweetów, które zawierają krótkie, czasami niezbyt treściwe przekazy. Jeśli będziemy je porównywali w prosty, czysto “statystyczny” sposób zestawiając wyrazy, nie uzyskamy odpowiedniego efektu.

Machine Learning w Apache Spark

Aby korzystać z dobrodziejstw ludzkości w zakresie machine learning, w tym text similarity w Apache Spark, należy wykorzystać bibliotekę Spark MlLib (w repozytorium Mavena dostępna tutaj). Tylko tutaj UWAGA! Wewnątrz biblioteki MlLib dostępne są dwa “rozgałęzienia”:

  1. Spark MlLib – starsza (choć wciąż utrzymywana) wersja, operująca bezpośrednio na RDD.
  2. Spark ML – nowocześniejsza część biblioteki. Możemy tutaj pisać operując na Datasetach i Dataframe’ach.

Wracając do technik – jednym z embeddingów jest właśnie USE. Jest on znacznie znacznie lepszym rozwiązaniem niż nieco podstarzały word2Vec, o innych, prostszych (np. Count Vectorizer) nie wspominając. Problem? Ano właśnie – nie wchodzi on w skład podstawowej biblioteki MlLib. Nie jest to jednak problem nie do przeskoczenia. Istnieje w Internecie gotowy zestaw rozwiązań, które poszerzają podstawowe biblioteki Sparkowe. Mam tu na myśli John Snow Labs. Udostępniają oni naprawdę imponująca liczbę algorytmów, które po prostu możemy wykorzystać – i to z całkiem niezłym skutkiem. Omówienie poszczególnych algorytmów można znaleźć tutaj. Samą bibliotekę do przetwarzania tekstu, czyli Spark-NLP zaciągniemy bez problemu z głównego repozytorium Mavena. To dzięki niej możemy rozwiązać bardzo wiele problemów, m.in. text-similarity w Apache Spark;-)

Jak technicznie dokładnie to zrobić, pokażę w kolejnym artykule. Już teraz zapraszam do subskrybowania;-).

Cosine Similarity

Skoro tylko udało mi się już porządnie sprowadzić tekst do jakiś ludzkich kształtów (czyli liczb;-)), należało znaleźć najlepszy z nich. Najlepszy – czyli najbardziej podobny do takiego, który wprowadzę. Dość dużo spędziłem czasu na szukaniu różnych gotowych rozwiązań. W końcu zdecydowałem się na zastosowanie czystej matematyki. Mowa tu o cosine similarity. Nie mam pojęcia jak to się tłumaczy na polski, a “podobieństwo kosinusowe” mi po prostu nie brzmi (ani nie znalazłem żeby tak się mówiło).

Z grubsza sprawa jest dość prosta – chodzi o to, żeby znaleźć podobieństwo między dwoma (niezerowymi) wektorami osadzonymi na jakiejś płaszczyźnie. Nie jest to żadna technika rakietowa i nie dotyczy ani NLP, ani nawet machine learning. Dotyczy zwykłej, prostej, nudnej matmy i można się zapoznać nawet na wikipedii.

Wzór na cosine similarity wygląda następująco:

{\displaystyle {\text{cosine similarity}}=S_{C}(A,B):=\cos(\theta )={\mathbf {A} \cdot \mathbf {B} \over \|\mathbf {A} \|\|\mathbf {B} \|}={\frac {\sum \limits _{i=1}^{n}{A_{i}B_{i}}}{{\sqrt {\sum \limits _{i=1}^{n}{A_{i}^{2}}}}{\sqrt {\sum \limits _{i=1}^{n}{B_{i}^{2}}}}}},}

Efekt jest prosty: wynik jest od -1 do 1. Im bliżej 1 tym bliższe są oba wektory. Problem? Oczywiście – w Sparku nie ma implementacji;-). Na szczęście jest to wzór na tyle prosty, że można go sobie zaimplementować samemu. Ja to zrobiłem w ramach UDF.

Podsumowanie

I to tyle! Tak więc można sprawę uprościć: zebrałem tweety, użyłem USE (od John Snow Labs) oraz cosine similarity. W efekcie dostałem naprawdę solidne wyniki podobieństwa. I to nie tylko jeśli chodzi o sam tekst, ale przede wszystkim jego znaczenie.

Już w najbliższym artykule pokażę jak dokładnie napisałem to w Sparku. Jeśli interesują Cię zagadnienia dotyczące Sparka, pamiętaj, że prowadzimy bardzo ciekawe szkolenia – od podstaw. Pracujemy z prawdziwymi danymi, na prawdziwych klastrach no i… cóż, uczymy się prawdziwego fachu;-). Jeśli interesuje Cię to – Zajrzyj tutaj!

Zostań z nami na dłużej. Razem budujmy polskie środowisko Big Data;-). Jeśli chcesz pozostać z nami w kontakcie – zapisz się na newsletter lub obserwuj RDF na LinkedIn. Koniecznie, zrób to i razem twórzmy polską społeczność Big Data!

 

Loading