Chrome暗藏的恐龙跳一跳,已经被AI轻松掌握了
夏乙 郭一璞 發(fā)自 凹非寺
量子位 出品 | 公眾號(hào) QbitAI
什么!未連接到互聯(lián)網(wǎng)!!
明明是聯(lián)網(wǎng)狀態(tài),為什么我想訪(fǎng)問(wèn)的頁(yè)面
無(wú)!法!打!開(kāi)!
淡定。
作為一個(gè)Google Chrome瀏覽器的用戶(hù),當(dāng)你看到上面那個(gè)頁(yè)面時(shí),不要沮喪。換個(gè)角度一想,墻內(nèi)還能有更多的Play時(shí)間哦~
你有沒(méi)有注意到畫(huà)面里那個(gè)小恐龍?
當(dāng)你遇到打不開(kāi)網(wǎng)頁(yè)的時(shí)候,只需要再點(diǎn)擊一下這個(gè)頁(yè)面(手機(jī)),或者按下空格(電腦),隨著小恐龍輕輕一跳——
一個(gè)新世界開(kāi)啟了。
這個(gè)“恐龍?zhí)惶逼鋵?shí)是藏在Chrome瀏覽器里好多年的一個(gè)彩蛋。小恐龍是一只霸王龍(T-Rex)。
2013年Chrome開(kāi)始用這個(gè)小恐龍的圖像代替令人煩惱的404頁(yè)面。2014年秋天,這只恐龍被正式改造成一個(gè)橫版小游戲。以彩蛋的方式隱藏在新版Chrome瀏覽器里。
吶,如果你還不知道這個(gè)彩蛋,可以抓緊試一試。比方說(shuō)——
-
訪(fǎng)問(wèn)一個(gè)不翻墻就看不了的網(wǎng)頁(yè)
-
或者直接輸入:chrome://dino
-
或者訪(fǎng)問(wèn):https://chromedino.com/ (需翻墻)
后來(lái),這個(gè)小游戲也成了不少AI練手的對(duì)象。
比如最近就有人在YouTube上貼了一段視頻,展示了他如何用神經(jīng)網(wǎng)絡(luò)+遺傳算法,讓一個(gè)AI系統(tǒng)獨(dú)秀于瀏覽器之中。
我們把精華的部分截取了一下,就是下面這段視頻。
動(dòng)圖版:
速度已經(jīng)快到飛起
總而言之,一句話(huà),這個(gè)AI能輕松玩到2萬(wàn)多分……
你能玩到幾分?大概率是玩不到這個(gè)成績(jī)的吧。畢竟在chromedino.com頁(yè)面上,人類(lèi)玩家的歷史最高分是18842。
不過(guò),上傳這段視頻的作者,并沒(méi)有詳細(xì)公布他用的方法,當(dāng)然也沒(méi)有給出一個(gè)開(kāi)源的地址。不過(guò)不要緊,也有別人公開(kāi)分享了更多細(xì)節(jié)。
例如,GitHub上就有一個(gè)開(kāi)源的代碼“IAMDinosaur”,同樣也是利用神經(jīng)網(wǎng)絡(luò)+遺傳算法,來(lái)搞定恐龍?zhí)惶?/p>
地址在此:https://github.com/ivanseidel/IAMDinosaur
美中不足,上面這個(gè)項(xiàng)目也沒(méi)有配上太詳盡的解讀。然而好消息是,最近有個(gè)國(guó)外的小哥Ravi Munde,列了一份非常詳盡的教程。
這個(gè)教程用的方法是強(qiáng)化學(xué)習(xí)中的Q-learning,比較適合入門(mén)練手,而且對(duì)硬件的要求不高。
量子位搬運(yùn)這份教程如下。
Q-learning了解/復(fù)習(xí)一下
對(duì)動(dòng)物來(lái)說(shuō),強(qiáng)化學(xué)習(xí)的能力是與生俱來(lái)的。拿兒童學(xué)步來(lái)舉例,如果小朋友努力的邁出第一步,就會(huì)獲得父母的鼓勵(lì)——可能是鼓掌叫好,也可能是一塊糖;但如果小朋友堅(jiān)決不肯學(xué)習(xí)走路,那父母就不會(huì)給它糖吃了。強(qiáng)化學(xué)習(xí)就是依照這類(lèi)激勵(lì)行為而設(shè)置的。
而在這個(gè)游戲中,對(duì)我們的AI小恐龍來(lái)說(shuō),強(qiáng)化學(xué)習(xí)需要讓他在無(wú)監(jiān)督的情況下,先認(rèn)識(shí)到做出不同動(dòng)作的結(jié)果,并且以獲得高分為最高激勵(lì)。
一個(gè)典型的強(qiáng)化學(xué)習(xí)閉環(huán)
Ravi Munde用Q-learning模擬了一個(gè)特殊函數(shù),這個(gè)函數(shù)驅(qū)動(dòng)AI在不同狀況下做出正確的選擇。
Q-learning是強(qiáng)化學(xué)習(xí)的一種無(wú)模型實(shí)現(xiàn),根據(jù)Q值對(duì)每個(gè)狀態(tài)進(jìn)行判斷此時(shí)如果采取行動(dòng),能獲得怎樣的獎(jiǎng)勵(lì)。一個(gè)樣本Q表讓我們了解數(shù)據(jù)的結(jié)構(gòu)。在恐龍跑酷游戲中,狀態(tài)是當(dāng)前的游戲截圖,能采取的行動(dòng)是跳或不跳[0,1]
一個(gè)樣本Q表
Ravi Munde決定用深度神經(jīng)網(wǎng)絡(luò)來(lái)決定小恐龍何時(shí)起跳,而且要在最簡(jiǎn)單的強(qiáng)化學(xué)習(xí)實(shí)現(xiàn)基礎(chǔ)上,引入不同參數(shù)來(lái)輔助它。
缺乏已標(biāo)記的數(shù)據(jù)讓強(qiáng)化學(xué)習(xí)非常不穩(wěn)定。為了獲得適用于這個(gè)游戲的數(shù)據(jù),Munde小哥決定,先讓小恐龍自己瞎跳幾千次,把每個(gè)動(dòng)作的反饋記下來(lái),然后從數(shù)據(jù)中隨機(jī)挑選一些來(lái)訓(xùn)練模型。
但之后,Munde小哥發(fā)現(xiàn),他訓(xùn)練了一個(gè)倔強(qiáng)的模型——模型堅(jiān)定的認(rèn)為,跳,一定比不跳好。所以,為了讓模型在訓(xùn)練時(shí)能在跳與不跳之間多嘗試一下,他引入了一個(gè)函數(shù)?來(lái)決定行動(dòng)的隨機(jī)性,然后再逐漸減小它的值來(lái)削減隨機(jī)性,最終讓模型去選擇最有可能獲得獎(jiǎng)勵(lì)的行動(dòng)。
贊譽(yù)分布(Credit Assignment)問(wèn)題可能會(huì)讓模型陷入混亂——目前獲得的獎(jiǎng)勵(lì)究竟來(lái)自于過(guò)去的哪個(gè)行為呢?在恐龍跑酷游戲中,小恐龍?zhí)桨肟罩泻鬅o(wú)法再次跳躍,但模型可能會(huì)在恐龍?zhí)幱诎肟罩袝r(shí)發(fā)出跳躍指令,這種情況就讓恐龍非常容易砸到仙人掌上。
在這種情況下,“砸到仙人掌上”這個(gè)負(fù)反饋實(shí)際上是此前上一次做出跳躍決定的結(jié)果,而不是剛剛恐龍?jiān)诎肟罩袝r(shí)做出的跳躍結(jié)果所導(dǎo)致的。
在面臨這種問(wèn)題的情況下,可以引入貼現(xiàn)因子(Discount Factor)γ來(lái)決定模型做出動(dòng)作時(shí)看得多遠(yuǎn)。γ間接解決了贊譽(yù)分布問(wèn)題,在這個(gè)游戲中,當(dāng)γ=0.99時(shí),模型認(rèn)識(shí)到在無(wú)障礙時(shí)隨便跳會(huì)導(dǎo)致真的遇到障礙時(shí)自己正在半空中,無(wú)法繼續(xù)跳躍。
除了這兩個(gè)參數(shù)之外,后面就幾乎不需要任何參數(shù)了。
#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
你需要準(zhǔn)備的是
-
Python 3.6
-
Selenium
-
OpenCV
-
PIL
-
Chromium driver for Selenium
-
Keras
略微解釋一下這幾個(gè)工具。
構(gòu)建這個(gè)AI模型,需要用Python編程。而游戲是用JavaScript寫(xiě)成的。所以,得借助一些工具才能更好地溝通。
Selenium是一種流行的瀏覽器自動(dòng)化工具,用于向?yàn)g覽器發(fā)送操作指令,以及獲取各種游戲參數(shù)。
接口的事情搞定了,還得想辦法獲得游戲截屏。用Selenium也行,但是速度很慢,截屏和處理一次大約得1秒鐘。
用PIL和OpenCV能夠更好地完成截屏和圖像預(yù)處理,可以達(dá)到5fps的幀率。你可能覺(jué)得還是慢,但已經(jīng)足夠?qū)Ω哆@個(gè)游戲了。
游戲模塊
下面這個(gè)模塊,實(shí)現(xiàn)了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()
恐龍智能體模塊
這個(gè)模塊在游戲模塊的幫助下,用于控制小恐龍的動(dòng)作。
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()
游戲狀態(tài)模塊
神經(jīng)網(wǎng)絡(luò)直接使用這個(gè)模塊,來(lái)執(zhí)行操作并獲取新的狀態(tài)。
'''
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
預(yù)處理
游戲修改
原始的游戲相對(duì)復(fù)雜,比如游戲速度會(huì)逐漸加快,障礙物會(huì)改變,還會(huì)出現(xiàn)云朵、星星、地面紋理等。一次同時(shí)學(xué)習(xí)這么多東西會(huì)消耗大量時(shí)間,甚至在訓(xùn)練過(guò)程中引入不必要的噪音。
為此作者修改了游戲的源代碼、簡(jiǎn)化局面,去除了一些視覺(jué)元素(云、歷史最佳成績(jī)等),還有讓恐龍的奔跑速度保持不變。
原圖
修改后
圖像處理
原始截圖的分辨率為1200×300,包含三個(gè)通道。作者計(jì)劃使用4個(gè)連續(xù)的屏幕截圖作為模型的單一輸入,也就是1200×300×3×4。
問(wèn)題是,這個(gè)小哥只有一個(gè)i7的CPU可用,所以他的電腦沒(méi)辦法在處理這個(gè)尺寸輸入的同時(shí)玩游戲。所以,還得繼續(xù)用OpenCV的庫(kù)調(diào)正截圖大小、裁剪等。最終輸入圖像大小為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張圖創(chuàng)建單個(gè)輸入,也就是:40×20×4。請(qǐng)注意,這里小恐龍也裁減掉了,因?yàn)檎麄€(gè)學(xué)習(xí)過(guò)程,只需要知道障礙物和與邊緣的距離即可。
模型架構(gòu)
現(xiàn)在輸入有了,用模型輸出來(lái)玩游戲的方法也有了,只差模型架構(gòu)。
小哥選擇把3個(gè)卷積層壓平,連接到一個(gè)512神經(jīng)元的全連接層(dense layer)上。池化層直接被砍掉了,這個(gè)東西在圖像分類(lèi)問(wèn)題上很有用,但是玩Dino的時(shí)候神經(jīng)網(wǎng)絡(luò)只需要知道障礙物的位置,池化層就起不了什么作用了。
多層網(wǎng)絡(luò)架構(gòu)
這個(gè)模型的輸出,形狀和可能的操作數(shù)量一樣。模型會(huì)預(yù)測(cè)各種操作的Q值,也叫discounted future reward,然后我們選數(shù)值最高的那個(gè)。
下面這段代碼,就能召喚一個(gè)用TensorFlow后端的Keras來(lái)搭建的模型:
#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
開(kāi)始訓(xùn)練
接下來(lái),就是見(jiàn)證奇跡的時(shí)刻~~
也就是用一段代碼來(lái)訓(xùn)練模型,這段代碼的任務(wù)是:
-
從無(wú)操作開(kāi)始,得到初始狀態(tài)initial state(s_t)
-
觀(guān)察玩游戲的過(guò)程,代碼中的OBSERVATION表示步數(shù)
-
預(yù)測(cè)一個(gè)操作的效果
-
在Replay Memory中存儲(chǔ)經(jīng)驗(yàn)
-
訓(xùn)練階段,從Replay Memory里隨機(jī)選擇一組,用它來(lái)訓(xùn)練模型
-
如果game over了,就重開(kāi)一局
更詳細(xì)的,可以看這段自帶注釋的代碼:
'''
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)
將這個(gè)模型用到從Replay Memory里隨機(jī)選擇的一批上:
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)
主體方法
調(diào)用下面的方法,就能啟動(dòng)上面的訓(xùn)練流程:
#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)
結(jié)果
這個(gè)模型,小哥用一周的時(shí)間訓(xùn)練了200萬(wàn)幀,其中前100萬(wàn)幀用來(lái)調(diào)整游戲參數(shù)修補(bǔ)bug,后100萬(wàn)幀真正用來(lái)訓(xùn)練。
現(xiàn)在,這個(gè)模型的最好成績(jī)是265分。從下面的得分和損失變化圖里,能看出模型的loss在后100萬(wàn)幀逐漸穩(wěn)定,比較低,但是會(huì)隨時(shí)間波動(dòng)。
游戲得分
后100幀的損失(loss)
目前的局限
雖然這個(gè)模型后來(lái)表現(xiàn)還算可以了,但比人類(lèi)還是差了一大截。
當(dāng)然,別忘了這個(gè)小哥比較窮,他只有一個(gè)i7的CPU。
他認(rèn)為,模型學(xué)得還不夠快,得分還不夠高,要怪這樣幾個(gè)因素:一是因?yàn)橛肅PU來(lái)學(xué)習(xí),它總是掉幀;二是供這個(gè)AI來(lái)玩耍的圖像實(shí)在是太小了,只有40×20,在當(dāng)前的模型架構(gòu)下就可能導(dǎo)致了特征的損失,還拖慢了學(xué)習(xí)速度。
如果改用GPU,說(shuō)不定……
相關(guān)鏈接
用GPU究竟會(huì)不會(huì)改善,你們可以拿這份代碼來(lái)試試:
https://github.com/ravi72munde/Chrome-Dino-Reinforcement-Learning
原文地址:
https://medium.com/acing-ai/how-i-build-an-ai-to-play-dino-run-e37f37bdf153
One More Thing
其實(shí)嘛,讓AI搞定小恐龍這件事,本質(zhì)上跟讓AI搞定Flappy Bird是一樣的。如果你想深入研究一下這件事,這里再推薦兩篇。
機(jī)器學(xué)習(xí)玩轉(zhuǎn)Flappy Bird全書(shū):六大“流派”從原理到代碼
使用神經(jīng)網(wǎng)絡(luò)+遺傳算法玩轉(zhuǎn)Flappy Bird | 教程
就醬~
— 完 —
誠(chéng)摯招聘
量子位正在招募編輯/記者,工作地點(diǎn)在北京中關(guān)村。期待有才氣、有熱情的同學(xué)加入我們!相關(guān)細(xì)節(jié),請(qǐng)?jiān)诹孔游还娞?hào)(QbitAI)對(duì)話(huà)界面,回復(fù)“招聘”兩個(gè)字。
量子位 QbitAI · 頭條號(hào)簽約作者
?'?' ? 追蹤AI技術(shù)和產(chǎn)品新動(dòng)態(tài)
總結(jié)
以上是生活随笔為你收集整理的Chrome暗藏的恐龙跳一跳,已经被AI轻松掌握了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电脑开机以后一直重启解决办法如何让电脑开
- 下一篇: 如何在Mac电脑中删除Windows系统