隨機優化技術常被用來處理協做類問題,它特別擅長處理:受多種變量的影響,存在許多可能解的問題,以及結果因這些變量的組合而產生很大變化的問題。例如:html
優化算法是經過嘗試許多不一樣解並給這些解打分以肯定其質量的方式,來找到一個問題的最優解。優化算法的典型應用場景是,存在大量可能的解(解搜索空間)以致於咱們沒法對其進行一一嘗試的狀況。node
最粗暴簡單但也是最低效的求解方法,是去嘗試隨機猜想上千個題解,並從中找出最佳解。而相對的,更有效率的方法,則是一種對題解可能有改進的方式來對其進行智能地修正。算法
優化算法是否管用的最關鍵因素:spring
文章的前面提到了優化算法的幾個核心因素,但這裏筆者但願強調的是,一種優化算法是否管用很大程度取決於問題自己。大多數優化算法都依賴於這樣一個事實:最優解應該接近於其餘的優解(即損失函數連續可微),來看一個可能不起做用的例子,編程
在圖的右邊,成本的最低點實際上處在一個很是陡峭的峽谷區域內,接近它的任何解都有可能被排除在外,由於這些解的成本都很高,因此咱們可能永遠都沒法找到通往全局最小值的途徑。大多數優化算法都會陷入到圖中左邊某個局部最小化的區域裏。網絡
Relevant Link: app
《集體智慧編程》Toby segaran - 第5章
這一章節,筆者這裏會先描述定義出2種典型場景,分別是無約束搜索問題和帶約束搜索問題,它們的區別以下,dom
咱們平常生活中和工程中遇到的不少問題,均可以抽象爲上述兩類問題。本文咱們會經過3個具體的例子,來從不一樣方面考察優化算法的靈活性。機器學習
第一個例子是關於爲一個團隊安排去某地開會的往返航班,這種場景在現實生活中很常見,例如筆者所在的公司在全球多地有不一樣的分部,而每一年的BlackHat會議會在一個估計的地點(例如拉斯維加斯)和時間召開,這個時候,就須要爲各個不一樣地方的員工安排航班,使他們在同一天到達,而且一樣安排其在同一天返航會各自的城市。函數
計劃的制定要求有許多不一樣的輸入,好比:
許多輸出結果也必須考慮,好比:
很顯然,咱們對輸出的指望是總成本和總候機時間越短越好,可是咱們沒法將這些輸入用一個簡單的公式映射到輸出。要解決這個問題,就須要轉換思惟,將公式思惟轉換到計算機的優化思惟上。
爲來自不一樣地方去往同一個地點的人們(本例是Glass一家)安排一次旅遊是一件極富挑戰的事情。下面虛構出一個家庭成員及其所在地,
people = [('Seymour', 'BOS'), ('Franny', 'DAL'), ('Zooey', 'CAK'), ('Walt', 'MIA'), ('Buddy', 'ORD'), ('Les', 'OMA')] # Laguardia airport in NewYork destination = 'LGA'
家庭成員們來自全美各地,而且它們但願在紐約會面。他們將在同一天到達,並在同一天離開,並且他們想搭乘相同的交通工具往返(到達接機,離開送機)紐約機場,從各自所在地前往所在地的機場的交通不用安排。天天有許多航班從任何一位家庭成員的所在地飛往紐約,飛機的起飛時間都是不一樣的,這些航班在價格和飛行時間上也都不盡相同。數據示例以下,
LGA,OMA,6:19,8:13,239 OMA,LGA,6:11,8:31,249 LGA,OMA,8:04,10:59,136 OMA,LGA,7:39,10:24,219 LGA,OMA,9:31,11:43,210 OMA,LGA,9:15,12:03,99 LGA,OMA,11:07,13:24,171 OMA,LGA,11:08,13:07,175 LGA,OMA,12:31,14:02,234 OMA,LGA,12:18,14:56,172 LGA,OMA,14:05,15:47,226
數據中每一列表明分別爲:起點、終點、起飛時間、到達時間、價格。
接下來的問題是,家庭成員的每一個成員應該乘坐哪一個航班呢?固然,使總的票價降下來是一個目標,可是還有其餘因素要考慮,例如:總的候機時間、等地其餘成員到達的時間、總的飛行時間等。
描述題解的第一步要作的是,抽象化表達。
當處理相似這樣的問題時,咱們有必要明確潛在的題解將如何抽象的表達,使之不侷限於某個具體的業務問題場景。有一種很是通用的表達方式,就是數字序列,在本例中,一個數字能夠表明某人選擇乘坐的航班,0表明第一次航班、1表明第二次,以此類推。由於每一個人都要往返兩個航班,因此列表長度是人數的兩倍。
例如,
[1, 4, 3, 2, 7, 3, 6, 3, 2, 4, 5, 3] Seymour BOS 8:04-10:11 $ 95 12:08-14:05 $142 Franny DAL 12:19-15:25 $342 10:51-14:16 $256 Zooey CAK 10:53-13:36 $189 9:58-12:56 $249 Walt MIA 9:15-12:29 $225 16:50-19:26 $304 Buddy ORD 16:43-19:00 $246 10:33-13:11 $132 Les OMA 11:08-13:07 $175 15:07-17:21 $129
上述列表表明了一種題解:Seymour搭乘當天的第2次航班從Boston飛往New York,並搭乘當天的第5次航班返回到Boston。Franny搭乘第4次航班從Dallas飛往New York,並搭乘第3次航班返回。
即便不考慮價格,上述安排仍然是有問題的。例如Les的航班是下午3點的,可是由於Zooey的航班是早上9點的,因此全部人都必須在早晨6點到達機場。
基本上看,全部人的航班」到達紐約時間「和」從紐約出發時間「應該儘量接近,這樣整體的候機時間會減小,可是這沒有將總票價和總飛行時間考慮進去。因此,爲了肯定最佳組合,程序須要一種方法來爲不一樣日程安排的各類屬性進行量化地評估,從而決定哪個方案是最好的。
成本函數是用優化算法解決問題的關鍵,任何優化算法的目標,就是要尋找一組可以使成本函數的返回結果達到最小化的輸入(本例中即爲全部人的航班安排序列),所以成本函數須要返回一個值,用以表示方案的好壞。對於好壞程度的度量沒有固定不變的衡量尺度,可是有如下幾點基本準則:
一般來講,對多個隨機變量進行綜合評估方案的好壞是比較困難的,主要問題在於不一樣隨機變量之間的量綱維度不一致,咱們來考察一些在組團旅遊的例子中可以被度量的變量,
肯定了對成本產生影響的全部變量因素以後,咱們就必需要找到辦法將他們」組合「在一塊兒行程一個值。
例如在本例中,咱們就須要作以下轉換:
def schedulecost(sol): totalprice = 0 latestarrival = 0 earliestdep = 24 * 60 for d in range(len(sol) / 2): # Get the inbound and outbound flights origin = people[d][1] outbound = flights[(origin, destination)][int(sol[d])] returnf = flights[(destination, origin)][int(sol[d + 1])] # Total price is the price of all outbound and return flights totalprice += outbound[2] totalprice += returnf[2] # Track the latest arrival and earliest departure if latestarrival < getminutes(outbound[1]): latestarrival = getminutes(outbound[1]) if earliestdep > getminutes(returnf[0]): earliestdep = getminutes(returnf[0]) # Every person must wait at the airport until the latest person arrives. # They also must arrive at the same time and wait for their flights. totalwait = 0 for d in range(len(sol) / 2): origin = people[d][1] outbound = flights[(origin, destination)][int(sol[d])] returnf = flights[(destination, origin)][int(sol[d + 1])] totalwait += latestarrival - getminutes(outbound[1]) totalwait += getminutes(returnf[0]) - earliestdep # Does this solution require an extra day of car rental? That'll be $50! if latestarrival > earliestdep: totalprice += 50 return totalprice + totalwait
對前面假設的一個航班序列打印其總損失結果爲:
Seymour BOS 8:04-10:11 $ 95 12:08-14:05 $142 Franny DAL 12:19-15:25 $342 10:51-14:16 $256 Zooey CAK 10:53-13:36 $189 9:58-12:56 $249 Walt MIA 9:15-12:29 $225 16:50-19:26 $304 Buddy ORD 16:43-19:00 $246 10:33-13:11 $132 Les OMA 11:08-13:07 $175 15:07-17:21 $129 5285
成本函數創建後,咱們的目標就很是明確了,就是要經過選擇正確地數字序列來最小化該成本。
在文章的後半部分,咱們會集中介紹表明當前主流核心思想的集中優化算法,並經過比較不一樣優化算法之間的區別,來更加深入理解隨機優化的內涵。如今,咱們暫時先繼續咱們對典型問題場景的討論上。
本節咱們來考查另外一個不一樣的問題,這個問題明顯要藉助優化算法來加以解決。其通常的表述是:如何將有限的資源分配給多個表達了偏好的人,並儘量使他們都滿意,或儘量使全部人都滿意。
本節的示例問題是,依據學生的首選和次選,爲其分配宿舍。有5間宿舍,每間宿舍只有2個隔間(只能住2人),由10名學生來競爭住所。每一名學生都有一個首選和一個次選,以下:
# The dorms, each of which has two available spaces dorms=['Zeus','Athena','Hercules','Bacchus','Pluto'] # People, along with their first and second choices prefs=[('Toby', ('Bacchus', 'Hercules')), ('Steve', ('Zeus', 'Pluto')), ('Karen', ('Athena', 'Zeus')), ('Sarah', ('Zeus', 'Pluto')), ('Dave', ('Athena', 'Bacchus')), ('Jeff', ('Hercules', 'Pluto')), ('Fred', ('Pluto', 'Athena')), ('Suzie', ('Bacchus', 'Hercules')), ('Laura', ('Bacchus', 'Hercules')), ('James', ('Hercules', 'Athena'))]
經過觀察數據咱們發現,不可能知足全部人的首選。實際上這也是工程中最廣泛的狀況,全局最優解不多能直接獲得,咱們大部分時候是在求全局次優解。
如今來思考如何對這個問題進行抽象建模的問題,理論上,咱們能夠構造一個數字序列,讓每一個學生對應於一個學生,表示咱們將某個學生安排在了某個宿舍。可是問題在於,這種抽象表達方式沒法在題解中體現每間宿舍僅限兩名學生居住的約束條件。一個全零序列表明瞭全部人都安置在了Zeus宿舍,但這不是一個有效的解。
解決這個問題的正確辦法是,經過在抽象建模時增長一個約束條件,即找到一種讓每一個解都有效的題解表示法。這等價於縮小了題解搜索空間,強制優化算法在一個受約束的子空間進行最優化搜索。
將每間宿舍抽象出擁有兩個」槽「,如此,在本例中共有10個槽,咱們將每名學生依序安置在各個空槽內,第一位學生可置於10個槽中的任何一個內,第二位學生則可置於剩餘9個槽中的任何一個位置,依次類推。
# [(0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3), (0, 2), (0, 1), (0, 0)] domain=[(0,(len(dorms)*2)-i-1) for i in range(0,len(dorms)*2)]
下面代碼示範了槽的分配過程,5間宿舍總共有10個槽,每一個槽被分配後會從隊列中刪除,如此,其餘學生就不會再被安置於該槽中了。
def printsolution(vec): slots=[] # Create two slots for each dorm for i in range(len(dorms)): slots+=[i,i] print "slots: ", slots # Loop over each students assignment for i in range(len(vec)): x=int(vec[i]) # Choose the slot from the remaining ones dorm=dorms[slots[x]] # Show the student and assigned dorm print prefs[i][0],dorm # Remove this slot del slots[x]
這裏以一種安排序列爲例,即給每個學生都安排當前所剩槽位的第一個,
printsolution([0,0,0, 0,0,0, 0,0,0, 0]) Toby Zeus Steve Zeus Karen Athena Sarah Athena Dave Hercules Jeff Hercules Fred Bacchus Suzie Bacchus Laura Pluto James Pluto
顯然這是一個有效解,但不是一個最優解。能夠看到,槽的分配程序是一箇中立的程序,如何分配槽位,徹底取決於輸入的學生安排序列,優化算法的任務就是找到一個最佳的安排序列,使槽分配後的結果儘量知足學生的需求。
筆者提醒:
儘管這是一個很是具體的例子,可是將這種狀況推廣到其餘問題是很是容易的,例如紙牌遊戲中玩家的牌桌分配、家庭成員中的家務分配。要理解領會的是,這類問題的目的是爲了從個體中提取信息,並將其組合起來產生出優化的結果。
成本函數的計算過程,是經過將學生的當前宿舍安置狀況,與他的兩項指望選擇進行對比而獲得。
def dormcost(vec): cost=0 # Create list a of slots slots=[0,0,1,1,2,2,3,3,4,4] # Loop over each student for i in range(len(vec)): x=int(vec[i]) dorm=dorms[slots[x]] pref=prefs[i][1] # First choice costs 0, second choice costs 1 if pref[0]==dorm: cost+=0 elif pref[1]==dorm: cost+=1 else: cost+=3 # Not on the list costs 3 # Remove selected slot del slots[x] return cost
之前面舉例的[0,0,0, 0,0,0, 0,0,0, 0]爲例,
s = [0,0,0, 0,0,0, 0,0,0, 0] printsolution(s) print dormcost(s) 18
這個例子討論的是網絡的可視化問題,這裏的網絡指的是任何彼此相連的一組事物,像Myspace、Facebook、linkedIn這樣的社交應用即是社交網絡的一個典型例子,在這些應用中,人們由於朋友或具有特定關係而彼此相連。網站的每一位成員能夠選擇與他們相連的其餘成員,共同構築一我的際關係網絡。將這樣的網絡可視化輸出,以明確人們彼此間的社會關係結構,是一件頗有意義和研究價值的事情。
爲了展現一大羣人及其彼此間的關聯,咱們將網絡在一個二維畫布上進行繪製,繪製時會遇到一個問題,咱們應該如何安置圖中的每一個人名的空間佈局呢?如下圖爲例,
混亂的隨機佈局
在圖中,咱們能夠看到Augustus是Willy、Violet和Miranda的朋友。可是,網絡的佈局有點雜亂,鏈接線彼此交錯的狀況比較多,一個更爲清晰的佈局以下圖所示,
本例的目標就是討論如何運用優化算法來構建更清晰結構的網絡圖。首先,咱們虛構一些數據,這些數據表明着社會網絡的某一小部分,
people=['Charlie','Augustus','Veruca','Violet','Mike','Joe','Willy','Miranda'] links=[('Augustus', 'Willy'), ('Mike', 'Joe'), ('Miranda', 'Mike'), ('Violet', 'Augustus'), ('Miranda', 'Willy'), ('Charlie', 'Mike'), ('Veruca', 'Joe'), ('Miranda', 'Augustus'), ('Willy', 'Augustus'), ('Joe', 'Charlie'), ('Veruca', 'Augustus'), ('Miranda', 'Joe')]
此處,咱們的目標是創建一個程序,令其可以讀取一組有關誰是誰的朋友的事實數據,並生成一個易於理解的網絡圖。
要完成這項工做,最基本的須要藉助質點彈簧算法(mass-and-spring algorithm),這一算法是從物理學中建模而來的,各節點彼此向對方施以推力並試圖分離,而節點間的鏈接則試圖將關聯節點彼此拉近。如此一來,網絡便會逐漸呈現出這樣一個佈局,未關聯的節點被推離,而關聯的節點則被彼此拉近,卻又不會靠的很近。
但遺憾的是,原始的質點彈簧算法沒法避免交叉線,這使得咱們追蹤一個大型的社會網絡變得很困難。解決問題的辦法是使用優化算法來構建佈局,將比較交叉做爲成本構建一個成本函數,並嘗試令其返回值儘量小。
將每一個節點都抽象爲一個(x,y)座標,所以能夠將全部節點的座標放入一個列表中。隨後,成本函數只需對彼此交叉的連線進行計數便可,
def crosscount(v): # Convert the number list into a dictionary of person:(x,y) loc=dict([(people[i],(v[i*2],v[i*2+1])) for i in range(0,len(people))]) total=0 # Loop through every pair of links for i in range(len(links)): for j in range(i+1,len(links)): # Get the locations (x1,y1),(x2,y2)=loc[links[i][0]],loc[links[i][1]] (x3,y3),(x4,y4)=loc[links[j][0]],loc[links[j][1]] den=(y4-y3)*(x2-x1)-(x4-x3)*(y2-y1) # den==0 if the lines are parallel if den==0: continue # Otherwise ua and ub are the fraction of the # line where they cross ua=((x4-x3)*(y1-y3)-(y4-y3)*(x1-x3))/den ub=((x2-x1)*(y1-y3)-(y2-y1)*(x1-x3))/den # If the fraction is between 0 and 1 for both lines # then they cross each other if ua>0 and ua<1 and ub>0 and ub<1: total+=1 for i in range(len(people)): for j in range(i+1,len(people)): # Get the locations of the two nodes (x1,y1),(x2,y2)=loc[people[i]],loc[people[j]] # Find the distance between them dist=math.sqrt(math.pow(x1-x2,2)+math.pow(y1-y2,2)) # Penalize any nodes closer than 50 pixels if dist<50: total+=(1.0-(dist/50.0)) return total
筆者思考:
優化算法就像一個忠實知足咱們願望的」精靈「,你經過損失函數設定什麼樣的目標,優化的結果就會朝什麼方向發展。從這點來講,在利用機器學習解決生活和工程問題的時候,清楚咱們想要什麼是很是重要的。時常會有題解知足本來的」最優「條件,但卻並不是是咱們想要的結果,這個時候就要反思一下是否是損失函數的目標設置有問題。
Relevant Link:
《集體智慧編程》Toby segaran - 第5章
前面的章節,咱們已經列舉了幾種常見的典型問題場景,筆者這麼排版的目的是想明確傳達出一個意思:
優化算法是通用獨立的,它不依賴於具體問題,而是一種通用的抽象化接口,只要目標問題可以抽象出一個明確的損失函數,優化算法就能夠在損失函數的搜索空間中尋找到一個相對最優解。
這節咱們從隨機搜索開始討論,隨機搜索不是一種很是好的優化算法,可是它卻使咱們很容易領會全部算法的真正意圖,而且它也是咱們評估其餘算法優劣的基線(baseline)。
def randomoptimize(domain, costf): best = 999999999 bestr = None for i in range(0, 1000): # Create a random solution r = [float(random.randint(domain[i][0], domain[i][1])) for i in range(len(domain))] # Get the cost cost = costf(r) # Compare it to the best one so far if cost < best: best = cost bestr = r return r
這個函數接受兩個參數,
此函數會在domain的定義域內隨機產生1000次猜想,並對每一次猜想調用costf。並統計最終的最優結果。
import time import random import math people = [('Seymour', 'BOS'), ('Franny', 'DAL'), ('Zooey', 'CAK'), ('Walt', 'MIA'), ('Buddy', 'ORD'), ('Les', 'OMA')] # Laguardia destination = 'LGA' flights = {} # for line in file('schedule.txt'): origin, dest, depart, arrive, price = line.strip().split(',') flights.setdefault((origin, dest), []) # Add details to the list of possible flights flights[(origin, dest)].append((depart, arrive, int(price))) def getminutes(t): x = time.strptime(t, '%H:%M') return x[3] * 60 + x[4] def printschedule(r): for d in range(len(r) / 2): name = people[d][0] origin = people[d][1] out = flights[(origin, destination)][int(r[d])] ret = flights[(destination, origin)][int(r[d + 1])] print '%10s%10s %5s-%5s $%3s %5s-%5s $%3s' % (name, origin, out[0], out[1], out[2], ret[0], ret[1], ret[2]) def schedulecost(sol): totalprice = 0 latestarrival = 0 earliestdep = 24 * 60 for d in range(len(sol) / 2): # Get the inbound and outbound flights origin = people[d][1] outbound = flights[(origin, destination)][int(sol[d])] returnf = flights[(destination, origin)][int(sol[d + 1])] # Total price is the price of all outbound and return flights totalprice += outbound[2] totalprice += returnf[2] # Track the latest arrival and earliest departure if latestarrival < getminutes(outbound[1]): latestarrival = getminutes(outbound[1]) if earliestdep > getminutes(returnf[0]): earliestdep = getminutes(returnf[0]) # Every person must wait at the airport until the latest person arrives. # They also must arrive at the same time and wait for their flights. totalwait = 0 for d in range(len(sol) / 2): origin = people[d][1] outbound = flights[(origin, destination)][int(sol[d])] returnf = flights[(destination, origin)][int(sol[d + 1])] totalwait += latestarrival - getminutes(outbound[1]) totalwait += getminutes(returnf[0]) - earliestdep # Does this solution require an extra day of car rental? That'll be $50! if latestarrival > earliestdep: totalprice += 50 return totalprice + totalwait def randomoptimize(domain, costf): best = 999999999 bestr = None for i in range(0, 100000): # Create a random solution r = [float(random.randint(domain[i][0], domain[i][1])) for i in range(len(domain))] # Get the cost cost = costf(r) # Compare it to the best one so far if cost < best: best = cost bestr = r return r def hillclimb(domain, costf): # Create a random solution sol = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))] # Main loop while 1: # Create list of neighboring solutions neighbors = [] for j in range(len(domain)): # One away in each direction if sol[j] > domain[j][0]: neighbors.append(sol[0:j] + [sol[j] + 1] + sol[j + 1:]) if sol[j] < domain[j][1]: neighbors.append(sol[0:j] + [sol[j] - 1] + sol[j + 1:]) # See what the best solution amongst the neighbors is current = costf(sol) best = current for j in range(len(neighbors)): cost = costf(neighbors[j]) if cost < best: best = cost sol = neighbors[j] # If there's no improvement, then we've reached the top if best == current: break return sol def annealingoptimize(domain, costf, T=10000.0, cool=0.95, step=1): # Initialize the values randomly vec = [float(random.randint(domain[i][0], domain[i][1])) for i in range(len(domain))] while T > 0.1: # Choose one of the indices i = random.randint(0, len(domain) - 1) # Choose a direction to change it dir = random.randint(-step, step) # Create a new list with one of the values changed vecb = vec[:] vecb[i] += dir if vecb[i] < domain[i][0]: vecb[i] = domain[i][0] elif vecb[i] > domain[i][1]: vecb[i] = domain[i][1] # Calculate the current cost and the new cost ea = costf(vec) eb = costf(vecb) p = pow(math.e, (-eb - ea) / T) # Is it better, or does it make the probability # cutoff? if (eb < ea or random.random() < p): vec = vecb # Decrease the temperature T = T * cool return vec def geneticoptimize(domain, costf, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100): # Mutation Operation def mutate(vec): i = random.randint(0, len(domain) - 1) if random.random() < 0.5 and vec[i] > domain[i][0]: return vec[0:i] + [vec[i] - step] + vec[i + 1:] elif vec[i] < domain[i][1]: return vec[0:i] + [vec[i] + step] + vec[i + 1:] # Crossover Operation def crossover(r1, r2): i = random.randint(1, len(domain) - 2) return r1[0:i] + r2[i:] # Build the initial population pop = [] for i in range(popsize): vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))] pop.append(vec) # How many winners from each generation? topelite = int(elite * popsize) # Main loop for i in range(maxiter): scores = [(costf(v), v) for v in pop] scores.sort() ranked = [v for (s, v) in scores] # Start with the pure winners pop = ranked[0:topelite] # Add mutated and bred forms of the winners while len(pop) < popsize: if random.random() < mutprob: # Mutation c = random.randint(0, topelite) pop.append(mutate(ranked[c])) else: # Crossover c1 = random.randint(0, topelite) c2 = random.randint(0, topelite) pop.append(crossover(ranked[c1], ranked[c2])) # Print current best score print scores[0][0] return scores[0][1] if __name__ == '__main__': #s = [1, 4, 3, 2, 7, 3, 6, 3, 2, 4, 5, 3] #printschedule(s) #print schedulecost(s) domain = [(0, 9)] * (len(people) * 2) s = randomoptimize(domain, schedulecost) print schedulecost(s) printschedule(s) # s = hillclimb(domain, schedulecost) # print schedulecost(s) # printschedule(s) # s = annealingoptimize(domain, schedulecost) # print schedulecost(s) # printschedule(s) #s = geneticoptimize(domain, schedulecost) #printschedule(s)
隨機的結果並不算太好,固然也不算太差,並且最大的問題在於,優化結果不可預期,每次運行均可能獲得不一樣的結果。咱們但願優化算法能夠穩定地得到成功。
Relevant Link:
《集體智慧編程》Toby segaran - 第5章
隨機嘗試的問題在於,沒有充分利用已經發現的優解。具體來講,擁有較低總成本的時間安排極可能接近於其餘低成本的安排。可是由於隨機優化是處處跳躍的(jump around),因此它不會自動去尋找與已經被發現的優解相接近的題解。
本章要介紹的方法叫登山法(hill climbing),登山法以一個隨機解開始,而後在其臨近的解集中尋找更好的題解,這相似於從斜坡向上下走,以下圖所示,
這種登山法的合理性在於,充分利用了損失函數自己的連續可微特性,每次的嘗試都創建在充分利用歷史嘗試的信息之上,步步爲營,逐漸找到一個局部最優解。
咱們能夠應用這種登山法爲Glass一家找到最好的旅行安排方案,
s = hillclimb(domain, schedulecost)
print schedulecost(s)
printschedule(s)
登山法找到的解一般比隨機搜索要好,可是,登山法有一個較大的缺陷,以下圖,
簡單從斜坡滑下不必定會產生全局最優解。登山法獲得的只能是一個局部範圍內的最小值,它比鄰近解的表現逗號,但卻不是全局最優的。
一個緩解的手段被稱爲隨機重複登山法(random-restart hill climbing),即讓登山法以多個隨機生成的初始解爲起點運行若干次,並取其中最優解,藉此但願在屢次的隨機嘗試中,有一個解能逼近全局的最小值。
固然,登山法如今已經沒有使用了,可是咱們卻能從中領悟到優化算法進化的幾個核心要素,
Relevant Link:
《集體智慧編程》Toby segaran - 第5章
模擬退火算法是受物理學領域啓發而來的一種優化算法。退火是指將合金加熱後再慢慢冷卻的過程。
大量的原子由於受到激發而向周圍跳躍,而後又逐漸穩定到一個低能階的狀態(例如熔鍊武器的過程),全部這些原子可以找到一個低能階的配置(configuration)。
退火躍遷公式以下,退火算法以一個問題的隨機解開始,它用一個變量來表示溫度,這一溫度初始時很高,以後逐漸變低:
在某些狀況下,在咱們可以獲得一個更優的解以前轉向一個更差的解是有必要的,這就至關於黎明前的黑暗。模擬退火算法之因此管用,不只由於它老是接受一個更優的解並且還由於它在退火過程的開始階段會接受表現較差的解。而隨機退火過程的不斷進行,算法愈來愈不可能接受較差的解,收斂逐漸穩定,
def annealingoptimize(domain, costf, T=10000.0, cool=0.95, step=1): # Initialize the values randomly vec = [float(random.randint(domain[i][0], domain[i][1])) for i in range(len(domain))] while T > 0.1: # Choose one of the indices i = random.randint(0, len(domain) - 1) # Choose a direction to change it dir = random.randint(-step, step) # Create a new list with one of the values changed vecb = vec[:] vecb[i] += dir if vecb[i] < domain[i][0]: vecb[i] = domain[i][0] elif vecb[i] > domain[i][1]: vecb[i] = domain[i][1] # Calculate the current cost and the new cost ea = costf(vec) eb = costf(vecb) p = pow(math.e, (-eb - ea) / T) # Is it better, or does it make the probability # cutoff? if (eb < ea or random.random() < p): vec = vecb # Decrease the temperature T = T * cool return vec
使用模擬退火算法來對旅行規劃問題進行優化,
s = annealingoptimize(domain, schedulecost)
print schedulecost(s)
printschedule(s)
能夠看到,模擬退火的優化結果因爲隨機搜索。
筆者思考:
模擬退火有效的兼顧了」隨機擾動「和」漸進收斂「這兩個目標的平衡,對於一個優化算法來講,若是隻是一味地漸進優化(例如登山法),則會陷入局部最優的陷阱中。另外一方面,若是像隨機重複登山法那樣一味地不斷地擾動,則又沒法保證」可重複收斂「到某一個全局優解上。模擬退火經過一個遞減的函數(退火)來動態的調整每一輪優化的擾動幅度,逐步減少擾動幅度,這樣作的好處是使得優化算法能穩定的保證通過必定次數的優化後,穩定地收斂到某個全局優解上。
Relevant Link:
《集體智慧編程》Toby segaran - 第5章
遺傳算法也是受天然科學的啓發,這類算法的運行過程是先隨機生成一組解,咱們稱之爲種羣(population),在優化過程當中的每一步,算法會計算整個種羣的成本函數,從而獲得種羣每個題解的有序列表,以下列表所示,
在對題解進行排序以後,選出排名前列的一部分(例如10%)題解做爲下一代種羣的種子,這個過程被稱爲精英選拔法(elitism)。
接下來,新的種羣的種子題解,要通過一次」繁衍「獲得一個新的種羣。具體的繁衍方式有兩種:
這樣,一個新的種羣(咱們稱之爲下一代)被建立出來。它的大小與舊的種羣相同,然後,這個過程會一直重複進行。達到迭代次數,或者連續數代後題解都沒有獲得改善,則結束優化。
def geneticoptimize(domain, costf, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100): # Mutation Operation def mutate(vec): i = random.randint(0, len(domain) - 1) if random.random() < 0.5 and vec[i] > domain[i][0]: return vec[0:i] + [vec[i] - step] + vec[i + 1:] elif vec[i] < domain[i][1]: return vec[0:i] + [vec[i] + step] + vec[i + 1:] # Crossover Operation def crossover(r1, r2): i = random.randint(1, len(domain) - 2) return r1[0:i] + r2[i:] # Build the initial population pop = [] for i in range(popsize): vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))] pop.append(vec) # How many winners from each generation? topelite = int(elite * popsize) # Main loop for i in range(maxiter): scores = [(costf(v), v) for v in pop] scores.sort() ranked = [v for (s, v) in scores] # Start with the pure winners pop = ranked[0:topelite] # Add mutated and bred forms of the winners while len(pop) < popsize: if random.random() < mutprob: # Mutation c = random.randint(0, topelite) pop.append(mutate(ranked[c])) else: # Crossover c1 = random.randint(0, topelite) c2 = random.randint(0, topelite) pop.append(crossover(ranked[c1], ranked[c2])) # Print current best score print scores[0][0] return scores[0][1]
運用遺傳算法對旅行計劃進行優化,
2956 Seymour BOS 9:45-11:50 $172 9:58-11:18 $130 Franny DAL 9:08-12:12 $364 9:49-13:51 $229 Zooey CAK 9:15-12:14 $247 8:19-11:16 $122 Walt MIA 7:34- 9:40 $324 8:23-11:07 $143 Buddy ORD 8:25-10:34 $157 9:11-10:42 $172 Les OMA 9:15-12:03 $ 99 8:04-10:59 $136
能夠看到,遺傳算法比模擬退火算法的優化效果要好。
筆者提醒:
須要注意的是,遺傳算法中,一樣體現了優化算法的幾個核心要素,
Relevant Link:
《集體智慧編程》Toby segaran - 第5章
多元非線性損失函數,直接求解其逆矩陣很困難,所以不適合用極大似然估計直接獲得全局最優解
隨機梯度降低思想上相似於爬上法,最大的區別在於,
多元隨機函數的梯度計算須要用到Hessian矩陣,這裏再也不詳述。
梯度降低這塊遵循了優化算法的漸進收斂特性。而在隨機擾動方面,SGD每輪迭代只是隨機地選取一部分樣本計算損失函數的梯度,隨機選取的樣本子集可能不必定表明了真實全集的梯度方向,所以,SGD每次實際選擇的前進迭代方式又具有了必定的隨機性。