词向量空间详解:从One-Hot到Word2Vec、GloVe及现代嵌入技术原理与PyTorch实现

目录

为什么需要词向量?

在自然语言处理(NLP)中,我们面临一个最基础的问题:计算机只懂数字,不懂文字。怎么把“我喜欢机器学习”这样的句子变成计算机能处理的东西?

这就是词向量技术要解决的核心问题。它把离散的词汇转换成连续的向量表示,让计算机不仅能“看到”词,还能“理解”词的含义。

词向量的重要性

词向量是NLP的基础设施,几乎所有现代NLP模型都建立在良好的词表示之上。想象一下,我们给每个词分配一串数字,这些数字不再是随机的,而是携带了语义信息:

  • 语义理解:相似的词在向量空间里会“住”得很近。例如“猫”和“狗”的距离应该比“猫”和“汽车”近得多。
  • 降维:从超高维的稀疏表示(几万甚至几十万维)变成低维的稠密表示(通常100~300维),计算效率大幅提升。
  • 泛化能力:模型能举一反三,即使遇到类似的表达或新词,也能依靠向量空间的结构进行合理推测。

可以说,没有好的词向量,后面的情感分析、机器翻译、问答系统都会步履维艰。接下来,我们从最原始的One-Hot编码开始,看看词向量是如何一步步演化的。


One-Hot编码问题

One-Hot编码原理

One-Hot编码是最朴素的词表示方法,思路很简单:给每个词分配一个唯一的序号,然后把这个序号转换成一个只有0和1的向量,向量的长度就是词汇表的大小。

import numpy as np

def one_hot_encode(word, vocab):
    """
    One-Hot编码实现
    """
    vocab_size = len(vocab)
    word_index = vocab.index(word) if word in vocab else -1
    
    if word_index == -1:
        raise ValueError(f"词 '{word}' 不在词汇表中")
    
    # 创建One-Hot向量
    one_hot = np.zeros(vocab_size)
    one_hot[word_index] = 1
    return one_hot

# 示例
vocab = ["我", "爱", "机器", "学习", "深度", "人工智能"]
machine_vec = one_hot_encode("机器", vocab)
print(f"'机器'的One-Hot向量: {machine_vec}")
# [0. 0. 1. 0. 0. 0.]

# 计算两个词的相似度(总是0,无法表示语义关系)
learning_vec = one_hot_encode("学习", vocab)
similarity = np.dot(machine_vec, learning_vec)  # 0
print(f"'机器'和'学习'的相似度: {similarity}")

你可以看到,向量中只有一个位置是1,其余全是0。这种表示在小型词汇表上还能凑合用,但它有两个让人头疼的问题。

One-Hot编码的主要问题

下面这段代码集中展示了One-Hot编码的通病:

# One-Hot编码的问题演示
def analyze_onehot_issues():
    """
    分析One-Hot编码的主要问题
    """
    vocab = ["国王", "王后", "男人", "女人", "巴黎", "法国", "柏林", "德国"]
    
    # 任意两个词的相似度都是0
    king_vec = np.array([1 if i == 0 else 0 for i in range(len(vocab))])  # 国王
    queen_vec = np.array([1 if i == 1 else 0 for i in range(len(vocab))])  # 王后
    
    similarity = np.dot(king_vec, queen_vec)
    print(f"国王和王后的相似度: {similarity}")  # 0,但实际上它们有很强的语义联系
    
    print(f"向量维度: {len(vocab)}")
    print(f"稀疏度: {1 - 1/len(vocab):.4f}")  # 接近100%稀疏

analyze_onehot_issues()

总结一下One-Hot编码的致命伤:

  1. 维度灾难:词汇表有多大,向量就有多少维。一个10万词的词汇表就需要10万维的向量,这会让计算和存储变得异常昂贵。
  2. 语义缺失:所有词向量两两正交,相似度永远为0。因此你永远无法表达“国王-男人+女人=王后”这样的类比关系。
  3. 稀疏性:向量里几乎全是0,绝大多数存储空间被浪费。

显然,要让计算机真正理解语言,我们必须从稀疏的、无语义的One-Hot向量,跨越到稠密的、能表达语义的分布式表示。


Word2Vec原理详解

Word2Vec是Google在2013年提出的革命性方法,它用神经网络从大量文本中学习词的分布式表示,彻底解决了One-Hot的痛点。Word2Vec的核心思想可以用一句话概括:一个词的含义由它周围的词决定

Word2Vec主要有两种架构:Skip-Gram和CBOW。接下来我们分别用代码和通俗的解释来拆解它们。

Skip-Gram模型

Skip-Gram的思路是用中心词去预测它的上下文。通俗地说,给定“机器学习”这个词,模型要学会推测它周围可能出现“人工智能”、“深度”、“算法”等词。这种方法特别擅长处理数据量较小的情况,对罕见词也更友好。

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import random

class SkipGramModel(nn.Module):
    """
    Skip-Gram模型实现
    """
    def __init__(self, vocab_size, embed_dim=100):
        super(SkipGramModel, self).__init__()
        # 中心词嵌入
        self.center_embed = nn.Embedding(vocab_size, embed_dim)
        # 上下文词嵌入(负采样用)
        self.context_embed = nn.Embedding(vocab_size, embed_dim)
        
        # 初始化权重
        initrange = 0.5 / embed_dim
        self.center_embed.weight.data.uniform_(-initrange, initrange)
        self.context_embed.weight.data.uniform_(-initrange, initrange)
    
    def forward(self, center_words, context_words, neg_words):
        """
        前向传播:计算正样本得分和负样本得分。
        """
        # 中心词嵌入
        center_embeds = self.center_embed(center_words)  # (batch_size, embed_dim)
        
        # 正样本(真实上下文)得分
        context_embeds = self.context_embed(context_words)  # (batch_size, embed_dim)
        pos_scores = torch.sum(center_embeds * context_embeds, dim=1)  # (batch_size,)
        
        # 负样本得分
        neg_embeds = self.context_embed(neg_words)  # (batch_size, k, embed_dim)
        neg_scores = torch.bmm(neg_embeds, center_embeds.unsqueeze(2)).squeeze(2)  # (batch_size, k)
        
        # 使用log-sigmoid作为损失函数
        pos_loss = F.logsigmoid(pos_scores)
        neg_loss = torch.sum(F.logsigmoid(-neg_scores), dim=1)
        
        return -(pos_loss + neg_loss).mean()

def generate_training_data(sentences, window_size=2):
    """
    生成训练数据:从句子中提取(中心词,上下文词)对。
    """
    pairs = []
    for sentence in sentences:
        for i, center_word in enumerate(sentence):
            # 定义上下文窗口
            start = max(0, i - window_size)
            end = min(len(sentence), i + window_size + 1)
            
            # 收集上下文词
            for j in range(start, end):
                if i != j:  # 不包括自己
                    pairs.append((center_word, sentence[j]))
    return pairs

# 示例数据
sentences = [
    ["我", "爱", "机器", "学习"],
    ["深度", "学习", "是", "人工智能", "的重要", "分支"],
    ["自然", "语言", "处理", "是", "有趣的", "领域"]
]

# 构建词汇表
word_to_idx = {}
idx_to_word = {}
all_words = []
for sentence in sentences:
    all_words.extend(sentence)

vocab = list(set(all_words))
for i, word in enumerate(vocab):
    word_to_idx[word] = i
    idx_to_word[i] = word

print(f"词汇表大小: {len(vocab)}")

CBOW模型

CBOW(Continuous Bag of Words)的思路和Skip-Gram正好反一反:用上下文词来预测中心词。还拿“机器学习”举例,CBOW会根据周围的“人工智能”、“深度”、“算法”来猜中间应该出现“学习”。由于一次更新需要平均多个上下文词的信息,CBOW的训练速度通常比Skip-Gram更快,非常适合大规模语料。

class CBOWModel(nn.Module):
    """
    CBOW模型实现
    """
    def __init__(self, vocab_size, embed_dim=100):
        super(CBOWModel, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embed_dim)
        self.linear = nn.Linear(embed_dim, vocab_size)
        
        # 初始化权重
        initrange = 0.5 / embed_dim
        self.embeddings.weight.data.uniform_(-initrange, initrange)
        self.linear.weight.data.uniform_(-initrange, initrange)
        self.linear.bias.data.zero_()
    
    def forward(self, context_words):
        """
        context_words: (batch_size, context_size)
        """
        embeds = self.embeddings(context_words)  # (batch_size, context_size, embed_dim)
        # 平均上下文词向量
        avg_embeds = torch.mean(embeds, dim=1)  # (batch_size, embed_dim)
        scores = self.linear(avg_embeds)  # (batch_size, vocab_size)
        return F.log_softmax(scores, dim=1)

# 模拟预训练词向量的使用
def demonstrate_word2vec_usage():
    """
    演示如何使用预训练Word2Vec
    """
    # 模拟词向量
    class MockWord2Vec:
        def most_similar(self, positive=None, negative=None, topn=5):
            if positive and "机器学习" in positive:
                return [("深度学习", 0.85), ("人工智能", 0.82), ("自然语言处理", 0.78)]
            else:
                return [("王后", 0.92)]
    
    mock_model = MockWord2Vec()
    
    # 查找相似词
    similar = mock_model.most_similar(positive=["机器学习"], topn=3)
    print(f"与'机器学习'相似的词: {similar}")
    
    # 词语类比
    analogy = mock_model.most_similar(positive=["国王", "女人"], negative=["男人"], topn=1)
    print(f"国王 - 男人 + 女人 = {analogy[0][0]}")

demonstrate_word2vec_usage()

Word2Vec训练流程

整个Word2Vec的训练可以归纳成下面几个标准步骤:

  1. 准备大量的文本语料(维基百科、新闻、小说等都可以)。
  2. 构建词汇表,去掉出现次数过少的低频词,控制词汇表大小。
  3. 用滑动窗口生成(中心词,上下文词)训练样本。
  4. 随机初始化词向量。
  5. 通过梯度下降,不断优化目标函数,让模型对真实上下文给出更高的概率。
  6. 保存训练好的词向量,供下游任务使用。

就像上面代码演示的那样,训练完成后,我们就可以轻松计算出“国王”+“女人”-“男人”约等于“王后”这样惊艳的语义运算。


GloVe与FastText

Word2Vec虽然好用,但它主要依赖局部上下文窗口,有时会忽略全局统计信息。后来的研究者们又提出了GloVe和FastText,分别从不同角度进行了改进。

GloVe原理

GloVe(Global Vectors for Word Representation)巧妙地结合了全局词共现统计局部上下文信息。它的核心思路是:

  1. 先扫描整个语料库,统计出词对共现的频次,构建一个巨大的共现矩阵。
  2. 再利用这个矩阵,通过分解或回归的方式训练词向量,保证向量之间的运算能直接对应词与词之间的共现关系。

因为充分考虑了全局统计,GloVe通常在训练速度和稳定性上更胜一筹,在小规模语料上也能产生不错的结果。

def simulate_glove_training():
    """
    模拟GloVe训练过程
    """
    # 构建共现矩阵(简化版)
    vocab = ["我", "爱", "机器", "学习"]
    
    # 示例共现矩阵(实际会很大)
    cooccurrence = np.array([
        [0, 1, 1, 0],  # 我
        [1, 0, 0, 1],  # 爱
        [1, 0, 0, 1],  # 机器
        [0, 1, 1, 0]   # 学习
    ])
    
    print("共现矩阵示例:")
    print(cooccurrence)

simulate_glove_training()

FastText扩展

FastText由Facebook提出,最大的亮点是把词拆解为字符n-gram。例如,单词“where”可以拆分成<whwhehererere>这样的子串(加上边界符)。

这种设计带来了三个显著的好处:

  1. 处理OOV(未登录词):即使模型训练时没见过某个词,也可以通过它的字符n-gram组合出一个大致合理的向量。
  2. 对形态丰富的语言更为有效:像法语、德语、土耳其语等,词汇变形多,FastText能捕捉到词根、词缀的信息。
  3. 词的向量是所有其字符n-gram向量的平均:这实际上是在用“拼写”信息来辅助语义表示。
def explain_fasttext():
    """
    解释FastText的特点
    """
    print("FastText特点:")
    print("1. 将词分解为字符n-gram")
    print("2. 可以处理未登录词(OOV)")
    print("3. 对形态丰富的语言特别有效")
    print("4. 词向量是其字符n-gram向量的平均")
    
    # 示例:单词"where"的字符n-gram
    word = "where"
    n = 3  # trigram
    ngrams = []
    padded_word = "<" + word + ">"  # 添加边界符号
    for i in range(len(padded_word) - n + 1):
        ngram = padded_word[i:i+n]
        ngrams.append(ngram)
    
    print(f"\n'{word}'的{3}-gram: {ngrams}")
    # ['<wh', 'whe', 'her', 'ere', 're>']

explain_fasttext()

如果你需要在工业界落地,并且文本中存在很多拼写错误或罕见词,FastText往往是一个值得优先考虑的选项。


现代词嵌入技术

Word2Vec、GloVe、FastText都是静态词向量:无论一个词出现在什么句子里,它的向量始终不变。但现实中的语言充满多义性——比如“苹果”在“我吃了一个苹果”和“我买了一台苹果电脑”里,指代完全不同的事物。静态词向量无法区分这两种用法。

于是,上下文相关的动态词向量应运而生。BERT、ELMo、GPT等预训练模型,会根据词的上下文动态生成向量,让同一个词在不同句子中有不同的表示。

现代词嵌入对比

def modern_embeddings_comparison():
    """
    现代词嵌入技术对比
    """
    print("词嵌入技术演进:")
    print("1. One-Hot: 稀疏、高维、无语义")
    print("2. Word2Vec/GloVe: 稠密、低维、静态语义")
    print("3. FastText: 可处理未登录词")
    print("4. ELMo/BERT: 上下文相关、动态词向量")
    print("5. GPT系列: 生成式预训练")
    
    print("\n现代最佳实践:")
    print("- 简单任务: 使用预训练词向量(Word2Vec, GloVe)")
    print("- 复杂任务: 使用Transformer模型的隐藏状态作为词嵌入")
    print("- 资源受限: 使用轻量级模型(DistilBERT, TinyBERT)")

modern_embeddings_comparison()

使用HuggingFace的现代词嵌入

现在做NLP项目,最便捷的方式就是通过HuggingFace的transformers库调用预训练模型。几行代码,你就能拿到BERT等模型输出的高质量词嵌入。

def demonstrate_modern_embeddings():
    """
    演示现代词嵌入的上下文相关性
    """
    print("现代嵌入特点: 上下文相关,同一词在不同语境下有不同表示")
    print("想象一下:")
    print("- '苹果'在'我吃了一个苹果'里的向量")
    print("- '苹果'在'我买了一台苹果电脑'里的向量")
    print("这两个向量在BERT里完全不一样!")

demonstrate_modern_embeddings()

动态词向量让NLP模型真正开始理解“一词多义”,极大地提升了各类任务的天花板。


实际应用与案例

讲了这么多原理,我们来看看词向量在实际项目中怎么用。本节用两个经典场景来示范:文本分类相似度计算

文本分类中的词向量应用

文本分类是NLP的入门级任务,常见的情感分析、垃圾邮件检测、新闻归类都需要它。用词向量构建的文本表示,往往效果远超传统的词袋模型。

def text_classification_with_embeddings():
    """
    使用词向量进行文本分类的示例
    """
    import numpy as np
    from sklearn.linear_model import LogisticRegression
    
    # 模拟词向量
    vocab = {"很好", "优秀", "糟糕", "差劲", "喜欢", "讨厌", "推荐", "不推荐"}
    word_vectors = {}
    for word in vocab:
        word_vectors[word] = np.random.rand(50)
    
    def sentence_to_vector(sentence, word_vectors, dim=50):
        """
        将句子转换为向量(简单平均词向量)
        """
        words = sentence.split()
        vectors = []
        for word in words:
            if word in word_vectors:
                vectors.append(word_vectors[word])
        
        if vectors:
            return np.mean(vectors, axis=0)
        else:
            return np.zeros(dim)
    
    # 示例数据
    texts = [
        "这个产品很好很优秀",
        "质量很不错推荐购买", 
        "很糟糕差劲不推荐",
        "质量太差劲了讨厌"
    ]
    labels = [1, 1, 0, 0]  # 1表示正面,0表示负面
    
    # 转换为向量
    X = np.array([sentence_to_vector(text, word_vectors) for text in texts])
    y = np.array(labels)
    
    print(f"特征矩阵形状: {X.shape}")
    
    # 训练分类器
    clf = LogisticRegression()
    clf.fit(X, y)
    
    # 预测新文本
    new_text = "产品质量优秀值得推荐"
    new_vec = sentence_to_vector(new_text, word_vectors)
    prediction = clf.predict([new_vec])
    
    print(f"\n新文本: {new_text}")
    print(f"预测类别: {'正面' if prediction[0] == 1 else '负面'}")

text_classification_with_embeddings()

这段代码展示了最简单的流程:先将每个句子里的词向量取平均,得到句子向量,然后送入逻辑回归分类器。即便是这样朴素的做法,在很多真实场景下也能拿到不错的基线分数。

词向量相似度计算

词向量的另一个高频应用是计算语义相似度,用于推荐系统、同义词查找、信息检索等。

def word_similarity_demo():
    """
    词向量相似度计算示例
    """
    from sklearn.metrics.pairwise import cosine_similarity
    
    # 模拟词向量
    words = ["机器", "学习", "深度", "人工智能", "计算机", "科学"]
    vectors = np.random.rand(len(words), 100)  # 随机向量(实际会用预训练好的)
    
    # 计算相似度矩阵
    sim_matrix = cosine_similarity(vectors)
    
    print("词向量相似度矩阵:")
    print(f"{'':<8}", end="")
    for word in words:
        print(f"{word:<8}", end="")
    print()
    
    for i, word in enumerate(words):
        print(f"{word:<8}", end="")
        for j in range(len(words)):
            print(f"{sim_matrix[i][j]:<8.3f}", end="")
        print()

word_similarity_demo()

在实际项目中,只需将随机向量替换成Word2Vec或GloVe的预训练向量,你就能快速搭建一个语义检索或相关推荐的功能。


相关教程

词向量是NLP的基础,建议先理解One-Hot到Word2Vec的演进逻辑,再学习现代预训练模型的嵌入方法。实际项目中优先使用Hugging Face的预训练模型,效率和效果都要比从头训练好很多。

总结

词向量技术是自然语言处理的核心基础,它成功地将离散的词汇转换为连续的、富含语义的向量表示:

  1. 演进历程:从One-Hot稀疏表示到Word2Vec/GloVe的稠密表示,再到BERT等模型的上下文相关表示。
  2. 核心技术:Word2Vec的Skip-Gram/CBOW模型、GloVe的全局统计、FastText的字符n-gram。
  3. 实际应用:文本分类、相似度计算、信息检索等各类NLP任务都可以直接受益于高质量的词向量。
  4. 现代实践:优先使用HuggingFace等框架加载预训练模型,根据任务类型和资源条件选择合适的词嵌入方案。

💡 核心要点:词向量的质量直接影响下游NLP任务的性能。现代NLP任务中,推荐使用预训练Transformer模型的隐藏状态作为词嵌入,以获得上下文感知的表示。


🔗 扩展阅读

📂 所属阶段:第一阶段 — 文本预处理(基石篇)
🔗 相关章节:文本特征工程TF-IDF与相似度 · 分词技术Tokenization