数据工程:数据采集、清洗、去重、格式转换

做微调这么久,我越来越觉得一个道理:模型的上限由算法决定,但下限由数据决定。你想想看,再牛的模型,喂进去一堆垃圾,吐出来的只能是更精致的垃圾。

这一章,咱们就聊聊怎么把原始数据变成高质量微调数据集。说白了,就是数据工程的四个核心环节:采集、清洗、去重、格式转换。

数据采集:从哪里找数据?

数据采集是第一步,也是最容易被低估的一步。我个人习惯把数据来源分成三类:

  • 公开数据集:比如 Hugging Face Datasets、阿里天池、百度飞桨社区。这些数据已经做过初步清洗,适合快速验证想法。
  • 业务日志:公司内部积累的用户对话、客服记录、操作日志。这类数据最贴近真实场景,但噪声也最大。
  • 爬虫数据:从网页、论坛、文档中抓取。嗯,这里要注意版权问题,别踩红线。

我在项目中遇到过最头疼的事:客户说「我有一亿条客服对话数据」,结果一打开,80% 是「嗯」「好的」「谢谢」这种无效对话。所以采集阶段就要想清楚——你要的是数量,还是质量?

我的建议:优先从业务日志入手。公开数据集太通用,爬虫数据太脏。业务日志虽然也脏,但至少方向是对的。

数据清洗:把脏数据洗干净

数据清洗,说白了就是「去杂质」。我见过太多人拿到数据直接开训,结果模型学了一堆乱码和表情符号。

清洗工作通常包括:

  • 去除 HTML 标签:爬虫数据里经常夹带 <div><span> 这些标签,用正则或 BeautifulSoup 清理掉。
  • 统一编码:UTF-8 是标配。我遇到过 GBK 编码的数据,训练时直接报错,排查了半天。
  • 过滤过短/过长文本:少于 5 个字符的句子基本没意义,超过 2048 token 的文本容易撑爆显存。
  • 去除特殊符号:比如连续的空格、制表符、不可见字符。
import re

def clean_text(text):
    # 去除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 统一空格
    text = re.sub(r'\s+', ' ', text)
    # 去除特殊字符(保留中文、英文、数字、标点)
    text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、:;""''()]', '', text)
    return text.strip()
注意:清洗不要过度。我曾经为了追求「干净」,把数据里的专业术语和英文缩写全删了,结果模型在技术问答场景下表现极差。保留领域特有的表达方式,很重要。

数据去重:别让模型学重复了

为什么要去重?你想想看,如果训练数据里同一句话出现了一万次,模型会认为这句话特别重要,甚至把它当成「真理」来记忆。这不是学习,这是死记硬背。

去重的方法我常用两种:

  • 精确去重:直接用哈希值比对,完全相同的文本只保留一份。适合短文本。
  • 模糊去重:用 MinHash 或 SimHash 算法,计算文本相似度。适合长文本或语义相近但表述不同的数据。

我记得有一次做客服对话微调,数据里大量重复的「你好,请问有什么可以帮您?」。去重之后,数据量从 50 万降到 12 万,但模型效果反而提升了 15%。少即是多,这话在数据工程里特别对。

from datasketch import MinHash, MinHashLSH

def deduplicate(texts, threshold=0.8):
    lsh = MinHashLSH(threshold=threshold, num_perm=128)
    unique_texts = []
    
    for i, text in enumerate(texts):
        m = MinHash(num_perm=128)
        for d in set(text.split()):
            m.update(d.encode('utf8'))
        
        # 检查是否与已有数据相似
        if not lsh.query(m):
            lsh.insert(i, m)
            unique_texts.append(text)
    
    return unique_texts
核心原则:去重不是删数据,而是让数据分布更均匀。保留多样性,去掉冗余。

格式转换:JSON 还是 Parquet?

数据清洗干净了,接下来要转换成模型能吃的格式。目前主流的有两种:

格式 优点 缺点 适用场景
JSON / JSONL 可读性强,调试方便,生态好 文件大,解析慢 小规模数据、快速实验
Parquet 压缩率高,读取快,支持列式存储 可读性差,需要额外库 大规模数据、生产环境

我个人习惯:实验阶段用 JSONL,上线阶段转 Parquet。为什么?JSONL 每行一个样本,用 json.loads() 就能读,调试起来特别方便。但数据量上了百万级,JSONL 的 I/O 开销会让你怀疑人生。这时候 Parquet 的优势就出来了——读取速度能快 5-10 倍。

# JSONL 格式示例
{"instruction": "解释什么是微调", "output": "微调是指在预训练模型基础上,用特定领域数据继续训练..."}
{"instruction": "什么是过拟合", "output": "过拟合是指模型在训练数据上表现好,但在新数据上表现差..."}

# 转 Parquet
import pandas as pd

df = pd.read_json('data.jsonl', lines=True)
df.to_parquet('data.parquet', compression='snappy')
一个小技巧:如果你用 Hugging Face 的 datasets 库,它原生支持 JSONL 和 Parquet,而且会自动做缓存。我建议直接用 datasets.load_dataset(),省去自己写数据加载的麻烦。

构建高质量微调数据集

最后,咱们聊聊怎么把上面这些环节串起来,构建一个真正能用的数据集。

我的经验是:先小后大,先手工后自动。别一上来就想搞百万级数据集。先拿几百条数据,手工清洗、标注、验证,跑一轮微调看看效果。效果好了,再扩大规模。

具体步骤:

  1. 定义数据格式:确定输入字段(如 instruction、input、output),保持统一。
  2. 采集原始数据:从业务日志或公开数据集中获取。
  3. 清洗与去重:用上面提到的方法,去掉噪声和重复。
  4. 格式转换:输出为 JSONL 或 Parquet。
  5. 质量抽检:随机抽取 5%-10% 的数据,人工检查。我曾经抽检时发现 30% 的数据标签是错的,还好及时发现,不然模型就学歪了。
避坑指南:千万不要把所有数据一股脑丢进去训练。我曾经犯过这个错——把清洗后的 100 万条数据直接喂给模型,结果训练了三天,效果还不如用 10 万条精心筛选的数据。数据质量 > 数据数量,这是铁律。

好了,数据工程这部分就聊到这儿。下一章咱们会讲怎么用这些数据做微调训练,包括学习率设置、batch size 选择这些实战细节。到时候见。