飞道的博客

【机器学习】吴恩达机器学习视频作业-(手撕)神经网络

299人阅读  评论(0)

在这里使用前馈神经网络和反向传播的方式对数据进行分类。这也是神经网络最基础的部分,刚了解相关内人的话,还是比较难的,特别是在公式推导中。在本部分中,构建一个神经网络分类模型,就可以对手写数字进行识别,无需构建多个二分类模型。
视频讲解:

彻底搞定机器学习算法理论与实战——神经网络入门-NeuralNetworks

视频链接:https://www.bilibili.com/video/av96934866/

1 加载数据集与数据查看

本案例使用的数据集与使用逻辑回归进行多分类相同。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat   # 加载matlab数据
import matplotlib
import scipy.optimize as opt   # 优化函数
from sklearn.metrics import classification_report  # 评价报告
%matplotlib inline
def load_data(path, transpose=True):
    # 加载数据
    data = loadmat(path)
    y = data.get('y')   # (5000,1)
    y = y.reshape(y.shape[0])  # 构造列向量
    X = data.get('X')   # (5000, 400)
    if transpose:
        # 原始数据对图像进行了翻转,需要转置回来
        X = np.array([im.reshape((20, 20)).T for im in X])
        # 再次将数据压平
        X = np.array([im.reshape(400) for im in X])
    return X, y

查看原始数据并随机绘制100张图片

X, _ = load_data("ex4data1.mat")

def plot_100_image(X):
    """ 
    随机选取100张图片
    """
    size = int(np.sqrt(X.shape[1]))  # 获取形状
    # sample 100 image, reshape, reorg it
    sample_idx = np.random.choice(np.arange(X.shape[0]), 100)  # 100*400
    sample_images = X[sample_idx, :]   # 抽取图片
    # 共享x,y轴
    fig, ax_array = plt.subplots(nrows=10, ncols=10, sharey=True, sharex=True, figsize=(8, 8))

    for r in range(10):
        for c in range(10):
            # 把一个数组、矩阵绘图图片:matshow
            ax_array[r, c].matshow(sample_images[10 * r + c].reshape((size, size)),
                                   cmap=matplotlib.cm.binary)  # cmap=matplotlib.cm.binary 设置图片颜色
            plt.xticks(np.array([]))  # 设置x轴无标记
            plt.yticks(np.array([]))  # 设置y轴无标记

# 随机查看100条数据
plot_100_image(X)


查看权重数据形状

def load_weight(path):
    # 读取权重数据 即参数信息
    data = loadmat(path)
    return data['Theta1'], data['Theta2']
t1, t2 = load_weight('ex4weights.mat')
t1.shape, t2.shape   # 查看权重形状

参数的序列化和反序列化

def serialize(a, b):
    # 序列化数据
    return np.concatenate((np.ravel(a), np.ravel(b)))

def deserialize(seq):
    #  (25, 401), (10, 26)
    # 反序列化,分离参数theta1和theta2使用
    return seq[:25 * 401].reshape(25, 401), seq[25 * 401:].reshape(10, 26)

# 序列化2矩阵
# 在这个nn架构中,theta1(25,401),theta2(10,26),它们的梯度是delta1,delta2
theta = serialize(t1, t2)  # 扁平化参数,25*401+10*26=10285
theta.shape

2 数据预处理

标签y转成onehot编码,特征数据X插入数据列1

X_raw, y_raw = load_data('ex4data1.mat', transpose=False)  # 获取实验数据
X = np.insert(X_raw, 0, np.ones(X_raw.shape[0]), axis=1)#增加全部为1的一列
X.shape # (5000, 401)


y_raw
# array([10, 10, 10, ...,  9,  9,  9], dtype=uint8)
# 将y进行onehot编码
def expand_y(y):
    res = []
    for i in y:
        y_array = np.zeros(10)
        y_array[i - 1] = 1
        res.append(y_array)
    return np.array(res)

y = expand_y(y_raw)
y
"""
array([[0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 0., ..., 0., 0., 1.],
       ...,
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 0., 1., 0.]])
"""

3 神经网络构建

  • 输入层是400 + 1(偏置)个神经元,对应数据的各个特征。
  • 隐藏层只设置一层,有25 + 1(偏置)个神经元。
  • 输出层有10个神经元,总共10类,最后选择输出层概率最大那个,决定是哪一类。

前向传播图形如下

激活函数 在神经网络中有很多激活函数。这里使用sigmoid函数对神经元进行激活。
s i g m o i d ( z ) = g ( z ) = 1 1 + e z sigmoid(z) = g(z) = \frac{1}{1+e^{-z}}

# 定义一个sigmoid函数
def sigmoid(z):
    # z可以是一个数,也可以是np中的矩阵
    return 1.0/(1+np.exp(-z))

前向传播函数

# 前向传播函数创建
def forward_propagate(theta, X):
    # theta 参数 一个行向量
    # X 5000x401的数据
    t1, t2 = deserialize(theta)  # t1:(25,401)  t2:(10,26)
    m = X.shape[0]    # 数据数目
    a1 = X    # 5000x401
    z2 = a1 @ t1.T   # 矩阵相乘
    a2 = np.insert(sigmoid(z2), 0, values=1, axis=1) # 5000x26
    z3 = a2 @ t2.T   # 矩阵相乘  5000x10
    h = sigmoid(z3)  # 经过激活函数输出 5000x10
    return a1, z2, a2, z3, h  # 这些参数在反向回归时使用

查看前向传播情况

_,_,_,_, h = forward_propagate(theta, X)
h

5 损失函数构建

J ( θ ) = 1 m i = 1 m k = 1 K [ y k ( i ) log ( ( h Θ ( x ( i ) ) ) k ) ( 1 y k ( i ) ) log ( 1 ( h Θ ( x ( i ) ) ) k ) ] + λ 2 m [ j = 1 25 k = 1 400 ( Θ j , k ( 1 ) ) 2 + j = 1 10 k = 1 25 ( Θ j , k ( 2 ) ) 2 ] \begin{aligned}J(\theta)=& \frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K}\left[-y_{k}^{(i)} \log \left(\left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}\right)-\left(1-y_{k}^{(i)}\right) \log \left(1-\left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}\right)\right]+\\& \frac{\lambda}{2 m}\left[\sum_{j=1}^{25} \sum_{k=1}^{400}\left(\Theta_{j, k}^{(1)}\right)^{2}+\sum_{j=1}^{10} \sum_{k=1}^{25}\left(\Theta_{j, k}^{(2)}\right)^{2}\right]\end{aligned}
公式说明:

  • 左边部分为常规的损失函数,右边部分是正则化项

  • 以一条训练数据为例,则单条数据的损失函数为:
    k = 1 K [ y k log ( ( h θ ( x ) ) k ) ( 1 y k ) log ( 1 ( h θ ( x ) ) k ) ] \sum_{k=1}^{K}\left[-y_{k} \log \left(\left(h_{\theta}\left(x\right)\right)_{k}\right)-\left(1-y_{k}\right) \log \left(1-\left(h_{\theta}\left(x\right)\right)_{k}\right)\right ]
    这里的k表示分类的数目。y已经经过one-hot编码,一条训练数据x对应着有10个数值,而每个数值有2中可能,即为1或为0(二分类)。实际中也只在y=1时计算出该数据的损失值,其它情况计算结果为0。

  • 最后就是将所有数据的损失值相加求平均

  • 对于正则项,也以单条数据为例,则有:(注:不考虑偏置项)
    λ 2 [ j = 1 25 k = 1 400 ( Θ j , k ( 1 ) ) 2 + j = 1 10 k = 1 25 ( Θ j , k ( 2 ) ) 2 ] \frac{\lambda}{2}\left[ \sum_{j=1}^{25} \sum_{k=1}^{400}\left(\Theta_{j, k}^{(1)}\right)^{2}+\sum_{j=1}^{10} \sum_{k=1}^{25}\left(\Theta_{j,k}^{(2)}\right)^{2}\right]
    其中 Θ ( 1 ) \Theta^{(1)} Θ 2 ( 2 ) \Theta_2^{(2)} 分别表示输入层到隐藏层、隐藏层到输出层的参数。输入层非配置项神经元有400个,隐藏层神经元有25个,则需要25 × \times 400个参数。与此类似,数层神经元有10个,则隐藏层到输出层之间的神经元个数:10 × \times 25个。

  • 最后将所有数据的正则项计算结果加起来取平均即可,系数\frac{1}{2}是为了求导时消掉2次项的系数。 λ \lambda 正则的正则项系数。

  • 正则项使用的是L2正则化,即L2范数

# 常规损失函数
def cost(theta, X, y):
    # theta 包含第一部分和第二部分的参数 1行(401*25+26*10)列的行向量
    # 训练数据
    # 标签
    m = X.shape[0]  # 获取数据量
    _, _, _, _, h = forward_propagate(theta, X)
    return (-y*np.log(h) - (1 - y)*np.log(1 - h)).sum()/m
    

# 正则化损失函数定义损失函数
def regularized_cost(theta,  X, y, l):
    # theta 包含第一部分和第二部分的参数 1行(401*25+26*10)列的行向量
    # 训练数据
    # 标签
    # 正则项
    m = X.shape[0]      # 获取训练样本的个数
    # 分离两部分的参数值
    t1, t2 = deserialize(theta)
    # 正则项
    reg = l * (np.power(t1[:, 1:], 2).sum() + np.power(t2[:, 1:], 2).sum())/(2*m)
    return cost(theta, X, y) + reg

查看初始损失值

cost(theta, X, y), regularized_cost(theta, X, y, 1)
# (0.2876291651613189, 0.38376985909092365)

6 反向传播算法

6.1 sigmoid函数的导数

先回顾一下分式函数求导法则:
[ f ( x ) g ( x ) ] = f ( x ) g ( x ) f ( x ) g ( x ) g 2 ( x ) \left[ \frac{f(x)}{g(x)} \right]^\prime = \frac{f^\prime(x)g(x) - f(x)g^\prime(x)}{g^2(x)}
那么sigmoid函数的导数如下:
g ( z ) = 1 1 + e z = e z 1 + e z = e z ( 1 + e z ) e 2 z ( 1 + e z ) 2 = e z 1 + e z ( 1 + e z 1 1 + e z ) = g ( z ) ( 1 g ( z ) ) \begin{aligned}g (z) &= \frac{1}{1+e^{-z}}= \frac{e^z}{1+e^{-z}}\\&= \frac{e^z(1+e^z) - e^{2z}}{(1+e^z)^2}\\&= \frac{e^z}{1+e^z}\left( \frac{1+e^z - 1}{1+e^z} \right)\\& = g(z)(1-g(z))\end{aligned}

def gradient_sigmoid(z):
    # z 一个数或一个ndarray类型
    return sigmoid(z)*(1-sigmoid(z))

# 查看参数状况
X.shape, y.shape, t1.shape, t2.shape, theta.shape
# ((5000, 401), (5000, 10), (25, 401), (10, 26), (10285,))

6.2 梯度下降法计算参数值theta

这一块是整个程序最难的地方,数据的维度很难理得清。在吴恩达老师的视频中也没有进行具体的推导,也仅仅是罗列了相关结论。

可根据吴恩达老师给出的结论:


计算本模型中的参数 θ \theta 的偏导数,如下:
a ( 1 ) = x a^{(1)} = x ,
z ( 2 ) = θ ( 1 ) a ( 1 ) z^{(2)} = \theta^{(1)}a^{(1)} ,
a ( 2 ) = g ( z ( 2 ) ) a^{(2)}=g(z^{(2)}) ,
z ( 3 ) = θ ( 2 ) a ( 2 ) z^{(3)}=\theta^{(2)}a^{(2)} ,
a ( 3 ) = h Θ ( x ) = g ( z ( 3 ) ) a^{(3)}=h_\Theta(x)=g(z^{(3)}) ,
δ ( 3 ) = a ( 3 ) y \delta^{(3)} = a^{(3)}-y ,
δ ( 2 ) = θ ( 2 ) T δ ( 3 ) . g ( a ( 2 ) ) \delta^{(2)} = {\theta^{(2)}}^T\delta^{(3)} .* g^\prime(a^{(2)}) ,
θ ( 2 ) J ( Θ ) = δ ( 3 ) T a ( 2 ) \frac{\partial }{\partial \theta^{(2)}}J(\Theta) = {\delta^{(3)}}^Ta^{(2)} ,(需要注意维度)
θ ( 1 ) J ( Θ ) = δ ( 2 ) T a ( 1 ) \frac{\partial }{\partial \theta^{(1)}}J(\Theta) = {\delta^{(2)}}^T a^{(1)} (需要注意不需要的偏置项)

# 常规方法梯度下降算法
def backprop_regular_gradient(theta, X, y):
    t1, t2 = deserialize(theta)  # 获取两个参数值
    m = X.shape[0]
    delta1 = np.zeros_like(t1)
    delta2 = np.zeros_like(t2)
    a1, z2, a2, z3, h = forward_propagate(theta, X)
    # 使用单挑数据梯度值叠加计算方法
    for i in range(m):
        a1i = a1[i, :]  # (1, 401) 行向量
        z2i = z2[i, :]  # (1, 25)  行向量
        a2i = a2[i, :]  # (1, 26)  行向量
        hi = h[i, :]    # (1, 10)  行向量
        yi = y[i, :]    # (1, 10)  行向量
        
        d3i = hi - yi   # (1, 10)
        z2i = np.insert(z2i, 0, 1)  # 增加偏置项
        d2i = t2.T @ d3i * gradient_sigmoid(z2i)   # (1,26)
        # 注意np向量的转置 这里使用matrix时,行向量转置会成为2维向量,如果使用array,行向量转置还是行向量
        delta2 += np.matrix(d3i).T @ np.matrix(a2i) # (1, 10).T @ (1, 26).T
        # 注意在当前层会增加上一层没有的偏置项
        delta1 += np.matrix(d2i[1:]).T @ np.matrix(a1i)  # (1, 25).t @ (1, 401)
    
    delta1 = delta1/m
    delta2 = delta2/m
    
    return serialize(delta1, delta2)

# 查看计算情况
d1, d2 = deserialize(backprop_regular_gradient(theta, X, y))
d1.shape, d2.shape
# ((25, 401), (10, 26))

6.3 正则化梯度

Θ i j ( l ) J ( Θ ) = D i j ( l ) = 1 m Δ i j ( l )  for  j = 0 \frac{\partial}{\partial \Theta_{i j}^{(l)}} J(\Theta) =D_{i j}^{(l)} =\frac{1}{m} \Delta_{i j}^{(l)} \quad\quad \quad\quad \text { for } j=0
Θ i j ( l ) J ( Θ ) = D i j ( l ) = 1 m Δ i j ( l ) + λ m Θ i j ( l )  for  j 1 \frac{\partial}{\partial \Theta_{i j}^{(l)}} J(\Theta) =D_{i j}^{(l)} =\frac{1}{m} \Delta_{i j}^{(l)}+\frac{\lambda}{m} \Theta_{i j}^{(l)} \quad\text { for } j \geq 1

# 正则化参数的梯度
def regularized_gradient(theta, X, y, l=1):
    m = X.shape[0]
    # 常规梯度下降计算
    delta1, delta2 = deserialize(backprop_regular_gradient(theta, X, y))
    t1, t2 = deserialize(theta)
    t1[:, 0] = 0  # 0 列表示不要正则化的列
    reg_term_d1 = (l / m) * t1
    delta1 = delta1 + reg_term_d1
 
    t2[:, 0] = 0  # 0 列表示不要正则化的列
    reg_term_d2 = (l / m) * t2
    delta2 = delta2 + reg_term_d2

    return serialize(delta1, delta2)

7 模型训练

def nn_training(X, y):
    # 初始化模型参数theta
    init_theta = np.random.uniform(-0.12, 0.12, 10285)  # 25*401 + 10*26
    res = opt.minimize(fun=regularized_cost,
                       x0=init_theta,
                       args=(X, y, 1),
                       method='TNC',
                       jac=regularized_gradient,
                       options={'maxiter': 400})
    return res

res = nn_training(X, y)  # 运行时间长
res

8 查看正确率

def show_accuracy(theta, X, y):
    _, _, _, _, h = forward_propagate(theta, X)
    y_pred = np.argmax(h, axis=1) + 1  # 确定预测类
    # 打印报告
    print(classification_report(y, y_pred, digits=4))

show_accuracy(res.x, X, y_raw)

9 显示隐藏层

def plot_hidden_layer(theta):
    """
    theta: (10285, )
    """
    final_theta1, _ = deserialize(theta)
    hidden_layer = final_theta1[:, 1:]  # ger rid of bias term theta

    fig, ax_array = plt.subplots(nrows=5, ncols=5, sharey=True, sharex=True, figsize=(5, 5))

    for r in range(5):
        for c in range(5):
            ax_array[r, c].matshow(hidden_layer[5 * r + c].reshape((20, 20)),
                                   cmap=matplotlib.cm.binary)
            plt.xticks(np.array([]))
            plt.yticks(np.array([]))
plot_hidden_layer(res.x)

总结

曾尝试使用矩阵求导的方式去推导本模型中的反向传播方法,但是涉及的参数过多,很容易混乱。最后使用的是吴恩达老师给出的结论,然后根据本模型去推出适合本模型中的数据。当然本模型也还存在一定的缺陷,这个在今后深入深度学习后再进行改进和使用更加简洁的方法进行处理。
程序源码可在订阅号"AIAS编程有道"中回复“机器学习”即可获取。


转载:https://blog.csdn.net/meiqi0538/article/details/104955992
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场