微调特定于域的搜索的文本嵌入:附Python代码详解

# 微调特定于域的搜索的文本嵌入:附Python代码详解 📖阅读时长:20分钟 🕙发布时间:2025-02-02 >近日热文:[全网最全的神经网络数学原理(代码和公式)直观解释](https://mp.weixin.qq.com/s/ITFeM-RUVs9k9Kw4njl9KQ?token=992101443&lang=zh_CN) 欢迎关注知乎和公众号的专栏内容 [LLM架构专栏](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU5OTk5OTg4Ng==&action=getalbum&album_id=3803710040624594945#wechat_redirect) [知乎LLM专栏](https://zhuanlan.zhihu.com/column/c_1860259327224446976) [知乎【**柏企**】](https://www.zhihu.com/people/cbq-91) 公众号【**柏企科技说**】【**柏企阅文**】 嵌入模型将文本表示为具有语义意义的向量。尽管它们可以很容易地用于无数的用例(例如检索、分类),但通用嵌入模型在特定领域的任务上可能表现不佳。克服此限制的一种方法是微调。在本文中,我将讨论这项技术背后的关键思想,并分享一个微调嵌入以将查询与AI招聘信息匹配的具体示例。 ![](https://upload-images.jianshu.io/upload_images/17294212-d0985d4e5c9cccf3.png) 文本嵌入模型的一个常见用途是检索增强生成(RAG)。在这里,给定基于LLM的系统输入(例如客户问题),相关上下文(例如常见问题解答)会自动从知识库中检索并传递给LLM。 嵌入通过一个3步过程实现检索过程: 1. 向量表示形式(即嵌入向量)是针对知识库中的所有项目计算的。 2. 将输入文本转换为矢量表示形式(使用与步骤1中相同的嵌入模型)。 3. 计算输入文本向量与知识库中每个项目之间的相似性,并返回最相似的项目。 ![](https://upload-images.jianshu.io/upload_images/17294212-3d00f8db873e9b2e.png) 此过程(即语义搜索)提供了一种简单而灵活的方法来搜索任意文本项。但是,有一个问题。 ### 相似性搜索的问题 尽管语义搜索很受欢迎,但它有一个核心问题。也就是说,仅仅因为查询和知识库项相似(即它们关联的嵌入向量之间的角度很小),这并不一定意味着该项有助于回答查询。 例如,请考虑以下问题:“如何更新我的付款方式?”相似性搜索的顶部结果可能是:“要查看您的付款历史记录,请访问您帐户的Billing部分。”虽然它们在语义上相似,但结果并未提供有用的信息来回答问题。 ### 微调嵌入 解决此问题的一种方法是微调。这就是我们通过额外训练来调整嵌入模型行为的地方。 例如,我们可能希望将客户问题与常见问题解答中的相应答案进行匹配。这不仅需要将一小段文本(问题)与长文本(答案)相匹配,而且还可能涉及理解特定于领域的术语,例如,在云计算中,“扩展”和“实例”等术语具有非常具体的含义,通用模型可能无法适当地表示这些含义。 在这种情况下,微调嵌入模型涉及根据问题对及其适当的答案对其进行训练。做到这一点的关键方法是对比学习,它通过最小化相关对嵌入之间的距离同时最大化不相关的对嵌入之间的距离,教会模型区分有用和无用的结果。 ### 如何微调? 我们可以将微调过程分解为5个关键步骤: 1. 收集正(和负)对 2. 选择预训练模型 3. 选择损失函数 4. 微调模型 5. 评估模型 我不会抽象地讨论每个步骤,而是使用一个具体的例子来演示(和讨论)每个步骤。 ### 示例:微调AI Job Post上的嵌入 在这里,我将演练嵌入模型的微调,以将求职者与职位描述相匹配。正如我们将看到的,大多数步骤都是由sentence transformers库简化的。 #### 1. 收集正对 第一步是准备我们的训练数据。这是该过程中最重要(也是最耗时)的部分。 首先,我从这个Hugging Face数据集中提取了各种关键头衔(例如数据科学家、数据工程师、AI工程师)的职位描述。 ```python from datasets import load_dataset ds = load_dataset("datastax/linkedin_job_listings") ``` 接下来,我使用OpenAI的Batch API通过GPT-4o-mini生成与每个JD对应的类人搜索查询。Batch API需要24小时才能运行,但比立即完成要便宜50%。整个工作花了我0.12美元。(参见示例笔记本) 然后,我删除了与工作资格无关的描述的各个部分。这是一个重要的步骤,因为大多数文本嵌入模型无法处理超过512个标记。 为了生成正对,我将“清理”的JD与来自GPT-4o-mini的合成(类人)查询相匹配。然后,删除所有重复的行,从而产生1012个JD。 ![](https://upload-images.jianshu.io/upload_images/17294212-6fdf9a163d7f870d.png) 虽然我们可以到此为止,但我更进一步,为每个示例挑选出负对。我使用预先训练的嵌入模型来计算数据集中所有清理的JD之间的相似性。然后,对于每个正对,我挑选出与负数示例最不相似的JD(同时确保没有两行具有相同的负数示例)。 ```python from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer("all-mpnet-base-v2") job_embeddings = model.encode(df['job_description_pos'].to_list()) similarities = model.similarity(job_embeddings, job_embeddings) similarities_argsorted = np.argsort(similarities.numpy(), axis=1) negative_pair_index_list = [] for i in range(len(similarities)): j = 0 index = int(similarities_argsorted[i][j]) while index in negative_pair_index_list: j += 1 index = int(similarities_argsorted[i][j]) negative_pair_index_list.append(index) df['job_description_neg'] = df['job_description_pos'].iloc[negative_pair_index_list].values ``` 最后,我将数据拆分为train-validation-test集,并将它们上传到HuggingFace中心,这样就可以通过这个单行函数调用来访问它。 ```python df = df.sample(frac=1, random_state=42).reset_index(drop=True) train_frac = 0.8 valid_frac = 0.1 test_frac = 0.1 train_size = int(train_frac * len(df)) valid_size = int(valid_frac * len(df)) df_train = df[:train_size] df_valid = df[train_size:train_size + valid_size] df_test = df[train_size + valid_size:] from datasets import DatasetDict, Dataset train_ds = Dataset.from_pandas(df_train) valid_ds = Dataset.from_pandas(df_valid) test_ds = Dataset.from_pandas(df_test) dataset_dict = DatasetDict({ 'train': train_ds, 'validation': valid_ds, 'test': test_ds }) dataset_dict.push_to_hub("shawhin/ai-job-embedding-finetuning") ``` 我们可以使用一行代码导入生成的数据集。 ```python from datasets import load_dataset dataset = load_dataset("shawhin/ai-job-embedding-finetuning") ``` #### 2. 选择预训练模型 有了训练数据(终于),我们接下来选择一个预训练模型进行微调。我通过比较各种基础搜索模型和语义搜索模型来做到这一点。 为此,我创建了一个计算器,它采用我们的示例(查询、正JD、负JD)三元组并计算准确性。下面是验证集的样子。 ```python from sentence_transformers import SentenceTransformer from sentence_transformers.evaluation import TripletEvaluator model_name = "sentence-transformers/all-distilroberta-v1" model = SentenceTransformer(model_name) evaluator_valid = TripletEvaluator( anchors=dataset["validation"]["query"], positives=dataset["validation"]["job_description_pos"], negatives=dataset["validation"]["job_description_neg"], name="ai-job-validation", ) evaluator_valid(model) ``` 在比较了几个模型之后,我选择了“all-distilroberta-v1”,因为它在验证集上具有最高的准确性(在任何微调之前)。 #### 3. 选择一个损失函数 接下来,我们需要选择一个损失函数。这将取决于您的数据和下游任务 。sentence transformers doc中有一个很好的摘要表,其中列出了适当损失函数的各种数据格式。 在这里,我使用了MultipleNegativesRankingLoss,因为它与我们的(anchor, positive, negative)三元组格式匹配。 ```python from sentence_transformers.losses import MultipleNegativesRankingLoss loss = MultipleNegativesRankingLoss(model) ``` #### 4. 微调模型 数据、模型和损失函数准备就绪后,我们现在可以微调模型。为此,我们首先定义各种训练参数。 一个关键点是对比学习受益于更大的批量大小和训练时间 。为了简单起见,我使用了此处示例中显示的许多超参数。 ```python from sentence_transformers import SentenceTransformerTrainingArguments num_epochs = 1 batch_size = 16 lr = 2e-5 finetuned_model_name = "distilroberta-ai-job-embeddings" train_args = SentenceTransformerTrainingArguments( output_dir=f"models/{finetuned_model_name}", num_train_epochs=num_epochs, per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, learning_rate=lr, warmup_ratio=0.1, batch_sampler=BatchSamplers.NO_DUPLICATES, eval_strategy="steps", eval_steps=100, logging_steps=100, ) ``` 接下来,我们训练模型。我们可以通过SentenceTransformerTrainer轻松完成此作。 ```python from sentence_transformers import SentenceTransformerTrainer trainer = SentenceTransformerTrainer( model=model, args=train_args, train_dataset=dataset["train"], eval_dataset=dataset["validation"], loss=loss, evaluator=evaluator_valid, ) trainer.train() ``` #### 5. 评估模型 最后,我们可以像在第2步中评估预训练模型一样评估微调后的模型。结果显示验证集的准确率为99%,测试集的准确率为100%。 作为可选步骤,我们可以将模型推送到Hugging Face Hub,以便轻松导入以进行推理。 ```python model.push_to_hub(f"shawhin/{finetuned_model_name}") model = SentenceTransformer("shawhin/distilroberta-ai-job-embeddings") query = "data scientist 6 year experience, LLMs, credit risk, content marketing" query_embedding = model.encode(query) jd_embeddings = model.encode(dataset["test"]["job_description_pos"]) similarities = model.similarity(query_embedding, jd_embeddings) ``` 后续我们会持续带来更多相关技术的深度解析和实践案例,敬请关注公众号 **柏企科技圈** 和 **柏企阅文** 本文由[mdnice](https://mdnice.com/?platform=6)多平台发布
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容