【120】TensorFlow 從CSV檔案中讀取資料並訓練線性迴歸模型(面向新手)
正文開始。
學習 TensorFlow 讓我的思維發生了變化。
計算機本質上是一種數學的工具,而我在學習程式設計的時候,思維也不可避免地收到了影響。傳統的程式設計思想,常常認為程式就應該像數學定理或者數學函式一樣,給出一個確定的結果。這是一種基於邏輯推導的思維習慣。
然而,做實驗的科學家們的思維卻不像數學家一樣。實驗科學家通過做實驗收集資料,再根據資料推測其中蘊含的某種規律。
這篇文章以胡克定律為例子,向新手介紹如何使用TF。胡克定律是由物理學家胡克發現。簡單地說,這個定律指彈簧長度的增量和彈簧受力大小呈正比關係。(注:這並不是胡克定律的嚴格定義,只是為了方便讀者理解而作的簡化說明。這個說法更像中學物理對胡克定律的說明。)
現在假設你是一個實驗物理學家,要探索彈簧長度增量和受力大小之間的關係。你對一個彈簧施加了不同的力,並記錄下了彈簧的增量(資料都是我瞎編的),儲存到一個CSV檔案中。檔名是 HookeLaw.csv
HookeLaw.csv檔案內容
"force","length_variation"
8.0,3.9
4.3,2.2
5.7,3
7.2,4.1
10,5.2
21,9.0
14.5,7.1
11.3,5.8
12.3,6.1
21.3,10.5
18.2,9.8
21.3,10.8
21.5,10
19.1,9.8
32.3,15.9
28.3,14.0
33.3,16.2
30.3,15.5
31.7 ,17.1
29.9,14.6
30.5,14.9
34.5,17
9.9,10
35.5,18
29.9,15.0
這個樣是看不出什麼有價值的資訊。你需要畫出一個圖表來進行判斷。如果是在胡克那個時代,只能用手畫出圖表。現在只需用Python就可以了。我把 HookeLaw.csv 檔案放到了 nginx 伺服器上,方便 Python 讀取。
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# 從CSV檔案中讀取資料,並返回2個數組。分別是自變數x和因變數y。方便TF計算模型。
def zc_read_csv():
zc_dataframe = pd.read_csv("http://yoursize.com/HookeLaw.csv", sep=",")
x = []
y = []
for zc_index in zc_dataframe.index:
zc_row = zc_dataframe.loc[zc_index]
x.append(zc_row["force"])
y.append(zc_row["length_variation"])
return (x,y)
x, y = zc_read_csv()
# 獲得畫圖物件。
fig = plt.figure()
fig.set_size_inches(10, 4) # 整個繪圖區域的寬度10和高度4
ax = fig.add_subplot(1, 2, 1) # 整個繪圖區分成一行兩列,當前圖是第一個。
# 畫出原始資料的散點圖。
ax.set_title("Hooke's Law")
ax.set_xlabel("force")
ax.set_ylabel("length_variation")
ax.scatter(x, y)
plt.show()
程式執行結果:
排除掉明顯的誤差資料,散點圖看上去是一條直線。設彈簧長度增量是 y,受力是x,有理由推測 y = kx + b。在沒有計算機的時代,需要手動畫一條直線,並保持直線和各個點之間的距離最小,最後計算直線的 k 和 b。
因為有了計算機,我們可以用計算機來完成這些繁瑣的工作。利用線性迴歸,讓程式一點一點找到合適的b值和k值。在機器學習裡面b和k叫做權重,用 w0 和 w1 表示。公式可以寫成 y = w0 + w1x 。
下面的程式中,zc_x4tf 相當於是一個矩陣,在TF會話中是input:
zc_y4tf 相當於是一個列向量,在TF會話中是weights:
yhat = tf.matmul(input, weights) 可以理解成矩陣乘法,結果是預測值。
L2損失用於衡量每個實際值和預測值之間的偏差。對應概率論中的方差。
下面的程式給出了一個完整的實現。
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# 從CSV檔案中讀取資料,並返回2個數組。分別是自變數x和因變數y。方便TF計算模型。
def zc_read_csv():
zc_dataframe = pd.read_csv("http://yoursize.com/HookeLaw.csv", sep=",")
x = []
y = []
for zc_index in zc_dataframe.index:
zc_row = zc_dataframe.loc[zc_index]
x.append(zc_row["force"])
y.append(zc_row["length_variation"])
return (x,y)
x, y = zc_read_csv()
zc_x = []
for item in x:
zc_x.append([1., item])
zc_x4tf = np.array(zc_x).astype(np.float32)
zc_y = []
for item in y:
zc_y.append([item])
zc_y4tf = np.array(zc_y).astype(np.float32)
# 存放 L2 損失的陣列
loss_arr = []
# 訓練的步數。即訓練的迭代次數。
training_steps = 55
# 在梯度下降演算法中,控制梯度步長的大小。
learning_rate = 0.01
# 開啟TF會話,在TF 會話的上下文中進行 TF 的操作。
with tf.Session() as sess:
# 設定 tf 張量(tensor)。注意:TF會話中的註釋裡面提到的常量和變數是針對TF設定而言,不是python語法。
# 因為在TF運算過程中,x作為特徵值,y作為標籤
# 是不會改變的,所以分別設定成input 和 target 兩個常量。
# 這是 x 取值的張量。設一共有m條資料,可以把input理解成是一個m行2列的矩陣。矩陣第一列都是1,第二列是x取值。
input = tf.constant(zc_x4tf)
# 設定 y 取值的張量。target可以被理解成是一個m行1列的矩陣。 有些文章稱target為標籤。
target = tf.constant(zc_y4tf)
# 設定權重變數。因為在每次訓練中,都要改變權重,來尋找L2損失最小的權重,所以權重是變數。
# 可以把權重理解成一個2行1列的矩陣。初始值是隨機的。[2,1] 表示2行1列。
weights = tf.Variable(tf.random_normal([2, 1], 0, 0.1))
# 初始化上面所有的 TF 常量和變數。
tf.global_variables_initializer().run()
# input 作為特徵值和權重做矩陣乘法。m行2列矩陣乘以2行1列矩陣,得到m行1列矩陣。
# yhat是新矩陣,yhat中的每個數 yhat' = w0 * 1 + w1 * x。
# 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(training_steps):
# 重複執行梯度下降演算法,更新權重數值,找到最合適的權重數值。
sess.run(zc_optimizer)
# print(weights.eval())
# 每次迴圈都記錄下損失loss的值,病放到陣列loss_arr中。
loss_arr.append(loss.eval())
zc_weight_arr = weights.eval()
zc_yhat = yhat.eval()
print("weights", zc_weight_arr)
# 畫出原始資料的散點圖和數學模型的直線圖。
def paint_module(fig):
ax = fig.add_subplot(1, 2, 1) # 整個繪圖區分成一行兩列,當前圖是第一個。
# 畫出原始資料的散點圖。
ax.set_title("Hooke's Law")
ax.set_xlabel("force")
ax.set_ylabel("length_variation")
ax.scatter(x, y)
# 畫出預測值的散點圖。
p_yhat = [a[0] for a in zc_yhat]
ax.scatter(x, p_yhat, c="red", alpha=.6)
# 畫出線性迴歸計算出的直線模型。
line_x_arr = [1, 40]
line_y_arr = []
for item in line_x_arr:
line_y_arr.append(zc_weight_arr[0] + zc_weight_arr[1] * item)
ax.plot(line_x_arr, line_y_arr, "g", alpha=0.6)
# 畫出訓練過程中的損失變化
def paint_loss(fig):
print("loss", loss_arr)
ax = fig.add_subplot(1, 2, 2) # 整個繪圖區分成一行兩列,當前圖是第二個。
ax.plot(range(0, training_steps), loss_arr)
# 獲得畫圖物件。
fig = plt.figure()
fig.set_size_inches(10, 4) # 整個繪圖區域的寬度10和高度4
paint_module(fig)
paint_loss(fig)
plt.show()
有結果可知 y = 0.5x + 0.2 變形得 x = 2y - 0.4
而胡克定律公式是 F = kL (F是力,k是彈性係數,L是彈簧長度增量)。可以把常數項 -0.4 看作系統誤差,則彈簧彈性係數是2 。