Llama1-3 🦙:从一道美团大模型面试题讲起

从 LLaMA1 到 LLaMA3:这风云激荡的 2023 到 2024 年

这篇博客应当是我在 2025 年的首篇博客。24 年底,在我身上发生了很多意想不到的事情,总使我灰心丧气。忽然间,发觉自己除开写了十几个月的日记,并没有更多新的更新。前不久,在 B 站上看到名为 美团大模型面试真题:LLaMA怎么优化注意力机制计算? 的视频,我才惊觉时光飞逝。

于是想着,似乎应当写一篇详解 LLaMA 的博客出来。等到调查资料的时候,发现 LLaMA 原来早在 2023 年就已发布,且已到第三代。估计第四代就将在 2025 年发布吧?看来这篇博客实在是拖延不得了!

又:长久以来我总计划开设“NLP 经典论文”专区,整理自 2017 年以来“重点文”,不妨就以此篇作始吧!

2023 到 2024 年:从无到有,由弱及强

2023 到 2024 年,metaAI 先后发布 LLaMA1 到 LLaMA3 的论文。LLaMA 系列从无到有、由弱及强,成功跻身大语言模型先锋军之中。2023 也由此可以称作 LLaMA 元年

版本 规范化 激活函数 位置编码 GQA Tokens 上下文长度 Vocab Size
LLaMA1 RMSNorm SwiGLU RoPE 1.4T 2K 32K
LLaMA2 RMSNorm SwiGLU RoPE 2.0T 4K 32K
LLaMA3 RMSNorm SiLU RoPE 15.0T 8K 128K

在模型结构上,LLaMA 自 1 代至 3 代总体改动较小,其中比较关键的技术是:RMSNormRoPEGQALLaMA 的迭代更新主要得力于 metaAI 不断扩充的优质语料库:

  • 自 1 代到 3 代,语料库的总 Token 量从 1.4T 上升至 15T;
  • 词汇表大小也从最初的 32K 扩充至 128K;
  • 预训练的上下文长度从 2K 增加到 4K,再到现在的 8K。

在模型参数量上,三代 LLaMA 总体呈现上升趋势:

  • LLaMA1 分别具有 7B、13B、33B、65B 等版本;
  • LLaMA2 与 1 代比差距不大,包括 7B、13B、34B、70B 等;
  • LLaMA3 的参数量则显著提升,主要是 8B、70B 甚至 405B 等。

LLaMA 是如何优化注意力机制的?

回到我们的话题上来:LLaMA 是如何优化注意力机制的? 事实上,LLaMA 在模型架构上大体仍沿用 (Vanilla,2017) 提出的 Transformer,而进行了许多微小的改动。我根据自身对原论文的理解,认为其中比较重要的三个技术是:RMSNormRoPEGQA。这些技术是什么意思?它们为什么能优化注意力机制?接下来对其进行详细讲解。

RMSNorm:在不牺牲模型稳定性的同时降低计算复杂度

原始的 Transformer 使用的是 层规范化 (LayerNorm) 。之所以引入规范化,是希望模型中间层的输入分布始终保持稳定。具体来说,它对输入 $x$(假设 $x = [s_{0}^{j}, s_{1}^{j}, …, s_{N}^{j}]$,$s$ 是长度为 $N$ 的序列,每个词向量取第 $j$ 个维度)进行如下操作:

$$y = \frac{x - E[x]}{\sqrt{Var[x] + \epsilon}} * \gamma + \beta. \quad \text{其中:} E[x] = \frac{1}{N} \sum_{i=1}^{N} x_i, \quad Var[x] = \frac{1}{N} \sum_{i=1}^{N} (x_i - E[x])^2$$

而 RMSNorm,或称 均方根规范化 就是 LayerNorm 的变体,RMSNorm 省去了求均值的过程,也没有了偏置 $\beta$:

$$y = \frac{x}{\sqrt{Mean(x^2) + \epsilon}} * \gamma. \quad \text{其中:} Mean(x^2) = \frac{1}{N} \sum_{i=1}^{N} x_i^2$$

首先,在计算上:RMSNorm 相比 LayerNorm 更为简便。其次,在效果上:容易看出 RMSNorm 具有对输入缩放的不变性($y(ax) = y(x)$),一定程度上也能够稳定输入分布。最后,RMSNorm 并不保证输出的均值为 0。实验证明,该性质对模型的训练影响较小。

RoPE:以简便方式有效捕捉相对位置信息

LLaMA 在每个 Attention 层中分别对 $Q$ 和 $K$ 进行 旋转位置编码 (RoPE, Rotary Position Embedding) 具体来说,是在 $Q$ 和 $K$ 点积之前对 $Q$ 和 $K$ 分别编码。以下是部分 metaAI 官方给出的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Attention(nn.Module):
def forward(self, x, ...):
bsz, seqlen, hidden_dim = x.shape
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)

# 对 Q 和 K 施用旋转位置编码
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)

... # 此处省略部分代码

# 对 Q 和 K 求点积并缩放
scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)

那么,什么是 旋转位置编码 ?RoPE 原论文的说法是:“在RoPE中,我们的出发点就是 通过绝对位置编码的方式实现相对位置编码 。”也即,我们考虑:

  • 假设我们有编码操作 $f(·)$,分别对 $Q$ 的第 $m$ 维向量 $q$ 和 $K$ 的第 $n$ 维向量 $k$ 进行位置编码:
    • $q_m = f (q, m) \quad k_n = f (k, n)$
  • 那么在 Attention 中对 $Q$ 和 $K$ 求点积时,我们希望 $q_m$ 和 $k_n$ 的内积结果将带入 $m-n$ 这个 相对位置信息 (m-n 将影响内积结果)
    • $\langle f(q, m),, f(k, n)\rangle = g(q, k, m-n)$
  • 原论文经过复实变换得到了操作 $f(·)$ 的解:
    • $f(q, m) = q e^{i m \theta} = q \cdot (\cos m \theta+ i \sin m \theta)$. 其中 $i$ 为虚数单位,$\theta$ 为给定超参。
    • 根据复数乘法的几何意义,该变换实际上对应着向量的旋转(我们可以想象二维向量)。
  • 最后,我们可以将 $f(q, m)$ 的完整形式表现如下(假设 $q$ 为 $d$ 维向量):
    • 其中,$\theta_i = 10000^{-2i/d}$,这带来一定的远程衰减性。
    • $\otimes$ 是逐位对应相乘,即Numpy、Tensorflow等计算框架中的 $∗$ 运算。
    • 从这个实现也可以看到,RoPE可以视为是乘性位置编码的变体。

$$ \begin{bmatrix} q_0 \\ q_1 \\ q_2 \\ q_3 \\ \vdots \\ q_{d-2} \\ q_{d-1} \end{bmatrix} \otimes \begin{bmatrix} \cos m\theta_0 \\ \cos m\theta_0 \\ \cos m\theta_1 \\ \cos m\theta_1 \\ \vdots \\ \cos m\theta_{d/2-1} \\ \cos m\theta_{d/2-1} \end{bmatrix} +\quad \begin{bmatrix} -q_1 \\ q_0 \\ -q_3 \\ q_2 \\ \vdots \\ -q_{d-1} \\ q_{d-2} \end{bmatrix} \otimes \begin{bmatrix} \sin m\theta_0 \\ \sin m\theta_0 \\ \sin m\theta_1 \\ \sin m\theta_1 \\ \vdots \\ \sin m\theta_{d/2-1} \\ \sin m\theta_{d/2-1} \end{bmatrix} $$

最后在这里贴一份 metaAI 官方写的代码,以便更加直观地看到 RoPE 操作是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
t = torch.arange(end, device=freqs.device) # type: ignore
freqs = torch.outer(t, freqs).float() # type: ignore
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
return freqs_cis

def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
ndim = x.ndim
assert 0 <= 1 < ndim
assert freqs_cis.shape == (x.shape[1], x.shape[-1])
shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)]
return freqs_cis.view(*shape)

def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
return xq_out.type_as(xq), xk_out.type_as(xk)

GQA:减少 KV-Cache 大小以提升模型推理速度

引入 GQA 前先简单介绍下 KV-Cache

提高大模型推理速度的常用关键技术是 KV-Cache。大模型以 自回归 (auto regressive) 的方式推理,预测下个 token 时,总是基于之前已经生成的序列。从代码的角度来理解是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inputs = "这是初始的输入序列"

while not stop:
# 根据之前的序列预测下个 token
next_token = model(inputs)

# 如果下个 token 是休止符,退出
# 否则,将 token 加入序列进行下次预测
if next_token == '[SEP]':
stop = True
else:
inputs += next_token

outputs = inputs

显然,随着输入序列越来越长,每次都重新计算所有 token 注意力值的传统方法显得非常低效。针对该问题,KV-Cache 采用 空间换时间 的方法:在自回归过程中,开辟空间将每次计算的 $K$ 和 $V$ 缓存起来,这样在处理新序列时只需从缓存中读取之前的 $K$ 和 $V$,无需重复计算。LLaMA 中的代码实现如下:

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
class Attention(nn.Module):
def __init__(self, ...):
super().__init__()
... # 此处省略部分代码

# 开辟 cache_k 用于缓存 k
self.cache_k = torch.zeros(
(
args.max_batch_size,
args.max_seq_len,
self.n_local_kv_heads,
self.head_dim,
)
).cuda()
# 开辟 cache_v 用于缓存 v
self.cache_v = torch.zeros(
(
args.max_batch_size,
args.max_seq_len,
self.n_local_kv_heads,
self.head_dim,
)
).cuda()

def forward(self, x, ...):
... # 此处省略部分代码

# 将缓存置于 xq 所在的计算单元(CPU 或 CUDA)
self.cache_k = self.cache_k.to(xq)
self.cache_v = self.cache_v.to(xq)

# 将当前计算出的 K 与 V 缓存起来
# 缓存位置是从 start_pos 到 start_pos + seqlen
# start_pos 是上次缓存后的序列长度
# seqlen 是此次新增的子序列长度,KV-Cache 并非逐字缓存的
self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv

# 获取缓存的 K 与 V,用于之后的计算
keys = self.cache_k[:bsz, : start_pos + seqlen]
values = self.cache_v[:bsz, : start_pos + seqlen]

在了解了 KV-Cache 后,我们不禁会反思:既然 $K$ 和 $V$ 能够缓存,那为什么不缓存 $Q$ 呢?答案是:不需要!与 BERT 不同,LLaMA、GPT 等大语言模型都是 Decoder-Only 架构,采用 单向注意力机制。由于使用了注意力掩码,位置靠后 token 的 $K$ 值将无法与位置靠前 token 的 $Q$ 值求内积。因此,缓存历史 token 的 $Q$ 值是完全没有必要的。

GQA:用分组的方式减少 KV 对的数量

然而,引入 KV-Cahe 意味着需要开辟大量的缓存空间。在 多头注意力 (MHA) 中,所需开辟的缓存空间大小可按照如下公式计算:

$$N \times B \times L \times H \times D$$

  • $N$ 指 Transformer 块的数量。以 LLaMA2 为例,它共包含 32 个。
  • $B$ 指输入数据的批量大小。这里我们假设是 16。
  • $L$ 为输入句子的长度。为便于计算,我们取最大长度为 1024。
  • $H$ 是注意力头的个数。假设是 32,与原论文保持一致。
  • $D$ 是隐藏层的维度数。它在 LLaMA-7B 中被设为 4096。

根据公式,我们得出为了缓存 KV 所需的空间大小为 $32 * 16 * 1024 * 32 * 4096 = 2^{36}$ 份浮点数所需的空间。若我们使用半精度浮点数 float16 类型存储,就需要 $2^{36} * 2 \text{Byte} = 128 \text{GB}$ 大小的存储空间!参照 LLaMA1 论文中给出的硬件设置,这远远地超出了他们所提供的 RAM 80GB 的容量。

为解决这一问题,我们必须优化算法。由此,便引出了 LLaMA 自 2 代开始所使用的 GQA (Group Query Attention) 。以下摘自 GQA 的原论文的图片简单明了地说明了什么是 GQA。

MHA GQA MQA

如图所示:在 多头注意力机制(左,MHA) 中,每个注意力查询 Q 都有各自对应的 KV 对,这导致缓存 KV 所需的空间极大。最极端的情况下,如 多查询注意力机制(右,MQA),所有的查询 Q 共享唯一的 KV 对,虽最大程度地减少了所需的缓存空间,但带来了精度的下降。而 分组查询注意力机制(中间,GQA),在精度和计算之间折中:将各查询 Q 分组, 同组查询共享一个 KV 对,在保证精度的同时一定程度地减少缓存空间。

我们可以稍做计算:若我们将原有的 32 个注意力头分成 8 组,每组 4 个查询。那么就仅会有 8 个 KV 对,所需的缓存空间就变为了原来的四分之一(128GB -> 32GB)。由此,模型推理性能得到提升,我们就可以喂给模型更多和更长的句子,或者搭建更深的网络和设置更大的隐藏层维度。

metaAI 在代码中的具体实现方式为:限制 KV 头的数目小于 Q 头的数目,在计算的时候复制 KV 头以使其能够与 Q 的头对应,在缓存的时候仅缓存不重复的 KV 对。代码如下:

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
# 重复 KV:x 是要重复的对象(K 或 V)
# n_rep 是重复的次数,它等于 Q 的头除以 KV 的头
def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
"""torch.repeat_interleave(x, dim=2, repeats=n_rep)"""
bs, slen, n_kv_heads, head_dim = x.shape
if n_rep == 1:
return x # 规定重复 1 次等于不重复
return (
x[:, :, :, None, :] # 重复后第 3 个维度变成 n_rep * KV 的头数
.expand(bs, slen, n_kv_heads, n_rep, head_dim)
.reshape(bs, slen, n_kv_heads * n_rep, head_dim)
)

class Attention(nn.Module):
def forward(self, x, ...):
... # 此处省略部分代码

keys = self.cache_k[:bsz, : start_pos + seqlen]
values = self.cache_v[:bsz, : start_pos + seqlen]

# 重复 KV 的头以使之与 Q 的头对应
keys = repeat_kv(keys, self.n_rep)
values = repeat_kv(values, self.n_rep)

## 以下代码是注意力机制的具体计算过程
# B 批量大小 L 序列长度
# H 注意力头数 D 隐藏层维度数
# C 已缓存的序列长度
xq = xq.transpose(1, 2) # [B, L, H, D] -> [B, H, L, D]
keys = keys.transpose(1, 2) # [B, C + L, H, D] -> [B, H, C + L, D]
values = values.transpose(1, 2) # [B, C + L, H, D] -> [B, H, C + L, D]
scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores + mask # [B, H, L, C + L]
scores = F.softmax(scores.float(), dim=-1).type_as(xq)
output = torch.matmul(scores, values) # [B, H, L, D]
output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1)
return self.wo(output)

CodeWork:回顾与总结,尝试阅读完整代码

在之前对 RoPE、KV-Cache、GQA 的介绍中,我们事实上已经将 LLaMA 中 Attention 模块的 forward() 函数部分的全部代码展示出来了。现在贴一份完整的代码在这里,可以试着在无注释的条件下理解代码做了什么。如果想看源码,也可以访问 metaAI 官方的代码

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
class Attention(nn.Module):
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):
bsz, seqlen, _ = x.shape
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)

xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)

self.cache_k = self.cache_k.to(xq)
self.cache_v = self.cache_v.to(xq)

self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv

keys = self.cache_k[:bsz, : start_pos + seqlen]
values = self.cache_v[:bsz, : start_pos + seqlen]

keys = repeat_kv(keys, self.n_rep)
values = repeat_kv(values, self.n_rep)

xq = xq.transpose(1, 2)
keys = keys.transpose(1, 2)
values = values.transpose(1, 2)
scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores + mask
scores = F.softmax(scores.float(), dim=-1).type_as(xq)
output = torch.matmul(scores, values)
output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1)
return self.wo(output)

训练一个对话机器人:LLaMA2 是如何做的?

自从 ChatGPT 火出圈以后,使用强化学习来提升 LLM 的对话能力 成为了一个很通用的方法。现如今的 LLM 在表现效果方面的提升主要关注 有用性 (helpfulness)安全性 (safety) 两个方面,它们对优化人机交互的体验非常关键。 在 LLaMA2 中,这一点通过强化学习来搞定。

LLaMA2 Chat 的训练示意图

简单地看一下预训练和 SFT 微调部分

预训练和微调过程都以自回归 (auto-regressive) 为训练目标。最大的不同之处在于:预训练过程的输入是独立的一句话,计算损失时考虑整个句子;微调过程的输入则是由 prompt 和 answer 组成的句子对,计算损失时仅考虑 answer 部分。自回归损失可用如下公式表示:

$$\frac{1}{T} \sum_{t \in T} C (s_t ; P_{\theta}[s_0,s_1,{\dots}s_{t-1}] )$$

在预训练阶段,自回归赋予模型说话的能力,使模型的输出分布于 人类可接受 (Human Acceptable) 的域内。而在 SFT 微调阶段,通过 指令微调 (Instruction Fine-tuning) ,模型被要求以“提问-回答”的方式生成内容,从而使输出落在 人类可用 (Human Usable) 的域内。

至于具体的训练细节,如 使用 Adawm 优化器学习率呈 Cosine 式递减 等,限于篇幅,不再具体指出。感兴趣的小伙伴们可以通过原论文自行查阅。

重点!带人类反馈的强化学习 (RLHF)

LLaMA 引入强化学习,目的在于使模型的输出尽可能分布在 人类偏好 (Human Preference) 的域内。具体来说,metaAI 希望 LLaMA 是 有用的、无害的 (Helpful, Safe)。LLaMA 使用了与 ChatGPT 相同的 RLHF 算法,该算法分为三步:1) 定义并搜集人类偏好数据;2) 训练奖励模型(Reward Model);3) 确定所使用的强化学习算法,并不断迭代。下图反映了经过强化学习后模型输出的分布变化。

收集人类偏好数据

metaAI 从 helpfulness 和 safety 两个维度衡量人类对机器回答的偏好程度。标注者首先编写 prompt 喂给两个不同 Temperature 参数的模型变体,并按给定的 guidelince 给得到的两个回答打分。

  • 考察 helpfulness 时,标注员将回答标注为 significantly better, better, slightly better 或者 negligibly better / unsure。
  • 考察 safety 时,再额外收集一个安全标签,将模型回答分为三类:偏好回答安全而另一个回答不安全、两个回答都安全或者两个回答都不安全。

模型输出的分布变化

metaAI 按周分批收集标注,并在每次调优下个 Llama 2-Chat 前,使用最新一次迭代收集的偏好数据训练 RM (Reward Model,奖励模型),以保持 RM 的准确性和适应性。如上图所示,迭代将使 Llama 2-Chat 的输出尽可能分布到 RM 得分高的域内。这其中,RM 也在不断迭代升级。

在数据质量方面,metaAI 将其收集到的人类偏好数据与一些开源数据集,如 Anthropic Helpful & Harmless, OpenAI Summarize, OpenAI WebGPT, StackExchange, Stanford Human Preferences, 和 Synthetic GPT-J 做了对比。想详细了解的小伙伴可以到原论文查看,这里不再赘述。

训练一个奖励模型 (RM, Reward Model)

奖励模型接受模型的回答及其对应的提示作为输入,并返回一个实数(不一定介于 (0,1))表示模型输出的质量。如果将 Llama 2-Chat 看作一个 agent,那么 RM 的输出即是这个 agent 获得的 reward。metaAI 独立地训练两个 RM:一个用于评估 helpfulness,另一个用于评估 safety。

为了使 RM 和对话模型 (LLaMA2-Chat) “具备相同的知识”,metaAI 将两个 RM 初始化成经过预训练的 llama2 的一个 checkpoint。也就是说,RM 采用与 llama2 完全相同的架构和训练参数,而仅 将模型输出头从预测 next-token 头替换为回归头(就如下面公式展示的)。

$$\underset{\text{Next-Token head}}{[H, W]} \rightarrow \underset{\text{Regression head}}{[H, 1]}$$

这样,RM 将会输出一个实数值,作为该“提示+回答”对的质量评估。metaAI 并不要求将质量评估回归到精确的数值上,而采用了 排序损失函数。该损失函数适用于希望模型输出更加关注值的大小关系,而非具体数值的情况。同时,metaAI 引入了 margin 来衡量输出值间的大小差异程度,即“如大,有多大?如小,有多小?”。训练 RM 所使用的损失如下:

$$\mathcal{L}_{\text{ranking}} = -\log( \sigma ( r_\theta (x, y_c) - r_\theta (x, y_r) - m(r) ) )$$

其中,$\sigma$ 是 sigmoid 函数,用于将最终数值压缩到 (0,1) 之间。$r_\theta$ 即给定参数 $\theta$ 的 RM。$x$ 是提示词,而 $y_c$ 和 $y_r$ 则分别是标注员选择 (chosen) 和拒绝 (reject) 的回答。$m(r)$ 是一个离散函数,它由标注员给出的标签而唯一确定,如下表。

Significantly Better Better Slightly Better Negligibly Better/Unsure
Margin Small 1 2/3 1/3 0
Margin Large 3 2 1 0

该损失函数将会引导 RM 给标注员 chosen 的回答相对更高的分数,而给标注员 reject 的回答相对更低的分数。并且由于 margin 的引入,RM 会倾向于给获得更高级别标签的回答以更高分数。训练好的 RM 就相当于一个导师,它将告诉对话模型“如何说话才能更被人类偏爱”。metaAI 对其训练的 RM 的评估已在原论文给出,这里不再详述。

所使用的强化学习算法

metaAI 执行 RLHF 时主要用了两个算法: Proximal Policy Optimization(PPO)Rejection Sampling fine-tuning (RSF) 。前者是一个通用的 RLHF 的范式。后者则是在 RLHF 迭代早期时,模型尚未成熟所使用的方法。

Rejection Sampling fine-tuning 亦即 拒绝采样微调 。给定 prompt,它经由具有不同 Temperature 参数的 LLaMA2-chat 得到一组可能的回答。RSF 从这些回答中选取 RM 得分最高的前 N 个样本,并用这些样本对 LLaMA2-Chat 做一次小规模的 SFT (这种方法有点像遗传算法) 。 在 RLHF 的前三次迭代 (v1 to v3) 中,LLaMA2-Chat 只通过 RSF 学习,直到 v4 才引入 PPO 算法。

PPO,又称 临近策略优化 算法,是一种 Actor-Critic 算法。如果你不清楚什么是 PPO,你可以查看这篇博客。LLaMA2-Chat 所采用的 PPO 方法如下公式所示:

  • ${\arg \underset{\pi_{\theta}}{\max}} J(\theta) \quad \text{where.} \quad J(\theta) = \mathbb{E}_{p \sim \mathcal{D}, g \sim \pi _{\theta}}[ R(g | p) ]$

    • 在数据集 $\mathcal{D}$ 上采样提示 $p$,并让模型生成一定数量的内容 $g$。
    • $R(g|p)$ 表示对于提示 $p$,回答为 $g$ 的 reward。
    • 目的是使 $J(\theta)$ 最大,其中唯一可变的策略 $\pi$ 的参数 $\theta$。
  • $R(g|p) = \hat{R} _{c}(g|p) - \beta D _{KL} (\pi _\theta (g|p) || \pi _0 (g|p))$

    • $\hat{R} _{c}(g|p)$ 是优势函数,它的计算在之后解释。
    • $\beta$ 是惩罚因子,用于控制对 $D _{KL}$ 的重视程度。
    • $D _{KL}$ 及 KL 散度,用于衡量两个分布的差异程度。
    • KL 散度的存在限制了参数更新的“力度”,使新参数尽量不迥异于原始参数。
  • $\begin{aligned} &\hat{R} _{c}(g|p) = \mathbf{WHITEN}(\mathbf{LOGIT} (R _c (g|p))) \\ &R _c (g|q) = \begin{cases} R _s (g|p) & 如果 p 安全或 R _s (g|p) \lt 0.15 \\ R _h (g|p) & 其他情况下. \end{cases} \end{aligned}$

    • $\hat{R} _{c}(g|p)$ 由 $R _c(g|p)$ 经过 logit 变化和白化处理得到。
    • $R _{c}(g|p)$ 是分段函数:若提示词安全或其安全得分 $R _{s}(g|p)$ 小于 0.15,则为安全得分。
    • 否则,$R _{c}(g|p)$ 就是有用性得分 $R _{h}(g|p)$。

最终,我们应当对 $-J(\theta)$ 求其梯度 $-\nabla J(\theta)$,并通过梯度下降的方法优化。进行梯度下降时所使用的优化器、学习率,设置的迭代次数、轮次、采样数目等在此处略去不提。想要深入了解的童鞋可以自行到原论文去看!

通过 GAtt 保持对话过程的一致性

GAtt 又称 “幽灵注意力” (Ghost Attention) 👻,它的引入主要是为了使模型在对话过程中保持前后话题的一致性。下图解释了什么是多轮对话的一致性:左图中,robot 在新的对话中“忘记了”要用 emoji 作答,而在右图中则始终遵循这一规则。

多轮对话的一致性

幽灵注意力的实现,是通过 采样那些保持对话过程一致的样例 来对模型进行微调。为了得到这些样例,在采样时,给多轮对话中用户输入的所有提示语句都加上前缀 instruction,保证模型每次对话时都能知道它应当遵循什么规则。而在微调时,则将这些前缀剔除。然而,这样做无法保证模型的输出和采样时一致,因而在计算损失的时候,需要掩盖当前句子之前的损失。

多模态?来看看 LLaMA3 的表现

最后的最后,我们来看一下 LLaMA3 在多模态的表现,包括:图像识别 (Image Recognition)视频识别 (Video Recognition)语音理解和生成领域 (Speech Understanding & Generation)

LLaMA3:图像、视频、语音多模态框架

图像识别 (Image Recognition)

主要关注 LLaMA3 在 自然图像理解文本理解图表理解多模态推理 等任务中的图像理解能力。用到的数据集包括:MMMU、VQAv2、AI2 Diagram、ChartQA、TextVQA、DocVQA。取得的成绩如下表所示:

Llama 3-V 8B Llama 3-V 70B Llama 3-V 405B GPT-4V GPT-4o Gemini 1.5 Pro Claude 3.5
MMMU (val, CoT) 49.6 60.6 64.5 56.4 69.1 62.2 68.3
VQAv2 (test-dev) 78.0 79.1 80.2 77.2 -- 80.2 --
AI2 Diagram (test) 84.4 93.0 94.1 78.2 94.2 94.4 94.7
ChartQA (test, CoT) 78.7 83.2 85.8 78.4 85.7 87.2 90.8
TextVQA (val) 78.2 83.4 84.8 78.0 -- 78.7 --
DocVQA (test) 84.4 92.2 92.6 88.4 92.8 93.1 95.2

视频识别 (Video Recognition)

在视频识别方面,metaAI 使用了如下的数据集:

  • PerceptionTest:评估模型 时间推理 (temporal reasoning) 的能力,关注模型的记忆、抽象等技能,包含描述、预测、解释等推理类型
  • NExt-QA:与前一个类似,但更关注开放领域的问题
  • TVQA:评估模型 复合推理 (composition reasoning) 的能力
  • ActivityNet-QA:评估模型对长视频总结、理解的能力
Llama 3-V 8B Llama 3-V 70B Gemini 1.0 Pro Gemini 1.0 Ultra Gemini 1.5 Pro GPT-4V GPT-4o
PerceptionTest (test) 53.8 60.8 51.1 54.7 -- -- --
TVQA (val) 82.5 87.9 -- -- -- 87.3 --
NExT-QA (test) 27.3 30.3 28.0 29.9 -- -- --
ActivityNet-QA (test) 52.7 56.3 49.8 52.2 57.5 -- 61.9

语音理解与生成 (Speech Understanding & Generation)

在语音理解方面,关注 语音识别 (speech recognition)语音翻译 (speech translate)语音问答 (spoken question answer) 。在语音生成方面,主要评估使用 LLaMA3 嵌入向量的 逐字流输入模型 (token-wise input streaming model) 在规范化文本和写韵文这两个任务上的表现。

语音识别 (Speech Recogniton)

Llama 3 8B Llama 3 70B Whisper SeamlessM4T v2 Gemini 1.0 Ultra Gemini 1.5 Pro
MLS (English) 4.9 4.4 6.2 (v2) 6.5 4.4 4.2
LibriSpeech (test-other) 3.4 3.1 4.9 (v2) 6.2 -- --
VoxPopuli (English) 6.2 5.7 7.0 (v2) 7.0 -- --
FLEURS (34 languages) 9.6 8.2 14.4 (v3) 11.7 -- --

语音翻译 (Speech Translate)

Llama 3 8B Llama 3 70B Whisper v2 SeamlessM4T v2
FLEURS (33 lang. → English) 29.5 33.7 21.9 28.6
Covost 2 (15 lang. → English) 34.4 38.8 33.8 37.9

语音问答 (Spoken Question Answer)

语音对话:LLaMA3 在多语种及长对话方面的表现

规范化文本和写韵文 (TN & PM)

Model Context Accuracy
Without Llama 38B 3 73.6%
Without Llama 38B 88.0%
With Llama 38B 3 90.7%

规范化文本 (TN) 的准确率: 比较了使用/不使用 LLaMA 嵌入的模型,并使用不同的 right-context 值。

Model Preference
PM for Llama 38B 60.0%
Streaming phone-only baseline    40.0%
Model Preference
PM for Llama 38B 63.6%
Non-streaming phone-only baseline 36.4%

写韵文 (PM) 评估: 上例为带 llama38B 嵌入的 PM 与流式模型的对比;下例为与非流式模型的对比。

最后的最后:让我们简单总结下 metaAI 的技术路线

自 OpenAI 的 ChatGPT 诞生以来,大模型领域的更新换代愈发频繁。这一趋势不仅得益于各种技术的蓬勃涌现,更重要的是,开发大语言模型的路径已逐渐清晰并趋于成熟。若一家公司有意打造专属的大模型,很可能也能遵循这一明确路径迅速推出自己的产品。具体而言,该路径包含以下几个关键步骤:

  1. 通过各种手段优化注意力机制,核心有两方面:增强模型的 推理能力推理速度
  2. 利用强化学习或其他先进技术,进一步提升模型的对话交互能力,如 安全性有用性上下文一致性
  3. 实现模型的多模态融合,以拓宽其应用场景和增强功能多样性,包括 图像视频语音 的。

LLaMA 系列模型从一代到三代的演进,便大致遵循了上述发展路径。 可以预见,在未来的一段时间内,该路线是通用且火热 🔥🔥🔥 的。


Llama1-3 🦙:从一道美团大模型面试题讲起
http://example.com/2025/01/09/llama123/
Author
LazyPool
Posted on
January 9, 2025
Licensed under