带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

点击查看第一章
点击查看第二章

第3章

JavaScript深度学习框架
在本章中,我们会介绍用在浏览器上的三种不同的JavaScript深度学习框架:TensorFlow.js、WebDNN和Keras.js。我们将给每个项目、特性做概览式介绍,并提供代码示例运行简单的分类任务。
在3.1节中,我们将介绍TensorFlow.js,它是 Google贡献的一个JavaScript深度学习框架。我们将用核心API实现和训练一个神经网络来解决 XOR 问题,它以前以Deeplearn.js 的名字为人们熟知。
在3.2节,我们将介绍WebDNN框架,它是由东京大学开发的机器学习库。我们将转化一个预训练的ResNet模型,并在WebDNN框架中加载它来进行图像分类。
在3.3 节,我们将讲解第三个JavaScript深度学习框架Keras.js,它可以将Keras模型运行在浏览器上。Keras.js是由Leon Chen(MD.ai 的联合创始人)开发的,通过WebGL 2提供GPU支持。为了比较其他框架,我们将加载预训练的模型,并执行分类任务。

3.1 TensorFlow.js

TensorFlow.js是在2017年中期公开发布的一个开源的 JavaScript 机器学习框架,之前的名字是Deeplearn.js。它设计为运行在浏览器上,利用WebGL加速。虽然它不是第一个利用用户硬件执行深度神经网络(WebDNN和TensorFire已经发布过)的框架,但TensorFlow.js是第一个公开发行的、在浏览器中提供硬件加速的神经网络训练的框架。用户可以直接在浏览器中提供数据,进行实时训练学习,而不用额外安装软件。TensorFlow.js也无须用单独的深度学习框架构建离线的模型。随着浏览器提供数据(例如,网络摄像头、麦克风等)的独特性,TensorFlow.js开辟了许多新应用,这些会在本章的后续节中介绍。
学习新框架的最好的方法之一,是直接学习一个简单的例子,通过每行代码解释核心概念。TensorFlow.js也不例外。我们将通过一个基本的多层感知机学习XOR函数的例子来探索TensorFlow.js库。TensorFlow.js的训练API 是基于动态神经网络创建范式,类似TensorFlow 的Eager、Chainer和PyTorch这样的框架。如果你熟悉这些框架中的任意一个,你将毫不费力地学会TensorFlow.js。即使你不会也不用担心,接着我们会解释每个过程。

3.1.1 TensorFlow.js 介绍

TensorFlow.js的官方网站提供了一个在线演示、实验的神经网络平台(playgroud),无须安装任何其他库。简单地打开浏览器,访问TensorFlow.js官方网站。然后点击右上角的“Try it Live!”按钮。这将会开启一个Codepen,你可以在命名空间tf导入TensorFlow.js的函数库。为了介绍,我们将使用TensorFlow.js的在线演示的神经网络平台运行一个示例。
你可以在TensorFlow.js示例的Github找到示例应用,比如,MNIST分类、Mobilenet模型、情感分析、转移学习,等等。

3.1.2 XOR 问题

为了学习 TensorFlow.js 的核心概念,我们将训练一个多层感知机来学习 XOR 问题。这是一个典型的介绍神经网络的示例,因为使用线性方法解决不了该问题。
简单地说,我们要训练一个分类器去预测两个二值特征的一个二值结果。如果这两个二值特征相同,训练的分类器输出结果必须为0。反之,它必须预测为1。该函数的真值表如图3-1所示。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

为了让例子看起来更有趣,我们假设两个特征输入的取值为-1~1之间的实数值,而不是前面的二值。接着,如果输入都为正值或者都为负值,分类器预测必须为0。否则,分类器预测为1。图3-2是显示的一些样本输入,样本的颜色代表分类。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

正如你从图3-2看到的,没有单个直线可以将所有的样本点分为两个类别。所以上述问题不能用线性方法解决。

3.1.3 解决 XOR 问题

打开TensorFlow.js 在线演示平台,复制下面的代码到 JavaScript 文本框内并运行。即使你不懂下面的代码也不用担心。我们将一行一行地解释代码。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

运行上述代码之后,你会注意到控制台输出分成三部分。
第一部分是一个未训练的神经网络在生成的测试数据上的快速测试。生成100个测试样本点,使用未训练的神经网络模型为每个样本预测结果,并计算准确度。因为这些测试样本点和网络模型的权重都是随机生成的,所以你每次看到的结果可能都不同。然而,你将注意到未训练的网络模型的准确度为50%左右。这只是随机猜测每个样本属于哪个分类,因为我们没有输入训练数据进行学习。
第二部分显示神经网络模型每次迭代的训练误差。可以观察到随着训练过程的进行,误差会随之下降。根据你的计算机的计算速度,这步可能需要几秒钟。
最后,我们用新训练好的神经网络模型重复第一部分的测试。因为我们的数据样本和初始权重是随机的,所以每次运行的最终准确度的结果稍有不同。但是,大部分情况下你会看到准确度超过80%,也就是说,我们的神经网络模型确实学习到了XOR函数。这些都是在浏览器上完成的。

3.1.4 网络架构

在学习代码之前,我们来讨论下学习XOR函数的网络。它是一个朴素的多层感知机,具有2个隐藏层,隐藏层的神经元数目分别是20和5。XOR函数有两个输入和一个二值输出。这是当前比较常见的神经网络,每个隐藏层后接着ReLu激活函数。图3-3显示该神经网络的视图。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

训练神经网络的过程使用Adam优化算法,学习率为 0.01,batch的大小为20,迭代次数为100。
你可能会想,我们是如何想出这个神经网络的。对于一个简单的问题,比如,XOR函数,你可能不需要许多层和神经元。使用朴素的SGD优化算法就可以解决问题,根本不需要Adam优化算法。但是这个例子的目的不是构建最优的网络模型,而只是为了介绍TensorFlow.js的概念。一旦对TensorFlow.js 足够熟悉,你可以根据实际情况进行配置以得到更好的结果。
代码的前面几行定义神经网络模型的常量。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

3.1.5 张量

深度学习框架最常见的概念是张量(Tensor),它只是矩阵的一般化,存储一个多维数组。在TensorFlow.js 中,张量是处理过程中核心的数据结构。
当前,TensorFlow.js支持0维张量(标量)到 4 维张量。
任意神经网络处理的数据都要表示为一个张量,比如训练集、测试集、神经网络权重等。TensorFlow.js通过张量使得用户对WebGL着色器程序的使用无感。本质上,TensorFlow.js处理张量数据从CPU(主要的JavaScript线程)到GPU(WebGL 着色器)的来回的转换,并返回结果。
TensorFlow.js提供许多公共函数创建张量。在XOR解决方案中,我们使用如下的方式创建张量:

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

你会注意到前6行代码都在定义可训练的变量,它们是带有两个隐藏层的多层感知机将要使用的。每个隐藏层初始化它们的权重和偏置。我们也定义输出层的权重和偏置(单值,因为我们的模型只有一个输出神经元)。
我们使用公共函数tf.randomNormal创建权重张量W1。tf.randomNormal返回一个给定形状的张量, 其值从正态分布中随机抽样。 对于W1,它是一个形状为 [dimIn, numNeurons1]的2D张量,其中numNeurons1是第一个隐藏层的神经元数量。第一个值dimIn是输入到神经网络的输入数量,对于XOR问题,这里为2。
这里要指出,张量是不可变的。但是,对于神经网络的权重来说,我们需要其随着迭代训练不断地更新。因为这个特殊的要求,我们封装一个tf.variable调用,将张量转换成一个变量(Variable)。变量是一种特殊的张量,它的值是可变的。
我们使用公共函数tf.zeros创建偏置,该函数类似tf.randomNormal函数。唯一不同的是tf.zeros函数返回的张量的元素值都初始化为0。
最后,定义了两个标量张量eps和one。这些常量将用于后面代码的各种操作中。有一点要注意的是,你不能简单地将一个JavaScript变量和一个张量进行常规的操作。对常量使用张量操作时,我们必须将这些常量表示成一个张量对象。

3.1.6 张量操作

为了在张量之间处理数据,我们必须进行张量操作 (operation)。 跟前面一样,TensorFlow.js 提供了各种张量操作。让我们看下predict函数,该函数传入一个XOR输入的2D张量,为每个输入输出神经网络模型的输出结果。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

你可以看到predict函数是如何使用神经网络的权重和偏置预测一个batch的XOR输入的batch输出结果。input是一个形状为[batchSize, dimIn]的2D张量。
当前可以先忽略tf.tidy函数,第一步是使用下面的张量操作计算第一个隐藏层的输出:

  • 对输入input和权重张量W1进行矩阵乘法。
  • 接着在上一个结果中加上一个偏置b1。
  • 再对上面的结果应用 ReLu 激活函数。

上面这些张量操作可以用下面的一行代码实现。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

让我们来分解上述代码。
所有的张量对象都有方法来做各种操作。对于第一个矩阵乘法操作,我们调用input张量的matMul方法,并传入权重张量W1。在本例子中,操作的输出是一个形状为[batchSize, 20]的2D张量。
因为张量操作的输出也是张量对象,所以你可以在处理的过程中简单地链接下一步,在矩阵乘法的输出上增加一个偏置,只需要调用add方法并传入偏置b1。
最后,链接relu操作,用hidden常量存储第一个隐藏层的输出结果。
第二个隐藏层和输出层的输出结果的计算方法都与第一个隐藏层相似,除了使用的变量不同。输出层使用sigmoid激活函数,该激活函数表示XOR的一个二值输出。然后,你可以使用as1D方法将形状为[batchSize, 1]的输出张量转换成一个[batchSize]的1D张量。这使得损失函数的计算更加方便有效。
注意,对于多分类输出,你可能需要使用softmax激活函数,保证输出是一个形状为[batchSize, dimOut]的2D张量。
对于这个简单的神经网络模型来说predict函数就介绍完了。对于更复杂的神经网络,TensorFlow.js 提供更多的张量操作,比如卷积操作、池化操作和批量归一化(batch normalization)操作。这些操作在官方文档中都有描述。
现在我们介绍损失函数。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

对于单个二值输出,常用的误差指标是对数损失(logarithmic loss)。计算单个预测值与目标值的对数损失的公式如下:

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

其中y是期望输出结果($0$ 或者$1$),p是预测概率(取值范围在$0$~$1$之间)。为了避免取$0$的对数,我们在预测值的结果上增加一个较小的eps。
我们的eoss函数是计算一个batch的预测输出结果与目标值之间的平均损失。预测值和目标值都是一个形状为[batchSize]的1D张量。预测值是指predict函数的输出,目标值是指每个被预测的样本的实际分类。
注意,eoss函数返回一个标量张量。这对于后续的训练过程相当重要。因为该标量张量是优化器要最小化的值。
你会发现损失函数中的tf.add函数不同,这些操作并不需要从一个张量对象的方法中调用。它们是全局tf命名空间中的独立函数。然而,这些不同的仅仅是在语法上,使得你的代码更加方便地使用。
下面介绍tf.tidy的相关细节。无论何时我们创建张量对象,TensorFlow.js为每个张量创建相应的WebGL纹理。因为每个张量操作创建一个新的张量对象,像predict和loss函数中的链式操作都会创建一串新的中间WebGL纹理。在正常情况下,JavaScript的垃圾回收器释放不再引用的内存是没有问题的,但是,不幸的是,这对WebGL纹理不适用。在创建WebGL纹理后,我们需要使用张量的dispose方法手动去释放不再使用的WebGL纹理。这看起来很快会冗长乏味,想象一下每次张量操作后都需要调用dispose方法。
tf.tidy通过跟踪它的闭包函数创建的所有张量对象来解决上述问题。将所有的张量操作简单封装在tf.tidy调用中,所有调用中创建的纹理,不管纹理是否属于返回张量,都会在函数末尾一次性释放。非常方便!

3.1.7 模型训练

在准备好变量和张量操作后,是时候进行模型训练了。在程序的开始使用如下代码定义optimizer变量。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

这将返回一个Optimizer对象,我们可以在train函数中使用,代码如下。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

首先,train函数是异步的。因为模型训练的过程时间长,确保它运行在独立于主CPU UI线程之外的一个线程,以防阻塞浏览器。
train函数的参数有numIterations和done。numIterations是训练模型的迭代次数。
done参数允许我们在train函数完成主进程迭代后,尽快执行回调函数。
模型迭代训练由以下步骤组成:
1.生成batchSize个随机样本点来训练模型。注意,在其他常见的训练实例中,我们不可能飞快地生成训练数据。在本例中,你必须根据你的应用抽取训练batch,常用的技术比如,shuffle训练集中一个固定集合。
2.在每个batch的训练集上训练模型,得到每个样本点的预测值。
3.计算每个batch的损失值之和。
4.优化器算法在神经网络中反向传播,更新神经网络的变量值。
5.每迭代10次将训练损失取对数。
6.使用await tf.nextFrame()确保不会阻塞浏览器渲染任意UI变化。下面关注其中的第 2~4 步。这些步骤被包含在回调中。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

优化器Optimizer对象有一个最小化方法minimize,该方法输入一个函数f,返回一个标量,在本例中返回的是predLoss。然后,优化器Optimizer试图通过计算返回标量对变量张量(神经网络的权重和偏置)的梯度,达到最小化该标量。相应地,更新变量张量的值。
我们将布尔型true传给minimize状态,f和predLoss返回的标量分配给常量cost。这主要是用来取对数的。
接着把注意力放到第 5 步,执行下面的代码。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

上面这步的唯一目的是对训练损失值取对数。当我们需要通过一个张量操作抽取一个张量的值时,引入一个重要的概念。
当我们使用GPU时,默认所有的张量操作是非阻塞的、异步执行的。这意味着返回的张量只是一个简单的占位符,JavaScript的主线程没有立即获取它的数据。CPU抽取一个张量时,TensorFlow.js提供data和dataSync两种方法。
第一个方法data是非阻塞的,它返回一个Promise。当有了张量数据时,该Promise会快速获取数据。对于所有的Promise,你可以调用它们注册为一个函数,返回张量的数据。在本例中,我们使用data打印cost的值。
第二个方法dataSync是data方法的阻塞版。当你需要阻塞直到张量的值合适时,这时调用dataSync函数。一般在实际应用中要避免使用dataSync函数,因为它会阻塞CPU主线程,浏览器什么事也做不了。
重点要注意的是,从data或者dataSync函数获取值时是从GPU下载到CPU,会使得应用的运行变慢。只有当你确实需要时才建议使用,比如当显示最终的结果或者调试或者基准测试时。
最后,在每次迭代训练的末尾,你需要调用await tf.nextFrame()。这是Tensor-Flow.js的公共函数,提供暂停处理、调用requestAnimationFrame和浏览器调用完成后的
重新启动的功能。没有await tf.nextFrame(),动画只能在整个迭代模型训练完成后才能渲染。这在实际应用中将会暂停一下,渲染浏览器。
前面的代码使用了不止一次test函数,我们不在这里详细讨论了。该函数输入一个batch的特征(xs)和相应的标(ys),在神经网络模型中处理,并输出它们的准确度。此时,你应该知道了所有的概念来理解本例。我们将解密test函数留给读者作为一个练习。

3.1.8 TensorFlow.js 的生态

当我们深入学习TensorFlow.js时,你可以找到许多资源、代码仓库和在线示例。本节将简单介绍 TensorFlow.js生态。所以我们将介绍Github中tensorflow命名空间组织的tfjs*开头的所有仓库。
主要的TensorFlow.js仓库包括Core和Layer API代码,用在浏览器和Node.js。该仓库也有Core和Layer API的问题跟踪。

3.1.8.1 Core API(tfjs-core)

Core API实现CPU和GPU的底层张量函数功能,比如,卷积和padding操作、逻辑操作和数学操作,也有优化器代码。Core API原来开发时的名字是Deeplearn.js。

3.1.8.2 Layers API (tfjs-layers)

Layers API (https://github.com/tensorflow/tfjs-layers ) 是抽象的神经网络API,类似Keras。该仓库包含官方API文档所列的tf.layer.*方法的实现,以及Model的实现。
许多选项、参数和内部状态的实现和Keras相同。因此,有时比Core API配置参数(卷积操作的padding选项)少很多。Layer API设计为操作一个batch,而不是一个张量。因此,层输入和输出的第一维必须定义为batch大小。
下面是一个使用Layer API构建神经网络的例子。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

3.1.8.3 Node.js API (tfjs-node)

Node.js API仓库包含TensorFlow后端绑定,用来在TensorFlow上运行Node编写的TensorFlow.js代码。这些功能还不是用于正式生产环境,然而,这表明TensorFlow的 JavaScript API正在变成一个通用的解决方案。

3.1.8.4 转换预训练模型 (tfjs-converter)

TensorFlow.js Converter仓库实现转换预训练的Keras模型,冻结执行图,形成TensorFlow.js 格式。转换器将操作和参数映射成TensorFlow.js 中间状态和优化参数块以达到高效的推断。
下面的例子是转换Keras模型的例子。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

3.1.8.5 TensorFlow.js模型库(tfjs-models)

Google提供各种TensorFlow.js格式的模型,比如SqueezeNet、MobileNet、PoseNet等。TensorFlow.js Models仓库管理着这些模型的代码。这些模型可以用NPM安装,也可以直接用CDN直接加载。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

下面的代码是从unpkg.com的cdn中加载MobileNet模型。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

你可以在浏览器或者Node.js中使用导入的模型。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

我们讨论了TensorFlow.js周边的大部分核心概念。官方文档中有全部的API和一些示例。在这本书的结尾,我们展示了一个用TensorFlow.js构建的实际的应用。你可以用这个应用作为你的应用程序的起点。接着让我们介绍WebDNN。

3.2 WebDNN

WebDNN是运行在浏览器上的另外一个深度学习框架。它主要是由东京大学的机器智能实验室开发的。虽然它没有TensorFlow.js那么流行,但是它支持更多种类的深度学习框架,如下:

  • TensorFlow
  • Keras
  • PyTorch
  • Chainer
  • Cafie

如果你已经有了这些深度学习框架的模型,你可以用WebDNN很容易地导入这些模型。深度学习框架压缩的训练模型运行在Web浏览器上。WebDNN拥有一个优化器管道,它看似一个编译器。它将一个训练模型转换为一个WebDNN的中间表示(intermediate representation ,IR)的格式。在WebDNN优化 IR编写的中间表达之后,最后优化过的模型生成一个核操作图。图3-4显示一个模型定义如何编译为WebDNN。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

WebDNN,类似TensorFlow.js,也能通过WebGL 的硬件加速提升。优化模型的推断不仅可以运行在 WebGL上,也可以运行在WebAssembly和WebGPU 上。如果你的浏览器支持这些API,神经网络模型可以用各种方法加速。大部分现代浏览器已经支持WebGL和WebAssembly。所以,你能毫无烦恼地使用这种加速。
TensorFlow.js和WebDNN的一个最主要的不同是WebDNN只支持任务的推断阶段,而不能用在训练阶段。因此,我们需要导入一个预训练的模型到WebDNN,所以你必须熟悉前面介绍的一些深度学习框架。可以把WenDNN看作一个优化器,它能重写预训练的模型以在Web浏览器上运行得更快。
让我们使用WebDNN导入一个SqueezeNet预训练的模型。SqueezeNet模型是一个深度学习模型,它匹敌AlexNet模型,但是SqueezeNet模型可以用更小的内存。适合在移动设备或者物联网设备上运行深度学习的案例。你可以用pip安装webDNN。
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

首先,因为webDNN不像TensorFlow.js可以预训练模型,它不支持训练功能。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

转换已存储的网络模型为优化模型,就像前面在tfjs-converter中使用的。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

上面生成的文件可导入webDNN模型。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架
带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

根据官方网站的对比,webDNN在WebGPU后端运行时可以运行得更快。WebGPU还不是W3C的一个标准化 API。所以不能期望每个现代浏览器都支持该API。但是你可以看到WebGPU提高性能的潜力。

3.3 Keras.js

Keras.js是另外一个运行在Web浏览器上的深度学习框架。Keras.js只支持Keras生成的模型。但是记住Keras本身支持各种深度学习后端框架。虽然这里不会深入介绍,这意味着Keras.js间接支持Keras支持的深度学习框架后端。
像TensorFlow.js一样,Keras.js实现各种核函数,比如WebGL上运行的着色器程序。但是Keras.js同样不支持训练阶段,所以你需要为 Keras.js准备预训练模型来创建应用。它可以运行在独立于主线程外的WebWorker。这会使Keras.js避免阻塞渲染UI。一个深度学习应用给用户带来好的用户体验至关重要。不幸的是,WebWorker不能访问主线程中的DOM元素,这会限制深度学习预测结果的运用。WebWorker更深层次的细节会在本书后续章节介绍。
Keras.js提供很多试用的例子。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

你可以通过http://localhost:3000https://transcranial.github.io/keras-js 访问示例。
使用代码仓库中的encoder.py将预训练模型转换成Keras.js能够识别的格式。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

上面的脚本生成两个文件:model-weights.buf和model-metadata.json。除此之外, Keras生成的model.json也需要用来一起创建Keras.js模型。

带你读《基于浏览器的深度学习 》之三:JavaScript深度学习框架

如果你已经有了预训练模型,那么Keras.js和 WebDNN一样让你开发深度学习应用更容易。虽然 WebDNN似乎超过Keras.js了,但是Keras和Keras.js的开源社区开发
比WebDNN活跃。这两个深度学习框架都依赖预训练的模型。考虑到后续版本后端框架的模型格式的变化,你应该选择开发活跃的框架应用到你的应用中。

3.4 本章小结

虽然TensorFlow.js对于浏览器来说是最流行的深度学习实现框架,但是在本章我们也介绍了其他几个框架。WebDNN的历史比TensorFlow.js还长,它支持各种后端,比如WebAssembly和WebGPU。虽然这个特性不错,但是运行在这些平台上的深度学习程序几乎不存在。Keras.js也有挺长的历史,与 TensorFlow.js相似。因为其支持更广泛的模型类型,所以更容易和已有的资源整合。
重要的是记住哪种工具适合哪类问题。深度学习框架还相对年轻、不成熟,但是我们希望本章可以帮你选择更好的深度学习框架。下一章将会介绍深度学习中需要的JavaScript基础。

上一篇:SQL Server 阻止了对组件 \'Ad Hoc Distributed Queries\' 的访问


下一篇:人机协同时代,AI助力90.4%双11前端模块自动生成