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 :
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.
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 :
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 :
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)
len(target_train), len(filenames_train)
len(target_test), len(filenames_test)
É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).
L'instruction suivante permet de récupérer la taille du premier fichier de la liste en byte (octet)
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
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
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]
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.
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
TfidfVectorizer
avec les paramètres min_df=3
, max_df=0.8
et ngram_range=(1, 2)
."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.
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 :
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
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)
nous convertissons les classes de sortie en codages binaire (one-hot)
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):
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 :
type(tokenizer.word_index), len(tokenizer.word_index)
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é:
seq_lens = [len(s) for s in sequences]
print("longueur moyenne: %0.1f" % np.mean(seq_lens))
print("longueur maximale: %d" % max(seq_lens))
On remarque que la grande majorité des postes ont moins de 1000 symboles :
%matplotlib inline
import matplotlib.pyplot as plt
plt.hist([l for l in seq_lens if l < 3000], bins=50);
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. to_categorical
à target_train
pour créer l'objet y_train
. Afficher les dimensions de l'objet y_train
.## 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:
Construction d'une couche d'embedding qui associe chaque mot à une représentation vectorielle
Calcul de la représentation vectorielle de tous les mots de chaque séquence et en faire la moyenne
Ajout d'une couche dense pour obtenir 20 classes (+ softmax)
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'])
model.fit(x_train, y_train, validation_split=0.1,
epochs=10, batch_size=32)
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. Conv1D
et MaxPooling1D
). Insérer dans le réseau une couche LSTM
(de taille 64) (la couche LSTM est du type Recurrent neural networks).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
.
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)
# 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)
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).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
.