☰
Transformer 在大数据开发里的入门
2026/11/6 3:49:23 网站建设 项目流程

不讲模型内部(那本已经写过),只讲它作为管道里的一个节点,怎么用、怎么算账、怎么不翻车

这份笔记怎么用

  • 它假设你已经做过 ETL、写过 SQL、会一点 Python,但没在数据管道里接过模型。
  • 每一节的顺序固定:这件事在业务里长什么样 → 代码 → 一个能让你返工的坑。
  • 代码全部只依赖 numpy 与标准库,能直接跑;假模型是为了让你看清“批处理”和“评估”的形状,不是让你跑真模型。
  • 第一、三、六、八章是主干,第四、五章是算账,剩下按需翻。

学完能到什么程度

  • 给你一张百万行的表和一个模型接口,你能说出该切成多大 batch、要多少预算、瓶颈在哪。
  • 别人说“模型上线后准确率 92%”,你会先问是哪一类的 92%、按什么分桶、基线多少。
  • 能在 Spark / 批量任务里正确地把模型挂上去,而不是每行调一次接口。

一、先分清:数据开发用模型,和训模型是两件事

这两件事经常被混在一起谈,导致面试时说不清自己到底负责什么。先画一条线:

数据开发视角(这本笔记)

算法视角

输入

表、日志、文件流

样本、标注、loss

产出

带标签/带向量的新表

权重文件

关心

吞吐、成本、按时产出、可回溯

指标、收敛、泛化

失败的样子

任务没跑完、分区缺失、成本超了

指标不涨、过拟合

典型工具

SQL、Spark、airflow、调度、队列

pytorch、训练框架、GPU 集群

你在这本笔记里站的位置是左边那一列。模型对你来说就是一个函数:吃一段文本、吐一个标签或一个向量。你要保证的是它每天按时、按量、按预算跑完,并且结果可以回溯到“用了哪个版本的模型”。

二、Transformer 会出现在管道里的四个位置

位置

业务长什么样

输出什么

难点

打标 / 分类

百万条日志要分登陆/查询/写入/报错

一个类别 + 置信度

类别漂移、长尾类别

抽取

从工单正文里抠出订单号、金额、时间

结构化字段

幻觉、格式不稳

向量化

把商品/文章/工单变成向量供检索

定长向量

换模型要全量重建

质量检测

找重复、找异常、找乱码

分数或标记

阈值怎么定

备注这四个位置里,只有“抽取”是真的需要生成能力;另外三个都能用很小的模型或 embedding 完成。新手最容易犯的错是:明明是分类任务,却上了一整套生成式调用,成本高十倍、结果还不稳。

三、把模型当成一个 UDF:批处理是第一个分水岭

同一个模型,逐条调用和攒批调用,吞吐能差一个数量级。这不是玄学,是 GPU/向量化单元被闲置的问题。

下面这段是实测(本机 numpy 假模型,384×384 一层,256 行):

import time, numpy as np

W = np.random.randn(384, 384).astype("float32") * 0.02

def infer(x): # x: (batch, 384) -> (batch, 384)

return np.tanh(x @ W)

rows = np.random.randn(256, 384).astype("float32")

t0 = time.perf_counter()

for i in range(len(rows)):

infer(rows[i:i+1]) # 一次一行

per_row_ms = (time.perf_counter() - t0) * 1000 / len(rows)

t0 = time.perf_counter()

infer(rows) # 一次 256 行

batch_row_ms = (time.perf_counter() - t0) * 1000 / len(rows)

实测结果:逐条 0.0115 ms/行,整批 0.0026 ms/行,快了约 4.3 倍。

换成真模型,差距只会更大——因为每条一次调用还会多出一次网络往返、一次调度、一次显存搬运。

3.1 batch 不是越大越好

  • 显存:batch × 序列长度决定激活占用,32 条 × 512 token 大概是一个安全起点。
  • 延迟:batch 越大,单条等待越久。离线任务随便大,在线接口通常控制在 8~32。
  • 尾部:一批里最长的那条决定整批耗时,长短混排时先按长度分桶,能省 30% 以上。

# 按长度分桶,再组批——离线任务的标准做法

buckets = {}

for row in rows:

key = len(row["text"]) // 128 # 每 128 字一桶

buckets.setdefault(key, []).append(row)

for key in sorted(buckets):

for i in range(0, len(buckets[key]), 32):

run_batch(buckets[key][i:i+32])

四、离线批 与 在线推理:两套完全不同的约束

离线批(数据开发的主战场)

在线推理(服务端)

目标

按时产出、单位成本最低

单次延迟、稳定性

可接受的延迟

分钟到小时

100 ms ~ 2 s

重试

整分区重跑,必须幂等

快速失败 + 降级

扩缩容

按队列堆积调并发

按 QPS 预热

失败影响

分区缺失,下游报警

用户看到报错

关键设计

断点、分区覆盖、水位线

超时、限流、缓存

备注最常见的系统性错误:把在线接口那套“一条一请求”直接搬到离线任务里跑一千万行。结果不是慢一点,是慢 50 倍,且中途挂一次全白干。

五、算账:先把三个数算出来,再动手

这一节是全篇最实用的部分。任何一个“给百万行数据接模型”的需求,先算这三个数,再讨论架构。

5.1 token 总量

行数 × 每行平均 token = 总 token

1,000,000 行 × 120 token = 120,000,000 token = 120 M

5.2 成本

120 M token × 单价(举例 ¥1 / 百万 token)= ¥120

如果是每行调两次(一次分类、一次抽取),直接乘 2

备注便宜的地方在这里:分类任务根本不需要生成,用 embedding + 最近类心,成本能降到百分之一。“用大模型做分类”通常是过度设计。

5.3 时长

实测吞吐 T 条/秒(一定要自己压测,不要信文档峰值)

1,000,000 / T = 秒;再乘上并发数

例:T=40 条/秒、并发 4 -> 1000000/40/4 ≈ 6250 秒 ≈ 1.7 小时

如果算出来不对,按这个顺序调

能拿回多少

先加大 batch(在显存允许内)

2~8 倍

按长度分桶,减少 padding

1.3~1.5 倍

分类任务换 embedding,不跑生成

10~100 倍(成本)

加并发 / 多进程

近似线性,直到被接口限流

前置过滤:明显无用的行先扔掉

看数据,常见能砍 20~40%

六、embedding 与向量检索:数据开发要守的三条规矩

embedding 是这段管道里最像“数据”的一环:它产出的是向量列,要入库、要建索引、要重建。

6.1 余弦相似度,自己写一遍就懂了

import numpy as np

def cosine(a, b):

return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))

def search(q, mat, k=3): # mat: (n, d)

qn = q / (np.linalg.norm(q) + 1e-9)

mn = mat / (np.linalg.norm(mat, axis=1, keepdims=True) + 1e-9)

scores = mn @ qn

idx = np.argsort(-scores)[:k]

return idx, scores[idx]

6.2 三条规矩

  • 建库和查询必须同一个模型。换模型 = 全量重建,没有捷径——两个空间的向量不可比,余弦照样能算出 0.99,但那个数没有意义。
  • 维度要写进表结构。384/768/1536 决定存储量:1536 维 float32 是 6 KB/条,一千万条就是 60 GB。
  • 向量要带版本列。model_name + model_version + 建库时间,缺了这三个字段,半年后没人知道这批向量能不能用。

CREATE TABLE doc_vec (

doc_id BIGINT,

model_name TEXT, -- 必须

model_ver TEXT, -- 必须

dim INT, -- 必须

vec BLOB, -- 或 pgvector 的 vector(1536)

built_at TIMESTAMP

);

备注换模型时正确做法:新建一张 doc_vec_v2,双写一段时间,灰度读,跑一次对比评测,确认不劣化再切。直接原地更新是在给自己埋一颗定时炸弹。

七、数据质量:接模型之前先量三件事

模型出问题,一半的根因在数据侧。接之前先跑这三段检查,能省掉后面两天的扯皮。

7.1 长度分布与截断率

import statistics as st

lens = sorted(len(t) for t in texts)

def pct(p): return lens[int(len(lens) * p) - 1]

print("p50", pct(0.5), "p95", pct(0.95), "p99", pct(0.99), "max", lens[-1])

MAX = 512

over = sum(1 for L in lens if L > MAX)

print("截断率", round(over / len(lens) * 100, 1), "%")

截断率超过 5% 就要重新设计切片,不能再假装没看见——被截掉的部分往往是结论所在。

7.2 重复与近重复

# 精确去重:一行

uniq = list({t: t for t in texts})

# 近重复(MinHash 思路):把文本切成 3-gram,Hash 后取最小 k 个

def shingles(t, k=3): return {t[i:i+k] for i in range(len(t)-k+1)}

def sig(t, n=32):

hs = [hash(s) for s in shingles(t)]

return tuple(sorted(hs)[:n])

  • 训练/评测数据被重复样本污染,指标会虚高;这是“离线 95%、上线 70%”的第一嫌疑人。
  • 日志类数据天然大量重复,先去重再打标,标注成本能砍一半。

7.3 类别分布

先数一遍标签分布再动手。如果某一类占 95%,后面所有指标都要按类分开看——总体准确率会骗人。

八、评估:别信“准确率 92%”

这是数据开发接模型时最容易糊弄过去、也最容易被面试官戳穿的一节。

8.1 三段式:基线 → 模型 → 分桶

实测例子(假数据 200 条日志、4 类):关键词规则基线准确率 73.0%,类心模型 77.0%。

注意这个差距:模型只赢基线 6 个多点,这才会是真实项目里的样子。如果有人告诉你模型比规则高 30 个点,先查数据泄漏——通常是把标签写进了特征。

“报错”这一类的混淆矩阵:TP=38、FP=15、FN=12,得精确率 0.717、召回率 0.760、F1 0.738。

precision = TP / (TP + FP) # 说是报错里,真报错的占比

recall = TP / (TP + FN) # 真报错里,被抓出来的占比

f1 = 2 * p * r / (p + r)

备注为什么两个指标都要报:只报准确率时,模型可以“全部预测成多数类”拿到高分;只报召回时,模型可以“全部标成报错”拿满分。P 和 R 一起看,才堵住这两条路。

8.2 必须按桶再切一刀

分桶维度

为什么切

看到什么就该警惕

文本长度

短文本信息不足

短桶指标明显低 -> 补上下文

来源系统

不同系统日志格式差异大

某系统接近 0 -> 规则没覆盖

时间

模型/数据会漂移

最近一周掉点 -> 需要重训或换 prompt

置信度

低置信度是主要错误来源

阈值以下占比高 -> 加人工复核

九、失败模式清单(照着排查)

症状

根因

怎么改

离线好上线崩

训练/评测同分布,线上下个月就漂了

加时间分桶评估 + 定期重跑基线

长文本效果差

被截断,结论在被切掉的后半段

分块 + 汇总,或换长上下文模型

同一输入两次结果不同

温度 > 0 或没固定 seed

分类任务温度设 0

成本突然翻倍

重试放大了调用量,或 prompt 变长

统计调用数与平均 token,设预算告警

任务跑一半挂了全白干

没有断点,重跑从头开始

按分区覆盖写 + 水位线

结果没法回溯

没记录模型版本/prompt 版本

结果表里存 model_ver、prompt_ver

接口超时雪崩

没有限流,失败重试一起打

指数退避 + 抖动 + 并发上限

重复行翻倍

重试写成了 append 而不是覆盖

分区级幂等:先删后写

十、挂到 Spark / 批量任务上的四个注意点

10.1 反例:每行一次调用

# 千万别这么写:一千万行 = 一千万次调用

df.rdd.map(lambda r: infer_one(r["text"])).toDF()

10.2 正例:mapPartitions 攒批

def batch_map(it, size=32):

buf = []

for row in it:

buf.append(row)

if len(buf) == size:

yield from infer_batch(buf) # 一次调 32 条

buf = []

if buf:

yield from infer_batch(buf)

df.rdd.mapPartitions(batch_map).toDF()

10.3 其余三条

  • 闭包里别放客户端对象:它要能被序列化发到 executor,连接池会直接序列化失败。用 mapPartitions 里现建。
  • 写结果用分区覆盖,不要 append。重跑一次 append 一次,第二天数据就翻倍了。
  • 模型版本写进结果表,并且和任务参数一起记到调度日志里——出问题第一句要能答“用的是哪版”。

十一、串起来:一个最小可跑的日志分类管道

把前面几节合起来,就是一个完整的小项目:生成日志 → 规则基线 → 模型 → 评估 → 落库。

import sqlite3, numpy as np

# 1) 落库:结果表结构,版本列不能少

con = sqlite3.connect(":memory:")

con.execute("""CREATE TABLE log_label(

log_id INTEGER, text TEXT, label TEXT,

model_ver TEXT, score REAL)""")

# 2) 批量写入(不要逐条 execute)

rows = [(i, t, p, "v0-baseline", 1.0)

for i, (t, p) in enumerate(zip(texts, preds))]

con.executemany("INSERT INTO log_label VALUES (?,?,?,?,?)", rows)

con.commit()

# 3) 出报表:按标签统计

for lab, cnt in con.execute(

"SELECT label, COUNT(*) FROM log_label GROUP BY label ORDER BY 2 DESC"):

print(lab, cnt)

跑通这一遍,你就有了一个可以往真模型上替换的骨架:把 preds 换成真模型输出,其余一行不改。

十二、面试会被问到的十二个问题

Q 数据开发为什么要懂模型?

A 因为模型是管道里的一个节点。不懂它的吞吐和失败模式,就排不出靠谱的调度。

Q 一个百万行的打标任务,你怎么估时长?

A 先压测单条吞吐,再乘并发,加上重试和排队系数。数字要能当场算出来。

Q 为什么不能每行调一次接口?

A 网络往返和显存搬运占比过高,实测慢一个数量级,且失败重试成本翻倍。

Q batch 多大合适?

A 离线看显存与吞吐,在线看延迟;常见 32 左右,长短混排要先按长度分桶。

Q 准确率 92% 算好吗?

A 先问基线。规则基线可能就 88%,那模型只值 4 个点;再问按类、按桶的分布。

Q 为什么报 F1 不报准确率?

A 类别不平衡时准确率会被多数类支配,F1 同时约束精确率与召回率。

Q 模型升级了,历史数据怎么办?

A 向量要全量重建,标签要标版本;两个版本不能混在一张表里比较。

Q 向量库怎么选?

A 万级用 numpy 暴力算,十万以上再上 FAISS/pgvector,先别急着上专用库。

Q 数据漂移怎么发现?

A 固定一份黄金评测集定期跑;同时监控输入长度分布、类别分布、置信度分布。

Q 任务重跑为什么数据翻倍?

A 写成了 append。要按分区覆盖 + 幂等,重跑多少次结果都一样。

Q 长文本怎么处理?

A 先看截断率;能分块的按语义分块再汇总,不能分块的才考虑长上下文模型。

Q 你怎么保证成本不失控?

A 算 token 总量 × 单价,设日预算告警,统计每次调用的平均 token 与失败重试率。

附:一页速查

公式

要算的

公式

记住的量级

token 总量

行数 × 每行 token

百万行 × 120 = 120 M

成本

token 总量 × 单价

按 ¥1/百万 = 几百元级

时长

行数 / 吞吐 / 并发

40 条/秒 × 4 并发 ≈ 1.7 小时/百万

精确率

TP / (TP+FP)

—

召回率

TP / (TP+FN)

—

F1

2PR / (P+R)

—

余弦

a·b / (|a||b|)

只比方向不比长度

术语

  • UDF:用户自定义函数,模型挂到数据管道上最朴素的形态。
  • batch:一次送进模型的样本数,直接决定吞吐。
  • 分桶:按长度/类别把样本分组,让一批里的样本尽量同质。
  • 幂等:同一批数据重跑多少次,结果都一样。数据任务的底线。
  • 水位线:标记“处理到哪了”,断点续跑的依据。
  • 漂移:线上数据分布和建库/训练时不一样,指标会无声下滑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询