把「人能读的字」变成「模型能算的数」。本页聚焦分词 → 向量化 → 词嵌入 → 序列编码 → 手工特征等「处理与变换」环节,带你理解每种表示的原理、适用场景和常见坑。
分词是文本特征工程的第 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)) # 字级切分:['自','然','语','言','处','理','很','有','趣']
最朴素的向量化:把文本变成「词 → 数字」的稀疏向量。词袋模型(BoW)只数词频;TF-IDF 在词频基础上乘以 IDF,降低高频虚词("是""的")权重、突出区分性词。它们丢失词序、是稀疏离散表示,适合短文本分类基线。
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 是高维稀疏向量,词与词之间没有「相似度」概念。词嵌入把每个词映射成 低维稠密向量(如 100 维),让语义相近的词在向量空间里也相近。这是「一词一向量」的静态表示——同一个词在任何句子里向量都一样,不解决多义。
两种训练目标:CBOW 用上下文预测中心词;Skip-gram 用中心词预测上下文(小数据更稳)。窗口 window 控制上下文范围。
基于全局共现矩阵:先统计「词 i 和词 j 在窗口内共现多少次」,再用向量内积去拟合这个计数。强调全局统计信息。
在 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["未登录词"] # 子词拼接,仍可得到向量
下面用 5 维示意向量展示语义算术:从 king 减去「男性维度」加上「女性维度」,得到的向量最接近 queen。真实 Word2Vec 是 100~300 维,这里用 5 维方便可视化。
静态嵌入最大的硬伤:「苹果」在「吃苹果」和「苹果公司」里向量完全一样。BERT 等 Transformer 模型用自注意力机制,让每个词的向量随上下文动态变化——同一个词在不同句子里向量不同,真正捕捉了多义。上下文嵌入几乎全面胜过静态嵌入,但推理成本高,适合小数据 fine-tune 或语义检索。
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] 即"苹果"在"吃"上下文中的动态向量
神经网络要批量处理,输入必须是定长张量。所以文本要先 padding(补齐)到固定长度、超长的 truncating(截断),并配一个 attention_mask 告诉模型哪些是真实 token、哪些是补的 PAD——否则 PAD 会污染语义。还要加特殊 token:[CLS] 标句首、[SEP] 标句尾、[PAD] 补位。
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]',...]
深度学习时代,「手工特征」看似过时,但在 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
文本特征工程里 80% 的 bug 来自「训练/推理不一致」和「数据泄露」。下面五条是踩过最疼的坑:
自己训练的 Word2Vec、自建的 BPE 词表,推理时必须用完全相同的分词器和词表。换了 jieba 版本、换了 tokenizer 文件,词 id 全乱,模型直接废。最佳实践:把分词器连同模型一起 pickle / 保存,推理时从同一路径加载。
TfidfVectorizer 的 fit 只能在训练集上调用一次,测试集只能 transform。用全量数据 fit 就是数据泄露——测试集的词频泄漏进了 IDF,评估指标会虚高。正确写法:tfidf.fit(X_train); tfidf.transform(X_test)。
padding 出来的 [PAD] 如果不靠 attention_mask 屏蔽,会被模型当成真实 token 参与注意力计算,污染语义表示。哪怕用 [PAD]=0,也必须传 attention_mask。HuggingFace 模型默认会用到 mask,但自己写 forward 时别漏。
英文 BPE 把中文当成「未知字符」一堆 [UNK],等于丢光语义。中文要用 bert-base-chinese(字级)或先用 jieba 分词再喂模型。混合语种可用多语言模型(XLM-R、multilingual-MiniLM)。
在预训练嵌入(BERT、word2vec)上做下游 fine-tune 时,嵌入层用小学习率(如 1e-5~2e-5),避免「灾难性遗忘」——把预训练学到的语义知识冲掉。可用分层学习率(discriminative learning rate),越靠近输入层学习率越小。
点击下方按钮,导出一份「文本向量化结果表」CSV:以上方 ② 号章节的 5 篇背景语料 + 你当前输入 共 6 篇文档,扁平化为 doc_id / word / tf / idf / tfidf 五列。UTF-8 带 BOM、文本字段已加引号转义,pandas 直接 read_csv 即可解析。
# 下载后用 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, 词表大小)