文章目錄
1. 使用真實數據 ( 加州房價預測 )
1.1 流行的各個領域的開放數據集存儲庫:
1.2 元門戶站點
①http://dataportals.org/
② http://opendatamonitor.eu/
③ http://quandl.com/
1.3 其他一些列出許多流行的開放數據存儲庫的頁面
2. 觀察大局
Pipeline: 一個序列的數據處理組件稱爲一個數據流水線。
2.1 框架問題
① 首先要問老闆,我們的業務目標是什麼? 因爲建立模型本身可能不是最終問題。【審題很重要】
② 如果有現成的解決方案,可以參照。【站在巨人的肩膀上】
③ 如果數據量巨大,還會用到MapReduce
技術。
2.2 選擇性能指標
我們是預測房屋價格,典型的迴歸問題。迴歸常用的性能指標:RMSE(均方根誤差)、MAE(平均絕對誤差)。
RMSE和MAE如何選擇?當異常值非常少時,RMSE更加優異。因爲RMSE對異常值非常敏感,爲什麼?很好理解,平方比絕對值使誤差變更大。
2.3 檢查假設
搞清楚上下游系統到底是需要什麼數據,比如,下游系統需要輸出價格,那就是迴歸任務,需要輸出類別,就是分類任務。所以要溝通清楚後再着手開發,不要盲目。
3. 獲取數據
3.1 創建工作區
關於python的系統環境,我用的是anaconda,而且之前寫python爬蟲 的時候也介紹過隔離環境如何去安裝。所以這裏就不過多介紹了。
3.2 下載數據
import os
import tarfile
from six. moves import urllib
downLoad_root = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
housing_path = "datasets/housing"
housing_url = downLoad_root + housing_path + "/housing.tgz"
def fetch_housing_data ( housing_url = housing_url, housing_path = housing_path) :
if not os. path. isdir( housing_path) :
os. makedirs( housing_path)
print ( "======= housing_url ================" , housing_url)
tgz_path = os. path. join( housing_path, "housing.tgz" )
print ( "======= tgz_path ================" , tgz_path)
urllib. request. urlretrieve( housing_url, tgz_path)
housing_taz = tarfile. open ( tgz_path)
housing_taz. extractall( path= housing_path)
housing_taz. close( )
def load_data ( housing_path = housing_path) :
csv_path = os. path. join( housing_path, "housing.csv" )
print ( "======= csv_path ================" , csv_path)
return pd. read_csv( csv_path)
3.3 快速查看數據結構
data. shape
data. describe( )
data. info( )
describe依次展示的是:總數、均值、標準差、最小值、25%分位數、50%分位數、75%分位數、最大值。
從數據看出,total_bedrooms有缺失值,ocean_proximity是對象類型,可能是分類屬性。
另一種比較直觀的瞭解數據的方式:直方圖。反應的是落在某一範圍內(橫軸)的實例數量(縱軸)。
data.hist(bins=50,figsize=(20,15))
3.4 創建測試集(經常被忽視但至關重要
)
① 可以直接用sklearn提供的切分方法,但是在這之前要考慮數據量,數據量大的話用隨機抽樣
的方法,數據量小的話最好用分層抽樣
,否則會造成明顯的抽樣偏差
。
from sklearn. model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit( n_splits= 1 , test_size= 0.2 , random_state= 42 )
for train_index, test_index in split. split( data, data[ "income_cat" ] ) :
strat_train_set = data. loc[ train_index]
strat_test_set = data. loc[ test_index]
咦,data[“income_cat”]哪裏來的?這是因爲我們想讓測試集代表整個數據集中各種不同類型的收入
而加的新特徵—收入類別。處理如下:
data[ "income_cat" ] = np. ceil( data[ "median_income" ] / 1.5 )
data[ "income_cat" ] . where( data[ "income_cat" ] < 5 , 5.0 , inplace = True )
data[ "income_cat" ] . value_counts( )
data[ "income_cat" ] . hist( )
注意:
data[“income_cat”]就是爲了切分後的數據分佈更接近整個數據集的數據分佈
,所以,切分完後還要刪掉。
for d in ( strat_train_set, strat_test_set) :
d. drop( [ "income_cat" ] , axis = 1 , inplace= True )
至此,我們就完成了數據集的切分,得到了分層抽樣後的測試集。分層抽樣的測試集中的比例分佈更接近於完整數據集中的分佈。
4. 從數據探索和可視化中獲得洞見 EDA(pandas、matplotlib、seaborn)
4.1 將地理數據可視化
h. plot( kind= "scatter" , x= "longitude" , y= "latitude" , alpha= 0.4 ,
s= h[ "population" ] / 100 , label= "population" ,
c= "median_house_value" , cmap= plt. get_cmap( "jet" ) , colorbar= True ,
figsize= ( 15 , 10 ) , fontsize= 20 )
plt. legend( )
4.2 尋找相關性
數據集不大的話,可以直接用corr()方法計算出每對屬性之間的相關係數,即皮爾遜相關係數。
corr_matrix = h. corr( )
corr_matrix[ "median_house_value" ] . sort_values( ascending= False )
從上面可以分析出,收入越高,房價越高。越往北走,房價越低。(常識:北緯爲正,東經爲正,南緯和西經爲負
)
各種數據集的標準相關係數:
還有一種方法可以檢測屬性之間的相關性,就是使用pandas的scatter_matrix。
from pandas. tools. plotting import scatter_matrix
attr = [ "median_house_value" , "median_income" , "total_rooms" , "housing_median_age" ]
scatter_matrix( h[ attr] , figsize= ( 12 , 8 ) )
最有潛力預測房價中位數的特徵是收入中位數,所以我們放大其散點圖:
h. plot( kind= "scatter" , x= "median_income" , y= "median_house_value" , alpha= 0.1 )
分析上圖:
① 相關性很強。
② 50萬和35附近有一條水平線,可能數據已經經過了處理。
4.3 試驗不同屬性的組合
其實這屬於是特徵工程的部分。嘗試不同的組合特徵,可以簡單的使用四則運算來實驗。
思考:其實我認爲這塊東西要跟實際業務緊密相連,比如房價,在我們生活中很普遍,從購房者的角度來看,希望買到什麼樣的房子? 我會考慮是否學區,房屋質量(澆築或板樓),建築年代,是否可貸款,周邊配套等等方面,那麼這些特徵較好的房價肯定貴啊,所以多從實際業務思考,當然除了我們能想到的,也有很多想不到的特徵,就要多去試驗一下(四則運算、決策樹等方式),畢竟每個人的想法和心理是不一樣的。
h[ "rooms_per_household" ] = h[ "total_rooms" ] / h[ "households" ]
h[ "bedrooms_per_room" ] = h[ "total_bedrooms" ] / h[ "total_rooms" ]
h[ "population_per_household" ] = h[ "population" ] / h[ "households" ]
corr_matrix = h. corr( )
corr_matrix[ "median_house_value" ] . sort_values( ascending= False )
分析:
臥室房間比例越低,房價越高;每個家庭的房價數量越多,房價越貴。
5. 機器學習算法的數據準備 ( 數據清洗、特徵工程)
這裏我們應該開始編寫函數和積累函數,自己逐步建立一個轉換函數的函數庫
,而不是每次都手動操作。最大原因:重用
。
5.1 數據清理
對於缺失值,一般有3種方式:
① 刪除對應的樣本;
② 刪除對應的特徵;
③ 填充缺失值,填充值:中位數、衆數、均值等。
對於第3種方式,sk提供了imputer來處理缺失值。
from sklearn. preprocessing import Imputer
imputer = Imputer( strategy = "median" )
housing_nums = housing. drop( "ocean_proximity" , axis= 1 )
imputer. fit( housing_nums )
注意:此時imputer的估算只是針對【當前實例】,系統如果重啓,則失效。
所以,爲了穩妥,要對所有數值屬性進行變換transform
X = imputer. transform( housing_nums)
housing_transform = pd. DataFrame( X, columns= housing_nums. columns)
5.1.1 【插曲1】
數據的拼接和合並:
① concat,基於axis拼接,類似union,適合縱向
;
② merge,基於某列做關聯,表的合併,類似sql的表連接,適合橫向
;
③ join,基於index合併。
5.1.2 【插曲2】
sk的幾個常用函數:
sklearn做了一致性的設計,所以大部分數據集的操作通過以下函數就可以實現:
① fit 擬合;
② transform 變換;
③ fit_transform 擬合+變換;
④ predict 預測;
⑤ predict_prob 預測概率;
fit 和 transform 的區別和聯繫 :舉個通俗點的例子,從字面上來看,transform更像數據的實際轉換方法,比如標準化StandardScaler(),當數據集執行了transform纔算標準化完成,而fit更像是在這之前做一些基礎數據的計算,爲後面的transform作準備,所以一般我們在訓練集做fit_transform,然後在測試集直接用transform,因爲前面一步已經做了fit生成了一些基礎數據比如均值,標準差等。
5.2 處理文本和分類屬性
之前我們排除了分類屬性ocean_proximity,因爲它是一個文本屬性,我們無法計算中位數。大部分的機器學習更易於跟數字打交道,所以我們可以將這種數據轉換爲數字。sk中提供了這種轉換器,比如LabelEncoder。
from sklearn. preprocessing import LabelEncoder
encoder = LabelEncoder( )
housing_cat = housing[ "ocean_proximity" ]
housing_cat_encoded = encoder. fit_transform( housing_cat)
housing_cat_encoded
encoder. classes_
注意:
這裏有個比較實際的問題,映射器將類別型特徵轉換爲了0,1,2,3,4,但是這樣的編碼是有大小關係
的,而實際ocean_proximity這個特徵不具有大小關係的含義,所以通常解決方式:轉化爲二進制屬性。
如果特徵的值爲高等、中等、低等,這種具有大小關係的含義,就可以映射爲3,2,1這樣的編碼。
from sklearn. preprocessing import OneHotEncoder
encoder = OneHotEncoder( )
housing_cat_1hot = encoder. fit_transform( housing_cat_encoded. reshape( - 1 , 1 ) )
housing_cat_1hot
這裏輸出是一個Scipy稀疏矩陣,但是隻存非0元素的位置,因爲大量0佔用內存,所以做了優化。如果想轉換成密集的二維數組:
housing_cat_1hot. toarray( )
這樣是不是就更像one-hot了?但是,LabelBinarizer默認返回
的是一個密集的Numpy數組
但是,是不是很麻煩,難道每次我要先LabelEncoder再OneHotEncoder? sk提供了一個LabelBinarizer可以一次性完成2個轉換(從文本類別轉化爲整數類別,再從整數類別轉化爲獨熱向量)
from sklearn. preprocessing import LabelBinarizer
encoder = LabelBinarizer( )
housing_cat_1hot = encoder. fit_transform( housing_cat)
housing_cat_1hot
5.3 自定義轉化器
① 爲什麼?因爲一些清理操作或組合特定特徵等任務需要自定義;再就是轉換器能和sk的pipeline無縫銜接
。
② 具體實現:新建類,添加兩個基類BaseEstimator和TransformerMixin分別得到get_params()、set_params() 和 fit_transform() (這裏的基類類似於繼承),類中再實現fit和transform即可。
from sklearn. base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_ix = 3 , 4 , 5 , 6
class CombinedAttributesAdder ( BaseEstimator, TransformerMixin) :
def __init__ ( self, add_bedrooms_per_room = True ) :
self. add_bedrooms_per_room = add_bedrooms_per_room
def fit ( self, X, y= None ) :
return self
def transform ( self, X, y= None ) :
rooms_per_household = X[ : , rooms_ix] / X[ : , household_ix]
population_per_household = X[ : , population_ix] / X[ : , household_ix]
if self. add_bedrooms_per_room :
bedrooms_per_room = X[ : , bedrooms_ix] / X[ : , rooms_ix]
return np. c_[ X, rooms_per_household, population_per_household, bedrooms_per_room]
else :
return np. c_[ X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder( add_bedrooms_per_room = False )
housing_extra_attr = attr_adder. transform( housing. values)
type ( housing_extra_attr)
5.4 特徵縮放
爲什麼?比如有的特徵是0 ~ 9999,有的特徵是2 ~ 9,會導致算法對取值較大的特徵有所偏好,性能不佳,所以如果將兩個特徵都縮放到 0 ~ 1範圍內,就能提高模型算法的泛化能力。
常用方法:最小最大值縮放 MinMaxScaler(默認範圍0~1,可調)和 標準化StandadScaler。
區別:標準化不會將值縮放到特定範圍,但是標準化能減小異常值的影響。
5.5 轉換流水線
爲什麼? 許多數據轉換的步驟需要以正確的順序執行。
from sklearn. base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_ix = 3 , 4 , 5 , 6
class CombinedAttributesAdder ( BaseEstimator, TransformerMixin) :
def __init__ ( self, add_bedrooms_per_room = True ) :
self. add_bedrooms_per_room = add_bedrooms_per_room
def fit ( self, X, y= None ) :
return self
def transform ( self, X, y= None ) :
rooms_per_household = X[ : , rooms_ix] / X[ : , household_ix]
population_per_household = X[ : , population_ix] / X[ : , household_ix]
if self. add_bedrooms_per_room :
bedrooms_per_room = X[ : , bedrooms_ix] / X[ : , rooms_ix]
return np. c_[ X, rooms_per_household, population_per_household, bedrooms_per_room]
else :
return np. c_[ X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder( add_bedrooms_per_room = False )
housing_extra_attr = attr_adder. transform( housing. values)
type ( housing_extra_attr)
class DataFrameSelector ( BaseEstimator, TransformerMixin) :
def __init__ ( self, attribute_names) :
self. attribute_names = attribute_names
def fit ( self, X, y= None ) :
return self
def transform ( self, X) :
return X[ self. attribute_names] . values
class MyLabelBinarizer ( TransformerMixin) :
def __init__ ( self, * args, ** kwargs) :
self. encoder = LabelBinarizer( * args, ** kwargs)
def fit ( self, x, y= 0 ) :
self. encoder. fit( x)
return self
def transform ( self, x, y= 0 ) :
return self. encoder. transform( x)
from sklearn. pipeline import Pipeline, FeatureUnion
from sklearn. preprocessing import StandardScaler
from sklearn. preprocessing import Imputer
housing_num = housing. drop( "ocean_proximity" , axis= 1 )
num_attr = list ( housing_num)
cat_attr = [ "ocean_proximity" ]
num_pipeline = Pipeline( [
( 'selector' , DataFrameSelector( num_attr) ) ,
( 'imputer' , Imputer( strategy= "median" ) ) ,
( 'attribs_adder' , CombinedAttributesAdder( ) ) ,
( 'std_scaler' , StandardScaler( ) ) ,
] )
cat_pipeline = Pipeline( [
( 'selector' , DataFrameSelector( cat_attr) ) ,
( 'label_binarizer' , MyLabelBinarizer( ) ) ,
] )
full_pipeline = FeatureUnion( transformer_list= [
( "num_pipeline" , num_pipeline) ,
( "cat_pipeline" , cat_pipeline) ,
] )
housing_prepared = full_pipeline. fit_transform( housing)
housing_prepared
這一節主要完成了:自動清理 和 準備ML算法的數據
的轉換流水線
至此,看看我們完成了哪些內容:明確目標問題、獲取數據、EDA、對訓練集和測試集進行分層抽樣、編寫了自動清理和準備數據的轉換器。
6. 選擇和訓練模型
6.1 訓練和評估
首先,測試一下我們前面的組件。
from sklearn. linear_model import LinearRegression
m1 = LinearRegression( )
m1. fit( housing_prepared, housing_label)
some_data = housing. iloc[ : 5 ]
some_labels = housing_label. iloc[ : 5 ]
housing_prepared = full_pipeline. fit_transform( housing)
some_data_prepared = full_pipeline. transform( some_data)
print ( "Predictions:" , m1. predict( some_data_prepared) )
print ( "Labels:" , list ( some_labels) )
測試成功,說明我們前面的組件都可以正常工作。但是準確度堪憂~ 所以,先評估一下吧。
from sklearn. metrics import mean_squared_error
housing_predictions = m1. predict( housing_prepared)
m1_mse = mean_squared_error( housin_label, housing_predictions)
m1_rmse = np. sqrt( m1_mse)
m1_rmse
貌似損失有點大,說明欠擬合。如何解決?選擇一個更復雜的模型試試,比如DecisionTreeRegressor
竟然0損失,好吧,模型過擬合了。所以我們還需要模型進行評估,正常來說要對訓練集切分成訓練集和驗證集,然後用交叉驗證評估,最後
得到驗證的模型再去預測測試集
。
6.2 使用交叉驗證來更好地進行評估
k-fold交叉驗證
from sklearn. model_selection import cross_val_score
scores = cross_val_score( m2, housing_prepared, housing_label, scoring= "neg_mean_squared_error" , cv = 10 )
rmse_scores = np. sqrt( - scores)
def display_scores ( scores) :
print ( "Scores:" , scores)
print ( "Mean:" , scores. mean( ) )
print ( "Std:" , scores. std( ) )
display_scores( rmse_scores)
l_scores = cross_val_score( m1, housing_prepared, housing_label, scoring= "neg_mean_squared_error" , cv = 10 )
l_rmse_scores = np. sqrt( - l_scores)
display_scores( l_rmse_scores)
from sklearn. ensemble import RandomForestRegressor
m3 = RandomForestRegressor( )
m3. fit( housing_prepared, housing_label)
rf_scores = cross_val_score( m3, housing_prepared, housing_label, scoring= "neg_mean_squared_error" , cv = 10 )
rf_rmse_scores = np. sqrt( - rf_scores)
display_scores( rf_rmse_scores)
上面試驗了3種不同模型的交叉驗證效果,隨機森林效果最好,但是還是過擬合。注意:
此時不要急於去調超參數,而是要多試驗幾種其他模型
(不同內核的SVM,神經網絡模型等),我們這個階段的目的是篩選有效模型(一般選出2~5個)
。
模型保存:
嘗試過的模型要保存下來,包括超參數和訓練過的參數,以及交叉驗證的評分和實際預測的結果
。比賽和實際工作都要這麼做。我們通過python的pickel模塊或sklearn.externals.joblib,這樣可以有效將大型Numpy數組序列化。
from sklearn. externals import joblib
joblib. dump( m1, "linearReg_model.pkl" )
my_model = joblib. load( "linearReg_model.pkl" )
my_model. predict( some_data_prepared)
7. 微調模型
經過上面過程,我們已經有了一個有效的模型列表,下一步需要對它們進行微調。
8. 網格搜索
可以直接使用sk的GridSearchCV來替代手動調整超參數。
from sklearn. model_selection import GridSearchCV
params = {
'n_estimators' : ( 3 , 10 , 30 ) , 'max_features' : ( 2 , 3 , 4 , 6 , 8 ) ,
}
rf_reg = RandomForestRegressor( )
grid_search = GridSearchCV( rf_reg, params, cv= 5 , scoring= "neg_mean_squared_error" )
grid_search. fit( housing_prepared, housing_label)
查看最佳參數和最優估算器
grid_search. best_params_
grid_search. best_estimator_
查看評估分數
cvres = grid_search. cv_results_
for mean_score, param in zip ( cvres[ "mean_test_score" ] , cvres[ "params" ] ) :
print ( np. sqrt( - mean_score) , param)
8.1 隨機搜索
當數據量很大時,用網格搜索太慢,此時使用隨機搜索RandomizedSearchCV:在每次迭代中,爲每個超參數選擇一個隨機值,然後對一定數量的隨機組合進行評估。
8.2 集成方法
8.3 分析最佳模型及其錯誤
RF可以指出每個屬性的相對重要程度 ( 怎麼算的?根據某種增益和分裂次數等,曾經被面試官問到過。)
extra_attrs = [ "rooms_per_hhold" , "pop_per_hhold" , "bedrooms_per_room" ]
cat_one_hot_attrs = list ( encoder. classes_)
attrs = num_attr + extra_attrs + cat_one_hot_attrs
sorted ( zip ( feature_importances, attrs) , reverse= True )
從圖中看出,收入中位數median_income是最重要的特徵。
8.4 通過測試集評估系統
記住:在這之前我們沒有動過測試集,也不應該動。
final_m = grid_search. best_estimator_
X_test = strat_test_set. drop( "median_house_value" , axis= 1 )
y_test = strat_test_set[ "median_house_value" ] . copy( )
X_test_prepared = full_pipeline. transform( X_test)
final_predictions = final_m. predict( X_test_prepared)
final_mse = mean_squared_error( y_test, final_predictions)
final_rmse = np. sqrt( final_mse)
final_rmse
特別注意:
測試集效果可能略差於交叉驗證的結果,但是此時不要再調整超參數了,因爲再調可能會導致新數據集上的泛化能力
變差。
9. 啓動、監控和維護系統
① 將生產數據源接入系統。
② 編寫監控代碼,實時監控系統的性能,在性能下降時觸發警報。因爲模型會慢慢腐壞,除非定期使用新數據訓練模型。
③ 評估系統性能,需要對系統的預測結果進行抽樣評估(人工分析)。這塊需要將人工評估的流水線接入學習系統。
④ 需要定期用新數據訓練模型,並且能保存系統當前狀態,以便發生錯誤能及時回滾。
10. 試試看和練習
經過這一系列的過程,其實機器學習的大部分工作是:數據準備、構建監控工具、建立人工評估的流水線、自動定期訓練模型
。
11. 那些年我們踩過的坑。。。
① 已經訓練好的模型predict數據,報特徵維度不一致。報錯信息如下:
錯誤代碼:
from sklearn. linear_model import LinearRegression
m1 = LinearRegression( )
m1. fit( housing_prepared, housing_label)
some_data = housing. iloc[ : 5 ]
some_labels = housing_label. iloc[ : 5 ]
housing_prepared = full_pipeline. fit_transform( housing)
some_data_prepared = full_pipeline. fit_transform( some_data) 【錯誤處】
print ( "Predictions:" , m1. predict( some_data_prepared) )
仔細看上述代碼,訓練數據和測試數據都進行了fit_transform操作,相當於產生了2個不同pipeline實例,編碼出來特徵維度肯定不一致。我還傻傻的畫了個圖,造成這種情況的原因是創建了2個pipeline實例去編碼:
錯誤修改
some_data_prepared = full_pipeline. transform( some_data) 【修改後】