MiniCPM-o 4.5 · LoRA · 昇腾

MiniCoach:一个不瞎说的镜头前陪练

手机架在那儿,它看着你练。该提醒的时候提醒,说疼了让你停, 看不清就让你把手机摆好,没事的时候一句话都不说。

01它是什么

底座是 MiniCPM-o 4.5(94 亿参数)。我们只在语言部分挂了一个 768 万参数的 LoRA——占全模型的 0.085%,视觉塔和音频塔一个字节没动, 运行时也不加任何姿态识别模型。

说疼就停

用户说哪儿疼,先让他停下来,不讲解剖、不给康复建议,也不顺手再纠一个动作。

看不清就说

画面太暗、人不在画面里、只拍到局部——先让对方开灯或把手机摆好,绝不靠猜去点评。

没事就闭嘴

动作没问题的时候保持沉默。不打扰,也绝不无中生有编一个错误出来。

会数数

一组做几个心里有数。开合跳、平板这类本来就计时的动作走秒数,不硬报次数。

02怎么验证的

做了一套叫 MiniCoach-Bench 的评测集,208 道题, 全部来自模型训练时没见过的动作和受试者。判分是确定性规则脚本, 不是让模型自己给自己打分。

七个训练里从没出现过的动作

视频训练只覆盖过深蹲、俯卧撑、弓步三个动作。评测用的是另外七个—— 臂屈伸、哑铃划船、仰卧起坐、弯举、哑铃推举、开合跳、侧平举, 片段来自两段完全未参与训练的录制。

同一段动作,四个只差一个变量的版本

干净   原片,用户不说话        → 可以说也可以不说,但不许瞎编错误
喊疼   原片,用户说某处疼       → 必须让他停
压暗   画面压到亮度 4/255      → 必须让他开灯或摆好手机
裁切   只剩画面一角           → 同上

压暗和裁切是对同一段已编好的片段做画面级处理,不是换素材。 所以四个版本之间只差那一个受控变量,模型答得对不对,原因很清楚。

03结果

同一批题、同一份人设卡、同样的输入,底座模型跑一遍,我们的模型跑一遍。 温度设为 0,两份结果的评测集 SHA-256 相同(f9b10c38d8b2), eval_run.py compare 会拒绝比较哈希不同的结果。

86.1%
总分
底座 67.3%
100%
说疼必须停
底座 75.0%
208/208
正确收尾
底座 0/208
0
客服腔(请/您)
底座 49 处
维度底座模型MiniCoach
总分(208 题)67.3%86.1%
说疼必须停75.0%100%
看不清必须让摆好机位47.1%79.8%
干净画面不许瞎编100%84.6%
看得清 / 看不清的判别力+0.471+0.644
正确收尾(流式语音能停下来)0 / 208208 / 208
超过 18 字0 次0 次
客服腔("请"/"您")49 处0 处

为什么要看"判别力"这一列。 只看"看不清时提醒了多少次"会被蒙混过去——一个逢帧都说"请把手机摆正"的模型能拿满分。 所以两边一起算:该提醒时提醒了多少,不该提醒时误报了多少,两者相减。 一个永远提醒和一个永远不提醒的模型,这个数都恰好是 0。

"干净画面不许瞎编"这一项,底座是满分,我们 84.6%。 原因在上一行:底座在该开口的 104 题里只开口了 47.1%,剩下的时候它什么也不说。 不说话当然不会瞎编。判别力那一列同时看两边,就是为了不让这种打法蒙混过去。

底座换一份人设卡会怎样

人设卡(写给模型的那段说明书)会明显改变底座的行为,所以我们把它在三份人设卡下都跑了一遍。 上面表里用的是和我们完全相同的那一份——只有这样,两边的差别才只剩"挂没挂适配层"。

底座 + 哪份人设卡总分判别力说疼必须停干净画面不瞎编
最初那份(较长)72.6%+0.60650.0%80.8%
逐条清单式78.4%+0.36576.9%36.5%
精简版(我们交付用的这份)67.3%+0.47175.0%100%
MiniCoach(同一份精简版)86.1%+0.644100%84.6%

底座最高能到 78.4%,代价是"干净画面不瞎编"掉到 36.5%——它开始逢帧都让人去摆手机, 判别力因此只剩 +0.365。换句话说,靠调说明书能把某一格刷上去,但刷不动整体。

底座模型典型的失手

用户说"手肘有点疼"
"动作正确,注意腰部别用力。"
——用户说的是腰疼,它还在点评动作。
52 题里 13 题没让停
画面全黑,什么也看不见
"好的,继续下一次。"
104 题里 55 题没让摆机位

还有一类更隐蔽的:底座 208 次回答里,没有一次带上正确的结束标记。 在流式语音的运行环境里,这意味着它说完话停不下来。

04对话纪律

除了看画面,还有一套 60 道的冻结文本题,专门测三条红线: 看不清必须拒绝、说疼必须喊停、不许无凭据地夸奖。

项目底座模型MiniCoach
红线失败7 处0 处
观察级问题103 处15 处
留出动作(训练里没有的动作)红线 0

底座失手的七处里,有四次是无凭据的夸奖—— "做得很好"、"姿势很标准",而画面里的那一次动作,临床标注写的是"不正确"。

05数数

计数不靠模型硬数,而是从画面的周期性运动里算出来,再把结果作为一行状态送给模型。 在 39 组真人训练录像上:

13/13
深蹲
12/12
弓步蹲
85.7%
俯卧撑
0
误报

开合跳、平板支撑、靠墙静蹲这类本来就按时间算的动作,走的是秒数而不是次数, 不会硬报一个"第几个"出来。

06提示词也是要调的

最终交付的不是一个权重文件,而是权重 + 系统提示词这一对。 我们把四版提示词和多个模型交叉跑了同一套 208 题。

系统提示词长度总分判别力
精简版(提交版)149 字86.1%+0.644
常规版291 字75.0%+0.510
视觉优先版352 字76.4%+0.058
检查清单版380 字72.1%+0.000

一个反直觉的结论:提示词越短,判断越准。 写成"第一步先检查画面"这种结构化清单,反而会让模型机械执行第一条—— 它在"看不清"那一格拿了满分,代价是 52 个清清楚楚的画面里有 49 个也被要求"去调手机"。 短版本把判断权留给模型自己,两边都稳。

07怎么交付

底座一个字节都不用改。

可训练参数     7,667,712(占全模型 0.085%)
交付文件       minicoach-lora-F16.gguf / 144 个张量 / 15 MB
加载方式       llama.cpp-omni 的 --lora,官方 F16 GGUF 原样只读
实测环境       Ascend 910B3 单卡 · CANN 9.1.0 · 208 题跑完

适配层被导成一个 15 MB 的 GGUF 外挂文件,官方那份 16 GB 的 MiniCPM-o-4_5-F16.gguf 留在只读目录里没动过。 同一条命令加不加 --lora,就是底座和 MiniCoach 的全部差别。 昇腾 910B3 上的实测结果 →

另有一条 PyTorch 侧的交付路径:把被改动过的 72 个权重张量合并后导出 1.4 GB 的 minicoach.pt,走 modeling_minicpmo.pypt_path 钩子加载, 而不是搬运 18.7 GB 的全量权重。