Régression probit avec C# — Visual Studio Magazine

Le laboratoire de science des données

Régression probit avec C#

La régression probit (“unité de probabilité”) est une technique d’apprentissage automatique classique qui peut être utilisée pour la classification binaire – prédire un résultat qui ne peut être qu’une des deux valeurs discrètes. Par exemple, vous pouvez prédire la satisfaction au travail (0 = faible satisfaction, 1 = grande satisfaction) d’une personne en fonction de son sexe, de son âge, de son type d’emploi et de son revenu.

La régression probit est très similaire à la régression logistique et les deux techniques donnent généralement des résultats similaires. La régression probit a tendance à être utilisée le plus souvent avec des données financières et économiques, mais la régression probit et la régression logistique peuvent être utilisées pour tout type de problème de classification binaire.

Figure 1 : Régression probit utilisant C# en action
[Click on image for larger view.] Figure 1: Régression probit utilisant C # en action

Une bonne façon de voir où va cet article est de jeter un coup d’œil à la capture d’écran d’un programme de démonstration dans Figure 1. La démo met en place 40 éléments de données qui représentent les employés. Il existe quatre variables prédictives : sexe (homme = -1, femme = +1), âge (divisé par 100), type d’emploi (mgmt. = 1 0 0, supp = 0 1 0, tech = 0 0 1), et revenu (divisé par 100 000 $). Chaque employé a une valeur de satisfaction au travail codée comme 0 = faible, 1 = élevé. L’objectif est de prédire la satisfaction au travail à partir des quatre valeurs prédictives.

La démo met également en place un ensemble de données de test de 8 éléments pour évaluer le modèle après la formation. Les ensembles de données d’entraînement et de test sont complètement artificiels.

Le programme de démonstration utilise les données de formation de 40 éléments et une forme modifiée de descente de gradient stochastique (SGD) pour créer un modèle de prédiction de régression probit. Après la formation, le modèle obtient une précision de 75 % sur les données de formation (30 corrects sur 40), ainsi qu’une précision de 75 % sur les données de test (6 corrects sur 8).

Le programme de démonstration se termine en faisant une prédiction pour un nouvel élément de données jamais vu auparavant où sexe = homme, âge = 24 ans, type d’emploi = gestion, revenu = 48 500 $. La sortie du modèle est une valeur p de 0,6837. Étant donné que la valeur de p est supérieure à 0,5, la prédiction est satisfaction au travail = élevée. Si la valeur p avait été inférieure à 0,5, la prédiction aurait été satisfaction au travail = faible.

Cet article suppose que vous avez des compétences de programmation intermédiaires ou supérieures avec le langage C #, mais ne suppose pas que vous savez quoi que ce soit sur la régression probit. Le code de démonstration complet et les données associées sont présentés dans cet article. Le code source et les données sont également disponibles dans le téléchargement qui l’accompagne. Toutes les vérifications d’erreur normales ont été supprimées pour garder les idées principales aussi claires que possible.

Comprendre la régression probit
Supposons, comme dans la démo, que vous souhaitiez prédire la satisfaction au travail en fonction du sexe, de l’âge, du type d’emploi et du revenu. Les valeurs d’entrée correspondant à (homme, 24 ans, mgmt, 48 500 $) sont (x0, x1, x2, x3, x4, x5) = (-1, 0,24, 1, 0, 0, 0,485). Les valeurs numériques sont normalisées de sorte qu’elles soient toutes comprises entre 0 et 1, les valeurs booléennes sont codées moins un plus un et les valeurs catégorielles sont codées à chaud. Il existe plusieurs alternatives de normalisation et d’encodage, mais le schéma utilisé par la démo est simple et fonctionne bien dans la pratique.

Chaque valeur d’entrée a une constante numérique associée appelée poids. Il existe une constante supplémentaire appelée biais. Pour le problème de démonstration, les poids du modèle sont (w0, w1, w2, w3, w4, w5) = (-0,057, 0,099, 0,460, 0,041, -0,508, -0,113) et le biais du modèle est b = -0,0077.

Figure 2 : La fonction phi()
[Click on image for larger view.] Figure 2: La fonction phi()

La première étape consiste à calculer une valeur z comme la somme des produits des poids multipliés par les entrées, plus le biais :

z = (w0 * x0) +  (w1 * x1) +  (w2 * x2) +  (w3 * x3) +  (w4 * x4) +  (w5 * x5) +  b
  = (-0.057 * -1) + (0.099 * 0.24) + (0.460 * 1) + (0.041 * 0) + (-0.508 * 0) +
      (-0.113 * 0.485) + (-0.0077)
  = 0.0570 + 0.0238 + 0.4600 + 0.0000 + 0.0000 + (-0.0548) + (-0.0077)
  = 0.4783

La deuxième étape consiste à transmettre la valeur z calculée à la fonction phi() :

p = phi(0.4783)
  = 0.6837

Le résultat de la valeur p sera une valeur comprise entre 0,0 et 1,0 où une valeur inférieure à 0,5 indique une prédiction de classe 0 (faible satisfaction dans la démo) et une valeur p supérieure à 0,5 indique une prédiction de classe 1 (satisfaction élevée). Par conséquent, pour cet exemple, la prédiction est que l’employé a une grande satisfaction au travail.

La fonction phi() est la fonction de densité cumulée (CDF) de la distribution normale standard. Pour toute valeur z en entrée, la fonction phi() renvoie l’aire sous la distribution normale de moins l’infini à z. Le graphique dans Figure 2 montre la fonction phi() où une entrée de z = 0,4783 renvoie p = 0,6837.

Les deux principaux défis lors de la création d’un modèle de régression probit sont 1.) former le modèle pour trouver les valeurs des poids et du biais, et 2.) écrire du code pour implémenter la fonction phi().

Le programme de démonstration
Le programme de démonstration complet, avec quelques modifications mineures pour gagner de la place, est présenté dans Liste 1. Pour créer le programme, j’ai lancé Visual Studio et créé une nouvelle application console C# .NET Core nommée ProbitRegression. J’ai utilisé Visual Studio 2022 (édition communautaire gratuite) avec .NET Core 6.0, mais la démo n’a pas de dépendances significatives, donc n’importe quelle version de Visual Studio et de la bibliothèque .NET fonctionnera correctement. Vous pouvez également utiliser le programme Visual Studio Code.

Une fois le code du modèle chargé, dans la fenêtre de l’éditeur, j’ai supprimé toutes les références d’espace de noms inutiles, ne laissant que la référence à l’espace de noms système de niveau supérieur. Dans la fenêtre de l’Explorateur de solutions, j’ai cliqué avec le bouton droit sur le fichier Program.cs, je l’ai renommé en ProbitProgram.cs, plus descriptif, et j’ai autorisé Visual Studio à renommer automatiquement la classe Program en ProbitProgram.

Liste 1 :
Code de démonstration de régression probit

using System;  // .NET 6.0
namespace ProbitRegression
{
  class ProbitProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin probit regression demo ");
      Console.WriteLine("Predict job satisfaction " +
        "(0 = low, 1 = high) ");
      Random rnd = new Random(0);  // 28

      Console.WriteLine("Raw data: Sex, Age, Job, Income," +
        " Satisfaction looks like: ");
      Console.WriteLine("Male    66  mgmt  $52,100.00 |  low");
      Console.WriteLine("Female  35  tech  $86,300.00 |  high");
      Console.WriteLine("Male    27  supp  $47,900.00 |  high");
      Console.WriteLine(" . . . ");
      Console.WriteLine("Encoded and normed data looks like: ");
      Console.WriteLine("-1  0.66  1 0 0  0.52100  |  0 ");
      Console.WriteLine(" 1  0.35  0 0 1  0.86300  |  1 ");
      Console.WriteLine("-1  0.27  0 1 0  0.47900  |  1 ");
      Console.WriteLine(" . . . ");

      double[][] trainX = new double[40][];
      trainX[0] = new double[] { -1, 0.66, 1, 0, 0, 0.5210 };
      trainX[1] = new double[] { 1, 0.35, 0, 0, 1, 0.8630 };
      trainX[2] = new double[] { -1, 0.24, 0, 0, 1, 0.4410 };
      trainX[3] = new double[] { 1, 0.43, 0, 1, 0, 0.5170 };
      trainX[4] = new double[] { -1, 0.37, 1, 0, 0, 0.8860 };
      trainX[5] = new double[] { 1, 0.30, 0, 1, 0, 0.8790 };
      trainX[6] = new double[] { 1, 0.40, 1, 0, 0, 0.2020 };
      trainX[7] = new double[] { -1, 0.58, 0, 0, 1, 0.2650 };
      trainX[8] = new double[] { 1, 0.27, 1, 0, 0, 0.8480 };
      trainX[9] = new double[] { -1, 0.33, 0, 1, 0, 0.5600 };
      trainX[10] = new double[] { 1, 0.59, 0, 0, 1, 0.2330 };
      trainX[11] = new double[] { 1, 0.52, 0, 1, 0, 0.8700 };
      trainX[12] = new double[] { -1, 0.41, 1, 0, 0, 0.5170 };
      trainX[13] = new double[] { 1, 0.22, 0, 1, 0, 0.3500 };
      trainX[14] = new double[] { 1, 0.61, 0, 1, 0, 0.2980 };
      trainX[15] = new double[] { -1, 0.46, 1, 0, 0, 0.6780 };
      trainX[16] = new double[] { 1, 0.59, 1, 0, 0, 0.8430 };
      trainX[17] = new double[] { 1, 0.28, 0, 0, 1, 0.7730 };
      trainX[18] = new double[] { -1, 0.46, 0, 1, 0, 0.8930 };
      trainX[19] = new double[] { 1, 0.48, 0, 0, 1, 0.2920 };
      trainX[20] = new double[] { 1, 0.28, 1, 0, 0, 0.6690 };
      trainX[21] = new double[] { -1, 0.23, 0, 1, 0, 0.8970 };
      trainX[22] = new double[] { -1, 0.60, 1, 0, 0, 0.6270 };
      trainX[23] = new double[] { 1, 0.29, 0, 1, 0, 0.7760 };
      trainX[24] = new double[] { -1, 0.24, 0, 0, 1, 0.8750 };
      trainX[25] = new double[] { 1, 0.51, 1, 0, 0, 0.4090 };
      trainX[26] = new double[] { 1, 0.22, 0, 1, 0, 0.8910 };
      trainX[27] = new double[] { -1, 0.19, 0, 0, 1, 0.5380 };
      trainX[28] = new double[] { 1, 0.25, 0, 1, 0, 0.9000 };
      trainX[29] = new double[] { 1, 0.44, 0, 0, 1, 0.8980 };
      trainX[30] = new double[] { -1, 0.35, 1, 0, 0, 0.5380 };
      trainX[31] = new double[] { -1, 0.29, 0, 1, 0, 0.7610 };
      trainX[32] = new double[] { 1, 0.25, 1, 0, 0, 0.3450 };
      trainX[33] = new double[] { 1, 0.66, 1, 0, 0, 0.2210 };
      trainX[34] = new double[] { -1, 0.43, 0, 0, 1, 0.7450 };
      trainX[35] = new double[] { 1, 0.42, 0, 1, 0, 0.8520 };
      trainX[36] = new double[] { -1, 0.44, 1, 0, 0, 0.6580 };
      trainX[37] = new double[] { 1, 0.42, 0, 1, 0, 0.6970 };
      trainX[38] = new double[] { 1, 0.56, 0, 0, 1, 0.3680 };
      trainX[39] = new double[] { -1, 0.38, 1, 0, 0, 0.2600 };

      int[] trainY = new int[40] { 0, 0, 0, 1, 1, 0, 0, 0, 0, 1,
        1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0,
        1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1 };

      double[][] testX = new double[8][];
      testX[0] = new double[] { 1, 0.36, 0, 0, 1, 0.8670 };
      testX[1] = new double[] { -1, 0.26, 0, 0, 1, 0.4310 };
      testX[2] = new double[] { 1, 0.42, 0, 1, 0, 0.5190 };
      testX[3] = new double[] { -1, 0.37, 1, 0, 0, 0.8260 };
      testX[4] = new double[] { 1, 0.31, 0, 1, 0, 0.8890 };
      testX[5] = new double[] { 1, 0.42, 1, 0, 0, 0.2040 };
      testX[6] = new double[] { -1, 0.57, 0, 0, 1, 0.2750 };
      testX[7] = new double[] { -1, 0.32, 0, 1, 0, 0.5500 };

      int[] testY = new int[8] { 0, 0, 1, 1, 0, 0, 0, 1 };

      Console.WriteLine("Creating dim = 6 probit model ");
      int dim = 6; // number predictors

      double[] wts = new double[dim];
      double lo = -0.01; double hi = 0.01;
      for (int i = 0; i < wts.Length; ++i)
        wts[i] = (hi - lo) * rnd.NextDouble() + lo;
      double bias = 0.0;

      Console.WriteLine("Starting quasi-SGD training " +
        "with lr = 0.01 ");
      int maxEpochs = 100;
      double lr = 0.01;

      int N = trainX.Length;  // number training items
      int[] indices = new int[N];
      for (int i = 0; i < N; ++i)
        indices[i] = i;

      for (int epoch = 0; epoch < maxEpochs; ++epoch)
      {
        Shuffle(indices, rnd);
        for (int i = 0; i < N; ++i)  // each data item
        {
          int ii = indices[i];
          double[] x = trainX[ii];  // inputs
          double y = trainY[ii];    // target 0 or 1
          double p = ComputeOutput(x, wts, bias);  // [0.0, 1.0]

          for (int j = 0; j < dim; ++j)  // each predictor weight
            wts[j] += lr * x[j] * (y - p) *
              p * (1 - p);  // o-t error => t-o update
          bias += lr * (y - p) * p * (1 - p);  // update bias
        }

        if (epoch % 10 == 0)
        {
          double mse = Error(trainX, trainY, wts, bias);
          double acc = Accuracy(trainX, trainY, wts, bias);
          Console.WriteLine("epoch = " +
            epoch.ToString().PadLeft(4) +
            "  |  loss = " + mse.ToString("F4") +
            "  |  acc = " + acc.ToString("F4"));
        }
      }
      Console.WriteLine("Done ");

      Console.WriteLine("Trained model weights: ");
      ShowVector(wts, 3, false);
      Console.WriteLine("Model bias: " + bias.ToString("F4"));

      double accTrain = Accuracy(trainX, trainY, wts, bias);
      Console.WriteLine("Accuracy on train data = " +
        accTrain.ToString("F4"));

      double accTest = Accuracy(testX, testY, wts, bias);
      Console.WriteLine("Accuracy on test data = " +
        accTest.ToString("F4"));

      Console.WriteLine("Predicting satisfaction for: ");
      Console.WriteLine("Male  24  mgmt  $48,500.00 ");
      double[] xx = new double[] { -1, 0.24, 1, 0, 0, 0.485 };
      double pp = ComputeOutput(xx, wts, bias, true);
      Console.WriteLine("output p = " + pp.ToString("F4"));
      if (pp < 0.5)
        Console.WriteLine("satisfaction = low");
      else
        Console.WriteLine("satisfaction = high");

      Console.WriteLine("End probit regression demo ");
      Console.ReadLine();
    } // Main

    static double Phi(double z)
    {
      // cumulative density of Standard Normal
      // erf is Abramowitz and Stegun 7.1.26

      if (z < -6.0)
        return 0.0;
      if (z > 6.0)
        return 1.0;

      double a0 = 0.3275911;
      double a1 = 0.254829592;
      double a2 = -0.284496736;
      double a3 = 1.421413741;
      double a4 = -1.453152027;
      double a5 = 1.061405429;

      int sign = 0;
      if (z < 0.0)
        sign = -1;
      else
        sign = 1;

      double x = Math.Abs(z) / Math.Sqrt(2.0);  // inefficient
      double t = 1.0 / (1.0 + a0 * x);
      double erf = 1.0 - (((((a5 * t + a4) * t) +
        a3) * t + a2) * t + a1) * t * Math.Exp(-x * x);
      return 0.5 * (1.0 + (sign * erf));
    }

    static double ComputeOutput(double[] x, double[] wts,
      double bias, bool showZ = false)
    {
      double z = 0.0;
      for (int i = 0; i < x.Length; ++i)
        z += x[i] * wts[i];
      z += bias;
      if (showZ == true)
        Console.WriteLine("z = " + z.ToString("F4"));
      return Phi(z);  // probit
      // return Sigmoid(z);  // logistic sigmoid
    }

    static double Error(double[][] dataX, int[] dataY,
      double[] wts, double bias)
    {
      double sum = 0.0;
      int N = dataX.Length;
      for (int i = 0; i < N; ++i)
      {
        double[] x = dataX[i];
        int y = dataY[i];  // target, 0 or 1
        double p = ComputeOutput(x, wts, bias);
        sum += (p - y) * (p - y); // E = (o-t)^2 form
      }
      return sum / N; ;
    }

    static double Accuracy(double[][] dataX, int[] dataY,
      double[] wts, double bias)
    {
      int numCorrect = 0; int numWrong = 0;
      int N = dataX.Length;
      for (int i = 0; i < N; ++i)
      {
        double[] x = dataX[i];
        int y = dataY[i];  // actual, 0 or 1
        double p = ComputeOutput(x, wts, bias);
        if (y == 0 && p < 0.5 || y == 1 && p >= 0.5)
          ++numCorrect;
        else
          ++numWrong;
      }
      return (1.0 * numCorrect) / (numCorrect + numWrong);
    }

    static double AccuracyVerbose(double[][] dataX,
      int[] dataY, double[] wts, double bias)
    {
      int numCorrect = 0; int numWrong = 0;
      int N = dataX.Length;
      for (int i = 0; i < N; ++i)
      {
        Console.WriteLine("=========");
        double[] x = dataX[i];
        int y = dataY[i];  // actual, 0 or 1
        double p = ComputeOutput(x, wts, bias);

        Console.WriteLine(i);
        ShowVector(x, 1, false);
        Console.WriteLine("target = " + y);
        Console.WriteLine("computed = " + p.ToString("f4"));

        if (y == 0 && p < 0.5 || y == 1 && p >= 0.5)
        {
          Console.WriteLine("correct ");
          ++numCorrect;
        }
        else
        {
          Console.WriteLine("wrong ");
          ++numWrong;
        }
        Console.WriteLine("=========");
        Console.ReadLine();
      }
      return (1.0 * numCorrect) / (numCorrect + numWrong);
    }

    static void Shuffle(int[] arr, Random rnd)
    {
      int n = arr.Length;
      for (int i = 0; i < n; ++i)
      {
        int ri = rnd.Next(i, n);
        int tmp = arr[ri];
        arr[ri] = arr[i];
        arr[i] = tmp;
      }
    }

    static void ShowVector(double[] vector, int decs,
      bool newLine)
    {
      for (int i = 0; i < vector.Length; ++i)
        Console.Write(vector[i].ToString("F" + decs) + " ");
      Console.WriteLine("");
      if (newLine == true)
        Console.WriteLine("");
    }

  } // Program
} // ns

.

Leave a Comment