python数据分析与机器学习实战我爱编程机器学习与数据挖掘

(二十五)案例实战:Python实现逻辑回归与梯度下降策略-py

2018-05-25  本文已影响196人  努力奋斗的durian

文章原创,最近更新:2018-05-25

课程内容:Python实现逻辑回归与梯度下降策略

课程来源: python数据分析与机器学习实战-唐宇迪

我们将建立一个逻辑回归模型来预测一个学生是否被大学录取。假设你是一个大学系的管理员,你想根据两次考试的结果来决定每个申请人的录取机会。你有以前的申请人的历史数据,你可以用它作为逻辑回归的训练集。对于每一个培训例子,你有两个考试的申请人的分数和录取决定。为了做到这一点,我们将建立一个分类模型,根据考试成绩估计入学概率。

首先读取这个文件"LogiReg_data.txt"的数据前5行,看一下数据是长什么样子的,具体代码如下:(备注:Admitted是代表是否录取的意思)

import pandas as pd 
pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
print(pdData.head())

输出结果如下:

      Exam 1     Exam 2  Admitted
0  34.623660  78.024693         0
1  30.286711  43.894998         0
2  35.847409  72.902198         0
3  60.182599  86.308552         1
4  79.032736  75.344376         1

对数据有个大致的了解之后,求出这个数据的维度,如下:

import pandas as pd 

pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
print(pdData.shape)

输出结果如下:

(100, 3)

分别用0,1表示正例和负例.'Exam 1'以及'Exam 2'分别录取的人数的散点图

案例代码如下:

import pandas as pd 
import matplotlib.pyplot as plt

pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
pdData.head()
print(pdData.shape)

positive=pdData[pdData["Admitted"]==1]
negative=pdData[pdData["Admitted"]==0]
fig, ax = plt.subplots(figsize=(10,5))
ax.scatter(positive['Exam 1'], positive['Exam 2'], s=30, c='b', marker='o', label='Admitted')
ax.scatter(negative['Exam 1'], negative['Exam 2'], s=30, c='r', marker='x', label='Not Admitted')
ax.legend()
ax.set_xlabel('Exam 1 Score')
ax.set_ylabel('Exam 2 Score')

输出结果如下:



蓝色的点代表是录取的,未被录取的是红色的.对两种颜色的点是否可以进画一条决策边界呢?

数据现在也有了,算法之前也推倒过了,怎么用python将逻辑回归实现出来?

首先要了解一下逻辑回归:

目标:建立分类器(求解出三个参数 θ0、θ1、θ2 )
备注:θ1对应'Exam 1'成绩,θ2对应'Exam 2'

设定阈值,根据阈值判断录取结果
备注:阈值指的是最终得到的概率值.将概率值转化成一个类别.一般是>0.5是被录取了,<0.5未被录取.

要完成的模块:

sigmoid 函数:


g:ℝ→[0,1] (值域是0到1)
g(0)=0.5 (当x=0时,y=0.5)
g(−∞)=0 (当x趋于负无穷时,y=0)
g(+∞)=1 (当x趋于正无穷时,y=1)

如果不知道sigmoid 函数长什么样子,可以看看如下的案例代码

import numpy as np
import pandas as pd 
import matplotlib.pyplot as plt

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

nums=np.arange(-10,10,step=1)
fig,ax=plt.subplots(figsize=(12,4))
ax.plot(nums,sigmoid(nums),"r")

输出结果如下:


当构造一个回归模型的时候,要把数据当中插入一列,让那一列的值都等于1.要把数值计算转化成矩阵的运算.根据理论让代码实现出来,

备注:

import pandas as pd 
pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
pdData.insert(0, 'Ones', 1)
print(pdData.head())

输出结果如下:

   One     Exam 1     Exam 2  Admitted
0    1  34.623660  78.024693         0
1    1  30.286711  43.894998         0
2    1  35.847409  72.902198         0
3    1  60.182599  86.308552         1
4    1  79.032736  75.344376         1

建立一个4*2的矩阵,利用shape可输出矩阵的维度,即行数和列数。shape[0]和shape[1]分别代表多少行?和多少列?结果如图.


构造完之后,不知道准不准?那应该怎么办呢?

首先查看x前5行对应的值是对的?

import numpy as np
import pandas as pd 
pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
print(x[:5])

输出结果如下:

[[  1.          34.62365962  78.02469282]
 [  1.          30.28671077  43.89499752]
 [  1.          35.84740877  72.90219803]
 [  1.          60.18259939  86.3085521 ]
 [  1.          79.03273605  75.34437644]]

输出的结果是与截图相一致的


再看看y的前5列是否有问题?

import numpy as np
import pandas as pd
pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
print(y[:5])

输出结果如下:

[[ 0.]
 [ 0.]
 [ 0.]
 [ 1.]
 [ 1.]]

从输出的结果来看是没有问题的.

在看看theta是否有问题?

pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
print(theta)

输出结果如下:

[[ 0.  0.  0.]]

检查完之后,在分别打印x.shape,y.shape,theta.shape的维度

pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
print(x.shape,y.shape,theta.shape)

输出结果如下:

(100, 3) (100, 1) (1, 3)

从输出的结果来看,这三个矩阵的维度也是没有任何问题的.

接下来对数据进行组合,并得到最终的结果.

首先先定义损失函数:

将对数似然函数去负号


求平均损失

这个损失函数是怎么来的,是根据以下截图的应用梯度上升最大值得来的:(-1/n的过程分步骤,先对数似然函数去负号,再除以1/n)


这个函数用python代码怎么表达cost()这个损失函数呢?并计算这个损失函数?

import numpy as np
import pandas as pd 
pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])
def sigmoid(z):
    return 1/(1+np.exp(-z))

def model(X,theta):
    return sigmoid(np.dot(X,theta.T))

def cost(x,y,theta):
    left=np.multiply(-y,np.log(model(x,theta)))
    right=np.multiply(1-y,np.log(1-model(x,theta)))
    return np.sum(left-right)/(len(x))

pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
print(cost(x,y,theta))

输出结果如下:

0.69314718056

接下来,怎么计算梯度函数?

梯度函数是以下截图


就是求偏导,求导的过程就是以下截图的内容

构造theta参数的时候,构造的是3个参数θ0、θ1、θ2参数,算出来的梯度算几个呀?构造的时候构造了3个θ0、θ1、θ2参数,那这三个参数求解的时候,应该求出几个梯度呀?显然也是三个.具体相关知识点如下:
1)在python代码中,定义grad为梯度,用np.zeros(theta.shape)中的zeros函数进行占位.梯度应与theta.shape的维度是一致的.
2)这里的error=yi−hθ(xi),跟表达式有点相反了,原因是把负号提到里面去了.
3)np.ravel()将多维数组降为一维,返回的是视图,修改时会影响原始矩阵 .
案例代码如下:

import numpy as np
a = np.array([[1 , 2] , [3 , 4]])
c=a.ravel()
print(c)

c[0]=10
print(c)

输出结果如下:

[1 2 3 4]

[10  2  3  4]

梯度函数怎么用python表达呢?


def gradient(x, y, theta):
    grad = np.zeros(theta.shape)
    error = (model(x, theta)- y).ravel()
    for j in range(len(theta.ravel())): #for each parmeter
        term = np.multiply(error, x[:,j])
        grad[0, j] = np.sum(term) / len(x)
    
    return grad

比较3种不同梯度下降的方法,具体梯度下降的方法如下截图:


是按照迭代次数进行停止,因为进行模型优化的时候,可以进行迭代,在不断进行迭代的过程中,无论是多少次,最终都需要停下来.这里就设置了3种停止的策略.

比如设置迭代次数是2000次,一旦达到次数,每次进行优化,每次进行参数更新,就进行计数.迭代次数跟什么挂钩呢?就是更新一次参数就是完成了一次迭代.再更新一次参数,就是再完成了一次迭代.根据迭代次数,指定成一个值,达到了这样的一个值,就完成了更新.

是看一下损失函数的目标函数的变化,在迭代的过程中,可以知道迭代之前的目标函数,迭代完之后的目标函数,如果这两个目标函数差异非常小,几乎没有变化,迭代就可以停止下来.

根据梯度,如果前后两次迭代梯度的值差不多,几乎没啥变化,那么迭代也是可以停止下来的.

以上就是三种不同的梯度下降方法,不同的方法进行比较,看一下这三种算法得到的结果怎么样?不仅要看这三种停止策略,还要结合不同梯度下降,比如小批量的梯度下降/随机梯度下降/批量梯度下降.

首先定义一个函数,针对不同的三种停止策略,具体如下:

STOP_ITER = 0
STOP_COST = 1
STOP_GRAD = 2

def stopCriterion(type, value, threshold):
    #设定三种不同的停止策略
    if type == STOP_ITER:        return value > threshold
    elif type == STOP_COST:      return abs(value[-1]-value[-2]) < threshold
    elif type == STOP_GRAD:      return np.linalg.norm(value) < threshold

在做迭代更新的时候,首先对数据进行洗牌,洗牌是因为现在的数据看起来是有规律的,因为数据是我们自己进行收集的,收集的过程中可能按照某种顺序.比如身高排序,比如先收集男生的再收集女生的.

为了使模型的泛化能力更强,第一步先把数据进行打乱,做一个乱序的数据,通常把这种做法叫做洗牌,也叫shuffle操作,numpy有提供这样的操作.

通过numpy中的random模块,也就是随机模块,有shuffle函数,将数据传进去就可以进行洗牌.洗牌之后,把数据重新的指定好,然后再重新指定对应的x,y.

import numpy.random
#洗牌
def shuffleData(data):
    np.random.shuffle(data)
    cols = data.shape[1]
    x = data[:, 0:cols-1]
    y = data[:, cols-1:]
    return x, y

之前有说过,不同梯度下降,所消耗的时间也是不一样的.这里还要看一下时间对结果的影响.这里通过定义descent函数进行.

import time

def descent(data, theta, batchSize, stopType, thresh, alpha):
    #梯度下降求解
    
    init_time = time.time()
    i = 0 # 迭代次数
    k = 0 # batch
    x, y = shuffleData(data)
    grad = np.zeros(theta.shape) # 计算的梯度
    costs = [cost(x, y, theta)] # 损失值

    while True:
        grad = gradient(x[k:k+batchSize], y[k:k+batchSize], theta)
        k += batchSize #取batch数量个数据
        if k >= n: 
            k = 0 
            x, y = shuffleData(data) #重新洗牌
        theta = theta - alpha*grad # 参数更新
        costs.append(cost(x, y, theta)) # 计算新的损失
        i += 1 

        if stopType == STOP_ITER:       value = i
        elif stopType == STOP_COST:     value = costs
        elif stopType == STOP_GRAD:     value = grad
        if stopCriterion(stopType, value, thresh): break
    
    return theta, i-1, costs, grad, time.time() - init_time

为了使画图的时候知道当前迭代多少次,以及停止策略以及batch的一个结果,就定义了一个功能性的一个函数runExpe().

    name = "Original" if (data[:,1]>2).sum() > 1 else "Scaled"
    name += " data - learning rate: {} - ".format(alpha)
    if batchSize==n: strDescType = "Gradient"
    elif batchSize==1:  strDescType = "Stochastic"
    else: strDescType = "Mini-batch ({})".format(batchSize)
    name += strDescType + " descent - Stop: "
    if stopType == STOP_ITER: strStop = "{} iterations".format(thresh)
    elif stopType == STOP_COST: strStop = "costs change < {}".format(thresh)
    else: strStop = "gradient norm < {}".format(thresh)
    name += strStop
 fig, ax = plt.subplots(figsize=(12,4))
    ax.plot(np.arange(len(costs)), costs, 'r')
    ax.set_xlabel('Iterations')
    ax.set_ylabel('Cost')
    ax.set_title(name.upper() + ' - Error vs. Iteration')
def runExpe(data, theta, batchSize, stopType, thresh, alpha):
    #import pdb; pdb.set_trace();
    theta, iter, costs, grad, dur = descent(data, theta, batchSize, stopType, thresh, alpha)
    name = "Original" if (data[:,1]>2).sum() > 1 else "Scaled"
    name += " data - learning rate: {} - ".format(alpha)
    if batchSize==n: strDescType = "Gradient"
    elif batchSize==1:  strDescType = "Stochastic"
    else: strDescType = "Mini-batch ({})".format(batchSize)
    name += strDescType + " descent - Stop: "
    if stopType == STOP_ITER: strStop = "{} iterations".format(thresh)
    elif stopType == STOP_COST: strStop = "costs change < {}".format(thresh)
    else: strStop = "gradient norm < {}".format(thresh)
    name += strStop
    print ("***{}\nTheta: {} - Iter: {} - Last cost: {:03.2f} - Duration: {:03.2f}s".format(
        name, theta, iter, costs[-1], dur))
    fig, ax = plt.subplots(figsize=(12,4))
    ax.plot(np.arange(len(costs)), costs, 'r')
    ax.set_xlabel('Iterations')
    ax.set_ylabel('Cost')
    ax.set_title(name.upper() + ' - Error vs. Iteration')
    return theta

当n值指定为100的时候,相当于整体对于梯度下降,为什么呢?因为我的数据样本就100个.
传进来的数据是按照迭代次数进行停止的,指定迭代次数的参数是thresh=5000.学习率是alpha=0.000001.

n=100
runExpe(orig_data, theta, n, STOP_ITER, thresh=5000, alpha=0.000001)

以下是正常模型收敛的一个图的样子:


x轴是我的迭代次数,y轴是我的目标函数,随着迭代,目标函数进行一个收敛.5000次之后最终得到一个结果.共用了0.18s,完成了一个整体的迭代,速度还是比较快的,因为数据量比较小.

完整的代码如下:

import numpy as np
import pandas as pd 
import time
import matplotlib.pyplot as plt

pdData=pd.read_csv("LogiReg_data.txt",header=None,names=['Exam 1', 'Exam 2', 'Admitted'])

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

def model(x,theta):
    return sigmoid(np.dot(x,theta.T))

def cost(x,y,theta):
    left=np.multiply(-y,np.log(model(x,theta)))
    right=np.multiply(1-y,np.log(1-model(x,theta)))
    return np.sum(left-right)/(len(x))

def gradient(x,y,theta):
    grad=np.zeros(theta.shape)
    error=(model(x,theta)-y).ravel()
    for j in range(len(theta.ravel())):
        term=np.multiply(error,x[:,j])
        grad[0,j]=np.sum(term)/len(x)
    return grad[0,j]

STOP_ITER = 0
STOP_COST = 1
STOP_GRAD = 2
def stopCriterion(type, value, threshold):
    #设定三种不同的停止策略
    if type == STOP_ITER:        return value > threshold
    elif type == STOP_COST:      return abs(value[-1]-value[-2]) < threshold
    elif type == STOP_GRAD:      return np.linalg.norm(value) < threshold

def shuffleData(data):
    np.random.shuffle(data)
    cols = data.shape[1]
    x = data[:, 0:cols-1]
    y = data[:, cols-1:]
    return x, y

def descent(data, theta, batchSize, stopType, thresh, alpha):
    #梯度下降求解
    
    init_time = time.time()
    i = 0 # 迭代次数
    k = 0 # batch
    x, y = shuffleData(data)
    grad = np.zeros(theta.shape) # 计算的梯度
    costs = [cost(x, y, theta)] # 损失值

    while True:
        grad = gradient(x[k:k+batchSize], y[k:k+batchSize], theta)
        k += batchSize #取batch数量个数据
        if k >= n: 
            k = 0 
            x, y = shuffleData(data) #重新洗牌
        theta = theta - alpha*grad # 参数更新
        costs.append(cost(x, y, theta)) # 计算新的损失
        i += 1 

        if stopType == STOP_ITER:       value = i
        elif stopType == STOP_COST:     value = costs
        elif stopType == STOP_GRAD:     value = grad
        if stopCriterion(stopType, value, thresh): break
    
    return theta, i-1, costs, grad, time.time() - init_time

def runExpe(data, theta, batchSize, stopType, thresh, alpha):
    #import pdb; pdb.set_trace();
    theta, iter, costs, grad, dur = descent(data, theta, batchSize, stopType, thresh, alpha)
    name = "Original" if (data[:,1]>2).sum() > 1 else "Scaled"
    name += " data - learning rate: {} - ".format(alpha)
    if batchSize==n: strDescType = "Gradient"
    elif batchSize==1:  strDescType = "Stochastic"
    else: strDescType = "Mini-batch ({})".format(batchSize)
    name += strDescType + " descent - Stop: "
    if stopType == STOP_ITER: strStop = "{} iterations".format(thresh)
    elif stopType == STOP_COST: strStop = "costs change < {}".format(thresh)
    else: strStop = "gradient norm < {}".format(thresh)
    name += strStop
    print ("***{}\nTheta: {} - Iter: {} - Last cost: {:03.2f} - Duration: {:03.2f}s".format(
        name, theta, iter, costs[-1], dur))
    fig, ax = plt.subplots(figsize=(12,4))
    ax.plot(np.arange(len(costs)), costs, 'r')
    ax.set_xlabel('Iterations')
    ax.set_ylabel('Cost')
    ax.set_title(name.upper() + ' - Error vs. Iteration')
    return theta



pdData.insert(0, 'Ones', 1)
orig_data=pdData.as_matrix()
cols=orig_data.shape[1]
x=orig_data[:,0:cols-1]#1-倒数第1列的数据
y=orig_data[:,cols-1:cols]#倒数第1列的数据
theta=np.zeros([1,3])#1行三列的矩阵全部填充为0
n=100
runExpe(orig_data, theta, n, STOP_ITER, thresh=5000, alpha=0.000001)

停止策略

1)设定迭代次数停止策略:设定迭代次数
如果不认为的指定迭代次数,经过多少次的迭代可以达到阈值.

n=100
runExpe(orig_data, theta, n, STOP_ITER, thresh=5000, alpha=0.000001)

更改为

n=100
runExpe(orig_data, theta, n, STOP_ITER, thresh=0.0000001, alpha=0.001)

输出结果为


之前是迭代次数达到5000次接近0.63,现在的迭代次数达到了11万次接近0.37.只要改变迭代策略的时候,相当于不手动改变迭代次数,而是通过计算机自己计算,能够满足要求就自动停止下来,而自动停止共花了24.47s.

2)根据损失值停止策略:设定阈值 1E-6, 差不多需要110 000次迭代

当梯度值<阈值的时候,进行停止.同样的实验,可以得到我们当前的结果.

runExpe(orig_data, theta, n, STOP_COST, thresh=0.000001, alpha=0.001)

3)根据梯度变化停止:设定阈值 0.05,差不多需要40 000次迭代

runExpe(orig_data, theta, n, STOP_GRAD, thresh=0.05, alpha=0.001)
对比不同的梯度下降方法

1)随机梯度下降

这里的1是当前只迭代一个样本.损失值上上下下,跳转非常厉害.这样的问题应该怎么进行一个改变呢?迭代5000次的效果并不怎么好,但是时间非常快.

runExpe(orig_data, theta, 1, STOP_ITER, thresh=5000, alpha=0.001)

有点爆炸。。。很不稳定,再来试试把学习率调小一些..

runExpe(orig_data, theta, 1, STOP_ITER, thresh=15000, alpha=0.000002)

从输出的结果来看已经接近于收敛状态了.收敛效果比较低是接近于0.63,因此收敛结果也不是很满意.

速度快,但稳定性差,需要很小的学习率

2)小批量梯度下降

原来样本是100,现在改成16.

runExpe(orig_data, theta, 16, STOP_ITER, thresh=15000, alpha=0.001)

学习率比较小,但是也有一些问题,浮动也很大,这个问题应该怎么解决呢?不再把学习率调小一些,而是换一种方案.

浮动仍然比较大,我们来尝试下对数据进行标准化 将数据按其属性(按列进行)减去其均值,然后除以其方差。最后得到的结果是,对每个属性/每列来说所有数据都聚集在0附近,方差值为1.(备注:数据标准化,后面有专题)

from sklearn import preprocessing as pp

scaled_data = orig_data.copy()
scaled_data[:, 1:3] = pp.scale(orig_data[:, 1:3])
n=100
runExpe(scaled_data, theta, n, STOP_ITER, thresh=5000, alpha=0.001)

从输出结果来看,它好多了!原始数据,只能达到达到0.61,而我们得到了0.38个在这里! 所以对数据做预处理是非常重要的.

runExpe(scaled_data, theta, n, STOP_GRAD, thresh=0.02, alpha=0.001)

更多的迭代次数会使得损失下降的更多!

theta = runExpe(scaled_data, theta, 1, STOP_GRAD, thresh=0.002/5, alpha=0.001)

随机梯度下降更快,但是我们需要迭代的次数也需要更多,所以还是用batch的比较合适!!!

runExpe(scaled_data, theta, 16, STOP_GRAD, thresh=0.002*2, alpha=0.001)

对于概率值想得到一个类别值,那怎么样把概率值转换成类别值呢?
比如>0.5认为是≤0.5认为是0,这个概率值是可以自己自定义.
比如这里>0.5认为可以被录取,≤0.5认为这个人不能被录取.

def predict(X, theta):
    return [1 if x >= 0.5 else 0 for x in model(X, theta)]

阈值设置之后,就要设置精度,在当前数据当中,认为是自己对了多少个?错了多少个?100个数据当中,一共对了89个,最终的结果是等于89%.通过梯度下降,梯度回归,最终算出精度是等于89%.

scaled_X = scaled_data[:, :3]
y = scaled_data[:, 3]
predictions = predict(scaled_X, theta)
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(predictions, y)]
accuracy = (sum(map(int, correct)) % len(correct))
print ('accuracy = {0}%'.format(accuracy))
上一篇下一篇

猜你喜欢

热点阅读