10分鐘搞懂蟻羣算法

title

螞蟻幾乎沒有視力,但他們卻可以在黑暗的世界中找到食物,並且可以找到一條從洞穴到食物的最短路徑。它們是如何作到的呢?html

螞蟻尋找食物的過程

單隻螞蟻的行爲及其簡單,行爲數量在10種之內,但成千上萬只螞蟻組成的蟻羣卻能擁有巨大的智慧,這離不開它們信息傳遞的方式——信息素。node

螞蟻在行走過程當中會釋放一種稱爲「信息素」的物質,用來標識本身的行走路徑。在尋找食物的過程當中,根據信息素的濃度選擇行走的方向,並最終到達食物所在的地方。git

信息素會隨着時間的推移而逐漸揮發。github

在一開始的時候,因爲地面上沒有信息素,所以螞蟻們的行走路徑是隨機的。螞蟻們在行走的過程當中會不斷釋放信息素,標識本身的行走路徑。隨着時間的推移,有若干只螞蟻找到了食物,此時便存在若干條從洞穴到食物的路徑。因爲螞蟻的行爲軌跡是隨機分佈的,所以在單位時間內,短路徑上的螞蟻數量比長路徑上的螞蟻數量要多,從而螞蟻留下的信息素濃度也就越高。這爲後面的螞蟻們提供了強有力的方向指引,愈來愈多的螞蟻彙集到最短的路徑上去。算法

什麼是蟻羣算法?

蟻羣算法就是模擬螞蟻尋找食物的過程,它可以求出從原點出發,通過若干個給定的需求點,最終返回原點的最短路徑。這也就是著名的旅行商問題(Traveling Saleman Problem,TSP)。數組

本文使用蟻羣算法來解決分佈式環境下的負載均衡調度問題。瀏覽器

蟻羣算法的應用——負載均衡調度

集羣模式是目前較爲經常使用的一種部署結構,也就是當單機處理能力沒法知足業務需求,那麼就增長處理節點,並由一個負載均衡器負責請求的調度。然而對於一個龐大系統而言,狀況每每比較複雜。集羣中節點的處理能力每每各不相同,並且不一樣任務的處理複雜度也不盡相同。那麼負載均衡器如何進行任務分配,使得集羣性能達到最優?資源利用率達到最高呢?這是一個極具挑戰又頗有價值的問題。服務器

本文咱們就採用蟻羣算法來解決這一問題。負載均衡

數學建模

在開始以前,咱們首先須要將「負載均衡調度」這個問題進行數學建模,量化各項指標,並映射到蟻羣算法中。dom

問題描述

求一種最優的任務分配策略,可以將N個長度不等的任務按照某一種策略分配給M個處理能力不一樣的服務器節點,而且N個任務的完成時間最短。

在這個問題中,咱們將全部任務的完成時間做爲衡量分配策略優良的指標。每一種分配策略都是這個問題的一個可行解。那麼具備最小完成時間的分配策略就是這個問題的最優解。

title

參數定義

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次迭代。每一次迭代都會產生當前的最優分配策略,也就是「局部最優解」。迭代的次數越多,那麼局部最優解就越接近於全局最優解。可是,迭代次數過多會形成負載均衡器大量的時間和性能上的開銷,從而沒法知足海量任務的調度。但迭代次數太少了,可能獲得的並非全局最優解。那麼這個問題如何解決呢?有兩種辦法:

  1. 限定迭代次數 爲了不過多的迭代,咱們能夠事先設置一個迭代次數,從而迭代了這麼屢次後,就把當前的局部最優解看成全局最優解。
  2. 設置偏差容許範圍 咱們還能夠事先設置一個容許的偏差範圍。當迭代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);
}
複製代碼

任務分配函數負責將一個指定的任務按照某種策略分配給某一節點處理。分配策略一共有兩種:

  1. 按信息素濃度分配 也就是將任務分配給本行中信息素濃度最高的節點處理。好比:當前的任務編號是taskCount,當前的信息素濃度矩陣是pheromoneMatrix,那麼任務將會分配給pheromoneMatrix[taskCount]這一行中信息素濃度最高的節點。

  2. 隨機分配 將任務隨意分配給某一個節點處理。

那麼,這兩種分配策略究竟如何選擇呢?答案是——根據當前螞蟻的編號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)));
    }
}
複製代碼

每完成一次迭代,都須要更新信息素矩陣,這個函數的包含了以下四步:

  1. 將全部信息素濃度下降p% 這個過程用來模擬信息素的揮發。

  2. 找出本次迭代中最短路徑,並將該條路徑的信息素濃度提升q% 每次迭代,10只螞蟻就會產生10條路徑(即10種任務分配策略),咱們須要找出最短路徑,並將該條路徑的信息素濃度提升。

  3. 更新maxPheromoneMatrix矩陣 步驟1和步驟2完成後,信息素矩陣已經更新完畢。接下來須要基於這個最新的信息素矩陣,計算每行最大信息素對應的下標,即:maxPheromoneMatrix矩陣。經過上文可知,該矩陣供函數assignOneTask在分配任務時使用。

  4. 更新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號螞蟻採用隨機分配策略。

結果分析

算法的運行結果以下圖所示:

title

橫座標爲迭代次數,縱座標爲任務處理時間。 每一個點表示一隻螞蟻的任務處理時間。上圖的算法的迭代次數爲100,螞蟻數量爲1000,因此每次迭代都會產生1000種任務分配方案,而每次迭代完成後都會挑選出一個當前最優方案,並提高該方案的信息素濃度,從而保證在下一次迭代中,選擇該方案的機率較高。而且還使用必定機率的螞蟻採用隨機分配策略,以發現更優的方案。

從圖中咱們能夠看到,大約迭代30次時,出現了全局最優解。

寫在最後

全部代碼我已經上傳至個人Github,你們能夠隨意下載。 https://github.com/bz51/AntColonyAlgorithm

上面一共有兩個問題:

  • aca.html
  • aca.js

蟻羣算法的實現均在aca.js中,你把代碼down下來以後直接在瀏覽器打開aca.html便可查看。歡迎各位star。也歡迎關注個人公衆號,我按期分享技術乾貨的地方~

title
相關文章
相關標籤/搜索