>>(grads, clip_value, n);
+}
+
+}
diff --git a/ML/src/python/neuralforge/__init__.py b/ML/src/python/neuralforge/__init__.py
new file mode 100644
index 00000000000..f1a2c8f33b1
--- /dev/null
+++ b/ML/src/python/neuralforge/__init__.py
@@ -0,0 +1,10 @@
+from . import nn
+from . import optim
+from . import data
+from . import utils
+from . import nas
+from .trainer import Trainer
+from .config import Config
+
+__version__ = "1.0.0"
+__all__ = ['nn', 'optim', 'data', 'utils', 'nas', 'Trainer', 'Config']
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/cli/__init__.py b/ML/src/python/neuralforge/cli/__init__.py
new file mode 100644
index 00000000000..97019316414
--- /dev/null
+++ b/ML/src/python/neuralforge/cli/__init__.py
@@ -0,0 +1,6 @@
+from . import train
+from . import test
+from . import gui
+from . import nas
+
+__all__ = ['train', 'test', 'gui', 'nas']
diff --git a/ML/src/python/neuralforge/cli/gui.py b/ML/src/python/neuralforge/cli/gui.py
new file mode 100644
index 00000000000..6ce7d045597
--- /dev/null
+++ b/ML/src/python/neuralforge/cli/gui.py
@@ -0,0 +1,489 @@
+import sys
+import os
+
+def main():
+ try:
+ from PyQt6.QtWidgets import QApplication
+ except ImportError:
+ print("Error: PyQt6 not installed")
+ print("Install with: pip install neuralforge[gui]")
+ print("Or: pip install PyQt6")
+ sys.exit(1)
+
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_dir))))
+
+ sys.path.insert(0, root_dir)
+
+ from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QLabel, QLineEdit, QFileDialog,
+ QProgressBar, QTextEdit, QGroupBox)
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal
+ from PyQt6.QtGui import QPixmap, QFont
+
+ import torch
+ import torch.nn.functional as F
+ from torchvision import transforms
+ from PIL import Image
+
+ from neuralforge.data.datasets import get_dataset, get_num_classes
+ from neuralforge.models.resnet import ResNet18
+
+ class PredictionThread(QThread):
+ finished = pyqtSignal(list, list, str)
+ error = pyqtSignal(str)
+
+ def __init__(self, model, image_path, classes, device):
+ super().__init__()
+ self.model = model
+ self.image_path = image_path
+ self.classes = classes
+ self.device = device
+
+ def run(self):
+ try:
+ image = Image.open(self.image_path).convert('RGB')
+
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ image_tensor = transform(image).unsqueeze(0).to(self.device)
+
+ with torch.no_grad():
+ outputs = self.model(image_tensor)
+ probabilities = F.softmax(outputs, dim=1)
+
+ top5_prob, top5_idx = torch.topk(probabilities, min(5, len(self.classes)), dim=1)
+
+ predictions = []
+ confidences = []
+
+ for idx, prob in zip(top5_idx[0].cpu().numpy(), top5_prob[0].cpu().numpy()):
+ predictions.append(self.classes[idx])
+ confidences.append(float(prob) * 100)
+
+ main_prediction = predictions[0]
+
+ self.finished.emit(predictions, confidences, main_prediction)
+
+ except Exception as e:
+ self.error.emit(str(e))
+
+ class NeuralForgeGUI(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.model = None
+ self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
+ self.classes = []
+ self.dataset_name = 'cifar10'
+
+ self.init_ui()
+ self.apply_stylesheet()
+
+ def init_ui(self):
+ self.setWindowTitle('NeuralForge - Model Tester')
+ self.setGeometry(100, 100, 1200, 800)
+
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ main_layout = QHBoxLayout()
+ central_widget.setLayout(main_layout)
+
+ left_panel = self.create_left_panel()
+ right_panel = self.create_right_panel()
+
+ main_layout.addWidget(left_panel, 1)
+ main_layout.addWidget(right_panel, 1)
+
+ def create_left_panel(self):
+ panel = QWidget()
+ layout = QVBoxLayout()
+ panel.setLayout(layout)
+
+ title = QLabel('🚀 NeuralForge Model Tester')
+ title.setFont(QFont('Arial', 20, QFont.Weight.Bold))
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(title)
+
+ model_group = QGroupBox('Model Selection')
+ model_layout = QVBoxLayout()
+
+ model_path_layout = QHBoxLayout()
+ self.model_path_input = QLineEdit()
+ self.model_path_input.setPlaceholderText('Path to model file (.pt)')
+ model_path_layout.addWidget(self.model_path_input)
+
+ browse_btn = QPushButton('Browse')
+ browse_btn.clicked.connect(self.browse_model)
+ model_path_layout.addWidget(browse_btn)
+
+ default_btn = QPushButton('Use Default')
+ default_btn.clicked.connect(self.use_default_model)
+ model_path_layout.addWidget(default_btn)
+
+ model_layout.addLayout(model_path_layout)
+
+ dataset_layout = QHBoxLayout()
+ dataset_label = QLabel('Dataset:')
+ self.dataset_input = QLineEdit('cifar10')
+ self.dataset_input.setPlaceholderText('cifar10, mnist, stl10, tiny_imagenet, etc.')
+ self.dataset_input.setToolTip('Supported: cifar10, cifar100, mnist, fashion_mnist, stl10,\ntiny_imagenet, imagenet, food101, caltech256, oxford_pets')
+ dataset_layout.addWidget(dataset_label)
+ dataset_layout.addWidget(self.dataset_input)
+ model_layout.addLayout(dataset_layout)
+
+ self.load_model_btn = QPushButton('Load Model')
+ self.load_model_btn.clicked.connect(self.load_model)
+ model_layout.addWidget(self.load_model_btn)
+
+ self.model_status = QLabel('No model loaded')
+ self.model_status.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ model_layout.addWidget(self.model_status)
+
+ model_group.setLayout(model_layout)
+ layout.addWidget(model_group)
+
+ image_group = QGroupBox('Image Selection')
+ image_layout = QVBoxLayout()
+
+ image_path_layout = QHBoxLayout()
+ self.image_path_input = QLineEdit()
+ self.image_path_input.setPlaceholderText('Path to image file')
+ image_path_layout.addWidget(self.image_path_input)
+
+ browse_image_btn = QPushButton('Browse')
+ browse_image_btn.clicked.connect(self.browse_image)
+ image_path_layout.addWidget(browse_image_btn)
+
+ image_layout.addLayout(image_path_layout)
+
+ self.image_preview = QLabel()
+ self.image_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.image_preview.setMinimumHeight(300)
+ self.image_preview.setStyleSheet('border: 2px dashed #666; border-radius: 10px;')
+ self.image_preview.setText('No image selected')
+ image_layout.addWidget(self.image_preview)
+
+ self.predict_btn = QPushButton('🔍 Predict')
+ self.predict_btn.clicked.connect(self.predict_image)
+ self.predict_btn.setEnabled(False)
+ image_layout.addWidget(self.predict_btn)
+
+ image_group.setLayout(image_layout)
+ layout.addWidget(image_group)
+
+ layout.addStretch()
+
+ return panel
+
+ def create_right_panel(self):
+ panel = QWidget()
+ layout = QVBoxLayout()
+ panel.setLayout(layout)
+
+ results_group = QGroupBox('Prediction Results')
+ results_layout = QVBoxLayout()
+
+ self.main_prediction = QLabel('No prediction yet')
+ self.main_prediction.setFont(QFont('Arial', 24, QFont.Weight.Bold))
+ self.main_prediction.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.main_prediction.setStyleSheet('color: #4CAF50; padding: 20px;')
+ results_layout.addWidget(self.main_prediction)
+
+ self.confidence_label = QLabel('')
+ self.confidence_label.setFont(QFont('Arial', 16))
+ self.confidence_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ results_layout.addWidget(self.confidence_label)
+
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setVisible(False)
+ results_layout.addWidget(self.progress_bar)
+
+ results_group.setLayout(results_layout)
+ layout.addWidget(results_group)
+
+ top5_group = QGroupBox('Top-5 Predictions')
+ top5_layout = QVBoxLayout()
+
+ self.top5_display = QTextEdit()
+ self.top5_display.setReadOnly(True)
+ self.top5_display.setMinimumHeight(200)
+ top5_layout.addWidget(self.top5_display)
+
+ top5_group.setLayout(top5_layout)
+ layout.addWidget(top5_group)
+
+ info_group = QGroupBox('Model Information')
+ info_layout = QVBoxLayout()
+
+ self.model_info = QTextEdit()
+ self.model_info.setReadOnly(True)
+ self.model_info.setMaximumHeight(150)
+ info_layout.addWidget(self.model_info)
+
+ info_group.setLayout(info_layout)
+ layout.addWidget(info_group)
+
+ layout.addStretch()
+
+ return panel
+
+ def apply_stylesheet(self):
+ qss = """
+ QMainWindow {
+ background-color: #1e1e1e;
+ }
+
+ QWidget {
+ background-color: #1e1e1e;
+ color: #e0e0e0;
+ font-family: 'Segoe UI', Arial;
+ font-size: 12px;
+ }
+
+ QGroupBox {
+ border: 2px solid #3d3d3d;
+ border-radius: 8px;
+ margin-top: 10px;
+ padding-top: 15px;
+ font-weight: bold;
+ color: #4CAF50;
+ }
+
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 5px;
+ }
+
+ QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 5px;
+ font-weight: bold;
+ font-size: 13px;
+ }
+
+ QPushButton:hover {
+ background-color: #45a049;
+ }
+
+ QPushButton:pressed {
+ background-color: #3d8b40;
+ }
+
+ QPushButton:disabled {
+ background-color: #555555;
+ color: #888888;
+ }
+
+ QLineEdit {
+ background-color: #2d2d2d;
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ padding: 8px;
+ color: #e0e0e0;
+ }
+
+ QLineEdit:focus {
+ border: 2px solid #4CAF50;
+ }
+
+ QTextEdit {
+ background-color: #2d2d2d;
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ padding: 10px;
+ color: #e0e0e0;
+ }
+
+ QLabel {
+ color: #e0e0e0;
+ }
+
+ QProgressBar {
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ text-align: center;
+ background-color: #2d2d2d;
+ }
+
+ QProgressBar::chunk {
+ background-color: #4CAF50;
+ border-radius: 3px;
+ }
+ """
+ self.setStyleSheet(qss)
+
+ def browse_model(self):
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ 'Select Model File',
+ './models',
+ 'Model Files (*.pt *.pth);;All Files (*.*)'
+ )
+ if file_path:
+ self.model_path_input.setText(file_path)
+
+ def use_default_model(self):
+ default_path = './models/final_model.pt'
+ if not os.path.exists(default_path):
+ default_path = './models/best_model.pt'
+ self.model_path_input.setText(os.path.abspath(default_path))
+
+ def browse_image(self):
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ 'Select Image File',
+ '',
+ 'Image Files (*.png *.jpg *.jpeg *.bmp *.gif);;All Files (*.*)'
+ )
+ if file_path:
+ self.image_path_input.setText(file_path)
+ self.display_image(file_path)
+
+ def display_image(self, image_path):
+ try:
+ pixmap = QPixmap(image_path)
+ scaled_pixmap = pixmap.scaled(400, 300, Qt.AspectRatioMode.KeepAspectRatio,
+ Qt.TransformationMode.SmoothTransformation)
+ self.image_preview.setPixmap(scaled_pixmap)
+ except Exception as e:
+ self.image_preview.setText(f'Error loading image: {e}')
+
+ def load_model(self):
+ model_path = self.model_path_input.text()
+ dataset_input = self.dataset_input.text().lower().strip()
+
+ dataset_aliases = {
+ 'cifar10': 'cifar10', 'cifar-10': 'cifar10', 'cifar_10': 'cifar10',
+ 'cifar100': 'cifar100', 'cifar-100': 'cifar100', 'cifar_100': 'cifar100',
+ 'mnist': 'mnist',
+ 'fashionmnist': 'fashion_mnist', 'fashion-mnist': 'fashion_mnist', 'fashion_mnist': 'fashion_mnist',
+ 'stl10': 'stl10', 'stl-10': 'stl10', 'stl_10': 'stl10',
+ 'tinyimagenet': 'tiny_imagenet', 'tiny-imagenet': 'tiny_imagenet', 'tiny_imagenet': 'tiny_imagenet',
+ 'imagenet': 'imagenet',
+ 'food101': 'food101', 'food-101': 'food101', 'food_101': 'food101',
+ 'caltech256': 'caltech256', 'caltech-256': 'caltech256', 'caltech_256': 'caltech256',
+ 'oxfordpets': 'oxford_pets', 'oxford-pets': 'oxford_pets', 'oxford_pets': 'oxford_pets',
+ }
+
+ self.dataset_name = dataset_aliases.get(dataset_input, dataset_input)
+
+ if not model_path:
+ self.model_status.setText('Please select a model file')
+ self.model_status.setStyleSheet('color: #f44336;')
+ return
+
+ if not os.path.exists(model_path):
+ self.model_status.setText('Model file not found')
+ self.model_status.setStyleSheet('color: #f44336;')
+ return
+
+ try:
+ self.model_status.setText('Loading model...')
+ self.model_status.setStyleSheet('color: #FFC107;')
+ QApplication.processEvents()
+
+ num_classes = get_num_classes(self.dataset_name)
+ self.model = ResNet18(num_classes=num_classes)
+ self.model = self.model.to(self.device)
+
+ checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
+ self.model.load_state_dict(checkpoint['model_state_dict'])
+ self.model.eval()
+
+ try:
+ dataset = get_dataset(self.dataset_name, train=False, download=False)
+ self.classes = getattr(dataset, 'classes', [str(i) for i in range(num_classes)])
+ except:
+ from neuralforge.data.datasets import get_class_names
+ self.classes = get_class_names(self.dataset_name)
+
+ self.model_status.setText(f'✓ Model loaded successfully')
+ self.model_status.setStyleSheet('color: #4CAF50;')
+
+ self.predict_btn.setEnabled(True)
+
+ total_params = sum(p.numel() for p in self.model.parameters())
+ epoch = checkpoint.get('epoch', 'Unknown')
+ val_loss = checkpoint.get('best_val_loss', 'Unknown')
+
+ val_loss_str = f"{val_loss:.4f}" if isinstance(val_loss, float) else str(val_loss)
+
+ info_text = f"""
+Model: ResNet18
+Dataset: {self.dataset_name.upper()}
+Classes: {num_classes}
+Parameters: {total_params:,}
+Epoch: {epoch}
+Best Val Loss: {val_loss_str}
+Device: {self.device.upper()}
+ """
+ self.model_info.setText(info_text.strip())
+
+ except Exception as e:
+ self.model_status.setText(f'Error: {str(e)}')
+ self.model_status.setStyleSheet('color: #f44336;')
+
+ def predict_image(self):
+ image_path = self.image_path_input.text()
+
+ if not image_path or not os.path.exists(image_path):
+ self.main_prediction.setText('Please select a valid image')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+ return
+
+ if self.model is None:
+ self.main_prediction.setText('Please load a model first')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+ return
+
+ self.predict_btn.setEnabled(False)
+ self.progress_bar.setVisible(True)
+ self.progress_bar.setRange(0, 0)
+
+ self.prediction_thread = PredictionThread(self.model, image_path, self.classes, self.device)
+ self.prediction_thread.finished.connect(self.display_results)
+ self.prediction_thread.error.connect(self.display_error)
+ self.prediction_thread.start()
+
+ def display_results(self, predictions, confidences, main_prediction):
+ self.progress_bar.setVisible(False)
+ self.predict_btn.setEnabled(True)
+
+ self.main_prediction.setText(f'🎯 {main_prediction}')
+ self.main_prediction.setStyleSheet('color: #4CAF50; padding: 20px; font-size: 28px;')
+
+ self.confidence_label.setText(f'Confidence: {confidences[0]:.2f}%')
+
+ top5_text = 'Top-5 Predictions:
'
+ for i, (pred, conf) in enumerate(zip(predictions, confidences), 1):
+ bar_width = int(conf * 3)
+ bar = '█' * bar_width
+ top5_text += f'{i}. {pred}
'
+ top5_text += f'{bar} {conf:.2f}%
'
+
+ self.top5_display.setHtml(top5_text)
+
+ def display_error(self, error_msg):
+ self.progress_bar.setVisible(False)
+ self.predict_btn.setEnabled(True)
+
+ self.main_prediction.setText(f'Error: {error_msg}')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+
+ app = QApplication(sys.argv)
+ window = NeuralForgeGUI()
+ window.show()
+ sys.exit(app.exec())
+
+if __name__ == '__main__':
+ main()
diff --git a/ML/src/python/neuralforge/cli/nas.py b/ML/src/python/neuralforge/cli/nas.py
new file mode 100644
index 00000000000..f380e130626
--- /dev/null
+++ b/ML/src/python/neuralforge/cli/nas.py
@@ -0,0 +1,70 @@
+import argparse
+import torch
+from neuralforge.nas.search_space import SearchSpace
+from neuralforge.nas.evolution import EvolutionarySearch
+from neuralforge.nas.evaluator import ProxyEvaluator
+from neuralforge.data.datasets import get_dataset
+from neuralforge.data.dataset import SyntheticDataset, DataLoaderBuilder
+from neuralforge.config import Config
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='NeuralForge - Neural Architecture Search',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ neuralforge-nas --population 20 --generations 50
+ neuralforge-nas --dataset cifar10 --population 15 --generations 30
+ """
+ )
+
+ parser.add_argument('--dataset', type=str, default='synthetic', help='Dataset for evaluation')
+ parser.add_argument('--population', type=int, default=15, help='Population size')
+ parser.add_argument('--generations', type=int, default=20, help='Number of generations')
+ parser.add_argument('--mutation-rate', type=float, default=0.15, help='Mutation rate')
+ parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu')
+
+ args = parser.parse_args()
+
+ config = Config()
+ config.device = args.device
+ config.nas_enabled = True
+ config.nas_population_size = args.population
+ config.nas_generations = args.generations
+ config.nas_mutation_rate = args.mutation_rate
+
+ search_config = {
+ 'num_layers': 15,
+ 'num_blocks': 4
+ }
+
+ search_space = SearchSpace(search_config)
+
+ train_dataset = SyntheticDataset(num_samples=1000, num_classes=10)
+ val_dataset = SyntheticDataset(num_samples=200, num_classes=10)
+
+ loader_builder = DataLoaderBuilder(config)
+ train_loader = loader_builder.build_train_loader(train_dataset)
+ val_loader = loader_builder.build_val_loader(val_dataset)
+
+ evaluator = ProxyEvaluator(device=config.device)
+
+ evolution = EvolutionarySearch(
+ search_space=search_space,
+ evaluator=evaluator,
+ population_size=config.nas_population_size,
+ generations=config.nas_generations,
+ mutation_rate=config.nas_mutation_rate
+ )
+
+ print("Starting Neural Architecture Search...")
+ best_architecture = evolution.search()
+
+ print(f"\nBest Architecture Found:")
+ print(f"Fitness: {best_architecture.fitness:.4f}")
+ print(f"Accuracy: {best_architecture.accuracy:.2f}%")
+ print(f"Parameters: {best_architecture.params:,}")
+ print(f"FLOPs: {best_architecture.flops:,}")
+
+if __name__ == '__main__':
+ main()
diff --git a/ML/src/python/neuralforge/cli/test.py b/ML/src/python/neuralforge/cli/test.py
new file mode 100644
index 00000000000..64acf1b6faf
--- /dev/null
+++ b/ML/src/python/neuralforge/cli/test.py
@@ -0,0 +1,136 @@
+import argparse
+import sys
+import os
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+
+import torch
+import torch.nn.functional as F
+from torchvision import transforms
+from PIL import Image
+import numpy as np
+
+from neuralforge.data.datasets import get_dataset, get_num_classes
+from neuralforge.models.resnet import ResNet18
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='NeuralForge - Test trained models',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ neuralforge-test --model models/best_model.pt --dataset cifar10 --mode random
+ neuralforge-test --dataset mnist --mode accuracy
+ neuralforge-test --dataset stl10 --image cat.jpg
+ """
+ )
+
+ default_model = './models/best_model.pt'
+ parser.add_argument('--model', type=str, default=default_model, help='Path to model checkpoint')
+ parser.add_argument('--dataset', type=str, default='cifar10', help='Dataset name')
+ parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu')
+ parser.add_argument('--mode', type=str, default='random', choices=['random', 'accuracy', 'interactive'])
+ parser.add_argument('--samples', type=int, default=10, help='Number of samples for random mode')
+ parser.add_argument('--image', type=str, default=None, help='Path to image file')
+
+ args = parser.parse_args()
+
+ print("=" * 60)
+ print(" NeuralForge - Model Testing")
+ print("=" * 60)
+ print(f"Device: {args.device}")
+
+ dataset_aliases = {
+ 'cifar-10': 'cifar10', 'stl-10': 'stl10', 'fashion-mnist': 'fashion_mnist',
+ 'tiny-imagenet': 'tiny_imagenet', 'food-101': 'food101',
+ }
+ dataset_name = dataset_aliases.get(args.dataset.lower(), args.dataset.lower())
+
+ num_classes = get_num_classes(dataset_name)
+ model = ResNet18(num_classes=num_classes)
+ model = model.to(args.device)
+
+ if os.path.exists(args.model):
+ print(f"Loading model from: {args.model}")
+ checkpoint = torch.load(args.model, map_location=args.device, weights_only=False)
+ model.load_state_dict(checkpoint['model_state_dict'])
+ print(f"Model loaded from epoch {checkpoint.get('epoch', 'Unknown')}")
+ else:
+ print(f"Warning: No model found at {args.model}")
+ return
+
+ model.eval()
+
+ test_dataset = get_dataset(dataset_name, root='./data', train=False, download=True)
+ classes = getattr(test_dataset, 'classes', [str(i) for i in range(num_classes)])
+
+ print(f"Dataset: {dataset_name} ({len(test_dataset.dataset)} test samples)")
+ print("=" * 60)
+
+ if args.image:
+ image = Image.open(args.image).convert('RGB')
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+ image_tensor = transform(image).unsqueeze(0).to(args.device)
+
+ with torch.no_grad():
+ outputs = model(image_tensor)
+ probabilities = F.softmax(outputs, dim=1)
+ top5_prob, top5_idx = torch.topk(probabilities, min(5, num_classes), dim=1)
+
+ print(f"\nPrediction for {args.image}:")
+ print(f"Main: {classes[top5_idx[0][0].item()]} ({top5_prob[0][0].item()*100:.2f}%)")
+ print("\nTop-5:")
+ for i, (idx, prob) in enumerate(zip(top5_idx[0], top5_prob[0]), 1):
+ print(f" {i}. {classes[idx.item()]:15s} {prob.item()*100:.2f}%")
+
+ elif args.mode == 'random':
+ print(f"\nTesting {args.samples} random samples...")
+ print("-" * 60)
+
+ correct = 0
+ indices = np.random.choice(len(test_dataset.dataset), args.samples, replace=False)
+
+ for i, idx in enumerate(indices, 1):
+ image, label = test_dataset.dataset[idx]
+
+ with torch.no_grad():
+ image = image.unsqueeze(0).to(args.device)
+ outputs = model(image)
+ pred_class = outputs.argmax(1).item()
+ confidence = F.softmax(outputs, dim=1)[0][pred_class].item() * 100
+
+ is_correct = pred_class == label
+ correct += is_correct
+ status = "✓" if is_correct else "✗"
+
+ print(f"{i:2d}. {status} True: {classes[label]:15s} | Pred: {classes[pred_class]:15s} | Conf: {confidence:.1f}%")
+
+ print("-" * 60)
+ print(f"Accuracy: {correct/args.samples:.1%} ({correct}/{args.samples})")
+
+ elif args.mode == 'accuracy':
+ print("\nCalculating full test accuracy...")
+ correct = 0
+ total = 0
+
+ with torch.no_grad():
+ for image, label in test_dataset.dataset:
+ image = image.unsqueeze(0).to(args.device)
+ outputs = model(image)
+ pred_class = outputs.argmax(1).item()
+ total += 1
+ if pred_class == label:
+ correct += 1
+
+ if total % 100 == 0:
+ print(f"Processed {total}/{len(test_dataset.dataset)}...", end='\r')
+
+ print(f"\nOverall Accuracy: {100.0 * correct / total:.2f}% ({correct}/{total})")
+
+if __name__ == '__main__':
+ main()
diff --git a/ML/src/python/neuralforge/cli/train.py b/ML/src/python/neuralforge/cli/train.py
new file mode 100644
index 00000000000..768644f4f06
--- /dev/null
+++ b/ML/src/python/neuralforge/cli/train.py
@@ -0,0 +1,208 @@
+import argparse
+import sys
+import torch
+import torch.nn as nn
+import random
+import numpy as np
+
+from neuralforge.trainer import Trainer
+from neuralforge.config import Config
+from neuralforge.data.datasets import get_dataset, get_num_classes
+from neuralforge.data.dataset import SyntheticDataset, DataLoaderBuilder
+from neuralforge.models.resnet import ResNet18
+from neuralforge.optim.optimizers import AdamW
+from neuralforge.optim.schedulers import CosineAnnealingWarmRestarts, OneCycleLR
+from neuralforge.utils.logger import Logger
+
+def set_seed(seed):
+ random.seed(seed)
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+ torch.backends.cudnn.deterministic = True
+ torch.backends.cudnn.benchmark = False
+
+def create_simple_model(num_classes=10):
+ return nn.Sequential(
+ nn.Conv2d(3, 32, 3, padding=1),
+ nn.BatchNorm2d(32),
+ nn.ReLU(inplace=True),
+ nn.MaxPool2d(2),
+
+ nn.Conv2d(32, 64, 3, padding=1),
+ nn.BatchNorm2d(64),
+ nn.ReLU(inplace=True),
+ nn.MaxPool2d(2),
+
+ nn.Conv2d(64, 128, 3, padding=1),
+ nn.BatchNorm2d(128),
+ nn.ReLU(inplace=True),
+ nn.AdaptiveAvgPool2d(1),
+
+ nn.Flatten(),
+ nn.Linear(128, num_classes)
+ )
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='NeuralForge - Train neural networks with CUDA acceleration',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ neuralforge --dataset cifar10 --epochs 50
+ neuralforge --dataset mnist --model simple --batch-size 64
+ neuralforge --dataset stl10 --model resnet18 --epochs 100 --lr 0.001
+ neuralforge --dataset tiny_imagenet --batch-size 128 --epochs 200
+ """
+ )
+
+ parser.add_argument('--config', type=str, default=None, help='Path to config file')
+ parser.add_argument('--model', type=str, default='simple',
+ choices=['simple', 'resnet18', 'efficientnet', 'vit'],
+ help='Model architecture')
+ parser.add_argument('--dataset', type=str, default='synthetic',
+ help='Dataset (cifar10, mnist, stl10, tiny_imagenet, etc.)')
+ parser.add_argument('--batch-size', type=int, default=32, help='Batch size')
+ parser.add_argument('--epochs', type=int, default=50, help='Number of epochs')
+ parser.add_argument('--lr', type=float, default=0.001, help='Learning rate')
+ parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu',
+ help='Device (cuda/cpu)')
+ parser.add_argument('--num-samples', type=int, default=5000, help='Number of synthetic samples')
+ parser.add_argument('--num-classes', type=int, default=10, help='Number of classes (for synthetic)')
+ parser.add_argument('--seed', type=int, default=42, help='Random seed')
+ parser.add_argument('--optimizer', type=str, default='adamw',
+ choices=['adamw', 'adam', 'sgd'],
+ help='Optimizer')
+ parser.add_argument('--scheduler', type=str, default='cosine',
+ choices=['cosine', 'onecycle', 'none'],
+ help='Learning rate scheduler')
+
+ args = parser.parse_args()
+
+ if args.config:
+ config = Config.load(args.config)
+ else:
+ config = Config()
+ config.batch_size = args.batch_size
+ config.epochs = args.epochs
+ config.learning_rate = args.lr
+ config.device = args.device
+ config.num_classes = args.num_classes
+ config.seed = args.seed
+ config.optimizer = args.optimizer
+ config.scheduler = args.scheduler
+
+ # Set paths relative to current working directory (not package directory)
+ import os
+ cwd = os.getcwd()
+ config.model_dir = os.path.join(cwd, "models")
+ config.log_dir = os.path.join(cwd, "logs")
+ config.data_path = os.path.join(cwd, "data")
+
+ set_seed(config.seed)
+
+ logger = Logger(config.log_dir, "training")
+ logger.info("=" * 80)
+ logger.info("NeuralForge Training Framework")
+ logger.info("=" * 80)
+ logger.info(f"Configuration:\n{config}")
+
+ dataset_aliases = {
+ 'cifar-10': 'cifar10', 'cifar_10': 'cifar10',
+ 'cifar-100': 'cifar100', 'cifar_100': 'cifar100',
+ 'fashion-mnist': 'fashion_mnist', 'fashionmnist': 'fashion_mnist',
+ 'stl-10': 'stl10', 'stl_10': 'stl10',
+ 'tiny-imagenet': 'tiny_imagenet', 'tinyimagenet': 'tiny_imagenet',
+ 'food-101': 'food101', 'food_101': 'food101',
+ 'caltech-256': 'caltech256', 'caltech_256': 'caltech256',
+ 'oxford-pets': 'oxford_pets', 'oxfordpets': 'oxford_pets',
+ }
+
+ dataset_name = dataset_aliases.get(args.dataset.lower(), args.dataset.lower())
+
+ if dataset_name == 'synthetic':
+ logger.info("Creating synthetic dataset...")
+ train_dataset = SyntheticDataset(
+ num_samples=args.num_samples,
+ num_classes=config.num_classes,
+ image_size=config.image_size,
+ channels=3
+ )
+ val_dataset = SyntheticDataset(
+ num_samples=args.num_samples // 5,
+ num_classes=config.num_classes,
+ image_size=config.image_size,
+ channels=3
+ )
+ else:
+ logger.info(f"Downloading and loading {dataset_name} dataset...")
+ config.num_classes = get_num_classes(dataset_name)
+
+ train_dataset = get_dataset(dataset_name, root=config.data_path, train=True, download=True)
+ val_dataset = get_dataset(dataset_name, root=config.data_path, train=False, download=True)
+
+ if dataset_name in ['mnist', 'fashion_mnist']:
+ config.image_size = 28
+ elif dataset_name in ['cifar10', 'cifar100']:
+ config.image_size = 32
+ elif dataset_name == 'tiny_imagenet':
+ config.image_size = 64
+ elif dataset_name == 'stl10':
+ config.image_size = 96
+ elif dataset_name in ['imagenet', 'food101', 'caltech256', 'oxford_pets']:
+ config.image_size = 224
+
+ loader_builder = DataLoaderBuilder(config)
+ train_loader = loader_builder.build_train_loader(train_dataset)
+ val_loader = loader_builder.build_val_loader(val_dataset)
+
+ logger.info(f"Train dataset size: {len(train_dataset)}")
+ logger.info(f"Validation dataset size: {len(val_dataset)}")
+
+ logger.info(f"Creating model: {args.model}")
+ if args.model == 'simple':
+ model = create_simple_model(config.num_classes)
+ elif args.model == 'resnet18':
+ model = ResNet18(num_classes=config.num_classes)
+ else:
+ model = create_simple_model(config.num_classes)
+
+ logger.log_model_summary(model)
+
+ criterion = nn.CrossEntropyLoss()
+
+ if config.optimizer.lower() == 'adamw':
+ optimizer = AdamW(model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay)
+ elif config.optimizer.lower() == 'adam':
+ optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay)
+ else:
+ optimizer = torch.optim.SGD(model.parameters(), lr=config.learning_rate, momentum=0.9, weight_decay=config.weight_decay)
+
+ scheduler = None
+ if config.scheduler == 'cosine':
+ scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=1e-6)
+ elif config.scheduler == 'onecycle':
+ scheduler = OneCycleLR(optimizer, max_lr=config.learning_rate, total_steps=config.epochs * len(train_loader))
+
+ logger.info(f"Optimizer: {config.optimizer}")
+ logger.info(f"Scheduler: {config.scheduler}")
+
+ trainer = Trainer(
+ model=model,
+ train_loader=train_loader,
+ val_loader=val_loader,
+ optimizer=optimizer,
+ criterion=criterion,
+ config=config,
+ scheduler=scheduler,
+ device=config.device
+ )
+
+ logger.info("Starting training...")
+ trainer.train()
+
+ logger.info("Training completed successfully!")
+ logger.info(f"Best validation loss: {trainer.best_val_loss:.4f}")
+
+if __name__ == '__main__':
+ main()
diff --git a/ML/src/python/neuralforge/config.py b/ML/src/python/neuralforge/config.py
new file mode 100644
index 00000000000..8c8756bfe21
--- /dev/null
+++ b/ML/src/python/neuralforge/config.py
@@ -0,0 +1,55 @@
+import json
+import os
+from typing import Any, Dict, Optional
+from dataclasses import dataclass, asdict
+
+@dataclass
+class Config:
+ model_name: str = "neuralforge_model"
+ batch_size: int = 32
+ epochs: int = 100
+ learning_rate: float = 0.001
+ weight_decay: float = 0.0001
+ optimizer: str = "adamw"
+ scheduler: str = "cosine"
+ warmup_epochs: int = 5
+ grad_clip: float = 1.0
+
+ data_path: str = "./data"
+ num_workers: int = 4
+ pin_memory: bool = True
+
+ model_dir: str = "./models"
+ log_dir: str = "./logs"
+ checkpoint_freq: int = 10
+
+ use_amp: bool = True
+ device: str = "cuda"
+ seed: int = 42
+
+ nas_enabled: bool = False
+ nas_population_size: int = 20
+ nas_generations: int = 50
+ nas_mutation_rate: float = 0.1
+
+ image_size: int = 224
+ num_classes: int = 1000
+
+ def save(self, path: str):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'w') as f:
+ json.dump(asdict(self), f, indent=2)
+
+ @classmethod
+ def load(cls, path: str) -> 'Config':
+ with open(path, 'r') as f:
+ data = json.load(f)
+ return cls(**data)
+
+ def update(self, **kwargs):
+ for key, value in kwargs.items():
+ if hasattr(self, key):
+ setattr(self, key, value)
+
+ def __str__(self) -> str:
+ return json.dumps(asdict(self), indent=2)
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/data/__init__.py b/ML/src/python/neuralforge/data/__init__.py
new file mode 100644
index 00000000000..8cc8b5d9ced
--- /dev/null
+++ b/ML/src/python/neuralforge/data/__init__.py
@@ -0,0 +1,15 @@
+from .dataset import *
+from .datasets import *
+from .transforms import *
+from .augmentation import *
+
+__all__ = [
+ 'ImageDataset',
+ 'DataLoaderBuilder',
+ 'get_dataset',
+ 'get_num_classes',
+ 'get_transforms',
+ 'RandAugment',
+ 'CutMix',
+ 'MixUp',
+]
diff --git a/ML/src/python/neuralforge/data/augmentation.py b/ML/src/python/neuralforge/data/augmentation.py
new file mode 100644
index 00000000000..ed8cf5cd9a9
--- /dev/null
+++ b/ML/src/python/neuralforge/data/augmentation.py
@@ -0,0 +1,209 @@
+import torch
+import random
+import numpy as np
+from PIL import Image, ImageEnhance, ImageOps
+from typing import List, Tuple
+
+class RandAugment:
+ def __init__(self, n: int = 2, m: int = 9):
+ self.n = n
+ self.m = m
+ self.augment_list = [
+ (self.auto_contrast, 0, 1),
+ (self.equalize, 0, 1),
+ (self.invert, 0, 1),
+ (self.rotate, 0, 30),
+ (self.posterize, 0, 4),
+ (self.solarize, 0, 256),
+ (self.color, 0.1, 1.9),
+ (self.contrast, 0.1, 1.9),
+ (self.brightness, 0.1, 1.9),
+ (self.sharpness, 0.1, 1.9),
+ (self.shear_x, 0, 0.3),
+ (self.shear_y, 0, 0.3),
+ (self.translate_x, 0, 0.3),
+ (self.translate_y, 0, 0.3),
+ ]
+
+ def __call__(self, img):
+ ops = random.choices(self.augment_list, k=self.n)
+ for op, minval, maxval in ops:
+ val = (float(self.m) / 30) * float(maxval - minval) + minval
+ img = op(img, val)
+ return img
+
+ @staticmethod
+ def auto_contrast(img, _):
+ return ImageOps.autocontrast(img)
+
+ @staticmethod
+ def equalize(img, _):
+ return ImageOps.equalize(img)
+
+ @staticmethod
+ def invert(img, _):
+ return ImageOps.invert(img)
+
+ @staticmethod
+ def rotate(img, magnitude):
+ return img.rotate(magnitude)
+
+ @staticmethod
+ def posterize(img, magnitude):
+ magnitude = int(magnitude)
+ return ImageOps.posterize(img, magnitude)
+
+ @staticmethod
+ def solarize(img, magnitude):
+ return ImageOps.solarize(img, int(magnitude))
+
+ @staticmethod
+ def color(img, magnitude):
+ return ImageEnhance.Color(img).enhance(magnitude)
+
+ @staticmethod
+ def contrast(img, magnitude):
+ return ImageEnhance.Contrast(img).enhance(magnitude)
+
+ @staticmethod
+ def brightness(img, magnitude):
+ return ImageEnhance.Brightness(img).enhance(magnitude)
+
+ @staticmethod
+ def sharpness(img, magnitude):
+ return ImageEnhance.Sharpness(img).enhance(magnitude)
+
+ @staticmethod
+ def shear_x(img, magnitude):
+ return img.transform(img.size, Image.AFFINE, (1, magnitude, 0, 0, 1, 0))
+
+ @staticmethod
+ def shear_y(img, magnitude):
+ return img.transform(img.size, Image.AFFINE, (1, 0, 0, magnitude, 1, 0))
+
+ @staticmethod
+ def translate_x(img, magnitude):
+ magnitude = magnitude * img.size[0]
+ return img.transform(img.size, Image.AFFINE, (1, 0, magnitude, 0, 1, 0))
+
+ @staticmethod
+ def translate_y(img, magnitude):
+ magnitude = magnitude * img.size[1]
+ return img.transform(img.size, Image.AFFINE, (1, 0, 0, 0, 1, magnitude))
+
+class MixUp:
+ def __init__(self, alpha: float = 1.0, num_classes: int = 1000):
+ self.alpha = alpha
+ self.num_classes = num_classes
+
+ def __call__(self, images, labels):
+ batch_size = images.size(0)
+
+ if self.alpha > 0:
+ lam = np.random.beta(self.alpha, self.alpha)
+ else:
+ lam = 1
+
+ index = torch.randperm(batch_size).to(images.device)
+
+ mixed_images = lam * images + (1 - lam) * images[index]
+ labels_a = labels
+ labels_b = labels[index]
+
+ return mixed_images, labels_a, labels_b, lam
+
+class CutMix:
+ def __init__(self, alpha: float = 1.0, num_classes: int = 1000):
+ self.alpha = alpha
+ self.num_classes = num_classes
+
+ def __call__(self, images, labels):
+ batch_size = images.size(0)
+
+ if self.alpha > 0:
+ lam = np.random.beta(self.alpha, self.alpha)
+ else:
+ lam = 1
+
+ index = torch.randperm(batch_size).to(images.device)
+
+ _, _, H, W = images.shape
+ cut_rat = np.sqrt(1.0 - lam)
+ cut_w = int(W * cut_rat)
+ cut_h = int(H * cut_rat)
+
+ cx = np.random.randint(W)
+ cy = np.random.randint(H)
+
+ bbx1 = np.clip(cx - cut_w // 2, 0, W)
+ bby1 = np.clip(cy - cut_h // 2, 0, H)
+ bbx2 = np.clip(cx + cut_w // 2, 0, W)
+ bby2 = np.clip(cy + cut_h // 2, 0, H)
+
+ images[:, :, bby1:bby2, bbx1:bbx2] = images[index, :, bby1:bby2, bbx1:bbx2]
+
+ lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))
+
+ return images, labels, labels[index], lam
+
+class GridMask:
+ def __init__(self, d1: int = 96, d2: int = 224, rotate: float = 1, ratio: float = 0.5):
+ self.d1 = d1
+ self.d2 = d2
+ self.rotate = rotate
+ self.ratio = ratio
+
+ def __call__(self, img):
+ h, w = img.shape[-2:]
+
+ d = np.random.randint(self.d1, self.d2)
+ l = int(d * self.ratio + 0.5)
+
+ mask = np.ones((h, w), np.float32)
+ st_h = np.random.randint(d)
+ st_w = np.random.randint(d)
+
+ for i in range(h // d + 1):
+ s_h = d * i + st_h
+ t_h = min(s_h + l, h)
+ for j in range(w // d + 1):
+ s_w = d * j + st_w
+ t_w = min(s_w + l, w)
+ mask[s_h:t_h, s_w:t_w] = 0
+
+ mask = torch.from_numpy(mask).to(img.device)
+ img = img * mask
+
+ return img
+
+class RandomErasing:
+ def __init__(self, probability: float = 0.5, sl: float = 0.02, sh: float = 0.4, r1: float = 0.3):
+ self.probability = probability
+ self.sl = sl
+ self.sh = sh
+ self.r1 = r1
+
+ def __call__(self, img):
+ if random.uniform(0, 1) >= self.probability:
+ return img
+
+ for attempt in range(100):
+ area = img.size()[1] * img.size()[2]
+
+ target_area = random.uniform(self.sl, self.sh) * area
+ aspect_ratio = random.uniform(self.r1, 1 / self.r1)
+
+ h = int(round(np.sqrt(target_area * aspect_ratio)))
+ w = int(round(np.sqrt(target_area / aspect_ratio)))
+
+ if w < img.size()[2] and h < img.size()[1]:
+ x1 = random.randint(0, img.size()[1] - h)
+ y1 = random.randint(0, img.size()[2] - w)
+
+ img[0, x1:x1 + h, y1:y1 + w] = random.uniform(0, 1)
+ img[1, x1:x1 + h, y1:y1 + w] = random.uniform(0, 1)
+ img[2, x1:x1 + h, y1:y1 + w] = random.uniform(0, 1)
+
+ return img
+
+ return img
diff --git a/ML/src/python/neuralforge/data/dataset.py b/ML/src/python/neuralforge/data/dataset.py
new file mode 100644
index 00000000000..777ee501cda
--- /dev/null
+++ b/ML/src/python/neuralforge/data/dataset.py
@@ -0,0 +1,185 @@
+import torch
+from torch.utils.data import Dataset, DataLoader
+from torchvision import datasets, transforms
+from PIL import Image
+import os
+from typing import Optional, Callable, Tuple, List
+import numpy as np
+
+class ImageDataset(Dataset):
+ def __init__(
+ self,
+ root: str,
+ transform: Optional[Callable] = None,
+ target_transform: Optional[Callable] = None,
+ split: str = 'train'
+ ):
+ self.root = root
+ self.transform = transform
+ self.target_transform = target_transform
+ self.split = split
+
+ self.samples = []
+ self.class_to_idx = {}
+ self._load_dataset()
+
+ def _load_dataset(self):
+ split_dir = os.path.join(self.root, self.split)
+
+ if not os.path.exists(split_dir):
+ raise FileNotFoundError(f"Dataset directory not found: {split_dir}")
+
+ classes = sorted([d for d in os.listdir(split_dir)
+ if os.path.isdir(os.path.join(split_dir, d))])
+
+ self.class_to_idx = {cls_name: idx for idx, cls_name in enumerate(classes)}
+
+ for class_name in classes:
+ class_dir = os.path.join(split_dir, class_name)
+ class_idx = self.class_to_idx[class_name]
+
+ for img_name in os.listdir(class_dir):
+ if img_name.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
+ img_path = os.path.join(class_dir, img_name)
+ self.samples.append((img_path, class_idx))
+
+ def __len__(self) -> int:
+ return len(self.samples)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ img_path, label = self.samples[idx]
+
+ try:
+ image = Image.open(img_path).convert('RGB')
+ except Exception as e:
+ print(f"Error loading image {img_path}: {e}")
+ image = Image.new('RGB', (224, 224), color='black')
+
+ if self.transform:
+ image = self.transform(image)
+
+ if self.target_transform:
+ label = self.target_transform(label)
+
+ return image, label
+
+class SyntheticDataset(Dataset):
+ def __init__(
+ self,
+ num_samples: int = 10000,
+ num_classes: int = 10,
+ image_size: int = 224,
+ channels: int = 3
+ ):
+ self.num_samples = num_samples
+ self.num_classes = num_classes
+ self.image_size = image_size
+ self.channels = channels
+
+ def __len__(self) -> int:
+ return self.num_samples
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ image = torch.randn(self.channels, self.image_size, self.image_size)
+ label = idx % self.num_classes
+ return image, label
+
+class MemoryDataset(Dataset):
+ def __init__(self, data: torch.Tensor, labels: torch.Tensor):
+ assert len(data) == len(labels)
+ self.data = data
+ self.labels = labels
+
+ def __len__(self) -> int:
+ return len(self.data)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ return self.data[idx], self.labels[idx]
+
+class DataLoaderBuilder:
+ def __init__(self, config):
+ self.config = config
+
+ def build_train_loader(self, dataset: Dataset) -> DataLoader:
+ return DataLoader(
+ dataset,
+ batch_size=self.config.batch_size,
+ shuffle=True,
+ num_workers=self.config.num_workers,
+ pin_memory=self.config.pin_memory,
+ drop_last=True,
+ persistent_workers=self.config.num_workers > 0
+ )
+
+ def build_val_loader(self, dataset: Dataset) -> DataLoader:
+ return DataLoader(
+ dataset,
+ batch_size=self.config.batch_size,
+ shuffle=False,
+ num_workers=self.config.num_workers,
+ pin_memory=self.config.pin_memory,
+ drop_last=False,
+ persistent_workers=self.config.num_workers > 0
+ )
+
+ def build_test_loader(self, dataset: Dataset) -> DataLoader:
+ return DataLoader(
+ dataset,
+ batch_size=self.config.batch_size,
+ shuffle=False,
+ num_workers=self.config.num_workers,
+ pin_memory=self.config.pin_memory,
+ drop_last=False
+ )
+
+class CachedDataset(Dataset):
+ def __init__(self, dataset: Dataset, cache_size: int = 1000):
+ self.dataset = dataset
+ self.cache_size = cache_size
+ self.cache = {}
+
+ def __len__(self) -> int:
+ return len(self.dataset)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ if idx in self.cache:
+ return self.cache[idx]
+
+ item = self.dataset[idx]
+
+ if len(self.cache) < self.cache_size:
+ self.cache[idx] = item
+
+ return item
+
+class MultiScaleDataset(Dataset):
+ def __init__(
+ self,
+ dataset: Dataset,
+ scales: List[int] = [224, 256, 288, 320]
+ ):
+ self.dataset = dataset
+ self.scales = scales
+
+ def __len__(self) -> int:
+ return len(self.dataset)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ image, label = self.dataset[idx]
+
+ scale = np.random.choice(self.scales)
+ resize = transforms.Resize((scale, scale))
+ image = resize(image)
+
+ return image, label
+
+class PrefetchDataset(Dataset):
+ def __init__(self, dataset: Dataset, prefetch_size: int = 100):
+ self.dataset = dataset
+ self.prefetch_size = prefetch_size
+
+ def __len__(self) -> int:
+ return len(self.dataset)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ return self.dataset[idx]
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/data/datasets.py b/ML/src/python/neuralforge/data/datasets.py
new file mode 100644
index 00000000000..85a9c1db1fd
--- /dev/null
+++ b/ML/src/python/neuralforge/data/datasets.py
@@ -0,0 +1,341 @@
+import torch
+from torch.utils.data import Dataset
+from torchvision import datasets, transforms
+import os
+from typing import Optional, Callable
+
+class CIFAR10Dataset:
+ def __init__(self, root='./data', train=True, transform=None, download=True):
+ if transform is None:
+ if train:
+ transform = transforms.Compose([
+ transforms.RandomCrop(32, padding=4),
+ transforms.RandomHorizontalFlip(),
+ transforms.ToTensor(),
+ transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
+ ])
+
+ self.dataset = datasets.CIFAR10(root=root, train=train, transform=transform, download=download)
+ self.classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class CIFAR100Dataset:
+ def __init__(self, root='./data', train=True, transform=None, download=True):
+ if transform is None:
+ if train:
+ transform = transforms.Compose([
+ transforms.RandomCrop(32, padding=4),
+ transforms.RandomHorizontalFlip(),
+ transforms.RandomRotation(15),
+ transforms.ToTensor(),
+ transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))
+ ])
+
+ self.dataset = datasets.CIFAR100(root=root, train=train, transform=transform, download=download)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class MNISTDataset:
+ def __init__(self, root='./data', train=True, transform=None, download=True):
+ if transform is None:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize((0.1307,), (0.3081,))
+ ])
+
+ self.dataset = datasets.MNIST(root=root, train=train, transform=transform, download=download)
+ self.classes = [str(i) for i in range(10)]
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class FashionMNISTDataset:
+ def __init__(self, root='./data', train=True, transform=None, download=True):
+ if transform is None:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize((0.2860,), (0.3530,))
+ ])
+
+ self.dataset = datasets.FashionMNIST(root=root, train=train, transform=transform, download=download)
+ self.classes = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
+ 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class STL10Dataset:
+ def __init__(self, root='./data', split='train', transform=None, download=True):
+ if transform is None:
+ if split == 'train':
+ transform = transforms.Compose([
+ transforms.RandomCrop(96, padding=12),
+ transforms.RandomHorizontalFlip(),
+ transforms.ToTensor(),
+ transforms.Normalize((0.4467, 0.4398, 0.4066), (0.2603, 0.2566, 0.2713))
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize((0.4467, 0.4398, 0.4066), (0.2603, 0.2566, 0.2713))
+ ])
+
+ self.dataset = datasets.STL10(root=root, split=split, transform=transform, download=download)
+ self.classes = ['airplane', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck']
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+def get_dataset(name='cifar10', root='./data', train=True, download=True):
+ name = name.lower()
+
+ if name == 'cifar10':
+ return CIFAR10Dataset(root=root, train=train, download=download)
+ elif name == 'cifar100':
+ return CIFAR100Dataset(root=root, train=train, download=download)
+ elif name == 'mnist':
+ return MNISTDataset(root=root, train=train, download=download)
+ elif name == 'fashion_mnist' or name == 'fashionmnist':
+ return FashionMNISTDataset(root=root, train=train, download=download)
+ elif name == 'stl10':
+ split = 'train' if train else 'test'
+ return STL10Dataset(root=root, split=split, download=download)
+ else:
+ raise ValueError(f"Unknown dataset: {name}")
+
+class ImageNetDataset:
+ def __init__(self, root='./data/imagenet', split='train', transform=None, download=False):
+ if transform is None:
+ if split == 'train':
+ transform = transforms.Compose([
+ transforms.RandomResizedCrop(224),
+ transforms.RandomHorizontalFlip(),
+ transforms.ColorJitter(0.4, 0.4, 0.4),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ try:
+ self.dataset = datasets.ImageFolder(os.path.join(root, split), transform=transform)
+ except:
+ print(f"ImageNet not found at {root}. Please download manually from https://image-net.org/")
+ print("Expected structure: {root}/train/ and {root}/val/")
+ raise
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class TinyImageNetDataset:
+ def __init__(self, root='./data', train=True, transform=None, download=True):
+ if transform is None:
+ if train:
+ transform = transforms.Compose([
+ transforms.RandomCrop(64, padding=8),
+ transforms.RandomHorizontalFlip(),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ import zipfile
+ import urllib.request
+
+ data_dir = os.path.join(root, 'tiny-imagenet-200')
+ if download and not os.path.exists(data_dir):
+ print("Downloading Tiny ImageNet (237 MB)...")
+ url = 'http://cs231n.stanford.edu/tiny-imagenet-200.zip'
+ zip_path = os.path.join(root, 'tiny-imagenet-200.zip')
+
+ try:
+ urllib.request.urlretrieve(url, zip_path)
+ print("Extracting...")
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(root)
+ os.remove(zip_path)
+ except Exception as e:
+ print(f"Download failed: {e}")
+ print("Please download manually from: http://cs231n.stanford.edu/tiny-imagenet-200.zip")
+
+ split = 'train' if train else 'val'
+ self.dataset = datasets.ImageFolder(os.path.join(data_dir, split), transform=transform)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class Food101Dataset:
+ def __init__(self, root='./data', split='train', transform=None, download=True):
+ if transform is None:
+ if split == 'train':
+ transform = transforms.Compose([
+ transforms.RandomResizedCrop(224),
+ transforms.RandomHorizontalFlip(),
+ transforms.RandomRotation(15),
+ transforms.ColorJitter(0.3, 0.3, 0.3),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+ else:
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ self.dataset = datasets.Food101(root=root, split=split, transform=transform, download=download)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class Caltech256Dataset:
+ def __init__(self, root='./data', transform=None, download=True):
+ if transform is None:
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ self.dataset = datasets.Caltech256(root=root, transform=transform, download=download)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+class OxfordPetsDataset:
+ def __init__(self, root='./data', split='trainval', transform=None, download=True):
+ if transform is None:
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ self.dataset = datasets.OxfordIIITPet(root=root, split=split, transform=transform, download=download)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, idx):
+ return self.dataset[idx]
+
+def get_dataset(name='cifar10', root='./data', train=True, download=True):
+ name = name.lower()
+
+ if name == 'cifar10':
+ return CIFAR10Dataset(root=root, train=train, download=download)
+ elif name == 'cifar100':
+ return CIFAR100Dataset(root=root, train=train, download=download)
+ elif name == 'mnist':
+ return MNISTDataset(root=root, train=train, download=download)
+ elif name == 'fashion_mnist' or name == 'fashionmnist':
+ return FashionMNISTDataset(root=root, train=train, download=download)
+ elif name == 'stl10':
+ split = 'train' if train else 'test'
+ return STL10Dataset(root=root, split=split, download=download)
+ elif name == 'tiny_imagenet' or name == 'tinyimagenet':
+ return TinyImageNetDataset(root=root, train=train, download=download)
+ elif name == 'imagenet':
+ split = 'train' if train else 'val'
+ return ImageNetDataset(root=root, split=split, download=download)
+ elif name == 'food101':
+ split = 'train' if train else 'test'
+ return Food101Dataset(root=root, split=split, download=download)
+ elif name == 'caltech256':
+ return Caltech256Dataset(root=root, download=download)
+ elif name == 'oxford_pets' or name == 'oxfordpets':
+ split = 'trainval' if train else 'test'
+ return OxfordPetsDataset(root=root, split=split, download=download)
+ else:
+ raise ValueError(f"Unknown dataset: {name}")
+
+def get_num_classes(dataset_name):
+ dataset_name = dataset_name.lower()
+ if dataset_name in ['cifar10', 'mnist', 'fashion_mnist', 'fashionmnist', 'stl10']:
+ return 10
+ elif dataset_name == 'cifar100':
+ return 100
+ elif dataset_name in ['tiny_imagenet', 'tinyimagenet']:
+ return 200
+ elif dataset_name == 'imagenet':
+ return 1000
+ elif dataset_name == 'food101':
+ return 101
+ elif dataset_name == 'caltech256':
+ return 257
+ elif dataset_name in ['oxford_pets', 'oxfordpets']:
+ return 37
+ else:
+ return 10
+
+
+def get_class_names(dataset_name):
+ """Get class names for a dataset"""
+ dataset_name = dataset_name.lower()
+
+ class_names_map = {
+ 'cifar10': ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'],
+ 'mnist': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ 'fashion_mnist': ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'],
+ 'fashionmnist': ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'],
+ 'stl10': ['airplane', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck'],
+ }
+
+ if dataset_name in class_names_map:
+ return class_names_map[dataset_name]
+
+ # For other datasets, return generic class names
+ num_classes = get_num_classes(dataset_name)
+ return [f'class_{i}' for i in range(num_classes)]
diff --git a/ML/src/python/neuralforge/data/transforms.py b/ML/src/python/neuralforge/data/transforms.py
new file mode 100644
index 00000000000..f49e53b41e1
--- /dev/null
+++ b/ML/src/python/neuralforge/data/transforms.py
@@ -0,0 +1,108 @@
+from torchvision import transforms
+import torch
+from typing import List, Tuple
+
+def get_transforms(image_size: int = 224, is_training: bool = True, mean=None, std=None):
+ if mean is None:
+ mean = [0.485, 0.456, 0.406]
+ if std is None:
+ std = [0.229, 0.224, 0.225]
+
+ if is_training:
+ return transforms.Compose([
+ transforms.RandomResizedCrop(image_size, scale=(0.8, 1.0)),
+ transforms.RandomHorizontalFlip(p=0.5),
+ transforms.RandomVerticalFlip(p=0.1),
+ transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
+ transforms.RandomRotation(15),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=mean, std=std),
+ transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3))
+ ])
+ else:
+ return transforms.Compose([
+ transforms.Resize(int(image_size * 1.14)),
+ transforms.CenterCrop(image_size),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=mean, std=std)
+ ])
+
+class RandomMixup:
+ def __init__(self, alpha: float = 1.0):
+ self.alpha = alpha
+
+ def __call__(self, batch):
+ if self.alpha > 0:
+ lam = torch.distributions.Beta(self.alpha, self.alpha).sample()
+ else:
+ lam = 1.0
+
+ batch_size = batch[0].size(0)
+ index = torch.randperm(batch_size)
+
+ mixed_input = lam * batch[0] + (1 - lam) * batch[0][index, :]
+ y_a, y_b = batch[1], batch[1][index]
+
+ return mixed_input, y_a, y_b, lam
+
+class RandomCutmix:
+ def __init__(self, alpha: float = 1.0):
+ self.alpha = alpha
+
+ def __call__(self, batch):
+ images, labels = batch
+ batch_size = images.size(0)
+ index = torch.randperm(batch_size)
+
+ if self.alpha > 0:
+ lam = torch.distributions.Beta(self.alpha, self.alpha).sample()
+ else:
+ lam = 1.0
+
+ _, _, H, W = images.shape
+ cut_rat = torch.sqrt(1.0 - lam)
+ cut_w = (W * cut_rat).int()
+ cut_h = (H * cut_rat).int()
+
+ cx = torch.randint(W, (1,)).item()
+ cy = torch.randint(H, (1,)).item()
+
+ bbx1 = torch.clamp(cx - cut_w // 2, 0, W)
+ bby1 = torch.clamp(cy - cut_h // 2, 0, H)
+ bbx2 = torch.clamp(cx + cut_w // 2, 0, W)
+ bby2 = torch.clamp(cy + cut_h // 2, 0, H)
+
+ images[:, :, bby1:bby2, bbx1:bbx2] = images[index, :, bby1:bby2, bbx1:bbx2]
+
+ lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))
+
+ return images, labels, labels[index], lam
+
+class GaussianNoise:
+ def __init__(self, mean: float = 0.0, std: float = 0.1):
+ self.mean = mean
+ self.std = std
+
+ def __call__(self, tensor):
+ return tensor + torch.randn(tensor.size()) * self.std + self.mean
+
+class RandomGaussianBlur:
+ def __init__(self, kernel_size: int = 5, sigma: Tuple[float, float] = (0.1, 2.0)):
+ self.kernel_size = kernel_size
+ self.sigma = sigma
+
+ def __call__(self, img):
+ return transforms.GaussianBlur(self.kernel_size, self.sigma)(img)
+
+def get_strong_augmentation(image_size: int = 224):
+ return transforms.Compose([
+ transforms.RandomResizedCrop(image_size, scale=(0.5, 1.0)),
+ transforms.RandomHorizontalFlip(),
+ transforms.RandomApply([
+ transforms.ColorJitter(0.4, 0.4, 0.4, 0.2)
+ ], p=0.8),
+ transforms.RandomGrayscale(p=0.2),
+ transforms.RandomApply([transforms.GaussianBlur(kernel_size=23)], p=0.5),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
diff --git a/ML/src/python/neuralforge/models/__init__.py b/ML/src/python/neuralforge/models/__init__.py
new file mode 100644
index 00000000000..5d48e87b3e3
--- /dev/null
+++ b/ML/src/python/neuralforge/models/__init__.py
@@ -0,0 +1,11 @@
+from .resnet import ResNet18, ResNet34, ResNet50
+from .efficientnet import EfficientNetB0
+from .vit import VisionTransformer
+
+__all__ = [
+ 'ResNet18',
+ 'ResNet34',
+ 'ResNet50',
+ 'EfficientNetB0',
+ 'VisionTransformer',
+]
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/models/efficientnet.py b/ML/src/python/neuralforge/models/efficientnet.py
new file mode 100644
index 00000000000..6da47702cde
--- /dev/null
+++ b/ML/src/python/neuralforge/models/efficientnet.py
@@ -0,0 +1,43 @@
+import torch.nn as nn
+from ..nn.convolution import EfficientNetBlock
+
+class EfficientNetB0(nn.Module):
+ def __init__(self, num_classes=1000):
+ super().__init__()
+
+ self.stem = nn.Sequential(
+ nn.Conv2d(3, 32, 3, stride=2, padding=1, bias=False),
+ nn.BatchNorm2d(32),
+ nn.SiLU(inplace=True)
+ )
+
+ self.blocks = nn.Sequential(
+ EfficientNetBlock(32, 16, 3, 1, 1),
+ EfficientNetBlock(16, 24, 3, 2, 6),
+ EfficientNetBlock(24, 24, 3, 1, 6),
+ EfficientNetBlock(24, 40, 5, 2, 6),
+ EfficientNetBlock(40, 40, 5, 1, 6),
+ EfficientNetBlock(40, 80, 3, 2, 6),
+ EfficientNetBlock(80, 80, 3, 1, 6),
+ EfficientNetBlock(80, 112, 5, 1, 6),
+ EfficientNetBlock(112, 112, 5, 1, 6),
+ EfficientNetBlock(112, 192, 5, 2, 6),
+ EfficientNetBlock(192, 192, 5, 1, 6),
+ EfficientNetBlock(192, 320, 3, 1, 6),
+ )
+
+ self.head = nn.Sequential(
+ nn.Conv2d(320, 1280, 1, bias=False),
+ nn.BatchNorm2d(1280),
+ nn.SiLU(inplace=True),
+ nn.AdaptiveAvgPool2d(1),
+ nn.Flatten(),
+ nn.Dropout(0.2),
+ nn.Linear(1280, num_classes)
+ )
+
+ def forward(self, x):
+ x = self.stem(x)
+ x = self.blocks(x)
+ x = self.head(x)
+ return x
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/models/resnet.py b/ML/src/python/neuralforge/models/resnet.py
new file mode 100644
index 00000000000..417077e0dd4
--- /dev/null
+++ b/ML/src/python/neuralforge/models/resnet.py
@@ -0,0 +1,15 @@
+import torch.nn as nn
+from ..nn.convolution import ResNetBlock
+
+def ResNet18(num_classes=1000, in_channels=3):
+ from ..nn.convolution import ResNet
+ return ResNet(ResNetBlock, [2, 2, 2, 2], num_classes, in_channels)
+
+def ResNet34(num_classes=1000, in_channels=3):
+ from ..nn.convolution import ResNet
+ return ResNet(ResNetBlock, [3, 4, 6, 3], num_classes, in_channels)
+
+def ResNet50(num_classes=1000, in_channels=3):
+ from ..nn.layers import BottleneckBlock
+ from ..nn.convolution import ResNet
+ return ResNet(BottleneckBlock, [3, 4, 6, 3], num_classes, in_channels)
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/models/vit.py b/ML/src/python/neuralforge/models/vit.py
new file mode 100644
index 00000000000..9ac34c075c8
--- /dev/null
+++ b/ML/src/python/neuralforge/models/vit.py
@@ -0,0 +1,24 @@
+import torch.nn as nn
+from ..nn.attention import VisionTransformerBlock
+
+def VisionTransformer(
+ img_size=224,
+ patch_size=16,
+ in_channels=3,
+ num_classes=1000,
+ embed_dim=768,
+ depth=12,
+ num_heads=12,
+ mlp_ratio=4.0,
+ dropout=0.1
+):
+ return VisionTransformerBlock(
+ img_size=img_size,
+ patch_size=patch_size,
+ in_channels=in_channels,
+ embed_dim=embed_dim,
+ num_heads=num_heads,
+ num_layers=depth,
+ num_classes=num_classes,
+ dropout=dropout
+ )
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/nas/__init__.py b/ML/src/python/neuralforge/nas/__init__.py
new file mode 100644
index 00000000000..46ae660539c
--- /dev/null
+++ b/ML/src/python/neuralforge/nas/__init__.py
@@ -0,0 +1,10 @@
+from .search_space import *
+from .evolution import *
+from .evaluator import *
+
+__all__ = [
+ 'SearchSpace',
+ 'EvolutionarySearch',
+ 'ModelEvaluator',
+ 'Architecture',
+]
diff --git a/ML/src/python/neuralforge/nas/evaluator.py b/ML/src/python/neuralforge/nas/evaluator.py
new file mode 100644
index 00000000000..735d61cb0d8
--- /dev/null
+++ b/ML/src/python/neuralforge/nas/evaluator.py
@@ -0,0 +1,142 @@
+import torch
+import torch.nn as nn
+from torch.utils.data import DataLoader, Subset
+import time
+from typing import Tuple
+from .search_space import SearchSpace, Architecture
+
+class ModelEvaluator:
+ def __init__(
+ self,
+ train_loader: DataLoader,
+ val_loader: DataLoader,
+ device: str = 'cuda',
+ epochs: int = 5,
+ quick_eval: bool = True
+ ):
+ self.train_loader = train_loader
+ self.val_loader = val_loader
+ self.device = device
+ self.epochs = epochs
+ self.quick_eval = quick_eval
+
+ def evaluate(self, architecture: Architecture, search_space: SearchSpace) -> Tuple[float, float]:
+ try:
+ model = search_space.build_model(architecture)
+ model = model.to(self.device)
+
+ criterion = nn.CrossEntropyLoss()
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
+
+ if self.quick_eval:
+ accuracy = self._quick_evaluate(model, criterion, optimizer)
+ else:
+ accuracy = self._full_evaluate(model, criterion, optimizer)
+
+ complexity = search_space.estimate_complexity(architecture)
+ params = complexity['params']
+ flops = complexity['flops']
+
+ param_penalty = params / 1e7
+ flop_penalty = flops / 1e9
+
+ fitness = accuracy - 0.1 * param_penalty - 0.05 * flop_penalty
+
+ return fitness, accuracy
+
+ except Exception as e:
+ print(f"Error evaluating architecture: {e}")
+ return 0.0, 0.0
+
+ def _quick_evaluate(self, model: nn.Module, criterion: nn.Module, optimizer: torch.optim.Optimizer) -> float:
+ model.train()
+
+ num_batches = min(50, len(self.train_loader))
+
+ for epoch in range(self.epochs):
+ for batch_idx, (inputs, targets) in enumerate(self.train_loader):
+ if batch_idx >= num_batches:
+ break
+
+ inputs = inputs.to(self.device)
+ targets = targets.to(self.device)
+
+ optimizer.zero_grad()
+ outputs = model(inputs)
+ loss = criterion(outputs, targets)
+ loss.backward()
+ optimizer.step()
+
+ model.eval()
+ correct = 0
+ total = 0
+
+ num_val_batches = min(20, len(self.val_loader))
+
+ with torch.no_grad():
+ for batch_idx, (inputs, targets) in enumerate(self.val_loader):
+ if batch_idx >= num_val_batches:
+ break
+
+ inputs = inputs.to(self.device)
+ targets = targets.to(self.device)
+
+ outputs = model(inputs)
+ _, predicted = outputs.max(1)
+ total += targets.size(0)
+ correct += predicted.eq(targets).sum().item()
+
+ accuracy = 100.0 * correct / total if total > 0 else 0.0
+ return accuracy
+
+ def _full_evaluate(self, model: nn.Module, criterion: nn.Module, optimizer: torch.optim.Optimizer) -> float:
+ for epoch in range(self.epochs):
+ model.train()
+
+ for inputs, targets in self.train_loader:
+ inputs = inputs.to(self.device)
+ targets = targets.to(self.device)
+
+ optimizer.zero_grad()
+ outputs = model(inputs)
+ loss = criterion(outputs, targets)
+ loss.backward()
+ optimizer.step()
+
+ model.eval()
+ correct = 0
+ total = 0
+
+ with torch.no_grad():
+ for inputs, targets in self.val_loader:
+ inputs = inputs.to(self.device)
+ targets = targets.to(self.device)
+
+ outputs = model(inputs)
+ _, predicted = outputs.max(1)
+ total += targets.size(0)
+ correct += predicted.eq(targets).sum().item()
+
+ accuracy = 100.0 * correct / total if total > 0 else 0.0
+ return accuracy
+
+class ProxyEvaluator:
+ def __init__(self, device: str = 'cuda'):
+ self.device = device
+
+ def evaluate(self, architecture: Architecture, search_space: SearchSpace) -> Tuple[float, float]:
+ model = search_space.build_model(architecture)
+ model = model.to(self.device)
+
+ complexity = search_space.estimate_complexity(architecture)
+ params = complexity['params']
+ flops = complexity['flops']
+
+ num_layers = len([g for g in architecture.genome if g.get('type') != 'pooling'])
+
+ estimated_accuracy = 60.0 + torch.rand(1).item() * 20.0
+ estimated_accuracy = min(95.0, estimated_accuracy - params / 1e8)
+
+ fitness = estimated_accuracy - 0.1 * (params / 1e7) - 0.05 * (flops / 1e9)
+
+ return fitness, estimated_accuracy
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/nas/evolution.py b/ML/src/python/neuralforge/nas/evolution.py
new file mode 100644
index 00000000000..b46ff03703b
--- /dev/null
+++ b/ML/src/python/neuralforge/nas/evolution.py
@@ -0,0 +1,129 @@
+import torch
+import random
+import numpy as np
+from typing import List, Dict, Any
+from tqdm import tqdm
+from .search_space import SearchSpace, Architecture
+from .evaluator import ModelEvaluator
+
+class EvolutionarySearch:
+ def __init__(
+ self,
+ search_space: SearchSpace,
+ evaluator: ModelEvaluator,
+ population_size: int = 20,
+ generations: int = 50,
+ mutation_rate: float = 0.1,
+ crossover_rate: float = 0.5,
+ tournament_size: int = 3
+ ):
+ self.search_space = search_space
+ self.evaluator = evaluator
+ self.population_size = population_size
+ self.generations = generations
+ self.mutation_rate = mutation_rate
+ self.crossover_rate = crossover_rate
+ self.tournament_size = tournament_size
+
+ self.population = []
+ self.best_architecture = None
+ self.history = []
+
+ def initialize_population(self):
+ print(f"Initializing population of {self.population_size} architectures...")
+ self.population = []
+
+ for i in range(self.population_size):
+ arch = self.search_space.random_architecture()
+ self.population.append(arch)
+
+ print("Population initialized successfully")
+
+ def evaluate_population(self):
+ print("Evaluating population...")
+
+ for arch in tqdm(self.population, desc="Evaluating architectures"):
+ if arch.fitness == 0.0:
+ fitness, accuracy = self.evaluator.evaluate(arch, self.search_space)
+ arch.fitness = fitness
+ arch.accuracy = accuracy
+
+ complexity = self.search_space.estimate_complexity(arch)
+ arch.params = complexity['params']
+ arch.flops = complexity['flops']
+
+ def tournament_selection(self) -> Architecture:
+ tournament = random.sample(self.population, self.tournament_size)
+ return max(tournament, key=lambda x: x.fitness)
+
+ def select_parents(self) -> List[Architecture]:
+ parent1 = self.tournament_selection()
+ parent2 = self.tournament_selection()
+ return [parent1, parent2]
+
+ def create_offspring(self, parents: List[Architecture]) -> Architecture:
+ if random.random() < self.crossover_rate:
+ offspring = self.search_space.crossover(parents[0], parents[1])
+ else:
+ offspring = Architecture(parents[0].genome.copy())
+
+ if random.random() < self.mutation_rate:
+ offspring = self.search_space.mutate(offspring, self.mutation_rate)
+
+ return offspring
+
+ def evolve_generation(self):
+ self.population.sort(key=lambda x: x.fitness, reverse=True)
+
+ elite_size = max(1, self.population_size // 10)
+ new_population = self.population[:elite_size]
+
+ while len(new_population) < self.population_size:
+ parents = self.select_parents()
+ offspring = self.create_offspring(parents)
+ new_population.append(offspring)
+
+ self.population = new_population
+
+ def search(self) -> Architecture:
+ print(f"Starting evolutionary search for {self.generations} generations...")
+
+ self.initialize_population()
+ self.evaluate_population()
+
+ for generation in range(self.generations):
+ print(f"\n=== Generation {generation + 1}/{self.generations} ===")
+
+ self.population.sort(key=lambda x: x.fitness, reverse=True)
+ best_arch = self.population[0]
+
+ if self.best_architecture is None or best_arch.fitness > self.best_architecture.fitness:
+ self.best_architecture = best_arch
+
+ avg_fitness = np.mean([arch.fitness for arch in self.population])
+ avg_accuracy = np.mean([arch.accuracy for arch in self.population])
+
+ print(f"Best fitness: {best_arch.fitness:.4f}")
+ print(f"Best accuracy: {best_arch.accuracy:.2f}%")
+ print(f"Avg fitness: {avg_fitness:.4f}")
+ print(f"Avg accuracy: {avg_accuracy:.2f}%")
+ print(f"Best params: {best_arch.params:,}")
+
+ self.history.append({
+ 'generation': generation + 1,
+ 'best_fitness': best_arch.fitness,
+ 'best_accuracy': best_arch.accuracy,
+ 'avg_fitness': avg_fitness,
+ 'avg_accuracy': avg_accuracy,
+ })
+
+ if generation < self.generations - 1:
+ self.evolve_generation()
+ self.evaluate_population()
+
+ print(f"\nSearch completed! Best architecture: {self.best_architecture}")
+ return self.best_architecture
+
+ def get_top_k_architectures(self, k: int = 5) -> List[Architecture]:
+ self.population.sort(key=lambda x: x.fitness, reverse=True)
+ return self.population[:k]
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/nas/search_space.py b/ML/src/python/neuralforge/nas/search_space.py
new file mode 100644
index 00000000000..1a6fac8136e
--- /dev/null
+++ b/ML/src/python/neuralforge/nas/search_space.py
@@ -0,0 +1,181 @@
+import torch
+import torch.nn as nn
+from typing import List, Dict, Any, Optional
+import random
+import numpy as np
+
+class Architecture:
+ def __init__(self, genome: List[int]):
+ self.genome = genome
+ self.fitness = 0.0
+ self.accuracy = 0.0
+ self.params = 0
+ self.flops = 0
+
+ def __repr__(self):
+ return f"Architecture(fitness={self.fitness:.4f}, acc={self.accuracy:.2f}%, params={self.params})"
+
+class SearchSpace:
+ def __init__(self, config: Dict[str, Any]):
+ self.config = config
+
+ self.layer_types = ['conv3x3', 'conv5x5', 'conv7x7', 'depthwise', 'bottleneck', 'identity']
+ self.activation_types = ['relu', 'gelu', 'silu', 'mish']
+ self.pooling_types = ['max', 'avg', 'none']
+ self.channels = [32, 64, 128, 256, 512]
+
+ self.num_layers = config.get('num_layers', 20)
+ self.num_blocks = config.get('num_blocks', 5)
+
+ def random_architecture(self) -> Architecture:
+ genome = []
+
+ for block_idx in range(self.num_blocks):
+ num_layers_in_block = random.randint(2, 5)
+
+ for layer_idx in range(num_layers_in_block):
+ layer_gene = {
+ 'type': random.choice(self.layer_types),
+ 'channels': random.choice(self.channels),
+ 'activation': random.choice(self.activation_types),
+ 'use_bn': random.choice([True, False]),
+ 'dropout': random.uniform(0.0, 0.3),
+ }
+ genome.append(layer_gene)
+
+ pooling_gene = {
+ 'type': 'pooling',
+ 'pooling_type': random.choice(self.pooling_types),
+ }
+ genome.append(pooling_gene)
+
+ return Architecture(genome)
+
+ def build_model(self, architecture: Architecture, input_channels: int = 3, num_classes: int = 1000) -> nn.Module:
+ layers = []
+ current_channels = input_channels
+
+ for gene in architecture.genome:
+ if gene.get('type') == 'pooling':
+ if gene['pooling_type'] == 'max':
+ layers.append(nn.MaxPool2d(2))
+ elif gene['pooling_type'] == 'avg':
+ layers.append(nn.AvgPool2d(2))
+ else:
+ layer_type = gene['type']
+ out_channels = gene['channels']
+ activation = gene['activation']
+ use_bn = gene['use_bn']
+ dropout = gene['dropout']
+
+ if layer_type == 'conv3x3':
+ layers.append(nn.Conv2d(current_channels, out_channels, 3, padding=1))
+ elif layer_type == 'conv5x5':
+ layers.append(nn.Conv2d(current_channels, out_channels, 5, padding=2))
+ elif layer_type == 'conv7x7':
+ layers.append(nn.Conv2d(current_channels, out_channels, 7, padding=3))
+ elif layer_type == 'depthwise':
+ layers.append(nn.Conv2d(current_channels, current_channels, 3, padding=1, groups=current_channels))
+ layers.append(nn.Conv2d(current_channels, out_channels, 1))
+ elif layer_type == 'bottleneck':
+ mid_channels = out_channels // 4
+ layers.append(nn.Conv2d(current_channels, mid_channels, 1))
+ if use_bn:
+ layers.append(nn.BatchNorm2d(mid_channels))
+ layers.append(self._get_activation(activation))
+ layers.append(nn.Conv2d(mid_channels, mid_channels, 3, padding=1))
+ if use_bn:
+ layers.append(nn.BatchNorm2d(mid_channels))
+ layers.append(self._get_activation(activation))
+ layers.append(nn.Conv2d(mid_channels, out_channels, 1))
+ elif layer_type == 'identity':
+ if current_channels != out_channels:
+ layers.append(nn.Conv2d(current_channels, out_channels, 1))
+ else:
+ layers.append(nn.Identity())
+
+ if use_bn and layer_type != 'bottleneck':
+ layers.append(nn.BatchNorm2d(out_channels))
+
+ if layer_type != 'bottleneck':
+ layers.append(self._get_activation(activation))
+
+ if dropout > 0:
+ layers.append(nn.Dropout2d(dropout))
+
+ current_channels = out_channels
+
+ layers.append(nn.AdaptiveAvgPool2d(1))
+ layers.append(nn.Flatten())
+ layers.append(nn.Linear(current_channels, num_classes))
+
+ model = nn.Sequential(*layers)
+ return model
+
+ def _get_activation(self, activation: str) -> nn.Module:
+ if activation == 'relu':
+ return nn.ReLU(inplace=True)
+ elif activation == 'gelu':
+ return nn.GELU()
+ elif activation == 'silu':
+ return nn.SiLU(inplace=True)
+ elif activation == 'mish':
+ return nn.Mish(inplace=True)
+ else:
+ return nn.ReLU(inplace=True)
+
+ def mutate(self, architecture: Architecture, mutation_rate: float = 0.1) -> Architecture:
+ new_genome = []
+
+ for gene in architecture.genome:
+ if random.random() < mutation_rate:
+ if gene.get('type') == 'pooling':
+ gene = gene.copy()
+ gene['pooling_type'] = random.choice(self.pooling_types)
+ else:
+ gene = gene.copy()
+ gene['type'] = random.choice(self.layer_types)
+ gene['channels'] = random.choice(self.channels)
+ gene['activation'] = random.choice(self.activation_types)
+
+ new_genome.append(gene)
+
+ return Architecture(new_genome)
+
+ def crossover(self, parent1: Architecture, parent2: Architecture) -> Architecture:
+ min_len = min(len(parent1.genome), len(parent2.genome))
+ crossover_point = random.randint(1, min_len - 1)
+
+ child_genome = parent1.genome[:crossover_point] + parent2.genome[crossover_point:]
+
+ return Architecture(child_genome)
+
+ def estimate_complexity(self, architecture: Architecture, input_size: int = 224) -> Dict[str, float]:
+ total_params = 0
+ total_flops = 0
+ current_channels = 3
+ current_size = input_size
+
+ for gene in architecture.genome:
+ if gene.get('type') == 'pooling':
+ current_size = current_size // 2
+ else:
+ out_channels = gene['channels']
+
+ if gene['type'] in ['conv3x3', 'conv5x5', 'conv7x7']:
+ kernel_size = int(gene['type'][-3])
+ params = current_channels * out_channels * kernel_size * kernel_size
+ flops = params * current_size * current_size
+ elif gene['type'] == 'depthwise':
+ params = current_channels * 9 + current_channels * out_channels
+ flops = current_channels * 9 * current_size * current_size + current_channels * out_channels * current_size * current_size
+ elif gene['type'] == 'bottleneck':
+ mid_channels = out_channels // 4
+ params = current_channels * mid_channels + mid_channels * 9 + mid_channels * out_channels
+ flops = (current_channels * mid_channels + mid_channels * 9 + mid_channels * out_channels) * current_size * current_size
+
+ total_params += params
+ total_flops += flops
+ current_channels = out_channels
+
+ return {'params': total_params, 'flops': total_flops}
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/nn/__init__.py b/ML/src/python/neuralforge/nn/__init__.py
new file mode 100644
index 00000000000..c7bc6859afc
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/__init__.py
@@ -0,0 +1,18 @@
+from .modules import *
+from .layers import *
+from .attention import *
+from .convolution import *
+from .activations import *
+
+__all__ = [
+ 'TransformerBlock',
+ 'MultiHeadAttention',
+ 'FeedForward',
+ 'ResNetBlock',
+ 'DenseBlock',
+ 'ConvBlock',
+ 'SEBlock',
+ 'GELU',
+ 'Swish',
+ 'Mish',
+]
diff --git a/ML/src/python/neuralforge/nn/activations.py b/ML/src/python/neuralforge/nn/activations.py
new file mode 100644
index 00000000000..0a36438da5c
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/activations.py
@@ -0,0 +1,122 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+class GELU(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return 0.5 * x * (1.0 + torch.tanh(0.7978845608 * (x + 0.044715 * torch.pow(x, 3))))
+
+class Swish(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x * torch.sigmoid(x)
+
+class Mish(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x * torch.tanh(F.softplus(x))
+
+class HardSwish(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x * F.hardtanh(x + 3, 0.0, 6.0) / 6.0
+
+class HardSigmoid(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return F.relu6(x + 3.0) / 6.0
+
+class FReLU(nn.Module):
+ def __init__(self, channels, kernel_size=3):
+ super().__init__()
+ self.conv = nn.Conv2d(channels, channels, kernel_size, padding=kernel_size // 2, groups=channels)
+ self.bn = nn.BatchNorm2d(channels)
+
+ def forward(self, x):
+ tx = self.bn(self.conv(x))
+ return torch.max(x, tx)
+
+class GLU(nn.Module):
+ def __init__(self, dim=-1):
+ super().__init__()
+ self.dim = dim
+
+ def forward(self, x):
+ a, b = x.chunk(2, dim=self.dim)
+ return a * torch.sigmoid(b)
+
+class ReGLU(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ a, b = x.chunk(2, dim=-1)
+ return a * F.relu(b)
+
+class GEGLU(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.gelu = GELU()
+
+ def forward(self, x):
+ a, b = x.chunk(2, dim=-1)
+ return a * self.gelu(b)
+
+class SiLU(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x * torch.sigmoid(x)
+
+class ELU(nn.Module):
+ def __init__(self, alpha=1.0):
+ super().__init__()
+ self.alpha = alpha
+
+ def forward(self, x):
+ return torch.where(x > 0, x, self.alpha * (torch.exp(x) - 1))
+
+class SELU(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.alpha = 1.6732632423543772848170429916717
+ self.scale = 1.0507009873554804934193349852946
+
+ def forward(self, x):
+ return self.scale * torch.where(x > 0, x, self.alpha * (torch.exp(x) - 1))
+
+class PReLU(nn.Module):
+ def __init__(self, num_parameters=1, init=0.25):
+ super().__init__()
+ self.weight = nn.Parameter(torch.ones(num_parameters) * init)
+
+ def forward(self, x):
+ return torch.where(x > 0, x, self.weight * x)
+
+class LeakyReLU(nn.Module):
+ def __init__(self, negative_slope=0.01):
+ super().__init__()
+ self.negative_slope = negative_slope
+
+ def forward(self, x):
+ return F.leaky_relu(x, self.negative_slope)
+
+class Softplus(nn.Module):
+ def __init__(self, beta=1):
+ super().__init__()
+ self.beta = beta
+
+ def forward(self, x):
+ return F.softplus(x, self.beta)
diff --git a/ML/src/python/neuralforge/nn/attention.py b/ML/src/python/neuralforge/nn/attention.py
new file mode 100644
index 00000000000..47fb9cd8db6
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/attention.py
@@ -0,0 +1,207 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import math
+from typing import Optional
+
+class MultiHeadAttention(nn.Module):
+ def __init__(self, embed_dim, num_heads, dropout=0.1, bias=True):
+ super().__init__()
+ assert embed_dim % num_heads == 0
+
+ self.embed_dim = embed_dim
+ self.num_heads = num_heads
+ self.head_dim = embed_dim // num_heads
+ self.scale = self.head_dim ** -0.5
+
+ self.qkv = nn.Linear(embed_dim, embed_dim * 3, bias=bias)
+ self.proj = nn.Linear(embed_dim, embed_dim, bias=bias)
+ self.dropout = nn.Dropout(dropout)
+
+ def forward(self, x, mask=None):
+ B, N, C = x.shape
+
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
+ q, k, v = qkv[0], qkv[1], qkv[2]
+
+ attn = (q @ k.transpose(-2, -1)) * self.scale
+
+ if mask is not None:
+ attn = attn.masked_fill(mask == 0, float('-inf'))
+
+ attn = F.softmax(attn, dim=-1)
+ attn = self.dropout(attn)
+
+ x = (attn @ v).transpose(1, 2).reshape(B, N, C)
+ x = self.proj(x)
+ x = self.dropout(x)
+
+ return x
+
+class CrossAttention(nn.Module):
+ def __init__(self, embed_dim, num_heads, dropout=0.1):
+ super().__init__()
+ self.embed_dim = embed_dim
+ self.num_heads = num_heads
+ self.head_dim = embed_dim // num_heads
+ self.scale = self.head_dim ** -0.5
+
+ self.q_proj = nn.Linear(embed_dim, embed_dim)
+ self.k_proj = nn.Linear(embed_dim, embed_dim)
+ self.v_proj = nn.Linear(embed_dim, embed_dim)
+ self.out_proj = nn.Linear(embed_dim, embed_dim)
+ self.dropout = nn.Dropout(dropout)
+
+ def forward(self, query, key, value, mask=None):
+ B, N_q, C = query.shape
+ N_k = key.shape[1]
+
+ q = self.q_proj(query).reshape(B, N_q, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
+ k = self.k_proj(key).reshape(B, N_k, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
+ v = self.v_proj(value).reshape(B, N_k, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
+
+ attn = (q @ k.transpose(-2, -1)) * self.scale
+
+ if mask is not None:
+ attn = attn.masked_fill(mask == 0, float('-inf'))
+
+ attn = F.softmax(attn, dim=-1)
+ attn = self.dropout(attn)
+
+ x = (attn @ v).transpose(1, 2).reshape(B, N_q, C)
+ x = self.out_proj(x)
+
+ return x
+
+class FeedForward(nn.Module):
+ def __init__(self, embed_dim, hidden_dim, dropout=0.1, activation='gelu'):
+ super().__init__()
+ self.fc1 = nn.Linear(embed_dim, hidden_dim)
+ self.fc2 = nn.Linear(hidden_dim, embed_dim)
+ self.dropout = nn.Dropout(dropout)
+
+ if activation == 'gelu':
+ self.activation = nn.GELU()
+ elif activation == 'relu':
+ self.activation = nn.ReLU()
+ elif activation == 'silu':
+ self.activation = nn.SiLU()
+ else:
+ self.activation = nn.GELU()
+
+ def forward(self, x):
+ x = self.fc1(x)
+ x = self.activation(x)
+ x = self.dropout(x)
+ x = self.fc2(x)
+ x = self.dropout(x)
+ return x
+
+class TransformerBlock(nn.Module):
+ def __init__(self, embed_dim, num_heads, mlp_ratio=4.0, dropout=0.1, drop_path=0.0):
+ super().__init__()
+ self.norm1 = nn.LayerNorm(embed_dim)
+ self.attn = MultiHeadAttention(embed_dim, num_heads, dropout)
+ self.norm2 = nn.LayerNorm(embed_dim)
+ self.mlp = FeedForward(embed_dim, int(embed_dim * mlp_ratio), dropout)
+
+ from .modules import DropPath
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
+
+ def forward(self, x, mask=None):
+ x = x + self.drop_path(self.attn(self.norm1(x), mask))
+ x = x + self.drop_path(self.mlp(self.norm2(x)))
+ return x
+
+class TransformerEncoder(nn.Module):
+ def __init__(self, embed_dim, num_heads, num_layers, mlp_ratio=4.0, dropout=0.1):
+ super().__init__()
+ self.layers = nn.ModuleList([
+ TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout)
+ for _ in range(num_layers)
+ ])
+ self.norm = nn.LayerNorm(embed_dim)
+
+ def forward(self, x, mask=None):
+ for layer in self.layers:
+ x = layer(x, mask)
+ return self.norm(x)
+
+class VisionTransformerBlock(nn.Module):
+ def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768,
+ num_heads=12, num_layers=12, num_classes=1000, dropout=0.1):
+ super().__init__()
+ self.patch_size = patch_size
+ self.num_patches = (img_size // patch_size) ** 2
+
+ self.patch_embed = nn.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size)
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
+ self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches + 1, embed_dim))
+ self.dropout = nn.Dropout(dropout)
+
+ self.encoder = TransformerEncoder(embed_dim, num_heads, num_layers, dropout=dropout)
+ self.head = nn.Linear(embed_dim, num_classes)
+
+ nn.init.trunc_normal_(self.pos_embed, std=0.02)
+ nn.init.trunc_normal_(self.cls_token, std=0.02)
+
+ def forward(self, x):
+ B = x.shape[0]
+ x = self.patch_embed(x).flatten(2).transpose(1, 2)
+
+ cls_tokens = self.cls_token.expand(B, -1, -1)
+ x = torch.cat([cls_tokens, x], dim=1)
+ x = x + self.pos_embed
+ x = self.dropout(x)
+
+ x = self.encoder(x)
+ x = x[:, 0]
+ x = self.head(x)
+
+ return x
+
+class SelfAttention2D(nn.Module):
+ def __init__(self, in_channels):
+ super().__init__()
+ self.query = nn.Conv2d(in_channels, in_channels // 8, 1)
+ self.key = nn.Conv2d(in_channels, in_channels // 8, 1)
+ self.value = nn.Conv2d(in_channels, in_channels, 1)
+ self.gamma = nn.Parameter(torch.zeros(1))
+
+ def forward(self, x):
+ B, C, H, W = x.size()
+
+ query = self.query(x).view(B, -1, H * W).permute(0, 2, 1)
+ key = self.key(x).view(B, -1, H * W)
+ value = self.value(x).view(B, -1, H * W)
+
+ attention = F.softmax(torch.bmm(query, key), dim=-1)
+ out = torch.bmm(value, attention.permute(0, 2, 1))
+ out = out.view(B, C, H, W)
+
+ return self.gamma * out + x
+
+class LocalAttention(nn.Module):
+ def __init__(self, embed_dim, window_size=7, num_heads=8):
+ super().__init__()
+ self.embed_dim = embed_dim
+ self.window_size = window_size
+ self.num_heads = num_heads
+ self.head_dim = embed_dim // num_heads
+ self.scale = self.head_dim ** -0.5
+
+ self.qkv = nn.Linear(embed_dim, embed_dim * 3)
+ self.proj = nn.Linear(embed_dim, embed_dim)
+
+ def forward(self, x):
+ B, N, C = x.shape
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
+ q, k, v = qkv[0], qkv[1], qkv[2]
+
+ attn = (q @ k.transpose(-2, -1)) * self.scale
+ attn = F.softmax(attn, dim=-1)
+
+ x = (attn @ v).transpose(1, 2).reshape(B, N, C)
+ x = self.proj(x)
+
+ return x
diff --git a/ML/src/python/neuralforge/nn/convolution.py b/ML/src/python/neuralforge/nn/convolution.py
new file mode 100644
index 00000000000..f0755ba5bdd
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/convolution.py
@@ -0,0 +1,239 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from typing import List, Optional
+
+class ResNetBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, stride=1, downsample=None):
+ super().__init__()
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
+ self.bn1 = nn.BatchNorm2d(out_channels)
+ self.relu = nn.ReLU(inplace=True)
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
+ self.bn2 = nn.BatchNorm2d(out_channels)
+ self.downsample = downsample
+
+ def forward(self, x):
+ identity = x
+
+ out = self.conv1(x)
+ out = self.bn1(out)
+ out = self.relu(out)
+
+ out = self.conv2(out)
+ out = self.bn2(out)
+
+ if self.downsample is not None:
+ identity = self.downsample(x)
+
+ out += identity
+ out = self.relu(out)
+
+ return out
+
+class ResNet(nn.Module):
+ def __init__(self, block, layers, num_classes=1000, in_channels=3):
+ super().__init__()
+ self.in_channels = 64
+
+ self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False)
+ self.bn1 = nn.BatchNorm2d(64)
+ self.relu = nn.ReLU(inplace=True)
+ self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
+
+ self.layer1 = self._make_layer(block, 64, layers[0])
+ self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
+ self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
+ self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
+
+ self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
+ self.fc = nn.Linear(512, num_classes)
+
+ def _make_layer(self, block, out_channels, blocks, stride=1):
+ downsample = None
+ if stride != 1 or self.in_channels != out_channels:
+ downsample = nn.Sequential(
+ nn.Conv2d(self.in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
+ nn.BatchNorm2d(out_channels)
+ )
+
+ layers = []
+ layers.append(block(self.in_channels, out_channels, stride, downsample))
+ self.in_channels = out_channels
+
+ for _ in range(1, blocks):
+ layers.append(block(out_channels, out_channels))
+
+ return nn.Sequential(*layers)
+
+ def forward(self, x):
+ x = self.conv1(x)
+ x = self.bn1(x)
+ x = self.relu(x)
+ x = self.maxpool(x)
+
+ x = self.layer1(x)
+ x = self.layer2(x)
+ x = self.layer3(x)
+ x = self.layer4(x)
+
+ x = self.avgpool(x)
+ x = torch.flatten(x, 1)
+ x = self.fc(x)
+
+ return x
+
+class EfficientNetBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, kernel_size, stride, expand_ratio, se_ratio=0.25):
+ super().__init__()
+ self.stride = stride
+ self.use_residual = (stride == 1 and in_channels == out_channels)
+
+ hidden_dim = in_channels * expand_ratio
+ self.use_expansion = expand_ratio != 1
+
+ if self.use_expansion:
+ self.expand_conv = nn.Sequential(
+ nn.Conv2d(in_channels, hidden_dim, 1, bias=False),
+ nn.BatchNorm2d(hidden_dim),
+ nn.SiLU(inplace=True)
+ )
+
+ self.depthwise_conv = nn.Sequential(
+ nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, kernel_size // 2, groups=hidden_dim, bias=False),
+ nn.BatchNorm2d(hidden_dim),
+ nn.SiLU(inplace=True)
+ )
+
+ se_channels = max(1, int(in_channels * se_ratio))
+ self.se = nn.Sequential(
+ nn.AdaptiveAvgPool2d(1),
+ nn.Conv2d(hidden_dim, se_channels, 1),
+ nn.SiLU(inplace=True),
+ nn.Conv2d(se_channels, hidden_dim, 1),
+ nn.Sigmoid()
+ )
+
+ self.project_conv = nn.Sequential(
+ nn.Conv2d(hidden_dim, out_channels, 1, bias=False),
+ nn.BatchNorm2d(out_channels)
+ )
+
+ def forward(self, x):
+ identity = x
+
+ if self.use_expansion:
+ x = self.expand_conv(x)
+
+ x = self.depthwise_conv(x)
+
+ se_weight = self.se(x)
+ x = x * se_weight
+
+ x = self.project_conv(x)
+
+ if self.use_residual:
+ x = x + identity
+
+ return x
+
+class UNetBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, down=True):
+ super().__init__()
+ self.down = down
+
+ if down:
+ self.conv = nn.Sequential(
+ nn.Conv2d(in_channels, out_channels, 3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(out_channels, out_channels, 3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True)
+ )
+ self.pool = nn.MaxPool2d(2)
+ else:
+ self.conv = nn.Sequential(
+ nn.Conv2d(in_channels, out_channels, 3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(out_channels, out_channels, 3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True)
+ )
+ self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, 2, stride=2)
+
+ def forward(self, x, skip=None):
+ if self.down:
+ x = self.conv(x)
+ pool = self.pool(x)
+ return x, pool
+ else:
+ x = self.up(x)
+ if skip is not None:
+ x = torch.cat([x, skip], dim=1)
+ x = self.conv(x)
+ return x
+
+class ConvNeXtBlock(nn.Module):
+ def __init__(self, dim, drop_path=0.0, layer_scale_init_value=1e-6):
+ super().__init__()
+ self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)
+ self.norm = nn.LayerNorm(dim, eps=1e-6)
+ self.pwconv1 = nn.Linear(dim, 4 * dim)
+ self.act = nn.GELU()
+ self.pwconv2 = nn.Linear(4 * dim, dim)
+ self.gamma = nn.Parameter(layer_scale_init_value * torch.ones(dim)) if layer_scale_init_value > 0 else None
+
+ from .modules import DropPath
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
+
+ def forward(self, x):
+ identity = x
+ x = self.dwconv(x)
+ x = x.permute(0, 2, 3, 1)
+ x = self.norm(x)
+ x = self.pwconv1(x)
+ x = self.act(x)
+ x = self.pwconv2(x)
+ if self.gamma is not None:
+ x = self.gamma * x
+ x = x.permute(0, 3, 1, 2)
+ x = identity + self.drop_path(x)
+ return x
+
+class DilatedConvBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, dilation_rates=[1, 2, 4, 8]):
+ super().__init__()
+ self.convs = nn.ModuleList([
+ nn.Sequential(
+ nn.Conv2d(in_channels, out_channels // len(dilation_rates), 3, padding=d, dilation=d),
+ nn.BatchNorm2d(out_channels // len(dilation_rates)),
+ nn.ReLU(inplace=True)
+ )
+ for d in dilation_rates
+ ])
+
+ def forward(self, x):
+ return torch.cat([conv(x) for conv in self.convs], dim=1)
+
+class PyramidPoolingModule(nn.Module):
+ def __init__(self, in_channels, out_channels, pool_sizes=[1, 2, 3, 6]):
+ super().__init__()
+ self.stages = nn.ModuleList([
+ nn.Sequential(
+ nn.AdaptiveAvgPool2d(size),
+ nn.Conv2d(in_channels, out_channels // len(pool_sizes), 1),
+ nn.BatchNorm2d(out_channels // len(pool_sizes)),
+ nn.ReLU(inplace=True)
+ )
+ for size in pool_sizes
+ ])
+
+ def forward(self, x):
+ h, w = x.size(2), x.size(3)
+ features = [x]
+ for stage in self.stages:
+ pooled = stage(x)
+ features.append(F.interpolate(pooled, size=(h, w), mode='bilinear', align_corners=False))
+ return torch.cat(features, dim=1)
diff --git a/ML/src/python/neuralforge/nn/layers.py b/ML/src/python/neuralforge/nn/layers.py
new file mode 100644
index 00000000000..a0e8eb549b0
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/layers.py
@@ -0,0 +1,174 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from typing import Optional
+
+class ConvBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1,
+ use_bn=True, activation='relu', drop_rate=0.0):
+ super().__init__()
+ self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=not use_bn)
+ self.bn = nn.BatchNorm2d(out_channels) if use_bn else nn.Identity()
+
+ if activation == 'relu':
+ self.activation = nn.ReLU(inplace=True)
+ elif activation == 'gelu':
+ self.activation = nn.GELU()
+ elif activation == 'silu':
+ self.activation = nn.SiLU(inplace=True)
+ elif activation == 'mish':
+ self.activation = nn.Mish(inplace=True)
+ else:
+ self.activation = nn.Identity()
+
+ self.dropout = nn.Dropout2d(drop_rate) if drop_rate > 0 else nn.Identity()
+
+ def forward(self, x):
+ x = self.conv(x)
+ x = self.bn(x)
+ x = self.activation(x)
+ x = self.dropout(x)
+ return x
+
+class ResidualBlock(nn.Module):
+ def __init__(self, channels, kernel_size=3, drop_rate=0.0):
+ super().__init__()
+ self.conv1 = ConvBlock(channels, channels, kernel_size, padding=kernel_size // 2, drop_rate=drop_rate)
+ self.conv2 = ConvBlock(channels, channels, kernel_size, padding=kernel_size // 2, activation='none')
+ self.activation = nn.ReLU(inplace=True)
+
+ def forward(self, x):
+ residual = x
+ x = self.conv1(x)
+ x = self.conv2(x)
+ x = x + residual
+ x = self.activation(x)
+ return x
+
+class BottleneckBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, stride=1, expansion=4):
+ super().__init__()
+ mid_channels = out_channels // expansion
+
+ self.conv1 = ConvBlock(in_channels, mid_channels, kernel_size=1, padding=0)
+ self.conv2 = ConvBlock(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1)
+ self.conv3 = ConvBlock(mid_channels, out_channels, kernel_size=1, padding=0, activation='none')
+
+ self.shortcut = nn.Sequential()
+ if stride != 1 or in_channels != out_channels:
+ self.shortcut = nn.Sequential(
+ nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
+ nn.BatchNorm2d(out_channels)
+ )
+
+ self.activation = nn.ReLU(inplace=True)
+
+ def forward(self, x):
+ residual = self.shortcut(x)
+ x = self.conv1(x)
+ x = self.conv2(x)
+ x = self.conv3(x)
+ x = x + residual
+ x = self.activation(x)
+ return x
+
+class InvertedResidualBlock(nn.Module):
+ def __init__(self, in_channels, out_channels, stride=1, expand_ratio=6):
+ super().__init__()
+ hidden_dim = in_channels * expand_ratio
+ self.use_residual = stride == 1 and in_channels == out_channels
+
+ layers = []
+ if expand_ratio != 1:
+ layers.append(ConvBlock(in_channels, hidden_dim, kernel_size=1, padding=0))
+
+ layers.extend([
+ ConvBlock(hidden_dim, hidden_dim, kernel_size=3, stride=stride, padding=1, activation='relu'),
+ nn.Conv2d(hidden_dim, out_channels, kernel_size=1, bias=False),
+ nn.BatchNorm2d(out_channels)
+ ])
+
+ self.conv = nn.Sequential(*layers)
+
+ def forward(self, x):
+ if self.use_residual:
+ return x + self.conv(x)
+ return self.conv(x)
+
+class DenseLayer(nn.Module):
+ def __init__(self, in_channels, growth_rate, drop_rate=0.0):
+ super().__init__()
+ self.bn1 = nn.BatchNorm2d(in_channels)
+ self.relu1 = nn.ReLU(inplace=True)
+ self.conv1 = nn.Conv2d(in_channels, growth_rate * 4, kernel_size=1, bias=False)
+
+ self.bn2 = nn.BatchNorm2d(growth_rate * 4)
+ self.relu2 = nn.ReLU(inplace=True)
+ self.conv2 = nn.Conv2d(growth_rate * 4, growth_rate, kernel_size=3, padding=1, bias=False)
+
+ self.dropout = nn.Dropout2d(drop_rate) if drop_rate > 0 else nn.Identity()
+
+ def forward(self, x):
+ out = self.conv1(self.relu1(self.bn1(x)))
+ out = self.conv2(self.relu2(self.bn2(out)))
+ out = self.dropout(out)
+ return torch.cat([x, out], 1)
+
+class DenseBlock(nn.Module):
+ def __init__(self, num_layers, in_channels, growth_rate, drop_rate=0.0):
+ super().__init__()
+ layers = []
+ for i in range(num_layers):
+ layers.append(DenseLayer(in_channels + i * growth_rate, growth_rate, drop_rate))
+ self.layers = nn.Sequential(*layers)
+
+ def forward(self, x):
+ return self.layers(x)
+
+class TransitionLayer(nn.Module):
+ def __init__(self, in_channels, out_channels):
+ super().__init__()
+ self.bn = nn.BatchNorm2d(in_channels)
+ self.relu = nn.ReLU(inplace=True)
+ self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
+ self.pool = nn.AvgPool2d(kernel_size=2, stride=2)
+
+ def forward(self, x):
+ x = self.conv(self.relu(self.bn(x)))
+ x = self.pool(x)
+ return x
+
+class SEBlock(nn.Module):
+ def __init__(self, channels, reduction=16):
+ super().__init__()
+ self.squeeze = nn.AdaptiveAvgPool2d(1)
+ self.excitation = nn.Sequential(
+ nn.Linear(channels, channels // reduction, bias=False),
+ nn.ReLU(inplace=True),
+ nn.Linear(channels // reduction, channels, bias=False),
+ nn.Sigmoid()
+ )
+
+ def forward(self, x):
+ b, c, _, _ = x.size()
+ se = self.squeeze(x).view(b, c)
+ se = self.excitation(se).view(b, c, 1, 1)
+ return x * se.expand_as(x)
+
+class DepthwiseSeparableConv(nn.Module):
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
+ super().__init__()
+ self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size, stride, padding, groups=in_channels, bias=False)
+ self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
+ self.bn1 = nn.BatchNorm2d(in_channels)
+ self.bn2 = nn.BatchNorm2d(out_channels)
+ self.relu = nn.ReLU(inplace=True)
+
+ def forward(self, x):
+ x = self.depthwise(x)
+ x = self.bn1(x)
+ x = self.relu(x)
+ x = self.pointwise(x)
+ x = self.bn2(x)
+ x = self.relu(x)
+ return x
diff --git a/ML/src/python/neuralforge/nn/modules.py b/ML/src/python/neuralforge/nn/modules.py
new file mode 100644
index 00000000000..e127753ef4d
--- /dev/null
+++ b/ML/src/python/neuralforge/nn/modules.py
@@ -0,0 +1,188 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from typing import Optional, Tuple
+import math
+
+class DynamicConv2d(nn.Module):
+ def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, groups=1):
+ super().__init__()
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.kernel_size = kernel_size
+ self.stride = stride
+ self.padding = padding
+ self.groups = groups
+
+ self.weight = nn.Parameter(torch.randn(out_channels, in_channels // groups, kernel_size, kernel_size))
+ self.bias = nn.Parameter(torch.zeros(out_channels))
+
+ nn.init.kaiming_normal_(self.weight, mode='fan_out', nonlinearity='relu')
+
+ def forward(self, x):
+ return F.conv2d(x, self.weight, self.bias, self.stride, self.padding, groups=self.groups)
+
+class DynamicLinear(nn.Module):
+ def __init__(self, in_features, out_features, bias=True):
+ super().__init__()
+ self.in_features = in_features
+ self.out_features = out_features
+
+ self.weight = nn.Parameter(torch.randn(out_features, in_features))
+ if bias:
+ self.bias = nn.Parameter(torch.zeros(out_features))
+ else:
+ self.register_parameter('bias', None)
+
+ nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
+ if self.bias is not None:
+ fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
+ bound = 1 / math.sqrt(fan_in)
+ nn.init.uniform_(self.bias, -bound, bound)
+
+ def forward(self, x):
+ return F.linear(x, self.weight, self.bias)
+
+class AdaptiveBatchNorm2d(nn.Module):
+ def __init__(self, num_features, eps=1e-5, momentum=0.1):
+ super().__init__()
+ self.num_features = num_features
+ self.eps = eps
+ self.momentum = momentum
+
+ self.weight = nn.Parameter(torch.ones(num_features))
+ self.bias = nn.Parameter(torch.zeros(num_features))
+ self.register_buffer('running_mean', torch.zeros(num_features))
+ self.register_buffer('running_var', torch.ones(num_features))
+ self.register_buffer('num_batches_tracked', torch.tensor(0, dtype=torch.long))
+
+ def forward(self, x):
+ if self.training:
+ mean = x.mean([0, 2, 3])
+ var = x.var([0, 2, 3], unbiased=False)
+
+ with torch.no_grad():
+ self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean
+ self.running_var = (1 - self.momentum) * self.running_var + self.momentum * var
+ self.num_batches_tracked += 1
+
+ x_normalized = (x - mean[None, :, None, None]) / torch.sqrt(var[None, :, None, None] + self.eps)
+ else:
+ x_normalized = (x - self.running_mean[None, :, None, None]) / torch.sqrt(self.running_var[None, :, None, None] + self.eps)
+
+ return self.weight[None, :, None, None] * x_normalized + self.bias[None, :, None, None]
+
+class LayerNorm(nn.Module):
+ def __init__(self, normalized_shape, eps=1e-5):
+ super().__init__()
+ self.normalized_shape = normalized_shape
+ self.eps = eps
+ self.weight = nn.Parameter(torch.ones(normalized_shape))
+ self.bias = nn.Parameter(torch.zeros(normalized_shape))
+
+ def forward(self, x):
+ mean = x.mean(-1, keepdim=True)
+ std = x.std(-1, keepdim=True)
+ return self.weight * (x - mean) / (std + self.eps) + self.bias
+
+class GroupNorm(nn.Module):
+ def __init__(self, num_groups, num_channels, eps=1e-5):
+ super().__init__()
+ self.num_groups = num_groups
+ self.num_channels = num_channels
+ self.eps = eps
+ self.weight = nn.Parameter(torch.ones(num_channels))
+ self.bias = nn.Parameter(torch.zeros(num_channels))
+
+ def forward(self, x):
+ N, C, H, W = x.shape
+ x = x.reshape(N, self.num_groups, C // self.num_groups, H, W)
+ mean = x.mean([2, 3, 4], keepdim=True)
+ var = x.var([2, 3, 4], keepdim=True)
+ x = (x - mean) / torch.sqrt(var + self.eps)
+ x = x.reshape(N, C, H, W)
+ return x * self.weight[None, :, None, None] + self.bias[None, :, None, None]
+
+class DropPath(nn.Module):
+ def __init__(self, drop_prob=0.0):
+ super().__init__()
+ self.drop_prob = drop_prob
+
+ def forward(self, x):
+ if self.drop_prob == 0.0 or not self.training:
+ return x
+ keep_prob = 1 - self.drop_prob
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1)
+ random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
+ random_tensor.floor_()
+ output = x.div(keep_prob) * random_tensor
+ return output
+
+class GlobalAvgPool2d(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x.mean([2, 3])
+
+class GlobalMaxPool2d(nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return x.max(dim=2)[0].max(dim=2)[0]
+
+class AdaptiveAvgMaxPool2d(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.avg_pool = GlobalAvgPool2d()
+ self.max_pool = GlobalMaxPool2d()
+
+ def forward(self, x):
+ avg = self.avg_pool(x)
+ max_val = self.max_pool(x)
+ return torch.cat([avg, max_val], dim=1)
+
+class Flatten(nn.Module):
+ def __init__(self, start_dim=1):
+ super().__init__()
+ self.start_dim = start_dim
+
+ def forward(self, x):
+ return x.flatten(self.start_dim)
+
+class SqueezeExcitation(nn.Module):
+ def __init__(self, channels, reduction=16):
+ super().__init__()
+ self.fc1 = nn.Linear(channels, channels // reduction)
+ self.fc2 = nn.Linear(channels // reduction, channels)
+
+ def forward(self, x):
+ b, c, _, _ = x.size()
+ se = x.mean([2, 3])
+ se = F.relu(self.fc1(se))
+ se = torch.sigmoid(self.fc2(se))
+ return x * se.view(b, c, 1, 1)
+
+class SpatialAttention(nn.Module):
+ def __init__(self, kernel_size=7):
+ super().__init__()
+ self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2)
+
+ def forward(self, x):
+ avg_out = torch.mean(x, dim=1, keepdim=True)
+ max_out, _ = torch.max(x, dim=1, keepdim=True)
+ attention = torch.cat([avg_out, max_out], dim=1)
+ attention = torch.sigmoid(self.conv(attention))
+ return x * attention
+
+class CBAM(nn.Module):
+ def __init__(self, channels, reduction=16, kernel_size=7):
+ super().__init__()
+ self.channel_attention = SqueezeExcitation(channels, reduction)
+ self.spatial_attention = SpatialAttention(kernel_size)
+
+ def forward(self, x):
+ x = self.channel_attention(x)
+ x = self.spatial_attention(x)
+ return x
diff --git a/ML/src/python/neuralforge/optim/__init__.py b/ML/src/python/neuralforge/optim/__init__.py
new file mode 100644
index 00000000000..152ec2e4713
--- /dev/null
+++ b/ML/src/python/neuralforge/optim/__init__.py
@@ -0,0 +1,13 @@
+from .optimizers import *
+from .schedulers import *
+
+__all__ = [
+ 'AdamW',
+ 'LAMB',
+ 'AdaBound',
+ 'RAdam',
+ 'Lookahead',
+ 'CosineAnnealingWarmRestarts',
+ 'OneCycleLR',
+ 'WarmupScheduler',
+]
diff --git a/ML/src/python/neuralforge/optim/optimizers.py b/ML/src/python/neuralforge/optim/optimizers.py
new file mode 100644
index 00000000000..242e86178b6
--- /dev/null
+++ b/ML/src/python/neuralforge/optim/optimizers.py
@@ -0,0 +1,266 @@
+import torch
+from torch.optim.optimizer import Optimizer
+import math
+
+class AdamW(Optimizer):
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01, amsgrad=False):
+ if lr < 0.0:
+ raise ValueError(f"Invalid learning rate: {lr}")
+ if eps < 0.0:
+ raise ValueError(f"Invalid epsilon value: {eps}")
+ if not 0.0 <= betas[0] < 1.0:
+ raise ValueError(f"Invalid beta parameter at index 0: {betas[0]}")
+ if not 0.0 <= betas[1] < 1.0:
+ raise ValueError(f"Invalid beta parameter at index 1: {betas[1]}")
+
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, amsgrad=amsgrad)
+ super().__init__(params, defaults)
+
+ def step(self, closure=None):
+ loss = None
+ if closure is not None:
+ loss = closure()
+
+ for group in self.param_groups:
+ for p in group['params']:
+ if p.grad is None:
+ continue
+
+ grad = p.grad.data
+ if grad.is_sparse:
+ raise RuntimeError('AdamW does not support sparse gradients')
+
+ amsgrad = group['amsgrad']
+ state = self.state[p]
+
+ if len(state) == 0:
+ state['step'] = 0
+ state['exp_avg'] = torch.zeros_like(p.data)
+ state['exp_avg_sq'] = torch.zeros_like(p.data)
+ if amsgrad:
+ state['max_exp_avg_sq'] = torch.zeros_like(p.data)
+
+ exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
+ if amsgrad:
+ max_exp_avg_sq = state['max_exp_avg_sq']
+ beta1, beta2 = group['betas']
+
+ state['step'] += 1
+
+ p.data.mul_(1 - group['lr'] * group['weight_decay'])
+
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
+
+ if amsgrad:
+ torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq)
+ denom = max_exp_avg_sq.sqrt().add_(group['eps'])
+ else:
+ denom = exp_avg_sq.sqrt().add_(group['eps'])
+
+ bias_correction1 = 1 - beta1 ** state['step']
+ bias_correction2 = 1 - beta2 ** state['step']
+ step_size = group['lr'] * math.sqrt(bias_correction2) / bias_correction1
+
+ p.data.addcdiv_(exp_avg, denom, value=-step_size)
+
+ return loss
+
+class LAMB(Optimizer):
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-6, weight_decay=0.01):
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
+ super().__init__(params, defaults)
+
+ def step(self, closure=None):
+ loss = None
+ if closure is not None:
+ loss = closure()
+
+ for group in self.param_groups:
+ for p in group['params']:
+ if p.grad is None:
+ continue
+
+ grad = p.grad.data
+ state = self.state[p]
+
+ if len(state) == 0:
+ state['step'] = 0
+ state['exp_avg'] = torch.zeros_like(p.data)
+ state['exp_avg_sq'] = torch.zeros_like(p.data)
+
+ exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
+ beta1, beta2 = group['betas']
+ state['step'] += 1
+
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
+
+ bias_correction1 = 1 - beta1 ** state['step']
+ bias_correction2 = 1 - beta2 ** state['step']
+
+ exp_avg_hat = exp_avg / bias_correction1
+ exp_avg_sq_hat = exp_avg_sq / bias_correction2
+
+ update = exp_avg_hat / (exp_avg_sq_hat.sqrt() + group['eps'])
+ update.add_(p.data, alpha=group['weight_decay'])
+
+ weight_norm = p.data.norm()
+ update_norm = update.norm()
+
+ if weight_norm > 0 and update_norm > 0:
+ trust_ratio = weight_norm / update_norm
+ else:
+ trust_ratio = 1.0
+
+ p.data.add_(update, alpha=-group['lr'] * trust_ratio)
+
+ return loss
+
+class RAdam(Optimizer):
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0):
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
+ super().__init__(params, defaults)
+
+ def step(self, closure=None):
+ loss = None
+ if closure is not None:
+ loss = closure()
+
+ for group in self.param_groups:
+ for p in group['params']:
+ if p.grad is None:
+ continue
+
+ grad = p.grad.data
+ state = self.state[p]
+
+ if len(state) == 0:
+ state['step'] = 0
+ state['exp_avg'] = torch.zeros_like(p.data)
+ state['exp_avg_sq'] = torch.zeros_like(p.data)
+
+ exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
+ beta1, beta2 = group['betas']
+ state['step'] += 1
+
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
+
+ buffered = [[None, None, None] for _ in range(10)]
+
+ rho_inf = 2 / (1 - beta2) - 1
+ rho_t = rho_inf - 2 * state['step'] * (beta2 ** state['step']) / (1 - beta2 ** state['step'])
+
+ if rho_t > 4:
+ bias_correction1 = 1 - beta1 ** state['step']
+ bias_correction2 = 1 - beta2 ** state['step']
+
+ rt = math.sqrt(
+ (rho_t - 4) * (rho_t - 2) * rho_inf / ((rho_inf - 4) * (rho_inf - 2) * rho_t)
+ )
+
+ denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(group['eps'])
+ step_size = group['lr'] * rt / bias_correction1
+
+ p.data.addcdiv_(exp_avg, denom, value=-step_size)
+ else:
+ bias_correction1 = 1 - beta1 ** state['step']
+ step_size = group['lr'] / bias_correction1
+ p.data.add_(exp_avg, alpha=-step_size)
+
+ if group['weight_decay'] != 0:
+ p.data.add_(p.data, alpha=-group['weight_decay'] * group['lr'])
+
+ return loss
+
+class AdaBound(Optimizer):
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), final_lr=0.1, gamma=1e-3, eps=1e-8, weight_decay=0):
+ defaults = dict(lr=lr, betas=betas, final_lr=final_lr, gamma=gamma, eps=eps, weight_decay=weight_decay)
+ super().__init__(params, defaults)
+
+ def step(self, closure=None):
+ loss = None
+ if closure is not None:
+ loss = closure()
+
+ for group in self.param_groups:
+ for p in group['params']:
+ if p.grad is None:
+ continue
+
+ grad = p.grad.data
+ state = self.state[p]
+
+ if len(state) == 0:
+ state['step'] = 0
+ state['exp_avg'] = torch.zeros_like(p.data)
+ state['exp_avg_sq'] = torch.zeros_like(p.data)
+
+ exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
+ beta1, beta2 = group['betas']
+ state['step'] += 1
+
+ if group['weight_decay'] != 0:
+ grad.add_(p.data, alpha=group['weight_decay'])
+
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
+
+ bias_correction1 = 1 - beta1 ** state['step']
+ bias_correction2 = 1 - beta2 ** state['step']
+
+ step_size = group['lr'] * math.sqrt(bias_correction2) / bias_correction1
+
+ final_lr = group['final_lr'] * group['lr'] / group['lr']
+ lower_bound = final_lr * (1 - 1 / (group['gamma'] * state['step'] + 1))
+ upper_bound = final_lr * (1 + 1 / (group['gamma'] * state['step']))
+
+ denom = exp_avg_sq.sqrt().add_(group['eps'])
+ step_size_clipped = torch.full_like(denom, step_size).div_(denom).clamp_(lower_bound, upper_bound).mul_(exp_avg)
+
+ p.data.add_(step_size_clipped, alpha=-1)
+
+ return loss
+
+class Lookahead(Optimizer):
+ def __init__(self, optimizer, k=5, alpha=0.5):
+ self.optimizer = optimizer
+ self.k = k
+ self.alpha = alpha
+ self.param_groups = self.optimizer.param_groups
+ self.state = {}
+
+ for group in self.param_groups:
+ group['counter'] = 0
+
+ def update(self, group):
+ for fast_p in group['params']:
+ if fast_p.grad is None:
+ continue
+ param_state = self.state[fast_p]
+ if 'slow_buffer' not in param_state:
+ param_state['slow_buffer'] = torch.empty_like(fast_p.data)
+ param_state['slow_buffer'].copy_(fast_p.data)
+
+ slow = param_state['slow_buffer']
+ slow.add_(fast_p.data - slow, alpha=self.alpha)
+ fast_p.data.copy_(slow)
+
+ def step(self, closure=None):
+ loss = self.optimizer.step(closure)
+
+ for group in self.param_groups:
+ group['counter'] += 1
+ if group['counter'] >= self.k:
+ self.update(group)
+ group['counter'] = 0
+
+ return loss
+
+ def state_dict(self):
+ return {
+ 'state': self.state,
+ 'optimizer': self.optimizer.state_dict(),
+ 'param_groups': self.param_groups,
+ }
diff --git a/ML/src/python/neuralforge/optim/schedulers.py b/ML/src/python/neuralforge/optim/schedulers.py
new file mode 100644
index 00000000000..63f05aa6637
--- /dev/null
+++ b/ML/src/python/neuralforge/optim/schedulers.py
@@ -0,0 +1,142 @@
+import torch
+from torch.optim.lr_scheduler import _LRScheduler
+import math
+
+class WarmupScheduler(_LRScheduler):
+ def __init__(self, optimizer, warmup_epochs, base_scheduler=None, last_epoch=-1):
+ self.warmup_epochs = warmup_epochs
+ self.base_scheduler = base_scheduler
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ if self.last_epoch < self.warmup_epochs:
+ return [base_lr * (self.last_epoch + 1) / self.warmup_epochs for base_lr in self.base_lrs]
+
+ if self.base_scheduler is not None:
+ return self.base_scheduler.get_last_lr()
+
+ return self.base_lrs
+
+ def step(self, epoch=None):
+ if self.last_epoch < self.warmup_epochs:
+ super().step(epoch)
+ elif self.base_scheduler is not None:
+ self.base_scheduler.step(epoch)
+
+class CosineAnnealingWarmRestarts(_LRScheduler):
+ def __init__(self, optimizer, T_0, T_mult=1, eta_min=0, last_epoch=-1):
+ self.T_0 = T_0
+ self.T_mult = T_mult
+ self.eta_min = eta_min
+ self.T_cur = last_epoch
+ self.T_i = T_0
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ return [
+ self.eta_min + (base_lr - self.eta_min) * (1 + math.cos(math.pi * self.T_cur / self.T_i)) / 2
+ for base_lr in self.base_lrs
+ ]
+
+ def step(self, epoch=None):
+ if epoch is None:
+ epoch = self.last_epoch + 1
+ self.T_cur = self.T_cur + 1
+ if self.T_cur >= self.T_i:
+ self.T_cur = self.T_cur - self.T_i
+ self.T_i = self.T_i * self.T_mult
+ else:
+ if epoch < 0:
+ raise ValueError("Expected non-negative epoch, but got {}".format(epoch))
+ if epoch >= self.T_0:
+ if self.T_mult == 1:
+ self.T_cur = epoch % self.T_0
+ else:
+ n = int(math.log((epoch / self.T_0 * (self.T_mult - 1) + 1), self.T_mult))
+ self.T_cur = epoch - self.T_0 * (self.T_mult ** n - 1) / (self.T_mult - 1)
+ self.T_i = self.T_0 * self.T_mult ** n
+ else:
+ self.T_i = self.T_0
+ self.T_cur = epoch
+
+ self.last_epoch = math.floor(epoch)
+
+ for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
+ param_group['lr'] = lr
+
+class OneCycleLR(_LRScheduler):
+ def __init__(self, optimizer, max_lr, total_steps, pct_start=0.3, anneal_strategy='cos',
+ div_factor=25.0, final_div_factor=1e4, last_epoch=-1):
+ self.max_lr = max_lr if isinstance(max_lr, list) else [max_lr] * len(optimizer.param_groups)
+ self.total_steps = total_steps
+ self.pct_start = pct_start
+ self.anneal_strategy = anneal_strategy
+ self.div_factor = div_factor
+ self.final_div_factor = final_div_factor
+
+ self.initial_lr = [lr / self.div_factor for lr in self.max_lr]
+ self.min_lr = [lr / self.final_div_factor for lr in self.max_lr]
+
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ step_num = self.last_epoch
+
+ if step_num > self.total_steps:
+ return self.min_lr
+
+ if step_num <= self.pct_start * self.total_steps:
+ pct = step_num / (self.pct_start * self.total_steps)
+ return [initial + (maximum - initial) * pct
+ for initial, maximum in zip(self.initial_lr, self.max_lr)]
+ else:
+ pct = (step_num - self.pct_start * self.total_steps) / ((1 - self.pct_start) * self.total_steps)
+
+ if self.anneal_strategy == 'cos':
+ return [minimum + (maximum - minimum) * (1 + math.cos(math.pi * pct)) / 2
+ for minimum, maximum in zip(self.min_lr, self.max_lr)]
+ else:
+ return [maximum - (maximum - minimum) * pct
+ for minimum, maximum in zip(self.min_lr, self.max_lr)]
+
+class PolynomialLR(_LRScheduler):
+ def __init__(self, optimizer, total_iters, power=1.0, last_epoch=-1):
+ self.total_iters = total_iters
+ self.power = power
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ if self.last_epoch == 0 or self.last_epoch > self.total_iters:
+ return [group['lr'] for group in self.optimizer.param_groups]
+
+ decay_factor = ((1.0 - self.last_epoch / self.total_iters) / (1.0 - (self.last_epoch - 1) / self.total_iters)) ** self.power
+ return [group['lr'] * decay_factor for group in self.optimizer.param_groups]
+
+class LinearWarmupCosineAnnealingLR(_LRScheduler):
+ def __init__(self, optimizer, warmup_epochs, max_epochs, warmup_start_lr=0.0, eta_min=0.0, last_epoch=-1):
+ self.warmup_epochs = warmup_epochs
+ self.max_epochs = max_epochs
+ self.warmup_start_lr = warmup_start_lr
+ self.eta_min = eta_min
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ if self.last_epoch < self.warmup_epochs:
+ alpha = self.last_epoch / self.warmup_epochs
+ return [self.warmup_start_lr + (base_lr - self.warmup_start_lr) * alpha for base_lr in self.base_lrs]
+ else:
+ progress = (self.last_epoch - self.warmup_epochs) / (self.max_epochs - self.warmup_epochs)
+ return [self.eta_min + (base_lr - self.eta_min) * 0.5 * (1.0 + math.cos(math.pi * progress))
+ for base_lr in self.base_lrs]
+
+class ExponentialWarmup(_LRScheduler):
+ def __init__(self, optimizer, warmup_epochs, gamma=0.9, last_epoch=-1):
+ self.warmup_epochs = warmup_epochs
+ self.gamma = gamma
+ super().__init__(optimizer, last_epoch)
+
+ def get_lr(self):
+ if self.last_epoch < self.warmup_epochs:
+ return [base_lr * (self.last_epoch + 1) / self.warmup_epochs for base_lr in self.base_lrs]
+
+ return [base_lr * self.gamma ** (self.last_epoch - self.warmup_epochs) for base_lr in self.base_lrs]
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/trainer.py b/ML/src/python/neuralforge/trainer.py
new file mode 100644
index 00000000000..423d45b2f2e
--- /dev/null
+++ b/ML/src/python/neuralforge/trainer.py
@@ -0,0 +1,256 @@
+import torch
+import torch.nn as nn
+import torch.amp as amp
+from torch.utils.data import DataLoader
+from typing import Optional, Dict, Any, Callable
+import time
+import os
+from tqdm import tqdm
+from .utils.logger import Logger
+from .utils.metrics import MetricsTracker
+from .config import Config
+
+class Trainer:
+ def __init__(
+ self,
+ model: nn.Module,
+ train_loader: DataLoader,
+ val_loader: Optional[DataLoader],
+ optimizer: torch.optim.Optimizer,
+ criterion: nn.Module,
+ config: Config,
+ scheduler: Optional[Any] = None,
+ device: Optional[str] = None
+ ):
+ self.model = model
+ self.train_loader = train_loader
+ self.val_loader = val_loader
+ self.optimizer = optimizer
+ self.criterion = criterion
+ self.config = config
+ self.scheduler = scheduler
+ self.device = device or config.device
+
+ self.model.to(self.device)
+
+ self.scaler = amp.GradScaler('cuda') if config.use_amp and self.device == 'cuda' else None
+ self.logger = Logger(config.log_dir, config.model_name)
+ self.metrics = MetricsTracker()
+
+ self.current_epoch = 0
+ self.global_step = 0
+ self.best_val_loss = float('inf')
+
+ os.makedirs(config.model_dir, exist_ok=True)
+
+ self.logger.info(f"Trainer initialized with device: {self.device}")
+ self.logger.info(f"Model parameters: {sum(p.numel() for p in model.parameters()):,}")
+ self.logger.info(f"Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
+
+ def train_epoch(self) -> Dict[str, float]:
+ self.model.train()
+ epoch_loss = 0.0
+ correct = 0
+ total = 0
+
+ pbar = tqdm(self.train_loader, desc=f"Epoch {self.current_epoch + 1}/{self.config.epochs}")
+
+ for batch_idx, (inputs, targets) in enumerate(pbar):
+ inputs = inputs.to(self.device, non_blocking=True)
+ targets = targets.to(self.device, non_blocking=True)
+
+ self.optimizer.zero_grad(set_to_none=True)
+
+ if self.scaler is not None:
+ with amp.autocast('cuda'):
+ outputs = self.model(inputs)
+ loss = self.criterion(outputs, targets)
+
+ self.scaler.scale(loss).backward()
+
+ if self.config.grad_clip > 0:
+ self.scaler.unscale_(self.optimizer)
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.config.grad_clip)
+
+ self.scaler.step(self.optimizer)
+ self.scaler.update()
+ else:
+ outputs = self.model(inputs)
+ loss = self.criterion(outputs, targets)
+ loss.backward()
+
+ if self.config.grad_clip > 0:
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.config.grad_clip)
+
+ self.optimizer.step()
+
+ epoch_loss += loss.item()
+ _, predicted = outputs.max(1)
+ total += targets.size(0)
+ correct += predicted.eq(targets).sum().item()
+
+ self.global_step += 1
+
+ if batch_idx % 10 == 0:
+ pbar.set_postfix({
+ 'loss': f'{loss.item():.4f}',
+ 'acc': f'{100. * correct / total:.2f}%'
+ })
+
+ avg_loss = epoch_loss / len(self.train_loader)
+ accuracy = 100. * correct / total
+
+ return {'loss': avg_loss, 'accuracy': accuracy}
+
+ def validate(self) -> Dict[str, float]:
+ if self.val_loader is None:
+ return {}
+
+ self.model.eval()
+ val_loss = 0.0
+ correct = 0
+ total = 0
+
+ with torch.no_grad():
+ for inputs, targets in tqdm(self.val_loader, desc="Validation"):
+ inputs = inputs.to(self.device, non_blocking=True)
+ targets = targets.to(self.device, non_blocking=True)
+
+ if self.scaler is not None:
+ with amp.autocast('cuda'):
+ outputs = self.model(inputs)
+ loss = self.criterion(outputs, targets)
+ else:
+ outputs = self.model(inputs)
+ loss = self.criterion(outputs, targets)
+
+ val_loss += loss.item()
+ _, predicted = outputs.max(1)
+ total += targets.size(0)
+ correct += predicted.eq(targets).sum().item()
+
+ avg_loss = val_loss / len(self.val_loader)
+ accuracy = 100. * correct / total
+
+ return {'loss': avg_loss, 'accuracy': accuracy}
+
+ def train(self):
+ self.logger.info("Starting training...")
+ start_time = time.time()
+
+ for epoch in range(self.config.epochs):
+ self.current_epoch = epoch
+ epoch_start = time.time()
+
+ train_metrics = self.train_epoch()
+ val_metrics = self.validate()
+
+ if self.scheduler is not None:
+ if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
+ self.scheduler.step(val_metrics.get('loss', train_metrics['loss']))
+ else:
+ self.scheduler.step()
+
+ current_lr = self.optimizer.param_groups[0]['lr']
+ epoch_time = time.time() - epoch_start
+
+ self.logger.info(
+ f"Epoch {epoch + 1}/{self.config.epochs} | "
+ f"Train Loss: {train_metrics['loss']:.4f} | "
+ f"Train Acc: {train_metrics['accuracy']:.2f}% | "
+ f"Val Loss: {val_metrics.get('loss', 0):.4f} | "
+ f"Val Acc: {val_metrics.get('accuracy', 0):.2f}% | "
+ f"LR: {current_lr:.6f} | "
+ f"Time: {epoch_time:.2f}s"
+ )
+
+ self.metrics.update({
+ 'epoch': epoch + 1,
+ 'train_loss': train_metrics['loss'],
+ 'train_acc': train_metrics['accuracy'],
+ 'val_loss': val_metrics.get('loss', 0),
+ 'val_acc': val_metrics.get('accuracy', 0),
+ 'lr': current_lr,
+ 'time': epoch_time
+ })
+
+ if (epoch + 1) % self.config.checkpoint_freq == 0:
+ self.save_checkpoint(f'checkpoint_epoch_{epoch + 1}.pt')
+
+ if val_metrics and val_metrics['loss'] < self.best_val_loss:
+ self.best_val_loss = val_metrics['loss']
+ self.save_checkpoint('best_model.pt')
+ self.logger.info(f"New best model saved with val_loss: {self.best_val_loss:.4f}")
+
+ total_time = time.time() - start_time
+ self.logger.info(f"Training completed in {total_time / 3600:.2f} hours")
+
+ self.save_checkpoint('final_model.pt')
+ self.metrics.save(os.path.join(self.config.log_dir, 'metrics.json'))
+
+ def save_checkpoint(self, filename: str):
+ checkpoint_path = os.path.join(self.config.model_dir, filename)
+
+ checkpoint = {
+ 'epoch': self.current_epoch,
+ 'global_step': self.global_step,
+ 'model_state_dict': self.model.state_dict(),
+ 'optimizer_state_dict': self.optimizer.state_dict(),
+ 'best_val_loss': self.best_val_loss,
+ 'config': self.config,
+ }
+
+ if self.scheduler is not None:
+ checkpoint['scheduler_state_dict'] = self.scheduler.state_dict()
+
+ if self.scaler is not None:
+ checkpoint['scaler_state_dict'] = self.scaler.state_dict()
+
+ torch.save(checkpoint, checkpoint_path)
+ self.logger.info(f"Checkpoint saved: {checkpoint_path}")
+
+ def load_checkpoint(self, checkpoint_path: str):
+ self.logger.info(f"Loading checkpoint: {checkpoint_path}")
+ checkpoint = torch.load(checkpoint_path, map_location=self.device)
+
+ self.model.load_state_dict(checkpoint['model_state_dict'])
+ self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
+ self.current_epoch = checkpoint['epoch']
+ self.global_step = checkpoint['global_step']
+ self.best_val_loss = checkpoint['best_val_loss']
+
+ if self.scheduler is not None and 'scheduler_state_dict' in checkpoint:
+ self.scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
+
+ if self.scaler is not None and 'scaler_state_dict' in checkpoint:
+ self.scaler.load_state_dict(checkpoint['scaler_state_dict'])
+
+ self.logger.info(f"Checkpoint loaded from epoch {self.current_epoch}")
+
+ def test(self, test_loader: DataLoader) -> Dict[str, float]:
+ self.logger.info("Starting testing...")
+ self.model.eval()
+
+ test_loss = 0.0
+ correct = 0
+ total = 0
+
+ with torch.no_grad():
+ for inputs, targets in tqdm(test_loader, desc="Testing"):
+ inputs = inputs.to(self.device, non_blocking=True)
+ targets = targets.to(self.device, non_blocking=True)
+
+ outputs = self.model(inputs)
+ loss = self.criterion(outputs, targets)
+
+ test_loss += loss.item()
+ _, predicted = outputs.max(1)
+ total += targets.size(0)
+ correct += predicted.eq(targets).sum().item()
+
+ avg_loss = test_loss / len(test_loader)
+ accuracy = 100. * correct / total
+
+ self.logger.info(f"Test Loss: {avg_loss:.4f} | Test Acc: {accuracy:.2f}%")
+
+ return {'loss': avg_loss, 'accuracy': accuracy}
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/utils/__init__.py b/ML/src/python/neuralforge/utils/__init__.py
new file mode 100644
index 00000000000..bfd8573a296
--- /dev/null
+++ b/ML/src/python/neuralforge/utils/__init__.py
@@ -0,0 +1,10 @@
+from .logger import *
+from .metrics import *
+from .visualization import *
+
+__all__ = [
+ 'Logger',
+ 'MetricsTracker',
+ 'plot_training_curves',
+ 'visualize_architecture',
+]
\ No newline at end of file
diff --git a/ML/src/python/neuralforge/utils/logger.py b/ML/src/python/neuralforge/utils/logger.py
new file mode 100644
index 00000000000..321b045aac6
--- /dev/null
+++ b/ML/src/python/neuralforge/utils/logger.py
@@ -0,0 +1,115 @@
+import os
+import sys
+import logging
+from datetime import datetime
+from typing import Optional
+
+class Logger:
+ def __init__(self, log_dir: str, name: str = "neuralforge"):
+ self.log_dir = log_dir
+ self.name = name
+
+ os.makedirs(log_dir, exist_ok=True)
+
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ log_file = os.path.join(log_dir, f"{name}_{timestamp}.log")
+
+ self.logger = logging.getLogger(name)
+ self.logger.setLevel(logging.INFO)
+
+ if self.logger.hasHandlers():
+ self.logger.handlers.clear()
+
+ file_handler = logging.FileHandler(log_file)
+ file_handler.setLevel(logging.INFO)
+
+ console_handler = logging.StreamHandler(sys.stdout)
+ console_handler.setLevel(logging.INFO)
+
+ formatter = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ )
+
+ file_handler.setFormatter(formatter)
+ console_handler.setFormatter(formatter)
+
+ self.logger.addHandler(file_handler)
+ self.logger.addHandler(console_handler)
+
+ self.info(f"Logger initialized. Logging to: {log_file}")
+
+ def info(self, message: str):
+ self.logger.info(message)
+
+ def warning(self, message: str):
+ self.logger.warning(message)
+
+ def error(self, message: str):
+ self.logger.error(message)
+
+ def debug(self, message: str):
+ self.logger.debug(message)
+
+ def log_metrics(self, metrics: dict, step: Optional[int] = None):
+ if step is not None:
+ message = f"Step {step}: "
+ else:
+ message = "Metrics: "
+
+ metric_strs = [f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
+ for k, v in metrics.items()]
+ message += ", ".join(metric_strs)
+
+ self.info(message)
+
+ def log_model_summary(self, model):
+ total_params = sum(p.numel() for p in model.parameters())
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
+
+ self.info("=" * 50)
+ self.info("Model Summary")
+ self.info("=" * 50)
+ self.info(f"Total parameters: {total_params:,}")
+ self.info(f"Trainable parameters: {trainable_params:,}")
+ self.info(f"Non-trainable parameters: {total_params - trainable_params:,}")
+ self.info("=" * 50)
+
+ def separator(self, char: str = "=", length: int = 80):
+ self.info(char * length)
+
+class TensorBoardLogger:
+ def __init__(self, log_dir: str):
+ self.log_dir = log_dir
+
+ try:
+ from torch.utils.tensorboard import SummaryWriter
+ self.writer = SummaryWriter(log_dir)
+ self.enabled = True
+ except ImportError:
+ print("TensorBoard not available. Skipping TensorBoard logging.")
+ self.enabled = False
+
+ def log_scalar(self, tag: str, value: float, step: int):
+ if self.enabled:
+ self.writer.add_scalar(tag, value, step)
+
+ def log_scalars(self, main_tag: str, tag_scalar_dict: dict, step: int):
+ if self.enabled:
+ self.writer.add_scalars(main_tag, tag_scalar_dict, step)
+
+ def log_histogram(self, tag: str, values, step: int):
+ if self.enabled:
+ self.writer.add_histogram(tag, values, step)
+
+ def log_image(self, tag: str, img_tensor, step: int):
+ if self.enabled:
+ self.writer.add_image(tag, img_tensor, step)
+
+ def log_graph(self, model, input_to_model):
+ if self.enabled:
+ self.writer.add_graph(model, input_to_model)
+
+ def close(self):
+ if self.enabled:
+ self.writer.close()
diff --git a/ML/src/python/neuralforge/utils/metrics.py b/ML/src/python/neuralforge/utils/metrics.py
new file mode 100644
index 00000000000..633367d8764
--- /dev/null
+++ b/ML/src/python/neuralforge/utils/metrics.py
@@ -0,0 +1,168 @@
+import json
+import os
+from typing import Dict, List, Any
+import numpy as np
+
+class MetricsTracker:
+ def __init__(self):
+ self.metrics = []
+ self.best_metrics = {}
+
+ def update(self, metrics: Dict[str, Any]):
+ self.metrics.append(metrics.copy())
+
+ for key, value in metrics.items():
+ if isinstance(value, (int, float)):
+ if key not in self.best_metrics:
+ self.best_metrics[key] = value
+ else:
+ if 'loss' in key.lower():
+ self.best_metrics[key] = min(self.best_metrics[key], value)
+ else:
+ self.best_metrics[key] = max(self.best_metrics[key], value)
+
+ def get_history(self, key: str) -> List[Any]:
+ return [m.get(key) for m in self.metrics if key in m]
+
+ def get_latest(self, key: str) -> Any:
+ for m in reversed(self.metrics):
+ if key in m:
+ return m[key]
+ return None
+
+ def get_best(self, key: str) -> Any:
+ return self.best_metrics.get(key)
+
+ def get_average(self, key: str, last_n: int = None) -> float:
+ history = self.get_history(key)
+ if not history:
+ return 0.0
+
+ if last_n is not None:
+ history = history[-last_n:]
+
+ return np.mean([v for v in history if v is not None])
+
+ def save(self, filepath: str):
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+
+ data = {
+ 'metrics': self.metrics,
+ 'best_metrics': self.best_metrics
+ }
+
+ with open(filepath, 'w') as f:
+ json.dump(data, f, indent=2)
+
+ def load(self, filepath: str):
+ with open(filepath, 'r') as f:
+ data = json.load(f)
+
+ self.metrics = data.get('metrics', [])
+ self.best_metrics = data.get('best_metrics', {})
+
+ def summary(self) -> str:
+ lines = ["=" * 50, "Metrics Summary", "=" * 50]
+
+ for key, value in self.best_metrics.items():
+ latest = self.get_latest(key)
+ if isinstance(value, float):
+ lines.append(f"{key}: best={value:.4f}, latest={latest:.4f}")
+ else:
+ lines.append(f"{key}: best={value}, latest={latest}")
+
+ lines.append("=" * 50)
+ return "\n".join(lines)
+
+class AverageMeter:
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.val = 0
+ self.avg = 0
+ self.sum = 0
+ self.count = 0
+
+ def update(self, val, n=1):
+ self.val = val
+ self.sum += val * n
+ self.count += n
+ self.avg = self.sum / self.count if self.count > 0 else 0
+
+class EarlyStopping:
+ def __init__(self, patience: int = 10, min_delta: float = 0.0, mode: str = 'min'):
+ self.patience = patience
+ self.min_delta = min_delta
+ self.mode = mode
+ self.counter = 0
+ self.best_score = None
+ self.early_stop = False
+
+ def __call__(self, score: float) -> bool:
+ if self.best_score is None:
+ self.best_score = score
+ return False
+
+ if self.mode == 'min':
+ improved = score < (self.best_score - self.min_delta)
+ else:
+ improved = score > (self.best_score + self.min_delta)
+
+ if improved:
+ self.best_score = score
+ self.counter = 0
+ else:
+ self.counter += 1
+ if self.counter >= self.patience:
+ self.early_stop = True
+
+ return self.early_stop
+
+class ConfusionMatrix:
+ def __init__(self, num_classes: int):
+ self.num_classes = num_classes
+ self.matrix = np.zeros((num_classes, num_classes), dtype=np.int64)
+
+ def update(self, predictions: np.ndarray, targets: np.ndarray):
+ for pred, target in zip(predictions, targets):
+ self.matrix[target, pred] += 1
+
+ def reset(self):
+ self.matrix = np.zeros((self.num_classes, self.num_classes), dtype=np.int64)
+
+ def compute_metrics(self) -> Dict[str, float]:
+ tp = np.diag(self.matrix)
+ fp = np.sum(self.matrix, axis=0) - tp
+ fn = np.sum(self.matrix, axis=1) - tp
+ tn = np.sum(self.matrix) - (tp + fp + fn)
+
+ accuracy = np.sum(tp) / np.sum(self.matrix) if np.sum(self.matrix) > 0 else 0.0
+
+ precision = tp / (tp + fp + 1e-10)
+ recall = tp / (tp + fn + 1e-10)
+ f1_score = 2 * (precision * recall) / (precision + recall + 1e-10)
+
+ return {
+ 'accuracy': accuracy,
+ 'precision': np.mean(precision),
+ 'recall': np.mean(recall),
+ 'f1_score': np.mean(f1_score)
+ }
+
+ def get_matrix(self) -> np.ndarray:
+ return self.matrix
+
+def accuracy(predictions, targets):
+ correct = (predictions == targets).sum()
+ total = len(targets)
+ return 100.0 * correct / total if total > 0 else 0.0
+
+def top_k_accuracy(output, target, k=5):
+ with torch.no_grad():
+ maxk = min(k, output.size(1))
+ _, pred = output.topk(maxk, 1, True, True)
+ pred = pred.t()
+ correct = pred.eq(target.view(1, -1).expand_as(pred))
+ correct_k = correct[:k].reshape(-1).float().sum(0, keepdim=True)
+ return correct_k.mul_(100.0 / target.size(0)).item()
diff --git a/ML/src/python/neuralforge/utils/visualization.py b/ML/src/python/neuralforge/utils/visualization.py
new file mode 100644
index 00000000000..104a12950ad
--- /dev/null
+++ b/ML/src/python/neuralforge/utils/visualization.py
@@ -0,0 +1,192 @@
+import matplotlib.pyplot as plt
+import numpy as np
+import os
+from typing import List, Dict, Optional
+
+def plot_training_curves(
+ metrics_tracker,
+ save_path: Optional[str] = None,
+ figsize: tuple = (15, 5)
+):
+ train_loss = metrics_tracker.get_history('train_loss')
+ val_loss = metrics_tracker.get_history('val_loss')
+ train_acc = metrics_tracker.get_history('train_acc')
+ val_acc = metrics_tracker.get_history('val_acc')
+
+ fig, axes = plt.subplots(1, 2, figsize=figsize)
+
+ if train_loss:
+ axes[0].plot(train_loss, label='Train Loss', linewidth=2)
+ if val_loss:
+ axes[0].plot(val_loss, label='Val Loss', linewidth=2)
+ axes[0].set_xlabel('Epoch')
+ axes[0].set_ylabel('Loss')
+ axes[0].set_title('Training and Validation Loss')
+ axes[0].legend()
+ axes[0].grid(True, alpha=0.3)
+
+ if train_acc:
+ axes[1].plot(train_acc, label='Train Accuracy', linewidth=2)
+ if val_acc:
+ axes[1].plot(val_acc, label='Val Accuracy', linewidth=2)
+ axes[1].set_xlabel('Epoch')
+ axes[1].set_ylabel('Accuracy (%)')
+ axes[1].set_title('Training and Validation Accuracy')
+ axes[1].legend()
+ axes[1].grid(True, alpha=0.3)
+
+ plt.tight_layout()
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"Training curves saved to {save_path}")
+
+ plt.close()
+
+def plot_learning_rate(
+ lr_history: List[float],
+ save_path: Optional[str] = None,
+ figsize: tuple = (10, 5)
+):
+ plt.figure(figsize=figsize)
+ plt.plot(lr_history, linewidth=2)
+ plt.xlabel('Step')
+ plt.ylabel('Learning Rate')
+ plt.title('Learning Rate Schedule')
+ plt.grid(True, alpha=0.3)
+ plt.yscale('log')
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"Learning rate plot saved to {save_path}")
+
+ plt.close()
+
+def plot_confusion_matrix(
+ cm: np.ndarray,
+ class_names: Optional[List[str]] = None,
+ save_path: Optional[str] = None,
+ figsize: tuple = (10, 8)
+):
+ plt.figure(figsize=figsize)
+ plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
+ plt.title('Confusion Matrix')
+ plt.colorbar()
+
+ if class_names:
+ tick_marks = np.arange(len(class_names))
+ plt.xticks(tick_marks, class_names, rotation=45)
+ plt.yticks(tick_marks, class_names)
+
+ thresh = cm.max() / 2.0
+ for i in range(cm.shape[0]):
+ for j in range(cm.shape[1]):
+ plt.text(j, i, format(cm[i, j], 'd'),
+ ha="center", va="center",
+ color="white" if cm[i, j] > thresh else "black")
+
+ plt.ylabel('True label')
+ plt.xlabel('Predicted label')
+ plt.tight_layout()
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"Confusion matrix saved to {save_path}")
+
+ plt.close()
+
+def visualize_architecture(architecture, save_path: Optional[str] = None):
+ layer_types = [gene.get('type', 'unknown') for gene in architecture.genome]
+ layer_counts = {}
+
+ for layer_type in layer_types:
+ layer_counts[layer_type] = layer_counts.get(layer_type, 0) + 1
+
+ plt.figure(figsize=(10, 6))
+ plt.bar(layer_counts.keys(), layer_counts.values())
+ plt.xlabel('Layer Type')
+ plt.ylabel('Count')
+ plt.title('Architecture Layer Distribution')
+ plt.xticks(rotation=45)
+ plt.grid(True, alpha=0.3, axis='y')
+ plt.tight_layout()
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"Architecture visualization saved to {save_path}")
+
+ plt.close()
+
+def plot_nas_history(
+ history: List[Dict],
+ save_path: Optional[str] = None,
+ figsize: tuple = (15, 5)
+):
+ generations = [h['generation'] for h in history]
+ best_fitness = [h['best_fitness'] for h in history]
+ avg_fitness = [h['avg_fitness'] for h in history]
+ best_accuracy = [h['best_accuracy'] for h in history]
+ avg_accuracy = [h['avg_accuracy'] for h in history]
+
+ fig, axes = plt.subplots(1, 2, figsize=figsize)
+
+ axes[0].plot(generations, best_fitness, label='Best Fitness', linewidth=2, marker='o')
+ axes[0].plot(generations, avg_fitness, label='Avg Fitness', linewidth=2, marker='s')
+ axes[0].set_xlabel('Generation')
+ axes[0].set_ylabel('Fitness')
+ axes[0].set_title('NAS Fitness Evolution')
+ axes[0].legend()
+ axes[0].grid(True, alpha=0.3)
+
+ axes[1].plot(generations, best_accuracy, label='Best Accuracy', linewidth=2, marker='o')
+ axes[1].plot(generations, avg_accuracy, label='Avg Accuracy', linewidth=2, marker='s')
+ axes[1].set_xlabel('Generation')
+ axes[1].set_ylabel('Accuracy (%)')
+ axes[1].set_title('NAS Accuracy Evolution')
+ axes[1].legend()
+ axes[1].grid(True, alpha=0.3)
+
+ plt.tight_layout()
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"NAS history plot saved to {save_path}")
+
+ plt.close()
+
+def plot_gradient_flow(named_parameters, save_path: Optional[str] = None):
+ ave_grads = []
+ max_grads = []
+ layers = []
+
+ for n, p in named_parameters:
+ if p.requires_grad and p.grad is not None:
+ layers.append(n)
+ ave_grads.append(p.grad.abs().mean().cpu().item())
+ max_grads.append(p.grad.abs().max().cpu().item())
+
+ plt.figure(figsize=(12, 6))
+ plt.bar(np.arange(len(max_grads)), max_grads, alpha=0.5, lw=1, color="c", label="max gradient")
+ plt.bar(np.arange(len(ave_grads)), ave_grads, alpha=0.5, lw=1, color="b", label="mean gradient")
+ plt.hlines(0, 0, len(ave_grads) + 1, lw=2, color="k")
+ plt.xticks(range(0, len(ave_grads), 1), layers, rotation="vertical")
+ plt.xlim(left=0, right=len(ave_grads))
+ plt.ylim(bottom=-0.001, top=max(max_grads) * 1.1)
+ plt.xlabel("Layers")
+ plt.ylabel("Gradient")
+ plt.title("Gradient Flow")
+ plt.grid(True, alpha=0.3)
+ plt.legend()
+ plt.tight_layout()
+
+ if save_path:
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
+ print(f"Gradient flow plot saved to {save_path}")
+
+ plt.close()
\ No newline at end of file
diff --git a/ML/tests/README_GUI.md b/ML/tests/README_GUI.md
new file mode 100644
index 00000000000..7b293b00dae
--- /dev/null
+++ b/ML/tests/README_GUI.md
@@ -0,0 +1,184 @@
+# NeuralForge GUI Tester
+
+Beautiful PyQt6 GUI application for testing your trained models!
+
+## Features
+
+✅ **Model Selection**
+- Browse for any `.pt` model file
+- Quick "Use Default" button for `models/final_model.pt`
+- Dataset selector (CIFAR-10, MNIST, etc.)
+- Real-time model loading with status
+
+✅ **Image Testing**
+- Browse and select any image
+- Live image preview (auto-scaled)
+- Drag-and-drop style interface
+
+✅ **Predictions**
+- Large, clear main prediction display
+- Confidence percentage
+- Top-5 predictions with visual bars
+- Progress indicator during inference
+
+✅ **Modern UI**
+- Dark theme (easy on eyes)
+- Green accent colors
+- Smooth animations
+- Professional styling
+
+## Installation
+
+```bash
+pip install PyQt6
+```
+
+## Usage
+
+### Run the GUI
+
+```bash
+python tests/gui_test.py
+```
+
+### Steps:
+
+1. **Load Model:**
+ - Click "Use Default" for your trained model
+ - Or browse to select a `.pt` file
+ - Select dataset (e.g., `cifar10`)
+ - Click "Load Model"
+
+2. **Select Image:**
+ - Click "Browse" to select an image
+ - Preview appears automatically
+
+3. **Predict:**
+ - Click "🔍 Predict" button
+ - Results appear instantly!
+
+## Screenshots
+
+### Main Interface
+```
+┌──────────────────────────────────────────────────────────┐
+│ 🚀 NeuralForge Model Tester │
+├───────────────────────┬──────────────────────────────────┤
+│ │ │
+│ Model Selection │ Prediction Results │
+│ ┌──────────────┐ │ ┌────────────────────────┐ │
+│ │ [Browse] │ │ │ 🎯 cat │ │
+│ │ [Use Default]│ │ │ Confidence: 94.3% │ │
+│ └──────────────┘ │ └────────────────────────┘ │
+│ │ │
+│ Image Selection │ Top-5 Predictions │
+│ ┌──────────────┐ │ ┌────────────────────────┐ │
+│ │ [Image] │ │ │ 1. cat ████████ 94% │ │
+│ │ Preview │ │ │ 2. dog ██ 3% │ │
+│ │ │ │ │ 3. deer █ 1% │ │
+│ └──────────────┘ │ └────────────────────────┘ │
+│ [🔍 Predict] │ │
+└───────────────────────┴──────────────────────────────────┘
+```
+
+## Features Explained
+
+### Model Information Display
+Shows:
+- Model architecture (ResNet18)
+- Dataset name
+- Number of classes
+- Total parameters
+- Training epoch
+- Best validation loss
+- Device (CPU/CUDA)
+
+### Prediction Display
+- **Main Prediction:** Large, bold display
+- **Confidence:** Percentage score
+- **Top-5:** Visual bar chart with percentages
+- **Color-coded:** Green for results, red for errors
+
+## Supported Datasets
+
+- CIFAR-10 (10 classes)
+- CIFAR-100 (100 classes)
+- MNIST (10 classes)
+- Fashion-MNIST (10 classes)
+- STL-10 (10 classes)
+- Tiny ImageNet (200 classes)
+- Food-101 (101 classes)
+- Caltech-256 (257 classes)
+- Oxford Pets (37 classes)
+- ImageNet (1000 classes)
+
+## Tips
+
+1. **Best Image Quality:** Use clear, well-lit images
+2. **Image Size:** Any size works (auto-resized to 224x224)
+3. **Format:** Supports PNG, JPG, JPEG, BMP, GIF
+4. **Multiple Tests:** Load once, test many images
+5. **Quick Access:** Keep commonly used models in `models/` folder
+
+## Keyboard Shortcuts
+
+- `Ctrl+O` - Browse model
+- `Ctrl+I` - Browse image
+- `Ctrl+P` - Predict (when ready)
+- `Ctrl+D` - Use default model
+
+## Troubleshooting
+
+**GUI won't start:**
+```bash
+pip install --upgrade PyQt6
+```
+
+**Model not loading:**
+- Check file path is correct
+- Ensure dataset name matches training dataset
+- Verify `.pt` file is not corrupted
+
+**Image not displaying:**
+- Check image file format
+- Ensure file exists
+- Try different image
+
+**Slow predictions:**
+- First prediction is slower (model warming up)
+- GPU mode is much faster than CPU
+- Check CUDA availability in Model Info
+
+## Advanced Usage
+
+### Testing Custom Models
+
+```python
+# Your model must be compatible with the interface
+# Save with: torch.save({'model_state_dict': model.state_dict()}, 'model.pt')
+```
+
+### Batch Testing
+
+Run multiple images sequentially:
+1. Load model once
+2. Browse and predict for each image
+3. Results update in real-time
+
+## Theme Customization
+
+The dark theme uses:
+- Background: `#1e1e1e`
+- Accent: `#4CAF50` (green)
+- Text: `#e0e0e0`
+- Borders: `#3d3d3d`
+
+To customize, edit the `apply_stylesheet()` method in `gui_test.py`.
+
+## Performance
+
+- **Loading:** ~1-2 seconds
+- **Prediction:** ~0.1-0.5 seconds (GPU)
+- **Memory:** ~500MB (model loaded)
+
+## Enjoy Testing! 🚀
diff --git a/ML/tests/SUPPORTED_DATASETS.txt b/ML/tests/SUPPORTED_DATASETS.txt
new file mode 100644
index 00000000000..b8f71ae86e0
--- /dev/null
+++ b/ML/tests/SUPPORTED_DATASETS.txt
@@ -0,0 +1,26 @@
+Supported Datasets for GUI:
+
+You can type any of these (with or without dashes/underscores):
+
+Small Datasets:
+✓ cifar10 / cifar-10 / cifar_10
+✓ cifar100 / cifar-100 / cifar_100
+✓ mnist
+✓ fashion_mnist / fashion-mnist / fashionmnist
+
+Medium Datasets:
+✓ stl10 / stl-10 / stl_10
+✓ tiny_imagenet / tiny-imagenet / tinyimagenet
+✓ oxford_pets / oxford-pets / oxfordpets
+✓ caltech256 / caltech-256 / caltech_256
+
+Large Datasets:
+✓ food101 / food-101 / food_101
+✓ imagenet
+
+All formats work! The GUI automatically normalizes the name.
+
+Examples:
+- Type "stl-10" or "stl10" or "stl_10" → Works!
+- Type "tiny-imagenet" or "tinyimagenet" → Works!
+- Type "fashion-mnist" or "fashionmnist" → Works!
diff --git a/ML/tests/gui_test.py b/ML/tests/gui_test.py
new file mode 100644
index 00000000000..c368a004ae2
--- /dev/null
+++ b/ML/tests/gui_test.py
@@ -0,0 +1,492 @@
+import sys
+import os
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
+ QHBoxLayout, QPushButton, QLabel, QLineEdit,
+ QFileDialog, QProgressBar, QTextEdit, QGroupBox,
+ QGridLayout)
+from PyQt6.QtCore import Qt, QThread, pyqtSignal
+from PyQt6.QtGui import QPixmap, QFont
+
+import torch
+import torch.nn.functional as F
+from torchvision import transforms
+from PIL import Image
+
+from src.python.neuralforge.data.datasets import get_dataset, get_num_classes
+from src.python.neuralforge.models.resnet import ResNet18
+
+class PredictionThread(QThread):
+ finished = pyqtSignal(list, list, str)
+ error = pyqtSignal(str)
+
+ def __init__(self, model, image_path, classes, device):
+ super().__init__()
+ self.model = model
+ self.image_path = image_path
+ self.classes = classes
+ self.device = device
+
+ def run(self):
+ try:
+ image = Image.open(self.image_path).convert('RGB')
+
+ transform = transforms.Compose([
+ transforms.Resize(256),
+ transforms.CenterCrop(224),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ])
+
+ image_tensor = transform(image).unsqueeze(0).to(self.device)
+
+ with torch.no_grad():
+ outputs = self.model(image_tensor)
+ probabilities = F.softmax(outputs, dim=1)
+
+ top5_prob, top5_idx = torch.topk(probabilities, min(5, len(self.classes)), dim=1)
+
+ predictions = []
+ confidences = []
+
+ for idx, prob in zip(top5_idx[0].cpu().numpy(), top5_prob[0].cpu().numpy()):
+ predictions.append(self.classes[idx])
+ confidences.append(float(prob) * 100)
+
+ main_prediction = predictions[0]
+
+ self.finished.emit(predictions, confidences, main_prediction)
+
+ except Exception as e:
+ self.error.emit(str(e))
+
+class NeuralForgeGUI(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.model = None
+ self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
+ self.classes = []
+ self.dataset_name = 'cifar10'
+
+ self.init_ui()
+ self.apply_stylesheet()
+
+ def init_ui(self):
+ self.setWindowTitle('NeuralForge - Model Tester')
+ self.setGeometry(100, 100, 1200, 800)
+
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ main_layout = QHBoxLayout()
+ central_widget.setLayout(main_layout)
+
+ left_panel = self.create_left_panel()
+ right_panel = self.create_right_panel()
+
+ main_layout.addWidget(left_panel, 1)
+ main_layout.addWidget(right_panel, 1)
+
+ def create_left_panel(self):
+ panel = QWidget()
+ layout = QVBoxLayout()
+ panel.setLayout(layout)
+
+ title = QLabel('🚀 NeuralForge Model Tester')
+ title.setFont(QFont('Arial', 20, QFont.Weight.Bold))
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(title)
+
+ model_group = QGroupBox('Model Selection')
+ model_layout = QVBoxLayout()
+
+ model_path_layout = QHBoxLayout()
+ self.model_path_input = QLineEdit()
+ self.model_path_input.setPlaceholderText('Path to model file (.pt)')
+ model_path_layout.addWidget(self.model_path_input)
+
+ browse_btn = QPushButton('Browse')
+ browse_btn.clicked.connect(self.browse_model)
+ model_path_layout.addWidget(browse_btn)
+
+ default_btn = QPushButton('Use Default')
+ default_btn.clicked.connect(self.use_default_model)
+ model_path_layout.addWidget(default_btn)
+
+ model_layout.addLayout(model_path_layout)
+
+ dataset_layout = QHBoxLayout()
+ dataset_label = QLabel('Dataset:')
+ self.dataset_input = QLineEdit('cifar10')
+ self.dataset_input.setPlaceholderText('cifar10, mnist, stl10, tiny_imagenet, etc.')
+ self.dataset_input.setToolTip('Supported: cifar10, cifar100, mnist, fashion_mnist, stl10,\ntiny_imagenet, imagenet, food101, caltech256, oxford_pets')
+ dataset_layout.addWidget(dataset_label)
+ dataset_layout.addWidget(self.dataset_input)
+ model_layout.addLayout(dataset_layout)
+
+ self.load_model_btn = QPushButton('Load Model')
+ self.load_model_btn.clicked.connect(self.load_model)
+ model_layout.addWidget(self.load_model_btn)
+
+ self.model_status = QLabel('No model loaded')
+ self.model_status.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ model_layout.addWidget(self.model_status)
+
+ model_group.setLayout(model_layout)
+ layout.addWidget(model_group)
+
+ image_group = QGroupBox('Image Selection')
+ image_layout = QVBoxLayout()
+
+ image_path_layout = QHBoxLayout()
+ self.image_path_input = QLineEdit()
+ self.image_path_input.setPlaceholderText('Path to image file')
+ image_path_layout.addWidget(self.image_path_input)
+
+ browse_image_btn = QPushButton('Browse')
+ browse_image_btn.clicked.connect(self.browse_image)
+ image_path_layout.addWidget(browse_image_btn)
+
+ image_layout.addLayout(image_path_layout)
+
+ self.image_preview = QLabel()
+ self.image_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.image_preview.setMinimumHeight(300)
+ self.image_preview.setStyleSheet('border: 2px dashed #666; border-radius: 10px;')
+ self.image_preview.setText('No image selected')
+ image_layout.addWidget(self.image_preview)
+
+ self.predict_btn = QPushButton('🔍 Predict')
+ self.predict_btn.clicked.connect(self.predict_image)
+ self.predict_btn.setEnabled(False)
+ image_layout.addWidget(self.predict_btn)
+
+ image_group.setLayout(image_layout)
+ layout.addWidget(image_group)
+
+ layout.addStretch()
+
+ return panel
+
+ def create_right_panel(self):
+ panel = QWidget()
+ layout = QVBoxLayout()
+ panel.setLayout(layout)
+
+ results_group = QGroupBox('Prediction Results')
+ results_layout = QVBoxLayout()
+
+ self.main_prediction = QLabel('No prediction yet')
+ self.main_prediction.setFont(QFont('Arial', 24, QFont.Weight.Bold))
+ self.main_prediction.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.main_prediction.setStyleSheet('color: #4CAF50; padding: 20px;')
+ results_layout.addWidget(self.main_prediction)
+
+ self.confidence_label = QLabel('')
+ self.confidence_label.setFont(QFont('Arial', 16))
+ self.confidence_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ results_layout.addWidget(self.confidence_label)
+
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setVisible(False)
+ results_layout.addWidget(self.progress_bar)
+
+ results_group.setLayout(results_layout)
+ layout.addWidget(results_group)
+
+ top5_group = QGroupBox('Top-5 Predictions')
+ top5_layout = QVBoxLayout()
+
+ self.top5_display = QTextEdit()
+ self.top5_display.setReadOnly(True)
+ self.top5_display.setMinimumHeight(200)
+ top5_layout.addWidget(self.top5_display)
+
+ top5_group.setLayout(top5_layout)
+ layout.addWidget(top5_group)
+
+ info_group = QGroupBox('Model Information')
+ info_layout = QVBoxLayout()
+
+ self.model_info = QTextEdit()
+ self.model_info.setReadOnly(True)
+ self.model_info.setMaximumHeight(150)
+ info_layout.addWidget(self.model_info)
+
+ info_group.setLayout(info_layout)
+ layout.addWidget(info_group)
+
+ layout.addStretch()
+
+ return panel
+
+ def apply_stylesheet(self):
+ qss = """
+ QMainWindow {
+ background-color: #1e1e1e;
+ }
+
+ QWidget {
+ background-color: #1e1e1e;
+ color: #e0e0e0;
+ font-family: 'Segoe UI', Arial;
+ font-size: 12px;
+ }
+
+ QGroupBox {
+ border: 2px solid #3d3d3d;
+ border-radius: 8px;
+ margin-top: 10px;
+ padding-top: 15px;
+ font-weight: bold;
+ color: #4CAF50;
+ }
+
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 5px;
+ }
+
+ QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 5px;
+ font-weight: bold;
+ font-size: 13px;
+ }
+
+ QPushButton:hover {
+ background-color: #45a049;
+ }
+
+ QPushButton:pressed {
+ background-color: #3d8b40;
+ }
+
+ QPushButton:disabled {
+ background-color: #555555;
+ color: #888888;
+ }
+
+ QLineEdit {
+ background-color: #2d2d2d;
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ padding: 8px;
+ color: #e0e0e0;
+ }
+
+ QLineEdit:focus {
+ border: 2px solid #4CAF50;
+ }
+
+ QTextEdit {
+ background-color: #2d2d2d;
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ padding: 10px;
+ color: #e0e0e0;
+ }
+
+ QLabel {
+ color: #e0e0e0;
+ }
+
+ QProgressBar {
+ border: 2px solid #3d3d3d;
+ border-radius: 5px;
+ text-align: center;
+ background-color: #2d2d2d;
+ }
+
+ QProgressBar::chunk {
+ background-color: #4CAF50;
+ border-radius: 3px;
+ }
+ """
+ self.setStyleSheet(qss)
+
+ def browse_model(self):
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ 'Select Model File',
+ '../models',
+ 'Model Files (*.pt *.pth);;All Files (*.*)'
+ )
+ if file_path:
+ self.model_path_input.setText(file_path)
+
+ def use_default_model(self):
+ default_path = os.path.join(os.path.dirname(__file__), '..', 'models', 'final_model.pt')
+ self.model_path_input.setText(os.path.abspath(default_path))
+
+ def browse_image(self):
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ 'Select Image File',
+ '',
+ 'Image Files (*.png *.jpg *.jpeg *.bmp *.gif);;All Files (*.*)'
+ )
+ if file_path:
+ self.image_path_input.setText(file_path)
+ self.display_image(file_path)
+
+ def display_image(self, image_path):
+ try:
+ pixmap = QPixmap(image_path)
+ scaled_pixmap = pixmap.scaled(400, 300, Qt.AspectRatioMode.KeepAspectRatio,
+ Qt.TransformationMode.SmoothTransformation)
+ self.image_preview.setPixmap(scaled_pixmap)
+ except Exception as e:
+ self.image_preview.setText(f'Error loading image: {e}')
+
+ def load_model(self):
+ model_path = self.model_path_input.text()
+ dataset_input = self.dataset_input.text().lower().strip()
+
+ dataset_aliases = {
+ 'cifar10': 'cifar10',
+ 'cifar-10': 'cifar10',
+ 'cifar_10': 'cifar10',
+ 'cifar100': 'cifar100',
+ 'cifar-100': 'cifar100',
+ 'cifar_100': 'cifar100',
+ 'mnist': 'mnist',
+ 'fashionmnist': 'fashion_mnist',
+ 'fashion-mnist': 'fashion_mnist',
+ 'fashion_mnist': 'fashion_mnist',
+ 'stl10': 'stl10',
+ 'stl-10': 'stl10',
+ 'stl_10': 'stl10',
+ 'tinyimagenet': 'tiny_imagenet',
+ 'tiny-imagenet': 'tiny_imagenet',
+ 'tiny_imagenet': 'tiny_imagenet',
+ 'imagenet': 'imagenet',
+ 'food101': 'food101',
+ 'food-101': 'food101',
+ 'food_101': 'food101',
+ 'caltech256': 'caltech256',
+ 'caltech-256': 'caltech256',
+ 'caltech_256': 'caltech256',
+ 'oxfordpets': 'oxford_pets',
+ 'oxford-pets': 'oxford_pets',
+ 'oxford_pets': 'oxford_pets',
+ }
+
+ self.dataset_name = dataset_aliases.get(dataset_input, dataset_input)
+
+ if not model_path:
+ self.model_status.setText('Please select a model file')
+ self.model_status.setStyleSheet('color: #f44336;')
+ return
+
+ if not os.path.exists(model_path):
+ self.model_status.setText('Model file not found')
+ self.model_status.setStyleSheet('color: #f44336;')
+ return
+
+ try:
+ self.model_status.setText('Loading model...')
+ self.model_status.setStyleSheet('color: #FFC107;')
+ QApplication.processEvents()
+
+ num_classes = get_num_classes(self.dataset_name)
+ self.model = ResNet18(num_classes=num_classes)
+ self.model = self.model.to(self.device)
+
+ checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
+ self.model.load_state_dict(checkpoint['model_state_dict'])
+ self.model.eval()
+
+ try:
+ dataset = get_dataset(self.dataset_name, train=False, download=False)
+ self.classes = getattr(dataset, 'classes', [str(i) for i in range(num_classes)])
+ except:
+ from src.python.neuralforge.data.datasets import get_class_names
+ self.classes = get_class_names(self.dataset_name)
+
+ self.model_status.setText(f'✓ Model loaded successfully')
+ self.model_status.setStyleSheet('color: #4CAF50;')
+
+ self.predict_btn.setEnabled(True)
+
+ total_params = sum(p.numel() for p in self.model.parameters())
+ epoch = checkpoint.get('epoch', 'Unknown')
+ val_loss = checkpoint.get('best_val_loss', 'Unknown')
+
+ val_loss_str = f"{val_loss:.4f}" if isinstance(val_loss, float) else str(val_loss)
+
+ info_text = f"""
+Model: ResNet18
+Dataset: {self.dataset_name.upper()}
+Classes: {num_classes}
+Parameters: {total_params:,}
+Epoch: {epoch}
+Best Val Loss: {val_loss_str}
+Device: {self.device.upper()}
+ """
+ self.model_info.setText(info_text.strip())
+
+ except Exception as e:
+ self.model_status.setText(f'Error: {str(e)}')
+ self.model_status.setStyleSheet('color: #f44336;')
+
+ def predict_image(self):
+ image_path = self.image_path_input.text()
+
+ if not image_path or not os.path.exists(image_path):
+ self.main_prediction.setText('Please select a valid image')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+ return
+
+ if self.model is None:
+ self.main_prediction.setText('Please load a model first')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+ return
+
+ self.predict_btn.setEnabled(False)
+ self.progress_bar.setVisible(True)
+ self.progress_bar.setRange(0, 0)
+
+ self.prediction_thread = PredictionThread(self.model, image_path, self.classes, self.device)
+ self.prediction_thread.finished.connect(self.display_results)
+ self.prediction_thread.error.connect(self.display_error)
+ self.prediction_thread.start()
+
+ def display_results(self, predictions, confidences, main_prediction):
+ self.progress_bar.setVisible(False)
+ self.predict_btn.setEnabled(True)
+
+ self.main_prediction.setText(f'🎯 {main_prediction}')
+ self.main_prediction.setStyleSheet('color: #4CAF50; padding: 20px; font-size: 28px;')
+
+ self.confidence_label.setText(f'Confidence: {confidences[0]:.2f}%')
+
+ top5_text = 'Top-5 Predictions:
'
+ for i, (pred, conf) in enumerate(zip(predictions, confidences), 1):
+ bar_width = int(conf * 3)
+ bar = '█' * bar_width
+ top5_text += f'{i}. {pred}
'
+ top5_text += f'{bar} {conf:.2f}%
'
+
+ self.top5_display.setHtml(top5_text)
+
+ def display_error(self, error_msg):
+ self.progress_bar.setVisible(False)
+ self.predict_btn.setEnabled(True)
+
+ self.main_prediction.setText(f'Error: {error_msg}')
+ self.main_prediction.setStyleSheet('color: #f44336;')
+
+def main():
+ app = QApplication(sys.argv)
+ window = NeuralForgeGUI()
+ window.show()
+ sys.exit(app.exec())
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/ML/tests/quick_test.py b/ML/tests/quick_test.py
new file mode 100644
index 00000000000..7d89d2c36e7
--- /dev/null
+++ b/ML/tests/quick_test.py
@@ -0,0 +1,48 @@
+import sys
+import os
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+import torch
+from src.python.neuralforge.data.datasets import get_dataset
+from src.python.neuralforge.models.resnet import ResNet18
+
+print("=" * 60)
+print(" NeuralForge Quick Test")
+print("=" * 60)
+
+print("\n[1/3] Testing CIFAR-10 dataset download...")
+try:
+ dataset = get_dataset('cifar10', root='./data', train=False, download=True)
+ print(f"✓ CIFAR-10 loaded: {len(dataset)} samples")
+ print(f" Classes: {dataset.classes}")
+except Exception as e:
+ print(f"✗ Failed: {e}")
+
+print("\n[2/3] Testing model creation...")
+try:
+ model = ResNet18(num_classes=10)
+ print(f"✓ Model created: {sum(p.numel() for p in model.parameters()):,} parameters")
+except Exception as e:
+ print(f"✗ Failed: {e}")
+
+print("\n[3/3] Testing inference...")
+try:
+ model.eval()
+ image, label = dataset[0]
+ with torch.no_grad():
+ output = model(image.unsqueeze(0))
+ print(f"✓ Inference successful: output shape {output.shape}")
+ print(f" True label: {dataset.classes[label]}")
+ pred = output.argmax(1).item()
+ print(f" Predicted: {dataset.classes[pred]}")
+except Exception as e:
+ print(f"✗ Failed: {e}")
+
+print("\n" + "=" * 60)
+print(" All tests passed! Ready to train.")
+print("=" * 60)
+print("\nTry these commands:")
+print(" python train.py --dataset cifar10 --epochs 20")
+print(" python tests/test_model.py --dataset cifar10 --mode interactive")
+print("=" * 60)
diff --git a/ML/tests/test_model.py b/ML/tests/test_model.py
new file mode 100644
index 00000000000..b1fe00d72fa
--- /dev/null
+++ b/ML/tests/test_model.py
@@ -0,0 +1,265 @@
+import sys
+import os
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+import torch
+import torch.nn.functional as F
+from torchvision import transforms
+from PIL import Image
+import numpy as np
+
+from src.python.neuralforge.data.datasets import get_dataset, get_num_classes, get_class_names
+from src.python.neuralforge.models.resnet import ResNet18
+
+class ModelTester:
+ def __init__(self, model_path='./models/best_model.pt', dataset='cifar10', device='cuda'):
+ self.device = device if torch.cuda.is_available() else 'cpu'
+ self.dataset_name = dataset
+
+ print("=" * 60)
+ print(" NeuralForge - Interactive Model Testing")
+ print("=" * 60)
+ print(f"Device: {self.device}")
+
+ num_classes = get_num_classes(dataset)
+ self.model = self.create_model(num_classes)
+
+ if os.path.exists(model_path):
+ print(f"Loading model from: {model_path}")
+ checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
+ self.model.load_state_dict(checkpoint['model_state_dict'])
+ print(f"Model loaded from epoch {checkpoint['epoch']}")
+ else:
+ print(f"Warning: No model found at {model_path}, using untrained model")
+
+ self.model.eval()
+
+ test_dataset = get_dataset(dataset, root='./data', train=False, download=True)
+ self.dataset = test_dataset.dataset
+ self.classes = get_class_names(dataset)
+
+ if dataset in ['mnist', 'fashion_mnist']:
+ self.image_size = 28
+ elif dataset in ['cifar10', 'cifar100']:
+ self.image_size = 32
+ elif dataset == 'stl10':
+ self.image_size = 96
+ else:
+ self.image_size = 224
+
+ print(f"Dataset: {dataset} ({len(self.dataset)} test samples)")
+ print(f"Classes: {len(self.classes)}")
+ print("=" * 60)
+
+ def create_model(self, num_classes):
+ model = ResNet18(num_classes=num_classes)
+ return model.to(self.device)
+
+ def predict_image(self, image_tensor):
+ with torch.no_grad():
+ image_tensor = image_tensor.unsqueeze(0).to(self.device)
+ outputs = self.model(image_tensor)
+ probabilities = F.softmax(outputs, dim=1)
+ confidence, predicted = torch.max(probabilities, 1)
+
+ top5_prob, top5_idx = torch.topk(probabilities, min(5, len(self.classes)), dim=1)
+
+ return predicted.item(), confidence.item(), top5_idx[0].cpu().numpy(), top5_prob[0].cpu().numpy()
+
+ def test_random_samples(self, num_samples=10):
+ print(f"\nTesting {num_samples} random samples...")
+ print("-" * 60)
+
+ correct = 0
+ indices = np.random.choice(len(self.dataset), num_samples, replace=False)
+
+ for i, idx in enumerate(indices, 1):
+ image, label = self.dataset[idx]
+ pred_class, confidence, top5_idx, top5_prob = self.predict_image(image)
+
+ true_label = self.classes[label]
+ pred_label = self.classes[pred_class]
+
+ is_correct = pred_class == label
+ correct += is_correct
+
+ status = "✓" if is_correct else "✗"
+ print(f"{i:2d}. {status} True: {true_label:15s} | Pred: {pred_label:15s} | Conf: {confidence:.2%}")
+
+ if not is_correct:
+ print(f" Top-5: ", end="")
+ for j, (idx, prob) in enumerate(zip(top5_idx, top5_prob)):
+ print(f"{self.classes[idx]}({prob:.1%})", end=" ")
+ print()
+
+ accuracy = correct / num_samples
+ print("-" * 60)
+ print(f"Accuracy: {accuracy:.1%} ({correct}/{num_samples})")
+
+ def test_specific_sample(self, index):
+ if index < 0 or index >= len(self.dataset):
+ print(f"Error: Index must be between 0 and {len(self.dataset)-1}")
+ return
+
+ image, label = self.dataset[index]
+ pred_class, confidence, top5_idx, top5_prob = self.predict_image(image)
+
+ print(f"\nSample #{index}")
+ print("-" * 60)
+ print(f"True Label: {self.classes[label]}")
+ print(f"Predicted: {self.classes[pred_class]}")
+ print(f"Confidence: {confidence:.2%}")
+ print(f"Status: {'✓ Correct' if pred_class == label else '✗ Wrong'}")
+ print("\nTop-5 Predictions:")
+ for i, (idx, prob) in enumerate(zip(top5_idx, top5_prob), 1):
+ print(f" {i}. {self.classes[idx]:15s} {prob:.2%}")
+
+ def test_class_accuracy(self):
+ print("\nCalculating per-class accuracy...")
+ print("-" * 60)
+
+ class_correct = [0] * len(self.classes)
+ class_total = [0] * len(self.classes)
+
+ with torch.no_grad():
+ for i, (image, label) in enumerate(self.dataset):
+ pred_class, _, _, _ = self.predict_image(image)
+ class_total[label] += 1
+ if pred_class == label:
+ class_correct[label] += 1
+
+ if (i + 1) % 100 == 0:
+ print(f"Processed {i + 1}/{len(self.dataset)} samples...", end='\r')
+
+ print(" " * 60, end='\r')
+ print("Per-class Accuracy:")
+
+ overall_correct = sum(class_correct)
+ overall_total = sum(class_total)
+
+ for i, class_name in enumerate(self.classes):
+ if class_total[i] > 0:
+ acc = 100.0 * class_correct[i] / class_total[i]
+ print(f" {class_name:15s}: {acc:5.1f}% ({class_correct[i]}/{class_total[i]})")
+
+ print("-" * 60)
+ print(f"Overall Accuracy: {100.0 * overall_correct / overall_total:.2f}% ({overall_correct}/{overall_total})")
+
+ def test_custom_image(self, image_path):
+ if not os.path.exists(image_path):
+ print(f"Error: Image not found at {image_path}")
+ return
+
+ try:
+ image = Image.open(image_path).convert('RGB')
+
+ transform = transforms.Compose([
+ transforms.Resize((self.image_size, self.image_size)),
+ transforms.ToTensor(),
+ ])
+
+ image_tensor = transform(image)
+ pred_class, confidence, top5_idx, top5_prob = self.predict_image(image_tensor)
+
+ print(f"\nCustom Image: {image_path}")
+ print("-" * 60)
+ print(f"Predicted: {self.classes[pred_class]}")
+ print(f"Confidence: {confidence:.2%}")
+ print("\nTop-5 Predictions:")
+ for i, (idx, prob) in enumerate(zip(top5_idx, top5_prob), 1):
+ print(f" {i}. {self.classes[idx]:15s} {prob:.2%}")
+
+ except Exception as e:
+ print(f"Error loading image: {e}")
+
+ def interactive_mode(self):
+ print("\n" + "=" * 60)
+ print(" Interactive Mode")
+ print("=" * 60)
+ print("\nCommands:")
+ print(" random [N] - Test N random samples (default: 10)")
+ print(" sample - Test specific sample by index")
+ print(" image - Test custom image file")
+ print(" accuracy - Calculate full test set accuracy")
+ print(" help - Show this help")
+ print(" exit - Exit interactive mode")
+ print()
+
+ while True:
+ try:
+ command = input(">>> ").strip().lower()
+
+ if not command:
+ continue
+
+ if command == 'exit' or command == 'quit':
+ print("Exiting...")
+ break
+
+ elif command == 'help':
+ self.interactive_mode()
+ return
+
+ elif command.startswith('random'):
+ parts = command.split()
+ n = int(parts[1]) if len(parts) > 1 else 10
+ self.test_random_samples(n)
+
+ elif command.startswith('sample'):
+ parts = command.split()
+ if len(parts) < 2:
+ print("Usage: sample ")
+ else:
+ idx = int(parts[1])
+ self.test_specific_sample(idx)
+
+ elif command.startswith('image'):
+ parts = command.split(maxsplit=1)
+ if len(parts) < 2:
+ print("Usage: image ")
+ else:
+ self.test_custom_image(parts[1])
+
+ elif command == 'accuracy':
+ self.test_class_accuracy()
+
+ else:
+ print(f"Unknown command: {command}")
+ print("Type 'help' for available commands")
+
+ except KeyboardInterrupt:
+ print("\nExiting...")
+ break
+ except Exception as e:
+ print(f"Error: {e}")
+
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Test trained NeuralForge model')
+
+ default_model = os.path.join(os.path.dirname(__file__), '..', 'models', 'best_model.pt')
+ parser.add_argument('--model', type=str, default=default_model, help='Path to model checkpoint')
+ parser.add_argument('--dataset', type=str, default='cifar10',
+ choices=['cifar10', 'cifar100', 'mnist', 'fashion_mnist', 'stl10',
+ 'tiny_imagenet', 'imagenet', 'food101', 'caltech256', 'oxford_pets'],
+ help='Dataset to test on')
+ parser.add_argument('--device', type=str, default='cuda', help='Device to use')
+ parser.add_argument('--mode', type=str, default='interactive',
+ choices=['interactive', 'random', 'accuracy'],
+ help='Testing mode')
+ parser.add_argument('--samples', type=int, default=10, help='Number of samples for random mode')
+ args = parser.parse_args()
+
+ tester = ModelTester(model_path=args.model, dataset=args.dataset, device=args.device)
+
+ if args.mode == 'interactive':
+ tester.interactive_mode()
+ elif args.mode == 'random':
+ tester.test_random_samples(args.samples)
+ elif args.mode == 'accuracy':
+ tester.test_class_accuracy()
+
+if __name__ == '__main__':
+ main()
diff --git a/ML/train.py b/ML/train.py
new file mode 100644
index 00000000000..66f0be14e36
--- /dev/null
+++ b/ML/train.py
@@ -0,0 +1,196 @@
+import torch
+import torch.nn as nn
+import torch.optim as optim
+import argparse
+import os
+import random
+import numpy as np
+
+from src.python.neuralforge import nn as nf_nn
+from src.python.neuralforge import optim as nf_optim
+from src.python.neuralforge.trainer import Trainer
+from src.python.neuralforge.config import Config
+from src.python.neuralforge.data.dataset import SyntheticDataset, DataLoaderBuilder
+from src.python.neuralforge.data.datasets import get_dataset, get_num_classes
+from src.python.neuralforge.data.transforms import get_transforms
+from src.python.neuralforge.models.resnet import ResNet18
+from src.python.neuralforge.utils.logger import Logger
+
+def set_seed(seed):
+ random.seed(seed)
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+ torch.backends.cudnn.deterministic = True
+ torch.backends.cudnn.benchmark = False
+
+def create_simple_model(num_classes=10):
+ return nn.Sequential(
+ nn.Conv2d(3, 32, 3, padding=1),
+ nn.BatchNorm2d(32),
+ nn.ReLU(inplace=True),
+ nn.MaxPool2d(2),
+
+ nn.Conv2d(32, 64, 3, padding=1),
+ nn.BatchNorm2d(64),
+ nn.ReLU(inplace=True),
+ nn.MaxPool2d(2),
+
+ nn.Conv2d(64, 128, 3, padding=1),
+ nn.BatchNorm2d(128),
+ nn.ReLU(inplace=True),
+ nn.AdaptiveAvgPool2d(1),
+
+ nn.Flatten(),
+ nn.Linear(128, num_classes)
+ )
+
+def main():
+ parser = argparse.ArgumentParser(description='NeuralForge Training')
+ parser.add_argument('--config', type=str, default=None, help='Path to config file')
+ parser.add_argument('--model', type=str, default='simple', choices=['simple', 'resnet18', 'efficientnet', 'vit'])
+ parser.add_argument('--batch-size', type=int, default=32)
+ parser.add_argument('--epochs', type=int, default=50)
+ parser.add_argument('--lr', type=float, default=0.001)
+ parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu')
+ parser.add_argument('--num-samples', type=int, default=5000, help='Number of synthetic samples')
+ parser.add_argument('--num-classes', type=int, default=10)
+ parser.add_argument('--seed', type=int, default=42)
+ parser.add_argument('--dataset', type=str, default='synthetic',
+ choices=['synthetic', 'cifar10', 'cifar100', 'mnist', 'fashion_mnist', 'stl10',
+ 'tiny_imagenet', 'imagenet', 'food101', 'caltech256', 'oxford_pets'],
+ help='Dataset to use')
+ args = parser.parse_args()
+
+ if args.config:
+ config = Config.load(args.config)
+ else:
+ config = Config()
+ config.batch_size = args.batch_size
+ config.epochs = args.epochs
+ config.learning_rate = args.lr
+ config.device = args.device
+ config.num_classes = args.num_classes
+ config.seed = args.seed
+
+ set_seed(config.seed)
+
+ logger = Logger(config.log_dir, "training")
+ logger.info("=" * 80)
+ logger.info("NeuralForge Training Framework")
+ logger.info("=" * 80)
+ logger.info(f"Configuration:\n{config}")
+
+ if args.dataset == 'synthetic':
+ logger.info("Creating synthetic dataset...")
+ train_dataset = SyntheticDataset(
+ num_samples=args.num_samples,
+ num_classes=config.num_classes,
+ image_size=config.image_size,
+ channels=3
+ )
+
+ val_dataset = SyntheticDataset(
+ num_samples=args.num_samples // 5,
+ num_classes=config.num_classes,
+ image_size=config.image_size,
+ channels=3
+ )
+ else:
+ logger.info(f"Downloading and loading {args.dataset} dataset...")
+ config.num_classes = get_num_classes(args.dataset)
+
+ train_dataset = get_dataset(args.dataset, root=config.data_path, train=True, download=True)
+ val_dataset = get_dataset(args.dataset, root=config.data_path, train=False, download=True)
+
+ if args.dataset in ['mnist', 'fashion_mnist']:
+ config.image_size = 28
+ elif args.dataset in ['cifar10', 'cifar100']:
+ config.image_size = 32
+ elif args.dataset == 'tiny_imagenet':
+ config.image_size = 64
+ elif args.dataset == 'stl10':
+ config.image_size = 96
+ elif args.dataset in ['imagenet', 'food101', 'caltech256', 'oxford_pets']:
+ config.image_size = 224
+
+ loader_builder = DataLoaderBuilder(config)
+ train_loader = loader_builder.build_train_loader(train_dataset)
+ val_loader = loader_builder.build_val_loader(val_dataset)
+
+ logger.info(f"Train dataset size: {len(train_dataset)}")
+ logger.info(f"Validation dataset size: {len(val_dataset)}")
+
+ logger.info(f"Creating model: {args.model}")
+ if args.model == 'simple':
+ model = create_simple_model(config.num_classes)
+ elif args.model == 'resnet18':
+ model = ResNet18(num_classes=config.num_classes)
+ else:
+ model = create_simple_model(config.num_classes)
+
+ logger.log_model_summary(model)
+
+ criterion = nn.CrossEntropyLoss()
+
+ if config.optimizer.lower() == 'adamw':
+ optimizer = nf_optim.AdamW(
+ model.parameters(),
+ lr=config.learning_rate,
+ weight_decay=config.weight_decay
+ )
+ elif config.optimizer.lower() == 'adam':
+ optimizer = optim.Adam(
+ model.parameters(),
+ lr=config.learning_rate,
+ weight_decay=config.weight_decay
+ )
+ else:
+ optimizer = optim.SGD(
+ model.parameters(),
+ lr=config.learning_rate,
+ momentum=0.9,
+ weight_decay=config.weight_decay
+ )
+
+ if config.scheduler == 'cosine':
+ scheduler = nf_optim.CosineAnnealingWarmRestarts(
+ optimizer,
+ T_0=10,
+ T_mult=2,
+ eta_min=1e-6
+ )
+ elif config.scheduler == 'onecycle':
+ scheduler = nf_optim.OneCycleLR(
+ optimizer,
+ max_lr=config.learning_rate,
+ total_steps=config.epochs * len(train_loader)
+ )
+ else:
+ scheduler = None
+
+ logger.info(f"Optimizer: {config.optimizer}")
+ logger.info(f"Scheduler: {config.scheduler}")
+
+ trainer = Trainer(
+ model=model,
+ train_loader=train_loader,
+ val_loader=val_loader,
+ optimizer=optimizer,
+ criterion=criterion,
+ config=config,
+ scheduler=scheduler,
+ device=config.device
+ )
+
+ logger.info("Starting training...")
+ trainer.train()
+
+ logger.info("Training completed successfully!")
+ logger.info(f"Best validation loss: {trainer.best_val_loss:.4f}")
+
+ config.save(os.path.join(config.log_dir, 'config.json'))
+ logger.info(f"Configuration saved to {os.path.join(config.log_dir, 'config.json')}")
+
+if __name__ == '__main__':
+ main()
diff --git a/Multiply.py b/Multiply.py
index c8e1b52228f..8d4121cfe56 100644
--- a/Multiply.py
+++ b/Multiply.py
@@ -1,4 +1,8 @@
def product(a, b):
+ # Handle negative values
+ if b < 0:
+ return -product(a, -b)
+
if a < b:
return product(b, a)
elif b != 0:
@@ -9,4 +13,4 @@ def product(a, b):
a = int(input("Enter first number: "))
b = int(input("Enter second number: "))
-print("Product is: ", product(a, b))
+print("Product is:", product(a, b))
diff --git a/NumberToNumberName/numbername.py b/NumberToNumberName/numbername.py
new file mode 100644
index 00000000000..8eae393db6b
--- /dev/null
+++ b/NumberToNumberName/numbername.py
@@ -0,0 +1,141 @@
+# A program to write a number in words
+# Eg:
+# 61893: Sixty One Thousand Eight Hundred Ninety Three
+
+__import__('os').system('cls')
+
+
+Y = "\033[38;2;255;200;0m"
+W = "\033[38;2;212;212;212;0m"
+B = "\033[38;2;108;180;238m;0m"
+
+groupedList = []
+name = ""
+nameList = []
+
+numDict = {
+ 1 : "One", 2 : "Two", 3 : "Three", 4 : "Four", 5 : "Five",
+ 6 : "Six", 7 : "Seven", 8 : "Eight", 9 : "Nine", 10 : "Ten",
+ 11 : "Eleven", 12 : "Twelve", 13 : "Thirteen", 14 : "Fourteen", 15 : "Fifteen",
+ 16 : "Sixteen", 17 : "Seventeen", 18 : "Eighteen", 19 : "Ninteen", 20 : "Twenty",
+ 30 : "Thirty", 40 : "Forty", 50 : "Fifty", 60 : "Sixty", 70 : "Seventy",
+ 80 : "Eighty", 90 : "Ninety"
+}
+
+digits = {
+ "1" : "One", "2" : "Two", "3" : "Three", "4" : "Four", "5" : "Five",
+ "6" : "Six", "7" : "Seven", "8" : "Eight", "9" : "Nine", "0" : "Zero"
+}
+
+placeValueDict = {
+ 1 : "",
+ 2 : "Thousand",
+ 3 : "Million",
+ 4 : "Billion",
+ 5 : "Trillion",
+ 6 : "Quadrillion",
+ 7 : "Quintillion",
+ 8 : "Sextillion",
+ 9 : "Septilion",
+ 10 : "Octillion"
+}
+
+print("Maximum Input: 999,999,999,999,999,999,999,999,999,999")
+print("Minimum Input: -999,999,999,999,999,999,999,999,999,999\n")
+
+isNegative = False
+
+while True:
+ num = input(f"Enter a number: {Y}")
+ print(f"{W}", end="")
+
+ try:
+ splittedNum = num.split(".")
+
+ splittedNum[0] = splittedNum[0].replace(" ", "")
+ if len(splittedNum) == 2:
+ splittedNum[1] = splittedNum[1].replace(" ", "")
+ splittedNum[1] = splittedNum[1].rstrip("0")
+
+ if splittedNum[1] == "":
+ splittedNum.remove("")
+
+ num = int(splittedNum[0])
+
+ if len(splittedNum) == 1:
+ placeholder = splittedNum[0]
+ placeholder = int(placeholder)
+ else:
+ placeholder = splittedNum[0] + "." + splittedNum[1]
+ placeholder = float(placeholder)
+
+ if num >= 1000000000000000000000000000000 or num <= -1000000000000000000000000000000:
+ print("Input out of range\n")
+ else:
+ if num < 0:
+ isNegative = True
+ num = num * (-1)
+ break
+ except ValueError or EOFError:
+ print("Invalid Input\n")
+
+
+if num == 0:
+ print(f"0 in words is: {Y}Zero{W}")
+else:
+ while num > 0:
+ groupedList.append(num % 1000)
+ num //= 1000
+
+ groupedList.reverse()
+
+ for i in groupedList:
+ if i != 0:
+ if i >= 100:
+ name = name + numDict[int(i/100)] + " Hundred"
+ i = i % 100
+
+ if i >= 20:
+ if name == "":
+ name = name + numDict[i - (i % 10)]
+ else:
+ name = name + " " + numDict[i - (i % 10)]
+
+ i = i % 10
+ elif i >= 10:
+ if name == "":
+ name = name + numDict[i]
+ else:
+ name = name + " " + numDict[i]
+
+ i = i % 10
+
+ if i != 0:
+ if name == "":
+ name = name + numDict[i]
+ else:
+ name = name + " " + numDict[i]
+
+ nameList.append(name)
+ name = ""
+ else:
+ nameList.append("")
+
+ for i in range(len(groupedList)):
+ if nameList[i] != "":
+ name = name + nameList[i] + " " + placeValueDict[len(groupedList) - i] + " "
+
+ name = name.rstrip()
+
+ if len(splittedNum) == 2 and splittedNum[1] != "":
+ name = name + f" {B}Point{Y}"
+
+ for i in splittedNum[1]:
+ name = name + " " + digits[i]
+
+ print(f"{W}", end="")
+
+ if isNegative == False:
+ print(f"\n{placeholder} in words is: {Y}{name}{W}")
+ else:
+ print(f"\n{placeholder} in words is: {Y}Minus {name}{W}")
diff --git a/PDF/requirements.txt b/PDF/requirements.txt
index 63016005d13..6c369a4967e 100644
--- a/PDF/requirements.txt
+++ b/PDF/requirements.txt
@@ -1,2 +1,2 @@
-Pillow==12.0.0
+Pillow==12.1.0
fpdf==1.7.2
\ No newline at end of file
diff --git a/To print series 1,12,123,1234......py b/To print series 1,12,123,1234......py
index cc192eed3eb..d62d34aee3b 100644
--- a/To print series 1,12,123,1234......py
+++ b/To print series 1,12,123,1234......py
@@ -1,47 +1,20 @@
-# master
-def num(a):
- # initialising starting number
+def print_pattern(rows: int) -> None:
+ for i in range(1, rows + 1):
+ print("".join(str(j) for j in range(1, i + 1)))
- num = 1
- # outer loop to handle number of rows
+def start():
+ while True:
+ try:
+ n = int(input("Enter number of rows: "))
+ if n < 1:
+ print("Invalid value, enter a positive integer.")
+ continue
+ break
+ except ValueError:
+ print("Invalid input, please enter a number.")
- for i in range(0, a):
- # re assigning num
+ print_pattern(n)
- num = 1
- # inner loop to handle number of columns
-
- # values changing acc. to outer loop
-
- for k in range(0, i + 1):
- # printing number
-
- print(num, end=" ")
-
- # incrementing number at each column
-
- num = num + 1
-
- # ending line after each row
-
- print("\r")
-
-
-# Driver code
-
-a = 5
-
-num(a)
-# =======
-# 1-12-123-1234 Pattern up to n lines
-
-n = int(input("Enter number of rows: "))
-
-for i in range(1, n + 1):
- for j in range(1, i + 1):
- print(j, end="")
- print()
-
-# master
+start()
diff --git a/async_downloader/requirements.txt b/async_downloader/requirements.txt
index 4a3a6b978bc..bb1949d2e65 100644
--- a/async_downloader/requirements.txt
+++ b/async_downloader/requirements.txt
@@ -1 +1 @@
-aiohttp==3.13.2
+aiohttp==3.13.3
diff --git a/blackjack.py b/blackjack.py
index b2386ff7828..05f25e1f215 100644
--- a/blackjack.py
+++ b/blackjack.py
@@ -4,104 +4,102 @@
deck = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11] * 4
-random.shuffle(deck)
-
-print(
- " ********************************************************** "
-)
-print(
- " Welcome to the game Casino - BLACK JACK ! "
-)
-print(
- " ********************************************************** "
-)
-
-d_cards = [] # Initialising dealer's cards
-p_cards = [] # Initialising player's cards
-
-while len(d_cards) != 2:
- random.shuffle(deck)
- d_cards.append(deck.pop())
- if len(d_cards) == 2:
- print("The cards dealer has are X ", d_cards[1])
-
-# Displaying the Player's cards
-while len(p_cards) != 2:
- random.shuffle(deck)
- p_cards.append(deck.pop())
- if len(p_cards) == 2:
- print("The total of player is ", sum(p_cards))
- print("The cards Player has are ", p_cards)
-
-if sum(p_cards) > 21:
- print("You are BUSTED !\n **************Dealer Wins !!******************\n")
- exit()
-if sum(d_cards) > 21:
+def welcome():
+ print(
+ " ********************************************************** "
+ )
+ print(
+ " Welcome to the game Casino - BLACK JACK ! "
+ )
print(
- "Dealer is BUSTED !\n ************** You are the Winner !!******************\n"
+ " ********************************************************** "
)
- exit()
-if sum(d_cards) == 21:
- print("***********************Dealer is the Winner !!******************")
- exit()
-if sum(d_cards) == 21 and sum(p_cards) == 21:
- print("*****************The match is tie !!*************************")
- exit()
+def start_game():
+ random.shuffle(deck)
+ d_cards = []
+ p_cards = []
-def dealer_choice():
- if sum(d_cards) < 17:
- while sum(d_cards) < 17:
- random.shuffle(deck)
- d_cards.append(deck.pop())
+ # Dealer initial cards
+ while len(d_cards) != 2:
+ random.shuffle(deck)
+ d_cards.append(deck.pop())
+ if len(d_cards) == 2:
+ print("The cards dealer has are X ", d_cards[1])
+
+ # Player initial cards
+ while len(p_cards) != 2:
+ random.shuffle(deck)
+ p_cards.append(deck.pop())
+ if len(p_cards) == 2:
+ print("The total of player is ", sum(p_cards))
+ print("The cards Player has are ", p_cards)
- print("Dealer has total " + str(sum(d_cards)) + "with the cards ", d_cards)
+ if sum(p_cards) > 21:
+ print("You are BUSTED !\n **************Dealer Wins !!******************\n")
+ return
- if sum(p_cards) == sum(d_cards):
- print("***************The match is tie !!****************")
- exit()
+ if sum(d_cards) > 21:
+ print(
+ "Dealer is BUSTED !\n ************** You are the Winner !!******************\n"
+ )
+ return
+
+ if sum(d_cards) == 21 and sum(p_cards) == 21:
+ print("*****************The match is tie !!*************************")
+ return
if sum(d_cards) == 21:
- if sum(p_cards) < 21:
- print("***********************Dealer is the Winner !!******************")
- elif sum(p_cards) == 21:
- print("********************There is tie !!**************************")
- else:
- print("***********************Dealer is the Winner !!******************")
+ print("***********************Dealer is the Winner !!******************")
+ return
- elif sum(d_cards) < 21:
- if sum(p_cards) < 21 and sum(p_cards) < sum(d_cards):
- print("***********************Dealer is the Winner !!******************")
- if sum(p_cards) == 21:
- print("**********************Player is winner !!**********************")
- if sum(p_cards) < 21 and sum(p_cards) > sum(d_cards):
- print("**********************Player is winner !!**********************")
+ def dealer_choice():
+ if sum(d_cards) < 17:
+ while sum(d_cards) < 17:
+ random.shuffle(deck)
+ d_cards.append(deck.pop())
+
+ print("Dealer has total " + str(sum(d_cards)) + " with the cards ", d_cards)
- else:
- if sum(p_cards) < 21:
+ if sum(p_cards) == sum(d_cards):
+ print("***************The match is tie !!****************")
+ return
+
+ if sum(d_cards) > 21:
print("**********************Player is winner !!**********************")
- elif sum(p_cards) == 21:
+ return
+
+ if sum(d_cards) > sum(p_cards):
+ print("***********************Dealer is the Winner !!******************")
+ else:
print("**********************Player is winner !!**********************")
+
+ # Player turn
+ while sum(p_cards) < 21:
+ k = input("Want to hit or stay?\n Press 1 for hit and 0 for stay ")
+
+ if k == "1":
+ random.shuffle(deck)
+ p_cards.append(deck.pop())
+ print("You have a total of " + str(sum(p_cards)) + " with the cards ", p_cards)
+
+ if sum(p_cards) > 21:
+ print("*************You are BUSTED !*************\n Dealer Wins !!")
+ return
+
+ if sum(p_cards) == 21:
+ print(
+ "*******************You are the Winner !!*****************************"
+ )
+ return
else:
- print("***********************Dealer is the Winner !!******************")
+ dealer_choice()
+ break
-while sum(p_cards) < 21:
- k = input("Want to hit or stay?\n Press 1 for hit and 0 for stay ")
- if k == 1:
- random.shuffle(deck)
- p_cards.append(deck.pop())
- print("You have a total of " + str(sum(p_cards)) + " with the cards ", p_cards)
- if sum(p_cards) > 21:
- print("*************You are BUSTED !*************\n Dealer Wins !!")
- if sum(p_cards) == 21:
- print(
- "*******************You are the Winner !!*****************************"
- )
-
- else:
- dealer_choice()
- break
+# Run Game
+welcome()
+start_game()
diff --git a/calci.py b/calci.py
index 21d9ace5233..e988d10638e 100644
--- a/calci.py
+++ b/calci.py
@@ -1,4 +1,4 @@
-a = int(input("enter first value"))
-b = int(input("enter second value"))
-add = a + b
+First = int(input("enter first value"))
+Second = int(input("enter second value"))
+add = First + Second
print(add)
diff --git a/dice.py b/dice.py
index a2e5c12f99b..7f05f277683 100644
--- a/dice.py
+++ b/dice.py
@@ -1,45 +1,39 @@
-# Script Name : dice.py
-# Author : Craig Richards
-# Created : 05th February 2017
-# Last Modified :
-# Version : 1.0
-
-# Modifications :
-
-# Description : This will randomly select two numbers,
-# like throwing dice, you can change the sides of the dice if you wish
-
import random
-
-class Die(object):
- # A dice has a feature of number about how many sides it has when it's
- # established,like 6.
- def __init__(self):
- self.sides = 6
-
- """because a dice contains at least 4 planes.
- So use this method to give it a judgement when you need
- to change the instance attributes.
+class Die:
+ """
+ A class used to represent a multi-sided die.
+
+ Attributes:
+ sides (int): The number of sides on the die (default is 6).
"""
- def set_sides(self, sides_change):
- if sides_change >= 4:
- if sides_change != 6:
- print("change sides from 6 to ", sides_change, " !")
+ def __init__(self, sides=6):
+ """Initializes the die. Defaults to 6 sides if no value is provided."""
+ self.sides = 6 # Internal default
+ self.set_sides(sides)
+
+ def set_sides(self, num_sides):
+ """
+ Validates and sets the number of sides.
+ A physical die must have at least 4 sides.
+ """
+ if isinstance(num_sides, int) and num_sides >= 4:
+ if num_sides != self.sides:
+ print(f"Changing sides from {self.sides} to {num_sides}!")
else:
- # added else clause for printing a message that sides set to 6
- print("sides set to 6")
- self.sides = sides_change
+ print(f"Sides already set to {num_sides}.")
+ self.sides = num_sides
else:
- print("wrong sides! sides set to 6")
+ print(f"Invalid input: {num_sides}. Keeping current value: {self.sides}")
def roll(self):
+ """Returns a random integer between 1 and the number of sides."""
return random.randint(1, self.sides)
-
-d = Die()
-d1 = Die()
-d.set_sides(4)
-d1.set_sides(4)
-print(d.roll(), d1.roll())
+# --- Example Usage ---
+if __name__ == "__main__":
+ d1 = Die(4) # Initialize directly with 4 sides
+ d2 = Die(12) # A Dungeons & Dragons classic
+
+ print(f"Roll Result: D{d1.sides} -> {d1.roll()}, D{d2.sides} -> {d2.roll()}")
diff --git a/image_compressor.py b/image_compressor.py
new file mode 100644
index 00000000000..94d584136f6
--- /dev/null
+++ b/image_compressor.py
@@ -0,0 +1,51 @@
+import os
+import sys
+from PIL import Image
+
+def compress_image(image_path, quality=60):
+ """
+ Compresses an image by reducing its quality.
+
+ Args:
+ image_path (str): Path to the image file.
+ quality (int): Quality of the output image (1-100). Default is 60.
+ """
+ try:
+ # Open the image
+ with Image.open(image_path) as img:
+ # Check if file is an image
+ if img.format not in ["JPEG", "PNG", "JPG"]:
+ print(f"Skipping {image_path}: Not a standard image format.")
+ return
+
+ # Create output filename
+ filename, ext = os.path.splitext(image_path)
+ output_path = f"{filename}_compressed{ext}"
+
+ # Save with reduced quality
+ # Optimize=True ensures the encoder does extra work to minimize size
+ img.save(output_path, quality=quality, optimize=True)
+
+ # Calculate savings
+ original_size = os.path.getsize(image_path)
+ new_size = os.path.getsize(output_path)
+ savings = ((original_size - new_size) / original_size) * 100
+
+ print(f"[+] Compressed: {output_path}")
+ print(f" Original: {original_size/1024:.2f} KB")
+ print(f" New: {new_size/1024:.2f} KB")
+ print(f" Saved: {savings:.2f}%")
+
+ except Exception as e:
+ print(f"[-] Error compressing {image_path}: {e}")
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Usage: python image_compressor.py ")
+ print("Example: python image_compressor.py photo.jpg")
+ else:
+ target_file = sys.argv[1]
+ if os.path.exists(target_file):
+ compress_image(target_file)
+ else:
+ print(f"Error: File '{target_file}' not found.")
\ No newline at end of file
diff --git a/password_checker_code.py b/password_checker_code.py
new file mode 100644
index 00000000000..788b928d6b7
--- /dev/null
+++ b/password_checker_code.py
@@ -0,0 +1,34 @@
+import string
+
+def check_password_strength(password):
+ strength = 0
+
+ # Criteria 1: Length (Must be at least 8 characters)
+ if len(password) >= 8:
+ strength += 1
+
+ # Criteria 2: Must contain Digits (0-9)
+ has_digit = False
+ for char in password:
+ if char.isdigit():
+ has_digit = True
+ break
+ if has_digit:
+ strength += 1
+
+ # Criteria 3: Must contain Uppercase Letters (A-Z)
+ has_upper = False
+ for char in password:
+ if char.isupper():
+ has_upper = True
+ break
+ if has_upper:
+ strength += 1
+
+ return strength
+
+if __name__ == "__main__":
+ print("--- Password Strength Checker ---")
+ # Note: We cannot run input() on the website, but this code is correct.
+ # If users download it, it will work.
+ print("Run this script locally to test your password!")
diff --git a/photo_timestamp_renamer.py b/photo_timestamp_renamer.py
new file mode 100644
index 00000000000..ba5df2ed9f1
--- /dev/null
+++ b/photo_timestamp_renamer.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+"""
+Author: Ivan Costa Neto
+Date: 13-01-26
+
+Auto-rename photos by timestamp, so you can organize those vacation trip photos!!
+
+Name format: YYYY-MM-DD_HH-MM-SS[_NN].ext
+
+Uses EXIF DateTimeOriginal when available (best for JPEG),
+otherwise falls back to file modified time,
+
+i.e.
+ python rename_photos.py ~/Pictures/Trip --dry-run
+ python rename_photos.py ~/Pictures/Trip --recursive
+ python rename_photos.py . --prefix Japan --recursive
+"""
+
+from __future__ import annotations
+import argparse
+from dataclasses import dataclass
+from datetime import datetime
+from pathlib import Path
+import re
+import sys
+
+SUPPORTED_EXTS = {".jpg", ".jpeg", ".png", ".heic", ".webp", ".tif", ".tiff"}
+
+# EXIF support is optional (w\ Pillow)
+try:
+ from PIL import Image, ExifTags # type: ignore
+ PIL_OK = True
+except Exception:
+ PIL_OK = False
+
+
+def is_photo(p: Path) -> bool:
+ return p.is_file() and p.suffix.lower() in SUPPORTED_EXTS
+
+
+def sanitize_prefix(s: str) -> str:
+ s = s.strip()
+ if not s:
+ return ""
+ s = re.sub(r"[^\w\-]+", "_", s)
+ return s[:50]
+
+
+def exif_datetime_original(path: Path) -> datetime | None:
+ """
+ Try to read EXIF DateTimeOriginal/DateTime from image.
+ Returns None if unavailable.
+ """
+ if not PIL_OK:
+ return None
+ try:
+ img = Image.open(path)
+ exif = img.getexif()
+ if not exif:
+ return None
+
+ # map EXIF tag ids -> names
+ tag_map = {}
+ for k, v in ExifTags.TAGS.items():
+ tag_map[k] = v
+
+ # common EXIF datetime tags
+ dto = None
+ dt = None
+ for tag_id, value in exif.items():
+ name = tag_map.get(tag_id)
+ if name == "DateTimeOriginal":
+ dto = value
+ elif name == "DateTime":
+ dt = value
+
+ raw = dto or dt
+ if not raw:
+ return None
+
+ # EXIF datetime format: "YYYY:MM:DD HH:MM:SS"
+ raw = str(raw).strip()
+ return datetime.strptime(raw, "%Y:%m:%d %H:%M:%S")
+ except Exception:
+ return None
+
+
+def file_mtime(path: Path) -> datetime:
+ return datetime.fromtimestamp(path.stat().st_mtime)
+
+
+def unique_name(dest_dir: Path, base: str, ext: str) -> Path:
+ """
+ If base.ext exists, append _01, _02, ...
+ """
+ cand = dest_dir / f"{base}{ext}"
+ if not cand.exists():
+ return cand
+ i = 1
+ while True:
+ cand = dest_dir / f"{base}_{i:02d}{ext}"
+ if not cand.exists():
+ return cand
+ i += 1
+
+
+@dataclass
+class Options:
+ folder: Path
+ recursive: bool
+ dry_run: bool
+ prefix: str
+ keep_original: bool # if true, don't rename if it already matches our format
+
+
+def already_formatted(name: str) -> bool:
+ # matches: YYYY-MM-DD_HH-MM-SS or with prefix and/or _NN
+ pattern = r"^(?:[A-Za-z0-9_]+_)?\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:_\d{2})?$"
+ return re.match(pattern, Path(name).stem) is not None
+
+
+def gather_photos(folder: Path, recursive: bool) -> list[Path]:
+ if recursive:
+ return [p for p in folder.rglob("*") if is_photo(p)]
+ return [p for p in folder.iterdir() if is_photo(p)]
+
+
+def rename_photos(opts: Options) -> int:
+ photos = gather_photos(opts.folder, opts.recursive)
+ photos.sort()
+
+ if not photos:
+ print("No supported photo files found.")
+ return 0
+
+ if opts.prefix:
+ pref = sanitize_prefix(opts.prefix)
+ else:
+ pref = ""
+
+ renamed = 0
+ for p in photos:
+ if opts.keep_original and already_formatted(p.name):
+ continue
+
+ dt = exif_datetime_original(p) or file_mtime(p)
+ base = dt.strftime("%Y-%m-%d_%H-%M-%S")
+ if pref:
+ base = f"{pref}_{base}"
+
+ dest = unique_name(p.parent, base, p.suffix.lower())
+
+ if dest.name == p.name:
+ continue
+
+ if opts.dry_run:
+ print(f"[DRY] {p.relative_to(opts.folder)} -> {dest.name}")
+ else:
+ p.rename(dest)
+ print(f"[OK ] {p.relative_to(opts.folder)} -> {dest.name}")
+ renamed += 1
+
+ if not opts.dry_run:
+ print(f"\nDone. Renamed {renamed} file(s).")
+ return renamed
+
+
+def main(argv: list[str]) -> int:
+ ap = argparse.ArgumentParser(description="Auto-rename photos using EXIF date (or file modified time).")
+ ap.add_argument("folder", help="Folder containing photos")
+ ap.add_argument("--recursive", action="store_true", help="Process subfolders too")
+ ap.add_argument("--dry-run", action="store_true", help="Preview changes without renaming")
+ ap.add_argument("--prefix", default="", help="Optional prefix (e.g., Japan, RWTH, Trip)")
+ ap.add_argument("--keep-original", action="store_true",
+ help="Skip files that already match YYYY-MM-DD_HH-MM-SS naming")
+ args = ap.parse_args(argv)
+
+ folder = Path(args.folder).expanduser()
+ if not folder.exists() or not folder.is_dir():
+ print(f"Not a directory: {folder}", file=sys.stderr)
+ return 2
+
+ if not PIL_OK:
+ print("[Note] Pillow not installed; EXIF dates won't be read (mtime fallback only).")
+ print(" Install for best results: pip install pillow")
+
+ opts = Options(
+ folder=folder,
+ recursive=args.recursive,
+ dry_run=args.dry_run,
+ prefix=args.prefix,
+ keep_original=args.keep_original,
+ )
+ rename_photos(opts)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/requirements_with_versions.txt b/requirements_with_versions.txt
index 814931b3ebe..60d5414e0c8 100644
--- a/requirements_with_versions.txt
+++ b/requirements_with_versions.txt
@@ -1,5 +1,5 @@
pafy==0.5.5
-aiohttp==3.13.2
+aiohttp==3.13.3
fuzzywuzzy==0.18.0
hupper==1.12.1
seaborn==0.13.2
@@ -28,10 +28,10 @@ requests==2.32.5
quo==2023.5.1
PyPDF2==3.0.1
pyserial==3.5
-twilio==9.9.0
+twilio==9.9.1
tabula==1.0.5
nltk==3.9.2
-Pillow==12.0.0
+Pillow==12.1.0
SocksiPy-branch==1.01
xlrd==2.0.2
fpdf==1.7.2
@@ -41,7 +41,7 @@ tornado==6.5.4
obs==0.0.0
todo==0.1
oauth2client==4.1.3
-keras==3.13.0
+keras==3.13.1
pymongo==4.15.5
playsound==1.3.0
pyttsx3==2.99
@@ -49,16 +49,16 @@ auto-mix-prep==0.2.0
lib==4.0.0
pywifi==1.1.12
patterns==0.3
-openai==2.14.0
+openai==2.15.0
background==0.2.1
pydantic==2.12.5
openpyxl==3.1.2
pytesseract==0.3.13
requests-mock==1.12.1
pyglet==2.1.11
-urllib3==2.6.2
+urllib3==2.6.3
thirdai==0.9.33
-google-api-python-client==2.187.0
+google-api-python-client==2.188.0
sound==0.1.0
xlwt==1.3.0
pygame==2.6.1
@@ -81,7 +81,7 @@ Unidecode==1.4.0
Ball==0.2.9
pynput==1.8.1
gTTS==2.5.4
-ccxt==4.5.30
+ccxt==4.5.31
fitz==0.0.1.dev2
fastapi==0.128.0
Django==6.0
diff --git a/scrap_file.py b/scrap_file.py
index 7655e792cbe..aab6e2a2e08 100644
--- a/scrap_file.py
+++ b/scrap_file.py
@@ -6,33 +6,23 @@
import requests
-# Function for download file parameter taking as url
-
+def download(url, filename):
+ try:
+ with requests.get(url, stream=True, timeout=10) as response:
+ response.raise_for_status() # Raises error for 4xx/5xx
-def download(url):
- f = open(
- "file_name.jpg", "wb"
- ) # opening file in write binary('wb') mode with file_name.ext ext=extension
- f.write(requests.get(url).content) # Writing File Content in file_name.jpg
- f.close()
- print("Succesfully Downloaded")
+ with open(filename, "wb") as file:
+ for chunk in response.iter_content(chunk_size=8192):
+ if chunk:
+ file.write(chunk)
+ print(f"Successfully downloaded: {filename}")
-# Function is do same thing as method(download) do,but more strict
-def download_2(url):
- try:
- response = requests.get(url)
- except Exception:
- print("Failed Download!")
- else:
- if response.status_code == 200:
- with open("file_name.jpg", "wb") as f:
- f.write(requests.get(url).content)
- print("Succesfully Downloaded")
- else:
- print("Failed Download!")
+ except requests.exceptions.RequestException as e:
+ print(f"Download failed: {e}")
-url = "https://avatars0.githubusercontent.com/u/29729380?s=400&v=4" # URL from which we want to download
+# Example usage
+url = "https://avatars0.githubusercontent.com/u/29729380?s=400&v=4"
+download(url, "avatar.jpg")
-download(url)
diff --git a/tic-tac-toe.py b/tic-tac-toe.py
new file mode 100644
index 00000000000..30bc1c68ed8
--- /dev/null
+++ b/tic-tac-toe.py
@@ -0,0 +1,63 @@
+# Tic Tac Toe Game in Python
+
+board = [" " for _ in range(9)]
+
+def print_board():
+ print()
+ print(f" {board[0]} | {board[1]} | {board[2]} ")
+ print("---|---|---")
+ print(f" {board[3]} | {board[4]} | {board[5]} ")
+ print("---|---|---")
+ print(f" {board[6]} | {board[7]} | {board[8]} ")
+ print()
+
+def check_winner(player):
+ win_conditions = [
+ [0,1,2], [3,4,5], [6,7,8], # rows
+ [0,3,6], [1,4,7], [2,5,8], # columns
+ [0,4,8], [2,4,6] # diagonals
+ ]
+ for condition in win_conditions:
+ if all(board[i] == player for i in condition):
+ return True
+ return False
+
+def is_draw():
+ return " " not in board
+
+current_player = "X"
+
+print("Welcome to Tic Tac Toe!")
+print("Positions are numbered 1 to 9 as shown below:")
+print("""
+ 1 | 2 | 3
+---|---|---
+ 4 | 5 | 6
+---|---|---
+ 7 | 8 | 9
+""")
+
+while True:
+ print_board()
+ try:
+ move = int(input(f"Player {current_player}, choose position (1-9): ")) - 1
+ if board[move] != " ":
+ print("That position is already taken. Try again.")
+ continue
+ except (ValueError, IndexError):
+ print("Invalid input. Enter a number between 1 and 9.")
+ continue
+
+ board[move] = current_player
+
+ if check_winner(current_player):
+ print_board()
+ print(f"🎉 Player {current_player} wins!")
+ break
+
+ if is_draw():
+ print_board()
+ print("🤝 It's a draw!")
+ break
+
+ current_player = "O" if current_player == "X" else "X"