本文首發於:行者AI
強化學習 (reinforcement learning) 是機器學習和人工智能里的一類問題,研究如何通過一系列的順序決策來達成一個特定目標。它是一類算法, 是讓計算機實現從一開始什么都不懂,腦袋里沒有一點想法,,通過不斷地嘗試, 從錯誤中學習, 最后找到規律, 學會了達到目的的方法。 這就是一個完整的強化學習過程。這里我們可以引用下方圖做一個更直觀形象的解釋。
Agent為智能體,也就是我們的算法,在游戲當中以玩家的形式出現。智能體通過一系列策略,輸出一個行為(Action)從而作用到環境(Environment),而環境則返回作用后的狀態值也就是圖中的觀察(Observation)和獎勵值(Reward)。當環境返回獎勵值給智能體之后,更新自身所在的狀態,而智能體獲取到新的Observation。
1. ml-agents
1.1 介紹
目前游戲大部分Unity游戲數量龐大,引擎完善,訓練環境好搭建。由於Unity 可以跨平台,可以在Windows、Linux平台下訓練后再轉成WebGL發布到網頁上。而mlagents是Unity的一款開源插件,能讓開發者在Unity的環境下進行訓練,甚至不用去編寫python端的代碼,不用深入理解PPO,SAC等算法。只要開發者配置好參數,就可以很輕松的使用強化學習的算法來訓練自己的模型。
1.2 Anaconda、tensorflow及tensorboard安裝
本文介紹的ml-agents需要通過Python與Tensorflow通信,訓練時從ml-agents的Unity端拿到Observation、Action、Reward、Done等信息傳入Tensorflow進行訓練,然后將模型的決策傳入Unity。因此在安裝ml-agents前,需要根據如下鏈接進行tensorflow的安裝。
Tensorboard方便數據可視化,方便分析模型是否達到預期。
1.3 ml-agents安裝步驟
(1) 前往github下載ml-agents (本實例采用release6版本)
(2) 將壓縮包解壓,把com.unity.ml-agents
,com.unity.ml-agents.extensions
放入Unity的Packages目錄下(如果沒有請創建一個),將manifest.json
中加入此兩個目錄。
(3) 安裝完成后,到工程中就導入后,建立個新腳本,輸入以下引用以驗證安裝成功
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Policies;
public class MyAgent : Agent
{
}
2. ml-agents訓練實例
2.1 概要及工程
Environment 通常利用馬爾可夫過程來描述,agent 通過采取某種 policy 來產生Action,和 Environment 交互,產生一個 Reward。之后 agent 根據 Reward 來調整優化當前的 policy。
本例實際工程參考消消樂規則,湊齊三個同樣的顏色即可得分,本實例去除了四個連色及多連的額外獎勵(以方便設計環境)
工程實例下載處 點擊前往
Unity工程導出部分請參考官方 點擊前往 。
下面將從四個角度來分享項目項目實踐的方法,接口抽離、選算法、設計環境、參數調整。
2.2 游戲框架AI接口抽離
將工程的Observation、Action需要的接口從游戲中抽離出來。用於傳入游戲當前的狀態和執行游戲的動作。
static List<ML_Unit> states = new List<ML_Unit>();
public class ML_Unit
{
public int color = (int)CodeColor.ColorType.MaxNum;
public int widthIndex = -1;
public int heightIndex = -1;
}
//從當前畫面中,拿到所有方塊的信息,包含所在位置x(長度),位置y(高度),顏色(坐標軸零點在左上)
public static List<ML_Unit> GetStates()
{
states.Clear();
var xx = GameMgr.Instance.GetGameStates();
for(int i = 0; i < num_widthMax;i++)
{
for(int j = 0; j < num_heightMax; j++)
{
ML_Unit tempUnit = new ML_Unit();
try
{
tempUnit.color = (int)xx[i, j].getColorComponent.getColor;
}
catch
{
Debug.LogError($"GetStates i:{i} j:{j}");
}
tempUnit.widthIndex = xx[i, j].X;
tempUnit.heightIndex = xx[i, j].Y;
states.Add(tempUnit);
}
}
return states;
}
public enum MoveDir
{
up,
right,
down,
left,
}
public static bool CheckMoveValid(int widthIndex, int heigtIndex, int dir)
{
var valid = true;
if (widthIndex == 0 && dir == (int)MoveDir.left)
{
valid = false;
}
if (widthIndex == num_widthMax - 1 && dir == (int)MoveDir.right)
{
valid = false;
}
if (heigtIndex == 0 && dir == (int)MoveDir.up)
{
valid = false;
}
if (heigtIndex == num_heightMax - 1 && dir == (int)MoveDir.down)
{
valid = false;
}
return valid;
}
//執行動作的接口,根據位置信息和移動方向,調用游戲邏輯移動方塊。widthIndex 0-13,heigtIndex 0-6,dir 0-3 0上 1右 2下 3左
public static void SetAction(int widthIndex,int heigtIndex,int dir,bool immediately)
{
if (CheckMoveValid(widthIndex, heigtIndex, dir))
{
GameMgr.Instance.ExcuteAction(widthIndex, heigtIndex, dir, immediately);
}
}
2.3 游戲AI算法選擇
走入強化學習項目的第一個課題,面對眾多算法,選擇一個合適的算法能事半功倍。如果對算法的特性還不太熟悉,可以直接使用ml-agents自帶的PPO和SAC。
本例筆者最開始使用的PPO算法,嘗試了比較多的調整,平均9步才能走對一步,效果比較糟糕。
后來仔細分析游戲的環境,由於此工程的三消類的游戲,每次的環境都完全不一樣,每一步的結果對下一步產生的影響並沒有多大關系,對馬爾科夫鏈的需求不強。由於PPO是OnPolicy的policy-based的算法,每次更新的策略更新非常小心,導致結果很難收斂(筆者嘗試了XX布,依然沒有收斂)。
相比DQN是OffPolicy的value-base算法,可以收集大量環境的參數建立Qtable,逐步找到對應的環境的最大值。
簡單地說,PPO是在線學習,每次自己跑幾百步后,回過頭來學習這幾百步哪里做得對,哪里做的不對,然后更新學習后,再跑幾百步,如此反復。這樣學習效率慢不說,還很難找到全局最優的解。
而DQN是離線學習,可以跑上億步,然后回去把這些跑過的地方都拿出來學習,然后很容易找到全局最優的點。
(本例使用PPO做演示,后續分享在ml-agents外接算法,使用外部工具stable_baselines3,采用DQN的算法來訓練)
2.4 游戲AI設計環境
當我們確定了算法框架之后,如何設計Observation、Action及Reward,便成了決定訓練效果的決定性因素。在這個游戲中,環境的這里的環境主要有兩個變量,一個是方塊的位置,另一個是方塊的顏色。
--Observation:
針對如果上圖,我們的本例長14、寬7、顏色有6種。
ml-agents使用的swish作為激活函數,可以使用不太大的浮點數(-10f ~10f),但是為了讓agents獲得環境更純凈,訓練效果更理想,我們還是需要對環境進行編碼。
本例筆者使用Onehot的方式進行環境編碼,左上角定位坐標零點。如此下來,左上角的青色方塊的環境編碼就可以表示為 長[0,0,0,0,0,0,0,0,0,0,0,0,0,1],
高[0,0,0,0,0,0,1],顏色按固定枚舉來處理( 黃,綠,紫,粉,藍,紅)顏色[0,0,0,0,1,0]。
環境總共包含 (14+7+6)14 * 7 = 2646
代碼示例:
public class MyAgent : Agent
{
static List<ML_Unit> states = new List<ML_Unit>();
public class ML_Unit
{
public int color = (int)CodeColor.ColorType.MaxNum;
public int widthIndex = -1;
public int heightIndex = -1;
}
public static List<ML_Unit> GetStates()
{
states.Clear();
var xx = GameMgr.Instance.GetGameStates();
for(int i = 0; i < num_widthMax;i++)
{
for(int j = 0; j < num_heightMax; j++)
{
ML_Unit tempUnit = new ML_Unit();
try
{
tempUnit.color = (int)xx[i, j].getColorComponent.getColor;
}
catch
{
Debug.LogError($"GetStates i:{i} j:{j}");
}
tempUnit.widthIndex = xx[i, j].X;
tempUnit.heightIndex = xx[i, j].Y;
states.Add(tempUnit);
}
}
return states;
}
List<ML_Unit> curStates = new List<ML_Unit>();
public override void CollectObservations(VectorSensor sensor)
{
//需要判斷是否方塊移動結束,以及方塊結算結束
var receiveReward = GameMgr.Instance.CanGetState();
var codeMoveOver = GameMgr.Instance.IsCodeMoveOver();
if (!codeMoveOver || !receiveReward)
{
return;
}
//獲得環境的狀態信息
curStates = MlagentsMgr.GetStates();
for (int i = 0; i < curStates.Count; i++)
{
sensor.AddOneHotObservation(curStates[i].widthIndex, MlagentsMgr.num_widthMax);
sensor.AddOneHotObservation(curStates[i].heightIndex, MlagentsMgr.num_heightMax);
sensor.AddOneHotObservation(curStates[i].color, (int)CodeColor.ColorType.MaxNum);
}
}
}
--Action:
每個方塊可以上下左右移動,我們需要記錄的最小信息包含,14*7個方塊,以及每個方塊可以移動4個方向,本例方向枚舉(上,右,下,左)。
左上為零點,左上角的青色方塊占據了Action的前四個動作,分別是(左上角的青色方塊向上移動,左上角的青色方塊向右移動,左上角的青色方塊向下移動,
左上角的青色方塊向左移動)。
那么動作總共包含 14 * 7 * 4 = 392
細心的讀者可能會發現 左上角的青色方塊 並不能往上和往左移動,這時我們需要設置Actionmask,來屏蔽掉這些在規則上禁止的動作。
代碼示例:
public class MyAgent : Agent
{
public enum MoveDir
{
up,
right,
down,
left,
}
public void DecomposeAction(int actionId,out int width,out int height,out int dir)
{
width = actionId / (num_heightMax * num_dirMax);
height = actionId % (num_heightMax * num_dirMax) / num_dirMax;
dir = actionId % (num_heightMax * num_dirMax) % num_dirMax;
}
//執行動作,並獲得該動作的獎勵
public override void OnActionReceived(float[] vectorAction)
{
//需要判斷是否方塊移動結束,以及方塊結算結束
var receiveReward = GameMgr.Instance.CanGetState();
var codeMoveOver = GameMgr.Instance.IsCodeMoveOver();
if (!codeMoveOver || !receiveReward)
{
Debug.LogError($"OnActionReceived CanGetState = {GameMgr.Instance.CanGetState()}");
return;
}
if (invalidNums.Contains((int)vectorAction[0]))
{
//方塊結算的調用,這里可以獲得獎勵(這里是懲罰,因為這是在屏蔽動作內,訓練的時候會調用所有的動作,在非訓練的時候則不會進此邏輯)
GameMgr.Instance.OnGirdChangeOver?.Invoke(true, -5, false, false);
}
DecomposeAction((int)vectorAction[0], out int widthIndex, out int heightIndex, out int dirIndex);
//這里回去執行動作,移動對應的方塊,朝對應的方向。執行完畢后會獲得獎勵,並根據情況重置場景
MlagentsMgr.SetAction(widthIndex, heightIndex, dirIndex, false);
}
//MlagentsMgr.SetAction調用后,執行完動作,會進入這個函數
public void RewardShape(int score)
{
//計算獲得的獎勵
var reward = (float)score * rewardScaler;
AddReward(reward);
//將數據加入tensorboard進行統計分析
Mlstatistics.AddCumulativeReward(StatisticsType.action, reward);
//每一步包含懲罰的動作,可以提升探索的效率
var punish = -1f / MaxStep * punishScaler;
AddReward(punish);
//將數據加入tensorboard進行統計分析
Mlstatistics.AddCumulativeReward( StatisticsType.punishment, punish);
}
//設置屏蔽動作actionmask
public override void CollectDiscreteActionMasks(DiscreteActionMasker actionMasker)
{
// Mask the necessary actions if selected by the user.
checkinfo.Clear();
invalidNums.Clear();
int invalidNumber = -1;
for (int i = 0; i < MlagentsMgr.num_widthMax;i++)
{
for (int j = 0; j < MlagentsMgr.num_heightMax; j++)
{
if (i == 0)
{
invalidNumber = i * (num_widthMax + num_heightMax) + j * num_heightMax + (int)MoveDir.left;
actionMasker.SetMask(0, new[] { invalidNumber });
}
if (i == num_widthMax - 1)
{
invalidNumber = i * (num_widthMax + num_heightMax) + j * num_heightMax + (int)MoveDir.right;
actionMasker.SetMask(0, new[] { invalidNumber });
}
if (j == 0)
{
invalidNumber = i * (num_widthMax + num_heightMax) + j * num_heightMax + (int)MoveDir.up;
actionMasker.SetMask(0, new[] { invalidNumber });
}
if (j == num_heightMax - 1)
{
invalidNumber = i * (num_widthMax + num_heightMax) + j * num_heightMax + (int)MoveDir.down;
actionMasker.SetMask(0, new[] { invalidNumber });
}
}
}
}
}
原工程消除過程中使用大量協程,有很高的延遲,我們需要再訓練時把延遲的時間擠出來。
為了不影響游戲的主邏輯,一般情況下把協程里面的yield return new WaitForSeconds(fillTime)中的fillTime改成0.001f,這樣可以在不大量修改游戲邏輯的情況下,在模型選擇Action后能最快得到Reward。
public class MyAgent : Agent
{
private void FixedUpdate()
{
var codeMoveOver = GameMgr.Instance.IsCodeMoveOver();
var receiveReward = GameMgr.Instance.CanGetState();
if (!codeMoveOver || !receiveReward /*||!MlagentsMgr.b_isTrain*/)
{
return;
}
//因為有協程需要等待時間,需要等待產生Reward后才去請求決策。所以不能使用ml-agents自帶的DecisionRequester
RequestDecision();
}
}
2.5 參數調整
在設計好模型后,我們先初步跑一版本,看看結果跟我們設計的預期有多大的差異。
首先配置yaml文件,用於初始化網絡的參數:
behaviors:
SanXiaoAgent:
trainer_type: ppo
hyperparameters:
batch_size: 128
buffer_size: 2048
learning_rate: 0.0005
beta: 0.005
epsilon: 0.2
lambd: 0.9
num_epoch: 3
learning_rate_schedule: linear
network_settings:
normalize: false
hidden_units: 512
num_layers: 2
vis_encode_type: simple
memory: null
reward_signals:
extrinsic:
gamma: 0.99
strength: 1.0
init_path: null
keep_checkpoints: 25
checkpoint_interval: 100000
max_steps: 1000000
time_horizon: 128
summary_freq: 1000
threaded: true
self_play: null
behavioral_cloning: null
framework: tensorflow
訓練代碼請參照官方提供的接口,本例使用release6版本,命令如下
mlagents-learn config/ppo/sanxiao.yaml --env=G:\mylab\ml-agent-buildprojects\sanxiao\windows\display\121001display\fangkuaixiaoxiaole --run-id=121001xxl --train --width 800 --height 600 --num-envs 2 --force --initialize-from=121001
訓練完成后,打開Anaconda,在ml-agents工程主目錄上輸入tensorboard --logdir=results --port=6006,復制http://PS20190711FUOV:6006/到瀏覽器上打開,即可看到訓練結果。
(mlagents) PS G:\mylab\ml-agents-release_6> tensorboard --logdir=results --port=6006
TensorBoard 1.14.0 at http://PS20190711FUOV:6006/ (Press CTRL+C to quit)
訓練效果圖如下:
move count 為消掉一次方塊,需要走的平均步數,大概需要9布才能走正確一步。在使用Actionmask情況下,可以在6步左右消除一次方塊。
–Reward:
根據上面表格的Reward,查看獎勵獎勵設計的均值。筆者喜歡控制在0.5到2之間。過大過小可以調整rewardScaler。
//MlagentsMgr.SetAction調用后,執行完動作,會進入這個函數
public void RewardShape(int score)
{
//計算獲得的獎勵
var reward = (float)score * rewardScaler;
AddReward(reward);
//將數據加入tensorboard進行統計分析
Mlstatistics.AddCumulativeReward(StatisticsType.action, reward);
//每一步包含懲罰的動作,可以提升探索的效率
var punish = -1f / MaxStep * punishScaler;
AddReward(punish);
//將數據加入tensorboard進行統計分析
Mlstatistics.AddCumulativeReward( StatisticsType.punishment, punish);
}
3. 總結及雜談
目前ml-agents官方做法使用模仿學習,使用專家數據在訓練網絡。
筆者在此例中嘗試PPO,有一定的效果。但PPO目前針對三消訓練起來有一定難度的,比較難收斂,很難找到全局最優。
設置環境和Reward需要嚴謹的測試,否則對結果會產生極大的誤差,且難以排查。
強化學習目前算法迭代比較快,如果以上有錯誤的地方,歡迎指正,大家一起進步。
因篇幅有限,不能把整個項目的代碼全放出來,如有興趣研究的同學,可以在下方留言,我可以完整項目通過郵箱的方式發給大家。
后續將分享在ml-agents外接算法,使用外部工具stable_baselines3,采用DQN的算法來訓練。
PS:更多技術干貨,快關注【公眾號 | xingzhe_ai】,與行者一起討論吧!