在自然语言处理(NLP)领域,词嵌入(Word Embedding)是将词汇映射到低维连续向量空间的技术。这种表示方法能够捕捉词汇之间的语义和语法关系,是大多数NLP任务的基础组件。词嵌入的发展经历了从静态到动态、从局部到全局、从词级别到子词级别的演进过程。
词嵌入的核心价值在于:它让计算机能够用数学方式"理解"词语含义,相似的词在向量空间中距离相近,这为后续的文本处理任务提供了基础特征表示。
传统词嵌入方法如Word2Vec和GloVe虽然效果显著,但存在两个主要局限:一是每个词无论上下文如何都对应同一个向量(静态嵌入);二是无法有效处理罕见词和词形变化。这些问题催生了FastText的子词嵌入和BERT等上下文敏感模型的诞生。
Skip-Gram是Word2Vec的两种架构之一,其核心思想是通过中心词预测上下文词。给定一个中心词w_c,模型要预测其周围窗口大小为m的上下文词w_o的概率:
P(w_o|w_c) = exp(u_o^T v_c) / Σ_{w∈V} exp(u_w^T v_c)
其中v_c和u_o分别是中心词和上下文词的向量表示,V是词表。这个softmax公式的难点在于分母需要对整个词表进行计算,当词表很大时(通常有几万到几十万词),计算成本变得非常高。
在实际实现中,我们通常采用负采样(Negative Sampling)来近似这个softmax。负采样的核心思想是将多分类问题转化为二分类问题:对于真实的上下文词(正样本),我们最大化其概率;同时从噪声分布中采样一些词作为负样本,最小化它们的概率。修正后的目标函数变为:
log σ(u_o^T v_c) + Σ_{i=1}^k E_{w_i∼P(w)} [log σ(-u_i^T v_c)]
其中k是负样本数量,σ是sigmoid函数,P(w)是词频的3/4次方分布(经验表明这种分布效果最好)。
连续词袋模型(CBOW)是Word2Vec的另一种架构,与Skip-Gram相反,它通过上下文词预测中心词。CBOW首先将上下文词的向量取平均,然后用这个平均向量预测中心词:
P(w_c|w_o1,...,w_on) = exp(u_c^T (v_o1+...+v_on)/n) / Σ_{w∈V} exp(u_w^T (v_o1+...+v_on)/n)
CBOW的训练速度通常比Skip-Gram快,但在处理罕见词时表现稍差,因为多个上下文词的平均会稀释个别特征词的贡献。
除了负采样,分层softmax(Hierarchical Softmax)是另一种加速训练的技术。它将词表组织成一棵二叉树,每个叶子节点对应一个词。计算概率时,只需要沿着从根到目标词的路径计算一系列二分类概率,将复杂度从O(|V|)降到O(log|V|)。
具体实现中,Huffman树常用于构建这棵二叉树,高频词路径更短,进一步优化计算效率。每个非叶子节点都有一个向量表示,路径上的每个二分类决策使用sigmoid函数:
P(d=left|n,w) = σ(v_n^T v_w)
P(d=right|n,w) = 1 - σ(v_n^T v_w) = σ(-v_n^T v_w)
最终词的概率是路径上所有决策概率的乘积。
下面展示如何使用gensim训练中文Word2Vec模型,以《三国演义》文本为例:
python复制import jieba
import re
from gensim.models import Word2Vec
# 文本预处理
def preprocess_text(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = []
for line in f:
line = line.strip()
if not line:
continue
# 分词并过滤标点
words = [word for word in jieba.lcut(line)
if not re.match("[\s+\.\!\/_,$%^*(+\"\'《》]+|[+——!,。?、~@#¥%……&*():;]+", word)]
if words:
lines.append(words)
return lines
# 加载并预处理文本
corpus = preprocess_text("sanguo.txt")
# 训练Word2Vec模型
model = Word2Vec(
sentences=corpus,
vector_size=100, # 词向量维度
window=5, # 上下文窗口大小
min_count=5, # 忽略出现次数少于5的词
workers=4, # 使用4个线程
sg=1, # 使用Skip-Gram模型
hs=0, # 不使用分层softmax
negative=10, # 负采样数量
epochs=10 # 迭代次数
)
# 保存模型
model.save("sanguo_word2vec.model")
训练完成后,我们可以进行以下应用:
python复制# 查找相似词
similar_words = model.wv.most_similar('曹操', topn=10)
print("与'曹操'最相似的词:", similar_words)
# 词类比任务
analogy = model.wv.most_similar(positive=['孙权', '曹操'], negative=['刘备'], topn=1)
print("孙权 - 刘备 ≈ 曹操 -", analogy[0][0])
# 获取词向量
vector = model.wv['诸葛亮']
print("诸葛亮的词向量维度:", len(vector))
GloVe(Global Vectors for Word Representation)结合了全局统计信息和局部上下文窗口的优点。其核心思想是利用整个语料库的共现统计信息来学习词向量。
GloVe的损失函数基于共现概率比值的观察。例如,考虑"ice"和"steam"这两个词与各种探测词k的共现概率比:
GloVe的目标是让词向量能够编码这些概率比值。模型通过以下目标函数实现:
J = Σ_{i,j=1}^V f(X_{ij}) (w_i^T w̃_j + b_i + b̃_j - log X_{ij})^2
其中X_{ij}是词i和词j的共现次数,f(X_{ij})是权重函数,对高频共现给予更多重视但不过分强调:
f(x) = (x/x_max)^α if x < x_max else 1
通常α=0.75,x_max=100。
以下是加载和使用预训练GloVe词向量的Python实现:
python复制import torch
import numpy as np
class GloVeEmbedding:
def __init__(self, file_path):
self.word2idx = {"<unk>": 0}
self.idx2word = ["<unk>"]
self.embeddings = [np.zeros(100)] # 假设维度为100
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
values = line.strip().split()
word = values[0]
vector = np.asarray(values[1:], dtype='float32')
self.word2idx[word] = len(self.idx2word)
self.idx2word.append(word)
self.embeddings.append(vector)
self.embeddings = np.array(self.embeddings)
self.unk_embedding = self.embeddings[0]
def __getitem__(self, word):
idx = self.word2idx.get(word, 0)
return torch.tensor(self.embeddings[idx])
def get_similar_words(self, word, topk=5):
vec = self[word].numpy()
# 计算余弦相似度
dot_product = np.dot(self.embeddings, vec)
norm = np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(vec)
similarities = dot_product / (norm + 1e-9)
# 获取最相似的词(排除自己)
indices = np.argsort(-similarities)[1:topk+1]
return [(self.idx2word[i], similarities[i]) for i in indices]
# 使用示例
glove = GloVeEmbedding("glove.6B.100d.txt")
print("与'king'最相似的词:", glove.get_similar_words("king"))
词类比是评估词向量质量的常用方法,如"man:woman :: king:?"。用向量运算表示为:
v_queen = v_king - v_man + v_woman
然后找与v_queen最相似的词向量。实现代码如下:
python复制def word_analogy(glove, word1, word2, word3, topk=5):
vec1 = glove[word1].numpy()
vec2 = glove[word2].numpy()
vec3 = glove[word3].numpy()
target_vec = vec2 - vec1 + vec3
target_vec = target_vec / np.linalg.norm(target_vec)
# 计算所有词的相似度
similarities = np.dot(glove.embeddings, target_vec) / (
np.linalg.norm(glove.embeddings, axis=1) * np.linalg.norm(target_vec))
# 排除输入词
indices_to_exclude = [glove.word2idx.get(w, 0) for w in [word1, word2, word3]]
similarities[indices_to_exclude] = -1
# 获取最相似的词
indices = np.argsort(-similarities)[:topk]
return [(glove.idx2word[i], similarities[i]) for i in indices]
# 示例
print("man:woman :: king:", word_analogy(glove, "man", "woman", "king")[0][0])
FastText的核心创新是引入子词(subword)信息,将每个词表示为它的字符n-gram的集合。例如,对于单词"where"(假设n=3),其子词包括:
<wh, whe, her, ere, re>, 以及整个单词
词向量是这些子词向量的和。这种方法带来了几个优势:
FastText的模型架构与Word2Vec类似,区别在于输入词的表示方式。在训练时,FastText的目标函数为:
L = Σ_{w∈C} log P(w_c|w_o) + λΣ_{g∈G_w} ||v_g||^2
其中G_w是词w的子词集合,λ是正则化系数。
使用官方FastText库训练模型的示例:
python复制import fasttext
import fasttext.util
# 训练模型
model = fasttext.train_unsupervised(
input="corpus.txt", # 训练文本
model='skipgram', # 或'cbow'
lr=0.05, # 学习率
dim=100, # 向量维度
ws=5, # 上下文窗口
minn=3, # 最小n-gram
maxn=6, # 最大n-gram
bucket=2000000, # n-gram哈希桶数
thread=4, # 线程数
epoch=5 # 训练轮数
)
# 保存模型
model.save_model("model.bin")
# 使用示例
print("词向量:", model.get_word_vector("人工智能"))
print("最近邻:", model.get_nearest_neighbors("机器学习"))
字节对编码(Byte Pair Encoding)是一种数据驱动的子词分割算法,被广泛应用于现代NLP模型(如GPT、BERT)。其基本思想是:
BPE的优势在于:
以下是BPE的Python实现示例:
python复制from collections import defaultdict, Counter
def learn_bpe(vocab, num_merges):
"""学习BPE操作"""
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i], symbols[i+1]] += freq
merges = {}
for i in range(num_merges):
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
merges[best_pair] = i
new_vocab = defaultdict(int)
for word, freq in vocab.items():
new_word = []
i = 0
while i < len(word.split()):
if i < len(word.split())-1 and (word.split()[i], word.split()[i+1]) == best_pair:
new_word.append("".join(best_pair))
i += 2
else:
new_word.append(word.split()[i])
i += 1
new_vocab[" ".join(new_word)] = freq
vocab = new_vocab
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i], symbols[i+1]] += freq
return merges
# 示例使用
vocab = {
"l o w": 5,
"l o w e r": 2,
"n e w e s t": 6,
"w i d e s t": 3
}
merges = learn_bpe(vocab, 10)
print("学到的BPE合并操作:", merges)
BERT(Bidirectional Encoder Representations from Transformers)的核心创新在于:
BERT的输入表示由三部分组成:
输入序列格式为:
[CLS] 句子A [SEP] 句子B [SEP]
掩码语言模型(MLM)的特别之处在于:
这种策略避免了预训练-微调的不匹配问题,因为微调时没有[MASK]标记。
下一句预测(NSP)任务的目标是判断句子B是否是句子A的实际后续句子,帮助模型理解句子间关系。
以下是使用Hugging Face Transformers库加载中文BERT模型的示例:
python复制from transformers import BertTokenizer, BertModel
import torch
# 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
# 文本编码
text = "自然语言处理是人工智能的重要方向"
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# 获取BERT输出
with torch.no_grad():
outputs = model(**inputs)
# 获取最后一层的隐藏状态 [batch_size, seq_len, hidden_size]
last_hidden_states = outputs.last_hidden_state
print("BERT输出形状:", last_hidden_states.shape)
# 获取句子级别的表示(取[CLS]标记对应的向量)
sentence_embedding = last_hidden_states[:, 0, :]
print("句子向量:", sentence_embedding.shape)
以下是一个简单的文本分类微调示例:
python复制from transformers import BertForSequenceClassification, Trainer, TrainingArguments
from datasets import load_dataset
# 加载数据集
dataset = load_dataset("csv", data_files={"train": "train.csv", "test": "test.csv"})
# 预处理函数
def preprocess_function(examples):
return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=128)
# 应用预处理
tokenized_datasets = dataset.map(preprocess_function, batched=True)
# 加载模型
model = BertForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=2)
# 训练参数
training_args = TrainingArguments(
output_dir="./results",
evaluation_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3,
weight_decay=0.01,
)
# 创建Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
)
# 开始训练
trainer.train()
| 特性 | Word2Vec/GloVe | FastText | BERT |
|---|---|---|---|
| 上下文敏感 | ❌ | ❌ | ✅ |
| 处理未登录词 | ❌ | ✅ | ✅ |
| 捕捉词形变化 | ❌ | ✅ | ✅ |
| 训练速度 | 快 | 中等 | 慢 |
| 资源需求 | 低 | 中等 | 高 |
| 适合任务 | 简单语义任务 | 多语言/形态丰富 | 复杂理解任务 |
简单快速原型:Word2Vec/GloVe
多语言或形态丰富语言:FastText
复杂语义理解:BERT等预训练模型
维度选择:
上下文窗口:
BERT使用建议:
处理长文本:
词嵌入技术仍在快速发展,以下是一些值得关注的方向:
在实际应用中,选择合适的技术需要综合考虑任务需求、数据特点和计算资源。对于大多数中文NLP任务,BERT及其变体通常是当前的最佳选择,而FastText在处理用户生成内容(如社交媒体文本)时表现优异。传统方法如Word2Vec仍然在资源受限的场景下有其实用价值。