[JAVA] Introduction à l'apprentissage automatique avec Spark "Price Estimate" # 2 Prétraitement des données (gestion des variables de catégorie)

Aperçu

-Introduction à l'apprentissage automatique avec ** Java ** et ** Apache Spark **

environnement

Pré-traitement en tenant compte du type de données

Dernière fois est passé à la lecture des données CSV sous forme de données tabulaires Spark.

Maintenant, jetons un coup d'œil aux données utilisées pour l'apprentissage cette fois.

Les données peuvent être trouvées ici [https://raw.githubusercontent.com/riversun/spark-gradient-boosting-tree-regression-example/master/dataset/gem_price_ja.csv).

Ce sont les données.

gem_price_ja.csv


id,material,shape,weight,brand,shop,price
0,argent,bracelet,56,Marques d'outre-mer célèbres,Grand magasin,40864
1,or,bague,48,Marque nationale célèbre,Magasin géré directement,63055
2,Dia,Des boucles d'oreilles,37,Marque nationale célèbre,Magasin géré directement,112159
3,Dia,Collier,20,Marques d'outre-mer célèbres,Magasin géré directement,216053
4,Dia,Collier,33,Marques d'outre-mer célèbres,Grand magasin,219666
5,argent,broche,55,Marque nationale célèbre,Grand magasin,16482
6,platine,broche,58,Marque super célèbre à l'étranger,Magasin géré directement,377919
7,or,Des boucles d'oreilles,49,Marque nationale célèbre,Magasin géré directement,60484
8,argent,Collier,59,Sans marque,Boutique pas chère,6256
・ ・ ・ ・

Données qualitatives et quantitatives

En passant, si vous regardez quelles valeurs sont disponibles pour chaque nom de variable (matériau, forme, etc.), vous pouvez les organiser comme suit.

image.png

De cette façon, les données de gauche (** matière, marque, boutique, forme **) sont la valeur de la chaîne indiquant la catégorie. Ces données qui ne peuvent pas être mesurées directement en tant que valeur numérique sont appelées ** données qualitatives **.

En revanche, les données de droite (** poids, prix **) sont une valeur numérique qui prend des valeurs continues. Les données qui peuvent être directement mesurées sous forme de valeur numérique ou qui peuvent être calculées selon les quatre règles sont appelées ** données quantitatives **.

Variable catégorielle et variable continue

Comme mentionné ci-dessus, les variables telles que ** matériau, marque, magasin, forme ** qui prennent des ** données qualitatives ** sont appelées ** variables catégorielles **.

De plus, les ** données quantitatives **, c'est-à-dire les variables telles que ** poids, prix ** qui prennent des valeurs continues (valeurs continues), sont appelées ** variables catégorielles **.

image.png

Gestion des variables catégorielles

Puisque l'apprentissage automatique est un calcul numérique après tout, les données qualitatives des variables catégorielles doivent être converties en une valeur numérique avant le traitement.

Examinons de plus près les variables catégorielles.

Les variables catégoriques ** matériau ** et ** forme ** sont le matériau de l'accessoire et la forme après traitement (** forme **), mais il existe une différence sémantique entre elles.

image.png

** matériau ** sont *** diamant, platine, or, argent ***, mais nous savons par expérience que les diamants sont plus chers que l'argent, donc ** le matériau ** a Il semble y avoir une hiérarchie dans les données.

D'autre part, ** forme ** est *** bagues, colliers, boucles d'oreilles, broches, bracelets ***, mais il n'y a pas d'ordre dans lequel les bracelets sont supérieurs aux bagues, et toutes les options sont sur un pied d'égalité. Semble être.

Les variables de catégorie sont divisées en variables ordinales et variables nominales.

Ceux dans lesquels les valeurs des variables catégorielles sont ordonnées et dont la grandeur peut être comparée sont appelés ** Variables ordinales ** (ou variables d'échelle ordinales). [^ 1]

[^ 1]: Dans cet exemple, il semble que l'or soit plus cher que l'or et le platine du même poids à l'heure actuelle, il se peut donc que l'on ne sache pas lequel est le plus élevé, mais dans un exemple facile à comprendre ** haut, milieu, bas * Les variables avec * ou ** élevé, moyen, faible **, etc. peuvent être clairement définies comme des variables ordinales.

En revanche, les valeurs des variables catégorielles qui ne peuvent pas être ordonnées sont appelées ** Variables nominales ** (ou variables d'échelle nominales).

C'est le contenu qui apparaît au début du manuel sur les statistiques, mais il est important de s'en souvenir car il s'agit de connaissances importantes pour faire progresser l'apprentissage automatique.

En regardant les données cette fois, cela ressemble à ce qui suit.

image.png

Cette fois, nous traiterons les variables catégorielles comme des variables catégorielles et des variables continues sans les distinguer clairement en variables séquentielles et variables nominales.

Quantifier (indexer) les variables catégorielles

Comme mentionné ci-dessus, les variables catégorielles doivent être quantifiées pour l'apprentissage automatique.

Il existe plusieurs techniques de quantification, mais cette fois nous allons simplement indexer chaque variable.

↓ Une telle image.

image.png

Gérer les variables catégorielles dans Spark

Maintenant, écrivons le code pour quantifier les variables catégorielles. Commencez par exécuter le code suivant.


import org.apache.spark.ml.feature.StringIndexer;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;

public class GBTRegressionStep02 {

  public static void main(String[] args) {

    System.setProperty("hadoop.home.dir", "c:\\Temp\\winutil\\");

    org.apache.log4j.Logger.getLogger("org").setLevel(org.apache.log4j.Level.ERROR);
    org.apache.log4j.Logger.getLogger("akka").setLevel(org.apache.log4j.Level.ERROR);

    SparkSession spark = SparkSession
        .builder()
        .appName("GradientBoostingTreeGegression")
        .master("local[*]")
        .getOrCreate();

    Dataset<Row> dataset = spark
        .read()
        .format("csv")
        .option("header", "true")
        .option("inferSchema", "true")
        .load("dataset/gem_price_ja.csv");

    StringIndexer materialIndexer = new StringIndexer()// (1)
        .setInputCol("material")// (2)
        .setOutputCol("materialIndex");// (3)

    Dataset<Row> materialIndexAddedDataSet = materialIndexer.fit(dataset).transform(dataset);// (4)

    materialIndexAddedDataSet.show(10);// (5)

  }

}

Commentaire de code

** (1) ** ・ ・ ・ Utilisez *** StringIndexer *** pour quantifier (indexer) les variables catégorielles. Il a le rôle de saisir une certaine variable et de sortir le résultat codé (converti) comme une autre variable. L'encodage ici n'est qu'un processus de conversion d'une chaîne de caractères en une valeur numérique.

** (2) ** ***. SetInputCol ("material") *** ・ ・ ・ Spécifiez le nom de la variable à saisir dans ce *** StringIndexer ***. Les ** variables ** peuvent être considérées comme les ** colonnes ** de la table, car elles sont dans le ** Dataset ** tabulaire lorsque les données CSV sont chargées dans Spark. col est ** colonne ** col, n'est-ce pas? En d'autres termes, le nom de la colonne du côté entrée de *** StringIndexer ** est *** "material" ***

** (3) ** ***. SetOutputCol ("materialIndex") *** ・ ・ ・ Spécifiez ici le nom de la colonne sur le côté sortie.

** (4) ** ・ ・ ・ ** Créez un modèle d'apprentissage pour numériser des variables catégorielles avec la méthode fit () **. Lorsqu'un ensemble de données est donné au modèle d'apprentissage résultant avec la méthode de transformation, un nouvel ensemble de données est généré.

Créer un modèle d'apprentissage pour quantifier les variables catégorielles avec la méthode fit

mais, Ici, la méthode ** fit () ** de l'objet *** StringIndexer *** nommé ** materialIndexer ** est évidente, mais elle n'est pas réellement apprise. Il crée simplement un ** périphérique ** [^ 2] qui numérise la chaîne de caractères donnée en tant que variable catégorielle.

[^ 2]: *** StringIndexer *** hérite d'une classe appelée ** Estimator **. ** Estimator ** est à l'origine une classe pour ** apprenants **, mais lors du traitement du pipeline, qui est une fonctionnalité de ** spark.ml **, comme *** StringIndexer *** En équipant le système de prétraitement et l'apprenant d'une même interface, c'est une telle interface pour réaliser le flux de ** pré-traitement → pré-traitement → apprentissage ** sous forme de pipeline.

Dans la méthode ** transform **, le processus de création d'une nouvelle variable indexée appelée ** materialIndex ** est exécuté à partir du nom de colonne ** material ** dans l'ensemble de données qui a été réellement entré.

Je pense que c'est le moins clair, mais je comprendrai le sens plus tard, alors maintenant c'est OK avec un sort.

** (5) ** ・ ・ ・ ** Affiche l'ensemble de données après le traitement de StringIndexer **.

Lorsque j'exécute le code, cela ressemble à ceci:

image.png

Une colonne appelée ** materialIndex ** a été ajoutée à droite!

De cette manière, les variables catégorielles ne peuvent pas être utilisées dans l'apprentissage automatique à moins qu'elles ne soient quantifiées d'une manière ou d'une autre, de sorte que d'autres variables catégorielles sont également quantifiées.

Donc, je vais essayer de quantifier (indexer) d'autres variables catégoriques ** forme, marque, boutique ** en utilisant *** StringIndexer ** de la même manière.

Ensuite, ce sera comme suit

import org.apache.spark.ml.feature.StringIndexer;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;

public class GBTRegressionStep02_part02 {

  public static void main(String[] args) {
    SparkSession spark = SparkSession
        .builder()
        .appName("GradientBoostingTreeGegression")
        .master("local[*]")
        .getOrCreate();

    Dataset<Row> dataset = spark
        .read()
        .format("csv")
        .option("header", "true")
        .option("inferSchema", "true")
        .load("dataset/gem_price_ja.csv");

    StringIndexer materialIndexer = new StringIndexer()// (1)
        .setInputCol("material")
        .setOutputCol("materialIndex");

    StringIndexer shapeIndexer = new StringIndexer()// (2)
        .setInputCol("shape")
        .setOutputCol("shapeIndex");

    StringIndexer brandIndexer = new StringIndexer()// (3)
        .setInputCol("brand")
        .setOutputCol("brandIndex");

    StringIndexer shopIndexer = new StringIndexer()// (4)
        .setInputCol("shop")
        .setOutputCol("shopIndex");

    Dataset<Row> dataset1 = materialIndexer.fit(dataset).transform(dataset);// (5)
    Dataset<Row> dataset2 = shapeIndexer.fit(dataset).transform(dataset1);// (6)
    Dataset<Row> dataset3 = brandIndexer.fit(dataset).transform(dataset2);// (7)
    Dataset<Row> dataset4 = shopIndexer.fit(dataset).transform(dataset3);// (8)

    dataset4.show(10);
  }
}

Pour ** (1) à (4) **, *** StringIndexer *** est préparé pour chaque variable. ** (5) - (8) ** sont indexés l'un après l'autre dans l'ordre de ** materialIndexer → shapeIndexer → brandIndexer → shopIndexer ** pour créer un nouvel ensemble de données.

Lorsque vous faites cela, vous obtenez:

image.png

Vous pouvez voir que ** materialIndex, shapeIndex, brandIndex, shopIndex ** ont été ajoutés à droite.

Au fait, ↓ Est-ce quelque chose qui ne peut pas être fait?

    Dataset<Row> dataset1 = materialIndexer.fit(dataset).transform(dataset);// (5)
    Dataset<Row> dataset2 = shapeIndexer.fit(dataset).transform(dataset1);// (6)
    Dataset<Row> dataset3 = brandIndexer.fit(dataset).transform(dataset2);// (7)
    Dataset<Row> dataset4 = shopIndexer.fit(dataset).transform(dataset3);// (8)

Comme c'est joli.

Lorsqu'un certain processus est terminé, tel que ** materialIndexer-> shapeIndexer-> brandIndexer-> shopIndexer **, vous pouvez écrire plus intelligemment que le processus suivant est effectué avec ce processus en entrée.

C'est le mécanisme appelé ** Pipeline ** que nous verrons ensuite.

Prétraitement des données de pipeline

↓ Traitement

Before


    Dataset<Row> dataset1 = materialIndexer.fit(dataset).transform(dataset);
    Dataset<Row> dataset2 = shapeIndexer.fit(dataset).transform(dataset1);
    Dataset<Row> dataset3 = brandIndexer.fit(dataset).transform(dataset2);
    Dataset<Row> dataset4 = shopIndexer.fit(dataset).transform(dataset3);

Vous pouvez le réécrire comme ↓ en utilisant *** Pipeline ***.

After


    Pipeline pipeline = new Pipeline()
        .setStages(new PipelineStage[] { materialIndexer, shapeIndexer, brandIndexer, shopIndexer });// (1)

    PipelineModel pipelineModel = pipeline.fit(dataset);// (2)

    Dataset<Row> indexedDataset = pipelineModel.transform(dataset);// (3)

** (1) ** ・ ・ ・ Créez un objet *** Pipeline *** comme celui-ci et décrivez *** StringIndexer *** dans l'ordre dans lequel vous souhaitez exécuter le traitement avec *** setStages ***. Je vais. En les organisant simplement de cette manière, ces séries de processus peuvent être exécutées en tant que groupe *** Pipeline ***.

** (2) ** ・ ・ ・ *** Obtenez pipelineModel avec pipeline # fit ***. À ce stade, j'utilise toujours uniquement *** StringIndexer ***, donc je n'ai inclus aucun traitement pour l'apprentissage, mais lorsque le traitement pour l'apprentissage est inclus, il peut être obtenu avec la méthode *** fit () *** *** PipelineModel *** signifie un modèle d'entraînement basé sur l'ensemble de données spécifié.

** (3) ** ・ ・ ・ *** Lorsque PipelineModel # transform *** est exécuté, une série de processus *** StringIndexer *** est exécutée à la fois et un nouvel ensemble de données est créé en conséquence. Cela signifie également que si le Pipeline contient un traitement pour l'entraînement, le modèle d'apprentissage sera appliqué à l'ensemble de données spécifié.

Le code source et les résultats d'exécution sont affichés ci-dessous.


import org.apache.spark.ml.Pipeline;
import org.apache.spark.ml.PipelineModel;
import org.apache.spark.ml.PipelineStage;
import org.apache.spark.ml.feature.StringIndexer;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;

public class GBTRegressionStep02_part03 {

  public static void main(String[] args) {

    System.setProperty("hadoop.home.dir", "c:\\Temp\\winutil\\");

    org.apache.log4j.Logger.getLogger("org").setLevel(org.apache.log4j.Level.ERROR);
    org.apache.log4j.Logger.getLogger("akka").setLevel(org.apache.log4j.Level.ERROR);

    SparkSession spark = SparkSession
        .builder()
        .appName("GradientBoostingTreeGegression")
        .master("local[*]")
        .getOrCreate();

    Dataset<Row> dataset = spark
        .read()
        .format("csv")
        .option("header", "true")
        .option("inferSchema", "true")
        .load("dataset/gem_price_ja.csv");

    StringIndexer materialIndexer = new StringIndexer()
        .setInputCol("material")
        .setOutputCol("materialIndex");

    StringIndexer shapeIndexer = new StringIndexer()
        .setInputCol("shape")
        .setOutputCol("shapeIndex");

    StringIndexer brandIndexer = new StringIndexer()
        .setInputCol("brand")
        .setOutputCol("brandIndex");

    StringIndexer shopIndexer = new StringIndexer()
        .setInputCol("shop")
        .setOutputCol("shopIndex");

    Pipeline pipeline = new Pipeline()
        .setStages(new PipelineStage[] { materialIndexer, shapeIndexer, brandIndexer, shopIndexer });// (1)

    PipelineModel pipelineModel = pipeline.fit(dataset);// (2)

    Dataset<Row> indexedDataset = pipelineModel.transform(dataset);// (3)
    indexedDataset.show(10);

  }

}

Résultat d'exécution

image.png

Le résultat est le même car ils font la même chose sémantiquement.

Rédigez le processus d'indexation un peu plus intelligemment

À propos, la partie exécution du processus a été actualisée à l'aide de *** Pipeline ***, mais la partie suivante est également un peu redondante.

    StringIndexer materialIndexer = new StringIndexer()
        .setInputCol("material")
        .setOutputCol("materialIndex");

    StringIndexer shapeIndexer = new StringIndexer()
        .setInputCol("shape")
        .setOutputCol("shapeIndex");

    StringIndexer brandIndexer = new StringIndexer()
        .setInputCol("brand")
        .setOutputCol("brandIndex");

    StringIndexer shopIndexer = new StringIndexer()
        .setInputCol("shop")
        .setOutputCol("shopIndex");

    Pipeline pipeline = new Pipeline()
        .setStages(new PipelineStage[] { materialIndexer, shapeIndexer, brandIndexer, shopIndexer });/

C'est plus un problème d'écriture Java que Spark, mais rendons-le un peu plus propre.

Je l'ai réécrit comme suit.

    List<String> categoricalColNames = Arrays.asList("material", "shape", "brand", "shop");// (1)

    List<StringIndexer> stringIndexers = categoricalColNames.stream()
        .map(col -> new StringIndexer()
            .setInputCol(col)
            .setOutputCol(col + "Index"))// (2)
        .collect(Collectors.toList());

    Pipeline pipeline = new Pipeline()
        .setStages(stringIndexers.toArray(new PipelineStage[0]));// (3)

** (1) ** ・ ・ ・ Définissez le nom de la variable de catégorie sur Liste

** (2) ** ・ ・ ・ *** List # stream *** est utilisé pour générer *** StringIndexer ***. Dans *** setOutputCol ***, le nom de la colonne côté sortie est ** nom de la variable de catégorie + "Index" ** (c'est-à-dire ** materialIndex, shapeIndex, brandIndex, shopIndex **). Le résultat du traitement sera ** Liste ** de *** StringIndexer ***.

** (3) ** ・ ・ ・ Réglez sur l'étape Pipeline de sorte que ce soit un tableau de *** StringIndexer ***

Ainsi, le code source de cette époque est résumé comme suit. C'était plutôt rafraîchissant.


import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.spark.ml.Pipeline;
import org.apache.spark.ml.PipelineModel;
import org.apache.spark.ml.PipelineStage;
import org.apache.spark.ml.feature.StringIndexer;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;

public class GBTRegressionStep02_part04 {

  public static void main(String[] args) {

    // System.setProperty("hadoop.home.dir", "c:\\Temp\\winutil\\");//for windows

    org.apache.log4j.Logger.getLogger("org").setLevel(org.apache.log4j.Level.ERROR);
    org.apache.log4j.Logger.getLogger("akka").setLevel(org.apache.log4j.Level.ERROR);

    SparkSession spark = SparkSession
        .builder()
        .appName("GradientBoostingTreeGegression")
        .master("local[*]")
        .getOrCreate();

    Dataset<Row> dataset = spark
        .read()
        .format("csv")
        .option("header", "true")
        .option("inferSchema", "true")
        .load("dataset/gem_price_ja.csv");

    List<String> categoricalColNames = Arrays.asList("material", "shape", "brand", "shop");

    List<StringIndexer> stringIndexers = categoricalColNames.stream()
        .map(col -> new StringIndexer()
            .setInputCol(col)
            .setOutputCol(col + "Index"))
        .collect(Collectors.toList());

    Pipeline pipeline = new Pipeline()
        .setStages(stringIndexers.toArray(new PipelineStage[0]));

    PipelineModel pipelineModel = pipeline.fit(dataset);

    Dataset<Row> indexedDataset = pipelineModel.transform(dataset);
    indexedDataset.show(10);

  }

}

Si vous faites cela, vous obtiendrez un ensemble de données avec toutes les variables catégorielles indexées, comme auparavant.

Politique de quantification de StringIndexer

J'ai mentionné plus tôt que les variables catégorielles peuvent être divisées en variables ordinales et en variables nominales, mais quel type de politique le ** StringIndexer ** de Spark attribue-t-il des numéros d'index? C'est ça?

** StringIndexer ** trie par numérotation comme un index, mais la règle de classement par défaut est ** fréquence d'occurrence **.

En plus de la ** fréquence d'occurrence **, vous pouvez également spécifier ** l'ordre alphabétique **.

Un exemple de réglage est présenté ci-dessous.

Fréquence d'apparition-Ordre décroissant(Défaut)


    StringIndexer materialIndexer1 = new StringIndexer()
        .setStringOrderType("frequencyDesc")
        .setInputCol("material")
        .setOutputCol("materialIndex");

Ordre alphabétique-Ordre décroissant


    StringIndexer materialIndexer2 = new StringIndexer()
        .setStringOrderType("alphabetDesc")
        .setInputCol("material")
        .setOutputCol("materialIndex");

Ordre alphabétique-ordre croissant


    StringIndexer materialIndexer3 = new StringIndexer()
        .setStringOrderType("alphabetAsc")
        .setInputCol("material")
        .setOutputCol("materialIndex");

Je ne vais pas l'utiliser cette fois, mais si vous voulez rendre l'ordre d'index significatif, vous pouvez ajuster les noms de variable et trier par ** setStringOrderType **.

Résumé du traitement du pipeline Apache Spark ** spark.ml **

image.png

** Continuation vers La prochaine fois "# 3 S'entraîner avec les données d'entraînement et créer [Moteur d'estimation de prix]" **

La prochaine fois, nous formerons les données et créerons un "moteur d'estimation de prix".

Recommended Posts

Introduction à l'apprentissage automatique avec Spark "Price Estimate" # 2 Prétraitement des données (gestion des variables de catégorie)
Introduction à l'apprentissage automatique avec Spark "Estimation de prix" # 3 Apprenons avec les données d'entraînement et créons un [moteur d'estimation de prix]
Premiers pas avec le Machine Learning avec Spark "Price Estimate" # 1 Chargement des ensembles de données avec Apache Spark (Java)
[Introduction à l'informatique Partie 3: Essayons l'apprentissage automatique] Implémentons la méthode de moyennage k dans Java-Center of data set-
[Apprentissage automatique avec Apache Spark] Associez l'importance (Feature Importance) d'une variable de modèle d'arbre au nom de la variable (nom de la variable explicative)
Introduction à Spring Boot + In-Memory Data Grid (traitement des événements)
[Apprentissage automatique avec Apache Spark] Vecteur fragmenté (vecteur fragmenté) et vecteur dense (vecteur dense)