Skip to content

第三阶段:NLP 与序列建模 ​

3.1 文本的数字化表示 ​

One-Hot 编码 — 最简单也最没用 ​

python
vocab = ["我", "爱", "AI", "学习"]
# "AI" → [0, 0, 1, 0]
# "学习" → [0, 0, 0, 1]

问题:

  • 维度 = 词汇量(几万到几十万),极其稀疏
  • 所有词之间距离相等,"猫"和"狗"的距离 = "猫"和"火箭"的距离

词频 — TF-IDF ​

TF-IDF(t,d)=TF(t,d)×log⁡NDF(t)
  • TF:词 t 在文档 d 中出现的频率
  • IDF:逆文档频率,在所有文档中越常见的词权重越低

TF-IDF 解决了"常见词不重要"的问题,但依然无法捕获语义相似性。

词嵌入 (Word Embedding) — 2013 年的革命 ​

词嵌入的核心思想:一个词的含义由它周围的词决定(分布语义假说,Distributional Hypothesis)。

python
"""
Word2Vec 跳字模型 (Skip-Gram) 的完整实现
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset

class SkipGram(nn.Module):
    """
    Skip-Gram: 用中心词预测上下文词

    训练目标:最大化 log P(上下文词 | 中心词)

    直观理解:
    输入 "我 爱 深度 学习" → 中心词="深度"
    目标: 最大化 P("爱"|"深度") + P("学习"|"深度")
    """

    def __init__(self, vocab_size: int, embed_dim: int = 100):
        super().__init__()
        # 中心词嵌入: 将词索引映射到向量
        self.center_embed = nn.Embedding(vocab_size, embed_dim)
        # 上下文词嵌入: 注意!和中心词嵌入是不同的矩阵
        self.context_embed = nn.Embedding(vocab_size, embed_dim)

        # 初始化(小随机值)
        nn.init.uniform_(self.center_embed.weight, -0.05, 0.05)
        nn.init.uniform_(self.context_embed.weight, -0.05, 0.05)

    def forward(self, center: torch.Tensor, pos_context: torch.Tensor,
                neg_context: torch.Tensor):
        """
        Args:
            center: 中心词索引 (batch,)
            pos_context: 正样本(真实上下文词) (batch,)
            neg_context: 负样本(随机采样的假上下文词) (batch, num_neg)
        """
        # 1. 获取嵌入
        center_vec = self.center_embed(center)          # (batch, embed_dim)
        pos_vec = self.context_embed(pos_context)        # (batch, embed_dim)
        neg_vec = self.context_embed(neg_context)        # (batch, num_neg, embed_dim)

        # 2. 正样本得分(点积 → sigmoid 概率)
        pos_score = (center_vec * pos_vec).sum(dim=1)    # (batch,)
        pos_score = torch.sigmoid(pos_score)

        # 3. 负样本得分(点积 → sigmoid 概率)
        neg_score = torch.bmm(neg_vec, center_vec.unsqueeze(2)).squeeze(2)
        neg_score = torch.sigmoid(neg_score)             # (batch, num_neg)

        # 4. 损失 = -log(正样本概率) - Σlog(1-负样本概率)
        pos_loss = -torch.log(pos_score + 1e-8).mean()
        neg_loss = -torch.log(1 - neg_score + 1e-8).mean()
        return pos_loss + neg_loss


# === 训练后使用 ===
def find_similar_words(model, word_idx, embedding_matrix, vocab, top_k=5):
    """在训练好的词向量中找最相似的词"""
    word_vec = embedding_matrix[word_idx]
    # 余弦相似度
    similarities = F.cosine_similarity(
        word_vec.unsqueeze(0), embedding_matrix, dim=1
    )
    top_indices = similarities.argsort(descending=True)[1:top_k+1]
    return [(vocab[i], similarities[i].item()) for i in top_indices]

# 经典结果:
# vec("国王") - vec("男人") + vec("女人") ≈ vec("女王")

这一发现震惊了 NLP 界:词向量竟然可以"做算术"!语义关系被编码为向量空间中的方向。

GloVe — 基于全局统计的词向量 ​

GloVe (Global Vectors) 结合了 Word2Vec 的优点和全局共现矩阵的信息:

J=∑i,jf(Xij)(wiTw~j+bi+b~j−log⁡Xij)2

其中 Xij 是词 i 和词 j 在语料库中的共现次数。


3.2 循环神经网络 (RNN) ​

RNN 是第一个能真正处理序列的神经网络。它的核心思想很简单:在处理当前词时,参考之前所有词的信息。

mermaid
graph LR
    subgraph "RNN 展开 (Unrolled)"
        H0["h₀<br/>(初始状态)"] --> H1["h₁ = tanh(W_h·h₀ + W_x·x₁)"]
        X1["x₁<br/>'我'"] --> H1
        H1 --> H2["h₂ = tanh(W_h·h₁ + W_x·x₂)"]
        X2["x₂<br/>'爱'"] --> H2
        H2 --> H3["h₃ = tanh(W_h·h₂ + W_x·x₃)"]
        X3["x₃<br/>'AI'"] --> H3
    end

    H1 --> Y1["ŷ₁"]
    H2 --> Y2["ŷ₂"]
    H3 --> Y3["ŷ₃"]

数学定义:

ht=tanh⁡(Whht−1+Wxxt+b)

每一时刻的隐藏状态 ht 包含了"到目前为止"的所有信息。

RNN 的致命弱点 ​

长距离依赖:
"我出生在法国。我在那里度过了童年。我会说法语。"

RNN要记住第1句的"法国",到第3句才能理解"法语"。但实际上:
- 经过 10+ 时间步后,梯度已经衰减到近乎 0(梯度消失)
- 模型"忘记"了前面的信息

LSTM — 带"门控"的记忆网络 ​

LSTM 通过三个"门"来解决长距离依赖问题:

mermaid
graph TD
    subgraph "LSTM 单元内部结构"
        CT1["c_{t-1}<br/>长期记忆"] --> GATE1["遗忘门 f_t<br/>σ(W_f·[h_{t-1}, x_t] + b_f)<br/>决定丢弃哪些旧记忆"]
        GATE1 --> CT2["c_t (更新后)"]

        CONCAT["拼接 [h_{t-1}, x_t]"] --> GATE2["输入门 i_t<br/>σ(W_i·[h_{t-1}, x_t] + b_i)<br/>决定写入哪些新信息"]
        GATE2 --> CT2

        CONCAT --> GATE3["候选记忆 c̃_t<br/>tanh(W_c·[h_{t-1}, x_t] + b_c)"]
        GATE3 --> CT2

        CT2 --> GATE4["输出门 o_t<br/>σ(W_o·[h_{t-1}, x_t] + b_o)<br/>决定输出多少"]
        GATE4 --> HT["h_t = o_t ⊙ tanh(c_t)<br/>短期输出"]
    end

    style GATE1 fill:#e74c3c,color:#fff
    style GATE2 fill:#2ecc71,color:#fff
    style GATE4 fill:#3498db,color:#fff

四个公式总结 LSTM:

ft=σ(Wf⋅[ht−1,xt]+bf)遗忘门it=σ(Wi⋅[ht−1,xt]+bi)输入门c~t=tanh⁡(Wc⋅[ht−1,xt]+bc)候选记忆ct=ft⊙ct−1+it⊙c~t记忆更新ot=σ(Wo⋅[ht−1,xt]+bo)输出门ht=ot⊙tanh⁡(ct)输出

⊙ 表示逐元素乘法。σ 的输出在 (0,1) 之间 → "门"的直觉:0 = 完全关,1 = 完全开。

GRU — LSTM 的简化版 ​

GRU 只有两个门(重置门 + 更新门),参数更少,效果接近:

zt=σ(Wz⋅[ht−1,xt])更新门rt=σ(Wr⋅[ht−1,xt])重置门h~t=tanh⁡(W⋅[rt⊙ht−1,xt])ht=(1−zt)⊙ht−1+zt⊙h~t

Seq2Seq 与 Attention ​

RNN/LSTM 的最大问题:不论输入多长,都必须压缩到最后一个隐藏状态。Attention 机制解决了这个问题。

mermaid
graph TD
    subgraph "Encoder"
        E1["x₁"] --> E_H1["h₁"]
        E2["x₂"] --> E_H2["h₂"]
        E3["x₃"] --> E_H3["h₃"]
    end

    subgraph "Attention"
        S1["Decoder 当前状态 s₁"] --> ATTN["计算注意力权重<br/>权重 = softmax(score(s₁, hᵢ))"]
        E_H1 --> ATTN
        E_H2 --> ATTN
        E_H3 --> ATTN
        ATTN --> CTX["上下文向量<br/>c = Σ αᵢ·hᵢ"]
    end

    subgraph "Decoder"
        CTX --> D1["生成 y₁"]
        D1 --> D2["生成 y₂"]
    end

    style ATTN fill:#e74c3c,color:#fff

注意力权重的计算(以 Luong Attention 为例):

αij=exp⁡(score(si,hj))∑kexp⁡(score(si,hk))score(si,hj)=siTWahj

🧭 学习导航 ​

← 上一阶段:classic-neural-networks | 返回总览 | 下一阶段:llm-engineering →

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记