这一页回答什么
本页说明 X 用哪些专用模型分析图片和视频,内容包括:media-model-proxy 代理哪些模型,每个模型读取什么输入、返回什么;clip/ 训练的图文嵌入如何成为这些分类器的输入;adult-content/ 如何训练和校准成人内容分类器;pnsfwmedia/ 如何把 CLIP 媒体嵌入与账号级分数合并;以及这些分数最终由谁读取。
Grox 中基于大模型的媒体判定(PTOS、safemodel)不在本页范围内,见 Grox 内容分类器。
核心结论
- media-model-proxy 只转发请求,不存储分数:它按 Blobstore 路径读取一次媒体,并发请求多个 DeepBird 模型,再把“模型 → DataRecord 列表”返回给调用方。调用方负责解释和持久化分数(
media-model-proxy/README.rst:7-20)。README 写明当前调用方是 Media Analysis Service,该服务不在仓库中。 - 代理注册了 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开关没有被任何模型引用。 - 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)。 - 成人分类器是建在 CLIP 嵌入上的小网络:
adult-content/在 256 维媒体嵌入上训练“BatchNorm → 128 维 GELU → sigmoid”二分类器。随后它用保序回归把原始分数校准成概率,并输出 200 个阈值下的精确率、召回率、误报率表(adult-content/training.py:13-42、adult-content/calibration.py:72-88)。 - pnsfwmedia 联合媒体与账号做判定:冻结的 CLIP 模型给出媒体分。模型再拼接 agatha 的校准 NSFW 账号分、账号文本 NSFW 分、粉丝 NSFW 分和粉丝数对数,经过一个 50 维隐层输出概率(
pnsfwmedia/model_experimental.py:50-92)。文件名带experimental,源码未包含线上是否使用它的信息。 - botmaker 规则把分数转为标签:开源代码中可见的读取方是 scarecrow 的
FeaturesOfMediaUpdate。它把媒体更新事件中xx、sexy、xxnsfw、xxnsfwv2等标签的概率、精确率、召回率、误报率和校准档位转成 botmaker 规则特征(botmaker/scarecrow_features/src/main/java/com/twitter/scarecrow/features/FeaturesOfMediaUpdate.java:27-112)。规则再打标签,可见性过滤 读取这些标签。
全景
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):
- 请求带字节时,直接使用字节;否则,按路径从 Blobstore 读取一次。两者都没有时,返回错误。
- 对请求中的每个模型并发调用
ModelsService.getPrediction。 - 汇总成响应,并按采样率记录请求日志。采样率
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):
BatchNormalizationDense(128, activation="gelu")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):
- 在 0 到 1 之间均匀选取 200 个阈值(
num_thresholds=200,adult-content/config.py:36)。 - 对每个原始分数,找到它所在的两个相邻阈值之间的区间,用该区间内正样本的比例作为目标值。
- 用保序回归(单调递增,输出截断在 0 到 1)拟合“原始分数 → 正样本比例”。
- 对每个阈值输出一行:阈值、精确率、召回率、误报率、校准后概率,写入
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/ 校准脚本的输入一致(推测用于后续校准)。
这个模型的作用是:同一张擦边图片发在长期发布成人内容的账号下和发在普通账号下,模型给出的分数不同。账号分的计算见 账号信号。
分数的读取方
- media-model-proxy 的调用方:Media Analysis Service。源码未包含它把模型输出写成媒体更新事件的方式。
- 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 规则使用这些特征,规则打出的标签见 标签规则。
- 标签更新:只取
- 可见性过滤:可见性过滤规则读取帖子上的
NsfwHighPrecision、GoreAndViolenceHighPrecision等标签(visibility-filtering/rules/tweet_rules.rs:457、visibility-filtering/rules/tweet_rules.rs:587)。Grox PTOS 也可能直接打上这两个标签,见 Grox 内容分类器。规则对这些标签的具体动作见 可见性过滤。
开源的 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 的嵌入是整条帖子(文字、图片、视频、作者)的向量,用于召回。见 多模态帖子嵌入器。
源码锚点
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 内容分类器 —— 基于大模型的媒体安全判定与 safemodel
- 多模态帖子嵌入器 —— 整条帖子的嵌入
- 可见性过滤 —— 读取媒体相关标签的规则
- 标签规则 —— botmaker 与 scarecrow 如何用媒体特征打标签
- 账号信号 —— agatha 校准 NSFW 账号分的来源