【深度学习】从零实现全连接神经网络!

作者:Peter    编辑:Peter

本文给大家介绍深度学习神经网络中的基础知识:

  • 向量、矩阵和多维数组

  • 神经网络基础

  • 激活函数

  • 全连接网络从零实现

数学和Python基础

在神经网络中,向量和矩阵是随处可见的。下面介绍基于numpy创建一维、二维和高维数组

向量(一维数组)

向量是同时拥有大小和方向的量,向量可以表示成排成一排的数字集合。

In [1]:

import numpy as np

In [2]:

# 创建行向量  
row_vector = np.array([1, 2, 3, 4, 5])  
print("行向量:")  
print(row_vector)  
行向量:
[1 2 3 4 5]

In [3]:

# 创建列向量  
col_vector = np.array([[1], [2], [3],[4],[5]])  
print("列向量:")  
print(col_vector)
列向量:
[[1]
 [2]
 [3]
 [4]
 [5]]

矩阵(二维数组)

创建一个3乘3的矩阵:

In [4]:

# 创建一个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]:

# 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]:

array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

下面介绍矩阵的逆的求解(只有方阵才有逆):

已知,矩阵A,如果,其中E为单位矩阵,则B矩阵称之为A矩阵的逆矩阵

并非所有的方阵都有逆矩阵;当方阵不可逆时,引入零矩阵的概念

In [7]:

# 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]:

# 计算矩阵的行列式
determinant = np.linalg.det(matrix)
print("行列式:")
print(determinant)
行列式:
-9.51619735392994e-16

多维数组

将向量和矩阵扩展到N维,就是多维数组。

1、创建全0张量:

In [9]:

tensor = np.zeros((2, 3, 4, 4))
tensor

Out[9]:

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]:

tensor.shape  # 多维数组的形状

Out[10]:

(2, 3, 4, 4)

In [11]:

tensor.size  # 数组整体的元素个数

Out[11]:

96

In [12]:

tensor.ndim  # 表示4个维度

Out[12]:

4

2、全1数组

In [13]:

tensor1 = np.ones((2,4,3))
tensor1

Out[13]:

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]:

tensor1.ndim  # 表示4个维度

Out[14]:

3

3、自定义数组

In [15]:

tensor2 = np.arange(48)
tensor2

Out[15]:

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]:

tensor2.reshape((2,8,3))

Out[17]:

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]:

# 效果同上:numpy会自动推断-1所在维度的数值

tensor2.reshape((2,8,-1))

Out[18]:

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]:

A = np.array([[1,2,3],
              [4,5,6]])
A

Out[19]:

array([[1, 2, 3],
       [4, 5, 6]])

In [20]:

A * 100

Out[20]:

array([[100, 200, 300],
       [400, 500, 600]])

相当于是把100变成了[[100,100,100],[100,100,100]],然后和数组A中对应位置上的元素相乘。

In [21]:

A + 200

Out[21]:

array([[201, 202, 203],
       [204, 205, 206]])

在加法上的广播机制:

In [22]:

A + [100,200,300]

Out[22]:

array([[101, 202, 303],
       [104, 205, 306]])

乘积dot

向量的内积是一个具体的数值:

In [23]:

a = np.array([1,2,3])
b = np.array([4,5,6])

In [24]:

a * b  # 矩阵乘法

Out[24]:

array([ 4, 10, 18])

In [25]:

np.dot(a,b) # 4+10+18=32

Out[25]:

32

In [26]:

sum(a * b)  # 矩阵乘法的结果就是乘积

Out[26]:

32

矩阵的内积:

In [27]:

c = np.array([[1,2],[3,4]])
d = np.array([[5,6],[7,8]])

In [28]:

c

Out[28]:

array([[1, 2],
       [3, 4]])

In [29]:

d

Out[29]:

array([[5, 6],
       [7, 8]])

In [30]:

np.dot(c,d)

Out[30]:

array([[19, 22],
       [43, 50]])
  • 15+27=19

  • 16+28=22

  • 35+47=43

  • 36+48=50

神经网络基础

基本原理

神经网络就是一个复杂的函数。函数是将某些输入转变某些输出的变换器,神经网络的功能也是类似:

表示输入层的数据,、表示权重,表示偏置。

第一个隐藏神经元的结果可以表示为:

隐藏层的神经元是基于加权和计算出来的。

1f6362e20c01f5d231a5fc90238c2fff.png
  • 基础的神经网络有3个层:输入层、隐藏层、输出层

  • 箭头上带有两个信息:权重w和偏置b;权重和神经元的值相乘再加上偏置,经过某个激活函数后的值作为下个神经元的输入

隐藏层实现

所有相邻神经元之间通过权重和偏置连接的网络也称之为全连接网络

4个隐藏神经元基于矩阵乘积的完整计算:

可以缩写为:

其中,是1×2,是2×4,是1×4

下面实现隐藏层的计算:

In [31]:

# 简单实现隐藏层的计算

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]:

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函数

In [32]:

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()
f1f0edc491be3e2040e85e77f464b9d4.png

2、relu激活函数:

In [33]:

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()
53aa4fc2c35821d88e1c33c4e7396ffa.png

3、tanh函数:

In [34]:

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()
b9fc7c347d2e5dd51e5bb008428546a7.png

神经网络加上sigmoid激活函数

In [35]:

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笔样本数据:、,对应10个隐藏层的神经元、

In [36]:

h = np.dot(x,W1) + b1 # 隐藏神经元
a = sigmoid(h)  # 对隐藏神经元使用激活函数
a

Out[36]:

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]:

s = np.dot(a,W2) + b2  # 输出神经元
s

Out[37]:

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]])

用另一个全连接层来变换这个激活函数的输出。

隐藏层有4个神经元,输出层有3个神经元,所以全连接层使用的权重矩阵的形状设置成。

完整代码

整体的完整代码为:

In [38]:

# 神经网络 + 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]:

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]:

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]:

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]:

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__):

  • 第一层: 一个线性变换层 (通过Affine实现) 连接输入层和隐藏层。

  • 第二层: 一个Sigmoid激活函数层 (通过Sigmoid实现)。

  • 第三层: 一个线性变换层 (通过Affine实现) 连接隐藏层和输出层。

  • input_size: 输入层的神经元数量。

  • hidden_size: 隐藏层的神经元数量。

  • output_size: 输出层的神经元数量。

  • 输入参数:

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

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

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

  • self.params: 用于存储所有的权重参数。

预测方法 (predict):

  • 输入参数: x: 输入数据。

  • 对每一层,使用其forward方法进行前向传播,更新x的值。

  • 最后返回更新后的x值。这通常是网络的输出。

该网络没有包括偏置项的更新过程,所以偏置项只在前向传播中被使用,而在反向传播中不会被更新。

正向传播案例

In [42]:

x = np.random.randn(10,2)
model = TwoLayerNet(2,4,3)

s = model.predict(x)
s

Out[42]:

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 作为损失函数。

绘制softmax函数的图像:

In [43]:

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()
cfe0c0c379d72185b076e2779fbb5395.png

Softmax函数输出的各个元素是0.0~1.0的实数。如果将这些元素全部加起来,则和为1.因此,Softmax的输出可以解释为概率。之后这个概率被输入交叉熵误差中。交叉熵误差表示为:

  • 对应于第k个类别的监督标签

  • log是以e为底数的对数

在考虑了mini-batch处理的情况下,交叉熵误差可以表示为:

假设有N笔数据,表示第n笔数据的第k维元素的值;表示神经网络的输出,表示监督标签

导数和梯度

导数

神经网络的学习的目标是找到损失尽可能小的参数组合。简单介绍下导数和梯度:

函数,此时L关于x的导数记为,表示变化程度。具体地说,x的微小变化会导致L发生多大程度的变化。

关于的导数可以表示为:

梯度

那么对所有x的导数为:

将L关于向量各个元素的导数罗列在一起,就得到了梯度gradient

矩阵求解梯度:

其中是的矩阵,

具有相同的形状。

链式法则

学习阶段的神经网络在给定学习数据后会输出损失。当我们得到了损失关于各个参数的梯度,就可以利用这些梯度对参数对梯度进行更新。

如何求出神经网络的梯度?使用反向传播法。理解反向传播法的关键是:链式法则。链式法则是复合函数的求导法则,其中复合函数是由多个函数构成的函数。

考虑两个函数: 和 ,此时:,那么z关于x的导数为:

MatMul层的实现

实现矩阵操作的层:

In [44]:

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]:

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

In [46]:

print("原始数据a的地址:", id(a))
print("原始数据b的地址:", id(b))
原始数据a的地址: 2575053791920
原始数据b的地址: 2575053167312

In [47]:

a = b  #  将b赋值给a;
a

Out[47]:

array([4, 5, 6])

再次查看a和b的内存地址:

In [48]:

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]:

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

In [50]:

print("原始数据a的地址:", id(a))
print("原始数据b的地址:", id(b))
原始数据a的地址: 2575054711248
原始数据b的地址: 2575054715760

In [51]:

a[...] = b  # 赋值

In [52]:

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]:

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层

通过实现了Affine层的正向传播。

In [54]:

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)的梯度。

其中,表示的就是学习率,比如0.001、0.01等

In [55]:

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类更新神经网络的参数(提供伪代码)

# 伪代码

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)
 
 
 
 

89ddd2541f7d5abd3d30d9edc7bd12af.jpeg

 
 
 
 
往期精彩回顾




适合初学者入门人工智能的路线及资料下载(图文+视频)机器学习入门系列下载机器学习及深度学习笔记等资料打印《统计学习方法》的代码复现专辑
  • 交流群

欢迎加入机器学习爱好者微信群一起和同行交流,目前有机器学习交流群、博士群、博士申报交流、CV、NLP等微信群,请扫描下面的微信号加群,备注:”昵称-学校/公司-研究方向“,例如:”张小明-浙大-CV“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~(也可以加入机器学习交流qq群772479961)

22f5c0f59f82092f90ee1c22aa5682c0.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值