数据工程第一讲-语料清洗与质量评估

张开发
2026/4/13 3:10:33 15 分钟阅读

分享文章

数据工程第一讲-语料清洗与质量评估
数据工程第一讲语料清洗与质量评估做 AI 的同学都知道一句话“Garbage in, garbage out”。但你真的会把控语料质量吗从一个真实的翻车现场说起前段时间我们团队接了一个大模型微调的项目。客户给了一批高质量的行业语料说是从各种权威渠道收集的让我们直接拿去训练就行。我一听挺开心省了不少数据采集的活儿。结果你猜怎么着训练完之后模型效果贼差生成的内容驴唇不对马嘴。我们排查了半天代码没问题、超参没问题最后回头一看语料——好家伙里面混杂着重复内容同一篇文章被爬虫抓了七八遍格式混乱HTML 标签没清干净还有一堆nbsp;和\u3000语言混杂中文里夹杂着大量日文、韩文客户说是权威渠道但源是外文的低质量文本有大量广告词、导航栏文字、版权声明敏感内容居然还有一些敏感词这要上线就完犊子了说白了原始语料就像刚从菜市场买回来的菜看着挺多但得摘、得洗、得切才能下锅。今天咱们就聊聊语料清洗这套活儿该怎么干以及怎么评估洗得干不干净。语料清洗到底要洗什么我刚开始做这行的时候以为语料清洗就是去重 去空。后来发现太天真了实际生产环境里你需要面对的脏数据类型五花八门脏数据类型典型表现危害程度重复数据完全重复、近似重复改个标题内容一样⭐⭐⭐⭐⭐ 严重浪费训练资源格式污染HTML 标签、转义字符、乱码、多余空格⭐⭐⭐ 干扰模型学习语言混杂多语言混杂、繁简混用、方言俚语⭐⭐⭐⭐ 影响语言建模低质内容广告、导航文本、SEO 堆砌、太短/太长⭐⭐⭐⭐ 降低模型输出质量敏感信息隐私数据、有害内容、偏见言论⭐⭐⭐⭐⭐ 合规风险结构问题JSON/XML 解析失败、字段缺失、编码错误⭐⭐⭐ 导致处理中断问题又来了这些东西怎么处理顺序是啥我踩过的一个坑是先过滤再清洗。比如你先按长度过滤结果有些文本是因为带了 HTML 标签才超长标签一删其实长度刚好。所以顺序很重要。我的经验是这套流程原始语料 ↓ 【第一步】格式标准化编码统一、HTML 清洗 ↓ 【第二步】语言检测与筛选确保语言纯度高 ↓ 【第三步】去重精确去重 模糊去重 ↓ 【第四步】质量过滤长度、内容质量、敏感词 ↓ 【第五步】统计与评估看看洗得怎么样 ↓ 干净语料下面咱们一步步来看。第一步格式标准化原始语料从哪来可能是爬虫抓的网页、数据库导出的文件、第三方 API 返回的数据。格式五花八门编码问题有的 UTF-8有的 GBK甚至还有 Latin-1HTML 残留p内容/p这种标签没清转义字符lt;、\n、\u4e2d\u6587Unicode 转义特殊空格全角空格、不间断空格\u00a0、零宽字符这一段的核心思路是先把所有语料转成统一的、干净的纯文本格式。伪代码大概长这样defnormalize_text(raw_text):# 1. 统一编码转 UTF-8处理乱码textconvert_to_utf8(raw_text)# 2. 解码 Unicode 转义把 \u4e2d\u6587 变成中文textdecode_unicode_escape(text)# 3. 清洗 HTML 标签保留正文去掉 p 这种东西textstrip_html_tags(text)# 4. 解码 HTML 实体lt; 变成 nbsp; 变成空格textunescape_html_entities(text)# 5. 规范化空白字符各种奇奇怪怪的空格都换成普通空格textnormalize_whitespace(text)# 6. 去除零宽字符这些看不见的家伙会捣乱textremove_zero_width_chars(text)returntext.strip()关键点提醒编码转换的时候建议用errorsignore或者errorsreplace别因为一个字符解析失败就整段丢掉HTML 清洗可以用 BeautifulSoup 或 lxml但注意有些内容是用code或pre包裹的代码看你要不要保留零宽字符特别阴险比如零宽空格\u200b肉眼看不见但会干扰分词和建模第二步语言检测与筛选如果你的模型只服务中文用户那语料里混进英文、日文、韩文就是噪音。但语言检测没那么简单。比如这段Python 是一门很棒的 programming language适合初学者入门。你说这是中文还是英文其实是混用的。这种在代码教学、技术文档里特别常见。我的做法是设定一个阈值比如中文占比 80% 才保留。deffilter_by_language(text,target_langzh,threshold0.8): 保留目标语言占比超过阈值的文本 # 1. 检测文本中各语言占比lang_ratiosdetect_language_ratios(text)# 返回类似{zh: 0.75, en: 0.20, other: 0.05}# 2. 判断是否达标iflang_ratios.get(target_lang,0)threshold:returnTrue,lang_ratioselse:returnFalse,lang_ratios# 处理流程fordocincorpus:keep,ratiosfilter_by_language(doc[text])ifnotkeep:doc[filter_reason]f语言不达标:{ratios}continue# 跳过这条语料用什么工具langdetect老牌库支持多语言但慢一点fasttextFacebook 开源快且准有预训练的语言识别模型langid轻量速度还行一个小技巧如果你的语料来源比较单一比如都是国内网站可以先抽样看看语言分布别一刀切把有用的混用语料也丢了。第三步去重——精确去重 模糊去重去重是语料清洗里性价比最高的操作。重复数据不仅浪费存储和算力还会导致模型对重复内容过拟合。去重分两层3.1 精确去重Exact Deduplication完全一样的文本直接删掉。这个简单算个 MD5 或 SHA256 哈希就行defexact_deduplicate(corpus):seen_hashesset()unique_docs[]fordocincorpus:# 计算文本哈希注意先标准化再算哈希text_hashhashlib.md5(doc[text].encode()).hexdigest()iftext_hashnotinseen_hashes:seen_hashes.add(text_hash)unique_docs.append(doc)else:doc[filter_reason]精确重复returnunique_docs3.2 模糊去重Fuzzy Deduplication / Near Deduplication更常见的情况是内容几乎一样但改了几个字、换个标题、多了个时间戳。比如“2023年十大科技趋势” vs “2024年十大科技趋势”可能就年份变了“Python入门教程” vs “Python 入门教程”多个空格同一篇新闻被不同网站转载开头结尾加了各自的导语这种用精确匹配搞不定得用相似度检测。常用方案方案 ASimHash 海明距离把文本转成 64 位指纹SimHash海明距离 3 认为是近似重复适合大规模语料可以用 bucket 优化加速方案 BMinHash LSH局部敏感哈希适合找 Jaccard 相似度高的文档对长文本效果好方案 C文本嵌入 向量相似度用 Sentence-BERT 把文本转成向量向量相似度 0.95 认为是重复准但慢适合小批量精筛deffuzzy_deduplicate_minhash(corpus,threshold0.9): 基于 MinHash LSH 的模糊去重 # 1. 为每个文档生成 MinHashsignatures[]fordocincorpus:minhashcompute_minhash(doc[text])# 分词后计算signatures.append((doc,minhash))# 2. LSH 分桶快速找到候选相似对bucketslsh_hashing(signatures,band20,row5)# 3. 在候选对中精确计算 Jaccard 相似度duplicatesset()forbucketinbuckets:fori,doc_ainenumerate(bucket):fordoc_binbucket[i1:]:similarityjaccard_similarity(doc_a[shingles],doc_b[shingles])ifsimilaritythreshold:# 标记为重复保留较长的那个iflen(doc_a[text])len(doc_b[text]):duplicates.add(doc_b[id])else:duplicates.add(doc_a[id])# 4. 过滤掉重复return[docfordocincorpusifdoc[id]notinduplicates]去重这块有个坑阈值设太高漏掉很多重复设太低误伤相似但不同的内容。建议先抽样人工标一批数据找到合适的阈值。第四步质量过滤洗完格式、去完重接下来要判断这条语料值不值得保留。质量过滤的标准因项目而异但通常包括这几个维度4.1 长度过滤太短没信息量太长可能包含太多噪音。deffilter_by_length(text,min_len50,max_len10000):按字符数过滤也可以按词数/Token 数lengthlen(text)iflengthmin_len:returnFalse,太短iflengthmax_len:returnFalse,太长returnTrue,None注意有些高质量内容比如古诗词、代码片段本来就很短不能一刀切。建议根据语料类型设置不同阈值。4.2 内容质量评分判断文本是否有营养。常用方法A. 规则-based停用词比例的、了、是太多可能是 SEO 垃圾文标点符号比例感叹号太多可能是营销号数字/字母占比纯数字列表可能是表格数据重复字符比例啊啊啊啊啊这种B. 模型-based用fastText训练一个文本分类器区分高质量/低质量或者用预训练的 perplexity 模型perplexity 太高说明语言不流畅defquality_score_rules(text):基于规则的简单质量评分score100# 1. 停用词比例检查stopword_ratiocount_stopwords(text)/len(text)ifstopword_ratio0.5:score-30# 2. 重复字符检查ifhas_excessive_repetition(text,threshold0.3):score-40# 3. 乱码检查非中文、英文、标点的字符占比garbage_ratiocount_garbage_chars(text)/len(text)ifgarbage_ratio0.1:score-50# 4. 词汇多样性重复词太多可能是无意义文本unique_ratiolen(set(tokenize(text)))/len(tokenize(text))ifunique_ratio0.3:score-20returnscore# 过滤时ifquality_score_rules(text)60:mark_as_low_quality(doc)4.3 敏感内容过滤这个不用多说合规是底线。隐私数据手机号、身份证号、银行卡号正则匹配敏感词维护敏感词表或者用预训练的有害内容分类器偏见/歧视性别歧视、地域歧视等内容defsensitive_check(text):敏感内容检测risks[]# 1. 隐私信息ifre.search(r\d{11},text):# 手机号risks.append(疑似手机号)ifre.search(r\d{17}[\dXx],text):# 身份证号risks.append(疑似身份证号)# 2. 敏感词匹配matchedsensitive_word_tree.match(text)# 用 AC 自动机加速ifmatched:risks.append(f敏感词:{matched})# 3. 模型检测色情、暴力、政治敏感等ml_risktext_classification_model(text)# 返回各类风险概率ifml_risk[political]0.8:risks.append(政治敏感)returnlen(risks)0,risks第五步质量评估——怎么知道洗得干不干净清洗完之后你得知道洗掉了多少数据洗得对不对有没有误伤剩下的语料质量如何5.1 清洗统计报表每次跑完清洗流程输出一个统计报告defgenerate_cleaning_report(original_corpus,cleaned_corpus,filtered_docs):report{原始数据量:len(original_corpus),清洗后数据量:len(cleaned_corpus),过滤比例:f{(1-len(cleaned_corpus)/len(original_corpus))*100:.2f}%,过滤原因分布:{格式错误:count_by_reason(filtered_docs,format_error),语言不达标:count_by_reason(filtered_docs,language),精确重复:count_by_reason(filtered_docs,exact_dup),模糊重复:count_by_reason(filtered_docs,fuzzy_dup),太短:count_by_reason(filtered_docs,too_short),太长:count_by_reason(filtered_docs,too_long),质量分低:count_by_reason(filtered_docs,low_quality),敏感内容:count_by_reason(filtered_docs,sensitive),},平均长度变化:{清洗前:avg_length(original_corpus),清洗后:avg_length(cleaned_corpus)},语言分布:lang_distribution(cleaned_corpus),}returnreport输出大概是 语料清洗报告 原始数据量: 1,000,000 条 清洗后数据量: 650,000 条 过滤比例: 35.00% 过滤原因分布: - 格式错误: 12,000 (1.2%) - 语言不达标: 45,000 (4.5%) - 精确重复: 150,000 (15.0%) - 模糊重复: 80,000 (8.0%) - 太短: 35,000 (3.5%) - 质量分低: 28,000 (2.8%) - 敏感内容: 10,000 (1.0%) 平均长度变化: 清洗前: 245 字符 清洗后: 312 字符 语言分布: 中文: 98.5% 英文: 1.2% 其他: 0.3% 5.2 人工抽检自动化清洗之后一定要人工抽检。随机抽几百条看看有没有误杀的不该删的删了有没有漏网的该删的没删整体质量是否达标抽检的时候可以按过滤原因分层抽样重点关注那些被过滤的边界案例。5.3 质量评估指标怎么量化语料质量可以计算这些指标指标含义计算方式Perplexity语言流畅度用预训练语言模型计算Distinct-n词汇多样性n-gram 去重后的比例信息熵内容不确定性字符/词级别的熵值主题一致性主题聚焦程度LDA 主题模型困惑度这些指标可以用来对比清洗前后的变化理想情况下Perplexity降低更流畅Distinct-n提高更多样信息熵提高信息量更大完整流程总结好了咱们把这五步走串起来就是一个完整的语料清洗 pipelinedefclean_corpus(raw_corpus,config): 完整语料清洗流程 results{filtered:[],# 被过滤的文档kept:[],# 保留的文档}# 第一步格式标准化fordocinraw_corpus:doc[text]normalize_text(doc[text])# 第二步语言过滤fordocinraw_corpus:keep,infofilter_by_language(doc[text],target_langconfig[target_lang])ifnotkeep:doc[filter_reason]f语言过滤:{info}results[filtered].append(doc)continueresults[kept].append(doc)# 第三步精确去重results[kept]exact_deduplicate(results[kept])# 第四步模糊去重results[kept]fuzzy_deduplicate(results[kept],methodconfig[dedup_method])# 第五步质量过滤final_kept[]fordocinresults[kept]:# 长度过滤ifnotfilter_by_length(doc[text],config[min_len],config[max_len]):doc[filter_reason]长度不达标results[filtered].append(doc)continue# 质量评分scorequality_score_rules(doc[text])ifscoreconfig[quality_threshold]:doc[filter_reason]f质量分低:{score}results[filtered].append(doc)continue# 敏感内容检查is_safe,riskssensitive_check(doc[text])ifnotis_safe:doc[filter_reason]f敏感:{risks}results[filtered].append(doc)continuedoc[quality_score]score final_kept.append(doc)results[kept]final_kept# 生成报告reportgenerate_cleaning_report(raw_corpus,results[kept],results[filtered])returnresults[kept],report写在最后语料清洗是数据工程里最基础也最重要的环节之一。它的目标不是追求 100% 的干净那可能也把有用的数据删光了而是在质量和数量之间找到平衡点。几个实战经验送给大家先采样再清洗别一上来就全量跑先拿几千条试试流程和参数保留中间结果每一步清洗都把过滤掉的数据存下来方便事后复盘版本化管理语料清洗配置也要像代码一样版本管理方便回溯可视化很重要清洗过程中输出日志和报表一眼能看出问题这讲咱们聊了语料清洗的术下一讲咱们来聊聊数据工程的道——语料标注与数据管理。你在语料清洗中踩过哪些坑或者有什么独门绝技欢迎在评论区交流咱们一起进步参考工具库BeautifulSoup - HTML 清洗ftfy - 修复文本编码问题datasketch - MinHash/LSH 去重presidio - 微软开源的隐私数据识别sentence-transformers - 文本向量相似度

更多文章