十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BERT-PyTorch源码深度解析:核心机制与预训练实现

BERT-PyTorch源码深度解析:核心机制与预训练实现 简介这份压缩包是 Google AI 2018 年提出的 BERT 模型的 PyTorch 实现面向想要复现预训练语言模型、开展 NLP 下游任务迁移学习的开发者。代码结构简洁便于对照论文理解深度双向 Transformer 编码器的工作原理。资源共 33 个文件以 27 个 Python 源码为主涵盖模型定义、数据集处理、训练器、命令行入口与测试脚本并附有 setup.py、requirements.txt、Makefile 等安装配置文件总量约 28KB轻量易用。已有 878 人学习。通过该实现读者可以快速搭建 BERT 预训练流程并进一步利用其词向量或句子表示完成文本分类、命名实体识别、问答等任务是学习 PyTorch 与 NLP 预训练技术的实用参考。 如果你翻过GitHub上NLP入门项目的榜单大概率见过这个仓库——BERT-pytorch。它是GitHub上相当早期、star量很高的BERT PyTorch实现出自韩国开发者codertimo之手2018年论文刚出的时候就开始写了。这个项目解决的问题很直接Google AI 团队当年放出的BERT官方源码是基于TensorFlow的而很多研究者手头习惯用PyTorch想在PyTorch生态里复现BERT、做预训练、做微调需要一个足够简洁、能读懂的参照实现。BERT-pytorch就是这个角色它不追求完整复刻官方训练流程而是用不到两千行代码把BERT的核心组件——词嵌入、Transformer编码器、预训练任务——拆得明明白白。如果你正打算入门NLP、理解BERT结构或者在PyTorch里手动搭建预训练模型这个项目很适合拿来精读和跑通。这篇内容不是简单翻译README我会从模型实现、预训练原理、环境搭建、踩坑记录几个维度完整拆一遍这个项目结合我实际操作和二次开发的经验把那些文档里没写透的点都补出来。1. 先说清楚这个项目为什么值得看很多初学者上手BERT第一步就是pip装一个transformers然后调用BertModel就完事了。这种方式没毛病但有个隐藏问题——你只是API使用者不是模型理解者。一旦遇到需要改结构、加模块、做蒸馏、做量化、分析中间层表征的时候只会调包会很吃力。BERT-pytorch这个项目的价值恰恰在于它把BERT的核心计算流程“解剖”了。1.1 官方TensorFlow版与PyTorch社区版的差异Google官方发布的BERT源码google-research/bert有两个历史性约束一是基于TensorFlow 1.x的静态图模式那个年代写代码要先用tf.placeholder占位、再建session去run调试体验和现在的动态图完全两回事二是官方代码为了适配TPU训练做了大量与硬件相关的封装数据管道用的是Estimator API这导致代码读起来很绕很多逻辑被框架细节掩盖了。BERT-pytorch的好处在于动态图、命令式编程每一步张量怎么流动一目了然。对于学习来说PyTorch的实现更像一篇“带注释的论文代码”你从代码里能看到论文里每个公式是怎么落到实际张量运算上的比如自注意力里的Q、K、V矩阵是怎么从hidden state变换来的LayerNorm的gamma和beta是在哪个维度上做的归一化。这份可读性带来的学习效率提升是大多数人选它而不选官方实现的原因。1.2 这个版本实现了哪些核心能力从功能范围看BERT-pytorch实现了两阶段的核心能力。第一阶段是预训练仓库提供了两个经典任务Masked Language ModelMLM掩码语言模型和Next Sentence PredictionNSP下一句预测。前者随机遮住输入中15%的token让模型根据上下文预测被遮住的词从而习得双向语义表征后者在训练时同时输入句子A和句子B判断B是不是A在原文中真实的下一句从而让模型具备句子间关系建模能力。这两个任务组合在一起就是BERT预训练的基本盘。第二阶段是微调接口仓库里提供了一些下游任务的fine-tuning示例包括情感分类、语义相似度判断等用预训练好的模型参数作为初始化再在具体任务标注数据上做小规模训练。由于项目结构简单你可以很容易把输出层替换成自己的分类器、序列标注头甚至是生成模型的编码器来用。2. 核心代码是怎么组织的每个模块在干什么这个项目的主目录结构很清晰核心代码几乎都集中在bert文件夹下包括embedding、transformer、encoder、attention这些子模块。我建议你按这个顺序读代码先看模型入口再看encoder然后进到attention和embedding最后回来看预训练任务和训练循环。2.1 Embedding层的三个组成部分BERT的Embedding层由三类嵌入相加得到Token Embedding词嵌入、Segment Embedding句子分段嵌入、Position Embedding位置嵌入。Token Embedding比较好理解就是把词id映射成768维向量BERT-base设定本质上就是一张可训练的查找表。需要注意的一点是BERT使用WordPiece分词器词表大小是30522这个数字在BERT-pytorch里直接写死为vocab_size参数如果你用自己的语料做预训练需要自己训练分词器并替换词表。Segment Embedding是BERT处理“句子对”输入的关键。它给句子A的所有token分配一个可学习的向量默认0给句子B的所有token分配另一个向量默认1这样模型能区分当前位置属于哪个句子。注意这里的“句子”不是以句号划分的自然句而是文本片段可以是一个段落。Position Embedding是很多人容易忽略的细节。BERT用的不是Transformer原版论文里的三角函数位置编码而是可学习的位置嵌入最大位置长度设为512个token。这意味着BERT-base能处理的最长输入就是512个token超出部分需要截断或分段处理。这个512的上限很关键后面做长文本任务时会经常遇到。这三个嵌入做完逐元素相加后再过一层LayerNorm和Dropout得到的输出维度是(batch, seq_len, 768)然后送入Transformer编码器。代码里需要注意一个小细节embedding层默认把padding位置也参与计算了mask逻辑是在attention阶段才生效的这符合BERT原始设计。2.2 Transformer编码器与自注意力机制BERT-base共12层Transformer Encoder Block每层内部主要包含两个子层多头自注意力Multi-Head Self-Attention和前馈网络Feed-Forward Network每个子层后面接残差连接和LayerNorm。自注意力层是BERT最核心的模块。输入的hidden state形状为batch, seq_len, 768通过三个独立的线性层分别变换为Q、K、V维度仍然是768。然后按12个头拆分每个头的维度是768/1264形状变成(batch, 12, seq_len, 64)。Q乘以K的转置除以根号d_k即根号648这一步叫缩放点积注意力目的是防止内积过大导致softmax梯度消失。这里有个实操中容易踩的坑mask矩阵的处理方式。BERT-pytorch里用的是“将mask中为0的位置加上一个极大的负数”比如-1e9这样在softmax归一化后这些位置的注意力权重趋近于0等于模型完全忽略padding位置。要特别注意mask的维度必须能正确广播到(batch, 12, seq_len, seq_len)这个四维注意力分数矩阵上维度对不上是最常见的报错原因。前馈网络是两层线性变换加一个GELU激活函数第一层把768维升到3072维第二层再降回768维。这里的4倍隐藏层维度膨胀是Transformer的标配。关于激活函数论文和复现代码里都有一个小改动值得注意BERT原始论文用的是GELU而不是ReLUGELU在负区间不是完全置零保留了少量梯度实际训练中收敛更平稳。2.3 预训练任务的实现逻辑MLM的实现逻辑是对输入序列中15%的token随机挑选出来其中80%替换为[MASK]标记10%替换为随机词10%保留原词。之所以不是100%都用[MASK]是因为[MASK]标记在微调阶段不会出现如果预训练时全用[MASK]模型会过于依赖这个特殊标记导致对正常句子泛化能力差。训练时只计算被选中token位置上的交叉熵损失其他位置的logits不参与loss计算。在BERT-pytorch的代码里这个逻辑通过torch.where和masked_select方式实现读代码时建议把数据形状的变化一行一行打出来看。NSP的实现则简单直接对每个训练样本构造句子A和句子B50%概率B是真实的下一句50%概率B是随机采样的一句。这两个类型的样本各占一半模型输出一个二分类logits。由于NSP任务相对简单后续很多研究如RoBERTa发现去掉NSP任务反而能提升部分下游任务效果但BERT原始设计里它确实是重要组成部分。3. 实操从零跑通这个项目的完整过程3.1 环境准备与PyTorch安装先说环境。我用的是Ubuntu 20.04显卡是RTX 3090CUDA版本11.8。PyTorch安装这块起步就是确认自己的显卡驱动支持的CUDA版本。最简单的做法是直接在终端执行nvidia-smi看右上角的CUDA Version这个数字是驱动支持的最高版本不是说你机器上装了的版本。PyTorch安装时选择与驱动兼容的CUDA对应版本即可。我当时的安装命令是conda create -n bert python3.8 conda activate bert pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117实际上如果你不需要特定旧版本直接从PyTorch官网首页复制最新命令就行现在都用pip install torch这行命令会自动搭配好CUDA支持版本。但在国内网络环境下经常遇到超时我的建议是配置pip国内镜像源同时给pip加上--timeout60参数实测能解决大部分下载中断问题。装完验证一下GPU是否可用import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出True并且正确显示你的显卡型号说明环境没问题。如果显示False大概率是安装的PyTorch版本与CUDA不匹配或者CUDA路径没有生效。3.2 数据准备与训练命令BERT-pytorch仓库自带了一个demo数据集结构是txt格式的语料每行一段文本用作为两句话的分隔符训练时会自动切分为句子A和句子B。我第一次跑的时候用的就是自带demo数据训练脚本命令如下python train.py \ --data_dir data/demo/train.txt \ --output_dir output \ --bert_config_file bert_config.json \ --batch_size 32 \ --num_epochs 10 \ --lr 1e-4你需要严格按照仓库里的bert_config.json来设置模型参数BERT-base配置是hidden_size768num_hidden_layers12num_attention_heads12intermediate_size3072vocab_size30522。我自己试过直接改小hidden_size到256来快速验证流程这样训练会略快但最终效果和标准BERT完全没有可比性。如果你用自己的语料训练需要先自己做分词。BERT-pytorch用的tokenizer比较简单就是按空格切分的white space tokenizer对中文支持很差。中文场景建议你先用jieba分词或者换成transformers库里的BertTokenizer加载中文词表再把切好的token id作为输入。3.3 模型保存与加载的实践训练过程中模型会定期保存checkpoint里面包含模型参数和优化器状态。我自己二次开发时一般只保存模型参数用torch.save(model.state_dict(), path)即可推理时model BERT() model.load_state_dict(torch.load(path, map_locationcpu)) model.eval()一个容易忽略的问题是BERT模型参数比较大BERT-base大概1.1亿参数保存成文件大约是400多MB。如果你只是想在CPU上做演示或推理加载时最好指定map_locationcpu否则在有GPU的机器上会默认往cuda:0上放显存显存不够就直接OOM或加载失败。PyTorch 2.0之后还有一个坑保存模型时默认启用了一些新特性老版本加载新版本保存的权重文件偶尔会报错提示缺少state_dict中的某些键。解决办法是保存时统一用state_dict方式并且尽量保持训练和部署环境的PyTorch版本在同一个大版本范围内。4. 跑模型过程中的常见问题和排查技巧这部分写几个我实际遇到的高频问题可能你也会踩到。4.1 与CUDA和PyTorch版本相关的报错最经典的是这个提示CUDA error: device-side assert triggered。这种报错通常不是CUDA本身坏了而是模型内部某个操作出现了非法值最常见的触发点在Embedding层。当你传入的token id超出了vocab_size范围或出现了负数idCUDA就会在设备侧断言失败。排查方法很简单在训练循环里打印输入的min和max检查是否有越界id。如果是自己构造的数据十有八九是vocab映射那里有小bug。另一个常见告警是UserWarning: CUDA initialization: The NVIDIA driver on your system is too old。这种就是驱动和PyTorch版本不匹配。此时优先检查nvidia-smi的驱动版本然后选择对应的PyTorch安装源。举个例子如果你显卡驱动只支持CUDA 11.2却强行装了cu118版本的PyTorch就会出现这个警告需要降低PyTorch版本重新安装。4.2 显存不足OOM问题BERT-base在batch_size32、seq_len512的情况下显存占用相当可观大概要11GB到13GB具体取决于你有没有开梯度检查。如果你手里的显卡只有8GB显存有四个方向可以调减小batch size是最直接的办法从32降到8或4显存占用是线性下降的。第二是减小max_lenBERT-pytorch的max_len默认512如果你的任务没有长文本降到128能省很多显存。第三是开启混合精度训练在代码里加一个gradscaler能把显存占用降低约40%。第四是使用梯度累积batch size调小后每若干步累积一次梯度再更新参数模拟大batch训练效果。我在3090上实测过seq_len512batch_size32训练速度大约是每秒3到4步一个epoch如果数据有10万条样本大概需要14小时以上。如果你只是做实验验证建议把最大长度降到128batch_size降到16速度能提升3倍左右。4.3 训练不收敛或loss为NaN的问题loss变成NaN是预训练里最让人崩溃的问题。我梳理了一下这么几个原因方向一是学习率过大BERT这类模型对学习率很敏感建议使用带warmup的Adam初始学习率在1e-4量级训练前期先用较小的学习率热身。二是数据里有空行或全为[MASK]的异常样本输入模型时容易产生梯度爆炸。三是LayerNorm和残差连接的数值稳定性问题如果你改了模型结构检查一下LayerNorm的参数初始化是否正确。loss一直不降或者降得很慢先确认自己是不是所有token都参与了loss计算。MLM任务只计算被mask位置上的loss如果你错误地计算了整个序列的loss模型会一直学那些简单的高频词导致mask位置的准确率上不去。这个检查方式很简单打印第一个batch的loss和mask数量做一个对比。4.4 训练与推理时行为不一致的问题一个我在项目issue里看到很多人问过的现象训练时loss正常下降但推理时输出全部是同一个词。这个最典型的原因是训练时开启了dropout而推理时没有切换model.eval()模式。PyTorch里dropout在train和eval模式下行为不同如果你训练完直接跑预测而不调用model.eval()dropout层仍然在随机置零输出自然不稳定。另外注意BERT-pytorch代码里embedding之后有dropoutattention层里有dropout这些在推理时都应该关闭。你可以在模型代码里找到所有nn.Dropout的位置确认推理路径上已经切到eval模式。5. 基于这个项目的扩展方向如果你已经把BERT-pytorch完整跑通了接下来有几条很自然的扩展路径。一条是换用更大的语料做真正的预训练。你可以收集领域文本数据按BERT格式预处理用这个项目跑出领域BERT模型。我有个朋友在医疗领域做过类似的实验用200万条病历文本预训练了一个小型BERT在下游的实体识别任务上比直接用通用BERT-base效果好很多。当然自己预训练很烧卡这个项目本身可以用单卡跑但如果你想要达到标准BERT-base的效果数据量和训练时长都得翻很多倍。另一条是做模型压缩或者蒸馏。BERT-pytorch结构清晰你可以在第6层和第12层之后分别接输出头做层间蒸馏把12层模型蒸馏成6层甚至3层。因为代码读起来明白改起来比在transformers库层面改要顺手得多。还有就是接入自己的下游任务。我在做文本相似度计算的时候就是在这个项目基础上加载预训练权重然后去掉NSP头换成两个句子的pooled输出拼接接一个全连接层回归出相似度分数。整个改造量没有多大但底层原理因为读过源码就非常清楚调试的时候定位问题也快得多。对比直接用transformers库这种“自己拼模型”的方式对理解整个训练链条的帮助是不可替代的。最后提一句如果你打算长期做NLP方向建议把BERT-pytorch的代码读三遍第一遍看整体流程第二遍用pdb或print把每步数据的shape打出来对照理论第三遍尝试自己动手改结构比如把BERT的Position Embedding改成相对位置编码。三遍走完市面上大部分BERT变种论文的实现你都能快速读懂了。我个人在这些年的工作中时常回去翻这个仓库的代码就是因为它简练、干净是学习Transformer结构一个不会过时的起点。本文还有配套的精品资源点击获取
返回列表