AI数据处理实战指南:从清洗到增强
前言:Garbage In, Garbage Out
模型好坏先不说,至少要保证数据本身是干净的、标注是准确的、流程是可复现的。很多时候花在数据处理上的时间比模型训练要多,但这很正常——垃圾进,垃圾出,这点永远不会变。
数据处理的核心挑战:
- 数据脏:乱码、缺失、重复、格式不统一
- 标注乱:标签不一致、语义漂移、主观歧义
- 数据少:小样本场景过拟合严重
- 验证缺:训练到一半才发现数据问题
一、数据质量问题全景
1.1 典型数据问题
| 问题类型 | 典型表现 | 影响范围 |
|---|---|---|
| 标签混乱 | 同一实体在不同样本标签不同 | 模型学到混淆概念 |
| 语义漂移 | 产品改版后标注标准没跟上 | 数据中存在多套"正确答案" |
| 分布偏移 | 新旧数据类目比例失衡 | 模型在某类目表现异常 |
| 隐私泄露 | 手机号、身份证号未脱敏 | 合规风险 |
| 重复灌水 | 测试账号生成的垃圾对话 | 浪费训练资源 |
1.2 分层清洗流程
核心原则:清洗得分层做,指望一步到位不现实。
二、格式标准化
2.1 文本标准化
import re
def standardize_text(text):
# 统一换行符
text = text.replace('\r\n', '\n').replace('\r', '\n')
# 去除多余空格
text = re.sub(r'\s+', ' ', text).strip()
# 统一引号风格
text = text.replace('"', '"').replace('"', '"')
text = text.replace(''', "'").replace(''', "'")
return text
关键经验:不要在格式标准化阶段做太激进的清理。
曾经有团队把用户评论里的 emoji 全删了,结果模型对情感极性的判断准确率掉了 5 个点。
2.2 大文件读取
2.3GB 的 JSON 文件直接用 Pandas 读会 OOM。打开一看,是 NDJSON 格式(每行一条 JSON):
import json
data = []
with open('user_dialogues.json', 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
try:
data.append(json.loads(line))
except json.JSONDecodeError:
continue # 坏行隔离
三、显性噪声过滤
3.1 基础过滤规则
def filter_noise_samples(df, text_col='text', min_length=10):
"""过滤明显噪声样本"""
df = df.copy()
# 过滤过短文本
df = df[df[text_col].str.len() >= min_length]
# 过滤重复文本(保留第一个)
df = df.drop_duplicates(subset=[text_col], keep='first')
# 过滤疑似乱码文本
def is_garbage(text):
special_chars = re.findall(r'[^\w\s一-鿿]', text)
ratio = len(special_chars) / len(text) if len(text) > 0 else 0
return ratio > 0.5
df = df[~df[text_col].apply(is_garbage)]
return df.reset_index(drop=True)
3.2 去重的细节
去重看起来简单,但实际场景里有很多细节:
def deduplicate_by_hash(data, time_window=60, max_len=256):
"""
基于内容和时间窗口的去重
time_window: 秒数,在这个时间内的相同内容算重复
max_len: 超过这个长度的文本不进行去重(误伤风险高)
"""
seen = {} # {hash: last_time}
result = []
for item in data:
text = item.get('text', '')
timestamp = item.get('timestamp', 0)
# 只对短文本去重
if len(text) > max_len:
result.append(item)
continue
text_hash = hash(text[:100]) # 只 hash 前 100 个字符
if text_hash in seen:
if timestamp - seen[text_hash] < time_window:
continue # 重复
seen[text_hash] = timestamp
result.append(item)
return result
重要:重复文本不要全部删。 有些重复是有意义的,比如同一个用户在不同时间的重复反馈。
四、隐私信息脱敏
import hashlib
import re
def mask_pii(text):
"""脱敏隐私信息"""
text = str(text)
# 手机号脱敏:前3后4
text = re.sub(
r'1[3-9]\d{9}',
lambda m: m.group(0)[:3] + '****' + m.group(0)[-4:],
text
)
# 身份证号脱敏:前6后4
text = re.sub(
r'\d{17}[\dXx]',
lambda m: m.group(0)[:6] + '********' + m.group(0)[-4:],
text
)
# 邮箱脱敏
text = re.sub(
r'([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
lambda m: m.group(1)[:2] + '***@' + m.group(2),
text
)
# 订单号用哈希替换(保留可追溯性)
text = re.sub(
r'\b\d{18}\b',
lambda m: 'ORDER_' + hashlib.md5(m.group(0).encode()).hexdigest()[:8],
text
)
return text
建议:脱敏时尽量保留一定的可追溯性。 订单号用哈希值替换而不是全删,后续排查问题还能反向定位。
五、标签一致性检查
5.1 朴素方法的性能问题
两两比较的时间复杂度是 O(n²),10 万条数据要跑几小时:
# 朴素版本:慢
from difflib import SequenceMatcher
def check_label_consistency_naive(df):
inconsistent_pairs = []
for i in range(len(df)):
for j in range(i+1, len(df)):
sim = SequenceMatcher(None, df.iloc[i]['text'], df.iloc[j]['text']).ratio()
if sim >= 0.85 and df.iloc[i]['label'] != df.iloc[j]['label']:
inconsistent_pairs.append((i, j, sim))
return inconsistent_pairs
5.2 向量检索加速
用 TF-IDF 或 embedding 做相似度检索,只对候选对做精确验证:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def fast_label_consistency_check(df, text_col='text', label_col='label',
similarity_threshold=0.85, top_k=20):
"""快速标签一致性检查"""
# 计算 TF-IDF 矩阵
vectorizer = TfidfVectorizer(max_features=5000)
tfidf_matrix = vectorizer.fit_transform(df[text_col])
# 计算相似度矩阵
similarity_matrix = cosine_similarity(tfidf_matrix)
# 只检查每行最相似的 top_k 个
inconsistent_pairs = []
for i in range(len(df)):
similar_indices = np.argsort(similarity_matrix[i])[-(top_k+1):-1][::-1]
for j in similar_indices:
similarity = similarity_matrix[i][j]
if similarity >= similarity_threshold and df.iloc[i][label_col] != df.iloc[j][label_col]:
inconsistent_pairs.append({
'index_1': i,
'index_2': j,
'similarity': similarity,
'label_1': df.iloc[i][label_col],
'label_2': df.iloc[j][label_col]
})
return inconsistent_pairs
复杂度从 O(n²) 降到 O(n × k),10 万条数据几分钟就能跑完。
六、数据分布分析
6.1 检查类别不平衡
def analyze_label_distribution(df, label_col='label'):
label_counts = df[label_col].value_counts()
print(f"总样本数: {len(df)}")
print(f"标签类别数: {len(label_counts)}")
print(label_counts)
imbalance_ratio = label_counts.max() / label_counts.min()
if imbalance_ratio > 10:
print(f"警告: 存在严重类别不平衡 (最大/最小 = {imbalance_ratio:.1f})")
return label_counts
重要:不要看到类别不平衡就急着做过采样或欠采样。
业务场景本身不均衡时(比如欺诈检测正常样本远多于欺诈样本),强行采样到 1:1,模型上线后对正常交易的误报率会高得离谱。
6.2 数据漂移监控
class DataQualityMonitor:
def __init__(self, baseline_stats=None):
self.baseline_stats = baseline_stats or {}
def check_drift(self, current_stats, threshold=0.1):
"""检查数据漂移"""
drift_report = []
for key, current_value in current_stats.items():
if key in self.baseline_stats:
baseline_value = self.baseline_stats[key]
if isinstance(current_value, (int, float)):
if baseline_value != 0:
relative_change = abs(current_value - baseline_value) / baseline_value
if relative_change > threshold:
drift_report.append({
'metric': key,
'baseline': baseline_value,
'current': current_value,
'change': f"{relative_change*100:.1f}%"
})
return drift_report
七、标注质量控制
7.1 标注指南的重要性
不要直接把数据和任务丢给标注人员。要写清晰的标注指南:
- 每种分类的定义
- 边界情况的处理
- 正例和反例
例如意图分类标注指南:
意图 A(产品咨询):用户在询问产品功能、价格、使用方法等
- 明确包含:"这个产品多少钱"、"怎么用"、"支持哪些功能"
- 不包含:"你们公司怎么样"、"产品经理是谁"
意图 B(技术支持):用户遇到了具体问题需要帮助解决
- 明确包含:"出错了"、"不行"、"不能正常使用"
- 不包含:"我想了解技术细节"、"技术架构是什么"
7.2 双盲标注与一致性
from sklearn.metrics import cohen_kappa_score
def calculate_agreement(annotations_1, annotations_2):
"""计算两个标注人员的一致性"""
return cohen_kappa_score(annotations_1, annotations_2)
Kappa 系数判断标准:
- > 0.8:一致性很好
- 0.6 - 0.8:可以接受
- < 0.6:需要重新培训标注人员或修改指南
7.3 工具选择
| 工具 | 优点 | 缺点 |
|---|---|---|
| Label Studio | 功能全,界面漂亮 | 学习曲线陡,内存占用高 |
| Prodigy | 主动学习机制 | 商业软件,价格贵 |
| Doccano | 开源轻量 | 批量操作弱 |
小团队推荐 Doccano:
docker run -d --name doccano -p 8000:8000 ghcr.io/doccano/doccano:latest
八、自动化标注
8.1 基于规则的预标注
def rule_based_intent_detection(text):
"""基于规则的意图识别"""
text_lower = text.lower()
if any(kw in text_lower for kw in ['多少钱', '价格', '费用']):
return 'product_inquiry'
if any(kw in text_lower for kw in ['出错', '错误', '不行', '不能用']):
return 'technical_support'
if any(kw in text_lower for kw in ['登录', '注册', '密码']):
return 'account'
return None # 无法确定
这样能把明显样本先筛掉,人工只标注规则覆盖不到的样本。
8.2 预训练模型辅助
from transformers import pipeline
classifier = pipeline("text-classification", model="bert-base-uncased")
def predict_with_confidence(text, threshold=0.8):
result = classifier(text)
confidence = result[0]['score']
if confidence >= threshold:
return result[0]['label'], confidence
else:
return None, confidence
注意:自动标注会有放大错误的风险。 预标注质量本身差时,反而会增加人工审核工作量。
九、数据增强
9.1 何时需要数据增强
| 样本数 | 建议 |
|---|---|
| < 1000 | 先考虑收集更多真实数据 |
| 1000-10000 | 开始考虑增强 |
| > 10000 | 增强更多是为了鲁棒性而非数量 |
9.2 图像增强
基础几何变换:
import torchvision.transforms as transforms
transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(degrees=15, fill=(0, 0, 0)), # 明确填充
transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05),
])
关键经验:
- 旋转后边缘的黑边会被模型当成特征,要明确填充
- 翻转要注意语义:手势识别中左右翻转可能改变含义
- 颜色增强参数要小,组合使用效果更好
- 所有增强应该在归一化之前做
Mixup:
def mixup_data(x, y, alpha=1.0):
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
index = torch.randperm(batch_size)
mixed_x = lam * x + (1 - lam) * x[index]
y_a, y_b = y, y[index]
return mixed_x, y_a, y_b, lam
def mixup_criterion(criterion, pred, y_a, y_b, lam):
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
9.3 文本增强
同义词替换:
from nltk.corpus import wordnet
def synonym_replacement(text, n=2):
words = text.split()
new_words = words.copy()
random_word_list = [w for w in words if wordnet.synsets(w)]
random.shuffle(random_word_list)
num_replaced = 0
for random_word in random_word_list:
synonyms = wordnet.synsets(random_word)
if synonyms:
synonym = synonyms[0].lemmas()[0].name()
new_words = [synonym if w == random_word else w for w in new_words]
num_replaced += 1
if num_replaced >= n:
break
return ' '.join(new_words)
问题: 替换后的句子可能很怪。“这个项目很重要"改成"这个项目很紧要"就有点生硬。
上下文替换(更推荐):
from transformers import pipeline
generator = pipeline("text2text-generation", model="google/flan-t5-small")
def contextual_replacement(text):
prompt = f"Rewrite this sentence with different words but keep the meaning: {text}"
result = generator(prompt, num_return_sequences=3)
return [item['generated_text'] for item in result]
回译:
def back_translate(text, src='zh-cn', temp_lang='en'):
temp_translation = translator.translate(text, src=src, dest=temp_lang).text
back_translation = translator.translate(temp_translation, src=temp_lang, dest=src).text
return back_translation
建议: 在模型训练前批量回译存下来,而不是实时调用。免费 API 有调用限制。
9.4 领域词汇保护
同义词替换可能把"心肌梗死"换成"心脏病”——两者在医疗语境下差别很大。
PROTECTED_TERMS = {
'心肌梗死', '心力衰竭', '高血压', '糖尿病',
'冠状动脉', '心律失常', '脑卒中', '肺炎'
}
def safe_augmentation(text):
tokens = tokenize(text)
protected_positions = [i for i, t in enumerate(tokens) if t in PROTECTED_TERMS]
augmented_tokens = tokens.copy()
for i in range(len(augmented_tokens)):
if i not in protected_positions:
augmented_tokens[i] = augment_word(augmented_tokens[i])
return detokenize(augmented_tokens)
9.5 LLM 合成数据
终极方案:用 GPT-4 生成 synthetic data。
def generate_synthetic_samples(original_samples, n_per_sample=3):
prompt = """
你是一个医疗数据增强助手。请根据以下文本,生成语义相似但表述不同的新文本。
要求:
1. 保持语义准确
2. 改变句式结构
3. 替换同义词
4. 保持专业性和准确性
原始文本:{original_text}
请生成 {n} 个不同的版本:
"""
synthetic_samples = []
for sample in original_samples:
response = client.chat.completions.create(
model="gpt-4",
messages=[{
"role": "user",
"content": prompt.format(original_text=sample, n=n_per_sample)
}],
temperature=0.7
)
synthetic_samples.extend(response.choices[0].message.content.split('\n'))
return synthetic_samples
9.6 增强效果对比
某医疗文本分类项目实测:
| 方案 | 训练集准确率 | 测试集准确率 | F1 | 训练时间 |
|---|---|---|---|---|
| 原始数据(200条) | 95.2% | 62.3% | 0.58 | 15min |
| 基础增强(600条) | 88.7% | 71.5% | 0.68 | 45min |
| 上下文增强(800条) | 85.3% | 76.8% | 0.74 | 60min |
| LLM 合成数据(1000条) | 82.1% | 81.2% | 0.79 | 90min |
关键发现:
- 泛化能力从 62.3% 提升到 81.2%
- 训练集和测试集差距从 33% 缩小到 0.9%
- 过拟合明显缓解
十、Embedding 向量化
10.1 为什么需要向量化
传统文本搜索基于"字面匹配",问题是:
- “怎么安装"和"安装教程"被当成两个不同的问题
- 英文文档和中文查询无法关联
- 相关性排序差
向量 embedding 把文字映射到多维空间,意思相近的文字距离更近。
10.2 文本切分
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list:
"""把文本切分成固定大小的块,保留重叠部分"""
paragraphs = re.split(r'\n\s*\n', text)
chunks = []
current_chunk = ""
for para in paragraphs:
para = para.strip()
if not para:
continue
if len(current_chunk) + len(para) > chunk_size:
if current_chunk:
chunks.append(current_chunk)
current_chunk = current_chunk[-overlap:] + " " + para
else:
current_chunk = current_chunk + " " + para if current_chunk else para
if current_chunk:
chunks.append(current_chunk)
return chunks
参数经验:
- 中文 500-800 字、英文 800-1200 词效果较好
- overlap 设 50-100 个字符,避免切分把意思从中间断开
10.3 生成向量
from openai import OpenAI
import numpy as np
import time
client = OpenAI()
def batch_embeddings(texts, batch_size=10):
"""批量生成向量,处理速率限制"""
embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i+batch_size]
try:
response = client.embeddings.create(
input=batch,
model="text-embedding-3-small" # 1536 维,性价比高
)
embeddings.extend([np.array(d.embedding) for d in response.data])
except Exception as e:
print(f"Batch {i} failed: {e}")
time.sleep(5)
time.sleep(0.1) # 避免触发速率限制
return embeddings
10.4 向量搜索(FAISS)
import faiss
class VectorSearch:
def __init__(self, dimension=1536):
self.dimension = dimension
self.index = faiss.IndexFlatL2(dimension) # L2 距离
self.documents = []
def add(self, vectors, docs):
vectors_array = np.array(vectors, dtype=np.float32) # faiss 要 float32
self.index.add(vectors_array)
self.documents.extend(docs)
def search(self, query_vector, top_k=5):
query_vector = np.array([query_vector], dtype=np.float32)
distances, indices = self.index.search(query_vector, top_k)
results = []
for dist, idx in zip(distances[0], indices[0]):
if idx < len(self.documents):
similarity = 1 / (1 + dist) # L2 距离转相似度
results.append((similarity, self.documents[idx]))
return sorted(results, reverse=True, key=lambda x: x[0])
FAISS 索引类型选择:
IndexFlatL2:精确搜索,简单但慢IndexIVFFlat:聚类后搜索,更快,牺牲一点精度IndexPQ:量化压缩,适合大规模数据
10.5 常见坑
坑一:中英文编码问题
embedding API 要求 UTF-8 编码,某一步用了其他编码会导致相似度异常低,且不报错。
坑二:向量维度不匹配
# text-embedding-3-small 输出 1536 维
dimension = 1536
index = faiss.IndexFlatL2(dimension)
# 维度不匹配会报错
index.add(vector_768d) # Error
坑三:相似度阈值要按场景调
| 场景 | 相似度阈值 |
|---|---|
| 文档搜索 | > 0.7 算相关 |
| 推荐系统 | > 0.85 算高分 |
| 抄袭检测 | > 0.95 算可疑 |
十一、数据验证体系
11.1 多维度验证
11.2 分层验证提升效率
对每一列都做十几项验证,10 万行要跑 20 分钟。改进方案:
def efficient_validation(df, sample_size=10000):
# 第一层:快速检查(全量数据)
quick_checks = {
'missing_values': check_missing_values(df),
'data_size': check_data_size(df)
}
# 第二层:详细检查(抽样数据)
sample_df = df.sample(n=min(sample_size, len(df)))
detailed_checks = {
'data_types': check_data_types(sample_df, expected_types),
'value_ranges': check_value_ranges(sample_df, ranges)
}
return {**quick_checks, **detailed_checks}
11.3 业务规则验证
只做技术验证不够,还要做业务验证:
def order_amount_positive_rule(df):
"""订单金额必须为正数(退货订单除外)"""
violations = df[(df['order_amount'] <= 0) & (df['order_type'] != 'return')]
return violations.index.tolist()
11.4 异常值白名单
def check_anomalies_with_whitelist(df, column, whitelist=None):
"""带白名单的异常值检查"""
if whitelist is None:
whitelist = []
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
anomalies = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
# 过滤掉白名单中的值
filtered_anomalies = anomalies[~df[column].isin(whitelist)]
return {
'total_anomalies': len(anomalies),
'whitelisted_anomalies': len(anomalies) - len(filtered_anomalies),
'real_anomalies': len(filtered_anomalies)
}
教训:异常值不一定是错误值,需要人工确认。 曾经有团队把所有 100 岁以上的用户当异常删了,结果真的有 105 岁的用户。
十二、数据版本管理
12.1 最容易被忽略的一环
数据处理过程中最容易被忽略的就是版本管理。直接在原文件上修改,标注错了想回退都找不到原始数据。
目录结构:
raw_data/ # 原始数据
processed_data/ # 清洗后
user_dialogues_cleaned_v1.json
user_dialogues_cleaned_v2.json
annotated_data/ # 标注后
user_dialogues_annotated_v1.json
12.2 DVC 管理大数据
# 初始化
dvc init
# 追踪数据文件
dvc add data/user_dialogues.json
git add data/.gitignore data/user_dialogues.json.dvc
# 提交
git commit -m "add dataset tracking"
十三、效果验证
13.1 增强效果监控
# 检查增强分布
def check_augmentation_bias(dataset, transform, num_samples=1000):
transformed_labels = []
for i in range(min(num_samples, len(dataset))):
image, label = dataset[i]
transformed = transform(image)
transformed_labels.append(label)
label_counts = Counter(transformed_labels)
original_counts = Counter([dataset[i][1] for i in range(min(num_samples, len(dataset)))])
for label in set(list(label_counts.keys()) + list(original_counts.keys())):
print(f"Label {label}: original={original_counts[label]}, augmented={label_counts[label]}")
13.2 实际项目效果
数据质量改进后的实际收益:
- 训练时间缩短 30%:去掉噪声和重复数据后收敛更快
- 训练中断率从 30% 降到 5%:NaN/inf 问题基本消失
- 准确率提升 15%:相同模型架构下
- 数据验证时间从 2 天缩短到 2 小时
十四、踩过的坑
坑一:过度清洗丢了语义
把所有 URL 全删了,结果用户评价里"参考这个教程 https://xxx"携带的重要上下文没了。
正确做法: 用占位符替换 URL,保留前后上下文。
def sanitize_urls(text):
return re.sub(r'https?://[^\s]+', '[URL]', text)
坑二:依赖自动清洗工具的误判
某工具自动把所有纯数字文本标为"异常数据”,但业务场景里确实有大量 SKU、型号等正常数字编码。
教训:自动化工具可以做参考,但不能完全依赖。
坑三:标签标准化导致的错误传播
把"智能手表"和"运动手表"自动合并了,结果业务逻辑上两者是分开的。
正确做法:标签标准化之前,先找业务方确认语义边界。
坑四:过度增强导致语义偏移
一条原始样本生成 20 条增强样本,模型学到了很多"偏激"表述。
解决方案:控制增强比例。
def smart_augmentation(original_data, augment_ratio=0.3):
"""只对部分数据增强"""
augment_count = int(len(original_data) * augment_ratio)
to_augment = random.sample(original_data, augment_count)
# 每条生成 2-3 条
...
十五、写在最后
数据处理不是最光鲜的工作,但绝对是最关键的环节。模型算法再先进,数据不行也白搭。
几个实在的建议:
- 数据到底从哪来,源头质量如何
- 先在小规模数据上跑通整个流程,再放大到全量
- 建立持续的质量监控机制,而不是一次性清洗
- 业务规则、持续监控、人工抽查,三样都得有
- 数据增强不能替代真实数据,它只是让现有数据发挥更大价值
技术工具只能帮你走得快一点,坑还得自己盯。先想清楚业务场景,再选择合适的技术方案。
好的 AI 模型从好的数据开始,而好的数据需要认真的处理。
本文整合了 8 篇 AI 数据处理相关文章,涵盖数据清洗、隐私脱敏、标注质量管理、数据增强(图像/文本/LLM合成)、Embedding 向量化、数据验证体系、版本管理等核心技术。
版权声明: 本文首发于 指尖魔法屋-AI数据处理实战指南:从清洗到增强(https://blog.thinkmoon.cn/post/ai-data-processing-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。