我正在尝试实现Gerald Tesauro在TD-Gammon文章中提出的算法。学习算法的核心在下面的段落中描述:
我决定使用单一隐藏层(如果这在1990年代初期就足以玩世界级的西洋双陆棋,那么对我来说也足够了)。我非常确定除了train()
函数之外的所有部分都是正确的(它们更容易测试),但我不知道我是否正确实现了这个最终算法。
import numpy as npclass TD_network: """ 具有单一隐藏层的神经网络,并采用G. Tesauro 1995年TD-Gammon文章中的时间差分训练算法 """ def __init__(self, num_input, num_hidden, num_output, hnorm, dhnorm, onorm, donorm): self.w21 = 2*np.random.rand(num_hidden, num_input) - 1 self.w32 = 2*np.random.rand(num_output, num_hidden) - 1 self.b2 = 2*np.random.rand(num_hidden) - 1 self.b3 = 2*np.random.rand(num_output) - 1 self.hnorm = hnorm self.dhnorm = dhnorm self.onorm = onorm self.donorm = donorm def value(self, input): """评估神经网络输出""" assert(input.shape == self.w21[1,:].shape) h = self.w21.dot(input) + self.b2 hn = self.hnorm(h) o = self.w32.dot(hn) + self.b3 return(self.onorm(o)) def gradient(self, input): """ 计算神经网络在给定输入处的梯度。输出一个字典列表, 每个字典对应一个输出节点的梯度,每个字典中的元素给出权重子集的梯度。 """ assert(input.shape == self.w21[1,:].shape) J = [] h = self.w21.dot(input) + self.b2 hn = self.hnorm(h) o = self.w32.dot(hn) + self.b3 for i in range(len(self.b3)): db3 = np.zeros(self.b3.shape) db3[i] = self.donorm(o[i]) dw32 = np.zeros(self.w32.shape) dw32[i, :] = self.donorm(o[i])*hn db2 = np.multiply(self.dhnorm(h), self.w32[i,:])*self.donorm(o[i]) dw21 = np.transpose(np.outer(input, db2)) J.append(dict(db3 = db3, dw32 = dw32, db2 = db2, dw21 = dw21)) return(J) def train(self, input_states, end_result, a = 0.1, l = 0.7): """ 使用代表从开始到结束的一系列输入状态训练网络,并为最终状态提供最终(监督/期望)输出 """ outputs = [self(input_state) for input_state in input_states] outputs.append(end_result) for t in range(len(input_states)): delta = dict( db3 = np.zeros(self.b3.shape), dw32 = np.zeros(self.w32.shape), db2 = np.zeros(self.b2.shape), dw21 = np.zeros(self.w21.shape)) grad = self.gradient(input_states[t]) for i in range(len(self.b3)): for key in delta.keys(): td_sum = sum([l**(t-k)*grad[i][key] for k in range(t + 1)]) delta[key] += a*(outputs[t + 1][i] - outputs[t][i])*td_sum self.w21 += delta["dw21"] self.w32 += delta["dw32"] self.b2 += delta["db2"] self.b3 += delta["db3"]
我的使用方式是,我会玩完整的一局游戏(或者更确切地说,神经网络与自己对战),然后我将从开始到结束的游戏状态发送到train()
函数中,并附上最终结果。然后它会使用这个游戏日志,并应用上述公式来调整权重,首先使用第一个游戏状态,然后是第一个和第二个游戏状态,依此类推,直到最后一次使用整个游戏状态列表。然后我会重复多次,希望网络能够学习。
需要明确的是,我并不是在寻求对我的代码编写的反馈。这原本只是一个快速而粗糙的实现,以确保我把所有零件都放在了正确的位置。
然而,我不知道它是否正确,因为到目前为止,我无法让它在任何合理的水平上玩井字游戏。可能有很多原因。也许我没有给它足够的隐藏节点(我使用了10到12个)。也许它需要更多的游戏来训练(我使用了200,000局)。也许它需要不同的归一化函数(我尝试了sigmoid和ReLU,有漏和无漏的不同变体)。也许学习参数没有调整好。也许井字游戏及其确定性玩法意味着它会“锁定”在游戏树的某些路径上。或者也许训练实现本身就是错误的。这就是我在这里的原因。
我是否误解了Tesauro的算法?
回答:
我不能说我完全理解你的实现,但这一行引起了我的注意:
td_sum = sum([l**(t-k)*grad[i][key] for k in range(t + 1)])
与你引用的公式相比:
我至少看到了两个不同之处:
- 你的实现对
t+1
个元素求和,而公式中是对t
个元素求和 - 梯度应该使用与
l**(t-k)
中使用的k
相同的索引,但在你的实现中,它使用了i
和key
进行索引,没有任何对k
的引用
或许如果你修正这些差异,你的解决方案会表现得更符合预期。