Qwen代码层面解密

Instruct Model
“bos_token_id”: 151643,
“eos_token_id”: 151645, //instruct model的eos_token_id修改为<|im_end|>, base model本来都是<end_of_text>

解密1:ChatTemplate and Function Call

{%- if tools %} #是否存在工具类,在对话最开始提供
    {
  
  {- '<|im_start|>system\n' }}
    {%- if messages[0]['role'] == 'system' %}
        {
  
  {- messages[0]['content'] }}
    {%- else %}
        {
  
  {- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
    {%- endif %}
    {
  
  {- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
    {%- for tool in tools %}
        {
  
  {- "\n" }}
        {
  
  {- tool | tojson }}
    {%- endfor %}
    {
  
  {- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
{%- else %} #正常对话第一步走这个流程
    {%- if messages[0]['role'] == 'system' %}
        {
  
  {- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
    {%- else %}
        {
  
  {- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
    {%- endif %}
{%- endif %}
{%- for message in messages %} #遍历多轮对话信息
    {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
        {
  
  {- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
    {%- elif message.role == "assistant" %}
        {
  
  {- '<|im_start|>' + message.role }}
        {%- if message.content %}
            {
  
  {- '\n' + message.content }}
        {%- endif %}
        {%- for tool_call in message.tool_calls %}
            {%- if tool_call.function is defined %}
                {%- set tool_call = tool_call.function %}
            {%- endif %}
            {
  
  {- '\n<tool_call>\n{"name": "' }}
            {
  
  {- tool_call.name }}
            {
  
  {- '", "arguments": ' }}
            {
  
  {- tool_call.arguments | tojson }}
            {
  
  {- '}\n</tool_call>' }}
        {%- endfor %}
        {
  
  {- '<|im_end|>\n' }}
    {%- elif message.role == "tool" %}
        {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
            {
  
  {- '<|im_start|>user' }}
        {%- endif %}
        {
  
  {- '\n<tool_response>\n' }}
        {
  
  {- message.content }}
        {
  
  {- '\n</tool_response>' }}
        {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
            {
  
  {- '<|im_end|>\n' }}
        {%- endif %}
    {%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}  #一般需要add_generation_prompt=True,直到模型输出eos_token
    {
  
  {- '<|im_start|>assistant\n' }}
{%- endif %}

解密2: Qwen的tokenizer底层原理,word2id的映射关系

'<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n<|im_start|>user\nGive me a short introduction to large language model.<|im_end|>\n<|im_start|>assistant\n

['<|im_start|>', 'system', 'Ċ', 'You', 'Ġare', 'ĠQ', 'wen', ',', 'Ġcreated', 'Ġby', 'ĠAlibaba', 'ĠCloud', '.', 'ĠYou', 'Ġare', 'Ġa', 'Ġhelpful', 'Ġassistant', '.','<|im_end|>', 'Ċ', '|im_start|>', 'user', 'Ċ', 'Give', 'Ġme', 'Ġa', 'Ġshort', 'Ġintroduction', 'Ġto', 'Ġlarge', 'Ġlanguage', 'Ġmodel', '<|im_end|>','Ċ', '<|im_start|>', 'assistant', 'Ċ']

tensor([[151644,   8948,    198,   2610,    525,   1207,  16948,     11,   3465,553,  54364,  14817,     13,   1446,    525,    264,  10950,  17847,13, 151645,    198, 151644,    872,    198,  35127,    752,    264,2805,  16800,    311,   3460,   4128,   1614,     13, 151645,    198,151644,  77091,    198]])

疑问1:Ċ代表什么,似乎代表所有\n,空格等间隔符," You",“You”,"\nYou"的分词结果均不一致

tokenize分词结果解析:“i love You” [‘i’, ‘Ġlove’, ‘ĠYou’]
“i loveYou”[‘i’, ‘Ġlove’, ‘You’]
“I love\nYou”[‘I’, ‘Ġlove’, ‘Ċ’, ‘You’]

You的id为2610,而ĠYou为1446,不存在“ You”,"\nYou"的token,而分别用’Ċ’'Ġ’代替,tokenizer的分词流程和原理是什么呢?

首先tokenizer由normalizers,pre_tokenizers,model and post_tokenizers四部分组成,这部分基础可以参考huggingface-tokenizer doc

normalizer:NFC
NFC是“组合形式”(Canonical Composition),它将字符表示为其最“复合”的形式。
这意味着,如果一个字符可以通过Unicode中的组合规则由两个或更多的字符组合而成,那么在NFC中,这个字符就会被表示为一个单独的复合字符。
pre_tokenizer:Split and ByteLevel
通过Split组件,将文本分解成单词、缩写、数字和标点符号等基本单元。
通过ByteLevel组件,处理Unicode字符,确保多字节字符被正确识别。
model: ByteLevel BPE
post_processor: ByteLevel

code流程:PreTrainedTokenizer的tokenize方法会调用QwenTokenizer重写的_tokenize方法,处理BPE分词和special token的合并

解密3: tie_word_embeddings的实现

对于小参数模型,为了让中间的Decoder Layer占比提高,可以将nn.Embedding层和lm_head使用相同的权重矩阵
nn.Embedding是用于快速检索token_id->token_embedding的矩阵,形状为vocab_sizehidden_size
lm_head是token分类的预测头,形状为hidden_size
vocab_size

self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)

本例中vocab_size为151936,tokenizer预设了151664个,留了很多可以作下游special token 做SFT微调

在Qwen2ForCausalLM中定义:self._tie_or_clone_weights(output_embeddings, self.get_input_embeddings())
可以看到在torch下权重是浅克隆,参数更新对两头都有效

def _tie_or_clone_weights(self, output_embeddings, input_embeddings):
    """Tie or clone module weights depending of whether we are using TorchScript or not"""
    if self.config.torchscript:
        output_embeddings.weight = nn.Parameter(input_embeddings.weight.clone())
    else:
        output_embeddings.weight = input_embeddings.weight

    if getattr(output_embeddings, "bias", None) is not None:
        output_embeddings.bias.data = nn.functional.pad(
            output_embeddings.bias.data,
            (
                0,
                output_embeddings.weight.shape[0] - output_embeddings.bias.shape[0],
            ),
            "constant",
            0,
        )
    if hasattr(output_embeddings, "out_features") and hasattr(input_embeddings, "num_embeddings"):
        output_embeddings.out_features = input_embeddings.num_embeddings

解密4: Qwen2ForCausalLM的forward函数

这里对Qwen2Model基础模型的forward暂时不深入讨论

""" 入参说明
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None, [0-n_positions-1]位置索引
past_key_values: Optional[List[torch.FloatTensor]] = None,#Pre-computed hidden-states in attention for KV cached
inputs_embeds: Optional[torch.FloatTensor] = None, #没啥用,不想用look up embedding
labels: Optional[torch.LongTensor] = None, #[0-vocab_size]或者-100的整数token_id作为label,其中-100在交叉熵损失函数中是忽略的
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None, #whether or not to return hidden of all layers
return_dict: Optional[bool] = None, #return ModelOutput class
cache_position: Optional[torch.LongTensor] = None, #position_id not affected by padding
num_logits_to_keep: int = 0, # hidden_states[:, -num_logits_to_keep:, :] 只计算最后num个token的预测结果,在生成阶段一般取1来降低显存占用
**loss_kwargs,
"""

首先Qwen2ForCausalLM调用Qwen2Model的forward函数,入参基本和上述内容一致,获得多层处理后的hidden_states

参数说明
seq_len:39, hidden_size=896, layer=24
last_hidden_state和hidden_state表示最后一层和所有层的hidden,但是这里我的output_hidden_states=False
past_key_values存放了24层Attention Block里面的Key_list, value_list, 形状为batch, n_head,n_token, d_head,list长度为24
num_logits_to_keep设置为1,只计算最后一个token的预测结果,形状为batch, 1, vocab_size

在这里插入图片描述
得到的BaseModelOutputWithPast包含上述内容,其中last_hidden_state通过lm_head投影得到一组logits,表示tokens的预测概率分布
在这里插入图片描述
如果入参中包含labels,则会计算loss用于模型训练

labels (torch.LongTensor of shape (batch_size, sequence_length), optional):
Labels for computing the masked language modeling loss. Indices should either be in [0, ..., config.vocab_size] or -100 (see input_ids docstring). Tokens with indices set to -100 are ignored (masked), the loss is only computed for the tokens with labels in [0, ..., config.vocab_size].

输入labels, input_ids->hidden_states->logits,即可采用交叉熵损失函数计算分类loss
值得注意地,在LLM的训练过程中,每个token的下一个token就是该token的标签,所以这是一种自监督损失函数,只需要确定mask attention不产生信息泄露即可训练
调用:model(input_ids=model_inputs.data['input_ids'],attention_mask = model_inputs.data['attention_mask'],labels = model_inputs.data['input_ids'])

def ForCausalLMLoss(
    logits, labels, vocab_size: int, num_items_in_batch: int = None, ignore_index: int = -100, **kwargs
):
    # Upcast to float if we need to compute the loss to avoid potential precision issues
    logits = logits.float()
    # Shift so that tokens < n predict n
    shift_logits = logits[..., :-1, :].contiguous()
    shift_labels = labels[..., 1:].contiguous()
##这两行代码将 logits 和 labels 进行平移操作。在因果语言模型中,模型需要预测下一个词,因此对于每个位置 i,模型的输入是词 i,目标是预测词 i+1。通过将 logits 和 labels 平移一位,我们可以确保模型的输出(logits)与正确的目标标签(labels)对齐。
## 这样logits保留前n-1位,labels保留后n-1位
    # Flatten the tokens
    shift_logits = shift_logits.view(-1, vocab_size)
    shift_labels = shift_labels.view(-1)
    # Enable model parallelism
    shift_labels = shift_labels.to(shift_logits.device)
    loss = fixed_cross_entropy(shift_logits, shift_labels, num_items_in_batch, ignore_index, **kwargs)
    return loss

在这里插入图片描述
显然labels保留了后面的n-1个,logits保留了前面的n-1个,自监督是LLM训练的基础

解密5:Qwen2网络模型结构

Qwen网络结构总览图,来自B站UP主良睦路程序员绘制的网络结构我觉得很清晰,不过UP主绘制的是LLAMA的网络,但是Qwen和LLAMA的网络结构可以说是一模一样
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Qwen2 0.5B版本
Qwen2ForCausalLM(
  (model): Qwen2Model(
    (embed_tokens): Embedding(151936, 896)
    (layers): ModuleList(
      (0-23): 24 x Qwen2DecoderLayer(
        (self_attn): Qwen2Attention(
          (q_proj): Linear(in_features=896, out_features=896, bias=True)
          (k_proj): Linear(in_features=896, out_features=128, bias=True) #Group size=7
          (v_proj): Linear(in_features=896, out_features=128, bias=True)
          (o_proj): Linear(in_features=896, out_features=896, bias=False)
          (rotary_emb): Qwen2RotaryEmbedding()
        )
        (mlp): Qwen2MLP(
          (gate_proj): Linear(in_features=896, out_features=4864, bias=False)
          (up_proj): Linear(in_features=896, out_features=4864, bias=False)
          (down_proj): Linear(in_features=4864, out_features=896, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): Qwen2RMSNorm((896,), eps=1e-06)
        (post_attention_layernorm): Qwen2RMSNorm((896,), eps=1e-06)
      )
    )
    (norm): Qwen2RMSNorm((896,), eps=1e-06)
    (rotary_emb): Qwen2RotaryEmbedding()
  )
  (lm_head): Linear(in_features=896, out_features=151936, bias=False)
)

解密6: Qwen2RotaryEmbedding()细节原理

首先提一下苏剑林博客的正余弦绝对位置编码的基础概念
在这里插入图片描述
容易看出,对于不同的token position k, 存在一个正余弦交替的位置编码向量代表这个token的绝对位置信息;使用正余弦交替具备一定的推导规律和[-1,1]的上下界,在早期比较常用;
疑惑1:
d i v _ t e r m = 1000 0 − 2 i / d ∗ k div\_term = 10000^{-2i/d}*k div_term=100002i/dk代表什么,base选择10000的好处在哪里?
首先 1000 0 − 2 i / d 10000^{-2i/d} 100002i/d随着位置编码维度的趋近于 d / 2 d/2 d/2,这个负幂指数函数是单调递减的,例如d_model=512时,结果如下:
在这里插入图片描述
s i n ( k ∗ d i v _ t e r m ) sin(k*div\_term) sin(kdiv_term) c o s ( k ∗ d i v _ t e r m ) cos(k*div\_term) cos(kdiv_term)都是一个开始变化大,结尾变化小(低维高频、高频低维)的编码分布,如k=10的位置编码结果如下:
在这里插入图片描述
k=300的位置编码结果如下:
在这里插入图片描述
可以看到这种正余弦位置编码具备良好的数据范围,编码的向量存在低维高频、高维低频的信息分布;接下来分析位置编码的“远程衰减性”,即位置编码内积运算的结果应该和toknes之间的距离负相关,越远的tokens位置编码相似度越低,在Transformer中可以让模型更多关注“局部注意力”;
下图为k=1的位置编码与k=2-500位置编码的内积运算结果,显然满足远程衰减性质
在这里插入图片描述
综上,回顾刚刚提出的 1000 0 − 2 i / d 10000^{-2i/d} 100002i/d中base=10000的原因,其实主要是长度覆盖范围的问题,base过低如等于100时,
在这里插入图片描述
曲线明显更加平滑了,原来k=10的位置编码如下:
在这里插入图片描述
每个位置的位置编码的波长变得更加平滑,不同位置的位置编码间,波长差异明显减小了;在通过选择较大的base可以保证不同位置之间的波长差异足够大,从而使得即使是在很长的序列中,相邻位置也能有明显的区别。这样做的目的是为了确保模型能够区分出不同位置的token,即使它们相隔很远。
比较大的base, b a s e − 2 i / d base^{-2i/d} base2i/d曲线比较sharp,可以防止短周期内出现重复的位置信息,从而保障了长度衰减能力,base=100的时候,长度衰减能力很快就会失效,因为sin cos的周期性在相邻区间产生了相似的位置编码
在这里插入图片描述

# sincos位置编码的简单实现
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange( #0-d/2-1范围的, 0,2,4,6...的2i或2i+1维度分量的div_term
            0, d_model, 2).float() * (-math.log(base) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term) #max_len, d_model//2 一次赋值所有sin的内容
        pe[:, 1::2] = torch.cos(position * div_term) #max_len, d_model//2 一次赋值所有cos的内容
        pe = pe.unsqueeze(0).transpose(0, 1) #max_len, 1, d_model
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

Rope的原理

复数是数学中一种基本的数类型,它们由实数和虚数单位 ( i ) 组成,其中 ( i ) 定义为 ( i^2 = -1 )。一个复数通常写作 ( a + bi ),这里 ( a ) 和 ( b ) 是实数,( a ) 被称为复数的实部,而 ( b ) 被称为复数的虚部。
在这里插入图片描述
一个复数表示 e i θ e^{i\theta} eiθ基于欧拉公式可以得到其相应的旋转坐标表示
在这里插入图片描述
基于上述有关复数、极坐标和欧拉公式的概念,我们可以简单推导Rope的原理了(证明过程请见苏神博客

  1. 定义目标

对于二维向量 q = ( q 0 , q 1 ) q=(q_0,q_1) q=(q0,q1), k = ( k 0 , k 1 ) k=(k_0,k_1) k=(k0,k1),我们希望定义一个位置编码函数,能够表示 f ( q , m ) f(q,m) f(q,m), f ( k , n ) f(k,n) f(k,n)后, q , k q,k q,k的向量内积(Attention)运算后能够表示 m − n m-n mn的相对位置关系
即求解满足 < f ( q , m ) , f ( k , n ) > = g ( q , k , m − n ) <f(q,m),f(k,n)>=g(q,k,m-n) <f(q,m),f(k,n)>=g(q,k,mn) f f f的一个解

我们知道向量内积运算结果满足以下定义
< q , k > = q 0 ∗ k 0 + q 1 ∗ k 1 = R e [ q k ∗ ] <q,k>=q_0*k_0+q_1*k_1=Re[qk^*] <q,k>=q0k0+

### Qwen 源代码结构及实现细节 #### 1. **Qwen 的整体架构** Qwen 是一个大型语言模型家族,其设计涵盖了多个子模块和功能扩展。这些模块共同构成了 Qwen 的核心能力和应用场景支持。以下是几个主要方面的概述: - **基础大模型**:作为整个系列的核心部分,Qwen 提供了强大的自然语言理解和生成能力[^1]。 - **多模态支持 (Qwen-VL)**:通过引入视觉感知技术,Qwen 不仅可以处理纯文本数据,还能够结合图像等多媒体信息完成复杂的交互任务[^3]。 - **增强型工具集 (Qwen-Agent)**:内置有专门用于执行编程逻辑的功能组件——比如代码解释器,它允许系统动态生成并运行 Python 脚本以解决实际问题。 #### 2. **具体实现中的关键技术点** ##### a) **代码解释器集成机制** Qwen-Agent 中的代码解释器是一个非常重要的特性,使得该平台具备自动化数据分析的能力。当接收到包含数值或者表格形式的数据请求时,此部件会自动生成相应的 Python 程序片段来满足需求。下面展示了一个简单的例子说明如何利用此类服务来进行基本统计运算: ```python import pandas as pd def analyze_data(file_path): df = pd.read_csv(file_path) summary_stats = { 'mean': df.mean(), 'median': df.median() } return summary_stats result = analyze_data('example.csv') print(result) ``` 上述脚本展示了读取 CSV 文件之后计算平均数与中位数的过程;值得注意的是,在真实部署环境下,这类操作完全由 AI 自动生成而无需人工干预。 ##### b) **参数高效微调方法的应用** 针对特定领域内的定制化需求,可以通过 LoRA(Low-Rank Adaptation)以及 ALPACA_ZH 这样的先进技术手段对预训练好的通用版本进一步优化调整,从而获得更加贴合业务场景的表现效果[^2]。这种方法不仅保留了原有模型的知识积累,同时也大幅降低了重新训练所需的成本开销。 ##### c) **跨模态融合策略** 为了达成更好的用户体验,特别是在涉及图文混排的内容创作过程中,Qwen 开发团队采用了先进的交叉注意力网络结构,确保文字描述能精准映射到对应的图形特征之上,反之亦然。这种双向关联的设计思路极大地提升了最终输出的质量水平。 --- ####
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值