Einführung in ausgefranste Tensoren
- Von Torben Windler
Beitrag teilen:
Einführung
zerlumpten Tensoren
, das Ende 2018 eingeführt wurde. Wir erklären ihre grundlegende Struktur und warum sie nützlich sind.
Problemstellung
Durch die Standardisierung traditioneller Machine Learning-Probleme sind viele Modelle sehr einfach zu implementieren: Lesen Sie eine Tabelle mit Merkmalen aus einer Datenbank, verwenden Sie pandas und numpy für die Vorverarbeitung, erstellen Sie ein Modell mit einer der bekannten Bibliotheken und geben Sie model.fit() ein. In vielen Fällen – das war’s! Erledigt!
Aber halt – was ist, wenn die Daten nicht in Tabellenform vorliegen und von Natur aus unregelmäßig sind? Was ist, wenn es Instanzen mit unterschiedlichen Abmessungen gibt? Betrachten wir ein Szenario für die Klassifizierung von Zeitreihen und nehmen wir an, wir haben einen Datensatz, der aus vier verschiedenen kurzen Zeitreihen besteht:
Wie Sie sehen können, unterscheiden sich die Serien sowohl in der Anzahl als auch in der Zeit der Messungen. Da Modelle für maschinelles Lernen in der Regel eine feste Eingabegröße benötigen, ist es etwas komplizierter, solche Daten in unsere Modelle einzupassen.
Es gibt eine Reihe von Möglichkeiten, mit dieser Art von Eingaben umzugehen. Wir könnten zum Beispiel die Serien interpolieren und virtuelle Messungen zu den gleichen Zeitpunkten für jede Serie vornehmen:
Hier nehmen wir die Werte der Zeitstempel 0, 2, 4, 6, 8 und 10, so dass jede Serie aus 6 Werten besteht. In diesem Stadium müssen wir jedoch bereits Hyperparameter wie die Art der Interpolation, die Anzahl der Werte usw. auswählen. Wir können uns jedoch nicht auf die Genauigkeit der Interpolation verlassen, insbesondere nicht bei extrapolierten Werten und bei Werten innerhalb großer Lücken zwischen aufeinanderfolgenden Messungen (siehe die orangefarbenen und grünen Reihen bei Zeit 10).
Wenn wir die Daten in ein TensorFlow-Keras-Modell einspeisen und keine Interpolationstechniken verwenden möchten, besteht eine gängige Praxis darin, die Reihen aufzufüllen, z.B. mit Nullen am Ende. Dies ist notwendig, weil TensorFlow Datenstapel zusammenfasst, die in jeder Dimension die gleiche Form haben müssen. Eine Charge mit den oben genannten 4 Serien hätte die Form (4, 6), wobei 4 die Anzahl der Serien (=Chargengröße) und 6 die Anzahl der Messungen pro Serie ist.
Die 6 ergibt sich jedoch aus künstlichen Daten, entweder interpolierten Messungen oder Auffüllungswerten. Um die Ungewissheit und den Overhead dieser beiden Techniken zu überwinden, können wir die Originaldaten mit Ragged-Tensoren bearbeiten.
Konzept der ausgefransten Tensoren
Das Konzept der zerlumpten Tensoren ist überraschend einfach, nachdem Sie die Absicht dahinter verstanden haben. Bleiben wir bei unserem obigen Beispiel mit 4 Zeitreihen. Wie Sie sehen können, ist die Mindestanzahl der Messungen pro Serie 3, während die Höchstzahl 5 beträgt. Mit Padding müssten wir jede Reihe mit Nullen am Ende (oder manchmal am Anfang) auffüllen, um eine gemeinsame Länge von 5 zu erreichen.
Im Gegensatz dazu besteht ein Ragged-Tensor aus der Verkettung aller Werte aus allen Serien zusammen mit Metadaten, die angeben, wo die Verkettung in die einzelnen Serien aufgeteilt werden soll. Definieren wir unseren Datenrahmen df und dann unseren Ragged-Tensor rt:
Zeit | Wert |
---|---|
0 | 3 |
3 | 1 |
6 | 8 |
8 | 0 |
10 | 9 |
0 | 15 |
5 | 11 |
8 | 7 |
0 | 12 |
2 | 7 |
4 | 8 |
9 | 2 |
0 | 9 |
4 | 0 |
6 | 13 |
10 | 4 |
row_splits = [0, 5, 8, 12, 16]
rt = tf.RaggedTensor.from_row_splits(values=df.values, row_splits=row_splits)
rt
<tf.RaggedTensor [[[0, 3], [3, 1], [6, 8], [8, 0], [10, 9]], [[0, 15], [5, 11], [8, 7]], [[0, 12], [2, 7], [4, 8], [9, 2]], [[0, 9], [4, 0], [6, 13], [10, 4]]]>
Wie wir sehen können, definiert das row_splits-Array die einzelnen Reihen durch die Angabe ihrer Startreihe (einschließlich) und Endreihe (ausschließlich).
Das war’s. Dies ist die wirklich einfache Struktur der zerlumpten Tensoren. Alternativ zur Angabe von row_splits können wir denselben Ragged-Tensor auch mit einer der folgenden Methoden erstellen:
- value_rowids: für jede Zeile in der verketteten Reihe geben wir eine ID-Nummer an, die die einzelne Reihe indiziert:
value_rowids = [0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]
rt_1 = tf.RaggedTensor.from_value_rowids(values=df.values, value_rowids=value_rowids)
rt_1
<tf.RaggedTensor [[[0, 3], [3, 1], [6, 8], [8, 0], [10, 9]], [[0, 15], [5, 11], [8, 7]], [[0, 12], [2, 7], [4, 8], [9, 2]], [[0, 9], [4, 0], [6, 13], [10, 4]]]>
- row_lengths: wir geben die Länge jeder einzelnen Reihe an:
row_lengths = [5, 3, 4, 4]
rt_2 = tf.RaggedTensor.from_row_lengths(values=df.values, row_lengths=row_lengths)
rt_2
<tf.RaggedTensor [[[0, 3], [3, 1], [6, 8], [8, 0], [10, 9]], [[0, 15], [5, 11], [8, 7]], [[0, 12], [2, 7], [4, 8], [9, 2]], [[0, 9], [4, 0], [6, 13], [10, 4]]]>
- Konstante: Wir können den Ragged-Tensor als „Konstante“ definieren, indem wir direkt eine Liste von Arrays angeben:
rt_3 = tf.ragged.constant([df.loc[0:4, :].values, df.loc[5:7, :].values, df.loc[8:11, :].values, df.loc[12:15, :].values])
rt_3
<tf.RaggedTensor [[[0, 3], [3, 1], [6, 8], [8, 0], [10, 9]], [[0, 15], [5, 11], [8, 7]], [[0, 12], [2, 7], [4, 8], [9, 2]], [[0, 9], [4, 0], [6, 13], [10, 4]]]>
Intern spielt es keine Rolle, welche Methode wir wählen, um einen zerlumpten Tensor zu erstellen, die Ergebnisse sind alle gleichwertig. Als Nächstes sehen wir uns an, wie man mathematische Operationen mit zerlumpten Tensoren durchführt.
Arbeiten mit ausgefransten Tensoren
TensorFlow bietet eine sehr praktische Funktion, um Operationen auf Ragged-Tensoren durchzuführen: tf.ragged.map_flat_values(op, *args, **kwargs). Sie tut das, was der Funktionsname sagt – jeder zerlumpte Tensor in args wird durch seine verkettete (=flache) Version ersetzt, wobei die Stapeldimension weggelassen wird. In unserem Beispiel ist dies dasselbe, als wenn wir direkt mit den df.values arbeiten würden. Der einzige Unterschied besteht darin, dass die Ausgabe der Operation wieder ein Ragged Tensor mit denselben Metadateninformationen darüber ist, wo geteilt werden soll. Betrachten wir ein Beispiel, bei dem wir das Matrixprodukt des Ragged-Tensors mit einer Matrix m der Form (2, 5) berechnen. Jede einzelne Serie in unserem Ragged-Tensor hat die Form (k, 2), wobei k der Anzahl der Messungen in der jeweiligen Serie entspricht. Achten Sie darauf, dass Sie zuerst die Schwimmer anwerfen:
m = tf.random.uniform(shape=[2, 5])
print(m.shape)
(2, 5)
rt = tf.cast(rt, tf.float32)
result = tf.ragged.map_flat_values(tf.matmul, rt, m)
print(*(t.shape for t in result), sep='\n')
(5, 5)
(3, 5)
(4, 5)
(4, 5)
Perfekt! Der resultierende Ragged-Tensor hat die gleichen Zeilensplits wie die Eingabe, aber die innere Dimension hat sich durch die Matrixmultiplikation von 2 auf 5 geändert. Wir könnten einige kompliziertere Operationen durchführen, zum Beispiel wenn m keine 2-dimensionale Matrix, sondern ein 3-dimensionaler Tensor ist:
m = tf.random.uniform(shape=[2, 5, 4])
print(m.shape)
(2, 5, 4)
rt = tf.cast(rt, tf.float32)
result = tf.ragged.map_flat_values(tf.einsum, "bi, ijk -> bjk", rt, m)
print(*(t.shape for t in result), sep='\n')
(5, 5, 4)
(3, 5, 4)
(4, 5, 4)
(4, 5, 4)
Wie erwartet, entspricht die Chargen-Dimension b der Länge der einzelnen Serien, während die anderen Dimensionen von m
stammen. Übrigens bezieht sich tf.einsum auf die Einstein-Summenkonvention, die äußerst praktisch ist, wenn wir mit höherdimensionalen Tensoren arbeiten. Lesen Sie mehr darüber
hier
.
Ein letzter Punkt: Es ist auch sehr einfach, Aggregationen über ausgefranste Tensoren durchzuführen. Wenn wir zum Beispiel die spaltenweise Summe wissen wollen, können wir dafür Reduktionsfunktionen verwenden:
tf.reduce_sum(rt, axis=1)
<tf.Tensor: shape=(4, 2), dtype=float32, numpy=
array([[27., 21.],
[13., 33.],
[15., 29.],
[20., 26.]], dtype=float32)>
Es gibt noch viele weitere Operationen für ausgefranste Tensoren, die hier aufgelistet sind.
Fazit
Wir haben die Struktur von TensorFlow Ragged Tensoren kennengelernt und wie man grundlegende mathematische Operationen mit ihnen durchführt. Sie machen es unnötig, unnatürliche Vorverarbeitungstechniken wie Interpolation oder Auffüllen anzuwenden. Dies ist besonders nützlich für unregelmäßige Zeitreihendatensätze, obwohl es viele andere Anwendungen gibt. Stellen Sie sich einen Datensatz mit Bildern unterschiedlicher Größe vor – Ragged-Tensoren können sogar mit mehreren Ragged-Dimensionen umgehen, perfekt dafür.
In einem späteren Beitrag werde ich etwas tiefer in die Arbeit mit Ragged-Tensoren als Eingabetypen für ein Keras-Modell eintauchen, indem ich die einzelnen Zeitreihen als Sets behandle und die Aufmerksamkeit direkt auf die Ragged-Tensoren lenke. Bleiben Sie dran!