Eine große Herausforderung im Bereich Machine-Learning stellt der Umgang mit unausgewogenen (engl. unbalanced) Datensätzen bei der Klassifizierung dar. Dabei bezeichnet man einen Datensatz als unausgewogen, wenn eine Klasse des Datensatzes gegenüber den anderen deutlich über-/unterrepräsentiert ist.
In diesem Zusammenhang stellt Betrugsentdeckung (engl. Fraud Detection) ein besonderes Beispiel dar: Ein großer Teil der Daten des Datensatzes, meist mehr als 90%, genannt Mehrheitsklasse, repräsentiert ein „normales“ Verhalten und lediglich ein sehr kleiner Teil des Datensatzes, genannt Minderheitsklasse, sollte als „Betrug“ klassifiziert werden. In diesem Szenario kann ein Model eine sehr gute Genauigkeit erzielen, wenn schlicht jeder Datensatz als „normal“ klassifiziert wird, die Genauigkeit entspricht dann gerade dem Anteil der tatsächlich „normalen“ Datensätze im Datensatz. Auch wenn dieses Modell auf den ersten Blick durch seine hohe Genauigkeit zu überzeugen scheint, ist es doch völlig nutzlos, da es dem Anwender keinerlei Zusatzinformation liefert. Da die Genauigkeit in diesem Fall also ein wenig hilfreiches Maß für die Güte eines Klassifizierers darstellt, werden andere Metriken zur Bewertung herangezogen, wie auch im saracus-Blog zu lesen ist.
Die Verwendung einer anderen Metrik zur Bewertung der Güte eines Klassifizierers ist allerdings nur der erste Schritt, denn ist eine solche, den entsprechenden Anforderungen genügende, Metrik bestimmt, muss dafür gesorgt werden, dass die Machine-Learning Algorithmen auch bezüglich der Metrik gute Ergebnisse liefern. Viele der Machine-Learning Algorithmen haben jedoch Schwierigkeiten mit der Verarbeitung unausgewogener Datensätze und erzielen nur ungenügende Resultate. Um diesem Problem zu begegnen, gibt es etwaige Varianten, um den Datensatz zu balancieren.
Bei der Verwendung von Python bietet dabei das imbalanced-learn Paket diverse Funktionalitäten, welche die Balancierung des Datensatzes ausführen. In diesem Blogeintrag soll daher ein kurzer Überblick über das Paket und seine Methoden gegeben werden.
Die Voraussetzungen für die Verwendung des Paketes sind gering. Es erfordert in der derzeitigen Version (0.4.3) eine Installation der Pakete numpy (>= 1.8.2), scipy (>=0.13.3) und scikit-learn (>=0.20). Für die Installation des imbalanced-learn Pakets kann auf pip oder anaconda zurückgegriffen werden.
Das Paket wartet mit diversen Funktionalitäten auf, welche in vier Kategorien unterteilt werden können:
- Over-Sampling der Minderheitsklasse
- Under-Sampling der Mehrheitsklasse(n)
- Kombination von Under- und Over-Sampling
- Ensemble-Klassifizierung
Der Fokus dieses Artikels soll auf den Funktionalitäten der ersten drei Kategorien liegen.
Over-Sampling der Minderheitsklasse
Es stehen fünf Methoden zur Auswahl:
- Random minority over-sampling with replacement
- SMOTE – Synthetic Minority Over-sampling Technique
- bSMOTE (1&2) – Borderline SMOTE of types 1 and 2
- SVM SMOTE – Support Vectors SMOTE
- ADASYN – Adaptive Synthetic Sampling approach for imbalanced learning
Es wird ein unbalancierter Testdatensatz mit 2 Features generiert, um verschiedene Over-Sampling Methoden aus dem imbalanced-learn Paket zu veranschaulichen. Dabei kommt exemplarisch ein Linearer SVM Klassifizierer zum Einsatz, um die Unterschiede der Over-Sampling Methoden aufzuzeigen.
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_classification
# erstelle unbalancierten Testdatensatz mit 2 Features
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
n_redundant=0, n_repeated=0, n_classes=3,
n_clusters_per_class=1,
weights=[0.01, 0.05, 0.94],
class_sep=0.8, random_state=0)
# Hilfsfunktionen zur Darstellung der Ergebnisse als Plot
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
return xx, yy
def plot_contours(ax, clf, xx, yy, **params):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
# wende lineare SVM auf den Datensatz an
model = svm.SVC(kernel='linear')
clf = model.fit(X, y)
# stelle Resultate grafisch dar
fig, ax = plt.subplots()
X0, X1 = X[:, 0], X[:, 1]
xx, yy = make_meshgrid(X0, X1)
plot_contours(ax, clf, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
ax.scatter(X0, X1, c=y, cmap=plt.cm.coolwarm, s=20, edgecolors='k')
ax.set_ylabel('Feature 1')
ax.set_xlabel('Feature 2')
ax.set_xticks(())
ax.set_yticks(())
ax.set_title('Linear SVC with Raw Data')
ax.legend()
plt.show()
Um den Datensatz zu balancieren verwenden wir exemplarisch den Random Over Sampler, SMOTE und ADASYN.
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_classification
from imblearn.over_sampling import RandomOverSampler
# erstelle unbalancierten Testdatensatz mit 2 Features
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
n_redundant=0, n_repeated=0, n_classes=3,
n_clusters_per_class=1,
weights=[0.01, 0.05, 0.94],
class_sep=0.8, random_state=0)
# verwende den RandomOverSampler aus dem imblearn Paket, um den Datensatz zu balancieren
ros = RandomOverSampler(random_state=0)
X_resampled, y_resampled = ros.fit_resample(X, y)
# Hilfsfunktionen zur Darstellung der Ergebnisse als Plot
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
return xx, yy
def plot_contours(ax, clf, xx, yy, **params):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
# wende lineare SVM auf den Datensatz an
model = svm.SVC(kernel='linear')
clf = model.fit(X_resampled, y_resampled)
# stelle Resultate grafisch dar
fig, ax = plt.subplots()
X0, X1 = X_resampled[:, 0], X_resampled[:, 1]
xx, yy = make_meshgrid(X0, X1)
plot_contours(ax, clf, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
ax.scatter(X0, X1, c=y_resampled, cmap=plt.cm.coolwarm, s=20, edgecolors='k')
ax.set_ylabel('Feature 1')
ax.set_xlabel('Feature 2')
ax.set_xticks(())
ax.set_yticks(())
ax.set_title('Linear SVC with RandomOverSampler')
ax.legend()
plt.show()
Die Wahl der zu verwendenden Methode hat einen offensichtlichen Einfluss auf das Ergebnis der Klassifizierung seitens der SVM. Man beachte zudem, dass die Verwendung der Over-Sampling Methoden unkompliziert ist und sich zwischen den einzelnen Methoden nicht unterscheidet, was die Anwendbarkeit erhöht.
Under-Sampling der Mehrheitsklasse(n)
Es stehen elf Methoden zur Auswahl:
- Random majority under-sampling with replacement
- Extraction of majority-minority Tomek links
- Under-sampling with Cluster Centroids
- NearMiss-(1 & 2 & 3)
- Condensed Nearest Neighbour
- One-Sided Selection
- Neighboorhood Cleaning Rule
- Edited Nearest Neighbours
- Instance Hardness Threshold
- Repeated Edited Nearest Neighbours
- AllKNN
Erneut wird ein unausgewogener Testdatensatz mit 2 Features betrachtet, um eine Auswahl der Under-Sampling Methoden zu präsentieren. Wieder kommt ein Linearer SVM Klassifizierer zum Einsatz, um die Unterschiede der jeweiligen Under-Sampling Methoden darzustellen.
Zur Balancierung des Testdatensatzes verwenden wir exemplarisch Condensed Nearest Neighbour, Instance Hardness Threshold und Random Under Sampler.
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_classification
from imblearn.under_sampling import InstanceHardnessThreshold
from sklearn.linear_model import LogisticRegression
# erstelle unbalancierten Testdatensatz mit 2 Features
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
n_redundant=0, n_repeated=0, n_classes=3,
n_clusters_per_class=1,
weights=[0.01, 0.05, 0.94],
class_sep=0.8, random_state=0)
# verwende InstanceHardnessThreshold mit Logistischer Regression
# aus dem imblearn Paket, um den Datensatz zu balancieren
iht = InstanceHardnessThreshold(estimator=LogisticRegression(solver='lbfgs', multi_class='auto'))
X_resampled, y_resampled = iht.fit_resample(X, y)
# Hilfsfunktionen zur Darstellung der Ergebnisse als Plot
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
return xx, yy
def plot_contours(ax, clf, xx, yy, **params):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
# wende lineare SVM auf den Datensatz an
model = svm.SVC(kernel='linear')
clf = model.fit(X_resampled, y_resampled)
# stelle Resultate grafisch dar
fig, ax = plt.subplots()
X0, X1 = X_resampled[:, 0], X_resampled[:, 1]
xx, yy = make_meshgrid(X0, X1)
plot_contours(ax, clf, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
ax.scatter(X0, X1, c=y_resampled, cmap=plt.cm.coolwarm, s=20, edgecolors='k')
ax.set_ylabel('Feature 1')
ax.set_xlabel('Feature 2')
ax.set_xticks(())
ax.set_yticks(())
ax.set_title('Linear SVC with InstanceHardnessThreshold')
ax.legend()
plt.show()
Erneut ist erkennbar, dass die Wahl der Under-Sampling Methode einen signifikanten Einfluss auf das Endergebnis hat. Darüber hinaus fällt auf, dass dieses Mal auch weitere Parameter an die Methode InstanceHardnessThreshold übergeben wurden. Dazu später mehr.
Kombination von Under- und Over-Sampling
Das imbalanced-learn Paket bietet zwei Kombinationen von Under- und Over-Sampling Methoden an:
- SMOTE + Tomek links
- SMOTE + Edited Nearest Neighbours
Der bekannte Testdatensatz mit 2 Features wird verwendet, um die Unterschiede der beiden Methoden zu veranschaulichen. Erneut kommt exemplarisch der Linerae SVM Klassifizierer zum Einsatz.
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_classification
from imblearn.combine import SMOTETomek
# erstelle unbalancierten Testdatensatz mit 2 Features
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
n_redundant=0, n_repeated=0, n_classes=3,
n_clusters_per_class=1,
weights=[0.01, 0.05, 0.94],
class_sep=0.8, random_state=0)
# verwende den SMOTETomek aus dem imblearn Paket, um den Datensatz zu balancieren
smotetomek = SMOTETomek()
X_resampled, y_resampled = smotetomek.fit_resample(X, y)
# Hilfsfunktionen zur Darstellung der Ergebnisse als Plot
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
return xx, yy
def plot_contours(ax, clf, xx, yy, **params):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
# wende lineare SVM auf den Datensatz an
model = svm.SVC(kernel='linear')
clf = model.fit(X_resampled, y_resampled)
# stelle Resultate grafisch dar
fig, ax = plt.subplots()
X0, X1 = X_resampled[:, 0], X_resampled[:, 1]
xx, yy = make_meshgrid(X0, X1)
plot_contours(ax, clf, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
ax.scatter(X0, X1, c=y_resampled, cmap=plt.cm.coolwarm, s=20, edgecolors='k')
ax.set_ylabel('Feature 1')
ax.set_xlabel('Feature 2')
ax.set_xticks(())
ax.set_yticks(())
ax.set_title('Linear SVC with SMOTETomek')
ax.legend()
plt.show()
Erneut sind deutliche Unterschiede in der Herangehensweise der Methoden erkennbar, während der Aufruf der Methoden identisch ist.
Spezifizierung der Methoden
Es wurde bereits bei der Verwendung der InstanceHardnessThreshold Under-Sampling Methode darauf hingewiesen, dass den Methoden weitere Parameter übergeben werden können. In der Tat bieten alle Methoden die Möglichkeit, weiter spezifiziert zu werden, um den gewünschten Anforderungen gerecht zu werden. Wenngleich an dieser Stelle nicht alle Methoden mit ihren Parametern präsentiert werden können, soll doch anhand eines Beispiels aufgezeigt werden, welche Möglichkeiten das imbalanced-learn Paket offeriert. Betrachte dazu die InstanceHardnessThreshold Methode:
InstanceHardnessThreshold(estimator=None, sampling_strategy=’auto’, return_indices=False, random_state=None, cv=5, n_jobs=1, ratio=None)
Je nach Situation lassen sich etwa verschiedene Klassifizierer (estimator) zur Schätzung der instance hardness der Stichprobe wählen, zum Beispiel durch Übergabe eines Strings ‘knn‘, ‘decision-tree‘, ‘random-forest‘, ‘adaboost‘, ‘gradient-boosting‘ oder ‘linear-svm‘ oder durch Übergabe eines Objektes wie im obigen Beispiel etwa sklearn.linear_model.LogisticRegression.
Die Vorgehensweise des Samplings kann durch die sampling_strategy gesteuert werden. So wird durch Übergabe des Strings ‘not majority‘ spezifiziert, dass alle Klasse bis auf die Mehrheitsklasse resampled werden. Durch random_state lässt sich ein Seed spezifizieren, welcher die Randomisierung des Algorithmus steuert. Mit n_jobs lässt sich spezifizieren, auf wie viele Threads die Berechnung verteilt werden soll, sofern möglich.
Für eine detaillierte Dokumentation aller Methoden bietet sich die eigene Website des imbalanced-learn Pakets an, welche zusätzlich mit diversen Beispielen zur Anwendung aufwartet, welche über die hier dargestellten Beispiele hinausgehen. Nichtsdestotrotz dürfte dieses Beispiel einen guten Einblick in die Variabilität und Vielseitigkeit geben, die durch das imbalanced-learn Paket angeboten wird.
Fazit
Das imbalanced-learn Paket bietet genau das, was der Name nahelegt: eine Vielzahl von verschiedenen Methoden, um unausgewogene Datensätze zu balancieren. Die Methoden decken die gängigen Varianten für diese Problemstellung ab und können durch eine einfache Anwendbarkeit überzeugen. Durch die Angabe weiterer Parameter lassen sich die verschiedenen Methoden auf unterschiedliche Anforderungen zuschneiden, was die Verwendbarkeit universell macht. Zudem punktet die Website des imbalanced-learn Pakets neben einer übersichtlichen Dokumentation der Methoden mit diversen Anwendungs- und Code-Beispielen.
Wer mit unausgewogenen Daten konfrontiert ist und einen Bezug zu Python hat, sollte dem imbalanced-learn Paket auf jeden Fall einen Blick widmen und die Funktionalitäten austesten – die Zugangshürden sind gering und die Ergebnisse überzeugend.