def full_hill_climb(mtx, R=2, multithread=False): NodesA, NodesB = mtx.shape old_nodf = -100.0 count = 0 while (old_nodf < tb.nodf(mtx)): count = count + 1 old_nodf = tb.nodf(mtx) if (multithread): mtx = hill_climb_step(mtx, R=R) else: mtx = hill_climb_step_st(mtx, R=R) return mtx
def hill_climb(mtx, depth=2): NodesA, NodesB = mtx.shape old_nodf = -100.0 new_nodf = tb.nodf(mtx) old_mtx = np.copy(mtx) counter = 0 while (new_nodf > old_nodf and counter < 5): counter = counter + 1 old_mtx = np.copy(mtx) old_nodf = new_nodf for i in range(depth): mtx = greedy_remove_link(mtx) for i in range(depth): mtx = greedy_add_link(mtx) new_nodf = tb.nodf(mtx) return old_mtx
def hill_climb_step_st(mtx, R): NodesA, NodesB = mtx.shape oPosList = tb.get_valid_ones(mtx) # initialise nodf computations opt_mtx = np.copy(mtx) opt_nodf = tb.nodf(mtx) # List positions to test: posSwapList = [] for oidx in range(len(oPosList)): oPos = oPosList[oidx] xpos, ypos = oPos for xshift, yshift in zip(range(-R, R + 1), range(-R, R + 1)): # Test if new position is valid: xnew = xpos + xshift ynew = ypos + yshift if (xnew >= 0 and xnew < NodesA and ynew >= 0 and ynew < NodesB): # Test if there is a 0 if (mtx[xnew, ynew] == 0.0): # perform the swap: zPos = [xnew, ynew] posSwapList.append([oPos, zPos]) opt_mtx, opt_nodf = hill_climb_test_positions(mtx, posSwapList) mtx = np.copy(opt_mtx) return mtx
def sim_anneal_opt(mtx, alpha = 0.99, iters = 32, init_temp = 0.01, min_temp = 10**(-4)): """ A Simulated annealing algorithm. The default values are copied from my supervisors implementation. Given these defaults the user only needs to supply a initial solution [mtx_0] to get a well performing simulated annealing algorithm. Other parameters: alpha: cooling factor iters: the number of iterations at each temp level bar: show a progress bar Output will be the optimal solution encountered in the whole algorithm. Rest is clear. """ NodesA, NodesB = mtx.shape tot_steps = int(np.ceil(np.log(min_temp / init_temp) / np.log(alpha))) * iters cool_steps = int(np.ceil(np.log(min_temp / init_temp) / np.log(alpha))) # Initialise optimal parameters: opt_mtx = np.copy(mtx) opt_nodf = tb.nodf(opt_mtx) opt_cost = cost(opt_nodf) temp = init_temp for i in tqdm(range(cool_steps)): # Use multithreading to speed this step up. [new_mtx, new_cost, sums] = sim_anneal_step(mtx, temp, iters) # print(1.0 - opt_cost, temp, tb.nodf(mtx)) if(new_cost < opt_cost): # Found a new optimal matrix. opt_mtx = np.copy(new_mtx) # Try hillclimbing to improve it! opt_mtx = hill_climb.full_hill_climb(opt_mtx, multithread = True) # Update optimal parameters opt_nodf = tb.nodf(opt_mtx) opt_cost = cost(opt_nodf) # Test to see if we should go back to the opt. solution acc_prob = tb.acceptProb(opt_cost, new_cost, temp) if(np.random.random() > acc_prob): # Go back to the optimal solution: mtx = np.copy(opt_mtx) temp = temp * alpha return opt_mtx
def hill_climb_test_positions(mtx, posSwapList): MT, F, DM, ND, S = tb.init_nodf(mtx) opt_mtx = np.copy(mtx) opt_nodf = tb.nodf(mtx) for oPos, zPos in posSwapList: n1, S = tb.nodf_one_link_removed(mtx, MT, F, DM, ND, S, oPos) n1, S = tb.nodf_one_link_added(mtx, MT, F, DM, ND, S, zPos) if (n1 > opt_nodf): # Update the optimal matrix and params opt_mtx = np.copy(mtx) opt_nodf = n1 n1, S = tb.nodf_one_link_removed(mtx, MT, F, DM, ND, S, zPos) a, S = tb.nodf_one_link_added(mtx, MT, F, DM, ND, S, oPos) return opt_mtx, opt_nodf
def hill_climb_step(mtx, R): NodesA, NodesB = mtx.shape oPosList = tb.get_valid_ones(mtx) # initialise nodf computations opt_mtx = np.copy(mtx) opt_nodf = tb.nodf(mtx) # List positions to test: posSwapList = [] for oidx in range(len(oPosList)): oPos = oPosList[oidx] xpos, ypos = oPos for xshift, yshift in zip(range(-R, R + 1), range(-R, R + 1)): # Test if new position is valid: xnew = xpos + xshift ynew = ypos + yshift if (xnew >= 0 and xnew < NodesA and ynew >= 0 and ynew < NodesB): # Test if there is a 0 if (mtx[xnew, ynew] == 0.0): # perform the swap: zPos = [xnew, ynew] posSwapList.append([oPos, zPos]) processes = multiprocessing.cpu_count() posLists = [[] for i in range(processes)] for i in range(len(posSwapList)): j = i % processes posLists[j].append(posSwapList[i]) argList = [] for i in range(processes): argList.append([np.copy(mtx), posLists[i]]) with Pool(processes) as pool: resList = pool.starmap(hill_climb_test_positions, argList, chunksize=32) for new_mtx, new_nodf in resList: if (new_nodf > opt_nodf): opt_nodf = new_nodf opt_mtx = np.copy(new_mtx) mtx = np.copy(opt_mtx) return mtx
def sim_anneal_step(mtx, temp, iters): """ One step of the simmulated annealing algorithm. Supply a initial solution and nodf meta-data. Output will be the optimal matrix found in this process along with it's cost """ # Do the reqired dice rolls for this algorithm vectorised and in advance: decision = np.random.rand(iters) MT, F, DM, ND, S = tb.init_nodf(mtx) opt_mtx = np.copy(mtx) opt_cost = cost(tb.nodf(mtx)) old_cost = opt_cost ones = tb.get_valid_ones(mtx) zeros = tb.get_promising_zeros(mtx) opt_time = -1 for i in range(iters): o_idx = np.random.randint(len(ones)) z_idx = np.random.randint(len(zeros)) o_pos = ones[o_idx] z_pos = zeros[z_idx] # compute the new new_nodf, S = neighbor_nodf(mtx, MT, F, DM, ND, S, o_pos, z_pos) new_cost = cost(new_nodf) if(new_cost < opt_cost): opt_cost = new_cost opt_mtx = np.copy(mtx) opt_time = i acc_prob = tb.acceptProb(old_cost, new_cost, temp) if(decision[i] <= acc_prob): # accept the new solution by not changing it back old_cost = new_cost # modify the zero and ones list accordingly: # ones = tb.get_valid_ones(mtx) ones = tb.get_valid_ones(mtx) zeros[z_idx] = o_pos else: # reject the new solution by reverting back a, S = neighbor_nodf(mtx, MT, F, DM, ND, S, z_pos, o_pos) # old_cost will not be updated! return [opt_mtx, opt_cost, S]
opt_mtx, opt_nodf = hill_climb_test_positions(mtx, posSwapList) mtx = np.copy(opt_mtx) return mtx def full_hill_climb(mtx, R=2, multithread=False): NodesA, NodesB = mtx.shape old_nodf = -100.0 count = 0 while (old_nodf < tb.nodf(mtx)): count = count + 1 old_nodf = tb.nodf(mtx) if (multithread): mtx = hill_climb_step(mtx, R=R) else: mtx = hill_climb_step_st(mtx, R=R) return mtx if (__name__ == "__main__"): NodesA = 14 NodesB = 13 Edges = 52 mtx1 = greedySolver2.greedySolve(NodesA, NodesB, Edges) mtx2 = np.copy(mtx1) mtx1 = full_hill_climb(mtx1, R=2) mtx2 = full_hill_climb(mtx2, R=3) nodf1 = tb.nodf(mtx1) nodf2 = tb.nodf(mtx2) print("NODF: {} -> {}".format(nodf1, nodf2))
for i in tqdm(range(cool_steps)): # Use multithreading to speed this step up. [new_mtx, new_cost, sums] = sim_anneal_step(mtx, temp, iters) # print(1.0 - opt_cost, temp, tb.nodf(mtx)) if(new_cost < opt_cost): # Found a new optimal matrix. opt_mtx = np.copy(new_mtx) # Try hillclimbing to improve it! opt_mtx = hill_climb.full_hill_climb(opt_mtx, multithread = True) # Update optimal parameters opt_nodf = tb.nodf(opt_mtx) opt_cost = cost(opt_nodf) # Test to see if we should go back to the opt. solution acc_prob = tb.acceptProb(opt_cost, new_cost, temp) if(np.random.random() > acc_prob): # Go back to the optimal solution: mtx = np.copy(opt_mtx) temp = temp * alpha return opt_mtx if(__name__ == "__main__"): NodesA = 131 NodesB = 666 Edges = 2933 mtx = greedySolver2.greedySolve(NodesA, NodesB, Edges) mtx = hill_climb.full_hill_climb(mtx) mtx = sim_anneal_opt(mtx) nodf = tb.nodf(mtx) print(mtx.sum()) print("NODF = {}".format(nodf))
import pandas as pd import algSimulatedAnneal import toolbox as tb # This script solves optimises the NODF-metric for the # network considered in the paper. This purpose of this # script is to make the algorithm accessible and reproduction # of our results easy. if __name__ == "__main__": # Read in the list of the network configurations considered. fname = "webs.csv" DF = pd.read_csv(fname) for index, row in DF.iterrows(): rows = row['rows'] cols = row['columns'] links = row['links'] NODFsong = row['song'] print("Simulating web with config ({}, {}, {}).".format( rows, cols, links)) mtx = algSimulatedAnneal.optimise(rows, cols, links) NODFSa = tb.nodf(mtx) print("NODF_song = {}\t, NODF_SA = {}.".format(NODFsong, NODFSa))
import greedySolver2 import simulatedAnnealing import time import toolbox # This script contains an algorithm to optimise the NODF # metric by computing a first estimate for the marginal totals # and then applying a greedy hill climb approach to improve # the result. def optimise(NodesA, NodesB, Edges, verbose=True): mtx = greedySolver2.greedySolve(NodesA, NodesB, Edges) bestMTX = simulatedAnnealing.sim_anneal_opt(mtx) return bestMTX if __name__ == "__main__": NodesA = 131 NodesB = 666 Edges = 2933 t1 = time.time() mtx = optimise(NodesA, NodesB, Edges) t2 = time.time() print(toolbox.nodf(mtx), t2 - t1)
def greedy_gen(params): """ Generate an initial graph with the intention of using it later in a more sophisticated algorithm like simmulated annealing or a genetic algorithm. This function is not deterministic. It rather tests all potential positions for a new entry and then adds one using the nodf values to create a probability distribution. """ NodesA, NodesB, Edges = params #Initial fill mtx = np.zeros((NodesA, NodesB)) mtx[0,:] = 1.0 mtx[:,0] = 1.0 mtx[1,1] = 1.0 edges_left = Edges - NodesA - NodesB MT, F, DM, ND, S = tb.init_nodf(mtx) for i in range(edges_left): #greedy add a link greedyAddLink(mtx, MT, F, DM, ND, S, edges_left - i) return mtx if(__name__ == "__main__"): NodesA = 131 NodesB = 666 Edges = 2933 mtx = greedySolve(NodesA, NodesB, Edges, verbose = False) print(mtx.sum()) print(tb.nodf(mtx))