tensorflow本身的实现是带batch_size的,这里主要看单条样本怎么做的。

假设有两个特征年龄和性别,年龄数数值特征,性别是类别特征。

对年龄先分桶,然后和性别做交叉,大概实现如下。

假设年龄是0~100岁,分10个桶,其中几个年龄数值和分桶的对应关系:

age   bucketized_age
27岁  17
28岁  18 
29岁  19 
30岁  20

对性别的取值(字符串):

男  "1"
女  "2"

特征交叉python代码

age = tf.feature_column.numeric_column('age')
bucketized_age = tf.feature_column.bucketized_column(age, boundaries=[0,10,...,27,28,29,30,35,...100])
gender = tf.feature_column.categorical_column_with_vocabulary_list('gender', vocabulary_list=('1', '2')

age_cross_gender = tf.feature_column.crossed_column([gender, bucketized_age], 10)

此处的bucketized_column主要针对数值特征,数值特征必须经过分桶或identity才能和其他特征交叉。假设输入是29岁,分桶后的值为19。这是一个整数index,会直接作为交叉特征列的输入被使用。

对于性别,使用categorical_column_with_vocabulary_list后,性能男被映射到index=0上,性别女被映射到index=1上,所以如果是男,那此时对交叉特征列的输入就是整数0。

在上面这个例子里,交叉特征的输入都是整数,因为实际上都是index。

tensorflow内部怎么实现呢?官方给的解释是说,会按倒序的方式做hash拼接,如下:

# 假设输入的交叉特值分别是[a]和[b]
Fingerprint64Cat(Fingerprint64('b'), Fingerprint64('a'))

但实际上,这是针对字符串输入才是这样,整数不会做hash,直接拼接。源码如下:

dense tensor

// tensorflow/core/kernels/sparse_cross_op.cc
// 以下的 n 即是前面提到的输入 index
template <>
int64 DenseTensorColumn::Feature(int64 batch, int64 n) const {
  if (DT_STRING == tensor_.dtype())
    return Fingerprint64(tensor_.matrix()(batch, n));
  return tensor_.matrix()(batch, n);
}

sparse tensor

// 和dense tensor实现是一致的,对于整数值直接返回
template <>
string SparseTensorColumn::Feature(int64 batch, int64 n) const {
  const int64 start = feature_start_indices_[batch];
  if (DT_STRING == values_.dtype())
    return values_.vec().data()[start + n];
  return std::to_string(values_.vec().data()[start + n]);
}

取到值之后如何交叉呢:

// 以下实现来自代码 tensorflow/core/kernels/sparse_cross_op.cc:HashCrosser
// 会有一个默认的hash_key排在最前面,然后依次拼接余下的值
  int64 Generate(const int64 batch_index,
                 const std::vector& permutation) const {
    // Do the fingerprint concatenation on uint64.
    uint64 hashed_output = hash_key_;

    for (size_t i = 0; i < permutation.size(); ++i) {
      uint64 hash_i = columns_[i]->Feature(batch_index, permutation[i]);
      hashed_output = FingerprintCat64(hashed_output, hash_i);
    }
    // The return value is int64 based on the number of buckets.
    if (num_buckets_ > 0) {
      return hashed_output % num_buckets_;
    } else {
      // To prevent negative output we take modulo to max int64.
      return hashed_output % std::numeric_limits::max();
    }
  }

默认的hash*key定义在源码 *sparse_ops.py

_DEFAULT_HASH_KEY = 0xDECAFCAFFE

怎么测试正确性呢?可以在tensorflow/python/kernel_tests/sparse_cross_op_test.py中测试:

    print('##############')
    a1 = constant_op.constant([[19]])
    a2 = constant_op.constant([[0]])
    cross = sparse_ops.sparse_cross_hashed([a2, a1], num_buckets=12)
    cross_dense = sparse_ops.sparse_tensor_to_dense(cross)
    with session.Session():
      values = cross_dense.eval()
      print('#### ', values)
    print('##############')

编译命令:

bazel build tensorflow/python/kernel_tests:sparse_cross_op_test

以上就会输出a2和 a1的交叉特征值。

怎么验证实现方式?可以在tensorflow/core/platform/fingerprint_test.cc中测试:

  std::cout << "crossed_column (0, 17) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 0), 17) % 12)  << std::endl;
  std::cout << "crossed_column (1, 17) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 1), 17) % 12)  << std::endl;

  std::cout << "crossed_column (0, 18) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 0), 18) % 12)  << std::endl;
  std::cout << "crossed_column (1, 18) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 1), 18) % 12)  << std::endl;

  std::cout << "crossed_column (0, 19) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 0), 19) % 12)  << std::endl;
  std::cout << "crossed_column (1, 19) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 1), 19) % 12)  << std::endl;

  std::cout << "crossed_column (0, 20) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 0), 20) % 12)  << std::endl;
  std::cout << "crossed_column (1, 20) " << (FingerprintCat64(FingerprintCat64(0xDECAFCAFFE, 1), 20) % 12)  << std::endl;

编译命令:

bazel build tensorflow/core:platform_fingerprint_test

以上对于a1, a2的排列顺序非常重要,目前看来交叉特征取名是按字符串排序的,但对于输入值是否有排序还不确定,目前只有2个特征还不必研究排序,等需要的时候再研究吧。


original refer