import numpy as np
from pysisyphus.optimizers.Optimizer import Optimizer
[docs]
class BacktrackingOptimizer(Optimizer):
def __init__(
self,
geometry,
alpha,
bt_force=5,
dont_skip_after=2,
bt_max_scale=4,
bt_disable=False,
**kwargs,
):
# Setting some default values
self.alpha = alpha
assert self.alpha > 0, "Alpha must be positive!"
self.bt_force = bt_force
self.dont_skip_after = dont_skip_after
self.bt_max_scale = bt_max_scale
self.bt_disable = bt_disable
assert self.dont_skip_after >= 1
self.cycles_since_backtrack = self.bt_force
self.scale_factor = 0.5
super(BacktrackingOptimizer, self).__init__(geometry, **kwargs)
self.alpha0 = self.alpha
self.alpha_max = self.bt_max_scale * self.alpha0
# Keep the skipping history to avoid infinite skipping, e.g. always
# return skip = False if we already skipped in the last n iterations.
self.skip_log = list()
def _get_opt_restart_info(self):
opt_restart_info = {
"alpha": self.alpha,
"cycles_since_backtrack": self.cycles_since_backtrack,
}
return opt_restart_info
def _set_opt_restart_info(self, opt_restart_info):
self.alpha = opt_restart_info["alpha"]
self.cycles_since_backtrack = opt_restart_info["cycles_since_backtrack"]
[docs]
def reset(self):
if self.alpha > self.alpha0:
self.alpha = self.alpha0
self.log(
f"Reset current alpha {self.alpha} to initial value {self.alpha0}."
)
[docs]
def backtrack(self, cur_forces, prev_forces, reset_hessian=None, epsilon=1e-3):
"""Accelerated backtracking line search."""
if self.bt_disable:
return False
rms = lambda f: np.sqrt(np.mean(np.square(f)))
cur_rms_force = rms(cur_forces)
prev_rms_force = rms(prev_forces)
rms_diff = (cur_rms_force - prev_rms_force) / np.abs(
cur_rms_force + prev_rms_force
)
# Skip tells us if we overshot
skip = False
# When the optimiziation is converging cur_forces will
# be smaller than prev_forces, so rms_diff will be negative
# and hence smaller than epsilon, which is a positive number.
# We went uphill, slow alpha
self.log(f"Backtracking: rms_diff = {rms_diff:.03f}, {epsilon=:.4f}")
if rms_diff > epsilon:
self.log(f"rms_diff > epsilon! Scaling alpha with {self.scale_factor:.03f}")
# self.alpha = max(self.alpha0*.5, self.alpha*self.scale_factor)
self.alpha *= self.scale_factor
skip = True
self.cycles_since_backtrack = self.bt_force
# We continnue going downhill, rms_diff is smaller than epsilon
else:
self.cycles_since_backtrack -= 1
# Check if we didn't accelerate in the previous cycles
if self.cycles_since_backtrack < 0:
self.cycles_since_backtrack = self.bt_force
if self.alpha < self.alpha0:
# Reset alpha
self.alpha = self.alpha0
skip = True
self.log(f"Reset alpha to alpha0 = {self.alpha0:.4f}")
else:
# Accelerate alpha
self.alpha /= self.scale_factor
self.log(f"Scaled alpha to {self.alpha:.4f}")
# Avoid huge alphas
if self.alpha > self.alpha_max:
self.alpha = self.alpha_max
self.log(
"Didn't accelerate as alpha would become too large. "
f"keeping it at {self.alpha}."
)
# Don't skip if we already skipped the previous iterations to
# avoid infinite skipping.
if (len(self.skip_log) >= self.dont_skip_after) and all(
self.skip_log[-self.dont_skip_after :]
):
self.log(
f"already skipped last {self.dont_skip_after} "
"iterations don't skip now."
)
skip = False
if self.alpha > self.alpha0:
self.alpha = self.alpha0
self.log("Resetted alpha to alpha0.")
self.skip_log.append(skip)
self.log(f"alpha = {self.alpha:.4f}, skip = {skip}")
return skip