目录
实验前准备
本实验是在Anaconda下的jupyter notebook上进行的,使用的代码语言为python。在开始实验前,我们首先需要导入所需要的库与包或者模块。本实验是一个模型评估实验,需要处理大量的实验数据,需要处理多维数组对象,以及需要画图进行可视化处理,还有一些数学公式的运用,所以我们需要导入的包为numpy、pandas、math以及matplotlib.pyplot。同时,我们还需要对warning进行处理,忽略掉warning的输出。
代码实现:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
import matplotlib as mpl
import warnings
import random
warnings.filterwarnings('ignore')
三种数据划分方式
实验要求
实验数据预处理
我们首先需要使用pandas将csv文件导入,然后将csv格式转换为Dataframe的形式来进行操作,随后我们将labels标签内容从字符串替换为数值(0与1)。由于需要使用到逻辑回归,所以我们需要往特征向量中添加1列全为1的特征量。然后我们还需要使用到我们上一个实验所完成的逻辑回归函数,由于数据量较大,所以我们在这里使用的是小批量梯度下降算法,我们直接复制粘贴前面的算法即可。
代码实现:
#your code here
#第一步,通过pandas将csv文件转换为dataframe格式后进行操作
df = pd.read_csv('illness.csv')
#将label标签内容从字符串替换为数值
df['class'] = df['class'].map({'Abnormal': 0, 'Normal': 1}).fillna(-1)
# 第二步,需要往特征向量中,添加一列全为1的特征量
# 首先通过pandas的Series函数创建一个全为1的ones,其中索引为相对应的数据df的索引
ones=pd.Series(1, index=df.index)
# 然后直接在数据前面添加全为1的x0列
df.insert(0, 'x0', ones)
# 获取特征和标签列
features = df.iloc[:, :-1]
labels = df.iloc[:, -1]
# 将数据从DataForm的形式转换为矩阵的形式
df_array=np.array(df)
# 第三步,定义逻辑回归的模型
# 定义一个逻辑函数
def sigmoid(z):
return np.exp(z)/(1+np.exp(z))
# 定义一个梯度下降算法的函数,由于样本数量较大,故而使用小批量梯度下降算法
def Gradient_Descent(data):
# 首先获取数据的行与列
m,n=data.shape
# 将X与Y从数据中分离出来
X=data[:,:n-1]
Y=data[:,n-1]
# 初始化超参数
w=np.ones(n-1)
learning_rate=0.01
max_iterations=100000
threshold=1e-9
batch_size=5
# 开始迭代
for iterations in range(max_iterations):
# 随机选取小批量数据的序号
batch_indices=np.random.choice(len(Y),size=batch_size,replace=False)
# 定义一个临时变量,作为计算dw的中间变量
temp=np.zeros(n-1)
# 开始迭代小批量的数据
for i in batch_indices:
x_i=X[i]
y_i=Y[i]
temp+=(x_i*(sigmoid(np.dot(w.T,x_i))-y_i))
# 更新w
dw=temp/batch_size
w-=learning_rate*dw
# 判断参数更新的幅度是否小于某个阈值
if np.abs(dw).all()<threshold:
break
# 完成训练
return w
print(df_array)
w=Gradient_Descent(df_array)
print(w)
留出法
算法原理:
代码实现:
#留出法
#your code here
def Hold_Out(frame):
# 计算每个类别需要分配给测试集的样本数量的比例,假设为 30%
test_ratio = 0.3
# 计算每个类别需要分配给测试集的样本数量
test_samples_per_class = (labels.value_counts() * test_ratio).astype(int)
# 初始化测试集和训练集
train_data = pd.DataFrame()
test_data = pd.DataFrame()
# 根据标签的分布进行采样
for label, count in test_samples_per_class.items():
class_samples = frame[frame['class'] == label].sample(n=count, random_state=88)
test_data = test_data._append(class_samples)
train_data = train_data._append(frame[frame['class'] == label].drop(class_samples.index))
# 获取训练集和测试集的特征和标签
train_features = train_data.iloc[:, :-1]
train_labels = train_data.iloc[:, -1]
test_features = test_data.iloc[:, :-1]
test_labels = test_data.iloc[:, -1]
# # 打印训练集和测试集大小
# print("Train data number: ", len(train_data))
# print("Test data number: ", len(test_data))
# #打印训练集和测试集
# print("Train data : ", np.array(train_data))
# print("Test data : ", np.array(test_data))
return np.array(train_data),np.array(test_data)
Hold_Out(df)
交叉验证法
算法原理:
代码实现:
#交叉验证法
#your code here
def Cross_Validation(frame,k):
# 将数据按照标签分为两类
class_0_samples = frame[frame['class'] == 0]
class_1_samples = frame[frame['class'] == 1]
# 将每一类别的样本随机排序
class_0_samples = class_0_samples.sample(frac=1, random_state=42)
class_1_samples = class_1_samples.sample(frac=1, random_state=42)
# 初始化训练集和测试集的列表
train_data = []
test_data = []
for i in range(k):
# 初始化当前折的训练集和测试集
train_fold = pd.DataFrame()
test_fold = pd.DataFrame()
# 计算当前折中每个类别的样本数量
class_0_fold_size = len(class_0_samples) // k
class_1_fold_size = len(class_1_samples) // k
# 添加当前折中每个类别的样本到测试集
test_fold = test_fold._append(class_0_samples[i * class_0_fold_size: (i + 1) * class_0_fold_size])
test_fold = test_fold._append(class_1_samples[i * class_1_fold_size: (i + 1) * class_1_fold_size])
# 添加剩余样本到训练集
train_fold = df.drop(test_fold.index)
# 打乱训练集和测试集的顺序,以便更好的随机性
train_fold = train_fold.sample(frac=1, random_state=42)
test_fold = test_fold.sample(frac=1, random_state=42)
# 将当前折的训练集和测试集添加到列表
train_data.append(train_fold)
test_data.append(test_fold)
# # 打印每一折的测试集索引,可以看到均为不重复样本
# for i in range(k):
# print("第", i+1, "折的测试集: ", (test_data[i].index))
# print("第", i+1, "折的训练集: ", (train_data[i].index))
return np.array(train_data),np.array(test_data)
Cross_Validation(df,10)
自助法
算法原理:
代码实现:
#自助法
#your code here
def Bootstrap(frame):
# 创建自助样本集
num_samples = frame.shape[0]
train_data = []
test_data = []
for i in range(num_samples):
index = random.randint(0, num_samples-1)
train_data.append(frame.iloc[index])
# 创建测试集(排除训练集中已存在的样本)
for _, row in frame.iterrows():
is_in_train_data = False
for train_row in train_data:
if row.equals(train_row):
is_in_train_data = True
break
if not is_in_train_data:
test_data.append(row)
# # 打印训练集和测试集大小
# print("训练集大小: ", len(train_data))
# print("测试集大小: ", len(test_data))
return np.array(train_data),np.array(test_data)
Bootstrap(df)
三种数据划分方式的精度
留出法
我们首先需要利用留出法对数据进行训练集与测试集的划分,然后调用前面数据预处理阶段的小批量梯度下降算法对训练集进行训练,然后利用训练出来的模型对测试集的标签值进行预测,最后将预测结果与测试集的标签进行比较,并且最终得到精度。
代码实现:
#留出法测试精度
#your code here
def Hold_Out_Accuracy(data):
# 获取训练集与测试集
train_data,test_data=Hold_Out(data)
# 使用前面讲的梯度下降算法模型进行训练
w=Gradient_Descent(train_data)
# 获取测试集的特征向量与标签值
X = test_data[:, :-1]
Y = test_data[:, -1]
# 计算预测值
pred=sigmoid(np.dot(X,w.T))
Y_pred = np.where(pred >= 0.5, 1, 0)
# 计算精确率
accuracy = np.sum(Y == Y_pred) / len(Y)
# print("模型训练得到的参数为:",w)
# print("错误率为:",1-accuracy)
# print("精度为:",accuracy)
return accuracy
Hold_Out_Accuracy(df)
结果输出:
实验结果分析:根据模型训练得到的参数结果以及精度,我们可以得知,利用留出法进行数据划分,整体上来说划分的效果是不错的,精度达到了0.7956989247311828,精度达到了较高的水平。
交叉验证法
我们首先需要利用交叉验证法对数据进行训练集与测试集的划分,然后调用前面数据预处理阶段的小批量梯度下降算法对训练集进行训练,然后利用训练出来的模型对测试集的标签值进行预测,最后将预测结果与测试集的标签进行比较,并且最终得到精度。
代码实现:
#交叉验证法
#your code here
#每次选用一个子集作为测试集,剩余k-1个子集的并集作为训练集,所以需要每轮都进行测试
#可以使用k个轮次的精度的平均精度作为本方法的最终精度
def Cross_Validation_Accuracy(data,k):
# 获取每一折的训练集与测试集
train_data,test_data=Cross_Validation(data,k)
# 计算所有折的准确率的总值
accuracy_sum=0
for i in range(k):
# 使用前面讲的梯度下降算法模型进行训练
w=Gradient_Descent(train_data[i])
# 获取测试集的特征向量与标签值
X = test_data[i][:, :-1]
Y = test_data[i][:, -1]
# 计算预测值
pred=sigmoid(np.dot(X,w.T))
Y_pred = np.where(pred >= 0.5, 1, 0)
# 计算精确率
accuracy = np.sum(Y == Y_pred) / len(Y)
accuracy_sum+=accuracy
accuracy_mean=accuracy_sum/k
# print("模型训练得到的参数为:",w)
# print("错误率为:",1-accuracy_mean)
# print("精度为:",accuracy_mean)
return accuracy_mean
Cross_Validation_Accuracy(df,10)
结果输出:
实验结果分析:根据模型训练得到的参数结果以及精度,我们可以得知,利用交叉验证法进行数据划分,整体上来说划分的效果是不错的,精度达到了0.735483870967742,精度达到了较高的水平。
自助法
我们首先需要利用自助法对数据进行训练集与测试集的划分,然后调用前面数据预处理阶段的小批量梯度下降算法对训练集进行训练,然后利用训练出来的模型对测试集的标签值进行预测,最后将预测结果与测试集的标签进行比较,并且最终得到精度。
代码实现:
#自助法测试精度
#your code here
def Bootstrap_Accuracy(data):
# 获取训练集与测试集
train_data,test_data=Bootstrap(data)
# 使用前面讲的梯度下降算法模型进行训练
w=Gradient_Descent(train_data)
# 获取测试集的特征向量与标签值
X = test_data[:, :-1]
Y = test_data[:, -1]
# 计算预测值
pred=sigmoid(np.dot(X,w.T))
Y_pred = np.where(pred >= 0.5, 1, 0)
# 计算精确率
accuracy = np.sum(Y == Y_pred) / len(Y)
# print("模型训练得到的参数为:",w)
# print("错误率为:",1-accuracy)
# print("精度为:",accuracy)
return accuracy
Bootstrap_Accuracy(df)
结果输出:
实验结果分析:根据模型训练得到的参数结果以及精度,我们可以得知,利用自助法进行数据划分,整体上来说划分的效果是不错的,精度达到了0.773109243697479,精度达到了较高的水平。
性能判断
输出结果:
实验结果分析:通过运行多次得到的实验结果,我们可以发现,整体来说,利用交叉验证法进行数据划分得到的评估结果精度比较稳定,同时当改变k的值时,评估结果精度会发生变化。而对于留出法,评估结果不太稳定,经常会发生较大的变化,精度较高时会达到0.83,但是当精度较低时,又会降到0.56左右。最后对于自助法来说,由于数据集比较中等,同时会改变初始数据集的分布,所以评估的结果也是不太稳定的,但是变化的范围没有留出法那么大。
不同的数据划分方式在不同的应用场景下均有重要的意义,如果我们硬要划分性能“最好”以及性能“最差”的话,由于交叉验证法比较稳定,而且评估的结果也比较高,同时还是进行k次评估再求其平均解的,所以我觉得交叉验证法的性能是最好的。然后比较留出法以及自助法可以得知,留出法的划分是按照层次来划分的,数据分布比较均匀,虽然评估结果不太稳定;而自助法的划分是不全面的,有一些数据没有办法出现在训练集中,而且评估结果较留出法也不太理想,所以我觉得自助法是性能最差的。
所以综上所述,性能最好的为交叉验证法,性能最差的为自助法。
P-R曲线与ROC曲线
选择性能更好的数据划分方式
在上面的对比分析中,我们已经得知交叉验证法是性能最好的划分方式,其次是留出法,最后是自助法,当然,可能你们还会有不同的划分方式,但是在上面的基础上,由于我们只需要在留出法与自助法中做出选择,所以我们在这里选择的是留出法。
数据划分
由于我们已经选择了留出法作为本次数据划分的方式,那么我们可以利用前面我们已经写好的数据划分函数来对数据进行划分,划分为训练集和测试集。随后我们可以通过打印训练集和测试集来对结果进行验证。
代码实现:
#your code here
#选择一种数据划分方法
# 选择留出法进行数据划分
train_data,test_data=Hold_Out(df)
print(train_data)
print(test_data)
P-R曲线和F1度量
代码实现:
#your code here
#画出P-R曲线
# 首先划分测试集的特征值与标签值
x_test = test_data[:, :-1]
y_test = test_data[:, -1]
# 通过梯度下降法训练训练集求出w
w=Gradient_Descent(train_data)
# 求出预测值
y_pred_prob=sigmoid(np.dot(x_test,w.T))
# 设置不同的阈值
thresholds = np.sort(y_pred_prob)
# 初始化存储精确度和召回率的数组
precision = np.zeros(len(thresholds))
recall = np.zeros(len(thresholds))
# 计算精确度和召回率
for i, threshold in enumerate(thresholds):
y_pred = np.where(y_pred_prob >= threshold, 1, 0)
true_positives = np.sum((y_pred == 1) & (y_test == 1))
false_positives = np.sum((y_pred == 1) & (y_test == 0))
false_negatives = np.sum((y_pred == 0) & (y_test == 1))
precision[i] = true_positives / (true_positives + false_positives)
recall[i] = true_positives / (true_positives + false_negatives)
# 绘制P-R曲线
plt.plot(recall, precision,marker='.')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('P-R Curve')
plt.show()
# 计算各个点的F1度量
# 首先初始化存储F1度量的数组
F1=np.zeros(len(thresholds))
for i in range(len(thresholds)):
F1[i]=(2*precision[i]*recall[i])/(precision[i]+recall[i])
print("各个点的F1度量为:\n",F1)
P-R曲线输出:
F1度量:
实验结果分析:我们可以发现,我们所画出的P-R曲线是比较符合题意的,同时输出的F1度量也是在误差的允许范围内的,我们观察图像还可以知道,当阈值越低的时候,查全率增加,而查准率降低。总体而言,该P-R曲线是比较符合题意的。
ROC曲线
代码实现:
#your code here
#画出ROC曲线
# 首先划分测试数据集的特征值与标签值
x_test = test_data[:, :-1]
y_test = test_data[:, -1]
# 通过梯度下降法训练训练集求出w
w=Gradient_Descent(train_data)
# 求出预测值
y_pred_prob=sigmoid(np.dot(x_test, w.T))
# 设置不同的阈值
thresholds = np.sort(y_pred_prob)
# 初始化存储真正例率和假正例率的数组
true_positive_rate = np.zeros(len(thresholds))
false_positive_rate = np.zeros(len(thresholds))
# 计算假正例率和真正例率
for i, threshold in enumerate(thresholds):
y_pred = np.where(y_pred_prob >= threshold, 1, 0)
true_positives = np.sum((y_pred == 1) & (y_test == 1))
false_positives = np.sum((y_pred == 1) & (y_test == 0))
true_negative = np.sum((y_pred == 0) & (y_test == 0))
false_negative = np.sum((y_pred == 0) & (y_test == 1))
true_positive_rate[i] = true_positives / (true_positives + false_negative)
false_positive_rate[i] = false_positives / (false_positives + true_negative)
# 计算AUC
auc = np.trapz(true_positive_rate, false_positive_rate)
# 绘制ROC曲线
plt.plot(false_positive_rate, true_positive_rate,marker='.')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve (AUC = {:.2f})'.format(auc))
plt.show()
ROC曲线输出:
实验结果分析:如图所示,我们可以发现,我们所画出的ROC曲线是比较符合题意的,同时我们还算出了AUC为-0.88,我们观察图像可以得知,当AUC越大时,表示性能越好。那么总体而言,该ROC曲线还是比较符合题意的。
实验总结
数据划分方式:在本次实验中,我们的数据集并没有划分训练集和测试集,所以我们需要利用不同的数据划分方式对该数据集进行划分。我们一共学习了三种数据划分方式,分别为留出法、交叉验证法和自助法。
1. 留出法:是最简单的数据划分方法,直接将数据集划分为训练集和测试集。通常将一部分数据用于训练,另一部分数据用于评估模型性能。通常可以采用分层抽样的形式来对数据进行抽样划分,可以尽量地保持数据分布地一致性。留出法的优点为实现简单,计算速度快,同时可以很好地保留数据分布的特征,适用于数据量较大的情况。而缺点为划分不够灵活,无法充分利用数据;受随机划分的影响,可能会导致模型性能波动较大。比较适用于数据集较大,样本数量足够,且数据分布相对均匀的情况。
2. 交叉验证法:将数据集划分为K个子集,每个子集轮流作为测试集,其余子集作为训练集。通过多次训练和评估,最终得到模型性能的平均值。交叉验证法的优点为可以充分利用数据,减少模型性能评估的随机性;同时可以更好地估计模型的泛化能力。而缺点为交叉验证法的计算开销较大,需要进行K次训练和评估;而在某些情况下,可能会导致模型训练过拟合于某些特定数据子集。比较适用于数据集相对较小,样本数量不足,同时需要更准确地评估模型的泛化能力的情况。
3. 自助法:通过有放回地随机采样生成与原始数据集大小相同的新数据集,原始数据集中的一部分数据可能被多次采样,而另一部分可能完全被忽略。生成的新数据集用于训练模型,原始数据集中未被采样到的数据用于测试验证模型性能。自助法的优点为可以充分利用数据,可以使用更多的样本进行训练,可以生成多个不同的数据集,对模型性能的评估更稳定。而缺点为由于有放回地采样,生成的数据集中可能包含重复样本;对于较大的数据集,自助法可能会导致计算开销过大。比较适用于数据集较小,样本数量不足,同时需要更稳定地评估模型性能的情况。
精度评估:我们在本次实验中学习到了不同的精度评估方法,如精确度,召回率,F1度量等。这些指标可以帮助我们评估模型的分类性能。精确度衡量模型在所有预测为正类的样本中真实为正类的比例,召回率衡量模型在所有真实为正类的样本中预测为正类的比例,F1度量是精确度和召回率的调和平均值。我们先后计算了三种数据划分方式的精确度的大小,通过比较可以发现,由于交叉验证法比较稳定,而且评估的结果也比较高,同时还是进行k次评估再求其平均解的,所以我们可以觉得交叉验证法的性能是最好的。然后比较留出法以及自助法可以得知,留出法的划分是按照层次来划分的,数据分布比较均匀,虽然评估结果不太稳定;而自助法的划分是不全面的,有一些数据没有办法出现在训练集中,而且评估结果较留出法也不太理想,所以我们可以认为自助法是性能最差的。当然,不同的数据划分方式在不同的情况下的适用环境都可能不太一样,所以具体选择哪一种数据划分方式,还需要根据具体场景来考虑。
P-R曲线和F1度量:我们还学习到了P-R曲线和F1度量,它们比较适合用于评估二分类模型的性能。P-R曲线是以精确度为横坐标、召回率为纵坐标的曲线,可以帮助我们选择合适的阈值来平衡精确度和召回率。P-R曲线越往外的模型能力越好。而F1度量是精确度和召回率的调和平均值,可以衡量模型的整体性能。
ROC曲线:我们最后学习了ROC曲线,它是以假正例率(False Positive Rate)为横坐标、真正例率(True Positive Rate)为纵坐标的曲线。ROC曲线可以帮助我们评估模型在不同阈值下的分类性能,并选择合适的阈值来平衡假正例率和真正例率。ROC曲线也是越往外的模型能力越好,如果曲线交叉,可以根据ROC曲线下面积(AUC)大小进行比较,如果AUC越大,表示性能越好。