我用 Gemini 训练了一个替代模型,仅花费9美元

Hacker News Top 新闻

摘要

本文详细描述了一个项目,其中使用 Gemini AI 为 Reddit 评论打标签,以微调开源 NER 模型 (GLiNER),从而将 API 成本从持续的 Gemini 调用降低到一次性 9 美元的标记费用。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/17 15:12

# 我用 Gemini 花 9 美元训练出了它自己的替代模型 来源:https://www.petervijeh.com/projects/reddit-ner 我喜欢做饭,不知不觉中这变成了一种对高端厨师刀的痴迷。因此,我会爬取 Reddit 上人们争论刀具的帖子,提取出他们提到的每个品牌、型号和钢材,看看什么在被购买和讨论。 从文本中提取产品名称这项工作叫做命名实体识别,小型模型已经做了十年。我之前用 Gemini 3.1 Pro 来做这件事,每条评论调用一次付费 API。这有点大材小用,但确实有效:从 "picked up a Mazaki in white #2, way better than my old Fibrox" 这句话中,它返回 Mazaki 为品牌,Fibrox 为型号,white #2 为钢材,其他无。但爬虫会抓取每一条新评论,因此账单随着人们的发帖量水涨船高,唯一的控制方法是跳过评论。 一个显而易见的替代方案是,使用一个名为 GLiNER 的开源 NER 模型进行零样本预测,这几乎将成本降为零,但准确率相对于 Gemini 的答案只有约 0.65 F1。这个差距就是本文的重点:Gemini 能否一次性标注 4,290 条评论,并教会 GLiNER 来缩小这个差距? - **做什么:** 基于 Gemini 3.1 Pro 一次性写下的标签,微调 GLiNER large v2.5 (459M) 模型,用于标记 Reddit 评论中的品牌、型号和材料。 - **为什么:** 零样本 GLiNER 评分约 0.65 F1(估计)。Gemini 评分高,但只要爬虫运行,就会对每条评论收费。 - **方法:** 让 Gemini 返回字符串,而非字符偏移量。在代码中计算偏移量。添加不含产品的评论作为负样本。在第二次运行前,锁定一个包含 225 条评论的验证集。 - **问题:** 十次训练中有五次未产生可用模型。三次因配置失败。两次因一个名为 `words_mask` 的张量失败,我像填充注意力掩码那样填充了它。 - **结果:** 在 Tesla T4 上运行 24 分钟后,相对于 Gemini 的标签达到 0.83 F1。9 美元的标签费用,约 2.50 美元的 GPU 时间,以及数天的调试。 ## 我的目标 计划分三步:让 Gemini 一次性标注数千条 Reddit 评论,标记每个品牌、型号和钢材;在这些标签上训练 GLiNER;然后在自己的机器上对后续每条评论运行 GLiNER,不再调用 Gemini。 Gemini 以 9 美元标注了 4,290 条评论,相当于每条评论 0.0021 美元。这意味着,只要后续评论长度相近且能在我已有的 GPU 上运行,训练好的模型大约在第 4,291 条评论时就能收回成本。成功的测试很简单:在模型从未见过的 225 条评论上,它标记的词语与 Gemini 标记的词语一致的频率有多高?有一点需要记住:本文中的每个分数都没有人工核对 Gemini 的标签,因此模型是与 Gemini 对标,而非与绝对真相对标。当 Gemini 出错时,模型如果照抄错误则被判正确,如果修正错误则被判错误。 ## 方法 Gemini 通过 OpenRouter 在 25 分钟内以温度 0 完成了标注。最重要的提示设计决策是:永远不要要求模型提供字符偏移量。它的字符计数很差,返回的区间位置会偏差两到三个字符。提示要求返回确切的子字符串和标签,然后由 TypeScript 计算偏移量。如果字符串不在评论中,则该实体被丢弃并记录。 ```json // 模型返回字符串,代码计算偏移量 { "entities": [ { "text": "Benchmade", "label": "knife brand" }, { "text": "940", "label": "knife model" }, { "text": "S30V", "label": "knife steel" } ] } ``` 产品名称充满了通用分词器会拆分的标点符号,因此我使用正则表达式来保持 VG-10、CPM-154 和 1.4116 的完整性,并将每个非空格字符作为独立令牌输出。仍然无法匹配令牌边界的区间会被丢弃,而不是猜测。大约 30% 的训练集是包含已知误报触发词(如 gyuto、carbon、handle、patina)但没有产品的评论,被标记为空。在第二次运行前,我划出了 225 条评论作为验证集,并且再也不动它们。训练在 Modal 平台的 Tesla T4 上使用 HF Trainer 进行。 ``` per_device_train_batch_size = 2 gradient_accumulation_steps = 8 learning_rate = 1e-5 threshold = 0.45 ``` **模型训练使用了什么** 4,290 条由 LLM 标注的论坛评论(成本 $9) - 1,575 条正样本 (69.6%) - 675 条负样本 - 2,250 条训练样本 (2,029 条训练,225 条验证,验证集从第 10 次运行起锁定) - 3,907 个实体区间 - 品牌: 1,720 - 产品型号: 1,345 - 材料规格: 842 约 30% 的样本故意不包含任何实体。 ## 出了什么问题 有五次训练模型什么也没学到。前三次是配置错误,任何使用 HF Trainer 配合 GLiNER 的人都可能在一下午内遇到它们。 | 运行 | 问题 | 修复方法 | |------|------|----------| | 1 | GLiNER 默认的 `max_steps=10000` 覆盖了 `num_train_epochs=3`;训练了 39 个 epoch | 显式设置 `max_steps` | | 2 | 设置 `load_best_model_at_end` 但未设置 `eval_strategy` 会报错 | 设置 `eval_strategy="steps"` | | 3 | Trainer 保存的状态字典键名缺少 GLiNER 加载器期望的 `"model."` 前缀 | 保存时加回前缀 | | 4 | 负样本缺少 `ner_labels` | 在每个样本上设置标签列表 | | 4–5 | `words_mask` 被构建为二元掩码;损失在 70–130 间持平 | 输出递增的词索引 | 第 4 和第 5 次运行代价高昂。GLiNER 的 `tokenize_inputs` 在处理损坏的 Reddit 表情符号时崩溃,因此我修改了它,而补丁需要填充一个名为 `words_mask` 的张量。它与 `attention_mask` 相邻,形状相同,而我构建过的所有注意力掩码,真实令牌都是 1,填充都是 0。我就是那样构建的。从名称和形状上看,它与注意力掩码别无二致。 训练运行完成。损失从约 130 开始,漂移到约 70 并保持不变。没有崩溃,没有警告,没有 NaN,梯度大小正常,检查点按计划保存,评估 F1 接近零。我首先怀疑标签列表,因为第 4 次运行也对负样本未设置标签。修复后重新运行,损失依然持平。第 5 次运行唯一的问题是 `words_mask`,一个我从未仔细看过的张量。 **十次训练运行** 前五次未产生可用模型——都是工程问题,而非建模问题 0.0 0.2 0.4 0.6 0.8 1.0 ✕失败 ●得分 ──最佳F1 ─·在生产环境中使用 R1 步数限制 ✕ R2 配置错误 ✕ R3 检查点保存错误 ✕ R4 词掩码错误 ✕ R5 词掩码错误 ─未评分─ R6 首次成功 0.800 R7 微调 209M 0.879 R8 459M 0.799 R9 负样本过多 0.832 R10 锁定验证集 运行 1-5 完全失败。虚线追踪了词掩码修复后的整体 F1。 ## 我如何解决的 我阅读了 GLiNER 的训练循环代码,而非其文档。`words_mask` 不是一个掩码。它是一个词索引:特殊标记、提示标记和填充标记为 0,然后真实词的第一个子标记为 1、2、3。跨度评分头用它将子标记聚合回词。如果全部填充为 1,则表示整个评论是一个词,模型就会被要求在一个巨大的标记内寻找品牌和材料区间。它做不到,因此损失保持平坦,而平坦的损失无法显示哪个输入是错的。 ```python # 我写的代码 # GLiNER 期望的 words_mask = [1,1,1,1,1] words_mask = [0,1,2,2,3] # [CLS] Mazaki wh ##ite #2 ``` 修正索引后,第 6 次运行第一次就成功了。剩下的就是针对锁定的验证集进行调优。209M 的中型模型达到 0.800 F1;459M 的大型模型(仅通过梯度累积才能在 T4 上运行)达到 0.83 F1。十倍多的对抗性负样本(510 而非 51)使 F1 降至 0.799,因此第 10 次运行回到了 51 条。为每个类别设置独立阈值(而非全局截断值)将材料的召回率从 0.787 提高到 0.911,因为像 MagnaCut、S35VN 和 HAP40 这样的钢材名称得分置信度低于品牌,单一截断值会丢弃它们。每次大型运行都在第 2 个 epoch 触底并过拟合;对于 2,000 条样本来说,这是数据集规模问题,早停法是解决方案。 **按实体类别的 F1 分数** 零样本基线 vs. 微调检查点,相同保留数据 零样本 (估计) 微调 209M 微调 459M 0.0 0.2 0.4 0.6 0.8 1.0 ~0.65 0.800 0.879 总体 n/a 0.858 0.904 品牌 n/a 0.775 0.877 产品 n/a 0.712 0.829 规格 更大的编码器在纯领域特定词汇表上帮助最大。 ## 我学到了什么 它成功了。模型本地运行,在从未见过的评论上以 0.83 F1 匹配 Gemini 的标签,并且知道 "carbon steel" 是一个类别而非一种钢材,以及 PM2 有时指 Spyderco Paramilitary 2,有时只是字母。有一次早期的随机划分运行得分为 0.879。我不把它作为结果:在随机划分中,我原以为由我的修改导致的两次 F1 下降,实际上来自于哪些评论进入了验证集。 账面上,这个项目的成本低于一顿午餐:9 美元的标签,2.50 美元的 GPU 费用。它花费我的是那些耗费在一个通过所有代码检查但仍然错误的张量上的日子。我认为对于小型微调任务,损失这些日子是常态。模型和数据很少是问题;它们之间的代码会出错,而由错误输入张量导致的平坦损失,与由困难数据导致的平坦损失看起来一模一样。如果我必须在更好的标签集和为每个手工构建的张量添加断言之间选择,我会选择后者。 这个模型驱动着 "New Knife Day" 项目,该项目追踪 Reddit 上人们购买和争论的刀具。那边的刀具相关文章有完整的运行日志。两者均链接如下。 ## 概览 | 项目 | 内容 | |------|------| | **问题** | 在 Reddit 评论中找出品牌、型号和材料名称,同时忽略周围的通用词汇,无需为每条评论向 LLM 付费 | | **方法** | 让 Gemini 一次性标注评论,然后基于这些标签微调 GLiNER large v2.5(一个基于 DeBERTa-v3-large 编码器的模型)并本地运行 | | **结果** | 在训练前锁定的 225 条评论验证集上达到 0.83 F1;通过每类阈值实现材料召回率 0.911 | | **成本** | Gemini 标签 $9,加上十次运行中约 $2.50 的 T4 时间 | | **技术栈** | 爬虫和标签使用 TypeScript 和 MongoDB;训练使用 Python、PyTorch 和 Modal;模型服务使用 FastAPI |

相似文章

Gemini 3.5 Transcribe 智能转录

Google DeepMind Blog

Google 推出 Gemini 3.5 Transcribe,这是一款新的 AI 模型,提供精确且智能的实时语音转文本转录,开发者可通过 API 获取。

Gemini 3.8 Flash

Product Hunt

Google 发布 Gemini 3.8 Flash,这是一款先进的 AI 模型,在编程、智能体能力和多步推理方面有显著改进,提供入门价格。