運籌系列39:ALNS使用

1. ALNS介紹

ALNS(Adaptive Large Neighborhood Search)是現在routing和scheduling裏面用的很多的啓發式算法,發表於2010年。其基本思路是不斷destroying和repairing問題。
定義XX爲問題II的可行解集,c(X)表示要優化的目標。我們從一個初始解x1出發,搜索N(x1)領域範圍內的最優值x2,然後再搜索N(x2)範圍內的最優值x3……,這是最優梯度下降法。
第一個需要關注的問題就是領域N如何定義。在VRP問題中,2-opt算子和relocate算子可以到達的新解稱爲其領域。領域的大小定義了算法複雜度,例如剛剛兩個算法都是n方複雜度。
ALNS的核心是destroy和repair構成的領域,如下圖,很好理解。在這裏插入圖片描述
destroy階段,我們可以按照一定的規則優先destroy有問題的局部點,例如上述CVRP問題中的交錯的路線,再比如特別長的路線,這是worst destroy或者叫做critical destroy,完全隨機的叫做random destroy,根據歷史信息來的叫做history based destroy。
repair階段或者是使用啓發式算法,也可以使用精確求解算法。
ALNS與一般LNS不同的地方在於,會使用多種destroy和search方法,每種方法以一定的概率出現。

2. 例子與代碼

這裏介紹github上star最多的python框架。使用pip install alns 安裝此框架。重點如下:

  • 兩個基本類,ALNS用於運行程序,State用於存儲解
  • objective用於定義目標函數
  • alns.criteria來判斷每次的解是否接受,已實現的包括①HillClimbing:myopic的算法;RecordToRecordTravel:設置了update的條件;SimulatedAnnealing:概率大於隨機產生數時進行更新。

介紹兩個例子,TSP問題和CSP問題,來說明使用方法。

2.1 TSP問題

安裝tsplib95庫,求解其中的xqf131.tsp問題。

from alns import ALNS, State
from alns.criteria import HillClimbing
import copy
import itertools
import numpy.random as rnd
import networkx as nx
import tsplib95
import tsplib95.distances as distances
import matplotlib.pyplot as plt
data = tsplib95.load_problem('xqf131.tsp')

# These we will use in our representation of a TSP problem: a list of
# (city, coord)-tusples.
cities = [(city, tuple(coord)) for city, coord in data.node_coords.items()]
solution = tsplib95.load_solution('xqf131.opt.tour')
optimal = data.trace_tours(solution)[0]

定義state,裏面保存了當前解(點和邊)

class TspState(State):
    def __init__(self, nodes, edges):
        self.nodes = nodes
        self.edges = edges

    def copy(self):
        return copy.deepcopy(self)

    def objective(self):
        return sum(distances.euclidean(node[1], self.edges[node][1]) for node in self.nodes)
    
    def to_graph(self):
        graph = nx.Graph()

        for node, coord in self.nodes:
            graph.add_node(node, pos=coord)
 
        for node_from, node_to in self.edges.items():
            graph.add_edge(node_from[0], node_to[0])

        return graph

數據結構是這樣的:

在這裏插入圖片描述
在這裏插入圖片描述

下面定義destroy算子,注意輸入數據,第一項是當前解,第二個是隨機數發生器。

degree_of_destruction = 0.25
# 每次要destroy的算子個數
def edges_to_remove(state):
    return int(len(state.edges) * degree_of_destruction)
def random_removal(current, random_state):
    destroyed = current.copy()    
    for idx in random_state.choice(len(destroyed.nodes),
                                   edges_to_remove(current),
                                   replace=False):
        del destroyed.edges[destroyed.nodes[idx]]

    return destroyed

下面定義repair算子。greedy_repair輸入第一個是當前解,第二個是隨機數發生器。
would_form_subcycle是爲了阻止形成環。

def would_form_subcycle(from_node, to_node, state):
    for step in range(1, len(state.nodes)):
        if to_node not in state.edges:
            return False

        to_node = state.edges[to_node]
        
        if from_node == to_node and step != len(state.nodes) - 1:
            return True

    return False
    
def greedy_repair(current, random_state):
    visited = set(current.edges.values())
    shuffled_idcs = random_state.permutation(len(current.nodes))
    nodes = [current.nodes[idx] for idx in shuffled_idcs]

    while len(current.edges) != len(current.nodes):
        node = next(node for node in nodes 
                    if node not in current.edges)
        unvisited = {other for other in current.nodes
                     if other != node
                     if other not in visited
                     if not would_form_subcycle(node, other, current)}

        # Closest visitable node.
        nearest = min(unvisited,
                      key=lambda other: distances.euclidean(node[1], other[1]))

        current.edges[node] = nearest
        visited.add(nearest)
    return current

初始化問題,注意alns的使用方法,隨機數發生器時刻備着,定義完之後緊接着加入destroy operator和repair operator,並使用iterate方法加入criterion和initial_solution。其中第二個參數是更新策略的權值,4個數分別表示獲得destroy算子裏獲得全局最優、獲得一步最優,以及repair算子裏接受、拒絕的概率。

random_state = rnd.RandomState(SEED)
state = TspState(cities, {})
initial_solution = greedy_repair(state, random_state)
alns = ALNS(random_state)
alns.add_destroy_operator(random_removal)
alns.add_repair_operator(greedy_repair)
criterion = HillClimbing()
result = alns.iterate(initial_solution, [3, 2, 1, 0.5], 0.8, criterion,
                      iterations=5000, collect_stats=True)
solution = result.best_state
objective = solution.objective()
print('Best heuristic objective is {0}.'.format(objective))
print('This is {0:.1f}% worse than the optimal solution, which is {1}.'
      .format(100 * (objective - optimal) / optimal, optimal))

_, ax = plt.subplots(figsize=(12, 6))
result.plot_objectives(ax=ax, lw=2)

在這裏插入圖片描述

2.2 cutting stock problem

import copy

from functools import partial

import matplotlib.pyplot as plt
import numpy as np
import numpy.random as rnd

from alns import ALNS, State
from alns.criteria import HillClimbing
SEED = 5432
with open('640.csp') as file:
    data = file.readlines()

NUM_LINES = int(data[0])
BEAM_LENGTH = int(data[1])

# Beams to be cut from the available beams
BEAMS = [int(length)
         for datum in data[-NUM_LINES:]
         for length, amount in [datum.strip().split()]
         for _ in range(int(amount))]

print("Each available beam is of length:", BEAM_LENGTH)
print("Number of beams to be cut (orders):", len(BEAMS))
class CspState(State):
    """
    Solution state for the CSP problem. It has two data members, assignments
    and unassigned. Assignments is a list of lists, one for each beam in use.
    Each entry is another list, containing the ordered beams cut from this 
    beam. Each such sublist must sum to at most BEAM_LENGTH. Unassigned is a
    list of ordered beams that are not currently assigned to one of the
    available beams.
    """

    def __init__(self, assignments, unassigned=None):
        self.assignments = assignments
        self.unassigned = []
        
        if unassigned is not None:
            self.unassigned = unassigned

    def copy(self):
        """
        Helper method to ensure each solution state is immutable.
        """
        return CspState(copy.deepcopy(self.assignments),
                        self.unassigned.copy())

    def objective(self):
        """
        Computes the total number of beams in use.
        """
        return len(self.assignments)

    def plot(self):
        """
        Helper method to plot a solution.
        """
        _, ax = plt.subplots(figsize=(12, 6))

        ax.barh(np.arange(len(self.assignments)), 
                [sum(assignment) for assignment in self.assignments], 
                height=1)

        ax.set_xlim(right=BEAM_LENGTH)
        ax.set_yticks(np.arange(len(self.assignments), step=10))

        ax.margins(x=0, y=0)

        ax.set_xlabel('Usage')
        ax.set_ylabel('Beam (#)')

        plt.draw_if_interactive()

def wastage(assignment):
    """
    Helper method that computes the wastage on a given beam assignment.
    """
    return BEAM_LENGTH - sum(assignment)

degree_of_destruction = 0.25

def beams_to_remove(num_beams):
    return int(num_beams * degree_of_destruction)

def random_removal(state, random_state):
    """
    Iteratively removes randomly chosen beam assignments.
    """
    state = state.copy()

    for _ in range(beams_to_remove(state.objective())):
        idx = random_state.randint(state.objective())
        state.unassigned.extend(state.assignments.pop(idx))

    return state

def greedy_insert(state, random_state):
    """
    Inserts the unassigned beams greedily into the first fitting
    beam. Shuffles the unassigned ordered beams before inserting.
    """
    random_state.shuffle(state.unassigned)

    while len(state.unassigned) != 0:
        beam = state.unassigned.pop(0)

        for assignment in state.assignments:
            if beam <= wastage(assignment):
                assignment.append(beam)
                break
        else:
            state.assignments.append([beam])

    return state

rnd_state = rnd.RandomState(SEED)
state = CspState([], BEAMS.copy())
initial_solution = greedy_insert(state, rnd_state)
print("Initial solution has objective value:", initial_solution.objective())
alns = ALNS(rnd_state)
alns.add_destroy_operator(random_removal)
alns.add_repair_operator(greedy_insert)
criterion = HillClimbing()
result = alns.iterate(initial_solution, [3, 2, 1, 0.5], 0.8, criterion,
                      iterations=5000, collect_stats=True)
solution = result.best_state
objective = solution.objective()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章