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
1import os 2import pandas as pd 3import numpy as np 4import random 5from 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
1def get_nice_text(item):
2 text = item.getText()
3 text = text.replace('\r',' ')
4 text = text.replace('\n','')
5 return text.strip()
6
7with open(os.path.join(PATH_TO_DATA,'omq_public_interactions.xml'), 'rb') as f:
8 soup = BeautifulSoup(f.read(), 'html.parser')
9
10mails = soup.find_all("text")
11categories = soup.find_all("category")
12
13rows = []
14for item, cat in zip(mails, categories):
15 text = get_nice_text(item)
16 categories = cat.getText().split(',')
17 if len(categories) > 0:
18 category = random.choice(categories)
19 else:
20 category = np.nan
21 rows = rows + [[text, categories, category]]
22
23df = 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
1df.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.
1with open(os.path.join(PATH_TO_DATA, 'omq_public_categories.xml'), 'rb') as f: 2 soup = BeautifulSoup(f, 'html.parser') 3 4categoryGroups = soup.find_all('categorygroup') 5 6rows = [] 7for group in categoryGroups: 8 group_id = group['id'] 9 categories = group.find_all('category') 10 cat_ids = [] 11 for cat in categories: 12 cat_id = cat['id'] 13 cat_ids.append(cat_id) 14 cat_text = get_nice_text(cat) 15 rows = rows + [[group_id, cat_ids, cat_text]] 16 17df_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.
1def get_encode_dic():
2 encode_dic = {}
3 for index, row in df_cat.iterrows():
4 for cat_id in row.cat_ids:
5 encode_dic[cat_id] = row.group_id
6 return encode_dic
7
8encode_dic = get_encode_dic()
9df['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
1df.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
1import os 2import spacy 3import pandas as pd 4 5from spacy import displacy 6from spacy.util import minibatch, compounding, decaying 7 8from sklearn.model_selection import StratifiedShuffleSplit
Zunächst laden wir mit dem Befehl
1df = 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
1X_all = df.text 2y_all = df.category_group 3 4seed = 42 5sss = StratifiedShuffleSplit(n_splits=1, test_size=0.15, random_state=seed) 6for train_index, test_index in sss.split(X_all, y_all): 7 X_train, X_test = X_all.iloc[train_index], X_all.iloc[test_index] 8 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
1cats = df.category_group.unique()
2
3def get_catlist(y):
4 cat_list = []
5 for _, val in y.iteritems():
6 categories = {'cats': {cat: 0.0 for cat in cats}}
7 if val in cats:
8 categories['cats'][val] = 1.0
9 cat_list.append(categories)
10 return cat_list
11
12data_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
1python -m spacy download de_core_news_sm
über die Konsole heruntergeladen werden. Danach laden wir mit
1nlp = spacy.load('de_core_news_sm')
das deutsche Sprachmodell in unser Notebook und aktivieren über
1if 'textcat' not in nlp.pipe_names: 2 textcat = nlp.create_pipe('textcat') 3 nlp.add_pipe(textcat, last=True)
das spaCy-Modell textcat
zur Textklassifizierung. Als nächstes fügen wir über
1for cat in cats: 2 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.
1other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'textcat'] 2 3n_iter = 10 4with nlp.disable_pipes(*other_pipes): 5 optimizer = nlp.begin_training() 6 print("Training the model...") 7 print('{:^5}'.format('LOSS')) 8 for i in range(n_iter): 9 losses = {} 10 dropout = decaying(0.6, 0.2, 1e-4) 11 batches = minibatch(data_train, size=compounding(1., 16., 1.001)) 12 for batch in batches: 13 texts, annotations = zip(*batch) 14 nlp.update(texts, annotations, sgd=optimizer, drop=next(dropout), losses=losses) 15 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
1def keywithmaxval(d):
2 v=list(d.values())
3 k=list(d.keys())
4
5 return k[v.index(max(v))]
6
7y_pred = []
8for _, text in X_test.iteritems():
9 doc = nlp(text)
10 cat_pred = keywithmaxval(doc.cats)
11 y_pred.append(cat_pred)
erhalten wir die Vorhersagen unseres trainierten Modells für die E-Mails des Testdatensatzes. Nun können wir mittels
1import matplotlib.pyplot as plt 2import seaborn as sns; 3from sklearn.metrics import classification_report, confusion_matrix 4 5mat = confusion_matrix(y_test, y_pred, labels=cats) 6plt.figure(figsize = (10,7)) 7sns.set(font_scale=1.2) 8sns.heatmap(mat, square=True, annot=True, cbar=False, cmap="YlGnBu",annot_kws={"size": 13}, xticklabels=cats, yticklabels=cats) 9plt.xlabel('Predicted label') 10plt.ylabel('True label') 11plt.show() 12 13print(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.
Weitere Beiträge
von Marcel Mikl
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Marcel Mikl
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.