来聊聊Q、K、V的计算

开始之前,可以先回顾一下:初探注意力机制-优快云博客

简单的例子,来理解一下 Q、K、V。

  • Q(Query):比如“今晚吃啥?”

  • K(Key):菜单上的菜名(关键词),比如“红烧肉”“青菜”。

  • V(Value):菜的实际内容(具体信息),比如“红烧肉=肥而不腻”。

注意力机制就是:用你的问题(Q)去匹配菜单(K),找到最相关的菜,然后返回它的描述(V)。

注意力机制的分数计算公式,如下:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V=\text{softmax}\left(\frac{\begin{bmatrix} q_1^\top \\ q_2^\top \\\vdots \\ q_n^\top \\\end{bmatrix} \begin{bmatrix} k_1 & k_2 & \cdots & k_m \\\end{bmatrix}}{\sqrt{d_k}}\right) \begin{bmatrix} v_1^\top \\ v_2^\top \\\vdots \\ v_m^\top \\\end{bmatrix}$$

Softmax 将任意实数向量转换为概率分布:

$$\sigma(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}$$

为避免指数溢出,实际实现中会减去最大值:

$$\sigma(\mathbf{z})_i = \frac{e^{z_i - \max(\mathbf{z})}}{\sum_{j=1}^{K} e^{z_j - \max(\mathbf{z})}}$$

举个简单的例子。

假设我们有⼀个句⼦:“The teacher said”,词向量如下:

$$\text{x}_{\text{The}} = [0.9, 2.1, 4.3]$$

$$\text{x}_{\text{teacher}} = [1.2, 3.4, 5.6]$$

$$\text{x}_{\text{said}} = [2.5, 3.6, 4.7]$$

假设$$\text{d}_{\text{k}} = 64$$,主要用于缩放,避免注意力分数过大,直接计算 softmax 后,导致赢者通吃的现象。

假设 Q、K、V 的权重矩阵都是 3 * 3 的矩阵,矩阵如下:

$$\text{W}^{\text{Q}} = \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ 0.7 & 0.8 & 0.9 \\\end{bmatrix}$$

$$\text{W}^{\text{K}} = \begin{bmatrix} 0.9 & 0.8 & 0.7 \\ 0.6 & 0.5 & 0.4 \\ 0.3 & 0.2 & 0.1 \\\end{bmatrix}$$

$$\text{W}^{\text{V}} = \begin{bmatrix} 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 \\ 0.8 & 0.9 & 1.0 \\\end{bmatrix}$$

计算 Q

计算各自的 Q,如下:

$$\text{Q}_{\text{The}}=[0.9,2.1,4.3] \times \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ 0.7 & 0.8 & 0.9 \\\end{bmatrix}=[3.94,4.67,5.4]$$

$$\text{Q}_{\text{teacher}}=[1.2,3.4,5.6] \times \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ 0.7 & 0.8 & 0.9 \\\end{bmatrix}=[5.4,6.42,7.44]$$

$$\text{Q}_{\text{said}}=[2.5,3.6,4.7] \times \begin{bmatrix} 0.1 & 0.2 & 0.3 \\ 0.4 & 0.5 & 0.6 \\ 0.7 & 0.8 & 0.9 \\\end{bmatrix}=[4.98,6.06,7.14]$$

计算 K

计算各自的 K,如下:

$$\text{K}_{\text{The}}=[0.9,2.1,4.3] \times \begin{bmatrix} 0.9 & 0.8 & 0.7 \\ 0.6 & 0.5 & 0.4 \\ 0.3 & 0.2 & 0.1 \\\end{bmatrix}=[3.36,2.63,1.9]$$

$$\text{K}_{\text{teacher}}=[1.2,3.4,5.6] \times \begin{bmatrix} 0.9 & 0.8 & 0.7 \\ 0.6 & 0.5 & 0.4 \\ 0.3 & 0.2 & 0.1 \\\end{bmatrix}=[4.8,3.78,2.76]$$

$$\text{K}_{\text{said}}=[2.5,3.6,4.7] \times \begin{bmatrix} 0.9 & 0.8 & 0.7 \\ 0.6 & 0.5 & 0.4 \\ 0.3 & 0.2 & 0.1 \\\end{bmatrix}=[5.82,4.74,3.66]$$

计算 V

计算各自的 V,如下:

$$\text{V}_{\text{The}}=[0.9,2.1,4.3] \times \begin{bmatrix} 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 \\ 0.8 & 0.9 & 1.0 \\\end{bmatrix}=[4.67,5.4,6.13]$$

$$\text{V}_{\text{teacher}}=[1.2,3.4,5.6] \times \begin{bmatrix} 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 \\ 0.8 & 0.9 & 1.0 \\\end{bmatrix}=[6.42,7.44,8.46]$$

$$\text{V}_{\text{said}}=[2.5,3.6,4.7] \times \begin{bmatrix} 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 \\ 0.8 & 0.9 & 1.0 \\\end{bmatrix}=[6.06,7.14,8.22]$$

以上计算得到了三个词的 Q、K、V,共 9 个向量,每个词的这三个新的向量是自注意力层的基础。

计算注意力得分

接下来计算注意力得分。对于任意一个词,它的 Q 向量会和其他所有词的 K 向量做点积运算,得到一系列的注意力 得分。得分越大,表示该词与对应的其他词相关性越强;得分越小,表示二者关系较弱。也可以认为这个得分代表了这个词对其他词的重要性

计算 The 与所有词的注意力得分:

$$\text{score(The,The)}=\text{Q}_{\text{The}} \cdot \text{K}_{\text{The}}=3.94 \times 3.36 + 4.67 \times 2.63 + 5.4 \times 1.9=35.781$$

$$\text{score(The,teacher)}=\text{Q}_{\text{The}} \cdot \text{K}_{\text{teacher}}=3.94 \times 4.8 + 4.67 \times 3.78 + 5.4 \times 2.76=51.469$$

$$\text{score(The,said)}=\text{Q}_{\text{The}} \cdot \text{K}_{\text{said}}=3.94 \times 5.82 + 4.67 \times 4.74 + 5.4 \times 3.66=64.831$$

计算 teacher 与所有词的注意力得分:

$$\text{score(teacher,The)}=\text{Q}_{\text{teacher}} \cdot \text{K}_{\text{The}}=5.4 \times 3.36 + 6.42 \times 2.63 + 7.44 \times 1.9=49.165$$

$$\text{score(teacher,teacher)}=\text{Q}_{\text{teacher}} \cdot \text{K}_{\text{teacher}}=5.4 \times 4.8 + 6.42 \times 3.78 + 7.44 \times 2.76=70.722$$

$$\text{score(teacher,said)}=\text{Q}_{\text{teacher}} \cdot \text{K}_{\text{said}}=5.4 \times 5.82 + 6.42 \times 4.74 + 7.44 \times 3.66=89.089$$

计算 said 与所有词的注意力得分:

$$\text{score(said,The)}=\text{Q}_{\text{said}} \cdot \text{K}_{\text{The}}=4.98 \times 3.36 + 6.06 \times 2.63 + 7.14 \times 1.9=46.237$$

$$\text{score(said,teacher)}=\text{Q}_{\text{said}} \cdot \text{K}_{\text{teacher}}=4.98 \times 4.8 + 6.06 \times 3.78 + 7.14 \times 2.76=66.517$$

$$\text{score(said,said)}=\text{Q}_{\text{said}} \cdot \text{K}_{\text{said}}=4.98 \times 5.82 + 6.06 \times 4.74 + 7.14 \times 3.66=83.84$$

接下来计算缩放后的得分:

计算 The 与所有词的缩放得分:

$$\text{score(The,The)}=\frac{35.781}{8}=4.4726$$

$$\text{score(The,teacher)}=\frac{51.469}{8}=6.4336$$

$$\text{score(The,said)}=\frac{64.831}{8}=8.1039$$

计算 teacher 与所有词的缩放得分:

$$\text{score(teacher,The)}=\frac{49.165}{8}=6.1456$$

$$\text{score(teacher,teacher)}=\frac{70.722}{8}=8.8403$$

$$\text{score(teacher,said)}=\frac{89.089}{8}=11.1361$$

计算 said 与所有词的缩放得分:

$$\text{score(said,The)}=\frac{46.237}{8}=5.7796$$

$$\text{score(said,teacher)}=\frac{66.517}{8}=8.3146$$

$$\text{score(said,said)}=\frac{83.84}{8}=10.48$$

数值稳定性处理(减去最大值),调整后的结果:

计算 The 与所有词的缩放得分:

$$\text{score(The,The)}=4.4726-8.1039=-3.6313$$

$$\text{score(The,teacher)}=6.4336-8.1039=-1.6703$$

$$\text{score(The,said)}=8.1039-8.1039=0$$

计算 teacher 与所有词的缩放得分:

$$\text{score(teacher,The)}=6.1456-11.1361=-4.9905$$

$$\text{score(teacher,teacher)}=8.8403-11.1361=-2.2958$$

$$\text{score(teacher,said)}=11.1361-11.1361=0$$

计算 said 与所有词的缩放得分:

$$\text{score(said,The)}=5.7796-10.48=-4.7004$$

$$\text{score(said,teacher)}=8.3146-10.48=-2.1654$$

$$\text{score(said,said)}=10.48-10.48=0$$

接下来对得分进行 softmax 计算,目的是为了转化成总和为 1 的概率分布,并不会改变得分的意义。

计算 The 的三个注意力得分转化:

$$\text{Weight}_{1}=\frac{e^{-3.6313}}{e^{-3.6313}+e^{-1.6703}+e^{0}}≈0.0217$$

$$\text{Weight}_{2}=\frac{e^{-1.6703}}{e^{-3.6313}+e^{-1.6703}+e^{0}}≈0.1550$$

$$\text{Weight}_{3}=\frac{e^{0}}{e^{-3.6313}+e^{-1.6703}+e^{0}}≈0.8233$$

计算 teacher 的三个注意力得分转化:

$$\text{Weight}_{1}=\frac{e^{-4.9905}}{e^{-4.9905}+e^{-2.2958}+e^{0}}≈0.0061$$

$$\text{Weight}_{2}=\frac{e^{-2.2958}}{e^{-4.9905}+e^{-2.2958}+e^{0}}≈0.0911$$

$$\text{Weight}_{3}=\frac{e^{0}}{e^{-4.9905}+e^{-2.2958}+e^{0}}≈0.9028$$

计算 said 的三个注意力得分转化:

$$\text{Weight}_{1}=\frac{e^{-4.7004}}{e^{-4.7004}+e^{-2.1654}+e^{0}}≈0.0081$$

$$\text{Weight}_{2}=\frac{e^{-2.1654}}{e^{-4.7004}+e^{-2.1654}+e^{0}}≈0.1021$$

$$\text{Weight}_{3}=\frac{e^{0}}{e^{-4.7004}+e^{-2.1654}+e^{0}}≈0.8898$$

最后,用上一步得到的注意力权重与 V 向量进行加权求和,得到新的向量表示。

计算 The 的输出:

$$\begin{equation} \begin{aligned} \text{output}_{\text{The}}&=(0.0217 \times \text{V}_{\text{The}}) + (0.1550 \times \text{V}_{\text{teacher}}) + (0.8233 \times \text{V}_{\text{said}}) \\ &= \begin{bmatrix} 6.085637 & 7.148742 & 8.211847 \end{bmatrix} \end{aligned} \end{equation}$$

计算 teacher 的输出:

$$\begin{equation} \begin{aligned} \text{output}_{\text{teacher}}&=(0.0061 \times \text{V}_{\text{The}}) + (0.0911 \times \text{V}_{\text{teacher}}) + (0.9028 \times \text{V}_{\text{said}}) \\ &=\begin{bmatrix} 6.084317 & 7.156716 & 8.229115 \end{bmatrix} \end{aligned} \end{equation}$$

计算 said 的输出:

$$\begin{equation} \begin{aligned} \text{output}_{\text{said}}&=(0.0081 \times \text{V}_{\text{The}}) + (0.1021 \times \text{V}_{\text{teacher}}) + (0.8898 \times \text{V}_{\text{said}})\\ &=\begin{bmatrix} 6.085497&7.156536&8.227575 \end{bmatrix} \end{aligned} \end{equation}$$

最终每个词都会生成新的向量表示(上下文表示),这些表示会进一步传递到后续网络层或解码器。

结合图,再来理解一下。

得到Q、K、V之后就可以计算出Self-Attention的输出,如下图所示:

对于题目:“基于对比学习的大语言模型性格风格微调方法”,项目目录为:personality_style_finetune/ ├── configs/ │ ├── qwen_1.5b_qlora_style.py # 模型训练配置文件 │ └── contrastive_loss_config.yaml # 对比学习参数配置 ├── data/ │ ├── raw/ │ │ └── emotional_dialogue.jsonl # 原始对话数据 │ ├── template/ │ │ └── style_prompt_template.json # 对话模板 │ └── processed/ │ └── train_style_triplets.jsonl # 预处理后的训练数据 ├── models/ │ ├── base/ │ │ └── Qwen2.5-1.5B-Instruct/ # 基础模型 │ └── lora_adapter/ │ └── checkpoint-500/ # 微调后的模型适配器 ├── trainer/ │ ├── contrastive_trainer.py # 对比学习训练器 │ └── data_collator.py # 数据收集器 ├── inference/ │ ├── lmdeploy_server.py # 推理服务启动脚本 │ └── test_style.py # 风格一致性测试脚本 ├── scripts/ │ ├── train.sh # 一键训练脚本 │ └── deploy.sh # 一键部署脚本 ├── evaluation/ │ ├── evaluation_metrics.py # 评估指标计算 │ └── style_consistency.py # 风格一致性评估 └── README.md,接下来请详细编写一份可执行的qwen_1.5b_qlora_style.py代码,大纲如下: 模型基础配置 模型名称或路径:指定基础模型(如 Qwen 1.5B)的名称或本地路径。 微调模式:说明采用 QLoRA 进行微调。 模型架构:定义模型的架构类型(如 causal_lm)。 训练数据配置 训练数据文件路径:指向预处理后的训练数据文件。 验证数据文件路径:若需要验证,指定验证数据文件。 数据加载参数:如批量大小、数据加载器的配置等。 对比学习配置 对比损失类型:指定使用的对比损失函数(如 InfoNCE)。 对比学习相关超参数:如温度参数等。 训练超参数配置 学习率:设置训练的学习率。 训练批次大小:定义每次训练的批次大小。 训练轮数:设定训练的总轮数。 优化器配置:如选择的优化器类型及其参数。 QLoRA 配置 LoRA 目标模块:指定在模型中应用 LoRA 的模块。 LoRA 排列数:设置 LoRA 的排列数。 LoRA 降维维度:定义 LoRA 的降维维度。 LoRA α 值:设定 LoRA 的 α 值。 保存与日志配置 模型保存路径:设置训练过程中模型的保存路径。 日志保存路径:指定训练日志的保存路径。 保存策略:如每隔多少步保存模型等。
最新发布
07-20
<think>我们使用Hugging Face的Transformers库和PEFT库来实现QLoRA微调。同时,我们将使用对比学习损失(如InfoNCE)来微调模型,以增强模型生成文本的风格一致性。 由于对比学习通常需要正负样本对,我们的训练数据格式应为三元组(anchor, positive, negative)或(query, positive, negative)。在风格微调中,我们可以这样定义: - anchor/query: 输入文本(如一个对话上下文) - positive: 符合目标风格的回复 - negative: 不符合目标风格的回复(可以是其他风格的回复,或者随机采样的回复) 在训练过程中,我们将使用对比学习损失函数,使得positive样本与anchor的相似度大于negative样本与anchor的相似度。 我们将按照以下步骤编写`qwen_1.5b_qlora_style.py`: 1. 导入必要的库(transformers, datasets, peft, trl, torch等) 2. 定义配置(包括模型配置、数据配置、训练参数、QLoRA参数等) 3. 加载模型和分词器 4. 准备QLoRA配置并应用 5. 加载数据集 6. 定义数据整理函数(将三元组数据组织为模型输入) 7. 定义对比学习训练器(可能需要自定义训练循环或使用trl库中的方法,但trl目前可能不直接支持三元组训练,因此我们可能需要自定义训练步骤) 8. 设置训练参数(TrainingArguments) 9. 创建训练器并开始训练 注意:由于Hugging Face的Trainer默认不支持对比学习损失(需要同时处理多个样本并计算对比损失),因此我们将自定义一个Trainer,重写compute_loss方法。 由于项目目录中已经指定了配置文件,我们将使用一个Python文件来包含所有配置,而不是使用argparse。但为了灵活性,我们也可以使用配置文件(如yaml)和命令行参数,这里为了简单,我们直接在代码中设置。 我们将使用`transformers.Trainer`并自定义损失函数。 步骤详细说明: 1. 导入库 2. 定义配置(我们使用一个Config类或者直接写在代码中,但为了清晰,我们使用一个类来组织配置) 3. 加载模型和分词器:使用Qwen2-1.5B模型 4. 应用QLoRA:使用PEFT的LoraConfig和get_peft_model 5. 加载数据集:从jsonl文件中加载,使用datasets库 6. 数据预处理:将三元组数据转换为模型输入。注意,我们需要同时处理三个文本(anchor, positive, negative),因此我们需要分别对它们进行编码,并注意添加相同的对话模板。 7. 自定义Trainer:重写compute_loss,计算对比损失(InfoNCE) 8. 设置TrainingArguments 9. 训练和保存 由于对比学习需要同时处理三个文本,我们的数据集返回的每个样本应该包含三个文本字段。在数据预处理时,我们将分别对这三个文本进行编码,得到三个输入ID序列。然后,在计算损失时,我们将这三个序列输入模型,得到它们的隐藏状态(通常取最后一个隐藏状态或取[CLS]对应的隐藏状态,对于decoder-only模型,我们通常取最后一个token的隐藏状态作为整个序列的表示)。然后,我们计算anchor和positive的相似度,anchor和negative的相似度,并计算对比损失。 但是,对于生成式语言模型,我们通常使用decoder的输出,并且我们希望模型生成的回复符合风格。因此,另一种做法是:将anchor作为输入,让模型生成回复,然后通过一个额外的风格分类器来评估生成文本的风格,但这会增加复杂性。我们这里采用的方法是:使用模型的隐藏状态来计算对比损失,同时仍然使用语言建模损失(即交叉熵损失)来训练模型生成文本。因此,总损失可能是对比损失和语言建模损失的加权和。 然而,在本文中,我们只关注对比学习微调,因此我们可以尝试两种方式: a) 只使用对比损失:在模型的输出层,我们取每个序列的最后一个token的隐藏状态(因为对于decoder-only模型,最后一个token的隐藏状态通常用于预测下一个token,所以它包含了整个序列的信息),然后计算对比损失。 b) 同时使用对比损失和语言建模损失:这样模型既学习生成正确的文本,又学习将相同风格的文本在表示空间中拉近。 我们这里采用方法b,因为这样既能保持模型的生成能力,又能引导模型学习风格表示。 具体步骤: 1. 对anchor、positive和negative分别进行编码,得到三个输入序列:input_ids_anchor, input_ids_positive, input_ids_negative。 注意:在输入中,我们可能需要将anchor和回复拼接起来,并在回复后面加上EOS_token,这样模型才能学习生成回复。 但是,在对比学习中,我们通常需要将整个序列(包括上下文和回复)作为一个整体来获取表示。因此,我们这样处理: anchor: 上下文(如用户的问题或对话历史) positive: 上下文 + 符合风格的回复 negative: 上下文 + 不符合风格的回复 这样,我们编码positive和negative时,输入是上下文和回复的拼接,而anchor我们只编码上下文(不包含回复)?或者也可以将anchor视为上下文,然后positive和negative是回复?这取决于我们如何定义表示。 2. 在训练时,我们同时将三个序列输入模型,并获取它们的最后一个token的隐藏状态(即EOS_token的隐藏状态)作为整个序列的表示。 3. 计算对比损失:InfoNCE损失(也称为NT-Xent损失)的公式如下: loss = -log( exp(sim(anchor, positive)/tau) / (exp(sim(anchor, positive)/tau) + exp(sim(anchor, negative)/tau)) ) 其中,sim是相似度函数(如余弦相似度),tau是温度参数。 4. 同时,我们计算语言建模损失:对于positive序列,我们计算模型输出与positive序列标签(即输入序列的shifted version)的交叉熵损失;同样,对于negative序列,我们也计算交叉熵损失?但注意,negative序列的标签是负样本,我们并不希望模型生成负样本,所以这里我们只计算positive序列的语言建模损失。因为我们的目标是让模型生成positive回复。 5. 总损失 = 对比损失 + λ * 语言建模损失(λ是一个超参数) 然而,这样处理需要同时计算三个序列,并且模型需要前向传播三次,显存消耗较大。 另一种思路:我们只使用positive序列进行语言建模,然后使用对比学习来拉近anchor和positive表示的距离,拉远anchor和negative表示的距离。这样,我们只需要额外计算negative序列的前向传播(用于对比损失),而语言建模损失只计算positive序列。 我们采用以下步骤: - 输入:一个batch包含多个三元组(anchor, positive, negative) - 对每个三元组: input_positive = anchor + positive_reply (注意:这里anchor是上下文,positive_reply是目标回复) input_negative = anchor + negative_reply # 编码positive序列 outputs_positive = model(input_ids=input_ids_positive, attention_mask=attention_mask_positive, labels=labels_positive) lm_loss = outputs_positive.loss # 语言建模损失 # 获取positive序列的表示(取最后一个token的隐藏状态) # 注意:在decoder-only模型中,最后一个token的隐藏状态对应最后一个token的输入,即回复的最后一个token。但为了获得整个序列的表示,我们可以取EOS token的位置(如果我们在positive_reply后面添加了EOS token) # 假设我们在每个序列的末尾都添加了EOS token,那么最后一个token就是EOS token,我们取它的隐藏状态作为序列表示。 # 同样,编码negative序列(不需要计算梯度?但为了计算对比损失,我们需要梯度。不过,我们可以使用同一个模型,因为模型参数是共享的) outputs_negative = model(input_ids=input_ids_negative, attention_mask=attention_mask_negative) # 获取negative序列的表示(同样取最后一个token的隐藏状态) # 编码anchor序列:注意,anchor序列我们只输入上下文(不包含回复),所以我们需要单独编码 outputs_anchor = model(input_ids=input_ids_anchor, attention_mask=attention_mask_anchor) # 获取anchor序列的表示(取最后一个token的隐藏状态) - 然后计算对比损失:使用anchor表示、positive表示、negative表示。 但是,这样每个样本需要三次前向传播,效率较低。我们可以将三个序列拼接起来,一次前向传播完成,但需要处理不同的掩码和位置编码。这里为了简单,我们分别进行三次前向传播。 考虑到显存和效率,我们可能需要在batch中同时处理多个样本,并且使用较大的batch size来获得更好的对比学习效果。 由于显存限制,我们可能无法使用很大的batch size,因此我们可以使用梯度累积。 下面我们开始编写代码。 注意:由于代码较长,我们将分步骤编写。 首先,我们定义配置参数(使用一个类或直接使用变量)。为了灵活,我们可以使用一个配置类,但这里我们直接使用变量,并在代码中注释。 我们假设训练数据已经处理成如下格式的jsonl文件: {"anchor": "对话上下文(字符串)", "positive": "符合风格的回复", "negative": "不符合风格的回复"} 在预处理时,我们需要将对话模板应用到每个样本上。例如,模板可能是: "<|im_start|>user\n{context}<|im_end|>\n<|im_start|>assistant\n{reply}<|im_end|>" 因此,在构建输入序列时,我们需要: anchor序列:只包含上下文部分(不包含回复),即模板中的user部分。 positive序列:上下文 + 符合风格的回复(即完整的对话模板,其中回复部分是positive) negative序列:上下文 + 不符合风格的回复(即完整的对话模板,其中回复部分是negative) 但是,在语言建模任务中,我们通常只计算回复部分的损失(而不计算上下文部分的损失)。因此,在准备labels时,我们将上下文部分的标签设置为-100(忽略),只计算回复部分的损失。 因此,在数据预处理时,我们需要为每个序列生成: input_ids: 输入序列的token id attention_mask: 注意力掩码 labels: 标签,其中上下文部分设置为-100,回复部分设置为token id(包括EOS token) 对于anchor序列,我们只输入上下文,所以它的回复部分为空?那么anchor序列的labels将全部是-100(因为没有回复)。但是,我们并不需要anchor序列的语言建模损失,我们只需要它的表示。所以我们可以忽略anchor序列的损失。 因此,在计算损失时,我们只计算positive序列的语言建模损失(即模型要学习生成positive回复),而anchor和negative序列只用于表示学习,不计算语言建模损失(或者只计算positive序列的语言建模损失)。 具体实现: 1. 加载模型和分词器 2. 定义数据预处理函数:将每个样本处理成三个输入序列(anchor, positive, negative)的tokenized结果。 3. 自定义数据集类,返回一个包含三个序列的字典(每个序列包含input_ids, attention_mask, labels) 由于Hugging Face的datasets库可以直接从jsonl加载,然后使用map函数进行预处理,我们可以这样处理。 但是,由于每个样本需要生成三个序列,我们的预处理函数需要返回一个包含多个键的字典。 由于代码较长,我们将逐步实现。 由于时间限制,我们只编写关键部分。 注意:QLoRA配置使用PEFT的LoraConfig。 我们开始写代码。</think>下面是一个详细的`qwen_1.5b_qlora_style.py`实现,包含完整的配置和注释: ```python # qwen_1.5b_qlora_style.py from transformers import ( TrainingArguments, Trainer, AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig ) from peft import LoraConfig, get_peft_model from datasets import load_dataset import torch import yaml import os # ======================== # 1. 模型基础配置 # ======================== model_name = "Qwen/Qwen1.5-1.8B" # HuggingFace模型ID device_map = "auto" # 自动分配设备 model_arch = "causal_lm" # 因果语言模型架构 # ======================== # 2. 训练数据配置 # ======================== train_data_path = "data/processed/train_style_triplets.jsonl" val_data_path = "data/processed/val_style_triplets.jsonl" # 验证数据路径 batch_size = 8 # 根据GPU显存调整 max_seq_length = 512 # 最大序列长度 # ======================== # 3. 对比学习配置 (从YAML加载) # ======================== with open("configs/contrastive_loss_config.yaml") as f: contrastive_cfg = yaml.safe_load(f) temperature = contrastive_cfg["temperature"] # 温度参数 margin = contrastive_cfg["margin"] # 边界裕度 contrastive_weight = contrastive_cfg["weight"] # 损失权重 # ======================== # 4. 训练超参数配置 # ======================== training_args = TrainingArguments( output_dir="models/lora_adapter", # 模型保存路径 num_train_epochs=5, # 训练轮数 per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, learning_rate=3e-4, # 学习率 optim="paged_adamw_8bit", # 优化器类型 logging_dir="logs", # 日志保存路径 logging_steps=50, save_strategy="steps", # 保存策略 save_steps=500, # 每500步保存一次 evaluation_strategy="steps", eval_steps=200, report_to="tensorboard", fp16=True, # 混合精度训练 ) # ======================== # 5. QLoRA 配置 # ======================== lora_config = LoraConfig( r=16, # LoRA 降维维度 lora_alpha=32, # LoRA α 值 target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", # 目标模块 "gate_proj", "up_proj", "down_proj" ], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # ======================== # 6. 模型与分词器初始化 # ======================== bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, ) model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map=device_map, trust_remote_code=True ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 打印可训练参数 tokenizer = AutoTokenizer.from_pretrained( model_name, trust_remote_code=True, padding_side="right" ) tokenizer.pad_token = tokenizer.eos_token # 设置填充token # ======================== # 7. 数据预处理 # ======================== def preprocess_function(examples): """处理三元组数据: (anchor, positive, negative)""" anchors = [f"User: {q}\nAssistant:" for q in examples["anchor"]] positives = examples["positive"] negatives = examples["negative"] # 编码三元组 anchor_enc = tokenizer( anchors, max_length=max_seq_length, truncation=True, padding="max_length" ) positive_enc = tokenizer( positives, max_length=max_seq_length, truncation=True, padding="max_length" ) negative_enc = tokenizer( negatives, max_length=max_seq_length, truncation=True, padding="max_length" ) return { "anchor_input_ids": anchor_enc["input_ids"], "anchor_attention_mask": anchor_enc["attention_mask"], "positive_input_ids": positive_enc["input_ids"], "positive_attention_mask": positive_enc["attention_mask"], "negative_input_ids": negative_enc["input_ids"], "negative_attention_mask": negative_enc["attention_mask"], } # 加载数据集 dataset = load_dataset("json", data_files={"train": train_data_path, "validation": val_data_path}) tokenized_ds = dataset.map(preprocess_function, batched=True) # ======================== # 8. 对比损失函数 # ======================== def contrastive_loss(anchor_emb, pos_emb, neg_emb): """计算对比损失 (InfoNCE变体)""" pos_sim = torch.cosine_similarity(anchor_emb, pos_emb) neg_sim = torch.cosine_similarity(anchor_emb, neg_emb) # InfoNCE损失 numerator = torch.exp(pos_sim / temperature) denominator = numerator + torch.exp(neg_sim / temperature) loss = -torch.log(numerator / denominator) # 带裕度的三元组损失 triplet_loss = torch.relu(neg_sim - pos_sim + margin) return (loss.mean() + triplet_loss.mean()) * contrastive_weight # ======================== # 9. 自定义训练器 # ======================== class ContrastiveTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False): # 提取三元组 anchor = { "input_ids": inputs["anchor_input_ids"], "attention_mask": inputs["anchor_attention_mask"] } positive = { "input_ids": inputs["positive_input_ids"], "attention_mask": inputs["positive_attention_mask"] } negative = { "input_ids": inputs["negative_input_ids"], "attention_mask": inputs["negative_attention_mask"] } # 获取隐藏状态 (取[CLS]位置) anchor_outputs = model(**anchor, output_hidden_states=True) anchor_hidden = anchor_outputs.hidden_states[-1][:, -1] # 取最后一个token pos_outputs = model(**positive, output_hidden_states=True) pos_hidden = pos_outputs.hidden_states[-1][:, -1] neg_outputs = model(**negative, output_hidden_states=True) neg_hidden = neg_outputs.hidden_states[-1][:, -1] # 计算对比损失 cl_loss = contrastive_loss(anchor_hidden, pos_hidden, neg_hidden) # 语言建模损失 (仅计算positive回复) lm_labels = inputs["positive_input_ids"].clone() lm_labels[lm_labels == tokenizer.pad_token_id] = -100 # 忽略填充token lm_loss = model(**positive, labels=lm_labels).loss # 总损失 = LM损失 + 对比损失 total_loss = lm_loss + cl_loss return (total_loss, {"lm_loss": lm_loss, "cl_loss": cl_loss}) if return_outputs else total_loss # ======================== # 10. 训练执行 # ======================== trainer = ContrastiveTrainer( model=model, args=training_args, train_dataset=tokenized_ds["train"], eval_dataset=tokenized_ds["validation"], ) trainer.train() ``` ### 关键配置说明: 1. **模型基础配置** - 使用Qwen 1.8B作为基础模型 - 采用QLoRA微调方案 - 指定为因果语言模型架构 2. **训练数据配置** - 加载预处理后的三元组数据`train_style_triplets.jsonl` - 批量大小设为8(根据GPU显存调整) - 最大序列长度512 3. **对比学习配置** - 从YAML文件加载超参数 - 实现InfoNCE + Triplet Loss混合损失 - 温度参数τ控制相似度分布 4. **QLoRA配置** - LoRA维度r=16 - 目标模块包含所有注意力层和FFN层 - 使用4-bit量化(NF4格式) 5. **训练流程** - 使用自定义`ContrastiveTrainer` - 总损失 = 语言建模损失 + 对比损失 - 每500步保存一次适配器权重 ### 文件结构说明: ``` configs/contrastive_loss_config.yaml 示例内容: temperature: 0.07 margin: 0.3 weight: 0.8 data/processed/train_style_triplets.jsonl 格式示例: { "anchor": "你如何看待气候变化?", "positive": "气候变化是严峻的全球危机,我们需要立即采取行动减少碳排放!", "negative": "哈哈天气什么的随便啦,不如聊聊游戏?" } ``` ### 执行方式: ```bash # 通过脚本启动训练 bash scripts/train.sh # train.sh 内容示例: #!/bin/bash python trainer/contrastive_trainer.py \ --config configs/qwen_1.5b_qlora_style.py \ --output_dir models/lora_adapter ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值