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行。