AI数据处理实战指南:从清洗到增强

前言:Garbage In, Garbage Out

模型好坏先不说,至少要保证数据本身是干净的、标注是准确的、流程是可复现的。很多时候花在数据处理上的时间比模型训练要多,但这很正常——垃圾进,垃圾出,这点永远不会变。

数据处理的核心挑战:

  • 数据脏:乱码、缺失、重复、格式不统一
  • 标注乱:标签不一致、语义漂移、主观歧义
  • 数据少:小样本场景过拟合严重
  • 验证缺:训练到一半才发现数据问题

一、数据质量问题全景

1.1 典型数据问题

问题类型典型表现影响范围
标签混乱同一实体在不同样本标签不同模型学到混淆概念
语义漂移产品改版后标注标准没跟上数据中存在多套"正确答案"
分布偏移新旧数据类目比例失衡模型在某类目表现异常
隐私泄露手机号、身份证号未脱敏合规风险
重复灌水测试账号生成的垃圾对话浪费训练资源

1.2 分层清洗流程

flowchart TD A[原始数据采集] --> B[格式标准化] B --> C[显性噪声过滤] C --> D[隐私信息脱敏] D --> E[标签一致性检查] E --> F[语义质量评估] F --> G[数据分布分析] G --> H[质量报告与决策]

核心原则:清洗得分层做,指望一步到位不现实。

二、格式标准化

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.5815min
基础增强(600条)88.7%71.5%0.6845min
上下文增强(800条)85.3%76.8%0.7460min
LLM 合成数据(1000条)82.1%81.2%0.7990min

关键发现:

  • 泛化能力从 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 多维度验证

graph LR A[原始数据] --> B[完整性验证] B --> C[准确性验证] C --> D[一致性验证] D --> E[时效性验证] E --> F[生成验证报告]

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 条
    ...

十五、写在最后

数据处理不是最光鲜的工作,但绝对是最关键的环节。模型算法再先进,数据不行也白搭。

几个实在的建议:

  1. 数据到底从哪来,源头质量如何
  2. 先在小规模数据上跑通整个流程,再放大到全量
  3. 建立持续的质量监控机制,而不是一次性清洗
  4. 业务规则、持续监控、人工抽查,三样都得有
  5. 数据增强不能替代真实数据,它只是让现有数据发挥更大价值

技术工具只能帮你走得快一点,坑还得自己盯。先想清楚业务场景,再选择合适的技术方案。

好的 AI 模型从好的数据开始,而好的数据需要认真的处理。


本文整合了 8 篇 AI 数据处理相关文章,涵盖数据清洗、隐私脱敏、标注质量管理、数据增强(图像/文本/LLM合成)、Embedding 向量化、数据验证体系、版本管理等核心技术。

版权声明: 本文首发于 指尖魔法屋-AI数据处理实战指南:从清洗到增强https://blog.thinkmoon.cn/post/ai-data-processing-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!