?參考視頻:BERT代碼(源碼)從零解讀【Pytorch-手把手教你從零實現一個BERT源碼模型】_嗶哩嗶哩_bilibili
一、BertTokenizer
BertTokenizer
是基于 WordPiece 算法的 BERT 分詞器,繼承自 PreTrainedTokenizer。
繼承的PretrainedTokenizer,具體操作的實現是在PretrainedTokenizer中的__call__執行。
1. 初始化 __init__
- 加載詞匯表文件
- 創建 token 到 ID 的映射
- 初始化基礎分詞器和 WordPiece 分詞器
# 加載詞匯表
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:
special_tokens = {"[PAD]": "填充短序列","[UNK]": "未知詞匯","[CLS]": "分類任務的開始符","[SEP]": "句子分隔符","[MASK]": "掩碼語言模型的掩碼符"
}
1.2 BasicTokenizer 基礎分詞
BasicTokenizer 負責將原始文本轉換為基礎的 token 序列。
主要參數:
def __init__(self,do_lower_case=True, # 是否轉換為小寫never_split=None, # 永不分割的詞匯集合tokenize_chinese_chars=True, # 是否處理中文字符strip_accents=None, # 是否去除重音符號do_split_on_punc=True, # 是否在標點符號處分割
):
主要分詞方法:
1. 清理文本:清除無效字符和空白字符
2. 處理中文字符(在字符周圍添加空格)
3. unicode 標準化
Unicode 標準化有四種形式:NFC,NFKC,NFD,NFKD:
unicode_normalized_text = unicodedata.normalize("NFC", text)
NFC:規范分解后再組合,將字符分解后重新組合成最緊湊的形式;
NFD:規范分解,將組合字符分解為基礎字符 + 組合標記;
NFKC:兼容性分解后再組合,更激進的標準化,會轉換兼容性字符;
NFKD:兼容性分解,最徹底的分解形式。
4. 按空格分割成token
5. 對分割后的token做小寫化和去重音
6. 再通過標點分割
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. 處理每個 tokensplit_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)問題
- 詞匯的組合性表示
主要參數:
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 # 單詞最大字符數限制
核心算法:最長貪心匹配
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 = Falsestart = 0sub_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 = substrbreakend -= 1# 7. 如果沒找到匹配,標記為失敗if cur_substr is None:is_bad = Truebreaksub_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轉換
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)
?4. 特殊Token處理
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
5. 特殊Token掩碼
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
?
?關鍵參數:
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 # 隨機種子
):
?
訓練