1.3 MiB
Kurzfassung¶
Das Spritzgießen ist ein häufig in der Kunststoffverarbeitung eingesetztes Verfahren und ist deshalb von großer ökologischer und ökonomischer Bedeutung. Ziel dieser Arbeit war es, die üblicherweise von Spritzgussmaschinen bereitgestellten Daten zu nutzen, um Fehlteile – also Teile mit unzulässigen Qualitätsmängeln – unmittelbar zu erkennen und damit aussortieren zu können. Es hat sich gezeigt, dass dies für die untersuchten Qualitätsmängel mit einer Genauigkeit von 98,0 - 99,0 % unter Anwendung linearer Machine-Learning-Modelle möglich ist. Darüber hinaus konnte gezeigt werden, dass durch die Auswahl repräsentativer Datenpunkte der dafür erforderliche Aufwand beim Labeling auf einen Bruchteil des ursprünglichen Aufwands reduziert werden kann. Diese repräsentative Auswahl erfolgte auf der Basis unüberwachter Clustering-Algorithmen.
Inhaltsverzeichnis¶
- Einleitung
- Vorbereitung
- Einführung des vorliegenden Datensatzes
- Klassifizierung anhand eines Merkmals
- Klassifizierung anhand mehrerer Merkmale
- Teilüberwachtes Lernen
- Ergebnisse und Evaluation
- Ausblick
1. Einleitung¶
Das Spritzgießen ist ein Verfahren aus der Kunststoffverarbeitung, um Rohmaterial (Kunststoffgranulat) in eine gewünschte Form zu bringen (sog. Urformverfahren). Technisch handelt es sich dabei um einen komplexen Prozess, dessen Resultat von zahlreichen Variablen abhängt. Aus diesem Grund kommt es hin und wieder vor, dass die gespritzten Teile nicht den Qualitätsstandards eines Herstellers entsprechen und aussortiert werden müssen.
Ziel der vorliegenden Arbeit ist es, diese fehlerhaften Teile anhand der internen Messwerte der Spritzgussmaschine (sog. Prozessdaten) automatisch auszusortieren. Grundlage dafür ist ein Datensatz, welcher in einer vorherigen Hausarbeit [1] erarbeitet wurde. Im Zuge dessen sollen auch Pipelines erarbeitet und Methoden gefunden werden, sodass eine Übertragung der Ergebnisse auf andere Fehler und Produkte erleichtert wird.
Motiviert wird diese Zielsetzung aus verschiedenen Richtungen. Die untersuchten Produkte werden nach ihrer Fertigung vollautomatisch weiterverarbeitet und gehen anschließend direkt in den Verkauf. Es existiert bisher kein System, welches fehlerhafte Teile automatisch aussortiert. Aufgrund der vollautomatischen Abläufe fallen diese Teile auch den Mitarbeitern nicht immer auf und erreichen somit teilweise den Endkunden. Dies sorgt für Unzufriedenheit und unter Umständen einen Imageschaden. Außerdem entsteht sowohl beim Kunden als auch Hersteller ein Mehraufwand für den Austausch des Produkts. Hinzu kommt die zeitliche Verzögerung für den Endkunden.
Des Weiteren sind fehlerhafte und damit unbrauchbare Teile für ein Unternehmen sowohl aus ökologischer als auch ökonomischer Sicht zu vermeiden. Insbesondere für die Fertigungsplanung ist es außerdem wichtig, fehlerhafte Teile unmittelbar zu erkennen, damit die geplante Anzahl an (fehlerfreien) Teilen produziert werden kann.
Im nachfolgenden Kapitel 3 wird zunächst der vorliegende Datensatz eingeführt. Der Hauptteil beginnt in Kapitel 4 mit dem Versuch, den Datensatz anhand eines einzigen Merkmals linear zu separieren. Im anschließenden Kapitel 5 werden komplexerere Algorithmen untersucht, welche mehrere Merkmale gleichzeitig berücksichtigen können.
Kapitel 6 konzentriert sich darauf, wie die Berücksichtigung weiterer Fehler und Produkte vereinfacht werden kann. Dazu wird der Ansatz des teilüberwachten Lernens verfolgt. Dieser erfordert zunächst eine Dimensionsreduktion der Daten. Anschließend können mit Hilfe von Clustering-Algorithmen repräsentative Datenpunkte ausgewählt und gelabelt werden. Zum Abschluss wird die Qualität der Klassifikatoren untersucht, welche auf daraus resultierenden Daten trainiert wurden.
Im Schlussteil der Arbeit werden die Ergebnisse zusammengefasst und kritisch bewertet. Außerdem wird ein Ausblick gegeben.
2. Vorbereitung¶
Zunächst müssen einige allgemeine Vorbereitungen getroffen werden, um nachfolgend die Daten verarbeiten zu können. Grundlage dieser Arbeit bildet die Programmiersprache Python und insb. die Module Numpy, Pandas, SciKit-Learn sowie Matplotlib.
# Kontrolle der Python-Version
import sys
assert sys.version_info >= (3, 5)
# Import von Scikit-Learn und Kontrolle der Version
import sklearn
assert sklearn.__version__ >= "0.20"
# Weitere Imports
import pandas as pd
import numpy as np
import os
# Imports und Einstellungen um Abbildungen mit matplotlib erzeugen und im
# Notebook darstellen zu können
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc("axes", labelsize=14)
mpl.rc("xtick", labelsize=12)
mpl.rc("ytick", labelsize=12)
# Dictionary zur Abspeicherung der Zwischenergebnisse für den Ergebnis-Teil
results = []
Damit die Ausgaben des Notebooks vergleichbar sind wird außerdem der entsprechende Seed gesetzt.
np.random.seed(42)
Im nächsten Schritt kann der Datensatz eingelesen werden. Dieser kann entweder im aktuellen Verzeichnis liegen oder über einen entsprechenden Link aus Google Drive geladen werden.
# Datei via Link aus Google Drive laden
# Alternativ können Sie die Datei 'df.pkl' auch manuell dem aktuellen Unterordner
# hinzufügen
!gdown --id 1r5OzQmxj2TIZUMm3UwKOA-9znGQoe31j
# Daten einlesen
import pickle
with open("df.pkl", "rb") as file:
df = pickle.load(file)
# Kontrolle
assert len(df.columns) == 107
assert len(df.index) == 4548
print("Anzahl Spalten:", len(df.columns))
print("Anzahl Zeilen:", len(df.index))
3. Einführung des vorliegenden Datensatzes¶
3.1 Überblick¶
Wie bereits in der Einleitung erwähnt, wird in dieser Arbeit ein Datensatz untersucht, welcher in einer vorherigen Hausarbeit [1] erarbeitet wurde. Er umfasst die nachfolgende Anzahl an Datenpunkten, Merkmalen und Zielwerten:
print("Übersicht")
print("- Anzahl Datenpunkte:", len(df.index) - 1)
print("- Anzahl Merkmale:", len(df.drop(["Labels"], axis=1).columns))
print("- Anzahl Zielwerte:", len(df["Labels"].columns))
Jeder Datenpunkt beschreibt ein gespritztes Teil. Konkret handelt es sich dabei um das Unterteil des Kabelabzweigkastens DK 0200 G der Gustav Hensel GmbH & Co. KG. Abbildung 1 zeigt ein Exemplar:
Abbildung 1: DK 0200 G nach [2]
Die nachfolgende Abbildung 2 zeigt exemplarisch ein Fotos eines fehlerfreien DK 0200 G.
Abbildung 2: Foto eines fehlerfreien DK 0200 G
3.2 Merkmale¶
Da moderne Spritzgussmaschinen eine Vielzahl an Daten bereitstellen, wird jeder Datenpunkt durch mehr als 100 Merkmale beschrieben. Es wurde bewusst darauf verzichtet, auf Grundlage domänenspezifischen Wissens bereits vorab Merkmale auszusortieren. Es wird zunächst davon ausgegangen, dass sämtliche Merkmale potenziell relevant sind.
Alle Merkmale im Detail zu beschreiben wäre nicht zielführend. Stattdessen wird auf die relevanten Merkmale an den entsprechenden Stelle der Arbeit eingegangen. Für das Arbeiten mit einer solchen Vielzahl an Merkmalen ist jedoch eine Gruppierung hilfreich. Diese kann auf Grundlage des Fertigungsprozesses erfolgen. Der DK 0200 G wird aus zwei Komponenten – also zwei unterschiedlichen Kunststoffen – gespritzt. Zunächst wird der Grundkörper aus Polypropylen gespritzt. In einem zweiten Fertigungsschritt werden die Membranen zur Einführung von Kabeln aus TPE (Thermoplastischen Elastomeren) ergänzt. Beide Fertigungsschritte laufen prinzipiell ähnlich ab und werden deshalb durch dieselben Merkmale beschrieben. Insgesamt ergeben sich die nachfolgenden Gruppen an Merkmalen:
Internal
: komponentenunabhängige Messwerte der Spritzgussmaschine (z.B. Zykluszeit)Internal_C1
: Messwerte der Spritzgussmaschine an Komponente 1 (z.B. Einspritzvolumen)Internal_C2
: Messwerte der Spritzgussmaschine an Komponente 2 (z.B. Einspritzvolumen)External
: Messwerte externer Sensoren (z.B. Umgebungstemperatur)Time related
: Abgeleitete Merkmale aus den Zeitstempeln der Teile (z.B. Zeit seit letzter Wartung)
Für eine detaillierte Beschreibung des Spritzgussprozesses und der einzelnen Merkmale siehe [1]. Die Merkmale teilen sich folgendermaßen auf die Gruppen auf:
# Im DataFrame wurde diese Gruppierung mit Hilfe eines MultiIndex umgesetzt
pd.DataFrame(df.drop(["Labels"], axis=1).droplevel(1, axis=1).columns).value_counts()
Die meisten Merkmale sind interne Messwerte der Spritzgussmaschine, wobei jeweils ca. 20 % einer einzelnen Komponente zugeordnet werden können. Um die Daten nachfolgend einheitlich darzustellen, ist es sinnvoll, zunächst eine entsprechende Funktion zu definieren:
def formatForPlotting(df):
for number in range(10):
df.columns = df.columns.str.replace(r" " + str(number), "_" + str(number))
return df
Die ersten Daten der Gruppe Internal
sehen dann bspw. so aus:
formatForPlotting(df["Internal"]).head(3)
Die Merkmale der Gruppe Internal_C1
sind wie bereits erwähnt ähnlich der Gruppe Internal_C2
und sehen so aus:
formatForPlotting(df["Internal_C1"]).head(3)
Die restlichen Merkmale werden nachfolgend bei Bedarf dargestellt.
3.3 Zielwerte¶
Der Datensatz ist gelabelt und umfasst somit Zielwerte:
# Die Zielwerte bilden eine eigene Gruppe im MultiIndex
formatForPlotting(df["Labels"]["2021-01-13 17"]).head(3)
Jedes Label beschreibt einen Fehler, welcher im beobachteten Zeitraum aufgetreten ist. Die nachfolgende Abbildung 3 zeigt ein Teil, welches alle diese Fehler gleichzeitig aufweist. Dies muss nicht zwingend der Fall sein.

Abbildung 3: Beobachtete Fehler am DK 0200 G
Die Fehler 0_leak_corner_tl
und 0_leak_corner_tr
sind links bzw. rechts oben zu sehen. An diesen Stellen wird zu viel Material der Komponente 2 (TPE) in das Innere des Gehäuses gesprizt. Der Fehler 1_hole_bottom
ist unten zu beobachten: Hier fehlt das entsprechende Material. Die Häufigkeit dieser Fehler ist sehr unterschiedlich:
print("Anzahl Datenpunkte:", len(df))
print("Anzahl Fehler:")
# Wenn der Wert einer Zielvariable nicht 0 ist lag ein Fehler vor
print(df["Labels"].astype(bool).sum(axis=0))
# Quelle: https://stackoverflow.com/questions/26053849/counting-non-zero-values-in-each-column-of-a-dataframe-in-python
*) Anmerkung: Diese Zahlen wirken auf den ersten Blick sehr hoch. Allerdings wurde, um einen vollständigen Datensatz zu erhalten, jegliche Abweichung vom Optimum als Fehler eingestuft. Dem Endkunden würden diese in der Regel nicht auffallen. Außerdem wurde bewusst ein extrem fehlerlastiger Zeitraum gewählt. In anderen Zeiträumen treten wochenlang quasi gar keine Fehler auf. Bei diesen hohen Zahlen handelt es sich deshalb wahrscheinlich um einen – zumindest im Sinne dieser Arbeit – "glücklichen" Zufall.
Die Fehler 0_leak_corner_tl
und 0_leak_corner_tr
sind mit Abstand am häufigsten aufgetreten und stehen deshalb im Fokus dieser Arbeit. Im Datensatz werden die Fehler als eine Ganzzahl zwischen 0 bis 3 codiert. Die Zahlen haben nachfolgende Bedeutungen:
0
: kein Fehler1
: schwacher Fehler2
: mittlerer Fehler3
: starker Fehler
Für eine exakte Beschreibung, wie diese Einteilung erfolgt ist, siehe [1].
In der Regel lag entweder kein Fehler vor oder dieser war sehr stark:
def plotLabelHist(labels):
fig, ax = plt.subplots()
# Daten
bins = [0 - 0.5, 1 - 0.5, 2 - 0.5, 3 - 0.5, 4 - 0.5]
ax.hist(labels.to_numpy(), bins=bins, label=labels.columns, edgecolor="black")
# Achsen
plt.xlabel("Stärke der Ausprägung", size=18)
plt.ylabel("Anzahl", size=18)
plt.xticks([0, 1, 2, 3])
# Titel und Legende
plt.title("Übersicht der beobachteten Fehler", size=18, pad=10)
plt.legend(prop={"size": 10})
fig.set_size_inches(8, 6)
fig.tight_layout()
plotLabelHist(df["Labels"])
plt.show()
Zusammenfassend kann festgehalten werden, dass der Datensatz ca. 4500 Teile umfasst welche durch ca. 100 Merkmale beschrieben werden. Diese Merkmale ergeben sich überwiegend aus den internen Messungen der Spritzgussmaschine. Interessant sind vor Allem die Zielwerte 0_leak_corner_tl
und 0_leak_corner_tr
, welche in der Regel entweder gar nicht oder sehr stark auftreten.
3.4 Aufteilen der Daten¶
Zunächst wird der Datensatz in Trainings- und Testdaten unterteilt. Wie im vorherigen Unterkapitel gezeigt können manche Fehler sehr selten sein. Deshalb wird eine stratifizierte anstatt einer rein zufälligen Stichprobe gezogen, vgl. [3].
from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_idx, test_idx in split.split(df, df["Labels"]["0_leak_corner_tr"]):
df_train = df.iloc[train_idx]
df_test = df.iloc[test_idx]
print("Anzahl Datenpunkte:")
print("Trainingsdaten:", len(df_train))
print("Testdaten:", len(df_test))
Im nächsten Schritt werden die Merkmale und Zielwerte voneinander getrennt:
# Merkmale
X_train = df_train.drop("Labels", axis=1)
X_test = df_test.drop("Labels", axis=1)
# Zielwerte
y_train = df_train["Labels"].copy()
y_test = df_test["Labels"].copy()
3.5 Erkunden der Daten¶
Nun können die Trainingsdaten erkundet werden. Ein gängiges Hilfsmittel dazu ist die sog. Korrelationsmatrix. Diese enthält den Pearson-Korrelationskoeffizienten für jedes einzelne Merkmal mit jedem anderen. Um einen Überblick zu bekommen wird diese zunächst grafisch dargestellt:
def plotCorrMatrix(df):
fig = plt.figure(figsize=(7, 7))
# Korrelationsmatrix
ax = fig.add_subplot(111)
cax = ax.matshow(df.corr())
fig.colorbar(cax)
# Titel und Achsenbeschriftung
ax.set_title("Korrelationsmatrix", size=18, pad=18)
ax.set_xlabel("Merkmale", labelpad=20)
ax.set_ylabel("Merkmale", labelpad=20)
plotCorrMatrix(X_train)
plt.show()
In der Grafik sind sowohl stark positive (nahe 1) als auch stark negative (nahe -1) Korrelationen zu erkennen. Insbesondere rechts unten sind zusammenhängende Bereiche stark korrelierender Merkmale sichtbar. Diese Beobachtungen deuten darauf hin, dass eine Dimensionsreduktion nachfolgend ein hilfreicher Zwischenschritt sein könnte.
Interessant können auch die Korrelationen der Merkmale mit den Zielwerten sein. Nachfolgend werden die am stärksten mit dem Fehler 0_leak_corner_tr
korrelierenden Merkmale aufgelistet.
fault_corr = df.corr()["Labels"]["0_leak_corner_tr"].sort_values(ascending=False)
# Korrelationen der Fehler untereinander entfernen
fault_corr = fault_corr.drop("Labels", level=0, axis=0)
Diese 5 Merkmale besitzen die stärkste positive Korrelation mit dem Fehler:
fault_corr.head(5)
Diese 5 Merkmale besitzen die stärkste negative Korrelation:
fault_corr.tail(5)
Grundsätzlich existieren stark korrelierende Merkmale (> 0,8). Diese entstammen größtenteils der Gruppe Internal_C2
. Nachfolgend werden die Histogramme der beiden am stärksten korrelierenden Merkmale dargestellt:
fig, axes = plt.subplots(1, 2)
fig.set_size_inches(14, 6)
fig.suptitle("Histogramme der am stärksten korrelierenden Merkmale", fontsize=16)
# Histogramme
df.hist(fault_corr.index[0], bins=50, ax=axes[0])
df.hist(fault_corr.index[1], bins=50, ax=axes[1])
axes[0].set_title(fault_corr.index[0][1], pad=8)
axes[1].set_title(fault_corr.index[1][1], pad=8)
plt.show()
In beiden Histogramm sind zwei klar getrennte Cluster erkennbar. Es liegt die Vermutung nahe, dass es sich dabei um die fehlerfreien und fehlerhaften Teile handelt. Möglicherweise können diese bereits anhand eines einzigen Merkmals separiert werden.
Das Erkunden der Daten hat gezeigt, dass relativ starke lineare Zusammenhänge im Datensatz existieren sowie Cluster erkennbar sind. Diese Erkenntnisse können nachfolgend bspw. bei der Auswahl von Algorithmen für bestimmte Probleme hilfreich sein.
4. Klassifizierung anhand eines Merkmals¶
Am einfachsten könnten fehlerhafte Teile mit Hilfe der vorhandenen Funktionen der Spritzgussmaschine aussortiert werden. Bei diesen Maschinen können üblicherweise Grenzwerte für einzelne interne Messwerte vorgegeben werden. Werden diese Grenzwerte über- bzw. unterschritten sortiert das sog. Handling (der "Roboterarm") das Teil automatisch aus. Das Finden passender Grenzwerte ist dabei die größte Herausforderung.
Um diese Aufgabe zu automatisieren, bietet sich der CART (Classification and Regression Trees)-Algorithmus an. Dieser wird genutzt um Entscheidungsbäume zu generieren. Wichtig dabei ist, dass er ausschließlich Binärbäume erzeugt. Bei jeder Abzweigungen von einem Knoten versucht der Algorithmus, den gewichteten Informationsgehalt der nachfolgenden Knoten zu maximieren. Je höher dieser Informationsgehalt, desto genauer können die Datenpunkte in der nachfolgenden Ebene klassifiziert werden, vgl. [4]. Der Algorithmus arbeitet dabei in jedem Schritt "greedy". Das bedeutet, er versucht in jedem einzelnen Schritt das optimale Ergebnis zu erzielen. Ein größerer Kontext wird nicht beachtet. Aufgrund dieser Eigenschaften (binär und "greedy") eignet sich der CART optimal für das Finden der Grenzwerte der Spritzgussmaschine.
Die Spritzgussmaschine kann ein Teil entweder aussortieren oder nicht. Folglich müssen zunächst die Zielwerte angepasst werden. Nach Rücksprache mit dem Fachpersonal werden die Fehlerausprägungen 0 und 1 als fehlerfreies sowie 2 und 3 als fehlerhaftes Teil eingestuft:
def convertLabelsToBinary(labels):
labels_bin = labels.copy()
labels_bin.replace(1, 0, inplace=True)
labels_bin.replace(2, 3, inplace=True)
# Zur besseren Lesbarkeit wird die Stufe 3 abschließend in 1 "umbenannt"
labels_bin.replace(3, 1, inplace=True)
return labels_bin
y_train_01 = convertLabelsToBinary(y_train)
y_test_01 = convertLabelsToBinary(y_test)
Außerdem kann der CART (wie auch die Spritzgussmaschine) nur mit numerischen Merkmalen umgehen. Die text-basierten Merkmale werden nachfolgend entfernt.
X_train_num = X_train.drop(X_train.select_dtypes(exclude=np.number), axis=1)
X_test_num = X_test.drop(X_train.select_dtypes(exclude=np.number), axis=1)
Da für den CART keine Skalierung der Merkmale erforderlich ist kann der Algorithmus unmittelbar trainiert werden:
from sklearn.tree import DecisionTreeClassifier
# max_depth = 1 da nur der beste Grenzwert gesucht ist
tree_clf = DecisionTreeClassifier(max_depth=1, random_state=42)
tree_clf.fit(X_train_num, y_train_01["0_leak_corner_tr"])
Um das Ergebnis darzustellen kann das Modul graphviz
genutzt werden:
from sklearn.tree import export_graphviz
from graphviz import Source
# .dot-Datei exportieren
export_graphviz(
tree_clf,
out_file="tree_clf.dot",
feature_names=X_train_num.droplevel(0, axis=1).columns,
class_names=["Gut", "Schlecht"],
rounded=True,
filled=True,
)
# .dot-Datei in Graph umwandeln und darstellen
Source.from_file("tree_clf.dot")
Das Ergebnis sieht vielversprechend aus: In nur einem Schritt wurden zwei Gruppen gebildet, welche zu einem sehr großen Teil entweder nur aus Gut- oder nur aus Schlechtteilen bestehen. Der Informationsgehalt wurde somit stark erhöht.
An dieser Stelle offenbart sich ein weiterer großer Vorteil des CART. Bei ihm handelt es sich im Gegensatz zu vielen anderen ML-Algorithmen um einen sog. White-Box-Algorithmus. Das bedeutet, dass seine Entscheidungsfindung sehr einfach nachvollzogen werden kann. In diesem Fall scheint der Spezifische Einspritzdruck Spitzenwert entscheidend zu sein. Übersteigt dieser einen Wert von ca. 400, werden fehlerhafte Teile produziert. Hieraus könnten vom Fachpersonal weitere Erkenntnisse abgeleitet werden.
Um für die quantitative Beurteilung noch nicht auf die Testdaten zurückgreifen zu müssen wird zur Bewertung eine Kreuzvalidierung, implementiert durch die Klasse cross_val_score
, durchgeführt. Da fehlerhafte Teile nicht selten sind kann die Genauigkeit als Bewertungskriterium genutzt werden:
from sklearn.model_selection import cross_val_score
tree_cv = cross_val_score(
tree_clf, X_train_num, y_train_01["0_leak_corner_tr"], cv=10, scoring="accuracy"
)
def outputCVResults(cv):
print("Genauigkeit bei der Kreuzvalidierung")
print("- Mittelwert:", "{:.2f}".format(100 * cv.mean()), "%")
print("- Standardabw.:", "{:.2f}".format(100 * cv.std()), "%")
outputCVResults(tree_cv)
results.append(("Tree_cv", 100 * tree_cv.mean()))
Tatsächlich lassen sich die Trainingsdaten mit einer Genauigkeit von ca. 98,9 % anhand eines einzigen Merkmals klassifizieren. Auf den Testdaten sieht das Ergebnis ähnlich aus, es scheint kein Overfitting vorzuliegen:
tree_cv = cross_val_score(
tree_clf, X_test_num, y_test_01["0_leak_corner_tr"], cv=10, scoring="accuracy"
)
outputCVResults(tree_cv)
results.append(("Tree_test", 100 * tree_cv.mean()))
Diese Lösung kann weiter verbessert werden, indem auch die Grenzwerte für das 2., 3. usw. beste Merkmale ermittelt und in die Spritzgussmaschine eingetragen werden:
# Einstellungen
n_features = 10
label = "0_leak_corner_tr"
# Kopie der Daten erstellen da in jedem Durchlauf das "beste" Merkmal entfernt wird
X_train_cpy = X_train_num.copy()
X_test_cpy = X_test_num.copy()
# Initialisierungen
tree_clf_tmp = DecisionTreeClassifier(max_depth=1, random_state=42)
features, train_scores, test_scores = [], [], []
for _ in range(n_features):
# Entscheidungsbaum trainieren
tree_clf_tmp.fit(X_train_cpy, y_train_01[label])
# Ergebnis abspeichern
features.append(X_train_cpy.columns[tree_clf_tmp.feature_importances_.argmax()][1])
train_scores.append(
100
* cross_val_score(
tree_clf_tmp, X_train_cpy, y_train_01[label], cv=5, scoring="accuracy"
).mean()
)
test_scores.append(
100
* cross_val_score(
tree_clf_tmp, X_test_cpy, y_test_01[label], cv=5, scoring="accuracy"
).mean()
)
# Bestes Merkmal für den nächsten Durchlauf entfernen
X_train_cpy = X_train_cpy.drop(
X_train_cpy.columns[tree_clf_tmp.feature_importances_.argmax()], axis=1
)
X_test_cpy = X_test_cpy.drop(
X_test_cpy.columns[tree_clf_tmp.feature_importances_.argmax()], axis=1
)
# Zur besseren Darstellung in DataFrame umwandeln
data = {"Merkmal": features, "Training [%]": train_scores, "Test [%]": test_scores}
df_features = pd.DataFrame(data)
style = df_features.style
style = style.format({"Training [%]": "{:.2f}"})
style = style.format({"Test [%]": "{:.2f}"})
style = style.background_gradient(cmap="viridis")
style = style.set_properties(**{"text-align": "right"})
style = style.set_properties(**{"text-align": "left"}, subset=["Merkmal"])
style = style.hide_index()
style
Neben dem Spezifischen Einspritzdruck Spitzenwert erzielen fünf weitere Merkmale eine Genauigkeit von über 95 % auf den Testdaten. Wenn das beste Merkmal nicht mehr ausreichen sollte könnten aus diesen Merkmalen zukünftig weitere Grenzwerte abgeleitet werden. Dazu muss der entsprechende Entscheidungsbaum betrachtet werden. Durch eine Verschiebung der ermittelten Grenzwerte in Richtung der Gut- bzw. Schlechtteile kann außerdem entweder die Sensitivität oder die Präzision des tems entsprechend der spezifischen Anforderungen angepasst werden.
5. Klassifizierung anhand mehrerer Merkmale¶
Die bei der Firma Gustav Hensel GmbH & Co. KG verwendeten Spritzgussmaschinen stellen ihre internen Messwerte unmittelbar nach Fertigstellung eines Teils über einen USB-Anschluss zur Verfügung. Dies eröffnet die Möglichkeit, auf externer Hardware einen komplexeren Klassifikator laufen zu lassen. Die Ausgabe des Klassifikators kann von den Maschinen über einen digitalen Eingang eingelesen und das gespritzte Teil bei Bedarf aussortiert werden.
Die meisten dieser "komplexeren" Klassifikatoren erfordern eine Vorverarbeitung der Daten. Deshalb wird im nachfolgenden Unterkapitel zunächst eine Pipeline zur Datenvorverarbeitung aufgebaut.
5.1 Pipeline¶
Grundsätzlich haben die Merkmale sehr unterschiedliche Wertebereiche:
formatForPlotting(X_train["Internal_C1"].describe()).iloc[[1, 3, 7]]
Außerdem sind alle bis auf ein Merkmal numerisch. Das einzige text-basierte Merkmal ist dabei nicht sehr aussagekräftig, da es fast immer einen Bindestrich enthält:
X_train.select_dtypes(exclude=[np.number]).value_counts()
Deshalb wird dieses Merkmal von der nachfolgend aufgebauten Pipeline aussortiert.
Zunächst wird eine Pipeline für die numerischen Merkmale aufgebaut. Diese übernimmt im Wesentlichen die Skalierung der Merkmale. Der Datensatz besitzt einige Ausreißer. Aus diesem Grund wird der StandardScaler
verwendet, welcher wesentlich robuster gegenüber Ausreißern ist als der MinMaxScaler
.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([("scaler", StandardScaler())])
Im nächsten Schritt wird die vollständige Pipeline definiert. Diese sortiert das text-basierte Merkmal aus. Durch die Verwendung der Klasse ColumnTransformer
können diese Merkmale zukünftig sehr einfach mit Hilfe einer eigenen Pipeline vorverarbeitet werden.
from sklearn.compose import ColumnTransformer
num_features = list(X_train.select_dtypes(include=[np.number]))
text_features = list(X_train.select_dtypes(exclude=[np.number]))
pipeline = ColumnTransformer(
[("num", num_pipeline, num_features), ("text", "drop", text_features)]
)
Nun kann die Pipeline an die Trainingsdaten angepasst und anschließend auf alle Daten angewendet werden:
# Anpassen an die Trainingsdaten
pipeline.fit(X_train)
# Transformation der Trainings- und Testdaten
X_train_tr = pipeline.transform(X_train)
X_test_tr = pipeline.transform(X_test)
# Wiederherstellung der DataFrames
X_train_tr = pd.DataFrame(
X_train_tr, X_train[num_features].index, X_train[num_features].columns
)
X_test_tr = pd.DataFrame(
X_test_tr, X_test[num_features].index, X_test[num_features].columns
)
Die vorverarbeiteten Daten können im nächsten Schritt zum Trainieren der Klassifikatioren genutzt werden.
5.2 Klassifikatoren¶
Wie bereits in den vorherigen Kapitel wird stellvertretend der häufigste Fehler 0_leak_corner_tr
untersucht.
label = "0_leak_corner_tr"
5.3 Logistische Regression¶
In der Regel ist es sinnvoll, mit möglichst wenigen Annahmen zu starten. Deshalb wird im ersten Versuch ein lineares Modell trainiert. Dieses wird durch die Klasse LogisticRegeression
implementiert.
from sklearn.linear_model import LogisticRegression
log_clf = LogisticRegression(random_state=42)
log_clf.fit(X_train_tr, y_train_01[label])
Auch an dieser Stelle wird wieder auf die Kreuzvalidierung mit der Genauigkeit als Bewertungskritierium zurückgegriffen, siehe Kapitel 4.
log_clf_cv = cross_val_score(
log_clf, X_train_tr, y_train_01[label], cv=10, scoring="accuracy"
)
outputCVResults(log_clf_cv)
results.append(("Log_reg_cv", 100 * log_clf_cv.mean()))
Das Ergebnis ist im Durchschnitt mit 99,2 % nur etwas besser als mit einem sehr einfachen Entscheidungsbaum mit ca. 98,9 %.
5.4 Random Forest¶
Möglicherweise ist ein komplexeres Modell erforderlich. Aufgrund des Erfolgs des einfachen Entscheidungsbaums wirkt ein Random Forest-Modell vielversprechend. Ein Random Forest besteht aus mehreren - möglichst unkorrelierten - Entscheidungsbäumen und kann sowohl für die Klassifikation als auch Regression eingesetzt werden. Die Klassifikation erfolgt durch einen Mehrheitsentscheid der einzelnen Bäume. Um unkorrelierte Entscheidungsbäume zu erhalten werden diese bspw. auf zufällig aufgewählten Teilmengen der Merkmale trainiert, vgl. [5]. In Scikit-Learn wird dieses Modell durch die Klasse RandomForestRegressor
implementiert.
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(random_state=42)
forest_clf.fit(X_train_tr, y_train_01[label])
Wie zuvor kann nun die Kreuzvalidierung zur Bewertung genutzt werden:
forest_clf_cv = cross_val_score(
forest_clf, X_train_tr, y_train_01[label], cv=10, scoring="accuracy"
)
outputCVResults(forest_clf_cv)
results.append(("Forest_cv", 100 * forest_clf_cv.mean()))
Mit 99,26 % ist der Random Forest nur minimal besser als die Logistische Regression mit 99,20 %.
5.5 Analyse der Fehler¶
Dies ist ein guter Zeitpunkt um die Fehler des Modells genauer zu untersuchen. Dazu sind die "ehrlichen" Schätzungen des Klassifikators erforderlich. Das sind Schätzungen, bei denen der Klassifikator den zu klassifizierenden Datenpunkt noch nicht gesehen hat, also nicht auf diesem trainiert wurde. Auch hier bietet Scikit-Learn mit der Klasse cross_val_predict
eine passende Implementierung, welche auf der Kreuzvalidierung aufbaut:
from sklearn.model_selection import cross_val_predict
y_train_01_pred = cross_val_predict(forest_clf, X_train_tr, y_train_01[label])
Die Fehler können im nächsten Schritt als Konfusionsmatrix dargestellt werden:
from sklearn.metrics import confusion_matrix
print("Konfusionsmatrix")
print(confusion_matrix(y_train_01[label], y_train_01_pred))
Die allermeisten Teile wurden entweder korrekt positiv (1952) oder korrekt negativ (1654) klassifiziert. Allerdings wurden einige fehlerfreie Teile als fehlerhaft klassifiziert (26) und ein paar fehlerhafte Teile als fehlerfrei (6). Um diese fehlerhaften Klassifizierungen zu erklären werden nachfolgend die ursprünglichen Zielwerte (0 - 3) für ein paar dieser Teile dargestellt.
Fehlerfreie Teile die als fehlerhaft klassifiziert wurden:
y_train[
np.logical_and(y_train_01[label] == 0, y_train_01_pred == 1)
].sort_index().head()
Fehlerhafte Teile die als fehlerfrei klassifiziert wurden:
y_train[
np.logical_and(y_train_01[label] == 1, y_train_01_pred == 0)
].sort_index().head()
In der Spalte 0_leak_corner_tr
beider Tabellen ist erkennbar, dass die falsch klassifizierten Teile mit einer Fehlerstärke von 1 bzw. 2 jeweils am Rand der binären Entscheidungsgrenze (zwischen 1 und 2) liegen. Vermutlich handelt es sich bei diesen Teilen um Grenzfälle, welche sowohl der Fehlerstärke 1 als auch 2 hätten zugeordnet werden können. Die Fotos zweier dieser Teile - dargestellt in Abbildung 4 - unterstützen diese Vermutung:
Abbildung 4: Ein falsch positiv (links) und ein falsch negativ (rechts) klassifiziertes Teil
Im Vergleich zu den übrigen Teilen sind beide dargestellten Fehler weder besonders schwach noch besonders stark.
Auf Grundlage dieser Erkenntnis könnte im nächsten Schritt der Grenzwert des Klassifikators so festgelegt werden, dass dessen Sensitivität und Präzision für den vorliegenden Anwendungsfall optimal sind. Da die positive Kategorie bei diesem Datensatz nicht selten ist, bietet sich in diesem Fall die Verwendung einer ROC-Kurve an, um dieses Ziel zu erreichen, vgl. [3].
5.3 Zusammenfassung¶
Die Genauigkeit der beiden trainierten Klassifikatoren ist mit über 99,00 % bei der Kreuzvalidierung ausreichend hoch. Die fehlerhaft klassifizierten Teile liegen vermutlich nahe der binären Entscheidungsgrenze. Diese Fehler sind bei einem binären Klassifikator auf Basis analoger Merkmale zu erwarten. Abschließend werden die Klassifikatoren anhand der Testdaten überprüft:
print("Genauigkeit auf den Testdaten")
# Logistische Regression
log_clf_test = log_clf.score(X_test_tr, y_test_01[label])
print("- Logistische Regression:", "{:.2f}".format(100 * log_clf_test), "%")
results.append(("Log_reg_test", 100 * log_clf_test))
# Random Forest
forest_clf_test = forest_clf.score(X_test_tr, y_test_01[label])
print("- Random Forest:", "{:.2f}".format(100 * forest_clf_test), "%")
results.append(("Forest_test", 100 * forest_clf_test))
Auch die Genauigkeit auf den Testdaten ist mit jeweils über 99,2 % ausreichend hoch und sehr ähnlich zur Genauigkeit bei der Kreuzvalidierung. Von einem Under- oder Overfitting ist deshalb nicht auszugehen. Aufgrund der hohen Genauigkeit und der erklärbaren Fehler wird kein zusätzlicher Aufwand in die Optimierung der Hyperparamter investiert.
Zusammenfassend hat dieses Kapitel gezeigt, dass mit Klassifikatoren auf Basis mehrerer Merkmale sehr gute Ergebnisse erzielt werden können. Der große Nachteil dabei ist, dass dafür ca. 3600 Fotos manuell gelabelt werden mussten. Im nachfolgenden Kapitel wird deshalb untersucht, ob der Ansatz des teilüberwachten Lernens genutzt werden kann, um diesen Aufwand zu minimieren.
6. Teilüberwachtes Lernen¶
Die vorherigen Kapitel haben gezeigt, dass ML-Algorithmen mit Hilfe gelabelter Daten fehlerhafte Teile sehr gut erkennen können. Ziel dieses Kapitel ist, den Aufwand zu minimieren, um einen solchen Algorithmus zu trainieren. Dieser Aufwand besteht hauptsächlich im Labeln der Fotos. Beim Erkunden der Daten hat sich bereits gezeigt, dass diese eine Cluster-Struktur aufweisen. Ein vielversprechender Ansatz zur Minimierung des Label-Aufwands ist deshalb das teilüberwachte Lernen.
Beim teilüberwachten Lernen wird nur ein geringer Teil der Datenpunkte gelabelt. Es wird versucht, dafür repräsentative Datenpunkte zu finden. Dies könnten bspw. die Mittelpunkte der beobachteten Cluster sein. Die Label der repräsentativen Datenpunkte können auch auf andere – z.B. demselben Cluster angehörige – Datenpunkte übertragen werden, bevor ein ML-Algorithmus trainiert wird, vgl. [3]. In diesem Fall soll die Auswahl der repräsentativen Datenpunkte auf Basis der beobachteten Cluster erfolgen. Wie diese erfasst werden beschreibt das nachfolgende Unterkapitel.
6.1 Clustering der Daten¶
6.1.1 Auswahl relevanter Merkmale¶
In einem hochdimensionalen Merkmalsraum sind Datenpunkte relativ weit voneinander entfernt, wodurch deren Clustering erschwert wird, vgl. [3]. Eine einfache Möglichkeit diesen Merkmalsraum zu verkleinern ist das Entfernen von Merkmalen, welche für das untersuchte Problem wenig relevant erscheinen. Dies könnte z.B. auf Grundlage von Fachwissen erfolgen. In diesem Fall eröffnet jedoch der in Kap. 5 trainierte RandomForestClassifier
eine einfachere Möglichkeit. Dieser besitzt das Attribut feature_importances_
, welches Aufschluss über diejenigen Merkmale gibt, welche bei seinen Entscheidungen am wichtigsten sind. Nachfolgend die 10 wichtigsten Merkmale:
feature_importances = forest_clf.feature_importances_
features_df = pd.DataFrame(
feature_importances.T, X_train_tr.columns, ["Feature importance"]
)
features_df["Feature importance"].sort_values(ascending=False).head(10)
Noch aufschlussreicher ist ein Graph, welche die sortierten Wichtigkeiten der Merkmale über deren Indizes darstellt:
plt.figure(figsize=(8, 6))
plt.plot(features_df["Feature importance"].sort_values(ascending=False).to_numpy())
# Horizontale Linie
xticks = range(0, 101, 5)
hline = 0.01
plt.plot(xticks, np.array([hline for tick in xticks]), "r--", label="1 %")
# Formatierung
plt.title("Wichtigkeit der Merkmale im Random Forest", fontsize=16, pad=10)
plt.xlabel("Merkmale")
plt.ylabel("Relative Wichtigkeit [%]")
plt.xlim(0, 100)
plt.ylim(0, 0.15)
plt.xticks(xticks)
plt.legend()
plt.show()
Der Graph zeigt, dass ca. ab dem 15. Merkmal die relative Wichtigkeit unter 1 % sinkt. Deshalb werden nachfolgend nur die ersten 15 Merkmale betrachtet. An dieser Stelle kann selbstverständlich auch domänenspezifisches Wissen berücksichtigt werden.
n_features = 15
X_train_cl = X_train_tr.iloc[:, feature_importances.argsort()[-n_features:]]
X_test_cl = X_test_tr.iloc[:, feature_importances.argsort()[-n_features:]]
# Reihenfolge der Spaltennamen wiederherstellen
X_train_cl = X_train_cl.sort_index(axis=1)
X_test_cl = X_test_cl.sort_index(axis=1)
6.1.2 Dimensionsreduktion¶
Über das Entfernen der unwichtigen Merkmale hinaus kann die Dimension des Merkmalsraums weiter reduziert werden. Dies erleichtert das Clustering und ermöglicht eine visuelle Darstellung der Cluster.
Häufig wird dafür die Hauptkomponentenanalyse (Principal Component Analysis, PCA) eingesetzt. Bei dieser handelt sich um ein mathematisches Verfahren, bei dem der ursprüngliche Merkmalsraum unter Beibehaltung einer möglichst großen Varianz auf einen niedriger dimensionalen Unterraum projiziert wird, vgl. [6]. Die Dimension des Unterraums kann dabei beliebig gewählt werden.
Die Achsen des Unterraums werden als Hauptkomponenten bezeichnet und ergeben sich aus Linearkombinationen der ursprünglchen Merkmale. Beim Erkunden der Daten in Kap. 3.5 konnten starke lineare Korrelationen im Datensatz beobachtet werden. Aus diesem Grund wird nachfolgend die PCA zur Dimensionreduktion genutzt. Auch für dieses Verfahren liefert SciKit-Learn mit der Klasse PCA
eine Implementierung. Der nach nachfolgende Programmcode reduziert die Dimension der 15 wichtigsten Merkmale auf 3:
from sklearn.decomposition import PCA
pca_3D = PCA(n_components=3)
X3D_train = pca_3D.fit_transform(X_train_cl)
X3D_test = pca_3D.fit_transform(X_test_cl)
Die erklärte Varianz der reduzierten Daten beträgt:
print(
"Anteil erklärter Varianz (3D):",
"{:.2f}".format(100 * pca_3D.explained_variance_ratio_.sum()),
"%",
)
Bei der Reduktion von 15 auf 3 Dimensionen sind folglich nur ca. 5 % der Varianz verloren gegangen. Deshalb wird nachfolgend eine Reduktion auf 2 Dimensionen ausprobiert:
pca_2D = PCA(n_components=2)
X2D_train = pca_2D.fit_transform(X_train_cl)
X2D_test = pca_2D.fit_transform(X_test_cl)
Die erklärte Varianz beträgt in diesem Fall:
print(
"Anteil erklärter Varianz (2D):",
"{:.2f}".format(100 * pca_2D.explained_variance_ratio_.sum()),
"%",
)
Auch wenn in 2 Dimensionen mit ca. 13,5 % deutlich mehr Informationen verloren gegangen sind ist der zweidimensionale Datensatz für das Clustering potenziell noch sehr gut geeignet. Die nachfolgenden Visualisierungen sollen bei der Auswahl der Anzahl an Dimensionen unterstützen. Zunächst wird der dreidimensionale Datensatz dargestellt.
from mpl_toolkits.mplot3d import Axes3D
def plotX3D(X3D, color, anomalies=None):
# X3D bei Bedarf in np.ndarray umwandeln
if isinstance(X3D, pd.DataFrame):
X3D = X3D.to_numpy()
# Initialisierung
fig = plt.figure(figsize=(14, 11))
ax = fig.add_subplot(111, projection="3d")
# Über alle Fehlerausprägungen (0 - 3) iterieren
colors = {0: "green", 1: "yellow", 2: "orange", 3: "red"}
labels = {
0: "kein Fehler",
1: "schwacher Fehler",
2: "mittlerer Fehler",
3: "starker Fehler",
}
for c in np.sort(np.unique(color)):
ax.plot(
X3D_train[color == c, 0],
X3D_train[color == c, 1],
X3D_train[color == c, 2],
".",
label=labels[c],
c=colors[c],
alpha=0.2,
)
# Formatierung
ax.legend()
ax.set_xlabel("Hauptkomponente $x_1$", fontsize=14, labelpad=10)
ax.set_ylabel("Hauptkomponente $x_2$", fontsize=14, labelpad=10)
ax.set_zlabel("Hauptkomponente $x_3$", fontsize=14, labelpad=10)
ax.set_zlim(-6, 6)
ax.view_init(50, 145)
# Optional können Anomalien dargestellt werden
if isinstance(anomalies, np.ndarray):
ax.scatter(
anomalies[:, 0], anomalies[:, 1], anomalies[:, 2], marker="x", s=80, c="red"
)
plotX3D(X3D_train, y_train[label])
plt.show()
Es sind eindeutig Cluster zu erkennen. Sowohl für die Gut- als auch Schlechtteile existieren mehrere Cluster. Die Cluster der Schlechtteile (rot) sind deutlich weniger kompakt und länglicher gezogen als die Cluster der Gutteile (grün). Um zu überprüfen ob diese Informationen auch im zweidimensionalen Datensatz erhalten geblieben sind wird dieser als nächstes dargestellt.
def plotX2D(X2D, color, anomalies=None):
# X2D bei Bedarf in np.ndarray umwandeln
if isinstance(X2D, pd.DataFrame):
X2D = X2D.to_numpy()
# Initialisierung
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111)
# Über alle Fehlerausprägungen (0 - 3) iterieren
colors = {0: "green", 1: "yellow", 2: "orange", 3: "red"}
labels = {
0: "kein Fehler",
1: "schwacher Fehler",
2: "mittlerer Fehler",
3: "starker Fehler",
}
for c in np.sort(np.unique(color)):
ax.plot(
X2D[color == c, 0],
X2D[color == c, 1],
".",
label=labels[c],
c=colors[c],
alpha=0.2,
)
# Formatierung
ax.legend()
ax.set_xlabel("Hauptkomponente $x_1$", fontsize=14, labelpad=10)
ax.set_ylabel("Hauptkomponente $x_2$", fontsize=14, labelpad=10)
# Optional können Anomalien dargestellt werden
if isinstance(anomalies, np.ndarray):
ax.scatter(anomalies[:, 0], anomalies[:, 1], marker="x", s=80, c="red")
plotX2D(X2D_train, y_train[label])
plt.show()
Aus dieser Darstellung lassen sich sehr ähnliche Erkenntnisse ableiten wie aus der dreidimensionalen. Für das teilüberwachte Lernen wird aus Gründen der Einfachheit der zweidimensionale Datensatz verwendet.
6.1.3 Durchführung des Clustering¶
Auf Basis des zweidimensionalen Datensatz X2D_train
kann im nächsten Schritt das Clustering durchgeführt werden. Die Darstellungen im vorherigen Unterkapitel haben gezeigt, dass die Cluster typischerweise elliptisch sind und in ihrer Dichte stark variieren können. Der Algorithmus K-Means kommt deshalb nicht in Frage, vgl. [3]. Stattdessen wird ein Gaußsches Mischverteilungsmodell (Gaussian Mixture Model, GMM) genutzt, welches diese Art von Clustern sehr gut erzeugen kann. Das Modell versucht, die Datenpunkte im Datensatz durch eine Mischung verschiedener gaußscher Verteilungen zu erzeugen, vgl. [7] Die entsprechende Klasse SciKit-Learn heißt GaussianMixture
:
from sklearn.mixture import GaussianMixture
# n_components = 7 auf Grundlage der Darstellungen im vorherigen Unterkapitel
gaus_mix = GaussianMixture(n_components=7, random_state=42).fit(X2D_train)
y_pred_cluster = gaus_mix.predict(X2D_train)
Es ist hilfreich, diese Cluster darstellen zu können.
def plotX2DClustered(X2D, cluster, anomalies=None, cluster_centers=[]):
# X2D bei Bedarf in np.ndarray umwandeln
if isinstance(X2D, pd.DataFrame):
X2D = X2D.to_numpy()
# Initialisierung
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111)
# Über alle Cluster iterieren
for c in np.sort(np.unique(cluster)):
ax.plot(X2D[cluster == c, 0], X2D[cluster == c, 1], ".", label=c, alpha=0.2)
# Formatierung
ax.legend()
ax.set_xlabel("Hauptkomponente $x_1$", fontsize=14, labelpad=10)
ax.set_ylabel("Hauptkomponente $x_2$", fontsize=14, labelpad=10)
# Optional können Anomalien dargestellt werden
if isinstance(anomalies, np.ndarray):
ax.scatter(anomalies[:, 0], anomalies[:, 1], marker="x", s=80, c="red")
# Optional können die Zentren der Cluster dargestellt werden
if len(cluster_centers) > 0:
ax.plot(
cluster_centers[:, 0],
cluster_centers[:, 1],
"o",
c="red",
markersize=8,
markeredgecolor="black",
)
plotX2DClustered(X2D_train, y_pred_cluster, cluster_centers=gaus_mix.means_)
Der Algorithmus hat die Cluster sehr gut erkannt. Die jeweiligen Mittelwerte werden als roter Punkt dargestellt. Das Gaußsche Mischverteilungsmodell wird nachfolgend die Grundlage für das teilüberwachte Lernen bilden.
6.2 Durchführung des teilüberwachten Lernens¶
Auf Basis des Clustering kann nun der Einfluss des teilüberwachten Lernens auf die Genauigkeit eines Klassifikators ermittelt werden. Stellvertretend wird nachfolgend der Klassifikator LogisticRegression
eingesetzt, welcher in Kap. 5.3 bereits eine Genauigkeit von ca. 99,0 % auf dem gesamten Datensatz erreicht hat.
6.2.1 Referenz¶
Für den späteren Vergleich ist zunächst eine Referenzgenauigkeit mit allen Labeln zu ermitteln:
from sklearn.linear_model import LogisticRegression
log_clf_full = LogisticRegression(random_state=42)
log_clf_full.fit(X2D_train, y_train_01[label])
Um noch nicht auf die Testdaten zurückgreifen zu müssen wird zur Bewertung eine Kreuzvalidierung durchgeführt:
from sklearn.model_selection import cross_val_score
log_full_cv = cross_val_score(
log_clf_full, X2D_train, y_train_01[label], cv=10, scoring="accuracy"
)
def outputResults(scores):
print("Genauigkeit bei der Kreuzvalidierung")
print("- Mittelwert:", "{:.2f}".format(100 * scores.mean()), "%")
print("- Standardabw.:", "{:.2f}".format(100 * scores.std()), "%")
print("- 10. Perzentil:", "{:.2f}".format(100 * np.percentile(scores, 10), "%"))
outputResults(log_full_cv)
results.append(("Log_reg_full_cv", 100 * log_full_cv.mean()))
Interessanterweise hat dieser Klassifikator mit ca. 98,8 % auf dem zweidimensionalen Datensatz eine quasi identische Genauigkeit wie auf dem gesamten Datensatz mit über 100 Merkmalen.
Eine weitere Referenz ist die Genauigkeit mit rein zufällig ausgewählten Datenpunkten. Um diese zu ermitteln werden nachfolgend mehrfach Datenpunkte zufällig ausgewählt, das Modell mit ihnen trainiert und abschließend anhand der gesamten Trainingsdaten bewertet.
n_labeled = 10
def randomSemisupervisedLearning(X, y, n_labeled, runs=1000):
scores = []
# Initialisierung des Klassifikators
clf = LogisticRegression(random_state=42)
for i in range(runs):
# Auswahl zufälliger Datenpunkte
idx_rnd = pd.DataFrame(X).sample(n=n_labeled, random_state=i).index
# Kontrolle ob sowohl Gut- als auch Schlechtteile ausgewählt wurden
if y[idx_rnd].nunique() > 1:
# Trainieren des Modells
clf.fit(pd.DataFrame(X).iloc[idx_rnd], y.iloc[idx_rnd])
# Genauigkeit des Modells abspeichern
scores.append(clf.score(pd.DataFrame(X), y))
outputResults(np.array(scores))
return np.array(scores)
scores = randomSemisupervisedLearning(X2D_train, y_train_01[label], n_labeled=n_labeled)
results.append(("Log_reg_rnd", 100 * scores.mean()))
Auch mit nur 10 zufällig ausgewählten Datenpunkten erreicht der Klassifikator im Durchschnitt eine Genauigkeit von immerhin 97,4 %. Allerdings liegen 10 % der Klassifikatoren mit ihrer Genauigkeit unter 96,0 %. Folglich ist das Ziel des nachfolgenden Kapitels, mit 10 repräsentativ ausgewählten Datenpunkten eine Genauigkeit von min. 97,4 % und im Idealfall 99,0 % zu erreichen.
6.2.2 Auswahl durch Clustering¶
Nun kann das teilüberwachte Lernen durchgeführt werden. Zunächst werden durch den Clustering-Algorithmus die Cluster und deren Mittelpunkte bestimmt:
from sklearn.mixture import GaussianMixture
# Genauso viele Cluster wie Datenpunkte gelabelt werden sollen
gaus_mix = GaussianMixture(n_components=n_labeled, random_state=42)
gaus_mix.fit(X2D_train)
y_pred_cluster = gaus_mix.predict(X2D_train)
Als nächstes werden die Datenpunkte ausgewählt, welche den Mittelpunkten der Cluster am nächsten liegen:
from sklearn.metrics import pairwise_distances_argmin_min
closest, _ = pairwise_distances_argmin_min(gaus_mix.means_, X2D_train)
X2D_train_repr = X2D_train[closest]
Das Ergebnis stellt die nachfolgende Abbildung dar. Die gefundenen Datenpunkte sind als rote Punkte eingezeichnet:
plotX2DClustered(X2D_train, y_pred_cluster, cluster_centers=X2D_train_repr)
plt.show()
Im nächsten Schritt kann der Klassifikator mit diesen Datenpunkten trainiert und ausgewertet werden.
log_clf_repr = LogisticRegression(random_state=42)
log_clf_repr.fit(X2D_train[closest], y_train_01[label].iloc[closest])
print("Genauigkeit nach Auswahl durch Clustering")
print("-", "{:.2f}".format(100 * log_clf_repr.score(X2D_train, y_train_01[label])), "%")
results.append(("Log_reg_repr", 100 * log_clf_repr.score(X2D_train, y_train_01[label])))
Das Ergebnis ist mit 98,5 % deutlich besser als bei der rein zufälligen Auswahl mit 97,4 %, erreicht jedoch nicht die 99,0 %. Ein Propagieren der repräsentativen Label auf die anderen Datenpunkte im Cluster könnte eine weitere Verbesserung bringen:
y_train_01_prop = np.empty(len(y_train_01), dtype=np.int32)
for c in range(n_labeled):
y_train_01_prop[y_pred_cluster == c] = y_train_01[label].iloc[closest][c]
Die aus den 10 repräsentativen Datenpunkten für den gesamten Datensatz abgeleiteten Label zeigt die nachfolgende Abbildung:
plotX2DClustered(X2D_train, y_train_01_prop, cluster_centers=X2D_train_repr)
Nun kann erneut ein Klassifikator trainiert und ausgewertet werden:
log_clf_prop = LogisticRegression(random_state=42)
log_clf_prop.fit(X2D_train, y_train_01_prop)
print("Genauigkeit nach Auswahl durch Clustering und Propagieren")
print("-", "{:.2f}".format(100 * log_clf_prop.score(X2D_train, y_train_01[label])), "%")
results.append(("Log_reg_prop", 100 * log_clf_prop.score(X2D_train, y_train_01[label])))
Das Propagieren hat eine geringfügige Verbesserung auf 98,65 % gebracht, was nun beinahe der Referenz von 98,82 % entspricht. Abschließend wird dieses Ergebnis anhand der Testdaten überprüft:
print("Genauigkeit auf den Testdaten")
print(
"- Überwachtes Lernen:",
"{:.2f}".format(100 * log_clf_full.score(X2D_test, y_test_01[label])),
"%",
)
print(
"- Teilüberwachtes Lernen:",
"{:.2f}".format(100 * log_clf_prop.score(X2D_test, y_test_01[label])),
"%",
)
results.append(
("Log_reg_prop_test", 100 * log_clf_prop.score(X2D_test, y_test_01[label]))
)
Die Genauigkeit auf den Testdaten ist beim überwachten und teilüberwachten ähnlich hoch und mit jeweils über 98,5 % ausreichend. Grundsätzlich stellt das teilüberwachte Lernen somit einen vielversprechenden Ansatz für Aufgaben dieser Art dar, denn in diesem Fall musste nur ca. 0,3 % (10 / 3600) des ursprünglichen Aufwands für das Labeln investiert werden.
6.2.3 Analyse der Fehler¶
Abschließend werden die Teile analysiert, welche beim Propagieren innerhalb der Cluster ein falsches Label erhalten haben:
false_pos = np.logical_and(y_train_01[label] == 0, y_train_01_prop == 1)
false_neg = np.logical_and(y_train_01[label] == 1, y_train_01_prop == 0)
plotX2D(X2D_train, y_train[label])
# Falsch positiv bzw. negativ klassifizierte Datenpunkte darstellen
plt.plot(
X2D_train[false_pos][:, 0],
X2D_train[false_pos][:, 1],
"o",
c="red",
markeredgecolor="black",
label="Falsch positiv",
)
plt.plot(
X2D_train[false_neg][:, 0],
X2D_train[false_neg][:, 1],
"o",
c="green",
markeredgecolor="black",
label="Falsch negativ",
)
plt.legend()
plt.show()
In der Abbildung sind als schwarz umrandete Punkte diejenigen Teile markiert, welche beim propagieren der repräsentativen Label ein falsches Label erhalten haben. Falsch positive Teile sind dabei rot, falsch negative Teile grün ausgefüllt. Grundsätzliche liegen diese Teile wie erwartet im Grenzbereich zwischen Gut- und Schlechtteilen. Auf den ersten Blick wirkt die dargestellte Klassifizierung nachvollziehbar. Bspw. erscheint es sinnvoll, dass das große Cluster der falsch positiven Teile (untere Hälfte der Abbildung) scheinbar dem rechten unteren Cluster an Fehlteilen zugeordnet wurde. Interessant ist das originale Label (0 - 3) dieser falsch klassifizierten Teile:
print("Ursprüngliches Label der falsch positiven Klassifizierungen")
print(y_train[label][false_pos].value_counts())
Die meisten dieser Teile hatten einen leichten Fehler (Stufe 1). Es ist nachvollziehbar, dass diese ähnlich wie die anderen Fehlteile (Stufe 2 und 3) eingeschätzt wurden. Teile mit einem leichten Fehler unterscheiden sich grundsätzlich von den vollständig fehlerfreien Teilen. Dies deuten auch die Cluster in der Abbildung an. Nachfolgend wird der Klassifikator aus dem teilüberwachten nochmal ausgewertet, diesmal werden jedoch auch leichte Fehler der positiven Kategorie zugeordnet:
y_train_01_alt = y_train.copy()
y_train_01_alt.replace(1, 1, inplace=True)
y_train_01_alt.replace(2, 1, inplace=True)
y_train_01_alt.replace(3, 1, inplace=True)
print("Genauigkeit mit alternativem Label:")
print(
"-",
"{:.2f}".format(100 * log_clf_prop.score(X2D_train, y_train_01_alt[label])),
"%",
)
results.append(
("Log_reg_prop_1-3", 100 * log_clf_prop.score(X2D_train, y_train_01_alt[label]))
)
Auf den Testdaten ist das Ergebnis ebenfalls besser:
y_test_01_alt = y_test.copy()
y_test_01_alt.replace(1, 1, inplace=True)
y_test_01_alt.replace(2, 1, inplace=True)
y_test_01_alt.replace(3, 1, inplace=True)
print("Genauigkeit mit alternativem Label:")
print(
"-", "{:.2f}".format(100 * log_clf_prop.score(X2D_test, y_test_01_alt[label])), "%"
)
results.append(
("Log_reg_prop_1-3_test", 100 * log_clf_prop.score(X2D_test, y_test_01_alt[label]))
)
Je nach Problemstellung sollte somit gut abgewogen werden, ob bei einer binären Klassifikation Teile mit einem leichten Fehler als fehlerfrei oder fehlerhaft eingestuft werden.
7. Ergebnisse und Evaluation¶
Abschließend können die Ergebnisse zusammengefasst und kritisch bewertet werden. Dazu werden nachfolgend die Genauigkeiten der im Rahmen dieser Arbeit trainierten Klassifikatoren dargestellt:
# Für die Darstellung in DataFrame umwandeln
results_data = {
"Klassifikator": np.array(results)[:, 0],
"Genauigkeit [%]": np.array(results)[:, 1].astype(float),
}
results_df = pd.DataFrame(results_data).set_index("Klassifikator")
# Spalte für das Ergebnis auf den Testdaten hinzufügen
results_test = results_df[results_df.index.str.contains("_test")]
results_test.index = results_test.index.str.replace("_test", "")
results_test.columns = ["Test [%]"]
# Spalte für das Ergebnis auf den Trainingsdaten anpassen
results_training = results_df[np.invert(results_df.index.str.contains("_test"))]
results_training.index = results_training.index.str.replace("_cv", "")
results_training.columns = ["Training [%]"]
# Spalten kombinieren
results_df = pd.concat([results_training, results_test], axis=1)
results_df = results_df.reset_index()
results_df = results_df.rename(columns={"index": "Klassifikator"})
# Formatierung
style = results_df.style
style = style.format({"Test [%]": "{:.1f}"})
style = style.format({"Training [%]": "{:.1f}"})
style = style.set_properties(**{"text-align": "left"})
style = style.set_properties(**{"text-align": "right"}, subset=["Test [%]"])
style = style.set_properties(**{"text-align": "right"}, subset=["Training [%]"])
style = style.background_gradient(cmap="plasma", subset=["Training [%]"])
style = style.hide_index()
style
Die Genauigkeit auf den Trainingsdaten wurde mittels Kreuzvalidierung bestimmt und ist deshalb stets ähnlich hoch wie die Genauigkeit auf den Testdaten. Grundsätzlich erreichen alle Klassifikatoren mit über 97,0 % eine relativ hohe Genauigkeit. Die untersuchten Fehler lassen sich sehr gut durch ML-Algorithmen erkennen.
Bereits ein Entscheidungsbaum (Tree
, Kap. 4) erreicht mit einer einzigen Aufteilung eine Genauigkeit von 98,9 % auf den Testdaten. Mit Hilfe logistischer Regression (Log_reg
, Kap. 5.3) bzw. eines Random Forests (Forest
, Kap. 5.4) lässt sich diese Genauigkeit auf über 99,2 % steigern. Problematisch waren dabei vor allen diejenigen Teile, welche sich am Rand der binären Entscheidungsgrenze zwichen Gut- und Schlechtteil befanden.
In Kap. 6.1 hat sich gezeigt, dass sich die Dimension des Merkmalsraums für diesen Datensatz unter Beibehaltung eines Großteils seiner Varianz auf zwei oder drei reduzieren lässt. Auch in diesem reduzierten Merkmalsraum erzielte die logistische Regression (Log_reg_full
) noch eine Genauigkeit von 98,8 %. Mit Hilfe eines Gaußschen Mischverteilungsmodells ließen sich anschließend die zuvor beobachteten Cluster gut erkennen und für das teilüberwachte Lernen nutzen.
Das Kapitel 6.2 zum teilüberwachten Lernen hat gezeigt, dass es für diese Art von Daten nicht notwendig ist, sämtliche Datenpunkte zu labeln. So erreicht eine logistische Regression, welche auf 10 zufällig ausgewählten Datenpunkten trainiert wurde (Log_reg_rnd
) im Mittel eine Genauigkeit von 97,4 %. Werden auf Basis der erkannten Cluster 10 repräsentative Datenpunkte gelabelt (Log_reg_repr
), steigt diese Genauigkeit auf 98,2 %. Ein Propagieren der Label auf alle weiteren Datenpunkte im jeweiligen Cluster (Log_reg_prop
) erhöhte die Genauigkeit weiter auf 98,6 %. Dieses Ergebnis liegt nur noch minimal unter den Ergebnissen der auf dem gesamten Datensatz trainierten Klassifikatoren.
Zusammenfassend kann festgehalten werden, dass sich die untersuchten Fehler durch teilüberwachtes Lernen mit einem sehr geringen Aufwand und einer dennoch sehr hohen Genauigkeit von über 98,5 % erkennen lassen. Dafür waren relativ einfache lineare Machine-Learning-Modelle ausreichend.
Die Untersuchung hat jedoch auch gezeigt, dass sowohl mehrere Cluster mit Gut- als auch mit Schlechtteilen entstehen können. Es ist ungewiss, ob auch komplett neue Cluster den Regeln folgen, welche die im Rahmen dieser Arbeit trainierten Algorithmen gelernet haben. Allerdings stellt auch bei diesem Problem das teilüberwachte Lernen einen vielversprechenden Lösungsansatz dar.
Ungewiss ist ebenfalls, wie gut sich diese Vorgehensweise auf andere Fehler übertragen lässt. Die untersuchten Fehler traten relativ häufig und in Clustern auf. Seltene Fehler, welche darüber hinaus sehr stark verstreut sind, würden vom Clustering und damit vom teilüberwachten Lernen nicht möglicherweise nicht erfasst. Ein Beispiel ist der Fehler 1_hole_bottom
im Datensatz, welcher nur ein paar Mal aufgetreten ist.
Eine Herausforderung für den beschriebenen Lösungsansatz wäre es außerdem, wenn ein neuer Datensatz sich nicht auf relativ wenige Dimensionen reduzieren lässt, ohne einen zu großen Anteil seiner Varianz zu verlieren. Die resultierende geringe Dichte an Datenpunkten im Merkmalsraum könnte das Clustering erschweren. Zu demselben Problem würde eine zu geringe Anzahl an Datenpunkten führen. Dies ist im Spritzguss jedoch in der Regel kein Problem.
8. Ausblick¶
Grundsätzlich bildet diese Arbeit die Grundlage für eine Vielzahl möglicher Weiterentwicklungen. Mit einem System, welches die Prozessdaten regelmäßig auf neue Cluster untersucht und Fotos repräsentativer Teile zur Beurteilung an das Fachpersonal schickt, könnte der in dieser Arbeit beschriebenen Lösungsansatz in die Praxis überführt werden. Dabei könnten auch Aspekte des aktiven Lernens berücksichtigt werden. Im Zuge dessen könnte das System ebenfalls auf weitere Fehlertypen und Produkte übertragen werden. Es sind jedoch die in der kritischen Auseinandersetzung beschriebenen Aspekte zu berücksichtigen.
In der kritischen Auseinandersetzung wurde ebenfalls das Problem angesprochen, dass andere Fehlertypen selten und weit verstreut sein könnten. Um auch diese zu erkennen könnte die erarbeitete Lösung um eine Anomalieerkennung ergänzt werden. Das bereits implementierte Clustering mittels Gaußschem Mischverteilungsmodell bietet durch die bereitgestellten Wahrscheinlichkeitsdichtefunktionen eine sehr gute Grundlage dafür.
Des Weiteren könnten Zeitreihenanalysen auf dem vorliegenden Datensatz durchgeführt werden, um den Ursprung der Fehler zu finden und diese gar nicht erst entstehen zu lassen.
Literaturverzeichnis¶
[1] L. Schauerte, Vorstudie zum Potenzial des Data Mining im Spritzguss zur Verringerung von Produktionsfehlern, 2021
[2] Gustav Hensel GmbH & Co. KG, Available: https://www.hensel-electric.de, 2021
[3] Aurélien Géron, Praxiseinstieg Machine Learning mit Scikit-Learn und TensorFlow, 2018, O'Reilly Verlag
[4] L. Breiman, J. H. Friedman, R. A. Olshen, C. J. Stone: CART: Classification and Regression Trees, 1984.
[5] L. Breiman, Random forests. In: Machine Learning, 2001, Seite 5–32
[6] G. H. Dunteman: Principal Component Analysis, 1989, Sage Publications
[7] Ch. Fraley, A. Raftery, Normal Mixture Modeling and Model-Based Clustering, 2015