Exemple d’algorithme d’escalade

Exemple d'algorithme d'escalade

1. Vue d'ensemble

Dans ce didacticiel, nous allons montrer l'algorithme Hill-Climbing et sa mise en œuvre. Nous examinerons également ses avantages et ses inconvénients. Avant de vous lancer directement, parlons brièvement de l'approche des algorithmes de génération et de test.

2. Algorithme de génération et de test

C'est une technique très simple qui nous permet d'algorithmiser la recherche de solutions:

  1. Définir l'état actuel en tant qu'état initial

  2. Appliquer toute opération possible sur l'état actuel et générer une solution possible

  3. Comparer la solution nouvellement générée avec l'état d'objectif

  4. Si l'objectif est atteint ou si aucun nouvel état ne peut être créé, quittez. Sinon, retournez à l'étape 2

Cela fonctionne très bien avec des problèmes simples. Comme il s’agit d’une recherche exhaustive, il n’est pas envisageable de la prendre en compte lorsqu’il s’agit de grands espaces à problèmes. Il est également connu sous le nom d'algorithme du British Museum (essayer de trouver un artefact dans le British Museum en l'explorant au hasard).

C’est aussi l’idée principale de l’attaque en montagne dans le monde de la biométrie. Cette approche peut être utilisée pour générer des données biométriques synthétiques.

3. Introduction à l'algorithme simple d'escalade

Dans la technique d'escalade, en partant du pied d'une colline, nous montons jusqu'au sommet de la colline. En d’autres termes, nous commençons par l’état initial et continuons d’améliorer la solution jusqu’à son optimum.

Il s'agit d'une variante d'un algorithme de génération et de test qui supprime tous les états qui ne semblent pas prometteurs ou qui semblent peu susceptibles de nous conduire à l'état objectif. Pour prendre de telles décisions, il utilise des heuristiques (une fonction d'évaluation) qui indiquent à quel point l'état actuel est proche de l'état d'objectif.

En termes simples,Hill-Climbing = generate-and-test + heuristics

Regardons l’algorithme d’escalade Simple Hill:

  1. Définir l'état actuel en tant qu'état initial

  2. Boucle jusqu'à ce que l'état de l'objectif soit atteint ou qu'il ne soit plus possible d'appliquer d'opérateurs sur l'état actuel:

    1. Appliquer une opération à l'état actuel et àget a new state

    2. Compare le nouvel état avec l'objectif

    3. Quit si l'état de l'objectif est atteint

    4. Évaluer le nouvel état avec une fonction heuristique etcompare it with the current state

    5. If the newer state is closer par rapport à l'objectif par rapport à l'état actuel,update the current state

Comme nous pouvons le constater, l’objectif est atteint avec des améliorations itératives. Dans l'algorithme Hill-Climbing, trouver un objectif équivaut à atteindre le sommet de la colline.

4. Exemple

L'algorithme d'escalade de colline peut être classé comme une recherche éclairée. Nous pouvons donc implémenter toute recherche basée sur des nœuds ou des problèmes tels que le problème des n-reines. Pour comprendre facilement le concept, nous allons prendre un exemple très simple.

Regardons l'image ci-dessous:

image

Pour résoudre tout problème d’escalade, il est essentiel de choisir une fonction heuristique appropriée.

Définissons une telle fonctionh:

h(x) = +1 pour tous les blocs de la structure support si le bloc est correctement positionné sinon -1 pour tous les blocs de la structure support.

Ici, nous appellerons n'importe quel bloc correctement positionné s'il a la même structure de support que l'état d'objectif. Conformément à la procédure d'escalade discutée précédemment, examinons toutes les itérations et leurs heuristiques pour atteindre l'état cible:

image 5. la mise en oeuvre

Maintenant, implémentons le même exemple en utilisant l'algorithme Hill-Climbing.

Tout d'abord, nous avons besoin d'une classeState qui stockera la liste des piles représentant les positions des blocs à chaque état. Il stockera également des heuristiques pour cet état particulier:

public class State {
    private List> state;
    private int heuristics;

    // copy constructor, setters, and getters
}

Nous avons également besoin d'une méthode qui calcule la valeur heuristique de l'état.

public int getHeuristicsValue(
  List> currentState, Stack goalStateStack) {

    Integer heuristicValue;
    heuristicValue = currentState.stream()
      .mapToInt(stack -> {
          return getHeuristicsValueForStack(
            stack, currentState, goalStateStack);
    }).sum();

    return heuristicValue;
}

public int getHeuristicsValueForStack(
  Stack stack,
  List> currentState,
  Stack goalStateStack) {

    int stackHeuristics = 0;
    boolean isPositioneCorrect = true;
    int goalStartIndex = 0;
    for (String currentBlock : stack) {
        if (isPositioneCorrect
          && currentBlock.equals(goalStateStack.get(goalStartIndex))) {
            stackHeuristics += goalStartIndex;
        } else {
            stackHeuristics -= goalStartIndex;
            isPositioneCorrect = false;
        }
        goalStartIndex++;
    }
    return stackHeuristics;
}

De plus, nous devons définir des méthodes d'opérateur qui nous permettront d'obtenir un nouvel état. Pour notre exemple, nous allons définir deux de ces méthodes:

  1. Pop un bloc d'une pile et le pousser sur une nouvelle pile

  2. Pop un bloc d'une pile et le pousser dans l'une des autres piles

private State pushElementToNewStack(
  List> currentStackList,
  String block,
  int currentStateHeuristics,
  Stack goalStateStack) {

    State newState = null;
    Stack newStack = new Stack<>();
    newStack.push(block);
    currentStackList.add(newStack);
    int newStateHeuristics
      = getHeuristicsValue(currentStackList, goalStateStack);
    if (newStateHeuristics > currentStateHeuristics) {
        newState = new State(currentStackList, newStateHeuristics);
    } else {
        currentStackList.remove(newStack);
    }
    return newState;
}
private State pushElementToExistingStacks(
  Stack currentStack,
  List> currentStackList,
  String block,
  int currentStateHeuristics,
  Stack goalStateStack) {

    return currentStackList.stream()
      .filter(stack -> stack != currentStack)
      .map(stack -> {
          return pushElementToStack(
            stack, block, currentStackList,
            currentStateHeuristics, goalStateStack);
        })
      .filter(Objects::nonNull)
      .findFirst()
      .orElse(null);
}

private State pushElementToStack(
  Stack stack,
  String block,
  List> currentStackList,
  int currentStateHeuristics,
  Stack goalStateStack) {

    stack.push(block);
    int newStateHeuristics
      = getHeuristicsValue(currentStackList, goalStateStack);
    if (newStateHeuristics > currentStateHeuristics) {
        return new State(currentStackList, newStateHeuristics);
    }
    stack.pop();
    return null;
}

Maintenant que nous avons nos méthodes d'aide, écrivons une méthode pour mettre en œuvre la technique d'escalade.

Ici,we keep computing new states which are closer to goals than their predecessors. Nous continuons à les ajouter dans notre chemin jusqu'à ce que nous atteignions l'objectif.

Si nous ne trouvons aucun nouvel état, l'algorithme se termine par un message d'erreur:

public List getRouteWithHillClimbing(
  Stack initStateStack, Stack goalStateStack) throws Exception {
    // instantiate initState with initStateStack
    // ...
    List resultPath = new ArrayList<>();
    resultPath.add(new State(initState));

    State currentState = initState;
    boolean noStateFound = false;

    while (
      !currentState.getState().get(0).equals(goalStateStack)
      || noStateFound) {
        noStateFound = true;
        State nextState = findNextState(currentState, goalStateStack);
        if (nextState != null) {
            noStateFound = false;
            currentState = nextState;
            resultPath.add(new State(nextState));
        }
    }
    return resultPath;
}

En plus de cela, nous avons également besoin de la méthodefindNextState qui applique toutes les opérations possibles sur l'état actuel pour obtenir l'état suivant:

public State findNextState(State currentState, Stack goalStateStack) {
    List> listOfStacks = currentState.getState();
    int currentStateHeuristics = currentState.getHeuristics();

     return listOfStacks.stream()
      .map(stack -> {
          return applyOperationsOnState(
            listOfStacks, stack, currentStateHeuristics, goalStateStack);
      })
      .filter(Objects::nonNull)
      .findFirst()
      .orElse(null);
}

public State applyOperationsOnState(
  List> listOfStacks,
  Stack stack,
  int currentStateHeuristics,
  Stack goalStateStack) {

    State tempState;
    List> tempStackList
      = new ArrayList<>(listOfStacks);
    String block = stack.pop();
    if (stack.size() == 0)
      tempStackList.remove(stack);
    tempState = pushElementToNewStack(
      tempStackList, block, currentStateHeuristics, goalStateStack);
    if (tempState == null) {
        tempState = pushElementToExistingStacks(
          stack, tempStackList, block,
          currentStateHeuristics, goalStateStack);
        stack.push(block);
    }
    return tempState;
}

6. Algorithme d'escalade le plus raide

L'algorithme Steepest-Ascent Hill-Climbing (recherche par gradient) est une variante de l'algorithme Hill Climbing. Nous pouvons l'implémenter avec de légères modifications dans notre algorithme simple. Dans cet algorithme, nousconsider all possible states à partir de l'état actuel, puispick the best one as successor, contrairement à la technique d'escalade simple.

En d’autres termes, dans le cas de la technique d’escalade, nous avons choisi comme successeur tout État qui se rapproche davantage de l’objectif que de l’état actuel, tandis que dans l’algorithme Steepest-Ascent Hill Climbing, nous choisissons le meilleur successeur, puis nous le mettons à jour. l'état actuel.

7. Désavantages

L'escalade est une technique à courte vue car elle n'évalue que les possibilités immédiates. Donc, il peut se retrouver dans peu de situations dans lesquelles il ne peut pas choisir d'autres états. Examinons ces états et quelques solutions pour eux:

  1. Local maximum: C’est un état qui est meilleur que tous les voisins, mais il existe un meilleur état qui est loin de l’état actuel; si le maximum local se produit en vue de la solution, on parle de «contreforts»

  2. Plateau: Dans cet état, tous les états voisins ont les mêmes valeurs heuristiques, il n'est donc pas clair de choisir l'état suivant en effectuant des comparaisons locales

  3. Ridge: C'est une zone qui est plus élevée que les états environnants, mais elle ne peut pas être atteinte en un seul mouvement; par exemple, nous avons quatre directions possibles à explorer (N, E, W, S) et une zone existe dans la direction NE

Il existe peu de solutions pour surmonter ces situations:

  1. Nous pouvonsbacktrack vers l'un des états précédents et explorer d'autres directions

  2. On peut sauter quelques états et faire unjump dans de nouvelles directions

  3. Nous pouvonsexplore several directions pour trouver le chemin correct

8. Conclusion

Même si la technique d’escalade est bien meilleure que la recherche exhaustive, elle n’est toujours pas optimale dans les grands espaces à problèmes.

Nous pouvons toujours coder des informations globales dans des fonctions heuristiques pour prendre des décisions plus intelligentes, mais la complexité des calculs sera alors bien supérieure à ce qu'elle était auparavant.

L'algorithme d'escalade de colline peut être très bénéfique lorsqu'il est associé à d'autres techniques. Comme toujours, le code complet de tous les exemples peut être trouvéover on GitHub.