1.题目

选择两个 UCI 数据集,比较 10 折交叉验证法和留一法所估计出的对率回归的错误率。(本文就对一个UCI数据集用两种评估方法实现了,毕竟再对另一个数据集的实现方法是一样的)

2.下载UCI数据集

导入数据集的方法有很多,可以直接从官网下载数据集文件,也从keras库里直接导入,本文使用第一种方法。 首先,进入UCI官网:https://archive.ics.uci.edu/ml/ 在UCI主页下的 Most Popular Data Sets 能看见由很多常用数据集,包含最常用的鸢(yuan)尾花、酒、车等等,本文就使用最经典的鸢尾花了。 点击IRIS图标进入鸢尾花数据集页面:

这里表格包含了一些比较重要的信息:

同时,下方包含了对属性和类别的说明。

像该数据集,用于分类任务,包含150条数据,每条数据含有4个属性,不存在数据缺失,拥有3个类别(也就是多分类任务)。 确认数据集是我们想要的后,点击Data Folder进入数据的文件目录。 然后,点击iris.data下载该数据集文件。

打开下载后的文件,可以看到,每条数据(对应每朵花)占一行,每行包含用逗号隔开的4个数字和1个字符串,每个数字对应一朵花的对应属性值,字符串表示该条数据属于的类别。

3.实现思路

3.1 评估方法

本题需要实现两种评估算法,分别为10折交叉验证和留一法。

首先,对于10折交叉验证,其思想是将数据集划分为10份(划几份就是几折),然后任取1份做测试集,另取9份做训练集,根据排列组合可知有10种取法,然后,对于每种取法,对学习器(本文使用对率回归模型)进行训练,然后用测试集测试,10轮后能得到10个正确率(注意每轮训练都是重新训练,也就是说,一共要训练和测试10次),最后将10个正确率取平均值当作最后该模型的正确率。

然后,对于留一法,简单来说就是m折交叉认证(m等于数据实例数),对于该数据集就是150折交叉验证,每轮留1个做测试集,其余149个做训练集,最后会得到150个准确率(当然,不是100%就是0%),然后取平均值作为模型正确率。

3.2 多分类方法

对于线性模型,多分类任务一般使用拆分策略,即将任务差分为多个二分类任务,拆分策略包括一对一(OvO)、一对多(OvR)和多对多(MvM)。本文将会使用OvR实现多分类。

这里讲一下OvO,该策略会将任务中的多个类别两两组合,然后对于每个组合,将其看作一个二分类任务(例如本题有3个类,有3种组合方法,需要训练三个二分类学习器),训练完成后,在预测时,只需要将测试实例输入到这些二分类学习器中,最后统计哪个类别结果多即可。

OvR则是将任务中的多个类别按照“一个类是正类,其余类都是负类”的规则划分,例如本例中包含三个花的类别:Iris-setosa、Iris-versicolor、Iris-virginica,先将Iris-setosa视为正例,然后将Iris-versicolor、Iris-virginica视为反例,这样就会形成一个二分类学习器,然后同理能一共得到三个二分类学习器,在预测时,只需要将测试实例分别输入到三个学习器中,观察三个学习器的结果,然后看哪个学习器输出正例,则认为该实例属于对应类别。而对于MvM,由于这里只有3个类别,使用MvM也会退化成OvR,所以无需使用了。

4. python实现

(1)读取数据集 将数据集文件按照行依次读取,每行按逗号分割,然后将用于表示类别的字符串转换为数字(三个类对应数字1,2,3)。

import numpy as np

import math

import random

# 读取UCI数据集

def read_uci(dir):

iris = []

with open(dir, 'r+') as f:

for line in f.readlines():

iris.append(line.split(','))

x = []

y = []

for iri in iris[:-1]:

name = iri[4]

x.append([float(xi) for xi in iri[:-1]])

if "setosa" in name:

y.append(1)

elif "versicolor" in name:

y.append(2)

elif "virginica" in name:

y.append(3)

return x,y

(2) 二分类实现 实现多分类学习器前,先要实现二分类学习器,本文使用对率回归模型,对对率回归的损失函数进行梯度下降,其中对率回归的损失函数为:

l

(

β

)

=

i

=

1

m

[

y

i

β

T

x

^

i

+

ln

(

e

β

T

x

^

i

+

1

)

]

\boldsymbol{l{}}(\boldsymbol{\beta}) = \sum_{i=1}^{m}[-y_{i}\beta^{T}\widehat{x}_{i}+\ln(e^{\beta^{T}\widehat{x}_{i}}+1)]

l(β)=i=1∑m​[−yi​βTx

i​+ln(eβTx

i​+1)] 梯度下降公式为:

β

t

+

1

=

β

t

s

l

(

β

)

β

\boldsymbol{\beta} ^{t+1}=\boldsymbol{\beta} ^{t}-s\frac{\partial \boldsymbol{l{}}(\boldsymbol{\beta})}{\partial \boldsymbol{\beta}}

βt+1=βt−s∂β∂l(β)​ 按照上述公式实现梯度下降即可。然后说一下超参数,这里对beta设置初始值为全1,d表示收敛的界限,就是当beta在下降过程中如果不超过0.01则表示已经收敛,就不用继续下降了,alpha表示步长(对应公式的s),n表示最大下降次数。这些值设置可以相对随意,然后经过测试可以找到一个比较好的参数,本文就测试了6、7次,按照最高正确率选出的参数。

def train_2(x:np.array, y:np.array):

beta = np.array([1.0,1.0,1.0,1.0,1.0]).T

d = 0.01 # 变化量小于d时表示收敛

alpha = 0.005

n = 500

for i in range(n):

dl_sum = 0

for i in range(np.shape(x)[0]):

xi_hat = np.r_[x[i], 1].T

ei = np.math.exp(beta.T.dot(xi_hat))

p1 = ei / (1 + ei)

dl = xi_hat.dot(y[i]-p1)

dl_sum -= dl

# print(dl_sum)

if np.all(dl_sum < d) and np.all(dl_sum > -d):

break

beta -= alpha * dl_sum;

return beta

(3)三分类学习器 二分类实现完,就可以实现三分类了,主要就是把y,也就是标签处理一下,这里第一个循环用于将1类看为正类,2、3类分为反类,以此类推,第二个循环是,2正,1、3反,第三个循环是,3正,1,2反。

def train_3(x, y):

y1 = []

for yi in y:

if yi == 1:

y1.append(1)

else:

y1.append(0)

beta1 = train_2(x, y1)

print("学习器1训练完成,参数:", beta1)

y2 = []

for yi in y:

if yi == 2:

y2.append(1)

else:

y2.append(0)

beta2 = train_2(x, y2)

print("学习器2训练完成,参数:", beta2)

y3 = []

for yi in y:

if yi == 3:

y3.append(1)

else:

y3.append(0)

beta3 = train_2(x, y3)

print("学习器3训练完成,参数:", beta3)

return beta1, beta2, beta3

(4)10折交叉验证 这里为了实现每折数据的类别比例都是1:1:1,将数据集先按照类别分开,然后在各自打乱,最后再分成10部分,取9部分训练1部分测试。

def cv10_devide_test(x, y):

sumn = len(x)

r_rate_sum = 0

x1 = []

x2 = []

x3 = []

for i in range(sumn):

if y[i] == 1:

x1.append(x[i])

elif y[i] == 2:

x2.append(x[i])

elif y[i] == 3:

x3.append(x[i])

random.shuffle(x1)

random.shuffle(x2)

random.shuffle(x3)

for i in range(10):

train_x = []

train_y = []

test_x = []

test_y = []

x1_delta = len(x1)/10

x2_delta = len(x2)/10

x3_delta = len(x3)/10

for j in range(len(x1)):

if j >= i*x1_delta and j < (i+1)*x1_delta:

test_x.append(x1[j])

test_y.append(1)

else:

train_x.append(x1[j])

train_y.append(1)

for j in range(len(x2)):

if j >= i*x2_delta and j < (i+1)*x2_delta:

test_x.append(x2[j])

test_y.append(2)

else:

train_x.append(x2[j])

train_y.append(2)

for j in range(len(x3)):

if j >= i*x3_delta and j < (i+1)*x3_delta:

test_x.append(x3[j])

test_y.append(3)

else:

train_x.append(x3[j])

train_y.append(3)

# print("train_x:", train_x)

# print("train_y:", train_y)

# print("test_x:", test_x)

# print("test_y:", test_y)

beta1, beta2, beta3 = train_3(train_x, train_y)

print(f"第{i+1}折训练完成")

r_rate = test(beta1, beta2, beta3, test_x, test_y)

r_rate_sum += r_rate

avg_r_rate = r_rate_sum/10

print("交叉验证平均正确率为:",avg_r_rate)

return train_x, train_y, test_x, test_y

(5)留一法 留一法与上面交叉验证一样,区别在于只留一个进行测试,所以要进行150次训练测试,运行时间会长一些。

def cv1_devide_test(x, y):

sumn = len(x)

r_rate_sum = 0

for i in range(sumn):

train_x = []

train_y = []

test_x = []

test_y = []

for j in range(sumn):

if j == i:

test_x.append(x[j])

test_y.append(y[j])

else:

train_x.append(x[j])

train_y.append(y[j])

beta1, beta2, beta3 = train_3(train_x, train_y)

r_rate = test(beta1, beta2, beta3, test_x, test_y)

r_rate_sum += r_rate

print(f"第{i+1}折训练完成")

avg_r_rate = r_rate_sum/sumn

print("留一法平均正确率为:",avg_r_rate)

return train_x, train_y, test_x, test_y

(6)其他函数

# sigmoid函数

def sigmoid(x):

if x<-500:

x = -500

return 1 / (1 + math.exp(-x))

# 判断两个浮点是是否相同

def float_equal(a, b):

delta = a-b

if delta > -1e-5 and delta < 1e-5:

return True

else:

return False

# 预测

def predict(beta, x):

y = 0

for i in range(len(x)):

y += x[i] * beta[i]

y += beta[len(x)]

return sigmoid(y)

# 测试

def test(beta1, beta2, beta3, test_x, test_y):

right = 0

for i in range(len(test_x)):

y1 = predict(beta1, test_x[i])

y2 = predict(beta2, test_x[i])

y3 = predict(beta3, test_x[i])

if y1 >= 0.5 and test_y[i] == 1:

right += 1

elif y2 >= 0.5 and test_y[i] == 2:

right += 1

elif y3 >= 0.5 and test_y[i] == 3:

right += 1

r_rate = right/len(test_x)

print("测试样例总数:", len(test_x))

print("通过样例数:", right)

print("正确率为:", r_rate)

return r_rate

(7)主程序

if __name__ == "__main__":

x,y = read_uci("./iris.data")

cv10_devide_test(x, y)

cv1_devide_test(x, y)

7. 结果

10折交叉验证结果: 留一法运行结果:

10折交叉验证准确率是86%,留一法是84%,但不代表交叉验证要好,因为在测试运行过程中,该程序的准确率是有浮动的,比如说前几次交叉验证会经常出现81%左右的结果,由于留一法需要运行时间比较长,我就只运行了一次。 所以如果要想得到严谨的比较结果,就可以多运行几个结果,然后再进行比较。

查看原文