线性回归——从零开始

尽管强大的深度学习框架可以减少大量重复性工作,但若过于依赖它提供的便利,你就会很难深入理解深度学习是如何工作的。因此,我们的第一个教程是如何只利用ndarray和autograd来实现一个线性回归的训练。

线性回归

给定一个数据点集合X和对应的目标值y,线性模型的目标就是找到一条使用向量w和位移b描述的线,来尽可能地近似每个样本X[i]y[i]。用数学符号来表示就是:

\[\boldsymbol{\hat{y}} = X \boldsymbol{w} + b\]

并最小化所有数据点上的平方误差

\[\sum_{i=1}^n (\hat{y}_i-y_i)^2.\]

你可能会对我们把古老的线性回归作为深度学习的一个样例表示奇怪。实际上线性模型是最简单、但也是最有用的神经网络。一个神经网络就是一个由节点(神经元)和有向边组成的集合。我们一般把一些节点组成层,每一层先从下面一层的节点获取输入,然后输出给上面的层使用。要计算一个节点值,我们需要将输入节点值做加权和(权数值即 w),然后再加上一个激活函数(activation function)。对于线性回归而言,它是一个两层神经网络,其中第一层是(下图橙色点)输入,每个节点对应输入数据点的一个维度,第二层是单输出节点(下图绿色点),它使用身份函数(\(f(x)=x\))作为激活函数。

创建数据集

这里我们使用一个数据集来尽量简单地解释清楚,真实的模型是什么样的。具体来说,我们使用如下方法来生成数据;随机数值 X[i],其相应的标注为 y[i]

y[i] = 2 * X[i][0] - 3.4 * X[i][1] + 4.2 + noise

使用数学符号表示:

\[y = X \cdot w + b + \eta, \quad \text{for } \eta \sim \mathcal{N}(0,\sigma^2)\]

这里噪音服从均值0和标准差为0.01的正态分布。

In [1]:
from mxnet import ndarray as nd
from mxnet import autograd

num_inputs = 2
num_examples = 1000

true_w = [2, -3.4]
true_b = 4.2

X = nd.random_normal(shape=(num_examples, num_inputs))
y = true_w[0] * X[:, 0] + true_w[1] * X[:, 1] + true_b
y += .01 * nd.random_normal(shape=y.shape)

注意到X的每一行是一个长度为2的向量,而y的每一行是一个长度为1的向量(标量)。

In [2]:
print(X[0], y[0])

[ 2.21220636  0.7740038 ]
<NDArray 2 @cpu(0)>
[ 6.00058699]
<NDArray 1 @cpu(0)>

如果有兴趣,可以使用安装包中已包括的 Python 绘图包 matplotlib,生成第二个特征值 (X[:, 1]) 和目标值 Y 的散点图,更直观地观察两者间的关系。

In [3]:
import matplotlib.pyplot as plt
plt.scatter(X[:, 1].asnumpy(),y.asnumpy())
plt.show()
../_images/chapter_supervised-learning_linear-regression-scratch_5_0.png

数据读取

当我们开始训练神经网络的时候,我们需要不断读取数据块。这里我们定义一个函数它每次返回batch_size个随机的样本和对应的目标。我们通过python的yield来构造一个迭代器。

In [4]:
import random
batch_size = 10
def data_iter():
    # 产生一个随机索引
    idx = list(range(num_examples))
    random.shuffle(idx)
    for i in range(0, num_examples, batch_size):
        j = nd.array(idx[i:min(i+batch_size,num_examples)])
        yield nd.take(X, j), nd.take(y, j)

下面代码读取第一个随机数据块

In [5]:
for data, label in data_iter():
    print(data, label)
    break

[[-0.17849484 -0.14060703]
 [ 0.96766508  0.51227432]
 [ 0.86366373 -0.80888796]
 [ 1.06379759 -1.25351691]
 [-0.10930543 -1.21778405]
 [-0.41367719  1.22424209]
 [ 0.9252283  -1.49028432]
 [ 0.34383699  0.02602482]
 [-0.3300724   0.09964364]
 [-2.58991313 -0.31663969]]
<NDArray 10x2 @cpu(0)>
[  4.32575035   4.40550089   8.67557907  10.58337116   8.11946201
  -0.79734313  11.12431812   4.79263592   3.19644499   0.10322509]
<NDArray 10 @cpu(0)>

初始化模型参数

下面我们随机初始化模型参数

In [6]:
w = nd.random_normal(shape=(num_inputs, 1))
b = nd.zeros((1,))
params = [w, b]

之后训练时我们需要对这些参数求导来更新它们的值,使损失尽量减小;因此我们需要创建它们的梯度。

In [7]:
for param in params:
    param.attach_grad()

定义模型

线性模型就是将输入和模型的权重(w)相乘,再加上偏移(b):

In [8]:
def net(X):
    return nd.dot(X, w) + b

损失函数

我们使用常见的平方误差来衡量预测目标和真实目标之间的差距。

In [9]:
def square_loss(yhat, y):
    # 注意这里我们把y变形成yhat的形状来避免矩阵形状的自动转换
    return (yhat - y.reshape(yhat.shape)) ** 2

优化

虽然线性回归有显式解,但绝大部分模型并没有。所以我们这里通过随机梯度下降来求解。每一步,我们将模型参数沿着梯度的反方向走特定距离,这个距离一般叫学习率(learning rate) lr。(我们会之后一直使用这个函数,我们将其保存在utils.py。)

In [10]:
def SGD(params, lr):
    for param in params:
        param[:] = param - lr * param.grad

训练

现在我们可以开始训练了。训练通常需要迭代数据数次,在这里使用epochs表示迭代总次数;一次迭代中,我们每次随机读取固定数个数据点,计算梯度并更新模型参数。

In [11]:
# 模型函数
def real_fn(X):
    return true_w[0] * X[:, 0] + true_w[1] * X[:, 1] + true_b
# 绘制损失随训练次数降低的折线图,以及预测值和真实值的散点图
def plot(losses, X, sample_size=100):
    xs = list(range(len(losses)))
    f, (fg1, fg2) = plt.subplots(1, 2)
    fg1.set_title('Loss during training')
    fg1.plot(xs, losses, '-r')
    fg2.set_title('Estimated vs real function')
    fg2.plot(X[:sample_size, 1].asnumpy(),
             net(X[:sample_size, :]).asnumpy(), 'or', label='Estimated')
    fg2.plot(X[:sample_size, 1].asnumpy(),
             real_fn(X[:sample_size, :]).asnumpy(), '*g', label='Real')
    fg2.legend()
    plt.show()
In [12]:
epochs = 5
learning_rate = .001
niter = 0
losses = []
moving_loss = 0
smoothing_constant = .01

# 训练
for e in range(epochs):
    total_loss = 0

    for data, label in data_iter():
        with autograd.record():
            output = net(data)
            loss = square_loss(output, label)
        loss.backward()
        SGD(params, learning_rate)
        total_loss += nd.sum(loss).asscalar()

        # 记录每读取一个数据点后,损失的移动平均值的变化;
        niter +=1
        curr_loss = nd.mean(loss).asscalar()
        moving_loss = (1 - smoothing_constant) * moving_loss + (smoothing_constant) * curr_loss

        # correct the bias from the moving averages
        est_loss = moving_loss/(1-(1-smoothing_constant)**niter)

        if (niter + 1) % 100 == 0:
            losses.append(est_loss)
            print("Epoch %s, batch %s. Moving avg of loss: %s. Average loss: %f" % (e, niter, est_loss, total_loss/num_examples))
            plot(losses, X)
Epoch 0, batch 99. Moving avg of loss: 5.27403572128. Average loss: 6.851589
../_images/chapter_supervised-learning_linear-regression-scratch_22_1.png
Epoch 1, batch 199. Moving avg of loss: 1.48155771908. Average loss: 0.131150
../_images/chapter_supervised-learning_linear-regression-scratch_22_3.png
Epoch 2, batch 299. Moving avg of loss: 0.49471837288. Average loss: 0.002593
../_images/chapter_supervised-learning_linear-regression-scratch_22_5.png
Epoch 3, batch 399. Moving avg of loss: 0.175379171675. Average loss: 0.000146
../_images/chapter_supervised-learning_linear-regression-scratch_22_7.png
Epoch 4, batch 499. Moving avg of loss: 0.0635164873118. Average loss: 0.000100
../_images/chapter_supervised-learning_linear-regression-scratch_22_9.png

训练完成后,我们可以比较学得的参数和真实参数

In [13]:
true_w, w
Out[13]:
([2, -3.4],
 [[ 1.99985707]
  [-3.39991188]]
 <NDArray 2x1 @cpu(0)>)
In [14]:
true_b, b
Out[14]:
(4.2,
 [ 4.20012569]
 <NDArray 1 @cpu(0)>)

小结

我们现在看到,仅仅是使用NDArray和autograd就可以很容易实现的一个模型。在接下来的教程里,我们会在此基础上,介绍更多现代神经网络的知识,以及怎样使用少量的MXNet代码实现各种复杂的模型。

练习

尝试用不同的学习率查看误差下降速度(收敛率)

讨论

欢迎扫码直达本节内容讨论区