Files
faceswap/lib/model/optimizers.py
T
torzdf 837bc2d51d Faceswap 3 (#1516)
* FaceSwap 3 (#1515)
* Update extract pipeline
* Update requirements + setup for nvidia
* Remove allow-growth option
* tf.keras to keras updates
* lib.model.losses - Port + fix all loss functions for Keras3
* lib.model - port initializers, layers. normalization to Keras3
* lib.model.autoclip to Keras 3
* Update mixed precision layer storage
* model file to .keras format
* Restructure nn_blocks to initialize layers in __init__
* Tensorboard
  - Trainer: Add Torch compatible Tensorboard callbacks
  - GUI event reader remove TF dependency
* Loss logging
  - Flush TB logs on save
  - Replace TB live iterator for GUI
* Backup models on total loss drop rather than per side
* Update all models to Keras3 Compat
* Remove lib.model.session
* Update clip ViT to Keras 3
* plugins.extract.mask.unet-dfl - Fix for Keras3/Torch backend
* Port AdaBelief to Keras 3
* setup.py:
  - Add --dev flag for dev tool install
* Fix Keras 3 syntax
* Fix LR Finder for Keras 3
* Fix mixed precision switching for Keras  3
* Add more optimizers + open up config setting
* train: Remove updating FS1 weights to FS2 models
* Alignments: Remove support for legacy .json files
* tools.model:
  - Remove TF Saved Format saving
  - Fix Backup/Restore + Nan-Scan
* Fix inference model creation for Keras 3
* Preview tool: Fix for Keras3
* setup.py: Configure keras backend
* train: Migration of FS2 models to FS3
* Training: Default coverage to 100%
* Remove DirectML backend
* Update setup for MacOS
* GUI: Force line reading to UTF-8
* Remove redundant Tensorflow references
* Remove redundant code
* Legacy model loading: Fix TFLamdaOp scalar ops and DepthwiseConv2D
* Add vertical offset option for training
* Github actions: Add more python versions
* Add python version to workflow names
* Github workflow: Exclude Python 3.12 for macOS
* Implement custom training loop
* Fs3 - Add RTX5xxx and ROCm 6.1-6.4 support (#1511)
* setup.py: Add Cuda/ROCm version select options
* bump minimum python version to 3.11
* Switch from setup.cgf to pyproject.toml
* Documentation: Update all docs to use automodapi
* Allow sysinfo to run with missing packages + correctly install tk under Linux
* Bugfix: dot naming convention in clip models
* lib.config: Centralise globally rather than passing as object
- Add torch DataParallel for multi-gpu training
  - GUI: Group switches together when generating cli args
  - CLI: Remove deprecated multi-character argparse args
  - Refactor:
    - Centralise tensorboard reading/writing + unit tests
    - Create trainer plugin interfaces + add original + distributed
* Update installers
2025-12-21 02:45:11 +00:00

332 lines
14 KiB
Python

#!/usr/bin/env python3
""" Custom Optimizers for Torch/keras """
from __future__ import annotations
import inspect
import logging
import sys
import typing as T
from keras import ops, Optimizer, saving
from lib.logger import parse_class_init
from lib.utils import get_module_objects
if T.TYPE_CHECKING:
from keras import KerasTensor, Variable
logger = logging.getLogger(__name__)
class AdaBelief(Optimizer): # pylint:disable=too-many-instance-attributes,too-many-ancestors
""" Implementation of the AdaBelief Optimizer
Inherits from: keras.optimizers.Optimizer.
AdaBelief Optimizer is not a placement of the heuristic warmup, the settings should be kept if
warmup has already been employed and tuned in the baseline method. You can enable warmup by
setting `total_steps` and `warmup_proportion` (see examples)
Lookahead (see references) can be integrated with AdaBelief Optimizer, which is announced by
Less Wright and the new combined optimizer can also be called "Ranger". The mechanism can be
enabled by using the lookahead wrapper. (See examples)
Parameters
----------
learning_rate: `Tensor`, float or :class: `keras.optimizers.schedules.LearningRateSchedule`
The learning rate.
beta_1: float
The exponential decay rate for the 1st moment estimates.
beta_2: float
The exponential decay rate for the 2nd moment estimates.
epsilon: float
A small constant for numerical stability.
amsgrad: bool
Whether to apply AMSGrad variant of this algorithm from the paper "On the Convergence
of Adam and beyond".
rectify: bool
Whether to enable rectification as in RectifiedAdam
sma_threshold. float
The threshold for simple mean average.
total_steps: int
Total number of training steps. Enable warmup by setting a positive value.
warmup_proportion: float
The proportion of increasing steps.
min_lr: float
Minimum learning rate after warmup.
name: str, optional
Name for the operations created when applying gradients. Default: ``"AdaBeliefOptimizer"``.
**kwargs: dict
Standard Keras Optimizer keyword arguments. Allowed to be (`weight_decay`, `clipnorm`,
`clipvalue`, `global_clipnorm`, `use_ema`, `ema_momentum`, `ema_overwrite_frequency`,
`loss_scale_factor`, `gradient_accumulation_steps`)
Examples
--------
>>> from optimizers import AdaBelief
>>> opt = AdaBelief(lr=1e-3)
Example of serialization:
>>> optimizer = AdaBelief(learning_rate=lr_scheduler, weight_decay=wd_scheduler)
>>> config = keras.optimizers.serialize(optimizer)
>>> new_optimizer = keras.optimizers.deserialize(config,
... custom_objects=dict(AdaBelief=AdaBelief))
Example of warm up:
>>> opt = AdaBelief(lr=1e-3, total_steps=10000, warmup_proportion=0.1, min_lr=1e-5)
In the above example, the learning rate will increase linearly from 0 to `lr` in 1000 steps,
then decrease linearly from `lr` to `min_lr` in 9000 steps.
Example of enabling Lookahead:
>>> adabelief = AdaBelief()
>>> ranger = tfa.optimizers.Lookahead(adabelief, sync_period=6, slow_step_size=0.5)
Notes
-----
`amsgrad` is not described in the original paper. Use it with caution.
References
----------
Juntang Zhuang et al. - AdaBelief Optimizer: Adapting stepsizes by the belief in observed
gradients - https://arxiv.org/abs/2010.07468.
Original implementation - https://github.com/juntang-zhuang/Adabelief-Optimizer
Michael R. Zhang et.al - Lookahead Optimizer: k steps forward, 1 step back -
https://arxiv.org/abs/1907.08610v1
Adapted from https://github.com/juntang-zhuang/Adabelief-Optimizer
BSD 2-Clause License
Copyright (c) 2021, Juntang Zhuang
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
def __init__(self, # pylint:disable=too-many-arguments,too-many-positional-arguments
learning_rate: float = 0.001,
beta_1: float = 0.9,
beta_2: float = 0.999,
epsilon: float = 1e-14,
amsgrad: bool = False,
rectify: bool = True,
sma_threshold: float = 5.0,
total_steps: int = 0,
warmup_proportion: float = 0.1,
min_learning_rate: float = 0.0,
name="AdaBeliefOptimizer",
**kwargs):
logger.debug(parse_class_init(locals()))
super().__init__(learning_rate=learning_rate, name=name, **kwargs)
self.beta_1 = beta_1
self.beta_2 = beta_2
self.epsilon = epsilon
self.amsgrad = amsgrad
self.rectify = rectify
self.sma_threshold = sma_threshold
# TODO change the following 2 to "warm_up_steps"
# TODO Make learning rate warm up a global option
# Or these params can be calculated from a user "warm_up_steps" parameter
self.total_steps = total_steps
self.warmup_proportion = warmup_proportion
self.min_learning_rate = min_learning_rate
logger.debug("Initialized %s", self.__class__.__name__)
self._momentums: list[Variable] = []
self._velocities: list[Variable] = []
self._velocity_hats: list[Variable] = [] # Amsgrad only
def build(self, variables: list[Variable]) -> None:
"""Initialize optimizer variables.
AdaBelief optimizer has 3 types of variables: momentums, velocities and
velocity_hat (only set when amsgrad is applied),
Parameters
----------
variables: list[:class:`keras.Variable`]
list of model variables to build AdaBelief variables on.
"""
if self.built:
return
logger.debug("Building AdaBelief. var_list: %s", variables)
super().build(variables)
for var in variables:
self._momentums.append(self.add_variable_from_reference(
reference_variable=var, name="momentum"))
self._velocities.append(self.add_variable_from_reference(
reference_variable=var, name="velocity"))
if self.amsgrad:
self._velocity_hats.append(self.add_variable_from_reference(
reference_variable=var, name="velocity_hat"))
logger.debug("Built AdaBelief. momentums: %s, velocities: %s, velocity_hats: %s",
len(self._momentums), len(self._velocities), len(self._velocity_hats))
def _maybe_warmup(self, learning_rate: KerasTensor, local_step: KerasTensor) -> KerasTensor:
""" Do learning rate warm up if requested
Parameters
----------
learning_rate: :class:`keras.KerasTensor`
The learning rate
local_step: :class:`keras.KerasTensor`
The current training step
Returns
-------
:class:`keras.KerasTensor`
Either the original learning rate or adjusted learning rate if warmup is requested
"""
if self.total_steps <= 0:
return learning_rate
total_steps = ops.cast(self.total_steps, learning_rate.dtype)
warmup_steps = total_steps * ops.cast(self.warmup_proportion, learning_rate.dtype)
min_lr = ops.cast(self.min_learning_rate, learning_rate.dtype)
decay_steps = ops.maximum(total_steps - warmup_steps, 1)
decay_rate = ops.divide(min_lr - learning_rate, decay_steps)
return ops.where(local_step <= warmup_steps,
ops.multiply(learning_rate, (ops.divide(local_step, warmup_steps))),
ops.multiply(learning_rate + decay_rate,
ops.minimum(local_step - warmup_steps, decay_steps)))
def _maybe_rectify(self,
momentum: KerasTensor,
velocity: KerasTensor,
local_step: KerasTensor,
beta_2_power: KerasTensor) -> KerasTensor:
""" Apply rectification, if requested
Parameters
----------
momentum: :class:`keras.KerasTensor`
The momentum update
velocity: :class:`keras.KerasTensor`
The velocity update
local_step: :class:`keras.KerasTensor`
The current training step
beta_2_power
Adjusted exponential decay rate for the 2nd moment estimates.
Returns
-------
:class:`keras.KerasTensor`
The standard or rectified update (if rectification enabled)
"""
if not self.rectify:
return ops.divide(momentum, ops.add(velocity, self.epsilon))
sma_inf = 2 / (1 - self.beta_2) - 1
sma_t = sma_inf - 2 * local_step * beta_2_power / (1 - beta_2_power)
rect = ops.sqrt((sma_t - 4) / (sma_inf - 4) *
(sma_t - 2) / (sma_inf - 2) *
sma_inf / sma_t)
return ops.where(sma_t >= self.sma_threshold,
ops.divide(
ops.multiply(rect, momentum),
(ops.add(velocity, self.epsilon))),
momentum)
def update_step(self,
gradient: KerasTensor,
variable: Variable,
learning_rate: Variable) -> None:
"""Update step given gradient and the associated model variable for AdaBelief.
Parameters
----------
gradient :class:`keras.KerasTensor`
The gradient to update
variable: :class:`keras.Variable`
The variable to update
learning_rate: :class:`keras.Variable`
The learning rate
"""
local_step = ops.cast(self.iterations + 1, variable.dtype)
learning_rate = self._maybe_warmup(ops.cast(learning_rate, variable.dtype), local_step)
gradient = ops.cast(gradient, variable.dtype)
beta_1_power = ops.power(ops.cast(self.beta_1, variable.dtype), local_step)
beta_2_power = ops.power(ops.cast(self.beta_2, variable.dtype), local_step)
# m_t = b1 * m + (1 - b1) * g
# => m_t = m + (g - m) * (1 - b1)
momentum = self._momentums[self._get_variable_index(variable)]
self.assign_add(momentum, ops.multiply(ops.subtract(gradient, momentum), 1 - self.beta_1))
momentum_corr = ops.divide(momentum, (1 - beta_1_power))
# v_t = b2 * v + (1 - b2) * (g - m_t)^2 + e
# => v_t = v + ((g - m_t)^2 - v) * (1 - b2) + e
velocity = self._velocities[self._get_variable_index(variable)]
self.assign_add(velocity,
ops.multiply(
ops.subtract(ops.square(gradient - momentum), velocity),
1 - self.beta_2)
+ self.epsilon)
if self.amsgrad:
velocity_hat = self._velocity_hats[self._get_variable_index(variable)]
self.assign(velocity_hat, ops.maximum(velocity, velocity_hat))
velocity_corr = ops.sqrt(ops.divide(velocity_hat, (1 - beta_2_power)))
else:
velocity_corr = ops.sqrt(ops.divide(velocity, (1 - beta_2_power)))
var_t = self._maybe_rectify(momentum_corr, velocity_corr, local_step, beta_2_power)
self.assign_sub(variable, ops.multiply(learning_rate, var_t))
def get_config(self) -> dict[str, T.Any]:
""" Returns the config of the optimizer.
Optimizer configuration for AdaBelief.
Returns
-------
dict[str, Any]
The optimizer configuration.
"""
config = super().get_config()
config.update({"beta_1": self.beta_1,
"beta_2": self.beta_2,
"epsilon": self.epsilon,
"amsgrad": self.amsgrad,
"rectify": self.rectify,
"sma_threshold": self.sma_threshold,
"total_steps": self.total_steps,
"warmup_proportion": self.warmup_proportion,
"min_learning_rate": self.min_learning_rate})
return config
# Update Optimizers into Keras custom objects
for _name, obj in inspect.getmembers(sys.modules[__name__]):
if inspect.isclass(obj) and obj.__module__ == __name__:
saving.get_custom_objects().update({_name: obj})
__all__ = get_module_objects(__name__)