【超详细】神经网络的可视化解释

《------往期经典推荐------》

一、AI应用软件开发实战专栏【链接】

项目名称 项目名称
1.【人脸识别与管理系统开发 2.【车牌识别与自动收费管理系统开发
3.【手势识别系统开发 4.【人脸面部活体检测系统开发
5.【图片风格快速迁移软件开发 6.【人脸表表情识别系统
7.【YOLOv8多目标识别与自动标注软件开发 8.【基于YOLOv8深度学习的行人跌倒检测系统
9.【基于YOLOv8深度学习的PCB板缺陷检测系统 10.【基于YOLOv8深度学习的生活垃圾分类目标检测系统
11.【基于YOLOv8深度学习的安全帽目标检测系统 12.【基于YOLOv8深度学习的120种犬类检测与识别系统
13.【基于YOLOv8深度学习的路面坑洞检测系统 14.【基于YOLOv8深度学习的火焰烟雾检测系统
15.【基于YOLOv8深度学习的钢材表面缺陷检测系统 16.【基于YOLOv8深度学习的舰船目标分类检测系统
17.【基于YOLOv8深度学习的西红柿成熟度检测系统 18.【基于YOLOv8深度学习的血细胞检测与计数系统
19.【基于YOLOv8深度学习的吸烟/抽烟行为检测系统 20.【基于YOLOv8深度学习的水稻害虫检测与识别系统
21.【基于YOLOv8深度学习的高精度车辆行人检测与计数系统 22.【基于YOLOv8深度学习的路面标志线检测与识别系统
23.【基于YOLOv8深度学习的智能小麦害虫检测识别系统 24.【基于YOLOv8深度学习的智能玉米害虫检测识别系统
25.【基于YOLOv8深度学习的200种鸟类智能检测与识别系统 26.【基于YOLOv8深度学习的45种交通标志智能检测与识别系统
27.【基于YOLOv8深度学习的人脸面部表情识别系统 28.【基于YOLOv8深度学习的苹果叶片病害智能诊断系统
29.【基于YOLOv8深度学习的智能肺炎诊断系统 30.【基于YOLOv8深度学习的葡萄簇目标检测系统
31.【基于YOLOv8深度学习的100种中草药智能识别系统 32.【基于YOLOv8深度学习的102种花卉智能识别系统
33.【基于YOLOv8深度学习的100种蝴蝶智能识别系统 34.【基于YOLOv8深度学习的水稻叶片病害智能诊断系统
35.【基于YOLOv8与ByteTrack的车辆行人多目标检测与追踪系统 36.【基于YOLOv8深度学习的智能草莓病害检测与分割系统
37.【基于YOLOv8深度学习的复杂场景下船舶目标检测系统 38.【基于YOLOv8深度学习的农作物幼苗与杂草检测系统
39.【基于YOLOv8深度学习的智能道路裂缝检测与分析系统 40.【基于YOLOv8深度学习的葡萄病害智能诊断与防治系统
41.【基于YOLOv8深度学习的遥感地理空间物体检测系统 42.【基于YOLOv8深度学习的无人机视角地面物体检测系统
43.【基于YOLOv8深度学习的木薯病害智能诊断与防治系统 44.【基于YOLOv8深度学习的野外火焰烟雾检测系统
45.【基于YOLOv8深度学习的脑肿瘤智能检测系统 46.【基于YOLOv8深度学习的玉米叶片病害智能诊断与防治系统
47.【基于YOLOv8深度学习的橙子病害智能诊断与防治系统 48.【基于深度学习的车辆检测追踪与流量计数系统
49.【基于深度学习的行人检测追踪与双向流量计数系统 50.【基于深度学习的反光衣检测与预警系统
51.【基于深度学习的危险区域人员闯入检测与报警系统 52.【基于深度学习的高密度人脸智能检测与统计系统
53.【基于深度学习的CT扫描图像肾结石智能检测系统 54.【基于深度学习的水果智能检测系统
55.【基于深度学习的水果质量好坏智能检测系统 56.【基于深度学习的蔬菜目标检测与识别系统
57.【基于深度学习的非机动车驾驶员头盔检测系统 58.【太基于深度学习的阳能电池板检测与分析系统
59.【基于深度学习的工业螺栓螺母检测 60.【基于深度学习的金属焊缝缺陷检测系统
61.【基于深度学习的链条缺陷检测与识别系统 62.【基于深度学习的交通信号灯检测识别
63.【基于深度学习的草莓成熟度检测与识别系统 64.【基于深度学习的水下海生物检测识别系统
65.【基于深度学习的道路交通事故检测识别系统 66.【基于深度学习的安检X光危险品检测与识别系统
67.【基于深度学习的农作物类别检测与识别系统 68.【基于深度学习的危险驾驶行为检测识别系统
69.【基于深度学习的维修工具检测识别系统 70.【基于深度学习的维修工具检测识别系统
71.【基于深度学习的建筑墙面损伤检测系统 72.【基于深度学习的煤矿传送带异物检测系统
73.【基于深度学习的老鼠智能检测系统 74.【基于深度学习的水面垃圾智能检测识别系统
75.【基于深度学习的遥感视角船只智能检测系统 76.【基于深度学习的胃肠道息肉智能检测分割与诊断系统
77.【基于深度学习的心脏超声图像间隔壁检测分割与分析系统 78.【基于深度学习的心脏超声图像间隔壁检测分割与分析系统
79.【基于深度学习的果园苹果检测与计数系统 80.【基于深度学习的半导体芯片缺陷检测系统
81.【基于深度学习的糖尿病视网膜病变检测与诊断系统 82.【基于深度学习的运动鞋品牌检测与识别系统

二、机器学习实战专栏【链接】 ,已更新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^表示,计算如下:

为了可视化感知器的工作方式,我们使用了一个简单的训练数据集,只有两个特征x1x2 ,它是随机定义的,目标y 只有两个标签(y =0和y=1)。数据集如图3所示。具体代码如下:

python 复制代码
# 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

本文不详细介绍神经网络的训练过程。我们专注于已经训练好的神经网络的行为。在下面代码中,我们直接使用前面的数据集定义和训练感知器。

python 复制代码
# 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空间上创建一个网格,然后使用一个经过训练的模型来预测网格上所有点的目标。具有不同标签的点的颜色不同。因此,可以使用该函数可视化模型的决策边界。

python 复制代码
# 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所示。

python 复制代码
# 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库创建这个网络。

python 复制代码
# 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()

这个神经网络的代价函数称为交叉熵。接下来,我们使用定义的数据集来训练这个模型。

python 复制代码
# 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

在训练模型之后,我们可以查看输出层的权重(ww)。

python 复制代码
# 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所示。

python 复制代码
# 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所示。

python 复制代码
# 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神经元创建一个新的网络,并使用这个数据集训练它。

python 复制代码
# 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中绘制模型的数据点和决策边界。

python 复制代码
# 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)的确定性求解器来实现该目的。因此,这两个模型中参数的最终值可能不同,从而改变决策边界线的位置。

python 复制代码
# 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中。

python 复制代码
# 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层。

python 复制代码
# 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])

接下来,我们查看这个网络的权重和偏差:

python 复制代码
# 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]

最后,我们可以绘制这个模型的决策边界。

python 复制代码
# 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 。类似地,可以表明其他决策边界都是直线,并且标签ij 之间的线垂直于向量*w* _j

一般地,如果我们在训练数据集中有n个 特征,则决策边界将是n 维空间中的超平面。这里,标签ij 的超平面垂直于向量*w* _j,其中

具有softmax激活的单层神经网络是线性分类器到更高维度的推广。它继续使用超平面来预测目标的标签,但预测所有标签需要多个超平面。

到目前为止,所有显示的数据集都是线性可分的,这意味着我们可以使用超平面来分离具有不同标签的数据点。实际上,数据集很少是线性可分的。在下面的部分中,我们将看看分类非线性可分数据集的困难。

多层网络

下面代码创建了一个不能线性分离的数据集。该数据集如图15所示。

python 复制代码
# 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神经元。

python 复制代码
# 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显示了该图。

python 复制代码
# 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所示。

python 复制代码
# 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形神经元。现在,我们使用我们的数据集训练这个模型,并绘制决策边界。

python 复制代码
# 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空间中绘制它们。在这种情况下,输出层的决策边界是分离隐藏空间的数据点的平面。

python 复制代码
# 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所示。

python 复制代码
# 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

我们看到决策边界仍然是一条直线。这意味着隐藏层无法线性化数据集。让我们解释一下原因。由于我们使用线性激活函数,隐藏层的输出如下:

这些方程可以用矢量形式表示:

这意味着,在一 个向量空间中的每个数据点都在一个平行于向量的平面上:

下面用向量v1v2绘制隐藏层的输出。该图显示在图25的右侧。

python 复制代码
# 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空间中旋转原始数据集,并沿向量v1v2 沿着拉伸。然而,这些操作不会破坏原始数据集的结构,并且转换后的数据集仍然是非线性可分的。因此,由输出层创建的平面无法正确分类数据点。当这个平面映射回2D空间时,它显示为一条直线(图26)。

图26

总之,隐藏层中的神经元数量不是定义转换数据集的数学维度的唯一因素。如果没有非线性激活函数,原始数据集的数学维度不会改变,隐藏层无法达到其目的。

神经网络回归问题

在本节中,我们将看到神经网络如何解决回归问题。在回归问题中,数据集的目标是连续变量。我们首先创建这样一个数据集的示例,并将其绘制在图27中。

python 复制代码
# 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

python 复制代码
# 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])

训练模型后,我们可以绘制其预测与原始数据点的关系图。

python 复制代码
# 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

下面代码实现并训练这个神经网络。

python 复制代码
# 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中绘制了所有这些项。

python 复制代码
# 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

总结

在本文中,我们介绍了对神经网络的直观理解以及每一层在做出最终预测时所扮演的角色。我们从感知机开始,展示了单层网络的局限性。我们看到,在分类问题中,单层神经网络相当于线性分类器,而在回归问题中,行为就像线性回归模式。解释了隐层和非线性激活函数的作用。在分类问题中,隐藏层试图线性化非线性可分离的数据集。在回归问题中,隐藏层中神经元的输出就像非线性构建块,它们被加在一起以做出最终的预测。


好了,这篇文章就介绍到这里,喜欢的小伙伴感谢给点个赞和关注,更多精彩内容持续更新~~
关于本篇文章大家有任何建议或意见,欢迎在评论区留言交流!

相关推荐
LCG元25 分钟前
大模型驱动的围术期质控系统全面解析与应用探索
人工智能
lihuayong37 分钟前
计算机视觉:主流数据集整理
人工智能·计算机视觉·mnist数据集·coco数据集·图像数据集·cifar-10数据集·imagenet数据集
政安晨1 小时前
政安晨【零基础玩转各类开源AI项目】DeepSeek 多模态大模型Janus-Pro-7B,本地部署!支持图像识别和图像生成
人工智能·大模型·多模态·deepseek·janus-pro-7b
一ge科研小菜鸡1 小时前
DeepSeek 与后端开发:AI 赋能云端架构与智能化服务
人工智能·云原生
冰 河1 小时前
‌最新版DeepSeek保姆级安装教程:本地部署+避坑指南
人工智能·程序员·openai·deepseek·冰河大模型
维维180-3121-14551 小时前
AI赋能生态学暨“ChatGPT+”多技术融合在生态系统服务中的实践技术应用与论文撰写
人工智能·chatgpt
終不似少年遊*1 小时前
词向量与词嵌入
人工智能·深度学习·nlp·机器翻译·词嵌入
杜大哥1 小时前
如何在WPS打开的word、excel文件中,使用AI?
人工智能·word·excel·wps
Leiditech__1 小时前
人工智能时代电子机器人静电问题及电路设计防范措施
人工智能·嵌入式硬件·机器人·硬件工程
谨慎谦虚2 小时前
Trae 体验:探索被忽视的 Chat 模式
人工智能·trae