当前位置:   article > 正文

第三课.使用简单的NN模拟fizzbuzz_nn去模拟

nn去模拟

游戏介绍

这个游戏非常简单,名叫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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后需要输出"fizzbuzz"这样的游戏结果,所以再增加一个解码器:

def fizzbuzz_decode(num,index):
    return ["fizzbuzz","fizz","buzz",str(num)][index]
  • 1
  • 2

当编码器的返回值作为index,就可以实现模拟的效果:

#模拟一下,从1数到15
def play(num):
    print(fizzbuzz_decode(num,fizzbuzz_encode(num)))
for i in range(1,16):
    play(i)
  • 1
  • 2
  • 3
  • 4
  • 5

fig1

特征工程

目的是用简单的全连接神经网络去捕获数据分布,设想一下,如果直接让神经网络接收一个数字,那么它在数据输入那一层直接就是一个值连接到隐藏的神经元,这样的权重数量太少,难以学习到稍微复杂的分布;
所以需要对输入的数字进行适当的特征工程,可以将输入的数字转为固定位数的二进制形式(回顾python笔记本的附录:Python记事本),用数组或列表保存二进制的0,1值:

def binary_encode(num,num_digits):
    #回顾 附录:Python记事本
    return [num >> d & 1 for d in range(num_digits)][::-1]
  • 1
  • 2
  • 3

比如将数字15转为10位的二进制列表:
fig2

生成训练数据

基于前面的内容,现在已经具备了生成训练数据的条件,由于二进制列表共10位,则输入数字值可以从1到1023:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 1]->[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])
  • 1
  • 2
  • 3
  • 4
  • 5

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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

目前看到没有写softmax层,因为损失函数会使用torch.nn.CrossEntropyLoss()
在计算损失时,会写作:

loss_fn=torch.nn.CrossEntropyLoss()
...
loss=loss_fn(y_pred,target)
  • 1
  • 2
  • 3

注意到:
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计算交叉熵的公式为:
fig3
这也正好解释了前面target值在[0,class_number-1]的奇怪规则


训练

损失函数使用torch.nn.CrossEntropyLoss,优化方法选择SGD:

#损失函数MSELoss改成交叉熵
loss_fn=torch.nn.CrossEntropyLoss()
#优化方法
optimizer=torch.optim.SGD(model.parameters(),lr=0.1)
  • 1
  • 2
  • 3
  • 4

更新权重不再像第二课中的一个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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

反向传播计算梯度,并更新权重:

        #前向传播
        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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如果把loss记录下来,可以对其训练过程可视化:
fig4

使用NN模拟fizzbuzz

先生成1到100的二进制编码列表:

testx=torch.tensor([binary_encode(i,NUM_DIGITS) for i in range(1,101)],dtype=torch.float)
  • 1

计算获得分类结果:

#torch.no_grad()下的语句禁用梯度计算,节省了显存
#回顾 附录:pytorch记事本
with torch.no_grad():
    pred=model.forward(testx)
#pred [N,4] N为样本数量
  • 1
  • 2
  • 3
  • 4
  • 5

对于网络输出,先索引概率最大值,取出概率最大值所在位置作为获取分类的类别:

#回顾附录:pytorch记事本
pred.max(dim=1)[1]
  • 1
  • 2

解码打印,便于从人类角度检验结果:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

fig5

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/162833
推荐阅读
相关标签
  

闽ICP备14008679号