编辑:AIWay 1024
我们首先来介绍下文本特征表述的历史。
Bag of Words:
获取词袋向量的第一步是将文本分割成单词(标记tokens),然后将单词还原为其基本形式。例如,"running "将转化为 "run"。这一过程称为词干处理。我们可以使用 NLTK Python 软件包来完成这一过程。
from nltk.stem import SnowballStemmer
from nltk.tokenize import word_tokenize
text = 'We are lucky to live in an age in which we are still making discoveries'
# tokenization - splitting text into words
words = word_tokenize(text)
print(words)
# ['We', 'are', 'lucky', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
# 'we', 'are', 'still', 'making', 'discoveries']
stemmer = SnowballStemmer(language = "english")
stemmed_words = list(map(lambda x: stemmer.stem(x), words))
print(stemmed_words)
# ['we', 'are', 'lucki', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
# 'we', 'are', 'still', 'make', 'discoveri']
现在,我们已经获取得到了所有单词的列表。下一步就是计算它们出现的频率,从而创建一个向量。
import collections
bag_of_words = collections.Counter(stemmed_words)
print(bag_of_words)
# {'we': 2, 'are': 2, 'in': 2, 'lucki': 1, 'to': 1, 'live': 1,
# 'an': 1, 'age': 1, 'which': 1, 'still': 1, 'make': 1, 'discoveri': 1}
事实上,如果我们想把文本转换成向量,就不仅要单单考虑文本中出现的单词,还要考虑整个词汇量。
这种方法非常基础,而且没有考虑到单词的语义,因此 "女孩正在学习数据科学 "和 "年轻女子正在学习人工智能和 ML "这两个句子获得的向量在向量空间并不接近。
TF - IDF:
TF-IDF(词频-反向文档频率)是词袋法的一个略有改进的版本。它是两个指标的乘积。
词频(Term Frequency 简称TF)显示的是该词在文档中出现的频率。最常见的计算方法是用该文档中单词的计数(如词袋中的计数)除以文档中单词的总数。不过,还有许多其他方法,如布尔 "频率 "和不同的归一化方法。
反向文档频率(Inverse Document Frequency)表示单词提供了多少信息。例如,单词 "a "或 "that "不会为大家提供任何有关文档主题的额外信息。相反,"ChatGPT "等词可以帮助大家定义句子所属领域。它的计算方法是文档总数与包含该词的文档总数之比的对数。IDF 越接近 0,说明该词越常见,提供的信息越少。
Word2Vec:
最著名的密集表示方法之一是 word2vec,它是由谷歌公司于 2013 年在 Mikolov 等人撰写的论文《Efficient Estimation of Word Representations in Vector Space》中提出的。
论文中提到了两种不同的 word2vec 方法:CBOW(我们根据周围的词来预测词)和Skip-gram(相反的任务--我们根据词来预测上下文)。
密集向量表示法的高级理念是训练两个模型:编码器和解码器。例如,在skip-gram的情况下,我们可能会将单词 "christmas "传递给编码器。然后,编码器会生成一个向量,我们将其传递给解码器,期望得到 "merry"、"to "和 "you "等词。
这个模型开始考虑单词的含义,因为它是根据单词的上下文信息进行训练的。但是,它忽略了词形(我们可以从单词部分获得的信息,例如,"-less "表示缺少某种东西)。后来,通过研究 GloVe 中子单词的skip-gram,我们解决了这一缺陷。此外,word2vec 只能处理单词,但我们希望对整个句子进行编码。因此,让我们继续下一步的进化,使用Transformers。
Transformers:
下一次演变与论文《Attention Is All You Need》中介绍的Transformer方法有关。Transformers能够产生信息密集的向量,并成为现代语言模型的主流技术。
Transformers允许大家使用相同的 "核心 "模型,并针对不同的用例对其进行微调,而无需重新训练核心模型(这需要花费大量时间和成本)。这导致了预训练模型的兴起。最早流行的模型之一是谷歌人工智能公司推出的 BERT。
从内部来看,BERT 仍在Token层面上运行,类似于 word2vec,但我们仍希望获得句子的嵌入。因此,最简单的方法就是取所有标记Token向量的平均值。遗憾的是,这种方法的效果并不好。
这个问题在 2019 年 Sentence-BERT 发布时得到了解决。它在语义文本相似性任务方面的表现优于以往所有方法,并可计算句子嵌入。
在本文中,我们将使用 OpenAI 嵌入。我们将试用发布的新模型 text-embedding-3-small。与 text-embedding-ada-002 相比,新模型的性能更好:
广泛使用的多语言检索(MIRACL)基准的平均得分从 31.4% 上升到 44.0%。
英语任务常用基准(MTEB)的平均成绩也有所提高,从 61.0%上升到 62.3%。
作为数据源,我们将使用一小部分 Stack Exchange Data Dump 样本--Stack Exchange 网络上所有用户贡献内容的匿名转储。我选择了一些我认为有趣的主题,并从每个主题中抽取了 100 个问题。主题范围从生成式人工智能到咖啡或自行车,因此我们将看到相当广泛的主题。
from openai import OpenAI
client = OpenAI()
def get_embedding(text, model="text-embedding-3-small"):
text = text.replace("\n", " ")
return client.embeddings.create(input = [text], model=model)\
.data[0].embedding
get_embedding("We are lucky to live in an age in which we are still making discoveries.")
欧氏距离
定义两点(或向量)间距离的最标准方法是欧氏距离或 L2 准则。这种度量方法在日常生活中最常用,例如,当我们谈论两个城镇之间的距离时。
下面是 L2 距离的直观表示和计算公式。
我们可以使用普通 Python 或利用 numpy 函数来计算这一指标。
import numpy as np
sum(list(map(lambda x, y: (x - y) ** 2, vector1, vector2))) ** 0.5
# 2.2361
np.linalg.norm((np.array(vector1) - np.array(vector2)), ord = 2)
# 2.2361
曼哈顿距离 (L1)
另一个常用的距离是 L1 准则或曼哈顿距离。这个距离是根据曼哈顿岛而命名的。曼哈顿岛的街道呈网格状布局,曼哈顿两点之间的最短路线将是 L1 距离,因为您需要沿着网格走。
我们也可以从头开始实现它,或者使用 numpy 函数。
sum(list(map(lambda x, y: abs(x - y), vector1, vector2)))
# 3
np.linalg.norm((np.array(vector1) - np.array(vector2)), ord = 1)
# 3.0
向量点积
查看向量间距离的另一种方法是计算点积。这里有一个公式,我们可以很容易地实现它。
sum(list(map(lambda x, y: x*y, vector1, vector2)))
# 11
np.dot(vector1, vector2)
# 11
(1, 1) vs (1, 1)
(1, 1) 对 (10, 10)
余弦相似性
通常使用余弦相似性。余弦相似度是以向量的大小(或法线)归一化的点积。
dot_product = sum(list(map(lambda x, y: x*y, vector1, vector2)))
norm_vector1 = sum(list(map(lambda x: x ** 2, vector1))) ** 0.5
norm_vector2 = sum(list(map(lambda x: x ** 2, vector2))) ** 0.5
dot_product/norm_vector1/norm_vector2
# 0.8575
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(
np.array(vector1).reshape(1, -1),
np.array(vector2).reshape(1, -1))[0][0]
# 0.8575
余弦相似度等于两个向量之间的余弦值。两个向量越接近,度量值就越高。
我们甚至可以计算出矢量之间的精确角度(单位:度)。我们得到的结果是 30 度左右,看起来非常合理。
import math
math.degrees(math.acos(0.8575))
# 30.96
了解数据的最佳方法是将其可视化。不幸的是,我们例子中嵌入向量有 1536 个维度,因此直接可视化查看数据非常具有挑战性。不过,还是有办法的:我们可以使用降维技术在二维空间中投射向量。
PCA
最基本的降维技术是 PCA(主成分分析)。让我们尝试使用它。
首先,我们需要将嵌入向量转换为二维 numpy 数组,然后将其传递给 sklearn。
import numpy as np
embeddings_array = np.array(df.embedding.values.tolist())
print(embeddings_array.shape)
# (1400, 1536)
然后,我们需要初始化一个 n_components = 2 的 PCA 模型(因为我们想创建一个二维可视化效果),在整个数据上训练模型并预测新值。
from sklearn.decomposition import PCA
pca_model = PCA(n_components = 2)
pca_model.fit(embeddings_array)
pca_embeddings_values = pca_model.transform(embeddings_array)
print(pca_embeddings_values.shape)
# (1400, 2)
结果,我们得到了一个矩阵,每个问题只有两个特征,因此我们可以很容易地将其可视化为散点图。
fig = px.scatter(
x = pca_embeddings_values[:,0],
y = pca_embeddings_values[:,1],
color = df.topic.values,
hover_name = df.full_text.values,
title = 'PCA embeddings', width = 800, height = 600,
color_discrete_sequence = plotly.colors.qualitative.Alphabet_r
)
fig.update_layout(
xaxis_title = 'first component',
yaxis_title = 'second component')
fig.show()
结果如下:
t-SNE
PCA 是一种线性算法,而现实生活中的大多数关系都是非线性的。因此,由于非线性的原因,我们可能无法分离聚类。让我们尝试使用非线性算法 t-SNE,看看它是否能显示出更好的结果。
代码几乎完全相同。我只是使用了 t-SNE 模型,而不是 PCA。
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, random_state=42)
tsne_embeddings_values = tsne_model.fit_transform(embeddings_array)
fig = px.scatter(
x = tsne_embeddings_values[:,0],
y = tsne_embeddings_values[:,1],
color = df.topic.values,
hover_name = df.full_text.values,
title = 't-SNE embeddings', width = 800, height = 600,
color_discrete_sequence = plotly.colors.qualitative.Alphabet_r
)
fig.update_layout(
xaxis_title = 'first component',
yaxis_title = 'second component')
fig.show()
t-SNE 的结果看起来要好得多。除了 "genai"、"datascience "和 "ai "之外,大多数聚类都被分开了。
Barcodes
理解嵌入向量的方法是将其中几个嵌入向量可视化为条形码,并查看相关性。我选取了三个嵌入的例子:两个最接近,另一个是数据集中最远的例子。
embedding1 = df.loc[1].embedding
embedding2 = df.loc[616].embedding
embedding3 = df.loc[749].embedding
import seaborn as sns
import matplotlib.pyplot as plt
embed_len_thr = 1536
sns.heatmap(np.array(embedding1[:embed_len_thr]).reshape(-1, embed_len_thr),
cmap = "Greys", center = 0, square = False,
xticklabels = False, cbar = False)
plt.gcf().set_size_inches(15,1)
plt.yticks([0.5], labels = ['AI'])
plt.show()
sns.heatmap(np.array(embedding3[:embed_len_thr]).reshape(-1, embed_len_thr),
cmap = "Greys", center = 0, square = False,
xticklabels = False, cbar = False)
plt.gcf().set_size_inches(15,1)
plt.yticks([0.5], labels = ['AI'])
plt.show()
sns.heatmap(np.array(embedding2[:embed_len_thr]).reshape(-1, embed_len_thr),
cmap = "Greys", center = 0, square = False,
xticklabels = False, cbar = False)
plt.gcf().set_size_inches(15,1)
plt.yticks([0.5], labels = ['Bioinformatics'])
plt.show()
聚类
让我们从聚类开始。聚类是一种无监督学习技术,可以在没有任何初始标签的情况下将数据分成若干组。聚类可以帮助你了解数据的内部结构模式。
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
import tqdm
silhouette_scores = []
for k in tqdm.tqdm(range(2, 51)):
kmeans = KMeans(n_clusters=k,
random_state=42,
n_init = 'auto').fit(embeddings_array)
kmeans_labels = kmeans.labels_
silhouette_scores.append(
{
'k': k,
'silhouette_score': silhouette_score(embeddings_array,
kmeans_labels, metric = 'cosine')
}
)
fig = px.line(pd.DataFrame(silhouette_scores).set_index('k'),
title = '<b>Silhouette scores for K-means clustering</b>',
labels = {'value': 'silhoutte score'},
color_discrete_sequence = plotly.colors.qualitative.Alphabet)
fig.update_layout(showlegend = False)
tsne_model = TSNE(n_components=2, random_state=42)
tsne_embeddings_values = tsne_model.fit_transform(embeddings_array)
fig = px.scatter(
x = tsne_embeddings_values[:,0],
y = tsne_embeddings_values[:,1],
color = list(map(lambda x: 'cluster %s' % x, kmeans_labels)),
hover_name = df.full_text.values,
title = 't-SNE embeddings for clustering', width = 800, height = 600,
color_discrete_sequence = plotly.colors.qualitative.Alphabet_r
)
fig.update_layout(
xaxis_title = 'first component',
yaxis_title = 'second component')
fig.show()
分类
我们可以将嵌入用于分类或回归任务。例如,您可以用它来预测客户评论的情感(分类)或 NPS 分数(回归)。由于分类和回归属于监督学习,因此需要有标签。幸运的是,我们知道了问题的主题,并可以拟合一个模型来预测它们。
为了正确评估分类模型的性能,我们将把数据集分成训练集和测试集(80% 对 20%)。然后,我们可以在训练集上训练随机森林模型,并在测试集(模型以前从未见过的问题)上测试模型效果。
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
class_model = RandomForestClassifier(max_depth = 10)
# defining features and target
X = embeddings_array
y = df.topic
# splitting data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state = 42, test_size=0.2, stratify=y
)
# fit & predict
class_model.fit(X_train, y_train)
y_pred = class_model.predict(X_test)
为了评估模型的性能,我们来计算一下混淆矩阵。在理想情况下,所有非对角元素都应为 0。
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred)
fig = px.imshow(
x = class_model.classes_,
y = class_model.classes_, text_auto='d',
aspect="auto",
labels=dict(
x="predicted label", y="true label",
color="cases"),
color_continuous_scale='pubugn',
title = '<b>Confusion matrix</b>', height = 550)
fig.show()
RAG
LLM 对上下文大小有限制(目前,GPT-4 Turbo 为 128K) 我们支付的是Tokens,所以一直传递所有信息的成本更高。
计算所有文档的嵌入,并将其存储在向量数据库中。 当我们收到一个用户请求时,我们可以计算出它的嵌入,并从向量数据库中检索出该请求的相关文档。 只将相关文件交给LLM,以获得最终答复。
首先,我们回顾了文本处理方法的演变。
最后,我们尝试在聚类、分类和 RAG 等不同的实际任务中使用嵌入作为特征。
希望大家通过本文,可以加深对文本嵌入的深入理解。
© THE END
转载请联系本公众号获得授权