跳过正文
  1. Posts/

训练数据从哪里来:回看 2018 年的一套图片与视频采集系统

·5366 字·11 分钟
目录

起因
#

写训练代码的时候,经常从这里开始:

1dataset = load_dataset(...)
2model = build_model(...)
3train(model, dataset)

dataset 像是一个天然存在的东西。打开一个 benchmark,下载一个压缩包,解压,加载,开始训练。ML 课程、论文、demo 大多也是从这一步开始。

最近整理旧项目记录,翻到 2018 年刚毕业时做的一套图片和视频采集系统。当时我把它理解成一个爬虫项目——写了 40 多个爬虫,对接各种网站,把图片和视频抓回来给标注团队用。

多年以后再看,才觉得当年那个项目真正在解决的问题是:

这个 dataset 到底从哪里来。

当时在做什么
#

2018 年,刚毕业,在一家计算机视觉公司。

公司和有关部门有合作,提供内容审核相关的 CV 能力:鉴黄、鉴恐、暴力内容识别,以及其他图片和视频审核任务。

这些任务都是监督学习。标注团队需要持续获得大量尚未标注的原始图片和视频,进行人工标注,最终供模型训练使用。

原始数据从哪来?从互联网采集。Google 等搜索引擎、社交网站、图片站、视频站,各种数据源。

我当时的角色是这个采集项目的负责人。技术栈是 React + Django + Scrapy + Splash + Selenium + Headless Chrome。最终累计实现了 40 多类爬虫,覆盖搜索引擎、社交网站等不同类型的数据源。

当时我对这件事的理解,更多停留在"怎么把数据抓回来"。

训练数据不是天然存在的
#

多年以后再看,我更倾向于把这类项目理解成一个更大的生产链中的一环。

一个工业级的训练数据集,在进入 dataset = ... 这一行之前,可能已经走过了这样一条链路:

flowchart TD
    Internet["互联网 / 真实世界"]
    Discovery["数据发现"]
    Acquisition["数据采集"]
    Filter["筛选 / 去重"]
    Annotation["人工标注"]
    QA["标注质量控制"]
    Dataset["Dataset"]
    Training["模型训练"]
    Eval["评估 / 线上 Error Analysis"]

    Internet --> Discovery
    Discovery --> Acquisition
    Acquisition --> Filter
    Filter --> Annotation
    Annotation --> QA
    QA --> Dataset
    Dataset --> Training
    Training --> Eval
    Eval -->|"补充数据"| Discovery

    style Acquisition stroke-width:3px

我当时做的采集系统,就在这条链路非常靠前的位置。

它的直接下游是标注团队。没有原材料,标注团队无法生产新的训练数据。没有训练数据,模型迭代无从谈起。

用一句更直接的话说:Dataset is produced, not given。

这不是什么 insight——做过工业 ML 的人大概都知道。但如果一直只在 training 和 inference 这一段工作,确实容易忽略这条链路上游到底发生了什么。

不只是"数据越多越好"
#

没有数据的模型,是无水之源、无本之木。采集系统最直觉的价值也确实是这个:提供足够多的样本。

但样本量只是第一层。

真正决定模型在真实业务中表现的,往往是训练数据是否覆盖了足够多的模式。图片和视频数据的多样性维度很多:

  • 不同来源(搜索引擎、社交平台、图片站、视频站)
  • 不同画质和压缩方式
  • 不同分辨率
  • 不同拍摄角度和光照条件
  • 不同内容风格和场景
  • 边界案例和长尾案例

一个模型即使在已有测试集上效果很好,如果训练数据和测试数据没有覆盖真实业务的分布,上线后仍然会不断暴露问题。

所以爬更多网站的意义,并不只是把数据量从 N 变成 10N。

更重要的是扩大样本空间,补充模型原来没有看到的数据分布。

这是采集系统在整条数据生产链中的第二层价值——不仅提供 quantity,还提供 diversity 和 coverage。

再往下一层是 label quality。对于监督学习,标签本身就是训练信号。所谓 Garbage In, Garbage Out,这里的 garbage 不只是明显标错的样本,还包括:

  • 标注标准模糊,不同标注员理解不一致
  • 正负样本边界不清晰
  • 数据存在系统性 bias
  • 某些关键分布严重缺失
  • 测试集不能代表线上真实分布

模型不会自动修复训练数据里的系统性缺陷。模型效果不仅取决于 architecture,还取决于它到底看到了什么样的数据。

效果提升不总是来自更 fancy 的模型
#

模型结构和训练方法当然重要。但工业 ML 中,效果提升并不等价于"换一个更复杂的模型"。

很多时候更有效的路径是这样的:

flowchart TD
    Deploy["模型上线"]
    Error["发现线上 failure case"]
    Analyze["分析模型在哪种分布上表现差"]
    Collect["采集对应类型的数据"]
    Annotate["人工标注"]
    Retrain["重新训练"]
    Evaluate["评估"]

    Deploy --> Error
    Error --> Analyze
    Analyze --> Collect
    Collect --> Annotate
    Annotate --> Retrain
    Retrain --> Evaluate
    Evaluate --> Deploy

也就是说,模型迭代本身应该是一个 data feedback loop。

在这个循环里,采集系统承担的角色不是"一次性把数据抓完",而是持续地、有针对性地补充新数据。线上 error analysis 发现某类图片识别率低,就去找这类图片的数据源,抓回来给标注团队,标注完重新训练,再上线观察。

这里面涉及的思想——sample diversity、distribution coverage、error-driven data collection——今天会用更成熟的术语来描述,比如 hard case mining。但需要说清楚:这是多年以后回头总结出的认识,不是 2018 年当时已经形成的完整理论体系。当时并没有什么系统化的数据补充策略,更多是凭业务直觉在做:哪里效果不好,就去找对应类型的数据源,写个爬虫抓一批回来。

从 1 个爬虫到 40+ 个
#

如果只需要抓 Google Images,写一个专用脚本可能就够了。

但业务真正需要的不是"把 Google 爬下来",而是持续给标注团队提供足够丰富的数据。这意味着要不断增加新的数据源。

当数据源从 1 个增长到 5 个、10 个、20 个、40 个以上,工程问题发生了变化。

核心问题从:

这个网站应该怎么爬?

变成了:

怎样让下一个网站尽可能容易接入?

这时候才自然产生了 crawler framework 的需求。

一些能力开始在多个爬虫中反复出现:request scheduling、retry、downloading、image/video downloading、JavaScript rendering、browser automation、logging、task management、output normalization。

而单个网站的实现应该只描述自己的差异:URL 从哪里来、如何分页、是否需要 scroll、是否需要 click、图片/视频链接怎么解析、metadata 怎么提取。

这其实是一个通用的 engineering insight:当同一种复杂度开始在多个业务实现中反复出现时,它往往应该从业务代码中下沉到公共层。

Scrapy 的真正价值
#

Scrapy 是这个项目的 crawling foundation。

它的经典组件大致是这样的:

1Scheduler
23Downloader(+ Downloader Middleware)
45Spider
67Item Pipeline

不打算在这里写 Scrapy 教程。想讲的是它背后的 abstraction。

Scrapy 真正有价值的地方在于:把 crawl lifecycle 和 site-specific logic 分开了。

Spider 关心的是:这个站点如何发现和解析数据。从哪个 URL 开始,怎么翻页,怎么从页面里提取目标信息。

Framework 关心的是:request 怎么调度,失败怎么重试,并发怎么控制,数据怎么进入 pipeline。

这种分离,使得接入一个新的数据源时,开发者只需要写一个新的 Spider,描述这个站点的差异逻辑。Scheduling、retry、downloading、pipeline 这些公共能力由 framework 统一处理。

当站点数量从几个增长到几十个时,这种 abstraction 的价值就变得非常明显。每个 Spider 只关心自己的 parsing logic,不需要重新处理 HTTP 连接管理、错误恢复、数据输出这些重复的事情。

为什么需要真正的浏览器
#

传统 web crawler 的 mental model 是这样的:

1URL → HTTP Request → HTML → XPath / CSS Selector → Data

在这个模型里,拿到 HTTP response 就等于拿到了用户看到的网页内容。

但到了 2017-2018 年,很多网站已经大量使用 JavaScript 动态渲染。页面里的内容不在 HTML source 里,而是通过 JavaScript 执行之后才出现在 DOM 中。AJAX 加载、lazy loading、infinite scroll、动态 DOM、click 后才出现的数据、scroll 后才发送的请求——这些在当时已经非常普遍。

于是出现了一个关键变化:

HTTP response 已经不再等于用户真正看到的网页。

甚至可以说:网页已经不仅是一份 document,而逐渐变成了一段需要执行的 application。

要拿到用户真正看到的内容,crawler 需要有能力执行 JavaScript,甚至模拟用户行为。

这就是为什么当时会同时用到 Scrapy、Splash、Selenium 和 Headless Chrome。它们不是随意堆砌的技术栈,每一层解决的是不同层级的问题。

Splash、Selenium、Headless Chrome 各自解决什么
#

Splash
#

Splash 可以理解为 JavaScript Rendering as a Service

1Scrapy
23Splash(执行 JavaScript)
45渲染后的页面
67Scrapy 继续 parse

Scrapy 把请求发给 Splash,Splash 在内部执行 JavaScript,把渲染后的 HTML 返回给 Scrapy。对 Spider 来说,拿到的就是一份已经执行过 JS 的完整页面。

它适合的场景是:页面内容需要执行 JavaScript 之后才出现,但不需要复杂的用户交互。

Selenium
#

Selenium 解决的已经不只是 rendering,而是 browser automation

有些网站的数据获取流程是这样的:

1scroll → wait → find button → click → wait → open detail → continue scrolling

当数据获取行为越来越接近"模拟一个真实用户在浏览器中操作"时,仅仅拿到渲染后的 HTML 已经不够。需要能够控制浏览器执行一系列动作:滚动、等待、点击、输入、切换页面。

Selenium 提供的就是这种 programmable 的浏览器控制能力。

Headless Chrome
#

Headless Chrome 和 Selenium 不在同一个层级。

1Selenium
23WebDriver Protocol
45Chrome / Headless Chrome

Selenium 负责发出控制指令。Headless Chrome 是实际执行网页的 browser runtime——它是一个完整的 Chrome 浏览器,只是没有可见的 GUI 窗口。

从 HTTP Client 到 Programmable Browser
#

把这几层放在一起看,能看到一条演化路径:

flowchart LR
    subgraph layer1 ["静态 HTML"]
        HTTP["HTTP Client
(Scrapy Downloader)"] end subgraph layer2 ["需要执行 JS"] Splash["JS Rendering Service
(Splash)"] end subgraph layer3 ["需要模拟用户行为"] Selenium["Browser Automation
(Selenium + Headless Chrome)"] end layer1 --> layer2 layer2 --> layer3

爬虫系统的 execution model,从 HTTP client 开始,逐渐扩展到了 programmable browser。

这不是因为技术选型的偏好,而是因为 web 本身在变化。当网页从 document 变成 application,crawler 也不得不从 HTTP client 升级到浏览器。

从 Script 到 Platform
#

当 Scrapy + Splash + Selenium + Headless Chrome 构成了 crawling 的底层能力,Django + React 开始承载另一层需求:管理和运营。

Django 和 React 在这个系统里更可能承载的是:任务创建、配置管理、爬虫状态查看、数据源管理、采集结果查看、操作入口。

这里需要区分一件事:用了 React + Django 不等于它就是"平台"。

真正的平台化是:底层复杂度逐渐从使用者的 mental model 里消失。

使用者想要的是"我想增加一个数据源",而不应该每次都去想 Splash worker 怎么启动、Chrome 怎么拉起、Scrapy engine 如何运行。

整个演进可以概括为:

flowchart TD
    Script["Script
针对单个网站的专用脚本"] Framework["Reusable Crawling Framework
公共能力下沉,Spider 只描述差异"] Platform["Data Collection Platform
任务管理、配置、监控、操作入口"] Script --> Framework Framework --> Platform Script -.- S1["关注点:这个网站怎么爬"] Framework -.- S2["关注点:怎么让下一个站点容易接入"] Platform -.- S3["关注点:怎么让非开发者也能使用"]

需求复杂度推动抽象升级。 这条演化路径不是一开始就规划好的,而是随着数据源不断增加、使用者不断变多,自然被推着走到的。

多年以后再看
#

当时我并不会把这套系统称为 ML Infra。它首先就是一个服务于计算机视觉业务的数据采集项目。

但多年以后再看,它让我很早就接触到了机器学习系统里一个经常被忽略的事实:模型只是整条生产链中的一部分。

一个完整的 ML 系统,从数据到模型到线上,大致是这样一条链路:

1Data Acquisition → Annotation → Dataset → Training → Model Management → Inference → Online Feedback

我后来做的工作更多集中在 training、inference、model management、feature/data pipeline、ML platform。而 2018 年那个项目恰好处在整条链路的另一端:训练数据是怎么产生的。

这种联系只是事后看到的。当时更多是在解决眼前的工程问题:这个网站怎么爬、JavaScript 怎么渲染、40 个爬虫怎么管理。并没有一个宏大叙事在指导当时的决策。

结论
#

具体的 Selenium API、Splash 的某个参数、某个 Spider 当初怎么翻页——这些技术细节会随时间失效。我已经记不清大部分了。

但留下来的认识有几条:

Dataset 不是天然存在的。它是一套持续运行的数据生产系统的产物。

数据的价值不仅在 quantity,还在 diversity、coverage 和 label quality。 爬更多网站的意义不是把 N 变成 10N,而是扩大样本空间,补充模型没有看到的数据分布。

工业 ML 的效果优化不等价于 architecture 优化。 很多时候,找到缺失的样本、扩大数据分布、提高标注质量,比换一个更 fancy 的模型更直接地改善业务表现。

当数据源从 1 变成 40+,问题从"写一个爬虫"变成"设计一个扩展机制"。 这是这套系统在工程层面最值得保留的经验。

模型看到的是 tensor。模型工程师看到的是 dataset。但 dataset 再往前,还有采集系统、筛选规则、标注人员、质量控制——远比训练代码复杂得多的一套系统和一群人。

这些年我的工作从数据链路的上游转到了中下游。但回头看,对"训练数据到底从哪里来"这件事最直接的体感,反而来自 2018 年那半年。那 40 多个爬虫各自怎么写的我已经记不清了,但它们让我一直记得一件事:dataset 在进入模型之前,有人在互联网上找到了它们,有人把它们标注出来,有一整套系统把这些环节串在了一起。训练代码里那个不起眼的变量名,从来都不是凭空出现的。

参考资料
#

🏛️ 111qqz 的技术博客 · 15 年博客历史 (2011 - 2026)
发布于 2026-09-10

💡 觉得有启发?欢迎互动交流!

如果你在阅读、编译运行或系统优化中有任何疑问、思考或更好的解法,欢迎在下方发表评论,或通过邮件直接探讨。

本文链接:https://111qqz.com/2026/09/where-training-data-comes-from/ 知识共享署名-非商业性使用 4.0 国际许可 (CC BY-NC 4.0)

相关文章