技术

下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论及实践 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 LLM部分技术源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

大模型推理服务框架 模型服务化(未完成) 大模型RHLF 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 LLM工具栈 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

LLM微调理论及实践

2023年10月29日

为什么需要微调

通俗解读大模型微调(Fine Tuning)Prompt Engineering的方式是一种相对来说容易上手的使用大模型的方式,但是它的缺点也非常明显。

  1. 因为通常大模型的实现原理,都会对输入序列的长度有限制,Prompt Engineering 的方式会把Prompt搞得很长。越长的Prompt,大模型的推理成本越高,因为推理成本是跟Prompt长度的平方正向相关的。
  2. Prompt太长会因超过限制而被截断,进而导致大模型的输出质量打折口,这也是一个非常严重的问题。可能会胡乱地给你一些东西,它更适合一些泛化的任务。而fine-tuning则不一样,只要你的微调训练数据是高质量的,它可以很稳定地在你自己的数据集或任务上进行稳定的输出。
  3. Prompt Engineering的效果达不到要求时,企业又有比较好的自有数据,能够通过自有数据,更好的提升大模型在特定领域的能力。这时候微调就非常适用,微调可以使你为LLM提供比prompt多得多的数据,你的模型可以从该数据中进行学习,而不仅仅是访问该数据
  4. 要在个性化的服务中使用大模型的能力,这时候针对每个用户的数据,训练一个轻量级的微调模型,就是一个不错的方案。
  5. 数据安全的问题。如果数据是不能传递给第三方大模型服务的,那么搭建自己的大模型就非常必要。通常这些开源的大模型都是需要用自有数据进行微调,才能够满足业务的需求,这时候也需要对大模型进行微调。

外挂知识库也不是全能的

  1. 向量化的匹配能力是有上限的,搜索引擎实现语义搜索已经是好几年的事情了,为什么一直无法上线,自然有他的匹配精确度瓶颈问题,只是很多以前没有接触过AI 的朋友对之不熟悉罢了。
  2. 在引入外部知识这个事情上,如果是特别专业领域,纯粹依赖向量、NLP、策略/规则在某些场景仍然不奏效。对于某个具体业务而言,不要管是不是90%会被解决,只有需要被解决或不需要被解决。
  3. 微调的优势在于能够使LLM的行为适应特定的细微差别、语气或术语。如果我们希望模型听起来更像医学专业人士、或者使用特定行业的术语,那么对特定领域的数据进行微调可以让我们实现这些定制。
  4. 有多少标记的训练数据可用?微调LLM以适应特定任务或领域在很大程度上取决于可用标记数据的质量和数量。丰富的数据集可以帮助模型深入理解特定领域的细微差别、复杂性和独特模式,从而使其能够生成更准确且与上下文相关的响应。然而,如果我们使用有限的数据集,微调带来的改进可能微乎其微。从本质上讲,如果我们拥有大量的标记数据来捕获该领域的复杂性,那么微调可以提供更加定制和完善的模型行为。但在此类数据有限的情况下,RAG 系统提供了一个强大的替代方案,确保应用程序通过其检索功能保持数据知情和上下文感知。
  5. 抑制幻觉有多重要?
  6. 数据的静态/动态程度如何?在特定数据集上微调 LLM 意味着模型的知识成为训练时该数据的静态快照。如果数据频繁更新、更改或扩展,模型很快就会过时。相比之下,RAG 系统在动态数据环境中具有固有的优势。他们的检索机制不断查询外部来源,确保他们提取用于生成响应的信息是最新的。

整体思路

在大模型相关原理的指导下,AI 模型的构建也逐渐演化出了不同的阶段和流程:预训练、继续预训练、对齐(SFT、RLHF)。预训练模型(Base 模型)始终是智能的基础,而Alignment的主要目标是实现有效的人机接口。此外,由于RLHF的高成本和相对复杂性,SFT作为其廉价替代品已经获得了广泛的应用。

垂直领域大模型落地思考

垂直大模型基本套路

  1. Continue PreTraining: 一般垂直大模型是基于通用大模型进行二次的开发。为了给模型注入领域知识,就需要用领域内的语料进行继续的预训练。
  2. SFT: 通过SFT可以激发大模型理解领域内各种问题并进行回答的能力(在有召回知识的基础上)
  3. RLHF: 通过RLHF可以让大模型的回答对齐人们的偏好,比如行文的风格。 需要注意的是一般垂直领域大模型不会直接让模型生成答案,而是跟先检索相关的知识,然后基于召回的知识进行回答,这种方式能减少模型的幻觉,保证答案的时效性,还能快速干预模型对特定问题的答案。

SFT和RLHF阶段(微调也会有RLHF)主要要培养模型的三个能力:

  1. 领域内问题的判别能力,对领域外的问题需要能拒识
  2. 基于召回的知识回答问题的能力
  3. 领域内风格对齐的能力,例如什么问题要简短回答什么问题要翔实回答,以及措辞风格要与领域内的专业人士对齐。

继续预训练

  1. 混合数据,如果想要领域的模型还具备一定的通用能力,即通用的能力不会退化(或者灾难性遗忘)这就需要在语言模型训练的时候混杂通用的数据。
  2. 要不要从零训。回顾人对知识的理解:小学中学都在学习通用领域的知识,然后大学阶段继续进一步学习特定领域的知识。所以在通用模型的基础上继续二次预训练注入领域知识是合理的。但是如果想通过二次预训练进行语言层面的迁移就会比较难,没有从零开始训练好。回顾人对语言的学习,如果刚“出生”时候就在学习一门语言,进行听说读写的训练,这就是母语了。会比长大以后再去学习一门外语要容易的多,效果也要好很多。所以基于llama做的中文适配 不如 纯中文训练的baichuan 在中文任务上效果好。

领域微调

领域微调的核心是构建高质量大规模的领域微调数据。让人去收集一个领域内的语料是容易的,但是让人去编写领域内的微调指令和回答是很难的。下面介绍的方法都是来尝试解决这个问题。这些方法的核心都是基于一些已有的数据+GPT4,然后生成领域内的微调数据。

数据生成方法 已有数据 生成数据
Self-Instruct 一些单轮/多轮种子数据 单轮/多轮指令微调数据
Self-QA 文档数据 单轮指令微调数据
Self-KG 知识图谱 单轮指令微调数据

Self-Instruct是一种微调数据扩充的方法。如果已经一些种子微调数据(大约100条),可以通过Self-Instruct+GPT4进行扩充,生成更多相对符合要求的微调数据。一条微调数据包括三个部分:指令,输入 和 输出。下面具体介绍如何生成这三个部分。

  1. 首先从种子指令(人工编写的指令/业务侧积累的问题)中随机选择一些指令,然后让GPT4参考这些指令,生成一系列类似的指令。
  2. 有了指令后,再让GPT4判断这个指令是一个“分类”问题还是一个“生成”问题。后面会采用不同的答案生成策略。
    1. 如果一个问题是“分类”问题,则采用“output-first”的生成方式,即首先生成输出(具体哪个类别),然后再根据指令和输出,生成输入。例如指令是:”判断下面句子的情感是消极还是积极”,首先生成输出的类别:“积极”,然后再根据指令和类别生成输入的句子:“我今天很开心”。
    2. 如果一个问题是“生成”问题,则采用“input-first”的生成方式,即首先生成输入,然后再根据指令和输入,生成输出。例如指令是:“将下面的句子翻译成英文”,首先生成输入的句子:“我今天很开心”,然后再根据指令和输入生成输出的答案:“I am happy today”。如果一个指令不需要输入的句子,则输入为空。例如指令:“有哪些减肥的运动?”
  3. 经过上面的步骤就能初步获得一批微调数据,还需要进行进一步的过滤。例如过滤与已有数据相似度很高的结果,过滤明显低质的结果(指令过长或者过短)。过滤后的微调数据就可以继续加入“种子指令”中,以此循环,源源不断地进行生成。

如果连基础的种子指令数据都没有,那就不适于Self-Instruct的方法了。这时候可以尝试Self—QA的方法,直接从文档中生成指令数据。整体的流程如下:

  1. 基本的思想是:首先根据无结构的文档通过GPT4生成可能的指令,然后输入指令和对应的文档再让GPT4生成问题的答案。这里的文档可以直接就是文档语料,也可以从结构的表格数据或者图谱数据中生成无结构的文档数据。
  2. 基于设计的Prompt就可以让GPT4分别进行指令和答案的生成,由此构成指令微调数据。这些数据还需要进一步通过启发式和规则的方法进行过滤,来提高数据的质量。

如果一个领域已经有了高质量的知识图谱,也可以直接基于知识图谱生成指令数据。这种基于知识的指令数据生成方法是HuaTuo提出的,称为Self—KG。

如何对大模型进行微调

  1. 对全量的参数,进行全量的训练,这条路径叫全量微调FFT(Full Fine Tuning)。但FFT也会带来一些问题,影响比较大的问题,主要有以下两个:一个是训练的成本会比较高,因为微调的参数量跟预训练的是一样的多的;一个是叫灾难性遗忘(Catastrophic Forgetting),用特定训练数据去微调可能会把这个领域的表现变好,但也可能会把原来表现好的别的领域的能力变差。
  2. 只对部分的参数进行训练,这条路径叫PEFT(Parameter-Efficient Fine Tuning)。有以下几条技术路线:
    1. 一个是监督式微调SFT(Supervised Fine Tuning) ,这个方案主要是用人工标注的数据,用传统机器学习中监督学习的方法,对大模型进行微调;
    2. 一个是基于人类反馈的强化学习微调RLHF(Reinforcement Learning with Human Feedback) ,这个方案的主要特点是把人类的反馈,通过强化学习的方式,引入到对大模型的微调中去,让大模型生成的结果,更加符合人类的一些期望;
    3. 还有一个是基于AI反馈的强化学习微调RLAIF(Reinforcement Learning with AI Feedback) ,这个原理大致跟RLHF类似,但是反馈的来源是AI。这里是想解决反馈系统的效率问题,因为收集人类反馈,相对来说成本会比较高、效率比较低。

一些比较流行的PEFT方案

  1. Prompt Tuning,Prompt Tuning的出发点,是基座模型(Foundation Model)的参数不变,为每个特定任务,训练一个少量参数的小模型,在具体执行特定任务的时候按需调用。Prompt Tuning的基本原理是在输入序列X之前,增加一些特定长度的特殊Token,以增大生成期望序列的概率。具体来说,就是将$X = [x1, x2, …, xm]变成,X = [x1, x2, ..., xk; x1, x2, …, xm], Y = WX`$。如果将大模型比做一个函数:Y=f(X),那么Prompt Tuning就是在保证函数本身不变的前提下,在X前面加上了一些特定的内容,而这些内容可以影响X生成期望中Y的概率。
  2. Prefix Tuning,Prefix Tuning的出发点,跟Prompt Tuning的是类似的,只不过它们的具体实现上有一些差异。Prompt Tuning是在Embedding环节,往输入序列X前面加特定的Token。而Prefix Tuning是在Transformer的Encoder和Decoder的网络中都加了一些特定的前缀。具体来说,就是将Y=WX中的W,变成$W = [Wp; W],Y=WX$。Prefix Tuning也保证了基座模型本身是没有变的,只是在推理的过程中,按需要在W前面拼接一些参数。
  3. LoRA,LoRA背后有一个假设:我们现在看到的这些大语言模型,它们都是被过度参数化的。而过度参数化的大模型背后,都有一个低维的本质模型。通俗讲人话:大模型参数很多,但并不是所有的参数都是发挥同样作用的;大模型中有其中一部分参数,是非常重要的,是影响大模型生成结果的关键参数,这部分关键参数就是上面提到的低维的本质模型。LoRA的基本思路,在原始预训练模型旁边增加一个旁路,先用一个 Linear 层 A,将数据从 d 维降到 r 维,在用第二个 Linear 层 B,将数据从 r 维变回 d 维。LoRA 训练的时候固定预训练模型的参数,只训练降维矩阵 A 和升维矩阵 B。
  4. QLoRA,QLoRA就是量化版的LoRA(Quantize+LoRA),量化,是一种在保证模型效果基本不降低的前提下,通过降低参数的精度,来减少模型对于计算资源的需求的方法。量化的核心目标是降成本,降训练成本,特别是降后期的推理成本。

使用 DPO 微调 Llama 2 没看懂。

难点:

  1. 计算量太大
  2. 不好评估,因为大模型生成的海量内容暂无标准的答案,所以我们无法全部依赖人工去评判内容的质量。让模型做选择题不能准确的评估性能,一些垂类领域也很难搞到相关测试集,如果用GPT4评估又涉及到数据隐私问题,GPT4更倾向于给句子长的、回答更多样性的答案更高的分数,有时候也是不准的。
  3. 微调数据量到底要多少?
  4. LoRA VS 全量参数微调?微调基础模型LoRA还是比全量微调差一些的,微调对话模型差距不大,个人任务有时候全量微调是不如LoRA的,全量微调灾难遗忘现象会更加严重

微调算法

假如LLM的原始权重视为矩阵“X”。对于任何给定的任务,经过优化微调的 LLM 的权重由矩阵“Y”表示。微调的目标是发现一个 delta 矩阵“Z”,使得 X+Z=Y。然而,在 LoRA 的情况下,这个增量矩阵“Z”是通过低秩分解来近似的。因此,对于某些类型的任务来说, 一些数据集可能更容易对齐,而另一些数据集可能会有效果损失。相比之下,全参数微调则没有这个约束, 学习到的权重保留了原始模型的表达能力,可能简化了拟合不同数据的任务。

从0到1!得物如何打造通用大模型训练和推理平台

对于大语音模型来说,其参数量非常多。GPT3有1750亿参数,而且LLAMA系列模型包括 7B,13B,33B,65B,而其中最小的7B都有70亿参数。要让这些模型去适应特定的业务场景,需要对他们进行微调。如果直接对这些模型进行微调,由于参数量巨大,需要的GPU成本就会非常高。LoRA的做法是对这些预训练好的大模型参数进行冻结,也就是在微调训练的时候,这些模型的参数设置为不可训练。然后往模型中加入额外的网络层,并只训练这些新增的网络层参数。这样可训练的参数就会变的非常少,可以以低成本的GPU微调大语言模型。LoRA在Transformer架构的每一层注入可训练的秩分解矩阵,与使用Adam微调的GPT-3 175B相比,LoRA可以将可训练参数数量减少10000倍,GPU内存需求减少3倍,并且在效果上相比于传统微调技术表现的相当或更好。当前已经得到HuggingFace 的 PEFT库 https://github.com/huggingface/peft 的支持。

PEFT/LoRA原理

LoRA,LoRA背后有一个假设:我们现在看到的这些大语言模型,它们都是被过度参数化的。而过度参数化的大模型背后,都有一个低维的本质模型。通俗讲人话:大模型参数很多,但并不是所有的参数都是发挥同样作用的;大模型中有其中一部分参数,是非常重要的,是影响大模型生成结果的关键参数,这部分关键参数就是上面提到的低维的本质模型。LoRA的基本思路,在原始预训练模型旁边增加一个旁路,先用一个 Linear 层 A,将数据从 d 维降到 r 维,在用第二个 Linear 层 B,将数据从 r 维变回 d 维。LoRA 训练的时候固定预训练模型的参数,只训练降维矩阵 A 和升维矩阵 B。包括以下几步:

  1. 要适配特定的下游任务,要训练一个特定的模型,将Y=WX变成$Y=(W+∆W)X$,这里面$∆W$主是我们要微调得到的结果;
  2. 将∆W进行低维分解∆W=AB (∆W为m * n维,A为m * r维,B为r * n维,r就是上述假设中的低维);
  3. 接下来,用特定的训练数据,训练出A和B即可得到∆W,在推理的过程中直接将∆W加到W上去,再没有额外的成本。另外,如果要用LoRA适配不同的场景,切换也非常方便,做简单的矩阵加法即可:$(W + ∆W) - ∆W + ∆W`$。

深入浅出剖析 LoRA 技术原理 未细读。

HuggingFace Peft 实现

许多朋友在使用LoRA的过程中,都会用到HuggingFace Peft,使得预训练语言模型能够高效地适应各种下游应用,无需微调模型的所有参数。与 Accelerate 无缝集成,支持利用 DeepSpeed 和 Big Model Inference 来处理大规模模型。支持的方法:LoRA、Prefix Tuning、P-Tuning、Prompt Tuning、AdaLoRA等。

# 导入必要的库和函数
from transformers import AutoModelForSeq2SeqLM  # 用于加载和处理序列到序列的语言模型
from peft import get_peft_config, get_peft_model, LoraConfig, TaskType  # 导入PEFT相关的配置和功能
# 指定模型和分词器的路径或名称
model_name_or_path = "bigscience/mt0-large"      # 指定预训练模型的名称或路径
tokenizer_name_or_path = "bigscience/mt0-large"  # 指定分词器的名称或路径
# 配置PEFT的参数
peft_config = LoraConfig(
    task_type=TaskType.SEQ_2_SEQ_LM,  # 指定任务类型为序列到序列的语言模型
    inference_mode=False,  # 设置是否为推理模式
    r=8,  # 设置LoRA的rank
    lora_alpha=32,  # LoRA的alpha值,决定参数增加的数量
    lora_dropout=0.1  # LoRA层的dropout比例
)
# 加载预训练的序列到序列语言模型
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
# 应用PEFT方法调整模型
model = get_peft_model(model, peft_config)

HuggingFace Peft库封装好的LoRA接口,这个接口是对微软版LoRA代码的改写和封装,目的是减少大家在使用LoRA过程中的手工活(例如徒手更改模型架构,为模型添加LoRA adapter结构等),除此外核心处理逻辑不变。

LoRAAs with other methods supported by PEFT, to fine-tune a model using LoRA, you need to:

  1. Instantiate a base model.
  2. Create a configuration (LoraConfig) where you define LoRA-specific parameters.
  3. Wrap the base model with get_peft_model() to get a trainable PeftModel.
  4. Train the PeftModel as you normally would train the base model.
from peft import LoraConfig, get_peft_model
# # 创建基础transformer模型
model = xx.from_pretrained(args.model_dir)
config = LoraConfig(r=args.lora_r,lora_alpha=32,...)
model = get_peft_model(model, config)     # 初始化Lora模型
model = model.half().cuda()   

# 设置DeepSpeed配置参数,并进行DeepSpeed初始化
conf = {"train_micro_batch_size_per_gpu": args.train_batch_size,...)
model_engine, optimizer, _, _ = deepspeed.initialize(config=conf,model=model,model_parameters=model.parameters())
model_engine.train()                                                     
train_dataset = Seq2SeqDataSet(args.train_path, tokenizer,...)
train_dataloader = DataLoader(train_dataset,batch_size=...)
# 开始模型训练   
for i_epoch in range(args.num_train_epochs):         
    train_iter = iter(train_dataloader)    
    for step, batch in enumerate(train_iter):
        # 获取训练结果
        input_ids = batch["input_ids"].cuda()
        labels = batch["labels"].cuda()     
        outputs = model_engine.forward(input_ids=input_ids, labels=labels)     
        loss = outputs[0]    
        # 损失进行回传 
        model_engine.backward(loss)  
        model_engine.step() # 进行参数优化
    # 每一轮模型训练结束,进行模型保存  
    save_dir = os.path.join(args.output_dir, f"global_step-{global_step}")  
    model_engine.save_pretrained(save_dir)                           
def get_peft_model(model: PreTrainedModel, peft_config: PeftConfig, adapter_name: str = "default") -> PeftModel:
    model_config = getattr(model, "config", {"model_type": "custom"})
    peft_config.base_model_name_or_path = model.__dict__.get("name_or_path", None)
    if peft_config.task_type not in MODEL_TYPE_TO_PEFT_MODEL_MAPPING.keys() and not peft_config.is_prompt_learning:
        return PeftModel(model, peft_config, adapter_name=adapter_name)
    if peft_config.is_prompt_learning:
        peft_config = _prepare_prompt_learning_config(peft_config, model_config)
    return MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type](model, peft_config, adapter_name=adapter_name)

(微软的)LoraModel是LoRA模块的核心类,冻结base model的参数,旁路低秩矩阵的创建,替换,合并等逻辑都在这个类中。

# LoraModel也是继承torch.nn.Module,相当于pytorch的一个网络模块
class LoraModel(torch.nn.Module):
    def __init__(self, model, config, adapter_name) -> None:
        super().__init__()
        self.model = model  # # model 被用来微调的基础大模型
        self.forward = self.model.forward   # LoraModel把自己的前向传播函数forword设置为大模型的forward方法
        self.peft_config = config
        self.add_adapter(adapter_name,self.peft_config[adapter_name])
    def _find_and_replace(self,adapter_name):
        # 使用新的LoraLayer替换target_modules中配置的Layer,实现添加旁路低秩矩阵的功能。
        ...
    def mark_only_lora_as_trainable(model: nn.Module, bias:str="none") -> None:
        for n,p in model.named_parameters():
            if "lora_" not in n:
                p.requires_grad = False # 除了新增的LoraLayer的模块外,其他所有参数都被冻结。
        ...
    def forward(self, x:torch.Tensor):
        ...
        result = F.linear(x,...,bias=self.bias) # 使用大模型target_module中线性层进行计算,得出结果result。
        result += ( # 使用lora_A与lora_B的低秩矩阵进行计算并把计算结果加到result上。
            self.lora_B[self.active_adapter](
                self.lora_A[self.active_adapter](self.lora_dropout[self.active_adapter](x))
            )
            * self.scaling(self.active_adapter)
        )

可以看到

  1. Freeze 预训练模型权重/参数的本质:requires_grad = False ,不接受梯度更新,只微调参数A和B。,此时该步骤模型微调的参数量由$d×k$变成$d×r+r×k$,而$r≪min(d,k)$,因此微调参数量大量减少了。

图解大模型微调系列之:大模型低秩适配器LoRA(源码解读与实操篇) 未读。

深入浅出剖析 LoRA 源码及实践 未读。

P-Tuning 系列/可学习的提示

prompt tuning, prefix tuning 和p-tuning v1 有一定的联系,这几种方法都是基于优化continuous prompt,之前的工作都是手动设计模板或者自动生成模板,统称discrete prompt。discrete prompt有一定的局限性,找出的结果可能不是最优,而且对token的变动十分敏感,所以之后的研究方向也都是连续空间内的prompt。

P-Tuning,简称PT,是一种针对于大模型的soft-prompt方法,包括两个版本;P-Tuning,仅对大模型的Embedding加入新的参数; P-Tuning-V2,将大模型的Embedding和每一层前都加上新的参数,这个也叫深度prompt。

很多任务我们只需要在输入端输入合适的prompt,大语言模型就能给出一个比较合理的结果。但对于人工设计的prompt,prompt的变化对模型最终的性能特别敏感,加一个词、少一个词或者变动位置都会造成比较大的变化。那干脆让大模型学习一个合理的prompt,然后直接输入到模型中,这样是不是就可以直接得到一个比较合理的结果了?放弃之前人工或半自动离散空间的hard prompt设计,采用连续可微空间soft prompt设计,通过端到端优化学习不同任务对应的prompt参数。

Prefix Tuning(面向文本生成领域(NLG)) 方法在输入 token 之前构造一段任务相关的 virtual tokens 作为 Prefix,然后训练的时候只更新 Prefix 部分的参数,而 PLM 中的其他部分参数固定。

Prompt Tuning 可以看作是 Prefix Tuning 的简化版本,它给每个任务定义了自己的Prompt,然后拼接到数据上作为输入,但只在输入层加入prompt tokens(在输入embedding层加入一段定长的可训练的向量,在微调的时候只更新这一段prompt的参数),另外,virtual token的位置也不一定是前缀,插入的位置是可选的。 但也有一些缺点 PS: Prompt Tuning 和 P Tuning 分不清了。

  1. 针对模型参数量缺少通用性,之前的试验证明了p-tuning针对参数量大于10B的模型有很好的效果,甚至可以达到全量微调的效果。但是针对中等规模的模型,效果就不是很明显了。
  2. 针对不同任务的通用性也比较差,之前的实验结果证明了在一些NLU任务上效果比较好,对序列标注类比较难的任务效果较差。
  3. prompt只加在了embedding层,这样做就让prompt比较难训练,同时也导致了可训练参数比较少。

基于此,作者提出了P-tuning v2

  1. 在每一层都加入了Prompts tokens作为输入,而不是仅仅加在输入层,可学习的参数更多(从P-tuning和Prompt Tuning的0.01%增加到0.1%-3%),prompts与更深层相连,对模型输出产生更多的直接影响。

除Prefix Tuning用于NLG任务外,Prompt Tuning、P-Tuning、P-Tuning V2 均用于NLU,P-Tuning和Prompt Tuning技术本质等同,Prefix Tuning和P-Tuning V2技术本质等同。

对于领域化的数据定制处理,P-Tune(Parameter Tuning)更加合适。领域化的数据通常包含一些特定的领域术语、词汇、句法结构等,与通用领域数据不同。对于这种情况,微调模型的参数能够更好地适应新的数据分布,从而提高模型的性能。相比之下,LORA(Layer-wise Relevance Propagation)更注重对模型内部的特征权重进行解释和理解,通过分析模型对输入特征的响应来解释模型预测的结果。虽然LORA也可以应用于领域化的数据定制处理,但它更适合于解释模型和特征选择等任务,而不是针对特定领域的模型微调。

未细读

NEFT:新的指令微调技术大幅提升大模型性能(LLaMA系增加NEFT性能提升约10%)

代码

手写

  1. 加载数据集,pytorch Dataset/DataLoader
  2. 构建模型,在实际操作中,除了使用预训练模型编码文本外,我们通常还会进行许多自定义操作,因此在大部分情况下我们都需要自己编写模型,不过不用从0写,更为常见的写法是继承 Transformers 库中的预训练模型来创建自己的模型。
     class BertForPairwiseCLS(BertPreTrainedModel):     # 继承 BERT 模型(BertPreTrainedModel 类)
         def __init__(self, config):
             super().__init__(config)
             self.bert = BertModel(config, add_pooling_layer=False)
             self.dropout = nn.Dropout(config.hidden_dropout_prob)
             self.classifier = nn.Linear(768, 2)
             self.post_init()
            
         def forward(self, x):
             bert_output = self.bert(**x)
             cls_vectors = bert_output.last_hidden_state[:, 0, :]
             cls_vectors = self.dropout(cls_vectors)
             logits = self.classifier(cls_vectors)
             return logits
     config = AutoConfig.from_pretrained(checkpoint) # 通过预置的 from_pretrained 函数来加载模型参数
     model = BertForPairwiseCLS.from_pretrained(checkpoint, config=config).to(device) # 加载 预置模型
     print(model)
     # Transformers 库同样实现了很多的优化器,相比 Pytorch 固定学习率,Transformers 库的优化器会随着训练过程逐步减小学习率(通常会产生更好的效果)
     optimizer = AdamW(model.parameters(), lr=learning_rate)
     def train_loop(dataloader, model, loss_fn, optimizer,...): ...
     def test_loop(dataloader, model, mode='Test'): ...
     for t in range(epoch_num):
         total_loss = train_loop(train_dataloader, model, loss_fn, optimizer, lr_scheduler, t+1, total_loss)
         valid_acc = test_loop(valid_dataloader, model, mode='Valid')
         if valid_acc > best_acc:
             best_acc = valid_acc
             print('saving new weights...\n')
             torch.save(model.state_dict(), ...) #     # 保存模型
    

使用huggingface的Trainer API进行模型微调

Fine-tuning a model with the Trainer APITransformers provides a Trainer class to help you fine-tune any of the pretrained models it provides on your dataset. Once you’ve done all the data preprocessing work in the last section, you have just a few steps left to define the Trainer. The hardest part is likely to be preparing the environment to run Trainer.train()

from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

from transformers import TrainingArguments
training_args = TrainingArguments("test-trainer")

from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
from transformers import Trainer
trainer = Trainer(
    model,
    training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)
# To fine-tune the model on our dataset, we just have to call the train() method of our Trainer
trainer.train()

A full training 手写train loop。

使用医患对话数据训练新冠诊疗模型的例子

LLaMA-Factory
    /src
        /llmtuner
            /train
                /data 
                    /loader.py     # get_dataset
                    /preprocess.py # preprocess_dataset
                /model
                    /loader.py     # load_model_and_tokenizer
                /dpo
                    /trainer.py     # 一些trainer 用到的函数
                    /workflow.py    # run_dpo
                /ppo
                    /trainer.py     # 一些trainer 用到的函数
                    /workflow.py    # run_ppo
                /pt
                    /trainer.py     # 一些trainer 用到的函数
                    /workflow.py    # run_pt
                /rm
                    /trainer.py     # 一些trainer 用到的函数
                    /workflow.py    # run_rm
                /sft
                    /trainer.py     # 一些trainer 用到的函数
                    /workflow.py    # run_sft

workflow.py 的逻辑言简意赅,就是拼凑运行 Trainer的dataset、model、tokenizer、data_collator等参数

  1. 对于dataset 有一个load_dataset 和preprocess_dataset 的过程,preprocess_dataset 会根据任务目标不同,处理逻辑不同,也就是将数据转为input_ids 的方式不同。 最终转为trainer 也就是transformer model 可以接受的dataset,包含列 input_ids/attention_task/labels(或其它model.forward 可以支持的参数)。
  2. Trainer 对训练逻辑已经封的很好了,内部也支持了accelerate 和 deepspeed,只要合适的配置 training_args 即可。

以pt对应的workflow.py 为例

def run_pt(model_args: "ModelArguments",data_args: "DataArguments",training_args: "Seq2SeqTrainingArguments",finetuning_args: "FinetuningArguments",callbacks: Optional[List["TrainerCallback"]] = None):
    dataset = get_dataset(model_args, data_args)
    model, tokenizer = load_model_and_tokenizer(model_args, finetuning_args, training_args.do_train, stage="pt")
    dataset = preprocess_dataset(dataset, tokenizer, data_args, training_args, stage="pt")
    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
    trainer = Trainer(model,training_args,tokenizer,data_collator,callbacks,**split_dataset(dataset, data_args, training_args)
    # Training
    if training_args.do_train:
        train_result = trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
        trainer.log_metrics("train", train_result.metrics)
        trainer.save_metrics("train", train_result.metrics)
        trainer.save_state()
        trainer.save_model()
        if trainer.is_world_process_zero() and model_args.plot_loss:
            plot_loss(training_args.output_dir, keys=["loss", "eval_loss"])
     # Evaluation
    if training_args.do_eval:
        metrics = trainer.evaluate(metric_key_prefix="eval")
        perplexity = math.exp(metrics["eval_loss"])
        metrics["perplexity"] = perplexity
        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

不管是PreTraining阶段还是SFT阶段,loss函数都是一样的,只是计算的方式存在差异,PreTraining阶段计算的是整段输入文本的loss,而SFT阶段计算的是response部分的loss。

  1. preprocess_pretrain_dataset处理PreTraining阶段的数据,数据组成形式:
    1. 输入input: <bos> X1 X2 X3
    2. 标签labels:X1 X2 X3 </s> 典型的Decoder架构的数据训练方式;
  2. preprocess_supervised_dataset处理SFT阶段的数据,数据组成形式:
    1. 输入input:<bos> prompt response
    2. 标签labels: -100 ... -100 response </s> 对于prompt部分的labels被-100所填充,这样在计算loss的时候模型只计算response部分的loss,-100的部分被忽略了。这个机制得益于torch的CrossEntropyLossignore_index参数,ignore_index参数定义为如果labels中包含了指定了需要忽略的类别号(默认是-100),那么在计算loss的时候就不会计算该部分的loss也就对梯度的更新不起作用。

PS: 深度学习都得指定features/labels。在llm 场景下,features 和labels 有几个特点

  1. llm 有base model、sft model 等,不同的model 数据集格式不同,一般分为几个部分,比如sft 的{"question:":"xx","answer":"xx"},各家模型都不太一样,很多数据集是不公开的。但不管如何,这几部分都会拼为一个sentence(中间可能有一些特殊字符起到连接作用),然后把sentence通过tokenizer转换成input_ids,之后再走embedding 模块等等就是Transformer系列模型内的事儿了,最后得到output_ids.
  2. 模型输入格式,模型输入dict 一般包含3个key: input_ids,attention_mask,labels
    1. 有些模型内置从input ids 提取attention mask的操作
    2. 预训练场景 labels 一般由input_ids copy而来,然后做一些处理,比如labels 全部左移一位(预训练)
    3. 明确指定labels 的话,一般是要微调,比如sft时,sentence部分中question 的位置都置为-100,-100表示在计算loss的时候会被忽略,这个由任务性质决定。
  3. 预处理(将dataset 转为模型输入)过程由​ Dataset.map() + tokennizer 来办。
         def tokenize_function(example):
             # example 表示数据集中的一行数据
             return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
         tokenized_dataset = dataset.map(tokenize_function, batched=True)
    
  4. 之后就是对output_ids 和 labels 计算loss。