Sztuczna inteligencja

Original article was published by Karol Wieliczko on Artificial Intelligence on Medium


Analiza sentymentu, sztuczna inteligencja

Czyli jak zrobić, aby maszyna rozumiała naturalny język polski.

Pokrótce opiszę jak przy użyciu języka Python napisać analizator sentymentu, i jak dzięki temu wszczepić elementy sztucznej inteligencji, oraz nauczyć program podejmowania samodzielnych decyzji.

Algorytm, po przeanalizowaniu kilkuset tysięcy wpisów internautów, pochodzących z portalu zawierającego Opinie o produktach i firmach, będzie w stanie samodzielnie podejmować decyzje wyłącznie na podstawie analizy języka naturalnego.

Zanim omówimy budowę programu, zapoznaj się z jego działaniem. Po prostu wystaw opinię fikcyjnej firmie, lub produktowi jaki ostatnio kupiłeś a automat postara się odgadnąć, czy jesteś z zakupu zadowolony, czy nie.

Demo

Algorytmy tego typu mają swoje zastosowanie między innymi w filtrach antyspamowych, w których model, po odpowiednim wytrenowaniu, jest w stanie automatycznie podejmować decyzje dotyczące każdej nowej wiadomości e-mail.

Pełny kod źródłowy aplikacji udostępniam na swoim koncie na GitHubie.

Aby zacząć przygodę z algorytmami machine learning potrzebujemy narzędzi takich jak środowisko języka Python, oraz zainstalowane narzędzie Jupyter notebook. Nic więcej.

Aha! Potrzebujemy również danych 🙂

Pisząc program uświadomiłem sobie, skąd wzięło się nagłe zainteresowanie firm z sektora IT bezustannym zbieraniem danych i dlaczego przycisk “Lubię to” czy emotikony są dla nich tak bardzo ważne.

W naszych rozważaniach postaramy się skorelować dwie, nierozerwalnie związane wielkości:

  • Treść napisaną językiem potocznym,

oraz

  • Ocenę numeryczną, czyli liczbę gwiazdek jaką zdecydował się wystawić firmie lub produktowi internauta.

Dla nas, osób czytających opnie na portalach, skojarzenie zależności między nimi, to sprawa oczywista. Postarajmy się, aby maszyna również zauważyła
tę zależność.

Nasz program składać się będzie z trzech modułów głównych:

  • Scrappera — programu hurtowo pobierającego wpisy z portalu internetowego służącego do wystawiania opinii na temat firm
    i produktów,
  • Modułu czyszczącego dane i klasyfikatora — czyli kilku funkcji usuwających zbędne słowa, nieprawidłowe wpisy, oraz — i tu jest właśnie sedno sprawy — klasyfikatora!,
  • Interfejsu www — czyli prostego formularza internetowego wzbogaconego o kilka graficznych i dźwiękowych bajerów.

Rozpocznijmy od Scrappera

Większość operacji dziać będzie się w głównej klasie main, oraz w jej metodzie uruchomieniowej run. Okresowo sięgać będziemy również do innych klas, w których przechowujemy różne, przydatne metody, pogrupowane “tematycznie”.

Na początek utwórzmy obiekt klasy http_connect_class

from modules import http_requesthttp_request_obj = 
http_request.http_connect_class()

i pobierzmy zawartość całej strony portalu do zmiennej soup

def get_html(self, url):request = urllib.request.Request(url)
response = urllib.request.urlopen(request)

soup = BeautifulSoup(response, 'html.parser')

return soup
soup
= http_request_obj.get_html(url)

W kodzie portalu, liczba gwiazdek oraz tekst recenzji, w uproszczeniu wygląda tak:

<span class='review positive'>5</span>

<div class='text '>
<span>Przesyłka na czas. Produkt zgodny z opisem. Polecam :)</span>
</div>

lub

<span class='review negative'>1</span>

<div class='text '>
<span>Bateria w telefonie trzyma pół dnia. Nie polecam!</span>
</div>

Przy użyciu biblioteki BeautifulSoup parsujemy kod i budujemy listy zawierające ocenę oraz tekst opisu:

from bs4 import BeautifulSoup
import re
rating_star_list = soup.findAll('span', {'class':
re.compile("^review*")})
text_review_list = soup.findAll("div", {"class": "text"})

Z uwagi na to, że użyliśmy wyrażenia regularnego “^review*” zapisujemy do listy zarówno tagi ’review negative’ jak i ’review positive’.

Teraz powołujemy nową instancję klasy db_sql_lite_class,

from modules import sqlitedb_obj = sqlite.db_sql_lite_class()

otwieramy podręczną bazę SQLite:

def open_database(self, db_path):

try:
conn = sqlite3.connect(db_path)
except:
print("Błąd otwarcia bazy:", db_path)

return conn

conn = db_obj.open_database(db_path)

i zapisujemy przechowywane w zmiennych dane z portalu

def save_data_in_database(self, conn, db_path, rating_star, text_review):

try:
conn = sqlite3.connect(db_path)

sql = '''INSERT INTO reviews VALUES ('''+str(rating_star)+''',"'''+text_review+'''"); '''

c = conn.cursor()
c.execute(sql)
c.execute('commit')
conn.close()
except:
print("Błąd zapisu do bazy", db_path)
db_obj.save_data_in_database(conn, db_path, rating_star, text_review)

Praca scrappera została wykonana a dane zostały zapisane do bazy:

Klasyfikator

Prace nad klasyfikatorem prowadzić będziemy w narzędziu Jupyter notebook.

Jupyter notebook

To bardzo wygodne narzędzie, gdzie logiczne fragmenty kodu możemy przetwarzać w oddzielnych komórkach. Taka organizacja przestrzeni roboczej pozwala nam na wykonywanie długotrwałych operacji (np. trenowania modelu) tylko jeden raz. W oddzielnych komórkach możemy prowadzić dalszą analizę wyników, bez konieczności ponawiania długotrwałej operacji.

Połączmy się z naszą bazą danych a wynik przechowajmy w obiekcie DataFrame z biblioteki Pandas.

import sqlite3
import pandas as pd
cnx = sqlite3.connect('db.sqlite')df = pd.read_sql_query("SELECT text_review, rating_star FROM reviews", cnx)
df
DataFrame

Usuńmy zbędne słowa (stopwords), które nic nie wnoszą do wypowiedzi.

stop = ['a','aby','ach','acz','aczkolwiek'   [...]    'że','żeby']
def tokenizer(text):
tokenized = [w for w in text.split() if w not in stop]
return tokenized

Jak działa metoda tokenizer? Metoda przyjmuje jako parametr tekst:

tokenizer("Bardzo polecam tę firmę. Przesyłka została zrealizowana w ekspresowym czasie a towar spełnia moje oczekiwania.")

natomiast zwraca listę składającą się z pojedynczych słów, bez słów nieistotnych takich jak: bardzo, tę, w, a, moje.

Wyłuskaliśmy więc “sedno” wypowiedzi.

['polecam',
'firmę',
'przesyłka',
'została',
'zrealizowana',
'ekspresowym',
'czasie',
'towar',
'spełnia',
'oczekiwania']

Przyjrzyjmy się teraz poniższemu kodowi. Funkcja stream rozdziela każdy rekord na część tekstową i etykietę liczbową.

Podczas wstępnej obróbki danych w DataFrame, przyjąłem następującą zależność:

  • recenzja oceniona na 4 lub 5 gwiazdek jest pozytywna, czyli etykieta= 1
  • recenzja oceniona na 1, 2 lub 3 jest negatywna, czyli etykieta= 0

Operację owińmy blokiem try, except, ponieważ pojedyncze rekordy mogą być problematyczne i w takim wypadku, po prostu pomińmy je.

def stream(path):
with open(path, 'r', encoding='utf-8') as csv:for line in csv:
try:
text, label = line[:-3], int(line[-2])
except Exception as e:
print("Wystąpił błąd:",e)
pass

yield text, label

W tym miejscu podkreślmy pewną zasadę w obróbce danych przeznaczonych do trenowania modelu jaką znalazłem w literaturze, a dokładnie w książce “Big data — efektywna analiza danych” Viktora Mayer-Schonbergera.

Otóż:

“Pojedyncze rekordy danych nie stanowią dużej wartości samej w sobie.
W analizie najważniejszy jest
trend.”

from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifiervect = HashingVectorizer(decode_error='ignore',
n_features=2**21,
preprocessor=None,
tokenizer=tokenizer)clf = SGDClassifier(loss='log', random_state=1)
doc_stream = stream_docs(path='./input_data-pl.csv')X_test, y_test = get_minibatch(doc_stream, size=100)X_test = vect.transform(X_test)print('Dokładność: %.3f' % clf.score(X_test, y_test))clf = clf.partial_fit(X_test, y_test)import pickle
import osdest = os.path.join('pkl_objects')
if not os.path.exists(dest):
os.makedirs(dest)pickle.dump(stop, open(os.path.join(dest, 'stopwords.pkl'), 'wb'), protocol=4)
pickle.dump(clf, open(os.path.join(dest, 'classifier.pkl'), 'wb'), protocol=4)clf = pickle.load(open(os.path.join('pkl_objects', 'classifier.pkl'), 'rb'))import numpy as np
label = {0:'Negatywna', 1:'Pozytywna'}example = ['Sklep jest super']
X = vect.transform(example)
print('Prognoza: %s\nPrawdopodobieństwo: %.2f%%' %\
(label[clf.predict(X)[0]], clf.predict_proba(X).max()*100))Prognoza: Pozytywna
Prawdopodobieństwo: 98.99%

c.d.n.