当前位置: 首页 > news >正文

AI与机器学习ML:利用Python 从零实现神经网络

自线性回归以来,我们已经涵盖了很多领域。在本期中,我们将开始了解神经网络内部工作原理的旅程*。*

如果一个人试图了解任何使用生成式 AI 的工具、应用程序、网站或其他系统的内部工作原理,那么掌握神经网络的架构至关重要。在这个故事中,我们将讨论神经网络的所有主要组件,以及如何在 Python 中构建神经网络对象。

什么是神经网络?

None

神经网络是一种机器学习算法,它以类似于人脑运作方式的方式对数据进行建模。神经网络由一系列相互连接的节点组成,很像大脑中的神经元。信息流经每个节点,纵,并产生输出。此过程会发生多次,在每次传递期间,网络会根据结果对某些目标变量的偏离目标程度来调整信息的处理方式。神经网络有几种类型,在本文中,我们将重点介绍多层感知器 (MLP)。但是,我们将讨论的所有概念都将与所有类型的神经网络相关。

层 & 节点

典型的神经网络将由一个输入层、一个输出层和一个或多个隐藏层*(输入层和输出层之间的层)组成。这就是 Deep Learning 一词的由来。每一层都将由一系列节点组成。在输入层中,节点表示特征变量的向量。在每一层之间,数据使用特定函数(通常是线性函数)进行转换,其中权重偏差项是随机生成的。然后,此函数的输出通过另一个称为激活函数的函数。将 input 层之后的后续层视为原始数据的复杂转换。最后一层称为输出层,我们在其中预测模型的目标变量。我们预测的变量类型将决定我们在最后一层和输出层之间使用的激活函数类型。数据通过网络后,我们可以通过计算损失函数来了解它对目标变量的预测效果如何。通过一个称为反向传播的过程,**损失函数的结果用于调整权重和偏置项,从而提高网络的性能。这种情况发生了很多次,以最小化损失函数的结果,直到它收敛到最小值。这称为梯度下降。*

我确信这需要解开很多东西。我们来看看一个包含简单数据集的示例,而不是对每个步骤进行深入研究。

简单数据的示例

我们的数据集将有 10 个观察值,其中包含三个特征变量和一个二进制目标变量。X1、X2、X3 和 Y 将表示这些变量。我们将在具有三个层的网络上训练这些数据。第一层将有三个节点,代表我们的三个特征变量。下一层(*隐藏层)*将有两个节点,最后,我们的输出层中将有一个节点。查看下面的网络视觉效果:

None

图片由作者提供

我们将第一层中的节点表示为 X1、X2 和 X3。在隐藏层中,我们将用 H1 和 H2 表示节点。最后,我们的输出层将用 Y_hat 表示。

作为参考,我们将使用的数据如下。在这个例子中,我们将前三个观测值一个一个地通过网络传递,每次传递后,我们执行反向传播并记录损失函数的收敛性。

None

图片由作者提供

第一关

我们的第一个观测值的每个变量具有以下值:

  • X1:1
  • X2:10
  • X3:5
  • Y:0

让我们将这些插入到第一层的节点中。

None

图片由作者提供

请注意这三个节点是如何连接到隐藏层中的第一个节点和第二个节点的。在幕后,我们将执行以下作:

  1. 将值代入两个线性函数(每个节点一个)。我们将用 z1 和 z2 表示输出
  2. 通过 sigmoid 函数(激活函数)传递这些线性函数的输出。我们将用 a1 和 a2 表示它们

让我们从两个线性函数开始。在最简单的形式中,它看起来像这样:

None

图片由作者提供

WX 表示随机权重和输入节点值的点积,而 B 表示随机偏差项。输入层和隐藏层之间的随机权重和偏差将是:

  • 第一组权重:0.1、0.2、0.3
  • 偏差 1:0.1
  • 第二组权重 2:0.4、0.5、0.6
  • 偏差 2:0.2

现在,让我们将输入值代入到每个方程中。请注意,我将第一组权重称为 w11、w12 和 w13。换句话说,变量中的数字表示它属于哪组权重以及它在该集中表示的权重。让我们一步一步地了解 z1。

z1 = (w11 * x1) + (w12 * x2) + (w13 * x3) + b1

  • 插入随机生成的权重和偏差

z1 = (0.1* x1) + (0.2* x2) + (0.3* x3) + 0.1

  • 插入输入值。

z1 = (0.1* 1) + (0.2 * 10) + (0.3 * 5) + 0.1

  • 解决

z1 = 3.7

对 z2 进行相同的练习将得到:

z2 = 8.6

我们已经完成了 input 层和 hidden 层之间的连接。下一步是通过激活函数传递 z1 和 z2。为什么我们需要引入激活函数?这非常重要;如果没有激活函数,我们的神经网络将是一个过于复杂的线性模型。通过激活函数传递线性函数的输出,我们在数据中引入了非线性关系,从而捕获了线性模型单独无法发现的复杂性。有几种不同的激活函数可供选择,在本文中,我们将使用 ReLU 函数(Rectified Linear Unit)。

None

图片由作者提供

在激活函数方面,它没有比 ReLU 更简单的了。如果 x 为正,则输出与 x 相同,否则为 0。ReLU 可以说是隐藏层最流行的激活函数,因为它引入了非线性,提高了计算效率,并减轻了梯度消失(我们将在后面讨论)。我们将这些输出称为 a1 和 a2。根据我刚才描述的定义,您能否确认第一次传递中的 a1 和 a2 会是什么?与 z2 和 z2 相同,因为它们是正数。

A1: 3.7

A2: 8.6

让我们来看看我们的网络在第一轮中是什么样子的。

None

图片由作者提供

现在,让我们完成隐藏层和输出层之间的桥接。正如我们之前所做的那样,让我们获取隐藏层值,并通过具有随机权重和偏差项的线性函数传递它们。唯一的区别是,除了权重和偏差之外,我们只需要执行一个线性变换,因为我们只处理一个输出变量。我们将权重和偏差项称为 hw1 、 hw2 、 hb 和输出 z3 。

  • 隐藏图层权重:0.7、0.8
  • 隐藏层偏差项:0.3

把它们放在一起:

  • Z3 = (HW1 * A1) + (HW2 * A2) + HB
  • z3 = (0.7 * 3.7) + (0.8 * 8.6) + 0.3
  • z3 = 9.77

我们快到了!现在,让我们将 z3 传递给最终的激活函数,这将是 sigmoid 函数。为什么不再使用 ReLU 呢?我们的模型正在使用具有二进制目标变量的数据进行训练。为了通过反向传播过程为预测二进制目标的模型调整我们的权重和偏差项,我们需要一个产生概率的激活函数。当我们讨论在反向传播中查找导数的过程时,这将更加清楚。现在,让我们演示一下 sigmoid 函数的作用:

None

图片由作者提供

当我们为 z 代入 9.77 时,我们得到 0.99。换句话说,我们的模型认为这是对 “1” 的非常有把握的预测。在这种情况下,我们的模型当前处于关闭状态,但这是意料之中的。毕竟,权重和偏差项是在第一次传递期间随机生成的。

None

图片由作者提供

无论 z 是什么值,sigmoid 函数的输出都将是介于 0 和 1 之间的数字。这最终是预测我们的目标变量的原因。一旦我们有了最终模型,任何高于 0.5 的输出都被预测为“1”,而所有其他输出被预测为“0”。更重要的是,我们如何使用 sigmoid 函数的输出通过反向传播来调整权重和偏置项。现在让我们谈谈它。

反向传播

给我的读者一个提示:如果你在对所涵盖的内容有深入的了解的情况下走到了这一步,我必须给你严肃的道具。当我第一次了解神经网络时,我花了很多时间来掌握神经网络的架构及其所有组件如何协同工作。

现在让我们深入了解是什么让神经网络真正神奇,即反向传播。这是神经网络学习和调整权重和偏差项以更好地拟合目标变量的框架。我们结束了第一次传递的最终输出,即 sigmoid 函数产生预测的概率。对于第一个输出,我们得到 0.99。

反向传播的第一步是计算损失函数。对于二进制问题,典型的损失函数是二进制交叉熵 (BCE):

None

图片由作者提供

在为这个观察插入 y 的目标变量并为 y_hat 的 sigmoid 输出 0.99 之后,我们剩下:

  • 损失 = - (0 * log(0.999943) + (1–0) * log(1–0.999943)
  • 损失 = - 1 * -log(1–0.999943)
  • 亏损 = 9.77

请注意我们如何称这个变量为 loss。我们的目标是最小化此值。我们很快就会回到这个问题。接下来,让我们演练一下调整权重和偏差项的过程。

梯度下降

调整我们网络中的权重和偏差项将通过一个称为 Gradient Descent 的迭代过程来完成。请看下面的视觉效果。这基本上就是我们在这个过程中要努力实现的目标:

None

图片由作者提供

Y 轴表示损失函数的值,而 X 轴代表我们模型中的任何给定权重或偏差。我们可以通过损失函数(二进制交叉熵)实现一个最小值,要找到它,我们必须更改权重和偏置项的迭代值。

最大的问题是,我们如何改变这些值?再看一遍图表。在图中,我们只看到一个权重变量 (W)。尽管如此,我们的网络还有几个权重和偏差项,因此我们无法从技术上可视化这个过程,因为它将是多个维度。但是,只需在下面使用一个术语即可轻松说明此概念。“星号”是考虑到网络和数据的结构,我们的损失被最小化到尽可能小的值的地方。当我们开始训练过程时,理论上该点将在曲线上的位置高得多。如果我们相应地调整权重和偏置项,损失函数应按以下方式变化:

None

GIF 由作者提供

我们究竟如何更改权重和偏差项?我们现在将深入研究这个问题。

计算梯度和链式规则

None

照片由 卡琳·阿维蒂相 on Unsplash

让我们回顾一下我们的理论起点。有一条切线向我们显示网络的当前斜率。更具体地说,损失函数相对于我们网络参数的变化速率。这的正式术语是 derivative。当您听到 derivative 一词时,请知道它与短语*“change relative to”“rate of change”*同义。

None

图片由作者提供

当我们接近最小值时,请注意导数的变化:

None

GIF 由作者提供

请注意虚线是如何变得越来越水平的;这意味着导数正在缩小或接近零。这就是我们希望看到的,因为我们执行越来越多的反向传播。

我们将使用损失函数相对于每个权重和偏差项的导数(梯度)作为调整因子,主要有两个原因:

  • 它揭示了我们需要调整哪个方向以接近最小损失
  • 它揭示了我们应该如何调整以接近最小损失的程度。换句话说,绝对值的导数(梯度)越大,我们离最小值就越远;因此,我们需要以更高的速度进行调整。

此过程有一些注意事项。由于我们不仅更改了一个参数,而且更改了多个参数,因此一次更改所有参数可能会导致我们的损失函数以不稳定的速率变化,这可能导致超过最小值。因此,我们将不断更新我们的网络。因此,我们将引入一个介于 0 和 1 之间的学习率,以降低我们调整参数的速率。这个术语更正式地称为 alpha。

在我们开始调整权重之前,请再看一下网络的结构。

None

图片由作者提供

我们如何进行预测?这对于理解参数的导数计算至关重要。回顾我们网络的第一次传递可能没有什么坏处。

就在预测之前,我们通过 sigmoid 函数传递 z3。为了得到 z3,我们通过具有随机权重和偏差的线性函数传递 a1 和 a2。为了获得 a1 和 a2,我们将三个输入变量通过两个具有随机权重和偏差的独立线性函数传递,然后通过 ReLU 函数传递这两个输出。如果你仔细想想,这些参数都以某种方式相互关联;因此,当一个发生变化时,它会影响其他变化的方式。这使得计算相对于 Loss 的导数有点复杂;但是,理解链式规则肯定会澄清这一点。

简单来说,我们网络的预测是函数的函数,等等。因此,我们必须将函数链接在一起才能正确计算导数。想一想,让我们以 input 层和 hidden 层之间的桥梁为例。如果我们在第一个线性变换中只调整其中一个权重:

  • 它会影响我们通过 ReLU 激活函数传递的值。
  • 更改我们插入到下一个线性变换中的值
  • 更改该线性转换的输出
  • 更改 sigmoid 激活函数的输出
  • 更改预测。

现在让我们倒过来工作,我们的第一个任务是找到隐藏层和输出层之间桥接的权重和偏置项的导数。我们将在下表中进行跟踪。此外,我们将假设学习率为 0.1

None

图片由作者提供

首先,我们必须计算相对于 sigmoid 函数变化的损失变化。请参阅下面的原始表单,并在插入我们之前找到的关联值后。

None

图片由作者提供

接下来,我们将计算 Loss 关于 z3 的导数。请注意前一个导数是如何包含的。这就是链式规则的本质。

None

图片由作者提供

进一步简化:

None

图片由作者提供

现在我们可以开始计算权重和偏置项的梯度了!现在,我们来求解 loss 相对于输出层的权重和 bias 的变化。请注意,有三个单独的方程式,每个方程对应于此特定图层的每个参数。请记住,我们已经进行了多次计算来获得其中一些项,因此虽然这些方程看起来相对较短,但仍包含大量内容。另一个需要考虑的注意事项是 z 关于 w1 或 w2 的导数分别只是 a1 和 a2。关于这部分,我最后要注意的是,偏差项以恒定的速率改变 z,因为它没有加权因子。换句话说,当偏差增加 1 时,z 也会增加。

输出层 w1:

None

图片由作者提供

输出层 w2:

None

图片由作者提供

输出层偏置:

None

图片由作者提供

让我们重新审视我们的表。以下是我们目前计算的梯度:

None

图片由作者提供

我们通过了第一层,还有一层要进行。下一步是计算 Loss 函数相对于 ReLU 激活函数的导数。使用您目前所知道的,您知道这应该是什么样子吗?回想一下我们刚刚所做的工作,并考虑 ReLU 函数的输出在我们的网络中的位置。

现在,要获得下一组权重和偏置项,我们首先需要找到损失函数中关于 a1 和 a2 的变化。请注意,z 相对于 a1 和 a2 的变化只是权重 w1 和 w2。

None

图片由作者提供

ReLU 函数的两个调用的输出都转到 a1 和 a2。因此,我们需要了解 a1 相对于 ReLU 输出(我们称为 z1)的变化,以及 a2 相对于另一个 ReLU 输出(我们称为 z2)的变化。它们是 3.7 和 8.6,由于它们只是数字,因此 a1 和 a2 将相对于 z1 和 z2 以 1 的速率变化。话虽如此,关于 z1 和 z2 的损失变化也将是 0.69996 和 0.79995。

现在我们进入最后一组权重和偏差项,更具体地说,我们的隐藏权重和偏差项。此时,我们不是展示所有八个参数的计算,而是演示如何获得第一个隐藏节点的第一个权重的梯度和该节点的偏置项。碰巧这两个值都是相同的?您能弄清楚为什么吗?我们知道 bias 项只是一个数字,因此它只会以恒定的速率改变输出。另一方面,a1 和隐藏节点 1 权重 1 之间的关系取决于 x1 的值,在本例中为 1。

隐藏节点 1 — 权重 1:

None

图片由作者提供

隐藏节点 1 — 偏置项:

None

图片由作者提供

现在,我们在第一次通过后有了渐变!它们在这里,请注意,我还包括了学习率调整:

None

图片由作者提供

在进入 Python 之前,还有一点要注意 — Batches

我们可以多做几次调整,但会变得非常重复!相反,让我们准备好在 Python 中构建自定义神经网络对象。我想解决的最后一点是在批处理级别更新网络参数。我刚才向读者演示的内容表明,您可以在一次传递后更新神经网络中的参数。在现实世界中,情况往往并非如此。相反,大多数数据科学家或机器学习工程师将通过网络以我们所谓的批处理形式提供许多观察结果。每次传递后,都会计算损失函数。批次结束后,我们计算损失函数的平均值,并使用它来更新参数。

构建神经网络对象

让我们从我们的库和初始化函数开始。以下是有关这些属性如何工作的一些附加说明。

  • **input_size:**这应等于您在数据集中使用的要素数量。
  • hidden_layers: 这将是每层中节点数的列表。因此,此列表的长度也将是网络中隐藏层的数量。
  • num_batches: 一个整数,表示每个 epoch 的数据将拆分为的子集数。请记住,在输入一批中的所有观测值之前,我们不会调整权重,因此请将其视为在完整数据传递中将发生的调整次数。
  • 时代: 网络的完整传递次数
  • early_stopping_rounds: 在训练网络时,我们必须确保没有对训练数据进行过拟合。因此,我们将添加一个方法,该方法采用此属性,并在一定轮数后损失没有改善时停止训练。

其余属性将在我们训练和初始化模型时填充。

import numpy as np
import pandas as pd
import matplotlib.pyplot as pltclass DIYNeuralNetwork:def __init__(self, input_size, hidden_layers, num_batches, epochs, early_stopping_rounds=None):self.input_size = input_sizeself.hidden_layers = hidden_layersself.num_batches = num_batchesself.epochs = epochsself.early_stopping_rounds = early_stopping_roundsself.layers = []self.train_losses = []self.test_losses = []self._init_weights()
初始化权重和偏差项

虽然我们只是生成随机项,但让我们谈谈它背后的直觉。权重是从正态分布中随机选择的,因此它们很可能是介于 -3 到 3 之间的值。此外,我们将它们乘以 0.01,因此典型范围变为 -0.03 到 0.03。对于偏差项,它们都从 0 开始。

这一切的主要原因是为了防止所谓的梯度爆炸。当梯度足够大,导致模型开始显著超过最小损失时,就会发生这种情况。

def _init_weights(self):layer_structure = [self.input_size] + self.hidden_layers + [1]self.weights = []self.biases = []for i in range(len(layer_structure) - 1):weight = np.random.randn(layer_structure[i], layer_structure[i+1]) * 0.01bias = np.zeros(layer_structure[i+1])self.weights.append(weight)self.biases.append(bias)
选择函数

为 ReLU、ReLU 导数、sigmoid 和 BCE 创建了方法。请注意我们如何为 BCE 添加 epsilon 术语。这确保了我们不会遇到 log(0) 的计算,它等于无穷大。这将导致我们的损失函数中出现 NaN,从而影响我们的模型,使其在训练期间失败。

def _relu(self, x):return np.maximum(0, x)def _relu_derivative(self, x):return (x > 0).astype(float)def _sigmoid(self, x):return 1 / (1 + np.exp(-x))def _binary_cross_entropy(self, y_true, y_pred):epsilon = 1e-15  # To avoid log(0)y_pred = np.clip(y_pred, epsilon, 1 - epsilon)return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
Forward Pass 和 Backpropagation

请注意,在正向传递的方法中,“zs”和“activations”是如何记录的。当我们启动反向传播过程时,这一点至关重要。当我们向后工作时,我们将需要这些值来计算导数。

def _forward(self, X):a = np.array(X)activations = [a]zs = []for i in range(len(self.weights)-1):z = a.dot(self.weights[i]) + self.biases[i]a = self._relu(z)zs.append(z)activations.append(a)z = a.dot(self.weights[-1]) + self.biases[-1]a = self._sigmoid(z)zs.append(z)activations.append(a)return activations, zsdef _backward(self, activations, zs, y_true):m = y_true.shape[0]grads_w = [None] * len(self.weights)grads_b = [None] * len(self.biases)# Output layerdelta = activations[-1] - y_truegrads_w[-1] = activations[-2].T.dot(delta) / mgrads_b[-1] = np.mean(delta, axis=0).flatten()# Hidden layersfor i in reversed(range(len(self.hidden_layers))):delta = delta.dot(self.weights[i+1].T) * self._relu_derivative(zs[i])grads_w[i] = activations[i].T.dot(delta) / mgrads_b[i] = np.mean(delta, axis=0)return grads_w, grads_bdef _update_weights(self, grads_w, grads_b, learning_rate):for i in range(len(self.weights)):self.weights[i] -= learning_rate * grads_w[i]self.biases[i] -= learning_rate * grads_b[i]
训练和预测方法

这就是这一切汇集在一起的地方。还记得我们是如何包含批处理逻辑的吗?请注意,在每个 epoch 中,我们会先对数据集进行随机排序,然后再将其划分为多个批次。这将确保我们不会每次都对训练数据的相同子集进行训练。

请注意,当我们使用 predict 函数时,我们是如何调用该方法进行前向传递的。更具体地说,我们调用最新的激活值,如果它大于 0.5,我们预测 1;否则为 0。这是因为最后激活值是使用 sigmoid 函数计算的概率。

def train(self, X_train, y_train, X_test, y_test, learning_rate=0.01):best_loss = float('inf')patience = 0for epoch in range(self.epochs):perm = np.random.permutation(len(X_train))X_train = X_train[perm]y_train = y_train[perm]batch_size = len(X_train) // self.num_batchesepoch_train_loss = 0for i in range(self.num_batches):start = i * batch_sizeend = (i + 1) * batch_size if i < self.num_batches - 1 else len(X_train)X_batch = X_train[start:end]y_batch = y_train[start:end]activations, zs = self._forward(X_batch)grads_w, grads_b = self._backward(activations, zs, y_batch)self._update_weights(grads_w, grads_b, learning_rate)epoch_train_loss += self._binary_cross_entropy(y_batch, activations[-1])epoch_train_loss /= self.num_batchesself.train_losses.append(epoch_train_loss)# Evaluate on test settest_pred = self._forward(X_test)[0][-1]epoch_test_loss = self._binary_cross_entropy(y_test, test_pred)self.test_losses.append(epoch_test_loss)if self.early_stopping_rounds:if epoch_test_loss < best_loss:best_loss = epoch_test_losspatience = 0else:patience += 1if patience >= self.early_stopping_rounds:print(f"Early stopping at epoch {epoch+1}")breakprint(f"Epoch {epoch+1} - Train Loss: {epoch_train_loss:.4f} - Test Loss: {epoch_test_loss:.4f}")def predict(self, X):return (self._forward(X)[0][-1] > 0.5).astype(int)
评估、可视化和自定义训练/测试拆分方法

最后,我们有一个多合一的评估方法,它返回一些我最喜欢的评估指标。一种显示所有损失函数值的可视化方法。如果您想确定我们的模型是否有效收敛,这一点至关重要。理想情况下,它应该在训练和测试中显示损失函数逐渐减少。

def evaluate(self, X, y_true):y_pred = self.predict(X)# Confusion matrix componentstp = np.sum((y_pred == 1) & (y_true == 1))tn = np.sum((y_pred == 0) & (y_true == 0))fp = np.sum((y_pred == 1) & (y_true == 0))fn = np.sum((y_pred == 0) & (y_true == 1))# Metricsaccuracy = (tp + tn) / (tp + tn + fp + fn)precision = tp / (tp + fp + 1e-10)recall = tp / (tp + fn + 1e-10)# Confusion matrix as a dictionaryconfusion_matrix = {'TP': int(tp),'TN': int(tn),'FP': int(fp),'FN': int(fn)}return {'accuracy': accuracy,'precision': precision,'recall': recall,'confusion_matrix': confusion_matrix}def plot_losses(self):plt.plot(self.train_losses, label='Train Loss')plt.plot(self.test_losses, label='Test Loss')plt.xlabel('Epoch')plt.ylabel('Loss')plt.legend()plt.title('Loss Over Epochs')plt.show()@staticmethod
def train_test_split(X, y, test_size=0.2, random_state=None):if random_state:np.random.seed(random_state)indices = np.random.permutation(len(X))split_idx = int(len(X) * (1 - test_size))train_idx, test_idx = indices[:split_idx], indices[split_idx:]return X[train_idx], X[test_idx], y[train_idx], y[test_idx]

Heart Disease 数据示例

让我们使用 Kaggle 的 heart disease 数据集测试对象。

df = pd.read_csv('heart_2020_cleaned.csv')
df.head()

None

图片由作者提供

我们需要通过对所有分类变量进行编码来清理此数据集。这里要展示的特征有点太多了,但要知道,我们现在数据集中有 39 个特征列。

# Convert Yes/No to binary
df_cleaned_1 = df.replace('Yes', True, regex=True)
df_cleaned_2 = df_cleaned_1.replace('No', False, regex=True)# Identify object/string columns
object_cols = df_cleaned_2.select_dtypes(include=['object', 'string']).columns.tolist()# One-hot encode these columns
encoded_df = pd.get_dummies(df_cleaned_2[object_cols], prefix=object_cols)# Drop the original object columns from df
df_cleaned_prefinal = df_cleaned_2.drop(columns=object_cols)# Concatenate the one-hot encoded columns
df_cleaned_final = pd.concat([df_cleaned_prefinal, encoded_df], axis=1)# Optional: confirm the transformation
print(f"Original object columns: {object_cols}")
print(f"New shape: {df_cleaned_final.shape}")
df_cleaned_final.head()

在进入模型之前,我们先看一下目标类的值计数。注意到什么了吗?该数据集中的大多数参与者似乎没有心脏病。在评估模型的性能时,请记住这一点。

df_cleaned_final['HeartDisease'].value_counts()

None

图片由作者提供

让我们创建一个对象的实例并相应地拆分数据。此模型在输入层中有 39 个节点,在第一个隐藏层中有 20 个节点,在下一层中有 10 个节点。我们将对数据集执行 100 次完整传递,并将其分为 10 个批次。如果 10 轮后测试数据集丢失情况没有改善,训练作业将停止。最后,我们将学习率设置为 0.1。让我们开始训练作业,看看模型的性能如何。

X = df_cleaned_final.drop('HeartDisease', axis=1).astype(np.float32).to_numpy()
y = df_cleaned_final['HeartDisease'].astype(np.float32).to_numpy().reshape(-1, 1)nn = DIYNeuralNetwork(input_size=X.shape[1],hidden_layers=[20, 10],num_batches=10,epochs=100,early_stopping_rounds=10
)X_train, X_test, y_train, y_test = nn.train_test_split(X, y, test_size=0.2, random_state=42)X_train = np.array(X_train)
y_train = np.array(y_train)
X_test = np.array(X_test)
y_test = np.array(y_test)
nn.train(X_train, y_train, X_test, y_test, learning_rate=0.1)

你觉得怎么样?我们创建了一个强大的模型吗?它似乎在训练和测试数据中都有效地收敛了。

nn.plot_losses()

None

图片由作者提供

我们实现了 91% 的准确率,这在纸面上听起来令人印象深刻。看看混淆矩阵。注意到任何有趣的事情了吗?我们的模型每次都预测负类别。我们的模型并不比仅仅猜测参与者患有心脏病大约 10% 的时间好。

results = nn.evaluate(X_test, y_test)
results

None

图片由作者提供

让我们更上一层楼,让我们将 epoch 的数量增加到 200,将提前停止增加到 20,看看我们是否能收敛到一组更好的权重和偏差项。

看看结果;该模型的收敛程度似乎略高,但这对其性能没有影响。我们该怎么办?我们可以尝试几种不同的层、epoch、batch size、learning rate 等组合。

None

图片由作者提供

None

图片由作者提供

击打

SMOTE 或合成少数过度采样技术是一种为少数类的观察创建合成数据的方法。我不会在这里详细介绍它是如何工作的;但是,它的基本框架是它选择 Minority 类的随机实例,使用 K-Nearest Neighbors 模型确定同一类的最近邻,并生成位于这些实例之间的合成数据。

一个关键的澄清是,我们只对训练数据应用 SMOTE,因为我们希望确保我们的模型可以在看不见的真实数据上表现良好。让我们在下面这样做。如您所见,训练集中的新类分布为 50/50。

from imblearn.over_sampling import SMOTE# Step 1: Split first
X_train, X_test, y_train, y_test = DIYNeuralNetwork.train_test_split(X, y, test_size=0.2, random_state=42)# Step 2: Apply SMOTE to training data only
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train.ravel())
y_train_resampled = y_train_resampled.reshape(-1, 1)# Confirm new class distribution
print("Class distribution after SMOTE:", np.bincount(y_resampled.flatten().astype(int)))

None

图片由作者提供

我进行了一些试验和错误并调整了模型的参数。除了结果之外,还可以在下面查看它们。注意到任何有趣的事情了吗?如果您查看损失图,您可以看到一个非常健康的趋势,即最大限度地减少损失,甚至似乎还有进一步的改进空间。准确性确实有所下降,但我们的模型现在已经证明了预测正类实例的能力。这是个好消息。为什么?这是一个理论上可以用来预测某人是否有患心脏病风险的模型,所以让我们考虑一下预测的场景:

  • (真阳性)我们告诉某人**他们患有心脏病,而他们也患有**心脏病。
  • (真阴性)我们告诉某人他们**没有心脏病,他们也没有**心脏病。
  • (误报)我们告诉某人**他们患有心脏病,而他们没有**心脏病。
  • (假阴性)我们告诉某人他们**没有心脏病,他们确实患有**心脏病。

在这些选项中,我们应该寻求最小化或最大化什么?我不知道你是怎么想的,但在这种情况下的假阴性对个人来说是灾难性的。想象一下,如果我们告诉他们他们没有患心脏病的风险,并且他们不做任何改变就过自己的生活。作为数据科学家,我们必须认真对待这些情况。在这种情况下,我们必须确保获得尽可能高的召回分数,因为它可以最大限度地减少假阴性。

# Initialize and train
nn = DIYNeuralNetwork(input_size=X_train_resampled.shape[1],hidden_layers=[20],num_batches=10,epochs=200,early_stopping_rounds=30)nn.train(X_train_resampled, y_train_resampled, X_test, y_test, learning_rate=0.01)

None

图片由作者提供

None

相关文章:

  • 什么是云原生?什么样的框架符合云原生?
  • 分享| 低代码建模工具-大数据挖掘建模平台白皮书
  • 计算机视觉之三维重建(深入浅出SfM与SLAM核心算法)—— 3. 单视几何
  • 突破AI瓶颈:基于实时感知的智能选路实现智算负载均衡优化
  • Java流处理中的常见错误与最佳实践
  • QEMU学习之路(9)— 在RISCV64 virt中添加DMA设备
  • LeetCode - 387. 字符串中的第一个唯一字符
  • 商城系统微服务化改造:三大难点与实战解决方案
  • 【工具教程】批量PDF识别提取区域的内容重命名,将PDF指定区域位置的内容提取出来改名的注意事项
  • 动态规划: 背包DP大合集
  • 算法第15天:继续二叉树|前序递归+回溯与前序递归的场景总结、最大二叉树、合并二叉树、二叉搜索树中的搜索、验证二叉搜索树
  • 【Linux网络编程】基于udp套接字实现的网络通信
  • WebView工作原理全解析:如何实现混合开发的无缝衔接
  • 69、JS中如何调用上位机接口
  • 深入讲解一下 Nomic AI 的 GPT4All 这个项目
  • 局域网内电脑与安卓设备低延迟同屏技术【100ms - 200ms】
  • 开疆智能ModbusTCP转Devicenet网关连接三菱PLC与ABB机器人配置案例
  • 解决U盘安装Win11无法命令行跳过联网激活的问题
  • Python内存互斥与共享深度探索:从GIL到分布式内存的实战之旅
  • java发送excel附件的邮件
  • 网站名称 域名/网络推广工作
  • 可以做网站的电脑软件/网络营销策划方案格式
  • 用百度网盘做视频网站/怎么申请建立网站
  • 收购域名/长沙关键词优化平台
  • 深圳做营销网站公司简介/有哪些可以免费推广的平台
  • 做外贸怎么打开国外网站/武汉网站seo公司