Le laboratoire de science des données
Classification naïve de Bayes à l’aide de C #
Dr. James McCaffrey de Microsoft Research présente un exemple complet étape par étape avec tout le code pour prédire le score d’optimisme d’une personne à partir de sa profession, de la couleur de ses yeux et de son pays.
Le but d’un problème de classification naïve de Bayes est de prédire une valeur discrète appelée étiquette de classe. L’idée est mieux expliquée par un exemple concret. Supposons que vous ayez un ensemble de 40 éléments de données où chaque élément se compose de la profession d’une personne (acteur, boulanger, employé ou plongeur), de la couleur des yeux (vert ou noisette), du pays (Italie, Japon ou Corée) et de son score d’optimisme de personnalité ( 0, 1 ou 2). Vous voulez prédire le score d’optimisme d’une personne à partir de sa profession, de la couleur de ses yeux et de son pays. Ceci est un exemple de classification multiclasse car la variable à prédire, l’optimisme, a trois valeurs possibles ou plus. Si la variable à prédire n’avait que deux valeurs possibles, le problème serait appelé classification binaire. Contrairement à certaines techniques d’apprentissage automatique, Naive Bayes peut gérer à la fois les problèmes de classification multiclasses et binaires.
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 – profession, couleur des yeux, pays, optimisme. Les premiers éléments de données sont :
actor green korea 1 baker green italy 0 diver hazel japan 0 diver green japan 1 clerk hazel japan 2
Notez que toutes les valeurs de données sont catégorielles (non numériques). Il s’agit d’une caractéristique clé de la technique de classification bayésienne naïve présentée dans cet article. Si vous disposez de données numériques, telles que l’âge d’une personne en années, vous pouvez regrouper les données en catégories telles que enfant, adolescent, adulte.
La démo configure un élément à prédire comme (“boulanger”, “noisette”, “italie”). Ensuite, la démo parcourt les données et calcule et affiche les nombres de joints lissés (“ajouter 1”). Par exemple, le 5 dans la capture d’écran signifie qu’il y a 4 boulangers qui ont une classe d’optimisme = 0.
La démo calcule le nombre brut de classes non lissées comme (19, 14, 7). Cela signifie qu’il y a 19 personnes avec une classe d’optimisme = 0, 14 personnes avec une classe = 1 et 7 personnes avec une classe = 2. Notez que 19 + 14 + 7 = 40, le nombre d’éléments de données.
Les nombres d’articulations lissés et les nombres bruts de classes sont combinés mathématiquement pour produire des termes de preuve de (0,0027, 0,0013, 0,0021). Celles-ci correspondent aux vraisemblances de classe (0, 1, 2). Parce que la plus grande valeur de preuve est à l’indice [0]la prédiction pour la personne (“boulanger”, “noisette”, “italie”) est de classe 0.
Les termes de preuve étant quelque peu difficiles à interpréter, la démo convertit les trois termes de preuve en pseudo-probabilités : (0,4418, 0,2116, 0,3466). Les valeurs ne sont pas de véritables probabilités mathématiques, mais parce qu’elles totalisent 1,0, elles peuvent être interprétées de manière approximative comme des probabilités. La plus grande probabilité est à l’indice [0].
Cet article suppose que vous avez des compétences de programmation intermédiaires ou supérieures avec un langage de la famille C tel que Python ou Java, mais ne suppose pas que vous savez quoi que ce soit sur la classification Bayes naïve. 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 classification naïve de Bayes
La première étape de la classification bayésienne naïve consiste à calculer les nombres conjoints associés à l’élément de données à prédire. Pour (« boulanger », « hazel », « italie ») et les données de démonstration de 40 éléments, les décomptes conjoints sont :
baker and class 0 = 4 baker and class 1 = 0 baker and class 2 = 1 hazel and class 0 = 5 hazel and class 1 = 2 hazel and class 2 = 2 italy and class 0 = 1 italy and class 1 = 5 italy and class 2 = 1
Les comptes conjoints sont multipliés ensemble lors du calcul des termes de preuve. Si un décompte conjoint est égal à 0, le terme entier est mis à zéro, donc 1 est ajouté à chaque décompte conjoint pour éviter cela. C’est ce qu’on appelle le lissage laplacien.
La prochaine étape de la classification naïve de Bayes consiste à calculer le nombre de chacune des classes à prédire. Pour les données de démonstration, le nombre de classes est (19, 14, 7). Il n’est pas nécessaire de lisser le nombre de classes car il y aura toujours au moins une de chaque classe.
Ensuite, un terme de preuve (parfois appelé Z) est calculé pour chaque valeur de classe possible. Pour la classe 0, le terme de preuve est :
Z(0) = (5 / 19+3) * (6 / 19+3) * (2 / 19+3) * (19 / 40) = 4/22 * 5/22 * 1/22 * 19/40 = 0.1818 * 0.2273 * 0.0435 * 0.4750 = 0.0027
Le dernier terme, 19/40, est la probabilité de la classe 0. Dans les trois premiers termes, le numérateur est un nombre joint lissé. Le dénominateur est le nombre brut de classes avec 3 (nombre de valeurs de prédicteur) ajouté pour compenser le lissage.
Pour la classe 1, le terme de preuve est :
Z(1) = (1 / 14+3) * (3 / 14+3) * (6 / 14+3) * (14 / 40) = 1/17 * 3/17 * 6/17 * 14/40 = 0.0588 * 0.1765 * 0.3529 * 0.3500 = 0.0013
Et pour la classe 2, le terme de preuve est :
Z(2) = (2 / 7+3) * (3 / 7+3) * (2 / 7+3) * (7 / 40) = 2/10 * 3/10 * 2/10 * 7/40 = 0.2000 * 0.3000 * 0.2000 * 0.1750 = 0.0021
Le plus grand terme de preuve est associé à la classe 0, c’est donc la classe prédite. Pour faciliter la comparaison des termes de preuve, ils sont souvent normalisés afin que leur somme soit égale à 1,0. La somme des termes de preuve est 0,0027 + 0,0013 + 0,0021 = 0,0061. Les (pseudo-probabilités) normalisées sont :
P(class 0) = 0.0027 / 0.0061 = 0.4418 P(class 1) = 0.0013 / 0.0061 = 0.2116 P(class 2) = 0.0021 / 0.0061 = 0.3466
Étant donné que chaque terme de preuve est le produit de valeurs inférieures à 1, il est possible que le calcul rencontre des problèmes. Par conséquent, en pratique, le calcul des termes de preuve utilise le log mathématique () de chaque valeur qui permet des additions et des soustractions au lieu de multiplications et de divisions.
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 NaiveBayes. J’ai utilisé Visual Studio 2019 (édition communautaire gratuite), mais la démo n’a pas de dépendances importantes, donc n’importe quelle version de Visual Studio 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 NaiveBayesProgram.cs, plus descriptif, et j’ai autorisé Visual Studio à renommer automatiquement la classe Program en NaiveBayesProgram.
Liste 1 :
Code de démonstration complet de Naive Bayes
using System; namespace NaiveBayes { class NaiveBayesProgram { static void Main(string[] args) { Console.WriteLine("nBegin naive Bayes classification "); string[][] data = GetData(); Console.WriteLine("nData looks like: "); Console.WriteLine("actor green korea 1"); Console.WriteLine("baker green italy 0"); Console.WriteLine("diver hazel japan 0"); Console.WriteLine("diver green japan 1"); Console.WriteLine("clerk hazel japan 2"); Console.WriteLine(" . . . "); int nx = 3; // number predictors (job, eye, country) int nc = 3; // number classes (0, 1, 2) int N = 40; // number data items int[][] jointCounts = new int[nx][]; for (int i = 0; i < nx; ++i) jointCounts[i] = new int[nc]; int[] yCounts = new int[nc]; string[] X = new string[] { "baker", "hazel", "italy"}; Console.WriteLine("nItem to classify: "); Console.WriteLine("baker hazel italy"); for (int i = 0; i < N; ++i) { // compute joint counts int y = int.Parse(data[i][nx]); // get the class as int ++yCounts[y]; for (int j = 0; j < nx; ++j) { if (data[i][j] == X[j]) ++jointCounts[j][y]; } } for (int i = 0; i < nx; ++i) // Laplacian smoothing for (int j = 0; j < nc; ++j) ++jointCounts[i][j]; Console.WriteLine("nJoint counts (smoothed): "); Console.WriteLine("0 1 2 "); Console.WriteLine("------"); ShowMatrix(jointCounts); Console.WriteLine("nClass counts (raw): "); ShowVector(yCounts); // compute evidence terms double[] eTerms = new double[nc]; for (int k = 0; k < nc; ++k) { double v = 1.0; // direct approach for (int j = 0; j < nx; ++j) v *= (jointCounts[j][k] * 1.0) / (yCounts[k] + nx); v *= (yCounts[k] * 1.0) / N; eTerms[k] = v; //double v = 0.0; // use logs to avoid underflow //for (int j = 0; j < nx; ++j) // v += Math.Log(jointCounts[j][k]) - Math.Log(yCounts[k] + nx); //v += Math.Log(yCounts[k]) - Math.Log(N); //eTerms[k] = Math.Exp(v); } Console.WriteLine("nEvidence terms for each class: "); ShowVector(eTerms); double evidence = 0.0; for (int k = 0; k < nc; ++k) evidence += eTerms[k]; double[] probs = new double[nc]; for (int k = 0; k < nc; ++k) probs[k] = eTerms[k] / evidence; Console.WriteLine("nPseudo-probabilities each class: "); ShowVector(probs); Console.WriteLine("nEnd naive Bayes demo "); Console.ReadLine(); } // Main static void ShowMatrix(int[][] m) { int r = m.Length; int c = m[0].Length; for (int i = 0; i < r; ++i) { for (int j = 0; j < c; ++j) { Console.Write(m[i][j] + " "); } Console.WriteLine(""); } } static void ShowVector(int[] v) { for (int i = 0; i < v.Length; ++i) Console.Write(v[i] + " "); Console.WriteLine(""); } static void ShowVector(double[] v) { for (int i = 0; i < v.Length; ++i) Console.Write(v[i].ToString("F4") + " "); Console.WriteLine(""); } static string[][] GetData() { string[][] m = new string[40][]; // 40 rows m[0] = new string[] { "actor", "green", "korea", "1" }; m[1] = new string[] { "baker", "green", "italy", "0" }; m[2] = new string[] { "diver", "hazel", "japan", "0" }; m[3] = new string[] { "diver", "green", "japan", "1" }; m[4] = new string[] { "clerk", "hazel", "japan", "2" }; m[5] = new string[] { "actor", "green", "japan", "1" }; m[6] = new string[] { "actor", "green", "japan", "0" }; m[7] = new string[] { "clerk", "green", "italy", "1" }; m[8] = new string[] { "clerk", "green", "italy", "2" }; m[9] = new string[] { "diver", "green", "japan", "1" }; m[10] = new string[] { "diver", "green", "japan", "0" }; m[11] = new string[] { "diver", "green", "japan", "1" }; m[12] = new string[] { "diver", "green", "japan", "2" }; m[13] = new string[] { "clerk", "green", "italy", "1" }; m[14] = new string[] { "diver", "green", "japan", "1" }; m[15] = new string[] { "diver", "hazel", "japan", "0" }; m[16] = new string[] { "clerk", "green", "korea", "1" }; m[17] = new string[] { "baker", "green", "japan", "0" }; m[18] = new string[] { "actor", "green", "italy", "1" }; m[19] = new string[] { "actor", "green", "italy", "1" }; m[20] = new string[] { "diver", "green", "korea", "0" }; m[21] = new string[] { "baker", "green", "japan", "2" }; m[22] = new string[] { "diver", "green", "japan", "0" }; m[23] = new string[] { "baker", "green", "korea", "0" }; m[24] = new string[] { "diver", "green", "japan", "0" }; m[25] = new string[] { "actor", "hazel", "italy", "1" }; m[26] = new string[] { "diver", "hazel", "japan", "0" }; m[27] = new string[] { "diver", "green", "japan", "2" }; m[28] = new string[] { "diver", "green", "japan", "0" }; m[29] = new string[] { "clerk", "hazel", "japan", "2" }; m[30] = new string[] { "diver", "green", "korea", "0" }; m[31] = new string[] { "diver", "hazel", "korea", "0" }; m[32] = new string[] { "diver", "green", "japan", "0" }; m[33] = new string[] { "diver", "green", "japan", "2" }; m[34] = new string[] { "diver", "hazel", "japan", "0" }; m[35] = new string[] { "actor", "hazel", "japan", "1" }; m[36] = new string[] { "actor", "green", "japan", "0" }; m[37] = new string[] { "actor", "green", "japan", "1" }; m[38] = new string[] { "diver", "green", "japan", "0" }; m[39] = new string[] { "baker", "green", "japan", "0" }; return m; } // GetData() } // Program } // ns
Le programme de démonstration code en dur les données dans une fonction GetData() définie par le programme. Les données sont renvoyées sous la forme d'une matrice de style tableau de tableaux où chaque cellule contient une valeur de chaîne. Dans un scénario non démo, vous souhaiterez peut-être stocker des données dans un fichier texte et écrire une fonction d'assistance pour lire les données dans une chaîne de tableaux de tableaux.[ ][ ] matrice.
Toute la logique de contrôle se trouve dans la méthode Main(). Les structures de données clés sont un style tableau de tableaux int[ ][ ] matrice pour contenir les comptes conjoints et un int[ ] vecteur pour contenir les comptes de classe :
int nx = 3; // number predictors (job, eye, country) int nc = 3; // number classes (0, 1, 2) int N = 40; // number data items int[][] jointCounts = new int[nx][]; for (int i = 0; i < nx; ++i) jointCounts[i] = new int[nc]; int[] yCounts = new int[nc];
Le nombre brut de classes et le nombre brut d'articulations sont calculés comme suit :
string[] X = new string[] { "baker", "hazel", "italy"}; for (int i = 0; i < N; ++i) { // compute joint counts int y = int.Parse(data[i][nx]); // get the class as int ++yCounts[y]; for (int j = 0; j < nx; ++j) { if (data[i][j] == X[j]) ++jointCounts[j][y]; } }
Le code parcourt la matrice de données ligne par ligne. L'étiquette de classe de ligne est extraite sous forme d'entier à partir de la dernière valeur de la ligne à l'index [nx]. La classe est utilisée pour incrémenter le vecteur yCounts. Ensuite, le code parcourt chaque valeur de prédicteur et si la valeur de prédicteur dans la matrice de données correspond à la valeur de prédicteur dans l'élément à prédire, l'entrée correspondante dans la matrice jointCount est incrémentée. Une fois que toutes les valeurs de comptage conjointes ont été calculées, le code de démonstration parcourt la matrice et ajoute 1 à chaque comptage afin qu'il n'y ait pas de valeurs 0.
.