目录

一、模拟前向传播

二、编写一个完整的神经网络   

1)前向传播

2)损失函数

3)反向传播

4)权值更新

三、波士顿房价预测(应用)


        前面的文章 神经网络基础(一)讲了神经网络和深度学习的概念,神经网络基础(二)讲了神经网络的训练流程并补充了激活函数和损失函数的知识。在这篇内容中,我们要学习的是,如何用代码来实现神经网络的训练过程。

一、模拟前向传播

       大家看下,假如我们有如下的一个神经网络,共分为4层:

        输入层(第0层)有2个神经元

        隐藏层1(第1层)有3个神经元

        隐藏层2(第2层)有2个神经元

        输出层(第3层)有2个神经元

每两个神经元之间都有一个权重(weight)每个输入都有一个 偏置b(bias)。前向传播就是从左到右一层一层的向前传,传递的过程就是不断推进的过程。

        我们看下,初始化网络和设置输入值的过程,所有值都在图中标出来了。

# 初始化网络(初始权重和偏置)
def init_network():
    network = dict()
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network

# 初始化网络
network = init_network()
# 设置输入值
x = np.array([1.0, 0.5])
# 前向传播
y = forward(network, x)

还记得前面的这张图及图中的公式吗?

在代码中,我们需要借助numpy这个工具来帮我们完成计算的过程。

# sigmoid激活函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
  
# 恒等函数,作为输出层的激活函数
def identity_function(x):
    return x

# 前向传播
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    # hidden layer 1
    a1 = np.dot(x, W1) + b1
	# 前向传播 使用sigmoid
    z1 = sigmoid(a1)

    # hidden layer 2
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)

    # output layer
    a3 = np.dot(z2, W3) + b3
    y = identity_function(a3)
    return y

第0层x和第一层w1点乘后加上b1的结果,sigmoid函数激活后得到z1,z1又和第二层w2点乘后加上b2的结果,sigmoid函数激活后得到z2,z2又和输出层w3点乘后加上b3,就是最终输出:

总结一下:神经网络的计算函数 = 矩阵乘法(线性)+ 激活函数(非线性)

        网络层数、参数量属于人为设定的超参数。比如 2.6T、2.8 万亿参数量,2 层、3 层网络,不存在绝对的标准答案。每一层、每一个神经元数字对应的现实物理含义难以解读,属于黑盒模型,不适合拆解每一个数值去分析意义。

二、编写一个完整的神经网络   

        神经网络基础(二)中介绍了神经网络的完整训练过程。我们用代码实现一下这个过程:

首先是初始化神经网络:

# n为样本大小,d_in为输入维度,h为隐藏层维度,d_out为输出维度
n, d_in, h, d_out = 64, 1000, 100, 10

# 随机生成输入数据x和目标输出y
x = np.random.randn(n, d_in)      # 输入数据,形状为(64, 1000)
y = np.random.randn(n, d_out)     # 目标输出,形状为(64, 10)

# 随机初始化权重参数
# 输入层到隐藏层的权重(1000,100)
w1 = np.random.randn(d_in, h)
# 隐藏层到输出层的权重(100,10)
w2 = np.random.randn(h, d_out)

        样本一共有64个样本(可随意指定,但通常取 2 的幂次),即输入输出训练的数据集的个数是64个,输入层维度为1000,隐藏层维度为100,输出层维度为10。权重w1,w2也先随机生成。

训练100次的过程:

for t in range(100):
    # 前向传播
    temp = x.dot(w1)                  # 输入层到隐藏层的线性变换
    temp_relu = np.maximum(temp, 0)   # ReLU激活函数,隐藏层输出
    y_pred = temp_relu.dot(w2)        # 隐藏层到输出层的线性变换,得到预测值

    # 计算损失函数(均方误差和)
    loss = np.square(y_pred - y).sum()
    loss_history.append(loss)  # 记录loss值
    print(t, loss)

    # 反向传播,计算梯度
    grad_y_pred = 2.0 * (y_pred - y)              # 损失对预测输出的梯度
    #print('grad_y_pred=', grad_y_pred.shape) #(64, 10)
    grad_w2 = temp_relu.T.dot(grad_y_pred)        # 损失对w2的梯度
    grad_temp_relu = grad_y_pred.dot(w2.T)        # 损失对隐藏层输出的梯度
    grad_temp = grad_temp_relu.copy()             # 复制一份用于ReLU处理
    grad_temp[temp<0] = 0                         # ReLU小于0的部分梯度置零
    grad_w1 = x.T.dot(grad_temp)                  # 损失对w1的梯度

    # 更新权重参数
    w1 = w1 - learning_rate * grad_w1
    w2 = w2 - learning_rate * grad_w2

1)前向传播

        x点乘w1,再用Relu函数来激活(参看上篇文章,Relu的介绍,x>0时==》relu(x) = x,当x <= x时==》relu(x) = 0),得到temp_relu;temp_relu再去点乘w2完成正向传播,得到预测值y_pred。

2)损失函数

        用MSE(参看上篇文章)的方法计算预测值y_pred和真实值y之间的误差,并记录下来。

3)反向传播

        反向传播和正向传播方向相反,是以损失误差为起点,利用链式求导法则,把误差信号从输出层向输入层反向回传。大家可以看下计算公式:

这里的计算公式比较复杂,但是numpy帮我们处理了。我们就大概理解下,c和d的误差是通过e和f节点来计算的。通过上面的公式,其实是转置权重矩阵 * 误差 可以得到上一层的误差,从输入层一直往前推(从右往左),直到输出层。

        预测值y_pred和真实值y 之间的差值 * 梯度2(输入层到隐藏层,隐藏层到输出层)得到 grad_y_pred,grad_w2 等于 temp_relu转置后点乘grad_y_pred。grad_w1 等于 x转置后点乘grad_temp。

4)权值更新

        简单理解公式:新权重 = 旧权重 − 学习率 × 梯度 学习率控制参数每一步改动的幅度;学习率太大训练容易震荡不收敛,太小训练进度会十分缓慢。

输出结果:

        每一个循环就是一个迭代,在不断的迭代中,误差越来越小。大家看下Loss曲线:刚开始的迭代中误差非常迅速的下降,迭代到某一个次数后,误差基本不再下降了。这说明训练已基本完成。

输出误差:

我们可以直观的看到,输出的误差越来越小了。

总结:

        网络结构中有输入层、隐藏层和输出层。隐藏层就像一个黑箱子隐藏起来了,也可以把它理解成叫特征提取层,不需要人工手动设计特征规则。我们只需要传入输入和输出,就可以自动调整神经网络中的参数(自己去找对应的特征)。

        这种特征万事万物都可以按照这个模式来做匹配。从图像的识别,从房价的识别计算,甚至股票的涨跌等等。只要你特征提取的好,他自己作为一个组合以后的话,就可以帮你来去做后续的预测。

三、波士顿房价预测(应用)

波士顿房价数据集:housing.csv

        该数据集最初由美国波士顿地区的房屋信息统计而来, 最早用于研究空气质量对房价的影响,是机器学习的 经典回归数据集。

数据量:共506条样本,每条样本对应一处房产

特征数:13个特征+ 1个目标值(房价)

分隔符:空格分隔,无表头

大家看下,下面是数据的加载及初始化输入层和隐藏层的过程:

# 数据加载(从housing.csv读取,无表头,空格分隔)
data = pd.read_csv('housing.csv', header=None, delim_whitespace=True)
X = data.iloc[:, :-1].values  # 前13列为特征
y = data.iloc[:, -1].values  # 最后一列为目标
# 将y转为列向量,方便后续矩阵运算
y = y.reshape(-1, 1)

# 数据规范化(标准化处理,使每一列特征均值为0,方差为1)
x = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
print(x)  # 打印标准化后的特征矩阵
n_feature = x.shape[1]  # 特征数量(13)
print(n_feature)

n_hidden = 10  # 隐藏层神经元数量
# 初始化输入层到隐藏层的权重和偏置
w1 = np.random.randn(n_feature, n_hidden)
b1 = np.zeros(n_hidden)
# 初始化隐藏层到输出层的权重和偏置
w2 = np.random.randn(n_hidden, 1)
b2 = np.zeros(1)

  下面是迭代100次去调整神经网络的过程(可以调大迭代次数,我这里只是示范):

def Relu(x):
    # ReLU激活函数,小于0的部分置为0
    result = np.where(x>0, x, 0)
    return result

# 设置学习率
learning_rate = 1e-5

def MSE_loss(y, y_hat):
    # 均方误差损失函数
    return np.mean(np.square(y_hat - y))

def Linear(X, W1, b1):
    # 线性变换:X @ W1 + b1
    return X.dot(W1) + b1

# 输入特征维度13,隐藏层10维,输出1维(房价)
loss_history = []  # 记录每轮训练的损失,用于绘制误差曲线
for t in range(100):
    # 前向传播:输入->隐藏层->输出
    l1 = Linear(x, w1, b1)      # 输入层到隐藏层的线性变换
    s1 = Relu(l1)               # 隐藏层激活
    y_pred = Linear(s1, w2, b2) # 隐藏层到输出层的线性变换,得到预测值

    # 计算损失
    loss = MSE_loss(y, y_pred)
    loss_history.append(loss)
    print(t, loss)

    # 反向传播:计算梯度
    grad_y_pred = 2.0 * (y_pred - y)           # 损失对预测输出的梯度
    grad_w2 = s1.T.dot(grad_y_pred)            # 损失对w2的梯度
    grad_temp_relu = grad_y_pred.dot(w2.T)     # 损失对隐藏层输出的梯度
    grad_temp_relu[l1<0] = 0                   # ReLU小于0的部分梯度置零
    grad_w1 = x.T.dot(grad_temp_relu)          # 损失对w1的梯度

    # 更新权重参数
    w1 = w1 - learning_rate * grad_w1
    w2 = w2 - learning_rate * grad_w2

      大家可以看下,在这个例子中迭代的部分代码和上面的代码几乎一样。而且对于数据集中的数据,我完全不知道每一列代表什么,也不需要知道。大家看下训练情况:

预测的房价与实际的房价之间的误差越来越小了。

更多推荐