- Python 95.7%
- CSS 1.7%
- HTML 1.5%
- JavaScript 1.1%
- i18n.py: supported_langs() scannt translations/*.json - __init__.py: context_processor injectiert supported_langs + current_lang - templates: Lang-Switch rendert dynamisch aus supported_langs - translations/en.json: Web-Keys (login, register, settings, auth, session) - translations/ar.json: gelöscht |
||
|---|---|---|
| data | ||
| HyperParameter | ||
| Model | ||
| Server | ||
| tests | ||
| Trainer | ||
| translations | ||
| .gitignore | ||
| conftest.py | ||
| Errorcodes.md | ||
| ideas.md | ||
| manual_test.py | ||
| manual_test2.py | ||
| manual_test_gan.py | ||
| manual_test_llm.py | ||
| README.md | ||
| requirements-core.txt | ||
| requirements.txt | ||
| test_activationfunctions.py | ||
ML_Projekt
⚠️ Dieses README ist vorläufig und KI-generiert. Es wurde von einer KI auf Basis des Repository-Stands am 17.08.2026 erstellt und ist noch nicht durch einen Menschen geprüft worden. Code-Zitate, Zahlen (z. B. die Anzahl der Layer-Adapter) und API-Beispiele können fehlerhaft oder veraltet sein – bitte vor der Verwendung gegen den aktuellen Code und den Betreuer des Repositories gegenprüfen.
Ein eigenes, deklaratives Neural-Network-Framework auf PyTorch-Basis.
Statt Netze imperativ zusammenzustecken, beschreibt man die Topologie als
gerichteten Graphen aus Sources und Targets – aus diesem Graphen baut
ModelBuilder.build() dann das ausführbare nn.Module (inkl. Backprop).
Kernidee
Drei Bausteine, sauber getrennt:
-
Layer-Adapter (
Model/layer/) – jeder Adapter kapselt genau einen PyTorch-Baustein (nn.Linear,nn.Conv2d, …) hinter einer einheitlichenLayer-Basisklasse.generate_layer()liefert den fertigennn.Module. 174 Adapter, u. a. für:- Aktivierungen (
activations.py) - Linear & Bilinear (
linear.py) - Conv / ConvTranspose 1D/2D/3D (
conv.py) - Max/Avg/Adaptive/Global/LP-Pooling (
pooling.py,maxpool.py) - Normalisierung: Batch / Sync-Batch / Layer / Group / Instance (
normalization.py) - Rekurrent: RNN, LSTM, GRU (inkl. Cells) (
recurrent.py) - Tensor-Operationen: Reshape, Split/Concat, MatMul, Elementwise, Dim-Reduktion (
tensoroperation.py) - Dropout-Varianten (
dropout.py) - Loss-Funktionen als Layer: MSE, CrossEntropy, BCE, Focal, Dice, … (
loss.py) - Attention, Embedding (
attention.py) - Sonstiges: Upsample, Padding, PixelShuffle, OneHot, Fold/Unfold (
misc.py)
- Aktivierungen (
-
Graph-Building (
Model/model_builder.py) – Kanten werden als(Source, Target)-Paare definiert:Source Bedeutung InputSource(id)Modell-Eingang, id= Position des Arguments inforward(*x)NodeSource(id, output_id)Einzel-Ausgang eines Layers NodeSource(id, port, output_id)ein bestimmter Output-Port eines Multi-Output-Layers id= Position im Builder, nicht ein Name: Alle Klassen tragen dieselben Attribute wie die Kanten-Tabelle, wobeiidimmer die Position imModelBuildermeint – die Reihenfolge deradd_node()-Aufrufe beiNodeSource/NodeTarget, die Reihenfolge deradd_input()-Aufrufe beiInputSourceund die Reihenfolge deradd_output()-Aufrufe beiOutputTarget(die Listenmb.inputs/mb.outputsdefinieren damit die Signatur vonforward(*x)).Target Bedeutung NodeTarget(id, input_id)Eingang eines Layers. input_id= zeitliche Re-Nutzung (RNN-artig:fc → act → fcruft dieselbe Schicht mehrfach auf) – nicht parallele EingängeNodeTarget(id, port, input_id)parallele Eingänge (z. B. Bilinear,Concat),port= welche EingangspositionOutputTarget(id)Modell-Ausgang add_input(name)/add_output(name)registrieren die Modell-Eingänge/-Ausgänge in ihrer Position –add_input("x")ist Argument 0 vonforward(*x),add_input("y")Argument 1; ein einziger Modell-Output wird als Tensor zurückgegeben, mehrere als sortiertes Tuple. -
Hyperparameter-System (
HyperParameter/) – Parameter werden deklarativ alsHyperparameter-Objekte durchgereicht statt als Konstanten:TunableHyperparameter– manuell gesetzter WertSearchableHyperparameter– Wert mitdtype,min,max,stepsizefür automatisierte SucheHyperParameters– Container; verbindet Parameter mit einer Suchstrategie, iteriert Populationen viastep()/push()HyperParameterSearch– Basisklasse der Suche (ask/tell-Interface)GridSearch– erste konkrete Implementierung
Damit kann man ein Modell bauen, dessen Architekturentscheidungen (Abmessungen, Bias an/aus, …) im Trainingslauf automatisch durchsucht werden.
Quickstart
import torch
from HyperParameter.hyperparameter import TunableHyperparameter
from Model.model_builder import (
ModelBuilder,
InputSource,
OutputTarget,
NodeTarget,
NodeSource
)
from Model.layer.linear import Linear
from Model.layer.activations import ReLU
hin = TunableHyperparameter("in", 5)
hout = TunableHyperparameter("out", 2)
bias = TunableHyperparameter("bias", True)
fc = Linear(hin=hin, hout=hout, hbias=bias)
rel = ReLU()
mb = ModelBuilder()
mb.add_node(fc)
mb.add_node(rel)
mb.add_input("input")
mb.add_output("output")
# Eingang(0) → fc → ReLU → Ausgang
mb.add_edge(InputSource(0), NodeTarget(0))
mb.add_edge(NodeSource(0), NodeTarget(1))
mb.add_edge(NodeSource(1), OutputTarget(0))
Model = mb.build()
print(Model(torch.tensor([1., 2., 3., 4., 5.])))
Ein vollständiges (größeres) Beispiel liegt in manual_test.py.
Quellen & Senken: die Source- und Target-Klassen im Detail
Jede Kante ist ein Tupel (source, target). Die Basisklassen Source und
Target tragen nur das gemeinsame Attribut id; die konkreten Typen
fassen die zusätzlichen Attribute zusammen:
| Klasse | Konstruktor | type-Attribut |
Zusätzliche Attribute |
|---|---|---|---|
InputSource |
(id) |
"InputSource" |
– |
NodeSource |
(id, output_id=0) |
"NodeSource" |
output_id |
NodeSource |
(id, port, output_id=0) |
"NodeSource" |
port, output_id |
NodeTarget |
(id, input_id=0) |
"NodeTarget" |
input_id |
NodeTarget |
(id, port, input_id=0) |
"NodeTarget" |
port, input_id |
OutputTarget |
(id) |
"OutputTarget" |
– |
Attribute: id, input_id, output_id, port
output_id/input_id(Aufruf-Runde): ein Layer kann im selbenforwardmehrmals mit geteilten Gewichten aufgerufen werden (Weight Sharing).input_idmarkiert, in welcher Runde der Layer ausgeführt wird; die Ausgabe dieses Aufrufs landet im internen Wertespeicher unter(layer_id, input_id)und wird ausschließlich vonNodeSource(layer_id, output_id=input_id)gelesen. Default0deckt den gewöhnlichen Ein-Aufruffall ab – ein Ziel-input_id, zu dem nie eine passendeNodeSourcegehört (bzw. umgekehrt), bleibt unproduziert und wird als Deadlock abgefangen.port(Multi-Output): welcher Ausgabewert eines Layers, wenn es ein Tuple/List zurückliefert (Split2→ Port 0 = erste Hälfte, Port 1 = zweite). Intern wird jeder Wert unter(layer_id, output_id, port)adressiert. Nur beiNodeSourcevorhanden.port(Multi-Input): welche Eingangsposition eines Layers, z. B.Bilinear(x1, x2)→ Port 0 =x1, Port 1 =x2. Nur beiNodeTargetvorhanden. Die Engine puffert die eingehenden Tensoren pro Port und ruft den Layer genau dann auf, wennrange( layer.num_inputs)vollständig besetzt ist – Port-Reihenfolge = Positionsargumente vonforward, bei gerichteten Operationen (Sub,Div,Atan2) also inhaltlich relevant.
Wichtig: NodeTarget und NodeTarget sind zwei
unterschiedliche Dinge mit überlappenden Namen. input_id an einer
NodeTarget bedeutet zeitliche Re-Nutzung (nacheinander, gleiche
Gewichte), port an einer NodeTarget bedeutet parallele
Eingänge (gleichzeitig, ein Aufruf). Nicht verwechseln.
Wie die Engine den Graphen ausführt
ModelBuilder.build() liefert ein nn.Module. forward arbeitet in zwei
Phasen:
- Seed-Phase: Alle
(InputSource → *)-Kanten werden sofort verarbeitet – die Argumente vonforward(*x)fließen in die Graphen ein. - Fixpunkt-Phase: In jeder Runde werden alle
(Layer-Source → *)-Kanten geprüft, deren Quelle einen Wert im internen Speicher hat; deren Target wird daraufhin ausgeführt. Die Runde wiederholt sich, bis alleOutputTargets besetzt sind. Bleibt nach einer ganzen Runde nichts Neues passieren → Deadlock (ML-200203): ein Ziel wartet auf einen Wert, der nie produziert wird.
Daraus folgt: Die Reihenfolge der add_edge()-Aufrufe ist
irrelevant, die Engine leitet die Execution-Order allein aus den
Abhängigkeiten ab. Umgekehrt: jede Quelle muss einen Wert liefern können
(dessen Target ausgeführt wird) und jede produzierte Ausgabe muss gelesen
werden (via Layer*OutputSource oder OutputTarget) – sonst bleibt
etwas unproduziert bzw. unverbraucht und wird als Deadlock abgefangen.
Weight Sharing: dieselbe Schicht, mehrere Aufrufe
Weight Sharing entsteht, wenn dieselbe Layer-Instanz in mehreren
input_id-Runden aufgerufen wird. Jede Runde hat eine eigene
input_id; die Ausgabe dieser Runde wird durch die output_id der
NodeSource selektiert. Die Gewichte werden pro Vorwärts-Pass
nicht dupliziert; PyTorch akkumuliert die Gradienten aller Aufrufe über
dieselben Parameter – Parameter-Sparnis gegenüber zwei separaten
Schichten, mit dem Preis, dass beide Stufen zwingend dieselbe
Transformation lernen.
# fc → ReLU → fc, wobei fc beide Male dieselbe Instanz ist (geteilte Gewichte)
fc = Linear(hin=th("in", 8), hout=th("out", 8), hbias=th("bias", True))
act = ReLU()
mb = ModelBuilder()
mb.add_node(fc) # 0 – wird 2× aufgerufen: input_id 0 und 1
mb.add_node(act) # 1
mb.add_input("in")
mb.add_output("out")
# Runde 0: Input → fc(input_id=0) → ReLU
mb.add_edge(InputSource(0), NodeTarget(0, input_id=0))
mb.add_edge(NodeSource(0), NodeTarget(1)) # Ausgabe von fc(0)
# Runde 1: ReLU → fc(input_id=1) → Output
mb.add_edge(NodeSource(1), NodeTarget(0, input_id=1))
mb.add_edge(NodeSource(0, output_id=1), OutputTarget(0)) # Ausgabe von fc(1)
model = mb.build()
# Referenz: fc(ReLU(fc(x))) mit derselben Instanz – dieselben Gewichte, 2× durchlaufen
x = torch.randn(1, 8)
assert torch.allclose(model(x), model.layers[0](torch.relu(model.layers[0](x))), atol=1e-5)
Beachte die Paare: NodeTarget(id, input_id) sagt, in welcher Runde
die Schicht ausgeführt wird; NodeSource(id, output_id) sagt, welche
der Runden ausgelesen wird. Im obigen Beispiel: output_id=1 an
Layer 0 liefert das Ergebnis der zweiten Runde – das Modell-Output.
Ein Layer mit mehreren Eingängen (Multi-Input)
Adapter für parallele Eingänge (Bilinear, Add, Concat, MatMul, …)
declarieren ihre Eingangsanzahl im Konstruktor via num_inputs (Standard 1):
super().__init__(name, num_inputs=2). Der ModelBuilder liest num_inputs
ausschließlich vom Adapter (nicht vom generierten nn.Module) – generierte
Module müssen das Attribut nicht mitführen, nackte PyTorch-Instanzen als
generate_layer()-Ergebnis sind damit erlaubt. Jeder Graphen-Eingang geht an
einen eigenen NodeTarget-Port:
# x1 = fc_a(x), x2 = fc_b(x), out = x1 + x2 (Add hat num_inputs=2)
fc_a = Linear(hin=th("a", 8), hout=th("b", 4), hbias=th("bias", False))
fc_b = Linear(hin=th("c", 8), hout=th("d", 4), hbias=th("bias", False))
add = Add() # Tensor-Op: 2 Eingänge
mb = ModelBuilder()
mb.add_node(fc_a) # 0
mb.add_node(fc_b) # 1
mb.add_node(add) # 2
mb.add_input("x")
mb.add_output("y")
mb.add_edge(InputSource(0), NodeTarget(0)) # x → fc_a
mb.add_edge(InputSource(0), NodeTarget(1)) # x → fc_b
mb.add_edge(NodeSource(0), NodeTarget(2, port=0)) # fc_a → Port 0
mb.add_edge(NodeSource(1), NodeTarget(2, port=1)) # fc_b → Port 1
mb.add_edge(NodeSource(2), OutputTarget(0))
model = mb.build()
port ist die Position im Aufruf forward(*tensors) – bei symmetrischen
Operationen (Add, Mul) spielt das keine Rolle, bei gerichteten
(Sub(a, b), Div(a, b), Atan2(a, b)) bestimmt es das Ergebnis.
Ein Layer mit mehreren Ausgängen (Multi-Output)
Adapter, deren generate_layer() ein Tuple/List zurückliefert (Split2,
SplitN, TopK, ArgMax → Wert+Index, …), haben ebenso viele Output-Ports
wie Tuple-Elemente. Pro Port wird eine eigene NodeSource
angelegt und kann an beliebige Targets weiterverdrahtet werden – auch an
unterschiedliche NodeTarget-Ports, wie im Split→Concat-Roundtrip
(tests/test_multi_output.py):
# Split2(x) → [x_h1, x_h2] → fc_a(x_h1), fc_b(x_h2) → (y_a, y_b)
split = Split2(hdim=th("d", 1))
fc_a = Linear(hin=th("in", 4), hout=th("out", 2), hbias=th("bias", True))
fc_b = Linear(hin=th("in", 4), hout=th("out", 3), hbias=th("bias", True))
mb = ModelBuilder()
mb.add_node(split) # 0
mb.add_node(fc_a) # 1
mb.add_node(fc_b) # 2
mb.add_input("x")
mb.add_output("y_a")
mb.add_output("y_b")
mb.add_edge(InputSource(0), NodeTarget(0))
mb.add_edge(NodeSource(0, port=0), NodeTarget(1)) # erste Hälfte → fc_a
mb.add_edge(NodeSource(0, port=1), NodeTarget(2)) # zweite Hälfte → fc_b
mb.add_edge(NodeSource(1), OutputTarget(0))
mb.add_edge(NodeSource(2), OutputTarget(1))
model = mb.build()
x = torch.randn(1, 8)
y_a, y_b = model(x) # → Tuple, weil 2× add_output
Kurzübersicht
| Szenario | Was zu tun ist |
|---|---|
| 1 Ein → 1 Aus | NodeSource(id) → NodeTarget(id) (Defaults reichen) |
| Gewichtete Schicht, 2× aufgerufen | input_id=0 und input_id=1 am Target; output_id an der Source selektieren |
| Parallel-N-Eingänge | pro Kanten eine NodeTarget(id, port=k); Adapter deklariert num_inputs=N |
| Ein Layer → M-Outputs | pro Output NodeSource(id, port=k) |
| Modell-Input → Layer | InputSource(pos) → beliebiges Layer-Target; pos = Position in forward(*x) |
| Layer → Modell-Output | Layer*OutputSource(...) → OutputTarget(pos); pos = Position im Rückgabe-Tuple |
Installation & Tests
# Dependencies: Python ≥ 3.10
pip install -r requirements.txt # Laufzeit + Tests
# pip install -r requirements-core.txt # nur Laufzeit (ohne pytest)
# Test-Suite (~1420 Tests)
pytest
Beachte: Die Tests müssen aus dem Projekt-Root heraus laufen (conftest.py
legt das Projekt-Verzeichnis auf dem sys.path an).
Projektstruktur
├── Model/
│ ├── model_builder.py # Graph-Modell: Sources, Targets, Build & Execution
│ ├── model.py # (Legacy-Platzhalter)
│ ├── tests.py
│ └── layer/ # 174 Layer-Adapter, nach Funktion sortiert
├── HyperParameter/
│ ├── hyperparameter.py # Hyperparameter, Searchable*, Tunable*
│ ├── hyperparameters.py # Container + Search-Kopplung
│ ├── hyperparametersearch.py
│ └── searches/gridsearch.py
├── tests/ # pytest-Suite
├── translations/
│ ├── en.json # Referenzsprache (174 Einträge)
│ ├── de.json # Deutsch (174 Einträge)
│ ├── sv.json # Schwedisch (174 Einträge)
│ ├── ar.json # Arabisch (174 Einträge)
│ └── generate_lang.py # Übersetzungs-Generator (OpenAI-kompatible API)
├── requirements.txt # Python-Abhängigkeiten (Laufzeit + Tests)
├── requirements-core.txt # nur Laufzeit-Abhängigkeiten
├── conftest.py # path-setup + Fixtures
├── manual_test.py # durchlauffähiges Beispiel
└── Errorcodes.md # Fehlercode-Referenz (ML-XXXXXX)
Layer-Infos & GUI-Kategorien
Alle mehrsprachigen Texte liegen zentral in translations/<lang>.json.
Jede Adapter-Klasse trägt nur noch eine info_id (Key in der JSON-Datei).
get_info(lang) (geerbt aus Layer) holt den Eintrag und rendert ihn.
Neue Sprache = neue JSON-Datei anlegen, keine Python-Datei anfassen:
class ReLU(Layer):
info_id = "relu" # → Key in translations/de.json / sv.json / ar.json / …
from Model.layer.layer import available_languages
print(available_languages()) # {'en', 'de', 'sv', 'ar'} – ordnerdynamisch
layer.get_info("de")
# → {"Description": str,
# "Example use case": str,
# "Category": str,
# "Mode": str, # "Einfach" | "Vorgeschritten" (Simple | Advanced)
# "Params": {…}}
Ordnerdynamisch:
get_info(lang)erkennt alletranslations/*.jsonautomatisch – neue Sprachdatei = neue verfügbare Sprache, ohne Python-Code.available_languages()liefert die Liste für GUI/CLI.Neue Übersetzung erzeugen:
# Env-Vars setzen (Werte hier bewusst weggelassen – siehe .env oder Shell-Profil): # TRANSLATION_API_BASE – z. B. http://<host>:<port>/v1 # TRANSLATION_API_KEY – Bearer-Key # TRANSLATION_MODEL – Modell-Name python3 translations/generate_lang.py de # oder: sv, ar, fr, …
JSON-Struktur pro Eintrag (<info_id> als Key):
{
"conv2d": {
"description": "2D-Faltung: …",
"use_case": "Typisch für die ersten Schichten eines Bilderkenners (CNN).",
"category": "Faltung",
"mode": "Einfach",
"params": {
"hinc": "Anzahl der Eingabekanäle (z. B. 3 für RGB).",
"hpadmode": {
"description": "Polsterungsmodus.",
"options": ["zeros", "reflect", "replicate", "circular"]
}
}
}
}
"params"-Werte sind normalerweise einfache Strings.
Ein Wert ist ein Objekt mit "description" + "options", wenn der
Hyperparameter einen Integer-Index auf eine feste String-Liste abbildet
(z. B. Polsterungsmodus). Die Liste steht in Index-Reihenfolge –
get_info() rendert sie automatisch als "0" → "zeros", "1" → "reflect" …
so dass die GUI ein Drop-Down daraus bauen kann.
Kategorie-Vokabular (stabil, für die GUI-Sortierung)
Jeder Adapter gehört zu genau einer Kategorie. Die Werte sind ein fixes Vokabular (DE/EN-Paare), damit die GUI sie sortieren/gruppieren kann:
de |
en |
Beispiele |
|---|---|---|
Aktivierung |
Activation |
ReLU, Sigmoid, GELU |
Linear |
Linear |
Linear, Bilinear |
Faltung |
Convolution |
Conv1D/2D/3D, ConvTranspose* |
Pooling |
Pooling |
MaxPool*, AvgPool*, Adaptive*, Global* |
Normalisierung |
Normalization |
BatchNorm*, LayerNorm, GroupNorm, InstanceNorm* |
Rekurrent |
Recurrent |
RNN, LSTM, GRU (inkl. Cells) |
Attention |
Attention |
MultiHeadAttention, Embedding |
Dropout |
Dropout |
Dropout*, AlphaDropout, FeatureAlphaDropout |
Tensor-Op |
Tensor Op |
Reshape, Permute, Squeeze, CumSum, ArgMax … |
Matrix-Op |
Matrix Op |
MatMul, BatchMatMul, Det, Inverse, Diag … |
Verlust |
Loss |
MSELoss, CrossEntropyLoss, FocalLoss … |
Up-/Downsample |
Resampling |
Upsample*, PixelShuffle, Padding* |
Sonstiges |
Misc |
OneHot, Identity, Cast, Fold/Unfold |
Regel: Neue Adapter bekommen nur eine der obigen Kategorien. Eine neue Kategorie nur, wenn wirklich nichts passt – und dann hier dokumentieren.
Status
- ✅ Deklarative Graph-Topologie mit Multi-Input/Multi-Output und zeitlicher Re-Nutzung
- ✅ 174 PyTorch-Layer als Adapter portiert und getestet
- ✅ Mehrsprachige Layer-Infos: de, sv, ar (je 174 Einträge, ordnerdynamisch erkannt)
- ✅ Hyperparameter-System inkl. GridSearch
- ⬜ Automatisierte Suche: Feedback-Schleife (
tell) noch nicht implementiert - ⬜ Training-Loop / Optimizer-Integration noch offen
→ Weitere Ideen und offene Punkte: ideas.md