Repository: facebookresearch/nevergrad

Issue:
PSO has unusually high population std
Consider the following code: 

```
import nevergrad as ng
import numpy as np
def square(x, y=12):
    return sum((x - .5)**2) + abs(y)
popsize = 100
episodes = 500
budget = popsize * episodes
op = ng.optimizers.registry['PSO'](
      instrumentation = 200,
      num_workers = popsize,
      budget = budget)
for ep in range(budget // popsize):
  solutions = []
  candidates = []
  for i in range(popsize):
    candidate = op.ask()
    candidates.append(candidate)
    solutions.append(candidate.args[0])
  solutions = np.asarray(solutions)
  candidates = np.asarray(candidates)
  fitness = []
  for i in range(popsize):
    value = square(solutions[i])
    fitness.append(value)
    op.tell(candidates[i], value)
  if ep % 10 == 0:
    print(np.std(solutions))
recommendation = op.provide_recommendation()
```
The standard deviation of the population here is unusually high. For other optimizers (e.g. DE, TBPSA, etc) the std seems normal. Is this behaviour expected?





Discussion:
Thanks for raising the issue, I'm not sure if it's a bug or a feature :D

@teytaud this come from the initialization:
https://github.com/facebookresearch/nevergrad/blob/306df3810d2bca688ab18f5bf996de256a0fee42/nevergrad/optimization/optimizerlib.py#L601-L604
`PSO` is implemented on `[0, 1]` and the points are transformed to `R` when needed, hence since the initialization is uniform on `[0, 1]`, they span all R and are definitely not Gaussian. This is due to me changing the transform some time ago, I did not consider this effect.

Options:
 1. sample with a Gaussian and transform to `[0, 1]` (for both position and speed?)
 2. can it work on `R` natively?

Looking at other implementations (e.g. [pyswarms](https://github.com/ljvmiranda921/pyswarms)), I reckon it might be better to initialize with something of the following flavour: `np.random.uniform(-1, 1, dimension)`. This might be scaled up or down with another parameter: `sigma * np.random.uniform(-1, 1, dimension)`. Considering that PSO usually has premature convergence it might be useful to be using a uniform distribution rather than a gaussian one. 

My guess is that the results of robustness on deceptive functions will still hold.  

Resolving patch:
diff --git a/nevergrad/optimization/optimizerlib.py b/nevergrad/optimization/optimizerlib.py
--- a/nevergrad/optimization/optimizerlib.py
+++ b/nevergrad/optimization/optimizerlib.py
@@ -3,7 +3,7 @@
 # This source code is licensed under the MIT license found in the
 # LICENSE file in the root directory of this source tree.
 import typing as tp  # from now on, favor using tp.Dict etc instead of Dict
-from typing import Optional, List, Dict, Tuple, Deque, Union, Callable, Any, Sequence, Type
+from typing import Optional, List, Dict, Tuple, Callable, Any
 from collections import defaultdict, deque
 import warnings
 import cma
@@ -136,7 +136,7 @@ class ParametrizedOnePlusOne(base.ParametrizedFamily):
     _optimizer_class = _OnePlusOne
 
     def __init__(
-        self, *, noise_handling: Optional[Union[str, Tuple[str, float]]] = None, mutation: str = "gaussian", crossover: bool = False
+        self, *, noise_handling: tp.Optional[tp.Union[str, tp.Tuple[str, float]]] = None, mutation: str = "gaussian", crossover: bool = False
     ) -> None:
         if noise_handling is not None:
             if isinstance(noise_handling, str):
@@ -201,9 +201,9 @@ def __init__(self, instrumentation: IntOrParameter, budget: Optional[int] = None
         self._parameters = ParametrizedCMA()
         self._es: Optional[cma.CMAEvolutionStrategy] = None
         # delay initialization to ease implementation of variants
-        self.listx: List[ArrayLike] = []
-        self.listy: List[float] = []
-        self.to_be_asked: Deque[np.ndarray] = deque()
+        self.listx: tp.List[ArrayLike] = []
+        self.listy: tp.List[float] = []
+        self.to_be_asked: tp.Deque[np.ndarray] = deque()
 
     @property
     def es(self) -> cma.CMAEvolutionStrategy:
@@ -567,7 +567,7 @@ def _internal_ask(self) -> ArrayLike:
 
 
 class PSOParticle(utils.Individual):
-    """Particle for the PSO algorithm, holding relevant information
+    """Particle for the PSO algorithm, holding relevant information (in [0,1] box)
     """
 
     transform = transforms.ArctanBound(0, 1).reverted()
@@ -591,13 +591,14 @@ def __init__(
         self.random_state = random_state
 
     @classmethod
-    def random_initialization(cls, dimension: int, random_state: np.random.RandomState) -> "PSOParticle":
-        position = random_state.uniform(0.0, 1.0, dimension)
-        speed = random_state.uniform(-1.0, 1.0, dimension)
+    def from_data(cls, data: np.ndarray, random_state: np.random.RandomState) -> "PSOParticle":
+        position = cls.transform.backward(data)
+        speed = random_state.uniform(-1.0, 1.0, data.size)
         return cls(position, None, speed, position, float("inf"), random_state=random_state)
 
     def __repr__(self) -> str:
-        return f"{self.__class__.__name__}<position: {self.get_transformed_position()}, fitness: {self.value}, best: {self.best_value}>"
+        # return f"{self.__class__.__name__}<position: {self.get_transformed_position()}, fitness: {self.value}, best: {self.best_value}>"
+        return f"{self.__class__.__name__}<position: {self.x}, {self.get_transformed_position()}>"
 
     def mutate(self, best_x: np.ndarray, omega: float, phip: float, phig: float) -> None:
         dim = len(best_x)
@@ -621,6 +622,7 @@ class PSO(base.Optimizer):
 
     def __init__(self, instrumentation: IntOrParameter, budget: Optional[int] = None, num_workers: int = 1) -> None:
         super().__init__(instrumentation, budget=budget, num_workers=num_workers)
+        self._wide = False
         if budget is not None and budget < 60:
             warnings.warn("PSO is inefficient with budget < 60", base.InefficientSettingsWarning)
         self.llambda = max(40, num_workers)
@@ -634,11 +636,14 @@ def __init__(self, instrumentation: IntOrParameter, budget: Optional[int] = None
     def _internal_ask_candidate(self) -> base.Candidate:
         # population is increased only if queue is empty (otherwise tell_not_asked does not work well at the beginning)
         if self.population.is_queue_empty() and len(self.population) < self.llambda:
-            additional = [
-                self._PARTICULE.random_initialization(self.dimension, random_state=self._rng)
-                for _ in range(self.llambda - len(self.population))
-            ]
-            self.population.extend(additional)
+            param = self.instrumentation
+            for _ in range(self.llambda - len(self.population)):
+                if self._wide:
+                    # old initialization below seeds in the while R space, while other algorithms use normal distrib
+                    data = self._PARTICULE.transform.forward(self._rng.uniform(0, 1, self.dimension))
+                else:
+                    data = param.sample().get_standardized_data(reference=param)
+                self.population.extend([self._PARTICULE.from_data(data, random_state=self._rng)])
         particle = self.population.get_queued(remove=False)
         if particle.value is not None:  # particle was already initialized
             particle.mutate(best_x=self.best_x, omega=self.omega, phip=self.phip, phig=self.phig)
@@ -671,15 +676,13 @@ def _internal_tell_candidate(self, candidate: base.Candidate, value: float) -> N
     def _internal_tell_not_asked(self, candidate: base.Candidate, value: float) -> None:
         x = candidate.data
         if len(self.population) < self.llambda:
-            particle = self._PARTICULE.random_initialization(self.dimension, random_state=self._rng)
-            particle.x = self._PARTICULE.transform.backward(x)
+            particle = self._PARTICULE.from_data(x, random_state=self._rng)
             self.population.extend([particle])
         else:
             worst_part = max(iter(self.population), key=lambda p: p.best_value)  # or fitness?
             if worst_part.best_value < value:
                 return  # no need to update
-            particle = self._PARTICULE.random_initialization(self.dimension, random_state=self._rng)
-            particle.x = self._PARTICULE.transform.backward(x)
+            particle = self._PARTICULE.from_data(x, random_state=self._rng)
             worst_part._active = False
             self.population.replace(worst_part, particle)
         # go through standard pipeline
@@ -687,6 +690,17 @@ def _internal_tell_not_asked(self, candidate: base.Candidate, value: float) -> N
         self._internal_tell_candidate(c2, value)
 
 
+@registry.register
+class WidePSO(PSO):
+    """Partially following SPSO2011. However, no randomization of the population order.
+    This version uses a non-standard initialization with high standard deviation.
+    """
+
+    def __init__(self, instrumentation: IntOrParameter, budget: Optional[int] = None, num_workers: int = 1) -> None:
+        super().__init__(instrumentation, budget=budget, num_workers=num_workers)
+        self._wide = True
+
+
 @registry.register
 class SPSA(base.Optimizer):
     # pylint: disable=too-many-instance-attributes
@@ -1369,8 +1383,8 @@ class Chaining(base.ParametrizedFamily):
     """
     _optimizer_class = _Chain
 
-    def __init__(self, optimizers: Sequence[Union[base.OptimizerFamily, Type[base.Optimizer]]],
-                 budgets: Sequence[Union[str, int]]) -> None:
+    def __init__(self, optimizers: tp.Sequence[tp.Union[base.OptimizerFamily, tp.Type[base.Optimizer]]],
+                 budgets: tp.Sequence[tp.Union[str, int]]) -> None:
         # Either we have the budget for each algorithm, or the last algorithm uses the rest of the budget, so:
         self.budgets = tuple(budgets)
         self.optimizers = tuple(optimizers)
@@ -1494,7 +1508,7 @@ class NGO(base.Optimizer):
     """Nevergrad optimizer by competence map."""
     one_shot = True
 
-    def __init__(self, instrumentation: Union[int, Instrumentation], budget: Optional[int] = None, num_workers: int = 1) -> None:
+    def __init__(self, instrumentation: IntOrParameter, budget: Optional[int] = None, num_workers: int = 1) -> None:
         super().__init__(instrumentation, budget=budget, num_workers=num_workers)
         assert budget is not None
         self.who_asked: Dict[Tuple[float, ...], List[int]] = defaultdict(list)


Test patch:
diff --git a/nevergrad/optimization/test_optimizerlib.py b/nevergrad/optimization/test_optimizerlib.py
--- a/nevergrad/optimization/test_optimizerlib.py
+++ b/nevergrad/optimization/test_optimizerlib.py
@@ -169,7 +169,7 @@ def test_optimizers_recommendation(with_parameter: bool,
         np.random.seed(12)
         random.seed(12)  # may depend on non numpy generator
     # budget=6 by default, larger for special cases needing more
-    budget = {"PSO": 100, "MEDA": 100, "EDA": 100, "MPCEDA": 100, "TBPSA": 100}.get(name, 6)
+    budget = {"WidePSO": 100, "PSO": 100, "MEDA": 100, "EDA": 100, "MPCEDA": 100, "TBPSA": 100}.get(name, 6)
     if isinstance(optimizer_cls, optlib.DifferentialEvolution):
         budget = 80
     dimension = min(16, max(4, int(np.sqrt(budget))))