探索ModernBERT:传统BERT模型的重大升级
这次我们聚焦于ModernBERT,看看它是如何强化上下文嵌入的应用。我们还会讲讲如何生成用于微调的数据集,并展示怎样对ModernBERT进行微调,从而在自然语言处理(NLP)任务中取得更强大的效果。
嵌入在机器学习和NLP中的重要性
嵌入是机器学习和NLP里的关键概念,它能把复杂数据(像单词、短语或产品信息)转化为数字向量表示形式。这些向量可以捕捉实体之间的语义和句法关系,在密集的低维空间里,将相似的实体映射得更近。和把实体单独看待的one - hot编码不同,嵌入能让模型理解大型数据集中的模式和共现情况。
嵌入对于检索、推荐和分类等任务至关重要。在检索时,通过比较查询和文档的向量表示,系统就能找到语义相关的项目;在推荐系统中,嵌入会把有相似交互行为的用户和产品映射得更紧密,优化个性化推荐;分类任务里,嵌入能帮助模型理解更高级的概念,让模型基于含义而非仅仅是关键词做出更精准的预测。总体来说,嵌入就像是在非结构化数据和机器学习模型之间架起了一座桥梁,提升了各种任务的执行性能。
上下文嵌入
上下文嵌入是单词或标记的动态表示,不像静态嵌入那样,不管上下文是什么,都给一个单词分配固定向量。上下文嵌入会根据单词周围的文本,为同一个单词生成不同的向量,这样就能捕捉到句子或文档里单词的细微含义和用法差异。这是通过考虑整个输入序列来实现的。
捕捉上下文非常重要,因为它可以消除语言中的歧义和细微差别。一个单词往往有多种含义(比如“apple”,可以是“苹果”,也可能指“苹果公司”;“mouse”,既可以是“老鼠”,也能表示“鼠标” ),要理解其确切含义就得依靠上下文。与仅仅捕捉字面意思的静态模型相比,上下文嵌入能让模型理解这些复杂关系,从而更准确地表示文本内容。
在需要深入语义理解的任务中,上下文嵌入的表现总是优于静态嵌入。比如命名实体识别(NER),它能区分“Apple”到底指的是公司还是水果;问答任务,需要在正确的上下文里理解问题的细微差别;情感分析,以及机器翻译等。上下文嵌入提供的上下文敏感表示,让模型能实现类似人类的理解水平,这是它相较于静态模型的显著优势。
上下文嵌入 vs. 普通嵌入
普通(传统)嵌入是静态的,也就是说每个单词或实体都被赋予一个固定的向量表示,不考虑其所处的上下文。常见的例子有Word2Vec、GloVe和FastText。这些模型依据大量文本语料库中的共现模式,为词汇表里的每个单词生成一个向量。虽然普通嵌入能捕捉到一些语义关系(比如,“king”在语义上比“apple”更接近“queen” ),但它无法根据单词使用的上下文来解释其含义的变化。例如,单词“bat”,不管是指飞行的哺乳动物还是运动器材,它的向量表示都是一样的。
而上下文嵌入则会根据单词周围的上下文,为每个单词或实体动态生成唯一的向量表示。这些嵌入是通过深度学习模型生成的,像BERT、ELMo和RoBERTa等,这些模型会在句子或文档的上下文中处理单词。在这些模型里,同一个单词因为周围单词的不同,会有不同的表示形式。比如,“bank”这个词,在“river bank”(河岸)和“financial bank”(金融银行)这两个短语里的嵌入就截然不同。这种捕捉上下文的能力,让上下文嵌入更加通用和准确,在问答、机器翻译和情感分析等复杂的NLP任务中优势明显。
主要区别一目了然
普通嵌入基于词频和共现情况提供通用的表示,而上下文嵌入则提供更细致、动态的表示,这种表示对单词出现的特定上下文很敏感,在各种NLP应用中都能实现更好的性能。
ModernBERT
ModernBERT中的新增功能是什么?
ModernBERT引入了好几个高级功能,有效提升了它利用上下文嵌入的能力。其中一项关键创新是用旋转位置嵌入(RoPE)替代了传统的位置编码。RoPE能让模型更好地理解单词在序列中的相对位置,这对于处理较长的上下文至关重要。通过整合RoPE,ModernBERT可以处理长度高达8192个令牌的序列,极大地提升了处理长篇文档或代码的能力。这一特性解决了早期模型(比如原始BERT)的局限性之一,原始BERT的上下文窗口要短得多。
ModernBERT通过引入GeGLU层改进了标准的GeLU激活函数。这些层增强了模型的表达能力和效率,使其能够学习数据中更复杂的关系。该架构还受益于简化设计,去除了不必要的偏差项,更合理地利用模型的参数预算。此外,在嵌入之后添加了一个额外的归一化层,进一步稳定了训练过程,确保模型更可靠地收敛。
ModernBERT的一个突出特点是使用交替注意力机制。随着输入序列长度增加,完全依赖全局注意力的计算成本会变得很高,而ModernBERT则在全局注意力(每三层使用一次)和局部注意力(128个标记的滑动窗口)之间交替使用。这样一来,模型在处理长输入时速度更快,同时还不会降低嵌入的质量。从概念上讲,这种方法模仿了人类处理文本的方式——必要时理解更广泛的上下文,但大部分时候关注周围的文本。这大大降低了计算复杂性,使ModernBERT比传统模型更高效地处理长序列。
此外,ModernBERT还融入了Unpadding和Sequence Packing这两种技术,提高了计算效率。填充操作通常用于对齐不同长度的序列,但会浪费宝贵的计算资源。ModernBERT在处理过程中完全去除填充标记,以此解决这个问题,尤其在结合Flash Attention技术时,速度提升明显。序列打包技术通过将序列分组为串联批次,充分利用GPU的并行处理能力,进一步优化了训练过程,加快了预训练速度。
与传统模型和其他先进的嵌入方法相比,ModernBERT的上下文嵌入得益于这些创新,不仅速度更快,还能更好地捕捉数据中长上下文和复杂关系的微妙之处。这些特性共同提升了ModernBERT生成更准确、具有上下文感知能力的嵌入的能力,在各种NLP任务中都提升了性能表现。
ModernBERT对检索系统的影响
相较于其他编码器模型,ModernBERT显著提升了检索系统的速度和准确性。它能够处理多达8192个令牌,这使得它在处理长文档或查询时不会丢失上下文信息,这对于需要理解大段文本的检索任务来说至关重要。这种长上下文处理能力与它的交替注意力机制相结合,每三层就结合一次局部(128个令牌)和全局注意力,提高了计算效率和可扩展性。
在基准测试性能方面,在GLUE、CSN、SQA等基准测试中,ModernBERT的表现优于DeBERTaV3、NomicBERT等模型,充分展示了其卓越的语言理解能力。这直接增强了它在检索任务中检索上下文相关文档的能力。此外,在检索增强生成(RAG)流程中,ModernBERT通过生成用于文档检索的高质量嵌入,表现出色,有助于生成更准确、连贯的内容。
生成用于微调的数据
为了微调modernbert - embed - base模型,拥有高质量的训练数据至关重要。如果你使用Langfuse或Langsmith等可观测性工具,从基于大语言模型(LLM)的应用程序中收集到了真实的交互数据,那自然是再好不过。但要是没有这类数据,也可以像我们一样生成合成数据。
我们的方法是利用大语言模型从文档中提取问题 - 上下文对。这个结构化数据集遵循以下格式:
Dataset({
features: ['query', 'answer'],
num_rows: 600
})
每个条目都构成一个正对,也就是说查询和答案在语义上是相关的。这可以包括(查询,响应)对、释义、摘要、重复问题,甚至是自然语言推理(NLI)对。不管是使用真实数据还是合成数据,这一步都能确保我们的模型为检索任务学习到有意义的上下文表示。
代码时间
环境设置
我们要安装用于嵌入生成、基于LLM的应用程序以及数据集处理的必要库。
!pip install sentence - transformers llama - index llama - index - llms - gemini datasets transformers==4.48.0
此外,还需要配置用于API访问和报告的环境变量。
import os
GOOGLE_API_KEY = "xxxxxxxxxxxx"
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
os.environ["WANDB_DISABLED"] = "true"
导入必要的库
引入llama - index、datasets和pandas等库,用于数据处理、数据集创建和操作。同时导入Gemini模型,用于从文档中生成问题和上下文。
from llama_index.core.evaluation import generate_question_context_pairs
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.llms.gemini import Gemini
from llama_index.core import Settings
from datasets import Dataset
import pandas as pd
设置LLM
这里我们从llama - index配置Gemini模型,这个模型将用于生成问题 - 上下文对。加载模型并将其分配给Settings.llm,确保它能在后续的数据生成过程中正常使用。
llm = Gemini(
model="models/gemini-1.5-flash",
)
Settings.llm = llm
生成问题 - 上下文对
create_qa_dataset函数用于处理指定目录下的文档,将其拆分成较小的块,并生成问题 - 上下文对。它首先加载文档,然后使用SentenceSplitter将文档拆分成块。该函数利用generate_question_context_pairs方法,从这些块中创建问题和相关的上下文。
def create_qa_dataset(input_dir, llm, num_questions_per_chunk):
documents = SimpleDirectoryReader(input_dir=input_dir).load_data()
node_parser = SentenceSplitter(chunk_size=256)
nodes = node_parser.get_nodes_from_documents(documents)
qa_dataset = generate_question_context_pairs(
nodes,
llm=llm,
num_questions_per_chunk=num_questions_per_chunk
)
queries = qa_dataset.queries.values()
contexts = [doc.get_content() for doc in nodes]
return list(queries), contexts
调用create_qa_dataset函数来生成问题 - 上下文对。
展平数据并创建DataFrame
queries, contexts = create_qa_dataset("/content/train_data", llm, 2)
然后,将数据扁平化为简单的查询 - 上下文结构,每个查询都与相应的上下文配对。
data = {
"query": [],
"context": []
}
for i, context in enumerate(contexts):
for query in queries[i * 2: i * 2 + 2]:
data["query"].append(query)
data["context"].append(context)
df = pd.DataFrame(data)
df.to_csv("train_data.csv", index=False)
结果会被整理成一个DataFrame,并保存为CSV文件,以便进一步处理。
将数据拆分为训练集和测试集
在这里,数据被加载到Hugging Face Dataset中,按80 - 20的比例拆分为训练集和测试集,并打印数据集大小进行验证。
hf_dataset = Dataset.from_pandas(df).train_test_split(test_size=0.2)
train_dataset = hf_dataset["train"]
test_dataset = hf_dataset["test"]
print(f"Train dataset size: {len(train_dataset)}")
print(f"Test dataset size: {len(test_dataset)}")
加载模型和损失函数
使用SentenceTransformer库加载ModernBERT模型。会自动选择设备(GPU或CPU),并将MultipleNegativesRankingLoss设置为微调的损失函数,这个损失函数适用于语义相似度等任务。
import torch
from sentence_transformers.losses import MultipleNegativesRankingLoss
from sentence_transformers import SentenceTransformer
model_name = "nomic-ai/modernbert-embed-base"
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentenceTransformer(model_name, device=device)
loss = MultipleNegativesRankingLoss(model)
设置训练参数
这部分代码定义了微调模型的训练参数,包括训练轮数、批次大小、学习率和评估策略等。
from sentence_transformers import SentenceTransformerTrainingArguments, SentenceTransformerTrainer
args = SentenceTransformerTrainingArguments(
output_dir="output",
num_train_epochs=1,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
learning_rate=2e-5,
warmup_ratio=0.1,
fp16=False,
eval_strategy="steps",
eval_steps=0.125,
save_strategy="steps",
save_steps=0,
save_total_limit=2,
logging_steps=1,
report_to=None
)
fp16=False
这个设置确保训练使用32位浮点精度(fp32)。output_dir
指定了训练过程中模型输出的保存位置。
初始化并运行训练器
创建一个SentenceTransformerTrainer实例,传入模型、训练参数、数据集、分词器和损失函数。训练器会负责训练循环。
trainer = SentenceTransformerTrainer(
model=model,
args=args,
train_dataset=train_dataset,
eval_dataset=test_dataset,
tokenizer=model,
loss=loss,
)
trainer.train()
这条命令会启动微调过程,使用之前定义好的参数和数据集进行训练。
trainer.train()
推荐阅读
1. DeepSeek-R1的顿悟时刻是如何出现的? 背后的数学原理
2. 微调 DeepSeek LLM:使用监督微调(SFT)与 Hugging Face 数据
3. 使用 DeepSeek-R1 等推理模型将 RAG 转换为 RAT
4. DeepSeek R1:了解GRPO和多阶段训练
5. 深度探索:DeepSeek-R1 如何从零开始训练
6. DeepSeek 发布 Janus Pro 7B 多模态模型,免费又强大!
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。