Presentado por:
Juan Jose Correa Hurtado <jjcorreahu@unal.edu.co>, Jacobo Ochoa Ramirez <jochoara@unal.edu.co>, Hailer Serna Hernandez <hsernah@unal.edu.co> |
Profesor: Juan David Ospina Arango
Monitor: Andrés Mauricio Zapata Rincón
Universidad Nacional de Colombia Facultad de Minas Ingeniería de Sistemas e Informática
02 de mayo de 2025
library(GA)
library(ggplot2)
library(reshape2)
library(plot3D)
library(animation)
library(DEoptim)
library(pso)
library(dplyr)
library(gifski)
library(gganimate)
library(knitr)
library(magick)
Para evaluar el desempeño de distintos métodos de optimización numérica, se seleccionaron dos funciones de prueba ampliamente utilizadas en la literatura: la función de Rosenbrock y la función de Schwefel. Estas funciones presentan características contrastantes que permiten observar cómo los algoritmos se comportan en contextos diferentes: con mínimos globales difíciles de alcanzar o con múltiples mínimos locales que pueden confundir la convergencia.
La función de Rosenbrock es una función unimodal cuyo mínimo global se encuentra en un valle angosto y curvado de forma parabólica. Aunque este valle es fácil de localizar visualmente, alcanzar el mínimo representa un reto para muchos algoritmos debido a la geometría del terreno de optimización.
“La función es unimodal, y el mínimo global se encuentra en un valle parabólico angosto. Sin embargo, aunque este valle es fácil de encontrar, la convergencia al mínimo es difícil” (Surjanovic & Bingham, s.f.) [traducción propia].
Esto la convierte en un excelente caso para probar algoritmos que deben navegar por terrenos suaves pero estrechos, donde los gradientes pueden apuntar en direcciones poco útiles si no se ajustan bien los parámetros de convergencia.
Para \(d\) dimensiones la función de Rosenbrock está dada por la forma:
\[ f(\mathbf{x}) = \sum_{i=1}^{d-1} \left[100(x_{i+1} - x_i^2)^2 + (x_i-1)^2\right] \]
El siguiente código implementa la función:
funcion_rosenbrock <- function(vector_x){
# Evaluación de la función de Rosenbrock para un vector de entrada vector_x
# Se expresa de esta manera para prevenir el uso de ciclos y
# sacar mayor provecho al paradigma de programación funcional
d <- length(vector_x) # Obtener la dimensión del vector
xi <- vector_x[1:(d-1)] # x_i
xnext <- vector_x[2:d] # x_(i+1)
# f(x) = sum_{i=1}^{d-1} [100 * (x_{i+1} - x_i^2)^2 + (1 - x_i)^2]
result <- (sum(100*(xnext-xi^2)^2+(xi-1)^2))
return(result)
}
En contraste, la función de Schwefel presenta una superficie altamente compleja, con múltiples mínimos locales dispersos en todo el dominio. Esta característica desafía a los métodos de optimización a evitar caer en soluciones subóptimas y a explorar eficientemente el espacio de búsqueda.
“La función de Schwefel es compleja, con muchos mínimos locales” (Surjanovic & Bingham, s.f.) [traducción propia].
Este tipo de función es ideal para evaluar algoritmos en escenarios donde la exploración global es tan importante como la exploración local.
Para \(d\) dimensiones la función de Schwefel está dada por la forma:
\[ f(\mathbf{x}) = 418.9829 \cdot d - \sum_{i=1}^{d} x_i \sin\left(\sqrt{|x_i|}\right) \]
El siguiente código implementa la función:
funcion_schwefel <- function(vector_x){
# Evaluación de la función Schwefel dado un vector de entrada vector_x
d <- length(vector_x) # Obtener la dimensión del vector
# f(x) = 418.9829 * d - sum(x_i * sin(sqrt(|x_i|)))
# Este valor tiene su mínimo global en x_i ≈ 420.9687 para todo i
sum <- sum(vector_x*sin(sqrt(abs(vector_x))))
result <- 418.9829*d - sum
return(result)
}
Para poder optimizar ambas funciones en dos y tres dimensiones se necesita un optimizador que haga uso del gradiente numérico.
Inicialmente se debe obtener la derivada parcial de la función respecto a alguna de sus componentes.
partial_dev <- function(x,i,fun,h=0.01){
e <- x*0 # crea un vector de ceros de la misma longitud de x
e[i] <- h
y <- (fun(x+e)-fun(x-e))/(2*h)
return(y)
}
Se evalua el gradiente de la función al obtener cada una de las derivadas parciales de \(f\) en \(x\)
num_grad <- function(x,fun,h=0.01){
# x: punto del espacio donde se debe evaluar el gradiente
# fun: función para la que se desea calcular el gradiente en x
# h: es el tamaño de ventana para el cálculo de la derivada numérica
d <- length(x)
y <- mapply(FUN=partial_dev,i=1:d,MoreArgs=list(x=x,h=h,fun=fun))
return(y)
}
EL siguiente código hace uso del gradiente númerico para calcular el descenso del gradiente númerico con una tasa de aprendizaje variable:
optimizador_multivariado <- function(fun, x0, eta0=0.1, decay=0.01, max_eval=100, h=0.01, tol = 1e-5) {
x <- matrix(NA, nrow=max_eval, ncol=length(x0)) # Matriz para registrar los puntos iterativos
f_values <- numeric(max_eval) # Vector para registrar los valores de la función objetivo
x[1, ] <- x0 # asignar a la primer columna de la matriz x el vector x0
f_values[1] <- fun(x0) # valor inicial de la función
for (i in 2:max_eval) {
# Calcula el gradiente numérico en la iteración anterior
grad <- num_grad(x[i-1, ], fun, h)
# Tasa de aprendizaje variable (disminuye con el tiempo)
eta <- eta0 / (1 + decay * (i - 1))
# Paso de descenso: actualiza x restando el gradiente
x[i, ] <- x[i-1, ] - eta * grad
# Calcular valor de la función en el nuevo punto
f_values[i] <- fun(x[i, ])
# Magnitud del cambio entre iteraciones
cambio <- sqrt(sum((x[i, ] - x[i-1, ])^2))
# Si el cambio es muy pequeño, se asume convergencia
if (cambio < tol) {
break
}
}
result <- list(
puntos = x[1:i, ], # Puntos recorridos
valores = f_values[1:i], # Valores de la función objetivo
iteraciones = i, # Número total de iteraciones
solucion = x[i, ], # Solución final encontrada
valor_optimo = f_values[i] # Valor óptimo encontrado
)
return(result)
}
EL siguiente código genera un vector de \(n\) dimensiones para ser evaluado en el optimizador multivariado.
vector_aleatorio <- function(n,limInf, limSup, seed = NULL){
if (!is.null(seed)){
set.seed(seed) # semilla fija solo si fue proporcionada
}
return(runif(n, min = limInf, max = limSup)) # Vector aleatorio en el rango proporcionado
}
x0_2d_rosenbrock <- vector_aleatorio(2, -2.048 , 2.048, seed = 28)
res_rosenbrock_2d <- optimizador_multivariado(funcion_rosenbrock,
x0 = x0_2d_rosenbrock,
eta0 = 0.001,
decay = 0.2)
cat("Solución: ",res_rosenbrock_2d$solucion, "\nValor optimo: ", res_rosenbrock_2d$valor_optimo)
## Solución: 0.08757869 -0.01458303
## Valor optimo: 0.8820325
sol <- res_rosenbrock_2d$puntos
n_length <- 100
x1 <- seq(-2, 2, length.out = n_length)
x2 <- seq(-2, 2, length.out = n_length)
X <- expand.grid(x1, x2)
# Evaluar la función en toda la grilla
z <- apply(X, 1, function(row) funcion_rosenbrock(c(row[1], row[2])))
Z <- matrix(z, ncol = n_length, nrow = n_length)
# Crear gráfico de curvas de nivel
contour(x1, x2, Z,
xlab = expression(x[1]), ylab = expression(x[2]),
main = "Ejemplo: Curvas de nivel - Función de Rosenbrock",
sub = "Camino del optimizador multivariado",
las = 1, drawlabels = TRUE)
#leyendas
legend(-1.5, 2, legend = c("Punto de inicio", "Pasos"), fill = c("blue", "red"))
#Pasos
lines(sol[,1], sol[,2], type = "b", pch = 19, col = "red", lwd = 2)
#Punto de incio
points(sol[1,1], sol[1,2], col = "blue", pch = 19, cex = 1.8)
x0_3d_rosenbrock <- vector_aleatorio(3, -2.048 , 2.048, seed = 45)
res_rosenbrock_3d <- optimizador_multivariado(funcion_rosenbrock,
x0 = x0_3d_rosenbrock,
eta0 = 0.001,
decay = 0.2)
cat("Solución: ",res_rosenbrock_3d$solucion, "\nValor optimo: ", res_rosenbrock_3d$valor_optimo)
## Solución: 0.2708469 0.06569966 -0.04255669
## Valor optimo: 1.630156
x0_2d_schwefel <- vector_aleatorio(2, -500 , 500, seed = 69)
res_schwefel_2d <- optimizador_multivariado(funcion_schwefel,
x0 = x0_2d_schwefel,
eta0 = 0.3,
decay = 0.01)
cat("Vector origen: ", x0_2d_schwefel,"\nSolucion: ",res_schwefel_2d$solucion, "\nValor optimo: ", res_schwefel_2d$valor_optimo)
## Vector origen: 30.75401 268.8077
## Solucion: 65.07363 204.3101
## Valor optimo: 572.5487
sol <- res_schwefel_2d$puntos
n_length <- 100
x1 <- seq(-500, 500, length.out = n_length)
x2 <- seq(-500, 500, length.out = n_length)
X <- expand.grid(x1, x2)
# Evaluar la función en toda la grilla
z <- apply(X, 1, function(row) funcion_schwefel(c(row[1], row[2])))
Z <- matrix(z, ncol = n_length, nrow = n_length)
# Crear gráfico de curvas de nivel
contour(x1, x2, Z,
xlab = expression(x[1]), ylab = expression(x[2]),
main = "Curvas de nivel - Funcion de schwefel",
sub = "Camino del optimizador multivariado",
las = 1, drawlabels = TRUE)
#leyendas
legend(-400, -200, legend = c("Punto de inicio", "Pasos"), fill = c("blue", "red"))
#Pasos
lines(sol[,1], sol[,2], type = "b", pch = 19, col = "red", lwd = 2)
#Punto de incio
points(sol[1,1], sol[1,2], col = "blue", pch = 19, cex = 1.8)
sol <- res_schwefel_2d$puntos
n_length <- 100
x1 <- seq(0, 100, length.out = n_length)
x2 <- seq(180, 300, length.out = n_length)
X <- expand.grid(x1, x2)
# Evaluar la función en toda la grilla
z <- apply(X, 1, function(row) funcion_schwefel(c(row[1], row[2])))
Z <- matrix(z, ncol = n_length, nrow = n_length)
# Crear gráfico de curvas de nivel
contour(x1, x2, Z,
xlab = expression(x[1]), ylab = expression(x[2]),
main = "Curvas de nivel con zoom - Funcion de schwefel",
sub = "Camino del optimizador multivariado",
las = 1, drawlabels = TRUE)
#leyendas
legend(80, 290, legend = c("Punto de inicio", "Pasos"), fill = c("blue", "red"))
#Pasos
lines(sol[,1], sol[,2], type = "b", pch = 19, col = "red", lwd = 2)
#Punto de incio
points(sol[1,1], sol[1,2], col = "blue", pch = 19, cex = 1.8)
x0_3d_schwefel <- vector_aleatorio(3, -2.048 , 2.048, seed = 45)
res_schwefel_3d <- optimizador_multivariado(funcion_schwefel,
x0 = x0_3d_schwefel,
eta0 = 0.3,
decay = 0.01)
cat("Solución: ",res_schwefel_3d$solucion, "\nValor optimo: ", res_schwefel_3d$valor_optimo)
## Solución: 5.238147 5.236521 5.236186
## Valor optimo: 1245.113
Ante las limitaciones del algoritmo de optimización por descenso del gradiente se plantea el uso de otros tres algoritmos que pueden tener un mejor desempeño.Estos son:
Los algoritmos evolutivos son técnicas de optimización inspiradas en los principios de la selección natural y genética. Operan sobre una población de soluciones que evoluciona a través de generaciones mediante operadores como selección, cruzamiento y mutación. Cada individuo representa una solución candidata al problema, y su adecuación se evalúa mediante una función objetivo. A lo largo de las iteraciones, las soluciones de mayor aptitud tienden a prevalecer, promoviendo la exploración y explotación del espacio de búsqueda.
Mediante el uso de la librería 'GA'
se va implementar un
algoritmo genético para optimizar ambas funciones objetivo.
Para evaluar el comportamiento del algoritmo post-evaluación se va a definir una función que registra la población de cada generación junto con el mejor individuo de la población.
make_postfit <- function(pop_store_name, best_store_name = NULL) {
# Clear previous population variable
assign(pop_store_name, list(), envir = globalenv())
# Clear best individual variable if provided
if (!is.null(best_store_name)) {
assign(best_store_name, list(), envir = globalenv())
}
function(object, ...) {
pop <- object@population
fitness <- object@fitness
# Append current population
pop_list <- get(pop_store_name, envir = globalenv())
pop_list <- append(pop_list, list(pop))
assign(pop_store_name, pop_list, envir = globalenv())
# If best_store_name is provided, track best individual
if (!is.null(best_store_name)) {
best_index <- which.max(fitness) # For maximization
best_individual <- pop[best_index, , drop = FALSE]
best_list <- get(best_store_name, envir = globalenv())
best_list <- append(best_list, list(best_individual))
assign(best_store_name, best_list, envir = globalenv())
}
return(object)
}
}
Se procede a realizar la optimización en dos dimensiones de la función Rosenbrock.Se va a hacer uso de algoritmos bioinspirados como selección, mutación y cruzamiento, cada uno con sus respectivos parametros, adecuados para obtener mejores resultados.
postfit_rosenbrock_2d <- make_postfit("pop_rosenbrock", "best_rosenbrock")
TYPE = "real-valued"
popSize = 50
GA_rosenbrock <- ga(type = TYPE,
fitness = function(x){-funcion_rosenbrock(x)},
lower = c(-2.048 , -2.048), upper = c(2.048 , 2.048),
maxiter = 1000,
run = 100,
population = gaControl(TYPE)$population,
selection = gaControl(TYPE)$selection,
crossover = gaControl(TYPE)$crossover,
mutation = gaControl(TYPE)$mutation,
popSize = popSize,
pcrossover = 0.88,
pmutation = 0.1,
elitism = base::max(1, round(popSize*0.05)),
seed = 555,
postFitness = postfit_rosenbrock_2d
)
summary(GA_rosenbrock)
## ── Genetic Algorithm ───────────────────
##
## GA settings:
## Type = real-valued
## Population size = 50
## Number of generations = 1000
## Elitism = 2
## Crossover probability = 0.88
## Mutation probability = 0.1
## Search domain =
## x1 x2
## lower -2.048 -2.048
## upper 2.048 2.048
##
## GA results:
## Iterations = 256
## Fitness function value = -0.003070611
## Solution =
## x1 x2
## [1,] 0.9449736 0.8923217
plot(GA_rosenbrock)
x1 <- x2 <- seq(-5.12, 5.12, by = 0.1)
adapt_rosenbrock <- function(x1, x2) {
mapply(function(a, b) funcion_rosenbrock(c(a, b)), x1, x2)
}
f <- outer(x1, x2, adapt_rosenbrock)
iter_to_show = c(1,60,120, 180, 240, 256)
par(mfrow = c(3,2), mar = c(2,2,2,1),
mgp = c(1, 0.4, 0), tck = -.01)
for(i in seq(iter_to_show))
{
contour(x1, x2, f, drawlabels = FALSE, col = "grey50")
title(paste("Iter =", iter_to_show[i]))
points(pop_rosenbrock[[iter_to_show[i]]], pch = 20, col = "forestgreen")
}
Ahora se va a realizar la optimización para tres dimensiones de la función de Rosenbrock.
postfit_rosenbrock_3d <- make_postfit("pop_rosenbrock_3d", "best_rosenbrock_3d")
TYPE = "real-valued"
popSize = 200
GA_rosenbrock_3d <- ga(type = TYPE,
fitness = function(x){-funcion_rosenbrock(x)},
lower = c(-2.048 , -2.048, -2.048), upper = c(2.048 , 2.048, 2.048),
maxiter = 2000,
run = 200,
population = gaControl(TYPE)$population,
selection = gaControl(TYPE)$selection,
crossover = gaControl(TYPE)$crossover,
mutation = gaControl(TYPE)$mutation,
popSize = popSize,
pcrossover = 0.64,
pmutation = 0.05,
elitism = base::max(1, round(popSize*0.13)),
seed = 555,
postFitness = postfit_rosenbrock_3d
)
summary(GA_rosenbrock_3d)
## ── Genetic Algorithm ───────────────────
##
## GA settings:
## Type = real-valued
## Population size = 200
## Number of generations = 2000
## Elitism = 26
## Crossover probability = 0.64
## Mutation probability = 0.05
## Search domain =
## x1 x2 x3
## lower -2.048 -2.048 -2.048
## upper 2.048 2.048 2.048
##
## GA results:
## Iterations = 2000
## Fitness function value = -0.1377346
## Solution =
## x1 x2 x3
## [1,] 0.821034 0.6753304 0.454874
Se procede a realizar la optimización en dos dimensiones de la función de Schwefel.
postfit_schwefel_2d <- make_postfit("pop_schwefel_2d", "best_schwefel_2d")
TYPE = "real-valued"
popSize = 50
GA_schwefel <- ga(type = TYPE,
fitness = function(x){-funcion_schwefel(x)},
lower = c(-500 , -500), upper = c(500 , 500),
maxiter = 1000,
run = 100,
population = gaControl(TYPE)$population,
selection = gaControl(TYPE)$selection,
crossover = gaControl(TYPE)$crossover,
mutation = gaControl(TYPE)$mutation,
popSize = popSize,
pcrossover = 0.88,
pmutation = 0.1,
elitism = base::max(1, round(popSize*0.05)),
seed = 555,
postFitness = postfit_schwefel_2d
)
summary(GA_schwefel)
## ── Genetic Algorithm ───────────────────
##
## GA settings:
## Type = real-valued
## Population size = 50
## Number of generations = 1000
## Elitism = 2
## Crossover probability = 0.88
## Mutation probability = 0.1
## Search domain =
## x1 x2
## lower -500 -500
## upper 500 500
##
## GA results:
## Iterations = 241
## Fitness function value = -0.000586059
## Solution =
## x1 x2
## [1,] 420.9069 420.9438
postfit_schwefel_3d <- make_postfit("pop_schwefel_3d", "best_schwefel_3d")
TYPE = "real-valued"
popSize = 200
GA_schwefel_3d <- ga(type = TYPE,
fitness = function(x) {-funcion_schwefel(x)},
lower = c(-500, -500, -500),
upper = c(500, 500, 500),
maxiter = 1000,
run = 100,
population = gaControl(TYPE)$population,
selection = gaControl(TYPE)$selection,
crossover = gaControl(TYPE)$crossover,
mutation = gaControl(TYPE)$mutation,
popSize = popSize,
pcrossover = 0.64,
pmutation = 0.05,
elitism = base::max(1, round(popSize * 0.13)),
seed = 555,
postFitness = postfit_schwefel_3d
)
summary(GA_schwefel_3d)
## ── Genetic Algorithm ───────────────────
##
## GA settings:
## Type = real-valued
## Population size = 200
## Number of generations = 1000
## Elitism = 26
## Crossover probability = 0.64
## Mutation probability = 0.05
## Search domain =
## x1 x2 x3
## lower -500 -500 -500
## upper 500 500 500
##
## GA results:
## Iterations = 334
## Fitness function value = -3.81827e-05
## Solutions =
## x1 x2 x3
## [1,] 420.9687 420.9687 420.9687
## [2,] 420.9687 420.9687 420.9687
## [3,] 420.9687 420.9687 420.9687
## [4,] 420.9687 420.9687 420.9687
## [5,] 420.9687 420.9687 420.9687
## [6,] 420.9687 420.9687 420.9687
## [7,] 420.9687 420.9687 420.9687
## [8,] 420.9687 420.9687 420.9687
## [9,] 420.9687 420.9687 420.9687
## [10,] 420.9687 420.9687 420.9687
## ...
## [26,] 420.9687 420.9687 420.9687
## [27,] 420.9687 420.9687 420.9687
La optimización por enjambre de partículas es un algoritmo bioinspirado basado en el comportamiento colectivo observado en sistemas naturales como bandadas de aves o bancos de peces. Cada partícula representa una solución potencial que “vuela” en el espacio de búsqueda ajustando su posición según su experiencia personal y la de sus vecinas. Las actualizaciones se rigen por la mejor posición individual y global conocidas, lo que permite un equilibrio entre la búsqueda local y global de óptimos.
Mediante el uso de la librería 'pso'
se va implementar
el algoritmo para optimizar las dos funciones en cuestion.
# Particle Swarm Optimization Function
pso_optimization <- function(
func, # La función a optimizar
dim, # Dimensión
n_particles = 30, # Número de partículas en el enjambre
max_iter = 100, # Número máximo de iteraciones
w = 0.7, # Peso de la inercia
c1 = 1.4, # Coeficiente de cognitivo
c2 = 1.4, # Coeficiente social
lower_bound, # Límite inferior del espacio de búsqueda
upper_bound, # Límite superior del espacio de búsqueda
target_fitness = -Inf # Valor de aptitud física objetivo para detenerse temprano.
) {
# Inicializar partículas y velocidades
particles <- matrix(runif(n_particles * dim, min = lower_bound, max = upper_bound),
nrow = n_particles, ncol = dim)
velocities <- matrix(runif(n_particles * dim, min = -0.1 * (upper_bound - lower_bound),
max = 0.1 * (upper_bound - lower_bound)),
nrow = n_particles, ncol = dim)
personal_best_positions <- particles
personal_best_fitnesses <- apply(particles, 1, func) # Use apply for row-wise fitness evaluation
global_best_position <- particles[which.min(personal_best_fitnesses), ]
global_best_fitness <- min(personal_best_fitnesses)
# Almacenar posiciones de partículas para cada iteración para graficar
particle_positions_history <- list()
fitness_history <- c() # track the best fitness over iterations
# Bucle de optimización
for (iter in 1:max_iter) {
# Update velocities and positions
r1 <- matrix(runif(n_particles * dim), nrow = n_particles, ncol = dim)
r2 <- matrix(runif(n_particles * dim), nrow = n_particles, ncol = dim)
velocities <- w * velocities +
c1 * r1 * (personal_best_positions - particles) +
c2 * r2 * (matrix(rep(global_best_position, n_particles),
nrow = n_particles, byrow = TRUE) - particles)
particles <- particles + velocities
# Recortar partículas a los límites del espacio de búsqueda
particles <- pmax(particles, lower_bound)
particles <- pmin(particles, upper_bound)
# Evaluar la idoneidad de los nuevos puestos
current_fitnesses <- apply(particles, 1, func) # Use apply here too
# Actualizar mejores posiciones y estados físicos personales
improved_mask <- current_fitnesses < personal_best_fitnesses
personal_best_positions[improved_mask, ] <- particles[improved_mask, ]
personal_best_fitnesses[improved_mask] <- current_fitnesses[improved_mask]
# Actualizar la mejor posición y condición física a nivel mundial
if (min(current_fitnesses) < global_best_fitness) {
global_best_position <- particles[which.min(current_fitnesses), ]
global_best_fitness <- min(current_fitnesses)
}
# Almacenar posiciones de partículas para esta iteración
particle_positions_history[[iter]] <- particles
fitness_history <- c(fitness_history, global_best_fitness)
# Compruebe si hay parada anticipada
if (global_best_fitness <= target_fitness) {
break
}
}
return(list(
particles = particles,
global_best_position = global_best_position,
global_best_fitness = global_best_fitness,
particle_positions_history = particle_positions_history, # Return history
fitness_history = fitness_history,
iterations = iter
))
}
# Parámetros
dim <- 2
n_particles <- 50
max_iter <- 10
lower_bound <- -5
upper_bound <- 5
# Ejecute PSO en la función Rosenbrock
result_rosenbrock <- pso_optimization(funcion_rosenbrock, dim, n_particles, max_iter,
lower_bound = lower_bound, upper_bound = upper_bound)
# Ejecute PSO en la función Schwefel
result_schwefel <- pso_optimization(funcion_schwefel, dim, n_particles, max_iter,
lower_bound = -500, upper_bound = 500,
target_fitness = 0) # Example target fitness
La evolución diferencial es una técnica estocástica de optimización continua que opera sobre una población de vectores mediante mecanismos de mutación diferencial, recombinación y selección. Su característica distintiva es la generación de nuevos candidatos mediante combinaciones lineales de soluciones existentes, lo que facilita una exploración eficiente del espacio de búsqueda. DE ha demostrado ser particularmente eficaz en problemas no lineales, no diferenciables y multimodales.
# OPTIMIZACIÓN de la función
differential_evolution <- function(fn, bounds, max_iter = 100, pop_size = 30, F = 0.8, CR = 0.9) {
D <- length(bounds[[1]])
pop <- matrix(runif(pop_size * D, bounds[[1]], bounds[[2]]), nrow = pop_size, byrow = TRUE)
fitness <- apply(pop, 1, fn)
history <- list(pop) # guardar posiciones
for (iter in 1:max_iter) {
for (i in 1:pop_size) {
idxs <- sample(setdiff(1:pop_size, i), 3)
x1 <- pop[idxs[1],]
x2 <- pop[idxs[2],]
x3 <- pop[idxs[3],]
mutant <- x1 + F * (x2 - x3)
mutant <- pmin(pmax(mutant, bounds[[1]]), bounds[[2]]) # clip
crossover <- runif(D) < CR
if (!any(crossover)) crossover[sample(1:D, 1)] <- TRUE
trial <- ifelse(crossover, mutant, pop[i,])
f_trial <- fn(trial)
if (f_trial < fitness[i]) {
pop[i,] <- trial
fitness[i] <- f_trial
}
}
history[[iter + 1]] <- pop
}
list(best = pop[which.min(fitness),], value = min(fitness), history = history)
}
# Visualización de la optimización para 2D
plot_history_gif <- function(history, fn, bounds, filename = "de_optimization.gif") {
frames <- list()
grid_points <- expand.grid(
x = seq(bounds[[1]][1], bounds[[2]][1], length.out = 100),
y = seq(bounds[[1]][2], bounds[[2]][2], length.out = 100)
)
grid_points$z <- apply(grid_points, 1, fn)
for (i in seq_along(history)) {
df <- as.data.frame(history[[i]])
colnames(df) <- c("x", "y")
p <- ggplot() +
geom_raster(data = grid_points, aes(x, y, fill = z), alpha = 0.5) +
scale_fill_viridis_c() +
geom_point(data = df, aes(x, y), color = "red", size = 2) +
ggtitle(sprintf("Iteración %d", i)) +
xlim(bounds[[1]][1], bounds[[2]][1]) +
ylim(bounds[[1]][2], bounds[[2]][2]) +
theme_minimal()
img <- image_graph(600, 600, res = 96)
print(p)
frames[[i]] <- image_capture()
dev.off()
}
animation <- image_animate(image_join(frames), fps = 5)
image_write(animation,filename)
}
#USO DE LAS FUNCIONES Y GENERACIÓN DE GIFS
set.seed(1983)
# Elegir función y dominio
bounds_rs <- list(c(-2, -2), c(2, 2)) # para Rosenbrock
bounds_sc <- list(c(-500, -500), c(500, 500)) # para Schwefel
result_rs <- differential_evolution(funcion_rosenbrock, bounds_rs, max_iter = 50, pop_size = 30)
result_sc <- differential_evolution(funcion_schwefel, bounds_sc, max_iter = 50, pop_size = 30)
plot_history_gif(result_sc$history, funcion_schwefel, bounds_sc, filename = "optimizacion_DE_sc.gif")
plot_history_gif(result_rs$history, funcion_rosenbrock, bounds_rs, filename = "optimizacion_DE_rs.gif")
cat("Mejor solución encontrada Rosenbrock:\n")
## Mejor solución encontrada Rosenbrock:
print(result_rs$best)
## [1] 0.9999241 0.9999315
cat("Mejor solución encontrada Schwefel:\n")
## Mejor solución encontrada Schwefel:
print(result_sc$best)
## [1] 421.1128 421.1281
cat("Valor óptimo Rosenbrock:\n")
## Valor óptimo Rosenbrock:
print(result_rs$value)
## [1] 7.002326e-07
cat("Valor óptimo Schwefel:\n")
## Valor óptimo Schwefel:
print(result_sc$value)
## [1] 0.005848507
##### Schwefel PSO
##### Schwefel GA 3D
##### Schwefel GA 2D
knitr::include_graphics("GA_Schwefel.gif")
Para funciones suaves y bien comportadas como Rosenbrock, el descenso por gradiente es eficiente y preciso, siempre que se parta de una buena condición inicial. Mientras que para funciones altamente multimodales o con formas complejas como Schwefel, los métodos heurísticos son superiores en la exploración global y probabilidad de encontrar el mínimo global.
En la práctica, una estrategia mixta (heurístico global + refinamiento con gradiente) puede ofrecer lo mejor de ambos mundos. Dependiendo del contexto del problema, es probable que un método sea mejor que otro, los métodos heurísticos tienen más variabilidad y puede que tome más tiempo encontrar una solución que cumpla con los requisitos establecidos pero en caso de tenerse clara las condiciones iniciales que tiene el problema quizás sea mejor un descenso por gradiente, esto también puede ser algo negativo, el descenso por gradiente es sensible a estas condiciones iniciales. En conclusión, al momento de elegir un método es importante tener un entendimiento de tanto las opciones disponibles a aplicar como la complejidad del problema, es importante usar la herramienta adecuada, ni la más simple ni la más compleja.
# OPTIMIZACIÓN COMBINATORIA |
En este documento se resuelve el problema del vendedor viajero visitando las 13 ciudades principales de Colombia. Para encontrar la mejor ruta se utilizan dos métodos de optimización: un Algoritmo Genético y un Algoritmo de Colonias de Hormigas. Se construye una matriz de costos en pesos colombianos, donde todo se traduce a valores monetarios: el costo del combustible según la distancia recorrida, el valor de los peajes y el tiempo de trabajo del vendedor (convertido a costo por hora). Al final, se incluye una visualización del recorrido óptimo sobre el mapa de Colombia y un GIF que muestra paso a paso cómo se construye esa ruta.
# Cargar paquetes necesarios
library(dplyr)
library(ggplot2)
library(gganimate)
library(leaflet)
Creamos un data.frame
con las 13 ciudades principales de
Colombia y sus respectivas coordenadas geográficas (latitud y
longitud).
ciudades <- data.frame(
ciudad = c("Bogotá", "Medellín", "Cali", "Barranquilla", "Cartagena",
"Bucaramanga", "Pereira", "Manizales", "Ibagué", "Pasto",
"Cúcuta", "Montería", "Santa Marta"),
lat = c(4.7110, 6.2442, 3.4516, 10.9639, 10.3910,
7.1193, 4.8133, 5.0703, 4.4389, 1.2136,
7.8941, 8.7479, 11.2408),
lon = c(-74.0721, -75.5812, -76.5319, -74.7964, -75.4794,
-73.1227, -75.6961, -75.5138, -75.2322, -77.2811,
-72.5050, -75.8814, -74.1990)
)
ciudades_nombres <- ciudades$ciudad
ciudades_cantidad <- nrow(ciudades)
Para obtener las matrices de distancias, de horas y de precios de peajes entre cada par de ciudades, se hace uso del siguiente servicio web que pertenece al Ministerio de Transporte de Colombia. Esta es una fuente muy confiable ya que es gubernamental.
Obtener la información de la anterior fuente fué la mejor alternativa debido a la confianza de los datos. Lamentablemente, no es una API donde se pueda obtener esta información a partir de consultas, tampoco se puede descargar la matriz con todos los datos. Para obtener estos datos, manualmente se tuvo que ingresar cada par de ciudades para obtener la información de la distancia, del tiempo promedio en el mejor de los casos y del costo de peajes.
horas_vector <- c(
0, 5.83, 5.85, 12.03, 12.52, 5.83, 4.08, 4.2, 2.95, 9.42, 7.43, 9.95, 11.28, 5.83, 0, 5.20, 8.33, 7.53, 5.17, 2.63, 2.55, 4.12, 9.35, 7.85, 4.95, 9.40, 5.77, 5.20, 0, 13.55, 12.73, 9.03, 2.75, 3.27, 3.23, 4.57, 11.73, 10.17, 13.57, 12.03, 8.33, 13.55, 0, 1.6, 6.88, 10.98, 10.90, 11.52, 17.70, 8.30, 4.28, 1.33, 12.52, 7.53, 12.73, 1.60, 0, 7.38, 10.18, 10.10, 11.65, 16.88, 8.78, 3.15, 2.87, 5.83, 5.17, 9.03, 6.88, 7.38, 0, 6.48, 6.03, 6.23, 13.18, 2.68, 8.32, 6.13, 4, 2.63, 2.75, 10.98, 10.18, 6.48, 0, 0.7, 1.47, 6.09, 9.17, 7.60, 11.02, 4.20, 2.55, 3.27, 10.90, 10.10, 6.03, 0.70, 0, 2.18, 7.42, 8.73, 7.52, 10.57, 2.95, 4.20, 3.32, 11.52, 11.75, 6.23, 1.55, 2.27, 0, 7.47, 8.92, 9.17, 10.77, 9.42, 9.35, 4.93, 17.70, 16.88, 13.18, 6.90, 7.42, 7.38, 0, 15.47, 14.32, 17.72, 7.43, 7.85, 11.73, 8.30, 8.78, 2.68, 9.17, 8.73, 8.92, 15.47, 0, 9.72, 7.55, 9.95, 4.95, 10.17, 4.28, 3.15, 8.32, 7.60, 7.52, 9.08, 14.32, 9.72, 0, 5.33, 11.28, 9.40, 13.57, 1.33, 2.87, 6.13, 11.02, 10.57, 10.77, 17.72, 7.55, 5.33, 0
)
horas_matriz <- matrix(
horas_vector,
nrow = ciudades_cantidad,
ncol = ciudades_cantidad,
byrow = TRUE,
dimnames = list(ciudades_nombres, ciudades_nombres)
)
horas_matriz <- horas_matriz * 1.35
A continuación, mostramos una parte de la matriz de horas para validar los resultados.
round(horas_matriz[1:5, 1:5], 1)
## Bogotá Medellín Cali Barranquilla Cartagena
## Bogotá 0.0 7.9 7.9 16.2 16.9
## Medellín 7.9 0.0 7.0 11.2 10.2
## Cali 7.8 7.0 0.0 18.3 17.2
## Barranquilla 16.2 11.2 18.3 0.0 2.2
## Cartagena 16.9 10.2 17.2 2.2 0.0
distancias_viales_vector <- c(
0.0, 417.42, 447.76, 971.37, 1038.66, 402.10, 309.80, 292.91, 196.75, 770.34, 577.67, 804.78, 927.16, 417.42, 0.0, 414.42, 695.13, 647.92, 398.48, 210.94, 196.73, 323.95, 793.31, 595.77, 402.49, 787.97, 439.94, 414.42, 0.0, 1109.55, 1062.34, 766.71, 207.98, 254.40, 250.98, 371.34, 964.00, 816.91, 1197.26, 971.37, 695.13, 1109.55, 0.0, 128.49, 575.06, 906.07, 891.87, 998.29, 1488.44, 71.23, 348.61, 98, 1038.66, 647.92, 1062.34, 128.49, 0.0, 642.35, 858.86, 844.66, 971.88, 1441.23, 777.52, 272.17, 230.72, 402.10, 398.48, 761.23, 575.06, 642.35, 0.0, 557.75, 511.80, 523.53, 1140.12, 197.29, 714.89, 530.85, 301.98, 210.94, 207.98, 906.07, 858.86, 557.75, 0.0, 50.3, 113.02, 586.87, 755.05, 613.43, 988.30, 292.91, 196.73, 254.40, 891.87, 844.66, 511.80, 50.93, 0.0, 163.94, 633.30, 709.09, 599.23, 942.35, 196.75, 331.77, 258.80, 998.29, 979.70, 523.53, 120.84, 171.76, 0.0, 637.69, 720.82, 734.27, 954.07, 770.34, 793.31, 384.17, 1488.44, 1441.23, 1145.60, 586.87, 633.30, 629.87, 0.0, 1347.89, 1195.80, 1576.15, 577.67, 595.77, 958.52, 710.23, 777.52, 197.29, 755.05, 709.09, 720.82, 1347.89, 0.0, 850.06, 666.02, 804.78, 402.49, 816.91, 348.61, 272.17, 714.89, 613.43, 599.23, 726.45, 1195.80, 850.06, 0.0, 441.44, 927.16, 787.97, 1191.78, 98, 230.72, 530.85, 988.30, 942.35, 954.07, 1570.67, 666.02, 441.44, 0.0
)
distancias_viales_matriz <- matrix(
distancias_viales_vector,
nrow = ciudades_cantidad,
ncol = ciudades_cantidad,
byrow = TRUE,
dimnames = list(ciudades_nombres, ciudades_nombres)
)
A continuación, mostramos una parte de la matriz de distancias viales (en km) para validar los resultados.
round(distancias_viales_matriz[1:5, 1:5], 1)
## Bogotá Medellín Cali Barranquilla Cartagena
## Bogotá 0.0 417.4 447.8 971.4 1038.7
## Medellín 417.4 0.0 414.4 695.1 647.9
## Cali 439.9 414.4 0.0 1109.6 1062.3
## Barranquilla 971.4 695.1 1109.6 0.0 128.5
## Cartagena 1038.7 647.9 1062.3 128.5 0.0
peajes_vector <- c(
0, 98400, 119600, 123800, 122300, 51500, 82900, 59700, 53100, 93600, 41200, 199600, 103600, 98400, 0, 138900, 145600, 133700, 68800, 82400, 66300, 112200, 151100, 90000, 101200, 165800, 119600, 138900, 0, 284500, 272600, 128100, 56500, 72600, 66500, 34300, 149300, 240100, 190400, 123800, 145600, 284500, 0, 23100, 94000, 228000, 211900, 144100, 296700, 76300, 57300, 20200, 122300, 133700, 272600, 23100, 0, 92500, 216100, 200000, 245900, 284800, 74800, 75500, 43300, 51500, 68800, 120500, 94000, 92500, 0, 64000, 47900, 61600, 132700, 21200, 105300, 73800, 82900, 82400, 56500, 228000, 216100, 64000, 0, 16100, 29800, 68700, 85200, 183600, 126300, 59700, 66300, 72600, 211900, 200000, 47900, 16100, 0, 45900, 84800, 69100, 167500, 110200, 53100, 112200, 66500, 144100, 245900, 61600, 29800, 45900, 0, 78700, 82800, 213400, 123900, 93600, 151100, 34300, 296700, 284800, 140300, 68700, 84800, 78700, 0, 146000, 252300, 202600, 41200, 90000, 141700, 76300, 74800, 21200, 85200, 69100, 82800, 146000, 0, 87600, 56100, 199600, 101200, 240100, 57300, 75500, 105300, 183600, 167500, 213400, 252300, 87600, 0, 77500, 103600, 165800, 182800, 20200, 43300, 73800, 126300, 110200, 123900, 195000, 56100, 77500, 0
)
peajes_matriz <- matrix(
peajes_vector,
nrow = ciudades_cantidad,
ncol = ciudades_cantidad,
byrow = TRUE,
dimnames = list(ciudades_nombres, ciudades_nombres)
)
A continuación, mostramos una parte de la matriz de precios de peajes (en COP) para validar los resultados.
round(peajes_matriz[1:5, 1:5], 1)
## Bogotá Medellín Cali Barranquilla Cartagena
## Bogotá 0 98400 119600 123800 122300
## Medellín 98400 0 138900 145600 133700
## Cali 119600 138900 0 284500 272600
## Barranquilla 123800 145600 284500 0 23100
## Cartagena 122300 133700 272600 23100 0
El costo por kilómetro se calcula con base en los siguientes supuestos:
Cálculo del costo por km:
\[ \text{Costo por km} = \frac{4,308\ \text{COP}}{9.35\ \text{km/L}} \approx \mathbf{460\ COP\ por\ km} \]
Este valor se utiliza como base para estimar el gasto en combustible entre cada par de ciudades.
60.000 COP
Combustible =
distancia * costo_km
costo_km <- 460 # pesos colombianos por kilómetro recorrido
valor_hora <- 60000 # valor hora del vendedor
Creamos una matriz con el costo total entre cada par de ciudades, donde:
\[ \text{costo_total} = (\text{tiempo} \cdot \text{valor_hora}) + \text{peajes} + (\text{distancia} \cdot \text{costo_km}) \]
En la diagonal principal, el costo será cero porque no hay desplazamiento.
costos_matriz <- matrix(0, nrow = ciudades_cantidad, ncol = ciudades_cantidad)
colnames(costos_matriz) <- ciudades_nombres
rownames(costos_matriz) <- ciudades_nombres
for (i in 1:ciudades_cantidad) {
for (j in 1:ciudades_cantidad) {
if (i != j) {
distancia_vial <- distancias_viales_matriz[i, j]
horas <- horas_matriz[i, j]
costo_peaje <- peajes_matriz[i, j]
combustible <- distancia_vial * costo_km
costos_matriz[i, j] <- horas * valor_hora + costo_peaje + combustible
}
}
}
round(costos_matriz[1:5, 1:5], 0)
## Bogotá Medellín Cali Barranquilla Cartagena
## Bogotá 0 762643 799420 1545060 1614204
## Medellín 762643 0 750733 1140090 1041673
## Cali 789342 750733 0 1892443 1792406
## Barranquilla 1545060 1140090 1892443 0 211805
## Cartagena 1614204 1041673 1792406 211805 0
Implementaremos un algoritmo genético (GA) para minimizar el costo total del recorrido entre ciudades.
generaciones_cantidad <- 200
poblacion_cantidad <- 100
tasa_mutacion <- 0.1
Dado un vector con el orden de las ciudades, esta función suma los costos entre ciudades consecutivas.
evaluar_ruta <- function(ruta, matriz_costos) {
total <- 0
for (i in 1:(ciudades_cantidad - 1)) {
total <- total + matriz_costos[ruta[i], ruta[i + 1]]
}
# Regresar a la ciudad inicial
total <- total + matriz_costos[ruta[ciudades_cantidad], ruta[1]]
return(total)
}
seleccionar_padres_por_torneo <- function(poblacion, puntajes) {
padres <- vector("list", poblacion_cantidad)
for (i in 1:poblacion_cantidad) {
torneo <- sample(1:poblacion_cantidad, 2)
if (puntajes[torneo[1]] < puntajes[torneo[2]]) {
padres[[i]] <- poblacion[[torneo[1]]]
} else {
padres[[i]] <- poblacion[[torneo[2]]]
}
}
return(padres)
}
cruzar <- function(padre1, padre2) {
punto <- sample(2:(ciudades_cantidad - 1), 1)
hijo <- padre1[1:punto]
hijo <- c(hijo, padre2[!padre2 %in% hijo])
return(hijo)
}
mutar <- function(ruta) {
if (runif(1) < tasa_mutacion) {
pos <- sample(1:ciudades_cantidad, 2)
ruta[pos] <- ruta[rev(pos)]
}
return(ruta)
}
generar_nueva_poblacion <- function(padres) {
nueva_poblacion <- list()
for (i in seq(1, poblacion_cantidad, by = 2)) {
hijo1 <- mutar(cruzar(padres[[i]], padres[[i+1]]))
hijo2 <- mutar(cruzar(padres[[i+1]], padres[[i]]))
nueva_poblacion <- c(nueva_poblacion, list(hijo1, hijo2))
}
return(nueva_poblacion)
}
# Inicializar población
poblacion <- replicate(poblacion_cantidad, sample(ciudades_nombres), simplify = FALSE)
for (gen in 1:generaciones_cantidad) {
# Evaluar población
puntajes <- sapply(poblacion, function(ruta) evaluar_ruta(ruta, costos_matriz))
padres <- seleccionar_padres_por_torneo(poblacion, puntajes)
poblacion <- generar_nueva_poblacion(padres)
}
puntajes_finales <- sapply(poblacion, function(ruta) evaluar_ruta(ruta, costos_matriz))
mejor_ruta <- poblacion[[which.min(puntajes_finales)]]
mejor_costo <- min(puntajes_finales)
ruta_legible <- paste(c(mejor_ruta, mejor_ruta[1]), collapse = " → ")
cat("Mejor ruta encontrada:\n", ruta_legible, "\n\n")
## Mejor ruta encontrada:
## Bogotá → Ibagué → Cali → Pasto → Pereira → Manizales → Medellín → Montería → Cartagena → Santa Marta → Barranquilla → Cúcuta → Bucaramanga → Bogotá
cat("Costo total: ", format(mejor_costo, big.mark = ","), "COP\n")
## Costo total: 6,286,303 COP
Graficaremos el recorrido óptimo calculado por el Algoritmo Genetico usando coordenadas geográficas y líneas conectadas.
# Se obtienen los índices de las ciudades en el orden de mejor_ruta
indices <- match(mejor_ruta, ciudades_nombres)
# Se extraen y se reordenan las filas según mejor_ruta
ruta_coords <- ciudades[indices, ]
# Se agrega el punto inicial al final para cerrar el ciclo
ruta_coords <- rbind(ruta_coords, ruta_coords[1, ])
leaflet_output <- addCircleMarkers(
addPolylines(
addTiles(
leaflet(ruta_coords)
),
lng = ~lon, lat = ~lat,
color = "blue", weight = 2,
label = paste0("De ", ruta_coords$ciudad[-nrow(ruta_coords)],
" a ", ruta_coords$ciudad[-1])
),
lng = ~lon, lat = ~lat,
label = ~ciudad,
color = "red", radius = 5,
fillOpacity = 0.8
)
leaflet_output
Este algoritmo simula cómo las hormigas encuentran caminos cortos mediante feromonas y visibilidad (inverso de la distancia/costo).
###️ Parámetros del algoritmo
num_hormigas <- 50
num_iteraciones <- 100
alpha <- 1 # influencia de feromonas
beta <- 5 # influencia de visibilidad (1 / costo)
rho <- 0.5 # tasa de evaporación
Q <- 100 # cantidad de feromona depositada
# Inicializar feromonas
feromonas <- matrix(1, nrow = ciudades_cantidad, ncol = ciudades_cantidad)
rownames(feromonas) <- ciudades_nombres
colnames(feromonas) <- ciudades_nombres
# Visibilidad (1 / costo), evitando división por cero
visibilidad <- 1 / (costos_matriz + diag(Inf, ciudades_cantidad))
mejor_ruta_aco <- NULL
mejor_costo_aco <- Inf
for (iter in 1:num_iteraciones) {
rutas <- list()
costos <- numeric(num_hormigas)
for (k in 1:num_hormigas) {
visitadas <- sample(ciudades_nombres, 1)
while (length(visitadas) < ciudades_cantidad) {
actual <- tail(visitadas, 1)
no_visitadas <- setdiff(ciudades_nombres, visitadas)
probabilidades <- sapply(no_visitadas, function(ciudad) {
feromona <- feromonas[actual, ciudad]^alpha
visib <- visibilidad[actual, ciudad]^beta
feromona * visib
})
probabilidades <- probabilidades / sum(probabilidades)
siguiente <- sample(no_visitadas, 1, prob = probabilidades)
visitadas <- c(visitadas, siguiente)
}
rutas[[k]] <- visitadas
costos[k] <- evaluar_ruta(visitadas, costos_matriz)
if (costos[k] < mejor_costo_aco) {
mejor_costo_aco <- costos[k]
mejor_ruta_aco <- visitadas
}
}
# Evaporación de feromonas
feromonas <- (1 - rho) * feromonas
# Depositar nuevas feromonas
for (k in 1:num_hormigas) {
ruta <- rutas[[k]]
for (i in 1:(ciudades_cantidad - 1)) {
a <- ruta[i]
b <- ruta[i + 1]
feromonas[a, b] <- feromonas[a, b] + Q / costos[k]
feromonas[b, a] <- feromonas[b, a] + Q / costos[k]
}
# Conectar final con inicio
feromonas[ruta[ciudades_cantidad], ruta[1]] <- feromonas[ruta[ciudades_cantidad], ruta[1]] + Q / costos[k]
feromonas[ruta[1], ruta[ciudades_cantidad]] <- feromonas[ruta[1], ruta[ciudades_cantidad]] + Q / costos[k]
}
}
mejor_ruta_aco
## [1] "Pasto" "Cali" "Ibagué" "Bogotá" "Bucaramanga"
## [6] "Cúcuta" "Santa Marta" "Barranquilla" "Cartagena" "Montería"
## [11] "Medellín" "Manizales" "Pereira"
mejor_costo_aco
## [1] 6267715
# Nos aseguramos de tener el mejor recorrido
ruta <- mejor_ruta_aco
coords <- ciudades[match(ruta, ciudades$ciudad), ]
# Agregar punto de retorno al inicio
coords <- rbind(coords, coords[1, ])
# Crear data.frame animable paso a paso
anim_data <- data.frame()
for (i in 2:nrow(coords)) {
paso <- coords[1:i, ]
paso$frame <- i - 1
paso$orden <- 1:nrow(paso)
anim_data <- rbind(anim_data, paso)
}
p <- ggplot(anim_data, aes(x = lon, y = lat)) +
borders("world", regions = "Colombia", fill = "gray95", colour = "gray60") +
geom_path(aes(group = frame), color = "blue", linewidth = 1) +
geom_point(color = "red", size = 3) +
geom_text(aes(label = ciudad), vjust = -1, size = 3) +
labs(title = "Recorrido óptimo - Paso: {frame}") +
coord_fixed(1.3) +
theme_minimal()
anim <- p + transition_manual(frames = frame)
anim_save(
"recorrido_paso_a_paso.gif",
animation = anim,
renderer = gifski_renderer(),
width = 832, # ancho en píxeles
height = 1080, # alto en píxeles
units = "px"
)
## `nframes` and `fps` adjusted to match transition
Surjanovic, S., & Bingham, D. (s.f.). Rosenbrock Function. Simon Fraser University. https://www.sfu.ca/~ssurjano/rosen.html
Surjanovic, S., & Bingham, D. (s.f.). Schwefel Function. Simon Fraser University. https://www.sfu.ca/~ssurjano/schwef.html