Spark ML中的特征转换算法(一)

一、Tokenizer和RegexTokenizer

  标记化是获取文本(例如句子)并将其分解为单个术语(通常是单词)的过程。 一个简单的 Tokenizer 类提供了这个功能。 下面的示例显示了如何将句子拆分为单词序列。

  RegexTokenizer 允许基于正则表达式 (regex) 匹配的更高级的标记化。 默认情况下,参数“pattern”(正则表达式,默认值:“\\s+”)用作分隔输入文本的分隔符。 或者,用户可以将参数“gaps”设置为 false,表示正则表达式“pattern”表示“tokens”而不是拆分间隙,并找到所有匹配的出现作为标记化结果。

示例代码:(代码基于zeppelin平台实现)

%spark
// 特征转换 —— —— Tokenizer和RegexTokenizer
// Tokenizer可以将获取的文本简单拆分为单词
// RegexTokenizer 允许基于正则表达式 (regex) 匹配的更高级文本拆分
import org.apache.spark.ml.feature.{RegexTokenizer, Tokenizer}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._

val sentenceDataFrame = spark.createDataFrame(Seq(
  (0, "Hi I heard about Spark"),
  (1, "I wish Java could use case classes"),
  (2, "Logistic,regression,models,are,neat")
)).toDF("id", "sentence")

val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")

// RegexTokenizer默认情况下,参数“pattern”(正则表达式,默认值:“\\s+”)
//  \\s表示   空格,回车,换行等空白符,    +号表示一个或多个的意思
val regexTokenizer = new RegexTokenizer()
  .setInputCol("sentence")
  .setOutputCol("words")
  .setPattern("\\W") // alternatively .setPattern("\\w+").setGaps(false)  

val countTokens = udf { (words: Seq[String]) => words.length }

val tokenized = tokenizer.transform(sentenceDataFrame)
tokenized.select("sentence", "words")
    .withColumn("tokens", countTokens(col("words"))).show(false)

val regexTokenized = regexTokenizer.transform(sentenceDataFrame)
regexTokenized.select("sentence", "words")
    .withColumn("tokens", countTokens(col("words"))).show(false)

输出:
+-----------------------------------+------------------------------------------+------+
|sentence                           |words                                     |tokens|
+-----------------------------------+------------------------------------------+------+
|Hi I heard about Spark             |[hi, i, heard, about, spark]              |5     |
|I wish Java could use case classes |[i, wish, java, could, use, case, classes]|7     |
|Logistic,regression,models,are,neat|[logistic,regression,models,are,neat]     |1     |
+-----------------------------------+------------------------------------------+------+

+-----------------------------------+------------------------------------------+------+
|sentence                           |words                                     |tokens|
+-----------------------------------+------------------------------------------+------+
|Hi I heard about Spark             |[hi, i, heard, about, spark]              |5     |
|I wish Java could use case classes |[i, wish, java, could, use, case, classes]|7     |
|Logistic,regression,models,are,neat|[logistic, regression, models, are, neat] |5     |
+-----------------------------------+------------------------------------------+------+

二、StopWordsRemover

  停用词是应该从输入中排除的词,通常是因为这些词经常出现并且没有太多意义。 StopWordsRemover 将字符串序列(例如 Tokenizer 的输出)作为输入,并从输入序列中删除所有停用词。 停用词列表由 stopWords 参数指定。 某些语言的默认停用词可以通过调用 StopWordsRemover.loadDefaultStopWords(language) 来访问,可用的选项是“丹麦语”、“荷兰语”、“英语”、“芬兰语”、“法语”、“德语”、“匈牙利语”、 “意大利语”、“挪威语”、“葡萄牙语”、“俄语”、“西班牙语”、“瑞典语”和“土耳其语”。 布尔参数 caseSensitive 指示匹配项是否应区分大小写(默认为 false)。

示例代码:

%spark
// 特征转换 —— —— StopWordsRemover
// StopWordsRemover 将字符串序列(例如 Tokenizer 的输出)作为输入,并从输入序列中删除所有停用词。
// 停用词列表由 stopWords 参数指定。
import org.apache.spark.ml.feature.StopWordsRemover

val remover = new StopWordsRemover()
  .setInputCol("raw")
  .setOutputCol("filtered")
  .setStopWords(Array("saw","lamb")) //要求输入的参数类型为StringArray

val dataSet = spark.createDataFrame(Seq(
  (0, Seq("I", "saw", "the", "red", "balloon")),
  (1, Seq("Mary", "had", "a", "little", "lamb"))
)).toDF("id", "raw")

remover.transform(dataSet).show(false)

输出:
+---+----------------------------+----------------------+
|id |raw                         |filtered              |
+---+----------------------------+----------------------+
|0  |[I, saw, the, red, balloon] |[I, the, red, balloon]|
|1  |[Mary, had, a, little, lamb]|[Mary, had, a, little]|
+---+----------------------------+----------------------+

三、N-Gram

  n-gram 是一个整数 n 的 n 个标记(通常是单词)的序列。 NGram 类可用于将输入特征转换为 n-gram。 NGram 将字符串序列(例如 Tokenizer 的输出)作为输入。 参数 n 用于确定每个 n-gram 中的项数。 输出将由一系列 n-gram 组成,其中每个 n-gram 由 n 个连续单词的空格分隔字符串表示。 如果输入序列包含少于 n 个字符串,则不产生输出。

  简单来说,N-Gram是一种基于统计语言模型的算法,它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。

示例代码:

// 特征转换 —— ——  n-gram
// N-Gram是一种基于统计语言模型的算法。它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。
import org.apache.spark.ml.feature.NGram

val wordDataFrame = spark.createDataFrame(Seq(
  (0, Array("Hi", "I", "heard", "about", "Spark","hello")),
  (1, Array("I", "wish", "Java", "could", "use", "case", "classes")),
  (2, Array("Logistic", "regression", "models", "are", "neat"))
)).toDF("id", "words")

// N默认为2
val ngram = new NGram().setN(3).setInputCol("words").setOutputCol("ngrams")

val ngramDataFrame = ngram.transform(wordDataFrame)
ngramDataFrame.show(false)

输出:
+---+------------------------------------------+--------------------------------------------------------------------------------+
|id |words                                     |ngrams                                                                          |
+---+------------------------------------------+--------------------------------------------------------------------------------+
|0  |[Hi, I, heard, about, Spark, hello]       |[Hi I heard, I heard about, heard about Spark, about Spark hello]               |
|1  |[I, wish, Java, could, use, case, classes]|[I wish Java, wish Java could, Java could use, could use case, use case classes]|
|2  |[Logistic, regression, models, are, neat] |[Logistic regression models, regression models are, models are neat]            |
+---+------------------------------------------+--------------------------------------------------------------------------------+

四、Binarizer

  二值化是将数值特征阈值化为二值(0/1)特征的过程。 Binarizer 采用公共参数 inputCol 和 outputCol,以及二值化的阈值。 大于阈值的特征值被二值化为1.0; 等于或小于阈值的值被二值化为 0.0。 inputCol 支持 Vector 和 Double 类型。

 

示例代码:

%spark
//  特征转换 —— —— Binarizer
// Binarizer是将数值特征化为二值(0/1)特征的过程
import org.apache.spark.ml.feature.Binarizer

val data = Array((0, 0.1), (1, 0.8), (2, 0.2))
val dataFrame = spark.createDataFrame(data).toDF("id", "feature")

val binarizer: Binarizer = new Binarizer()
  .setInputCol("feature")
  .setOutputCol("binarized_feature")
//   用于二值化连续特征的阈值参数。 大于阈值的特征,将被二值化为 1.0。 等于或小于阈值的特征将被二值化为 0.0。 默认值:0.0 
  .setThreshold(0.5)

val binarizedDataFrame = binarizer.transform(dataFrame)

println(s"Binarizer output with Threshold = ${binarizer.getThreshold}")
binarizedDataFrame.show()

输出:
Binarizer output with Threshold = 0.5
+---+-------+-----------------+
| id|feature|binarized_feature|
+---+-------+-----------------+
|  0|    0.1|              0.0|
|  1|    0.8|              1.0|
|  2|    0.2|              0.0|
+---+-------+-----------------+

五、PCA

  PCA 是一种统计过程,它使用正交变换将一组可能相关变量的观察值转换为一组称为主成分的线性不相关变量值。 PCA 类训练模型以使用 PCA 将向量投影到低维空间。 下面的示例展示了如何将 5 维特征向量投影到 3 维主成分中。

示例代码:

%spark
// 特征转换 —— —— PCA
// 数据降维就是一种对高维度特征数据预处理方法。
// 降维是将高维度的数据保留下最重要的一些特征,去除噪声和不重要的特征,从而实现提升数据处理速度的目的。
import org.apache.spark.ml.feature.PCA
import org.apache.spark.ml.linalg.Vectors

val data = Array(
  Vectors.sparse(5, Seq((1, 1.0), (3, 7.0))),
  Vectors.dense(2.0, 0.0, 3.0, 4.0, 5.0),
  Vectors.dense(4.0, 0.0, 0.0, 6.0, 7.0)
)
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")

val pca = new PCA()
  .setInputCol("features")
  .setOutputCol("pcaFeatures")
//   主成分的数量,这个参数必须要有
  .setK(3)
  .fit(df)

val result = pca.transform(df).select("pcaFeatures")
result.show(false)

输出:
+-----------------------------------------------------------+
|pcaFeatures                                                |
+-----------------------------------------------------------+
|[1.6485728230883807,-4.013282700516296,-5.524543751369388] |
|[-4.645104331781534,-1.1167972663619026,-5.524543751369387]|
|[-6.428880535676489,-5.337951427775355,-5.524543751369389] |
+-----------------------------------------------------------+

六、PolynomialExpansion

  多项式扩展是将您的特征扩展为多项式空间的过程,该空间由原始维度的 n 次组合制定。 PolynomialExpansion 类提供此功能。 下面的示例展示了如何将特征扩展到 3 次多项式空间。

 

示例代码:

%spark
// 特征转换 —— —— PolynomialExpansion
// 是一个将特征展开到多元空间的处理过程
// 多项式展开形式如:(x,y)的三次展开(x,x^2,x^3,y,x*y,x^2*y,y^2,x*y^2,y^3)
import org.apache.spark.ml.feature.PolynomialExpansion
import org.apache.spark.ml.linalg.Vectors

val data = Array(
  Vectors.dense(2.0, 1.0),
  Vectors.dense(0.0, 0.0),
  Vectors.dense(3.0, -1.0)
)
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")

val polyExpansion = new PolynomialExpansion()
  .setInputCol("features")
  .setOutputCol("polyFeatures")
//   要展开的多项式次数,应大于等于 1。值为 1 表示不展开。 默认值:2 
  .setDegree(3)

val polyDF = polyExpansion.transform(df)
polyDF.show(false)

输出:
+----------+------------------------------------------+
|features  |polyFeatures                              |
+----------+------------------------------------------+
|[2.0,1.0] |[2.0,4.0,8.0,1.0,2.0,4.0,1.0,2.0,1.0]     |
|[0.0,0.0] |[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]     |
|[3.0,-1.0]|[3.0,9.0,27.0,-1.0,-3.0,-9.0,1.0,3.0,-1.0]|
+----------+------------------------------------------+

七、Discrete Cosine Transform(DCT)

  离散余弦变换,属于傅里叶变换的一种。主要运用于数据或图像的压缩。其将时域中长度为 N 的实数值序列变换为频域中长度为 N 的实数值序列。

示例代码:

import org.apache.spark.ml.feature.DCT
import org.apache.spark.ml.linalg.Vectors

val data = Seq(
  Vectors.dense(0.0, 1.0, -2.0, 3.0),
  Vectors.dense(-1.0, 2.0, 4.0, -7.0),
  Vectors.dense(14.0, -2.0, -5.0, 1.0))

// Tuple1就是只包含一个元素的Tuple,也就是一元组.
// 那么可以任意表达包含n个元素的Tuple吗?答案是否定的。Scala只定义了Tuple1~Tuple22共22个Tuple
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")

val dct = new DCT()
  .setInputCol("features")
  .setOutputCol("featuresDCT")
//   指示是执行反向 DCT (true) 还是正向 DCT (false)。 默认值:false
  .setInverse(false)

val dctDf = dct.transform(df)
dctDf.select("featuresDCT").show(false)

输出:
+----------------------------------------------------------------+
|featuresDCT                                                     |
+----------------------------------------------------------------+
|[1.0,-1.1480502970952693,2.0000000000000004,-2.7716385975338604]|
|[-1.0,3.378492794482933,-7.000000000000001,2.9301512653149677]  |
|[4.0,9.304453421915744,11.000000000000002,1.5579302036357163]   |
+----------------------------------------------------------------+

八、StringIndexer

  StringIndexer 将标签的字符串列编码为标签索引的列。 StringIndexer 可以编码多个列。索引在 [0, numLabels) 中,支持四种排序选项:“frequencyDesc”:按标签频率降序(最频繁的标签分配为 0),“frequencyAsc”:按标签频率升序(最不频繁的标签分配为 0) , “alphabetDesc”:字母降序,“alphabetAsc”:字母升序(默认 = “frequencyDesc”)。请注意,如果在“frequencyDesc”/“frequencyAsc”下的频率相同,则字符串将按字母进一步排序。

  如果用户选择保留看不见的标签,它们将被放在索引 numLabels 处。如果输入列是数字,我们将其转换为字符串并索引字符串值。当 Estimator 或 Transformer 等下游管道组件使用此字符串索引标签时,您必须将组件的输入列设置为此字符串索引列名称。在很多情况下,您可以使用 setInputCol 设置输入列。

%spark
// 特征转换 —— —— StringIndexer
// 字符串索引变换的作用是将标签的字符串列编码为标签索引的列
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.ml.param.Param

val df = spark.createDataFrame(
  Seq((0, "a"), (1, "b"), (2, "c"), (3, "a"), (4, "a"), (5, "c"))
).toDF("id", "category")

val indexer = new StringIndexer()
  .setInputCol("category")
  .setOutputCol("categoryIndex")
//   用于如何处理无效数据(看不见的标签或 NULL 值)的参数。 选项是“skip”(过滤掉包含无效数据的行)、“error”(抛出错误)或“keep”(将无效数据放入特殊的附加存储桶中,索引为 numLabels)。 默认值:“error” 
  .setHandleInvalid("keep")
//   用于如何排序字符串列标签的参数。 排序后的第一个标签的索引为 0。选项有: 'frequencyDesc'(默认),'frequencyAsc','alphabetDesc','alphabetAsc'(字母升序默认为'frequencyDesc')
  .setStringOrderType("frequencyDesc") 

val indexed = indexer.fit(df).transform(df)
indexed.show()

输出:
+---+--------+-------------+
| id|category|categoryIndex|
+---+--------+-------------+
|  0|       a|          0.0|
|  1|       b|          2.0|
|  2|       c|          1.0|
|  3|       a|          0.0|
|  4|       a|          0.0|
|  5|       c|          1.0|
+---+--------+-------------+

九、IndexToString

  与 StringIndexer 对称,IndexToString 将一列标签索引映射回包含作为字符串的原始标签的列。 一个常见的用例是使用 StringIndexer 从标签生成索引,使用这些索引训练模型并使用 IndexToString 从预测索引列中检索原始标签。 但是,您可以自由提供自己的标签。

%spark
// 特征转换 —— —— IndexToString
// 将一列索引映射回对应字符串值的新列的转换器。 索引字符串映射来自输入列的 ML 属性,或来自用户提供的标签(优先于 ML 属性)。 
import org.apache.spark.ml.attribute.Attribute
import org.apache.spark.ml.feature.{IndexToString, StringIndexer}

val df = spark.createDataFrame(Seq(
  (0, "a"),
  (1, "b"),
  (2, "c"),
  (3, "a"),
  (4, "a"),
  (5, "c")
)).toDF("id", "category")

val indexer = new StringIndexer()
  .setInputCol("category")
  .setOutputCol("categoryIndex")
  .fit(df)
val indexed = indexer.transform(df)

println(s"Transformed string column '${indexer.getInputCol}' " +
    s"to indexed column '${indexer.getOutputCol}'")
indexed.show()

val inputColSchema = indexed.schema(indexer.getOutputCol)
println(s"StringIndexer will store labels in output column metadata: " +
    s"${Attribute.fromStructField(inputColSchema).toString}\n")

val converter = new IndexToString()
  .setInputCol("categoryIndex")
  .setOutputCol("originalCategory")
//   用于指定索引字符串映射的标签数组的可选参数。
//   .setLabels()

val converted = converter.transform(indexed)

println(s"Transformed indexed column '${converter.getInputCol}' back to original string " +
    s"column '${converter.getOutputCol}' using labels in metadata")
converted.select("id", "categoryIndex", "originalCategory").show()

输出:
Transformed string column 'category' to indexed column 'categoryIndex'
+---+--------+-------------+
| id|category|categoryIndex|
+---+--------+-------------+
|  0|       a|          0.0|
|  1|       b|          2.0|
|  2|       c|          1.0|
|  3|       a|          0.0|
|  4|       a|          0.0|
|  5|       c|          1.0|
+---+--------+-------------+

StringIndexer will store labels in output column metadata: {"vals":["a","c","b"],"type":"nominal","name":"categoryIndex"}

Transformed indexed column 'categoryIndex' back to original string column 'originalCategory' using labels in metadata
+---+-------------+----------------+
| id|categoryIndex|originalCategory|
+---+-------------+----------------+
|  0|          0.0|               a|
|  1|          2.0|               b|
|  2|          1.0|               c|
|  3|          0.0|               a|
|  4|          0.0|               a|
|  5|          1.0|               c|
+---+-------------+----------------+

 

 

十、OneHotEncoder

   One-hot 编码将表示为标签索引的分类特征映射到二进制向量,该向量最多具有一个单值,表示所有特征值集中存在特定特征值。 这种编码允许期望连续特征的算法(例如逻辑回归)使用分类特征。 对于字符串类型的输入数据,通常首先使用 StringIndexer 对分类特征进行编码。

  OneHotEncoder 可以转换多个列,为每个输入列返回一个单热编码的输出向量列。 通常使用 VectorAssembler 将这些向量合并为单个特征向量。

  OneHotEncoder 支持 handleInvalid 参数来选择在转换数据时如何处理无效输入。 可用选项包括“keep”(任何无效输入都分配给额外的分类索引)和“error”(抛出错误),默认是error。

%spark
// 特征转换 —— —— OneHotEncoder
// 一种单热编码器又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效。
// 可以这样理解,对于每一个特征,如果它有m个可能值,那么经过独热编码后,就变成了m个二元特征。并且,这些特征互斥,每次只有一个激活。因此,数据会变成稀疏的。
// 作用:将离散的分类特征转换为数字表示的特征
// 这样做的好处:1、解决了分类器不好处理属性数据的问题;2、在一定程度上也起到了扩充特征的作用。

import org.apache.spark.ml.feature.OneHotEncoder

val df = spark.createDataFrame(Seq(
  (0.0, 1.0),
  (1.0, 0.0),
  (2.0, 1.0),
  (0.0, 2.0),
  (0.0, 1.0),
  (2.0, 0.0)
)).toDF("categoryIndex1", "categoryIndex2")

val encoder = new OneHotEncoder()
  .setInputCols(Array("categoryIndex1", "categoryIndex2"))
  .setOutputCols(Array("categoryVec1", "categoryVec2"))
//   默认是error
//   .setHandleInvalid("keep")
//   是否删除编码向量中的最后一个类别(默认值:true) 
//   .setDropLast(true)
val model = encoder.fit(df)

val encoded = model.transform(df)
encoded.show()

输出:
+--------------+--------------+-------------+-------------+
|categoryIndex1|categoryIndex2| categoryVec1| categoryVec2|
+--------------+--------------+-------------+-------------+
|           0.0|           1.0|(2,[0],[1.0])|(2,[1],[1.0])|
|           1.0|           0.0|(2,[1],[1.0])|(2,[0],[1.0])|
|           2.0|           1.0|    (2,[],[])|(2,[1],[1.0])|
|           0.0|           2.0|(2,[0],[1.0])|    (2,[],[])|
|           0.0|           1.0|(2,[0],[1.0])|(2,[1],[1.0])|
|           2.0|           0.0|    (2,[],[])|(2,[0],[1.0])|
+--------------+--------------+-------------+-------------+

详情见:Spark ML中的特征转换算法——OneHotEncoder - 干了这瓶老干妈 - 博客园 (cnblogs.com)

 

posted @ 2022-03-05 02:28  干了这瓶老干妈  阅读(337)  评论(0编辑  收藏  举报
Live2D