从零玩转 NLP:分词、文本表示与 Word2Vec 实战

一、开场:让计算机"看见"文字

先做一个思想实验。

你把一句中文"我爱自然语言处理"丢给计算机,它会"看到"什么?

答案是一串毫无意义的字符—— 这种 Unicode 编码的二进制组合。计算机本身既不"懂"汉字的语义,也"不懂"词的边界。

要让计算机真正"读懂"一句话,我们必须走完一段旅程:

  1. 先把句子"切碎"成最小语义单元(这一步叫分词,Tokenization);
  2. 再把每个单元"翻译"成数字——因为计算机只会算数字,不会算文字(这一步叫文本张量表示)。

这就是本文要讲的两件最基础也最关键的事:分词文本张量表示。搞懂这两步,你就能读懂后续 80% 的 NLP 代码(Word2Vec、Embedding、RNN、Transformer 全部建立在这两步之上)。

📌 本文脉络
我们会先聊 NLP 是什么、它能干什么、它怎么从"规则系统"一路进化到"深度学习";然后聚焦"文本处理的完整 pipeline",重点实战 jieba 分词One-Hot / Word2Vec / nn.Embedding 三种文本表示方法。代码全部可运行,注释拉满,建议边读边敲。


二、NLP 是什么?能干什么?

2.1 定义

自然语言处理(Natural Language Processing, NLP)是人工智能领域的一个重要分支。所谓"自然语言",指人类日常使用的语言(中文、英文、法文……)。NLP 的目标,是让计算机"理解"或"使用"这些语言——说人话就是:让机器听得懂人话、说得出人话

2.2 五大常见任务

NLP 的应用虽然千变万化,但拆开来看都离不开这五类核心任务:

任务 干什么 典型应用
文本分类 对整段文本打标签 / 归类 情感分析(正面 / 负面)、垃圾邮件识别、新闻主题分类
序列标注 对句子中的每个词打标签 命名实体识别(人名 / 地名 / 手机号)、词性标注
文本生成 根据已有内容生成新文本 自动写作、摘要生成、智能客服、对话系统
信息抽取 从文本中抽出结构化信息 关系抽取、阅读理解(给定文本 + 问题 → 抽答案)
文本转换 把一种文本变成另一种 机器翻译、文本改写、文本纠错

💡 快速判定任务类型
看输出。如果输出是"一个标签"(好 / 坏 / 体育 / 娱乐)→ 分类;如果输出是"一串等长的标签"(每个词一个标签)→ 序列标注;如果输出是"一段新的文字"→ 生成或转换。

2.3 技术演进史:四个阶段

理解 NLP 的历史,能帮你一眼看穿今天大火的 ChatGPT 为啥能这么"猛"——它是几十年的技术堆叠的产物。

阶段 时间 核心做法 典型代表 局限
规则系统 1950s ~ 1980s 语言学家手写语法规则 1954 年 Georgetown-IBM 机器翻译实验;1966 年 ELIZA 聊天机器人 通用性差,扩展性差
统计方法 1990s 用概率建模代替规则 N-gram 语言模型、隐马尔可夫模型(HMM) 依赖手工设计特征,泛化弱
机器学习 2000s SVM / LR / CRF + 特征工程 词袋模型 + LR 做文本分类 特征工程费时费力
深度学习 2010s 至今 神经网络自动学表示 RNN / LSTM / GRU → Transformer → BERT / GPT 数据 / 算力需求大

举个具体的例子感受下"统计方法的局限"。下面两条完全相反的评论:

  • 评论 A:“服务很好但味道差劲”
  • 评论 B:“味道很好但服务差劲”

早期"词袋模型"(Bag-of-Words)只会统计词频,完全忽略词序。这两句话分词后:

  • A:[服务, 很, 好, 但, 味道, 差劲]
  • B:[味道, 很, 好, 但, 服务, 差劲]

在词袋模型眼里,两条评论的向量完全一样。这显然不对,但当时只能靠"古人"手动加 n-gram(把相邻 N 个词作为整体)来救场——这正是后来深度学习要彻底解决的问题:让模型自己学出包含语序和语义的表示


三、文本处理的完整 Pipeline

在正式讲分词和文本表示之前,先上一张全景图。所有 NLP 任务,从经典的情感分类到最火的 ChatGPT,文本处理都遵循下面这个流程

整个流程可以拆成 5 步:

  1. 分词(Tokenization):把句子切成 token(最小语义单元);
  2. 命名实体识别 NER / 词性标注 POS:给每个 token 打标签(可选,但多数工业级任务会做);
  3. 文本张量表示:把 token 转成数字向量(本文重点);
  4. 文本特征处理:归一化、长度填充、停用词过滤等;
  5. 文本数据增强:在数据量小时扩充样本。

下面重点讲 分词文本张量表示 这两步。


四、分词:把句子"切碎"

分词(Tokenization)是把原始文本切分为若干具有独立语义的最小单元(token)的过程,是所有 NLP 任务的起点。没有这一步,后面所有的"向量化"都是空中楼阁。

4.1 英文分词的三种粒度

英文的分词有词级(Word-Level)、字符级(Character-Level)、子词级(Subword-Level)三种,特点如下:

方式 词表规模 OOV 问题 语义保留 序列长度 典型代表
词级分词 很大(10w+) 严重 传统 NLP 工具(NLTK、spaCy)
字符级分词 极小(~256) 几乎无 字符级 CNN / RNN
子词级分词 适中(3w~5w) 极少 中等 BERT、GPT 等现代大模型

📌 什么是 OOV?
OOV(Out-Of-Vocabulary)指"词表里没有的词"。举个例子:词表里没有"栓 Q",那"栓 Q"就是 OOV。词级分词最容易遇到 OOV;子词级(BPE)会把"栓 Q"拆成"栓"和"Q"两个子词,从而几乎不会 OOV——这也是现代大模型几乎都用子词分词的核心理由。

💡 想深入了解 BPE?
推荐 HuggingFace 官方课程:Byte-Pair Encoding tokenization。它一步步演示了 BPE 是怎么从字符开始、一步步合并出子词词表的。

4.2 中文分词与 jieba

中文没有天然的"空格"作为词边界,所以"分词"这个动作在中文里比英文更关键。中文分词的代表工具:

下面重点演示 jieba。它提供三种分词模式,适用场景各不相同:

模式 说明 API
精确模式 试图将句子最精确地切开,适合文本分析 jieba.cut(text, cut_all=False)
全模式 把句子中所有可能成词的词都扫出来,速度快但有歧义 jieba.cut(text, cut_all=True)
搜索引擎模式 在精确模式基础上对长词再次切分,提高召回率 jieba.cut_for_search(text)

📌 精确模式(默认,最常用)

import jieba

text = '我爱学习,我爱敲代码'

# 精确模式:cut_all=False(默认)
words = jieba.cut(text, cut_all=False)
print('/ '.join(words))

输出:

我/ 爱/ 学习/ ,/ 我/ 爱/ 敲/ 代码

📌 全模式

import jieba

text = '我爱学习,我爱敲代码'

# 全模式:cut_all=True
words = jieba.cut(text, cut_all=True)
print('/ '.join(words))

输出(注意"我爱学习"这种较长词组也会被切出来):

我/ 爱/ 学习/ 我爱/ 学习/ ,/ 我/ 爱/ 敲/ 代码

📌 搜索引擎模式

import jieba

text = '我爱学习自然语言处理'

# 搜索引擎模式:基于精确模式,对长词再次切分
words = jieba.cut_for_search(text)
print('/ '.join(words))

输出(既保留"自然语言处理"这种长词,又拆出"自然""语言"这种短词):

我/ 爱/ 学习/ 自然/ 语言/ 处理/ 自然语言/ 自然语言处理

💡 选哪个?

  • 做情感分析、文本分类等"重语义"任务 → 精确模式(歧义少);
  • 自己做搜索索引、要尽可能多召回 → 搜索引擎模式
  • 全模式在生产环境几乎不用(噪音太多)。

4.3 自定义词典:解决分词歧义

jieba 自带一个 30w+ 的通用词库,但对专业领域(医疗、法律、金融)和新兴网络用语(“栓 Q”“city 不 city”)是无能为力的。直接后果就是分词歧义。举经典例子:

“下雨天留客天留我不留”
不同断句可以解释出完全相反的意思。jieba 纯靠通用词库分不准。

解决办法:加载自定义词典

import jieba

# 加载自定义词典(每行一个词:词语 词频 词性)
# user_dict.txt 内容示例:
#   栓 Q 5 n
#   云计算 3 n
jieba.load_userdict('user_dict.txt')

text = '学 Python 离不开云计算'
print('/ '.join(jieba.cut(text)))
# 输出:学/ Python/ 离不开/ 云计算  ← 云计算被正确识别为一个词

4.4 命名实体识别(NER)与词性标注(POS)

分完词之后,下一步常常是给每个 token 打标签——这是后续做关系抽取、问答系统的基石。jieba 自带词性标注功能:

from jieba import posseg

# res 是一个生成器,遍历得到 (word, flag) 对
res = posseg.cut("小明硕士毕业于中国科学院计算所,后在日本京都大学深造")
for item in res:
    print(item.word, item.flag, sep='\t')

# 输出:
# 小明   nr      # nr: 人名
# 硕士   n       # n:  普通名词
# 毕业   n
# 于     p       # p:  介词
# 中国科学院  nt  # nt: 机构名
# 计算所 n
# ,     x       # x:  标点
# 后在   t
# 日本京都大学  nt
# 深造   v       # v:  动词

📷 jieba 词性对照表
完整标签集参见 jieba 官方词性对照表。生产环境做 NER 通常会用更专业的 HanLPLAC


五、文本张量表示:让文字变成"算得动"的数字

把一段文本用张量(Tensor)的形式表示,这个过程就叫文本张量表示。词被表示为词向量,一句话就成了一个词向量矩阵

为什么必须做这一步?因为计算机不懂字、也不懂词,它只懂张量上的加减乘除。

常见的文本表示方法有三类,我们由浅入深一一展开。

5.1 One-Hot:最朴素的离散表示

One-Hot 编码是最基础的表示方法,思路简单到极致:

  • 词表有多大,向量就有多长;
  • 每个词对应一个向量,只有该词索引位置是 1,其余全是 0

举例:词表为 [周杰伦, 陈奕迅, 王力宏, 吴亦凡, 刘德华],“周杰伦” 的 One-Hot 向量就是 [1, 0, 0, 0, 0]

One-Hot 优点

  • 简单直观,实现容易;
  • 配合 torch.nn.functional.one_hot 一行代码搞定。

One-Hot 致命缺点

  • 维度灾难:中文词表动辄几十万,向量长度也跟着几十万,模型根本跑不动;
  • 完全丢弃语义:任意两个不同词的 One-Hot 向量都是正交的(点积 = 0),无法表达"猫"和"狗"都是动物、"好"和"棒"是近义词。

⚠️ 重要提示
One-Hot 在生产中几乎不用,但它是所有 Embedding 方法的"地基"——理解 One-Hot 的"维度灾难"和"语义缺失"问题,才能体会 Word2Vec 为啥是 NLP 史上里程碑式的工作。

手写 One-Hot
"""
演示 One-Hot 编码的手写实现
理解原理即可:先把词映射成 id,再把 id 映射成"独热向量"
"""

# 1. 构建词表
words = ['周杰伦', '陈奕迅', '王力宏', '吴亦凡', '刘德华']
token_id = {'周杰伦': 0, '陈奕迅': 1, '王力宏': 2, '吴亦凡': 3, '刘德华': 4}

def one_hot(word: str) -> list:
    """根据词表生成该词的 One-Hot 向量"""
    if word not in token_id:
        raise ValueError(f'词表里没有 "{word}"')
    vector = [0] * len(token_id)       # 初始化全 0 向量
    vector[token_id[word]] = 1         # 把自己对应的位置置 1
    return vector

# 测试
print(one_hot('周杰伦'))   # [1, 0, 0, 0, 0]
print(one_hot('陈奕迅'))   # [0, 1, 0, 0, 0]
PyTorch 官方 One-Hot
import torch
import torch.nn.functional as F

# 假设有 3 个 token,词表大小 5
token_ids = torch.tensor([0, 3, 2])   # shape: (3,)
vocab_size = 5

# F.one_hot 会把 id 转成 0/1 向量
one_hot_vectors = F.one_hot(token_ids, num_classes=vocab_size)
# shape: (3, 5)
# 结果:
# [[1, 0, 0, 0, 0],
#  [0, 0, 0, 1, 0],
#  [0, 0, 1, 0, 0]]
print(one_hot_vectors)
print(one_hot_vectors.shape)   # torch.Size([3, 5])

# 注意:F.one_hot 返回的是 int64 类型
# 如果要喂给深度学习模型(float 输入),记得 .float()
one_hot_vectors = one_hot_vectors.float()

5.2 Word2Vec:让向量"有语义"

Word2Vec 是 Google 在 2013 年提出的词嵌入模型,核心思想只有一句话:

通过一个浅层神经网络在大规模语料上训练,把每个词映射成低维稠密向量,使得语义相近的词在向量空间里距离更近

它直接解决了 One-Hot 的两个核心痛点:维度从"几十万"压到"几百",并且"语义"被编码进了向量的方向里。

Word2Vec 包含两种训练模式:

  • CBOW(Continuous Bag-of-Words):用上下文预测中心词。训练快,适合小语料;

  • Skip-gram:用中心词预测上下文。对低频词更友好,适合大语料。

Word2Vec 最神奇的地方是支持语义代数

  • vec(国王) - vec(男性) + vec(女性) ≈ vec(女王)
  • vec(巴黎) - vec(法国) + vec(日本) ≈ vec(东京)

这就是"词的语义被编码到向量空间里"的实锤。

方式一:用 FastText 训练

FastText 是 Facebook 出的库,自带 Word2Vec 的 CBOW 和 Skip-gram 实现,安装简单,开箱即用。

📌 安装

# 推荐 uv 安装预编译 wheel
uv add fasttext-wheel
# 或 pip
pip install fasttext-wheel

⚠️ 版本坑
FastText 在 Python 3.14 上会有兼容性问题,Python 3.8 ~ 3.12 是最稳的。如果用 3.14 报 distutils 相关错误,建议降到 3.12 或换用 Gensim。

📌 准备语料

# corpus.txt:每行一句,词语已经用空格分开
我 爱 学习 自然 语言 处理
今天 天气 真 好
机器 学习 是 人工智能 的 重要 分支
深度 学习 推动 了 自然语言处理 的 进步

📌 训练 Skip-gram 模型

import fasttext

# 训练无监督 Word2Vec
model = fasttext.train_unsupervised(
    input='corpus.txt',
    model='skipgram',   # 也可填 'cbow'
    dim=100,            # 词向量维度,越大表示能力越强但越慢
    ws=5,               # 上下文窗口大小
    minCount=2,         # 词频低于此值的词会被过滤
    epoch=5             # 训练轮数
)

# 保存模型
model.save_model("word2vec.bin")

📌 加载 & 最近邻查询

import fasttext

# 加载训练好的模型
model = fasttext.load_model("word2vec.bin")

# 1. 取单个词的词向量
vec = model.get_word_vector("自然语言")
print(vec.shape)  # (100,)

# 2. 找最相近的 5 个词
neighbors = model.get_nearest_neighbors("人工智能", k=5)
for similarity, word in neighbors:
    print(f"{word}: {similarity:.4f}")

# 输出示例:
# 机器学习: 0.8765
# 深度学习: 0.8234
# 大数据:   0.7891

💡 FastText 关键参数速查

参数 含义 默认值
model 训练模式 cbowskipgram skipgram
dim 词向量维度 100
ws 上下文窗口 5
minCount 最低词频 5
epoch 训练轮数 5
lr 学习率 0.05
thread 训练线程数 CPU 核心数
方式二:用 Gensim 加载 / 训练

Gensim 是 NLP 圈最常用的主题模型 / 词向量库,提供了加载预训练词向量自行训练两种姿势。

📌 加载第三方预训练词向量

from gensim.models import KeyedVectors

# 加载腾讯 / 微博 / Google 公开词向量(这里以微博 300 维为例)
# 文件很大(几 GB),首次下载需耐心
model = KeyedVectors.load_word2vec_format('sgns.weibo.word.bz2')

print(model.vector_size)   # 300
print(model['地铁'][:10])  # 取"地铁"词向量的前 10 维

📷 推荐词向量资源

📌 余弦相似度与语义代数

# 1. 计算两个词的余弦相似度
sim = model.similarity('地铁', '公交')
print(f"地铁 vs 公交 相似度: {sim:.4f}")

# 2. 找最相似的词
print(model.similar_by_word('男孩', topn=3))

# 3. 经典语义代数:爸爸 - 男性 + 女性 ≈ 妈妈
result = model.most_similar(
    positive=['爸爸', '女性'],
    negative=['男性'],
    topn=3
)
print(result)

📌 余弦相似度公式
similarity ( w 1 , w 2 ) = cos ⁡ ( θ ) = w 1 ⋅ w 2 ∣ w 1 ∣ ⋅ ∣ w 2 ∣ \text{similarity}(w_1, w_2) = \cos(\theta) = \frac{w_1 \cdot w_2}{|w_1| \cdot |w_2|} similarity(w1,w2)=cos(θ)=w1w2w1w2
取值范围 [-1, 1]:接近 1 → 语义相近,接近 0 → 不相关,接近 -1 → 方向相反。

📌 用 Gensim 自行训练

from gensim.models import Word2Vec

# 语料:每个元素是一个已分词的句子(list of tokens)
sentences = [
    ['我', '每天', '乘坐', '地铁', '上班'],
    ['我', '每天', '乘坐', '公交', '上班'],
    ['地铁', '比', '公交', '快', '很多'],
    ['北京', '的', '地铁', '非常', '拥挤'],
]

# 训练 Skip-gram(sg=1);sg=0 表示 CBOW
model = Word2Vec(
    sentences,
    vector_size=100,    # 词向量维度
    window=5,           # 上下文窗口
    min_count=1,        # 最低词频(语料少时设小一点)
    sg=1,               # 1: Skip-Gram, 0: CBOW
    workers=4           # 并行线程数
)

# 找最相似的词
print(model.wv.most_similar('地铁', topn=3))

# 保存词向量
model.wv.save_word2vec_format('my_vectors.vec')

5.3 Word Embedding:深度学习里的"可训练词向量"

Word Embedding 是个比 Word2Vec 更宽泛的概念:把离散的 token 映射为低维稠密向量的过程都叫词嵌入

在现代深度学习中,它通常特指神经网络里的 Embedding 层——一个可训练的参数矩阵。这是和 Word2Vec 最大的区别:

Word2Vec nn.Embedding
训练方式 先在语料上预训练,词向量冻住 跟着下游任务端到端训练
适用场景 词向量当特征 / 检索 配合 LSTM / Transformer 微调
典型 API gensim.Word2Vec torch.nn.Embedding
PyTorch nn.Embedding 实战
import torch
import torch.nn as nn

# 1. 创建 Embedding 层
# 词表 1000 词,每个词映射成 128 维向量
embedding = nn.Embedding(num_embeddings=1000, embedding_dim=128)

# 2. 喂入 token id
# shape: (batch=2, seq_len=4)
token_ids = torch.tensor([
    [10, 200, 5, 333],
    [88,  2, 17, 999]
])

vectors = embedding(token_ids)
# 输出 shape: (2, 4, 128)
# 对应 batch=2 条句子,每条 4 个 token,每个 token 128 维向量
print(vectors.shape)  # torch.Size([2, 4, 128])

# 3. 查看可训练参数
print(embedding.weight.shape)  # torch.Size([1000, 128])

⚠️ 踩坑提示

  • 输入必须是 LongTensor(int64),不是 FloatTensor,否则会报 RuntimeError: expected scalar type Long
  • nn.Embedding 内部就是一个 (num_embeddings, embedding_dim)可学习矩阵,初始值随机;
  • 如果要加载预训练词向量(Word2Vec / GloVe),用 nn.Embedding.from_pretrained(...),并指定 freeze=False 决定是否微调。
import torch
import torch.nn as nn
import numpy as np

# 假设已经从 gensim 拿到词向量矩阵 (vocab_size, dim)
pretrained_weights = np.random.randn(1000, 128).astype(np.float32)  # 替换成真实词向量

embedding_from_pt = nn.Embedding.from_pretrained(
    torch.from_numpy(pretrained_weights),
    freeze=False   # True: 不微调;False: 跟着任务继续训练
)

六、避坑指南 & 最佳实践

⚠️ 5 个最常见的"翻车"现场

  1. One-Hot 维度爆炸:词表 50w,向量就是 50w 维,模型直接 OOM。生产环境几乎不用 One-Hot,永远优先用 Embedding / Word2Vec。
  2. OOV 直接崩:用 nn.Embedding 时如果遇到 id ≥ num_embeddings,会报 IndexError。一定要预留 <UNK> 词,把 OOV 都映射到 <UNK> 这个特殊 id。
  3. 中文分词歧义:默认 jieba 在垂直领域(医疗 / 法律)分不准。必须加载自定义词典,并用 jieba.add_word() 给关键术语兜底。
  4. FastText / Word2Vec 的 Python 版本坑:3.14 容易报 ModuleNotFoundError: distutilsnumpy ABI 错误。优先用 3.10 ~ 3.12
  5. 预训练词向量维度不匹配:自己训的 dim=100,想加载腾讯 300 维,必须用线性层 nn.Linear(300, 100) 重新映射维度,不能直接用。

💡 工业级最佳实践

  • 小数据 + 简单任务jieba + 预训练 Word2Vec + LR / SVM,传统但稳定;
  • 中等数据 + 文本分类jieba + 自训 Embedding + LSTM / BiLSTM;
  • 大数据 + 通用任务 → HuggingFace Tokenizers + BERT / RoBERTa 微调,永远不会错;
  • 超大规模 + 生成任务 → 直接调用 ChatGPT / Qwen API,自己训 LLM 性价比极低。

七、总结:一张表打通"选型"

维度 One-Hot Word2Vec (FastText / Gensim) nn.Embedding
出现时间 1980s ~ 2013 (Google) 深度学习时代
向量类型 高维稀疏(0/1) 低维稠密(float) 低维稠密(可训练)
词表规模 极小(<1w) 适中(10w 级) 任意
语义信息 ❌ 无 ✅ 强 ✅ 强(依赖数据量)
OOV 处理 ❌ 不支持 ⚠️ FastText 子词兜底 ⚠️ 需 <UNK> 兜底
训练成本 0 中(CPU 几十分钟) 跟着下游任务
适用场景 教学 / 入门 demo 传统 NLP 任务 / 文本检索 配合 LSTM / Transformer

📌 下一步学什么?
有了"分词 + 文本表示"这块地基,就可以继续往 NLP 深处走了:

  1. RNN / LSTM / GRU:处理变长序列,理解"上下文";
  2. 注意力机制 (Attention):解决 RNN 长距离依赖的痛点;
  3. Transformer / BERT / GPT:今天大模型的"基石架构",必读;
  4. HuggingFace 实战:把上面的理论全部用代码串起来。

笔者后续会按顺序更新这些笔记,欢迎关注~


参考资料

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐