本节涵盖使用功能的算法,大致分为以下几类:
术语频率逆文档频率(TF-IDF)是一种特征向量化方法,广泛用于文本挖掘中,以反映术语对语料库中文档的重要性。用t表示项,用d表示文档,用D表示语料库。术语频率TF(t,d)是术语t在文档d中出现的次数,而文档频率DF(t,D)是数字包含术语t的文档。如果我们仅使用术语频率来衡量重要性,则很容易过分强调那些经常出现但几乎没有有关文档信息的术语,例如“一个”,“该”和“的”。如果术语经常出现在整个语料库中,则表示该术语不包含有关特定文档的特殊信息。反向文档频率是一个术语提供多少信息的数字度量:
| D |是语料库中文档的总数。由于使用对数,因此如果一个术语出现在所有文档中,则其IDF值将变为0。请注意,应用了平滑术语以避免对主体外的术语除以零。 TF-IDF度量只是TF和IDF的乘积:
术语频率和文档频率的定义有多种变体。在MLlib中,我们将TF和IDF分开以使其具有灵活性。
TF:HashingTF和CountVectorizer均可用于生成术语频率向量。
HashingTF是一个Transformer,它接受多个术语集并将这些术语集转换为固定长度的特征向量。在文本处理中,“一组术语”可能是一袋单词。 HashingTF利用了哈希技巧。通过应用哈希函数将原始特征映射到索引(项)。这里使用的哈希函数是MurmurHash3。然后根据映射的索引计算词频。这种方法避免了需要计算全局项到索引图的情况,这对于大型语料库可能是昂贵的,但是它会遭受潜在的哈希冲突,即哈希后不同的原始特征可能成为同一术语。为了减少冲突的机会,我们可以增加目标要素的维数,即哈希表的存储桶数。由于使用散列值的简单模来确定向量索引,因此建议使用2的幂作为特征维,否则特征将不会均匀地映射到向量索引。默认特征尺寸为2^18 = 262,144。可选的二进制切换参数控制项频率计数。当设置为true时,所有非零频率计数都设置为1。这对于模拟二进制而不是整数计数的离散概率模型特别有用。
CountVectorizer将文本文档转换为术语计数的向量。有关更多详细信息,请参考CountVectorizer。
IDF:IDF是适合数据集并生成IDFModel的估算器。 IDFModel采用特征向量(通常从HashingTF或CountVectorizer创建)并缩放每个特征。从直觉上讲,它降低了经常出现在语料库中的特征的权重。
注意:spark.ml不提供用于文本分割的工具。我们将用户推荐给Stanford NLP Group和scalanlp / chalk。
例子
在下面的代码段中,我们从一组句子开始。我们使用Tokenizer将每个句子分成单词。对于每个句子(单词袋),我们使用HashingTF将句子哈希为特征向量。我们使用IDF重新缩放特征向量。使用文本作为特征时,通常可以提高性能。然后,我们的特征向量可以传递给学习算法。
import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
val sentenceData = spark.createDataFrame(Seq(
(0.0, "Hi I heard about Spark"),
(0.0, "I wish Java could use case classes"),
(1.0, "Logistic regression models are neat")
)).toDF("label", "sentence")
val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
val wordsData = tokenizer.transform(sentenceData)
val hashingTF = new HashingTF()
.setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(20)
val featurizedData = hashingTF.transform(wordsData)
// alternatively, CountVectorizer can also be used to get term frequency vectors
val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
val idfModel = idf.fit(featurizedData)
val rescaledData = idfModel.transform(featurizedData)
rescaledData.select("label", "features").show()
Word2Vec是一个估计器,它采用代表文档的单词序列并训练Word2VecModel。该模型将每个单词映射到唯一的固定大小的向量。 Word2VecModel使用文档中所有单词的平均值将每个文档转换为向量。然后,可以将此向量用作预测,文档相似度计算等的功能。有关更多详细信息,请参考Word2Vec上的MLlib用户指南。
例子
在下面的代码段中,我们从一组文档开始,每个文档都由一个单词序列表示。对于每个文档,我们将其转换为特征向量。然后可以将该特征向量传递给学习算法。
import org.apache.spark.ml.feature.Word2Vec
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.sql.Row
// Input data: Each row is a bag of words from a sentence or document.
val documentDF = spark.createDataFrame(Seq(
"Hi I heard about Spark".split(" "),
"I wish Java could use case classes".split(" "),
"Logistic regression models are neat".split(" ")
).map(Tuple1.apply)).toDF("text")
// Learn a mapping from words to Vectors.
val word2Vec = new Word2Vec()
.setInputCol("text")
.setOutputCol("result")
.setVectorSize(3)
.setMinCount(0)
val model = word2Vec.fit(documentDF)
val result = model.transform(documentDF)
result.collect().foreach { case Row(text: Seq[_], features: Vector) =>
println(s"Text: [${text.mkString(", ")}] => \nVector: $features\n") }
CountVectorizer和CountVectorizerModel旨在帮助将文本文档的集合转换为令牌计数的向量。当先验字典不可用时,CountVectorizer可用作估计器以提取词汇表并生成CountVectorizerModel。该模型在词汇表上生成文档的稀疏表示,然后可以将其传递给其他算法,例如LDA。
在拟合过程中,CountVectorizer将选择整个语料库中按词频排列的前vocabSize词。可选参数minDF还通过指定一个术语必须出现在词汇表中的最小数量(或小于1.0的分数)来影响拟合过程。另一个可选的二进制切换参数控制输出向量。如果将其设置为true,则所有非零计数都将设置为1。这对于模拟二进制而不是整数计数的离散概率模型特别有用。
例子
假设我们具有以下带有列ID和文本的DataFrame:
id | texts |
---|---|
0 | Array(“a”, “b”, “c”) |
1 | Array(“a”, “b”, “b”, “c”, “a”) |
文本中的每一行都是Array [String]类型的文档。调用CountVectorizer的fit会生成带有词汇表(a,b,c)的CountVectorizerModel。转换后的输出列“ vector”包含:
id | texts | vector |
---|---|---|
0 | Array(“a”, “b”, “c”) | (3,[0,1,2],[1.0,1.0,1.0]) |
1 | Array(“a”, “b”, “b”, “c”, “a”) | (3,[0,1,2],[2.0,2.0,1.0]) |
每个向量代表整个词汇表中文档的标记计数。
import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}
val df = spark.createDataFrame(Seq(
(0, Array("a", "b", "c")),
(1, Array("a", "b", "b", "c", "a"))
)).toDF("id", "words")
// fit a CountVectorizerModel from the corpus
val cvModel: CountVectorizerModel = new CountVectorizer()
.setInputCol("words")
.setOutputCol("features")
.setVocabSize(3)
.setMinDF(2)
.fit(df)
// alternatively, define CountVectorizerModel with a-priori vocabulary
val cvm = new CountVectorizerModel(Array("a", "b", "c"))
.setInputCol("words")
.setOutputCol("features")
cvModel.transform(df).show(false)
特征哈希将一组分类或数字特征投影到指定维度的特征向量中(通常大大小于原始特征空间的特征向量)。这是通过使用哈希技巧将特征映射到特征向量中的索引来完成的。
FeatureHasher变压器可在多列上运行。每列可以包含数字或分类特征。列数据类型的行为和处理如下:
空(缺失)值将被忽略(在所得特征向量中隐式为零)。
这里使用的哈希函数也是HashingTF中使用的MurmurHash 3。由于使用散列值的简单模来确定矢量索引,因此建议使用2的幂作为numFeatures参数。否则,这些特征将不会均匀地映射到矢量索引。
例子
假设我们有一个DataFrame,其中有4个输入列real,bool,stringNum和string。这些不同的数据类型作为输入将说明生成一列特征向量的变换的行为。
real | bool | stringNum | string |
---|---|---|---|
2.2 | true | 1 | foo |
3.3 | false | 2 | bar |
4.4 | false | 3 | baz |
5.5 | false | 4 | foo |
然后,此DataFrame上FeatureHasher.transform的输出为:
real | bool | stringNum | string | features |
---|---|---|---|---|
2.2 | true | 1 | foo | (262144,[51871, 63643,174475,253195],[1.0,1.0,2.2,1.0]) |
3.3 | false | 2 | bar | (262144,[6031, 80619,140467,174475],[1.0,1.0,1.0,3.3]) |
4.4 | false | 3 | baz | (262144,[24279,140467,174475,196810],[1.0,1.0,4.4,1.0]) |
5.5 | false | 4 | foo | (262144,[63643,140467,168512,174475],[1.0,1.0,1.0,5.5]) |
然后可以将所得的特征向量传递给学习算法。
import org.apache.spark.ml.feature.FeatureHasher
val dataset = spark.createDataFrame(Seq(
(2.2, true, "1", "foo"),
(3.3, false, "2", "bar"),
(4.4, false, "3", "baz"),
(5.5, false, "4", "foo")
)).toDF("real", "bool", "stringNum", "string")
val hasher = new FeatureHasher()
.setInputCols("real", "bool", "stringNum", "string")
.setOutputCol("features")
val featurized = hasher.transform(dataset)
featurized.show(false)
标记化是获取文本(例如句子)并将其分解为单个术语(通常是单词)的过程。一个简单的Tokenizer类提供了此功能。下面的示例显示了如何将句子分成单词序列。
RegexTokenizer允许基于正则表达式(regex)匹配进行更高级的标记化。默认情况下,参数“ pattern”(正则表达式,默认值:“ \ s +”)用作分隔输入文本的定界符。或者,用户可以将参数“ gap”设置为false,以表示正则表达式“ pattern”表示“令牌”,而不是拆分间隙,并找到所有匹配的出现作为标记化结果。
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")
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)
停用词是应从输入中排除的词,通常是因为这些词频繁出现且含义不大。
StopWordsRemover将一个字符串序列(例如Tokenizer的输出)作为输入,并从输入序列中删除所有停用词。停用词列表由stopWords参数指定。通过调用StopWordsRemover.loadDefaultStopWords(language)可以访问某些语言的默认停用词,其可用选项为“丹麦语”,“荷兰语”,“英语”,“芬兰语”,“法语”,“德语”,“匈牙利语”, “意大利语”,“挪威语”,“葡萄牙语”,“俄语”,“西班牙语”,“瑞典语”和“土耳其语”。布尔参数caseSensitive指示匹配是否区分大小写(默认情况下为false)。
例子:
假设我们有以下具有ID和raw列的DataFrame:
id | raw |
---|---|
0 | [I, saw, the, red, baloon] |
1 | [Mary, had, a, little, lamb] |
将Raw用作输入列,并将StopfiltersRemover应用于输出列,然后应用StopWordsRemover,我们应该获得以下内容:
id | raw | filtered |
---|---|---|
0 | [I, saw, the, red, baloon] | [saw, red, baloon] |
1 | [Mary, had, a, little, lamb] | [Mary, little, lamb] |
在过滤中,停用词“ I”,“ the”,“ had”和“ a”已被过滤掉。
import org.apache.spark.ml.feature.StopWordsRemover
val remover = new StopWordsRemover()
.setInputCol("raw")
.setOutputCol("filtered")
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)
n-gram是某个整数n的n个标记(通常是单词)的序列。 NGram类可用于将输入要素转换为n-gram。
NGram将字符串序列作为输入(例如Tokenizer的输出)。参数n用于确定每个n-gram中的项数。输出将由一系列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")),
(1, Array("I", "wish", "Java", "could", "use", "case", "classes")),
(2, Array("Logistic", "regression", "models", "are", "neat"))
)).toDF("id", "words")
val ngram = new NGram().setN(2).setInputCol("words").setOutputCol("ngrams")
val ngramDataFrame = ngram.transform(wordDataFrame)
ngramDataFrame.select("ngrams").show(false)
二进制化是将数字特征阈值化为二进制(0/1)特征的过程。
Binarizer采用公共参数inputCol和outputCol以及二进制化的阈值。大于阈值的特征值将二值化为1.0;等于或小于阈值的值将二值化为0.0。 inputCol支持Vector和Double类型。
例子
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")
.setThreshold(0.5)
val binarizedDataFrame = binarizer.transform(dataFrame)
println(s"Binarizer output with Threshold = ${binarizer.getThreshold}")
binarizedDataFrame.show()
PCA是一种统计过程,它使用正交变换将一组可能相关的变量的观测值转换为一组线性不相关的变量值(称为主成分)。 PCA类训练模型以使用PCA将向量投影到低维空间。下例显示了如何将5维特征向量投影到3维主成分中。
例子
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)
多项式扩展是将要素扩展到多项式空间的过程,该空间由原始尺寸的n次组合构成。 PolynomialExpansion类提供此功能。下面的示例显示如何将特征扩展到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")
.setDegree(3)
val polyDF = polyExpansion.transform(df)
polyDF.show(false)
离散余弦变换将时域中长度为N的实值序列转换为频域中另一个长度为N的实值序列。 DCT类提供此功能,实现DCT-II并将结果缩放1 / 2‾√,以使变换的表示矩阵为matrix。没有移位应用于变换后的序列(例如,变换后的序列的第0个元素是第0个DCT系数而不是第N / 2个)。
例子
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))
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val dct = new DCT()
.setInputCol("features")
.setOutputCol("featuresDCT")
.setInverse(false)
val dctDf = dct.transform(df)
dctDf.select("featuresDCT").show(false)
StringIndexer将标签的字符串列编码为标签索引的列。索引位于[0,numLabels)中,并支持四个排序选项:“ frequencyDesc”:按标签频率的降序(最频繁的标签分配为0),“ frequencyAsc”:按标签频率的升序(最不频繁的标签分配为0) ,“ alphabetDesc”:字母降序,“ alphabetAsc”:字母升序(默认=“ frequencyDesc”)。如果用户选择保留,则看不见的标签将放置在索引numLabels处。如果输入列为数字,则将其强制转换为字符串并为字符串值编制索引。当下游管道组件(例如Estimator或Transformer)使用此字符串索引标签时,必须将组件的输入列设置为此字符串索引列名称。在许多情况下,可以使用setInputCol设置输入列。
例子:
假设我们有如下数据结构,其中列为Id和category
id | category |
---|---|
0 | a |
1 | b |
2 | c |
3 | a |
4 | a |
5 | c |
category是一个有三个标签是"a",“b”和“c”的字符串列。在category这一列应用StringIndexer作为输入列,将得到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 |
"a"的引用值为0是因为a标签出现的频率最高,接着是引用值为1的"c"和引用值为2的“b”。
另外,当你在一个数据集上拟合了StringIndexer并用它去转换另外的数据集时,这里有三种策略来帮助StringIndexer处理无法观测的标签
例子
让我们回到我们之前的例子中,但是这次我们将重新在原来的数据集上定义StringIndexer
id | category |
---|---|
0 | a |
1 | b |
2 | c |
3 | d |
4 | e |
如果你没有设定StringIndexer如何处理不能识别的标签或者设定它为一个“错误”,那么一个异常将会被抛出。然而,如果你已经设定HandleInvalid(“skip”),那么将会产生如下的数据集:
id | category | categoryIndex |
---|---|---|
0 | a | 0.0 |
1 | b | 2.0 |
2 | c | 1.0 |
注意,包含"d"和”e“的行并没有出现
如果你设定HandleInvalid(“keep”),那么下列的数据集将会被产生:
id | category | categoryIndex |
---|---|---|
0 | a | 0.0 |
1 | b | 2.0 |
2 | c | 1.0 |
3 | d | 3.0 |
4 | e | 3 |
注意,这里包含"b"和“c”的列将会被映射到引用"3.0"
import org.apache.spark.ml.feature.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")
val indexed = indexer.fit(df).transform(df)
indexed.show()
与StringIndexer相对成,IndexToString将一列标签指数映射回到含有字符串类型的原始标签列。一个常见的使用场景就是从StringIndexer的标签中产生引用值,训练一个含有这些引用值的模型然后使用IndexToString从预测索引的列中检索原始标签。然而,你可以自由的生产你自己的标签。
例子:
建立于StringIndexer的例子,让我们假设我们拥有如下列为id和categoryIndex的DataFrame:
id | categoryIndex |
---|---|
0 | 0.0 |
1 | 2.0 |
2 | 1.0 |
3 | 0.0 |
4 | 0.0 |
5 | 1.0 |
将categoryIndex作为数据的列应用IndexToString,OriginalCategory作为输出列,我们能够找回我们原始的标签(它们将从列的元数据里面推断出来):
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 |
代码应用
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")
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()
独热编码将由标签指数代表的分类特征映射到有最多一个单独的值的二进制向量表明所有特征值集中存在特定特征值。这种编码使得需要连续的特征的算法,比如逻辑回归,能够使用分类特征。对于字符串类型的输入数据,通常首先使用StringIndexer对分类特征进行编码。
独热编码估计器能够转换多列,对于每个输入列都能够返回一个独热向量列。通常使用VectorAssembler将这些向量合并为单个特征向量。
OneHotEncoderEstimator支持handleInvalid参数,以选择在转换数据期间如何处理无效输入。可选的选项是‘keep’(任何无效的输入都分配给额外的分类索引)和‘error’(抛出一个错误)
import org.apache.spark.ml.feature.OneHotEncoderEstimator
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 OneHotEncoderEstimator()
.setInputCols(Array("categoryIndex1", "categoryIndex2"))
.setOutputCols(Array("categoryVec1", "categoryVec2"))
val model = encoder.fit(df)
val encoded = model.transform(df)
encoded.show()
VectorIndexer帮助索引Vector数据集中的分类特征。它既可以自动确定哪些特征是分类的,又可以将原始值转换为分类索引。具体来说,它执行以下操作:
采取类型为Vector的输入列和参数maxCategories。
根据不同值的数量确定应分类的要素,其中最多具有maxCategories的要素被声明为分类。
为每个分类特征计算从0开始的分类索引。
为分类特征建立索引,并将原始特征值转换为索引。
索引分类特征允许诸如决策树和树组合之类的算法适当地处理分类特征,从而提高性能。
例子:
在下述的例子中,我们读取了标记点的数据集,然后使用VectorIndexer决定应将哪些要素视为分类要素。我们将分类特征值转换为其索引。然后,可以将这种转换后的数据传递给处理分类特征的算法,例如DecisionTreeRegressor。
import org.apache.spark.ml.feature.VectorIndexer
val data = spark.read.format("libsvm").load("data/mllib/sample_libsvm_data.txt")
val indexer = new VectorIndexer()
.setInputCol("features")
.setOutputCol("indexed")
.setMaxCategories(10)
val indexerModel = indexer.fit(data)
val categoricalFeatures: Set[Int] = indexerModel.categoryMaps.keys.toSet
println(s"Chose ${categoricalFeatures.size} " +
s"categorical features: ${categoricalFeatures.mkString(", ")}")
// Create new column "indexed" with categorical values transformed to indices
val indexedData = indexerModel.transform(data)
indexedData.show()
Interaction是一个Transformer,它采用向量列或双值列,并生成一个向量列,其中包含来自每个输入列的一个值的所有组合的乘积。
例如,如果你有两个向量类型的列,每一个都有3维作为输入列,那么你将得到9维的向量作为输出列。
例子
假设我们有如下列为"id1",“vec1”和"vec2"的DataFrame
id1 | vec1 | vec2 |
---|---|---|
1 | [1.0,2.0,3.0] | [8.0,4.0,5.0] |
2 | [4.0,3.0,8.0] | [7.0,9.0,8.0] |
3 | [6.0,1.0,9.0] | [2.0,3.0,6.0] |
4 | [10.0,8.0,6.0] | [9.0,4.0,5.0] |
5 | [9.0,2.0,7.0] | [10.0,7.0,3.0] |
6 | [1.0,1.0,4.0] | [2.0,8.0,4.0] |
应用Interaction在这些输入列里面,然后我们就得到了输出列:InteractedCol
id1 | vec1 | vec2 | interactedCol |
---|---|---|---|
1 | [1.0,2.0,3.0] | [8.0,4.0,5.0] | [8.0,4.0,5.0,16.0,8.0,10.0,24.0,12.0,15.0] |
2 | [4.0,3.0,8.0] | [7.0,9.0,8.0] | [56.0,72.0,64.0,42.0,54.0,48.0,112.0,144.0,128.0] |
3 | [6.0,1.0,9.0] | [2.0,3.0,6.0] | [36.0,54.0,108.0,6.0,9.0,18.0,54.0,81.0,162.0] |
4 | [10.0,8.0,6.0] | [9.0,4.0,5.0] | [360.0,160.0,200.0,288.0,128.0,160.0,216.0,96.0,120.0] |
5 | [9.0,2.0,7.0] | [10.0,7.0,3.0] | [450.0,315.0,135.0,100.0,70.0,30.0,350.0,245.0,105.0] |
6 | [1.0,1.0,4.0] | [2.0,8.0,4.0] | [12.0,48.0,24.0,12.0,48.0,24.0,48.0,192.0,96.0] |
代码实现
import org.apache.spark.ml.feature.Interaction
import org.apache.spark.ml.feature.VectorAssembler
val df = spark.createDataFrame(Seq(
(1, 1, 2, 3, 8, 4, 5),
(2, 4, 3, 8, 7, 9, 8),
(3, 6, 1, 9, 2, 3, 6),
(4, 10, 8, 6, 9, 4, 5),
(5, 9, 2, 7, 10, 7, 3),
(6, 1, 1, 4, 2, 8, 4)
)).toDF("id1", "id2", "id3", "id4", "id5", "id6", "id7")
val assembler1 = new VectorAssembler().
setInputCols(Array("id2", "id3", "id4")).
setOutputCol("vec1")
val assembled1 = assembler1.transform(df)
val assembler2 = new VectorAssembler().
setInputCols(Array("id5", "id6", "id7")).
setOutputCol("vec2")
val assembled2 = assembler2.transform(assembled1).select("id1", "vec1", "vec2")
val interaction = new Interaction()
.setInputCols(Array("id1", "vec1", "vec2"))
.setOutputCol("interactedCol")
val interacted = interaction.transform(assembled2)
interacted.show(truncate = false)
归一化是一个转换器,将一个向量为行的数据集,归一化每一个向量使其有单位规范。
它采用参数p,该参数指定用于归一化的p范数。(默认p=2)。归一化可以帮助标准化你的输入数据并提升学习算法的表现
例子:
下列列子表明如果在libsvm格式下导入一个数据集并归一化每一列到单位为L1正则和L∞正则
代码示例
import org.apache.spark.ml.feature.Normalizer
import org.apache.spark.ml.linalg.Vectors
val dataFrame = spark.createDataFrame(Seq(
(0, Vectors.dense(1.0, 0.5, -1.0)),
(1, Vectors.dense(2.0, 1.0, 1.0)),
(2, Vectors.dense(4.0, 10.0, 2.0))
)).toDF("id", "features")
// Normalize each Vector using $L^1$ norm.
val normalizer = new Normalizer()
.setInputCol("features")
.setOutputCol("normFeatures")
.setP(1.0)
val l1NormData = normalizer.transform(dataFrame)
println("Normalized using L^1 norm")
l1NormData.show()
// Normalize each Vector using $L^\infty$ norm.
val lInfNormData = normalizer.transform(dataFrame, normalizer.p -> Double.PositiveInfinity)
println("Normalized using L^inf norm")
lInfNormData.show()
StandardScaler转换Vector行的数据集,将每个要素归一化以具有单位标准差和/或零均值。
它拥有以下的参数:
StandardScaler是一个估计器,可以拟合一个数据集来产生StandardScaler模型;这等价于计算摘要信息。然后,该模型可以将数据集中的Vector列转换为具有单位标准差和/或零均值特征。
请注意,如果特征的标准偏差为零,它将在向量中返回该特征的默认0.0值。
例子:
下述列子展示了如何加载libsvm类型的数据集并归一化每一个特征使其拥有单位标准偏差。
import org.apache.spark.ml.feature.StandardScaler
val dataFrame = spark.read.format("libsvm").load("data/mllib/sample_libsvm_data.txt")
val scaler = new StandardScaler().setInputCol("features").setOutputCol("scaledFeatures")
.setWithStd(true).setWithMean(false)
// Compute summary statistics by fitting the StandardScaler.
val scalerModel = scaler.fit(dataFrame)
// Normalize each feature to have unit standard deviation.
val scaledData = scalerModel.transform(dataFrame)
scaledData.show()
MinMaxScaler转换Vector行的数据集,将每个要素重新缩放到特定范围 (通常 [0, 1]). 它需要参数:
MinMaxScaler计算一个数据集上的摘要统计值,并产生一个MinMaxScaler模型。这个模型之后可以在给定的范围内转换每一个特征。
对于特征E来说,重新缩放的值计算方式如下:
Note that since zero values will probably be transformed to non-zero values, output of the transformer will be DenseVector even for sparse input.
示例代码
import org.apache.spark.ml.feature.MinMaxScaler
import org.apache.spark.ml.linalg.Vectors
val dataFrame = spark.createDataFrame(Seq(
(0, Vectors.dense(1.0, 0.1, -1.0)),
(1, Vectors.dense(2.0, 1.1, 1.0)),
(2, Vectors.dense(3.0, 10.1, 3.0))
)).toDF("id", "features")
val scaler = new MinMaxScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
// Compute summary statistics and generate MinMaxScalerModel
val scalerModel = scaler.fit(dataFrame)
// rescale each feature to range [min, max].
val scaledData = scalerModel.transform(dataFrame)
println(s"Features scaled to range: [${scaler.getMin}, ${scaler.getMax}]")
scaledData.select("features", "scaledFeatures").show()
MaxAbsScaler转换Vector行的数据集,通过除以每个要素中的最大绝对值,将每个要素重新缩放为[-1,1]范围。它不会移动/居中数据,因此不会破坏任何稀疏性。
MaxAbsScaler计算数据集的摘要统计信息,并生成MaxAbsScalerModel。然后,模型可以将每个特征分别转换为范围[-1,1]。
示例代码
import org.apache.spark.ml.feature.MaxAbsScaler
import org.apache.spark.ml.linalg.Vectors
val dataFrame = spark.createDataFrame(Seq(
(0, Vectors.dense(1.0, 0.1, -8.0)),
(1, Vectors.dense(2.0, 1.0, -4.0)),
(2, Vectors.dense(4.0, 10.0, 8.0))
)).toDF("id", "features")
val scaler = new MaxAbsScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
// Compute summary statistics and generate MaxAbsScalerModel
val scalerModel = scaler.fit(dataFrame)
// rescale each feature to range [-1, 1]
val scaledData = scalerModel.transform(dataFrame)
scaledData.select("features", "scaledFeatures").show()
Bucketizer将一列连续要素转换为一列要素存储桶,其中存储桶由用户指定。它带有一个参数:
请注意,如果您不了解目标列的上限和下限,则应添加Double.NegativeInfinity和Double.PositiveInfinity作为拆分的边界,以防止潜在的超出Bucketizer边界的异常。
还请注意,您提供的拆分必须严格按升序排列,即s0
示例代码
import org.apache.spark.ml.feature.Bucketizer
val splits = Array(Double.NegativeInfinity, -0.5, 0.0, 0.5, Double.PositiveInfinity)
val data = Array(-999.9, -0.5, -0.3, 0.0, 0.2, 999.9)
val dataFrame = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val bucketizer = new Bucketizer()
.setInputCol("features")
.setOutputCol("bucketedFeatures")
.setSplits(splits)
// Transform original data into its bucket index.
val bucketedData = bucketizer.transform(dataFrame)
println(s"Bucketizer output with ${bucketizer.getSplits.length-1} buckets")
bucketedData.show()
val splitsArray = Array(
Array(Double.NegativeInfinity, -0.5, 0.0, 0.5, Double.PositiveInfinity),
Array(Double.NegativeInfinity, -0.3, 0.0, 0.3, Double.PositiveInfinity))
val data2 = Array(
(-999.9, -999.9),
(-0.5, -0.2),
(-0.3, -0.1),
(0.0, 0.0),
(0.2, 0.4),
(999.9, 999.9))
val dataFrame2 = spark.createDataFrame(data2).toDF("features1", "features2")
val bucketizer2 = new Bucketizer()
.setInputCols(Array("features1", "features2"))
.setOutputCols(Array("bucketedFeatures1", "bucketedFeatures2"))
.setSplitsArray(splitsArray)
// Transform original data into its bucket index.
val bucketedData2 = bucketizer2.transform(dataFrame2)
println(s"Bucketizer output with [" +
s"${bucketizer2.getSplitsArray(0).length-1}, " +
s"${bucketizer2.getSplitsArray(1).length-1}] buckets for each input column")
bucketedData2.show()
ElementwiseProduct使用逐元素乘法将每个输入向量乘以提供的“权重”向量。换句话说,它通过标量乘子缩放数据集的每一列。这表示输入向量v和变换向量w之间的Hadamard乘积,以产生结果向量。
示例代码
import org.apache.spark.ml.feature.ElementwiseProduct
import org.apache.spark.ml.linalg.Vectors
// Create some vector data; also works for sparse vectors
val dataFrame = spark.createDataFrame(Seq(
("a", Vectors.dense(1.0, 2.0, 3.0)),
("b", Vectors.dense(4.0, 5.0, 6.0)))).toDF("id", "vector")
val transformingVector = Vectors.dense(0.0, 1.0, 2.0)
val transformer = new ElementwiseProduct()
.setScalingVec(transformingVector)
.setInputCol("vector")
.setOutputCol("transformedVector")
// Batch transform the vectors to create new column:
transformer.transform(dataFrame).show()
SQLTransformer实现由SQL语句定义的转换。当前,我们仅支持SQL语法,例如“ SELECT … FROM THIS …”,其中“ THIS”代表输入数据集的基础表。 select子句指定要在输出中显示的字段,常量和表达式,并且可以是Spark SQL支持的任何select子句。用户还可以使用Spark SQL内置函数和UDF对这些选定的列进行操作。例如,SQLTransformer支持以下语句:
示例代码
有关该API的更多详细信息,请参考SQLTransformer Scala文档。
import org.apache.spark.ml.feature.SQLTransformer
val df = spark.createDataFrame(
Seq((0, 1.0, 3.0), (2, 2.0, 5.0))).toDF("id", "v1", "v2")
val sqlTrans = new SQLTransformer().setStatement(
"SELECT *, (v1 + v2) AS v3, (v1 * v2) AS v4 FROM __THIS__")
sqlTrans.transform(df).show()
VectorAssembler是一种转换器,它将给定的列列表组合为单个向量列。这对于将原始特征和由不同特征转换器生成的特征合并到单个特征向量中很有用,以便训练逻辑模型回归和决策树之类的ML模型。 VectorAssembler接受以下输入列类型:所有数字类型,布尔类型和向量类型。在每一行中,输入列的值将按指定顺序连接到向量中。
示例代码
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.linalg.Vectors
val dataset = spark.createDataFrame(
Seq((0, 18, 1.0, Vectors.dense(0.0, 10.0, 0.5), 1.0))
).toDF("id", "hour", "mobile", "userFeatures", "clicked")
val assembler = new VectorAssembler()
.setInputCols(Array("hour", "mobile", "userFeatures"))
.setOutputCol("features")
val output = assembler.transform(dataset)
println("Assembled columns 'hour', 'mobile', 'userFeatures' to vector column 'features'")
output.select("features", "clicked").show(false)
有时为VectorType的列显式指定向量的大小可能很有用。例如,VectorAssembler使用其输入列中的大小信息来为其输出列生成大小信息和元数据。尽管在某些情况下可以通过检查列的内容来获取此信息,但是在流数据帧中,只有在启动流之后,内容才可用VectorSizeHint允许用户显式指定列的向量大小,以便VectorAssembler或可能需要知道向量大小的其他转换器可以将该列用作输入。
要使用VectorSizeHint,用户必须设置inputCol和size参数。将此转换器应用于数据框将生成一个新的数据框,其中包含用于inputCol的更新元数据,以指定矢量大小。生成的数据帧上的下游操作可以使用Meatadata获得此大小。
VectorSizeHint也可以采用可选的handleInvalid参数,当vector列包含null或错误大小的vector时,该参数控制其行为。默认情况下,handleInvalid设置为“错误”,指示应引发异常。此参数也可以设置为“跳过”,指示应从结果数据框中过滤出包含无效值的行,或“乐观”,指示不应检查该列的无效值,而应保留所有行。请注意,使用“乐观”可能导致结果数据帧处于不一致状态,即:VectorVectorHint列应用于的元数据与该列的内容不匹配。用户应注意避免这种不一致的状态。
示例代码
import org.apache.spark.ml.feature.{VectorAssembler, VectorSizeHint}
import org.apache.spark.ml.linalg.Vectors
val dataset = spark.createDataFrame(
Seq(
(0, 18, 1.0, Vectors.dense(0.0, 10.0, 0.5), 1.0),
(0, 18, 1.0, Vectors.dense(0.0, 10.0), 0.0))
).toDF("id", "hour", "mobile", "userFeatures", "clicked")
val sizeHint = new VectorSizeHint()
.setInputCol("userFeatures")
.setHandleInvalid("skip")
.setSize(3)
val datasetWithSize = sizeHint.transform(dataset)
println("Rows where 'userFeatures' is not the right size are filtered out")
datasetWithSize.show(false)
val assembler = new VectorAssembler()
.setInputCols(Array("hour", "mobile", "userFeatures"))
.setOutputCol("features")
// This dataframe can be used by downstream transformers as before
val output = assembler.transform(datasetWithSize)
println("Assembled columns 'hour', 'mobile', 'userFeatures' to vector column 'features'")
output.select("features", "clicked").show(false)
QuantileDiscretizer接收具有连续特征的列,并输出具有合并分类特征的列。箱数由numBuckets参数设置。例如,如果输入的不同值太少而无法创建足够的不同分位数,则所使用的存储桶的数量可能会小于该值。
NaN值:在QuantileDiscretizer拟合期间,将从柱中除去NaN值。这将产生一个Bucketizer模型进行预测。在转换期间,Bucketizer在数据集中找到NaN值时将引发错误,但用户也可以通过设置handleInvalid选择保留还是删除数据集中的NaN值。如果用户选择保留NaN值,则将对其进行特殊处理并将其放入自己的存储桶中,例如,如果使用4个存储桶,则将非NaN数据放入存储桶[0-3]中,但NaN将被存储放在一个特殊的桶中[4]。
算法:bin范围是使用近似算法选择的(有关详细说明,请参见aboutQuantile的文档)。可以使用relativeError参数控制近似精度。设置为零时,将计算精确的分位数(注意:计算精确的分位数是一项昂贵的操作)。 bin的上下边界将是-Infinity和+ Infinity,覆盖所有实数值。
示例代码
有关该API的更多详细信息,请参考QuantileDiscretizer Scala文档。
import org.apache.spark.ml.feature.QuantileDiscretizer
val data = Array((0, 18.0), (1, 19.0), (2, 8.0), (3, 5.0), (4, 2.2))
val df = spark.createDataFrame(data).toDF("id", "hour")
val discretizer = new QuantileDiscretizer()
.setInputCol("hour")
.setOutputCol("result")
.setNumBuckets(3)
val result = discretizer.fit(df).transform(df)
result.show(false)
Imputer估计器使用缺失值所在列的平均值或中位数来完成数据集中的缺失值。输入列应为DoubleType或FloatType。当前,Imputer不支持分类特征,并且可能为包含分类特征的列创建不正确的值。 Imputer可以通过.setMissingValue(custom_value)插入“ NaN”以外的自定义值。例如,.setMissingValue(0)将估算所有出现的(0)。
请注意,输入列中的所有空值都被视为丢失,因此也会被估算。
示例代码
import org.apache.spark.ml.feature.Imputer
val df = spark.createDataFrame(Seq(
(1.0, Double.NaN),
(2.0, Double.NaN),
(Double.NaN, 3.0),
(4.0, 4.0),
(5.0, 5.0)
)).toDF("a", "b")
val imputer = new Imputer()
.setInputCols(Array("a", "b"))
.setOutputCols(Array("out_a", "out_b"))
val model = imputer.fit(df)
model.transform(df).show()
VectorSlicer是一个转换器可以将特征向量输出为一个新的原始特征的子序列的特征向量。当从一个向量列里面抽取特征时它是有用的。VectorSlicer接受具有指定索引的向量列,然后输出一个新的向量列,其值通过这些索引选择。
这里有两种索引,
指定为整数和字符都可以接受。另外,你可以同时使用整数和字符索引,而且至少要选择一个特征。复制的特征将不会被允许,所以选择的索引和名字之间将不会有重合。注意如果特征的名字被选择了,则在遇到空的输入属性时将引发异常。输出的特征将会首先按照选中的索引进行排序(在给定的顺序下),然后是根据名字进行索引(在给定的名字下)。
例子:
假定我们有一个列为userFeatures的DataFrames:
Suppose that we have a DataFrame with the column userFeatures:
userFeatures是一个包含了三个用户特征的向量列。假定userFeatures的第一列都是零,所以我们想要去除它然后只选择后面的两列。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”, “f3”] |
import java.util.Arrays
import org.apache.spark.ml.attribute.{Attribute, AttributeGroup, NumericAttribute}
import org.apache.spark.ml.feature.VectorSlicer
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.types.StructType
val data = Arrays.asList(
Row(Vectors.sparse(3, Seq((0, -2.0), (1, 2.3)))),
Row(Vectors.dense(-2.0, 2.3, 0.0))
)
val defaultAttr = NumericAttribute.defaultAttr
val attrs = Array("f1", "f2", "f3").map(defaultAttr.withName)
val attrGroup = new AttributeGroup("userFeatures", attrs.asInstanceOf[Array[Attribute]])
val dataset = spark.createDataFrame(data, StructType(Array(attrGroup.toStructField())))
val slicer = new VectorSlicer().setInputCol("userFeatures").setOutputCol("features")
slicer.setIndices(Array(1)).setNames(Array("f3"))
// or slicer.setIndices(Array(1, 2)), or slicer.setNames(Array("f2", "f3"))
val output = slicer.transform(dataset)
output.show(false)
RFormula选择由R模型公式指定的列。当前,我们支持R操作符的有限子集,包括“〜”,“。”,“:”,“ +”和“-”。基本运算符为:
假设a和b是双列,我们使用以下简单示例说明RFormula的效果:
RFormula产生要素的向量列和标签的双列或字符串列。就像在R中使用公式进行线性回归时一样,数字列将转换为双精度。对于字符串输入列,将首先使用由stringOrderType确定的顺序使用StringIndexer对其进行转换,并删除排序后的最后一个类别,然后将对double进行一次热编码。
假设一个字符串要素列包含值{‘b’,‘a’,‘b’,‘a’,‘c’,‘b’},我们设置stringOrderType来控制编码:
stringOrderType | Category mapped to 0 by StringIndexer | Category dropped by RFormula |
---|---|---|
‘frequencyDesc’ | most frequent category (‘b’) | least frequent category (‘c’) |
‘frequencyAsc’ | least frequent category (‘c’) | most frequent category (‘b’) |
‘alphabetDesc’ | last alphabetical category (‘c’) | first alphabetical category (‘a’) |
‘alphabetAsc’ | first alphabetical category (‘a’) | last alphabetical category (‘c’) |
如果标签列的类型为字符串,则将首先使用frequencyDesc顺序使用StringIndexer将其转换为double。如果DataFrame中不存在label列,则将从公式中指定的响应变量创建输出label列。
注意:排序选项stringOrderType不用于标签列。索引标签列后,它将使用StringIndexer中的默认降序频率排序。
例子
假设我们有一个带有ID,国家,小时和单击列的DataFrame:
id | country | hour | clicked |
---|---|---|---|
7 | “US” | 18 | 1.0 |
8 | “CA” | 12 | 0.0 |
9 | “NZ” | 15 | 0.0 |
如果我们将RFormula与带有单击的〜国家+小时的公式字符串一起使用,这表示我们要基于国家和小时来预测点击,则在转换之后,我们应该获得以下DataFrame:
id | country | hour | clicked | features | label |
---|---|---|---|---|---|
7 | “US” | 18 | 1.0 | [0.0, 0.0, 18.0] | 1.0 |
8 | “CA” | 12 | 0.0 | [0.0, 1.0, 12.0] | 0.0 |
9 | “NZ” | 15 | 0.0 | [1.0, 0.0, 15.0] | 0.0 |
import org.apache.spark.ml.feature.RFormula
val dataset = spark.createDataFrame(Seq(
(7, "US", 18, 1.0),
(8, "CA", 12, 0.0),
(9, "NZ", 15, 0.0)
)).toDF("id", "country", "hour", "clicked")
val formula = new RFormula().setFormula("clicked ~ country + hour")
.setFeaturesCol("features").setLabelCol("label")
val output = formula.fit(dataset).transform(dataset)
output.select("features", "label").show()
ChiSqSelector代表Chi-Squared特征选择。它对具有分类特征的标记数据进行操作。 ChiSqSelector使用卡方独立性检验来决定选择哪些功能。它支持五种选择方法:numTopFeatures,percentile,fpr,fdr,fwe:
例子:
假设我们有一个DataFrame,具有列ID,features和clicked,其中clicked是我们要预测的目标变量:
id | features | clicked |
---|---|---|
7 | [0.0, 0.0, 18.0, 1.0] | 1.0 |
8 | [0.0, 1.0, 12.0, 0.0] | 0.0 |
9 | [1.0, 0.0, 15.0, 0.1] | 0.0 |
如果我们使用ChiSqSelector并设定numTopFeatures=1,那么在最后一列的标签clicked作为特征将会被选中作为最有用的特征:
id | features | clicked | selectedFeatures |
---|---|---|---|
7 | [0.0, 0.0, 18.0, 1.0] | 1.0 | [1.0] |
8 | [0.0, 1.0, 12.0, 0.0] | 0.0 | [0.0] |
9 | [1.0, 0.0, 15.0, 0.1] | 0.0 | [0.1] |
import org.apache.spark.ml.feature.ChiSqSelector
import org.apache.spark.ml.linalg.Vectors
val data = Seq(
(7, Vectors.dense(0.0, 0.0, 18.0, 1.0), 1.0),
(8, Vectors.dense(0.0, 1.0, 12.0, 0.0), 0.0),
(9, Vectors.dense(1.0, 0.0, 15.0, 0.1), 0.0))
val df = spark.createDataset(data).toDF("id", "features", "clicked")
val selector = new ChiSqSelector()
.setNumTopFeatures(1)
.setFeaturesCol("features")
.setLabelCol("clicked")
.setOutputCol("selectedFeatures")
val result = selector.fit(df).transform(df)
println(s"ChiSqSelector output with top ${selector.getNumTopFeatures} features selected")
result.show()
Locality Sensitive Hashing (LSH) 是一类重要的哈希技术,常用于聚类、近似最近邻居搜索和大数据的异常点探测。
LSH的基本观点是使用一族的函数(“LSH簇”)将数据点散列到存储桶中,这样彼此相近的点将有很高的概率被放入同一个桶里面,彼此距离很远的点将很有可能被放入到不同的桶里面。一个LSH簇被定义为如下:
在一个矩阵空间(M , d)中,M是一个集合,d是在M中的一个距离公式,一个LSH簇是一簇函数,满足如下的性质:
在Spark中,不用的LSH家族将会在分开的类别里面实施(例如:MinHash)并且在每一个类别里面会有不同的特征转换的API、近似相似连接和近似最近邻居。
在LSH中,我们定义一个false positive是一对遥远的输入特征(具有d(p,q)>2),将会被散列到同一个桶中,并且我们定义一个false negative是一堆相近的特征(具有d(p,q)<=1),将会被散列到不同的桶中。
我们描述了LSH可以被使用的主要的操作。一个拟合好的模型将会有针对这些操作的方法
(1)Feature Transformation(特征转换)
特征转换是将哈希值添加为新列的基本功能。这对于减少尺寸很有用。用户可以通过设置inputCol和outputCol来指定输入和输出列的名称。
LSH还支持多个LSH哈希表。用户可以通过设置numHashTables来指定哈希表的数量。这也用于近似相似连接和近似最近邻中的OR放大。散列表的数量增加将提高准确性,但也会增加通信成本和运行时间。
outputCol的类型为Seq [Vector],其中数组的维数等于numHashTables,向量的维数当前设置为1。在将来的版本中,我们将实现AND放大,以便用户可以指定这些向量的维数。
(2)Approximates Similarity Join(近似相似连接)
近似相似联接采用两个数据集,并近似返回数据集中距离小于用户定义阈值的行对。近似相似联接既支持联接两个不同的数据集,也支持自联接。自连接会产生一些重复的对。
近似相似性联接接受已转换和未转换的数据集作为输入。如果使用未转换的数据集,它将被自动转换。在这种情况下,哈希签名将创建为outputCol。
在合并的数据集中,可以在数据集A和数据集B中查询原始数据集。距离列将添加到输出数据集中,以显示返回的每对行之间的真实距离。
(3)Approximate Nearest Neighbor Search(近似最近邻居搜索)
近似最近邻居搜索采用(特征向量的)数据集和键(单个特征向量),并近似返回数据集中最接近向量的指定行数。
近似最近邻搜索将已转换和未转换的数据集都接受为输入。如果使用未转换的数据集,它将被自动转换。在这种情况下,哈希签名将创建为outputCol。
距离列将添加到输出数据集中,以显示每个输出行和搜索到的键之间的真实距离。
注意:如果哈希存储桶中没有足够的候选者,则近似最近邻居搜索将返回少于k行的结果。
(1)Bucketed Random Projection for Euclidean Distance
Bucketed Random Projection是一个用于欧几里得距离的LBH簇,欧几里得距离定义如下:
它的LSH簇投影特征向量x到一个随机单位向量并且将预测结果分配到哈希桶中
其中r是一个自定义的桶长度。这个桶的长度同样可以被用于控制哈希桶的平均尺寸(因此也可以是这个桶的数量)。一个更大的桶长度(例如更少数量的哈希桶)会提升特征被散列到同一个哈希桶的可能性(提升true和false positives的数量)
桶状随机投影接受任意矢量作为输入特征,并支持稀疏矢量和密集矢量。
import org.apache.spark.ml.feature.BucketedRandomProjectionLSH
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.col
val dfA = spark.createDataFrame(Seq(
(0, Vectors.dense(1.0, 1.0)),
(1, Vectors.dense(1.0, -1.0)),
(2, Vectors.dense(-1.0, -1.0)),
(3, Vectors.dense(-1.0, 1.0))
)).toDF("id", "features")
val dfB = spark.createDataFrame(Seq(
(4, Vectors.dense(1.0, 0.0)),
(5, Vectors.dense(-1.0, 0.0)),
(6, Vectors.dense(0.0, 1.0)),
(7, Vectors.dense(0.0, -1.0))
)).toDF("id", "features")
val key = Vectors.dense(1.0, 0.0)
val brp = new BucketedRandomProjectionLSH()
.setBucketLength(2.0)
.setNumHashTables(3)
.setInputCol("features")
.setOutputCol("hashes")
val model = brp.fit(dfA)
// Feature Transformation
println("The hashed dataset where hashed values are stored in the column 'hashes':")
model.transform(dfA).show()
// Compute the locality sensitive hashes for the input rows, then perform approximate
// similarity join.
// We could avoid computing hashes by passing in the already-transformed dataset, e.g.
// `model.approxSimilarityJoin(transformedA, transformedB, 1.5)`
println("Approximately joining dfA and dfB on Euclidean distance smaller than 1.5:")
model.approxSimilarityJoin(dfA, dfB, 1.5, "EuclideanDistance")
.select(col("datasetA.id").alias("idA"),
col("datasetB.id").alias("idB"),
col("EuclideanDistance")).show()
// Compute the locality sensitive hashes for the input rows, then perform approximate nearest
// neighbor search.
// We could avoid computing hashes by passing in the already-transformed dataset, e.g.
// `model.approxNearestNeighbors(transformedA, key, 2)`
println("Approximately searching dfA for 2 nearest neighbors of the key:")
model.approxNearestNeighbors(dfA, key, 2).show()
(2)Minhash for Jaccard Distance
MinHash是Jaccard距离的LSH系列,其中输入要素是自然数集。两组的Jaccard距离由其交集和并集的基数定义:
MinHash将随机哈希函数g应用于集合中的每个元素,并采用所有哈希值中的最小值:
MinHash的输入集表示为二进制向量,其中向量索引表示元素本身,向量中的非零值表示该元素在集合中的存在。虽然同时支持密集和稀疏向量,但通常建议使用稀疏向量以提高效率。例如,Vectors.sparse(10,Array [(2,1.0),(3,1.0),(5,1.0)])表示空间中有10个元素。该集合包含elem 2,elem 3和elem5。所有非零值都被视为二进制“ 1”值。
注意:MinHash不能转换空集,这意味着任何输入向量必须至少具有1个非零条目。
import org.apache.spark.ml.feature.MinHashLSH
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.col
val dfA = spark.createDataFrame(Seq(
(0, Vectors.sparse(6, Seq((0, 1.0), (1, 1.0), (2, 1.0)))),
(1, Vectors.sparse(6, Seq((2, 1.0), (3, 1.0), (4, 1.0)))),
(2, Vectors.sparse(6, Seq((0, 1.0), (2, 1.0), (4, 1.0))))
)).toDF("id", "features")
val dfB = spark.createDataFrame(Seq(
(3, Vectors.sparse(6, Seq((1, 1.0), (3, 1.0), (5, 1.0)))),
(4, Vectors.sparse(6, Seq((2, 1.0), (3, 1.0), (5, 1.0)))),
(5, Vectors.sparse(6, Seq((1, 1.0), (2, 1.0), (4, 1.0))))
)).toDF("id", "features")
val key = Vectors.sparse(6, Seq((1, 1.0), (3, 1.0)))
val mh = new MinHashLSH()
.setNumHashTables(5)
.setInputCol("features")
.setOutputCol("hashes")
val model = mh.fit(dfA)
// Feature Transformation
println("The hashed dataset where hashed values are stored in the column 'hashes':")
model.transform(dfA).show()
// Compute the locality sensitive hashes for the input rows, then perform approximate
// similarity join.
// We could avoid computing hashes by passing in the already-transformed dataset, e.g.
// `model.approxSimilarityJoin(transformedA, transformedB, 0.6)`
println("Approximately joining dfA and dfB on Jaccard distance smaller than 0.6:")
model.approxSimilarityJoin(dfA, dfB, 0.6, "JaccardDistance")
.select(col("datasetA.id").alias("idA"),
col("datasetB.id").alias("idB"),
col("JaccardDistance")).show()
// Compute the locality sensitive hashes for the input rows, then perform approximate nearest
// neighbor search.
// We could avoid computing hashes by passing in the already-transformed dataset, e.g.
// `model.approxNearestNeighbors(transformedA, key, 2)`
// It may return less than 2 rows when not enough approximate near-neighbor candidates are
// found.
println("Approximately searching dfA for 2 nearest neighbors of the key:")
model.approxNearestNeighbors(dfA, key, 2).show()