📝 特征工程 · 文本NLP

文本NLP数据 · 特征工程与数据处理全流程

把「人能读的字」变成「模型能算的数」。本页聚焦分词 → 向量化 → 词嵌入 → 序列编码 → 手工特征等「处理与变换」环节,带你理解每种表示的原理、适用场景和常见坑。

⚠️ 乱码/短句/重复/广告/符号/标准化等「脏数据检测与清洗」见 《文本NLP清洗全流程》,本页不再重复。

① 分词(Tokenization)

分词是文本特征工程的第 0 步:中文没有空格,必须先把句子切成「词」才能统计、向量化。英文看似按空格切就行,但 subword(子词) 分词(BPE / WordPiece)才是现代 NLP 的主流——它能解决 OOV(未登录词),还能让同词根的词共享向量。

🇨🇳 中文分词

jieba 最常用,三种模式:精确(默认,不重叠)、全模式(所有可能词,重叠)、搜索引擎模式(再细分长词)。另有 HanLP、pkuseg 支持领域分词。

🇬🇧 英文分词

空格切分最简单,但会把 don't、playing 当成整体。WordPunct 拆标点;BPE/WordPiece 拆成子词(play+##ing),共享词根。

🧩 子词的意义

词表大小可控(几万即可覆盖任意文本),未登录词由已知子词拼出,解决 OOV;同词根词共享子词向量,语义也共享。BERT/GPT 都用子词。

# 中文分词:jieba 三种模式
import jieba

text = "自然语言处理很有趣"

# 精确模式:最常用,按最合理切分(不重叠)
print(jieba.lcut(text, cut_all=False))
# ['自然语言', '处理', '很', '有趣']

# 全模式:把所有可能的词都切出来(会重叠),适合搜索引擎索引
print(jieba.lcut(text, cut_all=True))
# ['自然', '自然语言', '语言', '处理', '很', '有趣']

# 搜索引擎模式:在精确基础上再细分长词,粒度更细
print(jieba.lcut_for_search(text))
# ['自然', '自然语言', '语言', '处理', '很', '有趣']

# 英文 / 子词分词:HuggingFace 的 WordPiece(BERT 用)
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-chinese")
print(tok.tokenize(text))   # 字级切分:['自','然','语','言','处','理','很','有','趣']

🧪交互:选分词模式,实时看切分结果

鼠标悬停词胶囊查看 token id(本页用内置词典做示意性切分,真实项目请用 jieba):

② 离散统计向量(BoW / TF-IDF)

最朴素的向量化:把文本变成「词 → 数字」的稀疏向量。词袋模型(BoW)只数词频;TF-IDF 在词频基础上乘以 IDF,降低高频虚词("是""的")权重、突出区分性词。它们丢失词序、是稀疏离散表示,适合短文本分类基线。

公式:TF-IDF(t,d) = TF(t,d) × IDF(t)
其中 IDF(t) = log( (1+N) / (1+DF(t)) ) + 1,N=文档总数,DF=出现该词的文档数。
n-gram:把相邻 n 个词当成一个特征(如「机器_学习」),能捕捉短语,弥补 BoW 丢词序的问题。
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

docs = ["自然语言处理很有趣", "机器学习也很有趣", "自然语言和机器学习都是人工智能"]

# 词袋模型 BoW:统计每个词出现次数,得到稀疏矩阵
cv = CountVectorizer()
bow = cv.fit_transform(docs)
print(cv.get_feature_names_out())   # 词表
print(bow.toarray())               # 文档 × 词 的计数矩阵

# TF-IDF:降低高频词权重,突出区分性词
tfidf = TfidfVectorizer()
m = tfidf.fit_transform(docs)
print(m.toarray())                 # TF-IDF 权重矩阵

# n-gram:1-gram + 2-gram,捕捉 "机器_学习" 这类短语
tfidf2 = TfidfVectorizer(ngram_range=(1, 2))
tfidf2.fit_transform(docs)

🧪交互:输入一段文本,看 BoW 词频 + TF-IDF

IDF 由下方 5 篇背景语料统计(用户输入作为第 6 篇),高频虚词 IDF 低、权重被压低:
📚 背景语料:

③ 静态词嵌入(Word2Vec / GloVe / FastText)

BoW/TF-IDF 是高维稀疏向量,词与词之间没有「相似度」概念。词嵌入把每个词映射成 低维稠密向量(如 100 维),让语义相近的词在向量空间里也相近。这是「一词一向量」的静态表示——同一个词在任何句子里向量都一样,不解决多义。

🔗 Word2Vec

两种训练目标:CBOW 用上下文预测中心词;Skip-gram 用中心词预测上下文(小数据更稳)。窗口 window 控制上下文范围。

🌐 GloVe

基于全局共现矩阵:先统计「词 i 和词 j 在窗口内共现多少次」,再用向量内积去拟合这个计数。强调全局统计信息。

⚡ FastText

在 Word2Vec 基础上引入子词(n-gram 字符片段),未登录词也能由子词向量拼出,解决 OOV,对形态丰富语言尤友好。

# 训练自己的 Word2Vec(gensim)
from gensim.models import Word2Vec, FastText

sentences = [["自然", "语言", "处理"], ["机器", "学习", "模型"], ["深度", "学习"]]

# sg=1 → Skip-gram;sg=0 → CBOW;vector_size=词向量维度;window=上下文窗口
model = Word2Vec(sentences, vector_size=100, window=5, sg=1, min_count=1)
vec = model.wv["学习"]               # 取一个词的 100 维向量

# 著名类比:king - man + woman ≈ queen(语义算术)
result = model.wv.most_similar(positive=["国王", "女"], negative=["男"], topn=1)

# FastText:子词级,能处理未登录词 OOV
ft = FastText(sentences, vector_size=100, min_count=1)
ft.wv["未登录词"]                  # 子词拼接,仍可得到向量

🧪交互:词向量类比示意(king - man + woman ≈ queen)

下面用 5 维示意向量展示语义算术:从 king 减去「男性维度」加上「女性维度」,得到的向量最接近 queen。真实 Word2Vec 是 100~300 维,这里用 5 维方便可视化。

输入词向量(5 维示意)
king - man + woman = ?

④ 上下文嵌入(Transformer / BERT)

静态嵌入最大的硬伤:「苹果」在「吃苹果」和「苹果公司」里向量完全一样。BERT 等 Transformer 模型用自注意力机制,让每个词的向量随上下文动态变化——同一个词在不同句子里向量不同,真正捕捉了多义。上下文嵌入几乎全面胜过静态嵌入,但推理成本高,适合小数据 fine-tune 或语义检索。

🔁 动态 vs 静态

Word2Vec:词 → 固定向量,与上下文无关。BERT:词 + 整句上下文 → 动态向量,同词异义会被区分。多义词问题由此解决。

🎯 句向量策略

取 [CLS] 位置的向量、或对序列做 mean pooling(平均池化)作为整句表示。sentence-transformers 库封装了这些策略,推荐使用。

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

# 多语言模型,中文也适用(384 维句向量)
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

sents = ["我今天吃了一个苹果,很甜", "苹果公司发布了新一代手机"]
embs = model.encode(sents)             # 每句一个 384 维向量

# 同一个"苹果",在两句里的上下文向量完全不同
print(cosine_similarity([embs[0]], [embs[1]]))   # 两句语义相似度

# 直接取单个 token 的上下文向量:从 BERT 输出取对应位置
from transformers import AutoTokenizer, AutoModel
import torch
tok = AutoTokenizer.from_pretrained("bert-base-chinese")
bert = AutoModel.from_pretrained("bert-base-chinese")
out = bert(**tok("吃苹果", return_tensors="pt"))
# out.last_hidden_state[0, 2] 即"苹果"在"吃"上下文中的动态向量

🧪交互:同一个「苹果」在不同语境下的向量差异

🍎 语境 A(水果)
"我今天吃了一个苹果,很甜"
📱 语境 B(公司)
"苹果公司发布了新一代手机"

⑤ 序列编码(Sequence Encoding)

神经网络要批量处理,输入必须是定长张量。所以文本要先 padding(补齐)到固定长度、超长的 truncating(截断),并配一个 attention_mask 告诉模型哪些是真实 token、哪些是补的 PAD——否则 PAD 会污染语义。还要加特殊 token:[CLS] 标句首、[SEP] 标句尾、[PAD] 补位。

tokenizer 的三件输出:
• input_ids:每个 token 在词表里的编号序列([CLS]=1, [SEP]=3, [PAD]=0…)
• attention_mask:与 ids 等长的 0/1 序列,1=真实 token,0=PAD(模型注意力只看 1 的位置)
• token_type_ids:句子归属(单句全 0,句对 0/1 区分)
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("bert-base-chinese")
text = "自然语言处理很有趣"

# padding='max_length':补到固定长度;truncation=True:超长截断
# max_length=128:固定长度;return_tensors='pt':返回 PyTorch 张量
out = tok(text, padding='max_length', truncation=True,
          max_length=128, return_tensors='pt')

print(out['input_ids'])        # [CLS] 自 然 ... 趣 [SEP] [PAD] [PAD] ... → id 序列
print(out['attention_mask'])   # 1 1 1 ... 1 1 0 0 ... → 1=真词,0=PAD
print(tok.convert_ids_to_tokens(out['input_ids'][0]))
# ['[CLS]','自','然','语','言','处','理','很','有','趣','[SEP]','[PAD]',...]

🧪交互:输入一句话,看 token → id → padding 序列

input_ids
attention_mask

⑥ 文本特征构造(Hand-crafted Features)

深度学习时代,「手工特征」看似过时,但在 GBM 模型(XGBoost/LightGBM) 或作为深度模型的补充特征时依然有用:它们计算极快、可解释性强、小数据集上往往比端到端模型更稳。这些特征从原始文本直接统计得出,不需要预训练。

import re
import jieba

text = "自然语言处理很有趣!今天天气真好。"
tokens = jieba.lcut(text)

# ① 文本长度(字符数)
char_len = len(text)

# ② 词数
word_count = len(tokens)

# ③ 平均词长
avg_word_len = sum(len(w) for w in tokens) / word_count

# ④ 标点密度 = 标点数 / 字符数
punct_count = len(re.findall(r'[,。!?、,.!?;:]', text))
punct_density = punct_count / char_len

# ⑤ 情感词典命中数
pos_words = {"好", "喜欢", "棒", "优秀", "有趣", "成功"}
neg_words = {"坏", "讨厌", "差", "失败", "糟糕"}
sentiment_hits = sum(1 for w in tokens if w in pos_words | neg_words)

# ⑥ 停用词比例
stopwords = {"的", "是", "了", "在", "和", "与", "我", "你", "他", "这"}
stop_ratio = sum(1 for w in tokens if w in stopwords) / word_count

🧪交互:输入文本,实时显示 6 个手工特征

⑦ 常见坑与最佳实践

文本特征工程里 80% 的 bug 来自「训练/推理不一致」和「数据泄露」。下面五条是踩过最疼的坑:

① 分词器训练/推理必须用同一套

自己训练的 Word2Vec、自建的 BPE 词表,推理时必须用完全相同的分词器和词表。换了 jieba 版本、换了 tokenizer 文件,词 id 全乱,模型直接废。最佳实践:把分词器连同模型一起 pickle / 保存,推理时从同一路径加载。

② TF-IDF 的 IDF 必须用训练集统计

TfidfVectorizer 的 fit 只能在训练集上调用一次,测试集只能 transform。用全量数据 fit 就是数据泄露——测试集的词频泄漏进了 IDF,评估指标会虚高。正确写法:tfidf.fit(X_train); tfidf.transform(X_test)。

③ Transformer 输入必须带 attention_mask

padding 出来的 [PAD] 如果不靠 attention_mask 屏蔽,会被模型当成真实 token 参与注意力计算,污染语义表示。哪怕用 [PAD]=0,也必须传 attention_mask。HuggingFace 模型默认会用到 mask,但自己写 forward 时别漏。

④ 中文任务慎用英文 tokenizer

英文 BPE 把中文当成「未知字符」一堆 [UNK],等于丢光语义。中文要用 bert-base-chinese(字级)或先用 jieba 分词再喂模型。混合语种可用多语言模型(XLM-R、multilingual-MiniLM)。

⑤ 嵌入层 fine-tune 学习率要小

在预训练嵌入(BERT、word2vec)上做下游 fine-tune 时,嵌入层用小学习率(如 1e-5~2e-5),避免「灾难性遗忘」——把预训练学到的语义知识冲掉。可用分层学习率(discriminative learning rate),越靠近输入层学习率越小。

⑧ 下载 BoW/TF-IDF 特征表 CSV

点击下方按钮,导出一份「文本向量化结果表」CSV:以上方 ② 号章节的 5 篇背景语料 + 你当前输入 共 6 篇文档,扁平化为 doc_id / word / tf / idf / tfidf 五列。UTF-8 带 BOM、文本字段已加引号转义,pandas 直接 read_csv 即可解析。

文件名 text_features_tfidf.csv · 当前输入框文本作为第 6 篇文档
# 下载后用 pandas 读回,验证结构
import pandas as pd

df = pd.read_csv("text_features_tfidf.csv")   # 5 列:doc_id, word, tf, idf, tfidf
print(df.head())
print(df.dtypes)

# 透视成 文档 × 词 的 TF-IDF 矩阵(机器学习常用输入)
matrix = df.pivot_table(index='doc_id', columns='word', values='tfidf', fill_value=0)
print(matrix.shape)   # (6, 词表大小)