LLM大模型: RAG两大核心利器 — embedding和reranker模型微调fine-tune

    要想RAG好,embedding和reranker必须给力!目前市面上流行的embedding和reranker使用的都是通用预料训练,并未针对安全这个细分领域定制,所以有必要使用安全领域的预料微调一下!目前所有的预料中,获取成本比较低、并且不需要专门投入人力标注的有两种:

  • 网上各种安全论坛的博客、各大热门产品的漏洞说明等
  • 用户的点赞反馈数据(chatGPT、copilot等都有该功能)

  对于作者本人而言,用户的点赞反馈数据更容易获取,所以这里使用这类数据,借鉴RLHF-DPO的思路对embedding和reranker模型做微调!训练样本的数据格式如下:

{
  "query": "如何使用IDA Pro反汇编一个二进制文件?",
  "positive": [
    "使用IDA Pro反汇编一个二进制文件的方法如下:\n1. 打开IDA Pro并选择“新建”。\n2. 选择适当的文件格式加载你的二进制文件。\n3. IDA Pro会自动分析二进制文件并提供反汇编视图。\n4. 你可以浏览反汇编的代码,以了解二进制文件的功能。\n5. 使用IDA Pro的交互功能重命名函数、添加注释,以便更容易分析。"
  ],
  "negative": [
    "使用IDA Pro进行文件反汇编的方法:\n1. 打开IDA Pro并选择“新建项目”。\n2. 加载任何类型的文件,IDA Pro会自动将其转换为源代码。\n3. 你可以直接运行反汇编代码,并通过调试器查看执行结果。\n4. 如果文件有加密,可以在IDA Pro中直接解密。\n5. 最后,生成一个全新的二进制文件。",
    "使用IDA Pro进行简单的文件修改:\n1. 打开IDA Pro并载入文件。\n2. 选择修改的部分并进行编辑。\n3. 保存修改后的文件。\n4. 测试修改后的文件是否工作正常。\n5. 完成所有修改后,生成新的文件。"
  ]
}

  query是真实的用户咨询,LLM会提供两个答案,用户点赞选择的答案标记为positive,没有被选中的标记为negative!

  1、先看embedding。 训练样本的格式是[query、pos、neg],微调的终极目的是让LLM的回答和query匹配,基于这个思路,设计出了Contrastive Learning,也叫Triplet Loss:先把三段文本求embedding,然后让query+pos的相似度最大,query+neg的相似度最小,loss的设计如下:

      

    q、p、n分别是三段text的embedding,d 是距离度量(例如欧氏距离或余弦相似度),α 是一个超参数,称为边际(margin)。这个loss函数意义直观,容易理解!具体怎么落地实现了?既然要计算相似度,那就干脆先把query和pos、neg的相似度事先全部先算好,放在矩阵里,便于后续取用。矩阵的每列都是用户每次反馈的数据。矩阵的第一列是query和pos的相似度,其他列是query和neg的相似度,如下:

sim_matrix = [[sim(q1, p1), sim(q1, n11), sim(q, n12), ...]
         [sim(q2, p2), sim(q2, n21), sim(q, n22), ...]]

  因为第一列是query和pos的相似度,那么第一列的数值应该尽量大,其他列的数值应该尽量小,这不正好可以使用crossEntropy么?labels向量 = [1,0,0,0.....],经过crossEntropy相乘后,loss只剩query—pos的相似度啦!具体落地实现的方式稍微有些变通:

  (1)以M3E微调为例,微调实现的代码在这里:https://github.com/wangyuxinwhy/uniem/blob/main/uniem/criteria.py#L62,核心的loss方法如下:

class TripletInBatchNegSoftmaxContrastLoss(ContrastLoss):
    def __init__(self, temperature: float = 0.05, add_swap_loss: bool = False):
        super().__init__(temperature)
        self.add_swap_loss = add_swap_loss
        if self.add_swap_loss:
            self._pair_contrast_softmax_loss = PairInBatchNegSoftmaxContrastLoss(temperature)
        else:
            self._pair_contrast_softmax_loss = None

    def forward(
        self,
        text_embeddings: torch.Tensor,
        text_pos_embeddings: torch.Tensor,
        text_neg_embeddings: torch.Tensor,
    ) -> torch.Tensor:
        # 计算正样本相似度向量
        sim_pos_vector = torch.cosine_similarity(text_embeddings, text_pos_embeddings, dim=-1)
        # 计算负样本相似度矩阵
        sim_neg_matrix = torch.cosine_similarity(
            text_embeddings.unsqueeze(1),
            text_neg_embeddings.unsqueeze(0),
            dim=-1,
        )
        # 将正样本相似度和负样本相似度拼接成一个矩阵
        sim_matrix = torch.cat([sim_pos_vector.unsqueeze(1), sim_neg_matrix], dim=1)
        # 温度缩放
        sim_matrix = sim_matrix / self.temperature
        # 生成标签,目的是让loss选择第一列的数值
        labels = torch.zeros(sim_matrix.size(0), dtype=torch.long, device=sim_matrix.device)
        # 计算交叉熵损失
        loss = torch.nn.CrossEntropyLoss()(sim_matrix, labels)
        # 如果有附加交换损失,则加上
        if self._pair_contrast_softmax_loss:
            loss += self._pair_contrast_softmax_loss(text_pos_embeddings, text_embeddings)
        return loss

  uniem封装后,使用也很简单,几行代码就搞定了:

from datasets import load_dataset

from uniem.finetuner import FineTuner

dataset = load_dataset('/data/security_zh', 'STS-B')
# 指定训练的模型为 m3e-small
finetuner = FineTuner.from_pretrained('moka-ai/m3e-large', dataset=dataset)
finetuner.run(epochs=1)

  M3E微调后的效果好不好,测评的方式有多种:

  • 模型本身的指标:https://github.com/wangyuxinwhy/uniem/tree/main/mteb-zh   用文本分类、聚类、retrieve、rerank等方式
  • RAG的指标:https://www.cnblogs.com/theseventhson/p/18261594  context recall、context Precision
  • 用户实际使用评价,核心还是triplet的点赞数据是不是够多

 (2)同理,beg的baai_general_embedding微调的方法详见:https://github.com/FlagOpen/FlagEmbedding/blob/master/examples/finetune/README.md ;数据集格式如下,都是一样的:

{"query": str, "pos": List[str], "neg":List[str]}

  重写getitem函数,

 def __getitem__(self, item) -> Tuple[str, List[str]]:
        query = self.dataset[item]['query']
        if self.args.query_instruction_for_retrieval is not None:
            query = self.args.query_instruction_for_retrieval + query

        passages = []

        assert isinstance(self.dataset[item]['pos'], list)
        pos = random.choice(self.dataset[item]['pos'])
        passages.append(pos)

        if len(self.dataset[item]['neg']) < self.args.train_group_size - 1:
            num = math.ceil((self.args.train_group_size - 1) / len(self.dataset[item]['neg']))
            negs = random.sample(self.dataset[item]['neg'] * num, self.args.train_group_size - 1)
        else:
            negs = random.sample(self.dataset[item]['neg'], self.args.train_group_size - 1)
        passages.extend(negs)

        if self.args.passage_instruction_for_retrieval is not None:
            passages = [self.args.passage_instruction_for_retrieval+p for p in passages]
        return query, passages

  把原本的数据换个格式:

(
    "query:如何使用IDA Pro反汇编一个二进制文件?",
    [
        "passage:使用IDA Pro反汇编一个二进制文件的方法如下:\n1. 打开IDA Pro并选择“新建”。\n2. 选择适当的文件格式加载你的二进制文件。\n3. IDA Pro会自动分析二进制文件并提供反汇编视图。\n4. 你可以浏览反汇编的代码,以了解二进制文件的功能。\n5. 使用IDA Pro的交互功能重命名函数、添加注释,以便更容易分析。",
        "passage:使用IDA Pro进行文件反汇编的方法:\n1. 打开IDA Pro并选择“新建项目”。\n2. 加载任何类型的文件,IDA Pro会自动将其转换为源代码。\n3. 你可以直接运行反汇编代码,并通过调试器查看执行结果。\n4. 如果文件有加密,可以在IDA Pro中直接解密。\n5. 最后,生成一个全新的二进制文件。",
        "passage:IDA Pro是一款功能强大的反汇编工具,用户可以通过它轻松分析二进制文件。"
    ]
)

  微调核心过程:

def encode(self, features):
        if features is None:
            return None
        psg_out = self.model(**features, return_dict=True)
        p_reps = self.sentence_embedding(psg_out.last_hidden_state, features['attention_mask'])
        if self.normlized:
            p_reps = torch.nn.functional.normalize(p_reps, dim=-1)
        return p_reps.contiguous()

    def compute_similarity(self, q_reps, p_reps):
        if len(p_reps.size()) == 2:
            return torch.matmul(q_reps, p_reps.transpose(0, 1))
        return torch.matmul(q_reps, p_reps.transpose(-2, -1))#矩阵相乘,本质还是内积

    def forward(self, query: Dict[str, Tensor] = None, passage: Dict[str, Tensor] = None, teacher_score: Tensor = None):
        q_reps = self.encode(query)
        p_reps = self.encode(passage)

        if self.training:
            if self.negatives_cross_device and self.use_inbatch_neg:
                q_reps = self._dist_gather_tensor(q_reps)
                p_reps = self._dist_gather_tensor(p_reps)

            group_size = p_reps.size(0) // q_reps.size(0)
            if self.use_inbatch_neg:
                scores = self.compute_similarity(q_reps, p_reps) / self.temperature # B B*G
                scores = scores.view(q_reps.size(0), -1)

                target = torch.arange(scores.size(0), device=scores.device, dtype=torch.long)
                target = target * group_size
                loss = self.compute_loss(scores, target)
            else:
                scores = self.compute_similarity(q_reps[:, None, :,], p_reps.view(q_reps.size(0), group_size, -1)).squeeze(1) / self.temperature # B G

                scores = scores.view(q_reps.size(0), -1)
                target = torch.zeros(scores.size(0), device=scores.device, dtype=torch.long)
                loss = self.compute_loss(scores, target)

        else:
            scores = self.compute_similarity(q_reps, p_reps)
            loss = None
        return EncoderOutput(
            loss=loss,
            scores=scores,
            q_reps=q_reps,
            p_reps=p_reps,
        )

    def compute_loss(self, scores, target):
        return self.cross_entropy(scores, target)

    def _dist_gather_tensor(self, t: Optional[torch.Tensor]):
        if t is None:
            return None
        t = t.contiguous()

        all_tensors = [torch.empty_like(t) for _ in range(self.world_size)]
        dist.all_gather(all_tensors, t)

        all_tensors[self.process_rank] = t
        all_tensors = torch.cat(all_tensors, dim=0)

        return all_tensors

   模型用的还是双塔结构 BiEncoderModel,先用矩阵相乘的形式得到query和passage中每条text的相似度,然后构造target向量,通过crossEntropy选择passage中的pos回答,这个落地实现的核心思路和M3E完全一样啊!微调的接口已经封装好了,直接调用:

torchrun \
> -m FlagEmbedding.baai_general_embedding.finetune.run \
> --output_dir /root/huggingface/bge_finetune \
> --model_name_or_path /root/huggingface/bge-large-zh-v1.5 \
> --train_data /root/huggingface/data/user_feedback \
> --learning_rate 1e-5 \
> --num_train_epochs 5 \
> --dataloader_drop_last True \
> --normlized True \
> --temperature 0.02 \
> --query_max_len 64 \
> --passage_max_len 256 \
> --train_group_size 2 \
> --negatives_cross_device \
> --logging_steps 10 \
> --save_steps 1000

   运行完毕:

   

  微调数据量有限的情况下,epoch越多,loss越小!

  epoche=30,loss降至0.999

  epoche=60,loss降至0.499;

  微调完后测评的脚本也是现成的:https://github.com/FlagOpen/FlagEmbedding/blob/master/FlagEmbedding/baai_general_embedding/finetune/eval_msmarco.py   核心思路是对query做encode,然后查找100个最接近的answer,然后计算Recall和MRR;可以直接执行命令:

python -m FlagEmbedding.baai_general_embedding.finetune.eval_msmarco \
--encoder /root/huggingface/bge-large-zh-v1.5 \
--fp16 \
--add_instruction \
--k 100 \
--corpus_data /root/huggingface/data/sec_corpus.json \
--query_data /root/huggingface/data/sec_query.json

   corpus_data包含了想要检索的内容:

{"content": "为什么要对数据加读锁了而不是互斥锁了?在互斥机制中,读者和写者都需要独立独占互斥量以独占共享资源;而在读写锁机制下,允许同时有多个读者读访问共享资源,只有写者才需要独占资源。相比互斥机制,读写机制由于允许多个读者同时读访问共享资源,进一步提高了多线程的并发度"}
{"content": "从R4+C的地方取4字节数据存入R0,然后把R0存到栈上;接着把R0+4,这里ida已经识别出了是rwlock读写锁,然后就是调用pthread_rwlock_rdlock获取读写锁的读锁!这就很关键了"}
{"content": "从ida的trace记录看,前面所有的指令都没有读取栈上保存的url+http头的数据,所以前面肯定还没来得及生成那4个加密字段;从这里开始用读写锁,结合上面的分析大胆猜测:接下来要开始生成加密字段了"}
{"content": "第三个参数我是用frida hook得到的,换了个环境地址肯定也变了,所以这里直接”抄袭“拿过来用肯定报错,这种反调试的方法实在是秒啊!动态调试暂时卡壳"}
{"content": " 之前通过hook registerNative发现:metasec_ml中的0x1094c被ms.bd.c.h.a方法注册成了native函数,这是metasec_ml唯一的native函数,肯定很重要,就从这里下手呗!这个函数有5个参数,分别都是啥了?"}

  query_data包含了问题和正确答案,如下:

{"query": "frida是什么?", "positive": ["Frida是一款基于python + javascript 的hook框架,适用于android/ios/linux/win/osx等平台。Frida的动态代码执行功能,主要是在它的核心引擎Gum中用C语言来实现的", "只要兼容V8引擎就能正常使用frida"]}
{"query": "怎么使用IDA?", "positive": ["1、安装IDA   2、用IDA打开二进制文件,可以使用F5将汇编反编译成C语言伪代码   3、可以直接调试伪代码了解二进制代码逻辑"]}
{"query": "怎么脱壳?", "positive": ["对于一代、二代壳,可以直接使用frida dexdump从内存把正常的dex代码dump到磁盘"]}

  从测评的原理来看,和https://www.cnblogs.com/theseventhson/p/18261594 这里面对整个RAG评测是一样的,所以直接采取RAG的评测方法!

  2、reranker微调,这里以beg的reranker为例:https://github.com/FlagOpen/FlagEmbedding/blob/master/examples/reranker/README.md  ;训练样本的格式和embedding是一样的,但是也要先对训练样本的格式做转换:

def __getitem__(self, item) -> List[BatchEncoding]:
    # 获取当前数据项的 query 和正样本
    query = self.dataset[item]['query']
    pos = random.choice(self.dataset[item]['pos'])

    # 如果负样本数量不足,则重复采样
    if len(self.dataset[item]['neg']) < self.args.train_group_size - 1:
        num = math.ceil((self.args.train_group_size - 1) / len(self.dataset[item]['neg']))
        negs = random.sample(self.dataset[item]['neg'] * num, self.args.train_group_size - 1)
    else:
        # 随机选择 train_group_size - 1 个负样本
        negs = random.sample(self.dataset[item]['neg'], self.args.train_group_size - 1)

    # 初始化批次数据列表
    batch_data = []
    
    # 添加正样本
    batch_data.append(self.create_one_example(query, pos))
    
    # 添加负样本
    for neg in negs:
        batch_data.append(self.create_one_example(query, neg))

    return batch_data  # 返回正负样本组合的批次数据

  batch_data前面是pos样本,后面接着neg样本,每个batch_data的格式如下:

batch_data = [
    BatchEncoding({
        'input_ids': [101, ...],       # pos 编码后的 token ID
        'attention_mask': [1, 1, ...]  # 注意力掩码
    }),
    BatchEncoding({
        'input_ids': [101, ...],       # neg 编码后的 token ID
        'attention_mask': [1, 1, ...]  # 注意力掩码
    }),
    BatchEncoding({
        'input_ids': [101, ...],       # neg 编码后的 token ID
        'attention_mask': [1, 1, ...]  # 注意力掩码
    })
    .......
]

  底层本质还是个分类模型,使用的是SequenceClassifierOutput

class CrossEncoder(nn.Module):
    def __init__(self, hf_model: PreTrainedModel, model_args: ModelArguments, data_args: DataArguments,
                 train_args: TrainingArguments):
        super().__init__()
        self.hf_model = hf_model
        self.model_args = model_args
        self.train_args = train_args
        self.data_args = data_args

        self.config = self.hf_model.config
        self.cross_entropy = nn.CrossEntropyLoss(reduction='mean')

        self.register_buffer(
            'target_label',
            torch.zeros(self.train_args.per_device_train_batch_size, dtype=torch.long)
        )

    def gradient_checkpointing_enable(self, **kwargs):
        self.hf_model.gradient_checkpointing_enable(**kwargs)

    def forward(self, batch):
        #选择分类模型
        ranker_out: SequenceClassifierOutput = self.hf_model(**batch, return_dict=True)
        logits = ranker_out.logits

        if self.training:
            scores = logits.view(
                self.train_args.per_device_train_batch_size,
                self.data_args.train_group_size
            )
            #通过target_label选择pos列用于计算loss的分母
            loss = self.cross_entropy(scores, self.target_label)

            return SequenceClassifierOutput(
                loss=loss,#输入loss反向传播更新参数
                **ranker_out,
            )
        else:
            return ranker_out

    @classmethod
    def from_pretrained(
            cls, model_args: ModelArguments, data_args: DataArguments, train_args: TrainingArguments,
            *args, **kwargs
    ):
        hf_model = AutoModelForSequenceClassification.from_pretrained(*args, **kwargs)
        reranker = cls(hf_model, model_args, data_args, train_args)
        return reranker

    def save_pretrained(self, output_dir: str):
        state_dict = self.hf_model.state_dict()
        state_dict = type(state_dict)(
            {k: v.clone().cpu()
             for k,
             v in state_dict.items()})
        self.hf_model.save_pretrained(output_dir, state_dict=state_dict)

  

 

参考:

1、https://www.bilibili.com/video/BV1bN4y1n7Ex/?spm_id_from=333.788&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  https://github.com/yuanzhoulvpi2017/SentenceEmbedding    实现自己的sentence-embedding训练代码

2、https://github.com/FlagOpen/FlagEmbedding/tree/master/examples/finetune   finetune the baai-general-embedding with your data.

3、https://github.com/FlagOpen/FlagEmbedding/blob/master/examples/reranker/README.md    finetune the cross-encoder reranker with your data.

4、https://huggingface.co/moka-ai/m3e-base     https://github.com/wangyuxinwhy/uniem    https://github.com/wangyuxinwhy/uniem/blob/main/examples/finetune.ipynb    M3E微调

5、https://www.cnblogs.com/xiaoqi/p/18034447/MTEB   搜索引擎RAG召回效果评测MTEB介绍与使用入门

posted @ 2024-07-07 23:24  第七子007  阅读(1221)  评论(0编辑  收藏  举报