机器学习-线性回归

Posted by 周宝航 on July 13, 2018

Linear Regression

  • B站学习,了解一哈。Up主搬运了Cousera上吴恩达老师的机器学习课程,而且是熟肉,真的是很贴心。
  • 有位世界AI大佬曾经说过:现如今的AI所做的都是拟合函数的工作,即针对问题涉及的数据建立一个可以较好拟合它们的模型。然后基于这个模型,我们便可以开展我们的业务,如:垃圾邮件分类、预测房价等等。
  • 经过这一章的学习,确实感觉到了上面大佬所说的意思。下面,我就自己学习中的体会来总结一哈线性回归模型。

Hypothesis

  • 所谓模型其实就是一个或一组数学表达式,用最简洁的形式来表述实际问题的解。
  • 上面这个方程是我们建立的线性回归模型。在开始部分,我们用只有两个参数的模型来进行讲解。

Parameters:

  • 一般在机器学习中,模型中的参数都用$\theta$来表示。而参数正是模型中的关键之一,所谓训练模型就是不断调节它们的过程。
  • 其实在后面的学习中,我们可以了解到:参数就是实际问题的特征(feature)的抽象。我们用参数映射不同的特征,而数据通过参数映射至模型空间。

Cost Function:

  • 成本函数是组成模型的另一关键,是用来衡量模型的预测值与真实值之间差距的手段。
  • 在机器学习任务中,我们往往需要找到一个成本函数,然后围绕它来进行函数优化。如此一来,复杂的实际问题就转化为了函数问题。
  • 可是,往往寻找一个成本函数并不是那么容易,而且还需要围绕它寻找一个适合于它的优化算法。不过,在前人的基础上,我们已经得到了许多有效的机器学习算法。我们正在学习的线性回归就是其中一种。
  • 上面的方程便是线性回归模型中的成本函数,表示预测值与实际值的差的平方和。这个函数的输出可以代表我们的线性回归模型对于训练数据的拟合程度,其值越小表示拟合程度越好。

Goal:

  • 目标(函数)是一个机器学习任务中的最终问题。我们在对实际问题建模后,得到了上面提到的成本函数——一种衡量模型拟合数据程度的手段。基于成本函数,我们便可以提出,在线性回归模型中,终极目标就是最小化成本函数。
  • 当我们通过一种算法不断调节参数$\theta$,最终使得在训练数据上成本函数的输出达到全局(局部)最小,就表示我们的模型训练完成。下面我们就要介绍两种训练模型的方法。

Gradient descent algorithm

  • 梯度下降算法。不只用于线性回归,还广泛应用于其他机器学习算法。
  • 其实理解起来很简单,就是由目标函数对各个参数$\theta$求偏导数。接着用原来的参数值减去对应梯度乘以一个$\alpha$(学习率 learning rate),然后用这个新值来更新参数。
  • 在视频讲解中,吴老师用一张轮廓图,类似于地理上的等高图来讲解的梯度下降算法。这种算法就像是下山,我们想从较高的地方快速地来到最低的地方,每次移动就要环视一周找到下降最快的那个方向(梯度),然后移动一步(梯度乘以学习率)。
  • 下面就是只有两个参数的梯度下降算法。

repeat until convergence {

$\theta_j:=\theta_j-\alpha\frac{\partial}{\partial \theta_j}J(\theta_0,\theta_1)$ (for j = 0 and j = 1)

}

Correct: Simultaneous update

  • 当然在更新参数时注意,我们是基于原来的成本函数(旧参数)计算出所有的新参数;而不是边更新边计算。

For Linear Regression

  • 这里我们将线性回归模型的成本函数代入梯度下降算法。经过一点推倒,得到两参数模型的梯度下降算法流程。
  • 看起来很简单,就是高数中的偏微分。
  • 更加细致的算法流程。

repeat until convergence { }

Multiple features

Size($feet^2$) Number of bedrooms Number of floors Age of home(years) Price($1000)
2104 5 1 45 460
1416 3 2 40 232
1534 3 2 30 315
852 2 1 36 178
  • 在实际问题中,两个特征并不能很好地解决我们的问题。我们往往面对的是成百上千的特征。这里我们可以看出,假设(hypothesis)函数中的参数$\theta$对应的就是特征。我们将实际问题中的特征抽象为模型中的参数。

  • 我们要将线性回归模型推广至多特征情况。

  • Notation:

    $n=$ number of features

    $x^{(i)}$ = input (features) of $i^{th}$ training example

    $x_j^{(i)}$ = value of feature $j$ in $i^{th}$ training example

  • matrix presentation
  • 上面便是我们将线性回归模型向量化后的成果。在现在的编程语言中,向量化后的计算更加快速、方便,比for循环不知道高到哪里去了。因此,在之后解决此类问题时,我们优先考虑向量化模型。

Gradient descent for multiple variables

  • 下面是我们将梯度下降算法推广至多特征情况。

New algorithm ($n\geq1$):

Repeat {

​ $\theta_j:=\theta_j-\alpha\frac{1}{m}\sum_{i=1}^m(h_\theta(x^{(i)})-y^{(i)})x_j^{(i)}$

​ (simultaneously update $\theta_j$ for $j=0,\dots,n$)

}

  • 推广到多特征情况

Gradient descent in practice

  • 在实际使用梯度下降算法时,我们会遇到很多问题导致算法的性能下降。而这种问题可以通过一些手段来避免。下面,介绍两种优化梯度下降算法的技巧。

Feature Scaling

  • 由于特征的数值范围可能相差很大, 梯度下降算法就需要更多次迭代才会收敛。
  • E.g. $x_1=$ size (0-2000 $feet^2$) $x_2=$ number of bedrooms (1-5)
  • 目的:使每个特征的范围规约到 $-1 \leq x_i \leq 1$

Mean normalization

  • 均值归一化
    1. 使用$x_i-\mu_i$替代$x_i$.
    2. 使用$\frac{x_i-\mu_i}{S_i}$代替$x_i$.
      • $\mu_i$代表特征$x_i$的均值
      • $S_i$代表x的范围,即 $max(x) - min(x)$.

E.g.

Plot cost function & No. of iterations

  • 通过画出成本函数与迭代次数的关系图,我们可以明确地看出梯度下降算法是否正常工作。
  • 基于该图,我们可以调整学习率$\alpha$来使得模型更好地工作。
  • 如果,结果图中函数曲线呈上升或起伏较大趋势,我们应该尝试减小学习率。
  • 学习率的选择对于训练过程的影响较大
    • $\alpha$太小,收敛的速度变慢
    • $\alpha$太大,成本值在每次迭代中可能不会降低;同样,可能不会收敛
    • 选择$\alpha$的经验,$\dots,0.003,0.03,0.3,1,\dots$

Normal Equation

  • 标准方程法。一种解析式求解问题的方法, 说白了就是解方程。
  • 万万没想到,这种方法以前做曲线拟合时用到过。现在一想,拟合曲线也是求解机器学习问题啊。下面,我们就这种方法聊一哈,具体推倒过程呐,以后再发吧。
  • 假设我们使用的是上面Multiple Features部分的表格数据集。我们可以这样构造矩阵:
  • 推广到$m$ examples $(x^{(1)}, y^{(1)}),\dots,(x^{(m)},y^{(m)})$;$n$ features.
  • 然后通过一个方程即可得到参数$\theta$使得成本函数最小。

Normal Equation V.S. Gradient Descent

Gradient Descent Normal Equation
需要选择$\alpha$ 不需要考虑$\alpha$
需要多次迭代 不需要迭代
$n\uparrow$,依然效率较高 $n\uparrow$,效率变低
  需要计算$(X^TX)^{-1}$
  • 总体来说,在n较大的时候,我们考虑使用梯度下降算法。而当n不是很大时,我们可以直接使用标准方程法直接求解参数。

Exercise

  • 必须手敲一哈,实现视频讲解里提到的细节。话不多说,直接上代码。

linear_regression.py

# -*- coding: utf-8 -*-

import numpy as np
import matplotlib.pyplot as plt
import logging

class LinearRegression(object):
    
    def __init__(self, num_iters=None, alpha=None, num_params=2):
        # iteration numbers
        self.num_iters = num_iters if num_iters else 1500
        # learning rate
        self.alpha = alpha if alpha else 0.01
        # parameters
        self.theta = np.zeros([num_params,1])
        # training datas
        self.data = None
        # logger
        self.logger = logging.getLogger()
        logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
        logging.root.setLevel(level=logging.INFO)
        
    def read_data(self, file_path=None):
        if file_path:
            self.logger.info("reading the data from %s" % file_path)
            self.data = np.genfromtxt(file_path, delimiter=',', dtype=None)
            
    def save(self, path=None):
        if path:
            import pickle
            with open(path, "rb") as f:
                pickle.dump(self.theta, f)
                
    def load(self, path=None):
        if path:
            import pickle
            with open(path, "rb") as f:
                self.theta = pickle.load(f)

    def computeCost(self, X, y, theta):
        m = len(y)
        J = 0
        J = np.sum((X.dot(theta) - y) ** 2) / (2 * m)
        return J
    
    def gradientDescent(self, X, y):
        m = len(y)
        for i in range(self.num_iters):
            self.theta = self.theta - self.alpha / m * X.T.dot(X.dot(self.theta) - y)
            J = self.computeCost(X, y, self.theta)
            yield J
    
    def train_model(self, file_path=None):
        self.read_data(file_path)
        self.logger.info("getting the feature values")
        x = self.data[:,0].reshape([-1, 1])
        self.logger.info("getting the object values")
        y = self.data[:,1].reshape([-1, 1])
        # generate the feature matrix
        X = np.c_[np.ones([len(x), 1]), x]
        self.logger.info("start gradient descent")
        fig = plt.figure()
        ax_model = fig.add_subplot(1,2,1)
        ax_model.scatter(x, y)
        ax_loss = fig.add_subplot(1,2,2)
        J_history = []
        for J in self.gradientDescent(X, y):
            J_history.append(J)
        
            if len(ax_model.lines) > 0:
                    ax_model.lines.pop()
            ax_model.set_title('Linear regression')
            ax_model.plot(x, X.dot(self.theta), color='r')
            
            ax_loss.set_title('Loss')
            ax_loss.set_xlabel('Iteration')
            ax_loss.set_ylabel('Loss')
            ax_loss.set_xlim(0,self.num_iters)
            ax_loss.plot(J_history)
            plt.pause(0.001)
        plt.show()
        self.logger.info("end")

train_model.py

# -*- coding: utf-8 -*-
"""
Created on Fri Jul 13 11:14:40 2018

@author: 周宝航
"""

import logging
import os.path
import sys
import argparse
from linear_regression import LinearRegression

if __name__ == '__main__':
    program = os.path.basename(sys.argv[0])
    
    parser = argparse.ArgumentParser(prog=program, description = 'train the model by linear regression')
    parser.add_argument("--in_path", "-i", required=True, help="train data path")
    parser.add_argument("--out_path", "-o", help="output model path, file type is : *.pkl")
    parser.add_argument("--num_iters", "-n", type=int,help="iteration times")
    parser.add_argument("--alpha", "-a", type=float, help="learning rate")
    args = parser.parse_args()
    
    logger = logging.getLogger(program)
    logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
    logging.root.setLevel(level=logging.INFO)
    logger.info("running %s" % ' '.join(sys.argv))
    
    lr_model = LinearRegression(num_iters=args.num_iters, alpha=args.alpha)
    logger.info("start training")
    lr_model.train_model(args.in_path)   

    if args.out_path:
        if args.out_path.split('.')[-1] == "pkl":
            lr_model.save(args.out_path)
        else:
            print("model file type error. Please use *.pkl to name your model.")
            sys.exit(1)
  • 具体使用方法,cmd中输入 python train_model.py -i data\ex1data1.txt 即可。当然还有其他输入参数可以使用。
  • 最后,放一张实验效果gif。其中,左侧为模型的动态变化过程,右侧为损失函数输出随迭代次数的变化过程。网上生成工具限制gif的长度,所以只有60s的过程。

Alt text