【深入浅出强化学习-编程实战】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
- cv2.THRESH_BINARY(黑白二值)
- 该函数有两个返回值,第一个retVal(得到的阀值值),第二个就是阀值化后的图像。
- 阀值类型表:
阀值 | 小于阀值的像素点 | 大于阀值的像素点 |
---|---|---|
0 | 置0 | 置填充色 |
1 | 置填充色 | 置0 |
2 | 保持原色 | 置阀值 |
3 | 置0 | 保持原色 |
4 | 保持原色 | 置0 |
- THRESH_OTSU:使用Otsu算法选择阀值
- THRESH_TRIANGLE:使用三角形算法选择最佳阀值
- 参考:https://docs.opencv.org/3.4.3/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f
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]就代表第一张即第一批次。
但是还是有点不太聪明的亚子
需要再好好调整