2018/11/29

Feature Extraction (特徵抽取)

06 Feature Extraction (特徵抽取)
生活中的裸資料並非全都是實數,不易由演算法處理,把生活中遇到的文字、分類型等資料轉換成實數資料就是特徵抽取。

Categorical data (類資料)

像城市名、職業類別等都屬分類資料。這類資料可以整數進行編碼,但整數是有序數目,而分類資料並無序,故不適合。一般常用的是 one-hot encoding (獨一編碼):如有三個城市就編成三個二進位碼,碼字裡三個位元中有一個是1,其他的是0,1的不同位置代表不同城市,所以叫獨一編碼。可用 sklearn DictVectorizer 類別的fit_transform() method 進行獨一編碼。
from sklearn.feature_extraction import DictVectorizer
onehot_encoder = DictVectorizer()
X = [
{'city': 'New York'},
{'city': 'San Francisco'},
{'city': 'Chapel Hill'}
]
print(onehot_encoder.fit_transform(X).toarray())
[[0. 1. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]

Standardizing features (特徵標準化)

對數值型的不同特徵 若有些特徵的變異數很大,有些很小,則變異數大的特徵會支配學習的結果,使變異數小的特徵起不了作用,因而常常需要將每一特徵的數據都化成化 zero mean 和 unit variance。可使用sklearn 的 preprocessing class裡的 scale method ,將不同特徵的數據放在不同的列,scale 可對每一列特徵標準化為常態分佈。如
from sklearn import preprocessing
import numpy as np
X = np.array([
[0., 5., 13., 9., 1.],
[0., 13., 15., 10., 15.],
[3., 15., 2., 0., 11.]
])
x_scale=preprocessing.scale(X)
print(x_scale)
print(x_scale.mean(axis=1,keepdims=True))
print(x_scale.std(axis=1,keepdims=True))
[[-0.70710678 -1.38873015  0.52489066  0.59299945 -1.35873244]
 [-0.70710678  0.46291005  0.87481777  0.81537425  1.01904933]
 [ 1.41421356  0.9258201  -1.39970842 -1.4083737   0.33968311]]
[[-0.46733585]
 [ 0.49300892]
 [-0.02567307]]
[[0.8729112 ]
 [0.62731165]
 [1.1757493 ]]
可見輸出的平均值接近0,而std接近 1。程式中使用sklearn.preprocessing.scale()函數,直接將給定數據進行標準化 。mean函數裡的axis=1指的是把行抓來平均,所以列 的數目不變。sklearn 的 preprocessing class 除了 scale method 外,還有robust_scale method 可用來降低outlier (偏離點)所產生的負面影響。

從文字抽取特徵

文字是一種自然語言,很多機器學習的問題需處理文字,把文字用向量的方式表示 ,常見的有bag-of-words  (袋字 )和 word embedding (字內嵌)

∎ bag-of-words  (袋字 )
此法是獨一編碼的延伸,忽略文法、字的次序,其精神是以在不同的文件中若含有較多相同的字表示文件意義較相近為原則,將不同的文字放入袋中給予一個二進位編碼的位置,如該位置是1表示該文字有出現在文件裡。很多文件組成文集(corpus),以下列為例表示文集裡含有兩份文件:

from sklearn.feature_extraction.text import CountVectorizer
corpus = [
'UNC played Duke in basketball',
'Duke lost the basketball game']
vectorizer = CountVectorizer()
print(vectorizer.fit_transform(corpus).todense())
print(vectorizer.vocabulary_)
[[1 1 0 1 0 1 0 1]
 [1 1 1 0 1 0 1 0]]
{'unc': 7, 'played': 5, 'duke': 1, 'in': 3, 'basketball': 0, 'lost': 4, 'the': 6, 'game': 2}
因共有 8個字所以編成0-7等8個位元,每個位元代表一個單字,例如其中第0個位元是代表'basketball',  第1個位元是代表'duke',而第一個文件的向量[1 1 0 1 0 1 0 1]表示此文件含有0,1,3,5,7位元所代表的字,此方式就可把文件用向量表示。若在文集裡再加上一個文件,
corpus.append('I ate a sandwich')
print(vectorizer.fit_transform(corpus).todense())
print(vectorizer.vocabulary_)
[[0 1 1 0 1 0 1 0 0 1]
 [0 1 1 1 0 1 0 0 1 0]
 [1 0 0 0 0 0 0 1 0 0]]
{'basketball': 1, 'the': 8, 'sandwich': 7, 'game': 3, 'lost': 5, 'played': 6, 'ate': 0, 'in': 4, 'duke': 2, 'unc': 9}
現在有三個文件,所以有三列,新文件中的'I' 和 'a'並沒有加入編碼,所以新增兩個字。當要檢查這些文件的相似度時可用向量的歐氏距離(Euclidean norm, or $L^2$ norm)來衡量,
$d_{ij}=\left \| \textbf{x}_i- \textbf{x}_j\right \|$,
其中$\left \| \textbf{x} \right \|$的定義是
 $\left \| \textbf{x} \right \|=\sqrt{x_1^2+\cdots +x_n^2}$
sklearn有函數可算的歐氏距離,
from sklearn.metrics.pairwise import euclidean_distances
X = vectorizer.fit_transform(corpus).todense()
print('Distance between 1st and 2nd documents:',
euclidean_distances(X[0], X[1]))
print('Distance between 1st and 3rd documents:',
euclidean_distances(X[0], X[2]))
print('Distance between 2nd and 3rd documents:',
euclidean_distances(X[1], X[2]))
Distance between 1st and 2nd documents: [[2.44948974]]
Distance between 1st and 3rd documents: [[2.64575131]]
Distance between 2nd and 3rd documents: [[2.64575131]]
可件文件1和2的距離較近,表示相似度較高。
此方法的問題在於若文字很多的時候則編碼的長度就要很長,但其中只有少數的位元是1其他則0,此情形就稱sparse vector (稀疏向量)。資料維度的增加有所謂的維度的魔咒的問題,再者維度多所需的樣本也要增加才能有一定的學習成效,所以如何降低維度是器學習裡的一個重要課題。

◆ Stop word filtering (跳停字過濾)
在一語言中某些字元對句子的貢獻是經由文法而非文字本身的表示,這些字在自然語言處理中可被忽略而不影響結果,像英文的冠詞、助動詞如 the, a, an, be, do, will等都是。去掉這些「跳停」字可在CountVectorizer()中加入stop_word 參數,如
vectorizer = CountVectorizer(stop_words='english')
print(vectorizer.fit_transform(corpus).todense())
print(vectorizer.vocabulary_)
[[0 1 1 0 0 1 0 1]
 [0 1 1 1 1 0 0 0]
 [1 0 0 0 0 0 1 0]]
{'unc': 7, 'played': 5, 'duke': 2, 'basketball': 1, 'lost': 4, 'game': 3, 'ate': 0, 'sandwich': 6}
可見字詞減少了。但在一個語言中這些字元的數目也不多,也以跳停字過濾的助益也是有限的。

◆ Stemming (詞幹提取) and lemmatization (詞形還原)
   Stemming 通常是較粗糙的過程,將字的尾巴去掉,如automate, automatic, automation 等字就歸類到 automat 這樣一個字。 lemmatizationg 指的是找出字的原形,如gathering, gathers, gathered 等就歸類gather。

◆ 在bag-of-words中加入tf-idf weights
在上述的例子中,字是以 binary的方式編碼,並無分出那些字使用頻率較高,一般認為若某文章中出現某些字特別多,則該文章應與那些字較具相關性,因此可把編碼改成整數,字出現的次數也列在編碼中。tf 指term frequency,而idf指inverse document frequency。
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
corpus = ['The dog ate a sandwich, the wizard transfigured a sandwich, and I ate a sandwich']
vectorizer = CountVectorizer(stop_words='english')
frequencies = np.array(vectorizer.fit_transform(corpus).todense())[0]
print(frequencies)
print('Token indices %s' % vectorizer.vocabulary_)
[2 1 3 1 1]
Token indices {'dog': 1, 'ate': 0, 'sandwich': 2, 'wizard': 4, 'transfigured': 3}
例中可見'sandwich' 出現三次,這是裸頻率,但因若不同文章長度不一樣,只記單字出現的裸頻率資訊仍然不足,所以又有正規化的處理,sklearn 的 TfidfVectorizer 提供這個功能
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
'The dog ate a sandwich and I ate a sandwich',
'The wizard transfigured a sandwich'
 ] 
vectorizer = TfidfVectorizer(stop_words='english')
print(vectorizer.fit_transform(corpus).todense())
print(vectorizer.vocabulary_)
[[0.75458397 0.37729199 0.53689271 0.         0.        ]
 [0.         0.         0.44943642 0.6316672  0.6316672 ]]
{'dog': 1, 'ate': 0, 'sandwich': 2, 'wizard': 4, 'transfigured': 3}

∎ Word embedding (字內嵌)
  • 上述的bag-of-words將文字放在向量裡用元素表示文字。
  • 字內嵌則是用很多維度的向量來表示文字,相同意義的字在向量上表示的距離較近,不相同則較遠。
  • 需透過學習的方法才能建立內嵌向量。
  • word2vec是很有名的字內嵌向量,使用Group New訓練。在表內文字可用來相加。
  • word2vec共需3.4G空間,在電腦上執行記憶體需大於8G,目前初學暫不考慮。

沒有留言:

張貼留言