[PYTHON] Conseils pour gérer les entrées de longueur variable dans le cadre d'apprentissage en profondeur

introduction

Il existe plusieurs modèles lors de l'utilisation de matrices de longueur variable comme dans le traitement du langage naturel. J'ai l'impression de le réappliquer à chaque fois, alors je vais le résumer sous forme de mémorandum.

Dans cet article, je vais mettre en place l'implémentation de Chainer et Tensorflow que j'utilise souvent. (Remarque: je n'ai pas copié et collé le code de production, je l'ai réimplémenté à partir de zéro pour cet article, donc il n'a pas été testé.)

Points à noter à propos de Chainer

Je pense que Chainer recommande de gérer les longueurs variables comme une liste de «variables», plutôt que de les gérer avec «Variable» + «longueur» comme décrit ci-dessous. Plus précisément, L.NStepLSTM et [ F.pad_sequence](https: / /docs.chainer.org/en/stable/reference/generated/chainer.functions.pad_sequence.html) et ainsi de suite.

Remarque

Le code ci-dessous est basé sur l'hypothèse que les importations suivantes ont été effectuées.

Chainer


import chainer
import chainer.functions as F
import numpy as np

Tensorflow


import tensorflow as tf
import numpy as np


sess = tf.InteractiveSession()

Texte

Padding

De nombreux frameworks d'apprentissage en profondeur ne prennent pas directement en charge le calcul de matrices de longueur variable pour tirer parti des calculs parallèles GPU et CPU. Par conséquent, un remplissage est effectué pour remplir la partie en dehors de la longueur de série avec une valeur appropriée en fonction de la longueur maximale de la matrice.

De plus, cette partie est souvent effectuée par vous-même au stade de la création de données, pas dans le cadre du deep learning.

X = [np.array([1, 2]),
     np.array([11, 12, 13, 14]),
     np.array([21])]

#Avec int32 en supposant la gestion de l'ID de mot
x = np.zeros([3, 4], dtype=np.int32)

for i, xi in enumerate(X):
    x[i, :len(xi)] = xi[:]

print x
# [[ 1  2  0  0]
#  [11 12 13 14]
#  [21  0  0  0]]

Lorsque vous utilisez L.EmbedId de Chainer, il est préférable d'utiliser -1 padding au lieu de 0 padding et d'utiliser L.EmbedId (..., ignore_label = -1).

Masking

Lorsque vous effectuez un regroupement de somme, etc., masquez la partie en dehors de la longueur de la série créée par Padding avec 0 (cependant, [Ne pas surconfigurer le masquage](ne pas surconfigurer #Mask). Le calcul peut être réalisé par le calcul de «où».

mask.png (Si True, la lvalue est utilisée, et si False, la rvalue est utilisée pour fonctionner comme un masquage.)

Si vous écrivez ce processus étape par étape:

Chainer


x = chainer.Variable(np.arange(1, 7).reshape(2, 3))
print x
# variable([[1 2 3]
#           [4 5 6]])

length = np.array([3, 2], dtype=np.int32)
print length
# [3 2]

xp = chainer.cuda.get_array_module(x.data)
mask = xp.tile(xp.arange(x.shape[-1]).reshape(1, -1), (x.shape[0], 1))
print mask
# [[0 1 2]
#  [0 1 2]]

mask = mask < length.reshape(-1, 1)
print mask
# [[ True  True  True]
#  [ True  True False]]

padding = xp.zeros(x.shape, dtype=x.dtype)
print padding
# [[0 0 0]
#  [0 0 0]]

z = F.where(mask, x, padding)
print z
# variable([[1 2 3]
#           [4 5 0]])

sequence_mask est pratique dans Tensorflow.

Tensorflow


x = tf.constant(np.arange(1, 7).reshape(2, 3).astype(np.float32))
length = tf.constant(np.array([3, 2], dtype=np.int32))

mask = tf.sequence_mask(length, tf.shape(x)[-1])
padding = tf.fill(tf.shape(x), 0.0)
z = tf.where(mask, x, padding)
print z.eval()
# [[ 1.  2.  3.]
#  [ 4.  5.  0.]]

Version Chainer (plutôt que la version numpy) sequence_mask

Chainer


def sequence_mask(length, max_num=None):
    xp = chainer.cuda.get_array_module(length.data)
    if max_num is None:
        max_num = xp.max(length)
    # create permutation on (length.ndim + 1) dimension
    perms = xp.arange(max_num).reshape([1] * length.ndim + [-1])
    length = length.reshape([1] * (length.ndim - 1) + [-1] + [1])
    return perms < length

Reshape

Étant donné que l'apprentissage en profondeur traite souvent des matrices de rang 2 de «taille mini-lot x quantité de fonctionnalités», de nombreux frameworks fournissent de nombreuses fonctions qui prennent ces matrices en entrée. Afin de profiter des avantages de ces fonctions, une matrice de «mini-lot x longueur de séquence x quantité de caractéristiques» est convertie en une matrice de rang 2 de «(taille de mini-lot * longueur de séquence) x quantité de caractéristiques» et traitée.

reshape_1.png

Cependant, c'est un gaspillage de traitement supplémentaire lorsque la matrice est relativement clairsemée. Vous pouvez réduire le traitement en faisant de votre mieux dans l'indexation. (Je ne l'ai pas essayé, mais si la matrice n'est pas clairsemée, cela peut prendre du temps pour réallouer la mémoire, donc soyez prudent)

reshape_2.png

Dans le cas de Tensorflow, un tel traitement peut être réalisé par le traitement suivant.

reshape_4.png

Chainer


# WARNING: I have not checked it in case of rank != 3

x = chainer.Variable(np.arange(18).astype(np.float32).reshape(3, 3, 2))
length = np.array([2, 3, 1], dtype=np.int32)
w = chainer.Variable(np.ones([2, 3], dtype=np.float32))

# sequence_le masque est mentionné ci-dessus
mask = sequence_mask(length, x.shape[length.ndim])
print mask
# [[ True  True False]
#  [ True  True  True]
#  [ True False False]]

x_reshaped = F.get_item(x, mask)
print x_reshaped
# [[  0.   1.]
#  [  2.   3.]
#  [  6.   7.]
#  [  8.   9.]
#  [ 10.  11.]
#  [ 12.  13.]]

y_reshaped = F.matmul(x_reshaped, w)
print y_reshaped
# [[  1.   1.   1.]
#  [  5.   5.   5.]
#  [ 13.  13.  13.]
#  [ 17.  17.  17.]
#  [ 21.  21.  21.]
#  [ 25.  25.  25.]]

pad_shape = [[0, 0] for _ in xrange(y_reshaped.ndim)]
pad_shape[length.ndim - 1][1] = 1
y_reshaped = F.pad(y_reshaped, pad_shape, 'constant', constant_values=0.)
print y_reshaped
# variable([[  1.,   1.,   1.],
#           [  5.,   5.,   5.],
#           [ 13.,  13.,  13.],
#           [ 17.,  17.,  17.],
#           [ 21.,  21.,  21.],
#           [ 25.,  25.,  25.],
#           [  0.,   0.,   0.]])


idx_size = np.prod(mask.shape)
inv_idx = np.ones([idx_size], dtype=np.int32) * -1
inv_idx[np.nonzero(mask.flat)[0]] = np.arange(x_reshaped.shape[0]).astype(np.int32)
print inv_idx
# [ 0  1 -1  2  3  4  5 -1 -1]

y = F.reshape(F.get_item(y_reshaped, inv_idx), list(x.shape[:length.ndim + 1]) + [-1])
print y
# [[[  1.   1.   1.]
#   [  5.   5.   5.]
#   [  0.   0.   0.]]
# 
#  [[ 13.  13.  13.]
#   [ 17.  17.  17.]
#   [ 21.  21.  21.]]
# 
#  [[ 25.  25.  25.]
#   [  0.   0.   0.]
#   [  0.   0.   0.]]]

Dans le cas de Tensorflow, un tel traitement peut être réalisé par le traitement suivant.

reshape_3.png

Tensorflow


# WARNING: I have not checked it in case of rank != 3
x = tf.constant(np.arange(18).astype(np.float32).reshape(3, 3, 2))
length = tf.constant(np.array([2, 3, 1], dtype=np.int32))
w = tf.constant(np.ones([2, 3], dtype=np.float32))

mask = tf.sequence_mask(length, tf.shape(x)[tf.rank(length)])
print mask.eval()
# [[ True  True False]
#  [ True  True  True]
#  [ True False False]]

x_reshaped = tf.boolean_mask(x, mask)
print x_reshaped.eval()
# [[  0.   1.]
#  [  2.   3.]
#  [  6.   7.]
#  [  8.   9.]
#  [ 10.  11.]
#  [ 12.  13.]]

y_reshaped = tf.matmul(x_reshaped, w)
print y_reshaped.eval()
# [[  1.   1.   1.]
#  [  5.   5.   5.]
#  [ 13.  13.  13.]
#  [ 17.  17.  17.]
#  [ 21.  21.  21.]
#  [ 25.  25.  25.]]

idx = tf.to_int32(tf.where(mask))
print idx.eval()
# [[0 0]
#  [0 1]
#  [1 0]
#  [1 1]
#  [1 2]
#  [2 0]]

shape = tf.concat([tf.shape(x)[:-1], tf.shape(y_reshaped)[-1:]], 0)
print shape.eval()
# [3 3 3]

y = tf.scatter_nd(idx, y_reshaped, shape)
print y.eval()
# [[[  1.   1.   1.]
#   [  5.   5.   5.]
#   [  0.   0.   0.]]
# 
#  [[ 13.  13.  13.]
#   [ 17.  17.  17.]
#   [ 21.  21.  21.]]
# 
#  [[ 25.  25.  25.]
#   [  0.   0.   0.]
#   [  0.   0.   0.]]]

Implémentation de Softmax

Envisagez de faire un softmax sur la dimension la plus externe d'une matrice donnée. De telles situations se produisent dans Distribution de probabilité de permutation ListNet et dans les calculs d'attention.

Formule Softmax $ y_i = \frac{exp(x_i)}{\sum_jexp({x_j})} $

x = np.random.random([2, 3]).astype(np.float32)
# array([[ 0.44715771,  0.85983515,  0.08915455],
#        [ 0.02465274,  0.63411605,  0.01340247]], dtype=float32)

length = np.array([3, 2], dtype=np.int32)

Je veux calculer Softmax en utilisant uniquement la zone bleue comme indiqué dans la figure ci-dessous.

masked_softmax.png

Au fait, ne portez pas de masque avant / après.

Chainer


#Mauvais exemple 1
x_ = np.copy(x)
x_[1, 2] = 0.
print F.softmax(x_)
# variable([[ 0.31153342,  0.47068265,  0.21778394],
#           [ 0.26211682,  0.48214924,  0.25573397]])

#Mauvais exemple 2
y = F.softmax(x)
y[1, 2] = 0.
print y
# variable([[ 0.31153342,  0.47068265,  0.21778394],
#           [ 0.26121548,  0.48049128,  0.0       ]])
#Le total de la deuxième ligne est 1.Evidemment non car ce n'est pas 0

La raison est très simple, l'exemple 1 est pour $ exp (0.258) \ neq 0 $. Dans l'exemple 2, «x [2,1]» affecte le calcul du dénominateur.

Dans le calcul Softmax, le masquage est effectué en utilisant $ exp (-inf) = 0 $.

Chainer


def masked_softmax(x, length):
    """
    Softmax operation on the ourter-most dimenstion of x.

    Args:
         x (chainer.Variable): Values to be passed to softmax
         length (numpy.ndarray or cupy.ndarray):
             Number of items in the outer-most dimension of x
    """
    assert x.ndim - 1 == length.ndim
    xp = chainer.cuda.get_array_module(x.data)
    x_shape = x.shape
    x = F.reshape(x, (-1, x_shape[-1]))
    # mask: (B, T)
    mask = xp.tile(xp.arange(x.shape[-1]).reshape(1, -1), (x.shape[0], 1))
    mask = mask < length.reshape(-1, 1)
    padding = xp.ones(x.shape, dtype=x.dtype) * -np.inf
    z = F.where(mask, x, padding)
    return F.reshape(F.softmax(z), x_shape)


print masked_softmax(chainer.Variable(x), length)
# variable([[ 0.31153342,  0.47068265,  0.21778394],
#           [ 0.35218161,  0.64781839,  0.        ]])

Tensorflow


def masked_softmax(x, length):
    """
    Softmax operation on the ourter-most dimenstion of x.

    Args:
         x (tf.Tensor): Values to be passed to softmax
         length (tf.Tensor): Number of items in the outer-most dimension of x
    """
    mask = tf.sequence_mask(length, tf.shape(x)[-1])
    padding = tf.fill(tf.shape(x), -np.inf)
    z = tf.where(mask, x, padding)
    return tf.nn.softmax(z, dim=-1)


print masked_softmax(
    tf.constant(x),
    tf.constant(length)).eval()
# [[ 0.31153342,  0.47068265,  0.21778394],
#  [ 0.35218161,  0.64781839,  0.        ]]

Appendix:

Ne pas trop vous confier à Mask

Dans le cadre d'apprentissage en profondeur, lorsque la division à 0 se produit, il existe une spécification où le gradient devient ʻinf même si where` est utilisé. Par conséquent, "je devrais masquer même si je fais un calcul instable" ne fonctionne pas.

Il existe un réseau comme la formule suivante.

e = f_0(x) \\
w = f_1(e)

Ceci est exprimé par la règle de la chaîne comme suit. $ \frac{\partial w}{\partial x} = \frac{\partial w}{\partial e}\frac{\partial e}{\partial x} $

Maintenant, ceci est réalisé (en gros) par différenciation automatique comme suit.

x.grad = e.grad * g(f_0, e, x)

Ici, g (f_0, e, x) est un différentiel partiel exprimé à partir de $ f_0 $ et de son entrée / sortie. En d'autres termes, quelle que soit la valeur différentielle ʻe.grad provenant de la formule supérieure, si la valeur différentielle partielle de la formule $ f_0 $ est ʻinf ou nan, x.grad sera également ʻinf. Cela devient «ou» nan ». Si vous essayez ceci avec Chainer et Tensorflow,

Tensorflow


sess = tf.InteractiveSession()

x = tf.constant(0.0)

t = x
e = 1. / x
w = tf.where(True, t, e)

print w.eval()  # 0.0
print tf.gradients(w, x)[0].eval()  # nan

Chainer


x = chainer.Variable(np.array([0.0], dtype=np.float32))
t = x
e = 1. / x
w = chainer.functions.where(np.array([True]), t, e)

w.grad = np.array([1.0], np.float32)
w.backward(retain_grad=True)

print w  # 0.
print x.grad  # nan

Recommended Posts

Conseils pour gérer les entrées de longueur variable dans le cadre d'apprentissage en profondeur
Apprendre en profondeur à l'expérience avec Python Chapitre 2 (Matériel pour une conférence ronde)
Apprentissage profond pour la formation composée?
[AI] Apprentissage en profondeur pour le débruitage d'image
Apprentissage profond à partir de zéro - Conseils du chapitre 4 pour la théorie de l'apprentissage profond et la mise en œuvre apprise en Python
Windows → Linux Conseils pour importer des données
Conseils pour gérer les binaires en Python
Modèle de reconnaissance d'image utilisant l'apprentissage profond en 2016
Créez votre propre PC pour un apprentissage en profondeur
Exemple de gestion des fichiers eml en Python
Conseils pour créer de grandes applications avec Flask
"Deep Learning from scratch" avec Haskell (inachevé)
[Apprentissage en profondeur] Détection de visage Nogisaka ~ Pour les débutants ~
Conseils pour créer de petits outils avec python
À propos du traitement d'expansion des données pour l'apprentissage en profondeur
Introduction au Deep Learning (1) --Chainer est expliqué d'une manière facile à comprendre pour les débutants-
[Pour les débutants] Après tout, qu'est-ce qui est écrit dans Deep Learning fait à partir de zéro?