TF入門03-實現線性迴歸&邏輯迴歸

之前,我們介紹了TF的運算圖、會話以及基本的ops,本文使用前面介紹的東西實現兩個簡單的算法,分別是線性迴歸和邏輯迴歸。本文的內容安排如下:

  1. 實現線性迴歸
  2. 算法優化
  3. 實現邏輯迴歸

1. 線性迴歸

1.1 問題描述

image-20200525215526204

我們將收集到的不同國家的出生率以及平均壽命。通過上圖可以發現出生率越高的國家,人口的平均壽命大概率上會越低。

現在,我們想使用線性迴歸來對這種現象進行描述,之後給定一個國家的出生率後可以來預測其人口的平均壽命。

數據描述如下:

自變量X爲出生率,數據類型爲float,因變量Y爲平均壽命,類型爲float;數據集一共有190個數據點。

模型構建:我們使用一種簡單的算法-線性迴歸來描述這個模型,Y=wX+bY = wX + b, 其中,w,b均爲實數。

1.2 方法實現

我們之前知道TF將計算圖的定義與運行分離開來,模型實現時主要分爲兩個階段:

  1. 定義運算圖
  2. 使用會話執行運算圖,得到計算結果

我們先來進行運算圖的定義,這一部分主要是根據公式將模型在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)

第二階段:運行計算圖

這個階段又可以分爲:

  1. 變量初始化
  2. 運行優化器,同時使用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損失對離羣點不敏感,更魯棒。

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

其中,featureslabels應該是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一致。

TF中其他optimizers

使用上述優化方法後的代碼爲:

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是一個手寫數字的數據集。

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

模型的運算圖如下:

LogisticRegression

3. References

CS 20: Tensorflow for Deep Learning Research

歡迎關注公衆號,一起學習

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