之前,我們介紹了TF的運算圖、會話以及基本的ops,本文使用前面介紹的東西實現兩個簡單的算法,分別是線性迴歸和邏輯迴歸。本文的內容安排如下:
- 實現線性迴歸
- 算法優化
- 實現邏輯迴歸
1. 線性迴歸
1.1 問題描述
我們將收集到的不同國家的出生率以及平均壽命。通過上圖可以發現出生率越高的國家,人口的平均壽命大概率上會越低。
現在,我們想使用線性迴歸來對這種現象進行描述,之後給定一個國家的出生率後可以來預測其人口的平均壽命。
數據描述如下:
自變量X爲出生率,數據類型爲float,因變量Y爲平均壽命,類型爲float;數據集一共有190個數據點。
模型構建:我們使用一種簡單的算法-線性迴歸來描述這個模型,, 其中,w,b均爲實數。
1.2 方法實現
我們之前知道TF將計算圖的定義與運行分離開來,模型實現時主要分爲兩個階段:
- 定義運算圖
- 使用會話執行運算圖,得到計算結果
我們先來進行運算圖的定義,這一部分主要是根據公式將模型在graph中定義。
Step1:讀取數據
# read_birth_lift_data:讀取txt文件數據
data, n_samples = utils.read_birth_life_data(DATA_FILE)
Step2:創建佔位符,用於加載數據和標籤
#tf.placeholder(dtype, shape=None, name=None)
X = tf.placeholder(tf.float32, name='X')
Y = tf.placeholder(tf.float32, name='Y')
Step3:創建權重參數weight和bias
w = tf.get_variable('weights', initializer=tf.constant(0.0))
b = tf.get_variable('bias', initializer=tf.constant(0.0))
Step 4: 構建模型預測Y
Y_predicted = w * X + b
Step 5:定義損失函數
loss = tf.square(Y_predicted - Y, name='loss')
Step 6: 創建優化器
opt = tf.train.GradientDescentOptimizer(learning_rate=0.001)
optimizer = opt.minimize(loss)
第二階段:運行計算圖
這個階段又可以分爲:
- 變量初始化
- 運行優化器,同時使用feed_dict傳遞數據
完整代碼如下:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import time
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import utils
DATA_FILE = 'data/birth_life_2010.txt'
# Step 1: read in data from the .txt file
data, n_samples = utils.read_birth_life_data(DATA_FILE)
print(type(data))
# Step 2: create placeholders for X (birth rate) and Y (life expectancy)
X = tf.placeholder(tf.float32, name='X')
Y = tf.placeholder(tf.float32, name='Y')
# Step 3: create weight and bias, initialized to 0.0
# Make sure to use tf.get_variable
w = tf.get_variable('weights', initializer=tf.constant(0.0))
b = tf.get_variable('bias', initializer=tf.constant(0.0))
# Step 4: build model to predict Y
Y_predicted = w * X + b
# Step 5: use the square error as the loss function
loss = tf.square(Y_predicted - Y, name='loss')
# Step 6: using gradient descent with learning rate of 0.001 to minimize loss
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss)
start = time.time()
# Create a filewriter to write the model's graph to TensorBoard
writer = tf.summary.FileWriter('./graphs/linreg', tf.get_default_graph())
with tf.Session() as sess:
# Step 7: initialize the necessary variables, in this case, w and b
sess.run(tf.global_variables_initializer())
# Step 8: train the model for 100 epochs
for i in range(100):
total_loss = 0
for x, y in data:
# Execute train_op and get the value of loss.
# Don't forget to feed in data for placeholders
_, loss_ = sess.run([optimizer, loss], feed_dict={X:x, Y:y})
total_loss += loss_
print('Epoch {0}: {1}'.format(i, total_loss/n_samples))
# close the writer when you're done using it
writer.close()
# Step 9: output the values of w and b
w_out, b_out = sess.run([w, b])
print(w_out, b_out)
print('Took: %f seconds' %(time.time() - start))
值得注意的一點是,上述代碼有一行爲:
_, loss_ = sess.run([optimizer, loss], feed_dict={X:x, Y:y})
這行用於將數據通過feed_dict傳送到placeholder中,我們想要運行的是optimizer和loss;其中一個返回值沒用,因此我們用下劃線代替,第二個爲loss_,要注意返回值和run的fetches裏的名字不能相同,否則報錯TypeError: Fetch argument 841.0 has invalid type <class 'numpy.float32'>, must be a string or Tensor.
原因在於,fetches裏的對象爲tensor,返回值爲numpy數組,因此下次循環的時候會報類型錯誤TypeError。
線性迴歸模型的計算圖如下所示:
我們將訓練後的參數使用折線圖進行可視化:
可以看到,模型能對數據走向進行模擬,雖然模擬效果不夠精準,畢竟我們的模型非常簡單。
1.3 分析優化
損失函數
通過觀察上面的算法模擬圖,我們可以看到數據中存在一些離羣點,如左下方的幾個點,這些點會將擬合線向它們這邊**“拉”**,進而導致模型準確度下降。因爲我們使用的損失函數爲平方損失,這樣那些模擬特別差的點對損失函數的貢獻會進一步加大,我們需要想辦法降低離羣點對損失函數的“貢獻”,降低其佔得權重,因此,這裏使用Huber損失:如果預測值與標籤值之間的差距很小,損失值爲平方差;如果差距過大,採用絕對差。Huber損失對離羣點不敏感,更魯棒。
接下來的問題是我們如何使用TF實現這個分段函數呢?答案是使用TF的控制流ops:
tf.cond(condition, fn1, fn2, name=None)
這個函數類似於C++中的三元運算符z = cond ? x : y
,含義爲如果condition條件爲真,執行函數fn1,否則執行函數fn2。huber實現代碼如下:
def huber_loss(labels, preds, delta=14.0):
residual = tf.abs(labels - preds)
def f1(): return 0.5 * tf.square(residual)
def f2(): return delta * residual - 0.5 * tf.square(delta)
return tf.cond(residual < delta, f1, f2)
使用huber_loss重新訓練後,計算權重爲w: -5.883589, b: 85.124306,兩種損失的訓練結果如下:
可以看到採用huber的曲線將平方差曲線又“拉回去了”,減少離羣點對模型的影響。
數據輸入tf.data
之前的視線中,我們使用tf.placeholder結合feed_dict來實現數據的輸入,這種方法的優點在於將數據的處理過程和TF分離開來,可以在Python中實現數據的處理;缺點在於用戶通常用單線程實現這個處理過程,產生數據瓶頸進而拖慢程序的運行效率。
TensorFlow爲數據處理提供了一種數據結構:隊列。這種方式允許你進行數據流水線、多線程並行化進而能加快數據加載到placeholder的速度。但是,隊列難以使用而且容易崩潰。
TensorFlow還提供了另一種方式爲tf.data
,它比placeholders方式速度快,比隊列方式更簡單,還不容易崩潰。那麼tf.data
模塊應該怎麼使用呢?讓我們接着往下看。
在之前的線性迴歸中,我們的輸入數據存儲在numpy數組data中,其中每一行爲一個(x,y)pair對,對應圖中的一個數據點。爲了將data導入到TensorFlow模型中,我們分別爲x(特徵)和y(標籤)創建placeholder,之後再Step8中迭代數據集並使用feed_dict將數據feed到placeholders中。當然,我們也可以使用一個批量的數據來進行更新,但是這個過程的關鍵點在於將numpy形式數據傳送到TensorFlow模型中這個過程是比較緩慢的,限制了其他ops的執行速度。
使用tf.data
存儲數據,保存對象是一個tf.data.Dataset
對象,而不是非TensorFlow對象。我們可以從tensors創建一個Dataset對象:
tf.data.Dataset.from_tensor_slices((features, labels))
其中,features
和labels
應該是tensors形式,但由於TF能無縫集成Numpy,因此它們也可以是Numpy數組。這裏的初始化爲:
dataset = tf.data.Dataset.from_tensor_slices((data[:,0], data[:,1]))
此外,你還可以使用不同的文件格式解析器從不同格式的文件中創建tf.data.Dataset
對象:
tf.data.TextLineDataset(filenames)
:文件中的每一行被解析爲一個數據。這種方式適用於被換行符分割的數據,如機器翻譯的數據以及csv格式數據tf.data.FixedLengthRecordDataset(filenames)
:文件中每個數據點的長度相同。適用於每個數據點長度相同的數據集,如CIFAR、ImageNet數據集tf.data.TFRecord(filenames)
:適用於以tfrecord格式存儲的數據
dataset = tf.data.FixedLengthRecordDataset([file1, file2,...])
將數據轉換成TF Dataset對象後,我們可以用一個迭代器iterator對數據集進行遍歷。每次調用get_next()
函數,迭代器迭代Dataset對象,並返回一個樣本或者一個批量的樣本數據。我們先介紹make_one_shot_iterator()
,
iterator = dataset.make_one_shot_iterator()
X, Y = iterator.get_next() # X:出生率,Y:平均壽命
每次執行ops X,Y時,我們可以得到一個新的數據點:
with tf.Session() as sess:
print(sess.run([X, Y])) # >> [1.822, 74.82825]
print(sess.run([X, Y])) # >> [3.869, 70.81949]
print(sess.run([X, Y])) # >> [3.911, 72.15066]
之後Y_predicted的計算和使用placeholder方式相同,不同之處在於執行運算圖時,不用再使用feed_dict傳送數據。
for i in range(100): # 100 epochs
total_loss = 0
try:
while True:
sess.run([optimizer])
except tf.errors.OutOfRangeError:
pass
因爲TF不會自動捕獲OutOfRangeError
,因此我們需要顯式地進行處理。運行上述代碼後,可以看到total_loss在第一個epoch中可以得到一個非零值,之後的epoch,total_loss一直爲0。原因在於dataset.make_one_shot_iterator()
,這種方式顧名思義只能用於一次數據迭代過程,而且這種方式不用自己初始化.在數據集的第一次迭代完成之後,下一個epoch時,iterator沒有重新初始化,所以total_loss一直爲0.
如果要完成多次epochs的訓練,可以使用dataset.make_initializable_iterator()
.每次epoch之初,你必須重新初始化迭代器iterator。
iterator = dataset.make_initializable_iterator()
...
for i in range(100):
sess.run(iterator.initializer)
total_loss = 0
try:
while True:
sess.run(optimizer)
except tf.errors.OutOfRangeError:
pass
使用tf.data.Dataset
對象,你可以使用一行代碼完成數據的batch、shuffle和repeat,也可以將數據集中的每個對象進行轉換進而創建一個新的數據集。
dataset = dataset.shuffle(1000) # 數據順序打亂
dataset = dataset.repeat(100) # 用於多個epoch的數據遍歷
dataset = dataset.batch(128) # 將數據劃分爲batch,每個batch大小爲128
# 將每個元素轉換爲one_hot向量
dataset = dataset.map(lambda x: tf.one_hot(x, 10))
tf.data方式比placeholder表現更好嗎?
爲了比較兩種方式的優劣,我們將模型運行100次,計算運行時間的平均值來比較。在2.7GHz Intel Core i5 Macbook上,placeholder模型運行平均時間爲9.05271519s,tf.data模型爲6.12285947,性能提升32.4%。
優化器
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss)
sess.run([optimizer])
爲什麼op optimizer在運行的fetches中呢?TF如何確定需要更新的參數呢?
optimizer是一個用於最小化loss的op,爲了執行這個op,我們需要把它放到sess.run()的fetches列表裏。當TF執行optimizer op時,TF會執行與這個op依賴的部分子運算圖。在這裏,optimizer依賴於loss,而loss又依賴於輸入X、Y以及權重參數weights和bias。
從上圖我們可以知道GradientDescentOptimizer結點依賴於3個結點:weights、bias和gradients(梯度,TF可以自動計算)。
GradientDescentOptimizer是指我們的更新爲梯度下降。TF可以爲我們計算梯度,然後使用梯度值來進行weight和biase的更新,進而來最小化loss。
默認情況下,optimizer
可以對loss函數依賴的所有可訓練的變量。如果你不想訓練某個變量,你可以將其設置爲trainable=False
。比如對於訓練次數global_step
這個變量不需要訓練:
global_step = tf.Variable(0, trainable=False, dtype=tf.float32)
learning_rate = 0.01 * 0.99 ** tf.cast(global_step, tf.float32)
increment_step = global_step.assign_add(1)
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
你也可以將optimizer設定爲只對某些變量計算梯度,
# create an optimizer.
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.1)
# compute the gradients for a list of variables.
grads_and_vars = optimizer.compute_gradients(loss,<list of variables>)
# grads_and_vars is a list of tuples (gradient, variable). Do whatever you
# need to the 'gradient' part, for example, subtract each of them by 1.
subtracted_grads_and_vars = [(gv[0] - 1.0, gv[1]) for gv in grads_and_vars]
# ask the optimizer to apply the subtracted gradients.
optimizer.apply_gradients(subtracted_grads_and_vars)
你也可以阻止特定tensor對loss函數的梯度計算的貢獻:
stop_gradient(input, name=None)
對於某些變量在訓練過程中需要被凍結的情況非常有用。比如:
- GAN的訓練:在對抗樣本的生成過程中沒有BP
- EM算法:M階段不應該設計E階段的輸出進行反向傳播過程。
optimizer自動計算運算圖中的導數,此外,你也可以使TF來計算特定變量的梯度。
tf.gradients(
ys,
xs,
grad_ys=None,
name='gradients',
colocate_gradients_with_ops=False,
gate_gradients=False,
aggregation_method=None,
stop_gradients=None
)
這個方法計算ys相對於每個x的偏導數的和,其中ys、xs分別是一個tensor或一組tensor,grad_ys是一組tensor,內部值爲ys計算的梯度結果,長度和ys一致。
使用上述優化方法後的代碼爲:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import time
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import utils
DATA_FILE = 'data/birth_life_2010.txt'
# Step 1: read in the data
data, n_samples = utils.read_birth_life_data(DATA_FILE)
# Step 2: create Dataset and iterator
dataset = tf.data.Dataset.from_tensor_slices((data[:,0], data[:,1]))
# iterator = dataset.make_initializable_iterator()
iterator = dataset.make_initializable_iterator()
X, Y = iterator.get_next()
# Step 3: create weight and bias, initialized to 0
w = tf.get_variable('weights', initializer=tf.constant(0.0))
b = tf.get_variable('bias', initializer=tf.constant(0.0))
# Step 4: build model to predict Y
Y_predicted = X * w + b
# Step 5: use the square error as the loss function
#loss = tf.square(Y - Y_predicted, name='loss')
loss = utils.huber_loss(Y, Y_predicted)
# Step 6: using gradient descent with learning rate of 0.001 to minimize loss
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss)
start = time.time()
with tf.Session() as sess:
# Step 7: initialize the necessary variables, in this case, w and b
sess.run(tf.global_variables_initializer())
writer = tf.summary.FileWriter('./graphs/linear_reg', sess.graph)
# Step 8: train the model for 100 epochs
for i in range(100):
sess.run(iterator.initializer) # initialize the iterator
total_loss = 0
try:
while True:
_, l = sess.run([optimizer, loss])
total_loss += l
except tf.errors.OutOfRangeError:
pass
print('Epoch {0}: {1}'.format(i, total_loss/n_samples))
# close the writer when you're done using it
writer.close()
# Step 9: output the values of w and b
w_out, b_out = sess.run([w, b])
print('w: %f, b: %f' %(w_out, b_out))
print('Took: %f seconds' %(time.time() - start))
2. 邏輯迴歸
2.1 問題描述
手寫數字識別:使用邏輯迴歸模型來處理MNIST數字分類問題。
MNIST是一個手寫數字的數據集。
每張圖片爲28*28。我們這裏將它flatten成784的一維向量,數據標籤爲0-9表示數字0-9。數據集分爲訓練集、測試集和驗證集,其中訓練集爲55000張圖片,測試集爲10000張圖片,驗證集爲5000張圖片。
2.2 方法實現
實現過程和線性迴歸類似。
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import numpy as np
import tensorflow as tf
import time
import utils
# Define paramaters for the model
learning_rate = 0.01
batch_size = 128
n_epochs = 30
n_train = 60000
n_test = 10000
# Step 1: 讀取數據集
mnist_folder = 'data/mnist'
utils.download_mnist(mnist_folder)
train, val, test = utils.read_mnist(mnist_folder, flatten=True)
# Step 2: 創建dataset以及iterator迭代器
train_data = tf.data.Dataset.from_tensor_slices(train)
train_data = train_data.shuffle(10000) # 數據打亂
train_data = train_data.batch(batch_size)# 劃分批量
# 測試集
test_data = tf.data.Dataset.from_tensor_slices(test)
test_data = test_data.batch(batch_size)
# 創建iterator,用於對不同的dataset對象進行迭代
iterator = tf.data.Iterator.from_structure(train_data.output_types, train_data.output_shapes)
img, label = iterator.get_next()
# 迭代器的初始化:分別對訓練集和測試集進行遍歷,不同的初始化
train_init = iterator.make_initializer(train_data)
test_init = iterator.make_initializer(test_data)
# Step 3: 創建變量w和b
w = tf.get_variable('weights', initializer=tf.random_normal(shape=(784, 10),mean=0,stddev=0.1))
b = tf.get_variable('bias', initializer=tf.zeros((1, 10)))
# Step 4: 模型構建
# 沒有計算softmax,將softmax移動到loss的計算過程中
logits = tf.matmul(img, w) + b
# Step 5: loss定義-多分類的交叉熵
loss = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=label, name='loss')
loss = tf.reduce_mean(loss)
# Step 6: 定義優化器
optimizer = tf.train.AdamOptimizer(0.1).minimize(loss)
# Step 7: 計算準確率
preds = tf.nn.softmax(logits)
correct_preds = tf.equal(tf.argmax(preds, 1), tf.argmax(label, 1))
accuracy = tf.reduce_sum(tf.cast(correct_preds, tf.float32))
writer = tf.summary.FileWriter('./graphs/logreg', tf.get_default_graph())
with tf.Session() as sess:
start_time = time.time()
sess.run(tf.global_variables_initializer())
# 模型訓練
for i in range(n_epochs):
sess.run(train_init)# 訓練集iterator初始化
total_loss = 0
n_batches = 0
try:
while True:
_, l = sess.run([optimizer, loss])
total_loss += l
n_batches += 1
except tf.errors.OutOfRangeError:
pass
print('Average loss epoch {0}: {1}'.format(i, total_loss/n_batches))
print('Total time: {0} seconds'.format(time.time() - start_time))
# 測試集
sess.run(test_init) # 測試集iterator初始化,讀取測試集
total_correct_preds = 0
try:
while True:
accuracy_batch = sess.run(accuracy)
total_correct_preds += accuracy_batch
except tf.errors.OutOfRangeError:
pass
print('Accuracy {0}'.format(total_correct_preds/n_test))
writer.close()
模型的運算圖如下: