---
title: "图片与视频模型"
description: "media-model-proxy 代理的成人、暴力血腥、仇恨符号、主题、指纹模型；clip/ 训练的 256 维图文嵌入；adult-content/ 的训练与校准；pnsfwmedia 的媒体加账号联合模型"
---

> Documentation Index
> Fetch the complete documentation index at: https://x-algorithm.vergego.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 图片与视频模型

## 这一页回答什么

本页说明 X 用哪些专用模型分析图片和视频，内容包括：media-model-proxy 代理哪些模型，每个模型读取什么输入、返回什么；`clip/` 训练的图文嵌入如何成为这些分类器的输入；`adult-content/` 如何训练和校准成人内容分类器；`pnsfwmedia/` 如何把 CLIP 媒体嵌入与账号级分数合并；以及这些分数最终由谁读取。

Grox 中基于大模型的媒体判定（PTOS、safemodel）不在本页范围内，见 [Grox 内容分类器](https://x-algorithm.vergego.ai/concepts/grox-classifiers/)。

## 核心结论

1. **media-model-proxy 只转发请求，不存储分数**：它按 Blobstore 路径读取一次媒体，并发请求多个 DeepBird 模型，再把“模型 → DataRecord 列表”返回给调用方。调用方负责解释和持久化分数（`media-model-proxy/README.rst:7-20`）。README 写明当前调用方是 Media Analysis Service，该服务不在仓库中。
2. **代理注册了 7 个模型**：成人内容 `XxNsfw`、暴力血腥 `ViolenceAndGore`、仇恨符号 `HatefulSymbolRecognition`、主题 `CategoryClassification`、主题嵌入 `CategoryClassificationEmbeddings`、指纹 `Fingerprinting` 与 `FingerprintingV2`（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/model_descriptors/ModelDescriptorRegistry.scala:9-19`）。Thrift 枚举中的 `XX_NSFW_V2` 没有对应实现。decider 中的 `disable_adult_content_v1` 开关没有被任何模型引用。
3. **CLIP 是媒体嵌入的来源**：`clip/` 在 OpenAI CLIP ViT-B/32 上加一层前馈投影，用推文配图与推文文本做对比学习，产出 256 维图文嵌入（`clip/model.py:26`、`clip/config.py:6`）。模型发布到 packer 角色 `embeddings-category`（`clip/scripts/export_keras_models.py:68`）。
4. **成人分类器是建在 CLIP 嵌入上的小网络**：`adult-content/` 在 256 维媒体嵌入上训练“BatchNorm → 128 维 GELU → sigmoid”二分类器。随后它用保序回归把原始分数校准成概率，并输出 200 个阈值下的精确率、召回率、误报率表（`adult-content/training.py:13-42`、`adult-content/calibration.py:72-88`）。
5. **pnsfwmedia 联合媒体与账号做判定**：冻结的 CLIP 模型给出媒体分。模型再拼接 agatha 的校准 NSFW 账号分、账号文本 NSFW 分、粉丝 NSFW 分和粉丝数对数，经过一个 50 维隐层输出概率（`pnsfwmedia/model_experimental.py:50-92`）。文件名带 `experimental`，源码未包含线上是否使用它的信息。
6. **botmaker 规则把分数转为标签**：开源代码中可见的读取方是 scarecrow 的 `FeaturesOfMediaUpdate`。它把媒体更新事件中 `xx`、`sexy`、`xxnsfw`、`xxnsfwv2` 等标签的概率、精确率、召回率、误报率和校准档位转成 botmaker 规则特征（`botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:27-112`）。规则再打标签，[可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/) 读取这些标签。

## 全景

```mermaid
flowchart LR
    subgraph train["离线训练"]
        CLIP["clip/<br/>Twitter-ViT-B/32-256"] --> EMB["256 维媒体嵌入"]
        EMB --> AC["adult-content/<br/>MLP + 保序回归校准"]
        EMB --> PN["pnsfwmedia/<br/>媒体分 + 账号分"]
        AG["agatha 校准 NSFW 账号分"] --> PN
    end
    subgraph serve["在线推理"]
        MAS["Media Analysis Service<br/>源码未包含"] --> MMP["media-model-proxy"]
        MMP --> BS[("Blobstore")]
        MMP --> DB["DeepBird 模型<br/>7 个"]
    end
    CLIP -. "packer 角色 embeddings-category" .-> DB
    MAS -. "媒体更新事件" .-> SC["scarecrow<br/>FeaturesOfMediaUpdate"]
    SC --> BM["botmaker 规则"]
    BM --> VF["可见性过滤"]
```

虚线表示根据命名或数据格式推断出的连接，源码没有给出完整调用链。

## media-model-proxy：模型代理

### 请求与响应

服务沿用旧名 `CortexMediaAnnotator`，只有一个方法 `getAnnotations`（`media-model-proxy/src/main/thrift/com/twitter/cortex_media_annotator/annotate.thrift:75-78`）。请求带一个媒体实体和一组模型。媒体实体可以直接带字节 `media_data`，也可以只带 `blobstore_path`。响应的 `annotations` 是“模型 → DataRecord 列表”（`media-model-proxy/src/main/thrift/com/twitter/cortex_media_annotator/annotate.thrift:31-53`）。

`ProxyController` 按以下顺序执行（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/controllers/ProxyController.scala:33-92`）：

1. 请求带字节时，直接使用字节；否则，按路径从 Blobstore 读取一次。两者都没有时，返回错误。
2. 对请求中的每个模型并发调用 `ModelsService.getPrediction`。
3. 汇总成响应，并按采样率记录请求日志。采样率 `requests_scribe_sample_rate` 的开源默认值为 100（取值范围 0 到 10000，即 1%）（`media-model-proxy/config/decider.yml:57-59`）。

`ModelsService.getPrediction` 查找模型的描述器，把媒体字节转成该模型所需的特征，再发送给所有可用的客户端（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/services/ModelsService.scala:38-93`）。**单个客户端失败不会导致请求失败**：服务过滤掉非致命异常，该模型只返回成功的那部分结果，结果可能是空列表（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/services/ModelsService.scala:104-120`）。DeepBird 客户端在连接关闭和超时时自动重试（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/clients/DeepbirdClient.scala:63-70`）。

### 七个模型

| 模型 | 判定内容 | 输入特征 | 支持的媒体 | DeepBird 服务 | 超时 | 代理校验的输出 |
|------|--------|---------|-----------|--------------|------|--------------|
| `XxNsfw` | 成人内容 | Blob 特征 `media` + 离散特征 `meta.media_type` | 推文图片、推文视频、Periscope 视频、推文 GIF；必须带媒体类别 | `/s/xx-nsfw/xx-nsfw` | 8000 ms | `nsfw.default.probability`、`.score`、`.precision`、`.recall`、`.fpr` 五个连续值 |
| `ViolenceAndGore` | 暴力血腥 | 张量 `violence_and_gore.image` + `meta.media_type` | 头像、横幅、推文图片、推文视频、推文 GIF | `/s/violence-and-gore/violence-and-gore` | 3000 ms | 不校验；源码未包含输出字段 |
| `HatefulSymbolRecognition` | 仇恨符号 | 张量 `magicpony.hateful_symbol_recognition.image` | 不限 | `/s/magicpony-hateful/magicpony-hateful-symbol-recognition` | 3000 ms | 不校验；源码未包含输出字段 |
| `CategoryClassification` | 媒体主题 | 张量 `magicpony.category_classification.image` | GIF 需开启开关 `enable_gifs_for_category_classification` | `/s/magicpony-category/magicpony-category-classification` | 3000 ms | 不校验；源码未包含输出字段 |
| `CategoryClassificationEmbeddings` | 媒体嵌入 | 张量 `media.data` | GIF 需开启开关 `enable_gifs_for_category_classification_embeddings` | `/s/embeddings-category/embeddings-category-classification` | 4000 ms | 不校验；占位客户端返回 256 维 |
| `Fingerprinting` | 用于匹配已知媒体的指纹 | 张量 `magicpony.media_fingerprinting.image` | 不限 | `/s/magicpony-fingerprinting/magicpony-fingerprinting` | 3000 ms | `pixel_variance`、`embedding` 两个张量 |
| `FingerprintingV2` | 同上，第二版 | 同上 | 不限 | 同上，由不同开关控制 | 3000 ms | 同上 |

出处依次为 `XXNsfwModelDescriptor.scala:29-84`、`ViolenceAndGoreModelDescriptor.scala:35-72`、`HatefulSymbolRecognitionModelDescriptor.scala:25-50`、`CategoryClassificationModelDescriptor.scala:25-55`、`CategoryClassificationEmbeddingsModelDescriptor.scala:28-67`、`FingerprintingModelDescriptor.scala:25-69`、`FingerprintingV2ModelDescriptor.scala:60-69`，均位于 `media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/model_descriptors/`。

补充说明：

- **媒体类别归并**：模型不直接支持某个类别时，GIF 归为推文 GIF，视频归为推文视频，图片归为推文图片，其余类别返回错误（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/model_descriptors/ModelDescriptor.scala:31-46`）。
- **指纹只是特征**：代理只返回指纹向量和像素方差。用指纹比对已知媒体库的逻辑不在本服务中。scarecrow 能读到媒体更新事件中的聚类签名、MD5 哈希和匹配到的视频聚类，并用于规则判断（`botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:134-173`）。源码未写明这些聚类是否由本服务的指纹生成。
- **主题嵌入可切换为随机向量**：`CategoryClassificationEmbeddings` 用 `SwitchingModelClient` 在真实 DeepBird 客户端与占位客户端之间切换。开关 `enable_dummy_category_classification_embeddings` 打开时，它返回 256 个随机浮点数，用于压测或降级（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/clients/DummyEmbeddingsClient.scala:19-56`）。另一个开关可以把同一请求复制一份发到备用目标，用于暗流量对比（`media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/clients/SwitchingModelClient.scala:22-29`）。
- **每个模型都能单独关闭**：decider 为每个模型提供一个 `disable_*` 开关。开源默认值都是 0，即全部开启（`media-model-proxy/config/decider.yml:5-35`）。

## clip/：256 维图文嵌入

`TwitterCLIP` 以 OpenAI 的 CLIP ViT-B/32 为底座，在图像塔和文本塔顶部各加一个 `PositionwiseFeedForward` 投影层，把输出压缩到 `final_embedding_dim`（`clip/modules.py:31-81`）。默认生产模型是 `Twitter-ViT-B/32-256`，即 256 维；另有 128 维版本（`clip/model.py:26`、`clip/model.py:44-47`）。

训练采用标准的 CLIP 对比学习：模型为一批图文对计算相似度矩阵，按图到文、文到图各计算一次交叉熵，再把两者相加（`clip/training.py:143-171`、`clip/utils/model_utils.py:4-11`）。文本取推文正文字段 `tweet_text`（`clip/datasets/language_image_dataset.py:16`）。开源默认值如下（`clip/config.py:8-25`）：

| 参数 | 值 |
|------|----|
| 输入分辨率 | 224 |
| `final_embedding_dim` | 256 |
| `logit_scale` 初值 | 4.5065（取指数后约 90.6；前向计算时上限为 100，见 `clip/model.py:227`） |
| `batch_size` | 128 |
| `learning_rate` | 2e-5 |
| 训练步数 / 预热步数 | 100000 / 500 |

生产检查点文件名为 `clip_source_d256_bs2784`（`clip/model.py:45`）。按命名推测，该检查点使用 2784 的批大小训练，与上表默认值不同。

**导出与分发**：

- Keras SavedModel 暴露 `predict_from_encoded_images`。它输入编码后的图片字节，输出 L2 归一化的 `image_embeddings`（`clip/keras_model.py:117-131`）。模型发布到 packer 角色 `embeddings-category`，包名为 `CLIP-keras-Twitter-ViT-B-32-256`（`clip/keras_model.py:24-26`、`clip/scripts/export_keras_models.py:68`）。
- 图像编码器也能导出为 DeepBird v2 模型文件，输出名为 `embedding`（`clip/model.py:306-335`）。
- `clip/notebooks/media_embeddings_dataflow.ipynb` 演示批量推理：从推文表读取媒体 URL，下载媒体，计算 CLIP 嵌入，再写回 BigQuery 的 `media_embedding` 字段。

**与 media-model-proxy 的关系（推测）**：代理的 `CategoryClassificationEmbeddings` 指向 `/s/embeddings-category/...`，占位维度为 256。CLIP 包发布在同名角色 `embeddings-category`，生产维度也是 256。两处证据一致，因此推测该模型就是 CLIP 图像编码器。源码没有直接写出两者的对应关系。

## adult-content/：成人内容分类器的训练与校准

**数据**。每条 TFRecord 包含 256 维 `media_embedding` 和一组整数 `labels`。任一标签等于 1 时，该条为正样本（`adult-content/dataset_utils.py:14-38`）。训练时可以开启拒绝采样，把正负样本比例重采样到 50:50（`adult-content/dataset_utils.py:10-12`）。

**模型**。模型共三层（`adult-content/training.py:13-42`）：

1. `BatchNormalization`
2. `Dense(128, activation="gelu")`
3. `Dense(1, activation="sigmoid")`

损失函数为二元交叉熵，优化器为 Adam。监控指标是“召回率 0.9 时的精确率”和 PR 曲线下面积。验证损失连续 5 轮不下降时，训练提前停止并恢复最佳权重（`adult-content/training.py:66-74`）。开源默认值为：`batch_size` 256，学习率 0.001，最多 100 轮，每轮 400 步（`adult-content/config.py:15-20`）。

**校准**（`adult-content/calibration.py:38-97`）：

1. 在 0 到 1 之间均匀选取 200 个阈值（`num_thresholds=200`，`adult-content/config.py:36`）。
2. 对每个原始分数，找到它所在的两个相邻阈值之间的区间，用该区间内正样本的比例作为目标值。
3. 用保序回归（单调递增，输出截断在 0 到 1）拟合“原始分数 → 正样本比例”。
4. 对每个阈值输出一行：阈值、精确率、召回率、误报率、校准后概率，写入 `metrics_at_thresholds.csv`。

**与 XxNsfw 输出的关系（推测）**：代理强制要求 `XxNsfw` 返回 probability、score、precision、recall、fpr 五个字段，与校准表的列一一对应。推测线上 `XxNsfw` 先计算原始分数，再查校准表，给出该分数对应的概率、精确率、召回率和误报率。scarecrow 读取的 `xxnsfw` 标签同样带 probability、precision、recall、fpr 四个值（`botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:75-88`）。

## pnsfwmedia/：媒体分加账号分

`pnsfwmedia/model_experimental.py` 定义一个 Keras 模型，输入 8 个特征（`pnsfwmedia/model_experimental.py:50-92`）：

| 特征 | 来源 | 缺失时 |
|------|------|-------|
| 媒体分 | 冻结的 CLIP 模型作用于 256 维张量 `media.mediaunderstanding.media_embeddings.twitter_clip_as_tensor` | 默认全零向量 |
| `agatha_calibrated_nsfw_score` | 作者的 agatha 校准 NSFW 分 | -1.0 |
| `nsfw_text_user_score` | 作者发文的 NSFW 文本分 | -1.0 |
| 粉丝数对数 | `log(followed_by_count + 10)` | — |
| `nsfw_consumer_follower_score` | 作者粉丝中 NSFW 内容消费者的占比分 | -1.0 |
| 三个缺失指示 | 上面三个账号分是否等于 -1.0 | — |

前三个账号分都来自 `source_user.hss.user_health_scores` 命名空间（`pnsfwmedia/model_experimental.py:108-133`）。“粉丝中 NSFW 消费者占比”是根据字段名的解读，源码未包含计算方式。

网络结构为 `BatchNormalization → Dense(50, relu) → Dense(1, sigmoid)`，CLIP 部分冻结，不参与训练（`pnsfwmedia/model_experimental.py:17-48`）。训练使用 Adam 和二元交叉熵，开源默认值为 `batch_size` 32、20 轮、每轮 100 步（`pnsfwmedia/model_experimental.py:136-170`）。模型保存时带 `serve` 签名，可以直接接收 DeepBird 批量预测请求。训练脚本同时把验证集的分数与标签写入 `predictions.csv`（`pnsfwmedia/model_experimental.py:172-188`），格式与 `adult-content/` 校准脚本的输入一致（推测用于后续校准）。

这个模型的作用是：同一张擦边图片发在长期发布成人内容的账号下和发在普通账号下，模型给出的分数不同。账号分的计算见 [账号信号](https://x-algorithm.vergego.ai/concepts/account-signals/)。

## 分数的读取方

1. **media-model-proxy 的调用方**：Media Analysis Service。源码未包含它把模型输出写成媒体更新事件的方式。
2. **scarecrow → botmaker 规则**：`FeaturesOfMediaUpdate` 读取三种媒体更新（`botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:47-173`）：
   - 标签更新：只取 `xx`、`sexy`、`xxnsfw`、`xxnfswv2`、`xxnsfwv2`（前者是源码中的原样拼写，`FeaturesOfMediaUpdate.java:28`）。`xxnsfwv2` 写成 `probabilityNsfwV2` 等四个特征；`xxnsfw` 写成 `probabilityNewNsfw` 等四个特征；`xx` 和 `sexy` 按校准档位写成 `highPrecision*`、`highRecall*`、`nearPerfect*` 三个布尔量和概率，分数高于该档阈值时为真。
   - 预测标签更新：只取 `nsfw_score`。
   - 聚类、MD5 哈希、视频聚类更新：写成聚类 ID、签名向量和哈希列表。
   botmaker 规则使用这些特征，规则打出的标签见 [标签规则](https://x-algorithm.vergego.ai/concepts/labeling-rules/)。
3. **可见性过滤**：可见性过滤规则读取帖子上的 `NsfwHighPrecision`、`GoreAndViolenceHighPrecision` 等标签（`visibility-filtering/rules/tweet_rules.rs:457`、`visibility-filtering/rules/tweet_rules.rs:587`）。Grox PTOS 也可能直接打上这两个标签，见 [Grox 内容分类器](https://x-algorithm.vergego.ai/concepts/grox-classifiers/)。规则对这些标签的具体动作见 [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/)。

开源的 Grox 代码没有调用 media-model-proxy。Grox 的成人图片判定使用另一个服务 safemodel（`grox/flows/ptos/task_safety_ptos_safemodel_sex_nudity.py:22-24`）。

## 设计决策

| 决策 | 理由 |
|------|------|
| 代理层统一读取媒体 | 一次 Blobstore 读取服务多个模型，降低存储与网络开销（`media-model-proxy/README.rst:10-13`） |
| 代理不存储分数 | 各调用方对分数的解释和阈值不同；代理只负责推理 |
| 单个模型失败不影响整个请求 | 七个模型相互独立，部分结果仍有用 |
| 分类器建在共享 CLIP 嵌入上 | 一次嵌入计算可供多个下游分类器复用；新分类器只需训练一个小网络 |
| 输出校准表而非单一阈值 | 下游规则可按需选择高精确率档或高召回率档 |
| 媒体分与账号分联合建模 | 同一媒体在不同账号语境下风险不同；账号历史能弥补单张图片的歧义 |

## FAQ

**Q：这些模型会直接决定帖子是否出现在 For You 中吗？**
A：不会直接决定。模型只产出分数。botmaker 规则或 Grox 把分数转为帖子标签，可见性过滤再按场景决定隐藏、降级还是加警告。

**Q：CLIP 嵌入和 Grox 的多模态帖子嵌入是同一种嵌入吗？**
A：不是。CLIP 嵌入是单张媒体的 256 维向量，用于安全分类。Grox 的嵌入是整条帖子（文字、图片、视频、作者）的向量，用于召回。见 [多模态帖子嵌入器](https://x-algorithm.vergego.ai/concepts/multimodal-embedders/)。

## 源码锚点

- `media-model-proxy/README.rst:7-23` —— 服务定位与调用方
- `media-model-proxy/src/main/thrift/com/twitter/cortex_media_annotator/annotate.thrift:8-78` —— 模型枚举与接口
- `media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/controllers/ProxyController.scala:33-92` —— 读取媒体与并发调用
- `media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/services/ModelsService.scala:38-120` —— 单模型推理与失败过滤
- `media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/model_descriptors/ModelDescriptorRegistry.scala:9-19` —— 七个模型的注册
- `media-model-proxy/src/main/scala/com/twitter/media_understanding/model_proxy/model_descriptors/XXNsfwModelDescriptor.scala:29-84` —— 成人模型输入与五个输出字段
- `media-model-proxy/config/decider.yml:1-59` —— 模型开关与采样率
- `clip/model.py:26-146` —— TwitterCLIP 模型与检查点
- `clip/training.py:143-171` —— 对比学习损失
- `clip/keras_model.py:12-131` —— 生产模型名与 Keras 导出
- `adult-content/training.py:13-85` —— 成人分类器结构与训练
- `adult-content/calibration.py:38-97` —— 保序回归校准
- `pnsfwmedia/model_experimental.py:17-133` —— 媒体加账号联合模型
- `botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:27-173` —— 媒体分数转规则特征

## 相关页面

- [Grox 内容分类器](https://x-algorithm.vergego.ai/concepts/grox-classifiers/) —— 基于大模型的媒体安全判定与 safemodel
- [多模态帖子嵌入器](https://x-algorithm.vergego.ai/concepts/multimodal-embedders/) —— 整条帖子的嵌入
- [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/) —— 读取媒体相关标签的规则
- [标签规则](https://x-algorithm.vergego.ai/concepts/labeling-rules/) —— botmaker 与 scarecrow 如何用媒体特征打标签
- [账号信号](https://x-algorithm.vergego.ai/concepts/account-signals/) —— agatha 校准 NSFW 账号分的来源

Source: https://x-algorithm.vergego.ai/concepts/media-models/index.mdx
