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

我使用加利福尼亞州房價數據來作例子。訓練集和驗證集用到的CSV文件在這裏:https://download.csdn.net/download/zhangchao19890805/10584496

測試集用到的CSV文件在這裏:
https://download.csdn.net/download/zhangchao19890805/10631336

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

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

有兩種思路回答此問題:

  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();

運行結果:
3.png

用測試集進行測試,使用 ZcValidateTest 類:

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 ZcValidateTest:
    # 從CSV文件中讀取數據,返回DataFrame類型的數據集合。
    def read_csv(self, pa_url):
        v_dataframe = pd.read_csv(pa_url, sep=",")
        # 打亂數據集合的順序。有時候數據文件有可能是根據某種順序排列的,會影響到我們對數據的處理。
        v_dataframe = v_dataframe.reindex(np.random.permutation(v_dataframe.index))
        return v_dataframe
    
    
    # 處理緯度
    # 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 preprocess_features(self, california_housing_dataframe):
        v_selected_features = california_housing_dataframe[
            [
                "median_income",
                "latitude"
             ]
        ]
        result = self.fn_process_latitude(v_selected_features.copy())
        return result


    # 預處理標籤
    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
    
    # 根據數學模型計算預測值。公式是 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
    
     # 主函數
    def main(self):
        tf.logging.set_verbosity(tf.logging.ERROR)
        pd.options.display.max_rows = 10
        pd.options.display.float_format = '{:.1f}'.format
        
        # 通過訓練模型得到的權重
        v_weight_arr = [6.9194322e+00,  3.1475018e+01, -3.1626907e-03,  6.4394033e-01,
        3.4379005e+00,  1.7576288e-01,  1.6979814e-01,  9.4234413e-01,
        1.5170804e+00,  1.0799712e-01,  5.1610660e-02, -2.5423512e-01,
        1.0890015e-01, -1.8408354e-02, -2.0731870e-02, -3.8955014e-02]
        
        # 讀取驗證集
        california_housing_dataframe = self.read_csv("http://114.116.18.230/california_housing_train.csv")
        # 對於驗證集,我們從共 17000 個樣本中選擇後 5000 個樣本。
        validation_examples = self.preprocess_features(california_housing_dataframe.tail(5000))
        validation_targets = self.preprocess_targets(california_housing_dataframe.tail(5000))
        # 根據已經訓練得到的模型係數,計算預驗證集的預測值。
        v_validate_predict_arr = self.fn_predict(validation_examples, v_weight_arr)
        # 計算驗證集的預測值和標籤之間的均方根誤差。
        validation_root_mean_squared_error = math.sqrt(
            metrics.mean_squared_error(v_validate_predict_arr, validation_targets["median_house_value"]))
        print("validation RMSE: " + str(validation_root_mean_squared_error))
        
        # 讀取測試集
        test_dataframe = self.read_csv("http://114.116.18.230/california_housing_test.csv")
        test_examples = self.preprocess_features(test_dataframe)
        test_targets = self.preprocess_targets(test_dataframe)
        # 計算測試集的預測值
        v_test_predict_arr = self.fn_predict(test_examples, v_weight_arr)
        # 計算測試集的預測值和標籤之間的均方根誤差。
        test_root_mean_squared_error = math.sqrt(
                metrics.mean_squared_error(v_test_predict_arr, test_targets["median_house_value"]))
        print("test RMSE: " + str(test_root_mean_squared_error))
        

        
t = ZcValidateTest()
t.main()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章