Files
optimization/task2/1d/gradient_descent_1d.py
2026-01-09 12:13:15 +03:00

349 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Градиентный спуск для одномерной функции с тремя методами выбора шага:
1. Константный шаг
2. Золотое сечение (одномерная оптимизация)
3. Правило Армихо
Функция: f(x) = sqrt(x^2 + 9) / 4 + (5 - x) / 5
Область: [-3, 8]
"""
import shutil
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
import matplotlib.pyplot as plt
import numpy as np
from common.functions import TaskFunction1D
from common.gradient_descent import (
GradientDescentResult1D,
gradient_descent_1d,
heavy_ball_1d,
)
# ============================================================================
# НАСТРОЙКИ
# ============================================================================
# Стартовая точка
X0 = -1.0
# Параметры сходимости
EPS_X = 0.05
EPS_F = 0.001
MAX_ITERS = 100
# Шаг для константного метода (небольшой, чтобы было 3-4+ итерации)
CONSTANT_STEP = 12
# Параметры для правила Армихо
ARMIJO_PARAMS = {
"d_init": 12.0,
"epsilon": 0.1,
"theta": 0.5,
}
# Границы для золотого сечения
GOLDEN_SECTION_BOUNDS = (0.0, 30.0)
# Параметры для метода тяжёлого шарика
HEAVY_BALL_ALPHA = 0.5
HEAVY_BALL_BETA = 0.8
# Папка для сохранения графиков
OUTPUT_DIR = Path(__file__).parent / "plots"
# ============================================================================
# ВИЗУАЛИЗАЦИЯ
# ============================================================================
def plot_iteration(
func: TaskFunction1D,
result: GradientDescentResult1D,
iter_idx: int,
output_path: Path,
):
"""Построить график для одной итерации."""
info = result.iterations[iter_idx]
# Диапазон для графика
a, b = func.domain
x_plot = np.linspace(a, b, 500)
y_plot = [func(x) for x in x_plot]
plt.figure(figsize=(10, 6))
# Функция
plt.plot(x_plot, y_plot, "b-", linewidth=2, label="f(x)")
# Траектория до текущей точки
trajectory_x = [result.iterations[i].x for i in range(iter_idx + 1)]
trajectory_y = [result.iterations[i].f_x for i in range(iter_idx + 1)]
if len(trajectory_x) > 1:
plt.plot(
trajectory_x,
trajectory_y,
"g--",
linewidth=1.5,
alpha=0.7,
label="Траектория",
)
# Предыдущие точки
for i, (x, y) in enumerate(zip(trajectory_x[:-1], trajectory_y[:-1])):
plt.plot(x, y, "go", markersize=6, alpha=0.5)
# Текущая точка
plt.plot(
info.x,
info.f_x,
"ro",
markersize=12,
label=f"x = {info.x:.4f}, f(x) = {info.f_x:.4f}",
)
# Направление градиента (касательная)
grad_scale = 0.5
x_grad = np.array([info.x - grad_scale, info.x + grad_scale])
y_grad = info.f_x + info.grad * (x_grad - info.x)
plt.plot(
x_grad, y_grad, "m-", linewidth=2, alpha=0.6, label=f"f'(x) = {info.grad:.4f}"
)
plt.xlabel("x", fontsize=12)
plt.ylabel("f(x)", fontsize=12)
plt.title(
f"{result.method} — Итерация {info.iteration}\nШаг: {info.step_size:.6f}",
fontsize=14,
fontweight="bold",
)
plt.legend(fontsize=10, loc="upper right")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(output_path, dpi=150)
plt.close()
def plot_final_result(
func: TaskFunction1D,
result: GradientDescentResult1D,
output_path: Path,
):
"""Построить итоговый график с полной траекторией."""
a, b = func.domain
x_plot = np.linspace(a, b, 500)
y_plot = [func(x) for x in x_plot]
plt.figure(figsize=(10, 6))
# Функция
plt.plot(x_plot, y_plot, "b-", linewidth=2, label="f(x)")
# Траектория
trajectory_x = [it.x for it in result.iterations]
trajectory_y = [it.f_x for it in result.iterations]
plt.plot(
trajectory_x, trajectory_y, "g-", linewidth=2, alpha=0.7, label="Траектория"
)
# Все точки
for i, (x, y) in enumerate(zip(trajectory_x[:-1], trajectory_y[:-1])):
plt.plot(x, y, "go", markersize=8, alpha=0.6)
# Финальная точка
plt.plot(
result.x_star,
result.f_star,
"r*",
markersize=20,
label=f"x* = {result.x_star:.6f}\nf(x*) = {result.f_star:.6f}",
)
plt.xlabel("x", fontsize=12)
plt.ylabel("f(x)", fontsize=12)
plt.title(
f"{result.method} — Результат\nИтераций: {len(result.iterations) - 1}",
fontsize=14,
fontweight="bold",
)
plt.legend(fontsize=10, loc="upper right")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(output_path, dpi=150)
plt.close()
def run_and_visualize(
func: TaskFunction1D,
method: str,
method_name_short: str,
**kwargs,
):
"""Запустить метод и создать визуализации."""
result = gradient_descent_1d(
func=func,
x0=X0,
step_method=method,
eps_x=EPS_X,
eps_f=EPS_F,
max_iters=MAX_ITERS,
**kwargs,
)
# Создаём папку для этого метода
method_dir = OUTPUT_DIR / method_name_short
method_dir.mkdir(parents=True, exist_ok=True)
# Печатаем информацию
print(f"\n{'=' * 80}")
print(f"{result.method}")
print("=" * 80)
for info in result.iterations[:-1]: # Без финальной точки
print(
f"Итерация {info.iteration:3d}: "
f"x = {info.x:10.6f}, f(x) = {info.f_x:10.6f}, "
f"f'(x) = {info.grad:10.6f}, шаг = {info.step_size:.6f}"
)
# Строим график для каждой итерации
plot_iteration(
func,
result,
info.iteration - 1,
method_dir / f"iteration_{info.iteration:02d}.png",
)
# Итоговый результат
print("-" * 80)
print(f"x* = {result.x_star:.6f}")
print(f"f(x*) = {result.f_star:.6f}")
print(f"Итераций: {len(result.iterations) - 1}")
# Финальный график
plot_final_result(func, result, method_dir / "final_result.png")
print(f"Графики сохранены в: {method_dir}")
return result
def run_and_visualize_heavy_ball(
func: TaskFunction1D,
method_name_short: str,
alpha: float,
beta: float,
):
"""Запустить метод тяжёлого шарика и создать визуализации."""
result = heavy_ball_1d(
func=func,
x0=X0,
alpha=alpha,
beta=beta,
eps_x=EPS_X,
eps_f=EPS_F,
max_iters=MAX_ITERS,
)
# Создаём папку для этого метода
method_dir = OUTPUT_DIR / method_name_short
method_dir.mkdir(parents=True, exist_ok=True)
# Печатаем информацию
print(f"\n{'=' * 80}")
print(f"{result.method}")
print("=" * 80)
for info in result.iterations[:-1]:
print(
f"Итерация {info.iteration:3d}: "
f"x = {info.x:10.6f}, f(x) = {info.f_x:10.6f}, "
f"f'(x) = {info.grad:10.6f}, шаг = {info.step_size:.6f}"
)
plot_iteration(
func,
result,
info.iteration - 1,
method_dir / f"iteration_{info.iteration:02d}.png",
)
# Итоговый результат
print("-" * 80)
print(f"x* = {result.x_star:.6f}")
print(f"f(x*) = {result.f_star:.6f}")
print(f"Итераций: {len(result.iterations) - 1}")
# Финальный график
plot_final_result(func, result, method_dir / "final_result.png")
print(f"Графики сохранены в: {method_dir}")
return result
def main():
"""Главная функция."""
func = TaskFunction1D()
print("=" * 80)
print("ГРАДИЕНТНЫЙ СПУСК ДЛЯ ОДНОМЕРНОЙ ФУНКЦИИ")
print("=" * 80)
print(f"Функция: {func.name}")
print(f"Область: {func.domain}")
print(f"Стартовая точка: x₀ = {X0}")
print(f"Параметры: eps_x = {EPS_X}, eps_f = {EPS_F}, max_iters = {MAX_ITERS}")
# Очищаем и создаём папку для графиков
if OUTPUT_DIR.exists():
shutil.rmtree(OUTPUT_DIR)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 1. Константный шаг
run_and_visualize(
func,
method="constant",
method_name_short="constant",
step_size=CONSTANT_STEP,
)
# 2. Золотое сечение
run_and_visualize(
func,
method="golden_section",
method_name_short="golden_section",
golden_section_bounds=GOLDEN_SECTION_BOUNDS,
)
# 3. Правило Армихо
run_and_visualize(
func,
method="armijo",
method_name_short="armijo",
armijo_params=ARMIJO_PARAMS,
)
# 4. Метод тяжёлого шарика
run_and_visualize_heavy_ball(
func,
method_name_short="heavy_ball",
alpha=HEAVY_BALL_ALPHA,
beta=HEAVY_BALL_BETA,
)
print("\n" + "=" * 80)
print("ГОТОВО! Все графики сохранены.")
print("=" * 80)
if __name__ == "__main__":
main()