346 lines
9.6 KiB
Python
346 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Градиентный спуск для одномерной функции с тремя методами выбора шага:
|
||
1. Константный шаг
|
||
2. Золотое сечение (одномерная оптимизация)
|
||
3. Правило Армихо
|
||
|
||
Функция: f(x) = sqrt(x^2 + 9) / 4 + (5 - x) / 5
|
||
Область: [-3, 8]
|
||
"""
|
||
|
||
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 = 0.5
|
||
|
||
# Параметры для правила Армихо
|
||
ARMIJO_PARAMS = {
|
||
"d_init": 2.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}")
|
||
|
||
# Создаём папку для графиков
|
||
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()
|