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%左右的结果,由于留一法需要运行时间比较长,我就只运行了一次。 所以如果要想得到严谨的比较结果,就可以多运行几个结果,然后再进行比较。
发表评论