Extracting, transforming and selecting features
这一大章节讲的内容主要是与特征工程相关的算法,粗略的可以分为如下几类:
- Extraction:从Raw数据中提取出特征
- Transformation:Scaling, converting, or modifying features
- Selection:从大的特征集合中挑选一个子集
- 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中的使用情况简单说明:
TF:HashingTF与CountVectorizer都可以用于生成词频TF向量。
其中HashingTF是一个需要特征词集的转换器(Transformer),它可以将这些集合转换成固定长度的特征向量。CountVectorizer将文本文档转换为关键词计数的向量。
IDF:IDF是一个适合数据集并生成IDFModel的评估器(Estimator),IDFModel获取特征向量(通常由HashingTF或CountVectorizer创建)并缩放每列。直观地说,它下调了在语料库中频繁出现的列。
代码示例(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();
//最后得到的特征向量可以作为其他机器学习算法的输入
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();
}
}
Feature Transformers
下图是对特征转换的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();
}
}
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();
}
}
如果对于sparse向量不使用toDense方法那么结果就是对sparse结构的数据进行slice操作,结果如下:
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行。