代码大模型Wavecoder学习笔记及代码实践

目录

学习笔记

摘要(Abstract)

介绍(Introduction)

CodeSeaXDataset:四任务代码相关指令数据

四任务信息

增强指令生成

1. 原始代码收集(WaveCoder-main\WaveCoder-main\src\data\raw_code_collection)

2. 基于LLM的生成器-鉴别器框架

实验

设置

结果

代码生成任务评估:

其他代码相关任务评估:

消融与分析

代码相关任务的消融

关于数据泄露的讨论

相关工作

指令调优

Code LLM

结论

代码实践

环境配置

1. raw_code_collection

2. llm_gen_dis


学习笔记

原文:2312.14187 (arxiv.org)

Github链接:https://github.com/microsoft/WaveCoder


摘要(Abstract)

最近的研究表明,在指令调整之后,代码大型语言模型(Code LLMs)可以获得令人印象深刻的功能,以解决各种与代码相关的任务。然而,目前Code LLMs的指令调优方法主要集中在传统的代码生成任务上,导致在复杂的多任务场景下性能不佳。

本篇论文:WaveCoder,一系列使用广泛和通用增强指令数据训练的Code LLMs(a series of Code LLMs trained with Widespread And Versatile Enhanced instruction data )

  • 提出了一种在多任务场景下从开源代码数据集中稳定生成多样化、高质量的指令数据的方法,并得到CodeSeaXDataset,该数据集包含4个代码相关任务的19,915个指令实例,旨在提高Code LLM的泛化能力。

研究结果:

  • WaveCoder 模型在跨不同代码相关任务的泛化能力方面明显优于其他开源模型。

  • WaveCoder-Ultra-6.7B 在各种与代码相关的任务上提供了最先进的泛化能力。


介绍(Introduction)

之前的几项工作已经成功证明,在代码语料库上进行预训练(研究各种指令数据生成方法 -> 指令调优)可以显著提高模型处理代码相关问题的能力。这些工作主要集中在传统的代码生成任务上,缺乏在多任务场景中生成详细的、特定于任务的指令的能力。

本文主要关注多个与代码相关的任务,旨在生成针对特定任务要求量身定制的高质量和多样化的教学数据。

指令实例分类:通过将指令实例分类为CodeXGLUE Lu et al. (2021)中的四个通用代码相关任务来细化指令数据:

  1. 代码摘要

  2. 代码生成

  3. 代码转换

  4. 代码修复

提出增强指令生成方法:可以充分利用开源代码数据,并在多任务场景中稳定地生成高质量和多样化的指令数据。通过这种生成策略,我们获得了一个包含 19,915 个指令实例的数据集,这些指令实例跨越四个与代码相关的任务,称为 CodeSeaXDataset

实验验证(WaveCoder):用最初的 CodeSeaXDataset 数据集训练 StarCoder Li et al. ( 2023a)、CodeLLaMa Roziere et al. ( 2023) 和 DeepseekCoder Guo et al. ( 2024) 并得到 WaveCoder。在对 HumanEval Chen et al. (2021)、MBPP Austin et al. (2021)、HumanEvalPack Muennighoff et al. (2024) 基准进行全面评估后,实验结果表明,我们的 WaveCoder 基于广泛且通用的增强指令调优,表现出出色的泛化能力。

微调再验证(WaveCoder-Ultra-6.7B):使用 GPT-4 OpenAI ( 2023) 对 CodeSeaXDataset 中的指令重新生成响应。通过增强的 20K CodeSeaXDataset 数据集进行微调,我们得到了 WaveCoder-Pro-6.7B,它在 HumanEval Chen et al. ( 2021) 上实现了 72.0% 的pass@1,超过了开源Code LLMs,但仍落后于 SoTA Code LLM。将增强的 CodeSeaXDataset 与 WaveCoder-evol-codealpaca(净化后的 Magicoder-evol-codealpaca 2 数据集)相结合,我们提出了 WaveCoder-Ultra-6.7B,该 WaveCoder-Ultra-6.7B 具有多个代码相关任务的 SoTA 泛化功能。


CodeSeaXDataset:四任务代码相关指令数据

四任务信息

从三个生成任务(代码到文本、文本到代码、代码到代码)中选择了四个最具普遍代表性和最常见的任务,包括代码摘要、代码生成、代码转换和代码修复。

  • 代码摘要(代码到文本):此任务旨在创建给定代码的简短摘要。原始代码用作输入,教师模型的响应被重新表述为指令格式。

  • 代码生成(文本到代码、代码到代码):在此任务中,模型应根据用户的需求描述生成代码。因此,教师模型应生成指令和解决方案代码,给定原始代码作为指令-解决方案对。然后,将生成的解决方案代码视为输出。

  • 代码转换(代码到代码):此任务涉及将一种编程语言转换为另一种编程语言。特定于任务的提示和原始代码被提供给教师模型,然后模型生成指令和翻译的代码。

  • 代码修复(代码到代码):此任务的目的是根据给定代码中的潜在问题提供正确的代码。教师模型应为不正确的代码生成解决方案,通常使用正确的代码和一些描述,然后将其作为输出。

增强指令生成

为了保证指令实例的数据质量和多样性,提出了一种广泛且通用的增强指令生成方法,包括以下两部分:

  1. 一种通过最大限度地保留原始代码的多样性来保留指令数据多样性的方法。

  2. 基于LLMGenerator-Discriminator框架,稳定生成高质量的指令数据。

1. 原始代码收集(WaveCoder-main\WaveCoder-main\src\data\raw_code_collection)

选择 CodeSearchNet ,其中包含来自 GitHub 上托管的开源库的 200 万个<注释、代码>对作为我们的基础数据集,并按照以下步骤对其进行处理:

[基础数据集CodeSearchNet]  https://huggingface.co/datasets/code_search_net

手动定义筛选规则(保证代码高质量)

  • 对代码进行了过滤,以确保所需代码的长度既不太长也不太短。

  • 遵循 Code Alpaca Chaudhary ( 2023),从黑名单中删除了包含单词的原始代码,这可能会降低生成模型的性能。

核心集选择方法(保证代码多样性):采用了KCenterGreedy Sener和Savarese(2018)算法,该算法已被证明可以有效地获得一个发行版的一组核心样本,根据同一嵌入模型编码的代码嵌入从开源代码数据集中选择代表性样本(roberta-large-v1 Liu et al. (2019))。

通过将这种方法整合到开源代码数据集中,生成数据的多样性不再仅仅依赖于教师LLM本身或初始种子的能力。此外,由于KCenterGreedy算法的应用,语言的多样性也得到了显著保留。

# 原始代码收集
def get_code_embedding(
    data: List[str], model_name: str, batch_size: int
) -> List[Dict[str, Any]]:
    """
    使用句子转换器模型为代码片段列表生成嵌入向量。
​
    参数:
    data (List[str]): 要嵌入的代码片段列表。
    model_name (str): 要使用的句子转换器模型的名称。
    batch_size (int): 嵌入生成的批次大小。
​
    返回:
    List[Dict[str, Any]]: 包含 'text' 和 'embedding' 键的字典列表。
    """
    model = SentenceTransformer(model_name)     # 创建了一个SentenceTransformer对象,这个对象用于生成文本的嵌入向量。
    embeddings = model.encode(data, batch_size=batch_size, show_progress_bar=True)
    res = [{"text": t, "embedding": e.tolist()} for t, e in zip(data, embeddings)]
​
    return res
​
​
def coreset(embeddings: np.ndarray, num: int, seed: int) -> np.ndarray:
    """
    使用 k-Center Greedy算法从一组嵌入向量中选择一个核心集。
​
    参数:
    embeddings (np.ndarray): 嵌入向量的数组。
    num (int): 选择用于核心集的元素数量。
​
    返回:
    np.ndarray: 包含核心集元素的数组。
    """
    kcg = kCenterGreedy(X=embeddings, y=None, seed=seed)
    batch = kcg.select_batch_(model=None, already_selected=[], N=num)
    return embeddings[batch]

[k-Center Greedy(k-中心贪婪)]  是一种解决最小最大设施选址问题(minimax facility location problem)的方法,即从完整数据集T中选择k个样本作为核心集合S,使得T中的数据点与S中最近的数据点之间的最大距离最小化

k-Center Greedy已成功应用于广泛的领域,例如主动学和高效的GAN训练。k-Center Greedy的目标是选择一组k个样本,使得它们能够最好地代表整个数据集,并且能够满足最小最大距离的要求。算法通过贪婪地选择距离当前核心集合最远的样本,逐步构建核心集合。k-Center Greedy算法的优势在于它的高效性和可扩展性。它可以在大规模数据集上进行快速计算,并且可以通过调整k的值来控制核心集合的大小和代表性。这使得k-Center Greedy成为许多应用中的有用工具,特别是在需要选择一小部分样本来代表整个数据集的问题中。

核心做法就是首先选一个初始点,然后每次迭代都选取离当前中心集合最远的点,最终得到一份Seed Instruction Data

# k-Center Greedy算法
""" 返回最小化任意点到中心的最大距离的点。
​
实现了 Ozan Sener 和 Silvio Savarese 提出的 k-Center-Greedy 方法。
在他们的论文《A Geometric Approach to Active Learning for Convolutional Neural Networks》中有所描述。
链接:https://arxiv.org/abs/1708.00489
​
距离度量默认为 l2 距离(欧氏距离)。用于计算距离的特征是原始特征,或者如果模型有 transform 方法,
则使用 model.transform(X) 的输出。
​
可以扩展为一种鲁棒的 k 中心算法,该算法忽略一定数量的异常数据点。得到的中心点是多个整数规划问题的解。"""
import numpy as np
from sklearn.metrics import pairwise_distances
from utils.sampling_def import SamplingMethod
​
​
class kCenterGreedy(SamplingMethod):
​
    def __init__(self, X, y, seed, metric="euclidean"):
        self.X = X
        self.y = y
        self.flat_X = self.flatten_X()
        self.name = "kcenter"
        self.features = self.flat_X
        self.metric = metric
        self.min_distances = None
        self.n_obs = self.X.shape[0]
        self.already_selected = []
​
    def update_distances(self, cluster_centers, only_new=True, reset_dist=False):
        """Update min distances given cluster centers.
​
        Args:
          cluster_centers: indices of cluster centers
          only_new: only calculate distance for newly selected points and update
            min_distances.
          rest_dist: whether to reset min_distances.
        """
​
        if reset_dist:
            self.min_distances = None
        if only_new:
            cluster_centers = [
                d for d in cluster_centers if d not in self.already_selected
            ]
        if cluster_centers:
            # Update min_distances for all examples given new cluster center.
            x = self.features[cluster_centers]
            dist = pairwise_distances(self.features, x, metric=self.metric)
​
            if self.min_distances is None:
                self.min_distances = np.min(dist, axis=1).reshape(-1, 1)
            else:
                self.min_distances = np.minimum(self.min_distances, dist)
​
    def select_batch_(self, model, already_selected, N, **kwargs):
        """
        Diversity promoting active learning method that greedily forms a batch
        to minimize the maximum distance to a cluster center among all unlabeled
        datapoints.
​
        Args:
          model: model with scikit-like API with decision_function implemented
          already_selected: index of datapoints already selected
          N: batch size
​
        Returns:
          indices of points selected to minimize distance to cluster centers
        """
​
        try:
            # Assumes that the transform function takes in original data and not
            # flattened data.
            print("Getting transformed features...")
            self.features = model.transform(self.X)
            print("Calculating distances...")
            self.update_distances(already_selected, only_new=False, reset_dist=True)
        except:
            print("Using flat_X as features.")
            self.update_distances(already_selected, only_new=True, reset_dist=False)
​
        new_batch = []
​
        for _ in range(N):
            if self.already_selected is None:
                # Initialize centers with a randomly selected datapoint
                ind = np.random.choice(np.arange(self.n_obs))
            else:
                ind = np.argmax(self.min_distances)
            # New examples should not be in already selected since those points
            # should have min_distance of zero to a cluster center.
            assert ind not in already_selected
​
            self.update_distances([ind], only_new=True, reset_dist=False)
            new_batch.append(ind)
        print(
            "Maximum distance from cluster centers is %0.2f" % max(self.min_distances)
        )
​
        self.already_selected = already_selected
​
        return new_batch
2. 基于LLM的生成器-鉴别器框架

下一步是生成指令数据,以便从原始代码进行监督微调。

为了进一步确保生成的指令数据的质量,提出了一个LLM基于生成器-鉴别器的框架,其中生成器可以利用大量的无监督开源代码来生成监督指令数据,鉴别器可以对指令数据中的每个组件生成分析。

  • 生成阶段:利用 GPT-4 为每个与代码相关的任务生成定义。如图2所示,按照模型生成的任务定义,手动开发每个与代码相关的任务的生成要求。将任务定义和所有相关需求集成到生成提示中,将原始代码作为输入,并从示例数据库中选择不同的示例,通过 GPT-3.5 生成指令数据。

  • 鉴别阶段:为了增强数据生成的可控性,进一步保证数据质量,采用GPT-4作为基于LLM判别器的判别器,对指令数据进行持续的分析和过滤。

    建立了一系列规则,并将它们分解为一些子主题,以确保基于 LLM的判别器可以逐步分析生成的判别准确性。示例如下所示:

  • 在判别过程之后,每个指令实例被分类为好或坏的情况,随后在下一代中随机选择分类信息作为示例。

    与仅以初始种子任务为好示例的自指令不同,这里同时利用了好生成和坏生成作为少样本,以便生成器可以从不同坏示例中的错误中学习。因此,该框架提供了一种生成和评估指令数据的综合方法,确保了高质量的训练数据集。

def few_shot_task_gen(
    source_code: List[Dict],
    gen_prompt: str,
    dis_prompt: str,
    good_case_path: str,
    bad_case_path: str,
    sample_number: int,
    engine: str,
    api_key: str,
    base_url: str,
    gen_max_token: int = 800,
    data_stream_path: str = "data_stream.txt",
) -> List[Dict]:
    """
    生成少量样本任务并处理数据。

    参数:
    source_code (List[Dict]): 源代码数据的列表,每个元素是一个字典。
    gen_prompt (str): 生成器的提示文本。
    dis_prompt (str): 鉴别器的提示文本。
    good_case_path (str): 保存好案例的路径。
    bad_case_path (str): 保存坏案例的路径。
    sample_number (int): 少量学习样本的数量。
    engine (str): 要使用的OpenAI引擎。
    api_key (str): OpenAI的API密钥。
    base_url (str): OpenAI API的基础URL。
    gen_max_token (int): 生成器的最大令牌长度。
    data_stream_path (str): 保存数据流的路径。

    返回值:
    List[Dict]: 包含生成数据的字典列表。
    """

    good_prompt = "Here are some good examples:\n"
    bad_prompt = "Here are some bad examples. In each example, I also provide an <Analysis> pointing out the reasons why the case is not good. Please do not generate data like this.\n"
    #
    GoodCaser = GoodCase(good_case_path, good_prompt, sample_number=sample_number)
    BadCaser = BadCase(bad_case_path, bad_prompt, sample_number=sample_number)
    print(f"Good Case: {len(GoodCaser.sample_list)}")
    print(f"Bad Case: {len(BadCaser.sample_list)}")
    g_prompt = gen_prompt
    d_prompt = dis_prompt

    data_list = []
    with open(data_stream_path, "w") as f:
        for data in tqdm(source_code):
            ids = data["id"]
            # print("The code for example " + str(ids+1) + " is generating.")

            example_code = data["text"]

            good_few_shot = GoodCaser.generate_fewshot_text()
            bad_few_shot = BadCaser.generate_fewshot_text()
            example = {
                "good_few_shot": good_few_shot,
                "bad_few_shot": bad_few_shot,
                "input": example_code,
            }

            message = g_prompt.format_map(example)
            print(message)
            text = make_request(
                message=message, model=engine, api_key=api_key, base_url=base_url
            )
            if not text:
                raise Exception("No text generated")
            # print(text)
            # analysis filter
            text, analysis_content = analysis_filter(text)
            filter_messages = extract_message(text)
            if filter_messages["quality"] == False:
                continue
            generated_text = filter_messages["generated_text"]
            example_case = f"Input: \n{example_code}\n\nOutput:\n{generated_text}"

            # example_case = f"Output:\n{text}"
            ans = discriminator(
                prompt=d_prompt,
                GoodCaser=GoodCaser,
                BadCaser=BadCaser,
                engine=engine,
                api_key=api_key,
                base_url=base_url,
                generated_text=example_case,
            )
            if "no" not in ans:
                result_data = filter_messages["result"]
                assert result_data != None and len(result_data) > 0
                data_list.append(
                    {
                        "id": ids,
                        "task_type": "code generation",
                        "source_code": example_code,
                        "generation_data": result_data,
                    }
                )
                f.write(
                    json.dumps(
                        {
                            "id": ids,
                            "task_type": "code generation",
                            "source_code": example_code,
                            "generation_data": result_data,
                        }
                    )
                )
                f.write(",\n")

                time.sleep(2)

    if not data_list:
        raise Exception("No data generated")

    else:
        print(f"data_list={len(data_list)}\nsome examples are:")
        for key, value in data_list[0].items():
            print(f"{key}:{value}")

    return data_list


def discriminator(
    prompt: str,
    GoodCaser: GoodCase,
    BadCaser: BadCase,
    engine: str,
    api_key: str,
    base_url: str,
    generated_text: str,
    max_token: int = 500,
) -> str:
    """
    使用鉴别器对生成的文本进行好坏分类。

    参数:
    prompt (str): 鉴别器的提示文本。
    GoodCaser (GoodCase): GoodCase类的一个实例,用于好例子。
    BadCaser (BadCase): BadCase类的一个实例,用于坏例子。
    engine (str): 要使用的OpenAI引擎。
    api_key (str): OpenAI的API密钥。
    base_url (str): OpenAI API的基础URL。
    generated_text (str): 需要被分类的生成文本。
    max_token (int): 鉴别器的最大令牌长度。

    返回值:
    str: 鉴别器给出的总体答案。
    """

    prompt_format = """
{prompt}\n{bad_examples}\n\n{generated_text}\n\nAnalysis:\n
    """

    good_examples = GoodCaser.generate_fewshot_for_d()
    bad_examples = BadCaser.generate_fewshot_for_d()
    # generate instruction
    example = {
        "prompt": prompt,
        "bad_examples": bad_examples,
        "generated_text": generated_text,
    }
    message = prompt_format.format_map(example)
    # obtain answer
    ans = make_request(
        message=message, model=engine, api_key=api_key, base_url=base_url
    )
    feedback, part_answer, overall_answer = extract_answer(ans)
    print(f"part_answer={part_answer}\toverall_answer={overall_answer}")

    if feedback is None:
        print("no pattern information was extracted in the feedback")
        return None

    generated_text = f"{generated_text}\n\nAnalysis:\n{feedback}\n"

    if overall_answer == "yes":
        GoodCaser.add_case(generated_text)
    else:
        BadCaser.add_case(generated_text)

    return overall_answer
 

实验

设置

生成了大约 20K 个数据集,涵盖了 4 个常见的代码相关任务,以增强代码LLM的泛化能力。

(WaveCoder)基础模型:StarCoder-15B、CodeLLaMa(7B 和 13B)、DeepseekCoder-6.7B

  • StarCoder-15B、CodeLLaMa(7B 和 13B):batch = 256 (Tensor Parallel),lr = 2e-5

  • DeepseekCoder-6.7B:batch = 512(Pytorch 的完全分片数据并行FSDP模块),lr = 5e-5。

三个代码基准(不同任务):HumanEval Chen et al. (2021)、MBPP Austin et al. (2021) 和 HumanEvalPack Muennighoff et al. (2024)

结果

代码生成任务评估:

代码生成任务基准:HumanEval、MBPP,结论如下:

  1. WaveCoder-Pro-6.7B :6.7B 参数和 20K 指令数据,优于其他开源模型。使用 GPT-4 增强的 CodeSeaXDataset 数据集进行训练,在 HumanEval 上实现了 72.0% 的pass@1,在 MBPP 上实现了 63.6%,超过了所有开源模型,但仍落后于专有模型和 SoTA 开源模型。

  2. 精细化、多样化的指令数据,显著了提高指令调优效率:尽管与 WizardCoder(50.5 对 57.3)和 Magicoder(64.0 对 66.5)相比,代码生成基准测试存在明显不足,但必须考虑训练数据量的巨大差异。此外,观察到 WaveCoder-pro-6.7B 明显优于 Magicoder-DS-6.7B(72.0 对 66.5),证明了数据质量和指令调优多样性的有效性。

其他代码相关任务评估:

HumanEvalFix(33.0 vs 25.7 vs 27.0)和HumanEvalExpre(30.8 vs 27.5 vs 24.5)基准测试,结果如下:

  1. WaveCoder 模型在其他与代码相关的任务上优于所有开源模型。在Starcoder的基础上提出的WaveCoder-SC表现出了卓越的性能,超越了WizardCoder和OctoCoder的功能。WaveCoder-DS-6.7B在HumanEvalFix上的平均pass@1得分为49.4%,在HumanEvalExplain上的平均得分为41.3%,超过了所有开源模型,并在多任务场景中表现出强大的泛化能力。

  2. 数据细化和多样化的增强可以显著提高多任务场景中指令调优的效率。数据细化和指令分类为四个与代码相关的任务,推动模型在各种与代码相关的任务中达到不可预见的泛化能力。值得注意的是,我们的 WaveCoder-DS-6.7B 模型在 HumanEvalFix 上的表现优于 GPT-4(49.4 对 47.8),从而强调了较小模型在有效优化时实现与参数密集型模型接近奇偶校验的潜力。

将 CodeSeaXDataset 与 WaveCoder-evol-codealpaca 结合成一个 130K 数据集。通过对两个数据集的组合进行微调,得到了 WaveCoder-Ultra-6.7B。如表 3、4、5 所示,WaveCoder-Ultra-6.7B 在广泛的代码相关任务上具有最先进的泛化能力,这再次凸显了我们的 CodeSeaXDataset 数据集的重要性,并展示了更大数据集的潜力。


消融与分析

代码相关任务的消融

  • 基础模型:DeepseekCoder-Base-6.7B

  • 基础数据集:初始 20K CodeSeaXDataset 数据

  • 基准:HumanEval、HumanEvalFix、HumanEvalExplain

CG:代码生成 CS:代码总结 CT:代码翻译 CR:代码修复

  1. 精细化的指令数据可以显著提高预训练模型的泛化能力:如表6所示,将所有4个与代码相关的任务合并到训练数据中,WaveCoder-DS-6.7B在所有任务的基准测试中实现了最佳性能。例如,代码修复任务的参与为 HumanEvalFix 带来了 33.7% 的绝对值,而其他任务没有任何显着下降,甚至 HumanEval 基准测试的绝对值提高了 3.1%。

  2. 不同的任务可以相互促进,使模型表现出泛化能力:从表 6 中,我们可以观察到,三个任务的任意组合导致的分数低于所有任务。例如,添加代码摘要任务后,在所有基准测试中都提供了适度但显著的平均改进。而且,没有任何任务会导致HumanEval的分数下降,这也体现了不同任务之间的相互促进。

关于数据泄露的讨论

三个代码指令数据集:Code Alpaca、CodeSeaXDataset、Magicoder-evol-codealpaca

CodeSeaXDataset 的平均余弦相似度低于其他数据集。

分析了所有基准测试,注意到 HumanEval 和 Magicoder-evol-codealpaca 数据集之间存在严重的数据泄露问题。--> 在 HumanEval 中对每个评估问题对 Magicoder-evol-codealpaca 进行净化,并得到 WaveCoder-evol-codealpaca。如图 3 所示,WaveCoder-evol-codealpaca 的相似度低于 Magicoder-evol-codealpaca。


相关工作

指令调优

  • FLAN Wei et al. ( 2022)、ExT5 Aribandi et al. ( 2022) 和 FLANT5 Chung et al. ( 2022):强调了在训练过程中整合不同任务的有效性,以提高预训练模型对下游任务的适应性。(对 1.8K 任务的指令调整表明,广泛且通用的增强指令数据集显着提高了语言模型的性能)

  • InstructGPT Ouyang et al. ( 2022):结合了由人类注释者制作的优质指令数据,在使模型输出与用户意图保持一致方面显示出巨大的前景,促使对指令调优机制进行进一步研究。

  • Stanford Alpaca Taori et al. ( 2023):通过自指令 Wang et al. ( 2023a) 创新地将 GPT 生成的指令数据用于指令调整过程。

  • WizardLM Xu et al. ( 2024):通过应用进化-指令方法建立在这些进步的基础上,共同阐明了指令调整对LLM整体能力的变革性影响。

Code LLM

  • CodeGen Nijkamp et al. ( 2022)、CodeT5 Wang et al. ( 2021)、StarCoder Li et al. ( 2023a)、CodeLLaMa Roziere et al. ( 2023) 和 Deepseek-Coder Guo et al. ( 2024) :推动了代码生成的最新进展,这些Code LLMs受益于对扩展代码语料库的广泛预训练。

  • 为了进一步提高效率和解决问题的能力,开发了指令调优模型,如 InstructCodeT5+ Wang et al. ( 2023b), WizardCoder Luo et al. ( 2024), Pangu-coder2 Shen et al. ( 2023), 然而,他们使用的所有指令数据都来自 Code Alpaca,在多任务环境的背景下还不够完善,这促使我们提出指令数据生成的新方法。


结论

本文介绍了 WaveCoder,这是一种使用广泛且通用的增强指令数据进行微调的Code LLM。通过使语言模型能够有效地处理复杂的代码相关任务,方法展示了将多个代码相关任务集成到代码LLM的指令调优中,并为多任务场景中的特定任务需求生成高质量和多样化的指令数据的潜力。WaveCoder 在不同的代码相关任务上实现了最先进的泛化性能,超过了现有的开源代码LLMs。此外,对不同任务关系的分析为未来的研究提供了有价值的见解,为更广泛的代码相关任务和更大的数据集铺平了道路。


代码实践

环境配置

        项目链接:https://github.com/microsoft/WaveCoder 

        首先,进入项目github链接,下载项目包至服务器/本地,按要求进行基础环境的配置:

conda create -n wavecoder python=3.9
conda activate wavecoder
cd src
pip install -r requirements.txt
pip install transformers==4.34.1
pip install flash-attn==2.5.5

        进入/src目录,可以发现Wavecoder的代码主要分为数据生成/评估/训练等部分,而其中数据的生成(data部分)为Wavecoder项目的核心,因此以/data为例,可以发现数据生成主要分为两个部分:

  1.         原始代码生成
  2.         生成器-鉴别器

1. raw_code_collection

        原始代码收集部分中,main函数下主要包含两个函数:

        首先通过get_code_embedding函数调用sentence_transformers库,输入:代码列表 输出:生成的嵌入向量,以便于使用kcentergreedy算法对代码片段进行排序。

def get_code_embedding(
    data: List[str], model_name: str, batch_size: int
) -> List[Dict[str, Any]]:
    """
    使用句子转换器模型为代码片段列表生成嵌入向量。

    参数:
    data (List[str]): 要嵌入的代码片段列表。
    model_name (str): 要使用的句子转换器模型的名称。
    batch_size (int): 嵌入生成的批次大小。

    返回:
    List[Dict[str, Any]]: 包含 'text' 和 'embedding' 键的字典列表。
    """
    model = SentenceTransformer(model_name)     # 创建了一个SentenceTransformer对象,这个对象用于生成文本的嵌入向量。
    embeddings = model.encode(data, batch_size=batch_size, show_progress_bar=True)
    res = [{"text": t, "embedding": e.tolist()} for t, e in zip(data, embeddings)]

    return res

        之后在coreset函数中,调用前文介绍的k-Center Greedy算法从一组嵌入向量中选择一个核心集。

def coreset(embeddings: np.ndarray, num: int, seed: int) -> np.ndarray:
    """
    使用 k-Center Greedy算法从一组嵌入向量中选择一个核心集。

    参数:
    embeddings (np.ndarray): 嵌入向量的数组。
    num (int): 选择用于核心集的元素数量。

    返回:
    np.ndarray: 包含核心集元素的数组。

    """
    kcg = kCenterGreedy(X=embeddings, y=None, seed=seed)
    batch = kcg.select_batch_(model=None, already_selected=[], N=num)
    return embeddings[batch]

        CodeSearchNet:code-search-net/code_search_net · Datasets at Hugging Face

        论文中采用CodeSearchNet作为基础数据集,我们将其下载到服务器/本地,同时解压并选取其中的一小部分python代码jsonl文件作为实验的测试文件:

        进入配置好环境的服务器,运行raw_code_collection中的main.py文件(这里我自己对参数进行了修改,设置好初始源代码文件、代码保存文件地址、核心集数量),这里我选择核心集数量为10,进行测试:

python main_shy.py --data_path /data/home/Shehongyu/Project/WaveCoder-main/src/data/raw_code_collection/python_test_0.jsonl --save_path /data/home/Shehongyu/Project/WaveCoder-main/src/data/raw_code_collection/output.jsonl --coreset_size 10

        运行后生成了output.jsonl文件,其中包含了使用k-center greedy算法筛选出的十条最优的代码数据:

2. llm_gen_dis

        接下来就是借助大语言模型对筛选后的代码数据进行整合分析,在其中两个关键函数如下(这里我对源代码进行了部分修改/改错,后面会进行说明):

        首先是few_shot_task_gen函数,该函数主要是根据raw_code_collection中筛选出的代码数据,结合提供的提示案例文本(生成器/鉴别器、好数据/坏数据?例子、判断依据等),通过openai的接口调用openai,进行生成。

        openai url:https://api.openai.com/v1/engines/davinci/completions

        openai model:text-davinci-003

        但处于某些原因,openai的接口不对某地区开放,我在这里尝试了很多方法都无法连接openai接口进行使用,所以我这里采用了国内的一个接口进行使用(免费!感谢大佬,可以从开源项目中免费获取接口密钥,进行使用)。

        url:https://api.chatanywhere.tech/v1/chat/completions

        model:gpt-3.5-turbo

        项目地址:GitHub - chatanywhere/GPT_API_free: Free ChatGPT API Key,免费ChatGPT API,支持GPT4 API(免费),ChatGPT国内可用免费转发API,直连无需代理。可以搭配ChatBox等软件/插件使用,极大降低接口使用成本。国内即可无限制畅快聊天。

def few_shot_task_gen(
    source_code: List[Dict],
    gen_prompt: str,
    dis_prompt: str,
    good_case_path: str,
    bad_case_path: str,
    sample_number: int,
    engine: str,
    api_key: str,
    base_url: str,
    gen_max_token: int = 800,
    data_stream_path: str = "data_stream.txt",
) -> List[Dict]:
    """
    生成少量样本任务并处理数据。

    参数:
    source_code (List[Dict]): 源代码数据的列表,每个元素是一个字典。
    gen_prompt (str): 生成器的提示文本。
    dis_prompt (str): 鉴别器的提示文本。
    good_case_path (str): 保存好案例的路径。
    bad_case_path (str): 保存坏案例的路径。
    sample_number (int): 少量学习样本的数量。
    engine (str): 要使用的OpenAI引擎。
    api_key (str): OpenAI的API密钥。
    base_url (str): OpenAI API的基础URL。
    gen_max_token (int): 生成器的最大令牌长度。
    data_stream_path (str): 保存数据流的路径。

    返回值:
    List[Dict]: 包含生成数据的字典列表。
    """

    good_prompt = "Here are some good examples:\n"
    bad_prompt = "Here are some bad examples. In each example, I also provide an <Analysis> pointing out the reasons why the case is not good. Please do not generate data like this.\n"
    #
    GoodCaser = GoodCase(good_case_path, good_prompt, sample_number=sample_number)
    BadCaser = BadCase(bad_case_path, bad_prompt, sample_number=sample_number)
    print(f"Good Case: {len(GoodCaser.sample_list)}")
    print(f"Bad Case: {len(BadCaser.sample_list)}")
    g_prompt = gen_prompt
    d_prompt = dis_prompt

    data_list = []
    with open(data_stream_path, "w") as f:
        for data in tqdm(source_code):
            ids = data["id"]
            # print("The code for example " + str(ids+1) + " is generating.")

            example_code = data["text"]

            good_few_shot = GoodCaser.generate_fewshot_text()
            bad_few_shot = BadCaser.generate_fewshot_text()
            example = {
                "good_few_shot": good_few_shot,
                "bad_few_shot": bad_few_shot,
                "input": example_code,
            }

            message = g_prompt.format_map(example)

            print("\n\ngen_message:\n" + message + "\n\n")

            text = make_request(
                message=message, model=engine, api_key=api_key, base_url=base_url
            )
            if not text:
                raise Exception("No text generated")
            # print(text)
            # analysis filter
            text, analysis_content = analysis_filter(text)

            if analysis_content is None:
                analysis_content = ""
            print("\n\ntext:\n"+text+"\n\n")
            print("\n\nanalysis_content:\n"+analysis_content+"\n\n")

            filter_messages = extract_message(text)

            print("\n\nfilter_messages:\n")
            print(filter_messages)
            print("\n\n")

            if filter_messages["quality"] == False:
                continue
            generated_text = filter_messages["generated_text"]

            print("\n\ngenerated_text:\n"+generated_text+"\n\n")

            example_case = f"Input: \n{example_code}\n\nOutput:\n{generated_text}"

            print("\n\nexample_case:\n" + example_case + "\n\n")

            # example_case = f"Output:\n{text}"
            ans = discriminator(
                prompt=d_prompt,
                GoodCaser=GoodCaser,
                BadCaser=BadCaser,
                engine=engine,
                api_key=api_key,
                base_url=base_url,
                generated_text=example_case,
            )

            if ans is None:
                ans = ""

            if "no" not in ans:
                result_data = filter_messages["result"]
                assert result_data != None and len(result_data) > 0
                data_list.append(
                    {
                        "id": ids,
                        "task_type": "code generation",
                        "source_code": example_code,
                        "generation_data": result_data,
                    }
                )
                f.write(
                    json.dumps(
                        {
                            "id": ids,
                            "task_type": "code generation",
                            "source_code": example_code,
                            "generation_data": result_data,
                        }
                    )
                )
                f.write(",\n")

                time.sleep(2)

    if not data_list:
        raise Exception("No data generated")

    else:
        print(f"data_list={len(data_list)}\nsome examples are:")
        for key, value in data_list[0].items():
            print(f"{key}:{value}")

    return data_list

        接下来就是使用鉴别器对生成器所生成的代码数据文本进行好坏分类,以帮助大模型进行分析判断,最终对分析的结果进行好坏分类,并保存在相应的目录文件中,分类完成后的样例同时作为样本数据为训练所用。

def discriminator(
    prompt: str,
    GoodCaser: GoodCase,
    BadCaser: BadCase,
    engine: str,
    api_key: str,
    base_url: str,
    generated_text: str,
    max_token: int = 500,
) -> str:
    """
    使用鉴别器对生成的文本进行好坏分类。

    参数:
    prompt (str): 鉴别器的提示文本。
    GoodCaser (GoodCase): GoodCase类的一个实例,用于好例子。
    BadCaser (BadCase): BadCase类的一个实例,用于坏例子。
    engine (str): 要使用的OpenAI引擎。
    api_key (str): OpenAI的API密钥。
    base_url (str): OpenAI API的基础URL。
    generated_text (str): 需要被分类的生成文本。
    max_token (int): 鉴别器的最大令牌长度。

    返回值:
    str: 鉴别器给出的总体答案。
    """

    prompt_format = """
{prompt}\n{bad_examples}\n\n{generated_text}\n\nAnalysis:\n
    """

    good_examples = GoodCaser.generate_fewshot_for_d()
    bad_examples = BadCaser.generate_fewshot_for_d()
    # generate instruction
    example = {
        "prompt": prompt,
        "good_examples": good_examples,
        "bad_examples": bad_examples,
        "generated_text": generated_text,
    }

    message = prompt_format.format_map(example)

    print("\n\ndis_message:\n" + message + "\n\n")

    # obtain answer
    ans = make_request(
        message=message, model=engine, api_key=api_key, base_url=base_url
    )
    feedback, part_answer, overall_answer = extract_answer(ans)
    print(f"part_answer={part_answer}\toverall_answer={overall_answer}")

    if feedback is None:
        print("no pattern information was extracted in the feedback")
        return None

    generated_text = f"{generated_text}\n\nAnalysis:\n{feedback}\n"

    if overall_answer == "yes":
        GoodCaser.add_case(generated_text)
    elif overall_answer == "no":
        BadCaser.add_case(generated_text)

    return overall_answer

        同时需要注意的是,github上下载的项目源码在某些参数上出现了错误,以至于会产生一些报错,按照文件位置修改即可,例如:

        正确的应为:

        等等诸如此类的问题进行修改后,同时也需要在通过与openai接口交互过程中的格式问题等进行修改,修正过后即可成功运行,结果如下(这里我增加了一些中间的输出过程,以便于更好地观察其代码的运行过程和逻辑):

python main_shy.py

        这里我把一些参数都直接放在了parse的defaulut中,例如openai的url、采用的model、相应的密钥等等,因此可能运行指令比较简洁,有需要的可以自行修改代码,完成自己的需求。

   

         通过观察输出可以发现LLM按照要求不断地对筛选后的优质代码数据按要求进行分类,分析等,同时经过判别按要求生成的格式数据是否符合规范,分类为好数据/坏数据,以便训练使用。

         可以看到运行之前,原项目中/fewshot_case文件夹中包含了/good_case和/bad_case两组文件夹,同时文件夹中均包含一个初始的good_case_0/bad_case_0文件。

        运行后可以看到,在/good_case和/bad_case两组文件夹中分别生成了判别器所判别过后的好数据/坏数据,同时好坏数据均可以为小模型的学习提供借鉴和帮助。

        值得注意的是,项目中有许多细节部分需要进行修改,主要是针对一些报错处理与交互格式的问题,这里就不过多赘述,遇到报错就相应地对报错原因进行分析,修正即可。


        这一部分就是针对Wavecoder项目中数据生成的代码的实践,同时Wavecoder项目中还包含了利用生成的优质数据对模型进行训练,和对该方法的基准化评估。而数据生成过程才是Wavecoder项目的核心,因此另外两部分也不过多介绍,感兴趣的朋友们可以自己尝试跑跑看。

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值