## 研讀 預期目標 和 定點數計算 >Brian ### 定點數 概念:把精度為 n 的浮點數向右位移 n 個位元 #### Q Notation Q23.8: 表示該數有 23 個整數位、8 個小數位,是 32 位二補數。 #### 乘法除法校正理由 假設兩個精度為 n 的 2 進位浮點數 a, b 其定點數表達為 $a_f = a \times 2^n$ $b_f = b \times 2^n$ $a_f \times b_f = a \times b \times 2^n \times 2^n$ 在轉換過程多乘了一個 $2^n$ 故要校正回來 同理發生在除法運算 $\dfrac {a_f}{b_f} = \dfrac {a}{b} \times (\dfrac {2^n}{2^n})$ $2^n$ 被抵消所以要往左位移 n 補回來 #### `fixed_power_int` 解析 理解為在定點數的框架下做計算, 這也是為什麼需要在乘法後做校正 值得一提的是這行 ```cpp! x += 1UL << (frac_bits - 1) ``` 個人認為是做四捨五入讓右移時不會造成誤差大於單位的一半 ### load average 意義:表示系統的負載量而非 cpu 的負載 ${L_t} = {C_r} + {C_u}$ ${L_t}$: 在時間點 t 的 load ${C_r}$: 狀態為 TASK_RUNNABLE的task總數 ${C_u}$: 狀態為 TASK_UNINTERRUPTIBLE的task總數 系統在顯示的load average則是移動平滑後的結果 $S_t = \alpha \times X_{t-1} + (1 - \alpha) \times S_{t-1} , where 0 < α < 1$ 在load avg 中使用exponential moving average來作為load的計算 所以上述公式的$\alpha$為 $e^{-t_s/T}$ $t_s$ :kernel取樣的秒數,這裡為5秒 $T$ :多少時間尺度的load avg (1min, 5min, 15min) #### 理解常數定義 ```cpp! #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */ ``` - FSHIFT: 精度11的定點數 - FIXED_1:1在定點數的表示法, 右移11位 - LOAD_FREQ: 計算load avg的間隔, HZ在kernel的意義是1秒進行多少次timer interrput, 所以可以當作1單位秒 - EXP_1:$\alpha$在1分鐘的定點數表示法 = $e^{-5/60}$ - EXP_5:$\alpha$在5分鐘的定點數表示法 = $e^{-5/180}$ - EXP_15:$\alpha$在15分鐘的定點數表示法 = $e^{-5/900}$ calc_global_load 函數就是根據指數移動平滑平均和定點數運算在5秒一個間隔去更新load avg 1,5,15的值 ### 強化學習(Reinforcement Learning) #### 馬可夫決策過程(Markov Decision Process) 每列總和為1的轉移矩陣就是右隨機矩陣(right stochastic matrix) 折扣總和(discount sum)定義如下: $E\left[\sum_{t=0}^{\infty} \gamma^t R_{a_t}(s_t, s_{t+1})\right], \quad 0 \leq \gamma \leq 1$ 為什麼需要一個$\gamma$去做折扣的理由是假設有一個獎勵為正的環,在沒有discount的情況下,最佳選擇就會是一直繞著環的動作,而不會採取其他考慮 自己的對這個discount的見解是下個動作之外的未來是不確定的,所需要對在這個時間點所知的報酬打折扣 ##### 理解遞迴式 $V(s) = \sum_{s'} P_{\pi(s)}(s, s')\left(R_{\pi(s)}(s, s') + \gamma V(s')\right)$ 在狀態s的折扣回報我們可以分成兩個部分來計算 第一個部分是選擇下一個步驟的回報s' 另一個是s'之後的每個步驟s'', s''', ....,$s^n$的折扣回報 其實這個回報就是$\gamma V(s')$ $\pi(s) = \arg\max_a \left\{ \sum_{s'} P_a(s, s')\left(R_a(s, s') + \gamma V(s')\right) \right\}$ $\pi(s)$: 對應的最佳動作選擇 這樣還是有點難懂 所以去找了一些解說 Bellman Equation $V(s) = R(s) + \gamma\ max_{a\in A(s)}\sum_{s'}P(s' | s,a)V(s')$ [Value Iteration](https://youtu.be/KovN7WKI9Y0?si=7XNMXNBZA9tTlDgJ) $V_0(s) <- 0$ $V_{i+1}(s) <- R(s) + \gamma\ max_{a\in A(s)}\sum_{s'}P(s' | s,a)V(s')$ 利用每次迭代選擇的最佳步驟 至V(s)的值收斂 #### 例子 ![Screenshot 2025-04-01 at 2.15.04 PM](https://hackmd.io/_uploads/SyFhvAY6ke.png) 左邊狀態的回報計算 第一輪: $3 + 0.5*max(1.0*0.0, 0.5*0.0 + 0.5*0.0) = 3$ 第二輪: $3 + 0.5*max(1.0*-1.0, 0.5*3 + 0.5*(-1)) = 3.5$ 右邊狀態回報計算 第一輪: $-1 + 0.5*max(1.0*0.0, 0.0*0.0) = -1$ 第二輪: $-1 + 0.5*max(1.0*-1, 1.0*3) = 0.5$ 以此類推,終止條件是當兩輪的結果收斂 然後選出得到折扣報酬最大的動作 這個方法的必須先知道選擇動作的機率,在實務上可能是未知的 所以在專案ttt裡面的強化學習實作是使用時間差分學習 #### 強化學習演算法(RL Algorithm) 總結就是如同筆記的這句話 強化學習的核心目標是學習最佳的 𝜋,以最大化𝑉𝜋(𝑠),並透過探索與利用(exploration vs. exploitation)策略來達成最優解。 #### 時間差分學習 學習的目標: 價值函式 $V(s) = E[G_t | S_t = s]$ 以ttt為例, 我們學習的是在某個棋盤狀態下剩餘可以放子的格子的價值 $V(S_t) \leftarrow V(S_t) + \alpha \left[ R_{t+1} + \gamma V(S_{t+1}) - V(S_t) \right]$ 這個更新公式的關鍵想法是:目前狀態的價值應該向「即時獎勵 + 折扣後的下一狀態價值」靠攏 往下看括號項 $\left[ R_{t+1} + \gamma V(S_{t+1}) - V(S_t) \right]$ 意義是實際觀察到的狀態期望扣除當下預期期望, 換句話說,我們看到的比預期的好,遞迴狀態價值就加上學習率*$\left[ R_{t+1} + \gamma V(S_{t+1}) - V(S_t) \right]$ #### 研讀ttt程式碼 主要關注兩個檔案 - train.c:訓練的主要邏輯 - agents/reinforcement_learning.c: 強化學習的實作 ##### agents/reinforcement_learning.c **int get_action_exploit(char *table, rl_agent_t *agent)** - 找出在某個棋盤狀態下, 下一步 o/x 最大的報酬 - 回傳值是棋盤的位置 ```cpp! int get_action_exploit(char *table, rl_agent_t *agent) { int max_act = -1; float max_q = -FLT_MAX; float *state_value = agent->state_value; int candidate_count = 1; #ifdef VERBOSE printf("[ "); #endif // 列舉每一個可放棋子的狀態 for_each_empty_grid (i, table) { table[i] = agent->player; //棋手是誰 float new_q = state_value[table_to_hash(table)]; #ifdef VERBOSE printf("%f ", new_q); #endif if (new_q == max_q) { ++candidate_count; if (rand() % candidate_count == 0) { max_act = i; } } else if (new_q > max_q) { candidate_count = 1; max_q = new_q; max_act = i; } table[i] = ' '; } #ifdef VERBOSE printf(" ]\n"); printf("exploit %d\n", max_act); #endif return max_act; } ``` **int table_to_hash(char *table)** - 這個函式是把現在的棋盤狀態編碼成一個int - 方法就是用$\sum_{i=0} S_i*3^{16-i}$,最高位是棋盤i=0的位置 char *hash_to_table(int hash) - table_to_hash的相反, 把湊雜值變回棋盤 ##### train.c **static float update_state_value** 這個函式就是TD learning的遞迴函式 ```cpp! static float update_state_value(int after_state_hash, float reward, float next, rl_agent_t *agent) { float curr = reward - GAMMA * next; // 不懂為啥是減 agent->state_value[after_state_hash] = (1 - LEARNING_RATE) * agent->state_value[after_state_hash] + // 提項V(S_t) LEARNING_RATE * curr; #if MONTE_CARLO return curr; #else return agent->state_value[after_state_hash]; } ``` **static void train** 訓練的主函式 這裡埰用兩種方法去實作下一步該往哪走 - EPSILON_GREEDY 這裡利用exploration vs exploitation的概念,當取樣隨機小於$\epsilon$下一步就是可以走的格子隨機挑一個,也就是exploration的概念,反之則是採取NON-GREEDY的選擇方式 - NON-GREEDY 選擇state_value裡面最大的格子,詳見上面的敘述 ```cpp! static void train(int iter) { int episode_moves[N_GRIDS]; // from 0 moves to N_GRIDS moves. float reward[N_GRIDS]; int episode_len = 0; char table[N_GRIDS]; memset(table, ' ', N_GRIDS); int turn = (iter & 1) ? 0 : 1; // 0 for 'O', 1 for 'X' char win = ' '; while (1) { if (win == 'D') { #ifdef VERBOSE draw_board(table); printf("It is a draw!\n"); #endif break; } else if (win != ' ') { #ifdef VERBOSE draw_board(table); printf("%c won!\n", win); #endif break; } #if EPSILON_GREEDY int move = get_action_epsilon_greedy(table, &agent[turn]); #else int move = get_action_exploit(table, &agent[turn]); #endif table[move] = "OX"[turn]; // update move to the table array win = check_win(table); episode_moves[episode_len] = table_to_hash(table); reward[episode_len] = (1 - REWARD_TRADEOFF) * get_score(table, agent[turn].player) + REWARD_TRADEOFF * calculate_win_value(win, agent[turn].player); // update reward ++episode_len; #ifdef VERBOSE draw_board(table); #endif turn = !turn; } // turn = !turn; // the player who makes the last move. float next = 0; for (int i_move = episode_len - 1; i_move >= 0; --i_move) { next = update_state_value(episode_moves[i_move], reward[i_move], next, &agent[turn]); } } ```