《深度学习入门》4.4.2神经网络的梯度代码分析(内含为什么不能把loss(x,t)方法当成numerical_gradient(f,x)的参数f传进去)

这段代码不长,实现的原理也不是很难理解。

但是它的实现的代码来来回回看了很久才懂,主要是python语法的查阅等等耗时很久。

感谢B站致敬大神up主在群里对我提出的问题做出的解答。

B站传送地址:https://space.bilibili.com/389455044?spm_id_from=333.788.b_765f7570696e666f.1

 

先来贴上正确的代码我对其的注释

gradient_simplenet.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    #https://www.jianshu.com/p/608263a1f0c6 如何通俗解释python中的init ?
    #https://www.jianshu.com/p/d75931b2586c python类定义中__init__()的作用
    #https://blog.csdn.net/geerniya/article/details/77487941 类中为什么要定义__init__()方法
    def __init__(self):
        self.W = np.random.randn(2,3)#利用高斯分布进行初始化生成W的矩阵

    def predict(self, x):
        #进行矩阵的计算
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)#调用的common/functions中的代码

        return loss

x = np.array([0.6, 0.9])#输入的图像数据
t = np.array([0, 0, 1])#正确标签

#初始化神经网络(就是权重之类的)(实例化simpleNet类)
net = simpleNet()
#这里定义的参数w是一个伪参数
f = lambda w: net.loss(x, t)
#这里的梯度就是损失函数f对权重net.W的偏微分
dW = numerical_gradient(f, net.W)#调用的common/gradient中的代码
#这里lambda等价于以下:
#def f(W):
#    return net.loss(x,t)
#dW = numerical_gradient(f,net.W)

print(dW)

执行结果如下

其中调用的别处的代码cross_entropy_error(y,t)以及numerical_gradient(f,net.W)也贴上来,这样有个较为完整的理解,它们分别在common/functions.pycommon/gradient.py文件中。为了便于观看,所以只截取相关部分了。

common/functions.py

# coding: utf-8
import numpy as np
#数值微分方法计算梯度
#个人对这段算法的理解:
#1.设置h值用来数值微分法的计算
#2.设置一个全为0的数组grad用来存放梯度,它和输入数据x的shape一样
#3.将x数组设置成可以修改并且可以进行多重索引(这样就可以用(a1,a2)的坐标形式定位哪一个x了)
#4.使用while循环遍历x数组,对每一个x进行计算数值微分,并将计算结果保存在梯度数组中
def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)#初始化数组为0用来存放梯度
    #默认情况下,nditer将视待迭代遍历的数组为只读对象(read-only)
    #为了在遍历数组的同时,实现对数组元素值得修改,必须指定op_flags=['readwrite']模式。
    #flags=['multi_index']表示对x进行多重索引
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        #把元素的索引(it.multi_index)赋值给idx
        #https://www.cnblogs.com/xianhan/p/10414770.html
        idx = it.multi_index
        #用来还原值
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 还原值
        #nditer.iternext()检查是否保留迭代,并在不返回结果的情况下执行单个内部迭代。
        #it.iternext()表示进入下一次迭代,如果不加这一句的话,输出的结果就一直都是(0, 0)。
        #迭代(Iteration)如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple
        #迭代操作就是对于一个集合,无论该集合是有序还是无序,我们用 for 循环总是可以依次取出集合的每一个元素。
        it.iternext()   
        
    return grad

common/gradient.py 

# coding: utf-8
import numpy as np

#p91
#y是神经网络的输出(就是一系列的概率数组)
#t是监督数据(one-hot就是0,1,0那种,非one-hot就是[0,1,3]这种)
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 监督(训练)数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

这边对这段求交叉熵的代码给一个说明:

if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

 这边做了一个小小的例子来帮助对于ndim与reshape的理解:

import numpy as np

y = np.array([1,2,3,4,5,6,7,8])

print(y.shape)#(8,)
print(y.size)#8
print(y.ndim)#1
print(y)#[1 2 3 4 5 6 7 8]

#这边就是可以想象成求交叉熵时对神经网络的输出y所做的处理
y = y.reshape(1, y.size)
print(y.ndim)#2
print(y)#[[1 2 3 4 5 6 7 8]]
print(y.argmax(axis=1))#[7]是索引
#可以发现经过reshape的变形才能通过y[np.arange(batch_size),t]来取y中对应的数据,否则会报错
print(y[0,3])#4

y = y.reshape(2,4)
print(y.ndim)#2
print(y)#[[1 2 3 4][5 6 7 8]]
print(y[1,3])#8


============================================================

    #这段代码的目的是:
    #在正确解标签为one-hot-vector的情况下
    #找出每批数据所对应的的正确解标签的索引
    #因为正确解标签只有0和1,那么找到的最大的那个数对应的索引就是1对应的索引
    if t.size == y.size:
        #这个t是按行搜索得到的最大的那个数的索引,不是最大的那个数
        t = t.argmax(axis=1)

 

对axis=0和axis=1的理解:

参考:https://www.cnblogs.com/rrttp/p/8028421.html

axis参数作用方向图示

另外,记住,Pandas保持了Numpy对关键字axis的用法,用法在Numpy库的词汇表当中有过解释:

轴用来为超过一维的数组定义的属性,二维数据拥有两个轴:第0轴沿着行的垂直往下,第1轴沿着列的方向水平延伸。

================================================================

    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

这边做了一个小小的例子来帮助对于y[np.arange(batch_size), t] 的理解,之前对这个很迷茫,按书上的说法是:

假设batch_size为5,np.arange(5)生成一个NumPy 数组[0, 1, 2, 3, 4],t中标签是以[2, 7, 0, 9, 4]的形式存储的。
y[np.arange(batch_size), t] 会生成NumPy 数组[y[0,2], y[1,7], y[2,0],y[3,9], y[4,4]])。

我对这个不理解,自己又编了段代码如下:

import numpy as np

#这段可以用来理解从4批数据中,取出第四批的第4个数据
#随机生成4批次,每批次8个数
yyy = np.random.rand(4,6) 
print(yyy)
#取每批数据的索引为1的数据并打印
print(yyy[np.arange(3),1])

 执行结果如下:

这样一来就明白了:

y[np.arange(batch_size), t]:

数组y(由每批次最后神经网络的输出的概率所组成的数组,假设它打印出来就是上图的样子,那么它就是四组数据);

t在之前已经被赋值为one-hot标签中1所对应的索引了;

所以y[np.arange(batch_size), t]就是正确解标签索引对应的神经网络输出数组相应位置的那个概率。

我也曾想过:

这边我觉得应该会有for循环或者什么呀来遍历一下不然怎么能取出每批的概率呢?但是只有算梯度的地方有个while循环,那这个np.sum(np.log(y[np.arange(batch_size), t] + 1e-7))是怎么实现把所有的求和的

因为“np.arange(5)生成一个NumPy 数组[0, 1, 2, 3, 4]”呀,这个倒是还蛮方便的,可以看我自己打的那段代码的最后一排,确实是能做到取出每批的想要的索引对应的概率的。

 

下面主要是讲为什么不能把loss(x,t)方法当成numerical_gradient(f,x)的参数f传进去

这是会导致出错的代码:

就是只是把gradient_simplenet.py中的最后几排:

#这里定义的参数w是一个伪参数
f = lambda w: net.loss(x, t)
#这里的梯度就是损失函数f对权重net.W的偏微分
dW = numerical_gradient(f, net.W)#调用的common/gradient中的代码
#这里lambda等价于以下:
#def f(W):
#    return net.loss(x,t)
#dW = numerical_gradient(f,net.W)

改成了:

dW = numerical_gradient(loss(x, t), net.W)

 其中我直接把loss(x,t)方法当成numerical_gradient(f,x)的参数f传进去了。

下面上报错显示:


 

我真是搞不懂2333,本来就对lambda不熟悉,认为怎么能传进w,输出是net.loss(x,t)呢?loss方法完全不需要传w这个参数进去啊,也没有啥用啊。

书上对这个代码有一段解释是这样的:

def f(W):
    return net.loss(x,t)
dW = numerical_gradient(f,net.W)

 这里定义的函数f(W)的参数W是一个伪参数。因为numerical——gradient(f,x)会在内部执行f(x),为了与之兼容而定义了f(W) 。

当然我看不懂了。。。

问了up主,她是这么和我解释的,然后又稍稍理解了一下:

如果不用lambda,直接调用loss函数,把loss函数当参数传进numerical_gradient()函数中,交叉熵的值就会直接算出来,那么这时候loss()函数就无法作为一个(可求偏导的)变量来计算梯度。可以检测到f的类型是64位浮点数(这也就是报错中显示的TypeError: 'numpy.float64' object is not callable问题的体现了)。

如果定义了lambda的那个函数,就是仅仅定义了函数的关系,但是并没有直接执行它,也就是说值不会直接计算开来。此时检测f的类型就是function,是一个函数而不是一个值,这才是我们需要的。

  • 24
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值