为了账号安全,请及时绑定邮箱和手机立即绑定

使用 MLX 在 macOS 上进行 LLM 的微调,以打造属于自己的 LLM

保护隐私的LLM(大型语言模型)

1. 背景介绍

在我的上一系列帖子中,我讨论了使用如LlamaIndex、LangChain、GPT4All和Ollama等工具构建RAG应用程序,以利用LLM来解决特定用例问题。在这篇文章中,我将探讨另一种方法,即LLM的微调技术。我使用名为[MLX](https://github.com/ml-explore/mlx)的工具在macOS上微调了Meta的LLaMA-3和Mistral大模型,这是一个专为苹果芯片上的机器学习研究设计的数组框架。这个微调是通过一种称为低秩适配器(LoRA)的技术完成的。随后,这些微调后的LLM通过[Ollama](https://ollama.com/)运行。所有与此文相关的源代码已发布在GitLab上,请克隆该仓库以继续阅读本文。

2. LLM 微调过程

微调是指让一个预训练的大规模语言模型(LLM)更好地适应特定的任务或领域。虽然像GPT这样的预训练语言模型具有广泛的通用语言知识,但它们在专业领域中往往缺乏专业领域的知识。通过在特定领域的数据上训练模型,微调可以克服这一问题,提高其在特定应用中的准确性和效率。这一过程涉及向模型展示特定任务的例子,让它更好地理解和适应领域内的细微差别。这一步骤将把一个通用语言模型变成一个专门的工具,从而释放LLM在特定领域的全部潜力。然而,微调LLM需要大量的计算资源,比如GPU等,以确保高效的训练。

有多种大规模语言模型(LLM)微调技术可供使用,包括低秩适配器(LoRA)、量化LoRA(QLoRA)、参数高效微调(PEFT)、DeepSpeed 和 ZeRO。有关 LLM 微调的更多信息,请参阅此链接:这里。本文将讨论在 Apple MLX 框架中使用 LoRA 技术进行 LLM 微调的方法。LoRA 技术最初由微软的研究团队在 2021 年提出(详见这篇论文:详见此论文),LoRA 提供了一种参数高效的微调方法。与传统方法相比,它们需要对整个基础模型进行微调,这可能会很耗时且成本高昂,而 LoRA 只需添加少量可训练参数,而无需更改原有模型参数。

LoRA的核心在于向模型添加适配层,从而提升其效率和适应性。LoRA不是通过引入全新的层来实现这一点,而是通过引入低秩矩阵来调整现有层的行为。这种方法仅引入少量额外参数,从而大大减少了计算开销和内存使用,相比全模型重新训练而言。通过专注于特定模型组件的调整,LoRA保留了嵌入在原始权重中的知识,从而降低了灾难性遗忘的可能性。这种针对性的调整不仅保持了模型的一般能力,还使其能够快速迭代和特定任务增强,使LoRA成为微调大型预训练模型的灵活且可扩展的方案。有关LoRA的更多信息和示例,请在此处阅读:这里

3\ RAG vs LLM 微调

RAG(检索增强生成)通过让LLM访问一个整理好的数据库,使其能动态获取相关信息来回答问题。相比之下,微调则是通过在特定标记数据集上训练模型来调整其参数,从而提升其在特定任务上的表现。微调改变了模型本身,而RAG则扩展了模型能接触到的信息范围。

当您需要补充模型提示时,使用RAG,特别是在初始训练时无法获得的数据。RAG非常适合确保模型能够访问最及时和相关的数据。另一方面,微调更适合训练模型更准确地理解和执行特定任务。要了解更多用例及如何在RAG和微调之间做出选择,请阅读这篇文章。

四、LLM 微调与 Apple MLX

人们长期以来一直认为,机器学习的训练和推理只能在Nvidia的GPU上进行。然而,这种看法发生了转变,随着机器学习框架MLX的发布,它使在苹果的硅芯片(如CPU/GPU)上进行机器学习的训练和推理成为可能。由苹果开发的MLX库类似于TensorFlowPyTorch,支持GPU加速的任务。这个库允许在新的苹果硅芯片(M系列)上对大型语言模型(LLM)进行调优。此外,MLX支持使用LoRA方法对LLM进行调优。我已经成功使用MLX和LoRA微调了多个大型语言模型,包括Llama-3、Mistral等。

4.1. 使用场景

在这篇文章中,我将讨论如何使用MLX对LoRA进行微调,以完成特定的文本转SQL任务,该任务涉及根据用户提示生成自定义的SQL查询。微调后的LLM将根据用户的输入生成SQL查询。我使用了 [gretelai/synthetic_text_to_sql](https://huggingface.co/datasets/gretelai/synthetic_text_to_sql) 数据集,这是一个高质量的合成文本到SQL样本的丰富数据集。其中包含被转换成SQL格式的文本。使用这一数据集,我训练了Mistral-7B LLM,使其能够根据用户输入生成SQL查询。

4.2 配置 MLX 和其他工具:

首先,我需要安装MLX及其一系列所需的工具。下面是已安装工具的列表,以及我如何设置和配置MLX环境的方法。

# 使用名为mlxo的仓库:
❯❯ git clone https://gitlab.com/rahasak-labs/mlxo.git  
❯❯ cd mlxo
    # 创建并激活虚拟环境
    ❯❯ python -m venv .venv  
    ❯❯ source .venv/bin/activate  

    # 安装mlx-lm  
    ❯❯ pip install -U mlx-lm  

    # 安装其他所需的Python包  
    ❯❯ pip install pandas  
    ❯❯ pip install pyarrow
4.3 配置Huggingface-CLI (Hugging Face命令行工具)

我在从Hugging Face获取大型语言模型(LLM)和数据集。为此目的,我需要在Hugging Face上注册一个账户并配置huggingface-cli命令行工具的设置。

    # 在hugging-face上设置账户,请访问这里  
    https://huggingface.co/welcome  

    # 创建访问令牌以通过命令行接口(CLI)读取/写入数据  
    # 当使用huggingface cli登录时需要此令牌  
    https://huggingface.co/settings/tokens  

    # 安装huggingface-cli  
    ❯❯ pip install huggingface_hub  
    ❯❯ pip install "huggingface_hub[cli]"  

    # 通过CLI登录到huggingface  
    # 将被要求输入之前创建的访问令牌  
    ❯❯ huggingface-cli login  
        _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|  
        _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|  
        _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|  
        _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|  
        _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|  
        您的机器上已保存令牌。运行 `huggingface-cli whoami` 获取更多信息或 `huggingface-cli logout` 以注销。  
        设置新令牌会覆盖现有令牌。  
        登录时,`huggingface_hub` 需要从 https://huggingface.co/settings/tokens 生成的令牌。  
    输入您的令牌(输入将不显示):  
    是否将令牌添加到git凭证中? (Y/n) Y  
    令牌有效(权限:读取)。  
    您的令牌已保存到osxkeychain凭证助手。  
    您的令牌已保存在路径 /Users/lambda.eranga/.cache/huggingface/token  
    登录成功  

    # 登录成功后,令牌将保存在~/.cache/huggingface中  
    ❯❯ ls ~/.cache/huggingface  
    datasets   
    hub        
    token
4.4. 准备数据集

MLX需要数据遵循特定格式。在MLX中讨论了三种主要的数据格式:chatcompletion,和text。您可以在这里了解更多关于这些数据格式的信息:这里。对于这个用例,我将使用completion格式,这种格式遵循特定的提示和完成结构,如下所述。在这种情况下,我需要创建一个包含提示完成的数据集。数据集的生成在LLM的微调过程中扮演着关键角色,因为它直接影响微调模型的准确性。可以采用各种方法来生成用于LLM微调的数据集。例如,这篇文章介绍了一种使用LLM和提示工程生成数据集的方法。

    {  
      "prompt": "问题:",  
      "completion": "回答:是巴黎。"  
    }

Hugging Face上的原始数据集结构如下,以.paraquest文件形式提供。

    {  
      "id": 39325,  
      "domain": "公共健康",  
      "domain_description": "社区健康统计、传染病追踪数据、医疗保健可达性指标以及公共卫生政策分析。",  
      "sql_complexity": "聚合操作",  
      "sql_complexity_description": "聚合函数(COUNT、SUM、AVG、MIN、MAX 等)和 HAVING 子句",  
      "sql_task_type": "数据分析和报告",  
      "sql_task_type_description": "生成报告、仪表板和分析见解",  
      "sql_prompt": "每个州的总医院床位数是多少?",  
      "sql_context": "CREATE TABLE Beds (State VARCHAR(50), Beds INT); INSERT INTO Beds (State, Beds) VALUES ('California', 100000), ('Texas', 85000), ('New York', 70000);",  
      "sql": "SELECT State, SUM(Beds) FROM Beds GROUP BY State;",  
      "sql_explanation": "此查询计算 Beds 表中每个州的总医院床位数。它通过在 Beds 列上使用 SUM 函数并将结果按 State 列分组来实现。"  
    }

我已经将数据集转换成了 completion 格式,使用了数据集内的 sql_promptsql_contextsql 字段。我将 sql_promptsql_context 合并成了一个 prompt 字段,并将 sql 字段作为 completion。此外,MLX 还需要三组数据集:训练集、测试集验证集。数据文件需要使用 [JSONL](https://jsonlines.org/) 格式。以下是从 Hugging Face 转换数据到此格式的脚本。

    import pandas as pd  

    def prepare_train():  
        df = pd.read_parquet('train.parquet')  

        df['prompt'] = df['sql_prompt'] + " with given SQL structure " + df['sql_context']  
        df.rename(columns={'sql': 'completion'}, inplace=True)  
        df = df[['prompt', 'completion']]  

        print(df.head(10))  

        # 将DataFrame转换为每条记录占一行的JSON格式  
        # 保存为.jsonl文件  
        df.to_json('train.jsonl', orient='records', lines=True)  

    def prepare_test_valid():  
        df = pd.read_parquet('test.parquet')  

        df['prompt'] = df['sql_prompt'] + " with given SQL structure " + df['sql_context']  
        df.rename(columns={'sql': 'completion'}, inplace=True)  
        df = df[['prompt', 'completion']]  

        # 找出三分之二的数据分割点  
        split_index = int(len(df) * 2 / 3)  

        # 将DataFrame分割成两部分  
        test_df = df[:split_index]  
        valid_df = df[split_index:]  

        print(test_df.head(10))  
        print(valid_df.head(10))  

        # 分别保存为JSONL文件  
        test_df.to_json('test.jsonl', orient='records', lines=True)  
        valid_df.to_json('valid.jsonl', orient='records', lines=True)  

    prepare_train()  
    prepare_test_valid()

我已经从Hugging Face下载了数据文件,并将它们放在数据目录里。然后,我运行了一个脚本来生成traintestvalid数据集的JSONL文件。下面是生成的数据文件的结构。

    # 激活虚拟环境  
    ❯❯ source .venv/bin/activate  

    # 数据目录  
    # `test.parquet` 和 `train.parquet` 从 Hugging Face 下载  
    # https://huggingface.co/datasets/gretelai/synthetic_text_to_sql/tree/main  
    ❯❯ ls -al data  
    prepare.py  
    test.parquet  
    train.parquet  

    # 生成 jsonl 文件  
    ❯❯ cd data  
    ❯❯ python prepare.py  

    # 生成的文件  
    ❯❯ ls -ls  
    test.jsonl  
    train.jsonl  
    valid.jsonl  

    # train.jsonl  
    {"prompt":"每个销售员销售的木材总量,按销售员排序?给定的 SQL 架构如下:CREATE TABLE salesperson (salesperson_id INT, name TEXT, region TEXT); INSERT INTO salesperson (salesperson_id, name, region) VALUES (1, 'John Doe', 'North'), (2, 'Jane Smith', 'South'); CREATE TABLE timber_sales (sales_id INT, salesperson_id INT, volume REAL, sale_date DATE); INSERT INTO timber_sales (sales_id, salesperson_id, volume, sale_date) VALUES (1, 1, 120, '2021-01-01'), (2, 1, 150, '2021-02-01'), (3, 2, 180, '2021-01-01');","completion":"SELECT salesperson_id, name, SUM(volume) as total_volume FROM timber_sales JOIN salesperson ON timber_sales.salesperson_id = salesperson.salesperson_id GROUP BY salesperson_id, name ORDER BY total_volume DESC;"}  
    {"prompt":"从 equipment_maintenance 表中列出所有唯一的设备类型及其对应的总维护频率。给定的 SQL 架构如下:CREATE TABLE equipment_maintenance (equipment_type VARCHAR(255), maintenance_frequency INT);","completion":"SELECT equipment_type, SUM(maintenance_frequency) AS total_maintenance_frequency FROM equipment_maintenance GROUP BY equipment_type;"}  
    {"prompt":"南极洲海域中有多少种海洋生物?给定的 SQL 架构如下:CREATE TABLE marine_species (name VARCHAR(50), common_name VARCHAR(50), location VARCHAR(50));","completion":"SELECT COUNT(*) FROM marine_species WHERE location = 'Southern Ocean';"}  
    {"prompt":"贸易历史表中每个交易商和股票的总交易价值和平均价格是多少?给定的 SQL 架构如下:CREATE TABLE trade_history (id INT, trader_id INT, stock VARCHAR(255), price DECIMAL(5,2), quantity INT, trade_time TIMESTAMP);","completion":"SELECT trader_id, stock, SUM(price * quantity) as total_trade_value, AVG(price) as avg_price FROM trade_history GROUP BY trader_id, stock;"}  

    # test.jsonl  
    {"prompt":"在 'creative_ai' 表中,欧洲和北美地区创意 AI 应用的平均解释性分数是多少?给定的 SQL 架构如下:CREATE TABLE creative_ai (application_id INT, name TEXT, region TEXT, explainability_score FLOAT); INSERT INTO creative_ai (application_id, name, region, explainability_score) VALUES (1, 'ApplicationX', 'Europe', 0.87), (2, 'ApplicationY', 'North America', 0.91), (3, 'ApplicationZ', 'Europe', 0.84), (4, 'ApplicationAA', 'North America', 0.93), (5, 'ApplicationAB', 'Europe', 0.89);","completion":"SELECT AVG(explainability_score) FROM creative_ai WHERE region IN ('Europe', 'North America');"}  
    {"prompt":"删除印尼所有在 2010 年之前完成的农村基础设施项目的记录。给定的 SQL 架构如下:CREATE TABLE rural_infrastructure (id INT, project_name TEXT, sector TEXT, country TEXT, completion_date DATE); INSERT INTO rural_infrastructure (id, project_name, sector, country, completion_date) VALUES (1, 'Water Supply Expansion', 'Infrastructure', 'Indonesia', '2008-05-15'), (2, 'Rural Electrification', 'Infrastructure', 'Indonesia', '2012-08-28'), (3, 'Transportation Improvement', 'Infrastructure', 'Indonesia', '2009-12-31');","completion":"DELETE FROM rural_infrastructure WHERE country = 'Indonesia' AND completion_date < '2010-01-01';"}  
    {"prompt":"SpaceX 和 Blue Origin 火箭发射记录了多少次事故?给定的 SQL 架构如下:CREATE TABLE Accidents (id INT, launch_provider VARCHAR(255), year INT, description TEXT); INSERT INTO Accidents (id, launch_provider, year, description) VALUES (1, 'SpaceX', 2015, 'Falcon 9 explosion'), (2, 'Blue Origin', 2011, 'Propulsion system failure'), (3, 'SpaceX', 2016, 'Falcon 9 explosion');","completion":"SELECT launch_provider, COUNT(*) FROM Accidents GROUP BY launch_provider;"}  
    {"prompt":"单笔交易中销售的最大海鲜量是多少?给定的 SQL 架构如下:CREATE TABLE sales (id INT, location VARCHAR(20), quantity INT, price DECIMAL(5,2)); INSERT INTO sales (id, location, quantity, price) VALUES (1, 'Northeast', 50, 12.99), (2, 'Midwest', 75, 19.99), (3, 'West', 120, 14.49);","completion":"SELECT MAX(quantity) FROM sales;"}  

    # valid.jsonl  
    {"prompt":"所有足球比赛中总共售出了多少张票?给定的 SQL 架构如下:CREATE TABLE tickets (ticket_id INT, game_id INT, region VARCHAR(50), quantity INT); INSERT INTO tickets (ticket_id, game_id, region, quantity) VALUES (1, 1, 'Midwest', 500); INSERT INTO tickets (ticket_id, game_id, region, quantity) VALUES (2, 2, 'Northeast', 700); CREATE TABLE games (game_id INT, sport VARCHAR(50)); INSERT INTO games (game_id, sport) VALUES (1, 'Football'); INSERT INTO games (game_id, sport) VALUES (2, 'Basketball');","completion":"SELECT SUM(quantity) FROM tickets INNER JOIN games ON tickets.game_id = games.game_id WHERE sport = 'Football';"}  
    {"prompt":"从票务销售中,伦敦和巴黎的足球队总收益是多少?给定的 SQL 架构如下:CREATE TABLE tickets (ticket_id INT, game_id INT, quantity INT, price DECIMAL(5,2)); INSERT INTO tickets VALUES (1, 1, 50, 25.99); INSERT INTO tickets VALUES (2, 2, 30, 19.99); CREATE TABLE games (game_id INT, team VARCHAR(20), location VARCHAR(20), price DECIMAL(5,2)); INSERT INTO games VALUES (1, 'Arsenal', 'London', 50.00); INSERT INTO games VALUES (2, 'PSG', 'Paris', 40.00);","completion":"SELECT SUM(tickets.quantity * games.price) FROM tickets INNER JOIN games ON tickets.game_id = games.game_id WHERE games.location IN ('London', 'Paris');"}  
    {"prompt":"在欧洲上一个月发生的安全事件数量是多少?给定的 SQL 架构如下:CREATE TABLE incidents (incident_id INT PRIMARY KEY, incident_date DATE, incident_location VARCHAR(50)); INSERT INTO incidents (incident_id, incident_date, incident_location) VALUES (1, '2022-01-01', 'HQ'), (2, '2022-02-15', 'Branch01'), (3, '2022-03-30', 'Asia'), (4, '2022-04-15', 'Europe'), (5, '2022-04-20', 'Europe');","completion":"SELECT COUNT(*) FROM incidents WHERE incident_location = 'Europe' AND incident_date >= DATE_SUB(CURRENT_DATE, INTERVAL 1 MONTH);"}  
    {"prompt":"在过去一年中,根据我们的事件跟踪数据库,识别出报告事件最多的前 5 个威胁情报来源。给定的 SQL 架构如下:CREATE TABLE IncidentTracking (id INT, source VARCHAR(50), incident_count INT, timestamp DATETIME); INSERT INTO IncidentTracking (id, source, incident_count, timestamp) VALUES (1, 'TechFirmA', 200, '2021-01-01 10:00:00'), (2, 'TechFirmB', 150, '2021-01-01 10:00:00');","completion":"SELECT source, SUM(incident_count) as total_incidents FROM IncidentTracking WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR) GROUP BY source ORDER BY total_incidents DESC LIMIT 5;"}
4.6 调教和训练大型语言模型

下一步是使用我之前准备的数据集,用MLX对Mistral-7B大型语言模型进行微调(fine-tuning)。最初,我从Hugging Face下载了mistralai/Mistral-7B-Instruct-v0.2大型语言模型,使用了huggingface-cli工具。然后,我使用提供的数据集和LoRA对模型进行了训练。LoRA,或低秩调整,通过引入低秩矩阵来调整模型的行为,而不必进行大量的重新训练,从而实现了高效的、有针对性的调整,保持了原始模型参数。

训练过程大约需要36分钟完成,在一台装有64GB RAM30个GPU的Mac M2上训练大型语言模型及其必需的适配器。

    # 下载大语言模型  
    ❯❯ huggingface-cli download mistralai/Mistral-7B-Instruct-v0.2  
    /Users/lambda.eranga/.cache/huggingface/hub/models--mistralai--Mistral-7B-Instruct-v0.2/snapshots/1296dc8fd9b21e6424c9c305c06db9ae60c03ace  

    # 模型下载到 ~/.cache/huggingface/hub/  
    ❯❯ ls ~/.cache/huggingface/hub/models--mistralai--Mistral-7B-Instruct-v0.2  
    blobs     refs      snapshots  

    # 列出从Hugging Face下载的所有模型  
    ❯❯ huggingface-cli scan-cache  
    REPO ID                                REPO TYPE SIZE ON DISK NB FILES LAST_ACCESSED LAST_MODIFIED REFS LOCAL PATH  
    -------------------------------------- --------- ------------ -------- ------------- ------------- ---- -------------------------------------------------------------------------------------------  
    NousResearch/Meta-Llama-3-8B           model            16.1G       14 2天前        2天前         main /Users/lambda.eranga/.cache/huggingface/hub/models--NousResearch--Meta-Llama-3-8B  
    gpt2                                   model             2.9M        5 3个月前       3个月前       main /Users/lambda.eranga/.cache/huggingface/hub/models--gpt2  
    mistralai/Mistral-7B-Instruct-v0.2     model            29.5G       17 5小时        5小时         main /Users/lambda.eranga/.cache/huggingface/hub/models--mistralai--Mistral-7B-Instruct-v0.2  
    sentence-transformers/all-MiniLM-L6-v2 model            91.6M       11 3个月前       3个月前       main /Users/lambda.eranga/.cache/huggingface/hub/models--sentence-transformers--all-MiniLM-L6-v2  

    # 微调大语言模型   
    # --model - 原始模型,从Hugging Face下载  
    # --data data - 数据目录路径,包含train.jsonl  
    # --batch-size 4 - 批量大小  
    # --lora-layers 16 - Lora层的数量  
    # --iters 1000 - 训练迭代次数  
    ❯❯ python -m mlx_lm.lora \  
      --model mistralai/Mistral-7B-Instruct-v0.2 \  
      --data data \  
      --train \  
      --batch-size 4\  
      --lora-layers 16\  
      --iters 1000  

    # 以下为训练输出  
    # 当训练开始时,初始验证损失为1.939,训练损失为1.908  
    # 训练完成后,验证损失为0.548,训练损失为0.534  
    未找到PyTorch、TensorFlow 2.0及以上版本或Flax。模型将不可用,仅支持使用词嵌入器、配置和文件/数据实用程序。  
    加载预训练模型  
    正在获取11个文件:100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 11/11 [00:00<00:00, 96.71it/s]  
    加载数据集文件  
    开始训练,总迭代次数:1000  
    迭代1:验证步骤耗时47.185秒  
    迭代10:训练损失1.908,学习率1.000e-05,每秒迭代数0.276,每秒令牌数212.091,已训练令牌数7688,峰值内存21.234 GB  
    迭代20:训练损失1.162,学习率1.000e-05,每秒迭代数0.330,每秒令牌数275.972,已训练令牌数16040,峰值内存21.381 GB  
    迭代30:训练损失0.925,学习率1.000e-05,每秒迭代数0.360,每秒令牌数278.166,已训练令牌数23769,峰值内存21.381 GB  
    迭代40:训练损失0.756,学习率1.000e-05,每秒迭代数0.289,每秒令牌数258.912,已训练令牌数32717,峰值内存24.291 GB  
    ---  
    迭代960:训练损失0.510,学习率1.000e-05,每秒迭代数0.360,每秒令牌数283.649,已训练令牌数717727,峰值内存24.332 GB  
    迭代970:训练过程中的损失值为0.598,学习率1.000e-05,每秒迭代数0.398,每秒令牌数276.395,已训练令牌数724663,峰值内存24.332 GB  
    迭代980:训练过程中的损失值为0.612,学习率1.000e-05,每秒迭代数0.419,每秒令牌数280.406,已训练令牌数731359,峰值内存24.332 GB  
    迭代990:训练过程中的损失值为0.605,学习率1.000e-05,每秒迭代数0.371,每秒令牌数292.855,已训练令牌数739260,峰值内存24.332 GB  
    迭代1000:验证损失0.548,验证步骤耗时36.479秒  
    迭代1000:训练损失0.534,学习率1.000e-05,每秒迭代数2.469,每秒令牌数1886.081,已训练令牌数746899,峰值内存24.332 GB  
    迭代1000:保存适配器权重文件到adapters/adapters.safetensors和adapters/0001000_adapters.safetensors。  
    保存最终适配器权重文件到adapters/adapters.safetensors。  

    # 训练过程中的GPU使用情况  
    ❯❯ sudo powermetrics --samplers gpu_power -i500 -n1  
    机器型号: Mac14,6  
    操作系统版本: 23F79  
    引导参数:  
    引导时间: 2024年6月19日 20:50:45  

    *** 采样系统活动(2024年6月20日 16:25:57 -0400)(503.31ms)***  

    **** GPU使用情况 ****  
    GPU硬件运行频率:1398 MHz  
    GPU硬件活动驻留率:100.00%(444 MHz: 0% 612 MHz: 0% 808 MHz: 0% 968 MHz: 0% 1110 MHz: 0% 1236 MHz: 0% 1338 MHz: 0% 1398 MHz: 100%)  
    GPU软件请求状态:(P1 : 0% P2 : 0% P3 : 0% P4 : 0% P5 : 0% P6 : 0% P7 : 0% P8 : 100%)  
    GPU软件状态:(SW_P1 : 0% SW_P2 : 0% SW_P3 : 0% SW_P4 : 0% SW_P5 : 0% SW_P6 : 0% SW_P7 : 0% SW_P8 : 0%)  
    GPU空闲时间占比:0.00%  
    GPU功率:45630 mW  

    # 训练完成后,LoRA适配器文件生成并保存至adapters文件夹  
    ❯❯ ls adapters  
    0000100_adapters.safetensors   
    0000300_adapters.safetensors   
    0000500_adapters.safetensors   
    0000700_adapters.safetensors   
    0000900_adapters.safetensors   
    adapter_config.json  
    0000200_adapters.safetensors   
    0000400_adapters.safetensors   
    0000600_adapters.safetensors   
    0000800_adapters.safetensors   
    0001000_adapters.safetensors   
    adapters.safetensors
4.7. 评估微调后的LLM

LLM现在已经训练好了,LoRA适配器也已经准备好了。我们可以利用这些适配器与原来的LLM来测试微调后的LLM的表现。最开始,我使用--train参数对LLM进行了测试,使用的是MLX工具。接着,我向原版的LLM和微调后的LLM分别提出了同样的问题。这样的对比让我们能够看到,微调后的LLM在文本到SQL的应用场景中,是如何根据提供的数据集进行优化的。通过调整提示、数据集和其他参数等,可以进一步优化微调过程。我将在接下来的文章中详细介绍如何准备和生成用于LLM微调的数据集。敬请期待更多详细的见解。

    # 使用测试数据测试llm
    # --model - 从hugging face下载的原始模型
    # --adapter-path adapters - lora适配器的路径
    # --data data - 包含有test.jsonl的数据目录路径
    ❯❯ python -m mlx_lm.lora \  
      --model mistralai/Mistral-7B-Instruct-v0.2 \  
      --adapter-path adapters \  
      --data data \  
      --test  

    # 测试输出
    未找到 PyTorch、TensorFlow >= 2.0 或 Flax。模型将不可用,仅可使用分词器、配置及文件/数据工具。
    正在加载11个文件: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 11/11 [00:00<00:00, 127804.28it/s]  
    ==========  
    Prompt: <s>[INST] 列出来自非洲('Africa')区域的所有交易和客户。[/INST]  
    SELECT * FROM Transactions WHERE region = 'Africa' UNION SELECT * FROM Customers WHERE region = 'Africa';  
    ==========  
    Prompt: 63.182 tokens-per-sec  
    Generation: 21.562 tokens-per-sec  

    # 首先使用mlx从原始llm提问
    # --model - 从hugging face下载的原始模型
    # --max-tokens 500 - 生成多少token
    # --prompt - 提供给llm的提示
    ❯❯ python -m mlx_lm.generate \  
         --model mistralai/Mistral-7B-Instruct-v0.2 \  
         --max-tokens 500 \  
         --prompt "列出来自'Africa'区域的所有交易和客户。"  

    # 它提供如下的通用答案
    Prompt: <s>[INST] 列出来自非洲('Africa')区域的所有交易和客户。[/INST]  
    我是一个AI语言模型,没有直接访问或列出特定数据库或系统中具体数据的能力。我只能根据假设您有一个名为'transactions'和'customers'的数据库表,并且这些表都包含'region'列的情况下,为您提供一个编写检索'Africa'区域交易和客户的SQL查询的例子。  
    ```sql  
    -- 用于获取所有来自非洲的交易的查询
    SELECT *  
    FROM transactions  
    WHERE region = 'Africa';  
    -- 用于获取所有来自非洲的客户的查询
    SELECT *  
    FROM customers  
    WHERE region = 'Africa';  
这些查询将返回'region'设置为'Africa'的所有交易和客户。请根据您的具体数据库模式调整表名和列名。  
==========  
Prompt: 67.650 tokens-per-sec  
Generation: 23.078 tokens-per-sec  

# 使用lora适配器从微调过的llm提问相同问题
# --model - 从hugging face下载的原始模型
# --max-tokens 500 - 生成多少token
# --adapter-path adapters - lora适配器的路径
# --prompt - 提供给llm的提示
❯❯ python -m mlx_lm.generate \  
     --model mistralai/Mistral-7B-Instruct-v0.2 \  
     --max-tokens 500 \  
     --adapter-path adapters \  
     --prompt "列出来自'Africa'区域的所有交易和客户。"  

# 它提供特定的SQL查询
Prompt: <s>[INST] 列出来自非洲('Africa')区域的所有交易和客户。[/INST]  
SELECT * FROM Transactions WHERE region = 'Africa' UNION SELECT * FROM Customers WHERE region = 'Africa';  
==========  
Prompt: 64.955 tokens-per-sec  
Generation: 21.667 tokens-per-sec

## 4.8. 构建一个带有集成适配器的新模型

我们在完成微调后,可以将新模型学到的调整合并到现有模型的权重中,这个过程叫作融合。更具体地说,这涉及到更新预训练或基础模型的权重和参数,以便纳入微调模型的改进。基本上,我可以将LoRA适配器文件融合回基础模型中。

完成微调过程后,我可以将新模型学习到的调整与现有模型的权重合并,这个过程被称为 `融合`。技术上讲,这涉及到更新预训练模型的权重和参数,以融入微调模型的改进。基本上,我可以将LoRA适配器合并回基础模型。
# --model - 原始模型,从Hugging Face下载  
# --adapter-path adapters - lora适配器的位置  
# --save-path models/effectz-sql - 新模型路径  
# --de-quantize - 如果你想将模型转换为GGUF格式,请使用此标志  
❯❯ python -m mlx_lm.fuse \  
    --model mistralai/Mistral-7B-Instruct-v0.2 \  
    --adapter-path adapters \  
    --save-path models/effectz-sql \  
    --de-quantize  

# 输出  
未找到PyTorch、TensorFlow >= 2.0 或 Flax。模型无法加载,仅能使用tokenizers、配置和文件/数据工具。  
加载预训练模型  
正在获取11个文件: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 11/11 [00:00<00:00, 129599.28it/s]  
正在解除模型的量化  

# 新模型生成在models目录下  
❯❯ tree models  
models  
└── effectz-sql  
    ├── config.json  
    ├── model-00001-of-00003.safetensors  
    ├── model-00002-of-00003.safetensors  
    ├── model-00003-of-00003.safetensors  
    ├── model.safetensors.index.json  
    ├── special_tokens_map.json  
    ├── tokenizer.json  
    └── tokenizer_config.json  

# 现在可以直接向新模型提问  
# --model models/effectz-sql - 新模型路径  
# --max-tokens 500 - 生成的token数  
# --prompt - 提示语  
❯❯ python -m mlx_lm.generate \  
  --model models/effectz-sql \  
  --max-tokens 500 \  
  --prompt "列出所有来自'Africa'区域的交易和客户。"  

# 输出  
==========  
提示: <s>[INST] 列出所有来自'Africa'区域的交易和客户。[/INST]  
SELECT * FROM transactions WHERE region = 'Africa'; SELECT * FROM customers WHERE region = 'Africa';  
==========  
每秒65.532个token  
每秒生成23.193个token

## 4.9. 创建GGUF模型文件

我想使用 `[Ollama](https://github.com/ollama/ollama)` 运行这个新创建的模型,这是一个轻量且灵活的框架,旨在让个人电脑能够运行 LLM。为了在 Ollama 上运行这个新创建的模型,我需要将其转换为 `[GGUF (Georgi Gerganov Unified Format)](https://github.com/ggerganov/ggml/blob/master/docs/gguf.md)` 文件。GGUF 是 Ollama 用来存储模型的标准格式。为了将模型转换为 GGUF 格式,我使用了另一个名为 `[llama.cpp](https://github.com/ggerganov/llama.cpp)` 的开源工具,这是一个用 `C++` 编写的支持各种 LLM 推理的开源库。以下是将模型转换为 GGUF 格式并构建 Ollama 模型的步骤。
# 将 llama.cpp 克隆到 mlxo 仓库所在的相同位置
❯❯ git clone https://github.com/ggerganov/llama.cpp.git  

# 包含 llama.cpp 和 mlxo 的目录结构
❯❯ ls  
llama.cpp   
mlxo  

# 配置 llama.cpp 中所需的包,并设置虚拟环境
❯❯ cd llama.cpp  
❯❯ python -m venv .venv  
❯❯ source .venv/bin/activate  
❯❯ pip install -r requirements.txt  

# llama.cpp 包含一个脚本 `convert-hf-to-gguf.py`,用于将 hugging face 模型转换为 gguf 格式
❯❯ ls convert-hf-to-gguf.py  
convert-hf-to-gguf.py  

# 将 mlxo/models/effectz-sql 中新生成的模型转换为 gguf 格式
# --outfile ../mlxo/models/effectz-sql.gguf 指定输出 gguf 模型的路径
# --outtype q8_0 表示使用8位量化,这有助于提高推理速度
❯❯ python convert-hf-to-gguf.py ../mlxo/models/effectz-sql \  
     --outfile ../mlxo/models/effectz-sql.gguf \  
     --outtype q8_0  

# 输出
INFO:hf-to-gguf:加载模型:effectz-sql  
INFO:gguf.gguf_writer:此 GGUF 文件仅适用于小端序系统  
INFO:hf-to-gguf:设置模型参数
INFO:hf-to-gguf:gguf: 上下文长度 = 32768
INFO:hf-to-gguf:gguf: 嵌入长度 = 4096
INFO:hf-to-gguf:gguf: 前馈长度 = 14336
INFO:hf-to-gguf:gguf: 头数 = 32
INFO:hf-to-gguf:gguf: key-value 头数 = 8
INFO:hf-to-gguf:gguf: rope theta = 1000000.0
INFO:hf-to-gguf:gguf: rms 规范 epsilon = 1e-05
INFO:hf-to-gguf:gguf: 文件类型 = 7
INFO:hf-to-gguf:设置模型词元器
INFO:gguf.vocab:设置特殊令牌类型 bos 为 1
INFO:gguf.vocab:设置特殊令牌类型 eos 为 2
INFO:gguf.vocab:设置特殊令牌类型 unk 为 0
INFO:gguf.vocab:设置 add_bos_token 为 True
INFO:gguf.vocab:设置 add_eos_token 为 False
INFO:gguf.vocab:设置 chat_template 为 {{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('会话角色必须交替出现 user 和 assistant') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('仅支持 user 和 assistant 两种角色') }}{% endif %}{% endfor %}
INFO:hf-to-gguf:将模型导出为 '../mlxo/models/effectz-sql.gguf'
INFO:hf-to-gguf:gguf: 从 'model.safetensors.index.json' 加载模型权重映射文件
INFO:hf-to-gguf:gguf: 加载模型的一部分 'model-00001-of-00003.safetensors'
INFO:hf-to-gguf:token_embd.weight,           torch.bfloat16 --> Q8_0, 形状 = {4096, 32000}  
---  
INFO:hf-to-gguf:blk.31.attn_v.weight,        torch.bfloat16 --> Q8_0, 形状 = {4096, 1024}  
INFO:hf-to-gguf:output_norm.weight,          torch.bfloat16 --> F32, 形状 = {4096}  
写入进度: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7.70G/7.70G [01:17<00:00, 99.6Mbyte/s]  
INFO:hf-to-gguf:模型成功导出为 '../mlxo/models/effectz-sql.gguf'  

# 在 mlxo/models 文件夹中生成新的 gguf 模型
❯❯ cd ../mlxo  
❯❯ ls models/effectz-sql.gguf  
models/effectz-sql.gguf

## 4.10. 构建并运行Ollama模型

现在我可以创建一个 `[Ollama Modelfile](https://github.com/ollama/ollama/blob/main/docs/modelfile.md)` 并利用名为 `effectz-sql.gguf` 的 GGUF 文件构建 Ollama 模型。Ollama Modelfile 是一个用于定义和管理 Ollama 平台模型的配置文件。下面介绍如何创建 Modelfile 并生成新的 Ollama 模型。
# 在models目录下创建一个名为`Modelfile`的文件,内容如下
❯❯ cat models/Modelfile  
FROM ./effectz-sql.gguf  

# 使用ollama创建模型
❯❯ ollama create effectz-sql -f models/Modelfile  
正在传输模型的数据  
使用已存在的层 sha256:24ba3b41f3b846bd56142b713b12df8da3e7ab1c5ee9ae3f5afc76d87c69d796  
使用自动检测到的模板 mistral-instruct  
创建新的层级 sha256:fd230ae885e75bf5240291df2bfb0090af58f4bdf3689deafcef415582915a33  
写入清单文件  
成功  

# 列出ollama模型
# effectz-sql:latest 是新创建的模型
❯❯ ollama ls  
名称               ID           大小   修改时间
effectz-sql:latest 736275f4faa4 7.7 GB 17秒之前
mistral:latest     2ae6f6dd7a3d 4.1 GB 3天之前
llama3:latest      a6990ed6be41 4.7 GB 7周之前
llama3:8B          a6990ed6be41 4.7 GB 7周之前
llama2:latest      78e26419b446 3.8 GB 2个月之前
llama2:13b         d475bf4c50bc 7.4 GB 2个月之前  

# 使用ollama运行模型并提问
# 它会将提示转换为SQL
❯❯ ollama run effectz-sql  
>>> 列出来自非洲区域的所有交易记录和客户信息。  
SELECT * FROM transactions WHERE customer_region = 'Africa';


# 参考

1. <https://www.datacamp.com/tutorial/fine-tuning-large-language-models>《如何微调大规模语言模型》
2. <https://www.lakera.ai/blog/llm-fine-tuning-guide>《大规模语言模型微调指南》
3. [https://medium.com/rahasak/fine-tune-llms-on-your-pc-with-qlora-apple-mlx-c2aedf1f607d](https://medium.com/rahasak/fine-tune-llms-on-your-pc-with-qlora-apple-mlx-c2aedf1f607d) 使用QLora在你的电脑上微调LLMs
4. [https://medium.com/@elijahwongww/how-to-finetune-llama-3-model-on-macbook-4cb184e6d52e](https://medium.com/@elijahwongww/how-to-finetune-llama-3-model-on-macbook-4cb184e6d52e) 如何在MacBook上微调Llama 3模型
5. <https://heidloff.net/article/fine-tuning-llm-locally-apple-silicon-m3/> 本地使用Apple Silicon M3微调LLM
6. <https://heidloff.net/article/apple-mlx-fine-tuning/> 使用Apple MLX微调LLM
7. [https://medium.com/@anchen.li/fine-tune-llama3-with-function-calling-via-mlx-lm-5ebbee41558f](https://medium.com/@anchen.li/fine-tune-llama3-with-function-calling-via-mlx-lm-5ebbee41558f) 使用MLX LM通过函数调用微调Llama3
8. <https://iwasnothing.medium.com/llm-fine-tuning-with-macbook-pro-982fbea50b3d> 使用MacBook Pro微调LLM
9. [https://medium.com/@mustangs007/mlx-building-fine-tuning-llm-model-on-apple-m3-using-custom-dataset-9209813fd38e](https://medium.com/@mustangs007/mlx-building-fine-tuning-llm-model-on-apple-m3-using-custom-dataset-9209813fd38e) 使用Apple M3和自定义数据集构建和微调LLM模型
10. <https://mer.vin/2024/02/mlx-mistral-lora-fine-tuning/> 使用MLX Mistral Lora进行微调
11. <https://apeatling.com/articles/simple-guide-to-local-llm-fine-tuning-on-a-mac-with-mlx/> 使用MLX在Mac上进行本地LLM微调的简单指南
12. <https://dassum.medium.com/fine-tune-large-language-model-llm-on-a-custom-dataset-with-qlora-fb60abdeba07> 使用QLora在自定义数据集上微调大型语言模型
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消