Handleiding sentimentanalyse in Python: classificeren van recensies over films en producten
Inleiding
Sentimentanalyse in combinatie met machine learning wordt vaak gebruikt om te begrijpen hoe positief of negatief een doelgroep zich voelt over een bepaalde entiteit, zoals een film, productlijn of politieke kandidaat. De belangrijkste methode om dit te ontdekken is het verzamelen van tekstvoorbeelden van de doelgroep (of dat nu tweets, klantenservicevragen of, in dit geval, productbeoordelingen zijn). Het is een belangrijk onderdeel van natuurlijke taalverwerking. Deze tutorial leidt je stap voor stap door het proces van sentimentanalyse met behulp van een random forest classifier die behoorlijk goed presteert. We maken gebruik van Dimitrios Kotzias's Sentiment Labelled Sentences Data Set, gehost door de Universiteit van Californië, Irvine. Het bevat filmrecensies van IMDB, restaurantrecensies van Yelp en productrecensies van Amazon. Deze gids behandelt veel fundamentele machine learning concepten die je dan kunt toepassen in je volgende project. Als je de codevoorbeelden volgt, heb je een heel nuttige, inzichtelijke (en leuke) techniek tot je beschikking. ### Structuur van de tutorial Deze tutorial is verdeeld in de volgende secties: 1. Bibliotheken downloaden met pip 2. De dataset openen 3. Samenvattende statistieken 4. Vectorisatie: vertalen van Engels naar computer-taal 5. Feature selectie 6. Splitten van de dataset: De train en test sets 7. De Classifier 8. Hyperparameteroptimalisatie: Maximale prestaties 9. Analyse van de resultaten ### Bibliotheken downloaden met pip Machine learning en data science kunnen snel ingewikkeld worden: machine learning algorithmes zijn vaak lang en ingewikkeld, en data op een betrouwbare manier organiseren kan lastig zijn. Gelukkig is veel van het voorbereidende werk al gedaan dankzij Python bibliotheken. Met behulp van deze bibliotheken kun je een neuraal netwerk bouwen, trainen en inzetten met een paar regels code in plaats van honderden. Om het ons makkelijker te maken, gebruiken we een aantal bibliotheken: * pandas * nltk * string * collections * sklearn * scipy Als je bij het volgen van de code een fout krijgt bij het importeren van een van deze modules, controleer dan of de module is geïnstalleerd door pip install modulenaam
te typen in de shell/command line. Bijvoorbeeld: {docker} pip install pandas pip install scipy
Je kunt ook een distributie van Python gebruiken, zoals Anaconda, die veel van deze bibliotheken al vooraf geïnstalleerd heeft. Dat gezegd hebbende, raad ik je aan om later tijd te nemen om sommige van deze dingen van de grond af te proberen, vooral het schrijven van code voor sommige machine learning algoritmen (neural networks en decision trees, bijvoorbeeld). Maak je hier nu nog geen zorgen over - deze tutorial is voor nu genoeg. Let op dat de stopwoordenlijst van nltk misschien niet vooraf gedownload is met het pakket. Als je problemen hebt met het importeren van de stopwoordenlijst, typ dit dan een keer in een Python shell of typ dit in je Python-bestand: {python} import nltk nltk.download('stopwords')
Toegang tot de dataset
We gaan de Sentiment Labelled Sentences Data Set van Dimitrios Kotzias gebruiken. Je kunt deze downloaden van deze link of van Kaggle.com hier.
De dataset heeft 3000 reviews van klanten van yelp.com, imdb.com, en amazon.com. De helft van de reviews is positief, en de andere helft negatief. Meer informatie over de dataset vind je ook via de links.
Als je het .zip-bestand gedownload hebt, kun je de inhoud uitpakken naar een locatie die je zelf kiest. Daarna moet je de drie .txt-bestanden in de map 'sentiment labelled sentences' openen in je Python omgeving of IDE. Voor een eenvoudige editor wordt vaak Jupyter (hier) gebruikt, vooral als je Anaconda Navigator installeert.
Eerst lezen we de data in Python:
def openFile(path):
#param path: path/to/file.ext (str)
#Geeft inhoud van bestand terug (str)
with open(path) as file:
data = file.read()
return data
imdb_data = openFile('C:/Users/path/to/file/imdb_labelled.txt')
amzn_data = openFile('C:/Users/path/to/file/amazon_cells_labelled.txt')
yelp_data = openFile('C:/Users/path/to/file/yelp_labelled.txt')
Nu de data geladen is, moeten we deze goed organiseren:
datasets = [imdb_data, amzn_data, yelp_data]
combined_dataset = []
# scheidt de voorbeelden van elkaar
for dataset in datasets:
combined_dataset.extend(dataset.split('\n'))
# scheidt elke label van elk voorbeeld
dataset = [sample.split('\t') for sample in combined_dataset]
Nu hebben we een lijst in de vorm van [['review', 'label']]
. Label '0' is negatief, en label '1' is positief.
De lijst is al een goed begin, maar kan rommelig worden. Daarom zetten we de data over naar een pandas DataFrame. Dit is een populaire datastructuur onder datawetenschappers en biedt veel handige functies.
import pandas as pd
df = pd.DataFrame(data=dataset, columns=['Reviews', 'Labels'])
# Verwijder lege reviews
df = df[df["Labels"].notnull()]
# Schud de dataset voor later gebruik,
# dit is goede gewoonte al is het niet noodzakelijk.
df = df.sample(frac=1)
Nu ziet de inhoud van die tekstbestanden er ongeveer zo uit (zie afbeelding):
Met onze data goed georganiseerd, kunnen we beginnen met echte sentimentanalyse en het classificeren van de inhoud.
Samenvattende statistieken
Voordat we meteen allerlei algoritmes loslaten op onze corpus, kan het nuttig zijn om even een stap terug te doen en te kijken naar patronen die we in de data kunnen zien. Door dit te doen voordat we in de machine learning duiken, kunnen we vanaf het begin een effectieve strategie kiezen en geen tijd verspillen aan onbelangrijke zaken.
We willen uiteindelijk verschillen vinden tussen negatieve en positieve reviews - dat is wat onze classificator uiteindelijk gaat doen. Omdat we te maken hebben met tekst, kunnen we misschien kijken naar dingen zoals:
- Zinslengte.
- Gebruik van hoofdletters
- Gebruik van leestekens
- Woordkeuze.
We bekijken elk van deze aspecten voor zowel de negatieve als positieve klassen en vergelijken de statistieken. Eerst berekenen we enkele van deze gegevens met lijstbegrip en voegen ze toe aan ons DataFrame.
import string
df['Woord Aantal'] = [len(review.split()) for review in df['Reviews']]
df['Hoofdletter Aantal'] = [sum(char.isupper() for char in review) \
for review in df['Reviews']]
df['Speciale Tekens Aantal'] = [sum(char in string.punctuation for char in review) \
for review in df['Reviews']]
Nu hebben we
We kunnen de ingebouwde statistische methoden van het DataFrame gebruiken om een samenvatting van elk van de nieuwe kolommen te krijgen. Laten we eens kijken naar enkele van de samenvattende statistieken.
Woord Aantal
positive_samples['Woord Aantal'].describe()
count 1500.000000
mean 11.885333
std 7.597807
min 1.000000
25% 6.000000
50% <a href="/nl/routers-number-of-ports/10">10</a>.000000
75% 16.000000
max 56.000000
Name: Woord Aantal, dtype: float64
negative_samples['Woord Aantal'].describe()
count 1500.000000
mean 11.777333
std 8.140430
min 1.000000
25% 6.000000
50% <a href="/nl/routers-number-of-ports/10">10</a>.000000
75% 16.000000
max 71.000000
Name: Woord Aantal, dtype: float64
Verder op deze manier:
Hoofdletter Aantal
positive_samples['Hoofdletter Aantal'].describe()
count 1500.000000
mean 1.972667
std 2.103062
min 0.000000
25% 1.000000
50% 1.000000
75% 2.000000
max 17.000000
Name: Hoofdletter Aantal, dtype: float64
negative_samples['Hoofdletter Aantal'].describe()
count 1500.000000
mean 2.162000
std <a href="/nl/routers-number-of-ports/3">3</a>.912624
min 0.000000
25% 1.000000
50% 1.000000
75% 2.000000
max 78.000000
Name: Hoofdletter Aantal, dtype: float64
Speciale Tekens Aantal
positive_samples['Speciale Tekens Aantal'].describe()
count 1500.000000
mean 2.140667
std 1.827687
min 0.000000
25% 1.000000
50% 1.500000
75% 3.000000
max 19.000000
Name: Speciale Tekens Aantal, dtype: float64
negative_samples['Speciale Tekens Aantal'].describe()
count 1500.000000
mean 2.165333
std 1.661276
min 0.000000
25% 1.000000
50% 2.000000
75% 3.000000
max 14.000000
Name: Speciale Tekens Aantal, dtype: float64
Deze statistieken laten zien dat er geen grote verschillen zijn tussen de klassen - wat deze features betreft, zijn negatieve en positieve samples bijna hetzelfde. Laten we kijken of we verschillen in de woordkeuze tussen beide categorieën kunnen ontdekken.
We zullen de termfrequentie meten met behulp van de Counter methode van Python, uit de collections bibliotheek. Eerst moeten we onze data een beetje voorbewerken.
from collections import Counter
def krijgMeestVoorkomendeWoorden(reviews, n_meest_voorkomende, stopwoorden=None):
# param reviews: kolom uit pandas.DataFrame (bijv. df['Reviews'])
#(pandas.Series)
# param n_meest_voorkomende: de top n meest voorkomende woorden in reviews (int)
# param stopwoorden: lijst van stopwoorden (str) om uit reviews te verwijderen (lijst)
# Geeft lijst van n_meest_voorkomende woorden georganiseerd in tuple als
#('term', frequentie) (lijst)
# platmaak reviews kolom tot een lijst van woorden, en zet ze allemaal in kleine letters
afgeplatte_reviews = [word for review in reviews for word in \
review.lower().split()]
# verwijder leestekens uit reviews
afgeplatte_reviews = [''.join(char for char in review if \
char not in string.punctuation) for \
review in afgeplatte_reviews]
# verwijder stopwoorden, indien van toepassing
if stopwoorden:
afgeplatte_reviews = [word for word in afgeplatte_reviews if \
word not in stopwoorden]
# verwijder eventuele lege strings die in dit proces zijn gemaakt
afgeplatte_reviews = [review for review in afgeplatte_reviews if review]
return Counter(afgeplatte_reviews).most_common(n_meest_voorkomende)
Vanwege de hoge frequentie van woorden als "the" en "and", hebben we een manier nodig om de bovenste woordenaantallen te bekijken zonder deze woorden. De nltk-bibliotheek heeft een vooraf gemaakte lijst van veelvoorkomende frequente woorden, bekend als stopwoorden. We importeren nu die lijst.
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
We kunnen nu toegang krijgen tot een vooraf gemaakte lijst van stopwoorden via stopwords.words('english')
. Laten we een snelle momentopname krijgen van de twee klassen, met en zonder stopwoorden. Eerst voor de positieve klasse:
Positieve Klasse met Stopwoorden
krijgMeestVoorkomendeWoorden(positive_samples['Reviews'], 10)
[('the', 989),
('and', 669),
('a', 466),
('i', 418),
('is', 417),
('this', 326),
('it', 311),
('of', <a href="/nl/ram-column-address-strobe-latency/30">30</a>8),
('to', 305),
('was', 257)]
Positieve Klasse zonder Stopwoorden
krijgMeestVoorkomendeWoorden(positive_samples['Reviews'], 10, stopwords.words('english'))
[('great', 198),
('good', 174),
('film', 98),
('phone', 86),
('movie', 83),
('one', 76),
('best', 63),
('well', 61),
('food', 60),
('place', 58)]
En nu de negatieve klasse:
Negatieve Klasse met Stopwoorden
krijgMeestVoorkomendeWoorden(negative_samples['Reviews'], 10)
[('the', 951),
('i', 469),
('and', 460),
('a', 420),
('to', 361),
('it', 354),
('is', 336),
('this', 313),
('of', 313),
('was', 312)]
Negatieve Klasse zonder Stopwoorden
krijgMeestVoorkomendeWoorden(negative_samples['Reviews'], 10, stopwords.words('english'))
[('bad', 96),
('movie', 94),
('phone', 76),
('dont', 70),
('like', 67),
('one', 67),
('food', 64),
('time', 61),
('would', 57),
('film', 57)]
We zien meteen een paar verschillen, zoals het veelvuldig gebruik van woorden als "good", "great" en "best" in de positieve klasse, en woorden als "dont" en "bad" in de negatieve klasse. Daarnaast, als je de waarde van de n_meest_voorkomende parameter verhoogt, kun je zien dat woorden als "not" (die de nltk corpus classificeert als een stopwoord) vijf keer zo vaak voorkomen in de negatieve klasse als in de positieve klasse.
Na een paar minuten bestudeerd te hebben welke trends er in de data aanwezig zijn, kunnen we verder met het bouwen van een model met een idee van waar we ons op moeten richten en wat niet. Een eenvoudige classificator die zich richt op woordkeuze lijkt veelbelovend.
Vectorisatie: Vertalen van Engels naar Computerspraat
We zijn al een heel eind op weg en bijna klaar om onze classifier te bouwen. Voordat we dat doen, moeten we onze tekstuele gegevens omzetten in een vorm die de computer kan begrijpen. Dit gebeurt meestal via een proces dat vectorization heet. Er zijn veel manieren om dit te doen (ik raad aan om te kijken naar word embedding als je tijd hebt, het is wat ingewikkelder maar echt gaaf). Wij gaan de bag-of-words (BOW) model gebruiken. Dit is simpel, maar toch krachtig en veelgebruikt in de industrie en academische wereld.
Bij een BOW neem je een verzameling "documenten" (je corpus, dat kunnen zinnen, alinea's of andere tekststukken zijn) en zet je ze om in een "zak" met frequentietellingen voor elk "woord" dat je tegenkomt. Het resultaat is een lijst van lijsten, vectors, die dan door een machine learning classifier kunnen worden gebruikt. Bijvoorbeeld, het volgende corpus:
['de kat is zwart',
'ik ben kat hou van zwarte kat',
'de emoe is zwart']
Kan omgezet worden naar de volgende BOW:
array([[0, 1, 1, 0, 1, 0, 1],
[1, 1, 2, 0, 0, 1, 0],
[0, 1, 0, 1, 1, 0, 1]], dtype=int64)
Elke kolom stelt de frequentie van een woord voor en elke rij de woorden in een document. Hier is een mapping om het duidelijker te maken:
{'ben': 0, 'zwart': 1, 'kat': 2, 'emoe': 3, 'is': 4, 'hou': 5, 'de': 6}
Er zijn een paar manieren om de BOW model te implementeren, waaronder:
Woord-Frequentie: Wat we eerder besproken hebben, het tellen van de frequentie van woorden.
One Hot Encoding: Een woord krijgt een '1' als het in het document voorkomt, ongeacht de frequentie, anders '0'.
N-gram: In plaats van afzonderlijke woorden, meet je de voorkomst/frequentie van groepjes woorden met lengte N. Dit helpt de context te begrijpen.
TF-IDF (Term Frequency - Inverse Document Frequency): Wanneer minder voorkomende woorden zwaarder wegen dan veel voorkomende woorden. Dit is erg simpel uitgelegd, maar het laat zien waarom dit handig is. Bij TF-IDF hebben alle termen waarden tussen 0 en 1.
Hier is dezelfde corpus, maar dan gevectoriseerd met TF-IDF:
array([[0. , 0.409, 0.527, 0. , 0.527, 0. , 0.527],
[0.463, 0.274, 0.704, 0. , 0. , 0.463, 0. ],
[0. , 0.373, 0. , 0.632, 0.480, 0. , 0.480]])
De N-gram methode is iets complexer dan de bedoeling hier, maar de TF-IDF methode presteert net iets beter dan woord-frequentie op deze dataset (ik heb ze al vergeleken). Hoewel vectorisatie-code schrijven van nul af aan uitdagend kan zijn, biedt sklearn ons handige methodes om het werk te doen:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
bow = vectorizer.fit_transform(df['Reviews'])
labels = df['Labels']
Laten we nu eens kijken hoeveel unieke woorden (features) we hebben:
len(vectorizer.get_feature_names())
5159
Nu hebben we een interessant machine learning probleem. We hebben ongeveer 3000 samples maar 5159 features. Als regel van duim moet je minstens tien keer zoveel samples hebben als features. Deze regel suggereert minstens 51.590 samples.
Een grotere dataset maken is vaak beter, maar ook moeilijk. Het verzamelen en labelen van data kost veel tijd en geld. Dus, in plaats van meer samples te maken, kunnen we de aantal features verminderen.
We kunnen beginnen met het verwijderen van woorden die zeldzaam voorkomen, bijvoorbeeld minder dan 0.5% van de tijd. Dit doen we met min_df parameter instellen op 15:
vectorizer = TfidfVectorizer(min_df=15)
bow = vectorizer.fit_transform(df['Reviews'])
len(vectorizer.get_feature_names())
309
Dat heeft goed gewerkt, want het aantal features is nu ~300. Laten we verdergaan en "ruis", zoals het woord "de", verwijderen. We gebruiken hiervoor een sklearn Chi-Squared test:
from sklearn.feature_selection import SelectKBest, chi2
selected_features = \
SelectKBest(chi2, k=200).fit(bow, labels).get_support(indices=True)
Met de get_support() methode krijgen we de indices van geselecteerde features. We kunnen dan een nieuwe vectorizer en BOW maken.
vectorizer = TfidfVectorizer(min_df=15, vocabulary=selected_features)
bow = vectorizer.fit_transform(df['Reviews'])
bow
3000x200 sparse matrix of type with 11889 stored elements in Compressed Sparse Row format
Verdeling van de Dataset: De Train- en Testsets
Nu we onze dataset hebben verkleind tot een beheersbare grootte, kunnen we beginnen met het trainen van een model. De eerste stap is om onze dataset op te splitsen in een trainingsset en een testset. We gebruiken de trainingsset om het model te bouwen en de testset om de prestaties ervan te evalueren.
Het is belangrijk dat je je model test op data die het nog nooit eerder heeft gezien. Als je het model op dezelfde data traint en test, test je eigenlijk alleen maar het geheugen van het model. Dat zegt niet veel over hoe goed het model zal presteren wanneer het echte data tegenkomt.
Voordat we het model in de echte wereld inzetten, combineren we de train en test sets weer en trainen we het model opnieuw op de volledige dataset.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(bow, labels, test_size=0.33)
De bovenstaande code neemt een willekeurige 2/3-deel van onze BOW en de parallelle lijst van labels, en wijst dat deel toe aan X_train en y_train. We gebruiken dit deel om ons model te trainen. We zetten het resterende 1/3-deel van de dataset voorlopig aan de kant.
De Classificator
from sklearn.ensemble import RandomForestClassifier as rfc
We hebben gekozen voor Random Forest als ons model algoritme. Random Forests zijn groepen van beslissingsbomen. Wanneer een voorbeeld door het random forest gaat, maakt elke beslissingsboom een voorspelling over tot welke klasse dat voorbeeld behoort (bijvoorbeeld, een negatieve of positieve recensie). Zodra alle voorspellingen gedaan zijn, wordt de klasse met de meeste voorspellingen (of stemmen) gekozen als de uiteindelijke voorspelling.
Individuele beslissingsbomen (vooral de niet-gesnoeide) zijn vaak niet erg sterk als het gaat om nieuwe data; ze zijn vatbaar voor overfitting. Een model dat overfitting vertoont, onthoudt te veel specifieke kenmerken uit de dataset en is minder goed in staat om zich aan te passen aan nieuwe data. Dit komt omdat echte data vaak "ruis" bevat - kleine trends kunnen willekeurig opduiken, maar ze zijn niet echt betekenisvol.
Stel, je wilt een model maken om te voorspellen of een student een vak met succes afrondt, en je hebt wat historische data over studentenprofielen en studieresultaten verzameld. Je bouwt je classificator en deze behaalt een nauwkeurigheid van 85% op de testset. Maar als je dit model toepast in de echte wereld, daalt de nauwkeurigheid naar 70%. Hoe dat komt? Stel dat per toeval de gemiddelde lengte van succesvolle studenten in je dataset 1.8 meter was en dat 3% van hen "Angela" heette. Het model let daar op en besluit dat een student succesvoller is als deze 1.8 meter lang is en Angela heet. Maar in de echte wereld zijn er minder 1.8 meter lange Angela's dan in je dataset, waardoor de nauwkeurigheid zakt.
Een goed model past zich aan aan de trainingsdata zonder te veel betekenisloze ruis op te pikken. Je wilt ook onderfitting vermijden, waarbij belangrijke trends over het hoofd worden gezien. Een model presteert bijna nooit net zo goed met echte data als met de testset, omdat het lastig is om precies de balans te vinden tussen overfitting en onderfitting.
classifier = rfc()
classifier.fit(X_train,y_train)
classifier.score(X_test,y_test)
0.7494949494949495
Gefeliciteerd als je alles hebt gevolgd. Je hebt nu een werkende sentiment analyzer voor productrecensies. We zijn echter nog niet helemaal klaar; er is meer werk nodig om die score nog wat te verbeteren.
Hyperparameteroptimalisatie: Maximale Prestatie behalen
Een hyperparameter is een instelling die je zelf bepaalt voor een model. Dit zijn als het ware de opties van een classifier. Ze zijn anders dan de gewone modelparameters (zoals de gewichten of belangrijkheid die aan bepaalde kenmerken worden gegeven), want die worden automatisch bepaald door het algoritme tijdens het trainen van het model. Kortom, parameters worden bepaald tijdens het trainen (door het model), terwijl hyperparameters worden bepaald vóór het trainen (door jou of een selectie-algoritme). Er zijn enkele uitzonderingen op deze regel, vooral in deep learning, maar voor nu is deze uitleg genoeg.
Ons eerste model gebruikte de standaard instellingen van sklearn. Mogelijk kunnen we betere resultaten behalen door andere hyperparameters te proberen. Dit proces noemen we hyperparameter selectie.
Hyperparameter selectie houdt in dat je meerdere modellen traint en test met verschillende hyperparameters en dan het model kiest dat de beste resultaten behaalt. Populaire methoden hiervoor zijn: grid search, ook wel brute-force search genoemd, wat verschillende combinaties van hyperparameters organiseert test (meestal langzamer, maar waarschijnlijk vind je een zeer optimaal model); random search, waarbij modellen met willekeurige hyperparameter combinaties worden getest; en genetische algoritme search, waarbij hyperparameters over meerdere generaties "evolueren" om steeds betere modellen te produceren. We zullen de random search methode gebruiken, de eenvoudigste, maar zeer effectieve, om 65 willekeurige modellen te genereren.
from sklearn.model_selection import RandomizedSearchCV
from scipy import stats
classifier = rfc()
hyperparameters = {
'n_estimators': stats.randint(10, 300),
'criterion': ['gini', 'entropy'],
'min_samples_split': stats.randint(2, 9),
'bootstrap': [True, False]
}
random_search = RandomizedSearchCV(classifier, hyperparameters, n_iter=65, n_jobs=4)
random_search.fit(bow, labels)
RandomizedSearchCV maakt keuzes binnen deze parameters. Deze instellingen bepalen ons nieuwe, geoptimaliseerde model. Standaard gebruikt RandomizedSearchCV een 3-voudige cross validatie, wat inhoudt dat elk model op drie verschillende train/test-sets wordt getraind en getest.
Sklearn's random search ondersteunt cross validatie. Cross validatie splitst het trainingsset verder in meerdere train/test sets. Elk kandidaat model wordt verschillende keren getraind en geëvalueerd. Het model met de hoogste gemiddelde score op de CV-sets wordt dan gekozen. Dankzij cross validatie kunnen we onze testsset reserveren voor een laatste check op het gekozen model.
Laten we de best presterende classifier ophalen uit onze random_search, en kijken hoe het presteert op onze testset.
optimized_classifier = random_search.best_estimator_
optimized_classifier.fit(X_train, y_train)
optimized_classifier.score(X_test, y_test)
"
0.7626262626262627
Door willekeurige hyperparameters voor 65 modellen te kiezen, hebben we onze score een paar punten weten op te voeren. Het verhogen van n_iter naar een waarde groter dan 65 kan mogelijk een nog beter model opleveren - mogelijk, want het blijft willekeurig. Let op dat je resultaten iets kunnen variëren door de willekeurigheid in het random forest, de random search en de automatische schuffling en splitsing van de dataset.
Resultatenanalyse
We zijn bijna aan het eind van onze lange reis. Voordat we afscheid nemen, laten we eens kijken waarom ons model presteert zoals het doet. We beginnen met een beetje plezier met onze nieuwe tool. We trainen de classifier opnieuw op de hele dataset en testen het met wat reviews die we zelf schrijven.
optimized_classifier.fit(bow,labels)
our_negative_sentence = vectorizer.transform(['Ik **haatte** dit product. Het is \
helemaal niet goed ontworpen en viel meteen uit elkaar. \
Ik zou niets van dit bedrijf **aanbevelen**.'])
our_positive_sentence = vectorizer.transform(['De film was **geweldig** - ik zat \
de hele tijd op het puntje van mijn stoel. De acteerprestaties waren **excellent**, en het \
decor - ongelooflijk. Bekijk deze film nu!'])
optimized_classifier.predict_proba(our_negative_sentence)
array([[0.84355159, 0.15644841]])
optimized_classifier.predict_proba(our_positive_sentence)
array([[0.11276455, 0.88723545]])
De uitkomsten hierboven zijn geformatteerd als [kans op negatief, kans op positief]. Het lijkt erop dat de classifier het beide keren goed heeft, met onze negatieve zin die een 84% kans op negatief krijgt, en onze positieve zin een 89% kans op positief. Laten we nu iets moeilijkers proberen.
our_slightly_negative_sentence = vectorizer.transform(["Het product was **oké**. \
Ik heb beter besteld in het verleden, en in het algemeen zou ik een andere \
productlijn aanbevelen als je nieuw bent. Het bedrijf is goed, en ze hebben \
wel **uitstekende** producten. Dit product is dat echter niet."])
our_slightly_positive_sentence = vectorizer.transform(["De achterkant van de telefoon \
viel eraf bij levering - een bewijs van de goedkope, plastic bouw. Na 6 maanden \
gebruik moet ik zeggen dat dit product **geweldig** is voor zijn **prijs**. Het is best goed, \
en het is moeilijk om iets vergelijkbaars te vinden voor deze lage kosten."])
optimized_classifier.predict_proba(our_slightly_negative_sentence)
array([[0.1031746, 0.8968254]])
optimized_classifier.predict_proba(our_slightly_positive_sentence)
array([[0.6274093, 0.3725907]])
Voor reviews die op de grens van positief en negatief zijn, heeft onze classifier het veel moeilijker. Laten we dieper in onze dataset kijken, naar verkeerd geclassificeerde voorbeelden, om te zien of we die conclusie kunnen bevestigen.
optimized_classifier.fit(X_train,y_train)
correctly_classified = {}
incorrectly_classified = {}
for index, row in enumerate(X_test):
probability = optimized_classifier.predict_proba(row)
# krijg de locatie van de review in de data.
review_loc = y_test.index[index]
if optimized_classifier.predict(row) == y_test.iloc[index]:
correctly_classified[df['Reviews'].loc[review_loc]] = probability
else:
incorrectly_classified[df['Reviews'].iloc[review_loc]] = probability
Verkeerd geclassificeerde Voorbeelden
for review, score in incorrectly_classified.items():
print('{}: {}'.format(review, score[0]))
print('-----')
Dat klopt....de red velvet cake.....ohhh dit spul is **zo goed**.:
[0.50008503 0.49991497]
-----
Weer geen verhaal.: [0.52423469 0.47576531]
-----
Doet het werk niet.: [0.6735395 0.3264605]
-----
Penne vodka **excellent**!: [0.84047619 0.15952381]
-----
De Han Nan Chicken was ook erg lekker.: [0.54190239 0.45809761]
-----
Ik vond het product **eenvoudig** op te zetten en te gebruiken.: [0.5163053 0.4836947]
-----
We hebben veel **complimenten** ontvangen.: [0.3891861 0.6108139]
-----
Ik vond dit product veel te **groot**.: [0.37018315 0.62981685]
-----
ik voelde me **gekwetst** en **respectloos** behandeld, hoe kun je zo over een ander mens praten?: [0.46852324 0.53147676]
...
Goed Geclassificeerde Voorbeelden
for review, score in correctly_classified.items():
print('{}: {}'.format(review, score[0]))
print('-----')
De laatste 3 keer dat ik hier lunchte was slecht.: [0.89693878 0.10306122]
-----
Onze ober was erg **attent**, **vriendelijk**, en **informatief**.: [0.18739607 0.81260393]
-----
De wisselwerking tussen Martin en Emilio bevat dezelfde **geweldige** chemie als in Wall Street met Martin en Charlie.: [0.20173847 0.79826153]
-----
Go-To Plaats voor Gyros.: [0.39796863 0.60203137]
-----
Alles was vers en **heerlijk**!: [0.04166667 0.95833333]
-----
Ik **hou** van deze kabel - het laat me elk mini-USB apparaat <a href="/nl/ssds-compatible-devices/pc">aansluiten op mijn PC</a>.: [0.08222789 0.91777211]
-----
Dit is simpelweg de BESTE bluetooth headset voor **geluidskwaliteit**!: [0.06885359 0.93114641]
...
Het lijkt erop dat bij de goed geclassificeerde voorbeelden veel sleutelwoorden aanwezig zijn (de 200 kenmerken die we voor onze vectorizer hebben geselecteerd, zoals "heerlijk" en "houden van"), in vergelijking met de verkeerd geclassificeerde voorbeelden.
Conclusie
Deze handleiding is een eerste stap in sentimentanalyse met Python en machine learning. De voorbeeldzinnen die we schreven en onze snelle controle van foutief tegenover correct geklasseerde voorbeelden benadrukken een belangrijk punt: onze classifier kijkt alleen naar woordfrequentie - het "weet" niets over context of semantiek van de woorden. Voor dat probleem zou een n-gram BOW-benadering nuttig kunnen zijn. Dat valt echter buiten de reikwijdte van dit artikel. We zouden ook de probabiliteitsdrempel kunnen aanpassen: op dit moment wordt alles wat als meer dan 50% waarschijnlijk positief wordt berekend, voorspeld als een positieve recensie. Als we die drempel bijvoorbeeld naar 60% veranderen, kan dat helpen.
Voor nu zijn we erin geslaagd om van een tekstbestand naar een classifier te gaan die, met een beetje werk, veel dingen voor je zou kunnen automatiseren (bijvoorbeeld je kerstinkopen op Amazon).
Delen