用 Python 从零开始创建神经网络(四):激活函数(Activation Functions)

激活函数(Activation Functions)

  • 引言
  • 1. 激活函数的种类
    • a. 阶跃激活功能
    • b. 线性激活函数
    • c. Sigmoid激活函数
    • d. ReLU 激活函数
    • e. more
  • 2. 为什么使用激活函数
  • 3. 隐藏层的线性激活
  • 4. 一对神经元的 ReLU 激活
  • 5. 在隐蔽层中激活 ReLU
  • 6. ReLU 激活函数代码
  • 7. Softmax 激活函数

引言

在本章中,我们将讨论几种激活函数及其作用。我们会根据不同的情况使用不同的激活函数,了解它们的工作原理可以帮助你正确选择最适合你任务的激活函数。激活函数应用于神经元(或神经元层)的输出上,这会修改输出。我们使用激活函数是因为如果激活函数本身是非线性的,它允许通常有两层或更多隐藏层的神经网络来映射非线性函数。我们将在本章展示这是如何工作的。

一般来说,你的神经网络将有两种类型的激活函数。第一种将用于隐藏层,第二种将用于输出层。通常,用于隐藏神经元的激活函数对所有神经元都是相同的,但也不必须如此。


1. 激活函数的种类

a. 阶跃激活功能

在这里插入图片描述

b. 线性激活函数

在这里插入图片描述

c. Sigmoid激活函数

在这里插入图片描述

d. ReLU 激活函数

在这里插入图片描述

e. more

会有更多的激活函数


2. 为什么使用激活函数

现在我们了解了激活函数代表什么,它们的一些形状,以及它们返回什么,让我们讨论为什么我们首先要使用激活函数。在大多数情况下,为了让神经网络拟合非线性函数,我们需要它包含两个或更多隐藏层,并且需要这些隐藏层使用非线性激活函数。

首先,什么是非线性函数?非线性函数不能被直线很好地表示,例如正弦函数:

在这里插入图片描述

虽然生活中确实存在一些本质上是线性的问题,例如,试图计算一定数量衬衫的成本,我们知道单件衬衫的成本,并且没有批量折扣,那么计算这些产品任意数量的价格的方程是一个线性方程。但生活中的其他问题就没有那么简单了,比如房价。影响房价的因素有很多,如面积、位置、销售的时间、房间数量、院子、邻里等等,这些因素使得房屋定价成为一个非线性方程。我们时代许多更有趣和更困难的问题都是非线性的。神经网络的主要吸引力在于它们解决非线性问题的能力。首先,让我们考虑一种情况,即神经元没有激活函数,这与激活函数为 y = x y=x y=x 相同。在一个具有2个隐藏层,每层8个神经元的神经网络中使用这种线性激活函数,训练这个模型的结果将是这样的(红):

在这里插入图片描述
如果使用同样的 2 个隐藏层,每个隐藏层有 8 个神经元,并使用整流线性激活函数,训练后我们会看到以下结果:

在这里插入图片描述


3. 隐藏层的线性激活

既然你已经看到了这种情况,我们还是应该考虑一下为什么会出现这种情况。首先,让我们重温一下 y = x y=x y=x 的线性激活函数,并从单个神经元的层面来考虑这个问题。在给定权重和偏置值的情况下,具有 y = x y=x y=x 激活函数的神经元的输出会是什么?让我们来看几个例子——首先,让我们尝试用正值更新第一个权重:

在这里插入图片描述
我们继续调整权重,这次更新为负数:

在这里插入图片描述

更新权重并增加偏差:

在这里插入图片描述

无论我们如何处理该神经元的权重和偏置,该神经元的输出都将与激活函数的y=x完全线性。这种线性性质将贯穿整个网络:

在这里插入图片描述

无论我们做什么,无论我们有多少层,只有当我们使用线性激活函数时,这个网络才能描述线性关系。这一点应该相当明显,因为每一层中的每个神经元都是线性的,所以整个网络也是一个线性函数。


4. 一对神经元的 ReLU 激活

我们认为,对于一个勉强算得上非线性的激活函数,比如经过矫正的线性激活函数,我们是如何突然映射出非线性关系和函数的,这一点并不那么明显,所以现在让我们来讨论一下这个问题。让我们从一个神经元开始。权重为 0,偏置为 0:

在这里插入图片描述
在这种情况下,无论我们通过什么输入,这个神经元的输出都将是 0,因为权重是 0,而且没有偏差。让我们将权重设为 1:

在这里插入图片描述
现在,它看起来就像基本的整流线性函数,没有任何意外!现在我们将偏置值设置为 0.50:

在这里插入图片描述
我们可以看到,在这种情况下,通过单个神经元,偏置水平偏移了整个函数的激活点。通过增加偏置,我们使这个神经元提前激活。如果我们将权重降为-1.0,会发生什么情况呢?

在这里插入图片描述

当我们有一个负权重和这一个单一神经元时,问题变成了这个神经元何时停止活动。到目前为止,你已经看到我们如何使用偏置来水平偏移函数,以及权重如何影响激活的斜率。此外,我们还能控制函数是用来确定神经元何时激活还是停止活动。

当我们不仅有一个神经元,而是一对神经元时会发生什么?例如,假设我们有2个隐藏层,每层各有1个神经元。回想一下 y = x y=x y=x 激活函数,我们不出所料地发现,无论我们制造了什么样的神经元链,线性激活函数都产生线性结果。让我们看看使用整流线性函数作为激活时会发生什么。我们将从第一个神经元的最后值开始,对第二个神经元使用权重为1,偏置为0:

在这里插入图片描述
到目前为止,我们可以看到没有任何变化。这是因为第 2 个神经元的偏置没有做任何偏移,第 2 个神经元的权重只是将输出乘以 1,所以没有变化。现在让我们试着调整第 2 个神经元的偏置:

在这里插入图片描述
现在,我们看到了一些相当有趣的行为。第二个神经元的偏置确实移动了整个函数,但不是水平移动,而是垂直移动。那么,如果我们将第二个神经元的权重设置为-2 而不是 1,会出现什么情况呢?

在这里插入图片描述

令人兴奋的事情发生了!我们这里的神经元既有激活点,也有失活点。当这两个神经元都激活时,当它们的 “作用区域 ”发挥作用时,它们就会产生颗粒、变量和输出范围内的数值。如果这对神经元中的任何一个处于非激活状态,这对神经元将产生非变量输出:

在这里插入图片描述


5. 在隐蔽层中激活 ReLU

现在让我们利用这个概念,使用两个隐藏层,每层8个神经元,来拟合正弦波函数,并手动调整值以适应曲线。我们将通过一次处理一对神经元来做到这一点,这意味着单独从每层中选取一个神经元。为了简化,我们还将假设层之间不是密集连接的,第一个隐藏层的每个神经元只连接到第二个隐藏层的一个神经元。通常情况下真实模型并非如此,但我们希望这种简化是为了这次演示的目的。此外,这个示例模型将一个单一值作为输入,即正弦函数的输入,并输出一个类似正弦函数的单一值。输出层使用线性激活函数,隐藏层将使用ReLU激活函数

首先,我们将所有权重设置为0,并从第一对神经元开始工作:

在这里插入图片描述
接下来,我们可以将隐藏层神经元和输出神经元的权重设置为 1,然后就能看到这对输出的影响了:

在这里插入图片描述
在这种情况下,我们可以看到整个函数的斜率受到了影响。我们可以将第一层第一个神经元的权重调整为 6.0,从而进一步提高斜率:

在这里插入图片描述
现在我们可以看到,例如,这个函数的初始斜率是我们想要的,但我们有一个问题。当前这个函数永远不会结束,因为这对神经元从未停止活动。我们可以直观地看到我们希望停止活动发生的地方。就是红色拟合线(我们当前神经网络的输出)最初偏离绿色正弦波的地方。所以现在,虽然我们有了正确的斜率,我们需要将这个点设置为我们的停止活动点。为了做到这一点,我们首先将隐藏层对中第二个神经元的偏置增加到0.70。回想一下,这会垂直偏移整个函数:

在这里插入图片描述

现在,我们可以将第 2 个神经元的权重设置为-1,这样就会在我们想要的位置出现一个失活点,至少是水平失活点:

在这里插入图片描述

现在,我们想把这个斜率翻转回来。我们该如何翻转这两个神经元的输出呢?似乎我们可以将输出神经元的连接权重(目前为 1.0)翻转为-1,这样就可以翻转函数了:

在这里插入图片描述

当然,我们已经越来越接近让第一部分符合我们的要求了。现在,我们需要做的就是将其偏移一点点。在这个手工优化的例子中,我们将使用隐藏层中的前 7 对神经元来创建正弦波的形状,然后使用最下面的一对神经元来垂直偏移所有内容。如果我们将最下面一对神经元中第二个神经元的偏置设置为 1.0,输出神经元的权重设置为 0.7,就可以像这样垂直移动直线:

在这里插入图片描述

至此,我们已经完成了第一部分,“影响区域 ”是正弦波的第一个上升部分。我们可以开始下一个要做的部分。首先,我们可以将第二对神经元的所有权重设置为 1,包括输出神经元:

在这里插入图片描述

此时,这第二对神经元的激活开始得太早了,这影响了我们已经对齐的顶部一对的“影响区域”。为了解决这个问题,我们希望这第二对在第一对停止活动的地方开始影响输出,所以我们需要水平调整函数。正如你之前记得的,我们调整这对神经元中第一个神经元的偏置来实现这一点。此外,为了修改斜率,我们将设置进入这第二对中第一个神经元的权重,设为3.5。这和我们用来为第一部分设置斜率的方法相同,该部分由隐藏层中的顶部一对神经元控制。在这些调整之后:

在这里插入图片描述

现在,我们将使用与第一对神经元相同的方法来设置停用点。我们将隐藏层中第二个神经元的权重设为-1,偏置设为 0.27。

在这里插入图片描述

然后,我们可以翻转这部分的函数,方法与第一个函数相同,将输出神经元的权重从 1.0 设置为-1.0:

在这里插入图片描述
同样,就像第一对一样,我们将使用最下面的一对来固定垂直偏移:

在这里插入图片描述
然后,我们继续采用这种方法。我们将在顶部部分保持平缓,这意味着只有当我们希望斜坡开始下降时,才会开始激活第三对隐藏层神经元:

在这里插入图片描述

每个部分都重复这一过程,最终得出结果:

在这里插入图片描述
然后,我们就可以开始传递数据,看看这些神经元的作用区域是如何发挥作用的——只有当这两个神经元都根据输入被激活时才能发挥作用:

在这里插入图片描述

在这种情况下,如果输入值为 0.08,我们可以看到被激活的只有最上面的一对,因为这是它们的作用区域。继续看另一个例子:

在这里插入图片描述

在这种情况下,只有第四对神经元被激活。正如你所看到的,即使没有任何其他权重,我们也利用了一对神经元的一些粗略特性和整流线性激活函数,很好地拟合了这个正弦波。如果我们现在启用所有权重,让数学优化器进行训练,我们可以看到更好的拟合效果:

在这里插入图片描述

代码的可视化:https://nnfs.io/mvp

现在你应该明白,更多的神经元可以产生更独特的效果,为什么我们需要两个或更多的隐藏层,为什么我们需要非线性激活函数来映射非线性问题。举个更进一步的例子,我们可以把上面的例子中的 2 个隐藏层,每个隐藏层 8 个神经元,改为每个隐藏层使用 64 个神经元,这样就能看到更大的持续改进:

在这里插入图片描述

代码的可视化:https://nnfs.io/moo


6. ReLU 激活函数代码

尽管名字听起来很华丽,但整流线性激活函数的编码却很简单。最接近它的定义:

inputs = [0, 2, -1, 3.3, -2.7, 1.1, 2.2, -100]
output = []for i in inputs:if i > 0:output.append(i)else:output.append(0)print(output)
>>>
[0, 2, 0, 3.3, 0, 1.1, 2.2, 0]

我们先创建了一个值列表。这段代码中的 ReLU 是一个循环,我们在其中检查当前值是否大于 0,如果大于 0,我们就将其追加到输出列表中,如果不大于 0,我们就将其追加到输出列表中: 0 或神经元值。例如:

inputs = [0, 2, -1, 3.3, -2.7, 1.1, 2.2, -100]
output = []for i in inputs:output.append(max(0, i))print(output)
>>>
[0, 2, 0, 3.3, 0, 1.1, 2.2, 0]

NumPy 包含一个等价函数——np.maximum()

import numpy as npinputs = [0, 2, -1, 3.3, -2.7, 1.1, 2.2, -100]
output = np.maximum(0, inputs)print(output)
print("Type:", type(output))
>>>
[0.  2.  0.  3.3 0.  1.1 2.2 0. ]
Type: <class 'numpy.ndarray'>

该方法会比较输入列表(或数组)中的每个元素,并返回一个填充了新值的相同形状的对象。我们将在新的整流线性激活类中使用它:

import numpy as np# ReLU activation
class Activation_ReLU:# Forward passdef forward(self, inputs):# Calculate output values from inputself.output = np.maximum(0, inputs)

让我们在代码中将此激活函数应用于密集层的输出:

import numpy as np
import nnfs
from nnfs.datasets import spiral_datannfs.init()class Layer_Dense:# Layer initializationdef __init__(self, n_inputs, n_neurons):# Initialize weights and biasesself.weights = 0.01 * np.random.randn(n_inputs, n_neurons)self.biases = np.zeros((1, n_neurons))# Forward passdef forward(self, inputs):# Calculate output values from inputs, weights and biasesself.output = np.dot(inputs, self.weights) + self.biases# ReLU activation
class Activation_ReLU:# Forward passdef forward(self, inputs):# Calculate output values from inputself.output = np.maximum(0, inputs)# Create dataset
X, y = spiral_data(samples=100, classes=3)# Create Dense layer with 2 input features and 3 output values
dense1 = Layer_Dense(2, 3)# Create ReLU activation (to be used with Dense layer):
activation1 = Activation_ReLU()# Make a forward pass of our training data through this layer
dense1.forward(X)# Forward pass through activation func.
# Takes in output from previous layer
activation1.forward(dense1.output)# Let's see output of the first few samples:
print(activation1.output[:5])
>>>
[[0.         0.         0.        ][0.         0.00011395 0.        ][0.         0.00031729 0.        ][0.         0.00052666 0.        ][0.         0.00071401 0.        ]]
>>> 没有应用ReLU激活函数的输出:
[[ 0.0000000e+00  0.0000000e+00  0.0000000e+00][-1.0475188e-04  1.1395361e-04 -4.7983500e-05][-2.7414842e-04  3.1729150e-04 -8.6921798e-05][-4.2188365e-04  5.2666257e-04 -5.5912682e-05][-5.7707680e-04  7.1401405e-04 -8.9430439e-05]]

正如你所看到的,负值已被削去(修改为零)。这就是隐藏层中使用的整流线性激活函数的全部内容。下面我们来谈谈最后一层输出所使用的激活函数。


7. Softmax 激活函数

在我们的案例中,我们希望这个模型成为一个分类器,因此我们需要一个适用于分类的激活函数。其中之一是Softmax激活函数。首先,为什么我们要费心使用另一个激活函数呢?这完全取决于我们的总体目标。在这种情况下,修正线性单元是无界的,不与其他单元标准化,并且是独立的。"不标准化"意味着值可以是任何数,比如输出 [ 12 , 99 , 318 ] [12, 99, 318] [12,99,318] 是没有上下文的,而"独立的"意味着每个输出都与其他输出无关。为了解决这种缺乏上下文的问题,softmax激活函数可以接收非标准化或未校准的输入,并为我们的类别产生一个标准化的概率分布。在分类的情况下,我们希望看到的是网络“认为”输入代表哪个类的预测。由softmax激活函数返回的这个分布表示每个类的置信度得分,并且总和为1。预测的类别与返回最大置信度得分的输出神经元相关联。然而,我们还可以在使用这个网络的总体算法/程序中注意到其他的置信度得分。例如,如果我们的网络对两个类的置信度分布是 [ 0.45 , 0.55 ] [0.45, 0.55] [0.45,0.55],预测结果是第二个类,但这个预测的置信度不是很高。也许在这种情况下,由于不太自信,我们的程序可能不会采取行动。

以下是Softmax函数:

S i , j = e z i , j ∑ l = 1 L e z i , l S_{i,j} = \frac{e^{z_{i,j}}}{\sum_{l=1}^L e^{z_{i,l}}} Si,j=l=1Lezi,lezi,j

这看起来可能令人生畏,但我们可以将其分解成简单的部分,用 Python 代码来表达,你可能会发现这比上面的公式更容易理解。首先,下面是一个神经网络层的输出示例:

layer_outputs = [4.8, 1.21, 2.385]

我们的第一步是将输出 “指数化”。我们使用欧拉数 e e e 来实现这一过程, e e e 约为2.71828182846,被称为 “指数增长 ”数。指数化就是将这个常数乘以给定参数的幂:

y = e x y=e^x y=ex

Softmax 函数的分子和分母都包含 e e e 升至 z z z 的幂,其中给定指数的 z z z 表示奇异输出值——指数 i i i 表示当前样本,指数 j j j 表示该样本中的当前输出。分子将当前的输出值指数化,分母则是给定样本中所有指数化输出的总和。然后,我们需要计算这些指数值才能继续:

# Values from the previous output when we described
# what a neural network is
layer_outputs = [4.8, 1.21, 2.385]# e - mathematical constant, we use E here to match a common coding
# style where constants are uppercased
E = 2.71828182846 # you can also use math.e# For each value in a vector, calculate the exponential value
exp_values = []
for output in layer_outputs:exp_values.append(E ** output) # ** - power operator in Pythonprint('exponentiated values:')
print(exp_values)
>>>
exponentiated values:
[121.51041751893969, 3.3534846525504487, 10.85906266492961]

幂级数有多种用途。为了计算概率,我们需要非负值。想象一下输出结果是 [4.8, 1.21, -2.385] —— 即使经过归一化处理,最后一个值仍然是负数,因为我们只是用所有值除以它们的总和。负概率(或置信度)没有太大意义。任何数字的指数值总是非负的–负无穷时返回 0,输入 0 时返回 1,正值时返回正数:

在这里插入图片描述


指数函数是一个单调函数。这意味着,随着输入值的增加,输出值也会增加,因此在确保我们得到非负值的同时,应用它不会改变预测的类别。它还增加了结果的稳定性,因为归一化的指数化更多地关注数字之间的差异而非它们的大小。一旦我们进行了指数化,我们希望将这些数字转换为概率分布(将值转换为每个类别的置信度向量,向量中的所有置信度加起来为1)。这意味着我们即将进行归一化,其中我们将给定值除以所有值的总和。对于我们此阶段指数化的输出,这就是Softmax函数方程接下来描述的内容 —— 将给定的指数化值除以所有指数化值的总和。由于每个输出值都归一化为总和的一部分,所有的值现在都在0到1的范围内,并且总和为1 —— 它们之间共享1的概率。让我们在代码中添加总和和归一化:

# Values from the previous output when we described
# what a neural network is
layer_outputs = [4.8, 1.21, 2.385]# e - mathematical constant, we use E here to match a common coding
# style where constants are uppercased
E = 2.71828182846 # you can also use math.e# For each value in a vector, calculate the exponential value
exp_values = []
for output in layer_outputs:exp_values.append(E ** output) # ** - power operator in Pythonprint('exponentiated values:')
print(exp_values)# Now normalize values
norm_base = sum(exp_values) # We sum all values
norm_values = []for value in exp_values:norm_values.append(value / norm_base)print('Normalized exponentiated values:')
print(norm_values)
print('Sum of normalized values:', sum(norm_values))
>>>
exponentiated values:
[121.51041751893969, 3.3534846525504487, 10.85906266492961]
Normalized exponentiated values:
[0.8952826639573506, 0.024708306782070668, 0.08000902926057876]
Sum of normalized values: 1.0

我们可以使用 NumPy 以下面的方式执行相同的操作:

import numpy as np# Values from the earlier previous when we described
# what a neural network is
layer_outputs = [4.8, 1.21, 2.385]# For each value in a vector, calculate the exponential value
exp_values = np.exp(layer_outputs)print('exponentiated values:')
print(exp_values)# Now normalize values
norm_values = exp_values / np.sum(exp_values)print('normalized exponentiated values:')
print(norm_values)
print('sum of normalized values:', np.sum(norm_values))
exponentiated values:
[121.51041752   3.35348465  10.85906266]
normalized exponentiated values:
[0.89528266 0.02470831 0.08000903]
sum of normalized values: 0.9999999999999999

请注意,计算结果与 NumPy 类似,但计算速度更快,代码也更易于阅读。我们只需调用一次 np.exp(),就能对所有值进行指数化,然后立即用总和对它们进行归一化。为了进行分批训练,我们需要将此功能转换为分批接受层输出。具体操作如下:

# Get unnormalized probabilities
exp_values = np.exp(inputs)# Normalize them for each sample
probabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)

我们有了一些新函数。具体来说,np.exp() 可以完成 E** 输出部分。我们还应该讨论一下上面提到的 axiskeepdims 的含义。我们先讨论一下轴线。坐标轴更容易显示,但在二维数组/矩阵中,坐标轴 0 指的是行,坐标轴 1 指的是列。让我们举例说明轴如何影响 NumPy 的求和。首先,我们只显示默认值,即无:

import numpy as nplayer_outputs = np.array([[4.8, 1.21, 2.385],[8.9, -1.81, 0.2],[1.41, 1.051, 0.026]])print('Sum without axis')
print(np.sum(layer_outputs))
print('This will be identical to the above since default is None:')
print(np.sum(layer_outputs, axis=None))
>>>
Sum without axis
18.172
This will be identical to the above since default is None:
18.172

如果没有指定轴(axis),我们只是将所有的值求和,即使它们在不同的维度中。接下来,axis=0。这意味着沿着轴0按行求和。换句话说,输出的大小与这个轴一样,因为在这个输出的每个位置,该位置的所有其他维度的值都被求和以形成它。在我们的2D数组中,我们只有一个其他维度,即列,输出向量将求和这些列。这意味着我们将执行4.8+8.9+1.41等操作:

import numpy as nplayer_outputs = np.array([[4.8, 1.21, 2.385],[8.9, -1.81, 0.2],[1.41, 1.051, 0.026]])print('Sum without axis')
print(np.sum(layer_outputs))
print('This will be identical to the above since default is None:')
print(np.sum(layer_outputs, axis=None))print('Another way to think of it w/ a matrix == axis 0: columns:')
print(np.sum(layer_outputs, axis=0))
>>>
Sum without axis
18.172
This will be identical to the above since default is None:
18.172Another way to think of it w/ a matrix == axis 0: columns:
[15.11   0.451  2.611]

但这并不是我们想要的。我们要的是各行的总和。你也许能猜到如何用 NumPy 实现这一功能,但我们还是要展示一下 “从零开始 ”的版本:

import numpy as nplayer_outputs = np.array([[4.8, 1.21, 2.385],[8.9, -1.81, 0.2],[1.41, 1.051, 0.026]])print('Sum without axis')
print(np.sum(layer_outputs))
print('This will be identical to the above since default is None:')
print(np.sum(layer_outputs, axis=None))print('Another way to think of it w/ a matrix == axis 0: columns:')
print(np.sum(layer_outputs, axis=0))print('But we want to sum the rows instead, like this w/ raw py:')
for i in layer_outputs:print(sum(i))
>>>
Sum without axis
18.172
This will be identical to the above since default is None:
18.172Another way to think of it w/ a matrix == axis 0: columns:
[15.11   0.451  2.611]But we want to sum the rows instead, like this w/ raw py:
8.395
7.29
2.4869999999999997

有了上述方法,我们就能以任何方式将这些数据追加到某个列表中。因此,我们将使用 NumPy。正如你可能猜到的,我们将沿着轴 1 求和:

import numpy as nplayer_outputs = np.array([[4.8, 1.21, 2.385],[8.9, -1.81, 0.2],[1.41, 1.051, 0.026]])print('Sum without axis')
print(np.sum(layer_outputs))
print('This will be identical to the above since default is None:')
print(np.sum(layer_outputs, axis=None))print('Another way to think of it w/ a matrix == axis 0: columns:')
print(np.sum(layer_outputs, axis=0))print('But we want to sum the rows instead, like this w/ raw py:')
for i in layer_outputs:print(sum(i))print('So we can sum axis 1, but note the current shape:')
print(np.sum(layer_outputs, axis=1))
>>>
Sum without axis
18.172
This will be identical to the above since default is None:
18.172Another way to think of it w/ a matrix == axis 0: columns:
[15.11   0.451  2.611]But we want to sum the rows instead, like this w/ raw py:
8.395
7.29
2.4869999999999997So we can sum axis 1, but note the current shape:
[8.395 7.29  2.487]

正如 “注意当前形状 ”所指出的,我们确实得到了预期的总和,但实际上,我们希望将输出简化为每个样本的单一值。我们正试图将一个层中每个样本的所有输出相加;将该层的输出数组(行长等于层中神经元的数量)转换为一个值。我们需要一个包含这些值的列向量,因为它可以让我们通过一次计算,对整批样本进行样本归一化处理:

import numpy as nplayer_outputs = np.array([[4.8, 1.21, 2.385],[8.9, -1.81, 0.2],[1.41, 1.051, 0.026]])print('Sum without axis')
print(np.sum(layer_outputs))
print('This will be identical to the above since default is None:')
print(np.sum(layer_outputs, axis=None))print('Another way to think of it w/ a matrix == axis 0: columns:')
print(np.sum(layer_outputs, axis=0))print('But we want to sum the rows instead, like this w/ raw py:')
for i in layer_outputs:print(sum(i))print('So we can sum axis 1, but note the current shape:')
print(np.sum(layer_outputs, axis=1))print('Sum axis 1, but keep the same dimensions as input:')
print(np.sum(layer_outputs, axis=1, keepdims=True))
>>>
Sum without axis
18.172
This will be identical to the above since default is None:
18.172Another way to think of it w/ a matrix == axis 0: columns:
[15.11   0.451  2.611]But we want to sum the rows instead, like this w/ raw py:
8.395
7.29
2.4869999999999997So we can sum axis 1, but note the current shape:
[8.395 7.29  2.487]Sum axis 1, but keep the same dimensions as input:
[[8.395][7.29 ][2.487]]

这样,我们就能保持与输入相同的维数。现在,如果我们将包含一批输出的数组与该数组相除,NumPy 将按抽样方式执行。这意味着,它会将每行输出的所有值除以总和数组中的相应行。由于每一行中的和都是单个值,因此它将用于与相应输出行中的每个值进行除法)。我们可以将所有这些合并到一个 softmax 类中,例如:

# Softmax activation
class Activation_Softmax:# Forward passdef forward(self, inputs):# Get unnormalized probabilitiesexp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))# Normalize them for each sampleprobabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)self.output = probabilities

最后,在进行指数运算之前,我们还对最大输入值进行了减法运算。神经网络普遍面临两大挑战: “死神经元 “和极大的数字(称为 ”爆炸 "值)。“死亡 "神经元和巨大的数字会造成严重破坏,使网络长期失去作用。softmax 激活中使用的指数函数就是爆炸值的来源之一。让我们举例说明这种情况如何以及为何容易发生:

import numpy as npprint(np.exp(1))
print(np.exp(10))
print(np.exp(100))
print(np.exp(1000))
>>>
2.718281828459045
22026.465794806718
2.6881171418161356e+43
inf
RuntimeWarning: overflow encountered in expprint(np.exp(1000))

在这种情况下,一个并不非常大的数字,仅仅是1000,就足以引起溢出错误。我们知道,随着输入值趋近于负无穷,指数函数趋向于0,而当输入为0时输出为1(如之前的图表所示):

import numpy as npprint(np.exp(-np.inf), np.exp(0))
>>>
0.0 1.0

我们可以利用这一性质来防止指数函数溢出。假设我们从一系列输入值中减去最大值。我们将会改变输出值,使其总是在从某个负数值到0的范围内,因为最大数减去它自身得到0,任何较小的数减去它将得到一个负数 — 正是上述讨论的范围。在Softmax中,由于归一化的原因,我们可以从所有输入中减去任何值,这不会改变输出结果:

import numpy as np# Softmax activation
class Activation_Softmax:# Forward passdef forward(self, inputs):# Get unnormalized probabilitiesexp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))# Normalize them for each sampleprobabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)self.output = probabilitiessoftmax = Activation_Softmax()
softmax.forward([[1, 2, 3]])
print(softmax.output)
>>>
[[0.09003057 0.24472847 0.66524096]]
import numpy as np# Softmax activation
class Activation_Softmax:# Forward passdef forward(self, inputs):# Get unnormalized probabilitiesexp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))# Normalize them for each sampleprobabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)self.output = probabilitiessoftmax = Activation_Softmax()
softmax.forward([[1, 2, 3]])
print(softmax.output)softmax.forward([[-2, -1, 0]]) # subtracted 3 - max from the list
print(softmax.output)
>>>
[[0.09003057 0.24472847 0.66524096]]
[[0.09003057 0.24472847 0.66524096]]

这是指数化和归一化函数的另一个有用特性。除了这些计算之外,还有一点需要提及。例如,如果我们将层的输出数据 [1, 2, 3] 除以2会发生什么?

import numpy as np# Softmax activation
class Activation_Softmax:# Forward passdef forward(self, inputs):# Get unnormalized probabilitiesexp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))# Normalize them for each sampleprobabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)self.output = probabilitiessoftmax = Activation_Softmax()
softmax.forward([[1, 2, 3]])
print(softmax.output)softmax.forward([[-2, -1, 0]]) # subtracted 3 - max from the list
print(softmax.output)softmax.forward([[0.5, 1, 1.5]])
print(softmax.output)
>>>
[[0.09003057 0.24472847 0.66524096]]
[[0.09003057 0.24472847 0.66524096]]
[[0.18632372 0.30719589 0.50648039]]

输出置信度由于指数化的非线性特性而发生了变化。这是一个例子,说明为什么我们需要以相同的方式缩放所有输入到神经网络的数据,我们将在第22章中进一步详细解释。


现在,我们可以添加另一个密集层作为输出层,设置它包含与前一层输出数量相同的输入和与我们数据包含的类别数量相同的输出。然后我们可以将softmax激活应用到这个新层的输出上:

import numpy as np
import nnfs
from nnfs.datasets import spiral_datannfs.init()##NN
class Layer_Dense:# Layer initializationdef __init__(self, n_inputs, n_neurons):# Initialize weights and biasesself.weights = 0.01 * np.random.randn(n_inputs, n_neurons)self.biases = np.zeros((1, n_neurons))# Forward passdef forward(self, inputs):# Calculate output values from inputs, weights and biasesself.output = np.dot(inputs, self.weights) + self.biases# ReLU activation
class Activation_ReLU:# Forward passdef forward(self, inputs):# Calculate output values from inputself.output = np.maximum(0, inputs)# Softmax activation
class Activation_Softmax:# Forward passdef forward(self, inputs):# Get unnormalized probabilitiesexp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))# Normalize them for each sampleprobabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)self.output = probabilities# Create dataset
X, y = spiral_data(samples=100, classes=3)# Create Dense layer with 2 input features and 3 output values
dense1 = Layer_Dense(2, 3)# Create ReLU activation (to be used with Dense layer):
activation1 = Activation_ReLU()# Create second Dense layer with 3 input features (as we take output
# of previous layer here) and 3 output values
dense2 = Layer_Dense(3, 3)# Create Softmax activation (to be used with Dense layer):
activation2 = Activation_Softmax()# Make a forward pass of our training data through this layer
dense1.forward(X)# Make a forward pass through activation function
# it takes the output of first dense layer here
activation1.forward(dense1.output)# Make a forward pass through second Dense layer
# it takes outputs of activation function of first layer as inputs
dense2.forward(activation1.output)# Make a forward pass through activation function
# it takes the output of second dense layer here
activation2.forward(dense2.output)# Let's see output of the first few samples:
print(activation2.output[:5])
>>>
[[0.33333334 0.33333334 0.33333334][0.3333332  0.3333332  0.33333364][0.3333329  0.33333293 0.3333342 ][0.3333326  0.33333263 0.33333477][0.33333233 0.3333324  0.33333528]]

正如你所看到的,预测的分布几乎是相等的,因为每个样本对每个类的预测大约是33%(0.33)。这是由于权重的随机初始化(来自正态分布的抽取,不是每次随机初始化都会得到这个结果)和偏置归零造成的。这些输出也是我们的“置信度得分”。为了确定模型选择的预测分类,我们对这些输出执行 argmax 操作,它检查输出分布中哪个类别的置信度最高,并返回其索引 - 预测的类别索引。也就是说,置信度得分可以和类别预测本身一样重要。例如,[0.22, 0.6, 0.18] 的 argmax 与 [0.32, 0.36, 0.32] 的 argmax 相同。在这两种情况下,argmax 函数都会返回索引值 1(在Python的从零开始的索引系统中是第二个元素),但显然,60%的置信度比36%的置信度要好得多。

import numpy as npa = [0.22, 0.6, 0.18]
b = [0.32, 0.36, 0.32]print(np.argmax(a))
print(np.argmax(b))
>>>
1
1

我们已经完成了通过我们的模型进行前向传递数据所需的步骤。我们在隐藏层使用了整流线性(ReLU)激活函数,它是基于每个神经元工作的。我们还在输出层使用了Softmax激活函数,因为它接受非标准化的值作为输入,并输出概率分布,我们将其用作每个类别的置信度得分。回想一下,尽管神经元是相互连接的,它们各自拥有各自的权重和偏置,并且彼此之间不是“标准化”的。

正如你所见,我们的示例模型目前是随机的。为了解决这个问题,我们需要一种方法来计算神经网络在当前预测中的错误程度,并开始调整权重和偏置以随时间减少错误。因此,我们的下一步是通过所定义的损失函数来量化模型的错误程度。

代码的可视化:https://nnfs.io/ch4

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/60102.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

22.oop-strust与class

OOP是什么&#xff1a;oop 是面向对象编程,面向对象编程是一种计算机编程架构, OOP 的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或 对象组、合而成。 OOP有什么特性&#xff1a; 1、封装性&#xff1a;也称为信息隐藏&#xff0c;就是将一个类的使用和实现分开…

【Linux】ubuntu安装图形化界面步骤

一、ubuntu 安装桌面环境 1、更新软件包列表&#xff08;命令↓&#xff09; sudo apt update 2、安装桌面环境GNOME&#xff08;命令↓&#xff09; sudo apt install ubuntu-desktop 3、安装完成后需要重启服务器&#xff08;服务器重启命令↓&#xff09; sudo reboot 二、…

【Android】轮播图——Banner

引言 Banner轮播图是一种在网页和移动应用界面设计中常见的元素&#xff0c;主要用于在一个固定的区域内自动或手动切换一系列图片&#xff0c;以展示不同的内容或信息。这个控件在软件当中经常看到&#xff0c;商品促销、热门歌单、头像新闻等等。它不同于ViewPgaer在于无需手…

Vue2 doc、excel、pdf、ppt、txt、图片以及视频等在线预览

Vue2 doc、excel、pdf、ppt、txt、图片等在线预览 安装使用目录结构直接上代码src\components\FileView\doc\index.vuesrc\components\FileView\excel\index.vuesrc\components\FileView\img\index.vuesrc\components\FileView\pdf\index.vuesrc\components\FileView\ppt\index…

[OpenGL]使用OpenGL实现硬阴影效果

一、简介 本文介绍了如何使用OpenGL实现硬阴影效果&#xff0c;并在最后给出了全部的代码。本文基于[OpenGL]渲染Shadow Map&#xff0c;实现硬阴影的流程如下&#xff1a; 首先&#xff0c;以光源为视角&#xff0c;渲染场景的深度图&#xff0c;将light space中的深度图存储…

微服务是什么 SpringCloud是什么

微服务是什么 SpringCloud是什么 一、微服务概述二、微服务架构三、单体架构四、分布式架构五、SpringCloud概述六、SpringBoot和 SpringCloud的区别与联系七、SpringCloud版本选择 一、微服务概述 微服务&#xff08;MicroServices&#xff09;最初是由 Martin Fowler 于 2014…

K8S node节点没有相应的pod镜像运行故障处理办法

查看从节点状态 kubectl describe node k8s-node1以下是报错提示 解决办法 需要处理node1节点上的磁盘空间&#xff0c;磁盘空间需要在85%内 处理后的状态 处理正常

CSS教程(四)- 字体

1、尺寸单位 px 像素单位% 百分比&#xff0c;参照父元素对应属性的值进行计算em 字体尺寸单位&#xff0c;参照父元素的字体大小计算&#xff0c;1em16pxrem字体尺寸单位,参照根元素的字体大小计算&#xff0c;1rem16px 2、字体属性 介绍 CSS Fonts (字体)属性用于定义字体…

使用storcli工具配置RAID,收藏这一篇就够了

正文共&#xff1a;1888 字 28 图&#xff0c;预估阅读时间&#xff1a;2 分钟 前文&#xff08;怎么把银河麒麟系统装进U盘&#xff1f;&#xff09;提到&#xff0c;因为国产服务器固件暂不支持直接配置RAID&#xff0c;所以需要使用storcli这个工具来操作。今天就从RAID配置…

Three.js 搭建3D隧道监测

Three.js 搭建3D隧道监测 Three.js 基础元素场景scene相机carema网络模型Mesh光源light渲染器renderer控制器controls 实现3d隧道监测基础实现道路实现隧道实现多个摄像头点击模型进行属性操作实现点击模型发光效果 性能监视器stats引入使用 总结完整代码 我们将通过three.js技…

漫谈分布式唯一ID

文章目录 本系列前言UUIDDB自增主键Redis incr命令号段模式雪花算法 本系列 漫谈分布式唯一ID&#xff08;本文&#xff09;分布式唯一ID生成&#xff08;二&#xff09;&#xff1a;leaf分布式唯一ID生成&#xff08;三&#xff09;&#xff1a;uid-generator分布式唯一ID生成…

CSS教程(七)- 背景

介绍 背景属性可以设置背景颜色、背景图片、背景平铺、背景图片位置、背景图像固定等。 1 背景颜色 属性名&#xff1a;background-color 作用&#xff1a;指定HTML元素的背景色。 取值&#xff1a;英文颜色、16进制、rgb、rgba、transparent&#xff08;一般为透明&#…

网络安全SQL初步注入2

六.报错注入 mysql函数 updatexml(1,xpath语法,0) xpath语法常用concat拼接 例如: concat(07e,(查询语句),07e) select table_name from information_schema.tables limit 0,1 七.宽字节注入(如果后台数据库的编码为GBK) url编码:为了防止提交的数据和url中的一些有特殊意…

three.js 杂记

在Three.js中&#xff0c;Object3D是所有3D对象的基类&#xff0c;而Group是Object3D的一个子类。Group的目的是为了简化处理多个对象的集合。当你将对象添加到Group中时&#xff0c;它们会以一个单元格的形式被处理&#xff0c;参与Group的某些操作&#xff0c;例如位置更新、…

一文熟悉新版llama.cpp使用并本地部署LLAMA

0. 简介 最近是快到双十一了再给大家上点干货。去年我们写了一个大模型的系列&#xff0c;经过一年&#xff0c;大模型的发展已经日新月异。这一次我们来看一下使用llama.cpp这个项目&#xff0c;其主要解决的是推理过程中的性能问题。主要有两点优化&#xff1a; llama.cpp …

基于 Python flask 的微博舆论情感分析,微博大数据可视化系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【编码】【特征选择】【降维】

简要介绍 编码&#xff08;Encoding&#xff09; 编码是将原始数据转换为模型能够理解和处理的格式的过程。常见的编码方法包括&#xff1a; 标签编码&#xff08;Label Encoding&#xff09;&#xff1a; 适用于类别较少的分类数据。将每个类别映射到一个唯一的整数。独热编…

大数据之多级缓存方案

多级缓存介绍&#xff1f;多级缓存优缺点&#xff0c;应用场景&#xff1f;多级缓存架构&#xff1f; 多级缓存介绍 多级缓存方案是一种优化手段&#xff0c;通过在多个级别上存储数据来提高应用程序的性能和响应速度。以下是对多级缓存方案的详细解析&#xff1a; 一、多级缓…

HBuilderX运行微信小程序,编译的文件在哪,怎么运行

1. 点击HBuilderX顶部的运行-运行到小程序模拟器-微信开发者工具&#xff0c;就会开始编译 2. 编译完成后的文件在根目录找到 unpackage -- dist -- dev -- mp-weixin, 这里面就是编译后的文件&#xff0c;如果未跳转到开发者工具&#xff0c;那可能是没设置启动路径&#xff0…

Git超详细笔记包含IDEA整合操作

git超详细笔记 文章目录 git超详细笔记第1章Git概述1.1、何为版本控制1.2、为什么需要版本控制1.3、版本控制工具1.4 、Git简史1.5、Git工作机制1.6 、Git和代码托管中心 第2章Git安装第3章Git常用命令3.1、设置用户签名3.2、初始化本地库本地库&#xff08;Local Repository&a…