Sentiment Analysis

Bei der Sentiment Analysis geht es um die Analyse der Stimmung eines Textes. Ein Algorithmus könnte zum Beispiel zur Marktforschung die Stimmungslage zu einem bestimmten Produkt erfassen.

Bereits im Jahr 2020 hab ich einen Sentiment Analysis Algorithmus entwickelt. Damals ging es um die Erfassung der Stimmungslage von Zeitungsartikeln.

Hedonometer

Hedonometer ist ein Projekt, das bereits seit dem Jahr 2009 das Gemüt von Twitter-Posts analysiert.

Hedonometer Diagramm

Durchschnittliches Gemüt von Twitter 🐦 (hedonometer.org)

In meiner Implementation des Algorithmus, verwende ich den Datensatz von Hedonometer, der dort als csv Tabelle heruntergeladen werden kann.

Kleiner Auszug aus der Tabelle für Englische Wörter:

rankwordenglishscoredeviation
0laughterlaughter8.50.93
1happinesshappiness8.440.97
2lovelove8.421.11
10174epidemicepidemic1.720.99

Der score beschreibt das Gemüt von 1-10.
Die deviation beschreibt das Gewicht eines Wortes.

Um das Sentiment eines Textes zu bestimmen, können zunächst durch Tokenization alle Wörter des Textes extrahiert werden. Anschließend werden die Wörter in der Tabelle gesucht und mit dem score und der deviation (Gewicht) das durchschnittliche Gemüt des Textes bestimmt.

Mittlerweile gibt es bessere Algorithmen, um eine Sentiment Analysis durchzuführen. Mit Reinforcement Learning können einige Probleme vermieden werden, die mit diesem simplen Algorithmus auftreten.

Implementation des Algorithmus

Der Ganze Quellcode kann auf GitHub aufgefunden werden: GitHub

export async function getSentiment(text: string, options: GetSentimentOptions): Promise<Sentiment> {
  const wordScores = await getDataset();
  const datasetInfo = await getDatasetInfo();

  const words = (text.match(/\b\w+\b/g) || [])
    .map(w => w.toLocaleLowerCase());

  // This will find & map the word score for each word. Words with no matching word score will be filtered out!
  const mappedWords = words
    .map(w => wordScores.find(s => s.word === w))
    .filter(ws => ws);

  const missingWords = words.filter(w => !wordScores.some(ws => ws.word === w)); // Words that could not be mapped to a word score.

  // This will calculate the arithmetic mean of the word scores, while weighting the scores with the deviation.
  // Maybe the harmonic mean would be the correct forumula in this case, however my math skills are to bad to know for sure.
  // TODO: Maybe find out if the harmonic mean is better suited
  const sentiment = Myth.sumBy(mappedWords, ws => ws.score * ws.deviation)
    / Myth.sumBy(mappedWords, ws => ws.deviation);

  const neutralScoreRadius = (datasetInfo.scoreMax - datasetInfo.scoreMin) * options.neutralSentimentRange * 0.5;
  const neutralScore = options.neutralSentimentAnker === 'mean'
    ? datasetInfo.scoreMean
    : datasetInfo.scoreMiddle;

  return {
    lostWordCount: missingWords.length,
    lostWords: [...new Set(missingWords)],
    datasetInfo,
    sentiment,
    sentimentTxt: (sentiment <= neutralScore - neutralScoreRadius)
      ? 'negative'
      : (sentiment >= neutralScore + neutralScoreRadius) ? 'positive' : 'neutral'
  };
}

Evaluation

Warum ich dieses alte Thema nochmal aufgegriffen habe, hat aber eigentlich einen anderen Grund. In meinem KI Studiengang ging es im letzten Kapitel um die Evaluation von KI-Systemen. So kam mir die Idee, zur Übung die Theorie in der Praxis anzuwenden.

Zur Evaluation können die Ergebnisse des KI-Algorithmus in eine Konfusionsmatrix eingetragen werden und anschließend Metriken berechnet werden.

Konfusionsmatrix

Die Konfusionsmatrix stellt die erwarteten Ergebnisse, den tatsächlichen Ergebnissen gegenüber. Im Fall der Sentiment Analysis gibt es drei mögliche Klassen, in die ein Text eingeordnet werden kann. (positiv, neutral & negativ)

Hier die Konfusionsmatrix meines (unoptimierten) Sentiment Analysis Algorithmus:

 -----------------+----------+---------+----------
|                 | positive | neutral | negative |
 -----------------+----------+---------+----------
| konrad positive | 774      | 431     | 87       |
 -----------------+----------+---------+----------
| konrad neutral  | 319      | 944     | 703      |
 -----------------+----------+---------+----------
| konrad negative | 10       | 55      | 211      |
 -----------------+----------+---------+----------

konrad ist der Name des Algorithmus.

In diesem Test, hab ich den Algorithmus mit gelabelten Daten von kaggle getestet. Gelabelt heißt, dass die Daten von einem Menschen manuell analysiert und bewertet wurden.

Metriken

Mit unserer Konfusionsmatrix können wir nun einige Metriken berechnen.

Accuracy

Die Accuracy beschreibt das Verhältnis von richtigen zu falschen Ergebnissen. Zur Berechnung werden alle richtigen Ergebnisse addiert und durch die Gesamtzahl der Ergebnisse geteilt.

Die richtigen Ergebnisse sind in der Tabelle markiert:

positiveneutralnegative
konrad positive77443187
konrad neutral319944703
konrad negative1055211
Accuracy = TPOS + TNEU + TNEG TPOS + FPOS + TNEU + FNEU + TNEG + FNEG = 0.55

TPOS = True Positive
FPOS = False Positive

TNEU = True Neutral
FNEU = False Neutral

TNEG = True Negative
FNEG = False Negative

Precision

Die Precision gibt an, wieviel Prozent der Ergebnisse korrekt einer Klasse zugeordnet wurden. Die Metrik kann also für alle drei Klassen berechnet werden.

In diesem Beispiel wird die Precision für die Klasse positive berechnet.

Precision (positive) = TPOS TPOS + FPOS = 774 774 + (431 + 87) = 0.599

In der Tabelle ist der TPOS Wert farblich markiert und die FPOS Werte fett gedruckt:

positiveneutralnegative
konrad positive77443187
konrad neutral319944703
konrad negative1055211

Recall

Der Recall gibt an, wieviel Prozent der Daten einer Klasse tatsächlich vom Algorithmus dieser Klasse zugeordnet wurden. Auch hier kann die Metrik für die verschiedenen Klassen seperat berechnet werden.

In diesem Beispiel wird der Recall für die Klasse positive berechnet.

Recall (positive) = TPOS TPOS + XPOS = 774 774 + (319 + 10) = 0.701

XPOS = Missing Positives. Bei binären Klassifizierungsaufgaben (Zwei statt drei Klassen), würde man hier stattdessen den FNEG Wert nehmen. Da dieser Wert bei drei Klassen allerdings auch die Werte einschließt, die eigentlich in der Klasse neutral landen sollten, musste ich hier einen neuen Begriff erfinden.

In der Tabelle ist der TPOS Wert farblich markiert und die XPOS Werte fett gedruckt:

positiveneutralnegative
konrad positive77443187
konrad neutral319944703
konrad negative1055211

F-Score

Der F-Score kombiniert die Precision und den Recall, in dem der harmonische Mittelwert gebildet wird. Diese Metrik kann auch wieder für jede Klasse seperat ermittelt werden.

Hier wird der F-Score für die Klasse positive gebildet.

F-Score (positive) = 2 x Precision x Recall Precision + Recall = 2 x 0.599 x 0.701 0.599 + 0.701 = 0.645

Alle Metriken

 -----------------+----------+---------+----------
|                 | positive | neutral | negative |
 -----------------+----------+---------+----------
| konrad positive | 774      | 431     | 87       |
 -----------------+----------+---------+----------
| konrad neutral  | 319      | 944     | 703      |
 -----------------+----------+---------+----------
| konrad negative | 10       | 55      | 211      |
 -----------------+----------+---------+----------

Accuracy: 0.55

Precision (positive): 0.60
Precision (neutral): 0.48
Precision (negative): 0.76
Precision: 0.59

Recall (positive): 0.70
Recall (neutral): 0.66
Recall (negative): 0.21
Recall: 0.39

F-Score (positive): 0.65
F-Score (neutral): 0.56
F-Score (negative): 0.33
F-Score: 0.47