Source code for decent_bench.algorithms.p2p._nids
from dataclasses import dataclass
import decent_bench.utils.interoperability as iop
from decent_bench.algorithms.utils import initial_states
from decent_bench.networks import P2PNetwork
from decent_bench.utils._tags import tags
from decent_bench.utils.types import InitialStates
from ._p2p_algorithm import P2PAlgorithm
[docs]
@tags("peer-to-peer", "gradient-tracking")
@dataclass(eq=False)
class NIDS(P2PAlgorithm):
r"""
NIDS :footcite:p:`Alg_NIDS` gradient tracking algorithm characterized by the update steps below.
.. math::
\mathbf{x}_{i, k+1}
= \sum_j \tilde{\mathbf{W}}_{ij} (2 x_{j,k} - x_{j, k-1}
- \rho \nabla f_j(\mathbf{x}_{j,k}) + \rho \nabla f_j(\mathbf{x}_{j,k-1}))
where
:math:`\mathbf{x}_{i, k}` is agent i's local optimization variable at iteration k,
:math:`\rho` is the step size,
:math:`f_i` is agent i's local cost function,
j is a neighbor of i or i itself,
and :math:`\tilde{\mathbf{W}} = (\mathbf{I} + \mathbf{W}) / 2`
with :math:`\mathbf{W}` are the Metropolis weights.
This is a simplified version of the algorithm proposed in :footcite:p:`Alg_NIDS` (see eq. (9) therein).
.. footbibliography::
"""
iterations: int = 100
step_size: float = 0.001
x0: InitialStates = None
name: str = "NIDS"
def __post_init__(self) -> None:
"""
Validate hyperparameters.
Raises:
ValueError: if hyperparameters are invalid.
"""
if self.step_size <= 0:
raise ValueError("`step_size` must be positive")
def initialize(self, network: P2PNetwork) -> None:
self.x0 = initial_states(self.x0, network)
for i in network.agents():
z = iop.zeros_like(self.x0[i])
i.initialize(x=self.x0[i], aux_vars={"x_old": self.x0[i], "g": z, "g_old": z, "y": z})
W = network.weights # noqa: N806
W_tilde = 0.5 * (iop.eye_like(W) + W) # noqa: N806
self.W_tilde = W_tilde
def step(self, network: P2PNetwork, iteration: int) -> None:
if iteration == 0:
# first iteration (iteration k=1)
for i in network.active_agents():
i.aux_vars["g"] = i.cost.gradient(i.x) # store grad f_i(x_0)
i.x = i.aux_vars["x_old"] - self.step_size * i.aux_vars["g"]
else:
# subsequent iterations (k >= 2)
for i in network.active_agents():
i.aux_vars["g_old"] = i.aux_vars["g"] # store grad f_i(x_{k-1})
i.aux_vars["g"] = i.cost.gradient(i.x) # store grad f_i(x_k)
i.aux_vars["y"] = (
2 * i.x
- i.aux_vars["x_old"]
- self.step_size * i.aux_vars["g"]
+ self.step_size * i.aux_vars["g_old"]
)
for i in network.active_agents():
network.broadcast(i, i.aux_vars["y"])
for i in network.active_agents():
neighborhood_avg = self.W_tilde[i, i] * i.aux_vars["y"]
for j, y_j in i.messages().items():
neighborhood_avg += self.W_tilde[i, j] * y_j
i.aux_vars["x_old"] = i.x # store x_k
i.x = neighborhood_avg # update x_{k+1}