李航《统计学习方法》第2版 第6章 Python编程逻辑斯谛回归与最大熵模型 实现mnist数据集分类

(参考深度之眼dodo老师代码,代码注释中有老师博客地址)


逻辑斯谛回归模型(学习算法:梯度下降)


# coding=utf-8
# Author:Dodo
# Date:2018-11-27
# Email:[email protected]
# Blog:www.pkudodo.com

'''
数据集:Mnist
训练集数量:60000
测试集数量:10000
------------------------------
运行结果:
    正确率:98.91%
    运行时长:59s
'''

import time
import numpy as np


def loadData(fileName):
    '''
    加载Mnist数据集
    :param fileName:要加载的数据集路径
    :return: list形式的数据集及标记
    '''
    # 存放数据及标记的list
    dataList = []; labelList = []
    # 打开文件
    fr = open(fileName, 'r')
    # 将文件按行读取
    for line in fr.readlines():
        # 对每一行数据按切割福','进行切割,返回字段列表
        curLine = line.strip().split(',')

        # Mnsit有0-9是个标记,由于是二分类任务,所以将标记0的作为1,其余为0
        # 验证过<5为1 >5为0时正确率在90%左右,猜测是因为数多了以后,可能不同数的特征较乱,不能有效地计算出一个合理的超平面
        # 查看了一下之前感知机的结果,以5为分界时正确率81,重新修改为0和其余数时正确率98.91%
        # 看来如果样本标签比较杂的话,对于是否能有效地划分超平面确实存在很大影响
        if int(curLine[0]) == 0:
            labelList.append(1)
        else:
            labelList.append(0)
        #存放标记
        #[int(num) for num in curLine[1:]] -> 遍历每一行中除了以第一哥元素(标记)外将所有元素转换成int类型
        #[int(num)/255 for num in curLine[1:]] -> 将所有数据除255归一化(非必须步骤,可以不归一化)
        dataList.append([int(num)/255 for num in curLine[1:]])
        # dataList.append([int(num) for num in curLine[1:]])

    #返回data和label
    return dataList, labelList

def predict(w, x):
    '''
    预测标签
    :param w:训练过程中学到的w
    :param x: 要预测的样本
    :return: 预测结果
    '''
    #dot为两个向量的点积操作,计算得到w * x
    wx = np.dot(w, x)
    #计算标签为1的概率
    #该公式参考“6.1.2 二项逻辑斯蒂回归模型”中的式6.5
    P1 = np.exp(wx) / (1 + np.exp(wx))
    #如果为1的概率大于0.5,返回1
    if P1 >= 0.5:
        return 1
    #否则返回0
    return 0

def logisticRegression(trainDataList, trainLabelList, iter = 200):
    '''
    逻辑斯蒂回归训练过程
    :param trainDataList:训练集
    :param trainLabelList: 标签集
    :param iter: 迭代次数
    :return: 习得的w
    '''
    #按照书本“6.1.2 二项逻辑斯蒂回归模型”中式6.5的规则,将w与b合在一起,
    #此时x也需要添加一维,数值为1
    #循环遍历每一个样本,并在其最后添加一个1
    for i in range(len(trainDataList)):
        trainDataList[i].append(1)

    #将数据集由列表转换为数组形式,主要是后期涉及到向量的运算,统一转换成数组形式比较方便
    trainDataList = np.array(trainDataList)
    #初始化w,维数为样本x维数+1,+1的那一位是b,初始为0
    w = np.zeros(trainDataList.shape[1])

    #设置步长
    h = 0.001

    #迭代iter次进行随机梯度下降
    for i in range(iter):
        #每次迭代冲遍历一次所有样本,进行随机梯度下降
        for j in range(trainDataList.shape[0]):
            #随机梯度上升部分
            #在“6.1.3 模型参数估计”一章中给出了似然函数,我们需要极大化似然函数
            #但是似然函数由于有求和项,并不能直接对w求导得出最优w,所以针对似然函数求和
            #部分中每一项进行单独地求导w,得到针对该样本的梯度,并进行梯度上升(因为是
            #要求似然函数的极大值,所以是梯度上升,如果是极小值就梯度下降。梯度上升是
            #加号,下降是减号)
            #求和式中每一项单独对w求导结果为:xi * yi - (exp(w * xi) * xi) / (1 + exp(w * xi))
            #如果对于该求导式有疑问可查看我的博客 www.pkudodo.com

            #计算w * xi,因为后式中要计算两次该值,为了节约时间这里提前算出
            #其实也可直接算出exp(wx),为了读者能看得方便一点就这么写了,包括yi和xi都提前列出了
            wx = np.dot(w, trainDataList[j])
            yi = trainLabelList[j]
            xi = trainDataList[j]
            #梯度上升
            w +=  h * (xi * yi - (np.exp(wx) * xi) / ( 1 + np.exp(wx)))

    #返回学到的w
    return w

def model_test(testDataList, testLabelList, w):
    '''
    验证
    :param testDataList:测试集
    :param testLabelList: 测试集标签
    :param w: 训练过程中学到的w
    :return: 正确率
    '''

    #与训练过程一致,先将所有的样本添加一维,值为1,理由请查看训练函数
    for i in range(len(testDataList)):
        testDataList[i].append(1)

    #错误值计数
    errorCnt = 0
    #对于测试集中每一个测试样本进行验证
    for i in range(len(testDataList)):
        #如果标记与预测不一致,错误值加1
        if testLabelList[i] != predict(w, testDataList[i]):
            errorCnt += 1
    #返回准确率
    return 1 - errorCnt / len(testDataList)



if __name__ == '__main__':
    start = time.time()

    # 获取训练集及标签
    print('start read transSet')
    trainData, trainLabel = loadData('../Mnist/mnist_train.csv')

    # 获取测试集及标签
    print('start read testSet')
    testData, testLabel = loadData('../Mnist/mnist_test.csv')

    # 开始训练,学习w
    print('start to train')
    w = logisticRegression(trainData, trainLabel)

    #验证正确率
    print('start to test')
    accuracy = model_test(testData, testLabel, w)

    # 打印准确率
    print('the accuracy is:', accuracy)
    # 打印时间
    print('time span:', time.time() - start)



最大熵模型


# coding=utf-8
# Author:Dodo
# Date:2018-11-30
# Email:[email protected]
# Blog:www.pkudodo.com

'''
数据集:Mnist
训练集数量:60000(实际使用:20000)
测试集数量:10000
------------------------------
运行结果:
    正确率:96.9%
    运行时长:8.8h

备注:对于mnist而言,李航的统计学习方法中有一些关键细节没有阐述,
建议先阅读我的个人博客,其中有详细阐述。阅读结束后再看该程序。
Blog:www.pkudodo.com
'''

import time
import numpy as np
from collections import defaultdict

def loadData(fileName):
    '''
    加载Mnist数据集
    :param fileName:要加载的数据集路径
    :return: list形式的数据集及标记
    '''
    # 存放数据及标记的list
    dataList = []; labelList = []
    # 打开文件
    fr = open(fileName, 'r')
    # 将文件按行读取
    for line in fr.readlines():
        # 对每一行数据按切割福','进行切割,返回字段列表
        curLine = line.strip().split(',')
        #二分类,list中放置标签
        if int(curLine[0]) == 0:
            labelList.append(1)
        else:
            labelList.append(0)
        #二值化
        dataList.append([int(int(num) > 128) for num in curLine[1:]])

    #返回data和label
    return dataList, labelList


class maxEnt:
    '''
    最大熵类
    '''
    def __init__(self, trainDataList, trainLabelList, testDataList, testLabelList):
        '''
        各参数初始化
        '''
        self.trainDataList = trainDataList          #训练数据集
        self.trainLabelList = trainLabelList        #训练标签集
        self.testDataList = testDataList            #测试数据集
        self.testLabelList = testLabelList          #测试标签集
        self.featureNum = len(trainDataList[0])     #特征数量

        self.N = len(trainDataList)                 #总训练集长度
        self.n = 0                                  #训练集中(xi,y)对数量
        self.M = 10000                              #
        self.fixy = self.calc_fixy()                #所有(x, y)对出现的次数
        self.w = [0] * self.n                       #Pw(y|x)中的w
        self.xy2idDict, self.id2xyDict = self.createSearchDict()        #(x, y)->id和id->(x, y)的搜索字典
        self.Ep_xy = self.calcEp_xy()               #Ep_xy期望值

    def calcEpxy(self):
        '''
        计算特征函数f(x, y)关于模型P(Y|X)与经验分布P_(X, Y)的期望值(P后带下划线“_”表示P上方的横线
        程序中部分下划线表示“|”,部分表示上方横线,请根据具体公式自行判断,)
        即“6.2.2 最大熵模型的定义”中第二个期望(83页最上方的期望)
        :return:
        '''
        #初始化期望存放列表,对于每一个xy对都有一个期望
        #这里的x是单个的特征,不是一个样本的全部特征。例如x={x1,x2,x3.....,xk},实际上是(x1,y),(x2,y),。。。
        #但是在存放过程中需要将不同特诊的分开存放,李航的书可能是为了公式的泛化性高一点,所以没有对这部分提及
        #具体可以看我的博客,里面有详细介绍  www.pkudodo.com
        Epxy = [0] * self.n
        #对于每一个样本进行遍历
        for i in range(self.N):
            #初始化公式中的P(y|x)列表
            Pwxy = [0] * 2
            #计算P(y = 0 } X)
            #注:程序中X表示是一个样本的全部特征,x表示单个特征,这里是全部特征的一个样本
            Pwxy[0] = self.calcPwy_x(self.trainDataList[i], 0)
            #计算P(y = 1 } X)
            Pwxy[1] = self.calcPwy_x(self.trainDataList[i], 1)

            for feature in range(self.featureNum):
                for y in range(2):
                    if (self.trainDataList[i][feature], y) in self.fixy[feature]:
                        id = self.xy2idDict[feature][(self.trainDataList[i][feature], y)]
                        Epxy[id] += (1 / self.N) * Pwxy[y]
        return Epxy

    def calcEp_xy(self):
        '''
        计算特征函数f(x, y)关于经验分布P_(x, y)的期望值(下划线表示P上方的横线,
        同理Ep_xy中的“_”也表示p上方的横线)
        即“6.2.2 最大熵的定义”中第一个期望(82页最下方那个式子)
        :return: 计算得到的Ep_xy
        '''
        #初始化Ep_xy列表,长度为n
        Ep_xy = [0] * self.n

        #遍历每一个特征
        for feature in range(self.featureNum):
            #遍历每个特征中的(x, y)对
            for (x, y) in self.fixy[feature]:
                #获得其id
                id = self.xy2idDict[feature][(x, y)]
                #将计算得到的Ep_xy写入对应的位置中
                #fixy中存放所有对在训练集中出现过的次数,处于训练集总长度N就是概率了
                Ep_xy[id] = self.fixy[feature][(x, y)] / self.N

        #返回期望
        return Ep_xy

    def createSearchDict(self):
        '''
        创建查询字典
        xy2idDict:通过(x,y)对找到其id,所有出现过的xy对都有一个id
        id2xyDict:通过id找到对应的(x,y)对
        '''
        #设置xy搜多id字典
        #这里的x指的是单个的特征,而不是某个样本,因此将特征存入字典时也需要存入这是第几个特征
        #这一信息,这是为了后续的方便,否则会乱套。
        #比如说一个样本X = (0, 1, 1) label =(1)
        #生成的标签对有(0, 1), (1, 1), (1, 1),三个(x,y)对并不能判断属于哪个特征的,后续就没法往下写
        #不可能通过(1, 1)就能找到对应的id,因为对于(1, 1),字典中有多重映射
        #所以在生成字典的时总共生成了特征数个字典,例如在mnist中样本有784维特征,所以生成784个字典,属于
        #不同特征的xy存入不同特征内的字典中,使其不会混淆
        xy2idDict = [{
    
    } for i in range(self.featureNum)]
        #初始化id到xy对的字典。因为id与(x,y)的指向是唯一的,所以可以使用一个字典
        id2xyDict = {
    
    }

        #设置缩影,其实就是最后的id
        index = 0
        #对特征进行遍历
        for feature in range(self.featureNum):
            #对出现过的每一个(x, y)对进行遍历
            #fixy:内部存放特征数目个字典,对于遍历的每一个特征,单独读取对应字典内的(x, y)对
            for (x, y) in self.fixy[feature]:
                #将该(x, y)对存入字典中,要注意存入时通过[feature]指定了存入哪个特征内部的字典
                #同时将index作为该对的id号
                xy2idDict[feature][(x, y)] = index
                #同时在id->xy字典中写入id号,val为(x, y)对
                id2xyDict[index] = (x, y)
                #id加一
                index += 1

        #返回创建的两个字典
        return xy2idDict, id2xyDict


    def calc_fixy(self):
        '''
        计算(x, y)在训练集中出现过的次数
        :return:
        '''
        #建立特征数目个字典,属于不同特征的(x, y)对存入不同的字典中,保证不被混淆
        fixyDict = [defaultdict(int) for i in range(self.featureNum)]
        #遍历训练集中所有样本
        for i in range(len(self.trainDataList)):
            #遍历样本中所有特征
            for j in range(self.featureNum):
                #将出现过的(x, y)对放入字典中并计数值加1
                fixyDict[j][(self.trainDataList[i][j], self.trainLabelList[i])] += 1
        #对整个大字典进行计数,判断去重后还有多少(x, y)对,写入n
        for i in fixyDict:
            self.n += len(i)
        #返回大字典
        return fixyDict


    def calcPwy_x(self, X, y):
        '''
        计算“6.23 最大熵模型的学习” 式6.22
        :param X: 要计算的样本X(一个包含全部特征的样本)
        :param y: 该样本的标签
        :return: 计算得到的Pw(Y|X)
        '''
        #分子
        numerator = 0
        #分母
        Z = 0
        #对每个特征进行遍历
        for i in range(self.featureNum):
            #如果该(xi,y)对在训练集中出现过
            if (X[i], y) in self.xy2idDict[i]:
                #在xy->id字典中指定当前特征i,以及(x, y)对:(X[i], y),读取其id
                index = self.xy2idDict[i][(X[i], y)]
                #分子是wi和fi(x,y)的连乘再求和,最后指数
                #由于当(x, y)存在时fi(x,y)为1,因为xy对肯定存在,所以直接就是1
                #对于分子来说,就是n个wi累加,最后再指数就可以了
                #因为有n个w,所以通过id将w与xy绑定,前文的两个搜索字典中的id就是用在这里
                numerator += self.w[index]
            #同时计算其他一种标签y时候的分子,下面的z并不是全部的分母,再加上上式的分子以后
            #才是完整的分母,即z = z + numerator
            if (X[i], 1-y) in self.xy2idDict[i]:
                #原理与上式相同
                index = self.xy2idDict[i][(X[i], 1-y)]
                Z += self.w[index]
        #计算分子的指数
        numerator = np.exp(numerator)
        #计算分母的z
        Z = np.exp(Z) + numerator
        #返回Pw(y|x)
        return numerator / Z


    def maxEntropyTrain(self, iter = 500):
        #设置迭代次数寻找最优解
        for i in range(iter):
            #单次迭代起始时间点
            iterStart = time.time()

            #计算“6.2.3 最大熵模型的学习”中的第二个期望(83页最上方哪个)
            Epxy = self.calcEpxy()

            #使用的是IIS,所以设置sigma列表
            sigmaList = [0] * self.n
            #对于所有的n进行一次遍历
            for j in range(self.n):
                #依据“6.3.1 改进的迭代尺度法” 式6.34计算
                sigmaList[j] = (1 / self.M) * np.log(self.Ep_xy[j] / Epxy[j])

            #按照算法6.1步骤二中的(b)更新w
            self.w = [self.w[i] + sigmaList[i] for i in range(self.n)]

            #单次迭代结束
            iterEnd = time.time()
            #打印运行时长信息
            print('iter:%d:%d, time:%d'%(i, iter, iterStart - iterEnd))

    def predict(self, X):
        '''
        预测标签
        :param X:要预测的样本
        :return: 预测值
        '''
        #因为y只有0和1,所有建立两个长度的概率列表
        result = [0] * 2
        #循环计算两个概率
        for i in range(2):
            #计算样本x的标签为i的概率
            result[i] = self.calcPwy_x(X, i)
        #返回标签
        #max(result):找到result中最大的那个概率值
        #result.index(max(result)):通过最大的那个概率值再找到其索引,索引是0就返回0,1就返回1
        return result.index(max(result))

    def test(self):
        '''
        对测试集进行测试
        :return:
        '''
        #错误值计数
        errorCnt = 0
        #对测试集中所有样本进行遍历
        for i in range(len(self.testDataList)):
            #预测该样本对应的标签
            result = self.predict(self.testDataList[i])
            #如果错误,计数值加1
            if result != self.testLabelList[i]:   errorCnt += 1
        #返回准确率
        return 1 - errorCnt / len(self.testDataList)

if __name__ == '__main__':
    start = time.time()

    # 获取训练集及标签
    print('start read transSet')
    trainData, trainLabel = loadData('../Mnist/mnist_train.csv')

    # 获取测试集及标签
    print('start read testSet')
    testData, testLabel = loadData('../Mnist/mnist_test.csv')

    #初始化最大熵类
    maxEnt = maxEnt(trainData[:20000], trainLabel[:20000], testData, testLabel)

    #开始训练
    print('start to train')
    maxEnt.maxEntropyTrain()

    #开始测试
    print('start to test')
    accuracy = maxEnt.test()
    print('the accuracy is:', accuracy)

    # 打印时间
    print('time span:', time.time() - start)

Mnist数据集(csv格式)
链接:https://pan.baidu.com/s/1i3K61t-NqWuAchedGTAUPA
提取码:0i1e
复制这段内容后打开百度网盘手机App,操作更方便哦

猜你喜欢

转载自blog.csdn.net/weixin_43646592/article/details/109755470