Cette dépêche traite de l’exploration de données sur des données issues de LinuxFr.org.
Ayant découvert récemment scikit-learn, une bibliothèque Python d’apprentissage statistique (machine learning). Je voulais partager ici un début d’analyse sur des contenus issus de LinuxFr.org.
Avertissement : je ne suis pas programmeur, ni statisticien. Je ne maîtrise pas encore tous les arcanes de scikit-learn et de nombreux éléments théoriques m’échappent encore. Je pense néanmoins que les éléments présentés ici pourront en intéresser plus d’un(e).
Tous les scripts sont codés en Python et l’analyse à proprement parler a été réalisée à l’aide d’un notebook Jupyter. Un dépôt contenant les données et les scripts est disponible sur GitLab.
Sommaire
- Prédire la note d’un journal
- Obtenir les données
- Analyse des données
- Classifier les articles
- Validation croisée
- Optimisation des paramètres
- Utiliser des propriétés multiples
- L’union fait la force
- Données hors échantillon.
- Pour aller plus loin
- Conclusions
- Perspectives
- Rêvons un peu
- Note
Prédire la note d’un journal
Il y a eu récemment une vague de journaux politiques sur DLFP. La note de la plupart de ces journaux était assez basse. Par ailleurs, on lit régulièrement ici des personnes qui se plaignent de la note de leurs articles. Bien souvent, des gens postent des contenus incendiaires, parfois en rafale. Je me suis demandé si cela est évitable.
Est-il possible de prédire la note d'un journal en fonction de son contenu? Le problème est ambitieux mais il permettrait aux auteurs d'avoir une idée de l’accueil qui sera réservé à leur prose.
Prédire un score me paraît hasardeux, c'est pourquoi j'ai préféré classer les journaux dans 4 catégories en fonction de leur note, n (en english car il est bien connu que ça improve la productivitaÿ) :
- n < -20 : Magnificent Troll ;
- -20 < n < 0 : Great Troll ;
- 0 < n < 20 : Average Troll ;
- 20 < n : Qualitaÿ Troll.
Vous l'aurez compris, tout contenu est un Troll, car je pense que nous sommes tous le troll d'un autre.
Obtenir les données
Il n'existe pas à ma connaissance de base de données de DLFP disponible pour tests. Après avoir lu deux journaux précédents, j'ai décidé de construire une moulinette afin d'aspirer une partie du contenu.
Approche 1: le flux atom
Dans un premier temps, j'ai utilisé le flux atom des journaux à l'aide de la bibliothèque feedparser. Le script fonctionne et l'approche est très simple mais malheureusement, la quantité de données est trop limitées. Par ailleurs, le score d'un contenu n'est pas disponible dans les flux. J'ai donc changé mon fusil d'épaule.
Approche 2: l'heure de la soupe
Afin d'augmenter le volume de données, il faut parcourir la page https://linuxfr.org/journaux?page=x et collecter tous les liens vers les différents journaux. Chaque journal est ensuite analysé. Dans un premier temps, les informations suivantes sont utilisées : le nom de l'auteur, le titre du journal, l'URL, le contenu du journal, sa note.
La moulinette s'appuie sur la bibliothèque Beautiful Soup4. Les données sont enregistrées dans un fichier CSV. Étant donné que le contenu des journaux est très varié, j'ai choisi les caractères µ et £ en tant que délimiteur et séparateur, respectivement.
Analyse des données
L'analyse suivante est réalisée à l'aide du fichier diaries_classification.ipynb. La lecture du fichier CSV linuxfr.csv
montre qu'il contient 5921 journaux. 302 Magnificents Trolls, 460 Great Trolls, 2545 Quality Trolls et 2614 Average Trolls. Étant donné que les données sont déséquilibrées, il faudra en tenir compte dans les travaux car ces chiffres influencent les probabilités.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import sys
import matplotlib
# Enable inline plotting
%matplotlib inline
filename = r'linuxfr.csv'
lf_data = pd.read_csv(filename, encoding="UTF-8", sep='£', engine='python', quotechar='µ')
len(lf_data)
5921
lf_data.quality_content.value_counts()
Average Troll 2614
Quality Troll 2545
Great Troll 460
Magnificent Troll 302
Name: quality_content, dtype: int64
lf_data.quality_content.value_counts().plot(kind='bar')
plt.ylabel('Occurences', fontsize='xx-large')
plt.yticks(fontsize='xx-large')
plt.xlabel('Trolls', fontsize='xx-large')
plt.xticks(fontsize='xx-large')
Au passage, on observe qu'il y a beaucoup plus de contenu de qualité (pertinent), dont le score est positif que de négatif. Ou encore, qu'il y a beaucoup plus de contenu avec lequel les votants sont d'accord.
L’affaire est dans le sac (de mots)
À ce stade, j'ai suivi la documentation officielle de scikit-learn. L'analyse de texte est le plus souvent basée sur un algorithme de type "Bag of words". Chaque mot est compté dans le texte. On est alors en mesure de tracer un histogramme du nombre d’occurrence des mots en fonction de la liste des mots du dictionnaire. Dans scikit-learn, l'utilisation d'un sac de mots est très simple. Il faut faire appel à la classe CountVectorizer. Ma base de 5921 journaux contient 78879 mots différents.
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(lf_data['content'].values)
X_train_counts.shape
(5921, 78879)
Utiliser les fréquences d'apparition des mots
L'inconvénient du comptage de mots est qu'il entraîne un déséquilibre entre les textes de longueur différente. Il est possible de calculer les fréquences (tf) et éventuellement diminuer l'impact des mots qui apparaissent dans beaucoup de documents tels que les pronoms (tf-idf). L'utilisation de ces algorithmes est tout aussi simple :
from sklearn.feature_extraction.text import TfidfTransformer
tf_transformer = TfidfTransformer(use_idf=False).fit(X_train_counts)
X_train_tf = tf_transformer.transform(X_train_counts)
Classifier les articles
Approche naïve : filtrage bayésien
La manière la plus simple d'analyser les articles est d'utiliser la classification naïve bayésienne. Wikipedia éclaire un peu plus les concepts sous-jacents :
En termes simples, un classificateur bayésien naïf suppose que l'existence d'une caractéristique pour une classe, est indépendante de l'existence d'autres caractéristiques. Un fruit peut être considéré comme une pomme s'il est rouge, arrondi, et fait une dizaine de centimètres. Même si ces caractéristiques sont liées dans la réalité, un classificateur bayésien naïf déterminera que le fruit est une pomme en considérant indépendamment ces caractéristiques de couleur, de forme et de taille.
Une fois le modèle entraîné (fonction fit, d'adéquation en français), il est possible de prédire à quelle catégorie des articles appartiennent.
from sklearn.naive_bayes import MultinomialNB
classifier = MultinomialNB()
classifier.fit(X_train_tfidf, targets)
training_journals = ['Sécuriser son serveur avec la commande sudo rm -rf /*',
'Debian is dying',
'Windows Millenium est meilleur que Linux sur calculatrice graphique',
"MultiDeskOS est 42% plus performant que Redhat 3.0.3 (Picasso)",
"Pierre Tramo président !",
"Des chocolatines au menu des cantines situées dans les DOM-TOM",
"1515, l’année du Desktop Linux!"]
X_new_counts = count_vect.transform(training_journals)
X_new_tfidf = tfidf_transformer.transform(X_new_counts)
predicted = classifier.predict(X_new_tfidf)
for doc, category in zip(training_journals, predicted):
print('%r => %s' % (doc, category))
'Sécuriser son serveur avec la commande sudo rm -rf /*' => Quality Troll
'Debian is dying' => Quality Troll
'Windows Millenium est meilleur que Linux sur calculatrice graphique' => Quality Troll
'MultiDeskOS est 42% plus performant que Redhat 3.0.3 (Picasso)' => Average Troll
'Pierre Tramo président !' => Average Troll
'Des chocolatines au menu des cantines situées dans les DOM-TOM' => Quality Troll
'1515, l’année du Desktop Linux!' => Average Troll
La commande predict_proba
permet d'afficher les probabilités. Il en ressort que la marge d'erreur est énorme.
predicted_proba = classifier.predict_proba(X_new_tfidf)
print(targets_names)
predicted_proba
['Average Troll', 'Great Troll', 'Magnificent Troll', 'Quality Troll']
array([[ 0.38146407, 0.01242555, 0.00699732, 0.59911306],
[ 0.45180296, 0.03300345, 0.01880854, 0.49638505],
[ 0.37809693, 0.0190014 , 0.00917897, 0.5937227 ],
[ 0.47083803, 0.0629247 , 0.02837355, 0.43786371],
[ 0.54130358, 0.04642992, 0.03861831, 0.37364818],
[ 0.45172753, 0.03297976, 0.01805764, 0.49723507],
[ 0.59237292, 0.01164186, 0.00420374, 0.39178148]])
Mes "journaux" sont beaucoup trop courts pour être représentatifs, enfin cela dépend de la définition de "contenu de qualité". Par conséquent, il faut tester le modèle sur l'archive des contenus, dans un premier temps. Pour y arriver, je définis un pipeline qui consiste à assembler les étapes décrites précédemment dans un objet qui se comporte comme un classificateur.
from sklearn.pipeline import Pipeline
text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', MultinomialNB()),])
Tester le modèle avec les journaux connus
Je commence par échantillonner 20 % des journaux de la base de données et je teste le modèle sur cet ensemble, afin de voir s'il est capable de retrouver la bonne catégorie.
diaries_test = lf_data.sample(frac=0.2)
predicted = text_clf.predict(diaries_test['quality_content'])
from sklearn.metrics import confusion_matrix, f1_score
score = f1_score(diaries_test['quality_content'], predicted, average='weighted')
print('Diaries:', len(diaries_test))
print('Score:', score)
Diaries: 5921
Score: 0.269979533821
/usr/lib/python3.6/site-packages/sklearn/metrics/classification.py:1113: UndefinedMetricWarning: F-score is ill-defined and being set to 0.0 in labels with no predicted samples.
'precision', 'predicted', average, warn_for)
Ça ne marche pas du tout. La raison pour laquelle ce message est affiché est que le paramètre F (score F1) est indéterminé. Ce paramètre est un estimateur de la qualité d'une classification. Il dépend de la précision et du rappel. Une image vaut mieux qu'un long discours, le dessin sur la page wikipedia :
La matrice de confusion permet de comprendre pourquoi le score F est si mauvais : mis à part pour les trolls de qualité, je n'ai pas de vrai positif !
Pour lire le graphique : la prédiction parfaite aurait 100 % sur chaque case de la diagonale. C'est le cas ici des qualitaÿ trolls qui sont tous bien identifiés. Mais il y a un biais vers les qualitaÿ trolls. L'algorithme interprète ainsi erronément 100 % des average trolls comme des qualitaÿ trolls par exemple.
Au passage, j'affiche la matrice de confusion à l'aide du code de la documentation officielle.
Mon classificateur est mauvais. Il est probablement possible d'en améliorer les performances mais j'ai préféré changer d’algorithme.
Support vector machine (SVM)
D'après la documentation officielle, il s'agit de l’algorithme de classification de texte le plus performant pour le texte. SGDClassifier est basé sur un classificateur linéaire et un algorithme du gradient stochastique (abréviation SGD). Je vous avoue ne pas encore maîtriser ces subtilités. Si quelqu'un à l'aise avec ces notions veut participer à la discussion, il est le bienvenu.
from sklearn.linear_model import SGDClassifier
text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', SGDClassifier()),])
_ = text_clf.fit(lf_data.content, lf_data.quality_content)
predicted = text_clf.predict(diaries_test.content)
np.mean(predicted == diaries_test.quality_content)
0.95
Le score est très bon. Il est possible d'afficher plus d'informations à propos des prédictions :
from sklearn import metrics
print(metrics.classification_report(diaries_test.quality_content, predicted, target_names=targets_names))
precision recall f1-score support
Average Troll 0.99 0.93 0.96 523
Great Troll 1.00 0.94 0.97 80
Magnificent Troll 1.00 0.94 0.97 72
Quality Troll 0.92 0.99 0.96 509
avg / total 0.96 0.96 0.96 1184
# Affichage de la matrice de confusion
metrics.confusion_matrix(diaries_test.quality_content, predicted)
# Compute confusion matrix
import itertools
cnf_matrix = confusion_matrix(diaries_test['quality_content'], predicted)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=targets_names,
title='Confusion matrix, without normalization')
# Plot normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=targets_names, normalize=True,
title='Normalized confusion matrix')
plt.show()
Confusion matrix, without normalization
Confusion matrix, without normalization
[[489 0 0 34]
[ 2 75 0 3]
[ 0 0 68 4]
[ 5 0 0 504]]
Normalized confusion matrix
[[ 0.93 0. 0. 0.07]
[ 0.03 0.94 0. 0.04]
[ 0. 0. 0.94 0.06]
[ 0.01 0. 0. 0.99]]
Validation croisée
Ces résultats sont très intéressants mais il est important de tester la solidité du modèle. Cette étape est appelée validation croisée. scikit-learn permet de réaliser ces tests de manière automatisée. L'idée est d’échantillonner une partie des journaux (10 % dans notre cas), d'entraîner le modèle sur les 90 % restant et de tester le modèle sur ces 10 % "caché". On affiche ensuite les scores pondérés en fonction du nombre d’occurrence de journaux dans chaque catégorie.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(text_clf, # steps to convert raw messages into models
lf_data.content, # training data
lf_data.quality_content, # training labels
cv=10, # split data randomly into 10 parts: 9 for training, 1 for scoring
scoring='accuracy', # which scoring metric?
n_jobs=-1, # -1 = use all cores = faster
)
print(scores)
print('Total diaries classified:', len(lf_data))
print('Score:', sum(scores)/len(scores))
[ 0.54 0.53 0.55 0.55 0.56 0.57 0.54 0.52 0.56 0.56]
Total diaries classified: 5921
Score: 0.548226957256
Le score est égal à 0.55. Ce n'est pas terrible. Si on préfère afficher la matrice de confusion, il faut utiliser les Kfold qui reposent sur le même principe que cross_val_score
et implémenter une boucle.
from sklearn.model_selection import KFold
from sklearn.metrics import confusion_matrix, f1_score,precision_score
k_fold = KFold(n_splits=10)
scores = []
confusion = np.array([[0, 0,0,0], [0, 0,0,0], [0, 0,0,0], [0, 0,0,0]])
for train_indices, test_indices in k_fold.split(lf_data):
train_text = lf_data.iloc[train_indices]['content'].values
train_y = lf_data.iloc[train_indices]['quality_content'].values
test_text = lf_data.iloc[test_indices]['content'].values
test_y = lf_data.iloc[test_indices]['quality_content'].values
text_clf.fit(train_text, train_y)
predictions = text_clf.predict(test_text)
confusion += confusion_matrix(test_y, predictions)
score = f1_score(test_y, predictions, average='weighted')
ps = precision_score(test_y, predictions, average='weighted')
scores.append(score)
print('Total diaries classified:', len(lf_data))
print('Score:', sum(scores)/len(scores))
print('Confusion matrix:')
print(confusion)
Total diaries classified: 5921
Score: 0.519244446873
Confusion matrix:
[[1475 22 13 1104]
[ 253 11 16 180]
[ 164 15 26 97]
[ 794 7 8 1736]]
scores
[0.48812704076867125,
0.50096444244611738,
0.53296513209879548,
0.50865953156976373,
0.53358760110311787,
0.52464153844229733,
0.53897239391380014,
0.5090212038928732,
0.5340084448235829,
0.5214971396677468]
Comme on le voit, les résultats sont très mauvais. Environ 44 % des journaux "Average Troll" sont attribués à la classe "Quality Troll" ! Si les auteurs suivent la même logique que cet algorithme, ils ont tendance à sur-estimer fortement leurs écrits. De même, 30 % des "Quality Troll" sont attribués à la classe "Average Troll". En suivant cette logique, les auteurs de contenu de qualité auraient tendance à se sous-estimer. Par ailleurs, il faut noter que ces classes sont voisines : score de 0 à 20 et de 20 à l'infini (et au delà).
Plus inquiétant : les contenus avec un score négatif sont attribués majoritairement aux classes à score positif. Un auteur de contenu moinsé qui penserait comme la machine serait persuadé que son texte est de qualité. Il ne comprendrait pas le score négatif qui en résulte.
Optimisation des paramètres
Et si nos mauvais résultats étaient dus au choix d'un mauvais jeu de paramètres de départ ? Le pipeline choisi dépend de nombreux paramètres ajustables. Scikit-learn permet d'optimiser ces paramètres facilement afin de trouver le meilleur compromis.
from sklearn.linear_model import SGDClassifier
text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', SGDClassifier()),])
Les paramètres ajustables sont précédés du nom de l'étape correspondante. Les explications concernant ces paramètres sont disponibles dans la documentation officielle :
sorted(text_clf.get_params().keys())
['clf',
'clf__alpha',
'clf__average',
'clf__class_weight',
'clf__epsilon',
'clf__eta0',
'clf__fit_intercept',
'clf__l1_ratio',
'clf__learning_rate',
'clf__loss',
'clf__n_iter',
'clf__n_jobs',
'clf__penalty',
'clf__power_t',
'clf__random_state',
'clf__shuffle',
'clf__verbose',
'clf__warm_start',
'steps',
'tfidf',
'tfidf__norm',
'tfidf__smooth_idf',
'tfidf__sublinear_tf',
'tfidf__use_idf',
'vect',
'vect__analyzer',
'vect__binary',
'vect__decode_error',
'vect__dtype',
'vect__encoding',
'vect__input',
'vect__lowercase',
'vect__max_df',
'vect__max_features',
'vect__min_df',
'vect__ngram_range',
'vect__preprocessor',
'vect__stop_words',
'vect__strip_accents',
'vect__token_pattern',
'vect__tokenizer',
'vect__vocabulary']
Le code ci-dessous permet d'ajuster les paramètres suivants :
-
tfidf__use_idf
: utilisation ou non de la pondération par la fréquence des mots ; -
clf__loss
: le type de fonction qui "caractérise" la perte ou encore le type de transition entre les catégories. Cette transition peut être abrupte ou plus ou moins lissée. (illustration du phénomène (P 20)) ; -
clf__alpha
un paramètre mathématique strictement positif.
Évidemment, le temps de calcul dépend du nombre de paramètres à ajuster. Les autres paramètres sont laissés à leur valeur par défaut.
params = {
'tfidf__use_idf': (True, False),
'clf__loss':('huber', 'modified_huber', 'epsilon_insensitive', 'hinge', 'log'),
'clf__alpha':(1,0.001, 0.00001),}
gs_clf = GridSearchCV(text_clf, params, n_jobs=-1, verbose=0, refit=True,scoring='accuracy',)
print("Performing grid search...")
print("pipeline:", [name for name, _ in text_clf.steps])
print("parameters:")
print(params)
t0 = time()
gs_clf = gs_clf.fit(lf_data.content, targets)
print("done in %0.3fs" % (time() - t0))
print()
print("Best score: %0.3f" % gs_clf.best_score_)
print("Best parameters set:")
best_parameters = gs_clf.best_estimator_.get_params()
for param_name in sorted(params.keys()):
print("\t%s: %r" % (param_name, best_parameters[param_name]))
Ce qui donne :
Performing grid search...
pipeline: ['vect', 'tfidf', 'clf']
parameters:
{'tfidf__use_idf': (True, False), 'clf__loss': ('huber', 'modified_huber', 'epsilon_insensitive', 'hinge', 'log'), 'clf__alpha': (1, 0.001, 1e-05)}
done in 108.027s
Best score: 0.547
Best parameters set:
clf__alpha: 0.001
clf__loss: 'modified_huber'
tfidf__use_idf: True
Malheureusement, le score semble encore assez bas. Par ailleurs, le meilleur estimateur est également disponible pour utilisation future :
gs_clf.best_estimator_
Pipeline(steps=[('vect', CountVectorizer(analyzer='word', binary=False, decode_error='strict',
dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',
lowercase=True, max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), preprocessor=None, stop_words=None,
strip... penalty='l2', power_t=0.5, random_state=None, shuffle=True,
verbose=0, warm_start=False))])
Test sur un échantillon de données connues
Comme précédemment, il est possible de tester le modèle sur un échantillon de données connues. L'ajustement a été réalisé avec le meilleur jeu de paramètres grâce à l'option refit=True
passée à GridSearchCV
. Les résultats du score F1 sont encore une fois très bons mais l'amélioration du score est nulle : il plafonne entre 0.95 et 0.96.
print(metrics.classification_report(diaries_test.quality_content, predicted, target_names=targets_names))
precision recall f1-score support
Average Troll 0.99 0.93 0.96 523
Great Troll 1.00 0.94 0.97 80
Magnificent Troll 1.00 0.94 0.97 72
Quality Troll 0.92 0.99 0.96 509
avg / total 0.96 0.96 0.96 1184
De même, la matrice de confusion est excellente :
Test sur un échantillon de données inconnues
Pour aller plus loin, j'ai testé le modèle sur de nouvelles données (des journaux plus anciens). Ces données ne font pas partie de mes journaux de base. En principe, le résultat sera similaire à ce qu'on obtient par validation croisée mais cette technique a pour avantage d'augmenter la taille de la base de journaux disponibles. Une autre possibilité consiste à relancer la validation croisée après avoir fusionné ces nouvelles données aux anciennes.
filename = r'out_of_sample.csv'
lf_out = pd.read_csv(filename, encoding="UTF-8", sep='£', engine='python', quotechar='µ')
lf_out = lf_out.reindex(np.random.permutation(lf_out.index))
lf_out.quality_content.value_counts().plot(kind='bar')
plt.ylabel('Occurences', fontsize='xx-large')
plt.yticks(fontsize='xx-large')
plt.xlabel('Trolls', fontsize='xx-large')
plt.xticks(fontsize='xx-large')
Ces nouvelles données sont similaires aux journaux déjà disponibles.
predicted_out = text_clf.predict(lf_out.content)
np.mean(predicted_out == lf_out.quality_content)
score_out = f1_score(lf_out['quality_content'], predicted_out, average='weighted')
print('Diaries:', len(lf_out))
print('Score:', score_out)
cnf_matrix_out = confusion_matrix(lf_out['quality_content'], predicted_out)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix_out, classes=targets_names,
title='Confusion matrix, without normalization')
# Plot normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix_out, classes=targets_names, normalize=True,
title='Normalized confusion matrix')
plt.show()
print(metrics.classification_report(lf_out.quality_content, predicted_out, target_names=targets_names))
Diaries: 1500
Score: 0.444809984556
Confusion matrix, without normalization
[[452 9 10 457]
[ 90 4 0 52]
[ 42 5 6 26]
[126 2 0 219]]
Normalized confusion matrix
[[ 0.49 0.01 0.01 0.49]
[ 0.62 0.03 0. 0.36]
[ 0.53 0.06 0.08 0.33]
[ 0.36 0.01 0. 0.63]]
precision recall f1-score support
Average Troll 0.64 0.49 0.55 928
Great Troll 0.20 0.03 0.05 146
Magnificent Troll 0.38 0.08 0.13 79
Quality Troll 0.29 0.63 0.40 347
avg / total 0.50 0.45 0.44 1500
Malheureusement, le résultat n'est pas bon. Encore une fois, le modèle ne peut pas s'adapter à des données inconnues. Il s'agit d'un cas assez probant de surapprentissage. L'image suivante illustre bien le problème. En cherchant à classer correctement les éléments dans la bonne catégorie, le modèle se contorsionne et ne tient pas compte de la tendance "globale".
Utiliser des propriétés multiples
Bien qu'elle soit informative, l'analyse ne permet pas de prédire la catégorie avec un score supérieur à 0,5. Pour l'instant, le classificateur se comporte comme un mauvais élève pressé d'aller jouer un match de tennis après son examen Q.C.M. : il répond la même chose (la réponse D) à toutes les questions en se disant qu'il obtiendra bien la moitié. Évidemment, cela ne fonctionne pas. L'approche "bag of words" seule ne suffit pas pour classer des journaux. Les bons journaux ne sont pas tous techniques, de même que les mauvais ne sont pas tous "politiques" (quoiqu'un journal sur l'avortement part en général très mal). Le sujet d'un journal n'est pas corrélé avec sa note finale. D'autres indicateurs doivent être pris en compte : ancienneté du compte au moment de la soumission, taille du texte (les journaux trop courts sont parfois descendu, tout comme les 'journaux fleuve parfois hallucinés' dixit oumph). scikit-learn permet de combiner plusieurs propriétés (appelées "features"), de déterminer celles qui ont le plus gros impact sur les résultats et d'ajuster un modèle en tenant compte des propriétés sélectionnées.
Extraction et préparation des données
L'analyse suivante repose sur l'utilisation des données présentes dans le fichier linuxfr_complete.csv
. Elle correspond au notebook diaries_classification_2.ipynb. En plus des données présentes dans le fichier linuxfr.csv
, ce document comporte les champs suivant :
- la date de création du journal ;
- la date de création du compte ;
- les scores précédents de l'auteur (première page des anciennes publications) ;
- la longueur du document.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import sys
import matplotlib
# Enable inline plotting
%matplotlib inline
filename = r'linuxfr_complete.csv'
lf_data = pd.read_csv(filename, encoding="UTF-8", sep='£', engine='python', quotechar='µ')
Conversion des dates
Panda permet très facilement de convertir une chaîne de caractère correspondant à une date au format datetime
.
lf_data['birthday'] = pd.to_datetime(lf_data['birthday'])
lf_data['birthday'].head()
0 2004-08-28
1 2003-04-22
2 2004-02-14
3 2012-10-22
4 2009-10-05
Name: birthday, dtype: datetime64[ns]
lf_data['datetime'] = pd.to_datetime(lf_data['datetime'])
lf_data['datetime'].head()
0 2017-05-28 12:59:46
1 2017-05-28 09:57:04
2 2017-05-28 08:24:57
3 2017-05-27 14:18:10
4 2017-05-26 20:12:47
Name: datetime, dtype: datetime64[ns]
Évolution du score des journaux au fil du temps
score_df = lf_data[['datetime', 'score']].copy()
score_df.index = score_df['datetime']
del score_df['datetime']
L'évolution des scores au fil du temps est alors facilement affichable. Je trouve personnellement qu'on n'a pas trop à se plaindre : la qualité générale des journaux est plutôt bonne.
score_df.plot(marker='o', grid=True, figsize=(15,9))
plt.ylabel('Score', fontsize='xx-large')
plt.yticks(fontsize='xx-large')
plt.xlabel('Date', fontsize='xx-large')
plt.xticks(fontsize='xx-large')
Calcul de l’âge d’un compte
L'âge d'un compte peut facilement être calculé en soustrayant la date de création du compte à la date de création du journal. Pour une raison inconnue, cet âge est parfois négatif. Le code suivant tient compte de ce souci. Un compte qui a moins d'un jour se voit affublé de la propriété "Newbie".
lf_data['age'] = lf_data['datetime']-lf_data['birthday']
lf_data['newbie'] = False
for index, line in lf_data.iterrows():
# Problem: sometimes, age << 0
if line['age'] < pd.Timedelta("0 day"):
line['age'] = - line['age']
if line['age'] < pd.Timedelta("1 day"):
lf_data.set_value(index, 'newbie', True)
Qualité des posts des nouveaux
Il est à présent possible d'extraire les informations relatives aux nouveaux comptes (à la date de publication). Ces comptes sont à l'origine de contenu de qualité étonnante. On retrouve une grande quantité de très mauvais contenu ("magnificent troll") mais également de bons et très bons contenus ("quality troll" et "average troll").
noob = lf_data.loc[lf_data['newbie'] == True]
noob.quality_content.value_counts().plot(kind='bar')
plt.ylabel('Occurences', fontsize='xx-large')
plt.yticks(fontsize='xx-large')
plt.xlabel('Trolls', fontsize='xx-large')
plt.xticks(fontsize='xx-large')
Calcul de la moyenne des scores précédents
Afin de tenir compte de l'historique d'un compte, deux colonnes sont ajoutées : la médiane et la moyenne. Les scores précédents sont conservés dans la colonne author_previous_scores
. L'information, une chaîne de caractère sous la forme "[1,15,42,-12]"
, doit être extraite.
lf_data['median_score'] = 0
lf_data['average_score'] = 0
import statistics
for index, line in lf_data.iterrows():
ps = line['author_previous_scores']
#print(ps)
ps = ps.replace("[",'')
ps = ps.replace("]",'')
ps = ps.replace(",",'')
ps = ps.split()
ps = [float(x) for x in ps]
median = statistics.median(ps)
try:
avg = statistics.mean(ps)
except TypeError:
avg = np.NaN
lf_data.set_value(index, 'median_score', median)
lf_data.set_value(index, 'average_score', avg)
La plupart du temps, la médiane et la moyenne sont très proches. Dans de rares cas, elles diffèrent beaucoup mais la moyenne est plus sévère que la médiane.
Garder l’essentiel
Les informations nécessaires pour réaliser une analyse plus complète sont à présent disponibles. Pour plus de facilité, de nouveaux dataframes sont créés en éliminant les colonnes inutiles.
lf = lf_data[['content','newbie','average_score',
'quality_content', 'score', 'count', 'author']].copy()
target = lf_data[['quality_content']].copy()
L’union fait la force
Maintenant que je dispose d'un dataframe contenant mes variables (lf
) et un autre contenant mes catégories attendues (target
), il faut que je crée une procédure permettant d'effectuer les bonnes tâches avec le bon jeu de données :
- Les données numériques sont utilisées telles quelles.
- Le corps de l'article est vectorisé et la fréquence des mots est calculée.
- Le nom de l'auteur est vectorisé également.
Ces trois étapes sont unies dans un object FeatureUnion
dans un pipeline dont la dernière étape est un classificateur de type linéaire (SVC(kernel='linear')
). Encore une fois, les fonctions .fit
et .predict
sont accessibles depuis le pipeline pour faciliter son utilisation.
La classe MultipleItemSelector
permet d'extraire les données nécessaires à chaque étape.
Enfin, un poids est appliqué à chaque étape. Pour l'instant, il est égal sur les trois étapes mais des valeurs différentes ont donné des résultats similaires :
- 'author': 0.8 ;
- 'content': 0.5 ;
- 'num_values': 1.0.
# From http://scikit-learn.org/stable/auto_examples/hetero_feature_union.html
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.datasets import fetch_20newsgroups
from sklearn.datasets.twenty_newsgroups import strip_newsgroup_footer
from sklearn.datasets.twenty_newsgroups import strip_newsgroup_quoting
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction import DictVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report
from sklearn.pipeline import FeatureUnion
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
class MultipleItemSelector(BaseEstimator, TransformerMixin):
def __init__(self, keys):
self.keys = keys
def fit(self, x, y=None):
return self
def transform(self, data_dict):
return data_dict[self.keys]
pipeline = Pipeline([
# Extract the subject & body
#('subjectbody', DataExtractor()),
# Use FeatureUnion to combine the features from subject and body
('union', FeatureUnion(
transformer_list=[
# Pipeline for pulling features from the post's subject line
('author', Pipeline([
('selector', MultipleItemSelector(keys='author')),
('tfidf', TfidfVectorizer(min_df=50)),
])),
# Pipeline for standard bag-of-words model for body
('content', Pipeline([
('selector', MultipleItemSelector(keys='content')),
('tfidf', TfidfVectorizer()),
('best', TruncatedSVD(n_components=50)),
])),
# Pipeline dealing with numerical values stored in a dict
('num_values', Pipeline([
('selector', MultipleItemSelector(keys=['score', 'newbie', 'average_score', 'count']))
, # list of dicts -> feature matrix
])),
],
# weight components in FeatureUnion
transformer_weights={
'author': 1.0, # 0.8
'content': 1.0, # 0.5
'num_values': 1.0, # 1.0
},
)),
# Use a SVC classifier on the combined features
('svc', SVC(kernel='linear')),
])
#pipeline.fit(lf, target.values.ravel())
Validation croisée
Afin de valider le comportement du classificateur, la validation croisée est effectuée avec 10 échantillons. Cette fois, les résultats sont vraiment très bons.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(pipeline, # steps to convert raw messages into models
lf, # training data
target.values.ravel(), # training labels
cv=10, # split data randomly into 10 parts: 9 for training, 1 for scoring
#scoring='accuracy', # which scoring metric?
scoring='f1_weighted',
n_jobs=-1, # -1 = use all cores = faster
)
print(scores)
print('Total diaries classified:', len(lf_data))
print('Score:', sum(scores)/len(scores))
[ 1. 1. 1. 1. 1. 1. 1.
0.99831646 0.99831646 1. ]
Total diaries classified: 5955
Score: 0.999663292104
Afin de valider le comportement exceptionnel de ce classificateur, un test est réalisé avec des données qu'il ne connaît pas du tout : des journaux plus anciens.
Données hors échantillon.
La prédiction à l'aide de journaux anciens permet de vérifier que le modèle se comporte bien. Si c'est le cas, cela permet également d'affirmer que le type de contenu pertinent/inutile n'a pas radicalement changé ces dernières années.
Les données analysées dans cette section correspondent au fichier out_of_sample_complete.csv
. La liste des journaux est sensiblement la même que celle utilisée dans le paragraphe Test sur un échantillon de données inconnues.
Les images ci-dessous montrent que la distribution temporelle de scores est similaire aux données plus récentes. La matrice de confusion et le score confirment les résultats obtenus à l'aide de la validation croisée. Le but est atteint. Il est possible de prédire la catégorie dans laquelle se trouve un journal à partir de son contenu et du nom de l'auteur. 1
Diaries: 1485
Score: 1.0
from sklearn import metrics
print(metrics.classification_report(Y_out, predicted_out, target_names=targets_names))
precision recall f1-score support
Average Troll 1.00 1.00 1.00 917
Great Troll 1.00 1.00 1.00 145
Magnificent Troll 1.00 1.00 1.00 77
Quality Troll 1.00 1.00 1.00 346
avg / total 1.00 1.00 1.00 1485
Pour aller plus loin
Scikit-learn dispose de nombreuses autres possibilités pour traiter des données de tout type. Je citerai la sélection des propriétés (feature selection) qui permet d’éliminer les propriétés dont la variance est inférieure à un seuil donné. Dans ce cas, la propriété est considérée comme une constante. L'intérêt est de diminuer le temps de calcul et le risque de sur-apprentissage. Un test rapide sur les données issues des journaux a montré que toutes les données sont utiles pour déterminer la catégorie d'un journal.
Choisir le bon algorithme peut être très difficile selon les informations désirées et le type de données. L'aide-mémoire suivant permet de faciliter ce choix. Une version interactive est également disponible.
Enfin, je terminerai en mentionnant la possibilité de sauver un modèle entraîné. Cela permet d'éviter de devoir repasser par l'opération d'apprentissage qui peut être consommatrice de ressources. Le fichier généré pour le modèle le plus efficace de cette dépêche fait 46 Mo.
from sklearn.externals import joblib
joblib.dump(pipeline, 'linuxfr_pipeline.pkl')
Plus tard ou sur une autre machine:
from sklearn.externals import joblib
pipeline = joblib.load('linuxfr_pipeline.pkl')
La documentation mentionne des considérations à prendre en compte.
- Il existe un risque de sécurité dans le cas où on charge des données car cela pourrait mener à l'exécution de code malicieux ;
- la comptabilité entre versions n'est pas prise en compte ;
- il est important de laisser à disposition un jeu de données types, le score de validation du modèle associé et le code afin que la personne qui reçoit le modèle puisse vérifier les résultats obtenus.
Conclusions
Au cours de cette expérience, une moulinette a été codée afin d'aspirer le contenu des journaux.
Les données ont été analysées en deux temps. Dans la première phase, la vectorisation du contenu des journaux a été réalisée. Il a été montré que cette étape ne suffit pas à pouvoir classer correctement du contenu inconnu. Dans une seconde phase, le nom de l'auteur, la date de publication, la date de création du compte, l'historique récente des scores des publications de l'auteur ont été pris en compte et assemblés dans un pipeline d'analyse. Les résultats ont montré qu'il est possible de prédire la catégorie dans laquelle se trouve un journal à partir de son contenu et du nom de l'auteur. Le taux d'erreur est inférieur à 1/1000. Par ailleurs, l'optimisation des paramètres des classificateurs ainsi que la validation croisées ont été présentées.
L'analyse prédictive des scores permettra plusieurs grandes avancées sur le site. La première et la plus évidente sera la possibilité de renvoyer un lien vers cette dépêche chaque fois qu'un contributeur se plaindra de la note réservée à sa prose. Il s'agit là de l'argument ultime qui ne manquera pas de faire taire les trolls devant tant d'autorité. La seconde avancée sera la possibilité pour un contributeur d'améliorer ses journaux afin d'atteindre à chaque fois la catégorie visée. Provoquer un séisme de moinsage ou atteindre le summum de l'excellence ne s'improvise pas et scikit-learn permettra à chacun d'évaluer différentes variantes de ses journaux afin de poster la "meilleure". La troisième avancée concerne les journaux qui ne peuvent pas avoir été élaboré par un esprit humain. Ils sont probablement générés par une machine. Les plaisantins pourront améliorer les textes de leur programme en utilisant le modèle présenté afin de rendre la lecture du contenu plus agréable. La note finale s'en ressentira.
Pour terminer, je dirai qu'il est possible de rapidement effectuer des analyses de données avec scikit-learn. La syntaxe est très simple pour les personnes connaissant Python. Les concepts sont assez compliqués, mais la mise en œuvre est très bien faite et la documentation officielle est complète. Mais, tous ces éléments positifs ne garantissent pas des résultats probants et immédiats. Comme dans toute matière complexe, il faut comprendre ce qu'on fait pour obtenir des résultats qui ont du sens (et être en mesure de les analyser).
Perspectives
Plusieurs pistes de réflexion pourront permettre de poursuivre l'analyse :
- Le découpage des catégories est arbitraire (bornes -20 et + 20) ; en modifiant la répartition des données, les résultats seront probablement différents (exemples : répartitions en quartiles, score strictement positifs ou négatifs, etc.) ;
- au vu des résultats obtenus, la prédiction du score (valeur numérique) est envisageable ;
- les catégories ne sont pas équitablement peuplées, le nombre de journaux à score négatif est beaucoup plus faible dans ce cas ; pour y remédier, nous avons besoin de plus de journaux de mauvaise qualité abondamment moinssés. À vos claviers !
- La classification par un système d'arbre en limitant leur profondeur n'a pas été testée ;
- les paramètres du meilleur pipeline n'ont pas été optimisés.
Rêvons un peu
Un éventail se possibilités s'ouvre à la communauté LinuxFR.org. La liste ci-dessous reprend les éléments qui me viennent en premier à l'esprit.
- Réaliser une analyse temporelle des scores pour prédire la note d'un journal à venir en tenant compte de l'historique de publication général (comme la bourse) ;
- prédire la note d'un commentaire (après avoir modifié la moulinette) ;
- modifier le modèle afin de prédire le nombre de commentaires. Les contributeurs pourraient alors toucher du clavier la recette permettant de créer les trolls ultimes qui permettraient d'atteindre des sommets d'excellence, de courtoisie et de bienveillance dans une avalanche de remarques plus palpitantes les unes que les autres ;
- …
N'hésitez pas à partager dans les commentaires vos suggestions, vos impressions et vos idées innovantes ! De même, le dépôt gitlab est accessible. Je vous invite à tester vos recettes sur les données présentes et à les exposer dans un journal ou une dépêche. Les différences entre les modèles peuvent être difficiles à appréhender. Toute explication complémentaire sera la bienvenue.
Note
-
Les scores précédents et la date de création du score peuvent facilement être déduite à l'aide du nom de l'auteur. ↩
Aller plus loin
- Dépôt GitLab de l’analyse (157 clics)
- Scikit-learn (143 clics)
- Beautifulsoup (124 clics)
- Python Data Analysis Library (pandas) (103 clics)
# Merci aux contributeurs
Posté par jnanar (site web personnel) . Évalué à 10.
Je voudrais remercier tous les contributeurs de la dépêche. C'est ma première dépêche je ne m'attendais pas à voir autant de contributions extérieures qui ont vraiment permis d'améliorer la qualité de l'analyse et du texte. DLFP est encore bien vivant! Merci !
# Prédiction
Posté par Elfir3 . Évalué à 10. Dernière modification le 09 juin 2017 à 09:50.
Va falloir rajouter la classe "Über qualitaÿ troll of ze dead" pour cette dépèche :-)
Un grand merci à tous les contributeurs pour ce bel article digne d'un Pierre Tramo !
[^] # Re: Prédiction
Posté par Framasky (site web personnel) . Évalué à 7.
Oui, c'est vraiment un très bon article et manifestement un bien bon outil. Je te tire mon chapeau !
Being a sysadmin is easy. As easy as riding a bicycle. Except the bicycle is on fire, you’re on fire and you’re in Hell.
# Arrosage de l'arroseur
Posté par Philip Marlowe . Évalué à 10. Dernière modification le 09 juin 2017 à 10:03.
Il sera intéressant, une fois que la note générale de ta dépêche sera stabilisée, de l'étudier récursivement selon les méthodes qu'elle expose.
edit: s/ton journal/ta dépêche/
[^] # Re: Arrosage de l'arroseur
Posté par jnanar (site web personnel) . Évalué à 10.
Je l'ai déjà fait sur le texte avant soumission. Elle était classée dans la catégorie "Great Troll" (-20 < n < 0). Contrairement aux journaux aspirés, le texte était au format markdown. L'absence de formatage HTML et la longueur du texte sont peut-être à l'origine de la classification. Ou alors, la cabale Linuxfr n'a pas encore moinsé. A suivre !
[^] # Re: Arrosage de l'arroseur
Posté par Anthony Jaguenaud . Évalué à 4.
Ta moulinette fonctionne pour les journaux, as-tu testé sur les dépêches… car la notation n’est forcément pas la même.
[^] # Re: Arrosage de l'arroseur
Posté par jnanar (site web personnel) . Évalué à 3.
Non, j'ai entraîné le modèle uniquement sur les journaux. Je pense aussi que le comportement des votant est légèrement différent pour les dépêches. L'influence de la longueur du texte est aussi différente. Les dépêches moinsées sont en général plus courtes et le vocabulaire employé est plus typiques (contenu "commercial"). La toute grosse majorité des dépêches notées négativement est à -1. Il faudrait modifier la moulinette pour les analyser. Cela ne doit pas être très difficile. L'ancienneté du compte est peut-être moins cruciale (mis à part pour le contenu commercial).
[^] # Re: Arrosage de l'arroseur
Posté par Nicolas Boulay (site web personnel) . Évalué à 6.
As-tu tenté les "digram" en gros les groupes de 2 mots voir plus ? Cela permet de trouver les expressions. Bien sûr, il faut virer les mots simples et les digram trop peu fréquent.
Tu parles "d'extraction de feature" manuel, as-tu un mode automatique ? Le principe même du deep learning est de trouver ses fameuses features tout seul.
As-tu tenté de prédire les notes des commentaires ? C'est souvent le plus problématique pour un site web.
"La première sécurité est la liberté"
[^] # Re: Arrosage de l'arroseur
Posté par jnanar (site web personnel) . Évalué à 2.
Oui, j'ai essayé de modifier le paramètre
ngram_range=(1, 2)
pour trouver des "unigrams" et des "digrams" dans mes premiers essais (première partie). Malheureusement, cela ne fonctionnait pas car la vectorisation du texte n'est pas suffisante. Il faudrait lancer une optimisation des paramètres sur le pipeline des propriétés jointes (partie 2). Comme les résultats étaient excellents avec les paramètres par défaut, je n'ai pas cherché à raffiner le score.Concernant l'extraction des propriétés, comme je suis dépendant de la moulinette, j'ai choisis manuellement des paramètres qui ont du sens afin de ne pas trop rallonger les temps de calculs. Avec la base de données complète, il serait intéressant de laisser le modèle se débrouiller pour trouver les paramètres clés. Il est possible de laisser le programme sélectionner les propriétés les plus intéressantes (celles qui ont le plus une incidence sur le score):
http://scikit-learn.org/stable/modules/feature_selection.html
# async
Posté par dzecniv . Évalué à 4.
J'aurais bien étudié une moulinette en async/await :)
# Couplage avec IPoT
Posté par Benoît Sibaud (site web personnel) . Évalué à 10.
Avec IPoT + la moulinette, tu peux éviter d'éviter d'écrire les journaux qui seront mal notés pour te concentrer sur ceux qui seront bien notés. Sans parler de la discussion avec son soi futur pour parler de l'ancienneté du compte et des contenus/commentaires de mon futur et de son passé, tout ça pour extraite la quintessence de la DLFPtitude.
[^] # Re: Couplage avec IPoT
Posté par copapa . Évalué à 5.
Oui mais modifier ainsi le continuum espace temps modifierai également la base d’apprentissage et donc apporterai une sorte d'élitisme à ce classifieur; les great trolls glissant vers la catégorie magnificent trolls et ainsi de suite. Par application récursive, nous arriverions à une situation ou aucun journal ne serait publié avant d'atteindre une qualitaÿ parfaite !
Nom de Zeus !
[^] # Re: Couplage avec IPoT
Posté par Colin Pitrat (site web personnel) . Évalué à 5.
Ou alors juste afficher la note au preview, ce qui permet de retoucher jusqu'à l'obtention de la note recherchée
# Correction
Posté par Benjamin Verhaeghe (site web personnel) . Évalué à 0.
•20 < n : Qualitaÿ Troll.
Correction •20 > n : Qualitaÿ Troll.
[^] # Re: Correction
Posté par jnanar (site web personnel) . Évalué à 3.
Un troll de qualité a une note supérieure à 20. n > 20 ou 20 < n. La dépêche est correcte.
# Moi, j'aime pas être analysé
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 10. Dernière modification le 09 juin 2017 à 14:57.
Demain, je ne publie plus mes journaux que dans des images !!
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 6. Dernière modification le 09 juin 2017 à 16:03.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Moi, j'aime pas être analysé
Posté par Benoît Sibaud (site web personnel) . Évalué à 7.
Je pense que tu as eu un trouble dans ton enfance, en lien avec un décès violent des parents et un sentiment de culpabilité. Plus un penchant pour la violence, le cuir, les masques, et les chauve-souris (bien qu'il s'agisse aussi d'un traumatisme initial). Et une phobie de l'analyse apparemment.
# Commentaire supprimé
Posté par Anonyme . Évalué à 10.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Record
Posté par Gauthier Monserand (site web personnel) . Évalué à 2.
Ça ne me viendrai même pas à l'idée de tenter :)
[^] # Re: Record
Posté par Anthony Jaguenaud . Évalué à 3.
Bravo, effectivement : -135 et -450 c’est des beaux scores… je suis sûr que même dopé, je n’arriverai pas à te battre. Par contre, tu peux essayer de te battre toi même, non ?
# L'emo me manque
Posté par DL . Évalué à 7.
En un mot: wow. Autant ces derniers temps je lisais la plupart les journaux linux.fr d'un oeil lointain, autant je suis épaté par la qualité de celui-ci: le sujet, comment il à été traité, la clarté des explications -scikit.learn et les algos - et gitlab accessible avec son coulis de Jupyter.
Bravo.
# Super !
Posté par barmic . Évalué à 5.
Bravo !
J'avais l'idée de déterminer des niveaux de troll à partir de la forme des commentaires. Sans même en étudier le contenu, en prenant comme paramètres :
On doit pouvoir faire un détecteur de troll. Aujourd'hui l'image informant que tu es entrain de répondre à un troll vient d'une heuristique uniquement basée sur la profondeur (alors que des fois ça n'est pas du tout du troll).
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: Super !
Posté par barmic . Évalué à 3. Dernière modification le 09 juin 2017 à 17:53.
Hé ! J'ai perdu mon avatar !
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
# Prédictions
Posté par davandg . Évalué à 10.
Je ne suis pas un pro, mais voici mes 2c.
Ma petite expérience : ajoute un maximum de features. L'algo d'apprentissage se débrouillera tout seul pour donner de l'importance où non à la feature. Si ça commence à ramer, il existe plusieurs méthodes pour enlever les features inutiles (ou qui donne peu d'information) (va voir la doc de scikit :).
Sois imaginatif pour les features. Tu peux partir sur des features que tu penses rentrer en compte dans la qualité d'un Troll. Mes idées, à la volée : % du nombre de retours à la ligne par rapport au nombre de mots (aération du texte), nombre d'images, le jour de la semaine de la publication…
Je suis convaincu que tu pourrais obtenir une assez bonne qualité de classification avec uniquement les métadonnées.
Tu trouveras de super explications un peu partout sur le net, voici la mienne en quelques lignes.
Comme dans tout algo de Machine Learning, tu part d'un modèle construit à partir des features. Ce modèle est composé de plein de paramètres. Tu utilises ensuite un algorithme d'optimisation pour trouver les paramètres qui colle le mieux aux observations.
Dans les SVM, le modèle est super simple. Pour chaque feature "A", tu lui attribues un paramètres "a". Ensuite tu les combines linéairement:
valeur finale = a*A + b*B + c*C + d*D…
Lorsque tu fais de la classification, la valeur finale te donne un pourcentage : la confiance en l'appartenance à une catégorie donnée. Si tu fais de la régression la valeur finale est la valeur que tu chercheras à prédire (le score du journal pour nous).
En phase d'apprentissage, l'algo d'optimisation va chercher les valeurs des a,b,c,d… qui vont faire en sorte que les valeurs finales prédites soit la plus proche des valeurs finales observées.
Une fois l'apprentissage fini, tu connais les a,b,c,d… Pour prédire une valeur il te suffit d’effectuer l'opération et de récupérer le résultat.
Pour l'optimisation, on utilise une descente de gradient stochastique. Nous avons vu juste avant que l'algo d'optimisation cherche les meilleures valeurs des a,b,c,d… par rapport aux donnés que l'on connaît. Cet algo va donc chercher à minimiser l'erreur de prédiction commise. Elle va donc cherche à diminuer ("descente") l'erreur de prédiction, en faisant varier les paramètres. Ces paramètres, on ne le fait pas changer au hasard, mais en se rapprochant doucement ("gradient") des points les plus bas, en faisant des sauts de temps en temps ("stochastique").
C'est très souvent ce genre d'algo d'optimisation utilisé, car ils fonctionnent super bien si le modèle à les bonnes propriétés mathématiques. Si tu n'as pas ces propriétés, il faut bourriner avec des variantes de recuit simulés (algo génétique, Monte Carlo…).
[^] # Re: Prédictions
Posté par jnanar (site web personnel) . Évalué à 4.
Merci pour les explications, c'est un peu plus clair. Comme expliqué plus bas, il y a une erreur qui explique les scores aussi bons (la note est prise en compte dans les données de départ). Je vais tenter d'améliorer le modèle et d'augmenter le nombre de features.
# test/train
Posté par Thomas (site web personnel) . Évalué à 5.
Salut
C'est sympa scikit-learn, n'est-ce pas ?!
Pas le temps de creuser, mais j'ai une remarque: tu différencies bien tes dataset d'entraînement et de test ? Ca fausse tous les scores, sinon. La routine test_train_split (ou ~) sera ton amie.
Si le sujet du machine learning t'intéresse, je me permets de t'indiquer ce site : http://oceandata.io (le mien). Jouer avec les données, c'est mon gagne-pain.
# Mouais
Posté par Thomas (site web personnel) . Évalué à 4.
Bon, j'ai lu.
Pas jusqu'au bout, pas le temps.
En effet, il y a parfois confusion test/train, ce qui donne des scores de fou … normal.
De deux, je suis pas sur de la qualité du dataset de base. Faudrait voir s'il y a une qcq cohérence entre le contenu et la note. Fitter du bruit, ca doit donner du bruit. Ca manque cruellement de découverte/qualification des données d'entrées, padawan.
Mais bel effort :)
Et SGD != SVM.
[^] # Re: Mouais
Posté par jnanar (site web personnel) . Évalué à 4.
Merci pour le retour et les conseils. Je pense que la procédure de calcul du score par la validation croisée sépare les données en train et test. Ensuite, le gros problème est dû à une faute d'inattention. Le pipeline qui unit les features tient compte du score. Je m'en suis rendu compte hier. Le système connaît la réponse, ce qui rend les prédictions triviales. Je cherche une solution pour obtenir de bons scores sans ça. Je ferai un erratum dans un journal. Je débute dans le domaine (moins d'un mois) et je voulais m'attaquer à un problème ludique plutôt que de suivre les tutoriels classiques. C'était aussi l'occasion de tenter de communiquer sur le sujet (de manière imparfaite) sur ce site.
[^] # Re: Mouais
Posté par Nicolas Boulay (site web personnel) . Évalué à 5.
Le conseil du maximum de feature donné plus haut et bon je pense.
Il y a longtemps j'avais fais un concours d'IA, un des participants utilisaient une IA généré par algo génétique. L'outil générait du code javascript. Le niveau de l'IA a été limité par un truc stupide : les entrées étaient trop réduites. Au début, il avait artificiellement fusionné/regroupé/trier des données. Ensuite, il était trop tard pour relancer toutes la génération.
"La première sécurité est la liberté"
# Cool !
Posté par lorinc . Évalué à 9.
Super sympa comme projet.
Si tu cherches de la doc en ML plutôt sympa, je te recommande les vidéos Youtube d'Hugo Larochelle (en anglais et en français). Ce sont ses cours à l'université de Sherbrooke qui ont été filmés, et c'est ce vers quoi j'envoie mes étudiants curieux.
Si tu fais du surapprentissage, rajouter des features ne va pas résoudre le problème, au contraire. Le sur-apprentissage peut venir soit d'un modèle trop complexe par rapport à la réalité (comme sur ton image), soit du fait que des données de train ne sont pas représentatives de l'univers (problème de sampling, donc). Dans ton cas, comme tu utilises un classifieur linéaire, c'est difficilement lui qui peut être mis en cause. De plus, sa complexité est directement liée à la dimension des features (théorie de Vapnik), et rajouter des features va amplifier le phénomène plutôt que le corriger.
Donc, à mon avis, tu devrais plutôt t'orienter vers autre chose. D'abord t'assurer que tes données de train sont cohérentes avec celles de test. Ensuite, Tf/idf est particulièrement moche et surtout très bruité. Tu devrais pré-traiter tes vecteurs d'entrée pour supprimer les mots inutiles. Une PCA peut faire l'affaire, mais des outils de features selection feront sans doute mieux. Regarde aussi si tu peux mieux régulariser ton SVM, idéalement avec une norme l1 qui te permettra d'éliminer les mots (ou combinaisons de mots) non-informative.
Tu devrais aussi regarder les outils de deep learning pour comparer. Il y a pleins de papiers intéressant sur des réseaux convolutionnels ou récurrents pour apprendre des représentations à partir d'un texte.
En tout, encore bravo pour cette dépêche, c'est sympa de voir des gens faire ce genre de projet en dehors de projet académiques.
[^] # Re: Cool !
Posté par jnanar (site web personnel) . Évalué à 5.
Merci pour ces commentaires. Je vais creuser le sujet et analyser les données plus en détail avant d'essayer d'ajuster des modèles. Les "pairplots" de seaborn sont instructifs (par le manque de corrélation). Les données sont très bruitées. En tout cas, la discrimination sur le score n'est pas évidente à partir de features extraites du texte. Je vais continuer de lire et apprendre sur le sujet pour mieux comprendre le sujet. Je note les références, mots clés et la références aux vidéos pour plus tard. Les discussion et recommandations qui découlent de la dépêche sont passionnantes.
# petit détail sur les csv: pas besoin d'utiliser des caractères exotiques
Posté par Xavier Combelle (site web personnel) . Évalué à 4.
Si j'ai bien suivi tu utilise des caractères exotiques pour les séparateurs et délimiteurs. Ceci est inutile. En effet un champ avec des délimiteurs
Sera enregistré dans un csv comme ci après, en doublant les délimiteurs et en ajoutant un délimiteur à la fin et au début.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.