数据基础
数据基础
数据问题很少以数据问题的面貌出现;它们往往表现为训练运行无法提速、加速器闲置,或监控面板各项指标正常而准确率却在下降。为使各章的论述聚焦于系统设计,本附录汇集了用于粗略估算的计算方法和统计工具,将这些症状追溯到可测量的数据瓶颈:传输时间、存储布局成本、漂移诊断以及数值稳定性工具。这里的工具以基础概率论和机器学习系统铁律中的数据项为基础。
如何使用本附录
本附录旨在作为参考资料。当排查“训练缓慢”“加速器利用率低”或“生产环境准确率漂移”需要以最快路径从症状走向测量时,可查阅本附录。
此处的约定遵循全书统一的符号体系(例如,\(B\) 专用于表示批量大小,\(\text{BW}\) 表示带宽)。
- 数据传不动时:从 table 1 和 section 1.1.1 中的传输时间方程入手。
- 加速器供数不足时:参阅 table 2 和 section 1.1.3 中关于布局的讨论。
- 流水线成本激增时:使用 section 1.1.4 中的基本操作,尤其关注连接引发的数据混洗。
- “平均值看起来正常”但用户仍在抱怨时:参阅 section 1.2.1 和会话级尾部概率。
- 准确率悄然漂移时:使用 section 1.2.2 比较完整分布。
尽管生产流水线会遇到种类繁多的格式、编码和存储层,本附录聚焦于支配它们的物理与统计常量。
有了这些参考值,本附录先介绍数据工程的物理约束,再转向维持机器学习系统健康运行的统计监控。从决定 I/O 吞吐量的存储格式,到检测静默故障的漂移指标,这些基础知识都与 数据流水线架构 中的数据流水线和 模型与基础设施监控 中的运行监控直接相关。
数据工程基础
理解硬件约束只是成功的一半;我们还必须组织数据,使其适应这些约束。数据工程将存储层次结构的原理应用于存储格式和流水线设计,确保加速器始终获得充足的数据供给。这一过程首先要认识到,数据具有物理属性——它占据空间,移动需要时间,解析需要能量。
粗略估算:数据引力的物理约束
数据引力1是一个以传输时间计算为依据的比喻。计算能力逐年提升,但光速固定不变,网络带宽也是有限资源。当数据集增长到足够大的规模时,其实际惯性也随之增大——移动数据所需的时间和能量,可能超过将计算移至数据现有位置所需的时间和能量。
将数据量和带宽转换为兼容单位后,理想的线速传输时间为 \(T = D_{\text{vol}}/\text{BW}\)(下文的计算显式换算了十进制字节数与每秒比特数)。对于这里的大规模数据量,延迟可以忽略不计;完整方程见 带宽与延迟。Table 1 给出了下界;协议开销和端点限制会使实际时间增加。
| 数据量 | 1 Gbps(标准广域网) | 10 Gbps(高端广域网) | 100 Gbps(直连) | 卡车(示例) |
|---|---|---|---|---|
| 1 TB | 2.2 hours | 13.3 minutes | 80 seconds | 不适用 |
| 100 TB | 9 days | 22.2 hours | 2.2 hours | 不适用 |
| 1 PB | 3 months | 9 days | 22.2 hours | 2 days |
序列化的成本
即使数据已经抵达机器,序列化开销仍是最后一道障碍。2 如 table 2 所示,许多工程师精心优化加速器内核,却忽略了解码数据的 CPU 开销。解析 JavaScript 对象表示法(JSON)或 CSV 等文本格式极其消耗 CPU 资源,CPU 忙于将字符串转换为浮点数时,加速器往往只能闲置等待。
| 格式 | 解码速度(MB/s) | 相对 CPU 解码成本 | 适用场景 |
|---|---|---|---|
| CSV/JSON | ~100 MB/s | High | 检查/数据交换 |
| Protobuf | ~300 MB/s | Medium | RPC/消息 |
| Parquet | ~1,000 MB/s | Low | 列式存储 |
行式与列式格式
文件格式的选择决定了数据读取方式的“物理特性”。CSV 和 JSON 等行式格式逐条记录存储数据,因此即使只读取 age 字段,也仍需解析每一行。这适合追加式日志,却不适合仅分析选定特征的场景。Parquet 在磁盘上存储压缩列块,而 Arrow 提供列式内存和 IPC 格式。Parquet 读取器可以利用投影下推获取选定列块,两种格式都支持向量化处理。比较 figure 1 中的两种布局,即可理解列式访问为何能避免扫描不必要的字节。
\begin{tikzpicture}[font=\small\sffamily]
\tikzset{
DataBlock/.style={draw=GrayLine, line width=0.8pt, node distance=-0.8pt,
minimum height=8mm, minimum width=24mm, align=center, font=\small\sffamily},
Label/.style={text=TextBlack, font=\small\bfseries\sffamily}
}
\tikzset{mycylinder/.style={cylinder, shape border rotate=90, aspect=1.3, draw, fill=white,
minimum width=25mm,minimum height=11mm,line width=\Linewidth,node distance=-0.15},
pics/data/.style = {
code = {
\pgfkeys{/channel/.cd, #1}
\begin{scope}[local bounding box=STREAMING,scale=\scalefac, every node/.append style={transform shape}]
\node[mycylinder,fill=\filllcolor!50] (A) {};
\node[mycylinder, above=of A,fill=\filllcolor!30] (B) {};
\node[mycylinder, above=of B,fill=\filllcolor!10] (C) {};
\end{scope}
}
}
}
\tikzset{pics/lupa/.style = {
code = {
\pgfkeys{/channel/.cd, #1}
\begin{scope}[local bounding box=BRAIN,scale=\scalefac, every node/.append style={transform shape}]
\coordinate(PO)at(-0.1,0.2);
\node[circle,draw=white,line width=1pt,fill=\filllcirclecolor,minimum size=5mm,inner sep=0pt](LV)at(PO){};
\node[draw=none,rotate=40,rounded corners=2pt,rectangle,minimum width=1.2mm,inner sep=1pt,
fill=\filllcirclecolor,minimum height=6mm,anchor=north]at(PO){};
\node[circle,draw=none,fill=white,minimum size=3.0mm,inner sep=0pt](LM)at(PO){};
\node[font=\tiny\bfseries]at(LM){...};
\end{scope}
}
}
}
\pgfkeys{
/channel/.cd,
Depth/.store in=\Depth,
Height/.store in=\Height,
Width/.store in=\Width,
filllcirclecolor/.store in=\filllcirclecolor,
filllcolor/.store in=\filllcolor,
drawcolor/.store in=\drawcolor,
drawcircle/.store in=\drawcircle,
scalefac/.store in=\scalefac,
Linewidth/.store in=\Linewidth,
picname/.store in=\picname,
filllcolor=BrownLine,
filllcirclecolor=violet!20,
drawcolor=black,
drawcircle=violet,
scalefac=1,
Linewidth=0.5pt,
Depth=1.3,
Height=0.8,
Width=1.1,
picname=C
}
% Row Store
\node[DataBlock, fill=RedFill!60] (r1a) at (0, 2.8) {标识: 1};
\node[DataBlock, fill=cyan!20, right=of r1a] (r1b) {年龄: 25};
\node[DataBlock, fill=green!80!black!20, right=of r1b] (r1c) {位置: 美国};
%
\node[DataBlock, fill=RedFill!60, right=7mm of r1c] (r2a) {标识: 2};
\node[DataBlock, fill=cyan!20, right=of r2a] (r2b) {年龄: 30};
\node[DataBlock, fill=green!80!black!20, right=of r2b] (r2c) {位置: 英国};
%%fitting arrow
\node[draw=none,fill=none, inner ysep=3mm, inner xsep=6mm,fit=(r1a)(r2c),xshift=-3mm](A){};
\coordinate(AL)at($($(A.north west)!0.5!(A.south west)$)+(0.6,0)$);
\coordinate(AD)at($($(A.north east)!0.5!(A.south east)$)+(0.6,0)$);
\scoped[on background layer]
\draw[draw=none,fill=black!10](A.north west)--(A.north east)--(AD)--(A.south east)--(A.south west)--(AL)--cycle;
\node[Label,anchor=south,font=\small\sffamily\bfseries] at (A.north) {行式(CSV)};
\node[anchor=north east]at(A.south east) {扫描读取\textbf{所有字段}};
% Column Store
\node[DataBlock, fill=RedFill!60,below=1.25 of A] (c1a) {标识: 1};
\node[DataBlock, fill=cyan!20, below=of c1a] (c2a) {年龄: 25};
\node[DataBlock, fill=green!80!black!20, below=of c2a] (c3a) {位置: 美国};
\node[DataBlock, fill=RedFill!60, right=of c1a] (c1b) {标识: 2};
\node[DataBlock, fill=cyan!20, below=of c1b] (c2b) {年龄: 30};
\node[DataBlock, fill=green!80!black!20, below=of c2b] (c3b) {位置: 英国};
%%fitting arrow
\node[draw=none,fill=none, inner ysep=0mm, inner xsep=10mm,fit=(c2a)(c2b),xshift=-3mm](B){};
\coordinate(BL)at($($(B.north west)!0.5!(B.south west)$)+(0.4,0)$);
\coordinate(BD)at($($(B.north east)!0.5!(B.south east)$)+(0.4,0)$);
\scoped[on background layer]
\draw[draw=none,fill=black!10](B.north west)--(B.north east)--(BD)--(B.south east)--(B.south west)--(BL)--cycle;
\node[Label,anchor=south,above=2pt of c1a.north east,font=\small\sffamily\bfseries] {列式(Parquet)};
\node[anchor=north west]at(B.south east) {扫描\textbf{\color{cyan}仅读取年龄}};
%icons
\pic[shift={(-1.3,-0.5)}] at (AL){data={scalefac=0.4,picname=1,filllcolor=VioletLine, Linewidth=0.6pt}};
\pic[shift={(-0.8,-0.05)}] at (BL){lupa={scalefac=1,picname=1,filllcirclecolor=VioletLine!60, Linewidth=0.5pt}};
\end{tikzpicture}figure 1 中两种布局的差异,在于有多少无关字节进入训练路径。
Systems Perspective 1.1: 加速器供数不足问题
数据的代数
特征工程将原始记录转换为模型能够从中学习的列,而这种转换通常首先是数据流问题,然后才是建模问题。流水线先缩小样本范围,再选择特征,最后从其他表中补充上下文。这三个步骤对应三种结构化查询语言(SQL)基本操作,其计算成本决定了特征处理任务能否在本地完成、是否会扫描不必要的字节,或是否会演变为网络数据混洗。
- 选择(\(\sigma\)):筛选行(例如,
WHERE age > 30)。- 成本:若通过 B 树索引查找返回 \(K\) 行,则为 \(\mathcal{O}(\log N + K)\);全表扫描则为 \(\mathcal{O}(N)\)。
- 投影(\(\pi\)):选择列(例如,
SELECT age)。- 成本:在列式格式中,包括选定数据块的读取、解码和元数据处理。本行式格式示例为读取一个 4 字节的整数,需要读取 1 KB,其中 99.6 percent 的读取量被浪费。
- 连接(\(\bowtie\)):合并表。这是成本最高的操作。
- 混洗连接:两个表均按键分区,并通过网络交换数据。
- 成本:对两个大小均为 1 TB 的表重新混洗,需要移动约 ~2 TB 的数据;分区和局部性可以减少这一数据量。
- 广播连接:将一个小表发送给所有工作节点。
- 成本:\(S(W-1)\) 字节;这个大小为 \(S\) 字节的表必须能容纳在 \(W\) 个工作节点中的每一个节点上。
- 混洗连接:两个表均按键分区,并通过网络交换数据。
理解数据格式、序列化成本和代数基本操作,可以帮助我们高效地移动数据。然而,即使流水线的工程设计尽善尽美,如果其中承载的数据特性随时间变化,它也可能悄然失效。检测这种变化并量化其影响程度,需要另一套工具:概率与统计。
概率与统计
当数据开始流经流水线后,我们需要数学工具来确保其质量和一致性。概率与统计为监控系统健康状况、检测数据漂移引发的静默故障以及管理不确定性提供了描述语言。
分布与长尾
在系统中,均值往往具有误导性。延迟分布通常呈长尾形态,面向用户的服务通常受高百分位处的行为支配,而非平均响应时间 (Dean and Barroso 2013)。“P99”(第 99 百分位)延迟为 500 ms,意味着至多有 1% 的请求耗时超过 500 ms;当每个会话包含大量请求时,遇到至少一次慢请求的用户比例可能高得多。在大规模场景下(1M 名用户),即使受影响用户的比例仅为 1%,也会影响 10,000 users。
Napkin Math 1.1: 中位数体验
系统洞见:在此模型下,包含 100 个请求的会话发生超出 P99 的事件的概率超过一半。具有相关性的减速可能改变这一概率 (Dean and Barroso 2013)。
度量漂移(散度)
由于分布具有长尾,而最危险的故障往往隐藏其中,“均值偏移”等简单指标并不足够。我们需要能够比较分布整体形状的工具。检测服务阶段的分布(\(P_t\))与训练分布(\(P_0\))之间的漂移,需要度量分布之间的“距离”。
对于离散结果 \(x\),equation 1 将 KL 散度3 表示为用 \(P_0\) 近似 \(P_t\) 所产生的期望额外编码成本 (Kullback and Leibler 1951; Cover and Thomas 2006)。所示形式仅在所有满足 \(P_t(x) > 0\) 的位置均有 \(P_0(x) > 0\) 时才为有限值;因此,生产实现需要显式进行平滑或零值处理。 \[ \mathcal{D}_{\text{KL}}(P_t \lVert P_0) = \sum_x P_t(x) \log \frac{P_t(x)}{P_0(x)} \tag{1}\]
Napkin Math 1.2: 计算示例:使用 KL 散度检测漂移
数学计算:
训练分布 \(P_0\):[0.60, 0.30, 0.10]。服务阶段的分布 \(P_t\):[0.45, 0.40, 0.15]。
\[\begin{gather*} \mathcal{D}_{\text{KL}}(P_t \lVert P_0) = 0.45 \log\frac{0.45}{0.60} + 0.40 \log\frac{0.40}{0.30} + 0.15 \log\frac{0.15}{0.10}\end{gather*}\] \[\begin{gather*} = 0.45 \times (-0.2877) + 0.40 \times (0.2877) + 0.15 \times (0.4055) \\[-1pt] = -0.1295 + 0.1151 + 0.0608 = 0.0464\text{ nats}\end{gather*}\] \[\begin{gather*} \mathcal{D}_{\text{KL}}(P_0 \lVert P_t) = 0.60 \log\frac{0.60}{0.45} + 0.30 \log\frac{0.30}{0.40} + 0.10 \log\frac{0.10}{0.15}\end{gather*}\] \[\begin{gather*} = 0.60 \times 0.2877 + 0.30 \times (-0.2877) + 0.10 \times (-0.4055) \\[-1pt] = 0.1726 + (-0.0863) + (-0.0405) = 0.0458\text{ nats}\end{gather*}\]
注意其非对称性:\(\mathcal{D}_{\text{KL}}(P_0 \lVert P_t) \neq \mathcal{D}_{\text{KL}}(P_t \lVert P_0)\)。总体稳定性指数(PSI) 将其对称化;以下计算使用自然对数:
\[\begin{gather*} \text{PSI} = \sum (P_{0,i} - P_{t,i}) \log\frac{P_{0,i}}{P_{t,i}} = (0.15)(0.2877) + (-0.10)(-0.2877) + (-0.05)(-0.4055) \\[-1pt] = 0.04315 + 0.02877 + 0.02027 = 0.0922 \end{gather*}\]
系统洞见:由于 PSI = 0.0922 < 0.2,该漂移尚未超过此处用于示例的人工复核阈值。恰当的应对方式是继续在监控仪表板上跟踪该特征,并寻找能够相互印证的漂移信号,而不是仅凭这一统计量就进行重新训练。
该阈值仅用于示例:其含义取决于样本量、分箱、平滑和零值处理,而重新训练需要模型质量方面的证据。
KL 散度和 PSI 都建立在更深层的框架——信息论——之上。信息论提供了相应的单位和界限,使这些指标具有理论依据,而非临时拼凑。
面向系统的信息论
一次训练运行可能使用更多样本、运行更长时间,却仍然不再取得改进,原因在于新增数据携带的有用信号过少。此时,瓶颈不仅在于计算或带宽,还在于数据流水线向学习器传递的信息量。信息 Roofline(目的地) 将这一数据质量上限视为诊断约束,而信息论为分析这一约束提供了度量单位。
熵(\(H\))4 是分布中的平均不确定性,定义为 \(H(X) = -\sum p(x) \log p(x)\)。对数的底决定单位:自然对数对应奈特,而 \(\log_2\) 对应比特。只有在固定的有限支撑集上,均匀分布才具有最大熵。这与上文的 KL 散度直接相关:\(\mathcal{D}_{\text{KL}}\) 度量使用错误分布所产生的额外编码成本。
信息密度 在本书中用作一种启发式概念,表示每单位存储所包含的有用学习信号,而非标准的信息论量。
信噪比(SNR) 是信号功率与噪声功率之比。在机器学习语境下,本书以低信噪比定性描述这样一类数据:其中与任务相关的结构相对于噪声较弱;增加计算量无法弥补与任务相关的信号的缺失。
Logit 与数值稳定性
神经网络分类器通常输出一个 logit 向量5 \(\mathbf{z}\)(未归一化的分数)。对于类别索引 \(i\),softmax 在所有类别索引 \(j\) 上对 \(z_i\) 进行归一化,得到概率:
\[ \text{Softmax}(z_i) = \frac{e^{z_i}}{\sum e^{z_j}} \]
问题在于,如果 \(z_i\) 很大(例如 100),指数值 \(e^{z_i}\) 就会超出 FP32、BF16 和 FP16 等常见训练与推理格式的表示范围,发生溢出。FP64 可以表示这一特定值,但生产机器学习内核很少使用 FP64 计算 softmax。解决方法是在对数空间中进行计算:“Log-Sum-Exp”技巧 使我们无需直接计算巨大的指数值,就能计算 \(\log\left(\sum e^{z_j}\right)\),从而保持数值精度。6
下面通过一个简短的计算示例,说明即使 logit 值较大但仍处于实际可能出现的范围内,这一技巧为何在实践中依然重要:
Napkin Math 1.3: Log-sum-exp 的实际应用
不使用该技巧(朴素 softmax): \[\text{ $\exp(100) \approx 2.7 \times 10^{43}$, $\exp(101) \approx 7.3 \times 10^{43}$, $\exp(102) \approx 2.0 \times 10^{44}$}\]
这些数值可以用 FP64 表示,但在 FP32 中会溢出(最大值 \(\approx 3.4 \times 10^{38}\))。对于 FP16(最大值 \(\approx 65{,}504\)),即使 \(e^{12}\) 也会溢出。原始 logit 的绝对值很少达到 100,但较大的中间值确实会出现(例如,未缩放的注意力分数或过度自信的输出);由于 FP16 和 BF16 是标准训练精度,朴素 softmax 的溢出风险出现得足够频繁,因此默认采用数值稳定的形式。
使用该技巧:减去 \(a = \max(\mathbf{z}) =\) 102:
平移后的 logit:\(\mathbf{z} - a =\) \(\lbrack -2, -1, 0 \rbrack\)
指数值:\(\exp(-2) \approx 0.135\)、\(\exp(-1) \approx 0.368\)、\(\exp(0) = 1.0\)
总和 = 1.503。LogSumExp = 102 \(+ \log(1.503) = 102.408\)。
Softmax:\(\lbrack 0.135/1.503,\; 0.368/1.503,\; 1.0/1.503 \rbrack = \lbrack 0.090,\; 0.245,\; 0.665 \rbrack\)
系统洞见:平移后的值经指数运算得到的结果,以及最终的 softmax 概率,均位于 \([0, 1]\) 内,即使使用 FP16 也不存在溢出风险。
这些计算示例共同串联起了从物理数据移动到分布偏移和数值稳定性的完整链条。
总结
本附录中的工具——关注尾部行为的指标、漂移散度、信息论界限和数值稳定性技术——支持对数据相关故障进行定量诊断,而非依赖个别事例进行解释。
Footnotes
数据引力:Dave McCrory 于 2010 年提出这一术语,用来描述大型数据集如何吸引服务和应用向其聚集,正如物理学中大质量物体吸引较小物体一样 (McCrory 2010)。这一类比十分贴切:移动 PB 级数据集所需的“逃逸速度”,往往要以周计的时间来衡量。↩︎
序列化:源自拉丁语 serialis(形成序列)。指将内存中的数据结构转换为字节流以进行存储或传输的过程,以及其逆过程(反序列化)。在机器学习流水线中,序列化格式的选择可能主导端到端训练时间;数据摄取 介绍了尽量降低这一开销的流水线设计策略。↩︎
KL 散度:以 Solomon Kullback 和 Richard Leibler 的名字命名,两人于 1951 年提出了这一概念。它也称为相对熵,用于量化使用针对另一分布优化的编码来编码某一分布的样本时,所需的期望额外信息量;在本书的漂移监控中,标准方向为 \(\mathcal{D}_{\text{KL}}(P_t \lVert P_0)\)。使用自然对数时,如上例所示,单位为奈特;使用以 2 为底的对数时,单位为比特。在生产机器学习中,KL 散度是许多漂移检测指标的理论基础。↩︎
熵:Shannon 于 1948 年采用这一术语来量化信息含量 (Shannon 1948)。从系统角度看,香农熵是平均无损编码长度的下界。实际的前缀码可接近这一下界,而分组码可在渐近意义上逼近它。使用 \(\log_2\) 时,单位为比特;使用自然对数时,单位为奈特。这些界限为压缩比和数据流水线的容量规划提供依据。↩︎
Logit:Joseph Berkson 于 1944 年仿照 probit 创造了这一术语。logit 函数是 logistic(sigmoid)函数的反函数:\(\text{logit}(p) = \log(p/(1-p))\)。在深度学习中,“logits”更宽泛地指应用任何激活函数之前,最后一个线性层的原始、未归一化输出。↩︎
Log-sum-exp:在 PyTorch 中实现为
torch.logsumexp,在 SciPy 中实现为scipy.special.logsumexp。它依赖恒等式 \(\log\left(\sum e^{x_i}\right) = a + \log\left(\sum e^{x_i - a}\right)\),其中 \(a = \max(x_i)\)。平移后的值 \(x_i - a\) 均为 \(\le 0\),确保指数值不会溢出。↩︎