在这一章节,我们将针对于BERT
的两个任务MLM
和NSP
构建可用于模型训练的数据集。
- 构建相邻语句预测数据集:
- 定义相似句预测任务,解释任务目标和构建数据集方法。
- 通过采集大量文本数据,利用句号等标点符号分割句子,构建标签为
1
的相邻句对和标签为0
的非相邻句对。 - 解决数据集中可能存在的无句号或超长句子问题,通过切分句子长度保证模型训练效果。
- Mask语言模型及其实现:
- Mask语言模型通过在文本中随机掩盖一定比例的词,强迫模型学习上下文以预测被掩盖的词,从而提高模型对语言的理解能力。
- 在实现Mask语言模型时,选择掩盖的词的比例、掩盖的方式(如直接掩盖12%、替换为特定标记符1.5%或随机替换为其他词1.5%)等参数对模型的训练效果有重要影响。
代码data_process
将处理data.csv
文件,并基于这些文本内容构建用于模型训练的数据集。
-
数据读取:
- 使用
read_data
函数从名为data.csv
的文件中读取数据,特别是其中的content
列,该列包含了一系列的文本内容。这些文本内容被提取并存储在一个列表中。
def read_data(): all_data = pd.read_csv(os.path.join("data.csv")) all_text = all_data["content"].tolist() return all_text
- 使用
-
文本分割:
split_text
函数负责将每个文本内容按照指定的标点符号(如中文的逗号、顿号、冒号、分号、句号、问号)进行分割,形成一系列的句子或短语。
def split_text(text):
patten = r"[,、:;。?]"
sp_text = re.split(patten, text)
new_sp_text = resplit_text(sp_text)
return new_sp_text
resplit_text
函数进一步对这些分割后的文本进行随机合并或单独保留,以形成新的文本片段列表。这个过程包括了一定的随机性,这里的随机性参考论文内容。
- 在每个输入序列中,随机选择15%的单词进行遮蔽。
- 对于被选择的每个单词,有80%的概率将其替换为[MASK]标记。
- 有10%的概率将其替换为一个随机选择的单词。
- 有10%的概率保持单词不变。
def resplit_text(text_list):
result = []
sentence = ""
for text in text_list:
if len(text) < 3:
continue
if sentence == "":
if random.random() < 0.2:
result.append(text + "。")
continue
if len(sentence) < 30 or random.random() < 0.2:
sentence += text + ","
else:
result.append(sentence[:-1] + "。")
sentence = text
return result
- 构建数据集:
build_neg_pos_data
函数用于生成正负样本对。对于给定的文本列表,它首先创建一个正样本对(相邻的两个文本),然后为每个正样本创建一个负样本对(其中一个文本与列表中随机选择的另一个文本配对)。正负样本的标签分别为1
和0
。第一个文本片段(句子A)使用A嵌入,第二个文本片段(句子B)使用B嵌入。50%的情况下,句子B是句子A之后的实际下文,即它们在原始文本中是连续的。另外50%的情况下,句子B是从语料库中随机选择的一个句子,与句子A没有实际的上下文关系。
def build_neg_pos_data(text_list): all_text1, all_text2 = [], [] all_label = [] for tidx, text in enumerate(text_list): if tidx == len(text_list) - 1: break all_text1.append(text) all_text2.append(text_list[tidx + 1]) all_label.append(1) c_id = [i for i in range(len(text_list)) if i != tidx and i != tidx + 1] other_idx = random.choice(c_id) other_text = text_list[other_idx] all_text1.append(text) all_text2.append(other_text) all_label.append(0) return all_text1, all_text2, all_label
build_task2_dataset
函数利用上述的文本分割和正负样本生成逻辑,为整个文本列表构建一个新的数据集,该数据集包含两个文本列(text1
和text2
)和一个标签列(label
),并将这些数据保存到一个新的CSV文件(task2.csv
)中。
def build_task2_dataset(text_list):
all_text1 = []
all_text2 = []
all_label = []
for text in tqdm(text_list):
sp_text = split_text(text)
if len(sp_text) <= 2:
continue
text1, text2, label = build_neg_pos_data(sp_text)
all_text1.extend(text1)
all_text2.extend(text2)
all_label.extend(label)
pd.DataFrame({"text1": all_text1, "text2": all_text2, "label": all_label}).to_csv(
os.path.join("..", "data", "task2.csv"), index=False)
- 构建词汇索引:
build_word_2_index
函数负责为所有文本内容构建一个词汇到索引的映射(word_2_index
)以及一个索引到词汇的映射(index_2_word
)。这个映射对于将文本转换为数字表示(即词嵌入)在后续的机器学习或深度学习模型中非常有用。
def build_word_2_index(all_text):
if os.path.exists("index_2_word.txt") == True:
with open("index_2_word.txt", encoding="utf-8") as f:
index_2_word = f.read().split("\n")
word_2_index = {w: idx for idx, w in enumerate(index_2_word)}
return word_2_index, index_2_word
word_2_index = {"[PAD]": 0, "[unused1]": 1, "[CLS]": 2, "[SEP]": 3, "[MASK]": 4, "[UNK]": 5, }
for text in all_text:
for w in text:
if w not in word_2_index:
word_2_index[w] = len(word_2_index)
index_2_word = list(word_2_index)
with open("index_2_word.txt", "w", encoding="utf-8") as f:
f.write("\n".join(index_2_word))
return word_2_index, index_2_word
- 如果
index_2_word.txt
文件已经存在,则直接从该文件中读取索引到词汇的映射,并构建词汇到索引的映射。如果不存在,则根据所有文本内容动态生成这两个映射,并将索引到词汇的映射保存到index_2_word.txt
文件中。
- 主函数:
- 在主函数中,首先读取文本数据,然后构建
task2
数据集,并最后为所有文本内容构建词汇索引。
- 在主函数中,首先读取文本数据,然后构建
all_text = read_data()
build_task2_dataset(all_text)
word_2_index = build_word_2_index(all_text)
print("")
至此,我们已经完成数据集的构建任务,并且将数据保存为index_2_word.txt
和task2.csv
。
附: 在BERT论文中,这些特殊标记具有以下含义:
-
"[PAD]":这是一个填充标记(Padding Token),用于将所有输入序列统一到相同的长度。在处理不等长的文本数据时,较短的序列会用[PAD]标记填充,以便能够批量处理。
-
"[unused1]":这个标记在BERT的实现中并不使用,它是一个保留的标记,可能用于未来的扩展或者特定用途,但在当前的BERT模型中没有被赋予具体功能。
-
"[CLS]":这是一个特殊的分类标记(Classification Token)。在BERT模型中,这个标记通常放在输入序列的开始位置。经过模型处理后,[CLS]标记的最终隐藏状态被用作整个输入序列的聚合表示,尤其在分类任务中。
-
"[SEP]":这是一个分隔标记(Separator Token),用于分隔输入序列中的不同部分,比如句子对中的句子。在句子对任务中,[SEP]标记用来区分两个句子的边界。
-
"[MASK]":这是一个掩码标记(Mask Token),用于BERT的预训练任务之一——Masked Language Model(MLM)。在MLM任务中,输入序列中的一些单词会被随机替换为[MASK]标记,模型的任务是预测这些被掩码的单词。
-
"[UNK]":这是一个未知词标记(Unknown Token),用于表示词汇表中不存在的单词。当输入文本中的单词不在模型的词汇表内时,会用[UNK]标记来代替。