BERT 在自然语言处理(NLP)领域刷新了 11 个任务的记录,万众瞩目,成为异常火热的一个预训练语言模型。 今天我们就来详细讲解下 BERT 这个预训练语言模型,虽然 BERT 刷新了各个任务的记录,但是 BERT 并不是一个具有极大创新的算法,更多的是一个集大成者,把 BERT 之前各个语言模型的优点集于一身,并作出了适当的改进,而拥有了如今无与伦比的能力。
本篇文章共 15326 个词,一个字一个字手码的不容易,转载请标明出处:BERT 模型详解 - 二十三岁的有德
目录
- 一、BERT 引入
- 二、图像领域的预训练
- 三、词向量 Word Embedding
- 3.1 One-hot 编码
- 3.2 神经网络语言模型 - 词向量的起源
- 四、Word2Vec 模型
- 五、Attention 机制
- 六、Self-Attention 模型
- 6.1 Self-Attention 的向量运算
- 6.2 Self-Attention 的优点
- 6.3 Self-Attention 的矩阵运算
- 6.4 Masked Self Attention 模型
- 6.5 Multi-head Self Attention 模型
- 七、Position Embedding
- 八、Transformer
- 8.1 Transformer 的结构
- 8.2 Encoder
- 8.3 Decoder
- 8.4 Transformer 输出结果
- 九、Transformer 动态流程展示
- 9.1 为什么 Decoder 需要做 Mask
- 9.2 为什么 Encoder 给予 Decoders 的是 K、V 矩阵
- 十、BERT 模型
- 10.1 BERT:公认的里程碑
- 10.2 BERT 的结构:强大的特征提取能力
- 10.3 BERT 之无监督训练
- 10.4 BERT 之语言掩码模型(MLM)
- 10.5 BERT 之下句预测(NSP)
- 10.6 BERT 之输入表示
- 十一、BERT 下游任务改造
- 11.1 句对分类
- 11.2 单句分类
- 11.3 文本问答
- 11.4 单句标注
- 11.5 BERT效果展示
- 十二、BERT 总结
- 十三、参考资料
BERT 在自然语言处理(NLP)领域刷新了 11 个任务的记录,万众瞩目,成为异常火热的一个预训练语言模型。
今天我们就来详细讲解下 BERT 这个预训练语言模型,虽然 BERT 刷新了各个任务的记录,但是 BERT 并不是一个具有极大创新的算法,更多的是一个集大成者,把 BERT 之前各个语言模型的优点集于一身,并作出了适当的改进,而拥有了如今无与伦比的能力。
BERT 既然是集大成者,那么集了哪些大成呢?
- BERT 作为一个预训练语言模型,它的预训练思想借鉴了图像领域中的预训练的思想;
- 作者说的是借鉴了完形填空任务的思想(双向编码),但我感觉应该也借鉴了 Word2Vec 的 CBOW 的思想,因为两者本质上是相同的;
- 没有使用传统的类 RNN 模型作为特征提取器,而是使用了最近火热的 Transformer 作为特征提取器,而 Transformer 又使用了 Attention 机制作为特征提取器,更是火上浇油;
- 真要说创新,也许就是在 CBOW 的思想之上,添加了语言掩码模型(MLM),减少了训练阶段和推理阶段(测试阶段)的不匹配,避免了过拟合;
- 由于单词预测粒度的训练到不了句子关系这个层级,为了学会捕捉句子之间的语义联系,BERT 采用了下句预测(NSP )作为无监督预训练的一部分,这也算是一个小小的创新吧。
既然是集大成者,那么我们只要把上述五点大成一一解释清楚,相信再带你看 BERT 时,你一定会豁然开朗:“BERT 不过如此”。
在叙述图像领域的预训练之前,我们通过下图来看看 CNN 是如何对图片做特征提取的:一张图片放入到 CNN 中,由浅层到深层
- 浅层:浅层提取的特征对于 “人脸、汽车、大象、椅子” 这四个任务而言,非常的相似,都是 “横竖撇捺” 之类的
- 深层:深层提取的特征对于 “人脸、汽车、大象、椅子” 这四个任务而言,都是接近于任务本身,例如 “人脸” 任务的 CNN 深层提取的特征更接近于人脸的轮廓
通过上述分析,也就是说,CNN 浅层提取的特征对于各个任务而言都是通用的。
图 1 - CNN 提取图片特征
聪明的读者应该想到了,什么是 “预训练”,但很懵懵懂懂,下面我们来详细解释。
图 2 - 预训练在图像领域的应用
假设我们拥有两个尽量相似的任务 A 和 B(可以都为图像处理任务),任务 A 是我们目标任务,任务 B 是可以提前训练好的一个任务:
- 我们训练任务 B 得到一个 CNN 模型 B
- 由于 CNN 浅层特征的通用性,我们可以做如下两种处理:
- fine-tuning(微调):浅层参数使用模型 B 的参数,高层参数随机初始化,利用任务 A 训练模型 B,浅层参数随着变化
- frozen(冻结):浅层参数使用模型 B 的参数,高层参数随机初始化,利用任务 A 训练模型 B,浅层参数一直不变
上述两个步骤即是预训练的思想,那么预训练有什么好处呢?
- 如果我们还有相似任务 C,可以利用模型 B 的参数直接启动任务的训练,进而可以加快任务 C 训练的收敛速度
- 如果我们还有一个相似任务但数据量很小的任务 D,由于数据量小的任务很难训练出一个深度神经网络模型,然而利用预训练的思想,小数据量的任务也可以利用深度神经网络模型解决
在讲解 CBOW 思想之前,我们不得不讲解下词向量的发展史,这算是预训练语言模型的始祖,语言模型的预训练的思想最早来源于此,并且 CBOW 模型就是为了生成单词的词向量应运而生。
One-hot 编码相信大家都很熟悉了,一个很简单的小知识,让计算机认识现实世界中的单词。
由于计算机并不认识单词 “time、fruit……”,为此,人们构建一个词典 (V) ,如下图所示词典包含 8 个单词,对于词典中的每个单词,都有之对应的 (|V|)(词典大小的)维度的向量表示,如下图的 “time” 对应的向量为 8 维的 “1000 0000”。
图 3 - 词的 One-hot 编码
One-hot 编码虽然解决了计算机不认识单词的这一缺陷,但是 One-hot 编码本身也是有问题的,对于上图 “fruit” 和 “banana” 的 One-hot 编码,可以通过余弦相似度计算两者的相似性,可以发现结果为 0,也就是两者毫不相关,然而实际上两者是相关的,由此,词向量应运而生。
图 4 - 神经网络语言模型架构
现在让我们看看词向量到底是个什么玩意儿,对于上述的 3 层神经网络语言模型(NNLM),它的学习任务是输入某个句中单词 (w_t = banana) 前的 (t-1) 个单词,要求网络正确预测单词 “banana”,即最大化:
[P(w_t=bert|w_1,w_2,cdots,w_{t-1}; heta) ]
其中 ( heta) 是 NNLM 的参数。
首先输入前 (t-1) 个单词的 One-hot 编码
- 单词的 One-hot 编码经过第一层,输入层,One-hot 编码左乘矩阵 Q 得到词向量 (C(w_i))
- 矩阵 Q 是一个随机初始化的参数矩阵
- 词向量 (C(w_i)) 经过第二层,隐层(相当于全连接层),隐层的输出为 ( anh(Wx+d)),其中 (x) 为输入的词向量,(W) 为权重矩阵,(d) 为偏置,( anh) 为激活函数。
- 假设词典大小为 8,输出为一个 8 维的向量 [11, 22, 55, 44, 87, 84, 88]
- 第二层输出的向量经过第三层,输出层,经过 Softmax 函数归一化。
- 假设输入为一个输出为一个 8 维的向量 [11, 22, 55, 44, 87, 84, 88],则该层输出为 [0.000, 0.000, 0.000, 0.000, 0.265, 0.013, 0.721],对应图三,则该层输出的对应的词为 “banana”
在讲解 NNLM 的过程中,可以看到该模型的一个副产品词向量 (C(w_i)),这就是大名鼎鼎的词向量。
这个词向量是如何解决 One-hot 编码对于本应该相似的单词而不相似的问题的呢?假设矩阵 Q 己经被成功训练,下面我们来计算 “banana” 的词向量:
[begin{bmatrix} 0&0&0&0&0&0&0&1 end{bmatrix} begin{bmatrix} 17&24&1\ 23&5&7\ 4&6&13\ 10&12&19\ 11&18&25 \ 23&36&24 \ 5&34&23 \ 7&6&18 end{bmatrix} = begin{bmatrix} 7&6&18 end{bmatrix} ]
同理 “fruit” 的词向量为 [23,5,7],两者的余弦相似度为 “0.638”,可以发现词向量顺利的解决了词相似的问题。
上过构建词向量的过程被称作 Word Embedding,而且细心地读者应该发现训练矩阵 Q 并得到词向量的过程其实就是一个预训练的过程,对于未来的 NLP 的任务,直接使用训练得到的矩阵 Q 和词向量即可。
下图给出网上找的几个例子,可以看出有些效果还是蛮不错的:
图 5 - Word Embedding 例子
NNLM 的学习任务是输入某个句中单词 (w_t = banana) 前的 (t-1) 个单词,要求网络正确预测单词 “banana”,矩阵 Q 和词向量都只是它的副产品。
为此,2013 年出现了一个最火的用语言模型专门做 Word Embedding 的工具 Word2Vec ,它的架构如下图所示:
图 6 - Word2Vec 模型
由于 Word2Vec 专门用来做 Word Embedding,它不再是利用前 (t-1) 个单词,而是利用了上下文信息,这个也是很好理解的,对于一个单词的解释,利用上下文的信息作出的解释会更合理。
其实 Word2Vec 和 NNLM 非常类似,都是三层架构,除了利用了上下文信息,Word2Vec 还提供了 2 种训练方法:
- 第一种叫 CBOW,核心思想是从一个句子里面把一个词抠掉,用这个词的上文和下文去预测被抠掉的这个词;
- 第二种叫做 Skip-gram,和 CBOW 正好反过来,输入某个单词,要求网络预测它的上下文单词。
当你看到 CBOW 的时候,想必你也想到了为什么要在这里讲讲 Word2Vec 的原因,因为未来的 BERT 将会用到 CBOW 的思想:利用单词 (w_t) 上下文的信息,预测单词 (w_t)
Attention 机制来源于人类的视觉注意力机制:人类视觉通过快速扫描全局图像,获得需要重点关注的目标区域,也就是一般所说的注意力焦点,而后对这一区域投入更多注意力资源,以获取更多所需要关注目标的细节信息,而抑制其他无用信息。
对于下图,如果只关心颜色,我们可能第一眼就会把重心放在显眼的红色上,然后再观察其他地方。
图 7 - 婴儿图像
通过对人类视觉注意力机制的描述,Attention 机制简单点讲就是:通过一个 Query(查询变量)从一堆信息(Key-Value 表示的信息,可以把 Key 看作信息的地址,Value 表示信息的内容)中找到对于 Query 而言重要的信息,以此获取更多对于 Query 而言更重要的细节信息,进而抑制其他无用信息。
Attention 的具体做法如下图所示:
图 8 - Attention 计算图
- 计算 Q 和 K 的相似度,用 f 来表示:(f(Q,K_i)quad i=1,2,cdots,m),Self-Attention 模型中 (f(Q,K_i) = Q^TK_i)
- 第二步:将得到的相似度进行 softmax 操作,进行归一化:(alpha_i = softmax(frac{f(Q,K_i)}{sqrt d_k}))
- 这里简单讲解除以 (sqrt d_k) 的作用:假设 (Q) , (K) 里的元素的均值为0,方差为 1,那么 (A^T=Q^TK) 中元素的均值为 0,方差为 d。当 d 变得很大时, (A) 中的元素的方差也会变得很大,如果 (A) 中的元素方差很大(分布的方差大,分布集中在绝对值大的区域),在数量级较大时, softmax 将几乎全部的概率分布都分配给了最大值对应的标签,由于某一维度的数量级较大,进而会导致 softmax 未来求梯度时会消失。总结一下就是 (operatorname{softmax}left(A ight)) 的分布会和d有关。因此 (A) 中每一个元素乘上 (frac{1}{sqrt{d_k}}) 后,方差又变为 1,并且 (A) 的数量级也将会变小。
- 针对计算出来的权重 (alpha_i),对 (V) 中的所有 values 进行加权求和计算,得到 Attention 向量:(Attention = sum_{i=1}^m alpha_i V_i)
Self-Attention 模型是 Attention 机制的具体应用,下面我们给出 Self-Attention 的架构图:
图 9 - Self-Attention 模型架构图
通过上图可以看到 Self Attention 有三个输入 Q、K、V:对于 Self Attention,Q、K、V 来自输入句子 X 的 词向量 x 的线性转化,即对于词向量 x,给定三个可学习的矩阵参数 (W_Q,W_k,W_v),x 分别右乘上述矩阵得到 Q、K、V。这也是Self-Attention 名字的来源:Q、K、V 三个矩阵由同一个词向量线性转化而得。
下面我们来详细了解下 Self-Attention 的具体计算流程:
- 第一步,Q、K、V 的获取、
图 10 - Q、K、V 矩阵的获取
上图操作:两个单词 Thinking 和 Machines。通过线性变换,即 (x_i) 和 (x_2) 两个向量分别与(W_q,W_k,W_v) 三个矩阵点乘得到 ${q_1,q_2},{k_1,k_2},{v_1,v_2} $ 共 6 个向量。矩阵 Q 则是向量 (q_1,q_2) 的拼接,K、V 同理。
- 第二步,MatMul
图 11 - (q_1) 和 (k1、k_2) 的计算
上图操作:向量 ({q_1,k_1}) 做点乘得到得分 112, ({q_1,k_2}) 做点乘得到得分96。注意:这里是通过 (q_1) 这个信息找到 (x_1,x_2) 中的重要信息。
- 第三步和第四步,Scale + Softmax图 12 - (q_1) 和 (k1、k_2) 的计算做 Softmax上图操作:对该得分进行规范,除以 (sqrt {d_k} = 8)
- 第五步,MatMul图 13 - (z_1) 的计算
用得分比例 [0.88,0.12] 乘以 ([v_1,v_2]) 值得到一个加权后的值,将这些值加起来得到 (z_1)。
上述所说就是 Self Attention 模型所做的事,仔细感受一下,用 (q_1)、(K=[k_1,k_2]) 去计算一个 Thinking 相对于 Thinking 和 Machine 的权重,再用权重乘以 Thinking 和 Machine 的 (V=[v_1,v_2]) 得到加权后的 Thinking 和 Machine 的 (V=[v_1,v_2]),最后求和得到针对各单词的输出 (z_1)。
同理可以计算出 Machine 相对于 Thinking 和 Machine 的加权输出 (z_2)。
上述所计算的结果 (z_1) 就可以看作是查询变量 Thinking 的词向量,并且是 Thinking 从 Thinking Machine 这句话中找到了对于 Thinking 而言更为重要的信息,并且抑制了不重要的信息。这样讲,可能无法理解 Self-Attention 机制的好处,我们可以通过下图来了解 Self-Attention 机制的优点到底在哪里:
图 14 - attention 获取句法特征
图 15 - attention 获取语义特征
从图 14 可以看出,Self-Attention 捕获了同一个句子中单词之间的一些句法特征(有一定距离的短语结构);从图 15 可以看出,Self-Attention 捕获了同一个句子中单词之间的语义特征(“its” 的指代对象 “Law”)。
并且从上面的计算步骤和图片可以看出,无论句子序列多长,都可以很好的提取句子特征,并且既可以提取句法特征还可以提取语义特征,这很好的解决了 RNN 序列长距离依赖的问题,而且对于一个句子而言,每个单词的计算是可以并行处理的。
图 16 - RNN 结构图
上述例子是 Self-Attention 单个向量运算的例子。下图展示的是 Self-Attention 的矩阵运算的例子,输入是一个 [2x4] 的矩阵(句子中每个单词的词向量的拼接),每个运算是 [4x3] 的矩阵,求得 Q、K、V。
Q 对 K 转制做点乘,除以 (sqrt d_k),做一个 softmax 得到合为 1 的比例,对 V 做点乘得到输出 Z。那么这个 Z 就是一个考虑过 Thinking 周围单词 Machine 的输出。
注意看这个公式,(QK^T) 其实就会组成一个 word2word 的 attention map!(加了 softmax 之后就是一个合为 1 的权重了)。比如说你的输入是一句话 "i have a dream" 总共 4 个单词,这里就会形成一张 4x4 的注意力机制的图:
这样一来,每一个单词对应每一个单词都会有一个权重,这也是 Self Attention 名字的来源,即 Attention 的计算来源于 Source(源句) 和 Source 本身,通俗点讲就是 Q、K、V 都来源于输入 X 本身。
趁热打铁,我们讲讲 Transformer 未来会用到的 Masked Self Attention 模型,这里的 Masked 就是要在做语言模型(或者像翻译)的时候,不给模型看到未来的信息,它的结构如下图所示:
上图中和 Self Attention 重复的部分此处就不讲了,主要讲讲 Mask 这一块。
假设在此之前我们已经通过 scale 之前的步骤得到了一个 attention map,而 mask 就是沿着对角线把灰色的区域用0覆盖掉,不给模型看到未来的信息,如下图所示:
详细来说:
- "i" 作为第一个单词,只能有和 "i" 自己的 attention;
- "have" 作为第二个单词,有和 "i、have" 前面两个单词的 attention;
- "a" 作为第三个单词,有和 "i、have、a" 前面三个单词的 attention;
- "dream" 作为最后一个单词,才有对整个句子 4 个单词的 attention。
并且在做完 softmax 之后,横轴结果合为 1。如下图所示:
具体为什么 在 Transformer 中要用到 Masked Self Attention,未来在讲解 Transformer 的时候会详细解释。
由于 Transformer 使用的都是 Self Attention 的进阶版 Multi-head Self Attention,我们简单讲讲 Multi-head Self Attention 的架构,并且在该小节结尾处讲讲它的优点。
Multi-Head Attention 就是把 Self Attention的过程做 H 次,然后把输出 Z 合起来。论文中,它的结构图如下:
我们还是以上面的形式来解释,首先,我们使用 8 组不同的 (W_Q^i,W_k^i,W_V^iquad i=1,2,cdots,8) ,重复 8 次和 Self Attention 相似的操作,得到 8 个 (Z_i) 矩阵:
为了使得输出与输入结构相同,拼接矩阵 (Z_i) 后乘以一个线性 (W_0) 得到最终的Z:
看完了 Multi-head Self Attention 的架构,发现它与 Self Attention 的区别,在于用了 (n) 组 (W_Q^i,W_k^i,W_V^iquad i=1,2,cdots,n) 得到 (n) 组 (Q_i,K_i,V_i quad i=1,2,cdots,n)。
可以通过下图看看 multi-head attention 的整个流程:
上述操作有什么好处呢?使用多套参数,多套参数相当于把原始信息 Source 放入了多个子空间中,也就是捕捉了多个信息,对于使用 multi-head(多头) attention 的简单回答就是,多头保证了 attention 可以注意到不同子空间的信息,捕捉到更加丰富的特征信息。其实本质上是论文原作者发现这样效果确实好。
在 Attention 和 RNN 的对比中,我们说到 Attention 解决了长距离依赖问题,并且可以支持并行化,但是它就真的百利而无一害了吗?
其实不然,我们往前回顾,Self Attention 的 Q、K、V 三个矩阵是由同一个输入 (X_1=(x_1,x_2,cdots,x_n)) 线性转换而来,也就是说对于这样的一个被打乱序列顺序的 (X_2=(x_2,x_1,cdots,x_n)) 而言,由于 Attention 值的计算最终会被加权求和,也就是说两者最终计算的 Attention 值都是一样的,进而也就表明了 Attention 丢掉了 (X_1) 的序列顺序信息。
如上图所示,为了解决 Attention 丢失的序列顺序信息,Transformer 的提出者提出了 Position Embedding,也就是对于输入 (X) 进行 Attention 计算之前,在 (X) 的词向量中加上位置信息,也就是说 (X) 的词向量为 (X_{final_embedding} = Embedding + Positional, Embedding)
但是如何得到 (X) 的位置向量呢?
其中位置编码公式如下图所示:
其中 pos 表示位置、i 表示维度、(d_{model})表示位置向量的向量维度 、(2i、2i+1) 表示的是奇偶数(奇偶维度),上图所示就是偶数位置使用 (sin) 函数,奇数位置使用 (cos) 函数。
有了位置编码,我们再来看看位置编码是如何嵌入单词编码的(其中 512 表示编码维度),通过把单词的词向量和位置向量进行叠加,这种方式就称作位置嵌入,如下图所示:
Position Embedding 本身是一个绝对位置的信息,但在语言模型中,相对位置也很重要。那么为什么位置嵌入机制有用呢?
我们不要去关心三角函数公式,可以看看下图公式(3)中的第一行,我们做如下的解释,对于 “我爱吃苹果” 这一句话,有 5 个单词,假设序号分别为 1、2、3、4、5。
假设 (pos=1=我、k=2=爱、pos+k=3=吃),也就是说 (pos+k=3) 位置的位置向量的某一维可以通过 (pos=1) 位置的位置向量的某一维线性组合加以线性表示,通过该线性表示可以得出 “吃” 的位置编码信息蕴含了相对于前两个字 “我” 的位置编码信息。
总而言之就是,某个单词的位置信息是其他单词位置信息的线性组合,这种线性组合就意味着位置向量中蕴含了相对位置信息。
万事俱备,只欠东风,下面我们来讲讲我们的重点之一,Transformer,你可以先记住这一句话:Transformer 简单点看其实就是 self-attention 模型的叠加,首先我们来看看 Transformer 的整体框架。
Transformer 的整体框架如下图所示:
上图所示的整体框架乍一眼一看非常复杂,由于 Transformer 起初是作为翻译模型,因此我们以翻译举例,简化一下上述的整体框架:
从上图可以看出 Transformer 相当于一个黑箱,左边输入 “Je suis etudiant”,右边会得到一个翻译结果 “I am a student”。
再往细里讲,Transformer 也是一个 Seq2Seq 模型(Encoder-Decoder 框架的模型),左边一个 Encoders 把输入读进去,右边一个 Decoders 得到输出,如下所示:
在这里,我们穿插描述下 Encoder-Decoder 框架的模型是如何进行文本翻译的:
- 将序列 ((x_1,x_2,cdots,x_n)) 作为 Encoders 的输入,得到输出序列 ((z_1,z_2,cdots,z_n))
- 把 Encoders 的输出序列 ((z_1,z_2,cdots,z_n)) 作为 Decoders 的输入,生成一个输出序列 ((y_1,y_2,cdots,y_m))。注:Decoders 每个时刻输出一个结果
第一眼看到上述的 Encodes-Decoders 框架图,随之产生问题就是 Transformer 中 左边 Encoders 的输出是怎么和右边 Decoders 结合的。因为decoders 里面是有N层的,再画张图直观的看就是这样:
也就是说,Encoders 的输出,会和每一层的 Decoder 进行结合。
现在我们取其中一层进行详细的展示:
通过上述分析,发现我们想要详细了解 Transformer,只要了解 Transformer 中的 Encoder 和 Decoder 单元即可,接下来我们将详细阐述这两个单元。
有了上述那么多知识的铺垫,我们知道 Eecoders 是 N=6 层,通过上图我们可以看到每层 Encoder 包括两个 sub-layers:
- 第一个 sub-layer 是 multi-head self-attention,用来计算输入的 self-attention;
- 第二个 sub-layer 是简单的前馈神经网络层 Feed Forward;
注意:在每个 sub-layer 我们都模拟了残差网络(在下面的数据流示意图中会细讲),每个sub-layer的输出都是 (LayerNorm(x+Sub_layer(x))),其中 (sub_layer) 表示的是该层的上一层的输出
现在我们给出 Encoder 的数据流示意图,一步一步去剖析
- 深绿色的 (x_1) 表示 Embedding 层的输出,加上代表 Positional Embedding 的向量之后,得到最后输入 Encoder 中的特征向量,也就是浅绿色向量 (x_1);
- 浅绿色向量 (x_1) 表示单词 “Thinking” 的特征向量,其中 (x_1) 经过 Self-Attention 层,变成浅粉色向量 (z_1);
- (x_1) 作为残差结构的直连向量,直接和 (z_1) 相加,之后进行 Layer Norm 操作,得到粉色向量 (z_1);
- 残差结构的作用:避免出现梯度消失的情况
- Layer Norm 的作用:为了保证数据特征分布的稳定性,并且可以加速模型的收敛
- (z_1) 经过前馈神经网络(Feed Forward)层,经过残差结构与自身相加,之后经过 LN 层,得到一个输出向量 (r_1);
- 该前馈神经网络包括两个线性变换和一个ReLU激活函数:(FFN(x) = max(0,xW_1+b_1)W_2+b2)
- 由于 Transformer 的 Encoders 具有 6 个 Encoder,(r_1) 也将会作为下一层 Encoder 的输入,代替 (x_1) 的角色,如此循环,直至最后一层 Encoder。
需要注意的是,上述的 (x、z、r) 都具有相同的维数,论文中为 512 维。
Decoders 也是 N=6 层,通过上图我们可以看到每层 Decoder 包括 3 个 sub-layers:
- 第一个 sub-layer 是 Masked multi-head self-attention,也是计算输入的 self-attention;
- 在这里,先不解释为什么要做 Masked,后面在 “Transformer 动态流程展示” 这一小节会解释
- 第二个 sub-layer 是 Encoder-Decoder Attention 计算,对 Encoder 的输入和 Decoder 的Masked multi-head self-attention 的输出进行 attention 计算;
- 在这里,同样不解释为什么要对 Encoder 和 Decoder 的输出一同做 attention 计算,后面在 “Transformer 动态流程展示” 这一小节会解释
- 第三个 sub-layer 是前馈神经网络层,与 Encoder 相同。
以上,就讲完了 Transformer 编码和解码两大模块,那么我们回归最初的问题,将 “机器学习” 翻译成 “machine learing”,解码器的输出是一个浮点型的向量,怎么转化成 “machine learing” 这两个词呢?让我们来看看 Encoders 和 Decoders 交互的过程寻找答案:
从上图可以看出,Transformer 最后的工作是让解码器的输出通过线性层 Linear 后接上一个 softmax
- 其中线性层是一个简单的全连接神经网络,它将解码器产生的向量 A 投影到一个更高维度的向量 B 上,假设我们模型的词汇表是10000个词,那么向量 B 就有10000个维度,每个维度对应一个惟一的词的得分。
- 之后的softmax层将这些分数转换为概率。选择概率最大的维度,并对应地生成与之关联的单词作为此时间步的输出就是最终的输出啦!
假设词汇表维度是 6,那么输出最大概率词汇的过程如下:
首先我们来看看拿 Transformer 作翻译时,如何生成翻译结果的:
继续进行:
假设上图是训练模型的某一个阶段,我们来结合 Transformer 的完整框架描述下这个动态的流程图:
- 输入 “je suis etudiant” 到 Encoders,然后得到一个 (K_e)、(V_e) 矩阵;
- 输入 “I am a student” 到 Decoders ,首先通过 Masked Multi-head Attention 层得到 “I am a student” 的 attention 值 (Q_d),然后用 attention 值 (Q_d) 和 Encoders 的输出 (K_e)、(V_e) 矩阵进行 attention 计算,得到第 1 个输出 “I”;
- 输入 “I am a student” 到 Decoders ,首先通过 Masked Multi-head Attention 层得到 “I am a student” 的 attention 值 (Q_d),然后用 attention 值 (Q_d) 和 Encoders 的输出 (K_e)、(V_e) 矩阵进行 attention 计算,得到第 2 个输出 “am”;
- ……
现在我们来解释我们之前遗留的两个问题。
- 训练阶段:我们知道 “je suis etudiant” 的翻译结果为 “I am a student”,我们把 “I am a student” 的 Embedding 输入到 Decoders 里面,翻译第一个词 “I” 时
- 如果对 “I am a student” attention 计算不做 mask,“am,a,student” 对 “I” 的翻译将会有一定的贡献
- 如果对 “I am a student” attention 计算做 mask,“am,a,student” 对 “I” 的翻译将没有贡献
- 测试阶段:我们不知道 “我爱中国” 的翻译结果为 “I love China”,我们只能随机初始化一个 Embedding 输入到 Decoders 里面,翻译第一个词 “I” 时:
- 无论是否做 mask,“love,China” 对 “I” 的翻译都不会产生贡献
- 但是翻译了第一个词 “I” 后,随机初始化的 Embedding 有了 “I” 的 Embedding,也就是说在翻译第二词 “love” 的时候,“I” 的 Embedding 将有一定的贡献,但是 “China” 对 “love” 的翻译毫无贡献,随之翻译的进行,已经翻译的结果将会对下一个要翻译的词都会有一定的贡献,这就和做了 mask 的训练阶段做到了一种匹配
总结下就是:Decoder 做 Mask,是为了让训练阶段和测试阶段行为一致,不会出现间隙,避免过拟合
我们在讲解 Attention 机制中曾提到,Query 的目的是借助它从一堆信息中找到重要的信息。
现在 Encoder 提供了 (K_e、V_e) 矩阵,Decoder 提供了 (Q_d) 矩阵,通过 “我爱中国” 翻译为 “I love China” 这句话详细解释下。
当我们翻译 “I” 的时候,由于 Decoder 提供了 (Q_d) 矩阵,通过与 (K_e、V_e) 矩阵的计算,它可以在 “我爱中国” 这四个字中找到对 “I” 翻译最有用的单词是哪几个,并以此为依据翻译出 “I” 这个单词,这就很好的体现了注意力机制想要达到的目的,把焦点放在对自己而言更为重要的信息上。
- 其实上述说的就是 Attention 里的 soft attention机制,解决了曾经的 Encoder-Decoder 框架的一个问题,在这里不多做叙述,有兴趣的可以参考网上的一些资料。
- 早期的 Encoder-Decoder 框架中的 Encoder 通过 LSTM 提取出源句(Source) “我爱中国” 的特征信息 C,然后 Decoder 做翻译的时候,目标句(Target)“I love China” 中的任何一个单词的翻译都来源于相同特征信息 C,这种做法是极其不合理的,例如翻译 “I” 时应该着眼于 “我”,翻译 “China” 应该着眼于 “中国”,而早期的这种做法并没有体现出,然而 Transformer 却通过 Attention 的做法解决了这个问题。
BERT 模型可以作为公认的里程碑式的模型,但是它最大的优点不是创新,而是集大成者,并且这个集大成者有了各项突破,下面让我们看看 BERT 是怎么集大成者的。
- BERT 的意义在于:从大量无标记数据集中训练得到的深度模型,可以显著提高各项自然语言处理任务的准确率。
- 近年来优秀预训练语言模型的集大成者:参考了 ELMO 模型的双向编码思想、借鉴了 GPT 用 Transformer 作为特征提取器的思路、采用了 word2vec 所使用的 CBOW 方法
- BERT 和 GPT 之间的区别:
- GPT:GPT 使用 Transformer Decoder 作为特征提取器、具有良好的文本生成能力,然而当前词的语义只能由其前序词决定,并且在语义理解上不足
- BERT:使用了 Transformer Encoder 作为特征提取器,并使用了与其配套的掩码训练方法。虽然使用双向编码让 BERT 不再具有文本生成能力,但是 BERT 的语义信息提取能力更强
- 单向编码和双向编码的差异,以该句话举例 “今天天气很{},我们不得不取消户外运动”,分别从单向编码和双向编码的角度去考虑 {} 中应该填什么词:
- 单向编码:单向编码只会考虑 “今天天气很”,以人类的经验,大概率会从 “好”、“不错”、“差”、“糟糕” 这几个词中选择,这些词可以被划为截然不同的两类
- 双向编码:双向编码会同时考虑上下文的信息,即除了会考虑 “今天天气很” 这五个字,还会考虑 “我们不得不取消户外运动” 来帮助模型判断,则大概率会从 “差”、“糟糕” 这一类词中选择
- 如下图所示,我们来看看 ELMo、GPT 和 BERT 三者的区别
- ELMo 使用自左向右编码和自右向左编码的两个 LSTM 网络,分别以 (P(w_i|w_1,cdots,w_{i-1})) 和 (P(w_i|w_{i+1},cdots,w_n)) 为目标函数独立训练,将训练得到的特征向量以拼接的形式实现双向编码,本质上还是单向编码,只不过是两个方向上的单向编码的拼接而成的双向编码。
- GPT 使用 Transformer Decoder 作为 Transformer Block,以 (P(w_i|w_1,cdots,w_{i-1})) 为目标函数进行训练,用 Transformer Block 取代 LSTM 作为特征提取器,实现了单向编码,是一个标准的预训练语言模型,即使用 Fine-Tuning 模式解决下游任务。
- BERT 也是一个标准的预训练语言模型,它以 (P(w_i|w_1,cdots,w_{i-1},w_{i+1},cdots,w_n)) 为目标函数进行训练,BERT 使用的编码器属于双向编码器。
- BERT 和 ELMo 的区别在于使用 Transformer Block 作为特征提取器,加强了语义特征提取的能力;
- BERT 和 GPT 的区别在于使用 Transformer Encoder 作为 Transformer Block,并且将 GPT 的单向编码改成双向编码,也就是说 BERT 舍弃了文本生成能力,换来了更强的语义理解能力。
BERT 的模型结构如下图所示:
从上图可以发现,BERT 的模型结构其实就是 Transformer Encoder 模块的堆叠。在模型参数选择上,论文给出了两套大小不一致的模型。
(BERT_{base}) :L = 12,H = 768,A = 12,总参数量为 1.1 亿
(BERT_{LARGE}):L = 24,H = 1024,A = 16,总参数量为 3.4 亿
其中 L 代表 Transformer Block 的层数;H 代表特征向量的维数(此处默认 Feed Forward 层中的中间隐层的维数为 4H);A 表示 Self-Attention 的头数,使用这三个参数基本可以定义 BERT的量级。
BERT 参数量级的计算公式:
[begin{align*} & 词向量参数+ 12 * (Multi-Heads参数 + 全连接层参数 + layernorm参数)\ & = (30522+512 + 2)* 768 + 768 * 2 \ & + 12 * (768 * 768 / 12 * 3 * 12 + 768 * 768 + 768 * 3072 * 2 + 768 * 2 * 2) \ & = 108808704.0 \ & approx 110M end{align*} ]
训练过程也是很花费计算资源和时间的,总之表示膜拜,普通人即便有 idea 没有算力也只能跪着。
BERT 采用二段式训练方法:
- 第一阶段:使用易获取的大规模无标签余料,来训练基础语言模型;
- 第二阶段:根据指定任务的少量带标签训练数据进行微调训练。
不同于 GPT 等标准语言模型使用 (P(w_i|w_1,cdots,w_{i-1})) 为目标函数进行训练,能看到全局信息的 BERT 使用 (P(w_i|w_1,cdots,w_{i-1},w_{i+1},cdots,w_n)) 为目标函数进行训练。
并且 BERT 用语言掩码模型(MLM)方法训练词的语义理解能力;用下句预测(NSP)方法训练句子之间的理解能力,从而更好地支持下游任务。
BERT 作者认为,使用自左向右编码和自右向左编码的单向编码器拼接而成的双向编码器,在性能、参数规模和效率等方面,都不如直接使用深度双向编码器强大,这也是为什么 BERT 使用 Transformer Encoder 作为特征提取器,而不使用自左向右编码和自右向左编码的两个 Transformer Decoder作为特征提取器的原因。
由于无法使用标准语言模型的训练模式,BERT 借鉴完形填空任务和 CBOW 的思想,使用语言掩码模型(MLM )方法训练模型。
MLM 方法也就是随机去掉句子中的部分 token(单词),然后模型来预测被去掉的 token 是什么。这样实际上已经不是传统的神经网络语言模型(类似于生成模型)了,而是单纯作为分类问题,根据这个时刻的 hidden state 来预测这个时刻的 token 应该是什么,而不是预测下一个时刻的词的概率分布了。
随机去掉的 token 被称作掩码词,在训练中,掩码词将以 15% 的概率被替换成 [MASK],也就是说随机 mask 语料中 15% 的 token,这个操作则称为掩码操作。注意:在CBOW 模型中,每个词都会被预测一遍。
但是这样设计 MLM 的训练方法会引入弊端:在模型微调训练阶段或模型推理(测试)阶段,输入的文本中将没有 [MASK],进而导致产生由训练和预测数据偏差导致的性能损失。
考虑到上述的弊端,BERT 并没有总用 [MASK] 替换掩码词,而是按照一定比例选取替换词。在选择 15% 的词作为掩码词后这些掩码词有三类替换选项:
- 80% 练样本中:将选中的词用 [MASK] 来代替,例如:
- 10% 的训练样本中:选中的词不发生变化,该做法是为了缓解训练文本和预测文本的偏差带来的性能损失,例如:
- 10% 的训练样本中:将选中的词用任意的词来进行代替,该做法是为了让 BERT 学会根据上下文信息自动纠错,例如:
作者在论文中提到这样做的好处是,编码器不知道哪些词需要预测的,哪些词是错误的,因此被迫需要学习每一个 token 的表示向量,另外作者也表示双向编码器比单项编码器训练要慢,进而导致BERT 的训练效率低了很多,但是实验也证明 MLM 训练方法可以让 BERT 获得超出同期所有预训练语言模型的语义理解能力,牺牲训练效率是值得的。
在很多自然语言处理的下游任务中,如问答和自然语言推断,都基于两个句子做逻辑推理,而语言模型并不具备直接捕获句子之间的语义联系的能力,或者可以说成单词预测粒度的训练到不了句子关系这个层级,为了学会捕捉句子之间的语义联系,BERT 采用了下句预测(NSP )作为无监督预训练的一部分。
NSP 的具体做法是,BERT 输入的语句将由两个句子构成,其中,50% 的概率将语义连贯的两个连续句子作为训练文本(连续句对一般选自篇章级别的语料,以此确保前后语句的语义强相关),另外 50% 的概率将完全随机抽取两个句子作为训练文本。
连续句对:[CLS]今天天气很糟糕[SEP]下午的体育课取消了[SEP]
随机句对:[CLS]今天天气很糟糕[SEP]鱼快被烤焦啦[SEP]
其中 [SEP] 标签表示分隔符。 [CLS] 表示标签用于类别预测,结果为 1,表示输入为连续句对;结果为 0,表示输入为随机句对。
通过训练 [CLS] 编码后的输出标签,BERT 可以学会捕捉两个输入句对的文本语义,在连续句对的预测任务中,BERT 的正确率可以达到 97%-98%。
BERT 在预训练阶段使用了前文所述的两种训练方法,在真实训练中一般是两种方法混合使用。
由于 BERT 通过 Transformer 模型堆叠而成,所以 BERT 的输入需要两套 Embedding 操作:
- 一套为 One-hot 词表映射编码(对应下图的 Token Embeddings);
- 另一套为位置编码(对应下图的 Position Embeddings),不同于 Transformer 的位置编码用三角函数表示,BERT 的位置编码将在预训练过程中训练得到(训练思想类似于Word Embedding 的 Q 矩阵)
- 由于在 MLM 的训练过程中,存在单句输入和双句输入的情况,因此 BERT 还需要一套区分输入语句的分割编码(对应下图的 Segment Embeddings),BERT 的分割编码也将在预训练过程中训练得到
对于分割编码,Segment Embeddings 层只有两种向量表示。前一个向量是把 0 赋给第一个句子中的各个 token,后一个向量是把 1 赋给第二个句子中的各个 token ;如果输入仅仅只有一个句子,那么它的 segment embedding 就是全 0,下面我们简单举个例子描述下:
[CLS]I like dogs[SEP]I like cats[SEP] 对应编码 0 0 0 0 0 1 1 1 1
[SEP]I Iike dogs and cats[SEP] 对应编码 0 0 0 0 0 0 0
BERT 根据自然语言处理下游任务的输入和输出的形式,将微调训练支持的任务分为四类,分别是句对分类、单句分类、文本问答和单句标注,接下来我们将简要的介绍下 BERT 如何通过微调训练适应这四类任务的要求。
给定两个句子,判断它们的关系,称为句对分类,例如判断句对是否相似、判断后者是否为前者的答案。
针对句对分类任务,BERT 在预训练过程中就使用了 NSP 训练方法获得了直接捕获句对语义关系的能力。
如下图所示,句对用 [SEP] 分隔符拼接成文本序列,在句首加入标签 [CLS],将句首标签所对应的输出值作为分类标签,计算预测分类标签与真实分类标签的交叉熵,将其作为优化目标,在任务数据上进行微调训练。
针对二分类任务,BERT 不需要对输入数据和输出数据的结构做任何改动,直接使用与 NSP 训练方法一样的输入和输出结构就行。
针对多分类任务,需要在句首标签 [CLS] 的输出特征向量后接一个全连接层和 Softmax 层,保证输出维数与类别数目一致,最后通过 arg max 操作(取最大值时对应的索引序号)得到相对应的类别结果。
下面给出句对分相似性任务的实例:
任务:判断句子 “我很喜欢你” 和句子 “我很中意你” 是否相似
输入改写:“[CLS]我很喜欢你[SEP]我很中意你”
取 “[CLS]” 标签对应输出:[0.02, 0.98]
通过 arg max 操作得到相似类别为 1(类别索引从 0 开始),即两个句子相似
给定一个句子,判断该句子的类别,统称为单句分类,例如判断情感类别、判断是否为语义连贯的句子。
针对单句二分类任务,也无须对 BERT 的输入数据和输出数据的结构做任何改动。
如下图所示,单句分类在句首加入标签 [CLS],将句首标签所对应的输出值作为分类标签,计算预测分类标签与真实分类标签的交叉熵,将其作为优化目标,在任务数据上进行微调训练。
同样,针对多分类任务,需要在句首标签 [CLS] 的输出特征向量后接一个全连接层和 Softmax 层,保证输出维数与类别数目一致,最后通过 argmax 操作得到相对应的类别结果。
下面给出语义连贯性判断任务的实例:
任务:判断句子“海大球星饭茶吃” 是否为一句话
输入改写:“[CLS]海大球星饭茶吃”
取 “[CLS]” 标签对应输出:[0.99, 0.01]
通过 arg max 操作得到相似类别为 0,即这个句子不是一个语义连贯的句子
给定一个问句和一个蕴含答案的句子,找出答案在后这种的位置,称为文本问答,例如给定一个问题(句子 A),在给定的段落(句子 B)中标注答案的其实位置和终止位置。
文本问答任何和前面讲的其他任务有较大的差别,无论是在优化目标上,还是在输入数据和输出数据的形式上,都需要做一些特殊的处理。
为了标注答案的起始位置和终止位置,BERT 引入两个辅助向量 s(start,判断答案的起始位置) 和 e(end,判断答案的终止位置)。
如下图所示,BERT 判断句子 B 中答案位置的做法是,将句子 B 中的每一个次得到的最终特征向量 (T_i') 经过全连接层(利用全连接层将词的抽象语义特征转化为任务指向的特征)后,分别与向量 s 和 e 求内积,对所有内积分别进行 softmax 操作,即可得到词 Tok m((min [1,M]))作为答案其实位置和终止位置的概率。最后,去概率最大的片段作为最终的答案。
文本回答任务的微调训练使用了两个技巧:
- 用全连接层把 BERT 提取后的深层特征向量转化为用于判断答案位置的特征向量
- 引入辅助向量 s 和 e 作为答案其实位置和终止位置的基准向量,明确优化目标的方向和度量方法
下面给出文本问答任务的实例:
任务:给定问句 “今天的最高温度是多少”,在文本 “天气预报显示今天最高温度 37 摄氏度” 中标注答案的起始位置和终止位置
输入改写:“[CLS]今天的最高温度是多少[SEP]天气预报显示今天最高温度 37 摄氏度”
BERT Softmax 结果:
篇章文本天气预报显示今天最高温37摄氏度起始位置概率0.010.010.010.040.100.800.03终止位置概率0.010.010.010.030.040.100.80
对 Softmax 的结果取 arg max,得到答案的起始位置为 6,终止位置为 7,即答案为 “37 摄氏度”
给定一个句子,标注每个次的标签,称为单句标注。例如给定一个句子,标注句子中的人名、地名和机构名。
单句标注任务和 BERT 预训练任务具有较大差异,但与文本问答任务较为相似。
如下图所示,在进行单句标注任务时,需要在每个词的最终语义特征向量之后添加全连接层,将语义特征转化为序列标注任务所需的特征,单句标注任务需要对每个词都做标注,因此不需要引入辅助向量,直接对经过全连接层后的结果做 Softmax 操作,即可得到各类标签的概率分布。
由于 BERT 需要对输入文本进行分词操作,独立词将会被分成若干子词,因此 BERT 预测的结果将会是 5 类(细分为 13 小类):
- O(非人名地名机构名,O 表示 Other)
- B-PER/LOC/ORG(人名/地名/机构名初始单词,B 表示 Begin)
- I-PER/LOC/ORG(人名/地名/机构名中间单词,I 表示 Intermediate)
- E-PER/LOC/ORG(人名/地名/机构名终止单词,E 表示 End)
- S-PER/LOC/ORG(人名/地名/机构名独立单词,S 表示 Single)
将 5 大类的首字母结合,可得 IOBES,这是序列标注最常用的标注方法。
下面给出命名实体识别(NER)任务的示例:
任务:给定句子 “爱因斯坦在柏林发表演讲”,根据 IOBES 标注 NER 结果
输入改写:“[CLS]爱 因 斯坦 在 柏林 发表 演讲”
BERT Softmax 结果:
BOBES爱因斯坦在柏林发表演讲O0.010.010.010.900.010.900.90B-PER0.900.010.010.010.010.010.01I-PER0.010.900.010.010.010.010.01E-PER0.010.010.900.010.010.010.01S-LOC0.010.010.010.010.010.010.01
对 Softmax 的结果取 arg max,得到最终地 NER 标注结果为:“爱因斯坦” 是人名;“柏林” 是地名
无论如何,从上述讲解可以看出,NLP 四大类任务都可以比较方便地改造成 Bert 能够接受的方式,总之不同类型的任务需要对模型做不同的修改,但是修改都是非常简单的,最多加一层神经网络即可。这其实是 Bert 的非常大的优点,这意味着它几乎可以做任何NLP的下游任务,具备普适性,这是很强的。
但是讲了这么多,一个新模型好不好,效果才是王道。那么Bert 采用这种两阶段方式解决各种 NLP 任务效果如何?
在 11 个各种类型的 NLP 任务中达到目前最好的效果,某些任务性能有极大的提升。
最后,我讲讲我对Bert的评价和看法,我觉得 Bert 是 NLP 里里程碑式的工作,对于后面 NLP 的研究和工业应用会产生长久的影响,这点毫无疑问。但是从上文介绍也可以看出,从模型或者方法角度看,Bert 借鉴了 ELMO,GPT 及 CBOW,主要提出了 Masked 语言模型及 Next Sentence Prediction,但是这里 Next Sentence Prediction 基本不影响大局,而 Masked LM 明显借鉴了 CBOW 的思想。所以说 Bert 的模型没什么大的创新,更像最近几年 NLP 重要进展的集大成者,这点如果你看懂了上文估计也没有太大异议,如果你有大的异议,杠精这个大帽子我随时准备戴给你。
如果归纳一下这些进展就是:首先是两阶段模型,第一阶段双向语言模型预训练,这里注意要用双向而不是单向,第二阶段采用具体任务 Fine-tuning 或者做特征集成;第二是特征抽取要用Transformer 作为特征提取器而不是 RNN 或者 CNN;第三,双向语言模型可以采取 CBOW 的方法去做(当然我觉得这个是个细节问题,不算太关键,前两个因素比较关键)。Bert 最大的亮点在于效果好及普适性强,几乎所有 NLP 任务都可以套用 Bert 这种两阶段解决思路,而且效果应该会有明显提升。可以预见的是,未来一段时间在 NLP 应用领域,Transformer 将占据主导地位,而且这种两阶段预训练方法也会主导各种应用。
另外,我们应该弄清楚预训练这个过程本质上是在做什么事情,本质上预训练是通过设计好一个网络结构来做语言模型任务,然后把大量甚至是无穷尽的无标注的自然语言文本利用起来,预训练任务把大量语言学知识抽取出来编码到网络结构中,当手头任务带有标注信息的数据有限时,这些先验的语言学特征当然会对手头任务有极大的特征补充作用,因为当数据有限的时候,很多语言学现象是覆盖不到的,泛化能力就弱,集成尽量通用的语言学知识自然会加强模型的泛化能力。如何引入先验的语言学知识其实一直是 NLP 尤其是深度学习场景下的 NLP 的主要目标之一,不过一直没有太好的解决办法,而 ELMO/GPT/Bert 的这种两阶段模式看起来无疑是解决这个问题自然又简洁的方法,这也是这些方法的主要价值所在。
对于当前 NLP 的发展方向,我个人觉得有两点非常重要:
- 一个是需要更强的特征抽取器,目前看 Transformer 会逐渐担当大任,但是肯定还是不够强的,需要发展更强的特征抽取器;
- 第二个就是如何优雅地引入大量无监督数据中包含的语言学知识,注意我这里强调地是优雅,而不是引入,此前相当多的工作试图做各种语言学知识的嫁接或者引入,但是很多方法看着让人牙疼,就是我说的不优雅。
目前看预训练这种两阶段方法还是很有效的,也非常简洁,当然后面肯定还会有更好的模型出现。
完了,这就是预训练语言模型的前世今生。
以上就是本篇文章【Bert模型详解】的全部内容了,欢迎阅览 ! 文章地址:http://sicmodule.glev.cn/news/9497.html 资讯 企业新闻 行情 企业黄页 同类资讯 首页 网站地图 返回首页 歌乐夫资讯移动站 http://sicmodule.glev.cn/mobile/ , 查看更多