从“我爱你”到 0 和 1:一文看懂 NLP 文本表示(附代码,小白也能懂)

0 阅读15分钟

为什么 ChatGPT 能听懂你的话?秘密全在“文本表示”里。这篇文章不讲玄学,只讲干货,手把手带你从零学会分词、Word2Vec 和 BERT 的核心原理。

很多读者问:计算机只认识 0 和 1,凭什么能理解“我爱你”这种充满感情的话?答案是——文本表示(Text Representation)。简单来说,就是把人类写的文字,变成计算机能算的数字。这一步做不好,后面再厉害的模型也没用。

本文将按照 分词 → 词表示(One-hot → Word2Vec) → 上下文表示 这条主线,极其详细地讲解每个概念,并提供可直接复制运行的代码。读完你会掌握:

  • 英文和中文分别怎么分词,BPE 是什么,jieba 怎么用。

  • 为什么 One-hot 不行,Word2Vec 如何让数字拥有语义。

  • 用 Gensim 训练自己的词向量,并用 PyTorch 加载到神经网络中。

  • 静态词向量的局限,以及 BERT 如何解决“苹果”是水果还是手机的问题。


1. 分词:先把句子切成小块

文本表示的第一步不是“变成向量”,而是分词。就像做菜要先切食材,NLP 要先切词。

1.1 什么是分词和词表?

分词(Tokenization):把原始文本切分成有独立语义的最小单元(token)。
例如 “我爱你” → [“我”,“爱”,“你”]。

词表(Vocabulary):把所有出现过的 token 收集起来,每个分配一个唯一 ID,方便后续查找。

Token

ID

0

1

2

北京

3

烤鸭

4

1.2 英文分词:三种粒度

英文有天然的空格,但事情没那么简单。我们按粒度从大到小介绍。

1.2.1 词级分词(Word-level)

最简单粗暴——按空格和标点切。

优点:简单,符合直觉。
缺点:OOV(Out-Of-Vocabulary)问题。如果词表里没有 “outperform”,这个词就变成 ,信息丢失。另外,“models.” 带着句号,与 “models” 不同,也会被当作两个词。

1.2.2 字符级分词(Character-level)

每个字符(包括标点、空格)都是一个 token。

优点:OOV 几乎不存在(任何字符都在词表中)。
缺点:序列变得极长,单个字符语义弱,模型很难学习。比如 ‘h’ 单独出现几乎没有意义。

1.2.3 子词级分词(Subword-level)—— 现代 NLP 的标配

平衡上述两种方案:常见词保留完整,罕见词拆成有意义的子词。比如 “outperform” 拆成 “out” 和 “perform”,两者都是常见子词,既解决了 OOV,又保留了语义。

BPE(Byte Pair Encoding) 是最经典的子词算法,步骤如下:

  1. 初始词表:所有字符(加上结束符 )。

  2. 统计语料中所有相邻符号对的频次。

  3. 把频次最高的符号对合并成新的子词,加入词表。

  4. 重复 2-3 直到词表达到预设大小。

举例:语料中有 “low” 和 “lower”,初始词表 {l, o, w, e, r, }。统计发现 (l, o) 出现 2 次,(o, w) 出现 2 次,(w, e) 出现 1 次… 先合并 (l, o) 成 “lo”,再合并 (lo, w) 成 “low”,最终 “lower” 被分成 [“low”, “er”]。

其他子词算法

  • WordPiece

    (BERT 使用):类似 BPE,但合并时选择使语言模型似然增加最多的 pair。

  • Unigram Language Model

    :从大词表开始,逐步删除无用子词。

1.2.4 代码示例:训练自己的 BPE 分词器(使用 tokenizers 库)

语料文件:corpus_en.txt

Transformers are revolutionizing the field of natural language processing.
Byte Pair Encoding is a subword tokenization algorithm.
The quick brown fox jumps over the lazy dog.
Natural language processing is a subfield of artificial intelligence.
Machine learning models require large amounts of text data.
# 安装:pip install tokenizers
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
 
# 创建一个 BPE 分词器
tokenizer = Tokenizer(BPE(unk_token="<UNK>"))
tokenizer.pre_tokenizer = Whitespace()  # 先用空格预切分
 
# 训练器:设定词表大小 3000
trainer = BpeTrainer(vocab_size=3000, special_tokens=["<UNK>", "<PAD>"])
 
# 假设我们有一个文本文件 'corpus_en.txt'
files = ["corpus_en.txt"]
tokenizer.train(files, trainer)
 
# 保存和加载
tokenizer.save("bpe_tokenizer.json")
loaded = Tokenizer.from_file("bpe_tokenizer.json")
 
# 使用
output = loaded.encode("Transformers often outperform traditional models.")
print(output.tokens)

代码解读:Whitespace 预分词器先把文本按空格切分,然后 BPE 再在每个词内部进行子词合并。vocab_size=3000 控制最终词表大小。

输出内容:

['Transformers', 'of', 'te', 'n', 'o', 'u', 't', 'p', 'er', 'for', 'm', 't', 'r', 'a', 'd', 'it', 'io', 'n', 'al', 'models', '.']

1.3 中文分词:没有空格怎么办?

中文分词比英文难得多,因为词之间没有边界。例如 “北京大学” 可以切成 “北京”+“大学”,也可以保持完整。

1.3.1 字符级分词

最简单:每个汉字一个 token。

汉字本身有意义,所以比英文的字符级效果好。但长词(如“奥林匹克运动会”)的语义被拆散,模型需要多个字符组合才能理解。

1.3.2 词级分词(基于词典/模型)

典型工具是 jieba。它基于前缀词典和 HMM 模型。

安装与基本用法

pip install jieba
import jieba
 
text = "小明毕业于北京大学计算机系"
 
# 精确模式(默认)
print(jieba.lcut(text))
# ['小明', '毕业', '于', '北京大学', '计算机系']
 
# 全模式:扫描所有可能的词
print(jieba.lcut(text, cut_all=True))
# ['小','明','毕业','于','北京','北京大学','大学','计算','计算机','计算机系','算机','系']
 
# 搜索引擎模式:精确模式基础上,对长词再切分
print(jieba.lcut_for_search(text))
# ['小明', '毕业', '于', '北京', '大学', '北京大学', '计算', '计算机', '计算机系']

三种模式对比

模式

特点

适用场景

精确模式

最合理切分

文本分析、模型输入

全模式

冗余切分,召回率高

关键词提取

搜索引擎模式

细粒度

搜索引擎索引

自定义词典:jieba 默认词典可能缺少领域新词(如“大模型”、“深度学习”),可以手动添加。

词典文件格式(my_dict.txt):

大模型 10 n
深度学习 8 n

加载:

jieba.load_userdict("my_dict.txt")
# 或者动态添加
jieba.add_word("Transformer", freq=5)
jieba.del_word("无用词")
 
text = "大模型和深度学习都需要Transformer"
print(jieba.lcut(text))
# ['大模型', '和', '深度学习', '都', '需要', 'Transformer']

freq 越大,该词越倾向于被切分出来。词性标签不影响分词,只是标注用途。

1.3.3 子词级分词(中文也适用)

虽然中文没有明显的词根后缀,但 BPE 依然可以应用于汉字序列。例如 “自然语言处理” 可能被切成 [“自然”,“语言”,“处理”] 或者 [“自”,“然语”,“言处”,“理”] 取决于统计频率。大模型(如通义千问、DeepSeek)都用这种方法。

SentencePiece 是 Google 开源的子词分词工具,不依赖预分词(可以直接处理中文原文)。

安装:pip install sentencepiece

训练中文 BPE 模型

import sentencepiece as spm
 
# 准备一个中文语料文件 chinese_corpus.txt,每行一个句子
spm.SentencePieceTrainer.train(
    input='chinese_corpus.txt',
    model_prefix='chinese_sp',
    vocab_size=8000,
    character_coverage=0.9995,   # 覆盖几乎所有汉字
    model_type='bpe'              # 可选 'unigram' 或 'bpe'
)
 
# 加载模型
sp = spm.SentencePieceProcessor()
sp.load('chinese_sp.model')
 
# 分词
text = "我爱北京天安门"
tokens = sp.encode(text, out_type=str)
print(tokens)  # 例如 ['我', '爱', '北京', '天安门']
 
# 转为 ID
ids = sp.encode(text, out_type=int)
print(ids)  # [123, 45, 678, 901]
 
# 还原
decoded = sp.decode(ids)
print(decoded)  # 我爱北京天安门

character_coverage 对中文很重要,设为 0.9995 表示覆盖 99.95% 的字符,避免生僻字变成 。

1.4 分词工具总结

工具

粒度

优点

缺点

jieba

词级

易用,可自定义词典

存在 OOV,无法处理未登录词

SentencePiece

子词级

无 OOV,语言无关

需要训练,稍复杂

Hugging Face Tokenizers

子词级

与 Transformer 库无缝集成,极快

生态依赖


2. 词表示:把 token 变成数字

分词后,我们得到 token 序列。接下来要把每个 token 变成计算机能计算的数字。这一步叫“词表示”。

2.1 One-hot 编码:最原始的想法

假设词表大小为 V,每个词用一个 V 维的向量表示,该词对应位置为 1,其余为 0。

致命缺陷

  1. 稀疏且维度高

    :词表 10 万,向量就是 10 万维,存储和计算效率极低。

  2. 没有语义关系

    :“苹果” 和 “香蕉” 的向量内积为 0,模型无法知道它们相似。

  3. 无法处理 OOV

    :新词无法表示。

所以现代 NLP 基本不用 One-hot 作为输入特征,只作为某些算法的中间表示。

2.2 语义化词向量:Word2Vec 的革命

Word2Vec(2013)提出:用低维稠密向量(比如 100 维)表示词,并通过上下文自动学习语义——相似的词在向量空间中距离近。

2.2.1 核心思想:分布假设

“You shall know a word by the company it keeps.” —— 一个词的意思由它的上下文决定。

例如,“苹果” 和 “香蕉” 经常出现在“吃一个___”中,所以它们的向量会被拉近。

2.2.2 Word2Vec 的两种模型结构

CBOW(Continuous Bag-of-Words):用上下文预测中心词。

输入:上下文词的 one-hot(或索引) → 取词向量 → 平均 → 输出层预测中心词。

Skip-gram:用中心词预测上下文。

输入:中心词 one-hot → 取词向量 → 输出层预测每个上下文词。

对比

模型

输入

输出

训练速度

对低频词效果

CBOW

多个上下文词

中心词

较差

Skip-gram

一个中心词

多个上下文词

较好

2.2.3 训练样本构造(手工推导)

假设句子分词后为:[“我”,“每天”,“乘坐”,“地铁”,“上班”],窗口大小为 2(即考虑左右各 2 个词)。

  • Skip-gram 样本

    :对于中心词 “乘坐”,上下文是 [“我”,“每天”,“地铁”,“上班”],产生 4 个训练对:(乘坐, 我), (乘坐, 每天), (乘坐, 地铁), (乘坐, 上班)。

  • CBOW 样本

    :对于中心词 “地铁”,上下文是 [“每天”,“乘坐”,“上班”],产生一个样本:输入 [每天, 乘坐, 上班] → 输出 地铁。

2.2.4 数学原理(尽量通俗)

Word2Vec 其实是一个简单的神经网络

  • 输入层:词的 one-hot(V 维)。

  • 隐藏层:权重矩阵 W_in 形状 V × d,每一行是一个词的词向量(这正是我们要学的!)。

  • 输出层:权重矩阵 W_out 形状 d × V,再加 Softmax,输出每个词作为预测的概率。

前向传播(Skip-gram 为例)

import numpy as np
 
# 假设 V=10000, d=100
V, d = 10000, 100
 
# 随机初始化参数
W_in = np.random.randn(V, d) * 0.01
W_out = np.random.randn(d, V) * 0.01
 
# 输入:中心词的 one-hot (假设索引为 123)
x = np.zeros(V)
x[123] = 1.0
 
# 隐藏层:直接取出词向量(因为 one-hot 乘矩阵就是取对应行)
h = W_in[123]          # shape (d,)
 
# 输出层:计算得分
scores = h @ W_out     # shape (V,)
 
# Softmax 得到概率
exp_scores = np.exp(scores - np.max(scores))  # 减去最大值防止溢出
probs = exp_scores / np.sum(exp_scores)
 
# 假设真实上下文词索引是 [45, 67, 234, 567]
context = [45, 67, 234, 567]
loss = -np.sum(np.log(probs[context]))
print("损失值:", loss)

反向传播会更新 W_in[123] 和 W_out 的相应行,使得下一次预测上下文词的概率更高。

2.3 Gensim 实战:训练并使用词向量

Gensim 是 Python 中最常用的 Word2Vec 工具库,几行代码就能训练。

安装:pip install gensim

2.3.1 训练自己的词向量

假设我们有一些中文评论数据(CSV 格式),我们先用 jieba 分词,再训练。

import jieba
from gensim.models import Word2Vec
import pandas as pd
 
# 读取数据(示例中只有 review 列)
df = pd.read_csv('online_shopping_10_cats.csv', encoding='utf-8', usecols=['review'])
 
# 对每条评论分词,构造 sentences 列表
sentences = []
for review in df["review"]:
    words = jieba.lcut(review)                # 分词
    words = [w for w in words if w.strip()]   # 去掉空白
    sentences.append(words)
 
# 训练 Word2Vec 模型
model = Word2Vec(
    sentences,          # 分词后的句子列表
    vector_size=100,    # 词向量维度
    window=5,           # 上下文窗口大小
    min_count=2,        # 词频低于2的忽略
    sg=1,               # 1: Skip-gram, 0: CBOW
    workers=4           # 并行线程数
)
 
# 保存词向量(文本格式,方便查看)
model.wv.save_word2vec_format('my_vectors.kv')

参数详解

vector_size:维度越大,表达能力越强,但计算越慢,一般 100~300。

window:窗口大小,决定上下文范围。窗口越大,越注重长距离语义。

min_count:过滤低频词,既减少词表大小,又提高质量(低频词噪声大)。

sg:选择模型结构,Skip-gram 对低频词更好,CBOW 训练更快。

2.3.2 加载并使用词向量

from gensim.models import KeyedVectors
 
# 加载之前保存的词向量
model = KeyedVectors.load_word2vec_format('my_vectors.kv')
 
# 1. 查看词向量维度
print(model.vector_size)   # 100
 
# 2. 查看某个词的向量
vec = model['地铁']
print(vec.shape)           # (100,)
print(vec[:10])            # 打印前10个数字
 
# 3. 计算两个词的相似度(余弦相似度)
sim = model.similarity('地铁', '公交')
print(f"地铁 vs 公交 相似度: {sim:.4f}")   # 应该比较高,比如 0.78
 
# 4. 找出与“上班”最相似的5个词
similar_words = model.most_similar('上班', topn=5)
print(similar_words)
# 输出示例:[('下班', 0.79), ('工作', 0.74), ('加班', 0.71), ('打卡', 0.68), ('通勤', 0.65)]
 
# 5. 语义推理:爸爸 - 男性 + 女性 ≈ 妈妈
result = model.most_similar(positive=['爸爸', '女性'], negative=['男性'], topn=3)
print(result)
# 输出示例:[('妈妈', 0.68), ('母亲', 0.65), ('外婆', 0.52)]

余弦相似度公式

similarity=w1⋅w2∥w1∥∥w2∥similarity=∥w1∥∥w2∥w1⋅w2

返回值范围 [-1, 1],越接近 1 表示越相似。

2.3.3 使用公开的中文词向量

如果你不想自己训练,可以下载别人训练好的。推荐 Chinese-Word-Vectors,有微博、维基百科等多种语料。

from gensim.models import KeyedVectors
 
# 下载后加载(文件可能是 .bz2 压缩格式,直接支持)
model = KeyedVectors.load_word2vec_format('sgns.weibo.word.bz2')
print(f"词表大小: {len(model.key_to_index)}"print(f"向量维度: {model.vector_size}"

**微博语料下载:**pan.baidu.com/s/1EerLIkjY…

输出内容:

词表大小: 195202
向量维度: 300

2.4 将词向量应用到 PyTorch 模型中

训练好的词向量最大的实用价值是初始化神经网络的第一层(Embedding 层)。这样模型一开始就具备语义知识,收敛更快。

import torch
import torch.nn as nn
from gensim.models import KeyedVectors
 
# 1. 加载预训练的词向量(此处的词向量是上面 2.3 训练好的词向量)
word_vectors = KeyedVectors.load_word2vec_format('my_vectors.kv')
 
# 2. 构建词表映射(词 -> 索引)
word2idx = word_vectors.key_to_index   # 字典
vocab_size = len(word2idx)
embed_dim = word_vectors.vector_size
 
# 3. 构造词向量矩阵,形状 [vocab_size, embed_dim]
embedding_matrix = torch.zeros(vocab_size, embed_dim)
for word, idx in word2idx.items():
    embedding_matrix[idx] = torch.tensor(word_vectors[word])
 
# 4. 用预训练矩阵初始化 PyTorch 的 Embedding 层
embedding_layer = nn.Embedding.from_pretrained(
    embedding_matrix,
    freeze=False      # False: 词向量会在训练中微调;True: 冻结不变
)
 
# 5. 示例:把一句话转换成向量序列
sentence = "我喜欢吃烤鸭"
tokens = jieba.lcut(sentence)          # ['我', '喜欢', '吃', '烤鸭']
indices = [word2idx.get(token, 0) for token in tokens]  # 未登录词用 0(需提前设置 <UNK>)
input_tensor = torch.tensor([indices])  # shape: (1, 4)
 
output = embedding_layer(input_tensor)  # shape: (1, 4, embed_dim)
print(output.shape)  # torch.Size([1, 4, 100])

freeze 参数

freeze=False:词向量会随模型训练继续更新(微调),适合任务与预训练语料有一定差异的场景。

freeze=True:词向量固定,只训练上层网络,适合小数据集或防止过拟合。


3. 静态词向量的局限与上下文表示

3.1 一词多义问题

Word2Vec 为每个词只学习一个固定的向量。这就导致:

  • “我爱吃苹果” 和 “苹果发布了新手机” 中的 “苹果” 向量完全一样,模型永远分不清哪个是水果哪个是公司。

这种每个词只有一个向量的表示称为静态词向量(Static Embeddings)。

3.2 上下文相关词表示:ELMo 和 BERT

为了区分一词多义,研究者提出了动态词向量:词的向量会根据它所在的句子上下文实时变化。

ELMo(2018):使用双向 LSTM 语言模型,每个词的向量由整个句子双向计算得到。
例如,“苹果”在“吃苹果”和“买苹果手机”中会得到不同的向量。

BERT(2018):基于 Transformer 的掩码语言模型(MLM),真正实现了深度双向编码。BERT 的向量表示已经成为现代 NLP 的事实标准。

核心区别

维度

静态词向量(Word2Vec)

动态词向量(BERT)

表示方式

每个词一个固定向量

向量由上下文实时计算

一词多义

无法区分

能根据上下文区分

计算复杂度

低(查表即得)

高(需要模型前向计算)

典型模型

Word2Vec, GloVe

ELMo, BERT, GPT

BERT 等模型的详细介绍超出了本文范围,但你需要知道:今天的大语言模型(GPT、文心一言、通义千问)都基于动态上下文表示,这是它们能理解复杂语义的根本原因。


4. 总结:一张表看懂全流程

阶段

核心任务

通俗解释

代表方法

输出

分词

把句子切成小块

像切蛋糕,切成一口一块

jieba, BPE, SentencePiece

token 序列

词表示(基础)

把 token 变成数字

给每个小块贴编号

One-hot(已淘汰)

稀疏向量

词表示(进阶)

让数字带有语义

给每个小块一个“语义坐标”

Word2Vec, GloVe

稠密向量

词表示(高级)

根据上下文改变数字

同一个词在不同句子里坐标不同

BERT, GPT

动态向量

学习建议

  1. 动手实践

    :把上面的代码复制到你的电脑里跑一遍,哪怕只有几行评论,你也能亲眼看到“苹果”和“香蕉”的向量确实更接近。

  2. 深入理解

    :弄懂 BPE 和 Word2Vec 的原理,对理解现代大模型至关重要。

  3. 关注发展

    :从 Word2Vec 到 BERT,再到 GPT-4,文本表示的技术一直在进化,但核心思想——让机器从上下文中学习语义——从未改变。

希望这篇文章能帮你彻底搞懂 NLP 的文本表示。如果你有任何问题,欢迎在评论区留言讨论。