1. 程式人生 > >【127】TensorFlow對特徵值分箱並使用獨熱編碼

【127】TensorFlow對特徵值分箱並使用獨熱編碼

在實際應用的時候,許多特徵值和標籤之間不是線性關係。

那麼該如何處理這種特徵值呢?

有兩種思路回答此問題:

  1. 設計複雜的數學公式,並利用資料對模型進行訓練,獲得各個同類項的權重。比如把簡單的 y = w0 + w1x 改成複雜的 y = w0 + w1x2 + w2x
  2. 使用分箱技術。把特徵值分佈的數值範圍分成若干段,記錄下每條資料落在那一段。使用獨熱編碼的辦法,把每一段轉換成一項特徵值,然後用零和一標記每條資料落在哪一段。這是一種思考起來較為簡便的方法,更容易讓人理解。

本文用分箱加獨熱編碼的方法編寫DEMO。同上篇文章一樣,使用了加利福尼亞州房價的資料來源。按照慣例,先展示資料的概要資訊,這是處理資料的良好習慣。程式碼是 ZcSummary 類。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import math
from sklearn import metrics
from IPython import display

class ZcSummary:
    # 從CSV檔案中讀取資料,返回DataFrame型別的資料集合。
    def read_csv(self):
        v_dataframe = pd.read_csv("http://114.116.18.230/california_housing_train.csv"
, sep=",") # 打亂資料集合的順序。有時候資料檔案有可能是根據某種順序排列的,會影響到我們對資料的處理。 v_dataframe = v_dataframe.reindex(np.random.permutation(v_dataframe.index)) return v_dataframe # 預處理特徵值 def preprocess_features(self, california_housing_dataframe): selected_features = california_housing_dataframe[
["latitude", "longitude", "housing_median_age", "total_rooms", "total_bedrooms", "population", "households", "median_income"] ] processed_features = selected_features.copy() # 增加一個新屬性:人均房屋數量。 processed_features["rooms_per_person"] = ( california_housing_dataframe["total_rooms"] / california_housing_dataframe["population"]) return processed_features # 預處理標籤 def preprocess_targets(self, california_housing_dataframe): output_targets = pd.DataFrame() # 數值過大可能引起訓練過程中的錯誤。因此要把房價數值先縮小成原來的 # 千分之一,然後作為標籤值返回。 output_targets["median_house_value"] = ( california_housing_dataframe["median_house_value"] / 1000.0) return output_targets # 主函式 def main(self): tf.logging.set_verbosity(tf.logging.ERROR) pd.options.display.max_rows = 10 pd.options.display.float_format = '{:.1f}'.format california_housing_dataframe = self.read_csv() # 對於訓練集,我們從共 17000 個樣本中選擇前 12000 個樣本。 training_examples = self.preprocess_features(california_housing_dataframe.head(12000)) training_targets = self.preprocess_targets(california_housing_dataframe.head(12000)) # 對於驗證集,我們從共 17000 個樣本中選擇後 5000 個樣本。 validation_examples = self.preprocess_features(california_housing_dataframe.tail(5000)) validation_targets = self.preprocess_targets(california_housing_dataframe.tail(5000)) # 展示資料集的概要情況。 print("Training examples summary:") display.display(training_examples.describe()) print("Validation examples summary:") display.display(validation_examples.describe()) print("Training targets summary:") display.display(training_targets.describe()) print("Validation targets summary:") display.display(validation_targets.describe()) t = ZcSummary() t.main()

執行結果:

1.png

為了節約篇幅,我們使用了上篇文章:【126】TensorFlow 使用皮爾遜相關係數找出和標籤相關性最大的特徵值 中得到的結論。使用 median_income 和 latitude 這兩個特徵值來訓練模型。
我們先檢查維度 latitude 和房價 median_house_value 是否是線性關係,檢查方法是檢視散點圖。當發現沒有線性關係的時候,檢視直方圖,觀察 latitude 的分佈情況。我用 ZcCheck 類完成了以上工作:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import math
from sklearn import metrics
from IPython import display

class ZcCheck:
    # 從CSV檔案中讀取資料,返回DataFrame型別的資料集合。
    def read_csv(self):
        v_dataframe = pd.read_csv("http://114.116.18.230/california_housing_train.csv", sep=",")
        # 打亂資料集合的順序。有時候資料檔案有可能是根據某種順序排列的,會影響到我們對資料的處理。
        v_dataframe = v_dataframe.reindex(np.random.permutation(v_dataframe.index))
        return v_dataframe
    
    # 預處理特徵值
    def preprocess_features(self, california_housing_dataframe):
        selected_features = california_housing_dataframe[
            ["latitude",
             "longitude",
             "housing_median_age",
             "total_rooms",
             "total_bedrooms",
             "population",
             "households",
             "median_income"]
        ]
        processed_features = selected_features.copy()
        # 增加一個新屬性:人均房屋數量。
        processed_features["rooms_per_person"] = (
            california_housing_dataframe["total_rooms"] /
            california_housing_dataframe["population"])
        return processed_features


    # 預處理標籤
    def preprocess_targets(self, california_housing_dataframe):
        output_targets = pd.DataFrame()
        # 數值過大可能引起訓練過程中的錯誤。因此要把房價數值先縮小成原來的
        # 千分之一,然後作為標籤值返回。
        output_targets["median_house_value"] = (
            california_housing_dataframe["median_house_value"] / 1000.0)
        return output_targets
    
     # 主函式
    def main(self):
        tf.logging.set_verbosity(tf.logging.ERROR)
        pd.options.display.max_rows = 10
        pd.options.display.float_format = '{:.1f}'.format
        
        california_housing_dataframe = self.read_csv()
        # 對於訓練集,我們從共 17000 個樣本中選擇前 12000 個樣本。
        training_examples = self.preprocess_features(california_housing_dataframe.head(12000))
        training_targets = self.preprocess_targets(california_housing_dataframe.head(12000))
        # 對於驗證集,我們從共 17000 個樣本中選擇後 5000 個樣本。
        validation_examples = self.preprocess_features(california_housing_dataframe.tail(5000))
        validation_targets = self.preprocess_targets(california_housing_dataframe.tail(5000))
        
        # 檢查緯度latitude和房價之間是不是線性關係。
        fig = plt.figure()
        fig.set_size_inches(15,5)
        ax = fig.add_subplot(1,3,1)
        ax.scatter(training_examples["latitude"], training_targets["median_house_value"])
        
        # 發現latitude和房價之間不是線性關係後,檢查latitude的分佈情況,方便決定
        # latitude 如何分桶。
        ax = fig.add_subplot(1,3,2)
        ax = training_examples["latitude"].hist()
        plt.show()

t = ZcCheck()
t.main()

執行結果:

2.png

先在通過直方圖,我們瞭解了 latitude 的分佈範圍。先在可以對 latitude 分箱,然後使用獨熱編碼訓練模型。具體操作在 ZcTrain 類中:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import math
from sklearn import metrics
from IPython import display
  

class ZcTrain:
    # 從CSV檔案中讀取資料,返回DataFrame型別的資料集合。
    def read_csv(self):
        v_dataframe = pd.read_csv("http://114.116.18.230/california_housing_train.csv", sep=",")
        # 打亂資料集合的順序。有時候資料檔案有可能是根據某種順序排列的,會影響到我們對資料的處理。
        v_dataframe = v_dataframe.reindex(np.random.permutation(v_dataframe.index))
        return v_dataframe
    
    
    
    # 預處理特徵值
    def preprocess_features(self, california_housing_dataframe):
        selected_features = california_housing_dataframe[
            ["latitude",
             "longitude",
             "housing_median_age",
             "total_rooms",
             "total_bedrooms",
             "population",
             "households",
             "median_income"]
        ]
        processed_features = selected_features.copy()
        # 增加一個新屬性:人均房屋數量。
        processed_features["rooms_per_person"] = (
            california_housing_dataframe["total_rooms"] /
            california_housing_dataframe["population"])
        return processed_features


    # 預處理標籤
    def preprocess_targets(self, california_housing_dataframe):
        output_targets = pd.DataFrame()
        # Scale the target to be in units of thousands of dollars.
        output_targets["median_house_value"] = (
            california_housing_dataframe["median_house_value"] / 1000.0)
        return output_targets
    
    
    # 根據數學模型計算預測值。公式是 y = w0 + w1 * x1 + w2 * x2 .... + wN * xN
    def fn_predict(self, pa_dataframe, pa_weight_arr):
        v_result = []
        v_weight_num = len(pa_weight_arr)
        for var_row_index in pa_dataframe.index:
            y = pa_weight_arr[0]
            for v_index in range(v_weight_num-1):
                y = y + pa_weight_arr[v_index + 1] * pa_dataframe.loc[var_row_index].values[v_index]
            v_result.append(y)
        return v_result
    

    # 訓練形如 y = w0 + w1 * x1 + w2 * x2 + ...  的直線模型。x1 x2 ...是自變數,
    # w0 是常數項,w1 w2 ... 是對應自變數的權重。
    # feature_arr 特徵值的矩陣。每一行是 [1.0, x1_data, x2_data, ...] 
    # label_arr 標籤的陣列。相當於 y = kx + b 中的y。
    # training_steps 訓練的步數。即訓練的迭代次數。
    # period         誤差報告粒度
    # learning_rate 在梯度下降演算法中,控制梯度步長的大小。
    def fn_train_line(self, feature_arr, label_arr, validate_feature_arr, validate_label_arr, training_steps, periods, learning_rate):
        feature_tf_arr = feature_arr
        label_tf_arr = np.array([[e] for e in label_arr]).astype(np.float32)
        # 整個訓練分成若干段,即誤差報告粒度,用periods表示。
        # steps_per_period 表示平均每段有多少次訓練
        steps_per_period = training_steps / periods
        # 存放 L2 損失的陣列
        loss_arr = []
        # 權重陣列的長度。也就是權重的個數。
        weight_arr_length = len(feature_arr[0])
        # 開啟TF會話,在TF 會話的上下文中進行 TF 的操作。
        with tf.Session() as sess:
            # 訓練集的均方根誤差RMSE。這是儲存誤差報告的陣列。
            train_rmse_arr = []
            # 驗證集的均方根誤差RMSE。
            validate_rmse_arr = []

            # 設定 tf 張量(tensor)。注意:TF會話中的註釋裡面提到的常量和變數是針對TF設定而言,不是python語法。

            # 因為在TF運算過程中,x作為特徵值,y作為標籤
            # 是不會改變的,所以分別設定成input 和 target 兩個常量。
            # 這是 x 取值的張量。設一共有m條資料,可以把input理解成是一個m行2列的矩陣。矩陣第一列都是1,第二列是x取值。
            input = tf.constant(feature_tf_arr)
            # 設定 y 取值的張量。target可以被理解成是一個m行1列的矩陣。 有些文章稱target為標籤。
            target = tf.constant(label_tf_arr)

            # 設定權重變數。因為在每次訓練中,都要改變權重,來尋找L2損失最小的權重,所以權重是變數。
            # 可以把權重理解成一個多行1列的矩陣。初始值是隨機的。行數就是權重數。
            weights = tf.Variable(tf.random_normal([weight_arr_length, 1], 0, 0.1))

            # 初始化上面所有的 TF 常量和變數。
            tf.global_variables_initializer().run()
            # input 作為特徵值和權重做矩陣乘法。m行2列矩陣乘以2行1列矩陣,得到m行1列矩陣。
            # yhat是新矩陣,yhat中的每個數 yhat' = w0 * 1 + w1 * x1 + w2 * x2 ...。 
            # yhat是預測值,隨著每次TF調整權重,yhat都會變化。
            yhat = tf.matmul(input, weights)
            # tf.subtract計算兩個張量相減,當然兩個張量必須形狀一樣。 即 yhat - target。
            yerror = tf.subtract(yhat, target)
            # 計算L2損失,也就是方差。
            loss = tf.nn.l2_loss(yerror)
            # 梯度下降演算法。
            zc_optimizer = tf.train.GradientDescentOptimizer(learning_rate)
            # 注意:為了安全起見,我們還會通過 clip_gradients_by_norm 將梯度裁剪應用到我們的優化器。
            # 梯度裁剪可確保梯度大小在訓練期間不會變得過大,梯度過大會導致梯度下降法失敗。
            zc_optimizer = tf.contrib.estimator.clip_gradients_by_norm(zc_optimizer, 5.0)
            zc_optimizer = zc_optimizer.minimize(loss)
            for _ in range(periods):
                for _ in range(steps_per_period):
                    # 重複執行梯度下降演算法,更新權重數值,找到最合適的權重數值。
                    sess.run(zc_optimizer)
                    # 每次迴圈都記錄下損失loss的值,並放到陣列loss_arr中。
                    loss_arr.append(loss.eval())
                v_tmp_weight_arr = weights.eval()
                # 計算均方根誤差。其中 np.transpose(yhat.eval())[0] 把列向量轉換成一維陣列
                train_rmse_arr.append(math.sqrt(
                        metrics.mean_squared_error(np.transpose(yhat.eval())[0], label_tf_arr)))
                validate_rmse_arr.append(math.sqrt(
                        metrics.mean_squared_error(self.fn_predict(validate_feature_arr, v_tmp_weight_arr), validate_label_arr)))
            # 把列向量轉換成一維陣列
            zc_weight_arr = np.transpose(weights.eval())[0]
            zc_yhat = np.transpose(yhat.eval())[0]
        return (zc_weight_arr, zc_yhat, loss_arr, train_rmse_arr, validate_rmse_arr)
    
    
    # 構建用於訓練的特徵值。
    # pa_dataframe 原來資料的 Dataframe
    # 本質上是用二維陣列構建一個矩陣。裡面的每個一維陣列都是矩陣的一行,形狀類似下面這種形式:
    #    1.0, x1[0], x2[0], x3[0], ...
    #    1.0, x1[1], x2[1], x3[1], ...
    #    .........................
    # 其中x1, x2, x3 表示資料的某個維度,比如:"latitude","longitude","housing_median_age"。
    # 也可以看作是公式中的多個自變數。
    def fn_construct_tf_feature_arr(self, pa_dataframe):
        v_result = []
        # dataframe中每列的名稱。
        zc_var_col_name_arr = [e for e in pa_dataframe]
        # 遍歷dataframe中的每行。
        for row_index in pa_dataframe.index:
            zc_var_tf_row = [1.0]
            for i in range(len(zc_var_col_name_arr)):
                zc_var_tf_row.append(pa_dataframe.loc[row_index].values[i])
            v_result.append(zc_var_tf_row)
        return v_result
    
    # 畫損失的變化圖。
    # pa_ax  Axes
    # pa_arr_train_rmse 訓練次數。
    # pa_arr_validate_rmse 損失變化的記錄
    def fn_paint_loss(self, pa_ax, pa_arr_train_rmse, pa_arr_validate_rmse):
        pa_ax.plot(range(0, len(pa_arr_train_rmse)), pa_arr_train_rmse, label="training", color="blue")
        pa_ax.plot(range(0, len(pa_arr_validate_rmse)), pa_arr_validate_rmse, label="validate", color="orange")
        
    # 處理緯度
    # pa_source_df 源資料的DataFrame
    # 返回對 latitude進行獨熱編碼後的 DataFrame
    def fn_process_latitude(self, pa_source_df):
        v_result = pd.DataFrame()
        v_result["median_income"] = pa_source_df["median_income"]
        # 平均分箱
        v_min = 31.5
        v_arr = []
        for v_counter in range(14):
            v_left = v_min + v_counter
            v_right = v_left + 1
            v_arr.append([v_left, v_right])
        # 增加獨熱編碼
        for v_item in v_arr:
            v_key_str = "latitude_" + str(v_item[0]) + "_to_" + str(v_item[1])
            v_result[v_key_str] = pa_source_df["latitude"].apply(
                lambda l: 1.0 if l >= v_item[0] and l < v_item[1] else 0.0)
        return v_result
    
    
    # 主函式
    def main(self):
        tf.logging.set_verbosity(tf.logging.ERROR)
        pd.options.display.max_rows = 10
        pd.options.display.float_format = '{:.1f}'.format
        
        california_housing_dataframe = self.read_csv()
        # 對於訓練集,我們從共 17000 個樣本中選擇前 12000 個樣本。
        training_examples = self.preprocess_features(california_housing_dataframe.head(12000))
        training_targets = self.preprocess_targets(california_housing_dataframe.head(12000))
        # 對於驗證集,我們從共 17000 個樣本中選擇後 5000 個樣本。
        validation_examples = self.preprocess_features(california_housing_dataframe.tail(5000))
        validation_targets = self.preprocess_targets(california_housing_dataframe.tail(5000))

        # 對latitude進行分箱,處理成獨熱編碼
        v_one_hot_training_examples = self.fn_process_latitude(training_examples)
        v_one_hot_validation_examples =  self.fn_process_latitude(validation_examples)
        
        # 在模型訓練開始之前,做好特徵值的準備工作。構建適於訓練的矩陣。
        v_train_feature_arr = self.fn_construct_tf_feature_arr(v_one_hot_training_examples)
        
        (v_weight_arr, v_yhat, v_loss_arr, v_train_rmse_arr, v_validate_rmse_arr) = self.fn_train_line(v_train_feature_arr, 
                    training_targets["median_house_value"], v_one_hot_validation_examples, 
                    validation_targets["median_house_value"], 500, 20, 0.013)
        # 列印權重
        print("weights:  ", v_weight_arr)
        print("Training RMSE " + str(v_train_rmse_arr[len(v_train_rmse_arr) - 1]) + " Validate RMSE: " + 
          str(v_validate_rmse_arr[len(v_validate_rmse_arr) - 1]))
        
        # 畫出損失變化曲線
        fig = plt.figure()
        fig.set_size_inches(5,5)
        self.fn_paint_loss(fig.add_subplot(1,1,1), v_train_rmse_arr, v_validate_rmse_arr)
        
        plt.show()

        
t = ZcTrain();
t.main()<