赞
踩
这个游戏非常简单,名叫fizzbuzz,我猜测应该起源于早期的聚会活动,游戏规则如下:从1开始计数,当遇到是3的倍数的时候,就说fizz,当遇到是5的倍数的时候,就说buzz,当遇到是3和5的倍数(即15的倍数)时,就说fizzbuzz,其余数字保持正常计数;
关于用神经网络去玩这个游戏,需要明白其可行性,这里并不是像"打砖块"那样基于强化学习做出决策,这里只是单纯的拟合数据分布,所以,我会先根据游戏规则生成正确的数据,然后用NN去捕捉分布,再对陌生的数据进行分类,类似于模拟一个人在学习如何玩fizzbuzz;
这样的实现看起来有一些没有意义,但本质目的是通过实例复习第二课的内容
首先要生成正确的数据,对于输入的一个数字,需要判断其是3,5,15的倍数,所以定义一个函数作为数字的编码器:
def fizzbuzz_encode(num):
#注意优先级
#如果i%15放在后面,遇到i%3或者i%5就会先return退出,将永远不能反回1
if num%15==0:
return 0
elif num%3==0:
return 1
elif num%5==0:
return 2
else:
return 3
然后需要输出"fizzbuzz"这样的游戏结果,所以再增加一个解码器:
def fizzbuzz_decode(num,index):
return ["fizzbuzz","fizz","buzz",str(num)][index]
当编码器的返回值作为index,就可以实现模拟的效果:
#模拟一下,从1数到15
def play(num):
print(fizzbuzz_decode(num,fizzbuzz_encode(num)))
for i in range(1,16):
play(i)
目的是用简单的全连接神经网络去捕获数据分布,设想一下,如果直接让神经网络接收一个数字,那么它在数据输入那一层直接就是一个值连接到隐藏的神经元,这样的权重数量太少,难以学习到稍微复杂的分布;
所以需要对输入的数字进行适当的特征工程,可以将输入的数字转为固定位数的二进制形式(回顾python笔记本的附录:Python记事本),用数组或列表保存二进制的0,1值:
def binary_encode(num,num_digits):
#回顾 附录:Python记事本
return [num >> d & 1 for d in range(num_digits)][::-1]
比如将数字15转为10位的二进制列表:
基于前面的内容,现在已经具备了生成训练数据的条件,由于二进制列表共10位,则输入数字值可以从1到1023:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1]->[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
令从1到100用于测试,后面的部分数字用于训练,则可以得到trainx(借助binary_encode),trainy(借助fizzbuzz_encode),trainy的元素分4类[0,1,2,3]:
trainx=torch.tensor([binary_encode(i,NUM_DIGITS) for i in range(101,1024)],dtype=torch.float)#左闭右开
trainx.size()#torch.Size([923, 10])
trainy=torch.tensor([fizzbuzz_encode(i) for i in range(101,1024)],dtype=torch.long)
trainy.size()#torch.Size([923])
trainx为float型,这有利于变换不丢失精度,由于是分类问题,trainy先设为long型
网络沿用第二课的两层神经网络,在输出层增加softmax计算概率:
#定义网络
H=100
model=torch.nn.Sequential(
torch.nn.Linear(NUM_DIGITS,H,bias=False),
torch.nn.ReLU(),
torch.nn.Linear(H,4,bias=False)
)
if torch.cuda.is_available():
model=model.cuda()
目前看到没有写softmax层,因为损失函数会使用torch.nn.CrossEntropyLoss()
;
在计算损失时,会写作:
loss_fn=torch.nn.CrossEntropyLoss()
...
loss=loss_fn(y_pred,target)
注意到:
y_pred.shape:(batch_size,class_number);
target.shape:(batch_size);
所以target的每个元素需要是整数(一般是int64),取值范围为[0,class_number-1],这才能被函数认为是真实类别;
注意一个细节:当使用了torch.nn.CrossEntropyLoss的时候,其实不需要在model最后一层添加torch.nn.Softmax(),因为在官方文档中,pytorch使用CrossEntropyLoss计算交叉熵的公式为:
这也正好解释了前面target值在[0,class_number-1]的奇怪规则
损失函数使用torch.nn.CrossEntropyLoss,优化方法选择SGD:
#损失函数MSELoss改成交叉熵
loss_fn=torch.nn.CrossEntropyLoss()
#优化方法
optimizer=torch.optim.SGD(model.parameters(),lr=0.1)
更新权重不再像第二课中的一个epoch更新一次,而是在每个epoch中,按batch更新,因此,我将从trainx,trainy中取出每个batch需要的数据:
BATCH_SIZE=128
for epoch in range(2000):
#每个batch更新一次模型
for batch_start in range(0,len(trainx),BATCH_SIZE):
batch_end=batch_start+BATCH_SIZE
batchx=trainx[batch_start:batch_end]
batchy=trainy[batch_start:batch_end]
if torch.cuda.is_available():
batchx=batchx.cuda()
batchy=batchy.cuda()
反向传播计算梯度,并更新权重:
#前向传播 y_pred=model.forward(batchx) # y_pred.shape torch.Size([128, 4]) # batchy.shape torch.Size([128]) loss=loss_fn(y_pred,batchy) print(epoch,batch_start,loss.item()) #反向传播计算梯度 loss.backward() #使用优化方法对权重更新 optimizer.step() #梯度清零 model.zero_grad()
如果把loss记录下来,可以对其训练过程可视化:
先生成1到100的二进制编码列表:
testx=torch.tensor([binary_encode(i,NUM_DIGITS) for i in range(1,101)],dtype=torch.float)
计算获得分类结果:
#torch.no_grad()下的语句禁用梯度计算,节省了显存
#回顾 附录:pytorch记事本
with torch.no_grad():
pred=model.forward(testx)
#pred [N,4] N为样本数量
对于网络输出,先索引概率最大值,取出概率最大值所在位置作为获取分类的类别:
#回顾附录:pytorch记事本
pred.max(dim=1)[1]
解码打印,便于从人类角度检验结果:
encode=zip(range(1,101),list(pred.max(dim=1)[1]))
NN_result=[]
#对NN的结果解码
for elem in list(encode):
NN_result.append(fizzbuzz_decode(elem[0],elem[1].item()))
print(NN_result)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。