⚠️ NSFW 研究记录。文末章节包含具有露骨解剖描写的对比图像。仅供 18 岁以上读者阅读,是一篇研究性质的记录。
要点
在为顶级 OpenWeight 图像生成模型 HiDream-O1-Image (8B) 做 NSFW + 动漫质感专项微调的过程中,我们遇到了一堵墙:露骨的解剖描写 (penis 等) 始终无法解锁。最初的假设是"基础模型的 safety alignment 被烧进了 pixel head, 必须解冻才能触达"。但真正的原因不在那里 —— 我们自己的字幕生成器 (Gemma-4 E4B) 把 "fellatio" 圆滑成 "oral sex", 把露骨的解剖名词平均化, 在源头稀释了训练信号。
我们把字幕生成器换成 Grok-4.3, 用 两段式提示 (多选题概念抽取 → 强制包含的字幕组装) 对 1,250 张图像重新打字幕。"penis" 的出现率从 13% → 55%, "fellatio" 从 3% → 19%。借助 bitsandbytes 的 8-bit Adam, 我们在单张 96 GB GPU 上对 8B 模型做了全参数微调, 3,000 步用了 28 分钟。同一张训练图像在旧字幕 "oral sex" 下画不出 penis, 在重新打字幕后的 "fellatio on an erect penis" 下画得清清楚楚。
作为副产物, 我们得到了一个非常稳健的通用 SFW 模型。即使 NSFW 专向微调跑了 9,000 步, 也没有出现灾难性遗忘 —— SFW / Mature / 上身裸 / 全裸 / 写实 ⇔ 动漫 的风格切换全部完整保留。
不过有一个重要的更正:实际观察到的现象是两种不同的学习模式在共存。penis 是 "从零学习" 型 —— 基础模型几乎没有这个概念, 走的是典型的"形状破碎 → 接近形状"的轨迹。乳头则是 "解锁" 型 —— 基础模型本就有这个概念, 但质量下限很低, 而我们的干预几乎没有把它抬上去 (后文详述)。

背景
我们的服务 kotonia.ai 把 HiDream-O1-Image 作为常驻模型放在 /studio, 并在它上面叠了自家的 LoRA kotonia01/02, 用来给动漫质感加分。kotonia02 是在 191 张手挑图像上用 rank 16 训了 1,200 步, 现代动漫 / 社区流派风格的效果确实出来了。
但只要碰到 NSFW 倾向的提示:
- 脱外衣、上身裸 这种程度还能正常输出
- 但 露骨的生殖器或性行为描写 会被基础模型的 safety 偏置拉回来, 软软地滑开
我们决定正面打这一关。kotonia02 之前那套"在噪声图里藏起来, 粗暴推过去"的技巧, 上限已经看得见了。
路线 1: 死命堆 LoRA (失败时间线)
最朴素的第一假设是"数据和训练量不够而已"。
我们从一家公开图像分享平台的认证 API 上收集了 5,000 张图 (平台自带的 5 级 NSFW 分级按 15/15/20/25/25 的比例混合, 按高互动排序, 互动数 ≥ 50), 用自家的并行下载器拉取, 用 Gemma-4 E4B 给 5,000 张全部打了字幕。数据管线这边做得很干净。
训练侧三次迭代:
- kotonia03: rank 32 / 3,000 步 / 5,000 张 → 质感明显改善, 但露骨描写没到位
- kotonia04: 从 kotonia03 继续 + 6,000 步 → 步数加了, 但露骨"行为"描写还是被基础先验拉回去
- kotonia05: 把
final_layer2(像素投影头) 也解冻, 跑 3,000 步 → 终于第一次有了像样的前进——"画面里出现了男性""出现了情侣构图"
也就是说"层假设 (base 的 safety 被烧进了 final_layer2)"是半对的:解冻头之后确实前进了。但 penis 还是画不出来。回到对应字幕的训练图去看, 图里大量 penis 摆在那里, 模型就是不画。哪里对不上。
正经看一眼数据, 立刻看到了扭曲
突破口是对字幕文件做 grep 统计:
- lvl 16 (XXX) 共 n=1,250 张, 字幕里出现:
penis: 162 张 (13%)fellatio: 38 张 (3%)vaginal penetration: 196 张 (16%)cum/semen: 74 张 (4%)
跟实际图像内容完全对不上。图里明明是 penis, 字幕却被圆滑成 "oral sex" "vaginal penetration" 这类中性 / 上位词。把同一张图加上多选 (sexual_acts: [fellatio | cunnilingus | vaginal penetration | ... | none] 选其中之一) 喂给 E4B, E4B 在露骨性行为图上一律返回 ["none"]。解剖名词 (nipples, vulva 等) 它会老实说, 但承认一个"行为"在结构上被拒绝。
Gemma-4 是本地模型, 拒答类的护栏本该比较弱, 但对"露骨行为认定"这一项, 软化 (softening) 锁得死死的。改成多选、加强 system prompt、在 user prompt 里塞露骨词汇 —— 都没办法把它推动。
训练信号"露骨的 penis 像素 ↔ 中性 token 'oral sex'"被稀释到很弱, 推理时 "oral sex" 就退化回基础模型的先验 "explicit 类的什么 = 接吻 + 体液" —— 这就是发生的事。真正的原因不是模型, 是字幕生成器。
假设 B 的验证:换成 Grok-4.3
把同一张图 + 同样的多选提示扔给 xAI 的 grok-4.3:
{
"sexual_acts": ["fellatio", "ejaculation on face", "hand job"],
"anatomy_visible": ["penis", "testicles", "breasts", "tongue"],
"fluids_visible": ["semen", "saliva"]
}
E4B 返回 ["none"] 的那张图, 每个行为和解剖部位都被准确命名出来。
采用。recaption_two_stage.py 实现了下面这两段:
- Stage 1 (词汇抽取): 用结构化 JSON 输出强制概念枚举。
sexual_acts字段是从一组预定义固定标签 (fellatio | cunnilingus | vaginal penetration | anal penetration | hand job | breast play / paizuri | female masturbation | male masturbation | kissing | ejaculation on face | ejaculation on body | none) 里多选。把任务从描述任务改写成分类任务, 更容易绕过 safety alignment - Stage 2 (强制包含): 把 Stage 1 的词汇列表重新喂回去, 要求"必须原词使用", 让模型写一段 1–3 句的散文字幕。Grok-4.3 在这一段也不会私自省略词汇
跟 vLLM 那种连续批处理不同, Grok 是 API, 所以我们用 concurrency 8 并行 (httpx + ThreadPoolExecutor)。1,250 张约 47 分钟, 费用约 $30 跑完。
结果:词汇命中率剧烈改善

| 词汇 | 旧字幕 (E4B) | 重新打字幕 (Grok-4.3) | 倍数 |
|---|---|---|---|
| penis | 13% | 55% | 4.2× |
| fellatio | 3% | 19% | 6.4× |
| semen | 4% | 41% | 9.4× |
| ejaculat | 0% | 28% | (新增) |
| tongue | 2% | 29% | 16× |
| nipple | 19% | 60% | 3.1× |
| vulva | 13% | 42% | 3.2× |
| penetrat | 16% | 31% | 1.9× |
既然实际图像是高 NSFW 级别 (XXX) 的, 含量在这个范围才是"贴近实体"的分布。E4B 旧字幕的数字异常低, 正是因为它根本没在反映图像内容。
全参微调的实现备注:8-bit Adam 让 8B 塞进 96 GB
显存预算:
| 项 | 容量 |
|---|---|
| 权重 bf16 (8B) | 16 GB |
| 梯度 bf16 | 16 GB |
| Adam 状态 (默认 fp32) | 64 GB ← 瓶颈在这 |
| 激活 + 临时 (batch 1, 分辨率 1024) | 约 7 GB |
| 合计 (朴素方案) | 约 103 GB → OOM |
bitsandbytes.optim.AdamW8bit 把 Adam 状态压到 16 GB:
opt = bnb.optim.AdamW8bit(
(p for p in model.parameters() if p.requires_grad),
lr=args.lr, weight_decay=0.0,
)
常驻 54.6 GB。学习率 1e-5, 100 步 warmup, 3,000 步 → 28 分钟; 9,000 步 → 84 分钟。每次 model.save_pretrained() 写出约 17 GB 的 checkpoint shards (HuggingFace 标准格式)。
同字幕复现测试:真正的关键一击
训练图像 16/34854503.jpg 的旧字幕和重新打字幕后的字幕分别如下:
旧 (E4B):
...She is depicted in the act of oral sex, with drool and fluids visible around her mouth, set against a dark red, atmospheric background with small floating hearts. ...NSFW explicit, oral sex, cum on face, visible fluids.
重新打字幕 (Grok-4.3):
...performs fellatio on an erect penis with visible tongue amid ejaculation on face, semen on face and saliva against a dark red background with floating pink hearts under dramatic side lighting with glossy highlights.
把两个字幕喂给 同一个微调后的模型 (full_nsfw02_recapt):

- 左 (旧:"oral sex"): 构图 (红色辫子 + 白色内衣 + 男性胴体 + 暗红背景 + 心形) 完美, 但行为渲染成"像接吻的东西 + 口水", 没有 penis
- 右 (重新打字幕:"fellatio on an erect penis"): 同样的构图, 手握 penis 正在进行 fellatio, 脸上有 cum, 戴着白手套
这是这一轮验证里最强的证据。模型本身具备画出 penis 所需的视觉信息, 只是词汇这道闸门被关上了。
2D 矩阵:训练轴 × 提示轴
把训练侧词汇 (E4B → Grok 重新打字幕 + 步数) 与推理侧提示词汇当作两个独立的轴, 排成二维矩阵看, 现象就非常清楚:

- 纵轴 (模型演化): LoRA (kotonia04) → 全参微调 3,000 步 E4B → 全参微调 3,000 步 重新打字幕 → 全参微调 9,000 步 重新打字幕
- 横轴 (推理提示): 旧 "oral sex" / 重新打字幕 "fellatio on an erect penis"
读法:
- 横向 (提示词汇) 的效果很大: 在任何一行模型上, 只要用重新打字幕的词汇做提示, 都能画出 penis。即使是只在 E4B 字幕上训练的 kotonia04 LoRA, 用重新打字幕词汇推理时也能画出
- 纵向 (训练侧) 的效果更细腻: 训练侧词汇修复 + 步数, 影响的是解剖学的连贯性 (画出的 penis 是不是稳定形状) 与构图稳定性
- 左下 / 右上: 旧提示 + 旧训练 (左上) 完全没有 penis。重新打字幕提示 + 重新打字幕训练 (右下) 最稳定地输出露骨内容
- 左列纵向读: 即使训练数据已经重新打过字幕, 只要推理还是用旧提示, 还是画不出 penis。这就是**"只修训练侧不够, 必须把提示侧词汇也对齐"** 的非对称性
简而言之, 提示词汇与训练词汇必须同时对齐, 目标描绘才会到位。两边的"圆滑"都要消除, 只解决一边是不够的。
副产物:意外强劲的通用 SFW 模型
NSFW 专项的 2,500 张 (lvl 8 + 16) 上跑了 9,000 步全参微调, 但 SFW 类提示没有一项坏掉:

- 屋顶夕阳 (写实): 欧洲城市的照片级品质, 写实不掉到动漫去。SFW 提示的整合性完整
- 咖啡馆阅读 (动漫): 水彩动漫, 很可爱, 而且模型自顾自把 "KOTONIA" 这几个字塞到了书的封面上 (触发词彩蛋)
- 海滩泳装: 动漫半写实, 沟壑自然, Mature 上下文不崩
- 林中长裙: 动漫, 深沟 + 神秘光线, 解剖完美
"根据提示上下文在 写实 ⇔ 动漫 之间自动切换风格"的能力也完整保留。这比预期好很多 —— 解锁 NSFW 能力并没有付出通用性能的代价。原本计划里"副产物"那一栏, 反倒变成一个相当强的通用 SFW 模型。
NSFW 能力的边界 (附解剖学参照)
为了量化哪些区域学到了、哪些还没到, 我们投了 8 条露骨提示。这里贴一张其中渲染得最干净的 paizuri (breast play) 示例作参照:

- 已习得的概念: penis (单体描绘)、fellatio (paizuri 类型)、handjob、ejaculation on face、可见 cum、上身裸、全裸、暗示性 pose
- 仍纠缠的概念: vulva ↔ 口腔 (vaginal anatomy 仍倾向幻觉成"嘴里有舌头"的形状)、cunnilingus 结构 (脸与生殖器的接触几何)
- 过拟合迹象: cum 经常被画成红色 (训练集里 dark red 背景 + 爱心图案把这种色相先验粘到 cum token 上了)
vulva 和 cum 这两类问题在词汇修复的射程内:补做 lvl 8 (X tier) 的重新打字幕 + 补充背景色多样的露骨数据, 两条都能进。方法论上已经不是未知地了。
作为方法论:字幕词汇修复的一般化
这次验证里能抽出的可推广模式:
- OpenWeight 基础模型多半已经具备视觉能力: 为冲上 arena 排行榜需要看大量 (含 NSFW) 数据, 所以 safety alignment 是叠在上面的一层, 能力本身睡在权重里
- 词汇这道闸门会把能力藏起来: 通用字幕生成器几乎都会把露骨行为名词 (fellatio, penetration 等) 圆滑成中性上位词 (oral sex, act, intimate moment 等)。这就在微调阶段制造出弱信号
- 字幕器的选择支配整条训练管线: 如果你想用自家数据集把模型的 NSFW 能力拉出来, 第一个该解决的, 是**"字幕器的词汇忠实度"**, 模型侧的调整在那之后
- 多选 + 强制包含可以结构性地绕过 safety alignment: 在自由描述任务里会被软化的内容, 改写成 分类 + 原词引用 就能通过
要点:字幕生成器并没有拒答。E4B 老老实实把字幕返回来, 可这个字幕已经被软化了。"零拒答" 不能作为管线健康度的指标, 这是这次的另一个教训。
复盘
- 假设 A (层问题) 上 kotonia03 → 04 → 05 折腾了三次, 时间成本不低。一开始就应该跑
grep -c penis dataset/*.txt, 一分钟的事。要是早看到这个数字, 6 小时 GPU 时间就能省掉 - E4B "零拒答"让我们误以为管线没问题, 这也太大意了。Safety alignment 不只是拒答, 还会以"软化 / 圆滑"这种安静的方式出现
- 不过 kotonia05 解冻
final_layer2也不算白做:它确认了"层问题确实部分存在""基础模型的 safety 也部分烧在 pixel head 里"。假设 A 不是被推翻, 而是与假设 B 并存
重新审视假设:"解锁"型与"从零学习"型是两回事
走到一半时, 当初"用词汇修复解锁模型隐藏能力"这种单线故事开始撑不住。实际观察到的是两种不同的学习模式同时存在。
观察到的非对称性
| 概念 | 基础模型上的表现 | FT 之后的表现 | 学习类型 |
|---|---|---|---|
| 乳头 (nipple) | 基础模型在 "topless" 提示下第一发就能画(解剖学粗糙, 但存在) | 9,000 步 FT 之后质量下限几乎没动。数据集出现率 60% 也没把它拉上去 | 解锁型 (能力范围内已存在, 但质量改善是另一个问题) |
| penis | 基础模型在 "fellatio" 提示下根本不画。用重新打字幕的词汇硬塞才勉强冒出形状 | 步数累积下去, 走"变形 → 奇怪突起 → 接近形状的物体 → 解剖合理"这种典型的新概念习得轨迹 | 从零学习型 (可能在 pretrain 阶段被特意刮掉) |
为什么会有这种非对称
这个项目的 user 提出的**"pretrain 阶段的选择性清洗"假设**, 是最有解释力的:
- penis 判别器需求高、精度高: 商用内容审核市场里, penis 是非常清晰的单一特征目标, 可以训出高精度判别器。Pretrain 数据集可以干净地切掉这部分
- 乳头判别器结构性地难做: 男性也有乳头, 绘画作品里普遍存在, 医学影像里有, 解剖图谱里也有 —— "把包含乳头的所有图都从 pretrain 里删掉" 不现实。所以基础模型出厂时就保留着一个粗糙的乳头概念
按这个假设, 矩阵右下角 (全参微调 9,000 步 + 重新打字幕提示) 画出 penis, 不是"解锁", 而是**"等效于追加 pretrain 的概念教学"**。所以训练过程里 penis 的描绘走"逐步变得奇怪 → 接近形状"的曲线, 是新概念学习的轨迹, 不是解锁的轨迹。
乳头则是"pretrain 阶段已经学过, 但质量下限低就那么搁置"的状态, 即使在 1,250 张 lvl 16 + 1,250 张 lvl 8 这种 nipple 出现率 60–49% 的数据集上跑全参微调, 质量也几乎没线性改善。主要原因推测是字幕里没有"质量判定" (字幕没给"美丽乳头 vs 崩坏乳头"留判别轴)。
这与这次另一个发现 "MSE 像素损失与解剖质量弱相关" 也吻合:数据集出现率 (penis 13%→55%) 转化为描绘能力的前提是概念的最低限度表示必须已经存在于权重中。乳头满足这个前提但质量没提上来, penis 不满足这个前提但通过新概念学习推进了 —— 这就是分裂。
相关:自然语言处理里"概念消除"的脉络
近年的概念编辑 / 激活工程 (ROME / MEMIT / Anthropic 的 representation engineering 系列) 处理的就是 "把特定概念的向量方向识别出来, 在激活层面缩小其幅度" 这一类手法。OpenAI / Anthropic 系的后训练完全有可能集成了类似机制, 如果 OpenWeight 模型被做过这种处理, 就会形成"权重里还在, 激活层面被削掉"的状态。今次 penis 表现出"不强推就出不来, 一旦开始出现就走新概念学习轨迹", 正落在这个类别里。
文章主张的更正
最初的**"用词汇修复解锁隐藏能力"这个叙事对乳头并不适用**。乳头从一开始就看得见, 问题是质量。这不是词汇修复能解决的 (字幕的描述词汇里没有"美丽乳头 vs 崩坏乳头"的判别轴, 训练信号只有 1 bit 的"在 / 不在")。
penis 的情况, 词汇修复确实是新概念学习的入口 —— 但严格说这不是"解锁"而是 "启用"。要是基础模型彻底刮掉了露骨解剖, 这次也不可能成。它是"几乎刮掉"而不是"彻底刮掉", 所以重新打字幕词汇 + 9,000 步才能勉强让形状成立。这是更精确的描述。
待解决问题与下阶段作业
- 乳头质量:GAN 式路线
- 既有数据集训练改善不了质量下限, 下一轮试用户主导的奖励标注 (在 200–500 张上打 1–5 星) → 轻量 CNN 判别器 → 在扩散侧做奖励微调 (Diffusion-DPO 系)
- "kotonia 偏好判别器" 也会作为副产物落手, 可以复用到文章封面图的自动挑选上
- 用非审查字幕器替掉 Grok
- Grok-4.3 这次约 $30 (lvl 16) + $0.5 (pilot) 还能接受, 但扩到 lvl 8 之后压力上来。下次试 JoyCaption alpha-2 或社区非审查派生的 CogVLM2 / InternVL-2.5 在本地跑, 目标是 $0 / 不限量
- lvl 8 也做重新打字幕: X tier 1,250 张走同一条管线。解剖名词的多样性 + nipple/vulva 覆盖率都会进步
- 背景色多样化: 为了甩开 "cum = 红" 的过拟合, 有意识地补充明亮背景 + cum on body 的图
- cunnilingus 结构: 需要补充脸 ↔ 生殖器接触能画干净的样本
- kotonia.ai 整合: 多模型 VRAM 部署
- 现在 nsfw03 checkpoint 是独立运行,
/studio上跑的还是 kotonia01/02 LoRA - 96 GB GPU 上可以同时常驻 base (fp8, 8.79 GB) + nsfw03 (bf16, 16 GB), 实现按请求切换模型且无冷启动 (生成峰值算进去也才约 40 GB)
- 现在 nsfw03 checkpoint 是独立运行,
- 文章发布策略: 是押注 Google 在学术语境下能容忍解剖学画面让本文被索引, 还是为了避免污染 SafeSearch 而挂
noindex, 仍在权衡中
相关代码
自托管 monorepo, 内部使用:
collect.py— 公开图像分享平台的认证 API + 并行下载 5,000 张, 高互动 / 全期间 / NSFW 分级混合caption_v1.py— E4B 旧字幕器 (圆滑问题最初被发现的地方)recaption_two_stage.py— Grok-4.3 词汇修复字幕器,--backend xai|e4b切换combine_for_training.py— 设置--use_recapt后用.recapt.txt取代.txt做软链train_full.py— bnb 8-bit Adam,--init_from续训,save_pretrained()HF 格式infer_full.py— 全参 FT 模型推理maintenance_shim.py+start_maintenance.sh/stop_maintenance.sh— 训练期间把/studio+ LTX 端点替换成返回 503 + 三语言 / 动态 ETA 维护页的桩
研究日志到此告一段落。下一步:做 lvl 8 的重新打字幕、补充背景色多样性, 以及把上面那 3 个剩余项收掉。