周志华机器学习P69 3.3 使用对率回归对3.0α西瓜数据集实现分类
数据集的加载
编号,密度,含糖率,好瓜
1,0.697,0.46,1
2,0.774,0.376,1
3,0.634,0.264,1
4,0.608,0.318,1
5,0.556,0.215,1
6,0.403,0.237,1
7,0.481,0.149,1
8,0.437,0.211,1
9,0.666,0.091,0
10,0.243,0.0267,0
11,0.245,0.057,0
12,0.343,0.099,0
13,0.639,0.161,0
14,0.657,0.198,0
15,0.36,0.37,0
16,0.593,0.042,0
17,0.719,0.103,0
首先我们观察数据集,发现好瓜一般都是含糖率高的,而坏瓜一般含糖率不高~~,所以我们可以直接设置一个阈值,小于此值的都是坏瓜反之好瓜就行了~~
。
仿照pytorch的DataLoader风格,我仿写了一个相似的DataLoader
import pandas as pd
class DataLoader:
def __init__(self, csv: str or pd.DataFrame, x_label: list, y_label: list) -> None:
if type(csv) == str:
csv = pd.read_csv(csv)
elif type(csv) == pd.DataFrame:
pass
self.xs = csv[x_label].values # 含有所有样本特征值的特征矩阵
self.ys = csv[y_label].values # 含有所有样本真实标签的标签矩阵
def __len__(self):
return len(self.xs) # 返回数据集的长度
def __getitem__(self, idx):
return self.xs[idx], self.ys[idx] # 这个方法用于从外部获取数据集中的数据
这样我们可以每一次取一个数据,然后把它喂到我们的模型里。
if __name__ == '__main__':
data = DataLoader(pd.read_csv('./3.0a.csv'), ['密度', '含糖率'], ['好瓜'])
for x, label in data:
print(x, label)
[0.697 0.46 ] [1]
[0.774 0.376] [1]
[0.634 0.264] [1]
[0.608 0.318] [1]
[0.556 0.215] [1]
[0.403 0.237] [1]
[0.481 0.149] [1]
[0.437 0.211] [1]
[0.666 0.091] [0]
[0.243 0.0267] [0]
[0.245 0.057] [0]
[0.343 0.099] [0]
[0.639 0.161] [0]
[0.657 0.198] [0]
[0.36 0.37] [0]
[0.593 0.042] [0]
[0.719 0.103] [0]
模型
按照西瓜书上的定义,我们的模型定义为
y
=
1
1
+
e
−
(
w
T
x
+
b
)
y=\frac{1}{1+e^{-(w^Tx + b)}}
y=1+e−(wTx+b)1
当y值大于0.5时,结果为正类,结果小于0.5时为负类
这里专门定义了一个类用于储存模型以及参数,对应下面代码里的forward函数
需要注意的是,在实际编写程序的时候,没有必要专门定义一个b变量用于储存,只需把b添加在w的末尾即可,然后在x最后添加一列1,这样
算出来的
x
T
w
x^Tw
xTw就是
w
T
x
+
b
w^Tx+b
wTx+b了
from typing import Any
import numpy as np
class logit_regrassion:
def __init__(self, alpha=1) -> None:
self.w = np.array([0, 0, 1]) # 两个特征w加一个b
self.alpha = alpha
def forward(self, x): # 推导,即上面那个模型的公式
x = np.append(x, 1) # 添加一个1以计算b
y = 1 / (1 + np.power(np.e, -(self.w.T @ x)))
# print(self.w)
if y >= 0.5:
return 1
else:
return 0
def backward(self, x: np.ndarray, label):
x = np.append(x, 1) # 添加一个1以计算b
print(self.w)
if label == 1:
self.w = (self.w.T + self.alpha * x * (np.power(np.e, -(self.w.T @ x)) / np.power(np.power(np.e, -(self.w.T @ x)) + 1, 2)))
else:
self.w = (self.w.T - self.alpha * x * (np.power(np.e, -(self.w.T @ x)) / np.power(np.power(np.e, -(self.w.T @ x)) + 1, 2)))
def get_w(self):
return self.w
要使得我们的推导函数尽可能的正确的分类,我们要像线性回归那样去定义一个目标函数,不过这里不能像线性回归那样直接使用最小二乘法,
因为这样定义出来的目标函数对w求偏导时并不是一个凸函数,所以不能用最小二乘法,书上用的是极大似然估计,所以我们这里用的也是极大似然估计法,
L
=
y
1
1
+
e
(
w
T
x
)
+
(
1
−
y
)
(
1
−
1
1
+
e
(
w
T
x
)
)
L=y\frac{1}{1+e^(w^Tx)} + (1-y)(1-\frac{1}{1+e^(w^Tx)})
L=y1+e(wTx)1+(1−y)(1−1+e(wTx)1)
(这里省略了b,因为把b放进w里了)
用样本的真实值乘以推导出来是此样本的概率最后求和,即为极大似然估计法的公式
然后我们对这个函数求w的偏导即可得到这个函数的梯度,然后按照梯度的正方向去更新迭代参数即可,因为这里我们要求目标函数的最大值,故和线性回归更新参数的
方向相反,是要让函数的参数上升到最大值点,而不是下降,求导的过程我就不写了,相信大家都会求吧。因为这里只有两类,所以我就分情况讨论了。
对应上面的backward函数
训练
首先把以上我们写好的两个类引入,还有一些需要用到的包(这里每个类我都新建了一个文件)
from dataloader import DataLoader
from net import logit_regrassion
import pandas as pd
from tqdm import tqdm
import matplotlib.pyplot as plt
import numpy as np
然后这里用到了留一法进行训练,因为数据量较小,每一次取其中的一个样本为验证样本,其余为训练样本
# 当数据量很小的时候我们使用留一法 每一次取一个样本作为测试样本 其它为训练样本
raw_data = pd.read_csv('./3.0a.csv')
indexs = list(range(len(raw_data) - 1))
indexs = [x + 1 for x in indexs] # [0, 1, 2, 3 ... 16]
x_label = ['密度', '含糖率'] # 特征值对应csv文件里的列名
y_label = ['好瓜'] # 标签对应的列明
net = logit_regrassion() # 网络的构建
epoch = 100 # 循环的次数
total_num = 0 # 这两个参数用于统计准确率
right_num = 0
然后就是训练,每一个小epoch中先用训练样本对目标函数进行参数更新,然后剩下的一个样本用于测试,
这里还用到了进度条库tqdm 对于这个库的用法大家可以百度
with tqdm(total=epoch * 16) as pbar:
for i in range(epoch):
for index in indexs:
train_data = pd.concat([raw_data.loc[:index - 2], raw_data.loc[index:]])
val_data = pd.DataFrame(raw_data.loc[index - 1]).T
# 训练
for train_x, label in DataLoader(train_data, x_label, y_label):
net.backward(train_x, label)
# 测试
for val_x, label in DataLoader(val_data, x_label, y_label):
# print(net.forward(val_x), label[0])
if net.forward(val_x) == label:
right_num += 1
total_num += 1
pbar.update(1)
最后打印准确率并保存参数
print(right_num / total_num)
# 得到参数并写入
w = net.get_w()
np.save('w', w)
测试
画图以显示结果
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist as axisartist #导入坐标轴加工模块
import math
w = np.load('w.npy')
raw_data = pd.read_csv('./3.0a.csv')
positive_x = np.append(raw_data[raw_data['好瓜'] == 1][['密度', '含糖率']].values, np.ones(shape=(raw_data[raw_data['好瓜'] == 1][['密度', '含糖率']].values.shape[0], 1)), axis=1)
nagetive_x = np.append(raw_data[raw_data['好瓜'] == 0][['密度', '含糖率']].values, np.ones(shape=(raw_data[raw_data['好瓜'] == 0][['密度', '含糖率']].values.shape[0], 1)), axis=1)
positive_z = positive_x @ w
nagetive_z = nagetive_x @ w
print(positive_z, nagetive_z)
#
plt.rcParams['font.sans-serif'] = 'SimHei' ## 设置中文显示
plt.rcParams['axes.unicode_minus'] = False
fig=plt.figure(figsize=(4,2)) #新建画布
ax=axisartist.Subplot(fig,111) #使用axisartist.Subplot方法创建一个绘图区对象ax
fig.add_axes(ax) #将绘图区对象添加到画布中
ax.axis[:].set_visible(False) #隐藏原来的实线矩形
ax.axis["x"]=ax.new_floating_axis(0,0,axis_direction="bottom") #添加x轴
ax.axis["y"]=ax.new_floating_axis(1,0,axis_direction="bottom") #添加y轴
ax.axis["x"].set_axisline_style("->",size=1.0) #给x坐标轴加箭头
ax.axis["y"].set_axisline_style("->",size=1.0) #给y坐标轴加箭头
ax.annotate(s='z' ,xy=(2*math.pi,0) ,xytext=(2*math.pi,0.1)) #标注x轴
ax.annotate(s='y' ,xy=(0,1.0) ,xytext=(-0.5,1.0)) #标注y轴
plt.xlim(-15, 15) #设置横坐标范围
plt.ylim(0, 2) #设置纵坐标范围
ax.set_xticks(range(-15, 16, 1)) #设置x轴刻度
ax.set_yticks([-1,1]) #设置y轴刻度
# sigmoid 函数
sigmoid_x = np.linspace(-15, 15, 1000)
sigmoid_y = 1 / (1 + np.power(np.e, -sigmoid_x))
plt.plot(sigmoid_x, sigmoid_y, color="black") #描点连线
# z = wx
positive_dot = plt.scatter(positive_z, 1 / (1 + np.power(np.e, -positive_z)), c='red', label='正样本')
nagetive_dot = plt.scatter(nagetive_z, 1 / (1 + np.power(np.e, -nagetive_z)), c='blue', label='负样本')
#
plt.legend(handles=[positive_dot, nagetive_dot])
plt.show() #出图