用 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 np

inputs = [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 pass
    def forward(self, inputs):
        # Calculate output values from input
        self.output = np.maximum(0, inputs)

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

import numpy as np
import nnfs
from nnfs.datasets import spiral_data

nnfs.init()

class Layer_Dense:
    # Layer initialization
    def __init__(self, n_inputs, n_neurons):
        # Initialize weights and biases
        self.weights = 0.01 * np.random.randn(n_inputs, n_neurons)
        self.biases = np.zeros((1, n_neurons))
        
    # Forward pass
    def forward(self, inputs):
        # Calculate output values from inputs, weights and biases
        self.output = np.dot(inputs, self.weights) + self.biases
        
# ReLU activation
class Activation_ReLU:
    # Forward pass
    def forward(self, inputs):
        # Calculate output values from input
        self.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 Python
    
print('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 Python
    
print('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 np

layer_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 np

layer_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.172

Another way to think of it w/ a matrix == axis 0: columns:
[15.11   0.451  2.611]

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

import numpy as np

layer_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.172

Another 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 np

layer_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.172

Another 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

So we can sum axis 1, but note the current shape:
[8.395 7.29  2.487]

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

import numpy as np

layer_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.172

Another 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

So 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 pass
    def forward(self, inputs):
        # Get unnormalized probabilities
        exp_values = np.exp(inputs - np.max(inputs, axis=1, 
                                            keepdims=True))
        # Normalize them for each sample
        probabilities = exp_values / np.sum(exp_values, axis=1, 
                                            keepdims=True)
        self.output = probabilities

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

import numpy as np

print(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 exp
  print(np.exp(1000))

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

import numpy as np

print(np.exp(-np.inf), np.exp(0))
>>>
0.0 1.0

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

import numpy as np

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


softmax = 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 pass
    def forward(self, inputs):
        # Get unnormalized probabilities
        exp_values = np.exp(inputs - np.max(inputs, axis=1, 
                                            keepdims=True))
        # Normalize them for each sample
        probabilities = exp_values / np.sum(exp_values, axis=1, 
                                            keepdims=True)
        self.output = probabilities


softmax = 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 pass
    def forward(self, inputs):
        # Get unnormalized probabilities
        exp_values = np.exp(inputs - np.max(inputs, axis=1, 
                                            keepdims=True))
        # Normalize them for each sample
        probabilities = exp_values / np.sum(exp_values, axis=1, 
                                            keepdims=True)
        self.output = probabilities


softmax = 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_data

nnfs.init()

##NN
class Layer_Dense:
    # Layer initialization
    def __init__(self, n_inputs, n_neurons):
        # Initialize weights and biases
        self.weights = 0.01 * np.random.randn(n_inputs, n_neurons)
        self.biases = np.zeros((1, n_neurons))
        
    # Forward pass
    def forward(self, inputs):
        # Calculate output values from inputs, weights and biases
        self.output = np.dot(inputs, self.weights) + self.biases


# ReLU activation
class Activation_ReLU:
    # Forward pass
    def forward(self, inputs):
        # Calculate output values from input
        self.output = np.maximum(0, inputs)


# Softmax activation
class Activation_Softmax:
    # Forward pass
    def forward(self, inputs):
        # Get unnormalized probabilities
        exp_values = np.exp(inputs - np.max(inputs, axis=1, 
                                            keepdims=True))
        # Normalize them for each sample
        probabilities = 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 np

a = [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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从0到1基于LangChain制作一个AI猫娘

前言&#xff1a; 看到B站上的AIVtuber的项目落地了&#xff0c;就心血来潮想制作一个AI的猫娘供自己使用&#xff0c;顺便出一个简单的教程&#xff0c;跳过理论&#xff0c;直接实践&#xff0c;作者也还在学习摸索中&#xff0c;所以有错误可以直接在评论区指正。&#xff0…

【R78/G15 开发板测评】串口打印 DHT11 温湿度传感器、DS18B20 温度传感器数据,LabVIEW 上位机绘制演化曲线

【R78/G15 开发板测评】串口打印 DHT11 温湿度传感器、DS18B20 温度传感器数据&#xff0c;LabVIEW 上位机绘制演化曲线 主要介绍了 R78/G15 开发板基于 Arduino IDE 环境串口打印温湿度传感器 DHT11 和温度传感器 DS18B20 传感器的数据&#xff0c;并通过LabVIEW上位机绘制演…

【CAE SDK】CEETRON 28年应用案例——以船舶、结构仿真、材料成型领域为例

随着计算机辅助工程&#xff08;CAE&#xff09;技术的不断发展&#xff0c;虚拟仿真工具在工程设计、分析和优化中发挥着越来越重要的作用。CAE系统涵盖了前处理、求解、后处理等多个环节&#xff0c;极大地提升了工程仿真的准确性和效率。 CEETRON SDK作为虚拟仿真领域的一款…

ThingsBoard规则链节点:RPC Call Reply节点详解

引言 1. RPC Call Reply 节点简介 2. 节点配置 2.1 基本配置示例 3. 使用场景 3.1 设备控制 3.2 状态查询 3.3 命令执行 4. 实际项目中的应用 4.1 项目背景 4.2 项目需求 4.3 实现步骤 5. 总结 引言 ThingsBoard 是一个开源的物联网平台&#xff0c;提供了设备管理…

基于深度学习的路面裂缝检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

“心玲守护”乡村孩子的一片天公益活动在10所学校开展

2023年9月—2024年10月期间&#xff0c;由林志玲女士发起、中国乡村发展基金会支持&#xff0c;并联合重庆市渝中区红樱桃义工协会执行的“心玲守护”乡村孩子的一片天——儿童青少年心理健康援助项目活动&#xff0c;已在重庆市万州区、璧山区、巫山县和湖南省益阳市区域内的1…

计算机网络——1.1计算机网络概述

计算机网络——计算机网络概念 前言 计算机网络是计算机学习中必不可少的一环&#xff0c;甚至可以说&#xff0c;是离我们普通人日常生活最近的计算机知识。为什么呢&#xff1f;因为我们上网上网&#xff0c;都离不开计算机网络&#xff0c;打游戏&#xff0c;刷剧&#xff…

使用HtmlAgilityPack+PuppeteerSharp+iText7抓取IdentityServer4帮助文档

需要学习IdentityServer4的用法&#xff0c;但是在IdentityServer4帮助文档网站&#xff08;参考文献1&#xff09;中没有找到下载离线文档的地方&#xff0c;准备使用HtmlAgilityPackPuppeteerSharpiText7将网站内容抓取生成离线PDF文档&#xff0c;便于本机学习、查看。   …

热烈庆祝,2024年11月9日(星期六)骑行马刺沟顺利结束

晨光微露&#xff1a;蓄势待发清晨的第一缕阳光穿透薄雾&#xff0c;照亮了集合现场。我们校长群的骑行爱好者们早早地聚集在约定地点&#xff0c;检查装备、调整车辆&#xff0c;彼此间寒暄着&#xff0c;兴奋之情溢于言表。随着一声令下&#xff0c;队伍正式出发&#xff0c;…

python数据分析|二 IPython和JupyterNotebooks

一 python 解释器 Python解释器同一时间只能运行一个程序的一条语句。 如何适用&#xff1a; win r cmd 要退出Python解释器返回终端&#xff0c;可以输入 exit() 或 Ctrl-D。 假设创建了一个 hello_world.py 文件&#xff0c;它的内容是&#xff1a; 可以用下面的命令运…

【持续更新】【NLP项目】【自然语言处理】智能聊天机器人——“有问必答”【Chatbot】第2章、《模式一:问候模式》

智能聊天机器人——“有问必答” 【注】该项目已开源&#xff0c;开源地址为&#xff1a;链接&#xff0c;代码更新可能不及时。 第2章、《模式一&#xff1a;问候模式》 主窗体的布局如下图所示&#xff1a; 共九种功能模式&#xff0c;最下方为关闭窗口按钮。 点击问候模…

@RestController 源码解读:解决 Web 开发中 REST 服务的疑难杂症

目录 一、RestContrller注解 1.1 查看底层源码 1.2 AliasFor注解说明 1.2.1 注解别名 1.2.2 元数据别名 1.3 value() 方法的作用 一、RestContrller注解 1.1 查看底层源码 首先编写如下内容&#xff1a; RestController public class TestController {} 按住 Ctrl &am…

【Android】轮播图——Banner

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

游戏引擎学习第一天

视频参考: https://www.bilibili.com/video/BV1zGDCYHErA/ 创建一个保存项目的路径 VS的安装略过&#xff0c;个人自行百度 1. vs 创建第一个CMAKE的窗口项目 game.cpp 修改如下的代码 到https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-winmain 去…

ArcGIS软件之“计算面积几何”地图制作

目录 一、消防站的泰森多边形ex12二、人口调查的泰森多边形三、人口调查的泰森多边形属性设置四、计算面积几何&#xff0c;用于求密度五、求密度六、给“现有中学”属性 R1赋值七、“现有中学”设置多环缓存区 并为它赋值八、“土地使用”为不同的功能区赋值九、三个图层相交十…

Rust @绑定(Rust@绑定)(在模式匹配的同时将值绑定到变量)

文章目录 Rust中的绑定基础概念示例&#xff1a;基本模式匹配 绑定的使用示例&#xff1a;范围匹配并绑定变量 深入探索绑定的好处示例&#xff1a;复杂数据结构中的应用 总结 附加 Rust中的绑定 Rust 语言以其强类型系统和内存安全的特性著称。在进行模式匹配时&#xff0c;R…

使用EasyExcel实现导出excel文件时生成多级下拉选

前言 公司有个需求本来只涉及到两个下拉选项&#xff0c;后面就想能不能实现多个下拉选&#xff0c;当然我这里说的多个下拉选是联动的&#xff0c;比如省、地市、区县这种。 实现步骤 1、添加EasyExcel的Maven依赖 <dependency><groupId>com.alibaba</group…

海量小文件挑战下的CephFS:优化策略与实践探索

文章目录 1.背景2.基本概念2.1 CephFS IO流程2.2 Ceph-FUSE 3. 问题3.1 问题源起3.2 理论分析3.3 原因排查3.3.1 Ceph-FUSE日志分析3.3.2 提出猜想3.3.3 代码验证3.3.3.1 MDS端3.3.3.2 Ceph-FUSE端 3.4 小结 1.背景 随着大数据、人工智能技术的蓬勃发展&#xff0c;人类对于算…

编写一个脚本实现参数的远程主机网络探测python test_ip.py 192.168.0.10~192.168.0.100(sys模块)

""" 编写一个脚本实现参数的远程主机网络探测python test_ip.py 192.168.0.10~192.168.0.100 """ #导入模块 #读取起始IP&#xff0c;结束IP import sys start_ip sys.argv[1] end_ip sys.argv[2] # print(start_ip,end_ip)##########组装数据…

lvgl: 示例入门

目录 1. A very simple hello world label 2. A button with a label and react on click event 3. Create styles from scratch for buttons 4. Create a slider and write its value on a label 1. A very simple hello world label void _lv_example_get_started_1(void) …