继续上一篇的内容python实现波士顿房价预测—(1)。
梯度计算公式中引入计算因子 1 2 \frac{1}{2} 21,为了计算更加简洁。
L = 1 2 N ∑ i = 1 N ( y i − z i ) 2 L=\frac{1}{2N}\sum_{i=1}^{N}(y_i - z_i)^2 L=2N1∑i=1N(yi−zi)2
其中 z i z_i zi是模型对于第i个样本的预测值:
z i = ∑ j = 0 12 x i j . w j + b z_i = \sum_{j=0}^{12}x_{i}^{j}.w_j + b zi=∑j=012xij.wj+b
梯度的定义:
g r a d i e n t = ( ∂ L ∂ w 0 , ∂ L ∂ w 1 , ∂ L ∂ w 2 , ∂ L ∂ w 3 , . . . ∂ L ∂ b ) gradient =(\frac{\partial L}{\partial w_0},\frac{\partial L}{\partial w_1},\frac{\partial L}{\partial w_2},\frac{\partial L}{\partial w_3},...\frac{\partial L}{\partial b}) gradient=(∂w0∂L,∂w1∂L,∂w2∂L,∂w3∂L,...∂b∂L)
所以基于上面的损失函数计算梯度的表达式,分别计算w和b的偏导数:
∂ L ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) ∂ z i ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) x i j \frac{\partial L}{\partial w_j}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i)\frac{\partial z_i}{\partial w_j}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i)x_{i}^{j} ∂wj∂L=N1∑i=1N(zi−yi)∂wj∂zi=N1∑i=1N(zi−yi)xij
b:
∂ L ∂ b = 1 N ∑ i = 1 N ( z i − y i ) ∂ z i ∂ b = 1 N ∑ i = 1 N ( z i − y i ) \frac{\partial L}{\partial b}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i)\frac{\partial z_i}{\partial b}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i) ∂b∂L=N1∑i=1N(zi−yi)∂b∂zi=N1∑i=1N(zi−yi)
前面为什么要加一个 1 2 \frac{1}{2} 21,是因为在求导的时候会产生一个2,整好抵消。
下面我们考虑只有一个样本情况来计算梯度:
L = 1 2 ( z i − y i ) 2 L=\frac{1}{2}(z_i-y_i)^2 L=21(zi−yi)2
第一个样本数据:
z 1 = x 1 0 . w 0 + x 1 1 . w 1 + x 1 2 . w 2 + x 1 3 . w 3 + x 1 4 . w 4 + x 1 5 . w 5 + x 1 6 . w 6 + x 1 7 . w 7 + . . . + x 1 12 . w 12 + b z_1=x_{1}^{0}.w_0+x_{1}^{1}.w_1+x_{1}^{2}.w_2+x_{1}^{3}.w_3+x_{1}^{4}.w_4+x_{1}^{5}.w_5+x_{1}^{6}.w_6+x_{1}^{7}.w_7+...+x_{1}^{12}.w_{12}+b z1=x10.w0+x11.w1+x12.w2+x13.w3+x14.w4+x15.w5+x16.w6+x17.w7+...+x112.w12+b
带入 L L L表达式中:
L = 1 2 ( x 1 0 . w 0 + x 1 1 . w 1 + x 1 2 . w 2 + b − y 1 ) 2 L=\frac{1}{2}(x_{1}^{0}.w_0+x_{1}^{1}.w_1+x_{1}^{2}.w_2+b -y_1)^2 L=21(x10.w0+x11.w1+x12.w2+b−y1)2
开始计算w和b的偏导数:
∂ L ∂ w 0 = ( x 1 0 . w 0 + x 1 1 . w 1 + x 1 2 . w 2 + b − y 1 ) . x 1 0 = ( z i − y i ) . x 1 0 \frac{\partial L}{\partial w_0}=(x_{1}^{0}.w_0+x_{1}^{1}.w_1+x_{1}^{2}.w_2+b -y_1).x_{1}^{0}=(z_i-y_i).x_{1}^{0} ∂w0∂L=(x10.w0+x11.w1+x12.w2+b−y1).x10=(zi−yi).x10
…
∂ L ∂ b = ( x 1 0 . w 0 + x 1 1 . w 1 + x 1 2 . w 2 + b − y 1 ) = ( z i − y i ) \frac{\partial L}{\partial b}=(x_{1}^{0}.w_0+x_{1}^{1}.w_1+x_{1}^{2}.w_2+b -y_1)=(z_i-y_i) ∂b∂L=(x10.w0+x11.w1+x12.w2+b−y1)=(zi−yi)
接下来通过代码查查数据。
x1 = x[0]
y1 = y[0]
z1 = net.forward(x1)
print('x1 {}, shape {}'.format(x1, x1.shape))
print('y1 {}, shape {}'.format(y1, y1.shape))
print('z1 {}, shape {}'.format(z1, z1.shape))
x1 [0. 0.18 0.07344184 0. 0.31481481 0.57750527
0.64160659 0.26920314 0. 0.22755741 0.28723404 1.
0.08967991], shape (13,)
y1 [0.42222222], shape (1,)
z1 [130.86954441], shape (1,)
按照上面推到的公式:
w0
gradient_w0 = (z1 - y1) * x1[0]
print('gradient_w0 {}'.format(gradient_w0))
w1
gradient_w1 = (z1 - y1) * x1[1]
print('gradient_w1 {}'.format(gradient_w1))
那么在Numpy里面的广播机制(向量和矩阵都是可以看做一个单一变量),可以快速实现梯度计算。计算梯度 ( z 1 − y 1 ) . x 1 (z_1-y_1).x_1 (z1−y1).x1得到一个13维度的向量,表示在第一个样本在第一层神经网络中每一个神经元的影像。
gradient_w = (z1 - y1) * x1
print('gradient_w_by_sample1 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输入数据中有多个样本,每个样本都对梯度有贡献。如上代码计算了只有样本1时的梯度值,同样的计算方法也可以计算样本2和样本3对梯度的贡献。
x2 = x[1]
y2 = y[1]
z2 = net.forward(x2)
gradient_w = (z2 - y2) * x2
print('gradient_w_by_sample2 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
可能有的读者再次想到可以使用for循环把每个样本对梯度的贡献都计算出来,然后再作平均。但是我们不需要这么做,仍然可以使用Numpy的矩阵操作来简化运算,如3个样本的情况。
# 注意这里是一次取出3个样本的数据,不是取出第3个样本
x3samples = x[0:3]
y3samples = y[0:3]
z3samples = net.forward(x3samples)
print('x {}, shape {}'.format(x3samples, x3samples.shape))
print('y {}, shape {}'.format(y3samples, y3samples.shape))
print('z {}, shape {}'.format(z3samples, z3samples.shape))
x [[0.00000000e+00 1.80000000e-01 7.34418420e-02 0.00000000e+00
3.14814815e-01 5.77505269e-01 6.41606591e-01 2.69203139e-01
0.00000000e+00 2.27557411e-01 2.87234043e-01 1.00000000e+00
8.96799117e-02]
[2.35922539e-04 0.00000000e+00 2.62405717e-01 0.00000000e+00
1.72839506e-01 5.47997701e-01 7.82698249e-01 3.48961980e-01
4.34782609e-02 1.14822547e-01 5.53191489e-01 1.00000000e+00
2.04470199e-01]
[2.35697744e-04 0.00000000e+00 2.62405717e-01 0.00000000e+00
1.72839506e-01 6.94385898e-01 5.99382080e-01 3.48961980e-01
4.34782609e-02 1.14822547e-01 5.53191489e-01 9.87519166e-01
6.34657837e-02]], shape (3, 13)
y [[0.42222222]
[0.36888889]
[0.66 ]], shape (3, 1)
z [[130.86954441]
[108.34434338]
[131.3204395 ]], shape (3, 1)
上面的x3samples, y3samples, z3samples的第一维大小均为3,表示有3个样本。下面计算这3个样本对梯度的贡献。
gradient_w = (z3samples - y3samples) * x3samples
print('gradient_w {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
gradient_w [[0.00000000e+00 2.34805180e+01 9.58029163e+00 0.00000000e+00
4.10667496e+01 7.53340159e+01 8.36958617e+01 3.51168286e+01
0.00000000e+00 2.96842549e+01 3.74689117e+01 1.30447322e+02
1.16985043e+01]
[2.54738434e-02 0.00000000e+00 2.83333765e+01 0.00000000e+00
1.86624242e+01 5.91703008e+01 8.45121992e+01 3.76793284e+01
4.69458498e+00 1.23980167e+01 5.97311025e+01 1.07975454e+02
2.20777626e+01]
[3.07963708e-02 0.00000000e+00 3.42860463e+01 0.00000000e+00
2.25832858e+01 9.07287666e+01 7.83155260e+01 4.55955257e+01
5.68088867e+00 1.50027645e+01 7.22802431e+01 1.29029688e+02
8.29246719e+00]], gradient.shape (3, 13)
上面 g r a d i e n t w gradient_w gradientw的维度是(3,13)。第一行的数据与前面x1计算的梯度是一样的,第二行与x2的梯度一致。所以我们看到Numpy的广播机制带来的便捷:
z = net.forward(x)
gradient_w = (z - y) * x
print('gradient_w shape {}'.format(gradient_w.shape))
print(gradient_w)
gradient_w shape (404, 13)
[[0.00000000e+00 2.34805180e+01 9.58029163e+00 ... 3.74689117e+01
1.30447322e+02 1.16985043e+01]
[2.54738434e-02 0.00000000e+00 2.83333765e+01 ... 5.97311025e+01
1.07975454e+02 2.20777626e+01]
[3.07963708e-02 0.00000000e+00 3.42860463e+01 ... 7.22802431e+01
1.29029688e+02 8.29246719e+00]
...
[3.97706874e+01 0.00000000e+00 1.74130673e+02 ... 2.01043762e+02
2.48659390e+02 1.27554582e+02]
[2.69696515e+01 0.00000000e+00 1.75225687e+02 ... 2.02308019e+02
2.34270491e+02 1.28287658e+02]
[6.08972123e+01 0.00000000e+00 1.53017134e+02 ... 1.76666981e+02
2.18509161e+02 1.08772220e+02]]
我们可以看到在numpy的广播机制下,计算损失函数对于w的偏导数结果(403,13)维度,每一行表示一个样本对于总梯度的贡献,根据计算公式,总梯度w等于每个样本的梯度的平均值:
∂ L ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) ∂ z i ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) x i j \frac{\partial L}{\partial w_j}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i)\frac{\partial z_i}{\partial w_j}=\frac{1}{N}\sum_{i=1}^{N}(z_i-y_i)x_{i}^{j} ∂wj∂L=N1∑i=1N(zi−yi)∂wj∂zi=N1∑i=1N(zi−yi)xij
使用numpy的均值函数:
# axis = 0 表示把每一行做相加然后再除以总的行数
gradient_w = np.mean(gradient_w, axis=0)
print('gradient_w ', gradient_w.shape)
print('w ', net.w.shape)
print(gradient_w)
print(net.w)
gradient_w (13,)
w (13, 1)
[ 4.6555403 19.35268996 55.88081118 14.00266972 47.98588869
76.87210821 94.8555119 36.07579608 45.44575958 59.65733292
83.65114918 134.80387478 38.93998153]
[[ 1.76405235e+00]
[ 4.00157208e-01]
[ 9.78737984e-01]
[ 2.24089320e+00]
[ 1.86755799e+00]
[ 1.59000000e+02]
[ 9.50088418e-01]
[-1.51357208e-01]
[-1.03218852e-01]
[ 1.59000000e+02]
[ 1.44043571e-01]
[ 1.45427351e+00]
[ 7.61037725e-01]]
更新函数里面梯度下降的函数
class Network(object):
def __init__(self, num_of_weights):
# 随机产生w的初始值
# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
np.random.seed(0)
self.w = np.random.randn(num_of_weights, 1)
self.b = 0.
def forward(self, x):
z = np.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z-y)*x
gradient_w = np.mean(gradient_w, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
return gradient_w, gradient_b
查看一下w5,w9对应的loss值
# 调用上面定义的gradient函数,计算梯度
# 初始化网络
net = Network(13)
# 设置[w5, w9] = [-100., -100.]
net.w[5] = -100.0
net.w[9] = -100.0
z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
首先沿着梯度的反方向移动一小步,找到下一个点P1,观察损失函数的变化。
# 在[w5, w9]平面上,沿着梯度的反方向移动到下一个点P1
# 定义移动步长 eta
eta = 0.1
# 更新参数w5和w9
net.w[5] = net.w[5] - eta * gradient_w5
net.w[9] = net.w[9] - eta * gradient_w9
# 重新计算z和loss
z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
point [-95.41203171187678, -96.4497631155171], loss 7214.694816482369
gradient [-43.883932999069096, -34.019273908495926]
上面语句中梯度的更新:
net.w[5] = net.w[5] - eta * gradient_w5
其中有两个点:
这里大家可以思考一下?为什么在数据处理阶段需要做归一化操作,保持尺度一致?这是为了统一步长更加合适。
图8,特征输入归一化后,不同参数输出的Loss是一个比较规整的曲线,学习率可以设置成统一的值 ;特征输入未归一化时,不同特征对应的参数所需的步长不一致,尺度较大的参数需要大步长,尺寸较小的参数需要小步长,导致无法设置统一的学习率。
自定义类更新,训练函数train。
class Network(object):
def __init__(self, num_of_weights):
# 随机产生w的初始值
# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
np.random.seed(0)
self.w = np.random.randn(num_of_weights,1)
self.w[5] = -100.
self.w[9] = -100.
self.b = 0.
def forward(self, x):
z = np.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z-y)*x
gradient_w = np.mean(gradient_w, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
return gradient_w, gradient_b
def update(self, gradient_w5, gradient_w9, eta=0.01):
net.w[5] = net.w[5] - eta * gradient_w5
net.w[9] = net.w[9] - eta * gradient_w9
def train(self, x, y, iterations=100, eta=0.01):
points = []
losses = []
for i in range(iterations):
points.append([net.w[5][0], net.w[9][0]])
z = self.forward(x)
L = self.loss(z, y)
gradient_w, gradient_b = self.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
self.update(gradient_w5, gradient_w9, eta)
losses.append(L)
if i % 50 == 0:
print('iter {}, point {}, loss {}'.format(i, [net.w[5][0], net.w[9][0]], L))
return points, losses
# 获取数据
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 创建网络
net = Network(13)
num_iterations=2000
# 启动训练
points, losses = net.train(x, y, iterations=num_iterations, eta=0.01)
# 画出损失函数的变化趋势
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
上面演示只是包含了w5和w9两个参数。但是房价预测的完整模型,必须要对所有参数w和b求解。修改类中的update和train函数。
class Network(object):
def __init__(self, num_of_weights):
# 随机产生w的初始值
# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
np.random.seed(0)
self.w = np.random.randn(num_of_weights, 1)
self.b = 0.
def forward(self, x):
z = np.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z-y)*x
gradient_w = np.mean(gradient_w, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
return gradient_w, gradient_b
def update(self, gradient_w, gradient_b, eta = 0.01):
self.w = self.w - eta * gradient_w
self.b = self.b - eta * gradient_b
def train(self, x, y, iterations=100, eta=0.01):
losses = []
for i in range(iterations):
z = self.forward(x)
L = self.loss(z, y)
gradient_w, gradient_b = self.gradient(x, y)
self.update(gradient_w, gradient_b, eta)
losses.append(L)
if (i+1) % 10 == 0:
print('iter {}, loss {}'.format(i, L))
return losses
# 获取数据
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 创建网络
net = Network(13)
num_iterations=1000
# 启动训练
losses = net.train(x,y, iterations=num_iterations, eta=0.01)
# 画出损失函数的变化趋势
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
在上述梯度计算过程中,每次都是基于全部数据,对于波士顿房价预测数据集,样本少。但是在实际问题中,数据集都是非常大,每次使用全量数据效率非常低。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(SGD)。核心概念:
import numpy as np
class Network(object):
def __init__(self, num_of_weights):
# 随机产生w的初始值
# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
#np.random.seed(0)
self.w = np.random.randn(num_of_weights, 1)
self.b = 0.
def forward(self, x):
z = np.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def gradient(self, x, y):
z = self.forward(x)
N = x.shape[0]
gradient_w = 1. / N * np.sum((z-y) * x, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_b = 1. / N * np.sum(z-y)
return gradient_w, gradient_b
def update(self, gradient_w, gradient_b, eta = 0.01):
self.w = self.w - eta * gradient_w
self.b = self.b - eta * gradient_b
def train(self, training_data, num_epochs, batch_size=10, eta=0.01):
n = len(training_data)
losses = []
for epoch_id in range(num_epochs):
# 在每轮迭代开始之前,将训练数据的顺序随机打乱
# 然后再按每次取batch_size条数据的方式取出
np.random.shuffle(training_data)
# 将训练数据进行拆分,每个mini_batch包含batch_size条的数据
mini_batches = [training_data[k:k+batch_size] for k in range(0, n, batch_size)]
for iter_id, mini_batch in enumerate(mini_batches):
#print(self.w.shape)
#print(self.b)
x = mini_batch[:, :-1]
y = mini_batch[:, -1:]
a = self.forward(x)
loss = self.loss(a, y)
gradient_w, gradient_b = self.gradient(x, y)
self.update(gradient_w, gradient_b, eta)
losses.append(loss)
print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.
format(epoch_id, iter_id, loss))
return losses
# 获取数据
train_data, test_data = load_data()
# 创建网络
net = Network(13)
# 启动训练
losses = net.train(train_data, num_epochs=50, batch_size=100, eta=0.1)
# 画出损失函数的变化趋势
plot_x = np.arange(len(losses))
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
观察上述损失函数的变化,随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失ii,所以损失下降曲线会出现震荡。
————————————————————————
说明
由于房价预测的数据集样本太少,感受不到随机梯度下将带来的性能提升。
————————————————————————
在使用numpy构建神经网络三点要素: