Recuit d’inspiration quantique avec C# ou Python — Visual Studio Magazine

Le laboratoire de science des données

Recuit d’inspiration quantique avec C# ou Python

Dr. James McCaffrey de Microsoft Research explique une nouvelle idée qui modifie légèrement le recuit simulé standard en empruntant des idées à la mécanique quantique.

Le but d’un problème d’optimisation combinatoire est de trouver le meilleur ordre d’un ensemble d’éléments discrets. Un défi d’optimisation combinatoire classique est le problème du voyageur de commerce (TSP). Pour TSP, vous souhaitez trouver l’ordre dans lequel visiter un ensemble de villes afin que la distance totale parcourue soit minimisée.

L’une des techniques les plus anciennes et les plus simples pour résoudre les problèmes d’optimisation combinatoire est appelée recuit simulé. Une idée relativement nouvelle consiste à modifier légèrement le recuit simulé standard en empruntant une ou plusieurs idées à la mécanique quantique. Ceci est parfois appelé recuit d’inspiration quantique. Notez que les algorithmes d’inspiration quantique n’utilisent pas de matériel informatique quantique. Les algorithmes d’inspiration quantique ne sont que des modifications d’algorithmes classiques existants et ils fonctionnent sur du matériel standard.

Une bonne façon de voir où va cet article est de jeter un coup d’œil à la capture d’écran d’un programme de démonstration dans Figure 1. La démo met en place un problème synthétique où il y a 40 villes, étiquetées de 0 à 39. La distance entre les villes est conçue de sorte que le meilleur itinéraire commence à la ville 0, puis visite chaque ville dans l’ordre. La distance totale de l’itinéraire optimal est de 39,0.

La démo définit des paramètres de recuit d’inspiration quantique de max_iter = 20_000, start_temperature = 100_000.0, alpha = 0,9990 et pct_tunnel = 0,10. Le recuit d’inspiration quantique est un processus itératif et max_iter est le nombre maximal de fois que la boucle de traitement s’exécutera. Les paramètres start_temperature et alpha contrôlent la manière dont la partie classique de l’algorithme de recuit explore les voies de solution possibles. Le paramètre pct_tunnel contrôle la fréquence d’utilisation de la partie d’inspiration quantique de l’algorithme.

La démo met en place une estimation initiale aléatoire du meilleur itinéraire comme [ 7, 34, 21 . . 9, 10 ]. Après avoir itéré 20 000 fois, la démo signale le meilleur itinéraire trouvé comme [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 19, 18, 15, 14, 13, 12, 10, 11, 17, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 ]. La distance totale requise pour visiter les villes dans cet ordre est de 61,5 et la solution est donc proche, mais pas aussi bonne que la solution optimale. Les 10 premières villes et les 20 dernières villes sont visitées dans le bon ordre mais les 10 villes du milieu ne sont pas visitées correctement.

Figure 1 : Recuit d'inspiration quantique pour résoudre le problème du voyageur de commerce.
[Click on image for larger view.] Figure 1: Recuit d’inspiration quantique pour résoudre le problème du voyageur de commerce

Cet article suppose que vous avez une connaissance intermédiaire ou supérieure d’un langage de programmation de la famille C, de préférence C# ou Python, mais ne suppose pas que vous savez quoi que ce soit sur le recuit simulé. Le code source complet du programme de démonstration est présenté dans cet article, et le code est également disponible dans le téléchargement du fichier d’accompagnement.

Comprendre le recuit simulé classique
Le recuit d’inspiration quantique est une légère adaptation du recuit simulé classique. Supposons que vous ayez un problème d’optimisation combinatoire avec seulement cinq éléments, et où le meilleur ordre/permutation est [B, A, D, C, E]. Une approche primitive serait de commencer par une permutation aléatoire telle que [C, A, E, B, D] puis échangez à plusieurs reprises des paires d’éléments sélectionnées au hasard jusqu’à ce que vous trouviez le meilleur ordre. Par exemple, si vous permutez les éléments à [0] et [1]la nouvelle permutation est [A, C, E, B, D]. Vous répétez la génération de nouvelles permutations jusqu’à ce que vous trouviez un bon résultat.

Cette approche par force brute fonctionne s’il n’y a que quelques éléments dans la permutation du problème, mais échoue même pour un nombre modéré d’éléments. Pour le problème de démonstration avec n = 40 villes, il y en a 40 ! permutations possibles = 40 * 39 * 38 * . . * 1 = 815 915 283 247 897 734 345 611 269 596 115 894 272 000 000 000. Même si vous pouviez examiner un milliard de solutions candidates par seconde, vérifier toutes les permutations vous prendrait environ 9 000 000 000 000 000 000 000 000 000 d’années, ce qui est beaucoup plus long que l’âge estimé de l’univers (environ 14 000 000 000 d’années).

Pour comprendre le recuit d’inspiration quantique, vous devez d’abord comprendre le recuit simulé classique. Exprimé en pseudo-code, le recuit simulé classique est :

make a random initial guess permutation
  set a large initial temperature
  loop many times
    swap two randomly selected elements of the curr guess
    compute error of proposed candidate solution
    if proposed solution is better than curr solution then
      accept the proposed solution
    else if proposed solution is worse then
      sometimes accept solution anyway, based on curr temp
    end-if
    decrease temperature value slightly
  end-loop
  return best solution found

En échangeant deux éléments sélectionnés au hasard de la solution actuelle, une solution proposée adjacente est créée. La solution proposée adjacente sera similaire à la supposition actuelle, de sorte que le processus de recherche n’est pas complètement aléatoire. Une bonne solution actuelle conduira probablement à une bonne nouvelle solution proposée.

En acceptant parfois une solution proposée qui est pire que la supposition actuelle, vous pouvez éviter de vous retrouver piégé dans une solution bonne mais pas optimale. La probabilité d’accepter une solution moins bonne est donnée par :

accept_p = exp((err - adj_err) / curr_temperature)

où exp() la fonction mathématique exp(), err est l’erreur associée à la supposition actuelle, adj_err est l’erreur associée à la solution adjacente proposée et curr_temperature est une valeur telle que 1000.0.

Dans le recuit simulé, la valeur de température commence par une valeur élevée, telle que 1000000,0, puis est lentement réduite à chaque itération. Au début de l’algorithme, lorsque la température est grande, accept_p sera grand (proche de 1) et l’algorithme passera souvent à une solution moins bonne. Cela permet d’examiner de nombreuses permutations. Tard dans l’algorithme, lorsque la température est petite, accept_p sera petit (proche de 0) et l’algorithme passera rarement à une solution pire.

La réduction de température est contrôlée par la valeur alpha. Supposons que la température actuelle = 1000,0 et alpha = 0,90. Après la prochaine itération de traitement, la température devient 1000,0 * 0,90 = 900,0. Après la prochaine itération, la température devient 900,0 * 0,90 = 810,0. Après une troisième itération, la température devient 810,0 * 0,90 = 729,0. Etc. Des valeurs plus petites d’alpha diminuent la température plus rapidement.

Ajout d’un tunnel d’inspiration quantique
Le recuit d’inspiration quantique ajoute une légère touche au recuit simulé classique. En mécanique quantique, il existe une idée appelée effet tunnel quantique. En bref, et de manière très lâche, une particule quantique passera généralement de son état d’énergie actuel à un état d’énergie inférieur adjacent. Lorsqu’une particule rencontre une barrière potentielle, elle sera généralement bloquée, mais traversera parfois la barrière vers un état non adjacent.

Figure 2 : Recuit d'inspiration quantique pour résoudre le problème du voyageur de commerce.
[Click on image for larger view.] Figure 2: Tunnellisation d’inspiration quantique

Cet effet tunnel quantique peut être utilisé pour améliorer le recuit simulé classique. Les idées clés sont présentées dans le graphique Figure 2. Dans le recuit simulé classique pour TSP, les états sont des permutations de villes. Si vous passez uniquement de l’état actuel à un état adjacent (lorsque l’ordre d’une seule paire de villes est modifié), vous pouvez rester bloqué à une erreur minimale locale. Si vous autorisez le tunneling, vous pouvez faire un grand saut pour vous échapper et revenir sur la bonne voie pour trouver l’état avec l’erreur minimale globale.

Ainsi, l’effet tunnel d’inspiration quantique signifie simplement permettre parfois à une solution candidate d’être plus éloignée de la solution actuelle. Ceci peut être réalisé en échangeant plus que deux villes. Par exemple, si vous échangez 10 paires de villes sélectionnées au hasard, la permutation résultante sera plus éloignée de la permutation source que si vous échangez une seule paire de villes sélectionnées au hasard.

Au début de l’algorithme, lors de la création d’une solution candidate, vous pouvez échanger de nombreuses paires de villes, générant un candidat éloigné de la permutation actuelle. Plus tard dans l’algorithme, vous échangez moins de paires de villes. Exprimé en pseudo-code :

set pct_tunnel (comme 0.10) boucle plusieurs fois p = random in [0.0, 1.0)
    if p < pct_tunnel then (do tunneling)
      set num_swaps = large, based on pct iterations left
    else
      set num_swaps = 1 (no tunneling)
    end-if
    create candidate solution using num_swaps
    apply classical annealing
  end-loop
  return best solution found

The Python version of the function that creates a candidate permutation-solution is:

def adjacent(route, n_swaps, rnd):
  n = len(route)
  result = np.copy(route)
  for ns in range(n_swaps):
    i = rnd.randint(n)
    j = rnd.randint(n)
    tmp = result[i]
    résultat[i] = résultat[j]
    résultat[j] = tmp renvoie le résultat

Le paramètre route est la solution actuelle. Le paramètre n_swaps spécifie le nombre de paires de villes à permuter lors de la création de la solution candidate. Le paramètre rnd est un objet générateur de nombres aléatoires local.

Une version C# équivalente de la fonction est :

static int[] Adjacent(int[] route, int numSwaps, Random rnd)
{
  int n = route.Length;
  int[] result = (int[])route.Clone();
  for (int ns = 0; ns < numSwaps; ++ns) {
    int i = rnd.Next(0, n);
    int j = rnd.Next(0, n);
    int tmp = result[i];
    result[i] = result[j];
    result[j] = tmp;
  }
  return result;
}

Le paramètre clé est le nombre de paires de villes à permuter. Le programme de démonstration calcule cela comme le pourcentage d'itérations restantes multiplié par le nombre total de villes. Par exemple, pour n = 40 villes, si max_iterations = 1000 et l'itération actuelle est de 300, alors il reste 700 / 1000 = 0,70 des itérations restantes, et le nombre d'échanges serait de 0,70 * 40 = 28.

Le programme de démonstration
Le programme complet de démonstration de recuit d'inspiration quantique utilisant Python, avec quelques modifications mineures pour économiser de l'espace, est présenté dans Liste 1. J'indente en utilisant deux espaces plutôt que les quatre espaces standard. Le caractère barre oblique inverse est utilisé pour la continuation de ligne afin de décomposer les longues instructions. Vous pouvez trouver une version C# équivalente du programme de démonstration sur mon blog.

Liste 1 : Le programme de recuit quantique pour le langage Python TSP

# tsp_quantum_annealing.py
# traveling salesman problem 
# quantum inspired simulated annealing
# Python 3.7.6 (Anaconda3 2020.02)

import numpy as np

def total_dist(route):
  d = 0.0  # total distance between cities
  n = len(route)
  for i in range(n-1):
    if route[i] < route[i+1]:
      d += (route[i+1] - route[i]) * 1.0
    else:
      d += (route[i] - route[i+1]) * 1.5
  return d

def error(route):
  n = len(route)
  d = total_dist(route)
  min_dist = n-1
  return d - min_dist

def adjacent(route, n_swaps, rnd):
  # tunnel if n_swaps > 1
  n = len(route)
  result = np.copy(route)
  for ns in range(n_swaps):
    i = rnd.randint(n); j = rnd.randint(n)
    tmp = result[i]
    result[i] = result[j]; result[j] = tmp
  return result

def my_kendall_tau_dist(p1, p2):
  # p1, p2 are 0-based lists or np.arrays
  n = len(p1)
  index_of = [None] * n  # lookup into p2
  for i in range(n):
    v = p2[i]; index_of[v] = i

  d = 0  # raw distance = number pair misorderings
  for i in range(n):
    for j in range(i+1, n):
      if index_of[p1[i]] > index_of[p1[j]]:
        d += 1
  normer = n * (n - 1) / 2.0
  nd = d / normer  # normalized distance
  return (d, nd) 

def solve_qa(n_cities, rnd, max_iter, start_temperature,
  alpha, pct_tunnel):
  curr_temperature = start_temperature
  soln = np.arange(n_cities, dtype=np.int64)  # [0,1,2, . . ]
  rnd.shuffle(soln)
  print("Initial guess: ")
  print(soln); print("")

  err = error(soln)
  iter = 0
  interval = (int)(max_iter / 10)
  num_swaps = n_cities

  best_soln = np.copy(soln)
  best_err = err

  while iter < max_iter and err > 0.0:
  # while iter < max_iter:
    # pct left determines n_swaps determines distance
    pct_iters_left = (max_iter - iter) / (max_iter * 1.0)
    if pct_iters_left < 0.00001: 
      pct_iters = 0.00001

    p = rnd.random()  # [0.0, 1.0]
    if p < pct_tunnel:  # tunnel
      num_swaps = (int)(pct_iters_left * n_cities)
      if num_swaps < 1:
        num_swaps = 1
    else:  # no tunneling
      num_swaps = 1

    adj_route = adjacent(soln, num_swaps, rnd)
    adj_err = error(adj_route)

    if adj_err < best_err:
      best_soln = np.copy(adj_route)
      best_err = adj_err

    if adj_err < err:  # better route so accept
      soln = adj_route; err = adj_err
    else:          # adjacent is worse
      accept_p = np.exp((err - adj_err) / curr_temperature)
      p = rnd.random()
      if p < accept_p:  # accept anyway
        soln = adj_route
        err = adj_err 
      # else don't accept worse route
   
    if iter % interval == 0:
      (dist, nd) = my_kendall_tau_dist(soln, adj_route)
      print("iter = %6d | " % iter, end="")
      print("dist curr to candidate = %8.4f | " 
        % nd, end="")
      print("curr_temp = %12.4f | " 
        % curr_temperature, end="")
      print("error = %6.1f " % best_err)

    if curr_temperature < 0.00001:
      curr_temperature = 0.00001
    else:
      curr_temperature *= alpha

    iter += 1
  return best_soln 

def main():
  print("nBegin TSP using quantum inspired annealing ")

  num_cities = 40
  print("nSetting num_cities = %d " % num_cities)
  print("nOptimal solution is 0, 1, 2, . . " + 
    str(num_cities-1))
  print("Optimal solution has total distance = %0.1f " 
    % (num_cities-1))
  rnd = np.random.RandomState(6)
  max_iter = 20_000  # 120000 finds optimal solution
  start_temperature = 100_000.0
  alpha = 0.9990
  pct_tunnel = 0.10

  print("nQuantum inspired annealing settings: ")
  print("max_iter = %d " % max_iter)
  print("start_temperature = %0.1f " 
    % start_temperature)
  print("alpha = %0.4f " % alpha)
  print("pct_tunnel = %0.2f " % pct_tunnel)

  print("nStarting solve() ")
  soln = solve_qa(num_cities, rnd, max_iter, 
    start_temperature, alpha, pct_tunnel)
  print("Finished solve() ")

  print("nBest solution found: ")
  print(soln)
  dist = total_dist(soln)
  print("nTotal distance = %0.1f " % dist)

  print("nEnd demo ")
if __name__ == "__main__":
  main()

.

Leave a Comment