经过许多天的折腾,今天上午,总算是可以交差这个部分了。
网上的逻辑回归的理论有很多,但是具体可以看明白的代码却没有那么丰富,咱们姑且来个抛砖引玉。
简要代码讲解
数据加载类
class DataLoader:
"""数据加载类"""
def __init__(self):
self.irisData = load_iris().data # 读取150*4的数据集 - 其实就是一个ndarray二维数组
# 鸢尾花数据集
# 取有限个关于 setosa 花的 sepal length 和 sepal width 作为特征并且标记为正类(1)
# 取有限个关于其他两种花的 sepal length 和 sepal width 作为特征并且标记为负类(0)
def getData(self):
"""获取真正的训练集"""
setosaIndex = random.sample(range(0, 50), 30) # 从0~49中选取30个的值组成一个列表返回
otherIndex = random.sample(range(50, 150), 30) # 从50~149中选取30个值组成一个列表返回
dataIndex = setosaIndex + otherIndex # 列表组合,这就是全部的训练集对应样本集索引列表
X = np.array([[self.irisData[i, 0], self.irisData[i, 1]] for i in dataIndex]) # 用列表来构建的特征向量数组
Y = np.array([1 for i in range(len(setosaIndex)) ] + [0 for i in range(len(otherIndex))]) # 目标值数组,一一对应的
return X, Y # 输入空间和输出空间,两个ndarray
由于已经用了不少次了,现在咱也算对这个鸢尾花集驾轻就熟了。不要因为看了这东西源码而感到害怕,其实我们真正使用是很简单的。只需要把load.iris().data赋值给一个变量,然后就可以用了。这个变量就是一个150*4的ndarray类型,一共150个数据,每行4个特征。其中,每50个是一种花的类型,总共有3种。
在训练分类模型时,这个训练集是很好用的。
比如现在的二分类,那么我只需要把其中一种类型确定为正类,另外两种确定为负类就好。
上述代码中,先是用了random产生了二类的抽样索引,只用再用列表解析的方法生成了输入空间X。
而由于这是已知的训练集,我们完全知道哪些样本对应哪类,所以Y是可以自己创造的。
模型训练类
class LogisticRegression:
"""逻辑回归模型训练类"""
def __init__(self):
"""参数初始化"""
self.theta = np.ones((3,1)) # 参数向量
self.Jvalue = [] # 存储过程中的J(theta)值
def sigmoid(self, z):
return 1/(1 + np.exp(-z))
def train(self, X, Y, epochs, lr):
for _ in range(epochs):
y1 = self.sigmoid(np.dot(X, self.theta)) # 计算预测值向量
error = y1 - Y # 真实值与估计值的误差向量
m = Y.shape[0] # 获取样本个数
t = np.dot(np.log(y1).transpose(), Y) + np.dot(np.log(1-y1).transpose(), (1-Y))
self.Jvalue.append(-1 / m * t[0,0]) # 当前J值
self.theta = self.theta - lr * np.dot(X.transpose(), error) # 参数theta更新
return self.Jvalue, self.theta
不同于上次的"单变量线性回归",以后我们的训练模型可能面对的特征会越来越多,也就是说参数也会越来越多,那时再用临时变量t就显得有麻烦又慢了,所以这里开始引入了向量化写法。
创建numpy里的ndarray类型,然后进行后续操作。
向量化的过程中,很多求和操作都可以用前一个向量的转置与后一个向量的乘积来表示。
数据可视化类
class DrawTool:
"""画图类"""
# 画点
def drawPoint(self, X, Y):
plt.scatter(X[:,1], X[:,2], c=['b' if i==1 else 'r' for i in Y ]) # 画的点的两个坐标值表示两个特征值,颜色表示1(蓝)/0(红)
# 画线
def drawLine(self, X, theta):
x1 = np.arange(min(X[:,1]), max(X[:,1]), 0.1) # 创建ndarray数组,作为画线点横标
x2 = np.array([ -(theta[0,0]+theta[1,0]*i)/theta[2,0] for i in x1])
plt.plot(x1, x2)
plt.show()
# 画出J的变化曲线
def drawJ(self, Jvalue):
plt.plot(logistic_regression.Jvalue) # <=> plt.plot([i for i in range(len(Jvalue))], Jvalue)
plt.title('Cost Function')
plt.xlabel('iterations')
plt.ylabel('J(theta)')
plt.show()
画点:
在这次的数据中,每个训练样本有两个特征,对应某种分类。
画在一个二维图上,于是两个特征分别作为了平面坐标轴的两个维度,通过点的不同的颜色来表示对应的分类类型。y=1表示为蓝色,y=0表示为红色。通过调用plt.scatter()画出散点图。
画线:
通过arange生成了在x1特征上min~max的数组,y是对应运算出的值。
画J曲线:
发现使用plot比scatter看起来更舒服。scatter生成的J曲线上的点都很粗大,但是plot就很平滑细小。
一般plot都是传入两个参数,第一个是第一维数据数组,第二个是第二维数据数组。如果只传入一个,那么就会把列表下标为作为第一维度,表内值作为第二维,这正好适合咱们这个迭代-值图像。
期间遇到的问题及反思
注意你的数据集
其实这简单的一个逻辑回归代码,竟然耗费了我十几天的时间。
究其原因就在于此。
这个鸢尾花的代码,原本是从我过去研究的感知机代码中摘下来的。
因为他俩都是二分类,所以我想当然地以为二者地数据集是可以共用地。
但二者其实有一个非常大的区别,那就是标记。
感知机
逻辑回归
看到没,感知机的负类标记是-1,而逻辑回归的是0。
这可了不得,不说模型训练那里的隐性问题,那个咱们看不太好,就看那个成本函数J(theta)。
这个当初咱们是通过数学技巧合并到一起的,对应y=1/0就会只剩下Cost里面的其中一项。如果y=1/-1那区别就大了。
怪不得我当初拟合不好时,画J曲线时,发现J的值竟然有负的,而且越训练越离谱。
特征缩放(均值归一化)很有用
在大体训练确定之后,通过比较前后的J曲线,发现进行特征缩放确实有用。
直接使用原数据
加上两行特征缩放代码
# 训练特征标准化
X -= np.mean(X, axis=0)
X /= np.std(X, axis=0, ddof=1)
标准化之后
完美😎
可以用ndarray就不用matrix
发现用ndarray来装二维数据,然后用np.dot()也可以进行向量点乘,而且好像ndarray的类型更通用一点,用np.mat()好像多此一举,而且通用性不强。
这一点有待商榷,暂且这样认定。
完整代码
from sklearn.datasets import load_iris # 导入鸢尾花训练集
import numpy as np # 数学运算
import matplotlib.pyplot as plt # 画图
import random # 实现随机
class DrawTool:
"""画图类"""
# 画点
def drawPoint(self, X, Y):
plt.scatter(X[:,1], X[:,2], c=['b' if i==1 else 'r' for i in Y ]) # 画的点的两个坐标值表示两个特征值,颜色表示1(蓝)/0(红)
# 画线
def drawLine(self, X, theta):
x1 = np.arange(min(X[:,1]), max(X[:,1]), 0.1) # 创建ndarray数组,作为画线点横标
x2 = np.array([ -(theta[0,0]+theta[1,0]*i)/theta[2,0] for i in x1])
plt.plot(x1, x2)
plt.show()
# 画出J的变化曲线
def drawJ(self, Jvalue):
plt.plot(logistic_regression.Jvalue) # <=> plt.plot([i for i in range(len(Jvalue))], Jvalue)
plt.title('Cost Function')
plt.xlabel('iterations')
plt.ylabel('J(theta)')
plt.show()
class DataLoader:
"""数据加载类"""
def __init__(self):
self.irisData = load_iris().data # 读取150*4的数据集 - 其实就是一个ndarray二维数组
# 鸢尾花数据集
# 取有限个关于 setosa 花的 sepal length 和 sepal width 作为特征并且标记为正类(1)
# 取有限个关于其他两种花的 sepal length 和 sepal width 作为特征并且标记为负类(0)
def getData(self):
"""获取真正的训练集"""
setosaIndex = random.sample(range(0, 50), 30) # 从0~49中选取30个的值组成一个列表返回
otherIndex = random.sample(range(50, 150), 30) # 从50~149中选取30个值组成一个列表返回
dataIndex = setosaIndex + otherIndex # 列表组合,这就是全部的训练集对应样本集索引列表
X = np.array([[self.irisData[i, 0], self.irisData[i, 1]] for i in dataIndex]) # 用列表来构建的特征向量数组
Y = np.array([1 for i in range(len(setosaIndex)) ] + [0 for i in range(len(otherIndex))]) # 目标值数组,一一对应的
return X, Y # 输入空间和输出空间,两个ndarray
class LogisticRegression:
"""逻辑回归模型训练类"""
def __init__(self):
"""参数初始化"""
self.theta = np.ones((3,1)) # 参数向量
self.Jvalue = [] # 存储过程中的J(theta)值
def sigmoid(self, z):
return 1/(1 + np.exp(-z))
def train(self, X, Y, epochs, lr):
for _ in range(epochs):
y1 = self.sigmoid(np.dot(X, self.theta)) # 计算预测值向量
error = y1 - Y # 真实值与估计值的误差向量
m = Y.shape[0] # 获取样本个数
t = np.dot(np.log(y1).transpose(), Y) + np.dot(np.log(1-y1).transpose(), (1-Y))
self.Jvalue.append(-1 / m * t[0,0]) # 当前J值
self.theta = self.theta - lr * np.dot(X.transpose(), error) # 参数theta更新
return self.Jvalue, self.theta
if __name__ == "__main__":
# 建立训练集
dataloader = DataLoader()
X,Y = dataloader.getData()
# 训练特征标准化
# X -= np.mean(X, axis=0)
# X /= np.std(X, axis=0, ddof=1)
X = np.c_[np.ones(len(X)), X] # X加了一列
Y = np.c_[Y]
# 逻辑回归训练
logistic_regression = LogisticRegression()
Jvalue, theta = logistic_regression.train(X, Y, 500, 0.005)
# 数据可视化
drawtool = DrawTool()
drawtool.drawJ(Jvalue) # 画出J(theta)变化曲线
drawtool.drawPoint(X, Y) # 画出数据点
drawtool.drawLine(X, theta) # 画出决策界限
完美展示结果