螞蟻幾乎沒有視力,但他們卻能夠在黑暗的世界中找到食物,而且能夠找到一條從洞穴到食物的最短路徑。它們是如何做到的呢?
螞蟻尋找食物的過程
單隻螞蟻的行為及其簡單,行為數量在10種以內,但成千上萬只螞蟻組成的蟻群卻能擁有巨大的智慧,這離不開它們資訊傳遞的方式——資訊素。
螞蟻在行走過程中會釋放一種稱為“資訊素”的物質,用來標識自己的行走路徑。在尋找食物的過程中,根據資訊素的濃度選擇行走的方向,並最終到達食物所在的地方。
資訊素會隨著時間的推移而逐漸揮發。
在一開始的時候,由於地面上沒有資訊素,因此螞蟻們的行走路徑是隨機的。螞蟻們在行走的過程中會不斷釋放資訊素,標識自己的行走路徑。隨著時間的推移,有若干只螞蟻找到了食物,此時便存在若干條從洞穴到食物的路徑。由於螞蟻的行為軌跡是隨機分佈的,因此在單位時間內,短路徑上的螞蟻數量比長路徑上的螞蟻數量要多,從而螞蟻留下的資訊素濃度也就越高。這為後面的螞蟻們提供了強有力的方向指引,越來越多的螞蟻聚集到最短的路徑上去。
什麼是蟻群演算法?
蟻群演算法就是模擬螞蟻尋找食物的過程,它能夠求出從原點出發,經過若干個給定的需求點,最終返回原點的最短路徑。這也就是著名的旅行商問題(Traveling Saleman Problem,TSP)。
本文使用蟻群演算法來解決分散式環境下的負載均衡排程問題。
蟻群演算法的應用——負載均衡排程
叢集模式是目前較為常用的一種部署結構,也就是當單機處理能力無法滿足業務需求,那麼就增加處理節點,並由一個負載均衡器負責請求的排程。然而對於一個龐大系統而言,情況往往比較複雜。叢集中節點的處理能力往往各不相同,而且不同任務的處理複雜度也不盡相同。那麼負載均衡器如何進行任務分配,使得叢集效能達到最優?資源利用率達到最高呢?這是一個極具挑戰又很有價值的問題。
本文我們就採用蟻群演算法來解決這一問題。
數學建模
在開始之前,我們首先需要將“負載均衡排程”這個問題進行數學建模,量化各項指標,並對映到蟻群演算法中。
問題描述
求一種最優的任務分配策略,能夠將N個長度不等的任務按照某一種策略分配給M個處理能力不同的伺服器節點,並且N個任務的完成時間最短。
在這個問題中,我們將所有任務的完成時間作為衡量分配策略優良的指標。每一種分配策略都是這個問題的一個可行解。那麼具有最小完成時間的分配策略就是這個問題的最優解。
引數定義
var tasks = [];
var taskNum = 100;
複製程式碼
- tasks:任務陣列,陣列的下標表示任務的編號,陣列的值表示任務的長度。比如:tasks[0]=10表示第一個任務的任務長度是10.
- taskNum:任務的數量,也就是tasks陣列的長度。這裡為了提高程式碼的可讀性才專門使用taskNum來表示任務數量。
var nodes = [];
var nodeNum = 10;
複製程式碼
- nodes:處理節點的陣列。陣列的下標表示處理節點的編號,陣列值表示節點的處理速度。比如:nodes[0]=10表示第1個處理節點的處理速度為10.
- nodeNum:處理節點的數量,也就是nodes陣列的長度。這裡也是為了提高程式碼的可讀性才專門使用nodeNum來表示節點的數量。
var iteratorNum;
var antNum;
複製程式碼
- iteratorNum:蟻群演算法一共需要迭代的次數,每次迭代都有antNum只螞蟻進行任務分配。
- antNum:每次迭代中螞蟻的數量。每隻螞蟻都是一個任務排程者,每次迭代中的每一隻螞蟻都需要完成所有任務的分配,這也就是一個可行解。
var timeMatrix = [];
複製程式碼
- 任務處理時間矩陣。
- 它是一個二維矩陣。比如:timeMatrix[i][j]就表示第i個任務分配給第j個節點所需的處理時間。
- 這個矩陣是基於tasks陣列和nodes陣列計算而來的。比如task[i]表示第i個任務的任務長度,nodes[j]表示第j個節點的處理速度。所以,timeMatrix[i][j]=task[i]/nodes[j].
var pheromoneMatrix = [];
var maxPheromoneMatrix = [];
var criticalPointMatrix = [];
複製程式碼
- pheromoneMatrix:資訊素矩陣
- 它是一個二維矩陣,用於記錄任務i分配給節點j這條路徑上的資訊素濃度。
- 比如:pheromoneMatrix[i][j]=0.5就表示任務i分配給節點j這條路徑上的資訊素濃度為0.5
- maxPheromoneMatrix:pheromoneMatrix矩陣的每一行中最大資訊素的下標。
- 比如:maxPheromoneMatrix[0]=5表示pheromoneMatrix第0行的所有資訊素中,最大資訊素的下標是5.
- criticalPointMatrix:在一次迭代中,採用隨機分配策略的螞蟻的臨界編號。
- 比如:如果將螞蟻數量設為10,那麼每次迭代中都有10只螞蟻完成所有任務的分配工作。並且分配過程是按照螞蟻編號從小到大的順序進行的(螞蟻從0開始編號)。如果criticalPointMatrix[0]=5,那麼也就意味著,在分配第0個任務的時候,編號是0~5的螞蟻根據資訊素濃度進行任務分配(即:將任務分配給本行中資訊素濃度最高的節點處理),6~9號螞蟻則採用隨機分配的方式(即:將任務隨機分配給任意一個節點處理)。
- 為什麼要這麼做? 如果每隻螞蟻都將任務分配給資訊素濃度最高的節點處理,那麼就會出現停滯現象。也就是演算法過早地收斂至一個區域性最優解,無法發現全域性最優解。 因此需要一部分螞蟻遵循資訊素最高的分配策略,還需要一部分螞蟻遵循隨機分配的策略,以發現新的區域性最優解。
var p = 0.5;
var q = 2;
複製程式碼
- p:每完成一次迭代後,資訊素衰減的比例。 我們知道,在真實的蟻群中,螞蟻分泌的資訊素會隨著時間的推移而漸漸衰減。那麼在演算法中,我們使得資訊素每完成一次迭代後進行衰減,但在一次迭代過程中,資訊素濃度保持不變。
- q:螞蟻每次經過一條路徑,資訊素增加的比例。 我們也知道,在真實的蟻群中,螞蟻會在行進過程中分泌資訊素。那麼在演算法中,我們使得演算法每完成一次迭代後,就將螞蟻經過的路徑上增加資訊素q,但在一次迭代過程中,資訊素濃度不變。
演算法初始化
// 初始化任務集合
tasks = initRandomArray(_taskNum, taskLengthRange);
// 初始化節點集合
nodes = initRandomArray(_nodeNum, nodeSpeendRange);
複製程式碼
在正式開始之前,我們需要初始化任務陣列和節點陣列。這裡採用隨機賦值的方式,我們給tasks隨機建立100個任務,每個任務的長度是10~100之間的隨機整數。再給nodes隨機建立10個節點,每個節點的處理速度是10~100之間的隨機整數。
OK,準備工作完成,下面來看蟻群演算法的實現。
蟻群演算法
/**
* 蟻群演算法
*/
function aca() {
// 初始化任務執行時間矩陣
initTimeMatrix(tasks, nodes);
// 初始化資訊素矩陣
initPheromoneMatrix(taskNum, nodeNum);
// 迭代搜尋
acaSearch(iteratorNum, antNum);
}
複製程式碼
正如你所看到的,蟻群演算法並不複雜,總體而言就是這三部:
- 初始化任務執行時間矩陣
- 初始化資訊素矩陣
- 迭代搜尋
當然,第一第二步都較為簡單,相對複雜的程式碼在“迭代搜尋”中。那麼下面我們就分別來看一下這三個步驟的實現過程。
初始化任務執行時間矩陣
/**
* 初始化任務處理時間矩陣
* @param tasks 任務(長度)列表
* @param nodes 節點(處理速度)列表
*/
function initTimeMatrix(tasks, nodes) {
for (var i=0; i<tasks.length; i++) {
// 分別計算任務i分配給所有節點的處理時間
var timeMatrix_i = [];
for (var j=0; j<nodes.length; j++) {
timeMatrix_i.push(tasks[i] / nodes[j]);
}
timeMatrix.push(timeMatrix_i);
}
}
複製程式碼
通過上文的學習我們已經知道,當任務長度陣列tasks和節點處理速度陣列nodes確定下來後,所有任務的執行時間都是可以確定下來了,用公式tasks[i]/nodes[j]計算一下即可,也就是“時間=長度/速度”,小學數學知識。OK,那麼timeMatrix矩陣的計算也就是這樣。
這裡再次介紹下timeMatrix矩陣的含義:timeMatrix[i][j]表示任務i分配給節點j處理所需要的時間,其計算公式也就是:
timeMatrix[i][j] = tasks[i]/nodes[j]
複製程式碼
初始化資訊素矩陣
/**
* 初始化資訊素矩陣(全為1)
* @param taskNum 任務數量
* @param nodeNum 節點數量
*/
function initPheromoneMatrix(taskNum, nodeNum) {
for (var i=0; i<taskNum; i++) {
var pheromoneMatrix_i = [];
for (var j=0; j<nodeNum; j++) {
pheromoneMatrix_i.push(1);
}
pheromoneMatrix.push(pheromoneMatrix_i);
}
}
複製程式碼
初始化資訊素矩陣也就是將資訊素矩陣中所有元素置為1.
這裡再次重申一下資訊素矩陣的含義,pheromoneMatrix[i][j]表示將任務i分配給節點j這條路徑的資訊素濃度。
注意:我們將負載均衡排程過程中的一次任務分配當作蟻群演算法中一條路徑。如:我們將“任務i分配給節點j”這一動作,當作螞蟻從任務i走向節點j的一條路徑。因此,pheromoneMatrix[i][j]就相當於i——>j這條路徑上的資訊素濃度。
迭代搜尋過程
/**
* 迭代搜尋
* @param iteratorNum 迭代次數
* @param antNum 螞蟻數量
*/
function acaSearch(iteratorNum, antNum) {
for (var itCount=0; itCount<iteratorNum; itCount++) {
// 本次迭代中,所有螞蟻的路徑
var pathMatrix_allAnt = [];
for (var antCount=0; antCount<antNum; antCount++) {
// 第antCount只螞蟻的分配策略(pathMatrix[i][j]表示第antCount只螞蟻將i任務分配給j節點處理)
var pathMatrix_oneAnt = initMatrix(taskNum, nodeNum, 0);
for (var taskCount=0; taskCount<taskNum; taskCount++) {
// 將第taskCount個任務分配給第nodeCount個節點處理
var nodeCount = assignOneTask(antCount, taskCount, nodes, pheromoneMatrix);
pathMatrix_oneAnt[taskCount][nodeCount] = 1;
}
// 將當前螞蟻的路徑加入pathMatrix_allAnt
pathMatrix_allAnt.push(pathMatrix_oneAnt);
}
// 計算 本次迭代中 所有螞蟻 的任務處理時間
var timeArray_oneIt = calTime_oneIt(pathMatrix_allAnt);
// 將本地迭代中 所有螞蟻的 任務處理時間加入總結果集
resultData.push(timeArray_oneIt);
// 更新資訊素
updatePheromoneMatrix(pathMatrix_allAnt, pheromoneMatrix, timeArray_oneIt);
}
}
複製程式碼
這個過程略微複雜,但也還好,且聽我一一道來。
在整個蟻群演算法中,一共要進行iteratorNum次迭代。每一次迭代都會產生當前的最優分配策略,也就是“區域性最優解”。迭代的次數越多,那麼區域性最優解就越接近於全域性最優解。但是,迭代次數過多會造成負載均衡器大量的時間和效能上的開銷,從而無法滿足海量任務的排程。但迭代次數太少了,可能得到的並不是全域性最優解。那麼這個問題如何解決呢?有兩種辦法:
- 限定迭代次數 為了避免過多的迭代,我們可以事先設定一個迭代次數,從而迭代了這麼多次後,就把當前的區域性最優解當作全域性最優解。
- 設定誤差允許範圍 我們還可以事先設定一個允許的誤差範圍。當迭代N此後,當前最優的任務處理時間在這個允許範圍之內了,那麼就停止迭代。
這兩種方式各有千秋,我們這裡選擇第一種——限定迭代次數。並且將迭代次數限定為1000次。
注意:收斂速度也是衡量演算法優良的一個重要指標。比如演算法1迭代10次就能找到全域性最優解,而演算法2迭代1000次才能找到全域性最優解。所以演算法1的收斂速度要優於演算法2.
下面介紹上述演算法的執行流程。
蟻群演算法一共要進行iteratorNum次迭代,每次迭代中,所有螞蟻都需要完成所有任務的分配。因此上述演算法採用了三層for迴圈,第一層用於迭代次數的迴圈,在本演算法中一共要迴圈1000次;第二層用於螞蟻的迴圈,本演算法一共有10只螞蟻,因此需要進行10次迴圈;第三層用於所有任務的迴圈,本演算法一共有100個任務,因此需要迴圈100次,每一次迴圈,都將當前任務按照某一種策略分配給某一個節點,並在pathMatrix_oneAnt矩陣中記錄螞蟻的分配策略。
pathMatrix_oneAnt是一個二維矩陣,所有元素要麼是0要麼是1.比如:pathMatrix_oneAnt[i][j]=1就表示當前螞蟻將任務i分配給了節點j處理,pathMatrix_oneAnt[i][j]=0表示任務i沒有分配給節點j處理。該矩陣的每一行都有且僅有一個元素為1,其他元素均為0.
每一隻螞蟻當完成這100個任務的分配之後,就會產生一個pathMatrix_oneAnt矩陣,用於記錄該只螞蟻的分配策略。那麼當10只螞蟻均完成任務的分配後,就會產生一個pathMatrix矩陣。這是一個三維矩陣,第一維記錄了螞蟻的編號,第二維表示任務的下標,第三維表示節點的編號,從而pathMatrix[x][i][j]=1就表示編號為x的螞蟻將任務i分配給了節點j處理;pathMatrix[x][i][j]=0就表示編號為x的螞蟻沒有將任務i分配給了節點j處理。
這10只螞蟻完成一次任務的分配也被稱為一次迭代。每完成一次迭代後,都要使用calTime_oneIt函式在計算本次迭代中,所有螞蟻的任務處理時間,並記錄在timeArray_oneIt矩陣中。
在每次迭代完成前,還需要使用updatePheromoneMatrix函式來更新資訊素矩陣。
下面就分別詳細介紹迭代搜尋過程中的三個重要函式:
- 任務分配函式:assignOneTask
- 任務處理時間計算函式:calTime_oneIt
- 更新資訊素函式:updatePheromoneMatrix
任務分配函式
/**
* 將第taskCount個任務分配給某一個節點處理
* @param antCount 螞蟻編號
* @param taskCount 任務編號
* @param nodes 節點集合
* @param pheromoneMatrix 資訊素集合
*/
function assignOneTask(antCount, taskCount, nodes, pheromoneMatrix) {
// 若當前螞蟻編號在臨界點之前,則採用最大資訊素的分配方式
if (antCount <= criticalPointMatrix[taskCount]) {
return maxPheromoneMatrix[taskCount];
}
// 若當前螞蟻編號在臨界點之後,則採用隨機分配方式
return random(0, nodeNum-1);
}
複製程式碼
任務分配函式負責將一個指定的任務按照某種策略分配給某一節點處理。分配策略一共有兩種:
-
按資訊素濃度分配 也就是將任務分配給本行中資訊素濃度最高的節點處理。比如:當前的任務編號是taskCount,當前的資訊素濃度矩陣是pheromoneMatrix,那麼任務將會分配給pheromoneMatrix[taskCount]這一行中資訊素濃度最高的節點。
-
隨機分配 將任務隨意分配給某一個節點處理。
那麼,這兩種分配策略究竟如何選擇呢?答案是——根據當前螞蟻的編號antCount。
通過上文可知,矩陣criticalPointMatrix用於記錄本次迭代中,採用不同分配策略的螞蟻編號的臨界點。比如:criticalPointMatrix[i]=5就表示編號為0~5的螞蟻在分配任務i的時候採用“按資訊素濃度”的方式分配(即:將任務i分配給資訊素濃度最高的節點處理);而編號為6~9的螞蟻在分配任務i時,採用隨機分配策略。
計算任務處理時間
/**
* 計算一次迭代中,所有螞蟻的任務處理時間
* @param pathMatrix_allAnt 所有螞蟻的路徑
*/
function calTime_oneIt(pathMatrix_allAnt) {
var time_allAnt = [];
for (var antIndex=0; antIndex<pathMatrix_allAnt.length; antIndex++) {
// 獲取第antIndex只螞蟻的行走路徑
var pathMatrix = pathMatrix_allAnt[antIndex];
// 獲取處理時間最長的節點 對應的處理時間
var maxTime = -1;
for (var nodeIndex=0; nodeIndex<nodeNum; nodeIndex++) {
// 計算節點taskIndex的任務處理時間
var time = 0;
for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {
if (pathMatrix[taskIndex][nodeIndex] == 1) {
time += timeMatrix[taskIndex][nodeIndex];
}
}
// 更新maxTime
if (time > maxTime) {
maxTime = time;
}
}
time_allAnt.push(maxTime);
}
return time_allAnt;
}
複製程式碼
每完成一次迭代,都需要計算本次迭代中所有螞蟻的行走路徑(即:所有螞蟻的任務處理之間),並記錄在time_allAnt矩陣中。
在實際的負載均衡排程中,各個節點的任務處理是平行計算的,所以,所有任務的完成時間應該是所有節點任務完成時間的最大值,並非所有任務完成時間的總和。
每完成一次迭代,就會產生一個time_allAnt矩陣,並且加入resultData矩陣中。當演算法完成所有迭代後,所有螞蟻的所有任務處理時間都被記錄在resultData矩陣中,它是一個二維矩陣。比如:resultData[x][y]=10代表第x次迭代中第y只螞蟻的任務處理時間是10.
更新資訊素
/**
* 更新資訊素
* @param pathMatrix_allAnt 本次迭代中所有螞蟻的行走路徑
* @param pheromoneMatrix 資訊素矩陣
* @param timeArray_oneIt 本次迭代的任務處理時間的結果集
*/
function updatePheromoneMatrix(pathMatrix_allAnt, pheromoneMatrix, timeArray_oneIt) {
// 所有資訊素均衰減p%
for (var i=0; i<taskNum; i++) {
for (var j=0; j<nodeNum; j++) {
pheromoneMatrix[i][j] *= p;
}
}
// 找出任務處理時間最短的螞蟻編號
var minTime = Number.MAX_VALUE;
var minIndex = -1;
for (var antIndex=0; antIndex<antNum; antIndex++) {
if (timeArray_oneIt[antIndex] < minTime) {
minTime = timeArray_oneIt[antIndex];
minIndex = antIndex;
}
}
// 將本次迭代中最優路徑的資訊素增加q%
for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {
for (var nodeIndex=0; nodeIndex<nodeNum; nodeIndex++) {
if (pathMatrix_allAnt[minIndex][taskIndex][nodeIndex] == 1) {
pheromoneMatrix[taskIndex][nodeIndex] *= q;
}
}
}
maxPheromoneMatrix = [];
criticalPointMatrix = [];
for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {
var maxPheromone = pheromoneMatrix[taskIndex][0];
var maxIndex = 0;
var sumPheromone = pheromoneMatrix[taskIndex][0];
var isAllSame = true;
for (var nodeIndex=1; nodeIndex<nodeNum; nodeIndex++) {
if (pheromoneMatrix[taskIndex][nodeIndex] > maxPheromone) {
maxPheromone = pheromoneMatrix[taskIndex][nodeIndex];
maxIndex = nodeIndex;
}
if (pheromoneMatrix[taskIndex][nodeIndex] != pheromoneMatrix[taskIndex][nodeIndex-1]){
isAllSame = false;
}
sumPheromone += pheromoneMatrix[taskIndex][nodeIndex];
}
// 若本行資訊素全都相等,則隨機選擇一個作為最大資訊素
if (isAllSame==true) {
maxIndex = random(0, nodeNum-1);
maxPheromone = pheromoneMatrix[taskIndex][maxIndex];
}
// 將本行最大資訊素的下標加入maxPheromoneMatrix
maxPheromoneMatrix.push(maxIndex);
// 將本次迭代的螞蟻臨界編號加入criticalPointMatrix(該臨界點之前的螞蟻的任務分配根據最大資訊素原則,而該臨界點之後的螞蟻採用隨機分配策略)
criticalPointMatrix.push(Math.round(antNum * (maxPheromone/sumPheromone)));
}
}
複製程式碼
每完成一次迭代,都需要更新資訊素矩陣,這個函式的包含了如下四步:
-
將所有資訊素濃度降低p% 這個過程用來模擬資訊素的揮發。
-
找出本次迭代中最短路徑,並將該條路徑的資訊素濃度提高q% 每次迭代,10只螞蟻就會產生10條路徑(即10種任務分配策略),我們需要找出最短路徑,並將該條路徑的資訊素濃度提高。
-
更新maxPheromoneMatrix矩陣 步驟1和步驟2完成後,資訊素矩陣已經更新完畢。接下來需要基於這個最新的資訊素矩陣,計算每行最大資訊素對應的下標,即:maxPheromoneMatrix矩陣。通過上文可知,該矩陣供函式assignOneTask在分配任務時使用。
-
更新criticalPointMatrix矩陣 緊接著需要更新criticalPointMatrix矩陣,記錄採用何種任務分配策略的螞蟻臨界編號。 比如:資訊素矩陣第0行的元素為pheromoneMatrix[0]={1,3,1,1,1,1,1,1,1,1},那麼criticalPointMatrix[0]的計算方式如下:
- 計算最大資訊素的概率:最大資訊素/該行所有資訊素之和
- 3/(1+3+1+1+1+1+1+1+1+1)=0.25
- 計算螞蟻的臨界下標:螞蟻數量*最大資訊素的概率
- 10*0.25=3(四捨五入)
- 所以criticalPointMatrix[0]=3
- 也就意味著在下一次迭代過程中,當分配任務0時,0~3號螞蟻將該任務分配給資訊素濃度最高的節點,而4~9號螞蟻採用隨機分配策略。
- 計算最大資訊素的概率:最大資訊素/該行所有資訊素之和
結果分析
演算法的執行結果如下圖所示:
橫座標為迭代次數,縱座標為任務處理時間。 每個點表示一隻螞蟻的任務處理時間。上圖的演算法的迭代次數為100,螞蟻數量為1000,所以每次迭代都會產生1000種任務分配方案,而每次迭代完成後都會挑選出一個當前最優方案,並提升該方案的資訊素濃度,從而保證在下一次迭代中,選擇該方案的概率較高。並且還使用一定概率的螞蟻採用隨機分配策略,以發現更優的方案。
從圖中我們可以看到,大約迭代30次時,出現了全域性最優解。
寫在最後
所有程式碼我已經上傳至我的Github,大家可以隨意下載。 https://github.com/bz51/AntColonyAlgorithm
上面一共有兩個問題:
- aca.html
- aca.js
蟻群演算法的實現均在aca.js中,你把程式碼down下來之後直接在瀏覽器開啟aca.html即可檢視。歡迎各位star。也歡迎關注我的公眾號,我定期分享技術乾貨的地方~