Der Tensor
Alles in ML ist Rechnen auf Zahlen-Arrays.
Meilenstein a+b, a*b und a@b rechnen korrekt.
PyTorch heißt PyTorch wegen des Tensors. Alles, was durch ein neuronales Netz
läuft, ist einer: die Eingabedaten, die Gewichte, jedes Zwischenergebnis und
später auch jeder Gradient. Und ein Tensor ist nichts Geheimnisvolles, sondern
ein Feld aus Zahlen mit einer Form. Ein Graustufenbild ist ein Tensor der Form
(28, 28), ein Stapel aus 32 solchen Bildern einer der Form (32, 28, 28).
Dieser erste Schritt baut genau diese Hülle. Sie kann noch nichts von Gradienten wissen, das kommt im nächsten Kapitel. Sie kann drei Dinge: addieren, elementweise multiplizieren und das Matrixprodukt bilden. Mehr braucht ein Netz für den Vorwärtslauf nicht.
Das Ziel
Alles in ML ist Rechnen auf Zahlen-Arrays. Wer das einmal ausgesprochen hat, verliert einen guten Teil der Ehrfurcht vor der Sache. Ein Netz „versteht“ nichts. Es multipliziert eine Matrix mit einer anderen, addiert einen Vektor dazu und schneidet negative Werte ab. Die drei Operationen dieses Kapitels sind schon fast alles, was in einem Vorwärtslauf passiert.
Wichtig ist der Unterschied zwischen den beiden Multiplikationen, weil er später die häufigste Fehlerquelle ist:
a * bist elementweise: gleiche Form rein, gleiche Form raus. Jede Zahl trifft ihre Partnerin an derselben Stelle.a @ bist das Matrixprodukt:(n, k)mal(k, m)ergibt(n, m). Hier fallen Dimensionen zusammen, und genau das macht eine Schicht aus.
Zero Magic in diesem Schritt
Die Regel des Kurses gilt ab der ersten Zeile: NumPy ist erlaubt, torch,
jax, tinygrad und autograd sind verboten. NumPy nimmt dir das Rechnen auf
Zahlenfeldern ab - np.asarray, +, *, @ -, und das ist in Ordnung, weil
schnelle Array-Arithmetik nicht der Punkt dieses Kurses ist. Der Punkt ist die
Hülle darum, und die schreibst du selbst.
Das Gerüst
Leg dein Repo an. Zwei Ebenen, weil meintorch gleichzeitig das Projekt und das
Paket darin ist - das ist das übliche Layout für Python-Pakete:
meintorch/ ← dein Repo
├── meintorch/ ← das Paket, dein Framework
│ ├── __init__.py (leer)
│ └── tensor.py
└── tests/
└── test_01_tensor.py
Die Tests startest du immer aus dem Repo-Wurzelverzeichnis heraus mit
python -m pytest. Das -m ist nicht Geschmackssache: Es legt das aktuelle
Verzeichnis auf den Importpfad, und nur deshalb findet from meintorch.tensor import Tensor überhaupt etwas.
Die Aufgabe
Das ist der Coach-Modus: Du bekommst das Gerüst und die Lücken, den Kern
schreibst du. meintorch/tensor.py:
import numpy as np
def as_tensor(x):
"""Zahlen und Listen werden zu Tensoren, Tensoren bleiben, was sie sind."""
return x if isinstance(x, Tensor) else Tensor(x)
class Tensor:
"""Ein Feld aus Zahlen mit einer Form. Mehr ist es an dieser Stelle nicht."""
def __init__(self, data):
self.data = np.asarray(data, dtype=np.float64)
@property
def shape(self):
return self.data.shape
def __repr__(self):
return f"Tensor({self.data})"
def __add__(self, other):
# TODO(du): elementweise Addition, Ergebnis wieder als Tensor
raise NotImplementedError
def __mul__(self, other):
# TODO(du): elementweise Multiplikation - kein Matrixprodukt!
raise NotImplementedError
def __matmul__(self, other):
# TODO(du): das Matrixprodukt
raise NotImplementedError
Hinweis 1 - die Richtung
Jede der drei Methoden ist ein Dreizeiler nach demselben Muster: die andere Seite zu einem Tensor machen, NumPy rechnen lassen, das Ergebnis wieder in einen Tensor packen. Nichts davon braucht eine Schleife.
Hinweis 2 - konkreter
self.data und other.data sind NumPy-Arrays. NumPy kennt für alle drei Fälle
schon den passenden Operator; du gibst ihn nur weiter. Und as_tensor steht
nicht zufällig oben - ohne den Aufruf scheitert a * 2.0, weil eine Zahl kein
.data hat.
Lösung anzeigen
def __add__(self, other):
other = as_tensor(other)
return Tensor(self.data + other.data)
def __mul__(self, other):
other = as_tensor(other)
return Tensor(self.data * other.data)
def __matmul__(self, other):
other = as_tensor(other)
return Tensor(self.data @ other.data)
Im Goal-Modus
Derselbe Schritt, autonom: Der Agent legt Repo und Paket an, implementiert die drei Operatoren, schreibt den Meilenstein-Test mit und führt ihn aus, bis er grün ist. Er fragt nicht nach, er liefert ein fertiges Kapitel ab. Der Auftrag dafür lautet:
Lege ein Python-Repo `meintorch/` mit dem Paket `meintorch/meintorch/` an. Implementiere in `meintorch/tensor.py` eine Klasse `Tensor`, die ein NumPy-Array (dtype float64) kapselt und `__add__`, `__mul__` und `__matmul__` unterstützt - elementweise Addition, elementweise Multiplikation, Matrixprodukt. Skalare und Listen auf der rechten Seite müssen ebenfalls funktionieren. Zwingend: NumPy ist erlaubt, torch/jax/tinygrad/autograd sind verboten. Schreibe dazu `tests/test_01_tensor.py` und führe `python -m pytest` aus, bis alle Tests grün sind. Zeige mir am Ende die Testausgabe.
Der Meilenstein-Test
tests/test_01_tensor.py. Grün heißt: Kapitel fertig.
import numpy as np
from meintorch.tensor import Tensor
def test_addition_ist_elementweise():
a = Tensor([1.0, 2.0, 3.0])
b = Tensor([10.0, 20.0, 30.0])
assert np.allclose((a + b).data, [11.0, 22.0, 33.0])
def test_multiplikation_ist_elementweise():
a = Tensor([1.0, 2.0, 3.0])
b = Tensor([10.0, 20.0, 30.0])
assert np.allclose((a * b).data, [10.0, 40.0, 90.0])
def test_matmul_ist_das_matrixprodukt():
a = Tensor([[1.0, 2.0], [3.0, 4.0]])
b = Tensor([[5.0, 6.0], [7.0, 8.0]])
assert np.allclose((a @ b).data, [[19.0, 22.0], [43.0, 50.0]])
def test_form_und_skalare():
a = Tensor([[1.0, 2.0, 3.0]])
assert a.shape == (1, 3)
assert np.allclose((a * 2.0).data, [[2.0, 4.0, 6.0]])