avatar


1.回归与分类

回归和分类是监督学习中,最常见的两类问题。
主要区别在于目标值的不同。

  • 回归:目标值是连续的
  • 分类:目标值是离散的

我们依次讨论回归和分类这两个问题,并试图借此引出一个深度学习的简单雏形。

回归

在回归问题中,线性回归是一种最常见的一种。
我们从线性回归开始。

损失函数

首先,我们讨论一下,为什么需要损失函数?什么是损失函数?

我们假设存在一个线性方程如下:

y=wx+by = wx +b

现在,存在两个点:(1,2)(2,3)
通过消元法,我们很容易得到这个方程的wb

但,这是在非常理想的情况下。
实际中,因为我们模型不确定和存在噪音等原因,我们无法简单的通过消元法得到线性方程的wb
例如,我们有几组数据,如下。

带有噪音

这时候,想通过消元法来求线性方程的wb,是不可能的。

那么,我们换一个思路。我们不求精确解,我们求近似解。我们构造一个模型去拟合。
那么,怎么衡量我们的模型与这些点的拟合好不好呢?损失函数

loss=in(wxi+byi)2nloss = \frac{\sum_i^n{(w * x_i + b - y_i)}^2}{n}

这里,我们把均方误差作为损失函数。
损失函数的值越小,说明拟合的越好。所以,现在我们的目标是要使损失函数的值最小。

梯度下降

但是,我们还有一个问题没解决,wb的近似解,还是不知道。
那么怎么求近似解呢?梯度下降

例如f(x)=x2f(x) = x^2
函数图像
现在我们用梯度下降求这个函数的极值。

  1. 我们随机选取一点,比如选取了(4,16)。
  2. 求该点处的导数,导数为8。
  3. 所以我们往后退8个单位,选取新的点。
    1. 如果,我们的单位是1,则新的点是(-4,16)。
    2. 如果,我们的单位是0.1,则新的点是(3.2,9.4)。
    3. 所以每次后退几个单位,这个要调到一个合适的值。
  4. 如此循环往复,直到取到了极小值,或达到了一定的迭代次数。

这就是梯度下降的方法,而在这里的单位就是学习率

梯度下降的过程,用数学公式表达如下:

w=wlrlossww' = w - lr * \frac{\partial loss}{\partial w}

b=blrlossbb' = b - lr * \frac{\partial loss}{\partial b}

线性回归的实现

通过刚刚的讨论,我们知道了需要损失函数、知道了用梯度下降求解wb的。现在我们试图通过Python代码实现。
我们的步骤有:

  1. 计算损失
  2. 梯度下降
  3. 用梯度下降得到的新的wb,再计算损失。如此迭代更新。

计算损失

loss=in(wxi+byi)2nloss = \frac{\sum_i^n{(w * x_i + b - y_i)}^2}{n}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 损失函数
def loss(b,w,xArr,yArr):
'''
损失函数
:param b: 偏置
:param w: 权重
:param xArr: xArr
:param yArr: yArr
:return: 损失函数的值
'''
# 损失值
total_loss = 0
for i in range(0,len(xArr)):
x = xArr[i]
y = yArr[i]
total_loss = total_loss + (y - (w * x + b)) ** 2

return total_loss/(float(len(xArr)))

梯度下降

w=wlrlossww' = w - lr * \frac{\partial loss}{\partial w}

b=blrlossbb' = b - lr * \frac{\partial loss}{\partial b}

其中

lossw=in2(wxi+byi)xin\frac{\partial loss}{\partial w} = \frac{\sum_i^n 2(w x_i + b - y_i) x_i}{n}

lossb=in2(wxi+byi)n\frac{\partial loss}{\partial b} = \frac{\sum_i^n 2(w x_i +b - y_i)}{n}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 一步,梯度
def step_gradient(b_cur,w_cur,xArr,yArr,lr):
'''
一步,梯度
:param b_cur: 当前的偏置
:param w_cur: 当前的权重
:param xArr: xArr
:param yArr: yArr
:param lr: 学习率
:return:
'''
b_gradient = 0
w_gradient = 0
n = float(len(xArr))
for i in range(0,len(xArr)):
x = xArr[i]
y = yArr[i]
w_gradient = w_gradient + (2/n) * ((w_cur * x + b_cur) - y) * x
b_gradient = b_gradient + (2/n) * ((w_cur * x + b_cur) - y)
b_new = b_cur - (lr * b_gradient)
w_new = w_cur - (lr * w_gradient)

return b_new,w_new

迭代更新

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np

# 迭代更新
def gradient_descent(b_start,w_start,xArr,yArr,lr,iterations):
'''
迭代更新
:param points: 数据点
:param b_start: 偏置的初始值
:param w_start: 权重的初始值
:param lr: 学习率
:param iterations: 最大迭代次数
:return:
'''
b = b_start
w = w_start
for i in range(iterations):
b,w = step_gradient(b_cur=b,w_cur=w,xArr=xArr,yArr=yArr,lr=lr)
return b,w

Main方法

我们还需要一个Main方法把这些方法组装在一起。
首先,我们需要数据。这里我们用生成的模拟数据。
示例代码:

1
2
3
4
# 等差数列
xData = np.linspace(-10,10,100)
# 加上噪音
yData = 2.0 * xData + 1.0 + np.random.randn(xData.shape[0]) * 0.2

我们对训练前的损失和训练后的损失进行比较。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
# 给b和w赋初值
b = 0
w = 0

# 初值的损失
print('初值的损失:',loss(b=b,w=w,xArr=xArr,yArr=yArr))

# 迭代更新 学习率0.001,迭代10000次
b,w = gradient_descent(b_start=b,w_start=w,xArr=xArr,yArr=yArr,lr=0.001,iterations=100000)
print('b:',b,' w:',w)
print('训练后的损失:',loss(b=b,w=w,xArr=xArr,yArr=yArr))

运行结果:

1
2
3
初值的损失: 136.91315134241083
b: 1.0170737051230807 w: 1.9987051102211508
训练后的损失: 0.027859262495676634

分类问题

在讨论回归问题之后,我们再讨论分类问题。

我们来个有点挑战,也更有意思的:手写数字识别。

手写数字识别分析

我们首先分析一下整个应用场景。

输入

MNIST手写数字
如图所示,就是手写数字,数据来源与MNIST。每一张图片的形状是[28,28,1],代表这是一张28*28个像素的单通道黑白图片。我们把这张图片转换形状为[784]的向量,即把每一行拼接在上一行的后面。而且,我们会有很多张样本图片,所以我们的输入数据为[N,784],其中N代表输入的样本图片数量。

输出

我们用One-Hot编码作为输出,关于One-Hot编码,在《经典机器学习及其Python实现:1.特征抽取》中有讨论。

例如,数字零的One-Hot编码是[1,0,0···0]。当然,我们得到的输出大概不会那么恰到好处的非0即1,更有可能是[0.8,0.02,0.05,···0.01]这种。

线性模型

在上一章中,我们的模型是

y=wx+by = wx +b

现在,我们换成矩阵模式,则有:

out=XW+b\bold{out} = \bold{X} * \bold{W} + \bold{b}

  • X:[N,784]\bold{X}: [N,784],N行784列的矩阵
  • W:[784,10]\bold{W}: [784,10],784行10列的矩阵
  • b:[10]\bold{b}: [10]

例如,当N=1N=1,即输入的图片数目是1。

[1,784][784,10]+[10][1,784] * [784,10] + [10]

最后我们得到的结果为[1,10][1,10]

但是,这么做,是有问题的。
这是一个线性模型,但实际上,图片识别大概不是线性这么简单,而且比较复杂。
线性方程的复杂度有限,从数据中学习复杂函数映射的能力很小。
所以,我们需要一个新的东西:激活函数

激活函数

我们对之前的线性模型进行修改。

out=f(XW+b)\bold{out} = f(\bold{X}*\bold{W} + \bold{b})

  • f(x)f(x):激活函数

激活函数有很多种,最简单的最常见的一种是ReLU

f(x)={xfor x00for x<0f(x) = \begin{cases} x & \text{for } x \ge 0 \\ 0 & \text{for } x < 0 \end{cases}

ReLU

我们可以看到,ReLU函数是一个非线性函数,这个函数的引入使得我们的模型不再是线性模型。

但是这个模型的效果其实仍不好,因为只有一层,无法学习到复杂的函数映射。

多个模型串行

猫猫流水线
我们把多个模型串在一起。效仿流水线,每一个输出,作为下一个的输入。

h1=relu(xw1+b1)\bold{h_1} = relu(\bold{x} * \bold{w_1} + \bold{b_1})

h2=relu(h1w2+b2)\bold{h_2} = relu(\bold{h_1} * \bold{w_2} + \bold{b_2})

out=relu(h2w3+b3)\bold{out} = relu(\bold{h_2} * \bold{w_3} + \bold{b_3})

我们继续以一张图片为例。

  • x:[1,784]\bold{x}:[1,784]

假设第一层的参数为:

  • w1:[784,512]\bold{w_1}:[784,512]
  • b1:[1,512]\bold{b_1}:[1, 512]

则,经过第一道工序后:

  • h1:[1,512]\bold{h_1}:[1,512]

假设第二层的参数为:

  • w2:[512,256]\bold{w_2}:[512,256]
  • b2:[1,256]\bold{b_2}:[1,256]

则,经过第二道工序后:

  • h2:[1,256]\bold{h_2}:[1,256]

假设第三层的参数为:

  • w3:[256,10]\bold{w_3}:[256,10]
  • b3:[1,10]\bold{b_3}:[1,10]

则,经过第三道工序后:

  • out:[1,10]\bold{out}:[1,10]

损失函数

现在,我们还剩下最后一项。损失函数。我们采用均方误差。

loss=in(wxi+byi)2nloss = \frac{\sum_i^n{(w * x_i + b - y_i)}^2}{n}

小结

现在,我们做个小结。

  1. 初始化w1\bold{w_1}b1\bold{b_1}w2\bold{w_2}b2\bold{b_2}w3\bold{w_3}b3\bold{b_3},并用初始值计算h1\bold{h_1}h2\bold{h_2}out\bold{out}
  2. 计算损失
  3. 计算梯度,迭代更新w1\bold{w_1}b1\bold{b_1}w2\bold{w_2}b2\bold{b_2}w3\bold{w_3}b3\bold{b_3}

基于keras的实现

准备数据

在TensorFlow中自带MNIST数据,我们直接用TensorFlow中的数据。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 准备数据
import tensorflow as tf
from tensorflow.keras import datasets

(x_train, y_train), (x_test, y_test) = datasets.mnist.load_data()

xs = tf.convert_to_tensor(x_train, dtype=tf.float32) / 255.0
db = tf.data.Dataset.from_tensor_slices((x_train, y_train))

for step, (x, y) in enumerate(db):
print(step)
print(x.shape)
print(y)
print(y.shape)
print('\n')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0
(28, 28)
tf.Tensor(5, shape=(), dtype=uint8)
()

1
(28, 28)
tf.Tensor(0, shape=(), dtype=uint8)
()

2
(28, 28)
tf.Tensor(4, shape=(), dtype=uint8)
()

【部分运行结果略】

from_tensor_slices
上面的代码几乎都是顾名思义即可,除了from_tensor_slices
from_tensor_slices的作用是把给定的元组、列表和张量等数据进行特征切片,从第一个维度进行切片。
举一个很形象的例子,南昌的一道菜,藜蒿炒腊肉。
藜蒿炒腊肉
我们把藜蒿和腊肉放一起时,他们的第一个维度是品种,一个是藜蒿,一个是腊肉。那么我们把藜蒿和腊肉放在一起切,这个操作就叫from_tensor_slices

正如我们刚刚的代码,把60000张28*28的图片,从最外层,剥开。

再比如,这个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
import tensorflow as tf
import numpy as np

features, labels = (np.random.sample((6, 3)),
np.random.sample((6, 1)))

print((features, labels))
data = tf.data.Dataset.from_tensor_slices((features, labels))

for var in enumerate(data):
print(var)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(array([[0.45108748, 0.23347295, 0.38551173],
[0.19734783, 0.43067265, 0.25006582],
[0.7943179 , 0.25185879, 0.95525868],
[0.51064133, 0.02124388, 0.25417909],
[0.90138399, 0.94569396, 0.00832238],
[0.55127232, 0.15551541, 0.18047405]]), array([[0.10617923],
[0.51067773],
[0.58266726],
[0.24896404],
[0.64681774],
[0.59264838]]))
(0, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.45108748, 0.23347295, 0.38551173])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.10617923])>))
(1, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.19734783, 0.43067265, 0.25006582])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.51067773])>))
(2, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.7943179 , 0.25185879, 0.95525868])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.58266726])>))
(3, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.51064133, 0.02124388, 0.25417909])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.24896404])>))
(4, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.90138399, 0.94569396, 0.00832238])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.64681774])>))
(5, (<tf.Tensor: shape=(3,), dtype=float64, numpy=array([0.55127232, 0.15551541, 0.18047405])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.59264838])>))

准备模型

根据之前的讨论,我们的模型如下:

h1=relu(xw1+b1)\bold{h_1} = relu(\bold{x} * \bold{w_1} + \bold{b_1})

h2=relu(h1w2+b2)\bold{h_2} = relu(\bold{h_1} * \bold{w_2} + \bold{b_2})

out=relu(h2w3+b3)\bold{out} = relu(\bold{h_2} * \bold{w_3} + \bold{b_3})

现在我们实现这个模型。
示例代码:

1
2
3
4
5
6
7
8
9
10
from tensorflow import keras
from tensorflow.keras import layers,optimizers

model = keras.Sequential([
layers.Dense(512,activation='relu'),
layers.Dense(256,activation='relu'),
layers.Dense(10, activation='relu')
])

optimizer = optimizers.SGD(learning_rate=0.001)

损失函数

示例代码:

1
2
3
4
5
6
7
8
# 计算输出和损失
with tf.GradientTape() as tape:
# [n,28,28] -> [n,784]
x = tf.reshape(x,(-1,28*28))
# 计算输出
out = model(x)
# 计算损失
loss = tf.reduce_mean(tf.square(out - y))

迭代更新

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train_epoch(epoch):
for step,(x,y) in enumerate(train_dataset):
with tf.GradientTape() as tape:
# [n,28,28] -> [n,784]
x = tf.reshape(x,(-1,28*28))
# 计算输出
out = model(x)
# 计算损失
loss = tf.reduce_mean(tf.square(out - y))

# 梯度优化
grads = tape.gradient(loss,model.trainable_variables)
optimizer.apply_gradients(zip(grads,model.trainable_variables))

print(epoch,step,loss.numpy())

epoch、batch、step
我们来讨论一下epoch、batch、step这几个有什么区别。
假设train_dataset中共有1000个样本,每次只取10个样本,则需要循环100次。
用所有的样本的进行一次训练,我们称之为epoch。用一个batch进行一次训练,我们称之为step。一个epoch通常包含一个或多个step。

Main方法

示例代码:

1
2
for epoch in range(10):
train_epoch(epoch)

运行结果:

1
2
3
4
5
6
7
8
9
0 0 1.11935
0 1 1.1162438
0 2 1.1197898

【部分运行结果略】

9 297 0.18006939
9 298 0.24696392
9 299 0.19443126

基于TensorFlow的实现

在上述基于keras的实现中,有很多技术都被封装了。现在我们试图基于TensorFlow,用比较底层的方法重新实现一遍。

准备数据

准备数据的方法和之前基于keras的类似,这里不再赘述。

准备模型

这里,我们用比较底层的方法来实现一个模型。
模型的参数有:

  1. w1:[784,512],b1:[1,512]\bold{w_1}:[784,512],\bold{b_1}:[1,512]
  2. w2:[512,256],b2:[1,256]\bold{w_2}:[512,256],\bold{b_2}:[1,256]
  3. w3:[256,10],b3:[1,10]\bold{w_3}:[256,10],\bold{b_3}:[1,10]

而且,这些参数在训练过程都是变量,不是常量。所以要用tf.Variable
示例代码:

1
2
3
4
5
6
7
8
import tensorflow as tf

w1 = tf.Variable(tf.random.truncated_normal([784, 512], stddev=0.1))
b1 = tf.Variable(tf.zeros([512]))
w2 = tf.Variable(tf.random.truncated_normal([512, 256], stddev=0.1))
b2 = tf.Variable(tf.zeros([256]))
w3 = tf.Variable(tf.random.truncated_normal([256, 10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))
  • 必须是tf.Variable,变量。

计算损失

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
x = tf.reshape(x, [-1,28*28])
with tf.GradientTape() as tape:
h1 = x@w1 + b1
h1 = tf.nn.relu(h1)

h2 = h1@w2 + b2
h2 = tf.nn.relu(h2)

out = h2@w3 + b3
out = tf.nn.relu(out)

loss = tf.square(y - out)
loss = tf.reduce_mean(loss)
  • 为了便于后面的梯度下降,我们这里把这一段代码用tf.GradientTape()包裹起来。
  • 其中@是矩阵相乘。

梯度下降

示例代码:

1
2
3
4
5
6
7
8
9
10
11
# 计算梯度
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
# 更新权重
# w1 = w1 - lr * w1_grad
# 原地更新
w1.assign_sub(lr * grads[0])
b1.assign_sub(lr * grads[1])
w2.assign_sub(lr * grads[2])
b2.assign_sub(lr * grads[3])
w3.assign_sub(lr * grads[4])
b3.assign_sub(lr * grads[5])
  • 必须用assign_sub,原地更新,这样可以维持这些参数更新后还是变量。

迭代更新

与基于keras的实现方式类似,计算损失和梯度下降的方法需要进行迭代更新。这里不再赘述。

Main方法

与基于keras的实现方式类似,这里不再赘述。

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10401
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区