深度学习入门1-手搓神经网络-《深度学习入门:基于Python的理论与实现》学习笔记
文章目录
在这学期开始学习深度学习,找学长要了一些书籍,包括《d2l-zh-pytorch》《深度学习入门:基于Python的理论与实现》等,这两本书作为入门书籍十分友好,只需一些python、numpy的基础即可。相比于我上学期硬看周志华的西瓜书,看了一半实在看不进去了,数学推导太多太繁琐,大部分时候不说为什么,更像是上课用的教材,在没有老师的指导时难以自学。以前我还很担心机器学习学不会的话无法开展深度学习,实际上,0基础的人也可以从深度学习开始,注重动手实践从而理解算法本质。
1. 引言
为什么学习神经网络?
- 神经网络是一种强大的人工智能工具,近年来已经成为推动技术革新的核心力量。神经网络模拟人脑的结构和功能,通过大量的计算单元(神经元)进行数据处理和模式识别。实践已经证明,神经网络在性能上已经远远超过传统的统计方法。
神经网络的未来发展
- 随着计算资源的提升(尤其是GPU的应用)、大数据的积累和优化算法的发展,神经网络的应用越来越广泛,从学术研究到工业应用都产生了深远的影响。如今,深度学习已经成为人工智能领域的核心技术之一,未来有望在更多领域带来创新性突破。
2. 什么是神经网络?
神经网络的基本结构
-
神经元(Neuron):输入x,权重w,偏置b或cita,激活函数f
在生物神经网中, 每个神经元与其他神经元相连, 当它“兴奋”时,就会问相连的神经元发送化学物质, 从而改变这些神经元内的电位; 如果神经元的电位超过了一个“阈值”(threshold), 那么它会被激活。

-
参数:权重和偏置(Weight and Bias):神经网络中用于调整输入信息的参数
对于每个神经元,包含权重w,偏置b或cita:

-
激活函数(Activation Function):Sigmoid、ReLU、Tanh等
即上图中的f。

理想的激活函数为上图(a)的阶跃函数,但它不够平滑,导数为0,以至于我们无法用它的梯度(导数)来传递参数的更新。
常用激活函数为Sigmoid、ReLU。
Sigmoid:如上图(b)所示

优点:它的图像最接近阶跃函数,将结果限制在[0, 1]之间,且处处可导。
缺点:1.梯度消失问题:Sigmoid函数的导数在极端值(如很大的正数或负数)时非常小,导致梯度消失问题。这意味着在深层网络中,反向传播时梯度会变得非常小,导致学习速度极慢甚至停滞。2. 计算开销较大:Sigmoid的计算涉及指数运算,计算开销较大,尤其在深层网络中可能会影响性能。
下面的ReLU激活函数可改善这些问题:
ReLU:ReLU函数在输入大于0时,直接输出该值;在输入小于等于0时,输出0


优点:1.解决了Sigmod函数的两个缺点。2.稀疏激活:ReLU会将负数输出为0,产生稀疏激活(即大量神经元的输出为0)。这使得网络更具稀疏性和一定的正则化效果。
缺点:1.死神经元问题:当输入为负数时,ReLU的输出为0,这意味着如果在训练过程中某个神经元的输入始终为负数,那么它的梯度也会是0,导致该神经元永远不会被激活,进而无法对网络产生有效的贡献。这个问题被称为"死神经元"问题。
2.输出不被限制:ReLU没有上界限制,输出可以变得非常大,这可能导致梯度爆炸等问题,尤其是在深层网络中。
对比之下,ReLU常用在各种网络中间的激活函数,Sigmod常用于网络的末端,将输入(一般是分类问题)限制在0,1之间
-
层(Layer):输入层,隐藏层,输出层
多个神经元并行在一行,即为一层。输入层即为自变量此时,把该层的神经元统一计算输出,就可以使用矩阵运算: Y = WX + B
输入X的维度为 d X 1, 权重参数W的维度为 n X d(即这一层有n个神经元,每个神经元有d个权重w,1个偏执b)输出Y的维度为 n X 1,我们把这个隐层的结构类写出来:
class Layer: def __init__(self, W, b): self.W = W self.b = b初始化时给定参数

3. 神经网络的工作原理
前向传播(Forward Propagation)
即上面说的矩阵运算: Y = WX + B,这一个层的输入即下一个层的输出。
我们在上面的类里加入forward方法:
def forward(self, x):
self.x = x
return np.dot(x, self.W) + self.b
这样调用Layer.forward(x)即可输出这层的结果
反向传播(Backpropagation)
反向传播,即从最右端的输出向左端传播梯度,告诉每一层它们的参数要变化多少。参考《深度学习入门:基于Python的理论与实现》第五章,讲的非常浅显易懂。

这里不展开详细的求导推导,总结起来就是,如果我们想让损失函数减少1,对于这一层的某个参数来说要变化多少呢?
对此,根据链式法则传递梯度(导数),我们要获得这个层的下一层的输入(就是本层的输出)要变化多少(梯度),然后再计算本层的参数要变化多少,同时把本层的输入(x)要变化多少传给上一层,然后上一层再做同样的事情。这里注意我们在算梯度时要用到x(比如y=x**2, 导数为2x),因此forward时把x先存起来,后面backward时用。
我们在反向传播时
把backward方法写入刚才的类里:
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dx
我们获得的梯度(dw db)当然要用来更新本来的参数,因此要先保存起来(self.dw, self,db,故这些变量也应在init里声明),返回dx
因此完整的层如下:
class Layer:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
return np.dot(x, self.W) + self.b
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dx
我在动手实现时,还加了一个这样的函数:
def update(self, rate):
self.W -= rate * self.dW
self.b -= rate * self.db
以为能方便更新参数了,但后面的学习发现有许多不同的参数更新策略,不单单是简单的梯度下降。因此无需这样的函数,而是将这个层的参数返回,在类的外面选择合适的策略更新。(果然书上的东西都有它的道理)
4. 激活函数
上面的层只实现了线性变换: Y = WX + B, 神经元的激活与否还要使用第2部分的激活函数。
下面给出实现 Sigmod:
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
self.out = sigmoid(x)
return self.out
def backward(self, dout):
return dout * (1 - self.out) * self.out
ReLU:
class ReLU:
def __init__(self):
self.x = None
def forward(self, x):
self.x = x
return x * (x > 0)
def backward(self, dout):
return dout * (self.x > 0)
注意:这些激活函数中并没有可学习的参数,仅需保证完成前向传播和梯度反向传播
softmax函数用于在分类任务中使神经网络的输出变为概率分布,一般仅加在网络的最后一层的输出上:

在指数里面加上 C 是为了防止溢出(分母接近0),并不改变原数值大小。容易看出,softmax函数的输出是0.0到1.0之间的实数。并且,softmax 函数的输出值的总和是1。输出总和为1是softmax函数的一个重要性质。正 因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”。带有Loss的实现如下(因为我们只要结果的时候,也就是预测的时候,取网络最后一层输出的最大值即可,softmax不改变大小关系,但在训练时,我们需要每种预测的概率来构成交叉熵损失,进行学习):

class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据(one-hot vector)
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t) #交叉熵损失,也可以是其他
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dout * dx
5. 神经网络的训练过程
损失函数(Loss Function)
常见的如下:
-
均方误差(MSE):

-
交叉熵(Cross-Entropy):

具体细节不展开了,这就是我们要最小化的量。
优化算法(Optimization Algorithms: optimizer)
梯度下降算法及其变种,如批量梯度下降、随机梯度下降(SGD)、AdaGrad等,Adam优化器等。
最简单的随机梯度下降(SGD):

class SGD:
def __init__(self, lr=0.001):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
学习率(Learning Rate: lr)
如何选择合适的学习率通常要试验得出,选用进阶的梯度下降方法如动量法、Adam等可减轻因选取的学习率不当引起的训练异常。
运用以上我们实现的小模块,就可以组成一个完整的多层的神经网络(上文的Layer改名为Affine,因为不加激活函数的它不是一个完整的层):
class Network3Layer:
def __init__(self, input_size, hidden_size1, output_size):
self.params = {}
# 参数初始化为高斯分布
self.params['W1'] = np.sqrt(2 / input_size) * np.random.randn(input_size, hidden_size1)
self.params['b1'] = np.random.randn(hidden_size1)
self.params['W2'] = np.sqrt(2 / hidden_size1) * np.random.randn(hidden_size1, output_size)
self.params['b2'] = np.random.randn(output_size)
self.Layer = {}
self.Layer['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.Layer['ReLU1'] = ReLU()
self.Layer['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for i in self.Layer.values():
x = i.forward(x)
# return self.lastLayer.forward(x)
return x
ef gradient(self, x, t):
# forword
for i in self.Layer.values():
x = i.forward(x)
self.lastLayer.forward(x, t)
# backward
grad = self.lastLayer.backward(1)
LayerList = list(self.Layer.values())
LayerList.reverse()
for l in LayerList:
grad = l.backward(grad)
grads = {}
grads['W1'] = self.Layer['Affine1'].dW
grads['b1'] = self.Layer['Affine1'].db
grads['W2'] = self.Layer['Affine2'].dW
grads['b2'] = self.Layer['Affine2'].db
return grads
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
if t.ndim != 1 : t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
def loss(self, y, t):
d = 1e-7
return self.lastLayer.forward(y,t)
假设已有输入输出数据x,y(可以在网上下载MNIST手写数据集),则训练过程如下:
network = Network3Layer(784, 100, 10)
batch_size = 100 # 批数量
loss_ave = 0
optimizer = SGD(0.001)
for j in range(100):
for i in range(0, len(x), batch_size):
x_batch = x[i:i+batch_size]
t_batch = t[i:i + batch_size]
y_batch = network.predict(x_batch) # score
loss_ave = network.loss(y_batch, t_batch)
grads = network.gradient(x_batch, t_batch)
optimizer.update(network.params, grads)
print(j, loss_ave)
print('accuracy: {}'.format(network.accuracy(x, t)))
到此为止,我们不依赖任何库(除numpy)完成了一个可扩充的神经网络,上面的3层网络(一层隐层)在MNIST手写数据集中准确率可达96.23%.
代码在:https://github.com/PUBGprofessor/DeepLearning.git
参考文献
- 《深度学习入门:基于Python的理论与实现》
- 《机器学习—周志华》
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)