参考视频:BERT代码(源码)从零解读【Pytorch-手把手教你从零实现一个BERT源码模型】_哔哩哔哩_bilibili
一、BertTokenizer
BertTokenizer
是基于 WordPiece 算法的 BERT 分词器,继承自 PreTrainedTokenizer。
继承的PretrainedTokenizer,具体操作的实现是在PretrainedTokenizer中的__call__执行。
1. 初始化 __init__
- 加载词汇表文件
- 创建 token 到 ID 的映射
- 初始化基础分词器和 WordPiece 分词器
python
# 加载词汇表
self.vocab = load_vocab(vocab_file)
# 创建反向映射 (ID -> token)
self.ids_to_tokens = collections.OrderedDict([(ids, tok) for tok, ids in self.vocab.items()])
# 初始化两个分词器
if do_basic_tokenize:
self.basic_tokenizer = BasicTokenizer(...) # 基础分词
self.wordpiece_tokenizer = WordpieceTokenizer(...) # WordPiece分词

1.1 加载词汇
Vocab 是 vocabulary(词汇表)的缩写,指的是模型能够理解和处理的所有词汇的集合。
有不同类型的vocab:

在BERT中,有几个特殊token:
python
special_tokens = {
"[PAD]": "填充短序列",
"[UNK]": "未知词汇",
"[CLS]": "分类任务的开始符",
"[SEP]": "句子分隔符",
"[MASK]": "掩码语言模型的掩码符"
}
1.2 BasicTokenizer 基础分词
BasicTokenizer 负责将原始文本转换为基础的 token 序列。
主要参数:
python
def __init__(
self,
do_lower_case=True, # 是否转换为小写
never_split=None, # 永不分割的词汇集合
tokenize_chinese_chars=True, # 是否处理中文字符
strip_accents=None, # 是否去除重音符号
do_split_on_punc=True, # 是否在标点符号处分割
):
主要分词方法:
- 清理文本:清除无效字符和空白字符

-
处理中文字符(在字符周围添加空格)
-
unicode 标准化
Unicode 标准化有四种形式:NFC,NFKC,NFD,NFKD:
pythonunicode_normalized_text = unicodedata.normalize("NFC", text)
NFC:规范分解后再组合,将字符分解后重新组合成最紧凑的形式;
NFD:规范分解,将组合字符分解为基础字符 + 组合标记;
NFKC:兼容性分解后再组合,更激进的标准化,会转换兼容性字符;
NFKD:兼容性分解,最彻底的分解形式。
-
按空格分割成token
-
对分割后的token做小写化和去重音
-
再通过标点分割
python
def tokenize(self, text, never_split=None):
# 处理流程:
# 1. 清理文本
text = self._clean_text(text)
# 2. 处理中文字符(在字符周围添加空格)
if self.tokenize_chinese_chars:
text = self._tokenize_chinese_chars(text)
# 3. Unicode 标准化
unicode_normalized_text = unicodedata.normalize("NFC", text)
# 4. 按空格分割
orig_tokens = whitespace_tokenize(unicode_normalized_text)
# 5. 处理每个 token
split_tokens = []
for token in orig_tokens:
if token not in never_split:
# 小写化和去重音
if self.do_lower_case:
token = token.lower()
if self.strip_accents is not False:
token = self._run_strip_accents(token)
elif self.strip_accents:
token = self._run_strip_accents(token)
# 6. 标点符号分割
split_tokens.extend(self._run_split_on_punc(token, never_split))
# 7. 最终清理
output_tokens = whitespace_tokenize(" ".join(split_tokens))
return output_tokens
1.3 WordPiece子词分词
WordPiece 是一种子词分词算法,它将单词分解为更小的、有意义的片段,以解决:
- 词汇表大小限制
- 未登录词(OOV)问题
- 词汇的组合性表示
主要参数:
python
def __init__(self, vocab, unk_token, max_input_chars_per_word=100):
self.vocab = vocab # 词汇表(字典)
self.unk_token = unk_token # 未知词标记,通常是 "[UNK]"
self.max_input_chars_per_word = 100 # 单词最大字符数限制
核心算法:最长贪心匹配

python
def tokenize(self, text):
output_tokens = []
# 1. 按空格分割文本(假设已经过 BasicTokenizer 处理)
for token in whitespace_tokenize(text):
chars = list(token)
# 2. 检查单词长度限制
if len(chars) > self.max_input_chars_per_word:
output_tokens.append(self.unk_token)
continue
# 3. 执行 WordPiece 分词
is_bad = False
start = 0
sub_tokens = []
while start < len(chars):
end = len(chars)
cur_substr = None
# 4. 贪心最长匹配
while start < end:
substr = "".join(chars[start:end])
# 5. 非首个子词添加 "##" 前缀
if start > 0:
substr = "##" + substr
# 6. 检查是否在词汇表中
if substr in self.vocab:
cur_substr = substr
break
end -= 1
# 7. 如果没找到匹配,标记为失败
if cur_substr is None:
is_bad = True
break
sub_tokens.append(cur_substr)
start = end
# 8. 根据结果添加到输出
if is_bad:
output_tokens.append(self.unk_token)
else:
output_tokens.extend(sub_tokens)
return output_tokens
2. 核心分词方法 tokenize
分两种情况,第一种do_basic_tokenize: 先基础分词在wordpiece;第二种直接wordpiece。

3. Token与ID转换
python
def _convert_token_to_id(self, token):
"""Token -> ID"""
return self.vocab.get(token, self.vocab.get(self.unk_token))
def _convert_id_to_token(self, index):
"""ID -> Token"""
return self.ids_to_tokens.get(index, self.unk_token)
- 特殊Token处理
python
def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None):
"""构建BERT输入格式"""
if token_ids_1 is None:
# 单句:[CLS] X [SEP]
return [self.cls_token_id] + token_ids_0 + [self.sep_token_id]
else:
# 句对:[CLS] A [SEP] B [SEP]
cls = [self.cls_token_id]
sep = [self.sep_token_id]
return cls + token_ids_0 + sep + token_ids_1 + sep
- 特殊Token掩码
python
def get_special_tokens_mask(self, token_ids_0, token_ids_1=None, already_has_special_tokens=False):
"""生成特殊token掩码:1表示特殊token,0表示普通token"""
if token_ids_1 is not None:
# 句对:[1] + [0]*len(A) + [1] + [0]*len(B) + [1]
return [1] + ([0] * len(token_ids_0)) + [1] + ([0] * len(token_ids_1)) + [1]
# 单句:[1] + [0]*len(X) + [1]
return [1] + ([0] * len(token_ids_0)) + [1]
二、DataFrame
Parquet文件
是一种列式存储文件格式,专为大数据分析和处理设计。
dfs 从数据集的单个文件读取内容;
df concat合并dfs读取到的两个文件内容;

三、Dataset


dataset.map()流程


set_format()

作用 :
指定数据集返回的数据格式为 PyTorch 张量(torch.Tensor
),并筛选需要保留的列(如 input_ids
、attention_mask
)
关键参数:
-
type
:目标格式(支持"torch"
、"numpy"
、"pandas"
等)。 -
columns
:保留的字段列表(其他字段将被隐藏,但不会删除)。 -
output_all_columns
:若为True
,即使未在columns
中指定,也会保留所有字段(默认为False
)。
为什么要这一步:
(1) 适配 PyTorch 训练
PyTorch 的
DataLoader
和模型输入要求张量格式,直接使用 NumPy/列表会报错。自动转换省去手动调用
torch.tensor()
的麻烦。(2) 减少内存占用
- 隐藏不需要的字段(如未使用的
token_type_ids
),避免数据加载时的冗余传输。(3) 灵活性
可随时切换格式(例如从
"torch"
改为"numpy"
)或重置为默认状态:重置为原始格式(Arrow)
tokenized_dataset.reset_format()
DataCollatorForLanguageModeling
DataCollatorForLanguageModeling
是 HuggingFace Transformers 库中专门为语言模型(如 BERT)训练设计的数据整理器,主要用于 动态生成掩码语言模型(MLM)任务所需的输入。
核心功能
(1) 动态掩码(Dynamic Masking)
mlm=True
:启用掩码语言模型任务,随机遮盖输入 Token(如 BERT 的预训练方式)。
mlm_probability=0.15
:每个 Token 有 15% 的概率被遮盖(原始 BERT 论文的设置)。其中:
80% 替换为
[MASK]
(如"hello" → "[MASK]"
)10% 替换为随机 Token(如
"hello" → "apple"
)10% 保持原 Token(如
"hello" → "hello"
)(2) 批量填充(Padding)
自动将一批样本填充到相同长度(根据
tokenizer.pad_token_id
)。生成
attention_mask
标记有效 Token。(3) 标签生成(Labels)
自动生成与
input_ids
长度相同的labels
,其中:被遮盖的 Token 位置:标注原始 Token ID(用于计算损失)。
未被遮盖的位置:标注为
-100
(PyTorch 中忽略损失计算)。
默认都是-100,在损失计算中设置忽略labels=-100的位置:
为什么需要**DataCollatorForLanguageModeling
?**

关键参数:
python
def __init__(
self,
tokenizer, # 必须传入的分词器实例
mlm=True, # 是否启用MLM任务
mlm_probability=0.15, # 单Token被掩码的概率
mask_replace_prob=0.8, # 掩码Token替换为[MASK]的比例
random_replace_prob=0.1, # 掩码Token替换为随机词的比例
pad_to_multiple_of=None, # 填充长度对齐(如8的倍数优化GPU显存)
return_tensors="pt", # 输出格式(pt/tf/np)
seed=None # 随机种子
):

训练
