最近刚刚学完了吴恩达老师机器学习课程第一周的内容,涉及"机器学习相关介绍"和"单变量线性回归模型"。
不得不说吴恩达老师的课讲的真好,没有什么门槛,而且讲得也非常细致。
这不,学完了第1周的部分,我就想试着去写一个单变量线性回归模型的代码。
以下是我完整的实践过程。
我把这个一个具体的项目分成3步。
- 创建训练集
- 训练模型
- 数据可视化
思路展示
1.创建训练集
因为是自己模拟着玩,所以并没有去考虑网上找那些公开的但是还需要额外付出时间了解的训练集。
我的想法是通过使用伪随机数生成模块random来生成一些点。
我可以创建两个列表,里面的数据都由random生成,装有大量数据。一个是样本节点的特征表(因为是单变量,所以是一个值),一个是标记表。这样说可能有些繁琐,其实如果对应二维平面上的点,就是一个x坐标列表,一个y坐标列表,两表数据一一对应,合起来表示一个训练样本(x,y)。
代码
class DataGenerator:
"""数据生成类"""
def __init__(self, m):
self.m = m # 样本容量
self.list_x = [] # 生成点的x坐标
self.list_y = [] # 生成点的y坐标
def generate(self):
"""生成两个坐标列表"""
for i in range(self.m):
self.list_x.append(random.randint(1, 200)) # 其实是有瑕疵的,就是可能会有一个x对应多个y的情况,就当是测量误差吧
self.list_y.append(random.randint(1, 200))
def show(self):
"""用来输出随机生成的样本数据,测试用"""
for i in range(self.m):
print("(" + str(self.list_x[i]) + "," + str(self.list_y[i]) + ")") # 因为里面存的是数字,但现在连接是用的字符串语法,所有要显式转一下
2.训练模型
对于单变量线性回归模型的训练,无非就是这样一张图。
代码
class LinearRegressionForOneVariable:
"""单变量线性回归模型训练器"""
# 初始化
def __init__(self):
self.k = 0 # 变量系数 可能是初中对y = kx + b 印象太深了,哈哈
self.b = 0 # 常数项
# 训练
def train(self, m, list_x, list_y, epochs, ls): # 容量、x列表、y列表、循环次数、学习率
for _ in range(epochs): # 通过训练次数保证尽量收敛
t = 0 # 先算出公式后面那个求和项吧
for i in range(m):
t += self.k * list_x[i] + self.b - list_y[i]
t_b = self.b - ls * 1/m * t
t = 0
for i in range(m):
t += (self.k * list_x[i] + self.b - list_y[i]) * list_x[i]
t_k = self.k - ls * 1/m * t
self.b = t_b # 要保证是同时更新(老师强调的细节!!!)
self.k = t_k
3.数据可视化
训练好之后怎么看自己训练的怎么样呢,传入单个数据来预测可能没那么明显,而且有偶然性。
于是通过数据可视化的方法,这样就不再需要注意单个数,而是可以使用我们的几何直觉。因为所谓模型拟合的好,不就是样本点尽量都在模型线上嘛。
这部分可视化使用了Matplotlib库。
代码
class DrawTool:
"""画图类"""
# 画散点
def drawPoint(self,list_x,list_y):
plt.scatter(list_x,list_y)
# plt.show()
# 画线
def drawLine(self, k, b):
plt.plot([1,200],[k+b,200*k+b]) # 两点定线
plt.show()
最后再按照生成数据,训练,画图的过程写好执行代码
if __name__ == "__main__":
m = 50 # 样本容量
# 产生训练数据
datagenerator = DataGenerator(m)
datagenerator.generate()
# datagenerator.show()
drawtool = DrawTool()
drawtool.drawPoint(datagenerator.list_x, datagenerator.list_y)
# 训练
model = LinearRegressionForOneVariable()
model.train(m, datagenerator.list_x, datagenerator.list_y, 50, 0.01)
# 数据可视化
drawtool.drawLine(model.k, model.b)
发现问题:拟合错得离谱
以上是我起初的理想状态。
但是当我真的去调用时却出现了问题。
当我只画随机生成样本点时,图像是这样:
但当我把直线也加上时,结果却变成了这样:
就算我的拟合算法写的再差,你也不至于这个样子吧,真的是做到了完美避开所有点的超级不拟合直线。
我最开始还以为是散点图和直线图不能同时共存,这是出现了冲突,后来当我注意到二者的坐标轴时,才发现了一些端倪。
横轴都一样,关键在纵轴,我的样本点都出现在了y为正的图里,我的直线却朝着y为负的图里一往无前,这变成两个象限的事了。也正因为在第二幅图中y轴为负,但是它却还要显示y为正的那些点,最后可不是把所有样本点都显示在y=0那里了,因为那是能显示的最上面的位置了呀。
因此,没有什么冲突,Just我的模型拟合的太离谱了。
那会不会是因为随机生成的样本没有什么规则才导致这件事的呢?
咱就控制一下变量检验一下。
于是,关于随机生成那里,让我改成了按照y=x生成。对,咱就给出一个很简单的预设模型,就看看咱的模型最后能不能拟合到这个结果。
所以生成数据的代码变成了这样:
class DataGenerator:
"""数据生成类"""
--snip--
def generate(self):
"""生成两个坐标列表"""
for i in range(self.m):
# self.list_x.append(random.randint(1, 200)) 不要了!!!
# self.list_y.append(random.randint(1, 200))
self.list_x.append(i) # 存的样本就是(1,1)(2,2)...
self.list_y.append(i)
--snip--
结果呢?还是错得离谱。
既然表面看不上去,那只能慢慢调试看看能不能发现点什么了。
我尝试输出训练后的k和b。
这...e+42,太大了吧,我的初始k和b可都是0啊.
尝试增加训练次数 50->100
e+85了
再大呢?
好吧,无限大了。
解决问题:原来是学习率的锅
上面这个与k=1,b=0的正确答案相差可太远了,而且似乎随着训练次数的增加反而越来越远。似乎永远不能收敛了。
能想到什么?没错,当学习率过大就会发生这件事,就是下面第2幅图。
于是这才领悟,原来α(learning rate) = 0.01还不够小。
0.01 -> 0.001
非常接近!!!
再看看图
基本完美,那个细线就是我的模型。左下角的厚线其实是那个训练样本节点的堆叠,因为此时我的样本容量m=50,所以它到50就停止了。
这样,我们总算解决了问题。
要不,再试试前面那个随机数的(这次lr = 0.0001才行)
差不多
这就是我这次实践的这个过程啦,果然还是自己亲自写写代码才知道哪里可能会有问题。
附上y=x的拟合完整代码
import matplotlib.pyplot as plt # 画图用
import random # 生成伪随机数,也就是我的训练集来源
class DrawTool:
"""画图类"""
# 画散点
def drawPoint(self,list_x,list_y):
plt.scatter(list_x,list_y)
# plt.show()
# 画线
def drawLine(self, k, b):
plt.plot([1,200],[k+b,200*k+b]) # 两点定线
plt.show()
class DataGenerator:
"""数据生成类"""
def __init__(self, m):
self.m = m # 样本容量
self.list_x = [] # 生成点的x坐标
self.list_y = [] # 生成点的y坐标
def generate(self):
"""生成两个坐标列表"""
for i in range(self.m):
# self.list_x.append(random.randint(1, 200)) # 其实是有瑕疵的,就是可能会有一个x对应多个y的情况,就当是测量误差吧
# self.list_y.append(random.randint(1, 200))
self.list_x.append(i) # 样本(1,1)(2,2)...
self.list_y.append(i)
def show(self):
"""用来输出随机生成的数据"""
for i in range(self.m):
print("(" + str(self.list_x[i]) + "," + str(self.list_y[i]) + ")") # 因为里面存的是数字,但现在连接是用的字符串语法,所有要显式转一下
class LinearRegressionForOneVariable:
"""单变量线性回归模型训练器"""
# 初始化
def __init__(self):
self.k = 0 # 变量系数
self.b = 0 # 常数项
# 训练
def train(self, m, list_x, list_y, epochs, ls): # 容量、x、y、循环次数、学习率
for _ in range(epochs): # 通过训练次数保证尽量收敛
t = 0 # 先算出那个求和项吧
for i in range(m):
t += self.k * list_x[i] + self.b - list_y[i]
t_b = self.b - ls * 1/m * t
t = 0
for i in range(m):
t += (self.k * list_x[i] + self.b - list_y[i]) * list_x[i]
t_k = self.k - ls * 1/m * t
self.b = t_b # 要保证是同时更新
self.k = t_k
if __name__ == "__main__":
m = 50 # 样本容量
# 产生训练数据
datagenerator = DataGenerator(m)
datagenerator.generate()
# datagenerator.show()
drawtool = DrawTool()
drawtool.drawPoint(datagenerator.list_x, datagenerator.list_y)
# 训练
model = LinearRegressionForOneVariable()
model.train(m, datagenerator.list_x, datagenerator.list_y, 500, 0.001)
print("k = " + str(model.k) + ", b = " + str(model.b))
# 数据可视化
drawtool.drawLine(model.k, model.b)