← 返回首页
Word2Vec
发表时间:2025-04-09 15:44:07
Word2Vec-词向量

Word2Vec :是词嵌入的一种具体实现方法。它是由谷歌公司在 2013 年提出的一种用于产生词向量的两层神经网络模型。

1.Word2Vec

Word2Vec是Google于2013年开源推出的一个用于获取词向量(word vector)的工具包。它是语言模型中的一种,从大量文本预料中以无监督方式学习语义知识的模型,被广泛地应用于自然语言处理中。

Word2Vec与WordEmbedding之间的关系

Word Embedding是一个更广泛的概念,涵盖了多种不同的词向量表示方法。除了 Word2Vec 外,还包括 GloVe(Global Vectors for Word Representation)、FastText 等。Word2Vec 是 Word Embedding 的一种实现技术。它在词嵌入方法的发展过程中具有重要的地位,为后续的词嵌入研究提供了思路和方法。

Word2Vec的核心就是建立一个简单的神经网络实现词嵌入。其模型仅仅包括输入层、隐藏层和输出层,模型框架根据输入输出的不同,主要包括CBOW和Skip-gram模型。

2.CBOW(Continuous Bag of Words)

CBOW 的目标是:给定一个单词的上下文(比如周围的几个单词),预测这个单词本身(也称为中心词)。比如,给定句子 "I love reading books",如果目标单词是 "reading",上下文可能是 "love" 和 "books"。CBOW 会根据上下文单词的向量,预测目标单词的向量。

CBOW 的计算过程可以分为以下几个步骤: (1) 输入层:将上下文单词转换为向量 - 假设我们有一个词汇表(vocabulary),比如包含 10000 个单词。 - 每个单词会被表示为一个独热编码(one-hot vector),比如 "I" 是一个 10000 维的向量,其中对应 "I" 的位置是 1,其他位置是 0。 - 然后,每个独热编码会被映射到一个低维向量(词向量)。比如,我们将每个单词映射到一个 100 维的向量。

(2) 隐藏层:上下文单词的向量求和

(3) 输出层:预测目标单词 - 隐藏层的向量会被用来预测目标单词 "reading" 的概率分布。 - 这个过程是通过一个矩阵运算和 Softmax 函数完成的。Softmax 会把隐藏层的向量转换为一个概率分布,表示每个单词成为目标单词的可能性。 - 比如,Softmax 输出可能是一个 10000 维的向量,其中 "reading" 的位置的概率很高,而其他单词的概率较低。

(4) 损失函数:调整模型参数 - 我们希望模型预测的目标单词的概率尽可能高,因此会用交叉熵损失函数(cross-entropy loss)来衡量预测结果和真实结果的差距。 - 通过反向传播(backpropagation)和梯度下降(gradient descent),模型会不断调整词向量和权重矩阵,使得预测结果更接近真实结果。

CBOW 的核心思想是:通过上下文单词的向量,预测中心词的向量。 它的计算过程可以看作是一个“拼图”过程: - 输入:上下文单词的向量(拼图的碎片)。 - 输出:目标单词的向量(拼图的完整图案)。 - 模型通过不断调整拼图碎片的位置(词向量),最终拼出完整的图案(目标单词)。

下面是一个使用 PyTorch 实现 CBOW 模型的完整代码示例。这个代码包括数据准备、模型定义、训练过程和预测部分。

import torch
import torch.nn as nn
import torch.optim as optim
from collections import defaultdict
import random

# 1.准备训练数据
# 示例句子
sentences = [
    "I love reading books",
    "Books are my favorite",
    "I enjoy learning",
    "Learning is fun",
    "Fun makes life better",
]

# 2.构建词汇表
vocab = defaultdict(lambda: len(vocab))
for sentence in sentences:
    for word in sentence.split():
        vocab[word]

# 3.将词汇表转换为字典
vocab = dict(vocab)
word_to_idx = {word: idx for idx, word in enumerate(vocab)}
idx_to_word = {idx: word for word, idx in word_to_idx.items()}
vocab_size = len(vocab)

print(f"词汇表大小: {vocab_size}")
print(f"词汇表: {word_to_idx}")


# 4.将句子转换为上下文-目标单词对。
def prepare_data(sentences, window_size=1):
    data = []
    for sentence in sentences:
        words = sentence.split()
        for i in range(len(words)):
            context_words = []
            target_word = words[i]
            for j in range(max(0, i - window_size), min(len(words), i + window_size + 1)):
                if j != i:
                    context_words.append(words[j])
            context_indices = [word_to_idx[word] for word in context_words]
            target_index = word_to_idx[target_word]
            data.append((context_indices, target_index))
    return data


window_size = 1
data = prepare_data(sentences, window_size)
print(f"训练数据示例: {data[:3]}")


# 5.定义 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.linear = nn.Linear(embedding_dim, vocab_size)

    def forward(self, context_words):
        # context_words: [batch_size, window_size * 2]
        embeds = self.embeddings(context_words)  # [batch_size, window_size * 2, embedding_dim]
        embeds_avg = torch.mean(embeds, dim=1)  # [batch_size, embedding_dim]
        out = self.linear(embeds_avg)  # [batch_size, vocab_size]
        log_probs = torch.log_softmax(out, dim=1)
        return log_probs


# 6.训练模型
# 超参数
embedding_dim = 10
num_epochs = 1000
learning_rate = 0.001

# 初始化模型、损失函数和优化器
model = CBOW(vocab_size, embedding_dim)
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 训练循环
for epoch in range(num_epochs):
    total_loss = 0
    for context_indices, target_index in data:
        # 准备输入和目标
        context = torch.LongTensor(context_indices).unsqueeze(0)  # 添加一个批次维度
        target = torch.LongTensor([target_index])

        # 前向传播
        log_probs = model(context)
        loss = criterion(log_probs, target)

        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {total_loss / len(data):.4f}")


# 7.使用模型进行预测
def predict(context_words):
    context_indices = [word_to_idx[word] for word in context_words]
    context = torch.LongTensor(context_indices).unsqueeze(0)  # 添加一个批次维度
    with torch.no_grad():
        log_probs = model(context)
        probs = torch.exp(log_probs)
        predicted_idx = torch.argmax(probs).item()
    return idx_to_word[predicted_idx]


# 8.测试预测
context_words = ["love", "books"]
predicted_word = predict(context_words)
print(f"上下文: {context_words}, 预测的目标单词: {predicted_word}")

运行结果:

词汇表大小: 17
词汇表: {'I': 0, 'love': 1, 'reading': 2, 'books': 3, 'Books': 4, 'are': 5, 'my': 6, 'favorite': 7, 'enjoy': 8, 'learning': 9, 'Learning': 10, 'is': 11, 'fun': 12, 'Fun': 13, 'makes': 14, 'life': 15, 'better': 16}
训练数据示例: [([1], 0), ([0, 2], 1), ([1, 3], 2)]
Epoch [10/1000], Loss: 2.6303
...
Epoch [1000/1000], Loss: 0.1552
上下文: ['love', 'reading'], 预测的目标单词: reading

这个代码实现了一个简单的 CBOW 模型,可以根据上下文单词预测目标单词。你可以根据需要调整超参数(如嵌入维度、窗口大小等)来优化模型性能。

3.Skip-gram

跳字模型(Skip-gram)是 Word2Vec 中的另一种训练词嵌入(词向量)的模型,与 CBOW(连续词袋模型) 相对。与CBOW模型不同,Skip-gram模型的核心思想是:给定一个目标词,模型通过该目标词来预测它在上下文窗口中的词。换句话说,Skip-gram模型是通过一个目标词来预测其周围的上下文词汇。

举个例子,假设有一个简单的句子:

我 喜欢 学习 人工智能

假设选择窗口大小为2,那么在Skip-gram模型中,如果目标词是“学习”,那么模型的任务是预测“学习”的上下文词:“我”、“喜欢”、“人工”、“智能”。

Skip-gram模型的具体流程: 1. 构建训练样本:从语料库中选择一个目标词,并使用该目标词来预测其上下文词(即目标词周围的词汇)。上下文的大小由窗口参数(通常是固定大小的窗口)决定。 2. 词向量初始化:每个词都会被映射为一个稠密的向量,这些向量会在训练中被更新。 3. 预测上下文词:给定目标词的向量表示,Skip-gram模型通过神经网络预测目标词的上下文词。通常,预测过程会通过 softmax 函数来输出每个词的概率分布,从而得到上下文词。 4. 损失计算与优化:通过计算预测上下文词和实际上下文词之间的误差(通常使用交叉熵损失),并使用反向传播算法来优化词向量,最小化损失函数。

Skip-gram模型的优缺点:

优点 - 处理低频词:Skip-gram模型特别适用于处理低频词。由于Skip-gram是通过目标词来预测多个上下文词,目标词的向量能够更好地表示低频词。 - 能够捕捉长距离依赖:与CBOW相比,Skip-gram更能有效地捕捉长距离的语法和语义关系,因为它在训练过程中依赖于多个上下文词,而不仅仅是一个简单的平均。 - 广泛应用:Skip-gram模型被广泛应用于词向量学习、信息检索、推荐系统等领域。

缺点 - 计算效率较低:由于每次训练都是基于单一的目标词来预测多个上下文词,因此训练时需要考虑每个目标词的所有上下文词,计算量比较大。 - 对大量负样本的依赖:为了提高计算效率,Skip-gram模型通常会使用负采样(Negative Sampling)来简化计算,负采样的引入虽然提高了效率,但同时也增加了模型的复杂性。 - 上下文词的数量影响模型效果:如果窗口大小过大,Skip-gram模型可能会捕捉到不相关的上下文信息;如果窗口大小过小,则可能无法有效捕捉长距离的依赖关系。

下面是一个使用 PyTorch 实现 Skip-Gram 模型的完整代码示例。这个代码包括数据准备、模型定义、训练过程和预测部分。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np

# 示例句子
sentence = "我 喜欢 学习 人工智能"
words = sentence.split()

# 构建词汇表
vocab = list(set(words))
word_to_idx = {word: idx for idx, word in enumerate(vocab)}
idx_to_word = {idx: word for idx, word in enumerate(vocab)}
vocab_size = len(vocab)

# 生成训练数据
context_window = 1  # 上下文窗口大小
train_data = []

for i in range(context_window, len(words) - context_window):
    center_word = word_to_idx[words[i]]
    context_words = []
    for j in range(i - context_window, i + context_window + 1):
        if j != i:
            context_words.append(word_to_idx[words[j]])
    for context_word in context_words:
        train_data.append((center_word, context_word))


# 定义Skip-gram模型
class SkipGram(nn.Module):
    def __init__(self, vocab_size, embed_dim):
        super(SkipGram, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embed_dim)
        self.output_layer = nn.Linear(embed_dim, vocab_size)

    def forward(self, center_words):
        embeds = self.embeddings(center_words)
        output = self.output_layer(embeds)
        log_probs = nn.functional.log_softmax(output, dim=1)
        return log_probs


# 超参数
embed_dim = 10  # 词向量维度
num_epochs = 2000
learning_rate = 0.001


# 创建数据集和数据加载器
class WordDataset(Dataset):
    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        center, context = self.data[idx]
        return torch.LongTensor([center]), torch.LongTensor([context])


dataset = WordDataset(train_data)
dataloader = DataLoader(dataset, batch_size=8, shuffle=True)

# 初始化模型、损失函数和优化器
model = SkipGram(vocab_size, embed_dim)
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 训练模型
for epoch in range(num_epochs):
    total_loss = 0
    for center_words, context_words in dataloader:
        # 前向传播
        outputs = model(center_words.squeeze())
        loss = criterion(outputs, context_words.squeeze())

        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {total_loss / len(dataloader):.4f}')


# 预测函数(仅针对中心词“学习”,并排除中心词本身)
def predict(center_word):
    model.eval()
    with torch.no_grad():
        center_idx = torch.LongTensor([word_to_idx[center_word]])
        output = model(center_idx)
        probabilities = torch.exp(output)  # 转换为概率

        # 排除中心词本身
        mask = torch.arange(vocab_size) != word_to_idx[center_word]
        masked_probabilities = probabilities * mask.float()

        # 获取前3个最可能的词
        top_probs, top_indices = torch.topk(masked_probabilities, 3)
        predicted_words = [idx_to_word[idx.item()] for idx in top_indices[0]]
        return predicted_words, top_probs[0].tolist()


# 验证结果(仅针对中心词“学习”)
print("\n模型预测结果:")
center_word = "学习"
predicted_words, probabilities = predict(center_word)
print(f"中心词: '{center_word}', 预测上下文词: {predicted_words} (概率: {probabilities})")

运行效果:

Epoch [10/2000], Loss: 1.6132
...
Epoch [2000/2000], Loss: 0.6944

模型预测结果:
中心词: '学习', 预测上下文词: ['人工智能', '喜欢', '我'] (概率: [0.49972331523895264, 0.4988761842250824, 0.00038383150240406394])