一、算法概述
圖 beam search
二、算法原理
Beam Search算法是一種在搜索階段保留固定大小集合并只考慮概率最高一部分結果的搜索算法。算法從初始狀態(tài)開始,計算每個可能的下一個狀態(tài),并將這些狀態(tài)添加到一個新的Beam集合中。然后,從新的Beam集合中選擇概率最高的前N個狀態(tài),并將它們作為新的Beam集合。重復上述步驟直到滿足終止條件,返回概率最高的狀態(tài)。當N等于1時,Beam Search與貪心算法等同,下面是Beam Search算法的偽代碼。
束寬大小和節(jié)點評價函數(shù)是Beam Search算法的兩個核心點,對于搜索效率和結果質量都具有重要影響。合適的束寬大小能夠平衡搜索速度和結果準確性;正確的節(jié)點評價函數(shù)能夠確保選擇概率最高的候選項,從而提高搜索結果的質量。
三、單源最短路徑問題
為了詳細闡述Beam Search算法的運行原理,本文以較為簡單的單源最短路徑問題為例,逐步向讀者介紹Beam Search的求解過程。同時以Greedy作為該問題的baseline,論證Beam Search相對于Greedy的優(yōu)越性,全局性。
單源最短路徑問題是在一個有向或無向加權圖中,找到從一個起點到其他所有節(jié)點的最短路徑的問題。其中,邊的權重表示從一個節(jié)點到另一個節(jié)點的距離、時間或費用等。該問題被廣泛應用于尋找路線規(guī)劃、網絡優(yōu)化、資源分配等領域。常用的解決算法包括Dijkstra算法、Bellman-Ford算法和SPFA算法等。下圖為一個簡單的單源最短路徑問題,問題的目標是找到從節(jié)點0到節(jié)點4的最短路徑。
圖 單源最短路徑問題
四、貪心算法求解最短路徑問題
假如使用貪心算法求解該問題,第一步會從0走到1,第二步從1走到2,第三步從2走到8,,第四步從8走到6,第五步從6走到7,從節(jié)點7只能走到1或者8,都是已經訪問過的節(jié)點,因此回退一步,第五步改為從節(jié)點6到節(jié)點5,第六步從節(jié)點5到節(jié)點4。貪心算法最終探索出的最短路徑為0-1-2-8-6-5-4,總距離為32,貪心算法代碼如下。
def greedy(graph,start,end):
nodes=graph['nodes']
'''
轉字典,便于查詢,例:[0,1,4]->{0:[(1,4)],1:[(0,4)]}
'''
edges={}
for edge in graph['edges']:
if not edge[0] in edges:
edges[edge[0]]=[(edge[1],edge[2])]
else:
edges[edge[0]].append((edge[1],edge[2]))
if not edge[1] in edges:
edges[edge[1]]=[(edge[0],edge[2])]
else:
edges[edge[1]].append((edge[0],edge[2]))
'''
貪心求解
'''
cur=start
path=[start]
tabu=[start]
distance=[]
while cur!=end:
'''
選擇和cur相連的最短節(jié)點
'''
minDis=1e10
bestNode=-1
for edge in edges[cur]:
'''
如果存在節(jié)點為最終節(jié)點,則此節(jié)點最優(yōu)。
否則選擇最近節(jié)點
'''
if edge[0]==end:
minDis=edge[1]
bestNode=end
break
else:
if minDis>edge[1] and not edge[0] in tabu:
minDis=edge[1]
bestNode=edge[0]
'''
存在可選節(jié)點則更新當前節(jié)點、路徑、距離。
如果不存在則倒退一步
'''
if not bestNode==-1:
cur=bestNode
path.append(cur)
distance.append(minDis)
tabu.append(cur)
else:
tabu.append(path[-1])
path.pop()
distance.pop()
cur=path[-1]
return path,sum(distance)
graph={}
nodes=[0,1,2,3,4,5,6,7,8]
edges=[(0,1,4),(0,7,8),(1,2,8),(1,7,11),(2,3,7),(2,5,4),\
(2,8,2),(3,4,9),(3,5,14),(4,5,10),(5,6,2),(6,7,1),(6,8,6),(7,8,7)]
graph['nodes']=nodes
graph['edges']=edges
greedy(graph,0,4)
五、Beam Search求解最短路徑問題
不同于簡單的Greedy算法,Beam Search考慮一個固定寬度的beam,即每個階段得分最高的固定數(shù)量的節(jié)點。算法的輸入為一張有向圖、起始節(jié)點編號終止節(jié)點編號、beam的寬度,輸出為最短路徑以及路徑對應的距離
首先,算法定義隊列queue,記錄每個節(jié)點的狀態(tài),即[當前節(jié)點的距離,當前節(jié)點編號,具體路徑]。初始狀態(tài)為[0,0,[0]],下一個階段可觸達的節(jié)點是1和7,將新增節(jié)點放入隊列中,而后對隊列進行排序,保留距離最短的beam_search個節(jié)點;接著對隊列中所有節(jié)點進行expand,考慮所有可觸達的節(jié)點,將節(jié)點添加到隊列,再保留最優(yōu)的beam_width個節(jié)點,重復操作,直到隊列為空。
下面給出了Beam Search求解單源最短路徑的源代碼,代碼中包含詳細的注釋。
import heapq
def beam_search(graph, start, end, beam_width=3):
queue = [(0, start, [start])] # 初始化隊列,第一個元素為代價、第二個為節(jié)點、第三個為路徑
visited = set() # 初始化已訪問節(jié)點集合
while queue:
cost, node, path = heapq.heappop(queue) # 取出最小代價的節(jié)點
if node == end: # 如果找到了終止點,則返回最短路徑
return path,cost
if node in visited: # 如果節(jié)點已經被訪問過,則跳過
continue
visited.add(node) # 將當前節(jié)點添加到已訪問集合中
for neighbor, weight in graph[node].items(): # 擴展當前節(jié)點的鄰居節(jié)點
new_cost = cost + weight # 計算新的代價
new_path = path + [neighbor] # 添加新節(jié)點到路徑中
heapq.heappush(queue, (new_cost, neighbor, new_path)) # 將擴展出來的節(jié)點加入優(yōu)先隊列中
queue = sorted(queue)[:beam_width] # 限制隊列寬度,只保留代價最小的beam_width個節(jié)點
return None # 如果沒有找到可行路徑,則返回None
'''
定義圖
'''
graph={0: {1: 4, 7: 8},
1: {0: 4, 2: 8, 7: 11},
7: {0: 8, 1: 11, 6: 1, 8: 7},
2: {1: 8, 3: 7, 5: 4, 8: 2},
3: {2: 7, 4: 9, 5: 14},
5: {2: 4, 3: 14, 4: 10, 6: 2},
8: {2: 2, 6: 6, 7: 7},
4: {3: 9, 5: 10},
6: {5: 2, 7: 1, 8: 6}}
'''
求解
'''
beam_search(graph,0,4,20)
當beam_width等于10時,得到的最優(yōu)路徑為0-1-2-3-4,總距離為28;當beam_width為20時,得到的最優(yōu)路徑為0-7-6-5-4,總距離為21。
六、本文小結
本文首先簡單介紹了Beam Search的基本概念以及應用場景,然后通過單源最短路徑問題對Beam Search做了進一步闡述,并給出了貪心算法和Beam Search的源碼,與貪心算法相比,Beam Search考慮了更多節(jié)點,能夠在可接受的時間范圍內獲得更好的解。
為了讀者快速理解,本文選擇的例子是一個規(guī)模極小的單源最短路徑問題,實際上Beam Search在很多問題的求解上都有不俗表現(xiàn)。它既不像貪心算法,僅考慮當前狀態(tài),極易陷入局部最優(yōu),又能夠有效避免深度廣度搜索等精確算法對應的時間復雜度過高的問題,通過調整beam的寬度可以在時間和效果上進行有效的trade-off??偠灾?,Beam Search的應用廣泛,操作簡單,是極為有效的算法。