iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0

「我過了這麼多年也沒有聽到你說半句話。等聽到了,第一句話你就說,你才是真正的我。那你為什麼不早點宣佈這重大的消息?」
「這些年來我一直在這裡,可是,這是你第一次安靜到能夠聽見我的聲音。」
「如果你才是真正的我,那麼,我是誰?」
「你不能指望一下子就了解每件事情。你為什麼不休息休息?」
「好吧!可是在睡之前,我想知道怎麼稱呼你?」
「稱呼我?為什麼?我就是你。」
「我不能叫你『我』,這樣我會弄迷糊的。」
「好吧,叫我『山』。」
「為什麼叫『山』?」
「為什麼不?」

-- <為自己出征>,羅伯.費雪著,王石珍譯

先前曾經很粗略的提及我們使用殘差塊來形成一個 ResNet 網路,但這裡還是描述一下實際上的串接方法。

輸入

這個神經網路的輸入資料,可以溯源至伺服器端輸出的盤面編碼,也就是 encode,使用多個 ndarray::Array1 物件存放內容之後,最後將之全部整合起來,

    let ret = e
        .into_shape((BOARD_DATA,))
        .unwrap()
        .into_iter()
        .chain(m.into_shape((MAP_DATA,)).unwrap().into_iter())
        .chain(t.into_shape((TURN_DATA,)).unwrap().into_iter())
        .chain(fm.into_shape((FLOW_MAP_DATA,)).unwrap().into_iter())
        .chain(fe.into_shape((FLOW_ENV_DATA,)).unwrap().into_iter())
        .collect::<Array1<_>>();

    assert_eq!(ret.len(), DATA_UNIT - CODE_DATA);
    ret

這些 em 之類的 ndarray 物件,對應到遊戲局面的各個部份,其中 fmfe 也編碼了部份的標準回合內容。更詳細說,fm 實際上編碼了兩組 5x5 的棋盤資訊,用來代表標準回合中的羅盤階段可能有兩個選點(自己的選點,以及醫療方潛在可能會挪動疫病方的羅盤標記);fe 則支援 11 組 6x6 的棋盤選點。這些都是以 u8 型別儲存。

當初考慮過疫病方的標記是否就用 -1 儲存。但是後來沒有那樣進行。仔細想想還好沒有那樣進行,因為後來我將多個 5x5 與 6x6 平面資訊都整併成 Cx7x7 的張量,而邊界的 padding 我用 -1。如果當初資料有包含 -1 的話,直覺上應該不適合那樣做?

這些資訊會被轉換成方便網路傳輸的格式,如

    fn update_agent(&mut self, g: &Game, a: &Action, s: &'static str) -> bool {
        let encoded = encode(g, &a);
        let enc = encoded.as_slice().unwrap();
        let sb = s.as_bytes();

        let response = [&sb, &enc[..]].concat();
        assert!(response.len() == DATA_UNIT);
        match self.write(&response) {

這裡先使用 as_slice 轉換之後,就能夠直接當作一群字元陣列,透過 write 方法寫出去了(這裡的 self 可以是 TcpStream 或是單元測試時描述的 &[u8])。

到了代理人端,從 Python 接收也很容易

    def play(self):
        data = self.s.recv(CODE_DATA+S)

因為都配得剛剛好,又都是本機傳輸,所以我也沒有另外處理可能傳輸不完全的狀況。收到之後,在有需要推論的時候,當然還是得將這些資訊轉換成張量,

            # Make next action
            self.state = np.frombuffer(self.current_node.state[4:], dtype=np.uint8)
            self.state = torch.from_numpy(np.copy(self.state)).float().unsqueeze(0).to(self.device)
            self.dataset.write(self.state.cpu().numpy().tobytes()) # section 1: state
            policy, valid, value = self.model(self.state)
            probabilities = spice(torch.nn.functional.softmax(policy, dim=1).squeeze(0), TEMPERATURE)

這個片段是每次模擬次數到了,相當於思考完畢,使用模型下出真正的一步,這時候將稍早的 data(此時已經轉換到當前節點,current_node 當中存放),扣除前四個位元組(狀態碼),先使用 numpy 轉一次,再透過 torch.from_numpy 轉成可以使用的模式。帶入 self.model 的內容則需要追溯到 examples/coord_clients/reinforcement_network.py 裡面,推論時走過的 forward 方法:

    def forward(self, x):
        # Padding the game board and the map to get ready for a Nx6x7x7 tensor
        genv, gmap, gturn, gfm, gfe = x[:, 0: BOARD_DATA], x[:, BOARD_DATA: E_MAP], x[:, E_MAP: E_TURN], x[:, E_TURN: E_FM], x[:, E_FM: S]
        genv = torch.nn.functional.pad(genv.reshape(-1, 8, 6, 6), (1, 0, 1, 0), mode='constant', value=-1.0)
        gmap = torch.nn.functional.pad(gmap.reshape(-1, 2, 5, 5), (1, 1, 1, 1), mode='constant', value=-1.0)
        gturn = torch.nn.functional.pad(gturn.reshape(-1, 1, 5, 5), (1, 1, 1, 1), mode='constant', value=-1.0)
        gfm = torch.nn.functional.pad(gfm.reshape(-1, 2, 5, 5), (1, 1, 1, 1), mode='constant', value=-1.0)
        gfe = torch.nn.functional.pad(gfe.reshape(-1, 11, 6, 6), (1, 0, 1, 0), mode='constant', value=-1.0)

根據幾個索引拆分這些值(可與先前在伺服器的 encode 相互參照),然後做成標準的四個維度(NxCxHxW,個數x特徵數x高x寬)的張量,就可以繼續推論。這裡我是都把他們做成 7x7 的大小,其實也不確定這樣是不是會比較好就是了。

至於殘差塊,都是很普通的作法,這裡就不描述了。

輸出

輸出部份,除了強化學習常見的策略(policy)與價值(value)兩組輸出之外,我自己還獨創了一個合法性(valid)。之所以想要加這個,是來自先前的經驗。其實遠在 2023 年初啟動這個專案之前的一年,我曾經試著想要攻略 The Crew:探索第九行星這個別出心裁的吃墩遊戲。我當時想法很簡單,就是如果我不論勝負,只論是否習得規則,應該是比較簡單的切入點吧?但是,當時怎麼訓練,網路怎麼變(Well,當時只會很粗淺的亂拼全連接層的 MLP,所以也的確不算是把所有可用的神經網路結構都探索過了,但...),代理人就是總是會跟錯牌,以萬分之一左右的機率。

這不可忍受,對吧?所以我希望可以特定把合法性當作一組輸出,所以就也會是最佳化的對象。

當時的研究,沒有後續。但或許在附錄時可以補充一些思考的過程。

至於損失函數的定義,也是用了標準的、很菜市場的作法,

    policy_loss_func = torch.nn.CrossEntropyLoss()
    valid_loss_func = torch.nn.BCEWithLogitsLoss(reduction='mean')
    value_loss_func = torch.nn.MSELoss()
    misunderstanding_loss_func = torch.nn.L1Loss(reduction='sum')

策略分佈部份,輸出未機率化的原始值,所以用 CrossEntropy 是標準的損失函數作法;合法性的部份,輸出的是一群一或是零,也是問了 ChatGPT 才知道有這個 BCE 可以用(AI 的事情問 AI 最清楚???);然後盤面價值的部份,是以 tanh 作為最後一層處理過再輸出,希望數值可以總是落在 -1 ~ 1 之間。我總覺得合法性還不夠保險,所以又設計了第四個可以最佳化的項,定義成 (1-valid)*policyzeros 之間的差距,直接使用 L1Loss,這是 torch 版本的 MAE 損失函數。

至於這四個的比重,在整個訓練當中也是時常調整的超參數。

不過,在昨天的目前狀況一節更新的最新訓練狀況,其實已經將最後一個 misunderstanding 移除了。其實只是為了儀表板一目了然;是,我不知道怎麼更動 tensorboard 工具的預設每頁圖表數量。所以 12 張的話,現在的 3 種輸出、每種輸出 3 個損失函數值與一個加權的總損失值,就剛剛好了。

目前狀況

的確是蠻順利的!以昨天展示的那個練得很不錯的,首次觀測到一個比較直覺的現象,就是在遊玩過程當中引用蒙地卡羅模擬(明天開始的系列文會提到)的結果,而非模型直接推論出來的結果,有助於提昇勝率這件事情。比方說一樣的一組設置、一樣的對局數,隨機猴子的基準值是

Doctor's winrate: 53.00%
Plague's steps to win: 27.38 +/- 13.77
Doctor's steps to win: 32.45 +/- 11.09

而模型直接推論的結果是,

Doctor's winrate: 48.00%
Plague's steps to win: 23.00 +/- 11.38
Doctor's steps to win: 29.62 +/- 11.48

雖然看起來很好,但 5%,可能就是讓人有一點點信心而已。關於怎麼合理的統計這些勝負值,從中講出有學理的概念,還是需要好好讀書學習。但經驗上來講,5% 開始可以有一點點信心了。合理可以相信與預估,下次跑,很可能也差不多,但也可能更接近。

但重點是,當我提昇了模擬步數來到 10 步之後,

Doctor's winrate: 42.00%
Plague's steps to win: 22.93 +/- 10.92
Doctor's steps to win: 32.57 +/- 11.59

由於都是引用在疫病方上,所以這可謂效果顯著!而且疫病方的平均勝利步數與勝利步數的標準差(程式碼在這裡,大致上,開資料夾、限定副檔名讀取、平均與標準差的數學函數,都是 ChatGPT 幫忙生的)都降低了,這也是讓人很樂觀的指標。因此這個專案,從去年二月至此,總算開始有點樂觀了。


上一篇
強化學習/AlphaZero 演算法介紹
下一篇
導入蒙地卡羅樹搜尋之 1
系列文
DeltaPathogen:國產雙人不對稱抽象棋「疫途」之桌遊 AI 實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言