机器学习练习(四)——多元逻辑回归

作者:John Wittenauer

翻译:GreatX

源:Machine Learning Exercises In Python, Part 4

这篇文章是一系列 Andrew NgCoursera 上的机器学习课程的练习的一部分。这篇文章的原始代码,练习文本,数据文件可从这里获得。

Part 1 简单线性回归(Simple Linear Regression)

Part 2 多元线性回归(Multivariate Linear Regression)

Part 3 逻辑回归(Logistic Regression)

Part 4 多元逻辑回归(Multivariate Logistic Regression)

Part 5 神经网络(Neural Networks)

Part 6 支持向量机(Support Vector Machines)

Part 7 K-均值聚类与主成分分析(K-Means Clustering & PCA)

Part 8 异常检测与推荐(Anomaly Detection & Recommendation)

在第三部分我们实现了简单和正则化逻辑回归,完成了 Andrew Ng 的机器学习课程第二个练习的 Python 实现版本。然而我们的解也有局限性——它只支持二分类(binary classification)。在这篇文章中我们将扩展我们先前练习中的解以处理多类分类。在这种情况下,我们将完成 练习3 的前半部分并准备下一个大主题,神经网络。

语法速记:为了显示语句的输出,我在代码块后面加 > 表示之前语句运行的结果。如果结果非常长(超过1,2行)我将把它放在代码块的后面作为独立的块。希望它能清楚地表明哪些是语句输入哪些是输出。

在这个练习里我们的任务是用逻辑回归识别手写数字(0到9)。首先,导入数据集。不像之前的练习,我们的数据文件格式是 MATLAB 的格式,它并不能被 pandas 自动识别,所以为了用 python 导入它我们需要使用一个 SciPy 实用程序(数据文件可在文章的前面找到)。

import numpy as np  
import pandas as pd  
import matplotlib.pyplot as plt  
from scipy.io import loadmat  
%matplotlib inline

data = loadmat('data/ex3data1.mat')  
data  
{'X': array([[ 0.,  0.,  0., ...,  0.,  0.,  0.],
        [ 0.,  0.,  0., ...,  0.,  0.,  0.],
        [ 0.,  0.,  0., ...,  0.,  0.,  0.],
        ..., 
        [ 0.,  0.,  0., ...,  0.,  0.,  0.],
        [ 0.,  0.,  0., ...,  0.,  0.,  0.],
        [ 0.,  0.,  0., ...,  0.,  0.,  0.]]),
 '__globals__': [],
 '__header__': 'MATLAB 5.0 MAT-file, Platform: GLNXA64, Created on: Sun Oct 16 13:09:09 2011',
 '__version__': '1.0',
 'y': array([[10],
        [10],
        [10],
        ..., 
        [ 9],
        [ 9],
        [ 9]], dtype=uint8)}

让我们快速检查一下刚刚载入内存的数组 shape 。

data['X'].shape, data['y'].shape

> ((5000L, 400L), (5000L, 1L))

我们已经载入了数据。这些图像在矩阵 X 中表示为一个 400 维的向量。这 400个“特征”代表在原始 20x20 图像中每个像素的灰度强度。在向量 y 中的类标签是数值类其数值代表图像中的数字。下面的插图给出了一个其中一些数字的样子的例子。每个带白色手写数字的灰块代表在我们数据集中 400 维的一行。

我们第一个任务是修改逻辑回归实现使之完全向量化(即没有 for 循环)。这是因为向量化代码,除了简洁外还能够利用线性代数优化并且通常比迭代代码更快。但是如果你看了我们在 练习2 中的代价函数实现,你就知道它已经向量化了。所以在这里我们重用该实现。注意,我们直接跳到了最后,正则化版。

def sigmoid(z):  
    return 1 / (1 + np.exp(-z))

def cost(theta, X, y, learningRate):  
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)
    first = np.multiply(-y, np.log(sigmoid(X * theta.T)))
    second = np.multiply((1 - y), np.log(1 - sigmoid(X * theta.T)))
    reg = (learningRate / 2 * len(X)) * np.sum(np.power(theta[:,1:theta.shape[1]], 2))
    return np.sum(first - second) / (len(X)) + reg

再次提醒,这个代价函数同我们在之前练习中创建的是一模一样的,所以如果你不确定我们是怎么到这一步的,看看前面的文章再看下面。

接下来,我们需要计算梯度的函数。我们在之前的练习中已经定义好了,只是在这里我们确实需要去掉在 更新 那一步的 for 循环。这里给出原始代码供参考。

def gradient_with_loop(theta, X, y, learningRate):  
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)

    parameters = int(theta.ravel().shape[1])
    grad = np.zeros(parameters)

    error = sigmoid(X * theta.T) - y

    for i in range(parameters):
        term = np.multiply(error, X[:,i])

        if (i == 0):
            grad[i] = np.sum(term) / len(X)
        else:
            grad[i] = (np.sum(term) / len(X)) + ((learningRate / len(X)) * theta[:,i])

    return grad

让我们退一步,提醒自己在这里发生了什么。我们只定义了一个代价函数说,简单地说,“给定一些候选 θ 应用于输入的 X ,看看得到的结果与真正期望的输出 y 相差多远”。这是我们评估一组参数的方法。相比之下,梯度函数指出了如何改变这些参数以获得比之前的略好一点的答案。如何你不熟悉线性代数的话,这一切是如何运行的背后的数学,可能有点复杂。但在练习文本中它推导的很清楚,我鼓励读者去熟悉它以更好地理解为什么这些函数能够成立。

现在我们需要创建一个不用任何循环版本的梯度函数。在新版本中我们将拿出 for 循环并用线性代数一次性计算每个参数的梯度(除了截距参数,它没有被正则化所以需要分开计算)。

也要注意到,我们将数据结构转换为了 NumPy 矩阵(这是我在大部分练习中使用的结构)。做这些是试图让代码(与使用数组相比)看起来和 Octave 更相似。这将会用上数组,因为矩阵自动遵循矩阵运算法则还有(数组中默认的)点乘、点除运算。社区中有一些关于是否应该完全使用矩阵类的争论,但这是在这里,所以我们将在我们的例子中使用它。

def gradient(theta, X, y, learningRate):  
    theta = np.matrix(theta)
    X = np.matrix(X)
    y = np.matrix(y)

    parameters = int(theta.ravel().shape[1])
    error = sigmoid(X * theta.T) - y

    grad = ((X.T * error) / len(X)).T + ((learningRate / len(X)) * theta)

    # intercept gradient is not regularized
    grad[0, 0] = np.sum(np.multiply(error, X[:,0])) / len(X)

    return np.array(grad).ravel()

现在我们已经定义好了代价函数和梯度函数,是时候建立一个分类器了。对于这个任务我们有 10 个可能的类,并且因为逻辑回归只能一次辨别 2 个类,我们需要一个策略处理 多类 的情况。在这个练习中我们肩负着实现一个一对多( one-vs-all)分类方法的任务,其中带有 k 个类的标签会有 k 个分类器,每一个标签都会在“类 i ”和“非类 i”(即任何不是 i 的类)之间判决。我们将分类器包装在一个函数中,计算每个分类器最终的权值返回为一个 k(n+1) 的数组,其中 n 是参数的个数。

from scipy.optimize import minimize

def one_vs_all(X, y, num_labels, learning_rate):  
    rows = X.shape[0]
    params = X.shape[1]

    # k X (n + 1) array for the parameters of each of the k classifiers
    all_theta = np.zeros((num_labels, params + 1))

    # insert a column of ones at the beginning for the intercept term
    X = np.insert(X, 0, values=np.ones(rows), axis=1)

    # labels are 1-indexed instead of 0-indexed
    for i in range(1, num_labels + 1):
        theta = np.zeros(params + 1)
        y_i = np.array([1 if label == i else 0 for label in y])
        y_i = np.reshape(y_i, (rows, 1))

        # minimize the objective function
        fmin = minimize(fun=cost, x0=theta, args=(X, y_i, learning_rate), method='TNC', jac=gradient)
        all_theta[i-1,:] = fmin.x

    return all_theta

在这里有一些事需要注意。第一,我们给 θ 加了额外的一个参数(一列 1 元)代表截距项。第二,我们将 y 从类标签改为每个分类器的二进制值(不是类 i 的就是非类 i 的)。最后,我们正在使用 SciPy 新的优化 API 来最小化每个分类器的代价函数。 该 API 需要一个目标函数,一组初始参数,一个优化方法,(如果指定了)和一个雅可比( jacobian)(梯度)函数。由优化 API 找到参数并将其分配给参数数组。

实现向量化代码更有挑战的一部分是把所有相互关联的矩阵之间的运算正确的写出来,所以我发现做完整性检查很有必要,检查一下要处理的数组/矩阵的形状,确保自己对它们是可感知的。让我们看看上面的函数中用到的数据结构。

rows = data['X'].shape[0]  
params = data['X'].shape[1]

all_theta = np.zeros((10, params + 1))

X = np.insert(data['X'], 0, values=np.ones(rows), axis=1)

theta = np.zeros(params + 1)

y_0 = np.array([1 if label == 0 else 0 for label in data['y']])  
y_0 = np.reshape(y_0, (rows, 1))

X.shape, y_0.shape, theta.shape, all_theta.shape

> ((5000L, 401L), (5000L, 1L), (401L,), (10L, 401L))

这些都似乎都能讲得通。注意, θ 是一维数组,所以在代码中当它被转换为矩阵以计算梯度时,它变成了一个(1 × 401)矩阵。让我们也检查在 y 中的类标签以确保他们看起来如我们所期待的一样。

np.unique(data['y'])

> array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10], dtype=uint8)

让我们确保我们的训练函数能够正确的运行,在进一步动作之前,我们先获取一些可感知的输出。

all_theta = one_vs_all(data['X'], data['y'], 10, 1)  
all_theta  
array([[ -5.79312170e+00,   0.00000000e+00,   0.00000000e+00, ...,
          1.22140973e-02,   2.88611969e-07,   0.00000000e+00],
       [ -4.91685285e+00,   0.00000000e+00,   0.00000000e+00, ...,
          2.40449128e-01,  -1.08488270e-02,   0.00000000e+00],
       [ -8.56840371e+00,   0.00000000e+00,   0.00000000e+00, ...,
         -2.59241796e-04,  -1.12756844e-06,   0.00000000e+00],
       ..., 
       [ -1.32641613e+01,   0.00000000e+00,   0.00000000e+00, ...,
         -5.63659404e+00,   6.50939114e-01,   0.00000000e+00],
       [ -8.55392716e+00,   0.00000000e+00,   0.00000000e+00, ...,
         -2.01206880e-01,   9.61930149e-03,   0.00000000e+00],
       [ -1.29807876e+01,   0.00000000e+00,   0.00000000e+00, ...,
          2.60651472e-04,   4.22693052e-05,   0.00000000e+00]])

现在开始准备最后一步——使用训练好的分类器来预测每个图片的标签。对于这一步,我们将计算每个类、每个训练实例(当然用的是向量化代码)的类概率,对每个输出分配有最高概率的类标签。

def predict_all(X, all_theta):  
    rows = X.shape[0]
    params = X.shape[1]
    num_labels = all_theta.shape[0]

    # same as before, insert ones to match the shape
    X = np.insert(X, 0, values=np.ones(rows), axis=1)

    # convert to matrices
    X = np.matrix(X)
    all_theta = np.matrix(all_theta)

    # compute the class probability for each class on each training instance
    h = sigmoid(X * all_theta.T)

    # create array of the index with the maximum probability
    h_argmax = np.argmax(h, axis=1)

    # because our array was zero-indexed we need to add one for the true label prediction
    h_argmax = h_argmax + 1

    return h_argmax

现在,我们可以使用 predict_all 函数对每个实例生成类预测并看看分类器表现如何。

y_pred = predict_all(data['X'], all_theta)  
correct = [1 if a == b else 0 for (a, b) in zip(y_pred, data['y'])]  
accuracy = (sum(map(int, correct)) / float(len(correct)))  
print 'accuracy = {0}%'.format(accuracy * 100)

> accuracy = 97.58%

接近 98% 实际上是相当不错的,对于一个如逻辑回归一般,相对简单的方法而言。虽然,我们可以得到更好的。在下一篇文章中,我们将看到通过(从头开始)实现一个前馈神经网络来改进这个结果,并将其应用到相同的图像分类任务中。

你可能感兴趣的:(翻译文档,机器学习)