3. 数据加载与预处理:批量数据读取与特征工程
数据加载与预处理,说白了就是推理管线的「第一道关卡」。我见过太多团队,模型选得挺好,推理框架也搭得漂亮,结果数据加载环节成了瓶颈——GPU 空转,CPU 忙死。嗯,这章我们就来聊聊怎么把这关打通。
3.1 批量数据读取:CSV、Parquet、JSON
推理场景下,数据源五花八门。我个人习惯先把数据格式理清楚,再选读取工具。下面是我常用的三种格式和对应的读取方式。
3.1.1 CSV 读取
CSV 是最常见的格式,但坑也不少。我曾经遇到一个项目,CSV 文件里某列混入了换行符,导致 pandas 读取直接崩掉。后来我养成了一个习惯:读取时指定 dtype 和 na_values,避免类型推断带来的意外。
import pandas as pd
# 推荐:指定列类型,避免自动推断
dtype_spec = {
'user_id': 'int32',
'age': 'float32',
'score': 'float32'
}
df = pd.read_csv('data.csv', dtype=dtype_spec, na_values=['', 'NA', 'null'])
print(f"加载完成,共 {len(df)} 行")
chunksize 参数分块读取,避免内存爆炸。我一般设成 10000 行一块。
3.1.2 Parquet 读取
Parquet 是我在推理项目中的首选格式。为什么?因为它列式存储 + 压缩,读取速度比 CSV 快 3-5 倍。你想想看,推理时我们往往只需要部分列,Parquet 能直接跳过无关列,省带宽省内存。
import pyarrow.parquet as pq
# 只读取需要的列,减少 I/O
table = pq.read_table('data.parquet', columns=['feature_1', 'feature_2', 'label'])
df = table.to_pandas()
print(f"Parquet 加载完成,列数:{len(df.columns)}")
use_threads=True 开启多线程,否则单线程读会慢得让你怀疑人生。
3.1.3 JSON 读取
JSON 在日志和 API 场景很常见。但 JSON 嵌套结构多,直接读成 DataFrame 容易出问题。我一般先解析成 Python 字典,再展平。
import json
import pandas as pd
with open('data.json', 'r') as f:
records = [json.loads(line) for line in f] # 每行一个 JSON 对象
# 展平嵌套字段
def flatten(record, parent_key='', sep='_'):
items = []
for k, v in record.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(flatten(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)
flat_records = [flatten(r) for r in records]
df = pd.DataFrame(flat_records)
3.2 数据清洗与标准化
数据清洗,说白了就是「去脏」。我总结了一个三步走策略:缺失值处理 → 异常值过滤 → 标准化。
3.2.1 缺失值处理
推理时遇到缺失值,不能像训练时那样随便填充。我曾经犯过一个错:用均值填充缺失值,结果推理结果全偏了。后来我改用前向填充 + 标记位的方式。
# 前向填充,同时记录哪些位置被填充过
df['feature_1'] = df['feature_1'].fillna(method='ffill')
df['feature_1_was_missing'] = df['feature_1'].isna().astype(int)
3.2.2 异常值过滤
异常值在推理时很要命。我习惯用 IQR(四分位距)方法做初步过滤,但注意不要一刀切——有些业务场景下「异常值」反而是关键信号。
Q1 = df['score'].quantile(0.25)
Q3 = df['score'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常值,而不是直接删除
df['is_outlier'] = (df['score'] < lower_bound) | (df['score'] > upper_bound)
3.2.3 标准化
标准化是推理前的最后一步。我建议用 Z-score 标准化,但均值和标准差必须从训练集计算,推理时直接复用。
# 假设训练集统计量已保存
mean_val = 0.5
std_val = 0.2
df['score_normalized'] = (df['score'] - mean_val) / std_val
3.3 特征工程与向量化
特征工程,说白了就是把原始数据变成模型能「吃」的格式。我把它分成两类:数值特征和类别特征。
3.3.1 数值特征处理
数值特征除了标准化,还可以做分箱或对数变换。比如用户年龄,直接输入模型效果一般,但分箱后效果立竿见影。
import numpy as np
# 对数变换:处理长尾分布
df['amount_log'] = np.log1p(df['amount'])
# 分箱:将连续值离散化
bins = [0, 18, 30, 45, 60, 100]
labels = ['0-18', '19-30', '31-45', '46-60', '60+']
df['age_bin'] = pd.cut(df['age'], bins=bins, labels=labels)
3.3.2 类别特征处理
类别特征我推荐用 Label Encoding 或 Target Encoding。One-Hot 编码在类别数多的时候会爆炸,不推荐。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['city_encoded'] = le.fit_transform(df['city'])
# 注意:推理时要用同一个 LabelEncoder 对象
# 保存 le 到文件,推理时加载
3.3.3 向量化
文本特征需要向量化。我常用 TF-IDF 或 预训练词向量。注意:TF-IDF 的词汇表必须和训练时一致。
from sklearn.feature_extraction.text import TfidfVectorizer
# 假设训练时已拟合的 vectorizer
vectorizer = TfidfVectorizer(max_features=5000)
vectorizer.fit(train_texts) # 训练时拟合
# 推理时直接 transform
X_text = vectorizer.transform(df['text_column'])
3.4 使用 DataLoader 进行批量加载
DataLoader 是批量推理的核心组件。我习惯自己封装一个,支持预取和多线程。
3.4.1 自定义 DataLoader
import torch
from torch.utils.data import Dataset, DataLoader
class InferenceDataset(Dataset):
def __init__(self, df, feature_cols):
self.data = df[feature_cols].values.astype('float32')
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
dataset = InferenceDataset(df, feature_cols=['feature_1', 'feature_2', 'feature_3'])
loader = DataLoader(
dataset,
batch_size=64,
shuffle=False, # 推理时不要 shuffle
num_workers=4, # 多进程加载
pin_memory=True # 加速 GPU 传输
)
num_workers 一般设为 CPU 核心数的一半。设太多反而会因为上下文切换而变慢。
3.4.2 批量推理循环
model.eval()
all_predictions = []
with torch.no_grad():
for batch in loader:
batch = batch.to('cuda')
outputs = model(batch)
all_predictions.append(outputs.cpu().numpy())
predictions = np.concatenate(all_predictions, axis=0)
3.5 本章知识体系
下面这张图概括了数据加载与预处理的完整流程。我把它画成流程图,方便你对照着搭管线。
嗯,数据加载与预处理这块,说白了就是「磨刀不误砍柴工」。管线搭好了,后面推理才能跑得稳。我建议你从 CSV 或 Parquet 开始,先把清洗和标准化跑通,再逐步加上特征工程和 DataLoader。别一上来就想搞全套,容易翻车。
想要更多资料,联系可可