# ============================================================================
# VISUALIZATION 2: Loss Function Surface (3D)
# ============================================================================
print("\n" + "=" * 70)
print("VISUALIZATION 2: The Loss Function Landscape")
print("=" * 70)
print("This 3D surface shows how MSE changes with different w0 and w1 values")
print("The red star shows the optimal parameters!")
print()
# Compute optimal parameters using normal equation
X_design = np.column_stack([np.ones_like(X_simple), X_simple])
w_optimal = np.linalg.inv(X_design.T @ X_design) @ X_design.T @ y_simple
# Create grid of w0 and w1 values
w0_range = np.linspace(-5, 15, 50)
w1_range = np.linspace(0, 5, 50)
W0, W1 = np.meshgrid(w0_range, w1_range)
# Compute MSE for each combination
MSE = np.zeros_like(W0)
for i in range(W0.shape[0]):
for j in range(W0.shape[1]):
y_pred = W1[i, j] * X_simple + W0[i, j]
MSE[i, j] = np.mean((y_simple - y_pred)**2)
# Create 3D plot
fig = plt.figure(figsize=(14, 6))
# 3D surface plot
ax1 = fig.add_subplot(121, projection='3d')
surf = ax1.plot_surface(W0, W1, MSE, cmap='viridis', alpha=0.8, edgecolor='none')
ax1.scatter([w_optimal[0]], [w_optimal[1]],
[np.mean((y_simple - (w_optimal[1] * X_simple + w_optimal[0]))**2)],
color='red', s=200, marker='*', edgecolors='black', linewidth=2,
label='Optimal (w₀*, w₁*)')
ax1.set_xlabel('w₀ (Bias)', fontsize=11, fontweight='bold')
ax1.set_ylabel('w₁ (Weight)', fontsize=11, fontweight='bold')
ax1.set_zlabel('MSE (Loss)', fontsize=11, fontweight='bold')
ax1.set_title('3D Loss Surface', fontsize=13, fontweight='bold')
ax1.legend()
fig.colorbar(surf, ax=ax1, shrink=0.5)
# Contour plot
ax2 = fig.add_subplot(122)
contour = ax2.contour(W0, W1, MSE, levels=20, cmap='viridis')
ax2.clabel(contour, inline=True, fontsize=8)
ax2.scatter(w_optimal[0], w_optimal[1], color='red', s=200, marker='*',
edgecolors='black', linewidth=2, zorder=5, label='Optimal point')
ax2.set_xlabel('w₀ (Bias)', fontsize=12, fontweight='bold')
ax2.set_ylabel('w₁ (Weight)', fontsize=12, fontweight='bold')
ax2.set_title('Contour Plot (Top View)', fontsize=13, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Optimal parameters: w₀* = {w_optimal[0]:.3f}, w₁* = {w_optimal[1]:.3f}")
# ============================================================================
# VISUALIZATION 3: Gradient Descent Animation
# ============================================================================
print("\n" + "=" * 70)
print("VISUALIZATION 3: Gradient Descent in Action")
print("=" * 70)
print("Watch how gradient descent finds the optimal parameters step by step!")
print()
def gradient_descent_path(X, y, alpha=0.001, iterations=50):
"""Run gradient descent and record the path"""
X_design = np.column_stack([np.ones_like(X), X])
n = len(X)
# Start from a random point
w = np.array([0.0, 0.5])
path = [w.copy()]
losses = []
for _ in range(iterations):
y_pred = X_design @ w
gradient = -2/n * X_design.T @ (y - y_pred)
w = w - alpha * gradient
path.append(w.copy())
losses.append(np.mean((y - y_pred)**2))
return np.array(path), losses
# Run gradient descent
path, losses = gradient_descent_path(X_simple, y_simple, alpha=0.005, iterations=50)
# Create visualization
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# Plot 1: Contour with gradient descent path
ax = axes[0]
contour = ax.contour(W0, W1, MSE, levels=20, cmap='viridis', alpha=0.6)
ax.plot(path[:, 0], path[:, 1], 'ro-', markersize=4, linewidth=2,
label='Gradient Descent Path', alpha=0.7)
ax.scatter(path[0, 0], path[0, 1], color='green', s=200, marker='o',
edgecolors='black', linewidth=2, zorder=5, label='Start')
ax.scatter(path[-1, 0], path[-1, 1], color='red', s=200, marker='*',
edgecolors='black', linewidth=2, zorder=5, label='End')
ax.set_xlabel('w₀ (Bias)', fontsize=11, fontweight='bold')
ax.set_ylabel('w₁ (Weight)', fontsize=11, fontweight='bold')
ax.set_title('Gradient Descent Path', fontsize=12, fontweight='bold')
ax.legend()
ax.grid(True, alpha=0.3)
# Plot 2: Loss over iterations
ax = axes[1]
ax.plot(losses, 'o-', color='#F77F00', linewidth=2, markersize=5)
ax.set_xlabel('Iteration', fontsize=11, fontweight='bold')
ax.set_ylabel('MSE (Loss)', fontsize=11, fontweight='bold')
ax.set_title('Loss Decreasing Over Time', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3)
# Plot 3: Parameter convergence
ax = axes[2]
ax.plot(path[:, 0], 'o-', label='w₀ (bias)', linewidth=2, markersize=4)
ax.plot(path[:, 1], 's-', label='w₁ (weight)', linewidth=2, markersize=4)
ax.axhline(y=w_optimal[0], color='blue', linestyle='--', alpha=0.5, label='w₀* (optimal)')
ax.axhline(y=w_optimal[1], color='orange', linestyle='--', alpha=0.5, label='w₁* (optimal)')
ax.set_xlabel('Iteration', fontsize=11, fontweight='bold')
ax.set_ylabel('Parameter Value', fontsize=11, fontweight='bold')
ax.set_title('Parameter Convergence', fontsize=12, fontweight='bold')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()