AIGC宇宙 AIGC宇宙

揭秘大模型的魔法:从嵌入向量说起

大家好,我是写代码的中年人,上一篇文章我们介绍了词元的概念及如何训练自己的词元,待训练的数据变成词元后,我们发现词元(文本)之间没有任何联系,也就是说它们是离散的数据,所以我们没办法对词元进行计算。 将离散的文本转化为连续的向量表示,即嵌入向量(Embedding Vector)。 嵌入向量是大模型处理自然语言的起点,它将人类语言的符号转化为机器可以理解的数学表示。

揭秘大模型的魔法:从嵌入向量说起

大家好,我是写代码的中年人,上一篇文章我们介绍了词元的概念及如何训练自己的词元,待训练的数据变成词元后,我们发现词元(文本)之间没有任何联系,也就是说它们是离散的数据,所以我们没办法对词元进行计算。

将离散的文本转化为连续的向量表示,即嵌入向量(Embedding Vector)。嵌入向量是大模型处理自然语言的起点,它将人类语言的符号转化为机器可以理解的数学表示。

本文将以Transformer架构为核心,深入探讨嵌入向量的生成过程,剖析其背后的“魔法”,并通过代码示例展示如何实现这一过程。

嵌入向量的简介

从上一篇我们已经了解了词元和词元ID的概念,最后我们生成了一个词汇表(Vocabulary),并且知道词汇表的大小通常在几万到几十万之间,具体大小取决于模型设计。

词元ID是离散的整数,无法直接用于神经网络的数学运算。因此,嵌入层(Embedding Layer)将词元ID映射为连续的向量表示。嵌入层本质上是一个可学习的查找表,存储为一个形状为 [vocab_size, embedding_dim] 的矩阵,其中:

vocab_size:词汇表的大小。

embedding_dim:每个词元的向量维度。

词汇表的概念我们已经了解,嵌入向量的概念可以简单理解为:你用多少个数字来表示一个词,维度越高,词向量表达的语义就越丰富,但也更复杂。

我们要记住的是嵌入向量是模型最早期的“参数矩阵”,通常是随机初始化的,然后在训练中慢慢学习。

我们先看一个例子,如下代码:

复制
import torch
import torch.nn as nn


# 设置打印选项
torch.set_printoptions(threshold=10000, precisinotallow=4, sci_mode=False)


# 参数定义
vocab_size = 10000
embedding_dim = 256
embedding_layer = nn.Embedding(vocab_size, embedding_dim)


# 输入 token id
token_ids = torch.tensor([101, 102, 103, 104, 105, 106, 107])


# 获取嵌入向量
embeddings = embedding_layer(token_ids)


# 输出嵌入矩阵
print("嵌入矩阵:")
print(embeddings)

执行上面代码后,我们看到程序会输出如下信息:

复制
嵌入矩阵:
tensor([[    -1.1887,     -0.3787,     -1.6036,      1.2109,     -1.5041,
              0.5217,     -0.0660,      0.8761,     -1.3062,     -0.5456,
             -2.2370,     -0.7596,      0.6463,      1.3679,     -0.7995,
             -0.8499,     -1.1883,     -0.4964,     -0.9248,      1.3193,
             -0.3776,     -1.6146,     -0.2606,      1.3084,      1.5899,
             -0.3184,      0.7106,      0.4439,     -1.0974,     -0.0911,
              0.0765,     -1.1273,     -2.0399,     -0.7867,      0.5819,
....中间信息省略
-0.6946,      0.1002,     -0.8110,     -1.1093,      0.4499,
             -0.5466,      0.8090,      1.3586,     -0.4617,      0.0936,
              0.4514,     -1.0935,      1.1986,      0.5158,      0.7961,
              0.1658,      0.9241,     -0.2872,     -1.5406,      0.6301,
              1.3381,     -1.6376,      0.5164,     -1.1603,     -1.0949,
              0.7568,     -0.8883,     -0.0534,     -1.1359,     -0.1575,
             -0.7413]], grad_fn=<EmbeddingBackward0>)

这段代码到底做了什么事情?我们接下来进行详解:

定义嵌入矩阵的大小:

vocab_size = 10000

表示你有一个词汇表(vocabulary),大小是 10,000,意思是你有 10,000 个独立的词(或子词、token),词汇表的概念可以参照上篇文章的介绍。

embedding_dim = 256

表示每个词要被映射为一个256维的向量。这就是“嵌入维度”,你可以理解为:

把每个离散的 token 映射到一个连续空间中,变成一个可学习的向量(表示它的“意义”或“语义”)

初始化嵌入层:

embedding_layer = nn.Embedding(vocab_size, embedding_dim)

nn.Embedding(vocab_size, embedding_dim) 是 PyTorch 提供的嵌入层。

它的作用是创建一个大小为 [vocab_size, embedding_dim] 的查找表,每行对应一个 token 的向量。

换句话说,它是一个形状为 [10000, 256] 的矩阵。每一行是一个词的向量:

token_id = 0 → [0.1234, -0.5321, ..., 0.0012]  # 长度为256

token_id = 1 → [0.3332, -0.8349, ..., -0.2176]

...

token_id = 9999 → [...]

这个矩阵的参数是可训练的,会随着模型训练不断优化,使得语义相近的 token 向量距离也更近。

定义 token id:

token_ids = torch.tensor([101, 102, 103, 104, 105, 106, 107])

这里创建了一个 tensor,内容是 [101, 102, 103, 104, 105, 106, 107],它代表你输入的 7 个词/子词的索引(ID)。

每个数字表示词表中的一个词,例如:

101 → “写”

102 → “代”

103 → “码”

104 → “的”

105 → “中”

106 → “年”

107 → “人”

(这里只是举例,真实情况看 tokenizer)

变为嵌入向量:

embeddings = embedding_layer(token_ids)

把 token_ids [101, 102, 103, 104, 105, 106, 107] 送进嵌入层后,会从嵌入矩阵中取出它们对应的向量,得到:

embeddings.shape == [7, 256]

每个词变成了一个 256 维的向量,这些向量是浮点数,比如:

embeddings[0] = tensor([ 0.1371, -0.0208, ..., 0.0415])  # token 101 的嵌入

embeddings[1] = tensor([-0.0817, 0.2991, ..., 0.0034])  # token 102 的嵌入

...

 输出的向量是啥?

这些 256 维向量就是词的语义向量表示(Word Embedding):

它们是模型可训练参数;

它们的数值是随机初始化的(除非你加载了预训练模型);

它们的作用是:把 token 编码成模型能处理的“连续表示”;

在模型训练过程中,这些向量会逐步学习到语义,比如 “我” 和 “我们” 的向量距离会比 “我” 和 “电脑” 更近。如何训练我们后续再讲,这里只要明白它们是怎么初始化的和有什么作用就行。

最终经过大量语料训练之后,每个 token 的 embedding都是模型学习到的语义表示,它不再“随机”,而是能捕捉词义的相似性。

大概的流程为:

原始输入文本 → tokenizer → token_id → embedding向量 → 加入位置编码 → 输入Transformer

位置编码简介

位置编码(Positional Encoding)是 Transformer 架构的关键组件之一,在Transformer架构中,模型主要依赖自注意力机制来处理输入序列。然而,自注意力机制本身是无序的,即它不考虑输入序列中词或标记(token)的相对位置或绝对位置信息。这会导致模型无法区分序列中不同位置的词,即使它们的语义完全相同。为了解决这个问题,引入了位置编码(Positional Encoding),其作用是:

提供位置信息:为序列中的每个位置赋予一个独特的表示,使模型能够感知词的顺序和相对位置。

保持序列顺序的语义:通过位置编码,Transformer可以理解序列中词的排列顺序对语义的影响。

支持并行计算:位置编码是预先计算或固定的(不像RNN那样依赖序列处理),因此不会影响Transformer的并行化优势。

常见的位置编码方法:

揭秘大模型的魔法:从嵌入向量说起

位置编码是在 进入 Transformer 架构的第一层之前添加的,通常在模型的输入端(即嵌入层之后)。

对于标准 Transformer(如 GPT 或 BERT),位置编码是直接加到词嵌入上,作为整个模型的初始输入。

对于某些变体(如使用 RoPE 的模型),位置信息可能在注意力机制内部通过旋转矩阵应用,但这仍然发生在 Transformer 层处理之前或作为注意力计算的一部分。

接着上面的嵌入向量代码,我们先使用正弦/余弦编码来实现一个位置编码:

复制
import torch
import torch.nn as nn
import math


# 设置打印选项
torch.set_printoptions(threshold=10000, precisinotallow=4, sci_mode=False)


# 定义位置编码类
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)  # Shape: (1, max_len, d_model)
        self.register_buffer('pe', pe)


    def forward(self, x):
        # x: (batch_size, seq_len, d_model)
        x = x + self.pe[:, :x.size(1), :]  # Add positional encoding
        return x


# 参数定义
vocab_size = 10000
embedding_dim = 256
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
pos_encoder = PositionalEncoding(d_model=embedding_dim, max_len=5000)


# 输入 token id
token_ids = torch.tensor([101, 102, 103, 104, 105, 106, 107])


# 获取嵌入向量
embeddings = embedding_layer(token_ids)


# 输出嵌入矩阵
print("嵌入矩阵:")
print(embeddings)


# 添加位置编码
embeddings_with_pe = pos_encoder(embeddings.unsqueeze(0)).squeeze(0)  # Add batch dimension and remove it


# 输出添加位置编码后的矩阵
print("\n添加位置编码后的嵌入矩阵:")
print(embeddings_with_pe)

RoPE 旋转位置编码:(RoPE 只作用在自注意力中的 Query 和 Key 上,不是 Value,也不是 Embedding 本身,下面代码只是示例。)

复制
import torch
import torch.nn as nn
import math


# 设置打印选项(便于查看向量)
torch.set_printoptions(threshold=10000, precisinotallow=4, sci_mode=False)


# ========================
# 旋转位置编码(RoPE)模块
# ========================
class RotaryPositionalEncoding(nn.Module):
    def __init__(self, dim, max_len=5000, base=10000):
        super(RotaryPositionalEncoding, self).__init__()
        assert dim % 2 == 0, "RoPE要求维度必须是偶数。"
        self.dim = dim
        self.max_len = max_len
        self.base = base
        self._build_cache()


    def _build_cache(self):
        half_dim = self.dim // 2
        inv_freq = 1.0 / (self.base ** (torch.arange(0, half_dim).float() / half_dim))  # [dim/2]
        pos = torch.arange(self.max_len).float()  # [max_len]
        sinusoid = torch.einsum('i,j->ij', pos, inv_freq)  # [max_len, dim/2]
        self.register_buffer('sin', torch.sin(sinusoid))  # [max_len, dim/2]
        self.register_buffer('cos', torch.cos(sinusoid))  # [max_len, dim/2]


    def forward(self, x):
        """
        输入:
            x: Tensor, shape (batch, seq_len, dim)
        输出:
            Tensor, shape (batch, seq_len, dim),应用RoPE后
        """
        batch_size, seq_len, dim = x.size()
        sin = self.sin[:seq_len].unsqueeze(0).to(x.device)  # [1, seq_len, dim/2]
        cos = self.cos[:seq_len].unsqueeze(0).to(x.device)


        x1 = x[..., 0::2]
        x2 = x[..., 1::2]
        x_rotated = torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
        return x_rotated


# ========================
# 主程序:嵌入 + RoPE 演示
# ========================


# 参数定义
vocab_size = 10000
embedding_dim = 256
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
rope_encoder = RotaryPositionalEncoding(dim=embedding_dim, max_len=5000)


# 输入 token ids(假设是一个样本)
token_ids = torch.tensor([101, 102, 103, 104, 105, 106, 107])  # [seq_len]
embeddings = embedding_layer(token_ids).unsqueeze(0)  # [1, seq_len, dim]


# 应用 RoPE 位置编码
rope_embeddings = rope_encoder(embeddings).squeeze(0)  # [seq_len, dim]


# 打印结果
print("原始嵌入向量:")
print(embeddings.squeeze(0))
print("\n应用 RoPE 后的嵌入向量:")
print(rope_embeddings)

结尾语

在大模型的世界里,嵌入向量和位置编码就像是两把开启理解语言奥秘的钥匙:前者将离散的语言符号映射到连续的语义空间,后者则帮助模型理解“谁先谁后”、“谁靠谁近”。我们从嵌入矩阵的初始化讲起,了解了这些向量是如何从随机开始,逐步在训练中学会“懂语言”的;然后走进了位置编码的演化史,从经典的正弦余弦到如今主流的旋转位置编码(RoPE),我们看到了模型如何用巧妙的方式“感知顺序”,并最终在注意力机制中扮演关键角色。

值得强调的是,RoPE 并不是一种加法编码,而是一种乘法思维,它精准地嵌入在自注意力中的 Query 和 Key 上,为模型引入位置的相对关系感。这种设计既数学优雅,又计算高效,成为当前大语言模型如 LLaMA、ChatGLM 的标配。

理解这些底层机制,不仅有助于我们更好地使用大模型,更是在 AI 工程实践中迈出的坚实一步,也是为我们亲自训练一个基础模型,必须打通的一道关卡。

相关资讯

小红书翻译紧急上线,见证历史:大模型翻译首次上线C端应用!AI竟自称是GPT-4?网友变身“测试狂魔”,疯狂套话,效果拉满了!

编辑 | 伊风出品 | 51CTO技术栈(微信号:blog51cto)程序员键盘敲冒烟,小红书翻译功能这不是就来了吗! 之前大家各种吐槽美国人用的翻译机器不准确,导致大家交流起来“人机感很重”,一些美网友还需要额外用ChatGPT才能实现无缝交流。 这翻译功能一出来,语言障碍什么的都不存在了。
1/20/2025 1:52:45 PM
伊风

几个开发大模型应用常用的 Python 库

一、应用层开发1. FastAPIFastAPI是构建API的优选。 顾名思义,它快速、简单,并能与Pydantic完美集成,实现无缝数据验证。
1/22/2025 10:33:44 AM
zone7

Meta杨立昆引燃全民大讨论:美政府有些人被洗脑了,监管让开源变得像非法一样!Meta也犯过错!大模型不如猫,保质期就3年!

编辑 | 言征出品 | 51CTO技术栈(微信号:blog51cto)1月23日,在冬季达沃斯论坛的“辩论技术”环节,Meta公司副总裁兼首席人工智能科学家Yann Lecun、麻省理工学院媒体实验室主任 Dava Newman、Axios首席技术记者Ina Turpen Fried(主持人)就未来十年前沿科技进行了时长47分钟的“全民”大讨论,话题涵盖了LLM、智能体、消费机器人、脑机接口、跨物种、太空探索,也讨论了非常让Meta敏感的“技术作恶”、审查监管、开闭源之争。 观众们更是抓住机会让两位嘉宾抖出了很多猛料。 Lecun表示,现在的大模型并没有达到预期效果,在很多方面都存在不足:“我认为当前 LLM范式的保质期相当短,可能只有3到5年。
1/26/2025 11:35:05 AM
言征
  • 1