+-
Spark FeatureHasher代码实现及源码分析

一.简介

  特征哈希将一组分类或数字特征投影到指定维度的特征向量中(通常远小于原始特征空间的特征向量)。这是通过使用哈希技巧,将特征映射到特征向量中的索引来完成的。

二.代码实现

package org.com.tl.spark.mllib import org.apache.log4j.{Level, Logger} import org.apache.spark.ml.feature.FeatureHasher import org.apache.spark.sql.SparkSession /** * Created by zhen on 2020/11/27. */ object FeatureHasherExample { /** * 设置日志级别 */ Logger.getLogger("org").setLevel(Level.WARN) def main(args: Array[String]) { val spark = SparkSession.builder() .appName("FeatureHasherExample") .master("local[2]") .getOrCreate() val dataset = spark.createDataFrame(Seq( ("1", 1.2, true, "Spark"), ("2", 3.4, false, "Spark"), ("3", 5.6, true, "Flink"), ("4", 7.8, false, "Flink") )).toDF("index", "number", "boolean", "technology") val hasher = new FeatureHasher() .setInputCols("index", "number", "boolean", "technology") .setOutputCol("features") val featurized = hasher.transform(dataset) featurized.show(false) spark.close() } }

  执行结果:

...

  看到这个结果大家可能是比较懵逼的,它不像一些算法,把特征向量映射为一个稀疏矩阵,通过填充指定位置的数值来对特征进行正则化;FeatureHasher通过使用Hash映射,把特征映射为Hash值来实现的;大家看源码!

三.源码分析

  从代码上可以看出,特征哈希首先new了一个FeatureHasher()对象,然后调用transform方法:

...

  进入FeatureHasher源码:

...

  从源码上可以看出,该算法是Spark2.3版本的新特性,在使用时要注意,别在低版本中使用。下面进入transform方法中:

...

  后续会多次使用该函数:

...

  进入FeatureHasher的伴生对象中的murmur3Hash方法中,首先看到的是seed随机种子,该参数来自HashingTF:

... ...

  FeatureHasher在多列上运行。每列可以包含数字或分类特征。列数据类型的行为和处理如下:

数字列:对于数字特征,列名称的哈希值用于将特征值映射到特征向量的索引中。默认情况下,数字功能不被视为分类功能(即使它们是整数)。要将它们视为分类,请使用categoricalCols参数指定相关列。 字符串行:对于分类特征,字符串“ column_name = value”的哈希值用于映射到向量索引,指示符值为1.0。因此,分类特征被“一次热”编码(类似于将OneHotEncoder与一起使用 dropLast=false)。 布尔列:布尔值与字符串行的处理方式相同。即,布尔特征表示为“ column_name = true”或“ column_name = false”,指示符值为1.0。 空(缺失)值将被忽略(在所得特征向量中隐式为零)。

  进入其中一个hashInt函数探究一下具体的代码逻辑,需要进入类Murmur3_x86_32

public static int hashInt(int input, int seed) { int k1 = mixK1(input); int h1 = mixH1(seed, k1); return fmix(h1, 4); }

  可知该函数使用另一个函数mixK1对输入input进行处理,mixK1代码如下:

private static int mixK1(int k1) { k1 *= C1; k1 = Integer.rotateLeft(k1, 15); k1 *= C2; return k1; }

  其中C1、C2都是常数:

...

  而Integer.rotateLeft(k1, 15)的代码逻辑如下:

...

  其实就是按位向左平移15。通过以上的代码梳理,可以看出,要生成一个特征的hash映射,首先需要判断其类型,不同的类型采用不同的处理逻辑,其次使用murmur3Hash函数将不同类型的特征数据导入不同的方法中,最后使用Murmur3_x86_32类中的方法对特征数据做乘积,位平移等处理逻辑实现特征数据Hash化。

  最后来看一下开始时的执行结果,前面的特征数据:

...

  主要看一下后面生成的hash映射:

...

  看transform函数:

...

  使用的就是嵌入hashFunc的hashFeatures自定义函数!