公众号:尤而小屋
作者:Peter
编辑:Peter
大家好,我是Peter~
本文给大家介绍深度学习神经网络中的基础知识:
- 向量、矩阵和多维数组
- 神经网络基础
- 激活函数
- 全连接网络从零实现
数学和Python基础
在神经网络中,向量和矩阵是随处可见的。下面介绍基于numpy创建一维、二维和高维数组
向量(一维数组)
向量是同时拥有大小和方向的量,向量可以表示成排成一排的数字集合。
In [1]:
javascript
import numpy as np
In [2]:
scss
# 创建行向量
row_vector = np.array([1, 2, 3, 4, 5])
print("行向量:")
print(row_vector)
行向量:
[1 2 3 4 5]
In [3]:
lua
# 创建列向量
col_vector = np.array([[1], [2], [3],[4],[5]])
print("列向量:")
print(col_vector)
列向量:
[[1]
[2]
[3]
[4]
[5]]
矩阵(二维数组)
创建一个3乘3的矩阵:
In [4]:
lua
# 创建一个3x3的矩阵
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("原始矩阵:")
print(matrix)
原始矩阵:
[[1 2 3]
[4 5 6]
[7 8 9]]
矩阵的相关计算操作:
In [5]:
lua
# 1-计算矩阵的转置
transpose_matrix = np.transpose(matrix)
print("转置矩阵:")
print(transpose_matrix)
转置矩阵:
[[1 4 7]
[2 5 8]
[3 6 9]]
也可以使用矩阵的T属性来实现转置:
In [6]:
matrix.T
Out[6]:
lua
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
下面介绍矩阵的逆的求解(只有方阵才有逆):
已知,矩阵A,如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> A B = E AB=E </math>AB=E,其中E为单位矩阵,则B矩阵称之为A矩阵的逆矩阵
并非所有的方阵都有逆矩阵;当方阵不可逆时,引入零矩阵的概念
In [7]:
lua
# 2-计算矩阵的逆
inverse_matrix = np.linalg.inv(matrix)
print("逆矩阵:")
print(inverse_matrix)
逆矩阵:
[[ 3.15251974e+15 -6.30503948e+15 3.15251974e+15]
[-6.30503948e+15 1.26100790e+16 -6.30503948e+15]
[ 3.15251974e+15 -6.30503948e+15 3.15251974e+15]]
矩阵A的行列式表示为|A|或det(A):如果一个矩阵的行列式不为零,则该矩阵是可逆的;反之若行列式为0,则不可逆。
In [8]:
scss
# 计算矩阵的行列式
determinant = np.linalg.det(matrix)
print("行列式:")
print(determinant)
行列式:
-9.51619735392994e-16
多维数组
将向量和矩阵扩展到N维,就是多维数组。
1、创建全0张量:
In [9]:
ini
tensor = np.zeros((2, 3, 4, 4))
tensor
Out[9]:
css
array([[[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]],
[[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]]])
In [10]:
bash
tensor.shape # 多维数组的形状
Out[10]:
scss
(2, 3, 4, 4)
In [11]:
arduino
tensor.size # 数组整体的元素个数
Out[11]:
96
In [12]:
bash
tensor.ndim # 表示4个维度
Out[12]:
4
2、全1数组
In [13]:
ini
tensor1 = np.ones((2,4,3))
tensor1
Out[13]:
lua
array([[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]]])
In [14]:
bash
tensor1.ndim # 表示4个维度
Out[14]:
3
3、自定义数组
In [15]:
ini
tensor2 = np.arange(48)
tensor2
Out[15]:
scss
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])
In [16]:
tensor2.ndim
Out[16]:
1
实施形状shape的改变:
In [17]:
bash
tensor2.reshape((2,8,3))
Out[17]:
css
array([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17],
[18, 19, 20],
[21, 22, 23]],
[[24, 25, 26],
[27, 28, 29],
[30, 31, 32],
[33, 34, 35],
[36, 37, 38],
[39, 40, 41],
[42, 43, 44],
[45, 46, 47]]])
In [18]:
bash
# 效果同上:numpy会自动推断-1所在维度的数值
tensor2.reshape((2,8,-1))
Out[18]:
css
array([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17],
[18, 19, 20],
[21, 22, 23]],
[[24, 25, 26],
[27, 28, 29],
[30, 31, 32],
[33, 34, 35],
[36, 37, 38],
[39, 40, 41],
[42, 43, 44],
[45, 46, 47]]])
numpy广播机制broadcast
在NumPy多维数组中,形状不同的数组之间也可以进行运算,看下面的例子:
In [19]:
lua
A = np.array([[1,2,3],
[4,5,6]])
A
Out[19]:
lua
array([[1, 2, 3],
[4, 5, 6]])
In [20]:
css
A * 100
Out[20]:
lua
array([[100, 200, 300],
[400, 500, 600]])
相当于是把100变成了[[100,100,100],[100,100,100]],然后和数组A中对应位置上的元素相乘。
In [21]:
css
A + 200
Out[21]:
lua
array([[201, 202, 203],
[204, 205, 206]])
在加法上的广播机制:
In [22]:
css
A + [100,200,300]
Out[22]:
lua
array([[101, 202, 303],
[104, 205, 306]])
乘积dot
向量的内积是一个具体的数值:
In [23]:
css
a = np.array([1,2,3])
b = np.array([4,5,6])
In [24]:
css
a * b # 矩阵乘法
Out[24]:
scss
array([ 4, 10, 18])
In [25]:
css
np.dot(a,b) # 4+10+18=32
Out[25]:
32
In [26]:
bash
sum(a * b) # 矩阵乘法的结果就是乘积
Out[26]:
32
矩阵的内积:
In [27]:
lua
c = np.array([[1,2],[3,4]])
d = np.array([[5,6],[7,8]])
In [28]:
r
c
Out[28]:
lua
array([[1, 2],
[3, 4]])
In [29]:
d
Out[29]:
lua
array([[5, 6],
[7, 8]])
In [30]:
r
np.dot(c,d)
Out[30]:
lua
array([[19, 22],
[43, 50]])
- 15+27=19
- 16+28=22
- 35+47=43
- 36+48=50
神经网络基础
基本原理
神经网络就是一个复杂的函数。函数是将某些输入转变某些输出的变换器,神经网络的功能也是类似:
<math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 , x 2 ) (x_1,x_2) </math>(x1,x2)表示输入层的数据, <math xmlns="http://www.w3.org/1998/Math/MathML"> w 11 、 w 21 w_{11}、w_{21} </math>w11、w21表示权重, <math xmlns="http://www.w3.org/1998/Math/MathML"> b 1 b_1 </math>b1表示偏置。
第一个隐藏神经元的结果可以表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML"> h 1 = x 1 w 11 + x 2 w 21 + b 1 h_1 = x_1w_{11} + x_2w_{21} + b_1 </math>h1=x1w11+x2w21+b1
隐藏层的神经元是基于加权和计算出来的。
-
基础的神经网络有3个层:输入层、隐藏层、输出层
-
箭头上带有两个信息:权重w和偏置b;权重和神经元的值相乘再加上偏置,经过某个激活函数后的值作为下个神经元的输入
隐藏层实现
所有相邻神经元之间通过权重和偏置连接的网络也称之为全连接网络。
4个隐藏神经元基于矩阵乘积的完整计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ( h 1 , h 2 , h 3 , h 4 ) = ( x 1 , x 2 ) ( w 11 w 12 w 13 w 14 w 21 w 22 w 23 w 24 ) + ( b 1 , b 2 , b 3 , b 4 ) \left(h_1, h_2, h_3, h_4\right)=\left(x_1, x_2\right)\left(\begin{array}{cccc} w_{11} & w_{12} & w_{13} & w_{14} \\ w_{21} & w_{22} & w_{23} & w_{24} \end{array}\right)+\left(b_1, b_2, b_3, b_4\right) </math>(h1,h2,h3,h4)=(x1,x2)(w11w21w12w22w13w23w14w24)+(b1,b2,b3,b4)
可以缩写为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> h = x W + b h=\boldsymbol{x} \boldsymbol{W}+\boldsymbol{b} </math>h=xW+b
其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x是1×2, <math xmlns="http://www.w3.org/1998/Math/MathML"> W W </math>W是2×4, <math xmlns="http://www.w3.org/1998/Math/MathML"> h h </math>h是1×4
下面实现隐藏层的计算:
In [31]:
ini
# 简单实现隐藏层的计算
W1 = np.random.randn(2,4)
b1 = np.random.randn(4) # 会进行广播机制
x = np.random.randn(10,2)
h = np.dot(x,W1) + b1
h
Out[31]:
css
array([[-0.80051904, -0.98416179, 1.7341734 , -2.04167071],
[ 0.78748052, 1.16324088, 1.06046707, -0.14729655],
[ 0.48977828, 1.97419074, -0.2551278 , -1.81056956],
[-1.70298304, -1.89547071, 1.74981458, -3.45140937],
[-0.33747008, 0.19029478, 0.88625396, -2.08032192],
[-0.83369394, -0.94217107, 1.64505112, -2.17486955],
[ 1.76349184, 3.52267844, -0.58885411, -0.10364203],
[ 0.68911993, 1.36470324, 0.70478019, -0.62518316],
[ 2.28604672, 5.22576684, -1.99452197, -0.55441134],
[ 1.67011023, 1.98066418, 1.13292561, 1.31107412]])
激活函数
全连接层网络的变换是线性变换。使用激活函数能够赋予它"非线性"的效果。使用激活函数能够增强神经网络的表现力。
在这里介绍下常用的激活函数:
1、sigmoid函数
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> σ ( x ) = 1 1 + exp ( − x ) \sigma(x)=\frac{1}{1+\exp (-x)} </math>σ(x)=1+exp(−x)1
In [32]:
scss
import numpy as np
import matplotlib.pyplot as plt
# 定义函数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# x-y
x = np.linspace(-10, 10, 100)
y = sigmoid(x)
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('sigmoid(x)')
plt.title('Sigmoid Function')
plt.grid(True)
plt.show()
2、relu激活函数:
In [33]:
scss
import numpy as np
import matplotlib.pyplot as plt
def relu(x):
return np.maximum(0, x)
x = np.linspace(-10, 10, 100)
y = relu(x)
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('ReLU(x)')
plt.title('ReLU Function')
plt.grid(True)
plt.show()
3、tanh函数:
In [34]:
python
import numpy as np
import matplotlib.pyplot as plt
# 定义Tanh函数
def tanh(x):
return np.tanh(x)
# 生成x值
x = np.linspace(-10, 10, 1000)
# 计算y值
y = tanh(x)
# 绘制图像
plt.plot(x,y)
plt.xlabel('x')
plt.ylabel('tanh(x)')
plt.title('Tanh Function')
plt.grid(True)
plt.show()
神经网络加上sigmoid激活函数
In [35]:
lua
def sigmoid(x):
"""
定义sigmoid函数
"""
return 1 / (1 + np.exp(-x))
x = np.random.randn(10,2)
W1 = np.random.randn(2,4)
b1 = np.random.randn(4)
W2 = np.random.randn(4,3)
b2 = np.random.randn(3)
print("W1:\n",W1)
print("b1:\n",b1)
print("W1:\n",W2)
print("b1:\n",b2)
W1:
[[-0.81716844 -0.2700162 0.47712972 1.52610728]
[-0.13728734 -0.48808859 -0.39338065 0.75255599]]
b1:
[ 1.21057066 0.14936438 0.8861704 -0.49434345]
W1:
[[ 0.77540225 -0.0813373 1.61562571]
[ 0.18555707 -1.57503291 -1.48010281]
[-1.26013418 -0.71906974 1.98427043]
[-0.09948728 -0.06870956 -0.0222825 ]]
b1:
[ 1.24730692 -0.26517252 -0.21867687]
上面的例子中有10笔样本数据: <math xmlns="http://www.w3.org/1998/Math/MathML"> x [ 0 ] 、 x [ 1 ] . . . . . . x[0]、x[1]...... </math>x[0]、x[1]......,对应10个隐藏层的神经元 <math xmlns="http://www.w3.org/1998/Math/MathML"> h [ 0 ] 、 h [ 1 ] . . . . . . h[0]、h[1]...... </math>h[0]、h[1]......
In [36]:
ini
h = np.dot(x,W1) + b1 # 隐藏神经元
a = sigmoid(h) # 对隐藏神经元使用激活函数
a
Out[36]:
css
array([[0.89036829, 0.68166094, 0.6709089 , 0.07559634],
[0.61832049, 0.42523258, 0.74774176, 0.75044002],
[0.34969505, 0.34159362, 0.85077636, 0.95890215],
[0.80167595, 0.4228318 , 0.55454109, 0.43608519],
[0.7444589 , 0.34582381, 0.54414278, 0.64542647],
[0.86841737, 0.75140648, 0.78167754, 0.07047421],
[0.73173076, 0.58966053, 0.78724036, 0.39576964],
[0.95512889, 0.64006136, 0.40382409, 0.02324403],
[0.5662982 , 0.40702709, 0.77004949, 0.81874147],
[0.46174302, 0.57379292, 0.91075142, 0.79929614]])
In [37]:
ini
s = np.dot(a,W2) + b2 # 输出神经元
s
Out[37]:
css
array([[ 1.21123139, -1.89885556, 1.54047697],
[ 0.78874474, -1.57446122, 1.61790986],
[ 0.41435542, -1.50929024, 1.50750941],
[ 1.20520658, -1.42506962, 1.54133929],
[ 1.13882744, -1.30603225, 1.53757998],
[ 1.06807862, -2.0862201 , 1.62169098],
[ 0.89270575, -1.84669814, 1.64404699],
[ 1.5954989 , -1.64295259, 1.17787556],
[ 0.71012253, -1.5622894 , 1.60354995],
[ 0.48462603, -1.91628525, 1.46742132]])
用另一个全连接层来变换这个激活函数的输出 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a。
隐藏层有4个神经元,输出层有3个神经元,所以全连接层使用的权重矩阵的形状设置成 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 ∗ 3 4*3 </math>4∗3。
完整代码
整体的完整代码为:
In [38]:
ini
# 神经网络 + sigmoid激活函数的完整代码
import numpy as np
def sigmoid(x):
return 1 / (1+np.exp(-x))
x = np.random.randn(10,2) # 10*2
W1 = np.random.randn(2, 4) # 2*4 # 4表示隐藏神经元个数;2是和输入x的x.shape[1]相匹配
b1 = np.random.randn(4) # 10*4;偏置必须为4
W2 = np.random.randn(4, 3) # 4*3 # 3个输出神经元个数;4个第一个隐藏神经元的shape[1]
b2 = np.random.randn(3) # # 10*3;偏置必须为3
h = np.dot(x, W1) + b1
a = sigmoid(h)
s = np.dot(a, W2) + b2
s
Out[38]:
css
array([[ 1.52757614, -1.50378018, 0.69751311],
[ 2.16511559, -1.24918194, 0.23662412],
[ 2.76594566, -1.29153453, -0.57892373],
[ 2.41784231, -1.19402867, 0.22564103],
[ 1.26703665, -1.46154016, 1.37571233],
[ 2.15837849, -1.33121603, 0.69843425],
[ 2.02575248, -1.32591319, 0.9561904 ],
[ 2.02329408, -1.30629759, 0.93633673],
[ 2.24129269, -1.22068445, 0.50451103],
[ 2.74841634, -1.30657481, -0.51901695]])
神经网络层的实现(类)
正向传播
正向传播(Forward Propagation)是指神经网络中的信息从输入层开始,经过各层神经元的处理后,最终到达输出层的过程。
在正向传播过程中,每一层的神经元将前一层的输出作为输入,经过内部的计算后,将结果传递给下一层。这个过程会一直持续到输出层,产生网络的最终输出。
正向传播过程中,神经元的输入和输出之间通过权重连接,并且会经过激活函数的非线性变换,使得网络可以学习和模拟复杂的非线性关系
反向传播
反向传播(Backpropagation)是一种优化算法,用于训练神经网络。
它是通过计算损失函数对神经网络参数的梯度来更新参数,从而最小化损失函数。在神经网络的训练过程中,反向传播算法通过对每个节点的输出误差进行反向传播,调整每个节点的权重,使得网络能够更准确地预测结果。
具体来说,首先给网络输入一组训练数据,并计算输出结果。然后计算输出结果与实际结果的差异,也就是网络的误差。接下来计算每个节点对误差的贡献,并将这些贡献反向传播到前一层。根据贡献的大小,调整每个节点的权重,使得误差减小。重复以上过程,直到误差达到一定程度为止。通过不断调整权重,反向传播算法能够使网络越来越准确地预测结果。
定义网络层
- sigmoid激活函数的变换:Sigmoid层
- 全连接层的变换相当于几何学领域的放射变换:Affine层
代码规范:
- 所有的层都使用forward()方法和backward()方法,分别代表正向传播和反向传播。
- 所有的层都使用params 和 grads实例变量;其中params使用列表保存权重和偏置参数(可能多个参数,用列表),grads以与params中的参数对应的形式。
Sigmoid层
定义激活函数的Sigmoid层:
In [39]:
ruby
import numpy as np
class Sigmoid:
def __init__(self):
self.params = [] # 没有学习的参数,使用空列表
def forward(self,x):
return 1 / (1 + np.exp(-x))
Affine层
定义全连接层Affine:
In [40]:
python
class Affine:
def __init__(self, W, b):
"""
初始化参数:权重W和偏置b
"""
self.params = [W,b] # 参数列表保存:W-权重 b-偏置
def forward(self, x):
"""
基于矩阵点积的前向传播功能forward
"""
W,b = self.params # 将参数列表进行赋给W和b
out = np.dot(x,W) + b # 实现前向传播功能
return out
TwoLayerNet网络
In [41]:
python
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size):
"""
初始化方法:输入层的神经元数量-隐藏层神经元数量-输出层神经元数量
"""
I,H,O = input_size, hidden_size, output_size
# 连接输入层和隐藏层
W1 = np.random.randn(I,H) # 权重和偏置项的初始值
b1 = np.random.randn(H)
# 连接隐藏层和输出层
W2 = np.random.randn(H,O)
b2 = np.random.randn(O)
# 定义层列表,包含全连接层1、激活层、全连接层2
self.layers = [
Affine(W1, b1),
Sigmoid(),
Affine(W2,b2)
]
# 将所有的权重整理到列表中
self.params = []
for layer in self.layers: # 循环每个层
self.params += layer.params # 权重参数放到列表params中
def predict(self, x):
for layer in self.layers:
x = layer.forward(x) # 对每个层使用forward的更新方法,最终输出Out
return x
定义一个名为TwoLayerNet
的类,表示一个包含两个隐藏层的神经网络。以下是对代码的详细解释:
-
初始化方法 (
__init__
):-
输入参数:
input_size
: 输入层的神经元数量。hidden_size
: 隐藏层的神经元数量。output_size
: 输出层的神经元数量。
-
W1
和b1
: 随机生成用于连接输入层和隐藏层的权重矩阵和偏置项。 -
W2
和b2
: 随机生成用于连接隐藏层和输出层的权重矩阵和偏置项。self.layers:定义了一个层列表,包含以下三层:
- 第一层: 一个线性变换层 (通过
Affine
实现) 连接输入层和隐藏层。 - 第二层: 一个Sigmoid激活函数层 (通过
Sigmoid
实现)。 - 第三层: 一个线性变换层 (通过
Affine
实现) 连接隐藏层和输出层。
- 第一层: 一个线性变换层 (通过
-
self.params
: 用于存储所有的权重参数。
-
-
预测方法 (
predict
):- 输入参数:
x
: 输入数据。 - 对每一层,使用其
forward
方法进行前向传播,更新x
的值。 - 最后返回更新后的
x
值。这通常是网络的输出。
- 输入参数:
该网络没有包括偏置项的更新过程,所以偏置项只在前向传播中被使用,而在反向传播中不会被更新。
正向传播案例
In [42]:
ini
x = np.random.randn(10,2)
model = TwoLayerNet(2,4,3)
s = model.predict(x)
s
Out[42]:
css
array([[-0.11149934, 2.92086863, 0.02600311],
[-0.52853339, 2.55687161, 0.09639072],
[-0.37599196, 2.70311037, 0.07967748],
[-0.08386817, 2.84851586, 0.09121393],
[-0.36363321, 2.50668478, 0.14880286],
[-0.18938764, 2.80382617, 0.08435871],
[-0.36065023, 2.69994522, 0.08593762],
[-0.38339136, 2.57659206, 0.12620684],
[-0.29318787, 2.68147118, 0.1135041 ],
[ 0.15702997, 2.94857814, 0.09733163]])
神经网络的学习
神经网络的过程一般是先进行学习,再利用好的参数进行推理。为了知道学习的效果如何,通常需要一个指标。这个指标一般称之为损失loss。
损失函数loss function(Softmax)
基于监督学习或者神经网络的预测结果,与实际结果之间差异程度。也就说将模型的恶劣程度作为标量值计算出来,得到的就是损失。
多类别分类问题中,通常使用的交叉熵误差cross entropy
作为损失函数。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y k = e S k ∑ i = 1 n ∗ e S i y_k = \frac{e^{S_{k}}}{\sum_{i=1}^n* e^{S_i}} </math>yk=∑i=1n∗eSieSk
绘制softmax函数的图像:
In [43]:
python
import numpy as np
import matplotlib.pyplot as plt
def softmax(x):
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum()
# 生成输入数据
x = np.linspace(-10, 10, 100)
# 计算softmax值
y = softmax(x)
# 绘制softmax曲线
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('softmax(x)')
plt.title('Softmax Function')
plt.grid(True)
plt.show()
Softmax函数输出的各个元素是0.0~1.0的实数。如果将这些元素全部加起来,则和为1.因此,Softmax的输出可以解释为概率。之后这个概率被输入交叉熵误差中。交叉熵误差表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − ∑ k t k l o g y k L=-\sum_kt_klogy_{k} </math>L=−k∑tklogyk
- <math xmlns="http://www.w3.org/1998/Math/MathML"> t k t_k </math>tk对应于第k个类别的监督标签
- log是以e为底数的对数
在考虑了mini-batch处理的情况下,交叉熵误差可以表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − 1 N ∑ n ∑ k t n k l o g y n k L=-\frac{1}{N}\sum_n\sum_kt_{nk}logy_{nk} </math>L=−N1n∑k∑tnklogynk
假设有N笔数据, <math xmlns="http://www.w3.org/1998/Math/MathML"> t n k t_{nk} </math>tnk表示第n笔数据的第k维元素的值; <math xmlns="http://www.w3.org/1998/Math/MathML"> y n k y_{nk} </math>ynk表示神经网络的输出, <math xmlns="http://www.w3.org/1998/Math/MathML"> t n k t_{nk} </math>tnk表示监督标签
导数和梯度
导数
神经网络的学习的目标是找到损失尽可能小的参数组合。简单介绍下导数和梯度:
函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> L = f ( x ) L=f(x) </math>L=f(x),此时L关于x的导数记为 <math xmlns="http://www.w3.org/1998/Math/MathML"> d L d x \frac{\mathrm{d} L}{\mathrm{~d} x} </math> dxdL,表示变化程度。具体地说,x的微小变化会导致L发生多大程度的变化。
<math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L关于 <math xmlns="http://www.w3.org/1998/Math/MathML"> x i x_i </math>xi的导数可以表示为: <math xmlns="http://www.w3.org/1998/Math/MathML"> ∂ L ∂ x i \frac{\partial L}{\partial x_i} </math>∂xi∂L
梯度
那么对所有x的导数为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∂ L ∂ x = ( ∂ L ∂ x 1 , ∂ L ∂ x 2 , ... , ∂ L ∂ x n ) \frac{\partial L}{\partial \boldsymbol{x}}=\left(\frac{\partial L}{\partial x_1}, \frac{\partial L}{\partial x_2}, \ldots, \frac{\partial L}{\partial x_n}\right) </math>∂x∂L=(∂x1∂L,∂x2∂L,...,∂xn∂L)
将L关于向量各个元素的导数罗列在一起,就得到了梯度gradient。
矩阵求解梯度:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∂ L ∂ W = ( ∂ L ∂ W 11 ⋯ ∂ L ∂ W 1 n ⋮ ⋱ ∂ L ∂ W m 1 ∂ L ∂ W m n ) \frac{\partial L}{\partial \boldsymbol{W}}=\left(\begin{array}{ccc} \frac{\partial L}{\partial W_{11}} & \cdots & \frac{\partial L}{\partial W_{1 n}} \\ \vdots & \ddots & \\ \frac{\partial L}{\partial W_{m 1}} & & \frac{\partial L}{\partial W_{m n}} \end{array}\right) </math>∂W∂L=⎝ ⎛∂W11∂L⋮∂Wm1∂L⋯⋱∂W1n∂L∂Wmn∂L⎠ ⎞
其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> W W </math>W是 <math xmlns="http://www.w3.org/1998/Math/MathML"> m × n m×n </math>m×n的矩阵, <math xmlns="http://www.w3.org/1998/Math/MathML"> W 和 ∂ L ∂ W W \text { 和 } \frac{\partial L}{\partial \boldsymbol{W}} </math>W 和 ∂W∂L具有相同的形状。
链式法则
学习阶段的神经网络在给定学习数据后会输出损失。当我们得到了损失关于各个参数的梯度,就可以利用这些梯度对参数对梯度进行更新。
如何求出神经网络的梯度?使用反向传播法。理解反向传播法的关键是:链式法则。链式法则是复合函数的求导法则,其中复合函数是由多个函数构成的函数。
考虑两个函数: <math xmlns="http://www.w3.org/1998/Math/MathML"> y = f ( x ) y=f(x) </math>y=f(x) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> z = g ( y ) z=g(y) </math>z=g(y),此时: <math xmlns="http://www.w3.org/1998/Math/MathML"> z = g ( ( f ( x ) ) ) z=g((f(x))) </math>z=g((f(x))),那么z关于x的导数为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∂ z ∂ x = ∂ z ∂ y ∂ y ∂ x \frac{\partial z}{\partial x}=\frac{\partial z}{\partial y} \frac{\partial y}{\partial x} </math>∂x∂z=∂y∂z∂x∂y
MatMul层的实现
实现矩阵操作的层:
In [44]:
python
class MatMul:
def __init__(self, W):
self.params = [W] # 保存学习的参数,此时只有权重W
self.grads = [np.zeros_like(W)] # 梯度保存在grads
self.x = None
# 前向传播
def forward(self, x):
W, = self.params # 参数
out = np.dot(x,W) # 输出
self.x = x
return out
# 后向传播
def backword(self, dout):
W, = self.params
dx = np.dot(dout, W.T)
dW = np.dot(self.x.T, dout)
# grads[0][...] 使用了省略号:可以固定Numpy数组的内存地址,覆盖Numpy数组的元素
# grads[0]=dW 浅复制 grads[0][...] = dW 深复制
self.grads[0][...] = dW # 实例变量grads中设置权重的梯度;grads列表中每个元素是Numpy数组
return dx
关于Numpy的[...]复制问题
案例
其实讨论的就是深浅复制的问题。
In [45]:
css
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
In [46]:
bash
print("原始数据a的地址:", id(a))
print("原始数据b的地址:", id(b))
原始数据a的地址: 2575053791920
原始数据b的地址: 2575053167312
In [47]:
css
a = b # 将b赋值给a;
a
Out[47]:
scss
array([4, 5, 6])
再次查看a和b的内存地址:
In [48]:
css
print("经过a=b后数据a的地址:", id(a))
print("经过a=b后数据b的地址:", id(b))
经过a=b后数据a的地址: 2575053167312
经过a=b后数据b的地址: 2575053167312
可以看到a和b的内存地址是完全相同的。也就是收将b的引用赋值给a,此时a和b指向内存中的不同位置。
In [49]:
css
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
In [50]:
bash
print("原始数据a的地址:", id(a))
print("原始数据b的地址:", id(b))
原始数据a的地址: 2575054711248
原始数据b的地址: 2575054715760
In [51]:
css
a[...] = b # 赋值
In [52]:
css
print("经过a[...]=b后数据a的地址:", id(a))
print("经过a[...]=b后数据b的地址:", id(b))
经过a[...]=b后数据a的地址: 2575054711248
经过a[...]=b后数据b的地址: 2575054715760
可以看到a和b的内存地址是不同的;且a的地址还是赋值前的地址。
a[...]=b
表示的是原地修改数据:正在将数组b赋值为数组a,因为这是一个原地操作,所以a和b仍然指向同一个内存地址。
结论
在上面的例子中,a = b
是浅复制,而 a[...] = b
是深复制。
a = b
是浅复制,因为它创建了一个新的引用 a,指向与 b 相同的内存地址。此时,修改 b 的值也会影响 a,因为它们引用的是同一个对象。a[...] = b
是深复制,因为它在原地修改了数组 a 的值 ,使其与数组 b 相等。这个操作不会影响数组 b 的内存地址,而只是将 b 的值复制到 a 中。因此,即使后续修改了 b 的值,也不会影响 a 的值。
梯度的推导和反向传播实现
Sigmoid层
实现基于Sigmoid函数的前项传播和反向传播的过程:
In [53]:
ruby
class Sigmoid:
def __init__(self):
self.params, self.grads = [], [] # 保存参数和及其梯度
self.out = None # 存储前项传播的结果
def forward(self, x):
# 前向传播过程;经过Sigmoid函数进行输出
out = 1 / (1 + np.exp(-x)) # sigmoid函数
self.out = out # 保存输出out
return out
def backward(self, dout):
# 后项传播过程
dx = dout * (1.0 - self.out) * self.out # sigmoid的导数是y*(1-y)
return dx # 返回梯度
Affine层
通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = n p . d o t ( x , W ) + b y = np.dot(x,W) + b </math>y=np.dot(x,W)+b实现了Affine层的正向传播。
In [54]:
python
class Affine:
def __init__(self, W, b):
"""
类的初始化函数,接受两个参数
"""
# 保存权重矩阵和偏置向量
self.params = [W,b]
# 初始化两个与权重矩阵和偏置向量形状相同的零梯度数组,保存在实例的grads属性中
self.grads = [np.zeros_like(W), np.zeros_like(W)]
self.x = None
def forward(self, x):
"""
定义前项传播方法
"""
W,b = self.params # 从params属性中取出权重和偏置
out = np.dot(x,W) + b # 前向输出:基于线性变换
self.x = x # 将输入x保存在实例的x属性中
return out
def backword(self, dout):
"""
定义后项传播方法
"""
W, b = self.params # 从params属性中取出权重和偏置
dx = np.dot(dout, W.T) # 通过点乘计算梯度
dW = np.dot(self.x.T, dout) # 计算关于权重矩阵的梯度
db = np.sum(dout, axis=0) # 计算关于偏置向量的梯度
self.grads[0][...] = dW # 权重矩阵和梯度存储在实例的grads属性中
self.grads[1][...] = db
return dx
权重更新
通过误差反向传播法求出梯度后,就可以使用该梯度更新神经网络的参数。
步骤1:mini-batch
- 从训练数据中随机选出多笔数据
步骤2:计算梯度
- 基于误差反向传播法,计算损失函数关于各个权重参数的梯度
步骤3:更新参数
- 使用梯度更新权重参数
重复步骤1-2-3
这里说的梯度指向当前的权重参数所处位置中损失增加最多的方向。通常将参数向该梯度的反方向进行更新,可以加速降低损失,这就是梯度下降法(gradient descent)。
下面介绍随机梯度下降法(Stochastic Gradient Descent, SGD)。随机指的就是选择的数据(mini-batch)的梯度。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> W ← W − η ∂ L ∂ W \boldsymbol{W} \leftarrow \boldsymbol{W}-\eta \frac{\partial L}{\partial \boldsymbol{W}} </math>W←W−η∂W∂L
其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> η \eta </math>η表示的就是学习率,比如0.001、0.01等
In [55]:
python
class SGD:
def __init__(self, lr=0.01):
self.lr = lr # 学习率设置
def update(self, params, grads):
for i in range(len(params)):
params[i] -= self.lr * grads[i] # 参数更新
使用SGD类更新神经网络的参数(提供伪代码)
scss
# 伪代码
model = TwoLayerNet(...)
optimizer = SGD()
for i in range(10000):
x_batch, t_batch = get_mini_batch()
loss = model.farward(x_batch, t_batch)
model.backward()
optimizer.update(model.params, model.grads)