Notebooks reichen für ML im großen Maßstab nicht aus

Foto von Sylvain Mauroux An Unsplash

Alle Bilder stammen, sofern nicht anders angegeben, vom Autor

Es gibt ein Missverständnis (um nicht zu sagen Fantasie), das in Unternehmen immer wieder auftritt, wenn es um KI und maschinelles Lernen geht. Menschen schätzen die Komplexität und die Fähigkeiten, die erforderlich sind, um Machine-Studying-Projekte in die Produktion zu bringen, oft falsch ein, entweder weil sie die Aufgabe nicht verstehen oder (noch schlimmer) weil sie glauben, sie zu verstehen, obwohl sie es nicht verstehen.

Ihre erste Reaktion, wenn sie KI entdecken, könnte etwa so lauten: „KI ist eigentlich ziemlich einfach, ich brauche nur ein Jupyter-Pocket book, kopiere den Code hier und da, füge ihn ein – oder frage Copilot – und growth.“ Es besteht schließlich keine Notwendigkeit, Datenwissenschaftler einzustellen …“ Und die Geschichte endet immer schlecht, mit Bitterkeit, Enttäuschung und dem Gefühl, dass KI ein Betrug ist: Schwierigkeiten beim Übergang zur Produktion, Datendrift, Fehler, unerwünschtes Verhalten.

Schreiben wir es additionally ein für alle Mal auf: KI/maschinelles Lernen/jeder datenbezogene Job ist ein echter Job, kein Pastime. Es erfordert Fähigkeiten, handwerkliches Können und Werkzeuge. Wenn Sie denken, dass Sie ML in der Produktion mit Notebooks durchführen können, liegen Sie falsch.

Ziel dieses Artikels ist es, anhand eines einfachen Beispiels den gesamten Aufwand, die Fähigkeiten und die Werkzeuge aufzuzeigen, die erforderlich sind, um von einem Pocket book zu einer echten Pipeline in der Produktion zu gelangen. Denn bei ML in der Produktion geht es vor allem darum, die Ausführung Ihres Codes regelmäßig durch Automatisierung und Überwachung automatisieren zu können.

Und für diejenigen, die nach einem Finish-to-Finish-Tutorial „Pocket book zu Vertex-Pipelines“ suchen, könnte dieses hilfreich sein.

Stellen wir uns vor, Sie sind Datenwissenschaftler und arbeiten bei einem E-Commerce-Unternehmen. Ihr Unternehmen verkauft Kleidung on-line und das Marketingteam bittet Sie um Ihre Hilfe: Sie bereiten ein Sonderangebot für bestimmte Produkte vor und möchten Kunden effizient ansprechen, indem sie E-Mail-Inhalte anpassen, die an sie weitergeleitet werden, um die Konversion zu maximieren. Ihre Aufgabe ist additionally einfach: Jedem Kunden soll eine Punktzahl zugewiesen werden, die die Wahrscheinlichkeit darstellt, dass er/sie ein Produkt aus dem Sonderangebot kauft.

Das Sonderangebot richtet sich speziell an diese Marken, was bedeutet, dass das Marketingteam wissen möchte, welche Kunden ihr nächstes Produkt von den folgenden Marken kaufen werden:

Allegra Okay, Calvin Klein, Carhartt, Hanes, Volcom, Nautica, Quiksilver, Diesel, Dockers, Hurley

Für diesen Artikel verwenden wir einen öffentlich verfügbaren Datensatz von Google, den „thelook_ecommerce` Datensatz. Es enthält gefälschte Daten mit Transaktionen, Kundendaten, Produktdaten, alles, was uns bei der Arbeit bei einem On-line-Modehändler zur Verfügung steht.

Um diesem Notizbuch zu folgen, benötigen Sie Zugriff auf die Google Cloud Platform, die Logik kann jedoch auf andere Cloud-Anbieter oder Drittanbieter wie Neptune, MLFlow usw. repliziert werden.

Als angesehener Datenwissenschaftler erstellen Sie zunächst ein Notizbuch, das uns bei der Erkundung der Daten hilft.

Wir importieren zunächst Bibliotheken, die wir in diesem Artikel verwenden werden:

import catboost as cb
import pandas as pd
import sklearn as sk
import numpy as np
import datetime as dt

from dataclasses import dataclass
from sklearn.model_selection import train_test_split
from google.cloud import bigquery

%load_ext watermark
%watermark --packages catboost,pandas,sklearn,numpy,google.cloud.bigquery

catboost             : 1.0.4
pandas : 1.4.2
numpy : 1.22.4
google.cloud.bigquery: 3.2.0

Beschaffung und Aufbereitung der Daten

Anschließend laden wir die Daten mithilfe des Python-Purchasers aus BigQuery. Verwenden Sie unbedingt Ihre eigene Projekt-ID:

question = """
SELECT
transactions.user_id,
merchandise.model,
merchandise.class,
merchandise.division,
merchandise.retail_price,
customers.gender,
customers.age,
customers.created_at,
customers.nation,
customers.metropolis,
transactions.created_at
FROM `bigquery-public-data.thelook_ecommerce.order_items` as transactions
LEFT JOIN `bigquery-public-data.thelook_ecommerce.customers` as customers
ON transactions.user_id = customers.id
LEFT JOIN `bigquery-public-data.thelook_ecommerce.merchandise` as merchandise
ON transactions.product_id = merchandise.id
WHERE standing <> 'Cancelled'
"""

consumer = bigquery.Consumer()
df = consumer.question(question).to_dataframe()

Wenn Sie sich den Datenrahmen ansehen, sollten Sie etwa Folgendes sehen:

Diese stellen die von den Kunden getätigten Transaktionen/Käufe dar, angereichert mit Kunden- und Produktinformationen.

Da es unser Ziel ist, vorherzusagen, welche Marke Kunden bei ihrem nächsten Einkauf kaufen werden, gehen wir wie folgt vor:

  1. Gruppieren Sie Einkäufe chronologisch für jeden Kunden
  2. Wenn ein Kunde N-Käufe tätigt, betrachten wir den N-ten Kauf als Ziel und den N-1-Kauf als unsere Merkmale.
  3. Wir schließen daher Kunden mit nur 1 Einkauf aus

Fügen wir das in den Code ein:

# Compute recurrent clients
recurrent_customers = df.groupby('user_id')('created_at').depend().to_frame("n_purchases")

# Merge with dataset and filter these with greater than 1 buy
df = df.merge(recurrent_customers, left_on='user_id', right_index=True, how='inside')
df = df.question('n_purchases > 1')

# Fill lacking values
df.fillna('NA', inplace=True)

target_brands = (
'Allegra Okay',
'Calvin Klein',
'Carhartt',
'Hanes',
'Volcom',
'Nautica',
'Quiksilver',
'Diesel',
'Dockers',
'Hurley'
)

aggregation_columns = ('model', 'division', 'class')

# Group purchases by person chronologically
df_agg = (df.sort_values('created_at')
.groupby(('user_id', 'gender', 'nation', 'metropolis', 'age'), as_index=False)(('model', 'division', 'class'))
.agg({ok: ";".be a part of for ok in ('model', 'division', 'class')})
)

# Create the goal
df_agg('last_purchase_brand') = df_agg('model').apply(lambda x: x.break up(";")(-1))
df_agg('goal') = df_agg('last_purchase_brand').isin(target_brands)*1

df_agg('age') = df_agg('age').astype(float)

# Take away final merchandise of sequence options to keep away from goal leakage :
for col in aggregation_columns:
df_agg(col) = df_agg(col).apply(lambda x: ";".be a part of(x.break up(";")(:-1)))

Beachten Sie, dass wir das letzte Aspect in der Sequenz Options entfernt haben: Dies ist sehr wichtig, da es sonst zu einem sogenannten „Datenleck“ kommt: Das Ziel ist Teil der Options, das Modell erhält die Antwort beim Lernen.

Das bekommen wir jetzt neu df_agg Datenrahmen:

Beim Vergleich mit dem ursprünglichen Datenrahmen sehen wir, dass user_id 2 tatsächlich IZOD, Parke & Ronen und schließlich Orvis gekauft hat, was nicht zu den Zielmarken gehört.

Aufteilung in Zug, Validierung und Take a look at

Als erfahrener Datenwissenschaftler werden Sie Ihre Daten nun in verschiedene Sätze aufteilen, da Sie offensichtlich wissen, dass alle drei für die Durchführung eines rigorosen maschinellen Lernens erforderlich sind. (Kreuzvalidierung ist heutzutage nicht mehr möglich, Leute, halten wir es einfach.)

Ein wichtiger Punkt bei der Aufteilung der Daten ist die Verwendung nicht so bekannter Daten stratify Parameter aus scikit-learn train_test_split() Methode. Der Grund dafür liegt im Klassenungleichgewicht: Wenn die Zielverteilung (in unserem Fall % von 0 und 1) zwischen Coaching und Take a look at unterschiedlich ist, kann es sein, dass wir bei der Bereitstellung des Modells über schlechte Ergebnisse frustriert werden. ML 101 Kinder: Halten Sie die Datenverteilung zwischen Trainingsdaten und Testdaten so ähnlich wie möglich.

# Take away unecessary options

df_agg.drop('last_purchase_category', axis=1, inplace=True)
df_agg.drop('last_purchase_brand', axis=1, inplace=True)
df_agg.drop('user_id', axis=1, inplace=True)

# Cut up the information into practice and eval
df_train, df_val = train_test_split(df_agg, stratify=df_agg('goal'), test_size=0.2)
print(f"{len(df_train)} samples in practice")

df_train, df_val = train_test_split(df_agg, stratify=df_agg('goal'), test_size=0.2)
print(f"{len(df_train)} samples in practice")
# 30950 samples in practice

df_val, df_test = train_test_split(df_val, stratify=df_val('goal'), test_size=0.5)
print(f"{len(df_val)} samples in val")
print(f"{len(df_test)} samples in check")
# 3869 samples in practice
# 3869 samples in check

Nachdem dies erledigt ist, werden wir unseren Datensatz elegant zwischen Options und Zielen aufteilen:

X_train, y_train = df_train.iloc(:, :-1), df_train('goal')
X_val, y_val = df_val.iloc(:, :-1), df_val('goal')
X_test, y_test = df_test.iloc(:, :-1), df_test('goal')

Zu den Funktionen gehören verschiedene Typen. Normalerweise trennen wir diese zwischen:

  • numerische Merkmale: Sie sind kontinuierlich und spiegeln eine messbare oder geordnete Größe wider.
  • kategoriale Merkmale: Sie sind normalerweise diskret und werden oft als Zeichenfolgen dargestellt (z. B. ein Land, eine Farbe usw.)
  • Textmerkmale: Es handelt sich in der Regel um Wortfolgen.

Natürlich kann es mehr wie Bild, Video, Audio usw. geben.

Das Modell: Einführung von CatBoost

Für unser Klassifizierungsproblem (Sie wussten bereits, dass wir uns in einem Klassifizierungsrahmen befinden, nicht wahr?) werden wir eine einfache, aber sehr leistungsstarke Bibliothek verwenden: CatBoost. Es wird von Yandex erstellt und verwaltet und bietet eine Excessive-Stage-API zum einfachen Spielen mit geboosteten Bäumen. Es kommt XGBoost nahe, funktioniert aber unter der Haube nicht genau gleich.

CatBoost bietet einen schönen Wrapper für den Umgang mit Funktionen unterschiedlicher Artwork. In unserem Fall können einige Options als „Textual content“ betrachtet werden, da es sich um eine Aneinanderreihung von Wörtern handelt, wie zum Beispiel „Calvin Klein;BCBGeneration;Hanes“. Der Umgang mit dieser Artwork von Funktionen kann manchmal mühsam sein, da Sie sie mit Textsplittern, Tokenizern, Lemmatisierern usw. handhaben müssen. Hoffentlich kann CatBoost alles für uns erledigen!

# Outline options
options = {
'numerical': ('retail_price', 'age'),
'static': ('gender', 'nation', 'metropolis'),
'dynamic': ('model', 'division', 'class')
}

# Construct CatBoost "swimming pools", that are datasets
train_pool = cb.Pool(
X_train,
y_train,
cat_features=options.get("static"),
text_features=options.get("dynamic"),
)

validation_pool = cb.Pool(
X_val,
y_val,
cat_features=options.get("static"),
text_features=options.get("dynamic"),
)

# Specify textual content processing choices to deal with our textual content options
text_processing_options = {
"tokenizers": (
{"tokenizer_id": "SemiColon", "delimiter": ";", "lowercasing": "false"}
),
"dictionaries": ({"dictionary_id": "Phrase", "gram_order": "1"}),
"feature_processing": {
"default": (
{
"dictionaries_names": ("Phrase"),
"feature_calcers": ("BoW"),
"tokenizers_names": ("SemiColon"),
}
),
},
}

Jetzt sind wir bereit, unser Modell zu definieren und zu trainieren. Es würde heute den Rahmen sprengen, jeden einzelnen Parameter durchzugehen, da die Anzahl der Parameter recht beeindruckend ist. Schauen Sie sich die API jedoch gerne selbst an.

Und der Kürze halber werden wir heute kein Hyperparameter-Tuning durchführen, aber das ist offensichtlich ein großer Teil der Arbeit des Datenwissenschaftlers!

# Prepare the mannequin
mannequin = cb.CatBoostClassifier(
iterations=200,
loss_function="Logloss",
random_state=42,
verbose=1,
auto_class_weights="SqrtBalanced",
use_best_model=True,
text_processing=text_processing_options,
eval_metric='AUC'
)

mannequin.match(
train_pool,
eval_set=validation_pool,
verbose=10
)

Und voilà, unser Modell ist trainiert. Sind wir fertig?

Nein. Wir müssen überprüfen, ob die Leistung unseres Modells zwischen Coaching und Take a look at konsistent ist. Eine große Lücke zwischen Coaching und Take a look at bedeutet, dass unser Modell überangepasst ist (d. h. „die Trainingsdaten auswendig lernen und nicht intestine darin sind, unsichtbare Daten vorherzusagen“).

Für unsere Modellbewertung verwenden wir den ROC-AUC-Rating. Auch hier möchte ich nicht tief in die Materie eintauchen, aber meiner eigenen Erfahrung nach ist dies eine im Allgemeinen recht robuste Metrik und viel besser als Genauigkeit.

Eine kurze Randbemerkung zur Genauigkeit: Ich empfehle normalerweise nicht, dies als Bewertungsmaßstab zu verwenden. Stellen Sie sich einen unausgeglichenen Datensatz vor, bei dem 1 % der positiven und 99 % der negativen Daten vorliegen. Wie genau wäre ein sehr dummes Modell, das ständig 0 vorhersagt? 99 %. Genauigkeit ist hier additionally nicht hilfreich.

from sklearn.metrics import roc_auc_score

print(f"ROC-AUC for practice set : {roc_auc_score(y_true=y_train, y_score=mannequin.predict(X_train)):.2f}")
print(f"ROC-AUC for validation set : {roc_auc_score(y_true=y_val, y_score=mannequin.predict(X_val)):.2f}")
print(f"ROC-AUC for check set : {roc_auc_score(y_true=y_test, y_score=mannequin.predict(X_test)):.2f}")

ROC-AUC for practice set      : 0.612
ROC-AUC for validation set : 0.586
ROC-AUC for check set : 0.622

Um ehrlich zu sein, ist 0,62 AUC überhaupt nicht großartig und ein wenig enttäuschend für den Experten im Bereich Datenwissenschaft, der Sie sind. Unser Modell muss hier definitiv ein wenig an den Parametern angepasst werden, und vielleicht sollten wir auch das Function-Engineering ernsthafter betreiben.

Aber es ist schon besser als zufällige Vorhersagen (puh):

# random predictions

print(f"ROC-AUC for practice set : {roc_auc_score(y_true=y_train, y_score=np.random.rand(len(y_train))):.3f}")
print(f"ROC-AUC for validation set : {roc_auc_score(y_true=y_val, y_score=np.random.rand(len(y_val))):.3f}")
print(f"ROC-AUC for check set : {roc_auc_score(y_true=y_test, y_score=np.random.rand(len(y_test))):.3f}")

ROC-AUC for practice set      : 0.501
ROC-AUC for validation set : 0.499
ROC-AUC for check set : 0.501

Gehen wir einmal davon aus, dass wir mit unserem Modell und unserem Pocket book vorerst zufrieden sind. Hier würden Novice-Datenwissenschaftler aufhören. Wie machen wir additionally den nächsten Schritt und werden produktionsbereit?

Lernen Sie Docker kennen

Docker ist eine Reihe von Platform-as-a-Service-Produkten, die Virtualisierung auf Betriebssystemebene nutzen, um Software program in Paketen, sogenannten Containern, bereitzustellen. Stellen Sie sich Docker vor diesem Hintergrund als Code vor, der überall ausgeführt werden kann und es Ihnen ermöglicht, die Scenario „funktioniert auf Ihrem Pc, aber nicht auf meinem“ zu vermeiden.

Warum Docker verwenden? Denn neben coolen Dingen wie der Möglichkeit, Ihren Code zu teilen, Versionen davon zu behalten und seine einfache Bereitstellung überall sicherzustellen, kann er auch zum Erstellen von Pipelines verwendet werden. Hab Geduld mit mir und du wirst es im weiteren Verlauf verstehen.

Der erste Schritt zum Erstellen einer Containeranwendung besteht darin, unser unordentliches Notizbuch umzugestalten und aufzuräumen. Wir werden 2 Dateien definieren, preprocess.py Und practice.py für unser sehr einfaches Beispiel, und fügen Sie sie in a ein src Verzeichnis. Wir werden auch unsere einbeziehen necessities.txt Datei mit allem darin.

# src/preprocess.py

from sklearn.model_selection import train_test_split
from google.cloud import bigquery

def create_dataset_from_bq():
question = """
SELECT
transactions.user_id,
merchandise.model,
merchandise.class,
merchandise.division,
merchandise.retail_price,
customers.gender,
customers.age,
customers.created_at,
customers.nation,
customers.metropolis,
transactions.created_at
FROM `bigquery-public-data.thelook_ecommerce.order_items` as transactions
LEFT JOIN `bigquery-public-data.thelook_ecommerce.customers` as customers
ON transactions.user_id = customers.id
LEFT JOIN `bigquery-public-data.thelook_ecommerce.merchandise` as merchandise
ON transactions.product_id = merchandise.id
WHERE standing <> 'Cancelled'
"""
consumer = bigquery.Consumer(challenge='<replace_with_your_project_id>')
df = consumer.question(question).to_dataframe()
print(f"{len(df)} rows loaded.")

# Compute recurrent clients
recurrent_customers = df.groupby('user_id')('created_at').depend().to_frame("n_purchases")

# Merge with dataset and filter these with greater than 1 buy
df = df.merge(recurrent_customers, left_on='user_id', right_index=True, how='inside')
df = df.question('n_purchases > 1')

# Fill lacking worth
df.fillna('NA', inplace=True)

target_brands = (
'Allegra Okay',
'Calvin Klein',
'Carhartt',
'Hanes',
'Volcom',
'Nautica',
'Quiksilver',
'Diesel',
'Dockers',
'Hurley'
)

aggregation_columns = ('model', 'division', 'class')

# Group purchases by person chronologically
df_agg = (df.sort_values('created_at')
.groupby(('user_id', 'gender', 'nation', 'metropolis', 'age'), as_index=False)(('model', 'division', 'class'))
.agg({ok: ";".be a part of for ok in ('model', 'division', 'class')})
)

# Create the goal
df_agg('last_purchase_brand') = df_agg('model').apply(lambda x: x.break up(";")(-1))
df_agg('goal') = df_agg('last_purchase_brand').isin(target_brands)*1

df_agg('age') = df_agg('age').astype(float)

# Take away final merchandise of sequence options to keep away from goal leakage :
for col in aggregation_columns:
df_agg(col) = df_agg(col).apply(lambda x: ";".be a part of(x.break up(";")(:-1)))

df_agg.drop('last_purchase_category', axis=1, inplace=True)
df_agg.drop('last_purchase_brand', axis=1, inplace=True)
df_agg.drop('user_id', axis=1, inplace=True)
return df_agg

def make_data_splits(df_agg):

df_train, df_val = train_test_split(df_agg, stratify=df_agg('goal'), test_size=0.2)
print(f"{len(df_train)} samples in practice")

df_val, df_test = train_test_split(df_val, stratify=df_val('goal'), test_size=0.5)
print(f"{len(df_val)} samples in val")
print(f"{len(df_test)} samples in check")

return df_train, df_val, df_test

# src/practice.py

import catboost as cb
import pandas as pd
import sklearn as sk
import numpy as np
import argparse

from sklearn.metrics import roc_auc_score

def train_and_evaluate(
train_path: str,
validation_path: str,
test_path: str
):
df_train = pd.read_csv(train_path)
df_val = pd.read_csv(validation_path)
df_test = pd.read_csv(test_path)

df_train.fillna('NA', inplace=True)
df_val.fillna('NA', inplace=True)
df_test.fillna('NA', inplace=True)

X_train, y_train = df_train.iloc(:, :-1), df_train('goal')
X_val, y_val = df_val.iloc(:, :-1), df_val('goal')
X_test, y_test = df_test.iloc(:, :-1), df_test('goal')

options = {
'numerical': ('retail_price', 'age'),
'static': ('gender', 'nation', 'metropolis'),
'dynamic': ('model', 'division', 'class')
}

train_pool = cb.Pool(
X_train,
y_train,
cat_features=options.get("static"),
text_features=options.get("dynamic"),
)

validation_pool = cb.Pool(
X_val,
y_val,
cat_features=options.get("static"),
text_features=options.get("dynamic"),
)

test_pool = cb.Pool(
X_test,
y_test,
cat_features=options.get("static"),
text_features=options.get("dynamic"),
)

params = CatBoostParams()

text_processing_options = {
"tokenizers": (
{"tokenizer_id": "SemiColon", "delimiter": ";", "lowercasing": "false"}
),
"dictionaries": ({"dictionary_id": "Phrase", "gram_order": "1"}),
"feature_processing": {
"default": (
{
"dictionaries_names": ("Phrase"),
"feature_calcers": ("BoW"),
"tokenizers_names": ("SemiColon"),
}
),
},
}

# Prepare the mannequin
mannequin = cb.CatBoostClassifier(
iterations=200,
loss_function="Logloss",
random_state=42,
verbose=1,
auto_class_weights="SqrtBalanced",
use_best_model=True,
text_processing=text_processing_options,
eval_metric='AUC'
)

mannequin.match(
train_pool,
eval_set=validation_pool,
verbose=10
)

roc_train = roc_auc_score(y_true=y_train, y_score=mannequin.predict(X_train))
roc_eval = roc_auc_score(y_true=y_val, y_score=mannequin.predict(X_val))
roc_test = roc_auc_score(y_true=y_test, y_score=mannequin.predict(X_test))
print(f"ROC-AUC for practice set : {roc_train:.2f}")
print(f"ROC-AUC for validation set : {roc_eval:.2f}")
print(f"ROC-AUC for check. set : {roc_test:.2f}")

return {"mannequin": mannequin, "scores": {"practice": roc_train, "eval": roc_eval, "check": roc_test}}

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--train-path", sort=str)
parser.add_argument("--validation-path", sort=str)
parser.add_argument("--test-path", sort=str)
parser.add_argument("--output-dir", sort=str)
args, _ = parser.parse_known_args()
_ = train_and_evaluate(
args.train_path,
args.validation_path,
args.test_path)

Jetzt viel sauberer. Sie können Ihr Skript jetzt tatsächlich über die Befehlszeile starten!

$ python practice.py --train-path xxx --validation-path yyy and many others.

Wir sind jetzt bereit, unser Docker-Picture zu erstellen. Dazu müssen wir eine Docker-Datei im Stammverzeichnis des Projekts schreiben:

# Dockerfile

FROM python:3.8-slim
WORKDIR /
COPY necessities.txt /necessities.txt
COPY src /src
RUN pip set up --upgrade pip && pip set up -r necessities.txt
ENTRYPOINT ( "bash" )

Dies wird unsere Anforderungen übernehmen, kopieren Sie die src Ordner und seinen Inhalt und installieren Sie die Anforderungen mit pip, wenn das Picture erstellt wird.

Um dieses Picture zu erstellen und in einer Container-Registrierung bereitzustellen, können wir das Google Cloud SDK und das verwenden gcloud Befehle:

PROJECT_ID = ...
IMAGE_NAME=f'thelook_training_demo'
IMAGE_TAG='newest'
IMAGE_URI='eu.gcr.io/{}/{}:{}'.format(PROJECT_ID, IMAGE_NAME, IMAGE_TAG)

!gcloud builds submit --tag $IMAGE_URI .

Wenn alles intestine geht, sollten Sie etwa Folgendes sehen:

Vertex Pipelines, der Übergang zur Produktion

Docker-Pictures sind der erste Schritt zu ernsthaftem maschinellem Lernen in der Produktion. Der nächste Schritt ist der Aufbau dessen, was wir „Pipelines“ nennen. Pipelines sind eine Reihe von Vorgängen, die von einem Framework namens Kubeflow orchestriert werden. Kubeflow kann auf Vertex AI in Google Cloud ausgeführt werden.

Die Gründe für die Bevorzugung von Pipelines gegenüber Notebooks in der Produktion können umstritten sein, aber ich nenne Ihnen drei basierend auf meiner Erfahrung:

  1. Überwachung und Reproduzierbarkeit: Jede Pipeline wird mit ihren Artefakten (Datensätze, Modelle, Metriken) gespeichert, sodass Sie Läufe vergleichen, erneut ausführen und prüfen können. Jedes Mal, wenn Sie ein Pocket book erneut ausführen, geht der Verlauf verloren (oder Sie müssen Artefakte und Protokolle selbst verwalten. Viel Glück.)
  2. Kosten: Der Betrieb eines Notebooks setzt voraus, dass es eine Maschine gibt, auf der es läuft. — Diese Maschine hat ihren Preis und für große Modelle oder große Datensätze benötigen Sie virtuelle Maschinen mit umfangreichen Spezifikationen.
    — Denken Sie daran, es auszuschalten, wenn Sie es nicht benutzen.
    – Oder Sie stürzen Ihre lokale Maschine einfach ab, wenn Sie keine virtuelle Maschine verwenden und andere Anwendungen ausführen möchten.
    – Vertex AI Pipelines ist ein serverlos Dies bedeutet, dass Sie die zugrunde liegende Infrastruktur nicht verwalten müssen und nur für das bezahlen, was Sie nutzen, d. h. für die Ausführungszeit.
  3. Skalierbarkeit: Viel Glück beim gleichzeitigen Ausführen Dutzender Experimente auf Ihrem lokalen Laptop computer. Sie kehren zur Verwendung einer VM zurück, skalieren diese VM und lesen den obigen Aufzählungspunkt noch einmal.

Der letzte Grund, Pipelines gegenüber Notebooks zu bevorzugen, ist subjektiv und höchst umstritten, aber meiner Meinung nach sind Notebooks einfach nicht für die termingerechte Ausführung von Workloads konzipiert. Sie eignen sich jedoch hervorragend zum Erkunden.

Verwenden Sie zumindest einen Cron-Job mit einem Docker-Picture oder Pipelines, wenn Sie die Dinge richtig machen möchten, aber führen Sie niemals ein Pocket book in der Produktion aus.

Schreiben wir ohne weiteres die Komponenten unserer Pipeline auf:

# IMPORT REQUIRED LIBRARIES
from kfp.v2 import dsl
from kfp.v2.dsl import (Artifact,
Dataset,
Enter,
Mannequin,
Output,
Metrics,
Markdown,
HTML,
element,
OutputPath,
InputPath)
from kfp.v2 import compiler
from google.cloud.aiplatform import pipeline_jobs

%watermark --packages kfp,google.cloud.aiplatform

kfp                    : 2.7.0
google.cloud.aiplatform: 1.50.0

Die erste Komponente lädt die Daten von Bigquery herunter und speichert sie als CSV-Datei.

Das BASE_IMAGE, das wir verwenden, ist das Picture, das wir zuvor erstellt haben! Wir können damit Module und Funktionen importieren, die wir in unserem Docker-Picture definiert haben src Ordner:

@element(
base_image=BASE_IMAGE,
output_component_file="get_data.yaml"
)
def create_dataset_from_bq(
output_dir: Output(Dataset),
):

from src.preprocess import create_dataset_from_bq

df = create_dataset_from_bq()

df.to_csv(output_dir.path, index=False)

Nächster Schritt: Daten aufteilen

@element(
base_image=BASE_IMAGE,
output_component_file="train_test_split.yaml",
)
def make_data_splits(
dataset_full: Enter(Dataset),
dataset_train: Output(Dataset),
dataset_val: Output(Dataset),
dataset_test: Output(Dataset)):

import pandas as pd
from src.preprocess import make_data_splits

df_agg = pd.read_csv(dataset_full.path)

df_agg.fillna('NA', inplace=True)

df_train, df_val, df_test = make_data_splits(df_agg)
print(f"{len(df_train)} samples in practice")
print(f"{len(df_val)} samples in practice")
print(f"{len(df_test)} samples in check")

df_train.to_csv(dataset_train.path, index=False)
df_val.to_csv(dataset_val.path, index=False)
df_test.to_csv(dataset_test.path, index=False)

Nächster Schritt: Modellschulung. Wir werden die Modellwerte speichern, um sie im nächsten Schritt anzuzeigen:

@element(
base_image=BASE_IMAGE,
output_component_file="train_model.yaml",
)
def train_model(
dataset_train: Enter(Dataset),
dataset_val: Enter(Dataset),
dataset_test: Enter(Dataset),
mannequin: Output(Mannequin)
):

import json
from src.practice import train_and_evaluate

outputs = train_and_evaluate(
dataset_train.path,
dataset_val.path,
dataset_test.path
)
cb_model = outputs('mannequin')
scores = outputs('scores')

mannequin.metadata("framework") = "catboost"
# Save the mannequin as an artifact
with open(mannequin.path, 'w') as f:
json.dump(scores, f)

Der letzte Schritt ist die Berechnung der Metriken (die tatsächlich beim Coaching des Modells berechnet werden). Es ist lediglich notwendig, aber es ist schön, Ihnen zu zeigen, wie einfach es ist, leichte Komponenten zu bauen. Beachten Sie, dass wir in diesem Fall die Komponente nicht aus dem BASE_IMAGE erstellen (das manchmal recht groß sein kann), sondern nur ein kompaktes Picture mit den erforderlichen Komponenten erstellen:

@element(
base_image="python:3.9",
output_component_file="compute_metrics.yaml",
)
def compute_metrics(
mannequin: Enter(Mannequin),
train_metric: Output(Metrics),
val_metric: Output(Metrics),
test_metric: Output(Metrics)
):

import json

file_name = mannequin.path
with open(file_name, 'r') as file:
model_metrics = json.load(file)

train_metric.log_metric('train_auc', model_metrics('practice'))
val_metric.log_metric('val_auc', model_metrics('eval'))
test_metric.log_metric('test_auc', model_metrics('check'))

Normalerweise können wir weitere Schritte einbeziehen, beispielsweise wenn wir unser Modell als API-Endpunkt bereitstellen möchten. Dies ist jedoch ein fortgeschritteneres Niveau und erfordert die Erstellung eines weiteren Docker-Pictures für die Bereitstellung des Modells. Wird beim nächsten Mal behandelt.

Nun kleben wir die Komponenten zusammen:

# USE TIMESTAMP TO DEFINE UNIQUE PIPELINE NAMES
TIMESTAMP = dt.datetime.now().strftime("%YpercentmpercentdpercentHpercentMpercentS")
DISPLAY_NAME = 'pipeline-thelook-demo-{}'.format(TIMESTAMP)
PIPELINE_ROOT = f"{BUCKET_NAME}/pipeline_root/"

# Outline the pipeline. Discover how steps reuse outputs from earlier steps
@dsl.pipeline(
pipeline_root=PIPELINE_ROOT,
# A reputation for the pipeline. Use to find out the pipeline Context.
title="pipeline-demo"
)

def pipeline(
challenge: str = PROJECT_ID,
area: str = REGION,
display_name: str = DISPLAY_NAME
):

load_data_op = create_dataset_from_bq()
train_test_split_op = make_data_splits(
dataset_full=load_data_op.outputs("output_dir")
)
train_model_op = train_model(
dataset_train=train_test_split_op.outputs("dataset_train"),
dataset_val=train_test_split_op.outputs("dataset_val"),
dataset_test=train_test_split_op.outputs("dataset_test"),
)
model_evaluation_op = compute_metrics(
mannequin=train_model_op.outputs("mannequin")
)

# Compile the pipeline as JSON
compiler.Compiler().compile(
pipeline_func=pipeline,
package_path='thelook_pipeline.json'
)

# Begin the pipeline
start_pipeline = pipeline_jobs.PipelineJob(
display_name="thelook-demo-pipeline",
template_path="thelook_pipeline.json",
enable_caching=False,
location=REGION,
challenge=PROJECT_ID
)

# Run the pipeline
start_pipeline.run(service_account=<your_service_account_here>)

Wenn alles intestine funktioniert, sehen Sie nun Ihre Pipeline in der Vertex-Benutzeroberfläche:

Sie können darauf klicken und die verschiedenen Schritte sehen:

Obwohl alle No-Code-/Low-Code-Enthusiasten Ihnen sagen, dass Sie kein Entwickler sein müssen, um maschinelles Lernen zu betreiben, ist Knowledge Science ein echter Job. Wie jeder Job erfordert er Fähigkeiten, Konzepte und Werkzeuge, die über Notizbücher hinausgehen.

Und für diejenigen, die Knowledge Scientists werden möchten, ist hier die Realität des Jobs.

Viel Spaß beim Codieren.

Von admin

Schreibe einen Kommentar

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