[PYTHON] J'ai essayé d'implémenter Attention Seq2Seq avec PyTorch

introduction

Suite à l'implémentation de Seq2Seq dans Dernière fois, cette fois j'ai essayé d'implémenter Attention Seq2Seq avec Attention ajoutée à Seq2Seq avec PyTorch.

Même les débutants comme moi ne peuvent pas trouver beaucoup de code source qui implémente Attention dans PyTorch, et il y a aussi PyTorch Attention Tutorial. Il y en a, mais il semble que je n'ai pas appris le mini-batch (?), Et j'ai voulu implémenter une attention simple (?) Avec le sentiment qu'il semble être customisé pour cette tâche. J'ai essayé de mettre en œuvre Attention moi-même. Nous espérons pouvoir vous fournir des informations utiles pour ceux qui rencontrent des difficultés pour mettre en œuvre Attention.

Le mécanisme de l'attention est toujours [Deep Learning from scratch ❷ - Natural language processing](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%] E3% 82% 89% E4% BD% 9C% E3% 82% 8B Apprentissage en profondeur-% E2% 80% 95% E8% 87% AA% E7% 84% B6% E8% A8% 80% E8% AA% 9E % E5% 87% A6% E7% 90% 86% E7% B7% A8-% E6% 96% 8E% E8% 97% A4-% E5% BA% B7% E6% AF% 85 / dp / 4873118360 / réf = sr_1_2? __ mk_ja_JP =% E3% 82% AB% E3% 82% BF% E3% 82% AB% E3% 83% 8A & mots-clés =% E3% 82% BC% E3% 83% AD% E3% 81% 8B% E3 % 82% 89% E4% BD% 9C% E3% 82% 8B & qid = 1568304570 & s = gateway & sr = 8-2) était extrêmement facile à comprendre.

L'exemple d'implémentation que je vais vous présenter est juste une implémentation scratch de Zero Work 2 (devrait être), donc si cet article est difficile à comprendre, je vous recommande fortement de lire Zero Work 2. ..

Supplément

Je pense qu'il existe différents types d'attention, comme l'attention douce et l'attention dure, mais l'attention ici est Deep Learning from scratch ❷-Natural language processing. 82% BC% E3% 83% AD% E3% 81% 8B% E3% 82% 89% E4% BD% 9C% E3% 82% 8BApprentissage en profondeur-% E2% 80% 95% E8% 87% AA% E7 % 84% B6% E8% A8% 80% E8% AA% 9E% E5% 87% A6% E7% 90% 86% E7% B7% A8-% E6% 96% 8E% E8% 97% A4-% E5 % BA% B7% E6% AF% 85 / dp / 4873118360 / ref = sr_1_2? __Mk_ja_JP =% E3% 82% AB% E3% 82% BF% E3% 82% AB% E3% 83% 8A & mots-clés =% E3% 82 % BC% E3% 83% AD% E3% 81% 8B% E3% 82% 89% E4% BD% 9C% E3% 82% 8B & qid = 1568304570 & s = passerelle & sr = 8-2) (souple) Je ferai référence à Attention.

Mécanisme d'attention

Les défis de Seq2Seq

Seq2Seq a le problème que les caractéristiques des séries longues ne peuvent pas être capturées car Encoder le convertit en un vecteur de longueur fixe indépendamment de la longueur de la série d'entrée. Attention fournit un mécanisme pour prendre en compte la longueur de la séquence d'entrée du côté du codeur afin de résoudre ce problème.

Explication super grossière

Si vous expliquez Attention très grossièrement

  1. ** Passez toutes les valeurs de chaque couche cachée du côté de l'encodeur à chaque couche du côté du décodeur **
  2. ** Dans chaque couche du côté du décodeur, sélectionnez le vecteur le plus remarquable parmi les vecteurs de chaque couche cachée passée du côté de l'encodeur et ajoutez-le aux fonctionnalités **

Je vais faire l'opération. En 1., le nombre de vecteurs de calque cachés du côté de l'encodeur dépend de la longueur de la série qui est l'entrée du côté de l'encodeur, ainsi la forme prend en compte la longueur de la série. En 2., l'opération de sélection ne peut pas être différenciée, mais l'opération de sélection où prêter attention à chaque élément est pondérée de manière probabiliste avec $ softmax $.

Expliquez un peu plus en détail le flux du traitement de l'attention à l'aide de chiffres

Par souci de simplicité, la figure ci-dessous traite de deux cas où le côté codeur a trois séquences d'entrée w1, w2 et w3, et le côté décodeur a w'1, w'2.

① Lorsque la valeur de chaque couche cachée du côté de l'encodeur est $ h_1 $, $ h_2 $, $ \ cdots $, $ h_n $, $ hs = [h_1, h_2, \ cdots, h_n] $ est chaque couche du côté du décodeur. Passer au.

(2) Calculez le produit interne du vecteur de chaque couche cachée côté décodeur (ici, $ d_i $) et de chaque vecteur de $ hs $ $ h_1, h_2, \ cdots $. Cela signifie que nous calculons la similitude de chaque vecteur du côté du décodeur et de chaque vecteur de $ hs $. (Le produit interne est exprimé par $ (\ cdot, \ cdot) $.)

③ Convertissez le produit interne calculé en ② en une expression probabiliste avec $ softmax $ (cela s'appelle aussi poids d'attention)

④ Chaque élément de $ hs $ est pondéré par le poids d'attention et additionné pour former un vecteur (c'est aussi appelé un vecteur de contexte).

⑤ Combinez le vecteur de contexte et $ d_i $ en un seul vecteur

la mise en oeuvre

――Ajoutez les processus 1 à 5 expliqués ci-dessus du côté du décodeur et vous avez terminé. Il traite le problème de conversion du format de date ainsi que Zero Saku 2. (Parce qu'il est facile de confirmer la certitude lorsque le poids d'attention est visualisé)

Problème de réglage

Résolvons la tâche de conversion de divers styles d'écriture de date tels que les suivants au format AAAA-MM-JJ avec Attention seq 2seq.

Avant la conversion Après la conversion
Nobenver, 30, 1995 1995-11-30
Monday, July 9, 2001 2001-07-09
1/23/01 2001-01-23
WEDNESDAY, AUGUST 1, 2001 2001-08-01
sep 7, 1981 1981-09-07

Préparation des données

Nous empruntons les données du référentiel Github de Zero Work 2. https://github.com/oreilly-japan/deep-learning-from-scratch-2/tree/master/dataset

Placez ce fichier sur Google Drive et séparez-le avant et après la conversion comme suit.

from sklearn.model_selection import train_test_split
import random
from sklearn.utils import shuffle

#Montez Google Drive à l'avance et datez à l'emplacement suivant.Stocker txt
file_path = "drive/My Drive/Colab Notebooks/date.txt"

input_date = [] #Date des données avant la conversion
output_date = [] #Données de date après conversion

# date.Lire le txt ligne par ligne, diviser avant et après la conversion et séparer par entrée et sortie
with open(file_path, "r") as f:
  date_list = f.readlines()
  for date in date_list:
    date = date[:-1]
    input_date.append(date.split("_")[0])
    output_date.append("_" + date.split("_")[1])

#Obtenir la longueur des séries d'entrée et de sortie
#Comme ils ont tous la même longueur, nous prenons len au 0ème élément
input_len = len(input_date[0]) # 29
output_len = len(output_date[0]) # 10

# date.Attribuez un identifiant à chaque caractère qui apparaît dans txt
char2id = {}
for input_chars, output_chars in zip(input_date, output_date):
  for c in input_chars:
    if not c in char2id:
      char2id[c] = len(char2id)
  for c in output_chars:
    if not c in char2id:
      char2id[c] = len(char2id)

input_data = [] #Données de date de pré-conversion identifiées
output_data = [] #Données de date converties identifiées
for input_chars, output_chars in zip(input_date, output_date):
  input_data.append([char2id[c] for c in input_chars])
  output_data.append([char2id[c] for c in output_chars])

# 7:Divisez en train et testez en 3
train_x, test_x, train_y, test_y = train_test_split(input_data, output_data, train_size= 0.7)

#Définir une fonction pour regrouper les données
def train2batch(input_data, output_data, batch_size=100):
    input_batch = []
    output_batch = []
    input_shuffle, output_shuffle = shuffle(input_data, output_data)
    for i in range(0, len(input_data), batch_size):
      input_batch.append(input_shuffle[i:i+batch_size])
      output_batch.append(output_shuffle[i:i+batch_size])
    return input_batch, output_batch

Encoder

import torch
import torch.nn as nn
import torch.optim as optim

#Divers paramètres, etc.
embedding_dim = 200
hidden_dim = 128
BATCH_NUM = 100
vocab_size = len(char2id)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#Classe d'encodeur
class Encoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(Encoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=char2id[" "])
        self.gru = nn.GRU(embedding_dim, hidden_dim, batch_first=True)
    
    def forward(self, sequence):
        embedding = self.word_embeddings(sequence)
        #hs est le vecteur de la couche cachée de GRU de chaque série
        #Élément d'attention
        hs, h = self.gru(embedding)
        return hs, h

Decoder ――Similaire au côté encodeur, LSTM est changé en GRU par rapport à la fois précédente. ――Vous pouvez organiser votre esprit en écrivant sur un morceau de papier ce que signifie l'axe du tenseur de chaque couche. ――J'ai également répertorié la taille de chaque tenseur dans la couche Attention pour vous aider à la comprendre.

#Attention classe de décodeur
class AttentionDecoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, batch_size):
        super(AttentionDecoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.batch_size = batch_size
        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=char2id[" "])
        self.gru = nn.GRU(embedding_dim, hidden_dim, batch_first=True)
        # hidden_dim*2 est torche le vecteur de contexte calculé par la couche cachée et la couche Attention de chaque série de GRU..Parce que la longueur est doublée en se connectant au chat
        self.hidden2linear = nn.Linear(hidden_dim * 2, vocab_size)
        #Je veux convertir la direction de la colonne avec probabilité, donc dim=1
        self.softmax = nn.Softmax(dim=1)
        
    def forward(self, sequence, hs, h):
        embedding = self.word_embeddings(sequence)
        output, state = self.gru(embedding, h)

       #Couche d'attention
       # hs.size() = ([100, 29, 128])
       # output.size() = ([100, 10, 128])

       #Sortie côté codeur en utilisant bmm(hs)Et la sortie côté décodeur(output)Afin de calculer la matrice pour chaque lot, fixez le lot comme sortie côté décodeur et prenez une matrice transposée.
        t_output = torch.transpose(output, 1, 2) # t_output.size() = ([100, 128, 10])

        #Calcul matriciel avec bmm en considérant le lot
        s = torch.bmm(hs, t_output) # s.size() = ([100, 29, 10])

        #Direction de la colonne(dim=1)Prenez softmax avec et convertissez-vous en expression probabiliste
        #Cette valeur sera utilisée pour la visualisation de Attention plus tard, donc renvoyez-la avec retour.
        attention_weight = self.softmax(s) # attention_weight.size() = ([100, 29, 10])

        #Préparer un conteneur pour organiser le vecteur de contexte
        c = torch.zeros(self.batch_size, 1, self.hidden_dim, device=device) # c.size() = ([100, 1, 128])

        #Je ne savais pas comment calculer le vecteur de contexte pour la couche GRU de chaque décodeur à la fois.
        #Extrayez le poids d'attention dans chaque couche (la couche GRU du côté du décodeur a 10 caractères car la chaîne de caractères générée est de 10 caractères) et créez un vecteur de contexte dans la boucle for.
        #Étant donné que la direction du lot peut être calculée collectivement, le lot reste tel quel
        for i in range(attention_weight.size()[2]): #10 boucles

          # attention_weight[:,:,i].size() = ([100, 29])
          #Prenez le poids d'attention pour la i-ème couche GRU, mais desserrez pour aligner la taille du tenseur avec hs
          unsq_weight = attention_weight[:,:,i].unsqueeze(2) # unsq_weight.size() = ([100, 29, 1])

          #Pondérer chaque vecteur de h avec le poids d'attention
          weighted_hs = hs * unsq_weight # weighted_hs.size() = ([100, 29, 128])

          #Créer un vecteur de contexte en ajoutant tous les vecteurs de chaque hs pondérés par le poids d'attention
          weight_sum = torch.sum(weighted_hs, axis=1).unsqueeze(1) # weight_sum.size() = ([100, 1, 128])

          c = torch.cat([c, weight_sum], dim=1) # c.size() = ([100, i, 128])

        #Puisque l'élément zéro préparé comme une boîte reste, coupez-le et supprimez-le
        c = c[:,1:,:]
        
        output = torch.cat([output, c], dim=2) # output.size() = ([100, 10, 256])
        output = self.hidden2linear(output)
        return output, state, attention_weight

Déclaration de modèle, fonction de perte, optimisation


encoder = Encoder(vocab_size, embedding_dim, hidden_dim).to(device)
attn_decoder = AttentionDecoder(vocab_size, embedding_dim, hidden_dim, BATCH_NUM).to(device)

#Fonction de perte
criterion = nn.CrossEntropyLoss()

#optimisation
encoder_optimizer = optim.Adam(encoder.parameters(), lr=0.001)
attn_decoder_optimizer = optim.Adam(attn_decoder.parameters(), lr=0.001)

Apprentissage

BATCH_NUM=100
EPOCH_NUM = 100

all_losses = []
print("training ...")
for epoch in range(1, EPOCH_NUM+1):
    epoch_loss = 0
    #Divisez les données en mini-lots
    input_batch, output_batch = train2batch(train_x, train_y, batch_size=BATCH_NUM)
    for i in range(len(input_batch)):
        
        #Initialisation du gradient
        encoder_optimizer.zero_grad()
        attn_decoder_optimizer.zero_grad()
        
        #Convertir les données en tenseur
        input_tensor = torch.tensor(input_batch[i], device=device)
        output_tensor = torch.tensor(output_batch[i], device=device)
        
        #Propagation vers l'avant du codeur
        hs, h = encoder(input_tensor)

        #Attention Entrée décodeur
        source = output_tensor[:, :-1]
        
        #Attention Décodeur données de réponse correcte
        target = output_tensor[:, 1:]

        loss = 0
        decoder_output, _, attention_weight= attn_decoder(source, hs, h)
        for j in range(decoder_output.size()[1]):
            loss += criterion(decoder_output[:, j, :], target[:, j])

        epoch_loss += loss.item()
        
        #Erreur de propagation de retour
        loss.backward()

        #Mise à jour des paramètres
        encoder_optimizer.step()
        attn_decoder_optimizer.step()
    
    #Montrer la perte
    print("Epoch %d: %.2f" % (epoch, epoch_loss))
    all_losses.append(epoch_loss)
    if epoch_loss < 0.1: break
print("Done")
# training ...
# Epoch 1: 1500.33
# Epoch 2: 77.53
# Epoch 3: 12.98
# Epoch 4: 3.40
# Epoch 5: 1.78
# Epoch 6: 1.13
# Epoch 7: 0.78
# Epoch 8: 0.56
# Epoch 9: 0.42
# Epoch 10: 0.32
# Epoch 11: 0.25
# Epoch 12: 0.20
# Epoch 13: 0.16
# Epoch 14: 0.13
# Epoch 15: 0.11
# Epoch 16: 0.09
# Done

Visualisation des pertes

import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(all_losses)

Prévoir

――Il est prédit par presque la même méthode que la prédiction au moment de la précédente Seq2Seq.

import pandas as pd

#Renvoie l'index avec le plus grand élément du tenseur de sortie Decoder. Cela signifie le caractère généré
def get_max_index(decoder_output):
  results = []
  for h in decoder_output:
    results.append(torch.argmax(h))
  return torch.tensor(results, device=device).view(BATCH_NUM, 1)
    
#Données d'évaluation
test_input_batch, test_output_batch = train2batch(test_x, test_y)
input_tensor = torch.tensor(test_input_batch, device=device)

predicts = []
for i in range(len(test_input_batch)):
  with torch.no_grad():
    hs, encoder_state = encoder(input_tensor[i])
    
    #Le décodeur indique d'abord le début de la génération de la chaîne de caractères"_"Parce que c'est une entrée"_"Créer un tenseur pour la taille du lot
    start_char_batch = [[char2id["_"]] for _ in range(BATCH_NUM)]
    decoder_input_tensor = torch.tensor(start_char_batch, device=device)

    decoder_hidden = encoder_state
    batch_tmp = torch.zeros(100,1, dtype=torch.long, device=device)
    for _ in range(output_len - 1):
      decoder_output, decoder_hidden, _ = attn_decoder(decoder_input_tensor, hs, decoder_hidden)
      #Lors de l'acquisition du caractère prédit, il devient l'entrée du décodeur suivant tel quel
      decoder_input_tensor = get_max_index(decoder_output.squeeze())
      batch_tmp = torch.cat([batch_tmp, decoder_input_tensor], dim=1)
    predicts.append(batch_tmp[:,1:])


#Lors de l'affichage du résultat de la prédiction, si l'ID reste tel quel, la lisibilité est médiocre, définissez donc un dictionnaire à convertir d'ID en chaîne de caractères pour restaurer la chaîne de caractères d'origine.
id2char = {}
for k, v in char2id.items():
  id2char[v] = k

row = []
for i in range(len(test_input_batch)):
  batch_input = test_input_batch[i]
  batch_output = test_output_batch[i]
  batch_predict = predicts[i]
  for inp, output, predict in zip(batch_input, batch_output, batch_predict):
    x = [id2char[idx] for idx in inp]
    y = [id2char[idx] for idx in output[1:]]
    p = [id2char[idx.item()] for idx in predict]
    
    x_str = "".join(x)
    y_str = "".join(y)
    p_str = "".join(p)
    
    judge = "O" if y_str == p_str else "X"
    row.append([x_str, y_str, p_str, judge])
predict_df = pd.DataFrame(row, columns=["input", "answer", "predict", "judge"])
predict_df.head()

Taux de réponse correct

Il est arrivé que ce n'était pas 100% cette fois, mais je pense que ce sera environ 100% de taux de réponse correcte.

print(len(predict_df.query('judge == "O"')) / len(predict_df))
# 0.9999333333333333

predict_df.query('judge == "X"').head(10)

――J'ai fait une erreur dans le cas suivant ――Lorsque vous faites une erreur dans cette tâche, il semble qu'il existe de nombreux formats de date séparés par des barres obliques, comme indiqué ci-dessous.

visualisation du poids d'attention

―― Visualisons le poids de l'attention, qui est l'un des vrais plaisirs de l'attention. ―― Vous pouvez vérifier la certitude d'apprendre en regardant le poids d'attention.

import seaborn as sns
import pandas as pd

input_batch, output_batch = train2batch(test_x, test_y, batch_size=BATCH_NUM)
input_minibatch, output_minibatch = input_batch[0], output_batch[0]

with torch.no_grad():
  #Convertir les données en tenseur
  input_tensor = torch.tensor(input_minibatch, device=device)
  output_tensor = torch.tensor(output_minibatch, device=device)
  hs, h = encoder(input_tensor)
  source = output_tensor[:, :-1]
  decoder_output, _, attention_weight= attn_decoder(source, hs, h)


for i in range(3):
  with torch.no_grad():
    df = pd.DataFrame(data=torch.transpose(attention_weight[i], 0, 1).cpu().numpy(), 
                      columns=[id2char[idx.item()] for idx in input_tensor[i]], 
                      index=[id2char[idx.item()] for idx in output_tensor[i][1:]])
    plt.figure(figsize=(12, 8)) 
    sns.heatmap(df, xticklabels = 1, yticklabels = 1, square=True, linewidths=.3,cbar_kws = dict(use_gridspec=False,location="top"))

Présentation de quelques visualisations

C'est un peu difficile à voir, mais les lettres «mardi 27 mars 2012» en bas de la figure ci-dessus sont les chaînes avant la conversion (entrée de l'encodeur), et «2012-03-27» disposé verticalement à gauche est généré. C'est un personnage. Voici comment lire la carte thermique, mais lorsque vous regardez les caractères générés de Decoder un par un, cela signifie que la couleur de la boîte de gauche est le caractère généré avec l'attention la plus brillante. Je pense que ce sera. (Veuillez préciser si c'est différent ...) (Bien sûr, si vous ajoutez toutes les valeurs dans la case de gauche, ce sera 1.)

Dans l'exemple ci-dessus, vous pouvez voir ce qui suit.

――On constate que vous faites attention à la partie année si vous générez AAAA dans son ensemble, et à la partie mois si vous générez MM. --Cette tâche est convertie en AAAA-MM-JJ, c'est-à-dire que le jour n'est pas converti, je ne fais donc pas attention aux caractères générés dans "Mardi"

D'ailleurs, il est attiré comme ça ↓

en conclusion

――Il semble qu'il existe différents modèles dans Attention comme décrit dans Zero work 2. ――Nous traiterons ensuite (?) De l'attention personnelle, qui est plus polyvalente que l'attention!

fin

Recommended Posts

J'ai essayé d'implémenter Attention Seq2Seq avec PyTorch
J'ai essayé d'implémenter VQE avec Blueqat
J'ai créé Word2Vec avec Pytorch
J'ai essayé d'implémenter DeepPose avec PyTorch
Seq2Seq (2) ~ Attention Model edition ~ avec chainer
J'ai essayé d'implémenter la classification des phrases par Self Attention avec PyTorch
[Introduction à Pytorch] J'ai joué avec sinGAN ♬
J'ai essayé d'implémenter DeepPose avec PyTorch PartⅡ
J'ai essayé d'implémenter CVAE avec PyTorch
J'ai implémenté CycleGAN (1)
Validation croisée avec PyTorch
À partir de PyTorch
Seq2Seq (1) avec chainer
J'ai implémenté ResNet!
J'ai essayé d'implémenter la lecture de Dataset avec PyTorch
J'ai réécrit le code MNIST de Chainer avec PyTorch + Ignite
Utilisez RTX 3090 avec PyTorch
J'ai essayé de déplacer Faster R-CNN rapidement avec pytorch
J'ai essayé d'implémenter et d'apprendre DCGAN avec PyTorch
J'ai joué avec wordcloud!
Qiskit: j'ai implémenté VQE
[Introduction à Pytorch] J'ai essayé de catégoriser Cifar10 avec VGG16 ♬
Installer la diffusion de la torche avec PyTorch 1.7
J'ai essayé de mettre en œuvre le co-filtrage (recommandation) avec redis et python
J'ai essayé d'implémenter SSD avec PyTorch maintenant (Dataset)
J'ai eu une erreur lors de l'utilisation de Tensorboard avec Pytorch
J'ai essayé d'implémenter l'algorithme FloodFill avec TRON BATTLE de CodinGame
J'ai essayé de classer MNIST par GNN (avec PyTorch géométrique)
J'ai essayé d'implémenter SSD avec PyTorch maintenant (édition du modèle)
J'ai essayé fp-growth avec python
J'ai essayé de gratter avec Python
J'ai écrit GP avec numpy
J'ai essayé Learning-to-Rank avec Elasticsearch!
Essayez Auto Encoder avec Pytorch
J'ai fait un blackjack avec du python!
J'ai essayé le clustering avec PyCaret
Essayez d'implémenter XOR avec PyTorch
Implémentation de SMO avec Python + NumPy
Seq2Seq (3) ~ Edition CopyNet ~ avec chainer
Implémenter le GPU PyTorch + avec Docker
Prédiction de la moyenne Nikkei avec Pytorch 2
Démineur d'apprentissage automatique avec PyTorch
PyTorch avec AWS Lambda [importation Lambda]
Implémentation du GAN conditionnel avec chainer
Je ne peux pas effectuer de recherche avec # google-map. ..
Implémentation d'un GAN efficace avec keras
Prédiction de la moyenne Nikkei avec Pytorch
Effectuer un fractionnement stratifié avec PyTorch
J'ai essayé d'implémenter Extreme Learning Machine
J'ai mesuré l'IMC avec tkinter
J'ai essayé gRPC avec Python
J'ai créé COVID19_simulator avec JupyterLab
J'ai essayé de gratter avec du python
Implémentation de SmoothGrad avec Chainer v2
J'ai fait un blackjack avec Python.
Zura avec fonction softmax implémentée
J'ai créé wordcloud avec Python.
[Classification de texte] J'ai essayé d'implémenter des réseaux de neurones convolutifs pour la classification des phrases avec Chainer