{"spec_id":"line-loss-training","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-loss-training: Training Loss Curve\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens ------------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# Imprint palette — canonical order (position 1 always brand green)\nIMPRINT_PALETTE <- c(\n  \"#009E73\", # 1 — training loss\n  \"#C475FD\"  # 2 — validation loss\n)\n\n# --- Data --------------------------------------------------------------------\n# Simulated training history for an image classifier trained for 80 epochs.\n# Training loss decays smoothly; validation loss decays then climbs again\n# past epoch ~34, the classic overfitting signature.\nn_epochs <- 80\nepoch <- 1:n_epochs\n\ntrain_loss <- 2.4 * exp(-epoch * 0.065) + 0.05 + rnorm(n_epochs, 0, 0.02)\ntrain_loss <- pmax(train_loss, 0.02)\n\noverfit_penalty <- 0.0009 * pmax(0, epoch - 34)^1.6\nval_loss <- 2.5 * exp(-epoch * 0.058) + 0.09 + overfit_penalty + rnorm(n_epochs, 0, 0.035)\nval_loss <- pmax(val_loss, 0.05)\n\nhistory_wide <- tibble::tibble(epoch, train_loss, val_loss)\nhistory <- history_wide |>\n  pivot_longer(cols = c(train_loss, val_loss), names_to = \"split\", values_to = \"loss\") |>\n  mutate(split = factor(split,\n    levels = c(\"train_loss\", \"val_loss\"),\n    labels = c(\"Training\", \"Validation\")\n  ))\n\nbest_epoch <- epoch[which.min(val_loss)]\nbest_val_loss <- min(val_loss)\n\n# Generalization-gap ribbon: shade the area between the curves once\n# validation loss has diverged past the optimal-stopping epoch.\ngap_region <- history_wide |> filter(epoch >= best_epoch)\n\n# --- Plot ----------------------------------------------------------------\np <- ggplot(history, aes(x = epoch, y = loss, color = split, linewidth = split)) +\n  geom_ribbon(\n    data = gap_region,\n    aes(x = epoch, ymin = train_loss, ymax = val_loss),\n    inherit.aes = FALSE,\n    fill = IMPRINT_PALETTE[2], alpha = 0.10\n  ) +\n  geom_vline(xintercept = best_epoch, linetype = \"dashed\", linewidth = 0.6, color = INK_SOFT) +\n  geom_line() +\n  annotate(\"point\",\n    x = best_epoch, y = best_val_loss,\n    color = IMPRINT_PALETTE[2], size = 3.2, shape = 21, fill = PAGE_BG, stroke = 1.2\n  ) +\n  annotate(\"text\",\n    x = best_epoch, y = max(train_loss, val_loss) * 0.98,\n    label = sprintf(\"Optimal stopping · epoch %d\", best_epoch),\n    color = INK_SOFT, size = 3.2, hjust = -0.05, vjust = 1\n  ) +\n  scale_color_manual(values = IMPRINT_PALETTE) +\n  scale_linewidth_manual(values = c(Training = 0.95, Validation = 1.3)) +\n  scale_x_continuous(expand = expansion(mult = c(0.01, 0.03))) +\n  labs(\n    title = \"line-loss-training · r · ggplot2 · anyplot.ai\",\n    x = \"Epoch\",\n    y = \"Cross-Entropy Loss\",\n    color = NULL,\n    linewidth = NULL\n  ) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major  = element_line(color = INK, linewidth = 0.25),\n    panel.grid.minor  = element_blank(),\n    panel.grid.major.x = element_blank(),\n    axis.title        = element_text(color = INK, size = 10),\n    axis.text         = element_text(color = INK_SOFT, size = 8),\n    axis.line         = element_line(color = INK_SOFT),\n    plot.title        = element_text(color = INK, size = 12),\n    legend.position    = \"top\",\n    legend.justification = \"left\",\n    legend.margin      = margin(t = 0, b = -4, l = 0, r = 0),\n    legend.background  = element_blank(),\n    legend.key         = element_blank(),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 10)\n  )\n\n# --- Save --------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}