社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  机器学习算法

人工智能机器学习算法总结--3.神经网络算法(前向及反向传播)

稀土掘金技术社区 • 1 年前 • 177 次点击  

点击关注公众号,“技术干货” 及时达!



1.定义,意义和优缺点

定义:

神经网络算法是一种「模仿人类大脑神经元之间连接方式」的机器学习算法。通过「多层神经元的组合和激活函数的非线性转换」,神经网络能够学习数据的特征和模式,实现对复杂数据的「建模和预测」。(我们可以借助人类的神经元模型来更好的帮助我们理解该算法的本质,不过这里需要说明的是,虽然名字是神经网络,并且结构等等也是借鉴了神经网络,但其原型以及算法本质上还和生物层面的神经网络运行原理存在较大的区别)

20200125161252361.png

人类单个神经元结构图

如果大家先前对该生物神经元的结构,工作原理等等没有相关了解的话,笔者建议大家先停下来去百度或者必应一下,因为神经网络算法是由若干个类似神经元的模型小单元组成的,工作机制也相类似,为了能够较好的整体理解后面神经网络算法,建议先深刻地了解下该神经元。

意义:

神经网络算法在当今的科技领域中具有重要的意义和价值。

  • 首先,神经网络算法在人工智能领域扮演着关键角色,广泛应用于「图像识别、语音识别、自然语言处理、智能推荐系统」等各种领域。通过神经网络算法,计算机可以模仿人类大脑的工作方式,实现「对复杂数据的学习和理解」,从而实现智能化的应用。

  • 此外,神经网络算法也在工业生产、医疗健康、金融领域等多个行业中发挥着重要作用。在工业生产中,神经网络算法可以用于质量控制、预测维护、生产优化等方面;在医疗健康领域,神经网络算法可以帮助医生进行疾病诊断、药物研发等工作;在金融领域,神经网络算法可以用于风险评估、股票预测、信用评分等方面。

  • 另外,神经网络算法的发展也推动了计算机科学和机器学习领域的研究进步。通过不断改进神经网络结构、优化算法和提高计算性能,研究者们不断提升神经网络算法的性能和效率,推动了整个领域的发展。

总的来说,神经网络算法的意义在于推动了人工智能技术的发展和应用,提高了各行各业的效率和智能化水平,同时也促进了科学研究的进步。

优缺点:(关于各类算法的优缺点我会后续的文章中提取出来进行比较,并包括一些其他的区别)

优点:

  1. 「强大的拟合能力:」  神经网络能够学习复杂的非线性关系,对数据进行高效的建模和预测,适用于各种复杂的任务。
  2. 「自动特征学习:」  神经网络能够自动从数据中学习到特征表示,无需手动提取特征,减少了特征工程的工作量。
  3. 「适应性强:」  神经网络对于不同类型的数据和任务都有良好的适应性,可以应用于图像识别、自然语言处理、语音识别等多个领域。
  4. 「并行处理能力:」  神经网络可以在大规模数据上进行并行计算,加速训练过程,适用于大规模数据集和高维数据。

缺点:

  1. 「训练时间长:」  神经网络通常需要大量数据和较长的训练时间来达到较好的性能,尤其是在深层网络中。
  2. 「过拟合风险:」  神经网络容易在训练过程中出现过拟合现象,特别是在参数较多的情况下,需要进行正则化等方法来避免过拟合。
  3. 「黑盒模型:」  神经网络通常被视为黑盒模型,难以解释其内部的决策过程,不如一些传统机器学习算法具有可解释性。
  4. 「需要大量数据:」  神经网络通常需要大量的标记数据来训练模型,对数据的依赖性较强。

综上所述,神经网络算法具有强大的拟合能力和适应性,但在训练时间长、过拟合风险、黑盒模型和数据需求等方面存在一些挑战。在实际应用中,需要根据具体任务和数据情况来选择是否使用神经网络算法。

2.关于神经网络算法本质和运行原理本质的探究

相信很多同学到这里还是无法理解神经网络的本质是什么,为什么要这样组成神经网络算法,为什么这样组成的神经网络算法可以用来拟合和预测模型以及算法的具体实施和运行过程又是什么样的,由于对该算法的理解需要一个连续并且多方面组合的过程,所以笔者还是建议大家可以先去找些关于神经网络算法过程讲解的视频看一下,有助于理解,当然,下面是一些笔者自身的经验,感受和一些理解技巧,希望能够帮助到你(这里先大概讲解,每一个具体的细节知识点后面会拎出来单独解释下):

  1. 关于神经网络算法的结构,你可以先简单的理解为其是由很多个神经元按层分层交叉组成,每一个神经单元同时又是线性回归+逻辑回归的算法小集合体,利用线性回归对线性连续值特征的拟合和特征处理优势加上逻辑回归对离散非线性值(不同类别)特征的拟合和处理优势来共同达到对非线性,多类别的一个高精度的预测和拟合。
R-C.jpg

神经网络算法单个神经元结构示意图

「样本数据X1,X2,Xn等分别乘以对应的权重比例Wi1,Wi2,Win即为一个简单的线性回归的过程,再将其加权得到的全部结果输入激活函数进行非线性运算就是逻辑回归的过程」

nn_model.png

该图是多层由多个神经元组成的神经网络算法的整体结构,我们把最左边和最右边的两层(也就是首尾两层)分别称为输入层和输出层

下面该点注意:

2.对于这个算法整体,我们最关心的其实就是「需要输入什么,能够得到什么」,以此来解决我们想要它解决的问题。关于神经网络算法的输入数据,我们除了需要常规的特征矩阵X之外,我们还需要标签列向量Y(后面会解释),并且该标签列向量还需要经过特别的编码处理才能转化成我们想要的结果(也就是下图的效果),以此来方便后面对输出结果的比较和分类。关于神经网络算法的目标和输出结果,由于我们采用的是Sigmoid函数,所以其最终输出结果是0和1组成矩阵,相信学过或者了解逻辑回归的同学都知道,经过逻辑回归函数之后我们所得到的结果是处于0与1之间的一个值,在经过一些处理后最终的结果就只有0和1构成的矩阵,并且不同类别的输入特征矩阵X必定会产生不同的输出矩阵,我们在通过对比输出的矩阵和下图标签列经过编码处理得到矩阵之后我们就能够判别该特征数据X所对应的类别。

向量化标签.png

在这里,我们神经网络算法的目的是辨别手写的痕迹,所以在标签列y(至于什么是标签列,我会在后面详细解释)中,我们把每一种类别都加了进去并且把相同的类别都竖着排列在一起(左部列向量),然后通过编码处理(图中给出的是独热编码)将标签列y转化成k类(总共的类数)不同的,由0,1组成的列矩阵。

3.算法结构

神经网络算法的全步骤以及结构可以大致分为以下几个部分:

  1. 「数据准备」:首先需要准备训练数据集和测试数据集。通常会对数据进行预处理,包括归一化、标准化、处理缺失值等操作。
  2. 「选择模型架构」:确定神经网络的结构,包括选择层数、每层的神经元数量、激活函数等。常见的神经网络结构包括多层感知器(MLP)、卷积神经网络(CNN)、循环神经网络(RNN)等(具体要视问题本身而定)。
  3. 「初始化参数」:初始化神经网络的权重和偏置,通常采用随机初始化的方法。
  4. 「前向传播」:将输入数据通过神经网络的各层进行计算,直到得到最终的输出。
  5. 「计算损失函数」:将神经网络的输出与真实标签进行比较,计算损失函数来衡量预测结果的准确性。
  6. 「反向传播」:根据损失函数,利用反向传播算法计算梯度,并更新网络中的参数,以减小损失函数。
  7. 「参数优化」:使用优化算法(如梯度下降、Adam等)来调整网络参数,使得损失函数最小化。
  8. 「模型评估」:使用测试数据集对训练好的模型进行评估,计算准确率、精确率、召回率等指标。
  9. 「调参和优化」:根据评估结果对模型进行调参和优化,以提高模型的性能。

这就是神经网络算法的一般步骤和结构,不同类型的神经网络可能会有一些特定的步骤或结构。神经网络的训练过程是一个迭代的过程,通过不断调整参数来提高模型的性能和泛化能力。

注意:

看到这个过程,相信肯定有同学有疑问,我们之前都已经学过线性回归和逻辑回归算法了,知道怎么优化其中的参数,那为什么我们不在神经网络算法开始前就对线性回归和逻辑回归的参数进行相关的优化呢?

「答:」

如果你提前对神经网络的参数进行了优化,然后再进行神经网络的训练,实际上这种做法可能会导致一些问题。

当你提前优化神经网络的参数时,这些参数可能「已经过拟合了特定的数据集」,导致神经网络「在新数据上的泛化能力下降」。因为神经网络的目标是学习数据中的特征和模式,而不是简单地记住训练集的数据。

另外,如果你提前优化了参数,神经网络可能会「失去在训练过程中自适应数据的能力」,因为它的参数已经被固定了。这可能会导致训练过程无法有效地更新参数,从而「影响神经网络的性能」

因此,一般情况下,不建议提前对神经网络的参数进行优化,而是让神经网络在训练过程中通过「反向传播算法」(后面我们会讲)来自动学习数据的特征。这样可以确保神经网络能够更好地适应数据并提高泛化能力。

那么接下来,我们就一起来一步步地搭建这个算法

4.算法组成One-数据准备

# 引入相关的库函数
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from scipy.io import loadmat
import scipy.optimize as opt
from sklearn.metrics import classification_report

# 加载并存储文件中的数据
def load_data(path, transpose=True):
    data = loadmat(path)
    X = data['X']
    y = data['y']
    y = y.reshape(y.shape[0])
    
    if transpose:
        X = np.array([im.reshape((20,20)).T.reshape(400for im in X])
    return X, y
    
# 找到文件路径和位置    
X, y = load_data('ex4data1.mat', transpose=False)

# MATLAB文件(.mat文件)中的数据可以包含各种类型的数据,包括图像数据。
# 在MATLAB中,图像通常以矩阵的形式表示,其中每个元素对应于图像中的像素值。
# 通过`loadmat`函数,可以轻松地将MATLAB文件中的数据加载到Python中,并进行进一步的处理和分析。

X = np.insert(X, 0, np.ones(X.shape[0]), axis=1)
X.shape, y.shape #  ((5000, 401), (5000,))
# 此时X就是加了一列全是1的列向量之后的特征矩阵,y则为标签列
y
# 这里我们输出y的值看看  array([10, 10, 10, ..., 9, 9, 9], dtype=uint8) 总共y中有5000个元素

「标签列y的具体定义:」

在神经网络算法中,标签列y是指用来表示训练数据样本对应输出的列。在监督学习任务中,我们通常会有输入数据(特征)和对应的输出数据(标签)。标签列包含了我们希望模型学习并预测的目标变量或结果。

例如,在一个图像分类任务中,对于每张图像,我们会有对应的标签,表示图像所属的类别(如猫、狗、车等)。在训练神经网络时,我们将输入图像数据作为特征,将对应的类别标签作为标签列,让模型学习如何将输入图像映射到正确的类别标签。

标签列在神经网络算法中起着非常重要的作用,它是训练过程中监督学习的关键部分。通过比较模型预测的输出与真实的标签,我们可以计算损失函数,并通过反向传播算法更新模型的参数,使模型逐渐提高对标签的预测准确性。

因此,标签列在神经网络算法中扮演着指导模型学习的角色,帮助模型调整参数以更好地拟合数据并进行准确的预测。如果你还不理解标签列Y,那么接下来我举一个具体的例子:

对于类别型的标签列,如“垃圾邮件”和“非垃圾邮件”,我们可以使用独热编码(One-Hot Encoding)来将这些类别转换为机器学习模型可以理解的形式。独热编码是一种常见的编码方式,将每个类别映射到一个向量,其中只有一个元素为1,其他元素为0。

在进行独热编码时,我们首先需要确定所有可能的类别。对于“垃圾邮件”和“非垃圾邮件”这两个类别,我们可以将它们表示为一个二元向量。例如,如果我们有一个样本的标签是“垃圾邮件”,则独热编码后的向量为[1, 0];如果标签是“非垃圾邮件”,则编码后的向量为[0, 1]。

下面是进行独热编码的步骤:

  1. 确定所有可能的类别,例如“垃圾邮件”和“非垃圾邮件”。
  2. 为每个类别分配一个索引,例如“垃圾邮件”为0,“非垃圾邮件”为1。
  3. 对于每个样本的标签,创建一个全零向量,长度等于类别数量,然后将对应类别的索引位置设为1。

在机器学习任务中,独热编码通常用于处理分类标签,使得模型能够更好地理解类别之间的关系,而不会引入不必要的数值大小关系。通过独热编码,我们可以有效地表示类别型数据,并将其应用于各种机器学习算法中。

代码解释:

def load_data(path, transpose=True):
    data = loadmat(path)
    X = data['X']
    y = data['y']
    y = y.reshape(y.shape[0])
    
    if transpose:
        X = np.array([im.reshape((20,20)).T.reshape(400for im in X])
    return X, y

这段代码是用Python编写的函数,用于加载MATLAB数据文件中的数据(由于我们本次算法是用于识别手写字迹,所以需要从matlab类型的文件中获得训练的样本数据),通常用于神经网络算法中。让我逐句解释一下:

  1. def load_data(path, transpose=True)::这里定义了一个函数load_data ,它接受一个参数path表示文件路径,并有一个可选参数transpose,默认值为True。
  2. data = loadmat(path):这一行使用loadmat函数从MATLAB文件中加载数据,并将其存储在变量data中。
  3. X = data['X']y = data['y']:这两行将从MATLAB数据中提取出键为'X''y'的数据,分别存储在变量Xy中。
  4. y = y.reshape(y.shape[0]):这一行对y进行重塑操作,将其形状调整为(y.shape[0],),即将y变成一个一维数组。
  5. if transpose::这里开始一个条件语句,检查transpose参数是否为True。
  6. X = np.array([im.reshape((20,20)).T.reshape(400) for im in X]):这行代码是一个列表推导式,它对变量X中的每个图像im进行操作。首先,它将每个图像im重塑为一个20x20的矩阵,然后转置这个矩阵,最后再将其展平为一个长度为400的一维数组。这样做是为了将图像数据转换为神经网络输入所需的形式。
  7. return X, y:最后,函数返回处理后的输入数据X和标签数据y

这段代码的作用是加载MATLAB数据文件中的图像数据和标签,将图像数据转换为适合神经网络输入的形式,并返回处理后的数据。

X = np.insert(raw_x, 0, np.ones(raw_x.shape[0]), axis=1)
X.shape

这段代码看起来是在对输入数据raw_x进行处理,其中np.insert(raw_x, 0, np.ones(raw_x.shape[0]), axis=1)这部分代码的作用是在raw_x的第一列插入一列全为1的数据。让我们来解释一下X.shape的含义:

  • X: 这是经过处理后的特征矩阵,其中包含了原始特征数据raw_x以及插入的一列全为1的数据。
  • X.shape: 这是一个属性,用来表示矩阵X的形状,即它的维度和各个维度的大小。

假设raw_x原本是一个形状为 (m, n)  的矩阵,其中 m 表示样本数量,n 表示特征数量。经过插入一列全为1的操作后,X 的形状会变成 (m, n+1),即样本数量不变,特征数量增加了1。

因此,X.shape表示的是经过处理后的特征矩阵X的形状,它是一个 (m, n+1) 的矩阵,其中 m 表示样本数量,n+1 表示特征数量(包括了插入的一列全为1的数据)。

以上我们就准备好了初步的原始数据,之所以要进行X = np.insert(X, 0, np.ones(X.shape[0]), axis=1)的操作(也就是在特征矩阵的最前面加上一个全由1组成的向量),是因为此处要估计线性函数截距的影响。了解线性函数的同学都知道,线性函数(就算是曲线)有经过原点的和不经过原点的,经过原点的函数自然截距为0,但是如果该函数不经过原点呢?考虑到这个影响,我们加上一列的截距向量,默认为1,因为1*实际的截距=截距本身,而对于截距为0时1 * 0 还是等于0,不会对函数本身造成影响,因此这个加1向量的操作是个点睛之笔。

5.算法组成Two-标签列编码

通过本文第二点中对于算法整体的讨论,我们知道,对于多分类问题,我们需要将真实值标签列经过特殊处理后才能拿到每一类所应对应的结果,因此,这个过程我们常常采用的是编码处理。

def expend_y(y):
   
# expend 5000*1 -> 5000*10
# y=2 -> y=[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
  
    
    res = []
    for i in y:
        tmp = np.zeros(10)
        tmp[i-1] = 1
        res.append(tmp)
    return np.array(res)


# 与expand_y(y)结果一致
# from sklearn.preprocessing import OneHotEncoder
# encoder = OneHotEncoder(sparse=False)
# y_onehot = encoder.fit_transform(y)
# y_onehot.shape 

y = expend_y(y)

这段代码是在进行对标签列进行编码处理,进行了一种称为独热编码(One-Hot Encoding)的处理方式。它接受一个参数y,该参数通常代表神经网络中的目标输出。在这个函数中,它通过循环遍历输入的y,对每个元素进行处理,最终返回一个经过处理的NumPy数组。

  1. res = []: 这一行创建了一个空列表res,用于存储处理后的结果。
  2. for i in y:: 这一行开始一个for循环,遍历输入的y中的每个元素,将当前元素赋值给变量i
  3. tmp = np.zeros(10): 在每次循环中,这一行创建了一个长度为10的全零NumPy数组tmp,用于存储处理后的结果。
  4. tmp[i-1] = 1: 这一行将tmp数组中索引为i-1的位置设为1,即将y中的元素值减一作为索引,设为1,实现了对目标输出的处理,通常用于独热编码。
  5. res.append(tmp): 将处理后的tmp数组添加到结果列表res中。
  6. return np.array(res): 最后,将结果列表res转换为NumPy数组并返回。这样,函数最终返回了一个经过处理的NumPy数组,其中每个元素都是一个长度为10的独热编码向量,用于表示神经网络的目标输出。

再次给出一下原理图

向量化标签.png

「原理:」

核心概念很简单:对于具有 n 个不同类别的分类变量,独热编码会创建一个 n 维的向量,其中只有一个元素是 1,其他元素都是 0。这样每个类别都被表示为一个唯一的向量,使得模型能够更好地理解分类变量之间的关系。

举个例子,假设有一个颜色属性,可能有三个类别:红、绿和蓝。通过独热编码,这三个类别会被表示为以下向量:

  • 红:[1, 0, 0]
  • 绿:[0, 1, 0]
  • 蓝:[0, 0, 1]

这样,在模型中使用独热编码后,每个颜色类别都被表示为一个独立的向量,避免了模型误解类别之间存在顺序或距离关系的可能性。

6.算法组成Three-自定义函数的准备

在正式进行神经网络算法之前,我们需要准备好一些子函数或者又叫做自定义函数

  1. 权重加载函数(目的:获得并初始化两个参数矩阵)
def load_weight(path):
    data = loadmat(path)
    return data['Theta1'], data['Theta2']
    
t1, t2 = load_weight('ex4weights.mat')
t1.shape, t2.shape   #((25, 401), (10, 26))

我们需要两个权重(即分别对应线性回归的参数矩阵和逻辑回归的参数矩阵)来进行后续神经网络算法的运行,在这里我们将该参数已经整理好放在ex4weights.mat文件里,那么如果我们没有先行整理出来或者没有拿到相关参数矩阵怎么办呢?如果你经验丰富的话,你可以考虑采用人工的手动设置参数,但如果你和我一样是个新手,建议还是采用「随机初始化的方法」进行参数的初始化,随机初始化可以遈避参数对称性,并且有助于避免神经网络陷入局部最优解。

2.合成一维数组(或向量)矩阵函数

def serialize(a, b):
    return np.concatenate((np.ravel(a), np.ravel(b)))

这段代码是一个Python函数,名为serialize,它接受两个参数ab,这两个参数是神经网络中的权重矩阵或其他数组。在函数内部,它使用NumPy库的ravel函数来将参数ab展平为一维数组,然后使用concatenate函数将这两个展平后的数组连接起来,形成一个新的一维数组。这个新的一维数组包含了参数ab中所有元素的值,实现了将两个数组序列化为一个一维数组的功能。在神经网络算法中,这种序列化操作可能用于将权重矩阵等参数转换为一维数组形式,方便存储或传输。

3.参数塑形矩阵(一切有关整体调整数据矩阵的操作或函数都是为了方便向量之间的计算)

def deserialize(seq):
    return seq[ : 25*401].reshape(25401), seq[25*401  : ].reshape(1026)

这段代码是一个Python函数,名为deserialize,它接受一个参数seq,这个参数通常是一个一维数组,可能是在神经网络中序列化后的参数数组。在函数内部,它通过NumPy库的切片操作和reshape函数将参数seq重新构造成两个矩阵。

第一行seq[:25*401].reshape(25, 401)seq数组的前25*401个元素切片出来,并将其重新形状为一个25行401列的矩阵。这可能是用于重建神经网络中的权重矩阵或参数矩阵。

第二行seq[25*401:].reshape(10, 26)seq数组的剩余部分切片出来,并将其重新形状为一个10行26列的矩阵。这也可能是用于重建神经网络中的另一个权重矩阵或参数矩阵。

总的来说,这段代码的作用是将一个一维数组序列化后的参数重新构造成两个特定形状的矩阵,以便在神经网络算法中使用。这种反序列化操作通常用于恢复神经网络的参数状态。

4.激活函数(这里使用的是Sigmoid函数)

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

这里就不做讲解了,有疑问的请看文章2

7.算法组成Four-神经网络之前向传播

nn_model.png
def feed_forward(theta, X):
    t1, t2 = deserialize(theta) # t1:(25, 401) t2:(10, 26)
    a1 = X # 5000*401
    
    z2 = a1 @ t1.T # 5000*25
    a2 = np.insert(sigmoid(z2), 0, np.ones(z2.shape[0]), axis=1# 5000*26
    
    z3 = a2 @ t2.T # 5000*10
    h = sigmoid(z3) # 5000*10
    return a1, z2, a2, z3, h

这段代码是一个Python函数feed_forward,它实现了神经网络的前向传播过程。

  1. def feed_forward(theta, X):: 这一行定义了一个函数feed_forward,它接受两个参数thetaX,分别代表神经网络的参数和输入数据。
  2. t1, t2 = deserialize(theta) # t1:(25, 401) t2:(10, 26): 这一行调用了deserialize函数来将参数theta解析成两个矩阵t1t2,分别表示神经网络的第一层和第二层的权重。
  3. a1 = X # 5000*401 : 将输入数据X赋值给变量a1,其中X的维度是5000*401。
  4. z2 = a1 @ t1.T # 5000*25: 计算第一层的加权输入z2,通过将a1t1的转置相乘得到,得到一个5000*25的矩阵。
  5. a2 = np.insert(sigmoid(z2), 0, np.ones(z2.shape[0]), axis=1) # 5000*26: 将经过激活函数(这里使用了sigmoid函数)处理后的z2插入一列全为1的偏置项,并赋值给a2,形成一个5000*26的矩阵。
  6. z3 = a2 @ t2.T # 5000*10: 计算第二层的加权输入z3,通过将a2t2的转置相乘得到,得到一个5000*10的矩阵。
  7. h = sigmoid(z3) # 5000*10: 将z3经过激活函数处理(这里也使用了sigmoid函数),得到最终的输出h,形成一个5000*10的矩阵。
  8. return a1, z2, a2, z3, h: 最后,函数返回了a1(第一层的激活值)、z2(第一层的加权输入)、a2(第二层的激活值,包括偏置项)、z3(第二层的加权输入)和h(神经网络的输出)这几个值。

这段代码实现了神经网络的前向传播过程,计算了输入数据经过各层权重和激活函数后的输出结果。

细化原理图

20200309102932420.png

结果:

array([[1.12661530e-04, 1.74127856e-03, 2.52696959e-03, ..., 4.01468105e-04, 6.48072305e-03, 9.95734012e-01], [4.79026796e-04, 2.41495958e-03, 3.44755685e-03, ..., 2.39107046e-03, 1.97025086e-03, 9.95696931e-01], [8.85702310e-05, 3.24266731e-03, 2.55419797e-02, ..., 6.22892325e-02, 5.49803551e-03, 9.28008397e-01], ..., [5.17641791e-02, 3.81715020e-03, 2.96297510e-02, ..., 2.15667361e-03, 6.49826950e-01, 2.42384687e-05], [8.30631310e-04, 6.22003774e-04, 3.14518512e-04, ..., 1.19366192e-02, 9.71410499e-01, 2.06173648e-04], [4.81465717e-05, 4.58821829e-04, 2.15146201e-05, ..., 5.73434571e-03, 6.96288990e-01, 8.18576980e-02]])

[简单说一下,根据输入的X矩阵以及标签列Y的总类数,该结果其实是(5000,10)的二维数组,5000对应样本数量,10代表这5000个样本中每个样本对于各个类别的预测值]

以上就是我们神经网络算法中的前向传播过程,通过该过程,我们可以得到每个样本对应的每种种类的预测值,但是我们通常不直接采用该结果作为最终判定标准,因为本文是因为参数矩阵已经给出并且本文是做为范例所以算法大致只运行一次,但大多数的项目或者其他背景下我们通常都不提前训练好参数,因为过早的训练参数可能会导致算法在后面的不断训练拟合的过程中产生过拟合问题,大多数我们都是采用随机化参数的方法,所以我们想要在后续对神经网络算法的参数进行进一步的调优那么我们就要利用好前向传播的损失值和梯度下降对神经网络算法进行反向传播,以此让算法本身更好地去调参与拟合函数。

8.算法组成Five-神经网络之反向传播

定义以及目的:

反向传播是神经网络算法中非常重要的一部分。它是一种通过「计算梯度」「更新神经网络参数」的技术。在训练神经网络时,我们首先通过前向传播计算出模型的预测值,然后使用反向传播来计算损失函数相对于每个参数的梯度。这些梯度指示了参数应该如何调整才能最小化损失函数。

反向传播的目的是「优化神经网络的参数」,使其能够更好地拟合训练数据,「提高模型的准确性和泛化能力」 。通过不断迭代前向传播和反向传播过程,神经网络可以学习到数据中的模式和特征,从而提高其性能。反向传播是深度学习中的关键步骤,帮助神经网络不断优化自身以适应不同的任务和数据。

(1)代价函数:计算输出层的误差

cost.png
def cost(theta, X, y):
    h = feed_forward(theta, X)[-1]
    tmp = -y * np.log(h) - (1-y) * np.log(1-h)
    return tmp.sum() / y.shape[0]
cost(theta, X, y)

# 这里不做讲解,详情看文章2

正则化代价函数-减轻欠拟合与过拟合问题

regularized_cost.png
def regularized_cost(theta, X, y, l=1):
    t1, t2 = deserialize(theta)
    m = X.shape[0]
    
    reg1 = np.power(t1[:, 1:], 2).sum() / (2 * m)
    reg2 = np.power(t2[:, 1:], 2).sum() / (2 * m)
    
    return cost(theta, X, y) + reg1 + reg2
regularized_cost(theta, X, y) 

这段代码是一个正则化代价函数,名为regularized_cost,它计算带有正则化项的神经网络成本函数。让我逐句解释一下:

  1. def regularized_cost(theta, X, y, l=1):: 这是定义一个函数regularized_cost,它接受四个参数thetaXyl(默认为1)。
  2. t1, t2 = deserialize(theta): 这一行将参数theta解析成两个矩阵t1t2,通常用于表示神经网络的权重。
  3. m = X.shape[0]: 这一行计算训练样本的数量,即矩阵X的行数。
  4. reg1 = np.power(t1[:, 1:], 2).sum() / (2 * m): 这一行计算第一层权重的正则化项。它首先对t1矩阵的除第一列外的所有元素进行平方操作,然后对结果求和并除以2 * m
  5. reg2 = np.power(t2[:, 1:], 2).sum() / (2 * m): 这一行计算第二层权重的正则化项,类似于上一行。
  6. return cost(theta, X, y) + reg1 + reg2: 这一行返回带有正则化项的总成本。它调用了另一个名为cost 的函数,用于计算未正则化的成本,然后将其与reg1reg2相加。
  7. regularized_cost(theta, X, y): 这一行调用了regularized_cost函数,传入参数thetaXy。函数将返回带有正则化项的总成本。

(2)计算梯度

def sigmoid_gradient(z):
    return sigmoid(z) * (1 - sigmoid(z))
sigmoid_gradient(0)

这段代码是计算Sigmoid函数的梯度,用于反向传播算法中的梯度计算。

  1. def sigmoid_gradient(z):: 这是定义一个函数sigmoid_gradient,它接受一个参数z,这个参数通常是神经网络中的某个中间值。
  2. return sigmoid(z) * (1 - sigmoid(z)): 这一行计算了Sigmoid函数的导数。Sigmoid函数的导数可以通过Sigmoid函数本身来表示,即Sigmoid函数的输出乘以1减去Sigmoid函数的输出。

在反向传播过程中,我们需要计算激活函数(如Sigmoid函数)对于某个中间值的导数,以便计算梯度并更新神经网络的参数。这个函数将帮助我们计算Sigmoid函数的梯度,以便在反向传播过程中使用。

(3)更新梯度

屏幕截图 2024-04-17 223009.png
def gradient(theta, X, y):
    t1, t2 = deserialize(theta)
    m = X.shape[0]
    
    delta1 = np.zeros(t1.shape) # 25*401
    delta2 = np.zeros(t2.shape) # 10*26
    
    a1, z2, a2, z3, h = feed_forward(theta, X)
    
    for i in range(m):
        a1i = a1[i] # 1*401
        z2i = z2[i] # 1*25
        a2i = a2[i] # 1*26

        hi  = h[i]  # 1*10
        yi  = y[i]  # 1*10
        d3i = hi - yi # 1*10,输出层的误差
        
        z2i = np.insert(z2i, 0, np.ones(1))
        d2i = t2.T @ d3i * sigmoid_gradient(z2i) # 1*26 隐藏层的误差
        
        # careful with np vector transpose
        delta2 += np.matrix(d3i).T @ np.matrix(a2i)
        delta1 += np.matrix(d2i[1:]).T @ np.matrix(a1i)
    
    return serialize(delta1, delta2)
d1, d2 = deserialize(gradient(theta, X, y))
d1.shape, d2.shape

让我逐句解释一下这段代码:

  1. def gradient(theta, X, y): - 定义了一个名为gradient的函数,它接受三个参数thetaXy
  2. t1, t2 = deserialize(theta) - 将参数theta反序列化为t1t2两个变量。
  3. m = X.shape[0] - 获取输入数据X的行数,赋值给变量m
  4. delta1 = np.zeros(t1.shape) - 创建一个大小为t1的零矩阵,赋值给delta1,这里t1的形状为25*401。
  5. delta2 = np.zeros(t2.shape) - 创建一个大小为t2的零矩阵,赋值给delta2,这里t2的形状为10*26。
  6. a1, z2, a2, z3, h = feed_forward(theta, X) - 调用feed_forward函数,返回前向传播的结果,分别赋值给a1z2a2z3h
  7. for i in range(m): - 循环遍历数据集的样本。
  8. a1i = a1[i]z2i = z2[i]a2i = a2[i]hi = h[i]yi = y[i] - 分别获取当前样本的输入、中间层输出、隐藏层输出、神经网络输出和标签。
  9. d3i = hi - yi - 计算输出层的误差。
  10. z2i = np.insert(z2i, 0, np.ones(1)) - 在z2i的开头插入一个值为1的元素,用于偏置。
  11. d2i = t2.T @ d3i * sigmoid_gradient(z2i) - 计算隐藏层的误差。
  12. delta2 += np.matrix(d3i).T @ np.matrix(a2i) - 累加计算输出层权重的梯度。
  13. delta1 += np.matrix(d2i[1:]).T @ np.matrix(a1i) - 累加计算隐藏层权重的梯度。
  14. return serialize(delta1, delta2) - 序列化delta1delta2,并返回结果。

这段代码主要是一个神经网络的反向传播过程,用于计算梯度并更新网络的权重。

tips:

1.反向传播为什么要通过链式法则不停的求偏导?

反向传播是深度学习中用于计算神经网络权重梯度的关键算法。在神经网络中,通过链式法则来计算梯度是因为「神经网络是一个复合函数,由多个层组成,每一层都包含激活函数和权重参数」。在反向传播过程中,我们需要计算损失函数对每个参数的偏导数,以便通过梯度下降等优化算法来更新参数。

通过链式法则,我们可以将整个网络的梯度计算分解为每一层的局部梯度计算,然后将这些局部梯度相乘以获得最终的梯度。这种分解的过程使得计算梯度变得更加高效和可行,特别是在深度神经网络中,层数较多时。

2.为什么隐藏层的误差项可以通过前一层的误差项加权和当前层的激活函数导数计算得到?

当我们讨论神经网络中隐藏层的误差项时,我们需要考虑如何将来自后一层的误差项传播回当前隐藏层。这种传播是通过「权重连接」实现的,因为隐藏层的神经元与后一层神经元之间存在权重连接。

具体来说,隐藏层的误差项可以通过后一层的误差项加权和当前隐藏层的激活函数导数计算得到的原因在于「反向传播算法的链式法则」。这个过程可以被理解为误差项沿着网络反向传播,以计算每一层的误差项。

当我们需要计算隐藏层的误差项时,我们希望「考虑后一层的误差项如何影响当前隐藏层的误差。这种影响是通过后一层到当前隐藏层的权重连接实现的。通过将后一层的误差项与这些权重相乘,我们可以获得对当前隐藏层误差的贡献。」

同时,我们还需要考虑当前隐藏层的激活函数导数。这是因为激活函数的导数反映了神经元输出对输入的敏感度,即激活函数的斜率。将后一层的误差项乘以当前隐藏层的激活函数导数,可以有效地传播误差并考虑激活函数对误差的影响。

综合来看,隐藏层的误差项通过后一层的误差项加权和当前隐藏层的激活函数导数计算得到,是为了有效地传播误差并更新隐藏层的权重,以使神经网络逐渐优化并提高性能。这种计算方式是反向传播算法的基础,通过链式法则实现了误差的传播和更新。

(4)正则化梯度下降

regularized_gradient.jpg
def regularized_gradient(theta, X, y, l=1):
    m = X.shape[0]
    delta1, delta2 = deserialize(gradient(theta, X, y))
    delta1 /= m
    delta2 /= m
    
    t1, t2 = deserialize(theta)
    t1[:, 0] = 0
    t2[:, 0] = 0
    
    delta1 += l / m * t1
    delta2 += l / m * t2
    
    return serialize(delta1, delta2)

函数的定义,函数名为regularized_gradient,接受四个参数:theta是模型参数,X是特征矩阵,y是标签,l是正则化参数,默认值为1。

  1. m = X.shape[0]:获取特征矩阵X的行数,即样本数量,存储在变量m中。
  2. delta1, delta2 = deserialize(gradient(theta, X, y)):调用deserialize函数将梯度gradient(theta, X, y)拆分为两部分,并将结果分别存储在delta1delta2中。
  3. delta1 /= m 和 delta2 /= m:将delta1delta2除以样本数量m,以计算平均梯度。
  4. t1, t2 = deserialize(theta):调用deserialize函数将参数theta拆分为两部分,并将结果分别存储在t1t2中。
  5. t1[:, 0] = 0 和 t2[:, 0] = 0:将参数t1t2的第一列(偏置项)设为0,因为正则化通常不应用于偏置项。
  6. delta1 += l / m * t1 和 delta2 += l / m * t2:将正则化项添加到梯度中,这里使用L2正则化,通过乘以正则化参数l除以样本数量m乘以参数t1t2
  7. return serialize(delta1, delta2):调用serialize函数将更新后的梯度delta1delta2重新组合为一个参数向量,并返回结果。

这段代码实现了对梯度进行正则化处理,通过添加正则化项来防止过拟合,并确保模型泛化能力。正则化有助于控制模型的复杂度,避免参数过大,从而提高模型的泛化性能。

9.算法组成Six-梯度检查

定义:

梯度检查是一种用于验证反向传播算法实现的正确性的技术。在神经网络训练过程中,梯度是损失函数相对于模型参数的偏导数,它指导着参数更新的方向。梯度下降算法依赖于正确计算的梯度来更新模型参数,以最小化损失函数。

梯度检查的基本思想是通过数值方法来估计梯度,然后将这个估计值与反向传播算法计算得到的梯度进行比较。如果两者非常接近,那么反向传播算法实现可能是正确的;如果存在显著差异,那么就可能存在错误。

具体来说,梯度检查的步骤如下:

  1. 对于每个参数

  2. 使用反向传播算法计算相同参数

  3. 将数值梯度和反向传播算法计算得到的梯度进行比较,通常使用它们之间的差异来评估算法的准确性。

梯度检查是一种验证神经网络实现的常用技术,尤其在实现复杂的神经网络结构时,有助于排除梯度计算中的错误。通过梯度检查,我们可以增强对神经网络模型的信心,确保它们能够有效地学习和适应数据。

代码复现:

def gradient_checking(theta, X, y, epsilon, regularized=False):
    m = len(theta)
    def a_numeric_grad(plus, minus, regularized=False):
        if regularized:
            return (regularized_cost(plus, X, y) - regularized_cost(minus, X, y)) / (epsilon*2)
        else:
            return (cost(plus, X, y) - cost(minus, X, y)) / (epsilon*2)
    
    theta_matrix = expand_array(theta)
    epsilon_matrix = np.identity(m) * epsilon # identity单位矩阵
    plus_matrix = theta_matrix + epsilon_matrix
    minus_matrix = theta_matrix - epsilon_matrix
    
    approx_grad = np.array([a_numeric_grad(plus_matrix[i], minus_matrix[i], regularized) 
                            for i in range(m)])
    analytic_grad = regularized_gradient(theta, X, y) if regularized else gradient(theta, X, y)
    diff = np.linalg.norm(approx_grad - analytic_grad) / np.linalg.norm(approx_grad + analytic_grad)
    
    print('If your backpropagation implementation is correct,\nthe relative difference will be smaller than 10e-9 (assume epsilon=0.0001).\nRelative Difference: {}\n'.format(diff))
    
gradient_checking(theta, X, y, epsilon=0.0001, regularized=True# 慢

这段代码是用于执行梯度检查的函数。让我逐句解释一下:

  1. def gradient_checking(theta, X, y, epsilon, regularized=False)::这是一个名为gradient_checking的函数,它接受参数theta(模型参数)、X(输入特征)、y(标签)、epsilon(用于计算数值梯度的微小增量)、regularized(指示是否使用正则化)。
  2. m = len(theta): 这里计算了参数theta的长度,即模型参数的数量。
  3. def a_numeric_grad(plus, minus, regularized=False)::这是一个内部函数a_numeric_grad,用于计算数值梯度。它接受两个参数plusminus,分别表示带有微小增量的参数,根据这些参数计算数值梯度。
  4. theta_matrix = expand_array(theta): 将参数theta扩展为矩阵形式,以便进行矩阵运算。
  5. epsilon_matrix = np.identity(m) * epsilon: 创建一个单位矩阵乘以微小增量epsilon,用于构建数值梯度计算所需的矩阵。
  6. plus_matrix = theta_matrix + epsilon_matrixminus_matrix = theta_matrix - epsilon_matrix:分别计算带有微小增量和减量的参数矩阵。
  7. approx_grad = np.array([a_numeric_grad(plus_matrix[i], minus_matrix[i], regularized) for i in range(m)]):计算数值梯度的近似值,通过调用a_numeric_grad函数来计算每个参数的数值梯度。
  8. analytic_grad = regularized_gradient(theta, X, y) if regularized else gradient(theta, X, y): 计算解析梯度,根据是否使用正则化选择调用regularized_gradientgradient函数。
  9. diff = np.linalg.norm(approx_grad - analytic_grad) / np.linalg.norm(approx_grad + analytic_grad): 计算数值梯度和解析梯度之间的相对差异。
  10. print('If your backpropagation implementation is correct,\nthe relative difference will be smaller than 10e-9 (assume epsilon=0.0001).\nRelative Difference: {}\n'.format(diff)): 打印出梯度检查的结果,如果反向传播实现正确,相对差异应该小于10e-9。

这段代码的主要目的是通过比较数值梯度和解析梯度之间的差异来验证反向传播算法的正确性。如果差异很小,则说明反向传播实现正确。

以上就是机器学习神经网络算法的全部基础步骤和讲解,文中有些地方笔者可能解释不到位还请看官海涵,如果有问题,欢迎在评论区和笔者交流!

点击关注公众号,“技术干货” 及时达!

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/169731
 
177 次点击