统计语言模型(Statistical Language Model, SLM) 和 神经语言模型(Neural Language Model, NLM) 是自然语言处理中用来计算词序列概率(即判断一句话是否像人话)的两大类方法。它们代表了NLP从“基于规则/统计”向“基于表示学习/深度学习”的范式转变,但统计语言模型是“死记硬背”(查词典统计词频),神经语言模型是“理解归纳”(学习词向量和上下文语义)。后者凭借更强的泛化能力和长距离建模能力,已成为当今 NLP 的绝对主流。
Word2Vec分为CBOW和Skip-gram,两者区别是CBOW 用周围词猜中间词; Skip-gram 用中间词猜周围词。
CBOW(Continuous Bag-of-Words,连续词袋模型)
一、核心思想,我 爱 自然 语言 处理,如果中心词是 “自然”,窗口大小为 2,CBOW 要做的是:把这些上下文词作为输入,预测出中心词 “自然”。
二、核心目标,找到低纬度词向量。把模型中的嵌入矩阵 W 取出来,作为每个词的向量表示。
三、模型架构
1,输入层
输入是上下文词的 one-hot 向量。
2,嵌入层 / 投影层
通过一个共享的词向量矩阵,把每个上下文词映射成稠密向量,然后对这些向量求平均或求和
h=12c∑i=−c,i≠0cvwt+i
h = \\frac{1}{2c}\\sum_{i=-c,i\\neq0}^{c} v_{w_{t+i}}
h=2c1i=−c,i=0∑cvwt+i
其中 c 是窗口大小,v 是输入词向量
3, 融合层(词袋)
- 将 上下文词向量求和或求平均,得到一个综合上下文向量。
- 因为这里忽略了上下文词的顺序,所以叫“词袋”
4, 隐藏层(可选)
对融合后的向量做一次线性变换 + 非线性激活(如 ReLU),映射到隐藏维度。
5,输出层
用另一个矩阵把 h 映射到词表大小的向量,再经过 Softmax,得到中心词的概率:
P(wt∣context)=exp(uwtTh)∑w=1Vexp(uwTh)
P(w_t \\mid context) = \\frac{\\exp(u_{w_t}^T h)}{\\sum_{w=1}^{V}\\exp(u_{w}^T h)}
P(wt∣context)=∑w=1Vexp(uwTh)exp(uwtTh)
训练目标就是最大化真实中心词的概率,等价于最小化交叉熵损失
6, 概率化
用 softmax 或 log_softmax 将得分转为概率分布
四、训练优化
由于词表通常很大,直接做 Softmax 很慢,Word2Vec 常用两种近似方法: - 层次 Softmax(Hierarchical Softmax):用 Huffman 树把词表组织起来,把多分类变成多个二分类。
- 负采样(Negative Sampling):只更新正样本和少量负样本,训练速度更快,也更常用。
优化方法通常用 SGD 或 Adam。
五、CBOW 与 Skip-gram 的对比
| 输入/输出 | 上下文词 → 中心词 | 中心词 → 上下文词 |
| 训练速度 | 较快 | 较慢 |
| 高频词效果 | 较好 | 一般 |
| 低频词效果 | 一般 | 较好 |
| 计算量 | 较小 | 较大 |
| 词序信息 | 丢失较多 | 保留相对多一些 |
六、优缺点
优点:
- 训练速度快,适合大规模语料。
- 对高频词学习较好。
- 得到的词向量能捕捉一定的语义和语法关系。
缺点: - 上下文词顺序被平均掉,丢失词序信息。
- 静态词向量,无法处理一词多义。
- 对低频词效果不如 Skip-gram。
- 窗口大小等超参数影响较大。
七、代码解答
1,导入与超参数
"""任务:根据中心词左右各 2 个上下文词,预测中心词。"""
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
from tqdm import tqdm
import re
#任务:已经有了语料库,1、构造训练集数据,(单词,语库)
#真实的单词模拟,每个单词的词性,你训练大量的输入文本,
CONTEXT_SIZE = 2 #设置词左边和右边选择的个数(即上下文词汇个数) 共取4个词汇
2,语料与词汇表
#导入语料库
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()
#—————————-思考句号影响———————————-
# 清洗文本
# text = re.sub(r'[^\\w\\s]', '', raw_text.lower())
# raw_text = text.split()
#中文的语句,你可以选择分词,也可以选择分字
vocab = set(raw_text) #集合,语库,里面独一无二
#—————————-思考有序,无序哪个好———————————-
# vocab = sorted(set(raw_text))
vocab_size = len(vocab)
word_to_idx = {word:i for i, word in enumerate(vocab)}#for游场的复与法,第1次场.1得到的索引号:Word.第个单詞
idx_to_word = {i:word for i, word in enumerate(vocab)}
3,训练数据构造
#训练数据构造
data = []
for i in range(CONTEXT_SIZE, len(raw_text) – CONTEXT_SIZE):# (2,47)
context = (
[raw_text[i–(2–j)]for j in range(2)] #raw_text[0],raw_text[1]开始,raw_text[45],raw_text[46]结束
+[raw_text[i+j+1] for j in range(CONTEXT_SIZE)]#raw_text[3],raw_text[4]开始,raw_text[48],raw_text[49]结束
)
target = raw_text[i]
data.append((context, target))
# # 动态窗口
# CONTEXT_SIZE = 2
# data = []
# for i in range(CONTEXT_SIZE, len(raw_text) – CONTEXT_SIZE):
# context = raw_text[i-CONTEXT_SIZE:i] + raw_text[i+1:i+CONTEXT_SIZE+1]
# target = raw_text[i]
# data.append((context, target))
4,上下文向量化
#将文字数字化,并转化成tensor格式
def make_context_vector(context, word_to_ix):
idxs = [word_to_ix[w] for w in context]
return torch.tensor(idxs, dtype=torch.long)
print(make_context_vector(data[0][0],word_to_idx))#打印转换后的张量,看上下文词转成的单词编号
5,设备选择
#自动选择设备( CUDA / MPS / CPU)
device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
6,CBOW 模型
#CBOW模型
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.proj = nn.Linear(embedding_dim, 128)
self.output = nn.Linear(128, vocab_size)
def forward(self, inputs):
embeds =sum(self.embeddings(inputs)).view(1, –1)/vocab_size
out = F.relu(self.proj(embeds))
out = self.output(out)
nll_prob = F.log_softmax(out, dim=1)#先 softmax 变成概率,再取 log。结果每个元素都是 ≤ 0 的数,越接近 0 表示概率越大。
return nll_prob
#log_softmax + NLLLoss 在数学上等价于 softmax + CrossEntropyLoss,但数值上更稳定,是 PyTorch 的经典搭配。
7,训练过程
for epoch in tqdm(range(100)):
total_loss = 0
for context, target in data:
context_vector = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device)
train_predict = model(context_vector)
loss = loss_function(train_predict, target)
optimizer.zero_grad() #PyTorch 默认会累加梯度。如果不清零,上一步的梯度会和这一步叠加,导致更新错误。所以每次 backward() 前必须清零。
loss.backward() #自动微分,算出每个参数对 loss 的偏导,存进 param.grad。
optimizer.step() #用 Adam 规则,根据梯度更新所有参数
total_loss += loss.item()
losses.append(total_loss)
print(losses)
8,测试与保存
#测试
context = ['processes','manipulate','abstract', 'things']
context_vector = make_context_vector(context, word_to_idx).to(device)
#预测的值
model.eval()
predict = model(context_vector)
# print(predict)
max_idx = predict.argmax(1)
# print(max_idx)
# print('word_to_idx',word_to_idx)
# print("CBOW embedding'weight =",model.embeddings.weight)
w = model.embeddings.weight.cpu().data.numpy()
np.savez('word.npz', file1=w)
a = np.load('word.npz')
print(a[a.files[0]])
9,完整代码
"""任务:根据中心词左右各 2 个上下文词,预测中心词。"""
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
from tqdm import tqdm
import re
#任务:已经有了语料库,1、构造训练集数据,(单词,语库)
#真实的单词模拟,每个单词的词性,你训练大量的输入文本,
CONTEXT_SIZE = 2 #设置词左边和右边选择的个数(即上下文词汇个数) 共取4个词汇
#导入语料库
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()
#—————————-思考句号影响———————————-
# 清洗文本
# text = re.sub(r'[^\\w\\s]', '', raw_text.lower())
# raw_text = text.split()
#中文的语句,你可以选择分词,也可以选择分字
vocab = set(raw_text) #集合,语库,里面独一无二
#—————————-思考有序,无序哪个好———————————-
# vocab = sorted(set(raw_text))
vocab_size = len(vocab)
word_to_idx = {word:i for i, word in enumerate(vocab)}#for游场的复与法,第1次场.1得到的索引号:Word.第个单詞
idx_to_word = {i:word for i, word in enumerate(vocab)}
#训练数据构造
data = []
for i in range(CONTEXT_SIZE, len(raw_text) – CONTEXT_SIZE):# (2,47)
context = (
[raw_text[i–(2–j)]for j in range(2)] #raw_text[0],raw_text[1]开始,raw_text[45],raw_text[46]结束
+[raw_text[i+j+1] for j in range(CONTEXT_SIZE)]#raw_text[3],raw_text[4]开始,raw_text[48],raw_text[49]结束
)
target = raw_text[i]
data.append((context, target))
# # 动态窗口
# CONTEXT_SIZE = 2
# data = []
# for i in range(CONTEXT_SIZE, len(raw_text) – CONTEXT_SIZE):
# context = raw_text[i-CONTEXT_SIZE:i] + raw_text[i+1:i+CONTEXT_SIZE+1]
# target = raw_text[i]
# data.append((context, target))
#将文字数字化,并转化成tensor格式
def make_context_vector(context, word_to_ix):
idxs = [word_to_ix[w] for w in context]
return torch.tensor(idxs, dtype=torch.long)
print(make_context_vector(data[0][0],word_to_idx))
#自动选择设备( CUDA / MPS / CPU)
device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
#CBOW模型
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOW, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.proj = nn.Linear(embedding_dim, 128)
self.output = nn.Linear(128, vocab_size)
def forward(self, inputs):
embeds =sum(self.embeddings(inputs)).view(1, –1)/vocab_size
out = F.relu(self.proj(embeds))
out = self.output(out)
nll_prob = F.log_softmax(out, dim=1)#先 softmax 变成概率,再取 log。结果每个元素都是 ≤ 0 的数,越接近 0 表示概率越大。
return nll_prob
#log_softmax + NLLLoss 在数学上等价于 softmax + CrossEntropyLoss,但数值上更稳定,是 PyTorch 的经典搭配。
#设置模型
model = CBOW(vocab_size, 10).to(device)
#创建一个优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
losses=[]
# 损失函数
loss_function = nn.NLLLoss()
#告诉模型,我要开始训练,模型中w进行随机化操作,已经更新w。在训练过程中,w会被修改的
# #pytorch提供2种方式来切换训练和测试的模式,分别是:model.train()和model.eval()。
# 一般用法是:在训练开始之前写上model.trian(),在测试时写上 model.eval()
model.train()
# 外层 epoch:整个数据集过 100 遍。
# 内层:逐个样本训练(batch size = 1)。
for epoch in tqdm(range(100)):
total_loss = 0
for context, target in data:
context_vector = make_context_vector(context, word_to_idx).to(device)
target = torch.tensor([word_to_idx[target]]).to(device)
train_predict = model(context_vector)
loss = loss_function(train_predict, target)
optimizer.zero_grad() #PyTorch 默认会累加梯度。如果不清零,上一步的梯度会和这一步叠加,导致更新错误。所以每次 backward() 前必须清零。
loss.backward() #自动微分,算出每个参数对 loss 的偏导,存进 param.grad。
optimizer.step() #用 Adam 规则,根据梯度更新所有参数
total_loss += loss.item()
losses.append(total_loss)
print(losses)
#测试
context = ['processes','manipulate','abstract', 'things']
context_vector = make_context_vector(context, word_to_idx).to(device)
#预测的值
model.eval()
predict = model(context_vector)
# print(predict)
max_idx = predict.argmax(1)
# print(max_idx)
# print('word_to_idx',word_to_idx)
# print("CBOW embedding'weight =",model.embeddings.weight)
w = model.embeddings.weight.cpu().data.numpy()
np.savez('word.npz', file1=w)
a = np.load('word.npz')
print(a[a.files[0]])
八、总结
把上下文词的向量求平均,去猜中心词;猜得越准,词向量就被调整得越合理;
最终,语义相似的词因为常出现在相似的上下文里,被“拉扯”到了向量空间中相近的位置。 预测是手段,低维词向量才是目的。


