【深入浅出强化学习-编程实战】6 基于函数逼近的方法-flappy bird

【深入浅出强化学习-编程实战】6 基于函数逼近的方法-flappy bird


玩家通过控制小鸟上下运动躲避不断到来的柱子,有两个动作可选:一个是飞,一个是不进行任何操作。采用动作飞时,小鸟会向上飞;不进行操作,小鸟会向下掉。小鸟飞行一步没有撞到柱子立即回报0.1;当小鸟撞到柱子立即回报-1;当小鸟躲过一个柱子时立即回报为1。玩家的目的是控制小鸟躲过尽可能多的柱子,得到尽量多的分数。

6.2.1 代码

dqn_bird.py

# 经验池类
# 该类用于经验数据的存储和训练数据的采集

from __future__ import print_function
import tensorflow as tf
import numpy as np
import cv2 # 图像处理库
import sys # 系统控制库
sys.path.append("game/")
import game.wrapped_flappy_bird as game# 游戏模块
import random

# 设置与本算法相关的超参数
GAME = 'flappy bird' # 游戏名
ACTIONS = 2 # “飞行”和“什么都不做”
GAMMA = 0.99 # 折扣因子
OBSERVE = 10000 # 训练前观察的步长,在这段时间内探索率不变,已得到各种情况
EXPLORE = 3.0e6 # 随机探索的时间,即从初始探索率衰减到最终探索率的时间设置为30万步,在这段时间内探索率线性减小
FINAL_EPSILON = 1.0e-4 # 最终的探索率
INITIAL_EPSILON = 0.1 # 初始探索率
REPLAY_MEMORY = 50000 # 经验池的大小,即经验池中有50000个可以用于采样学习的数据
BATCH = 32 # mini-batch的大小,即在学习训练的时候,从经验池中随机采集32个数据进行训练
FRAME_PER_ACTION = 1 # 跳帧

# 定义经验回报类
class Experience_Buffer():
    def __init__(self,buffer_size = REPLAY_MEMORY):
        # 定义一个空的经验池
        self.buffer = []
        # 定义经验池最大容量
        self.buffer_size = buffer_size

    # 向经验池添加一条经验数据
    def add_experience(self,experience):
        # 先判断经验池是否已经满了
        # 如果满了,将最顶端的数据清空,换成最新的经验数据
        if len(self.buffer)+len(experience) >=  self.buffer_size:
            self.buffer[0:len(self.buffer)+len(experience)-self.buffer_size] = []
            self.buffer.extend(experience)


    # 采集训练数据样本
    def sample(self,samples_num):
        # 随机采样mini-batch的数据
        # 然后将数据进行整理,返回训练时所需要的数据格式
        sample_data = random.sample(self.buffer,samples_num)
        train_s = [d[0] for d in sample_data]
        train_a = [d[1] for d in sample_data]
        train_r = [d[2] for d in sample_data]
        train_s_ = [d[3] for d in sample_data]
        train_terminal = [d[4] for d in sample_data]
        return train_s,train_a,train_r,train_s_,train_terminal

# 深度q学习类
# 该类中定义DQN学习算法,对小鸟进行训练

class Deep_Q_N():
    # 初始类成员函数
    # 该函数内我们调用Tensorflow,声明一个图,定义输入层,调用类成员子函数创建行为-值网络、目标值网络,定义目标值网络的更新方式,定义损失函数,构建优化器。
    # 初始化图中变量,保存声明
    def __init__(self,lr=1.0e-6,model_file=None):
        self.gamma = GAMMA
        self.tau = 0.01
        # tf工程
        self.sess = tf.Session()
        self.learning_rate = lr
        # 1.输入层
        self.obs = tf.placeholder(tf.float32,shape = [None,80,80,4])
        self.obs_ = tf.placeholder(tf.float32,shape= [None,80,80,4])
        self.action = tf.placeholder(tf.float32,shape=[None,ACTIONS])
        self.action_ = tf.placeholder(tf.float32,shape=[None,ACTIONS])
        # 2.1 创建深度q网络
        self.Q = self.build_q_net(self.obs,scope='eval',trainable=True)
        # 2.2 创建目标q网络
        self.Q_ = self.build_q_net(self.obs_,scope='target',trainable=False)
        # 2.3 整理两套网络参数
        self.qe_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope='eval')# 获取训练变量
        self.qt_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope='target')
        # 2.4 定义新旧参数的替换操作
        self.update_oldq_op = [oldq.assign((1-self.tau)*oldq+self.tau*p) for p,oldq in zip(self.qe_params,self.qt_params)]
        # 3.构建损失函数
        # td target
        self.Q_target = tf.placeholder(tf.float32,[None])
        readout_q = tf.reduce_sum(tf.multiply(self.Q,self.action),reduction_indices=1)
        self.q_loss = tf.losses.mean_squared_error(labels=self.Q_target,predictions = readout_q)
        # 4.定义优化器
        self.q_train_op = tf.train.AdamOptimizer(lr).minimize(self.q_loss,var_list=self.qe_params)
        # 5.初始化图中的变量
        self.sess.run(tf.global_variables_initializer())
        # 定义保存和恢复模型
        self.saver = tf.train.Saver()
        if model_file is not None:
            self.restore_model(model_file)

    # 模型存储子函数save_model,用于存储模型参数
    def save_model(self,model_path,global_step):
        self.saver.save(self.sess,model_path,global_step=global_step)

    # 模型恢复子函数restore_model,用于恢复模型参数
    def restore_model(self,model_path):
        self.saver.restore(self.sess,model_path)

    # 深度q网络构建子函数build_q_net,
    # 输入为观测、变量命名空间scope和变量性质trainable
    # 该子函数在初始化成员函数中被调用,由于预测用的行为-值函数和用于目标的行为-值函数是两套参数,所以可以通过使用不同的命名空间scope来区分两组参数
    def build_q_net(self,obs,scope,trainable):
        # 该深度网络由3个卷积层,1个池化层,2个全连接层组成
        # 第一个卷积层的卷积核大小为8*8*4*32,步长为4;后面连接一个池化层,池化层特征2*2,步长为2
        # 第二个卷积层的卷积核大小为4*4*32*64,步长为2;后面连一个卷积层
        # 第三个卷积层的卷积核大小为3*3*64*64,步长为1,将第三个卷积层的输出展开成维数为1600的1维向量,后面接两个全连接层
        # 第一个全连接层为1600*512,激活函数为ReLU
        # 第二个全连接层为512*2,没有激活函数,即线性输出
        with tf.variable_scope(scope):
            #第一个卷积层的卷积核大小为8 * 8 * 4 * 32,步长为4;
            h_conv1 = tf.layers.conv2d(inputs=obs, filters=32, kernel_size=[8, 8], strides=4, padding="same",
                                       activation=tf.nn.relu,
                                       kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01), \
                                       bias_initializer=tf.constant_initializer(0.01), trainable=trainable)
            #后面连接一个池化层,池化层特征2 * 2,步长为2
            h_pool1 = tf.layers.max_pooling2d(h_conv1, pool_size=[2,2],strides=2, padding="SAME")
            #第二个卷积层的卷积核大小为4 * 4 * 32 * 64,步长为2;
            h_conv2 = tf.layers.conv2d(inputs=h_pool1, filters=64, kernel_size=[4, 4], strides=2, padding="same",
                                       activation=tf.nn.relu,
                                       kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01), \
                                       bias_initializer=tf.constant_initializer(0.01), trainable=trainable)
            # 第三个卷积层的卷积核大小为3*3*64*64,步长为1,
            h_conv3 = tf.layers.conv2d(inputs=h_conv2, filters=64, kernel_size=[3, 3], strides=1, padding="same",
                                       activation=tf.nn.relu,
                                       kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01), \
                                       bias_initializer=tf.constant_initializer(0.01), trainable=trainable)
            # 将第三个卷积层的输出展开成维数为1600的1维向量,
            h_conv3_flat = tf.reshape(h_conv3,[-1,1600])
            # 第一个全连接层为1600 * 512,激活函数为ReLU
            h_fc1 = tf.layers.dense(inputs=h_conv3_flat,
                                    units=512,
                                    activation=tf.nn.relu,
                                    kernel_initializer=tf.random_normal_initializer(0,stddev=0.1),
                                    bias_initializer=tf.constant_initializer(0.1),
                                    trainable=trainable)
            # 第二个全连接层为512*2,没有激活函数,即线性输出
            # 读出层,没有激活函数
            qout = tf.layers.dense(inputs=h_fc1,
                                    units=ACTIONS,
                                    kernel_initializer=tf.random_normal_initializer(0,stddev=0.1),
                                    bias_initializer=tf.constant_initializer(0.1),
                                    trainable=trainable)
        return qout

    # 用于采样动作的利用探索-平衡策略子函数epsilon-greedy
    # 该算法输入为当前状态和探索率,输出为当前状态所对应的探索策略,用于于环境交互
    # 与表格型Q-learning不同的是,这里调用神经网络来确定哪个是最优动作
    def epsilon_greedy(self,s_t,epsilon):
        a_t = np.zeros([ACTIONS])
        amax = np.argmax(self.sess.run(self.Q,{self.obs:[s_t]})[0])
        # 概率部分
        if np.random.uniform()<1-epsilon:
            # 最优动作
            a_t[amax] = 1
        else:
            a_t[random.randrange(ACTIONS)] = 1
        return a_t

    # 网络训练子函数train_Network
    # 该子函数基于Q-learning的框架,基于网络表示的行为值函数对智能体进行训练
    # 与环境交互,将数据存入经验池中,从经验池中采集数据对神经网络进行训练
    def train_Network(self,experience_buffer):
        # 打开游戏状态与模拟器进行通信
        game_state = game.GameState()
        # 获得第1个状态并将图像进行预处理
        do_nothing = np.zeros([ACTIONS])
        do_nothing[0] = 1
        # 与游戏交互1次
        x_t,r_0,terminal = game_state.frame_step(do_nothing)
        x_t = cv2.cvtColor(cv2.resize(x_t,(80,80)),cv2.COLOR_BGR2GRAY)
        ret,x_t = cv2.threshold(x_t,1,255,cv2.THRESH_BINARY)
        s_t = np.stack((x_t,x_t,x_t,x_t),axis=2)
        # 开始训练
        epsilon = INITIAL_EPSILON
        t = 0
        while "flappy bird"!="angry bird":
            a_t = self.epsilon_greedy(s_t,epsilon=epsilon)
            # epsilon递减
            if epsilon > FINAL_EPSILON and t > OBSERVE:
                epsilon -= (INITIAL_EPSILON-FINAL_EPSILON)/EXPLORE
            # 运动动作,与游戏环境交互1次
            x_t1_colored,r_t,terminal = game_state.frame_step(a_t)
            x_t1 = cv2.cvtColor(cv2.resize(x_t1_colored,(80,80)),cv2.COLOR_BGR2GRAY)
            ret,x_t1 = cv2.threshold(x_t1,1,255,cv2.THRESH_BINARY)
            x_t1 = np.reshape(x_t1,(80,80,1))
            s_t1 =np.append(x_t1,s_t[:,:,:3],axis=2)
            # 将数据存储到经验池中
            experience = np.reshape(np.array([s_t,a_t,r_t,s_t1,terminal]),[1,5])
            print("experience", r_t, terminal)
            experience_buffer.add_experience(experience)
            # 在观测结束后进行训练
            if t>OBSERVE:
                # 采集样本
                train_s,train_a,train_r,train_s_,train_terminal = experience_buffer.sample(BATCH)
                target_q = []
                read_target_Q = self.sess.run(self.Q_,{self.obs_:train_s_})
                for i in range(len(train_r)):
                    if train_terminal[i]:
                        target_q.append(train_r[i])
                    else:
                        target_q.append(train_r[i]+GAMMA*np.max(read_target_Q[i]))
                # 训练1次
                self.sess.run(self.q_train_op,feed_dict={self.obs:train_s,self.action:train_a,self.Q_target:target_q})
                # 更新旧的目标网络
                self.sess.run(self.update_oldq_op)
            # 往前推进一步
            s_t = s_t1
            t+=1
            # 每10000次迭代保存一次
            if t % 10000 == 0:
                self.save_model('saved_networks/',global_step=t)
            if t <= OBSERVE:
                print("OBSERVE",t)
            else:
                if t % 1 == 0:
                    print("train,steps",t,"/epsilon",epsilon,"/action_index",a_t,"/reward",r_t)


# 主函数
# 对DQN进行训练
# 首先实例化1个经验池类buffer,声明一个深度值网络类brain,调用brain类的训练子函数对深度值网络进行训练
if __name__=="__main__":
    buffer = Experience_Buffer()
    brain = Deep_Q_N()
    brain.train_Network(buffer)

wrapped_flappy_bird.py

import numpy as np
import sys
import random
import pygame
import flappy_bird_utils
import pygame.surfarray as surfarray
from pygame.locals import *
from itertools import cycle

FPS = 30
SCREENWIDTH  = 288
SCREENHEIGHT = 512

pygame.init()
FPSCLOCK = pygame.time.Clock()
SCREEN = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption('Flappy Bird')

IMAGES, SOUNDS, HITMASKS = flappy_bird_utils.load()
PIPEGAPSIZE = 100 # gap between upper and lower part of pipe
BASEY = SCREENHEIGHT * 0.79

PLAYER_WIDTH = IMAGES['player'][0].get_width()
PLAYER_HEIGHT = IMAGES['player'][0].get_height()
PIPE_WIDTH = IMAGES['pipe'][0].get_width()
PIPE_HEIGHT = IMAGES['pipe'][0].get_height()
BACKGROUND_WIDTH = IMAGES['background'].get_width()

PLAYER_INDEX_GEN = cycle([0, 1, 2, 1])


class GameState:
    def __init__(self):
        self.score = self.playerIndex = self.loopIter = 0
        self.playerx = int(SCREENWIDTH * 0.2)
        self.playery = int((SCREENHEIGHT - PLAYER_HEIGHT) / 2)
        self.basex = 0
        self.baseShift = IMAGES['base'].get_width() - BACKGROUND_WIDTH

        newPipe1 = getRandomPipe()
        newPipe2 = getRandomPipe()
        self.upperPipes = [
            {'x': SCREENWIDTH, 'y': newPipe1[0]['y']},
            {'x': SCREENWIDTH + (SCREENWIDTH / 2), 'y': newPipe2[0]['y']},
        ]
        self.lowerPipes = [
            {'x': SCREENWIDTH, 'y': newPipe1[1]['y']},
            {'x': SCREENWIDTH + (SCREENWIDTH / 2), 'y': newPipe2[1]['y']},
        ]

        # player velocity, max velocity, downward accleration, accleration on flap
        self.pipeVelX = -4
        self.playerVelY    =  0    # player's velocity along Y, default same as playerFlapped
        self.playerMaxVelY =  10   # max vel along Y, max descend speed
        self.playerMinVelY =  -8   # min vel along Y, max ascend speed
        self.playerAccY    =   1   # players downward accleration
        self.playerFlapAcc =  -9   # players speed on flapping
        self.playerFlapped = False # True when player flaps

    def frame_step(self, input_actions):
        pygame.event.pump()

        reward = 0.1
        terminal = False

        if sum(input_actions) != 1:
            raise ValueError('Multiple input actions!')

        # input_actions[0] == 1: do nothing
        # input_actions[1] == 1: flap the bird
        if input_actions[1] == 1:
            if self.playery > -2 * PLAYER_HEIGHT:
                self.playerVelY = self.playerFlapAcc
                self.playerFlapped = True
                #SOUNDS['wing'].play()

        # check for score
        playerMidPos = self.playerx + PLAYER_WIDTH / 2
        for pipe in self.upperPipes:
            pipeMidPos = pipe['x'] + PIPE_WIDTH / 2
            if pipeMidPos <= playerMidPos < pipeMidPos + 4:
                self.score += 1
                #SOUNDS['point'].play()
                reward = 1

        # playerIndex basex change
        if (self.loopIter + 1) % 3 == 0:
            self.playerIndex = next(PLAYER_INDEX_GEN)
        self.loopIter = (self.loopIter + 1) % 30
        self.basex = -((-self.basex + 100) % self.baseShift)

        # player's movement
        if self.playerVelY < self.playerMaxVelY and not self.playerFlapped:
            self.playerVelY += self.playerAccY
        if self.playerFlapped:
            self.playerFlapped = False
        self.playery += min(self.playerVelY, BASEY - self.playery - PLAYER_HEIGHT)
        if self.playery < 0:
            self.playery = 0

        # move pipes to left
        for uPipe, lPipe in zip(self.upperPipes, self.lowerPipes):
            uPipe['x'] += self.pipeVelX
            lPipe['x'] += self.pipeVelX

        # add new pipe when first pipe is about to touch left of screen
        if 0 < self.upperPipes[0]['x'] < 5:
            newPipe = getRandomPipe()
            self.upperPipes.append(newPipe[0])
            self.lowerPipes.append(newPipe[1])

        # remove first pipe if its out of the screen
        if self.upperPipes[0]['x'] < -PIPE_WIDTH:
            self.upperPipes.pop(0)
            self.lowerPipes.pop(0)

        # check if crash here
        isCrash= checkCrash({'x': self.playerx, 'y': self.playery,
                             'index': self.playerIndex},
                            self.upperPipes, self.lowerPipes)
        if isCrash:
            #SOUNDS['hit'].play()
            #SOUNDS['die'].play()
            terminal = True
            #重新初始化
            self.__init__()
            reward = -3

        # draw sprites
        SCREEN.blit(IMAGES['background'], (0,0))

        for uPipe, lPipe in zip(self.upperPipes, self.lowerPipes):
            SCREEN.blit(IMAGES['pipe'][0], (uPipe['x'], uPipe['y']))
            SCREEN.blit(IMAGES['pipe'][1], (lPipe['x'], lPipe['y']))

        SCREEN.blit(IMAGES['base'], (self.basex, BASEY))
        # print score so player overlaps the score
        # showScore(self.score)
        SCREEN.blit(IMAGES['player'][self.playerIndex],
                    (self.playerx, self.playery))

        image_data = pygame.surfarray.array3d(pygame.display.get_surface())
        pygame.display.update()
        FPSCLOCK.tick(FPS)
        #print self.upperPipes[0]['y'] + PIPE_HEIGHT - int(BASEY * 0.2)
        return image_data, reward, terminal

def getRandomPipe():
    """returns a randomly generated pipe"""
    # y of gap between upper and lower pipe
    gapYs = [20, 30, 40, 50, 60, 70, 80, 90]
    index = random.randint(0, len(gapYs)-1)
    gapY = gapYs[index]

    gapY += int(BASEY * 0.2)
    pipeX = SCREENWIDTH + 10

    return [
        {'x': pipeX, 'y': gapY - PIPE_HEIGHT},  # upper pipe
        {'x': pipeX, 'y': gapY + PIPEGAPSIZE},  # lower pipe
    ]


def showScore(score):
    """displays score in center of screen"""
    scoreDigits = [int(x) for x in list(str(score))]
    totalWidth = 0 # total width of all numbers to be printed

    for digit in scoreDigits:
        totalWidth += IMAGES['numbers'][digit].get_width()

    Xoffset = (SCREENWIDTH - totalWidth) / 2

    for digit in scoreDigits:
        SCREEN.blit(IMAGES['numbers'][digit], (Xoffset, SCREENHEIGHT * 0.1))
        Xoffset += IMAGES['numbers'][digit].get_width()


def checkCrash(player, upperPipes, lowerPipes):
    """returns True if player collders with base or pipes."""
    pi = player['index']
    player['w'] = IMAGES['player'][0].get_width()
    player['h'] = IMAGES['player'][0].get_height()

    # if player crashes into ground
    if player['y'] + player['h'] >= BASEY - 1:
        return True
    else:

        playerRect = pygame.Rect(player['x'], player['y'],
                      player['w'], player['h'])

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            # upper and lower pipe rects
            uPipeRect = pygame.Rect(uPipe['x'], uPipe['y'], PIPE_WIDTH, PIPE_HEIGHT)
            lPipeRect = pygame.Rect(lPipe['x'], lPipe['y'], PIPE_WIDTH, PIPE_HEIGHT)

            # player and upper/lower pipe hitmasks
            pHitMask = HITMASKS['player'][pi]
            uHitmask = HITMASKS['pipe'][0]
            lHitmask = HITMASKS['pipe'][1]

            # if bird collided with upipe or lpipe
            uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask)
            lCollide = pixelCollision(playerRect, lPipeRect, pHitMask, lHitmask)

            if uCollide or lCollide:
                return True

    return False

def pixelCollision(rect1, rect2, hitmask1, hitmask2):
    """Checks if two objects collide and not just their rects"""
    rect = rect1.clip(rect2)

    if rect.width == 0 or rect.height == 0:
        return False

    x1, y1 = rect.x - rect1.x, rect.y - rect1.y
    x2, y2 = rect.x - rect2.x, rect.y - rect2.y

    for x in range(rect.width):
        for y in range(rect.height):
            if hitmask1[x1+x][y1+y] and hitmask2[x2+x][y2+y]:
                return True
    return False

flappy_bird_utils.py

import pygame
import sys
def load():
    # path of player with different states
    PLAYER_PATH = (
            'assets/sprites/redbird-upflap.png',
            'assets/sprites/redbird-midflap.png',
            'assets/sprites/redbird-downflap.png'
    )

    # path of background
    BACKGROUND_PATH = 'assets/sprites/background-black.png'

    # path of pipe
    PIPE_PATH = 'assets/sprites/pipe-green.png'

    IMAGES, SOUNDS, HITMASKS = {}, {}, {}

    # numbers sprites for score display
    IMAGES['numbers'] = (
        pygame.image.load('assets/sprites/0.png').convert_alpha(),
        pygame.image.load('assets/sprites/1.png').convert_alpha(),
        pygame.image.load('assets/sprites/2.png').convert_alpha(),
        pygame.image.load('assets/sprites/3.png').convert_alpha(),
        pygame.image.load('assets/sprites/4.png').convert_alpha(),
        pygame.image.load('assets/sprites/5.png').convert_alpha(),
        pygame.image.load('assets/sprites/6.png').convert_alpha(),
        pygame.image.load('assets/sprites/7.png').convert_alpha(),
        pygame.image.load('assets/sprites/8.png').convert_alpha(),
        pygame.image.load('assets/sprites/9.png').convert_alpha()
    )

    # base (ground) sprite
    IMAGES['base'] = pygame.image.load('assets/sprites/base.png').convert_alpha()

    # sounds
    if 'win' in sys.platform:
        soundExt = '.wav'
    else:
        soundExt = '.ogg'

    SOUNDS['die']    = pygame.mixer.Sound('assets/audio/die' + soundExt)
    SOUNDS['hit']    = pygame.mixer.Sound('assets/audio/hit' + soundExt)
    SOUNDS['point']  = pygame.mixer.Sound('assets/audio/point' + soundExt)
    SOUNDS['swoosh'] = pygame.mixer.Sound('assets/audio/swoosh' + soundExt)
    SOUNDS['wing']   = pygame.mixer.Sound('assets/audio/wing' + soundExt)

    # select random background sprites
    IMAGES['background'] = pygame.image.load(BACKGROUND_PATH).convert()

    # select random player sprites
    IMAGES['player'] = (
        pygame.image.load(PLAYER_PATH[0]).convert_alpha(),
        pygame.image.load(PLAYER_PATH[1]).convert_alpha(),
        pygame.image.load(PLAYER_PATH[2]).convert_alpha(),
    )

    # select random pipe sprites
    IMAGES['pipe'] = (
        pygame.transform.rotate(
            pygame.image.load(PIPE_PATH).convert_alpha(), 180),
        pygame.image.load(PIPE_PATH).convert_alpha(),
    )

    # hismask for pipes
    HITMASKS['pipe'] = (
        getHitmask(IMAGES['pipe'][0]),
        getHitmask(IMAGES['pipe'][1]),
    )

    # hitmask for player
    HITMASKS['player'] = (
        getHitmask(IMAGES['player'][0]),
        getHitmask(IMAGES['player'][1]),
        getHitmask(IMAGES['player'][2]),
    )

    return IMAGES, SOUNDS, HITMASKS

def getHitmask(image):
    """returns a hitmask using an image's alpha."""
    mask = []
    for x in range(image.get_width()):
        mask.append([])
        for y in range(image.get_height()):
            mask[x].append(bool(image.get_at((x,y))[3]))
    return mask

assets文件包
链接:https://pan.baidu.com/s/1ctJF5_X6MCOBWX96WkaO7w
提取码:jg08

6.2.2 代码解析

line 65

self.sess = tf.Session()
  • Session 是 Tensorflow 为了控制,和输出文件的执行的语句. 运行 session.run() 可以获得你要得知的运算结果, 或者是你所要运算的部分.

line 68

 self.obs = tf.placeholder(tf.float32,shape = [None,80,80,4])
  • 函数形式:
tf.placeholder(
    dtype,
    shape=None,
    name=None
)
  • 参数:
    dtype:数据类型。常用的是tf.float32,tf.float64等数值类型
    shape:数据形状。默认是None,就是一维值,也可以是多维(比如[2,3], [None, 3]表示列是3,行不定)
    name:名称
  • 为什么要用placeholder?
    Tensorflow的设计理念称之为计算流图,在编写程序时,首先构筑整个系统的graph,代码并不会直接生效,这一点和python的其他数值计算库(如Numpy等)不同,graph为静态的,类似于docker中的镜像。然后,在实际的运行时,启动一个session,程序才会真正的运行。这样做的好处就是:避免反复地切换底层程序实际运行的上下文,tensorflow帮你优化整个系统的代码。我们知道,很多python程序的底层为C语言或者其他语言,执行一行脚本,就要切换一次,是有成本的,tensorflow通过计算流图的方式,帮你优化整个session需要执行的代码,还是很有优势的。
    所以placeholder()函数是在神经网络构建graph的时候在模型中的占位,此时并没有把要输入的数据传入模型,它只会分配必要的内存。等建立session,在会话中,运行模型的时候通过feed_dict()函数向占位符喂入数据。

line 73

self.Q = self.build_q_net(self.obs,scope='eval',trainable=True)
self.Q_ = self.build_q_net(self.obs_,scope='target',trainable=False)
  • trainable如果为True,则会默认将变量添加到图形集合GraphKeys.TRAINABLE_VARIABLES中。此集合用于优化器Optimizer类优化的的默认变量列表,如果为False则在训练时不会更新该值。注意是在优化器有用,正常的赋值操作还是会让其改变。

line 77

self.qe_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope='eval')# 获取训练变量

等价于

self.qe_params=[var for var in train_vars if var.name.startswith('eval')]

line 84

readout_q = tf.reduce_sum(tf.multiply(self.Q,self.action),reduction_indices=1)
  • 原型
tf.reduce_sum(
    input_tensor, 
    axis=None, 
    keepdims=None,
    name=None,
    reduction_indices=None, 
    keep_dims=None)
  • input_tensor:待求和的tensor;
  • tf.reduce_sum函数中reduction_indices参数表示函数的处理维度。
    • reduction_indices参数的值默认的时候为None,默认把所有的数据求和,即结果是一维的。
    • reduction_indices参数的值为0的时候,是第0维对应位置相加。
    • reduction_indices参数的值为1的时候,是第1维对应位置相加。
    • reduction_indices只有tensorflow1.0+才有,已弃用。

line 85

 self.q_loss = tf.losses.mean_squared_error(labels=self.Q_target,predictions = readout_q)
  • tf.losses.mean_squared_error函数
tf.losses.mean_squared_error(
    labels,
    predictions,
    weights=1.0,
    scope=None,
    loss_collection=tf.GraphKeys.LOSSES,
    reduction=Reduction.SUM_BY_NONZERO_WEIGHTS
)
  • 在训练过程中增加了平方和loss.
    在这个函数中,weights作为loss的系数.如果提供了标量,那么loss只是按给定值缩放.如果weights是一个大小为[batch_size]的张量,那么批次的每个样本的总损失由weights向量中的相应元素重新调整.如果weights的形状与predictions的形状相匹配,则predictions中每个可测量元素的loss由相应的weights值缩放.
  • 参数:
    labels:真实的输出张量,与“predictions”相同.
    predictions:预测的输出.
    weights:可选的Tensor,其秩为0或与labels具有相同的秩,并且必须可广播到labels(即,所有维度必须为1与相应的losses具有相同的维度).
    scope:计算loss时执行的操作范围.
    loss_collection:将添加loss的集合.
    reduction:适用于loss的减少类型.
  • 返回:
    加权损失浮动Tensor.如果reduction是NONE,则它的形状与labels相同;否则,它是标量.
  • 可能引发的异常:
    ValueError:如果predictions与labels的形状不匹配,或者形状weights是无效,亦或,如果labels或是predictions为None,则会引发此类异常.
  • 转载:https://www.cnblogs.com/xiaoniu-666/p/11102805.html

line 87

self.q_train_op = tf.train.AdamOptimizer(lr).minimize(self.q_loss,var_list=self.qe_params)
  • tf.train.AdamOptimizer()函数是Adam优化算法:是一个寻找全局最优点的优化算法,引入了二次方梯度校正。
tf.train.AdamOptimizer.__init__(
	learning_rate=lr, 
	beta1=0.9, 
	beta2=0.999, 
	epsilon=1e-08, 
	use_locking=False, 
	name='Adam'
)
  • 参数:
    learning_rate:张量或浮点值。学习速率
    beta1:一个浮点值或一个常量浮点张量。一阶矩估计的指数衰减率
    beta2:一个浮点值或一个常量浮点张量。二阶矩估计的指数衰减率
    epsilon:数值稳定性的一个小常数
    use_locking:如果True,要使用lock进行更新操作
    name:应用梯度时为了创建操作的可选名称。默认为“Adam”
  • 本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
  • 实际上运行tf.train.AdamOptimizer(),除了利用反向传播算法对权重和偏置项进行修正外,也在运行中不断修正学习率。根据其损失量学习自适应,损失量大则学习率大,进行修正的角度越大,损失量小,修正的幅度也小,学习率就小,但是不会超过自己所设定的学习率。
  • 转载:https://blog.csdn.net/qq_39852676/article/details/98477214

line 89

self.sess.run(tf.global_variables_initializer())
  • 当我们训练自己的神经网络的时候,无一例外的就是都会加上一句 sess.run(tf.global_variables_initializer()),用来初始化变量。
  • 必须要使用global_variables_initializer的场合
    • 含有tf.Variable的环境下,因为tf中建立的变量是没有初始化的,也就是在debug时还不是一个tensor量,而是一个Variable变量类型
  • 可以不适用初始化的场合
    • 不含有tf.Variable、tf.get_Variable的环境下
    • 比如只有tf.random_normal或tf.constant等

line 91

self.saver = tf.train.Saver()
  • 我们经常在训练完一个模型之后希望保存训练的结果,这些结果指的是模型的参数,以便下次迭代的训练或者用作测试。Tensorflow针对这一需求提供了Saver类。
  • Saver类提供了向checkpoints文件保存和从checkpoints文件中恢复变量的相关方法。Checkpoints文件是一个二进制文件,它把变量名映射到对应的tensor值。
  • 只要提供一个计数器,当计数器触发时,Saver类可以自动的生成checkpoint文件。这让我们可以在训练过程中保存多个中间结果。例如,我们可以保存每一步训练的结果。
    为了避免填满整个磁盘,Saver可以自动的管理Checkpoints文件。例如,我们可以指定保存最近的N个Checkpoints文件。
  • 写的位置
    • with tf.Session() as sess:这一句的前面写就行,不用再Session()会话中
    • 在Session()会话的内部结尾中再加这么一句,指明地址
saver.save(sess,check_dir + 'model.ckpt') 

line 97

self.saver.save(self.sess,model_path,global_step=global_step)
  • Saver类
    • Saver类是用于保存和恢复变量的。它有将变量保存到checkpoint和从checkpoint中恢复变量的操作。
    • Checkpoints是一个二进制文件,它的属性值和tensor变量值一一对应。最好的检查checkpoints内容的方法就是用一个Saver去加载它。
    • Saver可以自动的为chackpoint文件进行计数。这可以让你在训练模型时,保存多个checkpoint(通过计数来区分)。例如你可以通过训练的epoch来标识你的checkpoint文件。为了防止过分使用内存,你可以为saver设置最多保存的checkpoint文件数量。
    • 你可以通过为save()函数传入global_step参数值来标识checkpoint文件
  • save
save(
    sess,
    save_path,
    global_step=None,
    latest_filename=None,
    meta_graph_suffix='meta',
    write_meta_graph=True,
    write_state=True,
    strip_default_attrs=False)
  • 保存变量
    这个方法用来保存变量,它需要一个session参数来指明哪个图。保存的参数必须已经被初始化过了。
  • 参数
    sess:保存变量需要的session
    save_path:checkpoint文件保存的路径。
    global_step:如果指定了,则会将这个数字添加到save_path后面,用于唯一标识checkpoint文件。
    latest_filename:和save_path在同一个文件夹中,用于最后一个checkpoint文件的命名。默认为checkpoint。
    其他不常用。

line 101

self.saver.restore(self.sess,model_path)
  • restore
restore(
    sess,
    save_path)
  • 从save_path中恢复模型的参数。
    它需要一个session,需要恢复的参数不需要初始化,因为恢复本身就是一种初始化变量的方法。而参数save_path就是save()函数产生的文件的路径名。
  • 参数
    sess:一个session
    save_path:保存的路径

line 113

with tf.variable_scope(scope):{}

原型:

def variable_scope(name_or_scope,
                   default_name=None,
                   values=None,
                   initializer=None,
                   regularizer=None,
                   caching_device=None,
                   partitioner=None,
                   custom_getter=None,
                   reuse=None,
                   dtype=None,
                   use_resource=None)
  • 作用:这个函数返回上下文管理器,用于定义创建变量(或层)的操作。
  • 此上下文管理器验证(可选)values是否来自同一个图,确保该图是默认图,并推送名称作用域和变量作用域。
  • 如果name_or_scope不是None,则按原样使用。如果scope为None,则使用default_name。在这种情况下,如果先前在同一作用域内使用了相同的名称,那么它将被追加_N以保证唯一性。
  • 变量作用域允许创建新变量并共享已创建的变量,同时提供检查以避免意外创建或共享。
  • 参数
    name_or_scope:string or VariableScope: 待打开的作用域.

line 115

 h_conv1 = tf.layers.conv2d(inputs=obs,
                            filters=32,
                            kernel_size=[8*8],
                            strides=4,
                            padding="same",
                            activation=tf.nn.relu,
                            kernel_initializer=tf.random_normal_initializer(mean=0,stddev=0.1),
                            bias_initializer=tf.constant_initializer(0.1),
                            trainable=trainable)

原型

conv2d(inputs, filters, kernel_size, 
    strides=(1, 1), 
    padding='valid', 
    data_format='channels_last', 
    dilation_rate=(1, 1),
    activation=None, 
    use_bias=True, 
    kernel_initializer=None,
    bias_initializer=<tensorflow.python.ops.init_ops.Zeros object at 0x000002596A1FD898>, 
    kernel_regularizer=None,
    bias_regularizer=None, 
    activity_regularizer=None, 
    kernel_constraint=None, 
    bias_constraint=None, 
    trainable=True, 
    name=None,
    reuse=None)
  • 作用
    2D 卷积层的函数接口
    这个层创建了一个卷积核,将输入进行卷积来输出一个 tensor。如果 use_bias 是 True(且提供了 bias_initializer),则一个偏差向量会被加到输出中。最后,如果 activation 不是 None,激活函数也会被应用到输出中。
  • 参数
    • inputs:Tensor 输入
    • filters:整数,表示输出空间的维数(即卷积过滤器的数量)
    • kernel_size:一个整数,或者包含了两个整数的元组/队列,表示卷积核的高和宽。如果是一个整数,则宽高相等。
    • strides:一个整数,或者包含了两个整数的元组/队列,表示卷积的纵向和横向的步长。如果是一个整数,则横纵步长相等。另外, strides 不等于1 和 dilation_rate 不等于1 这两种情况不能同时存在。
    • padding:“valid” 或者 “same”(不区分大小写)。“valid” 表示不够卷积核大小的块就丢弃,"same"表示不够卷积核大小的块就补0。 “valid” 的输出形状为
    • data_format:channels_last 或者 channels_first,表示输入维度的排序。
    • ilation_rate:一个整数,或者包含了两个整数的元组/队列,表示使用扩张卷积时的扩张率。如果是一个整数,则所有方向的扩张率相等。另外, strides 不等于1 和 dilation_rate 不等于1 这两种情况不能同时存在。
    • activation:激活函数。如果是None则为线性函数。
    • use_bias:Boolean类型,表示是否使用偏差向量。
    • kernel_initializer:卷积核的初始化。
    • bias_initializer:偏差向量的初始化。如果是None,则使用默认的初始值。
    • kernel_regularizer:卷积核的正则项
    • bias_regularizer:偏差向量的正则项
    • activity_regularizer:输出的正则函数
    • kernel_constraint:映射函数,当核被Optimizer更新后应用到核上。Optimizer 用来实现对权重矩阵的范数约束或者值约束。映射函数必须将未被影射的变量作为输入,且一定输出映射后的变量(有相同的大小)。做异步的分布式训练时,使用约束可能是不安全的。
    • bias_constraint:映射函数,当偏差向量被Optimizer更新后应用到偏差向量上。
    • trainable:Boolean类型。
    • name:字符串,层的名字。
    • reuse:Boolean类型,表示是否可以重复使用具有相同名字的前一层的权重。
  • 返回值
    输出 Tensor

其中

kernel_initializer=tf.random_normal_initializer(mean=0,stddev=0.1),
  • 用正态分布产生张量的初始化器.
  • 参数:
    mean:一个 python 标量或一个标量张量.要生成的随机值的均值.
    stddev:一个 python 标量或一个标量张量.要生成的随机值的标准偏差.
    seed:一个 Python 整数.用于创建随机种子.查看 tf.set_random_seed 行为.
    dtype:数据类型.只支持浮点类型.

line 120

 h_pool1 = tf.layers.max_pooling2d(h_conv1,pool_size=[2*2],strides=2,padding="same")
  • tf.layers.max_pooling2d函数
tf.layers.max_pooling2d(
    inputs,
    pool_size,
    strides,
    padding='valid',
    data_format='channels_last',
    name=None
)
  • 参数:
    inputs:池的张量,秩必须为4.
    pool_size:2个整数的整数或元组/列表:(pool_height,pool_width),用于指定池窗口的大小.可以是单个整数,以指定所有空间维度的相同值.
    strides:2个整数的整数或元组/列表,用于指定池操作的步幅.可以是单个整数,以指定所有空间维度的相同值.
    padding:一个字符串,表示填充方法,“valid”或“same”,不区分大小写.
    data_format:一个字符串,表示输入中维度的顺序.支持channels_last(默认)和channels_first;channels_last对应于具有形状(batch, height, width, channels)的输入,而channels_first对应于具有形状(batch, channels, height, width)的输入.
    name:字符串,图层的名称.
  • 返回:
    输出张量(Tensor).

line 127

h_conv3_flat = tf.reshape(h_conv3,[-1,1600])
  • 原型
tf.reshape(tensor,shape,name=None)
  • 将第三个卷积层的输出展开成维数为1600的1维向量
  • 函数的作用是将tensor变换为参数shape形式,其中的shape为一个列表形式,特殊的是列表可以实现逆序的遍历,即list(-1).-1所代表的含义是我们不用亲自去指定这一维的大小,函数会自动进行计算,但是列表中只能存在一个-1。(如果存在多个-1,就是一个存在多解的方程)

line 134

h_fc1 = tf.layers.dense(inputs=h_conv3_flat,
                                    units=512,
                                    activation=tf.nn.relu,
                                    kernel_initializer=tf.random_normal_initializer(0,stddev=0.1),
                                    bias_initializer=tf.constant_initializer(0.1),
                                    trainable=trainable)
  • dense :全连接层 相当于添加一个层
    • units:输出的维度大小,改变inputs的最后一维

line 160

 a_t[random.randrange(ACTIONS)] = 1
  • 原型
random.randrange(start, stop, step)
  • 注意:randrange()是不能直接访问的,需要导入 random 模块,然后通过 random 静态对象调用该方法。
  • 参数
    start – 指定范围内的开始值,包含在范围内。
    stop – 指定范围内的结束值,不包含在范围内。
    step – 指定递增基数。
  • 返回值
    从给定的范围返回随机项。

line 174

x_t = cv2.cvtColor(cv2.resize(x_t,(80,80)),
				   cv2.COLOR_BGR2GRAY)
  • opencv中颜色空间转换函数 cv2.cvtColor()
  • opencv中有多种色彩空间,包括 RGB、HSI、HSL、HSV、HSB、YCrCb、CIE XYZ、CIE Lab8种,使用中经常要遇到色彩空间的转化,以便生成mask图等操作。
  • 可以使用下面的色彩空间转化函数 cv2.cvtColor( )进行色彩空间的转换:
    HSV 表示hue、saturation、value
  • 原型
image_hsv = cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
  • 用这个函数把图像从RGB转到HSV夜色空间,注意是BGR2HSV,因为在opencv中默认的颜色空间是BGR。

其中

  • OpenCV: 图像缩放(cv2.resize)
  • 原型
cv2.resize(src,dsize,dst=None,fx=None,fy=None,interpolation=None)
  • 参数
    scr: 原图
    dsize: 输出图像尺寸
    fx: 沿水平轴的比例因子
    fy: 沿垂直轴的比例因子
    interpolation: 插值方法

line 175

ret,x_t = cv2.threshold(x_t,1,255,cv2.THRESH_BINARY)
  • 像素高于阈值时,给像素赋予新值。
  • 原型
cv2.threshold(src,thresh,maxval,type[,dst])
  • cv2.threshold(源图片,阀值,填充色,阀值类型)
  • 参数
    • src:源图片,必须是单通道
    • thresh:阀值,取值范围0~255
    • maxval:填充色,取值范围0~255
    • type:阀值类型,具体见下表
      • cv2.THRESH_BINARY(黑白二值)
        cv2.THRESH_BINARY_INV(黑白二值反转)
        cv2.THRESH_TRUNC(得到的图像为多像素值)
        cv2.THRESH_TOZERO
        cv2.THRESH_TOZERO_INV
        cv2.THRESH_MASK
        cv2.THRESH_OTSU
        cv2.THRESH_TRIANGLE
  • 该函数有两个返回值,第一个retVal(得到的阀值值),第二个就是阀值化后的图像。
  • 阀值类型表:
阀值小于阀值的像素点大于阀值的像素点
0置0置填充色
1置填充色置0
2保持原色置阀值
3置0保持原色
4保持原色置0

line 176

s_t = np.stack((x_t,x_t,x_t,x_t),axis=2)
  • numpy.stack()函数用于沿新轴连接相同尺寸数组的序列。axis参数指定结果轴尺寸中新轴的索引。例如,如果axis = 0,它将是第一个尺寸;如果axis = -1,它将是最后的尺寸。
  • 原型
numpy.stack(arrays, axis)
  • 参数:
    arrays :[数组]相同形状的数组序列。
    axis :[int]输入数组沿其堆叠的结果数组中的轴。
  • 返回值
    [stacked ndarray]输入数组的堆栈数组,其维数比输入数组大。

line 190

s_t1 =np.append(x_t1,s_t[:,:,:3],axis=2)
  • 原型
np.append(arr, values, axis=None)
  • 作用:
    为原始array添加一些values
  • 参数:
    arr:需要被添加values的数组
    values:添加到数组arr中的值(array_like,类数组)
    axis:可选参数,如果axis没有给出,那么arr,values都将先展平成一维数组。注:如果axis被指定了,那么arr和values需要有相同的shape,否则报错:ValueError: arrays must have same number of dimensions
  • 补充对axis的理解
    • axis的最大值为数组arr的维数-1,如arr维数等于1,axis最大值为0;arr维数等于2,axis最大值为1,以此类推。
    • 当arr的维数为2(理解为单通道图),axis=0表示沿着行方向添加values;axis=1表示沿着列方向添加values
    • 当arr的维数为3(理解为多通道图),axis=0,axis=1时同上;axis=2表示沿着深度方向添加values
  • 返回:
    添加了values的新数组

line 207

self.sess.run(self.q_train_op,feed_dict={self.obs:train_s,self.action:train_a,self.Q_target:target_q})
  • 原型
tmp = self.sess.run(self.out,
					feed_dict={self.input:[x[iself.img_size:(i+1)self.img_size,
							  jself.img_size:(j+1)self.img_size]]})[0]
  • feed_dict喂入网络,self.out 网络输出
  • [0]代表有批次,其实输入应该为1wh*c,虽然只有一张图片,[0]就代表第一张即第一批次。

在这里插入图片描述
但是还是有点不太聪明的亚子
需要再好好调整

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值