针对 OpenAI 机器学习设计题(无标注数据)的完整解答:定义新颖性与有用性、质量过滤、大规模去重、为十亿级条目生成嵌入、最近邻新颖性评分、结合主动学习的目标搜索,以及精确率与召回率的度量。

作者:The Forward Deployed 编辑团队已审阅

本文属于 OpenAI 系统设计题库。题目是该轮面试的典型代表,分析与解答为本站原创。

题目描述

语料库包含数十亿条目且没有任何标签。一个目标是从中选出能让模型学到新东西的数据;另一个目标是找出某个特定对象的所有图像,例如叉车或带有某种病灶的 X 光图像。领域变体将任务设定在医疗数据上:从大型临床语料库中挖掘高信号训练样本。

题目刻意写得简略。面试官考察的是你如何把模糊的目标转化为可度量的定义。

澄清问题

  • 对这个团队来说,“新颖”意味着什么? 提出建议:远离现有训练集,且不是任何已选数据的近似副本。请面试官确认。
  • “有用”意味着什么? 质量好,并且与某个目标相关,例如模型薄弱的某项能力。询问目标是什么。
  • 语料库有多大? 练习时假设:10 亿张图像和 50 亿份文本文档。
  • 标注预算有多少? 假设每周有几千条人工标注,专家标注成本高昂。
  • 如何评判成功? 最终以用所选数据训练出的模型来评判。询问是否存在下游评测。
  • 隐私与许可问题? 要询问。对于医疗数据,去标识化是第一位的。

难点在哪里

三个问题叠加在一起。

没有标签,所以一切信号都只是代理指标。“远离训练集”是可度量的,但垃圾数据同样远离一切:损坏的文件、随机噪声和垃圾信息往往是任何语料库中“最新颖”的条目。

规模不允许对每条数据做任何昂贵的处理。10 亿条数据每条哪怕只花一毫秒,单机也要跑十一天。每个环节要么对单条数据足够廉价,要么只在一小部分子集上运行。

而且结果难以验证。你可以检查选出的数据,却很难度量漏掉了什么。

因此核心矛盾是覆盖率与成本的权衡。用精确模型扫描全部数据不可能;廉价的扫描又会漏掉东西。整个设计是一个漏斗:对全部数据用廉价过滤器,对不断缩减的子集做昂贵的检查,在漏斗最窄处使用人工标注。

flowchart TB C[(语料库:数十亿)]:::store --> F1[廉价过滤器:格式、大小、语言、损坏]:::svc F1 --> F2[去重:精确哈希、近似重复]:::svc F2 --> F3[对所有数据嵌入一次]:::svc F3 --> F4[打分:新颖度 x 质量 x 相关性]:::svc F4 --> F5[对顶部切片进行昂贵检查]:::svc F5 --> H[人工审核样本]:::user H --> OUT[(选定集合)]:::store classDef user fill:#e6efec,stroke:#315e55,color:#171717; classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
核心思想。 先把目标定义为可度量的分数,然后构建一个漏斗:对全部数据做廉价处理,对少数数据做昂贵处理,人工放在最后。

关键概念

嵌入作为通用货币

嵌入模型将每个条目映射为一个向量;相似的条目会落在相近的位置。一旦每个条目都有向量,新颖度、去重、聚类和搜索就都变成了向量运算。图文模型将图像和文本嵌入到同一空间,因此可以用文字描述来搜索图像。

近似重复检测

精确重复共享同一个内容哈希。近似重复,例如调整过尺寸的图像或更改了页眉的文档,则需要相似度判断。MinHash 估计文档之间词序列集合的重叠程度,而局部敏感哈希无需比较所有配对即可将可能的重复项分组。对于图像,对嵌入设置余弦相似度阈值即可。

最近邻新颖度

对每个候选,在训练集的嵌入索引中查找其 k 个最近邻。距离很大意味着该候选与模型见过的任何内容都不相似。

主动学习

用少量标签训练一个分类器,用它给所有数据打分,并将它最不确定的案例交给人工。每一轮标签都用在最能提升分类器的地方。

无需完整标签的精确率与召回率

精确率通过给被选中的样本打标签来度量。召回率则需要估计:给未被选中的随机样本打标签,或者植入已知正例并统计有多少被找回。

核心思想。 嵌入一次,然后将向量复用于去重、新颖度、搜索和分类。通过对选中项抽样度量精确率,通过对被拒项抽样度量召回率。

需求

继续阅读之前。 为每个目标写一句输出的定义,精确到可以度量。

1.1 功能需求

  • 过滤掉损坏的、低质量的和不被允许的条目。
  • 去除精确重复和近似重复,包括语料库内部以及与现有训练集之间的重复。
  • 对条目打分:相对于训练集的新颖度、质量以及与目标的相关性。
  • 在大小预算内选出一个多样化的集合。
  • 找出所有包含指定对象的图像,并附带标注的置信度。
  • 为每次选择报告精确率和估计的召回率。

1.2 非功能需求

  • 吞吐量。 用几天而不是几个月处理十亿级条目。
  • 可复现性。 一次筛选可以根据记录的模型版本、阈值和随机种子重新生成。
  • 隐私。 敏感数据在任何人员查看之前都会被过滤或去标识化。
  • 成本。 用于嵌入和评分的 GPU 小时数保持在预算之内。

1.3 约束与属性

可衡量的筛选质量是属性。 无法评估的筛选结果无法被信任。十亿规模下的单条成本是约束。 它决定了漏斗形的结构。

粗略估算

2.1 嵌入图像

如果一块 GPU 每秒嵌入 2,000 张图像,10 亿张图像需要 500,000 GPU 秒,约 139 GPU 小时。在 100 块 GPU 上,实际耗时不到 2 小时。图像解码和从存储读取往往先于 GPU 成为瓶颈;要规划好数据加载,确保 GPU 不空转。

2.2 向量存储

10 亿 × 512 维 × 2 字节 = 约 1 TB。使用乘积量化将每个向量压缩到 64 字节后,可检索索引约为 64 GB,可以放进几台机器的内存中。完整向量保留在磁盘上用于重排序。

2.3 新颖性查询

每个条目都需要对训练集索引执行一次 k 近邻查询。按每台机器每秒 10,000 次查询计算,10 亿次查询在单台机器上需要 100,000 秒,约 28 小时;在 10 台机器上不到 3 小时。

2.4 昂贵的检查

一个目标检测器按每块 GPU 每秒 50 张图像的速度,只能在一小部分数据上运行。检查排名前 1,000 万的候选需要 200,000 GPU 秒,约 56 GPU 小时。如果对全部 10 亿条运行,则需要 5,600 GPU 小时。这个比例正是检测要在廉价的检索之后运行的原因。

关键思想。 对所有数据做嵌入大约需要一百个 GPU 小时;对所有数据运行检测器则需要数千个。先做廉价的,再对幸存者做昂贵的。

API 设计

该系统是一个批量流水线,上面叠加一个查询服务。

POST /v1/selections
  {goal: novelty | object_search, target, budget_items,
   filters: {...}, embedding_model, thresholds}
  -> {selection_id}
GET  /v1/selections/:id
  -> {status, items_selected, precision_estimate, recall_estimate, report_url}

POST /v1/object-search
  {description | example_image_ids[], min_confidence}
  -> {search_id}
GET  /v1/object-search/:id/results?cursor=
  -> [{item_id, score, detector_confidence, label_status}]

POST /v1/labels   {item_id, label, labeler_id, task_id}

数据模型

item (item_id, uri, type, size, content_hash, source, license, ingested_at) quality (item_id, filter_version, passed, reasons[]) embedding (item_id, model_version) -> vector (存储在向量索引 + 磁盘中) dup_cluster (cluster_id, item_id, is_representative) novelty_score (item_id, index_version, knn_mean_distance) relevance_score (item_id, classifier_version, score) selection (selection_id, config_json, created_at) selection_item (selection_id, item_id, rank, reason) label (item_id, task_id, label, labeler_id, created_at)

每个分数都记录产生它的模型或索引版本,因此筛选结果可以被复现。

高层设计

5.1 随机抽样

从语料库中随机选取一个子集。这很廉价且无偏。但它也大多是模型已经知道的内容的重复,外加垃圾数据,因为大多数语料库的内容就是如此。

5.2 修复 1:过滤与去重

用简单的规则和小型分类器移除损坏的文件、极小的图片、无法读取的文本和违规内容。通过哈希去除完全重复项,并用 MinHash 去除文本的近似重复项、用嵌入相似度去除图片的近似重复项。在网络规模的语料库中,仅去重往往就能移除相当大比例的数据。

5.3 修复 2:一次性嵌入所有内容

对筛选后的数据运行一次嵌入流程,将向量存入 ANN 索引。后续每个阶段都复用这些向量。

5.4 修复 3:相对于训练集的新颖度

为现有训练集的嵌入构建一个索引。对每个候选样本,计算其到 k 个最近训练样本的平均距离。距离越大,说明越新颖。

5.5 修复 4:将新颖度与质量和相关性相结合

纯粹的新颖度会选出垃圾数据。将新颖度乘以一个质量分数(由基于标注好坏样本训练的小型分类器给出),再乘以针对目标的相关性分数。然后按多样性进行选择:对高分候选样本进行聚类,并在各聚类间采样,以免某个密集的新主题占满全部预算。

flowchart LR E[Embeddings]:::store --> N[Novelty:<br/>kNN distance to training set]:::new E --> Q[Quality classifier]:::new E --> R[Relevance to target]:::new N --> S[Score = N x Q x R]:::new Q --> S R --> S S --> D[Cluster + sample across clusters]:::new classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717; classDef new fill:#ffffff,stroke:#c4492d,stroke-width:2px,stroke-dasharray:5 3,color:#171717;

5.6 修复 5:进行度量

对选出的数据随机抽样并标注,以衡量精确率。通过抽样被拒绝的样本或植入已知正样本,来估算召回率。分别在有选择和无选择的情况下进行训练,并在下游评测中比较效果。

5.7 组合后的流水线

flowchart TB C[(Corpus)]:::store --> Q1[Quality filters]:::svc --> DD[Exact + near dedup]:::svc DD --> EM[Embedding pass on GPUs]:::svc --> IX[(ANN index)]:::store T[(Training set embeddings)]:::store --> NV[Novelty scoring]:::svc IX --> NV IX --> QS[Quality + relevance scoring]:::svc NV --> SEL[Diverse selection under budget]:::svc QS --> SEL SEL --> LB[Label samples: precision, recall]:::user LB --> REP[(Selection report)]:::store classDef user fill:#e6efec,stroke:#315e55,color:#171717; classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
核心思想。 过滤、去重、一次性嵌入,用新颖度乘以质量乘以相关性打分,按多样性选择,并通过抽样进行度量。

深入探讨

6.1 定义新颖度,使其不会奖励垃圾数据

在继续阅读之前。 你的新颖度分数排名前 1,000 的样本大多是空白图片、故障艺术和垃圾页面。哪里出了问题,你该如何修复?

距离衡量的是陌生程度,而不是价值。最陌生的样本是那些与任何正常数据都不相似的样本,而它们通常是损坏的或毫无价值的。

三项修复协同工作。先运行质量过滤,再计算新颖性,这样垃圾内容永远不会被打分。将新颖性乘以一个学习到的质量分数,该分数基于几千条好与坏样本的标注进行训练。再看密度:一个远离训练集但靠近许多其他候选项的条目,属于一个真实的新话题;而一个远离一切的孤立条目往往是异常值。优先选择密集的新聚类,而不是孤立点。

由人来核查。在任何选择结果发布之前,审核人员会从每个分数区间抽取随机样本进行检查,并不断调整阈值,直到最高区间足够干净。

区分答案的关键:新颖性

Weak 选取最遥远的条目

直接使用原始嵌入距离作为选择分数,从不检查它选出了什么。

Good 新颖性乘以质量

先过滤垃圾内容,再将新颖性乘以质量分数。

Strong 新颖性结合密度与人工审核

还会优先选择密集的新聚类而非孤立异常值,跨聚类采样以保证多样性,并通过人工审核每个分数区间来校准阈值。

6.2 十亿级条目的去重

完全重复:对内容做哈希并按哈希分组。成本低且精确。

近似重复文本:将每个文档切分为词序列,计算一个 MinHash 签名(例如 128 个值),然后使用局部敏感哈希将签名在多个波段中一致的文档归入同一桶。只有同一桶中的文档才会被比较。这样无需将十亿个文档两两比较,就能找出高重叠的文档对。

近似重复图像:先做嵌入,然后用 ANN 索引查找余弦相似度高于阈值的邻居。将它们分组并保留一个代表,通常是分辨率最高的那张。

也要与现有训练集去重。与训练数据几乎重复的条目,按定义就不具备新颖性。

6.3 找出某个物体的所有图像

在继续阅读之前。 团队想要找出所有包含叉车的图像。请梳理整个流程,并说明哪些环节需要人参与。
  • 文本到图像搜索。 用图文模型对“一张叉车的照片”进行嵌入,并在索引中搜索排名前 100 万的图像。这一步只需几秒钟,精度一般。
  • 检测器验证。 对这些候选项运行一个开放词汇目标检测器。它确认叉车是否存在以及位置在哪里。精度大幅提升。
  • 种子标注。 人工标注几百条检测结果,包括困难案例:玩具叉车、图画、托盘搬运车。
  • 在嵌入上训练分类器。 在已存储的图像向量上训练一个小分类器,成本足够低,可以对全部十亿张图像打分。
  • 主动学习。 将分类器最不确定的图像发给标注人员。重新训练。重复这一过程,直到精度和估计的召回率达到目标。
  • 最后一轮。 对分类器打分高于低阈值的所有图像运行检测器,以捕捉文本搜索遗漏的内容。

当目标对象难以用文字描述时,使用示例图片而不仅仅是文本进行搜索会有帮助。将几张示例图片的嵌入向量取平均,然后用它来搜索。

6.4 测量精确率与召回率

精确率:随机抽取 400 个被选中的条目进行标注。其中正确的比例即为精确率的估计值,在 95% 置信水平下,误差范围约为正负 5 个百分点。

召回率更难测量。有两种方法:

  • 对被拒绝的集合抽样。 随机抽取未被选中的条目进行标注,估计漏掉了多少正例。当目标对象很稀少时,大多数样本都会是负例,因此要分层抽样:对分数中等的条目加大抽样比例。
  • 植入已知正例。 在运行之前,在语料库中隐藏一组已标注的正例。被找回的比例即为召回率的估计值。

在每次筛选中,都要连同误差范围一起报告这两个数字。

6.5 下游评估

“有用”的真正检验在于模型。训练两个小模型,一个用基线数据,另一个加入筛选出的数据,然后在针对目标能力的评测上比较它们。如果筛选出的数据没有提升评测结果,那么无论它多么新颖,都是无用的。

6.6 医疗场景的变体

首先要做去标识化处理:去除姓名、日期、病历号和人脸,并将原始数据保存在只有流水线可以读取的受限区域中。使用领域专用的编码器,它们在临床文本和图像上优于通用编码器。标注噪声更高,专家标注的成本也高得多,因此主动学习更为重要:只把专家时间花在不确定的病例上。为满足许可和知情同意要求,要追踪每个条目的来源。

6.7 大规模运行流水线

在继续阅读之前。 嵌入任务要从对象存储中读取十亿张图片。它最先会在哪里遇到瓶颈?你如何让 GPU 保持忙碌?

通常瓶颈在数据加载上。每张图片在被 GPU 处理之前,都必须先被获取、解码和缩放。如果一个 GPU 每秒嵌入 2,000 张图片,加载器就必须每秒为它解码 2,000 张图片,这可能需要许多 CPU 核心。

  • 对输入分片。 将语料库切分成若干分片,比如每个分片 100,000 个条目,并列在清单中。工作节点通过带租约的队列领取分片,因此失败工作节点的分片会被重试。
  • 打包小文件。 数百万个小对象在列举和获取时都很慢。将每个分片的小文件一次性打包成大的归档文件,然后流式读取。
  • CPU 解码,GPU 嵌入。 为每个 GPU 配备足够多的解码工作节点以保持其忙碌,并测量 GPU 利用率。低于 80% 时,就增加解码器。
  • 按分片写入结果。 每个分片的输出是一个包含条目 ID 和向量的文件,采用原子写入。重跑某个分片会覆盖其文件,因此重试是幂等的。
  • 在清单中记录检查点。 任务可以随时停止并恢复。

6.8 筛选中的偏差

按新颖性和学习到的质量分数进行选择,可能会悄悄地使数据产生偏差。基于几千个标签训练的质量分类器,可能会将某些语言、方言或图像风格评为低质量,因为它们在其训练标签中很罕见。新颖性可能会过度选择某个奇怪的聚类。

在你关心的属性上(语言、来源、地区和主题聚类),将所选集合的构成与语料库进行对比衡量。当目标要求平衡时,为每个组设置下限或上限。审查每个组中的样本,而不仅仅是排名靠前的部分。

答案的区分点:执行

弱:单个大任务

一个任务读取所有数据,失败时从零重启。

好:分片且幂等

对语料库分片,将分片租约分配给工作节点,并按分片写入结果。

强:吞吐量与构成

用打包的输入和足够的解码器保持 GPU 满载,对进度做检查点,并审计所选集合的构成是否存在偏差。

变体

7.1 流式语料库

当新数据每天到达时,增量运行该漏斗:过滤、与迄今所见的一切去重、仅对新条目进行嵌入和打分。当训练集变化时重建训练集索引。

7.2 用于语言模型的纯文本数据

同样的漏斗同样适用。质量过滤器使用小模型下的困惑度,以及针对垃圾内容和模板化文本的分类器。近似去重更为重要,因为重复文本会降低训练效果。

7.3 发现罕见的失败案例

反转目标:找出当前模型失败的输入。按模型的不确定性或两个模型之间的分歧对候选进行打分,并将排名靠前的发送给标注人员。

7.4 语料库扩大十倍时

在 100 亿张图像的规模下,嵌入计算约需 1,400 GPU 小时,量化索引约需 640 GB。将 ANN 索引分片到多台机器上,在召回率允许的情况下,针对训练集邻域的样本而非整个集合计算新颖性。去重改为以 LSH 桶为键的分布式任务。

可迁移的模式

这就是基于嵌入的选择漏斗:定义可衡量的分数,把廉价的计算花在所有数据上、昂贵的计算花在少数数据上,把人力放在能提供最多信息的地方,并以诚实的误差范围报告精确率和召回率。同样的漏斗也驱动着内容审核队列、欺诈审查和搜索相关性标注。

回顾:30 秒答案

  • 将新颖和有用定义为分数。 到训练数据的距离,乘以质量,乘以相关性。
  • 漏斗。 对所有数据过滤和去重,一次嵌入计算,对幸存者做昂贵的检查。
  • 不加干预时,新颖性会奖励垃圾。 质量优先,偏好密集的新聚类,审查每个分数区间。
  • 对象搜索就是检索、验证和主动学习。 文本搜索、检测器、标签、分类器,循环往复。
  • 度量。 抽样精确率、估计召回率以及下游模型评估。

测验

+为什么纯粹的 novelty 分数会选出垃圾数据?

损坏的文件、噪声和垃圾信息与正常数据中的任何内容都不同,因此它们在嵌入空间中远离训练集。距离衡量的是陌生程度,而不是价值。

+为什么要把所有内容都嵌入一次并复用这些向量?

嵌入是每个条目上最昂贵的步骤。一旦每个条目都有了向量,去重、novelty、搜索、聚类和分类都可以作为廉价的向量运算来运行,而无需再次接触原始数据。

+MinHash 结合 LSH 如何避免比较每一对文档?

它为每个文档计算一个紧凑的签名,并将签名在部分位置一致的文档分到同一个桶中。只有同一桶中的文档才会被比较,这样无需数十亿次两两比较就能找出可能的近似重复项。

+如何在不标注整个语料库的情况下估计召回率?

对未被选中的条目进行随机分层抽样并标注,估计漏掉的正例数量;或者在运行之前植入已知的正例,然后统计有多少被找回来。

+检验所选数据是否有用的最终测试是什么?

分别使用和不使用所选数据训练模型,并在针对目标能力的评测上比较结果。如果不能提升模型表现的 novelty,就是没有用的。

+对十亿张图片进行嵌入作业时,通常最先在哪里出现瓶颈?

数据加载:在 CPU 上获取、解码和缩放图片。将小文件打包成大档案并增加解码工作进程,可以让 GPU 保持忙碌。

+选择流水线如何产生偏差,你该如何检查?

质量分类器或 novelty 分数可能会偏袒或惩罚在其训练标签中罕见的类别。按语言、来源和主题比较所选数据与语料库的构成,并在需要时设置下限或上限。

资料来源与延伸阅读

NextCoffee-Shop Payments