这段代码不长,实现的原理也不是很难理解。
但是它的实现的代码来来回回看了很久才懂,主要是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.py与common/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,是一个函数而不是一个值,这才是我们需要的。