Bei der Verwendung von Matrizen variabler Länge gibt es verschiedene Muster, z. B. bei der Verarbeitung natürlicher Sprache. Ich habe das Gefühl, dass ich es jedes Mal neu implementiere, also werde ich es als Memorandum zusammenfassen.
In diesem Artikel werde ich die Implementierung von Chainer und Tensorflow vorstellen, die ich häufig verwende. (Hinweis: Ich habe den Produktionscode nicht kopiert und eingefügt, sondern für diesen Beitrag von Grund auf neu implementiert, sodass er nicht getestet wurde.)
Ich denke, Chainer empfiehlt, variable Längen als Liste von "Variablen" zu verwalten, anstatt sie mit "Variable" + "Länge" zu verwalten, wie unten beschrieben. Insbesondere L.NStepLSTM
und [ F.pad_sequence
](https: / /docs.chainer.org/en/stable/reference/generated/chainer.functions.pad_sequence.html) und so weiter.
Der folgende Code basiert auf der Annahme, dass die folgenden Importe durchgeführt wurden.
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
Viele Deep-Learning-Frameworks unterstützen die Berechnung von Matrizen variabler Länge nicht direkt, um parallele GPU- und CPU-Berechnungen zu nutzen. Daher wird eine Polsterung durchgeführt, um das Teil außerhalb der Serienlänge mit einem geeigneten Wert entsprechend der maximalen Länge der Matrix zu füllen.
Darüber hinaus wird dieser Teil häufig in der Phase der Datenerstellung von Ihnen selbst ausgeführt, nicht im Rahmen des Deep Learning.
X = [np.array([1, 2]),
np.array([11, 12, 13, 14]),
np.array([21])]
#Mit int32 unter der Annahme, dass die Wort-ID behandelt wird
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]]
Bei Verwendung von Chainers L.EmbedId
ist es besser, -1-Padding anstelle von 0-Padding zu verwenden und L.EmbedId (..., ignore_label = -1)
zu verwenden.
Masking
Wenn Sie Summenpooling usw. durchführen, maskieren Sie den Teil außerhalb der durch Auffüllen erstellten Serienlänge mit 0 (jedoch [Maskierung nicht überbewerten](#Maskierung nicht überbewerten). Die Berechnung kann durch die Berechnung von "wo" realisiert werden.
(Wenn True, wird der l-Wert verwendet, und wenn False, wird der r-Wert als Maskierung verwendet.)
Wenn Sie diesen Prozess Schritt für Schritt schreiben:
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
ist in Tensorflow praktisch.
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.]]
Chainer-Version (statt Numpy-Version) 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
Da sich Deep Learning häufig mit Matrizen des Ranges 2 mit der Größe "Mini-Batch-Größe x Merkmalsmenge" befasst, bieten viele Frameworks viele Funktionen, die solche Matrizen als Eingabe verwenden. Um die Vorteile dieser Funktionen nutzen zu können, wird eine Matrix aus "Mini-Batch x Sequenzlänge x Merkmalsmenge" in eine Rang-2-Matrix aus "(Mini-Batch-Größe * Sequenzlänge) x Merkmalsmenge" umgewandelt und verarbeitet.
Dies ist jedoch eine Verschwendung zusätzlicher Verarbeitung, wenn die Matrix relativ dünn ist. Sie können die Verarbeitung reduzieren, indem Sie bei der Indizierung Ihr Bestes geben. (Ich habe es nicht ausprobiert, aber wenn die Matrix nicht dünn ist, kann es einige Zeit dauern, den Speicher neu zuzuweisen. Seien Sie also vorsichtig.)
Im Fall von Tensorflow kann eine solche Verarbeitung durch die folgende Verarbeitung realisiert werden.
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_Maske ist oben erwähnt
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.]]]
Im Fall von Tensorflow kann eine solche Verarbeitung durch die folgende Verarbeitung realisiert werden.
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.]]]
Erwägen Sie einen Softmax für die äußerste Dimension einer bestimmten Matrix. Solche Situationen treten in ListNet Permutation Wahrscheinlichkeitsverteilung und bei der Berechnung der Aufmerksamkeit auf.
Softmax-Formel
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)
Ich möchte Softmax nur mit dem blauen Bereich berechnen, wie in der folgenden Abbildung gezeigt.
Tragen Sie vorher / nachher keine Maske.
Chainer
#Schlechtes Beispiel 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]])
#Schlechtes Beispiel 2
y = F.softmax(x)
y[1, 2] = 0.
print y
# variable([[ 0.31153342, 0.47068265, 0.21778394],
# [ 0.26121548, 0.48049128, 0.0 ]])
#Die Summe der zweiten Zeile ist 1.Offensichtlich nicht, weil es nicht 0 ist
Der Grund ist sehr einfach, Beispiel 1 ist für $ exp (0.258) \ neq 0 $. In Beispiel 2 beeinflusst "x [2,1]" die Berechnung des Nenners.
Bei der Softmax-Berechnung wird die Maskierung mit $ exp (-inf) = 0 $ durchgeführt.
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:
Im Deep-Learning-Framework gibt es bei einer Division auf 0 eine Spezifikation, bei der der Gradient zu "inf" wird, selbst wenn "where" verwendet wird. Daher funktioniert "Ich sollte maskieren, auch wenn ich eine instabile Berechnung mache" nicht.
Es gibt ein Netzwerk wie die folgende Formel.
e = f_0(x) \\
w = f_1(e)
Dies wird durch die Kettenregel wie folgt ausgedrückt.
Dies wird nun (ungefähr) durch automatische Differenzierung wie folgt erreicht.
x.grad = e.grad * g(f_0, e, x)
Hier ist "g (f_0, e, x)" ein partielles Differential, ausgedrückt aus $ f_0 $ und seiner Eingabe / Ausgabe. Mit anderen Worten, unabhängig davon, welcher Differentialwert "z. B. rad" aus der oberen Formel stammt, wenn "der partielle Differentialwert der Formel $ f_0 $" inf "oder" nan "ist, ist" x.grad "auch" inf ". Es wird "oder" nan ". Wenn Sie dies mit Chainer und Tensorflow versuchen,
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