乙 郭一璞 發自 凹非寺
量子位 出品 | 公眾號 QbitAI
什么!未連接到互聯網!!
明明是聯網狀態,為什么我想訪問的頁面
無!法!打!開!
淡定。
作為一個Google Chrome瀏覽器的用戶,當你看到上面那個頁面時,不要沮喪。換個角度一想,墻內還能有更多的Play時間哦~
你有沒有注意到畫面里那個小恐龍?
當你遇到打不開網頁的時候,只需要再點擊一下這個頁面(手機),或者按下空格(電腦),隨著小恐龍輕輕一跳——
一個新世界開啟了。
這個“恐龍跳一跳”其實是藏在Chrome瀏覽器里好多年的一個彩蛋。小恐龍是一只霸王龍(T-Rex)。
2013年Chrome開始用這個小恐龍的圖像代替令人煩惱的404頁面。2014年秋天,這只恐龍被正式改造成一個橫版小游戲。以彩蛋的方式隱藏在新版Chrome瀏覽器里。
吶,如果你還不知道這個彩蛋,可以抓緊試一試。比方說——
訪問一個不翻墻就看不了的網頁
或者直接輸入:chrome://dino
或者訪問:https://chromedino.com/ (需翻墻)
后來,這個小游戲也成了不少AI練手的對象。
比如最近就有人在YouTube上貼了一段視頻,展示了他如何用神經網絡+遺傳算法,讓一個AI系統獨秀于瀏覽器之中。
我們把精華的部分截取了一下,就是下面這段視頻。
動圖版:
速度已經快到飛起
總而言之,一句話,這個AI能輕松玩到2萬多分……
你能玩到幾分?大概率是玩不到這個成績的吧。畢竟在chromedino.com頁面上,人類玩家的歷史最高分是18842。
不過,上傳這段視頻的作者,并沒有詳細公布他用的方法,當然也沒有給出一個開源的地址。不過不要緊,也有別人公開分享了更多細節。
例如,GitHub上就有一個開源的代碼“IAMDinosaur”,同樣也是利用神經網絡+遺傳算法,來搞定恐龍跳一跳。
地址在此:https://github.com/ivanseidel/IAMDinosaur
美中不足,上面這個項目也沒有配上太詳盡的解讀。然而好消息是,最近有個國外的小哥Ravi Munde,列了一份非常詳盡的教程。
這個教程用的方法是強化學習中的Q-learning,比較適合入門練手,而且對硬件的要求不高。
量子位搬運這份教程如下。
對動物來說,強化學習的能力是與生俱來的。拿兒童學步來舉例,如果小朋友努力的邁出第一步,就會獲得父母的鼓勵——可能是鼓掌叫好,也可能是一塊糖;但如果小朋友堅決不肯學習走路,那父母就不會給它糖吃了。強化學習就是依照這類激勵行為而設置的。
而在這個游戲中,對我們的AI小恐龍來說,強化學習需要讓他在無監督的情況下,先認識到做出不同動作的結果,并且以獲得高分為最高激勵。
一個典型的強化學習閉環
Ravi Munde用Q-learning模擬了一個特殊函數,這個函數驅動AI在不同狀況下做出正確的選擇。
Q-learning是強化學習的一種無模型實現,根據Q值對每個狀態進行判斷此時如果采取行動,能獲得怎樣的獎勵。一個樣本Q表讓我們了解數據的結構。在恐龍跑酷游戲中,狀態是當前的游戲截圖,能采取的行動是跳或不跳[0,1]
一個樣本Q表
Ravi Munde決定用深度神經網絡來決定小恐龍何時起跳,而且要在最簡單的強化學習實現基礎上,引入不同參數來輔助它。
缺乏已標記的數據讓強化學習非常不穩定。為了獲得適用于這個游戲的數據,Munde小哥決定,先讓小恐龍自己瞎跳幾千次,把每個動作的反饋記下來,然后從數據中隨機挑選一些來訓練模型。
但之后,Munde小哥發現,他訓練了一個倔強的模型——模型堅定的認為,跳,一定比不跳好。所以,為了讓模型在訓練時能在跳與不跳之間多嘗試一下,他引入了一個函數?來決定行動的隨機性,然后再逐漸減小它的值來削減隨機性,最終讓模型去選擇最有可能獲得獎勵的行動。
贊譽分布(Credit Assignment)問題可能會讓模型陷入混亂——目前獲得的獎勵究竟來自于過去的哪個行為呢?在恐龍跑酷游戲中,小恐龍跳到半空中后無法再次跳躍,但模型可能會在恐龍處于半空中時發出跳躍指令,這種情況就讓恐龍非常容易砸到仙人掌上。
在這種情況下,“砸到仙人掌上”這個負反饋實際上是此前上一次做出跳躍決定的結果,而不是剛剛恐龍在半空中時做出的跳躍結果所導致的。
在面臨這種問題的情況下,可以引入貼現因子(Discount Factor)γ來決定模型做出動作時看得多遠。γ間接解決了贊譽分布問題,在這個游戲中,當γ=0.99時,模型認識到在無障礙時隨便跳會導致真的遇到障礙時自己正在半空中,無法繼續跳躍。
除了這兩個參數之外,后面就幾乎不需要任何參數了。
#game parameters GAMMA=0.99 # decay rate of past observations original 0.99 OBSERVATION=50000. # timesteps to observe before training EXPLORE=100000 # frames over which to anneal epsilon FINAL_EPSILON=0.0001 # final value of epsilon INITIAL_EPSILON=0.1 # starting value of epsilon REPLAY_MEMORY=50000 # number of previous transitions to remember BATCH=32 # size of minibatch FRAME_PER_ACTION=1
Python 3.6
Selenium
OpenCV
PIL
Chromium driver for Selenium
Keras
略微解釋一下這幾個工具。
構建這個AI模型,需要用Python編程。而游戲是用JavaScript寫成的。所以,得借助一些工具才能更好地溝通。
Selenium是一種流行的瀏覽器自動化工具,用于向瀏覽器發送操作指令,以及獲取各種游戲參數。
接口的事情搞定了,還得想辦法獲得游戲截屏。用Selenium也行,但是速度很慢,截屏和處理一次大約得1秒鐘。
用PIL和OpenCV能夠更好地完成截屏和圖像預處理,可以達到5fps的幀率。你可能覺得還是慢,但已經足夠對付這個游戲了。
下面這個模塊,實現了Python和瀏覽器(使用Selenium)的溝通。
''' * Game class: Selenium interfacing between the python and browser * __init__(): Launch the broswer window using the attributes in chrome_options * get_crashed() : return true if the agent as crashed on an obstacles. Gets javascript variable from game decribing the state * get_playing(): true if game in progress, false is crashed or paused * restart() : sends a signal to browser-javascript to restart the game * press_up(): sends a single to press up get to the browser * get_score(): gets current game score from javascript variables. * pause(): pause the game * resume(): resume a paused game if not crashed * end(): close the browser and end the game ''' class Game: def __init__(self,custom_config=True): chrome_options=Options() chrome_options.add_argument("disable-infobars") self._driver=webdriver.Chrome(executable_path=chrome_driver_path,chrome_options=chrome_options) self._driver.set_window_position(x=-10,y=0) self._driver.set_window_size(200, 300) self._driver.get(os.path.abspath(game_url)) #modifying game before training if custom_config: self._driver.execute_script("Runner.config.ACCELERATION=0") def get_crashed(self): return self._driver.execute_script("return Runner.instance_.crashed") def get_playing(self): return self._driver.execute_script("return Runner.instance_.playing") def restart(self): self._driver.execute_script("Runner.instance_.restart()") time.sleep(0.25)# no actions are possible # for 0.25 sec after game starts, # skip learning at this time and make the model wait def press_up(self): self._driver.find_element_by_tag_name("body").send_keys(Keys.ARROW_UP) def get_score(self): score_array=self._driver.execute_script("return Runner.instance_.distanceMeter.digits") score=''.join(score_array) # the javascript object is of type array with score in the formate[1,0,0] which is 100. return int(score) def pause(self): return self._driver.execute_script("return Runner.instance_.stop()") def resume(self): return self._driver.execute_script("return Runner.instance_.play()") def end(self): self._driver.close()
這個模塊在游戲模塊的幫助下,用于控制小恐龍的動作。
class DinoAgent: def __init__(self,game): #takes game as input for taking actions self._game=game; self.jump(); #to start the game, we need to jump once time.sleep(.5) # no action can be performed for the first time when game starts def is_running(self): return self._game.get_playing() def is_crashed(self): return self._game.get_crashed() def jump(self): self._game.press_up() def duck(self): self._game.press_down()
神經網絡直接使用這個模塊,來執行操作并獲取新的狀態。
''' get_state(): accepts an array of actions, performs the action on the agent returns : new state, reward and if the game ended. ''' class Game_sate: def __init__(self,agent,game): self._agent=agent self._game=game def get_state(self,actions): score=self._game.get_score() reward=0.1*score/10 # dynamic reward calculation is_over=False #game over if actions[1]==1: #else do nothing self._agent.jump() reward=0.1*score/11 image=grab_screen() if self._agent.is_crashed(): self._game.restart() reward=-11/score is_over=True return image, reward, is_over #return the Experience tuple
游戲修改
原始的游戲相對復雜,比如游戲速度會逐漸加快,障礙物會改變,還會出現云朵、星星、地面紋理等。一次同時學習這么多東西會消耗大量時間,甚至在訓練過程中引入不必要的噪音。
為此作者修改了游戲的源代碼、簡化局面,去除了一些視覺元素(云、歷史最佳成績等),還有讓恐龍的奔跑速度保持不變。
原圖
修改后
圖像處理
原始截圖的分辨率為1200×300,包含三個通道。作者計劃使用4個連續的屏幕截圖作為模型的單一輸入,也就是1200×300×3×4。
問題是,這個小哥只有一個i7的CPU可用,所以他的電腦沒辦法在處理這個尺寸輸入的同時玩游戲。所以,還得繼續用OpenCV的庫調正截圖大小、裁剪等。最終輸入圖像大小為40×20像素,單通道,并用Canny突出顯示邊緣。
def grab_screen(_driver=None): #bbox=region of interest on the entire screen screen=np.array(ImageGrab.grab(bbox=(40,180,440,400))) image=process_img(screen)#processing image as required return image def process_img(image): #game is already in grey scale canvas, canny to get only edges and reduce unwanted objects(clouds) # resale image dimensions image=cv2.resize(image, (0,0), fx=0.15, fy=0.10) #crop out the dino agent from the frame image=image[2:38,10:50] #img[y:y+h, x:x+w] image=cv2.Canny(image, threshold1=100, threshold2=200) #apply the canny edge detection return image
然后,堆疊4張圖創建單個輸入,也就是:40×20×4。請注意,這里小恐龍也裁減掉了,因為整個學習過程,只需要知道障礙物和與邊緣的距離即可。
現在輸入有了,用模型輸出來玩游戲的方法也有了,只差模型架構。
小哥選擇把3個卷積層壓平,連接到一個512神經元的全連接層(dense layer)上。池化層直接被砍掉了,這個東西在圖像分類問題上很有用,但是玩Dino的時候神經網絡只需要知道障礙物的位置,池化層就起不了什么作用了。
多層網絡架構
這個模型的輸出,形狀和可能的操作數量一樣。模型會預測各種操作的Q值,也叫discounted future reward,然后我們選數值最高的那個。
下面這段代碼,就能召喚一個用TensorFlow后端的Keras來搭建的模型:
#model hyper parameters LEARNING_RATE=1e-4 img_rows , img_cols=40,20 img_channels=4 #We stack 4 frames ACTIONS=2 def buildmodel(): print("Now we build the model") model=Sequential() model.add(Conv2D(32, (8, 8), strides=(4, 4), padding='same',input_shape=(img_cols,img_rows,img_channels))) #20*40*4 model.add(Activation('relu')) model.add(Conv2D(64, (4, 4), strides=(2, 2), padding='same')) model.add(Activation('relu')) model.add(Conv2D(64, (3, 3), strides=(1, 1), padding='same')) model.add(Activation('relu')) model.add(Flatten()) model.add(Dense(512)) model.add(Activation('relu')) model.add(Dense(ACTIONS)) adam=Adam(lr=LEARNING_RATE) model.compile(loss='mse',optimizer=adam) print("We finish building the model") return model
接下來,就是見證奇跡的時刻~~
也就是用一段代碼來訓練模型,這段代碼的任務是:
從無操作開始,得到初始狀態initial state(s_t)
觀察玩游戲的過程,代碼中的OBSERVATION表示步數
預測一個操作的效果
在Replay Memory中存儲經驗
訓練階段,從Replay Memory里隨機選擇一組,用它來訓練模型
如果game over了,就重開一局
更詳細的,可以看這段自帶注釋的代碼:
''' Parameters: * model=> Keras Model to be trained * game_state=> Game State module with access to game environment and dino * observe=> flag to indicate wherther the model is to be trained(weight updates), else just play ''' def trainNetwork(model,game_state): # store the previous observations in replay memory D=deque() #load from file system # get the first state by doing nothing do_nothing=np.zeros(ACTIONS) do_nothing[0]=1 #0=> do nothing, #1=> jump x_t, r_0, terminal=game_state.get_state(do_nothing) # get next step after performing the action s_t=np.stack((x_t, x_t, x_t, x_t), axis=2).reshape(1,20,40,4) # stack 4 images to create placeholder input reshaped 1*20*40*4 OBSERVE=OBSERVATION epsilon=INITIAL_EPSILON t=0 while (True): #endless running loss=0 Q_sa=0 action_index=0 r_t=0 #reward at t a_t=np.zeros([ACTIONS]) # action at t #choose an action epsilon greedy if random.random() <=epsilon: #randomly explore an action print("----------Random Action----------") action_index=random.randrange(ACTIONS) a_t[action_index]=1 else: # predict the output q=model.predict(s_t) #input a stack of 4 images, get the prediction max_Q=np.argmax(q) # chosing index with maximum q value action_index=max_Q a_t[action_index]=1 # o=> do nothing, 1=> jump #We reduced the epsilon (exploration parameter) gradually if epsilon > FINAL_EPSILON and t > OBSERVE: epsilon -=(INITIAL_EPSILON - FINAL_EPSILON) / EXPLORE #run the selected action and observed next state and reward x_t1, r_t, terminal=game_state.get_state(a_t) last_time=time.time() x_t1=x_t1.reshape(1, x_t1.shape[0], x_t1.shape[1], 1) #1x20x40x1 s_t1=np.append(x_t1, s_t[:, :, :, :3], axis=3) # append the new image to input stack and remove the first one # store the transition in D D.append((s_t, action_index, r_t, s_t1, terminal)) D.popleft() if len(D) > REPLAY_MEMORY #only train if done observing; sample a minibatch to train on trainBatch(random.sample(D, BATCH)) if t > OBSERVE s_t=s_t1 t=t + 1 print("TIMESTEP", t, "/ EPSILON", epsilon, "/ ACTION", action_index, "/ REWARD", r_t,"/ Q_MAX " , np.max(Q_sa), "/ Loss ", loss)
將這個模型用到從Replay Memory里隨機選擇的一批上:
def trainBatch(minibatch): for i in range(0, len(minibatch)): loss=0 inputs=np.zeros((BATCH, s_t.shape[1], s_t.shape[2], s_t.shape[3])) #32, 20, 40, 4 targets=np.zeros((inputs.shape[0], ACTIONS)) #32, 2 state_t=minibatch[i][0] # 4D stack of images action_t=minibatch[i][1] #This is action index reward_t=minibatch[i][2] #reward at state_t due to action_t state_t1=minibatch[i][3] #next state terminal=minibatch[i][4] #wheather the agent died or survided due the action inputs[i:i + 1]=state_t targets[i]=model.predict(state_t) # predicted q values Q_sa=model.predict(state_t1) #predict q values for next step if terminal: targets[i, action_t]=reward_t # if terminated, only equals reward else: targets[i, action_t]=reward_t + GAMMA * np.max(Q_sa) loss +=model.train_on_batch(inputs, targets)
調用下面的方法,就能啟動上面的訓練流程:
#argument: observe, only plays if true, else trains def playGame(observe=False): game=Game() dino=DinoAgent(game) game_state=Game_sate(dino,game) model=buildmodel() trainNetwork(model,game_state)
這個模型,小哥用一周的時間訓練了200萬幀,其中前100萬幀用來調整游戲參數修補bug,后100萬幀真正用來訓練。
現在,這個模型的最好成績是265分。從下面的得分和損失變化圖里,能看出模型的loss在后100萬幀逐漸穩定,比較低,但是會隨時間波動。
游戲得分
后100幀的損失(loss)
雖然這個模型后來表現還算可以了,但比人類還是差了一大截。
當然,別忘了這個小哥比較窮,他只有一個i7的CPU。
他認為,模型學得還不夠快,得分還不夠高,要怪這樣幾個因素:一是因為用CPU來學習,它總是掉幀;二是供這個AI來玩耍的圖像實在是太小了,只有40×20,在當前的模型架構下就可能導致了特征的損失,還拖慢了學習速度。
如果改用GPU,說不定……
用GPU究竟會不會改善,你們可以拿這份代碼來試試:
https://github.com/ravi72munde/Chrome-Dino-Reinforcement-Learning
原文地址:
https://medium.com/acing-ai/how-i-build-an-ai-to-play-dino-run-e37f37bdf153
其實嘛,讓AI搞定小恐龍這件事,本質上跟讓AI搞定Flappy Bird是一樣的。如果你想深入研究一下這件事,這里再推薦兩篇。
機器學習玩轉Flappy Bird全書:六大“流派”從原理到代碼
使用神經網絡+遺傳算法玩轉Flappy Bird | 教程
就醬~
— 完 —
誠摯招聘
量子位正在招募編輯/記者,工作地點在北京中關村。期待有才氣、有熱情的同學加入我們!相關細節,請在量子位公眾號(QbitAI)對話界面,回復“招聘”兩個字。
量子位 QbitAI · 頭條號簽約作者
?'?' ? 追蹤AI技術和產品新動態
.說明:
1.1 推薦指數:★★★★
1.2 熟悉html的css3相關知識,展現python的強大和matplotlib高級作圖法,熟悉相關編程知識和思維。
1.3 本解析通俗易懂,適合任何人士,代碼本人親測過,建議python3.8、微軟vscode編輯器和谷歌瀏覽器使用。
1.4 有點長,適合收藏,慢慢玩。
比較真實的音樂音效動畫
2 先說python的matplotlib法
2.1 代碼:
#---導出模塊---
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
#---定義畫布大小、顏色、布局---
#fig,ax=plt.subplots() #等同于下面,建議采用下面這種方式
fig=plt.figure(figsize=(22,14),facecolor='black',edgecolor='white')
ax=fig.add_subplot(111, projection='3d',facecolor='black')
#--定義3d坐標軸的z和x,y---
z=[30]
x=np.arange(10)
#跳1萬次結束
for i in range(10000):
y=np.random.rand(10)
ax.cla() #清楚之前的繪圖,顯示動態更新的效果
#color,可以選c,r,g,w,y,b
ax.bar(x, y, zs=z, zdir='y', color='y', alpha=1)
#隱藏網格
ax.grid(False)
#隱藏三維坐標軸
ax.axis('off')
#這個要放在上面2個隱藏的后面,否則沒效果
plt.pause(0.05)
#圖片展示
plt.show()
2.2 效果圖:
3 html的css3法:
3.1 效果圖:
3.2 新建幾個文件:如圖
matplotlib法.py是上面的代碼
3.3 css3法.html代碼:
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>CSS3波浪音階動畫特效</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div class="bars bars--paused">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
3.4 script.js代碼:
const bars=document.querySelectorAll('.bar');
let intervalValue=0;
const delay=time=> new Promise(resolve=> setTimeout(resolve, time));
[...bars].map((bar)=> {
delay(0).then(()=> {
setTimeout(()=> {
bar.style.animation='sound 500ms linear infinite alternate'
}, intervalValue +=100)
})
})
3.5 style.css代碼:
*{margin:0;padding:0;list-style-type:none;}
.container {
height: 100vh;
/*背景顏色/從左到右漸變效果*/
background: linear-gradient(to right,blue,pink);
display: grid;
place-items: center;
}
.container .bars {
width: 300px;
height: 150px;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.container .bars .bar {
height: 100%;
width: 9%;
}
/*瀏覽器兼容問題-谷歌瀏覽器*/
@-webkit-keyframes sound {
0% {
opacity: 0.35;
background: greenyellow;
height: 1px;
}
100% {
opacity: 1;
background:blueviolet;
height: 100%;
}
}
3.6 備注:本機時谷歌瀏覽器操作效果,很多時候考慮不同瀏覽器,需要在css文件后面繼續添加適合其他瀏覽器,也就是在style.css代碼后面將下面的代碼復制進去即可。故意單獨拿出來,主要是考慮代碼的簡潔性和突出相關瀏覽器設置知識的重要性。
/*其他瀏覽器兼容問題*/
/*瀏覽器兼容問題-歐朋瀏覽器*/
@-o-keyframes sound {
0% {
opacity: 0.35;
background: greenyellow;
height: 1px;
}
100% {
opacity: 1;
background:blueviolet;
height: 100%;
}
}
/*瀏覽器兼容問題-火狐瀏覽器*/
@-moz-keyframes sound {
0% {
opacity: 0.35;
background: greenyellow;
height: 1px;
}
100% {
opacity: 1;
background:blueviolet;
height: 100%;
}
}
4.講一個額外的小知識點:(小細節注意一下)
在導入文件的路徑時,html的:./xxx.xx和python的:./xxx.xx不同。
4.1 前者:html:./stytle.css指的是和html同一個文件夾即可。
4.2 后者:也是這個意思,但是在運行python的py文件時,需要注意,用微軟的vscode編輯器直接按run(綠色小三角形)可能報錯,在當前的目錄下或文件夾下,進入終端運行python的編輯器,然后python xxx.py不會報錯。
5.自己整理一下,分享出來,一看就懂。
niapp項目里面要做按鈕 ,下載了幾個按鈕圖標之后,想弄成灰色禁用效果,作為手越高、人越懶信念的堅定支持者,懶得再去下載灰色圖標了,直接CSS搜起,發現了方便的辦法。
隨便找點兒圖片:
使用此句可以全部變灰。
filter: grayscale(1)
如果是grayscal(0.5)?
只有一點兒顏色,半灰是也,如同加了個濾鏡。
filter:n. 過濾器;濾光器,濾聲器,濾波器;過濾程序,過濾軟件;(指示車輛轉彎的)分流指示燈 v. 過濾;滲入,透過;(消息、信息等)慢慢傳開,走漏;緩慢進入,陸續步入;(車輛)看到分流指示燈的信號后轉彎;(程序)篩選。
放在自己項目里面,直接使用動態class搞定。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。