TensorFlow 2 入门指南
1. TF 2 安装
可以通过命令行执行以下命令来安装 TensorFlow:
Pip3 install tensorflow
如果想安装特定版本(例如 2.3 版本)的 TF 2,可输入以下命令:
Pip3 install --upgrade tensorflow==2.3
也可以降低已安装的版本,pip3 会卸载当前版本并安装指定的版本(如 2.4)。
为了验证安装是否成功,可以创建一个包含以下三行代码的 Python 脚本,以确定机器上安装的 TF 版本:
import tensorflow as tf
print("TF Version:",tf.__version__)
print("eager execution:",tf.executing_eagerly())
运行上述代码,应该会看到类似以下的输出:
TF version: 2.0.0-beta1
eager execution: True
以下是一个简单的 TF 2 代码示例,将此代码片段放在一个文本文件中:
import tensorflow as tf
print("1 + 2 + 3 + 4 =", tf.reduce_sum([1, 2, 3, 4]))
从命令行运行上述代码,应该会看到以下输出:
1 + 2 + 3 + 4 = tf.Tensor(10, shape=(), dtype=int32)
2. TF 2 与 Python REPL
如果你还不熟悉 Python REPL(读取 - 求值 - 打印循环),可以通过打开命令 shell 并输入以下命令来访问:
python
作为一个简单的示例,在 REPL 中通过以下方式导入 TF 2 库来访问与 TF 2 相关的功能:
>>> import tensorflow as tf
现在使用以下命令检查机器上安装的 TF 2 版本:
>>> print('TF version:',tf.__version__)
上述代码片段的输出如下(看到的数字取决于安装的 TF 2 版本):
TF version: 2.0.0-beta1
虽然 REPL 对于短代码块很有用,但本文中的 TF 2 代码示例是可以用 Python 解释器执行的 Python 脚本。
3. 其他基于 TF 2 的工具包
除了支持在多个设备上运行基于 TF 2 的代码外,TF 2 还提供了以下工具包:
| 工具包 | 说明 |
| ---- | ---- |
| TensorBoard | 用于可视化(作为 TensorFlow 的一部分包含在内) |
| TensorFlow Serving | 用于在服务器上托管模型 |
| TensorFlow Hub | |
| TensorFlow Lite | 用于移动应用程序 |
| Tensorflow.js | 用于网页和 NodeJS |
3.1 TensorBoard
TensorBoard 是一个在浏览器中运行的图形可视化工具。可以通过以下方式从命令行启动 TensorBoard:打开命令 shell 并输入以下命令,以访问子目录
/tmp/abc
(或你选择的目录)中保存的 TF 图:
tensorboard --logdir /tmp/abc
注意上述命令中
logdir
参数前有两个连续的破折号(“–”)。然后启动浏览器会话并导航到以下 URL:
localhost:6006
。过一会儿,你将看到在代码中创建并保存在
/tmp/abc
目录中的 TF 2 图的可视化结果。
3.2 TensorFlow Serving
TensorFlow Serving 是一个基于云的灵活、高性能的机器学习模型服务系统,专为生产环境设计。它使得部署新算法和实验变得容易,同时保持相同的服务器架构和 API。更多信息请访问:https://www.TF 2.org/serving/
3.3 TensorFlow Lite
TensorFlow Lite 专门为移动开发(包括 Android 和 iOS)而创建。需要注意的是,TensorFlow Lite 取代了 TF 2 Mobile,后者是早期用于开发移动应用程序的 SDK。TensorFlow Lite 支持低延迟的设备端机器学习推理,并且二进制文件大小较小。此外,它还支持使用 Android 神经网络 API 进行硬件加速。更多关于 TensorFlow Lite 的信息请访问:https://www.tensorflow.org/lite/
3.4 tensorflow.js
tensorflow.js 提供了 JavaScript API,用于在网页中访问 TensorFlow。该工具包以前称为 deeplearning.js,也可以与 NodeJS 一起使用。更多关于 tensorflow.js 的信息请访问:https://js.tensorflow.org
4. TF 2 即时执行模式
与使用延迟执行模式的 TF 1.x 代码相比,TF 2 即时执行模式使 TF 2 代码的编写更加容易。可能会惊讶地发现,TF 在 1.4.1 版本中就引入了“即时执行”作为延迟执行的替代方案,但这个功能在当时未得到充分利用。
在 TF 1.x 代码中,TensorFlow 会创建一个数据流图,该图由 (a) 一组表示计算单元的
tf.Operation
对象和 (b) 表示在操作之间流动的数据单元的
tf.Tensor
对象组成。
然而,TF 2 无需实例化
Session
对象或创建图即可立即评估操作。操作返回具体的值,而不是创建计算图。TF 2 即时执行基于 Python 控制流,而不是图控制流。算术运算更加简单直观,在后续的代码示例中可以看到。此外,TF 2 即时执行模式简化了调试过程。但要记住,图和即时执行之间并不是一一对应的关系。
5. TF 2 张量、数据类型和原始类型
5.1 TF 2 张量
简单来说,TF 2 张量是一个 n 维数组,类似于 NumPy 的 ndarray。TF 2 张量由其维度定义,如下所示:
- 标量:零阶张量
- 向量:一阶张量
- 矩阵:二阶张量
- 三维数组:三阶张量
5.2 TF 2 数据类型
TF 2 支持以下数据类型(与 TensorFlow 1.x 中支持的数据类型类似):
-
tf.float32
-
tf.float64
-
tf.int8
-
tf.int16
-
tf.int32
-
tf.int64
-
tf.uint8
-
tf.string
-
tf.bool
这些数据类型很容易理解:两种浮点类型、四种整数类型、一种无符号整数类型、一种字符串类型和一种布尔类型。可以看到,有 32 位和 64 位的浮点类型,以及从 8 位到 64 位的整数类型。
5.3 TF 2 原始类型
TF 2 支持
tf.constant()
和
tf.Variable()
作为原始类型。注意
tf.Variable()
中的大写“V”,这表示它是一个 TF 2 类(而
tf.constant()
以小写字母开头则不是)。
TF 2 常量是一个不可变的值,一个简单的示例如下:
aconst = tf.constant(3.0)
TF 2 变量是 TF 2 图中的“可训练值”。例如,对于由欧几里得平面上的点组成的数据集,最佳拟合线的斜率
m
和 y 截距
b
就是两个可训练值的例子。以下是一些 TF 变量的示例:
b = tf.Variable(3, name="b")
x = tf.Variable(2, name="x")
z = tf.Variable(5*x, name="z")
W = tf.Variable(20)
lm = tf.Variable(W*x + b, name="lm")
可以注意到,
b
、
x
和
z
被定义为 TF 变量。此外,
b
和
x
用数值初始化,而变量
z
的值是一个依赖于
x
值(等于 2)的表达式。
6. TF 2 常量
TF 2 常量具有以下一些属性:
- 在定义时初始化
- 不能改变其值(“不可变”)
- 可以指定其名称(可选)
- 必须指定类型(例如
tf.float32
)
- 在训练过程中不会被修改
以下是
tf2_constants1.py
的内容,展示了如何分配和打印一些 TF 2 常量的值:
import tensorflow as tf
scalar = tf.constant(10)
vector = tf.constant([1,2,3,4,5])
matrix = tf.constant([[1,2,3],[4,5,6]])
cube = tf.constant([[[1],[2],[3]],[[4],[5],[6]],[[7],[8],[9]]])
print(scalar.get_shape())
print(vector.get_shape())
print(matrix.get_shape())
print(cube.get_shape())
上述代码包含四个
tf.constant()
语句,分别定义了维度为 0、1、2 和 3 的 TF 2 张量。第二部分包含四个
print()
语句,用于显示第一部分中定义的四个 TF 2 常量的形状。输出如下:
()
(5,)
(2, 3)
(3, 3, 1)
以下是
tf2_constants2.py
的内容,展示了如何为 TF 2 常量赋值并打印这些值:
import tensorflow as tf
x = tf.constant(5,name="x")
y = tf.constant(8,name="y")
@tf.function
def calc_prod(x, y):
z = 2*x + 3*y
return z
result = calc_prod(x, y)
print('result =',result)
上述代码定义了一个“装饰”的 Python 函数
calc_prod()
,其中的 TF 2 代码在 TF 1.x 中会包含在
tf.Session()
代码块中。具体来说,
z
会包含在
sess.run()
语句中,同时还会有一个
feed_dict
为
x
和
y
提供值。幸运的是,TF 2 中的装饰 Python 函数使代码看起来像“普通”的 Python 代码。
7. TF 2 变量
TF 2.0 消除了全局集合及其相关的 API,如
tf.get_variable
、
tf.variable_scope
和
tf.initializers.global_variables
。每当在 TF 2 中需要一个
tf.Variable
时,可直接构造并初始化它,如下所示:
tf.Variable(tf.random.normal([2, 4])
以下是
tf2_variables.py
的内容,展示了如何在
with
代码块中计算涉及 TF 常量和变量的值:
import tensorflow as tf
v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
print("v.value():", v.value())
print("")
print("v.numpy():", v.numpy())
print("")
v.assign(2 * v)
v[0, 1].assign(42)
v[1].assign([7., 8., 9.])
print("v:",v)
print("")
try:
v[1] = [7., 8., 9.]
except TypeError as ex:
print(ex)
上述代码定义了一个 TF 2 变量
v
并打印其值。接下来的部分更新了
v
的值并打印其新值。最后一部分包含一个
try/except
块,尝试更新
v[1]
的值。输出如下:
v.value(): tf.Tensor(
[[1. 2. 3.]
[4. 5. 6.]], shape=(2, 3), dtype=float32)
v.numpy(): [[1. 2. 3.]
[4. 5. 6.]]
v: <tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42., 6.],
[ 7., 8., 9.]], dtype=float32)>
"ResourceVariable" object does not support item assignment
8.
tf.rank()
API
TF 2 张量的秩是张量的维度,而张量的形状是每个维度中的元素数量。以下是
tf2_rank.py
的内容,展示了如何查找 TF 2 张量的秩:
import tensorflow as tf # tf2_rank.py
A = tf.constant(3.0)
B = tf.fill([2,3], 5.0)
C = tf.constant([3.0, 4.0])
@tf.function
def show_rank(x):
return tf.rank(x)
print('A:',show_rank(A))
print('B:',show_rank(B))
print('C:',show_rank(C))
上述代码中,首先定义了 TF 常量
A
,接着是 TF 张量
B
,它是一个 2×3 的张量,每个元素的值都为 5。TF 张量
C
是一个 1×2 的张量,值为 3.0 和 4.0。
接下来的代码块定义了装饰的 Python 函数
show_rank()
,它返回其输入变量的秩。最后一部分分别用
A
和
B
调用
show_rank()
。输出如下:
A: tf.Tensor(0, shape=(), dtype=int32)
B: tf.Tensor(2, shape=(), dtype=int32)
C: tf.Tensor(1, shape=(), dtype=int32)
9.
tf.shape()
API
TF 2 张量的形状是给定张量每个维度中的元素数量。以下是
tf2_getshape.py
的内容,展示了如何查找 TF 2 张量的形状:
import tensorflow as tf
a = tf.constant(3.0)
print("a shape:",a.get_shape())
b = tf.fill([2,3], 5.0)
print("b shape:",b.get_shape())
c = tf.constant([[1.0,2.0,3.0], [4.0,5.0,6.0]])
print("c shape:",c.get_shape())
上述代码定义了 TF 常量
a
,其值为 3.0。接着,TF 变量
b
被初始化为一个 1×2 的向量,值为
[[2,3], 5.0]
,然后是常量
c
,其值为
[[1.0,2.0,3.0],[4.0,5.0,6.0]]
。三个
print()
语句显示了
a
、
b
和
c
的形状。输出如下:
a shape: ()
b shape: (2, 3)
c shape: (2, 3)
指定 0 维张量(标量)的形状可以是数字(如 9、 -5、2.34 等)、
[]
和
()
。以下是
tf2_shapes.py
的内容,包含了各种张量及其形状:
import tensorflow as tf
list_0 = []
tuple_0 = ()
print("list_0:",list_0)
print("tuple_0:",tuple_0)
list_1 = [3]
tuple_1 = (3)
print("list_1:",list_1)
print("tuple_1:",tuple_1)
list_2 = [3, 7]
tuple_2 = (3, 7)
print("list_2:",list_2)
print("tuple_2:",tuple_2)
any_list1 = [None]
any_tuple1 = (None)
print("any_list1:",any_list1)
print("any_tuple1:",any_tuple1)
any_list2 = [7,None]
any_list3 = [7,None,None]
print("any_list2:",any_list2)
print("any_list3:",any_list3)
上述代码包含了各种维度的简单列表和元组,以说明这两种类型之间的区别。输出如下:
list_0: []
tuple_0: ()
list_1: [3]
tuple_1: 3
list_2: [3, 7]
tuple_2: (3, 7)
any_list1: [None]
any_tuple1: None
any_list2: [7, None]
any_list3: [7, None, None]
10. 再次认识 TF 2 变量
TF 2 变量可以在反向误差传播期间更新,也可以保存并在以后恢复。TF 2 变量具有以下一些属性:
- 初始值是可选的
- 必须在图执行前初始化
- 在训练期间更新
- 不断重新计算
- 保存权重和偏置的值
- 内存缓冲区(可从磁盘保存/恢复)
以下是一些简单的 TF 2 变量示例:
b = tf.Variable(3, name='b')
x = tf.Variable(2, name='x')
z = tf.Variable(5*x, name="z")
W = tf.Variable(20)
lm = tf.Variable(W*x + b, name="lm")
可以注意到,变量
b
、
x
和
W
指定了常量值,而变量
z
和
lm
指定了根据其他变量定义的表达式。如果熟悉线性回归,无疑会注意到变量
lm
(“线性模型”)在欧几里得平面中定义了一条直线。
TF 2 变量的其他属性如下:
- 可通过操作更新的张量
- 存在于
session.run
上下文之外
- 像“普通”变量一样
- 保存学习到的模型参数
- 变量可以共享(或不可训练)
- 用于存储/维护状态
- 内部存储一个持久张量
- 可以读取/修改张量的值
- 多个工作进程看到
tf.Variables
的相同值
- 是表示程序操作的共享持久状态的最佳方式
TF 2 还提供了
tf.assign()
方法来修改 TF 2 变量的值,一定要阅读后续相关的代码示例,以便正确学习如何使用这个 API。
11. TF 2 变量与张量的区别
需要记住 TF 变量和 TF 张量之间的以下区别:TF 变量表示模型的可训练参数(例如神经网络的权重和偏置),而 TF 张量表示输入到模型的数据以及数据在通过模型时的中间表示。
12. TF 2 中的 @tf.function
TF 2 为 Python 函数引入了
@tf.function
“装饰器”,它定义一个图并执行会话:它有点像是 TF 1.x 中
tf.Session()
的“继任者”。由于图仍然有用,
@tf.function
可以透明地将 Python 函数转换为由图“支持”的函数。这个装饰器还将依赖于张量的 Python 控制流转换为 TF 控制流,并添加控制依赖项以对 TF 2 状态的读写操作进行排序。要记住,
@tf.function
与 TF 2 操作配合使用效果最佳,而不是与 NumPy 操作或 Python 原语配合使用。
一般来说,不需要对所有函数都使用
@tf.function
进行装饰,可使用它来装饰高级计算,例如训练的一步或模型的前向传播。
虽然 TF 2 即时执行模式提供了更直观的用户界面,但这种易用性可能会导致性能下降。幸运的是,
@tf.function
装饰器是一种在 TF 2 代码中生成图的技术,这些图的执行速度比即时执行模式更快。性能提升取决于所执行的操作类型:矩阵乘法使用
@tf.function
不会有性能提升,而优化深度神经网络则可以从
@tf.function
中受益。
13. @tf.function 的工作原理
当用
@tf.function
装饰一个 Python 函数时,TF 2 会自动以图模式构建该函数。如果一个用
@tf.function
装饰的 Python 函数调用了其他未用
@tf.function
装饰的 Python 函数,那么这些“未装饰”的 Python 函数中的代码也会包含在生成的图中。
另一个需要记住的点是,在即时模式下,
tf.Variable
实际上是一个“普通”的 Python 对象:当它超出作用域时,这个对象会被销毁。然而,如果函数通过
@tf.function
进行装饰,
tf.Variable
对象会定义一个持久对象。在这种情况下,即时模式被禁用,
tf.Variable
对象在持久的 TF 2 图中定义一个节点。因此,一个在即时模式下无需注解就能正常工作的函数,在使用
@tf.function
装饰时可能会失败。
14. @tf.function 的注意事项
如果常量在装饰的 Python 函数定义之前定义,可以在函数内部使用 Python 的
print()
函数打印它们的值。然而,如果常量在装饰的 Python 函数定义内部定义,则需要使用 TF 2 的
tf.print()
函数在函数内部打印它们的值。
考虑以下代码块:
import tensorflow as tf
a = tf.add(4, 2)
@tf.function
def compute_values():
print(a) # 6
compute_values()
输出如下:
tf.Tensor(6, shape=(), dtype=int32)
可以看到,正确的结果被显示出来。然而,如果在装饰的 Python 函数内部定义常量,输出将包含类型和属性,但不会执行加法操作。考虑以下代码块:
import tensorflow as tf
@tf.function
def compute_values():
a = tf.add(4, 2)
print(a)
compute_values()
输出如下:
Tensor("Add:0", shape=(), dtype=int32)
上述输出中的零是张量名称的一部分,而不是输出的值。具体来说,
Add:0
是
tf.add()
操作的输出零。任何额外的
compute_values()
调用都不会打印任何内容。如果想要实际的结果,一种解决方案是从函数中返回一个值,如下所示:
import tensorflow as tf
@tf.function
def compute_values():
a = tf.add(4, 2)
return a
result = compute_values()
print("result:", result)
上述代码块的输出如下:
result: tf.Tensor(6, shape=(), dtype=int32)
第二种解决方案是使用 TF 的
tf.print()
函数而不是 Python 的
print()
函数,如下所示:
@tf.function
def compute_values():
a = tf.add(4, 2)
tf.print(a)
第三种解决方案是在不影响生成图的形状的情况下,将数值转换为张量。
TensorFlow 2 入门指南
15. 总结与实践建议
在学习和使用 TensorFlow 2 的过程中,我们已经了解了众多重要的概念和工具。下面通过一个 mermaid 流程图来总结从安装到使用
@tf.function
的整个流程:
graph LR
A[安装 TensorFlow 2] --> B[验证安装]
B --> C[使用 Python REPL 测试]
C --> D[了解基于 TF 2 的工具包]
D --> E[掌握即时执行模式]
E --> F[学习张量、数据类型和原始类型]
F --> G[区分常量和变量]
G --> H[使用 tf.rank() 和 tf.shape() API]
H --> I[深入理解 TF 2 变量]
I --> J[区分变量与张量]
J --> K[使用 @tf.function 装饰器]
根据这个流程,我们可以按照以下步骤进行实践:
1.
安装与验证
:按照前面介绍的方法安装指定版本的 TensorFlow 2,并使用验证代码确保安装成功。
2.
初步测试
:通过 Python REPL 进行简单的 TF 2 代码测试,熟悉基本操作。
3.
工具包探索
:尝试使用 TensorBoard 进行可视化,了解 TensorFlow Serving、TensorFlow Lite 和 tensorflow.js 等工具包的用途。
4.
核心概念学习
:掌握即时执行模式、张量、数据类型、常量和变量等核心概念,通过代码示例加深理解。
5.
API 使用
:运用
tf.rank()
和
tf.shape()
API 对张量进行操作。
6.
高级特性应用
:深入理解 TF 2 变量的属性和使用方法,区分变量与张量,合理使用
@tf.function
装饰器提升性能。
16. 常见问题与解决方案
在使用 TensorFlow 2 的过程中,可能会遇到一些常见问题,以下是一些问题及对应的解决方案:
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 安装失败 |
执行
pip3 install tensorflow
时出现错误
|
检查网络连接,确保使用的 Python 版本兼容,可尝试更新
pip
到最新版本
|
@tf.function
输出异常
| 装饰函数内打印常量结果异常 |
若常量在函数外定义,使用 Python
print()
;若在函数内定义,使用
tf.print()
或从函数返回值
|
| 变量更新错误 |
尝试更新 TF 2 变量时出现
TypeError
|
使用
tf.assign()
方法进行变量更新,避免直接赋值
|
17. 性能优化建议
为了充分发挥 TensorFlow 2 的性能,以下是一些优化建议:
-
合理使用
@tf.function
:对于复杂的计算和训练步骤,使用
@tf.function
装饰函数,将 Python 代码转换为图模式执行,提高执行效率。但要注意避免在装饰函数中使用过多的 NumPy 操作或 Python 原语。
-
硬件加速
:如果有可用的 GPU 或 TPU,可以配置 TensorFlow 2 以使用这些硬件进行加速。例如,在安装 TensorFlow 时选择支持 GPU 的版本,并确保安装了相应的驱动和 CUDA 库。
-
内存管理
:在处理大规模数据时,注意内存的使用情况。可以使用
tf.data.Dataset
进行数据加载和预处理,避免一次性将所有数据加载到内存中。
18. 拓展学习资源
TensorFlow 2 是一个功能强大且不断发展的深度学习框架,为了进一步深入学习,可以参考以下资源:
-
官方文档
:TensorFlow 官方网站提供了详细的文档和教程,包括 API 参考、指南和示例代码。可以访问 https://www.tensorflow.org/ 进行学习。
-
在线课程
:Coursera、Udemy 等在线学习平台上有许多关于 TensorFlow 2 的课程,由专业的讲师授课,提供系统的学习路径。
-
开源项目
:在 GitHub 上搜索 TensorFlow 2 相关的开源项目,学习他人的代码和实践经验。例如,TensorFlow 官方的示例项目和一些知名的深度学习研究项目。
19. 实战案例:简单线性回归
为了将所学知识应用到实际中,我们来实现一个简单的线性回归模型。线性回归是一种基本的机器学习算法,用于预测连续值。
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
# 生成一些模拟数据
x_train = np.linspace(0, 10, 100)
y_train = 2 * x_train + 1 + np.random.randn(*x_train.shape) * 0.5
# 定义模型参数
W = tf.Variable(tf.random.normal([1]), name='weight')
b = tf.Variable(tf.zeros([1]), name='bias')
# 定义线性回归模型
@tf.function
def linear_regression(x):
return W * x + b
# 定义损失函数
@tf.function
def loss(y_pred, y_true):
return tf.reduce_mean(tf.square(y_pred - y_true))
# 定义优化器
optimizer = tf.optimizers.SGD(learning_rate=0.01)
# 训练模型
num_epochs = 100
for epoch in range(num_epochs):
with tf.GradientTape() as tape:
y_pred = linear_regression(x_train)
current_loss = loss(y_pred, y_train)
gradients = tape.gradient(current_loss, [W, b])
optimizer.apply_gradients(zip(gradients, [W, b]))
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {current_loss.numpy()}')
# 可视化结果
plt.scatter(x_train, y_train, label='Data')
plt.plot(x_train, linear_regression(x_train).numpy(), color='red', label='Fitted line')
plt.legend()
plt.show()
在这个案例中,我们首先生成了一些模拟数据,然后定义了线性回归模型的参数
W
和
b
。使用
@tf.function
装饰器将模型和损失函数转换为图模式,提高执行效率。通过
tf.GradientTape
计算梯度,并使用随机梯度下降(SGD)优化器更新参数。最后,将训练结果可视化,观察模型的拟合效果。
20. 总结
通过本文的学习,我们全面了解了 TensorFlow 2 的安装、基本操作、核心概念和高级特性。从安装 TensorFlow 2 开始,我们学习了如何验证安装、使用 Python REPL 进行测试,以及了解了基于 TF 2 的各种工具包。掌握了即时执行模式、张量、数据类型、常量和变量等重要概念,学会了使用
tf.rank()
和
tf.shape()
API 对张量进行操作。深入理解了 TF 2 变量的属性和使用方法,区分了变量与张量,并学习了如何使用
@tf.function
装饰器提升性能。
在实践方面,我们通过一个简单的线性回归案例将所学知识应用到实际中,展示了如何构建和训练一个基本的机器学习模型。同时,我们还提供了常见问题的解决方案、性能优化建议和拓展学习资源,帮助你在学习和使用 TensorFlow 2 的过程中更加顺利。
希望本文能够帮助你快速入门 TensorFlow 2,并在深度学习的道路上取得更好的成果。不断实践和探索,你将发现 TensorFlow 2 的更多强大功能和应用场景。
超级会员免费看
36

被折叠的 条评论
为什么被折叠?



