Update index.md

This commit is contained in:
Cheng Lai
2022-01-20 17:09:39 +08:00
committed by GitHub
parent bf938ffd87
commit 5e628c33aa

View File

@@ -1,9 +1,4 @@
---
bibliography:
- references.bib
---
# 编程模型 {#ch:RL_toxonomy}
# 编程接口
现代机器学习框架包含大量的组件。这些组件使得用户得以高效开发机器学习算法处理数据部署模型性能调优和使用硬件加速器。在设计这些组件的编程接口时一个核心的诉求是如何平衡框架性能和易用性为了达到最优的性能开发者需要利用硬件亲和的编程语言如C和C++来进行开发。这是因为C和C++的使用使得机器学习框架可以高效硬件的底层API从而最大限度发挥硬件。同时现代操作系统如Linux和Windows提供丰富的基于C和C++的编程接口如文件系统网络编程多线程管理等通过直接调用操作系统API可以降低框架运行的开销。
@@ -21,11 +16,11 @@ bibliography:
## 机器学习系统编程模型的演进
![机器学习编程库发展历程](figs/ch03/framework_development_history.pdf){#fig:ch03/framework_development_history
width="\\linewidth"}
![机器学习编程库发展历程](../img/ch02/framework_development_history.png)
:width:`400px`
:label:`img_framedh`
随着机器学习通的诞生,如何设计易用且高性能的编程接口就一直成为了框架设计者首要解决的问题。在早期的机器学习框架中(如图[1.1](#fig:ch03/framework_development_history){reference-type="ref"
reference="fig:ch03/framework_development_history"}所示人们选择用LuaTorch和PythonTheano等高层次编程语言来编写机器学习程序。这些早期的机器学习框架提供了机器学习必须的模型定义自动微分等功能其适用于编写小型和科研为导向的机器学习应用。
随着机器学习通的诞生,如何设计易用且高性能的编程接口就一直成为了框架设计者首要解决的问题。在早期的机器学习框架中(如图:numref:`img_framedh`所示人们选择用LuaTorch和PythonTheano等高层次编程语言来编写机器学习程序。这些早期的机器学习框架提供了机器学习必须的模型定义自动微分等功能其适用于编写小型和科研为导向的机器学习应用。
在2011年深度神经网络快速崛起并很快在各个AI应用领域计算机视觉语音识别自然语言处理等取得了最先进的性能。训练深度神经网络需要消耗大量的算力而这些算力无法被以Lua和Python所主导开发的Torch和Theano所满足。与此同时计算加速卡如英伟达GPU的通用编程接口例如CUDA
C日趋成熟而构建于CPU多核技术之上的多线程库POSIX
@@ -48,8 +43,7 @@ API从而可以快速导入已有的模型
## 机器学习工作流
机器学习系统编程模型的首要设计目标是:对开发者的整个工作流进行完整的编程支持。一个常见的机器学习任务一般包含如图[1.2](#fig:ch03/workflow){reference-type="ref"
reference="fig:ch03/workflow"}所示的流程。这个工作流完成了训练数据集的读取模型的训练测试和调试。通过归纳我们可以将这一工作流中用户所需要自定义的部分通过定义以下API来支持我们这里假设用户的高层次API以Python函数的形式提供
机器学习系统编程模型的首要设计目标是:对开发者的整个工作流进行完整的编程支持。一个常见的机器学习任务一般包含如图numref:`img_workflow`所示的流程。这个工作流完成了训练数据集的读取模型的训练测试和调试。通过归纳我们可以将这一工作流中用户所需要自定义的部分通过定义以下API来支持我们这里假设用户的高层次API以Python函数的形式提供
- **数据处理:**
首先用户需要数据处理API来支持将数据集从磁盘读入。进一步用户需要对读取数据进行数据预处理从而可以将数据输入后续的机器学习模型中。
@@ -68,169 +62,180 @@ reference="fig:ch03/workflow"}所示的流程。这个工作流完成了训练
- **测试和调优:**
训练过程中用户需要测试API来对当前模型的精度进行评估。当精度达到目标后训练结束。这一过程中用户往往需要调试API来完成对模型的性能和正确性进行验证。
![机器学习系统工作流](ch03/workflow.pdf){#fig:ch03/workflow
width="\\linewidth"}
![机器学习系统工作流](../img/ch02/workflow.png)
:width:`400px`
:label:`img_workflow`
### 数据处理
### 环境配置
在构建机器学习工作流程前MindSpore需要通过context.set_context来配置运行需要的信息如运行模式、后端信息、硬件等信息。
导入context模块配置运行需要的信息。
import os
import argparse
from mindspore import context
parser = argparse.ArgumentParser(description='MindSpore MLPNet Example')
parser.add_argument('--device_target', type=str, default="CPU", choices=['Ascend', 'GPU', 'CPU'])
args = parser.parse_known_args()[0]
context.set_context(mode=context.GRAPH_MODE, device_target=args.device_target)
```python
import os
import argparse
from mindspore import context
parser = argparse.ArgumentParser(description='MindSpore MLPNet Example')
parser.add_argument('--device_target', type=str, default="CPU", choices=['Ascend', 'GPU', 'CPU'])
args = parser.parse_known_args()[0]
context.set_context(mode=context.GRAPH_MODE, device_target=args.device_target)
```
上述配置样例运行使用图模式。根据实际情况配置硬件信息譬如代码运行在Ascend
AI处理器上则--device_target选择Ascend代码运行在CPU、GPU同理。
### 数据处理
配置好运行信息后首先讨论数据处理API的设计。这些API提供了大量Python函数支持用户用一行命令即可读入常见的训练数据集如MNISTCIFARCOCO等
在加载之前需要下载数据集存放在./datasets/MNIST_Data路径中MindSpore提供了用于数据处理的API模块
mindspore.dataset用于存储样本和标签。在加载数据集前通常会对数据集进行一些处理mindspore.dataset也集成了常见的数据处理方法。
以下代码读取了MNIST的数据是大小为28$\times$`<!-- -->`{=html}28的图片返回DataSet对象。
以下代码读取了MNIST的数据是大小为$28 \times 28$的图片返回DataSet对象。
import mindspore.dataset as ds
DATA_DIR = './datasets/MNIST_Data/train'
mnist_dataset = ds.MnistDataset(DATA_DIR)
```python
import mindspore.dataset as ds
DATA_DIR = './datasets/MNIST_Data/train'
mnist_dataset = ds.MnistDataset(DATA_DIR)
```
有了DataSet对象后通常需要对数据进行增强常用的数据增强包括翻转、旋转、剪裁、缩放等在MindSpore中是使用map将数据增强的操作映射到数据集中的之后进行打乱Shuffle和批处理Batch
```python
# 导入需要用到的模块
import mindspore.dataset as ds
import mindspore.dataset.transforms.c_transforms as C
import mindspore.dataset.vision.c_transforms as CV
from mindspore.dataset.vision import Inter
from mindspore import dtype as mstype
# 数据处理过程
def create_dataset(data_path, batch_size=32, repeat_size=1,
num_parallel_workers=1):
# 定义数据集
mnist_ds = ds.MnistDataset(data_path)
resize_height, resize_width = 32, 32
rescale = 1.0 / 255.0
rescale_nml = 1 / 0.3081
shift_nml = -1 * 0.1307 / 0.3081
# 导入需要用到的模块
import mindspore.dataset as ds
import mindspore.dataset.transforms.c_transforms as C
import mindspore.dataset.vision.c_transforms as CV
from mindspore.dataset.vision import Inter
from mindspore import dtype as mstype
# 数据处理过程
def create_dataset(data_path, batch_size=32, repeat_size=1,
num_parallel_workers=1):
# 定义数据集
mnist_ds = ds.MnistDataset(data_path)
resize_height, resize_width = 32, 32
rescale = 1.0 / 255.0
rescale_nml = 1 / 0.3081
shift_nml = -1 * 0.1307 / 0.3081
# 定义所需要操作的map映射
resize_op = CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR)
rescale_nml_op = CV.Rescale(rescale_nml * rescale, shift_nml)
hwc2chw_op = CV.HWC2CHW()
type_cast_op = C.TypeCast(mstype.int32)
# 使用map映射函数将数据操作应用到数据集
mnist_ds = mnist_ds.map(operations=type_cast_op, input_columns="label", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=[resize_op, rescale_nml_op,hwc2chw_op], input_columns="image",num_parallel_workers=num_parallel_workers)
# 定义所需要操作的map映射
resize_op = CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR)
rescale_nml_op = CV.Rescale(rescale_nml * rescale, shift_nml)
hwc2chw_op = CV.HWC2CHW()
type_cast_op = C.TypeCast(mstype.int32)
# 使用map映射函数将数据操作应用到数据集
mnist_ds = mnist_ds.map(operations=type_cast_op, input_columns="label", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=[resize_op, rescale_nml_op,hwc2chw_op], input_columns="image",num_parallel_workers=num_parallel_workers)
# 进行shuffle、batch操作
buffer_size = 10000
mnist_ds = mnist_ds.shuffle(buffer_size=buffer_size)
mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True)
return mnist_ds
# 进行shuffle、batch操作
buffer_size = 10000
mnist_ds = mnist_ds.shuffle(buffer_size=buffer_size)
mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True)
return mnist_ds
```
### 模型定义
使用MindSpore定义神经网络需要继承mindspore.nn.Cell神经网络的各层需要预先在\_\_init\_\_方法中定义然后重载\_\_construct\_\_方法实现神经网络的前向传播过程。因为输入大小处理成32$\times$`<!-- -->`{=html}32的图片需要用Flatten将数据压平为一维向量后给全连接层全连接层输入大小为32$\times$`<!-- -->`{=html}32预测0$\sim$`<!-- -->`{=html}9中的哪个数字所以最后输出大小为10下面定义了一个三层的全连接层。
使用MindSpore定义神经网络需要继承mindspore.nn.Cell神经网络的各层需要预先在\_\_init\_\_方法中定义然后重载\_\_construct\_\_方法实现神经网络的前向传播过程。
因为输入大小被处理成$32 \times 32$的图片需要用Flatten将数据压平为一维向量后给全连接层全连接层输入大小为$32 \times 32$,预测$0 \sim 9$中的哪个数字所以最后输出大小为10下面定义了一个三层的全连接层。
```python
# 导入需要用到的模块
import mindspore.nn as nn
# 定义线性模型
class MLPNet(nn.Module):
def __init__(self):
super(MLPNet, self).__init__()
self.flatten = nn.Flatten()
self.dense1 = nn.Dense(32*32, 128)
self.dense2 = nn.Dense(128, 64)
self.dense3 = nn.Dense(64, 10)
# 导入需要用到的模块
import mindspore.nn as nn
# 定义线性模型
class MLPNet(nn.Module):
def __init__(self):
super(MLPNet, self).__init__()
self.flatten = nn.Flatten()
self.dense1 = nn.Dense(32*32, 128)
self.dense2 = nn.Dense(128, 64)
self.dense3 = nn.Dense(64, 10)
def construct(self, inputs):
x = self.flatten(inputs)
x = self.dense1(x)
x = self.dense2(x)
logits = self.dense3(x)
return logits
# 实例化网络
net = MLPNet()
def construct(self, inputs):
x = self.flatten(inputs)
x = self.dense1(x)
x = self.dense2(x)
logits = self.dense3(x)
return logits
# 实例化网络
net = MLPNet()
```
### 损失函数和优化器
有了神经网络组件构建的模型我们还需要定义**损失函数**来计算训练过程中输出和真实值的误差。**均方误差**(Mean
Squared
ErrorMSE)是线性回归中常用的,是计算估算值与真实值差值的平方和的平均数。**平均绝对误差**Mean
Absolute
ErrorMAE是计算估算值与真实值差值的绝对值求和再求平均。**交叉熵**Cross
EntropyCE是分类问题中常用的衡量已知数据分布情况下计算输出分布和已知分布的差值
有了神经网络组件构建的模型我们还需要定义**损失函数**来计算训练过程中输出和真实值的误差。**均方误差**(Mean Squared ErrorMSE)是线性回归中常用的,是计算估算值与真实值差值的平方和的平均数。
**平均绝对误差**Mean Absolute ErrorMAE是计算估算值与真实值差值的绝对值求和再求平均。
**交叉熵**Cross EntropyCE是分类问题中常用的衡量已知数据分布情况下计算输出分布和已知分布的差值
有了损失函数,我们就可以通过损失值利用**优化器**对参数进行训练更新。对于优化的目标函数$f(x)$;先求解其梯度$\nabla$$f(x)$,然后将训练参数$W$沿着梯度的负方向更新,更新公式为:$W_t = W_{t-1} - \alpha\nabla(W_{t-1})$,其中$\alpha$是学习率,$W$是训练参数,$\alpha\nabla(W_{t-1})$是方向。神经网络的优化器种类很多一类是学习率不受梯度影响的随机梯度下降Stochastic
Gradient
Descent及SGD的一些改进方法如带有Momentum的SGD另一类是自适应学习率如AdaGrad、RMSProp、Adam等。
有了损失函数,我们就可以通过损失值利用**优化器**对参数进行训练更新。对于优化的目标函数$f(x)$;先求解其梯度$\nabla$$f(x)$,然后将训练参数$W$沿着梯度的负方向更新,更新公式为:$W_t = W_{t-1} - \alpha\nabla(W_{t-1})$,其中$\alpha$是学习率,$W$是训练参数,$\alpha\nabla(W_{t-1})$是方向。
神经网络的优化器种类很多一类是学习率不受梯度影响的随机梯度下降Stochastic Gradient Descent及SGD的一些改进方法如带有Momentum的SGD另一类是自适应学习率如AdaGrad、RMSProp、Adam等。
**SGD**的更新是对每个样本进行梯度下降因此计算速度很快但是单样本更新频繁会造成震荡为了解决震荡问题提出了带有Momentum的SGD该方法的参数更新不仅仅由梯度决定也和累计的梯度下降方向有关使得增加更新梯度下降方向不变的维度减少更新梯度下降方向改变的维度从而速度更快也减少震荡。
自适应学习率**AdaGrad**是通过以往的梯度自适应更新学习率不同的参数$W_i$具有不同的学习率。AdaGrad对频繁变化的参数以更小的步长更新而稀疏的参数以更大的步长更新。因此对稀疏的数据表现比较好。**Adadelta**是对AdaGrad的改进解决了AdaGrad优化过程中学习率$\alpha$单调减少问题Adadelta不对过去的梯度平方进行累加用指数平均的方法计算二阶动量避免了二阶动量持续累积导致训练提前结束。**Adam**可以理解为Adadelta和Momentum的结合对一阶二阶动量均采用指数平均的方法计算。
MindSpore提供了丰富的API来让用户导入损失函数和优化器。在下面的例子中计算了输入和真实值之间的softmax交叉熵损失导入Momentum优化器。
# 定义损失函数
net_loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
# 定义优化器
net_opt = nn.Momentum(net.trainable_params(), learning_rate=0.01, momentum=0.9)
```python
# 定义损失函数
net_loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
# 定义优化器
net_opt = nn.Momentum(net.trainable_params(), learning_rate=0.01, momentum=0.9)
```
### 训练及保存模型
MindSpore提供了回调Callback机制可以在训练过程中执行自定义逻辑使用框架提供的ModelCheckpoint为例。ModelCheckpoint可以保存网络模型和参数以便进行后续的Fine-tuning微调操作。
# 导入模型保存模块
from mindspore.train.callback import ModelCheckpoint, CheckpointConfig
# 设置模型保存参数
config_ck = CheckpointConfig(save_checkpoint_steps=1875, keep_checkpoint_max=10)
# 应用模型保存参数
ckpoint = ModelCheckpoint(prefix="checkpoint_lenet", config=config_ck)
```python
# 导入模型保存模块
from mindspore.train.callback import ModelCheckpoint, CheckpointConfig
# 设置模型保存参数
config_ck = CheckpointConfig(save_checkpoint_steps=1875, keep_checkpoint_max=10)
# 应用模型保存参数
ckpoint = ModelCheckpoint(prefix="checkpoint_lenet", config=config_ck)
```
通过MindSpore提供的model.train接口可以方便地进行网络的训练LossMonitor可以监控训练过程中loss值的变化。
```python
# 导入模型训练需要的库
from mindspore.nn import Accuracy
from mindspore.train.callback import LossMonitor
from mindspore import Model
# 导入模型训练需要的库
from mindspore.nn import Accuracy
from mindspore.train.callback import LossMonitor
from mindspore import Model
def train_net(args, model, epoch_size, data_path, repeat_size, ckpoint_cb, sink_mode):
"""定义训练的方法"""
# 加载训练数据集
ds_train = create_dataset(os.path.join(data_path, "train"), 32, repeat_size)
model.train(epoch_size, ds_train, callbacks=[ckpoint_cb, LossMonitor(125)], dataset_sink_mode=sink_mode)
def train_net(args, model, epoch_size, data_path, repeat_size, ckpoint_cb, sink_mode):
"""定义训练的方法"""
# 加载训练数据集
ds_train = create_dataset(os.path.join(data_path, "train"), 32, repeat_size)
model.train(epoch_size, ds_train, callbacks=[ckpoint_cb, LossMonitor(125)], dataset_sink_mode=sink_mode)
```
其中dataset_sink_mode用于控制数据是否下沉数据下沉是指数据通过通道直接传送到Device上可以加快训练速度dataset_sink_mode为True表示数据下沉否则为非下沉。
有了数据集、模型、损失函数、优化器后就可以进行训练了这里把train_epoch设置为1对数据集进行1个迭代的训练。在train_net和
test_net方法中我们加载了之前下载的训练数据集mnist_path是MNIST数据集路径。
train_epoch = 1
mnist_path = "./datasets/MNIST_Data"
dataset_size = 1
model = Model(net, net_loss, net_opt, metrics={"Accuracy": Accuracy()})
train_net(args, model, train_epoch, mnist_path, dataset_size, ckpoint, False)
```python
train_epoch = 1
mnist_path = "./datasets/MNIST_Data"
dataset_size = 1
model = Model(net, net_loss, net_opt, metrics={"Accuracy": Accuracy()})
train_net(args, model, train_epoch, mnist_path, dataset_size, ckpoint, False)
```
### 测试和验证
测试是模型运行测试数据集得到的结果通常在训练过程中每训练一定的数据量后就会测试一次以验证模型的泛化能力。MindSpore使用model.eval接口读入测试数据集。
def test_net(network, model, data_path):
"""定义验证的方法"""
ds_eval = create_dataset(os.path.join(data_path, "test"))
acc = model.eval(ds_eval, dataset_sink_mode=False)
print("{}".format(acc))
```python
def test_net(network, model, data_path):
"""定义验证的方法"""
ds_eval = create_dataset(os.path.join(data_path, "test"))
acc = model.eval(ds_eval, dataset_sink_mode=False)
print("{}".format(acc))
```
在训练完毕后参数保存在checkpoint中可以将训练好的参数加载到模型中进行验证。
```python
from mindspore import load_checkpoint, load_param_into_net
# 加载已经保存的用于测试的模型
param_dict = load_checkpoint("checkpoint_lenet-1_1875.ckpt")
# 加载参数到网络中
load_param_into_net(net, param_dict)
# 使用函数model.predict预测image对应分类
output = model.predict(Tensor(data['image']))
```
from mindspore import load_checkpoint, load_param_into_net
# 加载已经保存的用于测试的模型
param_dict = load_checkpoint("checkpoint_lenet-1_1875.ckpt")
# 加载参数到网络中
load_param_into_net(net, param_dict)
# 使用函数model.predict预测image对应分类
output = model.predict(Tensor(data['image']))
## 定义深度神经网络
@@ -238,94 +243,120 @@ test_net方法中我们加载了之前下载的训练数据集mnist_path
### 以层为核心定义神经网络
神经网络层包含构建机器学习网络结构的基本组件,如计算机视觉领域常用到卷积(Convolution)、池化(Pooling)、全连接(Fully
Connected);自然语言处理常用到循环神经网络(Recurrent Neural
NetworkRNN)为了加速训练防止过拟合通常用到批标准化BatchNorm、Dropout等。
神经网络层包含构建机器学习网络结构的基本组件,如计算机视觉领域常用到卷积(Convolution)、池化(Pooling)、全连接(Fully Connected);自然语言处理常用到循环神经网络(Recurrent Neural NetworkRNN)为了加速训练防止过拟合通常用到批标准化BatchNorm、Dropout等。
**全连接**是将当前层每个节点都和上一层节点一一连接,本质上是特征空间的线性变换;可以将数据从高维映射到低维,也能从低维映射到高维度。图[1.3](#fig:ch03/fc_layer_1){reference-type="ref"
reference="fig:ch03/fc_layer_1"}展示了全连接的过程对输入的n个数据变换到另一个大小为m的特征空间再从大小为m的特征空间变换到大小为p的特征空间可见全连接层的参数量巨大两次变换所需的参数大小为n$\times$m和m$\times$p
**全连接**是将当前层每个节点都和上一层节点一一连接,本质上是特征空间的线性变换;可以将数据从高维映射到低维,也能从低维映射到高维度。
图numref:`fc_layer`展示了全连接的过程对输入的n个数据变换到另一个大小为m的特征空间再从大小为m的特征空间变换到大小为p的特征空间可见全连接层的参数量巨大两次变换所需的参数大小为$n \times m$和$m \times p$。
![全连接层](figs/ch03/fc_layer_1.png){#fig:ch03/fc_layer_1 width="60%"}
![全连接层](../img/ch02/fc_layer_1.png)
:width:`400px`
:label:`fc_layer`
**卷积**操作是卷积神经网络中常用的操作之一卷积相当于对输入进行滑动滤波。根据卷积核Kernel、卷积步长Stride、填充Padding对输入数据从左到右从上到下进行滑动每一次滑动操作是矩阵的乘加运算得到的加权值。如图[1.4](#fig:ch03/conv_component){reference-type="ref"
reference="fig:ch03/conv_component"}卷积操作主要由输入、卷积核、输出组成输出又被称为特征图Feature
Map
**卷积**操作是卷积神经网络中常用的操作之一卷积相当于对输入进行滑动滤波。根据卷积核Kernel、卷积步长Stride、填充Padding对输入数据从左到右从上到下进行滑动每一次滑动操作是矩阵的乘加运算得到的加权值。
如图numref:`conv_comp`卷积操作主要由输入、卷积核、输出组成输出又被称为特征图Feature Map
![卷积操作的组成](figs/ch03/conv_component.pdf){#fig:ch03/conv_component}
![卷积操作的组成](../img/ch02/conv_component.png)
:width:`400px`
:label:`conv_comp`
卷积的具体运算过程我们通过图[1.5](#fig:ch03/single_channel_conv){reference-type="ref"
reference="fig:ch03/single_channel_conv"}进行演示。该图输入为4$\times$`<!-- -->`{=html}4的矩阵卷积核大小为3$\times$`<!-- -->`{=html}3卷积步长为1不填充最终得到的2$\times$`<!-- -->`{=html}2的输出矩阵。计算过程为将3$\times$`<!-- -->`{=html}3的卷积核作用到左上角3$\times$`<!-- -->`{=html}3大小的输入图上;输出为1$\times$`<!-- -->`{=html}1+2$\times$`<!-- -->`{=html}0+2$\times$`<!-- -->`{=html}1+3$\times$`<!-- -->`{=html}0+2$\times$`<!-- -->`{=html}1+3$\times$`<!-- -->`{=html}0+4$\times$`<!-- -->`{=html}1+1$\times$`<!-- -->`{=html}0+3$\times$`<!-- -->`{=html}1=12,同理对卷积核移动1个步长再次执行相同的计算步骤得到第二个输出为11当再次移动将出界时结束从左往右执行从上往下移动1步再进行从左往右移动依次操作直到从上往下再移动也出界时结束整个卷积过程得到输出结果。我们不难发现相比于全连接卷积的优势是参数共享同一个卷积核遍历整个输入图和参数量小卷积核大小即是参数量
卷积的具体运算过程我们通过图numref:`single_conv`进行演示。该图输入为$4 \times 4$的矩阵,卷积核大小为$3 \times 3$卷积步长为1不填充最终得到的$2 \times 2$的输出矩阵。
计算过程为将$3 \times 3$的卷积核作用到左上角$3 \times 3$大小的输入图上;输出为$1 \times 1 + 2 \times 0 + 2 \times 1 + 3 \times 0 + 2 \times 1 + 3 \times 0 + 4 \times 1 + 1 \times 0 + 3 \ times 1 = 12$,
同理对卷积核移动1个步长再次执行相同的计算步骤得到第二个输出为11当再次移动将出界时结束从左往右执行从上往下移动1步再进行从左往右移动依次操作直到从上往下再移动也出界时结束整个卷积过程得到输出结果。我们不难发现相比于全连接卷积的优势是参数共享同一个卷积核遍历整个输入图和参数量小卷积核大小即是参数量
![卷积的具体运算过程](figs/ch03/single_channel_conv.pdf){#fig:ch03/single_channel_conv}
![卷积的具体运算过程](../img/ch02/single_channel_conv.png)
:width:`400px`
:label:`single_conv`
在卷积过程中如果我们需要对输出矩阵大小进行控制那么就需要对步长和填充进行设置。还是上面的输入图如需要得到和输入矩阵大小一样的输出矩阵步长为1时就需要对上下左右均填充一圈全为0的数。
在上述例子中我们介绍了一个输入一个卷积核的卷积操作。通常情况下我们输入的是彩色图片有三个输入这三个输入称为通道Channel分别代表红、绿、蓝RGB。此时我们执行卷积则为多通道卷积需要三个卷积核分别对RGB三个通道进行上述卷积过程之后将结果加起来。具体如图[1.6](#fig:ch03/channels_conv){reference-type="ref"
reference="fig:ch03/channels_conv"}描述了一个输入通道为3输出通道为1卷积核大小为3$\times$`<!-- -->`{=html}3卷积步长为1的多通道卷积过程需要注意的是每个通道都有各自的卷积核同一个通道的卷积核参数共享。如果输出通道为$out_c$,输入通道为$in_c$,那么需要$out_c$$\times$$in_c$个卷积核。
在上述例子中我们介绍了一个输入一个卷积核的卷积操作。通常情况下我们输入的是彩色图片有三个输入这三个输入称为通道Channel分别代表红、绿、蓝RGB。此时我们执行卷积则为多通道卷积需要三个卷积核分别对RGB三个通道进行上述卷积过程之后将结果加起来。
具体如图numref:`channels_conv`描述了一个输入通道为3输出通道为1卷积核大小为$3 \times 3$卷积步长为1的多通道卷积过程需要注意的是每个通道都有各自的卷积核同一个通道的卷积核参数共享。如果输出通道为$out_c$,输入通道为$in_c$,那么需要$out_c$$\times$$in_c$个卷积核。
![多通道卷积](figs/ch03/channels_conv.pdf){#fig:ch03/channels_conv
width="90%"}
![多通道卷积](../img/ch02/channels_conv.png)
:width:`400px`
:label:`channels_conv`
**池化**是常见的降维操作有最大池化和平均池化。池化操作和卷积的执行类似通过池化核、步长、填充决定输出最大池化是在池化核区域范围内取最大值平均池化则是在池化核范围内做平均。与卷积不同的是池化核没有训练参数池化层的填充方式也有所不同平均池化填充的是0最大池化填充的是$-inf$。图[1.7](#fig:ch03/pooling){reference-type="ref"
reference="fig:ch03/pooling"}是对4$\times$`<!-- -->`{=html}4的输入进行2$\times$`<!-- -->`{=html}2区域池化步长为2不填充图左边是最大池化的结果右边是平均池化的结果。
**池化**是常见的降维操作有最大池化和平均池化。池化操作和卷积的执行类似通过池化核、步长、填充决定输出最大池化是在池化核区域范围内取最大值平均池化则是在池化核范围内做平均。与卷积不同的是池化核没有训练参数池化层的填充方式也有所不同平均池化填充的是0最大池化填充的是$-inf$。
图numref:`pooling`是对$4 \times 4$的输入进行$2 \times 2$区域池化步长为2不填充图左边是最大池化的结果右边是平均池化的结果。
![池化操作](figs/ch03/pooling.pdf){#fig:ch03/pooling}
![池化操作](../img/ch02/pooling.png)
:width:`400px`
:label:`pooling`
有了卷积、池化、全连接组件就可以构建一个非常简单的卷积神经网络了,图[1.8](#fig:ch03/nn_network){reference-type="ref"
reference="fig:ch03/nn_network"}展示了一个卷积神经网络的模型结构。给定输入3$\times$`<!-- -->`{=html}64$\times$`<!-- -->`{=html}64的彩色图片使用16个3$\times$`<!-- -->`{=html}3大小的卷积核做卷积得到大小为16$\times$`<!-- -->`{=html}64$\times$`<!-- -->`{=html}64再进行池化操作降维得到大小为16$\times$`<!-- -->`{=html}32$\times$`<!-- -->`{=html}32的特征图对特征图再卷积得到大小为32$\times$`<!-- -->`{=html}32$\times$`<!-- -->`{=html}32特征图再进行池化操作得到3$\times$`<!-- -->`{=html}16$\times$`<!-- -->`{=html}16大小的特征图我们需要对特征图做全连接此时需要把特征图平铺成一维向量这部操作称为Flatten压平后特征大小为3$\times$`<!-- -->`{=html}16$\times$`<!-- -->`{=html}16=768之后做一次全连接对大小为768特征变换到大小为128的特征再依次做两次全连接分别得到6410。这里最后的输出结果是依据自己的实际问题而定假设我们的输入是包含0$\sim$`<!-- -->`{=html}9的数字图片做分类那输出对应是10个概率值分别对应0$\sim$`<!-- -->`{=html}9的概率大小。
有了卷积、池化、全连接组件就可以构建一个非常简单的卷积神经网络了,图numref:`nn_network`展示了一个卷积神经网络的模型结构。
给定输入$3 \times 64 \times 64$的彩色图片使用16个$3 \times 3$3大小的卷积核做卷积得到大小为$16 \times 64 \times 64$
再进行池化操作降维,得到大小为$16 \times 32 \times 32$的特征图;
对特征图再卷积得到大小为$32 \times 32 \times 32$特征图,再进行池化操作得到$3 \times 16 \times 16$大小的特征图;
我们需要对特征图做全连接此时需要把特征图平铺成一维向量这部操作称为Flatten压平后输入特征大小为$3\times 16 \times 16 = 768$
之后做一次全连接对大小为768特征变换到大小为128的特征再依次做两次全连接分别得到6410。
这里最后的输出结果是依据自己的实际问题而定,假设我们的输入是包含$0 \sim 9$的数字图片做分类那输出对应是10个概率值分别对应$0 \sim 9$的概率大小。
![卷积神经网络模型](figs/ch03/nn_network.pdf){#fig:ch03/nn_network}
![卷积神经网络模型](../img/ch02/nn_network.png)
:width:`400px`
:label:`nn_network`
有了上述基础知识,我们对卷积神经网络所需组件接口和模型构建使用伪代码描述如下:
```python
# 构建卷积神经网络的组件接口定义:
全连接层接口fully_connected(input, weights)
卷积层的接口convolution(input, filters, stride, padding)
最大池化接口pooling(input, pool_size, stride, padding, mode='max')
平均池化接口pooling(input, pool_size, stride, padding, mode='mean')
# 构建卷积神经网络的组件接口定义:
全连接层接口fully_connected(input, weights)
卷积层的接口convolution(input, filters, stride, padding)
最大池化接口pooling(input, pool_size, stride, padding, mode='max')
平均池化接口pooling(input, pool_size, stride, padding, mode='mean')
# 构建卷积神经网络描述:
input:(3,64,64)大小的图片
# 创建卷积模型的训练变量,使用随机数初始化变量值
conv1_filters = variable(random(size=(3, 3, 3, 16)))
conv2_filters = variable(random(size=(3, 3, 16, 32)))
fc1_weights = variable(random(size=(768, 128)))
fc2_weights = variable(random(size=(128, 64)))
fc3_weights = variable(random(size=(64, 10)))
# 将所有需要训练的参数收集起来
all_weights = [conv1_filters, conv2_filters, fc1_weights, fc2_weights, fc3_weights]
# 构建卷积神经网络描述:
input:(3,64,64)大小的图片
# 创建卷积模型的训练变量,使用随机数初始化变量值
conv1_filters = variable(random(size=(3, 3, 3, 16)))
conv2_filters = variable(random(size=(3, 3, 16, 32)))
fc1_weights = variable(random(size=(768, 128)))
fc2_weights = variable(random(size=(128, 64)))
fc3_weights = variable(random(size=(64, 10)))
# 将所有需要训练的参数收集起来
all_weights = [conv1_filters, conv2_filters, fc1_weights, fc2_weights, fc3_weights]
# 构建卷积模型的连接过程
output = convolution(input, conv1_filters, stride=1, padding=0)
output = pooling(output, kernel_size=3, stride=1, padding=0, mode='max')
output = convolution(output, conv2_filters, stride=1, padding=0)
output = pooling(output, kernel_size=3, stride=1, padding=0, mode='max')
output=flatten(output)
output = fully_connected(output, fc1_weights)
output = fully_connected(output, fc2_weights)
output = fully_connected(output, fc3_weights)
```
# 构建卷积模型的连接过程
output = convolution(input, conv1_filters, stride=1, padding=0)
output = pooling(output, kernel_size=3, stride=1, padding=0, mode='max')
output = convolution(output, conv2_filters, stride=1, padding=0)
output = pooling(output, kernel_size=3, stride=1, padding=0, mode='max')
output=flatten(output)
output = fully_connected(output, fc1_weights)
output = fully_connected(output, fc2_weights)
output = fully_connected(output, fc3_weights)
随着深度神经网络应用领域的扩大诞生出了丰富的模型构建组件。在卷积神经网络的计算过程中前后的输入是没有联系的然而在很多任务中往往需要处理序列信息如语句、语音、视频等为了解决此类问题诞生出循环神经网络Recurrent
Neural
NetworkRNN循环神经网络很好的解决了序列数据的问题但是随着序列的增加长序列又导致了训练过程中梯度消失和梯度爆炸的问题因此有了长短期记忆Long
Short-term
MemoryLSTM在语言任务中还有Seq2Seq它将RNN当成编解码Encoder-Decoder结构的编码器Encoder和解码器Decode在解码器中又常常使用注意力机制Attention;基于编解码器和注意力机制又有TransformerTransformer又是BERT模型架构的重要组成。随着深度神经网络的发展未来也会诞生各类模型架构架构的创新可以通过各类神经网络基本组件的组合来实现。
随着深度神经网络应用领域的扩大诞生出了丰富的模型构建组件。在卷积神经网络的计算过程中前后的输入是没有联系的然而在很多任务中往往需要处理序列信息如语句、语音、视频等为了解决此类问题诞生出循环神经网络Recurrent Neural NetworkRNN
循环神经网络很好的解决了序列数据的问题但是随着序列的增加长序列又导致了训练过程中梯度消失和梯度爆炸的问题因此有了长短期记忆Long Short-term MemoryLSTM
在语言任务中还有Seq2Seq它将RNN当成编解码Encoder-Decoder结构的编码器Encoder和解码器Decode
在解码器中又常常使用注意力机制Attention;基于编解码器和注意力机制又有Transformer
Transformer又是BERT模型架构的重要组成。随着深度神经网络的发展未来也会诞生各类模型架构架构的创新可以通过各类神经网络基本组件的组合来实现。
### 神经网络层的实现原理
2.3.1中使用伪代码定义了一些卷积神经网络接口和模型构建过程,整个构建过程,需要创建训练变量和构建连接过程;随着网络层数的增加手动管理训练变量是一个繁琐的过程因此2.3.1中描述的接口在机器学习库中属于低级API。机器学习编程库大都提供了更高级用户友好的API它将神经网络层抽象成一个基类所有的神经网络层实现都继承基类调用低级API。如MindSpore提供的mindspore.nn.Cell、mindspore.nn.Conv2d、mindspore.datasetPyTorch提供的torch.nn.Module、torch.nn.Conv2d、torch.utils.data.Datset。
2.3.1中使用伪代码定义了一些卷积神经网络接口和模型构建过程,整个构建过程,需要创建训练变量和构建连接过程;
随着网络层数的增加手动管理训练变量是一个繁琐的过程因此2.3.1中描述的接口在机器学习库中属于低级API。
机器学习编程库大都提供了更高级用户友好的API它将神经网络层抽象成一个基类所有的神经网络层实现都继承基类调用低级API。
如MindSpore提供的mindspore.nn.Cell、mindspore.nn.Conv2d、mindspore.dataset
PyTorch提供的torch.nn.Module、torch.nn.Conv2d、torch.utils.data.Datset。
[1.9](#fig:ch03/model_build){reference-type="ref"
reference="fig:ch03/model_build"}描述了神经网络构建过程中的基本细节。神经网络层需要的功能有该层的训练参数变量包括初始化方法和训练状态以及计算过程神经网络模型需要的功能是对神经网络层管理和神经网络层参数的管理。在机器学习编程库中承担此功能有MindSpore的Cell、PyTorch的Module。Cell和Module是模型抽象方法也是所有网络的基类。现有模型抽象方案有两种。一种是抽象出两个方法分别为Layer负责单个神经网络层的参数构建和前向计算Model负责对神经网络层进行连接组合和神经网络层参数管理另一种是将Layer和Modle抽象成一个方法该方法既能表示单层神经网络层也能表示包含多个神经网络层堆叠的模型Cell和Module就是这样实现的。
numref:`model_build`描述了神经网络构建过程中的基本细节。
神经网络层需要的功能有该层的训练参数(变量,包括初始化方法和训练状态)以及计算过程;
神经网络模型需要的功能是对神经网络层管理和神经网络层参数的管理。
在机器学习编程库中承担此功能有MindSpore的Cell、PyTorch的Module。
Cell和Module是模型抽象方法也是所有网络的基类。
现有模型抽象方案有两种。
一种是抽象出两个方法分别为Layer负责单个神经网络层的参数构建和前向计算Model负责对神经网络层进行连接组合和神经网络层参数管理
另一种是将Layer和Modle抽象成一个方法该方法既能表示单层神经网络层也能表示包含多个神经网络层堆叠的模型Cell和Module就是这样实现的。
![神经网络模型构建细节](figs/ch03/model_build.png){#fig:ch03/model_build
width="90%"}
![神经网络模型构建细节](../img/ch02/model_build.png)
:width:`400px`
:label:`model_build`
[1.10](#fig:ch03/cell_abstract){reference-type="ref"
reference="fig:ch03/cell_abstract"}展示了设计神经网络层抽象方法的通用表示。通常在构造器会选择使用Python中collections模块的OrderedDict来初始化神经网络层和神经网络层参数的存储它的输出是一个有序的相比与Dict更适合深度学习这种模型堆叠的模式。参数和神经网络层的管理是在\_\_setattr\_\_中实现的当检测到属性是属于神经网络层及神经网络层参数时就记录起来。神经网络模型比较重要的是计算连接过程可以在\_\_call\_\_里重载实现神经网络层时在这里定义计算过程。训练参数的返回接口是为了给优化器传所有训练参数。神经网络层返回为了遍历各层神经网络得到各个神经网络层的参数。这里只列出了一些重要的方法在自定义方法中通常需要实现参数插入删除方法、神经网络层插入删除、神经网络模型信息等。
numref:`cell_abs`展示了设计神经网络层抽象方法的通用表示。通常在构造器会选择使用Python中collections模块的OrderedDict来初始化神经网络层和神经网络层参数的存储它的输出是一个有序的相比与Dict更适合深度学习这种模型堆叠的模式。参数和神经网络层的管理是在\_\_setattr\_\_中实现的当检测到属性是属于神经网络层及神经网络层参数时就记录起来。神经网络模型比较重要的是计算连接过程可以在\_\_call\_\_里重载实现神经网络层时在这里定义计算过程。训练参数的返回接口是为了给优化器传所有训练参数。神经网络层返回为了遍历各层神经网络得到各个神经网络层的参数。这里只列出了一些重要的方法在自定义方法中通常需要实现参数插入删除方法、神经网络层插入删除、神经网络模型信息等。
![神经网络基类抽象方法](figs/ch03/cell_abstract.png){#fig:ch03/cell_abstract
width="90%"}
![神经网络基类抽象方法](../img/ch02/cell_abstract.png)
:width:`400px`
:label:`cell_abs`
神经网络接口层基类实现,仅做了简化的描述,在实际实现时,执行计算的\_\_call\_\_方法并不会让用户直接重载它往往在\_\_call\_\_之外定义一个执行操作的方法对于神经网络模型该方法是实现网络结构的连接对于神经网络层则是实现计算过程后然后在\_\_call\_\_调用如MindSpore的Cell因为动态图和静态图的执行是不一样的因此在\_\_call\_\_里定义动态图和计算图的计算执行在construct方法里定义层或者模型的操作过程。
@@ -333,65 +364,70 @@ width="90%"}
2.3.1中使用伪代码定义机器学习库中低级API有了实现的神经网络基类抽象方法那么就可以设计更高层次的接口解决手动管理参数的繁琐。假设已经有了神经网络模型抽象方法Cell构建Conv2D将继承Cell并重构\_\_init\_\_和\_\_call\_\_方法在\_\_init\_\_里初始化训练参数和输入参数在\_\_call\_\_里调用低级API实现计算逻辑。同样使用伪代码接口描述自定义卷积层的过程。
# 接口定义:
全连接层接口convolution(input, filters, stride, padding)
变量Variable(value, trainable=True)
高斯分布初始化方法random_normal(shape)
神经网络模型抽象方法Cell
```python
# 接口定义:
全连接层接口convolution(input, filters, stride, padding)
变量Variable(value, trainable=True)
高斯分布初始化方法random_normal(shape)
神经网络模型抽象方法Cell
# 定义卷积层
class Conv2D(Cell):
def __init__(self, in_channels, out_channels, ksize, stride, padding):
# 卷积核大小为 ksize x ksize x inchannels x out_channels
filters_shape = (out_channels, in_channels, ksize, ksize)
self.stride = stride
self.padding = padding
self.filters = Variable(random_normal(filters_shape))
# 定义卷积层
class Conv2D(Cell):
def __init__(self, in_channels, out_channels, ksize, stride, padding):
# 卷积核大小为 ksize x ksize x inchannels x out_channels
filters_shape = (out_channels, in_channels, ksize, ksize)
self.stride = stride
self.padding = padding
self.filters = Variable(random_normal(filters_shape))
def __call__(self, inputs):
outputs = convolution(inputs, self.filters, self.stride, self.padding)
def __call__(self, inputs):
outputs = convolution(inputs, self.filters, self.stride, self.padding)
```
有了上述定义在使用卷积层时,就不需要创建训练变量了。如我们需要对30$\times$`<!-- -->`{=html}30大小10个通道的输入使用3$\times$`<!-- -->`{=html}3的卷积核做卷积卷积后输出通道为20调用方式如下
conv = Conv2D(in_channel=10, out_channel=20, filter_size=3, stride=2, padding=0)
output = conv(input)
有了上述定义在使用卷积层时,就不需要创建训练变量了。
如我们需要对$30 \times 30$大小10个通道的输入使用$3 \times 3$的卷积核做卷积卷积后输出通道为20调用方式如下
```python
conv = Conv2D(in_channel=10, out_channel=20, filter_size=3, stride=2, padding=0)
output = conv(input)
```
其执行过程为在初始化Conv2D时\_\_setattr\_\_会判断属性属于Cell把神经网络层Conv2D记录到self.\_cellsfilters属于parameter把参数记录到self.\_params。查看神经网络层参数使用conv.parameters_and_names查看神经网络层列表使用conv.cells_and_names执行操作使用conv(input)。
### 自定义神经网络模型
神经网络层是Cell的子类SubClass实现同样的神经网络模型也可以采用SubClass的方法自定义神经网络模型构建时需要在\_\_init\_\_里将要使用的神经网络组件实例化在\_\_call\_\_里定义神经网络的计算逻辑。同样的以2.3.1的卷积神经网络模型为例,定义接口和伪代码描述如下:
```python
# 使用Cell子类构建的神经网络层接口定义
# 构建卷积神经网络的组件接口定义:
全连接层接口Dense(in_channel, out_channel)
卷积层的接口Conv2D(in_channel, out_channel, filter_size, stride, padding)
最大池化接口MaxPool2D(pool_size, stride, padding)
张量平铺Flatten()
# 使用Cell子类构建的神经网络层接口定义
# 构建卷积神经网络的组件接口定义:
全连接层接口Dense(in_channel, out_channel)
卷积层的接口:Conv2D(in_channel, out_channel, filter_size, stride, padding)
最大池化接口:MaxPool2D(pool_size, stride, padding)
张量平铺Flatten()
# 使用SubClass方式构建卷积模型
class CNN(Cell):
def __init__(self):
self.conv1 = Conv2D(in_channel=3, out_channel=16, filter_size=3, stride=1, padding=0)
self.maxpool1 = MaxPool2D(pool_size=3, stride=1, padding=0)
self.conv2 = Conv2D(in_channel=16, out_channel=32, filter_size=3, stride=1, padding=0)
self.maxpool2 = MaxPool2D(pool_size=3, stride=1, padding=0)
self.flatten = Flatten()
self.dense1 = Dense(in_channels=768, out_channel=128)
self.dense2 = Dense(in_channels=128, out_channel=64)
self.dense3 = Dense(in_channels=64, out_channel=10)
def __call__(self, inputs):
z = self.conv1(inputs)
z = self.maxpool1(z)
z = self.conv2(z)
z = self.maxpool2(z)
z = self.flatten(z)
z = self.dense1(z)
z = self.dense2(z)
z = self.dense3(z)
return z
# 使用SubClass方式构建卷积模型
class CNN(Cell):
def __init__(self):
self.conv1 = Conv2D(in_channel=3, out_channel=16, filter_size=3, stride=1, padding=0)
self.maxpool1 = MaxPool2D(pool_size=3, stride=1, padding=0)
self.conv2 = Conv2D(in_channel=16, out_channel=32, filter_size=3, stride=1, padding=0)
self.maxpool2 = MaxPool2D(pool_size=3, stride=1, padding=0)
self.flatten = Flatten()
self.dense1 = Dense(in_channels=768, out_channel=128)
self.dense2 = Dense(in_channels=128, out_channel=64)
self.dense3 = Dense(in_channels=64, out_channel=10)
def __call__(self, inputs):
z = self.conv1(inputs)
z = self.maxpool1(z)
z = self.conv2(z)
z = self.maxpool2(z)
z = self.flatten(z)
z = self.dense1(z)
z = self.dense2(z)
z = self.dense3(z)
return z
```
上述卷积模型进行实例化,其执行将从\_\_init\_\_开始第一个是Conv2DConv2D也是Cell的子类会进入到Conv2D的\_\_init\_\_此时会将第一个Conv2D的卷积参数收集到self.\_params之后回到Conv2D将第一个Conv2D收集到self.\_cells第二个的组件是MaxPool2D因为其没有训练参数因此将MaxPool2D收集到self.\_cells依次类推分别收集第二个卷积参数和卷积层三个全连接层的参数和全连接层。实例化之后可以调用.parameters_and_names来返回训练参数调用用conv.cells_and_names查看神经网络层列表。
## C/C++编程接口
@@ -403,18 +439,16 @@ width="90%"}
由于Python的解释器是由C实现的因此在Python中可以实现对于C和C++函数的调用。现代机器学习框架包括TensorFlowPyTorch和MindSpore主要依赖Pybind11来将底层的大量C和C++函数自动生成对应的Python函数这一过程一般被称为Python绑定
Binding。在Pybind11出现以前将C和C++函数进行Python绑定的手段主要包括
- Python的C-API。这种方式要求在一个C++程序中包含Python.h并使用Python的C-API对Python语言进行操作。使用这套API需要对Python的底层实现有一定了解比如如何管理引用计数等具有较高的使用门槛。
- Python的C-API。这种方式要求在一个C++程序中包含Python.h并使用Python的C-API对Python语言进行操作。使用这套API需要对Python的底层实现有一定了解比如如何管理引用计数等具有较高的使用门槛。
- 简单包装界面产生器Simplified Wrapper and Interface
GeneratorSWIG)。SWIG可以将C和C++代码暴露给Python。SWIG是TensorFlow早期使用的方式。这种方式需要用户便携一个复杂的SWIG接口声明文件并使用SWIG自动生成使用Python
- 简单包装界面产生器Simplified Wrapper and Interface GeneratorSWIG)。SWIG可以将C和C++代码暴露给Python。SWIG是TensorFlow早期使用的方式。这种方式需要用户便携一个复杂的SWIG接口声明文件并使用SWIG自动生成使用Python
C-API的C代码。自动生成的代码可读性很低因此具有很大代码维护开销。
- Python的ctypes模块提供了C语言中的类型以及直接调用动态链接库的能力。缺点是依赖于C的原生的类型对自定义类型支持不好。
- Python的ctypes模块提供了C语言中的类型以及直接调用动态链接库的能力。缺点是依赖于C的原生的类型对自定义类型支持不好。
- CPython是结合了Python和C语言的一种语言可以简单的认为就是给Python加上了静态类型后的语法使用者可以维持大部分的Python语法。CPython编写的函数会被自动转译为C和C++代码因此在CPython中可以插入对于C/C++函数的调用。
- CPython是结合了Python和C语言的一种语言可以简单的认为就是给Python加上了静态类型后的语法使用者可以维持大部分的Python语法。CPython编写的函数会被自动转译为C和C++代码因此在CPython中可以插入对于C/C++函数的调用。
- Boost::Python是一个C++库。它可以将C++函数暴露为Python函数。其原理和Python
C-API类似但是使用方法更简单。然而由于引入了Boost库因此有沉重的第三方依赖。
- Boost::Python是一个C++库。它可以将C++函数暴露为Python函数。其原理和Python C-API类似但是使用方法更简单。然而由于引入了Boost库因此有沉重的第三方依赖。
相对于上述的提供Python绑定的手段Pybind11提供了类似于Boost::Python的简洁性和易用性但是其通过专注支持C++
11并且去除Boost依赖因此成为了轻量级的Python库从而特别适合在一个复杂的C++项目例如本书讨论的机器学习系统中暴露大量的Python函数。
@@ -441,168 +475,160 @@ Binding。在Pybind11出现以前将C和C++函数进行Python绑定的手
算子输出
MindSpore中实现注册TensorAdd代码如下
```python
# mindspore/ops/operations/math_ops.py
class TensorAdd(PrimitiveWithInfer):
"""
Adds two input tensors element-wise.
"""
@prim_attr_register
def __init__(self):
self.init_prim_io_names(inputs=['x1', 'x2'], outputs=['y'])
# mindspore/ops/operations/math_ops.py
class TensorAdd(PrimitiveWithInfer):
"""
Adds two input tensors element-wise.
"""
@prim_attr_register
def __init__(self):
self.init_prim_io_names(inputs=['x1', 'x2'], outputs=['y'])
def infer_shape(self, x1_shape, x2_shape):
validator.check_integer('input dims', len(x1_shape), len(x2_shape), Rel.EQ, self.name)
for i in range(len(x1_shape)):
validator.check_integer('input_shape', x1_shape[i], x2_shape[i], Rel.EQ, self.name)
return x1_shape
def infer_dtype(self, x1_dtype, x2_type):
validator.check_tensor_type_same({'x1_dtype': x1_dtype}, [mstype.float32], self.name)
validator.check_tensor_type_same({'x2_dtype': x2_dtype}, [mstype.float32], self.name)
return x1_dtype
def infer_shape(self, x1_shape, x2_shape):
validator.check_integer('input dims', len(x1_shape), len(x2_shape), Rel.EQ, self.name)
for i in range(len(x1_shape)):
validator.check_integer('input_shape', x1_shape[i], x2_shape[i], Rel.EQ, self.name)
return x1_shape
def infer_dtype(self, x1_dtype, x2_type):
validator.check_tensor_type_same({'x1_dtype': x1_dtype}, [mstype.float32], self.name)
validator.check_tensor_type_same({'x2_dtype': x2_dtype}, [mstype.float32], self.name)
return x1_dtype
```
在mindspore/ops/operations/math_ops.py文件内注册加法算子原语后需要在mindspore/ops/operations/\_\_init\_\_中导出方便python导入模块时候调用。
# mindspore/ops/operations/__init__.py
from .math_ops import (Abs, ACos, ..., TensorAdd)
__all__ = [
'ReverseSequence',
'CropAndResize',
...,
'TensorAdd'
]
```python
# mindspore/ops/operations/__init__.py
from .math_ops import (Abs, ACos, ..., TensorAdd)
__all__ = [
'ReverseSequence',
'CropAndResize',
...,
'TensorAdd'
]
```
**2.GPU算子开发**继承GPUKernel实现加法使用类模板定义TensorAddGpuKernel需要实现以下方法
- Init(): 用于完成GPU
Kernel的初始化通常包括记录算子输入/输出维度完成Launch前的准备工作因此在此记录Tensor元素个数。
- Init(): 用于完成GPU Kernel的初始化通常包括记录算子输入/输出维度完成Launch前的准备工作因此在此记录Tensor元素个数。
- GetInputSizeList():
向框架反馈输入Tensor需要占用的显存字节数返回了输入Tensor需要占用的字节数TensorAdd有两个Input每个Input占用字节数为element_num$\ast$sizeof(T)。
- GetInputSizeList():向框架反馈输入Tensor需要占用的显存字节数返回了输入Tensor需要占用的字节数TensorAdd有两个Input每个Input占用字节数为element_num$\ast$sizeof(T)。
- GetOutputSizeList():
向框架反馈输出Tensor需要占用的显存字节数返回了输出Tensor需要占用的字节数TensorAdd有一个output占用element_num$\ast$sizeof(T)字节。
- GetOutputSizeList():向框架反馈输出Tensor需要占用的显存字节数返回了输出Tensor需要占用的字节数TensorAdd有一个output占用element_num$\ast$sizeof(T)字节。
- GetWorkspaceSizeList():
向框架反馈Workspace字节数Workspace是用于计算过程中存放临时数据的空间由于TensorAdd不需要Workspace因此GetWorkspaceSizeList()返回空的std::vector\<size_t\>。
- GetWorkspaceSizeList():向框架反馈Workspace字节数Workspace是用于计算过程中存放临时数据的空间由于TensorAdd不需要Workspace因此GetWorkspaceSizeList()返回空的std::vector\<size_t\>。
- Launch(): 通常调用CUDA kernel(CUDA kernel是基于Nvidia
GPU的并行计算架构开发的核函数)或者cuDNN接口等方式完成算子在GPU上加速Launch()接收input、output在显存的地址接着调用TensorAdd完成加速。
- Launch(): 通常调用CUDA kernel(CUDA kernel是基于Nvidia GPU的并行计算架构开发的核函数)或者cuDNN接口等方式完成算子在GPU上加速Launch()接收input、output在显存的地址接着调用TensorAdd完成加速。
```python
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.h
```{=html}
<!-- -->
template <typename T>
class TensorAddGpuKernel : public GpuKernel {
public:
TensorAddGpuKernel() : element_num_(1) {}
~TensorAddGpuKernel() override = default;
bool Init(const CNodePtr &kernel_node) override {
auto shape = AnfAlgo::GetPrevNodeOutputInferShape(kernel_node, 0);
for (size_t i = 0; i < shape.size(); i++) {
element_num_ *= shape[i];
}
InitSizeLists();
return true;
}
const std::vector<size_t> &GetInputSizeList() const override { return input_size_list_; }
const std::vector<size_t> &GetOutputSizeList() const override { return output_size_list_; }
const std::vector<size_t> &GetWorkspaceSizeList() const override { return workspace_size_list_; }
bool Launch(const std::vector<AddressPtr> &inputs, const std::vector<AddressPtr> &,
const std::vector<AddressPtr> &outputs, void *stream_ptr) override {
T *x1 = GetDeviceAddress<T>(inputs, 0);
T *x2 = GetDeviceAddress<T>(inputs, 1);
T *y = GetDeviceAddress<T>(outputs, 0);
TensorAdd(element_num_, x1, x2, y, reinterpret_cast<cudaStream_t>(stream_ptr));
return true;
}
protected:
void InitSizeLists() override {
input_size_list_.push_back(element_num_ * sizeof(T));
input_size_list_.push_back(element_num_ * sizeof(T));
output_size_list_.push_back(element_num_ * sizeof(T));
}
private:
size_t element_num_;
std::vector<size_t> input_size_list_;
std::vector<size_t> output_size_list_;
std::vector<size_t> workspace_size_list_;
};
```
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.h
template <typename T>
class TensorAddGpuKernel : public GpuKernel {
public:
TensorAddGpuKernel() : element_num_(1) {}
~TensorAddGpuKernel() override = default;
bool Init(const CNodePtr &kernel_node) override {
auto shape = AnfAlgo::GetPrevNodeOutputInferShape(kernel_node, 0);
for (size_t i = 0; i < shape.size(); i++) {
element_num_ *= shape[i];
}
InitSizeLists();
return true;
}
const std::vector<size_t> &GetInputSizeList() const override { return input_size_list_; }
const std::vector<size_t> &GetOutputSizeList() const override { return output_size_list_; }
const std::vector<size_t> &GetWorkspaceSizeList() const override { return workspace_size_list_; }
bool Launch(const std::vector<AddressPtr> &inputs, const std::vector<AddressPtr> &,
const std::vector<AddressPtr> &outputs, void *stream_ptr) override {
T *x1 = GetDeviceAddress<T>(inputs, 0);
T *x2 = GetDeviceAddress<T>(inputs, 1);
T *y = GetDeviceAddress<T>(outputs, 0);
TensorAdd(element_num_, x1, x2, y, reinterpret_cast<cudaStream_t>(stream_ptr));
return true;
}
protected:
void InitSizeLists() override {
input_size_list_.push_back(element_num_ * sizeof(T));
input_size_list_.push_back(element_num_ * sizeof(T));
output_size_list_.push_back(element_num_ * sizeof(T));
}
private:
size_t element_num_;
std::vector<size_t> input_size_list_;
std::vector<size_t> output_size_list_;
std::vector<size_t> workspace_size_list_;
};
TensorAdd中调用了CUDA
kernelTensorAddKernel来实现element_num个元素的并行相加:
```python
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.h
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.h
template <typename T>
__global__ void TensorAddKernel(const size_t element_num, const T* x1, const T* x2, T* y) {
for (size_t i = blockIdx.x * blockDim.x + threadIdx.x; i < element_num; i += blockDim.x * gridDim.x) {
y[i] = x1[i] + x2[i];
}
}
template <typename T>
__global__ void TensorAddKernel(const size_t element_num, const T* x1, const T* x2, T* y) {
for (size_t i = blockIdx.x * blockDim.x + threadIdx.x; i < element_num; i += blockDim.x * gridDim.x) {
y[i] = x1[i] + x2[i];
}
}
template <typename T>
void TensorAdd(const size_t &element_num, const T* x1, const T* x2, T* y, cudaStream_t stream){
size_t thread_per_block = 256;
size_t block_per_grid = (element_num + thread_per_block - 1 ) / thread_per_block;
TensorAddKernel<<<block_per_grid, thread_per_block, 0, stream>>>(element_num, x1, x2, y);
return;
}
template <typename T>
void TensorAdd(const size_t &element_num, const T* x1, const T* x2, T* y, cudaStream_t stream){
size_t thread_per_block = 256;
size_t block_per_grid = (element_num + thread_per_block - 1 ) / thread_per_block;
TensorAddKernel<<<block_per_grid, thread_per_block, 0, stream>>>(element_num, x1, x2, y);
return;
}
template void TensorAdd(const size_t &element_num, const float* x1, const float* x2, float* y, cudaStream_t stream);
```
template void TensorAdd(const size_t &element_num, const float* x1, const float* x2, float* y, cudaStream_t stream);
**3.GPU算子注册**算子信息包含1.Primive2.Input dtype, output dtype3.GPU Kernel class
4.CUDA内置数据类型。框架会根据Primive和Input dtype, output dtype调用以CUDA内置数据类型实例化GPU Kernel class模板类。如下代码中分别注册了支持float和int的TensorAdd算子。
```python
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.cc
**3.GPU算子注册**算子信息包含1.Primive2.Input dtype, output
dtype3.GPU Kernel class4.CUDA内置数据类型。框架会根据Primive和Input
dtype, output dtype调用以CUDA内置数据类型实例化GPU Kernel
class模板类。如下代码中分别注册了支持float和int的TensorAdd算子。
// mindspore/ccsrc/backend/kernel_compiler/gpu/math/tensor_add_v2_gpu_kernel.cc
MS_REG_GPU_KERNEL_ONE(TensorAddV2, KernelAttr()
.AddInputAttr(kNumberTypeFloat32)
.AddInputAttr(kNumberTypeFloat32)
.AddOutputAttr(kNumberTypeFloat32),
TensorAddV2GpuKernel, float)
MS_REG_GPU_KERNEL_ONE(TensorAddV2, KernelAttr()
.AddInputAttr(kNumberTypeInt32)
.AddInputAttr(kNumberTypeInt32)
.AddOutputAttr(kNumberTypeInt32),
TensorAddV2GpuKernel, int)
MS_REG_GPU_KERNEL_ONE(TensorAddV2, KernelAttr()
.AddInputAttr(kNumberTypeFloat32)
.AddInputAttr(kNumberTypeFloat32)
.AddOutputAttr(kNumberTypeFloat32),
TensorAddV2GpuKernel, float)
MS_REG_GPU_KERNEL_ONE(TensorAddV2, KernelAttr()
.AddInputAttr(kNumberTypeInt32)
.AddInputAttr(kNumberTypeInt32)
.AddOutputAttr(kNumberTypeInt32),
TensorAddV2GpuKernel, int)
```
完成上述三步工作后需要把MindSpore重新编译在源码的根目录执行bash
build.sh -e gpu最后使用算子进行验证。
## 总结
- 现代机器学习系统需要兼有易用性和高性能因此其一般选择Python作为前端编程语言而使用C和C++作为后端编程语言。
- 现代机器学习系统需要兼有易用性和高性能因此其一般选择Python作为前端编程语言而使用C和C++作为后端编程语言。
- 一个机器学习框架需要对一个完整的机器学习应用工作流进行编程支持。这些编程支持一般通过提供高层次Python
API来实现。
- 一个机器学习框架需要对一个完整的机器学习应用工作流进行编程支持。这些编程支持一般通过提供高层次Python API来实现。
- 数据处理编程接口允许用户下载,导入和预处理数据集。
- 数据处理编程接口允许用户下载,导入和预处理数据集。
- 模型定义编程接口允许用户定义和导入机器学习模型。
- 模型定义编程接口允许用户定义和导入机器学习模型。
- 损失函数接口允许用户定义损失函数来评估当前模型性能。同时,优化器接口允许用户定义和导入优化算法来基于损失函数计算梯度。
- 损失函数接口允许用户定义损失函数来评估当前模型性能。同时,优化器接口允许用户定义和导入优化算法来基于损失函数计算梯度。
- 机器学习框架同时兼有高层次Python
API来对训练过程模型测试和调试进行支持。
- 机器学习框架同时兼有高层次Python API来对训练过程模型测试和调试进行支持。
- 复杂的深度神经网络可以通过叠加神经网络层来完成。
- 复杂的深度神经网络可以通过叠加神经网络层来完成。
- 用户可以通过Python
API定义神经网络层并指定神经网络层之间的拓扑来定义深度神经网络。
- 用户可以通过Python API定义神经网络层并指定神经网络层之间的拓扑来定义深度神经网络。
- Python和C之间的互操作性一般通过CType等技术实现。
- Python和C之间的互操作性一般通过CType等技术实现。
- 机器学习框架一般具有多种C和C++接口允许用户定义和注册C++实现的算子。这些算子使得用户可以开发高性能模型,数据处理函数,优化器等一系列框架拓展。
- 机器学习框架一般具有多种C和C++接口允许用户定义和注册C++实现的算子。这些算子使得用户可以开发高性能模型,数据处理函数,优化器等一系列框架拓展。