Word2vec实战:用Python从零实现Skip-Gram模型(附代码)
·
Word2vec实战:用Python从零实现Skip-Gram模型(附代码)
在自然语言处理领域,词向量技术早已成为基础但至关重要的存在。想象一下,当我们能够将词语转化为具有语义信息的数字向量,计算机就能像理解坐标一样理解词语之间的关系——"国王"减去"男人"加上"女人"约等于"女王",这种直观的向量运算正是词向量魅力的体现。本文将带您从零开始,用Python和NumPy实现经典的Skip-Gram模型,不仅理解其数学原理,更能亲手构建出具备实用价值的词向量系统。
1. 环境准备与数据预处理
1.1 基础工具配置
实现Skip-Gram模型需要以下Python库支持:
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from sklearn.manifold import TSNE
建议使用Python 3.8+环境,主要依赖库版本要求:
- NumPy ≥ 1.20
- Matplotlib ≥ 3.4
1.2 文本预处理流程
原始文本需要经过标准化处理才能用于模型训练:
- 文本清洗:移除标点、特殊字符,统一为小写
- 分词处理:将句子拆分为单词序列
- 构建词汇表:统计词频,建立词到索引的映射
def preprocess_text(text):
text = text.lower()
text = ''.join([c for c in text if c.isalpha() or c == ' '])
words = text.split()
return words
1.3 词汇表构建技巧
高效的词汇表管理是模型成功的关键:
| 策略 | 说明 | 实现方法 |
|---|---|---|
| 词频过滤 | 去除低频噪声词 | Counter(words).most_common(vocab_size) |
| 特殊标记 | 处理未知词 | 添加<UNK> token |
| 二次采样 | 平衡高频词影响 | $P(w_i) = 1 - \sqrt{t/f(w_i)}$ |
提示:实际应用中,建议保留词频≥5的词语,词汇表大小通常控制在10,000-50,000之间
2. Skip-Gram模型架构解析
2.1 模型核心组件
Skip-Gram通过中心词预测上下文词,主要包含三个部分:
- 输入层:中心词的one-hot编码
- 投影层:词嵌入矩阵(核心参数)
- 输出层:上下文词概率分布
class SkipGram:
def __init__(self, vocab_size, embedding_dim):
self.W1 = np.random.randn(vocab_size, embedding_dim) * 0.01
self.W2 = np.random.randn(embedding_dim, vocab_size) * 0.01
2.2 负采样优化
传统Softmax计算开销大,负采样通过对比学习提高效率:
- 正样本:真实上下文词
- 负样本:随机采样的非上下文词
负采样概率公式: $$ P(w_i) = \frac{f(w_i)^{3/4}}{\sum_j f(w_j)^{3/4}} $$
2.3 损失函数设计
采用负对数似然损失,结合负采样:
def negative_sampling_loss(target, context, negative_samples):
# 正样本得分
pos_score = np.dot(self.W1[target], self.W2[:, context])
# 负样本得分
neg_scores = np.dot(self.W1[target], self.W2[:, negative_samples])
# 计算损失
loss = -np.log(sigmoid(pos_score)) - np.sum(np.log(sigmoid(-neg_scores)))
return loss
3. 模型训练实战
3.1 参数初始化策略
词向量矩阵初始化直接影响训练效果:
| 方法 | 优点 | 适用场景 |
|---|---|---|
| 随机小值 | 简单快速 | 小型词汇表 |
| Xavier初始化 | 保持方差一致 | 深度模型 |
| 预训练初始化 | 加速收敛 | 迁移学习 |
推荐初始化方案:
embedding_dim = 300
W = np.random.uniform(-1, 1, (vocab_size, embedding_dim)) / embedding_dim
3.2 训练过程优化
实际训练时需要关注的几个关键点:
- 学习率调整:初始0.025,线性衰减至0.0001
- 批次生成:使用滑动窗口构建(center, context)对
- 梯度裁剪:防止梯度爆炸
def train_skipgram(corpus, vocab, embedding_dim=100, epochs=5):
model = SkipGram(len(vocab), embedding_dim)
for epoch in range(epochs):
loss = 0
for center, context in generate_batches(corpus):
grad_W1, grad_W2 = compute_gradients(center, context)
model.W1 -= learning_rate * grad_W1
model.W2 -= learning_rate * grad_W2
loss += calculate_loss(center, context)
print(f"Epoch {epoch}, Loss: {loss/len(corpus)}")
3.3 训练监控技巧
有效监控训练过程的几种方法:
- 损失曲线可视化:观察收敛趋势
- 最近邻检查:定期查询词向量的最近邻
- 类比任务评估:man→woman as king→?
def plot_learning_curve(loss_history):
plt.plot(loss_history)
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.show()
4. 词向量应用与可视化
4.1 相似度计算
词向量最直接的应用是计算词语相似度:
def cosine_similarity(vec1, vec2):
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
def most_similar(word, vocab, W, topn=5):
idx = vocab[word]
vec = W[idx]
similarities = []
for i, other_vec in enumerate(W):
if i != idx:
similarities.append((vocab.lookup_token(i), cosine_similarity(vec, other_vec)))
return sorted(similarities, key=lambda x: -x[1])[:topn]
4.2 t-SNE降维可视化
高维词向量可通过t-SNE投影到2D空间:
def visualize_embeddings(vocab, W, num_words=100):
tsne = TSNE(n_components=2, random_state=0)
words = [vocab.lookup_token(i) for i in range(min(num_words, len(vocab)))]
vectors = W[:num_words]
Y = tsne.fit_transform(vectors)
plt.figure(figsize=(12,8))
for i, word in enumerate(words):
plt.scatter(Y[i,0], Y[i,1])
plt.annotate(word, xy=(Y[i,0], Y[i,1]), xytext=(5,2),
textcoords='offset points', ha='right', va='bottom')
plt.show()
4.3 实际应用案例
训练好的词向量可用于多种下游任务:
- 文本分类:作为特征输入
- 推荐系统:物品描述的向量表示
- 问答系统:问题与答案的语义匹配
# 示例:基于词向量的简单文本分类
def document_vector(doc, vocab, W):
vectors = [W[vocab[word]] for word in doc if word in vocab]
return np.mean(vectors, axis=0) if vectors else np.zeros(W.shape[1])
5. 高级优化与技巧
5.1 动态上下文窗口
传统固定窗口的改进方案:
- 自适应窗口大小:根据词频调整
- 位置加权:距离中心词越近权重越高
实现示例:
def dynamic_window(center_idx, sentence, max_window=5):
word_freq = get_word_frequencies()
base_window = max_window // (1 + np.log(word_freq[center_idx]))
return max(1, int(base_window))
5.2 子词信息融合
处理罕见词和形态学关系:
- 字符n-gram:将单词拆分为子单元
- FastText风格:用子词向量求和表示整词
def get_subword_vectors(word, subword_dict, W):
subwords = [word[i:i+n] for n in (3,4,5) for i in range(len(word)-n+1)]
return [W[subword_dict[sw]] for sw in subwords if sw in subword_dict]
5.3 多任务学习
联合优化Skip-Gram与其他相关任务:
| 任务类型 | 损失函数 | 共享参数 |
|---|---|---|
| 词性标注 | 交叉熵 | 词嵌入层 |
| 依存分析 | 边分类损失 | 投影矩阵 |
| 命名实体识别 | CRF损失 | 上下文向量 |
class MultiTaskSkipGram(SkipGram):
def __init__(self, vocab_size, embedding_dim, num_pos_tags):
super().__init__(vocab_size, embedding_dim)
self.pos_layer = np.random.randn(embedding_dim, num_pos_tags) * 0.01
def pos_tagging_loss(self, word_idx, pos_tag):
hidden = self.W1[word_idx]
pos_scores = np.dot(hidden, self.pos_layer)
return -np.log(softmax(pos_scores)[pos_tag])
6. 性能优化策略
6.1 并行化训练
加速大规模语料训练的几种方法:
- 数据并行:将语料分片多线程处理
- 模型并行:拆分参数矩阵到不同设备
- 异步更新:Hogwild!算法
from multiprocessing import Pool
def parallel_train(args):
model, batch = args
gradients = model.compute_gradients(batch)
return gradients
with Pool(processes=4) as pool:
results = pool.map(parallel_train, [(model, b) for b in batches])
6.2 内存优化
处理超大规模词汇表的技巧:
| 技术 | 节省内存 | 实现复杂度 |
|---|---|---|
| 哈希技巧 | 高 | 低 |
| 矩阵分块 | 中 | 中 |
| 量化压缩 | 极高 | 高 |
# 使用内存映射处理大矩阵
W = np.memmap('embeddings.bin', dtype='float32',
mode='w+', shape=(vocab_size, dim))
6.3 混合精度训练
利用现代GPU的FP16计算能力:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
with autocast():
loss = model(batch)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
7. 模型评估与调优
7.1 内在评估方法
直接评估词向量质量的指标:
- 词语相似度:与人类评分相关性
- 类比任务:语义/句法关系准确率
- 聚类质量:ARI、NMI等指标
def evaluate_analogies(vocab, W, analogy_file):
# 加载类比问题集
analogies = load_analogies(analogy_file)
correct = 0
for a, b, c, d in analogies:
vec = W[vocab[b]] - W[vocab[a]] + W[vocab[c]]
closest = find_closest_word(vec, vocab, W)
if closest == d:
correct += 1
return correct / len(analogies)
7.2 超参数搜索
关键超参数的影响及调优范围:
| 参数 | 典型值 | 影响 |
|---|---|---|
| 向量维度 | 100-500 | 表征能力 |
| 窗口大小 | 2-10 | 上下文范围 |
| 负采样数 | 5-20 | 训练效率 |
| 学习率 | 0.001-0.1 | 收敛速度 |
from sklearn.model_selection import ParameterGrid
param_grid = {
'dim': [100, 200, 300],
'window': [3, 5, 7],
'negative': [5, 10, 15]
}
for params in ParameterGrid(param_grid):
model = SkipGram(vocab_size, **params)
score = evaluate(model)
print(f"{params}: {score}")
7.3 错误分析与改进
常见问题及解决方案:
- 语义混淆:增大训练数据量
- 词频偏差:调整二次采样率
- 过拟合:增加负采样数量
- 欠拟合:提高向量维度
def error_analysis(model, vocab, W):
# 找出相似度异常的词语对
anomalies = []
for word in vocab:
similars = most_similar(word, vocab, W)
if any(not is_semantically_related(word, sim) for sim in similars):
anomalies.append((word, similars))
return anomalies
更多推荐
所有评论(0)