Exemplo de algoritmo de escalada
1. Visão geral
Neste tutorial, mostraremos o algoritmo Hill-Climbing e sua implementação. Também veremos seus benefícios e desvantagens. Antes de entrar diretamente nisso, vamos discutir brevemente a abordagem de algoritmos de geração e teste.
2. Algoritmo de geração e teste
É uma técnica muito simples que nos permite criar algoritmos para encontrar soluções:
-
Definir estado atual como um estado inicial
-
Aplique qualquer operação possível no estado atual e gere uma solução possível
-
Compare a solução recém-gerada com o estado da meta
-
Se o objetivo for alcançado ou nenhum novo estado puder ser criado, saia. Caso contrário, retorne à etapa 2
Funciona muito bem com problemas simples. Como é uma pesquisa exaustiva, não é possível considerá-la ao lidar com grandes espaços problemáticos. Também é conhecido como algoritmo do Museu Britânico (tentando encontrar um artefato no Museu Britânico, explorando-o aleatoriamente).
É também a principal idéia por trás do Ataque na Escalada no mundo da biometria. Essa abordagem pode ser usada para gerar dados biométricos sintéticos.
3. Introdução ao algoritmo de escalada simples
Na técnica de escalada, começando na base de uma colina, caminhamos para cima até chegarmos ao topo da colina. Em outras palavras, começamos com o estado inicial e continuamos aprimorando a solução até sua otimização.
É uma variação de um algoritmo de geração e teste que descarta todos os estados que não parecem promissores ou parecem improváveis de nos levar ao estado objetivo. Para tomar essas decisões, ele usa heurísticas (uma função de avaliação) que indica o quão perto o estado atual está do estado do objetivo.
Em palavras simples,Hill-Climbing = generate-and-test + heuristics
Vejamos o algoritmo de escalada Simple Hill:
-
Defina o estado atual como um estado inicial
-
Faça um loop até que o estado do objetivo seja alcançado ou que nenhum operador possa ser aplicado no estado atual:
-
Aplique uma operação ao estado atual eget a new state
-
Compare o novo estado com a meta
-
Quit se o estado da meta for alcançado
-
Avalie o novo estado com a função heurística ecompare it with the current state
-
If the newer state is closer para a meta em comparação com o estado atual,update the current state
-
Como podemos ver, atinge o estado do objetivo com melhorias iterativas. No algoritmo Hill-Climbing, encontrar objetivo é equivalente a atingir o topo da colina.
4. Exemplo
O algoritmo de alpinismo pode ser classificado como uma pesquisa informada. Portanto, podemos implementar qualquer pesquisa ou problema baseado em nó, como o problema das n-rainhas usando-o. Para entender o conceito facilmente, usaremos um exemplo muito simples.
Vejamos a imagem abaixo:
O ponto principal ao resolver qualquer problema de escalada é escolher uma função heurística apropriada.
Vamos definir essa funçãoh:
h(x) = +1 para todos os blocos na estrutura de suporte se o bloco estiver corretamente posicionado, caso contrário -1 para todos os blocos na estrutura de suporte.
Aqui, chamaremos qualquer bloco posicionado corretamente se ele tiver a mesma estrutura de suporte que o estado do objetivo. De acordo com o procedimento de hill climbing discutido anteriormente, vamos dar uma olhada em todas as iterações e suas heurísticas para atingir o estado de destino:
5. Implementação
Agora, vamos implementar o mesmo exemplo usando o algoritmo Hill-Climbing.
Em primeiro lugar, precisamos de uma classeState que armazenará a lista de pilhas representando as posições dos blocos em cada estado. Ele também armazenará heurísticas para esse estado específico:
public class State {
private List> state;
private int heuristics;
// copy constructor, setters, and getters
}
Também precisamos de um método que calcule o valor heurístico do estado.
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;
}
Além disso, precisamos definir métodos de operador que nos tragam um novo estado. Para o nosso exemplo, definiremos dois desses métodos:
-
Retire um bloco de uma pilha e empurre-o para uma nova pilha
-
Retire um bloco de uma pilha e empurre-o para uma das outras pilhas
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;
}
Agora que temos nossos métodos auxiliares, vamos escrever um método para implementar a técnica de hill climbing.
Aqui,we keep computing new states which are closer to goals than their predecessors. Continuamos adicionando-os em nosso caminho até atingir a meta.
Se não encontrarmos nenhum novo estado, o algoritmo termina com uma mensagem de erro:
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;
}
Além disso, também precisamos do métodofindNextState, que aplica todas as operações possíveis no estado atual para obter o próximo estado:
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. Algoritmo de escalada de subida íngreme
O algoritmo de escalada em subida mais íngreme (pesquisa por gradiente) é uma variante do algoritmo de escalada em subida. Podemos implementá-lo com pequenas modificações em nosso algoritmo simples. Neste algoritmo, obtemosconsider all possible states do estado atual e entãopick the best one as successor, ao contrário da técnica simples de hill climbing.
Em outras palavras, no caso da técnica de alpinismo, escolhemos qualquer estado como sucessor mais próximo do objetivo do que o estado atual, enquanto no algoritmo de escalada em subida mais íngreme, escolhemos o melhor sucessor entre todos os possíveis sucessores e atualizamos O estado atual.
7. Desvantagens
Hill Climbing é uma técnica míope, pois avalia apenas possibilidades imediatas. Portanto, pode acabar em poucas situações das quais não é possível escolher outros estados. Vejamos esses estados e algumas soluções para eles:
-
Local maximum: É um estado que é melhor do que todos os vizinhos, mas existe um estado melhor que está longe do estado atual; se o máximo local ocorrer à vista da solução, é conhecido como "sopé"
-
Plateau: Neste estado, todos os estados vizinhos têm os mesmos valores heurísticos, por isso não está claro escolher o próximo estado fazendo comparações locais
-
Ridge: É uma área que é mais alta do que os estados circundantes, mas não pode ser alcançada em um único movimento; por exemplo, temos quatro direções possíveis para explorar (N, E, W, S) e existe uma área na direção NE
Existem poucas soluções para superar essas situações:
-
Podemosbacktrack para um dos estados anteriores e explorar outras direções
-
Podemos pular alguns estados e fazer umjump em novas direções
-
Podemosexplore several directions descobrir o caminho correto
8. Conclusão
Mesmo que a técnica de escalada seja muito melhor do que a pesquisa exaustiva, ainda não é a ideal em grandes espaços problemáticos.
Sempre podemos codificar informações globais em funções heurísticas para tomar decisões mais inteligentes, mas a complexidade computacional será muito maior do que era antes.
O algoritmo de alpinismo pode ser muito benéfico quando batido com outras técnicas. Como sempre, o código completo para todos os exemplos pode ser encontradoover on GitHub.