Deep Learning with PyTorch

figure-gen.py

Código Python, 9 min de lectura.pytorch/01-deep-learning-fundamentals/img/figure-gen.py

"""Generate all static figures for the 01-deep-learning-fundamentals notebooks.

Run from the img/ directory:
    python figure_gen.py

All figures are saved as PNG with 150 DPI (good balance between quality and file size).
"""

from pathlib import Path

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np

OUTPUT_DIR = Path(__file__).parent
DPI = 150
STYLE = {"facecolor": "#FAFAFA"}


# ---------------------------------------------------------------------------
# 1. Framework trends (for 02_pytorch_basics)
# ---------------------------------------------------------------------------

def gen_framework_trends() -> None:
    years = np.array([2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026])
    pytorch_share = np.array([35, 55, 70, 78, 85, 88, 89, 87, 85])
    tensorflow_share = np.array([55, 38, 22, 14, 9, 6, 5, 4, 4])
    jax_share = np.array([0, 0, 1, 2, 3, 4, 5, 7, 9])
    other_share = np.array([10, 7, 7, 6, 3, 2, 1, 2, 2])

    fig, ax = plt.subplots(figsize=(10, 6), facecolor=STYLE["facecolor"])
    ax.set_facecolor(STYLE["facecolor"])

    ax.plot(years, pytorch_share, color="#EE4C2C", linewidth=3, marker="o", label="PyTorch")
    ax.plot(years, tensorflow_share, color="#FF9900", linewidth=3, marker="s", label="TensorFlow")
    ax.plot(years, jax_share, color="#4285F4", linewidth=3, marker="^", label="JAX")
    ax.plot(years, other_share, color="#A0A0A0", linewidth=2, linestyle="--", label="Other (MXNet, Caffe, …)")

    ax.set_title(
        "Share of ML Research Paper Implementations by Framework\n(Historical Trend to Feb 2026)",
        fontsize=16, fontweight="bold", pad=20,
    )
    ax.set_xlabel("Year", fontsize=12, fontweight="bold")
    ax.set_ylabel("Share of Implementations (%)", fontsize=12, fontweight="bold")
    ax.set_ylim(0, 100)
    ax.set_xlim(2018, 2026)
    ax.set_xticks(years)
    ax.set_yticks(np.arange(0, 101, 20))
    ax.set_yticklabels([f"{i}%" for i in range(0, 101, 20)])
    ax.grid(axis="y", linestyle="--", alpha=0.6)
    ax.grid(axis="x", linestyle="--", alpha=0.3)
    ax.legend(loc="center left", bbox_to_anchor=(1, 0.5), fontsize=11, frameon=False)

    ax.axvline(x=2022.5, color="gray", linestyle=":", alpha=0.5)
    ax.text(
        2022.6, 60, "Generative AI\n& LLM Boom", fontsize=10, color="gray",
        bbox={"facecolor": STYLE["facecolor"], "edgecolor": "none", "alpha": 0.8},
    )

    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "framework_trends_2026.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 2. Perceptron diagram (for 01_intro)
# ---------------------------------------------------------------------------

def gen_perceptron_diagram() -> None:
    fig, ax = plt.subplots(figsize=(10, 5), facecolor=STYLE["facecolor"])
    ax.set_facecolor(STYLE["facecolor"])
    ax.set_xlim(-0.5, 7.5)
    ax.set_ylim(-1.5, 4.5)
    ax.set_aspect("equal")
    ax.axis("off")

    # Input nodes
    input_labels = ["$x_1$", "$x_2$", "$x_3$"]
    input_y = [3.5, 2.0, 0.5]
    input_x = 0.5

    for i, (label, y) in enumerate(zip(input_labels, input_y)):
        circle = plt.Circle((input_x, y), 0.35, color="#4A90D9", ec="black", lw=1.5, zorder=3)
        ax.add_patch(circle)
        ax.text(input_x, y, label, ha="center", va="center", fontsize=14, fontweight="bold", color="white", zorder=4)

    # Summation node
    sum_x, sum_y = 3.5, 2.0
    circle = plt.Circle((sum_x, sum_y), 0.45, color="#F5A623", ec="black", lw=1.5, zorder=3)
    ax.add_patch(circle)
    ax.text(sum_x, sum_y, "$\\Sigma$", ha="center", va="center", fontsize=18, fontweight="bold", zorder=4)

    # Activation node
    act_x, act_y = 5.5, 2.0
    circle = plt.Circle((act_x, act_y), 0.45, color="#7ED321", ec="black", lw=1.5, zorder=3)
    ax.add_patch(circle)
    ax.text(act_x, act_y, "$f$", ha="center", va="center", fontsize=18, fontweight="bold", zorder=4)

    # Output node
    out_x, out_y = 7.0, 2.0
    circle = plt.Circle((out_x, out_y), 0.35, color="#D0021B", ec="black", lw=1.5, zorder=3)
    ax.add_patch(circle)
    ax.text(out_x, out_y, "$\\hat{y}$", ha="center", va="center", fontsize=14, fontweight="bold", color="white", zorder=4)

    # Connections: inputs → summation
    weight_labels = ["$w_1$", "$w_2$", "$w_3$"]
    for i, (y, wlabel) in enumerate(zip(input_y, weight_labels)):
        ax.annotate(
            "", xy=(sum_x - 0.45, sum_y + (y - sum_y) * 0.3), xytext=(input_x + 0.35, y),
            arrowprops={"arrowstyle": "->", "color": "#555", "lw": 1.5},
        )
        mid_x = (input_x + 0.35 + sum_x - 0.45) / 2
        mid_y = (y + sum_y) / 2
        ax.text(mid_x - 0.15, mid_y + 0.2, wlabel, fontsize=11, color="#333", ha="center")

    # Bias arrow
    ax.annotate(
        "", xy=(sum_x, sum_y - 0.45), xytext=(sum_x, -0.8),
        arrowprops={"arrowstyle": "->", "color": "#555", "lw": 1.5},
    )
    ax.text(sum_x + 0.2, -0.8, "$b$ (bias)", fontsize=11, color="#333", va="center")

    # Summation → activation
    ax.annotate(
        "", xy=(act_x - 0.45, act_y), xytext=(sum_x + 0.45, sum_y),
        arrowprops={"arrowstyle": "->", "color": "#555", "lw": 1.5},
    )
    ax.text((sum_x + act_x) / 2, sum_y + 0.35, "$z = \\sum w_i x_i + b$", fontsize=10, ha="center", color="#555")

    # Activation → output
    ax.annotate(
        "", xy=(out_x - 0.35, out_y), xytext=(act_x + 0.45, act_y),
        arrowprops={"arrowstyle": "->", "color": "#555", "lw": 1.5},
    )

    # Title & labels
    ax.text(3.5, 4.3, "Perceptron", fontsize=16, fontweight="bold", ha="center")
    ax.text(input_x, -1.2, "Inputs", fontsize=11, ha="center", color="#4A90D9", fontweight="bold")
    ax.text(sum_x, -1.2, "Weighted\nsum", fontsize=10, ha="center", color="#F5A623", fontweight="bold")
    ax.text(act_x, -1.2, "Activation\nfunction", fontsize=10, ha="center", color="#7ED321", fontweight="bold")
    ax.text(out_x, -1.2, "Output", fontsize=11, ha="center", color="#D0021B", fontweight="bold")

    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "perceptron_diagram.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 3. MLP architecture diagram (for 01_intro)
# ---------------------------------------------------------------------------

def gen_mlp_diagram() -> None:
    fig, ax = plt.subplots(figsize=(10, 6), facecolor=STYLE["facecolor"])
    ax.set_facecolor(STYLE["facecolor"])
    ax.axis("off")

    layer_sizes = [4, 6, 6, 3]
    layer_labels = ["Input\nlayer", "Hidden\nlayer 1", "Hidden\nlayer 2", "Output\nlayer"]
    layer_colors = ["#4A90D9", "#F5A623", "#F5A623", "#D0021B"]
    x_positions = [1, 3, 5, 7]
    max_neurons = max(layer_sizes)

    node_positions: list[list[tuple[float, float]]] = []

    for layer_idx, (n, x) in enumerate(zip(layer_sizes, x_positions)):
        y_start = (max_neurons - n) / 2
        positions = [(x, y_start + i * 1.0) for i in range(n)]
        node_positions.append(positions)

        for px, py in positions:
            circle = plt.Circle((px, py), 0.25, color=layer_colors[layer_idx], ec="black", lw=1, zorder=3)
            ax.add_patch(circle)

        ax.text(x, -1.2, layer_labels[layer_idx], fontsize=10, ha="center", fontweight="bold",
                color=layer_colors[layer_idx])

    # Draw connections between consecutive layers
    for l in range(len(layer_sizes) - 1):
        for x1, y1 in node_positions[l]:
            for x2, y2 in node_positions[l + 1]:
                ax.plot([x1 + 0.25, x2 - 0.25], [y1, y2], color="#CCCCCC", lw=0.5, zorder=1)

    ax.set_xlim(-0.5, 8.5)
    ax.set_ylim(-2, max_neurons + 0.5)
    ax.set_aspect("equal")
    ax.set_title("Multilayer Perceptron (MLP)", fontsize=16, fontweight="bold", pad=20)

    # Feature interpretation annotations
    annotations = [
        (1, max_neurons + 0.2, "Raw\nfeatures", "#4A90D9"),
        (4, max_neurons + 0.2, "Learned intermediate\nrepresentations", "#F5A623"),
        (7, max_neurons + 0.2, "Final\nprediction", "#D0021B"),
    ]
    for ax_x, ay, text, color in annotations:
        ax.text(ax_x, ay, text, fontsize=9, ha="center", color=color, style="italic")

    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "mlp_diagram.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 4. Activation functions comparison (for 01_intro)
# ---------------------------------------------------------------------------

def gen_activation_functions() -> None:
    x = np.linspace(-5, 5, 300)

    activations = [
        ("Sigmoid", 1 / (1 + np.exp(-x)), "(0, 1)"),
        ("Tanh", np.tanh(x), "(-1, 1)"),
        ("ReLU", np.maximum(0, x), "[0, ∞)"),
        ("Leaky ReLU (α=0.1)", np.where(x > 0, x, 0.1 * x), "(-∞, ∞)"),
    ]

    fig, axes = plt.subplots(1, 4, figsize=(16, 3.5), facecolor=STYLE["facecolor"])

    for ax, (name, y, range_str) in zip(axes, activations):
        ax.set_facecolor(STYLE["facecolor"])
        ax.plot(x, y, color="#2196F3", linewidth=2.5)
        ax.axhline(y=0, color="black", linewidth=0.5)
        ax.axvline(x=0, color="black", linewidth=0.5)
        ax.set_title(name, fontsize=12, fontweight="bold")
        ax.set_xlabel("x", fontsize=10)
        ax.grid(True, alpha=0.3)
        ax.set_xlim(-5, 5)
        ax.text(
            0.95, 0.05, f"Range: {range_str}", transform=ax.transAxes,
            fontsize=8, ha="right", va="bottom", color="#666",
            bbox={"facecolor": "white", "alpha": 0.7, "edgecolor": "none"},
        )

    fig.suptitle("Common Activation Functions", fontsize=14, fontweight="bold", y=1.02)
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "activation_functions.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 5. Gradient descent on simple surface (for 01_intro)
# ---------------------------------------------------------------------------

def gen_gradient_descent_surface() -> None:
    fig, axes = plt.subplots(1, 2, figsize=(14, 5), facecolor=STYLE["facecolor"])

    # Left: 2D contour with gradient descent path
    ax = axes[0]
    ax.set_facecolor(STYLE["facecolor"])

    w1 = np.linspace(-3, 3, 200)
    w2 = np.linspace(-3, 3, 200)
    W1, W2 = np.meshgrid(w1, w2)
    # Simple bowl-shaped loss: L(w1, w2) = w1² + 2*w2²
    Z = W1**2 + 2 * W2**2

    ax.contour(W1, W2, Z, levels=15, cmap="viridis", alpha=0.6)
    contour_filled = ax.contourf(W1, W2, Z, levels=15, cmap="viridis", alpha=0.3)

    # Simulate gradient descent path
    lr = 0.15
    path_w1, path_w2 = [2.5], [2.5]
    for _ in range(20):
        grad_w1 = 2 * path_w1[-1]
        grad_w2 = 4 * path_w2[-1]
        path_w1.append(path_w1[-1] - lr * grad_w1)
        path_w2.append(path_w2[-1] - lr * grad_w2)

    ax.plot(path_w1, path_w2, "o-", color="#EE4C2C", markersize=4, linewidth=1.5, label="GD path")
    ax.plot(path_w1[0], path_w2[0], "o", color="#EE4C2C", markersize=10, zorder=5)
    ax.plot(0, 0, "*", color="#FFD700", markersize=15, zorder=5, markeredgecolor="black", label="Minimum")
    ax.annotate("Start", (path_w1[0], path_w2[0]), textcoords="offset points", xytext=(10, 10), fontsize=10)
    ax.set_xlabel("$w_1$", fontsize=12)
    ax.set_ylabel("$w_2$", fontsize=12)
    ax.set_title("Gradient descent: 2D view\n\"Walk downhill\"", fontsize=12, fontweight="bold")
    ax.legend(fontsize=10)

    # Right: 3D surface
    ax3d = fig.add_subplot(122, projection="3d", facecolor=STYLE["facecolor"])
    axes[1].remove()
    W1_coarse = np.linspace(-3, 3, 50)
    W2_coarse = np.linspace(-3, 3, 50)
    W1c, W2c = np.meshgrid(W1_coarse, W2_coarse)
    Zc = W1c**2 + 2 * W2c**2

    ax3d.plot_surface(W1c, W2c, Zc, cmap="viridis", alpha=0.6, edgecolor="none")
    path_z = [w**2 + 2 * v**2 for w, v in zip(path_w1, path_w2)]
    ax3d.plot(path_w1, path_w2, path_z, "o-", color="#EE4C2C", markersize=3, linewidth=1.5, zorder=5)
    ax3d.set_xlabel("$w_1$")
    ax3d.set_ylabel("$w_2$")
    ax3d.set_zlabel("Loss")
    ax3d.set_title("Loss surface", fontsize=12, fontweight="bold")
    ax3d.view_init(elev=35, azim=225)

    fig.suptitle(
        "Gradient Descent: Iteratively Adjust Parameters to Minimize Loss",
        fontsize=14, fontweight="bold", y=1.02,
    )
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "gradient_descent_surface.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 6. XOR / AND / OR scatter (for 01_intro)
# ---------------------------------------------------------------------------

def gen_xor_scatter() -> None:
    fig, axes = plt.subplots(1, 3, figsize=(12, 3.5), facecolor=STYLE["facecolor"])

    problems = [
        ("AND", [0, 0, 0, 1], True),
        ("OR", [0, 1, 1, 1], True),
        ("XOR", [0, 1, 1, 0], False),
    ]
    inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])

    for ax, (name, labels, separable) in zip(axes, problems):
        ax.set_facecolor(STYLE["facecolor"])
        labels_arr = np.array(labels)

        # Plot points
        for cls, color, marker in [(0, "#4A90D9", "o"), (1, "#D0021B", "s")]:
            mask = labels_arr == cls
            ax.scatter(inputs[mask, 0], inputs[mask, 1], c=color, marker=marker,
                       s=120, zorder=3, edgecolors="black", linewidths=1, label=f"Class {cls}")

        # Draw decision boundary if separable
        if separable:
            if name == "AND":
                x_line = np.linspace(-0.5, 1.5, 100)
                y_line = 1.5 - x_line  # x1 + x2 = 1.5
                ax.plot(x_line, y_line, "--", color="#7ED321", linewidth=2, label="Decision boundary")
                ax.fill_between(x_line, y_line, 1.5, alpha=0.1, color="#D0021B")
                ax.fill_between(x_line, -0.5, y_line, alpha=0.1, color="#4A90D9")
            elif name == "OR":
                x_line = np.linspace(-0.5, 1.5, 100)
                y_line = 0.5 - x_line  # x1 + x2 = 0.5
                ax.plot(x_line, y_line, "--", color="#7ED321", linewidth=2, label="Decision boundary")
                ax.fill_between(x_line, y_line, 1.5, alpha=0.1, color="#D0021B")
                ax.fill_between(x_line, -0.5, y_line, alpha=0.1, color="#4A90D9")
        else:
            # XOR: show that no single line works
            ax.text(0.5, -0.35, "No single line can separate these!", fontsize=9, ha="center",
                    color="#999", style="italic")

        status = "✓ Linearly separable" if separable else "✗ NOT linearly separable"
        status_color = "#7ED321" if separable else "#D0021B"
        ax.set_title(f"{name}\n{status}", fontsize=12, fontweight="bold", color=status_color)
        ax.set_xlim(-0.5, 1.5)
        ax.set_ylim(-0.5, 1.5)
        ax.set_xlabel("$x_1$", fontsize=11)
        ax.set_ylabel("$x_2$", fontsize=11)
        ax.grid(True, alpha=0.2)
        ax.legend(fontsize=8, loc="upper left")
        ax.set_xticks([0, 1])
        ax.set_yticks([0, 1])

    fig.suptitle("Logical Gates: Linear Separability", fontsize=14, fontweight="bold", y=1.02)
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "xor_and_or_scatter.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# 7. Feature hierarchy diagram (for 01_intro — MLP section)
# ---------------------------------------------------------------------------

def gen_feature_hierarchy() -> None:
    fig, ax = plt.subplots(figsize=(12, 3), facecolor=STYLE["facecolor"])
    ax.set_facecolor(STYLE["facecolor"])
    ax.axis("off")

    stages = [
        ("Pixels", "#4A90D9", "Raw input\n(784 values)"),
        ("Edges &\nTextures", "#F5A623", "Layer 1\ndetects"),
        ("Shapes &\nParts", "#7ED321", "Layer 2\ncombines"),
        ("Objects", "#D0021B", "Output\nclassifies"),
    ]

    for i, (label, color, desc) in enumerate(stages):
        x = i * 3
        rect = mpatches.FancyBboxPatch(
            (x, 0.5), 2, 1.5, boxstyle="round,pad=0.15",
            facecolor=color, edgecolor="black", linewidth=1.5, alpha=0.8,
        )
        ax.add_patch(rect)
        ax.text(x + 1, 1.25, label, fontsize=12, fontweight="bold", ha="center", va="center", color="white")
        ax.text(x + 1, -0.1, desc, fontsize=9, ha="center", va="top", color="#555")

        if i < len(stages) - 1:
            ax.annotate(
                "", xy=(x + 2.6, 1.25), xytext=(x + 2.1, 1.25),
                arrowprops={"arrowstyle": "->", "color": "#888", "lw": 2},
            )

    ax.set_xlim(-0.5, 11.5)
    ax.set_ylim(-0.8, 2.8)
    ax.set_title(
        "How hidden layers build features (image classification example)",
        fontsize=13, fontweight="bold", pad=10,
    )

    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "feature_hierarchy.png", dpi=DPI, bbox_inches="tight")
    plt.close(fig)


# ---------------------------------------------------------------------------
# Main: generate all figures
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    generators = [
        ("framework_trends_2026.png", gen_framework_trends),
        ("perceptron_diagram.png", gen_perceptron_diagram),
        ("mlp_diagram.png", gen_mlp_diagram),
        ("activation_functions.png", gen_activation_functions),
        ("gradient_descent_surface.png", gen_gradient_descent_surface),
        ("xor_and_or_scatter.png", gen_xor_scatter),
        ("feature_hierarchy.png", gen_feature_hierarchy),
    ]
    for name, func in generators:
        print(f"Generating {name}...")
        func()
        print(f"  ✓ Saved to {OUTPUT_DIR / name}")

    print(f"\nDone! Generated {len(generators)} figures in {OUTPUT_DIR}")

Escribe al menos dos letras. Busca también dentro del código de los notebooks.