強化學習-利用Q-Learning算法玩走方格遊戲(C++)

本文通過Q-Learning算法玩走方格遊戲的例子和代碼,嘗試說明Q-Learning的思想與基本實現方法。

 

隨着人工智能的發展,強化學習相關的算法受到越來越多的關注。強化學習是一種無監督學習,通過智能體(Agent)自行根據現實世界及自身的狀態(state),決定策略(action),與現實世界交互,改變自身及現實世界的狀態(state),並從狀態中獲取本次執行的策略的獎勵(reward),來優化在不同環境及自身狀態下的決策。

 

Q-Learning是強化學習算法家族中應用最廣泛的一種算法,也體現着強化學習的基本思想。前些年大火的Alpha-Go程序的思想就是基於Q-Learning算法,加入了衆多搜索與優化算法。

 

走方格遊戲的規則是:有一個5*5的方格地圖(大小可調),智能體從左上角出發,目標是右下角的出口;地圖上有若干地雷,遇到地雷則智能體死亡;智能體可以通過向上下左右四個方向行走到達出口獲得遊戲勝利。

程序思路:

1、數據結構Q-table:Q-table是一張表,存儲着智能體的每一個狀態下,執行不同行爲時的預期獎勵。走方格遊戲中,對於固定的一張地圖,智能體的狀態可以由向量(x,y)表示其位置,現實世界是不變的,所以不算作狀態;下面程序中,數組Qtable[x][y][i]就代表智能體在(x,y)位置下執行動作i時的預期獎勵,i有上下左右四個值

 

備註:之前的代碼思路是:枚舉每一張可能的地圖,再枚舉每張地圖下智能體在每個位置,將向量(地圖,x,y)作爲狀態,但是這樣的後果是:存不下!所以處於學習的角度,將地圖固定,不算做狀態。事實上,用C++計算一5*5的地圖的策略只需要0.4秒。

 

2、算法:優化決策的過程

由馬爾科夫決策過程對Qtable進行優化。該過程的核心方程是

Qtable[x][y][i] = Qtable[x][y][i] + rate * ( reward[x1][y1] + max( Qtable[x1][y1][i] ))

即:執行一個策略之後智能體從(x,y)移動到(x1,y1)點,那麼在狀態(x,y)下執行動作 i 的獎勵就是:下一個行爲本身的收益 + 走到下一個方格之後,最好的預期收益。具體舉例來說,從(x,y)移動到(x1,y1)點,如果(x1,y1)點是地雷,那麼收益爲負,如果是好吃的,那麼收益爲正(遊戲中沒有好吃的,just 舉例),這就是“向右走”這一行爲本身帶來的收益;而走到(x1,y1)之後,可能離終點近了一步,那麼收益爲正,否則如果走進了死衚衕或者走向了起點,那麼收益爲負,這就是“向右走這一行爲的預期收益”。

上式中,rate爲一個比例係數,決定了預期收益的權重;本程序中取0.8

 

3、技巧:貪心繫數Greedy = 0.5

Q-learning本質上是貪心算法。但是如果每次都取預期獎勵最高的行爲去做,那麼在訓練過程中可能無法探索其他可能的行爲,甚至會進入“局部最優”,無法完成遊戲。所以,由貪心繫數,使得智能體有Greedy的概率採取最優行爲,也有一定概率探索新的路徑。

 

4、打分表:表示智能體走到某個位置獲得的分數。在走方格遊戲中,走到地雷分數爲-1,平地爲0,終點爲5。注意:走到平地必須爲0,否則智能體可能會向左走一步得分,再向右走得分,陷入這樣的死循環。而且分值不要太大,訓練過程中會對得分進行累加,後面可能數值很大

 

 

程序採用C++實現

什麼?我爲什麼不用python

python在多循環的整形運算程序上的速度至少也是C++的30倍,運行0.4秒調試一次,和運行10秒調試一次,差距還是蠻大的!

有幾個程序中應該注意的問題:

1、所有變量用double,Qtable本身就是double,否則浮點乘整形調試很麻煩

2、一步一步輸出Qtable看優化過程對不對

代碼如下:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <cstdlib>
#include <ctime>
#include <conio.h>
using namespace std;

//宏定義,定義上下左右四個動作的代號 
#define up 0
#define down 1
#define left 2
#define right 3

//馬爾可夫決策過程公式中的比例係數 
double rate = 0.8;

//走到每個位置的分數
//實際上就是存儲的地圖,0代表可以走,-1代表地雷,5代表終點 
int score[5][5] = {0,0,0,-1,0,
				 0,-1,0,0,0,
				 -1,0,-1,0,-1,
				 -1,0,0,0,-1,
				 0,-1,0,0,5};
//Q-table 
double Qtable[5][5][4] = {0};

//記錄是否走過這一點,測試結果時使用 
int vis[5][5] = {0};

//智能體的位置, 
int x = 0;
int y = 0;

//是否到達終點,0不到達,1到達 
int dead = 0;

//貪心繫數,20表示執行貪心決策的概率爲20% 
int greedy = 20;

//全局初始化函數 
bool game_set(){
	
	//初始化Q-table,智能體不能走出邊界,所以再邊界上再往外走,就會得到負獎勵 
	for (int i = 0; i <= 4; i++){
		Qtable[0][i][up] = -100;
		Qtable[4][i][down] = -100;
		Qtable[i][0][left] = -100;
		Qtable[i][4][right] = -100;
	}
		
	//初始化隨機數種子 
	srand(time(0));
	
	return true;
}

//每次訓練之前的變量初始化 
bool game_init(){
	//位置初始化爲0,0 
	x = 0;
	y = 0;
	dead = 0;
	return true;
}
//沒用的變量 
int cnt = 0;

//獲取最大的預期獎勵,對應馬爾可夫決策過程 
//Qtable[x][y][i] = Qtable[x][y][i] + rate * ( reward[x1][y1] + max( Qtable[x1][y1][i] )) 
//中的 max( Qtable[x1][y1][i]) 
double get_expected_max_score(){
	double s = -10000;
	for (int i = 0; i <= 3; i++)
		s = max(s, Qtable[x][y][i]);
	return s;
	
}

//執行一步操作,返回執行動作後的 獎勵 ,對應馬爾可夫決策過程 
//Qtable[x][y][i] = Qtable[x][y][i] + rate * ( reward[x1][y1] + max( Qtable[x1][y1][i] )) 
//中的 Qtable[x][y][i] + rate * ( reward[x1][y1] + max( Qtable[x1][y1][i] )) 

double game_go(int dir){
	
	//記錄現在所在位置 
	int xx = x, yy = y;
	
	//如果走出了邊界,則獎勵爲0,且x,y的值不變 
	if (x == 0 && dir == up)	{return 0;}
	if (x == 4 && dir == down)	{return 0;}
	if (y == 0 && dir == left)	{return 0;}
	if (y == 4 && dir == right)	{return 0;}
	
	
	
	//走到下一步 
	if (dir == up) x--;
	if (dir == down) x++;
	if (dir == right) y++;
	if (dir == left) y--;
	
	//如果到了終點,返回到達終點的獎勵 
	if (x == 4 && y == 4){
		dead = 1;
		return score[x][y];
	}

	//得到執行動作後的預期獎勵,見函數說明 
	double tmp = get_expected_max_score();
	
	//對應馬爾可夫決策過程 
	//Qtable[x][y][i] = Qtable[x][y][i] + rate * ( reward[x1][y1] + max( Qtable[x1][y1][i] ))
	//中的 reward[x1][y1] + max( Qtable[x1][y1][i] ) 
	return score[x][y] + 1.0 * rate * tmp;
	
}

//訓練結果測試 
void game_final_test(){
	//本局遊戲初始化 
	game_init();
	
	//當沒有走到終點時 
	while (!(x == 4 && y == 4)){
		int op = 0;
	//選取Qtable中獎勵最大的行爲執行 
	double maxx = -1000000;
	for (int i = 0; i < 4; i++)
		maxx = max(maxx + 0.0, Qtable[x][y][i]);
	for (int i = 0; i < 4; i++)	
		if (maxx == Qtable[x][y][i])
			op = i;
		
		game_go(op);
		//如果走到了一個點,記錄這個點的vis = 1, 方便輸出觀察 
		vis[x][y] = 1;
	}
	//輸出,帶有 @ 符號的代表智能體選擇的路徑 
	for (int i = 0 ; i <= 4; i++){
		for (int j = 0; j <= 4; j++){
			cout<<score[i][j];
			if (vis[i][j] == 1)
				cout<<'@';
			cout<<"    ";
		}
		cout<<endl;
	}
	
}


int main(){
	//全局初始化 
	game_set();
	
	//總共訓練1000次,取決於具體訓練效果 
	int episode = 1000;
	
	while(episode > 0){
		//遊戲初始化 
		game_init();
		episode--;
		int j = 0;
		
		//每輪遊戲走50步,走到終點或者走夠50步時結束 
		while(j < 50){
			j++;
			//operation,代表下一步要執行的行爲 
			int op;
			//記錄現在的位置 
			int xx = x, yy = y;
			
			//一定概率下,隨機選擇行爲 
			if (rand() % 101 > greedy){
				op = rand() % 4;
			}	
			//一定概率下,走最優解 
			else{
				int maxx = -1000000;
				for (int i = 0; i < 4; i++)
					maxx = max(maxx + 0.0, Qtable[x][y][i]);
				for (int i = 0; i < 4; i++)	
					if (maxx == Qtable[x][y][i])
						op = i;
			}
			//if (op == 0) cout<<"up"<<endl;
			//if (op == 1) cout<<"down"<<endl;
			//if (op == 2) cout<<"left"<<endl;
			//if (op == 3) cout<<"right"<<endl;	
			
			//執行行爲,獲取獎勵 
			double reward = game_go(op);
	
			//進行馬爾可夫決策過程,優化Qtable 
			Qtable[xx][yy][op] += reward / 1000;
			
			//如果到達終點,遊戲結束 
			if (dead == 1)	break;
		
		
	}
		
	}
	//輸出Qtable 
	for (int i = 0;i <= 4;i ++){
		for (int j = 0;j <= 4;j ++){
			for (int k = 0;k <= 3;k ++)
			cout<<Qtable[i][j][k]<<' ';
			cout<<"      ";
		}
	cout<<endl;
	}
	cout<<endl<<endl;
	
	//測試智能體最終選擇的路徑 
	game_final_test();
}

地圖可以隨意改動,附幾張地圖的測試結果

 

 

效果還是不錯的,最終的訓練結果Qtable中,需要只要是正值的點,都是可以走的。因爲加入了隨機數,所以訓練結果可能不同,但是在很大概率下,智能體能夠選擇最短路徑;因爲終點的大獎勵是一步一步傳播到其他的點,越遠的點,獲得來自終點的獎勵就越少。

 

這就是Q-Learning算法

 

很明顯,Q-Learning算法很消耗內存,對於狀態少的狀態,可以存儲,但是對於圍棋那樣,棋盤狀態千變萬化,狀態極多的活動,Q-Learning是無法計算的。更致命的是,現實世界總是連續的,而Qtable是離散的,這也使其很難處理複雜問題

但轉念一想,Qtable的作用就是將一個描述狀態的向量映射到另一個行爲向量,這不正是深度神經網路擅長的嗎?

所以,深度強化學習  出生了!

最近在學,過幾天更新吧!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章