用 Higress AI 网关降低 AI 调用成本 - 阿里云天池云原生编程挑战赛参赛攻略

作者介绍:杨贝宁,爱丁堡大学博士在读,研究方向为向量数据库

《Higress  AI 网关挑战赛》正在火热进行中,Higress 社区邀请了目前位于排行榜 top5 的选手杨贝宁同学分享他的心得。下面是他整理的参赛攻略:

背景

我们要在 Higress 网关中编写 WebAssembly(wasm)插件,使得在 http 请求的各个阶段(requestHeader,requestBody,responseHeader,responseBody)能够将相应的请求或返回捕获进行业务逻辑的处理。具体到本比赛,主要需要实现的是缓存对大模型的请求(openai 接口的形式)在本地(或云数据库),并设计语义级别的缓存命中逻辑来实现降低响应请求且减少 token 费用的目的。

AI Cache 示例

以上图为例,本比赛主要的问题可以归纳为:(1)如何根据 Query 字符串生成合适的 Query 向量 ⇒ 向量生成器选型。(2)如何根据 Query 向量进行语义级别的查找,快速找到合适的缓存向量 ⇒ 缓存命中逻辑设计。(3)如何管理大量的缓存⇒向量数据库选型及重复初始化逻辑。

实际上 Redis 也具备 Vector Store 能力,这里的 Cache Store 和 Vector Store 是可以合并的。不过本 Demo 将二者分开了,Cache Store 使用 Redis,Vector Store 使用阿里云 DashVector 服务。

网关环境搭建

首先我们需要在线上搭建网关环境以供测试和评测使用,本文也提供了本地搭建网关环境的方法供读者参考。注意这里的线上搭建环境是以赛题介绍为基础展开的,若读者已经搭建好线上的开源网关或企业网关可跳过本节。

1.1 搭建线上企业版 Higress 环境**

赛题支持开源 Higress 和企业版 Higress 两种不同的配置,本文以企业版 Higress 为例进行展示。

1.1.1 申请免费试用并创建相应资源

1.1.2 创建服务 (通义千问的地址、Redis 服务、Dashscope 服务)

1.1.3 创建路由转发给通义千问

1.1.4 开启 Higress 插件市场中的 AI Proxy 插件

网关需要 AI Proxy 插件作为处理 AI 请求的支撑,我们可以采用插件市场中已有的 ai-proxy 插件。从源码编译的命令和上次如下所示。最后,配置环节需要提供大模型服务商的 api 和 token key 等,注意比赛需要使用通义千问的 qwen_long 模型。

1.1.5 编译上传 AI Cache 插件* (注意可能需要修改 “ai-cache” 名称防止和插件市场已有插件重复)

本比赛的核心在于自定义 AI Cache 插件以实现更鲁棒的缓存逻辑。首先还是以 Higress 源码提供的基础代码为例进行编译和上传,配置环节需要提供 redis 服务的名称。注意此步骤会在后续迭代代码中反复使用。

git clone https://github.com/alibaba/higress.git
cd higress/plugins/wasm-go
PLUGIN_NAME=ai-cache EXTRA_TAGS=proxy_wasm_version_0_2_100  make build

1.2 本地测试环境搭建和代码更新逻辑

由于线上环境的测试的成本较高,我们也可以采用 Higress + LobeChat 快速搭建私人 GPT 助理 [ 1] 的方式起两个 Docker 容器进行本地测试,参考 dockerfile 如下:

version: '3.9'networks:higress-net:external: falseservices:higress:image: registry.cn-hangzhou.aliyuncs.com/ztygw/aio-redis:1.4.1-rc.1environment:- GATEWAY_COMPONENT_LOG_LEVEL=misc:error,wasm:debug # 重要,开启日志- CONFIG_TEMPLATE=ai-proxy- DEFAULT_AI_SERVICE=qwen- DASHSCOPE_API_KEY= [YOUR_KEY]networks:- higress-netports:- "9080:8080/tcp"- "9001:8001/tcp"volumes:- 本地data目录:/data- 本地log目录:/var/log/higress/ # 重要,方便在容器restrat之后查看日志restart: alwayslobechat:image: lobehub/lobe-chatenvironment:- CODE=123456ed- OPENAI_API_KEY=unused- OPENAI_PROXY_URL=http://higress:8080/v1networks:- higress-netports:- "3210:3210/tcp"restart: always

主要更改了 Higress 的 image,environment 以及 volumes 的配置,启动和重启就是 docker compose up -d docker compose restart。

进一步地,我们需要了解本地代码编写的逻辑如何能反馈到测试环境中。和线上网关环境直接上传编译后的二进制 wasm 插件不同的是,这里需要采用的是:本地编写代码 ⇒ 本地编译 wasm 插件 ⇒ Docker 打包镜像并上传 ⇒ 修改本地测试环境配置中的镜像版本 ⇒ 开始测试并打印日志的流程。具体参考代码如下:

cd ${workspaceFolder}/higress/plugins/wasm-go
PLUGIN_NAME=ai-cache EXTRA_TAGS=proxy_wasm_version_0_2_100 make build 
// 修改版本号(version.txt)
export cur_version=$(cat ${workspaceFolder}/version.txt) && docker build -t [YOUR IMAGE_BASE_URL]:$cur_version -f Dockerfile . && docker push [YOUR_IMAGE_BASE_URL]:$cur_version
// 修改本地测试环境配置中的镜像版本
sudo bash -c \"sed -i 's|oci://registry.cn-hangzhou.aliyuncs.com/XXX:[0-9]*\\\\.[0-9]*\\\\.[0-9]*|oci://registry.cn-hangzhou.aliyuncs.com/XXX:$(cat version.txt)|g' data/wasmplugins/ai-cache-1.0.0.yaml\

文本向量请求逻辑及缓存命中逻辑编写

在本节中,我们将通过一个简单的示例来说明如何在网关中编写请求外部服务的缓存逻辑。

当查询到达时,与 Redis 中存储的键进行匹配(`redisSearchHandler`)。如果完全一致,则直接返回结果(`handleCacheHit`)。
如果不匹配,则请求 `text_embedding` 接口将查询转换为 `query_embedding`(`fetchAndProcessEmbeddings`)。
使用 `query_embedding` 与向量数据库中的向量进行 ANN 搜索,返回最接近的键,并通过阈值进行过滤(`performQueryAndRespond`)。
如果返回结果为空或距离大于阈值,则丢弃结果,本轮缓存未命中,最后将 `query_embedding` 存入向量数据库(`uploadQueryEmbedding`)。
如果距离小于阈值,则再次调用 Redis 对最相似的键进行匹配(`redisSearchHandler`)。
在响应阶段,请求 Redis 新增键值对,键为查询的问题,值为LLM 返回结果。

可以看到,除了 Redis 服务外,我们还需要请求文本向量化服务和向量数据库服务,这里我们分别选取向量生成器:阿里灵积通用文本向量接口 [ 2] 和向量数据库:阿里向量检索服务 DashVector [ 3] 作为服务商。

注意:由于 wasm 插件不支持协程等特性,调用外部服务需遵循:如何在插件中请求外部服务 [ 4]

2.1 外部服务声明和注册

为了实现思路 1-5 的连续外部服务调用。在 Higress 相关的配置上,我们首先需要声明外部服务:

DashVectorClient      wrapper.HttpClient `yaml:"-" json:"-"`
DashScopeClient       wrapper.HttpClient `yaml:"-" json:"-"`
redisClient    wrapper.RedisClient `yaml:"-" json:"-"`

并且在 ParseConfig 函数中注册外部服务:

c.DashVectorInfo.DashVectorClient = wrapper.NewClusterClient(wrapper.DnsCluster{ServiceName: c.DashVectorInfo.DashVectorServiceName,Port:        443,Domain:      c.DashVectorInfo.DashVectorAuthApiEnd,
})
c.DashVectorInfo.DashScopeClient = wrapper.NewClusterClient(wrapper.DnsCluster{ServiceName: c.DashVectorInfo.DashScopeServiceName,Port:        443,Domain:      "dashscope.aliyuncs.com",
})

这里的 ParseConfig 函数是在 http 请求的各个阶段回调函数(requestHeader,requestBody,responseHeader,responseBody)之前的注册函数。

2.2 AI Cache 配置文件

在增加了上述外部服务的基础上,对应的 AI Cache 的配置文件也需要进行修改,此处对应 1.1.5 节的配置。示例配置如下:

Dash:dashScopeKey: "YOUR_DASHSCOPE_KEY" # 这个是文本向量的keydashScopeServiceName: "qwen" # 重要,需要和scope对应的服务名匹配dashVectorCollection: "YOUR_CLUSTER_NAME"dashVectorEnd: "YOUR_VECTOR_END" dashVectorKey: "YOUR_DASHVECTOR_KEY" # 这个是DASHVECTOR的keydashVectorServiceName: "DashVector.dns" # 重要,需要新建一个vector对应的DNS服务 sessionID: "XXX" # 可用可不用,主要用于重复初始化逻辑
redis: # 重要serviceName: "redis.static"timeout: 2000

2.3 连续 callback 实现连续服务调用

基于上述思路,实现的核心代码如下。其中的核心难点仍在于如何实现服务间的连续调用问题,以 onHttpRequestBody 函数为例,代码需要实现并发逻辑,而不是简单的顺序逻辑。因此,主函数代码必须返回 types.Action,即声明是阻塞还是继续执行。当前逻辑要求在处理完缓存命中逻辑后才能继续操作,因此主函数需要返回 types.Pause。最后,根据我们的处理逻辑,在调用外部服务的回调函数中,根据是否命中缓存执行 proxywasm.ResumeHttpRequest() 或直接返回 proxywasm.SendHttpResponse()。

// ===================== 以下是主要逻辑 =====================
// 主handler函数,根据key从redis中获取value ,如果不命中,则首先调用文本向量化接口向量化query,然后调用向量搜索接口搜索最相似的出现过的key,最后再次调用redis获取结果
// 可以把所有handler单独提取为文件,这里为了方便读者复制就和主逻辑放在一个文件中了
// 
// 1. query 进来和 redis 中存的 key 匹配 (redisSearchHandler) ,若完全一致则直接返回 (handleCacheHit)
// 2. 否则请求 text_embdding 接口将 query 转换为 query_embedding (fetchAndProcessEmbeddings)
// 3. 用 query_embedding 和向量数据库中的向量做 ANN search,返回最接近的 key ,并用阈值过滤 (performQueryAndRespond)
// 4. 若返回结果为空或大于阈值,舍去,本轮 cache 未命中, 最后将 query_embedding 存入向量数据库 (uploadQueryEmbedding)
// 5. 若小于阈值,则再次调用 redis对 most similar key 做匹配。(redisSearchHandler)
// 7. 在 response 阶段请求 redis 新增key/LLM返回结果func redisSearchHandler(key string, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, stream bool, ifUseEmbedding bool) error {err := config.redisClient.Get(config.CacheKeyPrefix+key, func(response resp.Value) {if err := response.Error(); err == nil && !response.IsNull() {log.Warnf("cache hit, key:%s", key)handleCacheHit(key, response, stream, ctx, config, log)} else {log.Warnf("cache miss, key:%s", key)if ifUseEmbedding {handleCacheMiss(key, err, response, ctx, config, log, key, stream)} else {proxywasm.ResumeHttpRequest()return}}})return err
}// 简单处理缓存命中的情况, 从redis中获取到value后,直接返回
func handleCacheHit(key string, response resp.Value, stream bool, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) {log.Warnf("cache hit, key:%s", key)ctx.SetContext(CacheKeyContextKey, nil)if !stream {proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)} else {proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)}
}// 处理缓存未命中的情况,调用fetchAndProcessEmbeddings函数向量化query
func handleCacheMiss(key string, err error, response resp.Value, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, queryString string, stream bool) {if err != nil {log.Warnf("redis get key:%s failed, err:%v", key, err)}if response.IsNull() {log.Warnf("cache miss, key:%s", key)}fetchAndProcessEmbeddings(key, ctx, config, log, queryString, stream)
}// 调用文本向量化接口向量化query, 向量化成功后调用processFetchedEmbeddings函数处理向量化结果
func fetchAndProcessEmbeddings(key string, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, queryString string, stream bool) {Emb_url, Emb_requestBody, Emb_headers := ConstructTextEmbeddingParameters(&config, log, []string{queryString})config.DashVectorInfo.DashScopeClient.Post(Emb_url,Emb_headers,Emb_requestBody,func(statusCode int, responseHeaders http.Header, responseBody []byte) {// log.Infof("statusCode:%d, responseBody:%s", statusCode, string(responseBody))log.Infof("Successfully fetched embeddings for key: %s", key)if statusCode != 200 {log.Errorf("Failed to fetch embeddings, statusCode: %d, responseBody: %s", statusCode, string(responseBody))ctx.SetContext(QueryEmbeddingKey, nil)proxywasm.ResumeHttpRequest()} else {processFetchedEmbeddings(key, responseBody, ctx, config, log, stream)}},10000)
}// 先将向量化的结果存入上下文ctx变量,其次发起向量搜索请求
func processFetchedEmbeddings(key string, responseBody []byte, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, stream bool) {text_embedding_raw, _ := ParseTextEmbedding(responseBody)text_embedding := text_embedding_raw.Output.Embeddings[0].Embedding// ctx.SetContext(CacheKeyContextKey, text_embedding)ctx.SetContext(QueryEmbeddingKey, text_embedding)ctx.SetContext(CacheKeyContextKey, key)performQueryAndRespond(key, text_embedding, ctx, config, log, stream)
}// 调用向量搜索接口搜索最相似的key,搜索成功后调用redisSearchHandler函数获取最相似的key的结果
func performQueryAndRespond(key string, text_embedding []float64, ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, stream bool) {vector_url, vector_request, vector_headers, err := ConstructEmbeddingQueryParameters(config, text_embedding)if err != nil {log.Errorf("Failed to perform query, err: %v", err)proxywasm.ResumeHttpRequest()return}config.DashVectorInfo.DashVectorClient.Post(vector_url,vector_headers,vector_request,func(statusCode int, responseHeaders http.Header, responseBody []byte) {log.Infof("statusCode:%d, responseBody:%s", statusCode, string(responseBody))query_resp, err_query := ParseQueryResponse(responseBody)if err_query != nil {log.Errorf("Failed to parse response: %v", err)proxywasm.ResumeHttpRequest()return}if len(query_resp.Output) < 1 {log.Warnf("query response is empty")uploadQueryEmbedding(ctx, config, log, key, text_embedding)return}most_similar_key := query_resp.Output[0].Fields["query"].(string)log.Infof("most similar key:%s", most_similar_key)most_similar_score := query_resp.Output[0].Scoreif most_similar_score < 0.1 {ctx.SetContext(CacheKeyContextKey, nil)redisSearchHandler(most_similar_key, ctx, config, log, stream, false)} else {log.Infof("the most similar key's score is too high, key:%s, score:%f", most_similar_key, most_similar_score)uploadQueryEmbedding(ctx, config, log, key, text_embedding)proxywasm.ResumeHttpRequest()return}},100000)
}// 未命中cache,则将新的query embedding和对应的key存入向量数据库
func uploadQueryEmbedding(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log, key string, text_embedding []float64) error {vector_url, vector_body, err := ConsturctEmbeddingInsertParameters(&config, log, text_embedding, key)if err != nil {log.Errorf("Failed to construct embedding insert parameters: %v", err)proxywasm.ResumeHttpRequest()return nil}err = config.DashVectorInfo.DashVectorClient.Post(vector_url,[][2]string{{"Content-Type", "application/json"},{"dashvector-auth-token", config.DashVectorInfo.DashVectorKey},},vector_body,func(statusCode int, responseHeaders http.Header, responseBody []byte) {if statusCode != 200 {log.Errorf("Failed to upload query embedding: %s", responseBody)} else {log.Infof("Successfully uploaded query embedding for key: %s", key)}proxywasm.ResumeHttpRequest()},10000,)if err != nil {log.Errorf("Failed to upload query embedding: %v", err)proxywasm.ResumeHttpRequest()return nil}return nil
}// ===================== 以上是主要逻辑 =====================

此外,该逻辑只能在返回值为 types.Action 的函数中使用,例如 onHttpResponseBody 这样的流式处理函数无法以类似方式处理。尽管可以确保请求被发送出去,但由于没有阻塞操作,无法调用回调函数。如果有需要,可以参考 wasm-go/pkg/wrapper/http_wrapper.go,添加信号变量进行修改。

总结

本文的完整代码已发布在 GitHub [ 5] 。本文提供的思路仅为抛砖引玉,如何在实际场景中解决复杂的缓存需求仍需各位的智慧。祝大家在比赛中取得理想的成绩!

相关链接:

[1] Higress + LobeChat 快速搭建私人 GPT 助理

https://github.com/alibaba/higress/issues/1023

[2] 向量生成器:阿里灵积通用文本向量接口

https://help.aliyun.com/zh/dashscope/developer-reference/text-embedding-quick-start

[3] 向量数据库:阿里向量检索服务 DashVector

https://www.aliyun.com/product/ai/dashvector

[4] 如何在插件中请求外部服务

https://higress.io/docs/latest/user/wasm-go/

[5] GitHub

https://github.com/Suchun-sv/ai-cache-Demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/878412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Serilog文档翻译系列(三) - 基础配置

Serilog 使用简单的 C# API 来配置日志记录。当需要外部配置时&#xff0c;可以&#xff08;慎用&#xff09;通过使用 Serilog.Settings.AppSettings 包或 Serilog.Settings.Configuration 包进行混合配置。 创建日志记录器 日志记录器是通过 LoggerConfiguration 对象创建的…

【RabbitMQ】快速上手

目 录 一. RabbitMQ 安装二. RabbitMQ 核心概念2.1 Producer 和 Consumer2.2 Connection 和 Channel2.3 Virtual host2.4 Queue2.5 Exchange2.6 RabbitMQ 工作流程 三. AMQP四. web界面操作4.1 用户相关操作4.2 虚拟主机相关操作 五. RabbitMQ 快速入门5.1 引入依赖5.2 编写生产…

stm32 8080时序驱动lcd屏幕

PSAM使用的硬件接口 PSAM读时序 PSAM写时序 相关时序 PSAM_RCRx NOR 和PSRAM控制寄存器

28. 双耳配对 - 配置

1. 概述 通过MAC地址的最后一位的奇偶来判断左右耳 2. 验证 右耳:奇数(主耳)-》BT ADDR: 12:42:22:34:34:6d 左耳:偶数(从耳)-》BT ADDR: 12:42:22:34:34:6c

TPH-YOLOv5:基于Transformer预测头的改进YOLOv5,用于无人机捕获场景的目标检测

摘要 提出了TPH-YOLOv5。在YOLOv5的基础上&#xff0c;增加了一个预测头来检测不同尺度的目标。然后用Transformer Prediction Heads&#xff08;TPH&#xff09;代替原有的预测头&#xff0c;探索自注意机制的预测潜力。还集成了卷积块注意力模型&#xff08;CBAM&#xff09;…

Gland安装与Debug

下载地址&#xff1a;https://www.jetbrains.com.cn/go/download/#sectionwindows debug官方文档: https://www.jetbrains.com/help/go/debugging-code.html 创建项目 选择新建项目 填写项目本地路径&#xff0c;以及选择go SDK 项目创建后检查项目设置 添加main包以及…

安装MySQL,navicat以及Django配置遇到的一些问题

MySQL安装问题 安装MySQL按照了此文章&#xff1a; MySQL数据库下载及安装教程&#xff08;最最新版&#xff09;_mysql下载安装-CSDN博客https://blog.csdn.net/weixin_39289696/article/details/128850498首先是遇到了starting the server红色叉号显示 按照上面文章的介绍…

Linux--IO多路复用(select,poll,epoll)

IO多路复用——select&#xff0c;poll&#xff0c;epoll IO多路复用是一种操作系统技术&#xff0c;旨在提高系统处理多个输入输出操作的性能和资源利用率。与传统的多线程或多进程模型相比&#xff0c;IO多路复用避免了因阻塞IO而导致的资源浪费和低效率问题。它通过将多个IO…

Linux awk案例

目录 1. 查询时间超过2000毫秒的请求2. 查询指定列组合出现的次数3. 统计所有文件的大小4. 获取大于指定大小的文件名&#xff0c;并按照从大到小排序5. grep指定字段后&#xff0c;使用awk列转行6. 查询第四个字段等于指定值的内容 1. 查询时间超过2000毫秒的请求 ✅log: 202…

[Leetcode 216][Medium]组合总和 III--回溯

目录 一、题目描述 二、整体思路 三、代码 一、题目描述 原题地址 二、整体思路 对于组合问题&#xff0c;首先要想到回溯法。那么可以根据回溯法模版进行设计。 void backtrace(元素){if(满足题目要求的条件){保存目前路径/状态/结果;return;}for循环,往目前状态相邻的所…

区块链通证系统功能分析

区块链通证系统功能分析涉及多个关键方面&#xff0c;以确保系统能够满足不同的业务需求和合规性要求。 同质与非同质通证&#xff1a;区块链通证系统需要支持同质通证&#xff08;如ERC-20&#xff09;和非同质通证&#xff08;如ERC-721&#xff09;&#xff0c;以适应不同类…

《NLP自然语言处理》—— 关键字提取之TF-IDF算法

文章目录 一、TF-IDF算法介绍二、举例说明三、示例&#xff1a;代码实现四、总结 一、TF-IDF算法介绍 TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;是一种用于信息检索与文本挖掘的常用加权技术。TF-IDF是一种统计方法&#xff0c;用以评估一个词…

机器人大会引领产业动向,卓翼飞思绘制无人系统教科研新蓝图

8月21日&#xff0c;万众瞩目的2024世界机器人大会暨博览会在北京亦创国际会展中心盛大开幕。这场为期5天&#xff0c;集“展览”“论坛”“赛事”于一体的机器人盛会&#xff0c;反映了当下机器人领域的繁荣生态。据官方统计数据&#xff0c;今年现场逛展观众高达25万人次&…

揭秘!糖尿病:从绝望到希望的治愈之路

在这个快节奏、高压力的时代&#xff0c;糖尿病这一“甜蜜的负担”正悄然影响着越来越多人的生活。面对这一全球性的健康挑战&#xff0c;许多患者心中都萦绕着一个共同的疑问&#xff1a;“糖尿病&#xff0c;真的能治好吗&#xff1f;”今天&#xff0c;就让我们一起揭开糖尿…

《黑神话:悟空》:30%抽成真相

《黑神话&#xff1a;悟空》自建服务器出售&#xff1f;揭秘游戏界的30%抽成真相&#xff01; 近年来&#xff0c;随着游戏行业的迅猛发展&#xff0c;游戏开发商与发行平台之间的利益分配问题逐渐成为业界关注的焦点。其中&#xff0c;《黑神话&#xff1a;悟空》作为一款备受…

排序算法:

冒泡排序&#xff1a; 从列表的第一个数字开始进行比较&#xff0c;判断该数和下一个数之间的大小关系&#xff0c;如果该数比右边的数大&#xff0c;则交换位置&#xff1b;否则不变。一般一轮可以确定最大的数字&#xff0c;在列表的最后一位。 代码&#xff1a; 注意&…

【设计模式】创建型模式——简单工厂模式

文章目录 一、创建型模式1. 包含模式1.1 工厂模式1.2 建造者模式1.3 原型模式1.4 单例模式 二、工厂模式1. 概要1.1 意图1.2 主要解决问题1.3 何时使用1.4 如何解决1.5 关键代码1.6 使用场景1.7 优点1.8 缺点1.9 简单工厂实现 2. 简单工厂模式2.1 模式结构2.2 实例2.3 模式定义…

python实战一:合并多个Excel中数据

看不懂可以跟着视频学一下&#xff0c;关于基础课程项目也可以私聊我&#xff01; 视频源码&#xff1a; 链接&#xff1a;https://pan.quark.cn/s/2055653f735b 之前我们已经学习了如何读取和写入Execl数据&#xff0c;今天我们来用一个实例来进行表格的合并。如下是 2021年…

YOLOv9改进策略【模型轻量化】| ShufflenetV2,通过通道划分构建高效网络

一、本文介绍 本文记录的是基于ShufflenetV2的YOLOv9目标检测轻量化改进方法研究。FLOPs是评价模型复杂独的重要指标&#xff0c;但其无法考虑到模型的内存访问成本和并行度&#xff0c;因此本文在YOLOv9的基础上引入ShufflenetV2&#xff0c;使其在在保持准确性的同时提高模型…

多线程篇(基本认识 - 锁优化)(持续更新迭代)

目录 一、前言 二、阿里开发手册 三、synchronized 锁优化的背景 四、Synchronized的性能变化 1. Java5之前&#xff1a;用户态和内核态之间的切换 2. java6开始&#xff1a;优化Synchronized 五、锁升级 1. 无锁 2. 偏向锁 2.1. 前言 2.2. 什么是偏向锁 2.3. 偏向…