2024年10月

今天早上,OpenAI实施团队的 @shyamal在Github上开源了
Swarm
这个OpenAI官方的多智能体框架。不得不说,OpenAI官方下场,获得的社区影响就是不一样,在微信群、朋友圈里已经出现大量的解析文章。

image

这个多智能体框架确实已经把多智能体的关键,说的很透彻了,Swarm 里面定义了两个核心
「Agents」

「Handoffs」,多智能体的核心是在这个Handoffs上面。
简单看了下examples 之后我觉得这个多智能体框架并不够好,恰巧的是,我对云原生技术很熟,借用一下云原生的发展历程,给这个[Swarn]框架做个简要点评:从云原生容器发展的历史来看,相当于docker swarm 和 k8s, 我们需要的智能体框架应该是k8s 这样的一个框架,如果你是一位云原生技术熟悉的同学很容易就知道我在说什么了。

单Agent这块,简单封装提示词和使用函数调用就可以完成业务,OpenAI就一个 /api/chatcompletions 接口就帮我们搞定了,市场上大量的Agent 产品都停留在单Agent 上,但是「Handoffs」这块,Swarm的确做的非常优雅了。

swarm_diagram

个人观点认为他的设计还没有我们的多智能体框架好用,OpenAI的[Swarm]是docker swarm,我们的多智能体框架就是k8s,我需要的是像k8s编排容器那样编排智能体,我们刚刚在9月26日对外发布了多智能体的工业设计产品,详见:
智用研究院AI Agent Foundry赋能的首个多Agent驱动的工业设计平台圆满发布

多智能体的核心难题其是不同智能体之间的通信问题。怎麼传递信息,传哪些信息,这些都很重要。多个智能体协作,也只需要在必要的时候被调用起来就可以了。看我们智能体协作图:

image


当我们多智能体应用接收到用户的请求,借用Semantic kernel的设计理念叫实现“目标导向”的AI应用,这意味着它能够帮助确定目标,然后寻找实现这些目标的方法和步骤。在“目标导向”的方法中,首先需要确定目标,然后通过规划器(Planner)将目标分解为一系列需要执行的任务。这些任务可以逐个执行,以实现最终目标。这个过程对于人类来说是很自然的,但对于机器来说则相对复杂。借助LLM AI的力量,我们可以更轻松地实现这一过程。

这个接收到用户请求的智能体我们叫做路由智能体,他负责路由到具体执行任务的任务智能体。我们的智能体框架的Planner 也是类似于OpenAI的Swarm的「Handoffs」处理了交接的逻辑,我们的Planner 要比Handoffs处理的更完美。OpenAI的Swarm 目前还处于实验阶段,期望他发展成为k8s 这样的一个多智能体编排框架:

image

这个框架是python写的,大家觉得用python 写多智能体应用是好选择吗? 我个人认为做应用开发,Python并不是好选择,Python之所以用的多,是因为这一波人工智能的主导者是算法工程师,他们习惯用的编程语言是Python罢了,随着复杂场景的人工智能应用需求的增加,控制权逐步要回归到应用开发者的手中,对于复杂度高、需要长期维护的应用系统还是需要用c# 、java等业务系统开发类的编程语言来主导。

image

说明

该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

友情提醒:本篇文章是属于系列文章,看该文章前,建议先看之前文章,可以更好理解项目结构。

有兴趣的朋友,请关注我吧(*^▽^*)。

关注我,学不会你来打我

为什么要用全局异常捕获?

对于一个系统来说,全局异常捕获是必不可少的,它不仅可以把异常信息精简后反馈给用户,还能帮助程序员减少解决问题的时间,以及记录系统中任何一处发生异常的信息。

你是否依然有以下苦恼?

你是否还在为怎么记录系统异常日志而苦恼?

你是否还在为系统报错位置和报错信息苦恼?

你是否还在每个接口处增加日志记录操作?

如果你有,那么本篇文章正好可以解决你的难题。

什么是全局异常捕获机制?

全局异常捕获,顾名思义就是系统无论在那个位置发生错误都会被捕获,从而进行处理。

创建接口返回模型

创建一个接口返回模型:ReceiveStatus.cs

它的主要作用是把接口返回的数据、信息推送给前端。

 /// <summary>
 ///接口返回实体模型/// </summary>
 public classReceiveStatus
{
/// <summary> ///编码/// </summary> public CodeStatuEnum code { get; set; }/// <summary> ///信息/// </summary> public string msg { get; set; }/// <summary> ///是否成功/// </summary> public bool success { get; set; }/// <summary> ///构造函数/// </summary> publicReceiveStatus()
{
code
=CodeStatuEnum.Successful;
success
= true;
msg
= "操作成功";
}
}
/// <summary> ///接口返回结果集/// </summary> /// <typeparam name="T"></typeparam> public class ReceiveStatus<T>: ReceiveStatus
{
/// <summary> ///数据/// </summary> public List<T> data { get; set; }/// <summary> ///总数量/// </summary> public int total { get; set; }
}
CodeStatuEnum.cs枚举值如下
 /// <summary>
 ///代码状态枚举/// </summary>
 public enumCodeStatuEnum
{
/// <summary> ///操作成功/// </summary> Successful = 200,/// <summary> ///警告/// </summary> Warning = 99991,/// <summary> ///操作引发错误/// </summary> Error = 99992}

创建好接口返回模型后,我们创建一个异常帮助类,它的主要用途,是区分【系统异常】还是用户自定义的【业务异常】。

/// <summary>
///异常帮助类/// </summary>
public classExceptionHelper
{
/// <summary> ///自定义异常(会写入错误日志表)/// </summary> /// <param name="msg"></param> public static void ThrowBusinessException(stringmsg)
{
throw newException(msg);
}
/// <summary> ///自定义业务异常(不会写入错误日志表)/// </summary> /// <param name="msg">信息信息</param> /// <param name="codeStatu">异常状态</param> /// <returns>返回结果集</returns> public static ReceiveStatus CustomException(string msg, CodeStatuEnum codeStatu =CodeStatuEnum.Warning)
{
ReceiveStatus receiveStatus
= new();
receiveStatus.code
=codeStatu;
receiveStatus.msg
=msg;
receiveStatus.success
= false;returnreceiveStatus;
}

}
/// <summary> ///异常帮助类(返回数据)/// </summary> /// <typeparam name="T"></typeparam> public class ExceptionHelper<T>: ExceptionHelper
{
/// <summary> ///自定义业务异常(不会写入错误日志表)/// </summary> /// <param name="msg">信息信息</param> /// <param name="codeStatu">异常状态</param> /// <returns>返回结果集</returns> public static ReceiveStatus<T> CustomExceptionData(string msg, CodeStatuEnum codeStatu =CodeStatuEnum.Warning)
{
ReceiveStatus
<T> receiveStatus = new();
receiveStatus.code
=codeStatu;
receiveStatus.msg
=msg;
receiveStatus.success
= false;
receiveStatus.data
= new System.Collections.Generic.List<T>();returnreceiveStatus;
}
}

创建全局异常捕获中间件

在wenApi启动项目中创建一个类:ExceptionPlugIn.cs

它的主要作用就是捕获系统中发生异常对代码和记录异常日志。

它需要继承一个接口:IAsyncExceptionFilter

/// <summary>
///全局异常捕获中间件/// </summary>
public classExceptionPlugIn : IAsyncExceptionFilter
{
/// <summary> ///全局异常捕获接口/// </summary> /// <param name="context"></param> /// <returns></returns> publicTask OnExceptionAsync(ExceptionContext context)
{
//异常信息 Exception ex =context.Exception;//异常位置 var DisplayName =context.ActionDescriptor.DisplayName;//异常行号 int lineNumber = 0;const string lineSearch = ":line";var index =ex.StackTrace.LastIndexOf(lineSearch);if (index != -1)
{
var lineNumberText = ex.StackTrace.Substring(index +lineSearch.Length);
lineNumber
= Convert.ToInt32(lineNumberText.Substring(0, lineNumberText.IndexOf("\r\n")));
}
//如果异常没有被处理则进行处理 if (context.ExceptionHandled == false)
{
string exceptionMsg = "错误位置:" + DisplayName + "\r\n" + "错误行号:" + lineNumber + "\r\n" + "错误信息:" +ex.Message;//定义返回类型 var result = new ReceiveStatus<string>{
code
=CodeStatuEnum.Error,
msg
= "错误信息:" +exceptionMsg,
success
= false,
};
context.Result
= newContentResult
{
//返回状态码设置为200,表示 StatusCode =StatusCodes.Status500InternalServerError,//设置返回格式 ContentType = "application/json;charset=utf-8",
Content
=JsonConvert.SerializeObject(result)
};
//记录日志 }//设置为true,表示异常已经被处理了 context.ExceptionHandled = true;returnTask.CompletedTask;
}
}

可以在OnExceptionAsync方法中添加记录日志、异常类型、异常分析等代码。

添加到服务中

编写好异常捕获机制后,我们需要把该类添加到Program.cs的服务中

//自定义全局异常处理
builder.Services.AddControllers(a =>{
a.Filters.Add(
typeof(ExceptionPlugIn));
});

测试全局异常捕获机制

添加一个异常测试接口

运行测试

以上就是全局异常捕获机制,感兴趣的可以下载项目,修改吧。

源代码地址:https://gitee.com/yangguangchenjie/overall-auth2.0-web-api

预览地址:http://139.155.137.144:8880/swagger/index.html

帮我Star,谢谢。

有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

plsql是什么:

就是这个,专门操作oracle的一个工具,好用还免费。

创建一个测试表:

create table Student(
Id number not
null,
Name varchar(
20),
Age number,
Grade number,
Gender varchar(
2)
)

里面的varchar2()是oracle自己专门的字符类型,用就行了。

光标移到表上,右键选择Describe:

现在这些字段都没有说明,不知道是什么意思,给他们都添加说明

comment on table Student is '学生表';
comment on column Student.id
is 'ID';
comment on column Student.Name
is '姓名';
comment on column Student.Age
is '年龄';
comment on column Student.Grade
is '年纪';
comment on column Student.Gender
is '性别';

添加一条测试数据

添加多条数据,但是不写insert

在后面输入一个for update,上面的操作栏会显示有可以提交的事务,先不用管,然后现在点击一下下面的锁

oracle会生成一个空白行,然后前面带有一个✳,我们先选中我们添加的那一行数据:

然后复制一下,复制以后再选中下一行,不停的粘贴就行了

然后改一下数据,最后点击一下那个绿色的小勾,再点一下绿色的锁,最后我们去点一下菜单栏的提交事务按钮

执行完毕以后点击查询就可以了:

如果只想执行某一段代码,可以用鼠标选中自己想执行的代码就行了,如图所示,后面的for update就没有执行;

如果想更新某个字段,也可以直接通过上面的步骤操作,有点像在操作excel的感觉;

如果想删除,也和上面的操作类似,只不过是点击的按钮不一样;

执行以后,刘德华就会被删除。

数据的导出:

可以选中行,按住ctrl可以选多行.

在粘贴板上就会把sql语句粘贴进去:

删掉多余的,只保留insert部分就可以了。

怎么看我们最开始的建表语句了:

点击  view

右下角有一个view sql的按钮,点一下

点进去就可以看到建表语句了,复制出来保存就行了。

暂时只想到这些

下面是一些常用的查询语句

select * from student t where instr(t.name, '') > 0; --模糊查询select *
  fromstudent twhere (t.name = '刘德华' and t.age = '50')
or t.name
= '梁朝伟'; --多个条件的查询select t.*,casewhen t.gender= ''then'帅哥'when t.gender= ''then'美女' else '不知道'end p--查询的时候条件判断fromstudent t;select t.*, decode(t.name, '刘德华', '我最喜欢的明星', '明星') --判断fromstudent t;select t.*, nvl(t.name, '非主流') from student t; --判断名字是不是空

select wm_concat(t.name) from student t --合并多行的某条数据,可以配合group by

QQ技术交流群:332035933;

有向图
和上一篇介绍的
无向图
基本一样,唯一的区别在于
有向图
的边有方向性,它表示的是顶点之间的单向或依赖关系。

有向图
G
一般表示为:
G=<V,E>
。和无向图一样,
V
是顶点集合,
E
是边的集合。

不同之处在于,
无向图
是用小括号
(V,E)

有向图
用尖括号
<V,E>


有向图
中,边是有方向的,所以,从
顶点A到顶点B
的边与从
顶点B到顶点A
的边是
不同
的。


无向图
一样,
有向图
也有很多应用场景,比如:


地图导航
中,
有向图
常被用来表示道路网络。

节点代表地点(如交叉路口、城市等),有向边代表道路,边的权重可以表示道路的长度、行驶时间或交通状况等。


供应链管理
中,有向图可以用来表示货物的流动路径。

节点代表供应链中的各个环节(如供应商、制造商、分销商等),边代表货物流动的路径,边的容量可以表示货物的承载能力。


社会网络
中,上一篇提到可以用无向图表示用户之间的好友关系。

而有向图同样可以用在社会网络分析,它可以用来表示用户之间的关注关系,转发关系等,用于分析用户的行为模式。

下面介绍
manim
中绘制
有向图
的对象
DiGraph

1. 主要参数

有向图对象
DiGraph
主要参数和无向图类似:

参数名称 类型 说明
vertices list 图的顶点列表
edges list 图的边列表,每个边
labels dict 顶点是否显示标签文本
label_fill_color str 标签的背景色
layout str 图中定点的布局方式
layout_config dict 配置如何布局图中各个顶点
layout_scale float 图各个顶点布局的比例
vertex_type Mobject 顶点的类型,不一定是点,也可以是manim中其他的对象
vertex_config dict 顶点相关的配置
vertex_mobjects dict 一系列的顶点对象
edge_type Mobject 边的类型,不一定是线,也可以是manim中其他的对象
edge_config dict 边相关的配置
paritions list
root_vertex dict

这些参数中,
vertices

edges
相关的参数(比如xxx_type,xxx_config)比较好理解。

labels
参数设置是否需要显示顶点的标签,默认是把
vertices
的数值作为标签的内容。

layout
参数内置了多种现成的布局方式:

  • 'circular',
  • 'kamada_kawai'
  • 'partite'
  • 'planar'
  • 'random'
  • 'shell'
  • 'spectral'
  • 'spiral'
  • 'spring'
  • 'tree'

layout_config
参数可以对上面现成布局方式的进行微调。

最后两个参数
paritions

root_vertex
比较特殊,

paritions
只能在
layout
设置为
'partite'
时使用,用来生成层状的图(比如描述神经网络的图),

paritions
用来设置每一层包含哪些顶点;

root_vertex
只能在
layout
设置为
'tree'
时使用,用来树状图,

root_vertex
用来设置树的根节点。

后面的示例会演示如何使用
paritions

root_vertex
来生成
层状

树状

有向图

2. 主要方法

有向图
DiGraph
的方法主要用来动态改变有向图,比如添加或删除顶点和边。

名称 说明
add_edges 增加有向图的边
add_vertices 增加有向图的顶点
remove_edges 删除有向图的边
remove_vertices 删除有向图的顶点
change_layout 动态改表有向图的结构
from_networkx
networkx
来生成有向图

networkx
是另一个常用的
Python
库,用于创建、操作和研究复杂网络的结构。

DiGraph
对象也可以直接根据
networkx
的对象生成图。

3. 使用示例

下面的示例和上一篇无向图的示例类似,只是改用有向图
DiGraph
对象来实现。

3.1. 顶点的配置

顶点的设置和无向图几乎是一样的。

# 不同颜色的设置
graph = DiGraph(
    vertex_config={
        0: {"color": RED},
        # ...
    },
)

# 顶点显示标签
graph = DiGraph(
    labels=True,
)

# 星形顶点
graph = DiGraph(
    vertex_config={"outer_radius": 0.15},
    vertex_type=Star,
)

3.2. 边的配置

有向图的边也和顶点一样,可以设置颜色,粗细等属性,

与无向图不同之处在于:有向图的边可以设置箭头的样式。

# 边的颜色
graph = DiGraph(
    edge_config={
        (0, 1): {"color": RED},
        # ...
    },
)

# 边的粗细
graph = DiGraph(
    edge_config={
        (0, 1): {"stroke_width": 1},
        # ...
    },
)

# 不同箭头的边
graph = DiGraph(
    edge_config={
        (0, 1): {
            "tip_config": {
                "tip_shape": ArrowCircleTip,
            },
        },
        (0, 2): {
            "tip_config": {
                "tip_shape": ArrowTriangleTip,
            },
        },
        # ...
    },
)

3.3. 内置的layout

有向图
中内置的layout和上一篇无向图中介绍的是一样的。

for layout in [
    "spring",
    "circular",
    "kamada_kawai",
    "planar",
    "random",
    "shell",
    "spectral",
    "spiral",
]:
    graph = DiGraph(
        layout=layout,
    )

3.4. 层状图

层状图的布局需要配合参数
partitions
一起使用,
partitions
中决定每一层中有哪些顶点。

有向图的边有方向,绘制出来更像神经网络的结构。

partitions = [[0, 1], [2, 3, 4], [5, 6], [7, 8]]
graph = DiGraph(
    layout="partite",
    partitions=partitions,
)

3.5. 树状图

树状图的布局需要配合参数
root_vertex
一起使用,
root_vertex
定义了树的
根顶点
是哪个。

这里与
无向图
有个不同的地方,绘制有向的树状图时,顶点和边的顺序很重要,需要从根节点开始,依次传入各个顶点。

下面示例中,第二个树状图改变了 根节点,不是仅仅改变
root_vertex
就行的,需要先改变图中顶点的顺序。

下面的代码是简略后的代码,完整的代码可以文中最后部分的链接中下载。

# 初始的树
graph = DiGraph(
    layout="tree",
    root_vertex=0,
)

# 重要!!! 
# 修改前需要调整节点和边的顺序

# 修改根节点
graph2 = DiGraph(
    layout="tree",
    root_vertex=2,
)

4. 附件

文中完整的代码放在网盘中了(
digraph.py
),

下载地址:
完整代码
(访问密码: 6872)

本研究解决了领域-类别增量学习问题,这是一个现实但富有挑战性的持续学习场景,其中领域分布和目标类别在不同任务中变化。为应对这些多样化的任务,引入了预训练的视觉-语言模型(
VLMs
),因为它们具有很强的泛化能力。然而,这也引发了一个新问题:在适应新任务时,预训练
VLMs
中编码的知识可能会受到干扰,从而损害它们固有的零样本能力。现有方法通过在额外数据集上对
VLMs
进行知识蒸馏来解决此问题,但这需要较大的计算开销。为了高效地解决此问题,论文提出了分布感知无干扰知识集成(
DIKI
)框架,从避免信息干扰的角度保留
VLMs
的预训练知识。具体而言,设计了一个完全残差机制,将新学习的知识注入到一个冻结的主干网络中,同时对预训练知识产生最小的不利影响。此外,这种残差特性使分布感知集成校准方案成为可能,明确控制来自未知分布的测试数据的信息植入过程。实验表明,
DIKI
超过了当前最先进的方法,仅使用
0.86%
的训练参数,并且所需的训练时间大幅减少。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Mind the Interference: Retaining Pre-trained Knowledge in Parameter Efficient Continual Learning of Vision-Language Models

Introduction


监督学习技术在对所有数据完全访问的情况下训练网络,这可能导致在扩展网络以获取新任务知识时缺乏灵活性。持续学习(
CL
)作为一种解决方案应运而生,使得模型能够在陆续到达的数据上进行持续训练,同时保留所学的信息。传统的
CL
设置一般考虑的只新引入的类别或领域分布的变化,这称为类别增量学习和领域增量学习。然而,只考虑一种增量的现有工作限制了它们在复杂现实场景中的适用性。

考虑一个更具挑战性的领域-类别增量学习(
DCIL
)设置,在该设置中,领域数据分布和待分类的类别在所有任务中可能不断变化,如图
1(a)
所示。在这种情况下,基于传统图像编码器的技术由于其不可扩展的分类头设计而无法实现。最近,对比训练的视觉-语言模型(
VLMs
)如
CLIP
的出现,使得解决这一要求高但实际的问题成为可能。
VLMs
是在大规模的图像-文本对上训练的,具有强大的零样本泛化能力,可以识别几乎无限的类别,应对这种严重的任务变化场景。

然而,使用视觉-语言模型引入了增量训练的新挑战。传统的持续学习方案旨在防止模型遗忘先前学习的知识,这被称为向后遗忘(忘记微调的知识)。现有的研究探讨了正则化机制、复习缓冲区和架构设计在减轻向后遗忘方面的潜力,并取得了令人鼓舞的成果。然而,当这些方法应用于视觉-语言模型时,出现了一种不同形式的灾难性遗忘:模型往往会遗忘在预训练阶段所学的知识,从而妨碍其强大的零样本泛化能力。这个问题被称为向前遗忘(忘记预训练的知识),因为它发生在
VLMs
对未知分布数据进行“向前”预测时。图
1(a)
展示了这两种遗忘类型。

最近的工作
ZSCL
尝试解决
CLIP
上的向前遗忘问题,引入了一个大规模的参考数据集来进行知识蒸馏,并结合了权重集成方案。然而,这种方法需要大量的计算和外部数据,在实际场景中可能不可行。同时,现有的基于
VLM
的参数高效持续学习方法主要利用提示调整机制,未能保留预训练知识,并导致零样本能力下降,如图
1
(b)所示。论文将这个问题归因于信息干扰:新引入的任务特定参数可能会干扰预训练知识。这些方法的示意图如图
1(c)
所示。

为了以计算和参数高效的方式缓解
VLMs
的向前遗忘问题,论文引入了分布感知无干扰知识融合(
DIKI
)框架。具体而言,将任务特定信息注入到冻结的
VLM
中,以便为每个任务高效地存储已学习的知识。

论文的贡献总结为三点:

  1. 引入了参数高效的
    DIKI
    ,以在
    DCIL
    设置下保留
    VLM
    中的预训练知识。它解决了信息干扰问题,降低了对大量计算和外部数据的需求。
  2. 为了缓解向前遗忘,
    DIKI
    以完全残差的方式植入新知识,保持预训练知识不受干扰。凭借这种残差特性,进一步集成了分布感知融合校准,以提高在未见任务上的性能。
  3. 综合实验表明,与以前的方法相比,
    DIKI
    以仅
    0.86%
    的训练参数和显著更少的训练时间实现了最先进的性能。

Preliminaries


  • Continual learning protocol

持续学习旨在以顺序方式学习不同的任务,同时不忘记之前学到的知识。考虑到
\(N\)
个顺序到达的任务
\(\left[ \mathcal{T}^1, \mathcal{T}^2, \cdots, \mathcal{T}^N \right]\)
,每个任务
\(\mathcal{T}^i\)
包含一个数据集
\(D^i=\{x^i_j, y^i_j\}_{j=1}^{N^i}\)
,其中
\(x^i_j\)
是一幅图像,
\(y^i_j\)
是当前数据集中对应的独热标签,
\(N^i\)
是图像样本的数量。此外,还包括一个类名集合
\(C^i=\{c^i_j\}_{j=1}^{N_{c}^i}\)
,将标签索引连接到
VLMs
使用的类别名称。

与之前的类别和领域增量学习设置不同,本研究强调了一种更实际的持续学习设置:领域-类别增量学习(
DCIL
)。在这个设置中,领域分布和需要识别的类别在不同任务之间不断变化,即
\(C^i \neq C^j\)

\(\mathbb{P}(D^i) \neq \mathbb{P}(D^j)\)
,对于
\(i \neq j\)
,其中
\(\mathbb{P}\)
表示任务数据集的数据分布。

  • Vision-language models

在具有挑战性的领域-类别增量学习(
DCIL
)设置中,训练基于普通图像编码器的模型,如
ResNets

ViTs
,对于增量学习强烈变化的领域和类别并不实用。因此,引入了预训练的视觉-语言模型,因为它们具有强大的零样本迁移能力。
CLIP
包含一个图像编码器
\(f\)
和一个文本编码器
\(g\)
,它们被训练用于生成成对图像-文本样本的紧密对齐特征。在推理时,
\(f\)
首先将输入图像
\(x\)
编码为特征向量
\(f(x)\)
。与此同时,潜在的类名被嵌入到一个模板中,例如“一个{
\(c\)
}的照片”,然后由
\(g\)
编码以形成文本嵌入
\(\{t_j\}_{j=1}^{N_c}\)
。模型的预测通过图像嵌入与所有文本嵌入之间的最大相似性得分来确定
\(s_j = \Braket{f(x), t_j}\)
,其中
\(\Braket{\cdot, \cdot}\)
表示余弦相似度。

  • Task-specific prompt learning

一系列研究开始探索在持续学习中参数高效微调的潜力,常见的做法是为每个任务学习和存储一组轻量级提示,在持续学习阶段形成一个“提示池”,表示为:

\[\begin{equation}
\mathbf{P}=\{P_1, P_2, \cdots, P_N\},\ \ \text{where}\ P_i\in \mathbb{R}^{l\times d},
\end{equation}
\]

其中
\(N\)
是任务编号,
\(l\)

\(d\)
分别是提示的长度和特征嵌入的维度。

在推理时,选择经过良好训练的提示并将附加到预训练的冻结模型上,以恢复学习到的知识。假设
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)

Transformer

\(h\)
的特征嵌入,那么可以将提示添加到
\(\mathbf{x_e}\)
前面,以生成提示输入:

\[\begin{equation}
\mathbf{x_p} = \left[P_s^1; P_s^2; \cdots; P_s^l; \mathbf{x_e}\right] \in \mathbb{R}^{(l+L)\times d},
\end{equation}
\]

其中
\(\{P_s^i\in \mathbb{R}^{d}\}_{i=1}^l\)
是选定提示
\(P_s\)
的嵌入向量,
\(;\)
表示沿着
token
长度维度的连接操作。通过这种植入的知识,生成了更好的图像和文本特征嵌入,并且最终的分类准确率得到了提高。

上述提到的提示选择过程是通过查询-键匹配来实现的。在持续训练阶段,通过最大化余弦相似度或应用聚类算法来学习每个任务的平均特征表示
\(\mathbf{I}=\{I^i\}_{i=1}^N\)
。当测试样本
\(\mathbf{x}\)
到来时,进行键查找操作:

\[\begin{equation}
\label{eq_matching}
I_s = {\arg \max}_{I^i\sim \mathbf{I}}\Braket{f(\mathbf{x}), I^i}.
\end{equation}
\]

通过最相关的键
\(I_s\)
,选择相应的提示
\(P_s\)
并将其附加到冻结模型上,执行推理过程。

Methodology


Interference-free Knowledge Integration

  • Is prepending the best choice?

尽管将提示预先添加到输入
tokens
的方法因其实现简单而被广泛使用,但论文发现它们面临两个方面的问题。

  1. 将提示与输入
    tokens
    进行连接会导致它们在注意力过程中相互作用,从而影响预训练知识的提取。当测试样本来自模型学习提示时的分布时,适应后的模型可以保持相对令人满意的结果。然而,一旦遇到分布发生改变的样本,这种干扰可能导致模型性能下降,并损失其重要的零样本泛化能力,造成前向遗忘问题。
  2. 简单地预先添加提示不可避免地增加了所有
    Transformer
    块的
    token
    长度,这在许多有
    token
    长度限制的场景中并不理想。另外,它的可扩展性有限:较长的提示上下文可能会使文本编码器忽视重要的类别名称,从而导致文本嵌入表示不佳。

上述问题的存在表明,基于提示调优的方法并不满足“残差属性”:期望学习到的参数应该是与冻结主干并行的残差路径,补充新的知识而不影响关键的预训练知识。因此,论文提出了一种无干扰知识整合(
Interference-free Knowledge Integration

IKI
)方案,以最小化噪声的方式将新学习的知识注入到预训练的
VLM
中。

  • IKI mechanism

论文不再为每个任务训练一系列预先添加的提示向量,而是关注自注意力机制的修改,这遵循了自然语言处理领域中广泛使用的参数高效微调方法。回想一下,在
Transformer

\(h\)
中,对输入
tokens
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)
进行的多头自注意力机制。为了简化,省略了多头设计,仅考虑单头情况,这可以自然扩展到多头场景。输入
tokens
首先通过线性投影转换为查询
\(Q\)
、键
\(K\)
和价值
\(V\)
矩阵:

\[\begin{equation}
Q_e = \mathbf{x_e}W^Q + b^Q; K_e = \mathbf{x_e}W^K + b^K; V_e = \mathbf{x_e}W^V + b^V,
\end{equation}
\]

其中
\(W\in \mathbb{R}^{d\times d}\)

\(b\in \mathbb{R}^{d}\)
是预训练参数。然后,执行自注意力计算,通过以下方式生成输出矩阵:

\[\begin{equation}
O_L = \text{Attn}(Q_e, K_e)V_e = \text{softmax}(\frac{Q_eK_e^T}{\sqrt{d}})V_e\ \ \in \mathbb{R}^{L\times d},
\end{equation}
\]

其中
\(\text{softmax}(\mathbf{z})_i = \frac{\exp{(\mathbf{z_i})}}{\sum_j\exp{(\mathbf{z_j})}}\)
可以约束注意力结果中的元素
\(\text{Attn}(Q_e, K_e)\in \mathbb{R}^{L\times L}\)
的总和为一。

普通的提示调优方法将可训练的提示添加到输入
tokens
中,将
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)
扩展为
\(\mathbf{x_p}\in \mathbb{R}^{(l+L)\times d}\)
。然后,将计算
\(Q_{p}K_{p}^T\in \mathbb{R}^{(l+L)\times (l+L)}\)
并传递给
softmax
函数。在
softmax
计算内部,输入
tokens
和提示的注意力分数相互作用并相互影响,导致预训练知识的不可避免损失,如图
2(a)
所示。

为了解决这个问题,论文分别计算输入
tokens
内的自注意力和提示与输入
tokens
之间的交叉注意力,如图
2(b)
所示。换句话说,只训练一个残差注意力分支,保持现有的注意力分数不变。通过新引入的键
\(K_r\)
和值
\(V_r\)
,残差注意力分支的输出可以表示为:

\[\begin{equation}
\label{eq:res_attn}
O_r = \text{softmax}(\frac{Q_eK_r^T}{\sqrt{d}})V_r, \text{where}\ K_r,V_r\in \mathbb{R}^{l\times d}.
\end{equation}
\]

这里,残差输出
\(O_r\in \mathbb{R}^{L\times d}\)
通过与原始输出
\(O_L\)
的正交路径得出,对原始注意力过程没有影响。最后,通过加法将存储在
\(O_r\)
中的学习知识植入输出中。在持续训练阶段,更新可学习的键
\(K_r\)
和值
\(V_r\)
,而不是常用的提示
\(P\)
。请注意,为了保持序列长度不变,没有引入任何查询参数。

理想情况下,一个理想的残差块在未在下游数据集上进行训练之前,应该不会影响原始分支,比如在初始化时。广泛使用的方式用均匀或正态分布初始化提示,这会在没有学习到任何知识的情况下向预训练的
VLMs
中注入随机噪声。具体而言,通过将参数
\(V_r\)
初始化为零,强制残差注意力加法成为一个恒等函数:

\[\begin{equation}
O = O_L+O_r^{\text{init}} = O_L+\text{softmax}(\frac{Q_eK_r^T}{\sqrt{d}})\mathbf{[0]}^{l\times d} = O_L.
\end{equation}
\]

注意,论文仅在开始时将值
\(V_r^{\text{init}}\)
限制为零,同时保持
\(K_r\)
随机初始化。这是因为将
\(K_r\)

\(V_r\)
都初始化为零矩阵会阻止
\(K_r\)
通过梯度更新,从而使
\(V_r\)
陷入到具有相同值的向量中。

由于零初始化更像是一种选择而非技术,一些研究在各种任务中采用了它。然而,这些工作利用零初始化来确保稳定和渐进的训练机制,而在
DCIL
场景中并不存在这一顾虑。论文认为,零初始化对于残差注意力设计是至关重要的,它可以以最小的噪声将新知识注入到预训练的
VLMs
中。

Distribution-aware Integration Calibration

  • Observations

在推理时,会执行公式
3
中描述的查询-键匹配机制,以检索适合当前测试样本的学习提示。这种方法是针对传统的持续学习设置而设计的,仅考虑了向后遗忘。然而,当面对来自未见领域的数据时,这种简单的匹配设计被强制执行,从而为测试样本分配一个相对相似的任务,尽管它们之间存在显著的分布差距。

得益于
IKI
的残差设计,与之前的方法相比,现在可以在这种不匹配的场景中引入更少的噪声。然而,当训练和测试分布之间的差异增加时,模型在某种程度上的性能下降是不可避免的,这会损害
VLMs
在预训练阶段所学到的零样本能力。

ZSCL
通过蒸馏来解决这个问题。他们构建了一个包含来自
ImageNet

100,000
张图像的参考数据集,以在每个训练步骤中将原始
CLIP
的预训练知识蒸馏到当前模型中,明确进行复习以避免遗忘。这种方法可能有效,但它依赖于大规模存储和高计算资源,从而在实际环境中显得不切实际。

一个直观的解决方案是控制知识植入模型的程度。然而,之前基于前置的提示调整技术只有两个选择:要么追加学习到的提示,要么不对原始
CLIP
模型进行任何修改。得益于
IKI
的优雅残差特性,现在可以控制这一并行分支的能力。

  • DIKI: calibrate the integration with distribution

为了确定测试样本属于已学习任务的可能性,为每个任务维护一个特征分布,而不是一个单一的关键向量。在这里,论文简单地应用多元高斯分布,并发现效果良好。形式上,在训练阶段为任务
\(i\)
构建一个
\(\mathcal{N}^i(\mathbf{\mu}^i, \mathbf{\Sigma}^i)\)

\[\begin{equation}
\begin{gathered}
\mathbf{\mu}^i = \mathbb{E}_{\mathbf{x}^i_j \sim D^i}[f(\mathbf{x}^i_j)], \ \ \ \mathbf{\Sigma}^i = \mathbb{E}_{\mathbf{x}^i_j \sim D^i}[(f(\mathbf{x}^i_j)-\mathbf{\mu}^i)^T(f(\mathbf{x}^i_j)-\mathbf{\mu}^i)],
\end{gathered}
\end{equation}
\]

其中
\(f(\mathbf{x}^i_j)\)
是由冻结编码器提取的图像特征。通过这些估计的分布,可以计算每个
\(\mathcal{N}^i\)
中测试样本被抽取的可能性。在这里,计算概率密度的对数作为输入
\(\mathbf{x}\)
在每个学习任务上的评分函数:

\[\begin{equation}
\begin{split}
S^i &= \log \varphi(f(\mathbf{x}); \mathbf{\mu}^i, \mathbf{\Sigma}^i) \\
&= - \frac{1}{2}[ (f(\mathbf{x})-\mathbf{\mu}^i)^T(\mathbf{\Sigma}^i)^{-1}(f(\mathbf{x})-\mathbf{\mu}^i) + d\log 2\pi + \log |\mathbf{\Sigma}^i|) ],
\end{split}
\end{equation}
\]

其中
\(\varphi\)
是概率密度函数。

直观上,得分较高的样本
\(S^i\)
更可能是从任务
\(i\)
中抽取的,并且应该引入参数
\(K_r^i, V_r^i\)
以进行模型预测。此外,还应该考虑到输入样本
\(\mathbf{x}\)
可能来自某些新的分布,如果所有
\(S^i\)
都很低,这一点就得到了暗示。因此,利用最大得分
\(\hat{S}=\max_{i\in [1,N]}S^{i}\)
来加权残余注意力输出:

\[\begin{equation}
\label{eq:final_output}
O = O_L+\mathcal{M}(\hat{S})O_r,
\end{equation}
\]

其中
\(\mathcal{M}\)
是一个映射函数,将得分
\(\hat{S}\)
缩放到范围
\([0,1]\)
。在这里,论文发现简单的
Sigmoid
函数
\(\sigma(x)=\frac{1}{1+e^{-x}}\)
在此效果很好。得益于这种基于分布感知的集成校准机制,
VLMs
的预训练零样本能力可以更好地保留,通过对不熟悉的图像分配较低的权重,进一步解决了前向遗忘的问题。

Experiments




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.