import math import random class Dropout: def __init__(self, p=0.5): self.p = p self.training = True self.mask = None def forward(self, x): if not self.training: return list(x) self.mask = [] output = [] for val in x: if random.random() < self.p: self.mask.append(0) output.append(0.0) else: self.mask.append(1) output.append(val / (1 - self.p)) return output def backward(self, grad_output): grads = [] for g, m in zip(grad_output, self.mask): if m == 0: grads.append(0.0) else: grads.append(g / (1 - self.p)) return grads def l2_regularization(weights, lambda_reg): penalty = 0.0 for w in weights: penalty += w * w return lambda_reg * 0.5 * penalty def l2_gradient(weights, lambda_reg): return [lambda_reg * w for w in weights] class BatchNorm: def __init__(self, num_features, momentum=0.1, eps=1e-5): self.gamma = [1.0] * num_features self.beta = [0.0] * num_features self.eps = eps self.momentum = momentum self.running_mean = [0.0] * num_features self.running_var = [1.0] * num_features self.training = True self.num_features = num_features def forward(self, batch): batch_size = len(batch) if self.training: mean = [0.0] * self.num_features for sample in batch: for j in range(self.num_features): mean[j] += sample[j] mean = [m / batch_size for m in mean] var = [0.0] * self.num_features for sample in batch: for j in range(self.num_features): var[j] += (sample[j] - mean[j]) ** 2 var = [v / batch_size for v in var] for j in range(self.num_features): self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j] self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j] else: mean = list(self.running_mean) var = list(self.running_var) self.x_hat = [] output = [] for sample in batch: normalized = [] out_sample = [] for j in range(self.num_features): x_h = (sample[j] - mean[j]) / math.sqrt(var[j] + self.eps) normalized.append(x_h) out_sample.append(self.gamma[j] * x_h + self.beta[j]) self.x_hat.append(normalized) output.append(out_sample) return output class LayerNorm: def __init__(self, num_features, eps=1e-5): self.gamma = [1.0] * num_features self.beta = [0.0] * num_features self.eps = eps self.num_features = num_features def forward(self, x): mean = sum(x) / len(x) var = sum((xi - mean) ** 2 for xi in x) / len(x) self.x_hat = [] output = [] for j in range(self.num_features): x_h = (x[j] - mean) / math.sqrt(var + self.eps) self.x_hat.append(x_h) output.append(self.gamma[j] * x_h + self.beta[j]) return output class RMSNorm: def __init__(self, num_features, eps=1e-6): self.gamma = [1.0] * num_features self.eps = eps self.num_features = num_features def forward(self, x): rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps) output = [] for j in range(self.num_features): output.append(self.gamma[j] * x[j] / rms) return output def sigmoid(x): x = max(-500, min(500, x)) return 1.0 / (1.0 + math.exp(-x)) def make_circle_data(n=200, seed=42): random.seed(seed) data = [] for _ in range(n): x = random.uniform(-2, 2) y = random.uniform(-2, 2) label = 1.0 if x * x + y * y < 1.5 else 0.0 data.append(([x, y], label)) return data class RegularizedNetwork: def __init__(self, hidden_size=16, lr=0.05, dropout_p=0.0, weight_decay=0.0): random.seed(0) self.hidden_size = hidden_size self.lr = lr self.dropout_p = dropout_p self.weight_decay = weight_decay self.dropout = Dropout(p=dropout_p) if dropout_p > 0 else None self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)] self.b1 = [0.0] * hidden_size self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)] self.b2 = 0.0 def forward(self, x, training=True): self.x = x self.z1 = [] self.h = [] for i in range(self.hidden_size): z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i] self.z1.append(z) self.h.append(max(0.0, z)) if self.dropout and training: self.dropout.training = True self.h = self.dropout.forward(self.h) elif self.dropout: self.dropout.training = False self.h = self.dropout.forward(self.h) self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2 self.out = sigmoid(self.z2) return self.out def backward(self, target): eps = 1e-15 p = max(eps, min(1 - eps, self.out)) d_loss = -(target / p) + (1 - target) / (1 - p) d_sigmoid = self.out * (1 - self.out) d_out = d_loss * d_sigmoid d_h_dropout = [d_out * self.w2[i] for i in range(self.hidden_size)] if self.dropout and self.dropout.mask is not None: d_h_dropout = [g * m / (1 - self.dropout.p) if m else 0.0 for g, m in zip(d_h_dropout, self.dropout.mask)] for i in range(self.hidden_size): d_relu = 1.0 if self.z1[i] > 0 else 0.0 d_h = d_h_dropout[i] * d_relu self.w2[i] -= self.lr * (d_out * self.h[i] + self.weight_decay * self.w2[i]) for j in range(2): self.w1[i][j] -= self.lr * (d_h * self.x[j] + self.weight_decay * self.w1[i][j]) self.b1[i] -= self.lr * d_h self.b2 -= self.lr * d_out def evaluate(self, data): correct = 0 total_loss = 0.0 for x, y in data: pred = self.forward(x, training=False) eps = 1e-15 p = max(eps, min(1 - eps, pred)) total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p)) if (pred >= 0.5) == (y >= 0.5): correct += 1 return total_loss / len(data), correct / len(data) * 100 def train_model(self, train_data, test_data, epochs=300): history = [] for epoch in range(epochs): total_loss = 0.0 correct = 0 for x, y in train_data: pred = self.forward(x, training=True) self.backward(y) eps = 1e-15 p = max(eps, min(1 - eps, pred)) total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p)) if (pred >= 0.5) == (y >= 0.5): correct += 1 train_loss = total_loss / len(train_data) train_acc = correct / len(train_data) * 100 test_loss, test_acc = self.evaluate(test_data) history.append((train_loss, train_acc, test_loss, test_acc)) if epoch % 75 == 0 or epoch == epochs - 1: gap = train_acc - test_acc print(f" Epoch {epoch:3d}: train_acc={train_acc:.1f}%, test_acc={test_acc:.1f}%, gap={gap:.1f}%") return history if __name__ == "__main__": print("=" * 60) print("STEP 1: Dropout Demonstration") print("=" * 60) drop = Dropout(p=0.5) test_input = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] random.seed(42) drop.training = True print(f" Input: {test_input}") for trial in range(3): output = drop.forward(test_input) active = sum(1 for v in output if v > 0) print(f" Train pass {trial+1}: {[f'{v:.1f}' for v in output]} ({active}/{len(test_input)} active)") drop.training = False output = drop.forward(test_input) print(f" Eval pass: {[f'{v:.1f}' for v in output]}") print(f" Train mean: ~{sum(test_input)/len(test_input):.1f} (scaled by 1/(1-p))") print(f" Eval mean: {sum(output)/len(output):.1f} (no scaling needed)") print("\n" + "=" * 60) print("STEP 2: L2 Regularization") print("=" * 60) weights = [0.5, -1.2, 3.0, 0.1, -2.5] lambda_val = 0.01 penalty = l2_regularization(weights, lambda_val) grads = l2_gradient(weights, lambda_val) print(f" Weights: {weights}") print(f" Lambda: {lambda_val}") print(f" L2 penalty: {penalty:.6f}") print(f" L2 grads: {[f'{g:.4f}' for g in grads]}") print(f" Largest weight (3.0) gets largest gradient ({grads[2]:.4f})") print("\n" + "=" * 60) print("STEP 3: BatchNorm vs LayerNorm vs RMSNorm") print("=" * 60) random.seed(42) batch = [[random.gauss(5, 2) for _ in range(4)] for _ in range(8)] sample = batch[0] bn = BatchNorm(4) bn_out = bn.forward(batch) ln = LayerNorm(4) ln_out = ln.forward(sample) rn = RMSNorm(4) rn_out = rn.forward(sample) print(f" Raw sample: {[f'{v:.2f}' for v in sample]}") print(f" BatchNorm: {[f'{v:.2f}' for v in bn_out[0]]}") print(f" LayerNorm: {[f'{v:.2f}' for v in ln_out]}") print(f" RMSNorm: {[f'{v:.2f}' for v in rn_out]}") ln_mean = sum(ln_out) / len(ln_out) ln_std = math.sqrt(sum((v - ln_mean) ** 2 for v in ln_out) / len(ln_out)) rn_mean = sum(rn_out) / len(rn_out) rn_rms = math.sqrt(sum(v * v for v in rn_out) / len(rn_out)) print(f"\n LayerNorm output: mean={ln_mean:.4f}, std={ln_std:.4f}") print(f" RMSNorm output: mean={rn_mean:.4f}, rms={rn_rms:.4f}") print(f" LayerNorm centers to mean=0. RMSNorm normalizes scale only.") print("\n" + "=" * 60) print("STEP 4: BatchNorm Training vs Eval Mode") print("=" * 60) bn2 = BatchNorm(4) bn2.training = True for step in range(10): batch = [[random.gauss(3 + step * 0.1, 1) for _ in range(4)] for _ in range(16)] bn2.forward(batch) print(f" Running mean after 10 batches: {[f'{v:.3f}' for v in bn2.running_mean]}") print(f" Running var after 10 batches: {[f'{v:.3f}' for v in bn2.running_var]}") bn2.training = False test_sample = [[5.0, 5.0, 5.0, 5.0]] eval_out = bn2.forward(test_sample) print(f" Eval mode uses running stats, not batch stats") print(f" Input [5,5,5,5] -> {[f'{v:.3f}' for v in eval_out[0]]}") print("\n" + "=" * 60) print("STEP 5: Training With vs Without Regularization") print("=" * 60) all_data = make_circle_data(n=300, seed=42) train_data = all_data[:150] test_data = all_data[150:] configs = [ ("No regularization", 0.0, 0.0), ("Dropout p=0.3", 0.3, 0.0), ("Weight decay 0.01", 0.0, 0.01), ("Dropout + weight decay", 0.3, 0.01), ] results = {} for name, drop_p, wd in configs: print(f"\n--- {name} ---") net = RegularizedNetwork(hidden_size=16, lr=0.05, dropout_p=drop_p, weight_decay=wd) history = net.train_model(train_data, test_data, epochs=300) results[name] = history print("\n" + "=" * 60) print("FINAL COMPARISON") print("=" * 60) print(f" {'Config':30s} {'Train Acc':>10s} {'Test Acc':>10s} {'Gap':>8s}") print(" " + "-" * 60) for name, history in results.items(): train_loss, train_acc, test_loss, test_acc = history[-1] gap = train_acc - test_acc print(f" {name:30s} {train_acc:>9.1f}% {test_acc:>9.1f}% {gap:>7.1f}%") print("\n Key insight: regularization reduces the train-test gap.") print(" The model with dropout + weight decay generalizes best,") print(" even if its training accuracy is lower.")