将分类特征的嵌入提取回 Python 中的原始数据框

Extracting embeddings of categorical features back to original data frame in Python

假设我有一个包含多个数值变量和 1 个包含 10000 个类别的分类变量的数据框。我使用带有 Keras 的神经网络来获取分类变量的嵌入矩阵。嵌入大小为 50,因此 Keras returns 的矩阵维度为 10002 x 50.
额外的 2 行用于未知类别,另一行我不完全知道 - 这是 Keras 工作的唯一方式,即

model_i = keras.layers.Embedding(input_dim=num_categories+2, output_dim=embedding_size, input_length=1,
                            name=f'embedding_{cat_feature}')(input_i)

没有 +2 它将无法工作。

所以,我有一个约 1200 万行的训练集和约 100 万行的验证集。 现在,我想到的重构嵌入的方式是:

  1. 有一个反向字典,其中数值(之前被编码以表示类别)作为键,类别名称作为值
  2. 向数据框添加 50 NaN
  3. for i in range(10002)(即类别数+2)在反向字典中查找key i对应的值,如果在字典中,使用 pandas .loc,替换与 i 的值对应的每一行(在这 50 个 NaN 列中)(即,分类变量等于类别名称的位置其中i被编码)与来自10002 x 50矩阵的相应行向量。

此解决方案的问题是效率极低。
一位朋友告诉我另一种解决方案,它包括将分类变量转换为维度为 12M x 10000 的单热稀疏矩阵(用于训练集),然后使用矩阵乘法与维度为 [=] 的嵌入矩阵23=] 从而得到一个 12M x 50 矩阵,然后我可以将其连接到我的原始数据框。这里的问题是:

  1. 它不适用于验证集,因为出现的类别数量与训练中不同,或者可能不同,因此维度不匹配。
  2. 即使在训练集上使用时,Keras 给我的矩阵中也有 10002 (=num_categories + 2) 行,而不是 10000 行。同样,维度不匹配。

有没有人知道更好的方法或者可以解决第二种方法中的问题?
我的最终目标是拥有一个包含所有变量 减去分类变量 的数据框,取而代之的是另外 50 列包含表示该分类变量嵌入的行向量。

这是我在评论里介绍的

df = pd.DataFrame({'int':np.random.uniform(0,1, 10),'cat':np.random.randint(0,333, 10)}) # cat are encoded

## define embedding model, you can also use multiple input source
inp = Input((1))
emb = Embedding(input_dim=10000+2, output_dim=50, name='embedding')(inp)
out = Dense(10)(emb)
model = Model(inp, out)
# model.compile(...)
# model.fit(...)

## get cat embeddings
extractor = Model(model.input, Flatten()(model.get_layer('embedding').output))
## concat embedding in the orgiginal df
df = pd.concat([df, pd.DataFrame(extractor.predict(df.cat.values))], axis=1)
df

所以最终我找到了我post中提到的第二种方法的解决方案。使用稀疏矩阵可以避免在尝试将矩阵与大数据(类别 and/or 观察值)相乘时可能出现的内存问题。
我编写了这个函数,其中 returns 原始数据框附加了所有所需的分类变量的嵌入向量。

def get_embeddings(model: keras.models.Model, cat_vars: List[str], df: pd.DataFrame,
                   dict: Dict[str, Dict[str, int]]) -> pd.DataFrame:

    df_list: List[pd.DataFrame] = [df]

    for var_name in cat_vars:
        df_1vec: pd.DataFrame = df.loc[:, var_name]
        enc = OneHotEncoder()
        sparse_mat = enc.fit_transform(df_1vec.values.reshape(-1, 1))
        sparse_mat = sparse.csr_matrix(sparse_mat, dtype='uint8')

        orig_dict = dict[var_name]

        match_to_arr = np.empty(
            (sparse_mat.shape[1], model.get_layer(f'embedding_{var_name}').get_weights()[0].shape[1]))
        match_to_arr[:] = np.nan

        unknown_cat = model.get_layer(f'embedding_{var_name}').get_weights()[0].shape[0] - 1

        for i, col in enumerate(tqdm.tqdm(enc.categories_[0])):
            if col in orig_dict.keys():
                val = orig_dict[col]
                match_to_arr[i, :] = model.get_layer(f'embedding_{var_name}').get_weights()[0][val, :]
            else:
                match_to_arr[i, :] = (model.get_layer(f'embedding_{var_name}')
                                                .get_weights()[0][unknown_cat, :])

        a = sparse_mat.dot(match_to_arr)
        a = pd.DataFrame(a, columns=[f'{var_name}_{i}' for i in range(1, match_to_arr.shape[1] + 1)])
        df_list.append(a)

    df_final = pd.concat(df_list, axis=1)
    return df_final

dict 是字典的字典,即,为我预先编码的每个分类变量保存一个字典,键是类别名称,值是整数。请注意,每个类别都用 num_values + 1 编码,最后一个类别保留给未知类别。

基本上我正在做的是询问每个类别值是否在字典中。如果是,我将临时数组中的相应行(因此,如果这是第一个类别,则为第一行)分配给嵌入矩阵中的相应行,其中行号对应于类别名称被编码为的值. 如果它不在字典中,那么我将嵌入矩阵中对应于未知类别的最后一行分配给这一行(this = ith row)。