用定租問題學透機器學習的K近鄰演算法

fondtiger發表於2021-09-09

k 近鄰思想是我覺得最純粹最清晰的一個思想,k 近鄰演算法(KNN)只是這個思想在資料領域都一個應用。

你的工資由你周圍的人決定。

你的水平由你身邊最接近的人的水平決定。

你所看到的世界,由你身邊的人決定。

思想歸思想,不能被編碼那也無法應用於資料科學領域。

我們提出問題,然後應用該方法加以解決,以此加深我們對方法的理解。

問題: 假設你是 airbnb 平臺的房東,怎麼給自己的房子定租金呢?

分析: 租客根據 airbnb 平臺上的租房資訊,主要包括價格、臥室數量、房屋型別、位置等等挑選自己滿意的房子。給房子定租金是跟市場動態息息相關的,同樣型別的房子我們收費太高租客肯定不租,收費太低收益又不好。

解答: 收集跟我們房子條件差不多的一些房子資訊,確定跟我們房子最相近的幾個,然後求其定價的平均值,以此作為我們房子的租金。

這就是 K-Nearest Neighbors(KNN),k 近鄰演算法。KNN 的核心思想是未標記樣本的類別,由距離其最近的 k 個鄰居投票決定。

圖片描述

本文就基於房租定價問題梳理下該演算法應用的全流程,包含如下部分。

  1. 讀入資料
  2. 資料處理
  3. 手寫演算法程式碼預測
  4. 利用 sklearn 作模型預測
  5. 超參最佳化
  6. 交叉驗證
  7. 總結

提前宣告,本資料集是公開的,你可以在網上找到很多相關主題的材料,本文力圖解釋地完整且精準,如果你找到了更詳實的學習材料,那再好不過了。

1.讀入資料

先讀入資料,瞭解下資料情況,發現目標變數price,以及cleaning_feesecurity_deposit的格式有點問題,另有一些變數是字元型,都需要處理。我對 dataframe 進行了轉置顯示,方便檢視。

圖片描述

2.資料處理

我們先只處理price,儘量集中在演算法思想本身上面去。

# 處理下目標變數price,並轉換成數值型
stripped_commas = dc_listings['price'].str.replace(',', '')
stripped_dollars = stripped_commas.str.replace('$', '')
dc_listings['price'] = stripped_dollars.astype('float')

# k近鄰演算法也是模型,需要劃分訓練集和測試集
sample_num = len(dc_listings)
# 在這我們先把資料隨機打散,保證資料集的切分隨機有效
dc_listings = dc_listings.loc[np.random.permutation(len(sample_num))]
train_df = dc_listings.iloc[0:int(0.7*sample_num)]
test_df = dc_listings.iloc[int(0.7*sample_num):]
複製程式碼

3.手寫演算法程式碼預測

根據 k 近鄰演算法的定義直接編寫程式碼,從簡單高效上考慮,我們僅針對單變數作預測。

入住人數應該是和租金關聯度很高的資訊,面積應該也是。我們這裡採用前者。

我們的目標是理解演算法邏輯。實際操作中一般不會只考慮單一變數。

# 注意,這兒是train_df
def predict_price(new_listing):
    temp_df = train_df.copy()
    temp_df['distance'] = temp_df['accommodates'].apply(lambda x: np.abs(x - new_listing))
    temp_df = temp_df.sort_values('distance')
    nearest_neighbor_prices = temp_df.iloc[0:5]['price']
    predicted_price = nearest_neighbor_prices.mean()
    return(predicted_price)

# 這兒是test_df
test_df['predicted_price'] = test_df['accommodates'].apply(predict_price)
# MAE(mean absolute error), MSE(mean squared error), RMSE(root mean squared error)
test_df['squared_error'] = (test_df['predicted_price'] - test_df['price'])**(2)
mse = test_df['squared_error'].mean()
rmse = mse ** (1/2)
複製程式碼

圖片描述

值得強調的是,模型演算法的構建都是基於訓練集的,預測評估基於測試集。應用評估嚴格上還有一類樣本,oot:跨時間樣本。

從結果來看,即使我們只用了入住人數accommodates這一個變數去做近鄰選擇,預測結果也是很有效的。

4.利用 sklearn 作模型預測

這次我們要用更多的變數,只剔掉字串和不可解釋的變數,剩下能用的變數都用上。

當用了多個變數的時候,這些不變數綱是不一樣的,我們需要進行標準化處理。保證了各自變數的分佈差異,同時又保證變數之間可疊加。

圖片描述

# 剔掉非數值型變數和不合適的變數
drop_columns = ['room_type', 'city', 'state', 'latitude', 'longitude', 'zipcode', 'host_response_rate', 'host_acceptance_rate', 'host_listings_count']
dc_listings = dc_listings.drop(drop_columns, axis=1)
# 剔掉缺失比例過高的列(變數)
dc_listings = dc_listings.drop(['cleaning_fee', 'security_deposit'], axis=1)
# 剔掉有缺失值的行(樣本)
dc_listings = dc_listings.dropna(axis=0)
# 多個變數的量綱不一樣,需要標準化
normalized_listings = (dc_listings - dc_listings.mean())/(dc_listings.std())
normalized_listings['price'] = dc_listings['price']

# 於是我們得到了可用於建模的資料集,7:3劃分訓練集測試集
train_df = normalized_listings.iloc[0:int(0.7*len(normalized_listings))]
test_df = normalized_listings.iloc[int(0.7*len(normalized_listings)):]
# price是y,其餘變數都是X
features = train_df.columns.tolist()
features.remove('price')
複製程式碼

處理後的資料集如下,其中price是我們要預測的目標,其餘是可用的變數。

圖片描述

from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_squared_error

knn = KNeighborsRegressor(n_neighbors=5, algorithm='brute')
knn.fit(train_df[features], train_df['price'])
predictions = knn.predict(test_df[features])
mse = mean_squared_error(test_df['price'], predictions)
rmse = mse ** (1/2)
複製程式碼

最後得到的 rmse=111.9,相比單變數 knn 的 117.4 要小,結果得到最佳化。嚴格來說,這個對比不完全公平,因為我們丟掉了少量的特徵缺失樣本。

5.超參最佳化(Hyperparameter Optimization)

在第 3 和第 4 部分,我們預設了 k=5,但這個拍腦袋確定的。該取值合不合理,是不是最優,都需要進一步確定。

其中,這個 k 就是一個超引數。對於任何一個資料集,只要你用 knn,就需要確定這個 k 值。

k 值不是透過模型基於資料去學習得到的,而是透過預設,然後根據結果反選確定的。任何一個超引數都是這樣確定的,其他演算法也如此。

import matplotlib.pyplot as plt
%matplotlib inline

hyper_params = [x for x in range(1,21)]
rmse_values = []
features = train_df.columns.tolist()
features.remove('price')

for hp in hyper_params:
    knn = KNeighborsRegressor(n_neighbors=hp, algorithm='brute')
    knn.fit(train_df[features], train_df['price'])
    predictions = knn.predict(test_df[features])
    mse = mean_squared_error(test_df['price'], predictions)
    rmse = mse**(1/2)
    rmse_values.append(rmse)

plt.plot(hyper_params, rmse_values,c='r',linestyle='-',marker='+')
複製程式碼

圖片描述

我們發現,k 越大,預測價格和真實價格的偏差從趨勢看會更準確。但要注意,k 越大計算量就越大。

我們在確定 k 值時,可以用 albow 法,也就是看上圖的拐點,形象上就是手肘的肘部。

相比 k=5,k=7 或 10 可能是更好的結果。

6.交叉驗證(Cross Validation)

上面我們的計算結果完全依賴訓練集和測試集,雖然對它們的劃分我們已經考慮了隨機性。但一次結果仍然具備偶爾性,尤其是當樣本量不夠大時。

交叉驗證就是為了解決這個問題。我們可以對同一個樣本集進行不同的訓練集測試集劃分。每次劃分後都重新進行訓練和預測,然後綜合去看待這些結果。

應用最廣泛的是 n 折交叉驗證,其過程是隨機將資料集切分成 n 份,用其中 n-1 個子集做訓練集,剩餘 1 個子集做測試集。這樣一共可以進行 n 次訓練和預測。

我們可以直接手寫該邏輯,如下。

sample_num = len(normalized_listings)
normalized_listings.loc[normalized_listings.index[0:int(0.2*sample_num)], "fold"] = 1
normalized_listings.loc[normalized_listings.index[int(0.2*sample_num):int(0.4*sample_num)], "fold"] = 2
normalized_listings.loc[normalized_listings.index[int(0.4*sample_num):int(0.6*sample_num)], "fold"] = 3
normalized_listings.loc[normalized_listings.index[int(0.6*sample_num):int(0.8*sample_num)], "fold"] = 4
normalized_listings.loc[normalized_listings.index[int(0.8*sample_num):], "fold"] = 5

fold_ids = [1,2,3,4,5]
def train_and_validate(df, folds):
    fold_rmses = []
    for fold in folds:
        # Train
        model = KNeighborsRegressor()
        train = df[df["fold"] != fold]
        test = df[df["fold"] == fold].copy()
        model.fit(train[features], train["price"])
        # Predict
        labels = model.predict(test[features])
        test["predicted_price"] = labels
        mse = mean_squared_error(test["price"], test["predicted_price"])
        rmse = mse**(1/2)
        fold_rmses.append(rmse)
    return(fold_rmses)

rmses = train_and_validate(normalized_listings, fold_ids)
avg_rmse = np.mean(rmses)
複製程式碼

圖片描述

工程上,我們要充分利用工具和資源。sklearn 庫就包含了我們常用的機器學習演算法實現,可以直接用來驗證。

from sklearn.model_selection import cross_val_score, KFold
kf = KFold(5, shuffle=True, random_state=1)
model = KNeighborsRegressor()
mses = cross_val_score(model, normalized_listings[features], normalized_listings["price"], scoring="neg_mean_squared_error", cv=kf)
rmses = np.sqrt(np.absolute(mses))
avg_rmse = np.mean(rmses)
複製程式碼

圖片描述

交叉驗證的結果置信度會更高,尤其是在小資料集上。因為它能夠一定程度地減輕偶然性誤差。

結合交叉驗證和超參最佳化,我們一般就得到了該資料集下用 knn 演算法預測的最優結果。

# 超參最佳化
num_folds = [x for x in range(2,50,2)]
rmse_values = []

for fold in num_folds:
    kf = KFold(fold, shuffle=True, random_state=1)
    model = KNeighborsRegressor()
    mses = cross_val_score(model, normalized_listings[features], normalized_listings["price"], scoring="neg_mean_squared_error", cv=kf)
    rmses = np.sqrt(np.absolute(mses))
    avg_rmse = np.mean(rmses)
    std_rmse = np.std(rmses)
    rmse_values.append(avg_rmse)

plt.plot(num_folds, rmse_values,c='r',linestyle='-',marker='+')
複製程式碼

圖片描述

我們得到了相同的趨勢,k 越大,效果趨勢上更好。同時因為交叉驗證一定程度上解決了過擬合問題,理想的 k 值越大,模型可以更復雜些。

7.總結

從 k-近鄰演算法的核心思想以及以上編碼過程可以看出,該演算法是基於例項的學習方法,因為它完全依靠訓練集裡的例項。

該演算法不需什麼數學方法,很容易理解。但是非常不適合應用在大資料集上,因為 k-近鄰演算法每一次預測都需要計算整個訓練集的資料到待預測資料的距離,然後增序排列,計算量巨大。

如果能用數學函式來描述資料集的特徵變數與目標變數的關係,那麼一旦用訓練集獲得了該函式表示,預測就是簡簡單單的數學計算問題了。計算複雜度大大降低。

其他的經典機器學習演算法基本都是一個函式表達問題。後面我們再看。

作者:thunderbang
連結:
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2508/viewspace-2797202/,如需轉載,請註明出處,否則將追究法律責任。

相關文章