TVM模块序列化全解析:从export_library到load_module的部署实战

模块序列化这件事,放在TVM整个链路里看起来不起眼,但实际部署时几乎所有跨环境、跨设备的场景都会被它卡住。编译出来的TVM模块,不管是通过Relay构建出来的可执行库,还是AutoTVM/Ansor调优后落地的 artifact,最终都要变成一个能在另一台机器上“取回来就能用”的文件,这个过程就是模块序列化。这篇文章会把tvm.runtime.Module的导出、加载、参数处理、调优后打包等一整套流程拆开讲,同时把我实际踩过的坑和排查思路一起写进去,适合正在做TVM部署、模型转换、边缘端落地,或者只是想搞清楚export_library和load_module到底在做什么的读者。

1. 为什么要把模块序列化看得这么重

1.1 模块离开内存后必须有一张“落盘通行证”

TVM中tvm.relay.build编译出来的东西,本质上不是一个单纯的权重文件,也不是一个ONNX模型那样的静态描述,而是一段可执行的运行时代码、调度逻辑、常量数据、权重参数、图结构元数据混在一起的复合体。我们在这个复合体上调用lib["default"]拿到某个函数,再套上graph_executor或vm来跑推理,整个过程都在内存里完成。如果只是在同一个Python进程里玩,那确实不需要序列化。但真正的部署场景不会这么温柔:要么这台机器编译完,下台机器是纯推理环境;要么同一个模块要在多个容器、多台设备、多个进程里同时加载;要么我们希望把编译产物当作缓存,跳过重复编译的时间开销。

这几类诉求,指向同一个能力:把所有跟这个模块相关的运行信息,打包成某种能落盘的格式,并让别的环境能够原样恢复。这就是模块序列化要做的事。在TVM中,它对应两个最基本的API:module.export_library(path)把模块写到文件里,tvm.runtime.load_module(path)再从文件里恢复出模块对象。理解了这两个API背后的文件内容、依赖关系、参数存储策略,基本就掌握了TVM部署最关键的一环。

1.2 模块里到底装了什么东西

我当时第一次导出模块后,好奇地翻看生成的文件内容,发现里面远不止一个“模型”这么简单。一个典型的经过序列化后的模块,至少包含以下几个层次的内容:

第一层是编译器生成的机器码或者底层IR代码。CPU后端常见的是LLVM生成的native code,GPU后端则可能是CUDA source、PTX 汇编,或者是 OpenCL 的 kernel 源码。这部分是模型真正跑起来时被调用的计算内核。

第二层是图的执行结构。Relay 构建出的GraphExecutor或VM执行器,需要一个描述计算图拓扑、算子顺序、IO接口的元数据,才能把各个kernel串起来。这个信息通常以JSON或二进制格式存放在模块中。

第三层是权重参数和常量。如果编译时传入了params,这些参数会被固化到模块的常量区里,甚至直接内嵌到生成的库文件中;如果把权重单独管理,则它们可以被拆出来放到独立的参数文件里。

第四层是系统库依赖信息。TVM生成的代码不是完全自包含的,它可能依赖TVM Runtime自身的函数符号,比如内存分配、算子注册、设备管理。因此导出的文件经常需要连同TVM Runtime库一起加载,或者通过system_lib的方式绑定到已有运行时进程上。

理解这四层内容,再看序列化API的设计就会清楚很多:export_library不是简单地把内存对象copy到磁盘,而是要协调这几类内容,生成一个在目标环境里依然能被TVM Runtime识别和装载的产物。

1.3 模块序列化不等于“保存模型”

刚接触TVM的人,很容易把export_library和“保存模型”混在一起。ONNX、PyTorch 里的torch.save,保存的是网络结构描述和参数;TVM模块序列化保存的则是一份“已经编好码、随时可以执行的程序”。可以这样类比:前两类保存的是“菜谱+原料”,TVM序列化保存的则是“已经做好的半成品菜”,稍微加热就能端上桌。

这也解释了为什么有些情况下序列化出来的文件特别大——因为机器码、kernel源码、图描述都被打进去了。也解释了为什么版本敏感度比普通模型保存高很多:一个小版本升级,运行时库的函数签名变了,载入旧模块可能立刻出现符号找不到、解释失败等错误。不是TVM故意折腾人,而是因为它保存的东西确实更接近“经过编译的程序”而不是“数据”。意识到这一点,再去看各种部署问题,思路就会顺很多。

2. 动手前必须搞清楚的三件事

2.1 编译出口统一是 Module:不同路径下怎么拿到它

不管用户是从Relay前端进来,还是用旧式的tvm.relay.build_module,还是直接手工构造一个Module,最后得到的都是一个tvm.runtime.Module对象。这个对象是序列化操作的主体。比较常见的几种拿到方式:

  • 使用tvm.relay.build(mod, target=target, params=params),返回的对象内部包含可执行库,通常配合graph_executor.GraphModule(lib["default"](dev))使用。
  • 使用tvm.compile配合tvm.relay.build_module.create_executor,拿到的是更抽象的GraphExecutor或VMExecutor,底层依赖的库模块同样可以序列化。
  • 使用AutoTVM或Ansor调优脚本时,调优后的lib同样是通过relay.build返回,后续对它执行export_library就能把调优结果固化下来。

关键点在于,不管前面用的是什么高级接口,最终我们操作的序列化对象都是Module。拿到Module后,先确认它里面有哪些子模块、入口函数叫什么名字。常见做法是在交互环境里打印lib.get_function("list_global_func_names")之类接口的返回结果,观察里面是否有"default"、"__tvm_main__"等关键函数名。这一步虽然不起眼,但能避免后面加载时找错函数名。

2.2 export_library 是主入口:两种输出形态

Module.export_library()的典型用法是这样:

lib.export_library("deploy_lib.so")

这在绝大多数CPU部署场景下就够了。输出文件是一个动态链接库,运行时文件大小取决于模型和优化等级。

另一种形态是打包文件,常见于调优后的卷积库、算子库,或者目标设备需要配套源文件的场景。此时会用fcompile参数指定打包函数:

from tvm.contrib import utils lib.export_library("deploy_lib.tar", fcompile=utils.tar.tar)

生成的是一个tar包,里面既有编译好的动态库,也可能包含配套的源文件、头文件和额外库。用tvm.runtime.load_module加载tar包时,TVM会先在临时目录解包,然后定位到真正的动态库并加载。这种形态对部署稍微复杂,但好处是模型的所有依赖可以被锁在一个包里,不会出现“动态库在,旁边的某个源文件却丢了”的尴尬情况。

选择哪种形态,核心看目标环境。如果目标机器能方便安装TVM Runtime且对文件管理有严格控制,单.so文件最简洁。如果要分发到多个异构设备,或者可能需要生成带源码的库,tar包更稳妥。

2.3 参数到底放哪儿:内嵌权重与外置权重

这是序列化操作里需要提前做决策的一点。打开TVM示例代码,常看到编译时直接传参:

lib = tvm.relay.build(mod, target=target, params=params)

传进去的params会被当作常量折叠进图里,并在export_library时一并写入产物。这种方式最简单,加载后直接使用,不需要再额外读取参数文件。缺点是灵活性差:模型权重一变,整个库必须重新编译、重新导出。

另一种做法是编译时不传params,或者把参数从模块中提取出来,单独保存:

params = lib.get_params() with open("deploy.params", "wb") as f: f.write(tvm.runtime.save_param_dict(params))

需要加载时再把参数填回去:

loaded_params = tvm.runtime.load_param_dict(open("deploy.params", "rb").read()) gmod.set_input(**loaded_params)

我实际更推荐生产环境采用外置权重策略,尤其是模型需要频繁更新、多个模型共享同一套图结构、或者有热更新需求的时候。因为图结构和算子库一旦编译好,基本是稳定的,真正经常变的是权重。把权重拆出来,可以大幅减少重新编译的频率,也让部署包更小。

3. 串起来走一遍:导出、加载、运行的完整实操

3.1 先跑通最小闭环:CPU模型一轮导出加载

为了不纸上谈兵,下面用一段最小代码演示从编译到导出的完整过程。我用的是Relay自带的一个测试网络作为示例,实际项目替换成自己的mod和params即可。

import numpy as np import tvm from tvm.relay import testing from tvm.contrib import graph_executor # 1. 构造一个简单的网络 mod, params = testing.mlp.get_workload(1000) # 2. 编译到本机CPU target = tvm.target.Target("llvm -mcpu=core-avx2") with tvm.transform.PassContext(opt_level=3): lib = tvm.relay.build(mod, target=target, params=params) # 3. 导出模块 lib.export_library("mlp_deploy.so") print("导出完成")

这段代码执行后,当前目录会多出一个mlp_deploy.so。导出时TVM把编译生成的机器码、图执行器的元数据以及内嵌的权重打包进了动态库。接着用另一个干净的Python进程去加载并推理:

import numpy as np import tvm from tvm.contrib import graph_executor # 加载模块 loaded_lib = tvm.runtime.load_module("mlp_deploy.so") dev = tvm.cpu() gmod = graph_executor.GraphModule(loaded_lib["default"](dev)) # 构造输入并执行 data = tvm.nd.array(np.random.rand(1, 1, 1000).astype("float32")) gmod.run(data=data) out = gmod.get_output(0).numpy() print(out.shape)

这里最需要注意的是loaded_lib["default"]这个索引。tvm.relay.build导出的库入口函数默认叫"default",但如果用户手动设置过入口名,或者用的是VM执行器,就可能变成别的名字。调试的第一步永远是把loaded_lib.get_function("list_global_func_names")打出来看看,而不是想当然用"default"。

3.2 把参数单独拿出来:适配热更新与多模型共享权重

如果希望反复导出同一个网络的不同权重版本,不再每次把权重编进库文件,可以改成下面的方式。

编译阶段不传权重,或者先编译一次完整模型,再把权重取出来单独保存:

import tvm import numpy as np from tvm.relay import testing from tvm.contrib import graph_executor mod, params = testing.mlp.get_workload(1000) target = tvm.target.Target("llvm") with tvm.transform.PassContext(opt_level=3): lib = tvm.relay.build(mod, target=target) # 故意不传 params # 导出无权重版本 lib.export_library("mlp_noweight.so")

这样导出的库文件体积会小很多。然后在需要推理时,配合外置参数文件使用:

# 保存参数 with open("mlp.params", "wb") as f: f.write(tvm.runtime.save_param_dict(params)) # 另一进程加载 loaded_lib = tvm.runtime.load_module("mlp_noweight.so") gmod = graph_executor.GraphModule(loaded_lib["default"](tvm.cpu())) from tvm.runtime import load_param_dict with open("mlp.params", "rb") as f: loaded_params = load_param_dict(f.read()) gmod.set_input(**loaded_params) data = tvm.nd.array(np.random.rand(1, 1, 1000).astype("float32")) gmod.run(data=data) out = gmod.get_output(0).numpy()

这里参数保存用的是save_param_dict,它会把字典里的所有NDArray序列化成一段二进制。加载的时候必须注意set_input的时机:要在run之前把参数设进去,否则推理用的就是未初始化或者默认的权重。我自己曾犯过一个低级错误,加载完模块忘记set_input,结果输出全是随机值或者NaN,排查了半天才发现是参数没填上。

3.3 打包成多文件tarball:调优结果的序列化

做算子调优或AutoTVM时,export_library直接导出一个.so往往不够,因为调优结果的呈现方式会复杂一些。我在实际项目中跑完调优后,通常这么做:

import tvm from tvm.contrib import utils from tvm.relay import testing mod, params = testing.mlp.get_workload(1000) target = tvm.target.Target("llvm") with tvm.transform.PassContext(opt_level=4): lib = tvm.relay.build(mod, target=target, params=params) # 导出成tar包,而不是单so lib.export_library("mlp_tuned.tar", fcompile=utils.tar.tar)

加载tar包的方式和加载so基本一致:

loaded_lib = tvm.runtime.load_module("mlp_tuned.tar")

TVM Runtime会自动解开tar包并定位库文件。这里有一个容易忽略的点:tar包里如果同时存在多个设备对应的库,加载时Runtime会根据当前运行时的设备上下文选择正确的库。比如你为llvm和cuda分别编译了不同的子模块,并打包进同一个tar,那在CPU机器上会加载CPU版本,在GPU机器上会加载GPU版本。这种设计在需要一套部署包覆盖多种硬件的场景下非常好用。

不过也要留意,tar包虽然装着多个子模块,但export_library并不会自动帮你把不同target的编译结果合并。你需要自己做好子模块的收集和拼装。序列化API只是负责把当前Module对象和它的子Module按格式落盘,不会帮你解决多target编排的问题。

3.4 从加载到推理前的校验:别急着上线

加载一个模块并成功run一次,不等于模块没问题。我在实际部署流程里,至少会有三个校验步骤:

第一步,检查模块加载后函数列表是否完整。这一步能快速发现入口名错误、缺少算子库、注册函数缺失等问题。我会直接打印:

funcs = loaded_lib.get_function("list_global_func_names") print(funcs())

如果缺少目标平台特有的自定义算子函数,多半是编译时没有正确链接自定义算子注册表,需要回到编译阶段检查。

第二步,用固定输入跑一遍,和原始编译环境的结果做一次数值对比。因为浮点计算在不同指令集上的优化可能改变舍入顺序,结果会有细微差异,但如果误差超过可接受范围,就要检查是否加载了错误的权重、是否图结构改变。

第三步,检查模块是否包含未预期的大文件。序列化后的.so或.tar文件体积异常增加,最常见原因是把权重和调试信息也打进去了。这时可以用体积分析和模块内部对象检查来定位。

4. 常见陷坑与排障:一份实用的排查清单

4.1 加载时版本不匹配:最经典的拦路虎

报错形式很多,有的直接抛ValueError: Cannot find module,有的在加载动态库时出现符号找不到的链接错误,还有的是graph executor解析失败。这几类问题的根源基本都可以归到版本不匹配。

TVM编译器后续版本对内部结构做了很多调整,序列化文件中记录的版本信息和运行时库期望的版本信息不一致时,加载就会失败。尤其是使用打包生成的system_lib或需要和底层的libtvm_runtime.so配合时,编译器版本、运行时版本、前端Python包版本,三者必须保持同源。

我排查这个问题时的常规操作是:

  • 在编译环境里执行tvm.__version__,记录精确版本。
  • 在加载环境里同样执行tvm.__version__做对比。
  • 检查导出文件中嵌入的版本元数据,或者用strings命令查看动态库里的版本标识。

最稳妥的方案是部署侧直接使用和编译侧完全一致的TVM版本,或者至少保持大版本一致,同时在导出时使用上游版本自己的API,避免混用修改过的分支代码。

4.2 加载时找不到动态库依赖

很多时候序列化的.so文件本身在,但load_module报错,提示依赖库找不到。这个依赖不是模型依赖,而是TVM运行时的动态链接库依赖。比如说目标机器上没装libtvm_runtime.so,或者模型内嵌代码依赖了某个LLVM运行时库而系统路径里没有。

解决办法通常有几个方向。第一,优先把TVM Runtime以system_lib方式编进部署进程,这样序列化后的库只需要依赖最小运行时。第二,确保目标机器上LD_LIBRARY_PATH包含了TVM安装目录下的lib路径。第三,导出时手动指定需要一同分发的依赖库,用export_library的额外参数传入,让它们跟着主库一起打包。

我在Linux环境里的一个习惯是,在加载前先用ldd查看导出的.so:

ldd mlp_deploy.so

如果输出列表里有“not found”的项,就能非常迅速地锁定缺失的动态库,比直接调load_module看报错高效很多。

4.3 权重数量对不上:图结构没变但推理结果全错

这类问题最隐蔽。模块能加载、能run,但输出结果完全不对。最常见原因是:编译时传入的params和外置加载时传入的params不是同一套。例如把A模型编译成图结构,却把B模型的权重通过set_input填进去,参数名字或者形状对不上时,graph executor可能不会报错,但推理结果一定错误。

排查顺序一般是:

  1. 对比编译时params的键列表和加载时params的键列表。
  2. 检查每个张量形状是否和get_input_info返回的一致。
  3. 检查数据精度是否一致,float32和float64混用会造成精度漂移。

还有一个容易踩的坑:set_input传参时如果直接把dict传进去,要和set_input(**params)区分开。前者在某些版本里被解释为设置了单个输入,后者才是把整个参数字典展开到对应输入项。写代码时要仔细。

4.4 跨目标平台加载:很常见的部署盲区

不少项目是在x86开发机上完成编译和序列化,然后把产物部署到ARM设备或GPU设备。CPU的llvm产物拿到ARM上当然加载不了,这个谁都知道。但GPU场景有个更隐蔽的问题:模型里可能同时包含GPU算子和CPU算子,即使主target是cuda,仍有部分算子被回退到CPU。如果你只导出了GPU版本,部署时就没法自由回退;如果你同时打包了两部分,一定要确认运行时能找到正确设备上下文。

我建议在序列化前,先明确目标设备列表。如果目标是单一硬件,直接用对应的target编译并导出。如果目标环境不确定,就不要把全部希望寄托在单个.so上,尽量用多target组合、tar包分发。序列化只是最后一步,真正的问题在编译期的target规划上就已经定了。

4.5 一个容易忽略的调试技巧:直接把模块加载到当前进程检查

在排查序列化问题时,尽量直接在目标机器的Python交互环境里做最小加载实验,不要放进完整业务代码里。这样能把“业务逻辑问题”和“序列化/加载问题”快速隔离。

import tvm, numpy as np for name in ["mlp_deploy.so", "mlp_deploy.tar"]: try: lib = tvm.runtime.load_module(name) print(f"{name} loaded, funcs: {lib.get_function('list_global_func_names')()}") except Exception as e: print(f"{name} failed: {e}")

这个几行的脚本帮我节省了大量时间。如果这个脚本能过,说明序列化链路没问题,问题在业务侧;如果连这个都过不了,再去追版本、依赖库、打包内容。成功的调试永远是先把问题范围缩到最小。

5. 经验收尾,序列化实践的几条心得

完整走一遍模块序列化流程之后,我个人的感觉是:这个看似简单的API背后,其实隐藏着部署方案的分水岭。选内嵌权重还是外置权重,选单so还是tar包,选统一版本还是多target打包,每个决策都会直接影响产物的可维护性。

在我做过的几个内部项目里,最终稳定运行的方案几乎都是同一个套路:编译阶段把参数拿出来单独管理,导出阶段采用tar包或so和参数文件分离的方式,加载阶段增加一次函数列表和参数键的自动校验。这种方案牺牲了一点点“拿到就能跑”的便利,换来了更新权重不需要重新编译、部署包可以复用、出错时更容易定位问题的长期收益。如果你的项目还在早期,我建议一上来就按这个套路走,不要先做最简方案,后面再返工改参数外置策略,返工成本远高于现在多写的几行代码。

最后再分享一个小技巧:每次导出模块后,顺手写一个记录脚本,把TVM版本、目标参数、导出时间、网络名称、参数文件哈希这些信息一并存成一个manifest文本文件。这个文件本身不参与加载,但在排查线上问题时,它能让你几秒钟就判断出部署环境跑的是不是正确的产物,而不是对着一个无名的.so反复猜测。序列化只是起点,能被人理解和追溯的部署产物,才是真正能上线的部署产物。

相关新闻

从零搭建轻量级地图可视化组件:核心原理与工程实践

从零搭建轻量级地图可视化组件:核心原理与工程实践

1. 从零搭建 lingbot-map:一个轻量级地图可视化组件的完整实践第一次看到lingbot-map这个命名,我的直觉是:这大概率是一个围绕地图渲染与交互的轻量级前端组件,名字里的lingbot像是项目代号或作者昵称,map则直接点明了…

2026/10/21 7:00:47 阅读更多 →
插件化知识工作系统:架构设计、实操路径与避坑指南

插件化知识工作系统:架构设计、实操路径与避坑指南

1. 从“知识工作插件”这个命名说起:它到底想解决什么问题第一次看到“knowledge-work-plugins”这个命名,我的直觉是:这不是一个普通的工具库,而是一套面向“知识工作者”的扩展机制。所谓知识工作,说白了就是那些以信…

2026/10/21 7:00:47 阅读更多 →
Docker核心概念与生产运维实战:从容器原理到故障排查

Docker核心概念与生产运维实战:从容器原理到故障排查

接触Docker也有些年头了,带过好几批刚入行的运维和转岗的开发,第一课的内容始终逃不开“Docker简介”这四个字。很多人喜欢一上来就背命令,docker run -d、docker ps、docker logs背得滚瓜烂熟,但真到了线上:容器内存被…

2026/10/21 7:00:47 阅读更多 →

最新新闻

自研分布式定时任务框架:从时间轮到OpenAPI异步调用的实践

自研分布式定时任务框架:从时间轮到OpenAPI异步调用的实践

定时任务这块,我相信每个后端团队都有过一段折腾史。早些年简单业务我直接塞进程里跑,后来规模大了开始用Quartz集群,再后来发现调度逻辑和业务代码越缠越紧,加上产品那边动不动就提“帮我跑个数据,明天给我结果”&…

2026/10/21 7:42:04 阅读更多 →
AI检测率过高怎么办?8款降AI率工具实测对比与写作攻略

AI检测率过高怎么办?8款降AI率工具实测对比与写作攻略

咱们先把话说在前面:这一两年,AI检测率这件事,快把专科生的毕业季逼成“渡劫现场”了。我接触过的学生里,十个有八个写过论文之后第一反应不是查重,而是先跑一遍AIGC检测,看到那个红彤彤的百分比&#xff0…

2026/10/21 7:42:04 阅读更多 →
Spring事务自调用失效:this.method()绕过AOP代理的深层原理与解决方案

Spring事务自调用失效:this.method()绕过AOP代理的深层原理与解决方案

“线上上了个单据审核接口,第一次调就发现事务没生效,回滚没执行,脏数据都进去了。排查了好半天才发现问题根本不在数据库配置,而是出在一个特别基础但又特别容易踩的坑:Spring 事务在自调用场景下失效。”这是我最近处…

2026/10/21 7:42:04 阅读更多 →
硬件噪声下WSN多跳物理层安全路径选择与仿真分析

硬件噪声下WSN多跳物理层安全路径选择与仿真分析

1. 项目概述与核心矛盾拆解老规矩,先说结论。这个项目看起来是做“无线传感器网络物理层安全”方向的一个仿真验证课题。标题里的关键词拆开来看,基本就是三件事:多跳收集-传输、窃听者存在、硬件噪声干扰。合在一起,研究的就是一…

2026/10/21 7:42:04 阅读更多 →
Poppler深度编译与PDF结构化解析实战指南

Poppler深度编译与PDF结构化解析实战指南

1. 这不是在装一个PDF工具,而是在打通AI阅读的底层链路“claude-paper 上手,从装 poppler 到跑出第一份速读摘要”——看到这个标题,我第一反应不是点开教程,而是下意识翻出自己三年前在某高校实验室做文献综述时的笔记。当时用Py…

2026/10/21 7:42:04 阅读更多 →
朱雀ai率检测和知网ai检测哪个准?论文降AI率、去AI味前先搞清楚学校用哪个AIGC检测

朱雀ai率检测和知网ai检测哪个准?论文降AI率、去AI味前先搞清楚学校用哪个AIGC检测

朱雀ai率检测和知网ai检测哪个准?这个问题我前后被问过不下几十次,每次的背景都差不多:同一篇论文,朱雀测一个数,知网测一个数,两个数对不上,人就慌了,不知道该信哪个。 我的答案很…

2026/10/21 7:41:03 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →