Weka, implémentée en Java. Weka est une librairie qui implémente sous une API relativement unifiée la plupart des algorithmes utilisés en apprentissage automatisé, pour la classification comme pour la régression. Elle n’est pas forcément la plus indiquée pour un usage en production (ne serait-ce que parce que son API est conçue pour une utilisation en ligne de commande et que sa gestion des exceptions est assez rudimentaire), mais l’étendue de sa couverture ainsi que son intégration à une interface graphique la rend particulièrement indiquée pour l’expérimentation. En particulier, elle permet de manière simple d’obtenir des statistiques sur le classifieur entraîné (erreur absolue et relative, individus correctement classés…) qui sont précieuses dans un cadre expérimental.
Un peu de code…
Tout d’abord, nous allons construire un vecteur à partir de notre document, soit ici la description de la transaction. Pour cela, nous avons choisi d’implémenter une classe dédiée, qui se charge de la construction incrémentale du dictionnaire (les imports ont été omis). Cette classe va dans un premier temps ajouter des mots au dictionnaire à partir du moment où ils apparaissent au minimum deux fois (méthode initialize). Dans un second temps, elle va renvoyer un vecteur indicé d’après le dictionnaire ainsi construit à partir d’un fragment de texte (méthode vectorize).
package aga.weka.naive;
public class TextVectorizer {
// rassemble les mots du corpus d’entraînement
private List<String> dictionary = null;
private Attribute classAttribute;
private FastVector allAttributes = null;
// Cette méthode construit le dictionnaire à partir
// de tous les mots présents au moins deux fois dans
// la chaîne doc passée en paramètre et les ajoute à
// la liste dictionary.
public void initialize(String doc, Attribute classAttr) {
...
}
// Cette méthode prend un document en paramètre et
// renvoie une liste indicée d’après le dictionnaire,
// dans laquelle un "1" indique la présence du mot
// d’indice correspondant.
public Instance vectorize(String doc) {
...
}
public Attribute getClassAttribute(String name) {
return classAttribute;
}
public FastVector getAttributes() {
return allAttributes;
}
}
Maintenant, à partir de notre ensemble d’entraînement préalablement constitué sous forme de fichier CSV et de la forme « description, classe », nous allons construire un classifieur bayésien en utilisant l’implémentation de Weka :
package aga.weka.naive;
public class TextCat {
private static final String DATA_DIR
= " /eclispe-wkspc/ml/weka-naivebayes/datasets";
private static final String TRAINING_FILE
= DATA_DIR + "/training-text.csv";
private static final String TEST_FILE
= DATA_DIR + "/testing-text.csv";
private static final String DATASET
= DATA_DIR + "/full-20110819.csv";
// le libellé de transaction apparaît en
// en premier dans le fichier CSV :
private static final int DOC_IDX = 0;
// et la classe en seconde position :
private static final int CLASS_IDX = 1;
private static TextVectorizer vectorizer
= new TextVectorizer();
/**
* @param fileName Path to a CSV file with the samples
* @param classIndex Index of the class attribute
* @return A dataset containing the data from the file
*
* @throws Exception (from the Weka API... sucks...)
*/
private static Instances getDataSetFromFile(
String fileName,
int classIndex
) throws Exception {
// utilitaire de lecture de notre fichier CSV
CSVLoader loader = new CSVLoader();
loader.setFile(new File(fileName));
Instances dataset = loader.getDataSet();
// précise quel champ représente la classe
dataset.setClassIndex(classIndex);
return dataset;
}
/**
* Takes in a dataset and, given the index of the
* text feature, outputs a feature vector indexed
* against the dictionary previously initialized.
*
* @param dataset The dataset to vectorize
* @param docAttr The index of the text feature
* @param name Name of the target dataset
*
* @return Dataset containing feature vectors
* for the input dataset
*
* @see #vectorizer
*/
private static Instances vectorize(
Instances dataset,
int docAttr, String name
) {
Instances vectors
= new Instances(
name,
vectorizer.getAttributes(),
dataset.numInstances()
);
vectors.setClass(vectorizer.getClassAttribute(null));
vectors.setClassIndex(vectors.numAttributes()-1);
Enumeration<Instance> i
= dataset.enumerateInstances();
while (i.hasMoreElements()) {
Instance instance = i.nextElement();
Instance vector
= vectorizer.vectorize(
instance.stringValue(docAttr)
);
vectors.add(vector);
}
i = dataset.enumerateInstances();
Enumeration<Instance> v
= vectors.enumerateInstances();
while (i.hasMoreElements()) {
Instance instance = i.nextElement();
Instance vector = v.nextElement();
// déterminer la classe si elle est fournie
if (!instance.classIsMissing()) {
int cidx = instance.classIndex();
String classValue
= instance.stringValue(cidx);
if (classValue != null
&& !classValue.equals("")
) {
vector.setClassValue(classValue);
}
}
}
return vectors;
}
/**
* @param args CL arguments
* @throws Exception (the Weka API leaking again...)
*/
public static void main(String[] args) throws Exception {
Instances trainingSet
= getDataSetFromFile(TRAINING_FILE, CLASS_IDX);
/*
* Initialise le vectorizer utilisé par la suite
* pour vectoriser les échantillons de test et
* d'entraînement.
* Ceci initialise en parallèle le dictionnaire
* utilisé pour représenter les transactions.
*/
Enumeration<Instance> e
= trainingSet.enumerateInstances();
StringBuilder words = new StringBuilder();
while (e.hasMoreElements()) {
words.append(' ')
.append(e.nextElement().attribute(DOC_IDX));
}
vectorizer.initialize(
words.toString(),
trainingSet.classAttribute()
);
Instances trainingVectors
= vectorize(
getDataSetFromFile(TRAINING_FILE, CLASS_IDX),
DOC_IDX, "training"
);
// entraîner le modèle avec les exemples :
Classifier model = new NaiveBayes() ;
model.buildClassifier(trainingVectors);
// évaluer le modèle avec le fichier de test :
Instances testingVectors
= vectorize(
getDataSetFromFile(TEST_FILE, CLASS_IDX),
DOC_IDX,
"testing"
);
Evaluation eval = new Evaluation(trainingVectors);
eval.evaluateModel(model, trainingVectors);
// afficher les indicateurs du modèle entraîné :
System.out.println(eval.toSummaryString());
// essayer de catégoriser de nouvelles transactions :
Instances dataset = getDataSetFromFile(DATASET, 4);
Instances vectors = vectorize(dataset, 2, "samples");
Enumeration<Instance>
i = dataset.enumerateInstances();
Enumeration<Instance>
v = vectors.enumerateInstances();
while (i.hasMoreElements()) {
Instance instance = i.nextElement();
Instance vector = v.nextElement();
if (!instance.classIsMissing()) continue;
double prediction = model.classifyInstance(vector);
int classIdx = (int)Math.round(prediction);
String detail = instance.stringValue(2);
String clazz
= vectors.classAttribute().value(classIdx);
System.out.println(
prediction + "(" + clazz + ") - " + detail
);
}
}
}
Pour tester notre classifieur, nous avons simplement exécuté ce programme sur un relevé bancaire fraîchement téléchargé auquel nous avons fait subir les mêmes transformations qu’à l’ensemble d’entraînement, c’est-à-dire :
Voici un extrait de la sortie de notre programme de test :
Correctly Classified Instances 229 94.6281 % Incorrectly Classified Instances 13 5.3719 % Kappa statistic 0.9238 Mean absolute error 0.0107 Root mean squared error 0.1037 Relative absolute error 7.6116 % Root relative squared error 39.2315 % Coverage of cases (0.95 level) 94.6281 % Mean rel. region size (0.95 level) 10 % Total Number of Instances 242
6.0(fees) - OPTION TRANQUILLITE
6.0(fees) - COTISATION JAZZ
0.0(transfer) - PRELEVEMENT 4871600969 TRESOR PUBLIC 75 MENM275026474023071 750710504383389328 111
1.0(withdrawal) - CARTE X2052 RETRAIT DAB 17/08 04H03 HSBC FRANCE MONTMARTRE 771031
1.0(withdrawal) - CARTE X2052 RETRAIT DAB 16/08 12H50 DAB DE LA BANQUE POSTALE 756451
2.0(payment) - CARTE X2052 16/08 COCCI MARKET
... 3.0(regularisation) - REGULARISATION DE COMMISSION FRAIS RET EUR DAB UE HORS SG DU MOIS DE JUILLET 2011 1 RETRAIT A 1.00 EUR NT 4.0(atmfees) - FRAIS RET EUR DAB UE HORS SG CARTE 4973019651342052 11 RETRAITS EN 07/2011 FORFAIT MENS 5.0(transferfees) - FRAIS SUR VIREMENT PERMANENT DU 28/07/11
0.0(transfer) - PRELEVEMENT 3269732986 BOUYGUES TELECOM PAGP01008RO2TM *418323
1.0(withdrawal) - CARTE X2052 RETRAIT DAB 30/07 03H09 BANQUE DE FRANCE 00010006
... 4.0(atmfees) - FRAIS RET EUR DAB UE HORS SG CARTE 4973019651342052 11 RETRAITS EN 06/2011 FORFAIT MENS 1.0(withdrawal) - CARTE X2052 RETRAIT DAB 30/06 00H57 DAB DE LA BANQUE POSTALE 757171
5.0(transferfees) - FRAIS SUR VIREMENT PERMANENT DU 28/06/11
... 9.0(regul) - REGULARISATION SUR CARTE X8463 PAIEMENT FRANCE 110111200666161 20/04/11 AIR FRANCE
9.0(regul) - REGULARISATION SUR CARTE X8463 PAIEMENT FRANCE 110111100096286 19/04/11 FINDMYORDER.COM
... 8.0(payfees) - FRAIS PAIEMENT HORS ZONE EURO 1 PAIEMENT A 1.00 EUR NT 3.39 EUR A 2.70
8.0(payfees) - FRAIS PAIEMENT HORS ZONE EURO 1 PAIEMENT A 1.00 EUR NT 16.89 EUR A 2.70
4.0(atmfees) - FRAIS RETRAIT HORS ZONE EURO 1 RETRAIT A 3.00 EUR NT 62.53 EUR A 2.70
4.0(atmfees) - FRAIS RETRAIT HORS ZONE EURO 1 RETRAIT A 3.00 EUR NT 81.29 EUR A 2.70
... 6.0(fees) - OPTION TRANQUILLITE
6.0(fees) - COTISATION JAZZ
...
La première partie nous indique comment notre classifieur s’est comporté vis-à-vis de notre ensemble de test. Les indicateurs intéressants sont les suivants :
Cela signifie concrètement que, sur notre ensemble de test, le classifieur entraîné sur nos exemples a correctement classé près de 95% des transactions qui lui ont été présentées (ce très bon score est en bonne partie dû à la faible variance des exemples et au petit nombre de classes). L’erreur relative est utile quant à elle pour comparer les performances de plusieurs classifieurs sur un même jeu d’entraînement/test.
Nous avons implémenté un embryon de gestionnaire de finances personnelles, ou tout du moins la partie le constituant qui permet de catégoriser des dépenses (sur des catégories très simples, il est vrai). Pour cela, nous avons utilisé un des classifieurs les plus simples existant, mais qui donne de bon résultats pour des volumes réduits d’exemples : ici, nous atteignons (sur la base d’un ensemble d’entraînement de quelques centaines d’individus), un taux de classification correcte de près de 95%.
Les classifieurs de Bayes se révèlent en pratique bien adaptés aux problèmes de catégorisation de texte, et ont l’avantage d’être extrêmement économes en puissance de traitement, du fait même de l’hypothèse d’indépendance qui les fonde (la loi de distribution de chaque caractéristique pouvant alors être exprimée indépendamment des autres en une seule dimension au lieu d’un nombre de dimensions en puissance du nombre de caractéristiques, qui pave la route au « fléau de la dimensionnalité », caractérisé notamment par le fait qu’un nombre exponentiel d’exemples est requis en l’absence d’une telle hypothèse pour aboutir à une performance identique). Cet exemple « jouet » nous a permis de les évaluer dans une situation concrète.
Voici quelques références pour creuser le sujet : L’incontournable article de Wikipedia Une lecture d’Andrew Ng, avec un passage sur les classifieurs de Bayes La justification au paradoxe apparent de la performance de ces classifieurs Un pointeur vers des librairies open source d’apprentissage automatisé