import itertools as it import random import benchmarks.common as common import scipy.stats class ProgressiveGAGenerator: """Generate a sequence progressively according to a predefined TL ratio and an even distribution""" def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): """Initialize the genetic algorithm optimizer for n-back sequences. :param choices: :param trials: :param tl: :param pool_size: :param n: """ self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n self.pool = [] self.tl_norm = scipy.stats.norm(self.tl, 0.5) self.skewness_norm = scipy.stats.norm(0, 0.5) self.__init_pool(pool_size) def generate(self): """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be close to the desired ones but not exactly the same. :return: a sequence of items in "list" format. """ generation = 0 best_parent = self.__find_best_parents(1)[0] while self.cost(best_parent) > 0.1 and generation < 1000: generation += 1 if random.random() > 0.5: self.pool = list(map(lambda s: self.mutate(s), self.pool)) self.pool = self.crossover_all() best_parent = self.__find_best_parents(1)[0] print(best_parent, 'cost=%f' % self.cost(best_parent)) return best_parent def __init_pool(self, pool_size) -> list: """ Initialize solution pool. :param pool_size: Num of initial random solutions :return: initial pool of """ print("Initializing the pool...") self.pool.clear() all_comb = it.combinations_with_replacement(self.choices, self.trials) sample = random.sample(list(all_comb), pool_size) self.pool.extend(map(lambda _: ''.join(_), sample)) return self.pool def __find_best_parents(self, count=1): """ Find best gene(s) or parent(s) from the current pool. :param count: Number of desired best parents to be returned. Default is 1. :return: A list of most fit sequences. """ sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) return sorted_pool[:count] def crossover_all(self): """ Perform random crossover for all pairs. :return: new pool """ new_pool = [] for i in range(int(self.pool_size/2)): seq1 = self.pool[i*2] # change to weighted random seq2 = self.pool[i*2 + 1] # change to weighted random new_pool.extend(self.crossover(seq1, seq2)) return new_pool def crossover(self, seq1, seq2): """ Crossover two sequences. :param seq1: :param seq2: :return: """ pos = random.randint(0, self.trials) return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] def mutate(self, seq): if random.random() > 0.5: pos = random.randint(0, len(seq)-1) seq_list = list(seq) seq_list[pos] = random.choice(self.choices) return ''.join(seq_list) return seq def cost(self, seq): targets, lures = common.count_targets_and_lures(seq, self.n) #print(seq, targets, lures) tl = targets / lures if lures >0 else targets c1, c2 = self.skewness_cost(seq), self.tl_cost(tl) return c1+c2 def skewness_cost(self, seq): even_ratio = self.trials / len(self.choices) costs = {c: abs(seq.count(c) - even_ratio) for c in self.choices} max_deviation_from_even_dist = max(list(costs.values())) return 1.0 - self.skewness_norm.pdf(max_deviation_from_even_dist) / self.skewness_norm.pdf(0) def tl_cost(self, tl): return 1.0 - self.tl_norm.pdf(tl) / self.tl_norm.pdf(self.tl) if __name__ == '__main__': generator = ProgressiveGAGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=24, n=4) sq = generator.generate() print('Progressively-Optimized Sequence: %s' % sq)