1. 程式人生 > >機器學習:Scikit-learn與特徵工程

機器學習:Scikit-learn與特徵工程

“資料決定了機器學習的上限,而演算法只是儘可能逼近這個上限”,這句話很好的闡述了資料在機器學習中的重要性。大部分直接拿過來的資料都是特徵不明顯的、沒有經過處理的或者說是存在很多無用的資料,那麼需要進行一些特徵處理,特徵的縮放等等,滿足訓練資料的要求。

我們將初次接觸到Scikit-learn這個機器學習庫的使用

Scikit-learn

  • Python語言的機器學習工具
  • 所有人都適用,可在不同的上下文中重用
  • 基於NumPy、SciPy和matplotlib構建
  • 開源、商業可用 - BSD許可
  • 目前穩定版本0.18

自2007年釋出以來,scikit-learn已經成為最給力的Python機器學習庫(library)了。scikit-learn支援的機器學習演算法包括分類,迴歸,降維和聚類。還有一些特徵提取(extracting features)、資料處理(processing data)和模型評估(evaluating models)的模組。作為Scipy庫的擴充套件,scikit-learn也是建立在Python的NumPy和matplotlib庫基礎之上。NumPy可以讓Python支援大量多維矩陣資料的高效操作,matplotlib提供了視覺化工具,SciPy帶有許多科學計算的模型。    scikit-learn文件完善,容易上手,豐富的API,使其在學術界頗受歡迎。開發者用scikit-learn實驗不同的演算法,只要幾行程式碼就可以搞定。scikit-learn包括許多知名的機器學習演算法的實現,包括LIBSVM和LIBLINEAR。還封裝了其他的Python庫,如自然語言處理的NLTK庫。另外,scikit-learn內建了大量資料集,允許開發者集中於演算法設計,節省獲取和整理資料集的時間。

安裝的話參考下面步驟:建立一個基於Python3的虛擬環境:

mkvirtualenv -p /usr/local/bin/python3.6 ml3

在ubuntu的虛擬環境當中執行以下命令

pip3 install Scikit-learn

然後通過匯入命令檢視是否可以使用:

import sklearn

資料的特徵工程

從資料中抽取出來的對預測結果有用的資訊,通過專業的技巧進行資料處理,是的特徵能在機器學習演算法中發揮更好的作用。優質的特徵往往描述了資料的固有結構。最初的原始特徵資料集可能太大,或者資訊冗餘,因此在機器學習的應用中,一個初始步驟就是選擇特徵的子集,或構建一套新的特徵集,減少功能來促進演算法的學習,提高泛化能力和可解釋性。

例如:你要檢視不同地域女性的穿衣品牌情況,預測不同地域的穿衣品牌。如果其中含有一些男性的資料,是不是要將這些資料給去除掉

特徵工程的意義

  • 更好的特徵意味著更強的魯棒性
  • 更好的特徵意味著只需用簡單模型
  • 更好的特徵意味著更好的結果

特徵工程之特徵處理

特徵工程中最重要的一個環節就是特徵處理,特徵處理包含了很多具體的專業技巧

  • 特徵預處理
    • 單個特徵
      • 歸一化
      • 標準化
      • 缺失值
    • 多個特徵
      • 降維
        • PCA

特徵工程之特徵抽取與特徵選擇

如果說特徵處理其實就是在對已有的資料進行運算達到我們目標的資料標準。特徵抽取則是將任意資料格式(例如文字和影象)轉換為機器學習的數字特徵。而特徵選擇是在已有的特徵中選擇更好的特徵。後面會詳細介紹特徵選擇主要區別於降維。

一、資料的來源與型別

大部分的資料都來自已有的資料庫,如果沒有的話也可以交給很多爬蟲工程師去採集,來提供。也可以來自平時的記錄,反正資料無處不在,大都是可用的。

資料的型別

按照機器學習的資料分類我們可以將資料分成:

  • 標稱型:標稱型目標變數的結果只在有限目標集中取值,如真與假(標稱型目標變數主要用於分類)
  • 數值型:數值型目標變數則可以從無限的數值集合中取值,如0.100,42.001等 (數值型目標變數主要用於迴歸分析)

按照資料的本身分佈特性

  • 離散型
  • 連續型

那麼什麼是離散型和連續型資料呢?首先連續型資料是有規律的,離散型資料是沒有規律的

  • 離散變數是指其數值只能用自然數或整數單位計算的則為離散變數.例如,班級人數、進球個數、是否是某個類別等等

  • 連續型資料是指在指定區間內可以是任意一個數值,例如,票房資料、花瓣大小分佈資料

二、資料的特徵抽取

現實世界中多數特徵都不是連續變數,比如分類、文字、影象等,為了對非連續變數做特徵表述,需要對這些特徵做數學化表述,因此就用到了特徵提取.sklearn.feature_extraction提供了特徵提取的很多方法

分類特徵變數提取

我們將城市和環境作為字典資料,來進行特徵的提取。

sklearn.feature_extraction.DictVectorizer(sparse = True)

將對映列表轉換為Numpy陣列或scipy.sparse矩陣

  • sparse 是否轉換為scipy.sparse矩陣表示,預設開啟

方法

fit_transform(X,y)

應用並轉化對映列表X,y為目標型別

inverse_transform(X[, dict_type])

將Numpy陣列或scipy.sparse矩陣轉換為對映列表

from sklearn.feature_extraction import DictVectorizer
onehot = DictVectorizer() # 如果結果不用toarray,請開啟sparse=False
instances = [{'city': '北京','temperature':100},{'city': '上海','temperature':60}, {'city': '深圳','temperature':30}]
X = onehot.fit_transform(instances).toarray()
print(onehot.inverse_transform(X))

文字特徵提取(只限於英文)

文字的特徵提取應用於很多方面,比如說文件分類、垃圾郵件分類和新聞分類。那麼文字分類是通過詞是否存在、以及詞的概率(重要性)來表示。

(1)文件的中詞的出現

數值為1表示詞表中的這個詞出現,為0表示未出現

sklearn.feature_extraction.text.CountVectorizer()

將文字文件的集合轉換為計數矩陣(scipy.sparse matrices)

方法

fit_transform(raw_documents,y)

學習詞彙詞典並返回詞彙文件矩陣

from sklearn.feature_extraction.text import CountVectorizer
content = ["life is short,i like python","life is too long,i dislike python"]
vectorizer = CountVectorizer()
print(vectorizer.fit_transform(content).toarray())

需要toarray()方法轉變為numpy的陣列形式

溫馨提示:每個文件中的詞,只是整個語料庫中所有詞,的很小的一部分,這樣造成特徵向量的稀疏性(很多值為0)為了解決儲存和運算速度的問題,使用Python的scipy.sparse矩陣結構

(2)TF-IDF表示詞的重要性

TfidfVectorizer會根據指定的公式將文件中的詞轉換為概率表示。(樸素貝葉斯介紹詳細的用法)

class sklearn.feature_extraction.text.TfidfVectorizer()

方法

fit_transform(raw_documents,y)

學習詞彙和idf,返回術語文件矩陣。

from sklearn.feature_extraction.text import TfidfVectorizer
content = ["life is short,i like python","life is too long,i dislike python"]
vectorizer = TfidfVectorizer(stop_words='english')
print(vectorizer.fit_transform(content).toarray())
print(vectorizer.vocabulary_)

三、資料的特徵預處理

單個特徵

(1)歸一化

歸一化首先在特徵(維度)非常多的時候,可以防止某一維或某幾維對資料影響過大,也是為了把不同來源的資料統一到一個參考區間下,這樣比較起來才有意義,其次可以程式可以執行更快。例如:一個人的身高和體重兩個特徵,假如體重50kg,身高175cm,由於兩個單位不一樣,數值大小不一樣。如果比較兩個人的體型差距時,那麼身高的影響結果會比較大,k-臨近演算法會有這個距離公式。

min-max方法

常用的方法是通過對原始資料進行線性變換把資料對映到[0,1]之間,變換的函式為:

$$X^{'}{=}\frac{x-min}{max-min}$$

其中min是樣本中最小值,max是樣本中最大值,注意在資料流場景下最大值最小值是變化的,另外,最大值與最小值非常容易受異常點影響,所以這種方法魯棒性較差,只適合傳統精確小資料場景。

  • min-max自定義處理

這裡我們使用相親約會物件資料在MatchData.txt,這個樣本時男士的資料,三個特徵,玩遊戲所消耗時間的百分比、每年獲得的飛行常客里程數、每週消費的冰淇淋公升數。然後有一個所屬類別,被女士評價的三個類別,不喜歡、魅力一般、極具魅力。首先匯入資料進行矩陣轉換處理

import numpy as np

def data_matrix(file_name):
  """
  將文字轉化為matrix
  :param file_name: 檔名
  :return: 資料矩陣
  """
  fr = open(file_name)
  array_lines = fr.readlines()
  number_lines = len(array_lines)
  return_mat = zeros((number_lines, 3))
  # classLabelVector = []
  index = 0
  for line in array_lines:
    line = line.strip()
    list_line = line.split('\t')
    return_mat[index,:] = list_line[0:3]
    # if(listFromLine[-1].isdigit()):
    #     classLabelVector.append(int(listFromLine[-1]))
    # else:
    #     classLabelVector.append(love_dictionary.get(listFromLine[-1]))
    # index += 1
  return return_mat

輸出結果為

[[  4.09200000e+04   8.32697600e+00   9.53952000e-01]
 [  1.44880000e+04   7.15346900e+00   1.67390400e+00]
 [  2.60520000e+04   1.44187100e+00   8.05124000e-01]
 ...,
 [  2.65750000e+04   1.06501020e+01   8.66627000e-01]
 [  4.81110000e+04   9.13452800e+00   7.28045000e-01]
 [  4.37570000e+04   7.88260100e+00   1.33244600e+00]]

我們檢視資料集會發現,有的數值大到幾萬,有的才個位數,同樣如果計算兩個樣本之間的距離時,其中一個影響會特別大。也就是說飛行里程數對於結算結果或者說相親結果影響較大,但是統計的人覺得這三個特徵同等重要,所以需要將資料進行這樣的處理。

這樣每個特徵任意的範圍將變成[0,1]的區間內的值,或者也可以根據需求處理到[-1,1]之間,我們再定義一個函式,進行這樣的轉換。

def feature_normal(data_set):
    """
    特徵歸一化
    :param data_set:
    :return:
    """
    # 每列最小值
    min_vals = data_set.min(0)
    # 每列最大值
    max_vals = data_set.max(0)
    ranges = max_vals - min_vals
    norm_data = np.zeros(np.shape(data_set))
    # 得出行數
    m = data_set.shape[0]
    # 矩陣相減
    norm_data = data_set - np.tile(min_vals, (m,1))
    # 矩陣相除
    norm_data = norm_data/np.tile(ranges, (m, 1)))
    return norm_data

輸出結果為

[[ 0.44832535  0.39805139  0.56233353]
 [ 0.15873259  0.34195467  0.98724416]
 [ 0.28542943  0.06892523  0.47449629]
 ...,
 [ 0.29115949  0.50910294  0.51079493]
 [ 0.52711097  0.43665451  0.4290048 ]
 [ 0.47940793  0.3768091   0.78571804]]

這樣得出的結果都非常相近,這樣的資料可以直接提供測試驗證了

  • min-max的scikit-learn處理

scikit-learn.preprocessing中的類MinMaxScaler,將資料矩陣縮放到[0,1]之間

>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
...
>>> min_max_scaler = preprocessing.MinMaxScaler()
>>> X_train_minmax = min_max_scaler.fit_transform(X_train)
>>> X_train_minmax
array([[ 0.5       ,  0.        ,  1.        ],
       [ 1.        ,  0.5       ,  0.33333333],
       [ 0.        ,  1.        ,  0.        ]])

(3)標準化

常用的方法是z-score標準化,經過處理後的資料均值為0,標準差為1,處理方法是:

$$X^{'}{=}\frac{x-\mu}{\sigma}$$

其中$$\mu$$是樣本的均值,$$\sigma$$是樣本的標準差,它們可以通過現有的樣本進行估計,在已有的樣本足夠多的情況下比較穩定,適合嘈雜的資料場景

sklearn中提供了StandardScalar類實現列標準化:

In [2]: import numpy as np

In [3]: X_train = np.array([[ 1., -1.,  2.],[ 2.,  0.,  0.],[ 0.,  1., -1.]])

In [4]: from sklearn.preprocessing import StandardScaler

In [5]: std = StandardScaler()

In [6]: X_train_std = std.fit_transform(X_train)

In [7]: X_train_std
Out[7]:
array([[ 0.        , -1.22474487,  1.33630621],
       [ 1.22474487,  0.        , -0.26726124],
       [-1.22474487,  1.22474487, -1.06904497]])

(3)缺失值

由於各種原因,許多現實世界的資料集包含缺少的值,通常編碼為空白,NaN或其他佔位符。然而,這樣的資料集與scikit的分類器不相容,它們假設陣列中的所有值都是數字,並且都具有和保持含義。使用不完整資料集的基本策略是丟棄包含缺失值的整個行和/或列。然而,這是以丟失可能是有價值的資料(即使不完整)的代價。更好的策略是估算缺失值,即從已知部分的資料中推斷它們。

(1)填充缺失值使用sklearn.preprocessing中的Imputer類進行資料的填充

class Imputer(sklearn.base.BaseEstimator, sklearn.base.TransformerMixin)
    """
    用於完成缺失值的補充

    :param param missing_values: integer or "NaN", optional (default="NaN")
        丟失值的佔位符,對於編碼為np.nan的缺失值,使用字串值“NaN”

    :param strategy: string, optional (default="mean")
        插補策略
        如果是“平均值”,則使用沿軸的平均值替換缺失值
        如果為“中位數”,則使用沿軸的中位數替換缺失值
        如果“most_frequent”,則使用沿軸最頻繁的值替換缺失

    :param axis: integer, optional (default=0)
        插補的軸
        如果axis = 0,則沿列排列
        如果axis = 1,則沿行排列
    """
>>> import numpy as np
>>> from sklearn.preprocessing import Imputer
>>> imp = Imputer(missing_values='NaN', strategy='mean', axis=0)
>>> imp.fit([[1, 2], [np.nan, 3], [7, 6]])
Imputer(axis=0, copy=True, missing_values='NaN', strategy='mean', verbose=0)
>>> X = [[np.nan, 2], [6, np.nan], [7, 6]]
>>> print(imp.transform(X))                          
[[ 4.          2.        ]
 [ 6.          3.666...]
 [ 7.          6.        ]]

多個特徵

降維

PCA(Principal component analysis),主成分分析。特點是儲存資料集中對方差影響最大的那些特徵,PCA極其容易受到資料中特徵範圍影響,所以在運用PCA前一定要做特徵標準化,這樣才能保證每維度特徵的重要性等同。

sklearn.decomposition.PCA

class PCA(sklearn.decomposition.base)
   """
   主成成分分析

   :param n_components: int, float, None or string
       這個引數可以幫我們指定希望PCA降維後的特徵維度數目。最常用的做法是直接指定降維到的維度數目,此時n_components是一個大於1的整數。
       我們也可以用預設值,即不輸入n_components,此時n_components=min(樣本數,特徵數)

   :param whiten: bool, optional (default False)
      判斷是否進行白化。所謂白化,就是對降維後的資料的每個特徵進行歸一化。對於PCA降維本身來說一般不需要白化,如果你PCA降維後有後續的資料處理動作,可以考慮白化,預設值是False,即不進行白化

   :param svd_solver:
      選擇一個合適的SVD演算法來降維,一般來說,使用預設值就夠了。
    """

通過一個例子來看

>>> import numpy as np
>>> from sklearn.decomposition import PCA
>>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
>>> pca = PCA(n_components=2)
>>> pca.fit(X)
PCA(copy=True, iterated_power='auto', n_components=2, random_state=None,
  svd_solver='auto', tol=0.0, whiten=False)
>>> print(pca.explained_variance_ratio_)
[ 0.99244...  0.00755...]

四、資料的特徵選擇

降維本質上是從一個維度空間對映到另一個維度空間,特徵的多少別沒有減少,當然在對映的過程中特徵值也會相應的變化。舉個例子,現在的特徵是1000維,我們想要把它降到500維。降維的過程就是找個一個從1000維對映到500維的對映關係。原始資料中的1000個特徵,每一個都對應著降維後的500維空間中的一個值。假設原始特徵中有個特徵的值是9,那麼降維後對應的值可能是3。而對於特徵選擇來說,有很多方法:

  • Filter(過濾式):VarianceThreshold
  • Embedded(嵌入式):正則化、決策樹
  • Wrapper(包裹式)

其中過濾式的特徵選擇後,資料本身不變,而資料的維度減少。而嵌入式的特徵選擇方法也會改變資料的值,維度也改變。Embedded方式是一種自動學習的特徵選擇方法,後面講到具體的方法的時候就能理解了。

特徵選擇主要有兩個功能:

(1)減少特徵數量,降維,使模型泛化能力更強,減少過擬合

(2)增強特徵和特徵值之間的理解

sklearn.feature_selection

去掉取值變化小的特徵(刪除低方差特徵)

VarianceThreshold 是特徵選擇中的一項基本方法。它會移除所有方差不滿足閾值的特徵。預設設定下,它將移除所有方差為0的特徵,即那些在所有樣本中數值完全相同的特徵。

假設我們要移除那些超過80%的資料都為1或0的特徵

from sklearn.feature_selection import VarianceThreshold
X = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], [0, 1, 1]]
sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
sel.fit_transform(X)
array([[0, 1],
       [1, 0],
       [0, 0],
       [1, 1],
       [1, 0],
       [1, 1]])