CS224n笔记(一)

自然语言涉及的几个 层次

pic1

NLP是一门交叉学科,它往往包含着Computer Science, artificial intelligence 和 linguistics。
如上图可以看到自然语言涉及的是内容很广泛,而本课程Cs224n主要围绕图中画圈部分。

Deep NLP=Deep Learning +NLP

将自然语言处理的思想与表示学习结合起来,用深度学习的方法解决NLP目标,不仅简化了过程而且提高了许多方面的效果。

应用:机器翻译、情感分析、客服系统、问答系统

比如机器翻译

传统方法在许多层级(词语、语法、语义之类)上做了尝试。而用Deep NLP这类方法试图找到一种世界通用的“国际语”(Interlingua)来作为原文和译文的桥梁。

pic2

Word Embedding

1.意义

  • 机器能够通过大量的阅读文档获取文字的意思(非监督学习)
  • 一个词汇可以被它的上下文理解
    Word_embedding_1

    2.方法

  • count based:如果两个单词$w_i$和$w_j$常同时出现,那么$V(w_i)和V(w_i)$将相似的。例如算法:Glove Vector,LSA,HAL,COALS,Hellinger-PCA
    Word_embedding_2
  • Prediction-based:
    Word_embedding_3

    3.word2Vector

    事实上,word2vec是一个工具包,里面包含着几种word embedding的方法,其中这些方法中最为突出的模型就是CBow、skip-gram.这些方法训练得到的embedding vector特别好,维度小,便于计算同时上下文之间的联系又好。

最后我们这里得到的vector会输入到RNN或者其他网络里继续其他任务的训练。

Word2Vec

抛出一个问题:计算机如何处理词语,或者说计算机如何表示一个词语的意思?(语义这是一个很主观的概念)
在此之前计算语言学采用的是WordNet那样的库。

WordNet

WordNet:
是面向语义的英语词典,与传统词典类似,但结构更丰富。我们可以用NLTK来了解一下WordNet
首先你需要pip install nltk

from nltk.corpus import wordnet as wn
wn.synsets(‘motorcar’)#找到它的同义词集
wn.synset(‘car.n.01’).lemma_names#car.n.01 指的是car的第一个名词的意思,
wn.synset(‘car.n.01’).lemma_names()#列出该同义词集的所有词条

WordNet的同义词集相当于抽象的概念,他们并不总是有对应的英语词汇。这些概念在层次结构中相互联系在一起。用树来表示,每个节点对应一个同义词集,边表示上位词/下位词关系,即上级概念与从属概念的关系。
总之,WordNet不光把单词以字母顺序排列像字典一样,而且按照单词的意义组成了一个单词网络。

还有一种表示词语意思的方式是discrete representation(典型代表onehot).这种方式会有以下一些问题:

  • 缺少韵味,比如同义词之间微妙的差别无法通过该表示来表现出来
  • 缺少新词
  • 主观化
  • 耗费大量人力去整理
  • 无法计算准确的词语相似度

从symbolic representation 到distributed representation 词语在符号表示上都体现不出意义的相似性。
需要找到一种用向量直接编码含义的方法。

Distributional Similarity based representation

如果能把单词放到正确的上下文中去,才能说明你真正掌握了它的含义。
即通过一个词的邻居来表达它的含义,You shall know a word by the company it keeps.

分布相似性的概念是一种关于词汇语义的理论,你可以通过理解单词出现的上下文来描述该单词的含义。

通过向量定义词语的含义:
通过调整一个单词及其上下文单词的向量,使得根据两个向量可以推测两个词汇的相似度;或根据向量可以预测词语的上下文。这种手法是递归的,根据向量来调整向量,与词典中意项的定义相似。

这里提出一个问题:distributed representation 和distributional representation的区别??

distributed:分布式描述的是若干个元素的连续表示形式,比如稠密的词嵌入向量的表示,与之相反的是onehot 向量

distributional:使用词语的上下文来表示其语义,Word2vec和基于计数的词向量表示都是分布表示,因为我们都是用词语的上下文来表征它的含义

更具体一点:考虑distributional models和distributed models的区别:

  • distributional models(BOW,LSI,LDA):1.共现在同一文本区域中词(如同一语句)相关,且在语料中的共现语句越多越相关。2.使用共现语句个数构建词与词(上下文)的PMI/PPMI矩阵(高维稀疏矩阵),然后进行SVD得到每个词的低维稠密向量(隐向量)
  • distributed models(NPLM,LBL,Word2vec,Glove):1.相同上下文中出现的词具有相关性,相同的上下文在预料库中越多越相关,不要求同时出现。2.思想来源于深度学习,使用预测代替共现计数

一脸懵逼的我来举个例子:

1.A woman is in the room. 2.A man is in the room. 这两个句子,我们可以看到,woman和room是Distributional(共现),woman和man是distributed(同上下文).

个人总结:distributional 思想是同一语句中出现的词是相关的。这是一种横向思想。distributed思想是具有相似上下文的词语相关。这是纵向思想。

至于方法上,distributional会使用隐矩阵分解、使用共现计数来构建原始矩阵等。distributed会使用神经网络词嵌入、用神经网络进行上下文预测等。

Word2vec的主要思想及模型

通过前面的比较distributional model 和distributed model我们应该能发现Word2Vec的一些特点。比如上面例子中的woman与man为什么意义可以用他的上下文来表示,此时不一定要woman与man共现,但我们能得到他们具有相似的向量。

word2vec的主要思想就是单词和上下文的彼此预测。

两个主要的算法:

  • skip-grams: 给定一个单词,预测其上下文
  • continuous Bag of words(CBOW): 给定上下文预测目标单词

Skip-grams:
skip-grams

CBOW:
CBOW

两种稍微高效一些的训练方法:

  • Hierarchical softmax
  • Negative sampling

我们慢慢来一点点介绍。(没办法,课程进度就是这样。。。。)

Skip-gram

从上面的示例图,我们可以看到skip-gram算法是已知中心词来最大化上下文词的联合分布概率,并且从算法名字可以看出这是一种词袋模型(与位置无关)。注意:这里上下文词是需要一个窗口来框定的。所以这里的联合概率分布也并非是一个全局的概念而是局部的。常将窗口大小设为5.当然最后是要求所有窗口概率的乘积。


skip-gram的理解

首先,网络的目的不是要预测上下文会出现啥词,这只是一个fake task,网络的真正目的正如 @Stark Einstein 所说,是为了实现嵌入。这一点与AntoEncoder是类似的,AutoEncoder的真正目的也不是为了复现输入,而是为了获得隐层压缩的特征。关于题主所说的权值共享问题,我从下面的角度尝试解释一下。从隐层到输出层的计算过程是:center word的词向量,与每个context的词向量进行内积,会计算一个相似度,再用softmax做一下归一化。下面来举个例子,例如“I really love machine learning and deep learning”,假如我们选择machine作为中间词,由这个句子构造出的训练集长这个样子(window_size=2的话):(machine,really),(machine,love),(machine,learning),(machine,and),也就是说通过context可以构造出4个训练样本。这个时候我们只训练一下这4个样本,意味着给定machine这个词以后,really,love,learning,and的出现的概率都一样。但是这里有个问题,我们的样本是多种多样的,我们下一个句子如果是”I like machine learning“,再训练一下,是不是给定machine这个中心词,context中出现learning的概率就更高了呢?同样的道理,如果我们训练样本足够大且无偏,根据这样训练下来的结果就会出现,给定machine这个center word,其输出的context word的概率也会有一个排序,这就意味着它对应的context的word的词向量肯定不一样。再回过头来说,如果你的100W个句子都是”I really love machine learning and deep learning“,加上权值共享,结果就是给定machine以后,它输出really,love,learning,and这四个词的概率完全相同,这就意味着这四个词的词向量也肯定完全一样了。

https://www.zhihu.com/question/268674520/answer/340499173

skip-gram_example

我们有目标函数:

其中T是表示窗口数,-m≤j≤m是窗口大小,当然不包括中心词自身。这里我们的true label是各个该中心词的context的onehot形式。(可以理解为我们希望中心词与该上下文词相似的概率为1,与其他词的概率为0,因此该true label也是onehot的形式。)

现在我们有了目标函数,知道要训练的参数是 $\Theta$,问题的关键是 $P(W_{t+j}|W_t)$ 的形式我们未知。

前面提到了我们希望用一个向量表示一个词汇的含义,而我们的向量内积就具有内积越大越相似的特点。因此我们可以用两个向量的内积形式来表示概率。在这里用到了一些trick,

强烈注释:这里O表示上下文词向量中的某一个,C表示中心词。u是对应的上下文词向量,v是中心词向量

我们完全有理由相信同一个词语具有上下文词向量和中心词向量两个向量。

前向传播

skip-gram_1
这就是上面计算P(O|C)的示意图。可以看到我们整个模型所要学习的参数就是这个向量,通过不断调整这个向量里面值得大小来使前面提到得目标函数 $J(\Theta)$ 最大。因此这些词向量就是我们得参数 $\Theta$.

那么问题又来了,我们的输入是什么??

显然如果把这个模型看成一个黑箱,我们希望输入的是一个单词,输出的是表示该单词意义的向量。但是我们不可能对一个字符串进行数字计算,但我们又需要能表示一个单词,则用到了前人所提到的onehot.(具体形式不多说)

总的模型形式是这样的:

model
左侧是我们的输入向量onehot向量。他与一个矩阵W相乘得到也得到一个向量。具体形式可以如下图给出,
skip-gram2
因为onehot只有一个元素为1其余都为0,因此上述过程可视为提取向量的过程。而上述W矩阵我们也可以称为中心词矩阵。

得到中心词向量以后我们又拿这个词向量与矩阵 $W’$ 相乘,很显然这个矩阵中的各个向量就能表示上下文的词向量。通过两者相乘我们可以知道中心词向量与某个上下文词向量的内积,然后如前做一个softmax,我们就能计算得到在某个中心词下某个上下文词出现的概率P(O|C)。

上述流程就是我们整个模型前向传播的过程。不要忘了我们的目标是反向传播学习 $\Theta$.这里我们的 $\Theta$可以表示为如下形式:
skip-gram_parameter

反向传播

来比一波公式推导:

其中

对上式求导:

其中

最后可得偏导数为:

同理对 $u_w$的偏导有如下形式:

值得注意的是,Skip-gram的目标函数需要建立在朴素贝叶斯的条件概率独立的假设上,即除了中心词,所有的上下文词都相互独立。

That’s all。我们可以从这里看到这里计算梯度的时候有一个求和符号 $\sum$这会造成极大的计算量,因此需要有一些改进的更新策略比如Hierarchical softmax和Negative sampling,都可以降低计算复杂度。

Skip-gram代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class SkipGramModel(nn.Module):
def __init__(self,emb_size,emb_dimension):
super(SkipGramModel,self).__init__()
self.emb_size=emb_size
self.emb_dimension=emb_dimension
self.u_embeddings=nn.Embedding(emb_size,emb_dimension,sparse=True)
self.v_embeddings=nn.Embedding(emb_size,emb_dimension,sparse=True)
self.init_emb()

def init_emb(self):
initrange=0.5/self.emb_dimension
self.u_embeddings.weight.data.uniform_(-initrange,initrange)
self.v_embeddings.weight.data.uniform_(-0,0)

def forward(self,pos_u,pos_v,neg_v):
emb_u=self.u_embeddings(pos_u)
emb_v=self.v_embeddings(pos_v)
score=torch.mul(emb_u,emb_v).squeeze()
score=torch.sum(score,dim=1)
F.logsigmoid(score)
neg_emb_v=self.v_embeddings(neg_v)
neg_score=torch.bmm(neg_emb_v,emb_u.unsqueeze(2)).squeeze()
neg_score=F.logsigmoid(-1*neg_score)
return -1*(torch.sum(score)+torch.sum(neg_score))

def save_embedding(self,id2word,file_name,use_cuda):
if use_cuda:
embedding=self.u_embeddings.weight.cpu().data.numpy()
else:
embedding=self.u_embeddings.weight.data.numpy()

fout=open(file_name,'w')
four.write('%d %d\n'%(len(id2word),self.emb_dimension))
for wid,w in id2word.items():
e=embedding[wid]
e=''.join(map(lambda x: str(x),e))
four.write('%s %s\n'%(w,e))

从代码中可以看出skip-gram也就是将word的onehot通过两个个embedding层(参数$\theta_v$和\theta_u)然后将两个embedding vector进行内积处理。

Continuous Bag of Words(CBOW)

如前所述,CBOW做的是给定上下文词汇预测中心词。很显然这就像是skip-grams的逆过程。如下图所示。

CBOW1

与前面的方法相似,这里也只有一个隐层,我们所要学习的参数也是两个矩阵W和$W’$。但是不同的是,这里输入的是上下文词的onehot,不再是单一的向量而是多个向量。而此时我们的输出变成了单个向量。

这里会有个问题,就是C个Onehot向量与$W_I$相乘会得到N×C的中间矩阵,再与N×V的$W_O$作用,得到的也只是V×C的矩阵,并不能得到我们所需要的V×1的表示中心词的向量。很显然,在中间隐层处我们需要做一些处理使中间的输出为N×1的向量。这里采用的是平均值法,即对中间得到的C个N×1的向量进行相加再取平均。

具体CBOW的流程如下所示(转自[1]):

假设 Courpus = { I drik coffee everyday } ,根据 “I”“drink”“everyday”来预测“coffee”。
CBOW_step1
CBOW_step2
CBOW_step3
CBOW_step4
理论上,像上图求出$U_o$之后我们只要做一个softmax就能计算出,各个词汇可能作为中心词的概率。(这里我们的true label是该词的onehot的形式)

但是就像前面提到那样,这样做softmax在计算分母的时候需要极大的计算量,因此我们我们会用别的方法来替代,比如Hierarchical softmax和Negative sampling。接下来将介绍。

Hierarchical Softmax

前提:无论是这里的Hierarchical Softmax还是后面的Negative Sampling都是对前面两种方法(skip-grams和CBOW的改进,目的是为了性能更好,计算量更小。Bengio的《A neural probabilistic language model》可以帮助理解上述两个算法)

算法做了改进整个模型的结构也不大一样了。此时无论是skip-grams还是CBOW都是三层:Input layer、projection layer、output layer。

1.Input layer:包含Context(w)中2c个词的词向量 $v(Context(w)_{1}), v(Context(W)_{2}),…v(Context(w)_{2c})$_.这里再定义一个表示词向量的长度。(这里输入的向量不是像前面介绍的那样是onehot向量)

2.projection layer:将输入层2c个向量求和累加(summation)得到$X_w$

3.output layer:输出层对应一棵Huffman树.

与前面介绍的完全不同,前面主要是采用矩阵的运算,最后进行一个softmax,而这里在投影层只做了累加和,最后的输出改用了一个Huffman树。因此可见,Hierarchical Softmax要体现其优点完全要靠输出层的Huffman树。

下面是CBOW模型用Hierarchical Softmax的示意图。
CBOW_Softmax
下图是skip-grams模型用Hierarchical Softmax。
skip-grams_Softmax

下面主要介绍用Hierarchical Softmax的CBOW

CBOW with Hierarchical Softmax:

梯度计算

考虑Haffman树,以预料库中出现的词作为叶子节点,以各词在语料中出现的次数当权值构造出一个Huffman树。这个Huffman树中叶子节点有N个,非叶子节点有N-1个。

考虑Huffman树中的某个叶子结点,假设它对应词典D中的词w,记

$1. p^{w}$:从根节点出发到达w对应叶子结点的路径
$2. l^{w}:路径p^{w}中包含结点的个数$
$3. p_1^{w},p_2^{w},…p_{l^{w}}^{w} :路径p^{w}中的l^{w}个结点,其中 p_1^{w}表示根节点,p_{l^{w}}^{w}表示词w对应的结点。$
$4. d_1^{w},d_2^{w},…,d_{l^{w}-1}^{w} ∈\{0,1\}:词w的Huffman编码,它由 l^{w}-1位编码构成,d_j^{w}表示路径p^{w}中第j条边对应的编码。$
$5.\Theta_1^{w},\Theta_1^{w},…,\Theta_{l^{w}-1}^{w}∈R^{m}:路径p^{w}中非叶子结点对应的向量,\Theta_j^{w}表示路径p^{w}中的第j个非叶子结点对应的向量$
CBOW_Huffman
上述Huffman树中的红线路径即为该叶子结点词的Huffman编码(可以考虑左0右1,但图上是左1右0)。
我们可以这么认为每经过一个非叶子结点就需要进行一次二分类,而我们希望选择通往目标叶子结点的路径概率最大。这里如何做一个二分类??很显然我们可以利用Sigmoid函数。

假设从我们的根节点出发到目标叶子结点需要经过4条边,显然我们需要经历四次二分类,得到下面式子(根据上面的Huffman编码书写的公式):
$第1次:p(d_1^{w}|x_w,\Theta_1^{w})=1-\sigma(x_w^{T}\Theta_1^{w})$
$第2次:p(d_2^{w}|x_w,\Theta_2^{w})=\sigma(x_w^{T}\Theta_2^{w})$
$第3次:p(d_3^{w}|x_w,\Theta_3^{w})=\sigma(x_w^{T}\Theta_3^{w})$
$第4次:p(d_4^{w}|x_w,\Theta_4^{w})=1-\sigma(x_w^{T}\Theta_4^{w})$

根据前面几节介绍的,我们的目标是求$P(O|C)$,现在得到的这些概率对我们有什么用呢?

很好理解:$P(O|C)=\prod_{j=1}^{4}P(d_j^{w}|x_w,\Theta_{j}^w),输入的是我们的x_w(已知),希望得到的目标词汇O的概率$。整合到这个预料库中,我们希望这个所有词汇的对数似然函数的和最大,即有下式:

接着就可以用梯度上升法来求参数了.

同理skip-grams也可以用到Hierarchical Softmax,方法与上述类似。

小结:尽管该方法名字中带有Softmax但是实际上并没有用到我们传统意义上的Softmax,而是进行了分层,考虑了每一层上二分类问题(用sigmoid),这样做就避免了之前softmax时分母计算量大的问题。(上面Sigmoid使用有点类似于逻辑回归中二分类用Sigmoid多分类用Softmax).这里用Huffman树有一个特点,就是越是频繁的词越接近根结点而越是生僻的词越接远离根节点。

Negative Sampling

从上述最后的总结里我们可以看出如果我们用的是一个特别大的预料库,生僻词会离树的根结点特别远,何况有时概率的乘积操作,这个值也会特别的小。因此提出了Negative Sampling希望能解决这个问题。

主要思想

还是假设是CBOW模型,我们已知词w的上下文context(w),需要预测w.显然如果考虑其为二分类问题,w是我们的正例,而其他所有乱七八糟的词都是负例。并且所有负例都选择显然是不可行的,因此我们要对负例进行采样,选择一部分负例。那么问题来了,怎么选择??

采样方法

对于一个给定的词w(正例),如何生成它的负例集NEG(w).

词典D中的词在预料C中出现的次数有高有低,对于那些高频词,被选为负样本的概率就应该比较大,反之,那些低频词被选为中的概率就比较小。这就是我们对采样过程的一个大致要求。本质上就是一个带权采样问题。

具体在word2vec中怎么处理呢,可以看下面这个流程(转自[2])

说白了上述过程就是采样里的直接采样方法。
与之前的不同的是,这里对词典D中每个词汇的权值定义与一般的不一样,

梯度计算(CBOW)

跟前面一样,现在我们知道了哪个是正例(w),哪些是负例(NEG(w))。我们的目标就是最大化正例的概率最小化负例的概率。有下式:

其中:

这里的$x_w$仍是context 各词向量的累加和,u是咱们从正例和负例里取出的值。

在一个样本集里,我们需要最大化的是一个样本里的连乘概率,即

之后的方法就与之前的相似了,我们需要使其最大值,更新参数采用梯度上升法。
同样我们的skip-grams也可以用这个方法训练模型。

Glove

该算法是一种利用共现矩阵的方法。

  • 模型目标:进行词的向量化表示,使得向量之间尽可能地蕴含语义和语法地信息。
  • 输入:语料库
  • 输出:词向量
  • 方法:首先基于语料库构建词地共现矩阵,然后基于共现矩阵和Glove模型学习词向量。

    统计共现矩阵

    设共现矩阵为X,其元素为$X_{i,j}$

$X_{i,j}$的意义为:在整个预料库中,单词i和单词j共同出现在一个窗口中的次数。
设有语料库:

1
i love you but you love him i am sad

该语料库只有一个句子,涉及到7个单词,如果我们采用一个窗口宽度为5(左右长度都为2)的统计窗口,则有以下窗口内容:
Glove_1
使用窗口将整个语料库遍历一遍,即可得到共现矩阵X

使用GloVe模型训练词向量

模型:

这是我们的损失函数。其中$v_i,v_j$是单词i和单词j的词向量,$b_i,b_j$是两个标量(作者定义的偏差),f是权重函数,N是词汇表的大小(共现矩阵维度为N×N)
可以看到,GloVe模型没有使用神经网络的方法。

模型是怎么来的

先定义几个符号:

其实就是矩阵单词i的那一行的和;

该条件概率表示单词k出现在单词i语境中的概率;

这表示两个条件概率的比率。

思想:假设我们已经得到了词向量,如果我们用词向量$V_{i}、V_{j}、{k}$通过某种函数计算$ratio_{i,j,k}$能够同样得到这样的规律的话,就意味着我们词向量与共现矩阵具有很好的一致性,也就说明了我们的词向量中蕴含着共现矩阵中所蕴涵的信息。

设词向量$V_{i}、V_{j}、V_{k}$计算$ratio_{i,j,k}$的函数为$g(V_i,V_j,V_k)$(暂时先不去管具体的函数形式),那么应该有:

即两者应该尽可能地接近;
很容易想到用二者的差方来作为损失函数

但这样做的话复杂度就是N×N×N.太复杂了

作者做了一些改变:

  • 1.考虑单词i和单词j之间的关系,那么$g(V_i,V_j,V_k)$中大概要有这么一项$V_i-V_j$。即如果我们要在线性空间中考虑两个向量的相似性,会不失线性地考察。
  • $ratio_{i,j,k}$是一个标量,那么$g(V_i,V_j,V_k)$最后应该也是个标量,虽然其输入都是向量,那么做内积也是合理地选择,应该有这么一项$(V_i-V_j)^{T}V_k$
  • 然后作者又往$(V_i-V_j)^{T}V_k$的外面套了一层指数运算exp(),得到最终的$g(V_i,V_j,V_k)=exp((V_i-V_j)^{T}V_k)$。套上之后,我们的目标就是让以下公式尽可能地成立:然后发现存在简化方法:只需让上式分子对应相等,分母对应相等,即:$P_{i,k}=exp(V_i^{T}V_k)$并且$P_{j,k}=exp(V_j^{T}V_k)$
    然而分子分母形式相同,可以将两者统一考虑了,即:本来追求地是现在只需追求:两边取对数:那么代价函数就可以简化为:

现在复杂度就变为了N×N.回过头来看那个套exp()的一步,是为了使差形式变为商形式,进而等式两边分子分母对应相等,进而简化模型。

然而仍存在着问题:
$log(P_{i,j})=V_i^{T}V_{j}$和$log(P_{j,i})=V_j^{T}V_{i}$
等式作者应该是不具有对称性的,然而等式右侧是有对称性的。因此还需要改进。

现将损失函数中的条件概率展开为:

扩展为:

而继续:

即添加了一个偏差项$b_j$,并将$log(X_{i})$吸收到偏差项$b_i$中。
于是代价函数变为:

然后基于出现频率越高的词对儿权重应该越大的原则,在损失函数中添加权重项,于是代价函数进一步完善:

具体权重项应该是怎么样的

首先应该是非减的,其次当词频过高时,权重不应过分增大。

作者通过实验确定权重函数为:

Countbased VS Direct Prediction

这些基于计数的方法在中小规模语料训练很快,有效地利用了统计信息。但用途受限于捕捉词语i相似度,也无法扩展到大规模预料。

而NNLM,HLBL,RNN,Skip-gram/CBOW这类进行预测地模型必须遍历所有的窗口训练,也无法有效地利用单词地全局统计信息。但它们显著地提高了上级NLP任务,其捕捉地不仅限于词语地相似度。

显然我们上面讲地Glove是这两种的优势结合

参考文献

[1]https://blog.csdn.net/github_36235341/article/details/78607323
[2]https://blog.csdn.net/itplus/article/details/37998797
[3]CS224n 1-4节课

-------------本文结束感谢您的阅读-------------

本文标题:CS224n笔记(一)

文章作者:Yif Du

发布时间:2018年10月30日 - 21:10

最后更新:2019年03月22日 - 17:03

原始链接:http://yifdu.github.io/2018/10/30/CS224n笔记(一)/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。