1. 深度学习简介
1.1 起源
时至今日,绝大多数神经网络都包含以下的核心原则:
- 交替使用线性处理单元与非线性处理单元,它们经常被称为“层”。
- 使用链式法则(即反向传播)来更新网络的参数。
2. 预备知识
2.1 获取和运行本书代码(Windows环境)
- 安装Anaconda3
- pip配置清华镜像源
- 下载包含本书全部代码的压缩包,解压后在目录下打开cmd执行
conda env create -f environment.yml
# To activate this environment, use:
# > activate gluon
#
# To deactivate an active environment, use:
# > deactivate
#
# * for power-users using bash, you must source
- 激活创建的环境
activate gluon
- 打开Jupyter记事本
jupyter notebook
2.2 数据操作
2.2.1 创建NDArray
- from mxnet import nd
- x = nd.arange(12) // 创建一个行向量,维度为12:[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.]
- x.shape // 获取NDArray实例的形状:(12, )
- x.size // 获取NDArray实例中元素的个数:12
- X = x.reshape((-1, 4)) // 将行向量x的形状改为(3, 4),即3行4列的矩阵,元素保持不变,等价于.reshape((3, 4))。因为指定一个维度之后,另一个维度可计算出来,因此用-1代替
- nd.zeros((2, 3, 4)) // 创建一个各元素为0,形状为(2, 3, 4)的张量。实际上,之前创建的向量和矩阵都是特殊的张量。
- nd.ones((3, 4)) // 创建各元素为1的张量
- Y = nd.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) // 通过Python的列表(list)指定需要创建的NDArray中每个元素的值
- nd.random.normal(0, 1, shape=(3, 4)) // 创建一个形状为(3, 4)的NDArray。它的每个元素都随机采样于均值为0、标准差为1的正态分布。
2.2.2 运算
- +:按元素加,两个NDArray实例的shape必须一样
- *:按元素乘,两个NDArray实例的shape必须一样
- /:按元素除,两个NDArray实例的shape必须一样
- Y.exp():按元素做指数运算,等价于nd.exp(Y)
- nd.dot(X, Y.T):矩阵乘法,其中Y.T表示Y的转置
- nd.concat(X, Y, dim=0):在行上(dim=0)连接两个NDArray实例,要求X和Y的列数相同
- nd.concat(X, Y, dim=1):在列上(dim=1)连接两个NDArray实例,要求X和Y的行数相同
- X == Y:判断X与Y的各个元素是否相等,要求X与Y的shape相同,输出结果为同shape的NDArray实例,对应元素相同输出1,反之输出0。X > Y和X < Y具有相同的操作逻辑。
- X.sum():对X中所有元素求和得到只有一个元素的NDArray实例,等价于nd.sum(X)
- X.norm():求X的L2范数,即各元素平方和开根号
- X.norm().asscalar():将单元素NDArray实例变换为Python中的标量
2.2.3 广播机制
当两个形状不同但兼容的NDArray实例进行按元素的加减乘除时,便会触发广播机制。所谓广播机制,是在运算之前,先将两个NDArray实例扩充成行列最大化的张量,再进行按元素的计算。
所谓兼容,是指两个NDArray实例shape相同或者需要扩展的NDArray实例在待扩展维度上的大小为1。下面举例说明:
- 假设X是2×3的矩阵,Y是3×2的矩阵,首先假设X与Y兼容,那么需要将两者均扩展至3×3的矩阵,从而X需要在行的维度扩展至三行,但X本身行的维度为2,所以无法进行扩展;Y在列的维度上无法扩展的理由类似。所以X与Y是不兼容的。
- 假设X是3×1的矩阵,Y是1×2的矩阵,首先假设X与Y兼容,那么需要将两者均扩展至3×2的矩阵,从而X需要在列的维度扩展至2列,相应的操作为将第一列的元素复制到第二列;Y需要在行的维度上扩展至3行,相应的操作为将第一行的元素复制到第2行和第3行。所以X与Y是兼容的。
2.2.4 索引
- X[1:3]:截取矩阵X中行索引为1和2的两行
- X[:, 1:3]:依据左闭右开指定范围的惯例,截取矩阵X中列索引为1和2的两列
- X[1, 2] = 9:访问单个元素并赋值
- X[1:3, :] = 12:截取部分元素并赋值
2.2.5 运算的内存开销
- Y = X + Y:为X + Y开了新的内存,然后Y指向新的内存
- Y[:] = X + Y:为X + Y开了新的内存,但将计算结果按元素赋值给Y
- Y += X:效果同Y[:] = X + Y
- nd.elemwise_add(X, Y, out=Z):不会新开内存
2.2.6 NDArray和NumPy相互变换
import numpy as np
from mxnet import nd
P = np.ones((2, 3))
D = nd.array(P) # 将NumPy转换为NDArray
T = D.asnumpy() #将NDArray转换为NumPy
2.3 自动求梯度
1、定义变量
from mxnet import autograd, nd
x = nd.arange(4).reshape((4, 1)) # 定义变量x并赋初值
2、申请存储梯度所需的内存
为了求有关变量x的梯度,我们需要先调用attach_grad函数来申请存储梯度所需要的内存。
x.attach_grad()
3、定义变量函数
下面定义有关变量x的函数。为了减少计算和内存开销,默认条件下MXNet不会记录用于求梯度的计算。我们需要调用record函数来要求MXNet记录与求梯度有关的计算。
with autograd.record():
y = 2 * nd.dot(x.T, x)
4、调用函数自动求梯度
由于x的形状为(4, 1),y是一个标量。接下来我们可以通过调用backward
函数自动求梯度。需要注意的是,如果y不是一个标量,MXNet将默认先对y中元素求和得到新的变量,再求该变量有关x的梯度。
y.backward()
5、获取梯度值
x.grad
2.4 查阅文档
2.4.1 查找模块里的所有函数和类
当我们想知道一个模块里面提供了哪些可以调用的函数和类的时候,可以使用dir函数。下面我们打印nd.random模块中所有的成员或属性。
from mxnet import nd
print(dir(nd.random))
2.4.2 查找特定函数和类的使用
在Jupyter记事本里,我们可以使用?来将文档显示在另外⼀个窗口中。例如,使用nd.random.uniform?
将得到与help(nd.random.uniform)
几乎一样的内容,但会显示在额外窗口里。此外,如果使⽤nd.random.uniform??
,那么会额外显示该函数实现的代码。
3. 深度学习基础
3.1 线性回归
在机器学习术语里,该数据集被称为训练数据集(training data set)或训练集(training set),一栋房屋被称为一个样本(sample),其真实售出价格叫作标签(label),用来预测标签的两个因素叫作特征(feature)。特征用来表征样本的特点。
损失函数中的常数1/2使对平方项求导后的常数系数为1,这样在形式上稍微简单一些。
解析解(analytical solution):直接通过表达式计算出结果
数值解(numerical solution):通过优化算法有限次迭代模型参数来尽可能降低损失函数的值
小批量随机梯度下降(mini-batch stochastic gradient descent):先选取一组模型参数的初始值,如随机选取;接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch) ,然后求小批量中数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。
超参数(hyperparameter)。我们通常所说的“调参”指的正是调节超参数,例如通过反复试错来找到超参数合适的值。
3.2 线性回归的从零开始实现
#coding=utf-8
'''
licensed to huwei
created on 12/27/2019
'''
from mxnet import nd,autograd
import random
# **********************生成数据集**********************
num_features = 2 # 特征数
num_samples = 1000 # 样本数
true_w = [2, -3.4] # 真实权重
true_b = 4.2 # 真实偏差
samples = nd.random.normal(scale = 1, shape = (num_samples, num_features)) # 随机正态分布生成样本特征
lables = true_w[0] * samples[:, 0] + true_w[1] * samples[:, 1] + true_b # 生成训练集的标签
lables += nd.random.normal(scale = 0.01, shape = lables.shape) # 为标签加上标准差为0.01的随机正态分布噪声
# **********************读取数据函数**********************
# 从样本中随机抽取批量为batch_size的样本,直至完全抽取
# 例如样本为1000,小批量的大小为10,则每次随机抽取10个样本
# 一轮完整的抽取需要100次
def data_iter(batch_size, samples, lables):
num_samples = len(samples) # 计算样本数量
indices = list(range(num_samples)) # 生成0~num_samples的list
random.shuffle(indices) # 随机打乱indices
for i in range(0, num_samples, batch_size):
j = nd.array(indices[i : min(i + batch_size, num_samples)]) # 随机返回10个样本的下标数组
yield samples.take(j), lables.take(j) # take函数根据索引数组获取samples的数组子集
# **********************初始化模型参数**********************
# 将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0
w = nd.random.normal(scale = 0.01, shape = (num_features, 1))
b = nd.zeros(shape = (1,))
# 之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此需要创建它们的梯度
w.attach_grad()
b.attach_grad()
# **********************定义模型**********************
# X为样本,w为权重,b为偏差
def linear_reg(X, w, b):
return nd.dot(X, w) + b
# **********************定义损失函数**********************
# pred_y为y的预测值,true_y为y的真实值
# 需要把真实值true_y变形成预测值pred_y的形状
# 损失函数除以2是为了使对平方项求导后的常数系数为1,形式上更加简洁
def squared_loss(pred_y, true_y):
return (pred_y - true_y.reshape(pred_y.shape)) ** 2 / 2
# **********************定义优化算法**********************
# 由于param并非标量,MXNet自动求梯度时会对param中元素求和得到新的变量,
# 再求该变量对模型参数的梯度,相当于对batch_size个标量表达式求梯度,再求和
# 最后除以batch_size得到梯度平均值
def sgd(params, learn_rate, batch_size):
for param in params:
param[:] = param - learn_rate * param.grad / batch_size
# **********************训练模型**********************
batch_size = 10 # 一次抽取的样本数量
learn_rate = 0.03 # 学习率(超参数)
num_iter_cycle = 3 # 迭代周期个数(超参数),每个迭代周期会训练整个训练集
for iter_cycle in range(num_iter_cycle):
# 在每一个迭代周期中,会使用训练数据集中所有样本一次(假设样本数能够被批量大小整除)。
# X和y分别是小批量样本的特征和标签
for X, y in data_iter(batch_size, samples, lables):
with autograd.record():
loss = squared_loss(linear_reg(X, w, b), y) # loss是有关小批量X和y的损失
loss.backward() # 小批量的损失对模型参数求梯度
sgd([w, b], learn_rate, batch_size) # 使用小批量随机梯度下降迭代模型参数
# 完成一个迭代周期,训练集中所有样本均经过训练
cur_cycle_loss = squared_loss(linear_reg(samples, w, b), lables) # 计算整个训练集的损失
print("Cycle %d, loss = %f" % (iter_cycle + 1, cur_cycle_loss.mean().asscalar()))