技术

上下文记忆 agentic chat 图数据库的一些考量 LLM一些探索 Agent实践 LLM预训练 向量数据库的一些考量 fastapi+sqlalchemy进行项目开发 LLM微调实践 Python协程实现 Agent Functon Calling LLamaIndex入门 Multi-Agent探索 Python虚拟机 LLM工作流编排 Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 Transformers源码学习 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快速入门

架构

bert rerank微调 大模型推理tips RAG向量检索与微调 dddfirework源码分析 RAG与知识图谱 大模型推理服务框架vLLM 大模型推理服务框架 模型服务化(未完成) 大模型Post-Training 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 kubevela源码分析 容器和CPU那些事儿 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 K8S YAML 资源清单管理方案 tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 推荐系统embedding原理及实践 机器学习中的python调用c 机器学习训练框架概述 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tensorflow学习 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——kubelet与容器引擎之间的接口 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 kubevela源码分析 容器和CPU那些事儿 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 K8S YAML 资源清单管理方案 从混部到统一调度 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——kubelet与容器引擎之间的接口 资源调度泛谈 如何学习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 组件
上下文记忆 agentic chat bert rerank微调 大模型推理tips LLM一些探索 Agent实践 LLM预训练 RAG向量检索与微调 LLM微调实践 RAG与知识图谱 大模型推理服务框架vLLM Agent Functon Calling LLamaIndex入门 Multi-Agent探索 LLM工作流编排 大模型推理服务框架 模型服务化(未完成) 大模型Post-Training 大模型训练 大模型推理 从Attention到Transformer 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Transformers源码学习 LangChain源码学习 如何应用LLM 小鼠如何驾驭大象(LLM)? AutoML和AutoDL 特征平台 实时训练 tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 推荐系统embedding原理及实践 机器学习中的python调用c 机器学习训练框架概述 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 推理服务 mpi 学习pytorch 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tensorflow学习 kaggle泰坦尼克问题实践 神经网络模型优化 概率论 直觉上理解深度学习 如何学习机器学习 深度学习泛谈

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. 数据安全的问题。如果数据是不能传递给第三方大模型服务的,那么搭建自己的大模型就非常必要。通常这些开源的大模型都是需要用自有数据进行微调,才能够满足业务的需求,这时候也需要对大模型进行微调。 当 通过 prompt engineering 无法解决或 prompt 中描述过于复杂时,通过优化 prompt 已经不能解决剩余的 badacse,期望通过 SFT 来减少 prompt 中的内容,加速线上推理的耗时,可以考虑sft了。反过来,prompt潜力还未挖掘时不必轻易开始。SFT 数据集中也依赖 prompt。因此,做 SFT 之前尽量把 prompt 做到最优。

外挂知识库也不是全能的

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

Meta祭出三篇最详尽的LLaMA微调指南

  1. pre-training,通常使用自监督算法进行训练。对last token 计算loss,目的是学习语言能力/一句话的统计概率/世界知识。
    1. 继续预训练(也称为第二阶段预训练)进一步在特定领域的数据上进行训练,以提高模型对该领域的理解和适应能力。同样使用与初始预训练相同的自监督算法。数据集通常是未标注的,并且规模较大。通常会涉及所有模型权重,并将一部分原始数据与新数据混合。
  2. post-training 微调一般的目的在于优化模型在特定任务上的表现。比如sft 微调, 在包含正确标签/答案/偏好的注释数据集上进行监督训练,而不是自监督训练,主要目的是提高能力,如指令遵循、人类对齐、任务执行等。仅对response 部分计算loss,目的是让response 对prompt 做出反应。prompt 本身并不值得太多学习。根据更改的参数量,有两类算法:
    • 全面微调:顾名思义,这包括更改模型的所有参数,包括在XLMR和BERT(100-300M参数)等小模型上所做的传统微调,以及对Llama 2、GPT3(1B+参数)等大模型上的微调。
    • 参数高效微调(PEFT):PEFT算法只微调少量额外参数,或更新预训练参数的子集,通常是总参数的1%-6%,而不是对所有LLM权重进行离线微调。
  3. 研究人员将以下场景确定为可从微调中受益的常见用例:
    • 语气、风格、形式定制:使用案例可能会寻求反映特定角色或服务特定受众的LLM。另外,研究者可能还希望它能以特定的方式组织输出,例如,JSON、YAML或Markdown格式的输出。
    • 提高精度并处理边缘情况:微调可以用于纠正幻觉或错误,这些错误很难通过prompt和上下文学习来纠正。尤其是处理代表性不足的领域:尽管LLM接受了大量通用数据的训练,但它们可能并不总是精通每一个特定领域的细微差别的行话、术语或具体情况。

整体思路

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

垂直领域大模型落地思考

垂直大模型基本套路

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

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

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

领域微调

领域微调的核心是构建高质量大规模的领域微调数据。让人去收集一个领域内的语料是容易的,但是让人去编写领域内的微调指令和回答是很难的。下面介绍的方法都是来尝试解决这个问题。这些方法的核心都是基于一些已有的数据+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)。全模型微调(Full Model 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背后有一个假设:我们现在看到的这些大语言模型,它们都是被过度参数化的

  1. 一旦我们找到了足够解决问题的参数空间,再增加这个参数空间的大小并不会显著提升模型的性能。
  2. 一个过参数的模型的参数空间是有压缩的空间的,这也就是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 技术原理 未细读。

  1. 训练:原模型固定,只训练降维矩阵A和升维矩阵B。PS:与其它参数高效微调方法正交,可有效组合
  2. 推理:可将BA加到原参数上,不引入额外的推理延迟。PS:将BA加到W上,不引入额外计算量
  3. 初始化:A采用高斯分布初始化,B初始化为全0,保证训练开始时旁路为0矩阵。
  4. 可插拔式的切换任务:当前任务W0+B1A1,将lora部分减掉,换成B2A2,即可实现任务切换。PS:由于LoRA不改变原模型的参数,它在多任务或多场景的应用中具有很高的可重用性。不同任务的低秩矩阵可以分别存储和加载,灵活应用于不同任务中。

缺点:LoRA参与训练的模型参数量不多,也就百万到千万级别的参数量,所以效果比全量微调差很多。(数据以及算力满足的情况下,还是微调的参数越多越好)如果有足够计算资源以及有10k以上数据,还是建议全参数微调,lora的一个初衷就是为了解决不够计算资源的情况下微调,只引入了少量参数,就可以在消费级gpu上训练,但lora的问题在于它不能节省训练时间,相比于全量微调,他要训练更久,同时因为可训练参数量很小,在同样大量数据训练下,比不过全量微调。

LoRA微调方法为啥能加速训练?

  1. 只更新了部分参数:比如LoRA原论文就选择只更新Self Attention的参数,实际使用时我们还可以选择只更新部分层的参数;
  2. 减少了通信时间:由于更新的参数量变少了,所以(尤其是多卡训练时)要传输的数据量也变少了,从而减少了传输时间;
  3. 采用了各种低精度加速技术,如FP16、FP8或者INT8量化等。PS:不是LoRA所独有的

代码示例

import torch
import torch.nn as nn

class LoRALayer(nn.Module):
    def __init__(self, original_layer, rank=4):
        super(LoRALayer, self).__init__()
        self.original_layer = original_layer
        self.rank = rank
        self.A = nn.Parameter(torch.randn(original_layer.weight.size(0), rank))
        self.B = nn.Parameter(torch.randn(rank, original_layer.weight.size(1)))

    def forward(self, x):
        delta_w = torch.matmul(self.A, self.B)  # 多维矩阵乘法
        return self.original_layer(x) + torch.matmul(x, delta_w.t())

# 示例:在Transformer模型中使用LoRA层
class TransformerWithLoRA(nn.Module):
    def __init__(self, transformer, rank=4):
        super(TransformerWithLoRA, self).__init__()
        self.transformer = transformer
        for name, module in transformer.named_modules():
            if isinstance(module, nn.Linear):
                setattr(self, name, LoRALayer(module, rank))

    def forward(self, x):
        return self.transformer(x)

大模型微调经验和认知lora_rank是一个很重要的参数,它影响旁路矩阵的大小。如果你的数据量比较小,那推荐用比较小的rank就可以了,我记得原论文里8和32区别不大,如果你数据量较大,那建议用更大的rank,来得到一个更大的旁路矩阵,它显然可以记住更多的东西。与此同时,除了q_proj,v_proj,强烈建议再试一下把所有的线性层都上lora,如k_proj, up_proj, down_proj这些。此外lora_alpha也很重要,它通常和lora_rank是正比关系,表示一个缩放系数。alpha越大,表示新建的旁路矩阵影响力越大、新数据学得越“猛”;alpha越小,表示原始模型参数对结果的影响力越大。很多人喜欢设置alpha是rank的2倍,其实可以二者1: 1跑个baseline看看效果。 1. lora的秩的值设置在1-16上还是存在不小的区别,从16到128上经常只是一些收敛上的差异,例如128可能n个epoch收敛到x,16可能要2n,但并不绝对,而且r大时间久,一般16-32是比较推荐的

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%)

克服微调垂类领域模型导致的通用领域知识遗忘的好方法——llama_pro