尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解大语言模型:语言模型原理、Transformer架构与部署实战

深入理解大语言模型:语言模型原理、Transformer架构与部署实战 聊大语言模型之前有个问题绕不开机器凭什么能“懂”人话这不是把一本词典塞给它就能解决的事而是要让它在海量文本中自己摸出语法、语义和上下文的规律。我见过太多新手一上来就pip install transformers跑通一个 demo 就以为懂了大模型结果遇到报错、显存溢出、生成质量差完全不知道从哪下手。根源就是没把底层的语言模型和 Transformer 架构吃透。这篇文章相当于把“第三章 大语言模型基础”里面的核心章节重新拆开揉碎讲一遍内容包括语言模型到底在优化什么、Transformer 为什么是当前大模型的骨架、从预训练到大规模部署之间到底发生了什么以及你下载一个开源模型之后怎么在本地跑起来、遇到问题怎么排错。适合刚接触 NLP 的读者也适合那些已经能熟练调用 API、但一直没时间把底层架构补齐的工程师。我会把关键公式、形状变化、部署配置都往上摆争取让你看完之后既能理解原理也能直接上手实操。1. 语言模型到底在学什么1.1 语言模型要解决的直觉问题语言模型的任务如果只用一句话概括就是给定一串前文预测下一个最可能出现的词。听起来简单但几乎所有大模型的“智能”都从这个朴素目标里长出来。你输入“今天天气真”模型会在词表上算出每个词出现的概率然后大概率给你“好”、“不错”、“热”这些候选词。这个过程的数学形式其实很干净。对于一个句子 ( w_1, w_2, ..., w_n )语言模型要估计整个句子的概率[ P(w_1, w_2, ..., w_n) \prod_{t1}^{n} P(w_t | w_1, w_2, ..., w_{t-1}) ]也就是说把联合概率拆成了逐词条件概率的乘积。每一步都在做“根据历史预测当前词”这个动作。你平时用输入法打字手机键盘给出的下一个词联想就是一个小型语言模型在背后计算概率分布。但这里有个很实际的问题条件概率 ( P(w_t | w_1, ..., w_{t-1}) ) 的维度会随着历史长度爆炸式增长。你不能把每一种前文组合都单独存一个概率值现实世界根本没有这么多数据去支撑这种统计。所以语言模型的核心矛盾就是用什么样的结构去近似这个条件概率才能在有限数据和有限算力下尽可能逼近真实分布。1.2 从 N-gram 到神经网络语言模型的转变最早被广泛使用的统计语言模型是 N-gram。它做了一个“马尔可夫假设”当前词只依赖于前面 N-1 个词而不是完整历史。比如三元模型Tri-gram假设 ( P(w_t | w_{t-1}, w_{t-2}) )这样概率表就变成一个“前两个词 - 下一个词”的三维计数表大小可管理。N-gram 的问题也很明显。第一N 不能设太大否则组合数量指数增长很多长度为 4 或 5 的词组在语料里根本没出现过概率就是 0。为了解决零概率问题还得引入平滑策略比如加一平滑、Kneser-Ney 平滑代码量还不小。第二它完全丢掉了远距离依赖比如“我昨天去了上海今天又回到了___”这种句子真正决定答案“北京”的关键信息在 10 个词之前N-gram 根本够不到。神经网络语言模型的出现改变了这个局面。Bengio 在 2003 年提出的经典作品把每个词映射成一个低维稠密向量Embedding然后用一个前馈神经网络去拟合条件概率。这样做的好处是词与词之间可以通过向量距离衡量相似度“猫”和“狗”虽然字面不同但向量相近模型在预测时能把这种相似性利用起来不需要每个组合都亲眼见过。这就是“分布式表示”的核心思想用低维向量的组合来编码无限多的语义状态。再往后RNN 和 LSTM 进一步把序列建模能力做上来。它们用隐藏状态 ( h_t ) 逐步累积历史信息理论上可以建模任意长度的依赖。但实际操作中RNN 存在两个硬伤。一是难以并行每个时间步都依赖前一步的隐藏状态GPU 的大规模并行优势发挥不出来二是长距离梯度传播容易消失即使 LSTM 加了门控机制能缓解但仍不能从根本上解决。这两个硬伤恰好是 Transformer 登场的直接原因。1.3 语言模型的分类和评价指标如果说“语言模型”是一个大类那么按训练目标和结构可以分成几个常见流派自回归模型预测下一个词也就是前面说的 ( P(w_t | w_{t}) )。GPT 系列就是典型代表。自编码模型通过遮掩输入中的部分词来预测被遮住的词比如 BERT 的 Masked Language Model。它不是按从左到右的顺序建模而是同时利用左右两侧上下文。序列到序列模型输入一个序列输出另一个序列。最早用于机器翻译典型代表是 T5 和 BART。这三类各有适用场景。自回归模型生成质量好适合对话、写作、代码生成自编码模型适合文本分类、实体抽取、句子相似度等判别型任务序列到序列模型则适合翻译、摘要这类输入输出都是序列的任务。现在大家说“大语言模型”默认指自回归路线。那怎么衡量一个语言模型学得好不好呢最常用的指标是困惑度PerplexityPPL本质是交叉熵的指数形式[ PPL \exp(-\frac{1}{N}\sum_{t1}^{N} \log P(w_t | w_{t})) ]直觉上困惑度可以理解成“模型在每一步平均犹豫多少个候选词”。如果 PPL 是 10意味着模型平均在 10 个词之间摇摆如果是 2那基本已经锁定答案了。新手容易犯的错是只看 loss 下降就觉得模型变好了其实在相同语料下比较 PPL 才有意义不同词表大小、不同领域的 PPL 不能直接对比。2. Transformer 架构的核心设计2.1 为什么 RNN 被替换注意力机制到底解决了什么2017 年Google 那篇“Attention Is All You Need”直接把 RNN 结构换掉了。核心思想很简单既然 RNN 靠逐步传递状态来积累信息会有各种问题那我干脆一步到位让每个词直接和序列里所有词两两交互计算它们之间的相关性再按相关性加权融合信息。这就是 Self-Attention也就是自注意力机制。自注意力最大的收益是三个一是全局依赖建模任意两个位置的词都能直接交互距离不再是问题二是可以并行计算因为每个位置的 Query 都可以同时去关注其他位置矩阵运算天然适合 GPU三是信息传递路径短不像 RNN 那样需要沿时间步一步一步走梯度在深层网络中的传播也更容易控制。2.2 Self-Attention 的计算过程拆解Self-Attention 的计算分四步走。假设输入序列是 ( X )形状为 ( [batch, seq_len, d_model] )。第一步通过三个可学习矩阵生成 QQuery、KKey、VValue。这里有个非常形象的类比把模型看作一个检索系统。你有问题Query去数据库里匹配相似度匹配依据是索引Key匹配到之后不直接用索引本身而是取出对应的信息内容Value。第二步计算 Q 和 K 的点积相似度再除以 ( \sqrt{d_k} ) 做缩放。为什么要除以根号 d_k因为当向量维度很高时点积的值会变得很大softmax 会被推到梯度很小的区域除以缩放因子后数值分布更稳定梯度也能保持住。第三步对相似度做 softmax 归一化得到注意力权重。第四步用权重对所有位置的 V 做加权求和得到当前词的输出。用 PyTorch 写一个最简洁的版本大概是import torch import torch.nn.functional as F def self_attention(q, k, v, maskNone): # q/k/v: [batch, seq_len, d_k] scores torch.matmul(q, k.transpose(-2, -1)) / (k.size(-1) ** 0.5) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights F.softmax(scores, dim-1) output torch.matmul(weights, v) return output注意这里省略了输入投影层和注意力头的拆分只是把最核心的公式展示出来。实际框架里还会包一层输出投影把多个头的输出拼接后线性变换回 ( d_model ) 维度。2.3 多头注意力一个注意力头不够用那就上多个单一注意力头的问题在于它只能学一种相关性模式。但语言的复杂性很明显一个词可能同时承担语法角色、语义指向、指代关系等多种角色。举个例子“小明说小华来了他很高兴”这里的“他”既可能指小明也可能指小华单一注意力头很难同时捕捉两种可能性。多头注意力Multi-Head Attention的做法是把 Q、K、V 分别映射到多个子空间每个子空间独立做注意力计算最后把所有头的输出拼起来再过一次线性层。每个头就相当于一组“专家”各管一摊。有的头可能偏向捕捉位置邻近关系有的头偏向捕捉句法依赖有的头偏向捕捉指代关系。流程拆开看就是输入 ( X ) 通过不同的权重矩阵 ( W_i^Q, W_i^K, W_i^V )将 d_model 维向量切分成 h 组每组维度 ( d_k d_model / h )。每个头单独计算 Self-Attention得到 ( head_i )。将 h 个头的结果拼接起来维度恢复成 ( [batch, seq_len, d_model] )。经过一个输出投影矩阵 ( W^O ) 得到最终结果。头数 h 是经验值GPT-2 里是 12 个头GPT-3 是 96 个头。头过多会导致每个子空间维度太小表达能力反而下降头太少则难以覆盖不同的关注模式。自己搭模型的时候一般保证 ( d_model ) 能被头数整除并且 ( d_k ) 不要小于 32 左右否则训练不稳定。2.4 位置编码给 Transformer 补上“顺序感”Self-Attention 有个天然缺陷——它是排列不变的。你把“我爱你”三个词向量输入进去把顺序换成“你爱我”如果不做额外处理注意力计算的结果完全一样。这显然不符合语言习惯因为语言里顺序往往意味着语义差异。为了让模型感知词序Transformer 在输入向量上叠加位置编码Positional Encoding。最经典的方案是使用不同频率的正弦和余弦函数[ PE_{(pos, 2i)} \sin(pos / 10000^{2i/d_{model}}) ] [ PE_{(pos, 2i1)} \cos(pos / 10000^{2i/d_{model}}) ]其中 ( pos ) 是词在序列中的绝对位置( i ) 是向量的维度下标。这样设计的好处是低维用高频变化可以区分相邻位置高维用低频变化可以覆盖长距离而且任意位置的编码向量之间可以通过线性变换建立相对位置关系的近似表达。实际实现中可以直接预计算一个固定大小的位置编码矩阵使用的时候按需切片。一个常见的小坑是最大长度限制比如训练时设置了max_position_embeddings2048推理时输入长度超过 2048 就会直接报错。所以很多新模型已经改用旋转位置编码或者 ALiBi来扩展长度外推能力但三角函数的编码思路仍然是理解位置信息的基础。2.5 残差连接、层归一化和前馈网络Transformer 的每一层不只是注意力模块。经典的 Block 结构是输入先经过多头注意力然后接残差连接和层归一化再经过一个两层的全连接前馈网络再做一次残差连接和层归一化。完整的计算流如下[ X X MultiHeadAttention(LayerNorm(X)) ] [ X X FFN(LayerNorm(X)) ]残差连接解决的是深层网络训练稳定性问题。没有残差连接几十层 Transformer 的梯度很容易在反向传播中消失或爆炸。加了残差之后即使深层网络每一层的信号变化都很大梯度也有一条快速通道回流到浅层。层归一化LayerNorm的作用是把特征分布拉回标准范围让每一层的输入不要偏离过大。和 BatchNorm 不同LayerNorm 是对每个样本的所有特征维度做归一化不依赖 batch 大小所以在变长序列输入时表现更稳定。需要注意的是Transformer 里有两种常见排布一是 Post-LN也就是原始论文里先注意力后归一化二是 Pre-LN也就是先归一化再注意力。现在开源大模型基本都用 Pre-LN原因是后者训练更稳定允许使用更大的学习率。前馈网络则是每个 token 独立进行两层的非线性变换通常是先升维再降维比如把 d_model 从 768 升到 3072 再降回来。这一步是模型非线性表达能力的主要来源因为注意力本身是线性加权求和缺少非线性堆多少层都等价于一层线性变换。3. 从语言模型到大语言模型的跃迁3.1 预训练加微调从“通识教育”到“专业训练”有了 Transformer 结构之后模型可以被训练得很大但光靠某一个特定任务的数据去训练根本喂不饱这么大的模型也容易严重过拟合。所以业界慢慢形成了“预训练 微调”的两阶段范式。预训练阶段模型在海量无标注文本上做自监督学习目标函数就是前面说的语言模型目标——给定前文预测下一个词。这个阶段可以理解成通识教育模型在数十亿甚至数万亿 token 上学习语法、事实知识、推理模式、代码逻辑。OpenAI 在训练 GPT 系列时用的语料几乎是互联网上能爬到的公开文本总和。微调阶段模型在特定任务的数据上进一步训练。比如要做客服问答就准备一批“用户提问 - 客服回答”的对话数据要做代码补全就准备大量代码文件。通过微调模型学会把预训练阶段学到的基础能力适配到具体场景。更进一步的方案是监督微调 人类反馈强化学习也就是 RLHF让模型输出更符合人类偏好。这里要提醒新手一个概念。很多人分不清“预训练语言模型”和“大语言模型”其实前者包含后者预训练语言模型是统一叫法大语言模型强调的是规模足够大之后出现了一些特殊能力。单纯增大规模、增加数据并不是所有问题都会自动解决但我们确实观察到一些规律性的现象。3.2 模型结构的三种图谱Encoder-only、Decoder-only、Encoder-DecoderTransformer 原始论文是一个 Encoder-Decoder 结构Encoder 负责理解输入Decoder 负责生成输出。但到了后续应用研究者发现可以根据任务类型对结构做裁剪于是形成了三个主流流派。Encoder-only代表是 BERT。它对输入做双向编码每一层的注意力可以同时看到所有词因此非常适合文本分类、实体识别、语义相似度这类“理解型”任务。缺点是生成能力弱因为训练时用的就是掩码预测没有显式建模生成顺序。Decoder-only代表是 GPT 系列。严格自回归每个 token 只能看到左侧的 token。训练目标和生成目标高度一致都是“给定前文预测下一个词”所以工程上实现简单统一而且扩展性极好。当前主流大语言模型几乎都走这条路。Encoder-Decoder代表是 T5 和 BART。适合机器翻译、文本摘要这类对输入和输出要求都很高的任务。但参数量比 Decoder-only 更大训练调度也更复杂所以在大模型时代反而没有成为绝对主流。为什么最后大家都选择了 Decoder-only我个人的理解是自回归目标够简单、够统一。预训练、微调、推理都是同一个目标函数不需要额外的任务适配层而且 Decoder-only 在零样本和少样本情境下的表现已经足够好。事实证明只要数据和算力堆到一定规模Decoder-only 就能涌现出指令跟随、思维链等复杂能力这比结构上的精巧设计更值钱。3.3 规模定律与涌现能力关于大模型为什么“大”了之后就变聪明有两个绕不开的观测。一是规模定律Scaling Law它说的是在算力、数据量、参数量三者之间存在一个幂律关系在三者都同步提升的前提下模型 loss 会持续稳步下降并不会在某个规模完全饱和。换句话说模型越大、数据越多性能越好远还没到天花板。二是涌现能力。简单说某些能力在小模型上完全不存在但当模型规模跨过某个阈值后突然出现。典型例子是上下文学习也就是 in-context learning。你给模型几条示例它不需要更新参数就能模仿示例完成任务再比如思维链你要求模型“一步一步想”它在复杂推理问题上的准确率会跳跃式提升。这些能力在几十亿参数的小模型上表现很弱但到了千亿参数级别就变得非常明显。不过规模效应不是单纯刷参数量。数据质量、数据多样性和去重同样关键。如果语料里大量重复或者只有单一领域模型能学到的知识面就很窄。现在很多开源模型的训练报告都把“数据配比”和“清洗流程”当成核心秘密因为数据上的差距直接决定了最终模型效果的上限。3.4 视觉大语言模型和多模态方向大语言模型的思路也在往其他模态迁移。视觉大语言模型就是把图像编码器接入语言模型让模型既能理解图片也能用自然语言和你对话。常见的做法是使用 CLIP 或 SigLIP 这类视觉编码器把图片转换成特征序列再通过投影层对齐到语言模型的输入空间然后继续用自回归目标去训练。这类模型在图片问答、文档理解、截图转代码等场景已经很成熟。医疗影像辅助、工业质检、老照片描述这类垂直领域也在逐渐尝试用视觉大语言模型打底做定制微调。技术原理本质没变核心还是语言模型那套损失函数和 Transformer 结构只是输入被换成了图像特征的序列。理解了语言模型基础再往上叠多模态模块思路就顺了。4. 动手实践模型下载、本地部署和调用4.1 模型下载下来到底是什么文件很多第一次接触开源模型的人会问从网上下载一个模型得到的是不是一个 .exe双击就能跑并不是。一个开源大模型通常是一堆权重文件加配置文件最基本的结构包括model-00001-of-000XX.safetensors或pytorch_model.bin模型权重保存神经网络每一层的张量数值。文件较大时会被切分成多个分片。config.json模型的超参数比如层数num_hidden_layers、注意力头数num_attention_heads、隐藏层维度hidden_size、词表大小vocab_size。tokenizer.json/tokenizer.model文本和 token id 之间映射的词典包含分词规则。generation_config.json生成参数配置比如温度、top_p、最大生成长度、结束符设置。可能还有preprocessor_config.json通常在视觉模型或带特殊预处理流程的模型中存在。权重文件的大小取决于模型参数和存储精度。一个 70 亿参数的模型用 FP162 字节存储每个参数占 2 字节总共大约是 14GB。如果量化到 INT40.5 字节大约 3.5GB所以能看到 7B 模型的量化包只有 4GB 左右。量化会带来精度损失但可以显著降低显存和内存需求是本地部署的关键手段。4.2 本地部署的主流方式和显存估算本地部署大模型的路线现在很成熟常见的三个方向是Transformers PyTorch直接加载原始权重适合做研究和微调但资源占用较高。llama.cpp / GGUF将模型量化成 GGUF 格式偏 CPU 和低显存场景支持 GPU 多层的混合推理。Ollama / vLLMOllama 更偏个人电脑快速体验vLLM 更偏生产级高并发推理。以 Ollama 为例一条命令就能拉取模型ollama pull qwen2.5:7b拉取完以后直接进入交互式对话ollama run qwen2.5:7b如果是用 Transformers 加载伪代码大概是from transformers import AutoModelForCausalLM, AutoTokenizer model_name Qwen/Qwen2.5-7B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name, device_mapauto)显存怎么估算规则很简单显存需求 参数量 × 每参数字节数再额外加一点中间激活值和 KV Cache 的开销。以 7B 模型为例FP16 权重约 14GB加上激活和缓存一般需要 16GB 显存才能流畅运行。如果只有 8GB 显卡就选 4BIT 量化版比如 Q4_K_M 这类 GGUF部署起来更实际。4.3 远程 API 调用和代理地址怎么配置本地部署要自己管显存和算力很多人图省事就直接调云端 API。那么“大语言模型代理地址怎么填”这个高频问题就来了。先明确一个概念这里的代理是指通过一个中转地址去访问大模型服务而不是指其他特殊用途。常见场景有两种。第一种是使用 OpenAI 兼容的 API 服务。很多开源模型部署平台都提供了兼容 OpenAI 格式的接口你只需要把base_url替换对应平台地址即可。例如from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttp://localhost:8000/v1, ) resp client.chat.completions.create( modellocal-model-name, messages[{role: user, content: 你好}], ) print(resp.choices[0].message.content)这里base_url就是 API 的基础路由地址后面要带/v1。如果你的推理服务部署在公司内网或者需要经过企业网关才能访问外部的云 API那还需要给 HTTP 客户端配置网络代理。这个代理是公司 IT 环境里合规的企业代理而不是什么特殊工具。配置方式是在请求客户端里设置环境变量或者直接指定代理地址import os os.environ[HTTP_PROXY] http://proxy.company.com:8080 os.environ[HTTPS_PROXY] http://proxy.company.com:8080用requests库时也可以直接传入proxies参数。如果是在 Docker 容器里部署模型服务还需要注意容器网桥和宿主机端口的连通性常见的错误是API 地址写成了 localhost但服务实际跑在另一个容器里这种情况要把地址改成对应的服务名或者宿主机 IP。4.4 图形界面和统一管理命令行式的 Chat 界面日常验证够用但团队内部想给非技术同事用还是需要一个 Web 界面。比较成熟的开源方案是 Open WebUI前身叫 Ollama WebUI支持对接 Ollama、OpenAI API 以及其他 OpenAI 兼容服务。最省事的安装方式是 Dockerdocker run -d -p 3000:8080 \ --name open-webui \ -e OPENAI_API_BASE_URLhttp://host.docker.internal:8000/v1 \ -v open-webui:/app/backend/data \ ghcr.io/open-webui/open-webui:main打开浏览器访问http://localhost:3000注册一个管理员账号就能进入一个类似 ChatGPT 的界面支持多会话管理、知识库上传、模型切换等功能。对团队内部直接拿来当统一模型入口非常方便。注意 Docker 容器里访问宿主机服务要用host.docker.internal不要写localhost。5. 常见问题与排查技巧实录5.1 高频拦路问题速查表跑模型这件事原理搞懂了实操里还是会碰到各种玄学报错。我把这几年踩过的坑整理成一张速查表遇到问题直接对照着看。现象可能原因处理办法加载模型时提示“Killed”内存不足进程被系统杀掉换更小模型或使用量化版本关闭其他大内存应用提示CUDA out of memory显存不够权重和激活超过显存容量使用低比特量化减小max_seq_len启用梯度检查点生成中文内容乱码分词器加载错误或模型权重不完整检查 tokenizer 路径是否正确下载完整分片重新加载请求 API 长时间无响应base_url 或代理配置错误端口不通先用curl验证地址能否返回结果再检查代理设置输出一直重复一句话温度过低或 repetition penalty 未开启调高 temperature 到 0.7 以上设置 repetition_penalty 1.1 左右上下文稍长就报错位置编码最大长度限制改用支持长度外推的模型或改用旋转位置编码版本微调时 loss 不降学习率太大或批次过小优化器参数不合适降低学习率增大 batch size检查数据预处理是否有标签错位5.2 我踩过几次坑之后的实操心得第一个建议是不要一上来就挑战 70B 模型。部署一个大模型的工程复杂度远不止显存一件事还有加载时间、推理速度、并发处理、上下文长度限制。先拿 1.5B 到 8B 的模型把整个流程跑通再根据效果和性能决定要不要换更大的。第二个建议是先用命令验证 API 地址再写代码。很多人写完代码报连接错误排查半天发现是服务根本没启动或者端口写错。先用curl简单测一下curl http://localhost:8000/v1/models如果返回 JSON 列表说明服务正常返回连接拒绝就去查服务日志别一上来改代码。第三个建议和模型文件有关。下载大模型时如果看到一堆.safetensors分片文件一定要把索引文件如model.safetensors.index.json一起下载不能只下载一部分权重。部分平台的下载工具容易漏文件结果加载时报某个 tensor 不存在这种问题很难一眼看出来先检查文件完整性。第四个建议是善用transformers的device_mapauto参数它会把不同层自动分配到 GPU 和 CPU 上避免单卡显存溢出。如果还想提速再配合torch.compile、FlashAttention 和bf16精度速度差距非常明显。5.3 从原理到实践的收尾心得最后分享一个我自己的经验。每次拿到一个新模型不管对方宣传多强我都先做三个固定动作第一看一眼 config.json确认层数、头数、词表大小和位置编码方式第二用一个小批量文本跑一次前向打印每一步张量的形状确保和预期维度一致第三写一个最简单的生成测试输入一句中文观察输出质量和首 token 延迟。这套固定动作帮我排掉了大量“看似奇怪”的问题。因为很多报错其实不是玄学而是维度和配置对不上。比如位置编码方式不一致、词表大小不匹配、模型头数设置错误这些都能从 config.json 里看出来。等你把 Transformer 的每一层形状都烂熟于心再回头看框架的报错日志基本一眼就能定位问题在哪一层。这也正是我为什么一直建议新人先啃架构、再上实操。大语言模型的外围工具链更新很快今天流行的部署框架半年后可能就换了一批但 Transformer 的 QKV、注意力公式、位置编码、残差连接这些底层逻辑一直没变过。把不变的东西学扎实再跟着变的东西走你会走得更稳。
返回列表