为什么 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方便后续查找。TokenID我0爱1你2北京3烤鸭41.2 英文分词三种粒度英文有天然的空格但事情没那么简单。我们按粒度从大到小介绍。1.2.1 词级分词Word-level最简单粗暴——按空格和标点切。优点简单符合直觉。缺点OOVOut-Of-Vocabulary问题。如果词表里没有 “outperform”这个词就变成 UNK信息丢失。另外“models.” 带着句号与 “models” 不同也会被当作两个词。1.2.2 字符级分词Character-level每个字符包括标点、空格都是一个 token。优点OOV 几乎不存在任何字符都在词表中。缺点序列变得极长单个字符语义弱模型很难学习。比如 ‘h’ 单独出现几乎没有意义。1.2.3 子词级分词Subword-level—— 现代 NLP 的标配平衡上述两种方案常见词保留完整罕见词拆成有意义的子词。比如 “outperform” 拆成 “out” 和 “perform”两者都是常见子词既解决了 OOV又保留了语义。BPEByte Pair Encoding是最经典的子词算法步骤如下初始词表所有字符加上结束符 /w。统计语料中所有相邻符号对的频次。把频次最高的符号对合并成新的子词加入词表。重复 2-3 直到词表达到预设大小。举例语料中有 “low” 和 “lower”初始词表 {l, o, w, e, r, /w}。统计发现 (l, o) 出现 2 次(o, w) 出现 2 次(w, e) 出现 1 次… 先合并 (l, o) 成 “lo”再合并 (lo, w) 成 “low”最终 “lower” 被分成 [“low”, “er”]。其他子词算法WordPieceBERT 使用类似 BPE但合并时选择使语言模型似然增加最多的 pair。Unigram Language Model从大词表开始逐步删除无用子词。1.2.4 代码示例训练自己的 BPE 分词器使用 tokenizers 库语料文件corpus_en.txtTransformers 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_tokenUNK)) tokenizer.pre_tokenizer Whitespace() # 先用空格预切分 # 训练器设定词表大小 3000 trainer BpeTrainer(vocab_size3000, 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_size3000 控制最终词表大小。输出内容[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 jiebaimport jieba text 小明毕业于北京大学计算机系 # 精确模式默认 print(jieba.lcut(text)) # [小明, 毕业, 于, 北京大学, 计算机系] # 全模式扫描所有可能的词 print(jieba.lcut(text, cut_allTrue)) # [小,明,毕业,于,北京,北京大学,大学,计算,计算机,计算机系,算机,系] # 搜索引擎模式精确模式基础上对长词再切分 print(jieba.lcut_for_search(text)) # [小明, 毕业, 于, 北京, 大学, 北京大学, 计算, 计算机, 计算机系]三种模式对比模式特点适用场景精确模式最合理切分文本分析、模型输入全模式冗余切分召回率高关键词提取搜索引擎模式细粒度搜索引擎索引自定义词典jieba 默认词典可能缺少领域新词如“大模型”、“深度学习”可以手动添加。词典文件格式my_dict.txt大模型 10 n 深度学习 8 n加载jieba.load_userdict(my_dict.txt) # 或者动态添加 jieba.add_word(Transformer, freq5) 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( inputchinese_corpus.txt, model_prefixchinese_sp, vocab_size8000, character_coverage0.9995, # 覆盖几乎所有汉字 model_typebpe # 可选 unigram 或 bpe ) # 加载模型 sp spm.SentencePieceProcessor() sp.load(chinese_sp.model) # 分词 text 我爱北京天安门 tokens sp.encode(text, out_typestr) print(tokens) # 例如 [我, 爱, 北京, 天安门] # 转为 ID ids sp.encode(text, out_typeint) print(ids) # [123, 45, 678, 901] # 还原 decoded sp.decode(ids) print(decoded) # 我爱北京天安门character_coverage 对中文很重要设为 0.9995 表示覆盖 99.95% 的字符避免生僻字变成 UNK。1.4 分词工具总结工具粒度优点缺点jieba词级易用可自定义词典存在 OOV无法处理未登录词SentencePiece子词级无 OOV语言无关需要训练稍复杂Hugging Face Tokenizers子词级与 Transformer 库无缝集成极快生态依赖2. 词表示把 token 变成数字分词后我们得到 token 序列。接下来要把每个 token 变成计算机能计算的数字。这一步叫“词表示”。2.1 One-hot 编码最原始的想法假设词表大小为 V每个词用一个 V 维的向量表示该词对应位置为 1其余为 0。致命缺陷稀疏且维度高词表 10 万向量就是 10 万维存储和计算效率极低。没有语义关系“苹果” 和 “香蕉” 的向量内积为 0模型无法知道它们相似。无法处理 OOV新词无法表示。所以现代 NLP 基本不用 One-hot 作为输入特征只作为某些算法的中间表示。2.2 语义化词向量Word2Vec 的革命Word2Vec2013提出用低维稠密向量比如 100 维表示词并通过上下文自动学习语义——相似的词在向量空间中距离近。2.2.1 核心思想分布假设“You shall know a word by the company it keeps.” —— 一个词的意思由它的上下文决定。例如“苹果” 和 “香蕉” 经常出现在“吃一个___”中所以它们的向量会被拉近。2.2.2 Word2Vec 的两种模型结构CBOWContinuous 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-hotV 维。隐藏层权重矩阵 W_in 形状 V × d每一行是一个词的词向量这正是我们要学的。输出层权重矩阵 W_out 形状 d × V再加 Softmax输出每个词作为预测的概率。前向传播Skip-gram 为例import numpy as np # 假设 V10000, d100 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 gensim2.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, encodingutf-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_size100, # 词向量维度 window5, # 上下文窗口大小 min_count2, # 词频低于2的忽略 sg1, # 1: Skip-gram, 0: CBOW workers4 # 并行线程数 ) # 保存词向量文本格式方便查看 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(上班, topn5) print(similar_words) # 输出示例[(下班, 0.79), (工作, 0.74), (加班, 0.71), (打卡, 0.68), (通勤, 0.65)] # 5. 语义推理爸爸 - 男性 女性 ≈ 妈妈 result model.most_similar(positive[爸爸, 女性], negative[男性], topn3) print(result) # 输出示例[(妈妈, 0.68), (母亲, 0.65), (外婆, 0.52)]余弦相似度公式similarityw1⋅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})微博语料下载https://pan.baidu.com/s/1EerLIkjYGUT26Ui_LvejHA?pwdv237输出内容词表大小: 195202 向量维度: 3002.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, freezeFalse # 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 参数freezeFalse词向量会随模型训练继续更新微调适合任务与预训练语料有一定差异的场景。freezeTrue词向量固定只训练上层网络适合小数据集或防止过拟合。3. 静态词向量的局限与上下文表示3.1 一词多义问题Word2Vec 为每个词只学习一个固定的向量。这就导致“我爱吃苹果” 和 “苹果发布了新手机” 中的 “苹果” 向量完全一样模型永远分不清哪个是水果哪个是公司。这种每个词只有一个向量的表示称为静态词向量Static Embeddings。3.2 上下文相关词表示ELMo 和 BERT为了区分一词多义研究者提出了动态词向量词的向量会根据它所在的句子上下文实时变化。ELMo2018使用双向 LSTM 语言模型每个词的向量由整个句子双向计算得到。例如“苹果”在“吃苹果”和“买苹果手机”中会得到不同的向量。BERT2018基于 Transformer 的掩码语言模型MLM真正实现了深度双向编码。BERT 的向量表示已经成为现代 NLP 的事实标准。核心区别维度静态词向量Word2Vec动态词向量BERT表示方式每个词一个固定向量向量由上下文实时计算一词多义无法区分能根据上下文区分计算复杂度低查表即得高需要模型前向计算典型模型Word2Vec, GloVeELMo, BERT, GPTBERT 等模型的详细介绍超出了本文范围但你需要知道今天的大语言模型GPT、文心一言、通义千问都基于动态上下文表示这是它们能理解复杂语义的根本原因。4. 总结一张表看懂全流程阶段核心任务通俗解释代表方法输出分词把句子切成小块像切蛋糕切成一口一块jieba, BPE, SentencePiecetoken 序列词表示基础把 token 变成数字给每个小块贴编号One-hot已淘汰稀疏向量词表示进阶让数字带有语义给每个小块一个“语义坐标”Word2Vec, GloVe稠密向量词表示高级根据上下文改变数字同一个词在不同句子里坐标不同BERT, GPT动态向量学习建议动手实践把上面的代码复制到你的电脑里跑一遍哪怕只有几行评论你也能亲眼看到“苹果”和“香蕉”的向量确实更接近。深入理解弄懂 BPE 和 Word2Vec 的原理对理解现代大模型至关重要。关注发展从 Word2Vec 到 BERT再到 GPT-4文本表示的技术一直在进化但核心思想——让机器从上下文中学习语义——从未改变。希望这篇文章能帮你彻底搞懂 NLP 的文本表示。如果你有任何问题欢迎在评论区留言讨论。