1. 为什么我上手第一件事,是放弃自己训练、转向下载预训练模型
1.1 我最初的思路完全跑偏了
先还原一下我自己的经历。早期做 NLP 项目的时候,我以为所谓的"掌握 Transformers",就是要从零把注意力机制搭出来,再拿自己的语料训一个像样的模型。那会儿我连 Transformer 的论文都读了好几遍,手写了部分前向传播,然后发现一个残酷的现实:光是一个中型规模的模型,跑一轮训练就要几个小时,而且我手里的数据量撑死也就几万条。训完效果一测,准确率还不如我用正则硬写的规则版。这件事让我意识到,在 2024 年以后做 NLP,绝大多数场景下"训练一个模型"和"使用一个模型"完全是两码事。
后来我把思路改成了"用 Python 加载别人已经训好的预训练大模型"。"预训练大模型"这个词听起来很高端,其实说白了就是有人已经花了大量算力和数据,把语言的通用知识塞进了一组权重里。我们要做的只是把这些权重下载下来,加载进内存,然后把自己手头的文本喂给模型,拿回结果。这条路才是大部分业务场景真正需要学会的。
1.2 算一笔自己从头训模型的账
为什么不建议自己训?拿一个经典的 Base 级模型来说,参数量大概在 1 亿到 3 亿之间。看上去不算夸张,但注意,这还只是模型结构部分的参数。要训出有意义的语言理解能力,消耗比模型参数量高好几个量级:
- 数据:一个像样的预训练语料通常要达到几十 GB 甚至上 TB 的纯文本。你手头的几万条评论、几千份文档,在"学语言"这个层面连毛毛雨都算不上。
- 算力:就算是 1 亿参数的小模型,在单张消费级显卡上训一次完整的预训练周期,往往要连续跑几十天。我见过朋友拿 8 张卡训一个中小模型,跑了一周多,光是电费和云服务器费用就够买一台不错的电脑了。
- 效果:你自训的模型,由于数据量和训练步数远不如社区开源模型,通常学到的东西更浅。最后很可能得到一个"能跑但不好用"的模型,反而浪费时间。
当然,我这么说不是否定自己训练。如果你是发论文、做特定领域研究、或者在根本没有开源模型的语种上做项目,那确实需要从零训。但对 90% 的工程师、数据分析师、学生来说,直接加载别人训好的模型来完成推理,是回报率最高的路径。
1.3 现成预训练模型能直接帮你干哪些活
举几个我实际用过的例子:
- 情感分析:判断一段评论是正面、负面还是中性。
- 文本分类:把客服工单自动分到"退款、咨询、故障、投诉"等类别。
- 命名实体识别:从合同、新闻里抽出人名、地名、公司名。
- 文本摘要:把长文章压缩成几句话。
- 翻译:中英、英中以及不少语种间的互译。
- 问答:给定一段上下文,让模型回答具体问题。
- 文本生成:续写故事、写邮件草稿、生成代码片段。
这些能力,绝大多数都已经有开源社区公开的权重,直接用即可。真正需要你动手微调的,通常是领域非常专门的场景,比如你有一套自己标注的行业数据,希望模型更懂你们公司的业务术语。即便是微调,起点也不是"随机初始化",而是"在一个强大的预训练权重上继续学",成本低得多,这就是 Transformers 这套开源库存在的核心意义。
2. 环境准备:把 Python 装到能顺利 import transformers 的状态
2.1 先建虚拟环境,别往全局 Python 里硬灌
第一步是装环境,这一环看着简单,实际上坑最多。我建议你从现在开始养成一个习惯:每个项目一个虚拟环境。原因很简单,transformers 和 torch 这两个包的体积很大,而且对版本要求相当敏感。如果你直接在系统 Python 里装,改天做个其他项目需要换 torch 版本,很容易变成灾难现场。
创建虚拟环境的方法很直接,以 Python 3.9 及以上版本为例:
python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate激活之后,命令行前面会出现一个(.venv)标识,说明你已经在虚拟环境里了。后面装的包都只会进入这个环境,不会污染系统全局。
2.2 安装 torch 和 transformers 的正确姿势
Transformers 库本身只是模型加载和推理的上层封装,真正干重活的是背后的深度学习框架。目前最常见的是 PyTorch,也就是torch这个包。安装命令本身不复杂:
pip install torch transformers sentencepiece acceleratetransformers负责加载模型和分词器,torch负责矩阵计算,sentencepiece是很多大模型用到的分词工具,accelerate在多卡、混合精度这些场景下会用到。一次性装齐能省去后面很多"缺一个依赖"的报错。
如果你有 NVIDIA 显卡,并且确定要用 GPU 加速,建议先访问 PyTorch 官网,按其对应 CUDA 版本给出的命令单独安装 torch,之后再装 transformers。很多人直接pip install torch也能装上,但装出来的通常是 CPU 版,显存完全用不上,推理速度慢到怀疑人生。
2.3 装完之后最该做的三件事
第一件事,确认版本号一致:
python -c "import transformers; print(transformers.__version__)"第二件事,检查自己的硬件能不能用 GPU:
import torch print(torch.cuda.is_available())输出True说明可以用 CUDA 加速;如果输出False,也别灰心,代码照样能跑,只是用 CPU,速度会慢不少。CPU 也不是完全不能干活,我用 Apple 芯片的 Mac 跑小型模型效果也可以接受,只是大模型会吃力。
第三件事,确认磁盘空间。现在的预训练模型不是一个小文件,一个模型目录常常 1GB 到 7GB 不等。下载之前先看看自己的磁盘剩余空间够不够,我见过有人下载到一半磁盘满了,然后缓存文件损坏,最后还是得删掉重来。
3. 第一段代码:pipeline() 把整个推理过程封装成了一行调用
3.1 用两三行代码跑通情感分析
环境装好之后,就可以接触 Transformers 库最友好的入口——pipeline()。我第一次用的时候还挺惊讶,因为这比我预想的简单太多:
from transformers import pipeline classifier = pipeline( "sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english" ) result = classifier("I really love this new feature!") print(result)输出效果类似这样的结构:
[{'label': 'POSITIVE', 'score': 0.9997}]意思是:模型认为这句话是正向情绪,置信度 99.97%。咱们不需要手写分词、不需要自己构造输入张量、不需要处理输出概率,三五行代码就把一个完整的预训练模型调用跑通了。
3.2 为什么 pipeline 能把步骤压缩成这样
很多人第一次看到这代码会好奇,pipeline 到底帮我们做了什么。其实它内部依次完成了三件事:
- 把输入文本交给对应的分词器,转成模型能读的 token id 序列。
- 把 token id 交给模型做前向计算,得到每个类别的分数。
- 把分数转成可读的标签和置信度,完成输出。
换句话说,pipeline()就是"分词器 + 模型 + 后处理"的打包组合。你完全可以自己一步步实现这三步,但用 pipeline 可以让你快速验证想法,先跑通再深入理解细节,我认为这种学习顺序比我反过来去啃源码效率高很多。
3.3 怎么更换模型来获得不同能力
pipeline()的第一参数是任务名,第二个参数可以指定具体的模型名称。我用过的几个高频组合:
| 需求 | 任务名 | 推荐模型示例 |
|---|---|---|
| 情感分析 | sentiment-analysis | distilbert-base-uncased-finetuned-sst-2-english |
| 文本生成 | text-generation | gpt2 或者更大的开源生成模型 |
| 文本摘要 | summarization | facebook/bart-large-cnn |
| 命名实体识别 | ner | dslim/bert-base-NER |
| 问答 | question-answering | distilbert-base-cased-distilled-squad |
| 翻译 | translation | Helsinki-NLP 系列模型 |
这里有必要提醒一句:同样的任务,不同模型效果差异可能非常大。比如用默认的情感分析模型处理中文评论,几乎必翻车,因为很多默认模型是英文语料训出来的。碰到中文场景,就去模型库按语言筛选,选合适的中文模型。模型名通常长成这样:uer/roberta-base-finetuned-chinanews-chinese,把这样的名字直接填进model=参数即可。
4. 拨开 pipeline 外壳:分词器和模型到底各自干了什么
4.1 Tokenizer:给文本"切块",再翻译成数字
当你不再满足于 pipeline,开始想自己在代码里加载模型的时候,第一个必须搞懂的概念就是 Tokenizer(分词器)。可以这样理解:神经网络吃不了原始的字符串,它只能处理数字矩阵。分词器要做的事情就是,把一个句子按规则切成一个个 token,再把每个 token 映射到一个唯一的数字 id。
比如"I love transformers"这个句子,可能会被切成["i", "love", "transform", "ers"]这样的 token 序列,然后变成[1045, 2293, 19081, 5652]这样的数字数组。注意,"transformers" 在这里被切成了两个 token,这是子词切分的常见操作,目的是让模型应对没见过的新词时也能拼出大致含义。
使用分词器也很简单:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") inputs = tokenizer("I love transformers", return_tensors="pt") print(inputs)return_tensors="pt"的意思是让分词器返回 PyTorch 张量,这样可以直接送给模型。
你会发现输出里不只有input_ids,往往还有个attention_mask。这个 mask 的作用是告诉模型:哪些位置是真正的文本,哪些位置是补齐用的填充。因为批量处理时,同一个 batch 里的句子会有长有短,我们会把短句子统一填充到和最长句子一样的长度,这个 mask 就是用来防止模型在填充符上浪费注意力的。
4.2 前向传播:model() 是如何把 token 变成分数的
拿到 token 后,下一步就是把输入扔给模型:
from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased") outputs = model(**inputs) logits = outputs.logits print(logits)这里的AutoModelForSequenceClassification是"带分类头"的版本,也就是说它在预训练语言模型基础上,额外加了一层全连接层,把最后一层输出映射到类别分数。这个分数叫 logits,它还不是概率,若要转成概率,交给 softmax 即可。
至于模型内部,确实非常复杂,但咱们入门阶段只需要建立一个粗印象:文本 token 经过嵌入层变成向量,再经过多层注意力机制,每一层都会让每个 token 的表示"参考"整个序列的信息,越深层的表示越富含上下文语义,最后我们用最后一层的结果去做分类、生成或其他任务。
注意力机制可以打个比方:读一句话时,有些词的重要性是上下文决定的。比如在"警察抓住了小偷,因为他跑得很快"里,"他"指代谁,要看前文提到"小偷"这个词时模型的注意力集中在哪个 token 上。Transformer 就是通过这种动态加权的方式,让文本中任意两个位置都能直接互动,从而解决老式 RNN 难以处理长距离依赖的问题。
4.3 为什么推荐用 AutoClass 系列接口
你可能注意到我一直用AutoTokenizer、AutoModelForXxx这种带Auto开头的类。原因是这类接口会根据你传入的模型名自动判断该用哪个具体的类。不同预训练模型的代码路径差别很大,有的用 BERT 结构,有的用 GPT 结构,有的用 T5 结构,如果每次都手动指定类,一来你需要记住大量细节,二来容易出错。AutoClass 系列的价值就是把这层判断自动化,让你把注意力放在业务逻辑上。
还有个实战经验:尽量用和预训练时一致的AutoTokenizer,不要自己拼分词器。很多模型发布时会一起公布配套分词器,你自己换一个,token 序列对不上,后面整个结果都会变得很怪。我最初图省事手动用一个通用分词器去加载 BERT,效果惨不忍睹,换回配套分词器之后一切正常。
5. 文本生成实操:GPT-2 等生成式模型要怎么调用、参数怎么调
5.1 先跑通最基础的续写功能
情感分类只是"理解类"任务,生成类任务才是现在大模型最有魅力的地方。所谓文本生成,就是给模型一个开头,让它顺着语言规律继续往下写。先看一段最基础的代码:
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM model_name = "gpt2" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name) prompt = "Once upon a time," inputs = tokenizer(prompt, return_tensors="pt") outputs = model.generate( **inputs, max_new_tokens=50, do_sample=True, temperature=0.9 ) print(tokenizer.decode(outputs[0], skip_special_tokens=True))这里的generate()是整个生成流程的关键所在。max_new_tokens=50表示最多生成 50 个新 token。do_sample=True表示采用概率采样而不是贪心策略,让生成的文本更有变化。temperature是采样温度,它控制着随机性的大小。
5.2 temperature、top_p、top_k 分别控制什么
生成模型的核心是:每一步都计算出下一个 token 的概率分布,然后从中挑一个 token 输出,再把输出拼回输入,继续算下一步。参数不同,挑 token 的策略不同。
temperature=0几乎变成确定性选择,每次选概率最高的词,结果稳定但容易重复;temperature偏高,比如 1.2 以上,输出会更发散、更有创意,但也更容易胡言乱语;top_k=50表示只在概率最高的前 50 个 token 里采样,避免选中那些概率极低的生僻词;top_p=0.9表示从累计概率达到 0.9 的那部分 token 里采样,是一个动态筛选策略。
我实践下来,写正式文案时常用temperature=0.7, top_p=0.9;跑代码生成的时候甚至会把 temperature 调到 0.2 左右,让它更保守更可控。如果你发现生成的文本不断重复同一个词,通常就是 temperature 太低或重复惩罚权重没调好,可以试试repetition_penalty=1.2这类参数。
5.3 显存和序列长度:生成任务最容易踩的坎
生成式模型和分类模型有个显著区别:分类只需要一次前向传播,生成却需要一步步反复前向传播,每生成一个 token 都要把整个序列重新过一遍模型。所以在 CPU 上跑文本生成尤其慢,GPU 上也得注意显存占用。
显存不够的典型报错是 CUDA out of memory。解决办法我按优先级排列:
- 换更小的模型,比如从 7B 降到 1B 级别;
- 降低序列长度,
max_new_tokens别一下子设太高; - 使用半精度加载,
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16),显存占用几乎减半; - 开启加速库的 CPU 模式下放,Keras 场景下也能跑,但速度要打折扣。
其实对我个人来说,生成式模型入门前最该记住的一件事是:模型看到的上下文越长,生成质量未必越高,但计算成本和显存占用一定越高。所以生成之前先想清楚,开头到底需要多长,不要无脑把全文都塞进去。
6. 从"能跑"到"耐久":放进业务前我整理出的几个注意点
6.1 改掉每次运行都从网络下载模型的坏习惯
用from_pretrained第一次加载模型时,transformers 会把它下载到本地缓存目录,默认是在用户主目录下的.cache/huggingface。这个机制本身是好的,但有几个小问题。
第一,如果你换了机器或者换了用户,缓存目录变了,就得重新下载一遍几 GB 的文件。第二,有些离线环境不允许访问外网,模型根本拉不下来。我的做法是,把常用模型固定放到项目目录里的一个 models 文件夹下,然后用本地路径加载:
# 先单独把模型下载到本地目录 huggingface-cli download gpt2 --local-dir ./models/gpt2加载的时候改为:
model = AutoModelForCausalLM.from_pretrained("./models/gpt2")这样等于把"下载"和"运行"拆开。项目部署到内网或者容器时,直接把 models 文件夹一起打包带过去,既稳又快。尤其是团队合作时,避免每个人各下载一套多 GB 的权重,是在替大家省时间。
6.2 批量推理的 hidden gem:一次性处理多条文本
业务场景里很少只处理一条文本。拿情感分析来说,你可能有一整个 CSV 文件几千条评论要跑。这时候最直观的想法是写个循环一条条处理,但速度会比较慢,因为每条都重新加载模型。更好的做法是复用同一个模型对象,把多条文本组成一个 batch 一起推进去。
texts = ["I love this!", "This is terrible.", "Not bad at all."] inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt") outputs = model(**inputs)注意这里的padding=True和truncation=True,前者会补齐短句到整批最长长度,后者会把超长句子截断。这正是前面说的 attention_mask 发挥作用的时候。批处理能大幅提高吞吐,但一次塞太多文本也可能撑爆显存,需要根据实际情况找到一个合理的 batch size,我常用的是 16 或 32,具体得看模型大小和文本长度。
6.3 推理速度不够,先做量化再做优化
如果你发现模型跑得太慢,或者显存一直不宽裕,优先考虑量化。所谓量化,简单理解就是把模型的浮点参数转成更低精度的整数表示,比如从 32 位浮点转成 8 位整数。这样模型体积变小,计算速度也会提升,代价是精度有一点损失,但在很多任务上并不明显。
Transformers 配合bitsandbytes库,可以几行代码加载一个 4-bit 量化模型:
from transformers import BitsAndBytesConfig import torch quant_config = BitsAndBytesConfig(load_in_4bit=True) model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=quant_config )我自己的体验是,7B 模型不开量化,个人电脑基本没戏;开了 4-bit 量化之后,显存占用降到原来的一半以下,推理速度反而因为内存宽裕了有所提升。当然,量化不是免费的,如果在实际评估中精度明显掉了,还是要回到浮点版本。
6.4 遇到乱七八槽的报错,先做这三件事
初次接触 transformers 的朋友,几乎都会在某一步卡住。我总结过最常见的三种情况:
- 缺包报错:提示
ModuleNotFoundError,别怀疑,直接pip install对应的包,常见的是sentencepiece和accelerate。 - 模型与任务不匹配:用
AutoModelForCausalLM去加载一个纯编码器模型,会直接报错。解决办法是看清楚模型卡上的说明,确认它属于 encoder、decoder 还是 encoder-decoder 架构,然后选对应的 AutoModel 子类。 - tokenizer 和模型版本不一致:如果你下载的时候是随便配的,很可能出现生成的 token 无法解码成正常文本。解决方法是把模型的
config.json和tokenizer_config.json放在一起,加载同一个目录里的两个文件。
我的习惯是,在任何新模型上跑正式业务之前,先拿两条样本数据快速验证,一条正常文本,一条极端长的文本,把输入、输出、耗时都打出来看一眼。这一步几乎不需要额外时间,却能避免很多只在正式运行时才暴露的坑。
说到最后,还想再分享一个小技巧:如果你用pipeline()只是为了验证想法,那非常好;但项目一旦要进入批量处理或者线上服务阶段,我建议直接换到底层 API,也就是用AutoTokenizer加AutoModel自己管理输入输出。原因只有一个——pipeline 虽然方便,但它内部的默认参数并不总是适合你的场景,你很难精准控制 batch、padding 和采样参数。底层 API 看起来多写几行代码,但换来的是完全的可控性和更清楚的问题定位能力。先靠 pipeline 跑通,再逐步拆开自己接管,这条路线我用了很多年,始终觉得很划算。