Evolutionary Computing: Notebook assignment - Traveling Salesman Problem 練習隨筆

星宫奏發表於2024-09-09

本次練習基於EC演算法經典的旅行商TSP問題

練習目標:

在所給的架構下編寫並執行一個正確的進化演算法,並解決經典的旅行商TSP問題。在該練習條件下,當每一次旅行商的旅行距離均低於13500公里時,則代表所編寫之演算法有效。

有兩點額外要求:(1) 演算法中人口規模Population必須 ≤100。(2) 迭代次數 ≤1000

實驗中可以自由更改超引數,如突變/交叉機率(後續程式碼中提及)。

Traveling Salesman Problem

在本例中,我們學習如何使用EA來解決旅行推銷員的問題。在這個問題中,一個推銷員需要完成以最短的方式訪問所有城市並返回出發點的任務。此外,我們假設旅行推銷員有一個固定的起點和終點(阿姆斯特丹)。因此,有兩個約束:

-每個城市都需要被訪問到。
-旅行者以家作為起點和終點(本例中以阿姆斯特丹為例)。

在該練習中,我們將使用以下城市,並使用以下編碼:

-0:阿姆斯特丹
-1:雅典
-2:柏林
-3:布魯塞爾
-4:哥本哈根
-5:愛丁堡
-6:里斯本
-7:倫敦
-8:馬德里
-9:巴黎

在本例中,我們使用鄰接表來表示城市之間的距離,其中Aij表示從城市i到城市j的距離,透過查詢我們可以得到上述城市間的距離鄰接表,透過該連結可以查到https://www.engineeringtoolbox.com/driving-distances-d_1029.html

adjacency_mat = np.asarray(
    #Remember that we use the encoding above, i.e. 0 refers to Amsterdam and 10 to Paris!
    [
        [0, 3082, 649, 209, 904, 1180, 2300, 494, 1782, 515], # Distance Amsterdam to the other cities
        [3082, 0, 2552, 3021, 3414, 3768, 4578, 3099, 3940, 3140], # Distance Athens to the other cities
        [649, 2552, 0, 782, 743, 1727, 3165, 1059, 2527, 1094], # Distance Berlin to the other cities
        [209, 3021, 782, 0, 1035, 996, 2080, 328, 1562, 294], # Distance Brussels to the other cities
        [904, 3414, 743, 1035, 0, 1864, 3115, 1196, 2597, 1329], # Distance Copenhagen to the other cities
        [1180, 3768, 1727, 996, 1864, 0, 2879, 656, 2372, 1082], # Distance Edinburgh to the other cities
        [2300, 4578, 3165, 2080, 3115, 2879, 0, 2210, 638, 1786], # Distance Lisbon to the other cities
        [494, 3099, 1059, 328, 1196, 656, 2210, 0, 1704, 414], # Distance London to the other cities
        [1782, 3940, 2527, 1562, 2597, 2372, 638, 1704, 0, 1268], # Distance Madrid to the other cities
        [515, 3140, 1094, 294, 1329, 1082, 1786, 414, 1268, 0] # Distance Paris to the other cities
    ]

)

對於進化演算法,首先我們需要完成Fitness Function(適應度函式)的編寫。在本例中,適應度函式將旅行者行駛的總公里數作為適應度的衡量標準是直觀的。給定路線覆蓋的總公里數越低越好,因此我們編寫如下fitness_function

 1 def compute_distance(route: list, adjacency_mat: np.ndarray) -> int:
 2     '''
 3     Calculates the total number of kilometers for a given route.
 4     '''
 5     total_distance = 0 # Initialize the value of total distance 初始化路徑的值為0
 6     number_city = len(route) # Number of cities in the route 路徑中的城市數量
 7     '''
 8     ToDo:
 9 
10     Please complete the function that calculates the total distance for a given route!
11     '''
12 
13     # 我們首先遍歷路徑中的每對連續城市
14     for i in range(number_city - 1): # 因為終點城市使用i+1,且Python的range()所取值是一個左閉右開區間,所以這樣編寫函式能保證最後一個城市被涵蓋的同時不超出陣列邊界
15         start_city = route[i]
16         des_city = route[i + 1]
17         total_distance += adjacency_mat[start_city, des_city]
18     # 別忘了從最後一個城市返回到起點    
19     total_distance += adjacency_mat[route[-1], route[0]] # Calculate the distance from the last city back to the starting point
20     return total_distance

解釋:

  1. 迴圈遍歷路線中的每對連續城市

    • 透過 for i in range(num_cities - 1) 迭代路線中的每對相鄰城市。
    • route 中獲取起點和終點城市的索引,然後使用 adjacency_mat[from_city, to_city] 查詢它們之間的距離並將其新增到 total_distance
  2. 處理回到起點的情況

    • 在旅行商問題中,路徑通常是迴圈的,所以在計算完所有相鄰城市之間的距離後,還需要計算從最後一個城市回到起點的距離,並將其新增到總距離中。
  3. 返回總距離

    • 最後返回計算的 total_distance

教材內容補充:

Evaluation/ fitness function

Role:

  • Fitness fuction represents the task to solve, the requirements to adapt to (can be seen as 'the environment') 適應度函式代表了要解決的任務、要適應的要求(可以看作是“環境”)
  • Enable Selection 使得後續的演算法中可以對樣本進行選擇
  • If some phenotypic traits are advantegous, desirable, then these traits are rewared by more offspring that will expectedly carry the same trait 如果一些表型特徵是有利的、可取的,那麼這些特徵會被更多的後代所認識(表現為適應度高),這些後代有望攜帶相同的特徵

  Assgin a single real-valued fitness to each phenotype which forms the basis for selection 為每個表型提供一個單一的實值適應度,作為選擇的基礎 (所以不同的值越多越好 the more discrimination[different values] the better)

  Typically we talk about fitness being maximised 通常我們討論如何讓適應值最大,但某些問題則是適應度越小越好(比如本例)

完成Fitness function的編寫後,下一步我要做的是從一代個體中找出表現最佳的個體(於本例而言,距離最短的)

def fittest_solution_TSP(compute_distance, generation: list, adjancency_mat: np.ndarray) -> tuple:
    '''
    This function calculates the fitness values of all individuals of a generation.
    It then returns the best fitness value (integer) and the corresponding individual (list).
    該函式計算一代中所有個體的適應度值,並返回最佳適應度值(整數)及其對應的個體(列表)。
    '''
        
    '''
    ToDo:

    Please complete the function!
    '''
    
    #WRITE YOUR CODE HERE!
    best_fitness_value = float("inf") # 初始化為無限大,表示我們尋找最小值
    best_individual = None # 初始化最佳路線
    
    # Calculate the fitness value of the current individual
    
    for indiv in generation:
        # 計算當前個體的適應度值(即總距離)用到了上一步編寫的適應度函式(計算路徑距離)
        current_fitness_value = compute_distance(indiv,adjacency_mat)
        # If the current individual's fitness value is better than the best fitness value found so far, update it
        # 如果當前個體的適應度值比目前找到的最佳適應度值更好,則更新
        
        if current_fitness_value <= best_fitness_value:
            best_fitness_value = current_fitness_value
            best_individual = indiv
            
    return best_fitness_value,best_individual

解釋:

  1. 初始化最佳值

    • best_fitness_value 被初始化為 float('inf'),這是一個極大的值。我們要尋找最小的距離,因此需要從一個極大的初始值開始。
    • best_individual 被初始化為 None,以便在找到最佳路徑時更新。
  2. 遍歷種群

    • 對於 generation 中的每一個個體(路徑),計算其適應度值(即透過呼叫 compute_distance(indiv, adjacency_mat) 計算總距離)。
  3. 更新最佳路徑

    • 如果當前個體的總距離小於當前的 best_fitness_value,則更新 best_fitness_valuebest_individual,記錄這個更優的個體。
  4. 返回值

    • 返回找到的最小總距離 best_fitness_value 和對應的路徑 best_individual

這個函式的核心是遍歷整個種群,找出在這代中最優的解決方案。輸出的最佳距離及其路徑可用於進一步的演算法最佳化或作為最終結果。

教材內容補充:

Selection

Role:

  • Identifies individuals 識別辨別個體 (成為親本、生存、淘汰等等)
  • Pushes population towards higher fitness 推動人口世代向更高的適應度水平擬合
  • Usally probabilistic/stochastic 通常為機率/隨機
    • high quality solution more likely be selected than low quality solutions 高質量解比低質量解更容易被選中
    • but no guaranteed 並不一定是高質量的被選中
    • even the worst in current population usually has non-zero pobability of being selected 最差的解被選中的機率也不是0
  • This stochastic nature can help escape from local optima 隨機性幫助擺脫區域性最優解

Assgin a single real-valued fitness to each phenotype which forms the basis for selection 為每個表型提供一個單一的實值適應度,作為選擇的基礎 (所以不同的值越多越好 the more discrimination[different values] the better)

  Typically we talk about fitness being maximised 通常我們討論如何讓適應值最大,但某些問題則是適應度越小越好(比如本例)

定義了適應度函式後,我們需要一個函式來初始化(生成)我們的解決方案。

def initialize_population(n_population: int) -> list:
    '''This returns a randomly initialized list of individual solutions of size n_population.'''

    population = []
    # Based on a literature review, the suggested range for population size
    # is between one and two times the path length. i.e len(route) <= n <= 2*len(route)
    '''
    ToDo:

    Please complete the function!
    '''
    for _ in range(n_population):
        # 建立一個包含城市 1 到 9 的路徑
        fixed_prefix,fixed_suffix = [0],[0]
        default_list  = list(range(1, 10))  # 從城市 1 到 9(排除城市 0)
        random.shuffle(default_list)  # 隨機打亂這些城市的順序
        individual = fixed_prefix + default_list + fixed_suffix # 為城市列表新增固定的字首後字尾(即起終點都為阿姆斯特丹)
        population.append(individual)
        
    for individual in population: # 對這些新新增的個體進行檢查
        #Implement some assertion tests for checking if the individual meets criteria
        assert (individual[0] == individual[-1]==0), 'Make sure you start and end in Amsterdam' #起終點
        assert len(set(individual)) == 10, "Individual must contain all unique values from 0 to 9, as is must visit all cities" # 每個城市都訪問了
        #set(individual):
        #將 individual 列表轉換為一個集合 set。集合是一個無序的資料結構,自動去除重複的元素。
        #因此,如果 individual 列表中有重複的城市,集合會自動去掉這些重複項。
        assert (len(individual) == 11), "Individual must be length 11" # 長度必須為11 


    return population

initialize_population 函式中,我們的目標是生成一個具有 n_population 個體的初始種群。每個個體代表一個城市的訪問順序,路徑的長度需要包括返回起點的步驟(即從城市 0 出發,經過所有其他城市,最後返回到城市 0)。你需要確保每個個體滿足以下條件:

  1. 路徑以城市 0(阿姆斯特丹)開始並結束
  2. 路徑包含所有城市,且每個城市只出現一次
  3. 路徑的長度為 11(包括從起點城市 0 開始,經過所有其他城市,最後返回到城市 0)。

解釋:

  1. 生成城市路徑

    • deafult_list = list(range(1, 10)) 建立了一個包含城市 19 的列表(排除起點城市 0)。
    • random.shuffle(deafult_list) 隨機打亂這些城市的順序,以生成不同的訪問路徑。
  2. 建立個體路徑

    • individual = fixed_prefix + default_list + fixed_suffix
      在打亂的城市列表前後新增城市 0,表示從起點出發並最終返回到起點城市。
  3. 新增到種群

    • 將生成的個體路徑新增到 population 列表中。
  4. 斷言檢查

    • 確保每個個體以城市 0 開始並結束。
    • 確保每個個體包含所有城市,並且每個城市只出現一次。
    • 確保個體的路徑長度為 11。

這個實現會生成一個滿足指定條件的初始種群列表,並對其進行驗證。

現在我們已經定義了適應度函式和初始化人口的函式,我們需要定義變分運算元,即交叉 Cross-Over和突變 Mutation。對於排列問題,一般將問題分為如下兩類

順序先決的問題(生產問題)或元素相鄰的問題(鄰接)。在我們的TSP問題中,重要的是哪個元素彼此相鄰,即鄰接。

在這個 mutation 函式中,我們需要實現一個突變操作,以對給定的路徑 child 進行修改。突變是遺傳演算法中的一個關鍵步驟,可以幫助搜尋到新的解空間。這裡我們將討論幾種突變操作(如題所述),並在函式中實現其中一個或多個。我們還要確保突變後的路徑滿足特定條件,比如路徑仍然以阿姆斯特丹(城市0)開始和結束,且必須覆蓋所有城市。

突變操作選項

  1. Swap Operator(交換操作):

    • 隨機選擇路徑中的兩個城市,並交換它們的位置。
  2. Insert Operator(插入操作):

    • 隨機選擇路徑中的一個城市,並將其插入到路徑中的另一個位置。
  3. Scramble Operator(打亂操作):

    • 隨機選擇路徑中的一個子序列,並將其打亂。
  4. Inversion Operator(逆序操作):

    • 隨機選擇路徑中的一個子序列,並將其反轉。
def mutation(child:list, p_mutation:float) -> list:
    '''This applies a mutation operator to a list and returns the mutated list.'''

    if np.random.uniform() > p_mutation: #np.random.uniform(default low=0.0, high = 1.0) -> generate a float in the interval [0,1)
        #no mutation
        return child

    else:

      child_mutated = child[:] # Create a copy of the child to mutate
      # mutation_operator = np.random.choice(['swap', 'insert', 'scramble', 'inversion']) # Using mutation operator randomly
      mutation_operator = 'swap' # Using 'swap' as example
      '''
      ToDo:
      Please complete the function!
      '''
      if mutation_operator == 'swap':
          i,j = np.random.choice(range(1,len(child_mutated) - 1),size = 2,replace = False)
          child_mutated[i],child_mutated[j] = child_mutated[j],child_mutated[i] # Swap two random cities
          
      elif mutation_operator == 'insert':
          i = np.random.choice(range(1,len(child_mutated) - 1))
          insert_city = child_mutated.pop(i) # Pop a city from list randomly
          j = np.random.choice(range(1,len(child_mutated) - 1)) # Choose a new position from list to insert
          child_mutated.append(j, insert_city)
          
      elif mutation_operator == 'scramble':
          i,j = sorted(np.random.choice(range(1,len(child_mutated) - 1),size = 2,replace=False)) # Choose two numbers(Ascending sorted) as the start and end of the subsequence
          np.random.shuffle(child_mutated[i:j+1]) # Shuffle a random subsequence of the path
          # 為什麼是[i:j+1] 因為Python中切片是不包含右側最大值的即如果我們想要獲取的子序列是列表中的第i到第j個值,那麼我們切片時
          # child[i:j+1]:返回從索引 i 到索引 j 的元素,即 包含起點 i 和終點 j。
          
      elif mutation_operator == 'inversion':
          i,j = sorted(np.random.choice(range(1,len(child_mutated) - 1),size = 2,replace=False))# Choose two numbers(Ascending sorted) as the start and end of the subsequence
          child_mutated[i:j+1] = child_mutated[i:j+1][::-1] # Reverse the subsequence of the path
          # child[i:j+1][::-1] 這一表示式中的 [::-1] 是 Python 中的切片語法,用於反轉序列(如列表、字串等)。
          
          


      #Implement some assertion tests for checking if the mutation goes as expected
      assert (child_mutated[0] == child_mutated[-1] and child_mutated[0] == 0 ), 'Make sure you start and end in Amsterdam'
      assert len(set(child_mutated)) == 10, "Individual must contain all unique values from 0 to 9, as is must visit all cities"
      assert (len(child_mutated) == 11), "Individual must be length 11"

    return child_mutated

解釋

  • 突變的機率控制:我們首先使用 np.random.uniform() 來決定是否對個體進行突變。如果生成的隨機數大於 p_mutation,則不進行突變,直接返回原始的 child

  • 突變操作選擇:我們隨機選擇一種突變操作,分別為交換、插入、打亂和逆序。

  • 操作具體實現

    • 交換:隨機選擇兩個位置並交換它們。
    • 插入:將一個城市移除,然後插入到另一個隨機位置。
    • 打亂:選擇一個子序列並隨機打亂它。
    • 逆序:選擇一個子序列並將其順序反轉。
  • 斷言檢查:我們使用斷言確保突變後的路徑依然滿足所有的條件,特別是路徑以城市0開始和結束,並且所有城市都被訪問一次。

1. Scramble Operator(打亂操作)

目的:打亂路徑中的某個子序列,但不改變子序列中各個城市的出現頻率。

步驟

  1. 隨機選擇路徑中的兩個位置(ij),定義一個子序列。
  2. 將這個子序列的城市順序隨機打亂。
  3. 替換原路徑中的該子序列。

2. Inversion Operator(逆序操作)

目的:將路徑中的某個子序列反轉,使其順序顛倒。

步驟

  1. 隨機選擇路徑中的兩個位置(ij),定義一個子序列。
  2. 將這個子序列的順序逆轉。
  3. 替換原路徑中的該子序列。
sorted(np.random.choice(range(1,len(child_mutated) - 1),size = 2,replace=False))

關於上述程式碼的解釋,使用np.random.choice()函式,從child序列中選出子序列的開頭和末尾,因為是獲得list的索引,所以使用range(1,len(child_muted))能剛好覆蓋除了出發點、結尾點(阿姆斯特丹)以外的其他城市使用sorted()保證取出的第一個數比第二個小,後續不需要做順序調整,簡化程式碼

逆序補充

Python 的切片語法是 list[start:end:step],它允許我們從列表或其他序列型別中提取特定部分。各部分的含義如下:

  • start:切片開始的索引(包含)。
  • end:切片結束的索引(不包含)。
  • step:切片的步長,即每次從序列中取元素時的間隔。

[::-1] 的含義

  • startend 都為空時,表示從頭到尾遍歷整個序列。
  • step-1 表示以倒序的方式遍歷序列,因此 [::-1] 表示從序列的最後一個元素到第一個元素,按相反的順序返回。

swap交換補充

child_mutated[i],child_mutated[j] = child_mutated[j],child_mutated[i] # Swap two random cities

該行程式碼之所以在Python中不會出錯,是因為Python 中的多重賦值(multiple assignment)允許我們在同一行程式碼中同時更新多個變數的值。在這個過程中,右側的表示式首先被完全計算,然後再一次性地將計算結果賦值給左側的變數。左側的兩個值是在同一時刻交換的。右側的值是在賦值之前就已經計算完成,因此互換的過程中,兩個索引對應的值都已經被臨時儲存下來,不會因為某個賦值操作而改變另一個操作的結果。

*注意,該操作在JAVA中不可行,必須使用臨時變數儲存中間值

完成了變異運算元後,我們可以繼續定義交叉運算元。同樣,我們有多種選擇可供選擇:

  • 單點/多點交叉
  • 均勻交叉
  • 洗牌交叉
  • 部分對映交叉(PMX)
  • 邊緣重組交叉

由於我們的基因型表示的是具有特定邊界的排列,因此我們需要確保保留父代的特定屬性(節點之間的連線),並確保邊界條件仍然滿足(兩端為零,且每個數字1-9只出現一次)。

邊緣重組交叉非常適合本例的問題,因為它能保留元素之間的相鄰關係,並確保每個元素的數量相同。

1. 剝離零點

旅行商問題的路徑通常是環形的(起點和終點為同一個城市),在本問題中,城市 0 代表阿姆斯特丹。為了處理中間的城市,我們需要剝離城市 0,即從兩個父代中移除起始和終止的 0。同樣在此處,我們運用到了Python的切片[1:-1]表示取列表索引1到最後一位之前的值(因為切片右側不計入)完成對起點終點 0的剝離

# Strip the zeros (remove the starting and ending city which is 0)
parent_1_stripped = parent_1[1:-1]
parent_2_stripped = parent_2[1:-1]

2. 建立邊緣表

邊緣表是一個記錄每個城市與之相鄰城市的表,它將幫助我們決定如何繼承父代中的路徑結構。對於每個父代中的每個城市,我們記錄其左鄰和右鄰的城市。

#create an edge table
edge_table = {key: set() for key in parent_1_stripped} 

#parent_1_stripped 是一個可以迭代的物件(例如列表、字串等),它的每一個元素將成為 edge_table 字典的鍵。
#{key: set() for key in parent_1_stripped} 是一個字典推導式(dictionary comprehension),它的作用是:
#遍歷 parent_1_stripped 中的每個 key。
#為每個 key 生成一個對應的空集合 set()。

# Fill edge table from parent 1
for i in range(len(parent_1_stripped)): # 遍歷整個列表
    city = parent_1_stripped[i] 
    left_neighbor = parent_1_stripped[i-1]
    right_neighbor = parent_1_stripped[(i+1) % len(parent_1_stripped)] # 此處取餘是防止右鄰居的索引值超出邊界,若超出則環形
    edge_table[city].update([left_neighbor, right_neighbor])

# Fill edge table from parent 2
for i in range(len(parent_2_stripped)):
    city = parent_2_stripped[i]
    left_neighbor = parent_2_stripped[i-1]
    right_neighbor = parent_2_stripped[(i+1) % len(parent_2_stripped)]
    edge_table[city].update([left_neighbor, right_neighbor])

使用集合的 update() 方法時,它會將這個列表視為一個可迭代物件,並將其所有元素新增到集合中。例如我們update了一個[4,5]到 my_set={1,2,3} 中,那麼集合將會被更新為 {1, 2, 3, 4, 5} 因為集合 my_set 會自動處理重複元素,所以即使列表中包含重複的元素,它們也不會在集合中重複出現 注意Update()的傳入引數必須是可以迭代的物件如list等等,否則會報TypeError

3. 生成孩子

我們從一個隨機城市開始,選擇有最少剩餘邊緣的城市,直到生成完整的路徑。如果沒有相鄰城市可選,則從未訪問過的城市中隨機選擇一個。

    #Start with a random city:
    current = random.choice(parent_1_stripped) # 從列表中隨機選擇一個城市作為起點
    child = [current] # 子列表為僅有當前城市的列表

    #until we build the entire child:
    while len(child) < len(parent_1_stripped): # 重複操作直到子列表和原列表長度相同
        #remove the current city from the others' adjacency lists
        #WRITE YOUR CODE HERE!
        for cities in edge_table.values():
            cities.discard(current)
        #在交叉過程中,每次選擇一個城市後,我們需要將這個城市從所有其他城市的鄰接表中移除,
        #確保它不會被重複選擇。edge_table 是一個記錄每個城市鄰接城市的表,
        #透過 discard() 方法,我們從每個城市的鄰接表中刪除當前城市 current。
        
        #if current city has neighbors, choose the one with the fewest connections left - this helps priroritizing a structure that preserves the parents' structures.
        #檢查當前城市 current 是否還有剩餘的鄰接城市。如果 edge_table[current] 還有鄰居,則繼續執行。
        if  edge_table[current]:
            next_city = min(edge_table[current], key=lambda city: len(edge_table[city]))
        #if no neighbors left, choose a random unvisited city
        #WRITE YOUR CODE HERE!
        else:
            #如果當前城市沒有剩餘的鄰接城市(即鄰接表為空),則需要隨機選擇一個未訪問的城市。
            other_city = list(set(parent_1_stripped) - set(child)) #從親代減去已經訪問過的城市列表來得到剩下的城市
            next_city = random.choice(other_city)


        #將選擇的下一個城市 next_city 新增到孩子路徑中
        child.append(next_city)
        #更新當前城市為剛剛選擇的下一個城市,準備下一次迴圈。
        current = next_city

補充:

  1. 為什麼使用Discard()而不是remove()? discard() 是集合(set)的一個方法,用於從集合中移除指定的元素。如果使用 remove(),而被移除的城市不在集合中,會丟擲 KeyError 異常。相比之下,discard() 如果發現當前城市不在集合中,它不會報錯,這讓程式碼更健壯,可以在不同情況下正常執行。
  2. 關於lambda函式
    lambda arguments: expression
    lambda:關鍵字,用於定義一個匿名函式。
    arguments:函式的引數,可以有多個,用逗號分隔。
    expression:函式的返回值,是一個表示式,不能是語句
# 普通函式定義
def square(x):
    return x ** 2

# 等價的 Lambda 函式
square_lambda = lambda x: x ** 2
# 使用 Lambda 函式
result = square_lambda(4)  # 16

Lambda 函式經常用作其他函式(如 sorted()map()filter())的引數。例如,在 sorted() 函式中用 lambda 作為排序的鍵

在本例中,lambda 函式用於 min() 函式的 key 引數,以確定 edge_table 中每個城市的鄰接城市數量。具體來說:

next_city = min(edge_table[current], key=lambda city: len(edge_table[city]))

傳入的cityedge_table[current]中取,然後min() 函式使用這個 lambda 函式來找到鄰接城市中擁有最少鄰接城市的城市。

*min() 函式中使用 lambda 函式可以方便地按特定的標準(如鄰接城市數量)來選擇最小值,幫助實現複雜的邏輯操作。

4. 處理結果

最後,別忘了將 0 新增回路徑的起點和終點,確保孩子個體符合旅行商問題的要求。

# Add zeros back (starting and ending city is Amsterdam)
child = [0] + child + [0]

5. 呼叫交叉操作

當你呼叫 crossover 時,它將生成兩個新孩子。

def crossover(parent_1: list, parent_2: list, p_crossover:float) -> tuple:
    """
    Performs the Edge Recombination crossover twice on the same pair of parents, returns a pair of children.
    """
    if np.random.uniform() > p_crossover:
        # Do not perform crossover uniform用於生成均勻分佈的隨機數 預設區間是[0,1) 當小於超引數p_crossover時 則不發生變異
        return parent_1, parent_2
    else:
        # Create two children
        child_1 = create_offrping_edge_recombination(parent_1, parent_2)
        child_2 = create_offrping_edge_recombination(parent_1, parent_2)
        return child_1, child_2

在實現了初始化種群函式(initialize_population)、適應度函式(compute_distance)、突變操作(變異mutation())、父代選擇函式(fittest_solution_TSP)和生存選擇函式後。我們將使用錦標賽選拔,並實施代際生存機制,即所有孩子都取代父母。

def tournament_selection(generation: list,
                             compute_distance, adjacency_mat: np.ndarray, k: int) -> int:
    '''
    Implements the tournament selection algorithm.
    It draws randomly with replacement k individuals and returns the index of the fittest individual.
    '''

    '''ToDo:
    Complete the building blocks below!

    '''
    current_winner = None # 初始化當前勝者
    winner_distance = float('inf')# 初始化勝者的距離為無窮大,表示我們要找最小的
    # 從代際中取樣k個樣本, 分為帶放回抽樣和不帶放回抽樣
    #chosen_individuals = np.random.choice(len(generation),size = k,replace = False) # Sampling without replacement
    chosen_individuals = np.random.choice(len(generation),size = k,replace = True) # Sampling with replacement
    for individuals in chosen_individuals:
        current_distance = compute_distance(generation[individuals],adjacency_mat)
        if current_distance < winner_distance:
            winner_distance = current_distance
            current_winner = individuals

    return current_winner
遺傳演算法錦標賽選擇(Tournament Selection)中,使用帶放回抽樣的主要原因有以下幾點:
以下內容來自chat

1. 增加隨機性,防止過早收斂

遺傳演算法本質上是一種啟發式搜尋演算法,透過引入隨機性來探索解空間。如果使用不放回抽樣,選擇過程中每個個體只能被選中一次,可能會導致過多關注在部分個體上,減少種群的多樣性。而帶放回抽樣允許同一個個體被多次選擇,有助於保持種群的多樣性,避免演算法過早收斂到區域性最優解。

2. 允許強個體有更高的被選擇機會

在帶放回的情況下,適應度較高的個體有可能被多次抽中,這使得它們有更高的機率參與多次錦標賽,並被選為父代個體。這樣,適應度高的個體更容易被傳遞到下一代,從而提高演算法的進化效率。

3. 減少個體選擇不足的情況

如果種群規模較小,而你又不允許帶放回抽樣,可能會在錦標賽選擇中耗盡不同的個體,導致沒有足夠的多樣性去選擇足夠多的父代。帶放回抽樣可以確保每次都能從全體種群中抽取出個體,即使個體已經被選中過。

4. 簡化實現和邏輯

帶放回抽樣可以保證每次選擇中不會出現某些個體因為其他個體被選擇而被排除在外,使得演算法更加簡潔且易於實現。透過允許相同個體多次參與錦標賽,可以避免在不放回的情況下考慮額外的約束條件。

舉例說明:

假設種群中有一個適應度非常高的個體 A 和多個適應度較低的個體。如果不帶放回抽樣,A 只能參與一次錦標賽。但是,如果帶放回抽樣,A 有機會參與多個錦標賽,從而有更高的機率被選為父代。

相關文章