03_bert_model_architecture_params
from transformers import BertModel, BertForSequenceClassificationmodel_name = 'bert-base-uncased'model = BertModel.from_pretrained(model_name)cls_model = BertForSequenceClassification.from_pretrained(model_name)model输出为:
BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ))1.1 BertModel 的架构
当你在 HuggingFace Transformers 中打印一个 BertModel 对象时,它会递归显示模型的所有模块层(modules/layers),就像一个层层嵌套的目录树。每个缩进代表一层封装,每个 () 里的内容就是一个 PyTorch 的 nn.Module 子模块。
简单来说,这就是在展示 BERT 模型的「骨架图」—— 从最外层到最内层的每一个神经网络层。
BERT-base 由 3 大块组成:
- Embeddings(嵌入层)—— 把文本转成向量:把输入的 token ID 转换成 768 维的稠密向量,并加上位置信息。
(embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768) # 词表大小 30522 → 768 维向量 (position_embeddings): Embedding(512, 768) # 位置编码,最多 512 个位置 (token_type_embeddings): Embedding(2, 768) # 句子类型(两句任务用) (LayerNorm): LayerNorm(768) # 层归一化 (dropout): Dropout(p=0.1) # 随机丢弃 10% 防止过拟合)- Encoder(编码器)—— 核心 Transformer 层
(encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( ... ) # 12 层完全相同的 Transformer 块 ))每层 BertLayer 又由 3 个子模块组成:
- Attention(注意力机制):让每个词「看到」句子中所有其他词,学习上下文关系。
(attention): BertAttention( (self): BertSelfAttention( # 多头自注意力 (query/key/value): Linear(768→768) # Q、K、V 三个线性变换 (dropout): Dropout(p=0.1) ) (output): BertSelfOutput( # 注意力输出 (dense): Linear(768→768) (LayerNorm): LayerNorm(768) (dropout): Dropout(p=0.1) ))- Intermediate(中间层)
(intermediate): BertIntermediate( (dense): Linear(768 → 3072) # 扩展到 4 倍维度 (intermediate_act_fn): GELUActivation() # GELU 激活函数)- Output(输出层)
(output): BertOutput( (dense): Linear(3072 → 768) # 压缩回 768 维 (LayerNorm): LayerNorm(768) (dropout): Dropout(p=0.1))中间层和输出层合称 FFN(前馈神经网络):先扩张再压缩,增加模型的表达能力。
- Pooler(汇聚层)—— 提取
[CLS]向量:取出编码器输出中第一个位置[CLS]的向量,再经过一层线性变换 + tanh,作为整句话的语义表示,常用于分类任务。
(pooler): BertPooler( (dense): Linear(768 → 768) (activation): Tanh() # tanh 激活)整体架构示意图:

这个架构就是 BERT 的核心:双向的 Transformer 编码器,通过 MLM(遮蔽语言模型)和 NSP(下一句预测)两个任务进行预训练,学到了丰富的语言知识。
cls_model输出为:
BertForSequenceClassification( (bert): BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ) ) (dropout): Dropout(p=0.1, inplace=False) (classifier): Linear(in_features=768, out_features=2, bias=True))1.2 BertForSequenceClassification 的输出
BertModel(基础模型)的架构:
BertModel├── embeddings├── encoder (12× BertLayer)└── poolerBertForSequenceClassification(分类模型)的架构:
BertForSequenceClassification├── bert (整个 BertModel)│ ├── embeddings│ ├── encoder (12× BertLayer)│ └── pooler├── dropout└── classifier: Linear(768 → 2)对比如下:
| 对比项 | BertModel | BertForSequenceClassification |
|---|---|---|
| 结构 | 只有 backbone | 在 backbone 基础上 + 分类头 |
| 额外模块 | 无 | dropout + classifier: Linear(768 → 2) |
| 输出 | 所有 token 的 768 维向量 + pooler 输出 | 2 个类别的 logits(如正面/负面) |
| 用途 | 通用特征提取器 | 专用于文本分类任务 |
新增的两个模块:
dropout—— 随机丢弃 10% 的神经元,防止微调时过拟合。classifier: Linear(768 → 2)—— 将 pooler 输出的[CLS]向量(768 维)映射到 2 个分类标签上。
为什么这样设计?
这背后是 “预训练 + 微调”(Pre-train then Fine-tune)范式:
预训练阶段: BertModel (在大规模无标注语料上学习通用语言知识) ↓微调阶段: BertForSequenceClassification (用标注数据微调整个模型)-
BertModel就像是一个通用的「语言理解引擎」,通过 MLM 和 NSP 任务学会了词法、句法和语义知识,但它本身不针对任何具体任务。 -
BertForSequenceClassification则是在这个引擎上加了一个「任务专用头」(classifier head),用于具体的分类任务(如情感分析、垃圾邮件检测等)。 -
微调时,整个模型(bert backbone + 分类头)一起训练。Bert 部分做微小的参数调整来适配任务,而新加的
classifier层则从头学习如何将[CLS]语义向量映射到目标类别。 -
in_features=768的 768 正是BertModel的hidden_size,而out_features=2表示该任务有 2 个分类(例如正面/负面)。
简单类比:BertModel 像是一个大学毕业生(通才),BertForSequenceClassification 像是在这个毕业生基础上进行「岗前培训」,让他成为某个特定岗位的专家(专才)。
1.3 为什么 BERT 没有 decoder
先回顾一下原始 Transformer 和 BERT 的关系:
1.3.1 原始 Transformer = Encoder + Decoder
原始的 Transformer(2017 年由 Google 提出)是 Encoder-Decoder 架构,专门用于序列到序列(seq2seq)任务,比如机器翻译:
原始 Transformer├── Encoder(编码器:6层) ← 双向注意力(能看到所有词)└── Decoder(解码器:6层) ← 掩码注意力(只能看左边的词)1.3.2 BERT 为什么只用 Encoder?
因为 BERT 的定位是「语言理解」模型,而不是「语言生成」模型。具体来说:
1. 预训练任务决定了架构
| 模型 | 预训练任务 | 需要什么能力 | 选用哪部分 |
|---|---|---|---|
| BERT | MLM(遮蔽语言模型)+ NSP | 理解上下文 → 预测被遮住的词 | Encoder(双向上下文) |
| GPT | 自回归语言模型(预测下一个词) | 从左到右依次生成 | Decoder(单向因果注意力) |
| T5 / BART | 去噪自编码(如翻译、文本修复) | 理解输入 + 生成输出 | Encoder + Decoder |
- MLM 任务:随机遮住 15% 的词,让模型根据左右两边的上下文来预测被遮住的词。
- 这就要求模型能同时看到某个词的左边和右边的信息——这正是 Encoder 的双向自注意力(bidirectional self-attention)的特性。
2. Decoder 的因果掩码(Causal Mask)不适合 BERT
Decoder 的核心特点是 因果注意力(causal attention):每个 token 只能看到它自己和它左边的 token,看不到右边的 token。这是为了在生成时保证自回归性质(不能偷看未来的词)。
但 BERT 的任务(MLM)恰恰需要看到未来的词(右边的上下文)来预测当前遮住的词,所以 Decoder 的因果掩码反而会阻碍 BERT 的学习目标。
一张图总结:不同模型家族的架构选择
| 流派 | 代表模型 | 使用部分 | 典型任务 |
|---|---|---|---|
| Encoder-only | BERT, RoBERTa, ALBERT | 只用 Encoder | 文本分类、情感分析、NER、阅读理解 |
| Decoder-only | GPT, LLaMA, Qwen | 只用 Decoder | 文本生成、对话、代码生成 |
| Encoder-Decoder | T5, BART, mT5 | Encoder + Decoder | 翻译、摘要、文本修复 |
1.3.3 一句话总结
BERT 只用 Encoder 是因为它的目标是「理解」而不是「生成」——需要同时看左右上下文来理解语义,而这正是 Encoder 双向注意力的专长。Decoder 的因果掩码(只能看左边)反而会限制这种理解能力。
2. 参数统计
total_params:总参数量total__learnable_params:可学习参数量
2.1 参数形状
for name, param in model.named_parameters(): print(name, '->', param.shape)输出为:
embeddings.word_embeddings.weight -> torch.Size([30522, 768])embeddings.position_embeddings.weight -> torch.Size([512, 768])embeddings.token_type_embeddings.weight -> torch.Size([2, 768])embeddings.LayerNorm.weight -> torch.Size([768])embeddings.LayerNorm.bias -> torch.Size([768])encoder.layer.0.attention.self.query.weight -> torch.Size([768, 768])encoder.layer.0.attention.self.query.bias -> torch.Size([768])encoder.layer.0.attention.self.key.weight -> torch.Size([768, 768])encoder.layer.0.attention.self.key.bias -> torch.Size([768])encoder.layer.0.attention.self.value.weight -> torch.Size([768, 768])encoder.layer.0.attention.self.value.bias -> torch.Size([768])encoder.layer.0.attention.output.dense.weight -> torch.Size([768, 768])encoder.layer.0.attention.output.dense.bias -> torch.Size([768])encoder.layer.0.attention.output.LayerNorm.weight -> torch.Size([768])encoder.layer.0.attention.output.LayerNorm.bias -> torch.Size([768])encoder.layer.0.intermediate.dense.weight -> torch.Size([3072, 768])encoder.layer.0.intermediate.dense.bias -> torch.Size([3072])encoder.layer.0.output.dense.weight -> torch.Size([768, 3072])encoder.layer.0.output.dense.bias -> torch.Size([768])encoder.layer.0.output.LayerNorm.weight -> torch.Size([768])encoder.layer.0.output.LayerNorm.bias -> torch.Size([768])encoder.layer.1.attention.self.query.weight -> torch.Size([768, 768])encoder.layer.1.attention.self.query.bias -> torch.Size([768])encoder.layer.1.attention.self.key.weight -> torch.Size([768, 768])encoder.layer.1.attention.self.key.bias -> torch.Size([768])...encoder.layer.11.output.LayerNorm.weight -> torch.Size([768])encoder.layer.11.output.LayerNorm.bias -> torch.Size([768])pooler.dense.weight -> torch.Size([768, 768])pooler.dense.bias -> torch.Size([768])输出解析:
- Embeddings 层(3 个嵌入矩阵 + LayerNorm)
- 前缀
embeddings说明在哪一层,后面的word_embeddings说明在层里的哪个模块中,weight表示具体的参数是什么。
- 前缀
embeddings.word_embeddings.weight -> [30522, 768] # 词嵌入矩阵:30522个词 × 768维embeddings.position_embeddings.weight -> [512, 768] # 位置嵌入:512个位置 × 768维embeddings.token_type_embeddings.weight -> [2, 768] # 句子类型嵌入:2种类型 × 768维embeddings.LayerNorm.weight -> [768] # LayerNorm 的缩放参数(γ)embeddings.LayerNorm.bias -> [768] # LayerNorm 的偏移参数(β)- 第 0 层 Encoder 的 Attention 部分(以 layer.0 为例)
# Q、K、V 三个线性变换:每个都是 768 → 768encoder.layer.0.attention.self.query.weight -> [768, 768] # 权重矩阵 W_qencoder.layer.0.attention.self.query.bias -> [768] # 偏置 b_qencoder.layer.0.attention.self.key.weight -> [768, 768] # 权重矩阵 W_kencoder.layer.0.attention.self.key.bias -> [768] # 偏置 b_kencoder.layer.0.attention.self.value.weight -> [768, 768] # 权重矩阵 W_vencoder.layer.0.attention.self.value.bias -> [768] # 偏置 b_v
# 注意力输出层encoder.layer.0.attention.output.dense.weight -> [768, 768] # 线性变换encoder.layer.0.attention.output.dense.bias -> [768]encoder.layer.0.attention.output.LayerNorm.weight -> [768]encoder.layer.0.attention.output.LayerNorm.bias -> [768]- 第 0 层 Encoder 的 FFN 部分
# Intermediate:扩展层 768 → 3072(4倍)encoder.layer.0.intermediate.dense.weight -> [3072, 768]encoder.layer.0.intermediate.dense.bias -> [3072]
# Output:压缩层 3072 → 768encoder.layer.0.output.dense.weight -> [768, 3072]encoder.layer.0.output.dense.bias -> [768]encoder.layer.0.output.LayerNorm.weight -> [768]encoder.layer.0.output.LayerNorm.bias -> [768]- 第 1~11 层(结构与 layer.0 完全相同)
encoder.layer.1.attention.self.query.weight -> [768, 768]encoder.layer.1.attention.self.key.weight -> [768, 768]... # 每一层都重复同样的结构,共 12 层encoder.layer.11.output.LayerNorm.weight -> [768]encoder.layer.11.output.LayerNorm.bias -> [768]- Pooler 层
pooler.dense.weight -> [768, 768] # 将 [CLS] 向量再做一次线性变换pooler.dense.bias -> [768]核心观察:
| 观察点 | 说明 | 原因 |
|---|---|---|
| 词嵌入矩阵最大 | [30522, 768] ≈ 2340 万参数 | 30522 是整个词表大小,这是模型中最大的单一张量 |
| 每层结构完全相同 | layer.0 ~ layer.11 的参数形状一样 | 12 层 Transformer 共享同一结构,参数独立 |
| Q/K/V 形状相同 | 都是 [768, 768] | 多头注意力中 Q/K/V 维度一致 |
| Intermediate 是 4 倍 | [3072, 768] = 768 × 4 | 这是 BERT 的设计惯例:FFN 中间层扩展到 4 倍隐藏维度 |
线性层的 weight 形状是 [out, in] | 例如 [3072, 768] 表示输出 3072、输入 768 | PyTorch 的 Linear 层使用 weight.shape = [out_features, in_features] |
你可以用这个输出手算 BERT-base 的总参数量(约 1.1 亿):把每个 shape 的元素个数相乘再求和即可。比如 [30522, 768] 就是 30522 × 768 个参数。
2.2 参数量
total_params = 0total_learnable_params = 0
for name, param in model.named_parameters(): print(name, '->', param.shape, '->', param.numel()) if param.requires_grad: total_learnable_params += param.numel() total_params += param.numel()
print('Total parameters:', total_params)print('Total learnable parameters:', total_learnable_params)输出为:
embeddings.word_embeddings.weight -> torch.Size([30522, 768]) -> 23440896embeddings.position_embeddings.weight -> torch.Size([512, 768]) -> 393216embeddings.token_type_embeddings.weight -> torch.Size([2, 768]) -> 1536embeddings.LayerNorm.weight -> torch.Size([768]) -> 768embeddings.LayerNorm.bias -> torch.Size([768]) -> 768encoder.layer.0.attention.self.query.weight -> torch.Size([768, 768]) -> 589824encoder.layer.0.attention.self.query.bias -> torch.Size([768]) -> 768encoder.layer.0.attention.self.key.weight -> torch.Size([768, 768]) -> 589824encoder.layer.0.attention.self.key.bias -> torch.Size([768]) -> 768encoder.layer.0.attention.self.value.weight -> torch.Size([768, 768]) -> 589824encoder.layer.0.attention.self.value.bias -> torch.Size([768]) -> 768encoder.layer.0.attention.output.dense.weight -> torch.Size([768, 768]) -> 589824encoder.layer.0.attention.output.dense.bias -> torch.Size([768]) -> 768encoder.layer.0.attention.output.LayerNorm.weight -> torch.Size([768]) -> 768encoder.layer.0.attention.output.LayerNorm.bias -> torch.Size([768]) -> 768encoder.layer.0.intermediate.dense.weight -> torch.Size([3072, 768]) -> 2359296encoder.layer.0.intermediate.dense.bias -> torch.Size([3072]) -> 3072encoder.layer.0.output.dense.weight -> torch.Size([768, 3072]) -> 2359296encoder.layer.0.output.dense.bias -> torch.Size([768]) -> 768encoder.layer.0.output.LayerNorm.weight -> torch.Size([768]) -> 768encoder.layer.0.output.LayerNorm.bias -> torch.Size([768]) -> 768encoder.layer.1.attention.self.query.weight -> torch.Size([768, 768]) -> 589824encoder.layer.1.attention.self.query.bias -> torch.Size([768]) -> 768encoder.layer.1.attention.self.key.weight -> torch.Size([768, 768]) -> 589824encoder.layer.1.attention.self.key.bias -> torch.Size([768]) -> 768...pooler.dense.weight -> torch.Size([768, 768]) -> 589824pooler.dense.bias -> torch.Size([768]) -> 768Total parameters: 109482240Total learnable parameters: 109482240可见,可学习的参数量与总参数量是一致的,接近 1.1 亿。
我们接下来看看每层每个模块的参数量:
total_embedding_params = 0total_encoder_params = 0total_pooler_params = 0
for name, param in model.named_parameters(): if name.startswith('embeddings'): total_embedding_params += param.numel() elif name.startswith('encoder'): total_encoder_params += param.numel() elif name.startswith('pooler'): total_pooler_params += param.numel()
print('Total embedding parameters:', total_embedding_params)print('Total encoder parameters:', total_encoder_params)print('Total pooler parameters:', total_pooler_params)输出为:
Total embedding parameters: 23837184Total encoder parameters: 85054464Total pooler parameters: 590592下面是占比:
params = [total_embedding_params, total_encoder_params, total_pooler_params]for param in params: print(f'{param / total_params:.2%}')输出为:
21.77%77.69%0.54%文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!