File size: 4,593 Bytes
91450f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ebebbb
91450f4
e9956a9
91450f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9956a9
91450f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Generate Word2Vec notebook."""

import nbformat as nbf

nb = nbf.v4.new_notebook()
nb.metadata = {
    "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
    "language_info": {"name": "python", "version": "3.12.0"},
}

cells = []
def md(s): cells.append(nbf.v4.new_markdown_cell(s))
def code(s): cells.append(nbf.v4.new_code_cell(s))

md("""\
# Word2Vec: Word Embeddings

CBOW + Skip-gram with Negative Sampling, trained on text8.
""")

md("""\
## 背景

Word2Vec(Mikolov 2013)学习词的分布式表示(dense vectors),
使得语义相近的词在向量空间中也相近。

两种架构:
- **CBOW**:用上下文词预测中心词
- **Skip-gram**:用中心词预测上下文词

两种加速技巧:
- **Negative Sampling**:训练时只更新少量负样本,避免完整的 softmax
- **Subsampling**:高频词("the", "a")被概率性丢弃,加速训练
""")

md("""\
## 数学原理

### Skip-gram with Negative Sampling

给定中心词 $w_t$ 和上下文词 $w_c$:

$$\\mathcal{L} = -\\log \\sigma(v_{w_c} \\cdot v_{w_t}) - \\sum_{k=1}^K \\log \\sigma(-v_{w_k} \\cdot v_{w_t})$$

其中 $w_k$ 是从噪声分布 $P(w) \\propto \\text{count}(w)^{0.75}$ 采样的负样本。

### CBOW

$$\\mathcal{L} = -\\log \\sigma(v_{w_t} \\cdot \\bar{v}_{\\text{ctx}}) - \\sum_{k=1}^K \\log \\sigma(-v_{w_k} \\cdot \\bar{v}_{\\text{ctx}})$$

其中 $\\bar{v}_{\\text{ctx}} = \\frac{1}{|C|} \\sum_{c \\in C} v_{w_c}$。
""")

code("""\
import torch
from torch.utils.data import DataLoader
from nlp.word2vec.train import (
    load_texts, build_vocab, subsample, generate_training_pairs,
    NoiseSampler, CBOWDataset, SkipGramDataset, train_epoch,
)
from nlp.word2vec.model import Word2Vec
from utils.device import get_device

device = get_device()
print(f"Device: {device}")
""")

code("""\
# 加载数据
print("Loading text8...")
texts = load_texts()
word_to_id, id_to_word, vocab = build_vocab(texts, min_count=3)
vocab_size = len(word_to_id)
print(f"Vocabulary: {vocab_size:,}")
""")

code("""\
# 生成训练数据
tokenized = subsample(texts, word_to_id)
cbow_pairs, skipgram_pairs = generate_training_pairs(tokenized, window_size=2)

max_pairs = 100000
cbow_pairs = cbow_pairs[:max_pairs]
sg_pairs = skipgram_pairs[:max_pairs]

cbow_dataset = CBOWDataset(cbow_pairs, max_window=2)
sg_dataset = SkipGramDataset(sg_pairs, vocab_size)
cbow_loader = DataLoader(cbow_dataset, batch_size=128, shuffle=True)
sg_loader = DataLoader(sg_dataset, batch_size=128, shuffle=True)
noise_sampler = NoiseSampler(vocab, vocab_size)
print(f"CBOW: {len(cbow_pairs):,}  Skip-gram: {len(sg_pairs):,}")
""")

md("""## 训练

> ⏱ 预估耗时:**5 epoch × ~1min/epoch ≈ 5 分钟**(M4 Max, batch_size=128)
""")

code("""\
NUM_EPOCHS = 5
LR = 0.01
K_NEG = 5

model_sg = Word2Vec(vocab_size, embed_dim=50)
optimizer = torch.optim.Adam(model_sg.parameters(), lr=LR)

sg_loss_hist = []
for epoch in range(1, NUM_EPOCHS + 1):
    loss = train_epoch(model_sg, sg_loader, noise_sampler, optimizer, k=K_NEG, mode="skipgram")
    sg_loss_hist.append(loss)
    print(f"Skip-gram Epoch [{epoch}/{NUM_EPOCHS}]  Loss: {loss:.4f}")
""")

code("""\
import matplotlib.pyplot as plt
from utils.device import get_device
plt.plot(sg_loss_hist, marker='o')
plt.xlabel("Epoch"); plt.ylabel("Loss"); plt.title("Skip-gram Training Loss"); plt.grid(True)
plt.show()
""")

md("""## 词向量相似度搜索

训练完成后,用余弦相似度查找语义相近的词。""")

code("""\
embeddings = model_sg.get_embeddings()

def similar_words(word, top_k=10):
    if word not in word_to_id:
        print(f"'{word}' not in vocabulary")
        return
    idx = word_to_id[word]
    vec = embeddings[idx]
    sims = (embeddings @ vec) / (torch.norm(embeddings, dim=1) * torch.norm(vec))
    vals, inds = torch.topk(sims, top_k + 1)
    print(f"Words similar to '{word}':")
    for val, idx in zip(vals[1:], inds[1:]):
        print(f"  {id_to_word[idx.item()]:<15} {val.item():.4f}")

for w in ["computer", "science", "king", "water"]:
    print(); similar_words(w)
""")

md("""\
## 思考题

1. Skip-gram 和 CBOW 分别擅长什么?(低频词 vs 高频词)"
2. 负样本数量 $k$ 越大越好还是越小越好?典型值是多少?
3. 为什么噪声分布要用 $\\text{count}(w)^{0.75}$ 而不是原始频率?
4. 词嵌入的维度(50)对语义质量有什么影响?试试改到 100。
""")

nb.cells = cells
out = "nlp/word2vec/word2vec.ipynb"
with open(out, "w") as f:
    nbf.write(nb, f)
print(f"Generated {out}")