TF入门03-实现线性回归&逻辑回归

作者: 七八音 | 来源:发表于2020-05-26 11:45 被阅读0次

    之前,我们介绍了TF的运算图、会话以及基本的ops,本文使用前面介绍的东西实现两个简单的算法,分别是线性回归和逻辑回归。本文的内容安排如下:

    1. 实现线性回归
    2. 算法优化
    3. 实现逻辑回归

    1. 线性回归

    1.1 问题描述

    image-20200525215526204

    我们将收集到的不同国家的出生率以及平均寿命。通过上图可以发现出生率越高的国家,人口的平均寿命大概率上会越低。

    现在,我们想使用线性回归来对这种现象进行描述,之后给定一个国家的出生率后可以来预测其人口的平均寿命。

    数据描述如下:

    image

    自变量X为出生率,数据类型为float,因变量Y为平均寿命,类型为float;数据集一共有190个数据点。

    模型构建:我们使用一种简单的算法-线性回归来描述这个模型,Y = 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。

    线性回归模型的计算图如下所示:

    image

    我们将训练后的参数使用折线图进行可视化:

    image

    可以看到,模型能对数据走向进行模拟,虽然模拟效果不够精准,毕竟我们的模型非常简单。

    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,两种损失的训练结果如下:

    [图片上传失败...(image-fd4f36-1590464984647)]

    可以看到采用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。

    image

    从上图我们可以知道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()
    

    模型的运算图如下:

    image.png

    3. References

    CS 20: Tensorflow for Deep Learning Research

    欢迎关注公众号,一起学习

    相关文章

      网友评论

        本文标题:TF入门03-实现线性回归&逻辑回归

        本文链接:https://www.haomeiwen.com/subject/qypmahtx.html