单层神经网络解决一元线性回归问题

问题背景

已知在广州,日间出租车费用计算为:三公里内12元。超过三公里的部分2.6元/公里 不考虑候时费、返空费。

分段函数表达如下:

此时,给出x,就能够计算出y,即已知自变量和对应关系,求因变量。

而机器学习,是已知自变量、因变量,从中学习对应规则的问题。因此,假设现在获得一些乘车费用数据,通过一元线性回归方法学习对应法则。

导包

1
import numpy as np

加载数据

为了简化问题,我们假设获得的数据中,行程distance都是三公里以上($distance>3$)。

令$x = distance-3$,即x是超出3公里的部分,那么根据上文的分段函数可知$y = 2.6x + 12$。

现在我们生成一些符合这个对应法则的数据,然后构造一个模型,在仅知道自变量、因变量的情况下,得到对应法则。

1
2
3
4
5
6
7
def load_data():
np.random.seed(1)
# 生成 0-10 之间的 100 个随机数,代表超过 3 公里的里程数
_x = np.random.randint(low=0, high=10, size=[100, ])
# 根据对应法则算出价格
_y = 2.6 * _x + 12
return _x, _y

运行load_data()函数的结果为:

生成的数据 >folded
1
2
3
4
5
6
7
8
9
10
11
x:[5 8 9 5 0 0 1 7 6 9 2 4 5 2 4 2 4 7 7 9 1 7 0 6 9 9 7 6 9 1 0 1 8 8 3 9 8
7 3 6 5 1 9 3 4 8 1 4 0 3 9 2 0 4 9 2 7 7 9 8 6 9 3 7 7 4 5 9 3 6 8 0 2 7
7 9 7 3 0 8 7 7 1 1 3 0 8 6 4 5 6 2 5 7 8 4 4 7 7 4]
y:[25. 32.8 35.4 25. 12. 12. 14.6 30.2 27.6 35.4 17.2 22.4 25. 17.2
22.4 17.2 22.4 30.2 30.2 35.4 14.6 30.2 12. 27.6 35.4 35.4 30.2 27.6
35.4 14.6 12. 14.6 32.8 32.8 19.8 35.4 32.8 30.2 19.8 27.6 25. 14.6
35.4 19.8 22.4 32.8 14.6 22.4 12. 19.8 35.4 17.2 12. 22.4 35.4 17.2
30.2 30.2 35.4 32.8 27.6 35.4 19.8 30.2 30.2 22.4 25. 35.4 19.8 27.6
32.8 12. 17.2 30.2 30.2 35.4 30.2 19.8 12. 32.8 30.2 30.2 14.6 14.6
19.8 12. 32.8 27.6 22.4 25. 27.6 17.2 25. 30.2 32.8 22.4 22.4 30.2
30.2 22.4]

这里用了我们已经知道的、待预测的运算法则来生成数据,并不是直接告诉模型运算法则,而是为了方便的生成数据并且只告诉模型这些数据,模型并不知道数据背后的法则,需要学习才能得知背后的规律。

构造模型

先上全部的模型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TaxiFareNetWork:
def __init__(self) -> None:
super().__init__()
self.w = np.random.rand(1, ) * 10
self.b = np.random.rand(1, ) * 10

# 前向计算,进行预测
def forward(self, _x):
return _x * self.w + self.b

# 计算损失
def loss(self, _x, _label):
_predict = self.forward(_x)
# 为什么×0.5?是为了和求导的2抵消
return 0.5 * np.sum((_predict - _label) ** 2)

# 计算梯度,更新参数
def backward(self, _x, _label, _lr):
w_grad = np.sum((self.w * _x + self.b - _label) * _x)
b_grad = np.sum(self.w * _x + self.b - _label)
print("w_grad:{}".format(w_grad))
print("b_grad:{}".format(b_grad))
self.w -= w_grad * _lr
self.b -= b_grad * _lr

函数解释:

__init__(self)

在初始化一个网络时,将此网络中的wb进行随机初始化。

初始化的范围是1-10,因为凭借经验,超过三公里后的每公里的路程十几元比较贴切实际。(其实直接0-1之间也是可以的,只是更贴合实际的话刚开始会学习的快一点 )

或许会疑惑,wb都是一个数字,为什么要用numpy数组?这是为了实现批量运算,用到了numpy的广播功能。

forward(self, _x)

前向计算,进行预测的函数。使用目前网络中的wb进行预测y的值,第一次调用的时候使用的是随机初始化的wb

_x是待计算的公里数,这个_x不是一个数字,而是一个numpy数组。

loss(self, _x, _label)

计算损失,用预测值与实际值的距离来衡量模型的好坏,即$loss = \left | 预测值 - 实际值 \right |$,对于单个数据是这么计算,对于批处理的loss,就是对他们进行加和,即$loss=\sum \left | 预测值 - 实际值 \right |$,使用平方替代绝对值运算,就得到了这里的损失函数:$loss=\sum (预测值 - 实际值)^2$

backward(self, _x, _label, _lr)

反向传播

loss是关于wb的函数,$loss = f(w, b)$
也就是说,wb决定着loss的大小,而想知道wb的变化各自如何影响loss的大小,这就需要用到偏导数。

$loss = f(w, b) = \frac{1}{2} \sum \left [ f(x_{i}) - y_{i} \right ]^2 = \frac{1}{2} \sum (wx_{i}+b-y_{i})^2$

$\frac{1}{2}$的作用体现出来了,即消去偏导数中的2倍。

对w的偏导数:$\frac{\alpha(loss)}{\alpha(w)} = \sum (wx_{i}+b-y_{i})*x_{i}$

对w的偏导数:$\frac{\alpha(loss)}{\alpha(b)} = \sum (wx_{i}+b-y_{i})$

偏导数的Python表示 >folded
1
2
w_grad = np.sum((self.w * _x + self.b - _label) * _x)
b_grad = np.sum(self.w * _x + self.b - _label)

然后进行参数的更新,用当前参数$梯度 \times 学习率$。

为什么是不是
导数指的是变化方向,比如$y = x^2$,$y’(2)=4>0$,正数意味着在$x=2$时,如果x继续变大,那么y也会变大(也就是,导数大于零,函数递增)。

偏导数同理,如果$\frac{\alpha(loss)}{\alpha(w)}$大于零,意味着如果w继续增大,loss也会增大。因此这个时候,我们需要让w变小,也就是让$w-偏导数 \times 学习率$。

反过来,如果$\frac{\alpha(loss)}{\alpha(w)}$小于零,意味着如果w继续增大,loss会减少。这个时候,我们需要让w变大,也就是让$w-偏导数 \times 学习率$。(注意:这里$偏导数 \times 学习率$为负数,因此负负得正,w变大)

b的变化与w相同。

开始训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import matplotlib.pyplot as plt

if __name__ == '__main__':
x, y = load_data()
net = TaxiFareNetWork()
print("初始化的参数 w 为:{}".format(net.w))
print("初始化的参数 b 为:{}".format(net.b))
loss = []
i = 1
while i <= 4000:
i += 1
predict = net.forward(x)
print("第{}次的 loss 是:{}".format(i, net.loss(x, y)))
loss.append(net.loss(x, y))
net.backward(x, y, 0.0003)
print("反向传播{}次后的参数 w 为:{}".format(i, net.w))
print("反向传播{}次后的参数 b 为:{}".format(i, net.b))
print("参数 w 为:{}".format(net.w))
print("参数 b 为:{}".format(net.b))
plt.plot(range(4000), loss)
plt.axis('on')
plt.xlim(-1, 5) # 先看看前五次的 loss 下降情况
plt.ylim(-1, 30000)
plt.show()

训练日志:

点击展开训练日志 >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
初始化的参数 w 为:[0.36126102]
初始化的参数 b 为:[0.57100856]
1次的 loss 是:27980.125927155328
w_grad:-13390.06942482138
b_grad:-2273.4623286746223
反向传播1次后的参数 w 为:[4.37828185]
反向传播1次后的参数 b 为:[1.25304726]
2次的 loss 是:1504.3688073626174
w_grad:624.2819937729694
b_grad:-176.66294095394446
反向传播2次后的参数 w 为:[4.19099725]
反向传播2次后的参数 b 为:[1.30604614]
3次的 loss 是:1432.896342611712

...

4000次的 loss 是:3.4469435025813155e-23
反向传播4000次后的参数 w 为:[2.6]
反向传播4000次后的参数 b 为:[12.]
4001次的 loss 是:3.3926864383811195e-23
反向传播4001次后的参数 w 为:[2.6]
反向传播4001次后的参数 b 为:[12.]
参数 w 为:[2.6]
参数 b 为:[12.]

前五轮的 loss 图像:

使用VisualDL可视化工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from visualdl import LogWriter

if __name__ == '__main__':
x, y = load_data()
net = TaxiFareNetWork()
print("初始化的参数 w 为:{}".format(net.w))
print("初始化的参数 b 为:{}".format(net.b))
i = 1
with LogWriter(logdir="./log/scalar_test/train") as writer:
while i <= 4000:
predict = net.forward(x)
# print("第{}次的 loss 是:{}".format(i, net.loss(x, y)))
# 使用scalar组件记录一个标量数据
writer.add_scalar(tag="loss", step=i, value=net.loss(x, y))
net.backward(x, y, 0.0003)
# print("反向传播{}次后的参数 w 为:{}".format(i, net.w))
# print("反向传播{}次后的参数 b 为:{}".format(i, net.b))
writer.add_scalar(tag="w", step=i, value=net.w[0])
writer.add_scalar(tag="b", step=i, value=net.b[0])
i += 1
print("参数 w 为:{}".format(net.w))
print("参数 b 为:{}".format(net.b))

loss的变化
b的变化
w的变化

扩展

这里每次梯度的更新都使用了全部的数据计算梯度,数据生成器只生成了100个数据,但是如果是100万个数据呢?使用全部的数据来计算下一小步的距离未免太不划算了,因此可以每次都使用其中随机抽取的一部分,而不是所有的数据一起,来计算梯度。

END

文章内容有误?请在下方评论反馈!

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×