Edit on GitHub

geneticalgorithm2.selections

  1from typing import Callable, Dict, List
  2
  3import math
  4import random
  5import numpy as np
  6
  7from .utils.aliases import array1D, TypeAlias
  8
  9SelectionFunc: TypeAlias = Callable[[array1D, int], array1D]
 10"""
 11Function (scores, count to select) -> indexes of selected
 12"""
 13
 14
 15#region UTILS
 16
 17def inverse_scores(scores: array1D) -> array1D:
 18    """
 19    inverses scores (min values become to max)
 20    """
 21    minobj = scores[0]
 22    normobj = scores - minobj if minobj < 0 else scores
 23
 24    return (np.amax(normobj) + 1) - normobj
 25
 26
 27def roulette(scores: array1D, parents_count: int) -> array1D:
 28    """simplest roulette selector for which the highest score means more preferred"""
 29
 30    sum_normobj = np.sum(scores)
 31    prob = scores / sum_normobj
 32    cumprob = np.cumsum(prob)
 33
 34    parents_indexes = np.array(
 35        [
 36            index if index < cumprob.size else np.random.randint(0, index - 1)
 37            for index in (
 38                np.searchsorted(cumprob, np.random.random())
 39                for _ in range(parents_count)
 40            )
 41        ]
 42    )
 43
 44    return parents_indexes
 45
 46
 47#endregion
 48
 49class Selection:
 50    """
 51    Selections functions static class
 52    
 53    Selection function selects the population subset according to scores and its own rules
 54    """
 55
 56    @staticmethod
 57    def selections_dict() -> Dict[str, SelectionFunc]:
 58        return {
 59            n: getattr(Selection, n)()
 60            for n in (
 61                'fully_random',
 62                'roulette',
 63                'stochastic',
 64                'sigma_scaling',
 65                'ranking',
 66                'linear_ranking',
 67                'tournament',
 68            )
 69        }
 70
 71    @staticmethod
 72    def fully_random() -> SelectionFunc:
 73        """returns the selector of fully random parents (for tests purposes)"""
 74        
 75        def func(scores: array1D, parents_count: int):
 76            indexes = np.arange(parents_count)
 77            return np.random.choice(indexes, parents_count, replace=False)
 78        
 79        return func
 80
 81    @staticmethod
 82    def roulette() -> SelectionFunc:
 83        
 84        def func(scores: array1D, parents_count: int):
 85            return roulette(inverse_scores(scores), parents_count)
 86        
 87        return func
 88
 89    @staticmethod
 90    def stochastic() -> SelectionFunc:
 91        
 92        def func(scores: np.ndarray, parents_count: int):
 93            f = inverse_scores(scores)
 94            
 95            fN: float = 1.0 / parents_count
 96            k: int = 0
 97            acc: float = 0.0
 98            parents: List[int] = []
 99            r: float = random.random() * fN
100            
101            while len(parents) < parents_count:
102                
103                acc += f[k]
104                
105                while acc > r:
106                    parents.append(k)
107                    if len(parents) == parents_count: 
108                        break
109                    r += fN
110                
111                k += 1
112            
113            return np.array(parents[:parents_count])
114        
115        return func
116
117    @staticmethod
118    def sigma_scaling(epsilon: float = 0.01, is_noisy: bool = False) -> SelectionFunc:
119        
120        def func(scores: array1D, parents_count):
121            f = inverse_scores(scores)
122            
123            sigma = np.std(f, ddof = 1) if is_noisy else np.std(f)
124            average = np.mean(f)
125            
126            if sigma == 0:
127                f = 1
128            else:
129                f = np.maximum(epsilon, 1 + (f - average)/(2*sigma))
130            
131            return roulette(f, parents_count)
132        
133        return func
134
135    @staticmethod
136    def ranking() -> SelectionFunc:
137        
138        def func(scores: array1D, parents_count: int):
139            return roulette(1 + np.arange(parents_count)[::-1], parents_count)
140        
141        return func
142
143    @staticmethod
144    def linear_ranking(selection_pressure: float = 1.5) -> SelectionFunc:
145        
146        assert 1 <= selection_pressure <= 2, f"selection_pressure should be in (1, 2), but got {selection_pressure}"
147        
148        def func(scores: array1D, parents_count: int):
149            tmp = parents_count * (parents_count-1)
150            alpha = (2 * parents_count - selection_pressure * (parents_count + 1)) / tmp
151            beta = 2 * (selection_pressure - 1) / tmp
152
153            a = -2 * alpha - beta
154            b = (2 * alpha + beta) ** 2
155            c = 8 * beta
156            d = 2 * beta
157            
158            indexes = np.arange(parents_count)
159            
160            return np.array(
161                [
162                    indexes[-round((a + math.sqrt(b + c * random.random())) / d)]
163                    for _ in range(parents_count)
164                ]
165            )
166
167        return func
168
169    @staticmethod
170    def tournament(tau: int = 2) -> SelectionFunc:
171
172        # NOTE
173        # this code really does tournament selection
174        # because scores are always sorted
175        
176        def func(scores: array1D, parents_count: int):
177            
178            indexes = np.arange(parents_count)
179            
180            return np.array(
181                [
182                    np.min(np.random.choice(indexes, tau, replace=False))
183                    for _ in range(parents_count)
184                ]
185            )
186        
187        return func
188    
189    
190    
SelectionFunc: typing_extensions.TypeAlias = typing.Callable[[numpy.ndarray, int], numpy.ndarray]

Function (scores, count to select) -> indexes of selected

def inverse_scores(scores: numpy.ndarray) -> numpy.ndarray:
19def inverse_scores(scores: array1D) -> array1D:
20    """
21    inverses scores (min values become to max)
22    """
23    minobj = scores[0]
24    normobj = scores - minobj if minobj < 0 else scores
25
26    return (np.amax(normobj) + 1) - normobj

inverses scores (min values become to max)

def roulette(scores: numpy.ndarray, parents_count: int) -> numpy.ndarray:
29def roulette(scores: array1D, parents_count: int) -> array1D:
30    """simplest roulette selector for which the highest score means more preferred"""
31
32    sum_normobj = np.sum(scores)
33    prob = scores / sum_normobj
34    cumprob = np.cumsum(prob)
35
36    parents_indexes = np.array(
37        [
38            index if index < cumprob.size else np.random.randint(0, index - 1)
39            for index in (
40                np.searchsorted(cumprob, np.random.random())
41                for _ in range(parents_count)
42            )
43        ]
44    )
45
46    return parents_indexes

simplest roulette selector for which the highest score means more preferred

class Selection:
 51class Selection:
 52    """
 53    Selections functions static class
 54    
 55    Selection function selects the population subset according to scores and its own rules
 56    """
 57
 58    @staticmethod
 59    def selections_dict() -> Dict[str, SelectionFunc]:
 60        return {
 61            n: getattr(Selection, n)()
 62            for n in (
 63                'fully_random',
 64                'roulette',
 65                'stochastic',
 66                'sigma_scaling',
 67                'ranking',
 68                'linear_ranking',
 69                'tournament',
 70            )
 71        }
 72
 73    @staticmethod
 74    def fully_random() -> SelectionFunc:
 75        """returns the selector of fully random parents (for tests purposes)"""
 76        
 77        def func(scores: array1D, parents_count: int):
 78            indexes = np.arange(parents_count)
 79            return np.random.choice(indexes, parents_count, replace=False)
 80        
 81        return func
 82
 83    @staticmethod
 84    def roulette() -> SelectionFunc:
 85        
 86        def func(scores: array1D, parents_count: int):
 87            return roulette(inverse_scores(scores), parents_count)
 88        
 89        return func
 90
 91    @staticmethod
 92    def stochastic() -> SelectionFunc:
 93        
 94        def func(scores: np.ndarray, parents_count: int):
 95            f = inverse_scores(scores)
 96            
 97            fN: float = 1.0 / parents_count
 98            k: int = 0
 99            acc: float = 0.0
100            parents: List[int] = []
101            r: float = random.random() * fN
102            
103            while len(parents) < parents_count:
104                
105                acc += f[k]
106                
107                while acc > r:
108                    parents.append(k)
109                    if len(parents) == parents_count: 
110                        break
111                    r += fN
112                
113                k += 1
114            
115            return np.array(parents[:parents_count])
116        
117        return func
118
119    @staticmethod
120    def sigma_scaling(epsilon: float = 0.01, is_noisy: bool = False) -> SelectionFunc:
121        
122        def func(scores: array1D, parents_count):
123            f = inverse_scores(scores)
124            
125            sigma = np.std(f, ddof = 1) if is_noisy else np.std(f)
126            average = np.mean(f)
127            
128            if sigma == 0:
129                f = 1
130            else:
131                f = np.maximum(epsilon, 1 + (f - average)/(2*sigma))
132            
133            return roulette(f, parents_count)
134        
135        return func
136
137    @staticmethod
138    def ranking() -> SelectionFunc:
139        
140        def func(scores: array1D, parents_count: int):
141            return roulette(1 + np.arange(parents_count)[::-1], parents_count)
142        
143        return func
144
145    @staticmethod
146    def linear_ranking(selection_pressure: float = 1.5) -> SelectionFunc:
147        
148        assert 1 <= selection_pressure <= 2, f"selection_pressure should be in (1, 2), but got {selection_pressure}"
149        
150        def func(scores: array1D, parents_count: int):
151            tmp = parents_count * (parents_count-1)
152            alpha = (2 * parents_count - selection_pressure * (parents_count + 1)) / tmp
153            beta = 2 * (selection_pressure - 1) / tmp
154
155            a = -2 * alpha - beta
156            b = (2 * alpha + beta) ** 2
157            c = 8 * beta
158            d = 2 * beta
159            
160            indexes = np.arange(parents_count)
161            
162            return np.array(
163                [
164                    indexes[-round((a + math.sqrt(b + c * random.random())) / d)]
165                    for _ in range(parents_count)
166                ]
167            )
168
169        return func
170
171    @staticmethod
172    def tournament(tau: int = 2) -> SelectionFunc:
173
174        # NOTE
175        # this code really does tournament selection
176        # because scores are always sorted
177        
178        def func(scores: array1D, parents_count: int):
179            
180            indexes = np.arange(parents_count)
181            
182            return np.array(
183                [
184                    np.min(np.random.choice(indexes, tau, replace=False))
185                    for _ in range(parents_count)
186                ]
187            )
188        
189        return func

Selections functions static class

Selection function selects the population subset according to scores and its own rules

@staticmethod
def selections_dict() -> Dict[str, Callable[[numpy.ndarray, int], numpy.ndarray]]:
58    @staticmethod
59    def selections_dict() -> Dict[str, SelectionFunc]:
60        return {
61            n: getattr(Selection, n)()
62            for n in (
63                'fully_random',
64                'roulette',
65                'stochastic',
66                'sigma_scaling',
67                'ranking',
68                'linear_ranking',
69                'tournament',
70            )
71        }
@staticmethod
def fully_random() -> Callable[[numpy.ndarray, int], numpy.ndarray]:
73    @staticmethod
74    def fully_random() -> SelectionFunc:
75        """returns the selector of fully random parents (for tests purposes)"""
76        
77        def func(scores: array1D, parents_count: int):
78            indexes = np.arange(parents_count)
79            return np.random.choice(indexes, parents_count, replace=False)
80        
81        return func

returns the selector of fully random parents (for tests purposes)

@staticmethod
def roulette() -> Callable[[numpy.ndarray, int], numpy.ndarray]:
83    @staticmethod
84    def roulette() -> SelectionFunc:
85        
86        def func(scores: array1D, parents_count: int):
87            return roulette(inverse_scores(scores), parents_count)
88        
89        return func
@staticmethod
def stochastic() -> Callable[[numpy.ndarray, int], numpy.ndarray]:
 91    @staticmethod
 92    def stochastic() -> SelectionFunc:
 93        
 94        def func(scores: np.ndarray, parents_count: int):
 95            f = inverse_scores(scores)
 96            
 97            fN: float = 1.0 / parents_count
 98            k: int = 0
 99            acc: float = 0.0
100            parents: List[int] = []
101            r: float = random.random() * fN
102            
103            while len(parents) < parents_count:
104                
105                acc += f[k]
106                
107                while acc > r:
108                    parents.append(k)
109                    if len(parents) == parents_count: 
110                        break
111                    r += fN
112                
113                k += 1
114            
115            return np.array(parents[:parents_count])
116        
117        return func
@staticmethod
def sigma_scaling( epsilon: float = 0.01, is_noisy: bool = False) -> Callable[[numpy.ndarray, int], numpy.ndarray]:
119    @staticmethod
120    def sigma_scaling(epsilon: float = 0.01, is_noisy: bool = False) -> SelectionFunc:
121        
122        def func(scores: array1D, parents_count):
123            f = inverse_scores(scores)
124            
125            sigma = np.std(f, ddof = 1) if is_noisy else np.std(f)
126            average = np.mean(f)
127            
128            if sigma == 0:
129                f = 1
130            else:
131                f = np.maximum(epsilon, 1 + (f - average)/(2*sigma))
132            
133            return roulette(f, parents_count)
134        
135        return func
@staticmethod
def ranking() -> Callable[[numpy.ndarray, int], numpy.ndarray]:
137    @staticmethod
138    def ranking() -> SelectionFunc:
139        
140        def func(scores: array1D, parents_count: int):
141            return roulette(1 + np.arange(parents_count)[::-1], parents_count)
142        
143        return func
@staticmethod
def linear_ranking( selection_pressure: float = 1.5) -> Callable[[numpy.ndarray, int], numpy.ndarray]:
145    @staticmethod
146    def linear_ranking(selection_pressure: float = 1.5) -> SelectionFunc:
147        
148        assert 1 <= selection_pressure <= 2, f"selection_pressure should be in (1, 2), but got {selection_pressure}"
149        
150        def func(scores: array1D, parents_count: int):
151            tmp = parents_count * (parents_count-1)
152            alpha = (2 * parents_count - selection_pressure * (parents_count + 1)) / tmp
153            beta = 2 * (selection_pressure - 1) / tmp
154
155            a = -2 * alpha - beta
156            b = (2 * alpha + beta) ** 2
157            c = 8 * beta
158            d = 2 * beta
159            
160            indexes = np.arange(parents_count)
161            
162            return np.array(
163                [
164                    indexes[-round((a + math.sqrt(b + c * random.random())) / d)]
165                    for _ in range(parents_count)
166                ]
167            )
168
169        return func
@staticmethod
def tournament(tau: int = 2) -> Callable[[numpy.ndarray, int], numpy.ndarray]:
171    @staticmethod
172    def tournament(tau: int = 2) -> SelectionFunc:
173
174        # NOTE
175        # this code really does tournament selection
176        # because scores are always sorted
177        
178        def func(scores: array1D, parents_count: int):
179            
180            indexes = np.arange(parents_count)
181            
182            return np.array(
183                [
184                    np.min(np.random.choice(indexes, tau, replace=False))
185                    for _ in range(parents_count)
186                ]
187            )
188        
189        return func