Classification de textes par réseaux de neurones

L'objectif de ce projet est d'apprendre à utiliser les réseaux de neurones pour la classification des textes.

Les objectifs de ce projet sont :

  1. Former un modèle peu profond (peu de couches) avec un apprentissage par embeddings (difficile à traduire en français).
  2. Télécharger et utiliser un embeddings pré-entrainé de Glove

Le jeu de données d'informations de la BBC (classification par sujet)

La BBC fournit quelques jeux de données de référence sur la classification des sujets d'information en anglais. Ils sont disponibles à l'adresse : http://mlg.ucd.ie/datasets/bbc.html.

Le texte brut (encodé avec le codage de caractères latin-1) peut être téléchargé sous forme d'archive ZIP. Le code suivant permet de télécharger et décompresser le jeu de données.

In [ ]:
import os
import os.path as op
import zipfile
try:
    from urllib.request import urlretrieve
except ImportError:
    from urllib import urlretrieve


BBC_DATASET_URL = "http://mlg.ucd.ie/files/datasets/bbc-fulltext.zip"
zip_filename = BBC_DATASET_URL.rsplit('/', 1)[1]
BBC_DATASET_FOLDER = 'bbc'
if not op.exists(zip_filename):
    print("Downloading %s to %s..." % (BBC_DATASET_URL, zip_filename))
    urlretrieve(BBC_DATASET_URL, zip_filename)

if not op.exists(BBC_DATASET_FOLDER):
    with zipfile.ZipFile(zip_filename, 'r') as f:
        print("Extracting contents of %s..." % zip_filename)
        f.extractall('.')

Chacun des cinq dossiers contient des fichiers texte de l'un des cinq sujets :

In [ ]:
target_names = sorted(folder for folder in os.listdir(BBC_DATASET_FOLDER)
                      if op.isdir(op.join(BBC_DATASET_FOLDER, folder)))
target_names

Nous allons créer une partition aléatoire apprentissage-test des fichiers de texte en enregistrant la catégorie cible de chaque fichier comme un nombre entier :

In [ ]:
import numpy as np
from sklearn.model_selection import train_test_split

target = []
filenames = []
for target_id, target_name in enumerate(target_names):
    class_path = op.join(BBC_DATASET_FOLDER, target_name)
    for filename in sorted(os.listdir(class_path)):
        filenames.append(op.join(class_path, filename))
        target.append(target_id)

target = np.asarray(target, dtype=np.int32)
target_train, target_test, filenames_train, filenames_test = train_test_split(
    target, filenames, test_size=200, random_state=0)
In [ ]:
len(target_train), len(filenames_train)
In [ ]:
len(target_test), len(filenames_test)

Q.1 affichage d'un document

Écrire un bout de code Python qui permet d'afficher le texte d'un document comme suit :

  • Choisir un numéro de fichier du jeu de données d'apprentissage à afficher idx=0

  • Faire appel à l'instruction with ..... as ... pour lire le fichier texte (utiliser le mode de lecture "rb" dans l'appel à la fonction open)

  • Afficher la catégorie d'information (la classe du document)

  • Afficher les 300 premiers caractères du fichier texte (utiliser l'encodage latin-1 à la lecture du fichier).

Q.2 taille mémoire de l'ensemble des fichiers du jeu de données d'apprentissage

L'instruction suivante permet de récupérer la taille du premier fichier de la liste en byte (octet)

In [ ]:
fn = filenames_train[0]
print(len(open(fn, 'rb').read()))

à l'aide de l'instruction for, calculer la somme des tailles des fichiers du jeu de données d'apprentissage et afficher la taille totale en Mo comme suit

In [ ]:
print("Training set size: %0.3f MB" % size_in_megabytes)

Le jeu de données semble de taille raisonnable, nous allons le charger en entier en mémoire

In [ ]:
texts_train = [open(fn, 'rb').read().decode('latin-1') for fn in filenames_train]
texts_test = [open(fn, 'rb').read().decode('latin-1') for fn in filenames_test]

Q.3 création d'un premier modèle de référence

Il est naturel de faire appel à un modèle de base pour commencer. Dans ce genre de problème (données textuelles), il faut toujours essayer une méthode simple en premier lieu. Nous allons ajuster un modèle de régression logistique pour assigner un sujet à un texte. Il est indispensable de tranformer un texte d'un document en un vecteur de variables explicatives pour pouvoir implémenter ce modèle. Pour transformer un texte en vecteur numérique, une technique de référence consiste à extraire un (sac de mots) normalisé de caractéristiques bi-grammes du TF-IDF. Ce billet peut être utile pour comprendre le calcul d'un sac de mots de type TF-IDF. Le code suivant permet de charger les fonctions et librairies nécessaires pour ajuster un premier modèle de régression logistique de référence.

In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline

Écrire et tester un pipeline text_classifier composé de deux étapes

  1. TfidfVectorizer avec les paramètres min_df=3, max_df=0.8 et ngram_range=(1, 2).
  2. Une régression logistique multinomiale, attention le solver doit être"lbfgs".

à faire :

  • Ajuster le modèle et afficher le temps de calcul en mettant un %time _ = ...... avant votre instruction.

  • Calculer et afficher le taux de bon classement du modèle logistique.

Modèles à base de sacs de mots continus (CBOW)

Nous mettrons en place un modèle de classification simple à l'aide de la librairie Keras. Le texte brut nécessite un prétraitement (parfois important).

Les cellules suivantes utilisent Keras pour prétraiter le texte via :

  1. Utilisation d'un tokenizer. Vous pouvez utiliser différents tokenizers (de scikit-learn, NLTK, fonction Python personnalisée, etc.) ). Cela permet de convertir les textes en séquences d'indices représentant les 20 000 mots les plus fréquents

  2. les séquences obtenues ont des longueurs différentes, donc on ajoute des 0 à la fin jusqu'à ce que la séquence soit de longueur 1000)

  3. nous convertissons les classes de sortie en codages binaire (one-hot)

In [ ]:
from tensorflow.keras.preprocessing.text import Tokenizer

MAX_NB_WORDS = 20000

# vectorize the text samples into a 2D integer tensor (matrix)
tokenizer = Tokenizer(num_words=MAX_NB_WORDS, char_level=False)
tokenizer.fit_on_texts(texts_train)
sequences = tokenizer.texts_to_sequences(texts_train)
sequences_test = tokenizer.texts_to_sequences(texts_test)

word_index = tokenizer.word_index
print('%s tokens uniques trouvés.' % len(word_index))

Les séquences obtenues sont sous formes de listes d'identifiants de tokens (avec un code donné par un nombre entie):

In [ ]:
sequences[0][:10]

L'objet tokenizer stocke une correspondance (vocabulaire) entre des chaînes de mots et des identifiants de token qui peuvent être inversés pour reconstruire le message original (sans formatage) comme suit :

In [ ]:
type(tokenizer.word_index), len(tokenizer.word_index)
In [ ]:
index_to_word = dict((i, w) for w, i in tokenizer.word_index.items())
" ".join([index_to_word[i] for i in sequences[0]])

Examinons de plus près les séquences obtenues par ce procédé:

In [13]:
seq_lens = [len(s) for s in sequences]
print("longueur moyenne: %0.1f" % np.mean(seq_lens))
print("longueur maximale: %d" % max(seq_lens))
longueur moyenne: 382.6
longueur maximale: 4355

On remarque que la grande majorité des postes ont moins de 1000 symboles :

In [15]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.hist([l for l in seq_lens if l < 3000], bins=50);

Q.4 Préparation des données

  • Tronquer les séquences sequences et sequences_test à une longueur maximale de 1000 à l'aide de la fonction pad_sequences. Stocker les résultats dans deux objets x_train et x_test et afficher les dimensions des deux objets obtenus.
  • Appliquer la fonction to_categorical à target_train pour créer l'objet y_train. Afficher les dimensions de l'objet y_train.
In [16]:
## chargement de la fonction pad_sequences et de la fonction to_categorical
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical

Voici un modèle très simple qui consistitué des 3 éléments suivants:

  1. Construction d'une couche d'embedding qui associe chaque mot à une représentation vectorielle

  2. Calcul de la représentation vectorielle de tous les mots de chaque séquence et en faire la moyenne

  3. Ajout d'une couche dense pour obtenir 20 classes (+ softmax)

In [ ]:
from tensorflow.keras.layers import Dense, Input, Flatten
from tensorflow.keras.layers import GlobalAveragePooling1D, Embedding
from tensorflow.keras.models import Model
from tensorflow.keras import optimizers

MAX_SEQUENCE_LENGTH = 1000
EMBEDDING_DIM = 50
N_CLASSES = len(target_names)

# input: a sequence of MAX_SEQUENCE_LENGTH integers
sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32')

embedding_layer = Embedding(MAX_NB_WORDS, EMBEDDING_DIM,
                            input_length=MAX_SEQUENCE_LENGTH,
                            trainable=True)
embedded_sequences = embedding_layer(sequence_input)

average = GlobalAveragePooling1D()(embedded_sequences)
predictions = Dense(N_CLASSES, activation='softmax')(average)

model = Model(sequence_input, predictions)
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.Adam(lr=0.01), metrics=['acc'])
In [ ]:
model.fit(x_train, y_train, validation_split=0.1,
          epochs=10, batch_size=32)
  • Quelle est l'erreur de test du modèle obtenu ci-dessus ? (écrire le code qui permet de l'évaluer)

Q.5 Création d'un modèle plus complexe

  • À partir du modèle précédent, construire des modèles plus complexes en utilisant:
  • Deux séquences de couches Conv1D (128 filtres de taille 5) et MaxPooling1D (de taille 5) et Conv1D (64 filtres de taille 5) et MaxPooling1D (de taille 5) à insérer entre la couche d'embedding et la couche Dense. Notez que vous aurez toujours besoin d'un Flatten après les convolutions car la couche Dense finale nécessite une entrée de taille fixe. Entrainer le modèle obtenu et calculer son erreur de test.
  • Reprendre le modèle précédent (avec les deux séquences de couches Conv1D et MaxPooling1D). Insérer dans le réseau une couche LSTM(de taille 64) (la couche LSTM est du type Recurrent neural networks).

  • Commenter le rapport complexité/efficacité des modèles testés.

Utilisation d'un embeddings pré-entraîné

Le fichier file glove100K.100d.txt est une extraction de Glove. (Ce billet est nécessaire pour comprendre la construction d'un embeddings). Nous avons extrait les 100000 mots les plus fréquents. Ils ont une dimension de 100.

In [18]:
embeddings_index = {}
embeddings_vectors = []
with open('glove100K.100d.txt', 'rb') as f:
    word_idx = 0
    for line in f:
        values = line.decode('utf-8').split()
        word = values[0]
        vector = np.asarray(values[1:], dtype='float32')
        embeddings_index[word] = word_idx
        embeddings_vectors.append(vector)
        word_idx = word_idx + 1

inv_index = {v: k for k, v in embeddings_index.items()}
print("Nous avons trouvé %d mots différents dans le fichier" % word_idx)
Nous avons trouvé 100000 mots différents dans le fichier
In [19]:
# Empiler tous les embeddings dans un numpy array
glove_embeddings = np.vstack(embeddings_vectors)
glove_norms = np.linalg.norm(glove_embeddings, axis=-1, keepdims=True)
glove_embeddings_normed = glove_embeddings / glove_norms
print(glove_embeddings.shape)
(100000, 100)

Q.6 représentation d'un mot

  • Écrire deux fonctions get_emb(word) et get_normed_emb(word) qui renvoient respectivement l'embedding et l'embedding normé d'un mot word. Si word n'est pas dans la liste, la fonction doit renvoyer None (valeur inconnue). Tester la fonction avec le mot computer pour afficher le vecteur de représentation du mot computer dans cet embedding (plongement dans un espace numérique).

Q.7 mots similaires dans notre plongement numérique

  • Écrire une fonction qui prend en entrée un mot word et un nombre de mots similaires à afficher topn (10 par défaut) et qui fournit en retour un dictionnaire (mot, similarité) composé du topn mots les plus similaires au sens de la similarité cosinus (égale au produit scalaire de deux vecteurs normés). Afficher les 10 mots les plus similaires à cpu.