核心摘要 (TL;DR)
原理 :图解 Transformer 是如何通过“注意力机制”和“位置编码”来理解人类语言的。实战 :在 Kaggle (双 T4 GPU) 环境下,拆解 HuggingFace 代码的“铁三角”(Config, Tokenizer, Model)。技巧 :掌握 Temperature 和 Top_p,学会控制 AI 的“创造力”。 前言 各位友人们,大家好,这里是阿尔。在上一节的“炼丹”环境搭建中,咱们成功地将Qwen2.5模型运行了起来,跑通了。 但是相信大家对运行的时候的那些参数都代表着什么,都还是懵的。 这篇博客就是为此准备的,我们打算先快速大概地了解一下当前的大模型的底层原理,再结合在一起介绍通用的transformers库, 去看看代码和如何对应这些理论的。
大模型和普通的机器学习模型一样,本质也是一个函数,不同的是,传统机器学习可能输入的是一些整理好的数据,比如房子的尺寸,地段,购买时长,和市中心的距离等等数据,输出一个预测的放假,而大模型输入的是我们的问题,拿到的是大模型给出的回答。但,咱们究其根本,它们都可以看作是一个函数,我们输入一些东西,经过运算之后,输出给我们一些东西。
对于大模型,它的本质就是一个下一个字符预测器 ,我们输入一些文字,它只负责根据它所训练的海量数据,输出最符合,最有可能的下一个字符,就像一个文字接龙机器 ,其核心任务只有一个:根据上文,猜下一个字是什么。
为了实现这个目标,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.json config.json : 是大模型的身份证,也可以说是体检表,它其中就是真正的我们的模型,具体由哪些层构成,是什么架构类型,隐藏层有多深,注意力头的数量有几个,词表的大小是多少。对于大模型而言,它就是模型本身,也是骨架 ,因为大模型重要的是训练完的参数,模型本身是很小的。
2.2 行为预设:generation_config.json generation_config.json :是模型的出厂默认设置。
2.3 大脑本身:*.safetensors和*.index.json 有了config.json中的躯体,我们再从*.safetensors和*.index.json载入灵魂,这才是我们能说会道的大模型。
model-xxxxx.safetensors :这里面存的是实打实的张量(Tensor)数据 ,即数十亿个参数的浮点数。为了方便存储和加载,通常会被切分成多个 2GB-5GB 的小文件(Shard)。model.safetensors.index.json :这是一张藏宝图 。因为权重被切分了,模型需要知道“第 5 层的权重”到底藏在哪个文件里。内部长这样 :1 2 3 4 5 6 7 { "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 如何工作。 内部长这样 :1 2 3 4 5 { "chat_template" : "{% for message in messages %}...<|im_start|>..." , "eos_token" : "<|im_end|>" , "pad_token" : "<|endoftext|>" }
在Transforms库中,我们永远绕不开三个核心类。
环境准备: 在 Kaggle 右侧设置中,确保 Internet: On 且 Accelerator: GPU T4 x2 。
1 !pip install -U transformers accelerate bitsandbytes
3.1 AutoTokenizer (翻译官) 对应理论中的 Token 化 步骤,和模型文件中的tokenizer相关文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from transformers import AutoTokenizermodel_path = "/kaggle/input/qwen2.5/transformers/7b-instruct/1/" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True ) text = "Transformer is amazing" input_ids = tokenizer.encode(text) print (f"原文: {text} " )print (f"数字 ID: {input_ids} " )decoded_text = tokenizer.decode(input_ids) print (f"还原: {decoded_text} " )
得到的结果将会是这样
1 2 3 原文: Transformer is amazing 数字 ID: [46358, 374, 7897] 还原: Transformer is amazing
对话格式:Chat Template 大模型需要特定的对话格式(Prompt)。来将模型的回答,和用户的问题做区分, 我们一般都可以通过载入语言模型对应模板(不同家的模型,可能模板会有不同),甚至去拼装历史记录。
1 2 3 4 5 6 7 8 messages = [ {"role" : "system" , "content" : "你是一个物理学家。" }, {"role" : "user" , "content" : "用一句话解释相对论。" } ] formatted_prompt = tokenizer.apply_chat_template(messages, tokenize=False , add_generation_prompt=True ) print ("模型实际看到的输入:\n" , formatted_prompt)
运行结果如下
1 2 3 4 5 6 模型实际看到的输入: <|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_map="auto" 让库自动分配显存。
1 2 3 4 5 6 7 8 9 10 11 12 from transformers import AutoModelForCausalLMimport torchmodel = AutoModelForCausalLM.from_pretrained( model_path, device_map="auto" , torch_dtype=torch.float16, trust_remote_code=True ) print (f"模型加载成功!显存分布: {model.hf_device_map} " )
输出结果为
1 模型加载成功!显存分布: {'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为参数去测试不同的参数对回答的影响
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def test_generation (temp, top_p, prompt_text ): messages = [ {"role" : "system" , "content" : "你是一个前卫的科幻小说家。" }, {"role" : "user" , "content" : prompt_text} ] text = tokenizer.apply_chat_template(messages, tokenize=False , add_generation_prompt=True ) inputs = tokenizer([text], return_tensors="pt" ).to(model.device) print (f"\n======== 设置: Temperature={temp} , Top_p={top_p} ========" ) try : generated_ids = model.generate( **inputs, max_new_tokens=100 , temperature=temp, top_p=top_p, do_sample=True , pad_token_id=tokenizer.eos_token_id ) response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True )[0 ] print (response.split('assistant' )[-1 ].strip()) except Exception as e: print (f"生成出错: {e} " )
温度实验 然后再其下新建code block去测试,看看效果
1 2 3 4 5 6 7 8 9 10 11 prompt = "请用这三个词写一个微故事:量子、失恋、炒饭。" test_generation(temp=0.1 , top_p=0.9 , prompt_text=prompt) test_generation(temp=0.7 , top_p=0.9 , prompt_text=prompt) test_generation(temp=1.5 , top_p=0.9 , prompt_text=prompt)
这是我的运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ======== 设置: Temperature=0.1, Top_p=0.9 ======== 在量子世界里,时间与空间的概念变得模糊不清,而李明的世界也因为一段失败的恋情而变得一片混沌。他尝试着用量子纠缠理论来修复自己破碎的心灵,却意外地将自己从现实世界送入了一个平行宇宙。 在这个平行宇宙中,李明发现了一家特别的餐馆,这里的厨师是一位曾经的恋人,她正在为一位顾客准备一道特别的炒饭。这道炒饭不仅色香味俱全,还 ======== 设置: Temperature=0.7, Top_p=0.9 ======== 在量子世界的边缘,李明独自一人坐在一家不起眼的小餐馆里,面前是一碗普通的炒饭。他和女朋友分手的原因,是因为她沉迷于虚拟现实中的量子世界,而他却对现实世界充满了留恋。 就在几天前,他们最后一次争吵后,她告诉他:“我找到了真正的自我——一个穿梭在量子世界的探索者。”她留下了一盘未吃完的炒饭,然后消失在了虚拟现实中。 李明望着那盘炒饭, ======== 设置: Temperature=1.5, Top_p=0.9 ======== 标题:时空泡饭 李明最近失恋了,每天只能煮一大锅泡饭来消磨时间。 这天李明正在做饭,他忽然接收到一条量子信号。他惊奇地发现那是自己失恋前女友的坐标位置。想到能与自己相爱过的人共度时光是多么美妙的事情,他便将自己煮了一锅泡饭送进了坐标传送器。 结果却是一锅炒饭。 李明百思不解。
top_p实验 接下来是top_p
1 2 3 4 5 6 7 prompt_2 = "请给一种不存在的颜色起个名字,并描述它的样子。" test_generation(temp=0.8 , top_p=0.01 , prompt_text=prompt_2) test_generation(temp=0.8 , top_p=0.95 , prompt_text=prompt_2)
输出结果为
1 2 3 4 5 6 7 ======== 设置: Temperature=0.8, Top_p=0.01 ======== 这种不存在的颜色我命名为“星尘紫”。它是一种梦幻般的颜色,介于紫色和银色之间,仿佛是宇宙中无数微小的星辰碎片在闪烁时所散发出的光芒。在不同的光线下,星尘紫会呈现出不同的色调,有时偏紫,有时偏银,有时又像是掺杂了点点星光的淡蓝色。它既神秘又优雅,仿佛能让人感受到宇宙的浩瀚与深邃。 ======== 设置: Temperature=0.8, Top_p=0.95 ======== 这种不存在的颜色我称之为“星际幻彩”(Stellar Mirage)。在视觉上,它并非单一色调,而是一种动态变化的色彩组合,像是无数微小的光点在眼前闪烁变幻,这些光点包含了所有可见光谱的颜色,同时又带着一种神秘的、不可名状的色彩。 当观察者注视着“星际幻彩”的时候,他可以看到蓝、绿、紫等颜色快速地在眼前切换和混合,它们以一种几
把所有积木搭在一起,这就是一段标准的模型推理代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import torchfrom transformers import AutoModelForCausalLM, AutoTokenizermodel_id = "/kaggle/input/qwen2.5/transformers/7b-instruct/1/" tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True ) model = AutoModelForCausalLM.from_pretrained( model_id, device_map="auto" , torch_dtype=torch.float16, trust_remote_code=True ) prompt = "请用这三个词写一个微小说:Kaggle、深夜、爆显存" messages = [ {"role" : "system" , "content" : "你是一个幽默的程序员。" }, {"role" : "user" , "content" : prompt} ] text = tokenizer.apply_chat_template(messages, tokenize=False , add_generation_prompt=True ) model_inputs = tokenizer([text], return_tensors="pt" ).to(model.device) print (" 正在生成..." )generated_ids = model.generate( **model_inputs, max_new_tokens=512 , temperature=0.8 , top_p=0.9 , do_sample=True ) 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_tokens=True )[0 ] print ("-" * 20 )print (f" 回答:\n{response} " )print ("-" * 20 )
这是输出结果
1 2 3 4 5 6 7 8 9 10 11 Loading checkpoint shards: 100% 4/4 [00:13<00: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. 常见问题 (Q&A) Q: 在 Kaggle 上 device_map="auto" 是必须的吗? A: 如果你使用单卡 T4 (15GB) 跑 7B 模型(约 14GB),勉强能塞进一张卡。但如果你开启了 Kaggle 的 T4 x2 ,为了利用全部 30GB 显存,必须 加这个参数,否则模型只会塞进第一张卡,导致第一张爆满,第二张围观。
Q: 为什么生成的每一句话都不一样? A: 因为我们开启了 do_sample=True 并且设置了 temperature > 0。模型在选择下一个词时是按概率随机抽取 的。如果你想让结果每次都一样(比如做数学题),请设置 do_sample=False(此时温度失效,变为贪婪解码)。
Q: 什么是 Logits? A: 在代码深处,模型输出的那个“概率表”在变成百分比之前,叫 Logits(未归一化的数值)。Softmax 函数的作用就是把 Logits 变成概率。你可以把 Logits 理解为模型对每个词的“原始打分”。
Q: Token 和字是一一对应的吗? A: 不一定。
英文:通常一个单词是一个 Token,长单词可能被切分。 中文:通常一个汉字是一个 Token,但常见词(如“你好”)可能会合并为一个 Token。 平均来说,0.75 个英文单词 ≈ 1 Token ,1 个汉字 ≈ 1.5 - 2 Token (取决于分词器效率,Qwen 的中文压缩率很高)。