Comment utiliser les fonctions de validation croisée de scikit-learn sur les classificateurs multi-étiquettes

20

Je teste différents classificateurs sur un ensemble de données où il y a 5 classes et chaque instance peut appartenir à une ou plusieurs de ces classes, j'utilise donc spécifiquement les classificateurs multi-étiquettes de scikit-learn sklearn.multiclass.OneVsRestClassifier. Maintenant, je veux effectuer une validation croisée en utilisant le sklearn.cross_validation.StratifiedKFold. Cela produit l'erreur suivante:

Traceback (most recent call last):
  File "mlfromcsv.py", line 93, in <module>
    main()
  File "mlfromcsv.py", line 77, in main
    test_classifier_multilabel(svm.LinearSVC(), X, Y, 'Linear Support Vector Machine')
  File "mlfromcsv.py", line 44, in test_classifier_multilabel
    scores = cross_validation.cross_val_score(clf_ml, X, Y_list, cv=cv, score_func=metrics.precision_recall_fscore_support, n_jobs=jobs)
  File "/usr/lib/pymodules/python2.7/sklearn/cross_validation.py", line 1046, in cross_val_score
    X, y = check_arrays(X, y, sparse_format='csr')
  File "/usr/lib/pymodules/python2.7/sklearn/utils/validation.py", line 144, in check_arrays
    size, n_samples))
ValueError: Found array with dim 5. Expected 98816

Notez que la formation du classificateur multi-étiquettes ne se bloque pas, mais la validation croisée le fait. Comment dois-je effectuer une validation croisée pour ce classificateur multi-étiquettes?

J'ai également écrit une deuxième version qui décompose le problème en formation et en validation croisée de 5 classificateurs distincts. Cela fonctionne très bien.

Voici mon code. La fonction test_classifier_multilabelest celle qui pose problème. test_classifierest mon autre tentative (décomposer le problème en 5 classificateurs et 5 validations croisées).

import numpy as np
from sklearn import *
from sklearn.multiclass import OneVsRestClassifier
from sklearn.neighbors import KNeighborsClassifier
import time

def test_classifier(clf, X, Y, description, jobs=1):
    print '=== Testing classifier {0} ==='.format(description)
    for class_idx in xrange(Y.shape[1]):
        print ' > Cross-validating for class {:d}'.format(class_idx)
        n_samples = X.shape[0]
        cv = cross_validation.StratifiedKFold(Y[:,class_idx], 3)
        t_start = time.clock()
        scores = cross_validation.cross_val_score(clf, X, Y[:,class_idx], cv=cv, score_func=metrics.precision_recall_fscore_support, n_jobs=jobs)
        t_end = time.clock();
        print 'Cross validation time: {:0.3f}s.'.format(t_end-t_start)
        str_tbl_fmt = '{:>15s}{:>15s}{:>15s}{:>15s}{:>15s}'
        str_tbl_entry_fmt = '{:0.2f} +/- {:0.2f}'
        print str_tbl_fmt.format('', 'Precision', 'Recall', 'F1 score', 'Support')
        for (score_class, lbl) in [(0, 'Negative'), (1, 'Positive')]:
            mean_precision = scores[:,0,score_class].mean()
            std_precision = scores[:,0,score_class].std()
            mean_recall = scores[:,1,score_class].mean()
            std_recall = scores[:,1,score_class].std()
            mean_f1_score = scores[:,2,score_class].mean()
            std_f1_score = scores[:,2,score_class].std()
            support = scores[:,3,score_class].mean()
            print str_tbl_fmt.format(
                lbl,
                str_tbl_entry_fmt.format(mean_precision, std_precision),
                str_tbl_entry_fmt.format(mean_recall, std_recall),
                str_tbl_entry_fmt.format(mean_f1_score, std_f1_score),
                '{:0.2f}'.format(support))

def test_classifier_multilabel(clf, X, Y, description, jobs=1):
    print '=== Testing multi-label classifier {0} ==='.format(description)
    n_samples = X.shape[0]
    Y_list = [value for value in Y.T]
    print 'Y_list[0].shape:', Y_list[0].shape, 'len(Y_list):', len(Y_list)
    cv = cross_validation.StratifiedKFold(Y_list, 3)
    clf_ml = OneVsRestClassifier(clf)
    accuracy = (clf_ml.fit(X, Y).predict(X) != Y).sum()
    print 'Accuracy: {:0.2f}'.format(accuracy)
    scores = cross_validation.cross_val_score(clf_ml, X, Y_list, cv=cv, score_func=metrics.precision_recall_fscore_support, n_jobs=jobs)
    str_tbl_fmt = '{:>15s}{:>15s}{:>15s}{:>15s}{:>15s}'
    str_tbl_entry_fmt = '{:0.2f} +/- {:0.2f}'
    print str_tbl_fmt.format('', 'Precision', 'Recall', 'F1 score', 'Support')
    for (score_class, lbl) in [(0, 'Negative'), (1, 'Positive')]:
        mean_precision = scores[:,0,score_class].mean()
        std_precision = scores[:,0,score_class].std()
        mean_recall = scores[:,1,score_class].mean()
        std_recall = scores[:,1,score_class].std()
        mean_f1_score = scores[:,2,score_class].mean()
        std_f1_score = scores[:,2,score_class].std()
        support = scores[:,3,score_class].mean()
        print str_tbl_fmt.format(
            lbl,
            str_tbl_entry_fmt.format(mean_precision, std_precision),
            str_tbl_entry_fmt.format(mean_recall, std_recall),
            str_tbl_entry_fmt.format(mean_f1_score, std_f1_score),
            '{:0.2f}'.format(support))

def main():
    nfeatures = 13
    nclasses = 5
    ncolumns = nfeatures + nclasses

    data = np.loadtxt('./feature_db.csv', delimiter=',', usecols=range(ncolumns))

    print data, data.shape
    X = np.hstack((data[:,0:3], data[:,(nfeatures-1):nfeatures]))
    print 'X.shape:', X.shape
    Y = data[:,nfeatures:ncolumns]
    print 'Y.shape:', Y.shape

    test_classifier(svm.LinearSVC(), X, Y, 'Linear Support Vector Machine', jobs=-1)
    test_classifier_multilabel(svm.LinearSVC(), X, Y, 'Linear Support Vector Machine')

if  __name__ =='__main__':
    main()

J'utilise Ubuntu 13.04 et scikit-learn 0.12. Mes données se présentent sous la forme de deux tableaux (X et Y) qui ont des formes (98816, 4) et (98816, 5), soit 4 fonctionnalités par instance et 5 étiquettes de classe. Les étiquettes sont 1 ou 0 pour indiquer l'appartenance à cette classe. Suis-je en utilisant le format correct car je ne vois pas beaucoup de documentation à ce sujet?

chippies
la source

Réponses:

10

L'échantillonnage stratifié signifie que la distribution des membres de la classe est préservée dans votre échantillonnage KFold. Cela n'a pas beaucoup de sens dans le cas de plusieurs étiquettes où votre vecteur cible peut avoir plus d'une étiquette par observation.

Il y a deux interprétations possibles de stratifié dans ce sens.

Pour étiquettes où au moins l'une d'entre elles est remplie, vous obtenez étiquettes uniques. Vous pouvez effectuer un échantillonnage stratifié sur chacun des bacs d'étiquettes uniques.ni=1n2n

L'autre option consiste à essayer de segmenter les données d'apprentissage afin que la masse de probabilité de la distribution des vecteurs d'étiquettes soit approximativement la même sur les plis. Par exemple

import numpy as np

np.random.seed(1)
y = np.random.randint(0, 2, (5000, 5))
y = y[np.where(y.sum(axis=1) != 0)[0]]


def proba_mass_split(y, folds=7):
    obs, classes = y.shape
    dist = y.sum(axis=0).astype('float')
    dist /= dist.sum()
    index_list = []
    fold_dist = np.zeros((folds, classes), dtype='float')
    for _ in xrange(folds):
        index_list.append([])
    for i in xrange(obs):
        if i < folds:
            target_fold = i
        else:
            normed_folds = fold_dist.T / fold_dist.sum(axis=1)
            how_off = normed_folds.T - dist
            target_fold = np.argmin(np.dot((y[i] - .5).reshape(1, -1), how_off.T))
        fold_dist[target_fold] += y[i]
        index_list[target_fold].append(i)
    print("Fold distributions are")
    print(fold_dist)
    return index_list

if __name__ == '__main__':
    proba_mass_split(y)

Pour obtenir l'entraînement normal, en testant les indices que KFold produit, vous voulez le réécrire pour qu'il retourne le np.setdiff1d de chaque index avec np.arange (y.shape [0]), puis envelopper cela dans une classe avec une méthode iter .

Jessica Mick
la source
Merci pour cette explication. Je voudrais juste vérifier quelque chose, l' OneVsRestClassifieraccepteur accepte-t-il un tableau 2D (par exemple ydans votre exemple de code) ou un tuple de listes d'étiquettes de classe? Je demande parce que j'ai regardé l'exemple de classification multi-étiquettes sur scikit-learn tout à l'heure et j'ai vu que la make_multilabel_classificationfonction renvoie un tuple de listes d'étiquettes de classe, par exemple ([2], [0], [0, 2], [0]...)lors de l'utilisation de 3 classes?
chippies
2
Cela fonctionne dans les deux sens. Lorsqu'une liste de tuples est passée, elle y adapte un sklearn.preprocessing.LabelBinarizer. Vous connaissez quelques-uns des algorithmes qui fonctionnent dans le cas multiclasse multiclasse. Notamment RandomForest.
Jessica Mick
Merci beaucoup, cela m'a au moins permis de surmonter les accidents. Pour le moment, je suis passé à la validation croisée K-fold mais je pense que je vais bientôt utiliser votre code. Maintenant cependant, le score renvoyé par cross_val_score n'a que deux colonnes, c'est-à-dire comme s'il n'y avait que deux classes. Le passage à metrics.confusion_matrixproduit des matrices de confusion 2x2. Certaines mesures prennent-elles en charge les classificateurs multi-étiquettes?
chippies
J'ai répondu à ma propre sous-question. Les mesures qui prennent en charge les classificateurs multi-étiquettes n'apparaissent que dans scikit-learn 0.14-rc, donc je devrai mettre à niveau si je veux cette capacité, ou le faire moi-même. Merci pour l'aide et le code.
chippies
J'ai supprimé le tableau sur l'instruction de retour. Il n'y a aucune raison que vous trouverez toujours un ensemble de points de données parfaitement partitionné. Faites-moi savoir si cela fonctionne. Vous devez également écrire des tests dans votre code. J'ai en quelque sorte expiré cet algorithme après avoir regardé des algorithmes d'optimisation convexes toute la journée.
Jessica Mick
3

Vous voudrez peut-être vérifier: Sur la stratification des données multi-étiquettes .

Ici, les auteurs racontent d'abord l'idée simple d'échantillonner à partir de jeux d'étiquettes uniques, puis introduisent une nouvelle approche de stratification itérative pour les jeux de données multi-étiquettes.

L'approche de la stratification itérative est gourmande.

Pour un aperçu rapide, voici ce que fait la stratification itérative:

Ils découvrent d'abord combien d'exemples doivent entrer dans chacun des k-folds.

  • Trouvez le nombre d'exemples souhaité par pli par étiquette , .jejcjej

  • A partir de l'ensemble de données qui ne sont pas encore distribués en plis k, l'étiquette est identifiée pour laquelle le nombre d'exemples est le minimum, .ll

  • Ensuite, pour chaque point de données dans trouvez le pli pour lequel est maximisé (rompez les liens ici). En d'autres termes, quel pli a la demande maximale pour l'étiquette ou est le plus déséquilibré par rapport à l'étiquette .lkckjll

  • Ajoutez le point de données actuel au pli trouvé à l'étape ci-dessus, supprimez le point de données du jeu de données d'origine et ajustez les valeurs de comptage de et continuez jusqu'à ce que tous les points de données ne soient pas répartis dans les plis.kc

L'idée principale est de se concentrer d'abord sur les labels qui sont rares, cette idée vient de l'hypothèse que

"si les étiquettes rares ne sont pas examinées en priorité, alors elles peuvent être distribuées de manière indésirable, et cela ne peut pas être réparé par la suite"

Pour comprendre comment les liens sont rompus et d'autres détails, je recommanderai de lire le document. De plus, à partir de la section des expériences, ce que je peux comprendre est que, selon le rapport étiquettes / exemples, on peut vouloir utiliser le jeu d'étiquettes unique ou cette méthode de stratification itérative proposée. Pour des valeurs inférieures de ce rapport, la distribution des étiquettes à travers les plis est proche ou meilleure dans quelques cas comme stratification itérative. Pour des valeurs plus élevées de ce rapport, la stratification itérative s'est avérée avoir maintenu de meilleures distributions dans les plis.

phoxis
la source
1
lien vers le PDF du document mentionné: lpis.csd.auth.gr/publications/sechidis-ecmlpkdd-2011.pdf
Temak