正者,正也。其心以为不然者,天门弗开矣。

作为《深度学习入门》的阅读笔记,本文简略概述深度学习的学习过程,损失函数,梯度法相关的内容,并辅以python实现

需要事先了解的

本文并不是从零开始的,需要实现了解

  • 蜻蜓点水python

    • 蜻蜓点水python_dlc
  • 简略感知机
  • 简易神经网络_推理和正向传播

不同的学习

首先,学习有不同的类型,人想,机器学习,深度学习;

然后,学习的基础是特征向量,机器学习中特征量还是人想出来的,深度学习则没有这一步骤,基本实现了全程机器管理
image-20251018220933182.png

损失函数

有道是能观测就能干涉,能干涉就能控制;那么应该观测什么变量才能知道学习的进度,从而干涉和控制学习的过程呢?这里直接给出答案:损失函数。损失函数理论上可以使用任何函数,这里使用均方误差交叉熵误差

这里有一个问题,为什么不使用精度(即准确度)来最为指标呢?首先说结论:在进行神经网络的学习时,不能将识别精度作为指标,并且对于大部分导数(后面会提到)为0的函数,都不能作为指标,会导致学习无法进行(无法更新参数),原因在了解学习的过程后自然能理解。

均方误差

直接公式,实现如下:
image-20251018223037143.png

  • y_k是前向传播的输出
  • t_k是监督数据的标签(一般是one-hot表示)
  • k是维度
import numpy as np
def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

print(mean_squared_error(np.array([0.1,0.7,0.2]),np.array([0,1,0]))) #判断正确
print(mean_squared_error(np.array([0.1,0.7,0.2]),np.array([1,0,0]))) #判断错误

#0.07000000000000002
#0.67

可以看出,如果判断正确,损失函数会是一个较小的数字,如果判断失败,损失函数会是一个较大的数字。

交叉熵误差

直接公式,实现如下:
image-20251018224310892.png

  • y_k是前向传播的输出
  • t_k是监督数据的标签(一般是one-hot表示)
  • k是维度
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

print(cross_entropy_error(np.array([0.1,0.7,0.2]),np.array([0,1,0])))
print(cross_entropy_error(np.array([0.1,0.7,0.2]),np.array([1,0,0])))

#0.3566748010815999
#2.302584092994546
  • 其中delta是为了避免log的参数有0的情况

Mini-Batch学习

和推理的过程一样,学习也可以用批处理的方法完成,不过实现上有些区别;这里以交叉熵误差为例

第一个是损失函数公式的变化:
image-20251018230403347.png

可见其实就是取平均,实现如下:

def cross_entropy_error2(y, t):
    batch_size = 1
    if y.ndim > 1:
        batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

#测试不同维度
print(cross_entropy_error2(np.array([0.1,0.7,0.2]),np.array([0,1,0])))
print(cross_entropy_error2(np.array([0.1,0.7,0.2]),np.array([1,0,0])))
print(cross_entropy_error2(np.array([[0.1,0.7,0.2],[0.1,0.7,0.2]]),np.array([[0,1,0],[0,1,0]])))
print(cross_entropy_error2(np.array([[0.1,0.7,0.2],[0.1,0.7,0.2]]),np.array([[1,0,0],[1,0,0]])))

第二个是训练数据其实还是很多的,想一次性全部学习是高耗能甚至有些不现实的,所以需要使用np.random.choice抽取一个batch来学习

# 这里的路径按本地实际来
from dfs.dataset.mnist import load_mnist
import sys,os
sys.path.append(os.pardir)

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True,
normalize=False,one_hot_label=True)

# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
# 输出各个数据的形状
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
print(batch_mask) # 输出抽选的索引
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

print(x_batch.shape) # (10, 784)
print(t_batch.shape) # (10,)


#(60000, 784)
#(60000, 10)
#(10000, 784)
#(10000, 10)
#[ 6681  4809 12952  4669  1444 20997 56685 39195 24206  8649]
#(10, 784)
#(10, 10)

第三如果标签不是one-hot表示,有那么一个特殊的实现方法,思路是虽然是求和,但只有正确项会有值

def cross_entropy_error2(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    batch_size = y.shape[0]
    print(y[np.arange(batch_size), t])
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

print(cross_entropy_error2(np.array([[0.1,0.7,0.2],[0.1,0.7,0.2]]),np.array([1,2])))
#[0.7 0.2]
#0.9830561067579126

另外有个概念会在后面用到这里先提出:

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000笔训练数据,用大小为 100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了 。此时,100次就是一个epoch。

导数和梯度

有了损失函数,就可以观测学习的进度,但是如何在此基础上对其进行控制呢?要做到这点需要解决三个问题,函数的值是会通过自变量的变化而变化的,改哪个变量会使损失函数变小?如何改?要改多少?

直接说结论:修改的变量是权重和偏置,依靠导数计算出的梯度来修改自变量,改多少依赖于人工设定的学习率

导数

不准确的说,导数就是x周围很小范围的斜率(中心差分),既不是x左边的斜率也不是x右边的斜率(前向差分);数值微分和真实的解析解必然是有所差距的。

求导数的实现如下:

def numerical_diff(f,x,h=1e-4):
    return (f(x+h)-f(x-h))/(2*h)
def func_test(x):
    return 2*x+1

numerical_diff(func_test,np.array([2,3]))

偏导数和梯度

多个变量的函数的导数就是偏导数,当然一个偏导数只能对其中一个变量来做,另外的变量就当作常数对待,称为函数对该变量的偏导数,这是解析解的做法;对于数值解则更加的简单一点,固定其他变量,只变化一个变量来求导数就是函数针对这个变量的偏导数。

把所有变量的偏导数组成向量,就是梯度,梯度指向的方向是函数变化(增加)率最大的方向。所以对其取负数,就是损失函数下降最快的方向。

实现如下:

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val # 还原值
        it.iternext()

    return grad

def test_func(x):
    return x[0]*x[0]+x[1]*x[1]

numerical_gradient(test_func,np.array([3.0,4.0]))
#array([6., 8.])

梯度下降法

按上述说的,按照梯度反方向更新参数就是梯度下降法,公式如下:

image-20251019112529214.png

其中偏导数前面的常数就是学习率,人为指定,规定学习的程度,过大过小都不好,这里默认0.01,实现如下:

def gradient_descent(f,init_x,lr=0.01,step_num=100):
    x = init_x

    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr*grad
    return x

def function_2(x):
    return x[0]*x[0]+x[1]*x[1]

init_x=np.array([-3.0,4.0])


gradient_descent(function_2,init_x,lr=0.1,step_num=100)
#array([-6.11110793e-10,  8.14814391e-10])

另外,像这种人为指定的参数,称为超参数,像是mini-batch的大小,输入的形状,学习的次数都是超参数

总结和实现

现在就可以继续上章,完成MINIST的学习步骤了,但是还有一些细节需要说明

神经网络的梯度法

在实际的神经网络中要求的是损失函数对于权重的梯度,也就是对权重矩阵中的每个成员求梯度,最后结果还是矩阵:

image-20251019120202053.png

对于实现而言,只要能保证矩阵的形状没有问题即可

学习算法的步骤

image-20251019120525911.png

步骤4则是重复1-3的步骤

而这种随机选取+计算梯度的方法称为:随机梯度下降法(SGD)

实现

先列出代码

# 这里的路径按本地实际来
import numpy as np
from dfs.dataset.mnist import load_mnist
import sys,os
sys.path.append(os.pardir)

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True,
normalize=False,one_hot_label=True)

# 输出各个数据的形状
#print(x_train.shape) # (60000, 784)
# 输出各个数据的形状
#print(t_train.shape) # (60000,10)
#print(x_test.shape) # (10000, 784)
#print(t_test.shape) # (10000,10)

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
#print(batch_mask)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

print(x_batch.shape) # (10, 784)
print(t_batch.shape) # (10,10)


def cross_entropy_error(y, t):
    batch_size = 1
    if y.ndim > 1:
        batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a-c) #溢出对策
    sum_exp_a = np.sum(exp_a)
    y = exp_a /sum_exp_a
    return y

def sigmoid(x):
    return 1/(1+np.exp(-x))


class SigMoidLayer:
    def __init__(self,w,b,s="SigMoid"):
        self.layer_name = s
        self.w=w
        self.b = b
    def forward(self,x):
        a=np.dot(x,self.w)+self.b
        return sigmoid(a)

class OutputLayer2:
    def __init__(self,w,b,s="Ouput"):
        self.layer_name = s
        self.w=w
        self.b = b
    def forward(self,x):
        a=np.dot(x,self.w)+self.b
        return softmax(a)

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val # 还原值
        it.iternext()

    return grad

class TestMNIST:
    # 正常情况下,权重或者偏置都应该是学习的结果,这里直接赋值只是一种假设
    def __init__(self):
        self.rate=0.01
        self.grads_b = {}
        self.grads_w = {}
        self.network_w = {}
        self.network_b = {}
        self.network_w['L1'] = np.random.randn(784,50)
        self.network_b['L1'] = np.zeros(50)
        self.network_w['L2'] = np.random.randn(50,100)
        self.network_b['L2'] = np.zeros(100)

        # 输出层
        self.network_w['L3'] = np.random.randn(100,10)
        self.network_b['L3'] = np.zeros(10)

        self.layers=['L1','L2']

    def forward(self,x):
        tmp=x
        for l in self.layers:
            tmp = SigMoidLayer(self.network_w[l],self.network_b[l],l).forward(tmp)
            #print(l,tmp)
        tmp = OutputLayer2(self.network_w['L3'],self.network_b['L3'],'L3').forward(tmp)
        return tmp

    def loss(self,x,t):
        return cross_entropy_error(self.forward(x),t)

    def grad(self,x,t):
        loss_W = lambda W: self.loss(x, t)

        self.grads_w={}
        self.grads_b={}

        self.grads_w['L1']=numerical_gradient(loss_W,self.network_w['L1'])
        self.grads_b['L1']=numerical_gradient(loss_W,self.network_b['L1'])
        print('L1 Done.')

        self.grads_w['L2']=numerical_gradient(loss_W,self.network_w['L2'])
        self.grads_b['L2']=numerical_gradient(loss_W,self.network_b['L2'])
        print('L2 Done.')
        self.grads_w['L3']=numerical_gradient(loss_W,self.network_w['L3'])
        self.grads_b['L3']=numerical_gradient(loss_W,self.network_b['L3'])
        print('L3 Done.')

    def update(self):
        self.network_w['L1']-=self.rate*self.grads_w['L1']
        self.network_b['L1']-=self.rate*self.grads_b['L1']
        self.network_w['L2']-=self.rate*self.grads_w['L2']
        self.network_b['L2']-=self.rate*self.grads_b['L2']
        self.network_w['L3']-=self.rate*self.grads_w['L3']
        self.network_b['L3']-=self.rate*self.grads_b['L3']



nett = TestMNIST()
#测试代码
#nett.forward(np.random.randn(100,784))

# 学习过程
mini_batch_size = x_train.shape[0]
select_size = 100
loss_point =[]
train_time=10000

for i in range(train_time):
    # 选择数据
    mask = np.random.choice(mini_batch_size, select_size)
    x_batch = x_train[mask]
    t_batch = t_train[mask]
    print(x_batch.shape,t_batch.shape)
    print("Loop",i+1)
    # 损失函数
    loss_p=nett.loss(x_batch,t_batch)
    loss_point.append(loss_p)
    print("Loss:",loss_p)
    # 计算梯度
    nett.grad(x_batch,t_batch)
    # 更新参数
    nett.update()



#推理过程
batch_size=100
accuracy_cnt=0

for i in range(0,len(x_test),batch_size):
    x_batch = x_test[i:i+batch_size]
    t_batch = t_test[i:i+batch_size]

    y_batch = nett.forward(x_batch)
    #print(y_batch.shape)
    p=np.argmax(y_batch,axis=1)
    t=np.argmax(t_batch,axis=1)
    accuracy_cnt+=(np.sum(p==t)/batch_size)

print("Accuracy:" + str(float(accuracy_cnt) / (len(x_test)/batch_size)))


import matplotlib.pyplot as plt
 # 生成数据
xx = np.arange(0, train_time)
# 绘制图形
plt.plot(xx, loss_point)
plt.show()
  • 首先要注意的是矩阵的形状一定要“严丝合缝”,这是做矩阵计算的基础

    • np.zeros(50)是创建0矩阵的方法,第一个参数是shape
  • 然后当运行该代码时可以发现一点,就是随机梯度下降的处理速度十分缓慢,所以在之后会需要反向传播这一方法来加速梯度的计算

评价方法

评价一个神经网络的方法就是评价它的泛化能力,也就是对于陌生数据的识别精度:

  • 如果只对训练数据识别精度较好,则是发生了过拟合
  • 一般来说,当学习经过一个epoch,就可以对测试数据和训练数据进行推理,然后进行可视化对比,这里只给出伪代码:
    image-20251019192442837.png

image-20251019192500604.png

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000笔训练数据,用大小为 100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了。此时,100次就是一个epoch。

不幸的结果

和原著对比,这里的效果差了很多(最重要的是运行时间巨长!!!!整整2个星期),原因会放在未来探索,可能的原因一方面是初始化参数的问题,一方面可能是层级结构的问题。准确度在:Accuracy:0.7011999999999997 而不是0.9以上,梯度下降的也不太对,如下图。

但不管怎么样,SGD是有效果的,接下来需要解决其速度太慢的问题。
image-20251110204818780.png

image-20251103191147939.png

标签: none

评论已关闭