天天看點

​Python人工智能在貪吃蛇遊戲中的運用與探索(下)

之前,我們簡單的分析介紹了實作貪吃蛇的基本原理和工具,本篇我們将進一步用代碼分析其具體的形成過程。

設定規則

首先,我們需要設計運作時彈出的框的大小,在已設環境中,初始化蛇的長度和寬度,以及蛇每次移動的距離。這裡看個人喜好,不加以講解。

接下來我們需要确定蛇如何運動,貪吃蛇中比較重要的就是控制蛇的方向,這裡我們使用「随機函數」來設定了蛇的「方向」。定義了初始位置之後,我們用0到3四個數模拟上下左右。

如下我們假設「a=random.randint」(0,3)為0,這時我們設定它為向左走0到3個機關,此時定義方向為1。同理可以得到a另外三個取值的情況。

if a == 0:
    self.X = deque([x_start, x_start - 1, x_start - 2, x_start - 3])
    self.Y = deque([y_start, y_start, y_start, y_start])
    init_dir = 1
           

複制

模拟蛇吃果實的場景,吃到果實時,蛇的身體會增加一機關長度(增加多少随參數設定變化而不同),這時,我們可以等價看成「果實的坐标加入了整個蛇的坐标隊列」,即隻需把果實的坐标添加,再區分蛇頭和身體即可。判定吃到果實後,分數增加50,果實重新整理。

def eat_food(self, x_food, y_food):
    self.X.appendleft(x_food)
    self.Y.appendleft(y_food)
    self.LENGTH += 1
    self.MOVES += 100

def update(self, x_change, y_change):
    # 更新貪吃蛇身體位置
    for i in range(self.LENGTH - 1, 0, -1):
        self.X[i] = self.X[i - 1]
        self.Y[i] = self.Y[i - 1]

    # 更新貪吃蛇頭部位置
    self.X[0] = self.X[0] + x_change
    self.Y[0] = self.Y[0] + y_change

    self.MOVES -= 1
           

複制

if new_head_x == self.FOOD_X and new_head_y == self.FOOD_Y:
    self.SNAKE.eat_food(self.FOOD_X, self.FOOD_Y)  # 确認吃了食物
    reward = 50  # 給予吃食物的獎勵
    food_eaten = True
           

複制

def reset(self):
    ......
    ......
    self.FOOD_X, self.FOOD_Y = self.get_randoms()           

複制

接下來模拟蛇當場去世的情況。我們用坐标來模拟蛇,那麼顯然,當蛇的頭位置與身體位置發生重合時,即判定撲街。同時,如果蛇頭觸碰了邊界,我們同樣判定其死亡。代碼中我們會設定一個函數來判定,當其滿足死亡條件時,将被指派為False(訓練時我們将結果存儲在done[])。

def bit_itself(self):
    for i in range(1, self.LENGTH):
        if self.X[0] == self.X[i] and self.Y[0] == self.Y[i]:
            return True
    return False

def is_on_body(self, check_x, check_y, remove_last=True):
    X, Y = self.X.copy(), self.Y.copy()

    if remove_last:  # 不考慮尾部
        Y.pop()
    for x, y in zip(X, Y):
        if x == check_x and y == check_y:
            return True
    return False
           

複制

if new_head_x < x1 or new_head_x > x2 or new_head_y < y1 or new_head_y > y2:
    reward = -50
    done = True
    self.SNAKE.kill()           

複制

def kill(self):
    self.is_alive = False
           

複制

此處我們涉及到了zip()函數,zip的功能主要是将涉及到的元素合并起來,這裡通過将蛇身體的坐标的合并,可以輕松得到坐标列,當蛇頭(x,y)正好在該列之中,即重合。

if action == 0:
    if self.VELOCITY == 1:  # 向右移動
        x_change = 1
    else:
        x_change = -1  # 向左移動
        update = True
......
           

複制

具體的行動規則依照設定的上下左右的方式來決定。

到此我們完成了貪吃蛇設計的第一階段。

​Python人工智能在貪吃蛇遊戲中的運用與探索(下)

上圖是貪吃蛇人工智能誕生,訓練與運作的主要流程,我們将以該圖流程為依據展開。

初始化環境

神經網絡參數的設定
HIDDEN_UNITS = (32, 16, 32)  # 神經網絡隐藏層神經元數目
NETWORK_LR = 0.001  # 神經網絡學習率
BATCH_SIZE = 64  # 訓練批次
UPDATE_EVERY = 5  # 本地模型每訓練5次更新
GAMMA = 0.95  # 強化學習中的折扣因子
NUM_EPISODES = 5000  # 最大疊代次數
def train():
    agent = DeepQ_agent(env, hidden_units=HIDDEN_UNITS, network_LR=NETWORK_LR,
                        batch_size=BATCH_SIZE, update_every=UPDATE_EVERY,                               gamma=GAMMA)  # 深度學習環境
    ......
           

複制

折扣因子是估算未來的價值所需要的元素。在貪吃蛇中,我們需要大約确定未來幾步的最優選擇,而距離現在越遠,其影響越小,即當我們計算rewards時,未來每一步的分數都會乘折扣因子的n次方。

環境狀态設定
def reset(self):

    # 初始化貪吃蛇和食物的位置
    snake_x, snake_y = self.get_randoms(length=4)
    self.SNAKE = Snake(snake_x, snake_y, self.WIDTH, self.HEIGHT)
    self.VELOCITY = self.SNAKE.INITIAL_DIRECTION
    self.FOOD_X, self.FOOD_Y = self.get_randoms()
    self.STATE = self.SNAKE.look(self.FOOD_X, self.FOOD_Y,
    self.get_boundaries())
    return self.STATE
           

複制

初始化蛇、果實的位置,方向,将其存儲在state中(Q(s,a)的 s)

設定訓練模型和張量

判斷蛇和與環境的關系,生成環境張量

上面的代碼塊中的look就是機器判斷環境的核心

def look(self, x_food, y_food, boundaries):
    # 往上觀察
    up = self.lookInDirection(boundaries, y=-1, x=0)
    ......#左右等剩餘七個觀察
    head_x, head_y = self.head_pos()
    ......
    aa = np.hstack((food_position, up, up_right, right, down_right, down, down_left, left, up_left))
    return np.hstack((food_position, up, up_right, right, down_right, down, down_left, left, up_left))


def lookInDirection(self, boundaries, y, x):
    tail_distance = 0  # 0如果尾部不在這個方向
    distance = 1  # 1是離牆的最小距離
    curr_x, curr_y = self.head_pos()
    check_x, check_y = curr_x + x, curr_y + y  # 按照方向變化頭部位置
    x1, x2, y1, y2 = boundaries
    while y1 <= check_y <= y2 and x1 <= check_x <= x2:
        if tail_distance == 0 and (self.is_on_body(check_x, check_y,
        remove_last=False)):
            tail_distance = (1 / distance)
        # 持續疊代
        check_y += y
        check_x += x
        distance += 1
    return np.array([1 / distance, tail_distance])
           

複制

邊界距離從行動後distance的疊代得到。通過對果實,邊界距離、方向的計算與估計,确立出state。通過look出來的結果,将各種影響環境狀态的因素用hstack合并,生成環境張量。

開始訓練

建立本地和目标模型,并預估動作
self.qnetwork_local = QNetwork(input_shape=self.env.STATE_SPACE,
                               output_size=self.nA,
                               learning_rate=self.NETWORK_LR)
#print(self.qnetwork_local.model.summary())

#一個模型用于本地訓練,另外一個模型随之更新權重
self.qnetwork_target = QNetwork(input_shape=self.env.STATE_SPACE,
                                output_size=self.nA,
                                learning_rate=self.NETWORK_LR)

self.memory = ReplayMemory(self.MEMORY_CAPACITY, self.BATCH_SIZE)
           

複制

#使用本地模型估計下一個動作
target = self.qnetwork_local.predict(states, self.BATCH_SIZE)
#使用目标模型估計下一個動作
target_val = self.qnetwork_target.predict(
    next_states, self.BATCH_SIZE)
target_next = self.qnetwork_local.predict(
    next_states, self.BATCH_SIZE)           

複制

用兩個模型的優點和各自用途在(上)中已經講到過,這裡不再解釋。

幹貨 | Python人工智能在貪吃蛇遊戲中的應用探索(上)

計算分數
for i in range(self.BATCH_SIZE):
    if dones[i]:
        target[i][actions[i]] = rewards[i]
    else:
        target[i][actions[i]] = rewards[i] + self.GAMMA * \
            target_val[i][max_action_values[i]
           

複制

當蛇死掉,分數即為目前的分數。否則将繼續疊代,但是預估的每個未來的行動都會乘以折扣因子。同時,在訓練批次範圍内的多次循環會得到一系列的rewards,其中的最大值就是我們預估出的最佳行動方式,我們以此會不斷更新權重,使貪吃蛇更加智能。

執行訓練

開始移動
def act(self, state, epsilon):  #設定epsilon預設為0
    state = state.reshape((1,)+state.shape)
    action_values = self.qnetwork_local.predict(
        state)
    if random.random() > epsilon:
        #選擇最好的行動
        action = np.argmax(action_values)
    else:
        #選擇随機的行動
        action = random.randint(0, self.nA-1)
    return action
           

複制

行動的時候有兩種情況。此處我們設定了epsilon(取0.1),并取(0,1)随機數,當大于epsilon時則按照rewards最大的經驗行動。當小于時,則會随機采取一次行動,同時epsilon會乘以一個系數,直到設定的最小值(我們設定了0.01)。這樣做的目的,是為了在剛開始訓練時,快速積累行動經驗,加快人工智能的進步。同時,随着無數次的訓練,權重的不斷更新,蛇的行動會越來越準确。

存儲與提取經驗
def add_experience(self, state, action, reward, next_state, done):
    self.memory.add(state, action, reward, next_state, done)
    
def add(self, state, action, reward, next_state, done):
        '''把經驗存儲起來'''
    e = tuple((state, action, reward, next_state, done))
           

複制

def sample(self, state_shape):
    experiences = random.sample(self.memory, k=self.BATCH_SIZE)
    # 提取回報資訊
    states, actions, rewards, next_states, dones = zip(*experiences)
           

複制

def learn(self):
    if self.memory.__len__() > self.BATCH_SIZE:
        states, actions, rewards, next_states, dones = self.memory.sample(
            self.env.STATE_SPACE)           

複制

當經驗組數量大于訓練批次後,每次我們都會提取出64組(訓練批次數量)的經驗,來得到相應的action。

model_arch = '2019816'
resume_model_path = model_out_dir + '/snake_dqn_2019815_final.h5'
           

複制

agent.qnetwork_local.model = load_model('F://貪吃蛇//SnakeAI-master//agents_models//snake_dqn_2019816_3000.h5')
           

複制

這一步是人工智能貪吃蛇的關鍵步驟,「我們定義訓練模型的路徑,并在正式運作時調用。該訓練檔案會存入訓練生成神經網絡模型的權重。當我們運作時會調用該檔案中的權重。如果該路徑下沒有已有的訓練檔案,那麼訓練生成的權重就會存入。如果想要重新訓練,我們就要删除并重新定義這一路經。」

這裡的原理是「通過神經網絡的無數次訓練來調整權重。當貪吃蛇訓練時遇到新環境,根據已有模型,預測一個接近的結果向量來選擇最好的方式移動。訓練環境也會逐漸擴大(訓練環境與遇到的環境不是一個),當達到訓練批次時就會固定。」

總流程如下

​Python人工智能在貪吃蛇遊戲中的運用與探索(下)

自此,貪吃蛇人工智能的介紹結束。

「參考文獻:」

貪吃蛇代碼

源代碼作者:齊浩洋(中國香港城市大學資料科學學院)

文案 && 編輯:白宇嘯(華中科技大學管理學院)

審稿&&修正:齊浩洋(中國香港城市大學資料科學學院)

指導老師:秦虎(華中科技大學管理學院)

如對文中内容有疑問,歡迎交流。