双网络DQN实现Blackjack RL AI遇TypeError及算法问题求助
首先,先解决你遇到的TypeError: Using a tf.Tensor as a Python bool is not allowed错误:
错误原因与修复
这个错误出现在你更新目标网络的代码行:
sess.run(update1,update2,update3)
TensorFlow的sess.run()函数第一个参数可以是单个Tensor/操作,或者多个操作组成的列表,而你这里把三个assign操作分开传递,导致第二个参数update2被当成了feed_dict参数(要求是字典类型),内部代码尝试判断这个Tensor是否为非空时,就触发了“不能把Tensor当作Python布尔值”的错误。
修复方法:把三个更新操作放在列表里执行,同时调整更新时机(当前在每个step里判断更新,会导致目标网络被频繁更新,应该放在每个episode结束后):
# 把这段代码从step循环里移到episode循环的末尾 if i % 10 == 0: sess.run([update1, update2, update3])
DQN算法的潜在问题与优化建议
你的当前代码实现了双网络DQN的雏形,但缺少一些核心机制,导致训练不稳定、成功率可能达不到预期,以下是关键改进点:
1. 加入经验回放(Experience Replay)
DQN的核心特性之一就是经验回放,它能打破样本的时间相关性,提升训练稳定性。你当前是在线学习(每一步直接用当前样本训练),很容易导致模型震荡。
实现思路:
- 创建一个经验池,存储每个step的
(状态, 动作, 奖励, 下一状态, 是否结束) - 每次训练时,从经验池中随机采样一批样本(比如32个)来更新网络,而不是用当前单一样本
2. 优化状态编码
你当前的状态编码生成了600维的one-hot向量,但Blackjack的有效状态空间其实更小:
- 玩家手牌总和:12-21(共10种,因为总和<12时最优策略必为Hit,无需学习)
- 庄家明牌:1-10(共10种)
- 是否有可用A:0/1(共2种)
总共有10*10*2=200种有效状态,这样可以减少网络参数,提升训练效率。
修改one_hot函数:
def one_hot(x): # 限制玩家总和在12-21之间,小于12的统一按12处理,大于21的按21处理 player_sum = max(12, min(x[0], 21)) dealer_card = x[1] has_ace = int(x[2]) # 计算索引:(player_sum-12)*10*2 + (dealer_card-1)*2 + has_ace idx = (player_sum - 12) * 20 + (dealer_card - 1) * 2 + has_ace s = np.identity(200) return s[idx].reshape(1, 200)
3. 调整学习率与优化器
当前learning_rate=0.5过高,会导致训练震荡,难以收敛。建议改用Adam优化器(比SGD更稳定),学习率设为0.001左右:
# 替换原来的GradientDescentOptimizer train = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
4. 优化探索率(ε)衰减策略
当前的ε衰减是1./((i/100)+1),后期ε会趋近于0,完全停止探索,容易过拟合。建议改成带最低值的指数衰减:
# 在episode循环外初始化 e = 1.0 e_decay_rate = 0.995 e_min = 0.01 # 在每个episode开始时更新ε e = max(e_min, e * e_decay_rate)
5. 替换激活函数为ReLU
当前用tanh作为激活函数,容易出现梯度饱和问题,建议改用ReLU:
layer1 = tf.nn.relu(tf.matmul(X, W1)) layer2 = tf.nn.relu(tf.matmul(layer1, W2)) # 目标网络同理 layer3 = tf.nn.relu(tf.matmul(X1, W4)) layer4 = tf.nn.relu(tf.matmul(layer3, W5))
6. 调整目标网络更新频率
当前每10个episode更新一次目标网络是可行的,但更常见的是按step数更新(比如每1000个step更新一次),你可以根据训练情况调整。
修改后的完整示例代码
import gym import tensorflow as tf import matplotlib.pyplot as plt import numpy as np from collections import deque import random def one_hot(x): # 优化状态编码,只保留有效状态 player_sum = max(12, min(x[0], 21)) dealer_card = x[1] has_ace = int(x[2]) idx = (player_sum - 12) * 20 + (dealer_card - 1) * 2 + has_ace s = np.identity(200) return s[idx].reshape(1, 200) env = gym.make('Blackjack-v0') learning_rate = 0.001 state_number = 200 action_number = 2 # 主网络(预测网络) X = tf.placeholder(tf.float32, shape=[None, state_number], name='input_data') W1 = tf.Variable(tf.random_uniform([state_number, 128], 0, 0.01)) layer1 = tf.nn.relu(tf.matmul(X, W1)) W2 = tf.Variable(tf.random_uniform([128, 256], 0, 0.01)) layer2 = tf.nn.relu(tf.matmul(layer1, W2)) W3 = tf.Variable(tf.random_uniform([256, action_number], 0, 0.01)) Qpred = tf.matmul(layer2, W3) # 目标网络 X1 = tf.placeholder(shape=[None, state_number], dtype=tf.float32) W4 = tf.Variable(tf.random_uniform([state_number, 128], 0, 0.01)) layer3 = tf.nn.relu(tf.matmul(X1, W4)) W5 = tf.Variable(tf.random_uniform([128, 256], 0, 0.01)) layer4 = tf.nn.relu(tf.matmul(layer3, W5)) W6 = tf.Variable(tf.random_uniform([256, action_number], 0, 0.01)) target_Q = tf.matmul(layer4, W6) # 目标网络更新操作 update_target = [W4.assign(W1), W5.assign(W2), W6.assign(W3)] Y = tf.placeholder(shape=[None, action_number], dtype=tf.float32) loss = tf.reduce_mean(tf.square(Y - Qpred)) train = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss) num_episodes = 10000 dis = 0.99 rList = [] # 经验回放池 replay_buffer = deque(maxlen=2000) batch_size = 32 # 探索率参数 e = 1.0 e_decay_rate = 0.995 e_min = 0.01 init = tf.global_variables_initializer() with tf.Session() as sess: sess.run(init) # 初始化目标网络 sess.run(update_target) for i in range(num_episodes): s = env.reset() rALL = 0 done = False # 更新探索率 e = max(e_min, e * e_decay_rate) while not done: # 状态编码 s_encoded = one_hot(s) # 选择动作 Qs = sess.run(Qpred, feed_dict={X: s_encoded}) if np.random.rand(1) < e: a = env.action_space.sample() else: a = np.argmax(Qs) s1, reward, done, _ = env.step(a) s1_encoded = one_hot(s1) # 存储经验到回放池 replay_buffer.append((s_encoded, a, reward, s1_encoded, done)) # 当回放池足够大时,采样训练 if len(replay_buffer) > batch_size: minibatch = random.sample(replay_buffer, batch_size) states = np.vstack([x[0] for x in minibatch]) actions = [x[1] for x in minibatch] rewards = [x[2] for x in minibatch] next_states = np.vstack([x[3] for x in minibatch]) dones = [x[4] for x in minibatch] # 计算目标Q值 target_Q_vals = sess.run(target_Q, feed_dict={X1: next_states}) Y_vals = sess.run(Qpred, feed_dict={X: states}) for j in range(batch_size): if dones[j]: Y_vals[j, actions[j]] = rewards[j] else: Y_vals[j, actions[j]] = rewards[j] + dis * np.max(target_Q_vals[j]) # 训练主网络 sess.run(train, feed_dict={X: states, Y: Y_vals}) # 统计奖励 if reward == 1: rALL += 1 s = s1 # 每10个episode更新一次目标网络 if i % 10 == 0: sess.run(update_target) rList.append(rALL) # 每100个episode打印一次当前胜率 if i % 100 == 0: print(f"Episode {i}, Current Success Rate: {sum(rList[-100:])/100:.2f}") print(f'Final success rate: {sum(rList)/num_episodes:.2f}') print("Final Q-table values summary (sample):") # 可以打印一些关键状态的Q值示例 sample_state = (18, 5, True) sample_encoded = one_hot(sample_state) q_vals = sess.run(Qpred, feed_dict={X: sample_encoded}) print(f"State: {sample_state} -> Q(Hit): {q_vals[0][0]:.2f}, Q(Stand): {q_vals[0][1]:.2f}")
预期效果
加入经验回放和上述优化后,你的DQN模型应该能收敛到更高的胜率(通常Blackjack的最优策略胜率在42%-45%左右),比你之前的38%有明显提升。
内容的提问来源于stack exchange,提问作者seunghyung lee

