文本嵌入、文本分类和语义搜索

大邓和他的Python

共 11313字,需浏览 23分钟

 · 2024-04-27

在实践中使用大型语言模型(LLM)中,RAG 的一个关键部分是使用文本嵌入从知识库中自动检索相关信息。在这里,我将更深入地讨论文本嵌入,并分享两个简单(但功能强大)的应用:文本分类和语义搜索。

ChatGPT 吸引了全世界对人工智能及其潜力的想象。ChatGPT 的聊天界面是这一影响的关键因素,它使人工智能的力量比以往任何时候都更容易获得。

LLM 带来了文本嵌入方面重大创新。在此,我将解释这些创新,以及我们如何将它们用于简单但高价值的用例。

文本嵌入

文本嵌入将文字转化为数字。然而,这些数字并不是普通的数字。它们是能捕捉底层文本含义的数字。

这一点很重要,因为数字比文字更容易分析。

例如,如果在参加一个交流活动,想知道房间里的人的典型身高,您可以测量每个人的身高,然后用 Excel 计算出平均值。但是,如果你想知道房间里的人的典型职位,就没有 Excel 函数可以帮你了。

这就是文本嵌入可以提供帮助的地方。如果我们有了将文本转化为数字的好方法,我们就能打开现有统计和机器学习技术的巨大工具箱,来研究文本数据。

文本嵌入直观解释

来看一个直观的例子,以便更好地理解 "将文本转化为数字"的含义。

请看下面一组文字:莲花雏菊太阳土星木星卫星航天飞机篮球棒球

虽然这看起来像是随机的词语组合,但其中一些概念比其他概念更加相似。我们可以用下面的方式来表达这些相似之处(和不同之处)。

文本嵌入的可视化表示

上述可视化方式直观地组织了概念。相似的项目(如树、雏菊和莲花)往往靠得很近,而不相似的项目(如树和棒球)往往相距甚远。

我们可以根据每个单词在上图中的位置来为其分配坐标,因此数字也符合这种情况。这些坐标(即数字)可以用来分析底层文本

坐标可用于分析文本之间的关系

坐标从何而来?

将文本转化为数字以提高其可计算性并不是什么新想法。早在计算机诞生之初(约 1950 年),研究人员就开始了这方面的探索。

虽然多年来人们有无数种方法来实现这一目标,但如今,最先进的文本表示法都来自大型语言模型(LLM)。

这是因为LLM在训练过程中学习了(非常)好的文本数字表征。生成这些表示的层可以从模型中分离出来,以独立的方式使用。这一过程的结果就是文本嵌入模型。

创建嵌入模型的三步秘诀。

文本嵌入已经存在了几十年,是一种轻量级的、非随机的(即可预测的)技术。因此,使用它们构建人工智能系统要比构建人工智能代理简单得多,也便宜得多

文本分类

如何使用文本嵌入来解决实际问题。

文本分类包括给给定文本贴标签。例如,给电子邮件贴上垃圾邮件或非垃圾邮件的标签,给信贷申请贴上高风险或低风险的标签,或者给安全警报贴上真实交易或虚假警报的标签。

本例使用文本嵌入技术将简历分类为 "数据科学家"或 "非数据科学家",这可能与招聘人员试图浏览大量求职者的简历有关。

为了避免隐私问题,我使用gpt-3.5-turbo创建了一个简历合成数据集。虽然使用合成数据要求我们对结果持谨慎态度,但这个示例仍然为我们提供了如何使用文本嵌入进行分类的指导性示范。

首先要导入依赖项。在本例中,我使用了OpenAI的文本嵌入模型(也可以使用其它模型),这需要一个 API 密钥。在这里, API 密钥存储在一个名为sk.py 的单独文件中。

import openai
from sk import my_sk

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

接下来,以 Pandas Dataframe的形式读取合成训练数据集。数据来自一个 .csv 文件,其中有两列包含简历文本和相关角色。

df_resume = pd.read_csv('resumes/resumes_train.csv')

生成嵌入式数据

要将简历转化为嵌入式,可以对 OpenAI API 进行简单的 API 调用。

def generate_embeddings(text, my_sk):
    # set credentials
    client = openai.OpenAI(api_key = my_sk)
    
    # make api call
    response = client.embeddings.create(
        input=text,
        model="text-embedding-3-small"
    )
    
    # return text embedding
    return response.data

对数据帧中的每份简历应用该函数,并将结果存储在列表中。

# generate embeddings
text_embeddings = generate_embeddings(df_resume['resume'], my_sk)

# extract embeddings
text_embedding_list = 
  [text_embeddings[i].embedding for i in range(len(text_embeddings))]

在数据帧中存储嵌入

将创建一个新的数据帧来存储文本嵌入和用于模型训练的目标变量。

# define df column names
column_names = 
  ["embedding_" + str(i) for i in range(len(text_embedding_list[0]))]

# store text embeddings in dataframe
df_train = pd.DataFrame(text_embedding_list, columns=column_names)

# create target variable
df_train['is_data_scientist'] = df_resume['role']=="Data Scientist"

模型训练

准备好训练数据后,用一行代码来训练分类模型(随机森林分类器)。

# split variables by predictors and target
X = df_train.iloc[:,:-1]
y = df_train.iloc[:,-1]

# train rf model
clf = RandomForestClassifier(max_depth=2, random_state=0).fit(X, y)

模型评估

为了快速了解模型的性能,我们可以在训练数据上对其进行评估(计算平均准确率和 ROC 下面积(即AUC))。

# model accuracy for training data
print(clf.score(X,y))

# AUC value for training data
print(roc_auc_score(y, clf.predict_proba(X)[:,1]))

# output
# 1
# 1

准确率和 AUC 值均为 1 表示在训练数据集上表现完美,这一点很可疑。因此,让我们在模型从未见过的测试数据集上对其进行评估。

# import testing data
df_resume = pd.read_csv('resumes/resumes_test.csv')

# generate embeddings
text_embedding_list = generate_embeddings(df_resume['resume'], my_sk)
text_embedding_list = 
  [text_embedding_list[i].embedding for i in range(len(text_embedding_list))]

# store text embeddings in dataframe
df_test = pd.DataFrame(text_embedding_list, columns=column_names)

# create target variable
df_test['is_data_scientist'] = df_resume['role']=="Data Scientist"

# define predictors and target
X_test = df_test.iloc[:,:-1]
y_test = df_test.iloc[:,-1]
# model accuracy for testing data
print(clf.score(X_test,y_test))

# AUC value for testing data
print(roc_auc_score(y_test, clf.predict_proba(X_test)[:,1]))

# output
# 0.98
# 0.9983333333333333

解决过度拟合问题

虽然该模型在应用于测试数据时表现良好,但仍有可能出现过拟合,原因有二。

首先,我们有 1537 个预测因子,而需要预测的简历只有 100 份,因此模型要 "记住 "训练数据中的每个示例并不难。其二,训练数据和测试数据是以类似的方式从gpt-3.5-turbo生成的。因此,它们有许多共同特征,这使得分类任务比应用于真实数据更容易。

我们可以采用很多技巧来克服过拟合问题,例如,使用预测器重要性排序来减少预测器数量,增加叶节点中样本的最小数量,或使用逻辑回归等更简单的分类技术。不过,如果我们的目标是在实际环境中使用该模型,那么最好的选择就是收集更多数据并使用现实世界中的简历

语义搜索

接下来,让我们看看语义搜索。与基于关键字的搜索不同,语义搜索是根据用户查询的含义而不是所使用的特定单词或短语来生成结果的。

例如,基于关键字的搜索可能不会为 "我需要有人来构建我的数据基础设施"这一查询提供很好的结果,因为它没有具体提及构建数据基础设施的角色(即数据工程师)。然而,语义搜索却不会担心这个问题,因为它可以将该查询匹配到具有 "精通数据建模、ETL 流程和数据仓库"等经验的求职者

在这里,我们将使用文本嵌入技术在与上一个用例相同的数据集上进行这种类型的搜索。

首先导入依赖关系和合成数据集。

import numpy as np
import pandas as pd

from sentence_transformers import SentenceTransformer

from sklearn.decomposition import PCA
from sklearn.metrics import DistanceMetric

import matplotlib.pyplot as plt
import matplotlib as mpl
df_resume = pd.read_csv('resumes/resumes_train.csv')

# relabel random role as "other"
df_resume['role'][df_resume['role'].iloc[-1] == df_resume['role']] = "Other" 

生成嵌入

接下来,我们将生成文本嵌入。我们将不使用OpenAI API,而是使用Sentence TransformersPython 库中的一个开源模型。该模型专门针对语义搜索进行了微调。

# import pre-trained model (full list: https://www.sbert.net/docs/pretrained_models.html)
model = SentenceTransformer("all-MiniLM-L6-v2")

# encode text
embedding_arr = model.encode(df_resume['resume'])

要查看数据集中的不同简历及其在概念空间中的相对位置,我们可以使用PCA]来降低嵌入向量的维度,并将数据可视化为二维图。

# apply PCA to embeddings
pca = PCA(n_components=2).fit(embedding_arr)
print(pca.explained_variance_ratio_)

# plot data along PCA components
plt.figure(figsize=(86))
plt.rcParams.update({'font.size'16})
plt.grid()

c=0
cmap = mpl.colormaps['jet']
for role in df_resume['role'].unique():
    idx = np.where(df_resume['role']==role)
    plt.scatter(pca.transform(embedding_arr)[idx,0], pca.transform(embedding_arr)[idx,1], c=[cmap(c)]*len(idx[0]), label=role)
    c = c + 1/len(df_resume['role'].unique())
    
plt.legend(bbox_to_anchor=(1.050.9))
plt.xticks(rotation = 45)
plt.xlabel("PC 1")
plt.ylabel("PC 2")
plt.show()

从这个视图中,我们可以看到特定职位的简历往往聚集在一起。

按角色着色的简历嵌入二维图

语义搜索

现在,要对这些简历进行语义搜索,我们可以接受用户查询,将其转化为文本嵌入,然后返回嵌入空间中最近的简历。下面是代码的样子。

# define query
query = "I need someone to build out my data infrastructure"

# encode query
query_embedding = model.encode(query)
# define distance metric (other options: manhattan, chebyshev)
dist = DistanceMetric.get_metric('euclidean')

# compute pair-wise distances between query embedding and resume embeddings
dist_arr = 
  dist.pairwise(embedding_arr, query_embedding.reshape(1-1)).flatten()
# sort results
idist_arr_sorted = np.argsort(dist_arr)

打印前 10 名结果的角色,我们发现几乎都是数据工程师,这是一个好兆头。

# print roles of top 10 closest resumes to query in embedding space
print(df_resume['role'].iloc[idist_arr_sorted[:10]])

虽然这是一份虚构的简历,但应聘者很可能拥有满足用户需求的所有必要技能和经验。

另一种查看搜索结果的方法是通过之前的二维图。下面是一些查询的结果(见图表标题)。

# plot query along with resumes using PCA components
query_pca = pca.transform(query_embedding.reshape(1-1))[0]

plt.figure(figsize=(86))
plt.rcParams.update({'font.size'16})
plt.grid()

c=0
cmap = mpl.colormaps['jet']
for role in df_resume['role'].unique():
    idx = np.where(df_resume['role']==role)
    plt.scatter(pca.transform(embedding_arr)[idx,0], pca.transform(embedding_arr)[idx,1], c=[cmap(c)]*len(idx[0]), label=role)
    c = c + 1/len(df_resume['role'].unique())

plt.scatter(query_pca[0], query_pca[1], c='k', marker='*', s=750, label='query')
    
plt.legend(bbox_to_anchor=(1.050.9))
plt.xticks(rotation = 45)
plt.xlabel("PC 1")
plt.ylabel("PC 2")
plt.title('"' + query + '"')
plt.show()
3 种不同查询的二维 PCA 图。

改进搜索

虽然这个简单的搜索示例能很好地将特定候选人与给定查询进行匹配,但它并不完美。其中一个不足之处是当用户查询包含特定技能时。例如,在查询 "具有 Apache Airflow 经验的数据工程师"时,前 5 个结果中只有 1 个具有 Airflow 经验。

这突出表明,语义搜索并非在所有情况下都比基于关键字的搜索更好。两者各有优缺点。

因此,一个强大的搜索系统将采用所谓的混合搜索,将两种技术的优点结合起来。虽然设计这种系统的方法有很多,但一种简单的方法是应用基于关键字的搜索来筛选结果,然后再进行语义搜索。

另外两种改进搜索的策略是使用重新排名器(Reranker)和微调文本嵌入

Reranker是一种直接比较两段文本的模型。换句话说,Reranker 不是通过嵌入空间中的距离度量来计算文本之间的相似度,而是直接计算相似度得分。

Reranker 常用于完善搜索结果。例如,可以使用语义搜索返回前 25 个结果,然后使用重排器精选出前 5 个结果。

文本嵌入的微调包括针对特定领域调整嵌入模型。这是一种强大的方法,因为大多数嵌入模型都是基于广泛的文本和知识集合。因此,它们可能无法为特定行业(如数据科学和人工智能)优化组织概念。

🏴‍☠️宝藏级🏴‍☠️原创公众号『机器学习研习院』,公众号专注分享机器学习深度学习领域的原创文章,一起研习,共同进步!
长按👇关注- 机器学习研习院 - 设为星标,干货速递




精选内容

LIST | 社科(经管)可用数据集列表
LIST | 文本分析代码列表
LIST | 社科(经管)文本挖掘文献列表
管理科学学报 | 使用「软余弦相似度」测量业绩说明会「答非所问程度」
中国工业经济(更新) | MD&A信息含量指标构建代码实现
文献&代码 | 使用Python计算语义品牌评分(Semantic Brand Score)
数据集(更新) | 2001-2022年A股上市公司年报&管理层讨论与分析
数据集(更新) | 372w政府采购合同公告明细数据(2024.03)
数据集  | 人民网政府留言板原始文本(2011-2023.12)
数据集  |  人民日报/经济日报/光明日报 等 7 家新闻数据集
可视化 | 人民日报语料反映七十年文化演变
数据集 | 3571万条专利申请数据集(1985-2022年)
数据集 |  专利转让数据集(1985-2021)
数据集 |  3394w条豆瓣书评数据集
数据集 | 豆瓣电影影评数据集
数据集 |  使用1000w条豆瓣影评训练Word2Vec
代码 | 使用 3571w 专利申请数据集构造面板数据
代码 | 使用「新闻数据集」计算 「经济政策不确定性」指数
数据集 | 国省市三级gov工作报告文本
代码 | 使用「新闻数据」生成概念词频「面板数据」
代码 | 使用 3571w 专利申请数据集构造面板数据
代码 | 使用gov工作报告生成数字化词频「面板数据」


浏览 90
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报