模擬退火演算法案例
2018年的華為軟體精英挑戰賽題目簡介:給出華為雲虛擬機器過去的租借數量歷史資料,用以訓練模型並預測下一個時間段裡的虛擬機器租借數量,然後把這些預測得到的虛擬機器裝填進一定規格的物理機中,即分為預測和裝填兩個部分。
總結一下裝填部分使用的模擬退火演算法:
演算法原理
裝填的基礎演算法是FF(首次適應演算法),而虛擬機器的序列會影響FF演算法的裝填效果。比較明顯的是FFD把序列降序之後再使用FF演算法往往效果更好。而模擬退火演算法目的就是獲得一個較好的序列。
模擬退火的基本原理就是設定一個初始溫度、一個最低溫度,一個降溫速率。例如初始溫度取100,最低溫度取1,降溫速率取0.9 。則溫度迭代從100開始,每次迭代都使當前溫度變為0.9T,直至到達1為止。
每次迭代都隨機交換虛擬機器序列中的任意兩個元素,然後用這個序列進行FF裝填,得到一個評判分數J_cur,而設當前記錄的最優分數為J_min(評判分數=目前使用物理機數-1+最後一個物理機的資源佔有率,因此分數越低越好)。設dE=J_cur-J_min,
如果dE<=0,即當前分數更小,裝填效果更好,則接受這個序列
如果dE>0,即當前分數更大,裝填效果更差,則按一定機率接受這個序列
這個機率的公式為:
是一個負數,隨著T的降低,減小,則exp()也減小。
就是說溫度越低,接受這個不合格的序列的可能性越低。這樣的目的是讓迭代前期接受更多不合格序列以期脫離區域性最優,而迭代後期接受更少不合格序列以得到最優方案。
每次迭代都設一個0到1之間的隨機數rand,用以跟P對比,如果P>rand,則接受這個方案。
迭代初期,由於T比較大,P趨近於1,因此很大概率會接收這個不合格方案,而迭代後期T比較小,P趨近於0,因此很大概率不接受這個方案。
如下圖,藍線為P、橙線為rand、灰線為P-rand。
可見隨著迭代次數的增加,灰線會慢慢的變得小於0
而上圖其實並不嚴格遵循公式,如果嚴格遵循,則效果如下:
可見P一直都很大,這樣無論迭代多少次,都會一律接受這些不合格的方案,也就相當於隨機序列了。原因是dE這個數值太小了,無論T怎麼變化都沒有用,所以我加了一個引數進去:
,k取50,然後效果就比較好了。
實際應用
單獨摘出裝填部分,則需要一個原始的虛擬機器序列和一些基本引數。我都設定好了:
15種虛擬機器規格為:
flavor1 1 1024
flavor2 1 2048
flavor3 1 4096
flavor4 2 2048
flavor5 2 4096
flavor6 2 8192
flavor7 4 4096
flavor8 4 8192
flavor9 4 16384
flavor10 8 8192
flavor11 8 16384
flavor12 8 32768
flavor13 16 16384
flavor14 16 32768
flavor15 16 65536
備註:flavor名稱 CPU核數 記憶體大小(MB)
且要求預測全部15種虛擬機器的數量
物理機規格為56核CPU、128G記憶體
需要優化的資源維度為CPU
則預測效果為:
可見按FF演算法和BFD演算法裝填需要17臺物理機
而模擬退火演算法只需要15臺物理機
程式碼
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <set>
#include <vector>
#include <ctime>
#include <sstream>
#include <cstdlib>
#include <cmath>
#include <iomanip>
#include <fstream>
using namespace std;
int FlavorCPU[20]={0,1,1,1,2,2,2,4,4,4,8,8,8,16,16,16}; //15種型號虛擬機器的CPU規格 單位為核
int FlavorMEM[20]={0,1,2,4,2,4,8,4,8,16,8,16,32,16,32,64};//15種型號虛擬機器的記憶體規格 單位為G
int PredictRes[20]={0,30,18,4,14,34,2,6,36,14,4,20,12,2,6,2};//下一個時間段的虛擬機器預測結果
int vir_cnt=15; //需要預測的虛擬機種類數量
int ChosenType[20];//被選中要預測的虛擬機種類
int phy_cpu=56,phy_memory=128;//物理機的統一規格為56核CPU、128G記憶體
string task="CPU"; //任務是使CPU資源佔用率最小
struct PhyMachine//一臺物理機
{
int RemainingCPU,RemainingMEM;//剩餘資源量
int VirItem[20];//VirItem[i]代表這臺物理機中裝載的種類i虛擬機器的數量
PhyMachine(){
for(int i=0;i<20;i++) VirItem[i]=0;
RemainingCPU=phy_cpu;
RemainingMEM=phy_memory;
}
};
class InstallCases//一個裝填樣例
{
public :
vector<PhyMachine> phymachine;//這種裝填方法總共要用多少臺物理機
vector<int> flavors; //待安裝的虛擬機器序列
double point; //對此裝填樣例優劣的評判分數,分數越低、效果越好
void Install() //用FF方法按虛擬機器序列順序安裝虛擬機器
{
PhyMachine p;
phymachine.push_back(p);//先塞一個空的物理機進去
for(int i=0;i<(int)flavors.size();i++)
{
int flavortype=flavors[i];
bool flag_enough=false;
for(int j=0;j<(int)phymachine.size();j++)
{
if(phymachine[j].RemainingCPU>=FlavorCPU[flavortype]&&phymachine[j].RemainingMEM>=FlavorMEM[flavortype])
{
phymachine[j].RemainingCPU-=FlavorCPU[flavortype];
phymachine[j].RemainingMEM-=FlavorMEM[flavortype];
phymachine[j].VirItem[flavortype]++;
flag_enough=true;
break;
}
}
if(flag_enough==false){//說明物理機不夠用,新開一個物理機
PhyMachine p;
p.RemainingCPU-=FlavorCPU[flavortype];
p.RemainingMEM-=FlavorMEM[flavortype];
p.VirItem[flavortype]++;
phymachine.push_back(p);
}
}
}
double J()//計算當前這個裝填方式的分數point,分數越低越好
{
//分數=(目前物理機數-1) +最後一個物理機的資源佔用率
point=(double)(phymachine.size()-1);
if(task=="CPU"){
double freecpu=(double)phy_cpu-(double)phymachine[phymachine.size()-1].RemainingCPU;
point+=freecpu/(double)phy_cpu;
}
else if(task=="MEM"){
double freemem=(double)phy_memory-(double)phymachine[phymachine.size()-1].RemainingMEM;
point+=freemem/(double)phy_memory;
}
return point;
}
};
vector<InstallCases> Cases;//儲存不同的裝填方式
void Install()
{
double T=100.0,T_min=1,r=0.995;//分別是起始溫度、停止溫度 、溫度下降速率
double J_min,J_cur,dE; //分別是目前最好的分數、當前安裝樣例的分數 、前面兩者的差值
//首先要把PredictRes這個數組裡的東西展開轉換成虛擬機器序列
vector<int> vi_flavors;
for(int i=0;i<vir_cnt;i++){
int flavortype=ChosenType[i];//需要預測的虛擬機種類
int flavornum=PredictRes[flavortype];//當前需要預測的虛擬機器的租借數
while(flavornum--){
vi_flavors.push_back(flavortype);
}
}
cout<<"原始虛擬機器序列:\n";
for(int i=0;i<(int)vi_flavors.size();i++) cout<<vi_flavors[i]<<" ";
cout<<endl;
vector<int> dice;
for(int i=0;i<(int)vi_flavors.size();i++){
dice.push_back(i);
}
InstallCases Case0;//原始裝填案例,相當於直接FF裝填
Case0.flavors=vi_flavors;
Case0.Install();
J_min=Case0.J();
cout<<"按原始虛擬機器序列裝填所得分數:"<<J_min<<endl;
Cases.push_back(Case0); //把這個裝填方法push進向量裡儲存起來
srand( (unsigned)time( NULL ));
int temcnt0=0,temcnt1=0;//分別是 得到更差的序列的次數和接受更差序列的次數
ofstream fout("T.csv");
while(T>T_min)
{
random_shuffle(dice.begin(),dice.end());//隨機打亂這個骰子向量裡的元素順序
vector<int> new_vi_flavors=vi_flavors;
swap(new_vi_flavors[dice[0]],new_vi_flavors[dice[1]]);//隨機交換虛擬機器序列中的兩個虛擬機器的位置
// for(int i=0;i<vi_flavors.size();i++) cout<<new_vi_flavors[i]<<" ";
// cout<<endl;
//按當前虛擬機器序列裝填:
InstallCases Case;
Case.flavors=new_vi_flavors;
Case.Install();
J_cur=Case.J(); //當前裝填方式的得分
dE=J_cur-J_min; //與最低分數之間的差值
if(dE<=0){ //J_cur<=J_min,即如果移動後J_cur分數更低得到更優解,則總是接受移動
vi_flavors=new_vi_flavors;//下一次迴圈會以這個陣列為基礎進行隨機變化
Cases.push_back(Case) ;
J_min=J_cur;
}
else{//這裡才是模擬退火的精華,會進來這裡說明dE<0,即 0<exp(dE/T)<1
temcnt0++;
double random_num=rand()/(double)(RAND_MAX);
fout<<exp(-50*dE/T)<<','<<random_num<<','<<exp(-50*dE/T)-random_num<<endl;
if ( exp(-50*dE/T) > random_num ){//T越大 (dE/T)越接近1,就越有可能接受這個方案
temcnt1++;
vi_flavors=new_vi_flavors;//下一次迴圈會以這個陣列為基礎進行隨機變化
Cases.push_back(Case) ;
J_min=J_cur;
}
}
T=r*T;//降溫
}
cout<<"得到更差的序列的次數:"<<temcnt0<<endl;
cout<<"接受更差序列的次數:"<<temcnt1<<endl;
fout.close();
}
bool cmp_cpu(int a,int b)
{
return (FlavorCPU[a]>FlavorCPU[b]);
}
bool cmp_mem(int a,int b)
{
return (FlavorMEM[a]>FlavorMEM[b]);
}
int BFD()
{
if(task=="CPU") sort(ChosenType,ChosenType+vir_cnt,cmp_cpu);
else if(task=="MEM") sort(ChosenType,ChosenType+vir_cnt,cmp_mem);
InstallCases Casebfd;
PhyMachine p;
Casebfd.phymachine.push_back(p);//先放一個空物理機進去
for(int i=0;i<vir_cnt;i++){
int temflavor=ChosenType[i];//需要預測的虛擬機種類
int temnum=PredictRes[temflavor];//當前需要預測的虛擬機器的租借數
while(temnum--)
{
bool flag=false;//現有的物理機是否夠用
int minspace_index=0;
bool first=true;
for(int k=0;k<(int)Casebfd.phymachine.size();k++)//遍歷已有的所有物理機
{
if(Casebfd.phymachine[k].RemainingCPU>=FlavorCPU[temflavor]&&Casebfd.phymachine[k].RemainingMEM>=FlavorMEM[temflavor])
{
if(first==true){
minspace_index=k;
first=false;
}
else if(task=="CPU"&&Casebfd.phymachine[k].RemainingCPU<Casebfd.phymachine[minspace_index].RemainingCPU)
minspace_index=k;
else if(task=="MEM"&&Casebfd.phymachine[k].RemainingMEM<Casebfd.phymachine[minspace_index].RemainingMEM)
minspace_index=k;
flag=true;
}
}
if(flag==true)//現在要放進空閒空間最小的箱子中
{
Casebfd.phymachine[minspace_index].RemainingCPU-=FlavorCPU[temflavor];
Casebfd.phymachine[minspace_index].RemainingMEM-=FlavorMEM[temflavor];
Casebfd.phymachine[minspace_index].VirItem[temflavor]++;
}
if(flag==false)//物理機不夠用
{
PhyMachine p;
Casebfd.phymachine.push_back(p);
int phycnt=Casebfd.phymachine.size();
Casebfd.phymachine[phycnt-1].RemainingCPU-=FlavorCPU[temflavor];
Casebfd.phymachine[phycnt-1].RemainingMEM-=FlavorMEM[temflavor];
Casebfd.phymachine[phycnt-1].VirItem[temflavor]++;
}
}
}
cout<<"使用BFD方法所需物理機數"<<Casebfd.phymachine.size()<<endl;
Casebfd.point=Casebfd.phymachine.size();
Cases.push_back(Casebfd) ;
}
int FindBestCase()
{
BFD(); //試一下BFD放置演算法並push進Case中儲存,用於對比體現模擬退火演算法的效果
cout<<"Cases.size"<<Cases.size()<<endl;
double min_point=100000.0;
int min_index=0;
for(int i=0;i<(int)Cases.size();i++)
{
if(Cases[i].point<=min_point){
min_point=Cases[i].point;
min_index=i;
}
}
cout<<"最佳分數"<< setprecision(5)<<min_point<<endl;
cout<<"最佳裝填樣例索引="<<min_index<<endl;
cout<<"最佳虛擬機器序列:\n";
for(int i=0;i<(int)Cases[min_index].flavors.size();i++){
cout<<Cases[min_index].flavors[i]<<" ";
}
cout<<endl;
return min_index;
}
int main()
{
for(int i=0;i<vir_cnt;i++){//設定為15種虛擬機器規格都要預測
ChosenType[i]=i+1;
}
Install();//開始裝填
FindBestCase();//在所有裝填案例中尋找最佳案例
}
--------------------- 本文來自 weixin_41519463 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/weixin_41519463/article/details/79951962?utm_source=copy