提交 0f8d1d42 编写于 作者: W wizardforcel

2021-01-24 22:14:28

上级 ca3db17b
......@@ -321,9 +321,9 @@ for param in model.parameters():
## 提取特征
正如我们前面提到的,VGG-19 网络包含 19 个不同的层,包括卷积,池化和全连接层。 卷积层在每个池化层之前先进入堆栈,其中五个是整个架构中的堆栈数。
正如我们前面提到的,VGG-19 网络包含 19 个不同的层,包括卷积,池化和全连接层。 卷积层在每个池化层之前先进入栈,其中五个是整个架构中的栈数。
在样式迁移领域,已经有不同的论文确定了对于识别内容和样式图像上的相关特征至关重要的那些层。 因此,常规上接受的是,每个堆栈的第一卷积层都能够提取样式特征,而仅第四堆栈的第二卷积层应用于提取内容特征。
在样式迁移领域,已经有不同的论文确定了对于识别内容和样式图像上的相关特征至关重要的那些层。 因此,常规上接受的是,每个栈的第一卷积层都能够提取样式特征,而仅第四栈的第二卷积层应用于提取内容特征。
从现在开始,我们将提取样式特征的层称为`conv1_1``conv2_1``conv3_1``conv4_1``conv5_1`,而负责提取内容特征的层将被称为`conv4_2`
......
......@@ -76,7 +76,7 @@
## RNN 如何工作?
简而言之,RNN 接受输入(`x`)并返回输出(`y`)。 在此,输出不仅受输入影响,而且还受过去输入的输入的整个历史影响。 输入的这种历史记录通常称为模型的内部状态或内存,它们是遵循订单并相互关联的数据序列,例如时间序列,它是一系列数据点(例如,销售) )(按月列出)。
简而言之,RNN 接受输入(`x`)并返回输出(`y`)。 在此,输出不仅受输入影响,而且还受过去输入的输入的整个历史影响。 输入的这种历史记录通常称为模型的内部状态或内存,它们是遵循顺序并相互关联的数据序列,例如时间序列,它是一系列数据点(例如,销售) )(按月列出)。
注意
......
......@@ -16,7 +16,7 @@
第 4 章,“计算机视觉”是迄今为止深度学习最成功的结果,它讨论了成功背后的关键思想,并贯穿了使用最广泛的视觉算法– **卷积神经网络(CNN)**。 我们将逐步实现 CNN 以了解其工作原理,然后使用 PyTorch 的`nn`包中预定义的 CNN。 本章可帮助您制作简单的 CNN 和基于高级 CNN 的视觉算法,称为语义分割。
第 5 章,“序列数据处理”着眼于循环神经网络,它是目前最成功的序列数据处理算法。 本章向您介绍主要的 RNN 组件,例如**长短期记忆****LSTM**)网络和**门控循环单元****GRU**)。 然后,在探索循环神经网络之前,我们将经历 RNN 实现中的算法更改,例如双向 RNN,并增加层数。 为了理解递归网络,我们将使用斯坦福大学 NLP 小组的著名示例(栈增强的解析器-解释器神经网络(SPINN)),并将其在 PyTorch 中实现。
第 5 章,“序列数据处理”着眼于循环神经网络,它是目前最成功的序列数据处理算法。 本章向您介绍主要的 RNN 组件,例如**长短期记忆****LSTM**)网络和**门控循环单元****GRU**)。 然后,在探索循环神经网络之前,我们将经历 RNN 实现中的算法更改,例如双向 RNN,并增加层数。 为了理解递归网络,我们将使用斯坦福大学 NLP 小组的著名示例(栈增强的解析器-解释器神经网络(SPINN)),并将其在 PyTorch 中实现。
第 6 章,“生成网络”,简要讨论了生成网络的历史,然后解释了各种生成网络。 在这些不同的类别中,本章向我们介绍了自回归模型和 GAN。 我们将研究作为自动回归模型一部分的 PixelCNN 和 WaveNet 的实现细节,然后详细研究 GAN。
......
......@@ -437,7 +437,7 @@ PyTorch 具有称为`unsqueeze`的防挤压操作,该操作会为张量对象
![Learning the basic operations](img/B09475_01_20.jpg)
图 1.18:级联,栈,压缩和取消压缩的图示
图 1.18:级联,栈,压缩和取消压缩的图示
如果您对的所有这些基本操作感到满意,则可以继续第二章并立即开始编码会话。 PyTorch 附带了许多其他重要操作,当您开始构建网络时,您一定会发现它们非常有用。 我们将在接下来的各章中看到其中的大多数内容,但是如果您想首先学习这一点,请访问 PyTorch 网站并查看其张量教程页面,该页面描述了张量对象可以执行的所有操作。
......
......@@ -483,7 +483,7 @@ RNN 实现通常是单向的,这就是到目前为止我们已经实现的。
语言研究人员的一部分永远不会认可 RNN 的工作方式,即从左到右依次进行,尽管那是多少人阅读一个句子。 某些人坚信语言具有层次结构,利用这种结构有助于我们轻松解决 NLP 问题。 循环神经网络是使用该方法解决 NLP 的尝试,其中,基于要处理的语言的短语,将序列安排为树。 SNLI 是为此目的而创建的数据集,其中每个句子都排列成一棵树。
我们正在尝试构建的特定递归网络是 SPINN,它是通过充分考虑这两个方面的优点而制成的。 SPINN 从左到右处理数据,就像人类的阅读方式一样,但仍保持层次结构完整。 从左向右读取的方法相对于按层次进行解析还有另一个优势:网络从左向右读取时可以最终学习生成解析树。 这可以通过使用称为移位减少解析器的特殊实现以及栈和缓冲区数据结构的使用来实现。
我们正在尝试构建的特定递归网络是 SPINN,它是通过充分考虑这两个方面的优点而制成的。 SPINN 从左到右处理数据,就像人类的阅读方式一样,但仍保持层次结构完整。 从左向右读取的方法相对于按层次进行解析还有另一个优势:网络从左向右读取时可以最终学习生成解析树。 这可以通过使用称为移位减少解析器的特殊实现以及栈和缓冲区数据结构的使用来实现。
![Recursive neural networks](img/B09475_05_11.jpg)
......@@ -523,7 +523,7 @@ class Reduce(nn.Module):
#### 追踪器
在循环中每次 SPINN 的`forward`调用中都会调用`Tracker``forward`方法。 在归约运算开始之前,我们需要将上下文向量传递到`Reduce`网络,因此,我们需要遍历`transition`向量并创建缓冲区,栈和上下文向量,然后才能执行 SPINN 的`forward()`函数。 由于 PyTorch 变量会跟踪历史事件,因此将跟踪所有这些循环操作并可以反向传播:
在循环中每次 SPINN 的`forward`调用中都会调用`Tracker``forward`方法。 在归约运算开始之前,我们需要将上下文向量传递到`Reduce`网络,因此,我们需要遍历`transition`向量并创建缓冲区,栈和上下文向量,然后才能执行 SPINN 的`forward()`函数。 由于 PyTorch 变量会跟踪历史事件,因此将跟踪所有这些循环操作并可以反向传播:
```py
class Tracker(nn.Module):
......@@ -566,7 +566,7 @@ class SPINN(nn.Module):
self.tracker = Tracker(config.d_hidden, config.d_tracker,predict=config.predict)
```
`forward`调用的主要部分是对`Tracker``forward`方法的调用,该方法将处于循环中。 我们遍历输入序列,并为转换序列中的每个单词调用`Tracker``forward`方法,然后根据转换实例将输出保存到上下文向量列表中。 如果转换是`shift`,则栈将在后面附加当前单词;如果转换是`reduce`,则将调用`Reduce`并创建跟踪,并在最左边和最右边的单词, 这将从左侧和右侧列表中弹出。
`forward`调用的主要部分是对`Tracker``forward`方法的调用,该方法将处于循环中。 我们遍历输入序列,并为转换序列中的每个单词调用`Tracker``forward`方法,然后根据转换实例将输出保存到上下文向量列表中。 如果转换是`shift`,则栈将在后面附加当前单词;如果转换是`reduce`,则将调用`Reduce`并创建跟踪,并在最左边和最右边的单词, 这将从左侧和右侧列表中弹出。
```py
def forward(self, buffers, transitions):
......
......@@ -294,7 +294,7 @@ class WaveNetModule(torch.nn.Module):
前面的代码块中给出的程序是主要的父 WaveNet 模块,该模块使用所有子组件来创建图形。 `init`定义了三个主要成分,其中是第一个普通卷积,然后是`res_stack`(它是由所有膨胀卷积和 Sigmoid 正切门组成的残差连接块)。 然后,最后的`convdensnet``1x1`卷积的顶部进行。 `forward`引入一个求和节点,依次执行这些模块。 然后,将`convdensnet`创建的输出通过`contiguous()`移动到存储器的单个块。 这是其余网络所必需的。
`ResidualStack`是需要更多说明的模块,它是 WaveNet 架构的核心。 `ResidualStack``ResidualBlock`的层的栈。 WaveNet 图片中的每个小圆圈都是一个残差块。 在正常卷积之后,数据到达`ResidualBlock`,如前所述。 `ResidualBlock`从膨胀的卷积开始,并且期望得到膨胀。 因此,`ResidualBlock`决定了架构中每个小圆节点的膨胀因子。 如前所述,膨胀卷积的输出然后通过类似于我们在 PixelCNN 中看到的门的门。
`ResidualStack`是需要更多说明的模块,它是 WaveNet 架构的核心。 `ResidualStack``ResidualBlock`的层的栈。 WaveNet 图片中的每个小圆圈都是一个残差块。 在正常卷积之后,数据到达`ResidualBlock`,如前所述。 `ResidualBlock`从膨胀的卷积开始,并且期望得到膨胀。 因此,`ResidualBlock`决定了架构中每个小圆节点的膨胀因子。 如前所述,膨胀卷积的输出然后通过类似于我们在 PixelCNN 中看到的门的门。
在那之后,它必须经历两个单独的卷积以进行跳跃连接和残差连接。 尽管作者并未将其解释为两个单独的卷积,但使用两个单独的卷积更容易理解。
......@@ -324,9 +324,9 @@ def forward(self, x, skip_size):
return x, skip
```
`ResidualStack`使用层数和堆栈数来创建膨胀因子。 通常,每个层具有`2 ^ l`作为膨胀因子,其中`l`是层数。 从`1``2 ^ l`开始,每个堆栈都具有相同数量的层和相同样式的膨胀因子列表。
`ResidualStack`使用层数和栈数来创建膨胀因子。 通常,每个层具有`2 ^ l`作为膨胀因子,其中`l`是层数。 从`1``2 ^ l`开始,每个栈都具有相同数量的层和相同样式的膨胀因子列表。
方法`stack_res_block`使用我们前面介绍的`ResidualBlock`为每个栈和每个层中的每个节点创建一个残差块。 该程序引入了一个新的 PyTorch API,称为`torch.nn.DataParallel`。 如果有多个 GPU,则`DataParallel` API 会引入​​并行性。 将模型制作为数据并行模型可以使 PyTorch 知道用户可以使用更多 GPU,并且 PyTorch 从那里获取了它,而没有给用户带来任何障碍。 PyTorch 将数据划分为尽可能多的 GPU,并在每个 GPU 中并行执行模型。
方法`stack_res_block`使用我们前面介绍的`ResidualBlock`为每个栈和每个层中的每个节点创建一个残差块。 该程序引入了一个新的 PyTorch API,称为`torch.nn.DataParallel`。 如果有多个 GPU,则`DataParallel` API 会引入​​并行性。 将模型制作为数据并行模型可以使 PyTorch 知道用户可以使用更多 GPU,并且 PyTorch 从那里获取了它,而没有给用户带来任何障碍。 PyTorch 将数据划分为尽可能多的 GPU,并在每个 GPU 中并行执行模型。
它还负责从每个 GPU 收集回结果,并将其合并在一起,然后再继续进行。
......
......@@ -711,9 +711,9 @@ while True:
我们还必须传递数据类型和输入张量的形状。 做张量集时,我们应该考虑的一件事是数据类型和形状。 由于我们将输入作为缓冲区传递,因此 RedisAI 尝试使用我们传递的形状和数据类型信息将缓冲区转换为 DLPack 张量。 如果这与我们传递的字节串的长度不匹配,RedisAI 将抛出错误。
设置张量后,我们将模型保存在名为`model`的键中,并将张量保存在名为`a`的键中。 现在,我们可以通过传递模型密钥名称和张量密钥名称来运行`AI.MODELRUN`命令。
设置张量后,我们将模型保存在名为`model`的键中,并将张量保存在名为`a`的键中。 现在,我们可以通过传递模型键名称和张量键名称来运行`AI.MODELRUN`命令。
如果有多个输入要传递,我们将使用张量集不止一次,并将所有键作为`INPUTS`传递给`MODELRUN`命令。 `MODELRUN`命令将输出保存到`OUTPUTS`下提到的密钥,然后`AI.TENSORGET`可以读取。
如果有多个输入要传递,我们将使用张量集不止一次,并将所有键作为`INPUTS`传递给`MODELRUN`命令。 `MODELRUN`命令将输出保存到`OUTPUTS`下提到的,然后`AI.TENSORGET`可以读取。
在这里,我们像保存了一样将张量读为`BLOB`。 张量命令为我们提供类型,形状和自身的缓冲。 然后将缓冲区传递给 NumPy 的`frombuffer()`函数,该函数为我们提供了结果的 NumPy 数组。
......
......@@ -85,7 +85,7 @@ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
该项目的数据是成千上万的英语到法语翻译对的集合。
[开放数据栈交换](https://opendata.stackexchange.com/questions/3888/dataset-of-sentences-translated-into-many-languages)上的这个问题使我指向[开放翻译站点](https://tatoeba.org/) ,该站点可从[这里](https://tatoeba.org/eng/downloads)下载。更好的是,有人在这里做了一些额外的工作,[将语言对拆分为单独的文本文件](https://www.manythings.org/anki/)
[开放数据栈交换](https://opendata.stackexchange.com/questions/3888/dataset-of-sentences-translated-into-many-languages)上的这个问题使我指向[开放翻译站点](https://tatoeba.org/) ,该站点可从[这里](https://tatoeba.org/eng/downloads)下载。更好的是,有人在这里做了一些额外的工作,[将语言对拆分为单独的文本文件](https://www.manythings.org/anki/)
英文对法文对太大,无法包含在仓库中,因此请先下载到`data/eng-fra.txt`,然后再继续。 该文件是制表符分隔的翻译对列表:
......
......@@ -73,9 +73,9 @@ test_data = data_process(test_filepaths)
## `DataLoader`
我们将使用的最后`torch`个特定函数是`DataLoader`,它易于使用,因为它将数据作为第一个参数。 具体来说,正如文档所说:`DataLoader`结合了一个数据集和一个采样器,并在给定的数据集上提供了可迭代的。 `DataLoader`支持地图样式和可迭代样式的数据集,具有单进程或多进程加载,自定义加载顺序以及可选的自动批量(归类)和内存固定。
我们将使用的最后`torch`个特定函数是`DataLoader`,它易于使用,因为它将数据作为第一个参数。 具体来说,正如文档所说:`DataLoader`结合了一个数据集和一个采样器,并在给定的数据集上提供了可迭代的。 `DataLoader`支持映射样式和可迭代样式的数据集,具有单进程或多进程加载,自定义加载顺序以及可选的自动批量(归类)和内存固定。
请注意`collate_fn`(可选),它将合并样本列表以形成张量的小批量。 在从地图样式数据集中使用批量加载时使用。
请注意`collate_fn`(可选),它将合并样本列表以形成张量的小批量。 在从映射样式数据集中使用批量加载时使用。
```py
import torch
......
......@@ -360,7 +360,7 @@ root@fa350df05ecf:/home/build# ./dcgan
在本次讨论中,所有权模型是指模块的存储和传递方式-确定特定模块实例的所有者或所有者。 在 Python 中,对象始终是动态分配的(在堆上),并且具有引用语义。 这是非常容易使用且易于理解的。 实际上,在 Python 中,您可以很大程度上忽略对象的位置以及如何引用它们,而将精力集中在完成事情上。
C++ 是一种较低级的语言,它在此领域提供了更多选择。 这增加了复杂性,并严重影响了 C++ 前端的设计和人体工程学。 特别是,对于 C++ 前端中的模块,我们可以选择使用*值语义**引用语义*。 第一种情况是最简单的,并且在到目前为止的示例中已进行了展示:模块对象在栈上分配,并在传递给函数时可以被复制,移动(使用`std::move`)或通过引用或指针获取:
C++ 是一种较低级的语言,它在此领域提供了更多选择。 这增加了复杂性,并严重影响了 C++ 前端的设计和人体工程学。 特别是,对于 C++ 前端中的模块,我们可以选择使用*值语义**引用语义*。 第一种情况是最简单的,并且在到目前为止的示例中已进行了展示:模块对象在栈上分配,并在传递给函数时可以被复制,移动(使用`std::move`)或通过引用或指针获取:
```py
struct Net : torch::nn::Module { };
......
# 在 C++ 中注册分派运算符
# 在 C++ 中注册调度运算符
> 原文:<https://pytorch.org/tutorials/advanced/dispatcher.html>
......@@ -77,7 +77,7 @@ TORCH_LIBRARY_IMPL(myops, CUDA, m) {
## 添加 Autograd 支持
至此,我们有了一个同时具有 CPU 和 CUDA 实现的运算符。 我们如何为它添加 Autograd 支持? 您可能会猜到,我们将注册一个 Autograd 内核(类似于[自定义 Autograd 函数](cpp_autograd)教程中描述的内容)! 但是,有一个变数:与 CPU 和 CUDA 内核不同,Autograd 内核需要*重新分发*:它需要回调分派器才能到达最终的 CPU 和 CUDA 实现。
至此,我们有了一个同时具有 CPU 和 CUDA 实现的运算符。 我们如何为它添加 Autograd 支持? 您可能会猜到,我们将注册一个 Autograd 内核(类似于[自定义 Autograd 函数](cpp_autograd)教程中描述的内容)! 但是,有一个变数:与 CPU 和 CUDA 内核不同,Autograd 内核需要*重新分发*:它需要回调调度器才能到达最终的 CPU 和 CUDA 实现。
因此,在编写 Autograd 内核之前,让我们编写一个*调度函数*,该函数调用调度器以为您的运算符找到合适的内核。 该函数构成了供您的运算符使用的公共 C++ API,实际上,PyTorch C++ API 中的所有张量函数都在后台完全以相同的方式调用了调度器。 调度函数如下所示:
......@@ -127,7 +127,7 @@ Tensor myadd_autograd(const Tensor& self, const Tensor& other) {
1. 使用`at::AutoNonVariableTypeMode` RAII 保护器关闭 Autograd 处理,然后
2. 调用调度函数`myadd`以回调调度器。
如果没有(1),您的调用将无限循环(并且栈溢出),因为`myadd`将使您返回此函数(因为最高优先级分配键仍将是自动微分的。)对于(1),自动微分从一组正在考虑的调度键中排除,我们将转到下一个处理器,即 CPU 和 CUDA。
如果没有(1),您的调用将无限循环(并且栈溢出),因为`myadd`将使您返回此函数(因为最高优先级分配键仍将是自动微分的。)对于(1),自动微分从一组正在考虑的调度键中排除,我们将转到下一个处理器,即 CPU 和 CUDA。
现在,我们可以按照注册 CPU/CUDA 函数的相同方式注册此函数:
......@@ -164,7 +164,7 @@ public:
那么为什么要使用调度器呢? 有几个原因:
1. 它是分散的。 您可以组装运算符的所有部分(CPU,CUDA,Autograd),而不必编写引用所有元素的集中式`if`语句。 重要的是,第三方可以注册其他方面的额外实现,而不必修补运算符的原始定义。
2. 它比 CPU,CUDA 和 Autograd 支持更多的调度键。 您可以在`c10/core/DispatchKey.h`中查看 PyTorch 中当前实现的调度密钥的完整列表。 这些分派密钥为运算符实现了多种可选功能,如果您决定希望自定义运算符支持该功能,则只需为相应的密钥注册内核即可。
2. 它比 CPU,CUDA 和 Autograd 支持更多的调度键。 您可以在`c10/core/DispatchKey.h`中查看 PyTorch 中当前实现的调度键的完整列表。 这些调度键为运算符实现了多种可选功能,如果您决定希望自定义运算符支持该功能,则只需为相应的键注册内核即可。
3. 调度器实现对盒装后备函数的支持,后者是可以一次实现并应用于系统中所有运算符的函数。 盒装后备可用于提供调度键的默认行为。 如果您使用调度器来实现您的运算符,那么您还可以选择所有这些操作的备用。
这是一些特定的调度键,您可能需要为其定义一个运算符。
......
......@@ -77,13 +77,13 @@ with profiler.profile(with_stack=True, profile_memory=True) as prof:
## 打印分析器结果
最后,我们打印分析器结果。 `profiler.key_averages`通过运算符名称,以及可选地通过输入形状和/或栈跟踪事件来聚合结果。 按输入形状分组有助于识别模型使用哪些张量形状。
最后,我们打印分析器结果。 `profiler.key_averages`通过运算符名称,以及可选地通过输入形状和/或栈跟踪事件来聚合结果。 按输入形状分组有助于识别模型使用哪些张量形状。
在这里,我们使用`group_by_stack_n=5`通过操作及其回溯(截断为最近的 5 个事件)聚合运行时,并按事件注册的顺序显示事件。 还可以通过传递`sort_by`参数对表进行排序(有关有效的排序键,请参阅[文档](https://pytorch.org/docs/stable/autograd.html#profiler))。
注意
在笔记本中运行 Profiler 时,您可能会在栈跟踪中看到`<ipython-input-18-193a910735e8>(13): forward`之类的条目,而不是文件名。 这些对应于`<notebook-cell>(line number): calling-function`
在笔记本中运行 Profiler 时,您可能会在栈跟踪中看到`<ipython-input-18-193a910735e8>(13): forward`之类的条目,而不是文件名。 这些对应于`<notebook-cell>(line number): calling-function`
```py
print(prof.key_averages(group_by_stack_n=5).table(sort_by='self_cpu_time_total', row_limit=5))
......
......@@ -196,7 +196,7 @@ Google DeepMind 已经开始使用 AlphaGo Zero 来了解蛋白质折叠,因
* 一个用于白色宝石的特征图(在具有白色宝石的位置具有 1,在其他位置具有 0 的二进制矩阵)
* 一个用于黑宝石的特征图(在具有黑宝石的位置具有 1,在其他位置具有 0 的二进制矩阵)
* 七个使用白色石头的玩家过去的特征图(代表历史,因为它捕获了过去的七个动作)
* 七个使用黑石头的玩家过去特征地图(代表历史,因为它捕获了过去七个动作)
* 七个使用黑石头的玩家过去特征映射(代表历史,因为它捕获了过去七个动作)
* 一个用于转弯指示的特征图(转弯可以用 1 位表示,但此处已在整个特征图上重复出现)
因此,网络输入由`19 x 19 x (1 + 1 + 7 + 7 + 1) = 19 x 19 x 17`张量表示。 使用过去七个动作的特征图的原因在于,这段历史就像一个注意力机制。
......
......@@ -152,7 +152,7 @@ EIIE 通过**在线随机批量学习**(**OSBL**)进行训练,其中强化
* `V[t]``V[t]^(hi)``V[t]^(lo)`是归一化价格矩阵
* `1 = [1, 1, ..., 1]^T``Φ`是逐元素除法运算符
因此,`X[t]`是三个归一化价格矩阵的栈:
因此,`X[t]`是三个归一化价格矩阵的栈:
![](img/d4c0367b-bfe2-4e38-8ee5-421da65351e6.png)
......@@ -199,7 +199,7 @@ EIIE 通过**在线随机批量学习**(**OSBL**)进行训练,其中强化
PVM 是按时间步长顺序(即时间顺序)收集投资组合向量的集合。 在训练周期的每个时间步`t`,策略网络从`t-1`的存储位置取最后时间段的投资组合权重向量`w[t-1]`,并进行覆盖`t`处的内存与输出投资组合权重向量`w[t]`的关系。 由于策略网络参数的收敛,PVM 中的值随着训练时期的增加而收敛。
单个内存栈(例如 PVM)还有助于使用小批量提高训练过程的并行性,从而提高训练过程的效率。
单个内存栈(例如 PVM)还有助于使用小批量提高训练过程的并行性,从而提高训练过程的效率。
对于有监督的学习,数据的排序以小批量为单位,但是在这里,数据需要按照训练过程中每批传递的时间步长进行排序。 现在,由于数据是按时间序列格式的,因此从不同时间段开始的小批量是更可取的,因为它们涵盖了训练过程的独特数据。 金融市场的持续性导致不断向智能体网络输入新数据,从而导致训练数据中的数据爆炸。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册