机器学习系列 【一】逻辑回归算法

内容说明

      本文主要讲解Logistic算法的数学原理、优化方法以及python代码实现,可供机器学习爱好者借鉴学习。文中所给出的python代码,来自于2020年华为软件精英挑战赛官方给出的LR算法源码,读者也可借助本文快速读懂大赛示例代码。

一、Logistic回归算法基本原理

      Logistic 回归是二分类任务中最常用的机器学习算法之一。它的设计思路简单,易于实现,可以用作性能基准,且在很多任务中都表现很好。其优点是计算代价不高,易于理解和实现;其缺点是容易欠拟合,分类精度可能不高。
在逻辑回归算法中,利用了Logistic函数(或称Signoid函数),在该函数中自变量的取值范围为(-INF,INF),函数值域为(0,1)。其表达式为:
Alt
由于sigmoid函数的定义域是(-INF, +INF),而值域为(0, 1)。因此最基本的LR分类器适合于对两分类(类0,类1)目标进行分类。Sigmoid 函数是个很漂亮的“S”形,如下图所示:
在这里插入图片描述      LR分类器(Logistic Regression Classifier)目的就是从训练数据特征学习出一个0/1分类模型–这个模型以样本特征的线性组合 ∑ i = 1 n θ i x i \sum_{i=1}^n{\theta_ix_i} i=1nθixi作为自变量,使用logistic函数将自变量映射到(0,1)上。因此LR分类器的求解就是求解一组权值 θ 1 , θ 2 , θ 3 , θ 4 . . . θ n \theta_1,\theta_2,\theta_3,\theta_4...\theta_n θ1,θ2,θ3,θ4...θn(其中 θ 0 \theta_0 θ0是名义变量–dummy,为常数,实际工程中常另 x 0 = 1.0 x_0=1.0 x0=1.0。不管常数项有没有意义,最好保留),并代入Logistic函数构造出一个预测函数:
在这里插入图片描述
      函数的值表示结果为1的概率,就是特征属于y=1的概率。因此对于输入x分类结果为类别1和类别0的概率分别为:
在这里插入图片描述
      当我们要判别一个新来的特征属于哪个类时,按照下式求出一个z值:
在这里插入图片描述
      式中, x 1 , x 2 , x 3 . . . . , x n x_1,x_2,x_3....,x_n x1,x2,x3....,xn为样本数据在各个维度的特征,n是维数。
      将上式结果带入Signoid函数中,即得到 g ( θ T x ) g(\theta^Tx) g(θTx)。若该值大于0.5,则认为样本x属于 y = 1 y=1 y=1的类,反之则属于 y = 0 y=0 y=0的类,这就是逻辑回归算法的分类依据。(注意,这里假设统计样本是均匀分布的,所以设阈值为0.5)。
      对逻辑回归算法的学习,核心就在求解权值 θ 1 , θ 2 , θ 3 , θ 4 . . . θ n \theta_1,\theta_2,\theta_3,\theta_4...\theta_n θ1,θ2,θ3,θ4...θn上。具体求解,涉及到了极大似然估计E和优化算法的概念,数学中最优化算法常用的就是梯度上升(下降)算法。下文将详细介绍权值求解过程。

二、Logistic回归数学推导

(1)梯度下降法求解

      假设有n个观测样本,观测值分别为 y 1 , y 2 . . . , y n y_1,y_2...,y_n y1,y2...,yn p i = p ( y i = 1 ∣ x i ) p_i=p(y_i=1|x_i) pi=p(yi=1xi)为给定条件下得到 y i = 1 y_i =1 yi=1的概率。在同样的条件下, y i = 0 y_i =0 yi=0的条件概率为 p ( y i = 0 ∣ x i ) = 1 − p i p(y_i=0|x_i)=1-p_i p(yi=0xi)=1pi。于是,得到一个观测值的概率为:
在这里插入图片描述
      (该式含义为,当y取1时,1-y=0,故上式可化为 p ( y = 1 ∣ x ; θ ) = ( h θ ( x ) ) p(y=1|x;\theta)=(h_\theta(x)) p(y=1x;θ)=(hθ(x));当当y取10时,1-y=1,故上式可化为 p ( y = 10 ∣ x ; θ ) = ( 1 − h θ ( x ) ) p(y=10|x;\theta)=(1-h_\theta(x)) p(y=10x;θ)=(1hθ(x))

因为各项观测独立,所以它们的联合分布可以表示为各边际分布的乘积:
在这里插入图片描述
      (其中,m为统计样本数目)

      该式称为n个观测的似然函数。我们的目标是能够求出使这一似然函数的值最大的参数估计。于是,最大似然估计的关键就是求出参数 θ 1 , θ 2 , θ 3 , θ 4 . . . θ n \theta_1,\theta_2,\theta_3,\theta_4...\theta_n θ1,θ2,θ3,θ4...θn,使上式取得最大值。
为了方便求导,对上式取对数:

在这里插入图片描述
      最大似然估计就是求使上式取最大值时的 θ θ θ,这里可以使用梯度上升法求解,求得的 θ θ θ就是要求的最佳参数。将 J ( θ ) J(θ) J(θ)取为: J ( θ ) = − ( 1 / m ) l ( θ ) , J ( θ ) J(θ)=-(1/m)l(θ),J(θ) J(θ)=(1/m)l(θ)J(θ)最小值时的 θ θ θ则为要求的最佳参数。通过梯度下降法求最小值。 θ θ θ的初始值可以全部为1.0,更新过程为:
在这里插入图片描述
      (其中,j表样本第j个属性,共n个; α \alpha α表示步长–每次移动量大小,可自由指定)
在这里插入图片描述
其中: ∂ g ( θ T x i ) ∂ θ j = ∂ ∂ θ j ( 1 1 + e − θ T x ) = g ( θ T x i ) ( 1 − g ( θ T x i ) ) ∂ ∂ θ j θ T x i \frac{\partial g(\theta^Tx_i)}{\partial \theta_j} = \frac{\partial }{\partial \theta_j}(\frac{1}{1+e^-\theta^Tx})=g(\theta^Tx_i)(1-g(\theta^Tx_i))\frac{\partial }{\partial \theta_j}\theta^Tx_i θjg(θTxi)=θj(1+eθTx1)=g(θTxi)(1g(θTxi))θjθTxi
因此, θ θ θ(可以设初始值全部为1.0)的更新过程可以写成:
在这里插入图片描述
      (i表示第i个统计样本,j表样本第j个属性; a a a表示步长)
      该公式将一直被迭代执行,直至达到收敛。( J ( θ ) J(\theta) J(θ)在每一步迭代中都减小,如果某一步减少的值少于某个很小的值 ϵ \epsilon ϵ(小于0.001),则其判定收敛)或某个停止条件为止(比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围)。

(2)向量化Vectorization求解

      Vectorization是使用矩阵计算来代替for循环以简化计算过程,提高效率。如上式,Σ(…)是一个求和的过程,显然需要一个for语句循环m次,所以根本没有完全的实现vectorization。下面介绍向量化的过程:约定训练数据的矩阵形式如下,x的每一行为一条训练样本,而每一列为不同的特称取值:
在这里插入图片描述
       g ( A ) g(A) g(A)的参数 A A A为一列向量,所以实现g函数时要支持列向量作为参数,并返回列向量。由上式可知 h θ ( x ) − y hθ(x)-y hθ(x)y可由 g ( A ) − y g(A)-y g(A)y直接计算求得。

θ更新过程可以改为:
在这里插入图片描述
      综上所述,Vectorization后θ更新的步骤如下:

      (1)求 A = X ∗ θ A=X*θ A=Xθ(此处为矩阵乘法, X X X ( m , n + 1 ) (m,n+1) m,n+1维向量, θ θ θ ( n + 1 , 1 ) (n+1,1) (n+11)维列向量, A A A就是 ( m , 1 ) (m,1) (m,1)维向量)

      (2)求 E = g ( A ) − y E=g(A)-y E=g(A)y ( E 、 y (E、y (Ey ( m , 1 ) (m,1) (m,1)维列向量)

      (3)求 θ : = θ − α x T E \theta:=\theta-\alpha x^TE θ:=θαxTE (a表示步长)

(3)步长 α \alpha α的选取

      在递推公式中, α \alpha α是调整步长,它的实质是梯度下降法中,每一步向当前负梯度方向调整的大小因子,也就是分类器学习到最优权向量的速度。因此,当我们在线性分类器的学习过程中使用梯度下降法时,调整步长 α \alpha α也称为“学习速率”。 α \alpha α的取值越大,求解的速度越快,但求解路径越不光滑,求解的精度越差,容易过冲甚至振荡; α \alpha α的取值越小,求解的速度越慢,但求解精度越高。因此,只要使用梯度下降法来训练分类器,其求解精度和速度之间是存在矛盾的。通常步长的选取,使用较多的两种方法为:固定步长和变步长法。

1、固定步长
      即令 α \alpha α取某一固定正值,选取之后测试 α \alpha α取该值时,分类器的训练效果,再根据训练效果来决定是否调整步长值。如果迭代不能正常进行(J增大了,步长太大,迈过了碗底)则考虑使用更小的步长,如果收敛较慢则考虑增大步长。每次选取的 α \alpha α值,约取为前一次的3倍。总的来说, α \alpha α的选定需要凭借一定的经验以及反复尝试方能找到合适的步长。\alpha$取值示例如:…, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1…。

2、变步长法
      变步长法的思路很符合我们通常的认识,即在优化过程中,刚开始迭代时,训练效果与目标相差较大,“还有很长一段路需要走”,故此时可增大 α \alpha α值,改进优化速度。当优化接近目标值时,参数的更新就需要小心谨慎了。如果步长选取太大,很容易一步跨过目标值,导致算法无法收敛。因此,在训练过程中,可以取按照某种规律逐步减少的 α \alpha α,使得算法开始时收敛较快,接近最优解时收敛速度变慢,以提高求解的精度。比较常用的变步长法是取: α ( k + 1 ) = λ 1 k \alpha(k+1)=\lambda \frac{1}{k} α(k+1)=λk1       其中, λ \lambda λ是选取的较大初始步长,k为迭代次数。

三、Logistic回归算法python实现

      本文给出的程序不可直接运行,代码的意义是为了帮助学习者了解逻辑回归算法的实现原理。使读者不通过机器学习算法库,也可以自己手写机器学习算法。介绍之前,先上代码:

class LR:
    def __init__(self, train_file_name, test_file_name, predict_result_file_name):
        self.train_file = train_file_name     # 训练集地址
        self.predict_file = test_file_name    # 测试集地址
        self.predict_result_file = predict_result_file_name   # 测试结果保存地址
        self.max_iters = 5000                 
        self.rate = 0.1  
        self.feats = []                       # 存放训练数据的空列表
        self.labels = []                      # 存放样本标签的空列表
        self.feats_test = []                  # 存放测试数据的空列表
        self.labels_predict = []              # 存放预测结果的空列表
        self.param_num = 0                    # 数据维度
        self.weight = []                      

    def loadDataSet(self, file_name, label_existed_flag):
        feats = []
        labels = []
        fr = open(file_name)
        lines = fr.readlines()
        for line in lines:
            temp = []
            allInfo = line.strip().split(',')
            dims = len(allInfo)
            if label_existed_flag == 1:
                for index in range(dims-1):
                    temp.append(float(allInfo[index]))
                feats.append(temp)
                labels.append(float(allInfo[dims-1]))
            else:
                for index in range(dims):
                    temp.append(float(allInfo[index]))
                feats.append(temp)
        fr.close()
        feats = np.array(feats)
        labels = np.array(labels)
        return feats, labels

    def loadTrainData(self):
        self.feats, self.labels = self.loadDataSet(self.train_file, 1)

    def loadTestData(self):
        self.feats_test, self.labels_predict = self.loadDataSet(
            self.predict_file, 0)

    def savePredictResult(self):
        print(self.labels_predict)
        f = open(self.predict_result_file, 'w')
        for i in range(len(self.labels_predict)):
            f.write(str(self.labels_predict[i])+"\n")
        f.close()

    def sigmod(self, x):
        return 1/(1+np.exp(-x))

    def printInfo(self):
        print(self.train_file)
        print(self.predict_file)
        print(self.predict_result_file)
        print(self.feats)
        print(self.labels)
        print(self.feats_test)
        print(self.labels_predict)

    def initParams(self):
        self.weight = np.ones((self.param_num,), dtype=np.float)

    def compute(self, recNum, param_num, feats, w):
        return self.sigmod(np.dot(feats, w))

    def error_rate(self, recNum, label, preval):
        return np.power(label - preval, 2).sum()

    def predict(self):
        self.loadTestData()
        preval = self.compute(len(self.feats_test),
                              self.param_num, self.feats_test, self.weight)
        self.labels_predict = (preval+0.5).astype(np.int)
        self.savePredictResult()

    def train(self):
        self.loadTrainData()
        recNum = len(self.feats)
        self.param_num = len(self.feats[0])
        self.initParams()
        ISOTIMEFORMAT = '%Y-%m-%d %H:%M:%S,f'
        for i in range(self.max_iters):
            preval = self.compute(recNum, self.param_num,
                                  self.feats, self.weight)
            sum_err = self.error_rate(recNum, self.labels, preval)
            if i%30 == 0:
                print("Iters:" + str(i) + " error:" + str(sum_err))
                theTime = datetime.datetime.now().strftime(ISOTIMEFORMAT)
                print(theTime)
            err = preval - self.labels
            delt_w = np.dot(self.feats.T, err)
            delt_w /= recNum
            self.weight -= self.rate*delt_w

      文中给出的,是逻辑回归算法完整的一个类。类中主要包括几个部分:变量的初始化、数据集的加载和预处理、预测结果的保存、Signoid函数定义以及具体的训练过程。下面对重点内容进行介绍:

(1)数据集加载

      在类中,首先构建了一个loadDataSet函数,通过该函数,可将给定的由逗号分隔开的txt格式数据文档,转化为数据,存放到预先初始化的列表中。代码中:allInfo = line.strip().split(’,’) 作用是对文档中的每一行数据,按“,”将每个数据提取出,按列存放到列表中。语句:if label_existed_flag == 1:是对训练集和测试集的一个判断,在后期调用loadDataSet函数,处理训练集文档时,令$label_existed_flag == 1;处理测试集文档时,令label_existed_flag = = 1。如此编写是因为训练数据与测试数据的差异性,在训练数据中,文档最后一列存放的是数据标签,数据标签也需要提取出来存放到列表中。而对测试数据而言,最后一列并非标签。因此对两个文档的处理方法,略有差别。故通过01判断,选择适合各自的处理方法。最后,对训练数据文档和测试数据文档,分别调用loadDataSet函数,将训练数据及标签存放在:self.feats和self.labels中。将测试数据存放在:self.feats_test中。

(2)模型训练

      在得到处理后的训练数据及标签之后,可以开始训练过程中。训练过程主要是对系列权值 θ 1 , θ 2 , θ 3 , θ 4 . . . θ n \theta_1,\theta_2,\theta_3,\theta_4...\theta_n θ1,θ2,θ3,θ4...θn的不断迭代更新,使用的优化方法为梯度下降法。在前面的数学推导中,我们得到了算法的迭代公式为:
在这里插入图片描述
      但考虑到倘若使用该式进行更新权值,不可避免的会使用到for循环语句,使用算法效率极低。为了提高算法效率,我们改用了向量化Vectorization求解的方法更新权值,迭代公式为:
在这里插入图片描述
在算法中的具体实现代码为:

 err = preval - self.labels
 delt_w = np.dot(self.feats.T, err)
 delt_w /= recNum
 self.weight = self.weight-self.rate*delt_w

      其中,算法中的 n p . d o t ( s e l f . f e a t s . T , e r r ) np.dot(self.feats.T, err) np.dot(self.feats.T,err)即对应公式中的: x T E x^TE xTE r e c N u m recNum recNum即为公式中的m。
      第一期博客到这里就结束了,文中不当之处还请各位读者批评指正。如果还有不太理解的地方,可在评论区留言。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值