Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Random topology class #155

Merged
merged 2 commits into from
Jul 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/api/pyswarms.topology.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ pyswarms.backend.topology.pyramid module
:undoc-members:
:show-inheritance:
:special-members: __init__

pyswarms.backend.topology.random module
--------------------------------------

.. automodule:: pyswarms.backend.topology.random
:members:
:undoc-members:
:show-inheritance:
:special-members: __init__
3 changes: 2 additions & 1 deletion pyswarms/backend/topology/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .star import Star
from .ring import Ring
from .pyramid import Pyramid
from .random import Random


__all__ = ["Topology", "Star", "Ring", "Pyramid"]
__all__ = ["Topology", "Star", "Ring", "Pyramid", "Random"]
2 changes: 1 addition & 1 deletion pyswarms/backend/topology/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def compute_gbest(self, swarm):
# Insert all the neighbors for each particle in the idx array
idx = np.array([index_pointer[indices[i]:indices[i+1]] for i in range(swarm.n_particles)])
idx_min = np.array([swarm.pbest_cost[idx[i]].argmin() for i in range(idx.size)])
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(idx.size)]).astype(int)
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(len(idx))]).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
Expand Down
201 changes: 201 additions & 0 deletions pyswarms/backend/topology/random.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-

"""
A Random Network Topology

This class implements a random topology. All particles are connected in a random fashion.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we add a short explanation on why dijkstra is used and how it relates to the Random topology. 😄

"""

# Import from stdlib
import logging
import itertools

# Import modules
import numpy as np
from scipy.sparse.csgraph import connected_components, dijkstra

# Import from package
from ..import operators as ops
from .base import Topology

# Create a logger
logger = logging.getLogger(__name__)


class Random(Topology):
def __init__(self):
super(Random, self).__init__()

def compute_gbest(self, swarm, k):
"""Update the global best using a random neighborhood approach

This uses random class from :code:`numpy` to give every particle k
randomly distributed, non-equal neighbors. The resulting topology
is a connected graph. The algorithm to obtain the neighbors was adapted
from [TSWJ2013].

[TSWJ2013] Qingjian Ni and Jianming Deng, “A New Logistic Dynamic
Particle Swarm Optimization Algorithm Based on Random Topology,”
The Scientific World Journal, vol. 2013, Article ID 409167, 8 pages, 2013.
https://doi.org/10.1155/2013/409167.

Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles-1`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit nitpicky but an empty line before Returns will make everything consistent 👍


Returns
-------
numpy.ndarray
Best position of shape :code:`(n_dimensions, )`
float
Best cost
"""
try:
adj_matrix = self.__compute_neighbors(swarm, k)
idx = np.array([adj_matrix[i].nonzero()[0] for i in range(swarm.n_particles)])
idx_min = np.array([swarm.pbest_cost[idx[i]].argmin() for i in range(len(idx))])
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(len(idx))]).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
best_pos = swarm.pbest_pos[
np.argmin(swarm.pbest_cost[best_neighbor])
]

except AttributeError:
msg = "Please pass a Swarm class. You passed {}".format(
type(swarm)
)
logger.error(msg)
raise
else:
return (best_pos, best_cost)

def compute_velocity(self, swarm, clamp=None):
"""Compute the velocity matrix

This method updates the velocity matrix using the best and current
positions of the swarm. The velocity matrix is computed using the
cognitive and social terms of the swarm.

A sample usage can be seen with the following:

.. code-block :: python

import pyswarms.backend as P
from pyswarms.swarms.backend import Swarm
from pyswarms.backend.topology import Random

my_swarm = P.create_swarm(n_particles, dimensions)
my_topology = Random()

for i in range(iters):
# Inside the for-loop
my_swarm.velocity = my_topology.update_velocity(my_swarm, clamp)

Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
clamp : tuple of floats (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum velocity
and the second entry is the maximum velocity. It
sets the limits for velocity clamping.

Returns
-------
numpy.ndarray
Updated velocity matrix
"""
return ops.compute_velocity(swarm, clamp)

def compute_position(self, swarm, bounds=None):
"""Update the position matrix

This method updates the position matrix given the current position and
the velocity. If bounded, it waives updating the position.

Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
bounds : tuple of :code:`np.ndarray` or list (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum bound while
the second entry is the maximum bound. Each array must be of shape
:code:`(dimensions,)`.

Returns
-------
numpy.ndarray
New position-matrix
"""
return ops.compute_position(swarm, bounds)

def __compute_neighbors(self, swarm, k):
"""Helper method to compute the adjacency matrix of the topology

This method computes the adjacency matrix of the topology using
the randomized algorithm proposed in [TSWJ2013]. The resulting
topology is a connected graph. This is achieved by creating three
matrices:

* adj_matrix : The adjacency matrix of the generated graph.
It's initialized as an identity matrix to
make sure that every particle has itself as
a neighbour. This matrix is the return
value of the method.
* neighbor_matrix : The matrix of randomly generated neighbors.
This matrix is a matrix of shape
:code:`(swarm.n_particles, k)`:
with randomly generated elements. It's used
to create connections in the adj_matrix.
* dist_matrix : The distance matrix computed with Dijkstra's
algorithm. It is used to determine where the
graph needs edges to change it to a connected
graph.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! This is good @whzup !


.. note:: If the graph isn't connected, it is possible that the
PSO algorithm does not find the best position within
the swarm.

Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles-1`

Returns
-------
numpy.ndarray
Adjacency matrix of the topology
"""

adj_matrix = np.identity(swarm.n_particles, dtype=int)

neighbor_matrix = np.array(
[np.random.choice(
# Exclude i from the array
np.setdiff1d(
np.arange(swarm.n_particles), np.array([i])
), k, replace=False
) for i in range(swarm.n_particles)])

# Set random elements to one using the neighbor matrix
adj_matrix[np.arange(swarm.n_particles).reshape(swarm.n_particles, 1), neighbor_matrix] = 1
adj_matrix[neighbor_matrix, np.arange(swarm.n_particles).reshape(swarm.n_particles, 1)] = 1

dist_matrix = dijkstra(adj_matrix, directed=False, return_predecessors=False, unweighted=True)

# Generate connected graph.
while connected_components(adj_matrix, directed=False, return_labels=False) != 1:
for i, j in itertools.product(range(swarm.n_particles), repeat=2):
if dist_matrix[i][j] == 0:
adj_matrix[i][j] = 1

return adj_matrix
59 changes: 44 additions & 15 deletions pyswarms/single/general_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
# Import from package
from ..base import SwarmOptimizer
from ..backend.operators import compute_pbest
from ..backend.topology import Topology, Ring
from ..backend.topology import Topology, Ring, Random
from ..utils.console_utils import cli_print, end_report


Expand Down Expand Up @@ -98,18 +98,29 @@ def __init__(
social parameter
* w : float
inertia parameter
if used with the :code:`Ring` topology the additional
parameters k and p must be included
if used with the :code:`Ring` or :code:`Random` topology the additional
parameter k must be included
* k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles`
if used with the :code:`Ring` topology the additional
parameter p must be included
* p: int {1,2}
the Minkowski p-norm to use. 1 is the
sum-of-absolute values (or L1 distance) while 2 is
the Euclidean (or L2) distance.
topology : :code:`Topology` object
topology : pyswarms.backend.topology.Topology
a :code:`Topology` object that defines the topology to use
in the optimization process
in the optimization process. The currently available topologies
are:
* Star
All particles are connected
* Ring
Particles are connected with the k nearest neighbours
* Pyramid
Particles are connected in N-dimensional simplices
* Random
Particles are connected to k random particles
bounds : tuple of :code:`np.ndarray` (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum bound
while the second entry is the maximum bound. Each array must
Expand Down Expand Up @@ -149,22 +160,33 @@ def __init__(

# Case for the Ring topology
if isinstance(topology, Ring):
# Assign k-neighbors and p-value as attributes
self.k, self.p = options["k"], options["p"]

# Exceptions for the k and p values
if not all(key in self.options for key in ("k", "p")):
raise KeyError("Missing either k or p in options")
if not 0 <= self.k <= self.n_particles:
raise ValueError(
"No. of neighbors must be between 0 and no. " "of particles."
)
# Assign p-value as attributes
self.p = options["p"]
# Exceptions for the p value
if "p" not in self.options:
raise KeyError("Missing p in options")
if self.p not in [1, 2]:
raise ValueError(
"p-value should either be 1 (for L1/Minkowski) "
"or 2 (for L2/Euclidean)."
)

# Case for Random and Ring topologies
if isinstance(topology, (Random, Ring)):
# Assign k-neighbors as attribute
self.k = options["k"]
if not isinstance(self.k, int):
raise ValueError(
"No. of neighbors must be an integer between"
"0 and no. of particles."
)
if not 0 <= self.k <= self.n_particles-1:
raise ValueError(
"No. of neighbors must be between 0 and no. " "of particles."
)
if "k" not in self.options:
raise KeyError("Missing k in options")

def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
"""Optimizes the swarm for a number of iterations.

Expand Down Expand Up @@ -207,6 +229,13 @@ def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
self.swarm.best_pos, self.swarm.best_cost = self.top.compute_gbest(
self.swarm, self.p, self.k
)
# If the topology is a random topology pass the neighbor attribute to the compute_gbest() method
elif isinstance(self.top, Random):
# Get minima of pbest and check if it's less than gbest
if np.min(self.swarm.pbest_cost) < self.swarm.best_cost:
self.swarm.best_pos, self.swarm.best_cost = self.top.compute_gbest(
self.swarm, self.k
)
else:
# Get minima of pbest and check if it's less than gbest
if np.min(self.swarm.pbest_cost) < self.swarm.best_cost:
Expand Down
7 changes: 7 additions & 0 deletions tests/backend/topology/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ def swarm():
"options": {"c1": 0.5, "c2": 1, "w": 2},
}
return Swarm(**attrs_at_t)


@pytest.fixture
def k():
"""Default neighbor number"""
_k = 1
return _k
Loading