Extracting, transforming and selecting features
這一大章節(jié)講的內(nèi)容主要是與特征工程相關(guān)的算法,粗略的可以分為如下幾類:
- Extraction:從Raw數(shù)據(jù)中提取出特征
- Transformation:Scaling, converting, or modifying features
- Selection:從大的特征集合中挑選一個(gè)子集
- Locality Sensitive Hashing (LSH):這類算法將特征變換的方面與其他算法相結(jié)合啊送。
Feature Extractors
TF-IDF
TF-IDF是Term frequency(詞頻)-inverse document frequency(逆文本頻率指數(shù))的縮寫辛馆。是一種在文本挖掘中廣泛使用的特征向量化方法唯卖,以反映一個(gè)單詞在語料庫中的重要性骄噪。定義:t 表示由一個(gè)單詞诸尽,d 表示一個(gè)文檔,D 表示語料庫(corpus)挨决,詞頻 TF(t,d) 表示某一個(gè)給定的單詞 t 出現(xiàn)在文檔 d 中的次數(shù)(單詞次數(shù))请祖, 而文檔頻率 DF(t,D) 表示包含單詞 t 的文檔次數(shù)。如果我們只使用詞頻 TF 來衡量重要性脖祈,則很容易過分強(qiáng)調(diào)出現(xiàn)頻率過高并且文檔包含少許信息的單詞肆捕,例如,'a'盖高,'the'慎陵,和 'of'眼虱。如果一個(gè)單詞在整個(gè)語料庫中出現(xiàn)的非常頻繁,這意味著它并沒有攜帶特定文檔的某些特殊信息(換句話說席纽,該單詞對(duì)整個(gè)文檔的重要程度低)捏悬。逆向文檔頻率是一個(gè)數(shù)字量度,表示一個(gè)單詞提供了多少信息:
其中润梯,|D| 是在語料庫中文檔總數(shù)过牙。由于使用對(duì)數(shù),所以如果一個(gè)單詞出現(xiàn)在所有的文件纺铭,其IDF值變?yōu)?寇钉。注意,應(yīng)用平滑項(xiàng)以避免在語料庫之外的項(xiàng)除以零(為了防止分母為0彤蔽,分母需要加1)摧莽。因此,TF-IDF測量只是TF和IDF的產(chǎn)物:(對(duì)TF-IDF定義為TF和IDF的乘積)
關(guān)于詞頻TF和文檔頻率DF的定義有多種形式顿痪。在MLlib镊辕,我們分離TF和IDF,使其靈活蚁袭。下面是MLlib中的使用情況簡單說明:
TF:HashingTF與CountVectorizer都可以用于生成詞頻TF向量征懈。
其中HashingTF是一個(gè)需要特征詞集的轉(zhuǎn)換器(Transformer),它可以將這些集合轉(zhuǎn)換成固定長度的特征向量揩悄。CountVectorizer將文本文檔轉(zhuǎn)換為關(guān)鍵詞計(jì)數(shù)的向量卖哎。
IDF:IDF是一個(gè)適合數(shù)據(jù)集并生成IDFModel的評(píng)估器(Estimator),IDFModel獲取特征向量(通常由HashingTF或CountVectorizer創(chuàng)建)并縮放每列删性。直觀地說亏娜,它下調(diào)了在語料庫中頻繁出現(xiàn)的列。
代碼示例(Java版)
//創(chuàng)建Row的數(shù)據(jù)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")
);
//創(chuàng)建Schema給Row定義數(shù)據(jù)類型和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將句子中的單詞哈希成特征向量(這個(gè)可以在前一章節(jié)的最后輸出打印截圖中看到具體的值)
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對(duì)上面產(chǎn)生的特征向量進(jìn)行rescale
IDF idf = new IDF().setInputCol("rawFeatures").setOutputCol("features");
IDFModel idfModel = idf.fit(featurizedData); //fit得到IDF的模型
Dataset<Row> rescaledData = idfModel.transform(featurizedData); //對(duì)特征向量進(jìn)行rescale
rescaledData.select("label", "features").show();
//最后得到的特征向量可以作為其他機(jī)器學(xué)習(xí)算法的輸入
Word2Vec
Word2Vec是一個(gè)Estimator(評(píng)估器)蹬挺,它采用表示文檔的單詞序列维贺,并訓(xùn)練一個(gè)Word2VecModel。 該模型將每個(gè)單詞映射到一個(gè)唯一的固定大小向量巴帮。 Word2VecModel使用文檔中所有單詞的平均值將每個(gè)文檔轉(zhuǎn)換為向量; 該向量然后可用作預(yù)測溯泣,文檔相似性計(jì)算等功能。有關(guān)更多詳細(xì)信息榕茧,請(qǐng)參閱有關(guān)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);
//創(chuàng)建Word2Vec的實(shí)例,然后設(shè)置參數(shù)
// 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); //對(duì)輸入進(jìn)行transform得到結(jié)果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
下圖是對(duì)特征轉(zhuǎn)換的API doc中列出的算法用押。不過這里不打算把每個(gè)都展開描述了肢簿,會(huì)簡單舉例幾個(gè),然后后面實(shí)際用到哪個(gè)再去查哪個(gè)。
Tokenizer
Tokenization(文本符號(hào)化)是將文本 (如一個(gè)句子)拆分成單詞的過程译仗。(在Spark ML中)Tokenizer(分詞器)提供此功能抬虽。下面的示例演示如何將句子拆分為詞的序列官觅。
RegexTokenizer 提供了(更高級(jí)的)基于正則表達(dá)式 (regex) 匹配的(對(duì)句子或文本的)單詞拆分纵菌。默認(rèn)情況下,參數(shù)"pattern"(默認(rèn)的正則表達(dá)式: "\\s+"
休涤,此時(shí)和Tokenizer沒有區(qū)別) 作為分隔符用于拆分輸入的文本咱圆。或者功氨,用戶可以將參數(shù)“gaps”設(shè)置為 false(不然默認(rèn)true的情況下分割出來的結(jié)果是分隔符的集合序苏,而不是單詞的集合),指定正則表達(dá)式"pattern"表示"tokens"捷凄,而不是分隔符忱详,這樣作為劃分結(jié)果找到的所有匹配項(xiàng)
代碼示例(Java版)
public class JavaTokenizerExample {
public static void main(String[] args) {
SparkSession spark = SparkSession
.builder()
.appName("JavaTokenizerExample")
.getOrCreate();
//構(gòu)造輸入數(shù)據(jù),注意第三行的數(shù)據(jù)word之間只有逗號(hào)沒有空格
// $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來讓正則表達(dá)的劃分按照非字母來做
RegexTokenizer regexTokenizer = new RegexTokenizer()
.setInputCol("sentence")
.setOutputCol("words")
.setPattern("\\W"); // alternatively .setPattern("\\w+").setGaps(false); 換成這句話一樣的結(jié)果跺涤。
//注冊(cè)一個(gè)user-defined functions匈睁,第一個(gè)參數(shù)是udf的名字,第二個(gè)參數(shù)是一個(gè)自定義的轉(zhuǎn)換函數(shù)桶错。
spark.udf().register("countTokens", new UDF1<WrappedArray, Integer>() {
@Override
public Integer call(WrappedArray words) {
return words.size();
}
}, DataTypes.IntegerType);
//按照空格劃分,結(jié)果第三行沒有劃分,當(dāng)作整體對(duì)待葛碧。
Dataset<Row> tokenized = tokenizer.transform(sentenceDataFrame);
tokenized.select("sentence", "words")
.withColumn("tokens", callUDF("countTokens", col("words"))).show(false);
//按照正則表達(dá)式對(duì)待尊浓,把非字母的地方劃分了。
Dataset<Row> regexTokenized = regexTokenizer.transform(sentenceDataFrame);
regexTokenized.select("sentence", "words")
.withColumn("tokens", callUDF("countTokens", col("words"))).show(false);
// $example off$
spark.stop();
}
}
VectorAssembler(特征向量合并)
【這個(gè)API超級(jí)有用退腥!】VectorAssembler 是將指定的一list的列合并到單個(gè)列向量中的 transformer任岸。它可以將原始特征和不同特征transformers(轉(zhuǎn)換器)生成的特征合并為單個(gè)特征向量,來訓(xùn)練 ML 模型,如邏輯回歸和決策樹等機(jī)器學(xué)習(xí)算法狡刘。VectorAssembler 可接受以下的輸入列類型:任何數(shù)值型享潜、布爾類型、向量類型颓帝。輸入列的值將按指定順序依次添加到一個(gè)向量中米碰。
舉例
假設(shè)現(xiàn)在有一個(gè)DataFrame,它的列為:id, hour, mobile, userFeatures和clicked:
id | hour | mobile | userFeatures | clicked |
---|---|---|---|---|
0 | 18 | 1.0 | [0.0, 10.0, 0.5] | 1.0 |
其中userFeatures是一個(gè)列向量包含三個(gè)用戶特征」撼牵現(xiàn)在想把hour, mobile, 和userFeatures合并到一個(gè)一個(gè)特征向量中(名為features吕座,這個(gè)也是很多MLlib算法默認(rèn)的特征輸入向量名字),然后用來預(yù)測clicked的值瘪板。具體用法就是將列hour吴趴、mobile和userFeatures作為input,features作為output侮攀,然后調(diào)用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();
}
}
運(yùn)行結(jié)果:
-------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厢拭,后續(xù)如果自己用到會(huì)再補(bǔ)充。
VectorSlicer(向量切片機(jī))
VectorSlicer是一個(gè)轉(zhuǎn)換器撇叁,它對(duì)于輸入的特征向量供鸠,輸出一個(gè)新的原始特征子集的特征向量。對(duì)于從列向量中提取特征很有幫助陨闹。
VectorSlicer對(duì)于指定索引的列向量楞捂,輸出一個(gè)新的列向量,所選擇的列向量通過這些索引進(jìn)行選擇趋厉。有兩種類型的索引:
- 整數(shù)索引:代表列向量的下標(biāo)寨闹,setIndices()
- 字符串索引:代表列的特征名稱,setNames()君账。這要求列向量有AttributeGroup繁堡,因?yàn)閷?shí)現(xiàn)中是在Attribute上的name字段匹配。
整數(shù)和字符串的索引都可以接受乡数。此外椭蹄,還可以同時(shí)使用整數(shù)索引和字符串名稱索引。但必須至少選擇一個(gè)特征瞳脓。重復(fù)的特征選擇是不允許的塑娇,所以選擇的索引和名稱之間不能有重疊。請(qǐng)注意劫侧,如果選擇了特征的名稱索引埋酬,則遇到空的輸入屬性時(shí)會(huì)拋出異常。
輸出時(shí)將按照選擇中給出的特征索引的先后順序進(jìn)行向量及其名稱的輸出烧栋。
舉例:
假設(shè)有一個(gè)DataFrame它的列名(AttributeGroup)為userFeatures
userFeatures |
---|
[0.0, 10.0, 0.5] |
userFeatures是一個(gè)包含三個(gè)用戶特征的列向量写妥。假設(shè)userFeature的第一列全部為0,因此我們要?jiǎng)h除它并僅選擇最后兩列审姓。VectorSlicer使用setIndices(1,2)選擇最后兩個(gè)元素珍特,然后生成一個(gè)名為features的新向量列:
userFeatures | features |
---|---|
[0.0, 10.0, 0.5] | 10.0, 0.5 |
如果userFeatures已經(jīng)輸入了屬性值["f1", "f2", "f3"]魔吐,那么我們可以使用setNames("f2", "f3")來選擇它們扎筒。
userFeatures | features |
---|---|
[0.0, 10.0, 0.5] | 10.0, 0.5 |
["f1", "f2", "f3"] | ["f2", "f2"] |
下面是一個(gè)Java代碼示例:
public class JavaVectorSlicerExample {
public static void main(String[] args) {
SparkSession spark = SparkSession
.builder()
.appName("JavaVectorSlicerExample")
.getOrCreate();
//構(gòu)造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);
//構(gòu)造數(shù)據(jù)
List<Row> data = Lists.newArrayList(
RowFactory.create(Vectors.sparse(3, new int[]{0, 1}, new double[]{-2.0, 2.3}).toDense()), //這里必須使用toDense()來避免sprse的數(shù)據(jù)結(jié)構(gòu)引起下面的切片時(shí)的問題酬姆。
RowFactory.create(Vectors.dense(-2.0, 2.3, 0.0)) //dense和sparse的區(qū)別在與sparse是稀疏的適合大量0數(shù)據(jù)的構(gòu)造嗜桌,dense是把每個(gè)數(shù)值都要賦值的適合非稀疏的情況。
);
Dataset<Row> dataset =
spark.createDataFrame(data, (new StructType()).add(group.toStructField()));
System.out.println("\n=======Original DataFrame is:");
dataset.show(false);
//構(gòu)造VectorSlicer設(shè)置輸入列為"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();
}
}
如果對(duì)于sparse向量不使用toDense方法那么結(jié)果就是對(duì)sparse結(jié)構(gòu)的數(shù)據(jù)進(jìn)行slice操作骨宠,結(jié)果如下:
Locality Sensitive Hashing
LSH是哈希技術(shù)中重要的一種,通常用于集群层亿,近似最近鄰搜索和大型數(shù)據(jù)集的孤立點(diǎn)檢測桦卒。
LSH的大致思路是用一系列函數(shù)(LSH families)將數(shù)據(jù)哈希到桶中,這樣彼此接近的數(shù)據(jù)點(diǎn)處于相同的桶中可能性就會(huì)很高匿又,而彼此相距很遠(yuǎn)的數(shù)據(jù)點(diǎn)很可能處于不同的桶中方灾。一個(gè)LSH family 正式定義如下。
在度量空間(M,d)中琳省,M是一個(gè)集合迎吵,d是M上的一個(gè)距離函數(shù)躲撰,LSH family是一系列能滿足以下屬性的函數(shù)h:
滿足以上條件的LSH family被稱為(r1, r2, p1, p2)-sensitive针贬。
在Spark中,不同的LSH families實(shí)現(xiàn)在不同的類中(例如:MinHash)拢蛋,并且在每個(gè)類中提供了用于特征變換的API桦他,近似相似性連接和近似最近鄰。
在LSH中谆棱,我們將一個(gè)假陽性定義為一對(duì)相距大的輸入特征(當(dāng) d(p,q)≥r2 時(shí))快压,它們被哈希到同一個(gè)桶中,并且將一個(gè)假陰性定義為一對(duì)相鄰的特征(當(dāng) d(p,q)≤r1 時(shí) )垃瞧,它們被分散到不同的桶中蔫劣。
LSH Operations(LSH運(yùn)算)
我們描述了大部分LSH會(huì)用到的運(yùn)算,每一個(gè)合適的LSH模型都有自己的方法實(shí)現(xiàn)了這些運(yùn)算个从。
Feature Transformation(特征變換)
特征變換是將哈希值添加為新列的基本功能脉幢。 這可以有助于降低維數(shù)。 用戶可以通過設(shè)置 inputCol 和 outputCol 參數(shù)來指定輸入和輸出列名嗦锐。
LSH 還支持多個(gè)LSH哈希表嫌松。 用戶可以通過設(shè)置 numHashTables 來指定哈希表的數(shù)量。 這也用于近似相似性連接和近似最近鄰的 OR-amplification(或放大器)放大奕污。 增加哈希表的數(shù)量將增加準(zhǔn)確性萎羔,但也會(huì)增加通信成本和運(yùn)行時(shí)間。
outputCol 的類型是 Seq [Vector]碳默,其中數(shù)組的維數(shù)等于 numHashTables 贾陷,并且向量的維度當(dāng)前設(shè)置為1。在將來的版本中嘱根,我們將實(shí)現(xiàn) AND-amplification(與放大器)髓废,以便用戶可以指定這些向量的維度 。
Approximate Similarity Join(近似相似度連接)
近似相似度連接采用兩個(gè)數(shù)據(jù)集儿子,并且近似返回距離小于用戶定義閾值的數(shù)據(jù)集中的行對(duì)瓦哎。 近似相似度連接支持兩個(gè)不同的數(shù)據(jù)集連接和自連接。 Self-joinin (自連接)會(huì)產(chǎn)生一些重復(fù)的對(duì)。
近似相似度連接接受已轉(zhuǎn)換和未轉(zhuǎn)換的數(shù)據(jù)集作為輸入蒋譬。 如果使用未轉(zhuǎn)換的數(shù)據(jù)集割岛,它將自動(dòng)轉(zhuǎn)換。 在這種情況下犯助,哈希簽名將被創(chuàng)建為outputCol癣漆。
在加入的數(shù)據(jù)集中,可以在數(shù)據(jù)集A和數(shù)據(jù)集B中查詢?cè)紨?shù)據(jù)集剂买。 距離列將被添加到輸出數(shù)據(jù)集惠爽,以顯示返回的每對(duì)行之間的真實(shí)距離。
Approximate Nearest Neighbor Search(近似最鄰近搜索)
近似最近鄰搜索采用數(shù)據(jù)集(特征向量)和Key鍵(單個(gè)特征向量)瞬哼,并且它近似返回?cái)?shù)據(jù)集中最接近向量的指定數(shù)量的行婚肆。
近似最近鄰搜索接受已轉(zhuǎn)換和未轉(zhuǎn)換的數(shù)據(jù)集作為輸入。 如果使用未轉(zhuǎn)換的數(shù)據(jù)集坐慰,它將自動(dòng)轉(zhuǎn)換较性。 在這種情況下,哈希簽名將被創(chuàng)建為outputCol结胀。
距離列將被添加到輸出數(shù)據(jù)集赞咙,以顯示每個(gè)輸出行和搜索的鍵之間的真實(shí)距離。
注意:當(dāng)哈希桶中沒有足夠的候選項(xiàng)時(shí)糟港,近似最近鄰搜索將返回少于k行攀操。