从分词到向量化:大语言模型核心预处理流程的Node.js实践
2026/9/9 11:05:56 网站建设 项目流程

1. 项目概述:为什么我们要关心“分词”和“向量化”?

如果你最近在折腾大语言模型,不管是想自己部署一个本地模型玩玩,还是想基于API开发个智能应用,大概率会频繁遇到两个词:Tokenization(分词/令牌化)和Embedding(嵌入/向量化)。它们听起来很技术,但其实是所有大模型理解人类语言的基石。简单来说,模型没法直接读懂“你好,世界”这串字符,它需要先把这句话“切”成模型能认识的小块(Tokenization),再把这些小块转换成模型能“计算”的数字(Embedding)。

这个过程,就像你要教一个完全不懂中文的外星人读古诗。你不能直接把《静夜思》的全文塞给它,它看不懂。你得先教它认识“床”、“前”、“明”、“月”、“光”这些最基本的字或词(这就是分词)。然后,光认识字还不够,你还需要告诉它,“月亮”和“明镜”、“思乡”和“忧愁”在意思上是相近的,这种“意思上的远近”你需要用一套复杂的数学坐标来表示(这就是向量化)。模型最终处理和生成的,就是这一串串的数字坐标。

所以,搞懂从Tokenization到Embedding的完整流水线,绝不是纸上谈兵。它能帮你:

  • 精准控制成本:几乎所有按Token收费的API(如OpenAI),其计费基础就是分词后的Token数量。理解分词规则,你才能准确预估调用开销,甚至优化你的提示词(Prompt)来省钱。
  • 高效调试模型:当模型输出一些匪夷所思的结果时,问题可能出在分词阶段。一个生僻词被切分成奇怪的片段,会导致模型无法正确理解其含义。
  • 构建高级应用:如果你想做语义搜索、文本分类、聚类分析,核心就是获取高质量的文本向量(Embedding)。自己动手实现这个流程,能让你更自由地定制和优化。
  • 深入理解模型原理:这是打开大模型黑盒的第一把钥匙,让你从“魔法调用者”转变为“原理明白人”。

本文将用Node.js作为实践工具,带你把这两个核心环节彻底拆解清楚。选择Node.js,是因为它生态丰富、上手快,非常适合快速构建原型和深入理解算法,而不是仅仅停留在理论层面。我们会从一段简单的文本开始,亲手实现一个简化的分词器,然后探索如何将分词结果转化为有意义的向量,最后串联成完整的流程。你会发现,这背后的思想,远比代码本身更精彩。

2. 核心环节一:Tokenization(分词/令牌化)深度解析

2.1 分词的底层逻辑与常见策略

为什么非要分词?因为计算机,包括神经网络,最擅长处理的是结构化的、固定长度的数字输入。自然语言是高度非结构化的变长序列。分词的核心目标,就是将连续、复杂的字符序列,切割成离散的、模型词汇表(Vocabulary)中存在的子单元。

主流的分词策略主要有三种:

  1. 词级别分词:最简单直观,按空格或标点将文本切分成独立的单词。例如,“I love coding” -> [“I”, “love”, “coding”]。

    • 优点:易于理解,每个Token携带丰富的语义信息。
    • 缺点:词汇表会极其庞大(需要覆盖所有单词的各种形式);无法处理未登录词(OOV);对于中文、日文等没有空格分隔的语言基本无效。
  2. 字符级别分词:将文本切分成单个字符。例如,“hello” -> [“h”, “e”, “l”, “l”, “o”]。

    • 优点:词汇表极小(英文字母+符号不过几百个);彻底解决OOV问题。
    • 缺点:每个Token的语义信息非常稀薄,模型需要从零学习字符组合成词的规律,训练效率低,序列长度极大。
  3. 子词级别分词:这是当今大模型(如GPT系列、BERT、T5)的绝对主流。它介于词和字符之间,旨在找到一种平衡。其核心思想是:将频繁出现的连续字符序列作为一个Token,而不常见的词则被拆分成更小的、可重用的子词单元。

    • 例如:“unhappily” 可能被拆成 [“un”, “happ”, “ily”]。其中 “un” 表示否定,“happ” 是 “happy” 的词干,“ily” 是副词后缀。这样,“happ” 这个Token可以在 “happy”, “happiness”, “happily” 中复用,极大地压缩了词汇表,并让模型能推理出未知词的语义。

目前最流行的子词分词算法是Byte-Pair Encoding (BPE)及其变种(如WordPiece, SentencePiece)。BPE的原理是:从一个基础字符词汇表开始,统计语料中相邻符号对的频率,将最高频的符号对合并成一个新的符号,不断迭代,直到达到预设的词汇表大小。

2.2 用Node.js实现一个简易BPE分词器

理论说再多不如动手。我们来用Node.js实现一个极度简化但核心逻辑完整的BPE分词器。我们会使用一个微型语料库来演示。

// 1. 准备初始词汇表和语料 function initializeVocab(text) { // 将文本转换成字符数组,并添加特殊的结束符 “</w>” 来标记词尾 const words = text.toLowerCase().split(/\s+/).map(word => word.split('').join(' ') + ' </w>'); // 初始词汇表是所有基本字符加上结束符 const vocab = new Set(); words.forEach(word => { word.split(' ').forEach(char => vocab.add(char)); }); // 统计初始的词频(以空格分隔的符号序列为单位) const frequencies = {}; words.forEach(word => { const symbols = word.split(' '); for (let i = 0; i < symbols.length; i++) { const token = symbols[i]; frequencies[token] = (frequencies[token] || 0) + 1; } // 同时统计相邻符号对的出现次数,这是BPE合并的基础 for (let i = 0; i < symbols.length - 1; i++) { const pair = symbols[i] + ' ' + symbols[i + 1]; frequencies[pair] = (frequencies[pair] || 0) + 1; } }); return { words, vocab: Array.from(vocab), frequencies }; } // 2. 核心合并函数 function getMostFrequentPair(frequencies, vocab) { let maxFreq = 0; let bestPair = null; // 遍历频率字典,找出当前最高频的、且可以合并的符号对 for (const [pair, freq] of Object.entries(frequencies)) { const symbols = pair.split(' '); if (symbols.length !== 2) continue; // 确保是配对 // 检查这两个符号是否都在当前词汇表中(即是否已经是基础单元) if (vocab.includes(symbols[0]) && vocab.includes(symbols[1])) { if (freq > maxFreq) { maxFreq = freq; bestPair = pair; } } } return bestPair ? { pair: bestPair, freq: maxFreq } : null; } function mergePair(pair, words, frequencies, vocab) { const [a, b] = pair.split(' '); const newToken = a + b; // 例如将 “l” 和 “o” 合并成 “lo” // 更新词汇表 vocab.push(newToken); // 更新words中的符号序列:将所有相邻的 (a, b) 替换为 newToken const newWords = []; words.forEach(word => { const symbols = word.split(' '); let newSymbols = []; let i = 0; while (i < symbols.length) { if (i < symbols.length - 1 && symbols[i] === a && symbols[i + 1] === b) { newSymbols.push(newToken); i += 2; } else { newSymbols.push(symbols[i]); i += 1; } } newWords.push(newSymbols.join(' ')); }); // 重要:更新频率统计。这是一个简化处理,实际BPE实现中频率更新更复杂。 // 这里我们清空并重新统计,以保持演示清晰。 const newFrequencies = {}; newWords.forEach(word => { const symbols = word.split(' '); symbols.forEach(sym => { newFrequencies[sym] = (newFrequencies[sym] || 0) + 1; }); for (let i = 0; i < symbols.length - 1; i++) { const p = symbols[i] + ' ' + symbols[i + 1]; newFrequencies[p] = (newFrequencies[p] || 0) + 1; } }); return { newWords, newFrequencies, newVocab: vocab }; } // 3. 执行BPE训练 function trainBPE(text, numMerges = 10) { let { words, vocab, frequencies } = initializeVocab(text); console.log('初始词汇表:', vocab); console.log('初始词序列示例:', words[0]); for (let merge = 0; merge < numMerges; merge++) { const pairInfo = getMostFrequentPair(frequencies, vocab); if (!pairInfo) break; console.log(`\n第 ${merge + 1} 次合并: 合并 "${pairInfo.pair}", 频率 ${pairInfo.freq}`); const result = mergePair(pairInfo.pair, words, frequencies, vocab); words = result.newWords; frequencies = result.newFrequencies; vocab = result.newVocab; console.log(`新词汇表大小: ${vocab.length}`); console.log(`新词序列示例: ${words[0]}`); } return { finalVocab: vocab, finalWords: words }; } // 测试 const corpus = "low lower lowest new newer newest"; // 微小语料 const { finalVocab, finalWords } = trainBPE(corpus, 5); console.log('\n=== 最终结果 ==='); console.log('最终词汇表:', finalVocab); console.log('编码后的序列:'); finalWords.forEach((w, i) => console.log(` ${corpus.split(/\s+/)[i]} -> ${w}`));

实操心得与注意事项:

  • 真实世界的复杂性:我们实现的这个版本是高度简化的。生产级的BPE(如tiktoken库中的)会处理字节级编码(解决所有Unicode字符)、更高效的频率统计和合并算法,并保存合并规则(Merge Rules)用于后续编码。
  • 结束符</w>的重要性:它用于区分词边界。例如,“eat”和“eater”中的“eat”部分,如果没有结束符,模型无法区分它们是同一个子词还是不同词的一部分。加上</w>后,“eat ”和“eat”就是两个不同的Token。
  • 词汇表大小的权衡numMerges(合并次数)直接决定词汇表大小。太大会导致词汇表膨胀,失去子词压缩的优势;太小则会导致序列过长,增加计算负担。通常需要在大规模语料上统计确定。
  • 中文分词的差异:对于中文,BPE的初始单元通常是单个汉字。因为汉字本身是语素文字,携带一定语义。所以中文BPE训练后,词汇表里会包含高频词(如“中国”、“经济”)和常用字。

运行上面的代码,你会看到词汇表如何从单个字符逐步生长,并形成像“low”、“er”、“est”这样的可复用子词。这就是大模型“认识”世界的第一步。

3. 核心环节二:Embedding(向量化/嵌入)的本质探究

3.1 从One-Hot到分布式表示:为什么需要向量?

分词之后,我们得到了一串Token ID(每个Token在词汇表中的索引)。最原始的表示方法是One-Hot Encoding:假设词汇表有5万个词,每个词就是一个长度为5万的向量,只有对应索引位置是1,其余全是0。

这种方法问题巨大:

  1. 维度灾难:向量维度等于词汇表大小,动辄数万维,计算和存储开销无法承受。
  2. 语义鸿沟:任意两个词的向量都是正交的(点积为0),无法表达“猫”和“狗”在语义上比“猫”和“飞机”更接近的关系。

Embedding就是为了解决这些问题而生的。它的核心思想是:学习一个低维、稠密的实数向量来表示每个Token,使得语义相似的Token在向量空间中的位置(距离)也相近。这个向量空间通常被称为“嵌入空间”。

例如,在一个训练好的嵌入空间中,vector(“国王”) - vector(“男人”) + vector(“女人”)的结果向量会非常接近vector(“女王”)。这就是著名的“词向量类比”特性,证明了嵌入空间捕获了语义和语法关系。

3.2 Embedding层是如何工作的?

在神经网络(尤其是Transformer)中,Embedding通常是一个可训练的查找表(Lookup Table),也称为Embedding Layer

  • 结构:它是一个大小为[vocab_size, embedding_dim]的矩阵。vocab_size是词汇表大小(如50000),embedding_dim是嵌入向量的维度(如768、1024等,远小于词汇表大小)。
  • 过程
    1. 输入是一个Token ID(整数),例如token_id = 1234
    2. 在Embedding矩阵中,查找第1234行。
    3. 这一行就是一个长度为embedding_dim的浮点数向量,即该Token的嵌入向量。
  • 训练:这个矩阵的参数在模型训练初期是随机初始化的。通过模型在大量文本数据上进行“完形填空”(如BERT的MLM任务)或“预测下一个词”(如GPT的LM任务)等训练,根据预测误差反向传播,不断调整这个矩阵中的每一个数字。最终,语义和语法上相关的Token,其对应的向量会在训练过程中被“推”到相近的位置。

3.3 在Node.js中模拟Embedding查找与简单运算

虽然训练一个高质量的Embedding层需要海量数据和计算资源,但我们可以用Node.js模拟其使用过程,并直观感受向量运算的魔力。

假设我们已经有一个训练好的、极小的词向量表(这里手动构造以便理解):

// 模拟一个微型的嵌入矩阵 [vocab_size=6, embedding_dim=4] // 词汇表: [“king”, “queen”, “man”, “woman”, “computer”, “data”] const embeddingMatrix = { “king”: [ 0.8, 0.2, -0.1, 0.5], “queen”: [ 0.7, 0.3, -0.2, 0.6], “man”: [ 0.1, 0.8, 0.4, -0.3], “woman”: [ 0.2, 0.9, 0.3, -0.2], “computer”:[ -0.5, -0.1, 0.9, 0.7], “data”: [ -0.6, 0.0, 0.8, 0.6] }; function getEmbedding(token) { return embeddingMatrix[token] || Array(4).fill(0); // 返回零向量处理未知词 } function vectorAdd(v1, v2) { return v1.map((val, idx) => val + v2[idx]); } function vectorSubtract(v1, v2) { return v1.map((val, idx) => val - v2[idx]); } function cosineSimilarity(v1, v2) { const dotProduct = v1.reduce((sum, val, idx) => sum + val * v2[idx], 0); const norm1 = Math.sqrt(v1.reduce((sum, val) => sum + val * val, 0)); const norm2 = Math.sqrt(v2.reduce((sum, val) => sum + val * val, 0)); return dotProduct / (norm1 * norm2); } // 1. 基础查找 console.log(“‘king’的嵌入向量:”, getEmbedding(“king”)); // 2. 经典类比运算: king - man + woman ≈ queen const resultVector = vectorAdd( vectorSubtract(getEmbedding(“king”), getEmbedding(“man”)), getEmbedding(“woman”) ); console.log(“\n计算 ‘king - man + woman’ 的结果向量:”, resultVector); // 3. 在词汇表中寻找与结果向量最相似的词 console.log(“\n寻找最相似的词:”); let bestMatch = { token: null, similarity: -Infinity }; for (const [token, vec] of Object.entries(embeddingMatrix)) { const sim = cosineSimilarity(resultVector, vec); console.log(` 与 “${token}” 的余弦相似度: ${sim.toFixed(4)}`); if (sim > bestMatch.similarity) { bestMatch = { token, similarity: sim }; } } console.log(`\n最匹配的词是: “${bestMatch.token}” (相似度: ${bestMatch.similarity.toFixed(4)})`); // 4. 计算语义相关性 console.log(“\n语义相关性对比:”); console.log(`“man” 与 “woman”: ${cosineSimilarity(getEmbedding(“man”), getEmbedding(“woman”)).toFixed(4)}`); console.log(`“king” 与 “queen”: ${cosineSimilarity(getEmbedding(“king”), getEmbedding(“queen”)).toFixed(4)}`); console.log(`“computer” 与 “data”: ${cosineSimilarity(getEmbedding(“computer”), getEmbedding(“data”)).toFixed(4)}`); console.log(`“king” 与 “computer”: ${cosineSimilarity(getEmbedding(“king”), getEmbedding(“computer”)).toFixed(4)}`);

运行这段代码,你会看到即使在这个手工构造的微型示例中,“king - man + woman”的结果向量与“queen”的向量相似度最高。同时,“man”和“woman”、“computer”和“data”之间的相似度,也远高于“king”和“computer”。这直观地展示了Embedding空间如何编码语义关系。

关键点解析:

  • 余弦相似度:是衡量向量方向相似度的常用指标,范围在[-1, 1]之间,值越大越相似。它比欧氏距离更能抵抗向量长度(范数)的影响,更适合衡量语义相似性。
  • 维度(embedding_dim)的意义:维度越高,理论上能捕获更细粒度和更复杂的语义、语法特征。但并非越高越好,过高的维度会增加计算量并可能导致过拟合。768或1024维是当前大模型的常见选择。
  • 位置编码的补充:在Transformer中,单纯的Token Embedding会丢失词序信息。因此需要额外加上Positional Encoding(位置编码),来告诉模型每个Token在序列中的位置。这才是完整的输入表示:输入 = Token Embedding + Positional Encoding

4. 完整流程串联:从原始文本到向量序列

现在,我们把Tokenization和Embedding两个环节串联起来,形成一个完整的文本预处理流水线。我们将使用一个在Node.js中可用的、相对轻量级的库来演示接近生产环境的流程。这里选择gpt-3-encoder(用于GPT-2的分词)和@tensorflow/tfjs(用于模拟Embedding查找)进行演示。

# 初始化项目并安装依赖 npm init -y npm install gpt-3-encoder @tensorflow/tfjs
const { encode, decode } = require(‘gpt-3-encoder’); const tf = require(‘@tensorflow/tfjs’); // 注意:在Node.js中运行TF.js需要安装‘@tensorflow/tfjs-node’以获得原生后端性能,此处为演示用CPU后端。 // 步骤1: Tokenization - 使用一个预训练的分词器 const text = “从Tokenization到Embedding:理解大语言模型的核心预处理步骤。”; console.log(“原始文本:”, text); const encodedTokens = encode(text); console.log(“\n1. 分词后Token IDs:”, encodedTokens); console.log(“Token数量:”, encodedTokens.length); // 我们可以解码回来看看(对于BPE,解码是无损的) const decodedText = decode(encodedTokens); console.log(“解码回文本:”, decodedText); // 应与原文本一致 // 步骤2: 模拟Embedding查找 // 假设我们有一个虚拟的嵌入矩阵。在实际模型中,这是一个从文件加载的、训练好的权重。 const vocabSize = 50257; // GPT-2的词汇表大小 const embeddingDim = 768; // 常见的嵌入维度 // 创建一个随机初始化的嵌入矩阵(模拟未训练的权重) // 注意:这里为了演示,我们只创建一个小批次所需的矩阵切片。 const embeddingMatrix = tf.randomNormal([vocabSize, embeddingDim]); // 根据Token IDs获取对应的嵌入向量 function getEmbeddingsForTokens(tokenIds) { // tokenIds 是一个普通数组,如 [123, 456, 789] // tf.gather 从embeddingMatrix中收集指定行的向量 const tokenTensor = tf.tensor1d(tokenIds, ‘int32’); const embeddings = tf.gather(embeddingMatrix, tokenTensor); return embeddings; // 形状为 [tokenIds.length, embeddingDim] } // 获取我们文本的嵌入向量 const tokenEmbeddings = getEmbeddingsForTokens(encodedTokens); console.log(“\n2. 嵌入向量形状:”, tokenEmbeddings.shape); // 应为 [token数量, 768] // 步骤3: (模拟)添加位置编码 // 这里我们使用Transformer论文中的正弦余弦位置编码公式 function getPositionalEncoding(maxLen, dModel) { const pe = tf.buffer([maxLen, dModel]); for (let pos = 0; pos < maxLen; pos++) { for (let i = 0; i < dModel; i++) { const angle = pos / Math.pow(10000, (2 * (i / 2)) / dModel); if (i % 2 === 0) { pe.set(Math.sin(angle), pos, i); } else { pe.set(Math.cos(angle), pos, i); } } } return pe.toTensor(); } const seqLen = encodedTokens.length; const positionalEncodings = getPositionalEncoding(seqLen, embeddingDim); console.log(“\n3. 位置编码形状:”, positionalEncodings.shape); // 应为 [token数量, 768] // 步骤4: 组合成最终的输入表示 // 输入 = Token Embedding + Positional Encoding const modelInput = tf.add(tokenEmbeddings, positionalEncodings); console.log(“\n4. 模型最终输入形状:”, modelInput.shape); // [token数量, 768] console.log(“第一个Token的最终输入向量(前10维):”, modelInput.slice([0,0], [1, 10]).arraySync()); // 步骤5: 清理内存(在TensorFlow.js中很重要) tokenEmbeddings.dispose(); positionalEncodings.dispose(); modelInput.dispose(); embeddingMatrix.dispose(); console.log(“\n=== 流程完成 ==="); console.log(`文本“${text}”已被成功转换为一个形状为 [${seqLen}, ${embeddingDim}] 的数值张量,可供Transformer模型处理。`);

流程详解与避坑指南:

  1. 分词器选择:我们使用了gpt-3-encoder,它封装了GPT-2的BPE分词逻辑。对于不同的模型(如BERT、T5),必须使用其对应的专用分词器(如tokenizers库提供的BertTokenizer),因为它们的词汇表和合并规则是不同的。混用会导致灾难性后果。
  2. 嵌入矩阵:在真实场景中,嵌入矩阵是模型权重的一部分,需要从预训练模型文件(如.bin.h5.safetensors)中加载。我们这里用随机数模拟。加载大矩阵时要注意内存。
  3. 位置编码:我们实现了原始Transformer的绝对位置编码。现代大模型(如GPT-3、LLaMA)可能使用更高效的旋转位置编码,其目的是让模型能更好地捕捉相对位置关系。但核心思想不变:为序列注入顺序信息。
  4. 张量内存管理:使用tf.dispose()tf.tidy()及时清理中间张量,在长时间运行或处理大批量数据时至关重要,可以避免内存泄漏。
  5. 批处理:上述流程是针对单个序列的。实际训练或推理时,需要处理一个批次(Batch)的序列。这涉及到填充(Padding)和注意力掩码(Attention Mask)来统一序列长度并忽略填充部分,这是另一个关键步骤。

5. 实战应用场景与高级话题探讨

理解了基础流程,我们来看看它在实际项目中的应用,以及一些更深入的话题。

5.1 应用场景一:精准计算API调用成本

假设你使用OpenAI的GPT-4 API,其定价是每1000个输入Token和输出Token收费一定金额。你可以利用分词器来精确计算每次请求的Token数。

const { encode } = require(‘gpt-3-encoder’); // 对于OpenAI模型,应使用`tiktoken` function estimateOpenAICost(prompt, model=‘gpt-4’, completionTokensEstimate=500) { // 注意:OpenAI实际使用`tiktoken`,此处用`gpt-3-encoder`近似演示 const promptTokens = encode(prompt).length; const totalTokens = promptTokens + completionTokensEstimate; // 假设价格(此为示例,请以OpenAI官网最新价格为准) const pricePer1K = 0.03; // 假设GPT-4输入$0.03/1K tokens const cost = (totalTokens / 1000) * pricePer1K; console.log(`提示词Token数: ${promptTokens}`); console.log(`预估总Token数: ${totalTokens}`); console.log(`预估成本: $${cost.toFixed(4)}`); return { promptTokens, totalTokens, cost }; } const myPrompt = `请将以下英文翻译成中文,并保持技术文档的严谨性: “The transformer architecture relies heavily on self-attention mechanisms to weigh the importance of different tokens in the input sequence.”`; estimateOpenAICost(myPrompt);

避坑技巧:不同模型的分词器不同。gpt-3-encoder适用于GPT-2/3,而GPT-4使用cl100k_base编码器。在Python中,使用tiktoken库是官方推荐做法。在Node.js中,可以寻找对应的移植或通过API预计算。

5.2 应用场景二:构建语义搜索系统

这是Embedding最经典的应用。核心思想:将文档库中的所有文本和查询文本都转换为向量,然后计算查询向量与所有文档向量的余弦相似度,返回最相似的文档。

// 伪代码/概念演示 class SimpleSemanticSearch { constructor() { this.documents = []; this.embeddings = null; // 假设这里存储所有文档的向量 this.model = null; // 假设这里加载了一个生成Embedding的模型(如`sentence-transformers`的MiniLM) } async addDocument(text) { this.documents.push(text); // 在实际中,这里应调用Embedding模型生成text的向量 // const embedding = await this.model.encode(text); // 并将embedding存入this.embeddings } async search(query, topK=5) { // 1. 生成查询语句的向量 // const queryEmbedding = await this.model.encode(query); // 2. 计算查询向量与所有文档向量的余弦相似度 // const similarities = this.embeddings.map(docVec => cosineSimilarity(queryEmbedding, docVec)); // 3. 按相似度排序,返回Top K的文档索引和分数 // const rankedIndices = similarities.map((sim, idx) => ({idx, sim})).sort((a,b) => b.sim - a.sim).slice(0, topK); // 4. 返回结果 // return rankedIndices.map(({idx, sim}) => ({ document: this.documents[idx], score: sim })); console.log(`语义搜索: “${query}”`); console.log(`(此处需接入真实的Embedding生成模型,如通过ONNX Runtime运行MiniLM)`); } } // 使用示例 // const searcher = new SimpleSemanticSearch(); // await searcher.addDocument(“机器学习是人工智能的一个分支。”); // await searcher.addDocument(“深度学习利用神经网络进行特征学习。”); // const results = await searcher.search(“什么是神经网络?”); // console.log(results);

实操心得

  • Embedding模型的选择:对于语义搜索,通常使用经过对比学习训练的专用模型(如all-MiniLM-L6-v2),它们生成的向量在语义相似度任务上比通用语言模型的Embedding效果更好。
  • 向量数据库:当文档数量巨大时(百万级以上),逐一遍历计算余弦相似度是不可行的。需要使用向量数据库(如Milvus, Pinecone, Weaviate, Qdrant)进行高效的近似最近邻搜索。
  • 混合搜索:将基于关键词的传统搜索(如BM25)和语义搜索结合,往往能取得更鲁棒的效果。

5.3 高级话题:分词与Embedding的局限性与前沿

  1. 分词的局限性

    • 语言偏见:BPE等基于统计的分词器,在训练语料上频率高的组合会优先合并,这可能放大语料中的社会文化偏见。
    • 上下文无关:同一个词在不同语境下可能有不同含义(如“苹果”公司 vs “苹果”水果),但标准的分词器会给出相同的Token ID。区分多义词义是后续模型(如Transformer)需要完成的任务。
    • 处理数字和代码:数字“12345”可能被切分成[“12”, “345”]或[“123”, “45”],这种任意性会影响模型对数学和代码的理解。一些专门为代码训练的模型(如Codex)会采用不同的分词策略。
  2. Embedding的演进

    • 上下文相关的Embedding:传统的Word2Vec、GloVe是静态的,每个词只有一个向量。而像BERT这样的模型,会根据上下文动态生成每个Token的表示。例如,“bank”在“river bank”和“bank account”中会有不同的向量。这被称为Contextual Embedding,是当前的主流。
    • 句子/段落级Embedding:如何将一组Token的向量聚合起来,形成一个句子或文档的单一向量表示?常用方法有:取平均、取最后一个Token的向量(在自回归模型中)、或者使用专门的[CLS]标记的向量(在BERT中)。更先进的方法如BERTScore或使用Sentence Transformers进行有监督的句子表示学习。
    • 多模态Embedding:CLIP模型将图像和文本映射到同一个向量空间,使得用文本搜索图像成为可能。这展示了Embedding思想从NLP向CV等领域的强大扩展性。

理解从Tokenization到Embedding的流程,是驾驭大语言模型的必备基础。它不再是神秘的黑盒,而是一套有章可循、可观测、可干预的数据转换管道。当你下次调用API或微调模型时,不妨想想你输入的文本正经历着怎样的“分词”与“向量化”之旅,这或许能帮你更好地理解模型的反馈,甚至设计出更巧妙的提示。在Node.js这样的环境中亲手实现简化版,是固化这一理解的最佳方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询