小新 編譯自 Medium
量子位 出品 | 公眾號 QbitAI
Q-Learning是強化學習中最常用的算法之一。
Medium上有篇文章,討論了這種算法的一個重要部分:搜索策略。
量子位搬運過來,以下為博客譯文:
我們先介紹下有關概念和符號。
強化學習
強化學習(Reinforcement Learning, RL)屬于機器學習的一個分支,利用智能體(agent)通過狀態感知、選擇動作和接收獎勵來與環境互動。每一步中,智能體都會通過觀察環境狀態,選擇并執行一個動作,來改變其狀態并獲得獎勵。
馬爾可夫決策過程
在傳統環境中,馬爾可夫決策過程(Markov Decision Processes, MDP)可以解決不少RL問題。這里,我們不會深入討論MDP的理論,有關MDP算法的更多內容可參考:
https://en.wikipedia.org/wiki/Markov_decision_process
我們用森林火災來解釋下MDP算法,代碼實現可使用python MDP Toolbox:
http://pymdptoolbox.readthedocs.io/en/latest/api/example.html
森林管理包括兩個動作,等待和砍伐。每年要做出一個決定,一是為林中動物保持古老森林,二是砍伐木材來而賺錢。而且,每年有p概率發生森林火災,有1-p的概率為森林生長。
先定義MDP算法中一些參數S、A、P、R和γ,其中:
S是狀態空間(有限),包括3種不同年齡樹木,年齡級分別為0-20年、21-40年和40年以上;
A是動作空間(有限),即等待或砍伐;
P和R分別是轉移矩陣和獎勵矩陣,很容易寫出它的閉合形式;
γ是數值在0與1之間的貼現因子,用來平衡短時和未來獎勵的關系;
策略π是當前狀態下決策的靜態分布;
該模型的目標是在未給出MDP動態知識的情況下找到一個最優策略π*。
要注意,如果具有這個動態知識,直接用最優值迭代方法就能找到最優策略。
1def optimal_value_iteration(mdp, V0, num_iterations, epsilon=0.0001):2 V=np.zeros((num_iterations+1, mdp.S))3 V[0][:]=np.ones(mdp.S)*V04 X=np.zeros((num_iterations+1, mdp.A, mdp.S))5 star=np.zeros((num_iterations+1,mdp.S))6 for k in range(num_iterations):7 for s in range(mdp.S):8 for a in range(mdp.A):9 X[k+1][a][s]=mdp.R[a][s] + mdp.discount*np.sum(mdp.P[a][s].dot(V[k]))10 star[k+1][s]=(np.argmax(X[k+1,:,s]))11 V[k+1][s]=np.max(X[k+1,:,s])12 if (np.max(V[k+1]-V[k])-np.min(V[k+1]-V[k])) < epsilon:13 V[k+1:]=V[k+1]14 star[k+1:]=star[k+1]15 X[k+1:]=X[k+1]16 break17 else: pass18 return star, V, X
獎勵變化曲線
最優策略是等到森林處于古老且茂盛的狀態時進行砍伐,這容易理解,因為在森林處于最古老的狀態時砍伐的獎勵是等待讓森林生長的獎勵的5倍,有r1=10,r2=50。
Q-Learning算法
Q-Learning算法中的“Q”代表著策略π的質量函數(Quality function),該函數能在觀察狀態s確定動作a后,把每個狀態動作對 (s, a) 與總期望的折扣未來獎勵進行映射。
Q-Learning算法屬于model-free型,這意味著它不會對MDP動態知識進行建模,而是直接估計每個狀態下每個動作的Q值。然后,通過在每個狀態下選擇具有最高Q值的動作,來繪制相應的策略。
如果智能體不斷地訪問所有狀態動作對,則Q-Learning算法會收斂到最優Q函數[1]。
下面我們給出關于Q-Learning算法的Python實現。
要注意,這里的學習率α是w=4/5時的多項式,這里使用了引用[3]的結果。
這里使用的ε-greedy搜索策略,后面會詳細介紹。
1def q_learning(mdp, num_episodes, T_max, epsilon=0.01):2 Q=np.zeros((mdp.S, mdp.A))3 episode_rewards=np.zeros(num_episodes)4 policy=np.ones(mdp.S)5 V=np.zeros((num_episodes, mdp.S))6 N=np.zeros((mdp.S, mdp.A))7 for i_episode in range(num_episodes):8 # epsilon greedy exploration9 greedy_probs=epsilon_greedy_exploration(Q, epsilon, mdp.A)10 state=np.random.choice(np.arange(mdp.S))11 for t in range(T_max):12 # epsilon greedy exploration13 action_probs=greedy_probs(state)14 action=np.random.choice(np.arange(len(action_probs)), p=action_probs)15 next_state, reward=playtransition(mdp, state, action)16 episode_rewards[i_episode] +=reward17 N[state, action] +=118 alpha=1/(t+1)**0.819 best_next_action=np.argmax(Q[next_state]) 20 td_target=reward + mdp.discount * Q[next_state][best_next_action]21 td_delta=td_target - Q[state][action]22 Q[state][action] +=alpha * td_delta23 state=next_state24 V[i_episode,:]=Q.max(axis=1)25 policy=Q.argmax(axis=1) 26 return V, policy, episode_rewards, N
獎勵變化曲線
探索與利用的平衡
序列學習算法會涉及到一個基本選擇:
利用:根據當前信息做出最佳決策;
探索:做出其他決策來收集更多信息。
合理平衡好探索和利用的關系,對智能體的學習能力有重大影響。過多的探索會阻礙智能體最大限度地獲得短期獎勵,因為選擇繼續探索可能獲得較低的環境獎勵。另一方面,由于選擇的利用動作可能不是最優的,因此靠不完全知識來利用環境會阻礙長期獎勵的最大化。
ε-greedy搜索策略
該策略在每一步利用概率ε來選擇隨機動作。
這可能是最常用也是最簡單的搜索策略,即用ε調整探索動作。在許多實現中,ε會隨著時間不斷衰減,但也有不少情況,ε會被設置為常數。
1def epsilon_greedy_exploration(Q, epsilon, num_actions):2 def policy_exp(state):3 probs=np.ones(num_actions, dtype=float) * epsilon / num_actions4 best_action=np.argmax(Q[state])5 probs[best_action] +=(1.0 - epsilon)6 return probs7 return policy_exp
不確定優先搜索策略
不確定優先(Optimism in Face of Uncertainty)搜索策略,最開始被用來解決隨機式多臂賭博機問題 (Stochastic Multi-Armed Bandit),這是一個很經典的決策問題,賭徒要轉動一個擁有n個槽的老虎機,轉動每個槽都有固定回報概率,目標是找到回報概率最高的槽并且不斷地選擇它來獲取最高的回報。
賭徒面臨著利用還是探索的問題,利用機器獲得最高的平均獎勵或探索其他未玩過的機器,以期望獲得更高的獎勵。
這個問題與Q-Learning算法中的探索問題非常相似:
利用:在給定狀態下選擇具有最高Q值的動作;
探索:做出其他決策來探索更多信息,通過選擇不了解或不夠了解的環境。
不確定優先狀態:只要我們對某個槽的回報不確定時不確定手臂的結果,我們就會考慮當前環境來選擇最佳的手臂。
不確定優先算法有兩方面:
若當前處于最佳環境,那算法會直接選擇最佳的手臂;
若當前不處于最佳環境,則算法會盡量降低不確定性。
置信區間上界(Upper Confidence Bound, UCB)是一種常用的不確定優先算法[2],我們把它結合到Q-Learning方法中,有:
Q(s, a):狀態s下動作a縮放后的Q值;
N(t,s,a):在時刻t和狀態s下動作a被選擇的次數。
此時,智能體的目標為Argmax {Q(s, a)/ a ∈ A},這意味著在狀態s中選擇具有最高Q值的動作。但是在t時刻Q(s,a)值是未知的。
在t時刻,Q估計值為Q(t, s, a),則有Q(s,a)=+ (Q(s,a) ? )。
(Q(s,a) ? )為相應誤差項。
霍夫不等式 (Hoeffding’s inequality)可用來處理這類誤差。事實上,當t變化時,有:
優先策略可寫成:Argmax {Q+(t, s, a)/ a ∈ A},且有:
當β大于0時,執行探索動作;當β為0時,僅利用已有估計。
這種界限方法是目前最常用的,基于這種界限后面也有許多改進工作,包括UCB-V,UCB*,KL-UCB,Bayes-UCB和BESA[4]等。
下面給出經典UCB算法的Python實現,及其在Q-Learning上的應用效果。
1def UCB_exploration(Q, num_actions, beta=1):2 def UCB_exp(state, N, t):3 probs=np.zeros(num_actions, dtype=float)4 Q_=Q[state,:]/max(Q[state,:]) + np.sqrt(beta*np.log(t+1)/(2*N[state]))5 best_action=Q_.argmax()6 probs[best_action]=17 return probs8 return UCB_exp
獎勵變化曲線
UCB搜索算法應該能很快地獲得高額獎勵,但是前期搜索對訓練過程的影響較大,有希望用來解決更復雜的多臂賭博機問題,因為這種方法能幫助智能體跳出局部最優值。
下面是兩種策略的對比圖。
總結與展望
Q-Learning是強化學習中最常用的算法之一。在這篇文章中,我們討論了搜索策略的重要性和如何用UCB搜索策略來替代經典的ε-greedy搜索算法。
更多更細致的優先策略可以被用到Q-Learning算法中,以平衡好利用和探索的關系。
[1] T. Jaakkola, M. I. Jordan, and S. P. Singh, “On the convergence of stochastic iterative dynamic programming algorithms” Neural computation, vol. 6, no. 6, pp. 1185–1201, 1994.
[2] P. Auer, “Using Confidence Bounds for Exploitation-Exploration Trade-offs”, Journal of Machine Learning Research 3 397–422, 2002.
[3] E. Even-Dar, and Y. Mansour, “Learning Rates for Q-learning”, Journal of Machine Learning Research 5 1–25, 2003.
[4] A. Baransi, O.-A. Maillard, and S. Mannor, “Sub-sampling for multi-armed bandits”, Joint European Conference on Machine Learning and Knowledge Discovery in Databases, 115–131, 2014.
原文:https://medium.com/sequential-learning/optimistic-q-learning-b9304d079e11
— 完 —
誠摯招聘
量子位正在招募編輯/記者,工作地點在北京中關村。期待有才氣、有熱情的同學加入我們!相關細節,請在量子位公眾號(QbitAI)對話界面,回復“招聘”兩個字。
量子位 QbitAI · 頭條號簽約作者
?'?' ? 追蹤AI技術和產品新動態
礎算法
一、排序
//冒泡排序 function bubbleSort(arr) { for(var i=1, len=arr.length; i < len - 1; ++i) { for(var j=0; j <=len - i; ++j) { if (arr[j] > arr[j + 1]) { let temp=arr[j]; arr[j]=arr[j + 1]; arr[j + 1]=temp; } } } }
//插入排序 過程就像你拿到一副撲克牌然后對它排序一樣 function insertionSort(arr) { var n=arr.length; // 我們認為arr[0]已經被排序,所以i從1開始 for (var i=1; i < n; i++) { // 取出下一個新元素,在已排序的元素序列中從后向前掃描來與該新元素比較大小 for (var j=i - 1; j >=0; j--) { if (arr[i] >=arr[j]) { // 若要從大到小排序,則將該行改為if (arr[i] <=arr[j])即可 // 如果新元素arr[i] 大于等于 已排序的元素序列的arr[j], // 則將arr[i]插入到arr[j]的下一位置,保持序列從小到大的順序 arr.splice(j + 1, 0, arr.splice(i, 1)[0]); // 由于序列是從小到大并從后向前掃描的,所以不必再比較下標小于j的值比arr[j]小的值,退出循環 break; } else if (j===0) { // arr[j]比已排序序列的元素都要小,將它插入到序列最前面 arr.splice(j, 0, arr.splice(i, 1)[0]); } } } return arr; }
//快速排序 function qSort(arr) { //聲明并初始化左邊的數組和右邊的數組 var left=[], right=[]; //使用數組第一個元素作為基準值 var base=arr[0]; //當數組長度只有1或者為空時,直接返回數組,不需要排序 if(arr.length <=1) return arr; //進行遍歷 for(var i=1, len=arr.length; i < len; i++) { if(arr[i] <=base) { //如果小于基準值,push到左邊的數組 left.push(arr[i]); } else { //如果大于基準值,push到右邊的數組 right.push(arr[i]); } } //遞歸并且合并數組元素 return [...qSort(left), ...[base], ...qSort(right)]; //return qSort(left).concat([base], qSort(right)); }
補充:
在這段代碼中,我們可以看到,這段代碼實現了通過pivot區分左右部分,然后遞歸的在左右部分繼續取pivot排序,實現了快速排序的文本描述,也就是說該的算法實現本質是沒有問題的。
雖然這種實現方式非常的易于理解。不過該實現也是有可以改進的空間,在這種實現中,我們發現在函數內定義了left/right兩個數組存放臨時數據。隨著遞歸的次數增多,會定義并存放越來越多的臨時數據,需要Ω(n)的額外儲存空間。
因此,像很多算法介紹中,都使用了原地(in-place)分區的版本去實現快速排序,我們先介紹什么是原地分區算法。
原地(in-place)分區算法描述
通過以上3個步驟,就將以基準值為中心,數組的左右兩側數字分別比基準值小或者大了。這個時候在遞歸的原地分區,就可以得到已排序后的數組。
原地分區算法實現
// 交換數組元素位置 function swap(array, i, j) { var temp=array[i]; array[i]=array[j]; array[j]=temp; } function partition(array, left, right) { var index=left; var pivot=array[right]; // 取最后一個數字當做基準值,這樣方便遍歷 for (var i=left; i < right; i++) { if (array[i] <=pivot) { swap(array, index, i); index++; } } swap(array, right, index); return index; }
因為我們需要遞歸的多次原地分區,同時,又不想額外的地址空間所以,在實現分區算法的時候會有3個參數,分別是原數組array,需要遍歷的數組起點left以及需要遍歷的數組終點right
最后返回一個已經排好序的index值用于下次遞歸,該索引對應的值一定比索引左側的數組元素小,比所有右側的數組元素大。
再次基礎上我們還是可以進一步的優化分區算法,我們發現 <=pivot可以改為<pivot,這樣可以減少一次交換
原地分區版快速排序實現
function quickSort(array) { function swap(array, i, j) { var temp=array[i]; array[i]=array[j]; array[j]=temp; } function partition(array, left, right) { var index=left; var pivot=array[right]; // 取最后一個數字當做基準值,這樣方便遍歷 for (var i=left; i < right; i++) { if (array[i] < pivot) { swap(array, index, i); index++; } } swap(array, right, index); return index; } function sort(array, left, right) { if (left > right) { return; } var storeIndex=partition(array, left, right); sort(array, left, storeIndex - 1); sort(array, storeIndex + 1, right); } sort(array, 0, array.length - 1); return array; }
二、字符串
//判斷回文字符串 function palindrome(str) { var reg=/[\W\_]/g; var str0=str.toLowerCase().replace(reg, ""); var str1=str0.split("").reverse().join(""); return str0===str1; }
function reverseString(str) { return str.split("").reverse().join(""); }
function findMaxDuplicateChar(str) { var cnt={}, //用來記錄所有的字符的出現頻次 c=''; //用來記錄最大頻次的字符 for (var i=0; i < str.length; i++) { var ci=str[i]; if (!cnt[ci]) { cnt[ci]=1; } else { cnt[ci]++; } if (c=='' || cnt[ci] > cnt[c]) { c=ci; } } console.log(cnt) return c; }
三、數組
//數組去重 function uniqueArray(arr) { var temp=[]; for (var i=0; i < arr.length; i++) { if (temp.indexOf(arr[i])==-1) { temp.push(arr[i]); } } return temp; //or return Array.from(new Set(arr)); }
四、查找
//二分查找 function binary_search(arr, l, r, v) { if (l > r) { return -1; } var m=parseInt((l + r) / 2); if (arr[m]==v) { return m; } else if (arr[m] < v) { return binary_search(arr, m+1, r, v); } else { return binary_search(arr, l, m-1, v); } }
五、樹的搜索/遍歷
//深搜 非遞歸實現 function dfs(node) { var nodeList=[]; if (node) { var stack=[]; stack.push(node); while(stack.length !=0) { var item=stack.pop(); nodeList.push(item); var children=item.children; for (var i=children.length-1; i >=0; i--) { stack.push(children[i]); } } } return nodeList; } //深搜 遞歸實現 function dfs(node, nodeList) { if (node) { nodeList.push(node); var children=node.children; for (var i=0; i < children.length; i++) { dfs(children[i], nodeList); } } return nodeList; }
//廣搜 非遞歸實現 function bfs(node) { var nodeList=[]; if (node !=null) { var queue=[]; queue.unshift(node); while (queue.length !=0) { var item=queue.shift(); nodeList.push(item); var children=item.children; for (var i=0; i < children.length; i++) queue.push(children[i]); } } return nodeList; } //廣搜 遞歸實現 var i=0; //自增標識符 function bfs(node, nodeList) { if (node) { nodeList.push(node); if (nodeList.length > 1) { bfs(node.nextElementSibling, nodeList); //搜索當前元素的下一個兄弟元素 } node=nodeList[i++]; bfs(node.firstElementChild, nodeList); //該層元素節點遍歷完了,去找下一層的節點遍歷 } return nodeList; }
高階函數衍生算法
1.filter去重
filter也是一個常用的操作,它用于把Array的某些元素過濾掉,然后返回剩下的元素。也可以這么理解,filter的回調函數把Array的每個元素都處理一遍,處理結果返回false則過濾結果去除該元素,true則留下來
用filter()這個高階函數,關鍵在于正確實現一個“篩選”函數。
其實這個篩選函數有多個參數,filter(function (element, index, self),演示一個使用filter去重,像這樣:
var r, arr=['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry']; r=arr.filter(function (element, index, self) { return self.indexOf(element)===index; //拿到元素,判斷他在數組里第一次出現的位置,是不是和當前位置一樣,一樣的話返回true,不一樣說明重復了,返回false。 });
2.sort排序算法
排序也是在程序中經常用到的算法。無論使用冒泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字符串或者兩個對象呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函數抽象出來。通常規定,對于兩個元素x和y,如果認為x < y,則返回-1,如果認為x==y,則返回0,如果認為x > y,則返回1,這樣,排序算法就不用關心具體的比較過程,而是根據比較結果直接排序。
值得注意的例子
// 看上去正常的結果: ['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft']; // apple排在了最后: ['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple'] // 無法理解的結果: [10, 20, 1, 2].sort(); // [1, 10, 2, 20]
解釋原因
第二個排序把apple排在了最后,是因為字符串根據ASCII碼進行排序,而小寫字母a的ASCII碼在大寫字母之后。
第三個排序結果,簡單的數字排序都能錯。
這是因為Array的sort()方法默認把所有元素先轉換為String再排序,結果’10’排在了’2’的前面,因為字符’1’比字符’2’的ASCII碼小。
因此我們把結合這個原理:
var arr=[10, 20, 1, 2]; arr.sort(function (x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; }); console.log(arr); // [1, 2, 10, 20]
上面的代碼解讀一下:傳入x,y,如果x<y,返回-1,x與前面排,如果x>y,返回-1,x后面排,如果x=y,無所謂誰拍誰前面。
還有一個,sort()方法會直接對Array進行修改,它返回的結果仍是當前Array,一個栗子:
插入排序是將數組分為待排序和已排序兩個區間。依次從待排序區間中取出一項,用該項跟已排序區間項逐個對比,通過位移來實現插入到對應位置的排序方式。插入排序平均時間復雜度是:O(n^2)
步驟是:
插入排序有多種實現方式,這里介紹常見的3種:
1、通用實現方式,自左往右遍歷待排序數組,再從當前的左側位置開始自右往左循環已排序數組,再逐個比較和移動被比較項,最后將當前項填入到空缺位置上。
2、利用數組splice方法,類似打撲克牌,先拿出要排序的牌,然后找準位置插入。這種方式利用了原生API,減少了數組反復移動位置的操作。性能上之前差不多。
3、新建數組法與splice結合法,這種方式會多建立一個數組,也就會多占用一個空間,但理解起來最容易,也利用了JS語言的特性。
插入排序與冒泡、選擇都是比較簡單好懂的排序方式,性能上也差不多。插入排序通俗來講就像打撲克牌排序,你抓了一手牌之后。假如是:2、1、5、3、4,你會:
1、先把牌分成兩組,假定左側第一張牌為一組(標識A,這時只有2),其他牌為另外一組(標識B,包括1、5、3、4)。
2、從B組里面從左起選擇第一張牌(位置空出等待填充),也就是1,拿這張牌與A組里面從右往左挨個對比,當遇到比這張牌還小時就在這個位置停留下來(如果A組全部比這張牌都大那就在A組最前面停留下來,如果A組里沒有比這張牌大的就在當前位置停留)。
3、然后將A組里比這張牌(也就是1)大的牌逐個往右移動1位,原B組空出位置被填充,此時剛才停留的位置空出,將1這張牌插入在這里。這時候A組增加一個數字,變為:1、2,B組減少1個,變為:5、3、4。
4、移動指針,繼續指向B組的第一個,也就是5。用5這張牌重復第二部,即拿5去跟A組自右往左逐個比較,然后插入到A組。此時A組:1、2、5,B組:3、4。
5、將B組里數字按照第二部重復操作,直到B組為空時整個循環結束。此時A組為:1、2、3、4、5。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。