Chapter3 大语言模型

1.文档阅读

1.1 语言模型与Transformer架构

  1. 从N-gram 到RNN

    N-gram

    • N-gram模型是基于马尔可夫假设建立的语言模型。这里的 “N” 代表我们考虑的上下文窗口大小。

    • 马尔可夫假设是为了解决概率的链式法则难以实现而提出的,其核心思想是:我们不必回溯一个词的全部历史,可以近似的认为一个词出现的概率只与它前面有限的n-1个词有关。

      例子:

      1. Bigram(当N=2时)

        P ( w i ∣ w 1 , . . . , w i − 1 ) ≈ P ( w i ∣ w i − 1 ) P(w_i|w_1,...,w_{i-1})\approx P(w_i|w_{i-1}) P(wiw1,...,wi1)P(wiwi1)

      2. Trigram(当N=3时)

        P ( w i ∣ w 1 , . . . , w i − 1 ) ≈ P ( w i ∣ w i − 2 , w i − 1 ) P(w_i|w_1,...,w_{i-1})\approx P(w_i|w_{i-2},w_{i-1}) P(wiw1,...,wi1)P(wiwi2,wi1)

      这些概率可以通过最大似然估计(Maximum Likelihood Estimation,MLE)来计算。

      例:

      P ( w i ∣ w i − 1 ) = C o u n t ( w i − 1 , w i ) C o u n t ( w i − 1 ) P(w_i|w_{i-1})=\frac{Count(w{i-1},w_{i})}{Count(w_{i-1})} P(wiwi1)=Count(wi1)Count(wi1,wi)

      其中

      • C o u n t ( w i − 1 , w i ) Count(w_{i-1},w_i) Count(wi1,wi)表示词对 ( w i − 1 , w i ) (w_{i-1},w_i) (wi1,wi)在语料库中连续出现的次数。
      • C o u n t ( w i − 1 ) Count(w_{i-1}) Count(wi1)表示单个词 w i − 1 w_{i-1} wi1在语料库中出现的次数。
    • N-gram的缺陷:

      1. 数据稀疏性
      2. 泛化能力差

    神经网络

    N-gram 模型的根本缺陷在于它将词视为孤立、离散的符号。为了克服这个问题,研究者们转向了神经网络,并提出了一种思想:用连续的向量来表示词。

    • 前馈神经网络语言模型 (Feedforward Neural Network Language Model)
      核心思想:

      1. 构建一个语义空间
      2. 学习从上下文到下一个词的映射

      我们将词转换成了向量,所以可以用数学工具来度量它们之间的关系。最常用的方法是余弦相似度(Cosine Similarity)

    s i m i l a r i t y ( a ⃗ , b ⃗ ) = cos ⁡ ( θ ) = a ⃗ ⋅ b ⃗ ∣ a ⃗ ∣ ∣ b ⃗ ∣ similarity(\vec{a},\vec{b})=\cos(\theta)=\frac{\vec{a} \cdot \vec{b}}{|\vec{a}||\vec{b}|} similarity(a ,b )=cos(θ)=a ∣∣b a b

    • 循环神经网络(RNN):

      提出:打破固定窗口的限制,核心思想是为网络增加“记忆能力”

      缺点:长期依赖问题

    • 长短时记忆网络(LSTM)

      提出:解决长期依赖问题,核心创新在于引入了细胞状态 (Cell State) 和一套精密的门控机制 (Gating Mechanism)

  2. Transformer架构解析

    Transformer 完全抛弃了循环结构,依赖注意力机制来捕捉序列内的依赖关系,从而实现了真正意义上的并行计算。

    2.1 Encoder-Decoder整体架构

    编码器:任务是“理解”输入的整个句子。

    解码器:任务是“生成”目标句子

    2.2 注意力机制

    自注意力(Self-Attention)机制允许模型在处理序列中的每个词时,都能兼顾句子中的所有其他词,并为这些词分配不同的注意力权重。权重越高的词,代表其与当前词的关联性越强,其信息也应该在当前词的表示中占据更大的比重。

    为了实现上述过程,自注意力机制为每个输入的词元向量引入了三个可学习的角色

    • 查询(Query,Q):代表当前词元,它正在主动的“查询”其他词元以获得信息。
    • 键(Key,K):代表句子中可被查询的词元“标签”或“索引”。
    • 值(Value,V):代表词元本身所携带的“内容”或“信息”。

    这三个向量都是由原始的词嵌入向量乘以三个不同的、可学习的权重矩阵( W Q , W K , W V W^Q,W^K,W^V WQ,WK,WV)得到的。

    计算过程公式表示:

    A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dk QKT)V

    只进行一次的注意力计算:单头注意力,多头注意力的思想:把一次做完然后分成几组,分开做,在合并。

    2.3 前馈神经网络

    在每个Encoder和Decoder层中,多头注意力子层之后都跟着一个逐位置前馈网络。这个前馈网络会独立的作用于序列中的每一个词元向量。

    F F N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x)=max(0,xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1+b1)W2+b2

    2.4 残差连接与层归一化

    在 Transformer 的每个编码器和解码器层中,所有子模块(如多头注意力和前馈网络)都被一个 Add & Norm 操作包裹。这个组合是为了保证 Transformer 能够稳定训练。

    2.5 位置编码

    为了解决自注意力机制计算不包括任何关于词元顺序或位置的信息,引入位置编码。

  3. Decoder-only 架构

    Decoder-only架构的工作模式被称为自回归(Autoregressive)描述的过程如下:

    • 给模型一个起始文本(例如“Datawhale Agent is”)
    • 模型预测出下一个最有可能的词(例如 “a”)
    • 模型将自己刚刚生成的词“a”添加到输入文本的末尾,形成新的输入
    • 模型基于这个新输入,再次预测下一个词
    • 不断重复这个过程,直到生成完整的句子或达到停止条件

    解码器通过掩码自注意力保证在预测第t个词时,不去偷看第t+1个词的答案。

    在自注意力机制计算出注意力分数矩阵(即每个词对其他所有词的关注度得分)之后,但在进行 Softmax 归一化之前,模型会应用一个“掩码”。这个掩码会将所有位于当前位置之后(即目前尚未观测到)的词元对应的分数,替换为一个非常大的负数。当这个带有负无穷分数的矩阵经过 Softmax 函数时,这些位置的概率就会变为 0。这样一来,模
    型在计算任何一个位置的输出时,都从数学上被阻止了去关注它后面的信息。这种机制保证了模型在预测下一个词时,能且仅能依赖它已经见过的、位于当前位置之前的所有信息,从而确保了预测的公平性和逻辑的连贯性。

1.2 与大语言模型交互

  1. 提示工程

    研究如何设计精准的提示,从而引导模型产生我们期望输出的回复。

    模型采样参数

    大模型中经常有Temperature的可配置参数,其本质是通过调整模型对“概论分布”的采样策略,让输出匹配具体场景需求。

    采样参数本质是在Softmax公式的基础上,根据不同策略“重新调整”或“截断”分布,从而改变大模型输出的下一个token。

    • Temperture:温度是控制模型输出“随机性”与“确定性”的关键参数。其原理是引入温度系数 T > 0 T\gt0 T>0,将Softmax改写为 p i ( T ) = e z i / T ∑ j = 1 k e z j / T p_i^{(T)} = \frac{e^{z_i / T}}{\sum_{j=1}^k e^{z_j / T}} pi(T)=j=1kezj/Tezi/T
      • 低温度时输出更“精确、确定”。
      • 中温度时输出“平衡、自然”。
      • 高温度时输出“创新、发散”
    • Top-k:其原理是将所有token按概率从高到低排序,取排名前k个token组成“候选集”,随后对筛选出的k个token的概率进行“归一化”
      • 与温度采样的区别与联系:温度采样通过温度 T 调整所有 token 的概率分布(平滑或陡峭),不改变候选token 的数量(仍考虑全部 N 个)。Top-k 采样通过 k 值限制候选 token 的数量(只保留前 k 个高概率token),再从其中采样。当k=1时输出完全确定,退化为 “贪心采样”
    • Top-p:其原理是将所有的token按概率从高到低排序,从排序后第一个token开始,逐步累加概率,直到累积和首次达到或超过阈值$$,此时累加过程中所有token组成“核集合”,最后对核集合进行归一化。

    零样本、单样本与少样本

    零样本提示 (Zero-shot Prompting) 这指的是我们不给模型任何示例,直接让它根据指令完成任务。这得益于模型在海量数据上预训练后获得的强大泛化能力。

    单样本提示 (One-shot Prompting) 我们给模型提供一个完整的示例,向它展示任务的格式和期望的输出风格。我们给模型提供一个完整的示例,向它展示任务的格式和期望的输出风格。

    少样本提示 (Few-shot Prompting) 我们提供多个示例,这能让模型更准确地理解任务的细节、边界和细微差别,从而获得更好的性能。

  2. 文本分词

    将文本序列转换为数字序列的过程,叫做分词。分词器的作用 是定义一套规则,将原始文本切分成一个个最小的单元,称之为词元(Token)。

    为了兼顾词表大小和语义表达,现代大语言模型普遍采用子词分词 (Subword Tokenization) 算法。它的核心思想是:将常见的词(如 “agent”)保留为完整的词元,同时将不常见的词(如 “Tokenization”)拆分成多个有意义的子词片段(如 “Token” 和 “ization”)。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。

    • 字节对编码算法解析

    字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一,GPT系列模型就采用了这种算法。其核心思想非常简洁,可以理解为一个“贪心”的合并过程:

    1. 初始化:将词表初始化为所有在语料库中出现过的基本字符。
    2. 迭代合并:在语料库上,统计所有相邻词元对的出现频率,找到频率最高的一对,将它们合并成一个新的词元,并加入词表。
    3. 重复:重复第 2 步,直到词表大小达到预设的阈值。
  3. 模型的选择

    公开的基准测试排行榜,可以用来评估模型的综合能力

    LMArena Leaderboard - a Hugging Face Space by lmarena-ai

1.3 大语言模型的缩放法则与局限性

  1. 缩放法则

    **缩放法则(Scaling Laws)**发现为大语言模型的持续发展提供了理论指导,阐明了增加资源投入能够系统性提升模型性能的底层逻辑。

    缩放法则最令人惊奇的产物是**“能力的涌现”**。所谓能力涌现,是指当模型规模达到一定阈值后,会突然展现出在小规模模型中完全不存在或表现不佳的全新能力。

  2. 模型幻觉

    **模型幻觉(Hallucination)**通常指的是大语言模型生成的内容与客观事实、用户输入或上下文信息相矛盾,或者生成了不存在的事实、实体或事件。

2.代码学习

2.1 N_gram

使用Bigram(N=2),每次考察连续的两个词

import collections

# 示例语料库,与上方案例讲解中的语料库保持一致
corpus = "datawhale agent learns datawhale agent works"
tokens = corpus.split()
total_tokens = len(tokens)

print(tokens) #['datawhale', 'agent', 'learns', 'datawhale', 'agent', 'works']
# --- 第一步:计算 P(datawhale) ---
count_datawhale = tokens.count('datawhale')
p_datawhale = count_datawhale / total_tokens
print(f"第一步: P(datawhale) = {count_datawhale}/{total_tokens} = {p_datawhale:.3f}")

# --- 第二步:计算 P(agent|datawhale) ---
# 先计算 bigrams 用于后续步骤
bigrams = zip(tokens, tokens[1:])
bigram_counts = collections.Counter(bigrams)
count_datawhale_agent = bigram_counts[('datawhale', 'agent')]
# count_datawhale 已在第一步计算
p_agent_given_datawhale = count_datawhale_agent / count_datawhale
print(f"第二步: P(agent|datawhale) = {count_datawhale_agent}/{count_datawhale} = {p_agent_given_datawhale:.3f}")

# --- 第三步:计算 P(learns|agent) ---
count_agent_learns = bigram_counts[('agent', 'learns')]
count_agent = tokens.count('agent')
p_learns_given_agent = count_agent_learns / count_agent
print(f"第三步: P(learns|agent) = {count_agent_learns}/{count_agent} = {p_learns_given_agent:.3f}")

# --- 最后:将概率连乘 ---
p_sentence = p_datawhale * p_agent_given_datawhale * p_learns_given_agent
print(f"最后: P('datawhale agent learns') ≈ {p_datawhale:.3f} * {p_agent_given_datawhale:.3f} * {p_learns_given_agent:.3f} = {p_sentence:.3f}")

2.2 Word_Embedding

一个著名的例子展示了词向量捕捉到的语义关系: vector('King') - vector('Man') + vector('Woman') 这个向量运算的结果,在向量空间中与 vector('Queen') 的位置惊人地接近。

代码如下:

import numpy as np

# 假设我们已经学习到了简化的二维词向量
embeddings = {
    "king": np.array([0.9, 0.8]),
    "queen": np.array([0.9, 0.2]),
    "man": np.array([0.7, 0.9]),
    "woman": np.array([0.7, 0.3])
}

def cosine_similarity(vec1, vec2):
    dot_product = np.dot(vec1, vec2)
    norm_product = np.linalg.norm(vec1) * np.linalg.norm(vec2)
    return dot_product / norm_product

# king - man + woman
result_vec = embeddings["king"] - embeddings["man"] + embeddings["woman"]

# 计算结果向量与 "queen" 的相似度
sim = cosine_similarity(result_vec, embeddings["queen"])

print(f"king - man + woman 的结果向量: {result_vec}")
print(f"该结果与 'queen' 的相似度: {sim:.4f}")

2.3 Transformer

import torch
import torch.nn as nn
import math
import copy

class MultiHeadAttention(nn.Module):
    """
    多头注意力机制模块
    """
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        """
        d_model: 输入和输出的特征维度
        num_heads: 注意力头的数量
        多头自注意力:
        将原始的 Q, K, V 向量在维度上切分成 h 份(h 就是“头”数),
        每一份都独立地进行一次单头注意力的计算。
        这就好比让 h 个不同的“专家”从不同的角度去审视句子,
        每个专家都能捕捉到一种不同的特征关系。
        最后,将这 h 个专家的“意见”(即输出向量)拼接起来,
        再通过一个线性变换进行整合,就得到了最终的输出。
        """
        assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        # 定义 Q, K, V 和输出的线性变换层
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)
        
    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        # 1. 计算注意力得分 (QK^T)
        attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        # 2. 应用掩码 (如果提供)
        if mask is not None:
            # 将掩码中为 0 的位置设置为一个非常小的负数,这样 softmax 后会接近 0
            attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
        
        # 3. 计算注意力权重 (Softmax)
        attn_probs = torch.softmax(attn_scores, dim=-1)
        
        # 4. 加权求和 (权重 * V)
        output = torch.matmul(attn_probs, V)
        return output
        
    def split_heads(self, x):
        # 将输入 x 的形状从 (batch_size, seq_length, d_model)
        # 变换为 (batch_size, num_heads, seq_length, d_k)
        # 用transpose调整维度顺序以适应多头计算
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
        
    def combine_heads(self, x):
        # 将输入 x 的形状从 (batch_size, num_heads, seq_length, d_k)
        # 变回 (batch_size, seq_length, d_model)
        batch_size, num_heads, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
        
    def forward(self, Q, K, V, mask=None):
        # 1. 对 Q, K, V 进行线性变换
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))
        
        # 2. 计算缩放点积注意力
        attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
        
        # 3. 合并多头输出并进行最终的线性变换
        output = self.W_o(self.combine_heads(attn_output))
        return output

class PositionWiseFeedForward(nn.Module):
    """
    位置前馈网络模块
    """
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionWiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        # x 形状: (batch_size, seq_len, d_model)
        x = self.linear1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.linear2(x)
        # 最终输出形状: (batch_size, seq_len, d_model)
        return x

class PositionalEncoding(nn.Module):
    """
    为输入序列的词嵌入向量添加位置编码。
    """
    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)

        # 创建一个足够长的位置编码矩阵
        position = torch.arange(max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
        
        # pe (positional encoding) 的大小为 (max_len, d_model)
        pe = torch.zeros(max_len, d_model)
        
        # 偶数维度使用 sin, 奇数维度使用 cos
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        # 将 pe 注册为 buffer,这样它就不会被视为模型参数,但会随模型移动(例如 to(device))
        self.register_buffer('pe', pe.unsqueeze(0))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x.size(1) 是当前输入的序列长度
        # 将位置编码加到输入向量上
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)

class EncoderLayer(nn.Module):
    """
    编码器核心层
    """
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, mask):
        # 1. 多头自注意力
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        
        # 2. 前馈网络
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        
        return x

class DecoderLayer(nn.Module):
    """
    解码器核心层
    """
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.cross_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, encoder_output, src_mask, tgt_mask):
        # 1. 掩码多头自注意力 (对自己)
        attn_output = self.self_attn(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout(attn_output))
        
        # 2. 交叉注意力 (对编码器输出)
        cross_attn_output = self.cross_attn(x, encoder_output, encoder_output, src_mask)
        x = self.norm2(x + self.dropout(cross_attn_output))
        
        # 3. 前馈网络
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))
        
        return x

class Encoder(nn.Module):
    def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len):
        super(Encoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoder = PositionalEncoding(d_model, dropout, max_len)
        self.layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
        self.norm = nn.LayerNorm(d_model)

    def forward(self, x, mask):
        x = self.embedding(x)
        x = self.pos_encoder(x)
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

class Decoder(nn.Module):
    def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len):
        super(Decoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoder = PositionalEncoding(d_model, dropout, max_len)
        self.layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
        self.norm = nn.LayerNorm(d_model)

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        x = self.embedding(x)
        x = self.pos_encoder(x)
        for layer in self.layers:
            x = layer(x, encoder_output, src_mask, tgt_mask)
        return self.norm(x)

class Transformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len=5000):
        super(Transformer, self).__init__()
        self.encoder = Encoder(src_vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len)
        self.decoder = Decoder(tgt_vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len)
        self.final_linear = nn.Linear(d_model, tgt_vocab_size)
    
    def generate_mask(self, src, tgt):
        # src_mask: (batch_size, 1, 1, src_len)
        src_mask = (src != 0).unsqueeze(1).unsqueeze(2) 
        
        # tgt_mask: (batch_size, 1, tgt_len, tgt_len)
        tgt_pad_mask = (tgt != 0).unsqueeze(1).unsqueeze(2) # (batch_size, 1, 1, tgt_len)
        tgt_len = tgt.size(1)
        # 下三角矩阵,用于防止看到未来的 token
        tgt_sub_mask = torch.tril(torch.ones((tgt_len, tgt_len), device=src.device)).bool() # (tgt_len, tgt_len)
        tgt_mask = tgt_pad_mask & tgt_sub_mask
        
        return src_mask, tgt_mask
    
    def forward(self, src, tgt):
        src_mask, tgt_mask = self.generate_mask(src, tgt)
        
        encoder_output = self.encoder(src, src_mask)
        decoder_output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
        
        output = self.final_linear(decoder_output)
        return output

# --- 演示如何使用模型 ---
if __name__ == "__main__":
    # 1. 定义超参数
    src_vocab_size = 5000
    tgt_vocab_size = 5000
    d_model = 512
    num_layers = 6
    num_heads = 8
    d_ff = 2048
    dropout = 0.1
    max_len = 100
    
    # 2. 实例化模型
    model = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_len)
    
    # 3. 创建模拟输入数据
    # 假设 batch_size=2, src_seq_len=10, tgt_seq_len=12
    src = torch.randint(1, src_vocab_size, (2, 10))  # (batch_size, seq_length)
    tgt = torch.randint(1, tgt_vocab_size, (2, 12))  # (batch_size, seq_length)

    # 4. 模型前向传播
    output = model(src, tgt)
    
    # 5. 打印输出形状
    print("模型输出的形状:", output.shape)
    # 预期输出: torch.Size([2, 12, 5000]) -> (batch_size, tgt_seq_len, tgt_vocab_size)

2.4 BPE

import re, collections

def get_stats(vocab):
    """统计词元对频率"""
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    return pairs

def merge_vocab(pair, v_in):
    """合并词元对"""
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

# 准备语料库,每个词末尾加上</w>表示结束,并切分好字符
vocab = {'h u g </w>': 1, 'p u g </w>': 1, 'p u n </w>': 1, 'b u n </w>': 1}
num_merges = 4 # 设置合并次数

for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print(f"第{i+1}次合并: {best} -> {''.join(best)}")
    print(f"新词表(部分): {list(vocab.keys())}")
    print("-" * 20)

2.5 Qwen

运行结果:
在这里插入图片描述

3.习题

  1. 自然语言处理中,语言模型经历了从统计到神经网络的模型演进。

    • 请使用本章提供的迷你语料库(datawhale agent learns, datawhale agent works),计算句子 agent works 在Bigram模型下的概率

      answer:

      同样的 N = 2 N=2 N=2

      代码如下:

      import collections
      
      # 示例语料库,与上方案例讲解中的语料库保持一致
      corpus = "datawhale agent learns datawhale agent works"
      tokens = corpus.split()
      total_tokens = len(tokens)
      
      print(tokens) #['datawhale', 'agent', 'learns', 'datawhale', 'agent', 'works']
      # --- 第一步:计算 P(agent) ---
      count_agent = tokens.count('agent')
      p_agent = count_agent / total_tokens
      print(f"第一步: P(agent) = {count_agent}/{total_tokens} = {p_agent:.3f}")
      
      # --- 第二步:计算 P(works|agent) ---
      # 先计算 bigrams 用于后续步骤
      bigrams = zip(tokens, tokens[1:])
      bigram_counts = collections.Counter(bigrams)
      count_agent_works = bigram_counts[('agent', 'works')]
      # count_agent 已在第一步计算
      p_works_given_agent = count_agent_works / count_agent
      print(f"第二步: P(works|agent) = {count_agent_works}/{count_agent} = {p_works_given_agent:.3f}")
      
      # # --- 第三步:计算 P(learns|agent) ---
      # count_agent_learns = bigram_counts[('agent', 'learns')]
      # count_agent = tokens.count('agent')
      # p_learns_given_agent = count_agent_learns / count_agent
      # print(f"第三步: P(learns|agent) = {count_agent_learns}/{count_agent} = {p_learns_given_agent:.3f}")
      
      # --- 最后:将概率连乘 ---
      p_sentence = p_agent * p_works_given_agent 
      # * p_learns_given_agent
      print(f"最后: P('agent works') ≈ {p_agent:.3f} * {p_works_given_agent:.3f}  = {p_sentence:.3f}")
      
      

      在这里插入图片描述

      运行结果

    • N-gram模型的核心假设是马尔可夫假设。请解释这个假设的含义,以及N-gram模型存在哪些根本性局限?

      answer

      马尔可夫假设的含义:一个词出现的改了只与它前面有限的n-1个词有关。

      N-gram模型的根本性局限:1. 数据稀疏性 2. 泛化能力差

    • 神经网络语言模型(RNN/LSTM)和Transformer分别是如何克服N-gram模型局限的?它们各自的优势是什么?

      answer :

      RNN:为网络增加记忆能力,打破固定窗口的限制

      LSTM:引入细胞状态和门控机制,解决长期依赖的问题

      Transformer:解决按顺序处理数据的问题,实现了并行计算。

  2. Transformer架构[4]是现代大语言模型的基础。其中:

    提示:可以结合本章3.1.2节的代码实现来辅助理解

    • 自注意力机制(Self-Attention)的核心思想是什么?

      A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dk QKT)V

    • 为什么Transformer能够并行处理序列,而RNN必须串行处理?位置编码(Positional Encoding)在其中起什么作用?

      answer:因为transformer可以同时计算整个序列,而RNN是有顺序依赖的。位置编码的作用是将语序纳入计算中。

    • Decoder-Only架构与完整的Encoder-Decoder架构有什么区别?为什么现在主流的大语言模型都采用Decoder-Only架构?

      answer:Decoder-Only:只有Decoder,因为”言的核心任务,不就是预测下一个最有可能出现的词吗?

  3. 文本子词分词算法是大语言模型的一项关键技术,负责将文本转换为模型可处理的 token 序列。那为什么不能直接以"字符"或"单词"作为模型的输入单元?BPE(Byte Pair Encoding)算法解决了什么问题?

    answer :因为计算机本质上只能处理数字。BPE算法控制了词表的大小又能通过组合子词来理解和生成新词。

  4. 本章3.2.3节介绍了如何本地部署开源大语言模型。请完成以下实践和分析:

    提示:这是一道动手实践题,建议实际操作

    • 按照本章的指导,在本地部署一个轻量级的开源模型(推荐Qwen3-0.6B),并尝试调整采样参数并观察其对输出的影响

      answer:

      generated_ids = model.generate(
          model_inputs.input_ids,
          max_new_tokens=512,
          # 调整参数
          do_sample=True,
          top_p=0.9,
          temperature=0.7
      )
      

      在这里插入图片描述

    • 选择一个具体任务(如文本分类、信息抽取、代码生成等),设计并对比以下不同的提示策略(如Zero-shot、Few-shot、Chain-of-Thought)对输出结果的效果差异

      answer:任务选择:文本分类

      # 准备对话输入
      messages = [
          {"role": "system", "content": "You are a text classifier."},
          {"role": "user", "content": "请帮我分类以下文本:\n\n'人工智能正在改变世界。'"},
      ]
      
      

      Zero-shot:
      在这里插入图片描述

      Few-shot:

      # 准备对话输入
      messages = [
          {"role": "system", "content": "You are a text classifier."},
          {"role": "user", "content": "文本:'我爱自然语言处理' 分类:陈述句"},
          {"role": "user", "content": "文本:'今天天气怎么样?' 分类:"},
          ]
      

      在这里插入图片描述

      Chain-of-Thought:

      # 准备对话输入
      messages = [
          {"role": "system", "content": "You are a text classifier."},
          {"role": "user", "content": "文本:'我爱自然语言处理' 分类:陈述句"},
          {"role": "user", "content": "文本:'今天天气怎么样?' 分类:疑问句"},
          {"role": "user", "content": "文本:'请帮我写一首诗。' 分类:祈使句"},
          {"role": "user", "content": "文本:'多喝热水!' 分类:感叹句"},
          {"role": "user", "content": "文本:'机器学习是人工智能的一个分支' 分类:"},
          ]
      

      在这里插入图片描述

    • 从性能、成本、可控性、隐私等维度比较闭源模型和开源模型

      answer:

      维度闭源模型 (Closed-source)开源模型 (Open-source)
      性能天花板更高。通常在逻辑推理、多模态能力上领先 3-6 个月。追赶速度极快。在特定任务和中等规模(7B-70B)上已并驾齐驱。
      成本按量计费 (OPEX)。初期投入极低,随使用量增加而线性增长。硬件/运维成本 (CAPEX)。需采购 GPU 或租用算力,长期大规模使用更省钱。
      可控性较低。黑盒操作,依赖 Prompt 和厂商提供的微调 API。极高。可修改架构、全量微调、量化剪枝,完全自定义输出行为。
      隐私信任机制。数据需传输至服务商云端(即使有企业版协议)。主权受控。可本地/专网部署,数据全程不出内网。
    • 如果你要构建一个企业级的客服智能体,你会选择哪种类型的模型?需要考虑哪些因素?

      answer:我选择闭源模型

  5. 模型幻觉(Hallucination)[11]是大语言模型当前存在的关键局限性之一。本章介绍了缓解幻觉的方法(如检索增强生成、多步推理、外部工具调用)

    • 请选择其中一种,说明其工作原理和适用场景

      answer

      RAG:RAG 系统通过在生成之前从外部知识库(如文档数据库、网页)中检索相关信息,然后将检索到的信息作为上下文,引导模型生成基于事实的回答。

    • 调研前沿的研究和论文,是否还有其他的缓解模型幻觉的方法,他们又有哪些改进和优势?

      answer

      OpenAI 语言模型为何会产生幻觉

      这篇论文的核心论点是:语言模型之所以产生幻觉,是因为其训练和评估程序奖励“猜测”,而非承认“不确定性”
      解决幻觉问题的关键,是一场自上而下的、针对评估体系的变革。通过引入明确的、带有惩罚机制的置信度目标,并将其实施于主流评估基准中,可以引导整个AI领域从“应试教育”模式转向培养更诚实、更可靠的AI模型。【引用: 全网最详细解读 OpenAI 最新论文:为什么大模型会存在“幻觉”? - 知乎

  6. 假设你要设计一个论文辅助阅读智能体,它能够帮助研究人员快速阅读并理解学术论文,包括:总结论文研究的核心内容、回答关于论文的问题、提取关键信息、比较多篇不同论文的观点等。请回答:

    • 你会选择哪个模型作为智能体设计时的基座模型?选择时需要考虑哪些因素?

      answer

      1. 性能与能力:应该要选择擅长逻辑推理、总结归纳强的模型
      2. 上下文窗口:需要理解长文档、维持长期对话记忆的智能体。
    • 如何设计提示词来引导模型更好地理解学术论文?学术论文通常很长,可能超过模型的上下文窗口限制,你会如何解决这个问题?

      answer :

      给模型特定的身份:例如审稿人 ☺️

      我会分段把论文喂给模型,再将分段得出的结论再喂给模型

    • 学术研究是严谨的,这意味着我们需要确保智能体生成的信息是准确客观忠于原文的。你认为系统中加入哪些设计能够更好的实现这一需求?

      answer :RAG检索

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐