Accéder au contenu principal

NLP avec PyTorch : guide complet

Bien démarrer avec le NLP : un tutoriel PyTorch pour débutants
· 12 min lire

Explorez l'IA

Introduction au NLP et à PyTorch

Le traitement du langage naturel (NLP) est un pilier de l’IA moderne, qui permet aux machines de comprendre et de répondre au langage humain. Avec l’explosion des interactions numériques, son importance ne cesse de croître. PyTorch, bibliothèque open source de machine learning très populaire, offre des outils robustes pour les tâches de NLP grâce à sa flexibilité et à ses calculs de tenseurs efficaces. Son graphe computationnel dynamique facilite également la modification et la construction de modèles complexes, ce qui en fait un choix idéal pour ce tutoriel.

Pour aller plus loin en NLP, consultez notre parcours de compétences Natural Language Processing in Python, ou si vous préférez apprendre les bases du NLP en R, découvrez le cours Introduction to Natural Language Processing in R sur DataCamp.

Mise en place de l’environnement

La configuration de l’environnement PyTorch peut parfois s’avérer délicate selon le système d’exploitation, le gestionnaire de paquets, le langage de programmation et la plateforme de calcul utilisés. Le processus d’installation peut donc légèrement varier et nécessiter des commandes spécifiques.

Pour obtenir la commande d’installation adéquate, rendez-vous sur la page officielle get started de PyTorch : sélectionnez vos préférences et suivez les instructions affichées.

image7.png

Nous utiliserons DataLab pour ce tutoriel. Le code complet pour entraîner un modèle d’analyse de sentiment avec PyTorch est disponible dans ce workbook DataLab si vous souhaitez suivre pas à pas.

Introduction aux tenseurs

image2.png

Les tenseurs sont des structures de données fondamentales en mathématiques et en physique, généralisant scalaires, vecteurs et matrices. Ce sont des tableaux multidimensionnels capables de stocker et de manipuler efficacement de grands volumes de données numériques. Les tenseurs ont une forme, une taille et un type d’unité définis, ce qui les rend très polyvalents pour diverses opérations de calcul.

Dans PyTorch, les tenseurs sont les blocs de construction et l’objet de représentation des données principal. Ils sont proches des tableaux NumPy, avec des fonctionnalités supplémentaires et des optimisations conçues pour l’apprentissage profond. Les opérations sur les tenseurs tirent parti de l’accélération matérielle, notamment des GPU, pour calculer efficacement des réseaux de neurones complexes.

Les tenseurs jouent un rôle crucial dans les tâches de NLP en raison de la nature séquentielle et hiérarchique du langage.

Le NLP consiste à traiter et à comprendre des informations textuelles. Les tenseurs permettent de représenter et de manipuler des textes en encodant des mots, des phrases ou des documents sous forme de vecteurs numériques.

Cette représentation numérique permet aux modèles d’apprentissage profond de traiter et d’apprendre efficacement à partir de textes. Les tenseurs facilitent la gestion de grands jeux de données, l’entraînement de réseaux de neurones et des techniques avancées comme les mécanismes d’attention pour des modèles de NLP plus précis.

Word embeddings

image5.png

Les word embeddings sont des représentations vectorielles denses de mots dans un espace continu. Ils capturent les relations sémantiques et syntaxiques entre les mots, ce qui améliore la compréhension et la contextualisation des textes. En représentant les mots par des vecteurs numériques, les embeddings saisissent proximités et différences sémantiques, permettant aux algorithmes de manipuler des mots comme des entrées numériques porteuses de sens.

En clair, les embeddings sont une façon astucieuse de représenter les mots par des nombres. Ces nombres portent une signification qui reflète les relations entre mots : une sorte de code qui aide les ordinateurs à comprendre et traiter plus facilement le langage.

Word2Vec et GloVe sont deux méthodes populaires pour générer des embeddings. Word2Vec est un modèle à réseau de neurones qui apprend les représentations en prédisant les mots du contexte à partir d’un mot cible (CBOW) ou le mot cible à partir de son contexte (skip-gram).

GloVe (Global Vectors for Word Representation) est une méthode fondée sur les cooccurrences, qui construit des vecteurs de mots à partir des statistiques de coapparition dans un large corpus. Elle capture des relations globales et offre souvent de meilleures performances sur les analogies de mots.

Architecture de modèle NLP

image4.png

Exemple d’architecture pour une tâche d’analyse de sentiment.

L’analyse de sentiment est une tâche courante en NLP qui vise à comprendre le sentiment exprimé dans un texte, le plus souvent classé en positif, négatif ou neutre. Pour l’aborder, on peut utiliser un réseau de neurones récurrent (RNN) simple ou une variante plus avancée appelée long short-term memory (LSTM).

RNN

L’architecture RNN traite le texte séquentiellement, mot par mot. Le réseau maintient un état caché qui évolue à chaque entrée et capture l’information accumulée jusque-là.

Cet état caché joue le rôle de mémoire du réseau. Cependant, les RNN standards peinent sur de longues séquences à cause du problème du gradient qui s’annule : la contribution des informations anciennes décroît avec le temps et le réseau finit par les oublier. Pour en savoir plus, consultez notre tutoriel RNN. 

LSTM

Pour y remédier, les LSTM, une variante de RNN, ont été conçus. Un LSTM maintient un contexte plus long grâce à une structure interne plus complexe de son état caché. Il comporte des « portes » (d’entrée, d’oubli et de sortie) qui contrôlent le flux d’information dans et hors de l’état mémoire.

La porte d’entrée détermine quelle part de l’information entrante conserver, la porte d’oubli décide quoi écarter, et la porte de sortie règle la quantité d’état interne transmise à la prochaine unité LSTM.

Un modèle RNN ou LSTM prend en entrée une séquence de mots d’une phrase ou d’un document. Chaque mot est généralement représenté par un vecteur dense (embedding) capturant sa signification sémantique.

Le réseau traite la séquence mot par mot, en mettant à jour son état interne en fonction du mot courant et de l’état précédent.

L’état final sert ensuite à prédire le sentiment. Il est passé dans une couche complètement connectée, puis une fonction d’activation softmax pour produire une distribution de probabilités sur les classes de sentiment (p. ex. : positif, négatif, neutre).

La classe à la probabilité la plus élevée est retenue comme prédiction du modèle.

Ce dispositif de base peut être amélioré avec des LSTM bidirectionnels (traitement dans les deux sens) et des mécanismes d’attention (focalisation sur les parties importantes de la séquence), entre autres.

Entraîner un modèle LSTM avec PyTorch pour l’analyse de sentiment

Exemple de code Python de bout en bout pour construire un modèle d’analyse de sentiment avec PyTorch

1. Charger le jeu de données

Dans cet exemple, nous utilisons l’IMDB dataset of 50K Movie reviews. L’objectif est d’entraîner un modèle LSTM pour prédire le sentiment. Deux valeurs possibles : « positive » et « negative ». Il s’agit donc d’une classification binaire.

file_name = 'IMDB Dataset.csv'
df = pd.read_csv(file_name)
df.head()

Sample dataset

2. Analyse exploratoire

X,y = df['review'].values,df['sentiment'].values
x_train,x_test,y_train,y_test = train_test_split(X,y,stratify=y)
print(f'train data shape: {x_train.shape}')
print(f'test data shape: {x_test.shape}')


dd = pd.Series(y_train).value_counts()
sns.barplot(x=np.array(['negative','positive']),y=dd.values)
plt.show()

Sortie :

>>> train data shape: (37500,)

>>> test data shape: (12500,)

image14.png

3. Prétraitement du texte

Le prétraitement et la tokenisation sont une étape essentielle. Nous commençons par nettoyer le texte en supprimant la ponctuation, les espaces superflus et les chiffres.

Nous transformons ensuite les phrases en mots individuels, retirons les mots très fréquents (stop words) et conservons les 1000 mots les plus utilisés du jeu de données. Chaque mot reçoit un identifiant unique, constituant un dictionnaire pour l’encodage one-hot.

Le code convertit ainsi les phrases originales en séquences de ces identifiants : il traduit le langage humain dans un format compréhensible par un modèle de machine learning.

def preprocess_string(s):
    # Remove all non-word characters (everything except numbers and letters)
    s = re.sub(r"[^\w\s]", '', s)
    # Replace all runs of whitespaces with no space
    s = re.sub(r"\s+", '', s)
    # replace digits with no space
    s = re.sub(r"\d", '', s)
    return s


def tokenize(x_train,y_train,x_val,y_val):
    word_list = []


    stop_words = set(stopwords.words('english'))
    for sent in x_train:
        for word in sent.lower().split():
            word = preprocess_string(word)
            if word not in stop_words and word != '':
                word_list.append(word)
 
    corpus = Counter(word_list)
    # sorting on the basis of most common words
    corpus_ = sorted(corpus,key=corpus.get,reverse=True)[:1000]
    # creating a dict
    onehot_dict = {w:i+1 for i,w in enumerate(corpus_)}
   
    # tokenize
    final_list_train,final_list_test = [],[]
    for sent in x_train:
            final_list_train.append([onehot_dict[preprocess_string(word)] for word in sent.lower().split()
                                     if preprocess_string(word) in onehot_dict.keys()])
    for sent in x_val:
            final_list_test.append([onehot_dict[preprocess_string(word)] for word in sent.lower().split()
                                    if preprocess_string(word) in onehot_dict.keys()])
           
    encoded_train = [1 if label =='positive' else 0 for label in y_train]  
    encoded_test = [1 if label =='positive' else 0 for label in y_val]
    return np.array(final_list_train), np.array(encoded_train),np.array(final_list_test), np.array(encoded_test),onehot_dict


x_train,y_train,x_test,y_test,vocab = tokenize(x_train,y_train,x_test,y_test)

Analysons la longueur des séquences de tokens dans x_train.

rev_len = [len(i) for i in x_train]
pd.Series(rev_len).hist()

image1.png

4. Préparation des données pour le modèle

Les avis ayant des longueurs variables, il faut les standardiser. Comme la majorité contient moins de 500 tokens, nous fixerons la longueur à 500 pour tous les avis.

def padding_(sentences, seq_len):
    features = np.zeros((len(sentences), seq_len),dtype=int)
    for ii, review in enumerate(sentences):
        if len(review) != 0:
            features[ii, -len(review):] = np.array(review)[:seq_len]
    return features


x_train_pad = padding_(x_train,500)
x_test_pad = padding_(x_test,500)

Ensuite, nous utilisons la classe DataLoader pour créer le jeu de données final destiné à l’entraînement.

# create Tensor datasets
train_data = TensorDataset(torch.from_numpy(x_train_pad), torch.from_numpy(y_train))
valid_data = TensorDataset(torch.from_numpy(x_test_pad), torch.from_numpy(y_test))


# dataloaders
batch_size = 50


# make sure to SHUFFLE your data
train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size)
valid_loader = DataLoader(valid_data, shuffle=True, batch_size=batch_size)


# obtain one batch of training data
dataiter = iter(train_loader)
sample_x, sample_y = next(dataiter)


print('Sample input size: ', sample_x.size()) # batch_size, seq_length
print('Sample input: \n', sample_x)
print('Sample output: \n', sample_y)

Sortie :

image3.png

5. Définir le modèle LSTM

Cette partie définit un modèle d’analyse de sentiment basé sur une architecture de réseau de neurones récurrent, plus précisément un LSTM (Long Short-Term Memory) comme indiqué plus haut. La classe SentimentRNN est un modèle PyTorch qui commence par une couche d’embedding transformant les indices de mots en représentations denses capturant leur sens sémantique. Elle est suivie d’une couche LSTM qui traite la séquence d’embeddings.

L’état caché du LSTM passe par une couche de dropout (pour régulariser et éviter le surapprentissage) puis une couche complètement connectée qui projette les sorties du LSTM vers la prédiction finale. Une activation sigmoïde convertit ensuite la sortie brute en probabilités. La méthode forward définit la propagation avant, et init_hidden initialise les états cachés du LSTM à zéro.

class SentimentRNN(nn.Module):
    def __init__(self,no_layers,vocab_size,hidden_dim,embedding_dim,drop_prob=0.5):
        super(SentimentRNN,self).__init__()
 
        self.output_dim = output_dim
        self.hidden_dim = hidden_dim
 
        self.no_layers = no_layers
        self.vocab_size = vocab_size
   
        # embedding and LSTM layers
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
       
        #lstm
        self.lstm = nn.LSTM(input_size=embedding_dim,hidden_size=self.hidden_dim,
                           num_layers=no_layers, batch_first=True)
       
        # dropout layer
        self.dropout = nn.Dropout(0.3)
   
        # linear and sigmoid layer
        self.fc = nn.Linear(self.hidden_dim, output_dim)
        self.sig = nn.Sigmoid()
       
    def forward(self,x,hidden):
        batch_size = x.size(0)
        # embeddings and lstm_out
        embeds = self.embedding(x)  # shape: B x S x Feature   since batch = True
        #print(embeds.shape)  #[50, 500, 1000]
        lstm_out, hidden = self.lstm(embeds, hidden)
       
        lstm_out = lstm_out.contiguous().view(-1, self.hidden_dim)
       
        # dropout and fully connected layer
        out = self.dropout(lstm_out)
        out = self.fc(out)
       
        # sigmoid function
        sig_out = self.sig(out)
       
        # reshape to be batch_size first
        sig_out = sig_out.view(batch_size, -1)


        sig_out = sig_out[:, -1] # get last batch of labels
       
        # return last sigmoid output and hidden state
        return sig_out, hidden
       
    def init_hidden(self, batch_size):
        ''' Initializes hidden state '''
        # Create two new tensors with sizes n_layers x batch_size x hidden_dim,
        # initialized to zero, for hidden state and cell state of LSTM
        h0 = torch.zeros((self.no_layers,batch_size,self.hidden_dim)).to(device)
        c0 = torch.zeros((self.no_layers,batch_size,self.hidden_dim)).to(device)
        hidden = (h0,c0)
        return hidden 

Nous allons maintenant initialiser la classe SentimentRNN avec les paramètres requis.

no_layers = 2
vocab_size = len(vocab) + 1 #extra 1 for padding
embedding_dim = 64
output_dim = 1
hidden_dim = 256


model = SentimentRNN(no_layers,vocab_size,hidden_dim,embedding_dim,drop_prob=0.5)


#moving to gpu
model.to(device)
print(model)

image10.png

La dernière étape avant l’entraînement consiste à définir la fonction de perte et la méthode d’optimisation. Nous définissons ici la perte, l’optimiseur et une fonction utilitaire pour calculer la précision de notre modèle d’analyse de sentiment.

La fonction de perte utilisée est la Binary Cross-Entropy (nn.BCELoss), fréquente pour les classifications binaires. L’optimiseur est Adam (torch.optim.Adam), très répandu pour son efficacité et sa faible empreinte mémoire. Le taux d’apprentissage est fixé à 0,001.

La fonction acc calcule la précision : elle arrondit les probabilités prédites (0 ou 1), les compare aux étiquettes réelles et renvoie le pourcentage de prédictions correctes.

# loss and optimization functions


lr=0.001


criterion = nn.BCELoss()


optimizer = torch.optim.Adam(model.parameters(), lr=lr)


# function to predict accuracy
def acc(pred,label):
    pred = torch.round(pred.squeeze())
    return torch.sum(pred == label.squeeze()).item()

6. Lancer l’entraînement

Ici, nous entraînons et validons le modèle. Chaque époque comporte une phase d’apprentissage et une phase de validation. Pendant l’apprentissage, le modèle ajuste ses paramètres pour minimiser la perte.

En validation, on évalue ses performances sur un ensemble distinct pour vérifier la capacité de généralisation et éviter le simple mémorisation du jeu d’entraînement.

La boucle d’entraînement initialise les états cachés du LSTM et place le modèle en mode apprentissage. Pour chaque lot, on compare les prédictions aux étiquettes réelles, on calcule la perte et on rétropropage pour mettre à jour les paramètres.

Les gradients sont écrêtés à une valeur maximale afin d’éviter les explosions de gradient, fréquentes avec les RNN/LSTM.

En validation, le modèle est passé en mode évaluation et ses performances sont mesurées sans mise à jour des paramètres. Pour les deux phases, la perte et la précision par époque sont suivies.

Si la perte de validation s’améliore, les paramètres courants sont enregistrés afin de conserver le meilleur modèle.

Enfin, à chaque époque, la perte et la précision moyennes sont affichées pour suivre la progression de l’apprentissage.

clip = 5
epochs = 5
valid_loss_min = np.Inf
# train for some number of epochs
epoch_tr_loss,epoch_vl_loss = [],[]
epoch_tr_acc,epoch_vl_acc = [],[]


for epoch in range(epochs):
    train_losses = []
    train_acc = 0.0
    model.train()
    # initialize hidden state
    h = model.init_hidden(batch_size)
    for inputs, labels in train_loader:
       
        inputs, labels = inputs.to(device), labels.to(device)  
        # Creating new variables for the hidden state, otherwise
        # we'd backprop through the entire training history
        h = tuple([each.data for each in h])
       
        model.zero_grad()
        output,h = model(inputs,h)
       
        # calculate the loss and perform backprop
        loss = criterion(output.squeeze(), labels.float())
        loss.backward()
        train_losses.append(loss.item())
        # calculating accuracy
        accuracy = acc(output,labels)
        train_acc += accuracy
        #`clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
        nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
       
    val_h = model.init_hidden(batch_size)
    val_losses = []
    val_acc = 0.0
    model.eval()
    for inputs, labels in valid_loader:
            val_h = tuple([each.data for each in val_h])


            inputs, labels = inputs.to(device), labels.to(device)


            output, val_h = model(inputs, val_h)
            val_loss = criterion(output.squeeze(), labels.float())


            val_losses.append(val_loss.item())
           
            accuracy = acc(output,labels)
            val_acc += accuracy
           
    epoch_train_loss = np.mean(train_losses)
    epoch_val_loss = np.mean(val_losses)
    epoch_train_acc = train_acc/len(train_loader.dataset)
    epoch_val_acc = val_acc/len(valid_loader.dataset)
    epoch_tr_loss.append(epoch_train_loss)
    epoch_vl_loss.append(epoch_val_loss)
    epoch_tr_acc.append(epoch_train_acc)
    epoch_vl_acc.append(epoch_val_acc)
    print(f'Epoch {epoch+1}')
    print(f'train_loss : {epoch_train_loss} val_loss : {epoch_val_loss}')
    print(f'train_accuracy : {epoch_train_acc*100} val_accuracy : {epoch_val_acc*100}')
    if epoch_val_loss <= valid_loss_min:
        torch.save(model.state_dict(), 'state_dict.pt')
        print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(valid_loss_min,epoch_val_loss))
        valid_loss_min = epoch_val_loss
    print(25*'==')

Sortie :

image9.png

7. Évaluation du modèle

Cette partie génère deux graphiques récapitulant l’évolution de la précision et de la perte en apprentissage et en validation au fil des époques. Le premier sous-graphe trace la précision, utile pour juger l’apprentissage et la généralisation.

Le second sous-graphe trace la perte d’entraînement et de validation, afin de déceler surapprentissage, sous-apprentissage ou bon ajustement.

fig = plt.figure(figsize = (20, 6))
plt.subplot(1, 2, 1)
plt.plot(epoch_tr_acc, label='Train Acc')
plt.plot(epoch_vl_acc, label='Validation Acc')
plt.title("Accuracy")
plt.legend()
plt.grid()
   
plt.subplot(1, 2, 2)
plt.plot(epoch_tr_loss, label='Train loss')
plt.plot(epoch_vl_loss, label='Validation loss')
plt.title("Loss")
plt.legend()
plt.grid()


plt.show()

Sortie :

image6.png

8. Inférence / prédiction

Nous définissons ici une fonction predict_text pour prédire le sentiment d’un texte donné, avec un exemple d’utilisation. La fonction prend une chaîne en entrée, la transforme en séquence d’indices de mots selon le vocabulaire, l’apprête pour le modèle via du padding et un remodelage, initialise les états cachés du LSTM, envoie l’entrée dans le modèle et renvoie la probabilité prédite.

def predict_text(text):
        word_seq = np.array([vocab[preprocess_string(word)] for word in text.split()
                         if preprocess_string(word) in vocab.keys()])
        word_seq = np.expand_dims(word_seq,axis=0)
        pad =  torch.from_numpy(padding_(word_seq,500))
        inputs = pad.to(device)
        batch_size = 1
        h = model.init_hidden(batch_size)
        h = tuple([each.data for each in h])
        output, h = model(inputs, h)
        return(output.item())


index = 30
print(df['review'][index])
print('='*70)
print(f'Actual sentiment is  : {df["sentiment"][index]}')
print('='*70)
pro = predict_text(df['review'][index])
status = "positive" if pro > 0.5 else "negative"
pro = (1 - pro) if status == "negative" else pro
print(f'Predicted sentiment is {status} with a probability of {pro}')

Sortie :

image13.png

Cet notebook complet a été développé avec DataLab et est accessible dans ce workbook. Notez que l’exécution du code peut prendre du temps sur CPU, alors que l’utilisation d’un GPU peut réduire significativement le temps d’entraînement.

Prochaines étapes / améliorer le modèle

Améliorer un modèle de NLP implique souvent plusieurs stratégies adaptées au contexte et aux contraintes de la tâche. Le réglage d’hyperparamètres est une approche classique : taux d’apprentissage, taille de lot, nombre de couches du réseau, etc.

Ces hyperparamètres influent fortement sur les performances et s’optimisent généralement via grid search ou random search.

Le transfert d’apprentissage, en particulier avec des modèles comme BERT ou GPT, a montré un fort potentiel pour améliorer les tâches de NLP. Pré-entraînés sur de larges corpus, ces modèles sont ensuite affinés sur une tâche cible et bénéficient ainsi d’une compréhension générale du langage acquise en amont. Cette approche a régulièrement fixé l’état de l’art, y compris en analyse de sentiment.

Cas d’usage réels du NLP avec PyTorch

Les modèles de traitement du langage naturel, notamment implémentés avec PyTorch, sont largement adoptés et transforment de nombreux aspects de nos vies numériques.

Les chatbots sont devenus centraux dans le service client : grâce au NLP, ils comprennent les requêtes et y répondent avec des interactions naturelles.

Dans les systèmes de recommandation, les modèles NLP analysent avis et commentaires pour cerner les préférences et personnaliser les suggestions.

L’analyse de sentiment s’appuie également fortement sur le NLP. Ces outils examinent réseaux sociaux, avis clients ou tout texte pour en inférer le sentiment. Les entreprises exploitent ces insights pour leurs études de marché ou pour jauger l’opinion publique sur leurs produits et services, et ainsi prendre des décisions basées sur les données.

Découvrez d’autres cas d’usage concrets de Google BERT dans ce tutoriel de traitement du langage naturel.

Conclusion

PyTorch offre une plateforme puissante et flexible pour construire des modèles de NLP. Dans ce tutoriel, nous avons détaillé la création d’un modèle d’analyse de sentiment avec une architecture LSTM, du prétraitement à l’entraînement, la validation et la prédiction sur des données inconnues.

Ce n’est qu’un aperçu de ce qu’il est possible de réaliser avec le NLP et PyTorch. Les applications sont nombreuses : chatbots, systèmes de recommandation, outils d’analyse de sentiment, et bien plus.

L’évolution rapide du domaine, notamment avec les modèles de transfert d’apprentissage comme BERT et GPT, ouvre des perspectives encore plus stimulantes. Maîtriser le NLP avec PyTorch est exigeant mais très enrichissant, car cela ouvre une nouvelle façon de comprendre et d’interagir avec le monde.

Pour approfondir PyTorch, suivez notre cours Deep Learning with PyTorch. Vous commencerez par une introduction à la bibliothèque et à ses usages pour les réseaux de neurones et le deep learning, puis vous aborderez les réseaux de neurones artificiels et leur entraînement sur des données réelles.

Moez Ali

Auteur

Moez Ali

Scientifique de données, fondateur et créateur de PyCaret

Sujets

Contenus associés

Tutoriel

Fonctions lambda Python : Guide pour débutants