Gekennzeichnete Daten sind ein wichtiger Bestandteil des maschinellen Lernens, aber die Erstellung von „Labels“ ist manchmal teuer. Aktives Lernen hilft dabei, die Kennzeichnung intelligent zu steuern, um die Kosten zu senken und bessere Modelle zu erstellen. In einem Beispiel wird gezeigt, wie eine Python-Bibliothek für aktives Lernen, modAL, zum Einsatz kommen kann, um einen Menschen bei der Kennzeichnung von Daten für ein einfaches Textklassifizierungsproblem zu unterstützen.
Modelle zum Bereich Machine Learning vermitteln oftmals den Eindruck von schwarzer Magie. Sie können zum Beispiel erkennen, ob sich auf einem Bild ein Hotdog befindet – oder auch nicht. Hierbei handelt es sich natürlich um eine recht simple Spielerei, die wohl auch ein Papagei erlernen kann. Aber maschinell lernende Modelle verfügen weiterhin über sprachaktivierte Assistenten, die mühelos menschliche Sprache verstehen. Durch sie können auch Autos (mehr oder weniger) sicher von selbst fahren. Es ist kein Wunder, dass wir annehmen, dass diese in gewisser Weise künstlich „intelligent“ sind.
Was die Experten allerdings ungerne zugeben ist, dass diese überwachten Modelle mehr Papagei als Orakel sind. Durch eine enorme Anzahl von Beispielen lernen sie, die Verbindung zwischen Input und Output zu emulieren. Hierin liegt allerdings das Grundproblem für Unternehmen, wenn sie sich für maschinelles Lernen entscheiden: Die Modellierung ist (relativ) einfach, die richtigen Beispiele zu finden, ist die Herausforderung.
Auf geeignete Beispiele zu kommen, kann kompliziert sein. Man kann sich nicht von einem Tag auf den anderen dazu entscheiden, die Daten der letzten fünf Jahre zu sammeln. Wenn überhaupt Daten vorhanden sind, kann es sich nur um „Inputs“ ohne die gewünschten „Outputs“ zum Lernen handeln. Schlimmer noch, die Erstellung des „Labels“ ist normalerweise ein manueller Prozess. Wenn es nämlich einen automatisierten Prozess dafür gäbe, bräuchte man ihn nicht als Modell neu zu lernen!
Wo Labels nicht ohne weiteres verfügbar sind, ist ein gewisses Maß an manueller Kennzeichnung unvermeidlich. Glücklicherweise müssen nicht alle Daten gekennzeichnet werden. Techniken, die allgemein als „Active Learning“ bezeichnet wird, kann den Prozess kollaborativ machen, wobei ein an einigen Daten trainiertes Modell dabei hilft, diejenigen Daten zu identifizieren, bei denen eine Kennzeichnung am sinnvollsten ist.
Beispiel in Python
Im folgenden Beispiel wird eine Python-Bibliothek für aktives Lernen, modAL, verwendet, um einen Menschen bei der Kennzeichnung von Daten für ein einfaches Textklassifizierungsproblem zu unterstützen. Es wird gezeigt, wie Apache Spark modAL im Maßstab anwenden kann und wie durch Spark in Databricks integrierte Open-Source-Tools wie Hyperopt und mlflow, auf dem Weg dorthin helfen können.
Lernproblem aus der realen Welt: Einstufung von Verbraucherbeschwerden als „besorgt“
Das amerikanische „Consumer Financial Protection Bureau (CFPB)“ beaufsichtigt die Beziehungen der Finanzinstitute zu den Verbrauchern. Unter anderem bearbeitet es Beschwerden von Verbrauchern. Sie haben einen anonymisierten Datensatz dieser Beschwerden veröffentlicht. Die meisten sind einfache tabellarische Daten, aber sie enthalten auch den freien Text einer Verbraucherbeschwerde (falls vorhanden). Jeder, der schon einmal Kundenservice-Tickets bearbeitet hat, wird nicht überrascht sein, wie diese aussehen.
complaints_df = full_complaints_df.\
select(col(„Complaint ID“).alias(„id“),\
col(„Consumer complaint narrative“).alias(„complaint“)).\
filter(„complaint IS NOT NULL“)
display(complaints_df)
Wenn wir uns vorstellen, dass die CFPB die Behandlung von Beschwerden priorisieren will: ein Verbraucher, der verängstigt oder wütend ist, würde bei einem Anruf seine Stimme erheben. Es handelt sich um ein einfaches Textklassifizierungsproblem – wenn diese Beschwerden bereits entsprechend gekennzeichnet sind. Das sind sie allerdings nicht. Bei über 440.000 Beschwerden ist es nicht realistisch, sie alle von Hand zu beschriften.
Unter diesem Link findet sich ein Datensatz, in dem für diese Demonstration 230 Beschwerden gekennzeichnet wurden.
labeled1_df = spark.read.option(„header“, True).option(„inferSchema“, True).\
csv(data_path + „/labeled.csv“)
input1_df = complaints_df.join(labeled1_df, „id“)
pool_df = complaints_df.join(labeled1_df, „id“, how=“left_anti“)
display(input1_df)

Bild 2: Verwendung von Spark ML zum Aufbau des anfänglichen Klassifizierungsmodells; Quelle: Databricks
Spark ML kann eine grundlegende TF-IDF-Einbettung des Textes im Maßstab konstruieren. Im Moment müssen nur die wenigen gekennzeichneten Beispiele transformiert werden, aber der gesamte Datensatz wird diese Transformation später benötigen.
# Tokenize into words
tokenizer = Tokenizer(inputCol=“complaint“, outputCol=“tokenized“)
# Remove stopwords
remover = StopWordsRemover(inputCol=tokenizer.getOutputCol(), outputCol=“filtered“)
# Compute term frequencies and hash into buckets
hashing_tf = HashingTF(inputCol=tokenizer.getOutputCol(), outputCol=“hashed“,\
numFeatures=1000)
# Convert to TF-IDF
idf = IDF(inputCol=hashing_tf.getOutputCol(), outputCol=“features“)
pipeline = Pipeline(stages=[tokenizer, remover, hashing_tf, idf])
pipeline_model = pipeline.fit(complaints_df)
# need array of float, not Spark vector, for pandas later
tolist_udf = udf(lambda v: v.toArray().tolist(), ArrayType(FloatType()))
featurized1_df = pipeline_model.transform(input1_df).\
select(„id“, „complaint“, „features“, „distressed“).\
withColumn(„features“, tolist_udf(„features“))
Es ist nicht sinnvoll, Spark ML in dieser Größenordnung anzuwenden. Stattdessen kann Scikit-Learn das Modell in Sekundenschnelle auf diesen winzigen Datensatz anpassen. Allerdings spielt Spark hier dennoch eine Rolle. Die Anpassung eines Modells bedeutet in der Regel die Anpassung vieler Varianten an das Modell, wobei die „Hyperparameter“ variiert werden.
Diese Varianten können von Spark parallel eingepasst werden. Hyperopt ist ein Open-Source-Tool, das in Spark in Databricks integriert ist und diese Suche nach optimalen Hyperparametern so betreiben kann, dass es lernt, welche Kombinationen am besten funktionieren, anstatt nur zufällig zu suchen.
Das beigefügte Notizbuch enthält eine vollständige Code-Liste, aber es folgt eine Bearbeitung des wichtigsten Teils der Implementierung:
# Core function to train a model given train set and params
def train_model(params, X_train, y_train):
lr = LogisticRegression(solver=’liblinear‘, max_iter=1000,\
penalty=params[‚penalty‘], C=params[‚C‘], random_state=seed)
return lr.fit(X_train, y_train)
# Wraps core modeling function to evaluate and return results for hyperopt
def train_model_fmin(params):
lr = train_model(params, X_train, y_train)
loss = log_loss(y_val, lr.predict_proba(X_val))
# supplement auto logging in mlflow with accuracy
accuracy = accuracy_score(y_val, lr.predict(X_val))
mlflow.log_metric(‚accuracy‘, accuracy)
return {’status‘: STATUS_OK, ‚loss‘: loss, ‚accuracy‘: accuracy}
penalties = [‚l1‘, ‚l2‘]
search_space = {
‚C‘: hp.loguniform(‚C‘, -6, 1),
‚penalty‘: hp.choice(‚penalty‘, penalties)
}
best_params = fmin(fn=train_model_fmin,
space=search_space,
algo=tpe.suggest,
max_evals=32,
trials=SparkTrials(parallelism=4),
rstate=np.random.RandomState(seed))
# Need to translate this back from 0/1 in output to be used again as input
best_params[‚penalty‘] = penalties[best_params[‚penalty‘]]
# Train final model on train + validation sets
final_model = train_model(best_params,\
np.concatenate([X_train, X_val]),\
np.concatenate([y_train, y_val]))
…
(X_train, X_val, X_test, y_train, y_val, y_test) = build_test_train_split(featurized1_pd, 80)
(best_params, best_model) = find_best_lr_model(X_train, X_val, y_train, y_val)
(accuracy, loss) = log_and_eval_model(best_model, best_params, X_test, y_test)
…
Accuracy: 0.6
Loss: 0.6928265768789768
Hyperopt versucht hier 128 verschiedene Hyperparameterkombinationen bei der Suche. Hier variiert die „regularization penalty“ L1 gegen L2 und die Stärke der Regularisierung, C. Es gibt die besten gefundenen Einstellungen zurück, aus denen ein endgültiges Modell auf Trainings- und Validierungsdaten nachgebessert wird. Die Ergebnisse dieser Versuche werden automatisch in mlflow protokolliert, wenn Databricks verwendet wird. Die obige Auflistung zeigt, dass es möglich ist, zusätzliche Metriken wie die Genauigkeit zu protokollieren, nicht nur den „Verlust“, den Hyperopt aufzeichnet. Es ist zum Beispiel klar, dass die L1-Regulierung besser ist:
Für den Lauf mit dem besten Verlust von etwa 0,7 beträgt die Genauigkeit nur 60 Prozent. Weitere Abstimmungen und ausgefeilte Modelle könnten dies verbessern, aber mit einem derartig kleinen Trainingssatz ist man natürlich limitiert. Es werden mehr gekennzeichnete Daten benötigt.
Anwendung von modAL für aktives Lernen
Hier kommt das aktive Lernen über die modAL-Bibliothek ins Spiel. Die Anwendung ist bequem und simpel. Wenn es auf einen Klassifikator oder Regressor angewandt wird, der eine probabilistische Schätzung seiner Vorhersage liefert, kann es die verbleibenden Daten analysieren und entscheiden, welche am nützlichsten zu beschriften sind.
„Nützlich“ bedeutet im Allgemeinen Kennzeichnungen für Eingaben, über die der Klassifikator derzeit am unsichersten ist. Die Kenntnis des Labels verbessert den Klassifikator mit größerer Wahrscheinlichkeit als die einer Eingabe, deren Vorhersage ziemlich sicher ist. Das modAL-Framework unterstützt über ActiveLearner Klassifikatoren wie die Regression, deren Ausgabe eine Wahrscheinlichkeit ist.
learner = ActiveLearner(estimator=best_model, X_training=X_train, y_training=y_train)
Es ist notwendig, den „Pool“ der verbleibenden Daten für die Abfrage vorzubereiten. Das bedeutet, dass die restlichen Daten mit Features versehen werden müssen. Es ist daher praktisch, dass sie mit Spark ML implementiert wurden:
featurized_pool_df = pipeline_model.transform(pool_df).\
select(„id“, „complaint“, „features“).\
withColumn(„features“, tolist_udf(„features“)).cache()
Die query()-Methode von ActiveLearner gibt die unwahrscheinlichsten Instanzen aus einem unmarkierten Datensatz zurück, kann aber nicht direkt parallel über Spark arbeiten. Spark kann sie jedoch parallel auf Teile der Daten anwenden, die mit einem Pandas-UDF versehen sind, das die Daten effizient als Pandas-DataFrames oder -Serien darstellt. Diese können dann unabhängig voneinander mit ActiveLearner abgefragt werden. Im nächsten Beispiel werden etwa 0,02 Prozent von den 440.000 im Pool ausgewählt:
query_fraction = 0.0002
@pandas_udf(„boolean“)
def to_query(features_series):
X_i = np.stack(features_series.to_numpy())
n = X_i.shape[0]
query_idx, _ = learner.query(X_i, n_instances=math.ceil(n * query_fraction))
# Output has same size of inputs; most instances were not sampled for query
query_result = pd.Series([False] * n)
# Set True where ActiveLearner wants a label
query_result.iloc[query_idx]= True
return query_result
with_query_df = featurized_pool_df.withColumn(„query“, to_query(„features“))
display(with_query_df.filter(„query“).select(„complaint“))
Es ist wichtig zu verstehen, dass dieser Vorgang nicht der Auswahl der besten 0,02 Prozent für die Abfrage aus dem gesamten Pool von 440.000 Daten entspricht, da hierbei die besten 0,02 Prozent aus jedem Stück dieser Daten als Pandas-DataFrame separat ausgewählt werden. Diese Ausgabe wird nicht unbedingt die besten Abfragekandidaten befördern. Der Vorteil liegt in der Parallelität. Dieser Kompromiss ist wahrscheinlich in praktischen Fällen nützlich, da die Ergebnisse immer noch nützlicher sein werden als die meisten, die abgefragt werden können.
Die Funktionsweise von Active Learning Queries
Tatsächlich liefert das Modell Wahrscheinlichkeiten zwischen 49,9 und 50,1 Prozent für alle Beschwerden in der Abfrage. Es ist also bei allen unsicher.
Die Eingabemerkmale können mit seaborn in zwei Dimensionen dargestellt werden (über die PCA von Scikit-Learn), um nicht nur zu veranschaulichen, welche Beschwerden als „besorgt“ eingestuft werden, sondern auch, welche das Machine Learning zur Kennzeichnung ausgewählt hat.
…
queried = with_query_pd[‚query‘]
ax = sns.scatterplot(x=pca_pd[:,0], y=pca_pd[:,1],\
hue=best_model.predict(with_query_np), style=~queried, size=~queried,\
alpha=0.8, legend=False)
# Zoom in on the interesting part
ax.set_xlim(-0.75,1)
ax.set_ylim(-1,1)
display()
Hier sind analog zum bisherigen Modell orangefarbene Punkte als „besorgt“ und blaue als „nicht besorgt“ markiert. Die größeren Punkte sind einige von denen, die zur Abfrage ausgewählt wurden; sie sind zufällig alle negativ.
Modellklassifizierung der (projizierten) Probe, mit abgefragten Punkten
Obwohl es visuell schwer zu interpretieren ist, scheinen Punkte in Regionen ausgewählt zu werden, in denen beide Klassifizierungen erscheinen und nicht aus einheitlichen Regionen.
Im Rahmen dieser Demonstration wurde nun der Abfragesatz als CSV heruntergeladen und knapp 100 weitere Daten beschriftet, dann exportiert und als CSV wieder in den Speicher hochgeladen. Ein Low-Tech-Verfahren wie dieses – eine Spalte in einer Tabellenkalkulation – kann für die Beschriftung in kleinem Maßstab durchaus ausreichend sein. Natürlich ist es auch möglich, die Abfrage als Tabelle zu speichern, die ein externes System zur Verwaltung der Beschriftung verwendet.
Der gleiche Prozess kann mit dem neuen, größeren Datensatz wiederholt werden—und das Ergebnis? Auf den Punkt gebracht, sind es 68 Prozent Genauigkeit. Diesmal fand die Suche von Hyperopt (siehe Auflistung oben) über Hyperparameter mit besseren Modellen aus den ersten Versuchen und verbesserte sich von dort aus, anstatt mit etwa 60-prozentiger Genauigkeit ihren Höhepunkt zu erreichen.
Variationen der Lernstrategie bei modAL-Abfragen
ModAL verfügt über andere Strategien zur Auswahl von Abfragekandidaten: Max-Uncertainty-Sampling, Max-Margin-Sampling und Entropy-Sampling. Diese unterscheiden sich im Mehrklassenfall, sind aber in einem binären Klassifikationsfall wie diesem gleichwertig.
Außerdem kann beispielsweise die Query-Strategie von Active Learning so angepasst werden, dass Uncertainty-Batch-Sampling verwendet wird, um Abfragen in der Reihenfolge der Unsicherheit zurückzugeben. Dies kann nützlich sein, um eine längere Liste von Abfragen vorzubereiten, die in der Reihenfolge ihrer Nützlichkeit beschriftet werden sollen, soweit es die Zeit vor der nächsten Modellerstellung und Abfrageschleife erlaubt.
def preset_batch(classifier, X_pool):
return uncertainty_batch_sampling(classifier, X_pool, 100)
learner = ActiveLearner(estimator=…, query_strategy=preset_batch)
Aktives Lernen durch Streaming
Oben war der gesamte Pool an Kandidaten für die query()-Methode verfügbar. Dies ist nützlich bei der Auswahl der besten Kandidaten, die in einem Batch-Kontext abgefragt werden sollen. Es kann jedoch erforderlich sein, dieselben Ideen auf einen Datenstrom anzuwenden, und zwar jeweils eine nach der anderen.
Es ist natürlich bereits möglich, das Modell mit einem Strom von Beschwerden zu bewerten und diejenigen zu kennzeichnen, für die mit hoher Wahrscheinlichkeit eine präventive Eskalation vorausgesagt wird. In einigen Fällen könnte es jedoch ebenso nützlich sein, hochgradig unsichere Eingaben zur Bewertung durch ein datenwissenschaftliches Team zu kennzeichnen, bevor das Modell neu aufgebaut wird.
@pandas_udf(„boolean“)
def uncertain(features_series):
X_i = np.stack(features_series.to_numpy())
n = X_i.shape[0]
uncertain = pd.Series([False] * n)
# Set True where uncertainty is high. Uncertainty is at most 0.5
uncertain[classifier_uncertainty(learner, X_i) > 0.4999] = True
return uncertain
display(pool2_df.filter(uncertain(pool2_df[‚features‘])).drop(„features“))
Im einfachen binären Klassifikationsfall reduziert sich dies im Wesentlichen auf die Feststellung, wo das Modell eine Wahrscheinlichkeit nahe 0,5 ausgibt. ModAL bietet jedoch andere Möglichkeiten zur Quantifizierung der Unsicherheit, die sich im Mehrklassenfall unterscheiden.
Erste Schritte mit einem aktiven Lernproblem
Wenn wir mit überwachten maschinellen Lernverfahren aus Daten lernen, ist es nicht wichtig, wie viele Daten wir haben, sondern wie viele beschriftete Daten. In einigen Fällen sind Labels teuer, wenn sie manuell erzeugt werden. Glücklicherweise können aktive Lerntechniken, wie sie in Open-Source-Tools wie modAL implementiert sind, Menschen dabei helfen, Prioritäten für die Kennzeichnung zu setzen. Das Rezept lautet:
- Kennzeichnen Sie eine kleine Menge an Daten, falls diese nicht bereits verfügbar sind.
- Trainieren Sie ein vorläufiges Modell.
- Wenden Sie Active Learning an, um zu entscheiden, welche Daten es zu kennzeichnen gilt.
- Trainieren Sie ein neues Modell und wiederholen Sie es, bis die Genauigkeit ausreichend ist oder Ihre Geduld beim Kennzeichnen erschöpft ist.
ModAL kann mit Apache Spark skaliert eingesetzt werden und lässt sich gut mit anderen Open-Source-Standard-Tools wie Scikit-Learn, Hyperopt und mlflow integrieren.
Sean Owen ist Data Scientist bei Databricks.
Mehr Details im Databricks-Blog