自然语言处理(NLP)任务中,数据预处理的质量直接影响模型性能。即使是最先进的大模型,在未经处理的原始文本上也难以发挥作用。本文将详细介绍 NLP 数据预处理的核心步骤,并基于 MindSpore 框架提供代码实现,帮助你构建完整的预处理流水线。
为什么数据预处理至关重要?
原始文本数据通常存在噪声(如错别字、特殊符号)、格式不一致(如大小写混合)、冗余信息(如重复段落)等问题。预处理的目标是:
- 统一数据格式,降低模型学习难度
- 去除噪声,减少干扰信息
- 将文本转化为模型可理解的数值形式
- 适应特定任务需求(如分类、翻译、摘要)
下面以情感分析任务为例(输入文本,输出正面 / 负面标签),演示完整预处理流程。
数据预处理核心步骤
1. 数据加载与探索
首先需要加载数据并初步探索,了解数据分布(如文本长度、标签分布),为后续处理提供依据。
示例数据格式(CSV):
python
text,label
"这部电影太精彩了!推荐大家去看",1
"剧情拖沓,浪费时间...",0
"演员演技在线,但结局有点仓促",1
...
MindSpore 加载代码:
python
import mindspore.dataset as ds
import pandas as pd
import matplotlib.pyplot as plt
# 加载CSV数据
df = pd.read_csv("reviews.csv")
print(f"数据集规模:{len(df)}条")
print(f"标签分布:{df['label'].value_counts().to_dict()}")
# 可视化文本长度分布
df["text_len"] = df["text"].apply(lambda x: len(x))
plt.hist(df["text_len"], bins=50)
plt.title("文本长度分布")
plt.xlabel("长度")
plt.ylabel("数量")
plt.show()
# 转换为MindSpore数据集
dataset = ds.NumpySlicesDataset(df[["text", "label"]].values, column_names=["text", "label"])
2. 文本清洗
去除无关字符、统一格式,减少噪声干扰。常见操作包括:
- 去除特殊符号、标点
- 大小写转换(通常转为小写)
- 去除多余空格
- 处理中英文混合场景(如保留中文,清理无意义符号)
清洗函数实现:
python
import re
def clean_text(text):
# 去除URL
text = re.sub(r"http\S+", "", text)
# 去除特殊符号和标点(保留中文、字母、数字)
text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s]", "", text)
# 转为小写(英文)
text = text.lower()
# 去除多余空格
text = re.sub(r"\s+", " ", text).strip()
return text
# 应用清洗函数
dataset = dataset.map(operations=lambda x: (clean_text(x[0]), x[1]), input_columns=["text", "label"])
3. 分词(Tokenization)
将连续文本拆分为最小语义单位(词语或子词),是 NLP 的基础步骤。中文通常使用 jieba、THULAC 等工具,英文可直接按空格分词(或使用更复杂的子词分词)。
中文分词实现:
python
import jieba
def tokenize(text):
# 中文分词,过滤空字符串
tokens = [token for token in jieba.cut(text) if token.strip()]
return tokens
# 应用分词
dataset = dataset.map(operations=lambda x: (tokenize(x[0]), x[1]), input_columns=["text", "label"])
示例效果:
- 原始文本:"这部电影太精彩了!推荐大家去看"
- 清洗后:"这部电影太精彩了推荐大家去看"
- 分词后:["这部", "电影", "太", "精彩", "了", "推荐", "大家", "去看"]
4. 去除停用词
停用词是指在文本中频繁出现但语义贡献小的词(如 "的"、"是"、"在"),去除它们可以减少冗余,提升效率。
停用词处理:
python
# 加载停用词表(可自定义或使用公开表)
with open("stopwords.txt", "r", encoding="utf-8") as f:
stopwords = set(f.read().splitlines())
def remove_stopwords(tokens):
return [token for token in tokens if token not in stopwords]
# 应用去停用词
dataset = dataset.map(operations=lambda x: (remove_stopwords(x[0]), x[1]), input_columns=["text", "label"])
示例效果:
- 分词后:["这部", "电影", "太", "精彩", "了", "推荐", "大家", "去看"]
- 去停用词后:["电影", "精彩", "推荐", "大家"]
5. 词表构建与数值映射
模型无法直接处理文本,需要将词语转换为数值。这一步需要:
- 构建词表(记录词语与索引的映射)
- 将分词后的文本转换为索引序列
词表构建实现:
python
from collections import Counter
# 收集所有词,统计频率
all_tokens = []
for data in dataset.create_dict_iterator():
all_tokens.extend(data["text"])
# 按频率排序,保留前5000个词(超参数)
word_counts = Counter(all_tokens).most_common(5000)
# 构建词表:预留0(PAD)、1(UNK)
vocab = {"<PAD>": 0, "<UNK>": 1}
for word, _ in word_counts:
vocab[word] = len(vocab)
print(f"词表大小:{len(vocab)}")
# 保存词表
import json
with open("vocab.json", "w", encoding="utf-8") as f:
json.dump(vocab, f, ensure_ascii=False)
数值映射:
python
def tokens_to_ids(tokens):
# 未在词表中的词用<UNK>代替
return [vocab.get(token, 1) for token in tokens]
# 应用映射
dataset = dataset.map(operations=lambda x: (tokens_to_ids(x[0]), x[1]), input_columns=["text", "label"])
示例效果:
- 去停用词后:["电影", "精彩", "推荐", "大家"]
- 数值映射后:[56, 128, 34, 92](假设词表中对应索引)
6. 序列长度对齐(Padding/Truncation)
模型输入需要固定长度,因此需对序列进行截断(超长)或填充(不足)。
长度对齐实现:
python
import mindspore.dataset.transforms as C
import mindspore.dataset.vision as vision
# 设定最大长度(根据前面的长度分布统计结果)
max_seq_len = 32
def pad_sequence(ids):
# 截断超长序列
if len(ids) > max_seq_len:
return ids[:max_seq_len]
# 填充短序列(用<PAD>的索引0)
else:
return ids + [0] * (max_seq_len - len(ids))
# 应用长度对齐
dataset = dataset.map(operations=lambda x: (pad_sequence(x[0]), x[1]), input_columns=["text", "label"])
7. 数据划分与格式转换
最后将数据集划分为训练集、验证集,并转换为模型可接受的格式(如 Tensor)。
最终处理步骤:
python
# 划分训练集(80%)和验证集(20%)
dataset = dataset.shuffle(buffer_size=len(df)) # 打乱数据
train_size = int(0.8 * len(df))
train_dataset, val_dataset = dataset.split([train_size, len(df) - train_size])
# 转换为Tensor格式
train_dataset = train_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["text"])
train_dataset = train_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["label"])
val_dataset = val_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["text"])
val_dataset = val_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["label"])
# 批量处理(设置batch_size)
train_dataset = train_dataset.batch(batch_size=32)
val_dataset = val_dataset.batch(batch_size=32)
# 查看最终数据格式
for batch in train_dataset.create_dict_iterator():
print("文本张量形状:", batch["text"].shape) # (32, 32)
print("标签张量形状:", batch["label"].shape) # (32,)
break
完整预处理流水线总结
将上述步骤整合,可构建一个可复用的预处理函数:
python
def preprocess_pipeline(file_path, max_seq_len=32, vocab_path=None):
# 1. 加载数据
df = pd.read_csv(file_path)
dataset = ds.NumpySlicesDataset(df[["text", "label"]].values, column_names=["text", "label"])
# 2. 文本清洗
dataset = dataset.map(operations=lambda x: (clean_text(x[0]), x[1]), input_columns=["text", "label"])
# 3. 分词
dataset = dataset.map(operations=lambda x: (tokenize(x[0]), x[1]), input_columns=["text", "label"])
# 4. 去停用词
dataset = dataset.map(operations=lambda x: (remove_stopwords(x[0]), x[1]), input_columns=["text", "label"])
# 5. 词表构建/加载与映射
if vocab_path:
with open(vocab_path, "r", encoding="utf-8") as f:
vocab = json.load(f)
else:
all_tokens = []
for data in dataset.create_dict_iterator():
all_tokens.extend(data["text"])
word_counts = Counter(all_tokens).most_common(5000)
vocab = {"<PAD>": 0, "<UNK>": 1}
for word, _ in word_counts:
vocab[word] = len(vocab)
with open("vocab.json", "w", encoding="utf-8") as f:
json.dump(vocab, f, ensure_ascii=False)
dataset = dataset.map(operations=lambda x: (tokens_to_ids(x[0]), x[1]), input_columns=["text", "label"])
# 6. 长度对齐
dataset = dataset.map(operations=lambda x: (pad_sequence(x[0], max_seq_len), x[1]), input_columns=["text", "label"])
# 7. 划分与转换
dataset = dataset.shuffle(buffer_size=len(df))
train_size = int(0.8 * len(df))
train_dataset, val_dataset = dataset.split([train_size, len(df) - train_size])
train_dataset = train_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["text", "label"]).batch(32)
val_dataset = val_dataset.map(operations=C.TypeCast(mstype.int32), input_columns=["text", "label"]).batch(32)
return train_dataset, val_dataset, vocab
注意事项
- 预处理的任务相关性:不同任务(如机器翻译、命名实体识别)可能需要调整步骤(如翻译需保留句子结构,NER 需保留特殊符号)。
- 超参数调整:max_seq_len、词表大小等需根据数据分布调整,过长会增加计算量,过短会丢失信息。
- 效率优化 :MindSpore 的 map 操作支持多线程加速(通过
num_parallel_workers参数),大规模数据可开启。 - 可复现性:保存词表等中间结果,确保测试 / 部署时使用相同的预处理规则。