用通俗易懂的方式讲解:万字长文带你入门大模型

告别2023,迎接2024。大模型技术已成为业界关注焦点,你是否也渴望掌握这一领域却又不知从何学起?

本篇文章将特别针对入门新手,以浅显易懂的方式梳理大模型的发展历程、核心网络结构以及数据微调等关键技术。

如果你在阅读中收获良多,期待你能积极地通过点赞、转发与收藏给予支持,让更多对此领域感兴趣的朋友能够共享这份学习资源,共同踏入大模型技术的广阔天地。

大模型发展历史&&相关基础知识介绍

图片
该图从左到右 基于 传统的词向量模型以灰色线显示:decoder-only 模型在蓝色分支,encoder-only 模型在粉色分支,encoder-decoder 模型在绿色分支。模型在时间线上的垂直位置表示它们的发布日期。开源模型由实心方块表示,而闭源模型由空心方块表示。右下角的堆积条形图显示了各公司和机构的模型数量。

国内开源大模型:清华: chatglm系列; 阿里: Qwen系列; 百川: baichuan 零一万物; vivo: BlueLM-7B; 智源: Aquila2-70B; 上海AI实验室:InternLM 昆仑万维: Skywork; meta: llama; google: gemini; openai: chatgpt

通俗易懂讲解大模型系列

技术交流

建了AIGC大模型技术交流群! 想要学习、技术交流、获取如下原版资料的同学,可以直接加微信号:mlc2060。加的时候备注一下:研究方向 +学校/公司+CSDN,即可。然后就可以拉你进群了。

方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2060,备注:来自CSDN + 技术交流

在这里插入图片描述

Transformers网络模型介绍

图片

2017年,Google机器翻译团队发表的《Attention is All You Need》https://arxiv.org/pdf/1706.03762.pdf; 首次提出基于transformers自注意机制来实现seq2seq任务,被视为开山鼻祖。

网络结构主要分为下面几块;

  • 位置编码模块;

  • 多头注意力机制模块;

  • 前馈网络结构(FFN)模块;

  • 解码器模块;

位置编码总结

绝对位置编码

Sinusoidal位置编码是谷歌在Transformer模型中提出的一种绝对位置编码,它的形式如下,其中 表示词向量的维度, 表示位置索引, 和 表示位置向量的分量索引;

例如 和 分别表示位置的位置向量的第 和第 个分量:

图片

Sinusoidal位置编码的每个分量都是正弦或余弦函数,所有每个分量的数值都具有周期性。Sinusoidal位置编码还具有远程衰减的特点。对于两个相同的词向量,如果它们之间的距离越近,则他们的内积分数越高,反之则越低。如下图所示,我们随机初始化两个向量x1和x2,pos的位置从0开始逐步变大,依次计算x1和x2之间的内积。我们发现随着x1和x2的相对距离的增加,它们之间的内积分数震荡衰减。

!pip install -q mplfonts
!pip install -q mpl-font
!mplfonts init
from mplfonts import use_font
use_font('Noto Sans Mono CJK SC')
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
import mpl_font.noto

class SinPositionEncoding(nn.Module):
    def __init__(self, max_sequence_length, d_model, base=10000):
        super().__init__()
        self.max_sequence_length = max_sequence_length
        self.d_model = d_model
        self.base = base
    def forward(self):
        even_i = torch.arange(0, self.d_model, 2).float()
        odd_i = torch.arange(1, self.d_model, 2).float()
        position = torch.arange(self.max_sequence_length, dtype=torch.float).reshape(-1, 1)
        even_pe = torch.sin(position / torch.pow(self.base, even_i/self.d_model))
        odd_pe = torch.cos(position / torch.pow(self.base, (odd_i-1)/self.d_model))
        stacked = torch.stack([even_pe, odd_pe], dim=2)
        return torch.flatten(stacked, start_dim=1, end_dim=2)
seq_len = 80
d_model = 128
spe = SinPositionEncoding(max_sequence_length=seq_len, d_model=d_model)()
print(spe.shape)

fig,ax =plt.subplots(1,1,figsize=(6,10),dpi=120)
im1= ax.imshow(spe,vmin=spe.min(), vmax=spe.max(),cmap=plt.cm.rainbow)
# add space for colour bar
fig.subplots_adjust(right=0.85)
cbar_ax = fig.add_axes([0.86, 0.35, 0.04, 0.28])
plt.colorbar(im1, cax=cbar_ax)
plt.show()
plt.close(fig)
torch.Size([80, 128])

图片

fig,axes =plt.subplots(1,2,figsize=(15,6),dpi=120)
out = torch.matmul(spe,spe.T)
print(out.shape)
# 不同位置的位置编码点积可视化。
im1 = axes[0].imshow(out ,vmin=out.min(), vmax=out.max(),cmap=plt.cm.cool)
fig.subplots_adjust(right=0.85)
cbar_ax = fig.add_axes([0.45, 0.30, 0.02, 0.5])
fig.colorbar(im1, cax=cbar_ax)
axes[0].set_title("序列长度80,编码向量128,不同位置的位置编码点积可视化")

# 远程位置衰减
base = 40
seq_len = 80
d_model = 128
color_list=['#55B7E6','#193E8F','#E53528','#F09739']
for index,dim in enumerate([32, 64,128, 256][::-1]):
    spe = SinPositionEncoding(max_sequence_length=seq_len, d_model=dim)()
    k_list = [-index for index in range(1, base)][::-1] + [index for index in range(base)]
    value = [torch.matmul(spe[base-k],spe[base+k].T ).tolist() for k in k_list]
    axes[1].plot(k_list, value, label='dim_%d'%dim,color=color_list[index])
axes[1].legend()
axes[1].set_title("两向量内积和向量之间的相对位置趋势图呈现远程位置衰减")
axes[1].set_xlabel("两向量直接的位置距离")
plt.show()
plt.close(fig)
torch.Size([80, 80])

图片

Sinusoidal位置编码中的正弦余弦函数具备周期性,并且具备远程衰减的特性,所以理论上也具备一定长度外推的能力。

旋转位置编码(RoPE)

可以看到,RoPE形式上和Sinusoidal位置编码有点相似,只不过Sinusoidal位置编码是加性的,而RoPE可以视为乘性的。在θi 的选择上,我们同样沿用了Sinusoidal位置编码的方案,即,它可以带来一定的远程衰减性。

  • Transformer升级之路:2、博采众长的旋转式位置编码

  • https://spaces.ac.cn/archives/8265/comment-page-1

#position_id = m-n 相对位置;
import numpy as np
def s(position_id, d=128):
    theta_i = lambda i: 10000**(-2*i/d)
    return np.mean([np.abs(np.sum(np.exp(1j*position_id*theta_i(np.arange(0, j))))) for j in range(0, d//2)])
seq_len = np.arange(256) # 
ys = [s(x) for x in seq_len]
# # # #从图中我们可以可以看到随着相对距离的变大,内积结果有衰减趋势的出现
plt.plot(seq_len, ys, c='#55B7E6')

plt.show()

图片

基于attention map的位置编码(Alibi)

使用正弦位置编码的transformer的外推能力非常弱。虽然旋转位置编码比正弦方法有所改进,但仍未达到令人满意的结果。

为了解决长度外推的问题,作者提出了一种更简单、更有效的位置方法,即具有线性偏置的注意力ALiBi。ALiBi不向词嵌入添加位置嵌入,相反,它通过与距离成比例的惩罚来偏置query-key注意力分数。

在这里插入图片描述

import math
import torch
from torch import nn

def get_slopes(n_heads: int):
    
    n = 2 ** math.floor(math.log2(n_heads))
    m_0 = 2.0 ** (-8.0 / n)
    m = torch.pow(m_0, torch.arange(1, 1 + n))

    if n < n_heads:
        m_hat_0 = 2.0 ** (-4.0 / n)
        m_hat = torch.pow(m_hat_0, torch.arange(1, 1 + 2 * (n_heads - n), 2))
        m = torch.cat([m, m_hat])
        
    return m


@torch.no_grad()
def get_alibi_biases(n_heads: int, mask: torch.Tensor):

    m = get_slopes(n_heads).to(mask.device)
    seq_len = mask.size(0)
    distance = torch.tril(torch.arange(0, -seq_len, -1).view(-1, 1).expand(seq_len, seq_len))
    print(distance)

    return distance[:, :, None] * m[None, None, :]

seq_len = 10
n_heads = 8

m = get_slopes(n_heads)
print("input shape:", m.shape)

alibi_biases = torch.zeros(seq_len,seq_len)
for j in range(1,seq_len):
    for i in range(j, seq_len):
        alibi_biases[i, i - j] = -j
print(alibi_biases)

print(alibi_biases[:, :, None].shape, m[None, None, :].shape)
output =  alibi_biases[:, :, None] * m[None, None, :]
output.shape
input shape: torch.Size([8])
tensor([[ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [-1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [-2., -1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [-3., -2., -1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [-4., -3., -2., -1.,  0.,  0.,  0.,  0.,  0.,  0.],
        [-5., -4., -3., -2., -1.,  0.,  0.,  0.,  0.,  0.],
        [-6., -5., -4., -3., -2., -1.,  0.,  0.,  0.,  0.],
        [-7., -6., -5., -4., -3., -2., -1.,  0.,  0.,  0.],
        [-8., -7., -6., -5., -4., -3., -2., -1.,  0.,  0.],
        [-9., -8., -7., -6., -5., -4., -3., -2., -1.,  0.]])
torch.Size([10, 10, 1]) torch.Size([1, 1, 8])

torch.Size([10, 10, 8])

位置编码的类型需要指定为float32

a = torch.tensor(255,dtype=torch.bfloat16)
b = a + 1
c = a + 2
d = a + 3
e = a + 4
a,b,c,d,e
(tensor(255., dtype=torch.bfloat16),
 tensor(256., dtype=torch.bfloat16),
 tensor(256., dtype=torch.bfloat16),
 tensor(258., dtype=torch.bfloat16),
 tensor(260., dtype=torch.bfloat16))
# self.inv_freq.dtype == torch.bfloat16 when bfloat16 is enabled during training
t = torch.arange(4096, dtype=torch.float32)
plt.scatter(t[-100:], t[-100:].to(torch.bfloat16).float(),s=1,c=t[-100:],cmap=plt.cm.cool)
plt.xlabel('position in float32')
plt.ylabel('position in bfloat16')
Text(0, 0.5, 'position in bfloat16')

在这里插入图片描述

从上图可以看出使用bfloat16和float16进行位置编码的时候,容易出现位置碰撞,因此位置编码需要指定float32类型

注意力机制(MHA,MQA,GQA)

MHA(Multi-head Attention)是多头注意力机制的标准形式,其中包括h个Query、Key和Value矩阵。

MQA(Multi-Query Attention)是多查询注意力的一种变体,专为自回归解码设计。与MHA不同,MQA让所有头共享同一份Key和Value矩阵,而每个头仅保留独立的Query参数。这种设计显著减少了Key和Value矩阵的参数数量,提高了计算效率。

GQA(Grouped-Query Attention)是另一注意力机制,它将查询头分为G组。每组共享一个Key和Value矩阵。GQA-G表示具有G组的GQA,而GQA-1相当于一个单一组,因此其Key和Value矩阵与MQA相同。相反,GQA-H具有与头数相等的组,等同于MHA。GQA在MHA和MQA之间提供了一个折衷方案。

总体而言,MHA、MQA和GQA在处理输入数据和生成输出时采用不同的策略。MHA注重并行处理以提高表示能力,MQA侧重于减少参数数量以增强计算效率,而GQA则通过分组查询来平衡这两方面的需求。

在这里插入图片描述

GQA:Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints

FlashAttention总结

FlashAttention是解决Transformer计算速度慢和存储占用高问题的关键技术。不同于其他Efficient Transformer仅关注降低FLOPS,FlashAttention将优化重心放在降低存储访问开销(MAC)上。

FLOPS直接决定了模型核心计算的密集程度,从而影响计算速度。学术界已经研发出许多技巧来降低Transformer的FLOPS,而由这些技巧改进得到的模型被称为Efficient Transformer。

然而,大多数Efficient Transformer仅关注FLOPS。FlashAttention的作者们发现,尽管这些Efficient Transformer能有效降低FLOPS,但计算速度并未显著提升。主要原因是计算速度不仅与FLOPS有关,还与MAC紧密相连。尤其在计算效率已很高的情况下,MAC的重要性更加凸显。MAC的开销主要来自两个方面:从存储中读取数据和向存储中写入数据。在GPU中,当需要进行计算时,需要从显存中读取数据并由计算单元进行处理。计算完成后,再将结果写回到显存中。

  • FlashAttentionV1: Fast and Memory-Efficient Exact Attention with IO-Awareness

  • FlashAttentionV2: Faster Attention with Better Parallelism and Work Partitioning

在这里插入图片描述

Feed Forward层

Feed Forward层一般是有2层简单的前馈神经网络层组成。例如chatglm3_6b的ffn层网络定义;

在这里插入图片描述

MoE介绍

这两天 Mistral 7B /w 8 experts 的 checkpoint 释出,彻底引爆了 AI 社区对 MoE 的热情。本质来说,MoE 是一种高效的 scaling 技术,用较少的 compute 实现更大的模型规模,从而获得更好的性能。MoE工作的主要动机:保持相同训练和推理资源的同时,通过增加模型的体积代价来提升模型学习效果。

Scaling laws for neural language models揭示了模型规模、数据集大小以及所需计算资源之间的紧密联系,并指出在数据集容量不变的前提下,最大限度地增加模型参数数量是充分利用计算力的高效策略。这里的“规模”主要指模型所包含的参数总量及其所需的计算复杂度,通常情况下,参数数量的增加会伴随着计算需求的增长。

而MoE(Mixture of Experts)结构则通过引入专家机制实现了一次关键性的解耦操作,它能够在保持所需计算量相对稳定的基础上显著提升模型参数的数量。因此,采用MoE架构的模型性能提升符合scaling law规律,即使在不额外增加计算成本的情况下也能借助更多的参数来提升模型表现。

图片

其中代表作品为Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity

作者观点指出,FFN(全连接前馈神经网络)在某种程度上可以类比为键值对(KV对),其中K表示文本中的模式信息,而V则代表了这些模式的分布情况。随着网络层次的加深,模型能够学习到愈发复杂的模式特征;但同时,也发现越靠近输出层的部分,其学习能力往往显得相对不足。

基于这两个观察点,可以提出这样一个假设:这种学习不充分的现象可能是由于模型容量的局限性所导致的,尤其是对于较深层而言,对模型参数容量的需求更为显著。为了验证这一假设,一个相关的实验现象是,当仅使用一个MoE(专家混合)层时,将其置于网络结构的更后位置往往可以获得更好的性能表现。

此外,从直观理解上,复杂模式的数量级理论上可能远超过简单模式,呈现出指数增长的特性,这就意味着需要更多的参数来适应和拟合这些多样且复杂的模式组合。

MoE结构介绍

Mixture of Experts (MoE) 模型是一种创新的机器学习架构,它通过整合多个专业化“专家”网络以提升模型的整体效能和效率。该模型的基本理念在于将复杂的任务划分为多个细分子任务,然后交由各个专门设计的专家网络(即小型神经网络)来分别处理,这些专家网络可能包括全连接层、卷积层等多种类型。

MoE 模型的核心组件主要包括:

  • 门控机制:作为 MoE 模型的关键组成部分,门控机制负责根据输入数据的特征动态决定每个数据应由哪个或哪些专家网络进行处理,从而优化模型的学习与预测性能。

  • 专家网络:这些是模型中实际执行数据处理的单元。每一个专家网络都经过训练,专注于处理特定类型的数据或任务。在 MoE 架构中,可配置任意数量的专家网络,且每个专家都可以是一个独立构建的神经网络。

  • 融合层:融合层的主要职责是集成来自所有专家网络的输出结果。基于门控机制的任务分配及各专家的输出响应,融合层综合生成最终的模型输出。

MoE 模型的优势体现在其高度的灵活性与扩展性上。由于能够动态调整专家网络的数量和类型,MoE 能够有效应对大规模复杂数据集的挑战。此外,借助并行处理不同的专家网络,MoE 模型还能显著提升计算效率。在实际应用情境下,MoE 模型广泛应用于对计算资源需求较大的任务,如自然语言处理、图像识别以及复杂的预测问题等。通过将大型难题拆解为更小、更易管理的部分,MoE 模型可以提供更为高效精准的解决方案。

相关参考文档:https://zhuanlan.zhihu.com/p/671873012

解码流程

目前主流的LLM模型都是基于tansformers的decoder网络结构。基于这类的生成式generative模型的推理过程很有特点,都采用“Next Token Prediction,NTP”方式。我们给一个输入文本,模型会输出一个回答(长度为N),其实该过程中执行了N次推理过程。即GPT类模型一次推理只输出一个token,输出token会与输入tokens 拼接在一起,然后作为下一次推理的输入,这样不断反复直到遇到终止符。其中会涉及解码方式。

解码方法

主要分为三大类;greedy search(贪心搜索), beam search, sample(采样)。

  • greedy search(贪心搜索);
  1. 只考虑当前词对应的最大概率,忽略整体的最大概率。
  • beam search:
  1. 在解码过程中,它始终坚持存储当前概率最高的num_beams个完整序列候选(不仅限于单个词,而是充分考虑了词组和短语的整体概率);

  2. 这种策略尤其适用于诸如句子级机器翻译或短文本生成等任务场景;

  3. beam search在实践中存在明显的复读现象,即有可能生成重复的n-gram结构,尽管直接禁止重复n-gram的方法可以缓解这一问题,但这种方法过于简单粗暴,可能会影响整体生成效果;

  4. 本质上,beam search通过选取top n的高概率候选结果进行迭代扩展,因此,在追求高概率路径的同时,生成的回复往往缺乏新颖性和意外性,难以带给用户惊喜。

  • sample(采样解码):
  1. 可以通过温度(temperate)参赛修改,让原本高概率的概率更高,低概率的概率更低,这样就可以尽量采样出高概率的词,在随机性和surprise之间做trade-off。

  2. 注意temperate越大越随机,如果temperate=0,就没有随机了,就是贪心算法。

如果使用huggingface库的model.generate方法来预测输出内容。则有下面的几种参数配置;

  • 如果num_beams=1且do_sample=False,则使用贪婪搜索,调用~generation.GenerationMixin.greedy_search。

  • 如果penalty_alpha>0且top_k>1,则使用对比搜索,调用~generation.GenerationMixin.contrastive_search。

  • 如果num_beams=1且do_sample=True,则使用多概率采样,调用~generation.GenerationMixin.sample。

  • 如果num_beams>1且do_sample=False,则使用beam搜索,调用~generation.GenerationMixin.beam_search。

  • 如果num_beams>1且do_sample=True,则使用beam搜索多概率采样,调用~generation.GenerationMixin.beam_sample。

  • 如果num_beams>1且num_beam_groups>1,则使用分群束搜索,调用~generation.GenerationMixin.group_beam_search。

  • 如果num_beams>1且constraints!=None或force_words_ids!=None,则使用约束束搜索,调用~generation.GenerationMixin.constrained_beam_search。

例如用chatglm2_6b采用多概率采样的方式输出
%%time 
query = '现有鸡兔22只,共有48只脚,请问鸡比兔多了几只?'
history= []
gen_kwargs = {"max_length": 1024, "num_beams": 1, "do_sample": True, "top_p": 0.8,
              "temperature": 0.1, "logits_processor": None}
inputs = model.build_inputs(tokenizer, query, history=history)
print(inputs)
# {'input_ids': tensor([[64790, 64792,   790, 30951,   517, 30910, 30939, 30996,    13,    13,
#          54761, 31211, 39701,    13,    13, 55437, 31211]], device='cuda:0'), 
#  'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0'),
#  'position_ids': tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16]], device='cuda:0')}

print(model.process_response(tokenizer.decode(inputs['input_ids'][0])))

outputs = model.generate(**inputs, **gen_kwargs)
outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):]
print(outputs)
# [64790, 64792, 790, 30951, 517, 30910, 30939, 30996, 13, 13, 54761, 31211, 39701, 13, 13, 55437,
# 31211, 36474, 54591, 243, 162, 148, 142, 31404, 33030, 34797, 42481, 22011, 10461, 30944, 30943,
# 30941, 30978, 30949, 31123, 48895, 35214, 54622, 31123, 32616, 39905, 31901, 31639, 31155, 2]

response = tokenizer.decode(outputs)
response = model.process_response(response)
response
    {'input_ids': tensor([[64790, 64792,   790, 30951,   517, 30910, 30939, 30996,    13,    13,
             54761, 31211, 34470, 55672, 56766, 30943, 30943, 54768, 31123, 33684,
             30972, 30973, 54768, 55673, 31123, 42693, 55672, 54703, 56766, 33851,
             55013, 54768, 30987,    13,    13, 55437, 31211]], device='cuda:0'), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0'), 'position_ids': tensor([[ 0,  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]], device='cuda:0')}
    [Round 1]
    
    问:现有鸡兔22只,共有48只脚,请问鸡比兔多了几只?
    
    答:

    CPU times: user 6.23 s, sys: 478 ms, total: 6.71 s
    Wall time: 6.74 s

    '假设鸡有 x 只,兔子有 22 - x 只。\n\n每只鸡有 2 只脚,每只兔子有 4 只脚。因此,所有鸡和兔子的脚的总数为:\n\n2x + 4(22 - x) = 48\n\n化简得:\n\n2x + 88 - 4x = 48\n\n-2x = -40\n\nx = 20\n\n因此,有 20 只鸡,22 - 20 = 2 只兔子。\n\n所以,鸡比兔子多了 20 - 2 = 18 只。'

限制采样Trick总结

主要有top_p&&top_k等参数

  • top_p:每个时间步,按照字出现的概率由高到底排序,当概率之和大于top-p的时候,就不取后面的样本了。然后对取到的这些字的概率重新归一化后,进行采样。参数:top_p (取值范围:0-1)。top-P采样方法往往与top-K采样方法结合使用,每次选取两者中最小的采样范围进行采样,可以减少预测分布过于平缓时采样到极小概率单词的几率。

  • top_k: 每个时间步,会保留topK个字,然后对topk个字的概率重新归一化,最后在重新归一化后的这K个字中进行采样。缺点:在分布陡峭的时候仍会采样到概率小的单词,或者在分布平缓的时候只能采样到部分可用单词。

  • Temperature:通过温度,控制每个词的概率分布曲线。温度越低,分布曲线越陡峭,越容易采样到概率大的字。温度越高,分布曲线越平缓,增加了低概率字被采样到的机会。参数:temperature(取值范围:0-1)设的越高,生成文本的自由创作空间越大;温度越低,生成的文本越偏保守。

  • 限制n-gram在生成结果中出现次数;

kv cache

在预测阶段,Transformer模型常采用KV cache技术以提高推理效率。具体而言,在Decoder进行逐词解码的过程中,若每生成一个token就需将已解码的所有tokens重新拼接为输入,并据此预测下一个token,则会导致大量的重复计算。

为此,在解码过程中,Transformer利用KV cache存储先前计算得到的Key和Value,这样一来,在后续生成步骤中,无需反复处理相同部分的输入数据,从而有效提升了模型推理速度并降低了计算成本。假如现在有一个任务是要写一首诗;

在解码过程中:

  • input: 写一首诗:output: 轻

  • input: 写一首诗:轻 output: 舟

  • input: 写一首诗:轻舟 output: 已

  • input: 写一首诗:轻舟已 output: 过

  • input: 写一首诗:轻舟已过 output: 万

  • input: 写一首诗:轻舟已过万 output: 重

  • input: 写一首诗:轻舟已过万重 output: 山

最终完整输出:轻舟已过万重山;

可以利用缓存的方式,把之前的计算结果(“写一首诗:轻舟已过万重”的K values | V values)缓存起来,下一次解码的时候直接利用缓存的结果,这样就可以大大加快解码的速度。

启用KV Cache后,Transformer的推理过程可以细分为两个核心阶段:

  • 预填充阶段:在生成首个输出token时,由于Cache为空,系统需要为每个Transformer层分别计算并初始化Key和Value缓存。这个阶段涉及大量的矩阵乘法(GEMM)操作,其FLOPs与未使用KV Cache的情况相仿,故推理速度相对较慢。

  • KV Cache利用阶段:自第二个输出token直至最后一个token的生成过程中,模型能够复用已填充好的Cache内容。此时,每一轮解码仅需读取先前存储的Key和Value,并将当前轮次新产生的Key、Value信息更新至Cache中。此阶段的FLOPs有所减少,原本的矩阵乘法(GEMM)操作转换为向量-矩阵乘法(GEMV),从而显著提升推理效率。这一阶段计算主要受内存访问限制(Memory-bound),相较于预填充阶段,推理速度有明显提升。

实现自定义的new_logits_processor添加违禁词

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, TextStreamer
from transformers.generation.logits_process import LogitsProcessor, LogitsProcessorList
from typing import List
import torch


class new_logits_processor(LogitsProcessor):
    """
    forbid_token_id_list是不让模型生成词语的id映射列表,对于这些抑制生成的词语,在自定义logits_processor时将其概率推向负无穷大即可。
    """
    def __init__(self, forbid_token_id_list: List[int] = None):
        self.forbid_token_id_list = forbid_token_id_list

    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
        for id_ in self.forbid_token_id_list:
            scores[:, id_] = -float('inf')
        return scores


# model_path = "THUDM/chatglm2-6b"
# tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# model = AutoModelForSeq2SeqLM.from_pretrained(model_path, trust_remote_code=True).to('mps')


def add_forbid_words():
    '''
    添加需要抑制的词语,这里简单添加了数字和几个词语进行对比
    :return:list
    '''
    forbid_words = []
    for i in range(10):
        forbid_words.append(tokenizer.convert_tokens_to_ids(str(i)))
    forbid_words.append(tokenizer.convert_tokens_to_ids("首先"))
    forbid_words.append(tokenizer.convert_tokens_to_ids("积极"))
    forbid_words.append(tokenizer.convert_tokens_to_ids("回答"))
    forbid_words.append(tokenizer.convert_tokens_to_ids("勇敢"))
    forbid_words.append(tokenizer.convert_tokens_to_ids("勇气"))
    return forbid_words


logits_processor = LogitsProcessorList()
logits_processor.append(new_logits_processor(add_forbid_words()))

streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
%%time

query = "列举出10个积极的词语:"

outputs = model.generate(tokenizer(query, return_tensors='pt').input_ids.to("cuda"),
    max_new_tokens=1024,
    logits_processor=logits_processor,  # 不开启注释即可
    streamer=streamer
)
decode_text = tokenizer.batch_decode(outputs, streamer=streamer)[0]
print(decode_text)
- 积极主动
- 乐观向上
- 自信
- 自律
- 诚实守信
- 乐于助人
- 勇于尝试
- 坚韧不拔
- 乐观开朗
- 团结一心
列举出10个积极的词语:

- 积极主动
- 乐观向上
- 自信
- 自律
- 诚实守信
- 乐于助人
- 勇于尝试
- 坚韧不拔
- 乐观开朗
- 团结一心
CPU times: user 1.67 s, sys: 25.5 ms, total: 1.69 s
Wall time: 1.68 s

网络模型微调总结

数据格式

数据集作为驱动大模型效能提升的“三驾马车”之一,与模型结构和算力资源并驾齐驱,起着至关重要的决定性作用。在深度学习领域,丰富的高质量数据集如同基石一般,为构建强大、泛化的机器学习模型提供了必不可少的基础支撑。

一个大型的数据集不仅涉及庞大的数据规模,更重要的是其内在的多样性和全面性。它涵盖了多种场景、多元特征以及广泛标注的信息,能够有效帮助模型理解和学习复杂的规律及模式,从而提高模型在实际应用中的准确率和鲁棒性。同时,数据集的质量直接影响模型训练的效果,高质量的数据集能够显著减少噪声干扰,确保模型在训练过程中能更好地挖掘深层次的知识关联。

因此,在大模型的研发过程中,精心设计和持续优化的数据集构建策略是不可或缺的一环,这包括但不限于:数据采集的全面性、数据清洗的严谨性、数据增强的有效性以及针对特定任务进行的精细化标注等环节。只有当数据、模型和算力这三者达到高度协同与融合时,才能最大程度地发挥出人工智能技术的强大潜力。

多轮对话的数据格式

  • 常见的对话类型,带工具调用的类型 Firefly项目训练多轮对话模型时,采取了一种更加充分高效的方法。如下图所示,我们将一条多轮对话数据拼接之后,输入模型,并行计算每个位置的loss,只有Assistant部分的loss参与权重更新。

图片

图片

这种做法之所以可行,关键在于因果语言模型中采用的attention mask机制。以GPT为代表的因果语言模型,其核心特点在于其使用的注意力掩码是一个对角形式的矩阵。在该结构下,每个token在编码阶段仅能访问和考虑它之前出现的所有token信息,而无法获取后续token的内容。因此,在处理对话序列时,User1部分经过编码后的输出只能依赖于User1本身的文本内容,无法预测或参考其后面出现的文本(如Assistant1的回答)。相应地,对于User2部分,其编码结果能够基于已知的User1、Assistant1以及自身的文本内容来预测Assistant2的回应,以此类推。

通过这样的设计,对于整个对话序列,只需一次性输入模型,即可利用并行计算的优势,分别获得每个位置对应的logits值,进而用于计算loss及优化模型参数。这样既确保了模型预测的因果一致性,也大大提高了训练和推理效率。组成的对话格式如下

<s>user1</s>Assistant1</s>user2</s>Assistant2</s>...

chatglm2_6b的数据格式

[gMASK]sop[Round1]

问:你好

答:我是chatglm2助手

[Round2]

问:现有鸡兔22只,共有48只脚,请问鸡比兔多了几只?

答:

chatglm3_6b的数据格式

[gMASK]sop<|system|>[gMASK]sop
[gMASK]sop你是一位高级python程序员,按照用户输入的任务转换为对应的python代码,请以```的格式作为开始和结尾。<|user|>[gMASK]sop
[gMASK]你好<|assistant|>

微调(三步走)

  • 在深度学习中,微调是一种重要的技术,即通过训练部分参数来提高模型的性能。微调可以分为全微调和部分微调两种方法:

• 全微调(Full Fine-tuning):全微调是指对整个预训练模型进行微调,包括所有的模型参数。在这种方法中,预训练模型的所有层和参数都会被更新和优化,以适应目标任务的需求。这种微调方法通常适用于任务和预训练模型之间存在较大差异的情况,或者任务需要模型具有高度灵活性和自适应能力的情况。Full Fine-tuning需要较大的计算资源和时间,但可以获得更好的性能。

• 部分微调(Repurposing):部分微调是指在微调过程中只更新模型的顶层或少数几层,而保持预训练模型的底层参数不变。这种方法的目的是在保留预训练模型的通用知识的同时,通过微调顶层来适应特定任务。Repurposing通常适用于目标任务与预训练模型之间有一定相似性的情况,或者任务数据集较小的情况。由于只更新少数层,Repurposing相对于Full Fine-tuning需要较少的计算资源和时间,但在某些情况下性能可能会有所降低。

随着LLM模型参数量巨大,往往参数量在几十亿的参数,甚至更大。在消费级显卡中无法完成全量微调。PEFT(Parameter-Efficient Fine-Tuning)是hugging face开源的一个参数高效微调大模型的工具,里面集成了多种微调大模型的方法,可以通过微调少量参数就达到接近微调全量参数的效果,使得在GPU资源不足的情况下也可以微调大模型。

第一步:预训练微调

大模型首先会在大量的无标签数据上进行无监督的训练,其中预训练的最终目的是让模型学习到语言的统计规律和基础知识。得到的预训练模型一般称为基座模型base model.例如;baichuan2_13b_base, chatglm3_6b_base

第二步: 指令监督微调(SFT)

  • 参考论文

当前以 ChatGPT 为代表的预训练语言模型(PLM)规模变得越来越大,在消费级硬件上进行全量微调(Full Fine-Tuning)变得不可行。此外,为每个下游任务单独存储和部署微调模型变得非常昂贵,因为微调模型与原始预训练模型的大小相同。参数高效微调方法(Parameter-Efficient Fine-Tuning,PEFT)方法被提出来解决这两个问题,PEFT 可以使 PLM 高效适应各种下游应用任务,而无需微调预训练模型的所有参数。微调大规模 PLM 所需的资源成本通常高得令人望而却步。在这方面,PEFT 方法仅微调少量或额外的模型参数,固定大部分预训练参数,大大降低了计算和存储成本,同时最先进的 PEFT 技术也能实现了与全量微调相当的性能。

PEFT 方法可以分为三类,不同的方法对 PLM 的不同部分进行下游任务的适配:

  • Prefix/Prompt-Tuning:在模型的输入或隐层添加n个额外可训练的前缀 tokens(这些前缀是连续的伪 tokens,不对应真实的 tokens),只训练这些前缀参数;

  • Adapter-Tuning:将较小的神经网络层或模块插入预训练模型的每一层,这些新插入的神经模块称为 adapter(适配器),下游任务微调时也只训练这些适配器参数;

  • LoRA:通过学习小参数的低秩矩阵来近似模型权重矩阵的参数更新,训练时只优化低秩矩阵参数。

图片

下面将依次介绍几种常见的微调方法实现大模型的SFT过程。

LoRa方法介绍
  • lora论文

LoRA的本质是在原模型的基础上插入若干新的参数,称之为adapter。在训练时,冻结原始模型的参数,只更新adapter的参数。对于不同的基座模型,adapter的参数量一般为几百万~几千万。具体的原理如下

假设,作者认为参数更新过程中存在一个内在秩,对于权重可以通过低秩分解来表示参数更新,即

即最终的参数量由 降至 ;假设 , 则参数量为:

图片

图片

图片

图片

图片

AdaLoRa方法介绍

AdaLoRA基于SVD的形式参数化增量更新,这种基于SVD的参数化形式可以在规避SVD复杂的计算的同时高效裁剪不重要的奇异值,从而降低计算量。该方法提出的作者认为在一个模型中,不同模块拥有着不同的贡献,那么在使用LoRA时如果我们能够根据它们重要性的不同为不同的模块分配不同的秩,那么将会带来很多好处。首先,我们为重要性更低的模块分配更小的秩,那么将有效的减少模型的计算量。其次,如果我们能够为更重要的特征分配更大的秩,那么将能够更有效的捕捉特征的细节信息。这也就是AdaLoRA的提出动机。

假设,

其中,为的左右奇异向量,为的奇异矩阵。

图片

图片

图片

图片

QLoRa方法介绍

LoRA的本质是在原模型的基础上插入若干新的参数,称之为adapter。在训练时,冻结原始模型的参数,只更新adapter的参数。对于不同的基座模型,adapter的参数量一般为几百万~几千万。LoRA存在的问题:

  • 参与训练的参数量较少,解空间较小,效果相比全量微调有一定的差距。

  • 微调大模型成本高:对于上百亿参数量的模型,LoRA微调的成本还是很高。精度损失。

QLoRa相当于Quantization量化+LoRa(AdaLoRa)技术,具有下面的特点;

  • 4-bit NormalFloat:提出一种理论最优的4-bit的量化数据类型,优于当前普遍使用的FP4与Int4。

  • Double Quantization:相比于当前的模型量化方法,更加节省显存空间。每个参数平均节省0.37bit,对于65B的LLaMA模型,大约能节省3GB显存空间。Paged Optimizers:使用NVIDIA统一内存来避免在处理小批量的长序列时出现的梯度检查点内存峰值。

  • 增加Adapter:4-bit的NormalFloat与Double Quantization,节省了很多空间,但带来了性能损失,作者通过插入更多adapter来弥补这种性能损失。在LoRA中,一般会选择在query和value的全连接层处插入adapter。

  • 而QLoRA则在所有全连接层处都插入了adapter,增加了训练参数,弥补精度带来的性能损失。

QLoRA(int8+adalora)

图片

图片

图片

QLoRa(4bits+Adalora)

图片

图片

图片

P-tuningV2方法介绍
  • P-Tuning v2: Prompt Tuning Can Be Comparable to Finetuning Universally Across Scales and TasksP-tuning v2被设计用于生成和知识探索,但最重要的改进之一是将连续提示应用于预训练模型的每个层,而不仅仅是输入层。

图片

图片

图片

第三步: 基于RLHF的微调(DPO,PPO)

奖励模型(reward model)

奖励模型 (RM) 微调类似于第一阶段有监督微调 (SFT) 。但是,RM 和 SFT 微调之间存在几个关键差异:

  • 训练数据差异:对于 SFT 微调,数据是查询(query)和答案(answer)拼接在一起。然而,对于 RM 微调,每批数据由两个查询-答案对组成,即具有高分答案和低分答案的相同查询。这也导致了如下所述的第二个差异。

  • 训练目标差异:对于 RW,训练目标是 pairwise ranking score,即对于两个查询-答案对,RM 应该给更好的答案更高的分数。有多种方法可以实现这一目标。在DeepSpeed Chat的实现中,使用序列的结束标记或第一个填充标记作为聚合分数并比较它们。当然,也可以使用整个答案的平均分数作为替代。

reward模型本质就是一个句子级的分类,难点在于如何收集偏好数据,具体需要样本高质量,足够多:尽可能覆盖各种场景,例如不同长度,避免在RL阶段出现OOD。

图片

PPO算法介绍

LLM模型微调通常有三个步骤如下图DeepSpeedChat流程;

图片

分别为actor model、reference model、critic model、reward model(reward model是为了避免critic model的value变化太大)。其中actor model和reference model是同一个模型,来源于sft操作。critic model和reward model来源于rw模型训练。

DPO算法介绍

图片

斯坦福大学研究团队Direct Preference Optimization提出了一项名为Direct Preference Optimization(DPO)的创新算法,旨在探寻更简洁高效的大规模语言模型优化解决方案。与传统的强化学习方法不同,DPO能够直接对语言模型行为进行精准调控,无需复杂的强化学习过程。该算法巧妙地建立了奖励函数与最优策略之间的内在联系,将原本复杂的约束奖励最大化问题简化为一次性策略训练任务。

DPO的独特之处在于其不仅省去了对奖励模型的拟合步骤,还在微调过程中免除了从语言模型中采样或精细调整关键超参数的需求。实验结果显示,DPO在诸如情感调节、文本摘要和单轮对话等任务上表现出了与现有RLHF方法相当甚至更好的性能,成功实现了从人类偏好中有效学习。

作为一种隐式优化策略,DPO与当前RLHF方法共享相同的目标导向,但具备更高的实现便捷性和训练友好性。针对单纯基于相对概率可能导致模型性能下降的问题,DPO引入了动态权重机制来体现每个示例回复的重要性。尽管同样依赖于理论上的偏好模型以评估奖励函数与实际偏好数据的一致性,但DPO独树一帜地通过变量变换,将偏好损失直接定义为策略函数的一部分。因此,DPO能依据给定的偏好数据和模型生成的回复,仅利用简单的二进制交叉熵作为目标函数来进行策略优化。

相较于现有的基于人类反馈的方法,DPO在大语言模型微调时更加高效且直接。传统方法通常需要先拟合一个奖励模型到包含提示和人类偏好的数据集上,再运用对比学习找到最大化所学奖励的策略。而DPO则摒弃了这一复杂流程,仅通过直观的分类目标即可直接针对最符合人类偏好的策略进行优化,无需明确指定奖励函数或应用强化学习技术。

图片

实战教程

最后给大家介绍2个关于大模型应用的案例,分别是推理优化和注入新知识到大模型中。

chatglm2_6b分层加载权重推理(使用3G显存实现推理)

chatglm2_6b按照半精度加载到GPU中,大约会占12G左右。通过使用分层加载推理技术(重构chaglm2_6b模型的层权重加载架构,使用逐层加载推理释放来实现这一过程),只需要3g左右的内存既可实现6b模型按照半精度类型加载推理。

图片

图片

图片

few_shot examples进行SFT微调

给大模型新增知识,通过构造简单的几条样本注入大模型中。下面将展示微调前后的效果对比;

案例1展示;

图片

案例2展示;

图片

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值