本文通過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的作用就是將一個描述狀態的向量映射到另一個行爲向量,這不正是深度神經網路擅長的嗎?
所以,深度強化學習 出生了!
最近在學,過幾天更新吧!