专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构,使之不断改善自身的性能。
构成算法的三要素:模型假设->损失函数->优化算法
机器学习的本质就是参数估计
最终目标都是拟合一个大的函数f:
两者在理论结构上是一致的,即:模型假设、评价函数和优化算法,其根本差别在于假设的复杂度。很多时候要训练的图形是很复杂的,比如识别一个人是美女还是帅哥,在肉眼看来很简单,但是对于计算机来说,只能从数字矩阵上入手,整个模型的复杂度难以想象。
因此,出现了神经网络模型,如下
人工神经网络包括多个神经网络层,如:卷积层、全连接层、LSTM
等,每一层又包括很多神经元,超过三层的非线性神经网络都可以被称为深度神经网络。通俗的讲,深度学习的模型可以视为是输入到输出的映射函数,如图像到高级语义(美女)的映射,足够深的神经网络理论上可以拟合任何复杂的函数。因此神经网络非常适合学习样本数据的内在规律和表示层次,对文字、图像和语音任务有很好的适用性。
Python
和NumPy
构建神经网络模型一共有13个影响因素:
要预测同类房屋价格的中位数
根据预测输出的类型是连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。
- **回归问题:**输出的结果是连续的值,如身高、体重
- **离散问题:**输出的结果是离散的值,如性别
线形假设:模型的求解即是通过数据拟合出每个wj和b。其中,wj和b分别表示该线性模型的权重和偏置。一维情况下,wj和b是直线的斜率和截距。
线性回归模型使用均方误差作为损失函数(Loss),用以衡量预测房价和真实房价的差异,公式如下:
上图为线性回归模型的神经网络结构。
数据处理包含五个部分:**数据导入、数据形状变换、数据集划分、数据归一化处理和封装load data
函数。**数据预处理后,才能被模型调用。
# 导入需要用到的package
import numpy as np
import json
# 读入训练数据
datafile = './work/housing.data'
data = np.fromfile(datafile, sep=' ')
可以看到这样读入的数据是连成一排的数据,但是我们期望看到的是这样的效果:
所以我们需要将数据的形状进行简单的转变,即将一维的数据变成二维的。
# 读入之后的数据被转化成1维array,其中array的第0-13项是第一条数据,第14-27项是第二条数据,以此类推....
# 这里对原始数据做reshape,变成N x 14的形式
feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS',
'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
feature_num = len(feature_names)
data = data.reshape([data.shape[0] // feature_num, feature_num])
# 查看数据
x = data[0]
print(x.shape)
print(x)
每条数据是一条长度为14的1-D array,前13项是影响房价的因素,最后一项是房价。
在本案例中,我们将80%的数据用作训练集,20%用作测试集,实现代码如下。通过打印训练集的形状,可以发现共有404个样本,每个样本含有13个特征和1个预测值。
ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data = data[:offset]
training_data.shape # 训练集形状,404行数据,每行14个值
对每个特征进行归一化处理,使得每个特征的取值缩放到0~1之间。这样做有两个好处:
# 计算train数据集的最大值,最小值,平均值
maximums, minimums, avgs = \
training_data.max(axis=0), \
training_data.min(axis=0), \
training_data.sum(axis=0) / training_data.shape[0]
# 对数据进行归一化处理
for i in range(feature_num):
#print(maximums[i], minimums[i], avgs[i])
data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
将上述几个部分的函数封装起来,便于之后操作。
def load_data():
# 从文件导入数据
datafile = './work/housing.data'
data = np.fromfile(datafile, sep=' ')
# 每条数据包括14项,其中前面13项是影响因素,第14项是相应的房屋价格中位数
feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \
'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
feature_num = len(feature_names)
# 将原始数据进行Reshape,变成[N, 14]这样的形状
data = data.reshape([data.shape[0] // feature_num, feature_num])
# 将原数据集拆分成训练集和测试集
# 这里使用80%的数据做训练,20%的数据做测试
# 测试集和训练集必须是没有交集的
ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data = data[:offset]
# 计算训练集的最大值,最小值,平均值
maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \
training_data.sum(axis=0) / training_data.shape[0]
# 对数据进行归一化处理
for i in range(feature_num):
#print(maximums[i], minimums[i], avgs[i])
data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
# 训练集和测试集的划分比例
training_data = data[:offset]
test_data = data[offset:]
return training_data, test_data
模型设计是深度学习模型关键要素之一,也称为网络结构设计,相当于模型的假设空间,即实现模型“前向计算”(从输入到输出)的过程。
如果将输入特征和输出预测值均以向量表示,输入特征x有13个分量,y有1个分量,那么参数权重的形状(shape)是13×1。
w = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3, -0.4, 0.0]
w = np.array(w).reshape([13, 1])
# 完整的线性回归公式,还需要初始化偏移量b,同样随意赋初值-0.2。那么,线性回归模型的完整输出是z = t + bz = t + bz = t + b,这个从特征和参数计算输出值的过程称为 “前向计算”。
b = -0.2
z = t + b
print(z)
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
基于NetWork
类的定义,模型计算过程如下:
net = Network(13)
x1 = x[0]
y1 = y[0]
z = net.forward(x1)
print(z)
从上述前向计算的过程可见,线性回归也可以表示成一种简单的神经网络(只有一个神经元,且激活函数为恒等式)。这也是机器学习模型普遍为深度学习模型替代的原因:由于深度学习网络强大的表示能力,很多传统机器学习模型的学习能力等同于相对简单的深度学习模型。
模型设计完成后,需要通过训练配置寻找模型的最优值,即通过损失函数来衡量模型的好坏。训练配置也是深度学习模型关键要素之一。
通过模型计算x1
表示的影响因素所对应的房价应该是z
, 但实际数据告诉我们房价是y
。这时我们需要有某种指标来衡量预测值z
跟真实值y
之间的差距。对于回归问题,最常采用的衡量方法是使用均方误差作为评价模型好坏的指标,具体定义如下:
L o s s = 1 N ∑ i = 1 N ( y i − z i ) 2 Loss = \frac{1}{N}\sum_{i=1}^{N}{(y_i-z_i)^2} Loss=N1i=1∑N(yi−zi)2
上式中的Loss
(简记为: L
)通常也被称作损失函数,它是衡量模型好坏的指标。如果要衡量预测放假和真实房价之间的差距,是否将每一个样本的差距的绝对值加和即可?差距绝对值加和是更加直观和朴素的思路,为何要平方加和? 损失函数的设计不仅要考虑准确衡量问题的“合理性”,通常还要考虑“易于优化求解”。至于这个问题的答案,在介绍完优化算法后再揭示。
在回归问题中,均方误差是一种比较常见的形式,分类问题中通常会采用交叉熵作为损失函数。
NetWork
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
cost = error * error
cost = np.mean(cost)
return cost
使用定义的Network类,可以方便的计算预测值和损失函数。需要注意的是,类中的变量x, w,b, z, error等均是向量。以变量x为例,共有两个维度,一个代表特征数量(值为13),一个代表样本数量,代码如下所示。
net = Network(13)
# 此处可以一次性计算多个样本的预测值和损失函数
x1 = x[0:3]
y1 = y[0:3]
z = net.forward(x1)
print('predict: ', z)
loss = net.loss(z, y1)
print('loss:', loss)
即求以下方程组的解:
∂ L ∂ w = 0 \frac{∂L}{∂w}=0 ∂w∂L=0
∂ L ∂ b = 0 \frac{∂L}{∂b}=0 ∂b∂L=0
其中L表示的是损失函数的值,w为模型权重,b为偏置项。w和b均为要学习的模型参数。
L = 1 N ( y − ( X w + b ) ) 2 L=\frac1N(y-(Xw+b))^2 L=N1(y−(Xw+b))2
其中y为N个样本的标签值构成的向量,形状为N×1;X为N个样本特征向量构成的矩阵,形状为N×D,D为数据特征长度;w为权重向量,形状为D×1;b为所有元素都为b的向量,形状为N×1。
但是当损失函数不是均方差这种简单的函数时,就会变得难以求解。
此时我们可以伸脚探索身边的坡度,但看不见远方的谷底。->梯度下降法
其中选择下一个[w5′,w9′]至关重要,要保证以下两点:
**微积分:**沿着梯度的反方向是函数值下降最快的方向。
计算梯度的前提:
基于NumPy
广播机制(对向量和矩阵计算如同对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))
x3 = x[2]
y3 = y[2]
z3 = net.forward(x3)
gradient_w = (z3 - y3) * x3
print('gradient_w_by_sample3 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
如何一次计算出所有样本的梯度的所有权重的梯度贡献?——————–NumPy矩阵广播
z = net.forward(x)
gradient_w = (z - y) * x
print('gradient_w shape {}'.format(gradient_w.shape))
print(gradient_w)
上面gradient_w的每一行代表了一个样本对梯度的贡献。根据梯度的计算公式,总梯度是对每个样本对梯度贡献的平均值。
# 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)
使用NumPy
的矩阵操作方便地完成了gradient的计算,但引入了一个问题,gradient_w
的形状是(13,),而w的维度是(13, 1)。导致该问题的原因是使用np.mean
函数时消除了第0维。为了加减乘除等计算方便,gradient_w
和w必须保持一致的形状。因此我们将gradient_w
的维度也设置为(13,1),代码如下:
gradient_w = gradient_w[:, np.newaxis]
print('gradient_w shape', gradient_w.shape)
将上面计算w和b的梯度的过程,写成Network类的gradient
函数,实现方法如下所示。
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
计算梯度的流程
1~4循环往复,直到损失达到最小值
将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=1000, 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=2000
# 启动训练
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()
在上述程序中,每次损失函数和梯度计算都是基于数据集中的全量数据。对于波士顿房价预测任务数据集而言,样本数比较少,只有404个。但在实际问题中,数据集往往非常大,如果每训练一次都用全量数据进行计算,效率非常低。由于参数每次只沿着梯度反方向更新一点点,因此方向并不需要那么精确。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
下面结合程序介绍具体的实现过程,涉及到数据处理和训练过程两部分代码的修改。
数据处理需要实现拆分数据批次和样本乱序(为了实现随机抽样的效果)两个功能。
关于随机抽取的作用还有:在模型训练的过程中,最后的几批训练集对于整个模型参数的影响是最大的,所以对于一些天然的数据本身就分布地不好的(最后面的数据全是某一类的),更应该用随机抽取方法。
将train_data分成大小为batch_size的多个mini_batch,如下代码所示:将train_data分成 404/10 + 1 = 41 个 mini_batch,其中前40个mini_batch,每个均含有10个样本,最后一个mini_batch只含有4个样本。
batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
print('total number of mini_batches is ', len(mini_batches))
print('first mini_batch shape ', mini_batches[0].shape)
print('last mini_batch shape ', mini_batches[-1].shape)
另外,这里是按顺序读取mini_batch,而SGD里面是随机抽取一部分样本代表总体。为了实现随机抽样的效果,我们先将train_data里面的样本顺序随机打乱,然后再抽取mini_batch。随机打乱样本顺序,需要用到np.random.shuffle
函数。特点(以二维矩阵为例),只会将第0维进行打乱操作,但是第1维不会改变顺序。
最终完整代码如下:
# 获取数据
train_data, test_data = load_data()
# 打乱样本顺序
np.random.shuffle(train_data)
# 将train_data分成多个mini_batch
batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
# 创建网络
net = Network(13)
# 依次使用每个mini_batch的数据
for mini_batch in mini_batches:
x = mini_batch[:, :-1]
y = mini_batch[:, -1:]
loss = net.train(x, y, iterations=1)
训练过程代码修改
加入多轮和多批次训练的双重循环
第一层循环
for epoic_id in range(num_epoches): # 代表样本集合要被训练几次,称为‘epoic’迭代次数
第二层循环
for iter_id, mini_batch in emumerate(mini_batches): # 代表每次遍历时,样本集合被拆分成的多个批次,需要全部执行训练,称为‘iter(iteration)’
两层循环内部的经典四步骤
前向计算 --> 计算损失 --> 计算梯度 --> 更新参数
最终完整代码如下:
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()
随机梯度下降加快训练的过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降曲线会出现震荡
注意: 由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升
NumPy
提供了save
接口,可直接将模型权重数组保存为.npy
格式的文件。
np.save('w.npy', net.w)
np.save('b.npy', net.b
loss一定越小越好么?
不是。在实际运用中肯定越小越好,但是在训练过程中,不是更小更好,在不断减小的过程中可能会产生过拟合,陷入局部最优值。
为什么是从梯度的反方向走?
坡度最大的方向其实就是梯度的反方向。
为什么要用NumPy
?
非常便捷地进行矩阵运算,用非常简单普通的方式去操作矩阵,又名为广播矩阵。
使用神经网络建模房价预测有三个要点:
构建网络,初始化参数w和b,定义预测和损失函数的计算方法。
随机选择初始点,建立梯度的计算方法和参数更新方式。
从总的数据集中抽取部分数据作为一个mini_batch,计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。
lot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
随机梯度下降加快训练的过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降曲线会出现震荡
注意: 由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升
[外链图片转存中…(img-yOqPC5rg-1657246223195)]
NumPy
提供了save
接口,可直接将模型权重数组保存为.npy
格式的文件。
np.save('w.npy', net.w)
np.save('b.npy', net.b
loss一定越小越好么?
不是。在实际运用中肯定越小越好,但是在训练过程中,不是更小更好,在不断减小的过程中可能会产生过拟合,陷入局部最优值。
为什么是从梯度的反方向走?
坡度最大的方向其实就是梯度的反方向。
为什么要用NumPy
?
非常便捷地进行矩阵运算,用非常简单普通的方式去操作矩阵,又名为广播矩阵。
使用神经网络建模房价预测有三个要点: