零、预知识

1.Numpy

使用
  1. 介绍:高效的操作多维数组的函数库。

  2. 安装:(前提已经安装了python)

    pip install numpy
    
  3. 导入

    import numpy as np
    
  4. 创建数组

    Numpy最重要的数据结构是多维数组(ndarray)。通过Numpy,你可以轻松创建数组:

    # 从Python列表创建一维数组
    arr1d = np.array([1, 2, 3, 4, 5])
    >[1, 2, 3, 4, 5]
    
    # 从Python嵌套列表创建二维数组
    arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >[[1, 2, 3],
      [4, 5, 6],
      [7, 8, 9]]
    
    # 创建全零数组
    zeros = np.zeros((3, 4))
    >[[0., 0., 0., 0.],
      [0., 0., 0., 0.],
      [0., 0., 0., 0.]]
    
    # 创建全一数组
    ones = np.ones((2, 3))
    >[[1., 1., 1.],
      [1., 1., 1.]]
    
    # 创建指定范围内的数组
    range_arr = np.arange(0, 10, 2)
    >[0, 2, 4, 6, 8]
    
    # 创建线性间隔的数组
    linspace_arr = np.linspace(0, 1, 5)
    >[0.  , 0.25, 0.5 , 0.75, 1.  ]
    
  5. 数组属性

    Numpy数组有许多属性,你可以通过它们来了解数组的维度、形状和元素类型:

    arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    
    print(arr.shape)        # 获取数组的形状 n行m列 输出:(3, 3)
    print(arr.ndim)         # 获取数组的维度 输出:2
    print(arr.size)         # 获取数组的长度 输出:9
    print(arr.dtype)        # 获取数组的元素类型 输出:int64
    
  6. 数组操作

    Numpy提供了许多数组操作函数,使得数组的操作和计算变得简单高效:

    # 数组加法
    arr1 = np.array([1, 2, 3])
    arr2 = np.array([4, 5, 6])
    result = arr1 + arr2
    >[5, 7, 9]
    
    # 数组乘法
    arr = np.array([1, 2, 3])
    result = arr * 2
    >[2, 4, 6]
    
    # 二维数组乘法
    x = np.array([[1, 2], [3, 4]])
    y = np.array([[2, 1], [3, 4]])
    >[[1, 2],   [[2, 1],
      [3, 4]]    [4, 3]]
    >[[2, 2],
      [12, 12]]
    
    # 矩阵乘法 点乘运算
    mat1 = np.array([[1, 2], [3, 4]])
    mat2 = np.array([[5, 6], [7, 8]])
    result = np.dot(mat1, mat2)
    >[[1, 2],     [[5, 6],
      [3, 4]]			 [7, 8]]
    >输出: [[19, 22],
            [43, 50]]
    
    # 数组索引和切片 与python内置数组操作一致
    arr = np.array([1, 2, 3, 4, 5])
    print(arr[0])         # 输出:1
    print(arr[1:4])       # 输出:[2, 3, 4]
    
    # 数组形状变换
    arr = np.array([1, 2, 3, 4, 5, 6])
    reshaped_arr = arr.reshape(2, 3)
    >[[1, 2, 3],
      [4, 5, 6]]
    
  7. 常用数学函数

    Numpy提供了许多常用的数学函数,可以直接应用于数组:

    arr = np.array([1, 2, 3, 4, 5])
    
    print(np.sum(arr))          # 输出:15
    print(np.mean(arr))         # 输出:3.0
    print(np.max(arr))          # 输出:5
    print(np.min(arr))          # 输出:1
    print(np.sin(arr))          # 输出:[0.84147098 0.90929743 0.14112001 -0.7568025  -0.95892427]
    print(np.cos(arr))          # 输出:[0.54030231 -0.41614684 -0.9899925 -0.65364362 0.28366219 0.96017029]
    print(np.power(arr, 2))     # 输出:[1,  4,  9, 16, 25]
    print(np.exp(arr))          # 输出:[2.71828183, 7.3890561, 20.08553692, 54.59815003, 148.4131591 ]
    
广播机制

广播是numpy中一种强大的机制,允许对不同形状的数组进行运算,而不需要显式地进行形状匹配或复制数据。

  1. 广播标量,下图将10当做2x2的矩阵来运算
    请添加图片描述

  2. 数组广播

    请添加图片描述
    通过以上的例子可以看到广播的原则都是低纬度向高纬度看齐,然后补全数据,再进行运算。

2.Matplotlib

Matplotlib是Python中最流行的数据可视化库之一,可以用来绘制图表内容。

安装Matplotlib

在开始之前,确保你已经安装了Python和Matplotlib。如果还没有安装Matplotlib,可以通过以下命令使用pip进行安装:

pip install matplotlib

导入Matplotlib

在使用Matplotlib之前,首先需要导入它。习惯上,我们使用以下方式导入Matplotlib并简写为plt

import matplotlib.pyplot as plt
1. 绘制简单的折线图

折线图是Matplotlib中最简单的图表类型之一,它用于显示数据随着变量的变化而变化的趋势。下面是一个简单的绘制折线图的例子:

# 示例数据
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

# 绘制折线图
plt.plot(x, y)

# 添加标题和标签
plt.title('简单折线图')
plt.xlabel('X轴')
plt.ylabel('Y轴')

# 显示图形
plt.show()

图形绘制如下

请添加图片描述

2.绘制散点图

散点图常用于显示两个变量之间的关系。下面是一个绘制散点图的例子:

# 示例数据
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

# 绘制散点图
plt.scatter(x, y)

# 添加标题和标签
plt.title('简单散点图')
plt.xlabel('X轴')
plt.ylabel('Y轴')

# 显示图形
plt.show()

请添加图片描述

3. 绘制柱状图

柱状图常用于比较不同类别的数据。下面是一个绘制柱状图的例子:

# 示例数据
categories = ['A', 'B', 'C', 'D', 'E']
values = [10, 25, 15, 30, 20]

# 绘制柱状图
plt.bar(categories, values)

# 添加标题和标签
plt.title('简单柱状图')
plt.xlabel('类别')
plt.ylabel('值')

# 显示图形
plt.show()

请添加图片描述

4. 绘制饼图

饼图常用于显示不同类别占总量的比例。下面是一个绘制饼图的例子:

# 示例数据
categories = ['A', 'B', 'C', 'D', 'E']
values = [10, 25, 15, 30, 20]

# 绘制饼图
plt.pie(values, labels=categories, autopct='%1.1f%%')

# 添加标题
plt.title('简单饼图')

# 显示图形
plt.show()

请添加图片描述

5. 自定义图形样式

Matplotlib允许我们自定义图形的样式,包括线条颜色、标记类型、图例等。例如:

x = np.arange(0,6, 0.1)
# 绘制sin图像
y1 = np.sin(x)
# 绘制cos图像
y2 = np.cos(x)

plt.plot(x, y1, label="sin", color='blue')
# 设置图线样式
plt.plot(x, y2, linestyle="--", color='red', label="cos")
plt.xlabel("x")
plt.ylabel("y")
plt.title("sin & cos")
plt.legend()
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9jJZ8AKc-1691499932714)(鱼书笔记.assets/image-20230728194801266.png)]

6.绘制其他图形的方法(总结于chatGPT)
plt.plot() # 绘制折线图。

plt.scatter() # 绘制散点图。

plt.bar() # 绘制柱状图。

plt.barh() # 绘制水平柱状图。

plt.hist() # 绘制直方图。

plt.pie() # 绘制饼图。

plt.boxplot() # 绘制箱线图。

plt.errorbar() # 绘制误差条形图。

plt.contour() # 绘制等高线图。

plt.imshow() # 绘制图像。

plt.polar() # 绘制极坐标图。

plt.stem() # 绘制离散序列的线型图。

plt.fill() 和 plt.fill_between() # 绘制填充图。

plt.stackplot() # 绘制堆叠区域图。

plt.barbs() # 绘制风羽图。

plt.quiver() # 绘制场矢量图。

plt.streamplot() # 绘制流线图。

plt.hexbin() # 绘制六边形二维直方图。

一、感知机

1.感知机原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yayXtuvz-1691499932714)(鱼书笔记.assets/image-20230726224334363.png)]

x1,x2是输入,y是输出,w1,w2是权值,x*w之和超过阀值θ时才会激活y
y={0  (ω1x1+ω2x2≤θ)1  (ω1x1+ω2x2>θ) y = \begin{cases} 0 \,\,( \omega 1x1 + \omega2x2 \le \theta )\\ 1 \,\,( \omega 1x1 + \omega2x2 > \theta )\\ \end{cases} y={0(ω1x1+ω2x2θ)1(ω1x1+ω2x2>θ)

可将 θ\thetaθ 变为-b移到不等式左边,变换为如下表达式。其中 ω1\omega1ω1ω2\omega2ω2 表示权重(用于控制各个信号的重要性),b表示偏置(用于控制神经元被激活的容易程度)。
y={0  (b+ω1x1+ω2x2≤0)1  (b+ω1x1+ω2x2>0) y = \begin{cases} 0 \, \,( b+ \omega 1x1 + \omega2x2 \le 0 )\\ 1 \, \,( b+ \omega 1x1 + \omega2x2 > 0 )\\ \end{cases} y={0(b+ω1x1+ω2x20)1(b+ω1x1+ω2x2>0)

2.简单逻辑电路

与门 AND 代码实现

def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    tmp = np.sum(w*x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

或门 OR 代码实现

def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2
    tmp = np.sum(w*x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

与非门 NAND 代码实现

def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5])
    b = 0.7
    tmp = np.sum(w*x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

3.多层感知机的实现

单层的感知机,只能划分线性空间,想要实现异或门仅靠单层感知机无法实现,所以借助多层感知机进行非线性的空间划分可以解决异或门无法实现的问题。如下图所示,通过一个与非门,一个或门,一个与门相互连接实现了异或门的功能

请添加图片描述
请添加图片描述

  1. 第0层的两个神经元接收输入信号,并将信号发送至第1层的神经元。
  2. 第1层的神经元将信号发送至第2层的神经元,第2层的神经元输出y。
异或门 代码实现
def XOR(x1, x2):
  s1 = NAND(x1, x2)
  s2 = OR(x1, x2)
  y = AND(s1, s2)
  return y

二、神经网络

前面设计与或非门的权重值是人工设计的,后续通过学习神经网络,利用已有的数据学习合适的权重作为参数解决上面的权重问题。

请添加图片描述

1.激活函数

请添加图片描述

根据上图的函数转换,我们就能转换为h(x),这就是激活函数

激活函数类型

激活函数分为阶跃函数和sigmoid函数,其中阶跃函数就是当输入值超过某一阀值时就换转变输出。

  1. 阶跃函数

    定义如下
    h(x)={0  (x≤0)1  (x>0) h(x) = \begin{cases} 0 \,\,( x \le 0 )\\ 1 \,\,( x > 0 )\\ \end{cases} h(x)={0(x0)1(x>0)
    代码实现

    import numpy as np
    import matplotlib.pylab as plt
    
    # 定义阶跃函数
    def step_function(x):
        y = x > 0
        return y.astype(int)
    
    X = np.arange(-5.0, 5.0, 0.1)
    Y = step_function(X)
    plt.plot(X, Y)
    plt.ylim(-0.1, 1.1)  # 指定图中绘制的y轴的范围
    plt.show()
    

请添加图片描述

  1. sigmoid函数

    定义如下
    h(x)=11+exp(−x) h(x) = \frac{1}{1 + exp(-x)} h(x)=1+exp(x)1

    代码实现

    # coding: utf-8
    import numpy as np
    import matplotlib.pylab as plt
    
    # sigmoid函数
    def sigmoid(x):
        return 1 / (1 + np.exp(-x))    
    
    X = np.arange(-5.0, 5.0, 0.1)
    Y = sigmoid(X)
    plt.plot(X, Y)
    plt.ylim(-0.1, 1.1)
    plt.show()
    
    

请添加图片描述

两个激活函数对比

请添加图片描述

  • 共同点:有相似的形状、输入小时输出接近(等于)0,输入大时输出接近(等于)1、输出信号都在0到1之间。
  • 不同点:sigmoid函数是光滑的曲线,阶跃函数是跳跃的折线。
ReLU函数

大于0时直接输出x,小于等于0时输出0
h(x)={x  (x>0)0  (x≤0) h(x) = \begin{cases} x \,\,( x > 0 )\\ 0 \,\,( x \le 0 )\\ \end{cases} h(x)={x(x>0)0(x0)
代码实现

import numpy as np
import matplotlib.pylab as plt

# 定义reLU函数
def relu(x):
    return np.maximum(0, x)

x = np.arange(-5.0, 5.0, 0.1)
y = relu(x)
plt.plot(x, y)
plt.ylim(-1.0, 5.5)
plt.show()

请添加图片描述

2.神经网络的内积

多维数组的运算

二维数组点乘二维数组的运算法则等同于线性代数中学习的矩阵相乘的结果。

使用二维数组点乘一维数组的运算过程中我发现与想象的不太一致。像如下两个数组进行点乘运算,按照线性代数中所学,b矩阵应该要求为2行1列。但使用np.array进行点乘运算结果没有问题。
请添加图片描述

以下总结了二维点乘一维数组的运算规律

请添加图片描述
请添加图片描述

神经网络的内积

请添加图片描述

实现该神经网络时,要注意X、W、Y的形状,特别是X和W的对应维度的元素个数是否一致。

代码实现

请添加图片描述

3层神经网络的实现

请添加图片描述

其中符号的含义
请添加图片描述

  1. 实现第0层到第一层,在上图x1和x2的基础上加上了b1
    请添加图片描述

    用数学式表示 a1a_1a1 如下
    a1(1)=ω11(1)x1+ω12(1)x2+b1(1) a^{(1)}_1 = \omega^{(1)}_{11}x_1 + \omega^{(1)}_{12}x2 + b^{(1)}_1 a1(1)=ω11(1)x1+ω12(1)x2+b1(1)
    根据矩阵点乘算法规则,那么可以将第一层的加权表示成下面的数学式
    A(1)=XW(1)+B(1) A^{(1)} = XW^{(1)} + B^{(1)} A(1)=XW(1)+B(1)
    请添加图片描述

    代码实现

    X = np.array([1.0, 0.5])
    W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    B1 = np.array([0.1, 0.2, 0.3])
    A1 = np.dot(X, W1) + B1
    
  2. 实现下图a1到z1激活函数的转变(sigmoid函数)

请添加图片描述

代码实现

Z1 = sigmoid(A1)
print(A1) # [0.3 0.7 1.1]
print(Z1) # [0.57444252 0.66818777 0.75026011]
  1. 同理实现第一层到第二层的传递

请添加图片描述

代码实现

W2 = np.array([[0.1, 0.4],[0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
  1. 第2层到第3层(输出层)也跟上面步骤基本一致,但激活函数不同
    请添加图片描述

    代码实现

    # 定义恒等函数
    def identity_function(x):
        return x
    
    W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
    B3 = np.array([0.1, 0.2])
    A3 = np.dot(Z2, W3) + B3
    Y = identity_function(A3)
    

    这里定义的恒等函数,会将输入按照原样输出,这里用恒等函数是为了和前面第0层到第1层和第1层到第2层的处理流程保持一致

  2. 总体代码实现

    def identity_function(x):
        return x
    
    # 权重和偏置的初始化  
    def init_network():
        network = {}
        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
    
    # 将输入信号转换为输出信号的方法
    def forward(network, x):
        W1, W2, W3 = network['W1'], network['W2'], network['W3']
        b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        z2 = sigmoid(a2)
        a3 = np.dot(z2, W3) + b3
        y = identity_function(a3)
    
        return y
    
    network = init_network()
    # 定义两个输入x的初值
    x = np.array([1.0, 0.5])
    y = forward(network, x)
    print(y)  # [0.31682708 0.69627909]
    

感知机中神经元流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。

神经网络的激活函数必须使用非线性函数。因为使用线性函数的话,加深神经网络的层数将没有意义

一般而言,对于输出层的激活函数,回归问题用恒等函数,分类问题用softmax函数。

3.输出层的设计

1.三种输出函数的类型
  1. 恒等函数,常用在回归问题上

    def identity_function(x):
        return x
    
  2. sigmoid函数,用在二元分类问题上
    h(x)=11+exp(−x) h(x) = \frac{1}{1 + exp(-x)} h(x)=1+exp(x)1

    def sigmoid(x):
        return 1 / (1 + np.exp(-x))    
    
  3. softmax函数,用在多元分类问题上
    yk=exp(ak)∑i=1nexp(ai) y_k = \frac{exp(a_k)}{\sum_{i=1}^n exp(a_i)} yk=i=1nexp(ai)exp(ak)

    def softmax(a):
        exp_a = np.exp(a)
        sum_exp_a = np.sum(exp_a)
        y = exp_a / sum_exp_a
    
        return y
    

    其中softmax函数表示在各输出之间都有收到输入信号的影响,如图

    请添加图片描述

2.softmax函数溢出改进

之所以要改进softmax函数,是因为计算机所表示的数字是有界限的,比如32位或64位,而 exe^xex 可以可以很大,会超过64位所表示数字的最大值,于是对softmax函数进行如下的改进:(1)分子分母同乘以一个常数(2)将常数移到指数函数内部,记为 logClogClogC (3)用另一个常数替换logClogClogC(4)实例中常用0减去a数组中的最大值C: $ -C $替换这个 C′C'C
请添加图片描述

例子:

请添加图片描述

代码实现改进后的softmax函数

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
3.softmax函数特征

请添加图片描述

我们可以看到输出的y都在0-1之间,且它们的和为1,所以我们可以把他转为概率问题,也就是说输出的越大,他的概率越高,从上图可以看出,输入的a数组元素越大,输出的数组对应元素(即概览)也越大;另外e^x是一个单调递增函数,所以上例中a元素的大小关系和y的大小关系不变,y[2]最大,所以我们在实际上根本不需要softmax函数,直接看a元素就能知道哪个概率最大了(因为softmax需要指数运算,计算量挺大的)

求解机器学习问题的步骤可以分为“学习”和“推理”两个阶段。在学习阶段进行模型的学习,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的softmax函数。在输出层使用softmax函数是因为它和神经网络的学习有关系

4.输出神经元数量

请添加图片描述

由上图可以知道,输出神经元数量由类别数量决定,如输出结果为0-9这10个类别,那么神经元输出则为10个。

4.手写数字识别

# coding: utf-8
import sys, os

sys.path.append(os.pardir)
from dataset.mnist import load_mnist
import numpy as np

# from dataset.mnist import load_mnist
from PIL import Image


def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()


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

img = x_train[0]
label = t_train[0]
print(label)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 把图像的形状变为原来的尺寸
print(img.shape)  # (28, 28)

img_show(img)

在这里插入图片描述
显示的图片是5,且打印的label是5,数字识别正确。

三、神经网络的学习

神经网络的学习指的是根据训练数据找出相关权重参数的过程

1.从数据中学习

  1. 数字识别的方案

请添加图片描述

  1. 训练数据和测试数据

    1.训练数据和测试数据:训练数据为监督数据,就是用来训练模型的,而测试数据就是不包含在训练模型内的数据,用来评判训练后模型好坏的数据。

    2.泛化能力:泛化能力其实就是先训练数据训练模型,然后用测试数据进行测试模型,如果测试的成绩好那么他的泛化能力就好。

    3.过拟合:根据训练数据训练出来的模型,他可以很好的处理测试已经训练过的数据,但是对没有测试过的测试数据却无法处理,所以模型和训练数据太过拟合以至于没有很好的泛化能力

2.损失函数

损失函数是用来评判神经网络好坏的一个重要指标,越低越好,一般有2种评判方法均方误差交叉熵误差

one-hot表示法:仅正确标签为1,其余为0

1.均方误差
  • 数学表达式:
    E=12∑k(yk−tk)2 E = \frac{1}{2}\sum_{k}(y_k - t_k)^2 E=21k(yktk)2

    yky_kyk表示神经网络的输出,tkt_ktk表示监督数据,k表示数据的维数。

  • 代码:

    import numpy as np
    
    def mean_squared_error(y, t):
        return 0.5 * np.sum((y - t) ** 2)
    
  • 实例:

请添加图片描述

2.交叉熵误差
  • 数学表达式:
    E=−∑k(tklogeyk) E = -\sum_{k} (t_klog_ey_k) E=k(tklogeyk)
    其中log表示以e为底的自然对数,yky_kyk是神经网络的输出,tkt_ktk是正确解标签。并且tkt_ktk中只有正确解的索引标签为1,其余为0(one-hot表示)

  • 代码实现:

    def corss_entropy_error(y, t):
        # 在Python中,1e-7 是一个表示科学计数法的数值,也称为浮点数。它表示的是数字 1 乘以 10 的负7次方,即 0.0000001。科学计数法用于表示非常大或非常小的数值,以便简化表示和处理。在这种情况下,1e-7 表示一个非常接近零的小数值。
        delta = 1e-7
        # log表示以e为底数的自然对数
        return -np.sum(t * np.log(y + delta))
    

    代码如下代码中加上了一个微小值delta,因为当出现np.log(0)时会变为负无限大的-inf,这样会导致后续计算无法进行。添加微小值可以防止负无限大的发生。

    因为只有t为1时才计算,所以计算量比均方误差小,同时log是个负数的单调递增函数,趋向于0,所以y越大则E的结果越趋向于0,那么其误差结果就越小。

  • 实例:

请添加图片描述

从上图可以看到第一个例子正确时概率高,损失函数的结果低,所以他的神经网络模型好。

3.mini-batch学习

E=−1N∑n∑k(tnkloge(ynk)) E=-\frac{1}{N}\sum_n\sum_k(t_{nk} log_e(y_{nk})) E=N1nk(tnkloge(ynk))

这里,假设数据有N个,tnkt_{nk}tnk表示第n个数据的第k个元素的值(ynky_{nk}ynk是神经网络的输出,tnkt_{nk}tnk是监督数据)。以上表达式就是将N个数据的损失函数的值取平均值。

# 改良交叉熵误差函数的实现
def cross_entropy_error_improved(y, t):
    # y的维度为1时
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    
    batch_size= y.shape[0]
    # 以下是t为one-hot表示形式的实现
    return -np.sum(t * np.log(y + 1e-7)) / batch_size
    # 以下是标签表示法,np.arange(batch_size)会生成一个0到batch_size-1的数组
    # return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

mini-batch简单说就是采取部分样本计算出的结果近似看为整体的计算结果。

在进行神经网络的学习时,不能将识别精度作为指标。因为如果以识别精度为指标,则参数的导数在绝大多数地方都会变为0。

得益于sigmoid函数的斜率不为0,神经网络的学习才得以正确进行。

3.数值微分

1.导数

导数的定义:表示函数某一点的瞬间变化率,数学表达式如下
df(x)dx=lim⁡h→0f(x+h)−f(x)h \frac{df(x)}{dx} = \lim_{h\rightarrow0} \frac{f(x+h) - f(x)}{h} dxdf(x)=h0limhf(x+h)f(x)
考虑代码实现求函数的导数,可以将h设置为非常非常小的值,如10−5010^{-50}1050,则代码如下:

def numerical_diff(f, x):
  h = 1e-50 # 0.0001
  return (f(x+h) - f(x)) / h

需改进点:

  • 10−5010^{-50}1050在Python中会产生舍入误差(rounding error)。如下运行的结果

    >>> np.float32(1e-50)
    0.0
    

    使用float32类型的浮点数表示10−5010^{-50}1050则直接变成了0.0,无法正确表示。所以需要改进这个微小值。这里考虑使用10−410^{-4}1041e-4

  • f(x+h)-f(x)/h(向前差分)这个误差也很大,因为根据1的改变,h不是一个趋近于0的数,所以误差变大,应该用中心法改成f(x+h)-f(x-h)/2h(中心差分)
    请添加图片描述

改进后代码:

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

注意:这种利用微小差分的导数过程为数值微分,而用数学公式推导的如y=x²导数为y=2x这种交解析性求导,这种叫做真导数

2.一个微分的例子

如:y=0.01x²+0.1x的导数实现

运行结果如下:

请添加图片描述

可以发现改进后的微分代码误差非常小

3.偏导数

一个函数有多个自变量时的导数成为偏导数,表达式∂f∂x0\frac{\partial f}{\partial x_0}x0f∂f∂x1\frac{\partial f}{\partial x_1}x1f


f(x0,x1)=x02+x12 f(x_0, x_1) = x_0^2 + x_1^2 f(x0,x1)=x02+x12
使用matplotlib绘制的图像如下

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 创建数据点
x0 = np.linspace(-10, 10, 100)
x1 = np.linspace(-10, 10, 100)
# 使用 np.meshgrid 函数可以将这两个一维数组转换为两个二维数组 x0 和 x1,这将构成我们的网格。
x0, x1 = np.meshgrid(x0, x1)
f = x0**2 + x1**2

# 创建 3D 图像
fig = plt.figure()
# projection='3d' 指定这是一个三维图像
ax = fig.add_subplot(111, projection='3d')

# 绘制曲面
# cmap='viridis' 指定了颜色映射,这里使用了 Viridis 颜色映射
ax.plot_surface(x0, x1, f, cmap='viridis')

# 设置轴标签
ax.set_xlabel('x0')
ax.set_ylabel('x1')
ax.set_zlabel('f(x0, x1)')

# 显示图像
plt.show()

请添加图片描述

偏导数实现:原理其实跟一元导数一样,就是带入一个真值消除一个变量而已

请添加图片描述

4.梯度

由全部变量的偏导数汇总而成的向量(∂f∂x0\frac{\partial f}{\partial x_0}x0f∂f∂x1\frac{\partial f}{\partial x_1}x1f)称为梯度

比如我们求一个函数y=x0²+x1²变量有x0,x1,当我们对他全部变量(这里最多只有2个)进行偏导汇总而成的变量叫梯度。

实现梯度的代码如下

# 实现梯度
def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x)

    for idx in range(x.size):
        tem_val = x[idx]
        # f(x + h) 的计算
        x[idx] = tem_val + h
        fxh1 = f(x)

        # f(x - h) 的计算
        x[idx] = tem_val - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = tem_val # 还原倍数
    
    return grad

请添加图片描述

从这个图可以看出,梯度指向函数f(x0,x1)f(x_0,x_1)f(x0,x1)的最低处(最小值),就像指南针一样,所有的箭头都指向同一点。其次我们发现,离“最低处”越远,箭头越大。梯度指示的方向是各点处的函数值减小最多的方向,这是一个重要的性质

1.梯度法

在梯度法中,函数的取值从当前位置沿着梯度方向前进一段距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样不断的沿梯度方向前进,逐渐减小函数值的过程就是梯度法,用数学表达式来表示则如下所示

x0=x0−η∂f∂x0 x_0 = x_0 - \eta\frac{\partial f}{\partial x_0} x0=x0ηx0f

x1=x1−η∂f∂x1 x_1 = x_1 - \eta\frac{\partial f}{\partial x_1} x1=x1ηx1f

其中,η\etaη表示更新量,在神经网络的学习中,称为学习率。学习率决定在一次更新中更新的程度。

梯度下降算法代码实现:

# 梯度下降法找最小值
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

用梯度法求函数f(x0,x1)=x02+x12f(x_0, x_1) = x_0^2 + x_1^2f(x0,x1)=x02+x12的最小值如下

请添加图片描述

最终结果为(-6.11110793e-10 8.14814391e-10),非常接近(0,0)。实际上,真的最小值就是(0, 0)。所以说通过梯度法我们基本得到了正确结果。用图示来表示梯度法的更新过程则如下:

请添加图片描述

学习率η\etaη不可过大也不可过小,太大时结果会发散成很大的数,太小的话结果几乎没更新就结束了

像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。

2.神经网络的梯度

我们有2*3的W权重参数,L为损失函数,梯度用∂L∂W\frac{\partial L}{\partial W}WL表示,如下所示
W={ω11ω12ω13ω21ω22ω23} W = \begin{Bmatrix} \omega_{11}&\omega_{12}&\omega_{13}\\ \omega_{21}&\omega_{22}&\omega_{23}\\ \end{Bmatrix} W={ω11ω21ω12ω22ω13ω23}

∂L∂W={∂L∂ω11∂L∂ω12∂L∂ω13∂L∂ω21∂L∂ω22∂L∂ω23} \frac{\partial L}{\partial W} = \begin{Bmatrix} \frac{\partial L}{\partial \omega_{11}}&\frac{\partial L}{\partial \omega_{12}}&\frac{\partial L}{\partial \omega_{13}}\\ \frac{\partial L}{\partial \omega_{21}}&\frac{\partial L}{\partial \omega_{22}}&\frac{\partial L}{\partial \omega_{23}}\\ \end{Bmatrix} WL={ω11Lω21Lω12Lω22Lω13Lω23L}

∂L∂W\frac{\partial L}{\partial W}WL的元素由各个元素关于W的偏导数构成。比如,第一行第一列的元素∂L∂ω11\frac{\partial L}{\partial \omega_{11}}ω11L表示当ω11\omega_{11}ω11稍微变化时,损失函数L会发生多大变化。这里的重点是∂L∂W\frac{\partial L}{\partial W}WL的形状和W相同。

代码实现:

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

numerical gradient(f,net.w) 的结果是dw,一个形状为2 x 3的二维数组。观察一下dw的内容,例如,会发现∂L∂W\frac{\partial L}{\partial W}WL∂L∂W11\frac{\partial L}{\partial W_{11}}W11L 的值大约是0.2,这表示如果将ω11\omega_{11}ω11增加h,那么损失函数的值会增加0.2h。再如,∂L∂W23\frac{\partial L}{\partial W_{23}}W23L对应的值大约是-0.5,这表示如果将ω23\omega_{23}ω23增加h,损失函数的值将减小0.5h。因此,从减小损失函数值的观点来看,ω23\omega_{23}ω23应向正方向更新,ω11\omega_{11}ω11应向负方向更新。至于更新的程度,ω23\omega_{23}ω23ω11\omega_{11}ω11的贡献要大。
说了那么多就一句话,dw是w的梯度,也就是斜率,dw的值正斜率就正,所以我们要反向减少他的w值,反之同理

3.学习算法的实现

神经网络的学习步骤如下:
在这里插入图片描述

1.实现两层神经网络:
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 初始化权重
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    # x:输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x:输入数据, t:监督数据
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        #这里的numerical_gradient(x,t)和(self,x,t)不是一个东西,他是外部包的函数
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

在这里插入图片描述
在这里插入图片描述

2.mini-batch实现
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000  # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 计算梯度
    #grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)
    
    # 更新参数
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key] #x_batch和这里是引用关系,所以会相互影响数据
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

结果如下图所示
在这里插入图片描述
根据梯度法,发现权重参数的损失值确实逐渐的减小,所以神经网络确实在学习。
可以发现,基于训练数据的神经网络,用测试数据进行测试时基本一致,所以达到了泛化的作用,也就是不拟合。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐