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é.)
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.
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()
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ù».
(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.
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)
Dans le cas de Tensorflow, un tel traitement peut être réalisé par le traitement suivant.
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.
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.]]]
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
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.
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:
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.
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