<jupyter_start><jupyter_text>0. Installieren aller Pakete<jupyter_code># Hier die Kaggle Credentials einfügen (ohne Anführungszeichen)

%env KAGGLE_USERNAME=openhpi
%env KAGGLE_KEY=das_ist_der_key
!pip install skorch
import pandas as pd
import numpy as np

from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from skorch import NeuralNetClassifier

from sklearn.model_selection import GridSearchCV

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from collections import Counter
import re
from bs4 import BeautifulSoup
import yaml
import os

from wordcloud import WordCloud
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
import spacy
!python -m spacy download en_core_web_sm

import torch
from torch import nn

class NeuralNetModule(nn.Module):
    def __init__(self, num_inputs, num_units=20, nonlin=nn.ReLU()):
        super(NeuralNetModule, self).__init__()

        self.nonlin = nonlin
        self.dense0 = nn.Linear(num_inputs, num_units)
        self.dropout = nn.Dropout(0.2)
        self.dense1 = nn.Linear(num_units, num_units)
        self.output = nn.Linear(num_units, 2)
        self.softmax = nn.Softmax(dim=-1)

    def forward(self, X, **kwargs):
        X = self.nonlin(self.dense0(X))
        X = self.dropout(X)
        X = self.nonlin(self.dense1(X))
        X = self.softmax(self.output(X))
        return X<jupyter_output><empty_output><jupyter_text>3.5 Sentiment Analyse  Was wir erreichen wollenIn diesem Anwendungsfall wollen wir Filmbewertungen in positive und negative Bewertungen unterteilen. Dafür liegen uns eine Vielzahl gelabelter Trainingsdaten in englischer Sprache vor.Mit einem Modell wollen wir in der Lage sein, für neue Kommentare automatisiert die Stimmung analysieren zu können.   Download Dataset   Manuellvia https://www.kaggle.com/columbine/imdb-dataset-sentiment-analysis-in-csv-format  Via APIHinzufügen der kaggle.jsonSpeichern als ~/.kaggle/kaggle.json auf Linux, OSX, oder andere UNIX-based Betriebssysteme und unter C:\Users.kaggle\kaggle.json auf WindowsSiehe https://www.kaggle.com/docs/api oder https://github.com/Kaggle/kaggle-api        Beispiel:~/kaggle/kaggle.json{"username":"openHPI","key":"das_ist_der_key"}<jupyter_code>!pip3 install kaggle
!kaggle datasets download -d columbine/imdb-dataset-sentiment-analysis-in-csv-format
import zipfile
with zipfile.ZipFile("imdb-dataset-sentiment-analysis-in-csv-format.zip", 'r') as zip_ref:
    zip_ref.extractall("")

import os  
os.rename('Train.csv','sentiment.csv') 
FILE_PATH = "sentiment.csv"<jupyter_output><empty_output><jupyter_text>Daten vorbereitenWir können nun unsere Daten laden und mit der Vorbereitung beginnen.<jupyter_code>df = pd.read_csv(FILE_PATH, nrows=10_000)
df.head() # label 1 == positive; label 0 == negative<jupyter_output><empty_output><jupyter_text>Schauen wir uns zunächst einmal die Datenverteilung unserer Zielvariable an. Die Zielvariable ist kategorisch in diesem Fall:<jupyter_code>df["label"].value_counts(normalize=True)<jupyter_output><empty_output><jupyter_text>Wir sehen: unsere Daten sind nahezu perfekt gleichverteilt (50% positiv, 50% negativ). Das ist prinzipiell eine gute Nachricht für unser Trainingsvorhaben (je unbalancierter Daten sind, desto komplexer wird das Training i.d.R.).Werfen wir einen Blick auf das erste Beispiel, um ein Gefühl für die Datenbeschaffenheit zu erlangen:<jupyter_code>df["text"].iloc[0]<jupyter_output><empty_output><jupyter_text>Wir stellen fest:- Eine einzelne Bewertung kann recht lange sein. Das bedeutet für uns, dass Beispiele sowohl mehr Informationen, aber auch mehr "Rauschen" (für uns unwichtige Informationen) beinhalten können.- Es handelt sich um Webdaten, da HTML-Tags gesetzt sind. Wir sollten diese korrekt verarbeiten.- 's werden mit \\'s kodiert (was wiederum auf den Rohtext zurückzuführen ist)- Wir sollten die Wörter im Text zu Kleinbuchstaben konvertieren, um die Anzahl möglicher Wort-Variationen zu reduzieren.<jupyter_code># preprocessing
df["text"] = df["text"].apply(lambda x: x.lower())
df["text"] = df["text"].apply(lambda x: x.replace("\'", ""))
df["text"] = df["text"].apply(lambda x: BeautifulSoup(x).text)<jupyter_output><empty_output><jupyter_text>Schauen wir uns die gleiche Bewertung erneut an:<jupyter_code>df["text"].iloc[0]<jupyter_output><empty_output><jupyter_text>Das sieht schon deutlich besser aus. Fangen wir nun an, unsere Daten zu splitten. Wir nutzen hier der Einfachheit halber einen ganz simplen Split mit Standardeinstellungen (wir könnten auch andere Verfahren einsetzen).Wir teilen unsere Daten dabei in Trainings-, Validierungs- und Testsplit. Mit dem ersten trainieren wir die Modelle, mit dem zweiten suchen wir unser bestes Modell heraus, mit dem letzten validieren wir die Ergebnisse.<jupyter_code>train_df, test_valid_df = train_test_split(df)
test_df, valid_df = train_test_split(test_valid_df)<jupyter_output><empty_output><jupyter_text>Bevor wir unsere KI-Pipeline bauen, schauen wir uns die Daten erneut an. Sicherheitshalber duplizieren wir unseren Trainingsdatensatz dafür. Wir schauen uns zunächst die Textlängen an:<jupyter_code>analysis_df = train_df.copy()
analysis_df["text_length"] = analysis_df["text"].apply(len)<jupyter_output><empty_output><jupyter_text>Nun können wir uns die Verteilungen einfach je Klasse ("positiv" und "negativ") visualisieren lassen:<jupyter_code>sns.histplot(data=analysis_df, x="text_length", hue="label", element="step");<jupyter_output><empty_output><jupyter_text>Wir erkennen keinen großen Unterschied zwischen den beiden Klassen in der Textlänge (d.h. sowohl positive als auch negative Bewertungen können sehr ausführlich sein).Schauen wir uns den Textkorpus nochmal genauer an:<jupyter_code>text_corpus = " ".join(analysis_df["text"])<jupyter_output><empty_output><jupyter_text>Bauen wir uns nun eine Hilfsfunktion, mit der wir uns einfach die *n* häufigsten Wörter des Korpus anschauen können. Als Korpus bezeichnet man eine Sammlung von Texten.<jupyter_code>def plot_most_common_words(text_corpus, n):
    counter = Counter(text_corpus.split())
    rank, words, occurences = [], [], []
    for idx, (word, occurence) in enumerate(counter.most_common(n=n)):
        rank.append(idx)
        words.append(word)
        occurences.append(occurence)
        
    fig, ax = plt.subplots()
    ax.scatter(rank, occurences, s=0.01)
    for idx, word in enumerate(words):
        ax.annotate(word, (rank[idx], occurences[idx]))
    plt.title("Zipf's law")
    plt.ylabel("Occurences")
    plt.xlabel("Rank");<jupyter_output><empty_output><jupyter_text>Wir können nun in der Verteilung das sogenannte Zipf's Law erkennen: Jedes Wort tritt ungefähr invers proportional zu seinem Rang auf, d.h. das häufigste Wort doppelt so häufig wie das zweithäufigste. Solche Füllwörter machen einen Großteil unserer Daten aus. Das ist ein ganz bekanntes Phönomen in natürlichen Sprachen.<jupyter_code>plot_most_common_words(text_corpus, 15)<jupyter_output><empty_output><jupyter_text>Wir können diese Wörter entfernen, da sie inhaltlich keinen Mehrwert bringen. Als ersten Schritt erstellen wir ein Embedding für einzelne Wörter. Schauen wir uns die Verteilung einmal an, wenn wir die typischen Füllwörter entfernen:<jupyter_code>pattern = re.compile(r'\b(' + r'|'.join(stopwords.words('english')) + r')\b\s*')
text_corpus_without_stopwords = pattern.sub('', text_corpus)<jupyter_output><empty_output><jupyter_text>Wir erzeugen die gleiche Visualisierung:<jupyter_code>plot_most_common_words(text_corpus_without_stopwords, 15)<jupyter_output><empty_output><jupyter_text>Wir können schon deutlich erkennen, dass es sich um einen Datenbestand zu Filmbewertungen handelt. Wir können uns das auch als Wordcloud ("Wortwolke") anschauen. Wir schreiben uns hierfür eine Funktion:<jupyter_code>def draw_word_cloud(text_corpus):
    word_cloud = WordCloud(
        collocations = False, background_color = 'white'
    ).generate(text_corpus)
    plt.imshow(word_cloud, interpolation='bilinear')
    plt.axis("off");<jupyter_output><empty_output><jupyter_text>Damit können wir die Worthäufigkeiten als Wordcloud darstellen lassen:<jupyter_code>draw_word_cloud(text_corpus_without_stopwords)<jupyter_output><empty_output><jupyter_text>*Hinweis*:An sich liefert eine Wordcloud vom Informationsgehalt keinen Mehrwert gegenüber einer einfachen Darstellung anhand eines Scatter Plots. Tatsächlich ist es für uns sogar schwieriger zuzuordnen, welches Wort am häufigsten vorkommt (`movie` oder `film`?). Außerdem müssen wir häufig unsere Leserichtung anpassen. Vorteilhaft ist lediglich die platzsparende Art der Darstellung.Word Clouds werden gerne bei Textvisualisierungen verwendet - durchaus auch, weil es schlichtweg "gut" aussieht.  Wir können uns diese Darstellung auch einmal je Klasse ("positiv" und "negativ") anzeigen; vielleicht erkennen wir klassenspezifische Wörter:<jupyter_code>positive_corpus = " ".join(analysis_df["text"].loc[analysis_df["label"] == 1])
negative_corpus = " ".join(analysis_df["text"].loc[analysis_df["label"] == 0])<jupyter_output><empty_output><jupyter_text>Schauen wir uns zunächst die positiven Filmbewertungen in einer Wordcloud an:<jupyter_code>draw_word_cloud(positive_corpus)<jupyter_output><empty_output><jupyter_text>Und anschließend die negativen Bewertungen:<jupyter_code>draw_word_cloud(negative_corpus)<jupyter_output><empty_output><jupyter_text>Wir stellen fest, dass es Wörter gibt, die wir intuitiv vielleicht einer Klasse zugeordnet hätten (`good` -> positiv), aber auch in der gegenteiligen Klasse auftreten. Entstehen können solche Szenarien dann, wenn Wörter im Kontext des Satzes ihre Bedeutung ändern: `This film was really good.` vs. `In my opinion, this movie was not good at all.`Wir könnten hier noch tiefer in eine Analyse gehen und versuchen, mehr über unsere Daten zu verstehen (z.B. wie oft treten verneinte Sätze auf? Was sind klassentrennde Wörter?). An dieser Stelle werden wir aber für dieses Projekt die explorative Analyse beenden und mit der KI-Pipeline beginnen.  3.5 Sentiment Analyse  LemmatisierungWir können unsere Texte nun so anpassen, dass wir Wörter auf ihren Wortstamm reduzieren. Dafür eignet sich die Lemmatisierung.<jupyter_code>nlp = spacy.load("en_core_web_sm")
doc = nlp(
    "lemmatization is the process of grouping together the inflected forms of a word."
)
for token in doc:
    print(token, token.lemma_)<jupyter_output><empty_output><jupyter_text>Wir bauen uns hier wieder eine Funktion für:<jupyter_code>def enrich_lemmatized_text(df):
    df = df.copy()
    df["lemmatized_text"] = df["text"].apply(
        lambda x: " ".join([token.lemma_ for token in nlp(x)])
    )
    return df<jupyter_output><empty_output><jupyter_text>Und wenden diese anschließend auf unseren DataFrames an:<jupyter_code>train_df = enrich_lemmatized_text(train_df)
valid_df = enrich_lemmatized_text(valid_df)
test_df = enrich_lemmatized_text(test_df)<jupyter_output><empty_output><jupyter_text>Nun können wir mit der Tf-Idf Vektorisierung beginnen, welche unsere Texte in ein numerisches Format umwandelt.<jupyter_code>vectorizer = TfidfVectorizer()

train_X = vectorizer.fit_transform(train_df["lemmatized_text"]).astype(np.float32)
valid_X = vectorizer.transform(valid_df["lemmatized_text"]).astype(np.float32)
test_X = vectorizer.transform(test_df["lemmatized_text"]).astype(np.float32)<jupyter_output><empty_output><jupyter_text>Unsere Daten sind vorbereitet, wir können mit dem Bau unserer KI-Modelle beginnen. Wir trainieren:- einen Entscheidungsbaum- einen Random Forest- eine logistische Regression- ein künstliches neuronales Netzwerk  Decision Tree (Entscheidungsbaum)Ein Entscheidungsbaum ist als sehr einfaches Modell und wird oft als eines der ersten Modelle verwendet. Wir trainieren und geben verschiedene Optionen für die Hyperparameter `max_depth` und `min_samples_split`. Die Hyperparameter-Suche GridSearch wählt hierbei die optimalen Hyperparameter aus.<jupyter_code>tree_clf = DecisionTreeClassifier()
tree_params = {
    'max_depth': list(range(10, 101, 20)) + [None],
    'min_samples_split': [2, 5]
}
tree_search = GridSearchCV(tree_clf, tree_params)
tree_search.fit(train_X, train_df["label"])
best_tree_clf = tree_search.best_estimator_<jupyter_output><empty_output><jupyter_text>Anschließend wollen wir unser Modell auf den Validierungsdaten prüfen. Da wir das wiederholt machen werden, bauen wir wieder eine Hilfsfunktion:<jupyter_code>def evaluate_clf(valid_X, labels, clf):
    predictions = clf.predict(valid_X)
    report = classification_report(labels, predictions)
    print(report)<jupyter_output><empty_output><jupyter_text>Anschließend führen wir diese aus und können die Ergebnisse prüfen:<jupyter_code>evaluate_clf(valid_X, valid_df["label"], best_tree_clf)<jupyter_output><empty_output><jupyter_text>Random ForestAls nächstes Modell eignet sich ein Random Forest, welcher eine Ensemble-Technik darstellt. Im Wesentlichen werden hier mehrere Entscheidungsbäume trainiert, und deren Ergebnis vereint. Wir haben zusätzlich zu den vorherigen Hyperparametern noch `n_estimators` zu wählen.<jupyter_code>forest_clf = RandomForestClassifier()
forest_params = {
    'n_estimators': list(range(10, 101, 20)),
    'max_depth': list(range(10, 101, 20)) + [None],
    'min_samples_split': [2, 5]
}
forest_search = GridSearchCV(forest_clf, forest_params)
forest_search.fit(train_X, train_df["label"])
best_forest_clf = forest_search.best_estimator_<jupyter_output><empty_output><jupyter_text>Wir evaluieren wieder das Ergebnis:<jupyter_code>evaluate_clf(valid_X, valid_df["label"], best_forest_clf)<jupyter_output><empty_output><jupyter_text>Logistic RegressionNun bauen wir eine logistische Regression. Diese ist ebenfalls sehr simpel, erweist sich oftmals aber als sehr gutes Modell. Wir haben vier Hyperparameter:<jupyter_code>lr_clf = LogisticRegression()
lr_params = {
    'penalty': ['l1', 'l2'],
    'max_iter': [100],
    'C': np.logspace(-4, 4, 20),
    'solver': ['liblinear']
}
lr_search = GridSearchCV(lr_clf, lr_params)
lr_search.fit(train_X, train_df["label"])
best_lr_clf = lr_search.best_estimator_<jupyter_output><empty_output><jupyter_text>Und wir evaluieren erneut:<jupyter_code>evaluate_clf(valid_X, valid_df["label"], best_lr_clf)<jupyter_output><empty_output><jupyter_text>Feedforward Neural NetworkZuletzt wollen wir noch ein künstliches neuronales Netz bauen. Dafür haben wir eine Architektur in der Datei `ffnn.py` gewählt, welche wir zuvor importiert haben und nun mit der Bibliothek skorch einfach anwenden können. Wir wählen hier direkt die Hyperparameter ohne GridSearch:<jupyter_code>neural_net = NeuralNetClassifier(
    module=NeuralNetModule,
    module__num_inputs = len(vectorizer.vocabulary_),
    max_epochs=10,
    optimizer=torch.optim.Adam,
    iterator_train__shuffle=True,
    verbose=0
)
neural_net.fit(train_X, train_df['label'])<jupyter_output><empty_output><jupyter_text>Nun prüfen wir das Ergebnis:<jupyter_code>evaluate_clf(valid_X, valid_df["label"], neural_net)<jupyter_output><empty_output><jupyter_text>Unser bestes Modell auf den Validierungsdaten ist die logistische Regression, daher wählen wir diese als unser finales Modell.  Test unseres ModellsWir haben die Validierungsdaten gewählt, um unser bestes Modell auszuwählen. Es kann aber sein, dass unser Modell auf den Validierungsdaten nur zufällig gute Prognosen erzeugt hatte oder wir uns zu sehr durch die Hyperparameter-Optimierung auf unser Validierungs-Daten "überangepasst" haben. Daher evaluieren wir auf einem noch vollkommen "neuen" Teil der Daten, unseren Testdaten:<jupyter_code>evaluate_clf(test_X, test_df["label"], best_lr_clf)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08692929166405712, 'fraction_numerical': 0.005135592158827582, 'mean_word_length': 6.621957040572792, 'pattern_counts': {'":': 2, '<': 137, '<?xml version=': 0, '>': 138, 'https://': 3, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '34', 'dir': 'jupyter-structured-clean-dedup', 'id': '475014', 'license': 'MIT', 'n_tokens_mistral': 5600, 'n_tokens_neox': 5556, 'n_words': 1404, 'path': 'Woche 3/3_5_Sentiment_Analyse.ipynb', 'repo_name': 'sting-aree/openhpi-kipraxis', 'size': '15967'}