ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手搓Embedding:理解大语言模型如何将文字转化为语义向量

从零手搓Embedding:理解大语言模型如何将文字转化为语义向量 如果你问一个刚接触大语言模型的朋友“GPT 到底是怎么理解我们说的话的” 十有八九他会提到一个词Embedding。这个词听起来很玄乎翻译成“嵌入”或“向量化”但依然让人摸不着头脑。它不像“神经网络”那样有生物学的类比也不像“注意力机制”那样有直观的比喻。很多人只是模糊地知道哦就是把文字变成一串数字。但这串数字为什么就能代表意义它又是怎么被“搓”出来的这正是理解现代 AI尤其是 GPT 这类模型的第一道也是最关键的一道门槛。今天我们不满足于只当一个 API 调用者而是要亲手“搓”一个极简版的 GPT 核心组件从零开始看看Embedding 究竟是如何给冷冰冰的字符注入丰富语义的。你会发现它的本质并非魔法而是一套精巧的、可解释的数学工程。我们将从一个最简单的文本例子出发用 Python 代码一步步实现一个微型“词嵌入”模型。这个过程会让你彻底明白为什么独热编码One-Hot是“哑巴”而 Embedding 是“智者”模型是如何从大量文本中“学习”到词语之间的关系的比如“国王” - “男人” “女人” ≈ “女王”我们手动实现的 Embedding和 GPT 等大模型中动辄数百维的 Embedding 在思路上有何异同读完本文你不仅能透彻理解 Embedding 的原理更能获得一种“手感”——一种从底层把握 AI 模型核心运作方式的直觉。这对于后续调优模型、设计提示词、甚至进行模型微调都至关重要。1. 从“哑巴”到“智者”为什么我们需要 Embedding在计算机眼里所有文字最初都是没有意义的符号。我们最早教会计算机“认识”词语的方法叫做独热编码One-Hot Encoding。假设我们的词汇表只有三个词[猫, 狗, 鱼]。“猫” 表示为[1, 0, 0]“狗” 表示为[0, 1, 0]“鱼” 表示为[0, 0, 1]独热编码的问题显而易见维度灾难词汇表有 5 万个词每个词就是一个 5 万维的向量其中只有一个是 1其余全是 0。极其稀疏计算和存储效率低下。语义缺失在这种表示下“猫”和“狗”的向量 ([1,0,0]和[0,1,0]) 之间的几何距离如欧氏距离与“猫”和“鱼”的距离 ([1,0,0]和[0,0,1]) 是一样的。计算机完全无法感知“猫”和“狗”都是宠物关系更近。无法处理新词遇到词汇表外的词直接抓瞎。Embedding 要解决的正是这两个核心痛点。它的目标是将每个词映射到一个低维、稠密、连续的向量空间中。在这个空间里向量的长度维度是固定的比如 50、100、300 维远小于词汇表大小。向量的每个位置都是一个浮点数包含了丰富的语义信息。语义相似的词其向量在空间中的位置也接近。那么关键问题来了这个充满语义信息的向量是怎么来的答案是从上下文中学习而来。2. 核心思想“观其友知其人”的分布式假设语言学中有一个著名的分布式假设Distributional Hypothesis一个词的含义是由它周围的上下文决定的。通俗讲“经常出现在相似语境中的词其含义也相似”。例如“苹果”这个词如果它经常和“吃”、“甜”、“水果”一起出现那它很可能指水果如果它经常和“公司”、“手机”、“iOS”一起出现那它很可能指科技品牌。Embedding 模型如经典的 Word2Vec正是基于这一思想。它通过让模型完成一个“填空题”任务来学习给定一个中心词预测它周围可能出现的词或反之。在完成这个预测任务的过程中模型被迫去学习每个词的向量表示使得在相似上下文中的词具有相似的向量。接下来我们就用最经典的Skip-gram模型Word2Vec 的一种思路来手搓一个微型 Embedding 生成器。3. 环境准备一个干净的 Python playground我们不需要复杂的深度学习框架仅用numpy进行基础数学运算即可。这能让我们更清晰地看到每一步的计算。# 建议创建一个新的虚拟环境 python -m venv venv_embedding # 激活环境 (Windows) venv_embedding\Scripts\activate # 激活环境 (Mac/Linux) source venv_embedding/bin/activate # 安装唯一依赖 pip install numpy准备好你的代码编辑器我们从一个简单的语料开始。4. 手搓 Embedding四步实现一个微型语义学习机我们将整个过程分解为四个可执行的步骤。4.1 第一步准备数据与构建词汇表我们使用一个极小的句子集合作为语料以便观察中间过程。# corpus.py import numpy as np # 1. 定义微型语料 corpus [ the king is a man, the queen is a woman, the man is strong, the woman is wise, the cat is a pet, the dog is a pet ] # 2. 文本预处理转小写分词 def preprocess(text): return text.lower().split() tokenized_corpus [preprocess(sentence) for sentence in corpus] print(分词后的语料, tokenized_corpus) # 3. 构建词汇表 vocab set() for sentence in tokenized_corpus: for word in sentence: vocab.add(word) vocab sorted(list(vocab)) # 排序以保证顺序固定 word_to_id {word: idx for idx, word in enumerate(vocab)} id_to_word {idx: word for word, idx in word_to_id.items()} print(词汇表, vocab) print(词汇到ID的映射, word_to_id)运行这段代码你会得到分词后的语料 [[the, king, is, a, man], [the, queen, is, a, woman], ...] 词汇表 [a, cat, dog, is, king, man, pet, queen, strong, the, wise, woman] 词汇到ID的映射 {a: 0, cat: 1, dog: 2, is: 3, king: 4, man: 5, pet: 6, queen: 7, strong: 8, the: 9, wise: 10, woman: 11}现在每个词都有了唯一的数字 ID。但这还是独热编码的起点。4.2 第二步生成训练数据中心词与上下文词对Skip-gram 模型的任务是给定一个中心词预测它窗口大小内的所有上下文词。 我们设置一个窗口大小window_size为 1即只看中心词左边一个词和右边一个词。# data_generator.py def generate_training_data(tokenized_corpus, word_to_id, window_size1): 生成Skip-gram模型需要的训练数据对 (center_word_id, context_word_id) training_pairs [] vocab_size len(word_to_id) for sentence in tokenized_corpus: sentence_ids [word_to_id[word] for word in sentence] for center_idx, center_word_id in enumerate(sentence_ids): # 确定上下文窗口的起止索引 start max(0, center_idx - window_size) end min(len(sentence_ids), center_idx window_size 1) for context_idx in range(start, end): if context_idx ! center_idx: # 跳过中心词自己 context_word_id sentence_ids[context_idx] training_pairs.append((center_word_id, context_word_id)) return np.array(training_pairs) # 生成数据 training_pairs generate_training_data(tokenized_corpus, word_to_id, window_size1) print(训练数据对 (中心词ID, 上下文词ID)) print(training_pairs[:10]) # 打印前10对 print(f总共生成 {len(training_pairs)} 个训练对)输出示例训练数据对 (中心词ID, 上下文词ID) [[ 9 4] # (the, king) [ 4 9] # (king, the) [ 4 3] # (king, is) ... ] 总共生成 56 个训练对我们的模型就要学习从9the预测4king从4king预测9the和3is这样的关系。4.3 第三步搭建并训练一个极简的 Embedding 模型这是最核心的部分。我们将实现一个没有隐藏层的“神经网络”它本质上是在学习两个矩阵W1 (Embedding 矩阵)形状为(vocab_size, embedding_dim)。它的每一行就是对应词的词向量输入向量。W2 (输出矩阵)形状为(embedding_dim, vocab_size)。用于将中心词向量映射到整个词汇表上的概率分布。模型流程将中心词的独热编码(1, vocab_size)乘以 W1得到其词向量(1, embedding_dim)。将词向量乘以 W2得到 logits(1, vocab_size)。对 logits 应用 softmax得到预测每个词作为上下文词的概率。用交叉熵损失比较预测概率和真实上下文词也是独热编码的差异。通过梯度下降更新 W1 和 W2。注意在实际的 Word2Vec 中会使用负采样等技巧来加速训练。为了极致简化我们这里使用完整的 softmax。# simple_embedding_model.py import numpy as np class SimpleEmbeddingModel: def __init__(self, vocab_size, embedding_dim, learning_rate0.01): self.vocab_size vocab_size self.embedding_dim embedding_dim self.lr learning_rate # 随机初始化权重矩阵 # W1 就是我们要的 Embedding 矩阵 self.W1 np.random.randn(vocab_size, embedding_dim) * 0.01 # 输入到隐藏层 self.W2 np.random.randn(embedding_dim, vocab_size) * 0.01 # 隐藏层到输出 def forward(self, center_word_id): 前向传播输入中心词ID返回预测的概率分布 # 1. 获取中心词的 one-hot 向量 (1, vocab_size) x np.zeros((1, self.vocab_size)) x[0, center_word_id] 1 # 2. 计算隐藏层即词向量 h x * W1 self.h np.dot(x, self.W1) # (1, embedding_dim) # 3. 计算输出层 logits u h * W2 self.u np.dot(self.h, self.W2) # (1, vocab_size) # 4. 应用 softmax 得到概率分布 y_hat # 防止数值溢出减去最大值 exp_u np.exp(self.u - np.max(self.u)) self.y_hat exp_u / np.sum(exp_u, axis1, keepdimsTrue) # (1, vocab_size) return self.y_hat def backward(self, center_word_id, context_word_id): 反向传播计算梯度并更新权重 # 1. 准备输入和真实标签的 one-hot x np.zeros((1, self.vocab_size)) x[0, center_word_id] 1 y np.zeros((1, self.vocab_size)) y[0, context_word_id] 1 # 2. 计算输出层的误差 (梯度) # 对于交叉熵损失 softmax输出层的误差 δ y_hat - y delta_output self.y_hat - y # (1, vocab_size) # 3. 计算 W2 和隐藏层的梯度 # dL/dW2 h.T * δ_output grad_W2 np.dot(self.h.T, delta_output) # (embedding_dim, vocab_size) # 隐藏层的误差 δ_hidden δ_output * W2.T delta_hidden np.dot(delta_output, self.W2.T) # (1, embedding_dim) # 4. 计算 W1 的梯度 # dL/dW1 x.T * δ_hidden grad_W1 np.dot(x.T, delta_hidden) # (vocab_size, embedding_dim) # 5. 更新参数 (SGD) self.W2 - self.lr * grad_W2 self.W1 - self.lr * grad_W1 def train(self, training_pairs, epochs100): 训练模型 for epoch in range(epochs): total_loss 0 np.random.shuffle(training_pairs) # 打乱数据 for center_id, context_id in training_pairs: # 前向传播 y_hat self.forward(center_id) # 计算交叉熵损失仅用于监控 loss -np.log(y_hat[0, context_id] 1e-8) # 加上极小值防止log(0) total_loss loss # 反向传播并更新 self.backward(center_id, context_id) if (epoch 1) % 20 0: avg_loss total_loss / len(training_pairs) print(fEpoch {epoch1}, Average Loss: {avg_loss:.4f}) def get_embedding(self, word): 获取训练好的词向量 if word in word_to_id: word_id word_to_id[word] return self.W1[word_id] # 直接返回 W1 矩阵的对应行 else: return None # 初始化并训练模型 vocab_size len(vocab) embedding_dim 5 # 为了可视化我们使用很小的维度 model SimpleEmbeddingModel(vocab_size, embedding_dim, learning_rate0.1) print(开始训练...) model.train(training_pairs, epochs200) print(训练完成)运行这段代码你会看到损失在逐渐下降。我们的微型模型正在从几十个“中心词-上下文词”对中学习规律。4.4 第四步观察结果与语义验证训练完成后model.W1这个矩阵就是我们千辛万苦得到的Embedding 矩阵它的每一行对应一个词的向量。# evaluation.py # 1. 查看所有词的 Embedding print(训练得到的词向量Embedding) for word in vocab: vec model.get_embedding(word) print(f{word:10s}: {np.round(vec, 4)}) # 保留4位小数便于观察 # 2. 计算词向量之间的余弦相似度 def cosine_similarity(vec_a, vec_b): 计算两个向量的余弦相似度 dot_product np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) return dot_product / (norm_a * norm_b 1e-8) # 防止除零 # 3. 测试一些有趣的语义关系 print(\n--- 语义相似度测试 ---) test_pairs [(king, queen), (man, woman), (cat, dog), (strong, wise), (king, pet)] for word1, word2 in test_pairs: vec1 model.get_embedding(word1) vec2 model.get_embedding(word2) if vec1 is not None and vec2 is not None: sim cosine_similarity(vec1, vec2) print(f相似度 {word1} vs {word2}: {sim:.4f}) # 4. 尝试经典的向量运算国王 - 男人 女人 ≈ 女王 print(\n--- 向量运算king - man woman ≈ queen ? ---) vec_king model.get_embedding(king) vec_man model.get_embedding(man) vec_woman model.get_embedding(woman) vec_queen model.get_embedding(queen) if all(v is not None for v in [vec_king, vec_man, vec_woman, vec_queen]): vec_result vec_king - vec_man vec_woman # 找出与结果向量最相似的词 similarities {} for word in vocab: vec model.get_embedding(word) sim cosine_similarity(vec_result, vec) similarities[word] sim # 按相似度排序 sorted_similarities sorted(similarities.items(), keylambda x: x[1], reverseTrue) print(与向量 (king - man woman) 最相似的词) for word, sim in sorted_similarities[:5]: # 显示前5个 print(f {word}: {sim:.4f}) print(f (真实的 queen 相似度: {cosine_similarity(vec_result, vec_queen):.4f}))运行结果分析你的具体数值会因随机初始化而不同但趋势一致词向量你会看到每个词都被表示为一个 5 维的稠密向量例如king: [ 0.12 -0.45 0.78 0.02 -0.33]。语义相似度cat和dog的相似度会非常高因为它们共享“宠物”上下文。king和queen的相似度也会较高共享“王室”上下文。king和pet的相似度会很低。向量运算最激动人心的部分。你会发现计算出的vec_king - vec_man vec_woman这个向量与queen这个词向量的余弦相似度很可能是最高的这证明了我们的微型模型已经捕捉到了“性别”和“王室地位”这种抽象的语义关系。这就是 Embedding 注入意义的过程模型通过完成“预测上下文”这个代理任务被迫将具有相似上下文的词如“猫”和“狗”映射到向量空间中相近的位置并将语义关系如“国王对男人”类似于“女王对女人”编码为向量空间中的平移关系。5. 从我们的“玩具”到 GPT 的“工业级” Embedding我们手搓的模型揭示了 Embedding 的核心学习机制但与现代大模型如 GPT中的 Embedding 相比还有几个关键区别特性我们的手搓模型GPT 等大模型的 Embedding训练目标预测固定窗口内的上下文词Skip-gram。预测下一个词自回归或完形填空MLM目标更复杂。输入单元词Word。更细粒度的子词Subword如 Byte-Pair Encoding能处理未登录词。向量维度5维示例。通常为768、1024、4096甚至更高维表达能力极强。上下文感知静态。每个词只有一个固定的向量。动态上下文相关。同一个词在不同句子中会有不同的向量表示通过 Transformer 的自注意力机制实现。这是质的飞跃。位置信息无。包含位置编码Positional Encoding让模型知道词的顺序。学习方式作为模型参数单独训练。通常是整个巨型 Transformer 模型的一部分与所有其他参数一起进行端到端训练。核心进阶点从静态到动态我们的模型学到的king的向量是固定的。但在 GPT 中“king”这个词的初始向量Token Embedding只是一个起点。当句子“The king ruled the country.”输入模型后经过多层 Transformer 的自注意力机制计算每个词包括“king”的表示都会与句中所有其他词进行交互最终形成一个融合了全句信息的上下文相关表示。这才是 GPT 能理解复杂语义和指代关系的根本。6. 常见问题与排查思路在实践 Embedding 相关项目时你可能会遇到以下问题问题现象可能原因排查方式解决方案词向量相似度计算不合理所有词都差不多。1. 模型未充分训练。2. 学习率设置不当。3. 语料太小或质量太差。4. 向量维度太低。1. 检查训练损失是否已收敛。2. 可视化词向量如用 PCA 降维到2D看图。3. 检查训练数据量。1. 增加训练轮数epochs。2. 调整学习率尝试更小值。3. 使用更大、更相关的语料。4. 适当增加embedding_dim。训练过程损失不下降或变为 NaN。1. 学习率过高导致梯度爆炸。2. 初始化权重值过大。3. 未对 softmax 做数值稳定处理。1. 打印每轮损失和梯度范数。2. 检查W1、W2初始化值。1. 大幅降低学习率如从0.1调到0.01。2. 使用更小的随机初始化标准差如* 0.001。3. 确保 softmax 计算时减去了最大值。无法处理新词OOV。使用了基于词的模型词汇表固定。确认输入词是否在word_to_id字典中。1. 在预处理时替换为UNK标记。2.更优方案使用子词分词如 BPE从根本上解决 OOV。向量运算如类比推理效果差。1. 语料不足以支撑该语义关系。2. 模型容量不足维度太低。3. 训练目标不适合捕捉此类关系。1. 检查语料中相关词的出现频率和共现模式。2. 尝试更大的embedding_dim。1. 使用领域相关的大规模语料。2. 使用更成熟的模型如 Gensim 的 Word2Vec。3. 考虑使用更先进的 Embedding 方法如 GloVe, FastText。7. 最佳实践与工程建议理解了原理后在实际项目中应用 Embedding 时应遵循以下最佳实践语料质量大于数量用于训练 Embedding 的语料必须与你的下游任务高度相关。用通用维基百科语料训练出的向量在医疗或法律领域任务上效果可能不佳。优先使用预训练模型除非有非常特殊的领域和充足的数据否则不要从头训练。像Sentence-Transformers、OpenAI 的 text-embedding 系列或M3E等模型提供了高质量、开箱即用的通用 Embedding。理解静态与动态 Embedding 的适用场景静态 Embedding如 Word2Vec适用于词汇级语义相似度计算、作为简单模型的输入特征。速度快资源消耗低。动态/上下文 Embedding如 BERT, GPT适用于需要理解句子整体语义、一词多义、复杂逻辑关系的任务如文本分类、问答、语义检索。计算成本高。Embedding 的维度选择并非维度越高越好。更高的维度能容纳更多信息但也需要更多数据来训练且可能引入噪声。通常 300-768 维是一个较好的平衡点。可以通过在下游任务上的表现来选择。标准化与相似度计算在使用余弦相似度检索前对 Embedding 向量进行 L2 标准化是标准操作。这能确保相似度计算完全依赖于向量方向不受长度影响。即vec_normalized vec / np.linalg.norm(vec)。生产环境部署对于海量向量的相似度检索务必使用专门的向量数据库Vector Database如 Milvus、Pinecone、Qdrant、Weaviate 等。它们针对高维向量的近似最近邻搜索ANN进行了极致优化远超传统数据库的线性扫描性能。8. 总结与后续方向通过这次“手搓”我们揭开了 Embedding 的神秘面纱。它的核心思想朴素而有力让词语在向量空间中的“位置”由其上下文“邻居”决定。我们从独热编码的“荒漠”出发构建了一个能够学习“国王-男人女人≈女王”这种抽象关系的稠密语义空间。本文带你走通了最关键的几步理解了为什么从独热编码的缺陷到分布式假设的必要性。看到了怎么做用 Skip-gram 目标构建训练数据通过前向/反向传播更新 Embedding 矩阵。验证了效果通过余弦相似度和向量运算直观感受到语义被编码进了数字。要进一步深入你可以复现完整 Word2Vec加入负采样Negative Sampling或层次 Softmax使其能处理大规模语料。探索 GloVe研究基于全局词频统计的另一种经典 Embedding 方法。深入 Transformer理解如何通过自注意力机制实现上下文相关的动态 Embedding这是通往 BERT、GPT 等现代模型的钥匙。实践向量数据库选一个开源向量数据库将本文生成的或下载的预训练 Embedding 导入实现一个简单的语义搜索系统。Embedding 是连接符号世界与连续向量世界的桥梁是让机器理解人类语言语义的基石。希望这次从零开始的手工体验能让你下次在调用model.encode()或看到embedding_dim768时眼前浮现的不再是黑盒而是那个由上下文编织而成的、充满意义的数学空间。建议收藏本文当你需要向他人解释 Embedding或自己遗忘想重温时这份从零构建的代码就是最好的备忘录。
返回列表