机器学习笔记(二):逻辑回归算法


机器学习(二):逻辑回归算法

逻辑回归(Logistic Regression, LR)模型其实仅在线性回归的基础上,套用了一个逻辑函数,但也就由于这个逻辑函数,使得逻辑回归模型能够输出类别的概率。

逻辑回归的本质是:假设数据服从这个分布,然后使用极大似然估计做参数的估计。

1. 什么是回归

一说回归最先想到的是终结者那句:I’ll be back

regress,re表示back,gress等于go,数值go back to mean value,也就是I’ll be back 的意思

在数理统计中,回归是确定多种变量相互依赖的定量关系的方法

通俗理解:越来越接近期望值的过程,回归 于事物的本质

最简单的回归是线性回归(Linear Regression),也就是通过最小二乘等方法得到模型的参数。线性回归假设输出变量是若干输出变量的线性组合,并根据这一关系求解线性组合中的最优系数。

通俗理解:输出一个线性函数,例如$y=f(x; \theta)$,通过寻找最优的参数$\theta$使得观测数据与模型数据相吻合。

2. 逻辑回归模型

回归是一种比较容易理解的模型,就相当于$y=f(x)$,表明自变量$x$与因变量$y$的关系。

以常见的看医举例,医生治病时的望、闻、问、切,之后判定病人是否生病或生了什么病,其中的望闻问切就是获取自变量$x$,即特征数据,判断是否生病就相当于获取因变量$y$,即预测分类。

$X$为数据点——肿瘤的大小,$Y$为观测值——是否是恶性肿瘤。

通过构建线性回归模型,如$h_\theta(x)$所示,构建线性回归模型后,即可以根据肿瘤大小,预测是否为恶性肿瘤$h_\theta(x)) \ge 0.5$为恶性,$h_\theta(x) \lt 0.5$为良性。

然而线性回归的鲁棒性很差,例如在上图的数据集上建立回归,因最右边噪点的存在,使回归模型在训练集上表现都很差。这主要是由于线性回归在整个实数域内敏感度一致,而分类范围,需要在$[0,1]$。

逻辑回归就是一种减小预测范围,将预测值限定为$[0,1]$间的一种回归模型,其回归方程与回归曲线如下图所示。逻辑曲线在$z=0$时,十分敏感,在$z>>0$或$z<<0$处,都不敏感,将预测值限定为$(0,1)$。

2.1 逻辑回归表达式

这个函数称为Logistic函数(Logistic Function),也称为Sigmoid函数(Sigmoid Function)。函数公式如下:

$$
g(z) = \frac{1}{1+e^{-z}}
$$

Logistic函数:

  • 当$z$趋近于无穷大时,$g(z)$趋近于1;
  • 当$z$趋近于无穷小时,$g(z)$趋近于0。

Logistic函数的图形如上图所示。Logistic函数求导时有一个特性,这个特性将在下面的推导中用到,这个特性为:
$$
g’(z) = \frac{d}{dz} \frac{1}{1+e^{-z}} \
= \frac{1}{(1+e^{-z})^2}(e^{-z}) \
= \frac{1}{(1+e^{-z})} (1 - \frac{1}{(1+e^{-z})}) \
= g(z)(1-g(z))
$$
逻辑回归本质上是线性回归,只是在特征到结果的映射中加入了一层函数映射,即先把特征线性求和,然后使用函数$g(z)$将做为假设函数来预测。

$g(z)$可以将连续值映射到0到1之间。线性回归模型的表达式带入$g(z)$,就得到逻辑回归的表达式:
$$
h_\theta(x) = g(\theta^T x) = \frac{1}{1+e^{-\theta^T x}}
$$

2.2 逻辑回归的软分类

现在我们将y的取值$h_\theta(x)$通过Logistic函数归一化到(0,1)间,$y$的取值有特殊的含义,它表示结果取1的概率,因此对于输入$x$分类结果为类别1和类别0的概率分别为:

$$
P(y=1|x,\theta) = h_\theta(x) \
P(y=0|x,\theta) = 1 - h_\theta(x)
$$

对上面的表达式合并一下就是:

$$
p(y|x,\theta) = (h_\theta(x))^y (1 - h_\theta(x))^{1-y}
$$

2.3 梯度上升

得到了逻辑回归的表达式,下一步跟线性回归类似,构建似然函数,然后最大似然估计,最终推导出$\theta$的迭代更新表达式。只不过这里用的不是梯度下降,而是梯度上升,因为这里是最大化似然函数。

假设训练样本相互独立,那么似然函数表达式为:

同样对似然函数取log,转换为:

转换后的似然函数对$\theta$求偏导,在这里我们以只有一个训练样本的情况为例:

这个求偏导过程中:

  • 第一步是对$\theta$偏导的转化,依据偏导公式:$y=lnx$, $y’=1/x$。
  • 第二步是根据$g(z)$求导的特性$g’(z) = g(z)(1 - g(z))$ 。
  • 第三步就是普通的变换。

这样我们就得到了梯度上升每次迭代的更新方向,那么$\theta$的迭代表达式为:
$$
\theta = \theta + \eta (y^i - h_\theta(x^i)) x_j^i
$$

其中$\eta$是学习速率。

3. 手写逻辑回归

这里以手写数字识别为例。我们手写一个逻辑回归类处理这个多分类问题,同时使用sklearn的逻辑回归模型作对比。

import random
import time
import matplotlib.pyplot as plt
import numpy as np
from math import inf
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix


def sigmoid(x):
    """
    sigmoid函数
    """
    return 1.0 / (1 + np.exp(-x))


def acc_score(predict_data, raw_data):
    """
    计算结果的准确率
    :param predict_data: 预测值
    :param raw_data: 真值
    """
    cnt = 0
    size = np.shape(predict_data)[0]
    for i in range(size):
        if int(raw_data[i]) == int(predict_data[i]):
            cnt += 1
    score = float(cnt / size)
    return score


fig_num = 0


def display(images, ground_truth, predict, fig_name):
    """
    画出预测结果
    :param images: 数字的图片
    :param ground_truth: 真值
    :param predict: 预测值
    :param fig_name: 图片名字
    """
    # plot the digits
    global fig_num
    fig_num += 1
    if fig_name is None:
        fig = plt.figure(fig_num, figsize=(6, 6))  # figure size in inches
    else:
        fig = plt.figure(fig_name, figsize=(6, 6))  # figure size in inches
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
    size = np.shape(predict)[0]
    if size > 56:
        size = 56
    # plot the digits: each image is 8x8 pixels
    for i in range(size):
        ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
        ax.imshow(images[i], cmap=plt.cm.binary)
        # label the image with the target value
        ax.text(0, 7, str(ground_truth[i]))
        if int(ground_truth[i]) == int(predict[i]):
            ax.text(6, 7, str(int(predict[i])), color='green', size=15)
        else:
            ax.text(6, 7, str(int(predict[i])), color='red', size=20)

    # plot confusion matrix
    cm = confusion_matrix(ground_truth, predict)
    plt.matshow(cm)
    plt.colorbar()
    plt.title(fig_name + ' ' + 'Confusion Matrix')
    plt.ylabel('Groundtruth')
    plt.xlabel('Predict')


def load_data(train_size=0.75):
    """
    加载数据同时分割出训练集和测试集
    :param train_size: 训练集的比例
    :return 分割好的数据集
    """
    # 加载数据
    # 这个函数返回的应该是一个类实例,里面有很多的变量,需要“单独的拿出来用”
    digits = load_digits()
    # 输出查看一下这个类的属性和方法
    # print(dir(digits))
    # 获得数据的大小
    data_size = np.shape(digits.data)[0]
    # 将数据打乱,这里需要把这个类的要用上的变量都对应上,有点麻烦(是否有更简便的方法?)
    shuffled_index = np.random.permutation(data_size)

    digits.data = digits.data[shuffled_index]
    digits.images = digits.images[shuffled_index]
    digits.target = digits.target[shuffled_index]

    # 分割数据集
    split_index = int(data_size * train_size)

    train_data = digits.data[0:split_index]
    train_target = digits.target[0:split_index]
    train_images = digits.images[0:split_index]

    test_data = digits.data[split_index:]
    test_target = digits.target[split_index:]
    test_images = digits.images[split_index:]

    return train_data, train_target, train_images, test_data, test_target, test_images


class Logistic_Regression:
    """
    逻辑回归模型,通过OVR(One-Vs-All)解决多分类问题
    """

    def __init__(self, data, label, max_iter=500, alpha=0.01, tol=0.000005):
        """
        初始化逻辑回归类
        :param data: 加载训练集
        :param label: 加载训练集的标签
        :param max_iter: 最大迭代次数
        :param alpha: 学习率(步长)
        :param tol: 停止求解的标准,当收敛速度过慢时停止求解
        """
        # 保存传入数据
        self.data = data
        self.label = label
        self.max_iter = max_iter
        self.alpha = alpha
        self.tol = tol

        # 进一步处理
        # 数据长度m与单个数据的长度n
        self.m, self.n = np.shape(data)
        # 数据权重
        self.weights = np.ones((10, self.n))
        # 常数项
        self.b = np.ones(10)

    def train(self, method="SGD"):
        """
        进行训练,这里采用的是OVR方法解决多分类问题
        :param method: 使用的求解方法
        """
        # 对于每一个数字
        for number in range(10):
            # 重新打标签
            label = np.copy(self.label)
            for i in range(self.m):
                if label[i] == number:
                    label[i] = 1
                else:
                    label[i] = 0
            if method == "SGD":
                # 采用随机梯度上升法进行迭代求解
                self.SGD(number, label)
            elif method == "MBGD":
                # 采用小批量梯度下降法
                self.MBGD(number, label)
            else:
                print("error method:", method)
                exit(-1)

    def predict(self, predict_data):
        """
        进行预测
        :param predict_data: 测试集
        """
        # 保存对测试集的预测结果
        result = np.zeros(np.shape(predict_data)[0])
        # 保存对单个数据,每个分类器给出的结果
        ans = np.zeros(10)
        # 开始对整个测试集进行预测
        for i in range(len(result)):
            # 分别用每个数字的分类器进行预测
            for k in range(10):
                ans[k] = sigmoid(sum(self.weights[k, :] * predict_data[i, :] + self.b[k]))
            ans = list(ans)
            result[i] = ans.index(max(ans))
        return result

    def SGD(self, number, label):
        """
        使用老师上课讲的随机梯度上升法求解
        :param number: 目前需要被分类的数字
        :param label: 根据这个数组制作的标签
        """
        for i in range(self.max_iter):
            # 上次的误差
            last_error = inf
            num_index = list(range(self.m))
            for j in range(self.m):
                # FIXME
                #  之前其实是随机打乱了的,所以这里是不是没有必要进行这样的操作?
                #  这里实际上还是用上了所有的样本,所以实际上这里随机取值对结果并没有影响
                rand_index = int(np.random.uniform(0, len(num_index)))
                error = label[rand_index] - sigmoid(
                    sum(self.weights[number] * train_data[rand_index]) + self.b[number])
                # 实际上下面这样做的效果不好,error可能为正也可能为负。。。
                # if abs(error) > abs(last_error):
                # if error > last_error:
                #     continue
                # 下面右边应该是 学习率*(1-g(z))*z,是求偏导的过程
                self.weights[number] += self.alpha * error * train_data[rand_index]
                self.b[number] += self.alpha * error
                del (num_index[rand_index])
                if abs(last_error - error) < self.tol:
                    break
                last_error = error

    def MBGD(self, number, label):
        """
        尝试使用小批量梯度下降法求解
        :param number: 目前需要被分类的数字
        :param label: 根据这个数组制作的标签
        """
        for i in range(self.max_iter):
            # 上次的误差
            last_error = inf
            # 小批量的数目
            if self.m < 200:
                m = random.randint(1, self.m)
            else:
                m = random.randint(10, 200)
            num_index = list(range(self.m))
            for j in range(m):
                rand_index = int(np.random.uniform(0, len(num_index)))
                error = label[rand_index] - sigmoid(
                    sum(self.weights[number] * train_data[rand_index]) + self.b[number])
                self.weights[number] += self.alpha * error * train_data[rand_index]
                self.b[number] += self.alpha * error
                del (num_index[rand_index])
                if abs(last_error - error) < self.tol:
                    break
                last_error = error


if __name__ == "__main__":
    # 加载并且分割各种数据
    train_data, train_target, train_images, test_data, test_target, test_images = load_data(train_size=0.75)

    # =====================使用自己的方法做预测========================
    # 生成一个逻辑回归类实例,并且把训练集导入
    lr = Logistic_Regression(train_data, train_target, 3000, 0.01, 0.0000005)
    # 开始训练并且计时
    start = time.clock()
    lr.train(method="SGD")
    end = time.clock()
    print("训练用时:\t%f s" % (end - start))
    # 对训练集做预测并且输出准确度与结果
    train_result = lr.predict(train_data)
    print("对训练集做预测:\t", acc_score(train_result, train_target))
    display(train_images, train_target, train_result, "My Train")
    # 对测试集做预测并且输出准确度与结果
    test_result = lr.predict(test_data)
    print("对测试集做预测:\t", acc_score(test_result, test_target))
    display(test_images, test_target, test_result, "My Test")

    # =====================使用sklearn做预测========================
    skl = LogisticRegression()

    start = time.clock()
    skl.fit(train_data, train_target)
    end = time.clock()
    print("训练用时:\t%f s" % (end - start))

    pred_train = skl.predict(train_data)
    print("对训练集做预测:\t", acc_score(pred_train, train_target))
    display(train_images, train_target, pred_train, "Sklearn Train")

    pred_test = skl.predict(test_data)
    print("对测试集做预测:\t", acc_score(pred_test, test_target))
    display(test_images, test_target, pred_test, "Sklearn Test")

    plt.show()

手写逻辑回归预测结果展示:


文章作者: Immortalqx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Immortalqx !
评论
  目录