神经网络 《深度学习入门 基于Python的理论实现》第三章理论部分

本文介绍了从感知机到神经网络的转变,重点讨论了神经网络中的激活函数,如阶跃函数、sigmoid函数和ReLU函数。激活函数的选择对神经网络的非线性能力至关重要。此外,还讲解了多层神经网络的实现,包括权重的传递、矩阵运算以及输出层的设计,如恒等函数和softmax函数在分类问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


layout: post
title: 深度学习入门 基于Python的理论实现
subtitle: 第三章 神经网络
tags: [Machine learning, Reading]


第三章 神经网络

紧接着第二章的内容,我们开始介绍神经网络。上一章感知机的参数都是我们认为验证给定的,对于神经网络来说,一个重要的性质就是可以自动地从数据中学习到合适的权重参数。

3.1 从感知机到神经网络

3.1.1 神经网络的例子

下图就是一个神经网络的例子,把最左边的一列称为输入层,中间一列称为中间层,中间层有时又称为隐藏层,最右边一列称为输出层

在这里插入图片描述

按之前的习惯,我们把输入层称为第0层,隐藏层称为第1层,输出层称为第二层,因此在这里我们称之为两层神经网络。注意,也有些地方将其称为三层神经网络。但怎么叫都无伤大雅。上图的结构和之前感知机的结构很相似。那么他们有什么异同呢。

3.1.2 复习感知机

先回顾一下感知机的结构。

感知机

感知机接收x1x_1x1,x2x_2x2两个输入信号,输出yyy,数学表达如下:

y={0(b+w1x1+w2x2⩽0)1(b+w1x1+w2x2>0) y=\left\{\begin{array}{ll} 0 & \left(b+w_{1} x_{1}+w_{2} x_{2} \leqslant 0\right) \\ 1 & \left(b+w_{1} x_{1}+w_{2} x_{2}>0\right) \end{array}\right. y={01(b+w1x1+w2x20)(b+w1x1+w2x2>0)

bbb称为偏置,控制神经元被激活的难易程度,而ω1\omega_1ω1ω2\omega_2ω2称为权重,表示信号的重要程度。我们发现,在上图中并没有表现出偏置,我们可以做一点小小的修改,改动后的结构如下图所示:

在这里插入图片描述

在上面的表示方法中,分了两种情况来表示,表示起来并不是很方便,因此我们将原来的公式改写成下面的式子:

y=h(b+ω1x1+ω2x2)y = h(b+\omega_1x_1+\omega_2x_2)y=h(b+ω1x1+ω2x2)

h={0(x⩽0)1(x>0) h=\left\{\begin{array}{ll} 0 & \left(x \leqslant 0 \right) \\ 1 & \left(x > 0\right) \end{array}\right. h={01(x0)(x>0)

3.1.3 激活函数

上面的h(x)h(x)h(x)函数将输入信号的总和转换为输出信号,这种函数一般称为激活函数,顾名思义,激活函数用来决定如何激活信号。
我们将上面的公式写的更详细一点:

a=b+ω1x1+ω2x2a=b+\omega_1x_1+\omega_2x_2a=b+ω1x1+ω2x2

y=h(a)y = h(a)y=h(a)

首先计算加权输入信号和偏执的总和,记为aaa,然后激活函数h()h()h(),将aaa转换为输出yyy。为了明确表示这个过程,我们得到了下面的图:

在这里插入图片描述

3.2 激活函数

下来我们详细介绍激活函数,激活函数以阈值为界,一旦超过阈值,就切换输出,这样的函数被称为阶跃函数。感知机使用了阶跃函数,那么选择其他函数会怎么样呢。

3.2.1 sigmoid函数

这是非常常见的激活函数。

h(x)=11+e−x h(x)=\frac{1}{1+e^{-x}} h(x)=1+ex1

神经网络使用sigmoid作为激活函数。神经网络和感知机的主要区别就在于激活函数

3.2.2 阶跃函数的实现

即:

h={0(x⩽0)1(x>0) h=\left\{\begin{array}{ll} 0 & \left(x \leqslant 0 \right) \\ 1 & \left(x > 0\right) \end{array}\right. h={01(x0)(x>0)


def step_function(x):
	if x>0:
		return 1
	else:
		return 0

这是一个最基本的实现,但是输入不能以数组的形式呈现,下面我们修改一下使之可以使用Numpy数组实现。

def step_function(x):
	y = x > 0
	
	return y.astype(np.int)

这里函数中的第二个行对输入的numpy数组判断是否大于零,大于零的返回True,反之则为False。astype将布尔值转换为整形,True变为1,False变为0。

在这里插入图片描述

3.2.3 阶跃函数的图形

我们用matplotlib来画图。

import numpy as np
import matplotlib.pyplot as plt

def step_function(x):
    return np.array(x>0,dtype=np.int)

x = np.arange(-5.0,5.0,0.1)
y = step_function(x)

plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()

执行结果如下:
在这里插入图片描述

3.2.4 sigmoid函数的实现

def sigmoid(x):
	return 1/(1+np.exp(-x))

绘图:

import numpy as np
import matplotlib.pyplot as plt

def sigmoid_function(x):
    return 1/(1+np.exp(-x))

x = np.arange(-5.0,5.0,0.1)
y = sigmoid_function(x)

plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()

执行结果如下:
在这里插入图片描述

将他们放在一起做一个对比:

import numpy as np
import matplotlib.pyplot as plt

def sigmoid_function(x):
    return 1/(1+np.exp(-x))

x = np.arange(-5.0,5.0,0.1)
y = sigmoid_function(x)
z = step_function(x)
plt.plot(x,y)
plt.plot(x,z,'--')
plt.ylim(-0.1,1.1)
plt.show()

在这里插入图片描述

通过上图观察,他们之间究竟有什么不同。我讲这些不同归纳为以下两点:

  • sigmoid函数更加平滑,阶跃函数以0为界发生阶跃。
  • 阶跃函数只能返回0和1,sigmoid函数返回连续的值
3.2.6 非线性函数

刚刚说过的阶跃函数和sigmoid函数都是非线性函数,因为他们不是一条直线。对于神经网络来说,激活函数必须使用非线性函数。否则神经网络具有的层的概念就没有意义了 。

在这里举一个简单的例子。假设我们在这里用线性函数h(x)=cxh(x) = cxh(x)=cx为激活函数。于是y(x)=h(h(h(x)))y(x)=h(h(h(x)))y(x)=h(h(h(x)))作为三层神经网络的运算,于是得到y(x)=c3xy(x)=c^3xy(x)=c3x,我们让c3=ac^3=ac3=a,于是有y(x)=axy(x)=axy(x)=ax,这个三层网络可以用一层网络代替,那么这个三层网络就没有意义了且无论多少层都可用一层代替。

3.2.7 ReLU函数

sigmoid函数很早就开始使用了,现在使用较多的是ReLU函数(Rectified Linear Unit)。ReLU函数也很简单,输入大于0时,直接输出该值,否则输出0。可表示为:

h={0(x⩽0)x(x>0) h= \left\{\begin{array}{ll} 0 & \left(x \leqslant 0 \right) \\ x & \left(x > 0\right) \end{array}\right. h={0x(x0)(x>0)

代码实现如下:

def relu(x):
	if x>0:
		return x
	else:
		return 0
def relu(x):
	return np.maxium(0,x)

ReLU函数的图像如下。

import numpy as np
import matplotlib.pyplot as plt

def relu(x):
    return np.maximum(0,x)

x = np.arange(-5.0,5.0,0.1)
y = relu(x)
plt.plot(x,y)
plt.ylim(-0.5,4.0)
plt.show()

在这里插入图片描述

3.3 多位数组的运算

这一部分主要是矩阵乘法的部分,包括多维数组和矩阵乘法等知识。就不再赘述了。

3.3.3 神经网络的内积

现在我们使用Numpy来实现神经网络。下面的神经网络省略了偏置和激活函数,只有权重。

在这里插入图片描述

结合上面的图,我们得到几个式子。

h={1×x1+2×x2=y13×x1+4×x2=y25×x1+6×x2=y3 h=\left\{\begin{array}{ll} 1 \times x_1+2 \times x_2 = y_1 \\ 3 \times x_1+4 \times x_2 = y_2 \\ 5 \times x_1+6 \times x_2 = y_3 \end{array}\right. h=1×x1+2×x2=y13×x1+4×x2=y25×x1+6×x2=y3

使得X=(x1x2)X = \begin{pmatrix} x_1\\ x_2 \end{pmatrix}X=(x1x2),使得Y=(y1y2y3)Y = \begin{pmatrix} y_1\\ y_2\\ y_3 \end{pmatrix}Y=y1y2y3 于是有:

(123456)(x1x2)=(y1y2y3) \begin{pmatrix} 1&2 \\ 3&4\\ 5&6 \end{pmatrix} \begin{pmatrix} x_1\\ x_2 \end{pmatrix} = \begin{pmatrix} y_1\\ y_2\\ y_3 \end{pmatrix} 135246(x1x2)=y1y2y3

在实现的时候要特别注意矩阵的维度。下面进行实现:

import numpy as np

def NLP_simple(x1,x2):
    X = np.array([[x1],[x2]])
    W = np.array([[1,2],[3,4],[5,6]])
    Y = np.dot(W,X)
    return Y


Y = NLP_simple(1,2)

在这里插入图片描述

在这里是一个简单的例子,但是应该注意到的是,当神经元个数增大时,相应的只是矩阵的大小发生了变化,也是使用矩阵的乘法完成计算。

3.4 3层神经网络的实现

我们再进一步计算更复杂的神经网络。先看下面3层神经网络的结构:

在这里插入图片描述

这个神经网络包含一个两神经元的输入层,两层隐藏层,神经元个数分别为3和2,还包含一个包含两个神经元的输出层。

3.4.1 符号确认

为了让之后的内容更加明晰,我们首先对后面出现的一些表示方法做一下说明。

在这里插入图片描述

如上图所示,a1(1)a_{1}^{(1)}a1(1)中,上面括号里的1代表这个是第一层的权重,下标的1代表这是第一个神经元。接下来,ω12(1)\omega_{12}^{(1)}ω12(1)的上标依旧代表第一层的权重,下标的12中,2代表前一层的第二个神经元,1代表后一层的第一个神经元,总结一句,就是代表前一层的第二个神经元到后一层的第一个神经元的权重。权重右下角按照后一层的索引号、前一层的索引号的顺序排列。

3.4.2 各层间信号传递的实现

我们首先看输入层到第一层的第一个神经元的信号传递过程。如下图所示:

在这里插入图片描述

于是有

a1(1)=x1ω11(1)+x2ω12(1)+b1(1) a_{1}^{(1)} = x_1\omega_{11}^{(1)}+x_2\omega_{12}^{(1)} + b_{1}^{(1)} a1(1)=x1ω11(1)+x2ω12(1)+b1(1)

按照之前的方法,我们将a2(1)a_{2}^{(1)}a2(1),a3(1)a_{3}^{(1)}a3(1)一起写出来,得到如下式子:

{a1(1)=x1ω11(1)+x2ω12(1)+b1(1)a2(1)=x1ω21(1)+x2ω22(1)+b2(1)a3(1)=x1ω31(1)+x2ω32(1)+b3(1) \left\{\begin{array}{ll} a_{1}^{(1)} = x_1\omega_{11}^{(1)}+x_2\omega_{12}^{(1)} + b_{1}^{(1)} \\ a_{2}^{(1)} = x_1\omega_{21}^{(1)}+x_2\omega_{22}^{(1)} + b_{2}^{(1)} \\ a_{3}^{(1)} = x_1\omega_{31}^{(1)}+x_2\omega_{32}^{(1)} + b_{3}^{(1)} \end{array}\right. a1(1)=x1ω11(1)+x2ω12(1)+b1(1)a2(1)=x1ω21(1)+x2ω22(1)+b2(1)a3(1)=x1ω31(1)+x2ω32(1)+b3(1)

转换为矩阵乘法的形式,得到:

A(1)=XW(1)+B(1) A^{(1)} = XW^{(1)}+B^{(1)} A(1)=XW(1)+B(1)

其中:

A(1)=(a1(1)a2(1)a3(1)) A^{(1)} = \begin{pmatrix} a_{1}^{(1)}&a_{2}^{(1)}&a_{3}^{(1)} \end{pmatrix} A(1)=(a1(1)a2(1)a3(1))

X=(x1x2) X = \begin{pmatrix} x_1&x_2 \end{pmatrix} X=(x1x2)

B(1)=(b1(1)b2(1)b3(1)) B^{(1)} = \begin{pmatrix} b_{1}^{(1)} & b_{2}^{(1)} & b_{3}^{(1)} \end{pmatrix} B(1)=(b1(1)b2(1)b3(1))

W(1)=(ω11(1)ω21(1)ω31(1)ω12(1)ω22(1)ω32(1)) W^{(1)} = \begin{pmatrix} \omega_{11}^{(1)}&\omega_{21}^{(1)}&\omega_{31}^{(1)}\\ \omega_{12}^{(1)}&\omega_{22}^{(1)}&\omega_{32}^{(1)} \end{pmatrix} W(1)=(ω11(1)ω12(1)ω21(1)ω22(1)ω31(1)ω32(1))

在实现时,我们首先将输入XXX和偏置B(1)B^{(1)}B(1)设置为某任意值。

import numpy as np

X = np.array([1.0,0.5])
W1 = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]])
B1 = np.array([0.1,0.2,0.3])

A1 = np.dot(X,W1) + B1
print(A1)

对于得到的加权和我们还要代入激活函数。

def sigmoid_function(x):
    return 1/(1+np.exp(-x))
    
Z1 = sigmoid_function(A1)

在这里插入图片描述

具体的计算细节如下图所示:

在这里插入图片描述

对照着第一层的计算过程,我们写出第二层的公式和代码。

在这里插入图片描述

import numpy as np

W2 = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])
B2 = np.array([0.1,0.2])

A2 = np.dot(Z1,W2) + B
Z2 = sigmoid_function(A2)
print(A1)

在这里插入图片描述

第二层和第一层是完全一样的,只不过第二层的输入变成了上一层的输出。

最后是第二层到输出层的代码和示意图,分析方法和表示方法同上。只是激活函数略有不同。代码如下:

def identity_function(x):
    return x

W3 = np.array([[0.1,0.3],[0.2,0.4]])
B3 = np.array([0.1,0.2])

A3 = np.dot(Z2,W3) + B3
Y = identity_function(A3)

print(Y)

在这里插入图片描述

在这里定义了identity_function作为激活函数,这里对输入不做任何操作直接输出,这样的函数叫做恒等函数。最后,输出层的激活函数区别于隐藏层用h(x)h(x)h(x)表示,输出层的激活函数用σ(x)\sigma(x)σ(x)表示。最后放出输出层的示意图。
在这里插入图片描述

输出层激活函数的选择将在下一节详细介绍。

3.4.3 代码实现小结

这一节将之前的代码做整理。

import numpy as np

def init_parameter():   #初始化参数
    network = {}
    network['W1'] = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]]) 
    network['W2'] = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]]) 
    network['W3'] = np.array([[0.1,0.3],[0.2,0.4]])
    
    network['B1'] = np.array([0.1,0.2,0.3])
    network['B2'] = np.array([0.1,0.2])
    network['B3'] = np.array([0.1,0.2])
    
    return network

def forward(network,x):   #运算
    Z1 = sigmoid_function(np.dot(x,network['W1'])+network['B1'])
    Z2 = sigmoid_function(np.dot(Z1,network['W2'])+network['B2'])
    Y = identity_function(np.dot(Z2,network['W3'])+network['B3'])
    
    return Y

# 实现
network = init_parameter()

x = np.array([1.0,0.5])

Y = forward(network,x)

print(Y)

运行结果如下,在这里使用init_parameter()函数初始化参数,再用forward函数整合计算过程。
在这里插入图片描述

3.5 输出层设计

神经网络可以用在分类问题和回归问题上,输出层根据情况进行选择,一般来说回归问题用恒等函数,分类问题用softmax函数。这里回顾一下分类和回归,分类问题顾名思义是判断数据属于哪一类的问题,回归是根据输入预测一个(连续的)数值的问题。

3.5.1 恒等函数和softmax函数

恒等函数很简单,对于输入的值,不做任何改动直接输出。输出层使用恒等函数,输入信号会原封不动的输出。

接下来着重介绍softmax函数,分类问题的softmax函数可以用下面的式子表示。

yk=exp(ak)∑i=1nexp(ai) y_k = \frac{exp(a_k)}{\sum_{i=1}^{n} exp(a_i)} yk=i=1nexp(ai)exp(ak)

这个式子还是比较好理解的,也就是说第kkk个输出等于,eke^kek除以所有输入信号指数和。图示如下:
在这里插入图片描述

明白了原理接下来实现。

a = np.array([0.3,2.9,4.0])
exp_a = np.exp(a)  #分别求指数
print(exp_a)
sum_exp_a = np.sum(exp_a) #指数和
print(sum_exp_a)
Y = exp_a/sum_exp_a  #结果
print(Y)

这只是简单验证,最终我们要写成函数。

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a/sum_exp_a
    
    return y
3.5.2 实现softmax函数的注意事项

上面的实现固然没有什么问题,就是严格按照函数的定义来写的,但是实际使用的时候,会遇到各种问题,因为这是一个指数函数,所以当输入信号很大时,会出现数据溢出的问题。因此我们可以做一点改进。

yk=exp(ak)∑i=1nexp(ai)=Cexp(ak)C∑i=1nexp(ai)=exp(ak+logC)∑i=1nexp(ai+logC)=exp(ak+C′)∑i=1nexp(ai+C′) y_k = \frac{exp(a_k)}{\sum_{i=1}^{n} exp(a_i)} = \frac{Cexp(a_k)}{C\sum_{i=1}^{n} exp(a_i)} = \frac{exp(a_k+logC)}{\sum_{i=1}^{n} exp(a_i+logC)} = \frac{exp(a_k+C')}{\sum_{i=1}^{n} exp(a_i+C')} yk=i=1nexp(ai)exp(ak)=Ci=1nexp(ai)Cexp(ak)=i=1nexp(ai+logC)exp(ak+logC)=i=1nexp(ai+C)exp(ak+C)

首先我们在分子分母上都乘以CCC,然后把CCC移进指数函数中,记为logClogClogC,最后把logClogClogC替换为C′C'C。对比原来的公式,我们发现,当使用softmax函数进行计算时,加上或减去某个常数并不会改变计算结果。于是对于溢出的结果,我们尝试让每个信号都减去信号中的最大值。


def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a/sum_exp_a
    
    return y
    
a = np.array([1010,1000,990])
print(softmax(a))

于是我们让每个信号都减去信号中的最大值。

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a/sum_exp_a
    
    return y

c = a - np.max(a)
print(softmax(c))

在这里插入图片描述

改进后的的代码如下:

def softmax(a):
    c= np.max(a)
    exp_a = np.exp(a-c) #溢出对策
    sum_exp_a = np.sum(exp_a)
    y = exp_a/sum_exp_a
    
    return y
3.5.3 softmax函数的特征

根据softmax函数的定义,我们知道softmax函数输出的总是0.0到1.0的示数,且输出值的总和总为1,因为这个性质,我们可以吧softmax函数的输出理解为“概率”。输出值最大那么就可以认为是这个类别的“概率”最大,也就完成了分类。

需要注意的是,使用softmax函数之后,各个信号的大小并没有改变,因为指数函数是单调递增的,比如a的最大值是第2个元素,那么经过softmax函数,最大的还是第2个元素。

所以一般来说,神经网络把输出值最大的神经元对应的类别作为识别的结果,以为softmax函数的使用与否并没有改变输出的大小结果,所以输出层的softmax函数可以省略。实际问题中,因为softmax函数有一定的运算量,所以一般会被省略。

3.5.4 输出层神经元的数量

对于分类问题,输出层神经元的数量和要分类的类别数相等。

总结

以上就是神经网络的理论部分内容,后面的部分是解决实际问题的例子,我想把这一部分单独提出来写。对上面的内容做一个总结:

  • 神经网络中激活函数使用平滑变化的sigmoid函数或ReLU函数。
  • 机器学习的问题大体可以分为分类问题和回归问题
  • 使用Numpy多维数组可以高效的实现神经网络。
  • 对于输出层的激活函数,回归问题用恒等函数,分类问题用softmax函数。
  • 分类问题,输出层神经元的数量和要分类的类别数相等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值