Klasyfikacja opinii w oparciu o treść

Klasyfikacja Opinii w oparciu o treść
Analiza danych Machine learning
2022-03-19

Głównym celem projektu było stworzenie modelu, który będzie potrafił zaklasyfikować opinię jako pozytywną lub negatywną jedynie na podstawie treści owej opinii. Dla człowieka zadanie jest całkiem proste, ale czy komputer zrobi to z taką samą łatwością?

Na jakich danych będziemy pracować?

Dane wykorzystane do uczenia a także testowania modeli pochodzą ze strony opineo.pl i dotyczą firmy DHL (wybór firmy jest przypadkowy – po prostu mieli dużo opinii). Zdobycie danych było możliwe dzięki wykorzystaniu tzw. web scraping (stworzony bot pobrał opinie, liczbę gwiazdek oraz informację czy jest ona pozytywna czy negatywna). Dane zostały następnie zapisane do pliku csv:

  • Star – Liczba gwiazdek z danej opinii
  • Information – informacja czy opinia jest pozytywna, negatywna czy neutralna (przypisywane na stronie na podstawie liczby gwiazdek)
  • Opinion – tekst opinii

Analiza danych

Zdecydowana większość danych to opinie pozytywne.

Możemy też sprawdzić częstość występowania słów w zależności od tego czy opinia jest pozytywna, negatywna czy neutralna.  Treść opinii została zlematyzowana (sprowadzona do podstawowych form). Dzięki temu to samo słowo występujące kilka razy w różnych formach nie będzie traktowane jako kilka słów (np. kurier, kurierowi). Dzięki lematyzacji nie trzeba normalizować tekstu, tj. zamieniać go na małe litery – lematyzacja uwzględnia pospolitość słów i zlematyzowana forma zaczyna się dużą/małą literą, zgodnie z tym, jak należy ją pisać.

Wykresy częstotliwości (kod)

def plot_word_freq(text, stopwords, n=30):
    # remove stopwords if obligatory
    if stopwords is None:
        text = text.split()
    else:
        text = [word for word in text.split() if word not in stop_words.values.flatten()]

    # create word counter
    words = Counter()
    words.update(text)

    # create df containing word frequency
    popular_words = list(map(lambda x: x[0], words.most_common()))
    amount = list(map(lambda x: x[1], words.most_common()))
    df = pd.DataFrame({'Word': popular_words, 'Amount': amount})

    # plot barplot
    plt.figure(figsize=(20, 10))
    sns.set(font_scale=1.5)
    sns.barplot('Amount', 'Word', data=df.iloc[:n])
    plt.show()
import re
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from collections import Counter

reviews = pd.read_csv('path_to_reviews_csv')
stopwords = pd.read_csv('path_to_stopwords')
for inf in ['pos', 'neg', 'neu']:
    # choose opinion type
    opinions = reviews['Opinion'][reviews['Information'] == inf]
    # remove punctuation marks for word cloud
    opinions = opinions.apply(lambda x: re.sub('[^a-żA-Ż]', ' ', x))
    # join opinions in one long text (in lowercase)
    opinions = " ".join(opinions.values)
    opinions = opinions.lower()
    # lemmatize text
    opinions = lemmatize_text(opinions)
    # create plot
    plot_word_freq(opinions, stopwords, n=30)
(a) negatywne
(b) neutralne
(c) pozytywne

Wykresy częstotliwości słów dla neutralnych i negatywnych opinii wyglądają prawie identycznie (dlatego też dalsza część analizy skupia się jedynie na pozytywnych i negatywnych opiniach). Poza tym opinie zakwalifikowane jako neutralne są często tak bardzo podobne do pozytywnych lub negatywnych, że nawet człowiek miałby problem zdecydować jaki to typ opinii. Oto kilka przykładów:

  • Pozytywne
    • Mily i uprzejmy  
    • bardzo dobra 
    • szybko 
    • kurier miły, pomocny, kontaktuje się przed przyjazdem. 
    • Bardzo miła obsługa
  • Neutralne
    • Bardzo sympatyczny kontakt, punktualność, niestety brak wcześniejszej informacji o dostawie. 
    • sprawna dostawa. 
    • Ok 
    • Dowiozl szybko ale bez kontaktu ze mna zpstawił paczke sasiadowi wiec niewiedzialem gdzie jest paczka. 
    • Jestem rozczarowana i zawiedziona.Trz przesyłki do podanego adresu tylko przekazuje przez osoby postronne paczke takie jak np. przypadkowy sąsiad spodkany w innej miejscowosci.Nie po to płacimy wysoki koszt dostawy aby nie dowoził jej na podany adres:(
  • Negatywne
    • Dostawa bez uszkodzeń.
    • Totalna porażka firmy kurisrskiej DHL. Przesyłka powinna byc w poniedziałek a dostałem ja w czwartek.
    • Słabo
    • Poza wszelką krytyką, skrajnie nieuprzejma obsługa, kurier zapomniał chyba że pracuje dla klienta a nie sam dla siebie…

Preprocessing danych

Strona opineo.pl oferuje program opiconnect, w ramach którego po otrzymaniu negatywnej opinii firma może skontaktować się z osobą wystawiającą opinię negatywną i wyjaśnić zaistniały problem. W związku z tym, wśród opinii mogły znaleźć się takie, których treść zamieniono na:  Opinia jest w trakcie OpiConnect. Strony nawiązały kontakt w celu wyjaśnienia sytuacji. Proces dialogu zakończy się do dnia dd.mm.rrrr. Takie opinie oczywiście należało usunąć ze zbioru. Część opinii negatywnych miała dopisek: Opinia była przedmiotem dialogu w ramach procesu OpiConnect. Strony nie osiągnęły porozumienia. Wówczas sam ten dopisek należało usunąć z treści. 

import os
import pandas as pd
from transformers import RobertaModel, PreTrainedTokenizerFast

model_dir = 'directory_to_RoBERTa_model'
tokenizer = PreTrainedTokenizerFast(tokenizer_file=os.path.join(model_dir, "tokenizer.json"))
model = RobertaModel.from_pretrained(model_dir, output_hidden_states=True)
reviews = pd.read_csv('path_to_reviews_csv')

for i, review in enumerate(reviews['Opinion']):
    # skip opinions if is in the middle of OpiConnect process (then text of opinion is hidden)
    if 'Opinia jest w trakcie OpiConnect' in review:
        continue
    # remove information that opinion was in OpiConnect before
    elif 'OpiConnect' in review:
        review = review.replace('Opinia była przedmiotem dialogu w ramach procesu OpiConnect. \
        Strony nie osiągnęły porozumienia.', '')
        review = review.replace('Opinia była przedmiotem dialogu w OpiConnect. Strony osiągnęły porozumienie.')
        # text to vector
        input_ = tokenizer.encode(review)
        output = model(torch.tensor([input_]))[1]

Tak wyczyszczone opinie zostały zamienione na wektory długości 1024 za pomocą modelu polish-RoBERTa (zobacz https://github.com/sdadas/polish-roberta ).  Jest on oparty na modelu BERT (Bidirectional Encoder Representations from Transformers) zaproponowanym przez Google pod koniec 2018 roku. Więcej o algorytmie RoBERTa dowiesz się tu: BERT – czyli jak bohater Ulicy Sezamkowej zmienia wyszukiwark.

Modele

Do uczenia wykorzystamy 3 modele, aby rozważyć 3 możliwe podejścia:

  • Klasyczny model uczenia maszynowego – Las Losowy
  • Uczenie głębokie – sieć neuronowa
  • Detekcja outlierów (gdzie opinie negatywne traktujemy jako outliery) – DBSCAN

Jak działa las losowy?

Las losowy jest przykładem uczenia zespołowego (ensemble models). W modelu wykorzystuje się określoną liczbę drzew decyzyjnych. Każde drzewo jest uczone na nieco innej próbce danych, ponieważ każde drzewo uczy się na bootstrapowanych danych tj. sztucznie wygenerowanym zbiorze, gdzie przeprowadza się losowanie ze zwracaniem z właściwego zbioru danych. Decyzja o przypisanej klasie (opinia pozytywna lub negatywna) podejmowana jest w oparciu o “głosowanie” drzew decyzyjnych.

Jak działa sieć neuronowa?

Sieci neuronowe inspirowane są połączeniami neuronów jakie istnieją w naszych mózgach 😱😱.  Podstawową jednostką w sieci neuronowej jest neuron. Wewnątrz niego odbywają się obliczenia.

Sieć neuronowa składa się z warstw, a każda warstwa składa się z pewnej ilości pojedynczych neuronów. Warstwy dzielą się na 3 typy: warstwa wejściowa (zawsze pierwsza), warstwa wyjściowa (zawsze ostatnia) oraz warstwy ukryte (wszystkie warstwy pomiędzy wejściową a wyjściową).

Liczba neuronów w warstwie wejściowej jest uzależniona od liczby zmiennych, a liczba neuronów wyjściowych od liczby klas – w przypadku gdy sieć służy do rozwiązania problemu klasyfikacyjnego (czyli np. rozważany problem). W warstwach ukrytych liczba neuronów może być w ramach doboru hiperparametrów. Zazwyczaj (chociaż nie zawsze) trudno jest określić jaka liczba neuronów powinna znaleźć się w poszczególnych warstwach ukrytych.

Jak działa DBSCAN

Ostatnim wykorzystanym modelem był DBSCAN – algorytm klasteryzacji, wykorzystywany do detekcji outlierów (identyfikacji obserwacji odstających). Korzystając z tego algorytmu należy zdefiniować maksymalną odległość pomiędzy obserwacjami, aby móc je zaliczyć do jednej grupy, oraz minimalną liczbę obserwacji, które muszą wystąpić “w pobliżu siebie”, aby rozważyć je jako jedną grupę.

To  dwa najważniejsze parametry jakie wykorzystuje DBSCAN. Algorytm dokonuje klasteryzacji (grupowania) obserwacji.  Jeżeli jakaś obserwacja jest na tyle daleko od pozostałych, że nie może zostać zaliczona do żadnej grupy (lub jest kilka takich obserwacji, ale ich liczba jest za mała, aby stworzyły osobną grupę), to takie obserwacje są uznawana za odstające.

Ze względu na bardzo nierówny podział grup (bardzo dużo opinii pozytywnych i niewiele negatywnych) można było rozważać problem jako problem identyfikacji odstających. 

Jeżeli ustalimy, że maksymalna odległość to “r” , minimalna liczba obserwacji – 3, to w przypadku przedstawionym na rysunku algorytm znajdzie dwie grupy (niebieskie i pomarańczowe kółka). Trójkąty są tylko 2, więc jest ich za mało, by tworzyć kolejną grupę. Są również za daleko od pozostałych, aby zaliczyć je do którejś z już utworzonych grup. Algorytm uzna zatem, że trójkąty są obserwacjami odstającymi.

Wyniki i wnioski

Do oceny modeli korzystamy precyzję (precision – stosunek liczby poprawnie sklasyfikowanych pozytywnych / negatywnych obserwacji do wszystkich zakwalifikowanych jako pozytywne / negatywne), czułość (recall – stosunek liczby poprawnie sklasyfikowanych pozytywnych / negatywnych do wszystkich pozytywnych / negatywnych), F1-score (średnia harmoniczna pomiędzy precyzją i czułością) oraz dokładność (accuracy – stosunek liczby poprawnie zakwalifikowanych próbek do wszystkich- Tu nie wyróżnia się podziału na klasy jak w przypadku precyzji czy czułości). Za najlepszy model uznamy ten, dla którego najwięcej metryk okaże się najlepszych.

Uwaga!

Takie podejście do wyboru najlepszego modelu jest spowodowane brakiem jasno sprecyzowanego celu jaki chcemy osiągnąć. Należy pamiętać, że metrykę służącą do oceny modeli należy dostosowywać do rozważanego problemu.

Np. chcąc stworzyć test klasyfikujący czy osoba cierpi na jakąś chorobę, to ważniejsze będzie poprawne zaklasyfikowanie jak największej liczby osób chorych niż ogólna dokładność modelu. W takim przypadku wybór modelu opierałby się o wartość recall (czułość) w grupie chorych.

Dla metody DBSCAN precyzja negatywnych próbek wypadła bardzo słabo. Być może jest to skutek tego, że nie były one “wystarczająco daleko” od obserwacji pozytywnych. Z drugiej strony recall negatywnych próbek wypadł bardzo dobrze. Metoda ta jednak najczęściej zwracała najsłabsze wyniki. Sieć neuronowa z kolei najczęściej dawała najlepsze metryki. Las losowy zazwyczaj był trochę słabszy niż sieć neuronowa. 

Kilka słów na koniec

Bardzo się cieszę, że poświęciłe_aś swój czas, aby przejść razem ze mną przez ten wpis! Jeżeli Cię zaciekawiłam i chcesz spróbować własnych sił w podobnym zadaniu, zajrzyj na mojego githuba, gdzie znajdziesz cały kod wykorzystany do projektu https://github.com/kingagla/reviews_classification.

Co można ulepszyć / plan na rozwinięcie projektu

  • Sztuczne zwiększenie liczby negatywnych opinii (poprzez generowanie ich) mogłoby się przyczynić do zwiększenia skuteczności modelu / lepszego rozpoznawania opinii negatywnych.
  • Zastosowanie XAI – pomogłoby zorientować się jakie słowa występujące w opiniach powodują nieprawidłowe rozpoznawanie klasy. Być może niektóre z nich okażą się nie być kluczowe dla rozpoznania sentymentu przez człowieka i usunięcie ich (być może) pomogłoby w podniesieniu skuteczności.

Będzie mi bardzo miło jeśli zostawisz po sobie ślad w formie komentarza. Widzimy się w następnym wpisie! 😊

Subskrybuj
Powiadom o
guest
2 komentarzy
Najnowsze
Najstarsze Najczęściej oceniane
Zobacz wszystkie komentarze
Anonimowo
Gość
Anonimowo
2 lat temu

Cześć Kinga. Ciekawy wpis. Zauważyłem, że RF ma słabe wyniki na negativesamples, a przy tak zdefiniowanej klasyfikacji binarnej, RF jest z zasady dobrym rozwiązaniem. Wobec tego, czy możesz coś więcej napisać o metodach sztucznego zwiększania training setu, by zmniejszyć wpływ klasy dominującej?
Pozdrawiam.