Trovare una soluzione ottimale per alcuni problemi di ottimizzazione può essere un compito incredibilmente difficile, spesso praticamente impossibile. Questo perché quando un problema diventa sufficientemente grande abbiamo bisogno di cercare attraverso un numero enorme di possibili soluzioni per trovare quello ottimale. Anche con la moderna potenza di calcolo ci sono ancora spesso troppe possibili soluzioni da considerare. In questo caso, poiché non possiamo realisticamente aspettarci di trovare quello ottimale in un lasso di tempo ragionevole, dobbiamo accontentarci di qualcosa che sia abbastanza vicino.
Un problema di ottimizzazione di esempio che di solito ha un gran numero di possibili soluzioni sarebbe il problema del commesso viaggiatore. Per trovare una soluzione a un problema come il problema del commesso viaggiatore, dobbiamo utilizzare un algoritmo in grado di trovare una soluzione abbastanza buona in un ragionevole lasso di tempo. In un precedente tutorial abbiamo esaminato come potremmo farlo con algoritmi genetici, e anche se gli algoritmi genetici sono un modo in cui possiamo trovare una soluzione “abbastanza buona” al problema del commesso viaggiatore, ci sono altri algoritmi più semplici che possiamo implementare che ci troveranno anche una soluzione vicina a quella ottimale. In questo tutorial l’algoritmo che useremo è, ‘ricottura simulata’.
Se non hai familiarità con il problema del commesso viaggiatore, potrebbe valere la pena dare un’occhiata al mio precedente tutorial prima di continuare.
Che cos’è la ricottura simulata?
In primo luogo, diamo un’occhiata a come funziona la ricottura simulata e perché è bravo a trovare soluzioni al problema del commesso viaggiatore in particolare. L’algoritmo di ricottura simulato è stato originariamente ispirato dal processo di ricottura nella lavorazione dei metalli. La ricottura comporta il riscaldamento e il raffreddamento di un materiale per alterare le sue proprietà fisiche a causa dei cambiamenti nella sua struttura interna. Quando il metallo si raffredda, la sua nuova struttura diventa fissa, causando di conseguenza il metallo a mantenere le sue proprietà appena ottenute. Nella ricottura simulata manteniamo una variabile di temperatura per simulare questo processo di riscaldamento. Inizialmente lo impostiamo in alto e poi lo lasciamo lentamente “raffreddare” mentre l’algoritmo viene eseguito. Mentre questa variabile di temperatura è alta, l’algoritmo sarà autorizzato, con più frequenza, ad accettare soluzioni peggiori della nostra soluzione attuale. Ciò dà all’algoritmo la possibilità di saltare fuori da qualsiasi optimum locale in cui si trova all’inizio dell’esecuzione. Come la temperatura è ridotta così è la possibilità di accettare soluzioni peggiori, consentendo quindi l’algoritmo di concentrarsi gradualmente in una zona dello spazio di ricerca in cui si spera, una soluzione vicina a ottimale può essere trovato. Questo processo di “raffreddamento” graduale è ciò che rende l’algoritmo di ricottura simulato straordinariamente efficace nel trovare una soluzione quasi ottimale quando si affrontano problemi di grandi dimensioni che contengono numerosi ottimismi locali. La natura del problema del commesso viaggiatore lo rende un esempio perfetto.
Vantaggi della ricottura simulata
Ci si potrebbe chiedere se ci sia un reale vantaggio nell’implementazione della ricottura simulata su qualcosa come un semplice scalatore. Anche se gli alpinisti possono essere sorprendentemente efficaci nel trovare una buona soluzione, hanno anche la tendenza a rimanere bloccati in optimum locali. Come abbiamo precedentemente determinato, l’algoritmo di ricottura simulato è eccellente per evitare questo problema ed è molto meglio in media a trovare un ottimale globale approssimativo.
Per aiutare a capire meglio diamo rapidamente un’occhiata al motivo per cui un algoritmo di arrampicata in collina di base è così incline a rimanere intrappolati in optimum locali.
Un algoritmo hill climber accetterà semplicemente soluzioni vicine che sono migliori della soluzione attuale. Quando l’alpinista non riesce a trovare vicini migliori, si ferma.
Nell’esempio sopra iniziamo il nostro alpinista alla freccia rossa e si fa strada fino a raggiungere un punto in cui non può salire più in alto senza prima scendere. In questo esempio possiamo vedere chiaramente che è bloccato in un ottimale locale. Se questo fosse un problema del mondo reale, non sapremmo come appare lo spazio di ricerca, quindi sfortunatamente non saremmo in grado di dire se questa soluzione è da nessuna parte vicina a un optimum globale.
La ricottura simulata funziona in modo leggermente diverso da questo e occasionalmente accetta soluzioni peggiori. Questa caratteristica della ricottura simulata lo aiuta a saltare fuori da qualsiasi optimum locale in cui potrebbe essere rimasto bloccato.
Funzione di accettazione
Diamo un’occhiata a come l’algoritmo decide quali soluzioni accettare in modo da poter capire meglio come è in grado di evitare questi ottimismi locali.
Per prima cosa controlliamo se la soluzione vicina è migliore della nostra soluzione attuale. Se lo è, lo accettiamo incondizionatamente. Se, tuttavia, la soluzione vicina non è migliore, dobbiamo considerare un paio di fattori. In primo luogo, quanto è peggiore la soluzione vicina; e in secondo luogo, quanto è alta l’attuale “temperatura” del nostro sistema. Alle alte temperature il sistema è più probabile accettare soluzioni che sono peggiori.
La matematica per questo è piuttosto semplice:
Fondamentalmente, minore è il cambiamento di energia (la qualità della soluzione) e maggiore è la temperatura, più è probabile che l’algoritmo accetti la soluzione.
Panoramica dell’algoritmo
Quindi, come appare l’algoritmo? Bene, nella sua implementazione più elementare è piuttosto semplice.
- Per prima cosa dobbiamo impostare la temperatura iniziale e creare una soluzione iniziale casuale.
- Quindi iniziamo il loop fino a quando la nostra condizione di stop non è soddisfatta. Di solito il sistema si è sufficientemente raffreddato o è stata trovata una soluzione abbastanza buona.
- Da qui selezioniamo un vicino apportando una piccola modifica alla nostra soluzione attuale.
- Decidiamo quindi se passare a quella soluzione vicina.
- Infine, diminuiamo la temperatura e continuiamo il loop
Inizializzazione della temperatura
Per una migliore ottimizzazione, quando inizializziamo la variabile di temperatura dovremmo selezionare una temperatura che inizialmente consenta praticamente qualsiasi movimento contro la soluzione corrente. Questo dà l’algoritmo la possibilità di esplorare meglio l’intero spazio di ricerca prima di raffreddamento e stabilirsi in una regione più focalizzata.
Codice di esempio
Ora usiamo ciò che sappiamo per creare un algoritmo di ricottura simulato di base e quindi applicarlo al problema del venditore ambulante di seguito. Useremo Java in questo tutorial, ma la logica dovrebbe essere abbastanza semplice da copiare in qualsiasi lingua di tua scelta.
Per prima cosa dobbiamo creare una classe di città che possa essere utilizzata per modellare le diverse destinazioni del nostro venditore ambulante.
Città.java
* Città.java
*Modelli a city
* /
pacchetto sa;
classe pubblica City {
int x;
int y;
/ / Costruisce una città posizionata casualmente
public City () {
this.x = (int) (Matematica.casuale ()*200);
questo.y = (int) (Matematica.casuale()*200);
}
// Costruisce una città nella posizione x, y scelta
Città pubblica (int x, int y) {
questo.x = x;
questo.y = y;
}
// Ottiene la coordinata x della città
public int getX () {
restituisce questo.x;
}
// Ottiene la coordinata y della città
public int getY () {
restituisce questo.y;
}
// Ottiene la distanza dalla città data
public double distanceTo (City city) {
int xDistance = Math.abs (getX () – città.getX());
int yDistance = Math.abs (getY () – città.getY ());
doppia distanza = Matematica.sqrt ((xDistance*xDistance) + (yDistance*yDistance) );
return distance;
}
@Override
public String toString(){
return getX ()+”, ” + getY();
}
}
Quindi creiamo una classe che può tenere traccia delle città.
TourManager.java
* TourManager.java
* Contiene le città di un tour
* /
pacchetto sa;
importa java.util.Elenco degli array;
public class TourManager {
/ / Contiene le nostre città
ArrayList statico privato destinationCities = new ArrayList < Città>();
// Aggiunge una città di destinazione
public static void addCity (City city) {
destinationCities.aggiungi (città);
}
// Ottieni una città
Città statica pubblica getCity (indice int){
Destinazioni di ritorno (Città).ottieni (indice);
}
// Ottieni il numero di città di destinazione
public static int numberOfCities () {
return destinationCities.dimensione();
}
}
Ora per creare la classe che può modellare un tour venditore itinerante.
Giro.java
* Giro.java
*Memorizza un tour candidato attraverso tutte le città
* /
pacchetto sa;
importa java.util.ArrayList;
importa java.util.Collections;
public class Tour{
/ / Tiene il nostro tour delle città
private ArrayList tour = new ArrayList < City>();
// Cache
private int distance = 0;
/ / Costruisce un tour vuoto
Tour pubblico () {
per (int i = 0; i < TourManager.numberOfCities (); i++) {
giro.aggiungi (null);
}
}
// Costruisce un tour da un altro tour
Tour pubblico (ArrayList tour) {
questo.tour = (ArrayList) tour.clone();
}
// Restituisce informazioni turistiche
public ArrayList getTour(){
ritorno tour;
}
// Crea un individuo casuale
public void generateIndividual() {
// Loop per tutta la città di destinazione e aggiungere al nostro tour
for (int cityIndex = 0; cityIndex < TourManager.numberOfCities (); cityIndex++) {
setCity (cityIndex, TourManager.getCity (cityIndex));
}
// Riordina in modo casuale le collezioni del tour
.shuffle (tour);
}
// Ottiene una città dal tour
Città pubblica getCity(int tourPosition) {
ritorno (Città)tour.ottieni (tourPosition);
}
// Imposta una città in una determinata posizione all’interno di un tour
public void setCity (int tourPosition, City city) {
tour.set (tourPosition, city);
// Se i tour sono stati modificati, è necessario ripristinare la distanza e la distanza
= 0;
}
// Ottiene la distanza totale del tour
public int getDistance () {
if (distance = = 0) {
int tourDistance = 0;
/ / Attraversa le città del nostro tour
per (int cityIndex=0; cityIndex < tourSize(); cityIndex++) {
// Get città siamo in viaggio da
Città fromCity = getCity(cityIndex);
// Città stiamo viaggiando a
Città destinationCity;
// Verifica che non siamo nel nostro tour, ultima città, se vogliamo impostare la nostra
// tour destinazione finale città di partenza città
se(cityIndex+1 < tourSize()){
destinationCity = getCity(cityIndex+1);
}
else{
destinationCity = getCity(0);
}
// Ottenere la distanza tra le due città
tourDistance += fromCity.distanceTo (destinationCity);
}
distanza = tourDistance;
}
distanza di ritorno;
}
// Ottieni il numero di città del nostro tour
public int tourSize () {
tour di ritorno.dimensione();
}
@Se non si desidera utilizzare la stringa, si prega di contattare il servizio clienti.)+”|”;
}
ritorno geneString;
}
}
Infine, creiamo il nostro algoritmo di ricottura simulato.
SimulatedAnnealing.java
public class SimulatedAnnealing {
// Calcola la probabilità di accettazione
public static double acceptanceProbability(int energia, int newEnergy, doppia temperatura) {
// Se la nuova soluzione è meglio, accettare
se (newEnergy < energia) {
ritorno 1.0;
}
// Se la nuova soluzione è peggio, calcolare la probabilità di accettazione
return Math.exp ((energy-newEnergy)/ temperature);
}
public static void main(String args) {
/ / Crea e aggiungi le nostre città
City city = new City(60, 200);
TourManager.addCity (città);
Città city2 = nuova città(180, 200);
TourManager.addCity (city2);
Città city3 = nuova città(80, 180);
TourManager.addCity(city3);
Città city4 = nuova città (140, 180);
TourManager.addCity(city4);
Città city5 = nuova città (20, 160);
TourManager.addCity(city5);
Città city6 = nuova città (100, 160);
TourManager.addCity(city6);
Città city7 = nuova città (200, 160);
TourManager.addCity(city7);
Città city8 = nuova città (140, 140);
TourManager.addCity(city8);
Città city9 = nuova città (40, 120);
TourManager.addCity (city9);
Città city10 = nuova città(100, 120);
TourManager.addCity(city10);
Città city11 = nuova città (180, 100);
TourManager.addCity (city11);
Città city12 = nuova città(60, 80);
TourManager.addCity (city12);
Città city13 = nuova città(120, 80);
TourManager.addCity (city13);
Città city14 = nuova città(180, 60);
TourManager.addCity (city14);
Città city15 = nuova città(20, 40);
TourManager.addCity (city15);
Città city16 = nuova città(100, 40);
TourManager.addCity (city16);
Città city17 = nuova città(200, 40);
TourManager.addCity (città17);
Città city18 = nuova città(20, 20);
TourManager.addCity (city18);
Città city19 = nuova città(60, 20);
TourManager.addCity (city19);
Città city20 = nuova città(160, 20);
TourManager.addCity (city20);
// Imposta temp iniziale
double temp = 10000;
// Velocità di raffreddamento
double coolingRate = 0.003;
// Inizializza la soluzione iniziale
Tour currentSolution = new Tour ();
currentSolution.generateIndividual ();
Sistema.fuori.println (“Distanza soluzione iniziale:” + currentSolution.getDistance ());
// Imposta come migliore corrente
Tour migliore = nuovo Tour(currentSolution.getTour ());
/ / Loop fino a quando il sistema si è raffreddato
mentre (temp > 1) {
// Crea nuovo tour vicino
Tour newSolution = nuovo Tour (currentSolution.getTour ());
// Ottieni una posizione casuale nel tour
int tourPos1 = (int) (newSolution.tourSize () * Matematica.casuale ());
int tourPos2 = (int) (newSolution.tourSize () * Matematica.random());
/ / Ottieni le città nelle posizioni selezionate nel tour
Città citySwap1 = newSolution.getCity (tourPos1);
Città citySwap2 = newSolution.getCity (tourPos2);
/ / Scambiali
newSolution.setCity (tourPos2, citySwap1);
newSolution.setCity (tourPos1, citySwap2);
// Ottieni energia delle soluzioni
int currentEnergy = currentSolution.getDistance ();
int neighbourEnergy = newSolution.getDistance ();
// Decidere se dovremmo accettare il vicino
if (acceptanceProbability (currentEnergy, neighbourEnergy, temp) > Math.random ()) {
currentSolution = nuovo Tour (newSolution.getTour());
}
// Tieni traccia della migliore soluzione trovata
if (currentSolution.getDistance () < migliore.getDistance ()) {
migliore = nuovo Tour (currentSolution.getTour());
}
// Sistema freddo
temp * = 1-coolingRate;
}
Sistema.fuori.println (“Distanza soluzione finale:” + migliore.getDistance ());
Sistema.fuori.println(“il Tour:” + meglio);
}
}
di Uscita
soluzione Finale distanza: 911
Tour: |180, 200/200, 160/140, 140/180, 100/180, 60/200, 40/160, 20/120, 80/100, 40/60, 20/20, 20/20, 40/60, 80/100, 120/40, 120/20, 160/60, 200/80, 180/100, 160/140, 180|
Conclusione
In questo esempio siamo stati in grado di più di metà della distanza iniziale generato in modo casuale del percorso. Si spera che questo mostri quanto sia utile questo algoritmo relativamente semplice quando applicato a determinati tipi di problemi di ottimizzazione.
Autore
Sono uno sviluppatore dal Regno Unito che ama la tecnologia e il business. Qui troverete articoli e tutorial su cose che mi interessano. Se vuoi assumermi o saperne di più su di me vai alla mia pagina su di me