1 情景引入
假如百度作为人工智能公司与A公司进行合作,进行图像类别识别(比如通过给定的眼底视网膜图片,判断是否为病理性患者)。
A公司需要收集尽可能多的图像,打包后发给百度。百度就得到了训练的数据集,如果百度将数据集全部用于模型构造,而A公司无法段时间内再次提供新的图像数据,那么百度作出的模型就无法验证其性能,也就是分类准确率。无法知晓性能的模型是不会被任何公司接受的。因此,必须想办法在有限的数据集中,既做到训练模型,又能评估模型性能。
而在机器学习发展的历史长河中,已经较好的解决了上述问题,就是使用交叉验证方法,它是一种将数据样本切割成较小子集的方法。换句话说,可以先在一些子集上做分析, 而其它子集则用来对此分析进行确认及验证。其中, 一开始的子集被称为训练集。而其它的子集则被称为验证集或测试集。
下面介绍三种交叉验证方法的优劣以及Python代码实现(不依赖sklearn):
2-1 留出法
方法是:将原始数据分为两组,一组作为训练集,一组作为验证集。训练集用于训练模型,而验证集验证模型,而此时验证集的分类准确率将作为留出法下该模型的性能指标。
比如说,有1000个样本,假设给定的训练集、测试集的样本比例为0.7,则训练集样本数为700,测试集为300.
至此,仍然要注意一个问题,就是训练集和测试集的划分要尽可能保持数据分布的一致性,以避免因数据划分过程引入额外的偏差而对最终结果产生影响。换句话说,要保持样本的类别比例相似,而保留类别比例的采样方式又称为“分层采样”,比如说,拿我们的实际问题,识别手写字体,包含十个数字,每个数字的样本数大致在270左右,因此,划分数据集时,如果划分比例为0.7,则需要使得每个数字的样本在训练集和测试集中的数量大致在189和81.
保留法存在一这个缺点,或者说是窘境:
训练集用于训练模型,样本足够多,才能使模型提炼出足够多的特征,而验证集用于验证模型,验证集足够多,才能使评估结果稳定准确。但是原始数据的总量是不变的,训练集多了,验证集就不可能多,因此这是划分数据集的窘境。但是这个问题并没有完美的解决方案,只能尽可能完善。
常见的做法是,三分之二或者五分之四的样本用于训练,剩余样本用于测试。
另一个缺点是,留下的验证样本不能用于训练模型,这将使得被实际评估的模型与期望评估的用原始数据集训练出的模型相差较大。
2-2 交叉验证法
方法是:将数据集划分为k个大小相似的数据子集;然后每次将k-1个子集的并集作为训练集,余下的子集将作为验证集,如此循环,可以得到K组训练、验证集,从而可以进行k次训练和验证,最终返回的是这K个测试结果的均值。
显然,交叉验证法评估结果的稳定性和保真性在很大程度上取决于K的取值,而为了强调这一点,通常把交叉验证法称为“K折交叉验证”,而k的常用取值是10,此时称为10折交叉验证。
优点在于,因为综合来说,每个样本都曾用于建模,因此得到的准确率相对来说比较可信与稳定。
缺点在于,k值选取是否合理。作为超参数,需要一直调参。
2-3 留一法
假定数据集中包含了m个样本,若k=m,则得到了交叉验证法的一个特例,留一法。
针对每次训练与验证,只有一个样本用验证,这样,留一法训练使用的数据集与原始数据集相比只少一个样本,这也就使得留一法中实际评估的模型与期望评估的用原始数据集训练出的模型很相似。
因此,留一法的评估结果往往被认为比较准确。
而缺点就在于,当数据量巨大时,训练m个模型的计算开销可能十分巨大,而这还是在为考虑算法调整的情况下。
2-4 扩展
这门课是让大家入门机器学习,但是以上的三个方法也只是冰山一角:
需要注意的是data_lst 和 type_lst分别对应,样本的特征集合,shape为(n,m)n为训练集的样本数,m是每个样本的特征个数以及样本的标签集合,shape为(n,1),标签为一个类别值,无须多言。
1 留出法
# Holdout method
def my_train_test_split(X, y, train_size=0.6, shuffle=True):
"""
:param X:数据集的内容
:param y:数据集的标签
:param train_size:
:param shuffle:
:return:
"""
order = np.arange(len(X))
if shuffle:
order = np.random.permutation(order)
border = int(train_size * len(y))
train_index_lst = order[:border]
test_index_lst = order[border:]
return train_index_lst, test_index_lst
# 留出法的验证集。
score_lst = {}
for train_size in np.linspace(0.2,0.95,num=16):
i2 = 0
count2 = 0
train_index_lst, test_index_lst = my_train_test_split(data_lst, type_lst, train_size)
X_train, X_test = data_lst[train_index_lst], data_lst[test_index_lst]
Y_train, Y_test = type_lst[train_index_lst], type_lst[test_index_lst]
print(X_train.shape, X_test.shape)
for index, valid_arr in enumerate(X_test):
count2 += 1
testName = test_foronePng(valid_arr, X_train, Y_train, k, height, width)
if testName == Y_test[index]:
i2 += 1
score = round(i2/count2,4)
print(score,train_size)
score_lst[train_size] = score
key_lst = list(score_lst.keys())
value_lst = list(score_lst.values())
plt.figure(figsize=(10,10))
plt.plot(key_lst, value_lst)
for a, b in zip(key_lst, value_lst):
plt.text(a, b, (round(float(a*100))/100, b), ha='center', va='bottom', fontsize=15)
plt.show()
2 交叉验证法
# k folds 产生一个迭代器
def my_KFold(X, n_folds=10, shuffe=True):
"""
作为一个迭代器,产生N个索引组合
:param X:训练样本的数据集合,关键在于通过len函数获得样本的数目
:param n_folds:指出是几折交叉验证
:param shuffe:是否需要乱序后再进行分组。
:return:返回N组索引组合,每一个组合都指定了用于训练的样本的索引号以及用于测试的样本的索引号。
"""
order = np.arange(len(X))
# 对索引列表乱序排列
if shuffe:
order = np.random.permutation(order)
# 分为n_folds个组,
fold_sizes = (len(X) // n_folds) * np.ones(n_folds, dtype=np.int) # folds have size n // n_folds
fold_sizes[:len(X) % n_folds] += 1 # The first n % n_folds folds have size n // n_folds + 1
current = 0
for fold_size in fold_sizes:
start, stop = current, current + fold_size
train_index = list(np.concatenate((order[:start], order[stop:])))
# print(train_index)
test_index = list(order[start:stop])
yield train_index, test_index
current = stop # move one step forward
# 交叉验证法
# 用于标注目前第几组在进行测试
folds_order = 0
# 用于记录每组实验的准确率
score = 0
# 指出交叉验证的组数 一般默认是10 ,而如果k = m 则为留一法。
# 当留一法时,每一组的准确率不是0就是1。
n_folds = 2381
# 为了图像展示,构造一个记录实验第几组和相应组的准确率的字典。
score_dict = {}
# 调用方法,得到 n_folds 组训练测试集的索引。
kfold_lst = my_KFold(data_lst,n_folds=n_folds,shuffe=True)
# 循环遍历每一组训练测试集。计算每一组的准确率。
for train_index_lst, test_index_lst in kfold_lst:
folds_order += 1
count3 = 0
i3 = 0
X_train, X_test = data_lst[train_index_lst], data_lst[test_index_lst]
Y_train, Y_test = type_lst[train_index_lst], type_lst[test_index_lst]
print(X_train.shape, X_test.shape)
for index, valid_arr in enumerate(X_test):
count3 += 1
testName = test_foronePng(valid_arr, X_train, Y_train, k, height, width)
if testName == Y_test[index]:
i3 += 1
score_one = round(i3/count3,4)
print(folds_order, score_one)
score_dict[folds_order] = score_one
score += score_one
# 打印最终平均的准确率
print("final:",round(score/n_folds,4))
# 画图展示
key_lst = list(score_dict.keys())
value_lst = list(score_dict.values())
plt.figure(figsize=(20,5))
plt.scatter(key_lst, value_lst,s=2)
plt.show()
小问题1 小数在matplotlib中显示多位数的问题解决
参考文章python:float数乘以100后小数点位数变多问题
round(float(2.003*1000))/10
即先乘以10的倍数,float转化一下,再经过round函数转化为整数,最后除以10的倍数。
其中float和round的过程是不可获取的。
小问题2 交叉验证方法未能实现分层采样
KNN算法实现及其交叉验证
# 数据操作
import numpy as np
import pandas as pd
import operator
import matplotlib.pyplot as plt
# 读取图片的数据
from PIL import Image
# 目录文件路径的提取
import os
# 距离优化需要
import distance
import scipy.spatial.distance as dist
# 记录运行时间
import time
# 用于评估训练集的模型
from sklearn.model_selection import train_test_split
# 准备数据的工具
# 查看目录路径下的文件路径列表
def getObjectPath(path):
"""
input:目录的相对路径
output:文件的相对路径的列表
"""
return [os.path.join(path, f) for f in os.listdir(path)]
# 准备数据阶段的工具
# 对每一张图片预处理得到固定维数的array序列
def img2vector(imgFile, height, width):
"""
input : 文件的相对路径
output:归一化后的resize后的一维400列序列。
"""
# 转化为灰质图
img = Image.open(imgFile).convert('L')
# 转化大小,统一大小
img = img.resize((height, width), Image.BILINEAR) # print(testName,filename)
# 将二维数据转化为array类型,方便利用numpy的方法做运算,比如shape等。
img_arr = np.array(img)
# 二值处理
for indexi, i in enumerate(img_arr):
for indexj, j in enumerate(i):
if j != 255:
img_arr[indexi][indexj] = 1
else:
img_arr[indexi][indexj] = 0
# 平坦化处理
img_arr2 = np.reshape(img_arr, (-1)) # 400, 一维矩阵
return img_arr2
def O_Distances(data_lst, test_imgarr):
# 计算样本点到测试点的空间距离
diffMat = np.tile(test_imgarr, (data_lst.shape[0], 1)) - data_lst
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
o_distances = sqDistances ** 0.5
return o_distances
def filenameToType(filename):
convert_dict = {"00000": "一",
"00001": "丁",
"00002": "七",
"00003": "万",
"00004": "丈",
"00005": "三",
"00006": "上",
"00007": "下",
"00008": "不",
"00009": "与"}
dataType = convert_dict.get(filename)
return dataType
# Holdout method
def my_train_test_split(X, y, train_size=0.6, shuffle=True):
"""
:param X:数据集的内容
:param y:数据集的标签
:param train_size:
:param shuffle:
:return:
"""
order = np.arange(len(X))
if shuffle:
order = np.random.permutation(order)
border = int(train_size * len(y))
train_index_lst = order[:border]
test_index_lst = order[border:]
return train_index_lst, test_index_lst
# k folds 产生一个迭代器
def my_KFold(X, n_folds=10, shuffe=True):
"""
作为一个迭代器,产生N个索引组合
:param X:训练样本的数据集合,关键在于通过len函数获得样本的数目
:param n_folds:指出是几折交叉验证
:param shuffe:是否需要乱序后再进行分组。
:return:返回N组索引组合,每一个组合都指定了用于训练的样本的索引号以及用于测试的样本的索引号。
"""
order = np.arange(len(X))
# 对索引列表乱序排列
if shuffe:
order = np.random.permutation(order)
# 分为n_folds个组,
fold_sizes = (len(X) // n_folds) * np.ones(n_folds, dtype=np.int) # folds have size n // n_folds
fold_sizes[:len(X) % n_folds] += 1 # The first n % n_folds folds have size n // n_folds + 1
current = 0
for fold_size in fold_sizes:
start, stop = current, current + fold_size
train_index = list(np.concatenate((order[:start], order[stop:])))
# print(train_index)
test_index = list(order[start:stop])
yield train_index, test_index
current = stop # move one step forward
# 收集数据
def getArrDataSet(mainPath, height=20, width=20):
# 1 获取到样本的部分数据
filepaths = getObjectPath(mainPath)
pngName_lst = []
img_arr_lst = []
dict_filetree = {}
for eachFilePath in filepaths:
# 获得文件夹的名字 即类型
filename = os.path.basename(eachFilePath)
# dict_filetree用来记录每个文件夹下有那些文件,并对每个文件做一个标签,由文件夹名与文件名组成。
dict_filetree[filename] = []
# 获取文件夹下文件的列表
pngPaths = getObjectPath(eachFilePath)
for eachPngPath in pngPaths:
# 从文件路径中获得文件的名字
pngName = os.path.basename(eachPngPath)
# 改名字
new_pngName = filename + "_" + pngName.split(".")[0]
pngName_lst.append(new_pngName)
dict_filetree[filename].append(new_pngName)
# 顺便将各个文件序列化
img_arr = img2vector(eachPngPath, height, width)
img_arr_lst.append(img_arr)
assert len(pngName_lst) == len(img_arr_lst)
# 构建DataFrame
pngDataSets = pd.DataFrame({"type": [pngName_lst_one.split("_")[0] for pngName_lst_one in pngName_lst],
"data": img_arr_lst,
},index=pngName_lst)
return pngDataSets
# 测试函数
def test_foronePng(test_imgarr, data_lst, type_lst, k=4, height=20, width=20):
"""
input:标准的图片序列、训练集二维的array形式,训练集标签的一维array形式。
output:测试图片的预测类别。
"""
# 1欧式距离
o_distances = O_Distances(data_lst, test_imgarr)
sortedDistancesIndicies = o_distances.argsort()
classCount = {}
for i in range(k):
# 相同的,如果通过 pngDataSets.type[sortedDistancesIndicies[i]],即可得到前几个最小距离的样本的类型的列表。
voteIlabel = type_lst[sortedDistancesIndicies[i]]
# Python 字典(Dictionary) get() 函数返回指定键的值,如果值不在字典中返回默认值。
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# 对字典的值的列表, 作为排序的依据,得到一个由键值构成元组组成的列表
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# print(sortedClassCount)
return sortedClassCount[0][0]
if __name__ == '__main__':
# 指定参数
k = 4
height = 20
width = 20
i = 0 # 计算预测正确的个数
count = 0 # 计算测试样本的总数
# 1 获得样本序列化的数据集
# 通过getArrDataSet函数,而getArrDataSet函数的运行,需要借助getObjectPath和img2vector两个工具
pngDataSets = getArrDataSet(r'./train', 20, 20)
data_lst = []
for arr in pngDataSets.data:
data_lst.append(arr)
# 转化为array形式
data_lst = np.array(data_lst)
# 转化为array形式。
type_lst = pngDataSets.type.values
# # 准备测试数据
# pngPaths = getObjectPath(r"./test")
#
# for eachPath in pngPaths:
# filename = os.path.basename(eachPath)
# second_pngPaths = getObjectPath(eachPath)
# for second_eachPath in second_pngPaths:
# count += 1
# if count % 50 == 0: print(count)
# # 得到标准的测试数据的array形式。
# test_imgarr = img2vector(second_eachPath, height, width)
#
# testName = test_foronePng(test_imgarr, data_lst,type_lst, k, height, width)
# if testName == filename:
# i += 1
#
# # 评价指标
# accuracy = i / count # 表示准确率
# ScoreF1 = accuracy * 1 * 2 / (accuracy + 1) # 表示F1Score
# print("准确率:{}".format(round(accuracy, 4)))
# print("F1-Score:{}".format(round(ScoreF1, 4)))
# 留出法的验证集。
# score_lst = {}
# for train_size in np.linspace(0.2,0.95,num=16):
# i2 = 0
# count2 = 0
# train_index_lst, test_index_lst = my_train_test_split(data_lst, type_lst, train_size)
# X_train, X_test = data_lst[train_index_lst], data_lst[test_index_lst]
# Y_train, Y_test = type_lst[train_index_lst], type_lst[test_index_lst]
# print(X_train.shape, X_test.shape)
# for index, valid_arr in enumerate(X_test):
# count2 += 1
# testName = test_foronePng(valid_arr, X_train, Y_train, k, height, width)
# if testName == Y_test[index]:
# i2 += 1
# score = round(i2/count2,4)
# print(score,train_size)
# score_lst[train_size] = score
# key_lst = list(score_lst.keys())
# value_lst = list(score_lst.values())
# #
# plt.figure(figsize=(10,10))
# plt.plot(key_lst, value_lst)
# for a, b in zip(key_lst, value_lst):
# plt.text(a, b, (round(float(a*100))/100, b), ha='center', va='bottom', fontsize=15)
# plt.show()
"""
还好,即使是0.2的分配比,预测效果就能达到0.9左右。
"""
# 交叉验证法
# 用于标注目前第几组在进行测试
folds_order = 0
# 用于记录每组实验的准确率
score = 0
# 指出交叉验证的组数 一般默认是10 ,而如果k = m 则为留一法。
# 当留一法时,每一组的准确率不是0就是1。
n_folds = 10
# 为了图像展示,构造一个记录实验第几组和相应组的准确率的字典。
score_dict = {}
# 调用方法,得到 n_folds 组训练测试集的索引。
kfold_lst = my_KFold(data_lst,n_folds=n_folds,shuffe=True)
# 循环遍历每一组训练测试集。计算每一组的准确率。
for train_index_lst, test_index_lst in kfold_lst:
folds_order += 1
count3 = 0
i3 = 0
X_train, X_test = data_lst[train_index_lst], data_lst[test_index_lst]
Y_train, Y_test = type_lst[train_index_lst], type_lst[test_index_lst]
print(X_train.shape, X_test.shape)
for index, valid_arr in enumerate(X_test):
count3 += 1
testName = test_foronePng(valid_arr, X_train, Y_train, k, height, width)
if testName == Y_test[index]:
i3 += 1
score_one = round(i3/count3,4)
print(folds_order, score_one)
score_dict[folds_order] = score_one
score += score_one
# 打印最终平均的准确率
print("final:",round(score/n_folds,4))
# 画图展示
key_lst = list(score_dict.keys())
value_lst = list(score_dict.values())
plt.figure(figsize=(20,5))
plt.plot(key_lst, value_lst)
plt.show()
数据集下载链接
链接:https://pan.baidu.com/s/1hWC-o5lU6zyEcJcOxYBaKQ
提取码:90pm
复制这段内容后打开百度网盘手机App,操作更方便哦