自然语言处理应用篇 · 第7章

文本向量化与词向量

让文字拥有"语义坐标":从 One-Hot 的符号世界,到 word2vec 的分布式表示,再到 BERT 上下文向量,理解现代 NLP 的表示学习主线。

🎯 学习目标

  • 理解离散表示(One-Hot/词袋/TF-IDF)的根本缺陷
  • 理解分布式语义假设与词向量的几何意义
  • 理解 word2vec 的 CBOW 与 Skip-gram 两种结构
  • 会用 gensim 训练、加载、查询词向量并做词语类比
  • 掌握余弦相似度与句向量平均池化,实现简易语义检索
  • 了解静态向量局限、预训练中文词向量与 BERT 上下文向量

1离散表示:从符号到数字的第一道桥

前面章节我们把文本转成了 TF-IDF 向量,回顾这条演进线的起点与问题:

One-Hot(独热编码)

词表:[计算机, 电脑, 汽车]
计算机 = [1, 0, 0]
电脑   = [0, 1, 0]
汽车   = [0, 0, 1]

问题一目了然:

  • 语义鸿沟:"计算机"和"电脑"是同义词,但向量夹角 90°、点积为 0,系统认为它们毫无关系。
  • 维度灾难:词表 10 万词 → 每个向量 10 万维,且几乎全是 0(稀疏)。
  • 无法表达类比、蕴含等任何语义关系。

词袋 / TF-IDF(文档向量)

把词的 One-Hot 按计数/权重累加,得到文档向量(第 6 章)。比 One-Hot 实用,但文档向量同样稀疏、无语义,且语序信息仍缺失。

2分布式语义:词的含义藏在邻居里

"You shall know a word by the company it keeps." —— J. R. Firth, 1957
一个词的含义,由它周围经常出现的词决定。

"喝"附近总是水、茶、咖啡;"驾驶"附近总是车、司机、马路。于是"喝 ___"和"饮 ___"的上下文高度相似——即使两个词从未同句出现,模型也能推断它们意思相近。这就是分布式表示(Distributional Representation)。

词向量(Word Embedding):为每个词学习一个低维稠密实数向量(如 100~300 维),让上下文相似的词在向量空间中距离也近。

One-Hot:10万维,99.99% 是 0,词之间等距无关
词向量:300维,全是稠密实数,语义相近的词在空间中成簇

        电脑 ●  ●计算机
              ╲
          手机 ●        ● 卡车
                        ● 汽车

3word2vec:词向量的开山之作

2013 年 Google 开源的 word2vec 用极简神经网络从大规模无标注语料中学词向量,有两种结构:

CBOW 连续词袋Skip-gram 跳字
输入→预测上下文词 → 中心词中心词 → 上下文词
训练速度快慢
低频词效果一般更好(每个低频词都要预测多次)
适用大语料、求快中小语料、重视稀有词
句子:我 爱 自然 语言 处理      窗口大小=2

CBOW:用 [我,爱,语言,处理] 预测中间的 [自然]
Skip-gram:用 [自然] 预测 [我,爱,语言,处理]

训练目标本质:让"真实共现"的词对向量点积高(softmax + 负采样加速)
训练完成后,取隐藏层权重矩阵作为词向量表 —— 副产品成了主角

关键直觉:模型其实不需要"预测得很准",为了完成预测任务,它被迫把语义压缩进那几百维向量——向量本身才是我们要的财富。

神奇的向量运算:词的类比

语义关系在向量空间中表现为方向,可以做加减法:

vec(国王) - vec(男人) + vec(女人) ≈ vec(女王)
vec(北京) - vec(中国) + vec(日本) ≈ vec(东京)
vec(走路) - vec(走) + vec(游) ≈ vec(游泳)

4gensim 训练与使用词向量

# pip install gensim
from gensim.models import Word2Vec
import jieba

# 1) 准备语料:分好词的句子列表(列表的列表)
raw_docs = [
    "深度学习是人工智能的核心技术",
    "机器学习推动人工智能快速发展",
    "自然语言处理让计算机理解人类语言",
    "计算机视觉让机器识别图像和视频",
    "大语言模型显著提升自然语言理解能力",
    "神经网络是深度学习的基础模型",
]
sentences = [jieba.lcut(d) for d in raw_docs]
# 真实项目:语料越大越好(千万~十亿字),且应做成迭代器逐行读,避免爆内存

# 2) 训练
model = Word2Vec(
    sentences=sentences,
    vector_size=100,      # 向量维度(常用 100~300)
    window=5,             # 上下文窗口大小
    min_count=1,          # 词频低于此值忽略(大语料设 5~10 去噪声)
    sg=1,                 # 1=Skip-gram,0=CBOW
    epochs=20,            # 训练轮数
    workers=4,            # 线程数
    negative=5)           # 负采样数量

# 3) 保存 / 加载
model.save("w2v.model")
model = Word2Vec.load("w2v.model")
# 只导出词向量本体(占用更小)
model.wv.save_word2vec_format("vectors.txt", binary=False)
# 4) 使用词向量
wv = model.wv

wv["人工智能"]              # shape=(100,) 的 numpy 向量
wv.similarity("人工智能", "机器学习")      # 余弦相似度 -1~1
wv.most_similar("自然语言", topn=3)        # 最相近的词(带分数)
wv.most_similar(positive=["中国", "东京"], negative=["北京"])  # 类比运算
wv.similar_by_vector(wv["人工智能"] + wv["神经网络"], topn=3)

# 词表外的词会 KeyError,先判断
word = "区块链"
if word in wv:
    print(wv[word])

# 注意:玩具语料太小,相似度结果没有统计意义;
# 真实效果需千万字以上语料,或直接加载预训练向量(下一节)

5余弦相似度与句向量

余弦相似度:用两个向量夹角的余弦衡量方向相似性,只看方向不看长度,是文本向量的标准度量:

import numpy as np

def cosine(a, b):
    a, b = np.asarray(a), np.asarray(b)
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)

# 句向量最简单的做法:词向量平均池化(average pooling)
def sentence_vector(text, wv):
    vecs = [wv[w] for w in jieba.lcut(text) if w in wv]
    if not vecs:
        return np.zeros(wv.vector_size)
    return np.mean(vecs, axis=0)

def doc_similarity(t1, t2, wv):
    return cosine(sentence_vector(t1, wv), sentence_vector(t2, wv))

# print(doc_similarity("人工智能发展很快", "机器学习进步迅速", wv))
# print(doc_similarity("人工智能发展很快", "今天中午吃了面条", wv))
# 前者余弦高,后者低 —— 即使没有一个相同词,语义近的句子也能被识别

平均池化朴素但有效,工程上改进:① 用 TF-IDF 加权平均(重要词权重大);② 去停用词后再平均;③ SIF(平滑逆频率加权 + 去主成分);④ 直接用 BERT 句向量。gensim 也内置 TF-IDF/LSI/LDA 等主题向量,可在无 w2v 时构建文档向量。

6预训练中文词向量(不必从零训练)

自己训练需要海量语料与时间。推荐直接使用大厂发布的预训练词向量(通用语料训练,覆盖百万词):

资源规模/特点获取
腾讯 AI Lab 中文词向量800万+词/短语,200维,覆盖新词短语,质量高搜索 "Tencent AI Lab Embedding"
Chinese-Word-Vectors(哈工大 SCIR)多领域(百科/新闻/微博),300维GitHub: Embedding/Chinese-Word-Vectors
fastText 中文向量Facebook,subword 机制对未登录词友好fastText 官网 vectors 页
# 加载 text 格式预训练向量(腾讯/哈工大均提供 txt/gz)
from gensim.models import KeyedVectors
wv = KeyedVectors.load_word2vec_format(
    "tencent-ailab-embedding-zh-d200-v0.2.0-s.txt",
    binary=False, unicode_errors="ignore")
print(wv.most_similar("碳中和", topn=5))

# 大文件建议转二进制加速加载:
# wv.save_word2vec_format("tencent.bin", binary=True)
# KeyedVectors.load_word2vec_format("tencent.bin", binary=True)

7静态向量的局限 → 上下文向量

word2vec 一个词只有一个向量,无法处理多义词:

"苹果发布了新手机"   苹果 = 公司
"苹果多少钱一斤"     苹果 = 水果
word2vec 中两处"苹果"共享同一个向量,语义被强行平均

演进路线:

  • ELMo(2018):基于 LSTM 的上下文相关词向量,同一个词在不同句子中向量不同。
  • BERT(2018,Google):基于 Transformer 的双向预训练模型,通过 MLM 掩码语言模型任务学习深度上下文表示,横扫 11 项 NLP 榜单。中文模型:bert-base-chinese。
  • GPT 系列 / 大语言模型:自回归生成式预训练 + 指令对齐,不仅能理解还能生成。

用 transformers 获取 BERT 句向量(选学)

pip install transformers torch
# 首次运行会自动下载 bert-base-chinese(约 400MB),可设镜像:
# set HF_ENDPOINT=https://hf-mirror.com   (Windows 命令行)
import torch
from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-chinese"
tok = AutoTokenizer.from_pretrained(model_name)
bert = AutoModel.from_pretrained(model_name)

@torch.no_grad()
def bert_sentence_vector(text):
    inputs = tok(text, return_tensors="pt", truncation=True, max_length=128)
    outputs = bert(**inputs)
    # 取 <[BOS_never_used_51bce0c785ca2f68081bfa7d91973934]> 位置向量作为整句表示(简单基线;更推荐 mean pooling)
    cls = outputs.last_hidden_state[0, 0]
    return torch.nn.functional.normalize(cls, dim=0).numpy()

import numpy as np
v1 = bert_sentence_vector("这家餐厅味道很好")
v2 = bert_sentence_vector("菜品不错,值得推荐")
v3 = bert_sentence_vector("明天可能会下雨")
print(v1 @ v2, v1 @ v3)    # v1-v2 相似度明显更高

生产级语义检索/聚类推荐使用专门训练过的句向量模型(如 text2vec、bge-small-zh、m3e、sentence-transformers 系列),余弦相似度质量显著高于直接取 BERT 的 <[BOS_never_used_51bce0c785ca2f68081bfa7d91973934]>。

8案例:语义相似度检索

# 思路:FAQ 文档全部编码成向量入库,新问题向量化后取余弦最近邻
import numpy as np

faqs = [
    "如何申请退货退款",
    "订单几天可以到货",
    "优惠券怎么使用",
    "账号密码忘记怎么办",
]
query = "买的东西不想要了能退吗"

# 方案一:TF-IDF + 余弦(无需训练,传统基线)
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
tv = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")
mat = tv.fit_transform([" ".join(jieba.lcut(x)) for x in faqs + [query]])
sims = cosine_similarity(mat[-1], mat[:-1])[0]
print(faqs[sims.argmax()], sims.round(3))
# TF-IDF 缺陷:query 与 FAQ 没有共同词(退货/退款 vs 不想要/退)时可能失配

# 方案二:词向量/句向量(语义泛化,方案见第8章搜索引擎项目)
# v_q = sentence_vector(query, wv)
# 对每条 FAQ 向量算余弦,取 TopK —— 没共同词也能匹配

9本章小结与练习

📌 关键结论

  • One-Hot/词袋稀疏且无语义;分布式表示让"上下文相近 → 向量相近"。
  • word2vec:CBOW(上下文预测中心,快)/ Skip-gram(反之,低频词好),gensim 训练。
  • 句向量基线:词向量平均(可 TF-IDF 加权);相似度用余弦。
  • word2vec 一词一向量,不懂多义;BERT 提供上下文向量,生产检索用 bge 等句向量模型。
练习1:为什么 One-Hot 向量无法表达"计算机"和"电脑"的相似性?词向量为什么能?
One-Hot 中两个词各占一个正交维度,点积为 0、夹角 90°,在表示上完全无关,且维度随词表膨胀。词向量基于分布式假设从共现上下文学习:"计算机"和"电脑"出现在极相似的上下文中,模型为降低预测误差会把二者压缩到向量空间的相近位置,余弦相似度高,自然表达了同义关系。
练习2:word2vec 的 CBOW 和 Skip-gram 在"输入什么预测什么"上有何区别?小语料重视罕见词应选哪种?
CBOW 用上下文多个词预测中心词,训练快但对低频词学习不足;Skip-gram 用中心词预测上下文,每个低频词作为中心时都会产生多个训练样本,因此低频词向量质量更好,代价是训练更慢。小语料且重视罕见词应选 Skip-gram(gensim 中 sg=1)。
练习3:"苹果公司财报"和"苹果五元一斤"中两个"苹果",word2vec 与 BERT 的表示有什么本质差别?
word2vec 是静态词向量,"苹果"在词表中只有一个向量,两种语境共用,无法区分公司与水果两种含义;BERT 是上下文相关表示,同一个词在不同句子中经过 Transformer 自注意力与全句交互后得到不同的向量,前者会与"公司、财报"语境靠近,后者与"五元、斤(水果)"语境靠近,从而实现多义消歧。