《------往期经典推荐------》
二、机器学习实战专栏【链接】,已更新31期,欢迎关注,持续更新中~~
三、深度学习【Pytorch】专栏【链接】
四、【Stable Diffusion绘画系列】专栏【链接】
五、YOLOv8改进专栏【链接】,持续更新中~~
六、YOLO性能对比专栏【链接】,持续更新中~
《------正文------》
目录
引言
人工神经网络是最强大的,同时也是最复杂的机器学习模型。它们对于传统机器学习算法效果不好的复杂任务特别有效。神经网络的主要优势是它们能够学习数据中复杂的模式和关系,即使数据是高维或非结构化的。
许多文章讨论了神经网络背后的数学原理,如不同的激活函数,前向和反向传播算法,梯度下降和优化方法进行了详细讨论。`在这篇文章中,我们采用了不同的方法,逐层呈现了对神经网络的可视化理解。我们将首先关注单层神经网络在分类和回归问题中的视觉解释,以及它们与其他机器学习模型的相似性。然后我们将讨论隐藏层和非线性激活函数的重要性。所有的可视化都是使用Python创建的。
分类问题
我们从分类问题开始。最简单的分类问题是二进制分类,其中目标只有两个类别或标签。如果目标有两个以上的标签,那么我们有一个多分类问题。
单层网络:感知器
单层神经网络是人工神经网络的最简单形式。这里我们只有一个接收输入数据的输入层和一个产生网络输出的输出层。在这个网络中,输入层不被认为是真正的层,因为它只是传递输入数据。这就是为什么这种架构被称为单层网络。Perceptron是有史以来创建的第一个神经网络,是单层神经网络最简单的例子。
感知器由Frank Rosenblatt于1957年发明。他认为感知器可以模拟大脑的原理,具有学习和决策的能力。最初的感知器被设计用于解决二进制分类问题。
图1显示了感知器的架构。输入数据具有n个特征,用x_1到x_n表示。目标y只有两个标签(y=0和y=1)。
图1
输入层接收数据并将其传递到输出层。输出层中的神经元计算输入特征的加权和。每一个输入特征,x都与权重w相关联。神经元将每个输入乘以其相应的权重,并将结果相加。偏置项w0也被添加到该和中。如果我们用z表示和,则:
激活函数是阶跃函数,定义为:
该激活函数如图2所示。
图2
感知器的输出由y^表示,计算如下:
为了可视化感知器的工作方式,我们使用了一个简单的训练数据集,只有两个特征x1和x2,它是随机定义的,目标y只有两个标签(y=0和y=1)。数据集如图3所示。具体代码如下:
# Listing 1
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import random
import tensorflow as tf
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, Input
from tensorflow.keras.utils import to_categorical
from tensorflow.keras import backend
np.random.seed(3)
n = 30
X1 = np.random.randn(n,2)
y1 = np.random.choice((0, 1),size=n)
X1[y1>0,0] -= 4
X1[y1>0,1] += 4
scaler = StandardScaler()
X1 = scaler.fit_transform(X1)
plt.figure(figsize=(5, 5))
marker_colors = ['red', 'blue']
target_labels = np.unique(y1)
n = len(target_labels)
for i, label in enumerate(target_labels):
plt.scatter(X1[y1==label, 0], X1[y1==label,1], label="y="+str(label),
edgecolor="white", color=marker_colors[i])
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='best', fontsize=11)
ax = plt.gca()
ax.set_aspect('equal')
plt.xlim([-2.3, 1.8])
plt.ylim([-1.9, 2.2])
plt.show()
图3
本文不详细介绍神经网络的训练过程。我们专注于已经训练好的神经网络的行为。在下面代码中,我们直接使用前面的数据集定义和训练感知器。
# Listing 2
class Perceptron(object):
def __init__(self, eta=0.01, epochs=50):
self.eta = eta
self.epochs = epochs
def fit(self, X, y):
self.w = np.zeros(1 + X.shape[1])
for epoch in range(self.epochs):
for xi, target in zip(X, y):
error = target - self.predict(xi)
self.w[1:] += self.eta * error * xi
self.w[0] += self.eta * error
return self
def net_input(self, X):
return np.dot(X, self.w[1:]) + self.w[0]
def predict(self, X):
return np.where(self.net_input(X) >= 0.0, 1, 0)
perc = Perceptron(epochs=150, eta=0.05)
perc.fit(X1, y1)
现在我们想看看这个模型如何对我们的训练数据集进行分类。因此,我们定义了一个函数来绘制训练好的神经网络的决策边界。下面代码中定义的这个函数在2D空间上创建一个网格,然后使用一个经过训练的模型来预测网格上所有点的目标。具有不同标签的点的颜色不同。因此,可以使用该函数可视化模型的决策边界。
# Listing 3
def plot_boundary(X, y, clf, lims, alpha=1):
gx1, gx2 = np.meshgrid(np.arange(lims[0], lims[1],
(lims[1]-lims[0])/500.0),
np.arange(lims[2], lims[3],
(lims[3]-lims[2])/500.0))
backgd_colors = ['lightsalmon', 'aqua', 'lightgreen', 'yellow']
marker_colors = ['red', 'blue', 'green', 'orange']
gx1l = gx1.flatten()
gx2l = gx2.flatten()
gx = np.vstack((gx1l,gx2l)).T
gyhat = clf.predict(gx)
if len(gyhat.shape)==1:
gyhat = gyhat.reshape(len(gyhat), 1)
if gyhat.shape[1] > 1:
gyhat = gyhat.argmax(axis=1)
gyhat = gyhat.reshape(gx1.shape)
target_labels = np.unique(y)
n = len(target_labels)
plt.pcolormesh(gx1, gx2, gyhat, cmap=ListedColormap(backgd_colors[:n]))
for i, label in enumerate(target_labels):
plt.scatter(X[y==label, 0], X[y==label,1],
label="y="+str(label),
alpha=alpha, edgecolor="white",
color=marker_colors[i])
现在,我们使用这个函数来绘制训练数据集的感知器的决策边界。结果如图4所示。
# Listing 4
plt.figure(figsize=(5, 5))
# Plot the vector w
plt.quiver([0], [0], perc.w[1], perc.w[2], color=['black'],
width=0.008, angles='xy', scale_units='xy',
scale=0.4, zorder=5)
# Plot the boundary
plot_boundary(X1, y1, perc, lims=[-2.3, 1.8, -1.9, 2.2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='best', fontsize=11)
plt.xlim([-2.3, 1.8])
plt.ylim([-1.9, 2.2])
plt.show()
图4
该图清楚地表明,决策边界是一条直线。我们使用感知器的权重定义向量*w*:
这个向量也在图4中绘制,它垂直于感知器的决策边界(向量很小,所以我们在图中缩放它)。我们现在可以解释这些结果背后的数学原因。
对于具有两个特征的数据集,有:
基于等式1,我们知道z=0的所有数据点的预测标签为1。另一方面,z<0的任何数据点的预测标签将为0。因此,决策边界是z=0的数据点的位置,并且它由以下等式定义:
这是一条直线的方程,这条直线的法向量(垂直于这条直线的向量)是:
这解释了为什么决策边界垂直于向量w。
sigmoid函数
感知器可以预测数据点的标签,但不能提供预测概率。事实上,这个网络无法告诉你它对自己的预测有多自信。我们需要一个不同的激活函数sigmoid来获得预测概率。sigmoid激活函数定义如下:
图5给出了该函数的曲线图。
图5
我们知道事件发生的概率是0到1之间的一个数。由于该图显示了sigmoid函数的范围是(0,1),因此它可以用来表示结果的概率。现在,我们将感知器的激活函数替换为sigmoid函数,以获得图6所示的网络。
图6
在这个网络中,我们用p表示网络的输出,所以我们可以写:
这里p是预测标签为1的概率(y^=1)。为了获得预测目标,我们必须将此概率与默认为0.5的阈值进行比较:
为了可视化这个网络,我们使用之前定义的数据集来训练它。使用keras
库创建这个网络。
# Listing 5
np.random.seed(0)
random.seed(0)
tf.random.set_seed(0)
model1 = Sequential()
model1.add(Dense(1, activation='sigmoid', input_shape=(2,)))
model1.compile(loss = 'binary_crossentropy',
optimizer='adam', metrics=['accuracy'])
model1.summary()
这个神经网络的代价函数称为交叉熵。接下来,我们使用定义的数据集来训练这个模型。
# Listing 6
history1 = model1.fit(X1, y1, epochs=1500, verbose=0, batch_size=X1.shape[0])
plt.plot(history1.history['accuracy'])
plt.title('Accuracy vs Epochs')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.show()
图7显示了该模型的准确度与训练轮数的关系图。
图7
在训练模型之后,我们可以查看输出层的权重(w和w)。
# Listing 7
output_layer_weights = model1.layers[0].get_weights()[0]
model1_w1, model1_w2 = output_layer_weights[0, 0], output_layer_weights[1, 0]
最后,我们画出了这个网络的决策边界。结果如图8所示。
# Listing 8
plt.figure(figsize=(5, 5))
# Plot the vector w
output_layer_weights = model1.layers[0].get_weights()[0]
plt.quiver([0], [0], model1_w1,
model1_w2, color=['black'],
width=0.008, angles='xy', scale_units='xy',
scale=1, zorder=5)
# Plot the boundary
plot_boundary(X1, y1, model1, lims=[-2.3, 1.8, -1.9, 2.2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='best', fontsize=11)
plt.xlim([-2.3, 1.8])
plt.ylim([-1.9, 2.2])
plt.show()
图8
我们再次看到决策边界是一条直线。我们使用输出层的权重定义向量*w*:
向量*w***垂直于决策边界,就像我们在感知器中看到的那样。让我们解释这些结果背后的数学原因。根据等式2,p=0.5的所有数据点的预测标签为1。另一方面,p<0.5的任何数据点的预测标签将为0。因此,决策边界是p=0.5的所有数据点的位置:
因此,决策边界是由以下等式定义的所有数据点的位置:
如前所述,这是一条直线的方程,这条直线的法向量(垂直于这条直线的向量)是:
添加更多功能
到目前为止,我们只考虑了一个只有两个特征的小数据集。让我们看看当我们有三个特征时会发生什么。下面代码定义了另一个具有3个特性的数据集。该数据集如图9所示。
# Listing 9
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X2[y2==0, 0], X2[y2==0,1], X2[y2==0,2],
label="y=0", alpha=0.8, color="red")
ax.scatter(X2[y2==1, 0], X2[y2==1,1], X2[y2==1,2],
label="y=1", alpha=0.8, color="blue")
ax.legend(loc="upper left", fontsize=12)
ax.set_xlabel("$x_1$", fontsize=18)
ax.set_ylabel("$x_2$", fontsize=18)
ax.set_zlabel("$x_3$", fontsize=15, labelpad=-0.5)
ax.view_init(5, -50)
plt.show()
图9
现在,我们用一个sigmoid神经元创建一个新的网络,并使用这个数据集训练它。
# Listing 10
backend.clear_session()
np.random.seed(0)
random.seed(0)
tf.random.set_seed(0)
model2 = Sequential()
model2.add(Dense(1, activation='sigmoid', input_shape=(3,)))
model2.compile(loss = 'binary_crossentropy',
optimizer='adam', metrics=['accuracy'])
history2 = model2.fit(X2, y2, epochs=1500, verbose=0,
batch_size=X2.shape[0])
接下来,我们查看训练模型中输出层的权重,并在图10中绘制模型的数据点和决策边界。
# Listing 11
model2_w0 = output_layer_biases[0]
model2_w1, model2_w2, model2_w3 = output_layer_weights[0, 0], \
output_layer_weights[1, 0], output_layer_weights[2, 0]
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111, projection='3d')
lims=[-2, 2, -2, 2]
ga1, ga2 = np.meshgrid(np.arange(lims[0], lims[1], (lims[1]-lims[0])/500.0),
np.arange(lims[2], lims[3], (lims[3]-lims[2])/500.0))
ga1l = ga1.flatten()
ga2l = ga2.flatten()
ga3 = -(model2_w0 + model2_w1*ga1l + model2_w2*ga2l) / model2_w3
ga3 = ga3.reshape(500, 500)
ax.plot_surface(ga1, ga2, ga3, alpha=0.5)
ax.quiver([0], [0], [0], model2_w1, model2_w2, model2_w3,
color=['black'], length=0.5, zorder=5)
ax.scatter(X2[y2==0, 0], X2[y2==0,1], X2[y2==0,2],
label="y=0", alpha=0.8, color="red")
ax.scatter(X2[y2==1, 0], X2[y2==1,1], X2[y2==1,2],
label="y=1", alpha=0.8, color="blue")
ax.legend(loc="upper left", fontsize=12)
ax.set_xlabel("$x_1$", fontsize=16)
ax.set_ylabel("$x_2$", fontsize=16)
ax.set_zlabel("$x_3$", fontsize=15, labelpad=-0.5)
ax.view_init(5, -50)
plt.show()
图10
如图所示,决策边界是垂直于向量的平面
其使用输出层的权重形成。这里,决策边界计算如下:
所以,决策边界就是这个方程的解
这是一个平面的方程,矢量***w(***在方程3中定义)是这个平面的法向矢量。
线性分类器
如果输入数据中有3个以上的特征,会发生什么?我们可以很容易地扩展相同的想法,找到一个感知器或sigmoid神经元的n个特征的网络的决策边界。在这两种情况下,决策边界都是这个方程的解:
这个方程描述了n维空间中垂直于向量的超平面
在2D空间中,超平面变成一维线,而在3D空间中,它变成2D平面。一条直线或一个平面没有曲率,虽然我们不能在更高的维度上想象超平面,但概念是一样的。在n维空间中,超平面是平坦且没有曲率的n-1维子空间。
在机器学习中,线性分类器是一种基于输入特征的线性组合做出决策的分类模型。因此,线性分类器的决策边界是超平面。感知器和sigmoid神经元是线性分类器的两个例子。
值得一提的是,具有交叉熵代价函数的sigmoid神经元相当于逻辑回归模型。下面代码在之前定义的2D数据集上训练逻辑回归模型(来自scikit-learn
库)。该模型的决策边界如图11所示。虽然这是一条直线,但它与图8中的sigmoid神经元所获得的线并不完全相同。
虽然逻辑回归和sigmoid神经元(具有交叉熵代价函数)是等价的模型,但在训练过程中使用不同的方法来找到它们的参数。在神经网络中,随机初始化的梯度下降算法用于训练,然而,逻辑回归模型使用称为lbfgs(有限内存Broyden-Fletcher-Goldfarb-Shanno)的确定性求解器来实现该目的。因此,这两个模型中参数的最终值可能不同,从而改变决策边界线的位置。
# Listing 12
# Comparing with a logistic regression model
lr_model = LogisticRegression().fit(X1, y1)
plt.figure(figsize=(5, 5))
# Plot the boundary
plot_boundary(X1, y1, lr_model, lims=[-2.3, 1.8, -1.9, 2.2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='best', fontsize=11)
plt.xlim([-2.3, 1.8])
plt.ylim([-1.9, 2.2])
plt.show()
图11
多分类和softmax层
到目前为止,我们的注意力一直集中在二元分类问题上。如果数据集的目标有两个以上的标签,那么我们有一个多分类问题,这样的问题需要一个softmax层。假设一个数据集有n个特征,其目标有C个标签。该数据集可用于训练具有softmax层的单层神经网络,如图12所示。
图12
softmax函数是sigmoid函数对多类分类问题的推广,其中目标具有超过2个标签。输出层中的神经元给出输入特征的线性组合:
softmax层的每个输出计算如下:
在该等式中,p表示预测目标等于第i个标签的概率。最后,预测的标签是具有最高概率的标签:
现在我们创建另一个数据集来可视化softmax层。在这个数据集中,我们有两个特征,目标有3个标签。它被绘制在图13中。
# Listing 13
np.random.seed(0)
xt1 = np.random.randn(50, 2) * 0.4 + np.array([2, 1])
xt2 = np.random.randn(50, 2) * 0.7 + np.array([6, 4])
xt3 = np.random.randn(50, 2) * 0.5 + np.array([2, 6])
y3 = np.array(50*[1]+50*[2]+50*[3])
X3 = np.vstack((xt1, xt2, xt3))
scaler = StandardScaler()
X3 = scaler.fit_transform(X3)
plt.figure(figsize=(6, 6))
plt.scatter(X3[y3==1, 0], X3[y3==1,1], label="y=1", alpha=0.7, color="red")
plt.scatter(X3[y3==2, 0], X3[y3==2,1], label="y=2", alpha=0.7, color="blue")
plt.scatter(X3[y3==3, 0], X3[y3==3,1], label="y=3", alpha=0.7, color="green")
plt.legend(loc="best", fontsize=11)
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$x_2$", fontsize=16)
ax = plt.gca()
ax.set_aspect('equal')
plt.show()
图13
接下来,我们创建一个单层神经网络,并使用该数据集对其进行训练。这个网络有一个softmax层。
# Listing 14
backend.clear_session()
np.random.seed(0)
random.seed(0)
tf.random.set_seed(0)
y3_categorical = to_categorical(y3-1, num_classes=3)
model3 = Sequential()
model3.add(Dense(3, activation='softmax', input_shape=(2,)))
model3.compile(loss = 'categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
history3 = model3.fit(X3, y3_categorical, epochs=2200,
verbose=0, batch_size=X3.shape[0])
接下来,我们查看这个网络的权重和偏差:
# Listing 15
output_layer_weights = model3.layers[-1].get_weights()[0]
output_layer_biases = model3.layers[-1].get_weights()[1]
model3_w10, model3_w20, model3_w30 = output_layer_biases[0], \
output_layer_biases[1], output_layer_biases[2]
model3_w1 = output_layer_weights[:, 0]
model3_w2 = output_layer_weights[:, 1]
model3_w3 = output_layer_weights[:, 2]
最后,我们可以绘制这个模型的决策边界。
# Listing 16
plt.figure(figsize=(5, 5))
plt.quiver([1.7], [0.7], model3_w3[0]-model3_w2[0],
model3_w3[1]-model3_w2[1], color=['black'],
width=0.008, angles='xy', scale_units='xy',
scale=1, zorder=5)
plt.quiver([-0.5], [-2.2], model3_w2[0]-model3_w1[0],
model3_w2[1]-model3_w1[1], color=['black'],
width=0.008, angles='xy', scale_units='xy',
scale=1, zorder=5)
plt.quiver([-1.8], [-1.7], model3_w3[0]-model3_w1[0],
model3_w3[1]-model3_w1[1], color=['black'],
width=0.008, angles='xy', scale_units='xy',
scale=1, zorder=5)
plt.text(0.25, 1.85, "$\mathregular{w_3-w_2}$", color="black",
fontsize=12, weight="bold", style="italic")
plt.text(1.2, -1.1, "$\mathregular{w_2-w_1}$", color="black",
fontsize=12, weight="bold", style="italic")
plt.text(-1.5, -0.5, "$\mathregular{w_3-w_1}$", color="black",
fontsize=12, weight="bold", style="italic")
plot_boundary(X3, y3, model3,lims=[-2.2, 2.4, -2.5, 2.1],
alpha= 0.7)
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='best', fontsize=11)
plt.xlim([-2.2, 2.4])
plt.ylim([-2.5, 2.1])
plt.show()
图14
如图14所示,softmax创建了3个决策边界,每个边界都是一条直线。例如,标签1和2之间的决策边界是标签1和2具有相等预测概率的点的位置。因此,我们可以这样写:
通过简化最后一个方程,我们得到:
这又是一条直线的方程。如果我们定义向量wi为:
这条线的法向量可以写为:
因此,决策边界垂直于w2-w1。类似地,可以表明其他决策边界都是直线,并且标签i和j之间的线垂直于向量*w*_j。
一般地,如果我们在训练数据集中有n个特征,则决策边界将是n维空间中的超平面。这里,标签i和j的超平面垂直于向量*w*_j,其中
具有softmax激活的单层神经网络是线性分类器到更高维度的推广。它继续使用超平面来预测目标的标签,但预测所有标签需要多个超平面。
到目前为止,所有显示的数据集都是线性可分的,这意味着我们可以使用超平面来分离具有不同标签的数据点。实际上,数据集很少是线性可分的。在下面的部分中,我们将看看分类非线性可分数据集的困难。
多层网络
下面代码创建了一个不能线性分离的数据集。该数据集如图15所示。
# Listing 17
np.random.seed(0)
n = 1550
Xt1 = np.random.uniform(low=[0, 0], high=[4, 4], size=(n,2))
drop = (Xt1[:, 0] < 3) & (Xt1[:, 1] < 3)
Xt1 = Xt1[~drop]
yt1= np.ones(len(Xt1))
Xt2 = np.random.uniform(low=[0, 0], high=[4, 4], size=(n,2))
drop = (Xt2[:, 0] > 2.3) | (Xt2[:, 1] > 2.3)
Xt2 = Xt2[~drop]
yt2= np.zeros(len(Xt2))
X4 = np.concatenate([Xt1, Xt2])
y4 = np.concatenate([yt1, yt2])
scaler = StandardScaler()
X4 = scaler.fit_transform(X4)
colors = ['red', 'blue']
plt.figure(figsize=(6, 6))
for i in np.unique(y4):
plt.scatter(X4[y4==i, 0], X4[y4==i, 1], label = "y="+str(i),
color=colors[int(i)], edgecolor="white", s=50)
plt.xlim([-1.9, 1.9])
plt.ylim([-1.9, 1.9])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='upper right', fontsize=11, framealpha=1)
plt.show()
图15
这个数据集有两个特征和一个二进制目标。首先,我们尝试用它来训练一个sigmoid神经元。
# Listing 18
backend.clear_session()
np.random.seed(2)
random.seed(2)
tf.random.set_seed(2)
model4 = Sequential()
model4.add(Dense(1, activation='sigmoid', input_shape=(2,)))
model4.compile(loss = 'binary_crossentropy',
optimizer='adam', metrics=['accuracy'])
history4 = model4.fit(X4, y4, epochs=4000, verbose=0,
batch_size=X4.shape[0])
在训练网络之后,我们可以绘制决策边界。图16显示了该图。
# Listing 19
plt.figure(figsize=(5,5))
plot_boundary(X4, y4, model5, lims=[-2, 2, -2, 2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='upper right', fontsize=11, framealpha=1)
plt.xlim([-1.9, 1.9])
plt.ylim([-1.9, 1.9])
plt.show()
图16
正如预期的那样,决策边界是一条直线。然而,在这个数据集中,直线不能将具有不同标签的数据点分开,因为数据集不是线性可分的。我们只能使用该模型分离一小部分数据点,导致预测精度较低。
隐藏层
我们了解到单层神经网络充当线性分类器。因此,在进入输出层之前,我们必须首先将原始数据集转换为线性可分数据集。这正是多层网络中隐藏层的作用。输入层接收来自原始数据集的要素。然后,这些特征被转移到一个或多个隐藏层,这些隐藏层试图将它们变成线性可分离的特征。最后,新的特征被传输到输出层,输出层充当线性分类器。
多层网络的性能取决于隐藏层线性化输入数据集的能力。如果隐藏层无法将原始数据集转换为线性可分离的数据集(或至少接近线性可分离的数据集),则输出层将无法提供准确的分类。
让我们创建一个多层网络,可以使用以前的数据集进行训练。下面代码定义了一个带有一个隐藏层的神经网络,如图17所示。
# Listing 20
backend.clear_session()
np.random.seed(2)
random.seed(2)
tf.random.set_seed(2)
input_layer = Input(shape=(2,))
hidden_layer = Dense(3, activation='relu')(input_layer)
output_layer = Dense(1, activation='sigmoid')(hidden_layer)
model5 = Model(inputs=input_layer, outputs=output_layer)
model5.compile(loss = 'binary_crossentropy', optimizer='adam',
metrics=['accuracy'])
图17
输入层有2个神经元,因为数据集只有两个特征。隐藏层有3个神经元,每个神经元都有一个ReLU(Rectified Linear Unit)激活函数。该非线性激活函数定义如下:
图18显示了ReLU的图。
图18
最后,我们在输出层中有一个S形神经元。现在,我们使用我们的数据集训练这个模型,并绘制决策边界。
# Listing 21
history5 = model5.fit(X4, y4, epochs=2200, verbose=0,
batch_size=X4.shape[0])
plt.figure(figsize=(5,5))
plot_boundary(X4, y4, model5, lims=[-2, 2, -2, 2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='upper right', fontsize=11, framealpha=1)
plt.xlim([-1.9, 1.9])
plt.ylim([-1.9, 1.9])
plt.show()
图19
该模型可以正确地分离标签为0和1的数据点,但决策边界不再是一条直线。模型如何实现这一点?让我们来看看隐藏层和输出层的输出。下面代码绘制了隐藏层的输出(图20)。请注意,我们在隐藏层中有三个神经元,它们的输出用一个A1,一个A2和一个A3来表示。因此,我们需要在3D空间中绘制它们。在这种情况下,输出层的决策边界是分离隐藏空间的数据点的平面。
# Listing 22
hidden_layer_model = Model(inputs=model5.input,
outputs=model5.layers[1].output)
hidden_layer_output = hidden_layer_model.predict(X4)
output_layer_weights = model5.layers[-1].get_weights()[0]
output_layer_biases = model5.layers[-1].get_weights()[1]
w0 = output_layer_biases[0]
w1, w2, w3= output_layer_weights[0, 0], \
output_layer_weights[1, 0], output_layer_weights[2, 0]
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111, projection='3d')
# Plot the bounday
lims=[0, 4, 0, 4]
ga1, ga2 = np.meshgrid(np.arange(lims[0], lims[1], (lims[1]-lims[0])/500.0),
np.arange(lims[2], lims[3], (lims[3]-lims[2])/500.0))
ga1l = ga1.flatten()
ga2l = ga2.flatten()
ga3 = (0.5 - (w0 + w1*ga1l + w2*ga2l)) / w3
ga3 = ga3.reshape(500, 500)
ax.plot_surface(ga1, ga2, ga3, alpha=0.5)
marker_colors = ['red', 'blue']
target_labels = np.unique(y4)
n = len(target_labels)
for i, label in enumerate(target_labels):
ax.scatter(hidden_layer_output[y4==label, 0],
hidden_layer_output[y4==label, 1],
hidden_layer_output[y4==label, 2],
label="y="+str(label),
color=marker_colors[i])
ax.view_init(0, 25)
ax.set_xlabel('$a_1$', fontsize=14)
ax.set_ylabel('$a_2$', fontsize=14)
ax.set_zlabel('$a_3$', fontsize=14)
ax.legend(loc="best")
plt.show()
图20
原始数据集是二维和非线性可分离的。因此,隐藏层将其转换为现在可线性分离的3D数据集。然后,由输出层创建的平面很容易对其进行分类。
因此,我们得出结论,图19中所示的非线性决策边界就像一个错觉,我们在输出层仍然有一个线性分类器。然而,当平面映射到原始2D数据集时,它显示为非线性决策边界(图21)。
图21
维度的游戏
当一个数据点通过神经网络的每一层时,该层中的神经元数量决定了它的维数。这里每个神经元编码一个维度。由于原始数据集是2D的,因此我们需要在输入层中使用两个神经元。隐藏层有三个神经元,因此它将2D数据点转换为3D数据点。额外的维度以某种方式展开输入数据集,并帮助将其转换为线性可分离的数据集。最后,输出层只是3D空间中的线性分类器。
多层网络的性能取决于隐藏层线性化输入数据集的能力。在这个例子中定义的神经网络的隐藏层可以将原始数据集转换为线性可分离的数据集。但实际上,这并不总是可能的。大致线性可分的数据集有时是隐藏层可以产生的最佳结果。因此,某些数据点可能会被输出层错误标记。然而,只要模型的总体准确度足以满足实际应用,这是可以接受的。
此外,通常具有多个隐藏层的神经网络。在这种情况下,隐藏层联合收割机最终会创建一个线性可分离的数据集。
非线性函数的必要性
在隐藏层中使用非线性激活函数(如ReLU)至关重要。我们可以用一个例子来解释非线性激活函数的重要性。让我们用一个线性激活函数来替换前面神经网络中的ReLU激活函数。线性激活函数定义如下:
图22显示了该激活函数的曲线图。
图22
现在让我们使用一个线性激活函数作为图17中的先验神经网络的隐藏层。这个重新设计的神经网络如图23所示。
图23
下面定义了神经网络,并用前面的数据集训练它。决策边界如图24所示。
# Listing 23
backend.clear_session()
np.random.seed(2)
random.seed(2)
tf.random.set_seed(2)
input_layer = Input(shape=(2,))
hidden_layer_linear = Dense(3, activation='linear')(input_layer)
output_layer = Dense(1, activation='sigmoid')(hidden_layer_linear)
model6 = Model(inputs=input_layer, outputs=output_layer)
model6.compile(loss = 'binary_crossentropy',
optimizer='adam', metrics=['accuracy'])
history6 = model6.fit(X4, y4, epochs=1000, verbose=0,
batch_size=X4.shape[0])
plt.figure(figsize=(5,5))
plot_boundary(X4, y4, model6, lims=[-2, 2, -2, 2])
ax = plt.gca()
ax.set_aspect('equal')
plt.xlabel('$x_1$', fontsize=16)
plt.ylabel('$x_2$', fontsize=16)
plt.legend(loc='upper right', fontsize=11, framealpha=1)
plt.xlim([-1.9, 1.9])
plt.ylim([-1.9, 1.9])
plt.show()
图24
我们看到决策边界仍然是一条直线。这意味着隐藏层无法线性化数据集。让我们解释一下原因。由于我们使用线性激活函数,隐藏层的输出如下:
这些方程可以用矢量形式表示:
这意味着,在一个向量空间中的每个数据点都在一个平行于向量的平面上:
下面用向量v1和v2绘制隐藏层的输出。该图显示在图25的右侧。
# Listing 24
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111, projection='3d')
# Plot the bounday
lims=[-3, 4, -3, 4]
ga1, ga2 = np.meshgrid(np.arange(lims[0], lims[1], (lims[1]-lims[0])/500.0),
np.arange(lims[2], lims[3], (lims[3]-lims[2])/500.0))
ga1l = ga1.flatten()
ga2l = ga2.flatten()
ga3 = (0.5 - (w0 + w1*ga1l + w2*ga2l)) / w3
ga3 = ga3.reshape(500, 500)
ax.plot_surface(ga1, ga2, ga3, alpha=0)
marker_colors = ['red', 'blue']
target_labels = np.unique(y4)
n = len(target_labels)
for i, label in enumerate(target_labels):
ax.scatter(hidden_layer_output[y4==label, 0],
hidden_layer_output[y4==label, 1],
hidden_layer_output[y4==label, 2],
label="y="+str(label),
color=marker_colors[i], alpha=0.15)
ax.quiver([0], [0], [0], hidden_layer_weights[0,0],
hidden_layer_weights[0,1], hidden_layer_weights[0,2],
color=['black'], length=1.1, zorder=15)
ax.quiver([0], [0], [0], hidden_layer_weights[1,0],
hidden_layer_weights[1,1], hidden_layer_weights[1,2],
color=['black'], length=1.1, zorder=15)
ax.view_init(30, 100)
ax.set_xlabel('$a_1$', fontsize=14)
ax.set_ylabel('$a_2$', fontsize=14)
ax.set_zlabel('$a_3$', fontsize=14)
ax.legend(loc="best")
plt.show()
图25
空间中的数据点显然是三维的,然而,它们的数学维数是2,因为它们都位于2D平面上。虽然隐藏层有3个神经元,但它不能生成真实的3D数据集。它只能在3D空间中旋转原始数据集,并沿向量v1和v2沿着拉伸。然而,这些操作不会破坏原始数据集的结构,并且转换后的数据集仍然是非线性可分的。因此,由输出层创建的平面无法正确分类数据点。当这个平面映射回2D空间时,它显示为一条直线(图26)。
图26
总之,隐藏层中的神经元数量不是定义转换数据集的数学维度的唯一因素。如果没有非线性激活函数,原始数据集的数学维度不会改变,隐藏层无法达到其目的。
神经网络回归问题
在本节中,我们将看到神经网络如何解决回归问题。在回归问题中,数据集的目标是连续变量。我们首先创建这样一个数据集的示例,并将其绘制在图27中。
# Listing 25
np.random.seed(0)
num_points = 100
X5 = np.linspace(0,1, num_points)
y5 = -(X5-0.5)**2 + 0.25
fig = plt.figure(figsize=(5, 5))
plt.scatter(X5, y5)
plt.xlabel('x', fontsize=14)
plt.ylabel('y', fontsize=14)
plt.show()
图27
单层网络
我们首先尝试单层神经网络。这里输出层有一个带有线性激活函数的神经元。该神经网络如图28所示。
图28
它的输出可以写为:
现在,如果我们对此使用均方误差(MSE)损失函数,它就会变得像线性回归模型。下面代码使用前面的数据集来训练这样一个网络。由于数据集只有一个特征,神经网络最终只有一个神经元(图29)。
图29
# Listing 26
backend.clear_session()
np.random.seed(0)
random.seed(0)
tf.random.set_seed(0)
model6 = Sequential()
model6.add(Dense(1, activation='linear', input_shape=(1,)))
model6.compile(optimizer='adam', loss='mse', metrics=['mse'])
history7 = model6.fit(X5, y5, epochs=500, verbose=0,
batch_size=X5.shape[0])
训练模型后,我们可以绘制其预测与原始数据点的关系图。
# Listing 27
X5_test = np.linspace(0,1, 1000)
yhat1 = model6.predict(X5_test)
fig = plt.figure(figsize=(5, 5))
plt.scatter(X5, y5, label="Train data")
plt.plot(X5_test, yhat1, color="red", label="Prediction")
plt.xlabel('x', fontsize=14)
plt.ylabel('y', fontsize=14)
plt.legend(loc="best", fontsize=11)
plt.show()
图30
因此,我们得出结论,具有线性激活函数和MSE损失函数的单层神经网络的行为类似于线性回归模型。
多层网络
为了学习非线性数据集,我们需要添加隐藏层。图31示出了这样的网络的示例。这里我们有一个带有线性激活函数的隐藏层。
图31
然而,这个神经网络也像线性回归模型一样。为了解释原因,我们首先写出隐藏层的输出:
现在,我们可以计算神经网络的输出:
这意味着使用MSE损失函数,神经网络仍然表现得像线性模型。为了避免这个问题,我们需要在隐藏层中使用非线性激活函数。
在下一个示例中,我们将隐藏层的激活函数替换为ReLU,如图32所示。这里,隐藏层有10个神经元。
图32
下面代码实现并训练这个神经网络。
# Listing 28
backend.clear_session()
np.random.seed(15)
random.seed(15)
tf.random.set_seed(15)
input_layer = Input(shape=(1,))
x = Dense(10, activation='relu')(input_layer)
output_layer = Dense(1, activation='linear')(x)
model7 = Model(inputs=input_layer, outputs=output_layer)
model7.compile(optimizer='adam', loss='mse', metrics=['mse'])
history8 = model7.fit(X5, y5, epochs=1500, verbose=0,
batch_size=X5.shape[0])
hidden_layer_model = Model(inputs=model7.input,
outputs=model7.layers[1].output)
hidden_layer_output = hidden_layer_model.predict(X5_test)
output_layer_weights = model7.layers[-1].get_weights()[0]
output_layer_biases = model7.layers[-1].get_weights()[1]
经过训练,我们最终可以绘制出这个神经网络的预测。
# Listing 29
X5_test = np.linspace(0,1, 1000)
yhat2 = model7.predict(X5_test)
fig = plt.figure(figsize=(5, 5))
plt.scatter(X5, y5, label="Train data", alpha=0.7)
plt.plot(X5_test, yhat2, color="red", label="Prediction")
plt.xlabel('x', fontsize=14)
plt.ylabel('y', fontsize=14)
plt.legend(loc="best", fontsize=11)
plt.show()
图33
我们看到网络现在可以生成非线性预测。让我们来看看隐藏层。下一个清单绘制了隐藏层的输出。图34中的一个示例。输出神经元首先将每个a乘以其对应的权重(w ^[1]a)。最后,它计算以下总和
这是神经网络的预测。图34中绘制了所有这些项。
# Listing 30
fig, axs = plt.subplots(10, 4, figsize=(18, 24))
plt.subplots_adjust(wspace=0.55, hspace=0.2)
for i in range(10):
axs[i, 0].plot(X5_test, hidden_layer_output[:, i], color="black")
axs[i, 1].plot(X5_test,
hidden_layer_output[:, i]*output_layer_weights[i],
color="black")
axs[i, 0].set_ylabel(r'$a_{%d}$' % (i+1), fontsize=21)
axs[i, 1].set_ylabel(r'$w^{[1]}_{%d}a_{%d}$' % (i+1, i+1), fontsize=21)
axs[i, 2].axis('off')
axs[i, 3].axis('off')
axs[i, 0].set_xlabel("x", fontsize=21)
axs[i, 1].set_xlabel("x", fontsize=21)
axs[4, 2].axis('on')
axs[6, 2].axis('on')
axs[4, 2].plot(X5_test, [output_layer_biases]*len(X5_test))
axs[6, 2].plot(X5_test,
(hidden_layer_output*output_layer_weights.T).sum(axis=1))
axs[6, 2].set_xlabel("x", fontsize=21)
axs[4, 2].set_ylabel("$w^{[1]}_0$", fontsize=21)
axs[4, 2].set_xlabel("x", fontsize=21)
axs[6, 2].set_ylabel("Sum", fontsize=21)
axs[5, 3].axis('on')
axs[5, 3].scatter(X5, y5, alpha=0.3)
axs[5, 3].plot(X5_test, yhat2, color="red")
axs[5, 3].set_xlabel("x", fontsize=21)
axs[5, 3].set_ylabel("$\hat{y}$", fontsize=21)
plt.show()
图34
在我们的神经网络中,隐藏层中的每个神经元都有一个ReLU激活函数。我们在图18中显示了ReLU激活函数的图。它由两条在原点相交的直线组成。左边的一个是水平的,而另一个的斜率为1。隐藏层中每个神经元的权重和偏置会修改ReLU的形状。它可以更改交点的位置、这些线的顺序以及非水平线的斜率。之后,输出层的权重也可以改变非水平线的斜率。图35中示出了这种改变的示例。
图35
然后将修改后的ReLU函数组合以近似数据集目标的形状,如图36所示。每个修改后的ReLU函数都有一个简单的结构,但是当它们组合在一起时,可以近似任何连续函数。最后,输出层的偏置被添加到ReLU函数的总和中,以垂直调整它们。
图36
通用逼近定理指出,具有一个包含足够大量神经元的隐藏层的前馈神经网络可以以任何期望的精度逼近输入子集上的任何连续函数,只要激活函数是非常数,有界和连续的。为了在实践中证明这一点,我们使用了上述同一个神经网络,但这次在隐藏层中使用了400个神经元。图37显示了该神经网络的预测。你可以看到,向隐藏层添加更多的神经元可以显著提高神经网络逼近目标的能力。
图37
总结
在本文中,我们介绍了对神经网络的直观理解以及每一层在做出最终预测时所扮演的角色。我们从感知机开始,展示了单层网络的局限性。我们看到,在分类问题中,单层神经网络相当于线性分类器,而在回归问题中,行为就像线性回归模式。解释了隐层和非线性激活函数的作用。在分类问题中,隐藏层试图线性化非线性可分离的数据集。在回归问题中,隐藏层中神经元的输出就像非线性构建块,它们被加在一起以做出最终的预测。
好了,这篇文章就介绍到这里,喜欢的小伙伴感谢给点个赞和关注,更多精彩内容持续更新~~
关于本篇文章大家有任何建议或意见,欢迎在评论区留言交流!