03_bert_model_architecture_params

2853 字
14 分钟
03_bert_model_architecture_params
from transformers import BertModel, BertForSequenceClassification
model_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 大块组成:

  1. 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% 防止过拟合
)
  1. 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(前馈神经网络):先扩张再压缩,增加模型的表达能力。

  1. 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)
└── pooler

BertForSequenceClassification(分类模型)的架构:

BertForSequenceClassification
├── bert (整个 BertModel)
│ ├── embeddings
│ ├── encoder (12× BertLayer)
│ └── pooler
├── dropout
└── classifier: Linear(768 → 2)

对比如下:

对比项BertModelBertForSequenceClassification
结构只有 backbone在 backbone 基础上 + 分类头
额外模块dropout + classifier: Linear(768 → 2)
输出所有 token 的 768 维向量 + pooler 输出2 个类别的 logits(如正面/负面)
用途通用特征提取器专用于文本分类任务

新增的两个模块:

  1. dropout —— 随机丢弃 10% 的神经元,防止微调时过拟合。
  2. 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 正是 BertModelhidden_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. 预训练任务决定了架构#

模型预训练任务需要什么能力选用哪部分
BERTMLM(遮蔽语言模型)+ 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 的学习目标。


一张图总结:不同模型家族的架构选择

graph LR subgraph "原始 Transformer" E[Encoder<br/>双向注意力] D[Decoder<br/>因果注意力] end subgraph "理解派 (Encoder-only)" B[BERT<br/>RoBERTa<br/>ALBERT] end subgraph "生成派 (Decoder-only)" G[GPT<br/>LLaMA<br/>Qwen] end subgraph "翻译/转换派 (Encoder-Decoder)" T[T5<br/>BART<br/>mT5] end E --> B D --> G E --> T D --> T
流派代表模型使用部分典型任务
Encoder-onlyBERT, RoBERTa, ALBERT只用 Encoder文本分类、情感分析、NER、阅读理解
Decoder-onlyGPT, LLaMA, Qwen只用 Decoder文本生成、对话、代码生成
Encoder-DecoderT5, BART, mT5Encoder + 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 → 768
encoder.layer.0.attention.self.query.weight -> [768, 768] # 权重矩阵 W_q
encoder.layer.0.attention.self.query.bias -> [768] # 偏置 b_q
encoder.layer.0.attention.self.key.weight -> [768, 768] # 权重矩阵 W_k
encoder.layer.0.attention.self.key.bias -> [768] # 偏置 b_k
encoder.layer.0.attention.self.value.weight -> [768, 768] # 权重矩阵 W_v
encoder.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 → 768
encoder.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、输入 768PyTorch 的 Linear 层使用 weight.shape = [out_features, in_features]

你可以用这个输出手算 BERT-base 的总参数量(约 1.1 亿):把每个 shape 的元素个数相乘再求和即可。比如 [30522, 768] 就是 30522 × 768 个参数。

2.2 参数量#

total_params = 0
total_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]) -> 23440896
embeddings.position_embeddings.weight -> torch.Size([512, 768]) -> 393216
embeddings.token_type_embeddings.weight -> torch.Size([2, 768]) -> 1536
embeddings.LayerNorm.weight -> torch.Size([768]) -> 768
embeddings.LayerNorm.bias -> torch.Size([768]) -> 768
encoder.layer.0.attention.self.query.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.0.attention.self.query.bias -> torch.Size([768]) -> 768
encoder.layer.0.attention.self.key.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.0.attention.self.key.bias -> torch.Size([768]) -> 768
encoder.layer.0.attention.self.value.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.0.attention.self.value.bias -> torch.Size([768]) -> 768
encoder.layer.0.attention.output.dense.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.0.attention.output.dense.bias -> torch.Size([768]) -> 768
encoder.layer.0.attention.output.LayerNorm.weight -> torch.Size([768]) -> 768
encoder.layer.0.attention.output.LayerNorm.bias -> torch.Size([768]) -> 768
encoder.layer.0.intermediate.dense.weight -> torch.Size([3072, 768]) -> 2359296
encoder.layer.0.intermediate.dense.bias -> torch.Size([3072]) -> 3072
encoder.layer.0.output.dense.weight -> torch.Size([768, 3072]) -> 2359296
encoder.layer.0.output.dense.bias -> torch.Size([768]) -> 768
encoder.layer.0.output.LayerNorm.weight -> torch.Size([768]) -> 768
encoder.layer.0.output.LayerNorm.bias -> torch.Size([768]) -> 768
encoder.layer.1.attention.self.query.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.1.attention.self.query.bias -> torch.Size([768]) -> 768
encoder.layer.1.attention.self.key.weight -> torch.Size([768, 768]) -> 589824
encoder.layer.1.attention.self.key.bias -> torch.Size([768]) -> 768
...
pooler.dense.weight -> torch.Size([768, 768]) -> 589824
pooler.dense.bias -> torch.Size([768]) -> 768
Total parameters: 109482240
Total learnable parameters: 109482240

可见,可学习的参数量与总参数量是一致的,接近 1.1 亿。

我们接下来看看每层每个模块的参数量:

total_embedding_params = 0
total_encoder_params = 0
total_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: 23837184
Total encoder parameters: 85054464
Total 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%

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

03_bert_model_architecture_params
https://github.com/chunhuizhang/bilibili_vlogs/
作者
HAC
发布于
2026-06-08
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
HAC
观之非易,行且克难
Greetings
欢迎来到我的博客!这里主要分享我的学习笔记与兴趣爱好。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
5
标签
13
总字数
79,889
运行时长
0
最后活动
0 天前

文章目录