Formation et évaluation d’un modèle de classification de texte dans Microsoft Fabric

Dans ce notebook, nous montrons comment résoudre une tâche de classification de texte avec word2vec + modèle de régression linéaire sur Spark.

Important

Microsoft Fabric est actuellement en préversion. Certaines informations portent sur un produit en préversion susceptible d’être substantiellement modifié avant sa publication. Microsoft ne donne aucune garantie, expresse ou implicite, concernant les informations fournies ici.

L’exemple de jeu de données se compose de métadonnées relatives aux livres numérisés par la British Library en partenariat avec Microsoft. Il comprend des étiquettes générées par l’homme pour classer un livre comme « fiction » ou « non-fiction ». Nous utilisons ce jeu de données pour entraîner un modèle pour la classification des genres qui prédit si un livre est « fiction » ou « non-fiction » en fonction de son titre.

ID d’enregistrement BL Type de ressource Nom Dates associées au nom Type de nom Rôle Tous les noms Titre Titres de variantes Titre de la série Nombre dans la série Pays de publication Lieu de publication Publisher Date de publication Édition Description physique Classification Dewey Bl shelfmark Rubriques Genre Langages Notes ID d’enregistrement BL pour la ressource physique classification_id user_id created_at subject_ids annotator_date_pub annotator_normalised_date_pub annotator_edition_statement annotator_genre annotator_FAST_genre_terms annotator_FAST_subject_terms annotator_comments annotator_main_language annotator_other_languages_summaries annotator_summaries_language annotator_translation annotator_original_language annotator_publisher annotator_place_pub annotator_country annotator_title Lien vers un livre numérisé Annotée
014602826 Monographie Yearsley, Ann 1753-1806 personne Plus, Hannah, 1745-1833 [personne]; Yearsley, Ann, 1753-1806 [personne] Poèmes à plusieurs reprises [Avec une lettre préliminaire d’Hannah More.] England London 1786 Note manuscrite de la quatrième édition Magasin numérique 11644.d.32 Anglais 003996603 False
014602830 Monographie A, T. personne Oldham, John, 1653-1683 [personne]; A, T. [person] Une satyre contre Vertue. (Un poème : censé être prononcé par un Town-Hector [Par John Oldham. Préface signée : T. A.]) England London 1679 15 pages (4°) Digital Store 11602.ee.10. (2.) Anglais 000001143 False

Étape 1 : Charger les données

Configurations de notebook

Installation des bibliothèques

Dans ce notebook, nous wordcloud, qui doit d’abord être installé. Le noyau PySpark est redémarré après %pip install. Nous devons donc l’installer avant d’exécuter d’autres cellules. Word Cloud est un package qui nous permet d’utiliser une technique de visualisation des données pour représenter des données de texte afin d’indiquer leur fréquence dans un texte donné.

# install wordcloud for text visualization
%pip install wordcloud

En définissant les paramètres suivants, nous pouvons facilement appliquer ce notebook à différents jeux de données.

IS_CUSTOM_DATA = False  # if True, dataset has to be uploaded manually by user
DATA_FOLDER = "Files/title-genre-classification"
DATA_FILE = "blbooksgenre.csv"

# data schema
TEXT_COL = "Title"
LABEL_COL = "annotator_genre"
LABELS = ["Fiction", "Non-fiction"]

EXPERIMENT_NAME = "aisample-textclassification"  # mlflow experiment name

Nous définissons également certains hyperparamètres pour l’entraînement du modèle. (NE modifiez pas ces paramètres, sauf si vous en connaissez la signification).

# hyper-params
word2vec_size = 128
min_word_count = 3
max_iter = 10
k_folds = 3

Importer les dépendances

import numpy as np
from itertools import chain

from wordcloud import WordCloud
import matplotlib.pyplot as plt
import seaborn as sns

import pyspark.sql.functions as F

from pyspark.ml import Pipeline
from pyspark.ml.feature import *
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import (
    BinaryClassificationEvaluator,
    MulticlassClassificationEvaluator,
)

from synapse.ml.stages import ClassBalancer
from synapse.ml.train import ComputeModelStatistics

import mlflow

Télécharger le jeu de données et le charger sur Lakehouse

Ajoutez un Lakehouse au bloc-notes avant de l’exécuter.

if not IS_CUSTOM_DATA:
    # Download demo data files into lakehouse if not exist
    import os, requests

    remote_url = "https://synapseaisolutionsa.blob.core.windows.net/public/Title_Genre_Classification"
    fname = "blbooksgenre.csv"
    download_path = f"/lakehouse/default/{DATA_FOLDER}/raw"

    if not os.path.exists("/lakehouse/default"):
        raise FileNotFoundError(
            "Default lakehouse not found, please add a lakehouse and restart the session."
        )
    os.makedirs(download_path, exist_ok=True)
    if not os.path.exists(f"{download_path}/{fname}"):
        r = requests.get(f"{remote_url}/{fname}", timeout=30)
        with open(f"{download_path}/{fname}", "wb") as f:
            f.write(r.content)
    print("Downloaded demo data files into lakehouse.")

Lire les données de Lakehouse

raw_df = spark.read.csv(f"{DATA_FOLDER}/raw/{DATA_FILE}", header=True, inferSchema=True)

display(raw_df.limit(20))

Étape 2 : traitement préalable des données

Propre de données

df = (
    raw_df.select([TEXT_COL, LABEL_COL])
    .where(F.col(LABEL_COL).isin(LABELS))
    .dropDuplicates([TEXT_COL])
    .cache()
)

display(df.limit(20))

Gérer les données déséquilibrés

cb = ClassBalancer().setInputCol(LABEL_COL)

df = cb.fit(df).transform(df)
display(df.limit(20))

Tokenize

## text transformer
tokenizer = Tokenizer(inputCol=TEXT_COL, outputCol="tokens")
stopwords_remover = StopWordsRemover(inputCol="tokens", outputCol="filtered_tokens")

## build the pipeline
pipeline = Pipeline(stages=[tokenizer, stopwords_remover])

token_df = pipeline.fit(df).transform(df)

display(token_df.limit(20))

Visualisation

Affichez un Wordcloud pour chaque classe.

# WordCloud
for label in LABELS:
    tokens = (
        token_df.where(F.col(LABEL_COL) == label)
        .select(F.explode("filtered_tokens").alias("token"))
        .where(F.col("token").rlike(r"^\w+$"))
    )

    top50_tokens = (
        tokens.groupBy("token").count().orderBy(F.desc("count")).limit(50).collect()
    )

    # Generate a word cloud image
    wordcloud = WordCloud(
        scale=10,
        background_color="white",
        random_state=42,  # Make sure the output is always the same for the same input
    ).generate_from_frequencies(dict(top50_tokens))

    # Display the generated image the matplotlib way:
    plt.figure(figsize=(10, 10))
    plt.title(label, fontsize=20)
    plt.axis("off")
    plt.imshow(wordcloud, interpolation="bilinear")

Vectoriser

Nous utilisons word2vec pour vectoriser le texte.

## label transformer
label_indexer = StringIndexer(inputCol=LABEL_COL, outputCol="labelIdx")
vectorizer = Word2Vec(
    vectorSize=word2vec_size,
    minCount=min_word_count,
    inputCol="filtered_tokens",
    outputCol="features",
)

## build the pipeline
pipeline = Pipeline(stages=[label_indexer, vectorizer])
vec_df = (
    pipeline.fit(token_df)
    .transform(token_df)
    .select([TEXT_COL, LABEL_COL, "features", "labelIdx", "weight"])
)

display(vec_df.limit(20))

Étape 3 : Formation et évaluation des modèles

Nous avons nettoyé le jeu de données, traité les données déséquilibrés, tokenisé le texte, affiché le nuage de mots et vectorisé le texte.

Ensuite, nous entraînons un modèle de régression linéaire pour classifier le texte vectorisé.

Fractionner le jeu de données en apprentissage et test

(train_df, test_df) = vec_df.randomSplit((0.8, 0.2), seed=42)

Créer le modèle

lr = (
    LogisticRegression()
    .setMaxIter(max_iter)
    .setFeaturesCol("features")
    .setLabelCol("labelIdx")
    .setWeightCol("weight")
)

Entraîner le modèle avec la validation croisée

param_grid = (
    ParamGridBuilder()
    .addGrid(lr.regParam, [0.03, 0.1, 0.3])
    .addGrid(lr.elasticNetParam, [0.0, 0.1, 0.2])
    .build()
)

if len(LABELS) > 2:
    evaluator_cls = MulticlassClassificationEvaluator
    evaluator_metrics = ["f1", "accuracy"]
else:
    evaluator_cls = BinaryClassificationEvaluator
    evaluator_metrics = ["areaUnderROC", "areaUnderPR"]
evaluator = evaluator_cls(labelCol="labelIdx", weightCol="weight")

crossval = CrossValidator(
    estimator=lr, estimatorParamMaps=param_grid, evaluator=evaluator, numFolds=k_folds
)

model = crossval.fit(train_df)

Évaluer le modèle

predictions = model.transform(test_df)

display(predictions)
log_metrics = {}
for metric in evaluator_metrics:
    value = evaluator.evaluate(predictions, {evaluator.metricName: metric})
    log_metrics[metric] = value
    print(f"{metric}: {value:.4f}")
metrics = ComputeModelStatistics(
    evaluationMetric="classification", labelCol="labelIdx", scoredLabelsCol="prediction"
).transform(predictions)
display(metrics)
# collect confusion matrix value
cm = metrics.select("confusion_matrix").collect()[0][0].toArray()
print(cm)

# plot confusion matrix
sns.set(rc={"figure.figsize": (6, 4.5)})
ax = sns.heatmap(cm, annot=True, fmt=".20g")
ax.set_title("Confusion Matrix")
ax.set_xlabel("Predicted label")
ax.set_ylabel("True label")

Modèle de journal et de chargement avec MLflow

Maintenant que nous disposons d’un modèle dont nous sommes satisfaits, nous pouvons l’enregistrer pour une utilisation ultérieure. Ici, nous utilisons MLflow pour journaliser les métriques/modèles et charger les modèles à des fins de prédiction.

# setup mlflow
mlflow.set_experiment(EXPERIMENT_NAME)
# log model, metrics and params
with mlflow.start_run() as run:
    print("log model:")
    mlflow.spark.log_model(
        model,
        f"{EXPERIMENT_NAME}-lrmodel",
        registered_model_name=f"{EXPERIMENT_NAME}-lrmodel",
        dfs_tmpdir="Files/spark",
    )

    print("log metrics:")
    mlflow.log_metrics(log_metrics)

    print("log parameters:")
    mlflow.log_params(
        {
            "word2vec_size": word2vec_size,
            "min_word_count": min_word_count,
            "max_iter": max_iter,
            "k_folds": k_folds,
            "DATA_FILE": DATA_FILE,
        }
    )

    model_uri = f"runs:/{run.info.run_id}/{EXPERIMENT_NAME}-lrmodel"
    print("Model saved in run %s" % run.info.run_id)
    print(f"Model URI: {model_uri}")
# load model back
loaded_model = mlflow.spark.load_model(model_uri, dfs_tmpdir="Files/spark")

# verify loaded model
predictions = loaded_model.transform(test_df)
display(predictions)