赞
踩
序列数据
是由一组相互关联的样本组成的数据,其中任意样本对应的标记是由其自身和其他样本共同决定的;序列数据任务
是输入或输出为序列数据的机器学习任务,用传统机器学习模型处理他们是困难的,比如 序列模型(1)—— 难处理的序列数据 中第 3 节的例子one-to-one
模型,即一个输入一个输出。这种模型会把序列数据作为一个整体来考虑,其输入输出的尺寸必须是固定的,比如输入一张固定尺寸的图像输出其类别,或者输入一段固定长度的句子预测下一个词
Note:借助一些手段,可以强行用传统模型处理变长的输入输出问题,如 各种监督学习范式(强监督、半监督、多标记、偏标记、多示例、多示例多标记、标记分布…) 中的多示例多标记问题,但是这些模型本质上还是要将输入输出处理成固定的长度(比如用固定长度的 0/1 向量选出不同数量的样本作为输入输出)。至于样本间的关系,虽然可以通过网络自己学出来,但由于缺乏考虑这种关系的显式结构,所以很难学得好(可以参考 从模型容量的视角看监督学习)
many-to-many
模型,即支持可变长度的输入输出,并且最好能对序列样本间的关系进行显式建模,注意这样的模型也可以直接用来处理 one-to-many
,many-to-one
甚至 one-to-one
问题。语音识别、本文情感分析、序列预测等等序列任务都能被这种模型更好地处理
RNN 就是一种良好的序列模型,下图给出了各类输入输出情况下的 RNN 结构
传统监督学习任务
(many-to-one),常将序列模型作为 “特征提取器”,只使用最后一个隐状态
h
t
\pmb{h}_t
ht 作为整个序列的特征向量,用它接一个分类头或回归头作为整个模型的输出。在训练时,只要像图像分类等普通监督学习任务一样训练即可标准语言模型任务
(one-to-many),即不断根据之前序列样本预测下一个样本值的任务,通常会如下图所示做 Autoregress,这时我们会增加一个分类头或回归头将隐状态
h
\pmb{h}
h 变换为输出
x
\pmb{x}
x,推断时不断地将上一步模型输出合并到下一步模型的输入中。在训练时,会构造很多以连续的 n 个样本作为输入,紧接着第 n+1 个样本作为标签的自监督样例,详见下文 1.4.1 节Seq2Seq 任务
(many-to-many),通常使用 Encoder-Decoder 结构。这时 Encoder 就是类似 1 中的 many-to-one 序列特征提取器,Decoder 就是类似 2 中的 one-to-many 序列生成器,Encoder 提取的特征作为 Decoder 的初始 seed,二者结合就能做 many-to-many 了。训练时通常用 teacher-forcing 形式,详见下文 1.4.2 节LSTM 是对 RNN 模型的改进,可以有效缓解 RNN 的梯度消失(见下文 1.3.2 节)问题,其结构如下所示
这个结构看上去很复杂,不过我们可以从先从宏观角度来理解:相比 RNN,LSTM 针对序列数据性质增加了对数据处理过程的限制,从而减少了模型的弹性/容量,使它能在使得相同样本量下更好地提取序列信息,这和 CNN 比 MLP 能更好地处理图像数据是一个道理
现在来仔细看一下这个结构,LSTM 的设计灵感来自于计算机的逻辑门,它的输出和 RNN 一样仍然是隐状态
H
\mathbf{H}
H,只是生成过程更复杂。相比 RNN,LSTM 引入了记忆单元cell
C
\mathbf{C}
C,它和隐状态
H
\mathbf{H}
H 具有相同的形状,用于记录附加的信息并产生输出
H
\mathbf{H}
H,其他的所有门都是为了控制这个记忆单元服务的。具体而言,设隐藏层维度为
h
h
h,batch size 为
n
n
n,样本维度为
d
d
d,则批量输入为
X
t
∈
R
n
×
d
\mathbf{X}_t \in \mathbb{R}^{n \times d}
Xt∈Rn×d,前一时刻隐状态为
H
t
−
1
∈
R
n
×
h
\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}
Ht−1∈Rn×h
列一下公式,有
C
~
t
=
tanh
(
X
t
W
x
c
+
H
t
−
1
W
h
c
+
b
c
)
,
I
t
=
σ
(
X
t
W
x
i
+
H
t
−
1
W
h
i
+
b
i
)
,
F
t
=
σ
(
X
t
W
x
f
+
H
t
−
1
W
h
f
+
b
f
)
,
O
t
=
σ
(
X
t
W
x
o
+
H
t
−
1
W
h
o
+
b
o
)
,
˜Ct=tanh(XtWxc+Ht−1Whc+bc),It=σ(XtWxi+Ht−1Whi+bi),Ft=σ(XtWxf+Ht−1Whf+bf),Ot=σ(XtWxo+Ht−1Who+bo),
C~tItFtOt=tanh(XtWxc+Ht−1Whc+bc),=σ(XtWxi+Ht−1Whi+bi),=σ(XtWxf+Ht−1Whf+bf),=σ(XtWxo+Ht−1Who+bo), 这里的
W
,
b
\pmb{W},\pmb{b}
W,b 都是要学习的参数,相比 RNN 多了三组。另外,所有信息选取都是通过对应位置乘以 (0,1) 间小数的方式进行的,如图可见有 “遗忘门
F
\mathbf{F}
F 去除部分老记忆
C
t
−
1
\mathbf{C}_{t-1}
Ct−1”、“输入门
I
\mathbf{I}
I 合并部分候选记忆
C
~
\mathbf{\tilde{C}}
C~” 和 “输出门
O
t
\mathbf{O}_t
Ot 保留部分新记忆
C
t
\mathbf{C}_t
Ct” 三处,公式表示为
C
t
=
F
t
⊙
C
t
−
1
+
I
t
⊙
C
~
t
H
t
=
O
t
⊙
tanh
(
C
t
)
.
Ct=Ft⊙Ct−1+It⊙˜CtHt=Ot⊙tanh(Ct).
CtHt=Ft⊙Ct−1+It⊙C~t=Ot⊙tanh(Ct).
直观地看这个结构,其实就是每次 batch 输入先产生和 RNN 中 H \mathbf{H} H 完全一样的 C ~ t \mathbf{\tilde{C}}_t C~t 作为 “当前步记忆”,然后用遗忘和输入门控制它和 “历史序列记忆” C t − 1 \mathbf{C}_{t-1} Ct−1 混合得到 “最新序列记忆” C t \mathbf{C}_{t} Ct,最后使用输出门将 C t \mathbf{C}_{t} Ct 衰减后输出为 H t \mathbf{H}_t Ht
最后再回到宏观来看,LTSM 通过学习四组系数 & 偏置参数,要求模型按照上述逻辑构造记忆并从中提取输出,相比 RNN 直接一个 FC 加激活函数,它通过模型结构引导其做出更符合序列性质的行为,从而缓解了 RNN 的梯度消失和梯度爆炸问题,大幅提升了 RNN 的性能
Stacked RNN/Stacked LSTM
,其容量更大,能表示的映射关系也更加复杂,如下图所示无论 RNN 还是 LSTM,模型都只能利用隐藏状态间接地获取之前序列的信息,由于隐藏状态的维度一定远远小于之前的变长序列所有样本的连接维度,这种做法无可避免地会损失一些信息,体现在微观层面上就是两个经典问题
梯度消失
:参数更新梯度被近期样本主导(和 MLP 里的梯度消失不太一样)梯度爆炸
:参数更新梯度梯度趋近
∞
\infin
∞在 RNN 中这两个问题尤其严重(可以参考 RNN梯度消失和爆炸的原因),以梯度消失为例,近期的序列样本主导了优化方向,这会导致模型快速忘记相隔时间比较久的早期序列输入,比如下例
这个 RNN 的任务是根据先前序列预测下一个单词,在 x 1 x_1 x1 处输入了 “China”,但由于梯度消失这个信息几乎没法被记住,模型难以在相隔较远的 h t + 1 h_{t+1} ht+1 处给出 “Chinese” 的预测
这里模型大概能学到应该输出一个语言,但是具体是什么语言会被近期序列的倾向所主导
LSTM 通过引入 “记忆单元” 缓解了这些问题,但依然无法完全解决。一个简单粗暴的优化方案是直接同时从两个方向训练 RNN 或 LSTM,这样得到的 Bidirectional RNN/Bidirectional LSTM
结构如下
这样一来,一个方向的早期样本就成了另一个方向的近期样本,可以缓解 RNN/LSTM 的遗忘问题。另外这里输出的
y
y
y 是两个模型输出的向量拼接,它也可以像上面那样进行 stack 从而扩展容量
处理序列数据时往往要做一步 embedding,使得序列样本变得可以处理。比如常见的文本模型,你没法直接向网络输入一个单词,这时有几个做法
一个问题是,FC 嵌入层往往很大,有时甚至比模型参数还多不少,如果直接把它接在 RNN 或 LSTM 的输入之前一起训练,很容易导致嵌入层过拟合,影响模型性能。这时我们可以先用一个别的任务专门训练这个 FC 嵌入层,这就是所谓的 预训练pre-train
过程,几个注意点是
完成预训练后,直接用预训练模型得到的 FC 嵌入层参数初始化目标模型的 FC 嵌入层,然后在目标任务训练过程中有时将其固定住只训练模型的其他部分,有时也带着这个 FC 层一起训练,这称为 微调fine-turn
过程
所有涉及预处理的部分都可以用类似思路进行预训练,可以有效提高模型性能
many-to-one
形式的,但是通过巧妙的应用,其能处理的问题涵盖了 one-to-many
,many-to-one
和 one-to-one
所有情况,下面简单举两个例子进行说明many-to-one
的形式,另外由于输入长度必须固定,某种程度上也能看做 one-to-one
形式
换个角度看,此模型也可理解为先用序列模型提取前驱序列的特征(RNN/LSTM 视角下就是隐变量),再用这个特征做多分类来选择生成下一个 token,直接把这两件事放在一起进行 end-to-end 的训练
one-to-many
形式to-one
任务的模型完成 to-many
任务。监督学习模型基本都是 to-one 的,所以借助这个结构,我们甚至可以用 MLP 等八竿子打不着的模型来做文本生成,但由于这些网络缺乏对于序列任务的归纳偏置,提取序列特征的能力差,效果通常很差。可以参考 序列模型(1)—— 难处理的序列数据机器翻译的输入和输出都是变长度序列,是典型的 many-to-many
问题,机器学习中也称这种任务为 “Seq2Seq” 任务。为了让 RNN/LSTM 有能力处理这类问题,模型要设计成特殊的 “编码器-解码器架构”。正如其名,这种架构含有两个组件
编码器Encoder
:接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态,即从输入序列中提取一个特征向量。原始的 RNN/LSTM 可以完成这种 many-to-one
的任务,但特征向量中不可避免地会损失长跨度样本的信息,这也是后面出现注意力机制的重要原因解码器Decoder
:将固定形状的编码状态映射到长度可变的序列。这恰好是上面 1.4.1 节那种 one-to-many
任务,所以 Decoder 也可以看做使用 Encoder 编码特征作为初始种子特征向量的自动文本生成任务。为了能自动控制输出长度,通常要增加 “起始” 和 “终止” 两个特殊 token机器翻译模型设计得很巧妙,通过连接 many-to-one
和 one-to-many
的两个组件真正实现了 many-to-many
任务
训练阶段,Encoder 和 Decoder 连在一起作为一个 many-to-many 模型训练。以文本翻译任务为例,对于数据库中一个形如 ( 样本序列 , 标签序列 ) (样本序列, 标签序列) (样本序列,标签序列) 的样本,如下操作
每次计算的损失梯度会从 Decoder 一直回传到 Encoder,同时更新两个组件的参数
测试阶段,Encoder 输入待翻译的句子,Decoder 输入 “起始” token,让它如 1.4.1 节一样自回归地生成序列,直到输出 “结束” token 未知
本节介绍的 “编码器-解码器结构” 是序列学习领域的一个重要模型,后面的 transformer 其实也是基于这个架构设计的
正如上文 1.3.2 节的讨论,RNN 和 LSTM 这种传统序列模型对长跨度序列样本的特征提取能力有限,很容易出现梯度消失等问题,这个问题即使用双向模型也无法完全解决。Attention 是针对这个遗忘问题设计的,以机器翻译任务为例
可见当句子长度超过 20 词时,传统序列模型翻译的 BLEU 评分会迅速下降,增加 attention 机制后问题解决
Attention 机制最早提出于 ICLR 2015 的文章 Neural machine translation by jointly learning to align and translate,这篇文章基于 Encoder-Decoder 结构做机器翻译任务,它的思想很简单,Decoder 输出每个词时不要再只依赖一个隐状态特征向量 s \pmb{s} s,而是去看完整地看一遍要翻译的原句,从原句的所有样本中提取信息汇聚成上下文向量 c \pmb{c} c 作为 Decoder 的附加输入,而所谓 attention,本质就是汇聚上下文向量时的权重
Note: c \pmb{c} c 可以作为附加信息和 s \pmb{s} s 一起作为解码器输入;也可以完全不用 s \pmb{s} s 只依靠 c \pmb{c} c 和输入 token x ′ \pmb{x}' x′ 进行解码,因为 c \pmb{c} c 中已经包含了从来自原始序列所有样本的特征
将这样的 attention 机制加入 RNN/LSTM Encoder-Decoder 结构,如下
相比原始 RNN/LSTM Encoder-Decoder,差别仅在于多了一个上下文向量
c
i
\pmb{c}_i
ci 作为解码的附加信息(下图显示了注意力汇聚过程和附加输入结构,忽略了注意力计算过程)
下面不严谨地歇写一下上下文向量的计算过程
注意力评分函数
a
a
a,计算输入 Decoder 的隐状态
s
j
\pmb{s}_j
sj 和 Encoder 的所有样本输出
h
1
,
.
.
.
,
h
m
\pmb{h}_1,...,\pmb{h}_m
h1,...,hm 的注意力得分
α
1
,
.
.
.
,
α
m
\alpha_1,...,\alpha_m
α1,...,αm,这个得分体现的是
s
j
\pmb{s}_j
sj 和 Encoder 各个输出
h
i
\pmb{h}_i
hi 的相关性上下文向量
,直接将其作为解码
s
\pmb{s}
s 时输入 RNN Decoder 的附加信息即可上述过程总结成一句话就是:利用 s \pmb{s} s 和 h \pmb{h} h 的相关性对 h \pmb{h} h 加权求和得到 c \pmb{c} c。这里有一个问题,直接用原始的 s \pmb{s} s 和 h \pmb{h} h 向量往往不够灵活,比如二者维度可能不同,或者我们想在更高维度计算相关性,又或者想在更高维度汇聚 h \pmb{h} h 中的信息,于是我们可以加一步抽象化
查询向量query
q
\pmb{q}
q键向量key
k
\pmb{k}
k值向量key
v
\pmb{v}
v这里的
Q
,
K
,
V
\pmb{Q},\pmb{K},\pmb{V}
Q,K,V 矩阵都是自己学出来的,在汇聚上下文向量
c
\pmb{c}
c 时,通过
q
,
k
\pmb{q},\pmb{k}
q,k 计算相关性,对
v
\pmb{v}
v 加权求和进行汇聚,如下图所示
注意力评分函数可以有多种设计
加性注意力additive attention
,它允许
q
\pmb{q}
q 和
k
\pmb{k}
k 是不同长度的向量(事实上原始论文中的
q
\pmb{q}
q 和
k
\pmb{k}
k 就是
s
\pmb{s}
s 和
h
\pmb{h}
h 本身),给定
q
∈
R
q
,
k
∈
R
k
\pmb{q}\in\mathbb{R}^q, \pmb{k}\in\mathbb{R}^k
q∈Rq,k∈Rk,注意力得分为缩放点积注意力scaled dot-product attention
,它的计算效率更高,但要求
q
\pmb{q}
q 和
k
\pmb{k}
k 是相同长度的向量。假设查询和键的所有元素都是独立的
d
d
d 维随机变量, 且都是均值0方差1,那么两个向量的点积的均值为0方差为
d
d
d,将点积除以
d
\sqrt{d}
d
使其方差为1,注意力得分为引入 attention 机制,要求模型显式地考虑输出 token 和输入句子各个 token 间的相关性,可以解决传统 Seq2Seq 模型的遗忘问题。通过可视化训练后的 attention 向量,发现机器确实能够学到输入句子和输出句子各个 token 间合理的相关性
但这种能力是有代价的,设输入序列长
m
m
m,输出序列长
t
t
t,对传统模型引入 attention 机制会使计算复杂复从
Q
(
m
+
t
)
Q(m+t)
Q(m+t) 大幅上升到
Q
(
m
×
t
)
Q(m\times t)
Q(m×t)
值得注意的是,这种和传统序列模型结合的 attention 在 transformer 语境中被称为 cross attention,用来强调 attention 计算发生在 Decoder 和 Encoder 之间
值得注意的是,这种和传统序列模型结合的 self-attention 在 transformer 语境中被称为 masked self-attention,它其实只关注了句子中该 token 之前的部分
btw,这个 RL 优化方法总体可以分两阶段
- 用我以前介绍过的 论文理解【IL - IRL】 —— Deep Reinforcement Learning from Human Preferences 这个 “从人类偏好中学习的” IRL 方法学出奖励
- 用 PPO 优化
ChatGPT 很会一本正经地胡说八道,其实也是那个 IRL 方法固有问题的体现
自编码器
,可以数据序列中提取特征;右边是 Decoder,它是一个自回归器
,可以生成目标序列。从整体上看,Transformer 的外部输入输出和过去基于 RNN 等传统序列模型的 Encoder-Decoder 模型完全一致。请看图中数字编号
Positional Encoding:由于 Transformer 模型没有循环神经网络的迭代操作,所以我们必须提供每个字的位置信息给 Transformer,这样它才能识别出句子中的顺序关系。具体而言,我们会对序列中的每个位置(0,1,2…max_length)给出一个和 token embedding 维度相同的 positional embedding 向量,再直接将其和 token embedding 相加
- 有人可能会疑惑为什么两个 embedding 是相加关系,这是因为之前 Input Embedding 的具体操作是先把原始 token 都转换为的 one-hot vector 表示,再用一个矩阵做线性变换到嵌入维度,而原始 token 的 one-hot vector 直接 concat 一个位置的 one-hot vector,再一起做线性变换到嵌入维度,就等价于分别做线性变换再加起来
- 这里位置信息的嵌入向量是用一个公式计算出来的,思想来自数的二进制表示,可以参考 Transformer 修炼之道(一)、Input Embedding。这个计算出来的嵌入不一定是最好的,后面比如 BERT 里就是让网络自己学出来的了
Multi-Head Attention:这个就是一个使用 2.1 节最后介绍的缩放点积注意力评分函数的 self-attention 层,对于序列中的每一个 token,都用它的 query 向量,和此句中所有 token 的 key 向量计算相似度得到 attention,再根据 attention 加权汇聚此句中所有 token 的 value 向量,最终得到这个 token 在这个句子中的一个动态嵌入特征。而所谓 “Multi-Head”,其实就是我们需要用多组不同初始化的 Q , K , V Q,K,V Q,K,V 矩阵算出多个汇聚向量,它们联合起来可以得到更丰富的特性信息,能更好地刻画这个 token 的动态嵌入特征。最后我们把所有这些汇聚向量 concat 连接起来一起用一个矩阵线性变换到 token embedding 维度,得到 token 的动态词嵌入向量
- 下图显示了一个 self-attention 头的计算过程,每个token 都得到了它对应的一个汇聚向量,注意这个汇聚向量的维度可以和 token embedding 不同
- 所谓 “动态嵌入特征”,是指 token 的这个特征受到了它所处句子中其他 token 的影响。举例来说,如果训练材料是描述农业种植的,那么 “苹果” 这个 token 的嵌入特征向量和 “香蕉” 的距离,会小于 “苹果” 和 “手机” 的距离;而如果训练材料是描述电子产品的,则正好相反。这种词向量的动态嵌入能力也是现代序列模型相对传统序列模型的一大优势
- 这里的多头注意力,可以类比 CNN 中同时使用多个滤波器的作用,每个注意力头,只关注一个独立的 “表示子空间”。举例来说,当你浏览网页的时候,你可能在颜色方面更加关注深色的文字,而在字体方面会去注意大的、粗体的文字。这里的颜色和字体就是两个不同的表示子空间。同时关注颜色和字体,可以有效定位到网页中强调的内容。使用多头注意力,也就是综合利用各方面的信息/特征
- 可以用下图来描述整个多头自注意力层的处理过程
残差连接和 Layer Normalization:注意上面多头自注意力层的输出维度和 token embedding 维度一致,所以可以直接残差连接这是一个常见的避免梯度消失的 trick。Layer Normalization 则是一种常见的加速训练的 trick,它对每个样本的不同特征维度做归一化,使得每个样本内的各个特征维度的均值为0,方差为1
Feed Forward & Add norm:最后这个带残差连接的前馈网络是 Poswised 的。注意第 3 步的输出是 n 个 token 的动态特征嵌入向量,维度都是 token embedding 维度,这个 Feed Forward 就是一个输入层和输出层都是 token embedding 维的 MLP,它会被依次接到每一个嵌入向量上,这也可以理解为每个嵌入向量都接了一个维持维度的 MLP,然后所有 MLP 共享参数。它也会做残差连接以及 Layer Normalization 来加速。我理解中这一步就是为了对 token 的动态特征嵌入向量做一个增强
- 下图显示了 cross attention 的计算过程
- 下图显示了宏观连续形式,最后一个 Transformer Encoder Block 的输出被连接到所有 Transformer Decoder Block 上
相比基于传统序列模型的 Encoder-Decoder 模型,Transformer 主要有两大优点
- 需要注意的是,为了实现全并行,Decoder 必须进行 teacher-forcing 形式的训练,而我们最后想让 Decoder 做 autoregress,所以这样学到的模型会有一点偏差,请参考 关于Teacher Forcing 和Exposure Bias的碎碎念。虽然也可以直接让 Decoder 做 autoregress 形式的训练,但是这样就没法并行了
- 另外,训练完做推理时还是串行的,因为 autoregress 本身就是一个串行的行为
Transformer 的缺点在于计算量巨大,由于任意两个 token 之间都要算 attention,一个 self-attention 的复杂度就从 O ( n ) O(n) O(n) 提升到 Q ( n 2 ) Q(n^2) Q(n2),带来两个后果
目前也提出了很多通过稀疏化 attention 来降低复杂度的方法,可以参考 降低Transformer的计算复杂度
回顾第一节的分析,很多序列任务并不是 Seq2Seq 的,比如情感分类这种 many-to-one 任务,又或者文本生成这种 one-to-many 任务,而 Transformer Encoder 作为一个 AutoEncoder 可以做特征提取,适用于前一类任务;Transformer Decoder 作为一个 AutoRegresser 可以做序列生成,适用于后一类任务。直接把二者拆开,就得到了 GPT 和 BERT 模型,如下所示
其中 BERT 和 Transformer Encoder 结构完全一致;GPT 是 Transformer Decoder 去掉 cross-attention 和最后的多分类头之后的部分,这样处理之后,二者结构上的区别仅在于 BERT 使用 self-attention 层,而 GPT 使用 masked self-attention 层。由于模型结构上没有什么新东西,这里就不再过多分析了
虽然模型上看和 Transformer 变化不大,但是他们的训练方式不同了,二者都可以使用自监督方式进行训练,这就意味着不需要标好的数据,具体而言
通过自监督学习,可以无成本地获取大量带标签数据,能完美满足这类超大容量的 Transformer 模型对数据复杂度的极高要求。在 Transformer 出现之前,NLP 领域由于缺乏类似 ImageNet 这样的大型带标记数据集,几何没法用 CV 领域成熟的 “pre-train + fine-turn” 训练模式,而随着 BERT 和 GPT 的诞生,有了这种自监督方案之后,这条路线就打通了,这促成了 NLP 领域近年来的爆发式突破
事实上,由于使用自监督方案之后训练成本几乎仅限于算力,我们可以不断增大模型容量,训练巨量参数组成的超大网络,这就是 GPT / GPT2 / GPT3 / GPT3.5 / ChatGPT 这一路主要在做的事情(CPT3.5 改用 RL 训练了,可以参考这个)。更恐怖的是,目前没有看到这种方式的有性能饱和的趋势,只要不断增大模型,增大数据量和算力,学出模型的性能几乎可以无限提升,这也带动了一般 “大模型” 的浪潮。如果对模型容量和性能感兴趣,可以参考 从模型容量的视角看监督学习
另外,因为各种监督学习问题大都能转换为序列预测问题,而 self-attention 的约束足够弱,表示能力足够强,近年来我们看到 transformer 在各个领域疯狂乱杀,最近我在关注如何把这些结构用到 Offline-RL 中
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。