來源:https://www.numpy.org.cn/deep/basics/fit_a_line.html
線性回歸
讓我們從經典的線性回歸(Linear Regression [1])模型開始這份教程。在這一章里,你將使用真實的數據集建立起一個房價預測模型,並且了解到機器學習中的若干重要概念。
本教程源代碼目錄在book/fit_a_line, 初次使用請您參考Book文檔使用說明。
#說明:
1.硬件環境要求: 本文可支持在CPU、GPU下運行 2. Docker鏡像支持的CUDA/cuDNN版本: 如果使用了Docker運行Book,請注意:這里所提供的默認鏡像的GPU環境為 CUDA 8/cuDNN 5,對於NVIDIA Tesla V100等要求CUDA 9的 GPU,使用該鏡像可能會運行失敗。 3. 文檔和腳本中代碼的一致性問題: 請注意:為使本文更加易讀易用,我們拆分、調整了train.py的代碼並放入本文。本文中代碼與train.py的運行結果一致,可直接運行train.py進行驗證。
#背景介紹
給定一個大小為nn的數據集 其中 xi1,…,xidxi1,…,xid 是第ii個樣本dd個屬性上的取值, yiyi是該樣本待預測的目標。線性回歸模型假設目標yiyi可以被屬性間的線性組合描述,即
例如,在我們將要建模的房價預測問題里,xijxij是描述房子ii的各種屬性(比如房間的個數、周圍學校和醫院的個數、交通狀況等),而 yiyi是房屋的價格。
初看起來,這個假設實在過於簡單了,變量間的真實關系很難是線性的。但由於線性回歸模型有形式簡單和易於建模分析的優點,它在實際問題中得到了大量的應用。很多經典的統計學習、機器學習書籍[2,3,4]也選擇對線性模型獨立成章重點講解。
#效果展示
我們使用從UCI Housing Data Set獲得的波士頓房價數據集進行模型的訓練和預測。下面的散點圖展示了使用模型對部分房屋價格進行的預測。其中,每個點的橫坐標表示同一類房屋真實價格的中位數,縱坐標表示線性回歸模型根據特征預測的結果,當二者值完全相等的時候就會落在虛線上。所以模型預測得越准確,則點離虛線越近。
圖1. 預測值 V.S. 真實值
#模型概覽
#模型定義
在波士頓房價數據集中,和房屋相關的值共有14個:前13個用來描述房屋相關的各種信息,即模型中的 xixi;最后一個值為我們要預測的該類房屋價格的中位數,即模型中的 yiyi。因此,我們的模型就可以表示成:
Y^Y^ 表示模型的預測結果,用來和真實值YY區分。模型要學習的參數即:ω1,…,ω13,bω1,…,ω13,b。
建立模型后,我們需要給模型一個優化目標,使得學到的參數能夠讓預測值Y^Y^盡可能地接近真實值YY。這里我們引入損失函數(Loss Function,或Cost Function)這個概念。 輸入任意一個數據樣本的目標值yiyi和模型給出的預測值yi^yi^,損失函數輸出一個非負的實值。這個實值通常用來反映模型誤差的大小。
對於線性回歸模型來講,最常見的損失函數就是均方誤差(Mean Squared Error, MSE)了,它的形式是:
即對於一個大小為nn的測試集,MSEMSE是nn個數據預測結果誤差平方的均值。
對損失函數進行優化所采用的方法一般為梯度下降法。梯度下降法是一種一階最優化算法。如果f(x)f(x)在點xnxn有定義且可微,則認為f(x)f(x)在點xnxn沿着梯度的負方向−▽f(xn)−▽f(xn)下降的是最快的。反復調節xx,使得f(x)f(x)接近最小值或者極小值,調節的方式為:
其中λ代表學習率。這種調節的方法稱為梯度下降法。
#訓練過程
定義好模型結構之后,我們要通過以下幾個步驟進行模型訓練
- 初始化參數,其中包括權重ωiωi和偏置bb,對其進行初始化(如0均值,1方差)。
- 網絡正向傳播計算網絡輸出和損失函數。
- 根據損失函數進行反向誤差傳播 (backpropagation),將網絡誤差從輸出層依次向前傳遞, 並更新網絡中的參數。
- 重復2~3步驟,直至網絡訓練誤差達到規定的程度或訓練輪次達到設定值。
#數據集
#數據集介紹
這份數據集共506行,每行包含了波士頓郊區的一類房屋的相關信息及該類房屋價格的中位數。其各維屬性的意義如下:
屬性名 | 解釋 | 類型 |
---|---|---|
CRIM | 該鎮的人均犯罪率 | 連續值 |
ZN | 占地面積超過25,000平方呎的住宅用地比例 | 連續值 |
INDUS | 非零售商業用地比例 | 連續值 |
CHAS | 是否鄰近 Charles River | 離散值,1=鄰近;0=不鄰近 |
NOX | 一氧化氮濃度 | 連續值 |
RM | 每棟房屋的平均客房數 | 連續值 |
AGE | 1940年之前建成的自用單位比例 | 連續值 |
DIS | 到波士頓5個就業中心的加權距離 | 連續值 |
RAD | 到徑向公路的可達性指數 | 連續值 |
TAX | 全值財產稅率 | 連續值 |
PTRATIO | 學生與教師的比例 | 連續值 |
B | 1000(BK - 0.63)^2,其中BK為黑人占比 | 連續值 |
LSTAT | 低收入人群占比 | 連續值 |
MEDV | 同類房屋價格的中位數 | 連續值 |
#數據預處理
#連續值與離散值
觀察一下數據,我們的第一個發現是:所有的13維屬性中,有12維的連續值和1維的離散值(CHAS)。離散值雖然也常使用類似0、1、2這樣的數字表示,但是其含義與連續值是不同的,因為這里的差值沒有實際意義。例如,我們用0、1、2來分別表示紅色、綠色和藍色的話,我們並不能因此說“藍色和紅色”比“綠色和紅色”的距離更遠。所以通常對一個有dd個可能取值的離散屬性,我們會將它們轉為dd個取值為0或1的二值屬性或者將每個可能取值映射為一個多維向量。不過就這里而言,因為CHAS本身就是一個二值屬性,就省去了這個麻煩。
#屬性的歸一化
另外一個稍加觀察即可發現的事實是,各維屬性的取值范圍差別很大(如圖2所示)。例如,屬性B的取值范圍是[0.32, 396.90],而屬性NOX的取值范圍是[0.3850, 0.8170]。這里就要用到一個常見的操作-歸一化(normalization)了。歸一化的目標是把各位屬性的取值范圍放縮到差不多的區間,例如[-0.5,0.5]。這里我們使用一種很常見的操作方法:減掉均值,然后除以原取值范圍。
做歸一化(或 Feature scaling)至少有以下3個理由:
-
過大或過小的數值范圍會導致計算時的浮點上溢或下溢。
-
不同的數值范圍會導致不同屬性對模型的重要性不同(至少在訓練的初始階段如此),而這個隱含的假設常常是不合理的。這會對優化的過程造成困難,使訓練時間大大的加長。
-
很多的機器學習技巧/模型(例如L1,L2正則項,向量空間模型-Vector Space Model)都基於這樣的假設:所有的屬性取值都差不多是以0為均值且取值范圍相近的。
圖2. 各維屬性的取值范圍
#整理訓練集與測試集
我們將數據集分割為兩份:一份用於調整模型的參數,即進行模型的訓練,模型在這份數據集上的誤差被稱為訓練誤差;另外一份被用來測試,模型在這份數據集上的誤差被稱為測試誤差。我們訓練模型的目的是為了通過從訓練數據中找到規律來預測未知的新數據,所以測試誤差是更能反映模型表現的指標。分割數據的比例要考慮到兩個因素:更多的訓練數據會降低參數估計的方差,從而得到更可信的模型;而更多的測試數據會降低測試誤差的方差,從而得到更可信的測試誤差。我們這個例子中設置的分割比例為8:28:2
在更復雜的模型訓練過程中,我們往往還會多使用一種數據集:驗證集。因為復雜的模型中常常還有一些超參數(Hyperparameter)需要調節,所以我們會嘗試多種超參數的組合來分別訓練多個模型,然后對比它們在驗證集上的表現選擇相對最好的一組超參數,最后才使用這組參數下訓練的模型在測試集上評估測試誤差。由於本章訓練的模型比較簡單,我們暫且忽略掉這個過程。
#訓練
fit_a_line/train.py
演示了訓練的整體過程。
#配置數據提供器(Datafeeder)
首先我們引入必要的庫:
from __future__ import print_function import paddle import paddle.fluid as fluid import numpy import math import sys
我們通過uci_housing模塊引入了數據集合UCI Housing Data Set
其中,在uci_housing模塊中封裝了:
- 數據下載的過程。下載數據保存在~/.cache/paddle/dataset/uci_housing/housing.data。
- 數據預處理的過程。
接下來我們定義了用於訓練的數據提供器。提供器每次讀入一個大小為BATCH_SIZE
的數據批次。如果用戶希望加一些隨機性,它可以同時定義一個批次大小和一個緩存大小。這樣的話,每次數據提供器會從緩存中隨機讀取批次大小那么多的數據。
BATCH_SIZE = 20 train_reader = paddle.batch( paddle.reader.shuffle( paddle.dataset.uci_housing.train(), buf_size=500), batch_size=BATCH_SIZE) test_reader = paddle.batch( paddle.reader.shuffle( paddle.dataset.uci_housing.test(), buf_size=500), batch_size=BATCH_SIZE)
如果想直接從txt文件中讀取數據的話,可以參考以下方式(需要自行准備txt文件)。
feature_names = [
'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX',
'PTRATIO', 'B', 'LSTAT', 'convert'
]
feature_num = len(feature_names)
data = numpy.fromfile(filename, sep=' ') # 從文件中讀取原始數據
data = data.reshape(data.shape[0] // feature_num, feature_num)
maximums, minimums, avgs = data.max(axis=0), data.min(axis=0), data.sum(axis=0)/data.shape[0]
for i in six.moves.range(feature_num-1):
data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i]) # six.moves可以兼容python2和python3
ratio = 0.8 # 訓練集和驗證集的划分比例
offset = int(data.shape[0]*ratio)
train_data = data[:offset]
test_data = data[offset:]
def reader_creator(train_data):
def reader():
for d in train_data:
yield d[:-1], d[-1:]
return reader
train_reader = paddle.batch(
paddle.reader.shuffle(
reader_creator(train_data), buf_size=500),
batch_size=BATCH_SIZE)
test_reader = paddle.batch(
paddle.reader.shuffle(
reader_creator(test_data), buf_size=500),
batch_size=BATCH_SIZE)
#配置訓練程序
訓練程序的目的是定義一個訓練模型的網絡結構。對於線性回歸來講,它就是一個從輸入到輸出的簡單的全連接層。更加復雜的結果,比如卷積神經網絡,遞歸神經網絡等會在隨后的章節中介紹。訓練程序必須返回平均損失
作為第一個返回值,因為它會被后面反向傳播算法所用到。
x = fluid.layers.data(name='x', shape=[13], dtype='float32') # 定義輸入的形狀和數據類型 y = fluid.layers.data(name='y', shape=[1], dtype='float32') # 定義輸出的形狀和數據類型 y_predict = fluid.layers.fc(input=x, size=1, act=None) # 連接輸入和輸出的全連接層 main_program = fluid.default_main_program() # 獲取默認/全局主函數 startup_program = fluid.default_startup_program() # 獲取默認/全局啟動程序 cost = fluid.layers.square_error_cost(input=y_predict, label=y) # 利用標簽數據和輸出的預測數據估計方差 avg_loss = fluid.layers.mean(cost) # 對方差求均值,得到平均損失
詳細資料請參考: fluid.default_main_program fluid.default_startup_program
#Optimizer Function 配置
在下面的 SGD optimizer
,learning_rate
是學習率,與網絡的訓練收斂速度有關系。
#克隆main_program得到test_program #有些operator在訓練和測試之間的操作是不同的,例如batch_norm,使用參數for_test來區分該程序是用來訓練還是用來測試 #該api不會刪除任何操作符,請在backward和optimization之前使用 test_program = main_program.clone(for_test=True) sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001) sgd_optimizer.minimize(avg_loss)
#定義運算場所
我們可以定義運算是發生在CPU還是GPU
use_cuda = False place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace() # 指明executor的執行場所 ###executor可以接受傳入的program,並根據feed map(輸入映射表)和fetch list(結果獲取表)向program中添加數據輸入算子和結果獲取算子。使用close()關閉該executor,調用run(...)執行program。 exe = fluid.Executor(place)
詳細資料請參考: fluid.executor
#創建訓練過程
訓練需要有一個訓練程序和一些必要參數,並構建了一個獲取訓練過程中測試誤差的函數。必要參數有executor,program,reader,feeder,fetch_list,executor表示之前創建的執行器,program表示執行器所執行的program,是之前創建的program,如果該項參數沒有給定的話則默認使用default_main_program,reader表示讀取到的數據,feeder表示前向輸入的變量,fetch_list表示用戶想得到的變量或者命名的結果。
num_epochs = 100 def train_test(executor, program, reader, feeder, fetch_list): accumulated = 1 * [0] count = 0 for data_test in reader(): outs = executor.run(program=program, feed=feeder.feed(data_test), fetch_list=fetch_list) accumulated = [x_c[0] + x_c[1][0] for x_c in zip(accumulated, outs)] # 累加測試過程中的損失值 count += 1 # 累加測試集中的樣本數量 return [x_d / count for x_d in accumulated] # 計算平均損失
#訓練主循環
給出需要存儲的目錄名,並初始化一個執行器。
%matplotlib inline params_dirname = "fit_a_line.inference.model" feeder = fluid.DataFeeder(place=place, feed_list=[x, y]) exe.run(startup_program) train_prompt = "train cost" test_prompt = "test cost" from paddle.utils.plot import Ploter plot_prompt = Ploter(train_prompt, test_prompt) step = 0 exe_test = fluid.Executor(place)
paddlepaddle提供了reader機制來讀取訓練數據。reader會一次提供多列數據,因此我們需要一個python的列表來定義讀取順序。我們構建一個循環來進行訓練,直到訓練結果足夠好或者循環次數足夠多。 如果訓練迭代次數滿足參數保存的迭代次數,可以把訓練參數保存到params_dirname
。 設置訓練主循環
for pass_id in range(num_epochs): for data_train in train_reader(): avg_loss_value, = exe.run(main_program, feed=feeder.feed(data_train), fetch_list=[avg_loss]) if step % 10 == 0: # 每10個批次記錄並輸出一下訓練損失 plot_prompt.append(train_prompt, step, avg_loss_value[0]) plot_prompt.plot() print("%s, Step %d, Cost %f" % (train_prompt, step, avg_loss_value[0])) if step % 100 == 0: # 每100批次記錄並輸出一下測試損失 test_metics = train_test(executor=exe_test, program=test_program, reader=test_reader, fetch_list=[avg_loss.name], feeder=feeder) plot_prompt.append(test_prompt, step, test_metics[0]) plot_prompt.plot() print("%s, Step %d, Cost %f" % (test_prompt, step, test_metics[0])) if test_metics[0] < 10.0: # 如果准確率達到要求,則停止訓練 break step += 1 if math.isnan(float(avg_loss_value[0])): sys.exit("got NaN loss, training failed.") #保存訓練參數到之前給定的路徑中 if params_dirname is not None: fluid.io.save_inference_model(params_dirname, ['x'], [y_predict], exe)
#預測
需要構建一個使用訓練好的參數來進行預測的程序,訓練好的參數位置在params_dirname
。
#准備預測環境
類似於訓練過程,預測器需要一個預測程序來做預測。我們可以稍加修改我們的訓練程序來把預測值包含進來。
infer_exe = fluid.Executor(place) inference_scope = fluid.core.Scope()
#預測
保存圖片
def save_result(points1, points2): import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt x1 = [idx for idx in range(len(points1))] y1 = points1 y2 = points2 l1 = plt.plot(x1, y1, 'r--', label='predictions') l2 = plt.plot(x1, y2, 'g--', label='GT') plt.plot(x1, y1, 'ro-', x1, y2, 'g+-') plt.title('predictions VS GT') plt.legend() plt.savefig('./image/prediction_gt.png')
通過fluid.io.load_inference_model,預測器會從params_dirname
中讀取已經訓練好的模型,來對從未遇見過的數據進行預測。
with fluid.scope_guard(inference_scope): [inference_program, feed_target_names, fetch_targets] = fluid.io.load_inference_model(params_dirname, infer_exe) # 載入預訓練模型 batch_size = 10 infer_reader = paddle.batch( paddle.dataset.uci_housing.test(), batch_size=batch_size) # 准備測試集 infer_data = next(infer_reader()) infer_feat = numpy.array( [data[0] for data in infer_data]).astype("float32") # 提取測試集中的數據 infer_label = numpy.array( [data[1] for data in infer_data]).astype("float32") # 提取測試集中的標簽 assert feed_target_names[0] == 'x' results = infer_exe.run(inference_program, feed={feed_target_names[0]: numpy.array(infer_feat)}, fetch_list=fetch_targets) # 進行預測 #打印預測結果和標簽並可視化結果 print("infer results: (House Price)") for idx, val in enumerate(results[0]): print("%d: %.2f" % (idx, val)) # 打印預測結果 print("\nground truth:") for idx, val in enumerate(infer_label): print("%d: %.2f" % (idx, val)) # 打印標簽值 save_result(results[0], infer_label) # 保存圖片
由於每次都是隨機選擇一個minibatch的數據作為當前迭代的訓練數據,所以每次得到的預測結果會有所不同。
#總結
在這章里,我們借助波士頓房價這一數據集,介紹了線性回歸模型的基本概念,以及如何使用PaddlePaddle實現訓練和測試的過程。很多的模型和技巧都是從簡單的線性回歸模型演化而來,因此弄清楚線性模型的原理和局限非常重要。
#參考文獻
- https://en.wikipedia.org/wiki/Linear_regression
- Friedman J, Hastie T, Tibshirani R. The elements of statistical learning[M]. Springer, Berlin: Springer series in statistics, 2001.
- Murphy K P. Machine learning: a probabilistic perspective[M]. MIT press, 2012.
- Bishop C M. Pattern recognition[J]. Machine Learning, 2006, 128.