Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
154 views
in Technique[技术] by (71.8m points)

python - Why is this simulated annealing algorithm applied to the TSP not converging?

For an exercise I have to find the optimal solution to this Travelling Salesman Problem, only difference is that the salesman may not visit the first coordinate twice. The optimum should lie around 1200 which is given. I don't see why this wouldn't converge. Also an important note is that the manhattan distance metric is used instead of the euclidian distance.

def shuffle_coords(coords):
     x0 = coords[0]
     coords = shuffle(coords[1:])
     return np.insert(coords,0,x0,axis = 0)

def distance(x,y):
    return abs(x[1] - y[1]) + abs(x[0] - y[0])

Here the coordinates are shuffled randomly.

def shuffle(array):
    df = pd.DataFrame(array)
    return df.sample(len(array)).to_numpy() 


def path_distance(path):
    dist     = []
    for i in range(1,len(path)):
        dist.append(distance(path[i],path[i-1]))
    return np.sum(dist)

Here the Simulated Annealing algorithm is initialised.

def SA_distance(path, T_0,T_min, alpha):
    T = T_0
    dist = path_distance(path)
    
    while T >  T_min:
        new_path = gen_subtour(path)
        diffF = path_distance(new_path) - dist
        if diffF < 0:
            path = new_path
            dist = path_distance(path)
 
        elif np.exp(-(diffF/T)) > random.uniform(0,1):
            path = new_path
            dist = path_distance(path)
      
        T = T * alpha
        print(dist,T)
    return dist,path

Here the random subtour is generated while keeping x0 in place.

def gen_subtour(path):
    subset = shuffle(np.delete(path,0,axis =0))
    subset = shuffle(path)
    if random.uniform(0,1) < 0.5:
        subset = np.flipud(subset)
    else:
        j = random.randint(1,(len(subset)-1))
        p = subset[j-1]
        q = subset[j]
        subset = np.delete(subset,[j-1,j],axis = 0)
        subset = np.insert(subset,0,p,axis = 0)
        subset = np.insert(subset,len(subset),q,axis = 0)
 
    return np.insert(subset,0,path[0],axis = 0)

def main():
    T_0     = 12
    T_min   = 10**-9
    alpha   =  0.999
    coords = np.array([[375, 375],[161, 190], [186, 169],[185, 124],
                       [122, 104],[109, 258], [55, 153] ,[120, 49],
                       [39, 85]  ,[59, 250] , [17, 310] ,[179, 265],
                       [184, 198]])
    path , distance = SA_distance(coords,T_0,T_min,alpha)
question from:https://stackoverflow.com/questions/65890955/why-is-this-simulated-annealing-algorithm-applied-to-the-tsp-not-converging

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I have tested, and my first instinct is that the gen_subtour() is not working well, maybe it is changing the route with too many steps.

I would try different versions and check how the effect it. The SA scheme seem to be working well, it is the proposals where I think the error is.

Anyway, here some code that hopefully will help you test better.

I use the pdist to pre-calculate manhatten distance, i.e;

import numpy as np
from scipy.spatial.distance import pdist, cdist, squareform

coords = np.array([[375, 375],[161, 190], [186, 169],[185, 124],
                   [122, 104],[109, 258], [55, 153] ,[120, 49],
                   [39, 85]  ,[59, 250] , [17, 310] ,[179, 265],
                   [184, 198]])

Y = pdist(coords, 'cityblock')

distance_matrix = squareform(Y)
nodes_count = coords.shape[0]

And define start with

def random_start():
    """
        Random start, returns a state
    """
    a = np.arange(0,nodes_count)
    np.random.shuffle(a)
    return a

The objective function, where we have the version that we don't return to the origin;

def objective_function( route ):
    # uncomment when testing new/modify neighbors
    # assert check_all_nodes_visited(route)

    return np.sum( distance_matrix[route[1:],route[:-1]] )

I have here 3 kinds of proposals, based on a route;

def random_swap( route ):
    """
        Random Swap - a Naive neighbour function

        Will only work for small instances of the problem
    """
    route_copy = route.copy()

    random_indici = np.random.choice( route , 2, replace = False)
    route_copy[ random_indici[0] ] = route[ random_indici[1] ]
    route_copy[ random_indici[1] ] = route[ random_indici[0] ]

    return route_copy

def vertex_insert( route, nodes=1 ):
    """
        Vertex Insert Neighbour, inspired by

        http://www.sciencedirect.com/science/article/pii/S1568494611000573
    """
    route_copy = route.copy()
    random_indici = np.random.choice( route , 2, replace = False)
    index_of_point_to_reroute = random_indici[0]
    value_of_point_to_reroute = route[ random_indici[0] ]
    index_of_new_place = random_indici[1]
    route_copy = np.delete(route_copy, index_of_point_to_reroute)
    route_copy = np.insert(route_copy, index_of_new_place, values=value_of_point_to_reroute)
    return route_copy

def block_reverse( route, nodes=1 ):
    """
        Block Reverse Neighbour, inspired by

        http://www.sciencedirect.com/science/article/pii/S1568494611000573

        Note that this is a random 2-opt operation.
    """
    route_copy = route.copy()
    random_indici = np.random.choice( route , 2, replace = False)
    index_of_cut_left = np.min(random_indici)
    index_of_cut_right = np.max(random_indici)
    route_copy[ index_of_cut_left:index_of_cut_right ] = np.flip(route_copy[ index_of_cut_left:index_of_cut_right ])

    return route_copy

Optionally, you can to a 2-opt round after the SA, to make sure there are no crossing.

def swap_for_2opt( route, i, k):
    """
        Helper for 2-opt search
    """
    route_copy = route.copy()
    index_of_cut_left = i
    index_of_cut_right = k
    route_copy[ index_of_cut_left:index_of_cut_right ] = np.flip(route_copy[ index_of_cut_left:index_of_cut_right ])

    return route_copy

def local_search_2opt( route ):
    """
        Local Optimum with 2-opt

        https://en.wikipedia.org/wiki/2-opt

    """
    steps_since_improved = 0
    still_improving = True

    route = route.copy()

    while still_improving :
        for i in range( route.size - 1 ):
            for k in np.arange( i + 1, route.size ):
                alt_route = swap_for_2opt(route, i, k)

                if objective_function(alt_route) < objective_function(route):
                    route = alt_route.copy()
                    steps_since_improved = 0

            steps_since_improved += 1

            if steps_since_improved > route.size + 1:
                still_improving = False
                break

    return route

And use frigidum for SA

import frigidum


local_opt = frigidum.sa(random_start=random_start,
           objective_function=objective_function,
           neighbours=[random_swap, vertex_insert, block_reverse],
           copy_state=frigidum.annealing.naked,
           T_start=10**5,
           alpha=.95,
           T_stop=0.001,
           repeats=10**2,
           post_annealing = local_search_2opt)

The returned route is almost always 1145.

I have posted general hints and tips on frigidum homepage.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...