从原理图解到 HuggingFace Transformers 实战
1. Transformer 极简原理大模型是怎么思考的大模型和普通的机器学习模型一样本质也是一个函数不同的是传统机器学习可能输入的是一些整理好的数据比如房子的尺寸地段购买时长和市中心的距离等等数据输出一个预测的放假而大模型输入的是我们的问题拿到的是大模型给出的回答。但咱们究其根本它们都可以看作是一个函数我们输入一些东西经过运算之后输出给我们一些东西。对于大模型它的本质就是一个下一个字符预测器我们输入一些文字它只负责根据它所训练的海量数据输出最符合最有可能的下一个字符就像一个文字接龙机器其核心任务只有一个根据上文猜下一个字是什么。为了实现这个目标Transformer 架构经历了一个精密的流水线下面我们会稍微详细一点地介绍一下各个步骤。1.1 第一步Token 化 (Tokenization) —— 查字典大模型既然是一个函数那么肯定是针对数字进行处理的所以我们就需要一个法子去将我们的文字字符甚至是图片变成大模型认识的数字或者说张量向量。 这就是Token化去做这一步操作的函数或者模块就是分词器Tokenizer。那么Tokenizer具体是如何工作的它实际上就是将一个个的字符对应成一个个数字或者说ID就像ASCII码一样不过它做得更高级。把每一个字都找一个数字对应有点奢侈特别是对于英语,act, acting,action,actor其实都有相同的词根主要的语义来源于其词根act所以分词器是按照词元token去拆分的能把有些词拆成词根、前缀和后缀等等当然具体如何拆取决于字典如何定义字典有多大。这里我们给一个简单的例子输入“我爱 AI”动作切分 -[我, 爱, AI]- 查表 -[2301, 452, 1083]1.2 第二步Embedding 位置编码 —— 赋予含义与顺序有了token之后我们想知道词和词的关系我们想要通过一个可以量化的量去判断两个词是否是有关系的关系多大。这就引入了我们的下一个模块词嵌入模块Embedding。Embedding (词向量)把每个 ID 变成一个长长的向量比如 4096 维的数组。这个向量在模型训练之前是随机的其后随着海量的训练数据洗礼越相关的词向量越靠近越不相关的词越远离。这个向量代表了词的含义。比如“猫”和“狗”的向量在空间里距离很近“苹果”和“手机”在某种语境下也更近。光知道词和词的关系还不够“我爱你”和“你爱我”的每一个词都是相同的但是它们确实可以不相关的两个句子“小狗咬了我”和“我咬了小狗”也会被人视作“可以正常理解”和“这人好像不对劲”两种完全不同的理解。显而易见词语在句中的位置是一个非常重要的信息我们不能弄丢它也需要将这一部分信息传递给模型训练时候去学习。Positional Encoding (位置编码)Transformer 是并行计算的它一眼看完所有词这导致它不知道“我爱你”和“你爱我”的区别。所以我们需要给每个词贴上一个“座位号”告诉模型谁在前面谁在后面。1.3 第三步Self-Attention (自注意力) —— 寻找关系接下来就是大模型的灵魂我们想知道一句话里的每一个词元和其他词元的关联度其实就是上下文的联系。当模型处理“苹果”这个词时如果上下文里有“手机”、“发布会”注意力机制会告诉模型“嘿这里的‘苹果’指的是科技公司不是水果”,自注意力机制让模型能理解上下文。1.4 第四步MLP (前馈神经网络) —— 消化吸收如果说注意力机制是“看”那么 MLP 就是“想”。它包含多层神经元负责对提取到的信息进行复杂的非线性变换和逻辑推理,也就是传统的深度学习。1.5 第五步Decoder Softmax —— 输出概率经过层层计算模型最终会输出一个包含了所有词汇比如 15 万个词的概率列表。AI(80%)吃(10%)睡(5%)...最后我们根据这个概率列表选择下一个词。这就是大模型词语接龙的原理了。2. 拆解模型文件夹下载下来的到底是什么理论讲完了我们来看看在Kaggle的文件系统里这些理论变成了什么文件。当你下载一个模型以 Qwen2.5-7B 为例时文件夹结构如下2.1 核心架构config.jsonconfig.json: 是大模型的身份证也可以说是体检表它其中就是真正的我们的模型具体由哪些层构成是什么架构类型隐藏层有多深注意力头的数量有几个词表的大小是多少。对于大模型而言它就是模型本身也是骨架因为大模型重要的是训练完的参数模型本身是很小的。2.2 行为预设generation_config.jsongeneration_config.json:是模型的出厂默认设置。2.3 大脑本身*.safetensors和*.index.json有了config.json中的躯体我们再从.safetensors和.index.json载入灵魂这才是我们能说会道的大模型。model-xxxxx.safetensors这里面存的是实打实的张量Tensor数据即数十亿个参数的浮点数。为了方便存储和加载通常会被切分成多个 2GB-5GB 的小文件Shard。model.safetensors.index.json这是一张藏宝图。因为权重被切分了模型需要知道“第 5 层的权重”到底藏在哪个文件里。内部长这样{ metadata: { total_size: 15423653888 }, weight_map: { model.layers.0.self_attn.q_proj.weight: model-00001-of-00004.safetensors, model.layers.20.mlp.gate_proj.weight: model-00003-of-00004.safetensors } }数字和文字的翻译官tokenizer相关文件vocab.json/merges.txt这是最原始的生词表。记录了所有字、词根对应的 ID。tokenizer.json这是一个编译后的高效字典文件包含了分词的所有逻辑Pre-tokenization, Normalization 等加载速度比读原始文本快得多。tokenizer_config.json(至关重要)这是分词器的配置文件。它定义了特殊符号Special Tokens比如哪个 ID 代表“开始”哪个代表“结束”。它包含了Chat Template (聊天模板)这是一段 Jinja2 代码决定了apply_chat_template如何工作。内部长这样{ chat_template: {% for message in messages %}...|im_start|..., eos_token: |im_end|, pad_token: |endoftext| }3. Transformers库实战代码中的“铁三角”在Transforms库中我们永远绕不开三个核心类。环境准备在 Kaggle 右侧设置中确保Internet: On且Accelerator: GPU T4 x2。!pip install -U transformers accelerate bitsandbytes3.1 AutoTokenizer (翻译官)对应理论中的Token 化步骤,和模型文件中的tokenizer相关文件。from transformers import AutoTokenizer model_path /kaggle/input/qwen2.5/transformers/7b-instruct/1/ # 加载分词器 tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) text Transformer is amazing # 1. 编码 (Encode): 文本 - 数字 ID input_ids tokenizer.encode(text) print(f原文: {text}) print(f数字 ID: {input_ids}) # 2. 解码 (Decode): 数字 ID - 文本 decoded_text tokenizer.decode(input_ids) print(f还原: {decoded_text})得到的结果将会是这样原文: Transformer is amazing 数字 ID: [46358, 374, 7897] 还原: Transformer is amazing对话格式Chat Template大模型需要特定的对话格式Prompt。来将模型的回答和用户的问题做区分, 我们一般都可以通过载入语言模型对应模板不同家的模型可能模板会有不同甚至去拼装历史记录。messages [ {role: system, content: 你是一个物理学家。}, {role: user, content: 用一句话解释相对论。} ] # 自动应用聊天模板 formatted_prompt tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) print(模型实际看到的输入:\n, formatted_prompt)运行结果如下模型实际看到的输入: |im_start|system 你是一个物理学家。|im_end| |im_start|user 用一句话解释相对论。|im_end| |im_start|assistant看到这里我们也能更好地理解为什么一个本质是词语接龙的模型能够区分问题然后做出回答。因为在训练的过程中我们会让他知道|im_start表示一个message的开始其中会告诉模型这句话是谁说的这句话在什么位置结束它接龙的时候也会带上开始和结束符号在推理模型中甚至会带上思考的标签。3.2 AutoModel (大脑本体)对应理论中的Embedding - Attention - MLP计算过程,通过从config.json中加载模型躯体在加载上模型的safetensor灵魂数据以及generation_config.json的默认初始化,在 Kaggle 上我们拥有双 T4 (15GB x 2)一定要利用device_mapauto让库自动分配显存。from transformers import AutoModelForCausalLM import torch # 加载模型 model AutoModelForCausalLM.from_pretrained( model_path, device_mapauto, # 关键自动将模型切分到两张 T4 显卡上 torch_dtypetorch.float16, # 使用半精度节省显存 trust_remote_codeTrue ) print(f模型加载成功显存分布: {model.hf_device_map})输出结果为模型加载成功显存分布: {model.embed_tokens: 0, model.layers.0: 0, model.layers.1: 0, model.layers.2: 0, model.layers.3: 0, model.layers.4: 0, model.layers.5: 0, model.layers.6: 0, model.layers.7: 0, model.layers.8: 0, model.layers.9: 0, model.layers.10: 0, model.layers.11: 0, model.layers.12: 1, model.layers.13: 1, model.layers.14: 1, model.layers.15: 1, model.layers.16: 1, model.layers.17: 1, model.layers.18: 1, model.layers.19: 1, model.layers.20: 1, model.layers.21: 1, model.layers.22: 1, model.layers.23: 1, model.layers.24: 1, model.layers.25: 1, model.layers.26: 1, model.layers.27: 1, model.norm: 1, model.rotary_emb: 1, lm_head: 1}4. 掌控生成的“调节旋钮”在模型输出的过程中我们有一些参数可以对输出结果进行调节对应于我们讲理论部分的中的第五步 (Softmax 概率输出), 我们有一堆下一个词元的概率分布了但是我们应该如何去选择呢4.1 Temperature (温度)我们可以设定的Temperature参数我们在generation_config.json中也看见过它值越大更热更具创造性更容易输出各种天马行空的词, 会缩小所有词元的差距雨露均沾以达到创造性让低概率的词也有机会被选中当然,也更容易胡说八道出现幻觉.值越小更冷更严肃在温度为0的时候甚至会固定输出最高的那个词它会拉大高概率和低概率的差距赢家通吃让模型的回答更稳定严谨。4.2 Top_p (核采样)和温度不同我们还有另一种方式这种方式更类似于拉网我们只要可能性前80%的词在那些词里进行挑选这个可能性就是P这个选词采样方法又叫top_p(核采样).简而言之其只在累积概率达到 P (e.g., 0.9) 的前几个词里选。直接切掉尾部那些极低概率的离谱词。4.3 实验一下我们这里先定义一个函数以温度和top_p为参数去测试不同的参数对回答的影响# 定义一个测试函数 def test_generation(temp, top_p, prompt_text): messages [ {role: system, content: 你是一个前卫的科幻小说家。}, {role: user, content: prompt_text} ] text tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) inputs tokenizer([text], return_tensorspt).to(model.device) print(f\n 设置: Temperature{temp}, Top_p{top_p} ) try: generated_ids model.generate( **inputs, max_new_tokens100, # 限制长度方便快速看结果 temperaturetemp, top_ptop_p, do_sampleTrue, # 必须开启采样 pad_token_idtokenizer.eos_token_id ) response tokenizer.batch_decode(generated_ids, skip_special_tokensTrue)[0] # 只打印回答部分 print(response.split(assistant)[-1].strip()) except Exception as e: print(f生成出错: {e})温度实验然后再其下新建code block去测试看看效果prompt 请用这三个词写一个微故事量子、失恋、炒饭。 # 1. 低温模式 (0.1)严谨、死板 test_generation(temp0.1, top_p0.9, prompt_textprompt) # 2. 适中模式 (0.7)正常、流畅 test_generation(temp0.7, top_p0.9, prompt_textprompt) # 3. 高温模式 (1.5)疯狂、混乱 # 注意可能会输出乱码或完全不通顺的句子 test_generation(temp1.5, top_p0.9, prompt_textprompt)这是我的运行结果 设置: Temperature0.1, Top_p0.9 在量子世界里时间与空间的概念变得模糊不清而李明的世界也因为一段失败的恋情而变得一片混沌。他尝试着用量子纠缠理论来修复自己破碎的心灵却意外地将自己从现实世界送入了一个平行宇宙。 在这个平行宇宙中李明发现了一家特别的餐馆这里的厨师是一位曾经的恋人她正在为一位顾客准备一道特别的炒饭。这道炒饭不仅色香味俱全还 设置: Temperature0.7, Top_p0.9 在量子世界的边缘李明独自一人坐在一家不起眼的小餐馆里面前是一碗普通的炒饭。他和女朋友分手的原因是因为她沉迷于虚拟现实中的量子世界而他却对现实世界充满了留恋。 就在几天前他们最后一次争吵后她告诉他“我找到了真正的自我——一个穿梭在量子世界的探索者。”她留下了一盘未吃完的炒饭然后消失在了虚拟现实中。 李明望着那盘炒饭 设置: Temperature1.5, Top_p0.9 标题时空泡饭 李明最近失恋了每天只能煮一大锅泡饭来消磨时间。 这天李明正在做饭他忽然接收到一条量子信号。他惊奇地发现那是自己失恋前女友的坐标位置。想到能与自己相爱过的人共度时光是多么美妙的事情他便将自己煮了一锅泡饭送进了坐标传送器。 结果却是一锅炒饭。 李明百思不解。top_p实验接下来是top_pprompt_2 请给一种不存在的颜色起个名字并描述它的样子。 # 1. 极窄采样 (0.01)只选概率最高的那个词近似贪婪搜索 test_generation(temp0.8, top_p0.01, prompt_textprompt_2) # 2. 宽广采样 (0.95)允许罕见词出现 test_generation(temp0.8, top_p0.95, prompt_textprompt_2)输出结果为 设置: Temperature0.8, Top_p0.01 这种不存在的颜色我命名为“星尘紫”。它是一种梦幻般的颜色介于紫色和银色之间仿佛是宇宙中无数微小的星辰碎片在闪烁时所散发出的光芒。在不同的光线下星尘紫会呈现出不同的色调有时偏紫有时偏银有时又像是掺杂了点点星光的淡蓝色。它既神秘又优雅仿佛能让人感受到宇宙的浩瀚与深邃。 设置: Temperature0.8, Top_p0.95 这种不存在的颜色我称之为“星际幻彩”Stellar Mirage。在视觉上它并非单一色调而是一种动态变化的色彩组合像是无数微小的光点在眼前闪烁变幻这些光点包含了所有可见光谱的颜色同时又带着一种神秘的、不可名状的色彩。 当观察者注视着“星际幻彩”的时候他可以看到蓝、绿、紫等颜色快速地在眼前切换和混合它们以一种几5. 完整 Transformers 代码实战把所有积木搭在一起这就是一段标准的模型推理代码import torch from transformers import AutoModelForCausalLM, AutoTokenizer # 1. 设置模型 ID model_id /kaggle/input/qwen2.5/transformers/7b-instruct/1/ # 2. 加载翻译官 tokenizer AutoTokenizer.from_pretrained(model_id, trust_remote_codeTrue) # 3. 加载大脑 (双卡模式) model AutoModelForCausalLM.from_pretrained( model_id, device_mapauto, torch_dtypetorch.float16, trust_remote_codeTrue ) # 4. 准备输入 prompt 请用这三个词写一个微小说Kaggle、深夜、爆显存 messages [ {role: system, content: 你是一个幽默的程序员。}, {role: user, content: prompt} ] text tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) model_inputs tokenizer([text], return_tensorspt).to(model.device) # 5. 生成 (调节参数) print( 正在生成...) generated_ids model.generate( **model_inputs, max_new_tokens512, temperature0.8, # 稍微有点创意 top_p0.9, # 剔除离谱词 do_sampleTrue # 必须开启采样温度才生效 ) # 6. 解码并输出 generated_ids [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response tokenizer.batch_decode(generated_ids, skip_special_tokensTrue)[0] print(- * 20) print(f 回答:\n{response}) print(- * 20)这是输出结果Loading checkpoint shards: 100% 4/4 [00:1300:00, 3.27s/it] The module name (originally ) is not a valid Python identifier. Please rename the original module to avoid import issues. WARNING:accelerate.big_modeling:Some parameters are on the meta device because they were offloaded to the cpu. 正在生成... -------------------- 回答: 在一个寒冷的深夜李雷坐在他那间堆满咖啡罐和代码文件的房间里屏幕上是Kaggle竞赛的数据集。他正尝试训练一个复杂的深度学习模型。然而就在他认为胜利在望的时候“嘶~”的一声显示器瞬间变成了一片漆黑伴随着一声悲壮的“爆显存了”。 李雷揉了揉眼睛看着眼前一片空白的屏幕心中充满了挫败感但他转念一想“还好不是‘爆内存了’否则我这台老旧电脑可能就要彻底退休了。”于是他又开始调整参数希望能在这个深夜里找到那个隐藏在数据海洋中的宝藏。 --------------------Hint:所有的代码都可以在 这个笔记本中直接获取运行哦6. 常见问题 (QA)Q: 在 Kaggle 上device_mapauto是必须的吗A:如果你使用单卡 T4 (15GB) 跑 7B 模型约 14GB勉强能塞进一张卡。但如果你开启了 Kaggle 的T4 x2为了利用全部 30GB 显存必须加这个参数否则模型只会塞进第一张卡导致第一张爆满第二张围观。Q: 为什么生成的每一句话都不一样A:因为我们开启了do_sampleTrue并且设置了temperature 0。模型在选择下一个词时是按概率随机抽取的。如果你想让结果每次都一样比如做数学题请设置do_sampleFalse此时温度失效变为贪婪解码。Q: 什么是 LogitsA:在代码深处模型输出的那个“概率表”在变成百分比之前叫 Logits未归一化的数值。Softmax 函数的作用就是把 Logits 变成概率。你可以把 Logits 理解为模型对每个词的“原始打分”。Q: Token 和字是一一对应的吗