我们知道了使用q-learning可以解决延迟奖励问题,但是还是有一个问题,他的表格太大了
所以我们使用神经网络来代替q表格,就是DQN啦
我们先设定一堆超参数(实际上跑一遍代码就知道他们分别有什么作用了):
# 超参数
BATCH_SIZE = 8 #一次抽8个batch训练
LR = 0.005 # learning rate
EPSILON = 0.9 # 就是0.1的几率瞎选
GAMMA = 0.9 # 奖励递减参数
TARGET_REPLACE_ITER = 100 # Q 现实网络的更新频率
MEMORY_CAPACITY = 2000 # 记忆库大小
#我们的转珠模拟结果
def pazzuleGame(x,y):
return puzzle.add(x,y)
然后搭个网络用于预测:
class Net(nn.Module):
def __init__(self,):
super(Net, self).__init__()
self.fc1 = nn.Linear(N_STATES, N_STATES*4)
…………
self.out4 = nn.Linear(N_STATES*16, N_ACTIONS)
self.out4.weight.data.normal_(0, 0.1) # initialization
def forward(self, x):
x = self.fc1(x)
x = F.tanh(x)
…………
actions_value = self.out4(x)
return actions_value
然后用目标网络和现实网络组合起来搭建DQN网络
这个网络的作用就是存储经验,然后通过经验训练预测网络。
然后用预测网络获取的值,和现实网络获取的值做loss
然后用loss去反向更新网络(参考上一章的神经网络小列)
然后更新一段时间后,再把预测网络 拷给 现实网络
上面看不懂 我就举个例子:
1 1 ② →→→→→→→→→→ → 1 2 3
2 2 3 →我转了一步后变成了→ 2 2 ②
3 3 4 →→→→→→→→→→→ 3 3 4
现实网络在左边的版面,预测往下走 不会得分 所以 Q现实= 0
但是实际上往下走得了1combo
所以Q实际 = 1分 + 0
Q现实和Q实际的误差就有了,然后后面做的就是缩小这个误差
(0*不是真的0,是GAMMA乘以当前网络后续可以取得的最大值)
class DQN(object):
def __init__(self):
self.eval_net, self.target_net = Net().to(device), Net().to(device)
self.learn_step_counter = 0 # 用于 target 更新计时
self.memory_counter = 0 # 记忆库记数
self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2)) # 初始化记忆库
self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=LR) # torch 的优化器
self.loss_func = nn.MSELoss().to(device) # 误差公式
def choose_action(self, x,limit):
x = torch.unsqueeze(torch.FloatTensor(x).to(device), 0)
# 这里只输入一个 sample
if np.random.uniform() < EPSILON: # 选最优动作
actions_value = self.eval_net.forward(x)[0].data.numpy()
if(len(limit)>0):
for index,__ in enumerate(actions_value):
if(not index in limit):
actions_value[index] = -1e9
#选一个最大的动作
action = actions_value.argmax()
else: # 选随机动作
if(len(limit)>0):
action = np.random.choice(limit,1,False)[0]
else:
action = np.random.randint(0, N_ACTIONS)
return action
def store_transition(self, s, a, r, s_):
transition = np.hstack((s, [a, r], s_))
# 如果记忆库满了, 就覆盖老数据
index = self.memory_counter % MEMORY_CAPACITY
self.memory[index, :] = transition
self.memory_counter += 1
def learn(self):
# target net 参数更新
if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
self.learn_step_counter += 1
# 抽取记忆库中的批数据
sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)
b_memory = self.memory[sample_index, :]
b_s = torch.FloatTensor(b_memory[:, :N_STATES])
b_a = torch.LongTensor(b_memory[:, N_STATES:N_STATES+1].astype(int))
b_r = torch.FloatTensor(b_memory[:, N_STATES+1:N_STATES+2])
b_s_ = torch.FloatTensor(b_memory[:, -N_STATES:])
# 针对做过的动作b_a, 来选 q_eval 的值, (q_eval 原本有所有动作的值)
q_eval = self.eval_net(b_s).gather(1, b_a) # shape (batch, 1)
q_next = self.target_net(b_s_).detach() # q_next 不进行反向传递误差, 所以 detach
q_target = b_r + GAMMA * q_next.max(1)[0].view(BATCH_SIZE, 1) # shape (batch, 1)
loss = self.loss_func(q_eval, q_target)
# 计算, 更新 eval net
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss
(可选)搭建一个可视化环境
可以用pygame和pyglet,前者简单一些,这里用的pygame
就是训练的时候把参数穿进去能看到系统选择
class Env:
def __init__(self, row=6, col=5, fps=-1):
self.rowSize = row
self.colSize = col
# 刷新率 -1不显示
self.fps = fps
# 三消
self.thres = 3
#self.board = np.array(row, col)
# pygame
self.board = np.random.randint(1,6,(5,6))
pygame.init()
self.screen = pygame.display.set_mode((135*row, 135*col))
pygame.display.set_caption("PadAutomation")
background = pygame.image.load(r'E:\RLpaz\data\normal.png').convert()
red = pygame.image.load(r'E:\RLpaz\data\red.png').convert()
green = pygame.image.load(r'E:\RLpaz\data\green.png').convert()
yellow = pygame.image.load(r'E:\RLpaz\data\yellow.png').convert()
dark = pygame.image.load(r'E:\RLpaz\data\dark.png').convert()
blue = pygame.image.load(r'E:\RLpaz\data\blue.png').convert()
pink = pygame.image.load(r'E:\RLpaz\data\pink.png').convert()
self.switch = {1: red,
2: blue,
3: green,
4: yellow,
5: dark,
6: pink,
}
def step(self,pos):
self.screen.fill(0)
for i in range(5):
for j in range(6):
photo = self.switch[pos[i][j]]
self.screen.blit(photo, (i*135, j*135))
pygame.display.update()
def getscreen(self,):
return self.screen
def gameStart(self,fps=2):
if(fps<0):
return
fcclock = pygame.time.Clock()
while True:
if(fps!=0):
fcclock.tick(fps)
self.step(self.board)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
def setBoard(self,board):
self.board = board
网络搭好了,剩下就是弄一个模拟游戏环境。
这一部分和前面一样就不重复了
主要是要多设计一个算分逻辑
怎么设计都行,目的是为了增加奖惩,训练神经网络收敛
#算分
def score(self,curcombo=0):
tmp = deepcopy(self.board)
maxcombo = self.maxcomb
combo = self.calculate()
self.board = tmp
if(combo == curcombo):
return 0,combo
if(combo < curcombo):
return -pow(2,curcombo*2-combo),combo
#分数计算规则
reward = pow(2,combo)
if(combo ==maxcombo):
reward = pow(2,maxcombo+2)
return reward,combo
说白了就是把单维度的特征向量分离出来
比如珠子的颜色是红色,蓝色、 不能单纯的把它们标注成1、2
而是用两种维度,比如 是红色:1 是蓝色: 0 这样
虽然增加了观测数据维度,但是解决了数据属性问题
pytorch可以利用cuda加速tensor(张量)计算
神经网络内部传播都是基于张量,所以可以用.to(‘cuda’)的方式将张量置于gpu中
注:取出来一定要转换成.cpu() 不然会报错
如 :
#判断有没有GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#使用举例:
self.eval_net, self.target_net = Net().to(device), Net().to(device)
self.loss_func = nn.MSELoss().to(device)
#取出来
actions_value = self.eval_net.forward(x)[0].cpu().data.numpy()
因为
1 1
2 2
和
2 2
1 1
这样的输入实际上没有本质区别,可以通过手动转换把数据维度缩减
6个颜色的珠子就可以缩减C62倍的训练量
实现方法怎样都可以,在训练的时候转换成输入就行了
def boardTrans(self,s):
record = []
pos= []
for i in s:
if(not i in record):
record.append(i)
pos.append(np.where(s==i))
if(len(record)==self.colorSize):
break
index = 0
res = np.zeros(s.shape[0])
for i in pos:
index+=1
for j in i:
res[j]=index
return res
然后就是重复路径筛选,我们知道转的时候不会走回头路,这就是一个可以优化的点……之类的
但是转珠游戏的话DQN还是不太适合
因为输入的是整个环境,环境特征过多 难以收敛
如果只输入周围珠子,反而会陷入局部最优