Deviner qui est l'auteur d'un message grâce au Machine Learning

On va essayer de savoir qui je suis

Je sais qui je suis, mais quelques uns m’ont déjà posé cette question : "Qui es-tu ?" et ben, c’est dans ce billet que je souhaite répondre.

Je me doute bien que si je me contente de dire, je suis XXX, on risque de me traiter de menteur, alors j’aimerai apporter la preuve que je suis qui je suis avec l’aide d’algorithmes de machine learning.

Sans plus attendre, raisonnons ensemble !

Allons à la recherche d'informations sur moi

Tout ce que l’on sait sur moi se trouve sur Zeste de Savoir. Et, croyez le ou pas, tout ce que l’on dit sur internet, même derrière un pseudonyme, permet de nous identifier. Sur internet on peu changer notre adresse email, notre pseudonyme, notre adresse ip, notre FAI, mais quelque chose de très compliqué à changer, c’est notre façon de parler/d’écrire. Et c’est comme ça que je vais vous prouver que je suis ce que je suis.

Nous allons nous concentrer sur tout ce que j’ai pu dire sur les forums de Zeste de Savoir. Pour cela, j’ai écrit un script qui me permet de collecter l’ensemble de mes messages sur le site.

Nous allons partir de l’hypothèse suivante : le jour ou j’ai écris cet article, j’étais infiltré dans le staff. Ce qui signifie donc que la liste des suspects est réduite aux membres du staff ce jour là.

C’est la raison pour laquelle on va aussi télécharger l’ensemble des messages des membres du staff à cette époque ainsi que des messages de ceux dont on est certain que ce n’est pas moi (des contres exemples quoi).

En résumé, voici les données que je vous propose de télécharger pour tester le code donné par la suite.

Lien de téléchargement

Le contenu de l’archive est le suivant :

fichier/dossier Description
staff.csv Fichier csv contenant la liste des membres du staff à l’époque ou le fameux article a été publié.
data.csv Liste des phrases que l’on trouve sur le forum. La colonne target est affectée à 1 lorsque c’est une phrase écrite par willard et à 0 si la phrase n’a pas été écrite par lui.
sentences Répertoire avec une liste de fichiers csv contenant des phrases de plusieurs membres, téléchargées sur le forum Zeste de Savoir
build_model.py Script de construction du modèle
guess.py Script qui permet de deviner qui est willard.

Une fois les fichiers téléchargés, vous devez obtenir l’arborescence suivante:

.
├── build_model.py
├── data.csv
├── guess.py
├── sentences
│   ├── sentences_10199.csv
│   ├── sentences_101.csv
│   ├── sentences_110.csv
│   ├── sentences_1309.csv
│   ├── sentences_138.csv
│   ├── sentences_141.csv
│   ├── sentences_143.csv
│   ├── sentences_155.csv
│   ├── sentences_2315.csv
│   ├── sentences_241.csv
│   ├── sentences_243.csv
│   ├── sentences_244.csv
│   ├── sentences_267.csv
│   ├── sentences_276.csv
│   ├── sentences_288.csv
│   ├── sentences_2.csv
│   ├── sentences_326.csv
│   ├── sentences_3349.csv
│   ├── sentences_379.csv
│   ├── sentences_542.csv
│   ├── sentences_5500.csv
│   ├── sentences_588.csv
│   ├── sentences_615.csv
│   ├── sentences_65.csv
│   ├── sentences_66.csv
│   ├── sentences_67.csv
│   ├── sentences_68.csv
│   ├── sentences_69.csv
│   ├── sentences_72.csv
│   ├── sentences_73.csv
│   ├── sentences_747.csv
│   ├── sentences_74.csv
│   ├── sentences_80.csv
│   ├── sentences_819.csv
│   ├── sentences_82.csv
│   ├── sentences_90.csv
│   ├── sentences_92.csv
│   └── sentences_94.csv
└── staff.csv

Construisons notre modèle de prédiction

Installation des libs externes

Pour nos travaux nous travaillons avec python 3.6. Nous aurons besoin d’installer les libs python suivantes:

pip install pandas
pip install scikit-learn

Séparer nos données (train et test)

Pour construire notre modèle, nous allons commencer par séparer nos données en deux catégories. Les données d’entrainement (celles qui vont servir à entraîner notre modèle) et les données de validation (celles qui vont permettre de valider que notre modèle est juste).

L’opération se fait grâce au code suivant :

import os
import pandas as pd
from sklearn.model_selection import train_test_split

# Import des données
data = pd.read_csv('data.csv')
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))

texts = data.sentence
target = data.target

# definir la colonne target comme étant de type booléen
target = target.astype('bool')

# split des données en deux catégories
sentences_train, sentences_test, y_train, y_test = train_test_split(texts.values, target.values, test_size=0.3, random_state=1000)

Nous venons d’effectuer une séparation dont 30% de nos données initiales serviront pour l’apprentissage.

La vectorisation du texte

Notre variable principale (la colonne X) contient surtout du texte. Le problème c’est que le texte ne se donne pas comme cela aux algorithmes, il faut au préalable le vectoriser. C’est à dire, transformer une phrase en une liste de nombre. On peut le voir comme la version mathématique de la phrase.

Il existe plusieurs types de vectorisation, et chacun s’utilise en fonction des cas que l’on veut traiter. Pour notre cas, nous utiliserons la vectorisation par comptage de mots.

En python ça donnera ceci :

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer()
vectorizer.fit(sentences_train)

# vectorise les phrases d'entrainement et de test avec CountVectorizer
X_train = vectorizer.transform(sentences_train)
X_test  = vectorizer.transform(sentences_test)

Maintenant que l’on dispose de la forme vectorisée des phrases, on enregistre le modèle de vectorisation dans un fichier (on le réutilisera quand il faudra effectuer les prédictions).

import pickle
pickle.dump(vectorizer, open(os.path.join(CURRENT_DIR, "vectorize.data"), 'wb'))

L’apprentissage du modèle

Nos phrases sont vectorisées, cela signifie que l’on peut les passer à un algorithme. Mais la question principale qui se pose est :

Quel est l’algorithme dois-je utiliser ?

Hey oui, il y en a tellement des algorithmes, mais on sait néanmoins choisir les algorithmes en fonction du type de nos variables. Par exemple observons ce que nous avons :

  • Nos variables sont des vecteurs (oui, on vient de les calculer)
  • On essaye de prédire une cible de type booléenne (willard or not willard ?)

Vous l’avez deviné, on est tout a fait dans le cas à traiter par une régression logistique.

  • Régression parce qu’on essaye de prédire quelque chose, on parle aussi d’expliquer un résultat en fonction d’une ou plusieurs variables.
  • Logistique parce que la variable que l’on essaye de prédire n’est pas quantitative, mais qualitative (booléenne dans notre cas).

C’est parti pour le code d’apprentissage.

from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
# l'apprentissage se fait bien sur la variable vectorisée de l'entrainement
model.fit(X_train, y_train)
# on calcule la précision du modèle sur les données de validation
score = model.score(X_test, y_test)

print("Precision:", score)

La précision renvoyée est :

Precision: 0.9800575263662512

On a donc ici une précision de 98% avec notre modèle d’apprentissage. Autant dire que c’est formidable. On saura, avec ce modèle prédire avec 98% de chances qu’une phrase a été écrite par moi. Fantastique non ?

Vite, vite ! Enregistrons notre modèle.

import pickle

pickle.dump(model, open(os.path.join(CURRENT_DIR,"model.data"), 'wb'))

Vous pouvez tout simplement lancer la commande python build_model.py qui résume ce que l’on a vu dans cette section.

Deux fichiers seront crées, model.data et vectorize.data.

A la recherche de l'infiltré

L’infiltré est un membre de l’ancien staff qui écrit de la même manière que moi. On va donc chercher dans la liste des messages des membres de l’ancien staff, lesquels correspondent à des phrases de Willard.

Le code qui permet de le calculer est le suivant :

# guess.py
import os
import csv
import pickle
import pandas as pd

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
# on charge le modèle de vectorisation
vectorizer = pickle.load(open(os.path.join(CURRENT_DIR,"vectorize.data"), 'rb'))
# on charge le modèle d'apprentissage
model = pickle.load(open(os.path.join(CURRENT_DIR,"model.data"), 'rb'))
res = []

def get_ids_from_file(filepath):
    """
    Cette fonction renvoi la liste des id dans un fichier à deux colonnes "id;username"
    """
    ids = []
    with open(filepath, 'r') as csvfile:
        readercsv = csv.reader(csvfile, delimiter=',', quotechar='"')
        next(readercsv)
        for row in readercsv:
            ids.append(row[0])

    return ids

def get_username_from_id(filepath, id):
    """
    Cette fonction renvoi le nom d'utilisateur correspondant à un id
    """
    with open(filepath, 'r') as csvfile:
        readercsv = csv.reader(csvfile, delimiter=',', quotechar='"')
        next(readercsv)
        for row in readercsv:
            if row[0] == id:
              return row[1]

    return None

def predict_from_sent(x):
    """
    Cette fonction renvoi True si la phrase est de willard.
    """
    p_value = model.predict(vectorizer.transform([x]))

    return p_value[0]

# on recupère la liste des ids des staff de l'époque
ids = get_ids_from_file("staff.csv")
for id in ids:
    data = pd.read_csv('sentences/sentences_{}.csv'.format(id), header=None)
    data["is_willard"] = data[1].map(predict_from_sent)
    guilty = data.loc[data['is_willard'] == True]
    res.append({"id": get_username_from_id("staff.csv", id), "guilty": len(guilty.index)})

print(sorted(res, key = lambda i: i['guilty'], reverse=True))

Vous pouvez tout simplement lancer la commande python guess.py.

Si vous lancer le code chez vous (après avoir construit le modèle), vous pouvez retrouver qui sur ce forum, s’amuse à parler comme moi.

Pour les plus paresseux (mais je n’en dirais pas plus), sachez que le coupable se trouve parmi les cinq membres suivants (il s’agit des cinq membres dont les messages ressemblent furieusement à ma façon d’écrire):

  • Andr0
  • Arius
  • firm1
  • Kje
  • ShigeruM

Bon courage.


L’objectif de ce billet était de montrer que, plus on poste sur les forums, les réseaux sociaux, plus les algorithmes arriveront à nous identifier.

Maintenant que vous savez qui je suis, une question éthique se pose

Est-ce que je peux être sanctionné uniquement parce qu’un algorithme l’a décidé ? Souhaitons nous faire confiance aux machines pour prendre des décisions importantes ?

Je vous laissez réfléchir !

37 commentaires

Est-ce que je peux être sanctionné uniquement parce qu’un algorithme l’a décidé ? Souhaitons nous faire confiance aux machines pour prendre des décisions importantes ?

C’est bizarre, mais t’as pas eu besoin d’un robot pour ton premier ban :o

L’article est intéressant.

Est-ce-que y’a des gens que le jeu de piste pour trouver Willard intéresse (genre des gens qui connaissent pas déjà la réponse, et qui voudraient recouper la liste des 5 profils donnés avec plus d’indices) ?

En un seul mot, on peut relancer une seconde source d’analyse (non seulement de texte, mais de dates aussi).

C’est via ce biais que j’ai appris qui était willard avant qu’il ne commence à en faire un business :o

+1 -0

Ça m’intéresse effectivement.

Cependant, j’ai pas le temps de me lancer là dedans >_<" !

Peut-être quand la période des examens sera passée ?

+0 -0

J’ai pris 5 minutes pour lancer effectivement le modèle via les données et les scripts fournis par @willard.

Le texte ce de billet dit ceci :

Precision: 0.9800575263662512

On a donc ici une précision de 98% avec notre modèle d’apprentissage. Autant dire que c’est formidable. On saura, avec ce modèle prédire avec 98% de chances qu’une phrase a été écrite par moi. Fantastique non ?

Je vais moi aussi poser une hypothèse : je sais qui est Willard, de manière certaine.

Or, le script ne donne pas le bon résultat. Ici, je vois plusieurs explications possibles :

  1. On tombe dans les 1,994247363 % de chances de mal tomber (pas de bol, surtout que les scores ne sont même pas serrés).
  2. Le code et/ou le jeu de données ne sont pas bons (2.1. par hasard, ou 2.2. parce que @willard s’est arrangé pour les trafiquer, je ne sais dire).
  3. La personne derrière @willard se débrouille depuis le début pour que ses messages ressemblent aux messages de la personne qui remonte dans ce script et pas à ses messages habituels (fourbe n’est-ce pas ?).
  4. C’est moi qui vous ment (évidemment que non, mais c’est une hypothèse que vous devez prendre en compte dans le raisonnement).

En tous cas, je persiste et signe : tout ça nécessiterait une étude un peu plus indépendante qu’un code et des données fournies par le principal intéressé. Surtout que lorsqu’il s’intéresse à des tiers, willard est bien capable de fournir des scripts et données qui fonctionnent.

Juste par mesure de précaution.

Les codes sources :

import pandas as pd
import os
from sklearn.model_selection import train_test_split
import pickle
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression

# Import data
data = pd.read_csv('data.csv')
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))

texts = data.sentence
target = data.target

# define target column as boolean type
target = target.astype('bool')

sentences_train, sentences_test, y_train, y_test = train_test_split(texts.values, target.values, test_size=0.3, random_state=1000)

vectorizer = CountVectorizer()
vectorizer.fit(sentences_train)

# vectorize train and test data with CountVectorizer
X_train = vectorizer.transform(sentences_train)
X_test  = vectorizer.transform(sentences_test)

pickle.dump(vectorizer, open(os.path.join(CURRENT_DIR, "vectorize.data"), 'wb'))

model = LogisticRegression()
# l'apprentissage se fait bien sur la variable vectorisée de l'entrainement
model.fit(X_train, y_train)
# on calcule la précision du modèle sur les données de validation
score = model.score(X_test, y_test)

print("Precision:", score)

pickle.dump(model, open(os.path.join(CURRENT_DIR,"model.data"), 'wb'))
build_model.py
import os
import pickle
import csv
import pandas as pd

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
# on charge le modèle de vectorisation
vectorizer = pickle.load(open(os.path.join(CURRENT_DIR,"vectorize.data"), 'rb'))
# on charge le modèle d'apprentissage
model = pickle.load(open(os.path.join(CURRENT_DIR,"model.data"), 'rb'))
res = []

def get_ids_from_file(filepath):
    urls = []
    with open(filepath, 'r') as csvfile:
        readercsv = csv.reader(csvfile, delimiter=',', quotechar='"')
        next(readercsv)
        for row in readercsv:
            urls.append(row[0])

    return urls

def get_username_from_id(filepath, id):
    with open(filepath, 'r') as csvfile:
        readercsv = csv.reader(csvfile, delimiter=',', quotechar='"')
        next(readercsv)
        for row in readercsv:
            if row[0] == id:
              return row[1]

    return None

def predict_from_sent(x):
    """
    Cette fonction renvoit True si la phrase est de willard.
    """
    p_value = model.predict(vectorizer.transform([x]))

    return p_value[0]

# on recupère la liste des ids des staff de l'époque
ids = get_ids_from_file("staff.csv")
for id in ids:
    data = pd.read_csv('sentences/sentences_{}.csv'.format(id), header=None)
    data["is_willard"] = data[1].map(predict_from_sent)
    guilty = data.loc[data['is_willard'] == True]
    res.append({"id": get_username_from_id("staff.csv", id), "guilty": len(guilty.index)})

print(sorted(res, key = lambda i: i['guilty'], reverse=True))
guess.py

Staff :

id,pseudo
2,ShigeruM
65,adri1
66,devock
67,Andr0
68,Alex-D
69,Fumble
72,SpaceFox
73,Arius
74,Talus
80,Taguan
90,nohar
92,Caduchon
141,Kje
143,Holosmos
155,Ymox
244,Natalya
101,Eskimon
94,Coyote
138,firm1
379,Thunderseb
747,Algue-Rythme
326,zeqL
267,viki53
588,tcit
110,simbilou
241,coma94
243,Micmaths

Il manque les données bien-sûr mais comme le dis, Spacefox il faut s’en méfier, le code source me semble plus important.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte