1.原理
首先粗略的讲一下原理,如果这部分你看起来有难度,建议去看看西瓜书或者其他作者的博客。
我们知道线性回归的损失函数如下:
L
o
s
s
=
1
N
∑
i
=
1
N
(
y
i
−
(
w
x
i
+
b
)
)
2
Loss=\frac{1}{N}\sum_{i=1}^N(y_i-(wx_i+b))^2
Loss=N1i=1∑N(yi−(wxi+b))2
我们所要做的是:
通过寻找最优的参数
w
,
b
w,b
w,b,使得上述损失函数最小。
你可以通过求偏导来实现,但是这种方法的适应范围在机器学习领域十分有限,所以我更推荐你使用更为通用的方法:梯度下降法。
所谓梯度下降法,是通过让参数通过梯度去更新自身。显然,损失函数对两个参数的梯度分别为:
∂
L
o
s
s
∂
w
=
−
2
N
∑
i
=
1
N
x
i
(
y
i
−
(
w
x
i
+
b
)
)
\frac{\partial Loss}{\partial w}=-\frac{2}{N}\sum_{i=1}^Nx_i(y_i-(wx_i+b))
∂w∂Loss=−N2i=1∑Nxi(yi−(wxi+b))
∂
L
o
s
s
∂
b
=
−
2
N
∑
i
=
1
N
(
y
i
−
(
w
x
i
+
b
)
)
\frac{\partial Loss}{\partial b}=-\frac{2}{N}\sum_{i=1}^N(y_i-(wx_i+b))
∂b∂Loss=−N2i=1∑N(yi−(wxi+b))
以参数w为例,其更新的策略为:
w
=
w
−
∂
L
o
s
s
∂
w
w=w-\frac{\partial Loss}{\partial w}
w=w−∂w∂Loss
其余参数类似。
2.各个模块的实现
2.1 初始化模块
首先,作为一个类,该模型需要传入一些必要的参数,比如特征与目标:
class LinearRegression:
def __init__(self, data, target, ):
self.data = data
self.target = target
n_feature = data.shape[1]
self.w = np.zeros(n_feature)
self.b = np.zeros(1)
2.2 预测模块
作为一个机器学习模型,预测模块是必不可少的,也是最基础的模块,这在神经网络中被称为前向传播模块:
def predict(self):
out = self.data.dot(self.w) + self.b
return out
2.3 梯度计算模块
在这个模块中,我们计算当前损失函数对当前参数的梯度,为此,我们需要使用第一节中的两个梯度公式:
∂
L
o
s
s
∂
w
=
−
2
N
∑
i
=
1
N
x
i
(
y
i
−
(
w
x
i
+
b
)
)
\frac{\partial Loss}{\partial w}=-\frac{2}{N}\sum_{i=1}^Nx_i(y_i-(wx_i+b))
∂w∂Loss=−N2i=1∑Nxi(yi−(wxi+b))
∂
L
o
s
s
∂
b
=
−
2
N
∑
i
=
1
N
(
y
i
−
(
w
x
i
+
b
)
)
\frac{\partial Loss}{\partial b}=-\frac{2}{N}\sum_{i=1}^N(y_i-(wx_i+b))
∂b∂Loss=−N2i=1∑N(yi−(wxi+b))
代码:
def gradient(self):
"""计算损失函数对w,b的梯度"""
sample_num = self.data.shape[0]
dw = (-2 / sample_num) * np.sum(self.data.T.dot(self.target - self.predict()))
db = (-2 / sample_num) * np.sum(self.target - self.predict())
return dw, db
2.4 训练模块
在这个模块中,通过梯度下降法去更新参数,实际上这也是训练(学习)的过程,既然是训练(学习),则必须要有训练的次数,即max_iter和学习率alpha:
def train(self, alpha=0.01, max_iter=200):
"""训练"""
loss_history = []
for i in range(max_iter):
dw, db = self.gradient()
self.w -= alpha * dw
self.b -= alpha * db
now_loss = self.loss()
loss_history.append(now_loss)
return loss_history
2.5 其他模块
如果你足够细心,你可能已经发现,在train模块中,有一个self.loss()方法,这个方法是用来计算当前预测值与真实值之间的误差(损失)的:
def loss(self):
"""误差,损失"""
sample_num = self.data.shape[0]
pre = self.predict()
loss = (1 / sample_num) * np.sum((self.target - pre) ** 2)
return loss
通过这个方法,将每次迭代得到的误差记录下来,便于我们观察训练情况,不仅如此,如果你对神经网络有所了解,你会发现这是一种非常常见的好方法。
3.实例化并训练
选取你的训练集,将其传入实例:
my_lr = LinearRegression(data=x_train_s, target=y_train)
loss_list = my_lr.train()
plt.plot(np.arange(len(loss_list)),loss_list,'r--')
plt.xlabel('iter num')
plt.ylabel('$Loss$')
plt.show()
结果:

这和我们的期望是一致的,说明训练成功。
也许你的程序会抛出这样的警告:
RuntimeWarning: overflow encountered in multiply
return 2 * self.X.T.dot(self.X.dot(self.w_hat) - self.y)
那是因为你可能忘记将你的数据进行标准化处理了,因为线性回归是要求样本近似服从正态分布的。
除此之外,还有其他归一化的好处:(摘自https://blog.csdn.net/weixin_43772533/article/details/100826616)
理论层面上,神经网络是以样本在事件中的统计分布概率为基础进行训练和预测的,所以它对样本数据的要求比较苛刻。具体说明如下:
1.样本的各个特征的取值要符合概率分布,即[0,1]
2.样本的度量单位要相同。我们并没有办法去比较1米和1公斤的区别,但是,如果我们知道了1米在整个样本中的大小比例,以及1公斤在整个样本中的大小比例,比如一个处于0.2的比例位置,另一个处于0.3的比例位置,就可以说这个样本的1米比1公斤要小!
3.神经网络假设所有的输入输出数据都是标准差为1,均值为0,包括权重值的初始化,激活函数的选择,以及优化算法的的设计。
4.数值问题
归一化可以避免一些不必要的数值问题。因为激活函数sigmoid/tanh的非线性区间大约在[-1.7,1.7]。意味着要使神经元有效,线性计算输出的值的数量级应该在1(1.7所在的数量级)左右。这时如果输入较大,就意味着权值必须较小,一个较大,一个较小,两者相乘,就引起数值问题了。
5.梯度更新
若果输出层的数量级很大,会引起损失函数的数量级很大,这样做反向传播时的梯度也就很大,这时会给梯度的更新带来数值问题。
6.学习率
知道梯度非常大,学习率就必须非常小,因此,学习率(学习率初始值)的选择需要参考输入的范围,不如直接将数据归一化,这样学习率就不必再根据数据范围作调整。 对w1适合的学习率,可能相对于w2来说会太小,若果使用适合w1的学习率,会导致在w2方向上步进非常慢,会消耗非常多的时间,而使用适合w2的学习率,对w1来说又太大,搜索不到适合w1的解。