[深度学习网络从入门到入土] 词向量模型word2vec

📢个人导航

知乎:https://www.zhihu.com/people/byzh_rc

CSDN:https://blog.csdn.net/qq_54636039

注:本文仅对所述内容做了框架性引导,具体细节可查询其余相关资料or源码

参考文章:各方资料

📖参考资料

Efficient Estimation of Word Representations in Vector Space.

🌱背景

在自然语言处理中,最基础的问题之一就是如何让计算机理解文字

对于人类而言,“猫”和“狗”具有一定语义相似性,但对于计算机来说,文字本质上只是符号

one-hot编码的问题:

  • 维度灾难(词表如果有 10 万词,就需要 10 万维向量)
  • 无法表达语义关系(cat 与 dog 的距离, cat 与 apple 的距离)
  • 不同词之间彼此独立,没有共享信息

-> 能不能把单词映射到低维稠密向量空间,并且让“语义相近”的词距离更近 -> 词向量Word Embedding

word2vec 则是其中最经典、最具有里程碑意义的方法之一

⚙️架构(公式)

在这里插入图片描述

word2vec 本质上是一个“通过上下文预测单词”的浅层神经网络
训练完成后, 真正需要的不是预测模型, 而是其中学到的词向量矩阵

word2vec 主要有两种结构:

  1. 连续词袋模型: CBOW: Continuous Bag of Words
  2. 跳字模型: Skip-Gram
1. CBOW

用“上下文单词的集合”预测“当前单词”

不考虑上下文单词的顺序 -> “词袋”的意思

  • 输入层:上下文的k个单词(比如左右各2个,共4个),每个单词用one-hot编码表示,维度为==[V, 1]==
  • 隐藏层:将k个输入的one-hot向量分别与权重矩阵W(维度[V, N],N是词向量维度,通常取50-300)相乘,得到k个==[N, 1]==的向量,再对这k个向量取平均值,得到隐藏层输出h(维度[N, 1])
  • 输出层:隐藏层输出h与权重矩阵W’(维度[N, V])相乘,得到维度[V, 1]的向量,再通过softmax函数归一化,得到每个单词作为“当前单词”的概率,目标是让真实当前单词的概率最大

h = 1 k ∑ i = 1 k W T x i h = \frac{1}{k} \sum_{i=1}^{k} W^T x_i h=k1i=1kWTxi

2. Skip-Gram

和CBOW相反,用“当前单词”预测“上下文单词”
同样不考虑上下文顺序,但更关注单个单词与周边单词的关联

  • 输入层:当前单词的one-hot向量(维度==[V, 1]==)
  • 隐藏层:输入向量与权重矩阵W(维度[V, N])相乘,得到隐藏层输出h(维度==[N, 1]==)
  • 输出层:隐藏层输出h与权重矩阵W’(维度[N, V])相乘,得到维度[V, 1]的向量,通过softmax得到每个单词作为“上下文单词”的概率,目标是让真实上下文单词的概率之和最大

h = W T x h = W^T x h=WTx

👍优点/创新点

word2vec精准解决了one-hot编码的痛点:

  1. 解决维度灾难: 将one-hot的V维稀疏向量,转化为N维(50-300维)稠密向量
  2. 捕捉语义关联: “猫”和“狗”的向量余弦距离近,“猫”和“苹果”的距离远
  3. 结构简单,训练高效

👎缺点

  1. 忽略单词顺序和上下文依赖
    (比如“我吃苹果”和“苹果吃我”,上下文单词完全一样,但语义完全不同)
  2. 无法处理多义词
    (word2vec只能给每个单词分配一个固定的词向量,无法体现多义性)
  3. 依赖局部上下文,忽略全局信息
    (word2vec只关注“当前单词的周边上下文”,无法利用整个语料库的全局信息)
  4. 对稀有词、生僻词效果差
    (无法学习到有效的词向量,通常会用随机向量或平均值替代)
  5. 缺乏语法信息

💻代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F


class Word2Vec(nn.Module):
    """
    Word2Vec

    支持:
        1. CBOW
        2. Skip-Gram
    """

    def __init__(
        self,
        vocab_size,
        embedding_dim,
        model_type="skipgram",
    ):
        super().__init__()

        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.model_type = model_type.lower()

        # W: [V, N]
        # 输入词向量矩阵
        self.embedding = nn.Embedding( # nn.Embedding 等价于 `one-hot` & `embedding matrix`
            num_embeddings=vocab_size,
            embedding_dim=embedding_dim,
        )

        # W': [N, V]
        # 输出投影矩阵
        self.output = nn.Linear(
            embedding_dim,
            vocab_size,
            bias=False,
        )

    def forward(self, x):
        """
        Skip-Gram:
            x shape: [batch_size]

        CBOW:
            x shape: [batch_size, context_size]
        """

        # =====================================================
        # Skip-Gram
        # =====================================================
        if self.model_type == "skipgram":

            # [B] -> [B, N]
            h = self.embedding(x)

            # [B, N] -> [B, V]
            logits = self.output(h)

            return logits

        # =====================================================
        # CBOW
        # =====================================================
        elif self.model_type == "cbow":

            # [B, C] -> [B, C, N]
            emb = self.embedding(x)

            # 对上下文词向量取平均
            # [B, C, N] -> [B, N]
            h = emb.mean(dim=1)

            # [B, N] -> [B, V]
            logits = self.output(h)

            return logits

        else:
            raise ValueError("model_type must be 'skipgram' or 'cbow'")


if __name__ == "__main__":
    batch = 32
    vocab_size = 10000 # 词表大小
    embedding_dim = 128 # 词向量维度

    # =========================================================
    # Skip-Gram
    # =========================================================

    skipgram_model = Word2Vec(
        vocab_size=vocab_size,
        embedding_dim=embedding_dim,
        model_type="skipgram",
    )

    # 当前单词
    print("Skip-Gram shape:")

    inputs = torch.randint(0, vocab_size, (batch,))
    logits = skipgram_model(inputs)
    print("- logits:", logits.shape)  # 映射回词表的预测概率: [32, vocab_size]

    x = torch.randint(0, vocab_size, (batch,))
    print("- x_embedding:", skipgram_model.embedding(x).shape) # 真正要的word2vec的词向量

    # =========================================================
    # CBOW
    # =========================================================

    cbow_model = Word2Vec(
        vocab_size=vocab_size,
        embedding_dim=embedding_dim,
        model_type="cbow",
    )

    # 上下文单词
    print("CBOW shape:")

    inputs = torch.randint(0, vocab_size, (batch, 4)) # 4代表: 4个上下文单词
    logits = cbow_model(inputs)
    print("- logits:", logits.shape)  # 映射回词表的预测概率: [32, vocab_size]

    x = torch.randint(0, vocab_size, (batch,))
    print("- x_embedding:", cbow_model.embedding(x).shape) # 真正要的word2vec的词向量
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐