第五章:进一步探索分类

训练集和测试集

通常我们将数据集分为两个部分,第一部分用来构造分类器,因此称为训练集;另一部分用来评估分类器的结果,因此称为测试集。

训练集和测试集在数据挖掘中很常用。

数据挖掘工程师不会用同一个数据集去训练和测试程序,因为如果使用训练集去测试分类器,得到的结果肯定是百分之百准确,所以这种做法不可取。

将数据集拆分成一大一小两个部分的做法就产生了,前者用来训练,否则用来测试。不过这种做法也有问题:如果分割的时候不凑巧,就会引发异常。

解决方法之一就是将数据集按不同的方式拆分,测试多次,取结果的平均值。比如我们将数据结构拆分为均等的两份:

我们可以先用第一部分做训练集,第二部分做测试集,然后再反过来,取两次测试的平均结果。我们还可以将数据集分成三份,用两个部分来做训练集,一个部分来做测试集,迭代三次:

  • 1.使用 Part 1 和 Part 2 训练,使用 Part 3 测试;

  • 2.使用 Part 1 和 Part 3 训练,使用 Part 2 测试;

  • 3.使用 Part 2 和 Part 3 训练,使用 Part 1 测试;

最后取三次测试的平均结果。

在数据挖掘中,通常的做法是将数据集拆分成十份,并按上述方式进行迭代测试。因此这种方式也成为——十折交叉验证。

十折交叉验证

第一步:将数据分成10份

第二步:重复以下步骤10次

    1. 每次迭代我们保留一个桶,比如第一次迭代保留木桶 1,第二次保留木桶 2。
    1. 我们使用剩余的 9 个桶来训练分类器,比如第一次迭代使用木桶 2 至 10 来训练。
    1. 我们用刚才保留的一个桶来进行测试,并记录结果,比如:35 个篮球运动员分类正确, 29 个普通人分类正确。

第三步:合并结果

留一法

在数据挖掘领域,N折交叉验证又称为留一法。

优点:

(1)我们用几乎所有的数据进行训练,然后用一个数据进行测试。

(2)确定性

缺点:

(1)计算时间很长

(2)分层问题

结论:留一法对小数据集是合适的,但大多数情况下我们会选择十折交叉验证。

混淆矩阵

混淆矩阵的对角线(绿色字体)表示正确的人数,因此求的准确率是:

代码实现

# -*- coding:utf-8 -*-

# 将数据等分成十份的示例代码

'''
Created on 2018年11月27日

@author: KingSley
'''

import random

def buckets(filename, bucketName, separator, classColumn):
    """
    filename 源文件名
    bucketName 十个目标文件的前缀名
    separator 分隔符,如制表符、逗号等
    classColumn 表示数据所属分类的那一列的序号
    """
    
    # 将数据分为 10 份
    numberOfBuckets = 10
    data = {}
    # 读取数据,并按分类放置
    with open(filename) as f:
        lines = f.readlines()
    for line in lines:
        if separator != '\t':
            line = line.replace(separator, '\t')
        # 获取分类
        category = line.split()[classColumn]
        data.setdefault(category, [])
        data[category].append(line)       
    # 初始化分桶
    buckets = []
    for i in range(numberOfBuckets):
        buckets.append([])       
    # 将各个类别的数据均匀地放置到桶中
    for k in data.keys():
        # 打乱分类顺序
        random.shuffle(data[k])
        bNum = 0
        # 分桶
        for item in data[k]:
            buckets[bNum].append(item)
            bNum = (bNum + 1) % numberOfBuckets
            
    # 写入文件
    for bNum in range(numberOfBuckets):
        f = open("%s-%02i" % (bucketName, bNum + 1), 'w')
        for item in buckets[bNum]:
            f.write(item)
        f.close()
  
# 调用示例      
buckets("pimaSmall.txt", 'pimaSmall',',',8)

Kappa指标

将对角线相加(35 + 88 + 28 = 151)除以合计(200)就可以了,结果是0.755。

首先,我们将上表中的数据抹去一部分,只留下合计:

真实的体操运动员一共有60人,随机分类器会将其中的20%(12人)分类为体操,50%(30人)分类为篮球,30%(18人)分类为马拉松,填入表格:

继续用这种方法填充空白。

100个真实的篮球运动员,20%(20人)分到体操,50%(50人)分到篮球,30%(30人)分到马拉松。

从而得到随机分类器的准确率是:

Kappa指标可以用来衡量我们之前构造的分类器和随机分类器的差异,公式为:


# -*- coding:utf-8 -*-

# 十折交叉验证

'''
Created on 2018年11月27日

@author: KingSley
'''

from dataclasses import fields

class Classifier:
    def __init__(self, bucketPrefix, testBucketNumber, dataFormat):
        """该分类器程序将从 bucketPrefix 指定的一系列文件中读取数据,
        并留出 testBucketNumber 指定的桶来做测试集,其余的做训练集。
        dataFormat 用来表示数据的格式,如:
        "class num num num num num comment"
        """
        
        self.medianAndDeviation = []
        
        # 从文件中读取文件

        self.format = dataFormat.strip().split('\t')
        self.data = []
        # 用 1-10 来标记桶
        for i in range(1, 11):
            # 判断该桶时候包含在训练集中
            if i != testBucketNumber:
                filename = "%s-%02i" % (bucketPrefix, i)
                f = open(filename)
                lines = f.readlines()
                f.close()
                for line in lines[1:]:
                    fields = line.strip().split('\t')
                    ignore = []
                    vector = []
                    for i in range(len(fields)):
                        if self.format[i] == 'num':
                            vector.append(float(fields[i]))
                        elif self.format[i] == 'comment':
                            ignore.append(fields[i])
                        elif self.format[i] == 'class':
                            classification = fields[i]
                    self.data.append((classification, vector, ignore))
        self.rawData = list(self.data)
        # 获取特征向量的长度
        self.vlen = len(self.data[0][1])
        # 标准化数据
        for i in range(self.vlen):
            self.normalizeColumn(i)
            
    def getMedian(self, alist):
        """返回中位数"""
        if alist == []:
            return []
        blist = sorted(alist)
        length = len(alist)
        if length % 2 == 1:
            # 列表有奇数个元素,返回中间元素
            return blist[int(((length + 1) / 2) -  1)]
        else:
            # 列表有偶数个元素,返回总量两个元素的均值
            v1 = blist[int(length / 2)]
            v2 = blist[(int(length / 2) - 1)]
            return (v1 + v2) / 2.0
        
    def getAbsoluteStandardDeviation(self, alist, median):
        """计算绝对偏差"""
        sum = 0
        for item in alist:
            sum += abs(item - median)
        return sum / len(alist)
    
    def normalizeColumn(self, columnNumber):
        """标准化 self.data 中的 columnNumber 列"""
        # 将该列所有值提取到一个列表中
        col = [v[1][columnNumber] for v in self.data]
        median = self.getMedian(col)
        asd = self.getAbsoluteStandardDeviation(col, median)
        #print("Median: %f   ASD = %f" % (median, asd))
        self.medianAndDeviation.append((median, asd))
        for v in self.data:
            v[1][columnNumber] = (v[1][columnNumber] - median) / asd

    def normalizeVector(self, v):
        """对每列的中位数和绝对偏差,计算标准化向量 v"""
        vector = list(v)
        for i in range(len(vector)):
            (median, asd) = self.medianAndDeviation[i]
            vector[i] = (vector[i] - median) / asd
        return vector
    
    def testBucket(self, bucketPrefix, bucketNumber):
        """读取 bucketPrefix - bucketNumber 所指定的文件作为测试集"""
        
        filename = "%s-%02i" % (bucketPrefix, bucketNumber)
        f = open(filename)
        lines = f.readlines()
        totals = {}
        f.close()
        for line in lines:
            data = line.strip().split('\t')
            vector = []
            classInColumn = -1
            for i in range(len(self.format)):
                if self.format[i] == 'num':
                    vector.append(float(data[i]))
                elif self.format[i] == 'class':
                    classInColumn = i
            theRealClass = data[classInColumn]
            classifiedAs = self.classify(vector)
            totals.setdefault(theRealClass, {})
            totals[theRealClass].setdefault(classifiedAs, 0)
            totals[theRealClass][classifiedAs] += 1
        return totals
        
    def manhattan(self, vector1, vector2):
        """计算曼哈顿距离"""
        return sum(map(lambda v1, v2: abs(v1 - v2), vector1, vector2))
    
    def nearestNeighbor(self, itemVector):
        """返回 itemVector 的邻近"""
        return min([(self.manhattan(itemVector, item[1]), item) for item in self.data])
    
    def classify(self, itemVector):
        """预测 itemVector 的分类"""
        return self.nearestNeighbor(self.normalizeVector(itemVector))[1][0]
    

def tenfold(bucketPrefix, dataFormat):
    results = {}
    for i in range(1, 11):
        c = Classifier(bucketPrefix, i, dataFormat)
        t = c.testBucket(bucketPrefix, i)
        for (key, value) in t.items():
            results.setdefault(key, {})
            for (ckey, cvalue) in value.items():
                results[key].setdefault(ckey, 0)
                results[key][ckey] += cvalue
            
    # 输出结果
    categories = list(results.keys())
    categories.sort()
    print(   "\n       Classified as: ")
    header =    "        "
    subheader = "      +"
    for category in categories:
        header += category + "   "
        subheader += "----+"
    print (header)
    print (subheader)
    total = 0.0
    correct = 0.0
    for category in categories:
        row = category + "    |"
        for c2 in categories:
            if c2 in results[category]:
                count = results[category][c2]
            else:
                count = 0
            row += " %2i |" % count
            total += count
            if c2 == category:
                correct += count
        print(row)
    print(subheader)
    print("\n%5.3f percent correct" %((correct * 100) / total))
    print("total of %i instances" % total)
    
tenfold("mpgData\mpgData", "class\tnum\tnum\tnum\tnum\tnum\tcomment")

参考原文作者:Ron Zacharski CC BY-NC 3.0] https://github.com/egrcc/guidetodatamining

参考原文原文 http://guidetodatamining.com/

参考译文来自 @egrcchttps://github.com/egrcc/guidetodatamining

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,914评论 2 89
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,857评论 25 707
  • 昨天晚上在群里看到有讨论信仰的问题,我就回了一句有信仰是好事,起码有敬畏之心。后续大家就信仰和敬畏似乎展开了讨论,...
    洁_寞碎阅读 167评论 0 0
  • 无力吐槽,昨晚两把,队友实在不给力啊,逆天也坑的很。不玩了,这游戏不适合我。
    柒灬月流火阅读 112评论 0 0