我有一个机器学习问题,需要计算pandas数据框文本列与字典值的二元Jaccard相似度。目前我将它们存储为列表,然后转换为列。这在生产环境中证明非常慢。有没有更有效的方法来做这件事?
以下是我目前遵循的步骤:对于字典中的每个键:1. 获取pandas列和dict[key]的二元组2. 计算Jaccard相似度3. 追加到一个空列表4. 将列表存储在数据框中5. 将列表转换为列
from itertools import tee, islicedef count_ngrams(lst, n): tlst = lst while True: a, b = tee(tlst) l = tuple(islice(a, n)) if len(l) == n: yield l next(b) tlst = b else: breakdef n_gram_jaccard_similarity(str1, str2,n): a = set(count_ngrams(str1.split(),n)) b = set(count_ngrams(str2.split(),n)) intersection = a.intersection(b) union = a.union(b) try: return len(intersection) / float(len(union)) except: return np.nandef jc_list(sample_dict,row,n): sim_list = [] for key in sample_dict: sim_list.append(n_gram_jaccard_similarity(sample_dict[key],row["text"],n)) return str(sim_list)
使用上述函数构建二元Jaccard相似度特征如下:
df["bigram_jaccard_similarity"]=df.apply(lambda row: jc_list(sample_dict,row,2),axis=1)df["bigram_jaccard_similarity"] = df["bigram_jaccard_similarity"].map(lambda x:[float(i) for i in [a for a in [s.replace(',','').replace(']', '').replace('[','') for s in x.split()] if a!='']])df[[i for i in sample_dict]] = pd.DataFrame(df["bigram_jaccard_similarity"].values.tolist(), index= df.index)
样本输入:
df = pd.DataFrame(columns=["id","text"],index=None)df.loc[0] = ["1","this is a sample text"]import collectionssample_dict = collections.defaultdict()sample_dict["r1"] = "this is sample 1" sample_dict["r2"] = "is sample" sample_dict["r3"] = "sample text 2"
预期输出:
回答:
这件事比我想象的要难,因为稀疏矩阵的广播问题。另外,在短时间内我无法完全向量化它。
我在框架中添加了一行额外的文本:
df = pd.DataFrame(columns=["id","text"],index=None)df.loc[0] = ["1","this is a sample text"]df.loc[1] = ["2","this is a second sample text"]import collectionssample_dict = collections.defaultdict()sample_dict["r1"] = "this is sample 1" sample_dict["r2"] = "is sample" sample_dict["r3"] = "sample text 2"
我们将使用以下模块/函数/类:
from sklearn.feature_extraction.text import CountVectorizerfrom scipy.sparse import csr_matriximport numpy as np
并定义一个CountVectorizer来创建基于字符的n_grams
ngram_vectorizer = CountVectorizer(ngram_range=(2, 2), analyzer="char")
你可以自由选择你需要的n-grams。我建议使用现有的分词器和n-gram创建器。你应该能找到很多这样的工具。CountVectorizer也可以进行广泛的调整(例如转换为小写,消除空格等)。
我们连接所有数据:
all_data = np.concatenate((df.text.to_numpy(),np.array(list(sample_dict.values()))))
我们这样做,因为我们的向量化器需要为所有出现的标记提供一个共同的索引方案。
现在让我们拟合Count向量化器并相应地转换数据:
ngrammed = ngram_vectorizer.fit_transform(all_data) >0
ngrammed
现在是一个稀疏矩阵,包含了在各自行中出现的标记的标识符,不再是之前的计数。你可以检查ngram_vecotrizer
并找到从标记到列ID的映射。
接下来,我们想将样本字典中的每个grammes条目与我们的ngrammed文本数据的每一行进行比较。我们需要在这里使用一些魔法:
texts = ngrammed[:len(df)]samples = ngrammed[len(df):]text_rows = len(df)jaccard_similarities = []for key, ngram_sample in zip(sample_dict.keys(), samples): repeated_row_matrix = (csr_matrix(np.ones([text_rows,1])) * ngram_sample).astype(bool) support = texts.maximum(repeated_row_matrix) intersection = texts.multiply(repeated_row_matrix).todense() jaccard_similarities.append(pd.Series((intersection.sum(axis=1)/support.sum(axis=1)).A1, name=key))
support
是一个布尔数组,用于测量两个可比较对象的n-grams的并集。intersection
只有在标记同时存在于两个可比较对象中时才为True。请注意,.A1
表示底层基本数组的matrix
对象。
现在
pd.concat(jaccard_similarities, axis=1)
得到
r1 r2 r30 0.631579 0.444444 0.5000001 0.480000 0.333333 0.384615
你也可以将它连接到df
,并通过
pd.concat([df, pd.concat(jaccard_similarities, axis=1)], axis=1) id text r1 r2 r30 1 this is a sample text 0.631579 0.444444 0.5000001 2 this is a second sample text 0.480000 0.333333 0.384615