LLM总览

ChatGPT复现之路

LLM物种进化图

LLM物种进化图

大模型训练流程图

动画科普LLM大模型进阶之路:为何GPT之外一定要关注LLaMA

四大步骤 预训练 监督式微调 奖励建模 强化学习
数据集 互联网公开数据集(2万亿token) 问题对(1万-10万) 人工反馈评价(10万-100万) 人工提示词(1万-10万)
算法 语言模型(预测下一个token) 语言模型(预测下一个token) 二元分类器(输出奖励) 强化学习(最大化奖励)
模型 基础模型 SFT模型 奖励模型 强化学习模型
所需资源 千块GPU训练数月 几十块GPU训练几天 几十块GPU

指标(TODO)

指令微调

Transformer

Attention Is All You Need

论文解读: Attention is All you need

Hugging Face的GitHub代码库

Transformer系列

通常而言,绝大部分NLP问题可以归入上图所示的四类任务

  1. 序列标注: 这是最典型的NLP任务,比如中文分词,词性标注,命名实体识别,语义角色标注等都可以归入这一类问题

    它的特点是句子中每个单词要求模型根据上下文都要给出一个分类类别

  2. 分类任务: 比如我们常见的文本分类,情感计算等都可以归入这一类。它的特点是不管文章有多长,总体给出一个分类类别即可

  3. 句子关系判断: 比如Entailment,QA,语义改写,自然语言推理等任务都是这个模式,给定两个句子,模型判断出两个句子是否具备某种语义关系

  4. 生成式任务: 比如机器翻译,文本摘要,写诗造句,看图说话等都属于这一类,输入文本内容后,需要自主生成另外一段文字

预训练语言模型

目前有两种预训练语言模型用于下游任务的方法:feature-based(以ELMo为例)和fine-tuning(以BERTGPT为例)

有专门的论文(To Tune or Not to Tune? Adapting Pretrained Representations to Diverse Tasks)讨论了这个话题

Feature-based Pre-Training:

  • 在Feature-based Pre-Training中,首先使用大规模的未标记数据集对模型进行预训练

    预训练任务通常是通过自监督学习或其他无监督学习方法来完成,例如预测下一个词语、图像的旋转角度等

  • 预训练的目标是学习到具有良好表示能力的特征,能够捕捉数据中的一般性信息

  • 预训练模型通常是一个通用的模型,不针对特定的任务。它学习到的特征表示可以应用于各种不同的任务

  • 预训练模型可以作为迁移学习的基础,通过将其特征提取部分应用于具体的任务

Fine-tuning:

  • Fine-tuning是在预训练模型的基础上,在特定任务的有标签数据集上进行进一步训练和优化
  • Fine-tuning阶段会调整预训练模型的权重和参数,以使其适应目标任务的特定要求
  • Fine-tuning过程中,通常会保持预训练模型的一部分权重固定,只更新部分权重,以保留预训练阶段学习到的通用特征表示
  • Fine-tuning旨在在特定任务的有限标记数据集上优化模型,使其更好地适应该任务的数据和特征

总结来说,Feature-based Pre-Training是通过在未标记数据上预训练模型来学习通用的特征表示,而Fine-tuning是在预训练模型的基础上,在特定任务的有标签数据上进行进一步优化和微调

Feature-based Pre-Training提供了一种学习通用特征表示的方式,而Fine-tuning则将这些通用特征应用于特定任务,以提升任务性能,一句话概括

  1. Feature-based Pre-Training把输入转特征,特征丢给后面的模型(新模型),其他就和它无关了
  2. Fine-tuning是同一个网络结构,换了数据,可以固定或不固定前几层,继续训练

词向量

预训练语言模型的前世今生 - 从Word Embedding到BERT

年份 2013 年 2014 年 2015 年 2016 年 2017 年
技术 word2vec GloVe LSTM/Attention Self-Attention Transformer
年份 2018 年 2019 年 2020 年
技术 GPT/ELMo/BERT/GNN XLNet/BoBERTa/GPT-2/ERNIE/T5 GPT-3/ELECTRA/ALBERT
  1. One-hot编码:早期的自然语言处理中,词语通常被表示为离散的one-hot向量。每个词语都被表示为一个维度等于词汇表大小的向量,其中只有一个维度为1,其余维度都为0

    这种表示方法无法捕捉词语之间的语义关系和相似度

  2. Word Embedding:为了克服one-hot编码的局限性,提出了基于分布假设的词向量表示方法,即Word Embedding。Word Embedding使用低维实数向量表示词语,通过训练模型将词语映射到一个连续的向量空间中。其中,每个维度代表一个语义特征。Word Embedding能够捕捉到词语之间的语义关系和相似度,提供更丰富的表示

  3. Word2Vec(静态):Word2Vec是一种经典的词向量模型,由Tomas Mikolov等人于2013年提出。它基于神经网络模型,通过训练预测词语周围的上下文或预测目标词语。Word2Vec模型包括两种算法:CBOW(Continuous Bag-of-Words)和Skip-gram`。这两种算法使用浅层的神经网络来学习词向量,具有高效、快速训练的优势。Word2Vec模型能够生成静态的词向量,但无法捕捉词语的上下文相关特征

  4. ELMo(Embeddings from Language Models)(动态):ELMo是在Word2Vec之后提出的一种上下文相关的词向量表示方法,由Peters等人于2018年提出。ELMo利用双向语言模型,通过训练正向和逆向的LSTM模型来学习词语的上下文表示。ELMo能够根据上下文动态地生成词向量,捕捉到词语在不同上下文中的语义特征。与静态词向量不同,ELMo提供了更丰富、更具语义的词语表示,适用于各种自然语言处理任务

    ELMo(Embeddings from Language Models)模型在训练过程中使用了双向长短期记忆网络(Bi-LSTM)

总的来说,历史发展中,从one-hot编码到Word Embedding,再到Word2Vec和ELMo,词向量表示方法逐渐从离散、静态的表示发展到了连续、上下文相关的表示。这些方法的提出和发展使得自然语言处理模型能够更好地理解和处理文本数据,提高了各种文本相关任务的性能

Word Embedding、Word2Vec和ELMo关系如下:

  • Word2Vec是Word Embedding的一种具体实现方式。Word Embedding指的是将词语映射到低维实数向量空间的表示方法,而Word2Vec则是一种用于训练Word Embedding的算法

  • ELMo和Word2Vec是两种不同的词向量表示方法。ELMo是一种上下文相关的词向量表示方法,通过训练双向语言模型来学习词语在不同上下文中的动态表示。而Word2Vec是一种上下文无关的词向量表示方法,通过训练预测词语的上下文或目标词语来学习静态的词向量

模型介绍

介绍Transformer比较好的文章

  1. 一个是Jay Alammar可视化地介绍Transformer的博客文章The Illustrated Transformer,非常容易理解整个机制
  2. 哈佛大学NLP研究组写的The Annotated Transformer,代码原理双管齐下

Attention机制

Attention机制最早在视觉领域提出,2014年Google Mind发表了《Recurrent Models of Visual Attention》,使Attention机制流行起来,这篇论文采用了RNN模型,并加入了Attention机制来进行图像的分类

2005年,Bahdanau等人在论文《Neural Machine Translation by Jointly Learning to Align and Translate》中,将attention机制首次应用在nlp领域,其采用Seq2Seq+Attention模型来进行机器翻译,并且得到了效果的提升,Seq2Seq With Attention中进行了介绍

2017 年,Google 机器翻译团队发表的《Attention is All You Need》中,完全抛弃了RNN和CNN等网络结构,而仅仅采用自注意力(self-attention)机制来学习文本表示来进行机器翻译任务,并且取得了很好的效果,注意力机制也成为了大家近期的研究热点

本文首先介绍常见的Attention机制,然后对论文《Attention is All You Need》进行介绍,该论文发表在NIPS 2017上

Architecture

模型结构如下

  • 输入层

    • 词嵌入编码层
    • 位置编码层
  • Encoder

    • 多头自注意力
    • 残差连接
    • 全连接网络
  • Dncoder

    • 多头自注意力
    • 多头注意力(不是自注意, 因为QK来自Encoder)
    • 残差连接
    • 全连接网络

模型整体结构如下所示

The_transformer_encoder_decoder_stack

Transformer是一种基于自注意力机制序列到序列模型,广泛应用于自然语言处理任务,如机器翻译文本摘要语言生成

Transformer整体结构由以下几个主要组件组成

  1. 编码器(Encoder):编码器负责将输入序列(例如源语言句子)转换为一系列高级特征表示。它由多个相同的层堆叠而成,每个层都包含两个子层:多头自注意力机制和全连接前馈神经网络。自注意力机制允许模型对输入序列中的不同位置进行自适应地关注,从而捕捉序列中的上下文信息
  2. 解码器(Decoder):解码器负责从编码器生成的特征表示中生成目标序列(例如目标语言句子)。解码器也由多个相同的层堆叠而成,每个层包含三个子层:多头自注意力机制、编码器-解码器注意力机制和全连接前馈神经网络。编码器-解码器注意力机制用于在生成目标序列时,引入对源语言句子的关注
  3. 自注意力机制(Self-Attention):自注意力机制是Transformer的关键组件之一。它允许模型在进行编码或解码时,根据输入序列中不同位置之间的关系,动态地计算注意力权重。通过自适应地关注不同位置的信息,自注意力机制能够捕捉输入序列中的上下文信息,提供更全面的表示
  4. 注意力机制(Attention):除了自注意力机制,Transformer还使用编码器-解码器注意力机制。这种注意力机制允许解码器在生成目标序列时,对编码器输出的特征表示进行关注。它能够帮助解码器对源语言句子中与当前生成位置相关的信息进行处理
  5. 前馈神经网络(Feed-Forward Network):Transformer中的每个子层都包含一个前馈神经网络。该网络由两个全连接层组成,通过应用非线性激活函数(如ReLU)来对特征表示进行映射和变换。前馈神经网络有助于捕捉特征之间的非线性关系

通过编码器和解码器的组合,Transformer模型能够将输入序列转换为输出序列,实现不同的序列到序列任务

它的并行计算性质和自注意力机制的能力使得它在处理长序列和捕捉全局依赖关系方面具有优势,成为自然语言处理领域的重要模型,更详细的模型结构如下所示

transformer模型框架

Embedding

Embedding层是Transformer模型中的一个重要组成部分,用于将离散的输入序列(如单词、字符等)映射到连续的低维向量表示

它负责将输入的符号型数据转换为密集的实数向量,从而能够在模型中进行有效的学习和处理

在Transformer中,Embedding层主要有两个作用:

  1. 词嵌入(Word Embedding):对于自然语言处理任务,Embedding层将每个词汇或字符映射到一个低维的连续向量表示,称为词嵌入或字符嵌入。这些嵌入向量捕捉了词汇或字符之间的语义和语法关系,能够编码单词的上下文信息,使得模型能够更好地理解和表示输入数据
  2. 位置编码(Positional Encoding):Transformer模型中没有使用循环神经网络或卷积神经网络,因此无法直接捕捉输入序列中顺序信息。为了引入位置信息,Embedding层会添加位置编码到词嵌入中。位置编码是一种用于表示输入序列位置的向量,它提供了关于词汇在序列中相对位置的信息,帮助模型理解序列中的顺序关系

在实现上,Embedding层可以使用一个矩阵作为参数来进行词嵌入的查找。每个词汇对应矩阵中的一行,通过查找输入序列中的词汇对应的行向量,得到词嵌入表示

位置编码通常使用正弦和余弦函数的组合来计算,以获取不同位置的编码向量

需要注意的是,Embedding层的参数通常是在模型训练的过程中学习得到的,根据任务和数据来调整嵌入向量的表示能力。通过Embedding层,模型能够在低维连续向量空间中对输入序列进行表征和建模,从而更好地处理自然语言处理等任务

导入库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v1.0
@author: huangyc
@file: embedding.py
@Description:
@time: 2023/2/19 15:00
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import math
import matplotlib.pyplot as plt
import numpy as np
import copy

Embedding层定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Embeddings(nn.Module):
def __init__(self, d_model: int, vocab: int):
"""
构建Embedding类来实现文本嵌入层
:param d_model: 词嵌入的维度
:param vocab: 词表的大小
"""
super(Embeddings, self).__init__()
# 定义Embedding层
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model

def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)

PositionalEncoding层

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
38
39
40
41
42
43
44
45
46
# 定义位置编码器类, 我们同样把它看做一个层,因此会继承nn.Module
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, dropout: float, max_len: int = 5000):
"""
位置编码器类的初始化函数, 共有三个参数
:param d_model: 词嵌入维度
:param dropout: 置0比率
:param max_len: 每个句子的最大长度
:return:
"""
super(PositionalEncoding, self).__init__()
# 实例化nn中预定义的Dropout层, 并将dropout传入其中,获得对象self.dropout
self.dropout = nn.Dropout(p=dropout)
# 初始化一个位置编码矩阵,它是一个0阵, 矩阵的大小是max_len * d_model
pe = torch.zeros(max_len, d_model)

# 初始化一个绝对位置矩阵, 在我们这里, 词汇的绝对位置就是用它的索引去表示
# 所以我们首先使用arange方法获得一个连续自然数向量, 然后再使用unsqueeze方法拓展向量维度
# #又因为参数传的是1, 代表矩阵拓展的位置, 会使向量变成一个max_len * 1的矩阵
position = torch.arange(0, max_len).unsqueeze(1)

# 绝对位置矩阵初始化之后, 接下来就是考虑如何将这些位置信息加入到位置编码矩阵中
# 最简单思路就是先将max_len * 1的绝对位置矩阵, 变换成max_len * d_model形状, 然后覆盖原来的初始位置编码矩阵即可
# 要做这种矩阵变换, 就需要一个1 * d_model形状的变换矩阵div_term, 我们对这个变换矩阵的要求除了形状外
# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字, 有助于在之后的梯度下降过程中更快的收敛
# 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1 * d_model的矩阵
# 而是有了一个跳跃,只初始化了一半即1*d_mode1/2的矩阵. 为什么是一半呢, 其实这里并不是真正意义上的初始化
# 我们可以把它看作是初始化了两次, 而每次初始化的变换矩阵会做不同的处理, 第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上, 组成最终的位置编码矩阵
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)

# 这样我们就得到了位置编码矩阵pe, pe现在还是个二维矩阵,要想和embedding的输出(一个三维张量)相加, 需要扩展维度
pe = pe.unsqueeze(0)
# 我们把它认为是对模型效果有帮助的, 但是却不是模型结构中超参数或者参数,不需要随着优化步骤优化
# 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载
self.register_buffer('pe', pe)

def forward(self, x):
"""
:param x: 文本序列的词嵌入表示
"""
# 根据句子最大长度切割, pe不需要做梯度求解
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
return self.dropout(x)

实际测试 + 位置编码可视化

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
if __name__ == '__main__':
p_d_model = 512
p_vocab = 1000
p_dropout: float = 0.1
p_max_len = 60

# 词嵌入测试
x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))
emb = Embeddings(d_model=p_d_model, vocab=p_vocab)
embr = emb(x)
print("embr", embr)
print("embr size", embr.shape)

# 位置编码测试
pe = PositionalEncoding(d_model=p_d_model, dropout=p_dropout, max_len=p_max_len)
pe_result = pe(embr)
print("pe_result", pe_result)
print("pe_result size", pe_result.shape)

# 创建一张15x5大小的画布
plt.figure(figsize=(15, 5))
# 实例化PositionalEncoding类得到pe对象, 输入参数是20和0
pe = PositionalEncoding(20, 0)
# 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数,
# 且这个tensor里的数值都是0, 被处理后相当于位置编码张量
y = pe(Variable(torch.zeros(1, 100, 20)))
# 然后定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# 因为总共有20维之多, 我们这里只查看4,5,6,7维的值
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
# 在画布上填写维度提示信息
plt.legend(["dim %d" % p for p in [4, 5, 6, 7]])
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
embr tensor([[[-17.5113,  -6.0699,  11.6839,  ...,  -8.1281,  -7.7986,  35.1275],
[ -6.3789, -7.7614, 13.2975, ..., 16.8397, -31.3230, -68.4385],
[ -4.1841, 8.4322, 34.6418, ..., 38.4747, -4.9060, 25.4163],
[-23.4562, -28.9742, 18.1234, ..., 38.6039, 15.0049, -2.8916]],

[[-21.7485, 0.3263, 54.4449, ..., -18.3120, -15.5987, -11.4275],
[ -0.6414, 2.9492, -32.3063, ..., -21.9781, -16.3307, -15.4014],
[-16.1775, 20.8547, -21.0333, ..., -11.7583, -7.2429, 5.8607],
[ -4.7708, -51.9955, 14.8529, ..., 21.0973, 13.4664, -10.8492]]],
grad_fn=<MulBackward0>)
embr size torch.Size([2, 4, 512])

pe_result tensor([[[-19.4569, -5.6332, 12.9821, ..., -7.9201, -0.0000, 0.0000],
[ -6.1526, -8.0235, 15.6882, ..., 19.8219, -34.8032, -74.9317],
[ -3.6387, 8.9068, 39.5314, ..., 43.8608, -5.4509, 29.3514],
[-25.9057, -33.2935, 20.4094, ..., 44.0043, 16.6725, -2.1017]],

[[-24.1650, 1.4736, 0.0000, ..., -19.2356, -17.3319, -11.5861],
[ 0.2223, 3.8772, -34.9827, ..., -23.3090, -18.1452, -16.0015],
[-16.9647, 22.7095, -22.3299, ..., -11.9537, -8.0475, 7.6230],
[ -0.0000, -58.8727, 0.0000, ..., 24.5526, 14.9630, -0.0000]]],
grad_fn=<MulBackward0>)
pe_result size torch.Size([2, 4, 512])

位置编码可视化

Attention

超详细图解Self-Attention的那些事儿

除了Scaled Dot-Product Attention,Transformer模型中还有几种常见的注意力机制

  1. 点积注意力(Dot-Product Attention):它是Scaled Dot-Product Attention的简化版本,直接计算查询(Q)和键(K)之间的点积,然后通过softmax函数将结果转化为注意力权重。点积注意力相比于Scaled Dot-Product Attention没有进行缩放操作
  2. 加性注意力(Additive Attention):加性注意力使用了一个额外的前馈神经网络来计算注意力权重。它通过将查询(Q)和键(K)映射到相同的低维空间,然后计算它们的相似度得分,最后将相似度得分通过softmax函数进行归一化。加性注意力在一些场景中能够更好地捕捉输入序列之间的非线性关系
  3. 缩放点积注意力(Scaled Dot-Product Attention):它是Transformer中最常用的注意力机制。在计算注意力权重时,对点积注意力进行了缩放操作,通过除以特征维度的平方根,以减小注意力权重的大小变化。这有助于防止梯度消失或梯度爆炸,并使得模型更稳定
  4. 按位置加权注意力(Relative Positional Attention):这种注意力机制考虑了位置信息对注意力计算的影响。它引入了位置编码,通过计算相对位置的差异,对注意力权重进行调整。这种注意力机制在处理序列任务时能够更好地建模长距离依赖关系

在Transformer中使用的Attention是Scaled Dot-Product Attention,是归一化的点乘Attention,假设输入的query 、key维度、value维度为,那么就计算query和每个key 的点乘操作,并除以,然后应用Softmax函数计算权重

在实践中,将query和keys、values分别处理为矩阵,那么计算输出矩阵为:

其中, , ,输出矩阵维度为,其中为句子长度,为Embedding后的特征长度

其中的维度为,表示句子中每个字之间的关注度(self-attention),

的维度为,表示attention下的句子特征向量,如下所示

attention代码如下: 其中qkv是x经过线性变换之后的结果

1
2
3
4
5
6
7
8
9
10
def attention(query, key, value, mask=None, dropout=None):
"Compute 'Scaled Dot Product Attention'"
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim = -1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn

矩阵与其转置的乘积

向量数量积的几何意义:一个向量在另一个向量上的投影

向量的相似性是用两者的角度余弦来度量,余弦值越大则两者越相似

而余弦值等于两者内积与两者模长积的比,当两个向量模长固定的情形下,内积大小则反映了两者相似性的大小

1
2
3
4
5
6
7
import numpy as np
mat_a = np.array([1, 2, 2], [4, 5, 8])
np.matmul(mat_a,mat_a.T)

Out[5]:
array([[ 9, 30],
[ 30, 105]])

那么Scaled Dot-Product Attention的示意图如下所示,Mask是可选的,如果是能够获取到所有时刻的输入,那么就不使用Mask;如果是不能获取到,那么就需要使用Mask

使用了Mask的Transformer模型也被称为Transformer Decoder,不使用Mask的Transformer模型也被称为Transformer Encoder

scaled-dot-product-and-multi-head-attention

如果只对Q、K、V做一次这样的权重操作是不够的,这里提出了Multi-Head Attention操作,包括:

  1. 首先对做一次线性映射,将输入维度均为矩阵映射到
  2. 然后再采用Scaled Dot-Product Attention算出结果
  3. 多次进行上述两步操作,然后将得到的结果进行合并
  4. 将合并的结果进行线性变换

多头注意力的引入有以下几个目的

  1. 平行计算:通过使用多个注意力头,可以并行地计算注意力权重和加权求和,从而加快模型的计算速度。每个注意力头都专注于不同的表示子空间,因此可以独立地计算和处理信息,提高模型的效率。
  2. 多样性表达:每个注意力头学习到的表示子空间不同,通过多个注意力头的组合,可以获得更丰富、多样性的表示。这有助于模型更好地捕捉输入序列中的不同特征和关系,提高模型的表达能力。
  3. 组合注意力:多头注意力允许模型在不同的表示子空间上进行多次注意力计算,并将这些计算的结果进行组合。这种组合能够从不同的关注角度和视角来处理输入序列,帮助模型更全面地理解序列中的信息。

通过这些方式,多头注意力可以提供更灵活、更强大的建模能力,增强模型对序列中的长距离依赖关系、全局上下文和特征之间复杂关系的建模能力

它是Transformer模型在处理自然语言处理任务时取得成功的重要组成部分,总结来说公式如下所示

其中第1步的线性变换参数为,第4 步的线性变化参数为,而第三步计算的次数是

在论文中取 ,表示每个时刻的输入维度和输出维度, 表示8次Attention操作, 表示经过线性变换之后、进行Attention操作之前的维度

进行一次Attention之后输出的矩阵维度是,然后进行次操作合并之后输出的结果是,因此输入和输出的矩阵维度相同

这样输出的矩阵,每行的向量都是对向量中每一行的加权,示意图如上所示

多头注意力代码实现

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
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"Take in model size and number of heads."
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h
self.h = h
# 多头在这里体现
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)

def forward(self, query, key, value, mask=None):
if mask is not None:
# Same mask applied to all h heads.
mask = mask.unsqueeze(1)
nbatches = query.size(0)

# 1) Do all the linear projections in batch from d_model => h x d_k
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]

# 2) Apply attention on all the projected vectors in batch.
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

# 3) "Concat" using a view and apply a final linear.
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)

# 4) 线性变换投影回原始表示维度
return self.linears[-1](x)

如果不好理解可以看下这部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def multihead_attention(Q, K, V, num_heads):
# 线性变换得到查询、键和值的表示
Q_transformed = linear_transform(Q)
K_transformed = linear_transform(K)
V_transformed = linear_transform(V)

# 分割头
Q_heads = split_heads(Q_transformed, num_heads)
K_heads = split_heads(K_transformed, num_heads)
V_heads = split_heads(V_transformed, num_heads)

# 每个头的注意力计算
attention_heads = []
for i in range(num_heads):
attention_head = scaled_dot_product_attention(Q_heads[i], K_heads[i], V_heads[i])
attention_heads.append(attention_head)

# 拼接注意力头
concatenated_attention = concatenate_heads(attention_heads)

# 线性变换投影回原始表示维度
output = linear_transform(concatenated_attention)

return output

Encoder

编码器和解码器如下所示

Transformer_encoder_decoder

Decoder

在encoder部分中的self-attention是不需要mask的,而decoder部分的self-attention是需要mask的,因为正是有了mask遮挡后面的信息,才能将transformer用来做推理

编码器和解码器如下所示

Transformer_encoder_decoder

编码器把最后一层的KV喂给了编码器,此时Q来源解码器,K=V来源于编码器,是为了让解码器能够在生成输出时使用编码器的信息

通过给解码器提供编码器的键和值矩阵,可以实现以下两个目的

  1. 上下文信息传递:编码器中的自注意力机制能够捕捉到输入序列中的局部和全局关系,生成对应的键和值。将这些键和值传递给解码器,可以将编码器的上下文信息传递给解码器,以帮助解码器在生成输出时了解输入序列的相关内容。
  2. 对齐和信息提取:解码器可以通过注意力机制对编码器的键和值进行加权汇总,以获取与当前解码位置相关的信息。通过计算解码器当前位置与编码器中每个位置之间的注意力分数,可以实现对齐和信息提取,使解码器能够专注于与当前位置相关的输入信息。

总结来说,通过将编码器的键和值矩阵提供给解码器,可以实现上下文信息传递和对齐机制,帮助解码器在生成输出时利用编码器的信息,从而改善模型的性能和输出质量

编码器到解码器

mask

  • Attention Mask

  • Padding Mask

Bert

The Illustrated BERT, ELMo, and co. (How NLP Cracked Transfer Learning)

ELMo、GPT 和 BERT 三者的区别

BERT-GPT-比较

  • GPT:GPT 使用Transformer Decoder作为特征提取器,实现了单向编码、具有良好的文本生成能力,然而当前词的语义只能由其前序词决定,并且在语义理解上不足
  • BERT:使用了Transformer Encoder作为特征提取器,为双向编码器,并使用了与其配套的掩码训练方法。虽然使用双向编码让BERT不再具有文本生成能力,但是BERT的语义信息提取能力更强
  • ELMo: 使用自左向右编码和自右向左编码的两个LSTM网络,分别以为目标函数独立训练,将训练得到的特征向量以拼接的形式实现双向编码,本质上还是单向编码,只不过是两个方向上的单向编码的拼接而成的双向编码

从GPT和EMLO及Word2Vec到Bert

bert是自编码模型,而gpt是自回归模型

bert概况

BERT(Bidirectional Encoder Representations from Transformers)模型的编码器由多个Transformer编码器层组成,通常使用了多个重复的编码器来形成深层的表示

每个BERT编码器层包含了以下组件:

  1. 多头自注意力(Multi-Head Self-Attention):该层使用多头自注意力机制来对输入序列进行建模。自注意力允许模型在处理序列时关注不同位置之间的相关性,有助于捕捉上下文信息
  2. 前馈神经网络(Feed-Forward Neural Network):在自注意力层后面是一个前馈神经网络。该网络通常由两个线性层和激活函数(如ReLU)组成,用于对自注意力输出进行非线性变换和特征提取
  3. 残差连接(Residual Connections)和层归一化(Layer Normalization):在每个子层(自注意力和前馈神经网络)之后都应用了残差连接和层归一化操作。这些操作有助于缓解梯度消失问题,并提供更稳定和高效的训练

BERT模型中通常会堆叠多个编码器层来形成深层表示。每个编码器层的输出会作为下一层的输入,通过多次重复这个过程,可以逐渐丰富输入序列的表示能力

值得注意的是,BERT模型还在编码器输入的开头添加了特殊的标记,如[CLS](用于分类任务)和[SEP](用于分隔输入)。这些特殊标记有助于模型在处理不同任务时进行序列级别的操作和分类

总结起来,BERT的编码器由多个Transformer编码器层组成,每个编码器层由多头自注意力、前馈神经网络和残差连接/层归一化组成。通过堆叠多个编码器层,BERT模型可以获得深层、高质量的语言表示

Architecture

输入

训练方式

由于无法使用标准语言模型的训练模式,BERT借鉴完形填空任务和CBOW的思想,使用语言掩码模型(MLM)方法训练模型

训练中的mask

MLM方法也就是随机去掉句子中的部分token(单词),然后模型来预测被去掉的token是什么。这样实际上已经不是传统的神经网络语言模型(类似于生成模型)了,而是单纯作为分类问题,根据这个时刻的hidden state来预测这个时刻的token应该是什么,而不是预测下一个时刻的词的概率分布了

随机去掉的token被称作掩码词,在训练中,掩码词将以15%的概率被替换成[MASK],也就是说随机mask语料中15%的token,这个操作则称为掩码操作

在选择15%的词作为掩码词后这些掩码词有三类替换选项:

  1. 80% 练样本中:将选中的词用 [MASK] 来代替
  2. 10% 的训练样本中:选中的词不发生变化,该做法是为了缓解训练文本和预测文本的偏差带来的性能损失
  3. 10% 的训练样本中:将选中的词用任意的词来进行代替,该做法是为了让 BERT 学会根据上下文信息自动纠错

预训练模型

预训练模型分类

自编码类模型、自回归类模型和Encoder-Decoder模型都是在自然语言处理(NLP)领域中常见的模型类型,它们在处理文本任务时有不同的特点和功能

  • 自编码类模型:自编码类模型主要用于学习输入数据的表示。它们通常由两个部分组成:编码器和解码器。编码器将输入数据编码为低维度的表示,而解码器则尝试从该表示中重构原始输入。自编码类模型的目标是通过最小化重构误差来学习数据的有用特征。常见的自编码类模型包括BERT、ALBert和RoBERTa

    • BERT:双向Transformer的Encoder,通过预训练方式使用掩码语言建模和下一句预测任务来学习文本的表示

    • ALBert:BERT的改进版本,通过参数共享和参数缩减来提高预训练模型的效率和性能

    • RoBERTa:对BERT进行改进和优化,通过更大的数据和训练步数来提高模型性能

  • 自回归类模型:自回归类模型用于生成序列数据,其中模型的输出是基于先前生成的内容的条件概率分布。这意味着模型会根据前面生成的内容来预测下一个标记或词。自回归类模型通常使用循环神经网络(RNN)或Transformer等结构来建模序列数据的依赖关系。常见的自回归类模型包括ELMO、XLnet、GPT1和GPT2
  • ELMO:采用双向LSTM进行预训练,通过将上下文信息融入词向量表示来解决多义词问题

  • XLnet:通过排列组合的方式进行预测,解决了自回归模型的问题,提高了模型的性能

  • GPT1:采用Transformer的解码器进行预训练,使用自回归语言建模方法

  • GPT2:在GPT1的基础上进行改进,具有更强的生成能力和更大的模型参数量

  • Encoder-Decoder模型:Encoder-Decoder模型是一种结构,由两部分组成:编码器和解码器。编码器负责将输入序列编码为固定长度的表示,而解码器利用该表示来生成输出序列。这种模型广泛用于机器翻译、文本摘要和对话生成等任务。T5(Text-to-Text Transfer Transformer)是一种基于Transformer的Encoder-Decoder模型,可以用于处理多种NLP任务
    • T5:采用Transformer的编码器-解码器模型,通过预训练和Fine-tuning来处理NLU和NLG任务,具有统一的框架和多种预训练方法

这些模型在NLP任务中取得了显著的成果,并被广泛用于各种文本处理和自然语言处理应用中

时间线

根据时间线,以下是对目前常见的NLP预训练模型进行的整理:

  1. ELMO(2018年3月):由华盛顿大学提出,采用双向LSTM进行预训练,在不同的层次上提取词向量表示
  2. GPT(2018年6月):由OpenAI提出,使用Transformer的解码器进行预训练,采用自回归语言建模方法
  3. BERT(2018年10月):由Google提出,基于Transformer的编码器,使用掩码语言建模和下一句预测任务进行预训练
  4. XLNet(2019年6月):由CMU和Google Brain提出,采用排列组合的方式进行预测,解决了GPT中自回归模型的问题
  5. ERNIE(2019年4月):由百度提出,结合了知识增强和语义理解目标,通过预训练来提高NLU(自然语言理解)和NLG(自然语言生成)任务的性能
  6. BERT-wwm(2019年6月30日):哈工大和讯飞合作提出,基于BERT的模型,在预训练过程中使用了更大的中文词表
  7. RoBERTa(2019年7月26日):由Facebook提出,对BERT进行了改进和优化,通过更大的数据和训练步数来提高模型性能
  8. ERNIE2.0(2019年7月29日):由百度提出的改进版本,结合了知识增强和预训练目标的多样性,以提高模型的泛化和迁移能力
  9. BERT-wwm-ext(2019年7月30日):哈工大和讯飞合作提出的改进版本,进一步优化了中文词表,提升了预训练模型的性能
  10. ALBERT(2019年10月):由Google提出,通过参数共享和参数缩减的方式来提高预训练模型的效率和性能

这些预训练模型在不同的任务和数据集上都取得了很好的效果,并且为各种NLP应用提供了强大的基础模型

对话模板

对话标记语言ChatML

ChatML对话标记语言

一文带你了解通义千问Chat model的Chat模版

ChatML是OpenAI发布的对话标记语言,由于指令注入攻击会对大语言模型产生安全隐患,ChatML被设计用于保护大语言模型免于攻击

为了抵御指令注入攻击,对话被分隔为不同的层级或角色:

  • 系统(System)
  • 助手(Assistant)
  • 用户(User)

ChatML明确告诉模型每个文本片段的来源,在人类文本和AI文本之间划清界限

具体来说,通过ChatML对话标记语言,模型可以识别出指令究竟是源自开发者、用户或是自身

以下是一个具有系统(system)、用户(user)和助手(assistant)角色定义的ChatML示例JSON文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[{
"role": "system",
"content": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: 2023-03-02"
},
{
"role": "user",
"content": "How are you?"
},
{
"role": "assistant",
"content": "I am doing well"
},
{
"role": "user",
"content": "What is the mission of the company OpenAI?"
}
]

label mask

Qwen使用ChatML格式的对话模板,ChatML将对话按角色做了分隔,包含system、user、assistant这3种角色,每轮对话如:<|im_start|> + role + \n + message + <|im_end|> + \n,对话的prompt如下所示

1
2
3
4
5
6
7
8
9
10
<|im_start|>system
system message<|im_end|>
<|im_start|>user
round 1 query<|im_end|>
<|im_start|>assistant
round 1 answer<|im_end|>
<|im_start|>user
round 2 query<|im_end|>
<|im_start|>assistant
round 2 answer<|im_end|>

Qwen-chat模型的sft数据处理采用如下label mask策略:

  1. <|im_start|>, <|im_end|>+\n等特殊token不作mask
  2. 对system和每轮user的message添加mask
  3. 每轮对话中的角色信息(”system\n”, “user\n”, “assistant\n”)添加mask

MOE

混合专家模型 (MoE) 详解

mistral.ai家的mixtral-of-experts

混合专家模型(MixtureofExperts:MoE)的思想可以追溯到集成学习,集成学习是通过训练多个模型(基学习器)来解决同一问题,并且将它们的预测结果简单组合(例如投票或平均)。集成学习的主要目标是通过减少过拟合,提高泛化能力,以提高预测性能。常见的集成学习方法包括Bagging,Boosting和Stacking

集成学习在训练过程中,利用训练数据集训练基学习器,基学习器的算法可以是决策树、SVM、线性回归、KNN等,在推理过程中对于输入的X,在每个基学习器得到相应的答案后将所有结果有机统一起来,例如通过求均值的方法解决数值类问题,通过投票方式解决分类问题

MoE和集成学习的思想异曲同工,都是集成了多个模型的方法,但它们的实现方式有很大不同。与MoE的最大不同的地方是集成学习不需要将任务分解为子任务,而是将多个基础学习器组合起来。这些基础学习器可以使用相同或不同的算法,并且可以使用相同或不同的训练数据

大模型的研究新方向:混合专家模型(MoE)

moe流程示例动态图

MoE模型本身也并不是一个全新的概念,它的理论基础可以追溯到1991年由MichaelJordan和GeoffreyHinton等人提出的论文,距今已经有30多年的历史,但至今依然在被广泛应用的技术。这一理念在被提出来后经常被应用到各类模型的实际场景中,在2017年得到了更进一步的发展,当时,一个由QuocLe,GeoffreyHinton和JeffDean领衔的团队提出了一种新型的MoE层,它通过引入稀疏性来大幅提高模型的规模和效率