diff --git a/TEST-README.md b/TEST-README.md new file mode 100644 index 0000000..05e9ade --- /dev/null +++ b/TEST-README.md @@ -0,0 +1,196 @@ +# i18n Test Suite Documentation + +This document describes the comprehensive test suite for the internationalization (i18n) functionality. + +## Quick Start + +**Run all tests with a single command:** +```bash +node test.js +``` + +This will run the comprehensive i18n tests, providing a unified summary. + +## Test Files + +### 0. `test.js` ⭐ **RECOMMENDED** +A unified test runner that executes all Node.js-based tests and provides a comprehensive summary. + +**Usage:** +```bash +# Run all tests +node test.js + +# Show help +node test.js --help +``` + +**Features:** +- Runs the comprehensive i18n test suite +- Provides colored output and clear summaries +- Shows overall pass/fail status +- Reminds about browser tests + +### 1. `test-i18n.js` +A Node.js-based test suite that performs static analysis and validation of the i18n implementation. This test can be run from the command line and doesn't require a browser. + +**Usage:** +```bash +node test-i18n.js +``` + +**What it tests:** +- Translation file structure and completeness +- i18n.js module code analysis +- HTML integration with data-i18n attributes +- JavaScript integration with translation functions +- Translation key coverage +- Code quality and best practices +- Edge cases and error handling +- Accessibility and internationalization + +### 2. `test-i18n-browser.html` +A browser-based test suite that tests the runtime behavior of the i18n system. This test requires opening the HTML file in a web browser. + +**Usage:** +1. Open `test-i18n-browser.html` in a web browser +2. Tests will run automatically on page load +3. Use the buttons to run specific test suites + +**What it tests:** +- Basic i18n module functionality +- Language switching behavior +- DOM element translation +- Error handling +- LocalStorage persistence +- Translation completeness + +### 3. `test-i18n.js` (also run via `test.js`) +The comprehensive test file that verifies the i18n implementation. This test performs static analysis and checks for hardcoded text, translation coverage, and i18n integration. + +**Usage:** +```bash +# Run directly +node test-i18n.js + +# Or run via the unified test runner +node test.js +``` + +## Test Coverage + +The comprehensive test suite includes **65 tests** across 8 test suites: + +### Test Suite 1: Translation File Structure and Completeness (7 tests) +- Validates JSON structure +- Ensures both languages have matching keys +- Verifies all values are strings +- Checks parameter placeholder consistency + +### Test Suite 2: i18n.js Module Code Analysis (18 tests) +- Verifies all exported functions exist +- Checks for proper error handling +- Validates localStorage usage +- Ensures event dispatching + +### Test Suite 3: HTML Integration (14 tests) +- Verifies data-i18n attributes are used +- Checks language toggle button +- Validates all translation keys exist +- Ensures proper HTML structure + +### Test Suite 4: JavaScript Integration (9 tests) +- Verifies t() function usage +- Checks translation key references +- Validates parameter substitution +- Ensures language change listeners + +### Test Suite 5: Translation Key Coverage (3 tests) +- Verifies error keys are used +- Checks UI key references +- Validates parameter placeholders + +### Test Suite 6: Code Quality and Best Practices (5 tests) +- Checks for hardcoded German text +- Validates translation file formatting +- Ensures no empty strings +- Verifies naming conventions + +### Test Suite 7: Edge Cases and Error Handling (5 tests) +- Tests missing key handling +- Validates fallback behavior +- Checks special character handling +- Tests parameter substitution edge cases + +### Test Suite 8: Accessibility and Internationalization (4 tests) +- Verifies HTML lang attribute updates +- Checks translatable aria-labels +- Validates accessibility attributes +- Ensures proper ARIA roles + +## Running All Tests + +**Recommended: Use the unified test runner:** +```bash +node test.js +``` + +**Or run tests individually:** +```bash +# Run comprehensive Node.js tests (via unified runner) +node test.js + +# Run comprehensive tests directly +node test-i18n.js + +# Open browser tests (requires a web server or file:// protocol) +open test-i18n-browser.html +``` + +## Test Results + +All tests should pass. The comprehensive test suite reports: +- **65 tests passed, 0 failed** (as of last run) + +## Adding New Tests + +When adding new translation keys or i18n features: + +1. **Add translation keys to both `en.json` and `de.json`** +2. **Update HTML with `data-i18n` attributes if needed** +3. **Use `t()` function in JavaScript for dynamic translations** +4. **Run the test suite to verify everything works** + +## Troubleshooting + +### Test fails: "Translation key not found" +- Ensure the key exists in both `en.json` and `de.json` +- Check that the key path uses dot notation (e.g., `"drawing.title"`) + +### Test fails: "Hardcoded German text found" +- Remove any hardcoded German strings from HTML or JavaScript +- Use translation keys instead + +### Browser tests don't run +- Ensure `assets/i18n.js` is accessible +- Check browser console for errors +- Verify translation JSON files are in `assets/i18n/` + +## Continuous Integration + +These tests can be integrated into CI/CD pipelines: + +```yaml +# Example GitHub Actions workflow +- name: Run i18n tests + run: | + node test.js +``` + +## Notes + +- The comprehensive test suite performs static analysis and doesn't require a browser +- The browser test suite tests actual runtime behavior +- Both test suites complement each other for complete coverage +- Tests are designed to catch regressions when modifying i18n code + diff --git a/assets/i18n.js b/assets/i18n.js new file mode 100644 index 0000000..2dfcfbe --- /dev/null +++ b/assets/i18n.js @@ -0,0 +1,174 @@ +/** + * Internationalization (i18n) module + * Handles language switching and translation + */ + +let currentLanguage = 'en'; +let translations = {}; + +/** + * Initialize i18n system + * @param {string} lang - Language code ('en' or 'de') + */ +async function initI18n(lang = 'en') { + currentLanguage = lang || 'en'; + + try { + const response = await fetch(`./assets/i18n/${currentLanguage}.json`); + if (!response.ok) { + throw new Error(`Failed to load translations for ${currentLanguage}`); + } + translations = await response.json(); + + // Update HTML lang attribute + document.documentElement.lang = currentLanguage; + + // Update page title + document.title = translations.pageTitle || 'MNIST MLP – Inference Visualization'; + + // Apply translations to all elements with data-i18n attribute + applyTranslations(); + + // Save language preference + localStorage.setItem('preferredLanguage', currentLanguage); + + return translations; + } catch (error) { + console.error('Error loading translations:', error); + // Fallback to English if translation file fails to load + if (currentLanguage !== 'en') { + return initI18n('en'); + } + throw error; + } +} + +/** + * Get translation for a key + * @param {string} key - Translation key (supports dot notation, e.g., "drawing.title") + * @param {object} params - Parameters to replace in translation (e.g., {digit: 5}) + * @returns {string} Translated string + */ +function t(key, params = {}) { + const keys = key.split('.'); + let value = translations; + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + console.warn(`Translation key not found: ${key}`); + return key; + } + } + + if (typeof value !== 'string') { + console.warn(`Translation value is not a string for key: ${key}`); + return key; + } + + // Replace parameters in the format {param} + if (params && Object.keys(params).length > 0) { + return value.replace(/\{(\w+)\}/g, (match, paramKey) => { + return params[paramKey] !== undefined ? String(params[paramKey]) : match; + }); + } + + return value; +} + +/** + * Apply translations to all elements with data-i18n attribute + */ +function applyTranslations() { + const elements = document.querySelectorAll('[data-i18n]'); + elements.forEach(element => { + const key = element.getAttribute('data-i18n'); + const translation = t(key); + + // Handle different element types + if (element.tagName === 'INPUT' && element.type === 'text') { + element.value = translation; + } else if (element.hasAttribute('aria-label')) { + element.setAttribute('aria-label', translation); + } else if (element.hasAttribute('placeholder')) { + element.placeholder = translation; + } else if (element.hasAttribute('title')) { + element.title = translation; + } else { + element.textContent = translation; + } + }); + + // Handle elements with data-i18n-html for HTML content + const htmlElements = document.querySelectorAll('[data-i18n-html]'); + htmlElements.forEach(element => { + const key = element.getAttribute('data-i18n-html'); + const translation = t(key); + element.innerHTML = translation; + }); + + // Handle elements with data-i18n-aria-label for aria-label attributes + const ariaLabelElements = document.querySelectorAll('[data-i18n-aria-label]'); + ariaLabelElements.forEach(element => { + const key = element.getAttribute('data-i18n-aria-label'); + const translation = t(key); + element.setAttribute('aria-label', translation); + }); + + // Update language toggle button text + const languageToggleText = document.getElementById('languageToggleText'); + if (languageToggleText) { + languageToggleText.textContent = currentLanguage.toUpperCase(); + } +} + +/** + * Switch language + * @param {string} lang - Language code ('en' or 'de') + */ +async function switchLanguage(lang) { + if (lang === currentLanguage) return; + await initI18n(lang); + + // Trigger custom event for components that need to update + window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } })); +} + +/** + * Get current language + * @returns {string} Current language code + */ +function getCurrentLanguage() { + return currentLanguage; +} + +// Load saved language preference on initialization +document.addEventListener('DOMContentLoaded', () => { + const savedLanguage = localStorage.getItem('preferredLanguage') || 'en'; + initI18n(savedLanguage).catch(error => { + console.error('Failed to initialize i18n:', error); + }); + + // Setup language toggle button + const languageToggleButton = document.getElementById('languageToggleButton'); + if (languageToggleButton) { + languageToggleButton.addEventListener('click', () => { + const currentLang = getCurrentLanguage(); + const newLang = currentLang === 'en' ? 'de' : 'en'; + switchLanguage(newLang); + }); + } +}); + +// Export for use in other modules +if (typeof window !== 'undefined') { + window.i18n = { + init: initI18n, + t, + switchLanguage, + getCurrentLanguage, + applyTranslations + }; +} + diff --git a/assets/i18n/de.json b/assets/i18n/de.json new file mode 100644 index 0000000..e760c3f --- /dev/null +++ b/assets/i18n/de.json @@ -0,0 +1,128 @@ +{ + "pageTitle": "MNIST MLP – Visualisierung der Inferenz", + "drawing": { + "title": "Ziffer zeichnen", + "instructions": "Auf dem Raster klicken und ziehen (Rechtsklick zum Löschen)", + "mobileInstructions": "Tippen und auf dem Raster ziehen" + }, + "controls3d": { + "title": "3D-Steuerung:", + "instructions": "• Linke Taste + ziehen = drehen • Rechte Taste + ziehen = verschieben • Scrollrad = zoomen", + "rotate": "Drehen", + "pan": "Verschieben", + "zoom": "Zoomen", + "rotateDesktop": "Linke Maustaste gedrückt halten und ziehen", + "panDesktop": "Rechte Maustaste gedrückt halten und ziehen", + "zoomDesktop": "Mausrad benutzen", + "rotateMobile": "Mit einem Finger ziehen", + "panMobile": "Mit zwei Fingern bewegen", + "zoomMobile": "Finger zusammenziehen oder spreizen" + }, + "mobile": { + "touchControls": "Touch-Steuerung" + }, + "predictions": { + "title": "Wahrscheinlichkeiten der Ziffern" + }, + "network": { + "overview": "Netzwerk-Übersicht", + "noData": "Keine Netzwerkdaten verfügbar.", + "totalParameters": "Gesamtparameter", + "inputNodes": "Eingabeknoten", + "outputClasses": "Ausgabeklassen", + "layers": "Ebenen (inkl. Ausgabe)", + "weights": "Gewichte", + "bias": "Bias", + "total": "Gesamt" + }, + "neuronDetail": { + "clearSelection": "Auswahl löschen", + "incomingContributions": "Eingehende Beiträge", + "outgoingContributions": "Ausgehende Beiträge", + "source": "Quelle", + "target": "Ziel", + "input": "Eingabe", + "weight": "Gewicht", + "product": "Produkt", + "activation": "Aktivierung (Ziel)", + "contribution": "Beitrag", + "noIncomingConnections": "Keine eingehenden Verbindungen für diese Ebene.", + "inputLayerSize": "Eingabeebenen-Größe:", + "outputLayerSize": "Ausgabeebenen-Größe:" + }, + "timeline": { + "title": "Trainingsfortschritt", + "ariaLabel": "Trainingsfortschritt" + }, + "infoModal": { + "title": "MNIST-Ziffernklassifizierung – Visualisierung der Inferenz", + "subtitle": "Interaktive Visualisierung eines neuronalen Netzes", + "description": "Diese Anwendung zeigt ein kompaktes Multi-Layer-Perceptron (MLP), das auf MNIST trainiert wurde. Zeichnen Sie eine Ziffer und beobachten Sie, wie die Aktivierungen in Echtzeit durch alle vollvernetzten Schichten propagieren.", + "howItWorks": "So funktioniert es:", + "howItWorksDrawing": "Im 2D-Raster (links oben) klicken und ziehen, um eine Ziffer zu skizzieren", + "howItWorksObserving": "Verfolgen Sie, wie Ihre Skizze durch die Schichten des Netzes im 3D-Raum wandert", + "howItWorksPrediction": "Prüfen Sie die Wahrscheinlichkeit für jede Ziffer (0–9) im Diagramm (rechts oben)", + "networkArchitecture": "Netzarchitektur (Standardexport):", + "inputLayer": "Eingabeschicht:", + "inputLayerDesc": "28×28 Pixelraster (Ihre Zeichnung)", + "denseLayer": "Dichte Ebene", + "denseLayerDesc": "Neuronen mit ReLU", + "outputLayer": "Ausgabeschicht:", + "outputLayerDesc": "32 → 10 Logits → Softmax-Wahrscheinlichkeiten", + "colorCoding": "Farbcodierung:", + "colorCodingNodes": "Die Farbe zeigt die Aktivierungsstärke (dunkle Blautöne für niedrige/negative Werte, kräftige Korallenfarben für starke positive Aktivierungen)", + "colorCodingConnections": "Warme Farben kennzeichnen starke positive Beiträge, kühle Töne negative Einflüsse, gedämpfte Linien nahezu Null.", + "training": "Eigenes Modell trainieren:", + "trainingStep1": "python training/mlp_train.py starten, um das MLP zu trainieren (inkl. Apple-Metal-Beschleunigung, falls verfügbar).", + "trainingStep2": "Das Skript schreibt exports/mlp_weights.json, das der Visualisierer beim Laden einliest.", + "trainingStep3": "Anzahl versteckter Neuronen, Epochen oder Exportpfade über die in training/mlp_train.py dokumentierten CLI-Optionen verändern.", + "realtimeFeatures": "Echtzeitfunktionen:", + "realtimeActivations": "Schichtaktivierungen: Instanzen von Kugeln zeigen Aktivierungen pro Neuron mit farbcodierter Stärke.", + "realtimeConnections": "Wichtige Verbindungen: Jedes Zielneuron hebt seine stärksten Eingangsgewichte für bessere Lesbarkeit hervor.", + "realtimeProbabilities": "Live-Wahrscheinlichkeiten: Das Balkendiagramm aktualisiert die Logits → Softmax-Werte in Echtzeit.", + "note": "Das Netzwerk ist bewusst kompakt gehalten, um eine flüssige Echtzeitdarstellung zu gewährleisten. Sie können gern mit anderen Schichtgrößen neu trainieren – halten Sie die Architektur nur schlank, damit die 3D-Ansicht reaktionsschnell bleibt." + }, + "advancedSettings": { + "title": "Erweiterte Einstellungen", + "maxConnections": "Maximale Verbindungen pro Neuron", + "maxConnectionsHint": "Steuert, wie viele der stärksten eingehenden Gewichte pro Zielneuron gleichzeitig angezeigt werden; hohe Werte können die Darstellung verlangsamen.", + "hideWeakConnections": "Schwache Verbindungen ausblenden", + "hideWeakConnectionsHint": "Blendet Verbindungen mit niedrigem absoluten Gewicht aus; 0 zeigt alle Verbindungen.", + "connectionThickness": "Verbindungsstärke", + "connectionThicknessHint": "Passt den Radius der Verbindungszylinder an; höhere Werte machen alle Linien sichtbar dicker.", + "brushThickness": "Pinselstärke", + "brushThicknessHint": "Kleinere Werte erzeugen feinere Linien; größere Werte füllen das Raster schneller.", + "brushStrength": "Pinselstärke", + "brushStrengthHint": "Bestimmt, wie stark ein Pinselstrich die Pixelhelligkeit erhöht." + }, + "errors": { + "initializationFailed": "Visualisierung konnte nicht initialisiert werden. Details in der Konsole.", + "loadMnistManifest": "MNIST-Manifest konnte nicht geladen werden", + "invalidManifest": "Manifest enthält keine gültigen Dateipfade für Bilder oder Beschriftungen.", + "loadMnistImages": "MNIST-Bilddaten konnten nicht geladen werden", + "loadMnistLabels": "MNIST-Beschriftungsdaten konnten nicht geladen werden", + "inferSampleSize": "Probengröße konnte nicht aus MNIST-Bilddaten abgeleitet werden.", + "labelMismatch": "Anzahl der Beschriftungen stimmt nicht mit abgeleiteten Proben überein.", + "invalidManifestSize": "Manifest enthält keine gültige Probengröße.", + "imageDataMismatch": "Länge der MNIST-Bilddaten stimmt nicht mit erwarteter Größe überein.", + "labelDataMismatch": "Länge der MNIST-Beschriftungsdaten stimmt nicht mit erwarteter Größe überein.", + "invalidNetworkDefinition": "Ungültige Netzwerkdefinition.", + "noTimelineSnapshots": "Keine gültigen Timeline-Snapshots gefunden.", + "loadNetworkWeights": "Netzwerkgewichte konnten nicht geladen werden", + "base64Unavailable": "Base64-Decodierung ist in dieser Umgebung nicht verfügbar.", + "invalidFloat16Length": "Float16-Daten haben ungültige Länge.", + "invalidFloat16Count": "Erwartet {expected} Float16-Werte, aber {actual} erhalten.", + "loadSnapshot": "Snapshot konnte nicht geladen werden", + "invalidSnapshot": "Snapshot-Datei enthält keine gültigen Ebenendaten", + "gridContainerNotFound": "Raster-Container nicht gefunden", + "invalidPixelValues": "Ungültige Pixelwerte für Zeichenfläche" + }, + "aria": { + "loadRandom": "Zufällige {digit} laden" + }, + "fps": { + "idle": "inaktiv", + "fps": "fps" + } +} + diff --git a/assets/i18n/en.json b/assets/i18n/en.json new file mode 100644 index 0000000..e9685dd --- /dev/null +++ b/assets/i18n/en.json @@ -0,0 +1,128 @@ +{ + "pageTitle": "MNIST MLP – Inference Visualization", + "drawing": { + "title": "Draw digit", + "instructions": "Click and drag on the grid (right-click to erase)", + "mobileInstructions": "Tap and drag on the grid" + }, + "controls3d": { + "title": "3D Controls:", + "instructions": "• Left button + drag = rotate • Right button + drag = pan • Scroll wheel = zoom", + "rotate": "Rotate", + "pan": "Pan", + "zoom": "Zoom", + "rotateDesktop": "Hold left mouse button and drag", + "panDesktop": "Hold right mouse button and drag", + "zoomDesktop": "Use mouse wheel", + "rotateMobile": "Drag with one finger", + "panMobile": "Move with two fingers", + "zoomMobile": "Pinch or spread fingers" + }, + "mobile": { + "touchControls": "Touch Controls" + }, + "predictions": { + "title": "Digit Probabilities" + }, + "network": { + "overview": "Network Overview", + "noData": "No network data available.", + "totalParameters": "Total Parameters", + "inputNodes": "Input Nodes", + "outputClasses": "Output Classes", + "layers": "Layers (incl. Output)", + "weights": "Weights", + "bias": "Bias", + "total": "Total" + }, + "neuronDetail": { + "clearSelection": "Clear selection", + "incomingContributions": "Incoming Contributions", + "outgoingContributions": "Outgoing Contributions", + "source": "Source", + "target": "Target", + "input": "Input", + "weight": "Weight", + "product": "Product", + "activation": "Activation (Target)", + "contribution": "Contribution", + "noIncomingConnections": "No incoming connections for this layer.", + "inputLayerSize": "Input Layer Size:", + "outputLayerSize": "Output Layer Size:" + }, + "timeline": { + "title": "Training Progress", + "ariaLabel": "Training Progress" + }, + "infoModal": { + "title": "MNIST Digit Classification – Inference Visualization", + "subtitle": "Interactive Neural Network Visualization", + "description": "This application shows a compact Multi-Layer Perceptron (MLP) trained on MNIST. Draw a digit and observe how activations propagate in real-time through all fully connected layers.", + "howItWorks": "How it works:", + "howItWorksDrawing": "Click and drag in the 2D grid (top left) to sketch a digit", + "howItWorksObserving": "Track how your sketch travels through the network layers in 3D space", + "howItWorksPrediction": "Check the probability for each digit (0–9) in the chart (top right)", + "networkArchitecture": "Network Architecture (Default Export):", + "inputLayer": "Input Layer:", + "inputLayerDesc": "28×28 pixel grid (your drawing)", + "denseLayer": "Dense Layer", + "denseLayerDesc": "neurons with ReLU", + "outputLayer": "Output Layer:", + "outputLayerDesc": "32 → 10 logits → Softmax probabilities", + "colorCoding": "Color Coding:", + "colorCodingNodes": "The color shows activation strength (dark blue tones for low/negative values, vibrant coral colors for strong positive activations)", + "colorCodingConnections": "Warm colors indicate strong positive contributions, cool tones negative influences, muted lines near zero.", + "training": "Training Your Own Model:", + "trainingStep1": "Run python training/mlp_train.py to train the MLP (including Apple Metal acceleration, if available).", + "trainingStep2": "The script writes exports/mlp_weights.json, which the visualizer reads on load.", + "trainingStep3": "Change the number of hidden neurons, epochs, or export paths via the CLI options documented in training/mlp_train.py.", + "realtimeFeatures": "Real-time Features:", + "realtimeActivations": "Layer Activations: Sphere instances show activations per neuron with color-coded strength.", + "realtimeConnections": "Important Connections: Each target neuron highlights its strongest input weights for better readability.", + "realtimeProbabilities": "Live Probabilities: The bar chart updates logits → Softmax values in real-time.", + "note": "The network is intentionally kept compact to ensure smooth real-time rendering. Feel free to retrain with different layer sizes – just keep the architecture lean so the 3D view remains responsive." + }, + "advancedSettings": { + "title": "Advanced Settings", + "maxConnections": "Maximum Connections per Neuron", + "maxConnectionsHint": "Controls how many of the strongest incoming weights per target neuron are displayed simultaneously; high values may slow down rendering.", + "hideWeakConnections": "Hide Weak Connections", + "hideWeakConnectionsHint": "Hides connections with low absolute weight; 0 shows all connections.", + "connectionThickness": "Connection Thickness", + "connectionThicknessHint": "Adjusts the radius of connection cylinders; higher values make all lines visibly thicker.", + "brushThickness": "Brush Thickness", + "brushThicknessHint": "Smaller values create finer lines; larger values fill the grid faster.", + "brushStrength": "Brush Strength", + "brushStrengthHint": "Determines how much a brush stroke increases pixel brightness." + }, + "errors": { + "initializationFailed": "Visualization could not be initialized. See console for details.", + "loadMnistManifest": "Could not load MNIST manifest", + "invalidManifest": "Manifest does not contain valid file paths for images or labels.", + "loadMnistImages": "Could not load MNIST image data", + "loadMnistLabels": "Could not load MNIST label data", + "inferSampleSize": "Could not infer sample size from MNIST image data.", + "labelMismatch": "Number of labels does not match inferred samples.", + "invalidManifestSize": "Manifest does not contain a valid sample size.", + "imageDataMismatch": "MNIST image data length does not match expected size.", + "labelDataMismatch": "MNIST label data length does not match expected size.", + "invalidNetworkDefinition": "Invalid network definition.", + "noTimelineSnapshots": "No valid timeline snapshots found.", + "loadNetworkWeights": "Network weights could not be loaded", + "base64Unavailable": "Base64 decoding is not available in this environment.", + "invalidFloat16Length": "Float16 data has invalid length.", + "invalidFloat16Count": "Expected {expected} Float16 values, but got {actual}.", + "loadSnapshot": "Snapshot could not be loaded", + "invalidSnapshot": "Snapshot file does not contain valid layer data", + "gridContainerNotFound": "Grid container not found", + "invalidPixelValues": "Invalid pixel values for drawing pad" + }, + "aria": { + "loadRandom": "Load random {digit}" + }, + "fps": { + "idle": "idle", + "fps": "fps" + } +} + diff --git a/assets/main.css b/assets/main.css index 8dc482b..f5a3781 100644 --- a/assets/main.css +++ b/assets/main.css @@ -598,6 +598,19 @@ body > canvas { font-size: 1.3rem; } +.language-toggle-button { + border-color: rgba(91, 160, 255, 0.35); + color: rgba(173, 205, 255, 0.88); + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.05em; +} + +.language-toggle-button span { + font-size: 0.9rem; + font-weight: 700; +} + .timeline-overlay { position: absolute; bottom: 24px; diff --git a/assets/main.js b/assets/main.js index 587bf86..146388b 100644 --- a/assets/main.js +++ b/assets/main.js @@ -20,10 +20,18 @@ const VISUALIZER_CONFIG = { const MNIST_SAMPLE_MANIFEST_URL = "./assets/data/mnist-test-manifest.json"; +// i18n helper function +function t(key, params) { + if (window.i18n && typeof window.i18n.t === 'function') { + return window.i18n.t(key, params); + } + return key; +} + document.addEventListener("DOMContentLoaded", () => { initializeVisualizer().catch((error) => { console.error(error); - renderErrorMessage("Visualisierung konnte nicht initialisiert werden. Details finden Sie in der Konsole."); + renderErrorMessage(t("errors.initializationFailed")); }); }); @@ -31,7 +39,7 @@ async function loadMnistTestSamples(manifestPath = MNIST_SAMPLE_MANIFEST_URL) { const manifestUrl = new URL(manifestPath, window.location.href); const manifestResponse = await fetch(manifestUrl.toString()); if (!manifestResponse.ok) { - throw new Error(`Konnte MNIST-Manifest nicht laden (${manifestResponse.status}).`); + throw new Error(`Could not load MNIST manifest (${manifestResponse.status}).`); } const manifest = await manifestResponse.json(); const rows = Number(manifest?.imageShape?.[0]) || 28; @@ -41,19 +49,19 @@ async function loadMnistTestSamples(manifestPath = MNIST_SAMPLE_MANIFEST_URL) { const imageFile = manifest?.image?.file; const labelFile = manifest?.labels?.file; if (!imageFile || !labelFile) { - throw new Error("Manifest enthält keine gültigen Dateipfade für Bilder oder Labels."); + throw new Error("Manifest does not contain valid file paths for images or labels."); } const [imageBuffer, labelBuffer] = await Promise.all([ fetch(new URL(imageFile, manifestUrl).toString()).then((response) => { if (!response.ok) { - throw new Error(`Konnte MNIST-Bilddaten nicht laden (${response.status}).`); + throw new Error(`Could not load MNIST image data (${response.status}).`); } return response.arrayBuffer(); }), fetch(new URL(labelFile, manifestUrl).toString()).then((response) => { if (!response.ok) { - throw new Error(`Konnte MNIST-Labeldaten nicht laden (${response.status}).`); + throw new Error(`Could not load MNIST label data (${response.status}).`); } return response.arrayBuffer(); }), @@ -65,22 +73,22 @@ async function loadMnistTestSamples(manifestPath = MNIST_SAMPLE_MANIFEST_URL) { if (sampleSize > 0) { const inferredSamples = Math.floor(imageBytes.length / sampleSize); if (inferredSamples <= 0) { - throw new Error("Aus den MNIST-Bilddaten konnte keine Stichprobengröße abgeleitet werden."); + throw new Error("Could not infer sample size from MNIST image data."); } if (labelBytes.length !== inferredSamples) { - throw new Error("Anzahl der Labels stimmt nicht mit den abgeleiteten Stichproben überein."); + throw new Error("Number of labels does not match inferred samples."); } } else { - throw new Error("Manifest enthält keine gültige Stichprobengröße."); + throw new Error("Manifest does not contain a valid sample size."); } } const totalSamples = numSamples > 0 ? numSamples : Math.floor(imageBytes.length / sampleSize); if (imageBytes.length !== totalSamples * sampleSize) { - throw new Error("MNIST-Bilddatenlänge stimmt nicht mit der erwarteten Größe überein."); + throw new Error("MNIST image data length does not match expected size."); } if (labelBytes.length !== totalSamples) { - throw new Error("MNIST-Labeldatenlänge stimmt nicht mit der erwarteten Größe überein."); + throw new Error("MNIST label data length does not match expected size."); } const digitBuckets = Array.from({ length: 10 }, () => []); @@ -141,7 +149,7 @@ async function setupMnistSampleButtons({ digitCanvas, onSampleApplied, manifestP try { loader = await loadMnistTestSamples(manifestPath ?? MNIST_SAMPLE_MANIFEST_URL); } catch (error) { - console.warn("MNIST-Testdaten konnten nicht geladen werden:", error); + console.warn("MNIST test data could not be loaded:", error); return null; } const column = document.createElement("div"); @@ -151,7 +159,7 @@ async function setupMnistSampleButtons({ digitCanvas, onSampleApplied, manifestP button.type = "button"; button.className = "digit-button"; button.textContent = String(digit); - button.setAttribute("aria-label", `Zufällige ${digit} laden`); + button.setAttribute("aria-label", t("aria.loadRandom", { digit })); button.addEventListener("click", () => { const sample = loader.getRandomSample(digit); if (!sample) return; @@ -175,7 +183,7 @@ async function initializeVisualizer() { const weightDefinitionUrl = new URL(VISUALIZER_CONFIG.weightUrl, window.location.href); const definition = await fetchNetworkDefinition(weightDefinitionUrl.toString()); if (!definition?.network) { - throw new Error("Ungültige Netzwerkdefinition."); + throw new Error("Invalid network definition."); } const timelineSnapshots = hydrateTimeline(definition.timeline, { @@ -183,7 +191,7 @@ async function initializeVisualizer() { baseUrl: weightDefinitionUrl, }); if (!timelineSnapshots.length) { - throw new Error("Keine gültigen Timeline-Snapshots gefunden."); + throw new Error("No valid timeline snapshots found."); } const defaultSnapshotIndex = Math.max(timelineSnapshots.length - 1, 0); const initialSnapshot = timelineSnapshots[defaultSnapshotIndex]; @@ -664,7 +672,7 @@ function initializeAdvancedSettings({ neuralScene, digitCanvas, onConnectionsSet async function fetchNetworkDefinition(url) { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) { - throw new Error(`Netzwerkgewichte konnten nicht geladen werden (${response.status})`); + throw new Error(`Network weights could not be loaded (${response.status})`); } return response.json(); } @@ -681,7 +689,7 @@ function resolveRelativeUrl(base, relativePath) { const baseUrl = base instanceof URL ? base : new URL(base, window.location.href); return new URL(relativePath, baseUrl).toString(); } catch (error) { - console.warn("Konnte relative URL nicht auflösen:", relativePath, error); + console.warn("Could not resolve relative URL:", relativePath, error); return null; } } @@ -699,7 +707,7 @@ function decodeBase64ToUint8Array(base64) { if (typeof Buffer === "function") { return Uint8Array.from(Buffer.from(base64, "base64")); } - throw new Error("Base64-Dekodierung ist in dieser Umgebung nicht verfügbar."); + throw new Error("Base64 decoding is not available in this environment."); } function float16ToFloat32(value) { @@ -726,13 +734,13 @@ function float16ToFloat32(value) { function decodeFloat16Base64(base64, expectedLength) { const bytes = decodeBase64ToUint8Array(base64); if (bytes.byteLength % 2 !== 0) { - throw new Error("Float16-Daten haben eine ungültige Länge."); + throw new Error("Float16 data has invalid length."); } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const length = bytes.byteLength / 2; if (Number.isFinite(expectedLength) && expectedLength > 0 && length !== expectedLength) { throw new Error( - `Erwartete ${expectedLength} Float16-Werte, erhalten wurden jedoch ${length}.`, + `Expected ${expectedLength} Float16 values, but got ${length}.`, ); } const result = new Float32Array(length); @@ -798,14 +806,14 @@ function normaliseWeightsDescriptor(descriptor, baseUrl) { async function fetchSnapshotPayload(url) { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) { - throw new Error(`Snapshot konnte nicht geladen werden (${response.status})`); + throw new Error(`Snapshot could not be loaded (${response.status})`); } return response.json(); } function decodeSnapshotLayers(payload, layerMetadata) { if (!payload || typeof payload !== "object" || !Array.isArray(payload.layers)) { - throw new Error("Snapshot-Datei enthält keine gültigen Layerdaten."); + throw new Error("Snapshot file does not contain valid layer data."); } return layerMetadata.map((meta, index) => { @@ -813,21 +821,21 @@ function decodeSnapshotLayers(payload, layerMetadata) { payload.layers[index] ?? payload.layers.find((layer) => Number(layer?.layer_index) === meta.layerIndex); if (!layerPayload) { - throw new Error(`Snapshot fehlt Layer ${meta.layerIndex}.`); + throw new Error(`Snapshot missing layer ${meta.layerIndex}.`); } const weightsInfo = layerPayload.weights ?? {}; const biasesInfo = layerPayload.biases ?? {}; if (typeof weightsInfo.data !== "string" || typeof biasesInfo.data !== "string") { - throw new Error("Snapshot-Layer enthält keine kodierten Gewichte."); + throw new Error("Snapshot layer does not contain encoded weights."); } const weightShape = normaliseShape(weightsInfo.shape, meta.weightShape); const biasShape = normaliseShape(biasesInfo.shape, meta.biasShape); if (weightShape.length !== 2) { - throw new Error("Snapshot-Layer hat eine ungültige Gewichtsdimension."); + throw new Error("Snapshot layer has invalid weight dimension."); } if (biasShape.length === 0) { - throw new Error("Snapshot-Layer hat eine ungültige Bias-Dimension."); + throw new Error("Snapshot layer has invalid bias dimension."); } const weights = decodeWeightMatrix(weightsInfo.data, weightShape); @@ -1019,7 +1027,7 @@ function setupTimelineSlider(timelineSnapshots, options = {}) { const nextIndex = Number(event.target.value); if (Number.isNaN(nextIndex)) return; setActiveIndex(nextIndex, { emit: true }).catch((error) => { - console.error("Fehler beim Aktualisieren des Snapshots:", error); + console.error("Error updating snapshot:", error); }); }); @@ -1027,7 +1035,7 @@ function setupTimelineSlider(timelineSnapshots, options = {}) { const nextIndex = Number(event.target.value); if (Number.isNaN(nextIndex)) return; setActiveIndex(nextIndex, { emit: true }).catch((error) => { - console.error("Fehler beim Aktualisieren des Snapshots:", error); + console.error("Error updating snapshot:", error); }); }); @@ -1045,7 +1053,7 @@ function setupTimelineSlider(timelineSnapshots, options = {}) { class DigitSketchPad { constructor(container, rows, cols, options = {}) { if (!container) { - throw new Error("Raster-Container nicht gefunden."); + throw new Error("Grid container not found."); } this.container = container; this.rows = rows; @@ -1085,7 +1093,7 @@ class DigitSketchPad { this.container.innerHTML = ""; const title = document.createElement("div"); title.className = "grid-title"; - title.textContent = "Ziffer zeichnen"; + title.textContent = t("drawing.title"); this.interactionRow = document.createElement("div"); this.interactionRow.className = "grid-interaction-row"; this.interactionRow.appendChild(this.gridElement); @@ -1096,6 +1104,16 @@ class DigitSketchPad { this.gridElement.addEventListener("pointermove", (event) => this.handlePointerMove(event)); window.addEventListener("pointerup", () => this.handlePointerUp()); this.gridElement.addEventListener("contextmenu", (event) => event.preventDefault()); + + // Listen for language changes + window.addEventListener("languageChanged", () => this.updateLanguage()); + } + + updateLanguage() { + const title = this.container.querySelector(".grid-title"); + if (title) { + title.textContent = t("drawing.title"); + } } setChangeHandler(handler) { @@ -1230,11 +1248,11 @@ class DigitSketchPad { setPixels(pixels) { if (!pixels || typeof pixels.length !== "number") { - throw new Error("Ungültige Pixelwerte für den Zeichenblock."); + throw new Error("Invalid pixel values for drawing pad."); } if (pixels.length !== this.values.length) { throw new Error( - `Erwartete ${this.values.length} Pixel, erhielt aber ${pixels.length}.`, + `Expected ${this.values.length} pixels, but got ${pixels.length}.`, ); } for (let i = 0; i < this.values.length; i += 1) { @@ -1271,7 +1289,7 @@ class DigitSketchPad { class FeedForwardModel { constructor(definition) { if (!definition.layers?.length) { - throw new Error("Die Netzwerkdefinition muss Schichten enthalten."); + throw new Error("Network definition must contain layers."); } this.normalization = definition.normalization ?? { mean: 0, std: 1 }; this.architecture = Array.isArray(definition.architecture) @@ -1322,7 +1340,7 @@ class FeedForwardModel { updateLayers(layerDefinitions) { if (!Array.isArray(layerDefinitions) || layerDefinitions.length === 0) { - throw new Error("Neue Layerdefinitionen müssen mindestens eine Schicht enthalten."); + throw new Error("New layer definitions must contain at least one layer."); } this.layers = layerDefinitions.map((layer, index) => this.normaliseLayer(layer, index)); this.architecture = this.computeArchitecture(this.layers); @@ -1379,21 +1397,30 @@ class ProbabilityPanel { this.container = container; this.rows = []; if (!this.container) { - throw new Error("Vorhersage-Diagrammcontainer nicht gefunden."); + throw new Error("Prediction chart container not found."); } this.build(); + + // Listen for language changes + window.addEventListener("languageChanged", () => this.updateLanguage()); + } + + updateLanguage() { + if (this.titleElement) { + this.titleElement.textContent = t("predictions.title"); + } } build() { this.container.innerHTML = ""; - const title = document.createElement("h3"); - title.textContent = "Wahrscheinlichkeiten der Ziffern"; - this.container.appendChild(title); + this.titleElement = document.createElement("h3"); + this.titleElement.textContent = t("predictions.title"); + this.container.appendChild(this.titleElement); this.chartElement = document.createElement("div"); this.chartElement.className = "prediction-chart"; this.container.appendChild(this.chartElement); - + for (let digit = 0; digit < 10; digit += 1) { const row = document.createElement("div"); row.className = "prediction-bar-container"; @@ -1443,9 +1470,9 @@ class NetworkInfoPanel { constructor(container) { this.container = container; if (!this.container) { - throw new Error("Netzwerkinfo-Container nicht gefunden."); + throw new Error("Network info container not found."); } - this.numberFormatter = new Intl.NumberFormat("de-DE"); + this.numberFormatter = new Intl.NumberFormat("en-US"); this.build(); } @@ -1456,7 +1483,7 @@ class NetworkInfoPanel { } this.titleElement = document.createElement("h3"); this.titleElement.className = "network-info-panel__title"; - this.titleElement.textContent = "Netzwerkübersicht"; + this.titleElement.textContent = t("network.overview"); this.summaryElement = document.createElement("div"); this.summaryElement.className = "network-info-panel__summary"; @@ -1466,7 +1493,7 @@ class NetworkInfoPanel { this.emptyElement = document.createElement("div"); this.emptyElement.className = "network-info-panel__empty"; - this.emptyElement.textContent = "Keine Netzwerkdaten verfügbar."; + this.emptyElement.textContent = t("network.noData"); this.container.appendChild(this.titleElement); this.container.appendChild(this.summaryElement); @@ -1476,6 +1503,22 @@ class NetworkInfoPanel { this.summaryElement.style.display = "none"; this.layersElement.style.display = "none"; this.emptyElement.style.display = "block"; + + // Listen for language changes + window.addEventListener("languageChanged", () => this.updateLanguage()); + } + + updateLanguage() { + if (this.titleElement) { + this.titleElement.textContent = t("network.overview"); + } + if (this.emptyElement) { + this.emptyElement.textContent = t("network.noData"); + } + // Rebuild if we have data + if (this.lastModel) { + this.update(this.lastModel); + } } formatNumber(value) { @@ -1517,6 +1560,7 @@ class NetworkInfoPanel { } update(model) { + this.lastModel = model; if (!model || !Array.isArray(model.layers) || model.layers.length === 0) { this.emptyElement.style.display = "block"; this.summaryElement.style.display = "none"; @@ -1563,7 +1607,7 @@ class NetworkInfoPanel { const totalParameters = layerSummaries.reduce((sum, entry) => sum + entry.parameterCount, 0); this.summaryElement.innerHTML = ""; - this.summaryElement.appendChild(this.buildSummaryLine("Gesamtparameter", totalParameters)); + this.summaryElement.appendChild(this.buildSummaryLine(t("network.totalParameters"), totalParameters)); if (architecture.length > 0) { const firstArchitectureValue = architecture[0]; const lastArchitectureValue = architecture[architecture.length - 1]; @@ -1576,10 +1620,10 @@ class NetworkInfoPanel { typeof lastArchitectureValue === "number" && Number.isFinite(lastArchitectureValue) ? lastArchitectureValue : lastLayer?.outputSize ?? 0; - this.summaryElement.appendChild(this.buildSummaryLine("Eingabeknoten", inputNodes)); - this.summaryElement.appendChild(this.buildSummaryLine("Ausgabeklassen", outputNodes)); + this.summaryElement.appendChild(this.buildSummaryLine(t("network.inputNodes"), inputNodes)); + this.summaryElement.appendChild(this.buildSummaryLine(t("network.outputClasses"), outputNodes)); } - this.summaryElement.appendChild(this.buildSummaryLine("Layer (inkl. Ausgaben)", layerSummaries.length)); + this.summaryElement.appendChild(this.buildSummaryLine(t("network.layers"), layerSummaries.length)); this.layersElement.innerHTML = ""; layerSummaries.forEach((entry) => { @@ -1593,9 +1637,9 @@ class NetworkInfoPanel { const metrics = document.createElement("div"); metrics.className = "network-info-panel__layer-metrics"; - metrics.appendChild(this.buildMetric("Gewichte", entry.weightCount)); - metrics.appendChild(this.buildMetric("Bias", entry.biasCount)); - metrics.appendChild(this.buildMetric("Summe", entry.parameterCount)); + metrics.appendChild(this.buildMetric(t("network.weights"), entry.weightCount)); + metrics.appendChild(this.buildMetric(t("network.bias"), entry.biasCount)); + metrics.appendChild(this.buildMetric(t("network.total"), entry.parameterCount)); layerRow.appendChild(title); layerRow.appendChild(metrics); @@ -1650,10 +1694,10 @@ class NeuronDetailPanel { .map( (entry) => `
-
Quelle
#${entry.sourceIndex + 1}
-
Input
${this.formatValue(entry.sourceActivation)}
-
Gewicht
${this.formatValue(entry.weight)}
-
Produkt
${this.formatValue(entry.contribution)}
+
${t("neuronDetail.source")}
#${entry.sourceIndex + 1}
+
${t("neuronDetail.input")}
${this.formatValue(entry.sourceActivation)}
+
${t("neuronDetail.weight")}
${this.formatValue(entry.weight)}
+
${t("neuronDetail.product")}
${this.formatValue(entry.contribution)}
`, ) @@ -1665,10 +1709,10 @@ class NeuronDetailPanel { .map( (entry) => `
-
Ziel
#${entry.targetIndex + 1}
-
Aktivierung (Ziel)
${this.formatValue(entry.targetActivation)}
-
Gewicht
${this.formatValue(entry.weight)}
-
Beitrag
${this.formatValue(entry.contribution)}
+
${t("neuronDetail.target")}
#${entry.targetIndex + 1}
+
${t("neuronDetail.activation")}
${this.formatValue(entry.targetActivation)}
+
${t("neuronDetail.weight")}
${this.formatValue(entry.weight)}
+
${t("neuronDetail.contribution")}
${this.formatValue(entry.contribution)}
`, ) @@ -1681,7 +1725,7 @@ class NeuronDetailPanel { payload.bias !== null && payload.bias !== undefined ? `
-
Bias
${this.formatValue(payload.bias)}
+
${t("network.bias")}
${this.formatValue(payload.bias)}
@@ -1713,27 +1757,27 @@ class NeuronDetailPanel { const incomingSection = hasIncoming ? `
-
Eingehende Beiträge
+
${t("neuronDetail.incomingContributions")}
-
Quelle
-
Input
-
Gewicht
-
Produkt
+
${t("neuronDetail.source")}
+
${t("neuronDetail.input")}
+
${t("neuronDetail.weight")}
+
${t("neuronDetail.product")}
${incomingRows}
` - : `
Keine eingehenden Verbindungen für diese Schicht.
`; + : `
${t("neuronDetail.noIncomingConnections")}
`; const outgoingSection = hasOutgoing ? `
-
Ausgehende Beiträge
+
${t("neuronDetail.outgoingContributions")}
-
Ziel
-
Aktivierung (Ziel)
-
Gewicht
-
Beitrag
+
${t("neuronDetail.target")}
+
${t("neuronDetail.activation")}
+
${t("neuronDetail.weight")}
+
${t("neuronDetail.contribution")}
${outgoingRows}
@@ -1742,7 +1786,7 @@ class NeuronDetailPanel { const summaryFormula = payload.preActivation !== null && payload.preActivation !== undefined - ? `Σ = Σ(input × gewicht)${payload.bias !== null && payload.bias !== undefined ? " + bias" : ""}` + ? `Σ = Σ(input × weight)${payload.bias !== null && payload.bias !== undefined ? " + bias" : ""}` : ""; this.root.innerHTML = ` @@ -1751,7 +1795,7 @@ class NeuronDetailPanel {
${payload.layerLabel} • Neuron ${payload.neuronIndex + 1}${ payload.activationName ? ` (${payload.activationName})` : "" }
- +
@@ -1759,8 +1803,8 @@ class NeuronDetailPanel {
${totalsBlock}
- Eingangsschicht-Größe: ${payload.previousLayerSize ?? "—"} - Ausgangsschicht-Größe: ${payload.nextLayerSize ?? "—"} + ${t("neuronDetail.inputLayerSize")} ${payload.previousLayerSize ?? "—"} + ${t("neuronDetail.outputLayerSize")} ${payload.nextLayerSize ?? "—"}
${incomingSection} ${outgoingSection} @@ -1773,6 +1817,18 @@ class NeuronDetailPanel { if (closeButton) { closeButton.addEventListener("click", this.handleClose); } + + // Store payload for language updates + this.lastPayload = payload; + + // Listen for language changes + window.addEventListener("languageChanged", () => this.updateLanguage()); + } + + updateLanguage() { + if (this.lastPayload) { + this.render(this.lastPayload); + } } formatValue(value) { @@ -1799,13 +1855,16 @@ class FpsMonitor { this.valueElement = document.createElement("span"); this.valueElement.className = "fps-overlay__value"; - this.valueElement.textContent = "— fps"; + this.valueElement.textContent = `— ${t("fps.fps")}`; this.root.appendChild(this.valueElement); document.body.appendChild(this.root); this.refreshDisplay = this.refreshDisplay.bind(this); this.displayTimer = window.setInterval(this.refreshDisplay, 250); this.refreshDisplay(); + + // Listen for language changes + window.addEventListener("languageChanged", () => this.refreshDisplay()); } update(time) { @@ -1838,15 +1897,15 @@ class FpsMonitor { this.lastFrameTimestamp > 0 ? now - this.lastFrameTimestamp : Number.POSITIVE_INFINITY; if (!Number.isFinite(timeSinceLastFrame) || timeSinceLastFrame > 600) { - this.valueElement.textContent = "idle"; + this.valueElement.textContent = t("fps.idle"); this.currentFps = null; return; } if (this.currentFps !== null) { - this.valueElement.textContent = `${this.currentFps} fps`; + this.valueElement.textContent = `${this.currentFps} ${t("fps.fps")}`; } else { - this.valueElement.textContent = "— fps"; + this.valueElement.textContent = `— ${t("fps.fps")}`; } } } @@ -2423,12 +2482,12 @@ class NeuralVisualizer { describeLayer(layerIndex) { if (layerIndex === 0) { - return `Eingabeschicht (${this.mlp.architecture[layerIndex]} Knoten)`; + return `Input Layer (${this.mlp.architecture[layerIndex]} nodes)`; } if (layerIndex === this.mlp.architecture.length - 1) { - return `Ausgabeschicht (${this.mlp.architecture[layerIndex]} Knoten)`; + return `Output Layer (${this.mlp.architecture[layerIndex]} nodes)`; } - return `Verborgene Schicht ${layerIndex} (${this.mlp.architecture[layerIndex]} Knoten)`; + return `Hidden Layer ${layerIndex} (${this.mlp.architecture[layerIndex]} nodes)`; } getActivationName(layerIndex) { diff --git a/index.html b/index.html index 48c0555..cb0fb64 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,10 @@ - + - MNIST MLP – Visualisierung der Inferenz + MNIST MLP – Inference Visualization + @@ -33,8 +34,8 @@
-

Zeichnen: Auf dem Raster klicken und ziehen (Rechtsklick zum Löschen)

-

3D-Steuerung: • Linke Taste + ziehen = drehen • Rechte Taste + ziehen = verschieben • Scrollrad = zoomen

+

Drawing:

+

@@ -42,18 +43,27 @@
-
Touch-Steuerung
+
    -
  • Zeichnen: Tippen und auf dem Raster ziehen
  • -
  • Drehen: Mit einem Finger ziehen
  • -
  • Verschieben: Mit zwei Fingern bewegen
  • -
  • Zoomen: Finger zusammenziehen oder spreizen
  • +
  • Drawing:
  • +
  • +
  • +
+
-

Interaktive Visualisierung eines neuronalen Netzes

-

Diese Anwendung zeigt ein kompaktes Multi-Layer-Perceptron (MLP), das auf MNIST trainiert wurde. Zeichnen Sie eine Ziffer und beobachten Sie, wie die Aktivierungen in Echtzeit durch alle vollvernetzten Schichten propagieren.

+

+

-

So funktioniert es:

+

-

Netzarchitektur (Standardexport):

+

-

3D-Steuerung:

+

-

Farbkodierung:

+

-

Eigenes Modell trainieren:

+

-

Echtzeitfunktionen:

+

-

Das Netzwerk ist bewusst kompakt gehalten, um eine flüssige Echtzeitdarstellung zu gewährleisten. Sie können gern mit anderen Schichtgrößen neu trainieren – halten Sie die Architektur nur schlank, damit die 3D-Ansicht reaktionsschnell bleibt.

+

@@ -175,13 +185,12 @@

Echtzeitfunktionen: