Spark MLlib学习——特征工程

Extracting, transforming and selecting features

这一大章节讲的内容主要是与特征工程相关的算法,粗略的可以分为如下几类:

  1. Extraction:从Raw数据中提取出特征
  2. Transformation:Scaling, converting, or modifying features
  3. Selection:从大的特征集合中挑选一个子集
  4. Locality Sensitive Hashing (LSH):这类算法将特征变换的方面与其他算法相结合。

Feature Extractors

TF-IDF

TF-IDF是Term frequency(词频)-inverse document frequency(逆文本频率指数)的缩写。是一种在文本挖掘中广泛使用的特征向量化方法,以反映一个单词在语料库中的重要性。定义:t 表示由一个单词,d 表示一个文档,D 表示语料库(corpus),词频 TF(t,d) 表示某一个给定的单词 t 出现在文档 d 中的次数(单词次数), 而文档频率 DF(t,D) 表示包含单词 t文档次数。如果我们只使用词频 TF 来衡量重要性,则很容易过分强调出现频率过高并且文档包含少许信息的单词,例如,'a','the',和 'of'。如果一个单词在整个语料库中出现的非常频繁,这意味着它并没有携带特定文档的某些特殊信息(换句话说,该单词对整个文档的重要程度低)。逆向文档频率是一个数字量度,表示一个单词提供了多少信息:


其中,|D| 是在语料库中文档总数。由于使用对数,所以如果一个单词出现在所有的文件,其IDF值变为0。注意,应用平滑项以避免在语料库之外的项除以零(为了防止分母为0,分母需要加1)。因此,TF-IDF测量只是TF和IDF的产物:(对TF-IDF定义为TF和IDF的乘积)

关于词频TF和文档频率DF的定义有多种形式。在MLlib,我们分离TF和IDF,使其灵活。下面是MLlib中的使用情况简单说明:

TFHashingTFCountVectorizer都可以用于生成词频TF向量。
其中HashingTF是一个需要特征词集的转换器(Transformer),它可以将这些集合转换成固定长度的特征向量。CountVectorizer将文本文档转换为关键词计数的向量。

IDF:IDF是一个适合数据集并生成IDFModel的评估器(Estimator),IDFModel获取特征向量(通常由HashingTFCountVectorizer创建)并缩放每列。直观地说,它下调了在语料库中频繁出现的列。

代码示例(Java版)

//创建Row的数据list
List<Row> data = Arrays.asList(
  RowFactory.create(0.0, "Hi I heard about Spark"),
  RowFactory.create(0.0, "I wish Java could use case classes"),
  RowFactory.create(1.0, "Logistic regression models are neat")
);
//创建Schema给Row定义数据类型和fieldName
StructType schema = new StructType(new StructField[]{
  new StructField("label", DataTypes.DoubleType, false, Metadata.empty()),
  new StructField("sentence", DataTypes.StringType, false, Metadata.empty())
});
Dataset<Row> sentenceData = spark.createDataFrame(data, schema);

//使用Tokenizer来将句子分割成单词
Tokenizer tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words");
Dataset<Row> wordsData = tokenizer.transform(sentenceData);

//使用HashingTF将句子中的单词哈希成特征向量(这个可以在前一章节的最后输出打印截图中看到具体的值)
int numFeatures = 20;
HashingTF hashingTF = new HashingTF()
  .setInputCol("words")
  .setOutputCol("rawFeatures")
  .setNumFeatures(numFeatures);

Dataset<Row> featurizedData = hashingTF.transform(wordsData);
// alternatively, CountVectorizer can also be used to get term frequency vectors

//使用IDF对上面产生的特征向量进行rescale
IDF idf = new IDF().setInputCol("rawFeatures").setOutputCol("features");
IDFModel idfModel = idf.fit(featurizedData); //fit得到IDF的模型

Dataset<Row> rescaledData = idfModel.transform(featurizedData); //对特征向量进行rescale
rescaledData.select("label", "features").show();

//最后得到的特征向量可以作为其他机器学习算法的输入
rescaledData.select("label", "words", "rawFeatures", "features").show()

Word2Vec

Word2Vec是一个Estimator(评估器),它采用表示文档的单词序列,并训练一个Word2VecModel。 该模型将每个单词映射到一个唯一的固定大小向量。 Word2VecModel使用文档中所有单词的平均值将每个文档转换为向量; 该向量然后可用作预测,文档相似性计算等功能。有关更多详细信息,请参阅有关Word2Vec的MLlib用户指南
代码示例(Java版)

public class JavaWord2VecExample {
  public static void main(String[] args) {
    SparkSession spark = SparkSession
      .builder()
      .master("local[4]")
      .appName("JavaWord2VecExample")
      .getOrCreate();

    // $example on$
    // Input data: Each row is a bag of words from a sentence or document.
    List<Row> data = Arrays.asList(
      RowFactory.create(Arrays.asList("Hi I heard about Spark".split(" "))),
      RowFactory.create(Arrays.asList("I wish Java could use case classes".split(" "))),
      RowFactory.create(Arrays.asList("Logistic regression models are neat".split(" ")))
    );
    StructType schema = new StructType(new StructField[]{
      new StructField("text", new ArrayType(DataTypes.StringType, true), false, Metadata.empty())
    });
    Dataset<Row> documentDF = spark.createDataFrame(data, schema);

    //创建Word2Vec的实例,然后设置参数
    // Learn a mapping from words to Vectors.
    Word2Vec word2Vec = new Word2Vec()
      .setInputCol("text")
      .setOutputCol("result")
      .setVectorSize(3)
      .setMinCount(0);

    Word2VecModel model = word2Vec.fit(documentDF); //fit出模型
    Dataset<Row> result = model.transform(documentDF); //对输入进行transform得到结果DataFrame

    for (Row row : result.collectAsList()) {
      List<String> text = row.getList(0);
      Vector vector = (Vector) row.get(1);
      System.out.println("\n\nText: " + text + " => \nVector: " + vector + "\n\n\n");
    }
    // $example off$

    spark.stop();
  }
}
Word2Vec示例运行结果

Feature Transformers

下图是对特征转换的API doc中列出的算法。不过这里不打算把每个都展开描述了,会简单举例几个,然后后面实际用到哪个再去查哪个。

API Doc中特征转换算法列表

Tokenizer

Tokenization(文本符号化)是将文本 (如一个句子)拆分成单词的过程。(在Spark ML中)Tokenizer(分词器)提供此功能。下面的示例演示如何将句子拆分为词的序列。
RegexTokenizer 提供了(更高级的)基于正则表达式 (regex) 匹配的(对句子或文本的)单词拆分。默认情况下,参数"pattern"(默认的正则表达式: "\\s+"
,此时和Tokenizer没有区别
) 作为分隔符用于拆分输入的文本。或者,用户可以将参数“gaps”设置为 false(不然默认true的情况下分割出来的结果是分隔符的集合,而不是单词的集合),指定正则表达式"pattern"表示"tokens",而不是分隔符,这样作为划分结果找到的所有匹配项

回顾下正则表达式

代码示例(Java版)

public class JavaTokenizerExample {
  public static void main(String[] args) {
    SparkSession spark = SparkSession
      .builder()
      .appName("JavaTokenizerExample")
      .getOrCreate();

    //构造输入数据,注意第三行的数据word之间只有逗号没有空格
    // $example on$
    List<Row> data = Arrays.asList(
      RowFactory.create(0, "Hi I heard about Spark"),
      RowFactory.create(1, "I wish Java could use case classes"),
      RowFactory.create(2, "Logistic,regression,models,are,neat")
    );

    StructType schema = new StructType(new StructField[]{
      new StructField("id", DataTypes.IntegerType, false, Metadata.empty()),
      new StructField("sentence", DataTypes.StringType, false, Metadata.empty())
    });

    Dataset<Row> sentenceDataFrame = spark.createDataFrame(data, schema);

    //Tokenizer划分单词是按照空格来做的
    Tokenizer tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words");

    //通过setPattern来让正则表达的划分按照非字母来做
    RegexTokenizer regexTokenizer = new RegexTokenizer()
        .setInputCol("sentence")
        .setOutputCol("words")
        .setPattern("\\W");  // alternatively .setPattern("\\w+").setGaps(false); 换成这句话一样的结果。

    //注册一个user-defined functions,第一个参数是udf的名字,第二个参数是一个自定义的转换函数。
    spark.udf().register("countTokens", new UDF1<WrappedArray, Integer>() {
      @Override
      public Integer call(WrappedArray words) {
        return words.size();
      }
    }, DataTypes.IntegerType);

    //按照空格划分,结果第三行没有划分,当作整体对待。
    Dataset<Row> tokenized = tokenizer.transform(sentenceDataFrame);
    tokenized.select("sentence", "words")
        .withColumn("tokens", callUDF("countTokens", col("words"))).show(false);

    //按照正则表达式对待,把非字母的地方划分了。
    Dataset<Row> regexTokenized = regexTokenizer.transform(sentenceDataFrame);
    regexTokenized.select("sentence", "words")
        .withColumn("tokens", callUDF("countTokens", col("words"))).show(false);
    // $example off$

    spark.stop();
  }
}
两种Tokenizer

VectorAssembler(特征向量合并)

【这个API超级有用!】VectorAssembler 是将指定的一list的列合并到单个列向量中的 transformer。它可以将原始特征和不同特征transformers(转换器)生成的特征合并为单个特征向量,来训练 ML 模型,如逻辑回归和决策树等机器学习算法。VectorAssembler 可接受以下的输入列类型:任何数值型布尔类型向量类型。输入列的值将按指定顺序依次添加到一个向量中。

举例
假设现在有一个DataFrame,它的列为:id, hour, mobile, userFeatures和clicked:

id hour mobile userFeatures clicked
0 18 1.0 [0.0, 10.0, 0.5] 1.0

其中userFeatures是一个列向量包含三个用户特征。现在想把hour, mobile, 和userFeatures合并到一个一个特征向量中(名为features,这个也是很多MLlib算法默认的特征输入向量名字),然后用来预测clicked的值。具体用法就是将列hour、mobile和userFeatures作为input,features作为output,然后调用transform之后得到新的DataFrame:

id hour mobile userFeatures clicked features
0 18 1.0 [0.0, 10.0, 0.5] 1.0 [18.0, 1.0, 0.0, 10.0, 0.5]

示例Java代码:

public class JavaVectorAssemblerExample {
  public static void main(String[] args) {
    SparkSession spark = SparkSession
      .builder()
      .appName("JavaVectorAssemblerExample")
      .getOrCreate();

    // $example on$
    StructType schema = createStructType(new StructField[]{
      createStructField("id", IntegerType, false),
      createStructField("hour", IntegerType, false),
      createStructField("mobile", DoubleType, false),
      createStructField("userFeatures", new VectorUDT(), false),
      createStructField("clicked", DoubleType, false)
    });
    Row row = RowFactory.create(0, 18, 1.0, Vectors.dense(0.0, 10.0, 0.5), 1.0);
    Dataset<Row> dataset = spark.createDataFrame(Arrays.asList(row), schema);
    System.out.println("\n-------Before assembled the original is:");
    dataset.show(false);

    VectorAssembler assembler = new VectorAssembler()
      .setInputCols(new String[]{"hour", "mobile", "userFeatures"})
      .setOutputCol("features");

    Dataset<Row> output = assembler.transform(dataset);
    System.out.println("\n+++++++Assembled columns 'hour', 'mobile', 'userFeatures' to vector column " +
        "'features'");
    output.select("features", "clicked").show(false);
    // $example off$

    spark.stop();
  }
}

运行结果:

-------Before assembled the original is:
+---+----+------+--------------+-------+
|id |hour|mobile|userFeatures  |clicked|
+---+----+------+--------------+-------+
|0  |18  |1.0   |[0.0,10.0,0.5]|1.0    |
+---+----+------+--------------+-------+


+++++++Assembled columns 'hour', 'mobile', 'userFeatures' to vector column 'features'
+-----------------------+-------+
|features               |clicked|
+-----------------------+-------+
|[18.0,1.0,0.0,10.0,0.5]|1.0    |
+-----------------------+-------+


Feature Selectors

下面只介绍几种MLlib提供的特特征选择算法,其余参见API Doc,后续如果自己用到会再补充。

VectorSlicer(向量切片机)

VectorSlicer是一个转换器,它对于输入的特征向量,输出一个新的原始特征子集的特征向量。对于从列向量中提取特征很有帮助。
VectorSlicer对于指定索引的列向量,输出一个新的列向量,所选择的列向量通过这些索引进行选择。有两种类型的索引:

  • 整数索引:代表列向量的下标,setIndices()
  • 字符串索引:代表列的特征名称,setNames()。这要求列向量有AttributeGroup,因为实现中是在Attribute上的name字段匹配。

整数和字符串的索引都可以接受。此外,还可以同时使用整数索引和字符串名称索引。但必须至少选择一个特征。重复的特征选择是不允许的,所以选择的索引和名称之间不能有重叠。请注意,如果选择了特征的名称索引,则遇到空的输入属性时会抛出异常。
输出时将按照选择中给出的特征索引的先后顺序进行向量及其名称的输出。
举例:
假设有一个DataFrame它的列名(AttributeGroup)为userFeatures

userFeatures
[0.0, 10.0, 0.5]

userFeatures是一个包含三个用户特征的列向量。假设userFeature的第一列全部为0,因此我们要删除它并仅选择最后两列。VectorSlicer使用setIndices(1,2)选择最后两个元素,然后生成一个名为features的新向量列:

userFeatures features
[0.0, 10.0, 0.5] 10.0, 0.5

如果userFeatures已经输入了属性值["f1", "f2", "f3"],那么我们可以使用setNames("f2", "f3")来选择它们。

userFeatures features
[0.0, 10.0, 0.5] 10.0, 0.5
["f1", "f2", "f3"] ["f2", "f2"]

下面是一个Java代码示例:

public class JavaVectorSlicerExample {
  public static void main(String[] args) {
    SparkSession spark = SparkSession
      .builder()
      .appName("JavaVectorSlicerExample")
      .getOrCreate();

    //构造AttributeGroup来方便vectorSlicer使用setNames
    // $example on$
    Attribute[] attrs = new Attribute[]{
      NumericAttribute.defaultAttr().withName("f1"),
      NumericAttribute.defaultAttr().withName("f2"),
      NumericAttribute.defaultAttr().withName("f3")
    };
    AttributeGroup group = new AttributeGroup("userFeatures", attrs);

    //构造数据
    List<Row> data = Lists.newArrayList(
      RowFactory.create(Vectors.sparse(3, new int[]{0, 1}, new double[]{-2.0, 2.3}).toDense()), //这里必须使用toDense()来避免sprse的数据结构引起下面的切片时的问题。
      RowFactory.create(Vectors.dense(-2.0, 2.3, 0.0)) //dense和sparse的区别在与sparse是稀疏的适合大量0数据的构造,dense是把每个数值都要赋值的适合非稀疏的情况。
    );

    Dataset<Row> dataset =
      spark.createDataFrame(data, (new StructType()).add(group.toStructField()));
    System.out.println("\n=======Original DataFrame is:");
    dataset.show(false);

    //构造VectorSlicer设置输入列为"userFeatures",输出列为“features”
    VectorSlicer vectorSlicer = new VectorSlicer()
      .setInputCol("userFeatures").setOutputCol("features");

    //setIndices和setNames来选择int[]和String[]的特征列
    vectorSlicer.setIndices(new int[]{1}).setNames(new String[]{"f3"});
    // or slicer.setIndices(new int[]{1, 2}), or slicer.setNames(new String[]{"f2", "f3"})

    Dataset<Row> output = vectorSlicer.transform(dataset);
    System.out.println("\n---------After slice select the output DataFrame is:");
    output.show(false);
    // $example off$

    spark.stop();
  }
}
VectorSlicer的输出结果

如果对于sparse向量不使用toDense方法那么结果就是对sparse结构的数据进行slice操作,结果如下:


错误的VectorSlicer结果,目前我已将这个小bug在github上提交了pull request

Locality Sensitive Hashing

LSH是哈希技术中重要的一种,通常用于集群,近似最近邻搜索和大型数据集的孤立点检测。
LSH的大致思路是用一系列函数(LSH families)将数据哈希到桶中,这样彼此接近的数据点处于相同的桶中可能性就会很高,而彼此相距很远的数据点很可能处于不同的桶中。一个LSH family 正式定义如下。
在度量空间(M,d)中,M是一个集合,d是M上的一个距离函数,LSH family是一系列能满足以下属性的函数h


满足以上条件的LSH family被称为(r1, r2, p1, p2)-sensitive。
在Spark中,不同的LSH families实现在不同的类中(例如:MinHash),并且在每个类中提供了用于特征变换的API,近似相似性连接和近似最近邻。
在LSH中,我们将一个假阳性定义为一对相距大的输入特征(当 d(p,q)≥r2 时),它们被哈希到同一个桶中,并且将一个假阴性定义为一对相邻的特征(当 d(p,q)≤r1 时 ),它们被分散到不同的桶中。

LSH Operations(LSH运算)

我们描述了大部分LSH会用到的运算,每一个合适的LSH模型都有自己的方法实现了这些运算。

Feature Transformation(特征变换)

特征变换是将哈希值添加为新列的基本功能。 这可以有助于降低维数。 用户可以通过设置 inputCol 和 outputCol 参数来指定输入和输出列名。
LSH 还支持多个LSH哈希表。 用户可以通过设置 numHashTables 来指定哈希表的数量。 这也用于近似相似性连接和近似最近邻的 OR-amplification(或放大器)放大。 增加哈希表的数量将增加准确性,但也会增加通信成本和运行时间。
outputCol 的类型是 Seq [Vector],其中数组的维数等于 numHashTables ,并且向量的维度当前设置为1。在将来的版本中,我们将实现 AND-amplification(与放大器),以便用户可以指定这些向量的维度 。

Approximate Similarity Join(近似相似度连接)

近似相似度连接采用两个数据集,并且近似返回距离小于用户定义阈值的数据集中的行对。 近似相似度连接支持两个不同的数据集连接和自连接。 Self-joinin (自连接)会产生一些重复的对。
近似相似度连接接受已转换和未转换的数据集作为输入。 如果使用未转换的数据集,它将自动转换。 在这种情况下,哈希签名将被创建为outputCol。
在加入的数据集中,可以在数据集A和数据集B中查询原始数据集。 距离列将被添加到输出数据集,以显示返回的每对行之间的真实距离。

Approximate Nearest Neighbor Search(近似最邻近搜索)

近似最近邻搜索采用数据集(特征向量)和Key键(单个特征向量),并且它近似返回数据集中最接近向量的指定数量的行。
近似最近邻搜索接受已转换和未转换的数据集作为输入。 如果使用未转换的数据集,它将自动转换。 在这种情况下,哈希签名将被创建为outputCol。
距离列将被添加到输出数据集,以显示每个输出行和搜索的键之间的真实距离。
注意:当哈希桶中没有足够的候选项时,近似最近邻搜索将返回少于k行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容