神经网络基础:从零实现全连接网络

公众号:尤而小屋

作者: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层

代码规范:

  1. 所有的层都使用forward()方法和backward()方法,分别代表正向传播和反向传播。
  2. 所有的层都使用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的类,表示一个包含两个隐藏层的神经网络。以下是对代码的详细解释:

  1. 初始化方法 (__init__):

    • 输入参数:

      • input_size: 输入层的神经元数量。
      • hidden_size: 隐藏层的神经元数量。
      • output_size: 输出层的神经元数量。
    • W1b1: 随机生成用于连接输入层和隐藏层的权重矩阵和偏置项。

    • W2b2: 随机生成用于连接隐藏层和输出层的权重矩阵和偏置项。

      self.layers:定义了一个层列表,包含以下三层:

      • 第一层: 一个线性变换层 (通过Affine实现) 连接输入层和隐藏层。
      • 第二层: 一个Sigmoid激活函数层 (通过Sigmoid实现)。
      • 第三层: 一个线性变换层 (通过Affine实现) 连接隐藏层和输出层。
    • self.params: 用于存储所有的权重参数。

  2. 预测方法 (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 是深复制。

  1. a = b 是浅复制,因为它创建了一个新的引用 a,指向与 b 相同的内存地址。此时,修改 b 的值也会影响 a,因为它们引用的是同一个对象。
  2. 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)
相关推荐
深度学习实战训练营32 分钟前
基于CNN-RNN的影像报告生成
人工智能·深度学习
孙同学要努力7 小时前
全连接神经网络案例——手写数字识别
人工智能·深度学习·神经网络
sniper_fandc10 小时前
深度学习基础—循环神经网络的梯度消失与解决
人工智能·rnn·深度学习
weixin_5182850510 小时前
深度学习笔记10-多分类
人工智能·笔记·深度学习
阿_旭11 小时前
基于YOLO11/v10/v8/v5深度学习的维修工具检测识别系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·qt·ai
YRr YRr11 小时前
深度学习:Cross-attention详解
人工智能·深度学习
阿_旭11 小时前
基于YOLO11/v10/v8/v5深度学习的煤矿传送带异物检测系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·目标检测·yolo11
算家云12 小时前
如何在算家云搭建Aatrox-Bert-VITS2(音频生成)
人工智能·深度学习·aigc·模型搭建·音频生成·算家云
小言从不摸鱼13 小时前
【NLP自然语言处理】深入解析Encoder与Decoder模块:结构、作用与深度学习应用
人工智能·深度学习·神经网络·机器学习·自然语言处理·transformer·1024程序员节
湫ccc13 小时前
Bert框架详解(上)
人工智能·深度学习·bert