E-Mail-Klassifizierung mit SpaCy

Keine Kommentare

Noch vor kurzer Zeit war E-Mail-Klassifikation mittels Deep Learning nur mit Spezialwissen und ausreichend Data Science Know-how möglich. Heute existieren sehr gute Open-Source-Bibliotheken mit fertigen Deep-Learning-Modellen, welche sehr weit optimiert sind und aktuellen Best Practices folgen. Mithilfe solcher Modelle kann man sofort und ohne viel Data Science Know-how Deep-Learning zur E-Mail-Klassifikation einsetzen.

Im Folgenden trainieren wir ein Deep-Learning-Modell der Open-Source-Bibliothek spaCy, um E-Mails zu klassifizieren. Das Beispiel ist so gedacht, dass es in einem Jupyter Notebook Schritt für Schritt nachvollzogen werden kann.

Warum es für Unternehmen hilfreich sein kann, sich mich E-Mail-Klassifikation zu beschäftigen, haben wir bereits im Blogpost Kunden-E-Mails effizient verarbeiten – mit Künstlicher Intelligenz beschrieben.

Wie gehen wir vor?

Unser Beispiel besteht aus vier Schritten:

Daten beschaffen

In der Praxis ist es häufig aufwändig einen Datensatz zu erstellen. Möchte man ein Modell zur Klassifikation von E-Mails aus einem spezifischen Unternehmenskontext trainieren, führt daran aber kein Weg vorbei. Neben dem Zusammenstellen der E-Mails gehört hierzu auch die Auswahl geeigneter Kategorien. Um brauchbare Ergebnisse erhalten zu können, benötigt man ausreichend (= mehrere Hundert) E-Mails pro Kategorie.

Daten vorbereiten

Typischerweise liegen die Daten nicht direkt in einem geeigneten Format vor. Daher ist es normalerweise notwendig, die Daten zunächst geeignet vorzubereiten. Ziel ist ein Datenformat, das einerseits statistische Analysen des Datensatzes erlaubt und mit dem andererseits leicht ein Modell trainiert werden kann.

Modell trainieren

Wir verwenden das in spaCy integrierte Deep-Learning Modell textcat. spaCy ist eine mächtige Open-Source Bibliothek für Python, die dafür gebaut wurde, Anwendungen im NLP-Bereich zu entwickeln. Um direkt loslegen zu können, beinhaltet spaCy bereits vortrainierte Sprachmodelle. Unsere Erfahrung zeigt, dass viele Use Cases bereits mit diesen Modellen umgesetzt werden können und es sich meistens nicht lohnt, ein eigenes Modell von Grund auf neu zu entwickeln.

Modell evaluieren

Abschließend überprüfen wir, wie gut das trainierte Modell neue E-Mails klassifiziert.

Fangen wir also an: Zunächst brauchen wir einen geeigneten Datensatz an E-Mails.

Daten beschaffen

Für unser Beispiel verwenden wir das öffentlich verfügbare omqdata Datenset, welches hier heruntergeladen werden kann. Der Datensatz besteht aus E-Mails, die von Kunden an den Support eines Multimedia-Software-Unternehmens geschickt wurden.

Die E-Mails sind bereits anhand von Fehlermeldungen wie "Fehlercode -9 beim Start des Programmes" oder "Altes Programm soll unter Windows 7 installiert werden" in Kategorien eingeteilt. Verwandte Fehlermeldungen sind in Kategoriegruppen zusammengefasst. So sind zum Beispiel in Kategoriegruppe 6 die Fehlermeldungen "Fehlercode -9 beim Start des Programmes", "Fehler 9000 bei der Programminstallation" und "Programm startet nicht" zu finden.

Im Folgenden gehe ich davon aus, dass der Datensatz heruntergeladen wurde und in einem Ordner entpackt vorliegt. Das XML-File omq_public_interactions.xml unseres Datensatzes enthält die E-Mails. Diese sind anhand von 41 unterschiedlichen Fehlermeldungen eingeordnet. Im XML-File omq_public_categories.xml befindet sich die Zuordnung der Fehlermeldungen in 20 Kategoriegruppen.

xml-Files sind für die weiteren Schritte unhandlich. Wir bereiten daher im nächsten Schritt unsere Daten zur Weiterverarbeitung vor.

Daten vorbereiten

Um die Daten vorzubereiten, verwenden wir die Python-Module

import os
import pandas as pd
import numpy as np
import random
from bs4 import BeautifulSoup

Außerdem bezeichnen wir mit PATH_TO_DATA den Pfad zum Ordner mit unserem Beispieldatensatz. Die E-Mails und Kategorien können dann mithilfe des Code-Blocks

def get_nice_text(item):
    text = item.getText()
    text = text.replace('\r',' ')
    text = text.replace('\n','')
    return text.strip()
 
with open(os.path.join(PATH_TO_DATA,'omq_public_interactions.xml'), 'rb') as f:
    soup = BeautifulSoup(f.read(), 'html.parser')
 
mails = soup.find_all("text")
categories = soup.find_all("category")
 
rows = []
for item, cat in zip(mails, categories):
    text = get_nice_text(item)
    categories = cat.getText().split(',')
    if len(categories) > 0:
        category = random.choice(categories)
    else:
        category = np.nan
    rows = rows + [[text, categories, category]]
 
df = pd.DataFrame(rows, columns=['text', 'categories', 'category'])

geparsed und in einem Pandas Dataframe gespeichert werden.

Im Datensatz befinden sich neun E-Mails, die mehr als einer Kategorie zugeordnet sind. Für unser Beispiel haben wir für die neun E-Mails einfach zufällig eine der Kategorien ausgewählt. Dadurch liegt nun ein Datensatz vor, in dem jede E-Mail genau einer Kategorie zugeordnet ist.

In der Praxis kann Multiklassifikation sinnvoll sein. Kunden können beispielsweise mehr als ein Anliegen in einer E-Mail beschreiben. Grundsätzlich kann das spaCy Modell mit Multiklassifikation umgehen, dies würde aber den Rahmen unseres Beispiels sprengen.

Es ist oft hilfreich, die Daten als Pandas DataFrame vorliegen zu haben, um das Datenset zu explorieren und statistische Analysen zu machen. So kann man zum Beispiel über

df.categories.value_counts()

sehen, wie viele E-Mails jeder Kategorie im Datensatz enthalten sind.

Grundsätzlich ist es sinnvoll, die Daten zu untersuchen, bevor man ein Modell damit trainiert. Die Analyse des Datensatzes ist aber nicht Gegenstand dieses Blogposts. Wer mehr über Datenanalyse und spaCy wissen möchte, kann zum Beispiel hier fündig werden.

Mit dem nachfolgenden Code-Block schreiben wir die Zuordnung der Kategorien in Kategoriegruppen in ein weiteres Pandas DataFrame.

with open(os.path.join(PATH_TO_DATA, 'omq_public_categories.xml'), 'rb') as f:
    soup = BeautifulSoup(f, 'html.parser')
 
categoryGroups = soup.find_all('categorygroup')
 
rows = []
for group in categoryGroups:
    group_id = group['id']    
    categories = group.find_all('category')
    cat_ids = []
    for cat in categories:
        cat_id = cat['id']
        cat_ids.append(cat_id)
        cat_text = get_nice_text(cat)
    rows = rows + [[group_id, cat_ids, cat_text]]
 
df_cat = pd.DataFrame(rows, columns=['group_id', 'cat_ids', 'cat_text'])

Im letzten Schritt ordnen wir nun allen E-Mails die zugehörige Kategoriegruppe zu.

def get_encode_dic():
    encode_dic = {}
    for index, row in df_cat.iterrows():
        for cat_id in row.cat_ids:
            encode_dic[cat_id] = row.group_id
    return encode_dic
 
encode_dic = get_encode_dic()
df['category_group'] = df.category.map(encode_dic)

Es ist eine gute Praxis, keine „Jupyter Notebook Monolithen“ zu bauen und das erste Notebook hier zu beenden. Die Ergebnisse der Datenvorbereitung können über den Befehl

df.to_pickle(os.path.join(PATH_TO_DATA, 'processed_mails.pkl'))

gespeichert werden. Wir verwenden nun die processed_mails im nächsten Notebook, um ein Modell zu trainieren. Das Modell soll E-Mails der korrekten Kategoriegruppe zuordnen.

Modell trainieren

Wir verwenden die Python-Module

import os
import spacy
import pandas as pd
 
from spacy import displacy
from spacy.util import minibatch, compounding, decaying
 
from sklearn.model_selection import StratifiedShuffleSplit

Zunächst laden wir mit dem Befehl

df = pd.read_pickle(os.path.join(PATH_TO_DATA, 'processed_mails.pkl'))

die gerade vorbereiteten E-Mail-Daten in unser Notebook. Bevor wir mit dem Training des Modells beginnen, teilen wir den Datensatz via

X_all = df.text
y_all = df.category_group
 
seed = 42
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.15, random_state=seed)
for train_index, test_index in sss.split(X_all, y_all):
    X_train, X_test = X_all.iloc[train_index], X_all.iloc[test_index]
    y_train, y_test = y_all.iloc[train_index], y_all.iloc[test_index]

in Trainings- und Testdaten auf. So haben wir später noch E-Mails übrig, mit denen wir unser trainiertes Modell evaluieren können.

spaCy erwartet die eingehenden Trainingsdaten in einem bestimmten Format, in welches wir unsere Trainingsdaten via

cats = df.category_group.unique()
 
def get_catlist(y):
    cat_list = []
    for _, val in y.iteritems():
        categories = {'cats': {cat: 0.0 for cat in cats}}
        if val in cats:
            categories['cats'][val] = 1.0
        cat_list.append(categories)
    return cat_list
 
data_train = list(zip(X_train.values, get_catlist(y_train)))

überführen können.

spaCy verfügt über ein vortrainiertes deutsches Sprachmodell, das wir im Folgenden verwenden möchten. Dieses muss zum Beispiel via

python -m spacy download de_core_news_sm

über die Konsole heruntergeladen werden. Danach laden wir mit

nlp = spacy.load('de_core_news_sm')

das deutsche Sprachmodell in unser Notebook und aktivieren über

if 'textcat' not in nlp.pipe_names:
    textcat = nlp.create_pipe('textcat')
    nlp.add_pipe(textcat, last=True)

das spaCy-Modell textcat zur Textklassifizierung. Als nächstes fügen wir über

for cat in cats:
    textcat.add_label(cat)

die Kategorien unserer E-Mails hinzu. Jetzt können wir mit dem Training beginnen.

Da wir lediglich die Textklassifizierung trainieren und das Sprachmodell ansonsten nicht verändern möchten, schalten wir alles andere während des Trainings aus. Der folgende Code-Block trainiert textcat mit n_iter Durchläufen durch den Trainingsdatensatz.

other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'textcat']
 
n_iter = 10
with nlp.disable_pipes(*other_pipes):  
    optimizer = nlp.begin_training()
    print("Training the model...")
    print('{:^5}'.format('LOSS'))
    for i in range(n_iter):
        losses = {}
        dropout = decaying(0.6, 0.2, 1e-4)
        batches = minibatch(data_train, size=compounding(1., 16., 1.001))
        for batch in batches:
            texts, annotations = zip(*batch)
            nlp.update(texts, annotations, sgd=optimizer, drop=next(dropout), losses=losses)
        print('{0:.3f}'.format(losses['textcat']))

spaCy bietet verschiedene Möglichkeiten, Einfluss auf das Training zu nehmen und verschiedene fortgeschrittene Techniken zum Trainieren einzusetzen. Wer sich dafür interessiert, kann hier fündig werden. Je nach Projektkontext gilt es aber frühzeitige Optimierungen zu vermeiden. Ein solides Modell in Produktion ist oft besser als Ideen für ein optimiertes Modell im Jupyter Notebook.

Evaluation

Unser Modell können wir nun mithilfe der Testdaten evaluieren. Durch

def keywithmaxval(d):
    v=list(d.values())
    k=list(d.keys())
 
    return k[v.index(max(v))]
 
y_pred = []
for _, text in X_test.iteritems():
    doc = nlp(text)
    cat_pred = keywithmaxval(doc.cats)
    y_pred.append(cat_pred)

erhalten wir die Vorhersagen unseres trainierten Modells für die E-Mails des Testdatensatzes. Nun können wir mittels

import matplotlib.pyplot as plt
import seaborn as sns;
from sklearn.metrics import classification_report, confusion_matrix
 
mat = confusion_matrix(y_test, y_pred, labels=cats)
plt.figure(figsize = (10,7))
sns.set(font_scale=1.2)
sns.heatmap(mat, square=True, annot=True, cbar=False, cmap="YlGnBu",annot_kws={"size": 13}, xticklabels=cats, yticklabels=cats)
plt.xlabel('Predicted label')
plt.ylabel('True label')
plt.show()
 
print(classification_report(y_test, y_pred, labels=cats))

einerseits eine Confusion Matrix und einen classification_report mit den Werten für Accuracy, Recall und F1-Score zu allen Kategorien erhalten. Anhand dieser Metriken kann man die Qualität des Modells in einer ersten Iteration ganz gut beurteilen.

Für unser vorliegendes Beispiel bekomme ich mit n_iter = 25 die folgende Confusion Matrix.

Die Ergebnisse sind für den vorliegenden kleinen Datensatz schon ganz gut. So wird zum Beispiel die Kategoriegruppe 12 "Problemen mit der Registrierung der Software" recht zuverlässig erkannt.

Auffällig ist Kategoriegruppe 16 "Kein Brenner gefunden". Hier wurden zwar alle vier im Testdatensatz enthaltenen E-Mails richtig erkannt, jedoch sind auch viele andere E-Mails fälschlicherweise dieser Kategoriegruppe zugeordnet. Um die Ergebnisse weiter zu verbessern gibt es verschiedene Möglichkeiten. Die wirksamste Möglichkeit ist bei Deep-Learning-Modellen in der Regel, dass man “einfach” mit einem größeren Datensatz trainiert. Insbesondere sind für einige Kategoriegruppen etwa von 1, 13, und 18 fast gar keine E-Mails im Datensatz vorhanden.

Für eine wirklich fundierte Beurteilung der Qualität des Modells reicht unser Beispieldatensatz leider nicht aus.

Fazit

In diesem Blogpost haben wir gesehen, dass es mithilfe von Open-Source-Bibliotheken wie spaCy heute möglich ist, ohne viel Data Science Know-how mit modernen Deep-Learning-Modellen E-Mails zu klassifizieren. Dabei wird sich der grundsätzliche Ablauf für einen anderen Datensatz nur minimal ändern.

Die Hürde für Softwareprojekte mit Deep-Learning-Anteil ist dadurch deutlich niedriger geworden. Es ist möglich, einen echten Mehrwert mit Deep-Learning zu erzeugen, ohne in einer Explorationsphase von mehreren Wochen mit viel Spezialwissen zunächst Modelle zu entwickeln und zu evaluieren.

Wer mehr über NLP erfahren möchte findet hier ein passendes Video.

Mehr Informationen zum Thema Künstliche Intelligenz haben wir auf codecentric.ai zusammengestellt.

Marcel Mikl

Durch die mathematische Prägung im Zuge seiner Promotion ist es Marcel gewohnt, auftretende Probleme strukturiert zu lösen. Derzeit interessiert sich Marcel insbesondere für aktuelle Technologien rund um das Thema Data Science und Machine Learning.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.